package main import ( "errors" "github.com/emersion/go-imap/v2" "github.com/emersion/go-imap/v2/imapserver" "github.com/google/uuid" "sort" "strings" ) const mailboxDelim rune = '/' type User struct { server *Server // may be nil if sub is not set sub uuid.UUID } func (u *User) Login(_, token string) error { sub, err := Authenticate(token, u.server.config) if err != nil { return err } u.sub = sub _, err = u.mailbox("INBOX") if err != nil { if errors.Is(err, ErrNoSuchMailbox) { err := u.Create("INBOX", nil) if err != nil { return err } } else { return err } } return nil } func (u *User) mailbox(name string) (*Mailbox, error) { return loadMbox(name, u) } func (u *User) Status(name string, options *imap.StatusOptions) (*imap.StatusData, error) { mbox, err := u.mailbox(name) if err != nil { return nil, err } return mbox.StatusData(options), nil } func (u *User) List(w *imapserver.ListWriter, ref string, patterns []string, options *imap.ListOptions) error { // TODO: fail if ref doesn't exist if len(patterns) == 0 { return w.WriteList(&imap.ListData{ Attrs: []imap.MailboxAttr{imap.MailboxAttrNoSelect}, Delim: mailboxDelim, }) } mailboxQuery, err := Database.DB.Query("SELECT id, mailbox, subscribed FROM mailboxes WHERE owner = $1", u.sub[:]) if err != nil { return err } defer func() { err := mailboxQuery.Close() if err != nil { log("Failed to close rows: "+err.Error()+", resource leaks may occur", 1) } }() var l []imap.ListData for mailboxQuery.Next() { var id []byte var name string var subscribed bool err := mailboxQuery.Scan(&id, &name, &subscribed) if err != nil { return err } mbox, err := loadMboxRaw(subscribed, id, name, u) if err != nil { return err } match := false for _, pattern := range patterns { match = imapserver.MatchList(name, mailboxDelim, ref, pattern) if match { break } } if !match { continue } data, err := mbox.list(options) if err != nil { return err } if data != nil { l = append(l, *data) } } sort.Slice(l, func(i, j int) bool { return l[i].Mailbox < l[j].Mailbox }) for _, data := range l { err := w.WriteList(&data) if err != nil { return err } } return nil } func (u *User) Create(name string, _ *imap.CreateOptions) error { name = strings.TrimRight(name, string(mailboxDelim)) err := u.NameNotExists(name) if err != nil { return err } newUUID := uuid.New() err = CreateMailbox(newUUID, name, u) if err != nil { return err } return nil } func (u *User) Delete(name string) error { mbox, err := u.mailbox(name) if err != nil { return err } err = mbox.Delete() if err != nil { return err } return nil } func (u *User) NameNotExists(name string) error { rows, err := Database.DB.Query("SELECT 1 FROM mailboxes WHERE mailbox = $1", name) if err != nil { return err } defer func() { err := rows.Close() if err != nil { log("Failed to close rows: "+err.Error()+", resource leaks may occur", 1) } }() if !rows.Next() { return nil } else { return &imap.Error{ Type: imap.StatusResponseTypeNo, Code: imap.ResponseCodeAlreadyExists, Text: "Mailbox already exists", } } } func (u *User) Rename(oldName, newName string) error { newName = strings.TrimRight(newName, string(mailboxDelim)) mbox, err := u.mailbox(oldName) if err != nil { return err } err = u.NameNotExists(newName) if err != nil { return err } err = mbox.rename(newName) if err != nil { return err } return nil } func (u *User) Subscribe(name string) error { mbox, err := u.mailbox(name) if err != nil { return err } err = mbox.SetSubscribed(true) if err != nil { return err } return nil } func (u *User) Unsubscribe(name string) error { mbox, err := u.mailbox(name) if err != nil { return err } err = mbox.SetSubscribed(false) if err != nil { return err } return nil } func (u *User) Namespace() (*imap.NamespaceData, error) { return &imap.NamespaceData{ Personal: []imap.NamespaceDescriptor{{Delim: mailboxDelim}}, }, nil }