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