Single user refactor

This commit is contained in:
Neil Alexander
2021-07-09 00:08:26 +01:00
parent 8411468451
commit 0735fa74de
15 changed files with 190 additions and 233 deletions

View File

@@ -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; * 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. * 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? ## Why?
@@ -28,9 +28,9 @@ Use a recent version of Go to install Yggmail:
go install github.com/neilalexander/yggmail/cmd/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: 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; * `-database=/path/to/yggmail.db` — use a specific database file;
* `-smtp=listenaddr:port` — listen for SMTP on a specific address/port * `-smtp=listenaddr:port` — listen for SMTP on a specific address/port
* `-imap=listenaddr:port` — listen for IMAP 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 ## Notes

View File

@@ -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 smtpaddr = flag.String("smtp", "localhost:1025", "SMTP listen address")
var imapaddr = flag.String("imap", "localhost:1026", "IMAP listen address") var imapaddr = flag.String("imap", "localhost:1026", "IMAP listen address")
var peeraddr = flag.String("peer", "", "Yggdrasil static peer") 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() { func main() {
flag.Parse() flag.Parse()
@@ -57,6 +57,9 @@ func main() {
if err := storage.ConfigSet("private_key", hex.EncodeToString(sk)); err != nil { if err := storage.ConfigSet("private_key", hex.EncodeToString(sk)); err != nil {
panic(err) panic(err)
} }
if err := storage.MailboxCreate("INBOX"); err != nil {
panic(err)
}
log.Printf("Generated new server identity") log.Printf("Generated new server identity")
} else { } else {
skBytes, err := hex.DecodeString(skStr) skBytes, err := hex.DecodeString(skStr)
@@ -66,35 +69,32 @@ func main() {
copy(sk, skBytes) copy(sk, skBytes)
} }
pk := sk.Public().(ed25519.PublicKey) 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 { switch {
case createuser != nil && *createuser != "": case password != nil && *password:
fmt.Printf("New password: ") log.Println("Please enter your new password:")
password1, err := term.ReadPassword(0) password1, err := term.ReadPassword(0)
if err != nil { if err != nil {
panic(err) panic(err)
} }
fmt.Println() fmt.Println()
fmt.Printf("Confirm password: ") log.Println("Please enter your new password again:")
password2, err := term.ReadPassword(0) password2, err := term.ReadPassword(0)
if err != nil { if err != nil {
panic(err) panic(err)
} }
fmt.Println() fmt.Println()
if !bytes.Equal(password1, password2) { if !bytes.Equal(password1, password2) {
fmt.Println("The supplied passwords do not match") log.Println("The supplied passwords do not match")
os.Exit(1) os.Exit(1)
} }
if err := storage.CreateUser(*createuser, strings.TrimSpace(string(password1))); err != nil { if err := storage.ConfigSetPassword(strings.TrimSpace(string(password1))); err != nil {
fmt.Printf("Failed to create user %q\n", *createuser) log.Println("Failed to set password:", err)
os.Exit(1) os.Exit(1)
} }
if err := storage.MailboxCreate(*createuser, "INBOX"); err != nil {
panic(err) log.Println("Password for IMAP and SMTP has been updated!")
}
fmt.Printf("Created user %q\n", *createuser)
fmt.Printf("Email address will be %s@%s%s\n", *createuser, base62.EncodeToString(pk), utils.TLD)
os.Exit(0) os.Exit(0)
} }

View File

@@ -20,14 +20,14 @@ type Backend struct {
func (b *Backend) Login(_ *imap.ConnInfo, username, password string) (backend.User, error) { func (b *Backend) Login(_ *imap.ConnInfo, username, password string) (backend.User, error) {
// If our username is email-like, then take just the localpart // If our username is email-like, then take just the localpart
if localpart, host, err := utils.ParseAddress(username); err == nil { if pk, err := utils.ParseAddress(username); err == nil {
if host != base62.EncodeToString(b.Config.PublicKey) { 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") return nil, fmt.Errorf("failed to authenticate: wrong domain in username")
} }
username = localpart
} }
username = base62.EncodeToString(b.Config.PublicKey)
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 IMAP user %q due to error: %s", username, err) b.Log.Printf("Failed to authenticate IMAP user %q due to error: %s", username, err)
return nil, fmt.Errorf("failed to authenticate: %w", err) return nil, fmt.Errorf("failed to authenticate: %w", err)
} else if !authed { } else if !authed {

View File

@@ -23,7 +23,7 @@ func (mbox *Mailbox) getIDsFromSeqSet(uid bool, seqSet *imap.SeqSet) ([]int32, e
var ids []int32 var ids []int32
for _, set := range seqSet.Set { for _, set := range seqSet.Set {
if set.Stop == 0 { 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 { if err != nil {
return nil, fmt.Errorf("mbox.backend.Storage.MailNextID: %w", err) 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++ { for i := set.Start; i <= set.Stop; i++ {
if !uid { 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 { if err != nil {
return nil, fmt.Errorf("mbox.backend.Storage.MailIDForSeq: %w", err) 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 { for _, name := range items {
switch name { switch name {
case imap.StatusMessages: 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 { if err != nil {
return nil, fmt.Errorf("mbox.backend.Storage.MailCount: %w", err) return nil, fmt.Errorf("mbox.backend.Storage.MailCount: %w", err)
} }
status.Messages = uint32(count) status.Messages = uint32(count)
case imap.StatusUidNext: 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 { if err != nil {
return nil, fmt.Errorf("mbox.backend.Storage.MailNextID: %w", err) 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 status.Recent = 0 // TODO
case imap.StatusUnseen: 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 { if err != nil {
return nil, fmt.Errorf("mbox.backend.Storage.MailUnseen: %w", err) 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 { 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 { 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 { 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 { if err != nil {
continue 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) { 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 { 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 { if err != nil {
return fmt.Errorf("b.ReadFrom: %w", err) 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 { if err != nil {
return fmt.Errorf("mbox.backend.Storage.MailCreate: %w", err) 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 deleted = true
} }
if err := mbox.backend.Storage.MailUpdateFlags( 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 { ); err != nil {
return err return err
} }
@@ -245,7 +245,7 @@ func (mbox *Mailbox) UpdateMessagesFlags(uid bool, seqSet *imap.SeqSet, op imap.
var mid int var mid int
if op != imap.SetFlags { if op != imap.SetFlags {
var err error 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 { if err != nil {
return fmt.Errorf("mbox.backend.Storage.MailSelect: %w", err) 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( 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 { ); err != nil {
return err return err
} }
@@ -279,15 +279,15 @@ func (mbox *Mailbox) CopyMessages(uid bool, seqSet *imap.SeqSet, destName string
} }
for _, id := range ids { 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 { if err != nil {
return fmt.Errorf("mbox.backend.Storage.MailSelect: %w", err) 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 { if err != nil {
return fmt.Errorf("mbox.backend.Storage.MailCreate: %w", err) 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) 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 { func (mbox *Mailbox) Expunge() error {
return mbox.backend.Storage.MailExpunge(mbox.user.username, mbox.name) return mbox.backend.Storage.MailExpunge(mbox.name)
} }

View File

@@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"github.com/emersion/go-imap/backend" "github.com/emersion/go-imap/backend"
"github.com/jxskiss/base62"
) )
type User struct { type User struct {
@@ -13,11 +14,11 @@ type User struct {
} }
func (u *User) Username() string { 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) { 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 { if err != nil {
return nil, err return nil, err
} }
@@ -41,7 +42,7 @@ func (u *User) GetMailbox(name string) (mailbox backend.Mailbox, err error) {
name: "", name: "",
}, nil }, nil
} }
ok, _ := u.backend.Storage.MailboxSelect(u.username, name) ok, _ := u.backend.Storage.MailboxSelect(name)
if !ok { if !ok {
return nil, fmt.Errorf("mailbox %q not found", name) 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 { 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 { func (u *User) DeleteMailbox(name string) error {
if name == "INBOX" { if name == "INBOX" {
return errors.New("Cannot delete 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 { func (u *User) RenameMailbox(existingName, newName string) error {
if existingName == "INBOX" { if existingName == "INBOX" {
return errors.New("Cannot rename 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 { func (u *User) Logout() error {

View File

@@ -32,15 +32,14 @@ func (b *Backend) Login(state *smtp.ConnectionState, username, password string)
switch b.Mode { switch b.Mode {
case BackendModeInternal: case BackendModeInternal:
// If our username is email-like, then take just the localpart // If our username is email-like, then take just the localpart
if localpart, host, err := utils.ParseAddress(username); err == nil { if pk, err := utils.ParseAddress(username); err == nil {
if host != base62.EncodeToString(b.Config.PublicKey) { if !pk.Equal(b.Config.PublicKey) {
return nil, fmt.Errorf("failed to authenticate: wrong domain in username") 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 // 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) b.Log.Printf("Failed to authenticate SMTP user %q due to error: %s", username, err)
return nil, fmt.Errorf("failed to authenticate: %w", err) return nil, fmt.Errorf("failed to authenticate: %w", err)
} else if !authed { } else if !authed {

View File

@@ -21,12 +21,12 @@ type SessionLocal struct {
} }
func (s *SessionLocal) Mail(from string, opts smtp.MailOptions) error { func (s *SessionLocal) Mail(from string, opts smtp.MailOptions) error {
_, host, err := utils.ParseAddress(from) pk, err := utils.ParseAddress(from)
if err != nil { if err != nil {
return fmt.Errorf("parseAddress: %w", err) 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) 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{}) servers := make(map[string]struct{})
for _, rcpt := range s.rcpt { for _, rcpt := range s.rcpt {
localpart, host, err := utils.ParseAddress(rcpt) pk, err := utils.ParseAddress(rcpt)
if err != nil { if err != nil {
return fmt.Errorf("parseAddress: %w", err) return fmt.Errorf("parseAddress: %w", err)
} }
host := base62.EncodeToString(pk)
if _, ok := servers[host]; ok { if _, ok := servers[host]; ok {
continue continue
} }
servers[host] = struct{}{} servers[host] = struct{}{}
if host == base62.EncodeToString(s.backend.Config.PublicKey) { if pk.Equal(s.backend.Config.PublicKey) {
var b bytes.Buffer var b bytes.Buffer
if err := m.WriteTo(&b); err != nil { if err := m.WriteTo(&b); err != nil {
return fmt.Errorf("m.WriteTo: %w", err) 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) return fmt.Errorf("s.backend.Storage.StoreMessageFor: %w", err)
} }
continue continue

View File

@@ -23,7 +23,7 @@ type SessionRemote struct {
} }
func (s *SessionRemote) Mail(from string, opts smtp.MailOptions) error { func (s *SessionRemote) Mail(from string, opts smtp.MailOptions) error {
_, host, err := utils.ParseAddress(from) pk, err := utils.ParseAddress(from)
if err != nil { if err != nil {
return fmt.Errorf("mail.ParseAddress: %w", err) 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) 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) 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 { func (s *SessionRemote) Rcpt(to string) error {
user, host, err := utils.ParseAddress(to) pk, err := utils.ParseAddress(to)
if err != nil { if err != nil {
return fmt.Errorf("mail.ParseAddress: %w", err) return fmt.Errorf("mail.ParseAddress: %w", err)
} }
if local := base62.EncodeToString(s.backend.Config.PublicKey); host != local { if !pk.Equal(s.backend.Config.PublicKey) {
return fmt.Errorf("not allowed to send mail to %q", host) return fmt.Errorf("unexpected recipient for wrong domain")
} }
s.localparts = append(s.localparts, user)
return nil return nil
} }
@@ -73,12 +72,11 @@ func (s *SessionRemote) Data(r io.Reader) error {
return fmt.Errorf("m.WriteTo: %w", err) return fmt.Errorf("m.WriteTo: %w", err)
} }
for _, localpart := range s.localparts { if _, err := s.backend.Storage.MailCreate("INBOX", b.Bytes()); err != nil {
if _, err := s.backend.Storage.MailCreate(localpart, "INBOX", b.Bytes()); err != nil {
return fmt.Errorf("s.backend.Storage.StoreMessageFor: %w", err) 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) s.backend.Log.Printf("Stored new mail from %s", s.from)
}
return nil return nil
} }

View File

@@ -9,7 +9,6 @@ import (
type SQLite3Storage struct { type SQLite3Storage struct {
*TableConfig *TableConfig
*TableUsers
*TableMailboxes *TableMailboxes
*TableMails *TableMails
} }
@@ -24,10 +23,6 @@ func NewSQLite3StorageStorage(filename string) (*SQLite3Storage, error) {
if err != nil { if err != nil {
return nil, fmt.Errorf("NewTableConfig: %w", err) 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) s.TableMailboxes, err = NewTableMailboxes(db)
if err != nil { if err != nil {
return nil, fmt.Errorf("NewTableMailboxes: %w", err) return nil, fmt.Errorf("NewTableMailboxes: %w", err)

View File

@@ -3,6 +3,8 @@ package sqlite3
import ( import (
"database/sql" "database/sql"
"fmt" "fmt"
"golang.org/x/crypto/bcrypt"
) )
type TableConfig struct { type TableConfig struct {
@@ -59,3 +61,26 @@ func (t *TableConfig) ConfigSet(key, value string) error {
_, err := t.set.Exec(key, value) _, err := t.set.Exec(key, value)
return err 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
}

View File

@@ -18,39 +18,38 @@ type TableMailboxes struct {
const mailboxesSchema = ` const mailboxesSchema = `
CREATE TABLE IF NOT EXISTS mailboxes ( 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'), mailbox TEXT NOT NULL DEFAULT('INBOX'),
subscribed BOOLEAN NOT NULL DEFAULT 1, subscribed BOOLEAN NOT NULL DEFAULT 1,
PRIMARY KEY(username, mailbox) PRIMARY KEY(mailbox)
); );
` `
const mailboxesList = ` const mailboxesList = `
SELECT mailbox FROM mailboxes WHERE username = $1 SELECT mailbox FROM mailboxes
` `
const mailboxesListSubscribed = ` const mailboxesListSubscribed = `
SELECT mailbox FROM mailboxes WHERE username = $1 AND subscribed = 1 SELECT mailbox FROM mailboxes WHERE subscribed = 1
` `
const mailboxesSelect = ` const mailboxesSelect = `
SELECT mailbox FROM mailboxes WHERE username = $1 AND mailbox = $2 SELECT mailbox FROM mailboxes WHERE mailbox = $1
` `
const mailboxesCreate = ` const mailboxesCreate = `
INSERT INTO mailboxes (username, mailbox) VALUES($1, $2) INSERT INTO mailboxes (mailbox) VALUES($1)
` `
const mailboxesRename = ` const mailboxesRename = `
UPDATE mailboxes SET mailbox = $1 WHERE username = $2 AND mailbox = $3 UPDATE mailboxes SET mailbox = $1 WHERE mailbox = $2
` `
const mailboxesDelete = ` const mailboxesDelete = `
DELETE FROM mailboxes WHERE username = $1 AND mailbox = $2 DELETE FROM mailboxes WHERE mailbox = $1
` `
const mailboxesSubscribe = ` 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) { func NewTableMailboxes(db *sql.DB) (*TableMailboxes, error) {
@@ -92,12 +91,12 @@ func NewTableMailboxes(db *sql.DB) (*TableMailboxes, error) {
return t, nil return t, nil
} }
func (t *TableMailboxes) MailboxList(user string, onlySubscribed bool) ([]string, error) { func (t *TableMailboxes) MailboxList(onlySubscribed bool) ([]string, error) {
stmt := t.listMailboxes stmt := t.listMailboxes
if onlySubscribed { if onlySubscribed {
stmt = t.listMailboxesSubscribed stmt = t.listMailboxesSubscribed
} }
rows, err := stmt.Query(user) rows, err := stmt.Query()
if err != nil { if err != nil {
return nil, fmt.Errorf("t.listMailboxes.Query: %w", err) 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 return mailboxes, nil
} }
func (t *TableMailboxes) MailboxSelect(user, mailbox string) (bool, error) { func (t *TableMailboxes) MailboxSelect(mailbox string) (bool, error) {
row := t.selectMailboxes.QueryRow(user, mailbox) row := t.selectMailboxes.QueryRow(mailbox)
if err := row.Err(); err != nil && err != sql.ErrNoRows { if err := row.Err(); err != nil && err != sql.ErrNoRows {
return false, fmt.Errorf("row.Err: %w", err) return false, fmt.Errorf("row.Err: %w", err)
} else if err == sql.ErrNoRows { } else if err == sql.ErrNoRows {
@@ -127,26 +126,26 @@ func (t *TableMailboxes) MailboxSelect(user, mailbox string) (bool, error) {
return mailbox == got, nil return mailbox == got, nil
} }
func (t *TableMailboxes) MailboxCreate(user, name string) error { func (t *TableMailboxes) MailboxCreate(name string) error {
_, err := t.createMailbox.Exec(user, name) _, err := t.createMailbox.Exec(name)
return err return err
} }
func (t *TableMailboxes) MailboxRename(user, old, new string) error { func (t *TableMailboxes) MailboxRename(old, new string) error {
_, err := t.renameMailbox.Exec(new, user, old) _, err := t.renameMailbox.Exec(old, new)
return err return err
} }
func (t *TableMailboxes) MailboxDelete(user, name string) error { func (t *TableMailboxes) MailboxDelete(name string) error {
_, err := t.deleteMailbox.Exec(user, name) _, err := t.deleteMailbox.Exec(name)
return err return err
} }
func (t *TableMailboxes) MailboxSubscribe(user, name string, subscribed bool) error { func (t *TableMailboxes) MailboxSubscribe(name string, subscribed bool) error {
sn := 1 sn := 1
if !subscribed { if !subscribed {
sn = 0 sn = 0
} }
_, err := t.subscribeMailbox.Exec(sn, user, name) _, err := t.subscribeMailbox.Exec(sn, name)
return err return err
} }

View File

@@ -23,7 +23,6 @@ type TableMails struct {
const mailsSchema = ` const mailsSchema = `
CREATE TABLE IF NOT EXISTS mails ( CREATE TABLE IF NOT EXISTS mails (
username TEXT NOT NULL,
mailbox TEXT NOT NULL, mailbox TEXT NOT NULL,
id INTEGER NOT NULL DEFAULT 1, id INTEGER NOT NULL DEFAULT 1,
mail BLOB NOT NULL, mail BLOB NOT NULL,
@@ -32,72 +31,72 @@ const mailsSchema = `
answered BOOLEAN NOT NULL DEFAULT 0, -- the mail has been replied to 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 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 deleted BOOLEAN NOT NULL DEFAULT 0, -- the email is marked for deletion at next EXPUNGE
PRIMARY KEY(username, mailbox, id), PRIMARY KEY (mailbox, id),
FOREIGN KEY (username, mailbox) REFERENCES mailboxes(username, mailbox) ON DELETE CASCADE ON UPDATE CASCADE FOREIGN KEY (mailbox) REFERENCES mailboxes(mailbox) ON DELETE CASCADE ON UPDATE CASCADE
); );
DROP VIEW IF EXISTS inboxes; DROP VIEW IF EXISTS inboxes;
CREATE VIEW IF NOT EXISTS inboxes AS SELECT * FROM ( 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 = ` const selectMailsStmt = `
SELECT * FROM inboxes SELECT * FROM inboxes
ORDER BY username, mailbox, id ORDER BY mailbox, id
` `
const selectMailStmt = ` const selectMailStmt = `
SELECT seq, id, mail, datetime, seen, answered, flagged, deleted FROM inboxes SELECT seq, id, mail, datetime, seen, answered, flagged, deleted FROM inboxes
WHERE username = $1 AND mailbox = $2 AND id = $3 WHERE mailbox = $1 AND id = $2
ORDER BY username, mailbox, id ORDER BY mailbox, id
` `
const selectMailCountStmt = ` const selectMailCountStmt = `
SELECT COUNT(*) FROM mails WHERE username = $1 AND mailbox = $2 SELECT COUNT(*) FROM mails WHERE mailbox = $1
` `
const selectMailUnseenStmt = ` 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 = ` const searchMailStmt = `
SELECT id FROM mails SELECT id FROM mails
WHERE username = $1 AND mailbox = $2 WHERE mailbox = $1
ORDER BY username, mailbox, id ORDER BY mailbox, id
` `
const insertMailStmt = ` const insertMailStmt = `
INSERT INTO mails (username, mailbox, id, mail, datetime) VALUES( INSERT INTO mails (mailbox, id, mail, datetime) VALUES(
$1, $2, ( $1, (
SELECT IFNULL(MAX(id)+1,1) AS id FROM mails SELECT IFNULL(MAX(id)+1,1) AS id FROM mails
WHERE username = $1 AND mailbox = $2 WHERE mailbox = $1
), $3, $4 ), $2, $3
) )
RETURNING id; RETURNING id;
` `
const selectIDForSeqStmt = ` const selectIDForSeqStmt = `
SELECT id FROM inboxes SELECT id FROM inboxes
WHERE username = $1 AND mailbox = $2 AND seq = $3 WHERE mailbox = $1 AND seq = $2
` `
const selectMailNextID = ` const selectMailNextID = `
SELECT IFNULL(MAX(id)+1,1) AS id FROM mails SELECT IFNULL(MAX(id)+1,1) AS id FROM mails
WHERE username = $1 AND mailbox = $2 WHERE mailbox = $1
` `
const updateMailFlagsStmt = ` 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 = ` 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 = ` 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) { func NewTableMails(db *sql.DB) (*TableMails, error) {
@@ -155,24 +154,24 @@ func NewTableMails(db *sql.DB) (*TableMails, error) {
return t, nil 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 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 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 data []byte
var seen, answered, flagged, deleted bool var seen, answered, flagged, deleted bool
var ts int64 var ts int64
var seq, pid int 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 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 var ids []uint32
rows, err := t.searchMail.Query(user, mailbox) rows, err := t.searchMail.Query(mailbox)
if err != nil { if err != nil {
return nil, fmt.Errorf("t.searchMail.Query: %w", err) 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 return ids, nil
} }
func (t *TableMails) MailNextID(user, mailbox string) (int, error) { func (t *TableMails) MailNextID(mailbox string) (int, error) {
var id int var id int
err := t.selectMailNextID.QueryRow(user, mailbox).Scan(&id) err := t.selectMailNextID.QueryRow(mailbox).Scan(&id)
return id, err 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 var id int
err := t.selectIDForSeq.QueryRow(user, mailbox, seq).Scan(&id) err := t.selectIDForSeq.QueryRow(mailbox, seq).Scan(&id)
return id, err return id, err
} }
func (t *TableMails) MailUnseen(user, mailbox string) (int, error) { func (t *TableMails) MailUnseen(mailbox string) (int, error) {
var unseen int var unseen int
err := t.countUnseenMails.QueryRow(user, mailbox).Scan(&unseen) err := t.countUnseenMails.QueryRow(mailbox).Scan(&unseen)
return unseen, err return unseen, err
} }
func (t *TableMails) MailUpdateFlags(user, mailbox string, id int, seen, answered, flagged, deleted bool) error { func (t *TableMails) MailUpdateFlags(mailbox string, id int, seen, answered, flagged, deleted bool) error {
_, err := t.updateMailFlags.Exec(seen, answered, flagged, deleted, user, mailbox, id) _, err := t.updateMailFlags.Exec(seen, answered, flagged, deleted, mailbox, id)
return err return err
} }
func (t *TableMails) MailDelete(user, mailbox, id string) error { func (t *TableMails) MailDelete(mailbox, id string) error {
_, err := t.deleteMail.Exec(user, mailbox, id) _, err := t.deleteMail.Exec(mailbox, id)
return err return err
} }
func (t *TableMails) MailExpunge(user, mailbox string) error { func (t *TableMails) MailExpunge(mailbox string) error {
_, err := t.expungeMail.Exec(user, mailbox) _, err := t.expungeMail.Exec(mailbox)
return err return err
} }
func (t *TableMails) MailCount(user, mailbox string) (int, error) { func (t *TableMails) MailCount(mailbox string) (int, error) {
var count int var count int
err := t.countMails.QueryRow(user, mailbox).Scan(&count) err := t.countMails.QueryRow(mailbox).Scan(&count)
return count, err return count, err
} }

View File

@@ -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
}

View File

@@ -5,24 +5,24 @@ import "time"
type Storage interface { type Storage interface {
ConfigGet(key string) (string, error) ConfigGet(key string) (string, error)
ConfigSet(key, value 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) MailCreate(mailbox string, data []byte) (int, error)
MailNextID(user, mailbox string) (int, error) MailSelect(mailbox string, id int) (int, int, []byte, bool, bool, bool, bool, time.Time, error)
MailIDForSeq(user, mailbox string, id int) (int, error) MailSearch(mailbox string) ([]uint32, error)
MailUnseen(user, mailbox string) (int, error) MailUpdateFlags(mailbox string, id int, seen, answered, flagged, deleted bool) error
MailboxList(user string, onlySubscribed bool) ([]string, error) MailDelete(mailbox, id string) error
MailboxCreate(user, name string) error MailExpunge(mailbox string) error
MailboxRename(user, old, new string) error MailCount(mailbox string) (int, 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)
} }

View File

@@ -1,19 +1,35 @@
package utils package utils
import ( import (
"crypto/ed25519"
"fmt" "fmt"
"strings" "strings"
"github.com/jxskiss/base62"
) )
const TLD = ".yggmail" const Domain = "yggmail"
func ParseAddress(email string) (string, string, error) { func CreateAddress(pk ed25519.PublicKey) string {
if !strings.HasSuffix(email, TLD) { return fmt.Sprintf(
return "", "", fmt.Errorf("invalid TLD") "%s@%s",
pk, Domain,
)
} }
func ParseAddress(email string) (ed25519.PublicKey, error) {
at := strings.LastIndex(email, "@") at := strings.LastIndex(email, "@")
if at == 0 { 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
} }