1// Package imapserver implements an IMAPv4 server, rev2 (RFC 9051) and rev1 with extensions (RFC 3501 and more).
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.
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
19We always respond with utf8 mailbox names. We do parse utf7 (only in IMAP4rev1,
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.
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.
61 "golang.org/x/exp/maps"
62 "golang.org/x/exp/slices"
64 "github.com/prometheus/client_golang/prometheus"
65 "github.com/prometheus/client_golang/prometheus/promauto"
67 "github.com/mjl-/bstore"
69 "github.com/mjl-/mox/config"
70 "github.com/mjl-/mox/message"
71 "github.com/mjl-/mox/metrics"
72 "github.com/mjl-/mox/mlog"
73 "github.com/mjl-/mox/mox-"
74 "github.com/mjl-/mox/moxio"
75 "github.com/mjl-/mox/moxvar"
76 "github.com/mjl-/mox/ratelimit"
77 "github.com/mjl-/mox/scram"
78 "github.com/mjl-/mox/store"
81// Most logging should be done through conn.log* functions.
82// Only use imaplog in contexts without connection.
83var xlog = mlog.New("imapserver")
86 metricIMAPConnection = promauto.NewCounterVec(
87 prometheus.CounterOpts{
88 Name: "mox_imap_connection_total",
89 Help: "Incoming IMAP connections.",
92 "service", // imap, imaps
95 metricIMAPCommands = promauto.NewHistogramVec(
96 prometheus.HistogramOpts{
97 Name: "mox_imap_command_duration_seconds",
98 Help: "IMAP command duration and result codes in seconds.",
99 Buckets: []float64{0.001, 0.005, 0.01, 0.05, 0.100, 0.5, 1, 5, 10, 20},
103 "result", // ok, panic, ioerror, badsyntax, servererror, usererror, error
108var limiterConnectionrate, limiterConnections *ratelimit.Limiter
111 // Also called by tests, so they don't trigger the rate limiter.
117 limiterConnectionrate = &ratelimit.Limiter{
118 WindowLimits: []ratelimit.WindowLimit{
121 Limits: [...]int64{300, 900, 2700},
125 limiterConnections = &ratelimit.Limiter{
126 WindowLimits: []ratelimit.WindowLimit{
128 Window: time.Duration(math.MaxInt64), // All of time.
129 Limits: [...]int64{30, 90, 270},
135// Delay after bad/suspicious behaviour. Tests set these to zero.
136var badClientDelay = time.Second // Before reads and after 1-byte writes for probably spammers.
137var authFailDelay = time.Second // After authentication failure.
139// Capabilities (extensions) the server supports. Connections will add a few more, e.g. STARTTLS, LOGINDISABLED, AUTH=PLAIN.
161const serverCapabilities = "IMAP4rev2 IMAP4rev1 ENABLE LITERAL+ IDLE SASL-IR BINARY UNSELECT UIDPLUS ESEARCH SEARCHRES MOVE UTF8=ONLY LIST-EXTENDED SPECIAL-USE LIST-STATUS AUTH=SCRAM-SHA-256 AUTH=SCRAM-SHA-1 AUTH=CRAM-MD5 ID APPENDLIMIT=9223372036854775807 CONDSTORE QRESYNC"
167 tls bool // Whether TLS has been initialized.
168 br *bufio.Reader // From remote, with TLS unwrapped in case of TLS.
169 line chan lineErr // If set, instead of reading from br, a line is read from this channel. For reading a line in IDLE while also waiting for mailbox/account updates.
170 lastLine string // For detecting if syntax error is fatal, i.e. if this ends with a literal. Without crlf.
171 bw *bufio.Writer // To remote, with TLS added in case of TLS.
172 tr *moxio.TraceReader // Kept to change trace level when reading/writing cmd/auth/data.
173 tw *moxio.TraceWriter
174 slow bool // If set, reads are done with a 1 second sleep, and writes are done 1 byte at a time, to keep spammers busy.
175 lastlog time.Time // For printing time since previous log line.
176 tlsConfig *tls.Config // TLS config to use for handshake.
178 noRequireSTARTTLS bool
179 cmd string // Currently executing, for deciding to applyChanges and logging.
180 cmdMetric string // Currently executing, for metrics.
182 ncmds int // Number of commands processed. Used to abort connection when first incoming command is unknown/invalid.
184 enabled map[capability]bool // All upper-case.
186 // Set by SEARCH with SAVE. Can be used by commands accepting a sequence-set with
187 // value "$". When used, UIDs must be verified to still exist, because they may
188 // have been expunged. Cleared by a SELECT or EXAMINE.
189 // Nil means no searchResult is present. An empty list is a valid searchResult,
190 // just not matching any messages.
192 searchResult []store.UID
194 // Only when authenticated.
195 authFailed int // Number of failed auth attempts. For slowing down remote with many failures.
196 username string // Full username as used during login.
197 account *store.Account
198 comm *store.Comm // For sending/receiving changes on mailboxes in account, e.g. from messages incoming on smtp, or another imap client.
200 mailboxID int64 // Only for StateSelected.
201 readonly bool // If opened mailbox is readonly.
202 uids []store.UID // UIDs known in this session, sorted. todo future: store more space-efficiently, as ranges.
205// capability for use with ENABLED and CAPABILITY. We always keep this upper case,
206// e.g. IMAP4REV2. These values are treated case-insensitive, but it's easier for
207// comparison to just always have the same case.
208type capability string
211 capIMAP4rev2 capability = "IMAP4REV2"
212 capUTF8Accept capability = "UTF8=ACCEPT"
213 capCondstore capability = "CONDSTORE"
214 capQresync capability = "QRESYNC"
225 stateNotAuthenticated state = iota
230func stateCommands(cmds ...string) map[string]struct{} {
231 r := map[string]struct{}{}
232 for _, cmd := range cmds {
239 commandsStateAny = stateCommands("capability", "noop", "logout", "id")
240 commandsStateNotAuthenticated = stateCommands("starttls", "authenticate", "login")
241 commandsStateAuthenticated = stateCommands("enable", "select", "examine", "create", "delete", "rename", "subscribe", "unsubscribe", "list", "namespace", "status", "append", "idle", "lsub")
242 commandsStateSelected = stateCommands("close", "unselect", "expunge", "search", "fetch", "store", "copy", "move", "uid expunge", "uid search", "uid fetch", "uid store", "uid copy", "uid move")
245var commands = map[string]func(c *conn, tag, cmd string, p *parser){
247 "capability": (*conn).cmdCapability,
248 "noop": (*conn).cmdNoop,
249 "logout": (*conn).cmdLogout,
253 "starttls": (*conn).cmdStarttls,
254 "authenticate": (*conn).cmdAuthenticate,
255 "login": (*conn).cmdLogin,
257 // Authenticated and selected.
258 "enable": (*conn).cmdEnable,
259 "select": (*conn).cmdSelect,
260 "examine": (*conn).cmdExamine,
261 "create": (*conn).cmdCreate,
262 "delete": (*conn).cmdDelete,
263 "rename": (*conn).cmdRename,
264 "subscribe": (*conn).cmdSubscribe,
265 "unsubscribe": (*conn).cmdUnsubscribe,
266 "list": (*conn).cmdList,
267 "lsub": (*conn).cmdLsub,
268 "namespace": (*conn).cmdNamespace,
269 "status": (*conn).cmdStatus,
270 "append": (*conn).cmdAppend,
271 "idle": (*conn).cmdIdle,
274 "check": (*conn).cmdCheck,
275 "close": (*conn).cmdClose,
276 "unselect": (*conn).cmdUnselect,
277 "expunge": (*conn).cmdExpunge,
278 "uid expunge": (*conn).cmdUIDExpunge,
279 "search": (*conn).cmdSearch,
280 "uid search": (*conn).cmdUIDSearch,
281 "fetch": (*conn).cmdFetch,
282 "uid fetch": (*conn).cmdUIDFetch,
283 "store": (*conn).cmdStore,
284 "uid store": (*conn).cmdUIDStore,
285 "copy": (*conn).cmdCopy,
286 "uid copy": (*conn).cmdUIDCopy,
287 "move": (*conn).cmdMove,
288 "uid move": (*conn).cmdUIDMove,
291var errIO = errors.New("io error") // For read/write errors and errors that should close the connection.
292var errProtocol = errors.New("protocol error") // For protocol errors for which a stack trace should be printed.
296// check err for sanity.
297// if not nil and checkSanity true (set during tests), then panic. if not nil during normal operation, just log.
298func (c *conn) xsanity(err error, format string, args ...any) {
303 panic(fmt.Errorf("%s: %s", fmt.Sprintf(format, args...), err))
305 c.log.Errorx(fmt.Sprintf(format, args...), err)
310// Listen initializes all imap listeners for the configuration, and stores them for Serve to start them.
312 names := maps.Keys(mox.Conf.Static.Listeners)
314 for _, name := range names {
315 listener := mox.Conf.Static.Listeners[name]
317 var tlsConfig *tls.Config
318 if listener.TLS != nil {
319 tlsConfig = listener.TLS.Config
322 if listener.IMAP.Enabled {
323 port := config.Port(listener.IMAP.Port, 143)
324 for _, ip := range listener.IPs {
325 listen1("imap", name, ip, port, tlsConfig, false, listener.IMAP.NoRequireSTARTTLS)
329 if listener.IMAPS.Enabled {
330 port := config.Port(listener.IMAPS.Port, 993)
331 for _, ip := range listener.IPs {
332 listen1("imaps", name, ip, port, tlsConfig, true, false)
340func listen1(protocol, listenerName, ip string, port int, tlsConfig *tls.Config, xtls, noRequireSTARTTLS bool) {
341 addr := net.JoinHostPort(ip, fmt.Sprintf("%d", port))
342 if os.Getuid() == 0 {
343 xlog.Print("listening for imap", mlog.Field("listener", listenerName), mlog.Field("addr", addr), mlog.Field("protocol", protocol))
345 network := mox.Network(ip)
346 ln, err := mox.Listen(network, addr)
348 xlog.Fatalx("imap: listen for imap", err, mlog.Field("protocol", protocol), mlog.Field("listener", listenerName))
351 ln = tls.NewListener(ln, tlsConfig)
356 conn, err := ln.Accept()
358 xlog.Infox("imap: accept", err, mlog.Field("protocol", protocol), mlog.Field("listener", listenerName))
362 metricIMAPConnection.WithLabelValues(protocol).Inc()
363 go serve(listenerName, mox.Cid(), tlsConfig, conn, xtls, noRequireSTARTTLS)
367 servers = append(servers, serve)
370// Serve starts serving on all listeners, launching a goroutine per listener.
372 for _, serve := range servers {
378// returns whether this connection accepts utf-8 in strings.
379func (c *conn) utf8strings() bool {
380 return c.enabled[capIMAP4rev2] || c.enabled[capUTF8Accept]
383func (c *conn) xdbwrite(fn func(tx *bstore.Tx)) {
384 err := c.account.DB.Write(context.TODO(), func(tx *bstore.Tx) error {
388 xcheckf(err, "transaction")
391func (c *conn) xdbread(fn func(tx *bstore.Tx)) {
392 err := c.account.DB.Read(context.TODO(), func(tx *bstore.Tx) error {
396 xcheckf(err, "transaction")
399// Closes the currently selected/active mailbox, setting state from selected to authenticated.
400// Does not remove messages marked for deletion.
401func (c *conn) unselect() {
402 if c.state == stateSelected {
403 c.state = stateAuthenticated
409func (c *conn) setSlow(on bool) {
411 c.log.Debug("connection changed to slow")
412 } else if !on && c.slow {
413 c.log.Debug("connection restored to regular pace")
418// Write makes a connection an io.Writer. It panics for i/o errors. These errors
419// are handled in the connection command loop.
420func (c *conn) Write(buf []byte) (int, error) {
428 err := c.conn.SetWriteDeadline(time.Now().Add(30 * time.Second))
429 c.log.Check(err, "setting write deadline")
431 nn, err := c.conn.Write(buf[:chunk])
433 panic(fmt.Errorf("write: %s (%w)", err, errIO))
437 if len(buf) > 0 && badClientDelay > 0 {
438 mox.Sleep(mox.Context, badClientDelay)
444func (c *conn) xtrace(level mlog.Level) func() {
450 c.tr.SetTrace(mlog.LevelTrace)
451 c.tw.SetTrace(mlog.LevelTrace)
455// Cache of line buffers for reading commands.
457var bufpool = moxio.NewBufpool(8, 16*1024)
459// read line from connection, not going through line channel.
460func (c *conn) readline0() (string, error) {
461 if c.slow && badClientDelay > 0 {
462 mox.Sleep(mox.Context, badClientDelay)
465 d := 30 * time.Minute
466 if c.state == stateNotAuthenticated {
469 err := c.conn.SetReadDeadline(time.Now().Add(d))
470 c.log.Check(err, "setting read deadline")
472 line, err := bufpool.Readline(c.br)
473 if err != nil && errors.Is(err, moxio.ErrLineTooLong) {
474 return "", fmt.Errorf("%s (%w)", err, errProtocol)
475 } else if err != nil {
476 return "", fmt.Errorf("%s (%w)", err, errIO)
481func (c *conn) lineChan() chan lineErr {
483 c.line = make(chan lineErr, 1)
485 line, err := c.readline0()
486 c.line <- lineErr{line, err}
492// readline from either the c.line channel, or otherwise read from connection.
493func (c *conn) readline(readCmd bool) string {
499 line, err = le.line, le.err
501 line, err = c.readline0()
504 if readCmd && errors.Is(err, os.ErrDeadlineExceeded) {
505 err := c.conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
506 c.log.Check(err, "setting write deadline")
507 c.writelinef("* BYE inactive")
509 if !errors.Is(err, errIO) && !errors.Is(err, errProtocol) {
510 err = fmt.Errorf("%s (%w)", err, errIO)
516 // We typically respond immediately (IDLE is an exception).
517 // The client may not be reading, or may have disappeared.
518 // Don't wait more than 5 minutes before closing down the connection.
519 // The write deadline is managed in IDLE as well.
520 // For unauthenticated connections, we require the client to read faster.
521 wd := 5 * time.Minute
522 if c.state == stateNotAuthenticated {
523 wd = 30 * time.Second
525 err = c.conn.SetWriteDeadline(time.Now().Add(wd))
526 c.log.Check(err, "setting write deadline")
531// write tagged command response, but first write pending changes.
532func (c *conn) writeresultf(format string, args ...any) {
533 c.bwriteresultf(format, args...)
537// write buffered tagged command response, but first write pending changes.
538func (c *conn) bwriteresultf(format string, args ...any) {
540 case "fetch", "store", "search":
544 c.applyChanges(c.comm.Get(), false)
547 c.bwritelinef(format, args...)
550func (c *conn) writelinef(format string, args ...any) {
551 c.bwritelinef(format, args...)
555// Buffer line for write.
556func (c *conn) bwritelinef(format string, args ...any) {
558 fmt.Fprintf(c.bw, format, args...)
561func (c *conn) xflush() {
563 xcheckf(err, "flush") // Should never happen, the Write caused by the Flush should panic on i/o error.
566func (c *conn) readCommand(tag *string) (cmd string, p *parser) {
567 line := c.readline(true)
568 p = newParser(line, c)
574 return cmd, newParser(p.remainder(), c)
577func (c *conn) xreadliteral(size int64, sync bool) string {
581 buf := make([]byte, size)
583 if err := c.conn.SetReadDeadline(time.Now().Add(30 * time.Second)); err != nil {
584 c.log.Errorx("setting read deadline", err)
587 _, err := io.ReadFull(c.br, buf)
589 // Cannot use xcheckf due to %w handling of errIO.
590 panic(fmt.Errorf("reading literal: %s (%w)", err, errIO))
596func (c *conn) xhighestModSeq(tx *bstore.Tx, mailboxID int64) store.ModSeq {
597 qms := bstore.QueryTx[store.Message](tx)
598 qms.FilterNonzero(store.Message{MailboxID: mailboxID})
599 qms.SortDesc("ModSeq")
602 if err == bstore.ErrAbsent {
603 return store.ModSeq(0)
605 xcheckf(err, "looking up highest modseq for mailbox")
609var cleanClose struct{} // Sentinel value for panic/recover indicating clean close of connection.
611func serve(listenerName string, cid int64, tlsConfig *tls.Config, nc net.Conn, xtls, noRequireSTARTTLS bool) {
613 if a, ok := nc.RemoteAddr().(*net.TCPAddr); ok {
616 // For net.Pipe, during tests.
617 remoteIP = net.ParseIP("127.0.0.10")
625 tlsConfig: tlsConfig,
627 noRequireSTARTTLS: noRequireSTARTTLS,
628 enabled: map[capability]bool{},
630 cmdStart: time.Now(),
632 c.log = xlog.MoreFields(func() []mlog.Pair {
635 mlog.Field("cid", c.cid),
636 mlog.Field("delta", now.Sub(c.lastlog)),
639 if c.username != "" {
640 l = append(l, mlog.Field("username", c.username))
644 c.tr = moxio.NewTraceReader(c.log, "C: ", c.conn)
645 c.tw = moxio.NewTraceWriter(c.log, "S: ", c)
646 // todo: tracing should be done on whatever comes out of c.br. the remote connection write a command plus data, and bufio can read it in one read, causing a command parser that sets the tracing level to data to have no effect. we are now typically logging sent messages, when mail clients append to the Sent mailbox.
647 c.br = bufio.NewReader(c.tr)
648 c.bw = bufio.NewWriter(c.tw)
650 // Many IMAP connections use IDLE to wait for new incoming messages. We'll enable
651 // keepalive to get a higher chance of the connection staying alive, or otherwise
652 // detecting broken connections early.
655 xconn = c.conn.(*tls.Conn).NetConn()
657 if tcpconn, ok := xconn.(*net.TCPConn); ok {
658 if err := tcpconn.SetKeepAlivePeriod(5 * time.Minute); err != nil {
659 c.log.Errorx("setting keepalive period", err)
660 } else if err := tcpconn.SetKeepAlive(true); err != nil {
661 c.log.Errorx("enabling keepalive", err)
665 c.log.Info("new connection", mlog.Field("remote", c.conn.RemoteAddr()), mlog.Field("local", c.conn.LocalAddr()), mlog.Field("tls", xtls), mlog.Field("listener", listenerName))
670 if c.account != nil {
672 err := c.account.Close()
673 c.xsanity(err, "close account")
679 if x == nil || x == cleanClose {
680 c.log.Info("connection closed")
681 } else if err, ok := x.(error); ok && isClosed(err) {
682 c.log.Infox("connection closed", err)
684 c.log.Error("unhandled panic", mlog.Field("err", x))
686 metrics.PanicInc(metrics.Imapserver)
691 case <-mox.Shutdown.Done():
693 c.writelinef("* BYE mox shutting down")
698 if !limiterConnectionrate.Add(c.remoteIP, time.Now(), 1) {
699 c.writelinef("* BYE connection rate from your ip or network too high, slow down please")
703 // If remote IP/network resulted in too many authentication failures, refuse to serve.
704 if !mox.LimiterFailedAuth.CanAdd(c.remoteIP, time.Now(), 1) {
705 metrics.AuthenticationRatelimitedInc("imap")
706 c.log.Debug("refusing connection due to many auth failures", mlog.Field("remoteip", c.remoteIP))
707 c.writelinef("* BYE too many auth failures")
711 if !limiterConnections.Add(c.remoteIP, time.Now(), 1) {
712 c.log.Debug("refusing connection due to many open connections", mlog.Field("remoteip", c.remoteIP))
713 c.writelinef("* BYE too many open connections from your ip or network")
716 defer limiterConnections.Add(c.remoteIP, time.Now(), -1)
718 // We register and unregister the original connection, in case it c.conn is
719 // replaced with a TLS connection later on.
720 mox.Connections.Register(nc, "imap", listenerName)
721 defer mox.Connections.Unregister(nc)
723 c.writelinef("* OK [CAPABILITY %s] mox imap", c.capabilities())
727 c.xflush() // For flushing errors, or possibly commands that did not flush explicitly.
731// isClosed returns whether i/o failed, typically because the connection is closed.
732// For connection errors, we often want to generate fewer logs.
733func isClosed(err error) bool {
734 return errors.Is(err, errIO) || errors.Is(err, errProtocol) || moxio.IsClosed(err)
737func (c *conn) command() {
738 var tag, cmd, cmdlow string
744 metricIMAPCommands.WithLabelValues(c.cmdMetric, result).Observe(float64(time.Since(c.cmdStart)) / float64(time.Second))
747 logFields := []mlog.Pair{
748 mlog.Field("cmd", c.cmd),
749 mlog.Field("duration", time.Since(c.cmdStart)),
754 if x == nil || x == cleanClose {
755 c.log.Debug("imap command done", logFields...)
764 c.log.Error("imap command panic", append([]mlog.Pair{mlog.Field("panic", x)}, logFields...)...)
769 var sxerr syntaxError
773 c.log.Infox("imap command ioerror", err, logFields...)
775 if errors.Is(err, errProtocol) {
779 } else if errors.As(err, &sxerr) {
782 // Other side is likely speaking something else than IMAP, send error message and
783 // stop processing because there is a good chance whatever they sent has multiple
785 c.writelinef("* BYE please try again speaking imap")
788 c.log.Debugx("imap command syntax error", sxerr.err, logFields...)
789 c.log.Info("imap syntax error", mlog.Field("lastline", c.lastLine))
790 fatal := strings.HasSuffix(c.lastLine, "+}")
792 err := c.conn.SetWriteDeadline(time.Now().Add(5 * time.Second))
793 c.log.Check(err, "setting write deadline")
795 if sxerr.line != "" {
796 c.bwritelinef("%s", sxerr.line)
799 if sxerr.code != "" {
800 code = "[" + sxerr.code + "] "
802 c.bwriteresultf("%s BAD %s%s unrecognized syntax/command: %v", tag, code, cmd, sxerr.errmsg)
805 panic(fmt.Errorf("aborting connection after syntax error for command with non-sync literal: %w", errProtocol))
807 } else if errors.As(err, &serr) {
808 result = "servererror"
809 c.log.Errorx("imap command server error", err, logFields...)
811 c.bwriteresultf("%s NO %s %v", tag, cmd, err)
812 } else if errors.As(err, &uerr) {
814 c.log.Debugx("imap command user error", err, logFields...)
816 c.bwriteresultf("%s NO [%s] %s %v", tag, uerr.code, cmd, err)
818 c.bwriteresultf("%s NO %s %v", tag, cmd, err)
821 // Other type of panic, we pass it on, aborting the connection.
823 c.log.Errorx("imap command panic", err, logFields...)
829 cmd, p = c.readCommand(&tag)
830 cmdlow = strings.ToLower(cmd)
832 c.cmdStart = time.Now()
833 c.cmdMetric = "(unrecognized)"
836 case <-mox.Shutdown.Done():
838 c.writelinef("* BYE shutting down")
843 fn := commands[cmdlow]
845 xsyntaxErrorf("unknown command %q", cmd)
850 // Check if command is allowed in this state.
851 if _, ok1 := commandsStateAny[cmdlow]; ok1 {
852 } else if _, ok2 := commandsStateNotAuthenticated[cmdlow]; ok2 && c.state == stateNotAuthenticated {
853 } else if _, ok3 := commandsStateAuthenticated[cmdlow]; ok3 && c.state == stateAuthenticated || c.state == stateSelected {
854 } else if _, ok4 := commandsStateSelected[cmdlow]; ok4 && c.state == stateSelected {
855 } else if ok1 || ok2 || ok3 || ok4 {
856 xuserErrorf("not allowed in this connection state")
858 xserverErrorf("unrecognized command")
864func (c *conn) broadcast(changes []store.Change) {
865 if len(changes) == 0 {
868 c.log.Debug("broadcast changes", mlog.Field("changes", changes))
869 c.comm.Broadcast(changes)
872// matchStringer matches a string against reference + mailbox patterns.
873type matchStringer interface {
874 MatchString(s string) bool
879// MatchString for noMatch always returns false.
880func (noMatch) MatchString(s string) bool {
884// xmailboxPatternMatcher returns a matcher for mailbox names given the reference and patterns.
885// Patterns can include "%" and "*", matching any character excluding and including a slash respectively.
886func xmailboxPatternMatcher(ref string, patterns []string) matchStringer {
887 if strings.HasPrefix(ref, "/") {
892 for _, pat := range patterns {
893 if strings.HasPrefix(pat, "/") {
899 s = path.Join(ref, pat)
902 // Fix casing for all Inbox paths.
903 first := strings.SplitN(s, "/", 2)[0]
904 if strings.EqualFold(first, "Inbox") {
905 s = "Inbox" + s[len("Inbox"):]
910 for _, c := range s {
916 rs += regexp.QuoteMeta(string(c))
919 subs = append(subs, rs)
925 rs := "^(" + strings.Join(subs, "|") + ")$"
926 re, err := regexp.Compile(rs)
927 xcheckf(err, "compiling regexp for mailbox patterns")
931func (c *conn) sequence(uid store.UID) msgseq {
932 return uidSearch(c.uids, uid)
935func uidSearch(uids []store.UID, uid store.UID) msgseq {
952func (c *conn) xsequence(uid store.UID) msgseq {
953 seq := c.sequence(uid)
955 xserverErrorf("unknown uid %d (%w)", uid, errProtocol)
960func (c *conn) sequenceRemove(seq msgseq, uid store.UID) {
962 if c.uids[i] != uid {
963 xserverErrorf(fmt.Sprintf("got uid %d at msgseq %d, expected uid %d", uid, seq, c.uids[i]))
965 copy(c.uids[i:], c.uids[i+1:])
966 c.uids = c.uids[:len(c.uids)-1]
972// add uid to the session. care must be taken that pending changes are fetched
973// while holding the account wlock, and applied before adding this uid, because
974// those pending changes may contain another new uid that has to be added first.
975func (c *conn) uidAppend(uid store.UID) {
976 if uidSearch(c.uids, uid) > 0 {
977 xserverErrorf("uid already present (%w)", errProtocol)
979 if len(c.uids) > 0 && uid < c.uids[len(c.uids)-1] {
980 xserverErrorf("new uid %d is smaller than last uid %d (%w)", uid, c.uids[len(c.uids)-1], errProtocol)
982 c.uids = append(c.uids, uid)
988// sanity check that uids are in ascending order.
989func checkUIDs(uids []store.UID) {
990 for i, uid := range uids {
991 if uid == 0 || i > 0 && uid <= uids[i-1] {
992 xserverErrorf("bad uids %v", uids)
997func (c *conn) xnumSetUIDs(isUID bool, nums numSet) []store.UID {
998 _, uids := c.xnumSetConditionUIDs(false, true, isUID, nums)
1002func (c *conn) xnumSetCondition(isUID bool, nums numSet) []any {
1003 uidargs, _ := c.xnumSetConditionUIDs(true, false, isUID, nums)
1007func (c *conn) xnumSetConditionUIDs(forDB, returnUIDs bool, isUID bool, nums numSet) ([]any, []store.UID) {
1008 if nums.searchResult {
1009 // Update previously stored UIDs. Some may have been deleted.
1010 // Once deleted a UID will never come back, so we'll just remove those uids.
1012 for _, uid := range c.searchResult {
1013 if uidSearch(c.uids, uid) > 0 {
1014 c.searchResult[o] = uid
1018 c.searchResult = c.searchResult[:o]
1019 uidargs := make([]any, len(c.searchResult))
1020 for i, uid := range c.searchResult {
1023 return uidargs, c.searchResult
1027 var uids []store.UID
1029 add := func(uid store.UID) {
1031 uidargs = append(uidargs, uid)
1034 uids = append(uids, uid)
1039 // Sequence numbers that don't exist, or * on an empty mailbox, should result in a BAD response.
../rfc/9051:7018
1040 for _, r := range nums.ranges {
1043 if len(c.uids) == 0 {
1044 xsyntaxErrorf("invalid seqset * on empty mailbox")
1046 ia = len(c.uids) - 1
1048 ia = int(r.first.number - 1)
1049 if ia >= len(c.uids) {
1050 xsyntaxErrorf("msgseq %d not in mailbox", r.first.number)
1059 if len(c.uids) == 0 {
1060 xsyntaxErrorf("invalid seqset * on empty mailbox")
1062 ib = len(c.uids) - 1
1064 ib = int(r.last.number - 1)
1065 if ib >= len(c.uids) {
1066 xsyntaxErrorf("msgseq %d not in mailbox", r.last.number)
1072 for _, uid := range c.uids[ia : ib+1] {
1076 return uidargs, uids
1079 // UIDs that do not exist can be ignored.
1080 if len(c.uids) == 0 {
1084 for _, r := range nums.ranges {
1090 uida := store.UID(r.first.number)
1092 uida = c.uids[len(c.uids)-1]
1095 uidb := store.UID(last.number)
1097 uidb = c.uids[len(c.uids)-1]
1101 uida, uidb = uidb, uida
1104 // Binary search for uida.
1109 if uida < c.uids[m] {
1111 } else if uida > c.uids[m] {
1118 for _, uid := range c.uids[s:] {
1119 if uid >= uida && uid <= uidb {
1121 } else if uid > uidb {
1127 return uidargs, uids
1130func (c *conn) ok(tag, cmd string) {
1131 c.bwriteresultf("%s OK %s done", tag, cmd)
1135// xcheckmailboxname checks if name is valid, returning an INBOX-normalized name.
1136// I.e. it changes various casings of INBOX and INBOX/* to Inbox and Inbox/*.
1137// Name is invalid if it contains leading/trailing/double slashes, or when it isn't
1138// unicode-normalized, or when empty or has special characters.
1139func xcheckmailboxname(name string, allowInbox bool) string {
1140 name, isinbox, err := store.CheckMailboxName(name, allowInbox)
1142 xuserErrorf("special mailboxname Inbox not allowed")
1143 } else if err != nil {
1144 xusercodeErrorf("CANNOT", err.Error())
1149// Lookup mailbox by name.
1150// If the mailbox does not exist, panic is called with a user error.
1151// Must be called with account rlock held.
1152func (c *conn) xmailbox(tx *bstore.Tx, name string, missingErrCode string) store.Mailbox {
1153 mb, err := c.account.MailboxFind(tx, name)
1154 xcheckf(err, "finding mailbox")
1156 // missingErrCode can be empty, or e.g. TRYCREATE or ALREADYEXISTS.
1157 xusercodeErrorf(missingErrCode, "%w", store.ErrUnknownMailbox)
1162// Lookup mailbox by ID.
1163// If the mailbox does not exist, panic is called with a user error.
1164// Must be called with account rlock held.
1165func (c *conn) xmailboxID(tx *bstore.Tx, id int64) store.Mailbox {
1166 mb := store.Mailbox{ID: id}
1168 if err == bstore.ErrAbsent {
1169 xuserErrorf("%w", store.ErrUnknownMailbox)
1174// Apply changes to our session state.
1175// If initial is false, updates like EXISTS and EXPUNGE are written to the client.
1176// If initial is true, we only apply the changes.
1177// Should not be called while holding locks, as changes are written to client connections, which can block.
1178// Does not flush output.
1179func (c *conn) applyChanges(changes []store.Change, initial bool) {
1180 if len(changes) == 0 {
1184 err := c.conn.SetWriteDeadline(time.Now().Add(5 * time.Minute))
1185 c.log.Check(err, "setting write deadline")
1187 c.log.Debug("applying changes", mlog.Field("changes", changes))
1189 // Only keep changes for the selected mailbox, and changes that are always relevant.
1190 var n []store.Change
1191 for _, change := range changes {
1193 switch ch := change.(type) {
1194 case store.ChangeAddUID:
1196 case store.ChangeRemoveUIDs:
1198 case store.ChangeFlags:
1200 case store.ChangeRemoveMailbox, store.ChangeAddMailbox, store.ChangeRenameMailbox, store.ChangeAddSubscription:
1201 n = append(n, change)
1203 case store.ChangeMailboxCounts, store.ChangeMailboxSpecialUse, store.ChangeMailboxKeywords, store.ChangeThread:
1205 panic(fmt.Errorf("missing case for %#v", change))
1207 if c.state == stateSelected && mbID == c.mailboxID {
1208 n = append(n, change)
1213 qresync := c.enabled[capQresync]
1214 condstore := c.enabled[capCondstore]
1217 for i < len(changes) {
1218 // First process all new uids. So we only send a single EXISTS.
1219 var adds []store.ChangeAddUID
1220 for ; i < len(changes); i++ {
1221 ch, ok := changes[i].(store.ChangeAddUID)
1225 seq := c.sequence(ch.UID)
1226 if seq > 0 && initial {
1230 adds = append(adds, ch)
1236 // Write the exists, and the UID and flags as well. Hopefully the client waits for
1237 // long enough after the EXISTS to see these messages, and doesn't request them
1238 // again with a FETCH.
1239 c.bwritelinef("* %d EXISTS", len(c.uids))
1240 for _, add := range adds {
1241 seq := c.xsequence(add.UID)
1242 var modseqStr string
1244 modseqStr = fmt.Sprintf(" MODSEQ (%d)", add.ModSeq.Client())
1246 c.bwritelinef("* %d FETCH (UID %d FLAGS %s%s)", seq, add.UID, flaglist(add.Flags, add.Keywords).pack(c), modseqStr)
1251 change := changes[i]
1254 switch ch := change.(type) {
1255 case store.ChangeRemoveUIDs:
1256 var vanishedUIDs numSet
1257 for _, uid := range ch.UIDs {
1260 seq = c.sequence(uid)
1265 seq = c.xsequence(uid)
1267 c.sequenceRemove(seq, uid)
1270 vanishedUIDs.append(uint32(uid))
1272 c.bwritelinef("* %d EXPUNGE", seq)
1278 for _, s := range vanishedUIDs.Strings(4*1024 - 32) {
1279 c.bwritelinef("* VANISHED %s", s)
1282 case store.ChangeFlags:
1283 // The uid can be unknown if we just expunged it while another session marked it as deleted just before.
1284 seq := c.sequence(ch.UID)
1289 var modseqStr string
1291 modseqStr = fmt.Sprintf(" MODSEQ (%d)", ch.ModSeq.Client())
1293 c.bwritelinef("* %d FETCH (UID %d FLAGS %s%s)", seq, ch.UID, flaglist(ch.Flags, ch.Keywords).pack(c), modseqStr)
1295 case store.ChangeRemoveMailbox:
1296 // Only announce \NonExistent to modern clients, otherwise they may ignore the
1297 // unrecognized \NonExistent and interpret this as a newly created mailbox, while
1298 // the goal was to remove it...
1299 if c.enabled[capIMAP4rev2] {
1300 c.bwritelinef(`* LIST (\NonExistent) "/" %s`, astring(ch.Name).pack(c))
1302 case store.ChangeAddMailbox:
1303 c.bwritelinef(`* LIST (%s) "/" %s`, strings.Join(ch.Flags, " "), astring(ch.Mailbox.Name).pack(c))
1304 case store.ChangeRenameMailbox:
1305 c.bwritelinef(`* LIST (%s) "/" %s ("OLDNAME" (%s))`, strings.Join(ch.Flags, " "), astring(ch.NewName).pack(c), string0(ch.OldName).pack(c))
1306 case store.ChangeAddSubscription:
1307 c.bwritelinef(`* LIST (%s) "/" %s`, strings.Join(append([]string{`\Subscribed`}, ch.Flags...), " "), astring(ch.Name).pack(c))
1309 panic(fmt.Sprintf("internal error, missing case for %#v", change))
1314// Capability returns the capabilities this server implements and currently has
1315// available given the connection state.
1318func (c *conn) cmdCapability(tag, cmd string, p *parser) {
1324 caps := c.capabilities()
1327 c.bwritelinef("* CAPABILITY %s", caps)
1331// capabilities returns non-empty string with available capabilities based on connection state.
1332// For use in cmdCapability and untagged OK responses on connection start, login and authenticate.
1333func (c *conn) capabilities() string {
1334 caps := serverCapabilities
1336 // We only allow starting without TLS when explicitly configured, in violation of RFC.
1337 if !c.tls && c.tlsConfig != nil {
1340 if c.tls || c.noRequireSTARTTLS {
1341 caps += " AUTH=PLAIN"
1343 caps += " LOGINDISABLED"
1348// No op, but useful for retrieving pending changes as untagged responses, e.g. of
1352func (c *conn) cmdNoop(tag, cmd string, p *parser) {
1360// Logout, after which server closes the connection.
1363func (c *conn) cmdLogout(tag, cmd string, p *parser) {
1370 c.state = stateNotAuthenticated
1372 c.bwritelinef("* BYE thanks")
1377// Clients can use ID to tell the server which software they are using. Servers can
1378// respond with their version. For statistics/logging/debugging purposes.
1381func (c *conn) cmdID(tag, cmd string, p *parser) {
1386 var params map[string]string
1388 params = map[string]string{}
1390 if len(params) > 0 {
1396 if _, ok := params[k]; ok {
1397 xsyntaxErrorf("duplicate key %q", k)
1406 // We just log the client id.
1407 c.log.Info("client id", mlog.Field("params", params))
1411 c.bwritelinef(`* ID ("name" "mox" "version" %s)`, string0(moxvar.Version).pack(c))
1415// STARTTLS enables TLS on the connection, after a plain text start.
1416// Only allowed if TLS isn't already enabled, either through connecting to a
1417// TLS-enabled TCP port, or a previous STARTTLS command.
1418// After STARTTLS, plain text authentication typically becomes available.
1420// Status: Not authenticated.
1421func (c *conn) cmdStarttls(tag, cmd string, p *parser) {
1432 if n := c.br.Buffered(); n > 0 {
1433 buf := make([]byte, n)
1434 _, err := io.ReadFull(c.br, buf)
1435 xcheckf(err, "reading buffered data for tls handshake")
1436 conn = &prefixConn{buf, conn}
1438 // We add the cid to facilitate debugging in case of TLS connection failure.
1439 c.ok(tag, cmd+" ("+mox.ReceivedID(c.cid)+")")
1441 cidctx := context.WithValue(mox.Context, mlog.CidKey, c.cid)
1442 ctx, cancel := context.WithTimeout(cidctx, time.Minute)
1444 tlsConn := tls.Server(conn, c.tlsConfig)
1445 c.log.Debug("starting tls server handshake")
1446 if err := tlsConn.HandshakeContext(ctx); err != nil {
1447 panic(fmt.Errorf("starttls handshake: %s (%w)", err, errIO))
1450 tlsversion, ciphersuite := mox.TLSInfo(tlsConn)
1451 c.log.Debug("tls server handshake done", mlog.Field("tls", tlsversion), mlog.Field("ciphersuite", ciphersuite))
1454 c.tr = moxio.NewTraceReader(c.log, "C: ", c.conn)
1455 c.tw = moxio.NewTraceWriter(c.log, "S: ", c)
1456 c.br = bufio.NewReader(c.tr)
1457 c.bw = bufio.NewWriter(c.tw)
1461// Authenticate using SASL. Supports multiple back and forths between client and
1462// server to finish authentication, unlike LOGIN which is just a single
1463// username/password.
1465// Status: Not authenticated.
1466func (c *conn) cmdAuthenticate(tag, cmd string, p *parser) {
1470 // For many failed auth attempts, slow down verification attempts.
1471 if c.authFailed > 3 && authFailDelay > 0 {
1472 mox.Sleep(mox.Context, time.Duration(c.authFailed-3)*authFailDelay)
1474 c.authFailed++ // Compensated on success.
1476 // On the 3rd failed authentication, start responding slowly. Successful auth will
1477 // cause fast responses again.
1478 if c.authFailed >= 3 {
1483 var authVariant string
1484 authResult := "error"
1486 metrics.AuthenticationInc("imap", authVariant, authResult)
1489 mox.LimiterFailedAuth.Reset(c.remoteIP, time.Now())
1491 mox.LimiterFailedAuth.Add(c.remoteIP, time.Now(), 1)
1497 authType := p.xatom()
1499 xreadInitial := func() []byte {
1503 line = c.readline(false)
1507 line = p.remainder()
1510 line = "" // Base64 decode will result in empty buffer.
1515 authResult = "aborted"
1516 xsyntaxErrorf("authenticate aborted by client")
1518 buf, err := base64.StdEncoding.DecodeString(line)
1520 xsyntaxErrorf("parsing base64: %v", err)
1525 xreadContinuation := func() []byte {
1526 line := c.readline(false)
1528 authResult = "aborted"
1529 xsyntaxErrorf("authenticate aborted by client")
1531 buf, err := base64.StdEncoding.DecodeString(line)
1533 xsyntaxErrorf("parsing base64: %v", err)
1538 switch strings.ToUpper(authType) {
1540 authVariant = "plain"
1542 if !c.noRequireSTARTTLS && !c.tls {
1544 xusercodeErrorf("PRIVACYREQUIRED", "tls required for login")
1547 // Plain text passwords, mark as traceauth.
1548 defer c.xtrace(mlog.LevelTraceauth)()
1549 buf := xreadInitial()
1550 c.xtrace(mlog.LevelTrace) // Restore.
1551 plain := bytes.Split(buf, []byte{0})
1552 if len(plain) != 3 {
1553 xsyntaxErrorf("bad plain auth data, expected 3 nul-separated tokens, got %d tokens", len(plain))
1555 authz := string(plain[0])
1556 authc := string(plain[1])
1557 password := string(plain[2])
1559 if authz != "" && authz != authc {
1560 xusercodeErrorf("AUTHORIZATIONFAILED", "cannot assume role")
1563 acc, err := store.OpenEmailAuth(authc, password)
1565 if errors.Is(err, store.ErrUnknownCredentials) {
1566 authResult = "badcreds"
1567 c.log.Info("authentication failed", mlog.Field("username", authc))
1568 xusercodeErrorf("AUTHENTICATIONFAILED", "bad credentials")
1570 xusercodeErrorf("", "error")
1576 authVariant = strings.ToLower(authType)
1582 chal := fmt.Sprintf("<%d.%d@%s>", uint64(mox.CryptoRandInt()), time.Now().UnixNano(), mox.Conf.Static.HostnameDomain.ASCII)
1583 c.writelinef("+ %s", base64.StdEncoding.EncodeToString([]byte(chal)))
1585 resp := xreadContinuation()
1586 t := strings.Split(string(resp), " ")
1587 if len(t) != 2 || len(t[1]) != 2*md5.Size {
1588 xsyntaxErrorf("malformed cram-md5 response")
1591 c.log.Debug("cram-md5 auth", mlog.Field("address", addr))
1592 acc, _, err := store.OpenEmail(addr)
1594 if errors.Is(err, store.ErrUnknownCredentials) {
1595 c.log.Info("failed authentication attempt", mlog.Field("username", addr), mlog.Field("remote", c.remoteIP))
1596 xusercodeErrorf("AUTHENTICATIONFAILED", "bad credentials")
1598 xserverErrorf("looking up address: %v", err)
1603 c.xsanity(err, "close account")
1606 var ipadhash, opadhash hash.Hash
1607 acc.WithRLock(func() {
1608 err := acc.DB.Read(context.TODO(), func(tx *bstore.Tx) error {
1609 password, err := bstore.QueryTx[store.Password](tx).Get()
1610 if err == bstore.ErrAbsent {
1611 c.log.Info("failed authentication attempt", mlog.Field("username", addr), mlog.Field("remote", c.remoteIP))
1612 xusercodeErrorf("AUTHENTICATIONFAILED", "bad credentials")
1618 ipadhash = password.CRAMMD5.Ipad
1619 opadhash = password.CRAMMD5.Opad
1622 xcheckf(err, "tx read")
1624 if ipadhash == nil || opadhash == nil {
1625 c.log.Info("cram-md5 auth attempt without derived secrets set, save password again to store secrets", mlog.Field("username", addr))
1626 c.log.Info("failed authentication attempt", mlog.Field("username", addr), mlog.Field("remote", c.remoteIP))
1627 xusercodeErrorf("AUTHENTICATIONFAILED", "bad credentials")
1631 ipadhash.Write([]byte(chal))
1632 opadhash.Write(ipadhash.Sum(nil))
1633 digest := fmt.Sprintf("%x", opadhash.Sum(nil))
1635 c.log.Info("failed authentication attempt", mlog.Field("username", addr), mlog.Field("remote", c.remoteIP))
1636 xusercodeErrorf("AUTHENTICATIONFAILED", "bad credentials")
1640 acc = nil // Cancel cleanup.
1643 case "SCRAM-SHA-1", "SCRAM-SHA-256":
1644 // todo: improve handling of errors during scram. e.g. invalid parameters. should we abort the imap command, or continue until the end and respond with a scram-level error?
1645 // todo: use single implementation between ../imapserver/server.go and ../smtpserver/server.go
1647 authVariant = strings.ToLower(authType)
1648 var h func() hash.Hash
1649 if authVariant == "scram-sha-1" {
1655 // No plaintext credentials, we can log these normally.
1657 c0 := xreadInitial()
1658 ss, err := scram.NewServer(h, c0)
1660 xsyntaxErrorf("starting scram: %s", err)
1662 c.log.Debug("scram auth", mlog.Field("authentication", ss.Authentication))
1663 acc, _, err := store.OpenEmail(ss.Authentication)
1665 // todo: we could continue scram with a generated salt, deterministically generated
1666 // from the username. that way we don't have to store anything but attackers cannot
1667 // learn if an account exists. same for absent scram saltedpassword below.
1668 xuserErrorf("scram not possible")
1673 c.xsanity(err, "close account")
1676 if ss.Authorization != "" && ss.Authorization != ss.Authentication {
1677 xuserErrorf("authentication with authorization for different user not supported")
1679 var xscram store.SCRAM
1680 acc.WithRLock(func() {
1681 err := acc.DB.Read(context.TODO(), func(tx *bstore.Tx) error {
1682 password, err := bstore.QueryTx[store.Password](tx).Get()
1683 if authVariant == "scram-sha-1" {
1684 xscram = password.SCRAMSHA1
1686 xscram = password.SCRAMSHA256
1688 if err == bstore.ErrAbsent || err == nil && (len(xscram.Salt) == 0 || xscram.Iterations == 0 || len(xscram.SaltedPassword) == 0) {
1689 c.log.Info("scram auth attempt without derived secrets set, save password again to store secrets", mlog.Field("address", ss.Authentication))
1690 xuserErrorf("scram not possible")
1692 xcheckf(err, "fetching credentials")
1695 xcheckf(err, "read tx")
1697 s1, err := ss.ServerFirst(xscram.Iterations, xscram.Salt)
1698 xcheckf(err, "scram first server step")
1699 c.writelinef("+ %s", base64.StdEncoding.EncodeToString([]byte(s1)))
1700 c2 := xreadContinuation()
1701 s3, err := ss.Finish(c2, xscram.SaltedPassword)
1703 c.writelinef("+ %s", base64.StdEncoding.EncodeToString([]byte(s3)))
1706 c.readline(false) // Should be "*" for cancellation.
1707 if errors.Is(err, scram.ErrInvalidProof) {
1708 authResult = "badcreds"
1709 c.log.Info("failed authentication attempt", mlog.Field("username", ss.Authentication), mlog.Field("remote", c.remoteIP))
1710 xusercodeErrorf("AUTHENTICATIONFAILED", "bad credentials")
1712 xuserErrorf("server final: %w", err)
1716 // The message should be empty. todo: should we require it is empty?
1720 acc = nil // Cancel cleanup.
1721 c.username = ss.Authentication
1724 xuserErrorf("method not supported")
1730 c.comm = store.RegisterComm(c.account)
1731 c.state = stateAuthenticated
1732 c.writeresultf("%s OK [CAPABILITY %s] authenticate done", tag, c.capabilities())
1735// Login logs in with username and password.
1737// Status: Not authenticated.
1738func (c *conn) cmdLogin(tag, cmd string, p *parser) {
1741 authResult := "error"
1743 metrics.AuthenticationInc("imap", "login", authResult)
1746 // todo: get this line logged with traceauth. the plaintext password is included on the command line, which we've already read (before dispatching to this function).
1750 userid := p.xastring()
1752 password := p.xastring()
1755 if !c.noRequireSTARTTLS && !c.tls {
1757 xusercodeErrorf("PRIVACYREQUIRED", "tls required for login")
1760 // For many failed auth attempts, slow down verification attempts.
1761 if c.authFailed > 3 && authFailDelay > 0 {
1762 mox.Sleep(mox.Context, time.Duration(c.authFailed-3)*authFailDelay)
1764 c.authFailed++ // Compensated on success.
1766 // On the 3rd failed authentication, start responding slowly. Successful auth will
1767 // cause fast responses again.
1768 if c.authFailed >= 3 {
1773 acc, err := store.OpenEmailAuth(userid, password)
1775 authResult = "badcreds"
1777 if errors.Is(err, store.ErrUnknownCredentials) {
1778 code = "AUTHENTICATIONFAILED"
1779 c.log.Info("failed authentication attempt", mlog.Field("username", userid), mlog.Field("remote", c.remoteIP))
1781 xusercodeErrorf(code, "login failed")
1787 c.comm = store.RegisterComm(acc)
1788 c.state = stateAuthenticated
1790 c.writeresultf("%s OK [CAPABILITY %s] login done", tag, c.capabilities())
1793// Enable explicitly opts in to an extension. A server can typically send new kinds
1794// of responses to a client. Most extensions do not require an ENABLE because a
1795// client implicitly opts in to new response syntax by making a requests that uses
1796// new optional extension request syntax.
1798// State: Authenticated and selected.
1799func (c *conn) cmdEnable(tag, cmd string, p *parser) {
1805 caps := []string{p.xatom()}
1808 caps = append(caps, p.xatom())
1811 // Clients should only send capabilities that need enabling.
1812 // We should only echo that we recognize as needing enabling.
1815 for _, s := range caps {
1816 cap := capability(strings.ToUpper(s))
1821 c.enabled[cap] = true
1824 c.enabled[cap] = true
1830 if qresync && !c.enabled[capCondstore] {
1831 c.xensureCondstore(nil)
1832 enabled += " CONDSTORE"
1836 c.bwritelinef("* ENABLED%s", enabled)
1841// If a mailbox is selected, an untagged OK with HIGHESTMODSEQ is written to the
1842// client. If tx is non-nil, it is used to read the HIGHESTMODSEQ from the
1843// database. Otherwise a new read-only transaction is created.
1844func (c *conn) xensureCondstore(tx *bstore.Tx) {
1845 if !c.enabled[capCondstore] {
1846 c.enabled[capCondstore] = true
1847 // todo spec: can we send an untagged enabled response?
1849 if c.mailboxID <= 0 {
1852 var modseq store.ModSeq
1854 modseq = c.xhighestModSeq(tx, c.mailboxID)
1856 c.xdbread(func(tx *bstore.Tx) {
1857 modseq = c.xhighestModSeq(tx, c.mailboxID)
1860 c.bwritelinef("* OK [HIGHESTMODSEQ %d] after condstore-enabling command", modseq.Client())
1864// State: Authenticated and selected.
1865func (c *conn) cmdSelect(tag, cmd string, p *parser) {
1866 c.cmdSelectExamine(true, tag, cmd, p)
1869// State: Authenticated and selected.
1870func (c *conn) cmdExamine(tag, cmd string, p *parser) {
1871 c.cmdSelectExamine(false, tag, cmd, p)
1874// Select and examine are almost the same commands. Select just opens a mailbox for
1875// read/write and examine opens a mailbox readonly.
1877// State: Authenticated and selected.
1878func (c *conn) cmdSelectExamine(isselect bool, tag, cmd string, p *parser) {
1886 name := p.xmailbox()
1888 var qruidvalidity uint32
1889 var qrmodseq int64 // QRESYNC required parameters.
1890 var qrknownUIDs, qrknownSeqSet, qrknownUIDSet *numSet // QRESYNC optional parameters.
1892 seen := map[string]bool{}
1894 for len(seen) == 0 || !p.take(")") {
1895 w := p.xtakelist("CONDSTORE", "QRESYNC")
1897 xsyntaxErrorf("duplicate select parameter %s", w)
1907 // Note: unlike with CONDSTORE, there are no QRESYNC-related commands/parameters
1908 // that enable capabilities.
1909 if !c.enabled[capQresync] {
1911 xsyntaxErrorf("QRESYNC must first be enabled")
1917 qrmodseq = p.xnznumber64()
1919 seqMatchData := p.take("(")
1923 seqMatchData = p.take(" (")
1926 ss0 := p.xnumSet0(false, false)
1927 qrknownSeqSet = &ss0
1929 ss1 := p.xnumSet0(false, false)
1930 qrknownUIDSet = &ss1
1936 panic("missing case for select param " + w)
1942 // Deselect before attempting the new select. This means we will deselect when an
1943 // error occurs during select.
1945 if c.state == stateSelected {
1947 c.bwritelinef("* OK [CLOSED] x")
1951 name = xcheckmailboxname(name, true)
1953 var highestModSeq store.ModSeq
1954 var highDeletedModSeq store.ModSeq
1955 var firstUnseen msgseq = 0
1956 var mb store.Mailbox
1957 c.account.WithRLock(func() {
1958 c.xdbread(func(tx *bstore.Tx) {
1959 mb = c.xmailbox(tx, name, "")
1961 q := bstore.QueryTx[store.Message](tx)
1962 q.FilterNonzero(store.Message{MailboxID: mb.ID})
1963 q.FilterEqual("Expunged", false)
1965 c.uids = []store.UID{}
1967 err := q.ForEach(func(m store.Message) error {
1968 c.uids = append(c.uids, m.UID)
1969 if firstUnseen == 0 && !m.Seen {
1978 xcheckf(err, "fetching uids")
1980 // Condstore extension, find the highest modseq.
1981 if c.enabled[capCondstore] {
1982 highestModSeq = c.xhighestModSeq(tx, mb.ID)
1984 // For QRESYNC, we need to know the highest modset of deleted expunged records to
1985 // maintain synchronization.
1986 if c.enabled[capQresync] {
1987 highDeletedModSeq, err = c.account.HighestDeletedModSeq(tx)
1988 xcheckf(err, "getting highest deleted modseq")
1992 c.applyChanges(c.comm.Get(), true)
1995 if len(mb.Keywords) > 0 {
1996 flags = " " + strings.Join(mb.Keywords, " ")
1998 c.bwritelinef(`* FLAGS (\Seen \Answered \Flagged \Deleted \Draft $Forwarded $Junk $NotJunk $Phishing $MDNSent%s)`, flags)
1999 c.bwritelinef(`* OK [PERMANENTFLAGS (\Seen \Answered \Flagged \Deleted \Draft $Forwarded $Junk $NotJunk $Phishing $MDNSent \*)] x`)
2000 if !c.enabled[capIMAP4rev2] {
2001 c.bwritelinef(`* 0 RECENT`)
2003 c.bwritelinef(`* %d EXISTS`, len(c.uids))
2004 if !c.enabled[capIMAP4rev2] && firstUnseen > 0 {
2006 c.bwritelinef(`* OK [UNSEEN %d] x`, firstUnseen)
2008 c.bwritelinef(`* OK [UIDVALIDITY %d] x`, mb.UIDValidity)
2009 c.bwritelinef(`* OK [UIDNEXT %d] x`, mb.UIDNext)
2010 c.bwritelinef(`* LIST () "/" %s`, astring(mb.Name).pack(c))
2011 if c.enabled[capCondstore] {
2014 c.bwritelinef(`* OK [HIGHESTMODSEQ %d] x`, highestModSeq.Client())
2018 if qruidvalidity == mb.UIDValidity {
2019 // We send the vanished UIDs at the end, so we can easily combine the modseq
2020 // changes and vanished UIDs that result from that, with the vanished UIDs from the
2021 // case where we don't store enough history.
2022 vanishedUIDs := map[store.UID]struct{}{}
2024 var preVanished store.UID
2025 var oldClientUID store.UID
2026 // If samples of known msgseq and uid pairs are given (they must be in order), we
2027 // use them to determine the earliest UID for which we send VANISHED responses.
2029 if qrknownSeqSet != nil {
2030 if !qrknownSeqSet.isBasicIncreasing() {
2031 xuserErrorf("QRESYNC known message sequence set must be numeric and strictly increasing")
2033 if !qrknownUIDSet.isBasicIncreasing() {
2034 xuserErrorf("QRESYNC known uid set must be numeric and strictly increasing")
2036 seqiter := qrknownSeqSet.newIter()
2037 uiditer := qrknownUIDSet.newIter()
2039 msgseq, ok0 := seqiter.Next()
2040 uid, ok1 := uiditer.Next()
2043 } else if !ok0 || !ok1 {
2044 xsyntaxErrorf("invalid combination of known sequence set and uid set, must be of equal length")
2046 i := int(msgseq - 1)
2047 if i < 0 || i >= len(c.uids) || c.uids[i] != store.UID(uid) {
2048 if uidSearch(c.uids, store.UID(uid)) <= 0 {
2049 // We will check this old client UID for consistency below.
2050 oldClientUID = store.UID(uid)
2054 preVanished = store.UID(uid + 1)
2058 // We gather vanished UIDs and report them at the end. This seems OK because we
2059 // already sent HIGHESTMODSEQ, and a client should know not to commit that value
2060 // until after it has seen the tagged OK of this command. The RFC has a remark
2061 // about ordering of some untagged responses, it's not immediately clear what it
2062 // means, but given the examples appears to allude to servers that decide to not
2063 // send expunge/vanished before the tagged OK.
2066 // We are reading without account lock. Similar to when we process FETCH/SEARCH
2067 // requests. We don't have to reverify existence of the mailbox, so we don't
2068 // rlock, even briefly.
2069 c.xdbread(func(tx *bstore.Tx) {
2070 if oldClientUID > 0 {
2071 // The client sent a UID that is now removed. This is typically fine. But we check
2072 // that it is consistent with the modseq the client sent. If the UID already didn't
2073 // exist at that modseq, the client may be missing some information.
2074 q := bstore.QueryTx[store.Message](tx)
2075 q.FilterNonzero(store.Message{MailboxID: mb.ID, UID: oldClientUID})
2078 // If client claims to be up to date up to and including qrmodseq, and the message
2079 // was deleted at or before that time, we send changes from just before that
2080 // modseq, and we send vanished for all UIDs.
2081 if m.Expunged && qrmodseq >= m.ModSeq.Client() {
2082 qrmodseq = m.ModSeq.Client() - 1
2085 c.bwritelinef("* OK [ALERT] Synchronization inconsistency in client detected. Client tried to sync with a UID that was removed at or after the MODSEQ it sent in the request. Sending all historic message removals for selected mailbox. Full synchronization recommended.")
2087 } else if err != bstore.ErrAbsent {
2088 xcheckf(err, "checking old client uid")
2092 q := bstore.QueryTx[store.Message](tx)
2093 q.FilterNonzero(store.Message{MailboxID: mb.ID})
2094 // Note: we don't filter by Expunged.
2095 q.FilterGreater("ModSeq", store.ModSeqFromClient(qrmodseq))
2096 q.FilterLessEqual("ModSeq", highestModSeq)
2098 err := q.ForEach(func(m store.Message) error {
2099 if m.Expunged && m.UID < preVanished {
2103 if qrknownUIDs != nil && !qrknownUIDs.contains(uint32(m.UID)) {
2107 vanishedUIDs[m.UID] = struct{}{}
2110 msgseq := c.sequence(m.UID)
2112 c.bwritelinef("* %d FETCH (UID %d FLAGS %s MODSEQ (%d))", msgseq, m.UID, flaglist(m.Flags, m.Keywords).pack(c), m.ModSeq.Client())
2116 xcheckf(err, "listing changed messages")
2119 // Add UIDs from client's known UID set to vanished list if we don't have enough history.
2120 if qrmodseq < highDeletedModSeq.Client() {
2121 // If no known uid set was in the request, we substitute 1:max or the empty set.
2123 if qrknownUIDs == nil {
2124 if len(c.uids) > 0 {
2125 qrknownUIDs = &numSet{ranges: []numRange{{first: setNumber{number: 1}, last: &setNumber{number: uint32(c.uids[len(c.uids)-1])}}}}
2127 qrknownUIDs = &numSet{}
2131 iter := qrknownUIDs.newIter()
2133 v, ok := iter.Next()
2137 if c.sequence(store.UID(v)) <= 0 {
2138 vanishedUIDs[store.UID(v)] = struct{}{}
2143 // Now that we have all vanished UIDs, send them over compactly.
2144 if len(vanishedUIDs) > 0 {
2145 l := maps.Keys(vanishedUIDs)
2146 sort.Slice(l, func(i, j int) bool {
2150 for _, s := range compactUIDSet(l).Strings(4*1024 - 32) {
2151 c.bwritelinef("* VANISHED (EARLIER) %s", s)
2157 c.bwriteresultf("%s OK [READ-WRITE] x", tag)
2160 c.bwriteresultf("%s OK [READ-ONLY] x", tag)
2164 c.state = stateSelected
2165 c.searchResult = nil
2169// Create makes a new mailbox, and its parents too if absent.
2171// State: Authenticated and selected.
2172func (c *conn) cmdCreate(tag, cmd string, p *parser) {
2178 name := p.xmailbox()
2184 name = xcheckmailboxname(name, false)
2186 var changes []store.Change
2187 var created []string // Created mailbox names.
2189 c.account.WithWLock(func() {
2190 c.xdbwrite(func(tx *bstore.Tx) {
2193 changes, created, exists, err = c.account.MailboxCreate(tx, name)
2196 xuserErrorf("mailbox already exists")
2198 xcheckf(err, "creating mailbox")
2201 c.broadcast(changes)
2204 for _, n := range created {
2206 if n == name && name != origName && !(name == "Inbox" || strings.HasPrefix(name, "Inbox/")) {
2207 more = fmt.Sprintf(` ("OLDNAME" (%s))`, string0(origName).pack(c))
2209 c.bwritelinef(`* LIST (\Subscribed) "/" %s%s`, astring(n).pack(c), more)
2214// Delete removes a mailbox and all its messages.
2215// Inbox cannot be removed.
2217// State: Authenticated and selected.
2218func (c *conn) cmdDelete(tag, cmd string, p *parser) {
2224 name := p.xmailbox()
2227 name = xcheckmailboxname(name, false)
2229 // Messages to remove after having broadcasted the removal of messages.
2230 var removeMessageIDs []int64
2232 c.account.WithWLock(func() {
2233 var mb store.Mailbox
2234 var changes []store.Change
2236 c.xdbwrite(func(tx *bstore.Tx) {
2237 mb = c.xmailbox(tx, name, "NONEXISTENT")
2239 var hasChildren bool
2241 changes, removeMessageIDs, hasChildren, err = c.account.MailboxDelete(context.TODO(), c.log, tx, mb)
2243 xusercodeErrorf("HASCHILDREN", "mailbox has a child, only leaf mailboxes can be deleted")
2245 xcheckf(err, "deleting mailbox")
2248 c.broadcast(changes)
2251 for _, mID := range removeMessageIDs {
2252 p := c.account.MessagePath(mID)
2254 c.log.Check(err, "removing message file for mailbox delete", mlog.Field("path", p))
2260// Rename changes the name of a mailbox.
2261// Renaming INBOX is special, it moves the inbox messages to a new mailbox, leaving inbox empty.
2262// Renaming a mailbox with submailboxes also renames all submailboxes.
2263// Subscriptions stay with the old name, though newly created missing parent
2264// mailboxes for the destination name are automatically subscribed.
2266// State: Authenticated and selected.
2267func (c *conn) cmdRename(tag, cmd string, p *parser) {
2278 src = xcheckmailboxname(src, true)
2279 dst = xcheckmailboxname(dst, false)
2281 c.account.WithWLock(func() {
2282 var changes []store.Change
2284 c.xdbwrite(func(tx *bstore.Tx) {
2285 srcMB := c.xmailbox(tx, src, "NONEXISTENT")
2287 // Inbox is very special. Unlike other mailboxes, its children are not moved. And
2288 // unlike a regular move, its messages are moved to a newly created mailbox. We do
2289 // indeed create a new destination mailbox and actually move the messages.
2292 exists, err := c.account.MailboxExists(tx, dst)
2293 xcheckf(err, "checking if destination mailbox exists")
2295 xusercodeErrorf("ALREADYEXISTS", "destination mailbox %q already exists", dst)
2298 xuserErrorf("cannot move inbox to itself")
2301 uidval, err := c.account.NextUIDValidity(tx)
2302 xcheckf(err, "next uid validity")
2304 dstMB := store.Mailbox{
2306 UIDValidity: uidval,
2308 Keywords: srcMB.Keywords,
2311 err = tx.Insert(&dstMB)
2312 xcheckf(err, "create new destination mailbox")
2314 modseq, err := c.account.NextModSeq(tx)
2315 xcheckf(err, "assigning next modseq")
2317 changes = make([]store.Change, 2) // Placeholders filled in below.
2319 // Move existing messages, with their ID's and on-disk files intact, to the new
2320 // mailbox. We keep the expunged messages, the destination mailbox doesn't care
2322 var oldUIDs []store.UID
2323 q := bstore.QueryTx[store.Message](tx)
2324 q.FilterNonzero(store.Message{MailboxID: srcMB.ID})
2325 q.FilterEqual("Expunged", false)
2327 err = q.ForEach(func(m store.Message) error {
2332 oldUIDs = append(oldUIDs, om.UID)
2334 mc := m.MailboxCounts()
2338 m.MailboxID = dstMB.ID
2339 m.UID = dstMB.UIDNext
2341 m.CreateSeq = modseq
2343 if err := tx.Update(&m); err != nil {
2344 return fmt.Errorf("updating message to move to new mailbox: %w", err)
2347 changes = append(changes, m.ChangeAddUID())
2349 if err := tx.Insert(&om); err != nil {
2350 return fmt.Errorf("adding empty expunge message record to inbox: %w", err)
2354 xcheckf(err, "moving messages from inbox to destination mailbox")
2356 err = tx.Update(&dstMB)
2357 xcheckf(err, "updating uidnext and counts in destination mailbox")
2359 err = tx.Update(&srcMB)
2360 xcheckf(err, "updating counts for inbox")
2362 var dstFlags []string
2363 if tx.Get(&store.Subscription{Name: dstMB.Name}) == nil {
2364 dstFlags = []string{`\Subscribed`}
2366 changes[0] = store.ChangeRemoveUIDs{MailboxID: srcMB.ID, UIDs: oldUIDs, ModSeq: modseq}
2367 changes[1] = store.ChangeAddMailbox{Mailbox: dstMB, Flags: dstFlags}
2368 // changes[2:...] are ChangeAddUIDs
2369 changes = append(changes, srcMB.ChangeCounts(), dstMB.ChangeCounts())
2373 var notExists, alreadyExists bool
2375 changes, _, notExists, alreadyExists, err = c.account.MailboxRename(tx, srcMB, dst)
2378 xusercodeErrorf("NONEXISTENT", "%s", err)
2379 } else if alreadyExists {
2380 xusercodeErrorf("ALREADYEXISTS", "%s", err)
2382 xcheckf(err, "renaming mailbox")
2384 c.broadcast(changes)
2390// Subscribe marks a mailbox path as subscribed. The mailbox does not have to
2391// exist. Subscribed may mean an email client will show the mailbox in its UI
2392// and/or periodically fetch new messages for the mailbox.
2394// State: Authenticated and selected.
2395func (c *conn) cmdSubscribe(tag, cmd string, p *parser) {
2401 name := p.xmailbox()
2404 name = xcheckmailboxname(name, true)
2406 c.account.WithWLock(func() {
2407 var changes []store.Change
2409 c.xdbwrite(func(tx *bstore.Tx) {
2411 changes, err = c.account.SubscriptionEnsure(tx, name)
2412 xcheckf(err, "ensuring subscription")
2415 c.broadcast(changes)
2421// Unsubscribe marks a mailbox as not subscribed. The mailbox doesn't have to exist.
2423// State: Authenticated and selected.
2424func (c *conn) cmdUnsubscribe(tag, cmd string, p *parser) {
2430 name := p.xmailbox()
2433 name = xcheckmailboxname(name, true)
2435 c.account.WithWLock(func() {
2436 c.xdbwrite(func(tx *bstore.Tx) {
2438 err := tx.Delete(&store.Subscription{Name: name})
2439 if err == bstore.ErrAbsent {
2440 exists, err := c.account.MailboxExists(tx, name)
2441 xcheckf(err, "checking if mailbox exists")
2443 xuserErrorf("mailbox does not exist")
2447 xcheckf(err, "removing subscription")
2450 // todo: can we send untagged message about a mailbox no longer being subscribed?
2456// LSUB command for listing subscribed mailboxes.
2457// Removed in IMAP4rev2, only in IMAP4rev1.
2459// State: Authenticated and selected.
2460func (c *conn) cmdLsub(tag, cmd string, p *parser) {
2468 pattern := p.xlistMailbox()
2471 re := xmailboxPatternMatcher(ref, []string{pattern})
2474 c.xdbread(func(tx *bstore.Tx) {
2475 q := bstore.QueryTx[store.Subscription](tx)
2477 subscriptions, err := q.List()
2478 xcheckf(err, "querying subscriptions")
2480 have := map[string]bool{}
2481 subscribedKids := map[string]bool{}
2482 ispercent := strings.HasSuffix(pattern, "%")
2483 for _, sub := range subscriptions {
2486 for p := path.Dir(name); p != "."; p = path.Dir(p) {
2487 subscribedKids[p] = true
2490 if !re.MatchString(name) {
2494 line := fmt.Sprintf(`* LSUB () "/" %s`, astring(name).pack(c))
2495 lines = append(lines, line)
2503 qmb := bstore.QueryTx[store.Mailbox](tx)
2505 err = qmb.ForEach(func(mb store.Mailbox) error {
2506 if have[mb.Name] || !subscribedKids[mb.Name] || !re.MatchString(mb.Name) {
2509 line := fmt.Sprintf(`* LSUB (\NoSelect) "/" %s`, astring(mb.Name).pack(c))
2510 lines = append(lines, line)
2513 xcheckf(err, "querying mailboxes")
2517 for _, line := range lines {
2518 c.bwritelinef("%s", line)
2523// The namespace command returns the mailbox path separator. We only implement
2524// the personal mailbox hierarchy, no shared/other.
2526// In IMAP4rev2, it was an extension before.
2528// State: Authenticated and selected.
2529func (c *conn) cmdNamespace(tag, cmd string, p *parser) {
2536 c.bwritelinef(`* NAMESPACE (("" "/")) NIL NIL`)
2540// The status command returns information about a mailbox, such as the number of
2541// messages, "uid validity", etc. Nowadays, the extended LIST command can return
2542// the same information about many mailboxes for one command.
2544// State: Authenticated and selected.
2545func (c *conn) cmdStatus(tag, cmd string, p *parser) {
2551 name := p.xmailbox()
2554 attrs := []string{p.xstatusAtt()}
2557 attrs = append(attrs, p.xstatusAtt())
2561 name = xcheckmailboxname(name, true)
2563 var mb store.Mailbox
2565 var responseLine string
2566 c.account.WithRLock(func() {
2567 c.xdbread(func(tx *bstore.Tx) {
2568 mb = c.xmailbox(tx, name, "")
2569 responseLine = c.xstatusLine(tx, mb, attrs)
2573 c.bwritelinef("%s", responseLine)
2578func (c *conn) xstatusLine(tx *bstore.Tx, mb store.Mailbox, attrs []string) string {
2579 status := []string{}
2580 for _, a := range attrs {
2581 A := strings.ToUpper(a)
2584 status = append(status, A, fmt.Sprintf("%d", mb.Total+mb.Deleted))
2586 status = append(status, A, fmt.Sprintf("%d", mb.UIDNext))
2588 status = append(status, A, fmt.Sprintf("%d", mb.UIDValidity))
2590 status = append(status, A, fmt.Sprintf("%d", mb.Unseen))
2592 status = append(status, A, fmt.Sprintf("%d", mb.Deleted))
2594 status = append(status, A, fmt.Sprintf("%d", mb.Size))
2596 status = append(status, A, "0")
2599 status = append(status, A, "NIL")
2600 case "HIGHESTMODSEQ":
2602 status = append(status, A, fmt.Sprintf("%d", c.xhighestModSeq(tx, mb.ID).Client()))
2604 xsyntaxErrorf("unknown attribute %q", a)
2607 return fmt.Sprintf("* STATUS %s (%s)", astring(mb.Name).pack(c), strings.Join(status, " "))
2610func flaglist(fl store.Flags, keywords []string) listspace {
2612 flag := func(v bool, s string) {
2614 l = append(l, bare(s))
2617 flag(fl.Seen, `\Seen`)
2618 flag(fl.Answered, `\Answered`)
2619 flag(fl.Flagged, `\Flagged`)
2620 flag(fl.Deleted, `\Deleted`)
2621 flag(fl.Draft, `\Draft`)
2622 flag(fl.Forwarded, `$Forwarded`)
2623 flag(fl.Junk, `$Junk`)
2624 flag(fl.Notjunk, `$NotJunk`)
2625 flag(fl.Phishing, `$Phishing`)
2626 flag(fl.MDNSent, `$MDNSent`)
2627 for _, k := range keywords {
2628 l = append(l, bare(k))
2633// Append adds a message to a mailbox.
2635// State: Authenticated and selected.
2636func (c *conn) cmdAppend(tag, cmd string, p *parser) {
2642 name := p.xmailbox()
2644 var storeFlags store.Flags
2645 var keywords []string
2646 if p.hasPrefix("(") {
2647 // Error must be a syntax error, to properly abort the connection due to literal.
2649 storeFlags, keywords, err = store.ParseFlagsKeywords(p.xflagList())
2651 xsyntaxErrorf("parsing flags: %v", err)
2656 if p.hasPrefix(`"`) {
2662 // todo: only with utf8 should we we accept message headers with utf-8. we currently always accept them.
2663 // todo: this is only relevant if we also support the CATENATE extension?
2665 utf8 := p.take("UTF8 (")
2666 size, sync := p.xliteralSize(0, utf8)
2668 name = xcheckmailboxname(name, true)
2669 c.xdbread(func(tx *bstore.Tx) {
2670 c.xmailbox(tx, name, "TRYCREATE")
2676 // Read the message into a temporary file.
2677 msgFile, err := store.CreateMessageTemp("imap-append")
2678 xcheckf(err, "creating temp file for message")
2681 err := msgFile.Close()
2682 c.xsanity(err, "closing APPEND temporary file")
2684 c.xsanity(err, "removing APPEND temporary file")
2686 defer c.xtrace(mlog.LevelTracedata)()
2687 mw := message.NewWriter(msgFile)
2688 msize, err := io.Copy(mw, io.LimitReader(c.br, size))
2689 c.xtrace(mlog.LevelTrace) // Restore.
2691 // Cannot use xcheckf due to %w handling of errIO.
2692 panic(fmt.Errorf("reading literal message: %s (%w)", err, errIO))
2695 xserverErrorf("read %d bytes for message, expected %d (%w)", msize, size, errIO)
2699 line := c.readline(false)
2700 np := newParser(line, c)
2704 line := c.readline(false)
2705 np := newParser(line, c)
2710 name = xcheckmailboxname(name, true)
2713 var mb store.Mailbox
2715 var pendingChanges []store.Change
2717 c.account.WithWLock(func() {
2718 var changes []store.Change
2719 c.xdbwrite(func(tx *bstore.Tx) {
2720 mb = c.xmailbox(tx, name, "TRYCREATE")
2722 // Ensure keywords are stored in mailbox.
2723 var mbKwChanged bool
2724 mb.Keywords, mbKwChanged = store.MergeKeywords(mb.Keywords, keywords)
2726 changes = append(changes, mb.ChangeKeywords())
2731 MailboxOrigID: mb.ID,
2738 mb.Add(m.MailboxCounts())
2740 // Update mailbox before delivering, which updates uidnext which we mustn't overwrite.
2741 err = tx.Update(&mb)
2742 xcheckf(err, "updating mailbox counts")
2744 err := c.account.DeliverMessage(c.log, tx, &m, msgFile, true, false, false)
2745 xcheckf(err, "delivering message")
2748 // Fetch pending changes, possibly with new UIDs, so we can apply them before adding our own new UID.
2750 pendingChanges = c.comm.Get()
2753 // Broadcast the change to other connections.
2754 changes = append(changes, m.ChangeAddUID(), mb.ChangeCounts())
2755 c.broadcast(changes)
2758 if c.mailboxID == mb.ID {
2759 c.applyChanges(pendingChanges, false)
2761 // todo spec: with condstore/qresync, is there a mechanism to the client know the modseq for the appended uid? in theory an untagged fetch with the modseq after the OK APPENDUID could make sense, but this probably isn't allowed.
2762 c.bwritelinef("* %d EXISTS", len(c.uids))
2765 c.writeresultf("%s OK [APPENDUID %d %d] appended", tag, mb.UIDValidity, m.UID)
2768// Idle makes a client wait until the server sends untagged updates, e.g. about
2769// message delivery or mailbox create/rename/delete/subscription, etc. It allows a
2770// client to get updates in real-time, not needing the use for NOOP.
2772// State: Authenticated and selected.
2773func (c *conn) cmdIdle(tag, cmd string, p *parser) {
2780 c.writelinef("+ waiting")
2786 case le := <-c.lineChan():
2788 xcheckf(le.err, "get line")
2791 case <-c.comm.Pending:
2792 c.applyChanges(c.comm.Get(), false)
2794 case <-mox.Shutdown.Done():
2796 c.writelinef("* BYE shutting down")
2801 // Reset the write deadline. In case of little activity, with a command timeout of
2802 // 30 minutes, we have likely passed it.
2803 err := c.conn.SetWriteDeadline(time.Now().Add(5 * time.Minute))
2804 c.log.Check(err, "setting write deadline")
2806 if strings.ToUpper(line) != "DONE" {
2807 // We just close the connection because our protocols are out of sync.
2808 panic(fmt.Errorf("%w: in IDLE, expected DONE", errIO))
2814// Check is an old deprecated command that is supposed to execute some mailbox consistency checks.
2817func (c *conn) cmdCheck(tag, cmd string, p *parser) {
2823 c.account.WithRLock(func() {
2824 c.xdbread(func(tx *bstore.Tx) {
2825 c.xmailboxID(tx, c.mailboxID) // Validate.
2832// Close undoes select/examine, closing the currently opened mailbox and deleting
2833// messages that were marked for deletion with the \Deleted flag.
2836func (c *conn) cmdClose(tag, cmd string, p *parser) {
2848 remove, _ := c.xexpunge(nil, true)
2851 for _, m := range remove {
2852 p := c.account.MessagePath(m.ID)
2854 c.xsanity(err, "removing message file for expunge for close")
2862// expunge messages marked for deletion in currently selected/active mailbox.
2863// if uidSet is not nil, only messages matching the set are deleted.
2865// messages that have been marked expunged from the database are returned, but the
2866// corresponding files still have to be removed.
2868// the highest modseq in the mailbox is returned, typically associated with the
2869// removal of the messages, but if no messages were expunged the current latest max
2870// modseq for the mailbox is returned.
2871func (c *conn) xexpunge(uidSet *numSet, missingMailboxOK bool) (remove []store.Message, highestModSeq store.ModSeq) {
2872 var modseq store.ModSeq
2874 c.account.WithWLock(func() {
2875 var mb store.Mailbox
2877 c.xdbwrite(func(tx *bstore.Tx) {
2878 mb = store.Mailbox{ID: c.mailboxID}
2880 if err == bstore.ErrAbsent {
2881 if missingMailboxOK {
2884 xuserErrorf("%w", store.ErrUnknownMailbox)
2887 qm := bstore.QueryTx[store.Message](tx)
2888 qm.FilterNonzero(store.Message{MailboxID: c.mailboxID})
2889 qm.FilterEqual("Deleted", true)
2890 qm.FilterEqual("Expunged", false)
2891 qm.FilterFn(func(m store.Message) bool {
2892 // Only remove if this session knows about the message and if present in optional uidSet.
2893 return uidSearch(c.uids, m.UID) > 0 && (uidSet == nil || uidSet.containsUID(m.UID, c.uids, c.searchResult))
2896 remove, err = qm.List()
2897 xcheckf(err, "listing messages to delete")
2899 if len(remove) == 0 {
2900 highestModSeq = c.xhighestModSeq(tx, c.mailboxID)
2904 // Assign new modseq.
2905 modseq, err = c.account.NextModSeq(tx)
2906 xcheckf(err, "assigning next modseq")
2907 highestModSeq = modseq
2909 removeIDs := make([]int64, len(remove))
2910 anyIDs := make([]any, len(remove))
2911 for i, m := range remove {
2914 mb.Sub(m.MailboxCounts())
2915 // Update "remove", because RetrainMessage below will save the message.
2916 remove[i].Expunged = true
2917 remove[i].ModSeq = modseq
2919 qmr := bstore.QueryTx[store.Recipient](tx)
2920 qmr.FilterEqual("MessageID", anyIDs...)
2921 _, err = qmr.Delete()
2922 xcheckf(err, "removing message recipients")
2924 qm = bstore.QueryTx[store.Message](tx)
2925 qm.FilterIDs(removeIDs)
2926 n, err := qm.UpdateNonzero(store.Message{Expunged: true, ModSeq: modseq})
2927 if err == nil && n != len(removeIDs) {
2928 err = fmt.Errorf("only %d messages set to expunged, expected %d", n, len(removeIDs))
2930 xcheckf(err, "marking messages marked for deleted as expunged")
2932 err = tx.Update(&mb)
2933 xcheckf(err, "updating mailbox counts")
2935 // Mark expunged messages as not needing training, then retrain them, so if they
2936 // were trained, they get untrained.
2937 for i := range remove {
2938 remove[i].Junk = false
2939 remove[i].Notjunk = false
2941 err = c.account.RetrainMessages(context.TODO(), c.log, tx, remove, true)
2942 xcheckf(err, "untraining expunged messages")
2945 // Broadcast changes to other connections. We may not have actually removed any
2946 // messages, so take care not to send an empty update.
2947 if len(remove) > 0 {
2948 ouids := make([]store.UID, len(remove))
2949 for i, m := range remove {
2952 changes := []store.Change{
2953 store.ChangeRemoveUIDs{MailboxID: c.mailboxID, UIDs: ouids, ModSeq: modseq},
2956 c.broadcast(changes)
2959 return remove, highestModSeq
2962// Unselect is similar to close in that it closes the currently active mailbox, but
2963// it does not remove messages marked for deletion.
2966func (c *conn) cmdUnselect(tag, cmd string, p *parser) {
2976// Expunge deletes messages marked with \Deleted in the currently selected mailbox.
2977// Clients are wiser to use UID EXPUNGE because it allows a UID sequence set to
2978// explicitly opt in to removing specific messages.
2981func (c *conn) cmdExpunge(tag, cmd string, p *parser) {
2988 xuserErrorf("mailbox open in read-only mode")
2991 c.cmdxExpunge(tag, cmd, nil)
2994// UID expunge deletes messages marked with \Deleted in the currently selected
2995// mailbox if they match a UID sequence set.
2998func (c *conn) cmdUIDExpunge(tag, cmd string, p *parser) {
3003 uidSet := p.xnumSet()
3007 xuserErrorf("mailbox open in read-only mode")
3010 c.cmdxExpunge(tag, cmd, &uidSet)
3013// Permanently delete messages for the currently selected/active mailbox. If uidset
3014// is not nil, only those UIDs are removed.
3016func (c *conn) cmdxExpunge(tag, cmd string, uidSet *numSet) {
3019 remove, highestModSeq := c.xexpunge(uidSet, false)
3022 for _, m := range remove {
3023 p := c.account.MessagePath(m.ID)
3025 c.xsanity(err, "removing message file for expunge")
3030 var vanishedUIDs numSet
3031 qresync := c.enabled[capQresync]
3032 for _, m := range remove {
3033 seq := c.xsequence(m.UID)
3034 c.sequenceRemove(seq, m.UID)
3036 vanishedUIDs.append(uint32(m.UID))
3038 c.bwritelinef("* %d EXPUNGE", seq)
3041 if !vanishedUIDs.empty() {
3043 for _, s := range vanishedUIDs.Strings(4*1024 - 32) {
3044 c.bwritelinef("* VANISHED %s", s)
3048 if c.enabled[capCondstore] {
3049 c.writeresultf("%s OK [HIGHESTMODSEQ %d] expunged", tag, highestModSeq.Client())
3056func (c *conn) cmdSearch(tag, cmd string, p *parser) {
3057 c.cmdxSearch(false, tag, cmd, p)
3061func (c *conn) cmdUIDSearch(tag, cmd string, p *parser) {
3062 c.cmdxSearch(true, tag, cmd, p)
3066func (c *conn) cmdFetch(tag, cmd string, p *parser) {
3067 c.cmdxFetch(false, tag, cmd, p)
3071func (c *conn) cmdUIDFetch(tag, cmd string, p *parser) {
3072 c.cmdxFetch(true, tag, cmd, p)
3076func (c *conn) cmdStore(tag, cmd string, p *parser) {
3077 c.cmdxStore(false, tag, cmd, p)
3081func (c *conn) cmdUIDStore(tag, cmd string, p *parser) {
3082 c.cmdxStore(true, tag, cmd, p)
3086func (c *conn) cmdCopy(tag, cmd string, p *parser) {
3087 c.cmdxCopy(false, tag, cmd, p)
3091func (c *conn) cmdUIDCopy(tag, cmd string, p *parser) {
3092 c.cmdxCopy(true, tag, cmd, p)
3096func (c *conn) cmdMove(tag, cmd string, p *parser) {
3097 c.cmdxMove(false, tag, cmd, p)
3101func (c *conn) cmdUIDMove(tag, cmd string, p *parser) {
3102 c.cmdxMove(true, tag, cmd, p)
3105func (c *conn) gatherCopyMoveUIDs(isUID bool, nums numSet) ([]store.UID, []any) {
3106 // Gather uids, then sort so we can return a consistently simple and hard to
3107 // misinterpret COPYUID/MOVEUID response. It seems safer to have UIDs in ascending
3108 // order, because requested uid set of 12:10 is equal to 10:12, so if we would just
3109 // echo whatever the client sends us without reordering, the client can reorder our
3110 // response and interpret it differently than we intended.
3112 uids := c.xnumSetUIDs(isUID, nums)
3113 sort.Slice(uids, func(i, j int) bool {
3114 return uids[i] < uids[j]
3116 uidargs := make([]any, len(uids))
3117 for i, uid := range uids {
3120 return uids, uidargs
3123// Copy copies messages from the currently selected/active mailbox to another named
3127func (c *conn) cmdxCopy(isUID bool, tag, cmd string, p *parser) {
3134 name := p.xmailbox()
3137 name = xcheckmailboxname(name, true)
3139 uids, uidargs := c.gatherCopyMoveUIDs(isUID, nums)
3141 // Files that were created during the copy. Remove them if the operation fails.
3142 var createdIDs []int64
3148 for _, id := range createdIDs {
3149 p := c.account.MessagePath(id)
3151 c.xsanity(err, "cleaning up created file")
3156 var mbDst store.Mailbox
3157 var origUIDs, newUIDs []store.UID
3158 var flags []store.Flags
3159 var keywords [][]string
3160 var modseq store.ModSeq // For messages in new mailbox, assigned when first message is copied.
3162 c.account.WithWLock(func() {
3163 var mbKwChanged bool
3165 c.xdbwrite(func(tx *bstore.Tx) {
3166 mbSrc := c.xmailboxID(tx, c.mailboxID) // Validate.
3167 mbDst = c.xmailbox(tx, name, "TRYCREATE")
3168 if mbDst.ID == mbSrc.ID {
3169 xuserErrorf("cannot copy to currently selected mailbox")
3172 if len(uidargs) == 0 {
3173 xuserErrorf("no matching messages to copy")
3177 modseq, err = c.account.NextModSeq(tx)
3178 xcheckf(err, "assigning next modseq")
3180 // Reserve the uids in the destination mailbox.
3181 uidFirst := mbDst.UIDNext
3182 mbDst.UIDNext += store.UID(len(uidargs))
3184 // Fetch messages from database.
3185 q := bstore.QueryTx[store.Message](tx)
3186 q.FilterNonzero(store.Message{MailboxID: c.mailboxID})
3187 q.FilterEqual("UID", uidargs...)
3188 q.FilterEqual("Expunged", false)
3189 xmsgs, err := q.List()
3190 xcheckf(err, "fetching messages")
3192 if len(xmsgs) != len(uidargs) {
3193 xserverErrorf("uid and message mismatch")
3196 msgs := map[store.UID]store.Message{}
3197 for _, m := range xmsgs {
3200 nmsgs := make([]store.Message, len(xmsgs))
3202 conf, _ := c.account.Conf()
3204 mbKeywords := map[string]struct{}{}
3206 // Insert new messages into database.
3207 var origMsgIDs, newMsgIDs []int64
3208 for i, uid := range uids {
3211 xuserErrorf("messages changed, could not fetch requested uid")
3214 origMsgIDs = append(origMsgIDs, origID)
3216 m.UID = uidFirst + store.UID(i)
3217 m.CreateSeq = modseq
3219 m.MailboxID = mbDst.ID
3220 if m.IsReject && m.MailboxDestinedID != 0 {
3221 // Incorrectly delivered to Rejects mailbox. Adjust MailboxOrigID so this message
3222 // is used for reputation calculation during future deliveries.
3223 m.MailboxOrigID = m.MailboxDestinedID
3227 m.JunkFlagsForMailbox(mbDst, conf)
3228 err := tx.Insert(&m)
3229 xcheckf(err, "inserting message")
3232 origUIDs = append(origUIDs, uid)
3233 newUIDs = append(newUIDs, m.UID)
3234 newMsgIDs = append(newMsgIDs, m.ID)
3235 flags = append(flags, m.Flags)
3236 keywords = append(keywords, m.Keywords)
3237 for _, kw := range m.Keywords {
3238 mbKeywords[kw] = struct{}{}
3241 qmr := bstore.QueryTx[store.Recipient](tx)
3242 qmr.FilterNonzero(store.Recipient{MessageID: origID})
3243 mrs, err := qmr.List()
3244 xcheckf(err, "listing message recipients")
3245 for _, mr := range mrs {
3248 err := tx.Insert(&mr)
3249 xcheckf(err, "inserting message recipient")
3252 mbDst.Add(m.MailboxCounts())
3255 mbDst.Keywords, mbKwChanged = store.MergeKeywords(mbDst.Keywords, maps.Keys(mbKeywords))
3257 err = tx.Update(&mbDst)
3258 xcheckf(err, "updating destination mailbox for uids, keywords and counts")
3260 // Copy message files to new message ID's.
3261 syncDirs := map[string]struct{}{}
3262 for i := range origMsgIDs {
3263 src := c.account.MessagePath(origMsgIDs[i])
3264 dst := c.account.MessagePath(newMsgIDs[i])
3265 dstdir := filepath.Dir(dst)
3266 if _, ok := syncDirs[dstdir]; !ok {
3267 os.MkdirAll(dstdir, 0770)
3268 syncDirs[dstdir] = struct{}{}
3270 err := moxio.LinkOrCopy(c.log, dst, src, nil, true)
3271 xcheckf(err, "link or copy file %q to %q", src, dst)
3272 createdIDs = append(createdIDs, newMsgIDs[i])
3275 for dir := range syncDirs {
3276 err := moxio.SyncDir(dir)
3277 xcheckf(err, "sync directory")
3280 err = c.account.RetrainMessages(context.TODO(), c.log, tx, nmsgs, false)
3281 xcheckf(err, "train copied messages")
3284 // Broadcast changes to other connections.
3285 if len(newUIDs) > 0 {
3286 changes := make([]store.Change, 0, len(newUIDs)+2)
3287 for i, uid := range newUIDs {
3288 changes = append(changes, store.ChangeAddUID{MailboxID: mbDst.ID, UID: uid, ModSeq: modseq, Flags: flags[i], Keywords: keywords[i]})
3290 changes = append(changes, mbDst.ChangeCounts())
3292 changes = append(changes, mbDst.ChangeKeywords())
3294 c.broadcast(changes)
3298 // All good, prevent defer above from cleaning up copied files.
3302 c.writeresultf("%s OK [COPYUID %d %s %s] copied", tag, mbDst.UIDValidity, compactUIDSet(origUIDs).String(), compactUIDSet(newUIDs).String())
3305// Move moves messages from the currently selected/active mailbox to a named mailbox.
3308func (c *conn) cmdxMove(isUID bool, tag, cmd string, p *parser) {
3315 name := p.xmailbox()
3318 name = xcheckmailboxname(name, true)
3321 xuserErrorf("mailbox open in read-only mode")
3324 uids, uidargs := c.gatherCopyMoveUIDs(isUID, nums)
3326 var mbSrc, mbDst store.Mailbox
3327 var changes []store.Change
3328 var newUIDs []store.UID
3329 var modseq store.ModSeq
3331 c.account.WithWLock(func() {
3332 c.xdbwrite(func(tx *bstore.Tx) {
3333 mbSrc = c.xmailboxID(tx, c.mailboxID) // Validate.
3334 mbDst = c.xmailbox(tx, name, "TRYCREATE")
3335 if mbDst.ID == c.mailboxID {
3336 xuserErrorf("cannot move to currently selected mailbox")
3339 if len(uidargs) == 0 {
3340 xuserErrorf("no matching messages to move")
3343 // Reserve the uids in the destination mailbox.
3344 uidFirst := mbDst.UIDNext
3346 mbDst.UIDNext += store.UID(len(uids))
3348 // Assign a new modseq, for the new records and for the expunged records.
3350 modseq, err = c.account.NextModSeq(tx)
3351 xcheckf(err, "assigning next modseq")
3353 // Update existing record with new UID and MailboxID in database for messages. We
3354 // add a new but expunged record again in the original/source mailbox, for qresync.
3355 // Keeping the original ID for the live message means we don't have to move the
3356 // on-disk message contents file.
3357 q := bstore.QueryTx[store.Message](tx)
3358 q.FilterNonzero(store.Message{MailboxID: c.mailboxID})
3359 q.FilterEqual("UID", uidargs...)
3360 q.FilterEqual("Expunged", false)
3362 msgs, err := q.List()
3363 xcheckf(err, "listing messages to move")
3365 if len(msgs) != len(uidargs) {
3366 xserverErrorf("uid and message mismatch")
3369 keywords := map[string]struct{}{}
3371 conf, _ := c.account.Conf()
3372 for i := range msgs {
3374 if m.UID != uids[i] {
3375 xserverErrorf("internal error: got uid %d, expected %d, for index %d", m.UID, uids[i], i)
3378 mbSrc.Sub(m.MailboxCounts())
3380 // Copy of message record that we'll insert when UID is freed up.
3383 om.ID = 0 // Assign new ID.
3386 m.MailboxID = mbDst.ID
3387 if m.IsReject && m.MailboxDestinedID != 0 {
3388 // Incorrectly delivered to Rejects mailbox. Adjust MailboxOrigID so this message
3389 // is used for reputation calculation during future deliveries.
3390 m.MailboxOrigID = m.MailboxDestinedID
3394 mbDst.Add(m.MailboxCounts())
3397 m.JunkFlagsForMailbox(mbDst, conf)
3400 xcheckf(err, "updating moved message in database")
3402 // Now that UID is unused, we can insert the old record again.
3403 err = tx.Insert(&om)
3404 xcheckf(err, "inserting record for expunge after moving message")
3406 for _, kw := range m.Keywords {
3407 keywords[kw] = struct{}{}
3411 // Ensure destination mailbox has keywords of the moved messages.
3412 var mbKwChanged bool
3413 mbDst.Keywords, mbKwChanged = store.MergeKeywords(mbDst.Keywords, maps.Keys(keywords))
3415 changes = append(changes, mbDst.ChangeKeywords())
3418 err = tx.Update(&mbSrc)
3419 xcheckf(err, "updating source mailbox counts")
3421 err = tx.Update(&mbDst)
3422 xcheckf(err, "updating destination mailbox for uids, keywords and counts")
3424 err = c.account.RetrainMessages(context.TODO(), c.log, tx, msgs, false)
3425 xcheckf(err, "retraining messages after move")
3427 // Prepare broadcast changes to other connections.
3428 changes = make([]store.Change, 0, 1+len(msgs)+2)
3429 changes = append(changes, store.ChangeRemoveUIDs{MailboxID: c.mailboxID, UIDs: uids, ModSeq: modseq})
3430 for _, m := range msgs {
3431 newUIDs = append(newUIDs, m.UID)
3432 changes = append(changes, m.ChangeAddUID())
3434 changes = append(changes, mbSrc.ChangeCounts(), mbDst.ChangeCounts())
3437 c.broadcast(changes)
3442 c.bwritelinef("* OK [COPYUID %d %s %s] moved", mbDst.UIDValidity, compactUIDSet(uids).String(), compactUIDSet(newUIDs).String())
3443 qresync := c.enabled[capQresync]
3444 var vanishedUIDs numSet
3445 for i := 0; i < len(uids); i++ {
3446 seq := c.xsequence(uids[i])
3447 c.sequenceRemove(seq, uids[i])
3449 vanishedUIDs.append(uint32(uids[i]))
3451 c.bwritelinef("* %d EXPUNGE", seq)
3454 if !vanishedUIDs.empty() {
3456 for _, s := range vanishedUIDs.Strings(4*1024 - 32) {
3457 c.bwritelinef("* VANISHED %s", s)
3461 if c.enabled[capQresync] {
3463 c.writeresultf("%s OK [HIGHESTMODSEQ %d] move", tag, modseq.Client())
3469// Store sets a full set of flags, or adds/removes specific flags.
3472func (c *conn) cmdxStore(isUID bool, tag, cmd string, p *parser) {
3479 var unchangedSince *int64
3482 p.xtake("UNCHANGEDSINCE")
3489 c.xensureCondstore(nil)
3491 var plus, minus bool
3494 } else if p.take("-") {
3498 silent := p.take(".SILENT")
3500 var flagstrs []string
3501 if p.hasPrefix("(") {
3502 flagstrs = p.xflagList()
3504 flagstrs = append(flagstrs, p.xflag())
3506 flagstrs = append(flagstrs, p.xflag())
3512 xuserErrorf("mailbox open in read-only mode")
3515 flags, keywords, err := store.ParseFlagsKeywords(flagstrs)
3517 xuserErrorf("parsing flags: %v", err)
3519 var mask store.Flags
3521 mask, flags = flags, store.FlagsAll
3523 mask, flags = flags, store.Flags{}
3525 mask = store.FlagsAll
3528 var mb, origmb store.Mailbox
3529 var updated []store.Message
3530 var changed []store.Message // ModSeq more recent than unchangedSince, will be in MODIFIED response code, and we will send untagged fetch responses so client is up to date.
3531 var modseq store.ModSeq // Assigned when needed.
3532 modified := map[int64]bool{}
3534 c.account.WithWLock(func() {
3535 var mbKwChanged bool
3536 var changes []store.Change
3538 c.xdbwrite(func(tx *bstore.Tx) {
3539 mb = c.xmailboxID(tx, c.mailboxID) // Validate.
3542 uidargs := c.xnumSetCondition(isUID, nums)
3544 if len(uidargs) == 0 {
3548 // Ensure keywords are in mailbox.
3550 mb.Keywords, mbKwChanged = store.MergeKeywords(mb.Keywords, keywords)
3552 err := tx.Update(&mb)
3553 xcheckf(err, "updating mailbox with keywords")
3557 q := bstore.QueryTx[store.Message](tx)
3558 q.FilterNonzero(store.Message{MailboxID: c.mailboxID})
3559 q.FilterEqual("UID", uidargs...)
3560 q.FilterEqual("Expunged", false)
3561 err := q.ForEach(func(m store.Message) error {
3562 // Client may specify a message multiple times, but we only process it once.
../rfc/7162:823
3567 mc := m.MailboxCounts()
3569 origFlags := m.Flags
3570 m.Flags = m.Flags.Set(mask, flags)
3571 oldKeywords := append([]string{}, m.Keywords...)
3573 m.Keywords, _ = store.RemoveKeywords(m.Keywords, keywords)
3575 m.Keywords, _ = store.MergeKeywords(m.Keywords, keywords)
3577 m.Keywords = keywords
3580 keywordsChanged := func() bool {
3581 sort.Strings(oldKeywords)
3582 n := append([]string{}, m.Keywords...)
3584 return !slices.Equal(oldKeywords, n)
3587 // If the message has a more recent modseq than the check requires, we won't modify
3588 // it and report in the final command response.
3591 // unchangedSince 0 always fails the check, we don't turn it into 1 like with our
3592 // internal modseqs. RFC implies that is not required for non-system flags, but we
3594 if unchangedSince != nil && m.ModSeq.Client() > *unchangedSince {
3595 changed = append(changed, m)
3600 // It requires that we keep track of the flags we think the client knows (but only
3601 // on this connection). We don't track that. It also isn't clear why this is
3602 // allowed because it is skipping the condstore conditional check, and the new
3603 // combination of flags could be unintended.
3606 if origFlags == m.Flags && !keywordsChanged() {
3607 // Note: since we didn't update the modseq, we are not adding m.ID to "modified",
3608 // it would skip the modseq check above. We still add m to list of updated, so we
3609 // send an untagged fetch response. But we don't broadcast it.
3610 updated = append(updated, m)
3615 mb.Add(m.MailboxCounts())
3617 // Assign new modseq for first actual change.
3620 modseq, err = c.account.NextModSeq(tx)
3621 xcheckf(err, "next modseq")
3624 modified[m.ID] = true
3625 updated = append(updated, m)
3627 changes = append(changes, m.ChangeFlags(origFlags))
3629 return tx.Update(&m)
3631 xcheckf(err, "storing flags in messages")
3633 if mb.MailboxCounts != origmb.MailboxCounts {
3634 err := tx.Update(&mb)
3635 xcheckf(err, "updating mailbox counts")
3637 changes = append(changes, mb.ChangeCounts())
3640 changes = append(changes, mb.ChangeKeywords())
3643 err = c.account.RetrainMessages(context.TODO(), c.log, tx, updated, false)
3644 xcheckf(err, "training messages")
3647 c.broadcast(changes)
3650 // In the RFC, the section about STORE/UID STORE says we must return MODSEQ when
3651 // UNCHANGEDSINCE was specified. It does not specify it in case UNCHANGEDSINCE
3652 // isn't specified. For that case it does say MODSEQ is needed in unsolicited
3653 // untagged fetch responses. Implying that solicited untagged fetch responses
3654 // should not include MODSEQ (why else mention unsolicited explicitly?). But, in
3655 // the introduction to CONDSTORE it does explicitly specify MODSEQ should be
3656 // included in untagged fetch responses at all times with CONDSTORE-enabled
3657 // connections. It would have been better if the command behaviour was specified in
3658 // the command section, not the introduction to the extension.
3661 if !silent || c.enabled[capCondstore] {
3662 for _, m := range updated {
3665 flags = fmt.Sprintf(" FLAGS %s", flaglist(m.Flags, m.Keywords).pack(c))
3667 var modseqStr string
3668 if c.enabled[capCondstore] {
3669 modseqStr = fmt.Sprintf(" MODSEQ (%d)", m.ModSeq.Client())
3672 c.bwritelinef("* %d FETCH (UID %d%s%s)", c.xsequence(m.UID), m.UID, flags, modseqStr)
3676 // We don't explicitly send flags for failed updated with silent set. The regular
3677 // notification will get the flags to the client.
3680 if len(changed) == 0 {
3685 // Write unsolicited untagged fetch responses for messages that didn't pass the
3688 var mnums []store.UID
3689 for _, m := range changed {
3690 c.bwritelinef("* %d FETCH (UID %d FLAGS %s MODSEQ (%d))", c.xsequence(m.UID), m.UID, flaglist(m.Flags, m.Keywords).pack(c), m.ModSeq.Client())
3692 mnums = append(mnums, m.UID)
3694 mnums = append(mnums, store.UID(c.xsequence(m.UID)))
3698 sort.Slice(mnums, func(i, j int) bool {
3699 return mnums[i] < mnums[j]
3701 set := compactUIDSet(mnums)
3703 c.writeresultf("%s OK [MODIFIED %s] conditional store did not modify all", tag, set.String())