mirror of
https://github.com/neilalexander/yggmail.git
synced 2026-05-04 11:06:29 +03:00
Single user refactor
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user