diff --git a/imapclient/parse.go b/imapclient/parse.go index 0eed343..794d94e 100644 --- a/imapclient/parse.go +++ b/imapclient/parse.go @@ -39,7 +39,7 @@ func (c *Conn) readbyte() (byte, error) { return b, err } -func (c *Conn) unreadbyte() { +func (c *Conn) xunreadbyte() { if c.record { c.recordBuf = c.recordBuf[:len(c.recordBuf)-1] } @@ -70,7 +70,7 @@ func (c *Conn) xcrlf() { func (c *Conn) peek(exp byte) bool { b, err := c.readbyte() if err == nil { - c.unreadbyte() + c.xunreadbyte() } return err == nil && strings.EqualFold(string(rune(b)), string(rune(exp))) } @@ -264,7 +264,7 @@ func (c *Conn) xtakeuntil(b byte) string { x, err := c.readbyte() c.xcheckf(err, "read byte") if x == b { - c.unreadbyte() + c.xunreadbyte() return s } s += string(rune(x)) @@ -279,14 +279,14 @@ func (c *Conn) xdigits() string { s += string(rune(b)) continue } - c.unreadbyte() + c.xunreadbyte() return s } } func (c *Conn) peekdigit() bool { if b, err := c.readbyte(); err == nil { - c.unreadbyte() + c.xunreadbyte() return b >= '0' && b <= '9' } return false @@ -692,7 +692,7 @@ func (c *Conn) xmsgatt1() FetchAttr { f += string(rune(b)) continue } - c.unreadbyte() + c.xunreadbyte() break } @@ -851,8 +851,7 @@ func (c *Conn) xatom() string { b, err := c.readbyte() c.xcheckf(err, "read byte for atom") if b <= ' ' || strings.IndexByte("(){%*\"\\]", b) >= 0 { - err := c.br.UnreadByte() - c.xcheckf(err, "unreadbyte") + c.xunreadbyte() if s == "" { c.xerrorf("expected atom") } @@ -1288,7 +1287,7 @@ func (c *Conn) xtaggedExtVal() TaggedExtVal { b, err := c.readbyte() c.xcheckf(err, "read byte for tagged-ext-val") if b < '0' || b > '9' { - c.unreadbyte() + c.xunreadbyte() ss := c.xsequenceSet() return TaggedExtVal{SeqSet: &ss} } diff --git a/imapserver/fetch.go b/imapserver/fetch.go index db3ae03..38029b4 100644 --- a/imapserver/fetch.go +++ b/imapserver/fetch.go @@ -242,7 +242,7 @@ func (c *conn) cmdxFetch(isUID bool, tag, cmdstr string, p *parser) { // No hard limit on response sizes, but clients are recommended to not send more // than 8k. We send a more conservative max 4k. for _, s := range compactUIDSet(vanishedUIDs).Strings(4*1024 - 32) { - c.bwritelinef("* VANISHED (EARLIER) %s", s) + c.xbwritelinef("* VANISHED (EARLIER) %s", s) } } @@ -338,7 +338,7 @@ func (c *conn) cmdxFetch(isUID bool, tag, cmdstr string, p *parser) { if cmd.expungeIssued { // ../rfc/2180:343 // ../rfc/9051:5102 - c.writeresultf("%s OK [EXPUNGEISSUED] at least one message was expunged", tag) + c.xwriteresultf("%s OK [EXPUNGEISSUED] at least one message was expunged", tag) } else { c.ok(tag, cmdstr) } @@ -447,7 +447,7 @@ func (cmd *fetchCmd) process(atts []fetchAtt) { // Write errors are turned into panics because we write through c. fmt.Fprintf(cmd.conn.xbw, "* %d FETCH ", cmd.conn.xsequence(cmd.uid)) - data.writeTo(cmd.conn, cmd.conn.xbw) + data.xwriteTo(cmd.conn, cmd.conn.xbw) cmd.conn.xbw.Write([]byte("\r\n")) } diff --git a/imapserver/list.go b/imapserver/list.go index 0d69c23..50ed277 100644 --- a/imapserver/list.go +++ b/imapserver/list.go @@ -115,7 +115,7 @@ func (c *conn) cmdList(tag, cmd string, p *parser) { if !isExtended && reference == "" && patterns[0] == "" { // ../rfc/9051:2277 ../rfc/3501:2221 - c.bwritelinef(`* LIST () "/" ""`) + c.xbwritelinef(`* LIST () "/" ""`) c.ok(tag, cmd) return } @@ -261,11 +261,11 @@ func (c *conn) cmdList(tag, cmd string, p *parser) { }) for _, line := range responseLines { - c.bwritelinef("%s", line) + c.xbwritelinef("%s", line) } for _, meta := range respMetadata { - meta.writeTo(c, c.xbw) - c.bwritelinef("") + meta.xwriteTo(c, c.xbw) + c.xbwritelinef("") } c.ok(tag, cmd) } diff --git a/imapserver/metadata.go b/imapserver/metadata.go index 5929ea8..f64f28a 100644 --- a/imapserver/metadata.go +++ b/imapserver/metadata.go @@ -160,20 +160,20 @@ func (c *conn) cmdGetmetadata(tag, cmd string, p *parser) { if i > 0 { fmt.Fprint(c.xbw, " ") } - astring(a.Key).writeTo(c, c.xbw) + astring(a.Key).xwriteTo(c, c.xbw) fmt.Fprint(c.xbw, " ") if a.IsString { - string0(string(a.Value)).writeTo(c, c.xbw) + string0(string(a.Value)).xwriteTo(c, c.xbw) } else { v := readerSizeSyncliteral{bytes.NewReader(a.Value), int64(len(a.Value)), true} - v.writeTo(c, c.xbw) + v.xwriteTo(c, c.xbw) } } - c.bwritelinef(")") + c.xbwritelinef(")") } if longentries >= 0 { - c.bwritelinef("%s OK [METADATA LONGENTRIES %d] getmetadata done", tag, longentries) + c.xbwritelinef("%s OK [METADATA LONGENTRIES %d] getmetadata done", tag, longentries) } else { c.ok(tag, cmd) } diff --git a/imapserver/pack.go b/imapserver/pack.go index 6244578..b232f64 100644 --- a/imapserver/pack.go +++ b/imapserver/pack.go @@ -9,7 +9,7 @@ import ( type token interface { pack(c *conn) string - writeTo(c *conn, xw io.Writer) // Writes to xw panic on error. + xwriteTo(c *conn, xw io.Writer) // Writes to xw panic on error. } type bare string @@ -18,7 +18,7 @@ func (t bare) pack(c *conn) string { return string(t) } -func (t bare) writeTo(c *conn, xw io.Writer) { +func (t bare) xwriteTo(c *conn, xw io.Writer) { xw.Write([]byte(t.pack(c))) } @@ -30,7 +30,7 @@ func (t niltoken) pack(c *conn) string { return "NIL" } -func (t niltoken) writeTo(c *conn, xw io.Writer) { +func (t niltoken) xwriteTo(c *conn, xw io.Writer) { xw.Write([]byte(t.pack(c))) } @@ -60,7 +60,7 @@ func (t string0) pack(c *conn) string { return r } -func (t string0) writeTo(c *conn, xw io.Writer) { +func (t string0) xwriteTo(c *conn, xw io.Writer) { xw.Write([]byte(t.pack(c))) } @@ -78,7 +78,7 @@ func (t dquote) pack(c *conn) string { return r } -func (t dquote) writeTo(c *conn, xw io.Writer) { +func (t dquote) xwriteTo(c *conn, xw io.Writer) { xw.Write([]byte(t.pack(c))) } @@ -88,7 +88,7 @@ func (t syncliteral) pack(c *conn) string { return fmt.Sprintf("{%d}\r\n", len(t)) + string(t) } -func (t syncliteral) writeTo(c *conn, xw io.Writer) { +func (t syncliteral) xwriteTo(c *conn, xw io.Writer) { fmt.Fprintf(xw, "{%d}\r\n", len(t)) xw.Write([]byte(t)) } @@ -112,7 +112,7 @@ func (t readerSizeSyncliteral) pack(c *conn) string { return fmt.Sprintf("%s{%d}\r\n", lit, t.size) + string(buf) } -func (t readerSizeSyncliteral) writeTo(c *conn, xw io.Writer) { +func (t readerSizeSyncliteral) xwriteTo(c *conn, xw io.Writer) { var lit string if t.lit8 { lit = "~" @@ -137,7 +137,7 @@ func (t readerSyncliteral) pack(c *conn) string { return fmt.Sprintf("{%d}\r\n", len(buf)) + string(buf) } -func (t readerSyncliteral) writeTo(c *conn, xw io.Writer) { +func (t readerSyncliteral) xwriteTo(c *conn, xw io.Writer) { buf, err := io.ReadAll(t.r) if err != nil { panic(err) @@ -162,13 +162,13 @@ func (t listspace) pack(c *conn) string { return s } -func (t listspace) writeTo(c *conn, xw io.Writer) { +func (t listspace) xwriteTo(c *conn, xw io.Writer) { fmt.Fprint(xw, "(") for i, e := range t { if i > 0 { fmt.Fprint(xw, " ") } - e.writeTo(c, xw) + e.xwriteTo(c, xw) } fmt.Fprint(xw, ")") } @@ -187,12 +187,12 @@ func (t concatspace) pack(c *conn) string { return s } -func (t concatspace) writeTo(c *conn, xw io.Writer) { +func (t concatspace) xwriteTo(c *conn, xw io.Writer) { for i, e := range t { if i > 0 { fmt.Fprint(xw, " ") } - e.writeTo(c, xw) + e.xwriteTo(c, xw) } } @@ -207,9 +207,9 @@ func (t concat) pack(c *conn) string { return s } -func (t concat) writeTo(c *conn, xw io.Writer) { +func (t concat) xwriteTo(c *conn, xw io.Writer) { for _, e := range t { - e.writeTo(c, xw) + e.xwriteTo(c, xw) } } @@ -231,7 +231,7 @@ next: return string(t) } -func (t astring) writeTo(c *conn, xw io.Writer) { +func (t astring) xwriteTo(c *conn, xw io.Writer) { xw.Write([]byte(t.pack(c))) } @@ -246,7 +246,7 @@ func (t mailboxt) pack(c *conn) string { return astring(s).pack(c) } -func (t mailboxt) writeTo(c *conn, xw io.Writer) { +func (t mailboxt) xwriteTo(c *conn, xw io.Writer) { xw.Write([]byte(t.pack(c))) } @@ -256,6 +256,6 @@ func (t number) pack(c *conn) string { return fmt.Sprintf("%d", t) } -func (t number) writeTo(c *conn, xw io.Writer) { +func (t number) xwriteTo(c *conn, xw io.Writer) { xw.Write([]byte(t.pack(c))) } diff --git a/imapserver/parse.go b/imapserver/parse.go index b02030a..4bef1cc 100644 --- a/imapserver/parse.go +++ b/imapserver/parse.go @@ -306,7 +306,7 @@ func (p *parser) xstring() (r string) { } size, sync := p.xliteralSize(false, true) buf := p.conn.xreadliteral(size, sync) - line := p.conn.readline(false) + line := p.conn.xreadline(false) p.orig, p.upper, p.o = line, toUpper(line), 0 return string(buf) } @@ -1041,7 +1041,7 @@ func (p *parser) xmetadataKeyValue() (key string, isString bool, value []byte) { if p.hasPrefix("~{") { size, sync := p.xliteralSize(true, true) value = p.conn.xreadliteral(size, sync) - line := p.conn.readline(false) + line := p.conn.xreadline(false) p.orig, p.upper, p.o = line, toUpper(line), 0 } else if p.hasPrefix(`"`) { value = []byte(p.xstring()) diff --git a/imapserver/replace.go b/imapserver/replace.go index 52fa434..0d68245 100644 --- a/imapserver/replace.go +++ b/imapserver/replace.go @@ -150,7 +150,7 @@ func (c *conn) cmdxReplace(isUID bool, tag, cmd string, p *parser) { }) }) - c.writelinef("+ ") + c.xwritelinef("+ ") } else { var err error name, _, err = store.CheckMailboxName(name, true) @@ -212,7 +212,7 @@ func (c *conn) cmdxReplace(isUID bool, tag, cmd string, p *parser) { } // Finish reading the command. - line := c.readline(false) + line := c.xreadline(false) p = newParser(line, c) if utf8 { p.xtake(")") @@ -328,8 +328,8 @@ func (c *conn) cmdxReplace(isUID bool, tag, cmd string, p *parser) { if mbDst.ID == c.mailboxID { c.uidAppend(nm.UID) // We send an untagged OK with APPENDUID, for sane bookkeeping in clients. ../rfc/8508:401 - c.bwritelinef("* OK [APPENDUID %d %d] ", mbDst.UIDValidity, nm.UID) - c.bwritelinef("* %d EXISTS", len(c.uids)) + c.xbwritelinef("* OK [APPENDUID %d %d] ", mbDst.UIDValidity, nm.UID) + c.xbwritelinef("* %d EXISTS", len(c.uids)) } // We must return vanished instead of expunge, and also highestmodseq, when qresync @@ -341,10 +341,10 @@ func (c *conn) cmdxReplace(isUID bool, tag, cmd string, p *parser) { omsgseq := c.xsequence(om.UID) c.sequenceRemove(omsgseq, om.UID) if qresync { - c.bwritelinef("* VANISHED %d", om.UID) + c.xbwritelinef("* VANISHED %d", om.UID) // ../rfc/7162:1916 } else { - c.bwritelinef("* %d EXPUNGE", omsgseq) + c.xbwritelinef("* %d EXPUNGE", omsgseq) } - c.writeresultf("%s OK [HIGHESTMODSEQ %d] replaced", tag, nm.ModSeq.Client()) + c.xwriteresultf("%s OK [HIGHESTMODSEQ %d] replaced", tag, nm.ModSeq.Client()) } diff --git a/imapserver/search.go b/imapserver/search.go index 99381dc..1a04c5a 100644 --- a/imapserver/search.go +++ b/imapserver/search.go @@ -431,7 +431,7 @@ func (c *conn) cmdxSearch(isUID, isE bool, tag, cmd string, p *parser) { lastUID = m.UID if time.Since(inProgressLast) > inProgressPeriod { - c.writelinef("* OK [INPROGRESS (%s %d %s)] still searching", inProgressTag, progress, goal) + c.xwritelinef("* OK [INPROGRESS (%s %d %s)] still searching", inProgressTag, progress, goal) inProgressLast = time.Now() } progress++ @@ -468,7 +468,7 @@ func (c *conn) cmdxSearch(isUID, isE bool, tag, cmd string, p *parser) { xcheckf(err, "list messages in mailbox") if time.Since(inProgressLast) > inProgressPeriod { - c.writelinef("* OK [INPROGRESS (%s %d %s)] still searching", inProgressTag, progress, goal) + c.xwritelinef("* OK [INPROGRESS (%s %d %s)] still searching", inProgressTag, progress, goal) inProgressLast = time.Now() } progress++ @@ -497,7 +497,7 @@ func (c *conn) cmdxSearch(isUID, isE bool, tag, cmd string, p *parser) { // In IMAP4rev1, an untagged SEARCH response is required. ../rfc/3501:2728 if len(result.UIDs) == 0 { - c.bwritelinef("* SEARCH") + c.xbwritelinef("* SEARCH") } // Old-style SEARCH response. We must spell out each number. So we may be splitting @@ -527,7 +527,7 @@ func (c *conn) cmdxSearch(isUID, isE bool, tag, cmd string, p *parser) { modseq = fmt.Sprintf(" (MODSEQ %d)", result.MaxModSeq.Client()) } - c.bwritelinef("* SEARCH%s%s", s, modseq) + c.xbwritelinef("* SEARCH%s%s", s, modseq) result.UIDs = result.UIDs[n:] } } else { @@ -595,7 +595,7 @@ func (c *conn) cmdxSearch(isUID, isE bool, tag, cmd string, p *parser) { fmt.Fprintf(c.xbw, " MODSEQ %d", result.MaxModSeq.Client()) } - c.bwritelinef("") + c.xbwritelinef("") } } } diff --git a/imapserver/server.go b/imapserver/server.go index 8f85aea..7759584 100644 --- a/imapserver/server.go +++ b/imapserver/server.go @@ -579,7 +579,7 @@ func (c *conn) lineChan() chan lineErr { } // readline from either the c.line channel, or otherwise read from connection. -func (c *conn) readline(readCmd bool) string { +func (c *conn) xreadline(readCmd bool) string { var line string var err error if c.line != nil { @@ -593,7 +593,7 @@ func (c *conn) readline(readCmd bool) string { if readCmd && errors.Is(err, os.ErrDeadlineExceeded) { err := c.conn.SetWriteDeadline(time.Now().Add(10 * time.Second)) c.log.Check(err, "setting write deadline") - c.writelinef("* BYE inactive") + c.xwritelinef("* BYE inactive") } if !errors.Is(err, errIO) && !errors.Is(err, errProtocol) { c.xbrokenf("%s (%w)", err, errIO) @@ -618,13 +618,13 @@ func (c *conn) readline(readCmd bool) string { } // write tagged command response, but first write pending changes. -func (c *conn) writeresultf(format string, args ...any) { - c.bwriteresultf(format, args...) +func (c *conn) xwriteresultf(format string, args ...any) { + c.xbwriteresultf(format, args...) c.xflush() } // write buffered tagged command response, but first write pending changes. -func (c *conn) bwriteresultf(format string, args ...any) { +func (c *conn) xbwriteresultf(format string, args ...any) { switch c.cmd { case "fetch", "store", "search": // ../rfc/9051:5862 ../rfc/7162:2033 @@ -633,16 +633,16 @@ func (c *conn) bwriteresultf(format string, args ...any) { c.applyChanges(c.comm.Get(), false) } } - c.bwritelinef(format, args...) + c.xbwritelinef(format, args...) } -func (c *conn) writelinef(format string, args ...any) { - c.bwritelinef(format, args...) +func (c *conn) xwritelinef(format string, args ...any) { + c.xbwritelinef(format, args...) c.xflush() } // Buffer line for write. -func (c *conn) bwritelinef(format string, args ...any) { +func (c *conn) xbwritelinef(format string, args ...any) { format += "\r\n" fmt.Fprintf(c.xbw, format, args...) } @@ -671,7 +671,7 @@ func (c *conn) xflush() { } func (c *conn) readCommand(tag *string) (cmd string, p *parser) { - line := c.readline(true) + line := c.xreadline(true) p = newParser(line, c) p.context("tag") *tag = p.xtag() @@ -683,7 +683,7 @@ func (c *conn) readCommand(tag *string) (cmd string, p *parser) { func (c *conn) xreadliteral(size int64, sync bool) []byte { if sync { - c.writelinef("+ ") + c.xwritelinef("+ ") } buf := make([]byte, size) if size > 0 { @@ -817,13 +817,13 @@ func serve(listenerName string, cid int64, tlsConfig *tls.Config, nc net.Conn, x select { case <-mox.Shutdown.Done(): // ../rfc/9051:5381 - c.writelinef("* BYE mox shutting down") + c.xwritelinef("* BYE mox shutting down") return default: } if !limiterConnectionrate.Add(c.remoteIP, time.Now(), 1) { - c.writelinef("* BYE connection rate from your ip or network too high, slow down please") + c.xwritelinef("* BYE connection rate from your ip or network too high, slow down please") return } @@ -831,13 +831,13 @@ func serve(listenerName string, cid int64, tlsConfig *tls.Config, nc net.Conn, x if !mox.LimiterFailedAuth.CanAdd(c.remoteIP, time.Now(), 1) { metrics.AuthenticationRatelimitedInc("imap") c.log.Debug("refusing connection due to many auth failures", slog.Any("remoteip", c.remoteIP)) - c.writelinef("* BYE too many auth failures") + c.xwritelinef("* BYE too many auth failures") return } if !limiterConnections.Add(c.remoteIP, time.Now(), 1) { c.log.Debug("refusing connection due to many open connections", slog.Any("remoteip", c.remoteIP)) - c.writelinef("* BYE too many open connections from your ip or network") + c.xwritelinef("* BYE too many open connections from your ip or network") return } defer limiterConnections.Add(c.remoteIP, time.Now(), -1) @@ -851,7 +851,7 @@ func serve(listenerName string, cid int64, tlsConfig *tls.Config, nc net.Conn, x acc, _, _, err := store.OpenEmail(c.log, preauthAddress, false) if err != nil { c.log.Debugx("open account for preauth address", err, slog.String("address", preauthAddress)) - c.writelinef("* BYE open account for address: %s", err) + c.xwritelinef("* BYE open account for address: %s", err) return } c.username = preauthAddress @@ -861,9 +861,9 @@ func serve(listenerName string, cid int64, tlsConfig *tls.Config, nc net.Conn, x if c.account != nil && !c.noPreauth { c.state = stateAuthenticated - c.writelinef("* PREAUTH [CAPABILITY %s] mox imap welcomes %s", c.capabilities(), c.username) + c.xwritelinef("* PREAUTH [CAPABILITY %s] mox imap welcomes %s", c.capabilities(), c.username) } else { - c.writelinef("* OK [CAPABILITY %s] mox imap", c.capabilities()) + c.xwritelinef("* OK [CAPABILITY %s] mox imap", c.capabilities()) } // Ensure any pending loginAttempt is written before we stop. @@ -1101,7 +1101,7 @@ func (c *conn) xtlsHandshakeAndAuthenticate(conn net.Conn) { // Verify client after session resumption. err := c.tlsClientAuthVerifyPeerCertParsed(cs.PeerCertificates[0]) if err != nil { - c.writelinef("* BYE [ALERT] Error verifying client certificate after TLS session resumption: %s", err) + c.xwritelinef("* BYE [ALERT] Error verifying client certificate after TLS session resumption: %s", err) c.xbrokenf("tls verify client certificate after resumption: %s (%w)", err, errIO) } } @@ -1181,7 +1181,7 @@ func (c *conn) command() { // Other side is likely speaking something else than IMAP, send error message and // stop processing because there is a good chance whatever they sent has multiple // lines. - c.writelinef("* BYE please try again speaking imap") + c.xwritelinef("* BYE please try again speaking imap") c.xbrokenf("not speaking imap (%w)", errIO) } c.log.Debugx("imap command syntax error", sxerr.err, logFields...) @@ -1192,13 +1192,13 @@ func (c *conn) command() { c.log.Check(err, "setting write deadline") } if sxerr.line != "" { - c.bwritelinef("%s", sxerr.line) + c.xbwritelinef("%s", sxerr.line) } code := "" if sxerr.code != "" { code = "[" + sxerr.code + "] " } - c.bwriteresultf("%s BAD %s%s unrecognized syntax/command: %v", tag, code, cmd, sxerr.errmsg) + c.xbwriteresultf("%s BAD %s%s unrecognized syntax/command: %v", tag, code, cmd, sxerr.errmsg) if fatal { c.xflush() panic(fmt.Errorf("aborting connection after syntax error for command with non-sync literal: %w", errProtocol)) @@ -1207,14 +1207,14 @@ func (c *conn) command() { result = "servererror" c.log.Errorx("imap command server error", err, logFields...) debug.PrintStack() - c.bwriteresultf("%s NO %s %v", tag, cmd, err) + c.xbwriteresultf("%s NO %s %v", tag, cmd, err) } else if errors.As(err, &uerr) { result = "usererror" c.log.Debugx("imap command user error", err, logFields...) if uerr.code != "" { - c.bwriteresultf("%s NO [%s] %s %v", tag, uerr.code, cmd, err) + c.xbwriteresultf("%s NO [%s] %s %v", tag, uerr.code, cmd, err) } else { - c.bwriteresultf("%s NO %s %v", tag, cmd, err) + c.xbwriteresultf("%s NO %s %v", tag, cmd, err) } } else { // Other type of panic, we pass it on, aborting the connection. @@ -1234,7 +1234,7 @@ func (c *conn) command() { select { case <-mox.Shutdown.Done(): // ../rfc/9051:5375 - c.writelinef("* BYE shutting down") + c.xwritelinef("* BYE shutting down") c.xbrokenf("shutting down (%w)", errIO) default: } @@ -1527,7 +1527,7 @@ func (c *conn) xnumSetConditionUIDs(forDB, returnUIDs bool, isUID bool, nums num } func (c *conn) ok(tag, cmd string) { - c.bwriteresultf("%s OK %s done", tag, cmd) + c.xbwriteresultf("%s OK %s done", tag, cmd) c.xflush() } @@ -1646,14 +1646,14 @@ func (c *conn) applyChanges(changes []store.Change, initial bool) { // Write the exists, and the UID and flags as well. Hopefully the client waits for // long enough after the EXISTS to see these messages, and doesn't request them // again with a FETCH. - c.bwritelinef("* %d EXISTS", len(c.uids)) + c.xbwritelinef("* %d EXISTS", len(c.uids)) for _, add := range adds { seq := c.xsequence(add.UID) var modseqStr string if condstore { modseqStr = fmt.Sprintf(" MODSEQ (%d)", add.ModSeq.Client()) } - c.bwritelinef("* %d FETCH (UID %d FLAGS %s%s)", seq, add.UID, flaglist(add.Flags, add.Keywords).pack(c), modseqStr) + c.xbwritelinef("* %d FETCH (UID %d FLAGS %s%s)", seq, add.UID, flaglist(add.Flags, add.Keywords).pack(c), modseqStr) } continue } @@ -1679,14 +1679,14 @@ func (c *conn) applyChanges(changes []store.Change, initial bool) { if qresync { vanishedUIDs.append(uint32(uid)) } else { - c.bwritelinef("* %d EXPUNGE", seq) + c.xbwritelinef("* %d EXPUNGE", seq) } } } if qresync { // VANISHED without EARLIER. ../rfc/7162:2004 for _, s := range vanishedUIDs.Strings(4*1024 - 32) { - c.bwritelinef("* VANISHED %s", s) + c.xbwritelinef("* VANISHED %s", s) } } case store.ChangeFlags: @@ -1700,29 +1700,29 @@ func (c *conn) applyChanges(changes []store.Change, initial bool) { if condstore { modseqStr = fmt.Sprintf(" MODSEQ (%d)", ch.ModSeq.Client()) } - c.bwritelinef("* %d FETCH (UID %d FLAGS %s%s)", seq, ch.UID, flaglist(ch.Flags, ch.Keywords).pack(c), modseqStr) + c.xbwritelinef("* %d FETCH (UID %d FLAGS %s%s)", seq, ch.UID, flaglist(ch.Flags, ch.Keywords).pack(c), modseqStr) } case store.ChangeRemoveMailbox: // Only announce \NonExistent to modern clients, otherwise they may ignore the // unrecognized \NonExistent and interpret this as a newly created mailbox, while // the goal was to remove it... if c.enabled[capIMAP4rev2] { - c.bwritelinef(`* LIST (\NonExistent) "/" %s`, mailboxt(ch.Name).pack(c)) + c.xbwritelinef(`* LIST (\NonExistent) "/" %s`, mailboxt(ch.Name).pack(c)) } case store.ChangeAddMailbox: - c.bwritelinef(`* LIST (%s) "/" %s`, strings.Join(ch.Flags, " "), mailboxt(ch.Mailbox.Name).pack(c)) + c.xbwritelinef(`* LIST (%s) "/" %s`, strings.Join(ch.Flags, " "), mailboxt(ch.Mailbox.Name).pack(c)) case store.ChangeRenameMailbox: // OLDNAME only with IMAP4rev2 or NOTIFY ../rfc/9051:2726 ../rfc/5465:628 var oldname string if c.enabled[capIMAP4rev2] { oldname = fmt.Sprintf(` ("OLDNAME" (%s))`, mailboxt(ch.OldName).pack(c)) } - c.bwritelinef(`* LIST (%s) "/" %s%s`, strings.Join(ch.Flags, " "), mailboxt(ch.NewName).pack(c), oldname) + c.xbwritelinef(`* LIST (%s) "/" %s%s`, strings.Join(ch.Flags, " "), mailboxt(ch.NewName).pack(c), oldname) case store.ChangeAddSubscription: - c.bwritelinef(`* LIST (%s) "/" %s`, strings.Join(append([]string{`\Subscribed`}, ch.Flags...), " "), mailboxt(ch.Name).pack(c)) + c.xbwritelinef(`* LIST (%s) "/" %s`, strings.Join(append([]string{`\Subscribed`}, ch.Flags...), " "), mailboxt(ch.Name).pack(c)) case store.ChangeAnnotation: // ../rfc/5464:807 ../rfc/5464:788 - c.bwritelinef(`* METADATA %s %s`, mailboxt(ch.MailboxName).pack(c), astring(ch.Key).pack(c)) + c.xbwritelinef(`* METADATA %s %s`, mailboxt(ch.MailboxName).pack(c), astring(ch.Key).pack(c)) default: panic(fmt.Sprintf("internal error, missing case for %#v", change)) } @@ -1742,7 +1742,7 @@ func (c *conn) cmdCapability(tag, cmd string, p *parser) { caps := c.capabilities() // Response syntax: ../rfc/9051:6427 ../rfc/3501:4655 - c.bwritelinef("* CAPABILITY %s", caps) + c.xbwritelinef("* CAPABILITY %s", caps) c.ok(tag, cmd) } @@ -1790,7 +1790,7 @@ func (c *conn) cmdLogout(tag, cmd string, p *parser) { c.unselect() c.state = stateNotAuthenticated // Response syntax: ../rfc/9051:6886 ../rfc/3501:4935 - c.bwritelinef("* BYE thanks") + c.xbwritelinef("* BYE thanks") c.ok(tag, cmd) panic(cleanClose) } @@ -1843,9 +1843,9 @@ func (c *conn) cmdID(tag, cmd string, p *parser) { // Response syntax: ../rfc/2971:243 // We send our name, and only the version for authenticated users. ../rfc/2971:193 if c.state == stateAuthenticated || c.state == stateSelected { - c.bwritelinef(`* ID ("name" "mox" "version" %s)`, string0(moxvar.Version).pack(c)) + c.xbwritelinef(`* ID ("name" "mox" "version" %s)`, string0(moxvar.Version).pack(c)) } else { - c.bwritelinef(`* ID ("name" "mox")`) + c.xbwritelinef(`* ID ("name" "mox")`) } c.ok(tag, cmd) } @@ -1981,8 +1981,8 @@ func (c *conn) cmdAuthenticate(tag, cmd string, p *parser) { xreadInitial := func() []byte { var line string if p.empty() { - c.writelinef("+ ") - line = c.readline(false) + c.xwritelinef("+ ") + line = c.xreadline(false) } else { // ../rfc/9051:1407 ../rfc/4959:84 p.xspace() @@ -2005,7 +2005,7 @@ func (c *conn) cmdAuthenticate(tag, cmd string, p *parser) { } xreadContinuation := func() []byte { - line := c.readline(false) + line := c.xreadline(false) if line == "*" { c.loginAttempt.Result = store.AuthAborted xsyntaxErrorf("authenticate aborted by client") @@ -2074,7 +2074,7 @@ func (c *conn) cmdAuthenticate(tag, cmd string, p *parser) { // ../rfc/2195:82 chal := fmt.Sprintf("<%d.%d@%s>", uint64(mox.CryptoRandInt()), time.Now().UnixNano(), mox.Conf.Static.HostnameDomain.ASCII) - c.writelinef("+ %s", base64.StdEncoding.EncodeToString([]byte(chal))) + c.xwritelinef("+ %s", base64.StdEncoding.EncodeToString([]byte(chal))) resp := xreadContinuation() t := strings.Split(string(resp), " ") @@ -2202,14 +2202,14 @@ func (c *conn) cmdAuthenticate(tag, cmd string, p *parser) { }) s1, err := ss.ServerFirst(xscram.Iterations, xscram.Salt) xcheckf(err, "scram first server step") - c.writelinef("+ %s", base64.StdEncoding.EncodeToString([]byte(s1))) + c.xwritelinef("+ %s", base64.StdEncoding.EncodeToString([]byte(s1))) c2 := xreadContinuation() s3, err := ss.Finish(c2, xscram.SaltedPassword) if len(s3) > 0 { - c.writelinef("+ %s", base64.StdEncoding.EncodeToString([]byte(s3))) + c.xwritelinef("+ %s", base64.StdEncoding.EncodeToString([]byte(s3))) } if err != nil { - c.readline(false) // Should be "*" for cancellation. + c.xreadline(false) // Should be "*" for cancellation. if errors.Is(err, scram.ErrInvalidProof) { c.loginAttempt.Result = store.AuthBadCredentials c.log.Info("failed authentication attempt", slog.String("username", username), slog.Any("remote", c.remoteIP)) @@ -2304,7 +2304,7 @@ func (c *conn) cmdAuthenticate(tag, cmd string, p *parser) { c.loginAttempt.Result = store.AuthSuccess c.authFailed = 0 c.state = stateAuthenticated - c.writeresultf("%s OK [CAPABILITY %s] authenticate done", tag, c.capabilities()) + c.xwriteresultf("%s OK [CAPABILITY %s] authenticate done", tag, c.capabilities()) } // Login logs in with username and password. @@ -2412,7 +2412,7 @@ func (c *conn) cmdLogin(tag, cmd string, p *parser) { c.authFailed = 0 c.setSlow(false) c.state = stateAuthenticated - c.writeresultf("%s OK [CAPABILITY %s] login done", tag, c.capabilities()) + c.xwriteresultf("%s OK [CAPABILITY %s] login done", tag, c.capabilities()) } // Enable explicitly opts in to an extension. A server can typically send new kinds @@ -2461,7 +2461,7 @@ func (c *conn) cmdEnable(tag, cmd string, p *parser) { } // Response syntax: ../rfc/9051:6520 ../rfc/5161:211 - c.bwritelinef("* ENABLED%s", enabled) + c.xbwritelinef("* ENABLED%s", enabled) c.ok(tag, cmd) } @@ -2486,7 +2486,7 @@ func (c *conn) xensureCondstore(tx *bstore.Tx) { } else { mb = c.xmailboxID(tx, c.mailboxID) } - c.bwritelinef("* OK [HIGHESTMODSEQ %d] after condstore-enabling command", mb.ModSeq.Client()) + c.xbwritelinef("* OK [HIGHESTMODSEQ %d] after condstore-enabling command", mb.ModSeq.Client()) } } @@ -2573,7 +2573,7 @@ func (c *conn) cmdSelectExamine(isselect bool, tag, cmd string, p *parser) { // ../rfc/9051:1809 if c.state == stateSelected { // ../rfc/9051:1812 ../rfc/7162:2111 - c.bwritelinef("* OK [CLOSED] x") + c.xbwritelinef("* OK [CLOSED] x") c.unselect() } @@ -2624,23 +2624,23 @@ func (c *conn) cmdSelectExamine(isselect bool, tag, cmd string, p *parser) { if len(mb.Keywords) > 0 { flags = " " + strings.Join(mb.Keywords, " ") } - c.bwritelinef(`* FLAGS (\Seen \Answered \Flagged \Deleted \Draft $Forwarded $Junk $NotJunk $Phishing $MDNSent%s)`, flags) - c.bwritelinef(`* OK [PERMANENTFLAGS (\Seen \Answered \Flagged \Deleted \Draft $Forwarded $Junk $NotJunk $Phishing $MDNSent \*)] x`) + c.xbwritelinef(`* FLAGS (\Seen \Answered \Flagged \Deleted \Draft $Forwarded $Junk $NotJunk $Phishing $MDNSent%s)`, flags) + c.xbwritelinef(`* OK [PERMANENTFLAGS (\Seen \Answered \Flagged \Deleted \Draft $Forwarded $Junk $NotJunk $Phishing $MDNSent \*)] x`) if !c.enabled[capIMAP4rev2] { - c.bwritelinef(`* 0 RECENT`) + c.xbwritelinef(`* 0 RECENT`) } - c.bwritelinef(`* %d EXISTS`, len(c.uids)) + c.xbwritelinef(`* %d EXISTS`, len(c.uids)) if !c.enabled[capIMAP4rev2] && firstUnseen > 0 { // ../rfc/9051:8051 ../rfc/3501:1774 - c.bwritelinef(`* OK [UNSEEN %d] x`, firstUnseen) + c.xbwritelinef(`* OK [UNSEEN %d] x`, firstUnseen) } - c.bwritelinef(`* OK [UIDVALIDITY %d] x`, mb.UIDValidity) - c.bwritelinef(`* OK [UIDNEXT %d] x`, mb.UIDNext) - c.bwritelinef(`* LIST () "/" %s`, mailboxt(mb.Name).pack(c)) + c.xbwritelinef(`* OK [UIDVALIDITY %d] x`, mb.UIDValidity) + c.xbwritelinef(`* OK [UIDNEXT %d] x`, mb.UIDNext) + c.xbwritelinef(`* LIST () "/" %s`, mailboxt(mb.Name).pack(c)) if c.enabled[capCondstore] { // ../rfc/7162:417 // ../rfc/7162-eid5055 ../rfc/7162:484 ../rfc/7162:1167 - c.bwritelinef(`* OK [HIGHESTMODSEQ %d] x`, highestModSeq.Client()) + c.xbwritelinef(`* OK [HIGHESTMODSEQ %d] x`, highestModSeq.Client()) } // If QRESYNC uidvalidity matches, we send any changes. ../rfc/7162:1509 @@ -2711,7 +2711,7 @@ func (c *conn) cmdSelectExamine(isselect bool, tag, cmd string, p *parser) { qrmodseq = m.ModSeq.Client() - 1 preVanished = 0 qrknownUIDs = nil - 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.") + c.xbwritelinef("* 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.") } } else if err != bstore.ErrAbsent { xcheckf(err, "checking old client uid") @@ -2738,7 +2738,7 @@ func (c *conn) cmdSelectExamine(isselect bool, tag, cmd string, p *parser) { } msgseq := c.sequence(m.UID) if msgseq > 0 { - c.bwritelinef("* %d FETCH (UID %d FLAGS %s MODSEQ (%d))", msgseq, m.UID, flaglist(m.Flags, m.Keywords).pack(c), m.ModSeq.Client()) + c.xbwritelinef("* %d FETCH (UID %d FLAGS %s MODSEQ (%d))", msgseq, m.UID, flaglist(m.Flags, m.Keywords).pack(c), m.ModSeq.Client()) } return nil }) @@ -2774,16 +2774,16 @@ func (c *conn) cmdSelectExamine(isselect bool, tag, cmd string, p *parser) { l := slices.Sorted(maps.Keys(vanishedUIDs)) // ../rfc/7162:1985 for _, s := range compactUIDSet(l).Strings(4*1024 - 32) { - c.bwritelinef("* VANISHED (EARLIER) %s", s) + c.xbwritelinef("* VANISHED (EARLIER) %s", s) } } } if isselect { - c.bwriteresultf("%s OK [READ-WRITE] x", tag) + c.xbwriteresultf("%s OK [READ-WRITE] x", tag) c.readonly = false } else { - c.bwriteresultf("%s OK [READ-ONLY] x", tag) + c.xbwriteresultf("%s OK [READ-ONLY] x", tag) c.readonly = true } c.mailboxID = mb.ID @@ -2870,7 +2870,7 @@ func (c *conn) cmdCreate(tag, cmd string, p *parser) { if c.enabled[capIMAP4rev2] && n == name && name != origName && !(name == "Inbox" || strings.HasPrefix(name, "Inbox/")) { oldname = fmt.Sprintf(` ("OLDNAME" (%s))`, mailboxt(origName).pack(c)) } - c.bwritelinef(`* LIST (\Subscribed) "/" %s%s`, mailboxt(n).pack(c), oldname) + c.xbwritelinef(`* LIST (\Subscribed) "/" %s%s`, mailboxt(n).pack(c), oldname) } c.ok(tag, cmd) } @@ -3145,7 +3145,7 @@ func (c *conn) cmdLsub(tag, cmd string, p *parser) { // Response syntax: ../rfc/3501:4833 ../rfc/3501:4837 for _, line := range lines { - c.bwritelinef("%s", line) + c.xbwritelinef("%s", line) } c.ok(tag, cmd) } @@ -3163,7 +3163,7 @@ func (c *conn) cmdNamespace(tag, cmd string, p *parser) { p.xempty() // Response syntax: ../rfc/9051:6778 ../rfc/2342:415 - c.bwritelinef(`* NAMESPACE (("" "/")) NIL NIL`) + c.xbwritelinef(`* NAMESPACE (("" "/")) NIL NIL`) c.ok(tag, cmd) } @@ -3200,7 +3200,7 @@ func (c *conn) cmdStatus(tag, cmd string, p *parser) { }) }) - c.bwritelinef("%s", responseLine) + c.xbwritelinef("%s", responseLine) c.ok(tag, cmd) } @@ -3394,7 +3394,7 @@ func (c *conn) cmdAppend(tag, cmd string, p *parser) { defer store.CloseRemoveTempFile(c.log, a.file, "temporary message file") f = a.file - c.writelinef("+ ") + c.xwritelinef("+ ") } else { // We'll discard the message and return an error as soon as we can (possible // synchronizing literal of next message, or after we've seen all messages). @@ -3422,7 +3422,7 @@ func (c *conn) cmdAppend(tag, cmd string, p *parser) { } totalSize += msize - line := c.readline(false) + line := c.xreadline(false) p = newParser(line, c) if utf8 { p.xtake(")") @@ -3524,7 +3524,7 @@ func (c *conn) cmdAppend(tag, cmd string, p *parser) { c.uidAppend(a.m.UID) } // todo spec: with condstore/qresync, is there a mechanism to let the client know the modseq for the appended uid? in theory an untagged fetch with the modseq after the OK APPENDUID could make sense, but this probably isn't allowed. - c.bwritelinef("* %d EXISTS", len(c.uids)) + c.xbwritelinef("* %d EXISTS", len(c.uids)) } // ../rfc/4315:289 ../rfc/3502:236 APPENDUID @@ -3535,7 +3535,7 @@ func (c *conn) cmdAppend(tag, cmd string, p *parser) { } else { uidset = fmt.Sprintf("%d:%d", appends[0].m.UID, appends[len(appends)-1].m.UID) } - c.writeresultf("%s OK [APPENDUID %d %s] appended", tag, mb.UIDValidity, uidset) + c.xwriteresultf("%s OK [APPENDUID %d %s] appended", tag, mb.UIDValidity, uidset) } // Idle makes a client wait until the server sends untagged updates, e.g. about @@ -3550,7 +3550,7 @@ func (c *conn) cmdIdle(tag, cmd string, p *parser) { // Request syntax: ../rfc/9051:6594 ../rfc/2177:163 p.xempty() - c.writelinef("+ waiting") + c.xwritelinef("+ waiting") var line string wait: @@ -3566,7 +3566,7 @@ wait: c.xflush() case <-mox.Shutdown.Done(): // ../rfc/9051:5375 - c.writelinef("* BYE shutting down") + c.xwritelinef("* BYE shutting down") c.xbrokenf("shutting down (%w)", errIO) } } @@ -3616,13 +3616,13 @@ func (c *conn) cmdGetquotaroot(tag, cmd string, p *parser) { // We only have one per account quota, we name it "" like the examples in the RFC. // Response syntax: ../rfc/9208:668 ../rfc/2087:242 - c.bwritelinef(`* QUOTAROOT %s ""`, astring(name).pack(c)) + c.xbwritelinef(`* QUOTAROOT %s ""`, astring(name).pack(c)) // We only write the quota response if there is a limit. The syntax doesn't allow // an empty list, so we cannot send the current disk usage if there is no limit. if quota > 0 { // Response syntax: ../rfc/9208:666 ../rfc/2087:239 - c.bwritelinef(`* QUOTA "" (STORAGE %d %d)`, (size+1024-1)/1024, (quota+1024-1)/1024) + c.xbwritelinef(`* QUOTA "" (STORAGE %d %d)`, (size+1024-1)/1024, (quota+1024-1)/1024) } c.ok(tag, cmd) } @@ -3660,7 +3660,7 @@ func (c *conn) cmdGetquota(tag, cmd string, p *parser) { // an empty list, so we cannot send the current disk usage if there is no limit. if quota > 0 { // Response syntax: ../rfc/9208:666 ../rfc/2087:239 - c.bwritelinef(`* QUOTA "" (STORAGE %d %d)`, (size+1024-1)/1024, (quota+1024-1)/1024) + c.xbwritelinef(`* QUOTA "" (STORAGE %d %d)`, (size+1024-1)/1024, (quota+1024-1)/1024) } c.ok(tag, cmd) } @@ -3830,18 +3830,18 @@ func (c *conn) cmdxExpunge(tag, cmd string, uidSet *numSet) { if qresync { vanishedUIDs.append(uint32(m.UID)) } else { - c.bwritelinef("* %d EXPUNGE", seq) + c.xbwritelinef("* %d EXPUNGE", seq) } } if !vanishedUIDs.empty() { // VANISHED without EARLIER. ../rfc/7162:2004 for _, s := range vanishedUIDs.Strings(4*1024 - 32) { - c.bwritelinef("* VANISHED %s", s) + c.xbwritelinef("* VANISHED %s", s) } } if c.enabled[capCondstore] { - c.writeresultf("%s OK [HIGHESTMODSEQ %d] expunged", tag, highestModSeq.Client()) + c.xwriteresultf("%s OK [HIGHESTMODSEQ %d] expunged", tag, highestModSeq.Client()) } else { c.ok(tag, cmd) } @@ -4119,7 +4119,7 @@ func (c *conn) cmdxCopy(isUID bool, tag, cmd string, p *parser) { }) // ../rfc/9051:6881 ../rfc/4315:183 - c.writeresultf("%s OK [COPYUID %d %s %s] copied", tag, mbDst.UIDValidity, compactUIDSet(origUIDs).String(), compactUIDSet(newUIDs).String()) + c.xwriteresultf("%s OK [COPYUID %d %s %s] copied", tag, mbDst.UIDValidity, compactUIDSet(origUIDs).String(), compactUIDSet(newUIDs).String()) } // Move moves messages from the currently selected/active mailbox to a named mailbox. @@ -4197,7 +4197,7 @@ func (c *conn) cmdxMove(isUID bool, tag, cmd string, p *parser) { // ../rfc/9051:4708 ../rfc/6851:254 // ../rfc/9051:4713 newUIDs := numSet{ranges: []numRange{{setNumber{number: uint32(uidFirst)}, &setNumber{number: uint32(mbDst.UIDNext - 1)}}}} - c.bwritelinef("* OK [COPYUID %d %s %s] moved", mbDst.UIDValidity, compactUIDSet(uids).String(), newUIDs.String()) + c.xbwritelinef("* OK [COPYUID %d %s %s] moved", mbDst.UIDValidity, compactUIDSet(uids).String(), newUIDs.String()) qresync := c.enabled[capQresync] var vanishedUIDs numSet for i := range uids { @@ -4206,19 +4206,19 @@ func (c *conn) cmdxMove(isUID bool, tag, cmd string, p *parser) { if qresync { vanishedUIDs.append(uint32(uids[i])) } else { - c.bwritelinef("* %d EXPUNGE", seq) + c.xbwritelinef("* %d EXPUNGE", seq) } } if !vanishedUIDs.empty() { // VANISHED without EARLIER. ../rfc/7162:2004 for _, s := range vanishedUIDs.Strings(4*1024 - 32) { - c.bwritelinef("* VANISHED %s", s) + c.xbwritelinef("* VANISHED %s", s) } } if qresync { // ../rfc/9051:6744 ../rfc/7162:1334 - c.writeresultf("%s OK [HIGHESTMODSEQ %d] move", tag, modseq.Client()) + c.xwriteresultf("%s OK [HIGHESTMODSEQ %d] move", tag, modseq.Client()) } else { c.ok(tag, cmd) } @@ -4570,7 +4570,7 @@ func (c *conn) cmdxStore(isUID bool, tag, cmd string, p *parser) { modseqStr = fmt.Sprintf(" MODSEQ (%d)", m.ModSeq.Client()) } // ../rfc/9051:6749 ../rfc/3501:4869 ../rfc/7162:2490 - c.bwritelinef("* %d FETCH (UID %d%s%s)", c.xsequence(m.UID), m.UID, flags, modseqStr) + c.xbwritelinef("* %d FETCH (UID %d%s%s)", c.xsequence(m.UID), m.UID, flags, modseqStr) } } @@ -4588,7 +4588,7 @@ func (c *conn) cmdxStore(isUID bool, tag, cmd string, p *parser) { // Also gather UIDs or sequences for the MODIFIED response below. ../rfc/7162:571 var mnums []store.UID for _, m := range changed { - 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()) + c.xbwritelinef("* %d FETCH (UID %d FLAGS %s MODSEQ (%d))", c.xsequence(m.UID), m.UID, flaglist(m.Flags, m.Keywords).pack(c), m.ModSeq.Client()) if isUID { mnums = append(mnums, m.UID) } else { @@ -4599,5 +4599,5 @@ func (c *conn) cmdxStore(isUID bool, tag, cmd string, p *parser) { slices.Sort(mnums) set := compactUIDSet(mnums) // ../rfc/7162:2506 - c.writeresultf("%s OK [MODIFIED %s] conditional store did not modify all", tag, set.String()) + c.xwriteresultf("%s OK [MODIFIED %s] conditional store did not modify all", tag, set.String()) } diff --git a/smtpserver/server.go b/smtpserver/server.go index def9194..7531ebc 100644 --- a/smtpserver/server.go +++ b/smtpserver/server.go @@ -779,10 +779,10 @@ func (c *conn) Read(buf []byte) (int, error) { // Filled on demand. var bufpool = moxio.NewBufpool(8, 2*1024) -func (c *conn) readline() string { +func (c *conn) xreadline() string { line, err := bufpool.Readline(c.log, c.xbr) if err != nil && errors.Is(err, moxio.ErrLineTooLong) { - c.writecodeline(smtp.C500BadSyntax, smtp.SeProto5Other0, "line too long, smtp max is 512, we reached 2048", nil) + c.xwritecodeline(smtp.C500BadSyntax, smtp.SeProto5Other0, "line too long, smtp max is 512, we reached 2048", nil) panic(fmt.Errorf("%s (%w)", err, errIO)) } else if err != nil { panic(fmt.Errorf("%s (%w)", err, errIO)) @@ -792,7 +792,7 @@ func (c *conn) readline() string { // Buffered-write command response line to connection with codes and msg. // Err is not sent to remote but is used for logging and can be empty. -func (c *conn) bwritecodeline(code int, secode string, msg string, err error) { +func (c *conn) xbwritecodeline(code int, secode string, msg string, err error) { var ecode string if secode != "" { ecode = fmt.Sprintf("%d.%s", code/100, secode) @@ -820,19 +820,19 @@ func (c *conn) bwritecodeline(code int, secode string, msg string, err error) { for ; e > 400 && line[e] != ' '; e-- { } // todo future: understand if ecode should be on each line. won't hurt. at least as long as we don't do expn or vrfy. - c.bwritelinef("%d-%s%s%s", code, ecode, sep, line[:e]) + c.xbwritelinef("%d-%s%s%s", code, ecode, sep, line[:e]) line = line[e:] } spdash := " " if i < len(lines)-1 { spdash = "-" } - c.bwritelinef("%d%s%s%s%s", code, spdash, ecode, sep, line) + c.xbwritelinef("%d%s%s%s%s", code, spdash, ecode, sep, line) } } // Buffered-write a formatted response line to connection. -func (c *conn) bwritelinef(format string, args ...any) { +func (c *conn) xbwritelinef(format string, args ...any) { msg := fmt.Sprintf(format, args...) fmt.Fprint(c.xbw, msg+"\r\n") } @@ -843,14 +843,14 @@ func (c *conn) xflush() { } // Write (with flush) a response line with codes and message. err is not written, used for logging and can be nil. -func (c *conn) writecodeline(code int, secode string, msg string, err error) { - c.bwritecodeline(code, secode, msg, err) +func (c *conn) xwritecodeline(code int, secode string, msg string, err error) { + c.xbwritecodeline(code, secode, msg, err) c.xflush() } // Write (with flush) a formatted response line to connection. -func (c *conn) writelinef(format string, args ...any) { - c.bwritelinef(format, args...) +func (c *conn) xwritelinef(format string, args ...any) { + c.xbwritelinef(format, args...) c.xflush() } @@ -965,13 +965,13 @@ func serve(listenerName string, cid int64, hostname dns.Domain, tlsConfig *tls.C select { case <-mox.Shutdown.Done(): // ../rfc/5321:2811 ../rfc/5321:1666 ../rfc/3463:420 - c.writecodeline(smtp.C421ServiceUnavail, smtp.SeSys3NotAccepting2, "shutting down", nil) + c.xwritecodeline(smtp.C421ServiceUnavail, smtp.SeSys3NotAccepting2, "shutting down", nil) return default: } if !limiterConnectionRate.Add(c.remoteIP, time.Now(), 1) { - c.writecodeline(smtp.C421ServiceUnavail, smtp.SePol7Other0, "connection rate from your ip or network too high, slow down please", nil) + c.xwritecodeline(smtp.C421ServiceUnavail, smtp.SePol7Other0, "connection rate from your ip or network too high, slow down please", nil) return } @@ -979,13 +979,13 @@ func serve(listenerName string, cid int64, hostname dns.Domain, tlsConfig *tls.C if submission && !mox.LimiterFailedAuth.CanAdd(c.remoteIP, time.Now(), 1) { metrics.AuthenticationRatelimitedInc("submission") c.log.Debug("refusing connection due to many auth failures", slog.Any("remoteip", c.remoteIP)) - c.writecodeline(smtp.C421ServiceUnavail, smtp.SePol7Other0, "too many auth failures", nil) + c.xwritecodeline(smtp.C421ServiceUnavail, smtp.SePol7Other0, "too many auth failures", nil) return } if !limiterConnections.Add(c.remoteIP, time.Now(), 1) { c.log.Debug("refusing connection due to many open connections", slog.Any("remoteip", c.remoteIP)) - c.writecodeline(smtp.C421ServiceUnavail, smtp.SePol7Other0, "too many open connections from your ip or network", nil) + c.xwritecodeline(smtp.C421ServiceUnavail, smtp.SePol7Other0, "too many open connections from your ip or network", nil) return } defer limiterConnections.Add(c.remoteIP, time.Now(), -1) @@ -1000,7 +1000,7 @@ func serve(listenerName string, cid int64, hostname dns.Domain, tlsConfig *tls.C // We include the string ESMTP. https://cr.yp.to/smtp/greeting.html recommends it. // Should not be too relevant nowadays, but does not hurt and default blackbox // exporter SMTP health check expects it. - c.writelinef("%d %s ESMTP mox", smtp.C220ServiceReady, c.hostname.ASCII) + c.xwritelinef("%d %s ESMTP mox", smtp.C220ServiceReady, c.hostname.ASCII) for { command(c) @@ -1051,7 +1051,7 @@ func command(c *conn) { var serr smtpError if errors.As(err, &serr) { - c.writecodeline(serr.code, serr.secode, fmt.Sprintf("%s (%s)", serr.errmsg, mox.ReceivedID(c.cid)), serr.err) + c.xwritecodeline(serr.code, serr.secode, fmt.Sprintf("%s (%s)", serr.errmsg, mox.ReceivedID(c.cid)), serr.err) if serr.printStack { c.log.Errorx("smtp error", serr.err, slog.Int("code", serr.code), slog.String("secode", serr.secode)) debug.PrintStack() @@ -1065,7 +1065,7 @@ func command(c *conn) { // todo future: we could wait for either a line or shutdown, and just close the connection on shutdown. - line := c.readline() + line := c.xreadline() t := strings.SplitN(line, " ", 2) var args string if len(t) == 2 { @@ -1079,7 +1079,7 @@ func command(c *conn) { select { case <-mox.Shutdown.Done(): // ../rfc/5321:2811 ../rfc/5321:1666 ../rfc/3463:420 - c.writecodeline(smtp.C421ServiceUnavail, smtp.SeSys3NotAccepting2, "shutting down", nil) + c.xwritecodeline(smtp.C421ServiceUnavail, smtp.SeSys3NotAccepting2, "shutting down", nil) panic(errIO) default: } @@ -1095,7 +1095,7 @@ func command(c *conn) { // Other side is likely speaking something else than SMTP, send error message and // stop processing because there is a good chance whatever they sent has multiple // lines. - c.writecodeline(smtp.C500BadSyntax, smtp.SeProto5Syntax2, "please try again speaking smtp", nil) + c.xwritecodeline(smtp.C500BadSyntax, smtp.SeProto5Syntax2, "please try again speaking smtp", nil) panic(errIO) } // note: not "command not implemented", see ../rfc/5321:2934 ../rfc/5321:2539 @@ -1186,17 +1186,17 @@ func (c *conn) cmdHello(p *parser, ehlo bool) { // https://www.iana.org/assignments/mail-parameters/mail-parameters.xhtml - c.bwritelinef("250-%s", c.hostname.ASCII) - c.bwritelinef("250-PIPELINING") // ../rfc/2920:108 - c.bwritelinef("250-SIZE %d", c.maxMessageSize) // ../rfc/1870:70 + c.xbwritelinef("250-%s", c.hostname.ASCII) + c.xbwritelinef("250-PIPELINING") // ../rfc/2920:108 + c.xbwritelinef("250-SIZE %d", c.maxMessageSize) // ../rfc/1870:70 // ../rfc/3207:237 if !c.tls && c.baseTLSConfig != nil { // ../rfc/3207:90 - c.bwritelinef("250-STARTTLS") + c.xbwritelinef("250-STARTTLS") } else if c.extRequireTLS { // ../rfc/8689:202 // ../rfc/8689:143 - c.bwritelinef("250-REQUIRETLS") + c.xbwritelinef("250-REQUIRETLS") } if c.submission { var mechs string @@ -1212,16 +1212,16 @@ func (c *conn) cmdHello(p *parser, ehlo bool) { if c.tls && len(c.conn.(*tls.Conn).ConnectionState().PeerCertificates) > 0 && !c.viaHTTPS { mechs = "EXTERNAL " + mechs } - c.bwritelinef("250-AUTH %s", mechs) + c.xbwritelinef("250-AUTH %s", mechs) // ../rfc/4865:127 t := time.Now().Add(queue.FutureReleaseIntervalMax).UTC() // ../rfc/4865:98 - c.bwritelinef("250-FUTURERELEASE %d %s", queue.FutureReleaseIntervalMax/time.Second, t.Format(time.RFC3339)) + c.xbwritelinef("250-FUTURERELEASE %d %s", queue.FutureReleaseIntervalMax/time.Second, t.Format(time.RFC3339)) } - c.bwritelinef("250-ENHANCEDSTATUSCODES") // ../rfc/2034:71 + c.xbwritelinef("250-ENHANCEDSTATUSCODES") // ../rfc/2034:71 // todo future? c.writelinef("250-DSN") - c.bwritelinef("250-8BITMIME") // ../rfc/6152:86 - c.bwritelinef("250-LIMITS RCPTMAX=%d", rcptToLimit) // ../rfc/9422:301 - c.bwritecodeline(250, "", "SMTPUTF8", nil) // ../rfc/6531:201 + c.xbwritelinef("250-8BITMIME") // ../rfc/6152:86 + c.xbwritelinef("250-LIMITS RCPTMAX=%d", rcptToLimit) // ../rfc/9422:301 + c.xbwritecodeline(250, "", "SMTPUTF8", nil) // ../rfc/6531:201 c.xflush() } @@ -1254,7 +1254,7 @@ func (c *conn) cmdStarttls(p *parser) { } // We add the cid to the output, to help debugging in case of a failing TLS connection. - c.writecodeline(smtp.C220ServiceReady, smtp.SeOther00, "go! ("+mox.ReceivedID(c.cid)+")", nil) + c.xwritecodeline(smtp.C220ServiceReady, smtp.SeOther00, "go! ("+mox.ReceivedID(c.cid)+")", nil) c.xtlsHandshakeAndAuthenticate(conn) @@ -1321,9 +1321,9 @@ func (c *conn) cmdAuth(p *parser) { xreadInitial := func(encChal string) []byte { var auth string if p.empty() { - c.writelinef("%d %s", smtp.C334ContinueAuth, encChal) // ../rfc/4954:205 + c.xwritelinef("%d %s", smtp.C334ContinueAuth, encChal) // ../rfc/4954:205 // todo future: handle max length of 12288 octets and return proper responde codes otherwise ../rfc/4954:253 - auth = c.readline() + auth = c.xreadline() if auth == "*" { // ../rfc/4954:193 la.Result = store.AuthAborted @@ -1355,7 +1355,7 @@ func (c *conn) cmdAuth(p *parser) { } xreadContinuation := func() []byte { - line := c.readline() + line := c.xreadline() if line == "*" { la.Result = store.AuthAborted xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5Other0, "authentication aborted") @@ -1444,7 +1444,7 @@ func (c *conn) cmdAuth(p *parser) { // Again, client should ignore the challenge, we send the same as the example in // the I-D. - c.writelinef("%d %s", smtp.C334ContinueAuth, base64.StdEncoding.EncodeToString([]byte("Password:"))) + c.xwritelinef("%d %s", smtp.C334ContinueAuth, base64.StdEncoding.EncodeToString([]byte("Password:"))) // Password is in line in plain text, so hide it. defer c.xtrace(mlog.LevelTraceauth)() @@ -1468,7 +1468,7 @@ func (c *conn) cmdAuth(p *parser) { // ../rfc/2195:82 chal := fmt.Sprintf("<%d.%d@%s>", uint64(mox.CryptoRandInt()), time.Now().UnixNano(), mox.Conf.Static.HostnameDomain.ASCII) - c.writelinef("%d %s", smtp.C334ContinueAuth, base64.StdEncoding.EncodeToString([]byte(chal))) + c.xwritelinef("%d %s", smtp.C334ContinueAuth, base64.StdEncoding.EncodeToString([]byte(chal))) resp := xreadContinuation() t := strings.Split(string(resp), " ") @@ -1597,14 +1597,14 @@ func (c *conn) cmdAuth(p *parser) { }) s1, err := ss.ServerFirst(xscram.Iterations, xscram.Salt) xcheckf(err, "scram first server step") - c.writelinef("%d %s", smtp.C334ContinueAuth, base64.StdEncoding.EncodeToString([]byte(s1))) // ../rfc/4954:187 + c.xwritelinef("%d %s", smtp.C334ContinueAuth, base64.StdEncoding.EncodeToString([]byte(s1))) // ../rfc/4954:187 c2 := xreadContinuation() s3, err := ss.Finish(c2, xscram.SaltedPassword) if len(s3) > 0 { - c.writelinef("%d %s", smtp.C334ContinueAuth, base64.StdEncoding.EncodeToString([]byte(s3))) // ../rfc/4954:187 + c.xwritelinef("%d %s", smtp.C334ContinueAuth, base64.StdEncoding.EncodeToString([]byte(s3))) // ../rfc/4954:187 } if err != nil { - c.readline() // Should be "*" for cancellation. + c.xreadline() // Should be "*" for cancellation. if errors.Is(err, scram.ErrInvalidProof) { la.Result = store.AuthBadCredentials c.log.Info("failed authentication attempt", slog.String("username", username), slog.Any("remote", c.remoteIP)) @@ -1696,7 +1696,7 @@ func (c *conn) cmdAuth(p *parser) { c.authFailed = 0 c.setSlow(false) // ../rfc/4954:276 - c.writecodeline(smtp.C235AuthSuccess, smtp.SePol7Other0, "nice", nil) + c.xwritecodeline(smtp.C235AuthSuccess, smtp.SePol7Other0, "nice", nil) } // ../rfc/5321:1879 ../rfc/5321:1025 @@ -1709,7 +1709,7 @@ func (c *conn) cmdMail(p *parser) { // If we get many bad transactions, it's probably a spammer that is guessing user names. // Useful in combination with rate limiting. // ../rfc/5321:4349 - c.writecodeline(smtp.C550MailboxUnavail, smtp.SeAddr1Other0, "too many failures", nil) + c.xwritecodeline(smtp.C550MailboxUnavail, smtp.SeAddr1Other0, "too many failures", nil) panic(errIO) } @@ -1905,7 +1905,7 @@ func (c *conn) cmdMail(p *parser) { c.mailFrom = &rpath - c.bwritecodeline(smtp.C250Completed, smtp.SeAddr1Other0, "looking good", nil) + c.xbwritecodeline(smtp.C250Completed, smtp.SeAddr1Other0, "looking good", nil) } // ../rfc/5321:1916 ../rfc/5321:1054 @@ -2049,7 +2049,7 @@ func (c *conn) cmdRcpt(p *parser) { c.log.Errorx("looking up account for delivery", err, slog.Any("rcptto", fpath)) xsmtpServerErrorf(codes{smtp.C451LocalErr, smtp.SeSys3Other0}, "error processing") } - c.bwritecodeline(smtp.C250Completed, smtp.SeAddr1Other0, "now on the list", nil) + c.xbwritecodeline(smtp.C250Completed, smtp.SeAddr1Other0, "now on the list", nil) } func hasNonASCII(s string) bool { @@ -2109,7 +2109,7 @@ func (c *conn) cmdData(p *parser) { }() // ../rfc/5321:1994 - c.writelinef("354 see you at the bare dot") + c.xwritelinef("354 see you at the bare dot") // Mark as tracedata. defer c.xtrace(mlog.LevelTracedata)() @@ -2131,12 +2131,12 @@ func (c *conn) cmdData(p *parser) { if n < config.DefaultMaxMsgSize { ecode = smtp.SeMailbox2MsgLimitExceeded3 } - c.writecodeline(smtp.C451LocalErr, ecode, fmt.Sprintf("error copying data to file (%s)", mox.ReceivedID(c.cid)), err) + c.xwritecodeline(smtp.C451LocalErr, ecode, fmt.Sprintf("error copying data to file (%s)", mox.ReceivedID(c.cid)), err) panic(fmt.Errorf("remote sent too much DATA: %w", errIO)) } if errors.Is(err, smtp.ErrCRLF) { - c.writecodeline(smtp.C500BadSyntax, smtp.SeProto5Syntax2, fmt.Sprintf("invalid bare \\r or \\n, may be smtp smuggling (%s)", mox.ReceivedID(c.cid)), err) + c.xwritecodeline(smtp.C500BadSyntax, smtp.SeProto5Syntax2, fmt.Sprintf("invalid bare \\r or \\n, may be smtp smuggling (%s)", mox.ReceivedID(c.cid)), err) return } @@ -2146,7 +2146,7 @@ func (c *conn) cmdData(p *parser) { // available and our write blocks us from reading remaining data, leading to // deadlock. We have a timeout on our connection writes though, so worst case we'll // abort the connection due to expiration. - c.writecodeline(smtp.C451LocalErr, smtp.SeSys3Other0, fmt.Sprintf("error copying data to file (%s)", mox.ReceivedID(c.cid)), err) + c.xwritecodeline(smtp.C451LocalErr, smtp.SeSys3Other0, fmt.Sprintf("error copying data to file (%s)", mox.ReceivedID(c.cid)), err) io.Copy(io.Discard, dr) return } @@ -2560,7 +2560,7 @@ func (c *conn) submit(ctx context.Context, recvHdrFor func(string) string, msgWr c.transactionBad-- // Compensate for early earlier pessimistic increase. c.rset() - c.writecodeline(smtp.C250Completed, smtp.SeMailbox2Other0, "it is done", nil) + c.xwritecodeline(smtp.C250Completed, smtp.SeMailbox2Other0, "it is done", nil) } func xrandomID(n int) string { @@ -3689,7 +3689,7 @@ func (c *conn) deliver(ctx context.Context, recvHdrFor func(string) string, msgW c.transactionGood++ c.transactionBad-- // Compensate for early earlier pessimistic increase. c.rset() - c.writecodeline(smtp.C250Completed, smtp.SeMailbox2Other0, "it is done", nil) + c.xwritecodeline(smtp.C250Completed, smtp.SeMailbox2Other0, "it is done", nil) } // Return whether msgFrom address is allowed to send a message to alias. @@ -3735,7 +3735,7 @@ func (c *conn) cmdRset(p *parser) { p.xend() c.rset() - c.bwritecodeline(smtp.C250Completed, smtp.SeOther00, "all clear", nil) + c.xbwritecodeline(smtp.C250Completed, smtp.SeOther00, "all clear", nil) } // ../rfc/5321:2108 ../rfc/5321:1222 @@ -3781,7 +3781,7 @@ func (c *conn) cmdHelp(p *parser) { // Let's not strictly parse the request for help. We are ignoring the text anyway. // ../rfc/5321:2166 - c.bwritecodeline(smtp.C214Help, smtp.SeOther00, "see rfc 5321 (smtp)", nil) + c.xbwritecodeline(smtp.C214Help, smtp.SeOther00, "see rfc 5321 (smtp)", nil) } // ../rfc/5321:2191 @@ -3793,7 +3793,7 @@ func (c *conn) cmdNoop(p *parser) { } p.xend() - c.bwritecodeline(smtp.C250Completed, smtp.SeOther00, "alrighty", nil) + c.xbwritecodeline(smtp.C250Completed, smtp.SeOther00, "alrighty", nil) } // ../rfc/5321:2205 @@ -3801,6 +3801,6 @@ func (c *conn) cmdQuit(p *parser) { // ../rfc/5321:2226 p.xend() - c.writecodeline(smtp.C221Closing, smtp.SeOther00, "okay thanks bye", nil) + c.xwritecodeline(smtp.C221Closing, smtp.SeOther00, "okay thanks bye", nil) panic(cleanClose) }