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: 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.
62 "golang.org/x/text/unicode/norm"
64 "github.com/prometheus/client_golang/prometheus"
65 "github.com/prometheus/client_golang/prometheus/promauto"
67 "github.com/mjl-/bstore"
68 "github.com/mjl-/flate"
70 "github.com/mjl-/mox/config"
71 "github.com/mjl-/mox/junk"
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 unhandledPanics atomic.Int64 // For tests.
108var limiterConnectionrate, limiterConnections *ratelimit.Limiter
111 // Also called by tests, so they don't trigger the rate limiter.
117 limiterConnectionrate = &ratelimit.Limiter{
118 WindowLimits: []ratelimit.WindowLimit{
121 Limits: [...]int64{300, 900, 2700},
125 limiterConnections = &ratelimit.Limiter{
126 WindowLimits: []ratelimit.WindowLimit{
128 Window: time.Duration(math.MaxInt64), // All of time.
129 Limits: [...]int64{30, 90, 270},
135// Delay after bad/suspicious behaviour. Tests set these to zero.
136var badClientDelay = time.Second // Before reads and after 1-byte writes for probably spammers.
137var authFailDelay = time.Second // After authentication failure.
139// Capabilities (extensions) the server supports. Connections will add a few more,
140// e.g. STARTTLS, LOGINDISABLED, AUTH=PLAIN.
142// We always announce support for SCRAM PLUS-variants, also on connections without
143// TLS. The client should not be selecting PLUS variants on non-TLS connections,
144// instead opting to do the bare SCRAM variant without indicating the server claims
145// to support the PLUS variant (skipping the server downgrade detection check).
146var serverCapabilities = strings.Join([]string{
162 "CREATE-SPECIAL-USE", //
165 "AUTH=SCRAM-SHA-256", //
167 "AUTH=SCRAM-SHA-1", //
170 "APPENDLIMIT=9223372036854775807", //
../rfc/7889:129, we support the max possible size, 1<<63 - 1
175 "QUOTA=RES-STORAGE", //
186 // "COMPRESS=DEFLATE", //
../rfc/4978, disabled for interoperability issues: The flate reader (inflate) still blocks on partial flushes, preventing progress.
193 connBroken bool // Once broken, we won't flush any more data.
194 tls bool // Whether TLS has been initialized.
195 viaHTTPS bool // Whether this connection came in via HTTPS (using TLS ALPN).
196 br *bufio.Reader // From remote, with TLS unwrapped in case of TLS, and possibly wrapping inflate.
197 tr *moxio.TraceReader // Kept to change trace level when reading/writing cmd/auth/data.
198 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.
199 lastLine string // For detecting if syntax error is fatal, i.e. if this ends with a literal. Without crlf.
200 xbw *bufio.Writer // To remote, with TLS added in case of TLS, and possibly wrapping deflate, see conn.xflateWriter. Writes go through xtw to conn.Write, which panics on errors, hence the "x".
201 xtw *moxio.TraceWriter
202 xflateWriter *moxio.FlateWriter // For flushing output after flushing conn.xbw, and for closing.
203 xflateBW *bufio.Writer // Wraps raw connection writes, xflateWriter writes here, also needs flushing.
204 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.
205 lastlog time.Time // For printing time since previous log line.
206 baseTLSConfig *tls.Config // Base TLS config to use for handshake.
208 noRequireSTARTTLS bool
209 cmd string // Currently executing, for deciding to applyChanges and logging.
210 cmdMetric string // Currently executing, for metrics.
212 ncmds int // Number of commands processed. Used to abort connection when first incoming command is unknown/invalid.
213 log mlog.Log // Used for all synchronous logging on this connection, see logbg for logging in a separate goroutine.
214 enabled map[capability]bool // All upper-case.
215 compress bool // Whether compression is enabled, via compress command.
217 // Set by SEARCH with SAVE. Can be used by commands accepting a sequence-set with
218 // value "$". When used, UIDs must be verified to still exist, because they may
219 // have been expunged. Cleared by a SELECT or EXAMINE.
220 // Nil means no searchResult is present. An empty list is a valid searchResult,
221 // just not matching any messages.
223 searchResult []store.UID
225 // userAgent is set by the ID command, which can happen at any time (before or
226 // after the authentication attempt we want to log it with).
228 // loginAttempt is set during authentication, typically picked up by the ID command
229 // that soon follows, or it will be flushed within 1s, or on connection teardown.
230 loginAttempt *store.LoginAttempt
231 loginAttemptTime time.Time
233 // Only set when connection has been authenticated. These can be set even when
234 // c.state is stateNotAuthenticated, for TLS client certificate authentication. In
235 // that case, credentials aren't used until the authentication command with the
236 // SASL "EXTERNAL" mechanism.
237 authFailed int // Number of failed auth attempts. For slowing down remote with many failures.
238 noPreauth bool // If set, don't switch connection to "authenticated" after TLS handshake with client certificate authentication.
239 username string // Full username as used during login.
240 account *store.Account
241 comm *store.Comm // For sending/receiving changes on mailboxes in account, e.g. from messages incoming on smtp, or another imap client.
243 mailboxID int64 // Only for StateSelected.
244 readonly bool // If opened mailbox is readonly.
245 uids []store.UID // UIDs known in this session, sorted. todo future: store more space-efficiently, as ranges.
248// capability for use with ENABLED and CAPABILITY. We always keep this upper case,
249// e.g. IMAP4REV2. These values are treated case-insensitive, but it's easier for
250// comparison to just always have the same case.
251type capability string
254 capIMAP4rev2 capability = "IMAP4REV2"
255 capUTF8Accept capability = "UTF8=ACCEPT"
256 capCondstore capability = "CONDSTORE"
257 capQresync capability = "QRESYNC"
258 capMetadata capability = "METADATA"
269 stateNotAuthenticated state = iota
274func stateCommands(cmds ...string) map[string]struct{} {
275 r := map[string]struct{}{}
276 for _, cmd := range cmds {
283 commandsStateAny = stateCommands("capability", "noop", "logout", "id")
284 commandsStateNotAuthenticated = stateCommands("starttls", "authenticate", "login")
285 commandsStateAuthenticated = stateCommands("enable", "select", "examine", "create", "delete", "rename", "subscribe", "unsubscribe", "list", "namespace", "status", "append", "idle", "lsub", "getquotaroot", "getquota", "getmetadata", "setmetadata", "compress", "esearch")
286 commandsStateSelected = stateCommands("close", "unselect", "expunge", "search", "fetch", "store", "copy", "move", "uid expunge", "uid search", "uid fetch", "uid store", "uid copy", "uid move", "replace", "uid replace", "esearch")
289var commands = map[string]func(c *conn, tag, cmd string, p *parser){
291 "capability": (*conn).cmdCapability,
292 "noop": (*conn).cmdNoop,
293 "logout": (*conn).cmdLogout,
297 "starttls": (*conn).cmdStarttls,
298 "authenticate": (*conn).cmdAuthenticate,
299 "login": (*conn).cmdLogin,
301 // Authenticated and selected.
302 "enable": (*conn).cmdEnable,
303 "select": (*conn).cmdSelect,
304 "examine": (*conn).cmdExamine,
305 "create": (*conn).cmdCreate,
306 "delete": (*conn).cmdDelete,
307 "rename": (*conn).cmdRename,
308 "subscribe": (*conn).cmdSubscribe,
309 "unsubscribe": (*conn).cmdUnsubscribe,
310 "list": (*conn).cmdList,
311 "lsub": (*conn).cmdLsub,
312 "namespace": (*conn).cmdNamespace,
313 "status": (*conn).cmdStatus,
314 "append": (*conn).cmdAppend,
315 "idle": (*conn).cmdIdle,
316 "getquotaroot": (*conn).cmdGetquotaroot,
317 "getquota": (*conn).cmdGetquota,
318 "getmetadata": (*conn).cmdGetmetadata,
319 "setmetadata": (*conn).cmdSetmetadata,
320 "compress": (*conn).cmdCompress,
321 "esearch": (*conn).cmdEsearch,
324 "check": (*conn).cmdCheck,
325 "close": (*conn).cmdClose,
326 "unselect": (*conn).cmdUnselect,
327 "expunge": (*conn).cmdExpunge,
328 "uid expunge": (*conn).cmdUIDExpunge,
329 "search": (*conn).cmdSearch,
330 "uid search": (*conn).cmdUIDSearch,
331 "fetch": (*conn).cmdFetch,
332 "uid fetch": (*conn).cmdUIDFetch,
333 "store": (*conn).cmdStore,
334 "uid store": (*conn).cmdUIDStore,
335 "copy": (*conn).cmdCopy,
336 "uid copy": (*conn).cmdUIDCopy,
337 "move": (*conn).cmdMove,
338 "uid move": (*conn).cmdUIDMove,
340 "replace": (*conn).cmdReplace,
341 "uid replace": (*conn).cmdUIDReplace,
344var errIO = errors.New("io error") // For read/write errors and errors that should close the connection.
345var errProtocol = errors.New("protocol error") // For protocol errors for which a stack trace should be printed.
349// check err for sanity.
350// if not nil and checkSanity true (set during tests), then panic. if not nil during normal operation, just log.
351func (c *conn) xsanity(err error, format string, args ...any) {
356 panic(fmt.Errorf("%s: %s", fmt.Sprintf(format, args...), err))
358 c.log.Errorx(fmt.Sprintf(format, args...), err)
361func (c *conn) xbrokenf(format string, args ...any) {
363 panic(fmt.Errorf(format, args...))
368// Listen initializes all imap listeners for the configuration, and stores them for Serve to start them.
370 names := slices.Sorted(maps.Keys(mox.Conf.Static.Listeners))
371 for _, name := range names {
372 listener := mox.Conf.Static.Listeners[name]
374 var tlsConfig *tls.Config
375 if listener.TLS != nil {
376 tlsConfig = listener.TLS.Config
379 if listener.IMAP.Enabled {
380 port := config.Port(listener.IMAP.Port, 143)
381 for _, ip := range listener.IPs {
382 listen1("imap", name, ip, port, tlsConfig, false, listener.IMAP.NoRequireSTARTTLS)
386 if listener.IMAPS.Enabled {
387 port := config.Port(listener.IMAPS.Port, 993)
388 for _, ip := range listener.IPs {
389 listen1("imaps", name, ip, port, tlsConfig, true, false)
397func listen1(protocol, listenerName, ip string, port int, tlsConfig *tls.Config, xtls, noRequireSTARTTLS bool) {
398 log := mlog.New("imapserver", nil)
399 addr := net.JoinHostPort(ip, fmt.Sprintf("%d", port))
400 if os.Getuid() == 0 {
401 log.Print("listening for imap",
402 slog.String("listener", listenerName),
403 slog.String("addr", addr),
404 slog.String("protocol", protocol))
406 network := mox.Network(ip)
407 ln, err := mox.Listen(network, addr)
409 log.Fatalx("imap: listen for imap", err, slog.String("protocol", protocol), slog.String("listener", listenerName))
412 // Each listener gets its own copy of the config, so session keys between different
413 // ports on same listener aren't shared. We rotate session keys explicitly in this
414 // base TLS config because each connection clones the TLS config before using. The
415 // base TLS config would never get automatically managed/rotated session keys.
416 if tlsConfig != nil {
417 tlsConfig = tlsConfig.Clone()
418 mox.StartTLSSessionTicketKeyRefresher(mox.Shutdown, log, tlsConfig)
423 conn, err := ln.Accept()
425 log.Infox("imap: accept", err, slog.String("protocol", protocol), slog.String("listener", listenerName))
429 metricIMAPConnection.WithLabelValues(protocol).Inc()
430 go serve(listenerName, mox.Cid(), tlsConfig, conn, xtls, noRequireSTARTTLS, false, "")
434 servers = append(servers, serve)
437// ServeTLSConn serves IMAP on a TLS connection.
438func ServeTLSConn(listenerName string, conn *tls.Conn, tlsConfig *tls.Config) {
439 serve(listenerName, mox.Cid(), tlsConfig, conn, true, false, true, "")
442func ServeConnPreauth(listenerName string, cid int64, conn net.Conn, preauthAddress string) {
443 serve(listenerName, cid, nil, conn, false, true, false, preauthAddress)
446// Serve starts serving on all listeners, launching a goroutine per listener.
448 for _, serve := range servers {
454// Logbg returns a logger for logging in the background (in a goroutine), eg for
455// logging LoginAttempts. The regular c.log has a handler that evaluates fields on
456// the connection at time of logging, which may happen at the same time as
457// modifications to those fields.
458func (c *conn) logbg() mlog.Log {
459 log := mlog.New("imapserver", nil).WithCid(c.cid)
460 if c.username != "" {
461 log = log.With(slog.String("username", c.username))
466// returns whether this connection accepts utf-8 in strings.
467func (c *conn) utf8strings() bool {
468 return c.enabled[capIMAP4rev2] || c.enabled[capUTF8Accept]
471func (c *conn) xdbwrite(fn func(tx *bstore.Tx)) {
472 err := c.account.DB.Write(context.TODO(), func(tx *bstore.Tx) error {
476 xcheckf(err, "transaction")
479func (c *conn) xdbread(fn func(tx *bstore.Tx)) {
480 err := c.account.DB.Read(context.TODO(), func(tx *bstore.Tx) error {
484 xcheckf(err, "transaction")
487// Closes the currently selected/active mailbox, setting state from selected to authenticated.
488// Does not remove messages marked for deletion.
489func (c *conn) unselect() {
490 if c.state == stateSelected {
491 c.state = stateAuthenticated
497func (c *conn) setSlow(on bool) {
499 c.log.Debug("connection changed to slow")
500 } else if !on && c.slow {
501 c.log.Debug("connection restored to regular pace")
506// Write makes a connection an io.Writer. It panics for i/o errors. These errors
507// are handled in the connection command loop.
508func (c *conn) Write(buf []byte) (int, error) {
516 err := c.conn.SetWriteDeadline(time.Now().Add(30 * time.Second))
517 c.log.Check(err, "setting write deadline")
519 nn, err := c.conn.Write(buf[:chunk])
521 c.xbrokenf("write: %s (%w)", err, errIO)
525 if len(buf) > 0 && badClientDelay > 0 {
526 mox.Sleep(mox.Context, badClientDelay)
532func (c *conn) xtrace(level slog.Level) func() {
535 c.xtw.SetTrace(level)
538 c.tr.SetTrace(mlog.LevelTrace)
539 c.xtw.SetTrace(mlog.LevelTrace)
543// Cache of line buffers for reading commands.
545var bufpool = moxio.NewBufpool(8, 16*1024)
547// read line from connection, not going through line channel.
548func (c *conn) readline0() (string, error) {
549 if c.slow && badClientDelay > 0 {
550 mox.Sleep(mox.Context, badClientDelay)
553 d := 30 * time.Minute
554 if c.state == stateNotAuthenticated {
557 err := c.conn.SetReadDeadline(time.Now().Add(d))
558 c.log.Check(err, "setting read deadline")
560 line, err := bufpool.Readline(c.log, c.br)
561 if err != nil && errors.Is(err, moxio.ErrLineTooLong) {
562 return "", fmt.Errorf("%s (%w)", err, errProtocol)
563 } else if err != nil {
565 return "", fmt.Errorf("%s (%w)", err, errIO)
570func (c *conn) lineChan() chan lineErr {
572 c.line = make(chan lineErr, 1)
574 line, err := c.readline0()
575 c.line <- lineErr{line, err}
581// readline from either the c.line channel, or otherwise read from connection.
582func (c *conn) readline(readCmd bool) string {
588 line, err = le.line, le.err
590 line, err = c.readline0()
593 if readCmd && errors.Is(err, os.ErrDeadlineExceeded) {
594 err := c.conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
595 c.log.Check(err, "setting write deadline")
596 c.writelinef("* BYE inactive")
598 if !errors.Is(err, errIO) && !errors.Is(err, errProtocol) {
599 c.xbrokenf("%s (%w)", err, errIO)
605 // We typically respond immediately (IDLE is an exception).
606 // The client may not be reading, or may have disappeared.
607 // Don't wait more than 5 minutes before closing down the connection.
608 // The write deadline is managed in IDLE as well.
609 // For unauthenticated connections, we require the client to read faster.
610 wd := 5 * time.Minute
611 if c.state == stateNotAuthenticated {
612 wd = 30 * time.Second
614 err = c.conn.SetWriteDeadline(time.Now().Add(wd))
615 c.log.Check(err, "setting write deadline")
620// write tagged command response, but first write pending changes.
621func (c *conn) writeresultf(format string, args ...any) {
622 c.bwriteresultf(format, args...)
626// write buffered tagged command response, but first write pending changes.
627func (c *conn) bwriteresultf(format string, args ...any) {
629 case "fetch", "store", "search":
633 c.applyChanges(c.comm.Get(), false)
636 c.bwritelinef(format, args...)
639func (c *conn) writelinef(format string, args ...any) {
640 c.bwritelinef(format, args...)
644// Buffer line for write.
645func (c *conn) bwritelinef(format string, args ...any) {
647 fmt.Fprintf(c.xbw, format, args...)
650func (c *conn) xflush() {
651 // If the connection is already broken, we're not going to write more.
657 xcheckf(err, "flush") // Should never happen, the Write caused by the Flush should panic on i/o error.
659 // If compression is enabled, we need to flush its stream.
661 // Note: Flush writes a sync message if there is nothing to flush. Ideally we
662 // wouldn't send that, but we would have to keep track of whether data needs to be
664 err := c.xflateWriter.Flush()
665 xcheckf(err, "flush deflate")
667 // The flate writer writes to a bufio.Writer, we must also flush that.
668 err = c.xflateBW.Flush()
669 xcheckf(err, "flush deflate writer")
673func (c *conn) readCommand(tag *string) (cmd string, p *parser) {
674 line := c.readline(true)
675 p = newParser(line, c)
681 return cmd, newParser(p.remainder(), c)
684func (c *conn) xreadliteral(size int64, sync bool) []byte {
688 buf := make([]byte, size)
690 if err := c.conn.SetReadDeadline(time.Now().Add(30 * time.Second)); err != nil {
691 c.log.Errorx("setting read deadline", err)
694 _, err := io.ReadFull(c.br, buf)
696 c.xbrokenf("reading literal: %s (%w)", err, errIO)
702var cleanClose struct{} // Sentinel value for panic/recover indicating clean close of connection.
704// serve handles a single IMAP connection on nc.
706// If xtls is set, immediate TLS should be enabled on the connection, unless
707// viaHTTP is set, which indicates TLS is already active with the connection coming
708// from the webserver with IMAP chosen through ALPN. activated. If viaHTTP is set,
709// the TLS config ddid not enable client certificate authentication. If xtls is
710// false and tlsConfig is set, STARTTLS may enable TLS later on.
712// If noRequireSTARTTLS is set, TLS is not required for authentication.
714// If accountAddress is not empty, it is the email address of the account to open
717// The connection is closed before returning.
718func serve(listenerName string, cid int64, tlsConfig *tls.Config, nc net.Conn, xtls, noRequireSTARTTLS, viaHTTPS bool, preauthAddress string) {
720 if a, ok := nc.RemoteAddr().(*net.TCPAddr); ok {
723 // For tests and for imapserve.
724 remoteIP = net.ParseIP("127.0.0.10")
733 baseTLSConfig: tlsConfig,
735 noRequireSTARTTLS: noRequireSTARTTLS,
736 enabled: map[capability]bool{},
738 cmdStart: time.Now(),
740 var logmutex sync.Mutex
741 // Also see (and possibly update) c.logbg, for logging in a goroutine.
742 c.log = mlog.New("imapserver", nil).WithFunc(func() []slog.Attr {
744 defer logmutex.Unlock()
747 slog.Int64("cid", c.cid),
748 slog.Duration("delta", now.Sub(c.lastlog)),
751 if c.username != "" {
752 l = append(l, slog.String("username", c.username))
756 c.tr = moxio.NewTraceReader(c.log, "C: ", c.conn)
757 // 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.
758 c.br = bufio.NewReader(c.tr)
759 c.xtw = moxio.NewTraceWriter(c.log, "S: ", c)
760 c.xbw = bufio.NewWriter(c.xtw)
762 // Many IMAP connections use IDLE to wait for new incoming messages. We'll enable
763 // keepalive to get a higher chance of the connection staying alive, or otherwise
764 // detecting broken connections early.
767 tcpconn = nc.(*tls.Conn).NetConn()
769 if tc, ok := tcpconn.(*net.TCPConn); ok {
770 if err := tc.SetKeepAlivePeriod(5 * time.Minute); err != nil {
771 c.log.Errorx("setting keepalive period", err)
772 } else if err := tc.SetKeepAlive(true); err != nil {
773 c.log.Errorx("enabling keepalive", err)
777 c.log.Info("new connection",
778 slog.Any("remote", c.conn.RemoteAddr()),
779 slog.Any("local", c.conn.LocalAddr()),
780 slog.Bool("tls", xtls),
781 slog.Bool("viahttps", viaHTTPS),
782 slog.String("listener", listenerName))
785 err := c.conn.Close()
787 c.log.Debugx("closing connection", err)
790 if c.account != nil {
792 err := c.account.Close()
793 c.xsanity(err, "close account")
799 if x == nil || x == cleanClose {
800 c.log.Info("connection closed")
801 } else if err, ok := x.(error); ok && isClosed(err) {
802 c.log.Infox("connection closed", err)
804 c.log.Error("unhandled panic", slog.Any("err", x))
806 metrics.PanicInc(metrics.Imapserver)
807 unhandledPanics.Add(1) // For tests.
811 if xtls && !viaHTTPS {
812 // Start TLS on connection. We perform the handshake explicitly, so we can set a
813 // timeout, do client certificate authentication, log TLS details afterwards.
814 c.xtlsHandshakeAndAuthenticate(c.conn)
818 case <-mox.Shutdown.Done():
820 c.writelinef("* BYE mox shutting down")
825 if !limiterConnectionrate.Add(c.remoteIP, time.Now(), 1) {
826 c.writelinef("* BYE connection rate from your ip or network too high, slow down please")
830 // If remote IP/network resulted in too many authentication failures, refuse to serve.
831 if !mox.LimiterFailedAuth.CanAdd(c.remoteIP, time.Now(), 1) {
832 metrics.AuthenticationRatelimitedInc("imap")
833 c.log.Debug("refusing connection due to many auth failures", slog.Any("remoteip", c.remoteIP))
834 c.writelinef("* BYE too many auth failures")
838 if !limiterConnections.Add(c.remoteIP, time.Now(), 1) {
839 c.log.Debug("refusing connection due to many open connections", slog.Any("remoteip", c.remoteIP))
840 c.writelinef("* BYE too many open connections from your ip or network")
843 defer limiterConnections.Add(c.remoteIP, time.Now(), -1)
845 // We register and unregister the original connection, in case it c.conn is
846 // replaced with a TLS connection later on.
847 mox.Connections.Register(nc, "imap", listenerName)
848 defer mox.Connections.Unregister(nc)
850 if preauthAddress != "" {
851 acc, _, _, err := store.OpenEmail(c.log, preauthAddress, false)
853 c.log.Debugx("open account for preauth address", err, slog.String("address", preauthAddress))
854 c.writelinef("* BYE open account for address: %s", err)
857 c.username = preauthAddress
859 c.comm = store.RegisterComm(c.account)
862 if c.account != nil && !c.noPreauth {
863 c.state = stateAuthenticated
864 c.writelinef("* PREAUTH [CAPABILITY %s] mox imap welcomes %s", c.capabilities(), c.username)
866 c.writelinef("* OK [CAPABILITY %s] mox imap", c.capabilities())
869 // Ensure any pending loginAttempt is written before we stop.
871 if c.loginAttempt != nil {
872 store.LoginAttemptAdd(context.Background(), c.logbg(), *c.loginAttempt)
874 c.loginAttemptTime = time.Time{}
880 c.xflush() // For flushing errors, or commands that did not flush explicitly.
882 // Flush login attempt if it hasn't already been flushed by an ID command within 1s
883 // after authentication.
884 if c.loginAttempt != nil && (c.loginAttempt.UserAgent != "" || time.Since(c.loginAttemptTime) >= time.Second) {
885 store.LoginAttemptAdd(context.Background(), c.logbg(), *c.loginAttempt)
887 c.loginAttemptTime = time.Time{}
892// isClosed returns whether i/o failed, typically because the connection is closed.
893// For connection errors, we often want to generate fewer logs.
894func isClosed(err error) bool {
895 return errors.Is(err, errIO) || errors.Is(err, errProtocol) || mlog.IsClosed(err)
898// newLoginAttempt initializes a c.loginAttempt, for adding to the store after
899// filling in the results and other details.
900func (c *conn) newLoginAttempt(useTLS bool, authMech string) {
901 if c.loginAttempt != nil {
902 store.LoginAttemptAdd(context.Background(), c.logbg(), *c.loginAttempt)
905 c.loginAttemptTime = time.Now()
907 var state *tls.ConnectionState
908 if tc, ok := c.conn.(*tls.Conn); ok && useTLS {
909 v := tc.ConnectionState()
913 localAddr := c.conn.LocalAddr().String()
914 localIP, _, _ := net.SplitHostPort(localAddr)
919 c.loginAttempt = &store.LoginAttempt{
920 RemoteIP: c.remoteIP.String(),
922 TLS: store.LoginAttemptTLS(state),
924 UserAgent: c.userAgent, // May still be empty, to be filled in later.
926 Result: store.AuthError, // Replaced by caller.
930// makeTLSConfig makes a new tls config that is bound to the connection for
931// possible client certificate authentication.
932func (c *conn) makeTLSConfig() *tls.Config {
933 // We clone the config so we can set VerifyPeerCertificate below to a method bound
934 // to this connection. Earlier, we set session keys explicitly on the base TLS
935 // config, so they can be used for this connection too.
936 tlsConf := c.baseTLSConfig.Clone()
938 // Allow client certificate authentication, for use with the sasl "external"
939 // authentication mechanism.
940 tlsConf.ClientAuth = tls.RequestClientCert
942 // We verify the client certificate during the handshake. The TLS handshake is
943 // initiated explicitly for incoming connections and during starttls, so we can
944 // immediately extract the account name and address used for authentication.
945 tlsConf.VerifyPeerCertificate = c.tlsClientAuthVerifyPeerCert
950// tlsClientAuthVerifyPeerCert can be used as tls.Config.VerifyPeerCertificate, and
951// sets authentication-related fields on conn. This is not called on resumed TLS
953func (c *conn) tlsClientAuthVerifyPeerCert(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
954 if len(rawCerts) == 0 {
958 // If we had too many authentication failures from this IP, don't attempt
959 // authentication. If this is a new incoming connetion, it is closed after the TLS
961 if !mox.LimiterFailedAuth.CanAdd(c.remoteIP, time.Now(), 1) {
965 cert, err := x509.ParseCertificate(rawCerts[0])
967 c.log.Debugx("parsing tls client certificate", err)
970 if err := c.tlsClientAuthVerifyPeerCertParsed(cert); err != nil {
971 c.log.Debugx("verifying tls client certificate", err)
972 return fmt.Errorf("verifying client certificate: %w", err)
977// tlsClientAuthVerifyPeerCertParsed verifies a client certificate. Called both for
978// fresh and resumed TLS connections.
979func (c *conn) tlsClientAuthVerifyPeerCertParsed(cert *x509.Certificate) error {
980 if c.account != nil {
981 return fmt.Errorf("cannot authenticate with tls client certificate after previous authentication")
984 // 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.
985 c.newLoginAttempt(false, "tlsclientauth")
987 // Get TLS connection state in goroutine because we are called while performing the
988 // TLS handshake, which already has the tls connection locked.
989 conn := c.conn.(*tls.Conn)
990 la := *c.loginAttempt
992 logbg := c.logbg() // Evaluate attributes now, can't do it in goroutine.
995 // In case of panic don't take the whole program down.
998 c.log.Error("recover from panic", slog.Any("panic", x))
1000 metrics.PanicInc(metrics.Imapserver)
1004 state := conn.ConnectionState()
1005 la.TLS = store.LoginAttemptTLS(&state)
1006 store.LoginAttemptAdd(context.Background(), logbg, la)
1009 if la.Result == store.AuthSuccess {
1010 mox.LimiterFailedAuth.Reset(c.remoteIP, time.Now())
1012 mox.LimiterFailedAuth.Add(c.remoteIP, time.Now(), 1)
1016 // For many failed auth attempts, slow down verification attempts.
1017 if c.authFailed > 3 && authFailDelay > 0 {
1018 mox.Sleep(mox.Context, time.Duration(c.authFailed-3)*authFailDelay)
1020 c.authFailed++ // Compensated on success.
1022 // On the 3rd failed authentication, start responding slowly. Successful auth will
1023 // cause fast responses again.
1024 if c.authFailed >= 3 {
1029 shabuf := sha256.Sum256(cert.RawSubjectPublicKeyInfo)
1030 fp := base64.RawURLEncoding.EncodeToString(shabuf[:])
1031 c.loginAttempt.TLSPubKeyFingerprint = fp
1032 pubKey, err := store.TLSPublicKeyGet(context.TODO(), fp)
1034 if err == bstore.ErrAbsent {
1035 c.loginAttempt.Result = store.AuthBadCredentials
1037 return fmt.Errorf("looking up tls public key with fingerprint %s: %v", fp, err)
1039 c.loginAttempt.LoginAddress = pubKey.LoginAddress
1041 // Verify account exists and still matches address. We don't check for account
1042 // login being disabled if preauth is disabled. In that case, sasl external auth
1043 // will be done before credentials can be used, and login disabled will be checked
1044 // then, where it will result in a more helpful error message.
1045 checkLoginDisabled := !pubKey.NoIMAPPreauth
1046 acc, accName, _, err := store.OpenEmail(c.log, pubKey.LoginAddress, checkLoginDisabled)
1047 c.loginAttempt.AccountName = accName
1049 if errors.Is(err, store.ErrLoginDisabled) {
1050 c.loginAttempt.Result = store.AuthLoginDisabled
1052 // note: we cannot send a more helpful error message to the client.
1053 return fmt.Errorf("opening account for address %s for public key %s: %w", pubKey.LoginAddress, fp, err)
1058 c.xsanity(err, "close account")
1061 c.loginAttempt.AccountName = acc.Name
1062 if acc.Name != pubKey.Account {
1063 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)
1066 c.loginAttempt.Result = store.AuthSuccess
1069 c.noPreauth = pubKey.NoIMAPPreauth
1071 acc = nil // Prevent cleanup by defer.
1072 c.username = pubKey.LoginAddress
1073 c.comm = store.RegisterComm(c.account)
1074 c.log.Debug("tls client authenticated with client certificate",
1075 slog.String("fingerprint", fp),
1076 slog.String("username", c.username),
1077 slog.String("account", c.account.Name),
1078 slog.Any("remote", c.remoteIP))
1082// xtlsHandshakeAndAuthenticate performs the TLS handshake, and verifies a client
1083// certificate if present.
1084func (c *conn) xtlsHandshakeAndAuthenticate(conn net.Conn) {
1085 tlsConn := tls.Server(conn, c.makeTLSConfig())
1087 c.tr = moxio.NewTraceReader(c.log, "C: ", c.conn)
1088 c.br = bufio.NewReader(c.tr)
1090 cidctx := context.WithValue(mox.Context, mlog.CidKey, c.cid)
1091 ctx, cancel := context.WithTimeout(cidctx, time.Minute)
1093 c.log.Debug("starting tls server handshake")
1094 if err := tlsConn.HandshakeContext(ctx); err != nil {
1095 c.xbrokenf("tls handshake: %s (%w)", err, errIO)
1099 cs := tlsConn.ConnectionState()
1100 if cs.DidResume && len(cs.PeerCertificates) > 0 {
1101 // Verify client after session resumption.
1102 err := c.tlsClientAuthVerifyPeerCertParsed(cs.PeerCertificates[0])
1104 c.writelinef("* BYE [ALERT] Error verifying client certificate after TLS session resumption: %s", err)
1105 c.xbrokenf("tls verify client certificate after resumption: %s (%w)", err, errIO)
1109 version, ciphersuite := moxio.TLSInfo(cs)
1110 attrs := []slog.Attr{
1111 slog.String("version", version),
1112 slog.String("ciphersuite", ciphersuite),
1113 slog.String("sni", cs.ServerName),
1114 slog.Bool("resumed", cs.DidResume),
1115 slog.Int("clientcerts", len(cs.PeerCertificates)),
1117 if c.account != nil {
1118 attrs = append(attrs,
1119 slog.String("account", c.account.Name),
1120 slog.String("username", c.username),
1123 c.log.Debug("tls handshake completed", attrs...)
1126func (c *conn) command() {
1127 var tag, cmd, cmdlow string
1133 metricIMAPCommands.WithLabelValues(c.cmdMetric, result).Observe(float64(time.Since(c.cmdStart)) / float64(time.Second))
1136 logFields := []slog.Attr{
1137 slog.String("cmd", c.cmd),
1138 slog.Duration("duration", time.Since(c.cmdStart)),
1143 if x == nil || x == cleanClose {
1144 c.log.Debug("imap command done", logFields...)
1146 if x == cleanClose {
1147 // If compression was enabled, we flush & close the deflate stream.
1149 // Note: Close and flush can Write and may panic with an i/o error.
1150 if err := c.xflateWriter.Close(); err != nil {
1151 c.log.Debugx("close deflate writer", err)
1152 } else if err := c.xflateBW.Flush(); err != nil {
1153 c.log.Debugx("flush deflate buffer", err)
1161 err, ok := x.(error)
1163 c.log.Error("imap command panic", append([]slog.Attr{slog.Any("panic", x)}, logFields...)...)
1168 var sxerr syntaxError
1170 var serr serverError
1172 c.log.Infox("imap command ioerror", err, logFields...)
1174 if errors.Is(err, errProtocol) {
1178 } else if errors.As(err, &sxerr) {
1179 result = "badsyntax"
1181 // Other side is likely speaking something else than IMAP, send error message and
1182 // stop processing because there is a good chance whatever they sent has multiple
1184 c.writelinef("* BYE please try again speaking imap")
1185 c.xbrokenf("not speaking imap (%w)", errIO)
1187 c.log.Debugx("imap command syntax error", sxerr.err, logFields...)
1188 c.log.Info("imap syntax error", slog.String("lastline", c.lastLine))
1189 fatal := strings.HasSuffix(c.lastLine, "+}")
1191 err := c.conn.SetWriteDeadline(time.Now().Add(5 * time.Second))
1192 c.log.Check(err, "setting write deadline")
1194 if sxerr.line != "" {
1195 c.bwritelinef("%s", sxerr.line)
1198 if sxerr.code != "" {
1199 code = "[" + sxerr.code + "] "
1201 c.bwriteresultf("%s BAD %s%s unrecognized syntax/command: %v", tag, code, cmd, sxerr.errmsg)
1204 panic(fmt.Errorf("aborting connection after syntax error for command with non-sync literal: %w", errProtocol))
1206 } else if errors.As(err, &serr) {
1207 result = "servererror"
1208 c.log.Errorx("imap command server error", err, logFields...)
1210 c.bwriteresultf("%s NO %s %v", tag, cmd, err)
1211 } else if errors.As(err, &uerr) {
1212 result = "usererror"
1213 c.log.Debugx("imap command user error", err, logFields...)
1214 if uerr.code != "" {
1215 c.bwriteresultf("%s NO [%s] %s %v", tag, uerr.code, cmd, err)
1217 c.bwriteresultf("%s NO %s %v", tag, cmd, err)
1220 // Other type of panic, we pass it on, aborting the connection.
1222 c.log.Errorx("imap command panic", err, logFields...)
1228 cmd, p = c.readCommand(&tag)
1229 cmdlow = strings.ToLower(cmd)
1231 c.cmdStart = time.Now()
1232 c.cmdMetric = "(unrecognized)"
1235 case <-mox.Shutdown.Done():
1237 c.writelinef("* BYE shutting down")
1238 c.xbrokenf("shutting down (%w)", errIO)
1242 fn := commands[cmdlow]
1244 xsyntaxErrorf("unknown command %q", cmd)
1249 // Check if command is allowed in this state.
1250 if _, ok1 := commandsStateAny[cmdlow]; ok1 {
1251 } else if _, ok2 := commandsStateNotAuthenticated[cmdlow]; ok2 && c.state == stateNotAuthenticated {
1252 } else if _, ok3 := commandsStateAuthenticated[cmdlow]; ok3 && c.state == stateAuthenticated || c.state == stateSelected {
1253 } else if _, ok4 := commandsStateSelected[cmdlow]; ok4 && c.state == stateSelected {
1254 } else if ok1 || ok2 || ok3 || ok4 {
1255 xuserErrorf("not allowed in this connection state")
1257 xserverErrorf("unrecognized command")
1263func (c *conn) broadcast(changes []store.Change) {
1264 if len(changes) == 0 {
1267 c.log.Debug("broadcast changes", slog.Any("changes", changes))
1268 c.comm.Broadcast(changes)
1271// matchStringer matches a string against reference + mailbox patterns.
1272type matchStringer interface {
1273 MatchString(s string) bool
1276type noMatch struct{}
1278// MatchString for noMatch always returns false.
1279func (noMatch) MatchString(s string) bool {
1283// xmailboxPatternMatcher returns a matcher for mailbox names given the reference and patterns.
1284// Patterns can include "%" and "*", matching any character excluding and including a slash respectively.
1285func xmailboxPatternMatcher(ref string, patterns []string) matchStringer {
1286 if strings.HasPrefix(ref, "/") {
1291 for _, pat := range patterns {
1292 if strings.HasPrefix(pat, "/") {
1298 s = path.Join(ref, pat)
1301 // Fix casing for all Inbox paths.
1302 first := strings.SplitN(s, "/", 2)[0]
1303 if strings.EqualFold(first, "Inbox") {
1304 s = "Inbox" + s[len("Inbox"):]
1309 for _, c := range s {
1312 } else if c == '*' {
1315 rs += regexp.QuoteMeta(string(c))
1318 subs = append(subs, rs)
1324 rs := "^(" + strings.Join(subs, "|") + ")$"
1325 re, err := regexp.Compile(rs)
1326 xcheckf(err, "compiling regexp for mailbox patterns")
1330func (c *conn) sequence(uid store.UID) msgseq {
1331 return uidSearch(c.uids, uid)
1334func uidSearch(uids []store.UID, uid store.UID) msgseq {
1341 return msgseq(i + 1)
1351func (c *conn) xsequence(uid store.UID) msgseq {
1352 seq := c.sequence(uid)
1354 xserverErrorf("unknown uid %d (%w)", uid, errProtocol)
1359func (c *conn) sequenceRemove(seq msgseq, uid store.UID) {
1361 if c.uids[i] != uid {
1362 xserverErrorf("got uid %d at msgseq %d, expected uid %d", uid, seq, c.uids[i])
1364 copy(c.uids[i:], c.uids[i+1:])
1365 c.uids = c.uids[:len(c.uids)-1]
1371// add uid to the session. care must be taken that pending changes are fetched
1372// while holding the account wlock, and applied before adding this uid, because
1373// those pending changes may contain another new uid that has to be added first.
1374func (c *conn) uidAppend(uid store.UID) {
1375 if uidSearch(c.uids, uid) > 0 {
1376 xserverErrorf("uid already present (%w)", errProtocol)
1378 if len(c.uids) > 0 && uid < c.uids[len(c.uids)-1] {
1379 xserverErrorf("new uid %d is smaller than last uid %d (%w)", uid, c.uids[len(c.uids)-1], errProtocol)
1381 c.uids = append(c.uids, uid)
1387// sanity check that uids are in ascending order.
1388func checkUIDs(uids []store.UID) {
1389 for i, uid := range uids {
1390 if uid == 0 || i > 0 && uid <= uids[i-1] {
1391 xserverErrorf("bad uids %v", uids)
1396func (c *conn) xnumSetUIDs(isUID bool, nums numSet) []store.UID {
1397 _, uids := c.xnumSetConditionUIDs(false, true, isUID, nums)
1401func (c *conn) xnumSetCondition(isUID bool, nums numSet) []any {
1402 uidargs, _ := c.xnumSetConditionUIDs(true, false, isUID, nums)
1406func (c *conn) xnumSetConditionUIDs(forDB, returnUIDs bool, isUID bool, nums numSet) ([]any, []store.UID) {
1407 if nums.searchResult {
1408 // Update previously stored UIDs. Some may have been deleted.
1409 // Once deleted a UID will never come back, so we'll just remove those uids.
1411 for _, uid := range c.searchResult {
1412 if uidSearch(c.uids, uid) > 0 {
1413 c.searchResult[o] = uid
1417 c.searchResult = c.searchResult[:o]
1418 uidargs := make([]any, len(c.searchResult))
1419 for i, uid := range c.searchResult {
1422 return uidargs, c.searchResult
1426 var uids []store.UID
1428 add := func(uid store.UID) {
1430 uidargs = append(uidargs, uid)
1433 uids = append(uids, uid)
1438 // Sequence numbers that don't exist, or * on an empty mailbox, should result in a BAD response.
../rfc/9051:7018
1439 for _, r := range nums.ranges {
1442 if len(c.uids) == 0 {
1443 xsyntaxErrorf("invalid seqset * on empty mailbox")
1445 ia = len(c.uids) - 1
1447 ia = int(r.first.number - 1)
1448 if ia >= len(c.uids) {
1449 xsyntaxErrorf("msgseq %d not in mailbox", r.first.number)
1458 if len(c.uids) == 0 {
1459 xsyntaxErrorf("invalid seqset * on empty mailbox")
1461 ib = len(c.uids) - 1
1463 ib = int(r.last.number - 1)
1464 if ib >= len(c.uids) {
1465 xsyntaxErrorf("msgseq %d not in mailbox", r.last.number)
1471 for _, uid := range c.uids[ia : ib+1] {
1475 return uidargs, uids
1478 // UIDs that do not exist can be ignored.
1479 if len(c.uids) == 0 {
1483 for _, r := range nums.ranges {
1489 uida := store.UID(r.first.number)
1491 uida = c.uids[len(c.uids)-1]
1494 uidb := store.UID(last.number)
1496 uidb = c.uids[len(c.uids)-1]
1500 uida, uidb = uidb, uida
1503 // Binary search for uida.
1508 if uida < c.uids[m] {
1510 } else if uida > c.uids[m] {
1517 for _, uid := range c.uids[s:] {
1518 if uid >= uida && uid <= uidb {
1520 } else if uid > uidb {
1526 return uidargs, uids
1529func (c *conn) ok(tag, cmd string) {
1530 c.bwriteresultf("%s OK %s done", tag, cmd)
1534// xcheckmailboxname checks if name is valid, returning an INBOX-normalized name.
1535// I.e. it changes various casings of INBOX and INBOX/* to Inbox and Inbox/*.
1536// Name is invalid if it contains leading/trailing/double slashes, or when it isn't
1537// unicode-normalized, or when empty or has special characters.
1538func xcheckmailboxname(name string, allowInbox bool) string {
1539 name, isinbox, err := store.CheckMailboxName(name, allowInbox)
1541 xuserErrorf("special mailboxname Inbox not allowed")
1542 } else if err != nil {
1543 xusercodeErrorf("CANNOT", "%s", err)
1548// Lookup mailbox by name.
1549// If the mailbox does not exist, panic is called with a user error.
1550// Must be called with account rlock held.
1551func (c *conn) xmailbox(tx *bstore.Tx, name string, missingErrCode string) store.Mailbox {
1552 mb, err := c.account.MailboxFind(tx, name)
1553 xcheckf(err, "finding mailbox")
1555 // missingErrCode can be empty, or e.g. TRYCREATE or ALREADYEXISTS.
1556 xusercodeErrorf(missingErrCode, "%w", store.ErrUnknownMailbox)
1561// Lookup mailbox by ID.
1562// If the mailbox does not exist, panic is called with a user error.
1563// Must be called with account rlock held.
1564func (c *conn) xmailboxID(tx *bstore.Tx, id int64) store.Mailbox {
1565 mb, err := store.MailboxID(tx, id)
1566 if err == bstore.ErrAbsent {
1567 xuserErrorf("%w", store.ErrUnknownMailbox)
1568 } else if err == store.ErrMailboxExpunged {
1570 xusercodeErrorf("NONEXISTENT", "mailbox has been deleted")
1575// Apply changes to our session state.
1576// If initial is false, updates like EXISTS and EXPUNGE are written to the client.
1577// If initial is true, we only apply the changes.
1578// Should not be called while holding locks, as changes are written to client connections, which can block.
1579// Does not flush output.
1580func (c *conn) applyChanges(changes []store.Change, initial bool) {
1581 if len(changes) == 0 {
1585 err := c.conn.SetWriteDeadline(time.Now().Add(5 * time.Minute))
1586 c.log.Check(err, "setting write deadline")
1588 c.log.Debug("applying changes", slog.Any("changes", changes))
1590 // Only keep changes for the selected mailbox, and changes that are always relevant.
1591 var n []store.Change
1592 for _, change := range changes {
1594 switch ch := change.(type) {
1595 case store.ChangeAddUID:
1597 case store.ChangeRemoveUIDs:
1599 c.comm.RemovalSeen(ch)
1600 case store.ChangeFlags:
1602 case store.ChangeRemoveMailbox, store.ChangeAddMailbox, store.ChangeRenameMailbox, store.ChangeAddSubscription:
1603 n = append(n, change)
1605 case store.ChangeAnnotation:
1606 // note: annotations may have a mailbox associated with them, but we pass all
1609 if c.enabled[capMetadata] {
1610 n = append(n, change)
1613 case store.ChangeMailboxCounts, store.ChangeMailboxSpecialUse, store.ChangeMailboxKeywords, store.ChangeThread:
1615 panic(fmt.Errorf("missing case for %#v", change))
1617 if c.state == stateSelected && mbID == c.mailboxID {
1618 n = append(n, change)
1623 qresync := c.enabled[capQresync]
1624 condstore := c.enabled[capCondstore]
1627 for i < len(changes) {
1628 // First process all new uids. So we only send a single EXISTS.
1629 var adds []store.ChangeAddUID
1630 for ; i < len(changes); i++ {
1631 ch, ok := changes[i].(store.ChangeAddUID)
1635 seq := c.sequence(ch.UID)
1636 if seq > 0 && initial {
1640 adds = append(adds, ch)
1646 // Write the exists, and the UID and flags as well. Hopefully the client waits for
1647 // long enough after the EXISTS to see these messages, and doesn't request them
1648 // again with a FETCH.
1649 c.bwritelinef("* %d EXISTS", len(c.uids))
1650 for _, add := range adds {
1651 seq := c.xsequence(add.UID)
1652 var modseqStr string
1654 modseqStr = fmt.Sprintf(" MODSEQ (%d)", add.ModSeq.Client())
1656 c.bwritelinef("* %d FETCH (UID %d FLAGS %s%s)", seq, add.UID, flaglist(add.Flags, add.Keywords).pack(c), modseqStr)
1661 change := changes[i]
1664 switch ch := change.(type) {
1665 case store.ChangeRemoveUIDs:
1666 var vanishedUIDs numSet
1667 for _, uid := range ch.UIDs {
1670 seq = c.sequence(uid)
1675 seq = c.xsequence(uid)
1677 c.sequenceRemove(seq, uid)
1680 vanishedUIDs.append(uint32(uid))
1682 c.bwritelinef("* %d EXPUNGE", seq)
1688 for _, s := range vanishedUIDs.Strings(4*1024 - 32) {
1689 c.bwritelinef("* VANISHED %s", s)
1692 case store.ChangeFlags:
1693 // The uid can be unknown if we just expunged it while another session marked it as deleted just before.
1694 seq := c.sequence(ch.UID)
1699 var modseqStr string
1701 modseqStr = fmt.Sprintf(" MODSEQ (%d)", ch.ModSeq.Client())
1703 c.bwritelinef("* %d FETCH (UID %d FLAGS %s%s)", seq, ch.UID, flaglist(ch.Flags, ch.Keywords).pack(c), modseqStr)
1705 case store.ChangeRemoveMailbox:
1706 // Only announce \NonExistent to modern clients, otherwise they may ignore the
1707 // unrecognized \NonExistent and interpret this as a newly created mailbox, while
1708 // the goal was to remove it...
1709 if c.enabled[capIMAP4rev2] {
1710 c.bwritelinef(`* LIST (\NonExistent) "/" %s`, mailboxt(ch.Name).pack(c))
1712 case store.ChangeAddMailbox:
1713 c.bwritelinef(`* LIST (%s) "/" %s`, strings.Join(ch.Flags, " "), mailboxt(ch.Mailbox.Name).pack(c))
1714 case store.ChangeRenameMailbox:
1717 if c.enabled[capIMAP4rev2] {
1718 oldname = fmt.Sprintf(` ("OLDNAME" (%s))`, mailboxt(ch.OldName).pack(c))
1720 c.bwritelinef(`* LIST (%s) "/" %s%s`, strings.Join(ch.Flags, " "), mailboxt(ch.NewName).pack(c), oldname)
1721 case store.ChangeAddSubscription:
1722 c.bwritelinef(`* LIST (%s) "/" %s`, strings.Join(append([]string{`\Subscribed`}, ch.Flags...), " "), mailboxt(ch.Name).pack(c))
1723 case store.ChangeAnnotation:
1725 c.bwritelinef(`* METADATA %s %s`, mailboxt(ch.MailboxName).pack(c), astring(ch.Key).pack(c))
1727 panic(fmt.Sprintf("internal error, missing case for %#v", change))
1732// Capability returns the capabilities this server implements and currently has
1733// available given the connection state.
1736func (c *conn) cmdCapability(tag, cmd string, p *parser) {
1742 caps := c.capabilities()
1745 c.bwritelinef("* CAPABILITY %s", caps)
1749// capabilities returns non-empty string with available capabilities based on connection state.
1750// For use in cmdCapability and untagged OK responses on connection start, login and authenticate.
1751func (c *conn) capabilities() string {
1752 caps := serverCapabilities
1754 // We only allow starting without TLS when explicitly configured, in violation of RFC.
1755 if !c.tls && c.baseTLSConfig != nil {
1758 if c.tls || c.noRequireSTARTTLS {
1759 caps += " AUTH=PLAIN"
1761 caps += " LOGINDISABLED"
1763 if c.tls && len(c.conn.(*tls.Conn).ConnectionState().PeerCertificates) > 0 && !c.viaHTTPS {
1764 caps += " AUTH=EXTERNAL"
1769// No op, but useful for retrieving pending changes as untagged responses, e.g. of
1773func (c *conn) cmdNoop(tag, cmd string, p *parser) {
1781// Logout, after which server closes the connection.
1784func (c *conn) cmdLogout(tag, cmd string, p *parser) {
1791 c.state = stateNotAuthenticated
1793 c.bwritelinef("* BYE thanks")
1798// Clients can use ID to tell the server which software they are using. Servers can
1799// respond with their version. For statistics/logging/debugging purposes.
1802func (c *conn) cmdID(tag, cmd string, p *parser) {
1807 var params map[string]string
1810 params = map[string]string{}
1812 if len(params) > 0 {
1818 if _, ok := params[k]; ok {
1819 xsyntaxErrorf("duplicate key %q", k)
1822 values = append(values, fmt.Sprintf("%s=%q", k, v))
1829 c.userAgent = strings.Join(values, " ")
1831 // The ID command is typically sent soon after authentication. So we've prepared
1832 // the LoginAttempt and write it now.
1833 if c.loginAttempt != nil {
1834 c.loginAttempt.UserAgent = c.userAgent
1835 store.LoginAttemptAdd(context.Background(), c.logbg(), *c.loginAttempt)
1836 c.loginAttempt = nil
1837 c.loginAttemptTime = time.Time{}
1840 // We just log the client id.
1841 c.log.Info("client id", slog.Any("params", params))
1845 if c.state == stateAuthenticated || c.state == stateSelected {
1846 c.bwritelinef(`* ID ("name" "mox" "version" %s)`, string0(moxvar.Version).pack(c))
1848 c.bwritelinef(`* ID ("name" "mox")`)
1853// Compress enables compression on the connection. Deflate is the only algorithm
1854// specified. TLS doesn't do compression nowadays, so we don't have to check for that.
1856// Status: Authenticated. The RFC doesn't mention this in prose, but the command is
1857// added to ABNF production rule "command-auth".
1858func (c *conn) cmdCompress(tag, cmd string, p *parser) {
1866 // Will do compression only once.
1869 xusercodeErrorf("COMPRESSIONACTIVE", "compression already active with previous compress command")
1872 if !strings.EqualFold(alg, "deflate") {
1873 xuserErrorf("compression algorithm not supported")
1876 // We must flush now, before we initialize flate.
1877 c.log.Debug("compression enabled")
1880 c.xflateBW = bufio.NewWriter(c)
1881 fw0, err := flate.NewWriter(c.xflateBW, flate.DefaultCompression)
1882 xcheckf(err, "deflate") // Cannot happen.
1883 xfw := moxio.NewFlateWriter(fw0)
1886 c.xflateWriter = xfw
1887 c.xtw = moxio.NewTraceWriter(c.log, "S: ", c.xflateWriter)
1888 c.xbw = bufio.NewWriter(c.xtw) // The previous c.xbw will not have buffered data.
1890 rc := xprefixConn(c.conn, c.br) // c.br may contain buffered data.
1891 // We use the special partial reader. Some clients write commands and flush the
1892 // buffer in "partial flush" mode instead of "sync flush" mode. The "sync flush"
1893 // mode emits an explicit zero-length data block that triggers the Go stdlib flate
1894 // reader to return data to us. It wouldn't for blocks written in "partial flush"
1895 // mode, and it would block us indefinitely while trying to read another flate
1896 // block. The partial reader returns data earlier, but still eagerly consumes all
1897 // blocks in its buffer.
1898 // 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.
1899 fr := flate.NewReaderPartial(rc)
1900 c.tr = moxio.NewTraceReader(c.log, "C: ", fr)
1901 c.br = bufio.NewReader(c.tr)
1904// STARTTLS enables TLS on the connection, after a plain text start.
1905// Only allowed if TLS isn't already enabled, either through connecting to a
1906// TLS-enabled TCP port, or a previous STARTTLS command.
1907// After STARTTLS, plain text authentication typically becomes available.
1909// Status: Not authenticated.
1910func (c *conn) cmdStarttls(tag, cmd string, p *parser) {
1919 if c.baseTLSConfig == nil {
1920 xsyntaxErrorf("starttls not announced")
1923 conn := xprefixConn(c.conn, c.br)
1924 // We add the cid to facilitate debugging in case of TLS connection failure.
1925 c.ok(tag, cmd+" ("+mox.ReceivedID(c.cid)+")")
1927 c.xtlsHandshakeAndAuthenticate(conn)
1930 // We are not sending unsolicited CAPABILITIES for newly available authentication
1931 // mechanisms, clients can't depend on us sending it and should ask it themselves.
1935// Authenticate using SASL. Supports multiple back and forths between client and
1936// server to finish authentication, unlike LOGIN which is just a single
1937// username/password.
1939// We may already have ambient TLS credentials that have not been activated.
1941// Status: Not authenticated.
1942func (c *conn) cmdAuthenticate(tag, cmd string, p *parser) {
1946 // For many failed auth attempts, slow down verification attempts.
1947 if c.authFailed > 3 && authFailDelay > 0 {
1948 mox.Sleep(mox.Context, time.Duration(c.authFailed-3)*authFailDelay)
1951 // If authentication fails due to missing derived secrets, we don't hold it against
1952 // the connection. There is no way to indicate server support for an authentication
1953 // mechanism, but that a mechanism won't work for an account.
1954 var missingDerivedSecrets bool
1956 c.authFailed++ // Compensated on success.
1958 if missingDerivedSecrets {
1961 // On the 3rd failed authentication, start responding slowly. Successful auth will
1962 // cause fast responses again.
1963 if c.authFailed >= 3 {
1968 c.newLoginAttempt(true, "")
1970 if c.loginAttempt.Result == store.AuthSuccess {
1971 mox.LimiterFailedAuth.Reset(c.remoteIP, time.Now())
1972 } else if !missingDerivedSecrets {
1973 mox.LimiterFailedAuth.Add(c.remoteIP, time.Now(), 1)
1979 authType := p.xatom()
1981 xreadInitial := func() []byte {
1985 line = c.readline(false)
1989 line = p.remainder()
1992 line = "" // Base64 decode will result in empty buffer.
1997 c.loginAttempt.Result = store.AuthAborted
1998 xsyntaxErrorf("authenticate aborted by client")
2000 buf, err := base64.StdEncoding.DecodeString(line)
2002 xsyntaxErrorf("parsing base64: %v", err)
2007 xreadContinuation := func() []byte {
2008 line := c.readline(false)
2010 c.loginAttempt.Result = store.AuthAborted
2011 xsyntaxErrorf("authenticate aborted by client")
2013 buf, err := base64.StdEncoding.DecodeString(line)
2015 xsyntaxErrorf("parsing base64: %v", err)
2020 // The various authentication mechanisms set account and username. We may already
2021 // have an account and username from TLS client authentication. Afterwards, we
2022 // check that the account is the same.
2023 var account *store.Account
2027 err := account.Close()
2028 c.xsanity(err, "close account")
2032 switch strings.ToUpper(authType) {
2034 c.loginAttempt.AuthMech = "plain"
2036 if !c.noRequireSTARTTLS && !c.tls {
2038 xusercodeErrorf("PRIVACYREQUIRED", "tls required for login")
2041 // Plain text passwords, mark as traceauth.
2042 defer c.xtrace(mlog.LevelTraceauth)()
2043 buf := xreadInitial()
2044 c.xtrace(mlog.LevelTrace) // Restore.
2045 plain := bytes.Split(buf, []byte{0})
2046 if len(plain) != 3 {
2047 xsyntaxErrorf("bad plain auth data, expected 3 nul-separated tokens, got %d tokens", len(plain))
2049 authz := norm.NFC.String(string(plain[0]))
2050 username = norm.NFC.String(string(plain[1]))
2051 password := string(plain[2])
2052 c.loginAttempt.LoginAddress = username
2054 if authz != "" && authz != username {
2055 xusercodeErrorf("AUTHORIZATIONFAILED", "cannot assume role")
2059 account, c.loginAttempt.AccountName, err = store.OpenEmailAuth(c.log, username, password, false)
2061 if errors.Is(err, store.ErrUnknownCredentials) {
2062 c.loginAttempt.Result = store.AuthBadCredentials
2063 c.log.Info("authentication failed", slog.String("username", username))
2064 xusercodeErrorf("AUTHENTICATIONFAILED", "bad credentials")
2066 xusercodeErrorf("", "error")
2070 c.loginAttempt.AuthMech = strings.ToLower(authType)
2076 chal := fmt.Sprintf("<%d.%d@%s>", uint64(mox.CryptoRandInt()), time.Now().UnixNano(), mox.Conf.Static.HostnameDomain.ASCII)
2077 c.writelinef("+ %s", base64.StdEncoding.EncodeToString([]byte(chal)))
2079 resp := xreadContinuation()
2080 t := strings.Split(string(resp), " ")
2081 if len(t) != 2 || len(t[1]) != 2*md5.Size {
2082 xsyntaxErrorf("malformed cram-md5 response")
2084 username = norm.NFC.String(t[0])
2085 c.loginAttempt.LoginAddress = username
2086 c.log.Debug("cram-md5 auth", slog.String("address", username))
2088 account, c.loginAttempt.AccountName, _, err = store.OpenEmail(c.log, username, false)
2090 if errors.Is(err, store.ErrUnknownCredentials) {
2091 c.loginAttempt.Result = store.AuthBadCredentials
2092 c.log.Info("failed authentication attempt", slog.String("username", username), slog.Any("remote", c.remoteIP))
2093 xusercodeErrorf("AUTHENTICATIONFAILED", "bad credentials")
2095 xserverErrorf("looking up address: %v", err)
2097 var ipadhash, opadhash hash.Hash
2098 account.WithRLock(func() {
2099 err := account.DB.Read(context.TODO(), func(tx *bstore.Tx) error {
2100 password, err := bstore.QueryTx[store.Password](tx).Get()
2101 if err == bstore.ErrAbsent {
2102 c.log.Info("failed authentication attempt", slog.String("username", username), slog.Any("remote", c.remoteIP))
2103 xusercodeErrorf("AUTHENTICATIONFAILED", "bad credentials")
2109 ipadhash = password.CRAMMD5.Ipad
2110 opadhash = password.CRAMMD5.Opad
2113 xcheckf(err, "tx read")
2115 if ipadhash == nil || opadhash == nil {
2116 c.log.Info("cram-md5 auth attempt without derived secrets set, save password again to store secrets", slog.String("username", username))
2117 c.log.Info("failed authentication attempt", slog.String("username", username), slog.Any("remote", c.remoteIP))
2118 missingDerivedSecrets = true
2119 xusercodeErrorf("AUTHENTICATIONFAILED", "bad credentials")
2123 ipadhash.Write([]byte(chal))
2124 opadhash.Write(ipadhash.Sum(nil))
2125 digest := fmt.Sprintf("%x", opadhash.Sum(nil))
2127 c.log.Info("failed authentication attempt", slog.String("username", username), slog.Any("remote", c.remoteIP))
2128 xusercodeErrorf("AUTHENTICATIONFAILED", "bad credentials")
2131 case "SCRAM-SHA-256-PLUS", "SCRAM-SHA-256", "SCRAM-SHA-1-PLUS", "SCRAM-SHA-1":
2132 // 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?
2133 // todo: use single implementation between ../imapserver/server.go and ../smtpserver/server.go
2135 // No plaintext credentials, we can log these normally.
2137 c.loginAttempt.AuthMech = strings.ToLower(authType)
2138 var h func() hash.Hash
2139 switch c.loginAttempt.AuthMech {
2140 case "scram-sha-1", "scram-sha-1-plus":
2142 case "scram-sha-256", "scram-sha-256-plus":
2145 xserverErrorf("missing case for scram variant")
2148 var cs *tls.ConnectionState
2149 requireChannelBinding := strings.HasSuffix(c.loginAttempt.AuthMech, "-plus")
2150 if requireChannelBinding && !c.tls {
2151 xuserErrorf("cannot use plus variant with tls channel binding without tls")
2154 xcs := c.conn.(*tls.Conn).ConnectionState()
2157 c0 := xreadInitial()
2158 ss, err := scram.NewServer(h, c0, cs, requireChannelBinding)
2160 c.log.Infox("scram protocol error", err, slog.Any("remote", c.remoteIP))
2161 xuserErrorf("scram protocol error: %s", err)
2163 username = ss.Authentication
2164 c.loginAttempt.LoginAddress = username
2165 c.log.Debug("scram auth", slog.String("authentication", username))
2166 // We check for login being disabled when finishing.
2167 account, c.loginAttempt.AccountName, _, err = store.OpenEmail(c.log, username, false)
2169 // todo: we could continue scram with a generated salt, deterministically generated
2170 // from the username. that way we don't have to store anything but attackers cannot
2171 // learn if an account exists. same for absent scram saltedpassword below.
2172 xuserErrorf("scram not possible")
2174 if ss.Authorization != "" && ss.Authorization != username {
2175 xuserErrorf("authentication with authorization for different user not supported")
2177 var xscram store.SCRAM
2178 account.WithRLock(func() {
2179 err := account.DB.Read(context.TODO(), func(tx *bstore.Tx) error {
2180 password, err := bstore.QueryTx[store.Password](tx).Get()
2181 if err == bstore.ErrAbsent {
2182 c.log.Info("failed authentication attempt", slog.String("username", username), slog.Any("remote", c.remoteIP))
2183 xusercodeErrorf("AUTHENTICATIONFAILED", "bad credentials")
2185 xcheckf(err, "fetching credentials")
2186 switch c.loginAttempt.AuthMech {
2187 case "scram-sha-1", "scram-sha-1-plus":
2188 xscram = password.SCRAMSHA1
2189 case "scram-sha-256", "scram-sha-256-plus":
2190 xscram = password.SCRAMSHA256
2192 xserverErrorf("missing case for scram credentials")
2194 if len(xscram.Salt) == 0 || xscram.Iterations == 0 || len(xscram.SaltedPassword) == 0 {
2195 missingDerivedSecrets = true
2196 c.log.Info("scram auth attempt without derived secrets set, save password again to store secrets", slog.String("username", username))
2197 xuserErrorf("scram not possible")
2201 xcheckf(err, "read tx")
2203 s1, err := ss.ServerFirst(xscram.Iterations, xscram.Salt)
2204 xcheckf(err, "scram first server step")
2205 c.writelinef("+ %s", base64.StdEncoding.EncodeToString([]byte(s1)))
2206 c2 := xreadContinuation()
2207 s3, err := ss.Finish(c2, xscram.SaltedPassword)
2209 c.writelinef("+ %s", base64.StdEncoding.EncodeToString([]byte(s3)))
2212 c.readline(false) // Should be "*" for cancellation.
2213 if errors.Is(err, scram.ErrInvalidProof) {
2214 c.loginAttempt.Result = store.AuthBadCredentials
2215 c.log.Info("failed authentication attempt", slog.String("username", username), slog.Any("remote", c.remoteIP))
2216 xusercodeErrorf("AUTHENTICATIONFAILED", "bad credentials")
2217 } else if errors.Is(err, scram.ErrChannelBindingsDontMatch) {
2218 c.loginAttempt.Result = store.AuthBadChannelBinding
2219 c.log.Warn("bad channel binding during authentication, potential mitm", slog.String("username", username), slog.Any("remote", c.remoteIP))
2220 xusercodeErrorf("AUTHENTICATIONFAILED", "channel bindings do not match, potential mitm")
2221 } else if errors.Is(err, scram.ErrInvalidEncoding) {
2222 c.loginAttempt.Result = store.AuthBadProtocol
2223 c.log.Infox("bad scram protocol message", err, slog.String("username", username), slog.Any("remote", c.remoteIP))
2224 xuserErrorf("bad scram protocol message: %s", err)
2226 xuserErrorf("server final: %w", err)
2230 // The message should be empty. todo: should we require it is empty?
2234 c.loginAttempt.AuthMech = "external"
2237 buf := xreadInitial()
2238 username = norm.NFC.String(string(buf))
2239 c.loginAttempt.LoginAddress = username
2242 xusercodeErrorf("AUTHENTICATIONFAILED", "tls required for tls client certificate authentication")
2244 if c.account == nil {
2245 xusercodeErrorf("AUTHENTICATIONFAILED", "missing client certificate, required for tls client certificate authentication")
2249 username = c.username
2250 c.loginAttempt.LoginAddress = username
2253 account, c.loginAttempt.AccountName, _, err = store.OpenEmail(c.log, username, false)
2254 xcheckf(err, "looking up username from tls client authentication")
2257 c.loginAttempt.AuthMech = "(unrecognized)"
2258 xuserErrorf("method not supported")
2261 if accConf, ok := account.Conf(); !ok {
2262 xserverErrorf("cannot get account config")
2263 } else if accConf.LoginDisabled != "" {
2264 c.loginAttempt.Result = store.AuthLoginDisabled
2265 c.log.Info("account login disabled", slog.String("username", username))
2266 // No AUTHENTICATIONFAILED code, clients could prompt users for different password.
2267 xuserErrorf("%w: %s", store.ErrLoginDisabled, accConf.LoginDisabled)
2270 // We may already have TLS credentials. They won't have been enabled, or we could
2271 // get here due to the state machine that doesn't allow authentication while being
2272 // authenticated. But allow another SASL authentication, but it has to be for the
2273 // same account. It can be for a different username (email address) of the account.
2274 if c.account != nil {
2275 if account != c.account {
2276 c.log.Debug("sasl authentication for different account than tls client authentication, aborting connection",
2277 slog.String("saslmechanism", c.loginAttempt.AuthMech),
2278 slog.String("saslaccount", account.Name),
2279 slog.String("tlsaccount", c.account.Name),
2280 slog.String("saslusername", username),
2281 slog.String("tlsusername", c.username),
2283 xusercodeErrorf("AUTHENTICATIONFAILED", "authentication failed, tls client certificate public key belongs to another account")
2284 } else if username != c.username {
2285 c.log.Debug("sasl authentication for different username than tls client certificate authentication, switching to sasl username",
2286 slog.String("saslmechanism", c.loginAttempt.AuthMech),
2287 slog.String("saslusername", username),
2288 slog.String("tlsusername", c.username),
2289 slog.String("account", c.account.Name),
2294 account = nil // Prevent cleanup.
2296 c.username = username
2298 c.comm = store.RegisterComm(c.account)
2302 c.loginAttempt.AccountName = c.account.Name
2303 c.loginAttempt.LoginAddress = c.username
2304 c.loginAttempt.Result = store.AuthSuccess
2306 c.state = stateAuthenticated
2307 c.writeresultf("%s OK [CAPABILITY %s] authenticate done", tag, c.capabilities())
2310// Login logs in with username and password.
2312// Status: Not authenticated.
2313func (c *conn) cmdLogin(tag, cmd string, p *parser) {
2316 c.newLoginAttempt(true, "login")
2318 if c.loginAttempt.Result == store.AuthSuccess {
2319 mox.LimiterFailedAuth.Reset(c.remoteIP, time.Now())
2321 mox.LimiterFailedAuth.Add(c.remoteIP, time.Now(), 1)
2325 // 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).
2329 username := norm.NFC.String(p.xastring())
2330 c.loginAttempt.LoginAddress = username
2332 password := p.xastring()
2335 if !c.noRequireSTARTTLS && !c.tls {
2337 xusercodeErrorf("PRIVACYREQUIRED", "tls required for login")
2340 // For many failed auth attempts, slow down verification attempts.
2341 if c.authFailed > 3 && authFailDelay > 0 {
2342 mox.Sleep(mox.Context, time.Duration(c.authFailed-3)*authFailDelay)
2344 c.authFailed++ // Compensated on success.
2346 // On the 3rd failed authentication, start responding slowly. Successful auth will
2347 // cause fast responses again.
2348 if c.authFailed >= 3 {
2353 account, accName, err := store.OpenEmailAuth(c.log, username, password, true)
2354 c.loginAttempt.AccountName = accName
2357 if errors.Is(err, store.ErrUnknownCredentials) {
2358 c.loginAttempt.Result = store.AuthBadCredentials
2359 code = "AUTHENTICATIONFAILED"
2360 c.log.Info("failed authentication attempt", slog.String("username", username), slog.Any("remote", c.remoteIP))
2361 } else if errors.Is(err, store.ErrLoginDisabled) {
2362 c.loginAttempt.Result = store.AuthLoginDisabled
2363 c.log.Info("account login disabled", slog.String("username", username))
2364 // There is no specific code for "account disabled" in IMAP. AUTHORIZATIONFAILED is
2365 // not a good idea, it will prompt users for a password. ALERT seems reasonable,
2366 // but may cause email clients to suppress the message since we are not yet
2368 xuserErrorf("%s", err)
2370 xusercodeErrorf(code, "login failed")
2374 err := account.Close()
2375 c.xsanity(err, "close account")
2379 // We may already have TLS credentials. They won't have been enabled, or we could
2380 // get here due to the state machine that doesn't allow authentication while being
2381 // authenticated. But allow another SASL authentication, but it has to be for the
2382 // same account. It can be for a different username (email address) of the account.
2383 if c.account != nil {
2384 if account != c.account {
2385 c.log.Debug("sasl authentication for different account than tls client authentication, aborting connection",
2386 slog.String("saslmechanism", "login"),
2387 slog.String("saslaccount", account.Name),
2388 slog.String("tlsaccount", c.account.Name),
2389 slog.String("saslusername", username),
2390 slog.String("tlsusername", c.username),
2392 xusercodeErrorf("AUTHENTICATIONFAILED", "authentication failed, tls client certificate public key belongs to another account")
2393 } else if username != c.username {
2394 c.log.Debug("sasl authentication for different username than tls client certificate authentication, switching to sasl username",
2395 slog.String("saslmechanism", "login"),
2396 slog.String("saslusername", username),
2397 slog.String("tlsusername", c.username),
2398 slog.String("account", c.account.Name),
2403 account = nil // Prevent cleanup.
2405 c.username = username
2407 c.comm = store.RegisterComm(c.account)
2409 c.loginAttempt.LoginAddress = c.username
2410 c.loginAttempt.AccountName = c.account.Name
2411 c.loginAttempt.Result = store.AuthSuccess
2414 c.state = stateAuthenticated
2415 c.writeresultf("%s OK [CAPABILITY %s] login done", tag, c.capabilities())
2418// Enable explicitly opts in to an extension. A server can typically send new kinds
2419// of responses to a client. Most extensions do not require an ENABLE because a
2420// client implicitly opts in to new response syntax by making a requests that uses
2421// new optional extension request syntax.
2423// State: Authenticated and selected.
2424func (c *conn) cmdEnable(tag, cmd string, p *parser) {
2430 caps := []string{p.xatom()}
2433 caps = append(caps, p.xatom())
2436 // Clients should only send capabilities that need enabling.
2437 // We should only echo that we recognize as needing enabling.
2440 for _, s := range caps {
2441 cap := capability(strings.ToUpper(s))
2446 c.enabled[cap] = true
2449 c.enabled[cap] = true
2453 c.enabled[cap] = true
2458 if qresync && !c.enabled[capCondstore] {
2459 c.xensureCondstore(nil)
2460 enabled += " CONDSTORE"
2464 c.bwritelinef("* ENABLED%s", enabled)
2469// If a mailbox is selected, an untagged OK with HIGHESTMODSEQ is written to the
2470// client. If tx is non-nil, it is used to read the HIGHESTMODSEQ from the
2471// database. Otherwise a new read-only transaction is created.
2472func (c *conn) xensureCondstore(tx *bstore.Tx) {
2473 if !c.enabled[capCondstore] {
2474 c.enabled[capCondstore] = true
2475 // todo spec: can we send an untagged enabled response?
2477 if c.mailboxID <= 0 {
2481 var mb store.Mailbox
2483 c.xdbread(func(tx *bstore.Tx) {
2484 mb = c.xmailboxID(tx, c.mailboxID)
2487 mb = c.xmailboxID(tx, c.mailboxID)
2489 c.bwritelinef("* OK [HIGHESTMODSEQ %d] after condstore-enabling command", mb.ModSeq.Client())
2493// State: Authenticated and selected.
2494func (c *conn) cmdSelect(tag, cmd string, p *parser) {
2495 c.cmdSelectExamine(true, tag, cmd, p)
2498// State: Authenticated and selected.
2499func (c *conn) cmdExamine(tag, cmd string, p *parser) {
2500 c.cmdSelectExamine(false, tag, cmd, p)
2503// Select and examine are almost the same commands. Select just opens a mailbox for
2504// read/write and examine opens a mailbox readonly.
2506// State: Authenticated and selected.
2507func (c *conn) cmdSelectExamine(isselect bool, tag, cmd string, p *parser) {
2515 name := p.xmailbox()
2517 var qruidvalidity uint32
2518 var qrmodseq int64 // QRESYNC required parameters.
2519 var qrknownUIDs, qrknownSeqSet, qrknownUIDSet *numSet // QRESYNC optional parameters.
2521 seen := map[string]bool{}
2523 for len(seen) == 0 || !p.take(")") {
2524 w := p.xtakelist("CONDSTORE", "QRESYNC")
2526 xsyntaxErrorf("duplicate select parameter %s", w)
2536 // Note: unlike with CONDSTORE, there are no QRESYNC-related commands/parameters
2537 // that enable capabilities.
2538 if !c.enabled[capQresync] {
2540 xsyntaxErrorf("QRESYNC must first be enabled")
2546 qrmodseq = p.xnznumber64()
2548 seqMatchData := p.take("(")
2552 seqMatchData = p.take(" (")
2555 ss0 := p.xnumSet0(false, false)
2556 qrknownSeqSet = &ss0
2558 ss1 := p.xnumSet0(false, false)
2559 qrknownUIDSet = &ss1
2565 panic("missing case for select param " + w)
2571 // Deselect before attempting the new select. This means we will deselect when an
2572 // error occurs during select.
2574 if c.state == stateSelected {
2576 c.bwritelinef("* OK [CLOSED] x")
2580 name = xcheckmailboxname(name, true)
2582 var highestModSeq store.ModSeq
2583 var highDeletedModSeq store.ModSeq
2584 var firstUnseen msgseq = 0
2585 var mb store.Mailbox
2586 c.account.WithRLock(func() {
2587 c.xdbread(func(tx *bstore.Tx) {
2588 mb = c.xmailbox(tx, name, "")
2590 q := bstore.QueryTx[store.Message](tx)
2591 q.FilterNonzero(store.Message{MailboxID: mb.ID})
2592 q.FilterEqual("Expunged", false)
2594 c.uids = []store.UID{}
2596 err := q.ForEach(func(m store.Message) error {
2597 c.uids = append(c.uids, m.UID)
2598 if firstUnseen == 0 && !m.Seen {
2607 xcheckf(err, "fetching uids")
2609 // Condstore extension, find the highest modseq.
2610 if c.enabled[capCondstore] {
2611 highestModSeq = mb.ModSeq
2613 // For QRESYNC, we need to know the highest modset of deleted expunged records to
2614 // maintain synchronization.
2615 if c.enabled[capQresync] {
2616 highDeletedModSeq, err = c.account.HighestDeletedModSeq(tx)
2617 xcheckf(err, "getting highest deleted modseq")
2621 c.applyChanges(c.comm.Get(), true)
2624 if len(mb.Keywords) > 0 {
2625 flags = " " + strings.Join(mb.Keywords, " ")
2627 c.bwritelinef(`* FLAGS (\Seen \Answered \Flagged \Deleted \Draft $Forwarded $Junk $NotJunk $Phishing $MDNSent%s)`, flags)
2628 c.bwritelinef(`* OK [PERMANENTFLAGS (\Seen \Answered \Flagged \Deleted \Draft $Forwarded $Junk $NotJunk $Phishing $MDNSent \*)] x`)
2629 if !c.enabled[capIMAP4rev2] {
2630 c.bwritelinef(`* 0 RECENT`)
2632 c.bwritelinef(`* %d EXISTS`, len(c.uids))
2633 if !c.enabled[capIMAP4rev2] && firstUnseen > 0 {
2635 c.bwritelinef(`* OK [UNSEEN %d] x`, firstUnseen)
2637 c.bwritelinef(`* OK [UIDVALIDITY %d] x`, mb.UIDValidity)
2638 c.bwritelinef(`* OK [UIDNEXT %d] x`, mb.UIDNext)
2639 c.bwritelinef(`* LIST () "/" %s`, mailboxt(mb.Name).pack(c))
2640 if c.enabled[capCondstore] {
2643 c.bwritelinef(`* OK [HIGHESTMODSEQ %d] x`, highestModSeq.Client())
2647 if qruidvalidity == mb.UIDValidity {
2648 // We send the vanished UIDs at the end, so we can easily combine the modseq
2649 // changes and vanished UIDs that result from that, with the vanished UIDs from the
2650 // case where we don't store enough history.
2651 vanishedUIDs := map[store.UID]struct{}{}
2653 var preVanished store.UID
2654 var oldClientUID store.UID
2655 // If samples of known msgseq and uid pairs are given (they must be in order), we
2656 // use them to determine the earliest UID for which we send VANISHED responses.
2658 if qrknownSeqSet != nil {
2659 if !qrknownSeqSet.isBasicIncreasing() {
2660 xuserErrorf("QRESYNC known message sequence set must be numeric and strictly increasing")
2662 if !qrknownUIDSet.isBasicIncreasing() {
2663 xuserErrorf("QRESYNC known uid set must be numeric and strictly increasing")
2665 seqiter := qrknownSeqSet.newIter()
2666 uiditer := qrknownUIDSet.newIter()
2668 msgseq, ok0 := seqiter.Next()
2669 uid, ok1 := uiditer.Next()
2672 } else if !ok0 || !ok1 {
2673 xsyntaxErrorf("invalid combination of known sequence set and uid set, must be of equal length")
2675 i := int(msgseq - 1)
2676 if i < 0 || i >= len(c.uids) || c.uids[i] != store.UID(uid) {
2677 if uidSearch(c.uids, store.UID(uid)) <= 0 {
2678 // We will check this old client UID for consistency below.
2679 oldClientUID = store.UID(uid)
2683 preVanished = store.UID(uid + 1)
2687 // We gather vanished UIDs and report them at the end. This seems OK because we
2688 // already sent HIGHESTMODSEQ, and a client should know not to commit that value
2689 // until after it has seen the tagged OK of this command. The RFC has a remark
2690 // about ordering of some untagged responses, it's not immediately clear what it
2691 // means, but given the examples appears to allude to servers that decide to not
2692 // send expunge/vanished before the tagged OK.
2695 // We are reading without account lock. Similar to when we process FETCH/SEARCH
2696 // requests. We don't have to reverify existence of the mailbox, so we don't
2697 // rlock, even briefly.
2698 c.xdbread(func(tx *bstore.Tx) {
2699 if oldClientUID > 0 {
2700 // The client sent a UID that is now removed. This is typically fine. But we check
2701 // that it is consistent with the modseq the client sent. If the UID already didn't
2702 // exist at that modseq, the client may be missing some information.
2703 q := bstore.QueryTx[store.Message](tx)
2704 q.FilterNonzero(store.Message{MailboxID: mb.ID, UID: oldClientUID})
2707 // If client claims to be up to date up to and including qrmodseq, and the message
2708 // was deleted at or before that time, we send changes from just before that
2709 // modseq, and we send vanished for all UIDs.
2710 if m.Expunged && qrmodseq >= m.ModSeq.Client() {
2711 qrmodseq = m.ModSeq.Client() - 1
2714 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.")
2716 } else if err != bstore.ErrAbsent {
2717 xcheckf(err, "checking old client uid")
2721 q := bstore.QueryTx[store.Message](tx)
2722 q.FilterNonzero(store.Message{MailboxID: mb.ID})
2723 // Note: we don't filter by Expunged.
2724 q.FilterGreater("ModSeq", store.ModSeqFromClient(qrmodseq))
2725 q.FilterLessEqual("ModSeq", highestModSeq)
2727 err := q.ForEach(func(m store.Message) error {
2728 if m.Expunged && m.UID < preVanished {
2732 if qrknownUIDs != nil && !qrknownUIDs.contains(uint32(m.UID)) {
2736 vanishedUIDs[m.UID] = struct{}{}
2739 msgseq := c.sequence(m.UID)
2741 c.bwritelinef("* %d FETCH (UID %d FLAGS %s MODSEQ (%d))", msgseq, m.UID, flaglist(m.Flags, m.Keywords).pack(c), m.ModSeq.Client())
2745 xcheckf(err, "listing changed messages")
2748 // Add UIDs from client's known UID set to vanished list if we don't have enough history.
2749 if qrmodseq < highDeletedModSeq.Client() {
2750 // If no known uid set was in the request, we substitute 1:max or the empty set.
2752 if qrknownUIDs == nil {
2753 if len(c.uids) > 0 {
2754 qrknownUIDs = &numSet{ranges: []numRange{{first: setNumber{number: 1}, last: &setNumber{number: uint32(c.uids[len(c.uids)-1])}}}}
2756 qrknownUIDs = &numSet{}
2760 iter := qrknownUIDs.newIter()
2762 v, ok := iter.Next()
2766 if c.sequence(store.UID(v)) <= 0 {
2767 vanishedUIDs[store.UID(v)] = struct{}{}
2772 // Now that we have all vanished UIDs, send them over compactly.
2773 if len(vanishedUIDs) > 0 {
2774 l := slices.Sorted(maps.Keys(vanishedUIDs))
2776 for _, s := range compactUIDSet(l).Strings(4*1024 - 32) {
2777 c.bwritelinef("* VANISHED (EARLIER) %s", s)
2783 c.bwriteresultf("%s OK [READ-WRITE] x", tag)
2786 c.bwriteresultf("%s OK [READ-ONLY] x", tag)
2790 c.state = stateSelected
2791 c.searchResult = nil
2795// Create makes a new mailbox, and its parents too if absent.
2797// State: Authenticated and selected.
2798func (c *conn) cmdCreate(tag, cmd string, p *parser) {
2804 name := p.xmailbox()
2806 var useAttrs []string // Special-use attributes without leading \.
2809 // We only support "USE", and there don't appear to be more types of parameters.
2814 useAttrs = append(useAttrs, p.xatom())
2830 name = xcheckmailboxname(name, false)
2832 var specialUse store.SpecialUse
2833 specialUseBools := map[string]*bool{
2834 "archive": &specialUse.Archive,
2835 "drafts": &specialUse.Draft,
2836 "junk": &specialUse.Junk,
2837 "sent": &specialUse.Sent,
2838 "trash": &specialUse.Trash,
2840 for _, s := range useAttrs {
2841 p, ok := specialUseBools[strings.ToLower(s)]
2844 xusercodeErrorf("USEATTR", `cannot create mailbox with special-use attribute \%s`, s)
2849 var changes []store.Change
2850 var created []string // Created mailbox names.
2852 c.account.WithWLock(func() {
2853 c.xdbwrite(func(tx *bstore.Tx) {
2856 _, changes, created, exists, err = c.account.MailboxCreate(tx, name, specialUse)
2859 xuserErrorf("mailbox already exists")
2861 xcheckf(err, "creating mailbox")
2864 c.broadcast(changes)
2867 for _, n := range created {
2870 if c.enabled[capIMAP4rev2] && n == name && name != origName && !(name == "Inbox" || strings.HasPrefix(name, "Inbox/")) {
2871 oldname = fmt.Sprintf(` ("OLDNAME" (%s))`, mailboxt(origName).pack(c))
2873 c.bwritelinef(`* LIST (\Subscribed) "/" %s%s`, mailboxt(n).pack(c), oldname)
2878// Delete removes a mailbox and all its messages and annotations.
2879// Inbox cannot be removed.
2881// State: Authenticated and selected.
2882func (c *conn) cmdDelete(tag, cmd string, p *parser) {
2888 name := p.xmailbox()
2891 name = xcheckmailboxname(name, false)
2893 c.account.WithWLock(func() {
2894 var mb store.Mailbox
2895 var changes []store.Change
2897 c.xdbwrite(func(tx *bstore.Tx) {
2898 mb = c.xmailbox(tx, name, "NONEXISTENT")
2900 var hasChildren bool
2902 changes, hasChildren, err = c.account.MailboxDelete(context.TODO(), c.log, tx, &mb)
2904 xusercodeErrorf("HASCHILDREN", "mailbox has a child, only leaf mailboxes can be deleted")
2906 xcheckf(err, "deleting mailbox")
2909 c.broadcast(changes)
2915// Rename changes the name of a mailbox.
2916// Renaming INBOX is special, it moves the inbox messages to a new mailbox, leaving
2917// inbox empty, but copying metadata annotations.
2918// Renaming a mailbox with submailboxes also renames all submailboxes.
2919// Subscriptions stay with the old name, though newly created missing parent
2920// mailboxes for the destination name are automatically subscribed.
2922// State: Authenticated and selected.
2923func (c *conn) cmdRename(tag, cmd string, p *parser) {
2934 src = xcheckmailboxname(src, true)
2935 dst = xcheckmailboxname(dst, false)
2937 var cleanupIDs []int64
2939 for _, id := range cleanupIDs {
2940 p := c.account.MessagePath(id)
2942 c.xsanity(err, "cleaning up message")
2946 c.account.WithWLock(func() {
2947 var changes []store.Change
2949 c.xdbwrite(func(tx *bstore.Tx) {
2950 mbSrc := c.xmailbox(tx, src, "NONEXISTENT")
2952 // Handle common/simple case first.
2954 var modseq store.ModSeq
2955 var alreadyExists bool
2957 changes, _, alreadyExists, err = c.account.MailboxRename(tx, &mbSrc, dst, &modseq)
2959 xusercodeErrorf("ALREADYEXISTS", "%s", err)
2961 xcheckf(err, "renaming mailbox")
2965 // Inbox is very special. Unlike other mailboxes, its children are not moved. And
2966 // unlike a regular move, its messages are moved to a newly created mailbox. We do
2967 // indeed create a new destination mailbox and actually move the messages.
2969 exists, err := c.account.MailboxExists(tx, dst)
2970 xcheckf(err, "checking if destination mailbox exists")
2972 xusercodeErrorf("ALREADYEXISTS", "destination mailbox %q already exists", dst)
2975 xuserErrorf("cannot move inbox to itself")
2978 var modseq store.ModSeq
2979 mbDst, chl, err := c.account.MailboxEnsure(tx, dst, false, store.SpecialUse{}, &modseq)
2980 xcheckf(err, "creating destination mailbox")
2984 qa := bstore.QueryTx[store.Annotation](tx)
2985 qa.FilterNonzero(store.Annotation{MailboxID: mbSrc.ID})
2986 qa.FilterEqual("Expunged", false)
2987 annotations, err := qa.List()
2988 xcheckf(err, "get annotations to copy for inbox")
2989 for _, a := range annotations {
2991 a.MailboxID = mbDst.ID
2993 a.CreateSeq = modseq
2994 err := tx.Insert(&a)
2995 xcheckf(err, "copy annotation to destination mailbox")
2996 changes = append(changes, a.Change(mbDst.Name))
2998 c.xcheckMetadataSize(tx)
3000 // Build query that selects messages to move.
3001 q := bstore.QueryTx[store.Message](tx)
3002 q.FilterNonzero(store.Message{MailboxID: mbSrc.ID})
3003 q.FilterEqual("Expunged", false)
3006 newIDs, chl := c.xmoveMessages(tx, q, 0, modseq, &mbSrc, &mbDst)
3007 changes = append(changes, chl...)
3013 c.broadcast(changes)
3019// Subscribe marks a mailbox path as subscribed. The mailbox does not have to
3020// exist. Subscribed may mean an email client will show the mailbox in its UI
3021// and/or periodically fetch new messages for the mailbox.
3023// State: Authenticated and selected.
3024func (c *conn) cmdSubscribe(tag, cmd string, p *parser) {
3030 name := p.xmailbox()
3033 name = xcheckmailboxname(name, true)
3035 c.account.WithWLock(func() {
3036 var changes []store.Change
3038 c.xdbwrite(func(tx *bstore.Tx) {
3040 changes, err = c.account.SubscriptionEnsure(tx, name)
3041 xcheckf(err, "ensuring subscription")
3044 c.broadcast(changes)
3050// Unsubscribe marks a mailbox as not subscribed. The mailbox doesn't have to exist.
3052// State: Authenticated and selected.
3053func (c *conn) cmdUnsubscribe(tag, cmd string, p *parser) {
3059 name := p.xmailbox()
3062 name = xcheckmailboxname(name, true)
3064 c.account.WithWLock(func() {
3065 c.xdbwrite(func(tx *bstore.Tx) {
3067 err := tx.Delete(&store.Subscription{Name: name})
3068 if err == bstore.ErrAbsent {
3069 exists, err := c.account.MailboxExists(tx, name)
3070 xcheckf(err, "checking if mailbox exists")
3072 xuserErrorf("mailbox does not exist")
3076 xcheckf(err, "removing subscription")
3079 // todo: can we send untagged message about a mailbox no longer being subscribed?
3085// LSUB command for listing subscribed mailboxes.
3086// Removed in IMAP4rev2, only in IMAP4rev1.
3088// State: Authenticated and selected.
3089func (c *conn) cmdLsub(tag, cmd string, p *parser) {
3097 pattern := p.xlistMailbox()
3100 re := xmailboxPatternMatcher(ref, []string{pattern})
3103 c.xdbread(func(tx *bstore.Tx) {
3104 q := bstore.QueryTx[store.Subscription](tx)
3106 subscriptions, err := q.List()
3107 xcheckf(err, "querying subscriptions")
3109 have := map[string]bool{}
3110 subscribedKids := map[string]bool{}
3111 ispercent := strings.HasSuffix(pattern, "%")
3112 for _, sub := range subscriptions {
3115 for p := mox.ParentMailboxName(name); p != ""; p = mox.ParentMailboxName(p) {
3116 subscribedKids[p] = true
3119 if !re.MatchString(name) {
3123 line := fmt.Sprintf(`* LSUB () "/" %s`, mailboxt(name).pack(c))
3124 lines = append(lines, line)
3132 qmb := bstore.QueryTx[store.Mailbox](tx)
3133 qmb.FilterEqual("Expunged", false)
3135 err = qmb.ForEach(func(mb store.Mailbox) error {
3136 if have[mb.Name] || !subscribedKids[mb.Name] || !re.MatchString(mb.Name) {
3139 line := fmt.Sprintf(`* LSUB (\NoSelect) "/" %s`, mailboxt(mb.Name).pack(c))
3140 lines = append(lines, line)
3143 xcheckf(err, "querying mailboxes")
3147 for _, line := range lines {
3148 c.bwritelinef("%s", line)
3153// The namespace command returns the mailbox path separator. We only implement
3154// the personal mailbox hierarchy, no shared/other.
3156// In IMAP4rev2, it was an extension before.
3158// State: Authenticated and selected.
3159func (c *conn) cmdNamespace(tag, cmd string, p *parser) {
3166 c.bwritelinef(`* NAMESPACE (("" "/")) NIL NIL`)
3170// The status command returns information about a mailbox, such as the number of
3171// messages, "uid validity", etc. Nowadays, the extended LIST command can return
3172// the same information about many mailboxes for one command.
3174// State: Authenticated and selected.
3175func (c *conn) cmdStatus(tag, cmd string, p *parser) {
3181 name := p.xmailbox()
3184 attrs := []string{p.xstatusAtt()}
3187 attrs = append(attrs, p.xstatusAtt())
3191 name = xcheckmailboxname(name, true)
3193 var mb store.Mailbox
3195 var responseLine string
3196 c.account.WithRLock(func() {
3197 c.xdbread(func(tx *bstore.Tx) {
3198 mb = c.xmailbox(tx, name, "")
3199 responseLine = c.xstatusLine(tx, mb, attrs)
3203 c.bwritelinef("%s", responseLine)
3208func (c *conn) xstatusLine(tx *bstore.Tx, mb store.Mailbox, attrs []string) string {
3209 status := []string{}
3210 for _, a := range attrs {
3211 A := strings.ToUpper(a)
3214 status = append(status, A, fmt.Sprintf("%d", mb.Total+mb.Deleted))
3216 status = append(status, A, fmt.Sprintf("%d", mb.UIDNext))
3218 status = append(status, A, fmt.Sprintf("%d", mb.UIDValidity))
3220 status = append(status, A, fmt.Sprintf("%d", mb.Unseen))
3222 status = append(status, A, fmt.Sprintf("%d", mb.Deleted))
3224 status = append(status, A, fmt.Sprintf("%d", mb.Size))
3226 status = append(status, A, "0")
3229 status = append(status, A, "NIL")
3230 case "HIGHESTMODSEQ":
3232 status = append(status, A, fmt.Sprintf("%d", mb.ModSeq.Client()))
3233 case "DELETED-STORAGE":
3235 // How much storage space could be reclaimed by expunging messages with the
3236 // \Deleted flag. We could keep track of this number and return it efficiently.
3237 // Calculating it each time can be slow, and we don't know if clients request it.
3238 // Clients are not likely to set the deleted flag without immediately expunging
3239 // nowadays. Let's wait for something to need it to go through the trouble, and
3240 // always return 0 for now.
3241 status = append(status, A, "0")
3243 xsyntaxErrorf("unknown attribute %q", a)
3246 return fmt.Sprintf("* STATUS %s (%s)", mailboxt(mb.Name).pack(c), strings.Join(status, " "))
3249func flaglist(fl store.Flags, keywords []string) listspace {
3251 flag := func(v bool, s string) {
3253 l = append(l, bare(s))
3256 flag(fl.Seen, `\Seen`)
3257 flag(fl.Answered, `\Answered`)
3258 flag(fl.Flagged, `\Flagged`)
3259 flag(fl.Deleted, `\Deleted`)
3260 flag(fl.Draft, `\Draft`)
3261 flag(fl.Forwarded, `$Forwarded`)
3262 flag(fl.Junk, `$Junk`)
3263 flag(fl.Notjunk, `$NotJunk`)
3264 flag(fl.Phishing, `$Phishing`)
3265 flag(fl.MDNSent, `$MDNSent`)
3266 for _, k := range keywords {
3267 l = append(l, bare(k))
3272// Append adds a message to a mailbox.
3273// The MULTIAPPEND extension is implemented, allowing multiple flags/datetime/data
3276// State: Authenticated and selected.
3277func (c *conn) cmdAppend(tag, cmd string, p *parser) {
3281 // A message that we've (partially) read from the client, and will be delivering to
3283 type appendMsg struct {
3284 storeFlags store.Flags
3288 file *os.File // Message file we are appending. Can be nil if we are writing to a nopWriteCloser due to being over quota.
3291 m store.Message // New message. Delivered file for m.ID is removed on error.
3294 var appends []*appendMsg
3297 for _, a := range appends {
3298 if !commit && a.m.ID != 0 {
3299 p := c.account.MessagePath(a.m.ID)
3301 c.xsanity(err, "cleaning up temporary append file after error")
3308 name := p.xmailbox()
3311 // Check how much quota space is available. We'll keep track of remaining quota as
3312 // we accept multiple messages.
3313 quotaMsgMax := c.account.QuotaMessageSize()
3314 quotaUnlimited := quotaMsgMax == 0
3315 var quotaAvail int64
3317 if !quotaUnlimited {
3318 c.account.WithRLock(func() {
3319 c.xdbread(func(tx *bstore.Tx) {
3320 du := store.DiskUsage{ID: 1}
3322 xcheckf(err, "get quota disk usage")
3323 quotaAvail = quotaMsgMax - du.MessageSize
3328 var overQuota bool // For response code.
3329 var cancel bool // In case we've seen zero-sized message append.
3332 // Append msg early, for potential cleanup.
3334 appends = append(appends, &a)
3336 if p.hasPrefix("(") {
3337 // Error must be a syntax error, to properly abort the connection due to literal.
3339 a.storeFlags, a.keywords, err = store.ParseFlagsKeywords(p.xflagList())
3341 xsyntaxErrorf("parsing flags: %v", err)
3345 if p.hasPrefix(`"`) {
3346 a.time = p.xdateTime()
3351 // todo: only with utf8 should we we accept message headers with utf-8. we currently always accept them.
3352 // todo: this is only relevant if we also support the CATENATE extension?
3354 utf8 := p.take("UTF8 (")
3359 // For utf8, we already consumed the required ~ above.
3360 size, synclit := p.xliteralSize(!utf8, false)
3362 if !quotaUnlimited && !overQuota {
3364 overQuota = quotaAvail < 0
3372 // Check for mailbox on first iteration.
3373 if len(appends) <= 1 {
3374 name = xcheckmailboxname(name, true)
3375 c.xdbread(func(tx *bstore.Tx) {
3376 c.xmailbox(tx, name, "TRYCREATE")
3382 xusercodeErrorf("OVERQUOTA", "account over maximum total message size %d", quotaMsgMax)
3387 xuserErrorf("empty message, cancelling append")
3390 // Read the message into a temporary file.
3392 a.file, err = store.CreateMessageTemp(c.log, "imap-append")
3393 xcheckf(err, "creating temp file for message")
3394 defer store.CloseRemoveTempFile(c.log, a.file, "temporary message file")
3399 // We'll discard the message and return an error as soon as we can (possible
3400 // synchronizing literal of next message, or after we've seen all messages).
3401 if overQuota || cancel {
3405 a.file, err = store.CreateMessageTemp(c.log, "imap-append")
3406 xcheckf(err, "creating temp file for message")
3407 defer store.CloseRemoveTempFile(c.log, a.file, "temporary message file")
3412 defer c.xtrace(mlog.LevelTracedata)()
3413 a.mw = message.NewWriter(f)
3414 msize, err := io.Copy(a.mw, io.LimitReader(c.br, size))
3415 c.xtrace(mlog.LevelTrace) // Restore.
3417 // Cannot use xcheckf due to %w handling of errIO.
3418 c.xbrokenf("reading literal message: %s (%w)", err, errIO)
3421 c.xbrokenf("read %d bytes for message, expected %d (%w)", msize, size, errIO)
3425 line := c.readline(false)
3426 p = newParser(line, c)
3431 // The MULTIAPPEND extension allows more appends.
3438 name = xcheckmailboxname(name, true)
3442 xusercodeErrorf("OVERQUOTA", "account over maximum total message size %d", quotaMsgMax)
3447 xuserErrorf("empty message, cancelling append")
3450 var mb store.Mailbox
3451 var pendingChanges []store.Change
3455 c.account.WithWLock(func() {
3456 var changes []store.Change
3458 c.xdbwrite(func(tx *bstore.Tx) {
3459 mb = c.xmailbox(tx, name, "TRYCREATE")
3461 nkeywords := len(mb.Keywords)
3463 // Check quota for all messages at once.
3464 ok, maxSize, err := c.account.CanAddMessageSize(tx, totalSize)
3465 xcheckf(err, "checking quota")
3468 xusercodeErrorf("OVERQUOTA", "account over maximum total message size %d", maxSize)
3471 modseq, err := c.account.NextModSeq(tx)
3472 xcheckf(err, "get next mod seq")
3476 msgDirs := map[string]struct{}{}
3477 for _, a := range appends {
3478 a.m = store.Message{
3480 MailboxOrigID: mb.ID,
3482 Flags: a.storeFlags,
3483 Keywords: a.keywords,
3489 // todo: do a single junk training
3490 err = c.account.MessageAdd(c.log, tx, &mb, &a.m, a.file, store.AddOpts{SkipDirSync: true})
3491 xcheckf(err, "delivering message")
3493 changes = append(changes, a.m.ChangeAddUID())
3495 msgDirs[filepath.Dir(c.account.MessagePath(a.m.ID))] = struct{}{}
3498 changes = append(changes, mb.ChangeCounts())
3499 if nkeywords != len(mb.Keywords) {
3500 changes = append(changes, mb.ChangeKeywords())
3503 err = tx.Update(&mb)
3504 xcheckf(err, "updating mailbox counts")
3506 for dir := range msgDirs {
3507 err := moxio.SyncDir(c.log, dir)
3508 xcheckf(err, "sync dir")
3514 // Fetch pending changes, possibly with new UIDs, so we can apply them before adding our own new UID.
3515 pendingChanges = c.comm.Get()
3517 // Broadcast the change to other connections.
3518 c.broadcast(changes)
3521 if c.mailboxID == mb.ID {
3522 c.applyChanges(pendingChanges, false)
3523 for _, a := range appends {
3524 c.uidAppend(a.m.UID)
3526 // todo spec: with condstore/qresync, is there a mechanism to let 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.
3527 c.bwritelinef("* %d EXISTS", len(c.uids))
3533 if len(appends) == 1 {
3534 uidset = fmt.Sprintf("%d", appends[0].m.UID)
3536 uidset = fmt.Sprintf("%d:%d", appends[0].m.UID, appends[len(appends)-1].m.UID)
3538 c.writeresultf("%s OK [APPENDUID %d %s] appended", tag, mb.UIDValidity, uidset)
3541// Idle makes a client wait until the server sends untagged updates, e.g. about
3542// message delivery or mailbox create/rename/delete/subscription, etc. It allows a
3543// client to get updates in real-time, not needing the use for NOOP.
3545// State: Authenticated and selected.
3546func (c *conn) cmdIdle(tag, cmd string, p *parser) {
3553 c.writelinef("+ waiting")
3559 case le := <-c.lineChan():
3561 xcheckf(le.err, "get line")
3564 case <-c.comm.Pending:
3565 c.applyChanges(c.comm.Get(), false)
3567 case <-mox.Shutdown.Done():
3569 c.writelinef("* BYE shutting down")
3570 c.xbrokenf("shutting down (%w)", errIO)
3574 // Reset the write deadline. In case of little activity, with a command timeout of
3575 // 30 minutes, we have likely passed it.
3576 err := c.conn.SetWriteDeadline(time.Now().Add(5 * time.Minute))
3577 c.log.Check(err, "setting write deadline")
3579 if strings.ToUpper(line) != "DONE" {
3580 // We just close the connection because our protocols are out of sync.
3581 c.xbrokenf("%w: in IDLE, expected DONE", errIO)
3587// Return the quota root for a mailbox name and any current quota's.
3589// State: Authenticated and selected.
3590func (c *conn) cmdGetquotaroot(tag, cmd string, p *parser) {
3595 name := p.xmailbox()
3598 // This mailbox does not have to exist. Caller just wants to know which limits
3599 // would apply. We only have one limit, so we don't use the name otherwise.
3601 name = xcheckmailboxname(name, true)
3603 // Get current usage for account.
3604 var quota, size int64 // Account only has a quota if > 0.
3605 c.account.WithRLock(func() {
3606 quota = c.account.QuotaMessageSize()
3608 c.xdbread(func(tx *bstore.Tx) {
3609 du := store.DiskUsage{ID: 1}
3611 xcheckf(err, "gather used quota")
3612 size = du.MessageSize
3617 // We only have one per account quota, we name it "" like the examples in the RFC.
3619 c.bwritelinef(`* QUOTAROOT %s ""`, astring(name).pack(c))
3621 // We only write the quota response if there is a limit. The syntax doesn't allow
3622 // an empty list, so we cannot send the current disk usage if there is no limit.
3625 c.bwritelinef(`* QUOTA "" (STORAGE %d %d)`, (size+1024-1)/1024, (quota+1024-1)/1024)
3630// Return the quota for a quota root.
3632// State: Authenticated and selected.
3633func (c *conn) cmdGetquota(tag, cmd string, p *parser) {
3638 root := p.xastring()
3641 // We only have a per-account root called "".
3643 xuserErrorf("unknown quota root")
3646 var quota, size int64
3647 c.account.WithRLock(func() {
3648 quota = c.account.QuotaMessageSize()
3650 c.xdbread(func(tx *bstore.Tx) {
3651 du := store.DiskUsage{ID: 1}
3653 xcheckf(err, "gather used quota")
3654 size = du.MessageSize
3659 // We only write the quota response if there is a limit. The syntax doesn't allow
3660 // an empty list, so we cannot send the current disk usage if there is no limit.
3663 c.bwritelinef(`* QUOTA "" (STORAGE %d %d)`, (size+1024-1)/1024, (quota+1024-1)/1024)
3668// Check is an old deprecated command that is supposed to execute some mailbox consistency checks.
3671func (c *conn) cmdCheck(tag, cmd string, p *parser) {
3677 c.account.WithRLock(func() {
3678 c.xdbread(func(tx *bstore.Tx) {
3679 c.xmailboxID(tx, c.mailboxID) // Validate.
3686// Close undoes select/examine, closing the currently opened mailbox and deleting
3687// messages that were marked for deletion with the \Deleted flag.
3690func (c *conn) cmdClose(tag, cmd string, p *parser) {
3697 c.xexpunge(nil, true)
3703// expunge messages marked for deletion in currently selected/active mailbox.
3704// if uidSet is not nil, only messages matching the set are expunged.
3706// Messages that have been marked expunged from the database are returned. While
3707// other sessions still reference the message, it is not cleared from the database
3708// yet, and the message file is not yet removed.
3710// The highest modseq in the mailbox is returned, typically associated with the
3711// removal of the messages, but if no messages were expunged the current latest max
3712// modseq for the mailbox is returned.
3713func (c *conn) xexpunge(uidSet *numSet, missingMailboxOK bool) (expunged []store.Message, highestModSeq store.ModSeq) {
3714 c.account.WithWLock(func() {
3715 var changes []store.Change
3717 c.xdbwrite(func(tx *bstore.Tx) {
3718 mb, err := store.MailboxID(tx, c.mailboxID)
3719 if err == bstore.ErrAbsent || err == store.ErrMailboxExpunged {
3720 if missingMailboxOK {
3724 xusercodeErrorf("NONEXISTENT", "%w", store.ErrUnknownMailbox)
3726 xcheckf(err, "get mailbox")
3728 qm := bstore.QueryTx[store.Message](tx)
3729 qm.FilterNonzero(store.Message{MailboxID: c.mailboxID})
3730 qm.FilterEqual("Deleted", true)
3731 qm.FilterEqual("Expunged", false)
3732 qm.FilterFn(func(m store.Message) bool {
3733 // Only remove if this session knows about the message and if present in optional uidSet.
3734 return uidSearch(c.uids, m.UID) > 0 && (uidSet == nil || uidSet.containsUID(m.UID, c.uids, c.searchResult))
3737 expunged, err = qm.List()
3738 xcheckf(err, "listing messages to expunge")
3740 if len(expunged) == 0 {
3741 highestModSeq = mb.ModSeq
3745 // Assign new modseq.
3746 modseq, err := c.account.NextModSeq(tx)
3747 xcheckf(err, "assigning next modseq")
3748 highestModSeq = modseq
3751 chremuids, chmbcounts, err := c.account.MessageRemove(c.log, tx, modseq, &mb, store.RemoveOpts{}, expunged...)
3752 xcheckf(err, "expunging messages")
3753 changes = append(changes, chremuids, chmbcounts)
3755 err = tx.Update(&mb)
3756 xcheckf(err, "update mailbox")
3759 c.broadcast(changes)
3762 return expunged, highestModSeq
3765// Unselect is similar to close in that it closes the currently active mailbox, but
3766// it does not remove messages marked for deletion.
3769func (c *conn) cmdUnselect(tag, cmd string, p *parser) {
3779// Expunge deletes messages marked with \Deleted in the currently selected mailbox.
3780// Clients are wiser to use UID EXPUNGE because it allows a UID sequence set to
3781// explicitly opt in to removing specific messages.
3784func (c *conn) cmdExpunge(tag, cmd string, p *parser) {
3791 xuserErrorf("mailbox open in read-only mode")
3794 c.cmdxExpunge(tag, cmd, nil)
3797// UID expunge deletes messages marked with \Deleted in the currently selected
3798// mailbox if they match a UID sequence set.
3801func (c *conn) cmdUIDExpunge(tag, cmd string, p *parser) {
3806 uidSet := p.xnumSet()
3810 xuserErrorf("mailbox open in read-only mode")
3813 c.cmdxExpunge(tag, cmd, &uidSet)
3816// Permanently delete messages for the currently selected/active mailbox. If uidset
3817// is not nil, only those UIDs are expunged.
3819func (c *conn) cmdxExpunge(tag, cmd string, uidSet *numSet) {
3822 expunged, highestModSeq := c.xexpunge(uidSet, false)
3825 var vanishedUIDs numSet
3826 qresync := c.enabled[capQresync]
3827 for _, m := range expunged {
3828 seq := c.xsequence(m.UID)
3829 c.sequenceRemove(seq, m.UID)
3831 vanishedUIDs.append(uint32(m.UID))
3833 c.bwritelinef("* %d EXPUNGE", seq)
3836 if !vanishedUIDs.empty() {
3838 for _, s := range vanishedUIDs.Strings(4*1024 - 32) {
3839 c.bwritelinef("* VANISHED %s", s)
3843 if c.enabled[capCondstore] {
3844 c.writeresultf("%s OK [HIGHESTMODSEQ %d] expunged", tag, highestModSeq.Client())
3851func (c *conn) cmdSearch(tag, cmd string, p *parser) {
3852 c.cmdxSearch(false, false, tag, cmd, p)
3856func (c *conn) cmdUIDSearch(tag, cmd string, p *parser) {
3857 c.cmdxSearch(true, false, tag, cmd, p)
3861func (c *conn) cmdFetch(tag, cmd string, p *parser) {
3862 c.cmdxFetch(false, tag, cmd, p)
3866func (c *conn) cmdUIDFetch(tag, cmd string, p *parser) {
3867 c.cmdxFetch(true, tag, cmd, p)
3871func (c *conn) cmdStore(tag, cmd string, p *parser) {
3872 c.cmdxStore(false, tag, cmd, p)
3876func (c *conn) cmdUIDStore(tag, cmd string, p *parser) {
3877 c.cmdxStore(true, tag, cmd, p)
3881func (c *conn) cmdCopy(tag, cmd string, p *parser) {
3882 c.cmdxCopy(false, tag, cmd, p)
3886func (c *conn) cmdUIDCopy(tag, cmd string, p *parser) {
3887 c.cmdxCopy(true, tag, cmd, p)
3891func (c *conn) cmdMove(tag, cmd string, p *parser) {
3892 c.cmdxMove(false, tag, cmd, p)
3896func (c *conn) cmdUIDMove(tag, cmd string, p *parser) {
3897 c.cmdxMove(true, tag, cmd, p)
3901func (c *conn) cmdReplace(tag, cmd string, p *parser) {
3902 c.cmdxReplace(false, tag, cmd, p)
3906func (c *conn) cmdUIDReplace(tag, cmd string, p *parser) {
3907 c.cmdxReplace(true, tag, cmd, p)
3910func (c *conn) gatherCopyMoveUIDs(isUID bool, nums numSet) ([]store.UID, []any) {
3911 // Gather uids, then sort so we can return a consistently simple and hard to
3912 // misinterpret COPYUID/MOVEUID response. It seems safer to have UIDs in ascending
3913 // order, because requested uid set of 12:10 is equal to 10:12, so if we would just
3914 // echo whatever the client sends us without reordering, the client can reorder our
3915 // response and interpret it differently than we intended.
3917 uids := c.xnumSetUIDs(isUID, nums)
3919 uidargs := make([]any, len(uids))
3920 for i, uid := range uids {
3923 return uids, uidargs
3926// Copy copies messages from the currently selected/active mailbox to another named
3930func (c *conn) cmdxCopy(isUID bool, tag, cmd string, p *parser) {
3937 name := p.xmailbox()
3940 name = xcheckmailboxname(name, true)
3942 uids, uidargs := c.gatherCopyMoveUIDs(isUID, nums)
3944 // Files that were created during the copy. Remove them if the operation fails.
3947 for _, id := range newIDs {
3948 p := c.account.MessagePath(id)
3950 c.xsanity(err, "cleaning up created file")
3954 var mbDst store.Mailbox
3956 var origUIDs, newUIDs []store.UID
3957 var flags []store.Flags
3958 var keywords [][]string
3959 var modseq store.ModSeq // For messages in new mailbox, assigned when first message is copied.
3961 c.account.WithWLock(func() {
3963 c.xdbwrite(func(tx *bstore.Tx) {
3964 mbSrc := c.xmailboxID(tx, c.mailboxID) // Validate.
3965 mbDst = c.xmailbox(tx, name, "TRYCREATE")
3966 if mbDst.ID == mbSrc.ID {
3967 xuserErrorf("cannot copy to currently selected mailbox")
3970 if len(uidargs) == 0 {
3971 xuserErrorf("no matching messages to copy")
3974 nkeywords = len(mbDst.Keywords)
3977 modseq, err = c.account.NextModSeq(tx)
3978 xcheckf(err, "assigning next modseq")
3979 mbSrc.ModSeq = modseq
3980 mbDst.ModSeq = modseq
3982 err = tx.Update(&mbSrc)
3983 xcheckf(err, "updating source mailbox for modseq")
3985 // Reserve the uids in the destination mailbox.
3986 uidFirst := mbDst.UIDNext
3987 mbDst.UIDNext += store.UID(len(uidargs))
3989 // Fetch messages from database.
3990 q := bstore.QueryTx[store.Message](tx)
3991 q.FilterNonzero(store.Message{MailboxID: c.mailboxID})
3992 q.FilterEqual("UID", uidargs...)
3993 q.FilterEqual("Expunged", false)
3994 xmsgs, err := q.List()
3995 xcheckf(err, "fetching messages")
3997 if len(xmsgs) != len(uidargs) {
3998 xserverErrorf("uid and message mismatch")
4001 // See if quota allows copy.
4003 for _, m := range xmsgs {
4006 if ok, maxSize, err := c.account.CanAddMessageSize(tx, totalSize); err != nil {
4007 xcheckf(err, "checking quota")
4010 xusercodeErrorf("OVERQUOTA", "account over maximum total message size %d", maxSize)
4012 err = c.account.AddMessageSize(c.log, tx, totalSize)
4013 xcheckf(err, "updating disk usage")
4015 msgs := map[store.UID]store.Message{}
4016 for _, m := range xmsgs {
4019 nmsgs := make([]store.Message, len(xmsgs))
4021 conf, _ := c.account.Conf()
4023 mbKeywords := map[string]struct{}{}
4026 // Insert new messages into database.
4027 var origMsgIDs, newMsgIDs []int64
4028 for i, uid := range uids {
4031 xuserErrorf("messages changed, could not fetch requested uid")
4034 origMsgIDs = append(origMsgIDs, origID)
4036 m.UID = uidFirst + store.UID(i)
4037 m.CreateSeq = modseq
4039 m.MailboxID = mbDst.ID
4040 if m.IsReject && m.MailboxDestinedID != 0 {
4041 // Incorrectly delivered to Rejects mailbox. Adjust MailboxOrigID so this message
4042 // is used for reputation calculation during future deliveries.
4043 m.MailboxOrigID = m.MailboxDestinedID
4047 m.JunkFlagsForMailbox(mbDst, conf)
4049 err := tx.Insert(&m)
4050 xcheckf(err, "inserting message")
4053 origUIDs = append(origUIDs, uid)
4054 newUIDs = append(newUIDs, m.UID)
4055 newMsgIDs = append(newMsgIDs, m.ID)
4056 flags = append(flags, m.Flags)
4057 keywords = append(keywords, m.Keywords)
4058 for _, kw := range m.Keywords {
4059 mbKeywords[kw] = struct{}{}
4062 qmr := bstore.QueryTx[store.Recipient](tx)
4063 qmr.FilterNonzero(store.Recipient{MessageID: origID})
4064 mrs, err := qmr.List()
4065 xcheckf(err, "listing message recipients")
4066 for _, mr := range mrs {
4069 err := tx.Insert(&mr)
4070 xcheckf(err, "inserting message recipient")
4073 mbDst.Add(m.MailboxCounts())
4076 mbDst.Keywords, _ = store.MergeKeywords(mbDst.Keywords, slices.Sorted(maps.Keys(mbKeywords)))
4078 err = tx.Update(&mbDst)
4079 xcheckf(err, "updating destination mailbox for uids, keywords and counts")
4081 // Copy message files to new message ID's.
4082 syncDirs := map[string]struct{}{}
4083 for i := range origMsgIDs {
4084 src := c.account.MessagePath(origMsgIDs[i])
4085 dst := c.account.MessagePath(newMsgIDs[i])
4086 dstdir := filepath.Dir(dst)
4087 if _, ok := syncDirs[dstdir]; !ok {
4088 os.MkdirAll(dstdir, 0770)
4089 syncDirs[dstdir] = struct{}{}
4091 err := moxio.LinkOrCopy(c.log, dst, src, nil, true)
4092 xcheckf(err, "link or copy file %q to %q", src, dst)
4093 newIDs = append(newIDs, newMsgIDs[i])
4096 for dir := range syncDirs {
4097 err := moxio.SyncDir(c.log, dir)
4098 xcheckf(err, "sync directory")
4101 err = c.account.RetrainMessages(context.TODO(), c.log, tx, nmsgs)
4102 xcheckf(err, "train copied messages")
4107 // Broadcast changes to other connections.
4108 if len(newUIDs) > 0 {
4109 changes := make([]store.Change, 0, len(newUIDs)+2)
4110 for i, uid := range newUIDs {
4111 changes = append(changes, store.ChangeAddUID{MailboxID: mbDst.ID, UID: uid, ModSeq: modseq, Flags: flags[i], Keywords: keywords[i]})
4113 changes = append(changes, mbDst.ChangeCounts())
4114 if nkeywords != len(mbDst.Keywords) {
4115 changes = append(changes, mbDst.ChangeKeywords())
4117 c.broadcast(changes)
4122 c.writeresultf("%s OK [COPYUID %d %s %s] copied", tag, mbDst.UIDValidity, compactUIDSet(origUIDs).String(), compactUIDSet(newUIDs).String())
4125// Move moves messages from the currently selected/active mailbox to a named mailbox.
4128func (c *conn) cmdxMove(isUID bool, tag, cmd string, p *parser) {
4135 name := p.xmailbox()
4138 name = xcheckmailboxname(name, true)
4141 xuserErrorf("mailbox open in read-only mode")
4144 uids, uidargs := c.gatherCopyMoveUIDs(isUID, nums)
4146 var mbDst store.Mailbox
4147 var uidFirst store.UID
4148 var modseq store.ModSeq
4150 var cleanupIDs []int64
4152 for _, id := range cleanupIDs {
4153 p := c.account.MessagePath(id)
4155 c.xsanity(err, "removing destination message file %v", p)
4159 c.account.WithWLock(func() {
4160 var changes []store.Change
4162 c.xdbwrite(func(tx *bstore.Tx) {
4163 mbSrc := c.xmailboxID(tx, c.mailboxID) // Validate.
4164 mbDst = c.xmailbox(tx, name, "TRYCREATE")
4165 if mbDst.ID == c.mailboxID {
4166 xuserErrorf("cannot move to currently selected mailbox")
4170 xuserErrorf("no matching messages to move")
4173 uidFirst = mbDst.UIDNext
4175 // Assign a new modseq, for the new records and for the expunged records.
4177 modseq, err = c.account.NextModSeq(tx)
4178 xcheckf(err, "assigning next modseq")
4180 // Make query selecting messages to move.
4181 q := bstore.QueryTx[store.Message](tx)
4182 q.FilterNonzero(store.Message{MailboxID: mbSrc.ID})
4183 q.FilterEqual("UID", uidargs...)
4184 q.FilterEqual("Expunged", false)
4187 newIDs, chl := c.xmoveMessages(tx, q, len(uidargs), modseq, &mbSrc, &mbDst)
4188 changes = append(changes, chl...)
4194 c.broadcast(changes)
4199 newUIDs := numSet{ranges: []numRange{{setNumber{number: uint32(uidFirst)}, &setNumber{number: uint32(mbDst.UIDNext - 1)}}}}
4200 c.bwritelinef("* OK [COPYUID %d %s %s] moved", mbDst.UIDValidity, compactUIDSet(uids).String(), newUIDs.String())
4201 qresync := c.enabled[capQresync]
4202 var vanishedUIDs numSet
4203 for i := range uids {
4204 seq := c.xsequence(uids[i])
4205 c.sequenceRemove(seq, uids[i])
4207 vanishedUIDs.append(uint32(uids[i]))
4209 c.bwritelinef("* %d EXPUNGE", seq)
4212 if !vanishedUIDs.empty() {
4214 for _, s := range vanishedUIDs.Strings(4*1024 - 32) {
4215 c.bwritelinef("* VANISHED %s", s)
4221 c.writeresultf("%s OK [HIGHESTMODSEQ %d] move", tag, modseq.Client())
4227// q must yield messages from a single mailbox.
4228func (c *conn) xmoveMessages(tx *bstore.Tx, q *bstore.Query[store.Message], expectCount int, modseq store.ModSeq, mbSrc, mbDst *store.Mailbox) (newIDs []int64, changes []store.Change) {
4229 newIDs = make([]int64, 0, expectCount)
4235 for _, id := range newIDs {
4236 p := c.account.MessagePath(id)
4238 c.xsanity(err, "removing added message file %v", p)
4243 mbSrc.ModSeq = modseq
4244 mbDst.ModSeq = modseq
4249 err := jf.CloseDiscard()
4250 c.log.Check(err, "closing junk filter after error")
4254 accConf, _ := c.account.Conf()
4256 changeRemoveUIDs := store.ChangeRemoveUIDs{
4257 MailboxID: mbSrc.ID,
4260 changes = make([]store.Change, 0, expectCount+4) // mbsrc removeuids, mbsrc counts, mbdst counts, mbdst keywords
4262 nkeywords := len(mbDst.Keywords)
4266 xcheckf(err, "listing messages to move")
4268 if expectCount > 0 && len(l) != expectCount {
4269 xcheckf(fmt.Errorf("moved %d messages, expected %d", len(l), expectCount), "move messages")
4272 // For newly created message directories that we sync after hardlinking/copying files.
4273 syncDirs := map[string]struct{}{}
4275 for _, om := range l {
4277 nm.MailboxID = mbDst.ID
4278 nm.UID = mbDst.UIDNext
4281 nm.CreateSeq = modseq
4283 if nm.IsReject && nm.MailboxDestinedID != 0 {
4284 // Incorrectly delivered to Rejects mailbox. Adjust MailboxOrigID so this message
4285 // is used for reputation calculation during future deliveries.
4286 nm.MailboxOrigID = nm.MailboxDestinedID
4291 nm.JunkFlagsForMailbox(*mbDst, accConf)
4293 err := tx.Update(&nm)
4294 xcheckf(err, "updating message with new mailbox")
4296 mbDst.Add(nm.MailboxCounts())
4298 mbSrc.Sub(om.MailboxCounts())
4302 om.TrainedJunk = nil
4303 err = tx.Insert(&om)
4304 xcheckf(err, "inserting expunged message in old mailbox")
4306 dstPath := c.account.MessagePath(om.ID)
4307 dstDir := filepath.Dir(dstPath)
4308 if _, ok := syncDirs[dstDir]; !ok {
4309 os.MkdirAll(dstDir, 0770)
4310 syncDirs[dstDir] = struct{}{}
4313 err = moxio.LinkOrCopy(c.log, dstPath, c.account.MessagePath(nm.ID), nil, false)
4314 xcheckf(err, "duplicating message in old mailbox for current sessions")
4315 newIDs = append(newIDs, nm.ID)
4316 // We don't sync the directory. In case of a crash and files disappearing, the
4317 // eraser will simply not find the file at next startup.
4319 err = tx.Insert(&store.MessageErase{ID: om.ID, SkipUpdateDiskUsage: true})
4320 xcheckf(err, "insert message erase")
4322 mbDst.Keywords, _ = store.MergeKeywords(mbDst.Keywords, nm.Keywords)
4324 if accConf.JunkFilter != nil && nm.NeedsTraining() {
4325 // Lazily open junk filter.
4327 jf, _, err = c.account.OpenJunkFilter(context.TODO(), c.log)
4328 xcheckf(err, "open junk filter")
4330 err := c.account.RetrainMessage(context.TODO(), c.log, tx, jf, &nm)
4331 xcheckf(err, "retrain message after moving")
4334 changeRemoveUIDs.UIDs = append(changeRemoveUIDs.UIDs, om.UID)
4335 changeRemoveUIDs.MsgIDs = append(changeRemoveUIDs.MsgIDs, om.ID)
4336 changes = append(changes, nm.ChangeAddUID())
4338 xcheckf(err, "move messages")
4340 for dir := range syncDirs {
4341 err := moxio.SyncDir(c.log, dir)
4342 xcheckf(err, "sync directory")
4345 changes = append(changes, changeRemoveUIDs, mbSrc.ChangeCounts())
4347 err = tx.Update(mbSrc)
4348 xcheckf(err, "updating counts for inbox")
4350 changes = append(changes, mbDst.ChangeCounts())
4351 if len(mbDst.Keywords) > nkeywords {
4352 changes = append(changes, mbDst.ChangeKeywords())
4355 err = tx.Update(mbDst)
4356 xcheckf(err, "updating uidnext and counts in destination mailbox")
4361 xcheckf(err, "saving junk filter")
4368// Store sets a full set of flags, or adds/removes specific flags.
4371func (c *conn) cmdxStore(isUID bool, tag, cmd string, p *parser) {
4378 var unchangedSince *int64
4381 p.xtake("UNCHANGEDSINCE")
4388 c.xensureCondstore(nil)
4390 var plus, minus bool
4393 } else if p.take("-") {
4397 silent := p.take(".SILENT")
4399 var flagstrs []string
4400 if p.hasPrefix("(") {
4401 flagstrs = p.xflagList()
4403 flagstrs = append(flagstrs, p.xflag())
4405 flagstrs = append(flagstrs, p.xflag())
4411 xuserErrorf("mailbox open in read-only mode")
4414 flags, keywords, err := store.ParseFlagsKeywords(flagstrs)
4416 xuserErrorf("parsing flags: %v", err)
4418 var mask store.Flags
4420 mask, flags = flags, store.FlagsAll
4422 mask, flags = flags, store.Flags{}
4424 mask = store.FlagsAll
4427 var mb, origmb store.Mailbox
4428 var updated []store.Message
4429 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.
4430 var modseq store.ModSeq // Assigned when needed.
4431 modified := map[int64]bool{}
4433 c.account.WithWLock(func() {
4434 var mbKwChanged bool
4435 var changes []store.Change
4437 c.xdbwrite(func(tx *bstore.Tx) {
4438 mb = c.xmailboxID(tx, c.mailboxID) // Validate.
4441 uidargs := c.xnumSetCondition(isUID, nums)
4443 if len(uidargs) == 0 {
4447 // Ensure keywords are in mailbox.
4449 mb.Keywords, mbKwChanged = store.MergeKeywords(mb.Keywords, keywords)
4451 err := tx.Update(&mb)
4452 xcheckf(err, "updating mailbox with keywords")
4456 q := bstore.QueryTx[store.Message](tx)
4457 q.FilterNonzero(store.Message{MailboxID: c.mailboxID})
4458 q.FilterEqual("UID", uidargs...)
4459 q.FilterEqual("Expunged", false)
4460 err := q.ForEach(func(m store.Message) error {
4461 // Client may specify a message multiple times, but we only process it once.
../rfc/7162:823
4466 mc := m.MailboxCounts()
4468 origFlags := m.Flags
4469 m.Flags = m.Flags.Set(mask, flags)
4470 oldKeywords := slices.Clone(m.Keywords)
4472 m.Keywords, _ = store.RemoveKeywords(m.Keywords, keywords)
4474 m.Keywords, _ = store.MergeKeywords(m.Keywords, keywords)
4476 m.Keywords = keywords
4479 keywordsChanged := func() bool {
4480 sort.Strings(oldKeywords)
4481 n := slices.Clone(m.Keywords)
4483 return !slices.Equal(oldKeywords, n)
4486 // If the message has a more recent modseq than the check requires, we won't modify
4487 // it and report in the final command response.
4490 // unchangedSince 0 always fails the check, we don't turn it into 1 like with our
4491 // internal modseqs. RFC implies that is not required for non-system flags, but we
4493 if unchangedSince != nil && m.ModSeq.Client() > *unchangedSince {
4494 changed = append(changed, m)
4499 // It requires that we keep track of the flags we think the client knows (but only
4500 // on this connection). We don't track that. It also isn't clear why this is
4501 // allowed because it is skipping the condstore conditional check, and the new
4502 // combination of flags could be unintended.
4505 if origFlags == m.Flags && !keywordsChanged() {
4506 // Note: since we didn't update the modseq, we are not adding m.ID to "modified",
4507 // it would skip the modseq check above. We still add m to list of updated, so we
4508 // send an untagged fetch response. But we don't broadcast it.
4509 updated = append(updated, m)
4514 mb.Add(m.MailboxCounts())
4516 // Assign new modseq for first actual change.
4519 modseq, err = c.account.NextModSeq(tx)
4520 xcheckf(err, "next modseq")
4524 modified[m.ID] = true
4525 updated = append(updated, m)
4527 changes = append(changes, m.ChangeFlags(origFlags))
4529 return tx.Update(&m)
4531 xcheckf(err, "storing flags in messages")
4533 if mb.MailboxCounts != origmb.MailboxCounts || modseq != 0 {
4534 err := tx.Update(&mb)
4535 xcheckf(err, "updating mailbox counts")
4537 if mb.MailboxCounts != origmb.MailboxCounts {
4538 changes = append(changes, mb.ChangeCounts())
4541 changes = append(changes, mb.ChangeKeywords())
4544 err = c.account.RetrainMessages(context.TODO(), c.log, tx, updated)
4545 xcheckf(err, "training messages")
4548 c.broadcast(changes)
4551 // In the RFC, the section about STORE/UID STORE says we must return MODSEQ when
4552 // UNCHANGEDSINCE was specified. It does not specify it in case UNCHANGEDSINCE
4553 // isn't specified. For that case it does say MODSEQ is needed in unsolicited
4554 // untagged fetch responses. Implying that solicited untagged fetch responses
4555 // should not include MODSEQ (why else mention unsolicited explicitly?). But, in
4556 // the introduction to CONDSTORE it does explicitly specify MODSEQ should be
4557 // included in untagged fetch responses at all times with CONDSTORE-enabled
4558 // connections. It would have been better if the command behaviour was specified in
4559 // the command section, not the introduction to the extension.
4562 if !silent || c.enabled[capCondstore] {
4563 for _, m := range updated {
4566 flags = fmt.Sprintf(" FLAGS %s", flaglist(m.Flags, m.Keywords).pack(c))
4568 var modseqStr string
4569 if c.enabled[capCondstore] {
4570 modseqStr = fmt.Sprintf(" MODSEQ (%d)", m.ModSeq.Client())
4573 c.bwritelinef("* %d FETCH (UID %d%s%s)", c.xsequence(m.UID), m.UID, flags, modseqStr)
4577 // We don't explicitly send flags for failed updated with silent set. The regular
4578 // notification will get the flags to the client.
4581 if len(changed) == 0 {
4586 // Write unsolicited untagged fetch responses for messages that didn't pass the
4589 var mnums []store.UID
4590 for _, m := range changed {
4591 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())
4593 mnums = append(mnums, m.UID)
4595 mnums = append(mnums, store.UID(c.xsequence(m.UID)))
4600 set := compactUIDSet(mnums)
4602 c.writeresultf("%s OK [MODIFIED %s] conditional store did not modify all", tag, set.String())