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 "log/slog"
48 "math"
49 "net"
50 "os"
51 "path"
52 "path/filepath"
53 "regexp"
54 "runtime/debug"
55 "slices"
56 "sort"
57 "strings"
58 "sync"
59 "time"
60
61 "golang.org/x/exp/maps"
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
1499 // If authentication fails due to missing derived secrets, we don't hold it against
1500 // the connection. There is no way to indicate server support for an authentication
1501 // mechanism, but that a mechanism won't work for an account.
1502 var missingDerivedSecrets bool
1503
1504 c.authFailed++ // Compensated on success.
1505 defer func() {
1506 if missingDerivedSecrets {
1507 c.authFailed--
1508 }
1509 // On the 3rd failed authentication, start responding slowly. Successful auth will
1510 // cause fast responses again.
1511 if c.authFailed >= 3 {
1512 c.setSlow(true)
1513 }
1514 }()
1515
1516 var authVariant string
1517 authResult := "error"
1518 defer func() {
1519 metrics.AuthenticationInc("imap", authVariant, authResult)
1520 if authResult == "ok" {
1521 mox.LimiterFailedAuth.Reset(c.remoteIP, time.Now())
1522 } else if !missingDerivedSecrets {
1523 mox.LimiterFailedAuth.Add(c.remoteIP, time.Now(), 1)
1524 }
1525 }()
1526
1527 // Request syntax: ../rfc/9051:6341 ../rfc/3501:4561
1528 p.xspace()
1529 authType := p.xatom()
1530
1531 xreadInitial := func() []byte {
1532 var line string
1533 if p.empty() {
1534 c.writelinef("+ ")
1535 line = c.readline(false)
1536 } else {
1537 // ../rfc/9051:1407 ../rfc/4959:84
1538 p.xspace()
1539 line = p.remainder()
1540 if line == "=" {
1541 // ../rfc/9051:1450
1542 line = "" // Base64 decode will result in empty buffer.
1543 }
1544 }
1545 // ../rfc/9051:1442 ../rfc/3501:1553
1546 if line == "*" {
1547 authResult = "aborted"
1548 xsyntaxErrorf("authenticate aborted by client")
1549 }
1550 buf, err := base64.StdEncoding.DecodeString(line)
1551 if err != nil {
1552 xsyntaxErrorf("parsing base64: %v", err)
1553 }
1554 return buf
1555 }
1556
1557 xreadContinuation := func() []byte {
1558 line := c.readline(false)
1559 if line == "*" {
1560 authResult = "aborted"
1561 xsyntaxErrorf("authenticate aborted by client")
1562 }
1563 buf, err := base64.StdEncoding.DecodeString(line)
1564 if err != nil {
1565 xsyntaxErrorf("parsing base64: %v", err)
1566 }
1567 return buf
1568 }
1569
1570 switch strings.ToUpper(authType) {
1571 case "PLAIN":
1572 authVariant = "plain"
1573
1574 if !c.noRequireSTARTTLS && !c.tls {
1575 // ../rfc/9051:5194
1576 xusercodeErrorf("PRIVACYREQUIRED", "tls required for login")
1577 }
1578
1579 // Plain text passwords, mark as traceauth.
1580 defer c.xtrace(mlog.LevelTraceauth)()
1581 buf := xreadInitial()
1582 c.xtrace(mlog.LevelTrace) // Restore.
1583 plain := bytes.Split(buf, []byte{0})
1584 if len(plain) != 3 {
1585 xsyntaxErrorf("bad plain auth data, expected 3 nul-separated tokens, got %d tokens", len(plain))
1586 }
1587 authz := string(plain[0])
1588 authc := string(plain[1])
1589 password := string(plain[2])
1590
1591 if authz != "" && authz != authc {
1592 xusercodeErrorf("AUTHORIZATIONFAILED", "cannot assume role")
1593 }
1594
1595 acc, err := store.OpenEmailAuth(c.log, authc, password)
1596 if err != nil {
1597 if errors.Is(err, store.ErrUnknownCredentials) {
1598 authResult = "badcreds"
1599 c.log.Info("authentication failed", slog.String("username", authc))
1600 xusercodeErrorf("AUTHENTICATIONFAILED", "bad credentials")
1601 }
1602 xusercodeErrorf("", "error")
1603 }
1604 c.account = acc
1605 c.username = authc
1606
1607 case "CRAM-MD5":
1608 authVariant = strings.ToLower(authType)
1609
1610 // ../rfc/9051:1462
1611 p.xempty()
1612
1613 // ../rfc/2195:82
1614 chal := fmt.Sprintf("<%d.%d@%s>", uint64(mox.CryptoRandInt()), time.Now().UnixNano(), mox.Conf.Static.HostnameDomain.ASCII)
1615 c.writelinef("+ %s", base64.StdEncoding.EncodeToString([]byte(chal)))
1616
1617 resp := xreadContinuation()
1618 t := strings.Split(string(resp), " ")
1619 if len(t) != 2 || len(t[1]) != 2*md5.Size {
1620 xsyntaxErrorf("malformed cram-md5 response")
1621 }
1622 addr := t[0]
1623 c.log.Debug("cram-md5 auth", slog.String("address", addr))
1624 acc, _, err := store.OpenEmail(c.log, addr)
1625 if err != nil {
1626 if errors.Is(err, store.ErrUnknownCredentials) {
1627 c.log.Info("failed authentication attempt", slog.String("username", addr), slog.Any("remote", c.remoteIP))
1628 xusercodeErrorf("AUTHENTICATIONFAILED", "bad credentials")
1629 }
1630 xserverErrorf("looking up address: %v", err)
1631 }
1632 defer func() {
1633 if acc != nil {
1634 err := acc.Close()
1635 c.xsanity(err, "close account")
1636 }
1637 }()
1638 var ipadhash, opadhash hash.Hash
1639 acc.WithRLock(func() {
1640 err := acc.DB.Read(context.TODO(), func(tx *bstore.Tx) error {
1641 password, err := bstore.QueryTx[store.Password](tx).Get()
1642 if err == bstore.ErrAbsent {
1643 c.log.Info("failed authentication attempt", slog.String("username", addr), slog.Any("remote", c.remoteIP))
1644 xusercodeErrorf("AUTHENTICATIONFAILED", "bad credentials")
1645 }
1646 if err != nil {
1647 return err
1648 }
1649
1650 ipadhash = password.CRAMMD5.Ipad
1651 opadhash = password.CRAMMD5.Opad
1652 return nil
1653 })
1654 xcheckf(err, "tx read")
1655 })
1656 if ipadhash == nil || opadhash == nil {
1657 c.log.Info("cram-md5 auth attempt without derived secrets set, save password again to store secrets", slog.String("username", addr))
1658 c.log.Info("failed authentication attempt", slog.String("username", addr), slog.Any("remote", c.remoteIP))
1659 missingDerivedSecrets = true
1660 xusercodeErrorf("AUTHENTICATIONFAILED", "bad credentials")
1661 }
1662
1663 // ../rfc/2195:138 ../rfc/2104:142
1664 ipadhash.Write([]byte(chal))
1665 opadhash.Write(ipadhash.Sum(nil))
1666 digest := fmt.Sprintf("%x", opadhash.Sum(nil))
1667 if digest != t[1] {
1668 c.log.Info("failed authentication attempt", slog.String("username", addr), slog.Any("remote", c.remoteIP))
1669 xusercodeErrorf("AUTHENTICATIONFAILED", "bad credentials")
1670 }
1671
1672 c.account = acc
1673 acc = nil // Cancel cleanup.
1674 c.username = addr
1675
1676 case "SCRAM-SHA-256-PLUS", "SCRAM-SHA-256", "SCRAM-SHA-1-PLUS", "SCRAM-SHA-1":
1677 // 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?
1678 // todo: use single implementation between ../imapserver/server.go and ../smtpserver/server.go
1679
1680 // No plaintext credentials, we can log these normally.
1681
1682 authVariant = strings.ToLower(authType)
1683 var h func() hash.Hash
1684 switch authVariant {
1685 case "scram-sha-1", "scram-sha-1-plus":
1686 h = sha1.New
1687 case "scram-sha-256", "scram-sha-256-plus":
1688 h = sha256.New
1689 default:
1690 xserverErrorf("missing case for scram variant")
1691 }
1692
1693 var cs *tls.ConnectionState
1694 requireChannelBinding := strings.HasSuffix(authVariant, "-plus")
1695 if requireChannelBinding && !c.tls {
1696 xuserErrorf("cannot use plus variant with tls channel binding without tls")
1697 }
1698 if c.tls {
1699 xcs := c.conn.(*tls.Conn).ConnectionState()
1700 cs = &xcs
1701 }
1702 c0 := xreadInitial()
1703 ss, err := scram.NewServer(h, c0, cs, requireChannelBinding)
1704 if err != nil {
1705 xsyntaxErrorf("starting scram: %s", err)
1706 }
1707 c.log.Debug("scram auth", slog.String("authentication", ss.Authentication))
1708 acc, _, err := store.OpenEmail(c.log, ss.Authentication)
1709 if err != nil {
1710 // todo: we could continue scram with a generated salt, deterministically generated
1711 // from the username. that way we don't have to store anything but attackers cannot
1712 // learn if an account exists. same for absent scram saltedpassword below.
1713 xuserErrorf("scram not possible")
1714 }
1715 defer func() {
1716 if acc != nil {
1717 err := acc.Close()
1718 c.xsanity(err, "close account")
1719 }
1720 }()
1721 if ss.Authorization != "" && ss.Authorization != ss.Authentication {
1722 xuserErrorf("authentication with authorization for different user not supported")
1723 }
1724 var xscram store.SCRAM
1725 acc.WithRLock(func() {
1726 err := acc.DB.Read(context.TODO(), func(tx *bstore.Tx) error {
1727 password, err := bstore.QueryTx[store.Password](tx).Get()
1728 if err == bstore.ErrAbsent {
1729 c.log.Info("failed authentication attempt", slog.String("username", ss.Authentication), slog.Any("remote", c.remoteIP))
1730 xusercodeErrorf("AUTHENTICATIONFAILED", "bad credentials")
1731 }
1732 xcheckf(err, "fetching credentials")
1733 switch authVariant {
1734 case "scram-sha-1", "scram-sha-1-plus":
1735 xscram = password.SCRAMSHA1
1736 case "scram-sha-256", "scram-sha-256-plus":
1737 xscram = password.SCRAMSHA256
1738 default:
1739 xserverErrorf("missing case for scram credentials")
1740 }
1741 if len(xscram.Salt) == 0 || xscram.Iterations == 0 || len(xscram.SaltedPassword) == 0 {
1742 missingDerivedSecrets = true
1743 c.log.Info("scram auth attempt without derived secrets set, save password again to store secrets", slog.String("address", ss.Authentication))
1744 xuserErrorf("scram not possible")
1745 }
1746 return nil
1747 })
1748 xcheckf(err, "read tx")
1749 })
1750 s1, err := ss.ServerFirst(xscram.Iterations, xscram.Salt)
1751 xcheckf(err, "scram first server step")
1752 c.writelinef("+ %s", base64.StdEncoding.EncodeToString([]byte(s1)))
1753 c2 := xreadContinuation()
1754 s3, err := ss.Finish(c2, xscram.SaltedPassword)
1755 if len(s3) > 0 {
1756 c.writelinef("+ %s", base64.StdEncoding.EncodeToString([]byte(s3)))
1757 }
1758 if err != nil {
1759 c.readline(false) // Should be "*" for cancellation.
1760 if errors.Is(err, scram.ErrInvalidProof) {
1761 authResult = "badcreds"
1762 c.log.Info("failed authentication attempt", slog.String("username", ss.Authentication), slog.Any("remote", c.remoteIP))
1763 xusercodeErrorf("AUTHENTICATIONFAILED", "bad credentials")
1764 }
1765 xuserErrorf("server final: %w", err)
1766 }
1767
1768 // Client must still respond, but there is nothing to say. See ../rfc/9051:6221
1769 // The message should be empty. todo: should we require it is empty?
1770 xreadContinuation()
1771
1772 c.account = acc
1773 acc = nil // Cancel cleanup.
1774 c.username = ss.Authentication
1775
1776 default:
1777 xuserErrorf("method not supported")
1778 }
1779
1780 c.setSlow(false)
1781 authResult = "ok"
1782 c.authFailed = 0
1783 c.comm = store.RegisterComm(c.account)
1784 c.state = stateAuthenticated
1785 c.writeresultf("%s OK [CAPABILITY %s] authenticate done", tag, c.capabilities())
1786}
1787
1788// Login logs in with username and password.
1789//
1790// Status: Not authenticated.
1791func (c *conn) cmdLogin(tag, cmd string, p *parser) {
1792 // Command: ../rfc/9051:1597 ../rfc/3501:1663
1793
1794 authResult := "error"
1795 defer func() {
1796 metrics.AuthenticationInc("imap", "login", authResult)
1797 }()
1798
1799 // 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).
1800
1801 // Request syntax: ../rfc/9051:6667 ../rfc/3501:4804
1802 p.xspace()
1803 userid := p.xastring()
1804 p.xspace()
1805 password := p.xastring()
1806 p.xempty()
1807
1808 if !c.noRequireSTARTTLS && !c.tls {
1809 // ../rfc/9051:5194
1810 xusercodeErrorf("PRIVACYREQUIRED", "tls required for login")
1811 }
1812
1813 // For many failed auth attempts, slow down verification attempts.
1814 if c.authFailed > 3 && authFailDelay > 0 {
1815 mox.Sleep(mox.Context, time.Duration(c.authFailed-3)*authFailDelay)
1816 }
1817 c.authFailed++ // Compensated on success.
1818 defer func() {
1819 // On the 3rd failed authentication, start responding slowly. Successful auth will
1820 // cause fast responses again.
1821 if c.authFailed >= 3 {
1822 c.setSlow(true)
1823 }
1824 }()
1825
1826 acc, err := store.OpenEmailAuth(c.log, userid, password)
1827 if err != nil {
1828 authResult = "badcreds"
1829 var code string
1830 if errors.Is(err, store.ErrUnknownCredentials) {
1831 code = "AUTHENTICATIONFAILED"
1832 c.log.Info("failed authentication attempt", slog.String("username", userid), slog.Any("remote", c.remoteIP))
1833 }
1834 xusercodeErrorf(code, "login failed")
1835 }
1836 c.account = acc
1837 c.username = userid
1838 c.authFailed = 0
1839 c.setSlow(false)
1840 c.comm = store.RegisterComm(acc)
1841 c.state = stateAuthenticated
1842 authResult = "ok"
1843 c.writeresultf("%s OK [CAPABILITY %s] login done", tag, c.capabilities())
1844}
1845
1846// Enable explicitly opts in to an extension. A server can typically send new kinds
1847// of responses to a client. Most extensions do not require an ENABLE because a
1848// client implicitly opts in to new response syntax by making a requests that uses
1849// new optional extension request syntax.
1850//
1851// State: Authenticated and selected.
1852func (c *conn) cmdEnable(tag, cmd string, p *parser) {
1853 // Command: ../rfc/9051:1652 ../rfc/5161:80
1854 // Examples: ../rfc/9051:1728 ../rfc/5161:147
1855
1856 // Request syntax: ../rfc/9051:6518 ../rfc/5161:207
1857 p.xspace()
1858 caps := []string{p.xatom()}
1859 for !p.empty() {
1860 p.xspace()
1861 caps = append(caps, p.xatom())
1862 }
1863
1864 // Clients should only send capabilities that need enabling.
1865 // We should only echo that we recognize as needing enabling.
1866 var enabled string
1867 var qresync bool
1868 for _, s := range caps {
1869 cap := capability(strings.ToUpper(s))
1870 switch cap {
1871 case capIMAP4rev2,
1872 capUTF8Accept,
1873 capCondstore: // ../rfc/7162:384
1874 c.enabled[cap] = true
1875 enabled += " " + s
1876 case capQresync:
1877 c.enabled[cap] = true
1878 enabled += " " + s
1879 qresync = true
1880 }
1881 }
1882 // QRESYNC enabled CONDSTORE too ../rfc/7162:1391
1883 if qresync && !c.enabled[capCondstore] {
1884 c.xensureCondstore(nil)
1885 enabled += " CONDSTORE"
1886 }
1887
1888 // Response syntax: ../rfc/9051:6520 ../rfc/5161:211
1889 c.bwritelinef("* ENABLED%s", enabled)
1890 c.ok(tag, cmd)
1891}
1892
1893// The CONDSTORE extension can be enabled in many different ways. ../rfc/7162:368
1894// If a mailbox is selected, an untagged OK with HIGHESTMODSEQ is written to the
1895// client. If tx is non-nil, it is used to read the HIGHESTMODSEQ from the
1896// database. Otherwise a new read-only transaction is created.
1897func (c *conn) xensureCondstore(tx *bstore.Tx) {
1898 if !c.enabled[capCondstore] {
1899 c.enabled[capCondstore] = true
1900 // todo spec: can we send an untagged enabled response?
1901 // ../rfc/7162:603
1902 if c.mailboxID <= 0 {
1903 return
1904 }
1905 var modseq store.ModSeq
1906 if tx != nil {
1907 modseq = c.xhighestModSeq(tx, c.mailboxID)
1908 } else {
1909 c.xdbread(func(tx *bstore.Tx) {
1910 modseq = c.xhighestModSeq(tx, c.mailboxID)
1911 })
1912 }
1913 c.bwritelinef("* OK [HIGHESTMODSEQ %d] after condstore-enabling command", modseq.Client())
1914 }
1915}
1916
1917// State: Authenticated and selected.
1918func (c *conn) cmdSelect(tag, cmd string, p *parser) {
1919 c.cmdSelectExamine(true, tag, cmd, p)
1920}
1921
1922// State: Authenticated and selected.
1923func (c *conn) cmdExamine(tag, cmd string, p *parser) {
1924 c.cmdSelectExamine(false, tag, cmd, p)
1925}
1926
1927// Select and examine are almost the same commands. Select just opens a mailbox for
1928// read/write and examine opens a mailbox readonly.
1929//
1930// State: Authenticated and selected.
1931func (c *conn) cmdSelectExamine(isselect bool, tag, cmd string, p *parser) {
1932 // Select command: ../rfc/9051:1754 ../rfc/3501:1743 ../rfc/7162:1146 ../rfc/7162:1432
1933 // Examine command: ../rfc/9051:1868 ../rfc/3501:1855
1934 // Select examples: ../rfc/9051:1831 ../rfc/3501:1826 ../rfc/7162:1159 ../rfc/7162:1479
1935
1936 // Select request syntax: ../rfc/9051:7005 ../rfc/3501:4996 ../rfc/4466:652 ../rfc/7162:2559 ../rfc/7162:2598
1937 // Examine request syntax: ../rfc/9051:6551 ../rfc/3501:4746
1938 p.xspace()
1939 name := p.xmailbox()
1940
1941 var qruidvalidity uint32
1942 var qrmodseq int64 // QRESYNC required parameters.
1943 var qrknownUIDs, qrknownSeqSet, qrknownUIDSet *numSet // QRESYNC optional parameters.
1944 if p.space() {
1945 seen := map[string]bool{}
1946 p.xtake("(")
1947 for len(seen) == 0 || !p.take(")") {
1948 w := p.xtakelist("CONDSTORE", "QRESYNC")
1949 if seen[w] {
1950 xsyntaxErrorf("duplicate select parameter %s", w)
1951 }
1952 seen[w] = true
1953
1954 switch w {
1955 case "CONDSTORE":
1956 // ../rfc/7162:363
1957 c.xensureCondstore(nil) // ../rfc/7162:373
1958 case "QRESYNC":
1959 // ../rfc/7162:2598
1960 // Note: unlike with CONDSTORE, there are no QRESYNC-related commands/parameters
1961 // that enable capabilities.
1962 if !c.enabled[capQresync] {
1963 // ../rfc/7162:1446
1964 xsyntaxErrorf("QRESYNC must first be enabled")
1965 }
1966 p.xspace()
1967 p.xtake("(")
1968 qruidvalidity = p.xnznumber() // ../rfc/7162:2606
1969 p.xspace()
1970 qrmodseq = p.xnznumber64()
1971 if p.take(" ") {
1972 seqMatchData := p.take("(")
1973 if !seqMatchData {
1974 ss := p.xnumSet0(false, false) // ../rfc/7162:2608
1975 qrknownUIDs = &ss
1976 seqMatchData = p.take(" (")
1977 }
1978 if seqMatchData {
1979 ss0 := p.xnumSet0(false, false)
1980 qrknownSeqSet = &ss0
1981 p.xspace()
1982 ss1 := p.xnumSet0(false, false)
1983 qrknownUIDSet = &ss1
1984 p.xtake(")")
1985 }
1986 }
1987 p.xtake(")")
1988 default:
1989 panic("missing case for select param " + w)
1990 }
1991 }
1992 }
1993 p.xempty()
1994
1995 // Deselect before attempting the new select. This means we will deselect when an
1996 // error occurs during select.
1997 // ../rfc/9051:1809
1998 if c.state == stateSelected {
1999 // ../rfc/9051:1812 ../rfc/7162:2111
2000 c.bwritelinef("* OK [CLOSED] x")
2001 c.unselect()
2002 }
2003
2004 name = xcheckmailboxname(name, true)
2005
2006 var highestModSeq store.ModSeq
2007 var highDeletedModSeq store.ModSeq
2008 var firstUnseen msgseq = 0
2009 var mb store.Mailbox
2010 c.account.WithRLock(func() {
2011 c.xdbread(func(tx *bstore.Tx) {
2012 mb = c.xmailbox(tx, name, "")
2013
2014 q := bstore.QueryTx[store.Message](tx)
2015 q.FilterNonzero(store.Message{MailboxID: mb.ID})
2016 q.FilterEqual("Expunged", false)
2017 q.SortAsc("UID")
2018 c.uids = []store.UID{}
2019 var seq msgseq = 1
2020 err := q.ForEach(func(m store.Message) error {
2021 c.uids = append(c.uids, m.UID)
2022 if firstUnseen == 0 && !m.Seen {
2023 firstUnseen = seq
2024 }
2025 seq++
2026 return nil
2027 })
2028 if sanityChecks {
2029 checkUIDs(c.uids)
2030 }
2031 xcheckf(err, "fetching uids")
2032
2033 // Condstore extension, find the highest modseq.
2034 if c.enabled[capCondstore] {
2035 highestModSeq = c.xhighestModSeq(tx, mb.ID)
2036 }
2037 // For QRESYNC, we need to know the highest modset of deleted expunged records to
2038 // maintain synchronization.
2039 if c.enabled[capQresync] {
2040 highDeletedModSeq, err = c.account.HighestDeletedModSeq(tx)
2041 xcheckf(err, "getting highest deleted modseq")
2042 }
2043 })
2044 })
2045 c.applyChanges(c.comm.Get(), true)
2046
2047 var flags string
2048 if len(mb.Keywords) > 0 {
2049 flags = " " + strings.Join(mb.Keywords, " ")
2050 }
2051 c.bwritelinef(`* FLAGS (\Seen \Answered \Flagged \Deleted \Draft $Forwarded $Junk $NotJunk $Phishing $MDNSent%s)`, flags)
2052 c.bwritelinef(`* OK [PERMANENTFLAGS (\Seen \Answered \Flagged \Deleted \Draft $Forwarded $Junk $NotJunk $Phishing $MDNSent \*)] x`)
2053 if !c.enabled[capIMAP4rev2] {
2054 c.bwritelinef(`* 0 RECENT`)
2055 }
2056 c.bwritelinef(`* %d EXISTS`, len(c.uids))
2057 if !c.enabled[capIMAP4rev2] && firstUnseen > 0 {
2058 // ../rfc/9051:8051 ../rfc/3501:1774
2059 c.bwritelinef(`* OK [UNSEEN %d] x`, firstUnseen)
2060 }
2061 c.bwritelinef(`* OK [UIDVALIDITY %d] x`, mb.UIDValidity)
2062 c.bwritelinef(`* OK [UIDNEXT %d] x`, mb.UIDNext)
2063 c.bwritelinef(`* LIST () "/" %s`, astring(c.encodeMailbox(mb.Name)).pack(c))
2064 if c.enabled[capCondstore] {
2065 // ../rfc/7162:417
2066 // ../rfc/7162-eid5055 ../rfc/7162:484 ../rfc/7162:1167
2067 c.bwritelinef(`* OK [HIGHESTMODSEQ %d] x`, highestModSeq.Client())
2068 }
2069
2070 // If QRESYNC uidvalidity matches, we send any changes. ../rfc/7162:1509
2071 if qruidvalidity == mb.UIDValidity {
2072 // We send the vanished UIDs at the end, so we can easily combine the modseq
2073 // changes and vanished UIDs that result from that, with the vanished UIDs from the
2074 // case where we don't store enough history.
2075 vanishedUIDs := map[store.UID]struct{}{}
2076
2077 var preVanished store.UID
2078 var oldClientUID store.UID
2079 // If samples of known msgseq and uid pairs are given (they must be in order), we
2080 // use them to determine the earliest UID for which we send VANISHED responses.
2081 // ../rfc/7162:1579
2082 if qrknownSeqSet != nil {
2083 if !qrknownSeqSet.isBasicIncreasing() {
2084 xuserErrorf("QRESYNC known message sequence set must be numeric and strictly increasing")
2085 }
2086 if !qrknownUIDSet.isBasicIncreasing() {
2087 xuserErrorf("QRESYNC known uid set must be numeric and strictly increasing")
2088 }
2089 seqiter := qrknownSeqSet.newIter()
2090 uiditer := qrknownUIDSet.newIter()
2091 for {
2092 msgseq, ok0 := seqiter.Next()
2093 uid, ok1 := uiditer.Next()
2094 if !ok0 && !ok1 {
2095 break
2096 } else if !ok0 || !ok1 {
2097 xsyntaxErrorf("invalid combination of known sequence set and uid set, must be of equal length")
2098 }
2099 i := int(msgseq - 1)
2100 if i < 0 || i >= len(c.uids) || c.uids[i] != store.UID(uid) {
2101 if uidSearch(c.uids, store.UID(uid)) <= 0 {
2102 // We will check this old client UID for consistency below.
2103 oldClientUID = store.UID(uid)
2104 }
2105 break
2106 }
2107 preVanished = store.UID(uid + 1)
2108 }
2109 }
2110
2111 // We gather vanished UIDs and report them at the end. This seems OK because we
2112 // already sent HIGHESTMODSEQ, and a client should know not to commit that value
2113 // until after it has seen the tagged OK of this command. The RFC has a remark
2114 // about ordering of some untagged responses, it's not immediately clear what it
2115 // means, but given the examples appears to allude to servers that decide to not
2116 // send expunge/vanished before the tagged OK.
2117 // ../rfc/7162:1340
2118
2119 // We are reading without account lock. Similar to when we process FETCH/SEARCH
2120 // requests. We don't have to reverify existence of the mailbox, so we don't
2121 // rlock, even briefly.
2122 c.xdbread(func(tx *bstore.Tx) {
2123 if oldClientUID > 0 {
2124 // The client sent a UID that is now removed. This is typically fine. But we check
2125 // that it is consistent with the modseq the client sent. If the UID already didn't
2126 // exist at that modseq, the client may be missing some information.
2127 q := bstore.QueryTx[store.Message](tx)
2128 q.FilterNonzero(store.Message{MailboxID: mb.ID, UID: oldClientUID})
2129 m, err := q.Get()
2130 if err == nil {
2131 // If client claims to be up to date up to and including qrmodseq, and the message
2132 // was deleted at or before that time, we send changes from just before that
2133 // modseq, and we send vanished for all UIDs.
2134 if m.Expunged && qrmodseq >= m.ModSeq.Client() {
2135 qrmodseq = m.ModSeq.Client() - 1
2136 preVanished = 0
2137 qrknownUIDs = nil
2138 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.")
2139 }
2140 } else if err != bstore.ErrAbsent {
2141 xcheckf(err, "checking old client uid")
2142 }
2143 }
2144
2145 q := bstore.QueryTx[store.Message](tx)
2146 q.FilterNonzero(store.Message{MailboxID: mb.ID})
2147 // Note: we don't filter by Expunged.
2148 q.FilterGreater("ModSeq", store.ModSeqFromClient(qrmodseq))
2149 q.FilterLessEqual("ModSeq", highestModSeq)
2150 q.SortAsc("ModSeq")
2151 err := q.ForEach(func(m store.Message) error {
2152 if m.Expunged && m.UID < preVanished {
2153 return nil
2154 }
2155 // If known UIDs was specified, we only report about those UIDs. ../rfc/7162:1523
2156 if qrknownUIDs != nil && !qrknownUIDs.contains(uint32(m.UID)) {
2157 return nil
2158 }
2159 if m.Expunged {
2160 vanishedUIDs[m.UID] = struct{}{}
2161 return nil
2162 }
2163 msgseq := c.sequence(m.UID)
2164 if msgseq > 0 {
2165 c.bwritelinef("* %d FETCH (UID %d FLAGS %s MODSEQ (%d))", msgseq, m.UID, flaglist(m.Flags, m.Keywords).pack(c), m.ModSeq.Client())
2166 }
2167 return nil
2168 })
2169 xcheckf(err, "listing changed messages")
2170 })
2171
2172 // Add UIDs from client's known UID set to vanished list if we don't have enough history.
2173 if qrmodseq < highDeletedModSeq.Client() {
2174 // If no known uid set was in the request, we substitute 1:max or the empty set.
2175 // ../rfc/7162:1524
2176 if qrknownUIDs == nil {
2177 if len(c.uids) > 0 {
2178 qrknownUIDs = &numSet{ranges: []numRange{{first: setNumber{number: 1}, last: &setNumber{number: uint32(c.uids[len(c.uids)-1])}}}}
2179 } else {
2180 qrknownUIDs = &numSet{}
2181 }
2182 }
2183
2184 iter := qrknownUIDs.newIter()
2185 for {
2186 v, ok := iter.Next()
2187 if !ok {
2188 break
2189 }
2190 if c.sequence(store.UID(v)) <= 0 {
2191 vanishedUIDs[store.UID(v)] = struct{}{}
2192 }
2193 }
2194 }
2195
2196 // Now that we have all vanished UIDs, send them over compactly.
2197 if len(vanishedUIDs) > 0 {
2198 l := maps.Keys(vanishedUIDs)
2199 sort.Slice(l, func(i, j int) bool {
2200 return l[i] < l[j]
2201 })
2202 // ../rfc/7162:1985
2203 for _, s := range compactUIDSet(l).Strings(4*1024 - 32) {
2204 c.bwritelinef("* VANISHED (EARLIER) %s", s)
2205 }
2206 }
2207 }
2208
2209 if isselect {
2210 c.bwriteresultf("%s OK [READ-WRITE] x", tag)
2211 c.readonly = false
2212 } else {
2213 c.bwriteresultf("%s OK [READ-ONLY] x", tag)
2214 c.readonly = true
2215 }
2216 c.mailboxID = mb.ID
2217 c.state = stateSelected
2218 c.searchResult = nil
2219 c.xflush()
2220}
2221
2222// Create makes a new mailbox, and its parents too if absent.
2223//
2224// State: Authenticated and selected.
2225func (c *conn) cmdCreate(tag, cmd string, p *parser) {
2226 // Command: ../rfc/9051:1900 ../rfc/3501:1888
2227 // Examples: ../rfc/9051:1951 ../rfc/6154:411 ../rfc/4466:212 ../rfc/3501:1933
2228
2229 // Request syntax: ../rfc/9051:6484 ../rfc/6154:468 ../rfc/4466:500 ../rfc/3501:4687
2230 p.xspace()
2231 name := p.xmailbox()
2232 // todo: support CREATE-SPECIAL-USE ../rfc/6154:296
2233 p.xempty()
2234
2235 origName := name
2236 name = strings.TrimRight(name, "/") // ../rfc/9051:1930
2237 name = xcheckmailboxname(name, false)
2238
2239 var changes []store.Change
2240 var created []string // Created mailbox names.
2241
2242 c.account.WithWLock(func() {
2243 c.xdbwrite(func(tx *bstore.Tx) {
2244 var exists bool
2245 var err error
2246 changes, created, exists, err = c.account.MailboxCreate(tx, name)
2247 if exists {
2248 // ../rfc/9051:1914
2249 xuserErrorf("mailbox already exists")
2250 }
2251 xcheckf(err, "creating mailbox")
2252 })
2253
2254 c.broadcast(changes)
2255 })
2256
2257 for _, n := range created {
2258 var oldname string
2259 // OLDNAME only with IMAP4rev2 or NOTIFY ../rfc/9051:2726 ../rfc/5465:628
2260 if c.enabled[capIMAP4rev2] && n == name && name != origName && !(name == "Inbox" || strings.HasPrefix(name, "Inbox/")) {
2261 oldname = fmt.Sprintf(` ("OLDNAME" (%s))`, string0(c.encodeMailbox(origName)).pack(c))
2262 }
2263 c.bwritelinef(`* LIST (\Subscribed) "/" %s%s`, astring(c.encodeMailbox(n)).pack(c), oldname)
2264 }
2265 c.ok(tag, cmd)
2266}
2267
2268// Delete removes a mailbox and all its messages.
2269// Inbox cannot be removed.
2270//
2271// State: Authenticated and selected.
2272func (c *conn) cmdDelete(tag, cmd string, p *parser) {
2273 // Command: ../rfc/9051:1972 ../rfc/3501:1946
2274 // Examples: ../rfc/9051:2025 ../rfc/3501:1992
2275
2276 // Request syntax: ../rfc/9051:6505 ../rfc/3501:4716
2277 p.xspace()
2278 name := p.xmailbox()
2279 p.xempty()
2280
2281 name = xcheckmailboxname(name, false)
2282
2283 // Messages to remove after having broadcasted the removal of messages.
2284 var removeMessageIDs []int64
2285
2286 c.account.WithWLock(func() {
2287 var mb store.Mailbox
2288 var changes []store.Change
2289
2290 c.xdbwrite(func(tx *bstore.Tx) {
2291 mb = c.xmailbox(tx, name, "NONEXISTENT")
2292
2293 var hasChildren bool
2294 var err error
2295 changes, removeMessageIDs, hasChildren, err = c.account.MailboxDelete(context.TODO(), c.log, tx, mb)
2296 if hasChildren {
2297 xusercodeErrorf("HASCHILDREN", "mailbox has a child, only leaf mailboxes can be deleted")
2298 }
2299 xcheckf(err, "deleting mailbox")
2300 })
2301
2302 c.broadcast(changes)
2303 })
2304
2305 for _, mID := range removeMessageIDs {
2306 p := c.account.MessagePath(mID)
2307 err := os.Remove(p)
2308 c.log.Check(err, "removing message file for mailbox delete", slog.String("path", p))
2309 }
2310
2311 c.ok(tag, cmd)
2312}
2313
2314// Rename changes the name of a mailbox.
2315// Renaming INBOX is special, it moves the inbox messages to a new mailbox, leaving inbox empty.
2316// Renaming a mailbox with submailboxes also renames all submailboxes.
2317// Subscriptions stay with the old name, though newly created missing parent
2318// mailboxes for the destination name are automatically subscribed.
2319//
2320// State: Authenticated and selected.
2321func (c *conn) cmdRename(tag, cmd string, p *parser) {
2322 // Command: ../rfc/9051:2062 ../rfc/3501:2040
2323 // Examples: ../rfc/9051:2132 ../rfc/3501:2092
2324
2325 // Request syntax: ../rfc/9051:6863 ../rfc/3501:4908
2326 p.xspace()
2327 src := p.xmailbox()
2328 p.xspace()
2329 dst := p.xmailbox()
2330 p.xempty()
2331
2332 src = xcheckmailboxname(src, true)
2333 dst = xcheckmailboxname(dst, false)
2334
2335 c.account.WithWLock(func() {
2336 var changes []store.Change
2337
2338 c.xdbwrite(func(tx *bstore.Tx) {
2339 srcMB := c.xmailbox(tx, src, "NONEXISTENT")
2340
2341 // Inbox is very special. Unlike other mailboxes, its children are not moved. And
2342 // unlike a regular move, its messages are moved to a newly created mailbox. We do
2343 // indeed create a new destination mailbox and actually move the messages.
2344 // ../rfc/9051:2101
2345 if src == "Inbox" {
2346 exists, err := c.account.MailboxExists(tx, dst)
2347 xcheckf(err, "checking if destination mailbox exists")
2348 if exists {
2349 xusercodeErrorf("ALREADYEXISTS", "destination mailbox %q already exists", dst)
2350 }
2351 if dst == src {
2352 xuserErrorf("cannot move inbox to itself")
2353 }
2354
2355 uidval, err := c.account.NextUIDValidity(tx)
2356 xcheckf(err, "next uid validity")
2357
2358 dstMB := store.Mailbox{
2359 Name: dst,
2360 UIDValidity: uidval,
2361 UIDNext: 1,
2362 Keywords: srcMB.Keywords,
2363 HaveCounts: true,
2364 }
2365 err = tx.Insert(&dstMB)
2366 xcheckf(err, "create new destination mailbox")
2367
2368 modseq, err := c.account.NextModSeq(tx)
2369 xcheckf(err, "assigning next modseq")
2370
2371 changes = make([]store.Change, 2) // Placeholders filled in below.
2372
2373 // Move existing messages, with their ID's and on-disk files intact, to the new
2374 // mailbox. We keep the expunged messages, the destination mailbox doesn't care
2375 // about them.
2376 var oldUIDs []store.UID
2377 q := bstore.QueryTx[store.Message](tx)
2378 q.FilterNonzero(store.Message{MailboxID: srcMB.ID})
2379 q.FilterEqual("Expunged", false)
2380 q.SortAsc("UID")
2381 err = q.ForEach(func(m store.Message) error {
2382 om := m
2383 om.ID = 0
2384 om.ModSeq = modseq
2385 om.PrepareExpunge()
2386 oldUIDs = append(oldUIDs, om.UID)
2387
2388 mc := m.MailboxCounts()
2389 srcMB.Sub(mc)
2390 dstMB.Add(mc)
2391
2392 m.MailboxID = dstMB.ID
2393 m.UID = dstMB.UIDNext
2394 dstMB.UIDNext++
2395 m.CreateSeq = modseq
2396 m.ModSeq = modseq
2397 if err := tx.Update(&m); err != nil {
2398 return fmt.Errorf("updating message to move to new mailbox: %w", err)
2399 }
2400
2401 changes = append(changes, m.ChangeAddUID())
2402
2403 if err := tx.Insert(&om); err != nil {
2404 return fmt.Errorf("adding empty expunge message record to inbox: %w", err)
2405 }
2406 return nil
2407 })
2408 xcheckf(err, "moving messages from inbox to destination mailbox")
2409
2410 err = tx.Update(&dstMB)
2411 xcheckf(err, "updating uidnext and counts in destination mailbox")
2412
2413 err = tx.Update(&srcMB)
2414 xcheckf(err, "updating counts for inbox")
2415
2416 var dstFlags []string
2417 if tx.Get(&store.Subscription{Name: dstMB.Name}) == nil {
2418 dstFlags = []string{`\Subscribed`}
2419 }
2420 changes[0] = store.ChangeRemoveUIDs{MailboxID: srcMB.ID, UIDs: oldUIDs, ModSeq: modseq}
2421 changes[1] = store.ChangeAddMailbox{Mailbox: dstMB, Flags: dstFlags}
2422 // changes[2:...] are ChangeAddUIDs
2423 changes = append(changes, srcMB.ChangeCounts(), dstMB.ChangeCounts())
2424 return
2425 }
2426
2427 var notExists, alreadyExists bool
2428 var err error
2429 changes, _, notExists, alreadyExists, err = c.account.MailboxRename(tx, srcMB, dst)
2430 if notExists {
2431 // ../rfc/9051:5140
2432 xusercodeErrorf("NONEXISTENT", "%s", err)
2433 } else if alreadyExists {
2434 xusercodeErrorf("ALREADYEXISTS", "%s", err)
2435 }
2436 xcheckf(err, "renaming mailbox")
2437 })
2438 c.broadcast(changes)
2439 })
2440
2441 c.ok(tag, cmd)
2442}
2443
2444// Subscribe marks a mailbox path as subscribed. The mailbox does not have to
2445// exist. Subscribed may mean an email client will show the mailbox in its UI
2446// and/or periodically fetch new messages for the mailbox.
2447//
2448// State: Authenticated and selected.
2449func (c *conn) cmdSubscribe(tag, cmd string, p *parser) {
2450 // Command: ../rfc/9051:2172 ../rfc/3501:2135
2451 // Examples: ../rfc/9051:2198 ../rfc/3501:2162
2452
2453 // Request syntax: ../rfc/9051:7083 ../rfc/3501:5059
2454 p.xspace()
2455 name := p.xmailbox()
2456 p.xempty()
2457
2458 name = xcheckmailboxname(name, true)
2459
2460 c.account.WithWLock(func() {
2461 var changes []store.Change
2462
2463 c.xdbwrite(func(tx *bstore.Tx) {
2464 var err error
2465 changes, err = c.account.SubscriptionEnsure(tx, name)
2466 xcheckf(err, "ensuring subscription")
2467 })
2468
2469 c.broadcast(changes)
2470 })
2471
2472 c.ok(tag, cmd)
2473}
2474
2475// Unsubscribe marks a mailbox as not subscribed. The mailbox doesn't have to exist.
2476//
2477// State: Authenticated and selected.
2478func (c *conn) cmdUnsubscribe(tag, cmd string, p *parser) {
2479 // Command: ../rfc/9051:2203 ../rfc/3501:2166
2480 // Examples: ../rfc/9051:2219 ../rfc/3501:2181
2481
2482 // Request syntax: ../rfc/9051:7143 ../rfc/3501:5077
2483 p.xspace()
2484 name := p.xmailbox()
2485 p.xempty()
2486
2487 name = xcheckmailboxname(name, true)
2488
2489 c.account.WithWLock(func() {
2490 c.xdbwrite(func(tx *bstore.Tx) {
2491 // It's OK if not currently subscribed, ../rfc/9051:2215
2492 err := tx.Delete(&store.Subscription{Name: name})
2493 if err == bstore.ErrAbsent {
2494 exists, err := c.account.MailboxExists(tx, name)
2495 xcheckf(err, "checking if mailbox exists")
2496 if !exists {
2497 xuserErrorf("mailbox does not exist")
2498 }
2499 return
2500 }
2501 xcheckf(err, "removing subscription")
2502 })
2503
2504 // todo: can we send untagged message about a mailbox no longer being subscribed?
2505 })
2506
2507 c.ok(tag, cmd)
2508}
2509
2510// LSUB command for listing subscribed mailboxes.
2511// Removed in IMAP4rev2, only in IMAP4rev1.
2512//
2513// State: Authenticated and selected.
2514func (c *conn) cmdLsub(tag, cmd string, p *parser) {
2515 // Command: ../rfc/3501:2374
2516 // Examples: ../rfc/3501:2415
2517
2518 // Request syntax: ../rfc/3501:4806
2519 p.xspace()
2520 ref := p.xmailbox()
2521 p.xspace()
2522 pattern := p.xlistMailbox()
2523 p.xempty()
2524
2525 re := xmailboxPatternMatcher(ref, []string{pattern})
2526
2527 var lines []string
2528 c.xdbread(func(tx *bstore.Tx) {
2529 q := bstore.QueryTx[store.Subscription](tx)
2530 q.SortAsc("Name")
2531 subscriptions, err := q.List()
2532 xcheckf(err, "querying subscriptions")
2533
2534 have := map[string]bool{}
2535 subscribedKids := map[string]bool{}
2536 ispercent := strings.HasSuffix(pattern, "%")
2537 for _, sub := range subscriptions {
2538 name := sub.Name
2539 if ispercent {
2540 for p := path.Dir(name); p != "."; p = path.Dir(p) {
2541 subscribedKids[p] = true
2542 }
2543 }
2544 if !re.MatchString(name) {
2545 continue
2546 }
2547 have[name] = true
2548 line := fmt.Sprintf(`* LSUB () "/" %s`, astring(c.encodeMailbox(name)).pack(c))
2549 lines = append(lines, line)
2550
2551 }
2552
2553 // ../rfc/3501:2394
2554 if !ispercent {
2555 return
2556 }
2557 qmb := bstore.QueryTx[store.Mailbox](tx)
2558 qmb.SortAsc("Name")
2559 err = qmb.ForEach(func(mb store.Mailbox) error {
2560 if have[mb.Name] || !subscribedKids[mb.Name] || !re.MatchString(mb.Name) {
2561 return nil
2562 }
2563 line := fmt.Sprintf(`* LSUB (\NoSelect) "/" %s`, astring(c.encodeMailbox(mb.Name)).pack(c))
2564 lines = append(lines, line)
2565 return nil
2566 })
2567 xcheckf(err, "querying mailboxes")
2568 })
2569
2570 // Response syntax: ../rfc/3501:4833 ../rfc/3501:4837
2571 for _, line := range lines {
2572 c.bwritelinef("%s", line)
2573 }
2574 c.ok(tag, cmd)
2575}
2576
2577// The namespace command returns the mailbox path separator. We only implement
2578// the personal mailbox hierarchy, no shared/other.
2579//
2580// In IMAP4rev2, it was an extension before.
2581//
2582// State: Authenticated and selected.
2583func (c *conn) cmdNamespace(tag, cmd string, p *parser) {
2584 // Command: ../rfc/9051:3098 ../rfc/2342:137
2585 // Examples: ../rfc/9051:3117 ../rfc/2342:155
2586 // Request syntax: ../rfc/9051:6767 ../rfc/2342:410
2587 p.xempty()
2588
2589 // Response syntax: ../rfc/9051:6778 ../rfc/2342:415
2590 c.bwritelinef(`* NAMESPACE (("" "/")) NIL NIL`)
2591 c.ok(tag, cmd)
2592}
2593
2594// The status command returns information about a mailbox, such as the number of
2595// messages, "uid validity", etc. Nowadays, the extended LIST command can return
2596// the same information about many mailboxes for one command.
2597//
2598// State: Authenticated and selected.
2599func (c *conn) cmdStatus(tag, cmd string, p *parser) {
2600 // Command: ../rfc/9051:3328 ../rfc/3501:2424 ../rfc/7162:1127
2601 // Examples: ../rfc/9051:3400 ../rfc/3501:2501 ../rfc/7162:1139
2602
2603 // Request syntax: ../rfc/9051:7053 ../rfc/3501:5036
2604 p.xspace()
2605 name := p.xmailbox()
2606 p.xspace()
2607 p.xtake("(")
2608 attrs := []string{p.xstatusAtt()}
2609 for !p.take(")") {
2610 p.xspace()
2611 attrs = append(attrs, p.xstatusAtt())
2612 }
2613 p.xempty()
2614
2615 name = xcheckmailboxname(name, true)
2616
2617 var mb store.Mailbox
2618
2619 var responseLine string
2620 c.account.WithRLock(func() {
2621 c.xdbread(func(tx *bstore.Tx) {
2622 mb = c.xmailbox(tx, name, "")
2623 responseLine = c.xstatusLine(tx, mb, attrs)
2624 })
2625 })
2626
2627 c.bwritelinef("%s", responseLine)
2628 c.ok(tag, cmd)
2629}
2630
2631// Response syntax: ../rfc/9051:6681 ../rfc/9051:7070 ../rfc/9051:7059 ../rfc/3501:4834
2632func (c *conn) xstatusLine(tx *bstore.Tx, mb store.Mailbox, attrs []string) string {
2633 status := []string{}
2634 for _, a := range attrs {
2635 A := strings.ToUpper(a)
2636 switch A {
2637 case "MESSAGES":
2638 status = append(status, A, fmt.Sprintf("%d", mb.Total+mb.Deleted))
2639 case "UIDNEXT":
2640 status = append(status, A, fmt.Sprintf("%d", mb.UIDNext))
2641 case "UIDVALIDITY":
2642 status = append(status, A, fmt.Sprintf("%d", mb.UIDValidity))
2643 case "UNSEEN":
2644 status = append(status, A, fmt.Sprintf("%d", mb.Unseen))
2645 case "DELETED":
2646 status = append(status, A, fmt.Sprintf("%d", mb.Deleted))
2647 case "SIZE":
2648 status = append(status, A, fmt.Sprintf("%d", mb.Size))
2649 case "RECENT":
2650 status = append(status, A, "0")
2651 case "APPENDLIMIT":
2652 // ../rfc/7889:255
2653 status = append(status, A, "NIL")
2654 case "HIGHESTMODSEQ":
2655 // ../rfc/7162:366
2656 status = append(status, A, fmt.Sprintf("%d", c.xhighestModSeq(tx, mb.ID).Client()))
2657 default:
2658 xsyntaxErrorf("unknown attribute %q", a)
2659 }
2660 }
2661 return fmt.Sprintf("* STATUS %s (%s)", astring(c.encodeMailbox(mb.Name)).pack(c), strings.Join(status, " "))
2662}
2663
2664func flaglist(fl store.Flags, keywords []string) listspace {
2665 l := listspace{}
2666 flag := func(v bool, s string) {
2667 if v {
2668 l = append(l, bare(s))
2669 }
2670 }
2671 flag(fl.Seen, `\Seen`)
2672 flag(fl.Answered, `\Answered`)
2673 flag(fl.Flagged, `\Flagged`)
2674 flag(fl.Deleted, `\Deleted`)
2675 flag(fl.Draft, `\Draft`)
2676 flag(fl.Forwarded, `$Forwarded`)
2677 flag(fl.Junk, `$Junk`)
2678 flag(fl.Notjunk, `$NotJunk`)
2679 flag(fl.Phishing, `$Phishing`)
2680 flag(fl.MDNSent, `$MDNSent`)
2681 for _, k := range keywords {
2682 l = append(l, bare(k))
2683 }
2684 return l
2685}
2686
2687// Append adds a message to a mailbox.
2688//
2689// State: Authenticated and selected.
2690func (c *conn) cmdAppend(tag, cmd string, p *parser) {
2691 // Command: ../rfc/9051:3406 ../rfc/6855:204 ../rfc/3501:2527
2692 // Examples: ../rfc/9051:3482 ../rfc/3501:2589
2693
2694 // Request syntax: ../rfc/9051:6325 ../rfc/6855:219 ../rfc/3501:4547
2695 p.xspace()
2696 name := p.xmailbox()
2697 p.xspace()
2698 var storeFlags store.Flags
2699 var keywords []string
2700 if p.hasPrefix("(") {
2701 // Error must be a syntax error, to properly abort the connection due to literal.
2702 var err error
2703 storeFlags, keywords, err = store.ParseFlagsKeywords(p.xflagList())
2704 if err != nil {
2705 xsyntaxErrorf("parsing flags: %v", err)
2706 }
2707 p.xspace()
2708 }
2709 var tm time.Time
2710 if p.hasPrefix(`"`) {
2711 tm = p.xdateTime()
2712 p.xspace()
2713 } else {
2714 tm = time.Now()
2715 }
2716 // todo: only with utf8 should we we accept message headers with utf-8. we currently always accept them.
2717 // todo: this is only relevant if we also support the CATENATE extension?
2718 // ../rfc/6855:204
2719 utf8 := p.take("UTF8 (")
2720 size, sync := p.xliteralSize(0, utf8)
2721
2722 name = xcheckmailboxname(name, true)
2723 c.xdbread(func(tx *bstore.Tx) {
2724 c.xmailbox(tx, name, "TRYCREATE")
2725 })
2726 if sync {
2727 c.writelinef("+ ")
2728 }
2729
2730 // Read the message into a temporary file.
2731 msgFile, err := store.CreateMessageTemp(c.log, "imap-append")
2732 xcheckf(err, "creating temp file for message")
2733 defer func() {
2734 p := msgFile.Name()
2735 err := msgFile.Close()
2736 c.xsanity(err, "closing APPEND temporary file")
2737 err = os.Remove(p)
2738 c.xsanity(err, "removing APPEND temporary file")
2739 }()
2740 defer c.xtrace(mlog.LevelTracedata)()
2741 mw := message.NewWriter(msgFile)
2742 msize, err := io.Copy(mw, io.LimitReader(c.br, size))
2743 c.xtrace(mlog.LevelTrace) // Restore.
2744 if err != nil {
2745 // Cannot use xcheckf due to %w handling of errIO.
2746 panic(fmt.Errorf("reading literal message: %s (%w)", err, errIO))
2747 }
2748 if msize != size {
2749 xserverErrorf("read %d bytes for message, expected %d (%w)", msize, size, errIO)
2750 }
2751
2752 if utf8 {
2753 line := c.readline(false)
2754 np := newParser(line, c)
2755 np.xtake(")")
2756 np.xempty()
2757 } else {
2758 line := c.readline(false)
2759 np := newParser(line, c)
2760 np.xempty()
2761 }
2762 p.xempty()
2763 if !sync {
2764 name = xcheckmailboxname(name, true)
2765 }
2766
2767 var mb store.Mailbox
2768 var m store.Message
2769 var pendingChanges []store.Change
2770
2771 c.account.WithWLock(func() {
2772 var changes []store.Change
2773 c.xdbwrite(func(tx *bstore.Tx) {
2774 mb = c.xmailbox(tx, name, "TRYCREATE")
2775
2776 // Ensure keywords are stored in mailbox.
2777 var mbKwChanged bool
2778 mb.Keywords, mbKwChanged = store.MergeKeywords(mb.Keywords, keywords)
2779 if mbKwChanged {
2780 changes = append(changes, mb.ChangeKeywords())
2781 }
2782
2783 m = store.Message{
2784 MailboxID: mb.ID,
2785 MailboxOrigID: mb.ID,
2786 Received: tm,
2787 Flags: storeFlags,
2788 Keywords: keywords,
2789 Size: mw.Size,
2790 }
2791
2792 ok, maxSize, err := c.account.CanAddMessageSize(tx, m.Size)
2793 xcheckf(err, "checking quota")
2794 if !ok {
2795 // ../rfc/9051:5155
2796 xusercodeErrorf("OVERQUOTA", "account over maximum total message size %d", maxSize)
2797 }
2798
2799 mb.Add(m.MailboxCounts())
2800
2801 // Update mailbox before delivering, which updates uidnext which we mustn't overwrite.
2802 err = tx.Update(&mb)
2803 xcheckf(err, "updating mailbox counts")
2804
2805 err = c.account.DeliverMessage(c.log, tx, &m, msgFile, true, false, false, true)
2806 xcheckf(err, "delivering message")
2807 })
2808
2809 // Fetch pending changes, possibly with new UIDs, so we can apply them before adding our own new UID.
2810 if c.comm != nil {
2811 pendingChanges = c.comm.Get()
2812 }
2813
2814 // Broadcast the change to other connections.
2815 changes = append(changes, m.ChangeAddUID(), mb.ChangeCounts())
2816 c.broadcast(changes)
2817 })
2818
2819 if c.mailboxID == mb.ID {
2820 c.applyChanges(pendingChanges, false)
2821 c.uidAppend(m.UID)
2822 // 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.
2823 c.bwritelinef("* %d EXISTS", len(c.uids))
2824 }
2825
2826 c.writeresultf("%s OK [APPENDUID %d %d] appended", tag, mb.UIDValidity, m.UID)
2827}
2828
2829// Idle makes a client wait until the server sends untagged updates, e.g. about
2830// message delivery or mailbox create/rename/delete/subscription, etc. It allows a
2831// client to get updates in real-time, not needing the use for NOOP.
2832//
2833// State: Authenticated and selected.
2834func (c *conn) cmdIdle(tag, cmd string, p *parser) {
2835 // Command: ../rfc/9051:3542 ../rfc/2177:49
2836 // Example: ../rfc/9051:3589 ../rfc/2177:119
2837
2838 // Request syntax: ../rfc/9051:6594 ../rfc/2177:163
2839 p.xempty()
2840
2841 c.writelinef("+ waiting")
2842
2843 var line string
2844wait:
2845 for {
2846 select {
2847 case le := <-c.lineChan():
2848 c.line = nil
2849 xcheckf(le.err, "get line")
2850 line = le.line
2851 break wait
2852 case <-c.comm.Pending:
2853 c.applyChanges(c.comm.Get(), false)
2854 c.xflush()
2855 case <-mox.Shutdown.Done():
2856 // ../rfc/9051:5375
2857 c.writelinef("* BYE shutting down")
2858 panic(errIO)
2859 }
2860 }
2861
2862 // Reset the write deadline. In case of little activity, with a command timeout of
2863 // 30 minutes, we have likely passed it.
2864 err := c.conn.SetWriteDeadline(time.Now().Add(5 * time.Minute))
2865 c.log.Check(err, "setting write deadline")
2866
2867 if strings.ToUpper(line) != "DONE" {
2868 // We just close the connection because our protocols are out of sync.
2869 panic(fmt.Errorf("%w: in IDLE, expected DONE", errIO))
2870 }
2871
2872 c.ok(tag, cmd)
2873}
2874
2875// Check is an old deprecated command that is supposed to execute some mailbox consistency checks.
2876//
2877// State: Selected
2878func (c *conn) cmdCheck(tag, cmd string, p *parser) {
2879 // Command: ../rfc/3501:2618
2880
2881 // Request syntax: ../rfc/3501:4679
2882 p.xempty()
2883
2884 c.account.WithRLock(func() {
2885 c.xdbread(func(tx *bstore.Tx) {
2886 c.xmailboxID(tx, c.mailboxID) // Validate.
2887 })
2888 })
2889
2890 c.ok(tag, cmd)
2891}
2892
2893// Close undoes select/examine, closing the currently opened mailbox and deleting
2894// messages that were marked for deletion with the \Deleted flag.
2895//
2896// State: Selected
2897func (c *conn) cmdClose(tag, cmd string, p *parser) {
2898 // Command: ../rfc/9051:3636 ../rfc/3501:2652 ../rfc/7162:1836
2899
2900 // Request syntax: ../rfc/9051:6476 ../rfc/3501:4679
2901 p.xempty()
2902
2903 if c.readonly {
2904 c.unselect()
2905 c.ok(tag, cmd)
2906 return
2907 }
2908
2909 remove, _ := c.xexpunge(nil, true)
2910
2911 defer func() {
2912 for _, m := range remove {
2913 p := c.account.MessagePath(m.ID)
2914 err := os.Remove(p)
2915 c.xsanity(err, "removing message file for expunge for close")
2916 }
2917 }()
2918
2919 c.unselect()
2920 c.ok(tag, cmd)
2921}
2922
2923// expunge messages marked for deletion in currently selected/active mailbox.
2924// if uidSet is not nil, only messages matching the set are deleted.
2925//
2926// messages that have been marked expunged from the database are returned, but the
2927// corresponding files still have to be removed.
2928//
2929// the highest modseq in the mailbox is returned, typically associated with the
2930// removal of the messages, but if no messages were expunged the current latest max
2931// modseq for the mailbox is returned.
2932func (c *conn) xexpunge(uidSet *numSet, missingMailboxOK bool) (remove []store.Message, highestModSeq store.ModSeq) {
2933 var modseq store.ModSeq
2934
2935 c.account.WithWLock(func() {
2936 var mb store.Mailbox
2937
2938 c.xdbwrite(func(tx *bstore.Tx) {
2939 mb = store.Mailbox{ID: c.mailboxID}
2940 err := tx.Get(&mb)
2941 if err == bstore.ErrAbsent {
2942 if missingMailboxOK {
2943 return
2944 }
2945 xuserErrorf("%w", store.ErrUnknownMailbox)
2946 }
2947
2948 qm := bstore.QueryTx[store.Message](tx)
2949 qm.FilterNonzero(store.Message{MailboxID: c.mailboxID})
2950 qm.FilterEqual("Deleted", true)
2951 qm.FilterEqual("Expunged", false)
2952 qm.FilterFn(func(m store.Message) bool {
2953 // Only remove if this session knows about the message and if present in optional uidSet.
2954 return uidSearch(c.uids, m.UID) > 0 && (uidSet == nil || uidSet.containsUID(m.UID, c.uids, c.searchResult))
2955 })
2956 qm.SortAsc("UID")
2957 remove, err = qm.List()
2958 xcheckf(err, "listing messages to delete")
2959
2960 if len(remove) == 0 {
2961 highestModSeq = c.xhighestModSeq(tx, c.mailboxID)
2962 return
2963 }
2964
2965 // Assign new modseq.
2966 modseq, err = c.account.NextModSeq(tx)
2967 xcheckf(err, "assigning next modseq")
2968 highestModSeq = modseq
2969
2970 removeIDs := make([]int64, len(remove))
2971 anyIDs := make([]any, len(remove))
2972 var totalSize int64
2973 for i, m := range remove {
2974 removeIDs[i] = m.ID
2975 anyIDs[i] = m.ID
2976 mb.Sub(m.MailboxCounts())
2977 totalSize += m.Size
2978 // Update "remove", because RetrainMessage below will save the message.
2979 remove[i].Expunged = true
2980 remove[i].ModSeq = modseq
2981 }
2982 qmr := bstore.QueryTx[store.Recipient](tx)
2983 qmr.FilterEqual("MessageID", anyIDs...)
2984 _, err = qmr.Delete()
2985 xcheckf(err, "removing message recipients")
2986
2987 qm = bstore.QueryTx[store.Message](tx)
2988 qm.FilterIDs(removeIDs)
2989 n, err := qm.UpdateNonzero(store.Message{Expunged: true, ModSeq: modseq})
2990 if err == nil && n != len(removeIDs) {
2991 err = fmt.Errorf("only %d messages set to expunged, expected %d", n, len(removeIDs))
2992 }
2993 xcheckf(err, "marking messages marked for deleted as expunged")
2994
2995 err = tx.Update(&mb)
2996 xcheckf(err, "updating mailbox counts")
2997
2998 err = c.account.AddMessageSize(c.log, tx, -totalSize)
2999 xcheckf(err, "updating disk usage")
3000
3001 // Mark expunged messages as not needing training, then retrain them, so if they
3002 // were trained, they get untrained.
3003 for i := range remove {
3004 remove[i].Junk = false
3005 remove[i].Notjunk = false
3006 }
3007 err = c.account.RetrainMessages(context.TODO(), c.log, tx, remove, true)
3008 xcheckf(err, "untraining expunged messages")
3009 })
3010
3011 // Broadcast changes to other connections. We may not have actually removed any
3012 // messages, so take care not to send an empty update.
3013 if len(remove) > 0 {
3014 ouids := make([]store.UID, len(remove))
3015 for i, m := range remove {
3016 ouids[i] = m.UID
3017 }
3018 changes := []store.Change{
3019 store.ChangeRemoveUIDs{MailboxID: c.mailboxID, UIDs: ouids, ModSeq: modseq},
3020 mb.ChangeCounts(),
3021 }
3022 c.broadcast(changes)
3023 }
3024 })
3025 return remove, highestModSeq
3026}
3027
3028// Unselect is similar to close in that it closes the currently active mailbox, but
3029// it does not remove messages marked for deletion.
3030//
3031// State: Selected
3032func (c *conn) cmdUnselect(tag, cmd string, p *parser) {
3033 // Command: ../rfc/9051:3667 ../rfc/3691:89
3034
3035 // Request syntax: ../rfc/9051:6476 ../rfc/3691:135
3036 p.xempty()
3037
3038 c.unselect()
3039 c.ok(tag, cmd)
3040}
3041
3042// Expunge deletes messages marked with \Deleted in the currently selected mailbox.
3043// Clients are wiser to use UID EXPUNGE because it allows a UID sequence set to
3044// explicitly opt in to removing specific messages.
3045//
3046// State: Selected
3047func (c *conn) cmdExpunge(tag, cmd string, p *parser) {
3048 // Command: ../rfc/9051:3687 ../rfc/3501:2695 ../rfc/7162:1770
3049
3050 // Request syntax: ../rfc/9051:6476 ../rfc/3501:4679
3051 p.xempty()
3052
3053 if c.readonly {
3054 xuserErrorf("mailbox open in read-only mode")
3055 }
3056
3057 c.cmdxExpunge(tag, cmd, nil)
3058}
3059
3060// UID expunge deletes messages marked with \Deleted in the currently selected
3061// mailbox if they match a UID sequence set.
3062//
3063// State: Selected
3064func (c *conn) cmdUIDExpunge(tag, cmd string, p *parser) {
3065 // Command: ../rfc/9051:4775 ../rfc/4315:75 ../rfc/7162:1873
3066
3067 // Request syntax: ../rfc/9051:7125 ../rfc/9051:7129 ../rfc/4315:298
3068 p.xspace()
3069 uidSet := p.xnumSet()
3070 p.xempty()
3071
3072 if c.readonly {
3073 xuserErrorf("mailbox open in read-only mode")
3074 }
3075
3076 c.cmdxExpunge(tag, cmd, &uidSet)
3077}
3078
3079// Permanently delete messages for the currently selected/active mailbox. If uidset
3080// is not nil, only those UIDs are removed.
3081// State: Selected
3082func (c *conn) cmdxExpunge(tag, cmd string, uidSet *numSet) {
3083 // Command: ../rfc/9051:3687 ../rfc/3501:2695
3084
3085 remove, highestModSeq := c.xexpunge(uidSet, false)
3086
3087 defer func() {
3088 for _, m := range remove {
3089 p := c.account.MessagePath(m.ID)
3090 err := os.Remove(p)
3091 c.xsanity(err, "removing message file for expunge")
3092 }
3093 }()
3094
3095 // Response syntax: ../rfc/9051:6742 ../rfc/3501:4864
3096 var vanishedUIDs numSet
3097 qresync := c.enabled[capQresync]
3098 for _, m := range remove {
3099 seq := c.xsequence(m.UID)
3100 c.sequenceRemove(seq, m.UID)
3101 if qresync {
3102 vanishedUIDs.append(uint32(m.UID))
3103 } else {
3104 c.bwritelinef("* %d EXPUNGE", seq)
3105 }
3106 }
3107 if !vanishedUIDs.empty() {
3108 // VANISHED without EARLIER. ../rfc/7162:2004
3109 for _, s := range vanishedUIDs.Strings(4*1024 - 32) {
3110 c.bwritelinef("* VANISHED %s", s)
3111 }
3112 }
3113
3114 if c.enabled[capCondstore] {
3115 c.writeresultf("%s OK [HIGHESTMODSEQ %d] expunged", tag, highestModSeq.Client())
3116 } else {
3117 c.ok(tag, cmd)
3118 }
3119}
3120
3121// State: Selected
3122func (c *conn) cmdSearch(tag, cmd string, p *parser) {
3123 c.cmdxSearch(false, tag, cmd, p)
3124}
3125
3126// State: Selected
3127func (c *conn) cmdUIDSearch(tag, cmd string, p *parser) {
3128 c.cmdxSearch(true, tag, cmd, p)
3129}
3130
3131// State: Selected
3132func (c *conn) cmdFetch(tag, cmd string, p *parser) {
3133 c.cmdxFetch(false, tag, cmd, p)
3134}
3135
3136// State: Selected
3137func (c *conn) cmdUIDFetch(tag, cmd string, p *parser) {
3138 c.cmdxFetch(true, tag, cmd, p)
3139}
3140
3141// State: Selected
3142func (c *conn) cmdStore(tag, cmd string, p *parser) {
3143 c.cmdxStore(false, tag, cmd, p)
3144}
3145
3146// State: Selected
3147func (c *conn) cmdUIDStore(tag, cmd string, p *parser) {
3148 c.cmdxStore(true, tag, cmd, p)
3149}
3150
3151// State: Selected
3152func (c *conn) cmdCopy(tag, cmd string, p *parser) {
3153 c.cmdxCopy(false, tag, cmd, p)
3154}
3155
3156// State: Selected
3157func (c *conn) cmdUIDCopy(tag, cmd string, p *parser) {
3158 c.cmdxCopy(true, tag, cmd, p)
3159}
3160
3161// State: Selected
3162func (c *conn) cmdMove(tag, cmd string, p *parser) {
3163 c.cmdxMove(false, tag, cmd, p)
3164}
3165
3166// State: Selected
3167func (c *conn) cmdUIDMove(tag, cmd string, p *parser) {
3168 c.cmdxMove(true, tag, cmd, p)
3169}
3170
3171func (c *conn) gatherCopyMoveUIDs(isUID bool, nums numSet) ([]store.UID, []any) {
3172 // Gather uids, then sort so we can return a consistently simple and hard to
3173 // misinterpret COPYUID/MOVEUID response. It seems safer to have UIDs in ascending
3174 // order, because requested uid set of 12:10 is equal to 10:12, so if we would just
3175 // echo whatever the client sends us without reordering, the client can reorder our
3176 // response and interpret it differently than we intended.
3177 // ../rfc/9051:5072
3178 uids := c.xnumSetUIDs(isUID, nums)
3179 sort.Slice(uids, func(i, j int) bool {
3180 return uids[i] < uids[j]
3181 })
3182 uidargs := make([]any, len(uids))
3183 for i, uid := range uids {
3184 uidargs[i] = uid
3185 }
3186 return uids, uidargs
3187}
3188
3189// Copy copies messages from the currently selected/active mailbox to another named
3190// mailbox.
3191//
3192// State: Selected
3193func (c *conn) cmdxCopy(isUID bool, tag, cmd string, p *parser) {
3194 // Command: ../rfc/9051:4602 ../rfc/3501:3288
3195
3196 // Request syntax: ../rfc/9051:6482 ../rfc/3501:4685
3197 p.xspace()
3198 nums := p.xnumSet()
3199 p.xspace()
3200 name := p.xmailbox()
3201 p.xempty()
3202
3203 name = xcheckmailboxname(name, true)
3204
3205 uids, uidargs := c.gatherCopyMoveUIDs(isUID, nums)
3206
3207 // Files that were created during the copy. Remove them if the operation fails.
3208 var createdIDs []int64
3209 defer func() {
3210 x := recover()
3211 if x == nil {
3212 return
3213 }
3214 for _, id := range createdIDs {
3215 p := c.account.MessagePath(id)
3216 err := os.Remove(p)
3217 c.xsanity(err, "cleaning up created file")
3218 }
3219 panic(x)
3220 }()
3221
3222 var mbDst store.Mailbox
3223 var origUIDs, newUIDs []store.UID
3224 var flags []store.Flags
3225 var keywords [][]string
3226 var modseq store.ModSeq // For messages in new mailbox, assigned when first message is copied.
3227
3228 c.account.WithWLock(func() {
3229 var mbKwChanged bool
3230
3231 c.xdbwrite(func(tx *bstore.Tx) {
3232 mbSrc := c.xmailboxID(tx, c.mailboxID) // Validate.
3233 mbDst = c.xmailbox(tx, name, "TRYCREATE")
3234 if mbDst.ID == mbSrc.ID {
3235 xuserErrorf("cannot copy to currently selected mailbox")
3236 }
3237
3238 if len(uidargs) == 0 {
3239 xuserErrorf("no matching messages to copy")
3240 }
3241
3242 var err error
3243 modseq, err = c.account.NextModSeq(tx)
3244 xcheckf(err, "assigning next modseq")
3245
3246 // Reserve the uids in the destination mailbox.
3247 uidFirst := mbDst.UIDNext
3248 mbDst.UIDNext += store.UID(len(uidargs))
3249
3250 // Fetch messages from database.
3251 q := bstore.QueryTx[store.Message](tx)
3252 q.FilterNonzero(store.Message{MailboxID: c.mailboxID})
3253 q.FilterEqual("UID", uidargs...)
3254 q.FilterEqual("Expunged", false)
3255 xmsgs, err := q.List()
3256 xcheckf(err, "fetching messages")
3257
3258 if len(xmsgs) != len(uidargs) {
3259 xserverErrorf("uid and message mismatch")
3260 }
3261
3262 // See if quota allows copy.
3263 var totalSize int64
3264 for _, m := range xmsgs {
3265 totalSize += m.Size
3266 }
3267 if ok, maxSize, err := c.account.CanAddMessageSize(tx, totalSize); err != nil {
3268 xcheckf(err, "checking quota")
3269 } else if !ok {
3270 // ../rfc/9051:5155
3271 xusercodeErrorf("OVERQUOTA", "account over maximum total message size %d", maxSize)
3272 }
3273 err = c.account.AddMessageSize(c.log, tx, totalSize)
3274 xcheckf(err, "updating disk usage")
3275
3276 msgs := map[store.UID]store.Message{}
3277 for _, m := range xmsgs {
3278 msgs[m.UID] = m
3279 }
3280 nmsgs := make([]store.Message, len(xmsgs))
3281
3282 conf, _ := c.account.Conf()
3283
3284 mbKeywords := map[string]struct{}{}
3285
3286 // Insert new messages into database.
3287 var origMsgIDs, newMsgIDs []int64
3288 for i, uid := range uids {
3289 m, ok := msgs[uid]
3290 if !ok {
3291 xuserErrorf("messages changed, could not fetch requested uid")
3292 }
3293 origID := m.ID
3294 origMsgIDs = append(origMsgIDs, origID)
3295 m.ID = 0
3296 m.UID = uidFirst + store.UID(i)
3297 m.CreateSeq = modseq
3298 m.ModSeq = modseq
3299 m.MailboxID = mbDst.ID
3300 if m.IsReject && m.MailboxDestinedID != 0 {
3301 // Incorrectly delivered to Rejects mailbox. Adjust MailboxOrigID so this message
3302 // is used for reputation calculation during future deliveries.
3303 m.MailboxOrigID = m.MailboxDestinedID
3304 m.IsReject = false
3305 }
3306 m.TrainedJunk = nil
3307 m.JunkFlagsForMailbox(mbDst, conf)
3308 err := tx.Insert(&m)
3309 xcheckf(err, "inserting message")
3310 msgs[uid] = m
3311 nmsgs[i] = m
3312 origUIDs = append(origUIDs, uid)
3313 newUIDs = append(newUIDs, m.UID)
3314 newMsgIDs = append(newMsgIDs, m.ID)
3315 flags = append(flags, m.Flags)
3316 keywords = append(keywords, m.Keywords)
3317 for _, kw := range m.Keywords {
3318 mbKeywords[kw] = struct{}{}
3319 }
3320
3321 qmr := bstore.QueryTx[store.Recipient](tx)
3322 qmr.FilterNonzero(store.Recipient{MessageID: origID})
3323 mrs, err := qmr.List()
3324 xcheckf(err, "listing message recipients")
3325 for _, mr := range mrs {
3326 mr.ID = 0
3327 mr.MessageID = m.ID
3328 err := tx.Insert(&mr)
3329 xcheckf(err, "inserting message recipient")
3330 }
3331
3332 mbDst.Add(m.MailboxCounts())
3333 }
3334
3335 mbDst.Keywords, mbKwChanged = store.MergeKeywords(mbDst.Keywords, maps.Keys(mbKeywords))
3336
3337 err = tx.Update(&mbDst)
3338 xcheckf(err, "updating destination mailbox for uids, keywords and counts")
3339
3340 // Copy message files to new message ID's.
3341 syncDirs := map[string]struct{}{}
3342 for i := range origMsgIDs {
3343 src := c.account.MessagePath(origMsgIDs[i])
3344 dst := c.account.MessagePath(newMsgIDs[i])
3345 dstdir := filepath.Dir(dst)
3346 if _, ok := syncDirs[dstdir]; !ok {
3347 os.MkdirAll(dstdir, 0770)
3348 syncDirs[dstdir] = struct{}{}
3349 }
3350 err := moxio.LinkOrCopy(c.log, dst, src, nil, true)
3351 xcheckf(err, "link or copy file %q to %q", src, dst)
3352 createdIDs = append(createdIDs, newMsgIDs[i])
3353 }
3354
3355 for dir := range syncDirs {
3356 err := moxio.SyncDir(c.log, dir)
3357 xcheckf(err, "sync directory")
3358 }
3359
3360 err = c.account.RetrainMessages(context.TODO(), c.log, tx, nmsgs, false)
3361 xcheckf(err, "train copied messages")
3362 })
3363
3364 // Broadcast changes to other connections.
3365 if len(newUIDs) > 0 {
3366 changes := make([]store.Change, 0, len(newUIDs)+2)
3367 for i, uid := range newUIDs {
3368 changes = append(changes, store.ChangeAddUID{MailboxID: mbDst.ID, UID: uid, ModSeq: modseq, Flags: flags[i], Keywords: keywords[i]})
3369 }
3370 changes = append(changes, mbDst.ChangeCounts())
3371 if mbKwChanged {
3372 changes = append(changes, mbDst.ChangeKeywords())
3373 }
3374 c.broadcast(changes)
3375 }
3376 })
3377
3378 // All good, prevent defer above from cleaning up copied files.
3379 createdIDs = nil
3380
3381 // ../rfc/9051:6881 ../rfc/4315:183
3382 c.writeresultf("%s OK [COPYUID %d %s %s] copied", tag, mbDst.UIDValidity, compactUIDSet(origUIDs).String(), compactUIDSet(newUIDs).String())
3383}
3384
3385// Move moves messages from the currently selected/active mailbox to a named mailbox.
3386//
3387// State: Selected
3388func (c *conn) cmdxMove(isUID bool, tag, cmd string, p *parser) {
3389 // Command: ../rfc/9051:4650 ../rfc/6851:119 ../rfc/6851:265
3390
3391 // Request syntax: ../rfc/6851:320
3392 p.xspace()
3393 nums := p.xnumSet()
3394 p.xspace()
3395 name := p.xmailbox()
3396 p.xempty()
3397
3398 name = xcheckmailboxname(name, true)
3399
3400 if c.readonly {
3401 xuserErrorf("mailbox open in read-only mode")
3402 }
3403
3404 uids, uidargs := c.gatherCopyMoveUIDs(isUID, nums)
3405
3406 var mbSrc, mbDst store.Mailbox
3407 var changes []store.Change
3408 var newUIDs []store.UID
3409 var modseq store.ModSeq
3410
3411 c.account.WithWLock(func() {
3412 c.xdbwrite(func(tx *bstore.Tx) {
3413 mbSrc = c.xmailboxID(tx, c.mailboxID) // Validate.
3414 mbDst = c.xmailbox(tx, name, "TRYCREATE")
3415 if mbDst.ID == c.mailboxID {
3416 xuserErrorf("cannot move to currently selected mailbox")
3417 }
3418
3419 if len(uidargs) == 0 {
3420 xuserErrorf("no matching messages to move")
3421 }
3422
3423 // Reserve the uids in the destination mailbox.
3424 uidFirst := mbDst.UIDNext
3425 uidnext := uidFirst
3426 mbDst.UIDNext += store.UID(len(uids))
3427
3428 // Assign a new modseq, for the new records and for the expunged records.
3429 var err error
3430 modseq, err = c.account.NextModSeq(tx)
3431 xcheckf(err, "assigning next modseq")
3432
3433 // Update existing record with new UID and MailboxID in database for messages. We
3434 // add a new but expunged record again in the original/source mailbox, for qresync.
3435 // Keeping the original ID for the live message means we don't have to move the
3436 // on-disk message contents file.
3437 q := bstore.QueryTx[store.Message](tx)
3438 q.FilterNonzero(store.Message{MailboxID: c.mailboxID})
3439 q.FilterEqual("UID", uidargs...)
3440 q.FilterEqual("Expunged", false)
3441 q.SortAsc("UID")
3442 msgs, err := q.List()
3443 xcheckf(err, "listing messages to move")
3444
3445 if len(msgs) != len(uidargs) {
3446 xserverErrorf("uid and message mismatch")
3447 }
3448
3449 keywords := map[string]struct{}{}
3450
3451 conf, _ := c.account.Conf()
3452 for i := range msgs {
3453 m := &msgs[i]
3454 if m.UID != uids[i] {
3455 xserverErrorf("internal error: got uid %d, expected %d, for index %d", m.UID, uids[i], i)
3456 }
3457
3458 mbSrc.Sub(m.MailboxCounts())
3459
3460 // Copy of message record that we'll insert when UID is freed up.
3461 om := *m
3462 om.PrepareExpunge()
3463 om.ID = 0 // Assign new ID.
3464 om.ModSeq = modseq
3465
3466 m.MailboxID = mbDst.ID
3467 if m.IsReject && m.MailboxDestinedID != 0 {
3468 // Incorrectly delivered to Rejects mailbox. Adjust MailboxOrigID so this message
3469 // is used for reputation calculation during future deliveries.
3470 m.MailboxOrigID = m.MailboxDestinedID
3471 m.IsReject = false
3472 m.Seen = false
3473 }
3474 mbDst.Add(m.MailboxCounts())
3475 m.UID = uidnext
3476 m.ModSeq = modseq
3477 m.JunkFlagsForMailbox(mbDst, conf)
3478 uidnext++
3479 err := tx.Update(m)
3480 xcheckf(err, "updating moved message in database")
3481
3482 // Now that UID is unused, we can insert the old record again.
3483 err = tx.Insert(&om)
3484 xcheckf(err, "inserting record for expunge after moving message")
3485
3486 for _, kw := range m.Keywords {
3487 keywords[kw] = struct{}{}
3488 }
3489 }
3490
3491 // Ensure destination mailbox has keywords of the moved messages.
3492 var mbKwChanged bool
3493 mbDst.Keywords, mbKwChanged = store.MergeKeywords(mbDst.Keywords, maps.Keys(keywords))
3494 if mbKwChanged {
3495 changes = append(changes, mbDst.ChangeKeywords())
3496 }
3497
3498 err = tx.Update(&mbSrc)
3499 xcheckf(err, "updating source mailbox counts")
3500
3501 err = tx.Update(&mbDst)
3502 xcheckf(err, "updating destination mailbox for uids, keywords and counts")
3503
3504 err = c.account.RetrainMessages(context.TODO(), c.log, tx, msgs, false)
3505 xcheckf(err, "retraining messages after move")
3506
3507 // Prepare broadcast changes to other connections.
3508 changes = make([]store.Change, 0, 1+len(msgs)+2)
3509 changes = append(changes, store.ChangeRemoveUIDs{MailboxID: c.mailboxID, UIDs: uids, ModSeq: modseq})
3510 for _, m := range msgs {
3511 newUIDs = append(newUIDs, m.UID)
3512 changes = append(changes, m.ChangeAddUID())
3513 }
3514 changes = append(changes, mbSrc.ChangeCounts(), mbDst.ChangeCounts())
3515 })
3516
3517 c.broadcast(changes)
3518 })
3519
3520 // ../rfc/9051:4708 ../rfc/6851:254
3521 // ../rfc/9051:4713
3522 c.bwritelinef("* OK [COPYUID %d %s %s] moved", mbDst.UIDValidity, compactUIDSet(uids).String(), compactUIDSet(newUIDs).String())
3523 qresync := c.enabled[capQresync]
3524 var vanishedUIDs numSet
3525 for i := 0; i < len(uids); i++ {
3526 seq := c.xsequence(uids[i])
3527 c.sequenceRemove(seq, uids[i])
3528 if qresync {
3529 vanishedUIDs.append(uint32(uids[i]))
3530 } else {
3531 c.bwritelinef("* %d EXPUNGE", seq)
3532 }
3533 }
3534 if !vanishedUIDs.empty() {
3535 // VANISHED without EARLIER. ../rfc/7162:2004
3536 for _, s := range vanishedUIDs.Strings(4*1024 - 32) {
3537 c.bwritelinef("* VANISHED %s", s)
3538 }
3539 }
3540
3541 if c.enabled[capQresync] {
3542 // ../rfc/9051:6744 ../rfc/7162:1334
3543 c.writeresultf("%s OK [HIGHESTMODSEQ %d] move", tag, modseq.Client())
3544 } else {
3545 c.ok(tag, cmd)
3546 }
3547}
3548
3549// Store sets a full set of flags, or adds/removes specific flags.
3550//
3551// State: Selected
3552func (c *conn) cmdxStore(isUID bool, tag, cmd string, p *parser) {
3553 // Command: ../rfc/9051:4543 ../rfc/3501:3214
3554
3555 // Request syntax: ../rfc/9051:7076 ../rfc/3501:5052 ../rfc/4466:691 ../rfc/7162:2471
3556 p.xspace()
3557 nums := p.xnumSet()
3558 p.xspace()
3559 var unchangedSince *int64
3560 if p.take("(") {
3561 // ../rfc/7162:2471
3562 p.xtake("UNCHANGEDSINCE")
3563 p.xspace()
3564 v := p.xnumber64()
3565 unchangedSince = &v
3566 p.xtake(")")
3567 p.xspace()
3568 // UNCHANGEDSINCE is a CONDSTORE-enabling parameter ../rfc/7162:382
3569 c.xensureCondstore(nil)
3570 }
3571 var plus, minus bool
3572 if p.take("+") {
3573 plus = true
3574 } else if p.take("-") {
3575 minus = true
3576 }
3577 p.xtake("FLAGS")
3578 silent := p.take(".SILENT")
3579 p.xspace()
3580 var flagstrs []string
3581 if p.hasPrefix("(") {
3582 flagstrs = p.xflagList()
3583 } else {
3584 flagstrs = append(flagstrs, p.xflag())
3585 for p.space() {
3586 flagstrs = append(flagstrs, p.xflag())
3587 }
3588 }
3589 p.xempty()
3590
3591 if c.readonly {
3592 xuserErrorf("mailbox open in read-only mode")
3593 }
3594
3595 flags, keywords, err := store.ParseFlagsKeywords(flagstrs)
3596 if err != nil {
3597 xuserErrorf("parsing flags: %v", err)
3598 }
3599 var mask store.Flags
3600 if plus {
3601 mask, flags = flags, store.FlagsAll
3602 } else if minus {
3603 mask, flags = flags, store.Flags{}
3604 } else {
3605 mask = store.FlagsAll
3606 }
3607
3608 var mb, origmb store.Mailbox
3609 var updated []store.Message
3610 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.
3611 var modseq store.ModSeq // Assigned when needed.
3612 modified := map[int64]bool{}
3613
3614 c.account.WithWLock(func() {
3615 var mbKwChanged bool
3616 var changes []store.Change
3617
3618 c.xdbwrite(func(tx *bstore.Tx) {
3619 mb = c.xmailboxID(tx, c.mailboxID) // Validate.
3620 origmb = mb
3621
3622 uidargs := c.xnumSetCondition(isUID, nums)
3623
3624 if len(uidargs) == 0 {
3625 return
3626 }
3627
3628 // Ensure keywords are in mailbox.
3629 if !minus {
3630 mb.Keywords, mbKwChanged = store.MergeKeywords(mb.Keywords, keywords)
3631 if mbKwChanged {
3632 err := tx.Update(&mb)
3633 xcheckf(err, "updating mailbox with keywords")
3634 }
3635 }
3636
3637 q := bstore.QueryTx[store.Message](tx)
3638 q.FilterNonzero(store.Message{MailboxID: c.mailboxID})
3639 q.FilterEqual("UID", uidargs...)
3640 q.FilterEqual("Expunged", false)
3641 err := q.ForEach(func(m store.Message) error {
3642 // Client may specify a message multiple times, but we only process it once. ../rfc/7162:823
3643 if modified[m.ID] {
3644 return nil
3645 }
3646
3647 mc := m.MailboxCounts()
3648
3649 origFlags := m.Flags
3650 m.Flags = m.Flags.Set(mask, flags)
3651 oldKeywords := append([]string{}, m.Keywords...)
3652 if minus {
3653 m.Keywords, _ = store.RemoveKeywords(m.Keywords, keywords)
3654 } else if plus {
3655 m.Keywords, _ = store.MergeKeywords(m.Keywords, keywords)
3656 } else {
3657 m.Keywords = keywords
3658 }
3659
3660 keywordsChanged := func() bool {
3661 sort.Strings(oldKeywords)
3662 n := append([]string{}, m.Keywords...)
3663 sort.Strings(n)
3664 return !slices.Equal(oldKeywords, n)
3665 }
3666
3667 // If the message has a more recent modseq than the check requires, we won't modify
3668 // it and report in the final command response.
3669 // ../rfc/7162:555
3670 //
3671 // unchangedSince 0 always fails the check, we don't turn it into 1 like with our
3672 // internal modseqs. RFC implies that is not required for non-system flags, but we
3673 // don't have per-flag modseq and this seems reasonable. ../rfc/7162:640
3674 if unchangedSince != nil && m.ModSeq.Client() > *unchangedSince {
3675 changed = append(changed, m)
3676 return nil
3677 }
3678
3679 // Note: we don't perform the optimization described in ../rfc/7162:1258
3680 // It requires that we keep track of the flags we think the client knows (but only
3681 // on this connection). We don't track that. It also isn't clear why this is
3682 // allowed because it is skipping the condstore conditional check, and the new
3683 // combination of flags could be unintended.
3684
3685 // We do not assign a new modseq if nothing actually changed. ../rfc/7162:1246 ../rfc/7162:312
3686 if origFlags == m.Flags && !keywordsChanged() {
3687 // Note: since we didn't update the modseq, we are not adding m.ID to "modified",
3688 // it would skip the modseq check above. We still add m to list of updated, so we
3689 // send an untagged fetch response. But we don't broadcast it.
3690 updated = append(updated, m)
3691 return nil
3692 }
3693
3694 mb.Sub(mc)
3695 mb.Add(m.MailboxCounts())
3696
3697 // Assign new modseq for first actual change.
3698 if modseq == 0 {
3699 var err error
3700 modseq, err = c.account.NextModSeq(tx)
3701 xcheckf(err, "next modseq")
3702 }
3703 m.ModSeq = modseq
3704 modified[m.ID] = true
3705 updated = append(updated, m)
3706
3707 changes = append(changes, m.ChangeFlags(origFlags))
3708
3709 return tx.Update(&m)
3710 })
3711 xcheckf(err, "storing flags in messages")
3712
3713 if mb.MailboxCounts != origmb.MailboxCounts {
3714 err := tx.Update(&mb)
3715 xcheckf(err, "updating mailbox counts")
3716
3717 changes = append(changes, mb.ChangeCounts())
3718 }
3719 if mbKwChanged {
3720 changes = append(changes, mb.ChangeKeywords())
3721 }
3722
3723 err = c.account.RetrainMessages(context.TODO(), c.log, tx, updated, false)
3724 xcheckf(err, "training messages")
3725 })
3726
3727 c.broadcast(changes)
3728 })
3729
3730 // In the RFC, the section about STORE/UID STORE says we must return MODSEQ when
3731 // UNCHANGEDSINCE was specified. It does not specify it in case UNCHANGEDSINCE
3732 // isn't specified. For that case it does say MODSEQ is needed in unsolicited
3733 // untagged fetch responses. Implying that solicited untagged fetch responses
3734 // should not include MODSEQ (why else mention unsolicited explicitly?). But, in
3735 // the introduction to CONDSTORE it does explicitly specify MODSEQ should be
3736 // included in untagged fetch responses at all times with CONDSTORE-enabled
3737 // connections. It would have been better if the command behaviour was specified in
3738 // the command section, not the introduction to the extension.
3739 // ../rfc/7162:388 ../rfc/7162:852
3740 // ../rfc/7162:549
3741 if !silent || c.enabled[capCondstore] {
3742 for _, m := range updated {
3743 var flags string
3744 if !silent {
3745 flags = fmt.Sprintf(" FLAGS %s", flaglist(m.Flags, m.Keywords).pack(c))
3746 }
3747 var modseqStr string
3748 if c.enabled[capCondstore] {
3749 modseqStr = fmt.Sprintf(" MODSEQ (%d)", m.ModSeq.Client())
3750 }
3751 // ../rfc/9051:6749 ../rfc/3501:4869 ../rfc/7162:2490
3752 c.bwritelinef("* %d FETCH (UID %d%s%s)", c.xsequence(m.UID), m.UID, flags, modseqStr)
3753 }
3754 }
3755
3756 // We don't explicitly send flags for failed updated with silent set. The regular
3757 // notification will get the flags to the client.
3758 // ../rfc/7162:630 ../rfc/3501:3233
3759
3760 if len(changed) == 0 {
3761 c.ok(tag, cmd)
3762 return
3763 }
3764
3765 // Write unsolicited untagged fetch responses for messages that didn't pass the
3766 // unchangedsince check. ../rfc/7162:679
3767 // Also gather UIDs or sequences for the MODIFIED response below. ../rfc/7162:571
3768 var mnums []store.UID
3769 for _, m := range changed {
3770 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())
3771 if isUID {
3772 mnums = append(mnums, m.UID)
3773 } else {
3774 mnums = append(mnums, store.UID(c.xsequence(m.UID)))
3775 }
3776 }
3777
3778 sort.Slice(mnums, func(i, j int) bool {
3779 return mnums[i] < mnums[j]
3780 })
3781 set := compactUIDSet(mnums)
3782 // ../rfc/7162:2506
3783 c.writeresultf("%s OK [MODIFIED %s] conditional store did not modify all", tag, set.String())
3784}
3785