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