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