1// Package smtpserver implements an SMTP server for submission and incoming delivery of mail messages.
28 "golang.org/x/exp/maps"
30 "github.com/prometheus/client_golang/prometheus"
31 "github.com/prometheus/client_golang/prometheus/promauto"
33 "github.com/mjl-/bstore"
35 "github.com/mjl-/mox/config"
36 "github.com/mjl-/mox/dkim"
37 "github.com/mjl-/mox/dmarc"
38 "github.com/mjl-/mox/dmarcdb"
39 "github.com/mjl-/mox/dns"
40 "github.com/mjl-/mox/dsn"
41 "github.com/mjl-/mox/iprev"
42 "github.com/mjl-/mox/message"
43 "github.com/mjl-/mox/metrics"
44 "github.com/mjl-/mox/mlog"
45 "github.com/mjl-/mox/mox-"
46 "github.com/mjl-/mox/moxio"
47 "github.com/mjl-/mox/moxvar"
48 "github.com/mjl-/mox/publicsuffix"
49 "github.com/mjl-/mox/queue"
50 "github.com/mjl-/mox/ratelimit"
51 "github.com/mjl-/mox/scram"
52 "github.com/mjl-/mox/smtp"
53 "github.com/mjl-/mox/spf"
54 "github.com/mjl-/mox/store"
55 "github.com/mjl-/mox/tlsrptdb"
58// Most logging should be done through conn.log* functions.
59// Only use log in contexts without connection.
60var xlog = mlog.New("smtpserver")
62// We use panic and recover for error handling while executing commands.
63// These errors signal the connection must be closed.
64var errIO = errors.New("fatal io error")
66// If set, regular delivery/submit is sidestepped, email is accepted and
67// delivered to the account named mox.
70var limiterConnectionRate, limiterConnections *ratelimit.Limiter
72// For delivery rate limiting. Variable because changed during tests.
73var limitIPMasked1MessagesPerMinute int = 500
74var limitIPMasked1SizePerMinute int64 = 1000 * 1024 * 1024
77 // Also called by tests, so they don't trigger the rate limiter.
83 // todo future: make these configurable
84 limiterConnectionRate = &ratelimit.Limiter{
85 WindowLimits: []ratelimit.WindowLimit{
88 Limits: [...]int64{300, 900, 2700},
92 limiterConnections = &ratelimit.Limiter{
93 WindowLimits: []ratelimit.WindowLimit{
95 Window: time.Duration(math.MaxInt64), // All of time.
96 Limits: [...]int64{30, 90, 270},
103 // Delays for bad/suspicious behaviour. Zero during tests.
104 badClientDelay = time.Second // Before reads and after 1-byte writes for probably spammers.
105 authFailDelay = time.Second // Response to authentication failure.
106 unknownRecipientsDelay = 5 * time.Second // Response when all recipients are unknown.
107 firstTimeSenderDelayDefault = 15 * time.Second // Before accepting message from first-time sender.
112 secode string // Enhanced code, but without the leading major int from code.
116 metricConnection = promauto.NewCounterVec(
117 prometheus.CounterOpts{
118 Name: "mox_smtpserver_connection_total",
119 Help: "Incoming SMTP connections.",
122 "kind", // "deliver" or "submit"
125 metricCommands = promauto.NewHistogramVec(
126 prometheus.HistogramOpts{
127 Name: "mox_smtpserver_command_duration_seconds",
128 Help: "SMTP server command duration and result codes in seconds.",
129 Buckets: []float64{0.001, 0.005, 0.01, 0.05, 0.100, 0.5, 1, 5, 10, 20, 30, 60, 120},
132 "kind", // "deliver" or "submit"
138 metricDelivery = promauto.NewCounterVec(
139 prometheus.CounterOpts{
140 Name: "mox_smtpserver_delivery_total",
141 Help: "SMTP incoming message delivery from external source, not submission. Result values: delivered, reject, unknownuser, accounterror, delivererror. Reason indicates why a message was rejected/accepted.",
148 // Similar between ../webmail/webmail.go:/metricSubmission and ../smtpserver/server.go:/metricSubmission
149 metricSubmission = promauto.NewCounterVec(
150 prometheus.CounterOpts{
151 Name: "mox_smtpserver_submission_total",
152 Help: "SMTP server incoming submission results, known values (those ending with error are server errors): ok, badmessage, badfrom, badheader, messagelimiterror, recipientlimiterror, localserveerror, queueerror.",
158 metricServerErrors = promauto.NewCounterVec(
159 prometheus.CounterOpts{
160 Name: "mox_smtpserver_errors_total",
161 Help: "SMTP server errors, known values: dkimsign, queuedsn.",
169var jitterRand = mox.NewRand()
171func durationDefault(delay *time.Duration, def time.Duration) time.Duration {
178// Listen initializes network listeners for incoming SMTP connection.
179// The listeners are stored for a later call to Serve.
181 names := maps.Keys(mox.Conf.Static.Listeners)
183 for _, name := range names {
184 listener := mox.Conf.Static.Listeners[name]
186 var tlsConfig *tls.Config
187 if listener.TLS != nil {
188 tlsConfig = listener.TLS.Config
191 maxMsgSize := listener.SMTPMaxMessageSize
193 maxMsgSize = config.DefaultMaxMsgSize
196 if listener.SMTP.Enabled {
197 hostname := mox.Conf.Static.HostnameDomain
198 if listener.Hostname != "" {
199 hostname = listener.HostnameDomain
201 port := config.Port(listener.SMTP.Port, 25)
202 for _, ip := range listener.IPs {
203 firstTimeSenderDelay := durationDefault(listener.SMTP.FirstTimeSenderDelay, firstTimeSenderDelayDefault)
204 listen1("smtp", name, ip, port, hostname, tlsConfig, false, false, maxMsgSize, false, listener.SMTP.RequireSTARTTLS, listener.SMTP.DNSBLZones, firstTimeSenderDelay)
207 if listener.Submission.Enabled {
208 hostname := mox.Conf.Static.HostnameDomain
209 if listener.Hostname != "" {
210 hostname = listener.HostnameDomain
212 port := config.Port(listener.Submission.Port, 587)
213 for _, ip := range listener.IPs {
214 listen1("submission", name, ip, port, hostname, tlsConfig, true, false, maxMsgSize, !listener.Submission.NoRequireSTARTTLS, !listener.Submission.NoRequireSTARTTLS, nil, 0)
218 if listener.Submissions.Enabled {
219 hostname := mox.Conf.Static.HostnameDomain
220 if listener.Hostname != "" {
221 hostname = listener.HostnameDomain
223 port := config.Port(listener.Submissions.Port, 465)
224 for _, ip := range listener.IPs {
225 listen1("submissions", name, ip, port, hostname, tlsConfig, true, true, maxMsgSize, true, true, nil, 0)
233func listen1(protocol, name, ip string, port int, hostname dns.Domain, tlsConfig *tls.Config, submission, xtls bool, maxMessageSize int64, requireTLSForAuth, requireTLSForDelivery bool, dnsBLs []dns.Domain, firstTimeSenderDelay time.Duration) {
234 addr := net.JoinHostPort(ip, fmt.Sprintf("%d", port))
235 if os.Getuid() == 0 {
236 xlog.Print("listening for smtp", mlog.Field("listener", name), mlog.Field("address", addr), mlog.Field("protocol", protocol))
238 network := mox.Network(ip)
239 ln, err := mox.Listen(network, addr)
241 xlog.Fatalx("smtp: listen for smtp", err, mlog.Field("protocol", protocol), mlog.Field("listener", name))
244 ln = tls.NewListener(ln, tlsConfig)
249 conn, err := ln.Accept()
251 xlog.Infox("smtp: accept", err, mlog.Field("protocol", protocol), mlog.Field("listener", name))
254 resolver := dns.StrictResolver{} // By leaving Pkg empty, it'll be set by each package that uses the resolver, e.g. spf/dkim/dmarc.
255 go serve(name, mox.Cid(), hostname, tlsConfig, conn, resolver, submission, xtls, maxMessageSize, requireTLSForAuth, requireTLSForDelivery, dnsBLs, firstTimeSenderDelay)
259 servers = append(servers, serve)
262// Serve starts serving on all listeners, launching a goroutine per listener.
264 for _, serve := range servers {
272 // OrigConn is the original (TCP) connection. We'll read from/write to conn, which
273 // can be wrapped in a tls.Server. We close origConn instead of conn because
274 // closing the TLS connection would send a TLS close notification, which may block
275 // for 5s if the server isn't reading it (because it is also sending it).
280 resolver dns.Resolver
283 tr *moxio.TraceReader // Kept for changing trace level during cmd/auth/data.
284 tw *moxio.TraceWriter
285 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.
286 lastlog time.Time // Used for printing the delta time since the previous logging for this connection.
288 tlsConfig *tls.Config
294 requireTLSForAuth bool
295 requireTLSForDelivery bool
296 cmd string // Current command.
297 cmdStart time.Time // Start of current command.
298 ncmds int // Number of commands processed. Used to abort connection when first incoming command is unknown/invalid.
300 firstTimeSenderDelay time.Duration
302 // If non-zero, taken into account during Read and Write. Set while processing DATA
303 // command, we don't want the entire delivery to take too long.
306 hello dns.IPDomain // Claimed remote name. Can be ip address for ehlo.
307 ehlo bool // If set, we had EHLO instead of HELO.
309 authFailed int // Number of failed auth attempts. For slowing down remote with many failures.
310 username string // Only when authenticated.
311 account *store.Account // Only when authenticated.
313 // We track good/bad message transactions to disconnect spammers trying to guess addresses.
317 // Message transaction.
319 has8bitmime bool // If MAIL FROM parameter BODY=8BITMIME was sent. Required for SMTPUTF8.
320 smtputf8 bool // todo future: we should keep track of this per recipient. perhaps only a specific recipient requires smtputf8, e.g. due to a utf8 localpart. we should decide ourselves if the message needs smtputf8, e.g. due to utf8 header values.
321 recipients []rcptAccount
324type rcptAccount struct {
326 local bool // Whether recipient is a local user.
328 // Only valid for local delivery.
330 destination config.Destination
331 canonicalAddress string // Optional catchall part stripped and/or lowercased.
334func isClosed(err error) bool {
335 return errors.Is(err, errIO) || moxio.IsClosed(err)
338// completely reset connection state as if greeting has just been sent.
340func (c *conn) reset() {
342 c.hello = dns.IPDomain{}
344 if c.account != nil {
345 err := c.account.Close()
346 c.log.Check(err, "closing account")
352// for rset command, and a few more cases that reset the mail transaction state.
354func (c *conn) rset() {
356 c.has8bitmime = false
361func (c *conn) earliestDeadline(d time.Duration) time.Time {
362 e := time.Now().Add(d)
363 if !c.deadline.IsZero() && c.deadline.Before(e) {
369func (c *conn) xcheckAuth() {
370 if c.submission && c.account == nil {
372 xsmtpUserErrorf(smtp.C530SecurityRequired, smtp.SePol7Other0, "authentication required")
376func (c *conn) xtrace(level mlog.Level) func() {
382 c.tr.SetTrace(mlog.LevelTrace)
383 c.tw.SetTrace(mlog.LevelTrace)
387// setSlow marks the connection slow (or now), so reads are done with 3 second
388// delay for each read, and writes are done at 1 byte per second, to try to slow
390func (c *conn) setSlow(on bool) {
392 c.log.Debug("connection changed to slow")
393 } else if !on && c.slow {
394 c.log.Debug("connection restored to regular pace")
399// Write writes to the connection. It panics on i/o errors, which is handled by the
400// connection command loop.
401func (c *conn) Write(buf []byte) (int, error) {
409 // We set a single deadline for Write and Read. This may be a TLS connection.
410 // SetDeadline works on the underlying connection. If we wouldn't touch the read
411 // deadline, and only set the write deadline and do a bunch of writes, the TLS
412 // library would still have to do reads on the underlying connection, and may reach
413 // a read deadline that was set for some earlier read.
414 if err := c.conn.SetDeadline(c.earliestDeadline(30 * time.Second)); err != nil {
415 c.log.Errorx("setting deadline for write", err)
418 nn, err := c.conn.Write(buf[:chunk])
420 panic(fmt.Errorf("write: %s (%w)", err, errIO))
424 if len(buf) > 0 && badClientDelay > 0 {
425 mox.Sleep(mox.Context, badClientDelay)
431// Read reads from the connection. It panics on i/o errors, which is handled by the
432// connection command loop.
433func (c *conn) Read(buf []byte) (int, error) {
434 if c.slow && badClientDelay > 0 {
435 mox.Sleep(mox.Context, badClientDelay)
439 // See comment about Deadline instead of individual read/write deadlines at Write.
440 if err := c.conn.SetDeadline(c.earliestDeadline(30 * time.Second)); err != nil {
441 c.log.Errorx("setting deadline for read", err)
444 n, err := c.conn.Read(buf)
446 panic(fmt.Errorf("read: %s (%w)", err, errIO))
451// Cache of line buffers for reading commands.
453var bufpool = moxio.NewBufpool(8, 2*1024)
455func (c *conn) readline() string {
456 line, err := bufpool.Readline(c.r)
457 if err != nil && errors.Is(err, moxio.ErrLineTooLong) {
458 c.writecodeline(smtp.C500BadSyntax, smtp.SeProto5Other0, "line too long, smtp max is 512, we reached 2048", nil)
459 panic(fmt.Errorf("%s (%w)", err, errIO))
460 } else if err != nil {
461 panic(fmt.Errorf("%s (%w)", err, errIO))
466// Buffered-write command response line to connection with codes and msg.
467// Err is not sent to remote but is used for logging and can be empty.
468func (c *conn) bwritecodeline(code int, secode string, msg string, err error) {
471 ecode = fmt.Sprintf("%d.%s", code/100, secode)
473 metricCommands.WithLabelValues(c.kind(), c.cmd, fmt.Sprintf("%d", code), ecode).Observe(float64(time.Since(c.cmdStart)) / float64(time.Second))
474 c.log.Debugx("smtp command result", err, mlog.Field("kind", c.kind()), mlog.Field("cmd", c.cmd), mlog.Field("code", fmt.Sprintf("%d", code)), mlog.Field("ecode", ecode), mlog.Field("duration", time.Since(c.cmdStart)))
481 // Separate by newline and wrap long lines.
482 lines := strings.Split(msg, "\n")
483 for i, line := range lines {
485 var prelen = 3 + 1 + len(ecode) + len(sep)
486 for prelen+len(line) > 510 {
488 for ; e > 400 && line[e] != ' '; e-- {
490 // 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.
491 c.bwritelinef("%d-%s%s%s", code, ecode, sep, line[:e])
495 if i < len(lines)-1 {
498 c.bwritelinef("%d%s%s%s%s", code, spdash, ecode, sep, line)
502// Buffered-write a formatted response line to connection.
503func (c *conn) bwritelinef(format string, args ...any) {
504 msg := fmt.Sprintf(format, args...)
505 fmt.Fprint(c.w, msg+"\r\n")
508// Flush pending buffered writes to connection.
509func (c *conn) xflush() {
510 c.w.Flush() // Errors will have caused a panic in Write.
513// Write (with flush) a response line with codes and message. err is not written, used for logging and can be nil.
514func (c *conn) writecodeline(code int, secode string, msg string, err error) {
515 c.bwritecodeline(code, secode, msg, err)
519// Write (with flush) a formatted response line to connection.
520func (c *conn) writelinef(format string, args ...any) {
521 c.bwritelinef(format, args...)
525var cleanClose struct{} // Sentinel value for panic/recover indicating clean close of connection.
527func serve(listenerName string, cid int64, hostname dns.Domain, tlsConfig *tls.Config, nc net.Conn, resolver dns.Resolver, submission, tls bool, maxMessageSize int64, requireTLSForAuth, requireTLSForDelivery bool, dnsBLs []dns.Domain, firstTimeSenderDelay time.Duration) {
528 var localIP, remoteIP net.IP
529 if a, ok := nc.LocalAddr().(*net.TCPAddr); ok {
532 // For net.Pipe, during tests.
533 localIP = net.ParseIP("127.0.0.10")
535 if a, ok := nc.RemoteAddr().(*net.TCPAddr); ok {
538 // For net.Pipe, during tests.
539 remoteIP = net.ParseIP("127.0.0.10")
546 submission: submission,
550 tlsConfig: tlsConfig,
554 maxMessageSize: maxMessageSize,
555 requireTLSForAuth: requireTLSForAuth,
556 requireTLSForDelivery: requireTLSForDelivery,
558 firstTimeSenderDelay: firstTimeSenderDelay,
560 c.log = xlog.MoreFields(func() []mlog.Pair {
563 mlog.Field("cid", c.cid),
564 mlog.Field("delta", now.Sub(c.lastlog)),
567 if c.username != "" {
568 l = append(l, mlog.Field("username", c.username))
572 c.tr = moxio.NewTraceReader(c.log, "RC: ", c)
573 c.tw = moxio.NewTraceWriter(c.log, "LS: ", c)
574 c.r = bufio.NewReader(c.tr)
575 c.w = bufio.NewWriter(c.tw)
577 metricConnection.WithLabelValues(c.kind()).Inc()
578 c.log.Info("new connection", mlog.Field("remote", c.conn.RemoteAddr()), mlog.Field("local", c.conn.LocalAddr()), mlog.Field("submission", submission), mlog.Field("tls", tls), mlog.Field("listener", listenerName))
581 c.origConn.Close() // Close actual TCP socket, regardless of TLS on top.
582 c.conn.Close() // If TLS, will try to write alert notification to already closed socket, returning error quickly.
584 if c.account != nil {
585 err := c.account.Close()
586 c.log.Check(err, "closing account")
591 if x == nil || x == cleanClose {
592 c.log.Info("connection closed")
593 } else if err, ok := x.(error); ok && isClosed(err) {
594 c.log.Infox("connection closed", err)
596 c.log.Error("unhandled panic", mlog.Field("err", x))
598 metrics.PanicInc(metrics.Smtpserver)
603 case <-mox.Shutdown.Done():
605 c.writecodeline(smtp.C421ServiceUnavail, smtp.SeSys3NotAccepting2, "shutting down", nil)
610 if !limiterConnectionRate.Add(c.remoteIP, time.Now(), 1) {
611 c.writecodeline(smtp.C421ServiceUnavail, smtp.SePol7Other0, "connection rate from your ip or network too high, slow down please", nil)
615 // If remote IP/network resulted in too many authentication failures, refuse to serve.
616 if submission && !mox.LimiterFailedAuth.CanAdd(c.remoteIP, time.Now(), 1) {
617 metrics.AuthenticationRatelimitedInc("submission")
618 c.log.Debug("refusing connection due to many auth failures", mlog.Field("remoteip", c.remoteIP))
619 c.writecodeline(smtp.C421ServiceUnavail, smtp.SePol7Other0, "too many auth failures", nil)
623 if !limiterConnections.Add(c.remoteIP, time.Now(), 1) {
624 c.log.Debug("refusing connection due to many open connections", mlog.Field("remoteip", c.remoteIP))
625 c.writecodeline(smtp.C421ServiceUnavail, smtp.SePol7Other0, "too many open connections from your ip or network", nil)
628 defer limiterConnections.Add(c.remoteIP, time.Now(), -1)
630 // We register and unregister the original connection, in case c.conn is replaced
631 // with a TLS connection later on.
632 mox.Connections.Register(nc, "smtp", listenerName)
633 defer mox.Connections.Unregister(nc)
637 // We include the string ESMTP. https://cr.yp.to/smtp/greeting.html recommends it.
638 // Should not be too relevant nowadays, but does not hurt and default blackbox
639 // exporter SMTP health check expects it.
640 c.writelinef("%d %s ESMTP mox %s", smtp.C220ServiceReady, c.hostname.ASCII, moxvar.Version)
645 // If another command is present, don't flush our buffered response yet. Holding
646 // off will cause us to respond with a single packet.
649 buf, err := c.r.Peek(n)
650 if err == nil && bytes.IndexByte(buf, '\n') >= 0 {
658var commands = map[string]func(c *conn, p *parser){
659 "helo": (*conn).cmdHelo,
660 "ehlo": (*conn).cmdEhlo,
661 "starttls": (*conn).cmdStarttls,
662 "auth": (*conn).cmdAuth,
663 "mail": (*conn).cmdMail,
664 "rcpt": (*conn).cmdRcpt,
665 "data": (*conn).cmdData,
666 "rset": (*conn).cmdRset,
667 "vrfy": (*conn).cmdVrfy,
668 "expn": (*conn).cmdExpn,
669 "help": (*conn).cmdHelp,
670 "noop": (*conn).cmdNoop,
671 "quit": (*conn).cmdQuit,
674func command(c *conn) {
690 if errors.As(err, &serr) {
691 c.writecodeline(serr.code, serr.secode, fmt.Sprintf("%s (%s)", serr.errmsg, mox.ReceivedID(c.cid)), serr.err)
696 // Other type of panic, we pass it on, aborting the connection.
697 c.log.Errorx("command panic", err)
702 // todo future: we could wait for either a line or shutdown, and just close the connection on shutdown.
705 t := strings.SplitN(line, " ", 2)
711 cmdl := strings.ToLower(cmd)
713 // todo future: should we return an error for lines that are too long? perhaps for submission or in a pedantic mode. we would have to take extensions for MAIL into account.
../rfc/5321:3500 ../rfc/5321:3552
716 case <-mox.Shutdown.Done():
718 c.writecodeline(smtp.C421ServiceUnavail, smtp.SeSys3NotAccepting2, "shutting down", nil)
724 c.cmdStart = time.Now()
726 p := newParser(args, c.smtputf8, c)
727 fn, ok := commands[cmdl]
731 // Other side is likely speaking something else than SMTP, send error message and
732 // stop processing because there is a good chance whatever they sent has multiple
734 c.writecodeline(smtp.C500BadSyntax, smtp.SeProto5Syntax2, "please try again speaking smtp", nil)
738 xsmtpUserErrorf(smtp.C500BadSyntax, smtp.SeProto5BadCmdOrSeq1, "unknown command")
744// For use in metric labels.
745func (c *conn) kind() string {
752func (c *conn) xneedHello() {
753 if c.hello.IsZero() {
754 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "no ehlo/helo yet")
758// If smtp server is configured to require TLS for all mail delivery, abort command.
759func (c *conn) xneedTLSForDelivery() {
760 if c.requireTLSForDelivery && !c.tls {
762 xsmtpUserErrorf(smtp.C530SecurityRequired, smtp.SePol7Other0, "STARTTLS required for mail delivery")
766func (c *conn) cmdHelo(p *parser) {
770func (c *conn) cmdEhlo(p *parser) {
775func (c *conn) cmdHello(p *parser, ehlo bool) {
776 var remote dns.IPDomain
777 if c.submission && !moxvar.Pedantic {
778 // Mail clients regularly put bogus information in the hostname/ip. For submission,
779 // the value is of no use, so there is not much point in annoying the user with
780 // errors they cannot fix themselves. Except when in pedantic mode.
781 remote = dns.IPDomain{IP: c.remoteIP}
785 remote = p.xipdomain(true)
787 remote = dns.IPDomain{Domain: p.xdomain()}
789 // Verify a remote domain name has an A or AAAA record, CNAME not allowed.
../rfc/5321:722
790 cidctx := context.WithValue(mox.Context, mlog.CidKey, c.cid)
791 ctx, cancel := context.WithTimeout(cidctx, time.Minute)
792 _, err := c.resolver.LookupIPAddr(ctx, remote.Domain.ASCII+".")
794 if dns.IsNotFound(err) {
795 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeProto5Other0, "your ehlo domain does not resolve to an IP address")
797 // For success or temporary resolve errors, we'll just continue.
800 // Though a few paragraphs earlier is a claim additional data can occur for address
801 // literals (IP addresses), although the ABNF in that document does not allow it.
802 // We allow additional text, but only if space-separated.
803 if len(remote.IP) > 0 && p.space() {
815 // https://www.iana.org/assignments/mail-parameters/mail-parameters.xhtml
817 c.bwritelinef("250-%s", c.hostname.ASCII)
821 if !c.tls && c.tlsConfig != nil {
823 c.bwritelinef("250-STARTTLS")
827 if c.tls || !c.requireTLSForAuth {
828 c.bwritelinef("250-AUTH SCRAM-SHA-256 SCRAM-SHA-1 CRAM-MD5 PLAIN")
830 c.bwritelinef("250-AUTH ")
834 // todo future? c.writelinef("250-DSN")
841func (c *conn) cmdStarttls(p *parser) {
847 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "already speaking tls")
849 if c.account != nil {
850 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "cannot starttls after authentication")
853 // We don't want to do TLS on top of c.r because it also prints protocol traces: We
854 // don't want to log the TLS stream. So we'll do TLS on the underlying connection,
855 // but make sure any bytes already read and in the buffer are used for the TLS
858 if n := c.r.Buffered(); n > 0 {
859 conn = &moxio.PrefixConn{
860 PrefixReader: io.LimitReader(c.r, int64(n)),
865 c.writecodeline(smtp.C220ServiceReady, smtp.SeOther00, "go!", nil)
866 tlsConn := tls.Server(conn, c.tlsConfig)
867 cidctx := context.WithValue(mox.Context, mlog.CidKey, c.cid)
868 ctx, cancel := context.WithTimeout(cidctx, time.Minute)
870 c.log.Debug("starting tls server handshake")
871 if err := tlsConn.HandshakeContext(ctx); err != nil {
872 panic(fmt.Errorf("starttls handshake: %s (%w)", err, errIO))
875 tlsversion, ciphersuite := mox.TLSInfo(tlsConn)
876 c.log.Debug("tls server handshake done", mlog.Field("tls", tlsversion), mlog.Field("ciphersuite", ciphersuite))
878 c.tr = moxio.NewTraceReader(c.log, "RC: ", c)
879 c.tw = moxio.NewTraceWriter(c.log, "LS: ", c)
880 c.r = bufio.NewReader(c.tr)
881 c.w = bufio.NewWriter(c.tw)
888func (c *conn) cmdAuth(p *parser) {
892 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "authentication only allowed on submission ports")
894 if c.account != nil {
896 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "already authenticated")
898 if c.mailFrom != nil {
900 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "authentication not allowed during mail transaction")
903 // todo future: we may want to normalize usernames and passwords, see stringprep in
../rfc/4013:38 and possibly newer mechanisms (though they are opt-in and that may not have happened yet).
905 // For many failed auth attempts, slow down verification attempts.
906 // Dropping the connection could also work, but more so when we have a connection rate limiter.
908 if c.authFailed > 3 && authFailDelay > 0 {
910 mox.Sleep(mox.Context, time.Duration(c.authFailed-3)*authFailDelay)
912 c.authFailed++ // Compensated on success.
914 // On the 3rd failed authentication, start responding slowly. Successful auth will
915 // cause fast responses again.
916 if c.authFailed >= 3 {
921 var authVariant string
922 authResult := "error"
924 metrics.AuthenticationInc("submission", authVariant, authResult)
927 mox.LimiterFailedAuth.Reset(c.remoteIP, time.Now())
929 mox.LimiterFailedAuth.Add(c.remoteIP, time.Now(), 1)
933 // todo: implement "AUTH LOGIN"? it looks like PLAIN, but without the continuation. it is an obsolete sasl mechanism. an account in desktop outlook appears to go through the cloud, attempting to submit email only with unadvertised and AUTH LOGIN. it appears they don't know "plain".
937 mech := p.xsaslMech()
939 xreadInitial := func() []byte {
943 // todo future: handle max length of 12288 octets and return proper responde codes otherwise
../rfc/4954:253
947 authResult = "aborted"
948 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5Other0, "authentication aborted")
955 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5Syntax2, "missing initial auth base64 parameter after space")
956 } else if auth == "=" {
958 auth = "" // Base64 decode below will result in empty buffer.
961 buf, err := base64.StdEncoding.DecodeString(auth)
964 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5Syntax2, "invalid base64: %s", err)
969 xreadContinuation := func() []byte {
972 authResult = "aborted"
973 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5Other0, "authentication aborted")
975 buf, err := base64.StdEncoding.DecodeString(line)
978 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5Syntax2, "invalid base64: %s", err)
985 authVariant = "plain"
989 if !c.tls && c.requireTLSForAuth {
990 xsmtpUserErrorf(smtp.C538EncReqForAuth, smtp.SePol7EncReqForAuth11, "authentication requires tls")
993 // Password is in line in plain text, so hide it.
994 defer c.xtrace(mlog.LevelTraceauth)()
995 buf := xreadInitial()
996 c.xtrace(mlog.LevelTrace) // Restore.
997 plain := bytes.Split(buf, []byte{0})
999 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5BadParams4, "auth data should have 3 nul-separated tokens, got %d", len(plain))
1001 authz := string(plain[0])
1002 authc := string(plain[1])
1003 password := string(plain[2])
1005 if authz != "" && authz != authc {
1006 authResult = "badcreds"
1007 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "cannot assume other role")
1010 acc, err := store.OpenEmailAuth(authc, password)
1011 if err != nil && errors.Is(err, store.ErrUnknownCredentials) {
1013 authResult = "badcreds"
1014 c.log.Info("failed authentication attempt", mlog.Field("username", authc), mlog.Field("remote", c.remoteIP))
1015 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass")
1017 xcheckf(err, "verifying credentials")
1025 c.writecodeline(smtp.C235AuthSuccess, smtp.SePol7Other0, "nice", nil)
1028 authVariant = strings.ToLower(mech)
1033 chal := fmt.Sprintf("<%d.%d@%s>", uint64(mox.CryptoRandInt()), time.Now().UnixNano(), mox.Conf.Static.HostnameDomain.ASCII)
1034 c.writelinef("%d %s", smtp.C334ContinueAuth, base64.StdEncoding.EncodeToString([]byte(chal)))
1036 resp := xreadContinuation()
1037 t := strings.Split(string(resp), " ")
1038 if len(t) != 2 || len(t[1]) != 2*md5.Size {
1039 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5BadParams4, "malformed cram-md5 response")
1042 c.log.Debug("cram-md5 auth", mlog.Field("address", addr))
1043 acc, _, err := store.OpenEmail(addr)
1045 if errors.Is(err, store.ErrUnknownCredentials) {
1046 c.log.Info("failed authentication attempt", mlog.Field("username", addr), mlog.Field("remote", c.remoteIP))
1047 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass")
1050 xcheckf(err, "looking up address")
1054 c.log.Check(err, "closing account")
1057 var ipadhash, opadhash hash.Hash
1058 acc.WithRLock(func() {
1059 err := acc.DB.Read(context.TODO(), func(tx *bstore.Tx) error {
1060 password, err := bstore.QueryTx[store.Password](tx).Get()
1061 if err == bstore.ErrAbsent {
1062 c.log.Info("failed authentication attempt", mlog.Field("username", addr), mlog.Field("remote", c.remoteIP))
1063 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass")
1069 ipadhash = password.CRAMMD5.Ipad
1070 opadhash = password.CRAMMD5.Opad
1073 xcheckf(err, "tx read")
1075 if ipadhash == nil || opadhash == nil {
1076 c.log.Info("cram-md5 auth attempt without derived secrets set, save password again to store secrets", mlog.Field("username", addr))
1077 c.log.Info("failed authentication attempt", mlog.Field("username", addr), mlog.Field("remote", c.remoteIP))
1078 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass")
1082 ipadhash.Write([]byte(chal))
1083 opadhash.Write(ipadhash.Sum(nil))
1084 digest := fmt.Sprintf("%x", opadhash.Sum(nil))
1086 c.log.Info("failed authentication attempt", mlog.Field("username", addr), mlog.Field("remote", c.remoteIP))
1087 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass")
1094 acc = nil // Cancel cleanup.
1097 c.writecodeline(smtp.C235AuthSuccess, smtp.SePol7Other0, "nice", nil)
1099 case "SCRAM-SHA-1", "SCRAM-SHA-256":
1100 // 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?
1101 // todo: use single implementation between ../imapserver/server.go and ../smtpserver/server.go
1103 authVariant = strings.ToLower(mech)
1104 var h func() hash.Hash
1105 if authVariant == "scram-sha-1" {
1111 // Passwords cannot be retrieved or replayed from the trace.
1113 c0 := xreadInitial()
1114 ss, err := scram.NewServer(h, c0)
1115 xcheckf(err, "starting scram")
1116 c.log.Debug("scram auth", mlog.Field("authentication", ss.Authentication))
1117 acc, _, err := store.OpenEmail(ss.Authentication)
1119 // todo: we could continue scram with a generated salt, deterministically generated
1120 // from the username. that way we don't have to store anything but attackers cannot
1121 // learn if an account exists. same for absent scram saltedpassword below.
1122 c.log.Info("failed authentication attempt", mlog.Field("username", ss.Authentication), mlog.Field("remote", c.remoteIP))
1123 xsmtpUserErrorf(smtp.C454TempAuthFail, smtp.SeSys3Other0, "scram not possible")
1128 c.log.Check(err, "closing account")
1131 if ss.Authorization != "" && ss.Authorization != ss.Authentication {
1132 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "authentication with authorization for different user not supported")
1134 var xscram store.SCRAM
1135 acc.WithRLock(func() {
1136 err := acc.DB.Read(context.TODO(), func(tx *bstore.Tx) error {
1137 password, err := bstore.QueryTx[store.Password](tx).Get()
1138 if authVariant == "scram-sha-1" {
1139 xscram = password.SCRAMSHA1
1141 xscram = password.SCRAMSHA256
1143 if err == bstore.ErrAbsent || err == nil && (len(xscram.Salt) == 0 || xscram.Iterations == 0 || len(xscram.SaltedPassword) == 0) {
1144 c.log.Info("scram auth attempt without derived secrets set, save password again to store secrets", mlog.Field("address", ss.Authentication))
1145 c.log.Info("failed authentication attempt", mlog.Field("username", ss.Authentication), mlog.Field("remote", c.remoteIP))
1146 xsmtpUserErrorf(smtp.C454TempAuthFail, smtp.SeSys3Other0, "scram not possible")
1148 xcheckf(err, "fetching credentials")
1151 xcheckf(err, "read tx")
1153 s1, err := ss.ServerFirst(xscram.Iterations, xscram.Salt)
1154 xcheckf(err, "scram first server step")
1155 c.writelinef("%d %s", smtp.C334ContinueAuth, base64.StdEncoding.EncodeToString([]byte(s1))) //
../rfc/4954:187
1156 c2 := xreadContinuation()
1157 s3, err := ss.Finish(c2, xscram.SaltedPassword)
1159 c.writelinef("%d %s", smtp.C334ContinueAuth, base64.StdEncoding.EncodeToString([]byte(s3))) //
../rfc/4954:187
1162 c.readline() // Should be "*" for cancellation.
1163 if errors.Is(err, scram.ErrInvalidProof) {
1164 authResult = "badcreds"
1165 c.log.Info("failed authentication attempt", mlog.Field("username", ss.Authentication), mlog.Field("remote", c.remoteIP))
1166 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad credentials")
1168 xcheckf(err, "server final")
1172 // The message should be empty. todo: should we require it is empty?
1179 acc = nil // Cancel cleanup.
1180 c.username = ss.Authentication
1182 c.writecodeline(smtp.C235AuthSuccess, smtp.SePol7Other0, "nice", nil)
1186 xsmtpUserErrorf(smtp.C504ParamNotImpl, smtp.SeProto5BadParams4, "mechanism %s not supported", mech)
1191func (c *conn) cmdMail(p *parser) {
1192 // requirements for maximum line length:
1194 // todo future: enforce?
1196 if c.transactionBad > 10 && c.transactionGood == 0 {
1197 // If we get many bad transactions, it's probably a spammer that is guessing user names.
1198 // Useful in combination with rate limiting.
1200 c.writecodeline(smtp.C550MailboxUnavail, smtp.SeAddr1Other0, "too many failures", nil)
1206 c.xneedTLSForDelivery()
1207 if c.mailFrom != nil {
1209 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "already have MAIL")
1211 // Ensure clear transaction state on failure.
1222 // Allow illegal space for submission only, not for regular SMTP. Microsoft Outlook
1223 // 365 Apps for Enterprise sends it.
1224 if c.submission && !moxvar.Pedantic {
1227 rawRevPath := p.xrawReversePath()
1228 paramSeen := map[string]bool{}
1231 key := p.xparamKeyword()
1233 K := strings.ToUpper(key)
1236 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5BadParams4, "duplicate param %q", key)
1244 if size > c.maxMessageSize {
1246 ecode := smtp.SeSys3MsgLimitExceeded4
1247 if size < config.DefaultMaxMsgSize {
1248 ecode = smtp.SeMailbox2MsgLimitExceeded3
1250 xsmtpUserErrorf(smtp.C552MailboxFull, ecode, "message too large")
1252 // We won't verify the message is exactly the size the remote claims. Buf if it is
1253 // larger, we'll abort the transaction when remote crosses the boundary.
1257 v := p.xparamValue()
1258 switch strings.ToUpper(v) {
1260 c.has8bitmime = false
1262 c.has8bitmime = true
1264 xsmtpUserErrorf(smtp.C555UnrecognizedAddrParams, smtp.SeProto5BadParams4, "unrecognized parameter %q", key)
1269 // We act as if we don't trust the client to specify a mailbox. Instead, we always
1270 // check the rfc5321.mailfrom and rfc5322.from before accepting the submission.
1274 // todo future: should we accept utf-8-addr-xtext if there is no smtputf8, and utf-8 if there is? need to find a spec
../rfc/6533:259
1284 xsmtpUserErrorf(smtp.C555UnrecognizedAddrParams, smtp.SeSys3NotSupported3, "unrecognized parameter %q", key)
1288 // We now know if we have to parse the address with support for utf8.
1289 pp := newParser(rawRevPath, c.smtputf8, c)
1290 rpath := pp.xbareReversePath()
1295 // For submission, check if reverse path is allowed. I.e. authenticated account
1296 // must have the rpath configured. We do a check again on rfc5322.from during DATA.
1297 rpathAllowed := func() bool {
1302 accName, _, _, err := mox.FindAccount(rpath.Localpart, rpath.IPDomain.Domain, false)
1303 return err == nil && accName == c.account.Name
1306 if !c.submission && !rpath.IPDomain.Domain.IsZero() {
1307 // If rpath domain has null MX record or is otherwise not accepting email, reject.
1310 cidctx := context.WithValue(mox.Context, mlog.CidKey, c.cid)
1311 ctx, cancel := context.WithTimeout(cidctx, time.Minute)
1312 valid, err := checkMXRecords(ctx, c.resolver, rpath.IPDomain.Domain)
1315 c.log.Infox("temporary reject for temporary mx lookup error", err)
1316 xsmtpServerErrorf(codes{smtp.C451LocalErr, smtp.SeNet4Other0}, "cannot verify mx records for mailfrom domain")
1318 c.log.Info("permanent reject because mailfrom domain does not accept mail")
1319 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SePol7SenderHasNullMX27, "mailfrom domain not configured for mail")
1323 if c.submission && (len(rpath.IPDomain.IP) > 0 || !rpathAllowed()) {
1325 c.log.Info("submission with unconfigured mailfrom", mlog.Field("user", c.username), mlog.Field("mailfrom", rpath.String()))
1326 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SePol7DeliveryUnauth1, "must match authenticated user")
1327 } else if !c.submission && len(rpath.IPDomain.IP) > 0 {
1328 // todo future: allow if the IP is the same as this connection is coming from? does later code allow this?
1329 c.log.Info("delivery from address without domain", mlog.Field("mailfrom", rpath.String()))
1330 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SePol7Other0, "domain name required")
1333 if Localserve && strings.HasPrefix(string(rpath.Localpart), "mailfrom") {
1334 c.xlocalserveError(rpath.Localpart)
1339 c.bwritecodeline(smtp.C250Completed, smtp.SeAddr1Other0, "looking good", nil)
1343func (c *conn) cmdRcpt(p *parser) {
1346 c.xneedTLSForDelivery()
1347 if c.mailFrom == nil {
1349 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "missing MAIL FROM")
1355 // Allow illegal space for submission only, not for regular SMTP. Microsoft Outlook
1356 // 365 Apps for Enterprise sends it.
1357 if c.submission && !moxvar.Pedantic {
1361 if p.take("<POSTMASTER>") {
1362 fpath = smtp.Path{Localpart: "postmaster"}
1364 fpath = p.xforwardPath()
1368 key := p.xparamKeyword()
1369 // K := strings.ToUpper(key)
1372 xsmtpUserErrorf(smtp.C555UnrecognizedAddrParams, smtp.SeSys3NotSupported3, "unrecognized parameter %q", key)
1376 // todo future: for submission, should we do explicit verification that domains are fully qualified? also for mail from.
../rfc/6409:420
1378 if len(c.recipients) >= 100 {
1380 xsmtpUserErrorf(smtp.C452StorageFull, smtp.SeProto5TooManyRcpts3, "max of 100 recipients reached")
1383 // We don't want to allow delivery to multiple recipients with a null reverse path.
1384 // Why would anyone send like that? Null reverse path is intended for delivery
1385 // notifications, they should go to a single recipient.
1386 if !c.submission && len(c.recipients) > 0 && c.mailFrom.IsZero() {
1387 xsmtpUserErrorf(smtp.C452StorageFull, smtp.SeProto5TooManyRcpts3, "only one recipient allowed with null reverse address")
1390 // Do not accept multiple recipients if remote does not pass SPF. Because we don't
1391 // want to generate DSNs to unverified domains. This is the moment we
1392 // can refuse individual recipients, DATA will be too late. Because mail
1393 // servers must handle a max recipient limit gracefully and still send to the
1394 // recipients that are accepted, this should not cause problems. Though we are in
1395 // violation because the limit must be >= 100.
1399 if !c.submission && len(c.recipients) == 1 && !Localserve {
1400 // note: because of check above, mailFrom cannot be the null address.
1402 d := c.mailFrom.IPDomain.Domain
1404 // todo: use this spf result for DATA.
1405 spfArgs := spf.Args{
1406 RemoteIP: c.remoteIP,
1407 MailFromLocalpart: c.mailFrom.Localpart,
1409 HelloDomain: c.hello,
1411 LocalHostname: c.hostname,
1413 cidctx := context.WithValue(mox.Context, mlog.CidKey, c.cid)
1414 spfctx, spfcancel := context.WithTimeout(cidctx, time.Minute)
1416 receivedSPF, _, _, err := spf.Verify(spfctx, c.resolver, spfArgs)
1419 c.log.Errorx("spf verify for multiple recipients", err)
1421 pass = receivedSPF.Identity == spf.ReceivedMailFrom && receivedSPF.Result == spf.StatusPass
1424 xsmtpUserErrorf(smtp.C452StorageFull, smtp.SeProto5TooManyRcpts3, "only one recipient allowed without spf pass")
1429 if strings.HasPrefix(string(fpath.Localpart), "rcptto") {
1430 c.xlocalserveError(fpath.Localpart)
1433 // If account or destination doesn't exist, it will be handled during delivery. For
1434 // submissions, which is the common case, we'll deliver to the logged in user,
1435 // which is typically the mox user.
1436 acc, _ := mox.Conf.Account("mox")
1437 dest := acc.Destinations["mox@localhost"]
1438 c.recipients = append(c.recipients, rcptAccount{fpath, true, "mox", dest, "mox@localhost"})
1439 } else if len(fpath.IPDomain.IP) > 0 {
1441 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeAddr1UnknownDestMailbox1, "not accepting email for ip")
1443 c.recipients = append(c.recipients, rcptAccount{fpath, false, "", config.Destination{}, ""})
1444 } else if accountName, canonical, addr, err := mox.FindAccount(fpath.Localpart, fpath.IPDomain.Domain, true); err == nil {
1446 c.recipients = append(c.recipients, rcptAccount{fpath, true, accountName, addr, canonical})
1447 } else if errors.Is(err, mox.ErrDomainNotFound) {
1449 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeAddr1UnknownDestMailbox1, "not accepting email for domain")
1451 // We'll be delivering this email.
1452 c.recipients = append(c.recipients, rcptAccount{fpath, false, "", config.Destination{}, ""})
1453 } else if errors.Is(err, mox.ErrAccountNotFound) {
1455 // For submission, we're transparent about which user exists. Should be fine for the typical small-scale deploy.
1457 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeAddr1UnknownDestMailbox1, "no such user")
1459 // We pretend to accept. We don't want to let remote know the user does not exist
1460 // until after DATA. Because then remote has committed to sending a message.
1461 // note: not local for !c.submission is the signal this address is in error.
1462 c.recipients = append(c.recipients, rcptAccount{fpath, false, "", config.Destination{}, ""})
1464 c.log.Errorx("looking up account for delivery", err, mlog.Field("rcptto", fpath))
1465 xsmtpServerErrorf(codes{smtp.C451LocalErr, smtp.SeSys3Other0}, "error processing")
1467 c.bwritecodeline(smtp.C250Completed, smtp.SeAddr1Other0, "now on the list", nil)
1471func (c *conn) cmdData(p *parser) {
1474 c.xneedTLSForDelivery()
1475 if c.mailFrom == nil {
1477 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "missing MAIL FROM")
1479 if len(c.recipients) == 0 {
1481 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "missing RCPT TO")
1487 // todo future: we could start a reader for a single line. we would then create a context that would be canceled on i/o errors.
1489 // Entire delivery should be done within 30 minutes, or we abort.
1490 cidctx := context.WithValue(mox.Context, mlog.CidKey, c.cid)
1491 cmdctx, cmdcancel := context.WithTimeout(cidctx, 30*time.Minute)
1493 // Deadline is taken into account by Read and Write.
1494 c.deadline, _ = cmdctx.Deadline()
1496 c.deadline = time.Time{}
1500 c.writelinef("354 see you at the bare dot")
1502 // Mark as tracedata.
1503 defer c.xtrace(mlog.LevelTracedata)()
1505 // We read the data into a temporary file. We limit the size and do basic analysis while reading.
1506 dataFile, err := store.CreateMessageTemp("smtp-deliver")
1508 xsmtpServerErrorf(errCodes(smtp.C451LocalErr, smtp.SeSys3Other0, err), "creating temporary file for message: %s", err)
1511 if dataFile != nil {
1512 err := os.Remove(dataFile.Name())
1513 c.log.Check(err, "removing temporary message file", mlog.Field("path", dataFile.Name()))
1514 err = dataFile.Close()
1515 c.log.Check(err, "removing temporary message file")
1518 msgWriter := message.NewWriter(dataFile)
1519 dr := smtp.NewDataReader(c.r)
1520 n, err := io.Copy(&limitWriter{maxSize: c.maxMessageSize, w: msgWriter}, dr)
1521 c.xtrace(mlog.LevelTrace) // Restore.
1523 if errors.Is(err, errMessageTooLarge) {
1525 ecode := smtp.SeSys3MsgLimitExceeded4
1526 if n < config.DefaultMaxMsgSize {
1527 ecode = smtp.SeMailbox2MsgLimitExceeded3
1529 c.writecodeline(smtp.C451LocalErr, ecode, fmt.Sprintf("error copying data to file (%s)", mox.ReceivedID(c.cid)), err)
1530 panic(fmt.Errorf("remote sent too much DATA: %w", errIO))
1533 // Something is failing on our side. We want to let remote know. So write an error response,
1534 // then discard the remaining data so the remote client is more likely to see our
1535 // response. Our write is synchronous, there is a risk no window/buffer space is
1536 // available and our write blocks us from reading remaining data, leading to
1537 // deadlock. We have a timeout on our connection writes though, so worst case we'll
1538 // abort the connection due to expiration.
1539 c.writecodeline(smtp.C451LocalErr, smtp.SeSys3Other0, fmt.Sprintf("error copying data to file (%s)", mox.ReceivedID(c.cid)), err)
1540 io.Copy(io.Discard, dr)
1544 // Basic sanity checks on messages before we send them out to the world. Just
1545 // trying to be strict in what we do to others and liberal in what we accept.
1547 if !msgWriter.HaveBody {
1549 xsmtpUserErrorf(smtp.C554TransactionFailed, smtp.SeMsg6Other0, "message requires both header and body section")
1551 // Check only for pedantic mode because ios mail will attempt to send smtputf8 with
1552 // non-ascii in message from localpart without using 8bitmime.
1553 if moxvar.Pedantic && msgWriter.Has8bit && !c.has8bitmime {
1555 xsmtpUserErrorf(smtp.C500BadSyntax, smtp.SeMsg6Other0, "message with non-us-ascii requires 8bitmime extension")
1560 // Require that message can be parsed fully.
1561 p, err := message.Parse(c.log, false, dataFile)
1563 err = p.Walk(c.log, nil)
1567 xsmtpUserErrorf(smtp.C554TransactionFailed, smtp.SeMsg6Other0, "malformed message: %v", err)
1571 // Prepare "Received" header.
1575 var iprevStatus iprev.Status // Only for delivery, not submission.
1577 // Hide internal hosts.
1578 // todo future: make this a config option, where admins specify ip ranges that they don't want exposed. also see
../rfc/5321:4321
1579 recvFrom = message.HeaderCommentDomain(mox.Conf.Static.HostnameDomain, c.smtputf8)
1581 if len(c.hello.IP) > 0 {
1582 recvFrom = smtp.AddressLiteral(c.hello.IP)
1584 // ASCII-only version added after the extended-domain syntax below, because the
1585 // comment belongs to "BY" which comes immediately after "FROM".
1586 recvFrom = c.hello.Domain.XName(c.smtputf8)
1588 iprevctx, iprevcancel := context.WithTimeout(cmdctx, time.Minute)
1590 var revNames []string
1591 iprevStatus, revName, revNames, err = iprev.Lookup(iprevctx, c.resolver, c.remoteIP)
1594 c.log.Infox("reverse-forward lookup", err, mlog.Field("remoteip", c.remoteIP))
1596 c.log.Debug("dns iprev check", mlog.Field("addr", c.remoteIP), mlog.Field("status", iprevStatus))
1600 } else if len(revNames) > 0 {
1603 name = strings.TrimSuffix(name, ".")
1605 if name != "" && name != c.hello.Domain.XName(c.smtputf8) {
1606 recvFrom += name + " "
1608 recvFrom += smtp.AddressLiteral(c.remoteIP) + ")"
1609 if c.smtputf8 && c.hello.Domain.Unicode != "" {
1610 recvFrom += " (" + c.hello.Domain.ASCII + ")"
1613 recvBy := mox.Conf.Static.HostnameDomain.XName(c.smtputf8)
1614 recvBy += " (" + smtp.AddressLiteral(c.localIP) + ")" // todo: hide ip if internal?
1615 if c.smtputf8 && mox.Conf.Static.HostnameDomain.Unicode != "" {
1616 // This syntax is part of "VIA".
1617 recvBy += " (" + mox.Conf.Static.HostnameDomain.ASCII + ")"
1630 if c.account != nil {
1635 // Assume transaction does not succeed. If it does, we'll compensate.
1638 recvHdrFor := func(rcptTo string) string {
1639 recvHdr := &message.HeaderWriter{}
1640 // For additional Received-header clauses, see:
1641 // https://www.iana.org/assignments/mail-parameters/mail-parameters.xhtml#table-mail-parameters-8
1642 recvHdr.Add(" ", "Received:", "from", recvFrom, "by", recvBy, "via", "tcp", "with", with, "id", mox.ReceivedID(c.cid)) //
../rfc/5321:3158
1644 tlsConn := c.conn.(*tls.Conn)
1645 tlsComment := message.TLSReceivedComment(c.log, tlsConn.ConnectionState())
1646 recvHdr.Add(" ", tlsComment...)
1648 recvHdr.Add(" ", "for", "<"+rcptTo+">;", time.Now().Format(message.RFC5322Z))
1649 return recvHdr.String()
1652 // Submission is easiest because user is trusted. Far fewer checks to make. So
1653 // handle it first, and leave the rest of the function for handling wild west
1654 // internet traffic.
1656 c.submit(cmdctx, recvHdrFor, msgWriter, &dataFile)
1658 c.deliver(cmdctx, recvHdrFor, msgWriter, iprevStatus, &dataFile)
1662// submit is used for mail from authenticated users that we will try to deliver.
1663func (c *conn) submit(ctx context.Context, recvHdrFor func(string) string, msgWriter *message.Writer, pdataFile **os.File) {
1664 // Similar between ../smtpserver/server.go:/submit\( and ../webmail/webmail.go:/MessageSubmit\(
1666 dataFile := *pdataFile
1668 var msgPrefix []byte
1670 // Check that user is only sending email as one of its configured identities. Not
1674 msgFrom, header, err := message.From(c.log, true, dataFile)
1676 metricSubmission.WithLabelValues("badmessage").Inc()
1677 c.log.Infox("parsing message From address", err, mlog.Field("user", c.username))
1678 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeMsg6Other0, "cannot parse header or From address: %v", err)
1680 accName, _, _, err := mox.FindAccount(msgFrom.Localpart, msgFrom.Domain, true)
1681 if err != nil || accName != c.account.Name {
1684 err = mox.ErrAccountNotFound
1686 metricSubmission.WithLabelValues("badfrom").Inc()
1687 c.log.Infox("verifying message From address", err, mlog.Field("user", c.username), mlog.Field("msgfrom", msgFrom))
1688 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SePol7DeliveryUnauth1, "must match authenticated user")
1691 // Outgoing messages should not have a Return-Path header. The final receiving mail
1692 // server will add it.
1694 if header.Values("Return-Path") != nil {
1695 metricSubmission.WithLabelValues("badheader").Inc()
1696 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeMsg6Other0, "message must not have Return-Path header")
1699 // Add Message-Id header if missing.
1701 messageID := header.Get("Message-Id")
1702 if messageID == "" {
1703 messageID = mox.MessageIDGen(c.smtputf8)
1704 msgPrefix = append(msgPrefix, fmt.Sprintf("Message-Id: <%s>\r\n", messageID)...)
1708 if header.Get("Date") == "" {
1709 msgPrefix = append(msgPrefix, "Date: "+time.Now().Format(message.RFC5322Z)+"\r\n"...)
1712 // Check outoging message rate limit.
1713 err = c.account.DB.Read(ctx, func(tx *bstore.Tx) error {
1714 rcpts := make([]smtp.Path, len(c.recipients))
1715 for i, r := range c.recipients {
1718 msglimit, rcptlimit, err := c.account.SendLimitReached(tx, rcpts)
1719 xcheckf(err, "checking sender limit")
1721 metricSubmission.WithLabelValues("messagelimiterror").Inc()
1722 xsmtpUserErrorf(smtp.C451LocalErr, smtp.SePol7DeliveryUnauth1, "max number of messages (%d) over past 24h reached, try increasing per-account setting MaxOutgoingMessagesPerDay", msglimit)
1723 } else if rcptlimit >= 0 {
1724 metricSubmission.WithLabelValues("recipientlimiterror").Inc()
1725 xsmtpUserErrorf(smtp.C451LocalErr, smtp.SePol7DeliveryUnauth1, "max number of new/first-time recipients (%d) over past 24h reached, try increasing per-account setting MaxFirstTimeRecipientsPerDay", rcptlimit)
1729 xcheckf(err, "read-only transaction")
1731 // todo future: in a pedantic mode, we can parse the headers, and return an error if rcpt is only in To or Cc header, and not in the non-empty Bcc header. indicates a client that doesn't blind those bcc's.
1733 // Add DKIM signatures.
1734 confDom, ok := mox.Conf.Domain(msgFrom.Domain)
1736 c.log.Error("domain disappeared", mlog.Field("domain", msgFrom.Domain))
1737 xsmtpServerErrorf(codes{smtp.C451LocalErr, smtp.SeSys3Other0}, "internal error")
1740 dkimConfig := confDom.DKIM
1741 if len(dkimConfig.Sign) > 0 {
1742 if canonical, err := mox.CanonicalLocalpart(msgFrom.Localpart, confDom); err != nil {
1743 c.log.Errorx("determining canonical localpart for dkim signing", err, mlog.Field("localpart", msgFrom.Localpart))
1744 } else if dkimHeaders, err := dkim.Sign(ctx, canonical, msgFrom.Domain, dkimConfig, c.smtputf8, store.FileMsgReader(msgPrefix, dataFile)); err != nil {
1745 c.log.Errorx("dkim sign for domain", err, mlog.Field("domain", msgFrom.Domain))
1746 metricServerErrors.WithLabelValues("dkimsign").Inc()
1748 msgPrefix = append(msgPrefix, []byte(dkimHeaders)...)
1752 authResults := message.AuthResults{
1753 Hostname: mox.Conf.Static.HostnameDomain.XName(c.smtputf8),
1754 Comment: mox.Conf.Static.HostnameDomain.ASCIIExtra(c.smtputf8),
1755 Methods: []message.AuthMethod{
1759 Props: []message.AuthProp{
1760 message.MakeAuthProp("smtp", "mailfrom", c.mailFrom.XString(c.smtputf8), true, c.mailFrom.ASCIIExtra(c.smtputf8)),
1765 msgPrefix = append(msgPrefix, []byte(authResults.Header())...)
1767 // We always deliver through the queue. It would be more efficient to deliver
1768 // directly, but we don't want to circumvent all the anti-spam measures. Accounts
1769 // on a single mox instance should be allowed to block each other.
1770 for i, rcptAcc := range c.recipients {
1772 code, timeout := localserveNeedsError(rcptAcc.rcptTo.Localpart)
1774 c.log.Info("timing out submission due to special localpart")
1775 mox.Sleep(mox.Context, time.Hour)
1776 xsmtpServerErrorf(codes{smtp.C451LocalErr, smtp.SeSys3Other0}, "timing out submission due to special localpart")
1777 } else if code != 0 {
1778 c.log.Info("failure due to special localpart", mlog.Field("code", code))
1779 xsmtpServerErrorf(codes{code, smtp.SeOther00}, "failure with code %d due to special localpart", code)
1783 xmsgPrefix := append([]byte(recvHdrFor(rcptAcc.rcptTo.String())), msgPrefix...)
1785 msgSize := int64(len(xmsgPrefix)) + msgWriter.Size
1786 if _, err := queue.Add(ctx, c.log, c.account.Name, *c.mailFrom, rcptAcc.rcptTo, msgWriter.Has8bit, c.smtputf8, msgSize, messageID, xmsgPrefix, dataFile, nil, i == len(c.recipients)-1); err != nil {
1787 // Aborting the transaction is not great. But continuing and generating DSNs will
1788 // probably result in errors as well...
1789 metricSubmission.WithLabelValues("queueerror").Inc()
1790 c.log.Errorx("queuing message", err)
1791 xsmtpServerErrorf(errCodes(smtp.C451LocalErr, smtp.SeSys3Other0, err), "error delivering message: %v", err)
1793 metricSubmission.WithLabelValues("ok").Inc()
1794 c.log.Info("message queued for delivery", mlog.Field("mailfrom", *c.mailFrom), mlog.Field("rcptto", rcptAcc.rcptTo), mlog.Field("smtputf8", c.smtputf8), mlog.Field("msgsize", msgSize))
1796 err := c.account.DB.Insert(ctx, &store.Outgoing{Recipient: rcptAcc.rcptTo.XString(true)})
1797 xcheckf(err, "adding outgoing message")
1800 err = dataFile.Close()
1801 c.log.Check(err, "closing file after submission")
1805 c.transactionBad-- // Compensate for early earlier pessimistic increase.
1808 c.writecodeline(smtp.C250Completed, smtp.SeMailbox2Other0, "it is done", nil)
1811func ipmasked(ip net.IP) (string, string, string) {
1812 if ip.To4() != nil {
1814 m2 := ip.Mask(net.CIDRMask(26, 32)).String()
1815 m3 := ip.Mask(net.CIDRMask(21, 32)).String()
1818 m1 := ip.Mask(net.CIDRMask(64, 128)).String()
1819 m2 := ip.Mask(net.CIDRMask(48, 128)).String()
1820 m3 := ip.Mask(net.CIDRMask(32, 128)).String()
1824func localserveNeedsError(lp smtp.Localpart) (code int, timeout bool) {
1826 if strings.HasSuffix(s, "temperror") {
1827 return smtp.C451LocalErr, false
1828 } else if strings.HasSuffix(s, "permerror") {
1829 return smtp.C550MailboxUnavail, false
1830 } else if strings.HasSuffix(s, "timeout") {
1837 v, err := strconv.ParseInt(s, 10, 32)
1841 if v < 400 || v > 600 {
1844 return int(v), false
1847func (c *conn) xlocalserveError(lp smtp.Localpart) {
1848 code, timeout := localserveNeedsError(lp)
1850 c.log.Info("timing out due to special localpart")
1851 mox.Sleep(mox.Context, time.Hour)
1852 xsmtpServerErrorf(codes{smtp.C451LocalErr, smtp.SeSys3Other0}, "timing out command due to special localpart")
1853 } else if code != 0 {
1854 c.log.Info("failure due to special localpart", mlog.Field("code", code))
1855 metricDelivery.WithLabelValues("delivererror", "localserve").Inc()
1856 xsmtpServerErrorf(codes{code, smtp.SeOther00}, "failure with code %d due to special localpart", code)
1860// deliver is called for incoming messages from external, typically untrusted
1861// sources. i.e. not submitted by authenticated users.
1862func (c *conn) deliver(ctx context.Context, recvHdrFor func(string) string, msgWriter *message.Writer, iprevStatus iprev.Status, pdataFile **os.File) {
1863 dataFile := *pdataFile
1865 // todo: in decision making process, if we run into (some) temporary errors, attempt to continue. if we decide to accept, all good. if we decide to reject, we'll make it a temporary reject.
1867 msgFrom, headers, err := message.From(c.log, false, dataFile)
1869 c.log.Infox("parsing message for From address", err)
1873 if len(headers.Values("Received")) > 100 {
1874 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeNet4Loop6, "loop detected, more than 100 Received headers")
1877 // We'll be building up an Authentication-Results header.
1878 authResults := message.AuthResults{
1879 Hostname: mox.Conf.Static.HostnameDomain.XName(c.smtputf8),
1882 // Reverse IP lookup results.
1883 // todo future: how useful is this?
1885 authResults.Methods = append(authResults.Methods, message.AuthMethod{
1887 Result: string(iprevStatus),
1888 Props: []message.AuthProp{
1889 message.MakeAuthProp("policy", "iprev", c.remoteIP.String(), false, ""),
1893 // SPF and DKIM verification in parallel.
1894 var wg sync.WaitGroup
1898 var dkimResults []dkim.Result
1902 x := recover() // Should not happen, but don't take program down if it does.
1904 c.log.Error("dkim verify panic", mlog.Field("err", x))
1906 metrics.PanicInc(metrics.Dkimverify)
1910 // We always evaluate all signatures. We want to build up reputation for each
1911 // domain in the signature.
1912 const ignoreTestMode = false
1913 // todo future: longer timeout? we have to read through the entire email, which can be large, possibly multiple times.
1914 dkimctx, dkimcancel := context.WithTimeout(ctx, time.Minute)
1916 // todo future: we could let user configure which dkim headers they require
1917 dkimResults, dkimErr = dkim.Verify(dkimctx, c.resolver, c.smtputf8, dkim.DefaultPolicy, dataFile, ignoreTestMode)
1923 var receivedSPF spf.Received
1924 var spfDomain dns.Domain
1927 spfArgs := spf.Args{
1928 RemoteIP: c.remoteIP,
1929 MailFromLocalpart: c.mailFrom.Localpart,
1930 MailFromDomain: c.mailFrom.IPDomain.Domain, // Can be empty.
1931 HelloDomain: c.hello,
1933 LocalHostname: c.hostname,
1938 x := recover() // Should not happen, but don't take program down if it does.
1940 c.log.Error("spf verify panic", mlog.Field("err", x))
1942 metrics.PanicInc(metrics.Spfverify)
1946 spfctx, spfcancel := context.WithTimeout(ctx, time.Minute)
1948 receivedSPF, spfDomain, spfExpl, spfErr = spf.Verify(spfctx, c.resolver, spfArgs)
1951 c.log.Infox("spf verify", spfErr)
1955 // Wait for DKIM and SPF validation to finish.
1958 // Give immediate response if all recipients are unknown.
1960 for _, r := range c.recipients {
1965 if nunknown == len(c.recipients) {
1966 // During RCPT TO we found that the address does not exist.
1967 c.log.Info("deliver attempt to unknown user(s)", mlog.Field("recipients", c.recipients))
1969 // Crude attempt to slow down someone trying to guess names. Would work better
1970 // with connection rate limiter.
1971 if unknownRecipientsDelay > 0 {
1972 mox.Sleep(ctx, unknownRecipientsDelay)
1975 // todo future: if remote does not look like a properly configured mail system, respond with generic 451 error? to prevent any random internet system from discovering accounts. we could give proper response if spf for ehlo or mailfrom passes.
1976 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeAddr1UnknownDestMailbox1, "no such user(s)")
1979 // Add DKIM results to Authentication-Results header.
1980 authResAddDKIM := func(result, comment, reason string, props []message.AuthProp) {
1981 dm := message.AuthMethod{
1988 authResults.Methods = append(authResults.Methods, dm)
1991 c.log.Errorx("dkim verify", dkimErr)
1992 authResAddDKIM("none", "", dkimErr.Error(), nil)
1993 } else if len(dkimResults) == 0 {
1994 c.log.Info("no dkim-signature header", mlog.Field("mailfrom", c.mailFrom))
1995 authResAddDKIM("none", "", "no dkim signatures", nil)
1997 for i, r := range dkimResults {
1998 var domain, selector dns.Domain
1999 var identity *dkim.Identity
2001 var props []message.AuthProp
2003 // todo future: also specify whether dns record was dnssec-signed.
2004 if r.Record != nil && r.Record.PublicKey != nil {
2005 if pubkey, ok := r.Record.PublicKey.(*rsa.PublicKey); ok {
2006 comment = fmt.Sprintf("%d bit rsa", pubkey.N.BitLen())
2010 sig := base64.StdEncoding.EncodeToString(r.Sig.Signature)
2011 sig = sig[:12] // Must be at least 8 characters and unique among the signatures.
2012 props = []message.AuthProp{
2013 message.MakeAuthProp("header", "d", r.Sig.Domain.XName(c.smtputf8), true, r.Sig.Domain.ASCIIExtra(c.smtputf8)),
2014 message.MakeAuthProp("header", "s", r.Sig.Selector.XName(c.smtputf8), true, r.Sig.Selector.ASCIIExtra(c.smtputf8)),
2015 message.MakeAuthProp("header", "a", r.Sig.Algorithm(), false, ""),
2018 domain = r.Sig.Domain
2019 selector = r.Sig.Selector
2020 if r.Sig.Identity != nil {
2021 props = append(props, message.MakeAuthProp("header", "i", r.Sig.Identity.String(), true, ""))
2022 identity = r.Sig.Identity
2027 errmsg = r.Err.Error()
2029 authResAddDKIM(string(r.Status), comment, errmsg, props)
2030 c.log.Debugx("dkim verification result", r.Err, mlog.Field("index", i), mlog.Field("mailfrom", c.mailFrom), mlog.Field("status", r.Status), mlog.Field("domain", domain), mlog.Field("selector", selector), mlog.Field("identity", identity))
2034 var spfIdentity *dns.Domain
2035 var mailFromValidation = store.ValidationUnknown
2036 var ehloValidation = store.ValidationUnknown
2037 switch receivedSPF.Identity {
2038 case spf.ReceivedHELO:
2039 if len(spfArgs.HelloDomain.IP) == 0 {
2040 spfIdentity = &spfArgs.HelloDomain.Domain
2042 ehloValidation = store.SPFValidation(receivedSPF.Result)
2043 case spf.ReceivedMailFrom:
2044 spfIdentity = &spfArgs.MailFromDomain
2045 mailFromValidation = store.SPFValidation(receivedSPF.Result)
2047 var props []message.AuthProp
2048 if spfIdentity != nil {
2049 props = []message.AuthProp{message.MakeAuthProp("smtp", string(receivedSPF.Identity), spfIdentity.XName(c.smtputf8), true, spfIdentity.ASCIIExtra(c.smtputf8))}
2051 authResults.Methods = append(authResults.Methods, message.AuthMethod{
2053 Result: string(receivedSPF.Result),
2056 switch receivedSPF.Result {
2057 case spf.StatusPass:
2058 c.log.Debug("spf pass", mlog.Field("ip", spfArgs.RemoteIP), mlog.Field("mailfromdomain", spfArgs.MailFromDomain.ASCII)) // todo: log the domain that was actually verified.
2059 case spf.StatusFail:
2062 for _, b := range []byte(spfExpl) {
2063 if b < ' ' || b >= 0x7f {
2069 if len(spfExpl) > 800 {
2070 spfExpl = spfExpl[:797] + "..."
2072 spfExpl = "remote claims: " + spfExpl
2076 spfExpl = fmt.Sprintf("your ip %s is not on the SPF allowlist for domain %s", spfArgs.RemoteIP, spfDomain.ASCII)
2078 c.log.Info("spf fail", mlog.Field("explanation", spfExpl)) // todo future: get this to the client. how? in smtp session in case of a reject due to dmarc fail?
2079 case spf.StatusTemperror:
2080 c.log.Infox("spf temperror", spfErr)
2081 case spf.StatusPermerror:
2082 c.log.Infox("spf permerror", spfErr)
2083 case spf.StatusNone, spf.StatusNeutral, spf.StatusSoftfail:
2085 c.log.Error("unknown spf status, treating as None/Neutral", mlog.Field("status", receivedSPF.Result))
2086 receivedSPF.Result = spf.StatusNone
2091 var dmarcResult dmarc.Result
2092 const applyRandomPercentage = true
2093 var dmarcMethod message.AuthMethod
2094 var msgFromValidation = store.ValidationNone
2095 if msgFrom.IsZero() {
2096 dmarcResult.Status = dmarc.StatusNone
2097 dmarcMethod = message.AuthMethod{
2099 Result: string(dmarcResult.Status),
2102 msgFromValidation = alignment(ctx, msgFrom.Domain, dkimResults, receivedSPF.Result, spfIdentity)
2104 dmarcctx, dmarccancel := context.WithTimeout(ctx, time.Minute)
2106 dmarcUse, dmarcResult = dmarc.Verify(dmarcctx, c.resolver, msgFrom.Domain, dkimResults, receivedSPF.Result, spfIdentity, applyRandomPercentage)
2108 dmarcMethod = message.AuthMethod{
2110 Result: string(dmarcResult.Status),
2111 Props: []message.AuthProp{
2113 message.MakeAuthProp("header", "from", msgFrom.Domain.ASCII, true, msgFrom.Domain.ASCIIExtra(c.smtputf8)),
2117 if dmarcResult.Status == dmarc.StatusPass && msgFromValidation == store.ValidationRelaxed {
2118 msgFromValidation = store.ValidationDMARC
2121 // todo future: consider enforcing an spf fail if there is no dmarc policy or the dmarc policy is none.
../rfc/7489:1507
2123 authResults.Methods = append(authResults.Methods, dmarcMethod)
2124 c.log.Debug("dmarc verification", mlog.Field("result", dmarcResult.Status), mlog.Field("domain", msgFrom.Domain))
2126 // Prepare for analyzing content, calculating reputation.
2127 ipmasked1, ipmasked2, ipmasked3 := ipmasked(c.remoteIP)
2128 var verifiedDKIMDomains []string
2129 dkimSeen := map[string]bool{}
2130 for _, r := range dkimResults {
2131 // A message can have multiple signatures for the same identity. For example when
2132 // signing the message multiple times with different algorithms (rsa and ed25519).
2133 if r.Status != dkim.StatusPass {
2136 d := r.Sig.Domain.Name()
2139 verifiedDKIMDomains = append(verifiedDKIMDomains, d)
2143 // When we deliver, we try to remove from rejects mailbox based on message-id.
2144 // We'll parse it when we need it, but it is the same for each recipient.
2145 var messageID string
2146 var parsedMessageID bool
2148 // We build up a DSN for each failed recipient. If we have recipients in dsnMsg
2149 // after processing, we queue the DSN. Unless all recipients failed, in which case
2150 // we may just fail the mail transaction instead (could be common for failure to
2151 // deliver to a single recipient, e.g. for junk mail).
2153 type deliverError struct {
2160 var deliverErrors []deliverError
2161 addError := func(rcptAcc rcptAccount, code int, secode string, userError bool, errmsg string) {
2162 e := deliverError{rcptAcc.rcptTo, code, secode, userError, errmsg}
2163 c.log.Info("deliver error", mlog.Field("rcptto", e.rcptTo), mlog.Field("code", code), mlog.Field("secode", "secode"), mlog.Field("usererror", userError), mlog.Field("errmsg", errmsg))
2164 deliverErrors = append(deliverErrors, e)
2167 // For each recipient, do final spam analysis and delivery.
2168 for _, rcptAcc := range c.recipients {
2169 log := c.log.Fields(mlog.Field("mailfrom", c.mailFrom), mlog.Field("rcptto", rcptAcc.rcptTo))
2171 // If this is not a valid local user, we send back a DSN. This can only happen when
2172 // there are also valid recipients, and only when remote is SPF-verified, so the DSN
2173 // should not cause backscatter.
2174 // In case of serious errors, we abort the transaction. We may have already
2175 // delivered some messages. Perhaps it would be better to continue with other
2176 // deliveries, and return an error at the end? Though the failure conditions will
2177 // probably prevent any other successful deliveries too...
2180 metricDelivery.WithLabelValues("unknownuser", "").Inc()
2181 addError(rcptAcc, smtp.C550MailboxUnavail, smtp.SeAddr1UnknownDestMailbox1, true, "no such user")
2185 acc, err := store.OpenAccount(rcptAcc.accountName)
2187 log.Errorx("open account", err, mlog.Field("account", rcptAcc.accountName))
2188 metricDelivery.WithLabelValues("accounterror", "").Inc()
2189 addError(rcptAcc, smtp.C451LocalErr, smtp.SeSys3Other0, false, "error processing")
2195 log.Check(err, "closing account after delivery")
2199 // We don't want to let a single IP or network deliver too many messages to an
2200 // account. They may fill up the mailbox, either with messages that have to be
2201 // purged, or by filling the disk. We check both cases for IP's and networks.
2202 var rateError bool // Whether returned error represents a rate error.
2203 err = acc.DB.Read(ctx, func(tx *bstore.Tx) (retErr error) {
2206 log.Debugx("checking message and size delivery rates", retErr, mlog.Field("duration", time.Since(now)))
2209 checkCount := func(msg store.Message, window time.Duration, limit int) {
2213 q := bstore.QueryTx[store.Message](tx)
2214 q.FilterNonzero(msg)
2215 q.FilterGreater("Received", now.Add(-window))
2216 q.FilterEqual("Expunged", false)
2224 retErr = fmt.Errorf("more than %d messages in past %s from your ip/network", limit, window)
2228 checkSize := func(msg store.Message, window time.Duration, limit int64) {
2232 q := bstore.QueryTx[store.Message](tx)
2233 q.FilterNonzero(msg)
2234 q.FilterGreater("Received", now.Add(-window))
2235 q.FilterEqual("Expunged", false)
2236 size := msgWriter.Size
2237 err := q.ForEach(func(v store.Message) error {
2247 retErr = fmt.Errorf("more than %d bytes in past %s from your ip/network", limit, window)
2251 // todo future: make these configurable
2252 // todo: should we have a limit for forwarded messages? they are stored with empty RemoteIPMasked*
2254 const day = 24 * time.Hour
2255 checkCount(store.Message{RemoteIPMasked1: ipmasked1}, time.Minute, limitIPMasked1MessagesPerMinute)
2256 checkCount(store.Message{RemoteIPMasked1: ipmasked1}, day, 20*500)
2257 checkCount(store.Message{RemoteIPMasked2: ipmasked2}, time.Minute, 1500)
2258 checkCount(store.Message{RemoteIPMasked2: ipmasked2}, day, 20*1500)
2259 checkCount(store.Message{RemoteIPMasked3: ipmasked3}, time.Minute, 4500)
2260 checkCount(store.Message{RemoteIPMasked3: ipmasked3}, day, 20*4500)
2262 const MB = 1024 * 1024
2263 checkSize(store.Message{RemoteIPMasked1: ipmasked1}, time.Minute, limitIPMasked1SizePerMinute)
2264 checkSize(store.Message{RemoteIPMasked1: ipmasked1}, day, 3*1000*MB)
2265 checkSize(store.Message{RemoteIPMasked2: ipmasked2}, time.Minute, 3000*MB)
2266 checkSize(store.Message{RemoteIPMasked2: ipmasked2}, day, 3*3000*MB)
2267 checkSize(store.Message{RemoteIPMasked3: ipmasked3}, time.Minute, 9000*MB)
2268 checkSize(store.Message{RemoteIPMasked3: ipmasked3}, day, 3*9000*MB)
2272 if err != nil && !rateError {
2273 log.Errorx("checking delivery rates", err)
2274 metricDelivery.WithLabelValues("checkrates", "").Inc()
2275 addError(rcptAcc, smtp.C451LocalErr, smtp.SeSys3Other0, false, "error processing")
2277 } else if err != nil {
2278 log.Debugx("refusing due to high delivery rate", err)
2279 metricDelivery.WithLabelValues("highrate", "").Inc()
2281 addError(rcptAcc, smtp.C452StorageFull, smtp.SeMailbox2Full2, true, err.Error())
2287 msgPrefix := []byte(
2290 authResults.Header() +
2291 receivedSPF.Header() +
2292 recvHdrFor(rcptAcc.rcptTo.String()),
2295 m := &store.Message{
2296 Received: time.Now(),
2297 RemoteIP: c.remoteIP.String(),
2298 RemoteIPMasked1: ipmasked1,
2299 RemoteIPMasked2: ipmasked2,
2300 RemoteIPMasked3: ipmasked3,
2301 EHLODomain: c.hello.Domain.Name(),
2302 MailFrom: c.mailFrom.String(),
2303 MailFromLocalpart: c.mailFrom.Localpart,
2304 MailFromDomain: c.mailFrom.IPDomain.Domain.Name(),
2305 RcptToLocalpart: rcptAcc.rcptTo.Localpart,
2306 RcptToDomain: rcptAcc.rcptTo.IPDomain.Domain.Name(),
2307 MsgFromLocalpart: msgFrom.Localpart,
2308 MsgFromDomain: msgFrom.Domain.Name(),
2309 MsgFromOrgDomain: publicsuffix.Lookup(ctx, msgFrom.Domain).Name(),
2310 EHLOValidated: ehloValidation == store.ValidationPass,
2311 MailFromValidated: mailFromValidation == store.ValidationPass,
2312 MsgFromValidated: msgFromValidation == store.ValidationStrict || msgFromValidation == store.ValidationDMARC || msgFromValidation == store.ValidationRelaxed,
2313 EHLOValidation: ehloValidation,
2314 MailFromValidation: mailFromValidation,
2315 MsgFromValidation: msgFromValidation,
2316 DKIMDomains: verifiedDKIMDomains,
2317 Size: int64(len(msgPrefix)) + msgWriter.Size,
2318 MsgPrefix: msgPrefix,
2320 d := delivery{m, dataFile, rcptAcc, acc, msgFrom, c.dnsBLs, dmarcUse, dmarcResult, dkimResults, iprevStatus}
2321 a := analyze(ctx, log, c.resolver, d)
2323 xmoxreason := "X-Mox-Reason: " + a.reason + "\r\n"
2324 m.MsgPrefix = append([]byte(xmoxreason), m.MsgPrefix...)
2325 m.Size += int64(len(xmoxreason))
2328 conf, _ := acc.Conf()
2329 if conf.RejectsMailbox != "" {
2330 present, _, messagehash, err := rejectPresent(log, acc, conf.RejectsMailbox, m, dataFile)
2332 log.Errorx("checking whether reject is already present", err)
2333 } else if !present {
2335 m.Seen = true // We don't want to draw attention.
2336 // Regular automatic junk flags configuration applies to these messages. The
2337 // default is to treat these as neutral, so they won't cause outright rejections
2338 // due to reputation for later delivery attempts.
2339 m.MessageHash = messagehash
2340 acc.WithWLock(func() {
2343 if !conf.KeepRejects {
2344 hasSpace, err = acc.TidyRejectsMailbox(c.log, conf.RejectsMailbox)
2347 log.Errorx("tidying rejects mailbox", err)
2348 } else if hasSpace {
2349 if err := acc.DeliverMailbox(log, conf.RejectsMailbox, m, dataFile, false); err != nil {
2350 log.Errorx("delivering spammy mail to rejects mailbox", err)
2352 log.Info("delivered spammy mail to rejects mailbox")
2355 log.Info("not storing spammy mail to full rejects mailbox")
2359 log.Info("reject message is already present, ignoring")
2363 log.Info("incoming message rejected", mlog.Field("reason", a.reason), mlog.Field("msgfrom", msgFrom))
2364 metricDelivery.WithLabelValues("reject", a.reason).Inc()
2366 addError(rcptAcc, a.code, a.secode, a.userError, a.errmsg)
2370 delayFirstTime := true
2371 if a.dmarcReport != nil {
2373 if err := dmarcdb.AddReport(ctx, a.dmarcReport, msgFrom.Domain); err != nil {
2374 log.Errorx("saving dmarc report in database", err)
2376 log.Info("dmarc report processed")
2378 delayFirstTime = false
2381 if a.tlsReport != nil {
2382 // todo future: add rate limiting to prevent DoS attacks.
2383 if err := tlsrptdb.AddReport(ctx, msgFrom.Domain, c.mailFrom.String(), a.tlsReport); err != nil {
2384 log.Errorx("saving TLSRPT report in database", err)
2386 log.Info("tlsrpt report processed")
2388 delayFirstTime = false
2392 // If this is a first-time sender and not a forwarded message, wait before actually
2393 // delivering. If this turns out to be a spammer, we've kept one of their
2394 // connections busy.
2395 if delayFirstTime && !m.IsForward && a.reason == reasonNoBadSignals && c.firstTimeSenderDelay > 0 {
2396 log.Debug("delaying before delivering from sender without reputation", mlog.Field("delay", c.firstTimeSenderDelay))
2397 mox.Sleep(mox.Context, c.firstTimeSenderDelay)
2400 // Gather the message-id before we deliver and the file may be consumed.
2401 if !parsedMessageID {
2402 if p, err := message.Parse(c.log, false, store.FileMsgReader(m.MsgPrefix, dataFile)); err != nil {
2403 log.Infox("parsing message for message-id", err)
2404 } else if header, err := p.Header(); err != nil {
2405 log.Infox("parsing message header for message-id", err)
2407 messageID = header.Get("Message-Id")
2412 code, timeout := localserveNeedsError(rcptAcc.rcptTo.Localpart)
2414 c.log.Info("timing out due to special localpart")
2415 mox.Sleep(mox.Context, time.Hour)
2416 xsmtpServerErrorf(codes{smtp.C451LocalErr, smtp.SeOther00}, "timing out delivery due to special localpart")
2417 } else if code != 0 {
2418 c.log.Info("failure due to special localpart", mlog.Field("code", code))
2419 metricDelivery.WithLabelValues("delivererror", "localserve").Inc()
2420 addError(rcptAcc, code, smtp.SeOther00, false, fmt.Sprintf("failure with code %d due to special localpart", code))
2423 acc.WithWLock(func() {
2424 if err := acc.DeliverMailbox(log, a.mailbox, m, dataFile, false); err != nil {
2425 log.Errorx("delivering", err)
2426 metricDelivery.WithLabelValues("delivererror", a.reason).Inc()
2427 addError(rcptAcc, smtp.C451LocalErr, smtp.SeSys3Other0, false, "error processing")
2430 metricDelivery.WithLabelValues("delivered", a.reason).Inc()
2431 log.Info("incoming message delivered", mlog.Field("reason", a.reason), mlog.Field("msgfrom", msgFrom))
2433 conf, _ := acc.Conf()
2434 if conf.RejectsMailbox != "" && m.MessageID != "" {
2435 if err := acc.RejectsRemove(log, conf.RejectsMailbox, m.MessageID); err != nil {
2436 log.Errorx("removing message from rejects mailbox", err, mlog.Field("messageid", messageID))
2442 log.Check(err, "closing account after delivering")
2446 // If all recipients failed to deliver, return an error.
2447 if len(c.recipients) == len(deliverErrors) {
2449 e0 := deliverErrors[0]
2450 var serverError bool
2453 for _, e := range deliverErrors {
2454 serverError = serverError || !e.userError
2455 if e.code != e0.code || e.secode != e0.secode {
2458 msgs = append(msgs, e.errmsg)
2464 xsmtpErrorf(e0.code, e0.secode, !serverError, "%s", strings.Join(msgs, "\n"))
2467 // Not all failures had the same error. We'll return each error on a separate line.
2469 for _, e := range deliverErrors {
2470 s := fmt.Sprintf("%d %d.%s %s", e.code, e.code/100, e.secode, e.errmsg)
2471 lines = append(lines, s)
2473 code := smtp.C451LocalErr
2474 secode := smtp.SeSys3Other0
2476 code = smtp.C554TransactionFailed
2478 lines = append(lines, "multiple errors")
2479 xsmtpErrorf(code, secode, !serverError, strings.Join(lines, "\n"))
2481 // Generate one DSN for all failed recipients.
2482 if len(deliverErrors) > 0 {
2484 dsnMsg := dsn.Message{
2485 SMTPUTF8: c.smtputf8,
2486 From: smtp.Path{Localpart: "postmaster", IPDomain: deliverErrors[0].rcptTo.IPDomain},
2488 Subject: "mail delivery failure",
2489 References: messageID,
2491 // Per-message details.
2492 ReportingMTA: mox.Conf.Static.HostnameDomain.ASCII,
2493 ReceivedFromMTA: smtp.Ehlo{Name: c.hello, ConnIP: c.remoteIP},
2497 if len(deliverErrors) > 1 {
2498 dsnMsg.TextBody = "Multiple delivery failures occurred.\n\n"
2501 for _, e := range deliverErrors {
2503 if e.code/100 == 4 {
2506 dsnMsg.TextBody += fmt.Sprintf("%s delivery failure to:\n\n\t%s\n\nError:\n\n\t%s\n\n", kind, e.errmsg, e.rcptTo.XString(false))
2507 rcpt := dsn.Recipient{
2508 FinalRecipient: e.rcptTo,
2510 Status: fmt.Sprintf("%d.%s", e.code/100, e.secode),
2511 LastAttemptDate: now,
2513 dsnMsg.Recipients = append(dsnMsg.Recipients, rcpt)
2516 header, err := message.ReadHeaders(bufio.NewReader(&moxio.AtReader{R: dataFile}))
2518 c.log.Errorx("reading headers of incoming message for dsn, continuing dsn without headers", err)
2520 dsnMsg.Original = header
2523 c.log.Error("not queueing dsn for incoming delivery due to localserve")
2524 } else if err := queueDSN(context.TODO(), c, *c.mailFrom, dsnMsg); err != nil {
2525 metricServerErrors.WithLabelValues("queuedsn").Inc()
2526 c.log.Errorx("queuing DSN for incoming delivery, no DSN sent", err)
2530 err = os.Remove(dataFile.Name())
2531 c.log.Check(err, "removing file after delivery")
2532 err = dataFile.Close()
2533 c.log.Check(err, "closing data file after delivery")
2537 c.transactionBad-- // Compensate for early earlier pessimistic increase.
2539 c.writecodeline(smtp.C250Completed, smtp.SeMailbox2Other0, "it is done", nil)
2542// ecode returns either ecode, or a more specific error based on err.
2543// For example, ecode can be turned from an "other system" error into a "mail
2544// system full" if the error indicates no disk space is available.
2545func errCodes(code int, ecode string, err error) codes {
2547 case moxio.IsStorageSpace(err):
2549 case smtp.SeMailbox2Other0:
2550 if code == smtp.C451LocalErr {
2551 code = smtp.C452StorageFull
2553 ecode = smtp.SeMailbox2Full2
2554 case smtp.SeSys3Other0:
2555 if code == smtp.C451LocalErr {
2556 code = smtp.C452StorageFull
2558 ecode = smtp.SeSys3StorageFull1
2561 return codes{code, ecode}
2565func (c *conn) cmdRset(p *parser) {
2570 c.bwritecodeline(smtp.C250Completed, smtp.SeOther00, "all clear", nil)
2574func (c *conn) cmdVrfy(p *parser) {
2575 // No EHLO/HELO needed.
2586 // todo future: we could support vrfy and expn for submission? though would need to see if its rfc defines it.
2589 xsmtpUserErrorf(smtp.C252WithoutVrfy, smtp.SePol7Other0, "no verify but will try delivery")
2593func (c *conn) cmdExpn(p *parser) {
2594 // No EHLO/HELO needed.
2606 xsmtpUserErrorf(smtp.C252WithoutVrfy, smtp.SePol7Other0, "no expand but will try delivery")
2610func (c *conn) cmdHelp(p *parser) {
2611 // Let's not strictly parse the request for help. We are ignoring the text anyway.
2614 c.bwritecodeline(smtp.C214Help, smtp.SeOther00, "see rfc 5321 (smtp)", nil)
2618func (c *conn) cmdNoop(p *parser) {
2619 // No idea why, but if an argument follows, it must adhere to the string ABNF production...
2626 c.bwritecodeline(smtp.C250Completed, smtp.SeOther00, "alrighty", nil)
2630func (c *conn) cmdQuit(p *parser) {
2634 c.writecodeline(smtp.C221Closing, smtp.SeOther00, "okay thanks bye", nil)