1// Package imapserver implements an IMAPv4 server, rev2 (RFC 9051) and rev1 with extensions (RFC 3501 and more).
2package imapserver
3
4/*
5Implementation notes
6
7IMAP4rev2 includes functionality that was in extensions for IMAP4rev1. The
8extensions sometimes include features not in IMAP4rev2. We want IMAP4rev1-only
9implementations to use extensions, so we implement the full feature set of the
10extension and announce it as capability. The extensions: LITERAL+, IDLE,
11NAMESPACE, BINARY, UNSELECT, UIDPLUS, ESEARCH, SEARCHRES, SASL-IR, ENABLE,
12LIST-EXTENDED, SPECIAL-USE, MOVE, UTF8=ONLY.
13
14We take a liberty with UTF8=ONLY. We are supposed to wait for ENABLE of
15UTF8=ACCEPT or IMAP4rev2 before we respond with quoted strings that contain
16non-ASCII UTF-8. Until that's enabled, we do use UTF-7 for mailbox names. See
17../rfc/6855:251
18
19- We never execute multiple commands at the same time for a connection. We expect a client to open multiple connections instead. ../rfc/9051:1110
20- Do not write output on a connection with an account lock held. Writing can block, a slow client could block account operations.
21- When handling commands that modify the selected mailbox, always check that the mailbox is not opened readonly. And always revalidate the selected mailbox, another session may have deleted the mailbox.
22- After making changes to an account/mailbox/message, you must broadcast changes. You must do this with the account lock held. Otherwise, other later changes (e.g. message deliveries) may be made and broadcast before changes that were made earlier. Make sure to commit changes in the database first, because the commit may fail.
23- Mailbox hierarchies are slash separated, no leading slash. We keep the case, except INBOX is renamed to Inbox, also for submailboxes in INBOX. We don't allow existence of a child where its parent does not exist. We have no \NoInferiors or \NoSelect. Newly created mailboxes are automatically subscribed.
24- For CONDSTORE and QRESYNC support, we set "modseq" for each change/expunge. Once expunged, a modseq doesn't change anymore. We don't yet remove old expunged records. The records aren't too big. Next step may be to let an admin reclaim space manually.
25*/
26
27/*
28- todo: do not return binary data for a fetch body. at least not for imap4rev1. we should be encoding it as base64?
29- todo: on expunge we currently remove the message even if other sessions still have a reference to the uid. if they try to query the uid, they'll get an error. we could be nicer and only actually remove the message when the last reference has gone. we could add a new flag to store.Message marking the message as expunged, not give new session access to such messages, and make store remove them at startup, and clean them when the last session referencing the session goes. however, it will get much more complicated. renaming messages would need special handling. and should we do the same for removed mailboxes?
30- todo: try to recover from syntax errors when the last command line ends with a }, i.e. a literal. we currently abort the entire connection. we may want to read some amount of literal data and continue with a next command.
31- todo future: more extensions: OBJECTID, MULTISEARCH, REPLACE, NOTIFY, CATENATE, MULTIAPPEND, SORT, THREAD, CREATE-SPECIAL-USE.
32*/
33
34import (
35 "bufio"
36 "bytes"
37 "context"
38 "crypto/md5"
39 "crypto/sha1"
40 "crypto/sha256"
41 "crypto/tls"
42 "encoding/base64"
43 "errors"
44 "fmt"
45 "hash"
46 "io"
47 "math"
48 "net"
49 "os"
50 "path"
51 "path/filepath"
52 "regexp"
53 "runtime/debug"
54 "sort"
55 "strings"
56 "sync"
57 "time"
58
59 "golang.org/x/exp/maps"
60 "golang.org/x/exp/slices"
61 "golang.org/x/exp/slog"
62
63 "github.com/prometheus/client_golang/prometheus"
64 "github.com/prometheus/client_golang/prometheus/promauto"
65
66 "github.com/mjl-/bstore"
67
68 "github.com/mjl-/mox/config"
69 "github.com/mjl-/mox/message"
70 "github.com/mjl-/mox/metrics"
71 "github.com/mjl-/mox/mlog"
72 "github.com/mjl-/mox/mox-"
73 "github.com/mjl-/mox/moxio"
74 "github.com/mjl-/mox/moxvar"
75 "github.com/mjl-/mox/ratelimit"
76 "github.com/mjl-/mox/scram"
77 "github.com/mjl-/mox/store"
78)
79
80var (
81 metricIMAPConnection = promauto.NewCounterVec(
82 prometheus.CounterOpts{
83 Name: "mox_imap_connection_total",
84 Help: "Incoming IMAP connections.",
85 },
86 []string{
87 "service", // imap, imaps
88 },
89 )
90 metricIMAPCommands = promauto.NewHistogramVec(
91 prometheus.HistogramOpts{
92 Name: "mox_imap_command_duration_seconds",
93 Help: "IMAP command duration and result codes in seconds.",
94 Buckets: []float64{0.001, 0.005, 0.01, 0.05, 0.100, 0.5, 1, 5, 10, 20},
95 },
96 []string{
97 "cmd",
98 "result", // ok, panic, ioerror, badsyntax, servererror, usererror, error
99 },
100 )
101)
102
103var limiterConnectionrate, limiterConnections *ratelimit.Limiter
104
105func init() {
106 // Also called by tests, so they don't trigger the rate limiter.
107 limitersInit()
108}
109
110func limitersInit() {
111 mox.LimitersInit()
112 limiterConnectionrate = &ratelimit.Limiter{
113 WindowLimits: []ratelimit.WindowLimit{
114 {
115 Window: time.Minute,
116 Limits: [...]int64{300, 900, 2700},
117 },
118 },
119 }
120 limiterConnections = &ratelimit.Limiter{
121 WindowLimits: []ratelimit.WindowLimit{
122 {
123 Window: time.Duration(math.MaxInt64), // All of time.
124 Limits: [...]int64{30, 90, 270},
125 },
126 },
127 }
128}
129
130// Delay after bad/suspicious behaviour. Tests set these to zero.
131var badClientDelay = time.Second // Before reads and after 1-byte writes for probably spammers.
132var authFailDelay = time.Second // After authentication failure.
133
134// Capabilities (extensions) the server supports. Connections will add a few more, e.g. STARTTLS, LOGINDISABLED, AUTH=PLAIN.
135// ENABLE: ../rfc/5161
136// LITERAL+: ../rfc/7888
137// IDLE: ../rfc/2177
138// SASL-IR: ../rfc/4959
139// BINARY: ../rfc/3516
140// UNSELECT: ../rfc/3691
141// UIDPLUS: ../rfc/4315
142// ESEARCH: ../rfc/4731
143// SEARCHRES: ../rfc/5182
144// MOVE: ../rfc/6851
145// UTF8=ONLY: ../rfc/6855
146// LIST-EXTENDED: ../rfc/5258
147// SPECIAL-USE: ../rfc/6154
148// LIST-STATUS: ../rfc/5819
149// ID: ../rfc/2971
150// AUTH=SCRAM-SHA-256-PLUS and AUTH=SCRAM-SHA-256: ../rfc/7677 ../rfc/5802
151// AUTH=SCRAM-SHA-1-PLUS and AUTH=SCRAM-SHA-1: ../rfc/5802
152// AUTH=CRAM-MD5: ../rfc/2195
153// APPENDLIMIT, we support the max possible size, 1<<63 - 1: ../rfc/7889:129
154// CONDSTORE: ../rfc/7162:411
155// QRESYNC: ../rfc/7162:1323
156// STATUS=SIZE: ../rfc/8438 ../rfc/9051:8024
157//
158// We always announce support for SCRAM PLUS-variants, also on connections without
159// TLS. The client should not be selecting PLUS variants on non-TLS connections,
160// instead opting to do the bare SCRAM variant without indicating the server claims
161// to support the PLUS variant (skipping the server downgrade detection check).
162const serverCapabilities = "IMAP4rev2 IMAP4rev1 ENABLE LITERAL+ IDLE SASL-IR BINARY UNSELECT UIDPLUS ESEARCH SEARCHRES MOVE UTF8=ACCEPT LIST-EXTENDED SPECIAL-USE LIST-STATUS AUTH=SCRAM-SHA-256-PLUS AUTH=SCRAM-SHA-256 AUTH=SCRAM-SHA-1-PLUS AUTH=SCRAM-SHA-1 AUTH=CRAM-MD5 ID APPENDLIMIT=9223372036854775807 CONDSTORE QRESYNC STATUS=SIZE"
163
164type conn struct {
165 cid int64
166 state state
167 conn net.Conn
168 tls bool // Whether TLS has been initialized.
169 br *bufio.Reader // From remote, with TLS unwrapped in case of TLS.
170 line chan lineErr // If set, instead of reading from br, a line is read from this channel. For reading a line in IDLE while also waiting for mailbox/account updates.
171 lastLine string // For detecting if syntax error is fatal, i.e. if this ends with a literal. Without crlf.
172 bw *bufio.Writer // To remote, with TLS added in case of TLS.
173 tr *moxio.TraceReader // Kept to change trace level when reading/writing cmd/auth/data.
174 tw *moxio.TraceWriter
175 slow bool // If set, reads are done with a 1 second sleep, and writes are done 1 byte at a time, to keep spammers busy.
176 lastlog time.Time // For printing time since previous log line.
177 tlsConfig *tls.Config // TLS config to use for handshake.
178 remoteIP net.IP
179 noRequireSTARTTLS bool
180 cmd string // Currently executing, for deciding to applyChanges and logging.
181 cmdMetric string // Currently executing, for metrics.
182 cmdStart time.Time
183 ncmds int // Number of commands processed. Used to abort connection when first incoming command is unknown/invalid.
184 log mlog.Log
185 enabled map[capability]bool // All upper-case.
186
187 // Set by SEARCH with SAVE. Can be used by commands accepting a sequence-set with
188 // value "$". When used, UIDs must be verified to still exist, because they may
189 // have been expunged. Cleared by a SELECT or EXAMINE.
190 // Nil means no searchResult is present. An empty list is a valid searchResult,
191 // just not matching any messages.
192 // ../rfc/5182:13 ../rfc/9051:4040
193 searchResult []store.UID
194
195 // Only when authenticated.
196 authFailed int // Number of failed auth attempts. For slowing down remote with many failures.
197 username string // Full username as used during login.
198 account *store.Account
199 comm *store.Comm // For sending/receiving changes on mailboxes in account, e.g. from messages incoming on smtp, or another imap client.
200
201 mailboxID int64 // Only for StateSelected.
202 readonly bool // If opened mailbox is readonly.
203 uids []store.UID // UIDs known in this session, sorted. todo future: store more space-efficiently, as ranges.
204}
205
206// capability for use with ENABLED and CAPABILITY. We always keep this upper case,
207// e.g. IMAP4REV2. These values are treated case-insensitive, but it's easier for
208// comparison to just always have the same case.
209type capability string
210
211const (
212 capIMAP4rev2 capability = "IMAP4REV2"
213 capUTF8Accept capability = "UTF8=ACCEPT"
214 capCondstore capability = "CONDSTORE"
215 capQresync capability = "QRESYNC"
216)
217
218type lineErr struct {
219 line string
220 err error
221}
222
223type state byte
224
225const (
226 stateNotAuthenticated state = iota
227 stateAuthenticated
228 stateSelected
229)
230
231func stateCommands(cmds ...string) map[string]struct{} {
232 r := map[string]struct{}{}
233 for _, cmd := range cmds {
234 r[cmd] = struct{}{}
235 }
236 return r
237}
238
239var (
240 commandsStateAny = stateCommands("capability", "noop", "logout", "id")
241 commandsStateNotAuthenticated = stateCommands("starttls", "authenticate", "login")
242 commandsStateAuthenticated = stateCommands("enable", "select", "examine", "create", "delete", "rename", "subscribe", "unsubscribe", "list", "namespace", "status", "append", "idle", "lsub")
243 commandsStateSelected = stateCommands("close", "unselect", "expunge", "search", "fetch", "store", "copy", "move", "uid expunge", "uid search", "uid fetch", "uid store", "uid copy", "uid move")
244)
245
246var commands = map[string]func(c *conn, tag, cmd string, p *parser){
247 // Any state.
248 "capability": (*conn).cmdCapability,
249 "noop": (*conn).cmdNoop,
250 "logout": (*conn).cmdLogout,
251 "id": (*conn).cmdID,
252
253 // Notauthenticated.
254 "starttls": (*conn).cmdStarttls,
255 "authenticate": (*conn).cmdAuthenticate,
256 "login": (*conn).cmdLogin,
257
258 // Authenticated and selected.
259 "enable": (*conn).cmdEnable,
260 "select": (*conn).cmdSelect,
261 "examine": (*conn).cmdExamine,
262 "create": (*conn).cmdCreate,
263 "delete": (*conn).cmdDelete,
264 "rename": (*conn).cmdRename,
265 "subscribe": (*conn).cmdSubscribe,
266 "unsubscribe": (*conn).cmdUnsubscribe,
267 "list": (*conn).cmdList,
268 "lsub": (*conn).cmdLsub,
269 "namespace": (*conn).cmdNamespace,
270 "status": (*conn).cmdStatus,
271 "append": (*conn).cmdAppend,
272 "idle": (*conn).cmdIdle,
273
274 // Selected.
275 "check": (*conn).cmdCheck,
276 "close": (*conn).cmdClose,
277 "unselect": (*conn).cmdUnselect,
278 "expunge": (*conn).cmdExpunge,
279 "uid expunge": (*conn).cmdUIDExpunge,
280 "search": (*conn).cmdSearch,
281 "uid search": (*conn).cmdUIDSearch,
282 "fetch": (*conn).cmdFetch,
283 "uid fetch": (*conn).cmdUIDFetch,
284 "store": (*conn).cmdStore,
285 "uid store": (*conn).cmdUIDStore,
286 "copy": (*conn).cmdCopy,
287 "uid copy": (*conn).cmdUIDCopy,
288 "move": (*conn).cmdMove,
289 "uid move": (*conn).cmdUIDMove,
290}
291
292var errIO = errors.New("io error") // For read/write errors and errors that should close the connection.
293var errProtocol = errors.New("protocol error") // For protocol errors for which a stack trace should be printed.
294
295var sanityChecks bool
296
297// check err for sanity.
298// if not nil and checkSanity true (set during tests), then panic. if not nil during normal operation, just log.
299func (c *conn) xsanity(err error, format string, args ...any) {
300 if err == nil {
301 return
302 }
303 if sanityChecks {
304 panic(fmt.Errorf("%s: %s", fmt.Sprintf(format, args...), err))
305 }
306 c.log.Errorx(fmt.Sprintf(format, args...), err)
307}
308
309type msgseq uint32
310
311// Listen initializes all imap listeners for the configuration, and stores them for Serve to start them.
312func Listen() {
313 names := maps.Keys(mox.Conf.Static.Listeners)
314 sort.Strings(names)
315 for _, name := range names {
316 listener := mox.Conf.Static.Listeners[name]
317
318 var tlsConfig *tls.Config
319 if listener.TLS != nil {
320 tlsConfig = listener.TLS.Config
321 }
322
323 if listener.IMAP.Enabled {
324 port := config.Port(listener.IMAP.Port, 143)
325 for _, ip := range listener.IPs {
326 listen1("imap", name, ip, port, tlsConfig, false, listener.IMAP.NoRequireSTARTTLS)
327 }
328 }
329
330 if listener.IMAPS.Enabled {
331 port := config.Port(listener.IMAPS.Port, 993)
332 for _, ip := range listener.IPs {
333 listen1("imaps", name, ip, port, tlsConfig, true, false)
334 }
335 }
336 }
337}
338
339var servers []func()
340
341func listen1(protocol, listenerName, ip string, port int, tlsConfig *tls.Config, xtls, noRequireSTARTTLS bool) {
342 log := mlog.New("imapserver", nil)
343 addr := net.JoinHostPort(ip, fmt.Sprintf("%d", port))
344 if os.Getuid() == 0 {
345 log.Print("listening for imap",
346 slog.String("listener", listenerName),
347 slog.String("addr", addr),
348 slog.String("protocol", protocol))
349 }
350 network := mox.Network(ip)
351 ln, err := mox.Listen(network, addr)
352 if err != nil {
353 log.Fatalx("imap: listen for imap", err, slog.String("protocol", protocol), slog.String("listener", listenerName))
354 }
355 if xtls {
356 ln = tls.NewListener(ln, tlsConfig)
357 }
358
359 serve := func() {
360 for {
361 conn, err := ln.Accept()
362 if err != nil {
363 log.Infox("imap: accept", err, slog.String("protocol", protocol), slog.String("listener", listenerName))
364 continue
365 }
366
367 metricIMAPConnection.WithLabelValues(protocol).Inc()
368 go serve(listenerName, mox.Cid(), tlsConfig, conn, xtls, noRequireSTARTTLS)
369 }
370 }
371
372 servers = append(servers, serve)
373}
374
375// Serve starts serving on all listeners, launching a goroutine per listener.
376func Serve() {
377 for _, serve := range servers {
378 go serve()
379 }
380 servers = nil
381}
382
383// returns whether this connection accepts utf-8 in strings.
384func (c *conn) utf8strings() bool {
385 return c.enabled[capIMAP4rev2] || c.enabled[capUTF8Accept]
386}
387
388func (c *conn) encodeMailbox(s string) string {
389 if c.utf8strings() {
390 return s
391 }
392 return utf7encode(s)
393}
394
395func (c *conn) xdbwrite(fn func(tx *bstore.Tx)) {
396 err := c.account.DB.Write(context.TODO(), func(tx *bstore.Tx) error {
397 fn(tx)
398 return nil
399 })
400 xcheckf(err, "transaction")
401}
402
403func (c *conn) xdbread(fn func(tx *bstore.Tx)) {
404 err := c.account.DB.Read(context.TODO(), func(tx *bstore.Tx) error {
405 fn(tx)
406 return nil
407 })
408 xcheckf(err, "transaction")
409}
410
411// Closes the currently selected/active mailbox, setting state from selected to authenticated.
412// Does not remove messages marked for deletion.
413func (c *conn) unselect() {
414 if c.state == stateSelected {
415 c.state = stateAuthenticated
416 }
417 c.mailboxID = 0
418 c.uids = nil
419}
420
421func (c *conn) setSlow(on bool) {
422 if on && !c.slow {
423 c.log.Debug("connection changed to slow")
424 } else if !on && c.slow {
425 c.log.Debug("connection restored to regular pace")
426 }
427 c.slow = on
428}
429
430// Write makes a connection an io.Writer. It panics for i/o errors. These errors
431// are handled in the connection command loop.
432func (c *conn) Write(buf []byte) (int, error) {
433 chunk := len(buf)
434 if c.slow {
435 chunk = 1
436 }
437
438 var n int
439 for len(buf) > 0 {
440 err := c.conn.SetWriteDeadline(time.Now().Add(30 * time.Second))
441 c.log.Check(err, "setting write deadline")
442
443 nn, err := c.conn.Write(buf[:chunk])
444 if err != nil {
445 panic(fmt.Errorf("write: %s (%w)", err, errIO))
446 }
447 n += nn
448 buf = buf[chunk:]
449 if len(buf) > 0 && badClientDelay > 0 {
450 mox.Sleep(mox.Context, badClientDelay)
451 }
452 }
453 return n, nil
454}
455
456func (c *conn) xtrace(level slog.Level) func() {
457 c.xflush()
458 c.tr.SetTrace(level)
459 c.tw.SetTrace(level)
460 return func() {
461 c.xflush()
462 c.tr.SetTrace(mlog.LevelTrace)
463 c.tw.SetTrace(mlog.LevelTrace)
464 }
465}
466
467// Cache of line buffers for reading commands.
468// QRESYNC recommends 8k max line lengths. ../rfc/7162:2159
469var bufpool = moxio.NewBufpool(8, 16*1024)
470
471// read line from connection, not going through line channel.
472func (c *conn) readline0() (string, error) {
473 if c.slow && badClientDelay > 0 {
474 mox.Sleep(mox.Context, badClientDelay)
475 }
476
477 d := 30 * time.Minute
478 if c.state == stateNotAuthenticated {
479 d = 30 * time.Second
480 }
481 err := c.conn.SetReadDeadline(time.Now().Add(d))
482 c.log.Check(err, "setting read deadline")
483
484 line, err := bufpool.Readline(c.log, c.br)
485 if err != nil && errors.Is(err, moxio.ErrLineTooLong) {
486 return "", fmt.Errorf("%s (%w)", err, errProtocol)
487 } else if err != nil {
488 return "", fmt.Errorf("%s (%w)", err, errIO)
489 }
490 return line, nil
491}
492
493func (c *conn) lineChan() chan lineErr {
494 if c.line == nil {
495 c.line = make(chan lineErr, 1)
496 go func() {
497 line, err := c.readline0()
498 c.line <- lineErr{line, err}
499 }()
500 }
501 return c.line
502}
503
504// readline from either the c.line channel, or otherwise read from connection.
505func (c *conn) readline(readCmd bool) string {
506 var line string
507 var err error
508 if c.line != nil {
509 le := <-c.line
510 c.line = nil
511 line, err = le.line, le.err
512 } else {
513 line, err = c.readline0()
514 }
515 if err != nil {
516 if readCmd && errors.Is(err, os.ErrDeadlineExceeded) {
517 err := c.conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
518 c.log.Check(err, "setting write deadline")
519 c.writelinef("* BYE inactive")
520 }
521 if !errors.Is(err, errIO) && !errors.Is(err, errProtocol) {
522 err = fmt.Errorf("%s (%w)", err, errIO)
523 }
524 panic(err)
525 }
526 c.lastLine = line
527
528 // We typically respond immediately (IDLE is an exception).
529 // The client may not be reading, or may have disappeared.
530 // Don't wait more than 5 minutes before closing down the connection.
531 // The write deadline is managed in IDLE as well.
532 // For unauthenticated connections, we require the client to read faster.
533 wd := 5 * time.Minute
534 if c.state == stateNotAuthenticated {
535 wd = 30 * time.Second
536 }
537 err = c.conn.SetWriteDeadline(time.Now().Add(wd))
538 c.log.Check(err, "setting write deadline")
539
540 return line
541}
542
543// write tagged command response, but first write pending changes.
544func (c *conn) writeresultf(format string, args ...any) {
545 c.bwriteresultf(format, args...)
546 c.xflush()
547}
548
549// write buffered tagged command response, but first write pending changes.
550func (c *conn) bwriteresultf(format string, args ...any) {
551 switch c.cmd {
552 case "fetch", "store", "search":
553 // ../rfc/9051:5862 ../rfc/7162:2033
554 default:
555 if c.comm != nil {
556 c.applyChanges(c.comm.Get(), false)
557 }
558 }
559 c.bwritelinef(format, args...)
560}
561
562func (c *conn) writelinef(format string, args ...any) {
563 c.bwritelinef(format, args...)
564 c.xflush()
565}
566
567// Buffer line for write.
568func (c *conn) bwritelinef(format string, args ...any) {
569 format += "\r\n"
570 fmt.Fprintf(c.bw, format, args...)
571}
572
573func (c *conn) xflush() {
574 err := c.bw.Flush()
575 xcheckf(err, "flush") // Should never happen, the Write caused by the Flush should panic on i/o error.
576}
577
578func (c *conn) readCommand(tag *string) (cmd string, p *parser) {
579 line := c.readline(true)
580 p = newParser(line, c)
581 p.context("tag")
582 *tag = p.xtag()
583 p.context("command")
584 p.xspace()
585 cmd = p.xcommand()
586 return cmd, newParser(p.remainder(), c)
587}
588
589func (c *conn) xreadliteral(size int64, sync bool) string {
590 if sync {
591 c.writelinef("+ ")
592 }
593 buf := make([]byte, size)
594 if size > 0 {
595 if err := c.conn.SetReadDeadline(time.Now().Add(30 * time.Second)); err != nil {
596 c.log.Errorx("setting read deadline", err)
597 }
598
599 _, err := io.ReadFull(c.br, buf)
600 if err != nil {
601 // Cannot use xcheckf due to %w handling of errIO.
602 panic(fmt.Errorf("reading literal: %s (%w)", err, errIO))
603 }
604 }
605 return string(buf)
606}
607
608func (c *conn) xhighestModSeq(tx *bstore.Tx, mailboxID int64) store.ModSeq {
609 qms := bstore.QueryTx[store.Message](tx)
610 qms.FilterNonzero(store.Message{MailboxID: mailboxID})
611 qms.SortDesc("ModSeq")
612 qms.Limit(1)
613 m, err := qms.Get()
614 if err == bstore.ErrAbsent {
615 return store.ModSeq(0)
616 }
617 xcheckf(err, "looking up highest modseq for mailbox")
618 return m.ModSeq
619}
620
621var cleanClose struct{} // Sentinel value for panic/recover indicating clean close of connection.
622
623func serve(listenerName string, cid int64, tlsConfig *tls.Config, nc net.Conn, xtls, noRequireSTARTTLS bool) {
624 var remoteIP net.IP
625 if a, ok := nc.RemoteAddr().(*net.TCPAddr); ok {
626 remoteIP = a.IP
627 } else {
628 // For net.Pipe, during tests.
629 remoteIP = net.ParseIP("127.0.0.10")
630 }
631
632 c := &conn{
633 cid: cid,
634 conn: nc,
635 tls: xtls,
636 lastlog: time.Now(),
637 tlsConfig: tlsConfig,
638 remoteIP: remoteIP,
639 noRequireSTARTTLS: noRequireSTARTTLS,
640 enabled: map[capability]bool{},
641 cmd: "(greeting)",
642 cmdStart: time.Now(),
643 }
644 var logmutex sync.Mutex
645 c.log = mlog.New("imapserver", nil).WithFunc(func() []slog.Attr {
646 logmutex.Lock()
647 defer logmutex.Unlock()
648 now := time.Now()
649 l := []slog.Attr{
650 slog.Int64("cid", c.cid),
651 slog.Duration("delta", now.Sub(c.lastlog)),
652 }
653 c.lastlog = now
654 if c.username != "" {
655 l = append(l, slog.String("username", c.username))
656 }
657 return l
658 })
659 c.tr = moxio.NewTraceReader(c.log, "C: ", c.conn)
660 c.tw = moxio.NewTraceWriter(c.log, "S: ", c)
661 // todo: tracing should be done on whatever comes out of c.br. the remote connection write a command plus data, and bufio can read it in one read, causing a command parser that sets the tracing level to data to have no effect. we are now typically logging sent messages, when mail clients append to the Sent mailbox.
662 c.br = bufio.NewReader(c.tr)
663 c.bw = bufio.NewWriter(c.tw)
664
665 // Many IMAP connections use IDLE to wait for new incoming messages. We'll enable
666 // keepalive to get a higher chance of the connection staying alive, or otherwise
667 // detecting broken connections early.
668 xconn := c.conn
669 if xtls {
670 xconn = c.conn.(*tls.Conn).NetConn()
671 }
672 if tcpconn, ok := xconn.(*net.TCPConn); ok {
673 if err := tcpconn.SetKeepAlivePeriod(5 * time.Minute); err != nil {
674 c.log.Errorx("setting keepalive period", err)
675 } else if err := tcpconn.SetKeepAlive(true); err != nil {
676 c.log.Errorx("enabling keepalive", err)
677 }
678 }
679
680 c.log.Info("new connection",
681 slog.Any("remote", c.conn.RemoteAddr()),
682 slog.Any("local", c.conn.LocalAddr()),
683 slog.Bool("tls", xtls),
684 slog.String("listener", listenerName))
685
686 defer func() {
687 c.conn.Close()
688
689 if c.account != nil {
690 c.comm.Unregister()
691 err := c.account.Close()
692 c.xsanity(err, "close account")
693 c.account = nil
694 c.comm = nil
695 }
696
697 x := recover()
698 if x == nil || x == cleanClose {
699 c.log.Info("connection closed")
700 } else if err, ok := x.(error); ok && isClosed(err) {
701 c.log.Infox("connection closed", err)
702 } else {
703 c.log.Error("unhandled panic", slog.Any("err", x))
704 debug.PrintStack()
705 metrics.PanicInc(metrics.Imapserver)
706 }
707 }()
708
709 select {
710 case <-mox.Shutdown.Done():
711 // ../rfc/9051:5381
712 c.writelinef("* BYE mox shutting down")
713 return
714 default:
715 }
716
717 if !limiterConnectionrate.Add(c.remoteIP, time.Now(), 1) {
718 c.writelinef("* BYE connection rate from your ip or network too high, slow down please")
719 return
720 }
721
722 // If remote IP/network resulted in too many authentication failures, refuse to serve.
723 if !mox.LimiterFailedAuth.CanAdd(c.remoteIP, time.Now(), 1) {
724 metrics.AuthenticationRatelimitedInc("imap")
725 c.log.Debug("refusing connection due to many auth failures", slog.Any("remoteip", c.remoteIP))
726 c.writelinef("* BYE too many auth failures")
727 return
728 }
729
730 if !limiterConnections.Add(c.remoteIP, time.Now(), 1) {
731 c.log.Debug("refusing connection due to many open connections", slog.Any("remoteip", c.remoteIP))
732 c.writelinef("* BYE too many open connections from your ip or network")
733 return
734 }
735 defer limiterConnections.Add(c.remoteIP, time.Now(), -1)
736
737 // We register and unregister the original connection, in case it c.conn is
738 // replaced with a TLS connection later on.
739 mox.Connections.Register(nc, "imap", listenerName)
740 defer mox.Connections.Unregister(nc)
741
742 c.writelinef("* OK [CAPABILITY %s] mox imap", c.capabilities())
743
744 for {
745 c.command()
746 c.xflush() // For flushing errors, or possibly commands that did not flush explicitly.
747 }
748}
749
750// isClosed returns whether i/o failed, typically because the connection is closed.
751// For connection errors, we often want to generate fewer logs.
752func isClosed(err error) bool {
753 return errors.Is(err, errIO) || errors.Is(err, errProtocol) || moxio.IsClosed(err)
754}
755
756func (c *conn) command() {
757 var tag, cmd, cmdlow string
758 var p *parser
759
760 defer func() {
761 var result string
762 defer func() {
763 metricIMAPCommands.WithLabelValues(c.cmdMetric, result).Observe(float64(time.Since(c.cmdStart)) / float64(time.Second))
764 }()
765
766 logFields := []slog.Attr{
767 slog.String("cmd", c.cmd),
768 slog.Duration("duration", time.Since(c.cmdStart)),
769 }
770 c.cmd = ""
771
772 x := recover()
773 if x == nil || x == cleanClose {
774 c.log.Debug("imap command done", logFields...)
775 result = "ok"
776 if x == cleanClose {
777 panic(x)
778 }
779 return
780 }
781 err, ok := x.(error)
782 if !ok {
783 c.log.Error("imap command panic", append([]slog.Attr{slog.Any("panic", x)}, logFields...)...)
784 result = "panic"
785 panic(x)
786 }
787
788 var sxerr syntaxError
789 var uerr userError
790 var serr serverError
791 if isClosed(err) {
792 c.log.Infox("imap command ioerror", err, logFields...)
793 result = "ioerror"
794 if errors.Is(err, errProtocol) {
795 debug.PrintStack()
796 }
797 panic(err)
798 } else if errors.As(err, &sxerr) {
799 result = "badsyntax"
800 if c.ncmds == 0 {
801 // Other side is likely speaking something else than IMAP, send error message and
802 // stop processing because there is a good chance whatever they sent has multiple
803 // lines.
804 c.writelinef("* BYE please try again speaking imap")
805 panic(errIO)
806 }
807 c.log.Debugx("imap command syntax error", sxerr.err, logFields...)
808 c.log.Info("imap syntax error", slog.String("lastline", c.lastLine))
809 fatal := strings.HasSuffix(c.lastLine, "+}")
810 if fatal {
811 err := c.conn.SetWriteDeadline(time.Now().Add(5 * time.Second))
812 c.log.Check(err, "setting write deadline")
813 }
814 if sxerr.line != "" {
815 c.bwritelinef("%s", sxerr.line)
816 }
817 code := ""
818 if sxerr.code != "" {
819 code = "[" + sxerr.code + "] "
820 }
821 c.bwriteresultf("%s BAD %s%s unrecognized syntax/command: %v", tag, code, cmd, sxerr.errmsg)
822 if fatal {
823 c.xflush()
824 panic(fmt.Errorf("aborting connection after syntax error for command with non-sync literal: %w", errProtocol))
825 }
826 } else if errors.As(err, &serr) {
827 result = "servererror"
828 c.log.Errorx("imap command server error", err, logFields...)
829 debug.PrintStack()
830 c.bwriteresultf("%s NO %s %v", tag, cmd, err)
831 } else if errors.As(err, &uerr) {
832 result = "usererror"
833 c.log.Debugx("imap command user error", err, logFields...)
834 if uerr.code != "" {
835 c.bwriteresultf("%s NO [%s] %s %v", tag, uerr.code, cmd, err)
836 } else {
837 c.bwriteresultf("%s NO %s %v", tag, cmd, err)
838 }
839 } else {
840 // Other type of panic, we pass it on, aborting the connection.
841 result = "panic"
842 c.log.Errorx("imap command panic", err, logFields...)
843 panic(err)
844 }
845 }()
846
847 tag = "*"
848 cmd, p = c.readCommand(&tag)
849 cmdlow = strings.ToLower(cmd)
850 c.cmd = cmdlow
851 c.cmdStart = time.Now()
852 c.cmdMetric = "(unrecognized)"
853
854 select {
855 case <-mox.Shutdown.Done():
856 // ../rfc/9051:5375
857 c.writelinef("* BYE shutting down")
858 panic(errIO)
859 default:
860 }
861
862 fn := commands[cmdlow]
863 if fn == nil {
864 xsyntaxErrorf("unknown command %q", cmd)
865 }
866 c.cmdMetric = c.cmd
867 c.ncmds++
868
869 // Check if command is allowed in this state.
870 if _, ok1 := commandsStateAny[cmdlow]; ok1 {
871 } else if _, ok2 := commandsStateNotAuthenticated[cmdlow]; ok2 && c.state == stateNotAuthenticated {
872 } else if _, ok3 := commandsStateAuthenticated[cmdlow]; ok3 && c.state == stateAuthenticated || c.state == stateSelected {
873 } else if _, ok4 := commandsStateSelected[cmdlow]; ok4 && c.state == stateSelected {
874 } else if ok1 || ok2 || ok3 || ok4 {
875 xuserErrorf("not allowed in this connection state")
876 } else {
877 xserverErrorf("unrecognized command")
878 }
879
880 fn(c, tag, cmd, p)
881}
882
883func (c *conn) broadcast(changes []store.Change) {
884 if len(changes) == 0 {
885 return
886 }
887 c.log.Debug("broadcast changes", slog.Any("changes", changes))
888 c.comm.Broadcast(changes)
889}
890
891// matchStringer matches a string against reference + mailbox patterns.
892type matchStringer interface {
893 MatchString(s string) bool
894}
895
896type noMatch struct{}
897
898// MatchString for noMatch always returns false.
899func (noMatch) MatchString(s string) bool {
900 return false
901}
902
903// xmailboxPatternMatcher returns a matcher for mailbox names given the reference and patterns.
904// Patterns can include "%" and "*", matching any character excluding and including a slash respectively.
905func xmailboxPatternMatcher(ref string, patterns []string) matchStringer {
906 if strings.HasPrefix(ref, "/") {
907 return noMatch{}
908 }
909
910 var subs []string
911 for _, pat := range patterns {
912 if strings.HasPrefix(pat, "/") {
913 continue
914 }
915
916 s := pat
917 if ref != "" {
918 s = path.Join(ref, pat)
919 }
920
921 // Fix casing for all Inbox paths.
922 first := strings.SplitN(s, "/", 2)[0]
923 if strings.EqualFold(first, "Inbox") {
924 s = "Inbox" + s[len("Inbox"):]
925 }
926
927 // ../rfc/9051:2361
928 var rs string
929 for _, c := range s {
930 if c == '%' {
931 rs += "[^/]*"
932 } else if c == '*' {
933 rs += ".*"
934 } else {
935 rs += regexp.QuoteMeta(string(c))
936 }
937 }
938 subs = append(subs, rs)
939 }
940
941 if len(subs) == 0 {
942 return noMatch{}
943 }
944 rs := "^(" + strings.Join(subs, "|") + ")$"
945 re, err := regexp.Compile(rs)
946 xcheckf(err, "compiling regexp for mailbox patterns")
947 return re
948}
949
950func (c *conn) sequence(uid store.UID) msgseq {
951 return uidSearch(c.uids, uid)
952}
953
954func uidSearch(uids []store.UID, uid store.UID) msgseq {
955 s := 0
956 e := len(uids)
957 for s < e {
958 i := (s + e) / 2
959 m := uids[i]
960 if uid == m {
961 return msgseq(i + 1)
962 } else if uid < m {
963 e = i
964 } else {
965 s = i + 1
966 }
967 }
968 return 0
969}
970
971func (c *conn) xsequence(uid store.UID) msgseq {
972 seq := c.sequence(uid)
973 if seq <= 0 {
974 xserverErrorf("unknown uid %d (%w)", uid, errProtocol)
975 }
976 return seq
977}
978
979func (c *conn) sequenceRemove(seq msgseq, uid store.UID) {
980 i := seq - 1
981 if c.uids[i] != uid {
982 xserverErrorf(fmt.Sprintf("got uid %d at msgseq %d, expected uid %d", uid, seq, c.uids[i]))
983 }
984 copy(c.uids[i:], c.uids[i+1:])
985 c.uids = c.uids[:len(c.uids)-1]
986 if sanityChecks {
987 checkUIDs(c.uids)
988 }
989}
990
991// add uid to the session. care must be taken that pending changes are fetched
992// while holding the account wlock, and applied before adding this uid, because
993// those pending changes may contain another new uid that has to be added first.
994func (c *conn) uidAppend(uid store.UID) {
995 if uidSearch(c.uids, uid) > 0 {
996 xserverErrorf("uid already present (%w)", errProtocol)
997 }
998 if len(c.uids) > 0 && uid < c.uids[len(c.uids)-1] {
999 xserverErrorf("new uid %d is smaller than last uid %d (%w)", uid, c.uids[len(c.uids)-1], errProtocol)
1000 }
1001 c.uids = append(c.uids, uid)
1002 if sanityChecks {
1003 checkUIDs(c.uids)
1004 }
1005}
1006
1007// sanity check that uids are in ascending order.
1008func checkUIDs(uids []store.UID) {
1009 for i, uid := range uids {
1010 if uid == 0 || i > 0 && uid <= uids[i-1] {
1011 xserverErrorf("bad uids %v", uids)
1012 }
1013 }
1014}
1015
1016func (c *conn) xnumSetUIDs(isUID bool, nums numSet) []store.UID {
1017 _, uids := c.xnumSetConditionUIDs(false, true, isUID, nums)
1018 return uids
1019}
1020
1021func (c *conn) xnumSetCondition(isUID bool, nums numSet) []any {
1022 uidargs, _ := c.xnumSetConditionUIDs(true, false, isUID, nums)
1023 return uidargs
1024}
1025
1026func (c *conn) xnumSetConditionUIDs(forDB, returnUIDs bool, isUID bool, nums numSet) ([]any, []store.UID) {
1027 if nums.searchResult {
1028 // Update previously stored UIDs. Some may have been deleted.
1029 // Once deleted a UID will never come back, so we'll just remove those uids.
1030 o := 0
1031 for _, uid := range c.searchResult {
1032 if uidSearch(c.uids, uid) > 0 {
1033 c.searchResult[o] = uid
1034 o++
1035 }
1036 }
1037 c.searchResult = c.searchResult[:o]
1038 uidargs := make([]any, len(c.searchResult))
1039 for i, uid := range c.searchResult {
1040 uidargs[i] = uid
1041 }
1042 return uidargs, c.searchResult
1043 }
1044
1045 var uidargs []any
1046 var uids []store.UID
1047
1048 add := func(uid store.UID) {
1049 if forDB {
1050 uidargs = append(uidargs, uid)
1051 }
1052 if returnUIDs {
1053 uids = append(uids, uid)
1054 }
1055 }
1056
1057 if !isUID {
1058 // Sequence numbers that don't exist, or * on an empty mailbox, should result in a BAD response. ../rfc/9051:7018
1059 for _, r := range nums.ranges {
1060 var ia, ib int
1061 if r.first.star {
1062 if len(c.uids) == 0 {
1063 xsyntaxErrorf("invalid seqset * on empty mailbox")
1064 }
1065 ia = len(c.uids) - 1
1066 } else {
1067 ia = int(r.first.number - 1)
1068 if ia >= len(c.uids) {
1069 xsyntaxErrorf("msgseq %d not in mailbox", r.first.number)
1070 }
1071 }
1072 if r.last == nil {
1073 add(c.uids[ia])
1074 continue
1075 }
1076
1077 if r.last.star {
1078 if len(c.uids) == 0 {
1079 xsyntaxErrorf("invalid seqset * on empty mailbox")
1080 }
1081 ib = len(c.uids) - 1
1082 } else {
1083 ib = int(r.last.number - 1)
1084 if ib >= len(c.uids) {
1085 xsyntaxErrorf("msgseq %d not in mailbox", r.last.number)
1086 }
1087 }
1088 if ia > ib {
1089 ia, ib = ib, ia
1090 }
1091 for _, uid := range c.uids[ia : ib+1] {
1092 add(uid)
1093 }
1094 }
1095 return uidargs, uids
1096 }
1097
1098 // UIDs that do not exist can be ignored.
1099 if len(c.uids) == 0 {
1100 return nil, nil
1101 }
1102
1103 for _, r := range nums.ranges {
1104 last := r.first
1105 if r.last != nil {
1106 last = *r.last
1107 }
1108
1109 uida := store.UID(r.first.number)
1110 if r.first.star {
1111 uida = c.uids[len(c.uids)-1]
1112 }
1113
1114 uidb := store.UID(last.number)
1115 if last.star {
1116 uidb = c.uids[len(c.uids)-1]
1117 }
1118
1119 if uida > uidb {
1120 uida, uidb = uidb, uida
1121 }
1122
1123 // Binary search for uida.
1124 s := 0
1125 e := len(c.uids)
1126 for s < e {
1127 m := (s + e) / 2
1128 if uida < c.uids[m] {
1129 e = m
1130 } else if uida > c.uids[m] {
1131 s = m + 1
1132 } else {
1133 break
1134 }
1135 }
1136
1137 for _, uid := range c.uids[s:] {
1138 if uid >= uida && uid <= uidb {
1139 add(uid)
1140 } else if uid > uidb {
1141 break
1142 }
1143 }
1144 }
1145
1146 return uidargs, uids
1147}
1148
1149func (c *conn) ok(tag, cmd string) {
1150 c.bwriteresultf("%s OK %s done", tag, cmd)
1151 c.xflush()
1152}
1153
1154// xcheckmailboxname checks if name is valid, returning an INBOX-normalized name.
1155// I.e. it changes various casings of INBOX and INBOX/* to Inbox and Inbox/*.
1156// Name is invalid if it contains leading/trailing/double slashes, or when it isn't
1157// unicode-normalized, or when empty or has special characters.
1158func xcheckmailboxname(name string, allowInbox bool) string {
1159 name, isinbox, err := store.CheckMailboxName(name, allowInbox)
1160 if isinbox {
1161 xuserErrorf("special mailboxname Inbox not allowed")
1162 } else if err != nil {
1163 xusercodeErrorf("CANNOT", "%s", err)
1164 }
1165 return name
1166}
1167
1168// Lookup mailbox by name.
1169// If the mailbox does not exist, panic is called with a user error.
1170// Must be called with account rlock held.
1171func (c *conn) xmailbox(tx *bstore.Tx, name string, missingErrCode string) store.Mailbox {
1172 mb, err := c.account.MailboxFind(tx, name)
1173 xcheckf(err, "finding mailbox")
1174 if mb == nil {
1175 // missingErrCode can be empty, or e.g. TRYCREATE or ALREADYEXISTS.
1176 xusercodeErrorf(missingErrCode, "%w", store.ErrUnknownMailbox)
1177 }
1178 return *mb
1179}
1180
1181// Lookup mailbox by ID.
1182// If the mailbox does not exist, panic is called with a user error.
1183// Must be called with account rlock held.
1184func (c *conn) xmailboxID(tx *bstore.Tx, id int64) store.Mailbox {
1185 mb := store.Mailbox{ID: id}
1186 err := tx.Get(&mb)
1187 if err == bstore.ErrAbsent {
1188 xuserErrorf("%w", store.ErrUnknownMailbox)
1189 }
1190 return mb
1191}
1192
1193// Apply changes to our session state.
1194// If initial is false, updates like EXISTS and EXPUNGE are written to the client.
1195// If initial is true, we only apply the changes.
1196// Should not be called while holding locks, as changes are written to client connections, which can block.
1197// Does not flush output.
1198func (c *conn) applyChanges(changes []store.Change, initial bool) {
1199 if len(changes) == 0 {
1200 return
1201 }
1202
1203 err := c.conn.SetWriteDeadline(time.Now().Add(5 * time.Minute))
1204 c.log.Check(err, "setting write deadline")
1205
1206 c.log.Debug("applying changes", slog.Any("changes", changes))
1207
1208 // Only keep changes for the selected mailbox, and changes that are always relevant.
1209 var n []store.Change
1210 for _, change := range changes {
1211 var mbID int64
1212 switch ch := change.(type) {
1213 case store.ChangeAddUID:
1214 mbID = ch.MailboxID
1215 case store.ChangeRemoveUIDs:
1216 mbID = ch.MailboxID
1217 case store.ChangeFlags:
1218 mbID = ch.MailboxID
1219 case store.ChangeRemoveMailbox, store.ChangeAddMailbox, store.ChangeRenameMailbox, store.ChangeAddSubscription:
1220 n = append(n, change)
1221 continue
1222 case store.ChangeMailboxCounts, store.ChangeMailboxSpecialUse, store.ChangeMailboxKeywords, store.ChangeThread:
1223 default:
1224 panic(fmt.Errorf("missing case for %#v", change))
1225 }
1226 if c.state == stateSelected && mbID == c.mailboxID {
1227 n = append(n, change)
1228 }
1229 }
1230 changes = n
1231
1232 qresync := c.enabled[capQresync]
1233 condstore := c.enabled[capCondstore]
1234
1235 i := 0
1236 for i < len(changes) {
1237 // First process all new uids. So we only send a single EXISTS.
1238 var adds []store.ChangeAddUID
1239 for ; i < len(changes); i++ {
1240 ch, ok := changes[i].(store.ChangeAddUID)
1241 if !ok {
1242 break
1243 }
1244 seq := c.sequence(ch.UID)
1245 if seq > 0 && initial {
1246 continue
1247 }
1248 c.uidAppend(ch.UID)
1249 adds = append(adds, ch)
1250 }
1251 if len(adds) > 0 {
1252 if initial {
1253 continue
1254 }
1255 // Write the exists, and the UID and flags as well. Hopefully the client waits for
1256 // long enough after the EXISTS to see these messages, and doesn't request them
1257 // again with a FETCH.
1258 c.bwritelinef("* %d EXISTS", len(c.uids))
1259 for _, add := range adds {
1260 seq := c.xsequence(add.UID)
1261 var modseqStr string
1262 if condstore {
1263 modseqStr = fmt.Sprintf(" MODSEQ (%d)", add.ModSeq.Client())
1264 }
1265 c.bwritelinef("* %d FETCH (UID %d FLAGS %s%s)", seq, add.UID, flaglist(add.Flags, add.Keywords).pack(c), modseqStr)
1266 }
1267 continue
1268 }
1269
1270 change := changes[i]
1271 i++
1272
1273 switch ch := change.(type) {
1274 case store.ChangeRemoveUIDs:
1275 var vanishedUIDs numSet
1276 for _, uid := range ch.UIDs {
1277 var seq msgseq
1278 if initial {
1279 seq = c.sequence(uid)
1280 if seq <= 0 {
1281 continue
1282 }
1283 } else {
1284 seq = c.xsequence(uid)
1285 }
1286 c.sequenceRemove(seq, uid)
1287 if !initial {
1288 if qresync {
1289 vanishedUIDs.append(uint32(uid))
1290 } else {
1291 c.bwritelinef("* %d EXPUNGE", seq)
1292 }
1293 }
1294 }
1295 if qresync {
1296 // VANISHED without EARLIER. ../rfc/7162:2004
1297 for _, s := range vanishedUIDs.Strings(4*1024 - 32) {
1298 c.bwritelinef("* VANISHED %s", s)
1299 }
1300 }
1301 case store.ChangeFlags:
1302 // The uid can be unknown if we just expunged it while another session marked it as deleted just before.
1303 seq := c.sequence(ch.UID)
1304 if seq <= 0 {
1305 continue
1306 }
1307 if !initial {
1308 var modseqStr string
1309 if condstore {
1310 modseqStr = fmt.Sprintf(" MODSEQ (%d)", ch.ModSeq.Client())
1311 }
1312 c.bwritelinef("* %d FETCH (UID %d FLAGS %s%s)", seq, ch.UID, flaglist(ch.Flags, ch.Keywords).pack(c), modseqStr)
1313 }
1314 case store.ChangeRemoveMailbox:
1315 // Only announce \NonExistent to modern clients, otherwise they may ignore the
1316 // unrecognized \NonExistent and interpret this as a newly created mailbox, while
1317 // the goal was to remove it...
1318 if c.enabled[capIMAP4rev2] {
1319 c.bwritelinef(`* LIST (\NonExistent) "/" %s`, astring(c.encodeMailbox(ch.Name)).pack(c))
1320 }
1321 case store.ChangeAddMailbox:
1322 c.bwritelinef(`* LIST (%s) "/" %s`, strings.Join(ch.Flags, " "), astring(c.encodeMailbox(ch.Mailbox.Name)).pack(c))
1323 case store.ChangeRenameMailbox:
1324 // OLDNAME only with IMAP4rev2 or NOTIFY ../rfc/9051:2726 ../rfc/5465:628
1325 var oldname string
1326 if c.enabled[capIMAP4rev2] {
1327 oldname = fmt.Sprintf(` ("OLDNAME" (%s))`, string0(c.encodeMailbox(ch.OldName)).pack(c))
1328 }
1329 c.bwritelinef(`* LIST (%s) "/" %s%s`, strings.Join(ch.Flags, " "), astring(c.encodeMailbox(ch.NewName)).pack(c), oldname)
1330 case store.ChangeAddSubscription:
1331 c.bwritelinef(`* LIST (%s) "/" %s`, strings.Join(append([]string{`\Subscribed`}, ch.Flags...), " "), astring(c.encodeMailbox(ch.Name)).pack(c))
1332 default:
1333 panic(fmt.Sprintf("internal error, missing case for %#v", change))
1334 }
1335 }
1336}
1337
1338// Capability returns the capabilities this server implements and currently has
1339// available given the connection state.
1340//
1341// State: any
1342func (c *conn) cmdCapability(tag, cmd string, p *parser) {
1343 // Command: ../rfc/9051:1208 ../rfc/3501:1300
1344
1345 // Request syntax: ../rfc/9051:6464 ../rfc/3501:4669
1346 p.xempty()
1347
1348 caps := c.capabilities()
1349
1350 // Response syntax: ../rfc/9051:6427 ../rfc/3501:4655
1351 c.bwritelinef("* CAPABILITY %s", caps)
1352 c.ok(tag, cmd)
1353}
1354
1355// capabilities returns non-empty string with available capabilities based on connection state.
1356// For use in cmdCapability and untagged OK responses on connection start, login and authenticate.
1357func (c *conn) capabilities() string {
1358 caps := serverCapabilities
1359 // ../rfc/9051:1238
1360 // We only allow starting without TLS when explicitly configured, in violation of RFC.
1361 if !c.tls && c.tlsConfig != nil {
1362 caps += " STARTTLS"
1363 }
1364 if c.tls || c.noRequireSTARTTLS {
1365 caps += " AUTH=PLAIN"
1366 } else {
1367 caps += " LOGINDISABLED"
1368 }
1369 return caps
1370}
1371
1372// No op, but useful for retrieving pending changes as untagged responses, e.g. of
1373// message delivery.
1374//
1375// State: any
1376func (c *conn) cmdNoop(tag, cmd string, p *parser) {
1377 // Command: ../rfc/9051:1261 ../rfc/3501:1363
1378
1379 // Request syntax: ../rfc/9051:6464 ../rfc/3501:4669
1380 p.xempty()
1381 c.ok(tag, cmd)
1382}
1383
1384// Logout, after which server closes the connection.
1385//
1386// State: any
1387func (c *conn) cmdLogout(tag, cmd string, p *parser) {
1388 // Commands: ../rfc/3501:1407 ../rfc/9051:1290
1389
1390 // Request syntax: ../rfc/9051:6464 ../rfc/3501:4669
1391 p.xempty()
1392
1393 c.unselect()
1394 c.state = stateNotAuthenticated
1395 // Response syntax: ../rfc/9051:6886 ../rfc/3501:4935
1396 c.bwritelinef("* BYE thanks")
1397 c.ok(tag, cmd)
1398 panic(cleanClose)
1399}
1400
1401// Clients can use ID to tell the server which software they are using. Servers can
1402// respond with their version. For statistics/logging/debugging purposes.
1403//
1404// State: any
1405func (c *conn) cmdID(tag, cmd string, p *parser) {
1406 // Command: ../rfc/2971:129
1407
1408 // Request syntax: ../rfc/2971:241
1409 p.xspace()
1410 var params map[string]string
1411 if p.take("(") {
1412 params = map[string]string{}
1413 for !p.take(")") {
1414 if len(params) > 0 {
1415 p.xspace()
1416 }
1417 k := p.xstring()
1418 p.xspace()
1419 v := p.xnilString()
1420 if _, ok := params[k]; ok {
1421 xsyntaxErrorf("duplicate key %q", k)
1422 }
1423 params[k] = v
1424 }
1425 } else {
1426 p.xnil()
1427 }
1428 p.xempty()
1429
1430 // We just log the client id.
1431 c.log.Info("client id", slog.Any("params", params))
1432
1433 // Response syntax: ../rfc/2971:243
1434 // We send our name and version. ../rfc/2971:193
1435 c.bwritelinef(`* ID ("name" "mox" "version" %s)`, string0(moxvar.Version).pack(c))
1436 c.ok(tag, cmd)
1437}
1438
1439// STARTTLS enables TLS on the connection, after a plain text start.
1440// Only allowed if TLS isn't already enabled, either through connecting to a
1441// TLS-enabled TCP port, or a previous STARTTLS command.
1442// After STARTTLS, plain text authentication typically becomes available.
1443//
1444// Status: Not authenticated.
1445func (c *conn) cmdStarttls(tag, cmd string, p *parser) {
1446 // Command: ../rfc/9051:1340 ../rfc/3501:1468
1447
1448 // Request syntax: ../rfc/9051:6473 ../rfc/3501:4676
1449 p.xempty()
1450
1451 if c.tls {
1452 xsyntaxErrorf("tls already active") // ../rfc/9051:1353
1453 }
1454
1455 conn := c.conn
1456 if n := c.br.Buffered(); n > 0 {
1457 buf := make([]byte, n)
1458 _, err := io.ReadFull(c.br, buf)
1459 xcheckf(err, "reading buffered data for tls handshake")
1460 conn = &prefixConn{buf, conn}
1461 }
1462 // We add the cid to facilitate debugging in case of TLS connection failure.
1463 c.ok(tag, cmd+" ("+mox.ReceivedID(c.cid)+")")
1464
1465 cidctx := context.WithValue(mox.Context, mlog.CidKey, c.cid)
1466 ctx, cancel := context.WithTimeout(cidctx, time.Minute)
1467 defer cancel()
1468 tlsConn := tls.Server(conn, c.tlsConfig)
1469 c.log.Debug("starting tls server handshake")
1470 if err := tlsConn.HandshakeContext(ctx); err != nil {
1471 panic(fmt.Errorf("starttls handshake: %s (%w)", err, errIO))
1472 }
1473 cancel()
1474 tlsversion, ciphersuite := moxio.TLSInfo(tlsConn)
1475 c.log.Debug("tls server handshake done", slog.String("tls", tlsversion), slog.String("ciphersuite", ciphersuite))
1476
1477 c.conn = tlsConn
1478 c.tr = moxio.NewTraceReader(c.log, "C: ", c.conn)
1479 c.tw = moxio.NewTraceWriter(c.log, "S: ", c)
1480 c.br = bufio.NewReader(c.tr)
1481 c.bw = bufio.NewWriter(c.tw)
1482 c.tls = true
1483}
1484
1485// Authenticate using SASL. Supports multiple back and forths between client and
1486// server to finish authentication, unlike LOGIN which is just a single
1487// username/password.
1488//
1489// Status: Not authenticated.
1490func (c *conn) cmdAuthenticate(tag, cmd string, p *parser) {
1491 // Command: ../rfc/9051:1403 ../rfc/3501:1519
1492 // Examples: ../rfc/9051:1520 ../rfc/3501:1631
1493
1494 // For many failed auth attempts, slow down verification attempts.
1495 if c.authFailed > 3 && authFailDelay > 0 {
1496 mox.Sleep(mox.Context, time.Duration(c.authFailed-3)*authFailDelay)
1497 }
1498 c.authFailed++ // Compensated on success.
1499 defer func() {
1500 // On the 3rd failed authentication, start responding slowly. Successful auth will
1501 // cause fast responses again.
1502 if c.authFailed >= 3 {
1503 c.setSlow(true)
1504 }
1505 }()
1506
1507 var authVariant string
1508 authResult := "error"
1509 defer func() {
1510 metrics.AuthenticationInc("imap", authVariant, authResult)
1511 switch authResult {
1512 case "ok":
1513 mox.LimiterFailedAuth.Reset(c.remoteIP, time.Now())
1514 default:
1515 mox.LimiterFailedAuth.Add(c.remoteIP, time.Now(), 1)
1516 }
1517 }()
1518
1519 // Request syntax: ../rfc/9051:6341 ../rfc/3501:4561
1520 p.xspace()
1521 authType := p.xatom()
1522
1523 xreadInitial := func() []byte {
1524 var line string
1525 if p.empty() {
1526 c.writelinef("+ ")
1527 line = c.readline(false)
1528 } else {
1529 // ../rfc/9051:1407 ../rfc/4959:84
1530 p.xspace()
1531 line = p.remainder()
1532 if line == "=" {
1533 // ../rfc/9051:1450
1534 line = "" // Base64 decode will result in empty buffer.
1535 }
1536 }
1537 // ../rfc/9051:1442 ../rfc/3501:1553
1538 if line == "*" {
1539 authResult = "aborted"
1540 xsyntaxErrorf("authenticate aborted by client")
1541 }
1542 buf, err := base64.StdEncoding.DecodeString(line)
1543 if err != nil {
1544 xsyntaxErrorf("parsing base64: %v", err)
1545 }
1546 return buf
1547 }
1548
1549 xreadContinuation := func() []byte {
1550 line := c.readline(false)
1551 if line == "*" {
1552 authResult = "aborted"
1553 xsyntaxErrorf("authenticate aborted by client")
1554 }
1555 buf, err := base64.StdEncoding.DecodeString(line)
1556 if err != nil {
1557 xsyntaxErrorf("parsing base64: %v", err)
1558 }
1559 return buf
1560 }
1561
1562 switch strings.ToUpper(authType) {
1563 case "PLAIN":
1564 authVariant = "plain"
1565
1566 if !c.noRequireSTARTTLS && !c.tls {
1567 // ../rfc/9051:5194
1568 xusercodeErrorf("PRIVACYREQUIRED", "tls required for login")
1569 }
1570
1571 // Plain text passwords, mark as traceauth.
1572 defer c.xtrace(mlog.LevelTraceauth)()
1573 buf := xreadInitial()
1574 c.xtrace(mlog.LevelTrace) // Restore.
1575 plain := bytes.Split(buf, []byte{0})
1576 if len(plain) != 3 {
1577 xsyntaxErrorf("bad plain auth data, expected 3 nul-separated tokens, got %d tokens", len(plain))
1578 }
1579 authz := string(plain[0])
1580 authc := string(plain[1])
1581 password := string(plain[2])
1582
1583 if authz != "" && authz != authc {
1584 xusercodeErrorf("AUTHORIZATIONFAILED", "cannot assume role")
1585 }
1586
1587 acc, err := store.OpenEmailAuth(c.log, authc, password)
1588 if err != nil {
1589 if errors.Is(err, store.ErrUnknownCredentials) {
1590 authResult = "badcreds"
1591 c.log.Info("authentication failed", slog.String("username", authc))
1592 xusercodeErrorf("AUTHENTICATIONFAILED", "bad credentials")
1593 }
1594 xusercodeErrorf("", "error")
1595 }
1596 c.account = acc
1597 c.username = authc
1598
1599 case "CRAM-MD5":
1600 authVariant = strings.ToLower(authType)
1601
1602 // ../rfc/9051:1462
1603 p.xempty()
1604
1605 // ../rfc/2195:82
1606 chal := fmt.Sprintf("<%d.%d@%s>", uint64(mox.CryptoRandInt()), time.Now().UnixNano(), mox.Conf.Static.HostnameDomain.ASCII)
1607 c.writelinef("+ %s", base64.StdEncoding.EncodeToString([]byte(chal)))
1608
1609 resp := xreadContinuation()
1610 t := strings.Split(string(resp), " ")
1611 if len(t) != 2 || len(t[1]) != 2*md5.Size {
1612 xsyntaxErrorf("malformed cram-md5 response")
1613 }
1614 addr := t[0]
1615 c.log.Debug("cram-md5 auth", slog.String("address", addr))
1616 acc, _, err := store.OpenEmail(c.log, addr)
1617 if err != nil {
1618 if errors.Is(err, store.ErrUnknownCredentials) {
1619 c.log.Info("failed authentication attempt", slog.String("username", addr), slog.Any("remote", c.remoteIP))
1620 xusercodeErrorf("AUTHENTICATIONFAILED", "bad credentials")
1621 }
1622 xserverErrorf("looking up address: %v", err)
1623 }
1624 defer func() {
1625 if acc != nil {
1626 err := acc.Close()
1627 c.xsanity(err, "close account")
1628 }
1629 }()
1630 var ipadhash, opadhash hash.Hash
1631 acc.WithRLock(func() {
1632 err := acc.DB.Read(context.TODO(), func(tx *bstore.Tx) error {
1633 password, err := bstore.QueryTx[store.Password](tx).Get()
1634 if err == bstore.ErrAbsent {
1635 c.log.Info("failed authentication attempt", slog.String("username", addr), slog.Any("remote", c.remoteIP))
1636 xusercodeErrorf("AUTHENTICATIONFAILED", "bad credentials")
1637 }
1638 if err != nil {
1639 return err
1640 }
1641
1642 ipadhash = password.CRAMMD5.Ipad
1643 opadhash = password.CRAMMD5.Opad
1644 return nil
1645 })
1646 xcheckf(err, "tx read")
1647 })
1648 if ipadhash == nil || opadhash == nil {
1649 c.log.Info("cram-md5 auth attempt without derived secrets set, save password again to store secrets", slog.String("username", addr))
1650 c.log.Info("failed authentication attempt", slog.String("username", addr), slog.Any("remote", c.remoteIP))
1651 xusercodeErrorf("AUTHENTICATIONFAILED", "bad credentials")
1652 }
1653
1654 // ../rfc/2195:138 ../rfc/2104:142
1655 ipadhash.Write([]byte(chal))
1656 opadhash.Write(ipadhash.Sum(nil))
1657 digest := fmt.Sprintf("%x", opadhash.Sum(nil))
1658 if digest != t[1] {
1659 c.log.Info("failed authentication attempt", slog.String("username", addr), slog.Any("remote", c.remoteIP))
1660 xusercodeErrorf("AUTHENTICATIONFAILED", "bad credentials")
1661 }
1662
1663 c.account = acc
1664 acc = nil // Cancel cleanup.
1665 c.username = addr
1666
1667 case "SCRAM-SHA-256-PLUS", "SCRAM-SHA-256", "SCRAM-SHA-1-PLUS", "SCRAM-SHA-1":
1668 // todo: improve handling of errors during scram. e.g. invalid parameters. should we abort the imap command, or continue until the end and respond with a scram-level error?
1669 // todo: use single implementation between ../imapserver/server.go and ../smtpserver/server.go
1670
1671 // No plaintext credentials, we can log these normally.
1672
1673 authVariant = strings.ToLower(authType)
1674 var h func() hash.Hash
1675 switch authVariant {
1676 case "scram-sha-1", "scram-sha-1-plus":
1677 h = sha1.New
1678 case "scram-sha-256", "scram-sha-256-plus":
1679 h = sha256.New
1680 default:
1681 xserverErrorf("missing case for scram variant")
1682 }
1683
1684 var cs *tls.ConnectionState
1685 requireChannelBinding := strings.HasSuffix(authVariant, "-plus")
1686 if requireChannelBinding && !c.tls {
1687 xuserErrorf("cannot use plus variant with tls channel binding without tls")
1688 }
1689 if c.tls {
1690 xcs := c.conn.(*tls.Conn).ConnectionState()
1691 cs = &xcs
1692 }
1693 c0 := xreadInitial()
1694 ss, err := scram.NewServer(h, c0, cs, requireChannelBinding)
1695 if err != nil {
1696 xsyntaxErrorf("starting scram: %s", err)
1697 }
1698 c.log.Debug("scram auth", slog.String("authentication", ss.Authentication))
1699 acc, _, err := store.OpenEmail(c.log, ss.Authentication)
1700 if err != nil {
1701 // todo: we could continue scram with a generated salt, deterministically generated
1702 // from the username. that way we don't have to store anything but attackers cannot
1703 // learn if an account exists. same for absent scram saltedpassword below.
1704 xuserErrorf("scram not possible")
1705 }
1706 defer func() {
1707 if acc != nil {
1708 err := acc.Close()
1709 c.xsanity(err, "close account")
1710 }
1711 }()
1712 if ss.Authorization != "" && ss.Authorization != ss.Authentication {
1713 xuserErrorf("authentication with authorization for different user not supported")
1714 }
1715 var xscram store.SCRAM
1716 acc.WithRLock(func() {
1717 err := acc.DB.Read(context.TODO(), func(tx *bstore.Tx) error {
1718 password, err := bstore.QueryTx[store.Password](tx).Get()
1719 switch authVariant {
1720 case "scram-sha-1", "scram-sha-1-plus":
1721 xscram = password.SCRAMSHA1
1722 case "scram-sha-256", "scram-sha-256-plus":
1723 xscram = password.SCRAMSHA256
1724 default:
1725 xserverErrorf("missing case for scram credentials")
1726 }
1727 if err == bstore.ErrAbsent || err == nil && (len(xscram.Salt) == 0 || xscram.Iterations == 0 || len(xscram.SaltedPassword) == 0) {
1728 c.log.Info("scram auth attempt without derived secrets set, save password again to store secrets", slog.String("address", ss.Authentication))
1729 xuserErrorf("scram not possible")
1730 }
1731 xcheckf(err, "fetching credentials")
1732 return err
1733 })
1734 xcheckf(err, "read tx")
1735 })
1736 s1, err := ss.ServerFirst(xscram.Iterations, xscram.Salt)
1737 xcheckf(err, "scram first server step")
1738 c.writelinef("+ %s", base64.StdEncoding.EncodeToString([]byte(s1)))
1739 c2 := xreadContinuation()
1740 s3, err := ss.Finish(c2, xscram.SaltedPassword)
1741 if len(s3) > 0 {
1742 c.writelinef("+ %s", base64.StdEncoding.EncodeToString([]byte(s3)))
1743 }
1744 if err != nil {
1745 c.readline(false) // Should be "*" for cancellation.
1746 if errors.Is(err, scram.ErrInvalidProof) {
1747 authResult = "badcreds"
1748 c.log.Info("failed authentication attempt", slog.String("username", ss.Authentication), slog.Any("remote", c.remoteIP))
1749 xusercodeErrorf("AUTHENTICATIONFAILED", "bad credentials")
1750 }
1751 xuserErrorf("server final: %w", err)
1752 }
1753
1754 // Client must still respond, but there is nothing to say. See ../rfc/9051:6221
1755 // The message should be empty. todo: should we require it is empty?
1756 xreadContinuation()
1757
1758 c.account = acc
1759 acc = nil // Cancel cleanup.
1760 c.username = ss.Authentication
1761
1762 default:
1763 xuserErrorf("method not supported")
1764 }
1765
1766 c.setSlow(false)
1767 authResult = "ok"
1768 c.authFailed = 0
1769 c.comm = store.RegisterComm(c.account)
1770 c.state = stateAuthenticated
1771 c.writeresultf("%s OK [CAPABILITY %s] authenticate done", tag, c.capabilities())
1772}
1773
1774// Login logs in with username and password.
1775//
1776// Status: Not authenticated.
1777func (c *conn) cmdLogin(tag, cmd string, p *parser) {
1778 // Command: ../rfc/9051:1597 ../rfc/3501:1663
1779
1780 authResult := "error"
1781 defer func() {
1782 metrics.AuthenticationInc("imap", "login", authResult)
1783 }()
1784
1785 // todo: get this line logged with traceauth. the plaintext password is included on the command line, which we've already read (before dispatching to this function).
1786
1787 // Request syntax: ../rfc/9051:6667 ../rfc/3501:4804
1788 p.xspace()
1789 userid := p.xastring()
1790 p.xspace()
1791 password := p.xastring()
1792 p.xempty()
1793
1794 if !c.noRequireSTARTTLS && !c.tls {
1795 // ../rfc/9051:5194
1796 xusercodeErrorf("PRIVACYREQUIRED", "tls required for login")
1797 }
1798
1799 // For many failed auth attempts, slow down verification attempts.
1800 if c.authFailed > 3 && authFailDelay > 0 {
1801 mox.Sleep(mox.Context, time.Duration(c.authFailed-3)*authFailDelay)
1802 }
1803 c.authFailed++ // Compensated on success.
1804 defer func() {
1805 // On the 3rd failed authentication, start responding slowly. Successful auth will
1806 // cause fast responses again.
1807 if c.authFailed >= 3 {
1808 c.setSlow(true)
1809 }
1810 }()
1811
1812 acc, err := store.OpenEmailAuth(c.log, userid, password)
1813 if err != nil {
1814 authResult = "badcreds"
1815 var code string
1816 if errors.Is(err, store.ErrUnknownCredentials) {
1817 code = "AUTHENTICATIONFAILED"
1818 c.log.Info("failed authentication attempt", slog.String("username", userid), slog.Any("remote", c.remoteIP))
1819 }
1820 xusercodeErrorf(code, "login failed")
1821 }
1822 c.account = acc
1823 c.username = userid
1824 c.authFailed = 0
1825 c.setSlow(false)
1826 c.comm = store.RegisterComm(acc)
1827 c.state = stateAuthenticated
1828 authResult = "ok"
1829 c.writeresultf("%s OK [CAPABILITY %s] login done", tag, c.capabilities())
1830}
1831
1832// Enable explicitly opts in to an extension. A server can typically send new kinds
1833// of responses to a client. Most extensions do not require an ENABLE because a
1834// client implicitly opts in to new response syntax by making a requests that uses
1835// new optional extension request syntax.
1836//
1837// State: Authenticated and selected.
1838func (c *conn) cmdEnable(tag, cmd string, p *parser) {
1839 // Command: ../rfc/9051:1652 ../rfc/5161:80
1840 // Examples: ../rfc/9051:1728 ../rfc/5161:147
1841
1842 // Request syntax: ../rfc/9051:6518 ../rfc/5161:207
1843 p.xspace()
1844 caps := []string{p.xatom()}
1845 for !p.empty() {
1846 p.xspace()
1847 caps = append(caps, p.xatom())
1848 }
1849
1850 // Clients should only send capabilities that need enabling.
1851 // We should only echo that we recognize as needing enabling.
1852 var enabled string
1853 var qresync bool
1854 for _, s := range caps {
1855 cap := capability(strings.ToUpper(s))
1856 switch cap {
1857 case capIMAP4rev2,
1858 capUTF8Accept,
1859 capCondstore: // ../rfc/7162:384
1860 c.enabled[cap] = true
1861 enabled += " " + s
1862 case capQresync:
1863 c.enabled[cap] = true
1864 enabled += " " + s
1865 qresync = true
1866 }
1867 }
1868 // QRESYNC enabled CONDSTORE too ../rfc/7162:1391
1869 if qresync && !c.enabled[capCondstore] {
1870 c.xensureCondstore(nil)
1871 enabled += " CONDSTORE"
1872 }
1873
1874 // Response syntax: ../rfc/9051:6520 ../rfc/5161:211
1875 c.bwritelinef("* ENABLED%s", enabled)
1876 c.ok(tag, cmd)
1877}
1878
1879// The CONDSTORE extension can be enabled in many different ways. ../rfc/7162:368
1880// If a mailbox is selected, an untagged OK with HIGHESTMODSEQ is written to the
1881// client. If tx is non-nil, it is used to read the HIGHESTMODSEQ from the
1882// database. Otherwise a new read-only transaction is created.
1883func (c *conn) xensureCondstore(tx *bstore.Tx) {
1884 if !c.enabled[capCondstore] {
1885 c.enabled[capCondstore] = true
1886 // todo spec: can we send an untagged enabled response?
1887 // ../rfc/7162:603
1888 if c.mailboxID <= 0 {
1889 return
1890 }
1891 var modseq store.ModSeq
1892 if tx != nil {
1893 modseq = c.xhighestModSeq(tx, c.mailboxID)
1894 } else {
1895 c.xdbread(func(tx *bstore.Tx) {
1896 modseq = c.xhighestModSeq(tx, c.mailboxID)
1897 })
1898 }
1899 c.bwritelinef("* OK [HIGHESTMODSEQ %d] after condstore-enabling command", modseq.Client())
1900 }
1901}
1902
1903// State: Authenticated and selected.
1904func (c *conn) cmdSelect(tag, cmd string, p *parser) {
1905 c.cmdSelectExamine(true, tag, cmd, p)
1906}
1907
1908// State: Authenticated and selected.
1909func (c *conn) cmdExamine(tag, cmd string, p *parser) {
1910 c.cmdSelectExamine(false, tag, cmd, p)
1911}
1912
1913// Select and examine are almost the same commands. Select just opens a mailbox for
1914// read/write and examine opens a mailbox readonly.
1915//
1916// State: Authenticated and selected.
1917func (c *conn) cmdSelectExamine(isselect bool, tag, cmd string, p *parser) {
1918 // Select command: ../rfc/9051:1754 ../rfc/3501:1743 ../rfc/7162:1146 ../rfc/7162:1432
1919 // Examine command: ../rfc/9051:1868 ../rfc/3501:1855
1920 // Select examples: ../rfc/9051:1831 ../rfc/3501:1826 ../rfc/7162:1159 ../rfc/7162:1479
1921
1922 // Select request syntax: ../rfc/9051:7005 ../rfc/3501:4996 ../rfc/4466:652 ../rfc/7162:2559 ../rfc/7162:2598
1923 // Examine request syntax: ../rfc/9051:6551 ../rfc/3501:4746
1924 p.xspace()
1925 name := p.xmailbox()
1926
1927 var qruidvalidity uint32
1928 var qrmodseq int64 // QRESYNC required parameters.
1929 var qrknownUIDs, qrknownSeqSet, qrknownUIDSet *numSet // QRESYNC optional parameters.
1930 if p.space() {
1931 seen := map[string]bool{}
1932 p.xtake("(")
1933 for len(seen) == 0 || !p.take(")") {
1934 w := p.xtakelist("CONDSTORE", "QRESYNC")
1935 if seen[w] {
1936 xsyntaxErrorf("duplicate select parameter %s", w)
1937 }
1938 seen[w] = true
1939
1940 switch w {
1941 case "CONDSTORE":
1942 // ../rfc/7162:363
1943 c.xensureCondstore(nil) // ../rfc/7162:373
1944 case "QRESYNC":
1945 // ../rfc/7162:2598
1946 // Note: unlike with CONDSTORE, there are no QRESYNC-related commands/parameters
1947 // that enable capabilities.
1948 if !c.enabled[capQresync] {
1949 // ../rfc/7162:1446
1950 xsyntaxErrorf("QRESYNC must first be enabled")
1951 }
1952 p.xspace()
1953 p.xtake("(")
1954 qruidvalidity = p.xnznumber() // ../rfc/7162:2606
1955 p.xspace()
1956 qrmodseq = p.xnznumber64()
1957 if p.take(" ") {
1958 seqMatchData := p.take("(")
1959 if !seqMatchData {
1960 ss := p.xnumSet0(false, false) // ../rfc/7162:2608
1961 qrknownUIDs = &ss
1962 seqMatchData = p.take(" (")
1963 }
1964 if seqMatchData {
1965 ss0 := p.xnumSet0(false, false)
1966 qrknownSeqSet = &ss0
1967 p.xspace()
1968 ss1 := p.xnumSet0(false, false)
1969 qrknownUIDSet = &ss1
1970 p.xtake(")")
1971 }
1972 }
1973 p.xtake(")")
1974 default:
1975 panic("missing case for select param " + w)
1976 }
1977 }
1978 }
1979 p.xempty()
1980
1981 // Deselect before attempting the new select. This means we will deselect when an
1982 // error occurs during select.
1983 // ../rfc/9051:1809
1984 if c.state == stateSelected {
1985 // ../rfc/9051:1812 ../rfc/7162:2111
1986 c.bwritelinef("* OK [CLOSED] x")
1987 c.unselect()
1988 }
1989
1990 name = xcheckmailboxname(name, true)
1991
1992 var highestModSeq store.ModSeq
1993 var highDeletedModSeq store.ModSeq
1994 var firstUnseen msgseq = 0
1995 var mb store.Mailbox
1996 c.account.WithRLock(func() {
1997 c.xdbread(func(tx *bstore.Tx) {
1998 mb = c.xmailbox(tx, name, "")
1999
2000 q := bstore.QueryTx[store.Message](tx)
2001 q.FilterNonzero(store.Message{MailboxID: mb.ID})
2002 q.FilterEqual("Expunged", false)
2003 q.SortAsc("UID")
2004 c.uids = []store.UID{}
2005 var seq msgseq = 1
2006 err := q.ForEach(func(m store.Message) error {
2007 c.uids = append(c.uids, m.UID)
2008 if firstUnseen == 0 && !m.Seen {
2009 firstUnseen = seq
2010 }
2011 seq++
2012 return nil
2013 })
2014 if sanityChecks {
2015 checkUIDs(c.uids)
2016 }
2017 xcheckf(err, "fetching uids")
2018
2019 // Condstore extension, find the highest modseq.
2020 if c.enabled[capCondstore] {
2021 highestModSeq = c.xhighestModSeq(tx, mb.ID)
2022 }
2023 // For QRESYNC, we need to know the highest modset of deleted expunged records to
2024 // maintain synchronization.
2025 if c.enabled[capQresync] {
2026 highDeletedModSeq, err = c.account.HighestDeletedModSeq(tx)
2027 xcheckf(err, "getting highest deleted modseq")
2028 }
2029 })
2030 })
2031 c.applyChanges(c.comm.Get(), true)
2032
2033 var flags string
2034 if len(mb.Keywords) > 0 {
2035 flags = " " + strings.Join(mb.Keywords, " ")
2036 }
2037 c.bwritelinef(`* FLAGS (\Seen \Answered \Flagged \Deleted \Draft $Forwarded $Junk $NotJunk $Phishing $MDNSent%s)`, flags)
2038 c.bwritelinef(`* OK [PERMANENTFLAGS (\Seen \Answered \Flagged \Deleted \Draft $Forwarded $Junk $NotJunk $Phishing $MDNSent \*)] x`)
2039 if !c.enabled[capIMAP4rev2] {
2040 c.bwritelinef(`* 0 RECENT`)
2041 }
2042 c.bwritelinef(`* %d EXISTS`, len(c.uids))
2043 if !c.enabled[capIMAP4rev2] && firstUnseen > 0 {
2044 // ../rfc/9051:8051 ../rfc/3501:1774
2045 c.bwritelinef(`* OK [UNSEEN %d] x`, firstUnseen)
2046 }
2047 c.bwritelinef(`* OK [UIDVALIDITY %d] x`, mb.UIDValidity)
2048 c.bwritelinef(`* OK [UIDNEXT %d] x`, mb.UIDNext)
2049 c.bwritelinef(`* LIST () "/" %s`, astring(c.encodeMailbox(mb.Name)).pack(c))
2050 if c.enabled[capCondstore] {
2051 // ../rfc/7162:417
2052 // ../rfc/7162-eid5055 ../rfc/7162:484 ../rfc/7162:1167
2053 c.bwritelinef(`* OK [HIGHESTMODSEQ %d] x`, highestModSeq.Client())
2054 }
2055
2056 // If QRESYNC uidvalidity matches, we send any changes. ../rfc/7162:1509
2057 if qruidvalidity == mb.UIDValidity {
2058 // We send the vanished UIDs at the end, so we can easily combine the modseq
2059 // changes and vanished UIDs that result from that, with the vanished UIDs from the
2060 // case where we don't store enough history.
2061 vanishedUIDs := map[store.UID]struct{}{}
2062
2063 var preVanished store.UID
2064 var oldClientUID store.UID
2065 // If samples of known msgseq and uid pairs are given (they must be in order), we
2066 // use them to determine the earliest UID for which we send VANISHED responses.
2067 // ../rfc/7162:1579
2068 if qrknownSeqSet != nil {
2069 if !qrknownSeqSet.isBasicIncreasing() {
2070 xuserErrorf("QRESYNC known message sequence set must be numeric and strictly increasing")
2071 }
2072 if !qrknownUIDSet.isBasicIncreasing() {
2073 xuserErrorf("QRESYNC known uid set must be numeric and strictly increasing")
2074 }
2075 seqiter := qrknownSeqSet.newIter()
2076 uiditer := qrknownUIDSet.newIter()
2077 for {
2078 msgseq, ok0 := seqiter.Next()
2079 uid, ok1 := uiditer.Next()
2080 if !ok0 && !ok1 {
2081 break
2082 } else if !ok0 || !ok1 {
2083 xsyntaxErrorf("invalid combination of known sequence set and uid set, must be of equal length")
2084 }
2085 i := int(msgseq - 1)
2086 if i < 0 || i >= len(c.uids) || c.uids[i] != store.UID(uid) {
2087 if uidSearch(c.uids, store.UID(uid)) <= 0 {
2088 // We will check this old client UID for consistency below.
2089 oldClientUID = store.UID(uid)
2090 }
2091 break
2092 }
2093 preVanished = store.UID(uid + 1)
2094 }
2095 }
2096
2097 // We gather vanished UIDs and report them at the end. This seems OK because we
2098 // already sent HIGHESTMODSEQ, and a client should know not to commit that value
2099 // until after it has seen the tagged OK of this command. The RFC has a remark
2100 // about ordering of some untagged responses, it's not immediately clear what it
2101 // means, but given the examples appears to allude to servers that decide to not
2102 // send expunge/vanished before the tagged OK.
2103 // ../rfc/7162:1340
2104
2105 // We are reading without account lock. Similar to when we process FETCH/SEARCH
2106 // requests. We don't have to reverify existence of the mailbox, so we don't
2107 // rlock, even briefly.
2108 c.xdbread(func(tx *bstore.Tx) {
2109 if oldClientUID > 0 {
2110 // The client sent a UID that is now removed. This is typically fine. But we check
2111 // that it is consistent with the modseq the client sent. If the UID already didn't
2112 // exist at that modseq, the client may be missing some information.
2113 q := bstore.QueryTx[store.Message](tx)
2114 q.FilterNonzero(store.Message{MailboxID: mb.ID, UID: oldClientUID})
2115 m, err := q.Get()
2116 if err == nil {
2117 // If client claims to be up to date up to and including qrmodseq, and the message
2118 // was deleted at or before that time, we send changes from just before that
2119 // modseq, and we send vanished for all UIDs.
2120 if m.Expunged && qrmodseq >= m.ModSeq.Client() {
2121 qrmodseq = m.ModSeq.Client() - 1
2122 preVanished = 0
2123 qrknownUIDs = nil
2124 c.bwritelinef("* OK [ALERT] Synchronization inconsistency in client detected. Client tried to sync with a UID that was removed at or after the MODSEQ it sent in the request. Sending all historic message removals for selected mailbox. Full synchronization recommended.")
2125 }
2126 } else if err != bstore.ErrAbsent {
2127 xcheckf(err, "checking old client uid")
2128 }
2129 }
2130
2131 q := bstore.QueryTx[store.Message](tx)
2132 q.FilterNonzero(store.Message{MailboxID: mb.ID})
2133 // Note: we don't filter by Expunged.
2134 q.FilterGreater("ModSeq", store.ModSeqFromClient(qrmodseq))
2135 q.FilterLessEqual("ModSeq", highestModSeq)
2136 q.SortAsc("ModSeq")
2137 err := q.ForEach(func(m store.Message) error {
2138 if m.Expunged && m.UID < preVanished {
2139 return nil
2140 }
2141 // If known UIDs was specified, we only report about those UIDs. ../rfc/7162:1523
2142 if qrknownUIDs != nil && !qrknownUIDs.contains(uint32(m.UID)) {
2143 return nil
2144 }
2145 if m.Expunged {
2146 vanishedUIDs[m.UID] = struct{}{}
2147 return nil
2148 }
2149 msgseq := c.sequence(m.UID)
2150 if msgseq > 0 {
2151 c.bwritelinef("* %d FETCH (UID %d FLAGS %s MODSEQ (%d))", msgseq, m.UID, flaglist(m.Flags, m.Keywords).pack(c), m.ModSeq.Client())
2152 }
2153 return nil
2154 })
2155 xcheckf(err, "listing changed messages")
2156 })
2157
2158 // Add UIDs from client's known UID set to vanished list if we don't have enough history.
2159 if qrmodseq < highDeletedModSeq.Client() {
2160 // If no known uid set was in the request, we substitute 1:max or the empty set.
2161 // ../rfc/7162:1524
2162 if qrknownUIDs == nil {
2163 if len(c.uids) > 0 {
2164 qrknownUIDs = &numSet{ranges: []numRange{{first: setNumber{number: 1}, last: &setNumber{number: uint32(c.uids[len(c.uids)-1])}}}}
2165 } else {
2166 qrknownUIDs = &numSet{}
2167 }
2168 }
2169
2170 iter := qrknownUIDs.newIter()
2171 for {
2172 v, ok := iter.Next()
2173 if !ok {
2174 break
2175 }
2176 if c.sequence(store.UID(v)) <= 0 {
2177 vanishedUIDs[store.UID(v)] = struct{}{}
2178 }
2179 }
2180 }
2181
2182 // Now that we have all vanished UIDs, send them over compactly.
2183 if len(vanishedUIDs) > 0 {
2184 l := maps.Keys(vanishedUIDs)
2185 sort.Slice(l, func(i, j int) bool {
2186 return l[i] < l[j]
2187 })
2188 // ../rfc/7162:1985
2189 for _, s := range compactUIDSet(l).Strings(4*1024 - 32) {
2190 c.bwritelinef("* VANISHED (EARLIER) %s", s)
2191 }
2192 }
2193 }
2194
2195 if isselect {
2196 c.bwriteresultf("%s OK [READ-WRITE] x", tag)
2197 c.readonly = false
2198 } else {
2199 c.bwriteresultf("%s OK [READ-ONLY] x", tag)
2200 c.readonly = true
2201 }
2202 c.mailboxID = mb.ID
2203 c.state = stateSelected
2204 c.searchResult = nil
2205 c.xflush()
2206}
2207
2208// Create makes a new mailbox, and its parents too if absent.
2209//
2210// State: Authenticated and selected.
2211func (c *conn) cmdCreate(tag, cmd string, p *parser) {
2212 // Command: ../rfc/9051:1900 ../rfc/3501:1888
2213 // Examples: ../rfc/9051:1951 ../rfc/6154:411 ../rfc/4466:212 ../rfc/3501:1933
2214
2215 // Request syntax: ../rfc/9051:6484 ../rfc/6154:468 ../rfc/4466:500 ../rfc/3501:4687
2216 p.xspace()
2217 name := p.xmailbox()
2218 // todo: support CREATE-SPECIAL-USE ../rfc/6154:296
2219 p.xempty()
2220
2221 origName := name
2222 name = strings.TrimRight(name, "/") // ../rfc/9051:1930
2223 name = xcheckmailboxname(name, false)
2224
2225 var changes []store.Change
2226 var created []string // Created mailbox names.
2227
2228 c.account.WithWLock(func() {
2229 c.xdbwrite(func(tx *bstore.Tx) {
2230 var exists bool
2231 var err error
2232 changes, created, exists, err = c.account.MailboxCreate(tx, name)
2233 if exists {
2234 // ../rfc/9051:1914
2235 xuserErrorf("mailbox already exists")
2236 }
2237 xcheckf(err, "creating mailbox")
2238 })
2239
2240 c.broadcast(changes)
2241 })
2242
2243 for _, n := range created {
2244 var oldname string
2245 // OLDNAME only with IMAP4rev2 or NOTIFY ../rfc/9051:2726 ../rfc/5465:628
2246 if c.enabled[capIMAP4rev2] && n == name && name != origName && !(name == "Inbox" || strings.HasPrefix(name, "Inbox/")) {
2247 oldname = fmt.Sprintf(` ("OLDNAME" (%s))`, string0(c.encodeMailbox(origName)).pack(c))
2248 }
2249 c.bwritelinef(`* LIST (\Subscribed) "/" %s%s`, astring(c.encodeMailbox(n)).pack(c), oldname)
2250 }
2251 c.ok(tag, cmd)
2252}
2253
2254// Delete removes a mailbox and all its messages.
2255// Inbox cannot be removed.
2256//
2257// State: Authenticated and selected.
2258func (c *conn) cmdDelete(tag, cmd string, p *parser) {
2259 // Command: ../rfc/9051:1972 ../rfc/3501:1946
2260 // Examples: ../rfc/9051:2025 ../rfc/3501:1992
2261
2262 // Request syntax: ../rfc/9051:6505 ../rfc/3501:4716
2263 p.xspace()
2264 name := p.xmailbox()
2265 p.xempty()
2266
2267 name = xcheckmailboxname(name, false)
2268
2269 // Messages to remove after having broadcasted the removal of messages.
2270 var removeMessageIDs []int64
2271
2272 c.account.WithWLock(func() {
2273 var mb store.Mailbox
2274 var changes []store.Change
2275
2276 c.xdbwrite(func(tx *bstore.Tx) {
2277 mb = c.xmailbox(tx, name, "NONEXISTENT")
2278
2279 var hasChildren bool
2280 var err error
2281 changes, removeMessageIDs, hasChildren, err = c.account.MailboxDelete(context.TODO(), c.log, tx, mb)
2282 if hasChildren {
2283 xusercodeErrorf("HASCHILDREN", "mailbox has a child, only leaf mailboxes can be deleted")
2284 }
2285 xcheckf(err, "deleting mailbox")
2286 })
2287
2288 c.broadcast(changes)
2289 })
2290
2291 for _, mID := range removeMessageIDs {
2292 p := c.account.MessagePath(mID)
2293 err := os.Remove(p)
2294 c.log.Check(err, "removing message file for mailbox delete", slog.String("path", p))
2295 }
2296
2297 c.ok(tag, cmd)
2298}
2299
2300// Rename changes the name of a mailbox.
2301// Renaming INBOX is special, it moves the inbox messages to a new mailbox, leaving inbox empty.
2302// Renaming a mailbox with submailboxes also renames all submailboxes.
2303// Subscriptions stay with the old name, though newly created missing parent
2304// mailboxes for the destination name are automatically subscribed.
2305//
2306// State: Authenticated and selected.
2307func (c *conn) cmdRename(tag, cmd string, p *parser) {
2308 // Command: ../rfc/9051:2062 ../rfc/3501:2040
2309 // Examples: ../rfc/9051:2132 ../rfc/3501:2092
2310
2311 // Request syntax: ../rfc/9051:6863 ../rfc/3501:4908
2312 p.xspace()
2313 src := p.xmailbox()
2314 p.xspace()
2315 dst := p.xmailbox()
2316 p.xempty()
2317
2318 src = xcheckmailboxname(src, true)
2319 dst = xcheckmailboxname(dst, false)
2320
2321 c.account.WithWLock(func() {
2322 var changes []store.Change
2323
2324 c.xdbwrite(func(tx *bstore.Tx) {
2325 srcMB := c.xmailbox(tx, src, "NONEXISTENT")
2326
2327 // Inbox is very special. Unlike other mailboxes, its children are not moved. And
2328 // unlike a regular move, its messages are moved to a newly created mailbox. We do
2329 // indeed create a new destination mailbox and actually move the messages.
2330 // ../rfc/9051:2101
2331 if src == "Inbox" {
2332 exists, err := c.account.MailboxExists(tx, dst)
2333 xcheckf(err, "checking if destination mailbox exists")
2334 if exists {
2335 xusercodeErrorf("ALREADYEXISTS", "destination mailbox %q already exists", dst)
2336 }
2337 if dst == src {
2338 xuserErrorf("cannot move inbox to itself")
2339 }
2340
2341 uidval, err := c.account.NextUIDValidity(tx)
2342 xcheckf(err, "next uid validity")
2343
2344 dstMB := store.Mailbox{
2345 Name: dst,
2346 UIDValidity: uidval,
2347 UIDNext: 1,
2348 Keywords: srcMB.Keywords,
2349 HaveCounts: true,
2350 }
2351 err = tx.Insert(&dstMB)
2352 xcheckf(err, "create new destination mailbox")
2353
2354 modseq, err := c.account.NextModSeq(tx)
2355 xcheckf(err, "assigning next modseq")
2356
2357 changes = make([]store.Change, 2) // Placeholders filled in below.
2358
2359 // Move existing messages, with their ID's and on-disk files intact, to the new
2360 // mailbox. We keep the expunged messages, the destination mailbox doesn't care
2361 // about them.
2362 var oldUIDs []store.UID
2363 q := bstore.QueryTx[store.Message](tx)
2364 q.FilterNonzero(store.Message{MailboxID: srcMB.ID})
2365 q.FilterEqual("Expunged", false)
2366 q.SortAsc("UID")
2367 err = q.ForEach(func(m store.Message) error {
2368 om := m
2369 om.ID = 0
2370 om.ModSeq = modseq
2371 om.PrepareExpunge()
2372 oldUIDs = append(oldUIDs, om.UID)
2373
2374 mc := m.MailboxCounts()
2375 srcMB.Sub(mc)
2376 dstMB.Add(mc)
2377
2378 m.MailboxID = dstMB.ID
2379 m.UID = dstMB.UIDNext
2380 dstMB.UIDNext++
2381 m.CreateSeq = modseq
2382 m.ModSeq = modseq
2383 if err := tx.Update(&m); err != nil {
2384 return fmt.Errorf("updating message to move to new mailbox: %w", err)
2385 }
2386
2387 changes = append(changes, m.ChangeAddUID())
2388
2389 if err := tx.Insert(&om); err != nil {
2390 return fmt.Errorf("adding empty expunge message record to inbox: %w", err)
2391 }
2392 return nil
2393 })
2394 xcheckf(err, "moving messages from inbox to destination mailbox")
2395
2396 err = tx.Update(&dstMB)
2397 xcheckf(err, "updating uidnext and counts in destination mailbox")
2398
2399 err = tx.Update(&srcMB)
2400 xcheckf(err, "updating counts for inbox")
2401
2402 var dstFlags []string
2403 if tx.Get(&store.Subscription{Name: dstMB.Name}) == nil {
2404 dstFlags = []string{`\Subscribed`}
2405 }
2406 changes[0] = store.ChangeRemoveUIDs{MailboxID: srcMB.ID, UIDs: oldUIDs, ModSeq: modseq}
2407 changes[1] = store.ChangeAddMailbox{Mailbox: dstMB, Flags: dstFlags}
2408 // changes[2:...] are ChangeAddUIDs
2409 changes = append(changes, srcMB.ChangeCounts(), dstMB.ChangeCounts())
2410 return
2411 }
2412
2413 var notExists, alreadyExists bool
2414 var err error
2415 changes, _, notExists, alreadyExists, err = c.account.MailboxRename(tx, srcMB, dst)
2416 if notExists {
2417 // ../rfc/9051:5140
2418 xusercodeErrorf("NONEXISTENT", "%s", err)
2419 } else if alreadyExists {
2420 xusercodeErrorf("ALREADYEXISTS", "%s", err)
2421 }
2422 xcheckf(err, "renaming mailbox")
2423 })
2424 c.broadcast(changes)
2425 })
2426
2427 c.ok(tag, cmd)
2428}
2429
2430// Subscribe marks a mailbox path as subscribed. The mailbox does not have to
2431// exist. Subscribed may mean an email client will show the mailbox in its UI
2432// and/or periodically fetch new messages for the mailbox.
2433//
2434// State: Authenticated and selected.
2435func (c *conn) cmdSubscribe(tag, cmd string, p *parser) {
2436 // Command: ../rfc/9051:2172 ../rfc/3501:2135
2437 // Examples: ../rfc/9051:2198 ../rfc/3501:2162
2438
2439 // Request syntax: ../rfc/9051:7083 ../rfc/3501:5059
2440 p.xspace()
2441 name := p.xmailbox()
2442 p.xempty()
2443
2444 name = xcheckmailboxname(name, true)
2445
2446 c.account.WithWLock(func() {
2447 var changes []store.Change
2448
2449 c.xdbwrite(func(tx *bstore.Tx) {
2450 var err error
2451 changes, err = c.account.SubscriptionEnsure(tx, name)
2452 xcheckf(err, "ensuring subscription")
2453 })
2454
2455 c.broadcast(changes)
2456 })
2457
2458 c.ok(tag, cmd)
2459}
2460
2461// Unsubscribe marks a mailbox as not subscribed. The mailbox doesn't have to exist.
2462//
2463// State: Authenticated and selected.
2464func (c *conn) cmdUnsubscribe(tag, cmd string, p *parser) {
2465 // Command: ../rfc/9051:2203 ../rfc/3501:2166
2466 // Examples: ../rfc/9051:2219 ../rfc/3501:2181
2467
2468 // Request syntax: ../rfc/9051:7143 ../rfc/3501:5077
2469 p.xspace()
2470 name := p.xmailbox()
2471 p.xempty()
2472
2473 name = xcheckmailboxname(name, true)
2474
2475 c.account.WithWLock(func() {
2476 c.xdbwrite(func(tx *bstore.Tx) {
2477 // It's OK if not currently subscribed, ../rfc/9051:2215
2478 err := tx.Delete(&store.Subscription{Name: name})
2479 if err == bstore.ErrAbsent {
2480 exists, err := c.account.MailboxExists(tx, name)
2481 xcheckf(err, "checking if mailbox exists")
2482 if !exists {
2483 xuserErrorf("mailbox does not exist")
2484 }
2485 return
2486 }
2487 xcheckf(err, "removing subscription")
2488 })
2489
2490 // todo: can we send untagged message about a mailbox no longer being subscribed?
2491 })
2492
2493 c.ok(tag, cmd)
2494}
2495
2496// LSUB command for listing subscribed mailboxes.
2497// Removed in IMAP4rev2, only in IMAP4rev1.
2498//
2499// State: Authenticated and selected.
2500func (c *conn) cmdLsub(tag, cmd string, p *parser) {
2501 // Command: ../rfc/3501:2374
2502 // Examples: ../rfc/3501:2415
2503
2504 // Request syntax: ../rfc/3501:4806
2505 p.xspace()
2506 ref := p.xmailbox()
2507 p.xspace()
2508 pattern := p.xlistMailbox()
2509 p.xempty()
2510
2511 re := xmailboxPatternMatcher(ref, []string{pattern})
2512
2513 var lines []string
2514 c.xdbread(func(tx *bstore.Tx) {
2515 q := bstore.QueryTx[store.Subscription](tx)
2516 q.SortAsc("Name")
2517 subscriptions, err := q.List()
2518 xcheckf(err, "querying subscriptions")
2519
2520 have := map[string]bool{}
2521 subscribedKids := map[string]bool{}
2522 ispercent := strings.HasSuffix(pattern, "%")
2523 for _, sub := range subscriptions {
2524 name := sub.Name
2525 if ispercent {
2526 for p := path.Dir(name); p != "."; p = path.Dir(p) {
2527 subscribedKids[p] = true
2528 }
2529 }
2530 if !re.MatchString(name) {
2531 continue
2532 }
2533 have[name] = true
2534 line := fmt.Sprintf(`* LSUB () "/" %s`, astring(c.encodeMailbox(name)).pack(c))
2535 lines = append(lines, line)
2536
2537 }
2538
2539 // ../rfc/3501:2394
2540 if !ispercent {
2541 return
2542 }
2543 qmb := bstore.QueryTx[store.Mailbox](tx)
2544 qmb.SortAsc("Name")
2545 err = qmb.ForEach(func(mb store.Mailbox) error {
2546 if have[mb.Name] || !subscribedKids[mb.Name] || !re.MatchString(mb.Name) {
2547 return nil
2548 }
2549 line := fmt.Sprintf(`* LSUB (\NoSelect) "/" %s`, astring(c.encodeMailbox(mb.Name)).pack(c))
2550 lines = append(lines, line)
2551 return nil
2552 })
2553 xcheckf(err, "querying mailboxes")
2554 })
2555
2556 // Response syntax: ../rfc/3501:4833 ../rfc/3501:4837
2557 for _, line := range lines {
2558 c.bwritelinef("%s", line)
2559 }
2560 c.ok(tag, cmd)
2561}
2562
2563// The namespace command returns the mailbox path separator. We only implement
2564// the personal mailbox hierarchy, no shared/other.
2565//
2566// In IMAP4rev2, it was an extension before.
2567//
2568// State: Authenticated and selected.
2569func (c *conn) cmdNamespace(tag, cmd string, p *parser) {
2570 // Command: ../rfc/9051:3098 ../rfc/2342:137
2571 // Examples: ../rfc/9051:3117 ../rfc/2342:155
2572 // Request syntax: ../rfc/9051:6767 ../rfc/2342:410
2573 p.xempty()
2574
2575 // Response syntax: ../rfc/9051:6778 ../rfc/2342:415
2576 c.bwritelinef(`* NAMESPACE (("" "/")) NIL NIL`)
2577 c.ok(tag, cmd)
2578}
2579
2580// The status command returns information about a mailbox, such as the number of
2581// messages, "uid validity", etc. Nowadays, the extended LIST command can return
2582// the same information about many mailboxes for one command.
2583//
2584// State: Authenticated and selected.
2585func (c *conn) cmdStatus(tag, cmd string, p *parser) {
2586 // Command: ../rfc/9051:3328 ../rfc/3501:2424 ../rfc/7162:1127
2587 // Examples: ../rfc/9051:3400 ../rfc/3501:2501 ../rfc/7162:1139
2588
2589 // Request syntax: ../rfc/9051:7053 ../rfc/3501:5036
2590 p.xspace()
2591 name := p.xmailbox()
2592 p.xspace()
2593 p.xtake("(")
2594 attrs := []string{p.xstatusAtt()}
2595 for !p.take(")") {
2596 p.xspace()
2597 attrs = append(attrs, p.xstatusAtt())
2598 }
2599 p.xempty()
2600
2601 name = xcheckmailboxname(name, true)
2602
2603 var mb store.Mailbox
2604
2605 var responseLine string
2606 c.account.WithRLock(func() {
2607 c.xdbread(func(tx *bstore.Tx) {
2608 mb = c.xmailbox(tx, name, "")
2609 responseLine = c.xstatusLine(tx, mb, attrs)
2610 })
2611 })
2612
2613 c.bwritelinef("%s", responseLine)
2614 c.ok(tag, cmd)
2615}
2616
2617// Response syntax: ../rfc/9051:6681 ../rfc/9051:7070 ../rfc/9051:7059 ../rfc/3501:4834
2618func (c *conn) xstatusLine(tx *bstore.Tx, mb store.Mailbox, attrs []string) string {
2619 status := []string{}
2620 for _, a := range attrs {
2621 A := strings.ToUpper(a)
2622 switch A {
2623 case "MESSAGES":
2624 status = append(status, A, fmt.Sprintf("%d", mb.Total+mb.Deleted))
2625 case "UIDNEXT":
2626 status = append(status, A, fmt.Sprintf("%d", mb.UIDNext))
2627 case "UIDVALIDITY":
2628 status = append(status, A, fmt.Sprintf("%d", mb.UIDValidity))
2629 case "UNSEEN":
2630 status = append(status, A, fmt.Sprintf("%d", mb.Unseen))
2631 case "DELETED":
2632 status = append(status, A, fmt.Sprintf("%d", mb.Deleted))
2633 case "SIZE":
2634 status = append(status, A, fmt.Sprintf("%d", mb.Size))
2635 case "RECENT":
2636 status = append(status, A, "0")
2637 case "APPENDLIMIT":
2638 // ../rfc/7889:255
2639 status = append(status, A, "NIL")
2640 case "HIGHESTMODSEQ":
2641 // ../rfc/7162:366
2642 status = append(status, A, fmt.Sprintf("%d", c.xhighestModSeq(tx, mb.ID).Client()))
2643 default:
2644 xsyntaxErrorf("unknown attribute %q", a)
2645 }
2646 }
2647 return fmt.Sprintf("* STATUS %s (%s)", astring(c.encodeMailbox(mb.Name)).pack(c), strings.Join(status, " "))
2648}
2649
2650func flaglist(fl store.Flags, keywords []string) listspace {
2651 l := listspace{}
2652 flag := func(v bool, s string) {
2653 if v {
2654 l = append(l, bare(s))
2655 }
2656 }
2657 flag(fl.Seen, `\Seen`)
2658 flag(fl.Answered, `\Answered`)
2659 flag(fl.Flagged, `\Flagged`)
2660 flag(fl.Deleted, `\Deleted`)
2661 flag(fl.Draft, `\Draft`)
2662 flag(fl.Forwarded, `$Forwarded`)
2663 flag(fl.Junk, `$Junk`)
2664 flag(fl.Notjunk, `$NotJunk`)
2665 flag(fl.Phishing, `$Phishing`)
2666 flag(fl.MDNSent, `$MDNSent`)
2667 for _, k := range keywords {
2668 l = append(l, bare(k))
2669 }
2670 return l
2671}
2672
2673// Append adds a message to a mailbox.
2674//
2675// State: Authenticated and selected.
2676func (c *conn) cmdAppend(tag, cmd string, p *parser) {
2677 // Command: ../rfc/9051:3406 ../rfc/6855:204 ../rfc/3501:2527
2678 // Examples: ../rfc/9051:3482 ../rfc/3501:2589
2679
2680 // Request syntax: ../rfc/9051:6325 ../rfc/6855:219 ../rfc/3501:4547
2681 p.xspace()
2682 name := p.xmailbox()
2683 p.xspace()
2684 var storeFlags store.Flags
2685 var keywords []string
2686 if p.hasPrefix("(") {
2687 // Error must be a syntax error, to properly abort the connection due to literal.
2688 var err error
2689 storeFlags, keywords, err = store.ParseFlagsKeywords(p.xflagList())
2690 if err != nil {
2691 xsyntaxErrorf("parsing flags: %v", err)
2692 }
2693 p.xspace()
2694 }
2695 var tm time.Time
2696 if p.hasPrefix(`"`) {
2697 tm = p.xdateTime()
2698 p.xspace()
2699 } else {
2700 tm = time.Now()
2701 }
2702 // todo: only with utf8 should we we accept message headers with utf-8. we currently always accept them.
2703 // todo: this is only relevant if we also support the CATENATE extension?
2704 // ../rfc/6855:204
2705 utf8 := p.take("UTF8 (")
2706 size, sync := p.xliteralSize(0, utf8)
2707
2708 name = xcheckmailboxname(name, true)
2709 c.xdbread(func(tx *bstore.Tx) {
2710 c.xmailbox(tx, name, "TRYCREATE")
2711 })
2712 if sync {
2713 c.writelinef("+ ")
2714 }
2715
2716 // Read the message into a temporary file.
2717 msgFile, err := store.CreateMessageTemp(c.log, "imap-append")
2718 xcheckf(err, "creating temp file for message")
2719 defer func() {
2720 p := msgFile.Name()
2721 err := msgFile.Close()
2722 c.xsanity(err, "closing APPEND temporary file")
2723 err = os.Remove(p)
2724 c.xsanity(err, "removing APPEND temporary file")
2725 }()
2726 defer c.xtrace(mlog.LevelTracedata)()
2727 mw := message.NewWriter(msgFile)
2728 msize, err := io.Copy(mw, io.LimitReader(c.br, size))
2729 c.xtrace(mlog.LevelTrace) // Restore.
2730 if err != nil {
2731 // Cannot use xcheckf due to %w handling of errIO.
2732 panic(fmt.Errorf("reading literal message: %s (%w)", err, errIO))
2733 }
2734 if msize != size {
2735 xserverErrorf("read %d bytes for message, expected %d (%w)", msize, size, errIO)
2736 }
2737
2738 if utf8 {
2739 line := c.readline(false)
2740 np := newParser(line, c)
2741 np.xtake(")")
2742 np.xempty()
2743 } else {
2744 line := c.readline(false)
2745 np := newParser(line, c)
2746 np.xempty()
2747 }
2748 p.xempty()
2749 if !sync {
2750 name = xcheckmailboxname(name, true)
2751 }
2752
2753 var mb store.Mailbox
2754 var m store.Message
2755 var pendingChanges []store.Change
2756
2757 c.account.WithWLock(func() {
2758 var changes []store.Change
2759 c.xdbwrite(func(tx *bstore.Tx) {
2760 mb = c.xmailbox(tx, name, "TRYCREATE")
2761
2762 // Ensure keywords are stored in mailbox.
2763 var mbKwChanged bool
2764 mb.Keywords, mbKwChanged = store.MergeKeywords(mb.Keywords, keywords)
2765 if mbKwChanged {
2766 changes = append(changes, mb.ChangeKeywords())
2767 }
2768
2769 m = store.Message{
2770 MailboxID: mb.ID,
2771 MailboxOrigID: mb.ID,
2772 Received: tm,
2773 Flags: storeFlags,
2774 Keywords: keywords,
2775 Size: mw.Size,
2776 }
2777
2778 ok, maxSize, err := c.account.CanAddMessageSize(tx, m.Size)
2779 xcheckf(err, "checking quota")
2780 if !ok {
2781 // ../rfc/9051:5155
2782 xusercodeErrorf("OVERQUOTA", "account over maximum total message size %d", maxSize)
2783 }
2784
2785 mb.Add(m.MailboxCounts())
2786
2787 // Update mailbox before delivering, which updates uidnext which we mustn't overwrite.
2788 err = tx.Update(&mb)
2789 xcheckf(err, "updating mailbox counts")
2790
2791 err = c.account.DeliverMessage(c.log, tx, &m, msgFile, true, false, false, true)
2792 xcheckf(err, "delivering message")
2793 })
2794
2795 // Fetch pending changes, possibly with new UIDs, so we can apply them before adding our own new UID.
2796 if c.comm != nil {
2797 pendingChanges = c.comm.Get()
2798 }
2799
2800 // Broadcast the change to other connections.
2801 changes = append(changes, m.ChangeAddUID(), mb.ChangeCounts())
2802 c.broadcast(changes)
2803 })
2804
2805 if c.mailboxID == mb.ID {
2806 c.applyChanges(pendingChanges, false)
2807 c.uidAppend(m.UID)
2808 // todo spec: with condstore/qresync, is there a mechanism to the client know the modseq for the appended uid? in theory an untagged fetch with the modseq after the OK APPENDUID could make sense, but this probably isn't allowed.
2809 c.bwritelinef("* %d EXISTS", len(c.uids))
2810 }
2811
2812 c.writeresultf("%s OK [APPENDUID %d %d] appended", tag, mb.UIDValidity, m.UID)
2813}
2814
2815// Idle makes a client wait until the server sends untagged updates, e.g. about
2816// message delivery or mailbox create/rename/delete/subscription, etc. It allows a
2817// client to get updates in real-time, not needing the use for NOOP.
2818//
2819// State: Authenticated and selected.
2820func (c *conn) cmdIdle(tag, cmd string, p *parser) {
2821 // Command: ../rfc/9051:3542 ../rfc/2177:49
2822 // Example: ../rfc/9051:3589 ../rfc/2177:119
2823
2824 // Request syntax: ../rfc/9051:6594 ../rfc/2177:163
2825 p.xempty()
2826
2827 c.writelinef("+ waiting")
2828
2829 var line string
2830wait:
2831 for {
2832 select {
2833 case le := <-c.lineChan():
2834 c.line = nil
2835 xcheckf(le.err, "get line")
2836 line = le.line
2837 break wait
2838 case <-c.comm.Pending:
2839 c.applyChanges(c.comm.Get(), false)
2840 c.xflush()
2841 case <-mox.Shutdown.Done():
2842 // ../rfc/9051:5375
2843 c.writelinef("* BYE shutting down")
2844 panic(errIO)
2845 }
2846 }
2847
2848 // Reset the write deadline. In case of little activity, with a command timeout of
2849 // 30 minutes, we have likely passed it.
2850 err := c.conn.SetWriteDeadline(time.Now().Add(5 * time.Minute))
2851 c.log.Check(err, "setting write deadline")
2852
2853 if strings.ToUpper(line) != "DONE" {
2854 // We just close the connection because our protocols are out of sync.
2855 panic(fmt.Errorf("%w: in IDLE, expected DONE", errIO))
2856 }
2857
2858 c.ok(tag, cmd)
2859}
2860
2861// Check is an old deprecated command that is supposed to execute some mailbox consistency checks.
2862//
2863// State: Selected
2864func (c *conn) cmdCheck(tag, cmd string, p *parser) {
2865 // Command: ../rfc/3501:2618
2866
2867 // Request syntax: ../rfc/3501:4679
2868 p.xempty()
2869
2870 c.account.WithRLock(func() {
2871 c.xdbread(func(tx *bstore.Tx) {
2872 c.xmailboxID(tx, c.mailboxID) // Validate.
2873 })
2874 })
2875
2876 c.ok(tag, cmd)
2877}
2878
2879// Close undoes select/examine, closing the currently opened mailbox and deleting
2880// messages that were marked for deletion with the \Deleted flag.
2881//
2882// State: Selected
2883func (c *conn) cmdClose(tag, cmd string, p *parser) {
2884 // Command: ../rfc/9051:3636 ../rfc/3501:2652 ../rfc/7162:1836
2885
2886 // Request syntax: ../rfc/9051:6476 ../rfc/3501:4679
2887 p.xempty()
2888
2889 if c.readonly {
2890 c.unselect()
2891 c.ok(tag, cmd)
2892 return
2893 }
2894
2895 remove, _ := c.xexpunge(nil, true)
2896
2897 defer func() {
2898 for _, m := range remove {
2899 p := c.account.MessagePath(m.ID)
2900 err := os.Remove(p)
2901 c.xsanity(err, "removing message file for expunge for close")
2902 }
2903 }()
2904
2905 c.unselect()
2906 c.ok(tag, cmd)
2907}
2908
2909// expunge messages marked for deletion in currently selected/active mailbox.
2910// if uidSet is not nil, only messages matching the set are deleted.
2911//
2912// messages that have been marked expunged from the database are returned, but the
2913// corresponding files still have to be removed.
2914//
2915// the highest modseq in the mailbox is returned, typically associated with the
2916// removal of the messages, but if no messages were expunged the current latest max
2917// modseq for the mailbox is returned.
2918func (c *conn) xexpunge(uidSet *numSet, missingMailboxOK bool) (remove []store.Message, highestModSeq store.ModSeq) {
2919 var modseq store.ModSeq
2920
2921 c.account.WithWLock(func() {
2922 var mb store.Mailbox
2923
2924 c.xdbwrite(func(tx *bstore.Tx) {
2925 mb = store.Mailbox{ID: c.mailboxID}
2926 err := tx.Get(&mb)
2927 if err == bstore.ErrAbsent {
2928 if missingMailboxOK {
2929 return
2930 }
2931 xuserErrorf("%w", store.ErrUnknownMailbox)
2932 }
2933
2934 qm := bstore.QueryTx[store.Message](tx)
2935 qm.FilterNonzero(store.Message{MailboxID: c.mailboxID})
2936 qm.FilterEqual("Deleted", true)
2937 qm.FilterEqual("Expunged", false)
2938 qm.FilterFn(func(m store.Message) bool {
2939 // Only remove if this session knows about the message and if present in optional uidSet.
2940 return uidSearch(c.uids, m.UID) > 0 && (uidSet == nil || uidSet.containsUID(m.UID, c.uids, c.searchResult))
2941 })
2942 qm.SortAsc("UID")
2943 remove, err = qm.List()
2944 xcheckf(err, "listing messages to delete")
2945
2946 if len(remove) == 0 {
2947 highestModSeq = c.xhighestModSeq(tx, c.mailboxID)
2948 return
2949 }
2950
2951 // Assign new modseq.
2952 modseq, err = c.account.NextModSeq(tx)
2953 xcheckf(err, "assigning next modseq")
2954 highestModSeq = modseq
2955
2956 removeIDs := make([]int64, len(remove))
2957 anyIDs := make([]any, len(remove))
2958 var totalSize int64
2959 for i, m := range remove {
2960 removeIDs[i] = m.ID
2961 anyIDs[i] = m.ID
2962 mb.Sub(m.MailboxCounts())
2963 totalSize += m.Size
2964 // Update "remove", because RetrainMessage below will save the message.
2965 remove[i].Expunged = true
2966 remove[i].ModSeq = modseq
2967 }
2968 qmr := bstore.QueryTx[store.Recipient](tx)
2969 qmr.FilterEqual("MessageID", anyIDs...)
2970 _, err = qmr.Delete()
2971 xcheckf(err, "removing message recipients")
2972
2973 qm = bstore.QueryTx[store.Message](tx)
2974 qm.FilterIDs(removeIDs)
2975 n, err := qm.UpdateNonzero(store.Message{Expunged: true, ModSeq: modseq})
2976 if err == nil && n != len(removeIDs) {
2977 err = fmt.Errorf("only %d messages set to expunged, expected %d", n, len(removeIDs))
2978 }
2979 xcheckf(err, "marking messages marked for deleted as expunged")
2980
2981 err = tx.Update(&mb)
2982 xcheckf(err, "updating mailbox counts")
2983
2984 err = c.account.AddMessageSize(c.log, tx, -totalSize)
2985 xcheckf(err, "updating disk usage")
2986
2987 // Mark expunged messages as not needing training, then retrain them, so if they
2988 // were trained, they get untrained.
2989 for i := range remove {
2990 remove[i].Junk = false
2991 remove[i].Notjunk = false
2992 }
2993 err = c.account.RetrainMessages(context.TODO(), c.log, tx, remove, true)
2994 xcheckf(err, "untraining expunged messages")
2995 })
2996
2997 // Broadcast changes to other connections. We may not have actually removed any
2998 // messages, so take care not to send an empty update.
2999 if len(remove) > 0 {
3000 ouids := make([]store.UID, len(remove))
3001 for i, m := range remove {
3002 ouids[i] = m.UID
3003 }
3004 changes := []store.Change{
3005 store.ChangeRemoveUIDs{MailboxID: c.mailboxID, UIDs: ouids, ModSeq: modseq},
3006 mb.ChangeCounts(),
3007 }
3008 c.broadcast(changes)
3009 }
3010 })
3011 return remove, highestModSeq
3012}
3013
3014// Unselect is similar to close in that it closes the currently active mailbox, but
3015// it does not remove messages marked for deletion.
3016//
3017// State: Selected
3018func (c *conn) cmdUnselect(tag, cmd string, p *parser) {
3019 // Command: ../rfc/9051:3667 ../rfc/3691:89
3020
3021 // Request syntax: ../rfc/9051:6476 ../rfc/3691:135
3022 p.xempty()
3023
3024 c.unselect()
3025 c.ok(tag, cmd)
3026}
3027
3028// Expunge deletes messages marked with \Deleted in the currently selected mailbox.
3029// Clients are wiser to use UID EXPUNGE because it allows a UID sequence set to
3030// explicitly opt in to removing specific messages.
3031//
3032// State: Selected
3033func (c *conn) cmdExpunge(tag, cmd string, p *parser) {
3034 // Command: ../rfc/9051:3687 ../rfc/3501:2695 ../rfc/7162:1770
3035
3036 // Request syntax: ../rfc/9051:6476 ../rfc/3501:4679
3037 p.xempty()
3038
3039 if c.readonly {
3040 xuserErrorf("mailbox open in read-only mode")
3041 }
3042
3043 c.cmdxExpunge(tag, cmd, nil)
3044}
3045
3046// UID expunge deletes messages marked with \Deleted in the currently selected
3047// mailbox if they match a UID sequence set.
3048//
3049// State: Selected
3050func (c *conn) cmdUIDExpunge(tag, cmd string, p *parser) {
3051 // Command: ../rfc/9051:4775 ../rfc/4315:75 ../rfc/7162:1873
3052
3053 // Request syntax: ../rfc/9051:7125 ../rfc/9051:7129 ../rfc/4315:298
3054 p.xspace()
3055 uidSet := p.xnumSet()
3056 p.xempty()
3057
3058 if c.readonly {
3059 xuserErrorf("mailbox open in read-only mode")
3060 }
3061
3062 c.cmdxExpunge(tag, cmd, &uidSet)
3063}
3064
3065// Permanently delete messages for the currently selected/active mailbox. If uidset
3066// is not nil, only those UIDs are removed.
3067// State: Selected
3068func (c *conn) cmdxExpunge(tag, cmd string, uidSet *numSet) {
3069 // Command: ../rfc/9051:3687 ../rfc/3501:2695
3070
3071 remove, highestModSeq := c.xexpunge(uidSet, false)
3072
3073 defer func() {
3074 for _, m := range remove {
3075 p := c.account.MessagePath(m.ID)
3076 err := os.Remove(p)
3077 c.xsanity(err, "removing message file for expunge")
3078 }
3079 }()
3080
3081 // Response syntax: ../rfc/9051:6742 ../rfc/3501:4864
3082 var vanishedUIDs numSet
3083 qresync := c.enabled[capQresync]
3084 for _, m := range remove {
3085 seq := c.xsequence(m.UID)
3086 c.sequenceRemove(seq, m.UID)
3087 if qresync {
3088 vanishedUIDs.append(uint32(m.UID))
3089 } else {
3090 c.bwritelinef("* %d EXPUNGE", seq)
3091 }
3092 }
3093 if !vanishedUIDs.empty() {
3094 // VANISHED without EARLIER. ../rfc/7162:2004
3095 for _, s := range vanishedUIDs.Strings(4*1024 - 32) {
3096 c.bwritelinef("* VANISHED %s", s)
3097 }
3098 }
3099
3100 if c.enabled[capCondstore] {
3101 c.writeresultf("%s OK [HIGHESTMODSEQ %d] expunged", tag, highestModSeq.Client())
3102 } else {
3103 c.ok(tag, cmd)
3104 }
3105}
3106
3107// State: Selected
3108func (c *conn) cmdSearch(tag, cmd string, p *parser) {
3109 c.cmdxSearch(false, tag, cmd, p)
3110}
3111
3112// State: Selected
3113func (c *conn) cmdUIDSearch(tag, cmd string, p *parser) {
3114 c.cmdxSearch(true, tag, cmd, p)
3115}
3116
3117// State: Selected
3118func (c *conn) cmdFetch(tag, cmd string, p *parser) {
3119 c.cmdxFetch(false, tag, cmd, p)
3120}
3121
3122// State: Selected
3123func (c *conn) cmdUIDFetch(tag, cmd string, p *parser) {
3124 c.cmdxFetch(true, tag, cmd, p)
3125}
3126
3127// State: Selected
3128func (c *conn) cmdStore(tag, cmd string, p *parser) {
3129 c.cmdxStore(false, tag, cmd, p)
3130}
3131
3132// State: Selected
3133func (c *conn) cmdUIDStore(tag, cmd string, p *parser) {
3134 c.cmdxStore(true, tag, cmd, p)
3135}
3136
3137// State: Selected
3138func (c *conn) cmdCopy(tag, cmd string, p *parser) {
3139 c.cmdxCopy(false, tag, cmd, p)
3140}
3141
3142// State: Selected
3143func (c *conn) cmdUIDCopy(tag, cmd string, p *parser) {
3144 c.cmdxCopy(true, tag, cmd, p)
3145}
3146
3147// State: Selected
3148func (c *conn) cmdMove(tag, cmd string, p *parser) {
3149 c.cmdxMove(false, tag, cmd, p)
3150}
3151
3152// State: Selected
3153func (c *conn) cmdUIDMove(tag, cmd string, p *parser) {
3154 c.cmdxMove(true, tag, cmd, p)
3155}
3156
3157func (c *conn) gatherCopyMoveUIDs(isUID bool, nums numSet) ([]store.UID, []any) {
3158 // Gather uids, then sort so we can return a consistently simple and hard to
3159 // misinterpret COPYUID/MOVEUID response. It seems safer to have UIDs in ascending
3160 // order, because requested uid set of 12:10 is equal to 10:12, so if we would just
3161 // echo whatever the client sends us without reordering, the client can reorder our
3162 // response and interpret it differently than we intended.
3163 // ../rfc/9051:5072
3164 uids := c.xnumSetUIDs(isUID, nums)
3165 sort.Slice(uids, func(i, j int) bool {
3166 return uids[i] < uids[j]
3167 })
3168 uidargs := make([]any, len(uids))
3169 for i, uid := range uids {
3170 uidargs[i] = uid
3171 }
3172 return uids, uidargs
3173}
3174
3175// Copy copies messages from the currently selected/active mailbox to another named
3176// mailbox.
3177//
3178// State: Selected
3179func (c *conn) cmdxCopy(isUID bool, tag, cmd string, p *parser) {
3180 // Command: ../rfc/9051:4602 ../rfc/3501:3288
3181
3182 // Request syntax: ../rfc/9051:6482 ../rfc/3501:4685
3183 p.xspace()
3184 nums := p.xnumSet()
3185 p.xspace()
3186 name := p.xmailbox()
3187 p.xempty()
3188
3189 name = xcheckmailboxname(name, true)
3190
3191 uids, uidargs := c.gatherCopyMoveUIDs(isUID, nums)
3192
3193 // Files that were created during the copy. Remove them if the operation fails.
3194 var createdIDs []int64
3195 defer func() {
3196 x := recover()
3197 if x == nil {
3198 return
3199 }
3200 for _, id := range createdIDs {
3201 p := c.account.MessagePath(id)
3202 err := os.Remove(p)
3203 c.xsanity(err, "cleaning up created file")
3204 }
3205 panic(x)
3206 }()
3207
3208 var mbDst store.Mailbox
3209 var origUIDs, newUIDs []store.UID
3210 var flags []store.Flags
3211 var keywords [][]string
3212 var modseq store.ModSeq // For messages in new mailbox, assigned when first message is copied.
3213
3214 c.account.WithWLock(func() {
3215 var mbKwChanged bool
3216
3217 c.xdbwrite(func(tx *bstore.Tx) {
3218 mbSrc := c.xmailboxID(tx, c.mailboxID) // Validate.
3219 mbDst = c.xmailbox(tx, name, "TRYCREATE")
3220 if mbDst.ID == mbSrc.ID {
3221 xuserErrorf("cannot copy to currently selected mailbox")
3222 }
3223
3224 if len(uidargs) == 0 {
3225 xuserErrorf("no matching messages to copy")
3226 }
3227
3228 var err error
3229 modseq, err = c.account.NextModSeq(tx)
3230 xcheckf(err, "assigning next modseq")
3231
3232 // Reserve the uids in the destination mailbox.
3233 uidFirst := mbDst.UIDNext
3234 mbDst.UIDNext += store.UID(len(uidargs))
3235
3236 // Fetch messages from database.
3237 q := bstore.QueryTx[store.Message](tx)
3238 q.FilterNonzero(store.Message{MailboxID: c.mailboxID})
3239 q.FilterEqual("UID", uidargs...)
3240 q.FilterEqual("Expunged", false)
3241 xmsgs, err := q.List()
3242 xcheckf(err, "fetching messages")
3243
3244 if len(xmsgs) != len(uidargs) {
3245 xserverErrorf("uid and message mismatch")
3246 }
3247
3248 // See if quota allows copy.
3249 var totalSize int64
3250 for _, m := range xmsgs {
3251 totalSize += m.Size
3252 }
3253 if ok, maxSize, err := c.account.CanAddMessageSize(tx, totalSize); err != nil {
3254 xcheckf(err, "checking quota")
3255 } else if !ok {
3256 // ../rfc/9051:5155
3257 xusercodeErrorf("OVERQUOTA", "account over maximum total message size %d", maxSize)
3258 }
3259 err = c.account.AddMessageSize(c.log, tx, totalSize)
3260 xcheckf(err, "updating disk usage")
3261
3262 msgs := map[store.UID]store.Message{}
3263 for _, m := range xmsgs {
3264 msgs[m.UID] = m
3265 }
3266 nmsgs := make([]store.Message, len(xmsgs))
3267
3268 conf, _ := c.account.Conf()
3269
3270 mbKeywords := map[string]struct{}{}
3271
3272 // Insert new messages into database.
3273 var origMsgIDs, newMsgIDs []int64
3274 for i, uid := range uids {
3275 m, ok := msgs[uid]
3276 if !ok {
3277 xuserErrorf("messages changed, could not fetch requested uid")
3278 }
3279 origID := m.ID
3280 origMsgIDs = append(origMsgIDs, origID)
3281 m.ID = 0
3282 m.UID = uidFirst + store.UID(i)
3283 m.CreateSeq = modseq
3284 m.ModSeq = modseq
3285 m.MailboxID = mbDst.ID
3286 if m.IsReject && m.MailboxDestinedID != 0 {
3287 // Incorrectly delivered to Rejects mailbox. Adjust MailboxOrigID so this message
3288 // is used for reputation calculation during future deliveries.
3289 m.MailboxOrigID = m.MailboxDestinedID
3290 m.IsReject = false
3291 }
3292 m.TrainedJunk = nil
3293 m.JunkFlagsForMailbox(mbDst, conf)
3294 err := tx.Insert(&m)
3295 xcheckf(err, "inserting message")
3296 msgs[uid] = m
3297 nmsgs[i] = m
3298 origUIDs = append(origUIDs, uid)
3299 newUIDs = append(newUIDs, m.UID)
3300 newMsgIDs = append(newMsgIDs, m.ID)
3301 flags = append(flags, m.Flags)
3302 keywords = append(keywords, m.Keywords)
3303 for _, kw := range m.Keywords {
3304 mbKeywords[kw] = struct{}{}
3305 }
3306
3307 qmr := bstore.QueryTx[store.Recipient](tx)
3308 qmr.FilterNonzero(store.Recipient{MessageID: origID})
3309 mrs, err := qmr.List()
3310 xcheckf(err, "listing message recipients")
3311 for _, mr := range mrs {
3312 mr.ID = 0
3313 mr.MessageID = m.ID
3314 err := tx.Insert(&mr)
3315 xcheckf(err, "inserting message recipient")
3316 }
3317
3318 mbDst.Add(m.MailboxCounts())
3319 }
3320
3321 mbDst.Keywords, mbKwChanged = store.MergeKeywords(mbDst.Keywords, maps.Keys(mbKeywords))
3322
3323 err = tx.Update(&mbDst)
3324 xcheckf(err, "updating destination mailbox for uids, keywords and counts")
3325
3326 // Copy message files to new message ID's.
3327 syncDirs := map[string]struct{}{}
3328 for i := range origMsgIDs {
3329 src := c.account.MessagePath(origMsgIDs[i])
3330 dst := c.account.MessagePath(newMsgIDs[i])
3331 dstdir := filepath.Dir(dst)
3332 if _, ok := syncDirs[dstdir]; !ok {
3333 os.MkdirAll(dstdir, 0770)
3334 syncDirs[dstdir] = struct{}{}
3335 }
3336 err := moxio.LinkOrCopy(c.log, dst, src, nil, true)
3337 xcheckf(err, "link or copy file %q to %q", src, dst)
3338 createdIDs = append(createdIDs, newMsgIDs[i])
3339 }
3340
3341 for dir := range syncDirs {
3342 err := moxio.SyncDir(c.log, dir)
3343 xcheckf(err, "sync directory")
3344 }
3345
3346 err = c.account.RetrainMessages(context.TODO(), c.log, tx, nmsgs, false)
3347 xcheckf(err, "train copied messages")
3348 })
3349
3350 // Broadcast changes to other connections.
3351 if len(newUIDs) > 0 {
3352 changes := make([]store.Change, 0, len(newUIDs)+2)
3353 for i, uid := range newUIDs {
3354 changes = append(changes, store.ChangeAddUID{MailboxID: mbDst.ID, UID: uid, ModSeq: modseq, Flags: flags[i], Keywords: keywords[i]})
3355 }
3356 changes = append(changes, mbDst.ChangeCounts())
3357 if mbKwChanged {
3358 changes = append(changes, mbDst.ChangeKeywords())
3359 }
3360 c.broadcast(changes)
3361 }
3362 })
3363
3364 // All good, prevent defer above from cleaning up copied files.
3365 createdIDs = nil
3366
3367 // ../rfc/9051:6881 ../rfc/4315:183
3368 c.writeresultf("%s OK [COPYUID %d %s %s] copied", tag, mbDst.UIDValidity, compactUIDSet(origUIDs).String(), compactUIDSet(newUIDs).String())
3369}
3370
3371// Move moves messages from the currently selected/active mailbox to a named mailbox.
3372//
3373// State: Selected
3374func (c *conn) cmdxMove(isUID bool, tag, cmd string, p *parser) {
3375 // Command: ../rfc/9051:4650 ../rfc/6851:119 ../rfc/6851:265
3376
3377 // Request syntax: ../rfc/6851:320
3378 p.xspace()
3379 nums := p.xnumSet()
3380 p.xspace()
3381 name := p.xmailbox()
3382 p.xempty()
3383
3384 name = xcheckmailboxname(name, true)
3385
3386 if c.readonly {
3387 xuserErrorf("mailbox open in read-only mode")
3388 }
3389
3390 uids, uidargs := c.gatherCopyMoveUIDs(isUID, nums)
3391
3392 var mbSrc, mbDst store.Mailbox
3393 var changes []store.Change
3394 var newUIDs []store.UID
3395 var modseq store.ModSeq
3396
3397 c.account.WithWLock(func() {
3398 c.xdbwrite(func(tx *bstore.Tx) {
3399 mbSrc = c.xmailboxID(tx, c.mailboxID) // Validate.
3400 mbDst = c.xmailbox(tx, name, "TRYCREATE")
3401 if mbDst.ID == c.mailboxID {
3402 xuserErrorf("cannot move to currently selected mailbox")
3403 }
3404
3405 if len(uidargs) == 0 {
3406 xuserErrorf("no matching messages to move")
3407 }
3408
3409 // Reserve the uids in the destination mailbox.
3410 uidFirst := mbDst.UIDNext
3411 uidnext := uidFirst
3412 mbDst.UIDNext += store.UID(len(uids))
3413
3414 // Assign a new modseq, for the new records and for the expunged records.
3415 var err error
3416 modseq, err = c.account.NextModSeq(tx)
3417 xcheckf(err, "assigning next modseq")
3418
3419 // Update existing record with new UID and MailboxID in database for messages. We
3420 // add a new but expunged record again in the original/source mailbox, for qresync.
3421 // Keeping the original ID for the live message means we don't have to move the
3422 // on-disk message contents file.
3423 q := bstore.QueryTx[store.Message](tx)
3424 q.FilterNonzero(store.Message{MailboxID: c.mailboxID})
3425 q.FilterEqual("UID", uidargs...)
3426 q.FilterEqual("Expunged", false)
3427 q.SortAsc("UID")
3428 msgs, err := q.List()
3429 xcheckf(err, "listing messages to move")
3430
3431 if len(msgs) != len(uidargs) {
3432 xserverErrorf("uid and message mismatch")
3433 }
3434
3435 keywords := map[string]struct{}{}
3436
3437 conf, _ := c.account.Conf()
3438 for i := range msgs {
3439 m := &msgs[i]
3440 if m.UID != uids[i] {
3441 xserverErrorf("internal error: got uid %d, expected %d, for index %d", m.UID, uids[i], i)
3442 }
3443
3444 mbSrc.Sub(m.MailboxCounts())
3445
3446 // Copy of message record that we'll insert when UID is freed up.
3447 om := *m
3448 om.PrepareExpunge()
3449 om.ID = 0 // Assign new ID.
3450 om.ModSeq = modseq
3451
3452 m.MailboxID = mbDst.ID
3453 if m.IsReject && m.MailboxDestinedID != 0 {
3454 // Incorrectly delivered to Rejects mailbox. Adjust MailboxOrigID so this message
3455 // is used for reputation calculation during future deliveries.
3456 m.MailboxOrigID = m.MailboxDestinedID
3457 m.IsReject = false
3458 m.Seen = false
3459 }
3460 mbDst.Add(m.MailboxCounts())
3461 m.UID = uidnext
3462 m.ModSeq = modseq
3463 m.JunkFlagsForMailbox(mbDst, conf)
3464 uidnext++
3465 err := tx.Update(m)
3466 xcheckf(err, "updating moved message in database")
3467
3468 // Now that UID is unused, we can insert the old record again.
3469 err = tx.Insert(&om)
3470 xcheckf(err, "inserting record for expunge after moving message")
3471
3472 for _, kw := range m.Keywords {
3473 keywords[kw] = struct{}{}
3474 }
3475 }
3476
3477 // Ensure destination mailbox has keywords of the moved messages.
3478 var mbKwChanged bool
3479 mbDst.Keywords, mbKwChanged = store.MergeKeywords(mbDst.Keywords, maps.Keys(keywords))
3480 if mbKwChanged {
3481 changes = append(changes, mbDst.ChangeKeywords())
3482 }
3483
3484 err = tx.Update(&mbSrc)
3485 xcheckf(err, "updating source mailbox counts")
3486
3487 err = tx.Update(&mbDst)
3488 xcheckf(err, "updating destination mailbox for uids, keywords and counts")
3489
3490 err = c.account.RetrainMessages(context.TODO(), c.log, tx, msgs, false)
3491 xcheckf(err, "retraining messages after move")
3492
3493 // Prepare broadcast changes to other connections.
3494 changes = make([]store.Change, 0, 1+len(msgs)+2)
3495 changes = append(changes, store.ChangeRemoveUIDs{MailboxID: c.mailboxID, UIDs: uids, ModSeq: modseq})
3496 for _, m := range msgs {
3497 newUIDs = append(newUIDs, m.UID)
3498 changes = append(changes, m.ChangeAddUID())
3499 }
3500 changes = append(changes, mbSrc.ChangeCounts(), mbDst.ChangeCounts())
3501 })
3502
3503 c.broadcast(changes)
3504 })
3505
3506 // ../rfc/9051:4708 ../rfc/6851:254
3507 // ../rfc/9051:4713
3508 c.bwritelinef("* OK [COPYUID %d %s %s] moved", mbDst.UIDValidity, compactUIDSet(uids).String(), compactUIDSet(newUIDs).String())
3509 qresync := c.enabled[capQresync]
3510 var vanishedUIDs numSet
3511 for i := 0; i < len(uids); i++ {
3512 seq := c.xsequence(uids[i])
3513 c.sequenceRemove(seq, uids[i])
3514 if qresync {
3515 vanishedUIDs.append(uint32(uids[i]))
3516 } else {
3517 c.bwritelinef("* %d EXPUNGE", seq)
3518 }
3519 }
3520 if !vanishedUIDs.empty() {
3521 // VANISHED without EARLIER. ../rfc/7162:2004
3522 for _, s := range vanishedUIDs.Strings(4*1024 - 32) {
3523 c.bwritelinef("* VANISHED %s", s)
3524 }
3525 }
3526
3527 if c.enabled[capQresync] {
3528 // ../rfc/9051:6744 ../rfc/7162:1334
3529 c.writeresultf("%s OK [HIGHESTMODSEQ %d] move", tag, modseq.Client())
3530 } else {
3531 c.ok(tag, cmd)
3532 }
3533}
3534
3535// Store sets a full set of flags, or adds/removes specific flags.
3536//
3537// State: Selected
3538func (c *conn) cmdxStore(isUID bool, tag, cmd string, p *parser) {
3539 // Command: ../rfc/9051:4543 ../rfc/3501:3214
3540
3541 // Request syntax: ../rfc/9051:7076 ../rfc/3501:5052 ../rfc/4466:691 ../rfc/7162:2471
3542 p.xspace()
3543 nums := p.xnumSet()
3544 p.xspace()
3545 var unchangedSince *int64
3546 if p.take("(") {
3547 // ../rfc/7162:2471
3548 p.xtake("UNCHANGEDSINCE")
3549 p.xspace()
3550 v := p.xnumber64()
3551 unchangedSince = &v
3552 p.xtake(")")
3553 p.xspace()
3554 // UNCHANGEDSINCE is a CONDSTORE-enabling parameter ../rfc/7162:382
3555 c.xensureCondstore(nil)
3556 }
3557 var plus, minus bool
3558 if p.take("+") {
3559 plus = true
3560 } else if p.take("-") {
3561 minus = true
3562 }
3563 p.xtake("FLAGS")
3564 silent := p.take(".SILENT")
3565 p.xspace()
3566 var flagstrs []string
3567 if p.hasPrefix("(") {
3568 flagstrs = p.xflagList()
3569 } else {
3570 flagstrs = append(flagstrs, p.xflag())
3571 for p.space() {
3572 flagstrs = append(flagstrs, p.xflag())
3573 }
3574 }
3575 p.xempty()
3576
3577 if c.readonly {
3578 xuserErrorf("mailbox open in read-only mode")
3579 }
3580
3581 flags, keywords, err := store.ParseFlagsKeywords(flagstrs)
3582 if err != nil {
3583 xuserErrorf("parsing flags: %v", err)
3584 }
3585 var mask store.Flags
3586 if plus {
3587 mask, flags = flags, store.FlagsAll
3588 } else if minus {
3589 mask, flags = flags, store.Flags{}
3590 } else {
3591 mask = store.FlagsAll
3592 }
3593
3594 var mb, origmb store.Mailbox
3595 var updated []store.Message
3596 var changed []store.Message // ModSeq more recent than unchangedSince, will be in MODIFIED response code, and we will send untagged fetch responses so client is up to date.
3597 var modseq store.ModSeq // Assigned when needed.
3598 modified := map[int64]bool{}
3599
3600 c.account.WithWLock(func() {
3601 var mbKwChanged bool
3602 var changes []store.Change
3603
3604 c.xdbwrite(func(tx *bstore.Tx) {
3605 mb = c.xmailboxID(tx, c.mailboxID) // Validate.
3606 origmb = mb
3607
3608 uidargs := c.xnumSetCondition(isUID, nums)
3609
3610 if len(uidargs) == 0 {
3611 return
3612 }
3613
3614 // Ensure keywords are in mailbox.
3615 if !minus {
3616 mb.Keywords, mbKwChanged = store.MergeKeywords(mb.Keywords, keywords)
3617 if mbKwChanged {
3618 err := tx.Update(&mb)
3619 xcheckf(err, "updating mailbox with keywords")
3620 }
3621 }
3622
3623 q := bstore.QueryTx[store.Message](tx)
3624 q.FilterNonzero(store.Message{MailboxID: c.mailboxID})
3625 q.FilterEqual("UID", uidargs...)
3626 q.FilterEqual("Expunged", false)
3627 err := q.ForEach(func(m store.Message) error {
3628 // Client may specify a message multiple times, but we only process it once. ../rfc/7162:823
3629 if modified[m.ID] {
3630 return nil
3631 }
3632
3633 mc := m.MailboxCounts()
3634
3635 origFlags := m.Flags
3636 m.Flags = m.Flags.Set(mask, flags)
3637 oldKeywords := append([]string{}, m.Keywords...)
3638 if minus {
3639 m.Keywords, _ = store.RemoveKeywords(m.Keywords, keywords)
3640 } else if plus {
3641 m.Keywords, _ = store.MergeKeywords(m.Keywords, keywords)
3642 } else {
3643 m.Keywords = keywords
3644 }
3645
3646 keywordsChanged := func() bool {
3647 sort.Strings(oldKeywords)
3648 n := append([]string{}, m.Keywords...)
3649 sort.Strings(n)
3650 return !slices.Equal(oldKeywords, n)
3651 }
3652
3653 // If the message has a more recent modseq than the check requires, we won't modify
3654 // it and report in the final command response.
3655 // ../rfc/7162:555
3656 //
3657 // unchangedSince 0 always fails the check, we don't turn it into 1 like with our
3658 // internal modseqs. RFC implies that is not required for non-system flags, but we
3659 // don't have per-flag modseq and this seems reasonable. ../rfc/7162:640
3660 if unchangedSince != nil && m.ModSeq.Client() > *unchangedSince {
3661 changed = append(changed, m)
3662 return nil
3663 }
3664
3665 // Note: we don't perform the optimization described in ../rfc/7162:1258
3666 // It requires that we keep track of the flags we think the client knows (but only
3667 // on this connection). We don't track that. It also isn't clear why this is
3668 // allowed because it is skipping the condstore conditional check, and the new
3669 // combination of flags could be unintended.
3670
3671 // We do not assign a new modseq if nothing actually changed. ../rfc/7162:1246 ../rfc/7162:312
3672 if origFlags == m.Flags && !keywordsChanged() {
3673 // Note: since we didn't update the modseq, we are not adding m.ID to "modified",
3674 // it would skip the modseq check above. We still add m to list of updated, so we
3675 // send an untagged fetch response. But we don't broadcast it.
3676 updated = append(updated, m)
3677 return nil
3678 }
3679
3680 mb.Sub(mc)
3681 mb.Add(m.MailboxCounts())
3682
3683 // Assign new modseq for first actual change.
3684 if modseq == 0 {
3685 var err error
3686 modseq, err = c.account.NextModSeq(tx)
3687 xcheckf(err, "next modseq")
3688 }
3689 m.ModSeq = modseq
3690 modified[m.ID] = true
3691 updated = append(updated, m)
3692
3693 changes = append(changes, m.ChangeFlags(origFlags))
3694
3695 return tx.Update(&m)
3696 })
3697 xcheckf(err, "storing flags in messages")
3698
3699 if mb.MailboxCounts != origmb.MailboxCounts {
3700 err := tx.Update(&mb)
3701 xcheckf(err, "updating mailbox counts")
3702
3703 changes = append(changes, mb.ChangeCounts())
3704 }
3705 if mbKwChanged {
3706 changes = append(changes, mb.ChangeKeywords())
3707 }
3708
3709 err = c.account.RetrainMessages(context.TODO(), c.log, tx, updated, false)
3710 xcheckf(err, "training messages")
3711 })
3712
3713 c.broadcast(changes)
3714 })
3715
3716 // In the RFC, the section about STORE/UID STORE says we must return MODSEQ when
3717 // UNCHANGEDSINCE was specified. It does not specify it in case UNCHANGEDSINCE
3718 // isn't specified. For that case it does say MODSEQ is needed in unsolicited
3719 // untagged fetch responses. Implying that solicited untagged fetch responses
3720 // should not include MODSEQ (why else mention unsolicited explicitly?). But, in
3721 // the introduction to CONDSTORE it does explicitly specify MODSEQ should be
3722 // included in untagged fetch responses at all times with CONDSTORE-enabled
3723 // connections. It would have been better if the command behaviour was specified in
3724 // the command section, not the introduction to the extension.
3725 // ../rfc/7162:388 ../rfc/7162:852
3726 // ../rfc/7162:549
3727 if !silent || c.enabled[capCondstore] {
3728 for _, m := range updated {
3729 var flags string
3730 if !silent {
3731 flags = fmt.Sprintf(" FLAGS %s", flaglist(m.Flags, m.Keywords).pack(c))
3732 }
3733 var modseqStr string
3734 if c.enabled[capCondstore] {
3735 modseqStr = fmt.Sprintf(" MODSEQ (%d)", m.ModSeq.Client())
3736 }
3737 // ../rfc/9051:6749 ../rfc/3501:4869 ../rfc/7162:2490
3738 c.bwritelinef("* %d FETCH (UID %d%s%s)", c.xsequence(m.UID), m.UID, flags, modseqStr)
3739 }
3740 }
3741
3742 // We don't explicitly send flags for failed updated with silent set. The regular
3743 // notification will get the flags to the client.
3744 // ../rfc/7162:630 ../rfc/3501:3233
3745
3746 if len(changed) == 0 {
3747 c.ok(tag, cmd)
3748 return
3749 }
3750
3751 // Write unsolicited untagged fetch responses for messages that didn't pass the
3752 // unchangedsince check. ../rfc/7162:679
3753 // Also gather UIDs or sequences for the MODIFIED response below. ../rfc/7162:571
3754 var mnums []store.UID
3755 for _, m := range changed {
3756 c.bwritelinef("* %d FETCH (UID %d FLAGS %s MODSEQ (%d))", c.xsequence(m.UID), m.UID, flaglist(m.Flags, m.Keywords).pack(c), m.ModSeq.Client())
3757 if isUID {
3758 mnums = append(mnums, m.UID)
3759 } else {
3760 mnums = append(mnums, store.UID(c.xsequence(m.UID)))
3761 }
3762 }
3763
3764 sort.Slice(mnums, func(i, j int) bool {
3765 return mnums[i] < mnums[j]
3766 })
3767 set := compactUIDSet(mnums)
3768 // ../rfc/7162:2506
3769 c.writeresultf("%s OK [MODIFIED %s] conditional store did not modify all", tag, set.String())
3770}
3771