diff --git a/README.md b/README.md index 7b0ed79..dd29d3d 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Yggmail is a single-binary all-in-one mail transfer agent which sends and receiv * All mail exchange traffic between any two Yggmail nodes is always end-to-end encrypted without exception; * Yggdrasil and Yggmail nodes on the same network are discovered automatically using multicast or you can configure a static Yggdrasil peer. -Email addresses are based on your public key, like `neilalexander@1mLp6AtYSE7rYOVDDTPKzasmFgG9BfKOk7aK4xOdZcT.yggmail`. +Email addresses are based on your public key, like `1mLp6AtYSE7rYOVDDTPKzasmFgG9BfKOk7aK4xOdZcT@yggmail`. ## Why? @@ -28,9 +28,9 @@ Use a recent version of Go to install Yggmail: go install github.com/neilalexander/yggmail/cmd/yggmail ``` -Create a mailbox, e.g. for user `alice`. Your Yggmail database will automatically be created in your working directory if it doesn't already exist: +Create a mailbox and set your password. Your Yggmail database will automatically be created in your working directory if it doesn't already exist: ``` -yggmail -createuser=alice +yggmail -password ``` Start Yggmail, using the database in your working directory: @@ -53,7 +53,7 @@ The following command line switches are supported by the `yggmail` binary: * `-database=/path/to/yggmail.db` — use a specific database file; * `-smtp=listenaddr:port` — listen for SMTP on a specific address/port * `-imap=listenaddr:port` — listen for IMAP on a specific address/port; -* `-createuser=username` — create a new user in the database (doesn't matter if Yggmail is running or not, just make sure that Yggmail is pointing at the right database file or that you are in the right working directory). +* `-password` — set your IMAP/SMTP password (doesn't matter if Yggmail is running or not, just make sure that Yggmail is pointing at the right database file or that you are in the right working directory). ## Notes diff --git a/cmd/yggmail/main.go b/cmd/yggmail/main.go index e105b20..4ed716d 100644 --- a/cmd/yggmail/main.go +++ b/cmd/yggmail/main.go @@ -30,7 +30,7 @@ var database = flag.String("database", "yggmail.db", "SQLite database file") var smtpaddr = flag.String("smtp", "localhost:1025", "SMTP listen address") var imapaddr = flag.String("imap", "localhost:1026", "IMAP listen address") var peeraddr = flag.String("peer", "", "Yggdrasil static peer") -var createuser = flag.String("createuser", "", "Create a user") +var password = flag.Bool("password", false, "Set a new IMAP/SMTP password") func main() { flag.Parse() @@ -57,6 +57,9 @@ func main() { if err := storage.ConfigSet("private_key", hex.EncodeToString(sk)); err != nil { panic(err) } + if err := storage.MailboxCreate("INBOX"); err != nil { + panic(err) + } log.Printf("Generated new server identity") } else { skBytes, err := hex.DecodeString(skStr) @@ -66,35 +69,32 @@ func main() { copy(sk, skBytes) } pk := sk.Public().(ed25519.PublicKey) - log.Println("Mail domain:", base62.EncodeToString(pk)) + log.Printf("Mail address: %s@%s\n", base62.EncodeToString(pk), utils.Domain) switch { - case createuser != nil && *createuser != "": - fmt.Printf("New password: ") + case password != nil && *password: + log.Println("Please enter your new password:") password1, err := term.ReadPassword(0) if err != nil { panic(err) } fmt.Println() - fmt.Printf("Confirm password: ") + log.Println("Please enter your new password again:") password2, err := term.ReadPassword(0) if err != nil { panic(err) } fmt.Println() if !bytes.Equal(password1, password2) { - fmt.Println("The supplied passwords do not match") + log.Println("The supplied passwords do not match") os.Exit(1) } - if err := storage.CreateUser(*createuser, strings.TrimSpace(string(password1))); err != nil { - fmt.Printf("Failed to create user %q\n", *createuser) + if err := storage.ConfigSetPassword(strings.TrimSpace(string(password1))); err != nil { + log.Println("Failed to set password:", err) os.Exit(1) } - if err := storage.MailboxCreate(*createuser, "INBOX"); err != nil { - panic(err) - } - fmt.Printf("Created user %q\n", *createuser) - fmt.Printf("Email address will be %s@%s%s\n", *createuser, base62.EncodeToString(pk), utils.TLD) + + log.Println("Password for IMAP and SMTP has been updated!") os.Exit(0) } diff --git a/internal/imapserver/backend.go b/internal/imapserver/backend.go index 458305e..a770e93 100644 --- a/internal/imapserver/backend.go +++ b/internal/imapserver/backend.go @@ -20,14 +20,14 @@ type Backend struct { func (b *Backend) Login(_ *imap.ConnInfo, username, password string) (backend.User, error) { // If our username is email-like, then take just the localpart - if localpart, host, err := utils.ParseAddress(username); err == nil { - if host != base62.EncodeToString(b.Config.PublicKey) { + if pk, err := utils.ParseAddress(username); err == nil { + if !pk.Equal(b.Config.PublicKey) { + b.Log.Println("Failed to authenticate IMAP user due to wrong domain", pk, b.Config.PublicKey) return nil, fmt.Errorf("failed to authenticate: wrong domain in username") } - username = localpart } - - if authed, err := b.Storage.TryAuthenticate(username, password); err != nil { + username = base62.EncodeToString(b.Config.PublicKey) + if authed, err := b.Storage.ConfigTryPassword(password); err != nil { b.Log.Printf("Failed to authenticate IMAP user %q due to error: %s", username, err) return nil, fmt.Errorf("failed to authenticate: %w", err) } else if !authed { diff --git a/internal/imapserver/mailbox.go b/internal/imapserver/mailbox.go index 09c5d21..95c93ea 100644 --- a/internal/imapserver/mailbox.go +++ b/internal/imapserver/mailbox.go @@ -23,7 +23,7 @@ func (mbox *Mailbox) getIDsFromSeqSet(uid bool, seqSet *imap.SeqSet) ([]int32, e var ids []int32 for _, set := range seqSet.Set { if set.Stop == 0 { - next, err := mbox.backend.Storage.MailNextID(mbox.user.username, mbox.name) + next, err := mbox.backend.Storage.MailNextID(mbox.name) if err != nil { return nil, fmt.Errorf("mbox.backend.Storage.MailNextID: %w", err) } @@ -31,7 +31,7 @@ func (mbox *Mailbox) getIDsFromSeqSet(uid bool, seqSet *imap.SeqSet) ([]int32, e } for i := set.Start; i <= set.Stop; i++ { if !uid { - pid, err := mbox.backend.Storage.MailIDForSeq(mbox.user.username, mbox.name, int(i)) + pid, err := mbox.backend.Storage.MailIDForSeq(mbox.name, int(i)) if err != nil { return nil, fmt.Errorf("mbox.backend.Storage.MailIDForSeq: %w", err) } @@ -67,14 +67,14 @@ func (mbox *Mailbox) Status(items []imap.StatusItem) (*imap.MailboxStatus, error for _, name := range items { switch name { case imap.StatusMessages: - count, err := mbox.backend.Storage.MailCount(mbox.user.username, mbox.name) + count, err := mbox.backend.Storage.MailCount(mbox.name) if err != nil { return nil, fmt.Errorf("mbox.backend.Storage.MailCount: %w", err) } status.Messages = uint32(count) case imap.StatusUidNext: - id, err := mbox.backend.Storage.MailNextID(mbox.user.username, mbox.name) + id, err := mbox.backend.Storage.MailNextID(mbox.name) if err != nil { return nil, fmt.Errorf("mbox.backend.Storage.MailNextID: %w", err) } @@ -87,7 +87,7 @@ func (mbox *Mailbox) Status(items []imap.StatusItem) (*imap.MailboxStatus, error status.Recent = 0 // TODO case imap.StatusUnseen: - unseen, err := mbox.backend.Storage.MailUnseen(mbox.user.username, mbox.name) + unseen, err := mbox.backend.Storage.MailUnseen(mbox.name) if err != nil { return nil, fmt.Errorf("mbox.backend.Storage.MailUnseen: %w", err) } @@ -99,7 +99,7 @@ func (mbox *Mailbox) Status(items []imap.StatusItem) (*imap.MailboxStatus, error } func (mbox *Mailbox) SetSubscribed(subscribed bool) error { - return mbox.backend.Storage.MailboxSubscribe(mbox.user.username, mbox.name, subscribed) + return mbox.backend.Storage.MailboxSubscribe(mbox.name, subscribed) } func (mbox *Mailbox) Check() error { @@ -115,7 +115,7 @@ func (mbox *Mailbox) ListMessages(uid bool, seqSet *imap.SeqSet, items []imap.Fe } for _, id := range ids { - mseq, mid, body, seen, answered, flagged, deleted, datetime, err := mbox.backend.Storage.MailSelect(mbox.user.username, mbox.name, int(id)) + mseq, mid, body, seen, answered, flagged, deleted, datetime, err := mbox.backend.Storage.MailSelect(mbox.name, int(id)) if err != nil { continue } @@ -201,7 +201,7 @@ func (mbox *Mailbox) ListMessages(uid bool, seqSet *imap.SeqSet, items []imap.Fe } func (mbox *Mailbox) SearchMessages(uid bool, criteria *imap.SearchCriteria) ([]uint32, error) { - return mbox.backend.Storage.MailSearch(mbox.user.username, mbox.name) + return mbox.backend.Storage.MailSearch(mbox.name) } func (mbox *Mailbox) CreateMessage(flags []string, date time.Time, body imap.Literal) error { @@ -209,7 +209,7 @@ func (mbox *Mailbox) CreateMessage(flags []string, date time.Time, body imap.Lit if err != nil { return fmt.Errorf("b.ReadFrom: %w", err) } - id, err := mbox.backend.Storage.MailCreate(mbox.user.username, mbox.name, b) + id, err := mbox.backend.Storage.MailCreate(mbox.name, b) if err != nil { return fmt.Errorf("mbox.backend.Storage.MailCreate: %w", err) } @@ -226,7 +226,7 @@ func (mbox *Mailbox) CreateMessage(flags []string, date time.Time, body imap.Lit deleted = true } if err := mbox.backend.Storage.MailUpdateFlags( - mbox.user.username, mbox.name, id, seen, answered, flagged, deleted, + mbox.name, id, seen, answered, flagged, deleted, ); err != nil { return err } @@ -245,7 +245,7 @@ func (mbox *Mailbox) UpdateMessagesFlags(uid bool, seqSet *imap.SeqSet, op imap. var mid int if op != imap.SetFlags { var err error - _, mid, _, seen, answered, flagged, deleted, _, err = mbox.backend.Storage.MailSelect(mbox.user.username, mbox.name, int(id)) + _, mid, _, seen, answered, flagged, deleted, _, err = mbox.backend.Storage.MailSelect(mbox.name, int(id)) if err != nil { return fmt.Errorf("mbox.backend.Storage.MailSelect: %w", err) } @@ -264,7 +264,7 @@ func (mbox *Mailbox) UpdateMessagesFlags(uid bool, seqSet *imap.SeqSet, op imap. } if err := mbox.backend.Storage.MailUpdateFlags( - mbox.user.username, mbox.name, int(mid), seen, answered, flagged, deleted, + mbox.name, int(mid), seen, answered, flagged, deleted, ); err != nil { return err } @@ -279,15 +279,15 @@ func (mbox *Mailbox) CopyMessages(uid bool, seqSet *imap.SeqSet, destName string } for _, id := range ids { - _, _, body, seen, answered, flagged, deleted, _, err := mbox.backend.Storage.MailSelect(mbox.user.username, mbox.name, int(id)) + _, _, body, seen, answered, flagged, deleted, _, err := mbox.backend.Storage.MailSelect(mbox.name, int(id)) if err != nil { return fmt.Errorf("mbox.backend.Storage.MailSelect: %w", err) } - pid, err := mbox.backend.Storage.MailCreate(mbox.user.username, destName, body) + pid, err := mbox.backend.Storage.MailCreate(destName, body) if err != nil { return fmt.Errorf("mbox.backend.Storage.MailCreate: %w", err) } - if err = mbox.backend.Storage.MailUpdateFlags(mbox.user.username, mbox.name, pid, seen, answered, flagged, deleted); err != nil { + if err = mbox.backend.Storage.MailUpdateFlags(mbox.name, pid, seen, answered, flagged, deleted); err != nil { return fmt.Errorf("mbox.backend.Storage.MailUpdateFlags: %w", err) } } @@ -295,5 +295,5 @@ func (mbox *Mailbox) CopyMessages(uid bool, seqSet *imap.SeqSet, destName string } func (mbox *Mailbox) Expunge() error { - return mbox.backend.Storage.MailExpunge(mbox.user.username, mbox.name) + return mbox.backend.Storage.MailExpunge(mbox.name) } diff --git a/internal/imapserver/user.go b/internal/imapserver/user.go index 2222b3f..3771939 100644 --- a/internal/imapserver/user.go +++ b/internal/imapserver/user.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/emersion/go-imap/backend" + "github.com/jxskiss/base62" ) type User struct { @@ -13,11 +14,11 @@ type User struct { } func (u *User) Username() string { - return u.username + return base62.EncodeToString(u.backend.Config.PublicKey) } func (u *User) ListMailboxes(subscribed bool) (mailboxes []backend.Mailbox, err error) { - names, err := u.backend.Storage.MailboxList(u.username, subscribed) + names, err := u.backend.Storage.MailboxList(subscribed) if err != nil { return nil, err } @@ -41,7 +42,7 @@ func (u *User) GetMailbox(name string) (mailbox backend.Mailbox, err error) { name: "", }, nil } - ok, _ := u.backend.Storage.MailboxSelect(u.username, name) + ok, _ := u.backend.Storage.MailboxSelect(name) if !ok { return nil, fmt.Errorf("mailbox %q not found", name) } @@ -53,21 +54,21 @@ func (u *User) GetMailbox(name string) (mailbox backend.Mailbox, err error) { } func (u *User) CreateMailbox(name string) error { - return u.backend.Storage.MailboxCreate(u.username, name) + return u.backend.Storage.MailboxCreate(name) } func (u *User) DeleteMailbox(name string) error { if name == "INBOX" { return errors.New("Cannot delete INBOX") } - return u.backend.Storage.MailboxDelete(u.username, name) + return u.backend.Storage.MailboxDelete(name) } func (u *User) RenameMailbox(existingName, newName string) error { if existingName == "INBOX" { return errors.New("Cannot rename INBOX") } - return u.backend.Storage.MailboxRename(u.username, existingName, newName) + return u.backend.Storage.MailboxRename(existingName, newName) } func (u *User) Logout() error { diff --git a/internal/smtpserver/backend.go b/internal/smtpserver/backend.go index 5bd5167..6deb781 100644 --- a/internal/smtpserver/backend.go +++ b/internal/smtpserver/backend.go @@ -32,15 +32,14 @@ func (b *Backend) Login(state *smtp.ConnectionState, username, password string) switch b.Mode { case BackendModeInternal: // If our username is email-like, then take just the localpart - if localpart, host, err := utils.ParseAddress(username); err == nil { - if host != base62.EncodeToString(b.Config.PublicKey) { + if pk, err := utils.ParseAddress(username); err == nil { + if !pk.Equal(b.Config.PublicKey) { return nil, fmt.Errorf("failed to authenticate: wrong domain in username") } - username = localpart } - + username = base62.EncodeToString(b.Config.PublicKey) // The connection came from our local listener - if authed, err := b.Storage.TryAuthenticate(username, password); err != nil { + if authed, err := b.Storage.ConfigTryPassword(password); err != nil { b.Log.Printf("Failed to authenticate SMTP user %q due to error: %s", username, err) return nil, fmt.Errorf("failed to authenticate: %w", err) } else if !authed { diff --git a/internal/smtpserver/session_local.go b/internal/smtpserver/session_local.go index a2236ca..08df060 100644 --- a/internal/smtpserver/session_local.go +++ b/internal/smtpserver/session_local.go @@ -21,12 +21,12 @@ type SessionLocal struct { } func (s *SessionLocal) Mail(from string, opts smtp.MailOptions) error { - _, host, err := utils.ParseAddress(from) + pk, err := utils.ParseAddress(from) if err != nil { return fmt.Errorf("parseAddress: %w", err) } - if host != base62.EncodeToString(s.backend.Config.PublicKey) { + if !pk.Equal(s.backend.Config.PublicKey) { return fmt.Errorf("not allowed to send outgoing mail as %s", from) } @@ -56,22 +56,23 @@ func (s *SessionLocal) Data(r io.Reader) error { servers := make(map[string]struct{}) for _, rcpt := range s.rcpt { - localpart, host, err := utils.ParseAddress(rcpt) + pk, err := utils.ParseAddress(rcpt) if err != nil { return fmt.Errorf("parseAddress: %w", err) } + host := base62.EncodeToString(pk) if _, ok := servers[host]; ok { continue } servers[host] = struct{}{} - if host == base62.EncodeToString(s.backend.Config.PublicKey) { + if pk.Equal(s.backend.Config.PublicKey) { var b bytes.Buffer if err := m.WriteTo(&b); err != nil { return fmt.Errorf("m.WriteTo: %w", err) } - if _, err := s.backend.Storage.MailCreate(localpart, "INBOX", b.Bytes()); err != nil { + if _, err := s.backend.Storage.MailCreate("INBOX", b.Bytes()); err != nil { return fmt.Errorf("s.backend.Storage.StoreMessageFor: %w", err) } continue diff --git a/internal/smtpserver/session_remote.go b/internal/smtpserver/session_remote.go index 4a7427a..8a48a7e 100644 --- a/internal/smtpserver/session_remote.go +++ b/internal/smtpserver/session_remote.go @@ -23,7 +23,7 @@ type SessionRemote struct { } func (s *SessionRemote) Mail(from string, opts smtp.MailOptions) error { - _, host, err := utils.ParseAddress(from) + pk, err := utils.ParseAddress(from) if err != nil { return fmt.Errorf("mail.ParseAddress: %w", err) } @@ -33,7 +33,7 @@ func (s *SessionRemote) Mail(from string, opts smtp.MailOptions) error { return fmt.Errorf("hex.DecodeString: %w", err) } - if remote := base62.EncodeToString(pks); host != remote { + if remote := base62.EncodeToString(pks); base62.EncodeToString(pk) != remote { return fmt.Errorf("not allowed to send incoming mail as %s", from) } @@ -42,16 +42,15 @@ func (s *SessionRemote) Mail(from string, opts smtp.MailOptions) error { } func (s *SessionRemote) Rcpt(to string) error { - user, host, err := utils.ParseAddress(to) + pk, err := utils.ParseAddress(to) if err != nil { return fmt.Errorf("mail.ParseAddress: %w", err) } - if local := base62.EncodeToString(s.backend.Config.PublicKey); host != local { - return fmt.Errorf("not allowed to send mail to %q", host) + if !pk.Equal(s.backend.Config.PublicKey) { + return fmt.Errorf("unexpected recipient for wrong domain") } - s.localparts = append(s.localparts, user) return nil } @@ -73,12 +72,11 @@ func (s *SessionRemote) Data(r io.Reader) error { return fmt.Errorf("m.WriteTo: %w", err) } - for _, localpart := range s.localparts { - if _, err := s.backend.Storage.MailCreate(localpart, "INBOX", b.Bytes()); err != nil { - return fmt.Errorf("s.backend.Storage.StoreMessageFor: %w", err) - } - s.backend.Log.Printf("Stored new mail for local user %q from %s", localpart, s.from) + if _, err := s.backend.Storage.MailCreate("INBOX", b.Bytes()); err != nil { + return fmt.Errorf("s.backend.Storage.StoreMessageFor: %w", err) } + s.backend.Log.Printf("Stored new mail from %s", s.from) + return nil } diff --git a/internal/storage/sqlite3/sqlite3.go b/internal/storage/sqlite3/sqlite3.go index 0109d5d..5a3769d 100644 --- a/internal/storage/sqlite3/sqlite3.go +++ b/internal/storage/sqlite3/sqlite3.go @@ -9,7 +9,6 @@ import ( type SQLite3Storage struct { *TableConfig - *TableUsers *TableMailboxes *TableMails } @@ -24,10 +23,6 @@ func NewSQLite3StorageStorage(filename string) (*SQLite3Storage, error) { if err != nil { return nil, fmt.Errorf("NewTableConfig: %w", err) } - s.TableUsers, err = NewTableUsers(db) - if err != nil { - return nil, fmt.Errorf("NewTableUsers: %w", err) - } s.TableMailboxes, err = NewTableMailboxes(db) if err != nil { return nil, fmt.Errorf("NewTableMailboxes: %w", err) diff --git a/internal/storage/sqlite3/table_config.go b/internal/storage/sqlite3/table_config.go index 00ac491..2ba200b 100644 --- a/internal/storage/sqlite3/table_config.go +++ b/internal/storage/sqlite3/table_config.go @@ -3,6 +3,8 @@ package sqlite3 import ( "database/sql" "fmt" + + "golang.org/x/crypto/bcrypt" ) type TableConfig struct { @@ -59,3 +61,26 @@ func (t *TableConfig) ConfigSet(key, value string) error { _, err := t.set.Exec(key, value) return err } + +func (t *TableConfig) ConfigSetPassword(password string) error { + hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + if err != nil { + return fmt.Errorf("bcrypt.GenerateFromPassword: %w", err) + } + return t.ConfigSet("password", string(hash)) +} + +func (t *TableConfig) ConfigTryPassword(password string) (bool, error) { + dbPasswordHash, err := t.ConfigGet("password") + if err != nil { + return false, err + } + if dbPasswordHash == "" { + return true, nil // TODO: Do we want to allow login if no password is set? + } + err = bcrypt.CompareHashAndPassword([]byte(dbPasswordHash), []byte(password)) + if err == nil { + return true, nil + } + return false, err +} diff --git a/internal/storage/sqlite3/table_mailboxes.go b/internal/storage/sqlite3/table_mailboxes.go index d37e76c..d36618f 100644 --- a/internal/storage/sqlite3/table_mailboxes.go +++ b/internal/storage/sqlite3/table_mailboxes.go @@ -18,39 +18,38 @@ type TableMailboxes struct { const mailboxesSchema = ` CREATE TABLE IF NOT EXISTS mailboxes ( - username TEXT NOT NULL REFERENCES users(username) ON DELETE CASCADE ON UPDATE CASCADE, mailbox TEXT NOT NULL DEFAULT('INBOX'), subscribed BOOLEAN NOT NULL DEFAULT 1, - PRIMARY KEY(username, mailbox) + PRIMARY KEY(mailbox) ); ` const mailboxesList = ` - SELECT mailbox FROM mailboxes WHERE username = $1 + SELECT mailbox FROM mailboxes ` const mailboxesListSubscribed = ` - SELECT mailbox FROM mailboxes WHERE username = $1 AND subscribed = 1 + SELECT mailbox FROM mailboxes WHERE subscribed = 1 ` const mailboxesSelect = ` - SELECT mailbox FROM mailboxes WHERE username = $1 AND mailbox = $2 + SELECT mailbox FROM mailboxes WHERE mailbox = $1 ` const mailboxesCreate = ` - INSERT INTO mailboxes (username, mailbox) VALUES($1, $2) + INSERT INTO mailboxes (mailbox) VALUES($1) ` const mailboxesRename = ` - UPDATE mailboxes SET mailbox = $1 WHERE username = $2 AND mailbox = $3 + UPDATE mailboxes SET mailbox = $1 WHERE mailbox = $2 ` const mailboxesDelete = ` - DELETE FROM mailboxes WHERE username = $1 AND mailbox = $2 + DELETE FROM mailboxes WHERE mailbox = $1 ` const mailboxesSubscribe = ` - UPDATE mailboxes SET subscribed = $1 WHERE username = $2 AND mailbox = $3 + UPDATE mailboxes SET subscribed = $1 WHERE mailbox = $2 ` func NewTableMailboxes(db *sql.DB) (*TableMailboxes, error) { @@ -92,12 +91,12 @@ func NewTableMailboxes(db *sql.DB) (*TableMailboxes, error) { return t, nil } -func (t *TableMailboxes) MailboxList(user string, onlySubscribed bool) ([]string, error) { +func (t *TableMailboxes) MailboxList(onlySubscribed bool) ([]string, error) { stmt := t.listMailboxes if onlySubscribed { stmt = t.listMailboxesSubscribed } - rows, err := stmt.Query(user) + rows, err := stmt.Query() if err != nil { return nil, fmt.Errorf("t.listMailboxes.Query: %w", err) } @@ -113,8 +112,8 @@ func (t *TableMailboxes) MailboxList(user string, onlySubscribed bool) ([]string return mailboxes, nil } -func (t *TableMailboxes) MailboxSelect(user, mailbox string) (bool, error) { - row := t.selectMailboxes.QueryRow(user, mailbox) +func (t *TableMailboxes) MailboxSelect(mailbox string) (bool, error) { + row := t.selectMailboxes.QueryRow(mailbox) if err := row.Err(); err != nil && err != sql.ErrNoRows { return false, fmt.Errorf("row.Err: %w", err) } else if err == sql.ErrNoRows { @@ -127,26 +126,26 @@ func (t *TableMailboxes) MailboxSelect(user, mailbox string) (bool, error) { return mailbox == got, nil } -func (t *TableMailboxes) MailboxCreate(user, name string) error { - _, err := t.createMailbox.Exec(user, name) +func (t *TableMailboxes) MailboxCreate(name string) error { + _, err := t.createMailbox.Exec(name) return err } -func (t *TableMailboxes) MailboxRename(user, old, new string) error { - _, err := t.renameMailbox.Exec(new, user, old) +func (t *TableMailboxes) MailboxRename(old, new string) error { + _, err := t.renameMailbox.Exec(old, new) return err } -func (t *TableMailboxes) MailboxDelete(user, name string) error { - _, err := t.deleteMailbox.Exec(user, name) +func (t *TableMailboxes) MailboxDelete(name string) error { + _, err := t.deleteMailbox.Exec(name) return err } -func (t *TableMailboxes) MailboxSubscribe(user, name string, subscribed bool) error { +func (t *TableMailboxes) MailboxSubscribe(name string, subscribed bool) error { sn := 1 if !subscribed { sn = 0 } - _, err := t.subscribeMailbox.Exec(sn, user, name) + _, err := t.subscribeMailbox.Exec(sn, name) return err } diff --git a/internal/storage/sqlite3/table_mails.go b/internal/storage/sqlite3/table_mails.go index 859a67c..57d9378 100644 --- a/internal/storage/sqlite3/table_mails.go +++ b/internal/storage/sqlite3/table_mails.go @@ -23,7 +23,6 @@ type TableMails struct { const mailsSchema = ` CREATE TABLE IF NOT EXISTS mails ( - username TEXT NOT NULL, mailbox TEXT NOT NULL, id INTEGER NOT NULL DEFAULT 1, mail BLOB NOT NULL, @@ -32,72 +31,72 @@ const mailsSchema = ` answered BOOLEAN NOT NULL DEFAULT 0, -- the mail has been replied to flagged BOOLEAN NOT NULL DEFAULT 0, -- the mail has been flagged for later attention deleted BOOLEAN NOT NULL DEFAULT 0, -- the email is marked for deletion at next EXPUNGE - PRIMARY KEY(username, mailbox, id), - FOREIGN KEY (username, mailbox) REFERENCES mailboxes(username, mailbox) ON DELETE CASCADE ON UPDATE CASCADE + PRIMARY KEY (mailbox, id), + FOREIGN KEY (mailbox) REFERENCES mailboxes(mailbox) ON DELETE CASCADE ON UPDATE CASCADE ); DROP VIEW IF EXISTS inboxes; CREATE VIEW IF NOT EXISTS inboxes AS SELECT * FROM ( - SELECT ROW_NUMBER() OVER (PARTITION BY username, mailbox) AS seq, * FROM mails + SELECT ROW_NUMBER() OVER (PARTITION BY mailbox) AS seq, * FROM mails ) - ORDER BY username, mailbox, id; + ORDER BY mailbox, id; ` const selectMailsStmt = ` SELECT * FROM inboxes - ORDER BY username, mailbox, id + ORDER BY mailbox, id ` const selectMailStmt = ` SELECT seq, id, mail, datetime, seen, answered, flagged, deleted FROM inboxes - WHERE username = $1 AND mailbox = $2 AND id = $3 - ORDER BY username, mailbox, id + WHERE mailbox = $1 AND id = $2 + ORDER BY mailbox, id ` const selectMailCountStmt = ` - SELECT COUNT(*) FROM mails WHERE username = $1 AND mailbox = $2 + SELECT COUNT(*) FROM mails WHERE mailbox = $1 ` const selectMailUnseenStmt = ` - SELECT COUNT(*) FROM mails WHERE username = $1 AND mailbox = $2 AND seen = 0 + SELECT COUNT(*) FROM mails WHERE mailbox = $1 AND seen = 0 ` const searchMailStmt = ` SELECT id FROM mails - WHERE username = $1 AND mailbox = $2 - ORDER BY username, mailbox, id + WHERE mailbox = $1 + ORDER BY mailbox, id ` const insertMailStmt = ` - INSERT INTO mails (username, mailbox, id, mail, datetime) VALUES( - $1, $2, ( + INSERT INTO mails (mailbox, id, mail, datetime) VALUES( + $1, ( SELECT IFNULL(MAX(id)+1,1) AS id FROM mails - WHERE username = $1 AND mailbox = $2 - ), $3, $4 + WHERE mailbox = $1 + ), $2, $3 ) RETURNING id; ` const selectIDForSeqStmt = ` SELECT id FROM inboxes - WHERE username = $1 AND mailbox = $2 AND seq = $3 + WHERE mailbox = $1 AND seq = $2 ` const selectMailNextID = ` SELECT IFNULL(MAX(id)+1,1) AS id FROM mails - WHERE username = $1 AND mailbox = $2 + WHERE mailbox = $1 ` const updateMailFlagsStmt = ` - UPDATE mails SET seen = $1, answered = $2, flagged = $3, deleted = $4 WHERE username = $5 AND mailbox = $6 AND id = $7 + UPDATE mails SET seen = $1, answered = $2, flagged = $3, deleted = $4 WHERE mailbox = $5 AND id = $6 ` const deleteMailStmt = ` - UPDATE mails SET deleted = 1 WHERE username = $1 AND mailbox = $2 AND id = $3 + UPDATE mails SET deleted = 1 WHERE mailbox = $1 AND id = $2 ` const expungeMailStmt = ` - DELETE FROM mails WHERE username = $1 AND mailbox = $2 AND deleted = 1 + DELETE FROM mails WHERE mailbox = $1 AND deleted = 1 ` func NewTableMails(db *sql.DB) (*TableMails, error) { @@ -155,24 +154,24 @@ func NewTableMails(db *sql.DB) (*TableMails, error) { return t, nil } -func (t *TableMails) MailCreate(user, mailbox string, data []byte) (int, error) { +func (t *TableMails) MailCreate(mailbox string, data []byte) (int, error) { var id int - err := t.createMail.QueryRow(user, mailbox, data, time.Now().Unix()).Scan(&id) + err := t.createMail.QueryRow(mailbox, data, time.Now().Unix()).Scan(&id) return id, err } -func (t *TableMails) MailSelect(user, mailbox string, id int) (int, int, []byte, bool, bool, bool, bool, time.Time, error) { +func (t *TableMails) MailSelect(mailbox string, id int) (int, int, []byte, bool, bool, bool, bool, time.Time, error) { var data []byte var seen, answered, flagged, deleted bool var ts int64 var seq, pid int - err := t.selectMail.QueryRow(user, mailbox, id).Scan(&seq, &pid, &data, &ts, &seen, &answered, &flagged, &deleted) + err := t.selectMail.QueryRow(mailbox, id).Scan(&seq, &pid, &data, &ts, &seen, &answered, &flagged, &deleted) return seq, pid, data, seen, answered, flagged, deleted, time.Unix(ts, 0), err } -func (t *TableMails) MailSearch(user, mailbox string) ([]uint32, error) { +func (t *TableMails) MailSearch(mailbox string) ([]uint32, error) { var ids []uint32 - rows, err := t.searchMail.Query(user, mailbox) + rows, err := t.searchMail.Query(mailbox) if err != nil { return nil, fmt.Errorf("t.searchMail.Query: %w", err) } @@ -187,41 +186,41 @@ func (t *TableMails) MailSearch(user, mailbox string) ([]uint32, error) { return ids, nil } -func (t *TableMails) MailNextID(user, mailbox string) (int, error) { +func (t *TableMails) MailNextID(mailbox string) (int, error) { var id int - err := t.selectMailNextID.QueryRow(user, mailbox).Scan(&id) + err := t.selectMailNextID.QueryRow(mailbox).Scan(&id) return id, err } -func (t *TableMails) MailIDForSeq(user, mailbox string, seq int) (int, error) { +func (t *TableMails) MailIDForSeq(mailbox string, seq int) (int, error) { var id int - err := t.selectIDForSeq.QueryRow(user, mailbox, seq).Scan(&id) + err := t.selectIDForSeq.QueryRow(mailbox, seq).Scan(&id) return id, err } -func (t *TableMails) MailUnseen(user, mailbox string) (int, error) { +func (t *TableMails) MailUnseen(mailbox string) (int, error) { var unseen int - err := t.countUnseenMails.QueryRow(user, mailbox).Scan(&unseen) + err := t.countUnseenMails.QueryRow(mailbox).Scan(&unseen) return unseen, err } -func (t *TableMails) MailUpdateFlags(user, mailbox string, id int, seen, answered, flagged, deleted bool) error { - _, err := t.updateMailFlags.Exec(seen, answered, flagged, deleted, user, mailbox, id) +func (t *TableMails) MailUpdateFlags(mailbox string, id int, seen, answered, flagged, deleted bool) error { + _, err := t.updateMailFlags.Exec(seen, answered, flagged, deleted, mailbox, id) return err } -func (t *TableMails) MailDelete(user, mailbox, id string) error { - _, err := t.deleteMail.Exec(user, mailbox, id) +func (t *TableMails) MailDelete(mailbox, id string) error { + _, err := t.deleteMail.Exec(mailbox, id) return err } -func (t *TableMails) MailExpunge(user, mailbox string) error { - _, err := t.expungeMail.Exec(user, mailbox) +func (t *TableMails) MailExpunge(mailbox string) error { + _, err := t.expungeMail.Exec(mailbox) return err } -func (t *TableMails) MailCount(user, mailbox string) (int, error) { +func (t *TableMails) MailCount(mailbox string) (int, error) { var count int - err := t.countMails.QueryRow(user, mailbox).Scan(&count) + err := t.countMails.QueryRow(mailbox).Scan(&count) return count, err } diff --git a/internal/storage/sqlite3/table_users.go b/internal/storage/sqlite3/table_users.go deleted file mode 100644 index 2b90924..0000000 --- a/internal/storage/sqlite3/table_users.go +++ /dev/null @@ -1,76 +0,0 @@ -package sqlite3 - -import ( - "database/sql" - "fmt" - - "golang.org/x/crypto/bcrypt" -) - -type TableUsers struct { - db *sql.DB - getPassword *sql.Stmt - insertUser *sql.Stmt -} - -const usersSchema = ` - CREATE TABLE IF NOT EXISTS users ( - username TEXT NOT NULL, - password TEXT NOT NULL, - PRIMARY KEY(username) - ); -` - -const usersGetPassword = ` - SELECT password FROM users WHERE username = $1 -` - -const usersInsertUser = ` - INSERT INTO users (username, password) VALUES($1, $2) -` - -func NewTableUsers(db *sql.DB) (*TableUsers, error) { - t := &TableUsers{ - db: db, - } - _, err := db.Exec(usersSchema) - if err != nil { - return nil, fmt.Errorf("db.Exec: %w", err) - } - t.getPassword, err = db.Prepare(usersGetPassword) - if err != nil { - return nil, fmt.Errorf("db.Prepare(getPassword): %w", err) - } - t.insertUser, err = db.Prepare(usersInsertUser) - if err != nil { - return nil, fmt.Errorf("db.Prepare(usersInsertUser): %w", err) - } - return t, nil -} - -func (t *TableUsers) CreateUser(username, password string) error { - hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) - if err != nil { - return fmt.Errorf("bcrypt.GenerateFromPassword: %w", err) - } - if _, err := t.insertUser.Exec(username, hash); err != nil { - return fmt.Errorf("t.insertUser.Exec: %w", err) - } - return nil -} - -func (t *TableUsers) TryAuthenticate(username, password string) (bool, error) { - var dbPasswordHash []byte - err := t.getPassword.QueryRow(username).Scan(&dbPasswordHash) - if err == sql.ErrNoRows { - return false, nil - } - if err != nil { - return false, err - } - err = bcrypt.CompareHashAndPassword(dbPasswordHash, []byte(password)) - if err == nil { - return true, nil - } - return false, err -} diff --git a/internal/storage/storage.go b/internal/storage/storage.go index 5a46497..a71bccf 100644 --- a/internal/storage/storage.go +++ b/internal/storage/storage.go @@ -5,24 +5,24 @@ import "time" type Storage interface { ConfigGet(key string) (string, error) ConfigSet(key, value string) error + ConfigSetPassword(password string) error + ConfigTryPassword(password string) (bool, error) - TryAuthenticate(username, password string) (bool, error) + MailboxSelect(mailbox string) (bool, error) + MailNextID(mailbox string) (int, error) + MailIDForSeq(mailbox string, id int) (int, error) + MailUnseen(mailbox string) (int, error) + MailboxList(onlySubscribed bool) ([]string, error) + MailboxCreate(name string) error + MailboxRename(old, new string) error + MailboxDelete(name string) error + MailboxSubscribe(name string, subscribed bool) error - MailboxSelect(user, mailbox string) (bool, error) - MailNextID(user, mailbox string) (int, error) - MailIDForSeq(user, mailbox string, id int) (int, error) - MailUnseen(user, mailbox string) (int, error) - MailboxList(user string, onlySubscribed bool) ([]string, error) - MailboxCreate(user, name string) error - MailboxRename(user, old, new string) error - MailboxDelete(user, name string) error - MailboxSubscribe(user, name string, subscribed bool) error - - MailCreate(user, mailbox string, data []byte) (int, error) - MailSelect(user, mailbox string, id int) (int, int, []byte, bool, bool, bool, bool, time.Time, error) - MailSearch(user, mailbox string) ([]uint32, error) - MailUpdateFlags(user, mailbox string, id int, seen, answered, flagged, deleted bool) error - MailDelete(user, mailbox, id string) error - MailExpunge(user, mailbox string) error - MailCount(user, mailbox string) (int, error) + MailCreate(mailbox string, data []byte) (int, error) + MailSelect(mailbox string, id int) (int, int, []byte, bool, bool, bool, bool, time.Time, error) + MailSearch(mailbox string) ([]uint32, error) + MailUpdateFlags(mailbox string, id int, seen, answered, flagged, deleted bool) error + MailDelete(mailbox, id string) error + MailExpunge(mailbox string) error + MailCount(mailbox string) (int, error) } diff --git a/internal/utils/address.go b/internal/utils/address.go index 934138b..53948db 100644 --- a/internal/utils/address.go +++ b/internal/utils/address.go @@ -1,19 +1,35 @@ package utils import ( + "crypto/ed25519" "fmt" "strings" + + "github.com/jxskiss/base62" ) -const TLD = ".yggmail" +const Domain = "yggmail" -func ParseAddress(email string) (string, string, error) { - if !strings.HasSuffix(email, TLD) { - return "", "", fmt.Errorf("invalid TLD") - } +func CreateAddress(pk ed25519.PublicKey) string { + return fmt.Sprintf( + "%s@%s", + pk, Domain, + ) +} + +func ParseAddress(email string) (ed25519.PublicKey, error) { at := strings.LastIndex(email, "@") if at == 0 { - return "", "", fmt.Errorf("invalid email address") + return nil, fmt.Errorf("invalid email address") } - return email[:at], strings.TrimSuffix(email[at+1:], TLD), nil + if email[at+1:] != Domain { + return nil, fmt.Errorf("invalid email domain") + } + pk, err := base62.DecodeString(email[:at]) + if err != nil { + return nil, fmt.Errorf("base62.DecodeString: %w", err) + } + ed := make(ed25519.PublicKey, ed25519.PublicKeySize) + copy(ed, pk) + return ed, nil }