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