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