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