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