Onboarding, Sent box and Outbox (#45)

* main

- Set

* Working

* Welcome

- Added welcome message generation

* Smtpsender

- On successful SMTP send create the "Sent" box and then
try move from "Outbox" to "Sent"

* Sent box

- Create the mailbox in `main.go` and not every time we try move from `Outbox` to `Sent`

* Use logegr

* USer

- Added logger pointer (and made use of it)
- Disallow renaming or deletion of 'Sent'

* When creating a new user set it up with logger

* Encoded message

* Added tests

* Send a welcome mail on startup (soon to mke it only happen once)

* try set flags

* Onboarding flag set

* Sender
- Removed testing code

* Welcome

- Moved welcomer code

* Cleaned up

* Added more

* renamed package

* Removed comment

* welcome

- FIxed variable names

* welcome

- Removed semi-colons
- Fixed imports

* welcome

- Ran `gofmt`

* welcome test

- Fixed up

* h

* main

- Ran `gofmt`

* Main

- Fxied

* Welcome

- Foxed name

* Added `.gitignore`

* Mailbox

- Disabled print logging

* Fixed

* fixedg

* fixe and use `%v`
This commit is contained in:
Tristan B. Velloza Kildaire
2025-12-20 16:30:37 +02:00
committed by GitHub
parent fa32249f2f
commit 8bf3ba5f47
7 changed files with 162 additions and 8 deletions

1
cmd/yggmail/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
yggmail

View File

@@ -32,6 +32,7 @@ import (
"github.com/neilalexander/yggmail/internal/storage/sqlite3"
"github.com/neilalexander/yggmail/internal/transport"
"github.com/neilalexander/yggmail/internal/utils"
"github.com/neilalexander/yggmail/internal/welcome"
"golang.org/x/crypto/bcrypt"
)
@@ -102,14 +103,18 @@ func main() {
copy(sk, skBytes)
}
pk := sk.Public().(ed25519.PublicKey)
log.Printf("Mail address: %s@%s\n", hex.EncodeToString(pk), utils.Domain)
mailAddrUser := hex.EncodeToString(pk)
mailAddr := fmt.Sprintf("%s@%s", mailAddrUser, utils.Domain)
log.Printf("Mail address: %s\n", mailAddr)
for _, name := range []string{"INBOX", "Outbox"} {
for _, name := range []string{"INBOX", "Outbox", "Sent"} {
if err := storage.MailboxCreate(name); err != nil {
panic(err)
}
}
welcome.Onboard(mailAddrUser, storage, log)
switch {
case password != nil && *password:
log.Println("Please enter your new password:")
@@ -144,7 +149,6 @@ func main() {
log.Println("Password for IMAP and SMTP has been updated!")
os.Exit(0)
case passwordhash != nil && *passwordhash != "":
var hash = strings.TrimSpace(*passwordhash)
if len(hash) == 0 {

View File

@@ -48,6 +48,7 @@ func (b *Backend) Login(conn *imap.ConnInfo, username, password string) (backend
backend: b,
username: username,
conn: conn,
log: b.Log,
}
return user, nil
}

View File

@@ -12,6 +12,7 @@ import (
"encoding/hex"
"errors"
"fmt"
"log"
"github.com/emersion/go-imap"
"github.com/emersion/go-imap/backend"
@@ -21,6 +22,7 @@ type User struct {
backend *Backend
username string
conn *imap.ConnInfo
log *log.Logger
}
func (u *User) Username() string {
@@ -64,21 +66,35 @@ func (u *User) GetMailbox(name string) (mailbox backend.Mailbox, err error) {
}
func (u *User) CreateMailbox(name string) error {
return u.backend.Storage.MailboxCreate(name)
u.log.Printf("Creating mailbox '%s'...\n", name)
if e := u.backend.Storage.MailboxCreate(name); e != nil {
u.log.Printf("Error creating mailbox '%s': %v\n", name, e);
return e;
}
u.log.Printf("Created mailbox '%s'\n", name);
return nil;
}
func (u *User) DeleteMailbox(name string) error {
switch name {
case "INBOX", "Outbox":
case "INBOX", "Outbox", "Sent":
return errors.New("Cannot delete " + name)
default:
return u.backend.Storage.MailboxDelete(name)
if e := u.backend.Storage.MailboxDelete(name); e != nil {
u.log.Printf("Error deleting mailbox '%s': %v\n", name, e)
return e;
} else {
u.log.Printf("Deleted mailbox '%s'\n", name)
return e;
}
}
}
func (u *User) RenameMailbox(existingName, newName string) error {
switch existingName {
case "INBOX", "Outbox":
case "INBOX", "Outbox", "Sent":
return errors.New("Cannot rename " + existingName)
default:
return u.backend.Storage.MailboxRename(existingName, newName)

View File

@@ -58,6 +58,7 @@ func (qs *Queues) manager() {
func (qs *Queues) QueueFor(from string, rcpts []string, content []byte) error {
pid, err := qs.Storage.MailCreate("Outbox", content)
if err != nil {
return fmt.Errorf("q.queues.Storage.MailCreate: %w", err)
}
@@ -176,7 +177,8 @@ func (q *Queue) run() {
if remaining, err := q.queues.Storage.QueueSelectIsMessagePendingSend("Outbox", ref.ID); err != nil {
return fmt.Errorf("q.queues.Storage.QueueSelectIsMessagePendingSend: %w", err)
} else if !remaining {
return q.queues.Storage.MailDelete("Outbox", ref.ID)
q.queues.Log.Printf("Moving mail with id '%d' from Outbox to Sent\n", ref.ID)
return q.queues.Storage.MailMove("Outbox", ref.ID, "Sent")
}
return nil

109
internal/welcome/welcome.go Normal file
View File

@@ -0,0 +1,109 @@
package welcome
import (
"bytes"
"fmt"
"github.com/emersion/go-message"
"github.com/neilalexander/yggmail/internal/storage"
"log"
)
const (
WEBSITE_URL = "https://github.com/neilalexander/yggmail"
CODE_URL = "https://github.com/neilalexander/yggmail"
)
func Onboard(user string, storage storage.Storage, log *log.Logger) {
// Fetch onboarding status
if f, e := storage.ConfigGet("onboarding_done"); e == nil {
// If we haven't onboarded yet
if len(f) == 0 {
log.Printf("Performing onboarding...\n")
// takes in addr and output writer
welcomeMsg, e := welcomeMessageFor(user)
if e != nil {
log.Println("Failure to generate welcome message")
}
var welcomeId int
if id, e := storage.MailCreate("INBOX", welcomeMsg); e != nil {
log.Printf("Failed to store welcome message: %v\n", e)
panic("See above")
} else {
welcomeId = id
}
if storage.MailUpdateFlags("INBOX", welcomeId, false, false, false, false) != nil {
panic("Could not set flags on onboarding message")
}
// set flag to never do it again
if storage.ConfigSet("onboarding_done", "true") != nil {
panic("Error storing onboarding flag")
}
log.Printf("Onboarding done\n")
} else {
log.Printf("Onboarding not required\n")
}
} else {
panic("Error fetching onboarding status")
}
}
func welcomeMessageFor(yourYggMailAddr string) ([]byte, error) {
var hdr = welcomeTo(yourYggMailAddr)
var buff = bytes.NewBuffer([]byte{})
// writer writes to underlying writer (our buffer)
// but returns a writer just for the body part
// (it will encode header to underlying writer
// first)
msgWrt, e := message.CreateWriter(buff, hdr)
if e != nil {
return nil, e
}
var formattedBody = fmt.Sprintf(welcomeBody, yourYggMailAddr, WEBSITE_URL, CODE_URL)
if _, e := msgWrt.Write([]byte(formattedBody)); e != nil {
return nil, e
}
// var ent, e = message.New(hdr, body_rdr)
return buff.Bytes(), nil
}
var welcomeSubject = "Welcome to Yggmail!"
var welcomeBody = `
Hey <b>%s</b>!
We'd like to welcome you to Yggmail!
You're about to embark in both a revolution and an
evolution as you know it. The revolution is that this
mailing system uses the new and experimental Yggdrasil
internet routing system, the evolution is that it's
good old email as you know it.
Want to learn more? See the <a href="%s">website</a>
Thinking of contributing; we'd be more than happy
to work together. Our project is hosted on <a href="%s">GitHub</a>.
`
func welcomeTo(yourYggMailAddr string) message.Header {
// header would be a nice preview of what to expect
// of the message
var welcomeHdr = message.Header{}
welcomeHdr.Add("From", "Yggmail Team")
welcomeHdr.Add("To", yourYggMailAddr+"@yggmail")
welcomeHdr.Add("Subject", welcomeSubject)
// FIXME: Add content-type entry here
fmt.Printf("Generated welcome mesg '%v'\n", welcomeHdr)
return welcomeHdr
}

View File

@@ -0,0 +1,21 @@
package welcome
import (
"fmt"
"testing"
)
func Test_WelcomeGenerate(t *testing.T) {
newUser := "Tristan"
// generate welcome message header
bytesOut, e := welcomeMessageFor(newUser)
if e != nil {
t.Fail()
} else if len(bytesOut) == 0 {
t.Fail()
}
fmt.Printf("Out: %v\n", bytesOut)
}