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