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. Until that's enabled, we do use UTF-7 for mailbox names. See
19- We never execute multiple commands at the same time for a connection. We expect a client to open multiple connections instead.
../rfc/9051:1110
20- Do not write output on a connection with an account lock held. Writing can block, a slow client could block account operations.
21- When handling commands that modify the selected mailbox, always check that the mailbox is not opened readonly. And always revalidate the selected mailbox, another session may have deleted the mailbox.
22- After making changes to an account/mailbox/message, you must broadcast changes. You must do this with the account lock held. Otherwise, other later changes (e.g. message deliveries) may be made and broadcast before changes that were made earlier. Make sure to commit changes in the database first, because the commit may fail.
23- Mailbox hierarchies are slash separated, no leading slash. We keep the case, except INBOX is renamed to Inbox, also for submailboxes in INBOX. We don't allow existence of a child where its parent does not exist. We have no \NoInferiors or \NoSelect. Newly created mailboxes are automatically subscribed.
24- For CONDSTORE and QRESYNC support, we set "modseq" for each change/expunge. Once expunged, a modseq doesn't change anymore. We don't yet remove old expunged records. The records aren't too big. Next step may be to let an admin reclaim space manually.
28- todo: do not return binary data for a fetch body. at least not for imap4rev1. we should be encoding it as base64?
29- todo: 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?
30- 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.
31- todo future: more extensions: OBJECTID, MULTISEARCH, REPLACE, NOTIFY, CATENATE, MULTIAPPEND, SORT, THREAD, CREATE-SPECIAL-USE.
61 "golang.org/x/exp/maps"
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"
81 metricIMAPConnection = promauto.NewCounterVec(
82 prometheus.CounterOpts{
83 Name: "mox_imap_connection_total",
84 Help: "Incoming IMAP connections.",
87 "service", // imap, imaps
90 metricIMAPCommands = promauto.NewHistogramVec(
91 prometheus.HistogramOpts{
92 Name: "mox_imap_command_duration_seconds",
93 Help: "IMAP command duration and result codes in seconds.",
94 Buckets: []float64{0.001, 0.005, 0.01, 0.05, 0.100, 0.5, 1, 5, 10, 20},
98 "result", // ok, panic, ioerror, badsyntax, servererror, usererror, error
103var limiterConnectionrate, limiterConnections *ratelimit.Limiter
106 // Also called by tests, so they don't trigger the rate limiter.
112 limiterConnectionrate = &ratelimit.Limiter{
113 WindowLimits: []ratelimit.WindowLimit{
116 Limits: [...]int64{300, 900, 2700},
120 limiterConnections = &ratelimit.Limiter{
121 WindowLimits: []ratelimit.WindowLimit{
123 Window: time.Duration(math.MaxInt64), // All of time.
124 Limits: [...]int64{30, 90, 270},
130// Delay after bad/suspicious behaviour. Tests set these to zero.
131var badClientDelay = time.Second // Before reads and after 1-byte writes for probably spammers.
132var authFailDelay = time.Second // After authentication failure.
134// Capabilities (extensions) the server supports. Connections will add a few more, e.g. STARTTLS, LOGINDISABLED, AUTH=PLAIN.
159// We always announce support for SCRAM PLUS-variants, also on connections without
160// TLS. The client should not be selecting PLUS variants on non-TLS connections,
161// instead opting to do the bare SCRAM variant without indicating the server claims
162// to support the PLUS variant (skipping the server downgrade detection check).
163const serverCapabilities = "IMAP4rev2 IMAP4rev1 ENABLE LITERAL+ IDLE SASL-IR BINARY UNSELECT UIDPLUS ESEARCH SEARCHRES MOVE UTF8=ACCEPT LIST-EXTENDED SPECIAL-USE LIST-STATUS AUTH=SCRAM-SHA-256-PLUS AUTH=SCRAM-SHA-256 AUTH=SCRAM-SHA-1-PLUS AUTH=SCRAM-SHA-1 AUTH=CRAM-MD5 ID APPENDLIMIT=9223372036854775807 CONDSTORE QRESYNC STATUS=SIZE QUOTA QUOTA=RES-STORAGE"
169 tls bool // Whether TLS has been initialized.
170 br *bufio.Reader // From remote, with TLS unwrapped in case of TLS.
171 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.
172 lastLine string // For detecting if syntax error is fatal, i.e. if this ends with a literal. Without crlf.
173 bw *bufio.Writer // To remote, with TLS added in case of TLS.
174 tr *moxio.TraceReader // Kept to change trace level when reading/writing cmd/auth/data.
175 tw *moxio.TraceWriter
176 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.
177 lastlog time.Time // For printing time since previous log line.
178 tlsConfig *tls.Config // TLS config to use for handshake.
180 noRequireSTARTTLS bool
181 cmd string // Currently executing, for deciding to applyChanges and logging.
182 cmdMetric string // Currently executing, for metrics.
184 ncmds int // Number of commands processed. Used to abort connection when first incoming command is unknown/invalid.
186 enabled map[capability]bool // All upper-case.
188 // Set by SEARCH with SAVE. Can be used by commands accepting a sequence-set with
189 // value "$". When used, UIDs must be verified to still exist, because they may
190 // have been expunged. Cleared by a SELECT or EXAMINE.
191 // Nil means no searchResult is present. An empty list is a valid searchResult,
192 // just not matching any messages.
194 searchResult []store.UID
196 // Only when authenticated.
197 authFailed int // Number of failed auth attempts. For slowing down remote with many failures.
198 username string // Full username as used during login.
199 account *store.Account
200 comm *store.Comm // For sending/receiving changes on mailboxes in account, e.g. from messages incoming on smtp, or another imap client.
202 mailboxID int64 // Only for StateSelected.
203 readonly bool // If opened mailbox is readonly.
204 uids []store.UID // UIDs known in this session, sorted. todo future: store more space-efficiently, as ranges.
207// capability for use with ENABLED and CAPABILITY. We always keep this upper case,
208// e.g. IMAP4REV2. These values are treated case-insensitive, but it's easier for
209// comparison to just always have the same case.
210type capability string
213 capIMAP4rev2 capability = "IMAP4REV2"
214 capUTF8Accept capability = "UTF8=ACCEPT"
215 capCondstore capability = "CONDSTORE"
216 capQresync capability = "QRESYNC"
227 stateNotAuthenticated state = iota
232func stateCommands(cmds ...string) map[string]struct{} {
233 r := map[string]struct{}{}
234 for _, cmd := range cmds {
241 commandsStateAny = stateCommands("capability", "noop", "logout", "id")
242 commandsStateNotAuthenticated = stateCommands("starttls", "authenticate", "login")
243 commandsStateAuthenticated = stateCommands("enable", "select", "examine", "create", "delete", "rename", "subscribe", "unsubscribe", "list", "namespace", "status", "append", "idle", "lsub", "getquotaroot", "getquota")
244 commandsStateSelected = stateCommands("close", "unselect", "expunge", "search", "fetch", "store", "copy", "move", "uid expunge", "uid search", "uid fetch", "uid store", "uid copy", "uid move")
247var commands = map[string]func(c *conn, tag, cmd string, p *parser){
249 "capability": (*conn).cmdCapability,
250 "noop": (*conn).cmdNoop,
251 "logout": (*conn).cmdLogout,
255 "starttls": (*conn).cmdStarttls,
256 "authenticate": (*conn).cmdAuthenticate,
257 "login": (*conn).cmdLogin,
259 // Authenticated and selected.
260 "enable": (*conn).cmdEnable,
261 "select": (*conn).cmdSelect,
262 "examine": (*conn).cmdExamine,
263 "create": (*conn).cmdCreate,
264 "delete": (*conn).cmdDelete,
265 "rename": (*conn).cmdRename,
266 "subscribe": (*conn).cmdSubscribe,
267 "unsubscribe": (*conn).cmdUnsubscribe,
268 "list": (*conn).cmdList,
269 "lsub": (*conn).cmdLsub,
270 "namespace": (*conn).cmdNamespace,
271 "status": (*conn).cmdStatus,
272 "append": (*conn).cmdAppend,
273 "idle": (*conn).cmdIdle,
274 "getquotaroot": (*conn).cmdGetquotaroot,
275 "getquota": (*conn).cmdGetquota,
278 "check": (*conn).cmdCheck,
279 "close": (*conn).cmdClose,
280 "unselect": (*conn).cmdUnselect,
281 "expunge": (*conn).cmdExpunge,
282 "uid expunge": (*conn).cmdUIDExpunge,
283 "search": (*conn).cmdSearch,
284 "uid search": (*conn).cmdUIDSearch,
285 "fetch": (*conn).cmdFetch,
286 "uid fetch": (*conn).cmdUIDFetch,
287 "store": (*conn).cmdStore,
288 "uid store": (*conn).cmdUIDStore,
289 "copy": (*conn).cmdCopy,
290 "uid copy": (*conn).cmdUIDCopy,
291 "move": (*conn).cmdMove,
292 "uid move": (*conn).cmdUIDMove,
295var errIO = errors.New("io error") // For read/write errors and errors that should close the connection.
296var errProtocol = errors.New("protocol error") // For protocol errors for which a stack trace should be printed.
300// check err for sanity.
301// if not nil and checkSanity true (set during tests), then panic. if not nil during normal operation, just log.
302func (c *conn) xsanity(err error, format string, args ...any) {
307 panic(fmt.Errorf("%s: %s", fmt.Sprintf(format, args...), err))
309 c.log.Errorx(fmt.Sprintf(format, args...), err)
314// Listen initializes all imap listeners for the configuration, and stores them for Serve to start them.
316 names := maps.Keys(mox.Conf.Static.Listeners)
318 for _, name := range names {
319 listener := mox.Conf.Static.Listeners[name]
321 var tlsConfig *tls.Config
322 if listener.TLS != nil {
323 tlsConfig = listener.TLS.Config
326 if listener.IMAP.Enabled {
327 port := config.Port(listener.IMAP.Port, 143)
328 for _, ip := range listener.IPs {
329 listen1("imap", name, ip, port, tlsConfig, false, listener.IMAP.NoRequireSTARTTLS)
333 if listener.IMAPS.Enabled {
334 port := config.Port(listener.IMAPS.Port, 993)
335 for _, ip := range listener.IPs {
336 listen1("imaps", name, ip, port, tlsConfig, true, false)
344func listen1(protocol, listenerName, ip string, port int, tlsConfig *tls.Config, xtls, noRequireSTARTTLS bool) {
345 log := mlog.New("imapserver", nil)
346 addr := net.JoinHostPort(ip, fmt.Sprintf("%d", port))
347 if os.Getuid() == 0 {
348 log.Print("listening for imap",
349 slog.String("listener", listenerName),
350 slog.String("addr", addr),
351 slog.String("protocol", protocol))
353 network := mox.Network(ip)
354 ln, err := mox.Listen(network, addr)
356 log.Fatalx("imap: listen for imap", err, slog.String("protocol", protocol), slog.String("listener", listenerName))
359 ln = tls.NewListener(ln, tlsConfig)
364 conn, err := ln.Accept()
366 log.Infox("imap: accept", err, slog.String("protocol", protocol), slog.String("listener", listenerName))
370 metricIMAPConnection.WithLabelValues(protocol).Inc()
371 go serve(listenerName, mox.Cid(), tlsConfig, conn, xtls, noRequireSTARTTLS)
375 servers = append(servers, serve)
378// Serve starts serving on all listeners, launching a goroutine per listener.
380 for _, serve := range servers {
386// returns whether this connection accepts utf-8 in strings.
387func (c *conn) utf8strings() bool {
388 return c.enabled[capIMAP4rev2] || c.enabled[capUTF8Accept]
391func (c *conn) encodeMailbox(s string) string {
398func (c *conn) xdbwrite(fn func(tx *bstore.Tx)) {
399 err := c.account.DB.Write(context.TODO(), func(tx *bstore.Tx) error {
403 xcheckf(err, "transaction")
406func (c *conn) xdbread(fn func(tx *bstore.Tx)) {
407 err := c.account.DB.Read(context.TODO(), func(tx *bstore.Tx) error {
411 xcheckf(err, "transaction")
414// Closes the currently selected/active mailbox, setting state from selected to authenticated.
415// Does not remove messages marked for deletion.
416func (c *conn) unselect() {
417 if c.state == stateSelected {
418 c.state = stateAuthenticated
424func (c *conn) setSlow(on bool) {
426 c.log.Debug("connection changed to slow")
427 } else if !on && c.slow {
428 c.log.Debug("connection restored to regular pace")
433// Write makes a connection an io.Writer. It panics for i/o errors. These errors
434// are handled in the connection command loop.
435func (c *conn) Write(buf []byte) (int, error) {
443 err := c.conn.SetWriteDeadline(time.Now().Add(30 * time.Second))
444 c.log.Check(err, "setting write deadline")
446 nn, err := c.conn.Write(buf[:chunk])
448 panic(fmt.Errorf("write: %s (%w)", err, errIO))
452 if len(buf) > 0 && badClientDelay > 0 {
453 mox.Sleep(mox.Context, badClientDelay)
459func (c *conn) xtrace(level slog.Level) func() {
465 c.tr.SetTrace(mlog.LevelTrace)
466 c.tw.SetTrace(mlog.LevelTrace)
470// Cache of line buffers for reading commands.
472var bufpool = moxio.NewBufpool(8, 16*1024)
474// read line from connection, not going through line channel.
475func (c *conn) readline0() (string, error) {
476 if c.slow && badClientDelay > 0 {
477 mox.Sleep(mox.Context, badClientDelay)
480 d := 30 * time.Minute
481 if c.state == stateNotAuthenticated {
484 err := c.conn.SetReadDeadline(time.Now().Add(d))
485 c.log.Check(err, "setting read deadline")
487 line, err := bufpool.Readline(c.log, c.br)
488 if err != nil && errors.Is(err, moxio.ErrLineTooLong) {
489 return "", fmt.Errorf("%s (%w)", err, errProtocol)
490 } else if err != nil {
491 return "", fmt.Errorf("%s (%w)", err, errIO)
496func (c *conn) lineChan() chan lineErr {
498 c.line = make(chan lineErr, 1)
500 line, err := c.readline0()
501 c.line <- lineErr{line, err}
507// readline from either the c.line channel, or otherwise read from connection.
508func (c *conn) readline(readCmd bool) string {
514 line, err = le.line, le.err
516 line, err = c.readline0()
519 if readCmd && errors.Is(err, os.ErrDeadlineExceeded) {
520 err := c.conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
521 c.log.Check(err, "setting write deadline")
522 c.writelinef("* BYE inactive")
524 if !errors.Is(err, errIO) && !errors.Is(err, errProtocol) {
525 err = fmt.Errorf("%s (%w)", err, errIO)
531 // We typically respond immediately (IDLE is an exception).
532 // The client may not be reading, or may have disappeared.
533 // Don't wait more than 5 minutes before closing down the connection.
534 // The write deadline is managed in IDLE as well.
535 // For unauthenticated connections, we require the client to read faster.
536 wd := 5 * time.Minute
537 if c.state == stateNotAuthenticated {
538 wd = 30 * time.Second
540 err = c.conn.SetWriteDeadline(time.Now().Add(wd))
541 c.log.Check(err, "setting write deadline")
546// write tagged command response, but first write pending changes.
547func (c *conn) writeresultf(format string, args ...any) {
548 c.bwriteresultf(format, args...)
552// write buffered tagged command response, but first write pending changes.
553func (c *conn) bwriteresultf(format string, args ...any) {
555 case "fetch", "store", "search":
559 c.applyChanges(c.comm.Get(), false)
562 c.bwritelinef(format, args...)
565func (c *conn) writelinef(format string, args ...any) {
566 c.bwritelinef(format, args...)
570// Buffer line for write.
571func (c *conn) bwritelinef(format string, args ...any) {
573 fmt.Fprintf(c.bw, format, args...)
576func (c *conn) xflush() {
578 xcheckf(err, "flush") // Should never happen, the Write caused by the Flush should panic on i/o error.
581func (c *conn) readCommand(tag *string) (cmd string, p *parser) {
582 line := c.readline(true)
583 p = newParser(line, c)
589 return cmd, newParser(p.remainder(), c)
592func (c *conn) xreadliteral(size int64, sync bool) string {
596 buf := make([]byte, size)
598 if err := c.conn.SetReadDeadline(time.Now().Add(30 * time.Second)); err != nil {
599 c.log.Errorx("setting read deadline", err)
602 _, err := io.ReadFull(c.br, buf)
604 // Cannot use xcheckf due to %w handling of errIO.
605 panic(fmt.Errorf("reading literal: %s (%w)", err, errIO))
611func (c *conn) xhighestModSeq(tx *bstore.Tx, mailboxID int64) store.ModSeq {
612 qms := bstore.QueryTx[store.Message](tx)
613 qms.FilterNonzero(store.Message{MailboxID: mailboxID})
614 qms.SortDesc("ModSeq")
617 if err == bstore.ErrAbsent {
618 return store.ModSeq(0)
620 xcheckf(err, "looking up highest modseq for mailbox")
624var cleanClose struct{} // Sentinel value for panic/recover indicating clean close of connection.
626func serve(listenerName string, cid int64, tlsConfig *tls.Config, nc net.Conn, xtls, noRequireSTARTTLS bool) {
628 if a, ok := nc.RemoteAddr().(*net.TCPAddr); ok {
631 // For net.Pipe, during tests.
632 remoteIP = net.ParseIP("127.0.0.10")
640 tlsConfig: tlsConfig,
642 noRequireSTARTTLS: noRequireSTARTTLS,
643 enabled: map[capability]bool{},
645 cmdStart: time.Now(),
647 var logmutex sync.Mutex
648 c.log = mlog.New("imapserver", nil).WithFunc(func() []slog.Attr {
650 defer logmutex.Unlock()
653 slog.Int64("cid", c.cid),
654 slog.Duration("delta", now.Sub(c.lastlog)),
657 if c.username != "" {
658 l = append(l, slog.String("username", c.username))
662 c.tr = moxio.NewTraceReader(c.log, "C: ", c.conn)
663 c.tw = moxio.NewTraceWriter(c.log, "S: ", c)
664 // 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.
665 c.br = bufio.NewReader(c.tr)
666 c.bw = bufio.NewWriter(c.tw)
668 // Many IMAP connections use IDLE to wait for new incoming messages. We'll enable
669 // keepalive to get a higher chance of the connection staying alive, or otherwise
670 // detecting broken connections early.
673 xconn = c.conn.(*tls.Conn).NetConn()
675 if tcpconn, ok := xconn.(*net.TCPConn); ok {
676 if err := tcpconn.SetKeepAlivePeriod(5 * time.Minute); err != nil {
677 c.log.Errorx("setting keepalive period", err)
678 } else if err := tcpconn.SetKeepAlive(true); err != nil {
679 c.log.Errorx("enabling keepalive", err)
683 c.log.Info("new connection",
684 slog.Any("remote", c.conn.RemoteAddr()),
685 slog.Any("local", c.conn.LocalAddr()),
686 slog.Bool("tls", xtls),
687 slog.String("listener", listenerName))
692 if c.account != nil {
694 err := c.account.Close()
695 c.xsanity(err, "close account")
701 if x == nil || x == cleanClose {
702 c.log.Info("connection closed")
703 } else if err, ok := x.(error); ok && isClosed(err) {
704 c.log.Infox("connection closed", err)
706 c.log.Error("unhandled panic", slog.Any("err", x))
708 metrics.PanicInc(metrics.Imapserver)
713 case <-mox.Shutdown.Done():
715 c.writelinef("* BYE mox shutting down")
720 if !limiterConnectionrate.Add(c.remoteIP, time.Now(), 1) {
721 c.writelinef("* BYE connection rate from your ip or network too high, slow down please")
725 // If remote IP/network resulted in too many authentication failures, refuse to serve.
726 if !mox.LimiterFailedAuth.CanAdd(c.remoteIP, time.Now(), 1) {
727 metrics.AuthenticationRatelimitedInc("imap")
728 c.log.Debug("refusing connection due to many auth failures", slog.Any("remoteip", c.remoteIP))
729 c.writelinef("* BYE too many auth failures")
733 if !limiterConnections.Add(c.remoteIP, time.Now(), 1) {
734 c.log.Debug("refusing connection due to many open connections", slog.Any("remoteip", c.remoteIP))
735 c.writelinef("* BYE too many open connections from your ip or network")
738 defer limiterConnections.Add(c.remoteIP, time.Now(), -1)
740 // We register and unregister the original connection, in case it c.conn is
741 // replaced with a TLS connection later on.
742 mox.Connections.Register(nc, "imap", listenerName)
743 defer mox.Connections.Unregister(nc)
745 c.writelinef("* OK [CAPABILITY %s] mox imap", c.capabilities())
749 c.xflush() // For flushing errors, or possibly commands that did not flush explicitly.
753// isClosed returns whether i/o failed, typically because the connection is closed.
754// For connection errors, we often want to generate fewer logs.
755func isClosed(err error) bool {
756 return errors.Is(err, errIO) || errors.Is(err, errProtocol) || moxio.IsClosed(err)
759func (c *conn) command() {
760 var tag, cmd, cmdlow string
766 metricIMAPCommands.WithLabelValues(c.cmdMetric, result).Observe(float64(time.Since(c.cmdStart)) / float64(time.Second))
769 logFields := []slog.Attr{
770 slog.String("cmd", c.cmd),
771 slog.Duration("duration", time.Since(c.cmdStart)),
776 if x == nil || x == cleanClose {
777 c.log.Debug("imap command done", logFields...)
786 c.log.Error("imap command panic", append([]slog.Attr{slog.Any("panic", x)}, logFields...)...)
791 var sxerr syntaxError
795 c.log.Infox("imap command ioerror", err, logFields...)
797 if errors.Is(err, errProtocol) {
801 } else if errors.As(err, &sxerr) {
804 // Other side is likely speaking something else than IMAP, send error message and
805 // stop processing because there is a good chance whatever they sent has multiple
807 c.writelinef("* BYE please try again speaking imap")
810 c.log.Debugx("imap command syntax error", sxerr.err, logFields...)
811 c.log.Info("imap syntax error", slog.String("lastline", c.lastLine))
812 fatal := strings.HasSuffix(c.lastLine, "+}")
814 err := c.conn.SetWriteDeadline(time.Now().Add(5 * time.Second))
815 c.log.Check(err, "setting write deadline")
817 if sxerr.line != "" {
818 c.bwritelinef("%s", sxerr.line)
821 if sxerr.code != "" {
822 code = "[" + sxerr.code + "] "
824 c.bwriteresultf("%s BAD %s%s unrecognized syntax/command: %v", tag, code, cmd, sxerr.errmsg)
827 panic(fmt.Errorf("aborting connection after syntax error for command with non-sync literal: %w", errProtocol))
829 } else if errors.As(err, &serr) {
830 result = "servererror"
831 c.log.Errorx("imap command server error", err, logFields...)
833 c.bwriteresultf("%s NO %s %v", tag, cmd, err)
834 } else if errors.As(err, &uerr) {
836 c.log.Debugx("imap command user error", err, logFields...)
838 c.bwriteresultf("%s NO [%s] %s %v", tag, uerr.code, cmd, err)
840 c.bwriteresultf("%s NO %s %v", tag, cmd, err)
843 // Other type of panic, we pass it on, aborting the connection.
845 c.log.Errorx("imap command panic", err, logFields...)
851 cmd, p = c.readCommand(&tag)
852 cmdlow = strings.ToLower(cmd)
854 c.cmdStart = time.Now()
855 c.cmdMetric = "(unrecognized)"
858 case <-mox.Shutdown.Done():
860 c.writelinef("* BYE shutting down")
865 fn := commands[cmdlow]
867 xsyntaxErrorf("unknown command %q", cmd)
872 // Check if command is allowed in this state.
873 if _, ok1 := commandsStateAny[cmdlow]; ok1 {
874 } else if _, ok2 := commandsStateNotAuthenticated[cmdlow]; ok2 && c.state == stateNotAuthenticated {
875 } else if _, ok3 := commandsStateAuthenticated[cmdlow]; ok3 && c.state == stateAuthenticated || c.state == stateSelected {
876 } else if _, ok4 := commandsStateSelected[cmdlow]; ok4 && c.state == stateSelected {
877 } else if ok1 || ok2 || ok3 || ok4 {
878 xuserErrorf("not allowed in this connection state")
880 xserverErrorf("unrecognized command")
886func (c *conn) broadcast(changes []store.Change) {
887 if len(changes) == 0 {
890 c.log.Debug("broadcast changes", slog.Any("changes", changes))
891 c.comm.Broadcast(changes)
894// matchStringer matches a string against reference + mailbox patterns.
895type matchStringer interface {
896 MatchString(s string) bool
901// MatchString for noMatch always returns false.
902func (noMatch) MatchString(s string) bool {
906// xmailboxPatternMatcher returns a matcher for mailbox names given the reference and patterns.
907// Patterns can include "%" and "*", matching any character excluding and including a slash respectively.
908func xmailboxPatternMatcher(ref string, patterns []string) matchStringer {
909 if strings.HasPrefix(ref, "/") {
914 for _, pat := range patterns {
915 if strings.HasPrefix(pat, "/") {
921 s = path.Join(ref, pat)
924 // Fix casing for all Inbox paths.
925 first := strings.SplitN(s, "/", 2)[0]
926 if strings.EqualFold(first, "Inbox") {
927 s = "Inbox" + s[len("Inbox"):]
932 for _, c := range s {
938 rs += regexp.QuoteMeta(string(c))
941 subs = append(subs, rs)
947 rs := "^(" + strings.Join(subs, "|") + ")$"
948 re, err := regexp.Compile(rs)
949 xcheckf(err, "compiling regexp for mailbox patterns")
953func (c *conn) sequence(uid store.UID) msgseq {
954 return uidSearch(c.uids, uid)
957func uidSearch(uids []store.UID, uid store.UID) msgseq {
974func (c *conn) xsequence(uid store.UID) msgseq {
975 seq := c.sequence(uid)
977 xserverErrorf("unknown uid %d (%w)", uid, errProtocol)
982func (c *conn) sequenceRemove(seq msgseq, uid store.UID) {
984 if c.uids[i] != uid {
985 xserverErrorf(fmt.Sprintf("got uid %d at msgseq %d, expected uid %d", uid, seq, c.uids[i]))
987 copy(c.uids[i:], c.uids[i+1:])
988 c.uids = c.uids[:len(c.uids)-1]
994// add uid to the session. care must be taken that pending changes are fetched
995// while holding the account wlock, and applied before adding this uid, because
996// those pending changes may contain another new uid that has to be added first.
997func (c *conn) uidAppend(uid store.UID) {
998 if uidSearch(c.uids, uid) > 0 {
999 xserverErrorf("uid already present (%w)", errProtocol)
1001 if len(c.uids) > 0 && uid < c.uids[len(c.uids)-1] {
1002 xserverErrorf("new uid %d is smaller than last uid %d (%w)", uid, c.uids[len(c.uids)-1], errProtocol)
1004 c.uids = append(c.uids, uid)
1010// sanity check that uids are in ascending order.
1011func checkUIDs(uids []store.UID) {
1012 for i, uid := range uids {
1013 if uid == 0 || i > 0 && uid <= uids[i-1] {
1014 xserverErrorf("bad uids %v", uids)
1019func (c *conn) xnumSetUIDs(isUID bool, nums numSet) []store.UID {
1020 _, uids := c.xnumSetConditionUIDs(false, true, isUID, nums)
1024func (c *conn) xnumSetCondition(isUID bool, nums numSet) []any {
1025 uidargs, _ := c.xnumSetConditionUIDs(true, false, isUID, nums)
1029func (c *conn) xnumSetConditionUIDs(forDB, returnUIDs bool, isUID bool, nums numSet) ([]any, []store.UID) {
1030 if nums.searchResult {
1031 // Update previously stored UIDs. Some may have been deleted.
1032 // Once deleted a UID will never come back, so we'll just remove those uids.
1034 for _, uid := range c.searchResult {
1035 if uidSearch(c.uids, uid) > 0 {
1036 c.searchResult[o] = uid
1040 c.searchResult = c.searchResult[:o]
1041 uidargs := make([]any, len(c.searchResult))
1042 for i, uid := range c.searchResult {
1045 return uidargs, c.searchResult
1049 var uids []store.UID
1051 add := func(uid store.UID) {
1053 uidargs = append(uidargs, uid)
1056 uids = append(uids, uid)
1061 // Sequence numbers that don't exist, or * on an empty mailbox, should result in a BAD response.
../rfc/9051:7018
1062 for _, r := range nums.ranges {
1065 if len(c.uids) == 0 {
1066 xsyntaxErrorf("invalid seqset * on empty mailbox")
1068 ia = len(c.uids) - 1
1070 ia = int(r.first.number - 1)
1071 if ia >= len(c.uids) {
1072 xsyntaxErrorf("msgseq %d not in mailbox", r.first.number)
1081 if len(c.uids) == 0 {
1082 xsyntaxErrorf("invalid seqset * on empty mailbox")
1084 ib = len(c.uids) - 1
1086 ib = int(r.last.number - 1)
1087 if ib >= len(c.uids) {
1088 xsyntaxErrorf("msgseq %d not in mailbox", r.last.number)
1094 for _, uid := range c.uids[ia : ib+1] {
1098 return uidargs, uids
1101 // UIDs that do not exist can be ignored.
1102 if len(c.uids) == 0 {
1106 for _, r := range nums.ranges {
1112 uida := store.UID(r.first.number)
1114 uida = c.uids[len(c.uids)-1]
1117 uidb := store.UID(last.number)
1119 uidb = c.uids[len(c.uids)-1]
1123 uida, uidb = uidb, uida
1126 // Binary search for uida.
1131 if uida < c.uids[m] {
1133 } else if uida > c.uids[m] {
1140 for _, uid := range c.uids[s:] {
1141 if uid >= uida && uid <= uidb {
1143 } else if uid > uidb {
1149 return uidargs, uids
1152func (c *conn) ok(tag, cmd string) {
1153 c.bwriteresultf("%s OK %s done", tag, cmd)
1157// xcheckmailboxname checks if name is valid, returning an INBOX-normalized name.
1158// I.e. it changes various casings of INBOX and INBOX/* to Inbox and Inbox/*.
1159// Name is invalid if it contains leading/trailing/double slashes, or when it isn't
1160// unicode-normalized, or when empty or has special characters.
1161func xcheckmailboxname(name string, allowInbox bool) string {
1162 name, isinbox, err := store.CheckMailboxName(name, allowInbox)
1164 xuserErrorf("special mailboxname Inbox not allowed")
1165 } else if err != nil {
1166 xusercodeErrorf("CANNOT", "%s", err)
1171// Lookup mailbox by name.
1172// If the mailbox does not exist, panic is called with a user error.
1173// Must be called with account rlock held.
1174func (c *conn) xmailbox(tx *bstore.Tx, name string, missingErrCode string) store.Mailbox {
1175 mb, err := c.account.MailboxFind(tx, name)
1176 xcheckf(err, "finding mailbox")
1178 // missingErrCode can be empty, or e.g. TRYCREATE or ALREADYEXISTS.
1179 xusercodeErrorf(missingErrCode, "%w", store.ErrUnknownMailbox)
1184// Lookup mailbox by ID.
1185// If the mailbox does not exist, panic is called with a user error.
1186// Must be called with account rlock held.
1187func (c *conn) xmailboxID(tx *bstore.Tx, id int64) store.Mailbox {
1188 mb := store.Mailbox{ID: id}
1190 if err == bstore.ErrAbsent {
1191 xuserErrorf("%w", store.ErrUnknownMailbox)
1196// Apply changes to our session state.
1197// If initial is false, updates like EXISTS and EXPUNGE are written to the client.
1198// If initial is true, we only apply the changes.
1199// Should not be called while holding locks, as changes are written to client connections, which can block.
1200// Does not flush output.
1201func (c *conn) applyChanges(changes []store.Change, initial bool) {
1202 if len(changes) == 0 {
1206 err := c.conn.SetWriteDeadline(time.Now().Add(5 * time.Minute))
1207 c.log.Check(err, "setting write deadline")
1209 c.log.Debug("applying changes", slog.Any("changes", changes))
1211 // Only keep changes for the selected mailbox, and changes that are always relevant.
1212 var n []store.Change
1213 for _, change := range changes {
1215 switch ch := change.(type) {
1216 case store.ChangeAddUID:
1218 case store.ChangeRemoveUIDs:
1220 case store.ChangeFlags:
1222 case store.ChangeRemoveMailbox, store.ChangeAddMailbox, store.ChangeRenameMailbox, store.ChangeAddSubscription:
1223 n = append(n, change)
1225 case store.ChangeMailboxCounts, store.ChangeMailboxSpecialUse, store.ChangeMailboxKeywords, store.ChangeThread:
1227 panic(fmt.Errorf("missing case for %#v", change))
1229 if c.state == stateSelected && mbID == c.mailboxID {
1230 n = append(n, change)
1235 qresync := c.enabled[capQresync]
1236 condstore := c.enabled[capCondstore]
1239 for i < len(changes) {
1240 // First process all new uids. So we only send a single EXISTS.
1241 var adds []store.ChangeAddUID
1242 for ; i < len(changes); i++ {
1243 ch, ok := changes[i].(store.ChangeAddUID)
1247 seq := c.sequence(ch.UID)
1248 if seq > 0 && initial {
1252 adds = append(adds, ch)
1258 // Write the exists, and the UID and flags as well. Hopefully the client waits for
1259 // long enough after the EXISTS to see these messages, and doesn't request them
1260 // again with a FETCH.
1261 c.bwritelinef("* %d EXISTS", len(c.uids))
1262 for _, add := range adds {
1263 seq := c.xsequence(add.UID)
1264 var modseqStr string
1266 modseqStr = fmt.Sprintf(" MODSEQ (%d)", add.ModSeq.Client())
1268 c.bwritelinef("* %d FETCH (UID %d FLAGS %s%s)", seq, add.UID, flaglist(add.Flags, add.Keywords).pack(c), modseqStr)
1273 change := changes[i]
1276 switch ch := change.(type) {
1277 case store.ChangeRemoveUIDs:
1278 var vanishedUIDs numSet
1279 for _, uid := range ch.UIDs {
1282 seq = c.sequence(uid)
1287 seq = c.xsequence(uid)
1289 c.sequenceRemove(seq, uid)
1292 vanishedUIDs.append(uint32(uid))
1294 c.bwritelinef("* %d EXPUNGE", seq)
1300 for _, s := range vanishedUIDs.Strings(4*1024 - 32) {
1301 c.bwritelinef("* VANISHED %s", s)
1304 case store.ChangeFlags:
1305 // The uid can be unknown if we just expunged it while another session marked it as deleted just before.
1306 seq := c.sequence(ch.UID)
1311 var modseqStr string
1313 modseqStr = fmt.Sprintf(" MODSEQ (%d)", ch.ModSeq.Client())
1315 c.bwritelinef("* %d FETCH (UID %d FLAGS %s%s)", seq, ch.UID, flaglist(ch.Flags, ch.Keywords).pack(c), modseqStr)
1317 case store.ChangeRemoveMailbox:
1318 // Only announce \NonExistent to modern clients, otherwise they may ignore the
1319 // unrecognized \NonExistent and interpret this as a newly created mailbox, while
1320 // the goal was to remove it...
1321 if c.enabled[capIMAP4rev2] {
1322 c.bwritelinef(`* LIST (\NonExistent) "/" %s`, astring(c.encodeMailbox(ch.Name)).pack(c))
1324 case store.ChangeAddMailbox:
1325 c.bwritelinef(`* LIST (%s) "/" %s`, strings.Join(ch.Flags, " "), astring(c.encodeMailbox(ch.Mailbox.Name)).pack(c))
1326 case store.ChangeRenameMailbox:
1329 if c.enabled[capIMAP4rev2] {
1330 oldname = fmt.Sprintf(` ("OLDNAME" (%s))`, string0(c.encodeMailbox(ch.OldName)).pack(c))
1332 c.bwritelinef(`* LIST (%s) "/" %s%s`, strings.Join(ch.Flags, " "), astring(c.encodeMailbox(ch.NewName)).pack(c), oldname)
1333 case store.ChangeAddSubscription:
1334 c.bwritelinef(`* LIST (%s) "/" %s`, strings.Join(append([]string{`\Subscribed`}, ch.Flags...), " "), astring(c.encodeMailbox(ch.Name)).pack(c))
1336 panic(fmt.Sprintf("internal error, missing case for %#v", change))
1341// Capability returns the capabilities this server implements and currently has
1342// available given the connection state.
1345func (c *conn) cmdCapability(tag, cmd string, p *parser) {
1351 caps := c.capabilities()
1354 c.bwritelinef("* CAPABILITY %s", caps)
1358// capabilities returns non-empty string with available capabilities based on connection state.
1359// For use in cmdCapability and untagged OK responses on connection start, login and authenticate.
1360func (c *conn) capabilities() string {
1361 caps := serverCapabilities
1363 // We only allow starting without TLS when explicitly configured, in violation of RFC.
1364 if !c.tls && c.tlsConfig != nil {
1367 if c.tls || c.noRequireSTARTTLS {
1368 caps += " AUTH=PLAIN"
1370 caps += " LOGINDISABLED"
1375// No op, but useful for retrieving pending changes as untagged responses, e.g. of
1379func (c *conn) cmdNoop(tag, cmd string, p *parser) {
1387// Logout, after which server closes the connection.
1390func (c *conn) cmdLogout(tag, cmd string, p *parser) {
1397 c.state = stateNotAuthenticated
1399 c.bwritelinef("* BYE thanks")
1404// Clients can use ID to tell the server which software they are using. Servers can
1405// respond with their version. For statistics/logging/debugging purposes.
1408func (c *conn) cmdID(tag, cmd string, p *parser) {
1413 var params map[string]string
1415 params = map[string]string{}
1417 if len(params) > 0 {
1423 if _, ok := params[k]; ok {
1424 xsyntaxErrorf("duplicate key %q", k)
1433 // We just log the client id.
1434 c.log.Info("client id", slog.Any("params", params))
1438 c.bwritelinef(`* ID ("name" "mox" "version" %s)`, string0(moxvar.Version).pack(c))
1442// STARTTLS enables TLS on the connection, after a plain text start.
1443// Only allowed if TLS isn't already enabled, either through connecting to a
1444// TLS-enabled TCP port, or a previous STARTTLS command.
1445// After STARTTLS, plain text authentication typically becomes available.
1447// Status: Not authenticated.
1448func (c *conn) cmdStarttls(tag, cmd string, p *parser) {
1457 if c.tlsConfig == nil {
1458 xsyntaxErrorf("starttls not announced")
1462 if n := c.br.Buffered(); n > 0 {
1463 buf := make([]byte, n)
1464 _, err := io.ReadFull(c.br, buf)
1465 xcheckf(err, "reading buffered data for tls handshake")
1466 conn = &prefixConn{buf, conn}
1468 // We add the cid to facilitate debugging in case of TLS connection failure.
1469 c.ok(tag, cmd+" ("+mox.ReceivedID(c.cid)+")")
1471 cidctx := context.WithValue(mox.Context, mlog.CidKey, c.cid)
1472 ctx, cancel := context.WithTimeout(cidctx, time.Minute)
1474 tlsConn := tls.Server(conn, c.tlsConfig)
1475 c.log.Debug("starting tls server handshake")
1476 if err := tlsConn.HandshakeContext(ctx); err != nil {
1477 panic(fmt.Errorf("starttls handshake: %s (%w)", err, errIO))
1480 tlsversion, ciphersuite := moxio.TLSInfo(tlsConn)
1481 c.log.Debug("tls server handshake done", slog.String("tls", tlsversion), slog.String("ciphersuite", ciphersuite))
1484 c.tr = moxio.NewTraceReader(c.log, "C: ", c.conn)
1485 c.tw = moxio.NewTraceWriter(c.log, "S: ", c)
1486 c.br = bufio.NewReader(c.tr)
1487 c.bw = bufio.NewWriter(c.tw)
1491// Authenticate using SASL. Supports multiple back and forths between client and
1492// server to finish authentication, unlike LOGIN which is just a single
1493// username/password.
1495// Status: Not authenticated.
1496func (c *conn) cmdAuthenticate(tag, cmd string, p *parser) {
1500 // For many failed auth attempts, slow down verification attempts.
1501 if c.authFailed > 3 && authFailDelay > 0 {
1502 mox.Sleep(mox.Context, time.Duration(c.authFailed-3)*authFailDelay)
1505 // If authentication fails due to missing derived secrets, we don't hold it against
1506 // the connection. There is no way to indicate server support for an authentication
1507 // mechanism, but that a mechanism won't work for an account.
1508 var missingDerivedSecrets bool
1510 c.authFailed++ // Compensated on success.
1512 if missingDerivedSecrets {
1515 // On the 3rd failed authentication, start responding slowly. Successful auth will
1516 // cause fast responses again.
1517 if c.authFailed >= 3 {
1522 var authVariant string
1523 authResult := "error"
1525 metrics.AuthenticationInc("imap", authVariant, authResult)
1526 if authResult == "ok" {
1527 mox.LimiterFailedAuth.Reset(c.remoteIP, time.Now())
1528 } else if !missingDerivedSecrets {
1529 mox.LimiterFailedAuth.Add(c.remoteIP, time.Now(), 1)
1535 authType := p.xatom()
1537 xreadInitial := func() []byte {
1541 line = c.readline(false)
1545 line = p.remainder()
1548 line = "" // Base64 decode will result in empty buffer.
1553 authResult = "aborted"
1554 xsyntaxErrorf("authenticate aborted by client")
1556 buf, err := base64.StdEncoding.DecodeString(line)
1558 xsyntaxErrorf("parsing base64: %v", err)
1563 xreadContinuation := func() []byte {
1564 line := c.readline(false)
1566 authResult = "aborted"
1567 xsyntaxErrorf("authenticate aborted by client")
1569 buf, err := base64.StdEncoding.DecodeString(line)
1571 xsyntaxErrorf("parsing base64: %v", err)
1576 switch strings.ToUpper(authType) {
1578 authVariant = "plain"
1580 if !c.noRequireSTARTTLS && !c.tls {
1582 xusercodeErrorf("PRIVACYREQUIRED", "tls required for login")
1585 // Plain text passwords, mark as traceauth.
1586 defer c.xtrace(mlog.LevelTraceauth)()
1587 buf := xreadInitial()
1588 c.xtrace(mlog.LevelTrace) // Restore.
1589 plain := bytes.Split(buf, []byte{0})
1590 if len(plain) != 3 {
1591 xsyntaxErrorf("bad plain auth data, expected 3 nul-separated tokens, got %d tokens", len(plain))
1593 authz := string(plain[0])
1594 authc := string(plain[1])
1595 password := string(plain[2])
1597 if authz != "" && authz != authc {
1598 xusercodeErrorf("AUTHORIZATIONFAILED", "cannot assume role")
1601 acc, err := store.OpenEmailAuth(c.log, authc, password)
1603 if errors.Is(err, store.ErrUnknownCredentials) {
1604 authResult = "badcreds"
1605 c.log.Info("authentication failed", slog.String("username", authc))
1606 xusercodeErrorf("AUTHENTICATIONFAILED", "bad credentials")
1608 xusercodeErrorf("", "error")
1614 authVariant = strings.ToLower(authType)
1620 chal := fmt.Sprintf("<%d.%d@%s>", uint64(mox.CryptoRandInt()), time.Now().UnixNano(), mox.Conf.Static.HostnameDomain.ASCII)
1621 c.writelinef("+ %s", base64.StdEncoding.EncodeToString([]byte(chal)))
1623 resp := xreadContinuation()
1624 t := strings.Split(string(resp), " ")
1625 if len(t) != 2 || len(t[1]) != 2*md5.Size {
1626 xsyntaxErrorf("malformed cram-md5 response")
1629 c.log.Debug("cram-md5 auth", slog.String("address", addr))
1630 acc, _, err := store.OpenEmail(c.log, addr)
1632 if errors.Is(err, store.ErrUnknownCredentials) {
1633 c.log.Info("failed authentication attempt", slog.String("username", addr), slog.Any("remote", c.remoteIP))
1634 xusercodeErrorf("AUTHENTICATIONFAILED", "bad credentials")
1636 xserverErrorf("looking up address: %v", err)
1641 c.xsanity(err, "close account")
1644 var ipadhash, opadhash hash.Hash
1645 acc.WithRLock(func() {
1646 err := acc.DB.Read(context.TODO(), func(tx *bstore.Tx) error {
1647 password, err := bstore.QueryTx[store.Password](tx).Get()
1648 if err == bstore.ErrAbsent {
1649 c.log.Info("failed authentication attempt", slog.String("username", addr), slog.Any("remote", c.remoteIP))
1650 xusercodeErrorf("AUTHENTICATIONFAILED", "bad credentials")
1656 ipadhash = password.CRAMMD5.Ipad
1657 opadhash = password.CRAMMD5.Opad
1660 xcheckf(err, "tx read")
1662 if ipadhash == nil || opadhash == nil {
1663 c.log.Info("cram-md5 auth attempt without derived secrets set, save password again to store secrets", slog.String("username", addr))
1664 c.log.Info("failed authentication attempt", slog.String("username", addr), slog.Any("remote", c.remoteIP))
1665 missingDerivedSecrets = true
1666 xusercodeErrorf("AUTHENTICATIONFAILED", "bad credentials")
1670 ipadhash.Write([]byte(chal))
1671 opadhash.Write(ipadhash.Sum(nil))
1672 digest := fmt.Sprintf("%x", opadhash.Sum(nil))
1674 c.log.Info("failed authentication attempt", slog.String("username", addr), slog.Any("remote", c.remoteIP))
1675 xusercodeErrorf("AUTHENTICATIONFAILED", "bad credentials")
1679 acc = nil // Cancel cleanup.
1682 case "SCRAM-SHA-256-PLUS", "SCRAM-SHA-256", "SCRAM-SHA-1-PLUS", "SCRAM-SHA-1":
1683 // 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?
1684 // todo: use single implementation between ../imapserver/server.go and ../smtpserver/server.go
1686 // No plaintext credentials, we can log these normally.
1688 authVariant = strings.ToLower(authType)
1689 var h func() hash.Hash
1690 switch authVariant {
1691 case "scram-sha-1", "scram-sha-1-plus":
1693 case "scram-sha-256", "scram-sha-256-plus":
1696 xserverErrorf("missing case for scram variant")
1699 var cs *tls.ConnectionState
1700 requireChannelBinding := strings.HasSuffix(authVariant, "-plus")
1701 if requireChannelBinding && !c.tls {
1702 xuserErrorf("cannot use plus variant with tls channel binding without tls")
1705 xcs := c.conn.(*tls.Conn).ConnectionState()
1708 c0 := xreadInitial()
1709 ss, err := scram.NewServer(h, c0, cs, requireChannelBinding)
1711 xsyntaxErrorf("starting scram: %s", err)
1713 c.log.Debug("scram auth", slog.String("authentication", ss.Authentication))
1714 acc, _, err := store.OpenEmail(c.log, ss.Authentication)
1716 // todo: we could continue scram with a generated salt, deterministically generated
1717 // from the username. that way we don't have to store anything but attackers cannot
1718 // learn if an account exists. same for absent scram saltedpassword below.
1719 xuserErrorf("scram not possible")
1724 c.xsanity(err, "close account")
1727 if ss.Authorization != "" && ss.Authorization != ss.Authentication {
1728 xuserErrorf("authentication with authorization for different user not supported")
1730 var xscram store.SCRAM
1731 acc.WithRLock(func() {
1732 err := acc.DB.Read(context.TODO(), func(tx *bstore.Tx) error {
1733 password, err := bstore.QueryTx[store.Password](tx).Get()
1734 if err == bstore.ErrAbsent {
1735 c.log.Info("failed authentication attempt", slog.String("username", ss.Authentication), slog.Any("remote", c.remoteIP))
1736 xusercodeErrorf("AUTHENTICATIONFAILED", "bad credentials")
1738 xcheckf(err, "fetching credentials")
1739 switch authVariant {
1740 case "scram-sha-1", "scram-sha-1-plus":
1741 xscram = password.SCRAMSHA1
1742 case "scram-sha-256", "scram-sha-256-plus":
1743 xscram = password.SCRAMSHA256
1745 xserverErrorf("missing case for scram credentials")
1747 if len(xscram.Salt) == 0 || xscram.Iterations == 0 || len(xscram.SaltedPassword) == 0 {
1748 missingDerivedSecrets = true
1749 c.log.Info("scram auth attempt without derived secrets set, save password again to store secrets", slog.String("address", ss.Authentication))
1750 xuserErrorf("scram not possible")
1754 xcheckf(err, "read tx")
1756 s1, err := ss.ServerFirst(xscram.Iterations, xscram.Salt)
1757 xcheckf(err, "scram first server step")
1758 c.writelinef("+ %s", base64.StdEncoding.EncodeToString([]byte(s1)))
1759 c2 := xreadContinuation()
1760 s3, err := ss.Finish(c2, xscram.SaltedPassword)
1762 c.writelinef("+ %s", base64.StdEncoding.EncodeToString([]byte(s3)))
1765 c.readline(false) // Should be "*" for cancellation.
1766 if errors.Is(err, scram.ErrInvalidProof) {
1767 authResult = "badcreds"
1768 c.log.Info("failed authentication attempt", slog.String("username", ss.Authentication), slog.Any("remote", c.remoteIP))
1769 xusercodeErrorf("AUTHENTICATIONFAILED", "bad credentials")
1771 xuserErrorf("server final: %w", err)
1775 // The message should be empty. todo: should we require it is empty?
1779 acc = nil // Cancel cleanup.
1780 c.username = ss.Authentication
1783 xuserErrorf("method not supported")
1789 c.comm = store.RegisterComm(c.account)
1790 c.state = stateAuthenticated
1791 c.writeresultf("%s OK [CAPABILITY %s] authenticate done", tag, c.capabilities())
1794// Login logs in with username and password.
1796// Status: Not authenticated.
1797func (c *conn) cmdLogin(tag, cmd string, p *parser) {
1800 authResult := "error"
1802 metrics.AuthenticationInc("imap", "login", authResult)
1805 // 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).
1809 userid := p.xastring()
1811 password := p.xastring()
1814 if !c.noRequireSTARTTLS && !c.tls {
1816 xusercodeErrorf("PRIVACYREQUIRED", "tls required for login")
1819 // For many failed auth attempts, slow down verification attempts.
1820 if c.authFailed > 3 && authFailDelay > 0 {
1821 mox.Sleep(mox.Context, time.Duration(c.authFailed-3)*authFailDelay)
1823 c.authFailed++ // Compensated on success.
1825 // On the 3rd failed authentication, start responding slowly. Successful auth will
1826 // cause fast responses again.
1827 if c.authFailed >= 3 {
1832 acc, err := store.OpenEmailAuth(c.log, userid, password)
1834 authResult = "badcreds"
1836 if errors.Is(err, store.ErrUnknownCredentials) {
1837 code = "AUTHENTICATIONFAILED"
1838 c.log.Info("failed authentication attempt", slog.String("username", userid), slog.Any("remote", c.remoteIP))
1840 xusercodeErrorf(code, "login failed")
1846 c.comm = store.RegisterComm(acc)
1847 c.state = stateAuthenticated
1849 c.writeresultf("%s OK [CAPABILITY %s] login done", tag, c.capabilities())
1852// Enable explicitly opts in to an extension. A server can typically send new kinds
1853// of responses to a client. Most extensions do not require an ENABLE because a
1854// client implicitly opts in to new response syntax by making a requests that uses
1855// new optional extension request syntax.
1857// State: Authenticated and selected.
1858func (c *conn) cmdEnable(tag, cmd string, p *parser) {
1864 caps := []string{p.xatom()}
1867 caps = append(caps, p.xatom())
1870 // Clients should only send capabilities that need enabling.
1871 // We should only echo that we recognize as needing enabling.
1874 for _, s := range caps {
1875 cap := capability(strings.ToUpper(s))
1880 c.enabled[cap] = true
1883 c.enabled[cap] = true
1889 if qresync && !c.enabled[capCondstore] {
1890 c.xensureCondstore(nil)
1891 enabled += " CONDSTORE"
1895 c.bwritelinef("* ENABLED%s", enabled)
1900// If a mailbox is selected, an untagged OK with HIGHESTMODSEQ is written to the
1901// client. If tx is non-nil, it is used to read the HIGHESTMODSEQ from the
1902// database. Otherwise a new read-only transaction is created.
1903func (c *conn) xensureCondstore(tx *bstore.Tx) {
1904 if !c.enabled[capCondstore] {
1905 c.enabled[capCondstore] = true
1906 // todo spec: can we send an untagged enabled response?
1908 if c.mailboxID <= 0 {
1911 var modseq store.ModSeq
1913 modseq = c.xhighestModSeq(tx, c.mailboxID)
1915 c.xdbread(func(tx *bstore.Tx) {
1916 modseq = c.xhighestModSeq(tx, c.mailboxID)
1919 c.bwritelinef("* OK [HIGHESTMODSEQ %d] after condstore-enabling command", modseq.Client())
1923// State: Authenticated and selected.
1924func (c *conn) cmdSelect(tag, cmd string, p *parser) {
1925 c.cmdSelectExamine(true, tag, cmd, p)
1928// State: Authenticated and selected.
1929func (c *conn) cmdExamine(tag, cmd string, p *parser) {
1930 c.cmdSelectExamine(false, tag, cmd, p)
1933// Select and examine are almost the same commands. Select just opens a mailbox for
1934// read/write and examine opens a mailbox readonly.
1936// State: Authenticated and selected.
1937func (c *conn) cmdSelectExamine(isselect bool, tag, cmd string, p *parser) {
1945 name := p.xmailbox()
1947 var qruidvalidity uint32
1948 var qrmodseq int64 // QRESYNC required parameters.
1949 var qrknownUIDs, qrknownSeqSet, qrknownUIDSet *numSet // QRESYNC optional parameters.
1951 seen := map[string]bool{}
1953 for len(seen) == 0 || !p.take(")") {
1954 w := p.xtakelist("CONDSTORE", "QRESYNC")
1956 xsyntaxErrorf("duplicate select parameter %s", w)
1966 // Note: unlike with CONDSTORE, there are no QRESYNC-related commands/parameters
1967 // that enable capabilities.
1968 if !c.enabled[capQresync] {
1970 xsyntaxErrorf("QRESYNC must first be enabled")
1976 qrmodseq = p.xnznumber64()
1978 seqMatchData := p.take("(")
1982 seqMatchData = p.take(" (")
1985 ss0 := p.xnumSet0(false, false)
1986 qrknownSeqSet = &ss0
1988 ss1 := p.xnumSet0(false, false)
1989 qrknownUIDSet = &ss1
1995 panic("missing case for select param " + w)
2001 // Deselect before attempting the new select. This means we will deselect when an
2002 // error occurs during select.
2004 if c.state == stateSelected {
2006 c.bwritelinef("* OK [CLOSED] x")
2010 name = xcheckmailboxname(name, true)
2012 var highestModSeq store.ModSeq
2013 var highDeletedModSeq store.ModSeq
2014 var firstUnseen msgseq = 0
2015 var mb store.Mailbox
2016 c.account.WithRLock(func() {
2017 c.xdbread(func(tx *bstore.Tx) {
2018 mb = c.xmailbox(tx, name, "")
2020 q := bstore.QueryTx[store.Message](tx)
2021 q.FilterNonzero(store.Message{MailboxID: mb.ID})
2022 q.FilterEqual("Expunged", false)
2024 c.uids = []store.UID{}
2026 err := q.ForEach(func(m store.Message) error {
2027 c.uids = append(c.uids, m.UID)
2028 if firstUnseen == 0 && !m.Seen {
2037 xcheckf(err, "fetching uids")
2039 // Condstore extension, find the highest modseq.
2040 if c.enabled[capCondstore] {
2041 highestModSeq = c.xhighestModSeq(tx, mb.ID)
2043 // For QRESYNC, we need to know the highest modset of deleted expunged records to
2044 // maintain synchronization.
2045 if c.enabled[capQresync] {
2046 highDeletedModSeq, err = c.account.HighestDeletedModSeq(tx)
2047 xcheckf(err, "getting highest deleted modseq")
2051 c.applyChanges(c.comm.Get(), true)
2054 if len(mb.Keywords) > 0 {
2055 flags = " " + strings.Join(mb.Keywords, " ")
2057 c.bwritelinef(`* FLAGS (\Seen \Answered \Flagged \Deleted \Draft $Forwarded $Junk $NotJunk $Phishing $MDNSent%s)`, flags)
2058 c.bwritelinef(`* OK [PERMANENTFLAGS (\Seen \Answered \Flagged \Deleted \Draft $Forwarded $Junk $NotJunk $Phishing $MDNSent \*)] x`)
2059 if !c.enabled[capIMAP4rev2] {
2060 c.bwritelinef(`* 0 RECENT`)
2062 c.bwritelinef(`* %d EXISTS`, len(c.uids))
2063 if !c.enabled[capIMAP4rev2] && firstUnseen > 0 {
2065 c.bwritelinef(`* OK [UNSEEN %d] x`, firstUnseen)
2067 c.bwritelinef(`* OK [UIDVALIDITY %d] x`, mb.UIDValidity)
2068 c.bwritelinef(`* OK [UIDNEXT %d] x`, mb.UIDNext)
2069 c.bwritelinef(`* LIST () "/" %s`, astring(c.encodeMailbox(mb.Name)).pack(c))
2070 if c.enabled[capCondstore] {
2073 c.bwritelinef(`* OK [HIGHESTMODSEQ %d] x`, highestModSeq.Client())
2077 if qruidvalidity == mb.UIDValidity {
2078 // We send the vanished UIDs at the end, so we can easily combine the modseq
2079 // changes and vanished UIDs that result from that, with the vanished UIDs from the
2080 // case where we don't store enough history.
2081 vanishedUIDs := map[store.UID]struct{}{}
2083 var preVanished store.UID
2084 var oldClientUID store.UID
2085 // If samples of known msgseq and uid pairs are given (they must be in order), we
2086 // use them to determine the earliest UID for which we send VANISHED responses.
2088 if qrknownSeqSet != nil {
2089 if !qrknownSeqSet.isBasicIncreasing() {
2090 xuserErrorf("QRESYNC known message sequence set must be numeric and strictly increasing")
2092 if !qrknownUIDSet.isBasicIncreasing() {
2093 xuserErrorf("QRESYNC known uid set must be numeric and strictly increasing")
2095 seqiter := qrknownSeqSet.newIter()
2096 uiditer := qrknownUIDSet.newIter()
2098 msgseq, ok0 := seqiter.Next()
2099 uid, ok1 := uiditer.Next()
2102 } else if !ok0 || !ok1 {
2103 xsyntaxErrorf("invalid combination of known sequence set and uid set, must be of equal length")
2105 i := int(msgseq - 1)
2106 if i < 0 || i >= len(c.uids) || c.uids[i] != store.UID(uid) {
2107 if uidSearch(c.uids, store.UID(uid)) <= 0 {
2108 // We will check this old client UID for consistency below.
2109 oldClientUID = store.UID(uid)
2113 preVanished = store.UID(uid + 1)
2117 // We gather vanished UIDs and report them at the end. This seems OK because we
2118 // already sent HIGHESTMODSEQ, and a client should know not to commit that value
2119 // until after it has seen the tagged OK of this command. The RFC has a remark
2120 // about ordering of some untagged responses, it's not immediately clear what it
2121 // means, but given the examples appears to allude to servers that decide to not
2122 // send expunge/vanished before the tagged OK.
2125 // We are reading without account lock. Similar to when we process FETCH/SEARCH
2126 // requests. We don't have to reverify existence of the mailbox, so we don't
2127 // rlock, even briefly.
2128 c.xdbread(func(tx *bstore.Tx) {
2129 if oldClientUID > 0 {
2130 // The client sent a UID that is now removed. This is typically fine. But we check
2131 // that it is consistent with the modseq the client sent. If the UID already didn't
2132 // exist at that modseq, the client may be missing some information.
2133 q := bstore.QueryTx[store.Message](tx)
2134 q.FilterNonzero(store.Message{MailboxID: mb.ID, UID: oldClientUID})
2137 // If client claims to be up to date up to and including qrmodseq, and the message
2138 // was deleted at or before that time, we send changes from just before that
2139 // modseq, and we send vanished for all UIDs.
2140 if m.Expunged && qrmodseq >= m.ModSeq.Client() {
2141 qrmodseq = m.ModSeq.Client() - 1
2144 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.")
2146 } else if err != bstore.ErrAbsent {
2147 xcheckf(err, "checking old client uid")
2151 q := bstore.QueryTx[store.Message](tx)
2152 q.FilterNonzero(store.Message{MailboxID: mb.ID})
2153 // Note: we don't filter by Expunged.
2154 q.FilterGreater("ModSeq", store.ModSeqFromClient(qrmodseq))
2155 q.FilterLessEqual("ModSeq", highestModSeq)
2157 err := q.ForEach(func(m store.Message) error {
2158 if m.Expunged && m.UID < preVanished {
2162 if qrknownUIDs != nil && !qrknownUIDs.contains(uint32(m.UID)) {
2166 vanishedUIDs[m.UID] = struct{}{}
2169 msgseq := c.sequence(m.UID)
2171 c.bwritelinef("* %d FETCH (UID %d FLAGS %s MODSEQ (%d))", msgseq, m.UID, flaglist(m.Flags, m.Keywords).pack(c), m.ModSeq.Client())
2175 xcheckf(err, "listing changed messages")
2178 // Add UIDs from client's known UID set to vanished list if we don't have enough history.
2179 if qrmodseq < highDeletedModSeq.Client() {
2180 // If no known uid set was in the request, we substitute 1:max or the empty set.
2182 if qrknownUIDs == nil {
2183 if len(c.uids) > 0 {
2184 qrknownUIDs = &numSet{ranges: []numRange{{first: setNumber{number: 1}, last: &setNumber{number: uint32(c.uids[len(c.uids)-1])}}}}
2186 qrknownUIDs = &numSet{}
2190 iter := qrknownUIDs.newIter()
2192 v, ok := iter.Next()
2196 if c.sequence(store.UID(v)) <= 0 {
2197 vanishedUIDs[store.UID(v)] = struct{}{}
2202 // Now that we have all vanished UIDs, send them over compactly.
2203 if len(vanishedUIDs) > 0 {
2204 l := maps.Keys(vanishedUIDs)
2205 sort.Slice(l, func(i, j int) bool {
2209 for _, s := range compactUIDSet(l).Strings(4*1024 - 32) {
2210 c.bwritelinef("* VANISHED (EARLIER) %s", s)
2216 c.bwriteresultf("%s OK [READ-WRITE] x", tag)
2219 c.bwriteresultf("%s OK [READ-ONLY] x", tag)
2223 c.state = stateSelected
2224 c.searchResult = nil
2228// Create makes a new mailbox, and its parents too if absent.
2230// State: Authenticated and selected.
2231func (c *conn) cmdCreate(tag, cmd string, p *parser) {
2237 name := p.xmailbox()
2243 name = xcheckmailboxname(name, false)
2245 var changes []store.Change
2246 var created []string // Created mailbox names.
2248 c.account.WithWLock(func() {
2249 c.xdbwrite(func(tx *bstore.Tx) {
2252 changes, created, exists, err = c.account.MailboxCreate(tx, name)
2255 xuserErrorf("mailbox already exists")
2257 xcheckf(err, "creating mailbox")
2260 c.broadcast(changes)
2263 for _, n := range created {
2266 if c.enabled[capIMAP4rev2] && n == name && name != origName && !(name == "Inbox" || strings.HasPrefix(name, "Inbox/")) {
2267 oldname = fmt.Sprintf(` ("OLDNAME" (%s))`, string0(c.encodeMailbox(origName)).pack(c))
2269 c.bwritelinef(`* LIST (\Subscribed) "/" %s%s`, astring(c.encodeMailbox(n)).pack(c), oldname)
2274// Delete removes a mailbox and all its messages.
2275// Inbox cannot be removed.
2277// State: Authenticated and selected.
2278func (c *conn) cmdDelete(tag, cmd string, p *parser) {
2284 name := p.xmailbox()
2287 name = xcheckmailboxname(name, false)
2289 // Messages to remove after having broadcasted the removal of messages.
2290 var removeMessageIDs []int64
2292 c.account.WithWLock(func() {
2293 var mb store.Mailbox
2294 var changes []store.Change
2296 c.xdbwrite(func(tx *bstore.Tx) {
2297 mb = c.xmailbox(tx, name, "NONEXISTENT")
2299 var hasChildren bool
2301 changes, removeMessageIDs, hasChildren, err = c.account.MailboxDelete(context.TODO(), c.log, tx, mb)
2303 xusercodeErrorf("HASCHILDREN", "mailbox has a child, only leaf mailboxes can be deleted")
2305 xcheckf(err, "deleting mailbox")
2308 c.broadcast(changes)
2311 for _, mID := range removeMessageIDs {
2312 p := c.account.MessagePath(mID)
2314 c.log.Check(err, "removing message file for mailbox delete", slog.String("path", p))
2320// Rename changes the name of a mailbox.
2321// Renaming INBOX is special, it moves the inbox messages to a new mailbox, leaving inbox empty.
2322// Renaming a mailbox with submailboxes also renames all submailboxes.
2323// Subscriptions stay with the old name, though newly created missing parent
2324// mailboxes for the destination name are automatically subscribed.
2326// State: Authenticated and selected.
2327func (c *conn) cmdRename(tag, cmd string, p *parser) {
2338 src = xcheckmailboxname(src, true)
2339 dst = xcheckmailboxname(dst, false)
2341 c.account.WithWLock(func() {
2342 var changes []store.Change
2344 c.xdbwrite(func(tx *bstore.Tx) {
2345 srcMB := c.xmailbox(tx, src, "NONEXISTENT")
2347 // Inbox is very special. Unlike other mailboxes, its children are not moved. And
2348 // unlike a regular move, its messages are moved to a newly created mailbox. We do
2349 // indeed create a new destination mailbox and actually move the messages.
2352 exists, err := c.account.MailboxExists(tx, dst)
2353 xcheckf(err, "checking if destination mailbox exists")
2355 xusercodeErrorf("ALREADYEXISTS", "destination mailbox %q already exists", dst)
2358 xuserErrorf("cannot move inbox to itself")
2361 uidval, err := c.account.NextUIDValidity(tx)
2362 xcheckf(err, "next uid validity")
2364 dstMB := store.Mailbox{
2366 UIDValidity: uidval,
2368 Keywords: srcMB.Keywords,
2371 err = tx.Insert(&dstMB)
2372 xcheckf(err, "create new destination mailbox")
2374 modseq, err := c.account.NextModSeq(tx)
2375 xcheckf(err, "assigning next modseq")
2377 changes = make([]store.Change, 2) // Placeholders filled in below.
2379 // Move existing messages, with their ID's and on-disk files intact, to the new
2380 // mailbox. We keep the expunged messages, the destination mailbox doesn't care
2382 var oldUIDs []store.UID
2383 q := bstore.QueryTx[store.Message](tx)
2384 q.FilterNonzero(store.Message{MailboxID: srcMB.ID})
2385 q.FilterEqual("Expunged", false)
2387 err = q.ForEach(func(m store.Message) error {
2392 oldUIDs = append(oldUIDs, om.UID)
2394 mc := m.MailboxCounts()
2398 m.MailboxID = dstMB.ID
2399 m.UID = dstMB.UIDNext
2401 m.CreateSeq = modseq
2403 if err := tx.Update(&m); err != nil {
2404 return fmt.Errorf("updating message to move to new mailbox: %w", err)
2407 changes = append(changes, m.ChangeAddUID())
2409 if err := tx.Insert(&om); err != nil {
2410 return fmt.Errorf("adding empty expunge message record to inbox: %w", err)
2414 xcheckf(err, "moving messages from inbox to destination mailbox")
2416 err = tx.Update(&dstMB)
2417 xcheckf(err, "updating uidnext and counts in destination mailbox")
2419 err = tx.Update(&srcMB)
2420 xcheckf(err, "updating counts for inbox")
2422 var dstFlags []string
2423 if tx.Get(&store.Subscription{Name: dstMB.Name}) == nil {
2424 dstFlags = []string{`\Subscribed`}
2426 changes[0] = store.ChangeRemoveUIDs{MailboxID: srcMB.ID, UIDs: oldUIDs, ModSeq: modseq}
2427 changes[1] = store.ChangeAddMailbox{Mailbox: dstMB, Flags: dstFlags}
2428 // changes[2:...] are ChangeAddUIDs
2429 changes = append(changes, srcMB.ChangeCounts(), dstMB.ChangeCounts())
2433 var notExists, alreadyExists bool
2435 changes, _, notExists, alreadyExists, err = c.account.MailboxRename(tx, srcMB, dst)
2438 xusercodeErrorf("NONEXISTENT", "%s", err)
2439 } else if alreadyExists {
2440 xusercodeErrorf("ALREADYEXISTS", "%s", err)
2442 xcheckf(err, "renaming mailbox")
2444 c.broadcast(changes)
2450// Subscribe marks a mailbox path as subscribed. The mailbox does not have to
2451// exist. Subscribed may mean an email client will show the mailbox in its UI
2452// and/or periodically fetch new messages for the mailbox.
2454// State: Authenticated and selected.
2455func (c *conn) cmdSubscribe(tag, cmd string, p *parser) {
2461 name := p.xmailbox()
2464 name = xcheckmailboxname(name, true)
2466 c.account.WithWLock(func() {
2467 var changes []store.Change
2469 c.xdbwrite(func(tx *bstore.Tx) {
2471 changes, err = c.account.SubscriptionEnsure(tx, name)
2472 xcheckf(err, "ensuring subscription")
2475 c.broadcast(changes)
2481// Unsubscribe marks a mailbox as not subscribed. The mailbox doesn't have to exist.
2483// State: Authenticated and selected.
2484func (c *conn) cmdUnsubscribe(tag, cmd string, p *parser) {
2490 name := p.xmailbox()
2493 name = xcheckmailboxname(name, true)
2495 c.account.WithWLock(func() {
2496 c.xdbwrite(func(tx *bstore.Tx) {
2498 err := tx.Delete(&store.Subscription{Name: name})
2499 if err == bstore.ErrAbsent {
2500 exists, err := c.account.MailboxExists(tx, name)
2501 xcheckf(err, "checking if mailbox exists")
2503 xuserErrorf("mailbox does not exist")
2507 xcheckf(err, "removing subscription")
2510 // todo: can we send untagged message about a mailbox no longer being subscribed?
2516// LSUB command for listing subscribed mailboxes.
2517// Removed in IMAP4rev2, only in IMAP4rev1.
2519// State: Authenticated and selected.
2520func (c *conn) cmdLsub(tag, cmd string, p *parser) {
2528 pattern := p.xlistMailbox()
2531 re := xmailboxPatternMatcher(ref, []string{pattern})
2534 c.xdbread(func(tx *bstore.Tx) {
2535 q := bstore.QueryTx[store.Subscription](tx)
2537 subscriptions, err := q.List()
2538 xcheckf(err, "querying subscriptions")
2540 have := map[string]bool{}
2541 subscribedKids := map[string]bool{}
2542 ispercent := strings.HasSuffix(pattern, "%")
2543 for _, sub := range subscriptions {
2546 for p := path.Dir(name); p != "."; p = path.Dir(p) {
2547 subscribedKids[p] = true
2550 if !re.MatchString(name) {
2554 line := fmt.Sprintf(`* LSUB () "/" %s`, astring(c.encodeMailbox(name)).pack(c))
2555 lines = append(lines, line)
2563 qmb := bstore.QueryTx[store.Mailbox](tx)
2565 err = qmb.ForEach(func(mb store.Mailbox) error {
2566 if have[mb.Name] || !subscribedKids[mb.Name] || !re.MatchString(mb.Name) {
2569 line := fmt.Sprintf(`* LSUB (\NoSelect) "/" %s`, astring(c.encodeMailbox(mb.Name)).pack(c))
2570 lines = append(lines, line)
2573 xcheckf(err, "querying mailboxes")
2577 for _, line := range lines {
2578 c.bwritelinef("%s", line)
2583// The namespace command returns the mailbox path separator. We only implement
2584// the personal mailbox hierarchy, no shared/other.
2586// In IMAP4rev2, it was an extension before.
2588// State: Authenticated and selected.
2589func (c *conn) cmdNamespace(tag, cmd string, p *parser) {
2596 c.bwritelinef(`* NAMESPACE (("" "/")) NIL NIL`)
2600// The status command returns information about a mailbox, such as the number of
2601// messages, "uid validity", etc. Nowadays, the extended LIST command can return
2602// the same information about many mailboxes for one command.
2604// State: Authenticated and selected.
2605func (c *conn) cmdStatus(tag, cmd string, p *parser) {
2611 name := p.xmailbox()
2614 attrs := []string{p.xstatusAtt()}
2617 attrs = append(attrs, p.xstatusAtt())
2621 name = xcheckmailboxname(name, true)
2623 var mb store.Mailbox
2625 var responseLine string
2626 c.account.WithRLock(func() {
2627 c.xdbread(func(tx *bstore.Tx) {
2628 mb = c.xmailbox(tx, name, "")
2629 responseLine = c.xstatusLine(tx, mb, attrs)
2633 c.bwritelinef("%s", responseLine)
2638func (c *conn) xstatusLine(tx *bstore.Tx, mb store.Mailbox, attrs []string) string {
2639 status := []string{}
2640 for _, a := range attrs {
2641 A := strings.ToUpper(a)
2644 status = append(status, A, fmt.Sprintf("%d", mb.Total+mb.Deleted))
2646 status = append(status, A, fmt.Sprintf("%d", mb.UIDNext))
2648 status = append(status, A, fmt.Sprintf("%d", mb.UIDValidity))
2650 status = append(status, A, fmt.Sprintf("%d", mb.Unseen))
2652 status = append(status, A, fmt.Sprintf("%d", mb.Deleted))
2654 status = append(status, A, fmt.Sprintf("%d", mb.Size))
2656 status = append(status, A, "0")
2659 status = append(status, A, "NIL")
2660 case "HIGHESTMODSEQ":
2662 status = append(status, A, fmt.Sprintf("%d", c.xhighestModSeq(tx, mb.ID).Client()))
2663 case "DELETED-STORAGE":
2665 // How much storage space could be reclaimed by expunging messages with the
2666 // \Deleted flag. We could keep track of this number and return it efficiently.
2667 // Calculating it each time can be slow, and we don't know if clients request it.
2668 // Clients are not likely to set the deleted flag without immediately expunging
2669 // nowadays. Let's wait for something to need it to go through the trouble, and
2670 // always return 0 for now.
2671 status = append(status, A, "0")
2673 xsyntaxErrorf("unknown attribute %q", a)
2676 return fmt.Sprintf("* STATUS %s (%s)", astring(c.encodeMailbox(mb.Name)).pack(c), strings.Join(status, " "))
2679func flaglist(fl store.Flags, keywords []string) listspace {
2681 flag := func(v bool, s string) {
2683 l = append(l, bare(s))
2686 flag(fl.Seen, `\Seen`)
2687 flag(fl.Answered, `\Answered`)
2688 flag(fl.Flagged, `\Flagged`)
2689 flag(fl.Deleted, `\Deleted`)
2690 flag(fl.Draft, `\Draft`)
2691 flag(fl.Forwarded, `$Forwarded`)
2692 flag(fl.Junk, `$Junk`)
2693 flag(fl.Notjunk, `$NotJunk`)
2694 flag(fl.Phishing, `$Phishing`)
2695 flag(fl.MDNSent, `$MDNSent`)
2696 for _, k := range keywords {
2697 l = append(l, bare(k))
2702// Append adds a message to a mailbox.
2704// State: Authenticated and selected.
2705func (c *conn) cmdAppend(tag, cmd string, p *parser) {
2711 name := p.xmailbox()
2713 var storeFlags store.Flags
2714 var keywords []string
2715 if p.hasPrefix("(") {
2716 // Error must be a syntax error, to properly abort the connection due to literal.
2718 storeFlags, keywords, err = store.ParseFlagsKeywords(p.xflagList())
2720 xsyntaxErrorf("parsing flags: %v", err)
2725 if p.hasPrefix(`"`) {
2731 // todo: only with utf8 should we we accept message headers with utf-8. we currently always accept them.
2732 // todo: this is only relevant if we also support the CATENATE extension?
2734 utf8 := p.take("UTF8 (")
2735 size, sync := p.xliteralSize(utf8, false)
2737 name = xcheckmailboxname(name, true)
2738 c.xdbread(func(tx *bstore.Tx) {
2739 c.xmailbox(tx, name, "TRYCREATE")
2745 // Read the message into a temporary file.
2746 msgFile, err := store.CreateMessageTemp(c.log, "imap-append")
2747 xcheckf(err, "creating temp file for message")
2750 err := msgFile.Close()
2751 c.xsanity(err, "closing APPEND temporary file")
2753 c.xsanity(err, "removing APPEND temporary file")
2755 defer c.xtrace(mlog.LevelTracedata)()
2756 mw := message.NewWriter(msgFile)
2757 msize, err := io.Copy(mw, io.LimitReader(c.br, size))
2758 c.xtrace(mlog.LevelTrace) // Restore.
2760 // Cannot use xcheckf due to %w handling of errIO.
2761 panic(fmt.Errorf("reading literal message: %s (%w)", err, errIO))
2764 xserverErrorf("read %d bytes for message, expected %d (%w)", msize, size, errIO)
2768 line := c.readline(false)
2769 np := newParser(line, c)
2773 line := c.readline(false)
2774 np := newParser(line, c)
2779 name = xcheckmailboxname(name, true)
2782 var mb store.Mailbox
2784 var pendingChanges []store.Change
2786 c.account.WithWLock(func() {
2787 var changes []store.Change
2788 c.xdbwrite(func(tx *bstore.Tx) {
2789 mb = c.xmailbox(tx, name, "TRYCREATE")
2791 // Ensure keywords are stored in mailbox.
2792 var mbKwChanged bool
2793 mb.Keywords, mbKwChanged = store.MergeKeywords(mb.Keywords, keywords)
2795 changes = append(changes, mb.ChangeKeywords())
2800 MailboxOrigID: mb.ID,
2807 ok, maxSize, err := c.account.CanAddMessageSize(tx, m.Size)
2808 xcheckf(err, "checking quota")
2811 xusercodeErrorf("OVERQUOTA", "account over maximum total message size %d", maxSize)
2814 mb.Add(m.MailboxCounts())
2816 // Update mailbox before delivering, which updates uidnext which we mustn't overwrite.
2817 err = tx.Update(&mb)
2818 xcheckf(err, "updating mailbox counts")
2820 err = c.account.DeliverMessage(c.log, tx, &m, msgFile, true, false, false, true)
2821 xcheckf(err, "delivering message")
2824 // Fetch pending changes, possibly with new UIDs, so we can apply them before adding our own new UID.
2826 pendingChanges = c.comm.Get()
2829 // Broadcast the change to other connections.
2830 changes = append(changes, m.ChangeAddUID(), mb.ChangeCounts())
2831 c.broadcast(changes)
2834 if c.mailboxID == mb.ID {
2835 c.applyChanges(pendingChanges, false)
2837 // 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.
2838 c.bwritelinef("* %d EXISTS", len(c.uids))
2841 c.writeresultf("%s OK [APPENDUID %d %d] appended", tag, mb.UIDValidity, m.UID)
2844// Idle makes a client wait until the server sends untagged updates, e.g. about
2845// message delivery or mailbox create/rename/delete/subscription, etc. It allows a
2846// client to get updates in real-time, not needing the use for NOOP.
2848// State: Authenticated and selected.
2849func (c *conn) cmdIdle(tag, cmd string, p *parser) {
2856 c.writelinef("+ waiting")
2862 case le := <-c.lineChan():
2864 xcheckf(le.err, "get line")
2867 case <-c.comm.Pending:
2868 c.applyChanges(c.comm.Get(), false)
2870 case <-mox.Shutdown.Done():
2872 c.writelinef("* BYE shutting down")
2877 // Reset the write deadline. In case of little activity, with a command timeout of
2878 // 30 minutes, we have likely passed it.
2879 err := c.conn.SetWriteDeadline(time.Now().Add(5 * time.Minute))
2880 c.log.Check(err, "setting write deadline")
2882 if strings.ToUpper(line) != "DONE" {
2883 // We just close the connection because our protocols are out of sync.
2884 panic(fmt.Errorf("%w: in IDLE, expected DONE", errIO))
2890// Return the quota root for a mailbox name and any current quota's.
2892// State: Authenticated and selected.
2893func (c *conn) cmdGetquotaroot(tag, cmd string, p *parser) {
2898 name := p.xmailbox()
2901 // This mailbox does not have to exist. Caller just wants to know which limits
2902 // would apply. We only have one limit, so we don't use the name otherwise.
2904 name = xcheckmailboxname(name, true)
2906 // Get current usage for account.
2907 var quota, size int64 // Account only has a quota if > 0.
2908 c.account.WithRLock(func() {
2909 quota = c.account.QuotaMessageSize()
2911 c.xdbread(func(tx *bstore.Tx) {
2912 du := store.DiskUsage{ID: 1}
2914 xcheckf(err, "gather used quota")
2915 size = du.MessageSize
2920 // We only have one per account quota, we name it "" like the examples in the RFC.
2922 c.bwritelinef(`* QUOTAROOT %s ""`, astring(name).pack(c))
2924 // We only write the quota response if there is a limit. The syntax doesn't allow
2925 // an empty list, so we cannot send the current disk usage if there is no limit.
2928 c.bwritelinef(`* QUOTA "" (STORAGE %d %d)`, (size+1024-1)/1024, (quota+1024-1)/1024)
2933// Return the quota for a quota root.
2935// State: Authenticated and selected.
2936func (c *conn) cmdGetquota(tag, cmd string, p *parser) {
2941 root := p.xastring()
2944 // We only have a per-account root called "".
2946 xuserErrorf("unknown quota root")
2949 var quota, size int64
2950 c.account.WithRLock(func() {
2951 quota = c.account.QuotaMessageSize()
2953 c.xdbread(func(tx *bstore.Tx) {
2954 du := store.DiskUsage{ID: 1}
2956 xcheckf(err, "gather used quota")
2957 size = du.MessageSize
2962 // We only write the quota response if there is a limit. The syntax doesn't allow
2963 // an empty list, so we cannot send the current disk usage if there is no limit.
2966 c.bwritelinef(`* QUOTA "" (STORAGE %d %d)`, (size+1024-1)/1024, (quota+1024-1)/1024)
2971// Check is an old deprecated command that is supposed to execute some mailbox consistency checks.
2974func (c *conn) cmdCheck(tag, cmd string, p *parser) {
2980 c.account.WithRLock(func() {
2981 c.xdbread(func(tx *bstore.Tx) {
2982 c.xmailboxID(tx, c.mailboxID) // Validate.
2989// Close undoes select/examine, closing the currently opened mailbox and deleting
2990// messages that were marked for deletion with the \Deleted flag.
2993func (c *conn) cmdClose(tag, cmd string, p *parser) {
3005 remove, _ := c.xexpunge(nil, true)
3008 for _, m := range remove {
3009 p := c.account.MessagePath(m.ID)
3011 c.xsanity(err, "removing message file for expunge for close")
3019// expunge messages marked for deletion in currently selected/active mailbox.
3020// if uidSet is not nil, only messages matching the set are deleted.
3022// messages that have been marked expunged from the database are returned, but the
3023// corresponding files still have to be removed.
3025// the highest modseq in the mailbox is returned, typically associated with the
3026// removal of the messages, but if no messages were expunged the current latest max
3027// modseq for the mailbox is returned.
3028func (c *conn) xexpunge(uidSet *numSet, missingMailboxOK bool) (remove []store.Message, highestModSeq store.ModSeq) {
3029 var modseq store.ModSeq
3031 c.account.WithWLock(func() {
3032 var mb store.Mailbox
3034 c.xdbwrite(func(tx *bstore.Tx) {
3035 mb = store.Mailbox{ID: c.mailboxID}
3037 if err == bstore.ErrAbsent {
3038 if missingMailboxOK {
3041 xuserErrorf("%w", store.ErrUnknownMailbox)
3044 qm := bstore.QueryTx[store.Message](tx)
3045 qm.FilterNonzero(store.Message{MailboxID: c.mailboxID})
3046 qm.FilterEqual("Deleted", true)
3047 qm.FilterEqual("Expunged", false)
3048 qm.FilterFn(func(m store.Message) bool {
3049 // Only remove if this session knows about the message and if present in optional uidSet.
3050 return uidSearch(c.uids, m.UID) > 0 && (uidSet == nil || uidSet.containsUID(m.UID, c.uids, c.searchResult))
3053 remove, err = qm.List()
3054 xcheckf(err, "listing messages to delete")
3056 if len(remove) == 0 {
3057 highestModSeq = c.xhighestModSeq(tx, c.mailboxID)
3061 // Assign new modseq.
3062 modseq, err = c.account.NextModSeq(tx)
3063 xcheckf(err, "assigning next modseq")
3064 highestModSeq = modseq
3066 removeIDs := make([]int64, len(remove))
3067 anyIDs := make([]any, len(remove))
3069 for i, m := range remove {
3072 mb.Sub(m.MailboxCounts())
3074 // Update "remove", because RetrainMessage below will save the message.
3075 remove[i].Expunged = true
3076 remove[i].ModSeq = modseq
3078 qmr := bstore.QueryTx[store.Recipient](tx)
3079 qmr.FilterEqual("MessageID", anyIDs...)
3080 _, err = qmr.Delete()
3081 xcheckf(err, "removing message recipients")
3083 qm = bstore.QueryTx[store.Message](tx)
3084 qm.FilterIDs(removeIDs)
3085 n, err := qm.UpdateNonzero(store.Message{Expunged: true, ModSeq: modseq})
3086 if err == nil && n != len(removeIDs) {
3087 err = fmt.Errorf("only %d messages set to expunged, expected %d", n, len(removeIDs))
3089 xcheckf(err, "marking messages marked for deleted as expunged")
3091 err = tx.Update(&mb)
3092 xcheckf(err, "updating mailbox counts")
3094 err = c.account.AddMessageSize(c.log, tx, -totalSize)
3095 xcheckf(err, "updating disk usage")
3097 // Mark expunged messages as not needing training, then retrain them, so if they
3098 // were trained, they get untrained.
3099 for i := range remove {
3100 remove[i].Junk = false
3101 remove[i].Notjunk = false
3103 err = c.account.RetrainMessages(context.TODO(), c.log, tx, remove, true)
3104 xcheckf(err, "untraining expunged messages")
3107 // Broadcast changes to other connections. We may not have actually removed any
3108 // messages, so take care not to send an empty update.
3109 if len(remove) > 0 {
3110 ouids := make([]store.UID, len(remove))
3111 for i, m := range remove {
3114 changes := []store.Change{
3115 store.ChangeRemoveUIDs{MailboxID: c.mailboxID, UIDs: ouids, ModSeq: modseq},
3118 c.broadcast(changes)
3121 return remove, highestModSeq
3124// Unselect is similar to close in that it closes the currently active mailbox, but
3125// it does not remove messages marked for deletion.
3128func (c *conn) cmdUnselect(tag, cmd string, p *parser) {
3138// Expunge deletes messages marked with \Deleted in the currently selected mailbox.
3139// Clients are wiser to use UID EXPUNGE because it allows a UID sequence set to
3140// explicitly opt in to removing specific messages.
3143func (c *conn) cmdExpunge(tag, cmd string, p *parser) {
3150 xuserErrorf("mailbox open in read-only mode")
3153 c.cmdxExpunge(tag, cmd, nil)
3156// UID expunge deletes messages marked with \Deleted in the currently selected
3157// mailbox if they match a UID sequence set.
3160func (c *conn) cmdUIDExpunge(tag, cmd string, p *parser) {
3165 uidSet := p.xnumSet()
3169 xuserErrorf("mailbox open in read-only mode")
3172 c.cmdxExpunge(tag, cmd, &uidSet)
3175// Permanently delete messages for the currently selected/active mailbox. If uidset
3176// is not nil, only those UIDs are removed.
3178func (c *conn) cmdxExpunge(tag, cmd string, uidSet *numSet) {
3181 remove, highestModSeq := c.xexpunge(uidSet, false)
3184 for _, m := range remove {
3185 p := c.account.MessagePath(m.ID)
3187 c.xsanity(err, "removing message file for expunge")
3192 var vanishedUIDs numSet
3193 qresync := c.enabled[capQresync]
3194 for _, m := range remove {
3195 seq := c.xsequence(m.UID)
3196 c.sequenceRemove(seq, m.UID)
3198 vanishedUIDs.append(uint32(m.UID))
3200 c.bwritelinef("* %d EXPUNGE", seq)
3203 if !vanishedUIDs.empty() {
3205 for _, s := range vanishedUIDs.Strings(4*1024 - 32) {
3206 c.bwritelinef("* VANISHED %s", s)
3210 if c.enabled[capCondstore] {
3211 c.writeresultf("%s OK [HIGHESTMODSEQ %d] expunged", tag, highestModSeq.Client())
3218func (c *conn) cmdSearch(tag, cmd string, p *parser) {
3219 c.cmdxSearch(false, tag, cmd, p)
3223func (c *conn) cmdUIDSearch(tag, cmd string, p *parser) {
3224 c.cmdxSearch(true, tag, cmd, p)
3228func (c *conn) cmdFetch(tag, cmd string, p *parser) {
3229 c.cmdxFetch(false, tag, cmd, p)
3233func (c *conn) cmdUIDFetch(tag, cmd string, p *parser) {
3234 c.cmdxFetch(true, tag, cmd, p)
3238func (c *conn) cmdStore(tag, cmd string, p *parser) {
3239 c.cmdxStore(false, tag, cmd, p)
3243func (c *conn) cmdUIDStore(tag, cmd string, p *parser) {
3244 c.cmdxStore(true, tag, cmd, p)
3248func (c *conn) cmdCopy(tag, cmd string, p *parser) {
3249 c.cmdxCopy(false, tag, cmd, p)
3253func (c *conn) cmdUIDCopy(tag, cmd string, p *parser) {
3254 c.cmdxCopy(true, tag, cmd, p)
3258func (c *conn) cmdMove(tag, cmd string, p *parser) {
3259 c.cmdxMove(false, tag, cmd, p)
3263func (c *conn) cmdUIDMove(tag, cmd string, p *parser) {
3264 c.cmdxMove(true, tag, cmd, p)
3267func (c *conn) gatherCopyMoveUIDs(isUID bool, nums numSet) ([]store.UID, []any) {
3268 // Gather uids, then sort so we can return a consistently simple and hard to
3269 // misinterpret COPYUID/MOVEUID response. It seems safer to have UIDs in ascending
3270 // order, because requested uid set of 12:10 is equal to 10:12, so if we would just
3271 // echo whatever the client sends us without reordering, the client can reorder our
3272 // response and interpret it differently than we intended.
3274 uids := c.xnumSetUIDs(isUID, nums)
3275 sort.Slice(uids, func(i, j int) bool {
3276 return uids[i] < uids[j]
3278 uidargs := make([]any, len(uids))
3279 for i, uid := range uids {
3282 return uids, uidargs
3285// Copy copies messages from the currently selected/active mailbox to another named
3289func (c *conn) cmdxCopy(isUID bool, tag, cmd string, p *parser) {
3296 name := p.xmailbox()
3299 name = xcheckmailboxname(name, true)
3301 uids, uidargs := c.gatherCopyMoveUIDs(isUID, nums)
3303 // Files that were created during the copy. Remove them if the operation fails.
3304 var createdIDs []int64
3310 for _, id := range createdIDs {
3311 p := c.account.MessagePath(id)
3313 c.xsanity(err, "cleaning up created file")
3318 var mbDst store.Mailbox
3319 var origUIDs, newUIDs []store.UID
3320 var flags []store.Flags
3321 var keywords [][]string
3322 var modseq store.ModSeq // For messages in new mailbox, assigned when first message is copied.
3324 c.account.WithWLock(func() {
3325 var mbKwChanged bool
3327 c.xdbwrite(func(tx *bstore.Tx) {
3328 mbSrc := c.xmailboxID(tx, c.mailboxID) // Validate.
3329 mbDst = c.xmailbox(tx, name, "TRYCREATE")
3330 if mbDst.ID == mbSrc.ID {
3331 xuserErrorf("cannot copy to currently selected mailbox")
3334 if len(uidargs) == 0 {
3335 xuserErrorf("no matching messages to copy")
3339 modseq, err = c.account.NextModSeq(tx)
3340 xcheckf(err, "assigning next modseq")
3342 // Reserve the uids in the destination mailbox.
3343 uidFirst := mbDst.UIDNext
3344 mbDst.UIDNext += store.UID(len(uidargs))
3346 // Fetch messages from database.
3347 q := bstore.QueryTx[store.Message](tx)
3348 q.FilterNonzero(store.Message{MailboxID: c.mailboxID})
3349 q.FilterEqual("UID", uidargs...)
3350 q.FilterEqual("Expunged", false)
3351 xmsgs, err := q.List()
3352 xcheckf(err, "fetching messages")
3354 if len(xmsgs) != len(uidargs) {
3355 xserverErrorf("uid and message mismatch")
3358 // See if quota allows copy.
3360 for _, m := range xmsgs {
3363 if ok, maxSize, err := c.account.CanAddMessageSize(tx, totalSize); err != nil {
3364 xcheckf(err, "checking quota")
3367 xusercodeErrorf("OVERQUOTA", "account over maximum total message size %d", maxSize)
3369 err = c.account.AddMessageSize(c.log, tx, totalSize)
3370 xcheckf(err, "updating disk usage")
3372 msgs := map[store.UID]store.Message{}
3373 for _, m := range xmsgs {
3376 nmsgs := make([]store.Message, len(xmsgs))
3378 conf, _ := c.account.Conf()
3380 mbKeywords := map[string]struct{}{}
3382 // Insert new messages into database.
3383 var origMsgIDs, newMsgIDs []int64
3384 for i, uid := range uids {
3387 xuserErrorf("messages changed, could not fetch requested uid")
3390 origMsgIDs = append(origMsgIDs, origID)
3392 m.UID = uidFirst + store.UID(i)
3393 m.CreateSeq = modseq
3395 m.MailboxID = mbDst.ID
3396 if m.IsReject && m.MailboxDestinedID != 0 {
3397 // Incorrectly delivered to Rejects mailbox. Adjust MailboxOrigID so this message
3398 // is used for reputation calculation during future deliveries.
3399 m.MailboxOrigID = m.MailboxDestinedID
3403 m.JunkFlagsForMailbox(mbDst, conf)
3404 err := tx.Insert(&m)
3405 xcheckf(err, "inserting message")
3408 origUIDs = append(origUIDs, uid)
3409 newUIDs = append(newUIDs, m.UID)
3410 newMsgIDs = append(newMsgIDs, m.ID)
3411 flags = append(flags, m.Flags)
3412 keywords = append(keywords, m.Keywords)
3413 for _, kw := range m.Keywords {
3414 mbKeywords[kw] = struct{}{}
3417 qmr := bstore.QueryTx[store.Recipient](tx)
3418 qmr.FilterNonzero(store.Recipient{MessageID: origID})
3419 mrs, err := qmr.List()
3420 xcheckf(err, "listing message recipients")
3421 for _, mr := range mrs {
3424 err := tx.Insert(&mr)
3425 xcheckf(err, "inserting message recipient")
3428 mbDst.Add(m.MailboxCounts())
3431 mbDst.Keywords, mbKwChanged = store.MergeKeywords(mbDst.Keywords, maps.Keys(mbKeywords))
3433 err = tx.Update(&mbDst)
3434 xcheckf(err, "updating destination mailbox for uids, keywords and counts")
3436 // Copy message files to new message ID's.
3437 syncDirs := map[string]struct{}{}
3438 for i := range origMsgIDs {
3439 src := c.account.MessagePath(origMsgIDs[i])
3440 dst := c.account.MessagePath(newMsgIDs[i])
3441 dstdir := filepath.Dir(dst)
3442 if _, ok := syncDirs[dstdir]; !ok {
3443 os.MkdirAll(dstdir, 0770)
3444 syncDirs[dstdir] = struct{}{}
3446 err := moxio.LinkOrCopy(c.log, dst, src, nil, true)
3447 xcheckf(err, "link or copy file %q to %q", src, dst)
3448 createdIDs = append(createdIDs, newMsgIDs[i])
3451 for dir := range syncDirs {
3452 err := moxio.SyncDir(c.log, dir)
3453 xcheckf(err, "sync directory")
3456 err = c.account.RetrainMessages(context.TODO(), c.log, tx, nmsgs, false)
3457 xcheckf(err, "train copied messages")
3460 // Broadcast changes to other connections.
3461 if len(newUIDs) > 0 {
3462 changes := make([]store.Change, 0, len(newUIDs)+2)
3463 for i, uid := range newUIDs {
3464 changes = append(changes, store.ChangeAddUID{MailboxID: mbDst.ID, UID: uid, ModSeq: modseq, Flags: flags[i], Keywords: keywords[i]})
3466 changes = append(changes, mbDst.ChangeCounts())
3468 changes = append(changes, mbDst.ChangeKeywords())
3470 c.broadcast(changes)
3474 // All good, prevent defer above from cleaning up copied files.
3478 c.writeresultf("%s OK [COPYUID %d %s %s] copied", tag, mbDst.UIDValidity, compactUIDSet(origUIDs).String(), compactUIDSet(newUIDs).String())
3481// Move moves messages from the currently selected/active mailbox to a named mailbox.
3484func (c *conn) cmdxMove(isUID bool, tag, cmd string, p *parser) {
3491 name := p.xmailbox()
3494 name = xcheckmailboxname(name, true)
3497 xuserErrorf("mailbox open in read-only mode")
3500 uids, uidargs := c.gatherCopyMoveUIDs(isUID, nums)
3502 var mbSrc, mbDst store.Mailbox
3503 var changes []store.Change
3504 var newUIDs []store.UID
3505 var modseq store.ModSeq
3507 c.account.WithWLock(func() {
3508 c.xdbwrite(func(tx *bstore.Tx) {
3509 mbSrc = c.xmailboxID(tx, c.mailboxID) // Validate.
3510 mbDst = c.xmailbox(tx, name, "TRYCREATE")
3511 if mbDst.ID == c.mailboxID {
3512 xuserErrorf("cannot move to currently selected mailbox")
3515 if len(uidargs) == 0 {
3516 xuserErrorf("no matching messages to move")
3519 // Reserve the uids in the destination mailbox.
3520 uidFirst := mbDst.UIDNext
3522 mbDst.UIDNext += store.UID(len(uids))
3524 // Assign a new modseq, for the new records and for the expunged records.
3526 modseq, err = c.account.NextModSeq(tx)
3527 xcheckf(err, "assigning next modseq")
3529 // Update existing record with new UID and MailboxID in database for messages. We
3530 // add a new but expunged record again in the original/source mailbox, for qresync.
3531 // Keeping the original ID for the live message means we don't have to move the
3532 // on-disk message contents file.
3533 q := bstore.QueryTx[store.Message](tx)
3534 q.FilterNonzero(store.Message{MailboxID: c.mailboxID})
3535 q.FilterEqual("UID", uidargs...)
3536 q.FilterEqual("Expunged", false)
3538 msgs, err := q.List()
3539 xcheckf(err, "listing messages to move")
3541 if len(msgs) != len(uidargs) {
3542 xserverErrorf("uid and message mismatch")
3545 keywords := map[string]struct{}{}
3547 conf, _ := c.account.Conf()
3548 for i := range msgs {
3550 if m.UID != uids[i] {
3551 xserverErrorf("internal error: got uid %d, expected %d, for index %d", m.UID, uids[i], i)
3554 mbSrc.Sub(m.MailboxCounts())
3556 // Copy of message record that we'll insert when UID is freed up.
3559 om.ID = 0 // Assign new ID.
3562 m.MailboxID = mbDst.ID
3563 if m.IsReject && m.MailboxDestinedID != 0 {
3564 // Incorrectly delivered to Rejects mailbox. Adjust MailboxOrigID so this message
3565 // is used for reputation calculation during future deliveries.
3566 m.MailboxOrigID = m.MailboxDestinedID
3570 mbDst.Add(m.MailboxCounts())
3573 m.JunkFlagsForMailbox(mbDst, conf)
3576 xcheckf(err, "updating moved message in database")
3578 // Now that UID is unused, we can insert the old record again.
3579 err = tx.Insert(&om)
3580 xcheckf(err, "inserting record for expunge after moving message")
3582 for _, kw := range m.Keywords {
3583 keywords[kw] = struct{}{}
3587 // Ensure destination mailbox has keywords of the moved messages.
3588 var mbKwChanged bool
3589 mbDst.Keywords, mbKwChanged = store.MergeKeywords(mbDst.Keywords, maps.Keys(keywords))
3591 changes = append(changes, mbDst.ChangeKeywords())
3594 err = tx.Update(&mbSrc)
3595 xcheckf(err, "updating source mailbox counts")
3597 err = tx.Update(&mbDst)
3598 xcheckf(err, "updating destination mailbox for uids, keywords and counts")
3600 err = c.account.RetrainMessages(context.TODO(), c.log, tx, msgs, false)
3601 xcheckf(err, "retraining messages after move")
3603 // Prepare broadcast changes to other connections.
3604 changes = make([]store.Change, 0, 1+len(msgs)+2)
3605 changes = append(changes, store.ChangeRemoveUIDs{MailboxID: c.mailboxID, UIDs: uids, ModSeq: modseq})
3606 for _, m := range msgs {
3607 newUIDs = append(newUIDs, m.UID)
3608 changes = append(changes, m.ChangeAddUID())
3610 changes = append(changes, mbSrc.ChangeCounts(), mbDst.ChangeCounts())
3613 c.broadcast(changes)
3618 c.bwritelinef("* OK [COPYUID %d %s %s] moved", mbDst.UIDValidity, compactUIDSet(uids).String(), compactUIDSet(newUIDs).String())
3619 qresync := c.enabled[capQresync]
3620 var vanishedUIDs numSet
3621 for i := 0; i < len(uids); i++ {
3622 seq := c.xsequence(uids[i])
3623 c.sequenceRemove(seq, uids[i])
3625 vanishedUIDs.append(uint32(uids[i]))
3627 c.bwritelinef("* %d EXPUNGE", seq)
3630 if !vanishedUIDs.empty() {
3632 for _, s := range vanishedUIDs.Strings(4*1024 - 32) {
3633 c.bwritelinef("* VANISHED %s", s)
3637 if c.enabled[capQresync] {
3639 c.writeresultf("%s OK [HIGHESTMODSEQ %d] move", tag, modseq.Client())
3645// Store sets a full set of flags, or adds/removes specific flags.
3648func (c *conn) cmdxStore(isUID bool, tag, cmd string, p *parser) {
3655 var unchangedSince *int64
3658 p.xtake("UNCHANGEDSINCE")
3665 c.xensureCondstore(nil)
3667 var plus, minus bool
3670 } else if p.take("-") {
3674 silent := p.take(".SILENT")
3676 var flagstrs []string
3677 if p.hasPrefix("(") {
3678 flagstrs = p.xflagList()
3680 flagstrs = append(flagstrs, p.xflag())
3682 flagstrs = append(flagstrs, p.xflag())
3688 xuserErrorf("mailbox open in read-only mode")
3691 flags, keywords, err := store.ParseFlagsKeywords(flagstrs)
3693 xuserErrorf("parsing flags: %v", err)
3695 var mask store.Flags
3697 mask, flags = flags, store.FlagsAll
3699 mask, flags = flags, store.Flags{}
3701 mask = store.FlagsAll
3704 var mb, origmb store.Mailbox
3705 var updated []store.Message
3706 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.
3707 var modseq store.ModSeq // Assigned when needed.
3708 modified := map[int64]bool{}
3710 c.account.WithWLock(func() {
3711 var mbKwChanged bool
3712 var changes []store.Change
3714 c.xdbwrite(func(tx *bstore.Tx) {
3715 mb = c.xmailboxID(tx, c.mailboxID) // Validate.
3718 uidargs := c.xnumSetCondition(isUID, nums)
3720 if len(uidargs) == 0 {
3724 // Ensure keywords are in mailbox.
3726 mb.Keywords, mbKwChanged = store.MergeKeywords(mb.Keywords, keywords)
3728 err := tx.Update(&mb)
3729 xcheckf(err, "updating mailbox with keywords")
3733 q := bstore.QueryTx[store.Message](tx)
3734 q.FilterNonzero(store.Message{MailboxID: c.mailboxID})
3735 q.FilterEqual("UID", uidargs...)
3736 q.FilterEqual("Expunged", false)
3737 err := q.ForEach(func(m store.Message) error {
3738 // Client may specify a message multiple times, but we only process it once.
../rfc/7162:823
3743 mc := m.MailboxCounts()
3745 origFlags := m.Flags
3746 m.Flags = m.Flags.Set(mask, flags)
3747 oldKeywords := append([]string{}, m.Keywords...)
3749 m.Keywords, _ = store.RemoveKeywords(m.Keywords, keywords)
3751 m.Keywords, _ = store.MergeKeywords(m.Keywords, keywords)
3753 m.Keywords = keywords
3756 keywordsChanged := func() bool {
3757 sort.Strings(oldKeywords)
3758 n := append([]string{}, m.Keywords...)
3760 return !slices.Equal(oldKeywords, n)
3763 // If the message has a more recent modseq than the check requires, we won't modify
3764 // it and report in the final command response.
3767 // unchangedSince 0 always fails the check, we don't turn it into 1 like with our
3768 // internal modseqs. RFC implies that is not required for non-system flags, but we
3770 if unchangedSince != nil && m.ModSeq.Client() > *unchangedSince {
3771 changed = append(changed, m)
3776 // It requires that we keep track of the flags we think the client knows (but only
3777 // on this connection). We don't track that. It also isn't clear why this is
3778 // allowed because it is skipping the condstore conditional check, and the new
3779 // combination of flags could be unintended.
3782 if origFlags == m.Flags && !keywordsChanged() {
3783 // Note: since we didn't update the modseq, we are not adding m.ID to "modified",
3784 // it would skip the modseq check above. We still add m to list of updated, so we
3785 // send an untagged fetch response. But we don't broadcast it.
3786 updated = append(updated, m)
3791 mb.Add(m.MailboxCounts())
3793 // Assign new modseq for first actual change.
3796 modseq, err = c.account.NextModSeq(tx)
3797 xcheckf(err, "next modseq")
3800 modified[m.ID] = true
3801 updated = append(updated, m)
3803 changes = append(changes, m.ChangeFlags(origFlags))
3805 return tx.Update(&m)
3807 xcheckf(err, "storing flags in messages")
3809 if mb.MailboxCounts != origmb.MailboxCounts {
3810 err := tx.Update(&mb)
3811 xcheckf(err, "updating mailbox counts")
3813 changes = append(changes, mb.ChangeCounts())
3816 changes = append(changes, mb.ChangeKeywords())
3819 err = c.account.RetrainMessages(context.TODO(), c.log, tx, updated, false)
3820 xcheckf(err, "training messages")
3823 c.broadcast(changes)
3826 // In the RFC, the section about STORE/UID STORE says we must return MODSEQ when
3827 // UNCHANGEDSINCE was specified. It does not specify it in case UNCHANGEDSINCE
3828 // isn't specified. For that case it does say MODSEQ is needed in unsolicited
3829 // untagged fetch responses. Implying that solicited untagged fetch responses
3830 // should not include MODSEQ (why else mention unsolicited explicitly?). But, in
3831 // the introduction to CONDSTORE it does explicitly specify MODSEQ should be
3832 // included in untagged fetch responses at all times with CONDSTORE-enabled
3833 // connections. It would have been better if the command behaviour was specified in
3834 // the command section, not the introduction to the extension.
3837 if !silent || c.enabled[capCondstore] {
3838 for _, m := range updated {
3841 flags = fmt.Sprintf(" FLAGS %s", flaglist(m.Flags, m.Keywords).pack(c))
3843 var modseqStr string
3844 if c.enabled[capCondstore] {
3845 modseqStr = fmt.Sprintf(" MODSEQ (%d)", m.ModSeq.Client())
3848 c.bwritelinef("* %d FETCH (UID %d%s%s)", c.xsequence(m.UID), m.UID, flags, modseqStr)
3852 // We don't explicitly send flags for failed updated with silent set. The regular
3853 // notification will get the flags to the client.
3856 if len(changed) == 0 {
3861 // Write unsolicited untagged fetch responses for messages that didn't pass the
3864 var mnums []store.UID
3865 for _, m := range changed {
3866 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())
3868 mnums = append(mnums, m.UID)
3870 mnums = append(mnums, store.UID(c.xsequence(m.UID)))
3874 sort.Slice(mnums, func(i, j int) bool {
3875 return mnums[i] < mnums[j]
3877 set := compactUIDSet(mnums)
3879 c.writeresultf("%s OK [MODIFIED %s] conditional store did not modify all", tag, set.String())