This commit is contained in:
2026-05-21 20:27:58 +00:00
parent 0d2615a9fd
commit 5a127bf2a2
38 changed files with 8644 additions and 0 deletions
+80
View File
@@ -0,0 +1,80 @@
// Package imap provides an IMAP4rev2 server backed by the encrypted SQLite store.
package imap
import (
"context"
"crypto/tls"
"fmt"
"log"
"net"
"time"
"github.com/emersion/go-imap/v2"
"github.com/emersion/go-imap/v2/imapserver"
"ghb.freebede.com/nahakubuilder/mailgosend/internal/auth"
"ghb.freebede.com/nahakubuilder/mailgosend/internal/config"
appCrypto "ghb.freebede.com/nahakubuilder/mailgosend/internal/crypto"
"ghb.freebede.com/nahakubuilder/mailgosend/internal/db"
)
// Deps groups dependencies needed by the IMAP server.
type Deps struct {
DB *db.DB
Crypt *appCrypto.Crypto
Brute *auth.BruteGuard
Cfg *config.Config
}
// NewServer creates an IMAP4rev2 server for port 143 (STARTTLS).
func NewServer(d *Deps, tlsCfg *tls.Config) *imapserver.Server {
return imapserver.New(&imapserver.Options{
NewSession: func(c *imapserver.Conn) (imapserver.Session, *imapserver.GreetingData, error) {
clientIP := connRemoteIP(c)
if d.Brute != nil && d.Cfg.BruteMaxTries > 0 {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
if banned, err := d.Brute.IsBanned(ctx, clientIP); err == nil && banned {
cancel()
return nil, nil, fmt.Errorf("connection refused")
}
cancel()
}
return &IMAPSession{deps: d, clientIP: clientIP}, &imapserver.GreetingData{}, nil
},
Caps: imap.CapSet{
imap.CapIMAP4rev2: {},
imap.CapIMAP4rev1: {},
},
TLSConfig: tlsCfg,
InsecureAuth: tlsCfg == nil,
})
}
// ListenAndServe listens on addr (port 143 / STARTTLS).
func ListenAndServe(s *imapserver.Server, addr, name string) error {
ln, err := net.Listen("tcp", addr)
if err != nil {
return fmt.Errorf("%s listen %s: %w", name, addr, err)
}
log.Printf("[%s] listening on %s", name, addr)
return s.Serve(ln)
}
// ListenAndServeTLS listens with implicit TLS (port 993 / IMAPS).
func ListenAndServeTLS(s *imapserver.Server, addr, name string) error {
log.Printf("[%s] listening on %s (TLS)", name, addr)
return s.ListenAndServeTLS(addr)
}
// connRemoteIP returns the client IP from an imapserver.Conn.
func connRemoteIP(c *imapserver.Conn) string {
if c == nil {
return ""
}
nc := c.NetConn()
if nc == nil {
return ""
}
host, _, _ := net.SplitHostPort(nc.RemoteAddr().String())
return host
}
+884
View File
@@ -0,0 +1,884 @@
package imap
import (
"bufio"
"bytes"
"context"
"fmt"
"log"
"sort"
"strings"
"time"
"github.com/emersion/go-imap/v2"
"github.com/emersion/go-imap/v2/imapserver"
"github.com/emersion/go-message/textproto"
"ghb.freebede.com/nahakubuilder/mailgosend/internal/crypto"
"ghb.freebede.com/nahakubuilder/mailgosend/internal/db"
"ghb.freebede.com/nahakubuilder/mailgosend/internal/models"
)
const mailboxDelim rune = '/'
// msgEntry holds the in-memory descriptor for one message in the selected mailbox.
type msgEntry struct {
dbID int64
uid imap.UID
isRead bool
isStarred bool
isDraft bool
isDeleted bool
extraFlags string
size int64
internalDate time.Time
}
func (e *msgEntry) flagList() []imap.Flag {
var flags []imap.Flag
if e.isRead {
flags = append(flags, imap.FlagSeen)
}
if e.isStarred {
flags = append(flags, imap.FlagFlagged)
}
if e.isDraft {
flags = append(flags, imap.FlagDraft)
}
if e.isDeleted {
flags = append(flags, imap.FlagDeleted)
}
for _, f := range strings.Fields(e.extraFlags) {
flags = append(flags, imap.Flag(f))
}
return flags
}
// IMAPSession implements imapserver.SessionIMAP4rev2.
type IMAPSession struct {
deps *Deps
clientIP string
user *models.User // set after Login
selectedMailbox *models.Mailbox
msgs []msgEntry // index+1 = IMAP sequence number
mboxTracker *imapserver.MailboxTracker
sessionTracker *imapserver.SessionTracker
}
var _ imapserver.SessionIMAP4rev2 = (*IMAPSession)(nil)
// ---- Authentication ----
func (s *IMAPSession) Close() error {
if s.sessionTracker != nil {
s.sessionTracker.Close()
}
return nil
}
func (s *IMAPSession) Login(username, password string) error {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
user, err := s.deps.DB.GetUserByEmail(ctx, username)
if err != nil {
log.Printf("[imap] login error %s: %v", username, err)
return imapserver.ErrAuthFailed
}
if user == nil || !user.Enabled {
s.recordAttempt(ctx, username, false)
return imapserver.ErrAuthFailed
}
if err := crypto.CheckPassword(user.PasswordHash, password); err != nil {
log.Printf("[imap] auth failed %s from %s", username, s.clientIP)
s.recordAttempt(ctx, username, false)
return imapserver.ErrAuthFailed
}
s.user = user
s.deps.DB.UpdateLastLogin(ctx, user.ID) //nolint:errcheck
s.recordAttempt(ctx, username, true)
log.Printf("[imap] auth OK %s from %s", username, s.clientIP)
return nil
}
func (s *IMAPSession) recordAttempt(ctx context.Context, email string, success bool) {
if s.deps.Brute != nil {
s.deps.Brute.RecordAttempt(ctx, s.clientIP, email, success) //nolint:errcheck
}
}
// ---- Mailbox management ----
func (s *IMAPSession) Select(mailboxName string, options *imap.SelectOptions) (*imap.SelectData, error) {
s.doUnselect()
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
mbox, err := s.deps.DB.GetMailbox(ctx, s.user.ID, mailboxName)
if err != nil {
return nil, imapErr(err)
}
if mbox == nil {
return nil, noSuchMailbox()
}
msgs, err := s.deps.DB.ListIMAPMessages(ctx, mbox.ID)
if err != nil {
return nil, imapErr(err)
}
s.selectedMailbox = mbox
s.msgs = make([]msgEntry, 0, len(msgs))
for _, m := range msgs {
s.msgs = append(s.msgs, msgEntry{
dbID: m.ID,
uid: imap.UID(m.UID),
isRead: m.IsRead,
isStarred: m.IsStarred,
isDraft: m.IsDraft,
extraFlags: m.Flags,
size: m.SizeBytes,
internalDate: m.ReceivedAt,
})
}
s.mboxTracker = imapserver.NewMailboxTracker(uint32(len(s.msgs)))
s.sessionTracker = s.mboxTracker.NewSession()
var firstUnseen uint32
for i, m := range s.msgs {
if !m.isRead {
firstUnseen = uint32(i + 1)
break
}
}
return &imap.SelectData{
Flags: allFlags(),
PermanentFlags: allFlags(),
NumMessages: uint32(len(s.msgs)),
FirstUnseenSeqNum: firstUnseen,
UIDNext: imap.UID(mbox.UIDNext),
UIDValidity: mbox.UIDValidity,
List: &imap.ListData{
Mailbox: mailboxName,
Delim: mailboxDelim,
Attrs: mboxAttrs(mbox),
},
}, nil
}
func (s *IMAPSession) Unselect() error {
s.doUnselect()
return nil
}
func (s *IMAPSession) doUnselect() {
if s.sessionTracker != nil {
s.sessionTracker.Close()
s.sessionTracker = nil
}
s.selectedMailbox = nil
s.msgs = nil
s.mboxTracker = nil
}
func (s *IMAPSession) Create(mailboxName string, options *imap.CreateOptions) error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
existing, _ := s.deps.DB.GetMailbox(ctx, s.user.ID, mailboxName)
if existing != nil {
return &imap.Error{Type: imap.StatusResponseTypeNo, Code: imap.ResponseCodeAlreadyExists, Text: "Mailbox already exists"}
}
_, err := s.deps.DB.CreateMailbox(ctx, s.user.ID, mailboxName, "", nil)
return imapErr(err)
}
func (s *IMAPSession) Delete(mailboxName string) error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
mbox, _ := s.deps.DB.GetMailbox(ctx, s.user.ID, mailboxName)
if mbox == nil {
return noSuchMailbox()
}
if mbox.Type != "" {
return &imap.Error{Type: imap.StatusResponseTypeNo, Text: "Cannot delete system mailbox"}
}
_, err := s.deps.DB.SQL().ExecContext(ctx, "DELETE FROM mailboxes WHERE id=?", mbox.ID)
return imapErr(err)
}
func (s *IMAPSession) Rename(oldName, newName string, options *imap.RenameOptions) error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
mbox, _ := s.deps.DB.GetMailbox(ctx, s.user.ID, oldName)
if mbox == nil {
return noSuchMailbox()
}
return imapErr(s.deps.DB.RenameMailbox(ctx, mbox.ID, newName))
}
func (s *IMAPSession) Subscribe(mailboxName string) error {
return s.setSubscribed(mailboxName, true)
}
func (s *IMAPSession) Unsubscribe(mailboxName string) error {
return s.setSubscribed(mailboxName, false)
}
func (s *IMAPSession) setSubscribed(name string, sub bool) error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
mbox, _ := s.deps.DB.GetMailbox(ctx, s.user.ID, name)
if mbox == nil {
return noSuchMailbox()
}
return imapErr(s.deps.DB.SetMailboxSubscribed(ctx, mbox.ID, sub))
}
func (s *IMAPSession) List(w *imapserver.ListWriter, ref string, patterns []string, options *imap.ListOptions) error {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if len(patterns) == 0 {
return w.WriteList(&imap.ListData{
Attrs: []imap.MailboxAttr{imap.MailboxAttrNoSelect},
Delim: mailboxDelim,
})
}
mailboxes, err := s.deps.DB.ListMailboxes(ctx, s.user.ID)
if err != nil {
return imapErr(err)
}
type entry struct {
name string
data imap.ListData
}
var entries []entry
for _, mbox := range mailboxes {
if options.SelectSubscribed && !mbox.Subscribed {
continue
}
matched := false
for _, pat := range patterns {
if imapserver.MatchList(mbox.Name, mailboxDelim, ref, pat) {
matched = true
break
}
}
if !matched {
continue
}
data := imap.ListData{
Mailbox: mbox.Name,
Delim: mailboxDelim,
Attrs: mboxAttrs(mbox),
}
if mbox.Subscribed {
data.Attrs = append(data.Attrs, imap.MailboxAttrSubscribed)
}
if options.ReturnStatus != nil {
sd, _ := s.statusFor(ctx, mbox, options.ReturnStatus)
data.Status = sd
}
entries = append(entries, entry{name: mbox.Name, data: data})
}
sort.Slice(entries, func(i, j int) bool { return entries[i].name < entries[j].name })
for _, e := range entries {
if err := w.WriteList(&e.data); err != nil {
return err
}
}
return nil
}
func (s *IMAPSession) Status(mailboxName string, options *imap.StatusOptions) (*imap.StatusData, error) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
mbox, _ := s.deps.DB.GetMailbox(ctx, s.user.ID, mailboxName)
if mbox == nil {
return nil, noSuchMailbox()
}
return s.statusFor(ctx, mbox, options)
}
func (s *IMAPSession) statusFor(ctx context.Context, mbox *models.Mailbox, opts *imap.StatusOptions) (*imap.StatusData, error) {
data := &imap.StatusData{Mailbox: mbox.Name}
total, unseen, err := s.deps.DB.GetMailboxMessageCounts(ctx, mbox.ID)
if err != nil {
return nil, imapErr(err)
}
if opts.NumMessages {
n := uint32(total)
data.NumMessages = &n
}
if opts.NumUnseen {
n := uint32(unseen)
data.NumUnseen = &n
}
if opts.UIDNext {
data.UIDNext = imap.UID(mbox.UIDNext)
}
if opts.UIDValidity {
data.UIDValidity = mbox.UIDValidity
}
if opts.NumRecent {
n := uint32(0)
data.NumRecent = &n
}
if opts.NumDeleted {
n := uint32(0)
data.NumDeleted = &n
}
if opts.Size {
sz, _ := s.deps.DB.GetMailboxSize(ctx, mbox.ID)
data.Size = &sz
}
return data, nil
}
func (s *IMAPSession) Append(mailboxName string, r imap.LiteralReader, options *imap.AppendOptions) (*imap.AppendData, error) {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
mbox, _ := s.deps.DB.GetMailbox(ctx, s.user.ID, mailboxName)
if mbox == nil {
return nil, &imap.Error{Type: imap.StatusResponseTypeNo, Code: imap.ResponseCodeTryCreate, Text: "No such mailbox"}
}
raw, err := readLiteral(r, s.deps.Cfg.MaxMessageSize)
if err != nil {
return nil, imapErr(err)
}
key, err := s.deps.Crypt.DeriveKey("messages", s.user.ID)
if err != nil {
return nil, imapErr(fmt.Errorf("derive key: %w", err))
}
rawEnc, err := crypto.Encrypt(key, raw)
if err != nil {
return nil, imapErr(err)
}
uid, err := s.deps.DB.NextUID(ctx, mbox.ID)
if err != nil {
return nil, imapErr(err)
}
internalDate := time.Now().UTC()
isRead, isDraft := false, false
var extraParts []string
if options != nil {
if !options.Time.IsZero() {
internalDate = options.Time
}
for _, f := range options.Flags {
switch f {
case imap.FlagSeen:
isRead = true
case imap.FlagDraft:
isDraft = true
default:
extraParts = append(extraParts, string(f))
}
}
}
ins := &db.MessageInsert{
MailboxID: mbox.ID,
UID: uid,
SizeBytes: int64(len(raw)),
RawEnc: rawEnc,
IsRead: isRead,
IsDraft: isDraft,
Flags: strings.Join(extraParts, " "),
Date: internalDate,
}
if _, err := s.deps.DB.InsertMessage(ctx, ins); err != nil {
return nil, imapErr(err)
}
return &imap.AppendData{
UIDValidity: mbox.UIDValidity,
UID: imap.UID(uid),
}, nil
}
func (s *IMAPSession) Poll(w *imapserver.UpdateWriter, allowExpunge bool) error {
if s.sessionTracker == nil {
return nil
}
return s.sessionTracker.Poll(w, allowExpunge)
}
func (s *IMAPSession) Idle(w *imapserver.UpdateWriter, stop <-chan struct{}) error {
if s.sessionTracker == nil {
return nil
}
return s.sessionTracker.Idle(w, stop)
}
// ---- Selected-state operations ----
func (s *IMAPSession) Expunge(w *imapserver.ExpungeWriter, uids *imap.UIDSet) error {
if s.selectedMailbox == nil {
return noSelectedMailbox()
}
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
// Soft-delete entries marked \Deleted (filtered by UIDs if provided).
for i := range s.msgs {
m := &s.msgs[i]
if uids != nil && !uids.Contains(m.uid) {
continue
}
if m.isDeleted {
s.deps.DB.SoftDeleteMessage(ctx, m.dbID) //nolint:errcheck
}
}
deletedUIDs, err := s.deps.DB.HardDeleteMessages(ctx, s.selectedMailbox.ID)
if err != nil {
return imapErr(err)
}
deletedSet := make(map[imap.UID]struct{}, len(deletedUIDs))
for _, uid := range deletedUIDs {
deletedSet[imap.UID(uid)] = struct{}{}
}
// Collect seq nums to expunge (in ascending order, write in reverse).
var seqNums []uint32
for i, m := range s.msgs {
if _, ok := deletedSet[m.uid]; ok {
seqNums = append(seqNums, uint32(i+1))
}
}
for i := len(seqNums) - 1; i >= 0; i-- {
if err := w.WriteExpunge(seqNums[i]); err != nil {
return err
}
}
// Remove from in-memory list.
filtered := s.msgs[:0]
for _, m := range s.msgs {
if _, ok := deletedSet[m.uid]; !ok {
filtered = append(filtered, m)
}
}
s.msgs = filtered
return nil
}
func (s *IMAPSession) Search(kind imapserver.NumKind, criteria *imap.SearchCriteria, options *imap.SearchOptions) (*imap.SearchData, error) {
if s.selectedMailbox == nil {
return nil, noSelectedMailbox()
}
if kind == imapserver.NumKindUID {
var result imap.UIDSet
for i, m := range s.msgs {
seqNum := uint32(i + 1)
if s.matchCriteria(seqNum, &m, criteria) {
result.AddNum(m.uid)
}
}
return &imap.SearchData{All: result}, nil
}
var result imap.SeqSet
for i, m := range s.msgs {
seqNum := uint32(i + 1)
if s.matchCriteria(seqNum, &m, criteria) {
result.AddNum(seqNum)
}
}
return &imap.SearchData{All: result}, nil
}
func (s *IMAPSession) matchCriteria(seqNum uint32, m *msgEntry, c *imap.SearchCriteria) bool {
if c == nil {
return true
}
for _, seqSet := range c.SeqNum {
if !seqSet.Contains(seqNum) {
return false
}
}
for _, uidSet := range c.UID {
if !uidSet.Contains(m.uid) {
return false
}
}
flagSet := make(map[imap.Flag]struct{})
for _, f := range m.flagList() {
flagSet[f] = struct{}{}
}
for _, f := range c.Flag {
if _, ok := flagSet[f]; !ok {
return false
}
}
for _, f := range c.NotFlag {
if _, ok := flagSet[f]; ok {
return false
}
}
if c.Larger != 0 && m.size <= c.Larger {
return false
}
if c.Smaller != 0 && m.size >= c.Smaller {
return false
}
if !matchDate(m.internalDate, c.Since, c.Before) {
return false
}
for _, sub := range c.Not {
if s.matchCriteria(seqNum, m, &sub) {
return false
}
}
for _, or := range c.Or {
if !s.matchCriteria(seqNum, m, &or[0]) && !s.matchCriteria(seqNum, m, &or[1]) {
return false
}
}
return true
}
func (s *IMAPSession) Fetch(w *imapserver.FetchWriter, numSet imap.NumSet, options *imap.FetchOptions) error {
if s.selectedMailbox == nil {
return noSelectedMailbox()
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
key, err := s.deps.Crypt.DeriveKey("messages", s.user.ID)
if err != nil {
return imapErr(err)
}
for i := range s.msgs {
m := &s.msgs[i]
seqNum := uint32(i + 1)
if !numSetContains(numSet, seqNum, m.uid) {
continue
}
rw := w.CreateMessage(seqNum)
rw.WriteUID(m.uid)
if options.Flags {
rw.WriteFlags(m.flagList())
}
if options.InternalDate {
rw.WriteInternalDate(m.internalDate)
}
if options.RFC822Size {
rw.WriteRFC822Size(m.size)
}
needRaw := options.Envelope || options.BodyStructure != nil ||
len(options.BodySection) > 0 || len(options.BinarySection) > 0 ||
len(options.BinarySectionSize) > 0
if needRaw {
rawEnc, err := s.deps.DB.GetMessageRaw(ctx, m.dbID)
if err != nil || rawEnc == nil {
rw.Close() //nolint:errcheck
continue
}
raw, err := crypto.Decrypt(key, rawEnc)
if err != nil {
log.Printf("[imap] decrypt %d: %v", m.dbID, err)
rw.Close() //nolint:errcheck
continue
}
if options.Envelope {
if env := extractEnvelope(raw); env != nil {
rw.WriteEnvelope(env)
}
}
if options.BodyStructure != nil {
rw.WriteBodyStructure(imapserver.ExtractBodyStructure(bytes.NewReader(raw)))
}
for _, bs := range options.BodySection {
buf := imapserver.ExtractBodySection(bytes.NewReader(raw), bs)
wc := rw.WriteBodySection(bs, int64(len(buf)))
wc.Write(buf) //nolint:errcheck
wc.Close() //nolint:errcheck
}
for _, bs := range options.BinarySection {
buf := imapserver.ExtractBinarySection(bytes.NewReader(raw), bs)
wc := rw.WriteBinarySection(bs, int64(len(buf)))
wc.Write(buf) //nolint:errcheck
wc.Close() //nolint:errcheck
}
for _, bss := range options.BinarySectionSize {
n := imapserver.ExtractBinarySectionSize(bytes.NewReader(raw), bss)
rw.WriteBinarySectionSize(bss, n)
}
}
if err := rw.Close(); err != nil {
return err
}
}
return nil
}
func (s *IMAPSession) Store(w *imapserver.FetchWriter, numSet imap.NumSet, flags *imap.StoreFlags, options *imap.StoreOptions) error {
if s.selectedMailbox == nil {
return noSelectedMailbox()
}
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
for i := range s.msgs {
m := &s.msgs[i]
seqNum := uint32(i + 1)
if !numSetContains(numSet, seqNum, m.uid) {
continue
}
applyStoreFlags(m, flags)
s.deps.DB.SetMessageFlags(ctx, m.dbID, m.isRead, m.isStarred, m.isDraft, m.extraFlags) //nolint:errcheck
if m.isDeleted {
s.deps.DB.SoftDeleteMessage(ctx, m.dbID) //nolint:errcheck
}
if !flags.Silent {
rw := w.CreateMessage(seqNum)
rw.WriteUID(m.uid)
rw.WriteFlags(m.flagList())
if err := rw.Close(); err != nil {
return err
}
}
}
return nil
}
func (s *IMAPSession) Copy(numSet imap.NumSet, destName string) (*imap.CopyData, error) {
if s.selectedMailbox == nil {
return nil, noSelectedMailbox()
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
dest, _ := s.deps.DB.GetMailbox(ctx, s.user.ID, destName)
if dest == nil {
return nil, &imap.Error{Type: imap.StatusResponseTypeNo, Code: imap.ResponseCodeTryCreate, Text: "No such mailbox"}
}
var sourceUIDs, destUIDs imap.UIDSet
for i, m := range s.msgs {
seqNum := uint32(i + 1)
if !numSetContains(numSet, seqNum, m.uid) {
continue
}
newUID, err := s.deps.DB.CopyMessageToMailbox(ctx, m.dbID, dest.ID, s.user.ID)
if err != nil {
log.Printf("[imap] copy msg %d: %v", m.dbID, err)
continue
}
sourceUIDs.AddNum(m.uid)
destUIDs.AddNum(imap.UID(newUID))
}
return &imap.CopyData{
UIDValidity: dest.UIDValidity,
SourceUIDs: sourceUIDs,
DestUIDs: destUIDs,
}, nil
}
func (s *IMAPSession) Move(w *imapserver.MoveWriter, numSet imap.NumSet, destName string) error {
copyData, err := s.Copy(numSet, destName)
if err != nil {
return err
}
if err := w.WriteCopyData(copyData); err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
var seqNums []uint32
for i := range s.msgs {
m := &s.msgs[i]
seqNum := uint32(i + 1)
if !numSetContains(numSet, seqNum, m.uid) {
continue
}
s.deps.DB.SoftDeleteMessage(ctx, m.dbID) //nolint:errcheck
seqNums = append(seqNums, seqNum)
}
s.deps.DB.HardDeleteMessages(ctx, s.selectedMailbox.ID) //nolint:errcheck
for i := len(seqNums) - 1; i >= 0; i-- {
if err := w.WriteExpunge(seqNums[i]); err != nil {
return err
}
}
// Remove moved messages.
kept := s.msgs[:0]
for i, m := range s.msgs {
seqNum := uint32(i + 1)
if !numSetContains(numSet, seqNum, m.uid) {
kept = append(kept, m)
}
}
s.msgs = kept
return nil
}
func (s *IMAPSession) Namespace() (*imap.NamespaceData, error) {
return &imap.NamespaceData{
Personal: []imap.NamespaceDescriptor{{Delim: mailboxDelim}},
}, nil
}
// ---- Helpers ----
// numSetContains checks seqNum (for SeqSet) or uid (for UIDSet).
func numSetContains(numSet imap.NumSet, seqNum uint32, uid imap.UID) bool {
switch ns := numSet.(type) {
case imap.SeqSet:
return ns.Contains(seqNum)
case imap.UIDSet:
return ns.Contains(uid)
}
return false
}
func applyStoreFlags(m *msgEntry, store *imap.StoreFlags) {
flagMap := map[imap.Flag]*bool{
imap.FlagSeen: &m.isRead,
imap.FlagFlagged: &m.isStarred,
imap.FlagDraft: &m.isDraft,
imap.FlagDeleted: &m.isDeleted,
}
switch store.Op {
case imap.StoreFlagsSet:
m.isRead, m.isStarred, m.isDraft, m.isDeleted = false, false, false, false
m.extraFlags = ""
for _, f := range store.Flags {
if ptr, ok := flagMap[f]; ok {
*ptr = true
} else {
m.extraFlags = strings.TrimSpace(m.extraFlags + " " + string(f))
}
}
case imap.StoreFlagsAdd:
for _, f := range store.Flags {
if ptr, ok := flagMap[f]; ok {
*ptr = true
} else if !strings.Contains(m.extraFlags, string(f)) {
m.extraFlags = strings.TrimSpace(m.extraFlags + " " + string(f))
}
}
case imap.StoreFlagsDel:
for _, f := range store.Flags {
if ptr, ok := flagMap[f]; ok {
*ptr = false
} else {
m.extraFlags = strings.TrimSpace(strings.ReplaceAll(m.extraFlags, string(f), ""))
}
}
}
}
func allFlags() []imap.Flag {
return []imap.Flag{
imap.FlagSeen,
imap.FlagAnswered,
imap.FlagFlagged,
imap.FlagDeleted,
imap.FlagDraft,
}
}
func mboxAttrs(mbox *models.Mailbox) []imap.MailboxAttr {
su := db.MailboxTypeToSpecialUse(mbox.Type)
if su != "" {
return []imap.MailboxAttr{imap.MailboxAttr(su)}
}
return nil
}
func extractEnvelope(raw []byte) *imap.Envelope {
br := bufio.NewReader(bytes.NewReader(raw))
header, err := textproto.ReadHeader(br)
if err != nil {
return nil
}
return imapserver.ExtractEnvelope(header)
}
func matchDate(t, since, before time.Time) bool {
t = time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, time.UTC)
if !since.IsZero() && t.Before(since) {
return false
}
if !before.IsZero() && !t.Before(before) {
return false
}
return true
}
func readLiteral(r imap.LiteralReader, maxSize int64) ([]byte, error) {
buf := make([]byte, 0, 4096)
tmp := make([]byte, 32768)
var total int64
for {
n, err := r.Read(tmp)
if n > 0 {
total += int64(n)
if total > maxSize {
return nil, fmt.Errorf("message too large")
}
buf = append(buf, tmp[:n]...)
}
if err != nil {
break
}
}
return buf, nil
}
func imapErr(err error) error {
if err == nil {
return nil
}
if _, ok := err.(*imap.Error); ok {
return err
}
return &imap.Error{Type: imap.StatusResponseTypeNo, Text: "server error"}
}
func noSuchMailbox() error {
return &imap.Error{Type: imap.StatusResponseTypeNo, Code: imap.ResponseCodeNonExistent, Text: "No such mailbox"}
}
func noSelectedMailbox() error {
return &imap.Error{Type: imap.StatusResponseTypeBad, Text: "no mailbox selected"}
}