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.
60 "golang.org/x/exp/maps"
61 "golang.org/x/exp/slices"
63 "github.com/prometheus/client_golang/prometheus"
64 "github.com/prometheus/client_golang/prometheus/promauto"
66 "github.com/mjl-/bstore"
68 "github.com/mjl-/mox/config"
69 "github.com/mjl-/mox/message"
70 "github.com/mjl-/mox/metrics"
71 "github.com/mjl-/mox/mlog"
72 "github.com/mjl-/mox/mox-"
73 "github.com/mjl-/mox/moxio"
74 "github.com/mjl-/mox/moxvar"
75 "github.com/mjl-/mox/ratelimit"
76 "github.com/mjl-/mox/scram"
77 "github.com/mjl-/mox/store"
80// Most logging should be done through conn.log* functions.
81// Only use imaplog in contexts without connection.
82var xlog = mlog.New("imapserver")
85 metricIMAPConnection = promauto.NewCounterVec(
86 prometheus.CounterOpts{
87 Name: "mox_imap_connection_total",
88 Help: "Incoming IMAP connections.",
91 "service", // imap, imaps
94 metricIMAPCommands = promauto.NewHistogramVec(
95 prometheus.HistogramOpts{
96 Name: "mox_imap_command_duration_seconds",
97 Help: "IMAP command duration and result codes in seconds.",
98 Buckets: []float64{0.001, 0.005, 0.01, 0.05, 0.100, 0.5, 1, 5, 10, 20},
102 "result", // ok, panic, ioerror, badsyntax, servererror, usererror, error
107var limiterConnectionrate, limiterConnections *ratelimit.Limiter
110 // Also called by tests, so they don't trigger the rate limiter.
116 limiterConnectionrate = &ratelimit.Limiter{
117 WindowLimits: []ratelimit.WindowLimit{
120 Limits: [...]int64{300, 900, 2700},
124 limiterConnections = &ratelimit.Limiter{
125 WindowLimits: []ratelimit.WindowLimit{
127 Window: time.Duration(math.MaxInt64), // All of time.
128 Limits: [...]int64{30, 90, 270},
134// Delay after bad/suspicious behaviour. Tests set these to zero.
135var badClientDelay = time.Second // Before reads and after 1-byte writes for probably spammers.
136var authFailDelay = time.Second // After authentication failure.
138// Capabilities (extensions) the server supports. Connections will add a few more, e.g. STARTTLS, LOGINDISABLED, AUTH=PLAIN.
160const serverCapabilities = "IMAP4rev2 IMAP4rev1 ENABLE LITERAL+ IDLE SASL-IR BINARY UNSELECT UIDPLUS ESEARCH SEARCHRES MOVE UTF8=ONLY LIST-EXTENDED SPECIAL-USE LIST-STATUS AUTH=SCRAM-SHA-256 AUTH=SCRAM-SHA-1 AUTH=CRAM-MD5 ID APPENDLIMIT=9223372036854775807 CONDSTORE QRESYNC"
166 tls bool // Whether TLS has been initialized.
167 br *bufio.Reader // From remote, with TLS unwrapped in case of TLS.
168 line chan lineErr // If set, instead of reading from br, a line is read from this channel. For reading a line in IDLE while also waiting for mailbox/account updates.
169 lastLine string // For detecting if syntax error is fatal, i.e. if this ends with a literal. Without crlf.
170 bw *bufio.Writer // To remote, with TLS added in case of TLS.
171 tr *moxio.TraceReader // Kept to change trace level when reading/writing cmd/auth/data.
172 tw *moxio.TraceWriter
173 slow bool // If set, reads are done with a 1 second sleep, and writes are done 1 byte at a time, to keep spammers busy.
174 lastlog time.Time // For printing time since previous log line.
175 tlsConfig *tls.Config // TLS config to use for handshake.
177 noRequireSTARTTLS bool
178 cmd string // Currently executing, for deciding to applyChanges and logging.
179 cmdMetric string // Currently executing, for metrics.
181 ncmds int // Number of commands processed. Used to abort connection when first incoming command is unknown/invalid.
183 enabled map[capability]bool // All upper-case.
185 // Set by SEARCH with SAVE. Can be used by commands accepting a sequence-set with
186 // value "$". When used, UIDs must be verified to still exist, because they may
187 // have been expunged. Cleared by a SELECT or EXAMINE.
188 // Nil means no searchResult is present. An empty list is a valid searchResult,
189 // just not matching any messages.
191 searchResult []store.UID
193 // Only when authenticated.
194 authFailed int // Number of failed auth attempts. For slowing down remote with many failures.
195 username string // Full username as used during login.
196 account *store.Account
197 comm *store.Comm // For sending/receiving changes on mailboxes in account, e.g. from messages incoming on smtp, or another imap client.
199 mailboxID int64 // Only for StateSelected.
200 readonly bool // If opened mailbox is readonly.
201 uids []store.UID // UIDs known in this session, sorted. todo future: store more space-efficiently, as ranges.
204// capability for use with ENABLED and CAPABILITY. We always keep this upper case,
205// e.g. IMAP4REV2. These values are treated case-insensitive, but it's easier for
206// comparison to just always have the same case.
207type capability string
210 capIMAP4rev2 capability = "IMAP4REV2"
211 capUTF8Accept capability = "UTF8=ACCEPT"
212 capCondstore capability = "CONDSTORE"
213 capQresync capability = "QRESYNC"
224 stateNotAuthenticated state = iota
229func stateCommands(cmds ...string) map[string]struct{} {
230 r := map[string]struct{}{}
231 for _, cmd := range cmds {
238 commandsStateAny = stateCommands("capability", "noop", "logout", "id")
239 commandsStateNotAuthenticated = stateCommands("starttls", "authenticate", "login")
240 commandsStateAuthenticated = stateCommands("enable", "select", "examine", "create", "delete", "rename", "subscribe", "unsubscribe", "list", "namespace", "status", "append", "idle", "lsub")
241 commandsStateSelected = stateCommands("close", "unselect", "expunge", "search", "fetch", "store", "copy", "move", "uid expunge", "uid search", "uid fetch", "uid store", "uid copy", "uid move")
244var commands = map[string]func(c *conn, tag, cmd string, p *parser){
246 "capability": (*conn).cmdCapability,
247 "noop": (*conn).cmdNoop,
248 "logout": (*conn).cmdLogout,
252 "starttls": (*conn).cmdStarttls,
253 "authenticate": (*conn).cmdAuthenticate,
254 "login": (*conn).cmdLogin,
256 // Authenticated and selected.
257 "enable": (*conn).cmdEnable,
258 "select": (*conn).cmdSelect,
259 "examine": (*conn).cmdExamine,
260 "create": (*conn).cmdCreate,
261 "delete": (*conn).cmdDelete,
262 "rename": (*conn).cmdRename,
263 "subscribe": (*conn).cmdSubscribe,
264 "unsubscribe": (*conn).cmdUnsubscribe,
265 "list": (*conn).cmdList,
266 "lsub": (*conn).cmdLsub,
267 "namespace": (*conn).cmdNamespace,
268 "status": (*conn).cmdStatus,
269 "append": (*conn).cmdAppend,
270 "idle": (*conn).cmdIdle,
273 "check": (*conn).cmdCheck,
274 "close": (*conn).cmdClose,
275 "unselect": (*conn).cmdUnselect,
276 "expunge": (*conn).cmdExpunge,
277 "uid expunge": (*conn).cmdUIDExpunge,
278 "search": (*conn).cmdSearch,
279 "uid search": (*conn).cmdUIDSearch,
280 "fetch": (*conn).cmdFetch,
281 "uid fetch": (*conn).cmdUIDFetch,
282 "store": (*conn).cmdStore,
283 "uid store": (*conn).cmdUIDStore,
284 "copy": (*conn).cmdCopy,
285 "uid copy": (*conn).cmdUIDCopy,
286 "move": (*conn).cmdMove,
287 "uid move": (*conn).cmdUIDMove,
290var errIO = errors.New("fatal io error") // For read/write errors and errors that should close the connection.
291var errProtocol = errors.New("fatal protocol error") // For protocol errors for which a stack trace should be printed.
295// check err for sanity.
296// if not nil and checkSanity true (set during tests), then panic. if not nil during normal operation, just log.
297func (c *conn) xsanity(err error, format string, args ...any) {
302 panic(fmt.Errorf("%s: %s", fmt.Sprintf(format, args...), err))
304 c.log.Errorx(fmt.Sprintf(format, args...), err)
309// Listen initializes all imap listeners for the configuration, and stores them for Serve to start them.
311 names := maps.Keys(mox.Conf.Static.Listeners)
313 for _, name := range names {
314 listener := mox.Conf.Static.Listeners[name]
316 var tlsConfig *tls.Config
317 if listener.TLS != nil {
318 tlsConfig = listener.TLS.Config
321 if listener.IMAP.Enabled {
322 port := config.Port(listener.IMAP.Port, 143)
323 for _, ip := range listener.IPs {
324 listen1("imap", name, ip, port, tlsConfig, false, listener.IMAP.NoRequireSTARTTLS)
328 if listener.IMAPS.Enabled {
329 port := config.Port(listener.IMAPS.Port, 993)
330 for _, ip := range listener.IPs {
331 listen1("imaps", name, ip, port, tlsConfig, true, false)
339func listen1(protocol, listenerName, ip string, port int, tlsConfig *tls.Config, xtls, noRequireSTARTTLS bool) {
340 addr := net.JoinHostPort(ip, fmt.Sprintf("%d", port))
341 if os.Getuid() == 0 {
342 xlog.Print("listening for imap", mlog.Field("listener", listenerName), mlog.Field("addr", addr), mlog.Field("protocol", protocol))
344 network := mox.Network(ip)
345 ln, err := mox.Listen(network, addr)
347 xlog.Fatalx("imap: listen for imap", err, mlog.Field("protocol", protocol), mlog.Field("listener", listenerName))
350 ln = tls.NewListener(ln, tlsConfig)
355 conn, err := ln.Accept()
357 xlog.Infox("imap: accept", err, mlog.Field("protocol", protocol), mlog.Field("listener", listenerName))
361 metricIMAPConnection.WithLabelValues(protocol).Inc()
362 go serve(listenerName, mox.Cid(), tlsConfig, conn, xtls, noRequireSTARTTLS)
366 servers = append(servers, serve)
369// Serve starts serving on all listeners, launching a goroutine per listener.
371 for _, serve := range servers {
377// returns whether this connection accepts utf-8 in strings.
378func (c *conn) utf8strings() bool {
379 return c.enabled[capIMAP4rev2] || c.enabled[capUTF8Accept]
382func (c *conn) xdbwrite(fn func(tx *bstore.Tx)) {
383 err := c.account.DB.Write(context.TODO(), func(tx *bstore.Tx) error {
387 xcheckf(err, "transaction")
390func (c *conn) xdbread(fn func(tx *bstore.Tx)) {
391 err := c.account.DB.Read(context.TODO(), func(tx *bstore.Tx) error {
395 xcheckf(err, "transaction")
398// Closes the currently selected/active mailbox, setting state from selected to authenticated.
399// Does not remove messages marked for deletion.
400func (c *conn) unselect() {
401 if c.state == stateSelected {
402 c.state = stateAuthenticated
408func (c *conn) setSlow(on bool) {
410 c.log.Debug("connection changed to slow")
411 } else if !on && c.slow {
412 c.log.Debug("connection restored to regular pace")
417// Write makes a connection an io.Writer. It panics for i/o errors. These errors
418// are handled in the connection command loop.
419func (c *conn) Write(buf []byte) (int, error) {
427 err := c.conn.SetWriteDeadline(time.Now().Add(30 * time.Second))
428 c.log.Check(err, "setting write deadline")
430 nn, err := c.conn.Write(buf[:chunk])
432 panic(fmt.Errorf("write: %s (%w)", err, errIO))
436 if len(buf) > 0 && badClientDelay > 0 {
437 mox.Sleep(mox.Context, badClientDelay)
443func (c *conn) xtrace(level mlog.Level) func() {
449 c.tr.SetTrace(mlog.LevelTrace)
450 c.tw.SetTrace(mlog.LevelTrace)
454// Cache of line buffers for reading commands.
456var bufpool = moxio.NewBufpool(8, 16*1024)
458// read line from connection, not going through line channel.
459func (c *conn) readline0() (string, error) {
460 if c.slow && badClientDelay > 0 {
461 mox.Sleep(mox.Context, badClientDelay)
464 d := 30 * time.Minute
465 if c.state == stateNotAuthenticated {
468 err := c.conn.SetReadDeadline(time.Now().Add(d))
469 c.log.Check(err, "setting read deadline")
471 line, err := bufpool.Readline(c.br)
472 if err != nil && errors.Is(err, moxio.ErrLineTooLong) {
473 return "", fmt.Errorf("%s (%w)", err, errProtocol)
474 } else if err != nil {
475 return "", fmt.Errorf("%s (%w)", err, errIO)
480func (c *conn) lineChan() chan lineErr {
482 c.line = make(chan lineErr, 1)
484 line, err := c.readline0()
485 c.line <- lineErr{line, err}
491// readline from either the c.line channel, or otherwise read from connection.
492func (c *conn) readline(readCmd bool) string {
498 line, err = le.line, le.err
500 line, err = c.readline0()
503 if readCmd && errors.Is(err, os.ErrDeadlineExceeded) {
504 err := c.conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
505 c.log.Check(err, "setting write deadline")
506 c.writelinef("* BYE inactive")
508 if !errors.Is(err, errIO) && !errors.Is(err, errProtocol) {
509 err = fmt.Errorf("%s (%w)", err, errIO)
515 // We typically respond immediately (IDLE is an exception).
516 // The client may not be reading, or may have disappeared.
517 // Don't wait more than 5 minutes before closing down the connection.
518 // The write deadline is managed in IDLE as well.
519 // For unauthenticated connections, we require the client to read faster.
520 wd := 5 * time.Minute
521 if c.state == stateNotAuthenticated {
522 wd = 30 * time.Second
524 err = c.conn.SetWriteDeadline(time.Now().Add(wd))
525 c.log.Check(err, "setting write deadline")
530// write tagged command response, but first write pending changes.
531func (c *conn) writeresultf(format string, args ...any) {
532 c.bwriteresultf(format, args...)
536// write buffered tagged command response, but first write pending changes.
537func (c *conn) bwriteresultf(format string, args ...any) {
539 case "fetch", "store", "search":
543 c.applyChanges(c.comm.Get(), false)
546 c.bwritelinef(format, args...)
549func (c *conn) writelinef(format string, args ...any) {
550 c.bwritelinef(format, args...)
554// Buffer line for write.
555func (c *conn) bwritelinef(format string, args ...any) {
557 fmt.Fprintf(c.bw, format, args...)
560func (c *conn) xflush() {
562 xcheckf(err, "flush") // Should never happen, the Write caused by the Flush should panic on i/o error.
565func (c *conn) readCommand(tag *string) (cmd string, p *parser) {
566 line := c.readline(true)
567 p = newParser(line, c)
573 return cmd, newParser(p.remainder(), c)
576func (c *conn) xreadliteral(size int64, sync bool) string {
580 buf := make([]byte, size)
582 if err := c.conn.SetReadDeadline(time.Now().Add(30 * time.Second)); err != nil {
583 c.log.Errorx("setting read deadline", err)
586 _, err := io.ReadFull(c.br, buf)
588 // Cannot use xcheckf due to %w handling of errIO.
589 panic(fmt.Errorf("reading literal: %s (%w)", err, errIO))
595func (c *conn) xhighestModSeq(tx *bstore.Tx, mailboxID int64) store.ModSeq {
596 qms := bstore.QueryTx[store.Message](tx)
597 qms.FilterNonzero(store.Message{MailboxID: mailboxID})
598 qms.SortDesc("ModSeq")
601 if err == bstore.ErrAbsent {
602 return store.ModSeq(0)
604 xcheckf(err, "looking up highest modseq for mailbox")
608var cleanClose struct{} // Sentinel value for panic/recover indicating clean close of connection.
610func serve(listenerName string, cid int64, tlsConfig *tls.Config, nc net.Conn, xtls, noRequireSTARTTLS bool) {
612 if a, ok := nc.RemoteAddr().(*net.TCPAddr); ok {
615 // For net.Pipe, during tests.
616 remoteIP = net.ParseIP("127.0.0.10")
624 tlsConfig: tlsConfig,
626 noRequireSTARTTLS: noRequireSTARTTLS,
627 enabled: map[capability]bool{},
629 cmdStart: time.Now(),
631 c.log = xlog.MoreFields(func() []mlog.Pair {
634 mlog.Field("cid", c.cid),
635 mlog.Field("delta", now.Sub(c.lastlog)),
638 if c.username != "" {
639 l = append(l, mlog.Field("username", c.username))
643 c.tr = moxio.NewTraceReader(c.log, "C: ", c.conn)
644 c.tw = moxio.NewTraceWriter(c.log, "S: ", c)
645 // todo: tracing should be done on whatever comes out of c.br. the remote connection write a command plus data, and bufio can read it in one read, causing a command parser that sets the tracing level to data to have no effect. we are now typically logging sent messages, when mail clients append to the Sent mailbox.
646 c.br = bufio.NewReader(c.tr)
647 c.bw = bufio.NewWriter(c.tw)
649 // Many IMAP connections use IDLE to wait for new incoming messages. We'll enable
650 // keepalive to get a higher chance of the connection staying alive, or otherwise
651 // detecting broken connections early.
654 xconn = c.conn.(*tls.Conn).NetConn()
656 if tcpconn, ok := xconn.(*net.TCPConn); ok {
657 if err := tcpconn.SetKeepAlivePeriod(5 * time.Minute); err != nil {
658 c.log.Errorx("setting keepalive period", err)
659 } else if err := tcpconn.SetKeepAlive(true); err != nil {
660 c.log.Errorx("enabling keepalive", err)
664 c.log.Info("new connection", mlog.Field("remote", c.conn.RemoteAddr()), mlog.Field("local", c.conn.LocalAddr()), mlog.Field("tls", xtls), mlog.Field("listener", listenerName))
669 if c.account != nil {
671 err := c.account.Close()
672 c.xsanity(err, "close account")
678 if x == nil || x == cleanClose {
679 c.log.Info("connection closed")
680 } else if err, ok := x.(error); ok && isClosed(err) {
681 c.log.Infox("connection closed", err)
683 c.log.Error("unhandled panic", mlog.Field("err", x))
685 metrics.PanicInc(metrics.Imapserver)
690 case <-mox.Shutdown.Done():
692 c.writelinef("* BYE mox shutting down")
697 if !limiterConnectionrate.Add(c.remoteIP, time.Now(), 1) {
698 c.writelinef("* BYE connection rate from your ip or network too high, slow down please")
702 // If remote IP/network resulted in too many authentication failures, refuse to serve.
703 if !mox.LimiterFailedAuth.CanAdd(c.remoteIP, time.Now(), 1) {
704 metrics.AuthenticationRatelimitedInc("imap")
705 c.log.Debug("refusing connection due to many auth failures", mlog.Field("remoteip", c.remoteIP))
706 c.writelinef("* BYE too many auth failures")
710 if !limiterConnections.Add(c.remoteIP, time.Now(), 1) {
711 c.log.Debug("refusing connection due to many open connections", mlog.Field("remoteip", c.remoteIP))
712 c.writelinef("* BYE too many open connections from your ip or network")
715 defer limiterConnections.Add(c.remoteIP, time.Now(), -1)
717 // We register and unregister the original connection, in case it c.conn is
718 // replaced with a TLS connection later on.
719 mox.Connections.Register(nc, "imap", listenerName)
720 defer mox.Connections.Unregister(nc)
722 c.writelinef("* OK [CAPABILITY %s] mox imap", c.capabilities())
726 c.xflush() // For flushing errors, or possibly commands that did not flush explicitly.
730// isClosed returns whether i/o failed, typically because the connection is closed.
731// For connection errors, we often want to generate fewer logs.
732func isClosed(err error) bool {
733 return errors.Is(err, errIO) || errors.Is(err, errProtocol) || moxio.IsClosed(err)
736func (c *conn) command() {
737 var tag, cmd, cmdlow string
743 metricIMAPCommands.WithLabelValues(c.cmdMetric, result).Observe(float64(time.Since(c.cmdStart)) / float64(time.Second))
746 logFields := []mlog.Pair{
747 mlog.Field("cmd", c.cmd),
748 mlog.Field("duration", time.Since(c.cmdStart)),
753 if x == nil || x == cleanClose {
754 c.log.Debug("imap command done", logFields...)
763 c.log.Error("imap command panic", append([]mlog.Pair{mlog.Field("panic", x)}, logFields...)...)
768 var sxerr syntaxError
772 c.log.Infox("imap command ioerror", err, logFields...)
774 if errors.Is(err, errProtocol) {
778 } else if errors.As(err, &sxerr) {
781 // Other side is likely speaking something else than IMAP, send error message and
782 // stop processing because there is a good chance whatever they sent has multiple
784 c.writelinef("* BYE please try again speaking imap")
787 c.log.Debugx("imap command syntax error", sxerr.err, logFields...)
788 c.log.Info("imap syntax error", mlog.Field("lastline", c.lastLine))
789 fatal := strings.HasSuffix(c.lastLine, "+}")
791 err := c.conn.SetWriteDeadline(time.Now().Add(5 * time.Second))
792 c.log.Check(err, "setting write deadline")
794 if sxerr.line != "" {
795 c.bwritelinef("%s", sxerr.line)
798 if sxerr.code != "" {
799 code = "[" + sxerr.code + "] "
801 c.bwriteresultf("%s BAD %s%s unrecognized syntax/command: %v", tag, code, cmd, sxerr.errmsg)
804 panic(fmt.Errorf("aborting connection after syntax error for command with non-sync literal: %w", errProtocol))
806 } else if errors.As(err, &serr) {
807 result = "servererror"
808 c.log.Errorx("imap command server error", err, logFields...)
810 c.bwriteresultf("%s NO %s %v", tag, cmd, err)
811 } else if errors.As(err, &uerr) {
813 c.log.Debugx("imap command user error", err, logFields...)
815 c.bwriteresultf("%s NO [%s] %s %v", tag, uerr.code, cmd, err)
817 c.bwriteresultf("%s NO %s %v", tag, cmd, err)
820 // Other type of panic, we pass it on, aborting the connection.
822 c.log.Errorx("imap command panic", err, logFields...)
828 cmd, p = c.readCommand(&tag)
829 cmdlow = strings.ToLower(cmd)
831 c.cmdStart = time.Now()
832 c.cmdMetric = "(unrecognized)"
835 case <-mox.Shutdown.Done():
837 c.writelinef("* BYE shutting down")
842 fn := commands[cmdlow]
844 xsyntaxErrorf("unknown command %q", cmd)
849 // Check if command is allowed in this state.
850 if _, ok1 := commandsStateAny[cmdlow]; ok1 {
851 } else if _, ok2 := commandsStateNotAuthenticated[cmdlow]; ok2 && c.state == stateNotAuthenticated {
852 } else if _, ok3 := commandsStateAuthenticated[cmdlow]; ok3 && c.state == stateAuthenticated || c.state == stateSelected {
853 } else if _, ok4 := commandsStateSelected[cmdlow]; ok4 && c.state == stateSelected {
854 } else if ok1 || ok2 || ok3 || ok4 {
855 xuserErrorf("not allowed in this connection state")
857 xserverErrorf("unrecognized command")
863func (c *conn) broadcast(changes []store.Change) {
864 if len(changes) == 0 {
867 c.log.Debug("broadcast changes", mlog.Field("changes", changes))
868 c.comm.Broadcast(changes)
871// matchStringer matches a string against reference + mailbox patterns.
872type matchStringer interface {
873 MatchString(s string) bool
878// MatchString for noMatch always returns false.
879func (noMatch) MatchString(s string) bool {
883// xmailboxPatternMatcher returns a matcher for mailbox names given the reference and patterns.
884// Patterns can include "%" and "*", matching any character excluding and including a slash respectively.
885func xmailboxPatternMatcher(ref string, patterns []string) matchStringer {
886 if strings.HasPrefix(ref, "/") {
891 for _, pat := range patterns {
892 if strings.HasPrefix(pat, "/") {
898 s = filepath.Join(ref, pat)
901 // Fix casing for all Inbox paths.
902 first := strings.SplitN(s, "/", 2)[0]
903 if strings.EqualFold(first, "Inbox") {
904 s = "Inbox" + s[len("Inbox"):]
909 for _, c := range s {
915 rs += regexp.QuoteMeta(string(c))
918 subs = append(subs, rs)
924 rs := "^(" + strings.Join(subs, "|") + ")$"
925 re, err := regexp.Compile(rs)
926 xcheckf(err, "compiling regexp for mailbox patterns")
930func (c *conn) sequence(uid store.UID) msgseq {
931 return uidSearch(c.uids, uid)
934func uidSearch(uids []store.UID, uid store.UID) msgseq {
951func (c *conn) xsequence(uid store.UID) msgseq {
952 seq := c.sequence(uid)
954 xserverErrorf("unknown uid %d (%w)", uid, errProtocol)
959func (c *conn) sequenceRemove(seq msgseq, uid store.UID) {
961 if c.uids[i] != uid {
962 xserverErrorf(fmt.Sprintf("got uid %d at msgseq %d, expected uid %d", uid, seq, c.uids[i]))
964 copy(c.uids[i:], c.uids[i+1:])
965 c.uids = c.uids[:len(c.uids)-1]
971// add uid to the session. care must be taken that pending changes are fetched
972// while holding the account wlock, and applied before adding this uid, because
973// those pending changes may contain another new uid that has to be added first.
974func (c *conn) uidAppend(uid store.UID) {
975 if uidSearch(c.uids, uid) > 0 {
976 xserverErrorf("uid already present (%w)", errProtocol)
978 if len(c.uids) > 0 && uid < c.uids[len(c.uids)-1] {
979 xserverErrorf("new uid %d is smaller than last uid %d (%w)", uid, c.uids[len(c.uids)-1], errProtocol)
981 c.uids = append(c.uids, uid)
987// sanity check that uids are in ascending order.
988func checkUIDs(uids []store.UID) {
989 for i, uid := range uids {
990 if uid == 0 || i > 0 && uid <= uids[i-1] {
991 xserverErrorf("bad uids %v", uids)
996func (c *conn) xnumSetUIDs(isUID bool, nums numSet) []store.UID {
997 _, uids := c.xnumSetConditionUIDs(false, true, isUID, nums)
1001func (c *conn) xnumSetCondition(isUID bool, nums numSet) []any {
1002 uidargs, _ := c.xnumSetConditionUIDs(true, false, isUID, nums)
1006func (c *conn) xnumSetConditionUIDs(forDB, returnUIDs bool, isUID bool, nums numSet) ([]any, []store.UID) {
1007 if nums.searchResult {
1008 // Update previously stored UIDs. Some may have been deleted.
1009 // Once deleted a UID will never come back, so we'll just remove those uids.
1011 for _, uid := range c.searchResult {
1012 if uidSearch(c.uids, uid) > 0 {
1013 c.searchResult[o] = uid
1017 c.searchResult = c.searchResult[:o]
1018 uidargs := make([]any, len(c.searchResult))
1019 for i, uid := range c.searchResult {
1022 return uidargs, c.searchResult
1026 var uids []store.UID
1028 add := func(uid store.UID) {
1030 uidargs = append(uidargs, uid)
1033 uids = append(uids, uid)
1038 // Sequence numbers that don't exist, or * on an empty mailbox, should result in a BAD response.
../rfc/9051:7018
1039 for _, r := range nums.ranges {
1042 if len(c.uids) == 0 {
1043 xsyntaxErrorf("invalid seqset * on empty mailbox")
1045 ia = len(c.uids) - 1
1047 ia = int(r.first.number - 1)
1048 if ia >= len(c.uids) {
1049 xsyntaxErrorf("msgseq %d not in mailbox", r.first.number)
1058 if len(c.uids) == 0 {
1059 xsyntaxErrorf("invalid seqset * on empty mailbox")
1061 ib = len(c.uids) - 1
1063 ib = int(r.last.number - 1)
1064 if ib >= len(c.uids) {
1065 xsyntaxErrorf("msgseq %d not in mailbox", r.last.number)
1071 for _, uid := range c.uids[ia : ib+1] {
1075 return uidargs, uids
1078 // UIDs that do not exist can be ignored.
1079 if len(c.uids) == 0 {
1083 for _, r := range nums.ranges {
1089 uida := store.UID(r.first.number)
1091 uida = c.uids[len(c.uids)-1]
1094 uidb := store.UID(last.number)
1096 uidb = c.uids[len(c.uids)-1]
1100 uida, uidb = uidb, uida
1103 // Binary search for uida.
1108 if uida < c.uids[m] {
1110 } else if uida > c.uids[m] {
1117 for _, uid := range c.uids[s:] {
1118 if uid >= uida && uid <= uidb {
1120 } else if uid > uidb {
1126 return uidargs, uids
1129func (c *conn) ok(tag, cmd string) {
1130 c.bwriteresultf("%s OK %s done", tag, cmd)
1134// xcheckmailboxname checks if name is valid, returning an INBOX-normalized name.
1135// I.e. it changes various casings of INBOX and INBOX/* to Inbox and Inbox/*.
1136// Name is invalid if it contains leading/trailing/double slashes, or when it isn't
1137// unicode-normalized, or when empty or has special characters.
1138func xcheckmailboxname(name string, allowInbox bool) string {
1139 name, isinbox, err := store.CheckMailboxName(name, allowInbox)
1141 xuserErrorf("special mailboxname Inbox not allowed")
1142 } else if err != nil {
1143 xusercodeErrorf("CANNOT", err.Error())
1148// Lookup mailbox by name.
1149// If the mailbox does not exist, panic is called with a user error.
1150// Must be called with account rlock held.
1151func (c *conn) xmailbox(tx *bstore.Tx, name string, missingErrCode string) store.Mailbox {
1152 mb, err := c.account.MailboxFind(tx, name)
1153 xcheckf(err, "finding mailbox")
1155 // missingErrCode can be empty, or e.g. TRYCREATE or ALREADYEXISTS.
1156 xusercodeErrorf(missingErrCode, "%w", store.ErrUnknownMailbox)
1161// Lookup mailbox by ID.
1162// If the mailbox does not exist, panic is called with a user error.
1163// Must be called with account rlock held.
1164func (c *conn) xmailboxID(tx *bstore.Tx, id int64) store.Mailbox {
1165 mb := store.Mailbox{ID: id}
1167 if err == bstore.ErrAbsent {
1168 xuserErrorf("%w", store.ErrUnknownMailbox)
1173// Apply changes to our session state.
1174// If initial is false, updates like EXISTS and EXPUNGE are written to the client.
1175// If initial is true, we only apply the changes.
1176// Should not be called while holding locks, as changes are written to client connections, which can block.
1177// Does not flush output.
1178func (c *conn) applyChanges(changes []store.Change, initial bool) {
1179 if len(changes) == 0 {
1183 err := c.conn.SetWriteDeadline(time.Now().Add(5 * time.Minute))
1184 c.log.Check(err, "setting write deadline")
1186 c.log.Debug("applying changes", mlog.Field("changes", changes))
1188 // Only keep changes for the selected mailbox, and changes that are always relevant.
1189 var n []store.Change
1190 for _, change := range changes {
1192 switch ch := change.(type) {
1193 case store.ChangeAddUID:
1195 case store.ChangeRemoveUIDs:
1197 case store.ChangeFlags:
1199 case store.ChangeRemoveMailbox, store.ChangeAddMailbox, store.ChangeRenameMailbox, store.ChangeAddSubscription:
1200 n = append(n, change)
1202 case store.ChangeMailboxCounts, store.ChangeMailboxSpecialUse, store.ChangeMailboxKeywords, store.ChangeThread:
1204 panic(fmt.Errorf("missing case for %#v", change))
1206 if c.state == stateSelected && mbID == c.mailboxID {
1207 n = append(n, change)
1212 qresync := c.enabled[capQresync]
1213 condstore := c.enabled[capCondstore]
1216 for i < len(changes) {
1217 // First process all new uids. So we only send a single EXISTS.
1218 var adds []store.ChangeAddUID
1219 for ; i < len(changes); i++ {
1220 ch, ok := changes[i].(store.ChangeAddUID)
1224 seq := c.sequence(ch.UID)
1225 if seq > 0 && initial {
1229 adds = append(adds, ch)
1235 // Write the exists, and the UID and flags as well. Hopefully the client waits for
1236 // long enough after the EXISTS to see these messages, and doesn't request them
1237 // again with a FETCH.
1238 c.bwritelinef("* %d EXISTS", len(c.uids))
1239 for _, add := range adds {
1240 seq := c.xsequence(add.UID)
1241 var modseqStr string
1243 modseqStr = fmt.Sprintf(" MODSEQ (%d)", add.ModSeq.Client())
1245 c.bwritelinef("* %d FETCH (UID %d FLAGS %s%s)", seq, add.UID, flaglist(add.Flags, add.Keywords).pack(c), modseqStr)
1250 change := changes[i]
1253 switch ch := change.(type) {
1254 case store.ChangeRemoveUIDs:
1255 var vanishedUIDs numSet
1256 for _, uid := range ch.UIDs {
1259 seq = c.sequence(uid)
1264 seq = c.xsequence(uid)
1266 c.sequenceRemove(seq, uid)
1269 vanishedUIDs.append(uint32(uid))
1271 c.bwritelinef("* %d EXPUNGE", seq)
1277 for _, s := range vanishedUIDs.Strings(4*1024 - 32) {
1278 c.bwritelinef("* VANISHED %s", s)
1281 case store.ChangeFlags:
1282 // The uid can be unknown if we just expunged it while another session marked it as deleted just before.
1283 seq := c.sequence(ch.UID)
1288 var modseqStr string
1290 modseqStr = fmt.Sprintf(" MODSEQ (%d)", ch.ModSeq.Client())
1292 c.bwritelinef("* %d FETCH (UID %d FLAGS %s%s)", seq, ch.UID, flaglist(ch.Flags, ch.Keywords).pack(c), modseqStr)
1294 case store.ChangeRemoveMailbox:
1295 // Only announce \NonExistent to modern clients, otherwise they may ignore the
1296 // unrecognized \NonExistent and interpret this as a newly created mailbox, while
1297 // the goal was to remove it...
1298 if c.enabled[capIMAP4rev2] {
1299 c.bwritelinef(`* LIST (\NonExistent) "/" %s`, astring(ch.Name).pack(c))
1301 case store.ChangeAddMailbox:
1302 c.bwritelinef(`* LIST (%s) "/" %s`, strings.Join(ch.Flags, " "), astring(ch.Mailbox.Name).pack(c))
1303 case store.ChangeRenameMailbox:
1304 c.bwritelinef(`* LIST (%s) "/" %s ("OLDNAME" (%s))`, strings.Join(ch.Flags, " "), astring(ch.NewName).pack(c), string0(ch.OldName).pack(c))
1305 case store.ChangeAddSubscription:
1306 c.bwritelinef(`* LIST (%s) "/" %s`, strings.Join(append([]string{`\Subscribed`}, ch.Flags...), " "), astring(ch.Name).pack(c))
1308 panic(fmt.Sprintf("internal error, missing case for %#v", change))
1313// Capability returns the capabilities this server implements and currently has
1314// available given the connection state.
1317func (c *conn) cmdCapability(tag, cmd string, p *parser) {
1323 caps := c.capabilities()
1326 c.bwritelinef("* CAPABILITY %s", caps)
1330// capabilities returns non-empty string with available capabilities based on connection state.
1331// For use in cmdCapability and untagged OK responses on connection start, login and authenticate.
1332func (c *conn) capabilities() string {
1333 caps := serverCapabilities
1335 // We only allow starting without TLS when explicitly configured, in violation of RFC.
1336 if !c.tls && c.tlsConfig != nil {
1339 if c.tls || c.noRequireSTARTTLS {
1340 caps += " AUTH=PLAIN"
1342 caps += " LOGINDISABLED"
1347// No op, but useful for retrieving pending changes as untagged responses, e.g. of
1351func (c *conn) cmdNoop(tag, cmd string, p *parser) {
1359// Logout, after which server closes the connection.
1362func (c *conn) cmdLogout(tag, cmd string, p *parser) {
1369 c.state = stateNotAuthenticated
1371 c.bwritelinef("* BYE thanks")
1376// Clients can use ID to tell the server which software they are using. Servers can
1377// respond with their version. For statistics/logging/debugging purposes.
1380func (c *conn) cmdID(tag, cmd string, p *parser) {
1385 var params map[string]string
1387 params = map[string]string{}
1389 if len(params) > 0 {
1395 if _, ok := params[k]; ok {
1396 xsyntaxErrorf("duplicate key %q", k)
1405 // We just log the client id.
1406 c.log.Info("client id", mlog.Field("params", params))
1410 c.bwritelinef(`* ID ("name" "mox" "version" %s)`, string0(moxvar.Version).pack(c))
1414// STARTTLS enables TLS on the connection, after a plain text start.
1415// Only allowed if TLS isn't already enabled, either through connecting to a
1416// TLS-enabled TCP port, or a previous STARTTLS command.
1417// After STARTTLS, plain text authentication typically becomes available.
1419// Status: Not authenticated.
1420func (c *conn) cmdStarttls(tag, cmd string, p *parser) {
1431 if n := c.br.Buffered(); n > 0 {
1432 buf := make([]byte, n)
1433 _, err := io.ReadFull(c.br, buf)
1434 xcheckf(err, "reading buffered data for tls handshake")
1435 conn = &prefixConn{buf, conn}
1439 cidctx := context.WithValue(mox.Context, mlog.CidKey, c.cid)
1440 ctx, cancel := context.WithTimeout(cidctx, time.Minute)
1442 tlsConn := tls.Server(conn, c.tlsConfig)
1443 c.log.Debug("starting tls server handshake")
1444 if err := tlsConn.HandshakeContext(ctx); err != nil {
1445 panic(fmt.Errorf("starttls handshake: %s (%w)", err, errIO))
1448 tlsversion, ciphersuite := mox.TLSInfo(tlsConn)
1449 c.log.Debug("tls server handshake done", mlog.Field("tls", tlsversion), mlog.Field("ciphersuite", ciphersuite))
1452 c.tr = moxio.NewTraceReader(c.log, "C: ", c.conn)
1453 c.tw = moxio.NewTraceWriter(c.log, "S: ", c)
1454 c.br = bufio.NewReader(c.tr)
1455 c.bw = bufio.NewWriter(c.tw)
1459// Authenticate using SASL. Supports multiple back and forths between client and
1460// server to finish authentication, unlike LOGIN which is just a single
1461// username/password.
1463// Status: Not authenticated.
1464func (c *conn) cmdAuthenticate(tag, cmd string, p *parser) {
1468 // For many failed auth attempts, slow down verification attempts.
1469 if c.authFailed > 3 && authFailDelay > 0 {
1470 mox.Sleep(mox.Context, time.Duration(c.authFailed-3)*authFailDelay)
1472 c.authFailed++ // Compensated on success.
1474 // On the 3rd failed authentication, start responding slowly. Successful auth will
1475 // cause fast responses again.
1476 if c.authFailed >= 3 {
1481 var authVariant string
1482 authResult := "error"
1484 metrics.AuthenticationInc("imap", authVariant, authResult)
1487 mox.LimiterFailedAuth.Reset(c.remoteIP, time.Now())
1489 mox.LimiterFailedAuth.Add(c.remoteIP, time.Now(), 1)
1495 authType := p.xatom()
1497 xreadInitial := func() []byte {
1501 line = c.readline(false)
1505 line = p.remainder()
1508 line = "" // Base64 decode will result in empty buffer.
1513 authResult = "aborted"
1514 xsyntaxErrorf("authenticate aborted by client")
1516 buf, err := base64.StdEncoding.DecodeString(line)
1518 xsyntaxErrorf("parsing base64: %v", err)
1523 xreadContinuation := func() []byte {
1524 line := c.readline(false)
1526 authResult = "aborted"
1527 xsyntaxErrorf("authenticate aborted by client")
1529 buf, err := base64.StdEncoding.DecodeString(line)
1531 xsyntaxErrorf("parsing base64: %v", err)
1536 switch strings.ToUpper(authType) {
1538 authVariant = "plain"
1540 if !c.noRequireSTARTTLS && !c.tls {
1542 xusercodeErrorf("PRIVACYREQUIRED", "tls required for login")
1545 // Plain text passwords, mark as traceauth.
1546 defer c.xtrace(mlog.LevelTraceauth)()
1547 buf := xreadInitial()
1548 c.xtrace(mlog.LevelTrace) // Restore.
1549 plain := bytes.Split(buf, []byte{0})
1550 if len(plain) != 3 {
1551 xsyntaxErrorf("bad plain auth data, expected 3 nul-separated tokens, got %d tokens", len(plain))
1553 authz := string(plain[0])
1554 authc := string(plain[1])
1555 password := string(plain[2])
1557 if authz != "" && authz != authc {
1558 xusercodeErrorf("AUTHORIZATIONFAILED", "cannot assume role")
1561 acc, err := store.OpenEmailAuth(authc, password)
1563 if errors.Is(err, store.ErrUnknownCredentials) {
1564 authResult = "badcreds"
1565 c.log.Info("authentication failed", mlog.Field("username", authc))
1566 xusercodeErrorf("AUTHENTICATIONFAILED", "bad credentials")
1568 xusercodeErrorf("", "error")
1574 authVariant = strings.ToLower(authType)
1580 chal := fmt.Sprintf("<%d.%d@%s>", uint64(mox.CryptoRandInt()), time.Now().UnixNano(), mox.Conf.Static.HostnameDomain.ASCII)
1581 c.writelinef("+ %s", base64.StdEncoding.EncodeToString([]byte(chal)))
1583 resp := xreadContinuation()
1584 t := strings.Split(string(resp), " ")
1585 if len(t) != 2 || len(t[1]) != 2*md5.Size {
1586 xsyntaxErrorf("malformed cram-md5 response")
1589 c.log.Debug("cram-md5 auth", mlog.Field("address", addr))
1590 acc, _, err := store.OpenEmail(addr)
1592 if errors.Is(err, store.ErrUnknownCredentials) {
1593 c.log.Info("failed authentication attempt", mlog.Field("username", addr), mlog.Field("remote", c.remoteIP))
1594 xusercodeErrorf("AUTHENTICATIONFAILED", "bad credentials")
1596 xserverErrorf("looking up address: %v", err)
1601 c.xsanity(err, "close account")
1604 var ipadhash, opadhash hash.Hash
1605 acc.WithRLock(func() {
1606 err := acc.DB.Read(context.TODO(), func(tx *bstore.Tx) error {
1607 password, err := bstore.QueryTx[store.Password](tx).Get()
1608 if err == bstore.ErrAbsent {
1609 c.log.Info("failed authentication attempt", mlog.Field("username", addr), mlog.Field("remote", c.remoteIP))
1610 xusercodeErrorf("AUTHENTICATIONFAILED", "bad credentials")
1616 ipadhash = password.CRAMMD5.Ipad
1617 opadhash = password.CRAMMD5.Opad
1620 xcheckf(err, "tx read")
1622 if ipadhash == nil || opadhash == nil {
1623 c.log.Info("cram-md5 auth attempt without derived secrets set, save password again to store secrets", mlog.Field("username", addr))
1624 c.log.Info("failed authentication attempt", mlog.Field("username", addr), mlog.Field("remote", c.remoteIP))
1625 xusercodeErrorf("AUTHENTICATIONFAILED", "bad credentials")
1629 ipadhash.Write([]byte(chal))
1630 opadhash.Write(ipadhash.Sum(nil))
1631 digest := fmt.Sprintf("%x", opadhash.Sum(nil))
1633 c.log.Info("failed authentication attempt", mlog.Field("username", addr), mlog.Field("remote", c.remoteIP))
1634 xusercodeErrorf("AUTHENTICATIONFAILED", "bad credentials")
1638 acc = nil // Cancel cleanup.
1641 case "SCRAM-SHA-1", "SCRAM-SHA-256":
1642 // todo: improve handling of errors during scram. e.g. invalid parameters. should we abort the imap command, or continue until the end and respond with a scram-level error?
1643 // todo: use single implementation between ../imapserver/server.go and ../smtpserver/server.go
1645 authVariant = strings.ToLower(authType)
1646 var h func() hash.Hash
1647 if authVariant == "scram-sha-1" {
1653 // No plaintext credentials, we can log these normally.
1655 c0 := xreadInitial()
1656 ss, err := scram.NewServer(h, c0)
1658 xsyntaxErrorf("starting scram: %s", err)
1660 c.log.Debug("scram auth", mlog.Field("authentication", ss.Authentication))
1661 acc, _, err := store.OpenEmail(ss.Authentication)
1663 // todo: we could continue scram with a generated salt, deterministically generated
1664 // from the username. that way we don't have to store anything but attackers cannot
1665 // learn if an account exists. same for absent scram saltedpassword below.
1666 xuserErrorf("scram not possible")
1671 c.xsanity(err, "close account")
1674 if ss.Authorization != "" && ss.Authorization != ss.Authentication {
1675 xuserErrorf("authentication with authorization for different user not supported")
1677 var xscram store.SCRAM
1678 acc.WithRLock(func() {
1679 err := acc.DB.Read(context.TODO(), func(tx *bstore.Tx) error {
1680 password, err := bstore.QueryTx[store.Password](tx).Get()
1681 if authVariant == "scram-sha-1" {
1682 xscram = password.SCRAMSHA1
1684 xscram = password.SCRAMSHA256
1686 if err == bstore.ErrAbsent || err == nil && (len(xscram.Salt) == 0 || xscram.Iterations == 0 || len(xscram.SaltedPassword) == 0) {
1687 c.log.Info("scram auth attempt without derived secrets set, save password again to store secrets", mlog.Field("address", ss.Authentication))
1688 xuserErrorf("scram not possible")
1690 xcheckf(err, "fetching credentials")
1693 xcheckf(err, "read tx")
1695 s1, err := ss.ServerFirst(xscram.Iterations, xscram.Salt)
1696 xcheckf(err, "scram first server step")
1697 c.writelinef("+ %s", base64.StdEncoding.EncodeToString([]byte(s1)))
1698 c2 := xreadContinuation()
1699 s3, err := ss.Finish(c2, xscram.SaltedPassword)
1701 c.writelinef("+ %s", base64.StdEncoding.EncodeToString([]byte(s3)))
1704 c.readline(false) // Should be "*" for cancellation.
1705 if errors.Is(err, scram.ErrInvalidProof) {
1706 authResult = "badcreds"
1707 c.log.Info("failed authentication attempt", mlog.Field("username", ss.Authentication), mlog.Field("remote", c.remoteIP))
1708 xusercodeErrorf("AUTHENTICATIONFAILED", "bad credentials")
1710 xuserErrorf("server final: %w", err)
1714 // The message should be empty. todo: should we require it is empty?
1718 acc = nil // Cancel cleanup.
1719 c.username = ss.Authentication
1722 xuserErrorf("method not supported")
1728 c.comm = store.RegisterComm(c.account)
1729 c.state = stateAuthenticated
1730 c.writeresultf("%s OK [CAPABILITY %s] authenticate done", tag, c.capabilities())
1733// Login logs in with username and password.
1735// Status: Not authenticated.
1736func (c *conn) cmdLogin(tag, cmd string, p *parser) {
1739 authResult := "error"
1741 metrics.AuthenticationInc("imap", "login", authResult)
1744 // todo: get this line logged with traceauth. the plaintext password is included on the command line, which we've already read (before dispatching to this function).
1748 userid := p.xastring()
1750 password := p.xastring()
1753 if !c.noRequireSTARTTLS && !c.tls {
1755 xusercodeErrorf("PRIVACYREQUIRED", "tls required for login")
1758 // For many failed auth attempts, slow down verification attempts.
1759 if c.authFailed > 3 && authFailDelay > 0 {
1760 mox.Sleep(mox.Context, time.Duration(c.authFailed-3)*authFailDelay)
1762 c.authFailed++ // Compensated on success.
1764 // On the 3rd failed authentication, start responding slowly. Successful auth will
1765 // cause fast responses again.
1766 if c.authFailed >= 3 {
1771 acc, err := store.OpenEmailAuth(userid, password)
1773 authResult = "badcreds"
1775 if errors.Is(err, store.ErrUnknownCredentials) {
1776 code = "AUTHENTICATIONFAILED"
1777 c.log.Info("failed authentication attempt", mlog.Field("username", userid), mlog.Field("remote", c.remoteIP))
1779 xusercodeErrorf(code, "login failed")
1785 c.comm = store.RegisterComm(acc)
1786 c.state = stateAuthenticated
1788 c.writeresultf("%s OK [CAPABILITY %s] login done", tag, c.capabilities())
1791// Enable explicitly opts in to an extension. A server can typically send new kinds
1792// of responses to a client. Most extensions do not require an ENABLE because a
1793// client implicitly opts in to new response syntax by making a requests that uses
1794// new optional extension request syntax.
1796// State: Authenticated and selected.
1797func (c *conn) cmdEnable(tag, cmd string, p *parser) {
1803 caps := []string{p.xatom()}
1806 caps = append(caps, p.xatom())
1809 // Clients should only send capabilities that need enabling.
1810 // We should only echo that we recognize as needing enabling.
1813 for _, s := range caps {
1814 cap := capability(strings.ToUpper(s))
1819 c.enabled[cap] = true
1822 c.enabled[cap] = true
1828 if qresync && !c.enabled[capCondstore] {
1829 c.xensureCondstore(nil)
1830 enabled += " CONDSTORE"
1834 c.bwritelinef("* ENABLED%s", enabled)
1839// If a mailbox is selected, an untagged OK with HIGHESTMODSEQ is written to the
1840// client. If tx is non-nil, it is used to read the HIGHESTMODSEQ from the
1841// database. Otherwise a new read-only transaction is created.
1842func (c *conn) xensureCondstore(tx *bstore.Tx) {
1843 if !c.enabled[capCondstore] {
1844 c.enabled[capCondstore] = true
1845 // todo spec: can we send an untagged enabled response?
1847 if c.mailboxID <= 0 {
1850 var modseq store.ModSeq
1852 modseq = c.xhighestModSeq(tx, c.mailboxID)
1854 c.xdbread(func(tx *bstore.Tx) {
1855 modseq = c.xhighestModSeq(tx, c.mailboxID)
1858 c.bwritelinef("* OK [HIGHESTMODSEQ %d] after condstore-enabling command", modseq.Client())
1862// State: Authenticated and selected.
1863func (c *conn) cmdSelect(tag, cmd string, p *parser) {
1864 c.cmdSelectExamine(true, tag, cmd, p)
1867// State: Authenticated and selected.
1868func (c *conn) cmdExamine(tag, cmd string, p *parser) {
1869 c.cmdSelectExamine(false, tag, cmd, p)
1872// Select and examine are almost the same commands. Select just opens a mailbox for
1873// read/write and examine opens a mailbox readonly.
1875// State: Authenticated and selected.
1876func (c *conn) cmdSelectExamine(isselect bool, tag, cmd string, p *parser) {
1884 name := p.xmailbox()
1886 var qruidvalidity uint32
1887 var qrmodseq int64 // QRESYNC required parameters.
1888 var qrknownUIDs, qrknownSeqSet, qrknownUIDSet *numSet // QRESYNC optional parameters.
1890 seen := map[string]bool{}
1892 for len(seen) == 0 || !p.take(")") {
1893 w := p.xtakelist("CONDSTORE", "QRESYNC")
1895 xsyntaxErrorf("duplicate select parameter %s", w)
1905 // Note: unlike with CONDSTORE, there are no QRESYNC-related commands/parameters
1906 // that enable capabilities.
1907 if !c.enabled[capQresync] {
1909 xsyntaxErrorf("QRESYNC must first be enabled")
1915 qrmodseq = p.xnznumber64()
1917 seqMatchData := p.take("(")
1921 seqMatchData = p.take(" (")
1924 ss0 := p.xnumSet0(false, false)
1925 qrknownSeqSet = &ss0
1927 ss1 := p.xnumSet0(false, false)
1928 qrknownUIDSet = &ss1
1934 panic("missing case for select param " + w)
1940 // Deselect before attempting the new select. This means we will deselect when an
1941 // error occurs during select.
1943 if c.state == stateSelected {
1945 c.bwritelinef("* OK [CLOSED] x")
1949 name = xcheckmailboxname(name, true)
1951 var highestModSeq store.ModSeq
1952 var highDeletedModSeq store.ModSeq
1953 var firstUnseen msgseq = 0
1954 var mb store.Mailbox
1955 c.account.WithRLock(func() {
1956 c.xdbread(func(tx *bstore.Tx) {
1957 mb = c.xmailbox(tx, name, "")
1959 q := bstore.QueryTx[store.Message](tx)
1960 q.FilterNonzero(store.Message{MailboxID: mb.ID})
1961 q.FilterEqual("Expunged", false)
1963 c.uids = []store.UID{}
1965 err := q.ForEach(func(m store.Message) error {
1966 c.uids = append(c.uids, m.UID)
1967 if firstUnseen == 0 && !m.Seen {
1976 xcheckf(err, "fetching uids")
1978 // Condstore extension, find the highest modseq.
1979 if c.enabled[capCondstore] {
1980 highestModSeq = c.xhighestModSeq(tx, mb.ID)
1982 // For QRESYNC, we need to know the highest modset of deleted expunged records to
1983 // maintain synchronization.
1984 if c.enabled[capQresync] {
1985 highDeletedModSeq, err = c.account.HighestDeletedModSeq(tx)
1986 xcheckf(err, "getting highest deleted modseq")
1990 c.applyChanges(c.comm.Get(), true)
1993 if len(mb.Keywords) > 0 {
1994 flags = " " + strings.Join(mb.Keywords, " ")
1996 c.bwritelinef(`* FLAGS (\Seen \Answered \Flagged \Deleted \Draft $Forwarded $Junk $NotJunk $Phishing $MDNSent%s)`, flags)
1997 c.bwritelinef(`* OK [PERMANENTFLAGS (\Seen \Answered \Flagged \Deleted \Draft $Forwarded $Junk $NotJunk $Phishing $MDNSent \*)] x`)
1998 if !c.enabled[capIMAP4rev2] {
1999 c.bwritelinef(`* 0 RECENT`)
2001 c.bwritelinef(`* %d EXISTS`, len(c.uids))
2002 if !c.enabled[capIMAP4rev2] && firstUnseen > 0 {
2004 c.bwritelinef(`* OK [UNSEEN %d] x`, firstUnseen)
2006 c.bwritelinef(`* OK [UIDVALIDITY %d] x`, mb.UIDValidity)
2007 c.bwritelinef(`* OK [UIDNEXT %d] x`, mb.UIDNext)
2008 c.bwritelinef(`* LIST () "/" %s`, astring(mb.Name).pack(c))
2009 if c.enabled[capCondstore] {
2012 c.bwritelinef(`* OK [HIGHESTMODSEQ %d] x`, highestModSeq.Client())
2016 if qruidvalidity == mb.UIDValidity {
2017 // We send the vanished UIDs at the end, so we can easily combine the modseq
2018 // changes and vanished UIDs that result from that, with the vanished UIDs from the
2019 // case where we don't store enough history.
2020 vanishedUIDs := map[store.UID]struct{}{}
2022 var preVanished store.UID
2023 var oldClientUID store.UID
2024 // If samples of known msgseq and uid pairs are given (they must be in order), we
2025 // use them to determine the earliest UID for which we send VANISHED responses.
2027 if qrknownSeqSet != nil {
2028 if !qrknownSeqSet.isBasicIncreasing() {
2029 xuserErrorf("QRESYNC known message sequence set must be numeric and strictly increasing")
2031 if !qrknownUIDSet.isBasicIncreasing() {
2032 xuserErrorf("QRESYNC known uid set must be numeric and strictly increasing")
2034 seqiter := qrknownSeqSet.newIter()
2035 uiditer := qrknownUIDSet.newIter()
2037 msgseq, ok0 := seqiter.Next()
2038 uid, ok1 := uiditer.Next()
2041 } else if !ok0 || !ok1 {
2042 xsyntaxErrorf("invalid combination of known sequence set and uid set, must be of equal length")
2044 i := int(msgseq - 1)
2045 if i < 0 || i >= len(c.uids) || c.uids[i] != store.UID(uid) {
2046 if uidSearch(c.uids, store.UID(uid)) <= 0 {
2047 // We will check this old client UID for consistency below.
2048 oldClientUID = store.UID(uid)
2052 preVanished = store.UID(uid + 1)
2056 // We gather vanished UIDs and report them at the end. This seems OK because we
2057 // already sent HIGHESTMODSEQ, and a client should know not to commit that value
2058 // until after it has seen the tagged OK of this command. The RFC has a remark
2059 // about ordering of some untagged responses, it's not immediately clear what it
2060 // means, but given the examples appears to allude to servers that decide to not
2061 // send expunge/vanished before the tagged OK.
2064 // We are reading without account lock. Similar to when we process FETCH/SEARCH
2065 // requests. We don't have to reverify existence of the mailbox, so we don't
2066 // rlock, even briefly.
2067 c.xdbread(func(tx *bstore.Tx) {
2068 if oldClientUID > 0 {
2069 // The client sent a UID that is now removed. This is typically fine. But we check
2070 // that it is consistent with the modseq the client sent. If the UID already didn't
2071 // exist at that modseq, the client may be missing some information.
2072 q := bstore.QueryTx[store.Message](tx)
2073 q.FilterNonzero(store.Message{MailboxID: mb.ID, UID: oldClientUID})
2076 // If client claims to be up to date up to and including qrmodseq, and the message
2077 // was deleted at or before that time, we send changes from just before that
2078 // modseq, and we send vanished for all UIDs.
2079 if m.Expunged && qrmodseq >= m.ModSeq.Client() {
2080 qrmodseq = m.ModSeq.Client() - 1
2083 c.bwritelinef("* OK [ALERT] Synchronization inconsistency in client detected. Client tried to sync with a UID that was removed at or after the MODSEQ it sent in the request. Sending all historic message removals for selected mailbox. Full synchronization recommended.")
2085 } else if err != bstore.ErrAbsent {
2086 xcheckf(err, "checking old client uid")
2090 q := bstore.QueryTx[store.Message](tx)
2091 q.FilterNonzero(store.Message{MailboxID: mb.ID})
2092 // Note: we don't filter by Expunged.
2093 q.FilterGreater("ModSeq", store.ModSeqFromClient(qrmodseq))
2094 q.FilterLessEqual("ModSeq", highestModSeq)
2096 err := q.ForEach(func(m store.Message) error {
2097 if m.Expunged && m.UID < preVanished {
2101 if qrknownUIDs != nil && !qrknownUIDs.contains(uint32(m.UID)) {
2105 vanishedUIDs[m.UID] = struct{}{}
2108 msgseq := c.sequence(m.UID)
2110 c.bwritelinef("* %d FETCH (UID %d FLAGS %s MODSEQ (%d))", msgseq, m.UID, flaglist(m.Flags, m.Keywords).pack(c), m.ModSeq.Client())
2114 xcheckf(err, "listing changed messages")
2117 // Add UIDs from client's known UID set to vanished list if we don't have enough history.
2118 if qrmodseq < highDeletedModSeq.Client() {
2119 // If no known uid set was in the request, we substitute 1:max or the empty set.
2121 if qrknownUIDs == nil {
2122 if len(c.uids) > 0 {
2123 qrknownUIDs = &numSet{ranges: []numRange{{first: setNumber{number: 1}, last: &setNumber{number: uint32(c.uids[len(c.uids)-1])}}}}
2125 qrknownUIDs = &numSet{}
2129 iter := qrknownUIDs.newIter()
2131 v, ok := iter.Next()
2135 if c.sequence(store.UID(v)) <= 0 {
2136 vanishedUIDs[store.UID(v)] = struct{}{}
2141 // Now that we have all vanished UIDs, send them over compactly.
2142 if len(vanishedUIDs) > 0 {
2143 l := maps.Keys(vanishedUIDs)
2144 sort.Slice(l, func(i, j int) bool {
2148 for _, s := range compactUIDSet(l).Strings(4*1024 - 32) {
2149 c.bwritelinef("* VANISHED (EARLIER) %s", s)
2155 c.bwriteresultf("%s OK [READ-WRITE] x", tag)
2158 c.bwriteresultf("%s OK [READ-ONLY] x", tag)
2162 c.state = stateSelected
2163 c.searchResult = nil
2167// Create makes a new mailbox, and its parents too if absent.
2169// State: Authenticated and selected.
2170func (c *conn) cmdCreate(tag, cmd string, p *parser) {
2176 name := p.xmailbox()
2182 name = xcheckmailboxname(name, false)
2184 var changes []store.Change
2185 var created []string // Created mailbox names.
2187 c.account.WithWLock(func() {
2188 c.xdbwrite(func(tx *bstore.Tx) {
2191 changes, created, exists, err = c.account.MailboxCreate(tx, name)
2194 xuserErrorf("mailbox already exists")
2196 xcheckf(err, "creating mailbox")
2199 c.broadcast(changes)
2202 for _, n := range created {
2204 if n == name && name != origName && !(name == "Inbox" || strings.HasPrefix(name, "Inbox/")) {
2205 more = fmt.Sprintf(` ("OLDNAME" (%s))`, string0(origName).pack(c))
2207 c.bwritelinef(`* LIST (\Subscribed) "/" %s%s`, astring(n).pack(c), more)
2212// Delete removes a mailbox and all its messages.
2213// Inbox cannot be removed.
2215// State: Authenticated and selected.
2216func (c *conn) cmdDelete(tag, cmd string, p *parser) {
2222 name := p.xmailbox()
2225 name = xcheckmailboxname(name, false)
2227 // Messages to remove after having broadcasted the removal of messages.
2228 var removeMessageIDs []int64
2230 c.account.WithWLock(func() {
2231 var mb store.Mailbox
2232 var changes []store.Change
2234 c.xdbwrite(func(tx *bstore.Tx) {
2235 mb = c.xmailbox(tx, name, "NONEXISTENT")
2237 var hasChildren bool
2239 changes, removeMessageIDs, hasChildren, err = c.account.MailboxDelete(context.TODO(), c.log, tx, mb)
2241 xusercodeErrorf("HASCHILDREN", "mailbox has a child, only leaf mailboxes can be deleted")
2243 xcheckf(err, "deleting mailbox")
2246 c.broadcast(changes)
2249 for _, mID := range removeMessageIDs {
2250 p := c.account.MessagePath(mID)
2252 c.log.Check(err, "removing message file for mailbox delete", mlog.Field("path", p))
2258// Rename changes the name of a mailbox.
2259// Renaming INBOX is special, it moves the inbox messages to a new mailbox, leaving inbox empty.
2260// Renaming a mailbox with submailboxes also renames all submailboxes.
2261// Subscriptions stay with the old name, though newly created missing parent
2262// mailboxes for the destination name are automatically subscribed.
2264// State: Authenticated and selected.
2265func (c *conn) cmdRename(tag, cmd string, p *parser) {
2276 src = xcheckmailboxname(src, true)
2277 dst = xcheckmailboxname(dst, false)
2279 c.account.WithWLock(func() {
2280 var changes []store.Change
2282 c.xdbwrite(func(tx *bstore.Tx) {
2283 srcMB := c.xmailbox(tx, src, "NONEXISTENT")
2285 // Inbox is very special. Unlike other mailboxes, its children are not moved. And
2286 // unlike a regular move, its messages are moved to a newly created mailbox. We do
2287 // indeed create a new destination mailbox and actually move the messages.
2290 exists, err := c.account.MailboxExists(tx, dst)
2291 xcheckf(err, "checking if destination mailbox exists")
2293 xusercodeErrorf("ALREADYEXISTS", "destination mailbox %q already exists", dst)
2296 xuserErrorf("cannot move inbox to itself")
2299 uidval, err := c.account.NextUIDValidity(tx)
2300 xcheckf(err, "next uid validity")
2302 dstMB := store.Mailbox{
2304 UIDValidity: uidval,
2306 Keywords: srcMB.Keywords,
2309 err = tx.Insert(&dstMB)
2310 xcheckf(err, "create new destination mailbox")
2312 modseq, err := c.account.NextModSeq(tx)
2313 xcheckf(err, "assigning next modseq")
2315 changes = make([]store.Change, 2) // Placeholders filled in below.
2317 // Move existing messages, with their ID's and on-disk files intact, to the new
2318 // mailbox. We keep the expunged messages, the destination mailbox doesn't care
2320 var oldUIDs []store.UID
2321 q := bstore.QueryTx[store.Message](tx)
2322 q.FilterNonzero(store.Message{MailboxID: srcMB.ID})
2323 q.FilterEqual("Expunged", false)
2325 err = q.ForEach(func(m store.Message) error {
2330 oldUIDs = append(oldUIDs, om.UID)
2332 mc := m.MailboxCounts()
2336 m.MailboxID = dstMB.ID
2337 m.UID = dstMB.UIDNext
2339 m.CreateSeq = modseq
2341 if err := tx.Update(&m); err != nil {
2342 return fmt.Errorf("updating message to move to new mailbox: %w", err)
2345 changes = append(changes, m.ChangeAddUID())
2347 if err := tx.Insert(&om); err != nil {
2348 return fmt.Errorf("adding empty expunge message record to inbox: %w", err)
2352 xcheckf(err, "moving messages from inbox to destination mailbox")
2354 err = tx.Update(&dstMB)
2355 xcheckf(err, "updating uidnext and counts in destination mailbox")
2357 err = tx.Update(&srcMB)
2358 xcheckf(err, "updating counts for inbox")
2360 var dstFlags []string
2361 if tx.Get(&store.Subscription{Name: dstMB.Name}) == nil {
2362 dstFlags = []string{`\Subscribed`}
2364 changes[0] = store.ChangeRemoveUIDs{MailboxID: srcMB.ID, UIDs: oldUIDs, ModSeq: modseq}
2365 changes[1] = store.ChangeAddMailbox{Mailbox: dstMB, Flags: dstFlags}
2366 // changes[2:...] are ChangeAddUIDs
2367 changes = append(changes, srcMB.ChangeCounts(), dstMB.ChangeCounts())
2371 var notExists, alreadyExists bool
2373 changes, _, notExists, alreadyExists, err = c.account.MailboxRename(tx, srcMB, dst)
2376 xusercodeErrorf("NONEXISTENT", "%s", err)
2377 } else if alreadyExists {
2378 xusercodeErrorf("ALREADYEXISTS", "%s", err)
2380 xcheckf(err, "renaming mailbox")
2382 c.broadcast(changes)
2388// Subscribe marks a mailbox path as subscribed. The mailbox does not have to
2389// exist. Subscribed may mean an email client will show the mailbox in its UI
2390// and/or periodically fetch new messages for the mailbox.
2392// State: Authenticated and selected.
2393func (c *conn) cmdSubscribe(tag, cmd string, p *parser) {
2399 name := p.xmailbox()
2402 name = xcheckmailboxname(name, true)
2404 c.account.WithWLock(func() {
2405 var changes []store.Change
2407 c.xdbwrite(func(tx *bstore.Tx) {
2409 changes, err = c.account.SubscriptionEnsure(tx, name)
2410 xcheckf(err, "ensuring subscription")
2413 c.broadcast(changes)
2419// Unsubscribe marks a mailbox as not subscribed. The mailbox doesn't have to exist.
2421// State: Authenticated and selected.
2422func (c *conn) cmdUnsubscribe(tag, cmd string, p *parser) {
2428 name := p.xmailbox()
2431 name = xcheckmailboxname(name, true)
2433 c.account.WithWLock(func() {
2434 c.xdbwrite(func(tx *bstore.Tx) {
2436 err := tx.Delete(&store.Subscription{Name: name})
2437 if err == bstore.ErrAbsent {
2438 exists, err := c.account.MailboxExists(tx, name)
2439 xcheckf(err, "checking if mailbox exists")
2441 xuserErrorf("mailbox does not exist")
2445 xcheckf(err, "removing subscription")
2448 // todo: can we send untagged message about a mailbox no longer being subscribed?
2454// LSUB command for listing subscribed mailboxes.
2455// Removed in IMAP4rev2, only in IMAP4rev1.
2457// State: Authenticated and selected.
2458func (c *conn) cmdLsub(tag, cmd string, p *parser) {
2466 pattern := p.xlistMailbox()
2469 re := xmailboxPatternMatcher(ref, []string{pattern})
2472 c.xdbread(func(tx *bstore.Tx) {
2473 q := bstore.QueryTx[store.Subscription](tx)
2475 subscriptions, err := q.List()
2476 xcheckf(err, "querying subscriptions")
2478 have := map[string]bool{}
2479 subscribedKids := map[string]bool{}
2480 ispercent := strings.HasSuffix(pattern, "%")
2481 for _, sub := range subscriptions {
2484 for p := filepath.Dir(name); p != "."; p = filepath.Dir(p) {
2485 subscribedKids[p] = true
2488 if !re.MatchString(name) {
2492 line := fmt.Sprintf(`* LSUB () "/" %s`, astring(name).pack(c))
2493 lines = append(lines, line)
2501 qmb := bstore.QueryTx[store.Mailbox](tx)
2503 err = qmb.ForEach(func(mb store.Mailbox) error {
2504 if have[mb.Name] || !subscribedKids[mb.Name] || !re.MatchString(mb.Name) {
2507 line := fmt.Sprintf(`* LSUB (\NoSelect) "/" %s`, astring(mb.Name).pack(c))
2508 lines = append(lines, line)
2511 xcheckf(err, "querying mailboxes")
2515 for _, line := range lines {
2516 c.bwritelinef("%s", line)
2521// The namespace command returns the mailbox path separator. We only implement
2522// the personal mailbox hierarchy, no shared/other.
2524// In IMAP4rev2, it was an extension before.
2526// State: Authenticated and selected.
2527func (c *conn) cmdNamespace(tag, cmd string, p *parser) {
2534 c.bwritelinef(`* NAMESPACE (("" "/")) NIL NIL`)
2538// The status command returns information about a mailbox, such as the number of
2539// messages, "uid validity", etc. Nowadays, the extended LIST command can return
2540// the same information about many mailboxes for one command.
2542// State: Authenticated and selected.
2543func (c *conn) cmdStatus(tag, cmd string, p *parser) {
2549 name := p.xmailbox()
2552 attrs := []string{p.xstatusAtt()}
2555 attrs = append(attrs, p.xstatusAtt())
2559 name = xcheckmailboxname(name, true)
2561 var mb store.Mailbox
2563 var responseLine string
2564 c.account.WithRLock(func() {
2565 c.xdbread(func(tx *bstore.Tx) {
2566 mb = c.xmailbox(tx, name, "")
2567 responseLine = c.xstatusLine(tx, mb, attrs)
2571 c.bwritelinef("%s", responseLine)
2576func (c *conn) xstatusLine(tx *bstore.Tx, mb store.Mailbox, attrs []string) string {
2577 status := []string{}
2578 for _, a := range attrs {
2579 A := strings.ToUpper(a)
2582 status = append(status, A, fmt.Sprintf("%d", mb.Total+mb.Deleted))
2584 status = append(status, A, fmt.Sprintf("%d", mb.UIDNext))
2586 status = append(status, A, fmt.Sprintf("%d", mb.UIDValidity))
2588 status = append(status, A, fmt.Sprintf("%d", mb.Unseen))
2590 status = append(status, A, fmt.Sprintf("%d", mb.Deleted))
2592 status = append(status, A, fmt.Sprintf("%d", mb.Size))
2594 status = append(status, A, "0")
2597 status = append(status, A, "NIL")
2598 case "HIGHESTMODSEQ":
2600 status = append(status, A, fmt.Sprintf("%d", c.xhighestModSeq(tx, mb.ID).Client()))
2602 xsyntaxErrorf("unknown attribute %q", a)
2605 return fmt.Sprintf("* STATUS %s (%s)", astring(mb.Name).pack(c), strings.Join(status, " "))
2608func flaglist(fl store.Flags, keywords []string) listspace {
2610 flag := func(v bool, s string) {
2612 l = append(l, bare(s))
2615 flag(fl.Seen, `\Seen`)
2616 flag(fl.Answered, `\Answered`)
2617 flag(fl.Flagged, `\Flagged`)
2618 flag(fl.Deleted, `\Deleted`)
2619 flag(fl.Draft, `\Draft`)
2620 flag(fl.Forwarded, `$Forwarded`)
2621 flag(fl.Junk, `$Junk`)
2622 flag(fl.Notjunk, `$NotJunk`)
2623 flag(fl.Phishing, `$Phishing`)
2624 flag(fl.MDNSent, `$MDNSent`)
2625 for _, k := range keywords {
2626 l = append(l, bare(k))
2631// Append adds a message to a mailbox.
2633// State: Authenticated and selected.
2634func (c *conn) cmdAppend(tag, cmd string, p *parser) {
2640 name := p.xmailbox()
2642 var storeFlags store.Flags
2643 var keywords []string
2644 if p.hasPrefix("(") {
2645 // Error must be a syntax error, to properly abort the connection due to literal.
2647 storeFlags, keywords, err = store.ParseFlagsKeywords(p.xflagList())
2649 xsyntaxErrorf("parsing flags: %v", err)
2654 if p.hasPrefix(`"`) {
2660 // todo: only with utf8 should we we accept message headers with utf-8. we currently always accept them.
2661 // todo: this is only relevant if we also support the CATENATE extension?
2663 utf8 := p.take("UTF8 (")
2664 size, sync := p.xliteralSize(0, utf8)
2666 name = xcheckmailboxname(name, true)
2667 c.xdbread(func(tx *bstore.Tx) {
2668 c.xmailbox(tx, name, "TRYCREATE")
2674 // Read the message into a temporary file.
2675 msgFile, err := store.CreateMessageTemp("imap-append")
2676 xcheckf(err, "creating temp file for message")
2679 err := os.Remove(msgFile.Name())
2680 c.xsanity(err, "removing APPEND temporary file")
2681 err = msgFile.Close()
2682 c.xsanity(err, "closing APPEND temporary file")
2685 defer c.xtrace(mlog.LevelTracedata)()
2686 mw := message.NewWriter(msgFile)
2687 msize, err := io.Copy(mw, io.LimitReader(c.br, size))
2688 c.xtrace(mlog.LevelTrace) // Restore.
2690 // Cannot use xcheckf due to %w handling of errIO.
2691 panic(fmt.Errorf("reading literal message: %s (%w)", err, errIO))
2694 xserverErrorf("read %d bytes for message, expected %d (%w)", msize, size, errIO)
2698 line := c.readline(false)
2699 np := newParser(line, c)
2703 line := c.readline(false)
2704 np := newParser(line, c)
2709 name = xcheckmailboxname(name, true)
2712 var mb store.Mailbox
2714 var pendingChanges []store.Change
2716 c.account.WithWLock(func() {
2717 var changes []store.Change
2718 c.xdbwrite(func(tx *bstore.Tx) {
2719 mb = c.xmailbox(tx, name, "TRYCREATE")
2721 // Ensure keywords are stored in mailbox.
2722 var mbKwChanged bool
2723 mb.Keywords, mbKwChanged = store.MergeKeywords(mb.Keywords, keywords)
2725 changes = append(changes, mb.ChangeKeywords())
2730 MailboxOrigID: mb.ID,
2737 mb.Add(m.MailboxCounts())
2739 // Update mailbox before delivering, which updates uidnext which we mustn't overwrite.
2740 err = tx.Update(&mb)
2741 xcheckf(err, "updating mailbox counts")
2743 err := c.account.DeliverMessage(c.log, tx, &m, msgFile, true, true, false, false)
2744 xcheckf(err, "delivering message")
2747 // Fetch pending changes, possibly with new UIDs, so we can apply them before adding our own new UID.
2749 pendingChanges = c.comm.Get()
2752 // Broadcast the change to other connections.
2753 changes = append(changes, m.ChangeAddUID(), mb.ChangeCounts())
2754 c.broadcast(changes)
2757 err = msgFile.Close()
2758 c.log.Check(err, "closing appended file")
2761 if c.mailboxID == mb.ID {
2762 c.applyChanges(pendingChanges, false)
2764 // todo spec: with condstore/qresync, is there a mechanism to the client know the modseq for the appended uid? in theory an untagged fetch with the modseq after the OK APPENDUID could make sense, but this probably isn't allowed.
2765 c.bwritelinef("* %d EXISTS", len(c.uids))
2768 c.writeresultf("%s OK [APPENDUID %d %d] appended", tag, mb.UIDValidity, m.UID)
2771// Idle makes a client wait until the server sends untagged updates, e.g. about
2772// message delivery or mailbox create/rename/delete/subscription, etc. It allows a
2773// client to get updates in real-time, not needing the use for NOOP.
2775// State: Authenticated and selected.
2776func (c *conn) cmdIdle(tag, cmd string, p *parser) {
2783 c.writelinef("+ waiting")
2789 case le := <-c.lineChan():
2791 xcheckf(le.err, "get line")
2794 case <-c.comm.Pending:
2795 c.applyChanges(c.comm.Get(), false)
2797 case <-mox.Shutdown.Done():
2799 c.writelinef("* BYE shutting down")
2804 // Reset the write deadline. In case of little activity, with a command timeout of
2805 // 30 minutes, we have likely passed it.
2806 err := c.conn.SetWriteDeadline(time.Now().Add(5 * time.Minute))
2807 c.log.Check(err, "setting write deadline")
2809 if strings.ToUpper(line) != "DONE" {
2810 // We just close the connection because our protocols are out of sync.
2811 panic(fmt.Errorf("%w: in IDLE, expected DONE", errIO))
2817// Check is an old deprecated command that is supposed to execute some mailbox consistency checks.
2820func (c *conn) cmdCheck(tag, cmd string, p *parser) {
2826 c.account.WithRLock(func() {
2827 c.xdbread(func(tx *bstore.Tx) {
2828 c.xmailboxID(tx, c.mailboxID) // Validate.
2835// Close undoes select/examine, closing the currently opened mailbox and deleting
2836// messages that were marked for deletion with the \Deleted flag.
2839func (c *conn) cmdClose(tag, cmd string, p *parser) {
2851 remove, _ := c.xexpunge(nil, true)
2854 for _, m := range remove {
2855 p := c.account.MessagePath(m.ID)
2857 c.xsanity(err, "removing message file for expunge for close")
2865// expunge messages marked for deletion in currently selected/active mailbox.
2866// if uidSet is not nil, only messages matching the set are deleted.
2868// messages that have been marked expunged from the database are returned, but the
2869// corresponding files still have to be removed.
2871// the highest modseq in the mailbox is returned, typically associated with the
2872// removal of the messages, but if no messages were expunged the current latest max
2873// modseq for the mailbox is returned.
2874func (c *conn) xexpunge(uidSet *numSet, missingMailboxOK bool) (remove []store.Message, highestModSeq store.ModSeq) {
2875 var modseq store.ModSeq
2877 c.account.WithWLock(func() {
2878 var mb store.Mailbox
2880 c.xdbwrite(func(tx *bstore.Tx) {
2881 mb = store.Mailbox{ID: c.mailboxID}
2883 if err == bstore.ErrAbsent {
2884 if missingMailboxOK {
2887 xuserErrorf("%w", store.ErrUnknownMailbox)
2890 qm := bstore.QueryTx[store.Message](tx)
2891 qm.FilterNonzero(store.Message{MailboxID: c.mailboxID})
2892 qm.FilterEqual("Deleted", true)
2893 qm.FilterEqual("Expunged", false)
2894 qm.FilterFn(func(m store.Message) bool {
2895 // Only remove if this session knows about the message and if present in optional uidSet.
2896 return uidSearch(c.uids, m.UID) > 0 && (uidSet == nil || uidSet.containsUID(m.UID, c.uids, c.searchResult))
2899 remove, err = qm.List()
2900 xcheckf(err, "listing messages to delete")
2902 if len(remove) == 0 {
2903 highestModSeq = c.xhighestModSeq(tx, c.mailboxID)
2907 // Assign new modseq.
2908 modseq, err = c.account.NextModSeq(tx)
2909 xcheckf(err, "assigning next modseq")
2910 highestModSeq = modseq
2912 removeIDs := make([]int64, len(remove))
2913 anyIDs := make([]any, len(remove))
2914 for i, m := range remove {
2917 mb.Sub(m.MailboxCounts())
2918 // Update "remove", because RetrainMessage below will save the message.
2919 remove[i].Expunged = true
2920 remove[i].ModSeq = modseq
2922 qmr := bstore.QueryTx[store.Recipient](tx)
2923 qmr.FilterEqual("MessageID", anyIDs...)
2924 _, err = qmr.Delete()
2925 xcheckf(err, "removing message recipients")
2927 qm = bstore.QueryTx[store.Message](tx)
2928 qm.FilterIDs(removeIDs)
2929 n, err := qm.UpdateNonzero(store.Message{Expunged: true, ModSeq: modseq})
2930 if err == nil && n != len(removeIDs) {
2931 err = fmt.Errorf("only %d messages set to expunged, expected %d", n, len(removeIDs))
2933 xcheckf(err, "marking messages marked for deleted as expunged")
2935 err = tx.Update(&mb)
2936 xcheckf(err, "updating mailbox counts")
2938 // Mark expunged messages as not needing training, then retrain them, so if they
2939 // were trained, they get untrained.
2940 for i := range remove {
2941 remove[i].Junk = false
2942 remove[i].Notjunk = false
2944 err = c.account.RetrainMessages(context.TODO(), c.log, tx, remove, true)
2945 xcheckf(err, "untraining expunged messages")
2948 // Broadcast changes to other connections. We may not have actually removed any
2949 // messages, so take care not to send an empty update.
2950 if len(remove) > 0 {
2951 ouids := make([]store.UID, len(remove))
2952 for i, m := range remove {
2955 changes := []store.Change{
2956 store.ChangeRemoveUIDs{MailboxID: c.mailboxID, UIDs: ouids, ModSeq: modseq},
2959 c.broadcast(changes)
2962 return remove, highestModSeq
2965// Unselect is similar to close in that it closes the currently active mailbox, but
2966// it does not remove messages marked for deletion.
2969func (c *conn) cmdUnselect(tag, cmd string, p *parser) {
2979// Expunge deletes messages marked with \Deleted in the currently selected mailbox.
2980// Clients are wiser to use UID EXPUNGE because it allows a UID sequence set to
2981// explicitly opt in to removing specific messages.
2984func (c *conn) cmdExpunge(tag, cmd string, p *parser) {
2991 xuserErrorf("mailbox open in read-only mode")
2994 c.cmdxExpunge(tag, cmd, nil)
2997// UID expunge deletes messages marked with \Deleted in the currently selected
2998// mailbox if they match a UID sequence set.
3001func (c *conn) cmdUIDExpunge(tag, cmd string, p *parser) {
3006 uidSet := p.xnumSet()
3010 xuserErrorf("mailbox open in read-only mode")
3013 c.cmdxExpunge(tag, cmd, &uidSet)
3016// Permanently delete messages for the currently selected/active mailbox. If uidset
3017// is not nil, only those UIDs are removed.
3019func (c *conn) cmdxExpunge(tag, cmd string, uidSet *numSet) {
3022 remove, highestModSeq := c.xexpunge(uidSet, false)
3025 for _, m := range remove {
3026 p := c.account.MessagePath(m.ID)
3028 c.xsanity(err, "removing message file for expunge")
3033 var vanishedUIDs numSet
3034 qresync := c.enabled[capQresync]
3035 for _, m := range remove {
3036 seq := c.xsequence(m.UID)
3037 c.sequenceRemove(seq, m.UID)
3039 vanishedUIDs.append(uint32(m.UID))
3041 c.bwritelinef("* %d EXPUNGE", seq)
3044 if !vanishedUIDs.empty() {
3046 for _, s := range vanishedUIDs.Strings(4*1024 - 32) {
3047 c.bwritelinef("* VANISHED %s", s)
3051 if c.enabled[capCondstore] {
3052 c.writeresultf("%s OK [HIGHESTMODSEQ %d] expunged", tag, highestModSeq.Client())
3059func (c *conn) cmdSearch(tag, cmd string, p *parser) {
3060 c.cmdxSearch(false, tag, cmd, p)
3064func (c *conn) cmdUIDSearch(tag, cmd string, p *parser) {
3065 c.cmdxSearch(true, tag, cmd, p)
3069func (c *conn) cmdFetch(tag, cmd string, p *parser) {
3070 c.cmdxFetch(false, tag, cmd, p)
3074func (c *conn) cmdUIDFetch(tag, cmd string, p *parser) {
3075 c.cmdxFetch(true, tag, cmd, p)
3079func (c *conn) cmdStore(tag, cmd string, p *parser) {
3080 c.cmdxStore(false, tag, cmd, p)
3084func (c *conn) cmdUIDStore(tag, cmd string, p *parser) {
3085 c.cmdxStore(true, tag, cmd, p)
3089func (c *conn) cmdCopy(tag, cmd string, p *parser) {
3090 c.cmdxCopy(false, tag, cmd, p)
3094func (c *conn) cmdUIDCopy(tag, cmd string, p *parser) {
3095 c.cmdxCopy(true, tag, cmd, p)
3099func (c *conn) cmdMove(tag, cmd string, p *parser) {
3100 c.cmdxMove(false, tag, cmd, p)
3104func (c *conn) cmdUIDMove(tag, cmd string, p *parser) {
3105 c.cmdxMove(true, tag, cmd, p)
3108func (c *conn) gatherCopyMoveUIDs(isUID bool, nums numSet) ([]store.UID, []any) {
3109 // Gather uids, then sort so we can return a consistently simple and hard to
3110 // misinterpret COPYUID/MOVEUID response. It seems safer to have UIDs in ascending
3111 // order, because requested uid set of 12:10 is equal to 10:12, so if we would just
3112 // echo whatever the client sends us without reordering, the client can reorder our
3113 // response and interpret it differently than we intended.
3115 uids := c.xnumSetUIDs(isUID, nums)
3116 sort.Slice(uids, func(i, j int) bool {
3117 return uids[i] < uids[j]
3119 uidargs := make([]any, len(uids))
3120 for i, uid := range uids {
3123 return uids, uidargs
3126// Copy copies messages from the currently selected/active mailbox to another named
3130func (c *conn) cmdxCopy(isUID bool, tag, cmd string, p *parser) {
3137 name := p.xmailbox()
3140 name = xcheckmailboxname(name, true)
3142 uids, uidargs := c.gatherCopyMoveUIDs(isUID, nums)
3144 // Files that were created during the copy. Remove them if the operation fails.
3145 var createdIDs []int64
3151 for _, id := range createdIDs {
3152 p := c.account.MessagePath(id)
3154 c.xsanity(err, "cleaning up created file")
3159 var mbDst store.Mailbox
3160 var origUIDs, newUIDs []store.UID
3161 var flags []store.Flags
3162 var keywords [][]string
3163 var modseq store.ModSeq // For messages in new mailbox, assigned when first message is copied.
3165 c.account.WithWLock(func() {
3166 var mbKwChanged bool
3168 c.xdbwrite(func(tx *bstore.Tx) {
3169 mbSrc := c.xmailboxID(tx, c.mailboxID) // Validate.
3170 mbDst = c.xmailbox(tx, name, "TRYCREATE")
3171 if mbDst.ID == mbSrc.ID {
3172 xuserErrorf("cannot copy to currently selected mailbox")
3175 if len(uidargs) == 0 {
3176 xuserErrorf("no matching messages to copy")
3180 modseq, err = c.account.NextModSeq(tx)
3181 xcheckf(err, "assigning next modseq")
3183 // Reserve the uids in the destination mailbox.
3184 uidFirst := mbDst.UIDNext
3185 mbDst.UIDNext += store.UID(len(uidargs))
3187 // Fetch messages from database.
3188 q := bstore.QueryTx[store.Message](tx)
3189 q.FilterNonzero(store.Message{MailboxID: c.mailboxID})
3190 q.FilterEqual("UID", uidargs...)
3191 q.FilterEqual("Expunged", false)
3192 xmsgs, err := q.List()
3193 xcheckf(err, "fetching messages")
3195 if len(xmsgs) != len(uidargs) {
3196 xserverErrorf("uid and message mismatch")
3199 msgs := map[store.UID]store.Message{}
3200 for _, m := range xmsgs {
3203 nmsgs := make([]store.Message, len(xmsgs))
3205 conf, _ := c.account.Conf()
3207 mbKeywords := map[string]struct{}{}
3209 // Insert new messages into database.
3210 var origMsgIDs, newMsgIDs []int64
3211 for i, uid := range uids {
3214 xuserErrorf("messages changed, could not fetch requested uid")
3217 origMsgIDs = append(origMsgIDs, origID)
3219 m.UID = uidFirst + store.UID(i)
3220 m.CreateSeq = modseq
3222 m.MailboxID = mbDst.ID
3223 if m.IsReject && m.MailboxDestinedID != 0 {
3224 // Incorrectly delivered to Rejects mailbox. Adjust MailboxOrigID so this message
3225 // is used for reputation calculation during future deliveries.
3226 m.MailboxOrigID = m.MailboxDestinedID
3230 m.JunkFlagsForMailbox(mbDst, conf)
3231 err := tx.Insert(&m)
3232 xcheckf(err, "inserting message")
3235 origUIDs = append(origUIDs, uid)
3236 newUIDs = append(newUIDs, m.UID)
3237 newMsgIDs = append(newMsgIDs, m.ID)
3238 flags = append(flags, m.Flags)
3239 keywords = append(keywords, m.Keywords)
3240 for _, kw := range m.Keywords {
3241 mbKeywords[kw] = struct{}{}
3244 qmr := bstore.QueryTx[store.Recipient](tx)
3245 qmr.FilterNonzero(store.Recipient{MessageID: origID})
3246 mrs, err := qmr.List()
3247 xcheckf(err, "listing message recipients")
3248 for _, mr := range mrs {
3251 err := tx.Insert(&mr)
3252 xcheckf(err, "inserting message recipient")
3255 mbDst.Add(m.MailboxCounts())
3258 mbDst.Keywords, mbKwChanged = store.MergeKeywords(mbDst.Keywords, maps.Keys(mbKeywords))
3260 err = tx.Update(&mbDst)
3261 xcheckf(err, "updating destination mailbox for uids, keywords and counts")
3263 // Copy message files to new message ID's.
3264 syncDirs := map[string]struct{}{}
3265 for i := range origMsgIDs {
3266 src := c.account.MessagePath(origMsgIDs[i])
3267 dst := c.account.MessagePath(newMsgIDs[i])
3268 dstdir := filepath.Dir(dst)
3269 if _, ok := syncDirs[dstdir]; !ok {
3270 os.MkdirAll(dstdir, 0770)
3271 syncDirs[dstdir] = struct{}{}
3273 err := moxio.LinkOrCopy(c.log, dst, src, nil, true)
3274 xcheckf(err, "link or copy file %q to %q", src, dst)
3275 createdIDs = append(createdIDs, newMsgIDs[i])
3278 for dir := range syncDirs {
3279 err := moxio.SyncDir(dir)
3280 xcheckf(err, "sync directory")
3283 err = c.account.RetrainMessages(context.TODO(), c.log, tx, nmsgs, false)
3284 xcheckf(err, "train copied messages")
3287 // Broadcast changes to other connections.
3288 if len(newUIDs) > 0 {
3289 changes := make([]store.Change, 0, len(newUIDs)+2)
3290 for i, uid := range newUIDs {
3291 changes = append(changes, store.ChangeAddUID{MailboxID: mbDst.ID, UID: uid, ModSeq: modseq, Flags: flags[i], Keywords: keywords[i]})
3293 changes = append(changes, mbDst.ChangeCounts())
3295 changes = append(changes, mbDst.ChangeKeywords())
3297 c.broadcast(changes)
3301 // All good, prevent defer above from cleaning up copied files.
3305 c.writeresultf("%s OK [COPYUID %d %s %s] copied", tag, mbDst.UIDValidity, compactUIDSet(origUIDs).String(), compactUIDSet(newUIDs).String())
3308// Move moves messages from the currently selected/active mailbox to a named mailbox.
3311func (c *conn) cmdxMove(isUID bool, tag, cmd string, p *parser) {
3318 name := p.xmailbox()
3321 name = xcheckmailboxname(name, true)
3324 xuserErrorf("mailbox open in read-only mode")
3327 uids, uidargs := c.gatherCopyMoveUIDs(isUID, nums)
3329 var mbSrc, mbDst store.Mailbox
3330 var changes []store.Change
3331 var newUIDs []store.UID
3332 var modseq store.ModSeq
3334 c.account.WithWLock(func() {
3335 c.xdbwrite(func(tx *bstore.Tx) {
3336 mbSrc = c.xmailboxID(tx, c.mailboxID) // Validate.
3337 mbDst = c.xmailbox(tx, name, "TRYCREATE")
3338 if mbDst.ID == c.mailboxID {
3339 xuserErrorf("cannot move to currently selected mailbox")
3342 if len(uidargs) == 0 {
3343 xuserErrorf("no matching messages to move")
3346 // Reserve the uids in the destination mailbox.
3347 uidFirst := mbDst.UIDNext
3349 mbDst.UIDNext += store.UID(len(uids))
3351 // Assign a new modseq, for the new records and for the expunged records.
3353 modseq, err = c.account.NextModSeq(tx)
3354 xcheckf(err, "assigning next modseq")
3356 // Update existing record with new UID and MailboxID in database for messages. We
3357 // add a new but expunged record again in the original/source mailbox, for qresync.
3358 // Keeping the original ID for the live message means we don't have to move the
3359 // on-disk message contents file.
3360 q := bstore.QueryTx[store.Message](tx)
3361 q.FilterNonzero(store.Message{MailboxID: c.mailboxID})
3362 q.FilterEqual("UID", uidargs...)
3363 q.FilterEqual("Expunged", false)
3365 msgs, err := q.List()
3366 xcheckf(err, "listing messages to move")
3368 if len(msgs) != len(uidargs) {
3369 xserverErrorf("uid and message mismatch")
3372 keywords := map[string]struct{}{}
3374 conf, _ := c.account.Conf()
3375 for i := range msgs {
3377 if m.UID != uids[i] {
3378 xserverErrorf("internal error: got uid %d, expected %d, for index %d", m.UID, uids[i], i)
3381 mbSrc.Sub(m.MailboxCounts())
3383 // Copy of message record that we'll insert when UID is freed up.
3386 om.ID = 0 // Assign new ID.
3389 m.MailboxID = mbDst.ID
3390 if m.IsReject && m.MailboxDestinedID != 0 {
3391 // Incorrectly delivered to Rejects mailbox. Adjust MailboxOrigID so this message
3392 // is used for reputation calculation during future deliveries.
3393 m.MailboxOrigID = m.MailboxDestinedID
3397 mbDst.Add(m.MailboxCounts())
3400 m.JunkFlagsForMailbox(mbDst, conf)
3403 xcheckf(err, "updating moved message in database")
3405 // Now that UID is unused, we can insert the old record again.
3406 err = tx.Insert(&om)
3407 xcheckf(err, "inserting record for expunge after moving message")
3409 for _, kw := range m.Keywords {
3410 keywords[kw] = struct{}{}
3414 // Ensure destination mailbox has keywords of the moved messages.
3415 var mbKwChanged bool
3416 mbDst.Keywords, mbKwChanged = store.MergeKeywords(mbDst.Keywords, maps.Keys(keywords))
3418 changes = append(changes, mbDst.ChangeKeywords())
3421 err = tx.Update(&mbSrc)
3422 xcheckf(err, "updating source mailbox counts")
3424 err = tx.Update(&mbDst)
3425 xcheckf(err, "updating destination mailbox for uids, keywords and counts")
3427 err = c.account.RetrainMessages(context.TODO(), c.log, tx, msgs, false)
3428 xcheckf(err, "retraining messages after move")
3430 // Prepare broadcast changes to other connections.
3431 changes = make([]store.Change, 0, 1+len(msgs)+2)
3432 changes = append(changes, store.ChangeRemoveUIDs{MailboxID: c.mailboxID, UIDs: uids, ModSeq: modseq})
3433 for _, m := range msgs {
3434 newUIDs = append(newUIDs, m.UID)
3435 changes = append(changes, m.ChangeAddUID())
3437 changes = append(changes, mbSrc.ChangeCounts(), mbDst.ChangeCounts())
3440 c.broadcast(changes)
3445 c.bwritelinef("* OK [COPYUID %d %s %s] moved", mbDst.UIDValidity, compactUIDSet(uids).String(), compactUIDSet(newUIDs).String())
3446 qresync := c.enabled[capQresync]
3447 var vanishedUIDs numSet
3448 for i := 0; i < len(uids); i++ {
3449 seq := c.xsequence(uids[i])
3450 c.sequenceRemove(seq, uids[i])
3452 vanishedUIDs.append(uint32(uids[i]))
3454 c.bwritelinef("* %d EXPUNGE", seq)
3457 if !vanishedUIDs.empty() {
3459 for _, s := range vanishedUIDs.Strings(4*1024 - 32) {
3460 c.bwritelinef("* VANISHED %s", s)
3464 if c.enabled[capQresync] {
3466 c.writeresultf("%s OK [HIGHESTMODSEQ %d] move", tag, modseq.Client())
3472// Store sets a full set of flags, or adds/removes specific flags.
3475func (c *conn) cmdxStore(isUID bool, tag, cmd string, p *parser) {
3482 var unchangedSince *int64
3485 p.xtake("UNCHANGEDSINCE")
3492 c.xensureCondstore(nil)
3494 var plus, minus bool
3497 } else if p.take("-") {
3501 silent := p.take(".SILENT")
3503 var flagstrs []string
3504 if p.hasPrefix("(") {
3505 flagstrs = p.xflagList()
3507 flagstrs = append(flagstrs, p.xflag())
3509 flagstrs = append(flagstrs, p.xflag())
3515 xuserErrorf("mailbox open in read-only mode")
3518 flags, keywords, err := store.ParseFlagsKeywords(flagstrs)
3520 xuserErrorf("parsing flags: %v", err)
3522 var mask store.Flags
3524 mask, flags = flags, store.FlagsAll
3526 mask, flags = flags, store.Flags{}
3528 mask = store.FlagsAll
3531 var mb, origmb store.Mailbox
3532 var updated []store.Message
3533 var changed []store.Message // ModSeq more recent than unchangedSince, will be in MODIFIED response code, and we will send untagged fetch responses so client is up to date.
3534 var modseq store.ModSeq // Assigned when needed.
3535 modified := map[int64]bool{}
3537 c.account.WithWLock(func() {
3538 var mbKwChanged bool
3539 var changes []store.Change
3541 c.xdbwrite(func(tx *bstore.Tx) {
3542 mb = c.xmailboxID(tx, c.mailboxID) // Validate.
3545 uidargs := c.xnumSetCondition(isUID, nums)
3547 if len(uidargs) == 0 {
3551 // Ensure keywords are in mailbox.
3553 mb.Keywords, mbKwChanged = store.MergeKeywords(mb.Keywords, keywords)
3555 err := tx.Update(&mb)
3556 xcheckf(err, "updating mailbox with keywords")
3560 q := bstore.QueryTx[store.Message](tx)
3561 q.FilterNonzero(store.Message{MailboxID: c.mailboxID})
3562 q.FilterEqual("UID", uidargs...)
3563 q.FilterEqual("Expunged", false)
3564 err := q.ForEach(func(m store.Message) error {
3565 // Client may specify a message multiple times, but we only process it once.
../rfc/7162:823
3570 mc := m.MailboxCounts()
3572 origFlags := m.Flags
3573 m.Flags = m.Flags.Set(mask, flags)
3574 oldKeywords := append([]string{}, m.Keywords...)
3576 m.Keywords, _ = store.RemoveKeywords(m.Keywords, keywords)
3578 m.Keywords, _ = store.MergeKeywords(m.Keywords, keywords)
3580 m.Keywords = keywords
3583 keywordsChanged := func() bool {
3584 sort.Strings(oldKeywords)
3585 n := append([]string{}, m.Keywords...)
3587 return !slices.Equal(oldKeywords, n)
3590 // If the message has a more recent modseq than the check requires, we won't modify
3591 // it and report in the final command response.
3594 // unchangedSince 0 always fails the check, we don't turn it into 1 like with our
3595 // internal modseqs. RFC implies that is not required for non-system flags, but we
3597 if unchangedSince != nil && m.ModSeq.Client() > *unchangedSince {
3598 changed = append(changed, m)
3603 // It requires that we keep track of the flags we think the client knows (but only
3604 // on this connection). We don't track that. It also isn't clear why this is
3605 // allowed because it is skipping the condstore conditional check, and the new
3606 // combination of flags could be unintended.
3609 if origFlags == m.Flags && !keywordsChanged() {
3610 // Note: since we didn't update the modseq, we are not adding m.ID to "modified",
3611 // it would skip the modseq check above. We still add m to list of updated, so we
3612 // send an untagged fetch response. But we don't broadcast it.
3613 updated = append(updated, m)
3618 mb.Add(m.MailboxCounts())
3620 // Assign new modseq for first actual change.
3623 modseq, err = c.account.NextModSeq(tx)
3624 xcheckf(err, "next modseq")
3627 modified[m.ID] = true
3628 updated = append(updated, m)
3630 changes = append(changes, m.ChangeFlags(origFlags))
3632 return tx.Update(&m)
3634 xcheckf(err, "storing flags in messages")
3636 if mb.MailboxCounts != origmb.MailboxCounts {
3637 err := tx.Update(&mb)
3638 xcheckf(err, "updating mailbox counts")
3640 changes = append(changes, mb.ChangeCounts())
3643 changes = append(changes, mb.ChangeKeywords())
3646 err = c.account.RetrainMessages(context.TODO(), c.log, tx, updated, false)
3647 xcheckf(err, "training messages")
3650 c.broadcast(changes)
3653 // In the RFC, the section about STORE/UID STORE says we must return MODSEQ when
3654 // UNCHANGEDSINCE was specified. It does not specify it in case UNCHANGEDSINCE
3655 // isn't specified. For that case it does say MODSEQ is needed in unsolicited
3656 // untagged fetch responses. Implying that solicited untagged fetch responses
3657 // should not include MODSEQ (why else mention unsolicited explicitly?). But, in
3658 // the introduction to CONDSTORE it does explicitly specify MODSEQ should be
3659 // included in untagged fetch responses at all times with CONDSTORE-enabled
3660 // connections. It would have been better if the command behaviour was specified in
3661 // the command section, not the introduction to the extension.
3664 if !silent || c.enabled[capCondstore] {
3665 for _, m := range updated {
3668 flags = fmt.Sprintf(" FLAGS %s", flaglist(m.Flags, m.Keywords).pack(c))
3670 var modseqStr string
3671 if c.enabled[capCondstore] {
3672 modseqStr = fmt.Sprintf(" MODSEQ (%d)", m.ModSeq.Client())
3675 c.bwritelinef("* %d FETCH (UID %d%s%s)", c.xsequence(m.UID), m.UID, flags, modseqStr)
3679 // We don't explicitly send flags for failed updated with silent set. The regular
3680 // notification will get the flags to the client.
3683 if len(changed) == 0 {
3688 // Write unsolicited untagged fetch responses for messages that didn't pass the
3691 var mnums []store.UID
3692 for _, m := range changed {
3693 c.bwritelinef("* %d FETCH (UID %d FLAGS %s MODSEQ (%d))", c.xsequence(m.UID), m.UID, flaglist(m.Flags, m.Keywords).pack(c), m.ModSeq.Client())
3695 mnums = append(mnums, m.UID)
3697 mnums = append(mnums, store.UID(c.xsequence(m.UID)))
3701 sort.Slice(mnums, func(i, j int) bool {
3702 return mnums[i] < mnums[j]
3704 set := compactUIDSet(mnums)
3706 c.writeresultf("%s OK [MODIFIED %s] conditional store did not modify all", tag, set.String())