diff --git a/cmd/yggmail/.gitignore b/cmd/yggmail/.gitignore new file mode 100644 index 0000000..e3c3b17 --- /dev/null +++ b/cmd/yggmail/.gitignore @@ -0,0 +1 @@ +yggmail diff --git a/cmd/yggmail/main.go b/cmd/yggmail/main.go index 304c0cd..f84bcc7 100644 --- a/cmd/yggmail/main.go +++ b/cmd/yggmail/main.go @@ -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 { diff --git a/internal/imapserver/backend.go b/internal/imapserver/backend.go index 7745b3c..c39c386 100644 --- a/internal/imapserver/backend.go +++ b/internal/imapserver/backend.go @@ -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 } diff --git a/internal/imapserver/user.go b/internal/imapserver/user.go index 2a1256f..29b0c77 100644 --- a/internal/imapserver/user.go +++ b/internal/imapserver/user.go @@ -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) diff --git a/internal/smtpsender/sender.go b/internal/smtpsender/sender.go index 40b32f6..e9cf573 100644 --- a/internal/smtpsender/sender.go +++ b/internal/smtpsender/sender.go @@ -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 diff --git a/internal/welcome/welcome.go b/internal/welcome/welcome.go new file mode 100644 index 0000000..c8fef60 --- /dev/null +++ b/internal/welcome/welcome.go @@ -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 %s! + +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 website + +Thinking of contributing; we'd be more than happy +to work together. Our project is hosted on GitHub. +` + +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 +} diff --git a/internal/welcome/welcome_test.go b/internal/welcome/welcome_test.go new file mode 100644 index 0000000..2d7b7cb --- /dev/null +++ b/internal/welcome/welcome_test.go @@ -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) +}