1// Package imapserver implements an IMAPv4 server, rev2 (RFC 9051) and rev1 with extensions (RFC 3501 and more).
 
7IMAP4rev2 includes functionality that was in extensions for IMAP4rev1. The
 
8extensions sometimes include features not in IMAP4rev2. We want IMAP4rev1-only
 
9implementations to use extensions, so we implement the full feature set of the
 
10extension and announce it as capability. The extensions: LITERAL+, IDLE,
 
11NAMESPACE, BINARY, UNSELECT, UIDPLUS, ESEARCH, SEARCHRES, SASL-IR, ENABLE,
 
12LIST-EXTENDED, SPECIAL-USE, MOVE, UTF8=ONLY.
 
14We take a liberty with UTF8=ONLY. We are supposed to wait for ENABLE of
 
15UTF8=ACCEPT or IMAP4rev2 before we respond with quoted strings that contain
 
16non-ASCII UTF-8. Until that's enabled, we do use UTF-7 for mailbox names. See
 
19- We never execute multiple commands at the same time for a connection. We expect a client to open multiple connections instead. 
../rfc/9051:1110 
20- Do not write output on a connection with an account lock held. Writing can block, a slow client could block account operations.
 
21- When handling commands that modify the selected mailbox, always check that the mailbox is not opened readonly. And always revalidate the selected mailbox, another session may have deleted the mailbox.
 
22- After making changes to an account/mailbox/message, you must broadcast changes. You must do this with the account lock held. Otherwise, other later changes (e.g. message deliveries) may be made and broadcast before changes that were made earlier. Make sure to commit changes in the database first, because the commit may fail.
 
23- Mailbox hierarchies are slash separated, no leading slash. We keep the case, except INBOX is renamed to Inbox, also for submailboxes in INBOX. We don't allow existence of a child where its parent does not exist. We have no \NoInferiors or \NoSelect. Newly created mailboxes are automatically subscribed.
 
24- For CONDSTORE and QRESYNC support, we set "modseq" for each change/expunge. Once expunged, a modseq doesn't change anymore. We don't yet remove old expunged records. The records aren't too big. Next step may be to let an admin reclaim space manually.
 
28- todo: do not return binary data for a fetch body. at least not for imap4rev1. we should be encoding it as base64?
 
29- todo: on expunge we currently remove the message even if other sessions still have a reference to the uid. if they try to query the uid, they'll get an error. we could be nicer and only actually remove the message when the last reference has gone. we could add a new flag to store.Message marking the message as expunged, not give new session access to such messages, and make store remove them at startup, and clean them when the last session referencing the session goes. however, it will get much more complicated. renaming messages would need special handling. and should we do the same for removed mailboxes?
 
30- todo: try to recover from syntax errors when the last command line ends with a }, i.e. a literal. we currently abort the entire connection. we may want to read some amount of literal data and continue with a next command.
 
31- todo future: more extensions: OBJECTID, MULTISEARCH, REPLACE, NOTIFY, CATENATE, MULTIAPPEND, SORT, THREAD, CREATE-SPECIAL-USE.
 
59	"golang.org/x/exp/maps"
 
60	"golang.org/x/exp/slices"
 
61	"golang.org/x/exp/slog"
 
63	"github.com/prometheus/client_golang/prometheus"
 
64	"github.com/prometheus/client_golang/prometheus/promauto"
 
66	"github.com/mjl-/bstore"
 
68	"github.com/mjl-/mox/config"
 
69	"github.com/mjl-/mox/message"
 
70	"github.com/mjl-/mox/metrics"
 
71	"github.com/mjl-/mox/mlog"
 
72	"github.com/mjl-/mox/mox-"
 
73	"github.com/mjl-/mox/moxio"
 
74	"github.com/mjl-/mox/moxvar"
 
75	"github.com/mjl-/mox/ratelimit"
 
76	"github.com/mjl-/mox/scram"
 
77	"github.com/mjl-/mox/store"
 
81	metricIMAPConnection = promauto.NewCounterVec(
 
82		prometheus.CounterOpts{
 
83			Name: "mox_imap_connection_total",
 
84			Help: "Incoming IMAP connections.",
 
87			"service", // imap, imaps
 
90	metricIMAPCommands = promauto.NewHistogramVec(
 
91		prometheus.HistogramOpts{
 
92			Name:    "mox_imap_command_duration_seconds",
 
93			Help:    "IMAP command duration and result codes in seconds.",
 
94			Buckets: []float64{0.001, 0.005, 0.01, 0.05, 0.100, 0.5, 1, 5, 10, 20},
 
98			"result", // ok, panic, ioerror, badsyntax, servererror, usererror, error
 
103var limiterConnectionrate, limiterConnections *ratelimit.Limiter
 
106	// Also called by tests, so they don't trigger the rate limiter.
 
112	limiterConnectionrate = &ratelimit.Limiter{
 
113		WindowLimits: []ratelimit.WindowLimit{
 
116				Limits: [...]int64{300, 900, 2700},
 
120	limiterConnections = &ratelimit.Limiter{
 
121		WindowLimits: []ratelimit.WindowLimit{
 
123				Window: time.Duration(math.MaxInt64), // All of time.
 
124				Limits: [...]int64{30, 90, 270},
 
130// Delay after bad/suspicious behaviour. Tests set these to zero.
 
131var badClientDelay = time.Second // Before reads and after 1-byte writes for probably spammers.
 
132var authFailDelay = time.Second  // After authentication failure.
 
134// Capabilities (extensions) the server supports. Connections will add a few more, e.g. STARTTLS, LOGINDISABLED, AUTH=PLAIN.
 
158// We always announce support for SCRAM PLUS-variants, also on connections without
 
159// TLS. The client should not be selecting PLUS variants on non-TLS connections,
 
160// instead opting to do the bare SCRAM variant without indicating the server claims
 
161// to support the PLUS variant (skipping the server downgrade detection check).
 
162const serverCapabilities = "IMAP4rev2 IMAP4rev1 ENABLE LITERAL+ IDLE SASL-IR BINARY UNSELECT UIDPLUS ESEARCH SEARCHRES MOVE UTF8=ACCEPT LIST-EXTENDED SPECIAL-USE LIST-STATUS AUTH=SCRAM-SHA-256-PLUS AUTH=SCRAM-SHA-256 AUTH=SCRAM-SHA-1-PLUS AUTH=SCRAM-SHA-1 AUTH=CRAM-MD5 ID APPENDLIMIT=9223372036854775807 CONDSTORE QRESYNC STATUS=SIZE"
 
168	tls               bool               // Whether TLS has been initialized.
 
169	br                *bufio.Reader      // From remote, with TLS unwrapped in case of TLS.
 
170	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.
 
171	lastLine          string             // For detecting if syntax error is fatal, i.e. if this ends with a literal. Without crlf.
 
172	bw                *bufio.Writer      // To remote, with TLS added in case of TLS.
 
173	tr                *moxio.TraceReader // Kept to change trace level when reading/writing cmd/auth/data.
 
174	tw                *moxio.TraceWriter
 
175	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.
 
176	lastlog           time.Time   // For printing time since previous log line.
 
177	tlsConfig         *tls.Config // TLS config to use for handshake.
 
179	noRequireSTARTTLS bool
 
180	cmd               string // Currently executing, for deciding to applyChanges and logging.
 
181	cmdMetric         string // Currently executing, for metrics.
 
183	ncmds             int // Number of commands processed. Used to abort connection when first incoming command is unknown/invalid.
 
185	enabled           map[capability]bool // All upper-case.
 
187	// Set by SEARCH with SAVE. Can be used by commands accepting a sequence-set with
 
188	// value "$". When used, UIDs must be verified to still exist, because they may
 
189	// have been expunged. Cleared by a SELECT or EXAMINE.
 
190	// Nil means no searchResult is present. An empty list is a valid searchResult,
 
191	// just not matching any messages.
 
193	searchResult []store.UID
 
195	// Only when authenticated.
 
196	authFailed int    // Number of failed auth attempts. For slowing down remote with many failures.
 
197	username   string // Full username as used during login.
 
198	account    *store.Account
 
199	comm       *store.Comm // For sending/receiving changes on mailboxes in account, e.g. from messages incoming on smtp, or another imap client.
 
201	mailboxID int64       // Only for StateSelected.
 
202	readonly  bool        // If opened mailbox is readonly.
 
203	uids      []store.UID // UIDs known in this session, sorted. todo future: store more space-efficiently, as ranges.
 
206// capability for use with ENABLED and CAPABILITY. We always keep this upper case,
 
207// e.g. IMAP4REV2. These values are treated case-insensitive, but it's easier for
 
208// comparison to just always have the same case.
 
209type capability string
 
212	capIMAP4rev2  capability = "IMAP4REV2"
 
213	capUTF8Accept capability = "UTF8=ACCEPT"
 
214	capCondstore  capability = "CONDSTORE"
 
215	capQresync    capability = "QRESYNC"
 
226	stateNotAuthenticated state = iota
 
231func stateCommands(cmds ...string) map[string]struct{} {
 
232	r := map[string]struct{}{}
 
233	for _, cmd := range cmds {
 
240	commandsStateAny              = stateCommands("capability", "noop", "logout", "id")
 
241	commandsStateNotAuthenticated = stateCommands("starttls", "authenticate", "login")
 
242	commandsStateAuthenticated    = stateCommands("enable", "select", "examine", "create", "delete", "rename", "subscribe", "unsubscribe", "list", "namespace", "status", "append", "idle", "lsub")
 
243	commandsStateSelected         = stateCommands("close", "unselect", "expunge", "search", "fetch", "store", "copy", "move", "uid expunge", "uid search", "uid fetch", "uid store", "uid copy", "uid move")
 
246var commands = map[string]func(c *conn, tag, cmd string, p *parser){
 
248	"capability": (*conn).cmdCapability,
 
249	"noop":       (*conn).cmdNoop,
 
250	"logout":     (*conn).cmdLogout,
 
254	"starttls":     (*conn).cmdStarttls,
 
255	"authenticate": (*conn).cmdAuthenticate,
 
256	"login":        (*conn).cmdLogin,
 
258	// Authenticated and selected.
 
259	"enable":      (*conn).cmdEnable,
 
260	"select":      (*conn).cmdSelect,
 
261	"examine":     (*conn).cmdExamine,
 
262	"create":      (*conn).cmdCreate,
 
263	"delete":      (*conn).cmdDelete,
 
264	"rename":      (*conn).cmdRename,
 
265	"subscribe":   (*conn).cmdSubscribe,
 
266	"unsubscribe": (*conn).cmdUnsubscribe,
 
267	"list":        (*conn).cmdList,
 
268	"lsub":        (*conn).cmdLsub,
 
269	"namespace":   (*conn).cmdNamespace,
 
270	"status":      (*conn).cmdStatus,
 
271	"append":      (*conn).cmdAppend,
 
272	"idle":        (*conn).cmdIdle,
 
275	"check":       (*conn).cmdCheck,
 
276	"close":       (*conn).cmdClose,
 
277	"unselect":    (*conn).cmdUnselect,
 
278	"expunge":     (*conn).cmdExpunge,
 
279	"uid expunge": (*conn).cmdUIDExpunge,
 
280	"search":      (*conn).cmdSearch,
 
281	"uid search":  (*conn).cmdUIDSearch,
 
282	"fetch":       (*conn).cmdFetch,
 
283	"uid fetch":   (*conn).cmdUIDFetch,
 
284	"store":       (*conn).cmdStore,
 
285	"uid store":   (*conn).cmdUIDStore,
 
286	"copy":        (*conn).cmdCopy,
 
287	"uid copy":    (*conn).cmdUIDCopy,
 
288	"move":        (*conn).cmdMove,
 
289	"uid move":    (*conn).cmdUIDMove,
 
292var errIO = errors.New("io error")             // For read/write errors and errors that should close the connection.
 
293var errProtocol = errors.New("protocol error") // For protocol errors for which a stack trace should be printed.
 
297// check err for sanity.
 
298// if not nil and checkSanity true (set during tests), then panic. if not nil during normal operation, just log.
 
299func (c *conn) xsanity(err error, format string, args ...any) {
 
304		panic(fmt.Errorf("%s: %s", fmt.Sprintf(format, args...), err))
 
306	c.log.Errorx(fmt.Sprintf(format, args...), err)
 
311// Listen initializes all imap listeners for the configuration, and stores them for Serve to start them.
 
313	names := maps.Keys(mox.Conf.Static.Listeners)
 
315	for _, name := range names {
 
316		listener := mox.Conf.Static.Listeners[name]
 
318		var tlsConfig *tls.Config
 
319		if listener.TLS != nil {
 
320			tlsConfig = listener.TLS.Config
 
323		if listener.IMAP.Enabled {
 
324			port := config.Port(listener.IMAP.Port, 143)
 
325			for _, ip := range listener.IPs {
 
326				listen1("imap", name, ip, port, tlsConfig, false, listener.IMAP.NoRequireSTARTTLS)
 
330		if listener.IMAPS.Enabled {
 
331			port := config.Port(listener.IMAPS.Port, 993)
 
332			for _, ip := range listener.IPs {
 
333				listen1("imaps", name, ip, port, tlsConfig, true, false)
 
341func listen1(protocol, listenerName, ip string, port int, tlsConfig *tls.Config, xtls, noRequireSTARTTLS bool) {
 
342	log := mlog.New("imapserver", nil)
 
343	addr := net.JoinHostPort(ip, fmt.Sprintf("%d", port))
 
344	if os.Getuid() == 0 {
 
345		log.Print("listening for imap",
 
346			slog.String("listener", listenerName),
 
347			slog.String("addr", addr),
 
348			slog.String("protocol", protocol))
 
350	network := mox.Network(ip)
 
351	ln, err := mox.Listen(network, addr)
 
353		log.Fatalx("imap: listen for imap", err, slog.String("protocol", protocol), slog.String("listener", listenerName))
 
356		ln = tls.NewListener(ln, tlsConfig)
 
361			conn, err := ln.Accept()
 
363				log.Infox("imap: accept", err, slog.String("protocol", protocol), slog.String("listener", listenerName))
 
367			metricIMAPConnection.WithLabelValues(protocol).Inc()
 
368			go serve(listenerName, mox.Cid(), tlsConfig, conn, xtls, noRequireSTARTTLS)
 
372	servers = append(servers, serve)
 
375// Serve starts serving on all listeners, launching a goroutine per listener.
 
377	for _, serve := range servers {
 
383// returns whether this connection accepts utf-8 in strings.
 
384func (c *conn) utf8strings() bool {
 
385	return c.enabled[capIMAP4rev2] || c.enabled[capUTF8Accept]
 
388func (c *conn) encodeMailbox(s string) string {
 
395func (c *conn) xdbwrite(fn func(tx *bstore.Tx)) {
 
396	err := c.account.DB.Write(context.TODO(), func(tx *bstore.Tx) error {
 
400	xcheckf(err, "transaction")
 
403func (c *conn) xdbread(fn func(tx *bstore.Tx)) {
 
404	err := c.account.DB.Read(context.TODO(), func(tx *bstore.Tx) error {
 
408	xcheckf(err, "transaction")
 
411// Closes the currently selected/active mailbox, setting state from selected to authenticated.
 
412// Does not remove messages marked for deletion.
 
413func (c *conn) unselect() {
 
414	if c.state == stateSelected {
 
415		c.state = stateAuthenticated
 
421func (c *conn) setSlow(on bool) {
 
423		c.log.Debug("connection changed to slow")
 
424	} else if !on && c.slow {
 
425		c.log.Debug("connection restored to regular pace")
 
430// Write makes a connection an io.Writer. It panics for i/o errors. These errors
 
431// are handled in the connection command loop.
 
432func (c *conn) Write(buf []byte) (int, error) {
 
440		err := c.conn.SetWriteDeadline(time.Now().Add(30 * time.Second))
 
441		c.log.Check(err, "setting write deadline")
 
443		nn, err := c.conn.Write(buf[:chunk])
 
445			panic(fmt.Errorf("write: %s (%w)", err, errIO))
 
449		if len(buf) > 0 && badClientDelay > 0 {
 
450			mox.Sleep(mox.Context, badClientDelay)
 
456func (c *conn) xtrace(level slog.Level) func() {
 
462		c.tr.SetTrace(mlog.LevelTrace)
 
463		c.tw.SetTrace(mlog.LevelTrace)
 
467// Cache of line buffers for reading commands.
 
469var bufpool = moxio.NewBufpool(8, 16*1024)
 
471// read line from connection, not going through line channel.
 
472func (c *conn) readline0() (string, error) {
 
473	if c.slow && badClientDelay > 0 {
 
474		mox.Sleep(mox.Context, badClientDelay)
 
477	d := 30 * time.Minute
 
478	if c.state == stateNotAuthenticated {
 
481	err := c.conn.SetReadDeadline(time.Now().Add(d))
 
482	c.log.Check(err, "setting read deadline")
 
484	line, err := bufpool.Readline(c.log, c.br)
 
485	if err != nil && errors.Is(err, moxio.ErrLineTooLong) {
 
486		return "", fmt.Errorf("%s (%w)", err, errProtocol)
 
487	} else if err != nil {
 
488		return "", fmt.Errorf("%s (%w)", err, errIO)
 
493func (c *conn) lineChan() chan lineErr {
 
495		c.line = make(chan lineErr, 1)
 
497			line, err := c.readline0()
 
498			c.line <- lineErr{line, err}
 
504// readline from either the c.line channel, or otherwise read from connection.
 
505func (c *conn) readline(readCmd bool) string {
 
511		line, err = le.line, le.err
 
513		line, err = c.readline0()
 
516		if readCmd && errors.Is(err, os.ErrDeadlineExceeded) {
 
517			err := c.conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
 
518			c.log.Check(err, "setting write deadline")
 
519			c.writelinef("* BYE inactive")
 
521		if !errors.Is(err, errIO) && !errors.Is(err, errProtocol) {
 
522			err = fmt.Errorf("%s (%w)", err, errIO)
 
528	// We typically respond immediately (IDLE is an exception).
 
529	// The client may not be reading, or may have disappeared.
 
530	// Don't wait more than 5 minutes before closing down the connection.
 
531	// The write deadline is managed in IDLE as well.
 
532	// For unauthenticated connections, we require the client to read faster.
 
533	wd := 5 * time.Minute
 
534	if c.state == stateNotAuthenticated {
 
535		wd = 30 * time.Second
 
537	err = c.conn.SetWriteDeadline(time.Now().Add(wd))
 
538	c.log.Check(err, "setting write deadline")
 
543// write tagged command response, but first write pending changes.
 
544func (c *conn) writeresultf(format string, args ...any) {
 
545	c.bwriteresultf(format, args...)
 
549// write buffered tagged command response, but first write pending changes.
 
550func (c *conn) bwriteresultf(format string, args ...any) {
 
552	case "fetch", "store", "search":
 
556			c.applyChanges(c.comm.Get(), false)
 
559	c.bwritelinef(format, args...)
 
562func (c *conn) writelinef(format string, args ...any) {
 
563	c.bwritelinef(format, args...)
 
567// Buffer line for write.
 
568func (c *conn) bwritelinef(format string, args ...any) {
 
570	fmt.Fprintf(c.bw, format, args...)
 
573func (c *conn) xflush() {
 
575	xcheckf(err, "flush") // Should never happen, the Write caused by the Flush should panic on i/o error.
 
578func (c *conn) readCommand(tag *string) (cmd string, p *parser) {
 
579	line := c.readline(true)
 
580	p = newParser(line, c)
 
586	return cmd, newParser(p.remainder(), c)
 
589func (c *conn) xreadliteral(size int64, sync bool) string {
 
593	buf := make([]byte, size)
 
595		if err := c.conn.SetReadDeadline(time.Now().Add(30 * time.Second)); err != nil {
 
596			c.log.Errorx("setting read deadline", err)
 
599		_, err := io.ReadFull(c.br, buf)
 
601			// Cannot use xcheckf due to %w handling of errIO.
 
602			panic(fmt.Errorf("reading literal: %s (%w)", err, errIO))
 
608func (c *conn) xhighestModSeq(tx *bstore.Tx, mailboxID int64) store.ModSeq {
 
609	qms := bstore.QueryTx[store.Message](tx)
 
610	qms.FilterNonzero(store.Message{MailboxID: mailboxID})
 
611	qms.SortDesc("ModSeq")
 
614	if err == bstore.ErrAbsent {
 
615		return store.ModSeq(0)
 
617	xcheckf(err, "looking up highest modseq for mailbox")
 
621var cleanClose struct{} // Sentinel value for panic/recover indicating clean close of connection.
 
623func serve(listenerName string, cid int64, tlsConfig *tls.Config, nc net.Conn, xtls, noRequireSTARTTLS bool) {
 
625	if a, ok := nc.RemoteAddr().(*net.TCPAddr); ok {
 
628		// For net.Pipe, during tests.
 
629		remoteIP = net.ParseIP("127.0.0.10")
 
637		tlsConfig:         tlsConfig,
 
639		noRequireSTARTTLS: noRequireSTARTTLS,
 
640		enabled:           map[capability]bool{},
 
642		cmdStart:          time.Now(),
 
644	var logmutex sync.Mutex
 
645	c.log = mlog.New("imapserver", nil).WithFunc(func() []slog.Attr {
 
647		defer logmutex.Unlock()
 
650			slog.Int64("cid", c.cid),
 
651			slog.Duration("delta", now.Sub(c.lastlog)),
 
654		if c.username != "" {
 
655			l = append(l, slog.String("username", c.username))
 
659	c.tr = moxio.NewTraceReader(c.log, "C: ", c.conn)
 
660	c.tw = moxio.NewTraceWriter(c.log, "S: ", c)
 
661	// 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.
 
662	c.br = bufio.NewReader(c.tr)
 
663	c.bw = bufio.NewWriter(c.tw)
 
665	// Many IMAP connections use IDLE to wait for new incoming messages. We'll enable
 
666	// keepalive to get a higher chance of the connection staying alive, or otherwise
 
667	// detecting broken connections early.
 
670		xconn = c.conn.(*tls.Conn).NetConn()
 
672	if tcpconn, ok := xconn.(*net.TCPConn); ok {
 
673		if err := tcpconn.SetKeepAlivePeriod(5 * time.Minute); err != nil {
 
674			c.log.Errorx("setting keepalive period", err)
 
675		} else if err := tcpconn.SetKeepAlive(true); err != nil {
 
676			c.log.Errorx("enabling keepalive", err)
 
680	c.log.Info("new connection",
 
681		slog.Any("remote", c.conn.RemoteAddr()),
 
682		slog.Any("local", c.conn.LocalAddr()),
 
683		slog.Bool("tls", xtls),
 
684		slog.String("listener", listenerName))
 
689		if c.account != nil {
 
691			err := c.account.Close()
 
692			c.xsanity(err, "close account")
 
698		if x == nil || x == cleanClose {
 
699			c.log.Info("connection closed")
 
700		} else if err, ok := x.(error); ok && isClosed(err) {
 
701			c.log.Infox("connection closed", err)
 
703			c.log.Error("unhandled panic", slog.Any("err", x))
 
705			metrics.PanicInc(metrics.Imapserver)
 
710	case <-mox.Shutdown.Done():
 
712		c.writelinef("* BYE mox shutting down")
 
717	if !limiterConnectionrate.Add(c.remoteIP, time.Now(), 1) {
 
718		c.writelinef("* BYE connection rate from your ip or network too high, slow down please")
 
722	// If remote IP/network resulted in too many authentication failures, refuse to serve.
 
723	if !mox.LimiterFailedAuth.CanAdd(c.remoteIP, time.Now(), 1) {
 
724		metrics.AuthenticationRatelimitedInc("imap")
 
725		c.log.Debug("refusing connection due to many auth failures", slog.Any("remoteip", c.remoteIP))
 
726		c.writelinef("* BYE too many auth failures")
 
730	if !limiterConnections.Add(c.remoteIP, time.Now(), 1) {
 
731		c.log.Debug("refusing connection due to many open connections", slog.Any("remoteip", c.remoteIP))
 
732		c.writelinef("* BYE too many open connections from your ip or network")
 
735	defer limiterConnections.Add(c.remoteIP, time.Now(), -1)
 
737	// We register and unregister the original connection, in case it c.conn is
 
738	// replaced with a TLS connection later on.
 
739	mox.Connections.Register(nc, "imap", listenerName)
 
740	defer mox.Connections.Unregister(nc)
 
742	c.writelinef("* OK [CAPABILITY %s] mox imap", c.capabilities())
 
746		c.xflush() // For flushing errors, or possibly commands that did not flush explicitly.
 
750// isClosed returns whether i/o failed, typically because the connection is closed.
 
751// For connection errors, we often want to generate fewer logs.
 
752func isClosed(err error) bool {
 
753	return errors.Is(err, errIO) || errors.Is(err, errProtocol) || moxio.IsClosed(err)
 
756func (c *conn) command() {
 
757	var tag, cmd, cmdlow string
 
763			metricIMAPCommands.WithLabelValues(c.cmdMetric, result).Observe(float64(time.Since(c.cmdStart)) / float64(time.Second))
 
766		logFields := []slog.Attr{
 
767			slog.String("cmd", c.cmd),
 
768			slog.Duration("duration", time.Since(c.cmdStart)),
 
773		if x == nil || x == cleanClose {
 
774			c.log.Debug("imap command done", logFields...)
 
783			c.log.Error("imap command panic", append([]slog.Attr{slog.Any("panic", x)}, logFields...)...)
 
788		var sxerr syntaxError
 
792			c.log.Infox("imap command ioerror", err, logFields...)
 
794			if errors.Is(err, errProtocol) {
 
798		} else if errors.As(err, &sxerr) {
 
801				// Other side is likely speaking something else than IMAP, send error message and
 
802				// stop processing because there is a good chance whatever they sent has multiple
 
804				c.writelinef("* BYE please try again speaking imap")
 
807			c.log.Debugx("imap command syntax error", sxerr.err, logFields...)
 
808			c.log.Info("imap syntax error", slog.String("lastline", c.lastLine))
 
809			fatal := strings.HasSuffix(c.lastLine, "+}")
 
811				err := c.conn.SetWriteDeadline(time.Now().Add(5 * time.Second))
 
812				c.log.Check(err, "setting write deadline")
 
814			if sxerr.line != "" {
 
815				c.bwritelinef("%s", sxerr.line)
 
818			if sxerr.code != "" {
 
819				code = "[" + sxerr.code + "] "
 
821			c.bwriteresultf("%s BAD %s%s unrecognized syntax/command: %v", tag, code, cmd, sxerr.errmsg)
 
824				panic(fmt.Errorf("aborting connection after syntax error for command with non-sync literal: %w", errProtocol))
 
826		} else if errors.As(err, &serr) {
 
827			result = "servererror"
 
828			c.log.Errorx("imap command server error", err, logFields...)
 
830			c.bwriteresultf("%s NO %s %v", tag, cmd, err)
 
831		} else if errors.As(err, &uerr) {
 
833			c.log.Debugx("imap command user error", err, logFields...)
 
835				c.bwriteresultf("%s NO [%s] %s %v", tag, uerr.code, cmd, err)
 
837				c.bwriteresultf("%s NO %s %v", tag, cmd, err)
 
840			// Other type of panic, we pass it on, aborting the connection.
 
842			c.log.Errorx("imap command panic", err, logFields...)
 
848	cmd, p = c.readCommand(&tag)
 
849	cmdlow = strings.ToLower(cmd)
 
851	c.cmdStart = time.Now()
 
852	c.cmdMetric = "(unrecognized)"
 
855	case <-mox.Shutdown.Done():
 
857		c.writelinef("* BYE shutting down")
 
862	fn := commands[cmdlow]
 
864		xsyntaxErrorf("unknown command %q", cmd)
 
869	// Check if command is allowed in this state.
 
870	if _, ok1 := commandsStateAny[cmdlow]; ok1 {
 
871	} else if _, ok2 := commandsStateNotAuthenticated[cmdlow]; ok2 && c.state == stateNotAuthenticated {
 
872	} else if _, ok3 := commandsStateAuthenticated[cmdlow]; ok3 && c.state == stateAuthenticated || c.state == stateSelected {
 
873	} else if _, ok4 := commandsStateSelected[cmdlow]; ok4 && c.state == stateSelected {
 
874	} else if ok1 || ok2 || ok3 || ok4 {
 
875		xuserErrorf("not allowed in this connection state")
 
877		xserverErrorf("unrecognized command")
 
883func (c *conn) broadcast(changes []store.Change) {
 
884	if len(changes) == 0 {
 
887	c.log.Debug("broadcast changes", slog.Any("changes", changes))
 
888	c.comm.Broadcast(changes)
 
891// matchStringer matches a string against reference + mailbox patterns.
 
892type matchStringer interface {
 
893	MatchString(s string) bool
 
898// MatchString for noMatch always returns false.
 
899func (noMatch) MatchString(s string) bool {
 
903// xmailboxPatternMatcher returns a matcher for mailbox names given the reference and patterns.
 
904// Patterns can include "%" and "*", matching any character excluding and including a slash respectively.
 
905func xmailboxPatternMatcher(ref string, patterns []string) matchStringer {
 
906	if strings.HasPrefix(ref, "/") {
 
911	for _, pat := range patterns {
 
912		if strings.HasPrefix(pat, "/") {
 
918			s = path.Join(ref, pat)
 
921		// Fix casing for all Inbox paths.
 
922		first := strings.SplitN(s, "/", 2)[0]
 
923		if strings.EqualFold(first, "Inbox") {
 
924			s = "Inbox" + s[len("Inbox"):]
 
929		for _, c := range s {
 
935				rs += regexp.QuoteMeta(string(c))
 
938		subs = append(subs, rs)
 
944	rs := "^(" + strings.Join(subs, "|") + ")$"
 
945	re, err := regexp.Compile(rs)
 
946	xcheckf(err, "compiling regexp for mailbox patterns")
 
950func (c *conn) sequence(uid store.UID) msgseq {
 
951	return uidSearch(c.uids, uid)
 
954func uidSearch(uids []store.UID, uid store.UID) msgseq {
 
971func (c *conn) xsequence(uid store.UID) msgseq {
 
972	seq := c.sequence(uid)
 
974		xserverErrorf("unknown uid %d (%w)", uid, errProtocol)
 
979func (c *conn) sequenceRemove(seq msgseq, uid store.UID) {
 
981	if c.uids[i] != uid {
 
982		xserverErrorf(fmt.Sprintf("got uid %d at msgseq %d, expected uid %d", uid, seq, c.uids[i]))
 
984	copy(c.uids[i:], c.uids[i+1:])
 
985	c.uids = c.uids[:len(c.uids)-1]
 
991// add uid to the session. care must be taken that pending changes are fetched
 
992// while holding the account wlock, and applied before adding this uid, because
 
993// those pending changes may contain another new uid that has to be added first.
 
994func (c *conn) uidAppend(uid store.UID) {
 
995	if uidSearch(c.uids, uid) > 0 {
 
996		xserverErrorf("uid already present (%w)", errProtocol)
 
998	if len(c.uids) > 0 && uid < c.uids[len(c.uids)-1] {
 
999		xserverErrorf("new uid %d is smaller than last uid %d (%w)", uid, c.uids[len(c.uids)-1], errProtocol)
 
1001	c.uids = append(c.uids, uid)
 
1007// sanity check that uids are in ascending order.
 
1008func checkUIDs(uids []store.UID) {
 
1009	for i, uid := range uids {
 
1010		if uid == 0 || i > 0 && uid <= uids[i-1] {
 
1011			xserverErrorf("bad uids %v", uids)
 
1016func (c *conn) xnumSetUIDs(isUID bool, nums numSet) []store.UID {
 
1017	_, uids := c.xnumSetConditionUIDs(false, true, isUID, nums)
 
1021func (c *conn) xnumSetCondition(isUID bool, nums numSet) []any {
 
1022	uidargs, _ := c.xnumSetConditionUIDs(true, false, isUID, nums)
 
1026func (c *conn) xnumSetConditionUIDs(forDB, returnUIDs bool, isUID bool, nums numSet) ([]any, []store.UID) {
 
1027	if nums.searchResult {
 
1028		// Update previously stored UIDs. Some may have been deleted.
 
1029		// Once deleted a UID will never come back, so we'll just remove those uids.
 
1031		for _, uid := range c.searchResult {
 
1032			if uidSearch(c.uids, uid) > 0 {
 
1033				c.searchResult[o] = uid
 
1037		c.searchResult = c.searchResult[:o]
 
1038		uidargs := make([]any, len(c.searchResult))
 
1039		for i, uid := range c.searchResult {
 
1042		return uidargs, c.searchResult
 
1046	var uids []store.UID
 
1048	add := func(uid store.UID) {
 
1050			uidargs = append(uidargs, uid)
 
1053			uids = append(uids, uid)
 
1058		// Sequence numbers that don't exist, or * on an empty mailbox, should result in a BAD response. 
../rfc/9051:7018 
1059		for _, r := range nums.ranges {
 
1062				if len(c.uids) == 0 {
 
1063					xsyntaxErrorf("invalid seqset * on empty mailbox")
 
1065				ia = len(c.uids) - 1
 
1067				ia = int(r.first.number - 1)
 
1068				if ia >= len(c.uids) {
 
1069					xsyntaxErrorf("msgseq %d not in mailbox", r.first.number)
 
1078				if len(c.uids) == 0 {
 
1079					xsyntaxErrorf("invalid seqset * on empty mailbox")
 
1081				ib = len(c.uids) - 1
 
1083				ib = int(r.last.number - 1)
 
1084				if ib >= len(c.uids) {
 
1085					xsyntaxErrorf("msgseq %d not in mailbox", r.last.number)
 
1091			for _, uid := range c.uids[ia : ib+1] {
 
1095		return uidargs, uids
 
1098	// UIDs that do not exist can be ignored.
 
1099	if len(c.uids) == 0 {
 
1103	for _, r := range nums.ranges {
 
1109		uida := store.UID(r.first.number)
 
1111			uida = c.uids[len(c.uids)-1]
 
1114		uidb := store.UID(last.number)
 
1116			uidb = c.uids[len(c.uids)-1]
 
1120			uida, uidb = uidb, uida
 
1123		// Binary search for uida.
 
1128			if uida < c.uids[m] {
 
1130			} else if uida > c.uids[m] {
 
1137		for _, uid := range c.uids[s:] {
 
1138			if uid >= uida && uid <= uidb {
 
1140			} else if uid > uidb {
 
1146	return uidargs, uids
 
1149func (c *conn) ok(tag, cmd string) {
 
1150	c.bwriteresultf("%s OK %s done", tag, cmd)
 
1154// xcheckmailboxname checks if name is valid, returning an INBOX-normalized name.
 
1155// I.e. it changes various casings of INBOX and INBOX/* to Inbox and Inbox/*.
 
1156// Name is invalid if it contains leading/trailing/double slashes, or when it isn't
 
1157// unicode-normalized, or when empty or has special characters.
 
1158func xcheckmailboxname(name string, allowInbox bool) string {
 
1159	name, isinbox, err := store.CheckMailboxName(name, allowInbox)
 
1161		xuserErrorf("special mailboxname Inbox not allowed")
 
1162	} else if err != nil {
 
1163		xusercodeErrorf("CANNOT", "%s", err)
 
1168// Lookup mailbox by name.
 
1169// If the mailbox does not exist, panic is called with a user error.
 
1170// Must be called with account rlock held.
 
1171func (c *conn) xmailbox(tx *bstore.Tx, name string, missingErrCode string) store.Mailbox {
 
1172	mb, err := c.account.MailboxFind(tx, name)
 
1173	xcheckf(err, "finding mailbox")
 
1175		// missingErrCode can be empty, or e.g. TRYCREATE or ALREADYEXISTS.
 
1176		xusercodeErrorf(missingErrCode, "%w", store.ErrUnknownMailbox)
 
1181// Lookup mailbox by ID.
 
1182// If the mailbox does not exist, panic is called with a user error.
 
1183// Must be called with account rlock held.
 
1184func (c *conn) xmailboxID(tx *bstore.Tx, id int64) store.Mailbox {
 
1185	mb := store.Mailbox{ID: id}
 
1187	if err == bstore.ErrAbsent {
 
1188		xuserErrorf("%w", store.ErrUnknownMailbox)
 
1193// Apply changes to our session state.
 
1194// If initial is false, updates like EXISTS and EXPUNGE are written to the client.
 
1195// If initial is true, we only apply the changes.
 
1196// Should not be called while holding locks, as changes are written to client connections, which can block.
 
1197// Does not flush output.
 
1198func (c *conn) applyChanges(changes []store.Change, initial bool) {
 
1199	if len(changes) == 0 {
 
1203	err := c.conn.SetWriteDeadline(time.Now().Add(5 * time.Minute))
 
1204	c.log.Check(err, "setting write deadline")
 
1206	c.log.Debug("applying changes", slog.Any("changes", changes))
 
1208	// Only keep changes for the selected mailbox, and changes that are always relevant.
 
1209	var n []store.Change
 
1210	for _, change := range changes {
 
1212		switch ch := change.(type) {
 
1213		case store.ChangeAddUID:
 
1215		case store.ChangeRemoveUIDs:
 
1217		case store.ChangeFlags:
 
1219		case store.ChangeRemoveMailbox, store.ChangeAddMailbox, store.ChangeRenameMailbox, store.ChangeAddSubscription:
 
1220			n = append(n, change)
 
1222		case store.ChangeMailboxCounts, store.ChangeMailboxSpecialUse, store.ChangeMailboxKeywords, store.ChangeThread:
 
1224			panic(fmt.Errorf("missing case for %#v", change))
 
1226		if c.state == stateSelected && mbID == c.mailboxID {
 
1227			n = append(n, change)
 
1232	qresync := c.enabled[capQresync]
 
1233	condstore := c.enabled[capCondstore]
 
1236	for i < len(changes) {
 
1237		// First process all new uids. So we only send a single EXISTS.
 
1238		var adds []store.ChangeAddUID
 
1239		for ; i < len(changes); i++ {
 
1240			ch, ok := changes[i].(store.ChangeAddUID)
 
1244			seq := c.sequence(ch.UID)
 
1245			if seq > 0 && initial {
 
1249			adds = append(adds, ch)
 
1255			// Write the exists, and the UID and flags as well. Hopefully the client waits for
 
1256			// long enough after the EXISTS to see these messages, and doesn't request them
 
1257			// again with a FETCH.
 
1258			c.bwritelinef("* %d EXISTS", len(c.uids))
 
1259			for _, add := range adds {
 
1260				seq := c.xsequence(add.UID)
 
1261				var modseqStr string
 
1263					modseqStr = fmt.Sprintf(" MODSEQ (%d)", add.ModSeq.Client())
 
1265				c.bwritelinef("* %d FETCH (UID %d FLAGS %s%s)", seq, add.UID, flaglist(add.Flags, add.Keywords).pack(c), modseqStr)
 
1270		change := changes[i]
 
1273		switch ch := change.(type) {
 
1274		case store.ChangeRemoveUIDs:
 
1275			var vanishedUIDs numSet
 
1276			for _, uid := range ch.UIDs {
 
1279					seq = c.sequence(uid)
 
1284					seq = c.xsequence(uid)
 
1286				c.sequenceRemove(seq, uid)
 
1289						vanishedUIDs.append(uint32(uid))
 
1291						c.bwritelinef("* %d EXPUNGE", seq)
 
1297				for _, s := range vanishedUIDs.Strings(4*1024 - 32) {
 
1298					c.bwritelinef("* VANISHED %s", s)
 
1301		case store.ChangeFlags:
 
1302			// The uid can be unknown if we just expunged it while another session marked it as deleted just before.
 
1303			seq := c.sequence(ch.UID)
 
1308				var modseqStr string
 
1310					modseqStr = fmt.Sprintf(" MODSEQ (%d)", ch.ModSeq.Client())
 
1312				c.bwritelinef("* %d FETCH (UID %d FLAGS %s%s)", seq, ch.UID, flaglist(ch.Flags, ch.Keywords).pack(c), modseqStr)
 
1314		case store.ChangeRemoveMailbox:
 
1315			// Only announce \NonExistent to modern clients, otherwise they may ignore the
 
1316			// unrecognized \NonExistent and interpret this as a newly created mailbox, while
 
1317			// the goal was to remove it...
 
1318			if c.enabled[capIMAP4rev2] {
 
1319				c.bwritelinef(`* LIST (\NonExistent) "/" %s`, astring(c.encodeMailbox(ch.Name)).pack(c))
 
1321		case store.ChangeAddMailbox:
 
1322			c.bwritelinef(`* LIST (%s) "/" %s`, strings.Join(ch.Flags, " "), astring(c.encodeMailbox(ch.Mailbox.Name)).pack(c))
 
1323		case store.ChangeRenameMailbox:
 
1326			if c.enabled[capIMAP4rev2] {
 
1327				oldname = fmt.Sprintf(` ("OLDNAME" (%s))`, string0(c.encodeMailbox(ch.OldName)).pack(c))
 
1329			c.bwritelinef(`* LIST (%s) "/" %s%s`, strings.Join(ch.Flags, " "), astring(c.encodeMailbox(ch.NewName)).pack(c), oldname)
 
1330		case store.ChangeAddSubscription:
 
1331			c.bwritelinef(`* LIST (%s) "/" %s`, strings.Join(append([]string{`\Subscribed`}, ch.Flags...), " "), astring(c.encodeMailbox(ch.Name)).pack(c))
 
1333			panic(fmt.Sprintf("internal error, missing case for %#v", change))
 
1338// Capability returns the capabilities this server implements and currently has
 
1339// available given the connection state.
 
1342func (c *conn) cmdCapability(tag, cmd string, p *parser) {
 
1348	caps := c.capabilities()
 
1351	c.bwritelinef("* CAPABILITY %s", caps)
 
1355// capabilities returns non-empty string with available capabilities based on connection state.
 
1356// For use in cmdCapability and untagged OK responses on connection start, login and authenticate.
 
1357func (c *conn) capabilities() string {
 
1358	caps := serverCapabilities
 
1360	// We only allow starting without TLS when explicitly configured, in violation of RFC.
 
1361	if !c.tls && c.tlsConfig != nil {
 
1364	if c.tls || c.noRequireSTARTTLS {
 
1365		caps += " AUTH=PLAIN"
 
1367		caps += " LOGINDISABLED"
 
1372// No op, but useful for retrieving pending changes as untagged responses, e.g. of
 
1376func (c *conn) cmdNoop(tag, cmd string, p *parser) {
 
1384// Logout, after which server closes the connection.
 
1387func (c *conn) cmdLogout(tag, cmd string, p *parser) {
 
1394	c.state = stateNotAuthenticated
 
1396	c.bwritelinef("* BYE thanks")
 
1401// Clients can use ID to tell the server which software they are using. Servers can
 
1402// respond with their version. For statistics/logging/debugging purposes.
 
1405func (c *conn) cmdID(tag, cmd string, p *parser) {
 
1410	var params map[string]string
 
1412		params = map[string]string{}
 
1414			if len(params) > 0 {
 
1420			if _, ok := params[k]; ok {
 
1421				xsyntaxErrorf("duplicate key %q", k)
 
1430	// We just log the client id.
 
1431	c.log.Info("client id", slog.Any("params", params))
 
1435	c.bwritelinef(`* ID ("name" "mox" "version" %s)`, string0(moxvar.Version).pack(c))
 
1439// STARTTLS enables TLS on the connection, after a plain text start.
 
1440// Only allowed if TLS isn't already enabled, either through connecting to a
 
1441// TLS-enabled TCP port, or a previous STARTTLS command.
 
1442// After STARTTLS, plain text authentication typically becomes available.
 
1444// Status: Not authenticated.
 
1445func (c *conn) cmdStarttls(tag, cmd string, p *parser) {
 
1456	if n := c.br.Buffered(); n > 0 {
 
1457		buf := make([]byte, n)
 
1458		_, err := io.ReadFull(c.br, buf)
 
1459		xcheckf(err, "reading buffered data for tls handshake")
 
1460		conn = &prefixConn{buf, conn}
 
1462	// We add the cid to facilitate debugging in case of TLS connection failure.
 
1463	c.ok(tag, cmd+" ("+mox.ReceivedID(c.cid)+")")
 
1465	cidctx := context.WithValue(mox.Context, mlog.CidKey, c.cid)
 
1466	ctx, cancel := context.WithTimeout(cidctx, time.Minute)
 
1468	tlsConn := tls.Server(conn, c.tlsConfig)
 
1469	c.log.Debug("starting tls server handshake")
 
1470	if err := tlsConn.HandshakeContext(ctx); err != nil {
 
1471		panic(fmt.Errorf("starttls handshake: %s (%w)", err, errIO))
 
1474	tlsversion, ciphersuite := moxio.TLSInfo(tlsConn)
 
1475	c.log.Debug("tls server handshake done", slog.String("tls", tlsversion), slog.String("ciphersuite", ciphersuite))
 
1478	c.tr = moxio.NewTraceReader(c.log, "C: ", c.conn)
 
1479	c.tw = moxio.NewTraceWriter(c.log, "S: ", c)
 
1480	c.br = bufio.NewReader(c.tr)
 
1481	c.bw = bufio.NewWriter(c.tw)
 
1485// Authenticate using SASL. Supports multiple back and forths between client and
 
1486// server to finish authentication, unlike LOGIN which is just a single
 
1487// username/password.
 
1489// Status: Not authenticated.
 
1490func (c *conn) cmdAuthenticate(tag, cmd string, p *parser) {
 
1494	// For many failed auth attempts, slow down verification attempts.
 
1495	if c.authFailed > 3 && authFailDelay > 0 {
 
1496		mox.Sleep(mox.Context, time.Duration(c.authFailed-3)*authFailDelay)
 
1498	c.authFailed++ // Compensated on success.
 
1500		// On the 3rd failed authentication, start responding slowly. Successful auth will
 
1501		// cause fast responses again.
 
1502		if c.authFailed >= 3 {
 
1507	var authVariant string
 
1508	authResult := "error"
 
1510		metrics.AuthenticationInc("imap", authVariant, authResult)
 
1513			mox.LimiterFailedAuth.Reset(c.remoteIP, time.Now())
 
1515			mox.LimiterFailedAuth.Add(c.remoteIP, time.Now(), 1)
 
1521	authType := p.xatom()
 
1523	xreadInitial := func() []byte {
 
1527			line = c.readline(false)
 
1531			line = p.remainder()
 
1534				line = "" // Base64 decode will result in empty buffer.
 
1539			authResult = "aborted"
 
1540			xsyntaxErrorf("authenticate aborted by client")
 
1542		buf, err := base64.StdEncoding.DecodeString(line)
 
1544			xsyntaxErrorf("parsing base64: %v", err)
 
1549	xreadContinuation := func() []byte {
 
1550		line := c.readline(false)
 
1552			authResult = "aborted"
 
1553			xsyntaxErrorf("authenticate aborted by client")
 
1555		buf, err := base64.StdEncoding.DecodeString(line)
 
1557			xsyntaxErrorf("parsing base64: %v", err)
 
1562	switch strings.ToUpper(authType) {
 
1564		authVariant = "plain"
 
1566		if !c.noRequireSTARTTLS && !c.tls {
 
1568			xusercodeErrorf("PRIVACYREQUIRED", "tls required for login")
 
1571		// Plain text passwords, mark as traceauth.
 
1572		defer c.xtrace(mlog.LevelTraceauth)()
 
1573		buf := xreadInitial()
 
1574		c.xtrace(mlog.LevelTrace) // Restore.
 
1575		plain := bytes.Split(buf, []byte{0})
 
1576		if len(plain) != 3 {
 
1577			xsyntaxErrorf("bad plain auth data, expected 3 nul-separated tokens, got %d tokens", len(plain))
 
1579		authz := string(plain[0])
 
1580		authc := string(plain[1])
 
1581		password := string(plain[2])
 
1583		if authz != "" && authz != authc {
 
1584			xusercodeErrorf("AUTHORIZATIONFAILED", "cannot assume role")
 
1587		acc, err := store.OpenEmailAuth(c.log, authc, password)
 
1589			if errors.Is(err, store.ErrUnknownCredentials) {
 
1590				authResult = "badcreds"
 
1591				c.log.Info("authentication failed", slog.String("username", authc))
 
1592				xusercodeErrorf("AUTHENTICATIONFAILED", "bad credentials")
 
1594			xusercodeErrorf("", "error")
 
1600		authVariant = strings.ToLower(authType)
 
1606		chal := fmt.Sprintf("<%d.%d@%s>", uint64(mox.CryptoRandInt()), time.Now().UnixNano(), mox.Conf.Static.HostnameDomain.ASCII)
 
1607		c.writelinef("+ %s", base64.StdEncoding.EncodeToString([]byte(chal)))
 
1609		resp := xreadContinuation()
 
1610		t := strings.Split(string(resp), " ")
 
1611		if len(t) != 2 || len(t[1]) != 2*md5.Size {
 
1612			xsyntaxErrorf("malformed cram-md5 response")
 
1615		c.log.Debug("cram-md5 auth", slog.String("address", addr))
 
1616		acc, _, err := store.OpenEmail(c.log, addr)
 
1618			if errors.Is(err, store.ErrUnknownCredentials) {
 
1619				c.log.Info("failed authentication attempt", slog.String("username", addr), slog.Any("remote", c.remoteIP))
 
1620				xusercodeErrorf("AUTHENTICATIONFAILED", "bad credentials")
 
1622			xserverErrorf("looking up address: %v", err)
 
1627				c.xsanity(err, "close account")
 
1630		var ipadhash, opadhash hash.Hash
 
1631		acc.WithRLock(func() {
 
1632			err := acc.DB.Read(context.TODO(), func(tx *bstore.Tx) error {
 
1633				password, err := bstore.QueryTx[store.Password](tx).Get()
 
1634				if err == bstore.ErrAbsent {
 
1635					c.log.Info("failed authentication attempt", slog.String("username", addr), slog.Any("remote", c.remoteIP))
 
1636					xusercodeErrorf("AUTHENTICATIONFAILED", "bad credentials")
 
1642				ipadhash = password.CRAMMD5.Ipad
 
1643				opadhash = password.CRAMMD5.Opad
 
1646			xcheckf(err, "tx read")
 
1648		if ipadhash == nil || opadhash == nil {
 
1649			c.log.Info("cram-md5 auth attempt without derived secrets set, save password again to store secrets", slog.String("username", addr))
 
1650			c.log.Info("failed authentication attempt", slog.String("username", addr), slog.Any("remote", c.remoteIP))
 
1651			xusercodeErrorf("AUTHENTICATIONFAILED", "bad credentials")
 
1655		ipadhash.Write([]byte(chal))
 
1656		opadhash.Write(ipadhash.Sum(nil))
 
1657		digest := fmt.Sprintf("%x", opadhash.Sum(nil))
 
1659			c.log.Info("failed authentication attempt", slog.String("username", addr), slog.Any("remote", c.remoteIP))
 
1660			xusercodeErrorf("AUTHENTICATIONFAILED", "bad credentials")
 
1664		acc = nil // Cancel cleanup.
 
1667	case "SCRAM-SHA-256-PLUS", "SCRAM-SHA-256", "SCRAM-SHA-1-PLUS", "SCRAM-SHA-1":
 
1668		// 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?
 
1669		// todo: use single implementation between ../imapserver/server.go and ../smtpserver/server.go
 
1671		// No plaintext credentials, we can log these normally.
 
1673		authVariant = strings.ToLower(authType)
 
1674		var h func() hash.Hash
 
1675		switch authVariant {
 
1676		case "scram-sha-1", "scram-sha-1-plus":
 
1678		case "scram-sha-256", "scram-sha-256-plus":
 
1681			xserverErrorf("missing case for scram variant")
 
1684		var cs *tls.ConnectionState
 
1685		requireChannelBinding := strings.HasSuffix(authVariant, "-plus")
 
1686		if requireChannelBinding && !c.tls {
 
1687			xuserErrorf("cannot use plus variant with tls channel binding without tls")
 
1690			xcs := c.conn.(*tls.Conn).ConnectionState()
 
1693		c0 := xreadInitial()
 
1694		ss, err := scram.NewServer(h, c0, cs, requireChannelBinding)
 
1696			xsyntaxErrorf("starting scram: %s", err)
 
1698		c.log.Debug("scram auth", slog.String("authentication", ss.Authentication))
 
1699		acc, _, err := store.OpenEmail(c.log, ss.Authentication)
 
1701			// todo: we could continue scram with a generated salt, deterministically generated
 
1702			// from the username. that way we don't have to store anything but attackers cannot
 
1703			// learn if an account exists. same for absent scram saltedpassword below.
 
1704			xuserErrorf("scram not possible")
 
1709				c.xsanity(err, "close account")
 
1712		if ss.Authorization != "" && ss.Authorization != ss.Authentication {
 
1713			xuserErrorf("authentication with authorization for different user not supported")
 
1715		var xscram store.SCRAM
 
1716		acc.WithRLock(func() {
 
1717			err := acc.DB.Read(context.TODO(), func(tx *bstore.Tx) error {
 
1718				password, err := bstore.QueryTx[store.Password](tx).Get()
 
1719				switch authVariant {
 
1720				case "scram-sha-1", "scram-sha-1-plus":
 
1721					xscram = password.SCRAMSHA1
 
1722				case "scram-sha-256", "scram-sha-256-plus":
 
1723					xscram = password.SCRAMSHA256
 
1725					xserverErrorf("missing case for scram credentials")
 
1727				if err == bstore.ErrAbsent || err == nil && (len(xscram.Salt) == 0 || xscram.Iterations == 0 || len(xscram.SaltedPassword) == 0) {
 
1728					c.log.Info("scram auth attempt without derived secrets set, save password again to store secrets", slog.String("address", ss.Authentication))
 
1729					xuserErrorf("scram not possible")
 
1731				xcheckf(err, "fetching credentials")
 
1734			xcheckf(err, "read tx")
 
1736		s1, err := ss.ServerFirst(xscram.Iterations, xscram.Salt)
 
1737		xcheckf(err, "scram first server step")
 
1738		c.writelinef("+ %s", base64.StdEncoding.EncodeToString([]byte(s1)))
 
1739		c2 := xreadContinuation()
 
1740		s3, err := ss.Finish(c2, xscram.SaltedPassword)
 
1742			c.writelinef("+ %s", base64.StdEncoding.EncodeToString([]byte(s3)))
 
1745			c.readline(false) // Should be "*" for cancellation.
 
1746			if errors.Is(err, scram.ErrInvalidProof) {
 
1747				authResult = "badcreds"
 
1748				c.log.Info("failed authentication attempt", slog.String("username", ss.Authentication), slog.Any("remote", c.remoteIP))
 
1749				xusercodeErrorf("AUTHENTICATIONFAILED", "bad credentials")
 
1751			xuserErrorf("server final: %w", err)
 
1755		// The message should be empty. todo: should we require it is empty?
 
1759		acc = nil // Cancel cleanup.
 
1760		c.username = ss.Authentication
 
1763		xuserErrorf("method not supported")
 
1769	c.comm = store.RegisterComm(c.account)
 
1770	c.state = stateAuthenticated
 
1771	c.writeresultf("%s OK [CAPABILITY %s] authenticate done", tag, c.capabilities())
 
1774// Login logs in with username and password.
 
1776// Status: Not authenticated.
 
1777func (c *conn) cmdLogin(tag, cmd string, p *parser) {
 
1780	authResult := "error"
 
1782		metrics.AuthenticationInc("imap", "login", authResult)
 
1785	// 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).
 
1789	userid := p.xastring()
 
1791	password := p.xastring()
 
1794	if !c.noRequireSTARTTLS && !c.tls {
 
1796		xusercodeErrorf("PRIVACYREQUIRED", "tls required for login")
 
1799	// For many failed auth attempts, slow down verification attempts.
 
1800	if c.authFailed > 3 && authFailDelay > 0 {
 
1801		mox.Sleep(mox.Context, time.Duration(c.authFailed-3)*authFailDelay)
 
1803	c.authFailed++ // Compensated on success.
 
1805		// On the 3rd failed authentication, start responding slowly. Successful auth will
 
1806		// cause fast responses again.
 
1807		if c.authFailed >= 3 {
 
1812	acc, err := store.OpenEmailAuth(c.log, userid, password)
 
1814		authResult = "badcreds"
 
1816		if errors.Is(err, store.ErrUnknownCredentials) {
 
1817			code = "AUTHENTICATIONFAILED"
 
1818			c.log.Info("failed authentication attempt", slog.String("username", userid), slog.Any("remote", c.remoteIP))
 
1820		xusercodeErrorf(code, "login failed")
 
1826	c.comm = store.RegisterComm(acc)
 
1827	c.state = stateAuthenticated
 
1829	c.writeresultf("%s OK [CAPABILITY %s] login done", tag, c.capabilities())
 
1832// Enable explicitly opts in to an extension. A server can typically send new kinds
 
1833// of responses to a client. Most extensions do not require an ENABLE because a
 
1834// client implicitly opts in to new response syntax by making a requests that uses
 
1835// new optional extension request syntax.
 
1837// State: Authenticated and selected.
 
1838func (c *conn) cmdEnable(tag, cmd string, p *parser) {
 
1844	caps := []string{p.xatom()}
 
1847		caps = append(caps, p.xatom())
 
1850	// Clients should only send capabilities that need enabling.
 
1851	// We should only echo that we recognize as needing enabling.
 
1854	for _, s := range caps {
 
1855		cap := capability(strings.ToUpper(s))
 
1860			c.enabled[cap] = true
 
1863			c.enabled[cap] = true
 
1869	if qresync && !c.enabled[capCondstore] {
 
1870		c.xensureCondstore(nil)
 
1871		enabled += " CONDSTORE"
 
1875	c.bwritelinef("* ENABLED%s", enabled)
 
1880// If a mailbox is selected, an untagged OK with HIGHESTMODSEQ is written to the
 
1881// client. If tx is non-nil, it is used to read the HIGHESTMODSEQ from the
 
1882// database. Otherwise a new read-only transaction is created.
 
1883func (c *conn) xensureCondstore(tx *bstore.Tx) {
 
1884	if !c.enabled[capCondstore] {
 
1885		c.enabled[capCondstore] = true
 
1886		// todo spec: can we send an untagged enabled response?
 
1888		if c.mailboxID <= 0 {
 
1891		var modseq store.ModSeq
 
1893			modseq = c.xhighestModSeq(tx, c.mailboxID)
 
1895			c.xdbread(func(tx *bstore.Tx) {
 
1896				modseq = c.xhighestModSeq(tx, c.mailboxID)
 
1899		c.bwritelinef("* OK [HIGHESTMODSEQ %d] after condstore-enabling command", modseq.Client())
 
1903// State: Authenticated and selected.
 
1904func (c *conn) cmdSelect(tag, cmd string, p *parser) {
 
1905	c.cmdSelectExamine(true, tag, cmd, p)
 
1908// State: Authenticated and selected.
 
1909func (c *conn) cmdExamine(tag, cmd string, p *parser) {
 
1910	c.cmdSelectExamine(false, tag, cmd, p)
 
1913// Select and examine are almost the same commands. Select just opens a mailbox for
 
1914// read/write and examine opens a mailbox readonly.
 
1916// State: Authenticated and selected.
 
1917func (c *conn) cmdSelectExamine(isselect bool, tag, cmd string, p *parser) {
 
1925	name := p.xmailbox()
 
1927	var qruidvalidity uint32
 
1928	var qrmodseq int64                                    // QRESYNC required parameters.
 
1929	var qrknownUIDs, qrknownSeqSet, qrknownUIDSet *numSet // QRESYNC optional parameters.
 
1931		seen := map[string]bool{}
 
1933		for len(seen) == 0 || !p.take(")") {
 
1934			w := p.xtakelist("CONDSTORE", "QRESYNC")
 
1936				xsyntaxErrorf("duplicate select parameter %s", w)
 
1946				// Note: unlike with CONDSTORE, there are no QRESYNC-related commands/parameters
 
1947				// that enable capabilities.
 
1948				if !c.enabled[capQresync] {
 
1950					xsyntaxErrorf("QRESYNC must first be enabled")
 
1956				qrmodseq = p.xnznumber64()
 
1958					seqMatchData := p.take("(")
 
1962						seqMatchData = p.take(" (")
 
1965						ss0 := p.xnumSet0(false, false)
 
1966						qrknownSeqSet = &ss0
 
1968						ss1 := p.xnumSet0(false, false)
 
1969						qrknownUIDSet = &ss1
 
1975				panic("missing case for select param " + w)
 
1981	// Deselect before attempting the new select. This means we will deselect when an
 
1982	// error occurs during select.
 
1984	if c.state == stateSelected {
 
1986		c.bwritelinef("* OK [CLOSED] x")
 
1990	name = xcheckmailboxname(name, true)
 
1992	var highestModSeq store.ModSeq
 
1993	var highDeletedModSeq store.ModSeq
 
1994	var firstUnseen msgseq = 0
 
1995	var mb store.Mailbox
 
1996	c.account.WithRLock(func() {
 
1997		c.xdbread(func(tx *bstore.Tx) {
 
1998			mb = c.xmailbox(tx, name, "")
 
2000			q := bstore.QueryTx[store.Message](tx)
 
2001			q.FilterNonzero(store.Message{MailboxID: mb.ID})
 
2002			q.FilterEqual("Expunged", false)
 
2004			c.uids = []store.UID{}
 
2006			err := q.ForEach(func(m store.Message) error {
 
2007				c.uids = append(c.uids, m.UID)
 
2008				if firstUnseen == 0 && !m.Seen {
 
2017			xcheckf(err, "fetching uids")
 
2019			// Condstore extension, find the highest modseq.
 
2020			if c.enabled[capCondstore] {
 
2021				highestModSeq = c.xhighestModSeq(tx, mb.ID)
 
2023			// For QRESYNC, we need to know the highest modset of deleted expunged records to
 
2024			// maintain synchronization.
 
2025			if c.enabled[capQresync] {
 
2026				highDeletedModSeq, err = c.account.HighestDeletedModSeq(tx)
 
2027				xcheckf(err, "getting highest deleted modseq")
 
2031	c.applyChanges(c.comm.Get(), true)
 
2034	if len(mb.Keywords) > 0 {
 
2035		flags = " " + strings.Join(mb.Keywords, " ")
 
2037	c.bwritelinef(`* FLAGS (\Seen \Answered \Flagged \Deleted \Draft $Forwarded $Junk $NotJunk $Phishing $MDNSent%s)`, flags)
 
2038	c.bwritelinef(`* OK [PERMANENTFLAGS (\Seen \Answered \Flagged \Deleted \Draft $Forwarded $Junk $NotJunk $Phishing $MDNSent \*)] x`)
 
2039	if !c.enabled[capIMAP4rev2] {
 
2040		c.bwritelinef(`* 0 RECENT`)
 
2042	c.bwritelinef(`* %d EXISTS`, len(c.uids))
 
2043	if !c.enabled[capIMAP4rev2] && firstUnseen > 0 {
 
2045		c.bwritelinef(`* OK [UNSEEN %d] x`, firstUnseen)
 
2047	c.bwritelinef(`* OK [UIDVALIDITY %d] x`, mb.UIDValidity)
 
2048	c.bwritelinef(`* OK [UIDNEXT %d] x`, mb.UIDNext)
 
2049	c.bwritelinef(`* LIST () "/" %s`, astring(c.encodeMailbox(mb.Name)).pack(c))
 
2050	if c.enabled[capCondstore] {
 
2053		c.bwritelinef(`* OK [HIGHESTMODSEQ %d] x`, highestModSeq.Client())
 
2057	if qruidvalidity == mb.UIDValidity {
 
2058		// We send the vanished UIDs at the end, so we can easily combine the modseq
 
2059		// changes and vanished UIDs that result from that, with the vanished UIDs from the
 
2060		// case where we don't store enough history.
 
2061		vanishedUIDs := map[store.UID]struct{}{}
 
2063		var preVanished store.UID
 
2064		var oldClientUID store.UID
 
2065		// If samples of known msgseq and uid pairs are given (they must be in order), we
 
2066		// use them to determine the earliest UID for which we send VANISHED responses.
 
2068		if qrknownSeqSet != nil {
 
2069			if !qrknownSeqSet.isBasicIncreasing() {
 
2070				xuserErrorf("QRESYNC known message sequence set must be numeric and strictly increasing")
 
2072			if !qrknownUIDSet.isBasicIncreasing() {
 
2073				xuserErrorf("QRESYNC known uid set must be numeric and strictly increasing")
 
2075			seqiter := qrknownSeqSet.newIter()
 
2076			uiditer := qrknownUIDSet.newIter()
 
2078				msgseq, ok0 := seqiter.Next()
 
2079				uid, ok1 := uiditer.Next()
 
2082				} else if !ok0 || !ok1 {
 
2083					xsyntaxErrorf("invalid combination of known sequence set and uid set, must be of equal length")
 
2085				i := int(msgseq - 1)
 
2086				if i < 0 || i >= len(c.uids) || c.uids[i] != store.UID(uid) {
 
2087					if uidSearch(c.uids, store.UID(uid)) <= 0 {
 
2088						// We will check this old client UID for consistency below.
 
2089						oldClientUID = store.UID(uid)
 
2093				preVanished = store.UID(uid + 1)
 
2097		// We gather vanished UIDs and report them at the end. This seems OK because we
 
2098		// already sent HIGHESTMODSEQ, and a client should know not to commit that value
 
2099		// until after it has seen the tagged OK of this command. The RFC has a remark
 
2100		// about ordering of some untagged responses, it's not immediately clear what it
 
2101		// means, but given the examples appears to allude to servers that decide to not
 
2102		// send expunge/vanished before the tagged OK.
 
2105		// We are reading without account lock. Similar to when we process FETCH/SEARCH
 
2106		// requests. We don't have to reverify existence of the mailbox, so we don't
 
2107		// rlock, even briefly.
 
2108		c.xdbread(func(tx *bstore.Tx) {
 
2109			if oldClientUID > 0 {
 
2110				// The client sent a UID that is now removed. This is typically fine. But we check
 
2111				// that it is consistent with the modseq the client sent. If the UID already didn't
 
2112				// exist at that modseq, the client may be missing some information.
 
2113				q := bstore.QueryTx[store.Message](tx)
 
2114				q.FilterNonzero(store.Message{MailboxID: mb.ID, UID: oldClientUID})
 
2117					// If client claims to be up to date up to and including qrmodseq, and the message
 
2118					// was deleted at or before that time, we send changes from just before that
 
2119					// modseq, and we send vanished for all UIDs.
 
2120					if m.Expunged && qrmodseq >= m.ModSeq.Client() {
 
2121						qrmodseq = m.ModSeq.Client() - 1
 
2124						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.")
 
2126				} else if err != bstore.ErrAbsent {
 
2127					xcheckf(err, "checking old client uid")
 
2131			q := bstore.QueryTx[store.Message](tx)
 
2132			q.FilterNonzero(store.Message{MailboxID: mb.ID})
 
2133			// Note: we don't filter by Expunged.
 
2134			q.FilterGreater("ModSeq", store.ModSeqFromClient(qrmodseq))
 
2135			q.FilterLessEqual("ModSeq", highestModSeq)
 
2137			err := q.ForEach(func(m store.Message) error {
 
2138				if m.Expunged && m.UID < preVanished {
 
2142				if qrknownUIDs != nil && !qrknownUIDs.contains(uint32(m.UID)) {
 
2146					vanishedUIDs[m.UID] = struct{}{}
 
2149				msgseq := c.sequence(m.UID)
 
2151					c.bwritelinef("* %d FETCH (UID %d FLAGS %s MODSEQ (%d))", msgseq, m.UID, flaglist(m.Flags, m.Keywords).pack(c), m.ModSeq.Client())
 
2155			xcheckf(err, "listing changed messages")
 
2158		// Add UIDs from client's known UID set to vanished list if we don't have enough history.
 
2159		if qrmodseq < highDeletedModSeq.Client() {
 
2160			// If no known uid set was in the request, we substitute 1:max or the empty set.
 
2162			if qrknownUIDs == nil {
 
2163				if len(c.uids) > 0 {
 
2164					qrknownUIDs = &numSet{ranges: []numRange{{first: setNumber{number: 1}, last: &setNumber{number: uint32(c.uids[len(c.uids)-1])}}}}
 
2166					qrknownUIDs = &numSet{}
 
2170			iter := qrknownUIDs.newIter()
 
2172				v, ok := iter.Next()
 
2176				if c.sequence(store.UID(v)) <= 0 {
 
2177					vanishedUIDs[store.UID(v)] = struct{}{}
 
2182		// Now that we have all vanished UIDs, send them over compactly.
 
2183		if len(vanishedUIDs) > 0 {
 
2184			l := maps.Keys(vanishedUIDs)
 
2185			sort.Slice(l, func(i, j int) bool {
 
2189			for _, s := range compactUIDSet(l).Strings(4*1024 - 32) {
 
2190				c.bwritelinef("* VANISHED (EARLIER) %s", s)
 
2196		c.bwriteresultf("%s OK [READ-WRITE] x", tag)
 
2199		c.bwriteresultf("%s OK [READ-ONLY] x", tag)
 
2203	c.state = stateSelected
 
2204	c.searchResult = nil
 
2208// Create makes a new mailbox, and its parents too if absent.
 
2210// State: Authenticated and selected.
 
2211func (c *conn) cmdCreate(tag, cmd string, p *parser) {
 
2217	name := p.xmailbox()
 
2223	name = xcheckmailboxname(name, false)
 
2225	var changes []store.Change
 
2226	var created []string // Created mailbox names.
 
2228	c.account.WithWLock(func() {
 
2229		c.xdbwrite(func(tx *bstore.Tx) {
 
2232			changes, created, exists, err = c.account.MailboxCreate(tx, name)
 
2235				xuserErrorf("mailbox already exists")
 
2237			xcheckf(err, "creating mailbox")
 
2240		c.broadcast(changes)
 
2243	for _, n := range created {
 
2246		if c.enabled[capIMAP4rev2] && n == name && name != origName && !(name == "Inbox" || strings.HasPrefix(name, "Inbox/")) {
 
2247			oldname = fmt.Sprintf(` ("OLDNAME" (%s))`, string0(c.encodeMailbox(origName)).pack(c))
 
2249		c.bwritelinef(`* LIST (\Subscribed) "/" %s%s`, astring(c.encodeMailbox(n)).pack(c), oldname)
 
2254// Delete removes a mailbox and all its messages.
 
2255// Inbox cannot be removed.
 
2257// State: Authenticated and selected.
 
2258func (c *conn) cmdDelete(tag, cmd string, p *parser) {
 
2264	name := p.xmailbox()
 
2267	name = xcheckmailboxname(name, false)
 
2269	// Messages to remove after having broadcasted the removal of messages.
 
2270	var removeMessageIDs []int64
 
2272	c.account.WithWLock(func() {
 
2273		var mb store.Mailbox
 
2274		var changes []store.Change
 
2276		c.xdbwrite(func(tx *bstore.Tx) {
 
2277			mb = c.xmailbox(tx, name, "NONEXISTENT")
 
2279			var hasChildren bool
 
2281			changes, removeMessageIDs, hasChildren, err = c.account.MailboxDelete(context.TODO(), c.log, tx, mb)
 
2283				xusercodeErrorf("HASCHILDREN", "mailbox has a child, only leaf mailboxes can be deleted")
 
2285			xcheckf(err, "deleting mailbox")
 
2288		c.broadcast(changes)
 
2291	for _, mID := range removeMessageIDs {
 
2292		p := c.account.MessagePath(mID)
 
2294		c.log.Check(err, "removing message file for mailbox delete", slog.String("path", p))
 
2300// Rename changes the name of a mailbox.
 
2301// Renaming INBOX is special, it moves the inbox messages to a new mailbox, leaving inbox empty.
 
2302// Renaming a mailbox with submailboxes also renames all submailboxes.
 
2303// Subscriptions stay with the old name, though newly created missing parent
 
2304// mailboxes for the destination name are automatically subscribed.
 
2306// State: Authenticated and selected.
 
2307func (c *conn) cmdRename(tag, cmd string, p *parser) {
 
2318	src = xcheckmailboxname(src, true)
 
2319	dst = xcheckmailboxname(dst, false)
 
2321	c.account.WithWLock(func() {
 
2322		var changes []store.Change
 
2324		c.xdbwrite(func(tx *bstore.Tx) {
 
2325			srcMB := c.xmailbox(tx, src, "NONEXISTENT")
 
2327			// Inbox is very special. Unlike other mailboxes, its children are not moved. And
 
2328			// unlike a regular move, its messages are moved to a newly created mailbox. We do
 
2329			// indeed create a new destination mailbox and actually move the messages.
 
2332				exists, err := c.account.MailboxExists(tx, dst)
 
2333				xcheckf(err, "checking if destination mailbox exists")
 
2335					xusercodeErrorf("ALREADYEXISTS", "destination mailbox %q already exists", dst)
 
2338					xuserErrorf("cannot move inbox to itself")
 
2341				uidval, err := c.account.NextUIDValidity(tx)
 
2342				xcheckf(err, "next uid validity")
 
2344				dstMB := store.Mailbox{
 
2346					UIDValidity: uidval,
 
2348					Keywords:    srcMB.Keywords,
 
2351				err = tx.Insert(&dstMB)
 
2352				xcheckf(err, "create new destination mailbox")
 
2354				modseq, err := c.account.NextModSeq(tx)
 
2355				xcheckf(err, "assigning next modseq")
 
2357				changes = make([]store.Change, 2) // Placeholders filled in below.
 
2359				// Move existing messages, with their ID's and on-disk files intact, to the new
 
2360				// mailbox. We keep the expunged messages, the destination mailbox doesn't care
 
2362				var oldUIDs []store.UID
 
2363				q := bstore.QueryTx[store.Message](tx)
 
2364				q.FilterNonzero(store.Message{MailboxID: srcMB.ID})
 
2365				q.FilterEqual("Expunged", false)
 
2367				err = q.ForEach(func(m store.Message) error {
 
2372					oldUIDs = append(oldUIDs, om.UID)
 
2374					mc := m.MailboxCounts()
 
2378					m.MailboxID = dstMB.ID
 
2379					m.UID = dstMB.UIDNext
 
2381					m.CreateSeq = modseq
 
2383					if err := tx.Update(&m); err != nil {
 
2384						return fmt.Errorf("updating message to move to new mailbox: %w", err)
 
2387					changes = append(changes, m.ChangeAddUID())
 
2389					if err := tx.Insert(&om); err != nil {
 
2390						return fmt.Errorf("adding empty expunge message record to inbox: %w", err)
 
2394				xcheckf(err, "moving messages from inbox to destination mailbox")
 
2396				err = tx.Update(&dstMB)
 
2397				xcheckf(err, "updating uidnext and counts in destination mailbox")
 
2399				err = tx.Update(&srcMB)
 
2400				xcheckf(err, "updating counts for inbox")
 
2402				var dstFlags []string
 
2403				if tx.Get(&store.Subscription{Name: dstMB.Name}) == nil {
 
2404					dstFlags = []string{`\Subscribed`}
 
2406				changes[0] = store.ChangeRemoveUIDs{MailboxID: srcMB.ID, UIDs: oldUIDs, ModSeq: modseq}
 
2407				changes[1] = store.ChangeAddMailbox{Mailbox: dstMB, Flags: dstFlags}
 
2408				// changes[2:...] are ChangeAddUIDs
 
2409				changes = append(changes, srcMB.ChangeCounts(), dstMB.ChangeCounts())
 
2413			var notExists, alreadyExists bool
 
2415			changes, _, notExists, alreadyExists, err = c.account.MailboxRename(tx, srcMB, dst)
 
2418				xusercodeErrorf("NONEXISTENT", "%s", err)
 
2419			} else if alreadyExists {
 
2420				xusercodeErrorf("ALREADYEXISTS", "%s", err)
 
2422			xcheckf(err, "renaming mailbox")
 
2424		c.broadcast(changes)
 
2430// Subscribe marks a mailbox path as subscribed. The mailbox does not have to
 
2431// exist. Subscribed may mean an email client will show the mailbox in its UI
 
2432// and/or periodically fetch new messages for the mailbox.
 
2434// State: Authenticated and selected.
 
2435func (c *conn) cmdSubscribe(tag, cmd string, p *parser) {
 
2441	name := p.xmailbox()
 
2444	name = xcheckmailboxname(name, true)
 
2446	c.account.WithWLock(func() {
 
2447		var changes []store.Change
 
2449		c.xdbwrite(func(tx *bstore.Tx) {
 
2451			changes, err = c.account.SubscriptionEnsure(tx, name)
 
2452			xcheckf(err, "ensuring subscription")
 
2455		c.broadcast(changes)
 
2461// Unsubscribe marks a mailbox as not subscribed. The mailbox doesn't have to exist.
 
2463// State: Authenticated and selected.
 
2464func (c *conn) cmdUnsubscribe(tag, cmd string, p *parser) {
 
2470	name := p.xmailbox()
 
2473	name = xcheckmailboxname(name, true)
 
2475	c.account.WithWLock(func() {
 
2476		c.xdbwrite(func(tx *bstore.Tx) {
 
2478			err := tx.Delete(&store.Subscription{Name: name})
 
2479			if err == bstore.ErrAbsent {
 
2480				exists, err := c.account.MailboxExists(tx, name)
 
2481				xcheckf(err, "checking if mailbox exists")
 
2483					xuserErrorf("mailbox does not exist")
 
2487			xcheckf(err, "removing subscription")
 
2490		// todo: can we send untagged message about a mailbox no longer being subscribed?
 
2496// LSUB command for listing subscribed mailboxes.
 
2497// Removed in IMAP4rev2, only in IMAP4rev1.
 
2499// State: Authenticated and selected.
 
2500func (c *conn) cmdLsub(tag, cmd string, p *parser) {
 
2508	pattern := p.xlistMailbox()
 
2511	re := xmailboxPatternMatcher(ref, []string{pattern})
 
2514	c.xdbread(func(tx *bstore.Tx) {
 
2515		q := bstore.QueryTx[store.Subscription](tx)
 
2517		subscriptions, err := q.List()
 
2518		xcheckf(err, "querying subscriptions")
 
2520		have := map[string]bool{}
 
2521		subscribedKids := map[string]bool{}
 
2522		ispercent := strings.HasSuffix(pattern, "%")
 
2523		for _, sub := range subscriptions {
 
2526				for p := path.Dir(name); p != "."; p = path.Dir(p) {
 
2527					subscribedKids[p] = true
 
2530			if !re.MatchString(name) {
 
2534			line := fmt.Sprintf(`* LSUB () "/" %s`, astring(c.encodeMailbox(name)).pack(c))
 
2535			lines = append(lines, line)
 
2543		qmb := bstore.QueryTx[store.Mailbox](tx)
 
2545		err = qmb.ForEach(func(mb store.Mailbox) error {
 
2546			if have[mb.Name] || !subscribedKids[mb.Name] || !re.MatchString(mb.Name) {
 
2549			line := fmt.Sprintf(`* LSUB (\NoSelect) "/" %s`, astring(c.encodeMailbox(mb.Name)).pack(c))
 
2550			lines = append(lines, line)
 
2553		xcheckf(err, "querying mailboxes")
 
2557	for _, line := range lines {
 
2558		c.bwritelinef("%s", line)
 
2563// The namespace command returns the mailbox path separator. We only implement
 
2564// the personal mailbox hierarchy, no shared/other.
 
2566// In IMAP4rev2, it was an extension before.
 
2568// State: Authenticated and selected.
 
2569func (c *conn) cmdNamespace(tag, cmd string, p *parser) {
 
2576	c.bwritelinef(`* NAMESPACE (("" "/")) NIL NIL`)
 
2580// The status command returns information about a mailbox, such as the number of
 
2581// messages, "uid validity", etc. Nowadays, the extended LIST command can return
 
2582// the same information about many mailboxes for one command.
 
2584// State: Authenticated and selected.
 
2585func (c *conn) cmdStatus(tag, cmd string, p *parser) {
 
2591	name := p.xmailbox()
 
2594	attrs := []string{p.xstatusAtt()}
 
2597		attrs = append(attrs, p.xstatusAtt())
 
2601	name = xcheckmailboxname(name, true)
 
2603	var mb store.Mailbox
 
2605	var responseLine string
 
2606	c.account.WithRLock(func() {
 
2607		c.xdbread(func(tx *bstore.Tx) {
 
2608			mb = c.xmailbox(tx, name, "")
 
2609			responseLine = c.xstatusLine(tx, mb, attrs)
 
2613	c.bwritelinef("%s", responseLine)
 
2618func (c *conn) xstatusLine(tx *bstore.Tx, mb store.Mailbox, attrs []string) string {
 
2619	status := []string{}
 
2620	for _, a := range attrs {
 
2621		A := strings.ToUpper(a)
 
2624			status = append(status, A, fmt.Sprintf("%d", mb.Total+mb.Deleted))
 
2626			status = append(status, A, fmt.Sprintf("%d", mb.UIDNext))
 
2628			status = append(status, A, fmt.Sprintf("%d", mb.UIDValidity))
 
2630			status = append(status, A, fmt.Sprintf("%d", mb.Unseen))
 
2632			status = append(status, A, fmt.Sprintf("%d", mb.Deleted))
 
2634			status = append(status, A, fmt.Sprintf("%d", mb.Size))
 
2636			status = append(status, A, "0")
 
2639			status = append(status, A, "NIL")
 
2640		case "HIGHESTMODSEQ":
 
2642			status = append(status, A, fmt.Sprintf("%d", c.xhighestModSeq(tx, mb.ID).Client()))
 
2644			xsyntaxErrorf("unknown attribute %q", a)
 
2647	return fmt.Sprintf("* STATUS %s (%s)", astring(c.encodeMailbox(mb.Name)).pack(c), strings.Join(status, " "))
 
2650func flaglist(fl store.Flags, keywords []string) listspace {
 
2652	flag := func(v bool, s string) {
 
2654			l = append(l, bare(s))
 
2657	flag(fl.Seen, `\Seen`)
 
2658	flag(fl.Answered, `\Answered`)
 
2659	flag(fl.Flagged, `\Flagged`)
 
2660	flag(fl.Deleted, `\Deleted`)
 
2661	flag(fl.Draft, `\Draft`)
 
2662	flag(fl.Forwarded, `$Forwarded`)
 
2663	flag(fl.Junk, `$Junk`)
 
2664	flag(fl.Notjunk, `$NotJunk`)
 
2665	flag(fl.Phishing, `$Phishing`)
 
2666	flag(fl.MDNSent, `$MDNSent`)
 
2667	for _, k := range keywords {
 
2668		l = append(l, bare(k))
 
2673// Append adds a message to a mailbox.
 
2675// State: Authenticated and selected.
 
2676func (c *conn) cmdAppend(tag, cmd string, p *parser) {
 
2682	name := p.xmailbox()
 
2684	var storeFlags store.Flags
 
2685	var keywords []string
 
2686	if p.hasPrefix("(") {
 
2687		// Error must be a syntax error, to properly abort the connection due to literal.
 
2689		storeFlags, keywords, err = store.ParseFlagsKeywords(p.xflagList())
 
2691			xsyntaxErrorf("parsing flags: %v", err)
 
2696	if p.hasPrefix(`"`) {
 
2702	// todo: only with utf8 should we we accept message headers with utf-8. we currently always accept them.
 
2703	// todo: this is only relevant if we also support the CATENATE extension?
 
2705	utf8 := p.take("UTF8 (")
 
2706	size, sync := p.xliteralSize(0, utf8)
 
2708	name = xcheckmailboxname(name, true)
 
2709	c.xdbread(func(tx *bstore.Tx) {
 
2710		c.xmailbox(tx, name, "TRYCREATE")
 
2716	// Read the message into a temporary file.
 
2717	msgFile, err := store.CreateMessageTemp(c.log, "imap-append")
 
2718	xcheckf(err, "creating temp file for message")
 
2721		err := msgFile.Close()
 
2722		c.xsanity(err, "closing APPEND temporary file")
 
2724		c.xsanity(err, "removing APPEND temporary file")
 
2726	defer c.xtrace(mlog.LevelTracedata)()
 
2727	mw := message.NewWriter(msgFile)
 
2728	msize, err := io.Copy(mw, io.LimitReader(c.br, size))
 
2729	c.xtrace(mlog.LevelTrace) // Restore.
 
2731		// Cannot use xcheckf due to %w handling of errIO.
 
2732		panic(fmt.Errorf("reading literal message: %s (%w)", err, errIO))
 
2735		xserverErrorf("read %d bytes for message, expected %d (%w)", msize, size, errIO)
 
2739		line := c.readline(false)
 
2740		np := newParser(line, c)
 
2744		line := c.readline(false)
 
2745		np := newParser(line, c)
 
2750		name = xcheckmailboxname(name, true)
 
2753	var mb store.Mailbox
 
2755	var pendingChanges []store.Change
 
2757	c.account.WithWLock(func() {
 
2758		var changes []store.Change
 
2759		c.xdbwrite(func(tx *bstore.Tx) {
 
2760			mb = c.xmailbox(tx, name, "TRYCREATE")
 
2762			// Ensure keywords are stored in mailbox.
 
2763			var mbKwChanged bool
 
2764			mb.Keywords, mbKwChanged = store.MergeKeywords(mb.Keywords, keywords)
 
2766				changes = append(changes, mb.ChangeKeywords())
 
2771				MailboxOrigID: mb.ID,
 
2778			ok, maxSize, err := c.account.CanAddMessageSize(tx, m.Size)
 
2779			xcheckf(err, "checking quota")
 
2782				xusercodeErrorf("OVERQUOTA", "account over maximum total message size %d", maxSize)
 
2785			mb.Add(m.MailboxCounts())
 
2787			// Update mailbox before delivering, which updates uidnext which we mustn't overwrite.
 
2788			err = tx.Update(&mb)
 
2789			xcheckf(err, "updating mailbox counts")
 
2791			err = c.account.DeliverMessage(c.log, tx, &m, msgFile, true, false, false, true)
 
2792			xcheckf(err, "delivering message")
 
2795		// Fetch pending changes, possibly with new UIDs, so we can apply them before adding our own new UID.
 
2797			pendingChanges = c.comm.Get()
 
2800		// Broadcast the change to other connections.
 
2801		changes = append(changes, m.ChangeAddUID(), mb.ChangeCounts())
 
2802		c.broadcast(changes)
 
2805	if c.mailboxID == mb.ID {
 
2806		c.applyChanges(pendingChanges, false)
 
2808		// todo spec: with condstore/qresync, is there a mechanism to the client know the modseq for the appended uid? in theory an untagged fetch with the modseq after the OK APPENDUID could make sense, but this probably isn't allowed.
 
2809		c.bwritelinef("* %d EXISTS", len(c.uids))
 
2812	c.writeresultf("%s OK [APPENDUID %d %d] appended", tag, mb.UIDValidity, m.UID)
 
2815// Idle makes a client wait until the server sends untagged updates, e.g. about
 
2816// message delivery or mailbox create/rename/delete/subscription, etc. It allows a
 
2817// client to get updates in real-time, not needing the use for NOOP.
 
2819// State: Authenticated and selected.
 
2820func (c *conn) cmdIdle(tag, cmd string, p *parser) {
 
2827	c.writelinef("+ waiting")
 
2833		case le := <-c.lineChan():
 
2835			xcheckf(le.err, "get line")
 
2838		case <-c.comm.Pending:
 
2839			c.applyChanges(c.comm.Get(), false)
 
2841		case <-mox.Shutdown.Done():
 
2843			c.writelinef("* BYE shutting down")
 
2848	// Reset the write deadline. In case of little activity, with a command timeout of
 
2849	// 30 minutes, we have likely passed it.
 
2850	err := c.conn.SetWriteDeadline(time.Now().Add(5 * time.Minute))
 
2851	c.log.Check(err, "setting write deadline")
 
2853	if strings.ToUpper(line) != "DONE" {
 
2854		// We just close the connection because our protocols are out of sync.
 
2855		panic(fmt.Errorf("%w: in IDLE, expected DONE", errIO))
 
2861// Check is an old deprecated command that is supposed to execute some mailbox consistency checks.
 
2864func (c *conn) cmdCheck(tag, cmd string, p *parser) {
 
2870	c.account.WithRLock(func() {
 
2871		c.xdbread(func(tx *bstore.Tx) {
 
2872			c.xmailboxID(tx, c.mailboxID) // Validate.
 
2879// Close undoes select/examine, closing the currently opened mailbox and deleting
 
2880// messages that were marked for deletion with the \Deleted flag.
 
2883func (c *conn) cmdClose(tag, cmd string, p *parser) {
 
2895	remove, _ := c.xexpunge(nil, true)
 
2898		for _, m := range remove {
 
2899			p := c.account.MessagePath(m.ID)
 
2901			c.xsanity(err, "removing message file for expunge for close")
 
2909// expunge messages marked for deletion in currently selected/active mailbox.
 
2910// if uidSet is not nil, only messages matching the set are deleted.
 
2912// messages that have been marked expunged from the database are returned, but the
 
2913// corresponding files still have to be removed.
 
2915// the highest modseq in the mailbox is returned, typically associated with the
 
2916// removal of the messages, but if no messages were expunged the current latest max
 
2917// modseq for the mailbox is returned.
 
2918func (c *conn) xexpunge(uidSet *numSet, missingMailboxOK bool) (remove []store.Message, highestModSeq store.ModSeq) {
 
2919	var modseq store.ModSeq
 
2921	c.account.WithWLock(func() {
 
2922		var mb store.Mailbox
 
2924		c.xdbwrite(func(tx *bstore.Tx) {
 
2925			mb = store.Mailbox{ID: c.mailboxID}
 
2927			if err == bstore.ErrAbsent {
 
2928				if missingMailboxOK {
 
2931				xuserErrorf("%w", store.ErrUnknownMailbox)
 
2934			qm := bstore.QueryTx[store.Message](tx)
 
2935			qm.FilterNonzero(store.Message{MailboxID: c.mailboxID})
 
2936			qm.FilterEqual("Deleted", true)
 
2937			qm.FilterEqual("Expunged", false)
 
2938			qm.FilterFn(func(m store.Message) bool {
 
2939				// Only remove if this session knows about the message and if present in optional uidSet.
 
2940				return uidSearch(c.uids, m.UID) > 0 && (uidSet == nil || uidSet.containsUID(m.UID, c.uids, c.searchResult))
 
2943			remove, err = qm.List()
 
2944			xcheckf(err, "listing messages to delete")
 
2946			if len(remove) == 0 {
 
2947				highestModSeq = c.xhighestModSeq(tx, c.mailboxID)
 
2951			// Assign new modseq.
 
2952			modseq, err = c.account.NextModSeq(tx)
 
2953			xcheckf(err, "assigning next modseq")
 
2954			highestModSeq = modseq
 
2956			removeIDs := make([]int64, len(remove))
 
2957			anyIDs := make([]any, len(remove))
 
2959			for i, m := range remove {
 
2962				mb.Sub(m.MailboxCounts())
 
2964				// Update "remove", because RetrainMessage below will save the message.
 
2965				remove[i].Expunged = true
 
2966				remove[i].ModSeq = modseq
 
2968			qmr := bstore.QueryTx[store.Recipient](tx)
 
2969			qmr.FilterEqual("MessageID", anyIDs...)
 
2970			_, err = qmr.Delete()
 
2971			xcheckf(err, "removing message recipients")
 
2973			qm = bstore.QueryTx[store.Message](tx)
 
2974			qm.FilterIDs(removeIDs)
 
2975			n, err := qm.UpdateNonzero(store.Message{Expunged: true, ModSeq: modseq})
 
2976			if err == nil && n != len(removeIDs) {
 
2977				err = fmt.Errorf("only %d messages set to expunged, expected %d", n, len(removeIDs))
 
2979			xcheckf(err, "marking messages marked for deleted as expunged")
 
2981			err = tx.Update(&mb)
 
2982			xcheckf(err, "updating mailbox counts")
 
2984			err = c.account.AddMessageSize(c.log, tx, -totalSize)
 
2985			xcheckf(err, "updating disk usage")
 
2987			// Mark expunged messages as not needing training, then retrain them, so if they
 
2988			// were trained, they get untrained.
 
2989			for i := range remove {
 
2990				remove[i].Junk = false
 
2991				remove[i].Notjunk = false
 
2993			err = c.account.RetrainMessages(context.TODO(), c.log, tx, remove, true)
 
2994			xcheckf(err, "untraining expunged messages")
 
2997		// Broadcast changes to other connections. We may not have actually removed any
 
2998		// messages, so take care not to send an empty update.
 
2999		if len(remove) > 0 {
 
3000			ouids := make([]store.UID, len(remove))
 
3001			for i, m := range remove {
 
3004			changes := []store.Change{
 
3005				store.ChangeRemoveUIDs{MailboxID: c.mailboxID, UIDs: ouids, ModSeq: modseq},
 
3008			c.broadcast(changes)
 
3011	return remove, highestModSeq
 
3014// Unselect is similar to close in that it closes the currently active mailbox, but
 
3015// it does not remove messages marked for deletion.
 
3018func (c *conn) cmdUnselect(tag, cmd string, p *parser) {
 
3028// Expunge deletes messages marked with \Deleted in the currently selected mailbox.
 
3029// Clients are wiser to use UID EXPUNGE because it allows a UID sequence set to
 
3030// explicitly opt in to removing specific messages.
 
3033func (c *conn) cmdExpunge(tag, cmd string, p *parser) {
 
3040		xuserErrorf("mailbox open in read-only mode")
 
3043	c.cmdxExpunge(tag, cmd, nil)
 
3046// UID expunge deletes messages marked with \Deleted in the currently selected
 
3047// mailbox if they match a UID sequence set.
 
3050func (c *conn) cmdUIDExpunge(tag, cmd string, p *parser) {
 
3055	uidSet := p.xnumSet()
 
3059		xuserErrorf("mailbox open in read-only mode")
 
3062	c.cmdxExpunge(tag, cmd, &uidSet)
 
3065// Permanently delete messages for the currently selected/active mailbox. If uidset
 
3066// is not nil, only those UIDs are removed.
 
3068func (c *conn) cmdxExpunge(tag, cmd string, uidSet *numSet) {
 
3071	remove, highestModSeq := c.xexpunge(uidSet, false)
 
3074		for _, m := range remove {
 
3075			p := c.account.MessagePath(m.ID)
 
3077			c.xsanity(err, "removing message file for expunge")
 
3082	var vanishedUIDs numSet
 
3083	qresync := c.enabled[capQresync]
 
3084	for _, m := range remove {
 
3085		seq := c.xsequence(m.UID)
 
3086		c.sequenceRemove(seq, m.UID)
 
3088			vanishedUIDs.append(uint32(m.UID))
 
3090			c.bwritelinef("* %d EXPUNGE", seq)
 
3093	if !vanishedUIDs.empty() {
 
3095		for _, s := range vanishedUIDs.Strings(4*1024 - 32) {
 
3096			c.bwritelinef("* VANISHED %s", s)
 
3100	if c.enabled[capCondstore] {
 
3101		c.writeresultf("%s OK [HIGHESTMODSEQ %d] expunged", tag, highestModSeq.Client())
 
3108func (c *conn) cmdSearch(tag, cmd string, p *parser) {
 
3109	c.cmdxSearch(false, tag, cmd, p)
 
3113func (c *conn) cmdUIDSearch(tag, cmd string, p *parser) {
 
3114	c.cmdxSearch(true, tag, cmd, p)
 
3118func (c *conn) cmdFetch(tag, cmd string, p *parser) {
 
3119	c.cmdxFetch(false, tag, cmd, p)
 
3123func (c *conn) cmdUIDFetch(tag, cmd string, p *parser) {
 
3124	c.cmdxFetch(true, tag, cmd, p)
 
3128func (c *conn) cmdStore(tag, cmd string, p *parser) {
 
3129	c.cmdxStore(false, tag, cmd, p)
 
3133func (c *conn) cmdUIDStore(tag, cmd string, p *parser) {
 
3134	c.cmdxStore(true, tag, cmd, p)
 
3138func (c *conn) cmdCopy(tag, cmd string, p *parser) {
 
3139	c.cmdxCopy(false, tag, cmd, p)
 
3143func (c *conn) cmdUIDCopy(tag, cmd string, p *parser) {
 
3144	c.cmdxCopy(true, tag, cmd, p)
 
3148func (c *conn) cmdMove(tag, cmd string, p *parser) {
 
3149	c.cmdxMove(false, tag, cmd, p)
 
3153func (c *conn) cmdUIDMove(tag, cmd string, p *parser) {
 
3154	c.cmdxMove(true, tag, cmd, p)
 
3157func (c *conn) gatherCopyMoveUIDs(isUID bool, nums numSet) ([]store.UID, []any) {
 
3158	// Gather uids, then sort so we can return a consistently simple and hard to
 
3159	// misinterpret COPYUID/MOVEUID response. It seems safer to have UIDs in ascending
 
3160	// order, because requested uid set of 12:10 is equal to 10:12, so if we would just
 
3161	// echo whatever the client sends us without reordering, the client can reorder our
 
3162	// response and interpret it differently than we intended.
 
3164	uids := c.xnumSetUIDs(isUID, nums)
 
3165	sort.Slice(uids, func(i, j int) bool {
 
3166		return uids[i] < uids[j]
 
3168	uidargs := make([]any, len(uids))
 
3169	for i, uid := range uids {
 
3172	return uids, uidargs
 
3175// Copy copies messages from the currently selected/active mailbox to another named
 
3179func (c *conn) cmdxCopy(isUID bool, tag, cmd string, p *parser) {
 
3186	name := p.xmailbox()
 
3189	name = xcheckmailboxname(name, true)
 
3191	uids, uidargs := c.gatherCopyMoveUIDs(isUID, nums)
 
3193	// Files that were created during the copy. Remove them if the operation fails.
 
3194	var createdIDs []int64
 
3200		for _, id := range createdIDs {
 
3201			p := c.account.MessagePath(id)
 
3203			c.xsanity(err, "cleaning up created file")
 
3208	var mbDst store.Mailbox
 
3209	var origUIDs, newUIDs []store.UID
 
3210	var flags []store.Flags
 
3211	var keywords [][]string
 
3212	var modseq store.ModSeq // For messages in new mailbox, assigned when first message is copied.
 
3214	c.account.WithWLock(func() {
 
3215		var mbKwChanged bool
 
3217		c.xdbwrite(func(tx *bstore.Tx) {
 
3218			mbSrc := c.xmailboxID(tx, c.mailboxID) // Validate.
 
3219			mbDst = c.xmailbox(tx, name, "TRYCREATE")
 
3220			if mbDst.ID == mbSrc.ID {
 
3221				xuserErrorf("cannot copy to currently selected mailbox")
 
3224			if len(uidargs) == 0 {
 
3225				xuserErrorf("no matching messages to copy")
 
3229			modseq, err = c.account.NextModSeq(tx)
 
3230			xcheckf(err, "assigning next modseq")
 
3232			// Reserve the uids in the destination mailbox.
 
3233			uidFirst := mbDst.UIDNext
 
3234			mbDst.UIDNext += store.UID(len(uidargs))
 
3236			// Fetch messages from database.
 
3237			q := bstore.QueryTx[store.Message](tx)
 
3238			q.FilterNonzero(store.Message{MailboxID: c.mailboxID})
 
3239			q.FilterEqual("UID", uidargs...)
 
3240			q.FilterEqual("Expunged", false)
 
3241			xmsgs, err := q.List()
 
3242			xcheckf(err, "fetching messages")
 
3244			if len(xmsgs) != len(uidargs) {
 
3245				xserverErrorf("uid and message mismatch")
 
3248			// See if quota allows copy.
 
3250			for _, m := range xmsgs {
 
3253			if ok, maxSize, err := c.account.CanAddMessageSize(tx, totalSize); err != nil {
 
3254				xcheckf(err, "checking quota")
 
3257				xusercodeErrorf("OVERQUOTA", "account over maximum total message size %d", maxSize)
 
3259			err = c.account.AddMessageSize(c.log, tx, totalSize)
 
3260			xcheckf(err, "updating disk usage")
 
3262			msgs := map[store.UID]store.Message{}
 
3263			for _, m := range xmsgs {
 
3266			nmsgs := make([]store.Message, len(xmsgs))
 
3268			conf, _ := c.account.Conf()
 
3270			mbKeywords := map[string]struct{}{}
 
3272			// Insert new messages into database.
 
3273			var origMsgIDs, newMsgIDs []int64
 
3274			for i, uid := range uids {
 
3277					xuserErrorf("messages changed, could not fetch requested uid")
 
3280				origMsgIDs = append(origMsgIDs, origID)
 
3282				m.UID = uidFirst + store.UID(i)
 
3283				m.CreateSeq = modseq
 
3285				m.MailboxID = mbDst.ID
 
3286				if m.IsReject && m.MailboxDestinedID != 0 {
 
3287					// Incorrectly delivered to Rejects mailbox. Adjust MailboxOrigID so this message
 
3288					// is used for reputation calculation during future deliveries.
 
3289					m.MailboxOrigID = m.MailboxDestinedID
 
3293				m.JunkFlagsForMailbox(mbDst, conf)
 
3294				err := tx.Insert(&m)
 
3295				xcheckf(err, "inserting message")
 
3298				origUIDs = append(origUIDs, uid)
 
3299				newUIDs = append(newUIDs, m.UID)
 
3300				newMsgIDs = append(newMsgIDs, m.ID)
 
3301				flags = append(flags, m.Flags)
 
3302				keywords = append(keywords, m.Keywords)
 
3303				for _, kw := range m.Keywords {
 
3304					mbKeywords[kw] = struct{}{}
 
3307				qmr := bstore.QueryTx[store.Recipient](tx)
 
3308				qmr.FilterNonzero(store.Recipient{MessageID: origID})
 
3309				mrs, err := qmr.List()
 
3310				xcheckf(err, "listing message recipients")
 
3311				for _, mr := range mrs {
 
3314					err := tx.Insert(&mr)
 
3315					xcheckf(err, "inserting message recipient")
 
3318				mbDst.Add(m.MailboxCounts())
 
3321			mbDst.Keywords, mbKwChanged = store.MergeKeywords(mbDst.Keywords, maps.Keys(mbKeywords))
 
3323			err = tx.Update(&mbDst)
 
3324			xcheckf(err, "updating destination mailbox for uids, keywords and counts")
 
3326			// Copy message files to new message ID's.
 
3327			syncDirs := map[string]struct{}{}
 
3328			for i := range origMsgIDs {
 
3329				src := c.account.MessagePath(origMsgIDs[i])
 
3330				dst := c.account.MessagePath(newMsgIDs[i])
 
3331				dstdir := filepath.Dir(dst)
 
3332				if _, ok := syncDirs[dstdir]; !ok {
 
3333					os.MkdirAll(dstdir, 0770)
 
3334					syncDirs[dstdir] = struct{}{}
 
3336				err := moxio.LinkOrCopy(c.log, dst, src, nil, true)
 
3337				xcheckf(err, "link or copy file %q to %q", src, dst)
 
3338				createdIDs = append(createdIDs, newMsgIDs[i])
 
3341			for dir := range syncDirs {
 
3342				err := moxio.SyncDir(c.log, dir)
 
3343				xcheckf(err, "sync directory")
 
3346			err = c.account.RetrainMessages(context.TODO(), c.log, tx, nmsgs, false)
 
3347			xcheckf(err, "train copied messages")
 
3350		// Broadcast changes to other connections.
 
3351		if len(newUIDs) > 0 {
 
3352			changes := make([]store.Change, 0, len(newUIDs)+2)
 
3353			for i, uid := range newUIDs {
 
3354				changes = append(changes, store.ChangeAddUID{MailboxID: mbDst.ID, UID: uid, ModSeq: modseq, Flags: flags[i], Keywords: keywords[i]})
 
3356			changes = append(changes, mbDst.ChangeCounts())
 
3358				changes = append(changes, mbDst.ChangeKeywords())
 
3360			c.broadcast(changes)
 
3364	// All good, prevent defer above from cleaning up copied files.
 
3368	c.writeresultf("%s OK [COPYUID %d %s %s] copied", tag, mbDst.UIDValidity, compactUIDSet(origUIDs).String(), compactUIDSet(newUIDs).String())
 
3371// Move moves messages from the currently selected/active mailbox to a named mailbox.
 
3374func (c *conn) cmdxMove(isUID bool, tag, cmd string, p *parser) {
 
3381	name := p.xmailbox()
 
3384	name = xcheckmailboxname(name, true)
 
3387		xuserErrorf("mailbox open in read-only mode")
 
3390	uids, uidargs := c.gatherCopyMoveUIDs(isUID, nums)
 
3392	var mbSrc, mbDst store.Mailbox
 
3393	var changes []store.Change
 
3394	var newUIDs []store.UID
 
3395	var modseq store.ModSeq
 
3397	c.account.WithWLock(func() {
 
3398		c.xdbwrite(func(tx *bstore.Tx) {
 
3399			mbSrc = c.xmailboxID(tx, c.mailboxID) // Validate.
 
3400			mbDst = c.xmailbox(tx, name, "TRYCREATE")
 
3401			if mbDst.ID == c.mailboxID {
 
3402				xuserErrorf("cannot move to currently selected mailbox")
 
3405			if len(uidargs) == 0 {
 
3406				xuserErrorf("no matching messages to move")
 
3409			// Reserve the uids in the destination mailbox.
 
3410			uidFirst := mbDst.UIDNext
 
3412			mbDst.UIDNext += store.UID(len(uids))
 
3414			// Assign a new modseq, for the new records and for the expunged records.
 
3416			modseq, err = c.account.NextModSeq(tx)
 
3417			xcheckf(err, "assigning next modseq")
 
3419			// Update existing record with new UID and MailboxID in database for messages. We
 
3420			// add a new but expunged record again in the original/source mailbox, for qresync.
 
3421			// Keeping the original ID for the live message means we don't have to move the
 
3422			// on-disk message contents file.
 
3423			q := bstore.QueryTx[store.Message](tx)
 
3424			q.FilterNonzero(store.Message{MailboxID: c.mailboxID})
 
3425			q.FilterEqual("UID", uidargs...)
 
3426			q.FilterEqual("Expunged", false)
 
3428			msgs, err := q.List()
 
3429			xcheckf(err, "listing messages to move")
 
3431			if len(msgs) != len(uidargs) {
 
3432				xserverErrorf("uid and message mismatch")
 
3435			keywords := map[string]struct{}{}
 
3437			conf, _ := c.account.Conf()
 
3438			for i := range msgs {
 
3440				if m.UID != uids[i] {
 
3441					xserverErrorf("internal error: got uid %d, expected %d, for index %d", m.UID, uids[i], i)
 
3444				mbSrc.Sub(m.MailboxCounts())
 
3446				// Copy of message record that we'll insert when UID is freed up.
 
3449				om.ID = 0 // Assign new ID.
 
3452				m.MailboxID = mbDst.ID
 
3453				if m.IsReject && m.MailboxDestinedID != 0 {
 
3454					// Incorrectly delivered to Rejects mailbox. Adjust MailboxOrigID so this message
 
3455					// is used for reputation calculation during future deliveries.
 
3456					m.MailboxOrigID = m.MailboxDestinedID
 
3460				mbDst.Add(m.MailboxCounts())
 
3463				m.JunkFlagsForMailbox(mbDst, conf)
 
3466				xcheckf(err, "updating moved message in database")
 
3468				// Now that UID is unused, we can insert the old record again.
 
3469				err = tx.Insert(&om)
 
3470				xcheckf(err, "inserting record for expunge after moving message")
 
3472				for _, kw := range m.Keywords {
 
3473					keywords[kw] = struct{}{}
 
3477			// Ensure destination mailbox has keywords of the moved messages.
 
3478			var mbKwChanged bool
 
3479			mbDst.Keywords, mbKwChanged = store.MergeKeywords(mbDst.Keywords, maps.Keys(keywords))
 
3481				changes = append(changes, mbDst.ChangeKeywords())
 
3484			err = tx.Update(&mbSrc)
 
3485			xcheckf(err, "updating source mailbox counts")
 
3487			err = tx.Update(&mbDst)
 
3488			xcheckf(err, "updating destination mailbox for uids, keywords and counts")
 
3490			err = c.account.RetrainMessages(context.TODO(), c.log, tx, msgs, false)
 
3491			xcheckf(err, "retraining messages after move")
 
3493			// Prepare broadcast changes to other connections.
 
3494			changes = make([]store.Change, 0, 1+len(msgs)+2)
 
3495			changes = append(changes, store.ChangeRemoveUIDs{MailboxID: c.mailboxID, UIDs: uids, ModSeq: modseq})
 
3496			for _, m := range msgs {
 
3497				newUIDs = append(newUIDs, m.UID)
 
3498				changes = append(changes, m.ChangeAddUID())
 
3500			changes = append(changes, mbSrc.ChangeCounts(), mbDst.ChangeCounts())
 
3503		c.broadcast(changes)
 
3508	c.bwritelinef("* OK [COPYUID %d %s %s] moved", mbDst.UIDValidity, compactUIDSet(uids).String(), compactUIDSet(newUIDs).String())
 
3509	qresync := c.enabled[capQresync]
 
3510	var vanishedUIDs numSet
 
3511	for i := 0; i < len(uids); i++ {
 
3512		seq := c.xsequence(uids[i])
 
3513		c.sequenceRemove(seq, uids[i])
 
3515			vanishedUIDs.append(uint32(uids[i]))
 
3517			c.bwritelinef("* %d EXPUNGE", seq)
 
3520	if !vanishedUIDs.empty() {
 
3522		for _, s := range vanishedUIDs.Strings(4*1024 - 32) {
 
3523			c.bwritelinef("* VANISHED %s", s)
 
3527	if c.enabled[capQresync] {
 
3529		c.writeresultf("%s OK [HIGHESTMODSEQ %d] move", tag, modseq.Client())
 
3535// Store sets a full set of flags, or adds/removes specific flags.
 
3538func (c *conn) cmdxStore(isUID bool, tag, cmd string, p *parser) {
 
3545	var unchangedSince *int64
 
3548		p.xtake("UNCHANGEDSINCE")
 
3555		c.xensureCondstore(nil)
 
3557	var plus, minus bool
 
3560	} else if p.take("-") {
 
3564	silent := p.take(".SILENT")
 
3566	var flagstrs []string
 
3567	if p.hasPrefix("(") {
 
3568		flagstrs = p.xflagList()
 
3570		flagstrs = append(flagstrs, p.xflag())
 
3572			flagstrs = append(flagstrs, p.xflag())
 
3578		xuserErrorf("mailbox open in read-only mode")
 
3581	flags, keywords, err := store.ParseFlagsKeywords(flagstrs)
 
3583		xuserErrorf("parsing flags: %v", err)
 
3585	var mask store.Flags
 
3587		mask, flags = flags, store.FlagsAll
 
3589		mask, flags = flags, store.Flags{}
 
3591		mask = store.FlagsAll
 
3594	var mb, origmb store.Mailbox
 
3595	var updated []store.Message
 
3596	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.
 
3597	var modseq store.ModSeq     // Assigned when needed.
 
3598	modified := map[int64]bool{}
 
3600	c.account.WithWLock(func() {
 
3601		var mbKwChanged bool
 
3602		var changes []store.Change
 
3604		c.xdbwrite(func(tx *bstore.Tx) {
 
3605			mb = c.xmailboxID(tx, c.mailboxID) // Validate.
 
3608			uidargs := c.xnumSetCondition(isUID, nums)
 
3610			if len(uidargs) == 0 {
 
3614			// Ensure keywords are in mailbox.
 
3616				mb.Keywords, mbKwChanged = store.MergeKeywords(mb.Keywords, keywords)
 
3618					err := tx.Update(&mb)
 
3619					xcheckf(err, "updating mailbox with keywords")
 
3623			q := bstore.QueryTx[store.Message](tx)
 
3624			q.FilterNonzero(store.Message{MailboxID: c.mailboxID})
 
3625			q.FilterEqual("UID", uidargs...)
 
3626			q.FilterEqual("Expunged", false)
 
3627			err := q.ForEach(func(m store.Message) error {
 
3628				// Client may specify a message multiple times, but we only process it once. 
../rfc/7162:823 
3633				mc := m.MailboxCounts()
 
3635				origFlags := m.Flags
 
3636				m.Flags = m.Flags.Set(mask, flags)
 
3637				oldKeywords := append([]string{}, m.Keywords...)
 
3639					m.Keywords, _ = store.RemoveKeywords(m.Keywords, keywords)
 
3641					m.Keywords, _ = store.MergeKeywords(m.Keywords, keywords)
 
3643					m.Keywords = keywords
 
3646				keywordsChanged := func() bool {
 
3647					sort.Strings(oldKeywords)
 
3648					n := append([]string{}, m.Keywords...)
 
3650					return !slices.Equal(oldKeywords, n)
 
3653				// If the message has a more recent modseq than the check requires, we won't modify
 
3654				// it and report in the final command response.
 
3657				// unchangedSince 0 always fails the check, we don't turn it into 1 like with our
 
3658				// internal modseqs. RFC implies that is not required for non-system flags, but we
 
3660				if unchangedSince != nil && m.ModSeq.Client() > *unchangedSince {
 
3661					changed = append(changed, m)
 
3666				// It requires that we keep track of the flags we think the client knows (but only
 
3667				// on this connection). We don't track that. It also isn't clear why this is
 
3668				// allowed because it is skipping the condstore conditional check, and the new
 
3669				// combination of flags could be unintended.
 
3672				if origFlags == m.Flags && !keywordsChanged() {
 
3673					// Note: since we didn't update the modseq, we are not adding m.ID to "modified",
 
3674					// it would skip the modseq check above. We still add m to list of updated, so we
 
3675					// send an untagged fetch response. But we don't broadcast it.
 
3676					updated = append(updated, m)
 
3681				mb.Add(m.MailboxCounts())
 
3683				// Assign new modseq for first actual change.
 
3686					modseq, err = c.account.NextModSeq(tx)
 
3687					xcheckf(err, "next modseq")
 
3690				modified[m.ID] = true
 
3691				updated = append(updated, m)
 
3693				changes = append(changes, m.ChangeFlags(origFlags))
 
3695				return tx.Update(&m)
 
3697			xcheckf(err, "storing flags in messages")
 
3699			if mb.MailboxCounts != origmb.MailboxCounts {
 
3700				err := tx.Update(&mb)
 
3701				xcheckf(err, "updating mailbox counts")
 
3703				changes = append(changes, mb.ChangeCounts())
 
3706				changes = append(changes, mb.ChangeKeywords())
 
3709			err = c.account.RetrainMessages(context.TODO(), c.log, tx, updated, false)
 
3710			xcheckf(err, "training messages")
 
3713		c.broadcast(changes)
 
3716	// In the RFC, the section about STORE/UID STORE says we must return MODSEQ when
 
3717	// UNCHANGEDSINCE was specified. It does not specify it in case UNCHANGEDSINCE
 
3718	// isn't specified. For that case it does say MODSEQ is needed in unsolicited
 
3719	// untagged fetch responses. Implying that solicited untagged fetch responses
 
3720	// should not include MODSEQ (why else mention unsolicited explicitly?). But, in
 
3721	// the introduction to CONDSTORE it does explicitly specify MODSEQ should be
 
3722	// included in untagged fetch responses at all times with CONDSTORE-enabled
 
3723	// connections. It would have been better if the command behaviour was specified in
 
3724	// the command section, not the introduction to the extension.
 
3727	if !silent || c.enabled[capCondstore] {
 
3728		for _, m := range updated {
 
3731				flags = fmt.Sprintf(" FLAGS %s", flaglist(m.Flags, m.Keywords).pack(c))
 
3733			var modseqStr string
 
3734			if c.enabled[capCondstore] {
 
3735				modseqStr = fmt.Sprintf(" MODSEQ (%d)", m.ModSeq.Client())
 
3738			c.bwritelinef("* %d FETCH (UID %d%s%s)", c.xsequence(m.UID), m.UID, flags, modseqStr)
 
3742	// We don't explicitly send flags for failed updated with silent set. The regular
 
3743	// notification will get the flags to the client.
 
3746	if len(changed) == 0 {
 
3751	// Write unsolicited untagged fetch responses for messages that didn't pass the
 
3754	var mnums []store.UID
 
3755	for _, m := range changed {
 
3756		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())
 
3758			mnums = append(mnums, m.UID)
 
3760			mnums = append(mnums, store.UID(c.xsequence(m.UID)))
 
3764	sort.Slice(mnums, func(i, j int) bool {
 
3765		return mnums[i] < mnums[j]
 
3767	set := compactUIDSet(mnums)
 
3769	c.writeresultf("%s OK [MODIFIED %s] conditional store did not modify all", tag, set.String())