diff --git a/DESIGN.md b/DESIGN.md index 0627116..23aebf4 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -38,3 +38,17 @@ following, though: * `Admin` * `Wildcard` * `PasswordHash` + * `Message` + * `ID` + * `Username` + * `Mailbox` + * `Header` + * `Body` + +I don't seem to be able to get accounts for a list of usernames very easily, or +indexed-ly. + +The message body is being stored inefficiently (json []byte, so base64-encoded) + +Message username and mailbox are indexed, but is that good enough? Perhaps we +should have a bucket per mailbox or something? diff --git a/internal/smtp/mta.go b/internal/smtp/mta.go index 87e86f5..c9ddddb 100644 --- a/internal/smtp/mta.go +++ b/internal/smtp/mta.go @@ -5,6 +5,7 @@ import ( "fmt" "io" "log" + "net/mail" "sync/atomic" "github.com/emersion/go-smtp" @@ -74,6 +75,30 @@ func (m *mta) AnonymousLogin() (smtp.User, error) { // User implementation for go-smtp after this point // Since only anonymous login is permitted, there's no need to introduce an // intermediary to track the logged-in user. +// +// TODO: wildcard address support. For now, just dump everything directly +// into a single hardcoded mailbox per-account func (m *mta) ServeSMTP(from string, to []string, r io.Reader) error { - return fmt.Errorf("Not yet implemented") + emails := make([]string, len(to)) + + for i, entry := range to { + addr, err := mail.ParseAddress(entry) // FIXME: should this be AddressList? + if err != nil { + return err + } + emails[i] = addr.Address + } + + log.Printf("emails: %#v", emails) + + recipients, err := m.store.FindAccounts(emails...) + if err != nil { + return err + } + + if len(recipients) != len(to) { + return fmt.Errorf("At least one recipient is not known locally") + } + + return m.store.SpoolMessage(recipients, r) } diff --git a/internal/store/account.go b/internal/store/account.go index 9a4e3f2..9edf7bb 100644 --- a/internal/store/account.go +++ b/internal/store/account.go @@ -53,9 +53,18 @@ func (c *concrete) FindAccount(username string) (Account, error) { } func (c *concrete) FindAccounts(usernames ...string) ([]Account, error) { - var accounts []Account + accounts := make([]Account, len(usernames)) - return accounts, c.storm.Find("Username", usernames, &accounts) + // FIXME: I don't know how to storm, it turns out + for i, username := range usernames { + account, err := c.FindAccount(username) + if err != nil { + return nil, err + } + accounts[i] = account + } + + return accounts, nil } func (c *concrete) FindAccountWithPassword(username, password string) (Account, error) { diff --git a/internal/store/message.go b/internal/store/message.go new file mode 100644 index 0000000..e0671b4 --- /dev/null +++ b/internal/store/message.go @@ -0,0 +1,22 @@ +package store + +import ( + "net/mail" +) + +type MessageInterface interface { + CreateMessage(Message) error +} + +type Message struct { + ID string + Username string `storm:"index"` // FK accounts.username + Mailbox string `storm:"index"` // The mailbox, e.g. `INBOX` or `Foo/Bar` + + Header mail.Header + Body []byte +} + +func (c *concrete) CreateMessage(message Message) error { + return c.storm.Save(&message) +} diff --git a/internal/store/spool.go b/internal/store/spool.go new file mode 100644 index 0000000..d46f302 --- /dev/null +++ b/internal/store/spool.go @@ -0,0 +1,48 @@ +package store + +import ( + "fmt" + "io" + "io/ioutil" + "net/mail" +) + +type SpoolInterface interface { + SpoolMessage([]Account, io.Reader) error +} + +// FIXME: we don't actually spool for now, we just store it to each user's +// mailbox. This might lead to oddness in the partial delivery case. +func (c *concrete) SpoolMessage(recipients []Account, r io.Reader) error { + message, err := mail.ReadMessage(r) + if err != nil { + return err + } + + // FIXME: we can assign our own ID in this case + mid := message.Header.Get("Message-ID") + if mid == "" { + return fmt.Errorf("No Message-ID for this email") + } + + body, err := ioutil.ReadAll(message.Body) + if err != nil { + return err + } + + for _, recipient := range recipients { + message := Message{ + ID: mid, + Username: recipient.Username, + Mailbox: "INBOX", // FIXME: at some point this will be changeable + Header: message.Header, + Body: body, + } + + if err := c.CreateMessage(message); err != nil { + return err + } + } + + return nil +} diff --git a/internal/store/store.go b/internal/store/store.go index 7e496ed..c1d2ca3 100644 --- a/internal/store/store.go +++ b/internal/store/store.go @@ -19,6 +19,8 @@ type Interface interface { SetTLS([]byte, []byte) error AccountInterface + MessageInterface + SpoolInterface io.Closer }