internal/imap/handler.go (view raw)
1package imap
2
3import (
4 "context"
5 "database/sql"
6 "errors"
7 "fmt"
8 "log/slog"
9 "math"
10 "slices"
11 "strings"
12 "time"
13
14 "postern/internal/db"
15 "postern/internal/model"
16 "postern/internal/persistence"
17 "postern/internal/policy"
18
19 "github.com/emersion/go-imap/v2"
20 "github.com/emersion/go-imap/v2/imapserver"
21 "github.com/google/uuid"
22)
23
24type imapSession struct {
25 db *db.DB
26 conn *imapserver.Conn
27 mTracker *mailboxTrackerRegistry
28 persistence *persistence.Persistence
29 searchRes imap.NumSet // RFC 9051 6.4.4.1
30 sessionTracker *imapserver.SessionTracker
31 selectedMailbox model.Mailbox
32 selectedMailboxReadOnly bool
33 user model.User
34 sessionID string
35 policyEngine *policy.Engine
36}
37
38func (i *Server) handleIMAPConn(conn *imapserver.Conn) (imapserver.Session, *imapserver.GreetingData, error) {
39 sessionID := uuid.NewString()
40 slog.Info("handle new IMAP connection", "remote_ip", conn.NetConn().RemoteAddr().String(), "session_id", sessionID)
41 return &imapSession{
42 db: i.db,
43 conn: conn,
44 selectedMailbox: model.Mailbox{},
45 mTracker: i.mTracker,
46 persistence: i.persistence,
47 sessionTracker: nil,
48 sessionID: sessionID,
49 policyEngine: i.policyEngine,
50 }, &imapserver.GreetingData{}, nil
51}
52
53func (m *imapSession) Close() error {
54 slog.Info("close IMAP session", "session_id", m.sessionID)
55 // Package calls EXPUNGE internally, which leads to a bug in which
56 // CLOSE emits an error instead of silently closing for read-only SELECTs
57 if m.sessionTracker != nil {
58 m.sessionTracker.Close()
59 }
60 m.selectedMailbox = model.Mailbox{}
61 return nil
62}
63
64func (m *imapSession) Login(username, password string) error {
65 logger := slog.With("session_id", m.sessionID, "cmd", "LOGIN", "username", username)
66 u, err := m.db.GetUser(context.Background(), username)
67 if err != nil {
68 if errors.Is(err, sql.ErrNoRows) {
69 logger.Info("user not found")
70 } else {
71 logger.Error("getting user", "error", err)
72 }
73 return imapserver.ErrAuthFailed
74 }
75
76 err = u.VerifyPassword([]byte(password))
77 if err != nil {
78 slog.Info("invalid password", "username", username)
79 return imapserver.ErrAuthFailed
80 }
81
82 logger.Info("logged in", "user_id", u.ID)
83 m.user = u
84 return nil
85
86}
87
88// Select and Examine are identical besides read-only (https://www.ietf.org/rfc/rfc9051.html#name-examine-command)
89func (m *imapSession) Select(mailboxName string, selectOpt *imap.SelectOptions) (*imap.SelectData, error) {
90 logger := slog.With("session_id", m.sessionID, "cmd", "SELECT", "user_id", m.user.ID)
91 // INBOX is case-insensitive
92 if strings.EqualFold(mailboxName, string(model.MailboxINBOX)) {
93 mailboxName = string(model.MailboxINBOX)
94 }
95
96 logger.Info("start SELECT", "mailbox", mailboxName, "read_only", selectOpt.ReadOnly)
97
98 userMailbox, err := m.db.GetUserMailbox(context.Background(), m.user.ID, mailboxName)
99 if err != nil {
100 if errors.Is(err, db.ErrMailboxNotFound) {
101 return nil, errMailboxNotExist
102 }
103 logger.Error("getting mailbox", "error", err)
104 return nil, err
105 }
106
107 if m.selectedMailbox.ID != userMailbox.ID && m.sessionTracker != nil {
108 m.sessionTracker.Close()
109 m.sessionTracker = nil
110 }
111
112 m.sessionTracker = m.mTracker.getMailboxTracker(m.user.ID, userMailbox.ID, userMailbox.NumMessages).NewSession()
113 m.selectedMailbox = userMailbox
114 m.selectedMailboxReadOnly = selectOpt.ReadOnly
115
116 activeFlags, err := m.db.GetMailboxFlags(context.Background(), m.selectedMailbox.ID)
117 if err != nil {
118 logger.Error("getting mailbox flags", "error", err)
119 return nil, err
120 }
121
122 // RFC 9051: Upon successful completion of a SELECT or an EXAMINE command (after the tagged OK response),
123 // the current search result variable is reset to the empty sequence.
124 m.searchRes = imap.UIDSet{}
125
126 logger.Info("end SELECT")
127 return &imap.SelectData{
128 NumMessages: userMailbox.NumMessages,
129 NumRecent: 0, // (legacy, send 0)
130 UIDValidity: userMailbox.UIDValidity,
131 UIDNext: imap.UID(userMailbox.UIDNext),
132 PermanentFlags: []imap.Flag{
133 imap.FlagWildcard, imap.FlagFlagged, imap.FlagSeen,
134 imap.FlagDeleted, imap.FlagAnswered, imap.FlagDraft,
135 },
136 Flags: toIMAPFlags(activeFlags),
137 List: &imap.ListData{
138 Attrs: getMailboxAttr(userMailbox),
139 Delim: '/',
140 Mailbox: mailboxName,
141 Status: &imap.StatusData{
142 Mailbox: mailboxName,
143 NumMessages: new(userMailbox.NumMessages),
144 NumRecent: new(uint32), // obsolete
145 UIDNext: imap.UID(userMailbox.UIDNext),
146 UIDValidity: userMailbox.UIDValidity,
147 NumUnseen: new(uint32(userMailbox.NumUnseen)),
148 NumDeleted: new(uint32(userMailbox.NumDeleted)),
149 Size: new(userMailbox.Size),
150 AppendLimit: new(uint32),
151 DeletedStorage: new(int64),
152 HighestModSeq: uint64(userMailbox.HighestModSeq),
153 },
154 },
155 }, nil
156}
157
158func (m *imapSession) Create(mailbox string, _ *imap.CreateOptions) error {
159 logger := slog.With("session_id", m.sessionID, "cmd", "CREATE", "user_id", m.user.ID)
160 logger.Info("start CREATE", "mailbox", mailbox)
161 if strings.EqualFold(mailbox, string(model.MailboxINBOX)) {
162 logger.Info("creating INBOX not allowed")
163 return &imap.Error{
164 Type: imap.StatusResponseTypeNo,
165 Code: imap.ResponseCodeAlreadyExists,
166 Text: "Mailbox INBOX always exists",
167 }
168 }
169
170 // per RFC 9051, 7.3.1, all mailbox attributes are optional.
171 // Furthermore, they are user-specific in case of a shared mailbox (for example \Sent has per-user meaning).
172 // Ignore special attributes now because we have pre-defined mailboxes already, which should be un-deletable.
173
174 // mailbox should exist under the name without the trailing delimiter
175 mailbox, _ = strings.CutPrefix(mailbox, "/")
176 mailbox, _ = strings.CutSuffix(mailbox, "/")
177
178 // This can be a toctou, but we have a unique constraint on mailbox names
179 mbID, _, err := m.db.GetMailboxID(context.Background(), m.user.ID, mailbox)
180 if err != nil && !errors.Is(err, db.ErrMailboxNotFound) {
181 logger.Error("getting mailbox", "error", err)
182 return errInternalServerError
183 }
184 if mbID != 0 {
185 logger.Info("cannot create already existing mailbox", "mailbox", mailbox)
186 return &imap.Error{
187 Type: imap.StatusResponseTypeNo,
188 Code: imap.ResponseCodeNonExistent,
189 Text: "Mailbox already exists",
190 }
191 }
192
193 err = m.db.CreateMailbox(context.Background(), m.user.ID, mailbox)
194 if err != nil {
195 logger.Error("creating mailbox", "error", err)
196 return errInternalServerError
197 }
198 logger.Info("end CREATE")
199 return nil
200}
201
202func (m *imapSession) Delete(mailbox string) error {
203 logger := slog.With("session_id", m.sessionID, "cmd", "DELETE", "user_id", m.user.ID)
204 logger.Info("start DELETE", "mailbox", mailbox)
205 // We do not allow to delete default mailboxes because we need their special use present
206 for _, dmb := range model.DefaultMailboxes() {
207 if string(dmb.Name) == mailbox {
208 logger.Info("denied deleting default mailbox", "mailbox", mailbox)
209 return &imap.Error{
210 Type: imap.StatusResponseTypeNo,
211 Code: imap.ResponseCodeCannot,
212 Text: "Deleting preset mailbox is not allowed",
213 }
214 }
215 }
216
217 mailboxID, _, err := m.db.GetMailboxID(context.Background(), m.user.ID, mailbox)
218 if errors.Is(err, db.ErrMailboxNotFound) {
219 return errMailboxNotExist
220 }
221 if err != nil {
222 logger.Error("getting mailbox ID", "error", err)
223 return errInternalServerError
224 }
225
226 // deleting messages is left to the "prune" CLI and not part of this operation
227 err = m.db.DeleteMailbox(context.Background(), m.user.ID, mailbox)
228 if err != nil {
229 logger.Error("deleting mailbox", "error", err)
230 return errInternalServerError
231 }
232
233 m.mTracker.remove(m.user.ID, mailboxID)
234 logger.Info("end DELETE", "mailbox", mailbox)
235 return nil
236}
237
238func (m *imapSession) Rename(mailbox, newName string, _ *imap.RenameOptions) error {
239 logger := slog.With("session_id", m.sessionID, "cmd", "RENAME", "user_id", m.user.ID)
240 logger.Info("start RENAME", "mailbox", mailbox, "new_mailbox_name", newName)
241
242 if mailbox == string(model.MailboxINBOX) {
243 // Special case per RFC 6.3.6:
244 // Renaming INBOX is permitted and does not result in a tagged BAD response, and it has special behavior:
245 // It moves all messages in INBOX to a new mailbox with the given name, leaving INBOX empty. If the server
246 // implementation supports inferior hierarchical names of INBOX, these are unaffected by a rename of INBOX.
247 err := m.db.CreateAndMoveAllMessagesFromINBOXToMailbox(context.Background(), m.user.ID, newName)
248 if err != nil {
249 logger.Error("rename INBOX", "error", err)
250 return errInternalServerError
251 }
252
253 logger.Info("end RENAME", "mailbox", mailbox, "new_mailbox_name", newName)
254 return nil
255 }
256
257 // We do not allow to rename default mailboxes because we need their special use present
258 for _, dmb := range model.DefaultMailboxes() {
259 if string(dmb.Name) == mailbox {
260 logger.Info("denied renaming default mailbox", "mailbox", mailbox)
261 return &imap.Error{
262 Type: imap.StatusResponseTypeNo,
263 Code: imap.ResponseCodeCannot,
264 Text: "Renaming preset mailbox is not allowed",
265 }
266 }
267 }
268
269 // Check if mailbox exists
270 _, _, err := m.db.GetMailboxID(context.Background(), m.user.ID, mailbox)
271 if errors.Is(err, db.ErrMailboxNotFound) {
272 logger.Info("mailbox does not exist", "mailbox", mailbox)
273 return errMailboxNotExist
274 }
275 if err != nil {
276 logger.Error("getting mailbox ID", "error", err)
277 return errInternalServerError
278 }
279
280 // Check if new mailbox name already exists
281 newMailboxID, _, err := m.db.GetMailboxID(context.Background(), m.user.ID, newName)
282 if newMailboxID != 0 {
283 slog.Info("new mailbox name already exists", "new_mailbox_name", newMailboxID)
284 return &imap.Error{
285 Type: imap.StatusResponseTypeNo,
286 Code: imap.ResponseCodeAlreadyExists,
287 Text: "Mailbox with the same name already exists",
288 }
289 }
290 if err != nil && !errors.Is(err, db.ErrMailboxNotFound) {
291 logger.Error("getting mailbox ID", "error", err)
292 return errInternalServerError
293 }
294
295 err = m.db.RenameMailbox(context.Background(), m.user.ID, mailbox, newName)
296 if err != nil {
297 logger.Error("renaming mailbox", "error", err)
298 return errInternalServerError
299 }
300 logger.Info("end RENAME", "mailbox", mailbox, "new_mailbox_name", newName)
301 return nil
302}
303
304func (m *imapSession) Subscribe(mb string) error {
305 logger := slog.With("session_id", m.sessionID, "cmd", "SUBSCRIBE", "user_id", m.user.ID)
306 logger.Info("start SUBSCRIBE", "mailbox", mb)
307 mailboxID, _, err := m.db.GetMailboxID(context.Background(), m.user.ID, mb)
308 if err != nil {
309 if errors.Is(err, db.ErrMailboxNotFound) {
310 return errMailboxNotExist
311 }
312 logger.Error("getting mailbox ID", "error", err)
313 return errInternalServerError
314 }
315 err = m.db.Subscribe(context.Background(), m.user.ID, mailboxID, mb)
316 if err != nil {
317 logger.Error("subscribe mailbox", "error", err)
318 return errInternalServerError
319 }
320 logger.Info("end SUBSCRIBE", "mailbox", mb)
321 return nil
322}
323
324func (m *imapSession) Unsubscribe(mb string) error {
325 logger := slog.With("session_id", m.sessionID, "cmd", "UNSUBSCRIBE", "user_id", m.user.ID)
326 logger.Info("start UNSUBSCRIBE", "mailbox", mb)
327
328 // We do not allow to unsubscribe from default mailboxes because we need their special use present
329 for _, dmb := range model.DefaultMailboxes() {
330 if string(dmb.Name) == mb {
331 logger.Info("denied unsubscribing default mailbox", "mailbox", mb)
332 return &imap.Error{
333 Type: imap.StatusResponseTypeNo,
334 Code: imap.ResponseCodeCannot,
335 Text: "Unsubscribing preset mailbox is not allowed",
336 }
337 }
338 }
339
340 err := m.db.Unsubscribe(context.Background(), m.user.ID, mb)
341 if err != nil {
342 logger.Error("unsubscribe mailbox", "error", err)
343 return errInternalServerError
344 }
345 logger.Info("end UNSUBSCRIBE", "mailbox", mb)
346 return nil
347}
348
349func (m *imapSession) List(w *imapserver.ListWriter, ref string, patterns []string, options *imap.ListOptions) error {
350 logger := slog.With("session_id", m.sessionID, "cmd", "LIST", "user_id", m.user.ID)
351 logger.Info("start LIST", "ref", ref, "patterns", patterns, "options", options)
352
353 // Special case LIST "" "" - mainly for the client to get the folder hierarchy delimiter
354 if ref == "" && len(patterns) == 0 {
355 err := w.WriteList(&imap.ListData{
356 Attrs: []imap.MailboxAttr{imap.MailboxAttrNoSelect},
357 Delim: '/',
358 Mailbox: "",
359 })
360 if err != nil {
361 logger.Error("listing mailboxes", "error", err)
362 return errInternalServerError
363 }
364 logger.Info("end LIST \"\" \"\"")
365 return nil
366 }
367
368 // LIST (SUBSCRIBED)
369 if options.SelectSubscribed {
370 subscribed, err := m.db.ListSubscribed(context.Background(), m.user.ID)
371 if err != nil {
372 logger.Error("listSubscribed", "error", err)
373 return errInternalServerError
374 }
375
376 for mb, exists := range subscribed {
377 for _, pattern := range patterns {
378 // We MUST pattern match subscribed mailboxes
379 if imapserver.MatchList(mb.Name, '/', ref, pattern) {
380 attr := getMailboxAttr(mb)
381
382 if !exists {
383 attr = append(attr, imap.MailboxAttrNonExistent, imap.MailboxAttrNoSelect)
384 }
385
386 err = w.WriteList(&imap.ListData{
387 Attrs: attr,
388 Delim: '/',
389 Mailbox: mb.Name,
390 })
391 if err != nil {
392 logger.Error("listing mailboxes", "error", err)
393 return errInternalServerError
394 }
395 // Matched this mailbox, don't write it again if it matches a 2nd pattern
396 break
397 }
398 }
399 }
400 logger.Info("end LIST subscribed")
401 return nil
402 }
403
404 // Standard LIST "" "*"
405 mailboxes, err := m.db.GetUserMailboxes(context.Background(), m.user.ID)
406 if err != nil {
407 logger.Error("get user mailboxes", "error", err)
408 return errInternalServerError
409 }
410
411 hasChildren := calcHasChildren(mailboxes)
412
413 for _, mb := range mailboxes {
414 for _, pattern := range patterns {
415 match := imapserver.MatchList(mb.Name, '/', ref, pattern)
416 logger.Debug("LIST imapserver.MatchList", "mailbox", mb.Name, "ref", ref, "pattern", pattern, "match", match)
417
418 if match {
419 attr := getMailboxAttr(mb)
420
421 // RFC 9051: MAY return children attribute even if it was not requested
422 if hasChildren[mb.Name] {
423 attr = append(attr, imap.MailboxAttrHasChildren)
424 } else {
425 attr = append(attr, imap.MailboxAttrHasNoChildren)
426 }
427
428 // 2. Pass the addresses (&) of those local variables
429 numMessages := mb.NumMessages
430 numUnseen := uint32(mb.NumUnseen)
431 numDeleted := uint32(mb.NumDeleted)
432 size := mb.Size
433 err := w.WriteList(&imap.ListData{
434 Attrs: attr,
435 Delim: '/',
436 Mailbox: mb.Name,
437 ChildInfo: nil, // Should be returned on RECURSIVEMATCH
438 Status: &imap.StatusData{
439 Mailbox: mb.Name,
440 UIDNext: imap.UID(mb.UIDNext),
441 UIDValidity: mb.UIDValidity,
442 HighestModSeq: uint64(mb.HighestModSeq),
443 NumMessages: &numMessages,
444 NumUnseen: &numUnseen,
445 NumDeleted: &numDeleted,
446 Size: &size,
447 NumRecent: new(uint32),
448 AppendLimit: new(uint32),
449 DeletedStorage: new(int64),
450 },
451 })
452
453 if err != nil {
454 logger.Error("listing mailboxes", "error", err)
455 return errInternalServerError
456 }
457
458 logger.Debug("WriteList", "mailbox", mb.Name, "attr", attr)
459
460 // Matched this mailbox, don't write it again
461 break
462 }
463 }
464 }
465
466 logger.Info("end LIST")
467 return nil
468}
469
470func calcHasChildren(mailboxes []model.Mailbox) map[string]bool {
471 hasChildren := make(map[string]bool)
472 for _, mb := range mailboxes {
473 s := strings.Split(mb.Name, "/")
474 if len(s) > 1 {
475 hasChildren[strings.Join(s[:len(s)-1], "/")] = true
476 }
477 }
478 return hasChildren
479}
480
481func (m *imapSession) Status(mailboxName string, _ *imap.StatusOptions) (*imap.StatusData, error) {
482 logger := slog.With("session_id", m.sessionID, "cmd", "STATUS", "user_id", m.user.ID)
483 logger.Info("start STATUS", "mailbox_name", mailboxName)
484 userMailboxes, err := m.db.GetUserMailboxes(context.Background(), m.user.ID)
485 if err != nil {
486 logger.Error("get user mailboxes", "error", err)
487 return &imap.StatusData{}, errInternalServerError
488 }
489
490 var statusBox model.Mailbox
491 found := false
492 for _, mb := range userMailboxes {
493 if mb.Name == mailboxName {
494 statusBox = mb
495 found = true
496 break
497 }
498 }
499
500 if !found {
501 logger.Info("mailbox not found", "name", mailboxName)
502 return &imap.StatusData{}, errMailboxNotExist
503 }
504
505 logger.Info("end STATUS", "mailbox_name", mailboxName)
506 return &imap.StatusData{
507 Mailbox: mailboxName,
508 NumMessages: new(statusBox.NumMessages),
509 NumRecent: new(uint32), // obsolete
510 UIDNext: imap.UID(statusBox.UIDNext),
511 UIDValidity: statusBox.UIDValidity,
512 NumUnseen: new(uint32(statusBox.NumUnseen)),
513 NumDeleted: new(uint32(statusBox.NumDeleted)),
514 Size: new(statusBox.Size),
515 AppendLimit: new(uint32),
516 DeletedStorage: new(int64),
517 HighestModSeq: uint64(statusBox.HighestModSeq),
518 }, nil
519}
520
521func (m *imapSession) Append(mailbox string, r imap.LiteralReader, options *imap.AppendOptions) (*imap.AppendData, error) {
522 logger := slog.With("session_id", m.sessionID, "cmd", "APPEND", "user_id", m.user.ID)
523 logger.Info("start APPEND", "mailbox", mailbox)
524 // If the destination mailbox does not exist, a server MUST return an error and MUST NOT automatically create the
525 // mailbox. Unless it is certain that the destination mailbox cannot be created, the server MUST send the response
526 // code "[TRYCREATE]" as the prefix of the text of the tagged NO response. This gives a hint to the client that it
527 // can attempt a CREATE command and retry the APPEND if the CREATE is successful.
528 mailboxID, uidValidity, err := m.db.GetMailboxID(context.Background(), m.user.ID, mailbox)
529 if err != nil {
530 if errors.Is(err, db.ErrMailboxNotFound) {
531 logger.Info("mailbox not found", "mailbox", mailbox)
532 return nil, errMailboxNotExist
533 }
534 logger.Error("get mailbox", "error", err)
535 return nil, errInternalServerError
536 }
537
538 for _, f := range options.Flags {
539 if !isAllowedFlag(f) {
540 logger.Info("flag not allowed", "flag", f)
541 return nil, errBadFlag
542 }
543 }
544
545 internalTime := time.Now()
546 if !options.Time.IsZero() {
547 internalTime = options.Time
548 }
549
550 fileName, size, parsedMsg, err := m.persistence.WriteBlobMessage(r)
551 if err != nil {
552 logger.Error("write blob", "error", err)
553 return nil, errInternalServerError
554 }
555
556 logger.Info("wrote file", "filename", fileName, "size", size)
557 uid, err := m.db.AppendMessage(context.Background(), mailboxID, m.user.ID, fileName, size, internalTime.UTC().Format(time.RFC3339), fromIMAPFlags(options.Flags), parsedMsg)
558 if err != nil {
559 logger.Error("write message metadata", "error", err)
560 return nil, errInternalServerError
561 }
562
563 // In the case of a mailbox that has permissions set so that the client can APPEND to the mailbox, but not SELECT or
564 // EXAMINE it, the server MUST NOT send an APPENDUID response code as it would disclose information about the mailbox.
565
566 mb, err := m.db.GetUserMailbox(context.Background(), m.user.ID, mailbox)
567 if err != nil {
568 logger.Error("get user mailbox", "error", err)
569 return nil, errInternalServerError
570 }
571
572 t := m.mTracker.get(m.user.ID, mailboxID)
573 if t != nil {
574 t.QueueNumMessages(mb.NumMessages)
575 }
576
577 logger.Info("end APPEND", "mailbox", mailbox)
578 return &imap.AppendData{
579 UID: imap.UID(uid),
580 UIDValidity: uidValidity,
581 }, nil
582}
583
584func (m *imapSession) Poll(w *imapserver.UpdateWriter, allowExpunge bool) error {
585 logger := slog.With("session_id", m.sessionID, "cmd", "POLL", "user_id", m.user.ID)
586 logger.Debug("start POLL")
587 if m.sessionTracker == nil {
588 logger.Debug("session tracker not initialized")
589 return nil
590 }
591 err := m.sessionTracker.Poll(w, allowExpunge)
592 if err != nil {
593 logger.Error("poll session tracker", "error", err)
594 return errInternalServerError
595 }
596 logger.Debug("end POLL")
597 return nil
598}
599
600func (m *imapSession) Idle(w *imapserver.UpdateWriter, stop <-chan struct{}) error {
601 logger := slog.With("session_id", m.sessionID, "cmd", "IDLE", "user_id", m.user.ID)
602 logger.Debug("start IDLE")
603 if m.sessionTracker == nil {
604 logger.Debug("session tracker not initialized")
605 return nil
606 }
607 err := m.sessionTracker.Idle(w, stop)
608 if err != nil {
609 logger.Error("idle session tracker", "error", err)
610 return errInternalServerError
611 }
612 logger.Debug("end IDLE")
613 return nil
614}
615
616func (m *imapSession) Unselect() error {
617 logger := slog.With("session_id", m.sessionID, "cmd", "UNSELECT", "user_id", m.user.ID)
618 logger.Info("start UNSELECT")
619 m.selectedMailbox = model.Mailbox{}
620 m.sessionTracker.Close()
621 m.sessionTracker = nil
622 logger.Info("end UNSELECT")
623 return nil
624}
625
626// removeFromSearchResult returns res with the given uid/seq removed.
627// If res is not a set type we know about, it is returned unchanged.
628func removeFromSearchResult(res imap.NumSet, uid, seq uint32) imap.NumSet {
629 switch s := res.(type) {
630 case imap.UIDSet:
631 if !s.Contains(imap.UID(uid)) {
632 return s
633 }
634 out := imap.UIDSetNum()
635 nums, _ := s.Nums()
636 for _, n := range nums {
637 if n != imap.UID(uid) {
638 out.AddNum(n) // FIX: was adding the deleted uid
639 }
640 }
641 return out
642 case imap.SeqSet:
643 if !s.Contains(seq) {
644 return s
645 }
646 out := imap.SeqSetNum()
647 nums, _ := s.Nums()
648 for _, n := range nums {
649 if n != seq {
650 out.AddNum(n) // FIX: was adding the deleted seq
651 }
652 }
653 return out
654 }
655 return res
656}
657
658// Expunge SELECTED state
659func (m *imapSession) Expunge(_ *imapserver.ExpungeWriter, uids *imap.UIDSet) error {
660 ctx := context.Background()
661
662 logger := slog.With("session_id", m.sessionID, "cmd", "EXPUNGE", "user_id", m.user.ID)
663 logger.InfoContext(ctx, "start EXPUNGE", "uids", uids)
664 if m.selectedMailboxReadOnly {
665 logger.InfoContext(ctx, "selected mailbox is read-only")
666 // This will either break Close() or Expunge() because CLOSE expects a clean OK, EXPUNGE below error
667 return errMailboxReadOnly
668 }
669
670 if m.selectedMailbox.ID == -1 {
671 logger.InfoContext(ctx, "no mailbox selected")
672 return errNoMailboxSelected
673 }
674
675 // UID EXPUNGE restricts removal to the given set. A nil or empty set means
676 // "expunge every \Deleted message" (plain EXPUNGE semantics).
677 uidFilter := func(uid imap.UID) bool { return true }
678 if uids != nil {
679 if nums, _ := uids.Nums(); len(nums) > 0 {
680 uidFilter = func(uid imap.UID) bool { return uids.Contains(uid) }
681 }
682 }
683
684 toBeDeleted, err := m.db.GetAllFlaggedDeletedMessages(ctx, m.selectedMailbox.ID)
685 if err != nil {
686 logger.ErrorContext(ctx, "get flagged deleted messages", "error", err)
687 return errInternalServerError
688 }
689 logger.InfoContext(ctx, "candidates", "mailbox_id", m.selectedMailbox.ID, "uids", toBeDeleted)
690
691 // Resolve all seqnums BEFORE any deletion, then delete high-to-low.
692 // RFC 9051: The UID EXPUNGE command permanently removes all messages that have both the \Deleted flag set and a UID
693 // that is included in the specified sequence set from the currently selected mailbox. If a message either does not
694 // have the \Deleted flag set or has a UID that is not included in the specified sequence set, it is not affected.
695 type expunge struct{ uid, seq uint32 }
696 list := make([]expunge, 0, len(toBeDeleted))
697 for _, uid := range toBeDeleted {
698 if !uidFilter(imap.UID(uid)) {
699 continue
700 }
701 seq, err := m.db.UIDToServerSeq(ctx, m.selectedMailbox.ID, uid)
702 if err != nil {
703 logger.ErrorContext(ctx, "UID to server seq", "error", err)
704 return errInternalServerError
705 }
706 list = append(list, expunge{uid, seq})
707 }
708 slices.SortFunc(list, func(i, j expunge) int { return int(j.seq) - int(i.seq) }) // descending
709
710 tracker := m.mTracker.get(m.user.ID, m.selectedMailbox.ID)
711 for _, seqNumUID := range list {
712 err = m.db.DeleteMessageFromMailbox(ctx, m.selectedMailbox.ID, seqNumUID.uid)
713 if err != nil {
714 logger.ErrorContext(ctx, "delete message from mailbox", "error", err)
715 return errInternalServerError
716 }
717
718 if tracker != nil {
719 tracker.QueueExpunge(seqNumUID.seq)
720 }
721
722 m.searchRes = removeFromSearchResult(m.searchRes, seqNumUID.uid, seqNumUID.seq)
723 }
724
725 logger.InfoContext(ctx, "end EXPUNGE")
726 return nil
727}
728
729// Search SELECTED State
730func (m *imapSession) Search(kind imapserver.NumKind, criteria *imap.SearchCriteria, options *imap.SearchOptions) (*imap.SearchData, error) {
731 logger := slog.With("session_id", m.sessionID, "cmd", "SEARCH", "user_id", m.user.ID)
732 logger.Info("start SEARCH", "criteria", criteria, "options", options)
733 allMailboxMessages, err := m.db.GetAllMailboxMessages(context.Background(), m.user.ID, m.selectedMailbox.ID)
734 if err != nil {
735 logger.Error("Unable to retrieve messages for searching", "error", err)
736 return nil, errInternalServerError
737 }
738
739 uidSet := make([]imap.UID, 0)
740 uidSetCount := 0 // leads to wrong results with len(uidSet)
741 for _, msg := range allMailboxMessages {
742 if m.evalSearchCriteria(criteria, msg, m.sessionTracker, m.persistence) {
743 uidSet = append(uidSet, imap.UID(msg.UID))
744 uidSetCount++
745 }
746 }
747
748 slices.Sort(uidSet)
749 uidMin := 0
750 if len(uidSet) > 0 {
751 uidMin = int(uidSet[0])
752 }
753 uidMax := 0
754 if len(uidSet) > 0 {
755 uidMax = int(uidSet[len(uidSet)-1])
756 }
757 uidSetNum := imap.UIDSetNum(uidSet...)
758
759 // Combination of Result Option "$" Marker Value
760 //SAVE MIN -> MIN
761 //SAVE MAX -> MAX
762 //SAVE MIN MAX -> MIN & MAX
763 //SAVE * [m] -> all found messages
764 if options.ReturnSave {
765 m.searchRes = uidSetNum
766
767 if options.ReturnMin {
768 m.searchRes = imap.UIDSetNum(imap.UID(uint32(uidMin)))
769 }
770
771 if options.ReturnMax {
772 m.searchRes = imap.UIDSetNum(imap.UID(uint32(uidMax)))
773 }
774
775 if options.ReturnMin && options.ReturnMax {
776 n := imap.UIDSetNum(imap.UID(uint32(uidMin)))
777 n.AddNum(imap.UID(uint32(uidMax)))
778 m.searchRes = n
779 }
780 }
781
782 if kind == imapserver.NumKindUID {
783 res := &imap.SearchData{
784 All: uidSetNum,
785 Count: uint32(uidSetCount),
786 Min: uint32(uidMin),
787 Max: uint32(uidMax),
788 }
789 logger.Info("end SEARCH", "search_result", res)
790 return res, nil
791 }
792
793 if kind == imapserver.NumKindSeq {
794 out := imap.SeqSetNum()
795 seqNumMin := uint32(math.MaxUint32)
796 seqNumMax := uint32(0)
797 seqNumCount := 0
798 num, _ := uidSetNum.Nums()
799 for _, n := range num {
800 serverSeq, err := m.db.UIDToServerSeq(context.Background(), m.selectedMailbox.ID, uint32(n))
801 if err != nil {
802 logger.Error("Unable to retrieve server seq", "error", err)
803 return nil, errInternalServerError
804 }
805 seqNumCount++
806 out.AddNum(serverSeq)
807 if serverSeq < seqNumMin {
808 seqNumMin = serverSeq
809 }
810 if serverSeq > seqNumMax {
811 seqNumMax = serverSeq
812 }
813 }
814 res := &imap.SearchData{
815 All: out,
816 Count: uint32(seqNumCount),
817 Min: seqNumMin,
818 Max: seqNumMax,
819 }
820 logger.Info("end SEARCH", "search_result", res)
821 return res, nil
822 }
823
824 return nil, errInternalServerError
825}
826
827// Fetch SELECTED State
828func (m *imapSession) Fetch(w *imapserver.FetchWriter, numSet imap.NumSet, options *imap.FetchOptions) error {
829 logger := slog.With("session_id", m.sessionID, "cmd", "FETCH", "user_id", m.user.ID)
830 logger.Info("start FETCH", "num_set", numSet, "options", options)
831 var outMessages []model.Message
832 var err error
833
834 sourceUIDs, err := m.resolveUIDs(context.Background(), numSet)
835 if err != nil {
836 if errors.Is(err, errDBSequenceOutOfRange) {
837 logger.Info("sequence out of range")
838 return errSequenceOutOfRange
839 }
840 logger.Error("unable to resolve UIDs", "error", err)
841 return errInternalServerError
842 }
843
844 // getMailboxMessagesByUID inherently filters out non-existing UIDs
845 // because it queries by mailboxID and the resolved sourceUIDs.
846 outMessages, err = m.db.GetMailboxMessagesByUID(context.Background(), m.user.ID, m.selectedMailbox.ID, sourceUIDs)
847 if err != nil {
848 logger.Error("unable to fetch mailbox messages", "error", err)
849 return errInternalServerError
850 }
851
852 // TODO but more on a global scale than just here:
853 // seq-number = nz-number / "*"
854 // ; message sequence number (COPY, FETCH, STORE
855 // ; commands) or unique identifier (UID COPY,
856 // ; UID FETCH, UID STORE commands).
857 // ; * represents the largest number in use. In
858 // ; the case of message sequence numbers, it is
859 // ; the number of messages in a non-empty mailbox.
860 // ; In the case of unique identifiers, it is the
861 // ; unique identifier of the last message in the
862 // ; mailbox or, if the mailbox is empty, the
863 // ; mailbox's current UIDNEXT value.
864 // ; The server should respond with a tagged BAD
865 // ; response to a command that uses a message
866 // ; sequence number greater than the number of
867 // ; messages in the selected mailbox. This
868 // ; includes "*" if the selected mailbox is empty.
869 for _, o := range outMessages {
870 seqNum, err := m.db.UIDToServerSeq(context.Background(), m.selectedMailbox.ID, o.UID)
871 if err != nil {
872 logger.Error("unable to retrieve server seq", "error", err)
873 return errInternalServerError
874 }
875
876 clientSeqNum := m.sessionTracker.EncodeSeqNum(seqNum)
877 if clientSeqNum == 0 { // message does not exist from client pov
878 continue
879 }
880
881 mw := w.CreateMessage(clientSeqNum)
882 if options.UID {
883 mw.WriteUID(imap.UID(o.UID))
884 }
885
886 if options.InternalDate {
887 t, _ := time.Parse(time.RFC3339, o.InternalDate)
888 mw.WriteInternalDate(t)
889 }
890
891 if options.RFC822Size {
892 mw.WriteRFC822Size(o.RFC822Size)
893 }
894
895 if options.Envelope {
896 fromAddresses, err := parseAddressList(o.EnvelopeFrom.String)
897 if err != nil {
898 logger.Error("unable to parse envelope FROM addresses", "error", err)
899 return errInternalServerError
900 }
901 senderAddresses, err := parseAddressList(o.EnvelopeSender.String)
902 if err != nil {
903 logger.Error("unable to parse envelope SENDER addresses", "error", err)
904 return errInternalServerError
905 }
906
907 // RFC: If the Sender or Reply-To header fields are absent in the [RFC5322] header, or are present but empty,
908 // the server sets the corresponding member of the envelope to be the same value as the from member
909 // (the client is not expected to know how to do this).
910 if len(senderAddresses) == 0 {
911 senderAddresses = fromAddresses
912 }
913
914 replyToAddresses, err := parseAddressList(o.EnvelopeReplyTo.String)
915 if err != nil {
916 logger.Error("unable to parse envelope REPLY TO addresses", "error", err)
917 return errInternalServerError
918 }
919 if len(replyToAddresses) == 0 {
920 replyToAddresses = fromAddresses
921 }
922
923 toAddresses, err := parseAddressList(o.EnvelopeTo.String)
924 if err != nil {
925 logger.Error("unable to parse envelope TO addresses", "error", err)
926 return errInternalServerError
927 }
928 ccAddresses, err := parseAddressList(o.EnvelopeCc.String)
929 if err != nil {
930 logger.Error("unable to parse envelope CC addresses", "error", err)
931 return errInternalServerError
932 }
933 bccAddresses, err := parseAddressList(o.EnvelopeBcc.String)
934 if err != nil {
935 logger.Error("unable to parse envelope BCC addresses", "error", err)
936 return errInternalServerError
937 }
938
939 var t time.Time
940 if o.EnvelopeDate.Valid {
941 t, _ = time.Parse(time.RFC3339, o.EnvelopeDate.String)
942 }
943
944 // imap-go: The In-Reply-To and Message-ID values contain message identifiers without angle brackets.
945 inReplyToAddresses := make([]string, 0)
946 for a := range strings.SplitSeq(o.EnvelopeInReplyTo.String, ",") {
947 inReplyTo, _ := strings.CutPrefix(a, "<")
948 inReplyTo, _ = strings.CutSuffix(inReplyTo, ">")
949 inReplyToAddresses = append(inReplyToAddresses, inReplyTo)
950 }
951
952 messageID, _ := strings.CutPrefix(o.EnvelopeMessageID.String, "<")
953 messageID, _ = strings.CutSuffix(messageID, ">")
954
955 mw.WriteEnvelope(&imap.Envelope{
956 Date: t,
957 Subject: o.EnvelopeSubject.String,
958 From: fromAddresses,
959 Sender: senderAddresses,
960 ReplyTo: replyToAddresses,
961 To: toAddresses,
962 Cc: ccAddresses,
963 Bcc: bccAddresses,
964 InReplyTo: inReplyToAddresses,
965 MessageID: messageID,
966 })
967 }
968
969 // RFC 9051: The \Seen flag is implicitly set; if this causes the flags to change, they SHOULD be included as
970 // part of the FETCH responses.
971 mustShowFlags := false
972 if options.BodyStructure != nil {
973 r, err := m.persistence.BlobReader(o.BlobHash)
974 if err != nil {
975 logger.Error("unable to read blob", "error", err)
976 return errInternalServerError
977 }
978 bs := imapserver.ExtractBodyStructure(r)
979 err = r.Close()
980 if err != nil {
981 logger.Error("unable to close body structure", "error", err)
982 }
983 mw.WriteBodyStructure(bs)
984
985 // Honestly not sure if flagging messages as \\Seen here is correct, or only below for BodySection
986 if !slices.Contains(o.Flags, string(imap.FlagSeen)) {
987 logger.Info("flag message as \\Seen", "uid", o.UID)
988 _, err = m.db.AddMessageFlags(context.Background(), m.selectedMailbox.ID, o.UID, []string{string(imap.FlagSeen)})
989 if err != nil {
990 logger.Error("unable to add message flags", "error", err)
991 return errInternalServerError
992 }
993 o.Flags = append(o.Flags, string(imap.FlagSeen))
994 mustShowFlags = true
995 }
996 }
997
998 for _, bs := range options.BodySection {
999 if !bs.Peek && !m.selectedMailboxReadOnly {
1000 if !slices.Contains(o.Flags, string(imap.FlagSeen)) {
1001 logger.Info("flag message as \\Seen", "uid", o.UID)
1002 _, err = m.db.AddMessageFlags(context.Background(), m.selectedMailbox.ID, o.UID, []string{string(imap.FlagSeen)})
1003 if err != nil {
1004 logger.Error("unable to add message flags", "error", err)
1005 return errInternalServerError
1006 }
1007 o.Flags = append(o.Flags, string(imap.FlagSeen))
1008 mustShowFlags = true
1009 }
1010 }
1011 r, err := m.persistence.BlobReader(o.BlobHash)
1012 if err != nil {
1013 logger.Error("unable to read blob", "error", err)
1014 return errInternalServerError
1015 }
1016 ebs := imapserver.ExtractBodySection(r, bs)
1017 err = r.Close()
1018 if err != nil {
1019 logger.Error("unable to close blob reader", "error", err)
1020 }
1021
1022 wc := mw.WriteBodySection(bs, int64(len(ebs)))
1023 _, writeErr := wc.Write(ebs)
1024 closeErr := wc.Close()
1025 if writeErr != nil {
1026 logger.Error("unable to write body section", "error", writeErr)
1027 return errInternalServerError
1028 }
1029 if closeErr != nil {
1030 logger.Error("unable to close body section", "error", closeErr)
1031 return errInternalServerError
1032 }
1033 }
1034
1035 for _, bs := range options.BinarySection {
1036 if !bs.Peek && !m.selectedMailboxReadOnly {
1037 _, err = m.db.AddMessageFlags(context.Background(), m.selectedMailbox.ID, o.UID, []string{string(imap.FlagSeen)})
1038 if err != nil {
1039 logger.Error("unable to add message flags", "error", err)
1040 return errInternalServerError
1041 }
1042 }
1043 r, err := m.persistence.BlobReader(o.BlobHash)
1044 if err != nil {
1045 logger.Error("unable to read blob", "error", err)
1046 return errInternalServerError
1047 }
1048 buf := imapserver.ExtractBinarySection(r, bs)
1049 err = r.Close()
1050 if err != nil {
1051 logger.Error("unable to close blob reader", "error", err)
1052 }
1053
1054 wc := mw.WriteBinarySection(bs, int64(len(buf)))
1055 _, writeErr := wc.Write(buf)
1056 closeErr := wc.Close()
1057 if writeErr != nil {
1058 logger.Error("unable to write body section", "error", writeErr)
1059 return errInternalServerError
1060 }
1061 if closeErr != nil {
1062 logger.Error("unable to close body section", "error", closeErr)
1063 return errInternalServerError
1064 }
1065 }
1066
1067 for _, bss := range options.BinarySectionSize {
1068 r, err := m.persistence.BlobReader(o.BlobHash)
1069 if err != nil {
1070 logger.Error("unable to read blob", "error", err)
1071 return errInternalServerError
1072 }
1073 n := imapserver.ExtractBinarySectionSize(r, bss)
1074 err = r.Close()
1075 if err != nil {
1076 logger.Error("unable to close blob reader", "error", err)
1077 }
1078 mw.WriteBinarySectionSize(bss, n)
1079 }
1080
1081 // Flags should come last so that the updated \Seen flag is properly communicated
1082 if options.Flags || mustShowFlags {
1083 mw.WriteFlags(toIMAPFlags(o.Flags))
1084 }
1085
1086 if err = mw.Close(); err != nil {
1087 logger.Error("unable to close imap writer", "error", err)
1088 return errInternalServerError
1089 }
1090 }
1091
1092 slog.Info("end FETCH")
1093 return nil
1094}
1095
1096// Store SELECTED state
1097func (m *imapSession) Store(w *imapserver.FetchWriter, numSet imap.NumSet, flags *imap.StoreFlags, _ *imap.StoreOptions) error {
1098 logger := slog.With("session_id", m.sessionID, "cmd", "STORE", "user_id", m.user.ID)
1099 logger.Info("start STORE", "num_set", numSet, "flags", flags)
1100
1101 if m.selectedMailbox.ID == -1 {
1102 logger.Info("no mailbox selected")
1103 return errNoMailboxSelected
1104 }
1105
1106 if m.selectedMailboxReadOnly {
1107 logger.Info("mailbox selected as read-only")
1108 return errMailboxReadOnly
1109 }
1110
1111 for _, f := range flags.Flags {
1112 if !isAllowedFlag(f) {
1113 logger.Info("flag not allowed", "flag", f)
1114 return errBadFlag
1115 }
1116 }
1117
1118 sourceUIDs, err := m.resolveUIDs(context.Background(), numSet)
1119 if err != nil {
1120 if errors.Is(err, errDBSequenceOutOfRange) {
1121 logger.Info("sequence out of range")
1122 return errSequenceOutOfRange
1123 }
1124 logger.Error("unable to resolve UIDs", "error", err)
1125 return errInternalServerError
1126 }
1127
1128 for _, sourceUID := range sourceUIDs {
1129 // Filter non-existing UIDs to prevent FK constraint errors.go and comply
1130 // with IMAP semantics where STORE on non-existent messages is a no-op.
1131 exists, err := m.db.UIDExists(context.Background(), m.user.ID, sourceUID)
1132 if err != nil {
1133 logger.Error("unable to retrieve UID", "error", err)
1134 return errInternalServerError
1135 }
1136 if !exists {
1137 continue
1138 }
1139
1140 var flagSet []string
1141 switch flags.Op {
1142 case imap.StoreFlagsAdd:
1143 flagSet, err = m.db.AddMessageFlags(context.Background(), m.selectedMailbox.ID, sourceUID, fromIMAPFlags(flags.Flags))
1144 case imap.StoreFlagsDel:
1145 flagSet, err = m.db.DeleteMessageFlags(context.Background(), m.selectedMailbox.ID, sourceUID, fromIMAPFlags(flags.Flags))
1146 case imap.StoreFlagsSet:
1147 flagSet, err = m.db.SetMessageFlags(context.Background(), m.selectedMailbox.ID, sourceUID, fromIMAPFlags(flags.Flags))
1148 default:
1149 logger.Error("unknown flag op", "flag", flags.Op)
1150 return &imap.Error{
1151 Type: imap.StatusResponseTypeBad,
1152 Code: imap.ResponseCodeClientBug,
1153 Text: fmt.Sprintf("Unknown store flags: %v", flags.Op),
1154 }
1155 }
1156
1157 if err != nil {
1158 logger.Error("unable to store flags", "error", err)
1159 return errInternalServerError
1160 }
1161
1162 seqNum, err := m.db.UIDToServerSeq(context.Background(), m.selectedMailbox.ID, sourceUID)
1163 if err != nil {
1164 logger.Error("unable to convert to server seq", "error", err)
1165 return errInternalServerError
1166 }
1167
1168 imapFlags := toIMAPFlags(flagSet)
1169
1170 if !flags.Silent {
1171 fetchWriter := w.CreateMessage(m.sessionTracker.EncodeSeqNum(seqNum))
1172 fetchWriter.WriteFlags(imapFlags)
1173 err = fetchWriter.Close()
1174 if err != nil {
1175 logger.Error("unable to close imap writer", "error", err)
1176 return errInternalServerError
1177 }
1178 }
1179
1180 m.mTracker.get(m.user.ID, m.selectedMailbox.ID).QueueMessageFlags(seqNum, imap.UID(sourceUID), imapFlags, m.sessionTracker)
1181 }
1182 logger.Info("end STORE")
1183 return nil
1184}
1185
1186func (m *imapSession) Copy(numSet imap.NumSet, dest string) (*imap.CopyData, error) {
1187 logger := slog.With("session_id", m.sessionID, "cmd", "COPY", "user_id", m.user.ID)
1188 logger.Info("start COPY", "num_set", numSet, "dest", dest)
1189 if m.selectedMailbox.ID == -1 {
1190 logger.Info("no mailbox selected")
1191 return nil, errNoMailboxSelected
1192 }
1193
1194 // must find destination mailbox, otherwise return failure (see RFC)
1195 mailboxID, uidValidity, err := m.db.GetMailboxID(context.Background(), m.user.ID, dest)
1196 if err != nil {
1197 if errors.Is(err, db.ErrMailboxNotFound) {
1198 logger.Info("destination mailbox not found")
1199 return nil, errMailboxNotExist
1200 }
1201 logger.Error("unable to get mailbox ID", "error", err)
1202 return nil, errInternalServerError
1203 }
1204
1205 sourceUIDs, err := m.resolveUIDs(context.Background(), numSet)
1206 if err != nil {
1207 if errors.Is(err, errDBSequenceOutOfRange) {
1208 logger.Info("sequence out of range")
1209 return nil, errSequenceOutOfRange
1210 }
1211 logger.Error("unable to resolve UIDs", "error", err)
1212 return nil, errInternalServerError
1213 }
1214
1215 // Filter non-existing UIDs to prevent copyMessagesToMailbox from burning
1216 // destination UIDs for messages that don't exist in the source mailbox.
1217 filteredSourceUIDs := make([]uint32, 0, len(sourceUIDs))
1218 for _, uid := range sourceUIDs {
1219 exists, err := m.db.UIDExists(context.Background(), m.user.ID, uid)
1220 if err != nil {
1221 logger.Error("unable to retrieve UID", "error", err)
1222 return nil, errInternalServerError
1223 }
1224 if exists {
1225 filteredSourceUIDs = append(filteredSourceUIDs, uid)
1226 }
1227 }
1228
1229 destUIDs, err := m.db.CopyMessagesToMailbox(context.Background(), m.selectedMailbox.ID, mailboxID, filteredSourceUIDs)
1230 if err != nil {
1231 logger.Error("unable to copy messages to mailbox", "error", err)
1232 return nil, errInternalServerError
1233 }
1234
1235 logger.Info("end COPY")
1236 return &imap.CopyData{
1237 UIDValidity: uidValidity,
1238 SourceUIDs: internalIDToUIDSet(filteredSourceUIDs),
1239 DestUIDs: internalIDToUIDSet(destUIDs),
1240 }, nil
1241}
1242
1243func (m *imapSession) Namespace() (*imap.NamespaceData, error) {
1244 logger := slog.With("session_id", m.sessionID, "cmd", "NAMESPACE", "user_id", m.user.ID)
1245 logger.Info("start NAMESPACE")
1246 logger.Info("end NAMESPACE")
1247 return &imap.NamespaceData{
1248 Personal: []imap.NamespaceDescriptor{
1249 {
1250 Prefix: "",
1251 Delim: '/',
1252 },
1253 },
1254 Other: nil,
1255 Shared: nil,
1256 }, nil
1257}
1258
1259// Move SELECTED state
1260func (m *imapSession) Move(w *imapserver.MoveWriter, numSet imap.NumSet, dest string) error {
1261 ctx := context.Background()
1262
1263 logger := slog.With("session_id", m.sessionID, "cmd", "MOVE", "user_id", m.user.ID)
1264 logger.InfoContext(ctx, "start MOVE", "num_set", numSet, "dest", dest)
1265
1266 if m.selectedMailbox.ID == -1 {
1267 logger.InfoContext(ctx, "no mailbox selected")
1268 return errNoMailboxSelected
1269 }
1270
1271 if m.selectedMailboxReadOnly {
1272 logger.InfoContext(ctx, "mailbox selected as read-only")
1273 return errMailboxReadOnly
1274 }
1275
1276 // must find destination mailbox, otherwise return failure (see RFC)
1277 destMailboxID, uidValidity, err := m.db.GetMailboxID(ctx, m.user.ID, dest)
1278 if err != nil {
1279 if errors.Is(err, db.ErrMailboxNotFound) {
1280 logger.InfoContext(ctx, "destination mailbox not found")
1281 return errMailboxNotExist
1282 }
1283 logger.ErrorContext(ctx, "unable to get mailbox ID", "error", err)
1284 return errInternalServerError
1285 }
1286
1287 if m.selectedMailbox.ID == destMailboxID {
1288 logger.InfoContext(ctx, "moving messages within the same mailbox is not supported")
1289 return &imap.Error{
1290 Type: imap.StatusResponseTypeNo,
1291 Code: imap.ResponseCodeCannot,
1292 Text: "messages already moved",
1293 }
1294 }
1295
1296 sourceUIDs, err := m.resolveUIDs(ctx, numSet)
1297 if err != nil {
1298 if errors.Is(err, errDBSequenceOutOfRange) {
1299 logger.InfoContext(ctx, "sequence out of range")
1300 return errSequenceOutOfRange
1301 }
1302 logger.ErrorContext(ctx, "unable to resolve UIDs", "error", err)
1303 return errInternalServerError
1304 }
1305
1306 // Filter non-existing UIDs to prevent burning destination UIDs
1307 filteredSourceUIDs := make([]uint32, 0, len(sourceUIDs))
1308 for _, uid := range sourceUIDs {
1309 exists, err := m.db.UIDExists(ctx, m.user.ID, uid)
1310 if err != nil {
1311 logger.ErrorContext(ctx, "unable to retrieve UID", "error", err)
1312 return errInternalServerError
1313 }
1314 if exists {
1315 filteredSourceUIDs = append(filteredSourceUIDs, uid)
1316 }
1317 }
1318
1319 if len(filteredSourceUIDs) == 0 {
1320 logger.InfoContext(ctx, "no messages to move")
1321 return nil
1322 }
1323
1324 // for expunging later, we need to calculate the serverSeq now
1325 expungeServerSeq := make([]uint32, len(filteredSourceUIDs))
1326 for i, uid := range filteredSourceUIDs {
1327 serverSeq, err := m.db.UIDToServerSeq(ctx, m.selectedMailbox.ID, uid)
1328 if err != nil {
1329 logger.ErrorContext(ctx, "unable to convert to server seq", "error", err)
1330 return errInternalServerError
1331 }
1332 expungeServerSeq[i] = serverSeq
1333 }
1334
1335 // Fetch message envelopes BEFORE the move (after the move, dest UIDs are
1336 // reassigned and source UIDs no longer resolve to live rows).
1337 movedMessages, err := m.db.GetMailboxMessagesByUID(ctx, m.user.ID, m.selectedMailbox.ID, filteredSourceUIDs)
1338 if err != nil {
1339 logger.ErrorContext(ctx, "unable to fetch messages for policy hook", "error", err)
1340 return errInternalServerError
1341 }
1342
1343 destUIDs, err := m.db.MoveMessagesToMailbox(ctx, m.selectedMailbox.ID, destMailboxID, filteredSourceUIDs)
1344 if err != nil {
1345 logger.ErrorContext(ctx, "unable to move messages to mailbox", "error", err)
1346 return errInternalServerError
1347 }
1348
1349 // Fire the on_message_moved policy hook for each moved message. The move
1350 // has already succeeded, so policy errors are logged but do not fail the
1351 // MOVE command.
1352 policyUser := policy.UserToStarlark(m.user, m.db)
1353 for _, mv := range movedMessages {
1354 msg := policy.MessageContextFromEnvelope(mv.EnvelopeFrom.String, mv.EnvelopeSubject.String, mv.RFC822Size)
1355 if err := m.policyEngine.OnMessageMoved(policyUser, msg, m.selectedMailbox.Name, dest); err != nil {
1356 logger.ErrorContext(ctx, "on_message_moved policy hook error", "error", err, "uid", mv.UID)
1357 }
1358 }
1359
1360 // RFC 9051: Servers are also REQUIRED to send the COPYUID response code in an untagged OK before sending
1361 // EXPUNGE or similar responses.
1362 err = w.WriteCopyData(&imap.CopyData{
1363 UIDValidity: uidValidity,
1364 SourceUIDs: internalIDToUIDSet(filteredSourceUIDs),
1365 DestUIDs: internalIDToUIDSet(destUIDs),
1366 })
1367 if err != nil {
1368 logger.ErrorContext(ctx, "unable to write copy data", "error", err)
1369 return errInternalServerError
1370 }
1371
1372 slices.Reverse(expungeServerSeq)
1373 for _, serverSeq := range expungeServerSeq {
1374 clientSeq := m.sessionTracker.EncodeSeqNum(serverSeq)
1375 if clientSeq == 0 {
1376 continue
1377 }
1378 m.mTracker.get(m.user.ID, m.selectedMailbox.ID).QueueExpunge(clientSeq)
1379 }
1380
1381 destMsgCount, err := m.db.MailboxMessageCount(ctx, destMailboxID)
1382 if err != nil {
1383 logger.ErrorContext(ctx, "unable to get mailbox message count", "error", err)
1384 return errInternalServerError
1385 }
1386 destMailboxTracker := m.mTracker.getMailboxTracker(m.user.ID, destMailboxID, destMsgCount)
1387 destMailboxTracker.QueueNumMessages(destMsgCount)
1388
1389 logger.Info("end MOVE")
1390 return nil
1391}
1392
1393func internalIDToUIDSet(internalID []uint32) imap.UIDSet {
1394 out := imap.UIDSet{}
1395 for _, i := range internalID {
1396 out.AddNum(imap.UID(i))
1397 }
1398 if len(out) == 0 {
1399 return nil // empty UIDSets are not allowed
1400 }
1401 return out
1402}
1403
1404func resolveRange[T ~uint32](start, stop, last T) (T, T) {
1405 if start == 0 {
1406 start = last
1407 }
1408 if stop == 0 {
1409 stop = last
1410 }
1411 return start, stop
1412}
1413
1414// resolveUIDs resolves a given imap.NumSet to the internal representation of imap UIDs which is uint32
1415func (m *imapSession) resolveUIDs(ctx context.Context, numSet imap.NumSet) ([]uint32, error) {
1416 staticSet, err := m.resolveDynamicSet(ctx, numSet)
1417 if err != nil {
1418 if errors.Is(err, sql.ErrNoRows) {
1419 return nil, nil
1420 }
1421 return nil, err
1422 }
1423
1424 switch v := staticSet.(type) {
1425 case imap.UIDSet:
1426 nums, ok := v.Nums()
1427 if !ok {
1428 // This should theoretically not happen after dynamic resolution,
1429 // but we handle it defensively.
1430 return nil, fmt.Errorf("unresolvable UIDSet")
1431 }
1432 out := make([]uint32, len(nums))
1433 for i, num := range nums {
1434 out[i] = uint32(num)
1435 }
1436 return out, nil
1437
1438 case imap.SeqSet:
1439 nums, ok := v.Nums()
1440 if !ok {
1441 return nil, fmt.Errorf("unresolvable SeqSet")
1442 }
1443
1444 uids := make([]uint32, 0, len(nums))
1445 for _, num := range nums {
1446 uid, err := m.db.ServerSeqToUID(ctx, m.selectedMailbox.ID, m.sessionTracker.DecodeSeqNum(num))
1447 if err != nil {
1448 if errors.Is(err, sql.ErrNoRows) {
1449 return nil, errDBSequenceOutOfRange
1450 }
1451 return nil, err
1452 }
1453 uids = append(uids, uid)
1454 }
1455 return uids, nil
1456 }
1457
1458 return nil, fmt.Errorf("unsupported NumSet type %T", numSet)
1459}
1460
1461// resolveDynamicSet resolves the NumSet to the client view, so it can be converted later to the server view
1462func (m *imapSession) resolveDynamicSet(ctx context.Context, numSet imap.NumSet) (imap.NumSet, error) {
1463 if !numSet.Dynamic() {
1464 return numSet, nil
1465 }
1466
1467 if numSet.String() == "$" {
1468 return m.searchRes, nil
1469 }
1470
1471 switch v := numSet.(type) {
1472 case imap.UIDSet:
1473 last, err := m.db.GetLastUID(ctx, m.user.ID, m.selectedMailbox.ID)
1474 if err != nil {
1475 return nil, err
1476 }
1477 if last == 0 {
1478 return imap.UIDSet{}, nil // empty mailbox: UID commands match nothing
1479 }
1480 out := imap.UIDSet{}
1481 for _, r := range v {
1482 start, stop := resolveRange(r.Start, r.Stop, last)
1483 out.AddRange(start, stop)
1484 }
1485 return out, nil
1486
1487 case imap.SeqSet:
1488 last, err := m.db.MailboxMessageCount(ctx, m.selectedMailbox.ID)
1489 if err != nil {
1490 return nil, err
1491 }
1492 if last == 0 {
1493 return imap.SeqSet{}, nil // empty mailbox: "*" is 0, so any seq set matches nothing
1494 }
1495 out := imap.SeqSet{}
1496 for _, r := range v {
1497 start, stop := resolveRange(r.Start, r.Stop, m.sessionTracker.EncodeSeqNum(last))
1498 out.AddRange(start, stop)
1499 }
1500 return out, nil
1501
1502 default:
1503 return numSet, nil
1504 }
1505}