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