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.
62 "golang.org/x/exp/maps"
63 "golang.org/x/text/unicode/norm"
65 "github.com/prometheus/client_golang/prometheus"
66 "github.com/prometheus/client_golang/prometheus/promauto"
68 "github.com/mjl-/bstore"
69 "github.com/mjl-/flate"
71 "github.com/mjl-/mox/config"
72 "github.com/mjl-/mox/message"
73 "github.com/mjl-/mox/metrics"
74 "github.com/mjl-/mox/mlog"
75 "github.com/mjl-/mox/mox-"
76 "github.com/mjl-/mox/moxio"
77 "github.com/mjl-/mox/moxvar"
78 "github.com/mjl-/mox/ratelimit"
79 "github.com/mjl-/mox/scram"
80 "github.com/mjl-/mox/store"
84 metricIMAPConnection = promauto.NewCounterVec(
85 prometheus.CounterOpts{
86 Name: "mox_imap_connection_total",
87 Help: "Incoming IMAP connections.",
90 "service", // imap, imaps
93 metricIMAPCommands = promauto.NewHistogramVec(
94 prometheus.HistogramOpts{
95 Name: "mox_imap_command_duration_seconds",
96 Help: "IMAP command duration and result codes in seconds.",
97 Buckets: []float64{0.001, 0.005, 0.01, 0.05, 0.100, 0.5, 1, 5, 10, 20},
101 "result", // ok, panic, ioerror, badsyntax, servererror, usererror, error
106var limiterConnectionrate, limiterConnections *ratelimit.Limiter
109 // Also called by tests, so they don't trigger the rate limiter.
115 limiterConnectionrate = &ratelimit.Limiter{
116 WindowLimits: []ratelimit.WindowLimit{
119 Limits: [...]int64{300, 900, 2700},
123 limiterConnections = &ratelimit.Limiter{
124 WindowLimits: []ratelimit.WindowLimit{
126 Window: time.Duration(math.MaxInt64), // All of time.
127 Limits: [...]int64{30, 90, 270},
133// Delay after bad/suspicious behaviour. Tests set these to zero.
134var badClientDelay = time.Second // Before reads and after 1-byte writes for probably spammers.
135var authFailDelay = time.Second // After authentication failure.
137// Capabilities (extensions) the server supports. Connections will add a few more, e.g. STARTTLS, LOGINDISABLED, AUTH=PLAIN.
168// We always announce support for SCRAM PLUS-variants, also on connections without
169// TLS. The client should not be selecting PLUS variants on non-TLS connections,
170// instead opting to do the bare SCRAM variant without indicating the server claims
171// to support the PLUS variant (skipping the server downgrade detection check).
172const serverCapabilities = "IMAP4rev2 IMAP4rev1 ENABLE LITERAL+ IDLE SASL-IR BINARY UNSELECT UIDPLUS ESEARCH SEARCHRES MOVE UTF8=ACCEPT LIST-EXTENDED SPECIAL-USE CREATE-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 METADATA SAVEDATE WITHIN NAMESPACE COMPRESS=DEFLATE"
178 tls bool // Whether TLS has been initialized.
179 viaHTTPS bool // Whether this connection came in via HTTPS (using TLS ALPN).
180 br *bufio.Reader // From remote, with TLS unwrapped in case of TLS, and possibly wrapping inflate.
181 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.
182 lastLine string // For detecting if syntax error is fatal, i.e. if this ends with a literal. Without crlf.
183 bw *bufio.Writer // To remote, with TLS added in case of TLS, and possibly wrapping deflate, see conn.flateWriter.
184 tr *moxio.TraceReader // Kept to change trace level when reading/writing cmd/auth/data.
185 tw *moxio.TraceWriter
186 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.
187 lastlog time.Time // For printing time since previous log line.
188 baseTLSConfig *tls.Config // Base TLS config to use for handshake.
190 noRequireSTARTTLS bool
191 cmd string // Currently executing, for deciding to applyChanges and logging.
192 cmdMetric string // Currently executing, for metrics.
194 ncmds int // Number of commands processed. Used to abort connection when first incoming command is unknown/invalid.
195 log mlog.Log // Used for all synchronous logging on this connection, see logbg for logging in a separate goroutine.
196 enabled map[capability]bool // All upper-case.
197 compress bool // Whether compression is enabled, via compress command.
198 flateWriter *flate.Writer // For flushing output after flushing conn.bw, and for closing.
199 flateBW *bufio.Writer // Wraps raw connection writes, flateWriter writes here, also needs flushing.
201 // Set by SEARCH with SAVE. Can be used by commands accepting a sequence-set with
202 // value "$". When used, UIDs must be verified to still exist, because they may
203 // have been expunged. Cleared by a SELECT or EXAMINE.
204 // Nil means no searchResult is present. An empty list is a valid searchResult,
205 // just not matching any messages.
207 searchResult []store.UID
209 // Set during authentication, typically picked up by the ID command that
210 // immediately follows, or will be flushed after any other command after
211 // authentication instead.
212 loginAttempt *store.LoginAttempt
214 // Only set when connection has been authenticated. These can be set even when
215 // c.state is stateNotAuthenticated, for TLS client certificate authentication. In
216 // that case, credentials aren't used until the authentication command with the
217 // SASL "EXTERNAL" mechanism.
218 authFailed int // Number of failed auth attempts. For slowing down remote with many failures.
219 noPreauth bool // If set, don't switch connection to "authenticated" after TLS handshake with client certificate authentication.
220 username string // Full username as used during login.
221 account *store.Account
222 comm *store.Comm // For sending/receiving changes on mailboxes in account, e.g. from messages incoming on smtp, or another imap client.
224 mailboxID int64 // Only for StateSelected.
225 readonly bool // If opened mailbox is readonly.
226 uids []store.UID // UIDs known in this session, sorted. todo future: store more space-efficiently, as ranges.
229// capability for use with ENABLED and CAPABILITY. We always keep this upper case,
230// e.g. IMAP4REV2. These values are treated case-insensitive, but it's easier for
231// comparison to just always have the same case.
232type capability string
235 capIMAP4rev2 capability = "IMAP4REV2"
236 capUTF8Accept capability = "UTF8=ACCEPT"
237 capCondstore capability = "CONDSTORE"
238 capQresync capability = "QRESYNC"
239 capMetadata capability = "METADATA"
250 stateNotAuthenticated state = iota
255func stateCommands(cmds ...string) map[string]struct{} {
256 r := map[string]struct{}{}
257 for _, cmd := range cmds {
264 commandsStateAny = stateCommands("capability", "noop", "logout", "id")
265 commandsStateNotAuthenticated = stateCommands("starttls", "authenticate", "login")
266 commandsStateAuthenticated = stateCommands("enable", "select", "examine", "create", "delete", "rename", "subscribe", "unsubscribe", "list", "namespace", "status", "append", "idle", "lsub", "getquotaroot", "getquota", "getmetadata", "setmetadata", "compress")
267 commandsStateSelected = stateCommands("close", "unselect", "expunge", "search", "fetch", "store", "copy", "move", "uid expunge", "uid search", "uid fetch", "uid store", "uid copy", "uid move")
270var commands = map[string]func(c *conn, tag, cmd string, p *parser){
272 "capability": (*conn).cmdCapability,
273 "noop": (*conn).cmdNoop,
274 "logout": (*conn).cmdLogout,
278 "starttls": (*conn).cmdStarttls,
279 "authenticate": (*conn).cmdAuthenticate,
280 "login": (*conn).cmdLogin,
282 // Authenticated and selected.
283 "enable": (*conn).cmdEnable,
284 "select": (*conn).cmdSelect,
285 "examine": (*conn).cmdExamine,
286 "create": (*conn).cmdCreate,
287 "delete": (*conn).cmdDelete,
288 "rename": (*conn).cmdRename,
289 "subscribe": (*conn).cmdSubscribe,
290 "unsubscribe": (*conn).cmdUnsubscribe,
291 "list": (*conn).cmdList,
292 "lsub": (*conn).cmdLsub,
293 "namespace": (*conn).cmdNamespace,
294 "status": (*conn).cmdStatus,
295 "append": (*conn).cmdAppend,
296 "idle": (*conn).cmdIdle,
297 "getquotaroot": (*conn).cmdGetquotaroot,
298 "getquota": (*conn).cmdGetquota,
299 "getmetadata": (*conn).cmdGetmetadata,
300 "setmetadata": (*conn).cmdSetmetadata,
301 "compress": (*conn).cmdCompress,
304 "check": (*conn).cmdCheck,
305 "close": (*conn).cmdClose,
306 "unselect": (*conn).cmdUnselect,
307 "expunge": (*conn).cmdExpunge,
308 "uid expunge": (*conn).cmdUIDExpunge,
309 "search": (*conn).cmdSearch,
310 "uid search": (*conn).cmdUIDSearch,
311 "fetch": (*conn).cmdFetch,
312 "uid fetch": (*conn).cmdUIDFetch,
313 "store": (*conn).cmdStore,
314 "uid store": (*conn).cmdUIDStore,
315 "copy": (*conn).cmdCopy,
316 "uid copy": (*conn).cmdUIDCopy,
317 "move": (*conn).cmdMove,
318 "uid move": (*conn).cmdUIDMove,
321var errIO = errors.New("io error") // For read/write errors and errors that should close the connection.
322var errProtocol = errors.New("protocol error") // For protocol errors for which a stack trace should be printed.
326// check err for sanity.
327// if not nil and checkSanity true (set during tests), then panic. if not nil during normal operation, just log.
328func (c *conn) xsanity(err error, format string, args ...any) {
333 panic(fmt.Errorf("%s: %s", fmt.Sprintf(format, args...), err))
335 c.log.Errorx(fmt.Sprintf(format, args...), err)
340// Listen initializes all imap listeners for the configuration, and stores them for Serve to start them.
342 names := maps.Keys(mox.Conf.Static.Listeners)
344 for _, name := range names {
345 listener := mox.Conf.Static.Listeners[name]
347 var tlsConfig *tls.Config
348 if listener.TLS != nil {
349 tlsConfig = listener.TLS.Config
352 if listener.IMAP.Enabled {
353 port := config.Port(listener.IMAP.Port, 143)
354 for _, ip := range listener.IPs {
355 listen1("imap", name, ip, port, tlsConfig, false, listener.IMAP.NoRequireSTARTTLS)
359 if listener.IMAPS.Enabled {
360 port := config.Port(listener.IMAPS.Port, 993)
361 for _, ip := range listener.IPs {
362 listen1("imaps", name, ip, port, tlsConfig, true, false)
370func listen1(protocol, listenerName, ip string, port int, tlsConfig *tls.Config, xtls, noRequireSTARTTLS bool) {
371 log := mlog.New("imapserver", nil)
372 addr := net.JoinHostPort(ip, fmt.Sprintf("%d", port))
373 if os.Getuid() == 0 {
374 log.Print("listening for imap",
375 slog.String("listener", listenerName),
376 slog.String("addr", addr),
377 slog.String("protocol", protocol))
379 network := mox.Network(ip)
380 ln, err := mox.Listen(network, addr)
382 log.Fatalx("imap: listen for imap", err, slog.String("protocol", protocol), slog.String("listener", listenerName))
385 // Each listener gets its own copy of the config, so session keys between different
386 // ports on same listener aren't shared. We rotate session keys explicitly in this
387 // base TLS config because each connection clones the TLS config before using. The
388 // base TLS config would never get automatically managed/rotated session keys.
389 if tlsConfig != nil {
390 tlsConfig = tlsConfig.Clone()
391 mox.StartTLSSessionTicketKeyRefresher(mox.Shutdown, log, tlsConfig)
396 conn, err := ln.Accept()
398 log.Infox("imap: accept", err, slog.String("protocol", protocol), slog.String("listener", listenerName))
402 metricIMAPConnection.WithLabelValues(protocol).Inc()
403 go serve(listenerName, mox.Cid(), tlsConfig, conn, xtls, noRequireSTARTTLS, false, "")
407 servers = append(servers, serve)
410// ServeTLSConn serves IMAP on a TLS connection.
411func ServeTLSConn(listenerName string, conn *tls.Conn, tlsConfig *tls.Config) {
412 serve(listenerName, mox.Cid(), tlsConfig, conn, true, false, true, "")
415func ServeConnPreauth(listenerName string, cid int64, conn net.Conn, preauthAddress string) {
416 serve(listenerName, cid, nil, conn, false, true, false, preauthAddress)
419// Serve starts serving on all listeners, launching a goroutine per listener.
421 for _, serve := range servers {
427// Logbg returns a logger for logging in the background (in a goroutine), eg for
428// logging LoginAttempts. The regular c.log has a handler that evaluates fields on
429// the connection at time of logging, which may happen at the same time as
430// modifications to those fields.
431func (c *conn) logbg() mlog.Log {
432 log := mlog.New("imapserver", nil).WithCid(c.cid)
433 if c.username != "" {
434 log = log.With(slog.String("username", c.username))
439// returns whether this connection accepts utf-8 in strings.
440func (c *conn) utf8strings() bool {
441 return c.enabled[capIMAP4rev2] || c.enabled[capUTF8Accept]
444func (c *conn) encodeMailbox(s string) string {
451func (c *conn) xdbwrite(fn func(tx *bstore.Tx)) {
452 err := c.account.DB.Write(context.TODO(), func(tx *bstore.Tx) error {
456 xcheckf(err, "transaction")
459func (c *conn) xdbread(fn func(tx *bstore.Tx)) {
460 err := c.account.DB.Read(context.TODO(), func(tx *bstore.Tx) error {
464 xcheckf(err, "transaction")
467// Closes the currently selected/active mailbox, setting state from selected to authenticated.
468// Does not remove messages marked for deletion.
469func (c *conn) unselect() {
470 if c.state == stateSelected {
471 c.state = stateAuthenticated
477func (c *conn) setSlow(on bool) {
479 c.log.Debug("connection changed to slow")
480 } else if !on && c.slow {
481 c.log.Debug("connection restored to regular pace")
486// Write makes a connection an io.Writer. It panics for i/o errors. These errors
487// are handled in the connection command loop.
488func (c *conn) Write(buf []byte) (int, error) {
496 err := c.conn.SetWriteDeadline(time.Now().Add(30 * time.Second))
497 c.log.Check(err, "setting write deadline")
499 nn, err := c.conn.Write(buf[:chunk])
501 panic(fmt.Errorf("write: %s (%w)", err, errIO))
505 if len(buf) > 0 && badClientDelay > 0 {
506 mox.Sleep(mox.Context, badClientDelay)
512func (c *conn) xtrace(level slog.Level) func() {
518 c.tr.SetTrace(mlog.LevelTrace)
519 c.tw.SetTrace(mlog.LevelTrace)
523// Cache of line buffers for reading commands.
525var bufpool = moxio.NewBufpool(8, 16*1024)
527// read line from connection, not going through line channel.
528func (c *conn) readline0() (string, error) {
529 if c.slow && badClientDelay > 0 {
530 mox.Sleep(mox.Context, badClientDelay)
533 d := 30 * time.Minute
534 if c.state == stateNotAuthenticated {
537 err := c.conn.SetReadDeadline(time.Now().Add(d))
538 c.log.Check(err, "setting read deadline")
540 line, err := bufpool.Readline(c.log, c.br)
541 if err != nil && errors.Is(err, moxio.ErrLineTooLong) {
542 return "", fmt.Errorf("%s (%w)", err, errProtocol)
543 } else if err != nil {
544 return "", fmt.Errorf("%s (%w)", err, errIO)
549func (c *conn) lineChan() chan lineErr {
551 c.line = make(chan lineErr, 1)
553 line, err := c.readline0()
554 c.line <- lineErr{line, err}
560// readline from either the c.line channel, or otherwise read from connection.
561func (c *conn) readline(readCmd bool) string {
567 line, err = le.line, le.err
569 line, err = c.readline0()
572 if readCmd && errors.Is(err, os.ErrDeadlineExceeded) {
573 err := c.conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
574 c.log.Check(err, "setting write deadline")
575 c.writelinef("* BYE inactive")
577 if !errors.Is(err, errIO) && !errors.Is(err, errProtocol) {
578 err = fmt.Errorf("%s (%w)", err, errIO)
584 // We typically respond immediately (IDLE is an exception).
585 // The client may not be reading, or may have disappeared.
586 // Don't wait more than 5 minutes before closing down the connection.
587 // The write deadline is managed in IDLE as well.
588 // For unauthenticated connections, we require the client to read faster.
589 wd := 5 * time.Minute
590 if c.state == stateNotAuthenticated {
591 wd = 30 * time.Second
593 err = c.conn.SetWriteDeadline(time.Now().Add(wd))
594 c.log.Check(err, "setting write deadline")
599// write tagged command response, but first write pending changes.
600func (c *conn) writeresultf(format string, args ...any) {
601 c.bwriteresultf(format, args...)
605// write buffered tagged command response, but first write pending changes.
606func (c *conn) bwriteresultf(format string, args ...any) {
608 case "fetch", "store", "search":
612 c.applyChanges(c.comm.Get(), false)
615 c.bwritelinef(format, args...)
618func (c *conn) writelinef(format string, args ...any) {
619 c.bwritelinef(format, args...)
623// Buffer line for write.
624func (c *conn) bwritelinef(format string, args ...any) {
626 fmt.Fprintf(c.bw, format, args...)
629func (c *conn) xflush() {
631 xcheckf(err, "flush") // Should never happen, the Write caused by the Flush should panic on i/o error.
633 // If compression is enabled, we need to flush its stream.
635 // Note: Flush writes a sync message if there is nothing to flush. Ideally we
636 // wouldn't send that, but we would have to keep track of whether data needs to be
638 err := c.flateWriter.Flush()
639 xcheckf(err, "flush deflate")
641 // The flate writer writes to a bufio.Writer, we must also flush that.
642 err = c.flateBW.Flush()
643 xcheckf(err, "flush deflate writer")
647func (c *conn) readCommand(tag *string) (cmd string, p *parser) {
648 line := c.readline(true)
649 p = newParser(line, c)
655 return cmd, newParser(p.remainder(), c)
658func (c *conn) xreadliteral(size int64, sync bool) []byte {
662 buf := make([]byte, size)
664 if err := c.conn.SetReadDeadline(time.Now().Add(30 * time.Second)); err != nil {
665 c.log.Errorx("setting read deadline", err)
668 _, err := io.ReadFull(c.br, buf)
670 // Cannot use xcheckf due to %w handling of errIO.
671 panic(fmt.Errorf("reading literal: %s (%w)", err, errIO))
677func (c *conn) xhighestModSeq(tx *bstore.Tx, mailboxID int64) store.ModSeq {
678 qms := bstore.QueryTx[store.Message](tx)
679 qms.FilterNonzero(store.Message{MailboxID: mailboxID})
680 qms.SortDesc("ModSeq")
683 if err == bstore.ErrAbsent {
684 return store.ModSeq(0)
686 xcheckf(err, "looking up highest modseq for mailbox")
690var cleanClose struct{} // Sentinel value for panic/recover indicating clean close of connection.
692// serve handles a single IMAP connection on nc.
694// If xtls is set, immediate TLS should be enabled on the connection, unless
695// viaHTTP is set, which indicates TLS is already active with the connection coming
696// from the webserver with IMAP chosen through ALPN. activated. If viaHTTP is set,
697// the TLS config ddid not enable client certificate authentication. If xtls is
698// false and tlsConfig is set, STARTTLS may enable TLS later on.
700// If noRequireSTARTTLS is set, TLS is not required for authentication.
702// If accountAddress is not empty, it is the email address of the account to open
705// The connection is closed before returning.
706func serve(listenerName string, cid int64, tlsConfig *tls.Config, nc net.Conn, xtls, noRequireSTARTTLS, viaHTTPS bool, preauthAddress string) {
708 if a, ok := nc.RemoteAddr().(*net.TCPAddr); ok {
711 // For tests and for imapserve.
712 remoteIP = net.ParseIP("127.0.0.10")
721 baseTLSConfig: tlsConfig,
723 noRequireSTARTTLS: noRequireSTARTTLS,
724 enabled: map[capability]bool{},
726 cmdStart: time.Now(),
728 var logmutex sync.Mutex
729 // Also see (and possibly update) c.logbg, for logging in a goroutine.
730 c.log = mlog.New("imapserver", nil).WithFunc(func() []slog.Attr {
732 defer logmutex.Unlock()
735 slog.Int64("cid", c.cid),
736 slog.Duration("delta", now.Sub(c.lastlog)),
739 if c.username != "" {
740 l = append(l, slog.String("username", c.username))
744 c.tr = moxio.NewTraceReader(c.log, "C: ", c.conn)
745 // 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.
746 c.br = bufio.NewReader(c.tr)
747 c.tw = moxio.NewTraceWriter(c.log, "S: ", c)
748 c.bw = bufio.NewWriter(c.tw)
750 // Many IMAP connections use IDLE to wait for new incoming messages. We'll enable
751 // keepalive to get a higher chance of the connection staying alive, or otherwise
752 // detecting broken connections early.
755 tcpconn = nc.(*tls.Conn).NetConn()
757 if tc, ok := tcpconn.(*net.TCPConn); ok {
758 if err := tc.SetKeepAlivePeriod(5 * time.Minute); err != nil {
759 c.log.Errorx("setting keepalive period", err)
760 } else if err := tc.SetKeepAlive(true); err != nil {
761 c.log.Errorx("enabling keepalive", err)
765 c.log.Info("new connection",
766 slog.Any("remote", c.conn.RemoteAddr()),
767 slog.Any("local", c.conn.LocalAddr()),
768 slog.Bool("tls", xtls),
769 slog.Bool("viahttps", viaHTTPS),
770 slog.String("listener", listenerName))
773 err := c.conn.Close()
775 c.log.Debugx("closing connection", err)
778 if c.account != nil {
780 err := c.account.Close()
781 c.xsanity(err, "close account")
787 if x == nil || x == cleanClose {
788 c.log.Info("connection closed")
789 } else if err, ok := x.(error); ok && isClosed(err) {
790 c.log.Infox("connection closed", err)
792 c.log.Error("unhandled panic", slog.Any("err", x))
794 metrics.PanicInc(metrics.Imapserver)
798 if xtls && !viaHTTPS {
799 // Start TLS on connection. We perform the handshake explicitly, so we can set a
800 // timeout, do client certificate authentication, log TLS details afterwards.
801 c.xtlsHandshakeAndAuthenticate(c.conn)
805 case <-mox.Shutdown.Done():
807 c.writelinef("* BYE mox shutting down")
812 if !limiterConnectionrate.Add(c.remoteIP, time.Now(), 1) {
813 c.writelinef("* BYE connection rate from your ip or network too high, slow down please")
817 // If remote IP/network resulted in too many authentication failures, refuse to serve.
818 if !mox.LimiterFailedAuth.CanAdd(c.remoteIP, time.Now(), 1) {
819 metrics.AuthenticationRatelimitedInc("imap")
820 c.log.Debug("refusing connection due to many auth failures", slog.Any("remoteip", c.remoteIP))
821 c.writelinef("* BYE too many auth failures")
825 if !limiterConnections.Add(c.remoteIP, time.Now(), 1) {
826 c.log.Debug("refusing connection due to many open connections", slog.Any("remoteip", c.remoteIP))
827 c.writelinef("* BYE too many open connections from your ip or network")
830 defer limiterConnections.Add(c.remoteIP, time.Now(), -1)
832 // We register and unregister the original connection, in case it c.conn is
833 // replaced with a TLS connection later on.
834 mox.Connections.Register(nc, "imap", listenerName)
835 defer mox.Connections.Unregister(nc)
837 if preauthAddress != "" {
838 acc, _, _, err := store.OpenEmail(c.log, preauthAddress, false)
840 c.log.Debugx("open account for preauth address", err, slog.String("address", preauthAddress))
841 c.writelinef("* BYE open account for address: %s", err)
844 c.username = preauthAddress
846 c.comm = store.RegisterComm(c.account)
849 if c.account != nil && !c.noPreauth {
850 c.state = stateAuthenticated
851 c.writelinef("* PREAUTH [CAPABILITY %s] mox imap welcomes %s", c.capabilities(), c.username)
853 c.writelinef("* OK [CAPABILITY %s] mox imap", c.capabilities())
856 // Ensure any pending loginAttempt is written before we stop.
858 if c.loginAttempt != nil {
859 store.LoginAttemptAdd(context.Background(), c.logbg(), *c.loginAttempt)
864 var storeLoginAttempt bool
867 c.xflush() // For flushing errors, or commands that did not flush explicitly.
869 // After an authentication command, we will have a c.loginAttempt. We typically get
870 // an "ID" command with the user-agent immediately after. So we wait for one more
871 // command after seeing a loginAttempt to gather it.
872 if storeLoginAttempt {
873 storeLoginAttempt = false
874 if c.loginAttempt != nil {
875 store.LoginAttemptAdd(context.Background(), c.logbg(), *c.loginAttempt)
878 } else if c.loginAttempt != nil {
879 storeLoginAttempt = true
884// isClosed returns whether i/o failed, typically because the connection is closed.
885// For connection errors, we often want to generate fewer logs.
886func isClosed(err error) bool {
887 return errors.Is(err, errIO) || errors.Is(err, errProtocol) || moxio.IsClosed(err)
890// newLoginAttempt initializes a c.loginAttempt, for adding to the store after
891// filling in the results and other details.
892func (c *conn) newLoginAttempt(useTLS bool, authMech string) {
893 if c.loginAttempt != nil {
894 store.LoginAttemptAdd(context.Background(), c.logbg(), *c.loginAttempt)
898 var state *tls.ConnectionState
899 if tc, ok := c.conn.(*tls.Conn); ok && useTLS {
900 v := tc.ConnectionState()
904 localAddr := c.conn.LocalAddr().String()
905 localIP, _, _ := net.SplitHostPort(localAddr)
910 c.loginAttempt = &store.LoginAttempt{
911 RemoteIP: c.remoteIP.String(),
913 TLS: store.LoginAttemptTLS(state),
916 Result: store.AuthError, // Replaced by caller.
920// makeTLSConfig makes a new tls config that is bound to the connection for
921// possible client certificate authentication.
922func (c *conn) makeTLSConfig() *tls.Config {
923 // We clone the config so we can set VerifyPeerCertificate below to a method bound
924 // to this connection. Earlier, we set session keys explicitly on the base TLS
925 // config, so they can be used for this connection too.
926 tlsConf := c.baseTLSConfig.Clone()
928 // Allow client certificate authentication, for use with the sasl "external"
929 // authentication mechanism.
930 tlsConf.ClientAuth = tls.RequestClientCert
932 // We verify the client certificate during the handshake. The TLS handshake is
933 // initiated explicitly for incoming connections and during starttls, so we can
934 // immediately extract the account name and address used for authentication.
935 tlsConf.VerifyPeerCertificate = c.tlsClientAuthVerifyPeerCert
940// tlsClientAuthVerifyPeerCert can be used as tls.Config.VerifyPeerCertificate, and
941// sets authentication-related fields on conn. This is not called on resumed TLS
943func (c *conn) tlsClientAuthVerifyPeerCert(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
944 if len(rawCerts) == 0 {
948 // If we had too many authentication failures from this IP, don't attempt
949 // authentication. If this is a new incoming connetion, it is closed after the TLS
951 if !mox.LimiterFailedAuth.CanAdd(c.remoteIP, time.Now(), 1) {
955 cert, err := x509.ParseCertificate(rawCerts[0])
957 c.log.Debugx("parsing tls client certificate", err)
960 if err := c.tlsClientAuthVerifyPeerCertParsed(cert); err != nil {
961 c.log.Debugx("verifying tls client certificate", err)
962 return fmt.Errorf("verifying client certificate: %w", err)
967// tlsClientAuthVerifyPeerCertParsed verifies a client certificate. Called both for
968// fresh and resumed TLS connections.
969func (c *conn) tlsClientAuthVerifyPeerCertParsed(cert *x509.Certificate) error {
970 if c.account != nil {
971 return fmt.Errorf("cannot authenticate with tls client certificate after previous authentication")
974 // todo: it would be nice to postpone storing the loginattempt for tls pubkey auth until we have the ID command. but delaying is complicated because we can't get the tls information in this function. that's why we store the login attempt in a goroutine below, where it can can get a lock when accessing the tls connection only when this function has returned. we can't access c.loginAttempt (we would turn it into a slice) in a goroutine without adding more locking. for now we'll do without user-agent/id for tls pub key auth.
975 c.newLoginAttempt(false, "tlsclientauth")
977 // Get TLS connection state in goroutine because we are called while performing the
978 // TLS handshake, which already has the tls connection locked.
979 conn := c.conn.(*tls.Conn)
980 la := *c.loginAttempt
982 logbg := c.logbg() // Evaluate attributes now, can't do it in goroutine.
985 // In case of panic don't take the whole program down.
988 c.log.Error("recover from panic", slog.Any("panic", x))
990 metrics.PanicInc(metrics.Imapserver)
994 state := conn.ConnectionState()
995 la.TLS = store.LoginAttemptTLS(&state)
996 store.LoginAttemptAdd(context.Background(), logbg, la)
999 if la.Result == store.AuthSuccess {
1000 mox.LimiterFailedAuth.Reset(c.remoteIP, time.Now())
1002 mox.LimiterFailedAuth.Add(c.remoteIP, time.Now(), 1)
1006 // For many failed auth attempts, slow down verification attempts.
1007 if c.authFailed > 3 && authFailDelay > 0 {
1008 mox.Sleep(mox.Context, time.Duration(c.authFailed-3)*authFailDelay)
1010 c.authFailed++ // Compensated on success.
1012 // On the 3rd failed authentication, start responding slowly. Successful auth will
1013 // cause fast responses again.
1014 if c.authFailed >= 3 {
1019 shabuf := sha256.Sum256(cert.RawSubjectPublicKeyInfo)
1020 fp := base64.RawURLEncoding.EncodeToString(shabuf[:])
1021 c.loginAttempt.TLSPubKeyFingerprint = fp
1022 pubKey, err := store.TLSPublicKeyGet(context.TODO(), fp)
1024 if err == bstore.ErrAbsent {
1025 c.loginAttempt.Result = store.AuthBadCredentials
1027 return fmt.Errorf("looking up tls public key with fingerprint %s: %v", fp, err)
1029 c.loginAttempt.LoginAddress = pubKey.LoginAddress
1031 // Verify account exists and still matches address. We don't check for account
1032 // login being disabled if preauth is disabled. In that case, sasl external auth
1033 // will be done before credentials can be used, and login disabled will be checked
1034 // then, where it will result in a more helpful error message.
1035 checkLoginDisabled := !pubKey.NoIMAPPreauth
1036 acc, accName, _, err := store.OpenEmail(c.log, pubKey.LoginAddress, checkLoginDisabled)
1037 c.loginAttempt.AccountName = accName
1039 if errors.Is(err, store.ErrLoginDisabled) {
1040 c.loginAttempt.Result = store.AuthLoginDisabled
1042 // note: we cannot send a more helpful error message to the client.
1043 return fmt.Errorf("opening account for address %s for public key %s: %w", pubKey.LoginAddress, fp, err)
1048 c.xsanity(err, "close account")
1051 c.loginAttempt.AccountName = acc.Name
1052 if acc.Name != pubKey.Account {
1053 return fmt.Errorf("tls client public key %s is for account %s, but email address %s is for account %s", fp, pubKey.Account, pubKey.LoginAddress, acc.Name)
1056 c.loginAttempt.Result = store.AuthSuccess
1059 c.noPreauth = pubKey.NoIMAPPreauth
1061 acc = nil // Prevent cleanup by defer.
1062 c.username = pubKey.LoginAddress
1063 c.comm = store.RegisterComm(c.account)
1064 c.log.Debug("tls client authenticated with client certificate",
1065 slog.String("fingerprint", fp),
1066 slog.String("username", c.username),
1067 slog.String("account", c.account.Name),
1068 slog.Any("remote", c.remoteIP))
1072// xtlsHandshakeAndAuthenticate performs the TLS handshake, and verifies a client
1073// certificate if present.
1074func (c *conn) xtlsHandshakeAndAuthenticate(conn net.Conn) {
1075 tlsConn := tls.Server(conn, c.makeTLSConfig())
1077 c.tr = moxio.NewTraceReader(c.log, "C: ", c.conn)
1078 c.br = bufio.NewReader(c.tr)
1080 cidctx := context.WithValue(mox.Context, mlog.CidKey, c.cid)
1081 ctx, cancel := context.WithTimeout(cidctx, time.Minute)
1083 c.log.Debug("starting tls server handshake")
1084 if err := tlsConn.HandshakeContext(ctx); err != nil {
1085 panic(fmt.Errorf("tls handshake: %s (%w)", err, errIO))
1089 cs := tlsConn.ConnectionState()
1090 if cs.DidResume && len(cs.PeerCertificates) > 0 {
1091 // Verify client after session resumption.
1092 err := c.tlsClientAuthVerifyPeerCertParsed(cs.PeerCertificates[0])
1094 c.bwritelinef("* BYE [ALERT] Error verifying client certificate after TLS session resumption: %s", err)
1095 panic(fmt.Errorf("tls verify client certificate after resumption: %s (%w)", err, errIO))
1099 attrs := []slog.Attr{
1100 slog.Any("version", tlsVersion(cs.Version)),
1101 slog.String("ciphersuite", tls.CipherSuiteName(cs.CipherSuite)),
1102 slog.String("sni", cs.ServerName),
1103 slog.Bool("resumed", cs.DidResume),
1104 slog.Int("clientcerts", len(cs.PeerCertificates)),
1106 if c.account != nil {
1107 attrs = append(attrs,
1108 slog.String("account", c.account.Name),
1109 slog.String("username", c.username),
1112 c.log.Debug("tls handshake completed", attrs...)
1115type tlsVersion uint16
1117func (v tlsVersion) String() string {
1118 return strings.ReplaceAll(strings.ToLower(tls.VersionName(uint16(v))), " ", "-")
1121func (c *conn) command() {
1122 var tag, cmd, cmdlow string
1128 metricIMAPCommands.WithLabelValues(c.cmdMetric, result).Observe(float64(time.Since(c.cmdStart)) / float64(time.Second))
1131 logFields := []slog.Attr{
1132 slog.String("cmd", c.cmd),
1133 slog.Duration("duration", time.Since(c.cmdStart)),
1138 if x == nil || x == cleanClose {
1139 c.log.Debug("imap command done", logFields...)
1141 if x == cleanClose {
1142 // If compression was enabled, we flush & close the deflate stream.
1144 // Note: Close and flush can Write and may panic with an i/o error.
1145 if err := c.flateWriter.Close(); err != nil {
1146 c.log.Debugx("close deflate writer", err)
1147 } else if err := c.flateBW.Flush(); err != nil {
1148 c.log.Debugx("flush deflate buffer", err)
1156 err, ok := x.(error)
1158 c.log.Error("imap command panic", append([]slog.Attr{slog.Any("panic", x)}, logFields...)...)
1163 var sxerr syntaxError
1165 var serr serverError
1167 c.log.Infox("imap command ioerror", err, logFields...)
1169 if errors.Is(err, errProtocol) {
1173 } else if errors.As(err, &sxerr) {
1174 result = "badsyntax"
1176 // Other side is likely speaking something else than IMAP, send error message and
1177 // stop processing because there is a good chance whatever they sent has multiple
1179 c.writelinef("* BYE please try again speaking imap")
1182 c.log.Debugx("imap command syntax error", sxerr.err, logFields...)
1183 c.log.Info("imap syntax error", slog.String("lastline", c.lastLine))
1184 fatal := strings.HasSuffix(c.lastLine, "+}")
1186 err := c.conn.SetWriteDeadline(time.Now().Add(5 * time.Second))
1187 c.log.Check(err, "setting write deadline")
1189 if sxerr.line != "" {
1190 c.bwritelinef("%s", sxerr.line)
1193 if sxerr.code != "" {
1194 code = "[" + sxerr.code + "] "
1196 c.bwriteresultf("%s BAD %s%s unrecognized syntax/command: %v", tag, code, cmd, sxerr.errmsg)
1199 panic(fmt.Errorf("aborting connection after syntax error for command with non-sync literal: %w", errProtocol))
1201 } else if errors.As(err, &serr) {
1202 result = "servererror"
1203 c.log.Errorx("imap command server error", err, logFields...)
1205 c.bwriteresultf("%s NO %s %v", tag, cmd, err)
1206 } else if errors.As(err, &uerr) {
1207 result = "usererror"
1208 c.log.Debugx("imap command user error", err, logFields...)
1209 if uerr.code != "" {
1210 c.bwriteresultf("%s NO [%s] %s %v", tag, uerr.code, cmd, err)
1212 c.bwriteresultf("%s NO %s %v", tag, cmd, err)
1215 // Other type of panic, we pass it on, aborting the connection.
1217 c.log.Errorx("imap command panic", err, logFields...)
1223 cmd, p = c.readCommand(&tag)
1224 cmdlow = strings.ToLower(cmd)
1226 c.cmdStart = time.Now()
1227 c.cmdMetric = "(unrecognized)"
1230 case <-mox.Shutdown.Done():
1232 c.writelinef("* BYE shutting down")
1237 fn := commands[cmdlow]
1239 xsyntaxErrorf("unknown command %q", cmd)
1244 // Check if command is allowed in this state.
1245 if _, ok1 := commandsStateAny[cmdlow]; ok1 {
1246 } else if _, ok2 := commandsStateNotAuthenticated[cmdlow]; ok2 && c.state == stateNotAuthenticated {
1247 } else if _, ok3 := commandsStateAuthenticated[cmdlow]; ok3 && c.state == stateAuthenticated || c.state == stateSelected {
1248 } else if _, ok4 := commandsStateSelected[cmdlow]; ok4 && c.state == stateSelected {
1249 } else if ok1 || ok2 || ok3 || ok4 {
1250 xuserErrorf("not allowed in this connection state")
1252 xserverErrorf("unrecognized command")
1258func (c *conn) broadcast(changes []store.Change) {
1259 if len(changes) == 0 {
1262 c.log.Debug("broadcast changes", slog.Any("changes", changes))
1263 c.comm.Broadcast(changes)
1266// matchStringer matches a string against reference + mailbox patterns.
1267type matchStringer interface {
1268 MatchString(s string) bool
1271type noMatch struct{}
1273// MatchString for noMatch always returns false.
1274func (noMatch) MatchString(s string) bool {
1278// xmailboxPatternMatcher returns a matcher for mailbox names given the reference and patterns.
1279// Patterns can include "%" and "*", matching any character excluding and including a slash respectively.
1280func xmailboxPatternMatcher(ref string, patterns []string) matchStringer {
1281 if strings.HasPrefix(ref, "/") {
1286 for _, pat := range patterns {
1287 if strings.HasPrefix(pat, "/") {
1293 s = path.Join(ref, pat)
1296 // Fix casing for all Inbox paths.
1297 first := strings.SplitN(s, "/", 2)[0]
1298 if strings.EqualFold(first, "Inbox") {
1299 s = "Inbox" + s[len("Inbox"):]
1304 for _, c := range s {
1307 } else if c == '*' {
1310 rs += regexp.QuoteMeta(string(c))
1313 subs = append(subs, rs)
1319 rs := "^(" + strings.Join(subs, "|") + ")$"
1320 re, err := regexp.Compile(rs)
1321 xcheckf(err, "compiling regexp for mailbox patterns")
1325func (c *conn) sequence(uid store.UID) msgseq {
1326 return uidSearch(c.uids, uid)
1329func uidSearch(uids []store.UID, uid store.UID) msgseq {
1336 return msgseq(i + 1)
1346func (c *conn) xsequence(uid store.UID) msgseq {
1347 seq := c.sequence(uid)
1349 xserverErrorf("unknown uid %d (%w)", uid, errProtocol)
1354func (c *conn) sequenceRemove(seq msgseq, uid store.UID) {
1356 if c.uids[i] != uid {
1357 xserverErrorf("got uid %d at msgseq %d, expected uid %d", uid, seq, c.uids[i])
1359 copy(c.uids[i:], c.uids[i+1:])
1360 c.uids = c.uids[:len(c.uids)-1]
1366// add uid to the session. care must be taken that pending changes are fetched
1367// while holding the account wlock, and applied before adding this uid, because
1368// those pending changes may contain another new uid that has to be added first.
1369func (c *conn) uidAppend(uid store.UID) {
1370 if uidSearch(c.uids, uid) > 0 {
1371 xserverErrorf("uid already present (%w)", errProtocol)
1373 if len(c.uids) > 0 && uid < c.uids[len(c.uids)-1] {
1374 xserverErrorf("new uid %d is smaller than last uid %d (%w)", uid, c.uids[len(c.uids)-1], errProtocol)
1376 c.uids = append(c.uids, uid)
1382// sanity check that uids are in ascending order.
1383func checkUIDs(uids []store.UID) {
1384 for i, uid := range uids {
1385 if uid == 0 || i > 0 && uid <= uids[i-1] {
1386 xserverErrorf("bad uids %v", uids)
1391func (c *conn) xnumSetUIDs(isUID bool, nums numSet) []store.UID {
1392 _, uids := c.xnumSetConditionUIDs(false, true, isUID, nums)
1396func (c *conn) xnumSetCondition(isUID bool, nums numSet) []any {
1397 uidargs, _ := c.xnumSetConditionUIDs(true, false, isUID, nums)
1401func (c *conn) xnumSetConditionUIDs(forDB, returnUIDs bool, isUID bool, nums numSet) ([]any, []store.UID) {
1402 if nums.searchResult {
1403 // Update previously stored UIDs. Some may have been deleted.
1404 // Once deleted a UID will never come back, so we'll just remove those uids.
1406 for _, uid := range c.searchResult {
1407 if uidSearch(c.uids, uid) > 0 {
1408 c.searchResult[o] = uid
1412 c.searchResult = c.searchResult[:o]
1413 uidargs := make([]any, len(c.searchResult))
1414 for i, uid := range c.searchResult {
1417 return uidargs, c.searchResult
1421 var uids []store.UID
1423 add := func(uid store.UID) {
1425 uidargs = append(uidargs, uid)
1428 uids = append(uids, uid)
1433 // Sequence numbers that don't exist, or * on an empty mailbox, should result in a BAD response.
../rfc/9051:7018
1434 for _, r := range nums.ranges {
1437 if len(c.uids) == 0 {
1438 xsyntaxErrorf("invalid seqset * on empty mailbox")
1440 ia = len(c.uids) - 1
1442 ia = int(r.first.number - 1)
1443 if ia >= len(c.uids) {
1444 xsyntaxErrorf("msgseq %d not in mailbox", r.first.number)
1453 if len(c.uids) == 0 {
1454 xsyntaxErrorf("invalid seqset * on empty mailbox")
1456 ib = len(c.uids) - 1
1458 ib = int(r.last.number - 1)
1459 if ib >= len(c.uids) {
1460 xsyntaxErrorf("msgseq %d not in mailbox", r.last.number)
1466 for _, uid := range c.uids[ia : ib+1] {
1470 return uidargs, uids
1473 // UIDs that do not exist can be ignored.
1474 if len(c.uids) == 0 {
1478 for _, r := range nums.ranges {
1484 uida := store.UID(r.first.number)
1486 uida = c.uids[len(c.uids)-1]
1489 uidb := store.UID(last.number)
1491 uidb = c.uids[len(c.uids)-1]
1495 uida, uidb = uidb, uida
1498 // Binary search for uida.
1503 if uida < c.uids[m] {
1505 } else if uida > c.uids[m] {
1512 for _, uid := range c.uids[s:] {
1513 if uid >= uida && uid <= uidb {
1515 } else if uid > uidb {
1521 return uidargs, uids
1524func (c *conn) ok(tag, cmd string) {
1525 c.bwriteresultf("%s OK %s done", tag, cmd)
1529// xcheckmailboxname checks if name is valid, returning an INBOX-normalized name.
1530// I.e. it changes various casings of INBOX and INBOX/* to Inbox and Inbox/*.
1531// Name is invalid if it contains leading/trailing/double slashes, or when it isn't
1532// unicode-normalized, or when empty or has special characters.
1533func xcheckmailboxname(name string, allowInbox bool) string {
1534 name, isinbox, err := store.CheckMailboxName(name, allowInbox)
1536 xuserErrorf("special mailboxname Inbox not allowed")
1537 } else if err != nil {
1538 xusercodeErrorf("CANNOT", "%s", err)
1543// Lookup mailbox by name.
1544// If the mailbox does not exist, panic is called with a user error.
1545// Must be called with account rlock held.
1546func (c *conn) xmailbox(tx *bstore.Tx, name string, missingErrCode string) store.Mailbox {
1547 mb, err := c.account.MailboxFind(tx, name)
1548 xcheckf(err, "finding mailbox")
1550 // missingErrCode can be empty, or e.g. TRYCREATE or ALREADYEXISTS.
1551 xusercodeErrorf(missingErrCode, "%w", store.ErrUnknownMailbox)
1556// Lookup mailbox by ID.
1557// If the mailbox does not exist, panic is called with a user error.
1558// Must be called with account rlock held.
1559func (c *conn) xmailboxID(tx *bstore.Tx, id int64) store.Mailbox {
1560 mb := store.Mailbox{ID: id}
1562 if err == bstore.ErrAbsent {
1563 xuserErrorf("%w", store.ErrUnknownMailbox)
1568// Apply changes to our session state.
1569// If initial is false, updates like EXISTS and EXPUNGE are written to the client.
1570// If initial is true, we only apply the changes.
1571// Should not be called while holding locks, as changes are written to client connections, which can block.
1572// Does not flush output.
1573func (c *conn) applyChanges(changes []store.Change, initial bool) {
1574 if len(changes) == 0 {
1578 err := c.conn.SetWriteDeadline(time.Now().Add(5 * time.Minute))
1579 c.log.Check(err, "setting write deadline")
1581 c.log.Debug("applying changes", slog.Any("changes", changes))
1583 // Only keep changes for the selected mailbox, and changes that are always relevant.
1584 var n []store.Change
1585 for _, change := range changes {
1587 switch ch := change.(type) {
1588 case store.ChangeAddUID:
1590 case store.ChangeRemoveUIDs:
1592 case store.ChangeFlags:
1594 case store.ChangeRemoveMailbox, store.ChangeAddMailbox, store.ChangeRenameMailbox, store.ChangeAddSubscription:
1595 n = append(n, change)
1597 case store.ChangeAnnotation:
1598 // note: annotations may have a mailbox associated with them, but we pass all
1601 if c.enabled[capMetadata] {
1602 n = append(n, change)
1605 case store.ChangeMailboxCounts, store.ChangeMailboxSpecialUse, store.ChangeMailboxKeywords, store.ChangeThread:
1607 panic(fmt.Errorf("missing case for %#v", change))
1609 if c.state == stateSelected && mbID == c.mailboxID {
1610 n = append(n, change)
1615 qresync := c.enabled[capQresync]
1616 condstore := c.enabled[capCondstore]
1619 for i < len(changes) {
1620 // First process all new uids. So we only send a single EXISTS.
1621 var adds []store.ChangeAddUID
1622 for ; i < len(changes); i++ {
1623 ch, ok := changes[i].(store.ChangeAddUID)
1627 seq := c.sequence(ch.UID)
1628 if seq > 0 && initial {
1632 adds = append(adds, ch)
1638 // Write the exists, and the UID and flags as well. Hopefully the client waits for
1639 // long enough after the EXISTS to see these messages, and doesn't request them
1640 // again with a FETCH.
1641 c.bwritelinef("* %d EXISTS", len(c.uids))
1642 for _, add := range adds {
1643 seq := c.xsequence(add.UID)
1644 var modseqStr string
1646 modseqStr = fmt.Sprintf(" MODSEQ (%d)", add.ModSeq.Client())
1648 c.bwritelinef("* %d FETCH (UID %d FLAGS %s%s)", seq, add.UID, flaglist(add.Flags, add.Keywords).pack(c), modseqStr)
1653 change := changes[i]
1656 switch ch := change.(type) {
1657 case store.ChangeRemoveUIDs:
1658 var vanishedUIDs numSet
1659 for _, uid := range ch.UIDs {
1662 seq = c.sequence(uid)
1667 seq = c.xsequence(uid)
1669 c.sequenceRemove(seq, uid)
1672 vanishedUIDs.append(uint32(uid))
1674 c.bwritelinef("* %d EXPUNGE", seq)
1680 for _, s := range vanishedUIDs.Strings(4*1024 - 32) {
1681 c.bwritelinef("* VANISHED %s", s)
1684 case store.ChangeFlags:
1685 // The uid can be unknown if we just expunged it while another session marked it as deleted just before.
1686 seq := c.sequence(ch.UID)
1691 var modseqStr string
1693 modseqStr = fmt.Sprintf(" MODSEQ (%d)", ch.ModSeq.Client())
1695 c.bwritelinef("* %d FETCH (UID %d FLAGS %s%s)", seq, ch.UID, flaglist(ch.Flags, ch.Keywords).pack(c), modseqStr)
1697 case store.ChangeRemoveMailbox:
1698 // Only announce \NonExistent to modern clients, otherwise they may ignore the
1699 // unrecognized \NonExistent and interpret this as a newly created mailbox, while
1700 // the goal was to remove it...
1701 if c.enabled[capIMAP4rev2] {
1702 c.bwritelinef(`* LIST (\NonExistent) "/" %s`, astring(c.encodeMailbox(ch.Name)).pack(c))
1704 case store.ChangeAddMailbox:
1705 c.bwritelinef(`* LIST (%s) "/" %s`, strings.Join(ch.Flags, " "), astring(c.encodeMailbox(ch.Mailbox.Name)).pack(c))
1706 case store.ChangeRenameMailbox:
1709 if c.enabled[capIMAP4rev2] {
1710 oldname = fmt.Sprintf(` ("OLDNAME" (%s))`, string0(c.encodeMailbox(ch.OldName)).pack(c))
1712 c.bwritelinef(`* LIST (%s) "/" %s%s`, strings.Join(ch.Flags, " "), astring(c.encodeMailbox(ch.NewName)).pack(c), oldname)
1713 case store.ChangeAddSubscription:
1714 c.bwritelinef(`* LIST (%s) "/" %s`, strings.Join(append([]string{`\Subscribed`}, ch.Flags...), " "), astring(c.encodeMailbox(ch.Name)).pack(c))
1715 case store.ChangeAnnotation:
1717 c.bwritelinef(`* METADATA %s %s`, astring(c.encodeMailbox(ch.MailboxName)).pack(c), astring(ch.Key).pack(c))
1719 panic(fmt.Sprintf("internal error, missing case for %#v", change))
1724// Capability returns the capabilities this server implements and currently has
1725// available given the connection state.
1728func (c *conn) cmdCapability(tag, cmd string, p *parser) {
1734 caps := c.capabilities()
1737 c.bwritelinef("* CAPABILITY %s", caps)
1741// capabilities returns non-empty string with available capabilities based on connection state.
1742// For use in cmdCapability and untagged OK responses on connection start, login and authenticate.
1743func (c *conn) capabilities() string {
1744 caps := serverCapabilities
1746 // We only allow starting without TLS when explicitly configured, in violation of RFC.
1747 if !c.tls && c.baseTLSConfig != nil {
1750 if c.tls || c.noRequireSTARTTLS {
1751 caps += " AUTH=PLAIN"
1753 caps += " LOGINDISABLED"
1755 if c.tls && len(c.conn.(*tls.Conn).ConnectionState().PeerCertificates) > 0 && !c.viaHTTPS {
1756 caps += " AUTH=EXTERNAL"
1761// No op, but useful for retrieving pending changes as untagged responses, e.g. of
1765func (c *conn) cmdNoop(tag, cmd string, p *parser) {
1773// Logout, after which server closes the connection.
1776func (c *conn) cmdLogout(tag, cmd string, p *parser) {
1783 c.state = stateNotAuthenticated
1785 c.bwritelinef("* BYE thanks")
1790// Clients can use ID to tell the server which software they are using. Servers can
1791// respond with their version. For statistics/logging/debugging purposes.
1794func (c *conn) cmdID(tag, cmd string, p *parser) {
1799 var params map[string]string
1802 params = map[string]string{}
1804 if len(params) > 0 {
1810 if _, ok := params[k]; ok {
1811 xsyntaxErrorf("duplicate key %q", k)
1814 values = append(values, fmt.Sprintf("%s=%q", k, v))
1821 // The ID command is typically sent immediately after authentication. So we've
1822 // prepared the LoginAttempt and write it now.
1823 if c.loginAttempt != nil {
1824 c.loginAttempt.UserAgent = strings.Join(values, " ")
1825 store.LoginAttemptAdd(context.Background(), c.logbg(), *c.loginAttempt)
1826 c.loginAttempt = nil
1829 // We just log the client id.
1830 c.log.Info("client id", slog.Any("params", params))
1834 c.bwritelinef(`* ID ("name" "mox" "version" %s)`, string0(moxvar.Version).pack(c))
1838// Compress enables compression on the connection. Deflate is the only algorithm
1839// specified. TLS doesn't do compression nowadays, so we don't have to check for that.
1841// Status: Authenticated. The RFC doesn't mention this in prose, but the command is
1842// added to ABNF production rule "command-auth".
1843func (c *conn) cmdCompress(tag, cmd string, p *parser) {
1851 // Will do compression only once.
1854 xusercodeErrorf("COMPRESSIONACTIVE", "compression already active with previous compress command")
1857 if !strings.EqualFold(alg, "deflate") {
1858 xuserErrorf("compression algorithm not supported")
1861 // We must flush now, before we initialize flate.
1862 c.log.Debug("compression enabled")
1865 c.flateBW = bufio.NewWriter(c)
1866 fw, err := flate.NewWriter(c.flateBW, flate.DefaultCompression)
1867 xcheckf(err, "deflate") // Cannot happen.
1871 c.tw = moxio.NewTraceWriter(c.log, "S: ", c.flateWriter)
1872 c.bw = bufio.NewWriter(c.tw) // The previous c.bw will not have buffered data.
1874 rc := xprefixConn(c.conn, c.br) // c.br may contain buffered data.
1875 // We use the special partial reader. Some clients write commands and flush the
1876 // buffer in "partial flush" mode instead of "sync flush" mode. The "sync flush"
1877 // mode emits an explicit zero-length data block that triggers the Go stdlib flate
1878 // reader to return data to us. It wouldn't for blocks written in "partial flush"
1879 // mode, and it would block us indefinitely while trying to read another flate
1880 // block. The partial reader returns data earlier, but still eagerly consumes all
1881 // blocks in its buffer.
1882 // todo: also _write_ in partial mode since it uses fewer bytes than a sync flush (which needs an additional 4 bytes for the zero-length data block). we need a writer that can flush in partial mode first. writing with sync flush will work with clients that themselves write with partial flush.
1883 fr := flate.NewReaderPartial(rc)
1884 c.tr = moxio.NewTraceReader(c.log, "C: ", fr)
1885 c.br = bufio.NewReader(c.tr)
1888// STARTTLS enables TLS on the connection, after a plain text start.
1889// Only allowed if TLS isn't already enabled, either through connecting to a
1890// TLS-enabled TCP port, or a previous STARTTLS command.
1891// After STARTTLS, plain text authentication typically becomes available.
1893// Status: Not authenticated.
1894func (c *conn) cmdStarttls(tag, cmd string, p *parser) {
1903 if c.baseTLSConfig == nil {
1904 xsyntaxErrorf("starttls not announced")
1907 conn := xprefixConn(c.conn, c.br)
1908 // We add the cid to facilitate debugging in case of TLS connection failure.
1909 c.ok(tag, cmd+" ("+mox.ReceivedID(c.cid)+")")
1911 c.xtlsHandshakeAndAuthenticate(conn)
1914 // We are not sending unsolicited CAPABILITIES for newly available authentication
1915 // mechanisms, clients can't depend on us sending it and should ask it themselves.
1919// Authenticate using SASL. Supports multiple back and forths between client and
1920// server to finish authentication, unlike LOGIN which is just a single
1921// username/password.
1923// We may already have ambient TLS credentials that have not been activated.
1925// Status: Not authenticated.
1926func (c *conn) cmdAuthenticate(tag, cmd string, p *parser) {
1930 // For many failed auth attempts, slow down verification attempts.
1931 if c.authFailed > 3 && authFailDelay > 0 {
1932 mox.Sleep(mox.Context, time.Duration(c.authFailed-3)*authFailDelay)
1935 // If authentication fails due to missing derived secrets, we don't hold it against
1936 // the connection. There is no way to indicate server support for an authentication
1937 // mechanism, but that a mechanism won't work for an account.
1938 var missingDerivedSecrets bool
1940 c.authFailed++ // Compensated on success.
1942 if missingDerivedSecrets {
1945 // On the 3rd failed authentication, start responding slowly. Successful auth will
1946 // cause fast responses again.
1947 if c.authFailed >= 3 {
1952 c.newLoginAttempt(true, "")
1954 if c.loginAttempt.Result == store.AuthSuccess {
1955 mox.LimiterFailedAuth.Reset(c.remoteIP, time.Now())
1956 } else if !missingDerivedSecrets {
1957 mox.LimiterFailedAuth.Add(c.remoteIP, time.Now(), 1)
1963 authType := p.xatom()
1965 xreadInitial := func() []byte {
1969 line = c.readline(false)
1973 line = p.remainder()
1976 line = "" // Base64 decode will result in empty buffer.
1981 c.loginAttempt.Result = store.AuthAborted
1982 xsyntaxErrorf("authenticate aborted by client")
1984 buf, err := base64.StdEncoding.DecodeString(line)
1986 xsyntaxErrorf("parsing base64: %v", err)
1991 xreadContinuation := func() []byte {
1992 line := c.readline(false)
1994 c.loginAttempt.Result = store.AuthAborted
1995 xsyntaxErrorf("authenticate aborted by client")
1997 buf, err := base64.StdEncoding.DecodeString(line)
1999 xsyntaxErrorf("parsing base64: %v", err)
2004 // The various authentication mechanisms set account and username. We may already
2005 // have an account and username from TLS client authentication. Afterwards, we
2006 // check that the account is the same.
2007 var account *store.Account
2011 err := account.Close()
2012 c.xsanity(err, "close account")
2016 switch strings.ToUpper(authType) {
2018 c.loginAttempt.AuthMech = "plain"
2020 if !c.noRequireSTARTTLS && !c.tls {
2022 xusercodeErrorf("PRIVACYREQUIRED", "tls required for login")
2025 // Plain text passwords, mark as traceauth.
2026 defer c.xtrace(mlog.LevelTraceauth)()
2027 buf := xreadInitial()
2028 c.xtrace(mlog.LevelTrace) // Restore.
2029 plain := bytes.Split(buf, []byte{0})
2030 if len(plain) != 3 {
2031 xsyntaxErrorf("bad plain auth data, expected 3 nul-separated tokens, got %d tokens", len(plain))
2033 authz := norm.NFC.String(string(plain[0]))
2034 username = norm.NFC.String(string(plain[1]))
2035 password := string(plain[2])
2036 c.loginAttempt.LoginAddress = username
2038 if authz != "" && authz != username {
2039 xusercodeErrorf("AUTHORIZATIONFAILED", "cannot assume role")
2043 account, c.loginAttempt.AccountName, err = store.OpenEmailAuth(c.log, username, password, false)
2045 if errors.Is(err, store.ErrUnknownCredentials) {
2046 c.loginAttempt.Result = store.AuthBadCredentials
2047 c.log.Info("authentication failed", slog.String("username", username))
2048 xusercodeErrorf("AUTHENTICATIONFAILED", "bad credentials")
2050 xusercodeErrorf("", "error")
2054 c.loginAttempt.AuthMech = strings.ToLower(authType)
2060 chal := fmt.Sprintf("<%d.%d@%s>", uint64(mox.CryptoRandInt()), time.Now().UnixNano(), mox.Conf.Static.HostnameDomain.ASCII)
2061 c.writelinef("+ %s", base64.StdEncoding.EncodeToString([]byte(chal)))
2063 resp := xreadContinuation()
2064 t := strings.Split(string(resp), " ")
2065 if len(t) != 2 || len(t[1]) != 2*md5.Size {
2066 xsyntaxErrorf("malformed cram-md5 response")
2068 username = norm.NFC.String(t[0])
2069 c.loginAttempt.LoginAddress = username
2070 c.log.Debug("cram-md5 auth", slog.String("address", username))
2072 account, c.loginAttempt.AccountName, _, err = store.OpenEmail(c.log, username, false)
2074 if errors.Is(err, store.ErrUnknownCredentials) {
2075 c.loginAttempt.Result = store.AuthBadCredentials
2076 c.log.Info("failed authentication attempt", slog.String("username", username), slog.Any("remote", c.remoteIP))
2077 xusercodeErrorf("AUTHENTICATIONFAILED", "bad credentials")
2079 xserverErrorf("looking up address: %v", err)
2081 var ipadhash, opadhash hash.Hash
2082 account.WithRLock(func() {
2083 err := account.DB.Read(context.TODO(), func(tx *bstore.Tx) error {
2084 password, err := bstore.QueryTx[store.Password](tx).Get()
2085 if err == bstore.ErrAbsent {
2086 c.log.Info("failed authentication attempt", slog.String("username", username), slog.Any("remote", c.remoteIP))
2087 xusercodeErrorf("AUTHENTICATIONFAILED", "bad credentials")
2093 ipadhash = password.CRAMMD5.Ipad
2094 opadhash = password.CRAMMD5.Opad
2097 xcheckf(err, "tx read")
2099 if ipadhash == nil || opadhash == nil {
2100 c.log.Info("cram-md5 auth attempt without derived secrets set, save password again to store secrets", slog.String("username", username))
2101 c.log.Info("failed authentication attempt", slog.String("username", username), slog.Any("remote", c.remoteIP))
2102 missingDerivedSecrets = true
2103 xusercodeErrorf("AUTHENTICATIONFAILED", "bad credentials")
2107 ipadhash.Write([]byte(chal))
2108 opadhash.Write(ipadhash.Sum(nil))
2109 digest := fmt.Sprintf("%x", opadhash.Sum(nil))
2111 c.log.Info("failed authentication attempt", slog.String("username", username), slog.Any("remote", c.remoteIP))
2112 xusercodeErrorf("AUTHENTICATIONFAILED", "bad credentials")
2115 case "SCRAM-SHA-256-PLUS", "SCRAM-SHA-256", "SCRAM-SHA-1-PLUS", "SCRAM-SHA-1":
2116 // 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?
2117 // todo: use single implementation between ../imapserver/server.go and ../smtpserver/server.go
2119 // No plaintext credentials, we can log these normally.
2121 c.loginAttempt.AuthMech = strings.ToLower(authType)
2122 var h func() hash.Hash
2123 switch c.loginAttempt.AuthMech {
2124 case "scram-sha-1", "scram-sha-1-plus":
2126 case "scram-sha-256", "scram-sha-256-plus":
2129 xserverErrorf("missing case for scram variant")
2132 var cs *tls.ConnectionState
2133 requireChannelBinding := strings.HasSuffix(c.loginAttempt.AuthMech, "-plus")
2134 if requireChannelBinding && !c.tls {
2135 xuserErrorf("cannot use plus variant with tls channel binding without tls")
2138 xcs := c.conn.(*tls.Conn).ConnectionState()
2141 c0 := xreadInitial()
2142 ss, err := scram.NewServer(h, c0, cs, requireChannelBinding)
2144 c.log.Infox("scram protocol error", err, slog.Any("remote", c.remoteIP))
2145 xuserErrorf("scram protocol error: %s", err)
2147 username = ss.Authentication
2148 c.loginAttempt.LoginAddress = username
2149 c.log.Debug("scram auth", slog.String("authentication", username))
2150 // We check for login being disabled when finishing.
2151 account, c.loginAttempt.AccountName, _, err = store.OpenEmail(c.log, username, false)
2153 // todo: we could continue scram with a generated salt, deterministically generated
2154 // from the username. that way we don't have to store anything but attackers cannot
2155 // learn if an account exists. same for absent scram saltedpassword below.
2156 xuserErrorf("scram not possible")
2158 if ss.Authorization != "" && ss.Authorization != username {
2159 xuserErrorf("authentication with authorization for different user not supported")
2161 var xscram store.SCRAM
2162 account.WithRLock(func() {
2163 err := account.DB.Read(context.TODO(), func(tx *bstore.Tx) error {
2164 password, err := bstore.QueryTx[store.Password](tx).Get()
2165 if err == bstore.ErrAbsent {
2166 c.log.Info("failed authentication attempt", slog.String("username", username), slog.Any("remote", c.remoteIP))
2167 xusercodeErrorf("AUTHENTICATIONFAILED", "bad credentials")
2169 xcheckf(err, "fetching credentials")
2170 switch c.loginAttempt.AuthMech {
2171 case "scram-sha-1", "scram-sha-1-plus":
2172 xscram = password.SCRAMSHA1
2173 case "scram-sha-256", "scram-sha-256-plus":
2174 xscram = password.SCRAMSHA256
2176 xserverErrorf("missing case for scram credentials")
2178 if len(xscram.Salt) == 0 || xscram.Iterations == 0 || len(xscram.SaltedPassword) == 0 {
2179 missingDerivedSecrets = true
2180 c.log.Info("scram auth attempt without derived secrets set, save password again to store secrets", slog.String("username", username))
2181 xuserErrorf("scram not possible")
2185 xcheckf(err, "read tx")
2187 s1, err := ss.ServerFirst(xscram.Iterations, xscram.Salt)
2188 xcheckf(err, "scram first server step")
2189 c.writelinef("+ %s", base64.StdEncoding.EncodeToString([]byte(s1)))
2190 c2 := xreadContinuation()
2191 s3, err := ss.Finish(c2, xscram.SaltedPassword)
2193 c.writelinef("+ %s", base64.StdEncoding.EncodeToString([]byte(s3)))
2196 c.readline(false) // Should be "*" for cancellation.
2197 if errors.Is(err, scram.ErrInvalidProof) {
2198 c.loginAttempt.Result = store.AuthBadCredentials
2199 c.log.Info("failed authentication attempt", slog.String("username", username), slog.Any("remote", c.remoteIP))
2200 xusercodeErrorf("AUTHENTICATIONFAILED", "bad credentials")
2201 } else if errors.Is(err, scram.ErrChannelBindingsDontMatch) {
2202 c.loginAttempt.Result = store.AuthBadChannelBinding
2203 c.log.Warn("bad channel binding during authentication, potential mitm", slog.String("username", username), slog.Any("remote", c.remoteIP))
2204 xusercodeErrorf("AUTHENTICATIONFAILED", "channel bindings do not match, potential mitm")
2205 } else if errors.Is(err, scram.ErrInvalidEncoding) {
2206 c.loginAttempt.Result = store.AuthBadProtocol
2207 c.log.Infox("bad scram protocol message", err, slog.String("username", username), slog.Any("remote", c.remoteIP))
2208 xuserErrorf("bad scram protocol message: %s", err)
2210 xuserErrorf("server final: %w", err)
2214 // The message should be empty. todo: should we require it is empty?
2218 c.loginAttempt.AuthMech = "external"
2221 buf := xreadInitial()
2222 username = norm.NFC.String(string(buf))
2223 c.loginAttempt.LoginAddress = username
2226 xusercodeErrorf("AUTHENTICATIONFAILED", "tls required for tls client certificate authentication")
2228 if c.account == nil {
2229 xusercodeErrorf("AUTHENTICATIONFAILED", "missing client certificate, required for tls client certificate authentication")
2233 username = c.username
2234 c.loginAttempt.LoginAddress = username
2237 account, c.loginAttempt.AccountName, _, err = store.OpenEmail(c.log, username, false)
2238 xcheckf(err, "looking up username from tls client authentication")
2241 c.loginAttempt.AuthMech = "(unrecognized)"
2242 xuserErrorf("method not supported")
2245 if accConf, ok := account.Conf(); !ok {
2246 xserverErrorf("cannot get account config")
2247 } else if accConf.LoginDisabled != "" {
2248 c.loginAttempt.Result = store.AuthLoginDisabled
2249 c.log.Info("account login disabled", slog.String("username", username))
2250 // No AUTHENTICATIONFAILED code, clients could prompt users for different password.
2251 xuserErrorf("%w: %s", store.ErrLoginDisabled, accConf.LoginDisabled)
2254 // We may already have TLS credentials. They won't have been enabled, or we could
2255 // get here due to the state machine that doesn't allow authentication while being
2256 // authenticated. But allow another SASL authentication, but it has to be for the
2257 // same account. It can be for a different username (email address) of the account.
2258 if c.account != nil {
2259 if account != c.account {
2260 c.log.Debug("sasl authentication for different account than tls client authentication, aborting connection",
2261 slog.String("saslmechanism", c.loginAttempt.AuthMech),
2262 slog.String("saslaccount", account.Name),
2263 slog.String("tlsaccount", c.account.Name),
2264 slog.String("saslusername", username),
2265 slog.String("tlsusername", c.username),
2267 xusercodeErrorf("AUTHENTICATIONFAILED", "authentication failed, tls client certificate public key belongs to another account")
2268 } else if username != c.username {
2269 c.log.Debug("sasl authentication for different username than tls client certificate authentication, switching to sasl username",
2270 slog.String("saslmechanism", c.loginAttempt.AuthMech),
2271 slog.String("saslusername", username),
2272 slog.String("tlsusername", c.username),
2273 slog.String("account", c.account.Name),
2278 account = nil // Prevent cleanup.
2280 c.username = username
2282 c.comm = store.RegisterComm(c.account)
2286 c.loginAttempt.AccountName = c.account.Name
2287 c.loginAttempt.LoginAddress = c.username
2288 c.loginAttempt.Result = store.AuthSuccess
2290 c.state = stateAuthenticated
2291 c.writeresultf("%s OK [CAPABILITY %s] authenticate done", tag, c.capabilities())
2294// Login logs in with username and password.
2296// Status: Not authenticated.
2297func (c *conn) cmdLogin(tag, cmd string, p *parser) {
2300 c.newLoginAttempt(true, "login")
2302 if c.loginAttempt.Result == store.AuthSuccess {
2303 mox.LimiterFailedAuth.Reset(c.remoteIP, time.Now())
2305 mox.LimiterFailedAuth.Add(c.remoteIP, time.Now(), 1)
2309 // 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).
2313 username := norm.NFC.String(p.xastring())
2314 c.loginAttempt.LoginAddress = username
2316 password := p.xastring()
2319 if !c.noRequireSTARTTLS && !c.tls {
2321 xusercodeErrorf("PRIVACYREQUIRED", "tls required for login")
2324 // For many failed auth attempts, slow down verification attempts.
2325 if c.authFailed > 3 && authFailDelay > 0 {
2326 mox.Sleep(mox.Context, time.Duration(c.authFailed-3)*authFailDelay)
2328 c.authFailed++ // Compensated on success.
2330 // On the 3rd failed authentication, start responding slowly. Successful auth will
2331 // cause fast responses again.
2332 if c.authFailed >= 3 {
2337 account, accName, err := store.OpenEmailAuth(c.log, username, password, true)
2338 c.loginAttempt.AccountName = accName
2341 if errors.Is(err, store.ErrUnknownCredentials) {
2342 c.loginAttempt.Result = store.AuthBadCredentials
2343 code = "AUTHENTICATIONFAILED"
2344 c.log.Info("failed authentication attempt", slog.String("username", username), slog.Any("remote", c.remoteIP))
2345 } else if errors.Is(err, store.ErrLoginDisabled) {
2346 c.loginAttempt.Result = store.AuthLoginDisabled
2347 c.log.Info("account login disabled", slog.String("username", username))
2348 // There is no specific code for "account disabled" in IMAP. AUTHORIZATIONFAILED is
2349 // not a good idea, it will prompt users for a password. ALERT seems reasonable,
2350 // but may cause email clients to suppress the message since we are not yet
2352 xuserErrorf("%s", err)
2354 xusercodeErrorf(code, "login failed")
2358 err := account.Close()
2359 c.log.Check(err, "close account")
2363 // We may already have TLS credentials. They won't have been enabled, or we could
2364 // get here due to the state machine that doesn't allow authentication while being
2365 // authenticated. But allow another SASL authentication, but it has to be for the
2366 // same account. It can be for a different username (email address) of the account.
2367 if c.account != nil {
2368 if account != c.account {
2369 c.log.Debug("sasl authentication for different account than tls client authentication, aborting connection",
2370 slog.String("saslmechanism", "login"),
2371 slog.String("saslaccount", account.Name),
2372 slog.String("tlsaccount", c.account.Name),
2373 slog.String("saslusername", username),
2374 slog.String("tlsusername", c.username),
2376 xusercodeErrorf("AUTHENTICATIONFAILED", "authentication failed, tls client certificate public key belongs to another account")
2377 } else if username != c.username {
2378 c.log.Debug("sasl authentication for different username than tls client certificate authentication, switching to sasl username",
2379 slog.String("saslmechanism", "login"),
2380 slog.String("saslusername", username),
2381 slog.String("tlsusername", c.username),
2382 slog.String("account", c.account.Name),
2387 account = nil // Prevent cleanup.
2389 c.username = username
2391 c.comm = store.RegisterComm(c.account)
2393 c.loginAttempt.LoginAddress = c.username
2394 c.loginAttempt.AccountName = c.account.Name
2395 c.loginAttempt.Result = store.AuthSuccess
2398 c.state = stateAuthenticated
2399 c.writeresultf("%s OK [CAPABILITY %s] login done", tag, c.capabilities())
2402// Enable explicitly opts in to an extension. A server can typically send new kinds
2403// of responses to a client. Most extensions do not require an ENABLE because a
2404// client implicitly opts in to new response syntax by making a requests that uses
2405// new optional extension request syntax.
2407// State: Authenticated and selected.
2408func (c *conn) cmdEnable(tag, cmd string, p *parser) {
2414 caps := []string{p.xatom()}
2417 caps = append(caps, p.xatom())
2420 // Clients should only send capabilities that need enabling.
2421 // We should only echo that we recognize as needing enabling.
2424 for _, s := range caps {
2425 cap := capability(strings.ToUpper(s))
2430 c.enabled[cap] = true
2433 c.enabled[cap] = true
2437 c.enabled[cap] = true
2442 if qresync && !c.enabled[capCondstore] {
2443 c.xensureCondstore(nil)
2444 enabled += " CONDSTORE"
2448 c.bwritelinef("* ENABLED%s", enabled)
2453// If a mailbox is selected, an untagged OK with HIGHESTMODSEQ is written to the
2454// client. If tx is non-nil, it is used to read the HIGHESTMODSEQ from the
2455// database. Otherwise a new read-only transaction is created.
2456func (c *conn) xensureCondstore(tx *bstore.Tx) {
2457 if !c.enabled[capCondstore] {
2458 c.enabled[capCondstore] = true
2459 // todo spec: can we send an untagged enabled response?
2461 if c.mailboxID <= 0 {
2464 var modseq store.ModSeq
2466 modseq = c.xhighestModSeq(tx, c.mailboxID)
2468 c.xdbread(func(tx *bstore.Tx) {
2469 modseq = c.xhighestModSeq(tx, c.mailboxID)
2472 c.bwritelinef("* OK [HIGHESTMODSEQ %d] after condstore-enabling command", modseq.Client())
2476// State: Authenticated and selected.
2477func (c *conn) cmdSelect(tag, cmd string, p *parser) {
2478 c.cmdSelectExamine(true, tag, cmd, p)
2481// State: Authenticated and selected.
2482func (c *conn) cmdExamine(tag, cmd string, p *parser) {
2483 c.cmdSelectExamine(false, tag, cmd, p)
2486// Select and examine are almost the same commands. Select just opens a mailbox for
2487// read/write and examine opens a mailbox readonly.
2489// State: Authenticated and selected.
2490func (c *conn) cmdSelectExamine(isselect bool, tag, cmd string, p *parser) {
2498 name := p.xmailbox()
2500 var qruidvalidity uint32
2501 var qrmodseq int64 // QRESYNC required parameters.
2502 var qrknownUIDs, qrknownSeqSet, qrknownUIDSet *numSet // QRESYNC optional parameters.
2504 seen := map[string]bool{}
2506 for len(seen) == 0 || !p.take(")") {
2507 w := p.xtakelist("CONDSTORE", "QRESYNC")
2509 xsyntaxErrorf("duplicate select parameter %s", w)
2519 // Note: unlike with CONDSTORE, there are no QRESYNC-related commands/parameters
2520 // that enable capabilities.
2521 if !c.enabled[capQresync] {
2523 xsyntaxErrorf("QRESYNC must first be enabled")
2529 qrmodseq = p.xnznumber64()
2531 seqMatchData := p.take("(")
2535 seqMatchData = p.take(" (")
2538 ss0 := p.xnumSet0(false, false)
2539 qrknownSeqSet = &ss0
2541 ss1 := p.xnumSet0(false, false)
2542 qrknownUIDSet = &ss1
2548 panic("missing case for select param " + w)
2554 // Deselect before attempting the new select. This means we will deselect when an
2555 // error occurs during select.
2557 if c.state == stateSelected {
2559 c.bwritelinef("* OK [CLOSED] x")
2563 name = xcheckmailboxname(name, true)
2565 var highestModSeq store.ModSeq
2566 var highDeletedModSeq store.ModSeq
2567 var firstUnseen msgseq = 0
2568 var mb store.Mailbox
2569 c.account.WithRLock(func() {
2570 c.xdbread(func(tx *bstore.Tx) {
2571 mb = c.xmailbox(tx, name, "")
2573 q := bstore.QueryTx[store.Message](tx)
2574 q.FilterNonzero(store.Message{MailboxID: mb.ID})
2575 q.FilterEqual("Expunged", false)
2577 c.uids = []store.UID{}
2579 err := q.ForEach(func(m store.Message) error {
2580 c.uids = append(c.uids, m.UID)
2581 if firstUnseen == 0 && !m.Seen {
2590 xcheckf(err, "fetching uids")
2592 // Condstore extension, find the highest modseq.
2593 if c.enabled[capCondstore] {
2594 highestModSeq = c.xhighestModSeq(tx, mb.ID)
2596 // For QRESYNC, we need to know the highest modset of deleted expunged records to
2597 // maintain synchronization.
2598 if c.enabled[capQresync] {
2599 highDeletedModSeq, err = c.account.HighestDeletedModSeq(tx)
2600 xcheckf(err, "getting highest deleted modseq")
2604 c.applyChanges(c.comm.Get(), true)
2607 if len(mb.Keywords) > 0 {
2608 flags = " " + strings.Join(mb.Keywords, " ")
2610 c.bwritelinef(`* FLAGS (\Seen \Answered \Flagged \Deleted \Draft $Forwarded $Junk $NotJunk $Phishing $MDNSent%s)`, flags)
2611 c.bwritelinef(`* OK [PERMANENTFLAGS (\Seen \Answered \Flagged \Deleted \Draft $Forwarded $Junk $NotJunk $Phishing $MDNSent \*)] x`)
2612 if !c.enabled[capIMAP4rev2] {
2613 c.bwritelinef(`* 0 RECENT`)
2615 c.bwritelinef(`* %d EXISTS`, len(c.uids))
2616 if !c.enabled[capIMAP4rev2] && firstUnseen > 0 {
2618 c.bwritelinef(`* OK [UNSEEN %d] x`, firstUnseen)
2620 c.bwritelinef(`* OK [UIDVALIDITY %d] x`, mb.UIDValidity)
2621 c.bwritelinef(`* OK [UIDNEXT %d] x`, mb.UIDNext)
2622 c.bwritelinef(`* LIST () "/" %s`, astring(c.encodeMailbox(mb.Name)).pack(c))
2623 if c.enabled[capCondstore] {
2626 c.bwritelinef(`* OK [HIGHESTMODSEQ %d] x`, highestModSeq.Client())
2630 if qruidvalidity == mb.UIDValidity {
2631 // We send the vanished UIDs at the end, so we can easily combine the modseq
2632 // changes and vanished UIDs that result from that, with the vanished UIDs from the
2633 // case where we don't store enough history.
2634 vanishedUIDs := map[store.UID]struct{}{}
2636 var preVanished store.UID
2637 var oldClientUID store.UID
2638 // If samples of known msgseq and uid pairs are given (they must be in order), we
2639 // use them to determine the earliest UID for which we send VANISHED responses.
2641 if qrknownSeqSet != nil {
2642 if !qrknownSeqSet.isBasicIncreasing() {
2643 xuserErrorf("QRESYNC known message sequence set must be numeric and strictly increasing")
2645 if !qrknownUIDSet.isBasicIncreasing() {
2646 xuserErrorf("QRESYNC known uid set must be numeric and strictly increasing")
2648 seqiter := qrknownSeqSet.newIter()
2649 uiditer := qrknownUIDSet.newIter()
2651 msgseq, ok0 := seqiter.Next()
2652 uid, ok1 := uiditer.Next()
2655 } else if !ok0 || !ok1 {
2656 xsyntaxErrorf("invalid combination of known sequence set and uid set, must be of equal length")
2658 i := int(msgseq - 1)
2659 if i < 0 || i >= len(c.uids) || c.uids[i] != store.UID(uid) {
2660 if uidSearch(c.uids, store.UID(uid)) <= 0 {
2661 // We will check this old client UID for consistency below.
2662 oldClientUID = store.UID(uid)
2666 preVanished = store.UID(uid + 1)
2670 // We gather vanished UIDs and report them at the end. This seems OK because we
2671 // already sent HIGHESTMODSEQ, and a client should know not to commit that value
2672 // until after it has seen the tagged OK of this command. The RFC has a remark
2673 // about ordering of some untagged responses, it's not immediately clear what it
2674 // means, but given the examples appears to allude to servers that decide to not
2675 // send expunge/vanished before the tagged OK.
2678 // We are reading without account lock. Similar to when we process FETCH/SEARCH
2679 // requests. We don't have to reverify existence of the mailbox, so we don't
2680 // rlock, even briefly.
2681 c.xdbread(func(tx *bstore.Tx) {
2682 if oldClientUID > 0 {
2683 // The client sent a UID that is now removed. This is typically fine. But we check
2684 // that it is consistent with the modseq the client sent. If the UID already didn't
2685 // exist at that modseq, the client may be missing some information.
2686 q := bstore.QueryTx[store.Message](tx)
2687 q.FilterNonzero(store.Message{MailboxID: mb.ID, UID: oldClientUID})
2690 // If client claims to be up to date up to and including qrmodseq, and the message
2691 // was deleted at or before that time, we send changes from just before that
2692 // modseq, and we send vanished for all UIDs.
2693 if m.Expunged && qrmodseq >= m.ModSeq.Client() {
2694 qrmodseq = m.ModSeq.Client() - 1
2697 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.")
2699 } else if err != bstore.ErrAbsent {
2700 xcheckf(err, "checking old client uid")
2704 q := bstore.QueryTx[store.Message](tx)
2705 q.FilterNonzero(store.Message{MailboxID: mb.ID})
2706 // Note: we don't filter by Expunged.
2707 q.FilterGreater("ModSeq", store.ModSeqFromClient(qrmodseq))
2708 q.FilterLessEqual("ModSeq", highestModSeq)
2710 err := q.ForEach(func(m store.Message) error {
2711 if m.Expunged && m.UID < preVanished {
2715 if qrknownUIDs != nil && !qrknownUIDs.contains(uint32(m.UID)) {
2719 vanishedUIDs[m.UID] = struct{}{}
2722 msgseq := c.sequence(m.UID)
2724 c.bwritelinef("* %d FETCH (UID %d FLAGS %s MODSEQ (%d))", msgseq, m.UID, flaglist(m.Flags, m.Keywords).pack(c), m.ModSeq.Client())
2728 xcheckf(err, "listing changed messages")
2731 // Add UIDs from client's known UID set to vanished list if we don't have enough history.
2732 if qrmodseq < highDeletedModSeq.Client() {
2733 // If no known uid set was in the request, we substitute 1:max or the empty set.
2735 if qrknownUIDs == nil {
2736 if len(c.uids) > 0 {
2737 qrknownUIDs = &numSet{ranges: []numRange{{first: setNumber{number: 1}, last: &setNumber{number: uint32(c.uids[len(c.uids)-1])}}}}
2739 qrknownUIDs = &numSet{}
2743 iter := qrknownUIDs.newIter()
2745 v, ok := iter.Next()
2749 if c.sequence(store.UID(v)) <= 0 {
2750 vanishedUIDs[store.UID(v)] = struct{}{}
2755 // Now that we have all vanished UIDs, send them over compactly.
2756 if len(vanishedUIDs) > 0 {
2757 l := maps.Keys(vanishedUIDs)
2758 sort.Slice(l, func(i, j int) bool {
2762 for _, s := range compactUIDSet(l).Strings(4*1024 - 32) {
2763 c.bwritelinef("* VANISHED (EARLIER) %s", s)
2769 c.bwriteresultf("%s OK [READ-WRITE] x", tag)
2772 c.bwriteresultf("%s OK [READ-ONLY] x", tag)
2776 c.state = stateSelected
2777 c.searchResult = nil
2781// Create makes a new mailbox, and its parents too if absent.
2783// State: Authenticated and selected.
2784func (c *conn) cmdCreate(tag, cmd string, p *parser) {
2790 name := p.xmailbox()
2792 var useAttrs []string // Special-use attributes without leading \.
2795 // We only support "USE", and there don't appear to be more types of parameters.
2800 useAttrs = append(useAttrs, p.xatom())
2816 name = xcheckmailboxname(name, false)
2818 var specialUse store.SpecialUse
2819 specialUseBools := map[string]*bool{
2820 "archive": &specialUse.Archive,
2821 "drafts": &specialUse.Draft,
2822 "junk": &specialUse.Junk,
2823 "sent": &specialUse.Sent,
2824 "trash": &specialUse.Trash,
2826 for _, s := range useAttrs {
2827 p, ok := specialUseBools[strings.ToLower(s)]
2830 xusercodeErrorf("USEATTR", `cannot create mailbox with special-use attribute \%s`, s)
2835 var changes []store.Change
2836 var created []string // Created mailbox names.
2838 c.account.WithWLock(func() {
2839 c.xdbwrite(func(tx *bstore.Tx) {
2842 changes, created, exists, err = c.account.MailboxCreate(tx, name, specialUse)
2845 xuserErrorf("mailbox already exists")
2847 xcheckf(err, "creating mailbox")
2850 c.broadcast(changes)
2853 for _, n := range created {
2856 if c.enabled[capIMAP4rev2] && n == name && name != origName && !(name == "Inbox" || strings.HasPrefix(name, "Inbox/")) {
2857 oldname = fmt.Sprintf(` ("OLDNAME" (%s))`, string0(c.encodeMailbox(origName)).pack(c))
2859 c.bwritelinef(`* LIST (\Subscribed) "/" %s%s`, astring(c.encodeMailbox(n)).pack(c), oldname)
2864// Delete removes a mailbox and all its messages and annotations.
2865// Inbox cannot be removed.
2867// State: Authenticated and selected.
2868func (c *conn) cmdDelete(tag, cmd string, p *parser) {
2874 name := p.xmailbox()
2877 name = xcheckmailboxname(name, false)
2879 // Messages to remove after having broadcasted the removal of messages.
2880 var removeMessageIDs []int64
2882 c.account.WithWLock(func() {
2883 var mb store.Mailbox
2884 var changes []store.Change
2886 c.xdbwrite(func(tx *bstore.Tx) {
2887 mb = c.xmailbox(tx, name, "NONEXISTENT")
2889 var hasChildren bool
2891 changes, removeMessageIDs, hasChildren, err = c.account.MailboxDelete(context.TODO(), c.log, tx, mb)
2893 xusercodeErrorf("HASCHILDREN", "mailbox has a child, only leaf mailboxes can be deleted")
2895 xcheckf(err, "deleting mailbox")
2898 c.broadcast(changes)
2901 for _, mID := range removeMessageIDs {
2902 p := c.account.MessagePath(mID)
2904 c.log.Check(err, "removing message file for mailbox delete", slog.String("path", p))
2910// Rename changes the name of a mailbox.
2911// Renaming INBOX is special, it moves the inbox messages to a new mailbox, leaving
2912// inbox empty, but copying metadata annotations.
2913// Renaming a mailbox with submailboxes also renames all submailboxes.
2914// Subscriptions stay with the old name, though newly created missing parent
2915// mailboxes for the destination name are automatically subscribed.
2917// State: Authenticated and selected.
2918func (c *conn) cmdRename(tag, cmd string, p *parser) {
2929 src = xcheckmailboxname(src, true)
2930 dst = xcheckmailboxname(dst, false)
2932 c.account.WithWLock(func() {
2933 var changes []store.Change
2935 c.xdbwrite(func(tx *bstore.Tx) {
2936 srcMB := c.xmailbox(tx, src, "NONEXISTENT")
2938 // Inbox is very special. Unlike other mailboxes, its children are not moved. And
2939 // unlike a regular move, its messages are moved to a newly created mailbox. We do
2940 // indeed create a new destination mailbox and actually move the messages.
2943 exists, err := c.account.MailboxExists(tx, dst)
2944 xcheckf(err, "checking if destination mailbox exists")
2946 xusercodeErrorf("ALREADYEXISTS", "destination mailbox %q already exists", dst)
2949 xuserErrorf("cannot move inbox to itself")
2952 uidval, err := c.account.NextUIDValidity(tx)
2953 xcheckf(err, "next uid validity")
2955 dstMB := store.Mailbox{
2957 UIDValidity: uidval,
2959 Keywords: srcMB.Keywords,
2962 err = tx.Insert(&dstMB)
2963 xcheckf(err, "create new destination mailbox")
2965 modseq, err := c.account.NextModSeq(tx)
2966 xcheckf(err, "assigning next modseq")
2968 changes = make([]store.Change, 2) // Placeholders filled in below.
2970 // Move existing messages, with their ID's and on-disk files intact, to the new
2971 // mailbox. We keep the expunged messages, the destination mailbox doesn't care
2973 var oldUIDs []store.UID
2974 q := bstore.QueryTx[store.Message](tx)
2975 q.FilterNonzero(store.Message{MailboxID: srcMB.ID})
2976 q.FilterEqual("Expunged", false)
2978 err = q.ForEach(func(m store.Message) error {
2983 oldUIDs = append(oldUIDs, om.UID)
2985 mc := m.MailboxCounts()
2989 m.MailboxID = dstMB.ID
2990 m.UID = dstMB.UIDNext
2992 m.CreateSeq = modseq
2994 if err := tx.Update(&m); err != nil {
2995 return fmt.Errorf("updating message to move to new mailbox: %w", err)
2998 changes = append(changes, m.ChangeAddUID())
3000 if err := tx.Insert(&om); err != nil {
3001 return fmt.Errorf("adding empty expunge message record to inbox: %w", err)
3005 xcheckf(err, "moving messages from inbox to destination mailbox")
3007 err = tx.Update(&dstMB)
3008 xcheckf(err, "updating uidnext and counts in destination mailbox")
3010 err = tx.Update(&srcMB)
3011 xcheckf(err, "updating counts for inbox")
3013 var dstFlags []string
3014 if tx.Get(&store.Subscription{Name: dstMB.Name}) == nil {
3015 dstFlags = []string{`\Subscribed`}
3019 annotations, err := bstore.QueryTx[store.Annotation](tx).FilterNonzero(store.Annotation{MailboxID: srcMB.ID}).List()
3020 xcheckf(err, "get annotations to copy for inbox")
3021 for i := range annotations {
3022 annotations[i].ID = 0
3023 annotations[i].MailboxID = dstMB.ID
3024 err := tx.Insert(&annotations[i])
3025 xcheckf(err, "copy annotation to destination mailbox")
3028 changes[0] = store.ChangeRemoveUIDs{MailboxID: srcMB.ID, UIDs: oldUIDs, ModSeq: modseq}
3029 changes[1] = store.ChangeAddMailbox{Mailbox: dstMB, Flags: dstFlags}
3030 // changes[2:...] are ChangeAddUIDs
3031 changes = append(changes, srcMB.ChangeCounts(), dstMB.ChangeCounts())
3032 for _, a := range annotations {
3033 changes = append(changes, a.Change(dstMB.Name))
3039 var notExists, alreadyExists bool
3041 changes, _, notExists, alreadyExists, err = c.account.MailboxRename(tx, srcMB, dst)
3044 xusercodeErrorf("NONEXISTENT", "%s", err)
3045 } else if alreadyExists {
3046 xusercodeErrorf("ALREADYEXISTS", "%s", err)
3048 xcheckf(err, "renaming mailbox")
3050 c.broadcast(changes)
3056// Subscribe marks a mailbox path as subscribed. The mailbox does not have to
3057// exist. Subscribed may mean an email client will show the mailbox in its UI
3058// and/or periodically fetch new messages for the mailbox.
3060// State: Authenticated and selected.
3061func (c *conn) cmdSubscribe(tag, cmd string, p *parser) {
3067 name := p.xmailbox()
3070 name = xcheckmailboxname(name, true)
3072 c.account.WithWLock(func() {
3073 var changes []store.Change
3075 c.xdbwrite(func(tx *bstore.Tx) {
3077 changes, err = c.account.SubscriptionEnsure(tx, name)
3078 xcheckf(err, "ensuring subscription")
3081 c.broadcast(changes)
3087// Unsubscribe marks a mailbox as not subscribed. The mailbox doesn't have to exist.
3089// State: Authenticated and selected.
3090func (c *conn) cmdUnsubscribe(tag, cmd string, p *parser) {
3096 name := p.xmailbox()
3099 name = xcheckmailboxname(name, true)
3101 c.account.WithWLock(func() {
3102 c.xdbwrite(func(tx *bstore.Tx) {
3104 err := tx.Delete(&store.Subscription{Name: name})
3105 if err == bstore.ErrAbsent {
3106 exists, err := c.account.MailboxExists(tx, name)
3107 xcheckf(err, "checking if mailbox exists")
3109 xuserErrorf("mailbox does not exist")
3113 xcheckf(err, "removing subscription")
3116 // todo: can we send untagged message about a mailbox no longer being subscribed?
3122// LSUB command for listing subscribed mailboxes.
3123// Removed in IMAP4rev2, only in IMAP4rev1.
3125// State: Authenticated and selected.
3126func (c *conn) cmdLsub(tag, cmd string, p *parser) {
3134 pattern := p.xlistMailbox()
3137 re := xmailboxPatternMatcher(ref, []string{pattern})
3140 c.xdbread(func(tx *bstore.Tx) {
3141 q := bstore.QueryTx[store.Subscription](tx)
3143 subscriptions, err := q.List()
3144 xcheckf(err, "querying subscriptions")
3146 have := map[string]bool{}
3147 subscribedKids := map[string]bool{}
3148 ispercent := strings.HasSuffix(pattern, "%")
3149 for _, sub := range subscriptions {
3152 for p := path.Dir(name); p != "."; p = path.Dir(p) {
3153 subscribedKids[p] = true
3156 if !re.MatchString(name) {
3160 line := fmt.Sprintf(`* LSUB () "/" %s`, astring(c.encodeMailbox(name)).pack(c))
3161 lines = append(lines, line)
3169 qmb := bstore.QueryTx[store.Mailbox](tx)
3171 err = qmb.ForEach(func(mb store.Mailbox) error {
3172 if have[mb.Name] || !subscribedKids[mb.Name] || !re.MatchString(mb.Name) {
3175 line := fmt.Sprintf(`* LSUB (\NoSelect) "/" %s`, astring(c.encodeMailbox(mb.Name)).pack(c))
3176 lines = append(lines, line)
3179 xcheckf(err, "querying mailboxes")
3183 for _, line := range lines {
3184 c.bwritelinef("%s", line)
3189// The namespace command returns the mailbox path separator. We only implement
3190// the personal mailbox hierarchy, no shared/other.
3192// In IMAP4rev2, it was an extension before.
3194// State: Authenticated and selected.
3195func (c *conn) cmdNamespace(tag, cmd string, p *parser) {
3202 c.bwritelinef(`* NAMESPACE (("" "/")) NIL NIL`)
3206// The status command returns information about a mailbox, such as the number of
3207// messages, "uid validity", etc. Nowadays, the extended LIST command can return
3208// the same information about many mailboxes for one command.
3210// State: Authenticated and selected.
3211func (c *conn) cmdStatus(tag, cmd string, p *parser) {
3217 name := p.xmailbox()
3220 attrs := []string{p.xstatusAtt()}
3223 attrs = append(attrs, p.xstatusAtt())
3227 name = xcheckmailboxname(name, true)
3229 var mb store.Mailbox
3231 var responseLine string
3232 c.account.WithRLock(func() {
3233 c.xdbread(func(tx *bstore.Tx) {
3234 mb = c.xmailbox(tx, name, "")
3235 responseLine = c.xstatusLine(tx, mb, attrs)
3239 c.bwritelinef("%s", responseLine)
3244func (c *conn) xstatusLine(tx *bstore.Tx, mb store.Mailbox, attrs []string) string {
3245 status := []string{}
3246 for _, a := range attrs {
3247 A := strings.ToUpper(a)
3250 status = append(status, A, fmt.Sprintf("%d", mb.Total+mb.Deleted))
3252 status = append(status, A, fmt.Sprintf("%d", mb.UIDNext))
3254 status = append(status, A, fmt.Sprintf("%d", mb.UIDValidity))
3256 status = append(status, A, fmt.Sprintf("%d", mb.Unseen))
3258 status = append(status, A, fmt.Sprintf("%d", mb.Deleted))
3260 status = append(status, A, fmt.Sprintf("%d", mb.Size))
3262 status = append(status, A, "0")
3265 status = append(status, A, "NIL")
3266 case "HIGHESTMODSEQ":
3268 status = append(status, A, fmt.Sprintf("%d", c.xhighestModSeq(tx, mb.ID).Client()))
3269 case "DELETED-STORAGE":
3271 // How much storage space could be reclaimed by expunging messages with the
3272 // \Deleted flag. We could keep track of this number and return it efficiently.
3273 // Calculating it each time can be slow, and we don't know if clients request it.
3274 // Clients are not likely to set the deleted flag without immediately expunging
3275 // nowadays. Let's wait for something to need it to go through the trouble, and
3276 // always return 0 for now.
3277 status = append(status, A, "0")
3279 xsyntaxErrorf("unknown attribute %q", a)
3282 return fmt.Sprintf("* STATUS %s (%s)", astring(c.encodeMailbox(mb.Name)).pack(c), strings.Join(status, " "))
3285func flaglist(fl store.Flags, keywords []string) listspace {
3287 flag := func(v bool, s string) {
3289 l = append(l, bare(s))
3292 flag(fl.Seen, `\Seen`)
3293 flag(fl.Answered, `\Answered`)
3294 flag(fl.Flagged, `\Flagged`)
3295 flag(fl.Deleted, `\Deleted`)
3296 flag(fl.Draft, `\Draft`)
3297 flag(fl.Forwarded, `$Forwarded`)
3298 flag(fl.Junk, `$Junk`)
3299 flag(fl.Notjunk, `$NotJunk`)
3300 flag(fl.Phishing, `$Phishing`)
3301 flag(fl.MDNSent, `$MDNSent`)
3302 for _, k := range keywords {
3303 l = append(l, bare(k))
3308// Append adds a message to a mailbox.
3310// State: Authenticated and selected.
3311func (c *conn) cmdAppend(tag, cmd string, p *parser) {
3317 name := p.xmailbox()
3319 var storeFlags store.Flags
3320 var keywords []string
3321 if p.hasPrefix("(") {
3322 // Error must be a syntax error, to properly abort the connection due to literal.
3324 storeFlags, keywords, err = store.ParseFlagsKeywords(p.xflagList())
3326 xsyntaxErrorf("parsing flags: %v", err)
3331 if p.hasPrefix(`"`) {
3337 // todo: only with utf8 should we we accept message headers with utf-8. we currently always accept them.
3338 // todo: this is only relevant if we also support the CATENATE extension?
3340 utf8 := p.take("UTF8 (")
3341 size, sync := p.xliteralSize(utf8, false)
3343 name = xcheckmailboxname(name, true)
3344 c.xdbread(func(tx *bstore.Tx) {
3345 c.xmailbox(tx, name, "TRYCREATE")
3351 // Read the message into a temporary file.
3352 msgFile, err := store.CreateMessageTemp(c.log, "imap-append")
3353 xcheckf(err, "creating temp file for message")
3356 err := msgFile.Close()
3357 c.xsanity(err, "closing APPEND temporary file")
3359 c.xsanity(err, "removing APPEND temporary file")
3361 defer c.xtrace(mlog.LevelTracedata)()
3362 mw := message.NewWriter(msgFile)
3363 msize, err := io.Copy(mw, io.LimitReader(c.br, size))
3364 c.xtrace(mlog.LevelTrace) // Restore.
3366 // Cannot use xcheckf due to %w handling of errIO.
3367 panic(fmt.Errorf("reading literal message: %s (%w)", err, errIO))
3370 xserverErrorf("read %d bytes for message, expected %d (%w)", msize, size, errIO)
3374 line := c.readline(false)
3375 np := newParser(line, c)
3379 line := c.readline(false)
3380 np := newParser(line, c)
3385 name = xcheckmailboxname(name, true)
3388 var mb store.Mailbox
3390 var pendingChanges []store.Change
3392 c.account.WithWLock(func() {
3393 var changes []store.Change
3394 c.xdbwrite(func(tx *bstore.Tx) {
3395 mb = c.xmailbox(tx, name, "TRYCREATE")
3397 // Ensure keywords are stored in mailbox.
3398 var mbKwChanged bool
3399 mb.Keywords, mbKwChanged = store.MergeKeywords(mb.Keywords, keywords)
3401 changes = append(changes, mb.ChangeKeywords())
3406 MailboxOrigID: mb.ID,
3413 ok, maxSize, err := c.account.CanAddMessageSize(tx, m.Size)
3414 xcheckf(err, "checking quota")
3417 xusercodeErrorf("OVERQUOTA", "account over maximum total message size %d", maxSize)
3420 mb.Add(m.MailboxCounts())
3422 // Update mailbox before delivering, which updates uidnext which we mustn't overwrite.
3423 err = tx.Update(&mb)
3424 xcheckf(err, "updating mailbox counts")
3426 err = c.account.DeliverMessage(c.log, tx, &m, msgFile, true, false, false, true)
3427 xcheckf(err, "delivering message")
3430 // Fetch pending changes, possibly with new UIDs, so we can apply them before adding our own new UID.
3432 pendingChanges = c.comm.Get()
3435 // Broadcast the change to other connections.
3436 changes = append(changes, m.ChangeAddUID(), mb.ChangeCounts())
3437 c.broadcast(changes)
3440 if c.mailboxID == mb.ID {
3441 c.applyChanges(pendingChanges, false)
3443 // 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.
3444 c.bwritelinef("* %d EXISTS", len(c.uids))
3447 c.writeresultf("%s OK [APPENDUID %d %d] appended", tag, mb.UIDValidity, m.UID)
3450// Idle makes a client wait until the server sends untagged updates, e.g. about
3451// message delivery or mailbox create/rename/delete/subscription, etc. It allows a
3452// client to get updates in real-time, not needing the use for NOOP.
3454// State: Authenticated and selected.
3455func (c *conn) cmdIdle(tag, cmd string, p *parser) {
3462 c.writelinef("+ waiting")
3468 case le := <-c.lineChan():
3470 xcheckf(le.err, "get line")
3473 case <-c.comm.Pending:
3474 c.applyChanges(c.comm.Get(), false)
3476 case <-mox.Shutdown.Done():
3478 c.writelinef("* BYE shutting down")
3483 // Reset the write deadline. In case of little activity, with a command timeout of
3484 // 30 minutes, we have likely passed it.
3485 err := c.conn.SetWriteDeadline(time.Now().Add(5 * time.Minute))
3486 c.log.Check(err, "setting write deadline")
3488 if strings.ToUpper(line) != "DONE" {
3489 // We just close the connection because our protocols are out of sync.
3490 panic(fmt.Errorf("%w: in IDLE, expected DONE", errIO))
3496// Return the quota root for a mailbox name and any current quota's.
3498// State: Authenticated and selected.
3499func (c *conn) cmdGetquotaroot(tag, cmd string, p *parser) {
3504 name := p.xmailbox()
3507 // This mailbox does not have to exist. Caller just wants to know which limits
3508 // would apply. We only have one limit, so we don't use the name otherwise.
3510 name = xcheckmailboxname(name, true)
3512 // Get current usage for account.
3513 var quota, size int64 // Account only has a quota if > 0.
3514 c.account.WithRLock(func() {
3515 quota = c.account.QuotaMessageSize()
3517 c.xdbread(func(tx *bstore.Tx) {
3518 du := store.DiskUsage{ID: 1}
3520 xcheckf(err, "gather used quota")
3521 size = du.MessageSize
3526 // We only have one per account quota, we name it "" like the examples in the RFC.
3528 c.bwritelinef(`* QUOTAROOT %s ""`, astring(name).pack(c))
3530 // We only write the quota response if there is a limit. The syntax doesn't allow
3531 // an empty list, so we cannot send the current disk usage if there is no limit.
3534 c.bwritelinef(`* QUOTA "" (STORAGE %d %d)`, (size+1024-1)/1024, (quota+1024-1)/1024)
3539// Return the quota for a quota root.
3541// State: Authenticated and selected.
3542func (c *conn) cmdGetquota(tag, cmd string, p *parser) {
3547 root := p.xastring()
3550 // We only have a per-account root called "".
3552 xuserErrorf("unknown quota root")
3555 var quota, size int64
3556 c.account.WithRLock(func() {
3557 quota = c.account.QuotaMessageSize()
3559 c.xdbread(func(tx *bstore.Tx) {
3560 du := store.DiskUsage{ID: 1}
3562 xcheckf(err, "gather used quota")
3563 size = du.MessageSize
3568 // We only write the quota response if there is a limit. The syntax doesn't allow
3569 // an empty list, so we cannot send the current disk usage if there is no limit.
3572 c.bwritelinef(`* QUOTA "" (STORAGE %d %d)`, (size+1024-1)/1024, (quota+1024-1)/1024)
3577// Check is an old deprecated command that is supposed to execute some mailbox consistency checks.
3580func (c *conn) cmdCheck(tag, cmd string, p *parser) {
3586 c.account.WithRLock(func() {
3587 c.xdbread(func(tx *bstore.Tx) {
3588 c.xmailboxID(tx, c.mailboxID) // Validate.
3595// Close undoes select/examine, closing the currently opened mailbox and deleting
3596// messages that were marked for deletion with the \Deleted flag.
3599func (c *conn) cmdClose(tag, cmd string, p *parser) {
3611 remove, _ := c.xexpunge(nil, true)
3614 for _, m := range remove {
3615 p := c.account.MessagePath(m.ID)
3617 c.xsanity(err, "removing message file for expunge for close")
3625// expunge messages marked for deletion in currently selected/active mailbox.
3626// if uidSet is not nil, only messages matching the set are deleted.
3628// messages that have been marked expunged from the database are returned, but the
3629// corresponding files still have to be removed.
3631// the highest modseq in the mailbox is returned, typically associated with the
3632// removal of the messages, but if no messages were expunged the current latest max
3633// modseq for the mailbox is returned.
3634func (c *conn) xexpunge(uidSet *numSet, missingMailboxOK bool) (remove []store.Message, highestModSeq store.ModSeq) {
3635 var modseq store.ModSeq
3637 c.account.WithWLock(func() {
3638 var mb store.Mailbox
3640 c.xdbwrite(func(tx *bstore.Tx) {
3641 mb = store.Mailbox{ID: c.mailboxID}
3643 if err == bstore.ErrAbsent {
3644 if missingMailboxOK {
3647 xuserErrorf("%w", store.ErrUnknownMailbox)
3650 qm := bstore.QueryTx[store.Message](tx)
3651 qm.FilterNonzero(store.Message{MailboxID: c.mailboxID})
3652 qm.FilterEqual("Deleted", true)
3653 qm.FilterEqual("Expunged", false)
3654 qm.FilterFn(func(m store.Message) bool {
3655 // Only remove if this session knows about the message and if present in optional uidSet.
3656 return uidSearch(c.uids, m.UID) > 0 && (uidSet == nil || uidSet.containsUID(m.UID, c.uids, c.searchResult))
3659 remove, err = qm.List()
3660 xcheckf(err, "listing messages to delete")
3662 if len(remove) == 0 {
3663 highestModSeq = c.xhighestModSeq(tx, c.mailboxID)
3667 // Assign new modseq.
3668 modseq, err = c.account.NextModSeq(tx)
3669 xcheckf(err, "assigning next modseq")
3670 highestModSeq = modseq
3672 removeIDs := make([]int64, len(remove))
3673 anyIDs := make([]any, len(remove))
3675 for i, m := range remove {
3678 mb.Sub(m.MailboxCounts())
3680 // Update "remove", because RetrainMessage below will save the message.
3681 remove[i].Expunged = true
3682 remove[i].ModSeq = modseq
3684 qmr := bstore.QueryTx[store.Recipient](tx)
3685 qmr.FilterEqual("MessageID", anyIDs...)
3686 _, err = qmr.Delete()
3687 xcheckf(err, "removing message recipients")
3689 qm = bstore.QueryTx[store.Message](tx)
3690 qm.FilterIDs(removeIDs)
3691 n, err := qm.UpdateNonzero(store.Message{Expunged: true, ModSeq: modseq})
3692 if err == nil && n != len(removeIDs) {
3693 err = fmt.Errorf("only %d messages set to expunged, expected %d", n, len(removeIDs))
3695 xcheckf(err, "marking messages marked for deleted as expunged")
3697 err = tx.Update(&mb)
3698 xcheckf(err, "updating mailbox counts")
3700 err = c.account.AddMessageSize(c.log, tx, -totalSize)
3701 xcheckf(err, "updating disk usage")
3703 // Mark expunged messages as not needing training, then retrain them, so if they
3704 // were trained, they get untrained.
3705 for i := range remove {
3706 remove[i].Junk = false
3707 remove[i].Notjunk = false
3709 err = c.account.RetrainMessages(context.TODO(), c.log, tx, remove, true)
3710 xcheckf(err, "untraining expunged messages")
3713 // Broadcast changes to other connections. We may not have actually removed any
3714 // messages, so take care not to send an empty update.
3715 if len(remove) > 0 {
3716 ouids := make([]store.UID, len(remove))
3717 for i, m := range remove {
3720 changes := []store.Change{
3721 store.ChangeRemoveUIDs{MailboxID: c.mailboxID, UIDs: ouids, ModSeq: modseq},
3724 c.broadcast(changes)
3727 return remove, highestModSeq
3730// Unselect is similar to close in that it closes the currently active mailbox, but
3731// it does not remove messages marked for deletion.
3734func (c *conn) cmdUnselect(tag, cmd string, p *parser) {
3744// Expunge deletes messages marked with \Deleted in the currently selected mailbox.
3745// Clients are wiser to use UID EXPUNGE because it allows a UID sequence set to
3746// explicitly opt in to removing specific messages.
3749func (c *conn) cmdExpunge(tag, cmd string, p *parser) {
3756 xuserErrorf("mailbox open in read-only mode")
3759 c.cmdxExpunge(tag, cmd, nil)
3762// UID expunge deletes messages marked with \Deleted in the currently selected
3763// mailbox if they match a UID sequence set.
3766func (c *conn) cmdUIDExpunge(tag, cmd string, p *parser) {
3771 uidSet := p.xnumSet()
3775 xuserErrorf("mailbox open in read-only mode")
3778 c.cmdxExpunge(tag, cmd, &uidSet)
3781// Permanently delete messages for the currently selected/active mailbox. If uidset
3782// is not nil, only those UIDs are removed.
3784func (c *conn) cmdxExpunge(tag, cmd string, uidSet *numSet) {
3787 remove, highestModSeq := c.xexpunge(uidSet, false)
3790 for _, m := range remove {
3791 p := c.account.MessagePath(m.ID)
3793 c.xsanity(err, "removing message file for expunge")
3798 var vanishedUIDs numSet
3799 qresync := c.enabled[capQresync]
3800 for _, m := range remove {
3801 seq := c.xsequence(m.UID)
3802 c.sequenceRemove(seq, m.UID)
3804 vanishedUIDs.append(uint32(m.UID))
3806 c.bwritelinef("* %d EXPUNGE", seq)
3809 if !vanishedUIDs.empty() {
3811 for _, s := range vanishedUIDs.Strings(4*1024 - 32) {
3812 c.bwritelinef("* VANISHED %s", s)
3816 if c.enabled[capCondstore] {
3817 c.writeresultf("%s OK [HIGHESTMODSEQ %d] expunged", tag, highestModSeq.Client())
3824func (c *conn) cmdSearch(tag, cmd string, p *parser) {
3825 c.cmdxSearch(false, tag, cmd, p)
3829func (c *conn) cmdUIDSearch(tag, cmd string, p *parser) {
3830 c.cmdxSearch(true, tag, cmd, p)
3834func (c *conn) cmdFetch(tag, cmd string, p *parser) {
3835 c.cmdxFetch(false, tag, cmd, p)
3839func (c *conn) cmdUIDFetch(tag, cmd string, p *parser) {
3840 c.cmdxFetch(true, tag, cmd, p)
3844func (c *conn) cmdStore(tag, cmd string, p *parser) {
3845 c.cmdxStore(false, tag, cmd, p)
3849func (c *conn) cmdUIDStore(tag, cmd string, p *parser) {
3850 c.cmdxStore(true, tag, cmd, p)
3854func (c *conn) cmdCopy(tag, cmd string, p *parser) {
3855 c.cmdxCopy(false, tag, cmd, p)
3859func (c *conn) cmdUIDCopy(tag, cmd string, p *parser) {
3860 c.cmdxCopy(true, tag, cmd, p)
3864func (c *conn) cmdMove(tag, cmd string, p *parser) {
3865 c.cmdxMove(false, tag, cmd, p)
3869func (c *conn) cmdUIDMove(tag, cmd string, p *parser) {
3870 c.cmdxMove(true, tag, cmd, p)
3873func (c *conn) gatherCopyMoveUIDs(isUID bool, nums numSet) ([]store.UID, []any) {
3874 // Gather uids, then sort so we can return a consistently simple and hard to
3875 // misinterpret COPYUID/MOVEUID response. It seems safer to have UIDs in ascending
3876 // order, because requested uid set of 12:10 is equal to 10:12, so if we would just
3877 // echo whatever the client sends us without reordering, the client can reorder our
3878 // response and interpret it differently than we intended.
3880 uids := c.xnumSetUIDs(isUID, nums)
3881 sort.Slice(uids, func(i, j int) bool {
3882 return uids[i] < uids[j]
3884 uidargs := make([]any, len(uids))
3885 for i, uid := range uids {
3888 return uids, uidargs
3891// Copy copies messages from the currently selected/active mailbox to another named
3895func (c *conn) cmdxCopy(isUID bool, tag, cmd string, p *parser) {
3902 name := p.xmailbox()
3905 name = xcheckmailboxname(name, true)
3907 uids, uidargs := c.gatherCopyMoveUIDs(isUID, nums)
3909 // Files that were created during the copy. Remove them if the operation fails.
3910 var createdIDs []int64
3916 for _, id := range createdIDs {
3917 p := c.account.MessagePath(id)
3919 c.xsanity(err, "cleaning up created file")
3924 var mbDst store.Mailbox
3925 var origUIDs, newUIDs []store.UID
3926 var flags []store.Flags
3927 var keywords [][]string
3928 var modseq store.ModSeq // For messages in new mailbox, assigned when first message is copied.
3930 c.account.WithWLock(func() {
3931 var mbKwChanged bool
3933 c.xdbwrite(func(tx *bstore.Tx) {
3934 mbSrc := c.xmailboxID(tx, c.mailboxID) // Validate.
3935 mbDst = c.xmailbox(tx, name, "TRYCREATE")
3936 if mbDst.ID == mbSrc.ID {
3937 xuserErrorf("cannot copy to currently selected mailbox")
3940 if len(uidargs) == 0 {
3941 xuserErrorf("no matching messages to copy")
3945 modseq, err = c.account.NextModSeq(tx)
3946 xcheckf(err, "assigning next modseq")
3948 // Reserve the uids in the destination mailbox.
3949 uidFirst := mbDst.UIDNext
3950 mbDst.UIDNext += store.UID(len(uidargs))
3952 // Fetch messages from database.
3953 q := bstore.QueryTx[store.Message](tx)
3954 q.FilterNonzero(store.Message{MailboxID: c.mailboxID})
3955 q.FilterEqual("UID", uidargs...)
3956 q.FilterEqual("Expunged", false)
3957 xmsgs, err := q.List()
3958 xcheckf(err, "fetching messages")
3960 if len(xmsgs) != len(uidargs) {
3961 xserverErrorf("uid and message mismatch")
3964 // See if quota allows copy.
3966 for _, m := range xmsgs {
3969 if ok, maxSize, err := c.account.CanAddMessageSize(tx, totalSize); err != nil {
3970 xcheckf(err, "checking quota")
3973 xusercodeErrorf("OVERQUOTA", "account over maximum total message size %d", maxSize)
3975 err = c.account.AddMessageSize(c.log, tx, totalSize)
3976 xcheckf(err, "updating disk usage")
3978 msgs := map[store.UID]store.Message{}
3979 for _, m := range xmsgs {
3982 nmsgs := make([]store.Message, len(xmsgs))
3984 conf, _ := c.account.Conf()
3986 mbKeywords := map[string]struct{}{}
3989 // Insert new messages into database.
3990 var origMsgIDs, newMsgIDs []int64
3991 for i, uid := range uids {
3994 xuserErrorf("messages changed, could not fetch requested uid")
3997 origMsgIDs = append(origMsgIDs, origID)
3999 m.UID = uidFirst + store.UID(i)
4000 m.CreateSeq = modseq
4002 m.MailboxID = mbDst.ID
4003 if m.IsReject && m.MailboxDestinedID != 0 {
4004 // Incorrectly delivered to Rejects mailbox. Adjust MailboxOrigID so this message
4005 // is used for reputation calculation during future deliveries.
4006 m.MailboxOrigID = m.MailboxDestinedID
4010 m.JunkFlagsForMailbox(mbDst, conf)
4012 err := tx.Insert(&m)
4013 xcheckf(err, "inserting message")
4016 origUIDs = append(origUIDs, uid)
4017 newUIDs = append(newUIDs, m.UID)
4018 newMsgIDs = append(newMsgIDs, m.ID)
4019 flags = append(flags, m.Flags)
4020 keywords = append(keywords, m.Keywords)
4021 for _, kw := range m.Keywords {
4022 mbKeywords[kw] = struct{}{}
4025 qmr := bstore.QueryTx[store.Recipient](tx)
4026 qmr.FilterNonzero(store.Recipient{MessageID: origID})
4027 mrs, err := qmr.List()
4028 xcheckf(err, "listing message recipients")
4029 for _, mr := range mrs {
4032 err := tx.Insert(&mr)
4033 xcheckf(err, "inserting message recipient")
4036 mbDst.Add(m.MailboxCounts())
4039 mbDst.Keywords, mbKwChanged = store.MergeKeywords(mbDst.Keywords, maps.Keys(mbKeywords))
4041 err = tx.Update(&mbDst)
4042 xcheckf(err, "updating destination mailbox for uids, keywords and counts")
4044 // Copy message files to new message ID's.
4045 syncDirs := map[string]struct{}{}
4046 for i := range origMsgIDs {
4047 src := c.account.MessagePath(origMsgIDs[i])
4048 dst := c.account.MessagePath(newMsgIDs[i])
4049 dstdir := filepath.Dir(dst)
4050 if _, ok := syncDirs[dstdir]; !ok {
4051 os.MkdirAll(dstdir, 0770)
4052 syncDirs[dstdir] = struct{}{}
4054 err := moxio.LinkOrCopy(c.log, dst, src, nil, true)
4055 xcheckf(err, "link or copy file %q to %q", src, dst)
4056 createdIDs = append(createdIDs, newMsgIDs[i])
4059 for dir := range syncDirs {
4060 err := moxio.SyncDir(c.log, dir)
4061 xcheckf(err, "sync directory")
4064 err = c.account.RetrainMessages(context.TODO(), c.log, tx, nmsgs, false)
4065 xcheckf(err, "train copied messages")
4068 // Broadcast changes to other connections.
4069 if len(newUIDs) > 0 {
4070 changes := make([]store.Change, 0, len(newUIDs)+2)
4071 for i, uid := range newUIDs {
4072 changes = append(changes, store.ChangeAddUID{MailboxID: mbDst.ID, UID: uid, ModSeq: modseq, Flags: flags[i], Keywords: keywords[i]})
4074 changes = append(changes, mbDst.ChangeCounts())
4076 changes = append(changes, mbDst.ChangeKeywords())
4078 c.broadcast(changes)
4082 // All good, prevent defer above from cleaning up copied files.
4086 c.writeresultf("%s OK [COPYUID %d %s %s] copied", tag, mbDst.UIDValidity, compactUIDSet(origUIDs).String(), compactUIDSet(newUIDs).String())
4089// Move moves messages from the currently selected/active mailbox to a named mailbox.
4092func (c *conn) cmdxMove(isUID bool, tag, cmd string, p *parser) {
4099 name := p.xmailbox()
4102 name = xcheckmailboxname(name, true)
4105 xuserErrorf("mailbox open in read-only mode")
4108 uids, uidargs := c.gatherCopyMoveUIDs(isUID, nums)
4110 var mbSrc, mbDst store.Mailbox
4111 var changes []store.Change
4112 var newUIDs []store.UID
4113 var modseq store.ModSeq
4115 c.account.WithWLock(func() {
4116 c.xdbwrite(func(tx *bstore.Tx) {
4117 mbSrc = c.xmailboxID(tx, c.mailboxID) // Validate.
4118 mbDst = c.xmailbox(tx, name, "TRYCREATE")
4119 if mbDst.ID == c.mailboxID {
4120 xuserErrorf("cannot move to currently selected mailbox")
4123 if len(uidargs) == 0 {
4124 xuserErrorf("no matching messages to move")
4127 // Reserve the uids in the destination mailbox.
4128 uidFirst := mbDst.UIDNext
4130 mbDst.UIDNext += store.UID(len(uids))
4132 // Assign a new modseq, for the new records and for the expunged records.
4134 modseq, err = c.account.NextModSeq(tx)
4135 xcheckf(err, "assigning next modseq")
4137 // Update existing record with new UID and MailboxID in database for messages. We
4138 // add a new but expunged record again in the original/source mailbox, for qresync.
4139 // Keeping the original ID for the live message means we don't have to move the
4140 // on-disk message contents file.
4141 q := bstore.QueryTx[store.Message](tx)
4142 q.FilterNonzero(store.Message{MailboxID: c.mailboxID})
4143 q.FilterEqual("UID", uidargs...)
4144 q.FilterEqual("Expunged", false)
4146 msgs, err := q.List()
4147 xcheckf(err, "listing messages to move")
4149 if len(msgs) != len(uidargs) {
4150 xserverErrorf("uid and message mismatch")
4153 keywords := map[string]struct{}{}
4156 conf, _ := c.account.Conf()
4157 for i := range msgs {
4159 if m.UID != uids[i] {
4160 xserverErrorf("internal error: got uid %d, expected %d, for index %d", m.UID, uids[i], i)
4163 mbSrc.Sub(m.MailboxCounts())
4165 // Copy of message record that we'll insert when UID is freed up.
4168 om.ID = 0 // Assign new ID.
4171 m.MailboxID = mbDst.ID
4172 if m.IsReject && m.MailboxDestinedID != 0 {
4173 // Incorrectly delivered to Rejects mailbox. Adjust MailboxOrigID so this message
4174 // is used for reputation calculation during future deliveries.
4175 m.MailboxOrigID = m.MailboxDestinedID
4179 mbDst.Add(m.MailboxCounts())
4182 m.JunkFlagsForMailbox(mbDst, conf)
4186 xcheckf(err, "updating moved message in database")
4188 // Now that UID is unused, we can insert the old record again.
4189 err = tx.Insert(&om)
4190 xcheckf(err, "inserting record for expunge after moving message")
4192 for _, kw := range m.Keywords {
4193 keywords[kw] = struct{}{}
4197 // Ensure destination mailbox has keywords of the moved messages.
4198 var mbKwChanged bool
4199 mbDst.Keywords, mbKwChanged = store.MergeKeywords(mbDst.Keywords, maps.Keys(keywords))
4201 changes = append(changes, mbDst.ChangeKeywords())
4204 err = tx.Update(&mbSrc)
4205 xcheckf(err, "updating source mailbox counts")
4207 err = tx.Update(&mbDst)
4208 xcheckf(err, "updating destination mailbox for uids, keywords and counts")
4210 err = c.account.RetrainMessages(context.TODO(), c.log, tx, msgs, false)
4211 xcheckf(err, "retraining messages after move")
4213 // Prepare broadcast changes to other connections.
4214 changes = make([]store.Change, 0, 1+len(msgs)+2)
4215 changes = append(changes, store.ChangeRemoveUIDs{MailboxID: c.mailboxID, UIDs: uids, ModSeq: modseq})
4216 for _, m := range msgs {
4217 newUIDs = append(newUIDs, m.UID)
4218 changes = append(changes, m.ChangeAddUID())
4220 changes = append(changes, mbSrc.ChangeCounts(), mbDst.ChangeCounts())
4223 c.broadcast(changes)
4228 c.bwritelinef("* OK [COPYUID %d %s %s] moved", mbDst.UIDValidity, compactUIDSet(uids).String(), compactUIDSet(newUIDs).String())
4229 qresync := c.enabled[capQresync]
4230 var vanishedUIDs numSet
4231 for i := 0; i < len(uids); i++ {
4232 seq := c.xsequence(uids[i])
4233 c.sequenceRemove(seq, uids[i])
4235 vanishedUIDs.append(uint32(uids[i]))
4237 c.bwritelinef("* %d EXPUNGE", seq)
4240 if !vanishedUIDs.empty() {
4242 for _, s := range vanishedUIDs.Strings(4*1024 - 32) {
4243 c.bwritelinef("* VANISHED %s", s)
4247 if c.enabled[capQresync] {
4249 c.writeresultf("%s OK [HIGHESTMODSEQ %d] move", tag, modseq.Client())
4255// Store sets a full set of flags, or adds/removes specific flags.
4258func (c *conn) cmdxStore(isUID bool, tag, cmd string, p *parser) {
4265 var unchangedSince *int64
4268 p.xtake("UNCHANGEDSINCE")
4275 c.xensureCondstore(nil)
4277 var plus, minus bool
4280 } else if p.take("-") {
4284 silent := p.take(".SILENT")
4286 var flagstrs []string
4287 if p.hasPrefix("(") {
4288 flagstrs = p.xflagList()
4290 flagstrs = append(flagstrs, p.xflag())
4292 flagstrs = append(flagstrs, p.xflag())
4298 xuserErrorf("mailbox open in read-only mode")
4301 flags, keywords, err := store.ParseFlagsKeywords(flagstrs)
4303 xuserErrorf("parsing flags: %v", err)
4305 var mask store.Flags
4307 mask, flags = flags, store.FlagsAll
4309 mask, flags = flags, store.Flags{}
4311 mask = store.FlagsAll
4314 var mb, origmb store.Mailbox
4315 var updated []store.Message
4316 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.
4317 var modseq store.ModSeq // Assigned when needed.
4318 modified := map[int64]bool{}
4320 c.account.WithWLock(func() {
4321 var mbKwChanged bool
4322 var changes []store.Change
4324 c.xdbwrite(func(tx *bstore.Tx) {
4325 mb = c.xmailboxID(tx, c.mailboxID) // Validate.
4328 uidargs := c.xnumSetCondition(isUID, nums)
4330 if len(uidargs) == 0 {
4334 // Ensure keywords are in mailbox.
4336 mb.Keywords, mbKwChanged = store.MergeKeywords(mb.Keywords, keywords)
4338 err := tx.Update(&mb)
4339 xcheckf(err, "updating mailbox with keywords")
4343 q := bstore.QueryTx[store.Message](tx)
4344 q.FilterNonzero(store.Message{MailboxID: c.mailboxID})
4345 q.FilterEqual("UID", uidargs...)
4346 q.FilterEqual("Expunged", false)
4347 err := q.ForEach(func(m store.Message) error {
4348 // Client may specify a message multiple times, but we only process it once.
../rfc/7162:823
4353 mc := m.MailboxCounts()
4355 origFlags := m.Flags
4356 m.Flags = m.Flags.Set(mask, flags)
4357 oldKeywords := append([]string{}, m.Keywords...)
4359 m.Keywords, _ = store.RemoveKeywords(m.Keywords, keywords)
4361 m.Keywords, _ = store.MergeKeywords(m.Keywords, keywords)
4363 m.Keywords = keywords
4366 keywordsChanged := func() bool {
4367 sort.Strings(oldKeywords)
4368 n := append([]string{}, m.Keywords...)
4370 return !slices.Equal(oldKeywords, n)
4373 // If the message has a more recent modseq than the check requires, we won't modify
4374 // it and report in the final command response.
4377 // unchangedSince 0 always fails the check, we don't turn it into 1 like with our
4378 // internal modseqs. RFC implies that is not required for non-system flags, but we
4380 if unchangedSince != nil && m.ModSeq.Client() > *unchangedSince {
4381 changed = append(changed, m)
4386 // It requires that we keep track of the flags we think the client knows (but only
4387 // on this connection). We don't track that. It also isn't clear why this is
4388 // allowed because it is skipping the condstore conditional check, and the new
4389 // combination of flags could be unintended.
4392 if origFlags == m.Flags && !keywordsChanged() {
4393 // Note: since we didn't update the modseq, we are not adding m.ID to "modified",
4394 // it would skip the modseq check above. We still add m to list of updated, so we
4395 // send an untagged fetch response. But we don't broadcast it.
4396 updated = append(updated, m)
4401 mb.Add(m.MailboxCounts())
4403 // Assign new modseq for first actual change.
4406 modseq, err = c.account.NextModSeq(tx)
4407 xcheckf(err, "next modseq")
4410 modified[m.ID] = true
4411 updated = append(updated, m)
4413 changes = append(changes, m.ChangeFlags(origFlags))
4415 return tx.Update(&m)
4417 xcheckf(err, "storing flags in messages")
4419 if mb.MailboxCounts != origmb.MailboxCounts {
4420 err := tx.Update(&mb)
4421 xcheckf(err, "updating mailbox counts")
4423 changes = append(changes, mb.ChangeCounts())
4426 changes = append(changes, mb.ChangeKeywords())
4429 err = c.account.RetrainMessages(context.TODO(), c.log, tx, updated, false)
4430 xcheckf(err, "training messages")
4433 c.broadcast(changes)
4436 // In the RFC, the section about STORE/UID STORE says we must return MODSEQ when
4437 // UNCHANGEDSINCE was specified. It does not specify it in case UNCHANGEDSINCE
4438 // isn't specified. For that case it does say MODSEQ is needed in unsolicited
4439 // untagged fetch responses. Implying that solicited untagged fetch responses
4440 // should not include MODSEQ (why else mention unsolicited explicitly?). But, in
4441 // the introduction to CONDSTORE it does explicitly specify MODSEQ should be
4442 // included in untagged fetch responses at all times with CONDSTORE-enabled
4443 // connections. It would have been better if the command behaviour was specified in
4444 // the command section, not the introduction to the extension.
4447 if !silent || c.enabled[capCondstore] {
4448 for _, m := range updated {
4451 flags = fmt.Sprintf(" FLAGS %s", flaglist(m.Flags, m.Keywords).pack(c))
4453 var modseqStr string
4454 if c.enabled[capCondstore] {
4455 modseqStr = fmt.Sprintf(" MODSEQ (%d)", m.ModSeq.Client())
4458 c.bwritelinef("* %d FETCH (UID %d%s%s)", c.xsequence(m.UID), m.UID, flags, modseqStr)
4462 // We don't explicitly send flags for failed updated with silent set. The regular
4463 // notification will get the flags to the client.
4466 if len(changed) == 0 {
4471 // Write unsolicited untagged fetch responses for messages that didn't pass the
4474 var mnums []store.UID
4475 for _, m := range changed {
4476 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())
4478 mnums = append(mnums, m.UID)
4480 mnums = append(mnums, store.UID(c.xsequence(m.UID)))
4484 sort.Slice(mnums, func(i, j int) bool {
4485 return mnums[i] < mnums[j]
4487 set := compactUIDSet(mnums)
4489 c.writeresultf("%s OK [MODIFIED %s] conditional store did not modify all", tag, set.String())