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