1// Package smtpserver implements an SMTP server for submission and incoming delivery of mail messages.
29 "golang.org/x/exp/maps"
31 "github.com/prometheus/client_golang/prometheus"
32 "github.com/prometheus/client_golang/prometheus/promauto"
34 "github.com/mjl-/bstore"
36 "github.com/mjl-/mox/config"
37 "github.com/mjl-/mox/dkim"
38 "github.com/mjl-/mox/dmarc"
39 "github.com/mjl-/mox/dmarcdb"
40 "github.com/mjl-/mox/dmarcrpt"
41 "github.com/mjl-/mox/dns"
42 "github.com/mjl-/mox/dsn"
43 "github.com/mjl-/mox/iprev"
44 "github.com/mjl-/mox/message"
45 "github.com/mjl-/mox/metrics"
46 "github.com/mjl-/mox/mlog"
47 "github.com/mjl-/mox/mox-"
48 "github.com/mjl-/mox/moxio"
49 "github.com/mjl-/mox/moxvar"
50 "github.com/mjl-/mox/publicsuffix"
51 "github.com/mjl-/mox/queue"
52 "github.com/mjl-/mox/ratelimit"
53 "github.com/mjl-/mox/scram"
54 "github.com/mjl-/mox/smtp"
55 "github.com/mjl-/mox/spf"
56 "github.com/mjl-/mox/store"
57 "github.com/mjl-/mox/tlsrptdb"
60// Most logging should be done through conn.log* functions.
61// Only use log in contexts without connection.
62var xlog = mlog.New("smtpserver")
64// We use panic and recover for error handling while executing commands.
65// These errors signal the connection must be closed.
66var errIO = errors.New("io error")
68// If set, regular delivery/submit is sidestepped, email is accepted and
69// delivered to the account named mox.
72var limiterConnectionRate, limiterConnections *ratelimit.Limiter
74// For delivery rate limiting. Variable because changed during tests.
75var limitIPMasked1MessagesPerMinute int = 500
76var limitIPMasked1SizePerMinute int64 = 1000 * 1024 * 1024
79 // Also called by tests, so they don't trigger the rate limiter.
85 // todo future: make these configurable
86 limiterConnectionRate = &ratelimit.Limiter{
87 WindowLimits: []ratelimit.WindowLimit{
90 Limits: [...]int64{300, 900, 2700},
94 limiterConnections = &ratelimit.Limiter{
95 WindowLimits: []ratelimit.WindowLimit{
97 Window: time.Duration(math.MaxInt64), // All of time.
98 Limits: [...]int64{30, 90, 270},
105 // Delays for bad/suspicious behaviour. Zero during tests.
106 badClientDelay = time.Second // Before reads and after 1-byte writes for probably spammers.
107 authFailDelay = time.Second // Response to authentication failure.
108 unknownRecipientsDelay = 5 * time.Second // Response when all recipients are unknown.
109 firstTimeSenderDelayDefault = 15 * time.Second // Before accepting message from first-time sender.
114 secode string // Enhanced code, but without the leading major int from code.
118 metricConnection = promauto.NewCounterVec(
119 prometheus.CounterOpts{
120 Name: "mox_smtpserver_connection_total",
121 Help: "Incoming SMTP connections.",
124 "kind", // "deliver" or "submit"
127 metricCommands = promauto.NewHistogramVec(
128 prometheus.HistogramOpts{
129 Name: "mox_smtpserver_command_duration_seconds",
130 Help: "SMTP server command duration and result codes in seconds.",
131 Buckets: []float64{0.001, 0.005, 0.01, 0.05, 0.100, 0.5, 1, 5, 10, 20, 30, 60, 120},
134 "kind", // "deliver" or "submit"
140 metricDelivery = promauto.NewCounterVec(
141 prometheus.CounterOpts{
142 Name: "mox_smtpserver_delivery_total",
143 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.",
150 // Similar between ../webmail/webmail.go:/metricSubmission and ../smtpserver/server.go:/metricSubmission
151 metricSubmission = promauto.NewCounterVec(
152 prometheus.CounterOpts{
153 Name: "mox_smtpserver_submission_total",
154 Help: "SMTP server incoming submission results, known values (those ending with error are server errors): ok, badmessage, badfrom, badheader, messagelimiterror, recipientlimiterror, localserveerror, queueerror.",
160 metricServerErrors = promauto.NewCounterVec(
161 prometheus.CounterOpts{
162 Name: "mox_smtpserver_errors_total",
163 Help: "SMTP server errors, known values: dkimsign, queuedsn.",
171var jitterRand = mox.NewPseudoRand()
173func durationDefault(delay *time.Duration, def time.Duration) time.Duration {
180// Listen initializes network listeners for incoming SMTP connection.
181// The listeners are stored for a later call to Serve.
183 names := maps.Keys(mox.Conf.Static.Listeners)
185 for _, name := range names {
186 listener := mox.Conf.Static.Listeners[name]
188 var tlsConfig *tls.Config
189 if listener.TLS != nil {
190 tlsConfig = listener.TLS.Config
193 maxMsgSize := listener.SMTPMaxMessageSize
195 maxMsgSize = config.DefaultMaxMsgSize
198 if listener.SMTP.Enabled {
199 hostname := mox.Conf.Static.HostnameDomain
200 if listener.Hostname != "" {
201 hostname = listener.HostnameDomain
203 port := config.Port(listener.SMTP.Port, 25)
204 for _, ip := range listener.IPs {
205 firstTimeSenderDelay := durationDefault(listener.SMTP.FirstTimeSenderDelay, firstTimeSenderDelayDefault)
206 listen1("smtp", name, ip, port, hostname, tlsConfig, false, false, maxMsgSize, false, listener.SMTP.RequireSTARTTLS, !listener.SMTP.NoRequireTLS, listener.SMTP.DNSBLZones, firstTimeSenderDelay)
209 if listener.Submission.Enabled {
210 hostname := mox.Conf.Static.HostnameDomain
211 if listener.Hostname != "" {
212 hostname = listener.HostnameDomain
214 port := config.Port(listener.Submission.Port, 587)
215 for _, ip := range listener.IPs {
216 listen1("submission", name, ip, port, hostname, tlsConfig, true, false, maxMsgSize, !listener.Submission.NoRequireSTARTTLS, !listener.Submission.NoRequireSTARTTLS, true, nil, 0)
220 if listener.Submissions.Enabled {
221 hostname := mox.Conf.Static.HostnameDomain
222 if listener.Hostname != "" {
223 hostname = listener.HostnameDomain
225 port := config.Port(listener.Submissions.Port, 465)
226 for _, ip := range listener.IPs {
227 listen1("submissions", name, ip, port, hostname, tlsConfig, true, true, maxMsgSize, true, true, true, nil, 0)
235func listen1(protocol, name, ip string, port int, hostname dns.Domain, tlsConfig *tls.Config, submission, xtls bool, maxMessageSize int64, requireTLSForAuth, requireTLSForDelivery, requireTLS bool, dnsBLs []dns.Domain, firstTimeSenderDelay time.Duration) {
236 addr := net.JoinHostPort(ip, fmt.Sprintf("%d", port))
237 if os.Getuid() == 0 {
238 xlog.Print("listening for smtp", mlog.Field("listener", name), mlog.Field("address", addr), mlog.Field("protocol", protocol))
240 network := mox.Network(ip)
241 ln, err := mox.Listen(network, addr)
243 xlog.Fatalx("smtp: listen for smtp", err, mlog.Field("protocol", protocol), mlog.Field("listener", name))
246 ln = tls.NewListener(ln, tlsConfig)
251 conn, err := ln.Accept()
253 xlog.Infox("smtp: accept", err, mlog.Field("protocol", protocol), mlog.Field("listener", name))
256 resolver := dns.StrictResolver{} // By leaving Pkg empty, it'll be set by each package that uses the resolver, e.g. spf/dkim/dmarc.
257 go serve(name, mox.Cid(), hostname, tlsConfig, conn, resolver, submission, xtls, maxMessageSize, requireTLSForAuth, requireTLSForDelivery, requireTLS, dnsBLs, firstTimeSenderDelay)
261 servers = append(servers, serve)
264// Serve starts serving on all listeners, launching a goroutine per listener.
266 for _, serve := range servers {
274 // OrigConn is the original (TCP) connection. We'll read from/write to conn, which
275 // can be wrapped in a tls.Server. We close origConn instead of conn because
276 // closing the TLS connection would send a TLS close notification, which may block
277 // for 5s if the server isn't reading it (because it is also sending it).
282 extRequireTLS bool // Whether to announce and allow the REQUIRETLS extension.
283 resolver dns.Resolver
286 tr *moxio.TraceReader // Kept for changing trace level during cmd/auth/data.
287 tw *moxio.TraceWriter
288 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.
289 lastlog time.Time // Used for printing the delta time since the previous logging for this connection.
291 tlsConfig *tls.Config
297 requireTLSForAuth bool
298 requireTLSForDelivery bool // If set, delivery is only allowed with TLS (STARTTLS), except if delivery is to a TLS reporting address.
299 cmd string // Current command.
300 cmdStart time.Time // Start of current command.
301 ncmds int // Number of commands processed. Used to abort connection when first incoming command is unknown/invalid.
303 firstTimeSenderDelay time.Duration
305 // If non-zero, taken into account during Read and Write. Set while processing DATA
306 // command, we don't want the entire delivery to take too long.
309 hello dns.IPDomain // Claimed remote name. Can be ip address for ehlo.
310 ehlo bool // If set, we had EHLO instead of HELO.
312 authFailed int // Number of failed auth attempts. For slowing down remote with many failures.
313 username string // Only when authenticated.
314 account *store.Account // Only when authenticated.
316 // We track good/bad message transactions to disconnect spammers trying to guess addresses.
320 // Message transaction.
322 requireTLS *bool // MAIL FROM with REQUIRETLS set.
323 has8bitmime bool // If MAIL FROM parameter BODY=8BITMIME was sent. Required for SMTPUTF8.
324 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.
325 recipients []rcptAccount
328type rcptAccount struct {
330 local bool // Whether recipient is a local user.
332 // Only valid for local delivery.
334 destination config.Destination
335 canonicalAddress string // Optional catchall part stripped and/or lowercased.
338func isClosed(err error) bool {
339 return errors.Is(err, errIO) || moxio.IsClosed(err)
342// completely reset connection state as if greeting has just been sent.
344func (c *conn) reset() {
346 c.hello = dns.IPDomain{}
348 if c.account != nil {
349 err := c.account.Close()
350 c.log.Check(err, "closing account")
356// for rset command, and a few more cases that reset the mail transaction state.
358func (c *conn) rset() {
361 c.has8bitmime = false
366func (c *conn) earliestDeadline(d time.Duration) time.Time {
367 e := time.Now().Add(d)
368 if !c.deadline.IsZero() && c.deadline.Before(e) {
374func (c *conn) xcheckAuth() {
375 if c.submission && c.account == nil {
377 xsmtpUserErrorf(smtp.C530SecurityRequired, smtp.SePol7Other0, "authentication required")
381func (c *conn) xtrace(level mlog.Level) func() {
387 c.tr.SetTrace(mlog.LevelTrace)
388 c.tw.SetTrace(mlog.LevelTrace)
392// setSlow marks the connection slow (or now), so reads are done with 3 second
393// delay for each read, and writes are done at 1 byte per second, to try to slow
395func (c *conn) setSlow(on bool) {
397 c.log.Debug("connection changed to slow")
398 } else if !on && c.slow {
399 c.log.Debug("connection restored to regular pace")
404// Write writes to the connection. It panics on i/o errors, which is handled by the
405// connection command loop.
406func (c *conn) Write(buf []byte) (int, error) {
414 // We set a single deadline for Write and Read. This may be a TLS connection.
415 // SetDeadline works on the underlying connection. If we wouldn't touch the read
416 // deadline, and only set the write deadline and do a bunch of writes, the TLS
417 // library would still have to do reads on the underlying connection, and may reach
418 // a read deadline that was set for some earlier read.
419 if err := c.conn.SetDeadline(c.earliestDeadline(30 * time.Second)); err != nil {
420 c.log.Errorx("setting deadline for write", err)
423 nn, err := c.conn.Write(buf[:chunk])
425 panic(fmt.Errorf("write: %s (%w)", err, errIO))
429 if len(buf) > 0 && badClientDelay > 0 {
430 mox.Sleep(mox.Context, badClientDelay)
436// Read reads from the connection. It panics on i/o errors, which is handled by the
437// connection command loop.
438func (c *conn) Read(buf []byte) (int, error) {
439 if c.slow && badClientDelay > 0 {
440 mox.Sleep(mox.Context, badClientDelay)
444 // See comment about Deadline instead of individual read/write deadlines at Write.
445 if err := c.conn.SetDeadline(c.earliestDeadline(30 * time.Second)); err != nil {
446 c.log.Errorx("setting deadline for read", err)
449 n, err := c.conn.Read(buf)
451 panic(fmt.Errorf("read: %s (%w)", err, errIO))
456// Cache of line buffers for reading commands.
458var bufpool = moxio.NewBufpool(8, 2*1024)
460func (c *conn) readline() string {
461 line, err := bufpool.Readline(c.r)
462 if err != nil && errors.Is(err, moxio.ErrLineTooLong) {
463 c.writecodeline(smtp.C500BadSyntax, smtp.SeProto5Other0, "line too long, smtp max is 512, we reached 2048", nil)
464 panic(fmt.Errorf("%s (%w)", err, errIO))
465 } else if err != nil {
466 panic(fmt.Errorf("%s (%w)", err, errIO))
471// Buffered-write command response line to connection with codes and msg.
472// Err is not sent to remote but is used for logging and can be empty.
473func (c *conn) bwritecodeline(code int, secode string, msg string, err error) {
476 ecode = fmt.Sprintf("%d.%s", code/100, secode)
478 metricCommands.WithLabelValues(c.kind(), c.cmd, fmt.Sprintf("%d", code), ecode).Observe(float64(time.Since(c.cmdStart)) / float64(time.Second))
479 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)))
486 // Separate by newline and wrap long lines.
487 lines := strings.Split(msg, "\n")
488 for i, line := range lines {
490 var prelen = 3 + 1 + len(ecode) + len(sep)
491 for prelen+len(line) > 510 {
493 for ; e > 400 && line[e] != ' '; e-- {
495 // 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.
496 c.bwritelinef("%d-%s%s%s", code, ecode, sep, line[:e])
500 if i < len(lines)-1 {
503 c.bwritelinef("%d%s%s%s%s", code, spdash, ecode, sep, line)
507// Buffered-write a formatted response line to connection.
508func (c *conn) bwritelinef(format string, args ...any) {
509 msg := fmt.Sprintf(format, args...)
510 fmt.Fprint(c.w, msg+"\r\n")
513// Flush pending buffered writes to connection.
514func (c *conn) xflush() {
515 c.w.Flush() // Errors will have caused a panic in Write.
518// Write (with flush) a response line with codes and message. err is not written, used for logging and can be nil.
519func (c *conn) writecodeline(code int, secode string, msg string, err error) {
520 c.bwritecodeline(code, secode, msg, err)
524// Write (with flush) a formatted response line to connection.
525func (c *conn) writelinef(format string, args ...any) {
526 c.bwritelinef(format, args...)
530var cleanClose struct{} // Sentinel value for panic/recover indicating clean close of connection.
532func serve(listenerName string, cid int64, hostname dns.Domain, tlsConfig *tls.Config, nc net.Conn, resolver dns.Resolver, submission, tls bool, maxMessageSize int64, requireTLSForAuth, requireTLSForDelivery, requireTLS bool, dnsBLs []dns.Domain, firstTimeSenderDelay time.Duration) {
533 var localIP, remoteIP net.IP
534 if a, ok := nc.LocalAddr().(*net.TCPAddr); ok {
537 // For net.Pipe, during tests.
538 localIP = net.ParseIP("127.0.0.10")
540 if a, ok := nc.RemoteAddr().(*net.TCPAddr); ok {
543 // For net.Pipe, during tests.
544 remoteIP = net.ParseIP("127.0.0.10")
551 submission: submission,
553 extRequireTLS: requireTLS,
556 tlsConfig: tlsConfig,
560 maxMessageSize: maxMessageSize,
561 requireTLSForAuth: requireTLSForAuth,
562 requireTLSForDelivery: requireTLSForDelivery,
564 firstTimeSenderDelay: firstTimeSenderDelay,
566 c.log = xlog.MoreFields(func() []mlog.Pair {
569 mlog.Field("cid", c.cid),
570 mlog.Field("delta", now.Sub(c.lastlog)),
573 if c.username != "" {
574 l = append(l, mlog.Field("username", c.username))
578 c.tr = moxio.NewTraceReader(c.log, "RC: ", c)
579 c.tw = moxio.NewTraceWriter(c.log, "LS: ", c)
580 c.r = bufio.NewReader(c.tr)
581 c.w = bufio.NewWriter(c.tw)
583 metricConnection.WithLabelValues(c.kind()).Inc()
584 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))
587 c.origConn.Close() // Close actual TCP socket, regardless of TLS on top.
588 c.conn.Close() // If TLS, will try to write alert notification to already closed socket, returning error quickly.
590 if c.account != nil {
591 err := c.account.Close()
592 c.log.Check(err, "closing account")
597 if x == nil || x == cleanClose {
598 c.log.Info("connection closed")
599 } else if err, ok := x.(error); ok && isClosed(err) {
600 c.log.Infox("connection closed", err)
602 c.log.Error("unhandled panic", mlog.Field("err", x))
604 metrics.PanicInc(metrics.Smtpserver)
609 case <-mox.Shutdown.Done():
611 c.writecodeline(smtp.C421ServiceUnavail, smtp.SeSys3NotAccepting2, "shutting down", nil)
616 if !limiterConnectionRate.Add(c.remoteIP, time.Now(), 1) {
617 c.writecodeline(smtp.C421ServiceUnavail, smtp.SePol7Other0, "connection rate from your ip or network too high, slow down please", nil)
621 // If remote IP/network resulted in too many authentication failures, refuse to serve.
622 if submission && !mox.LimiterFailedAuth.CanAdd(c.remoteIP, time.Now(), 1) {
623 metrics.AuthenticationRatelimitedInc("submission")
624 c.log.Debug("refusing connection due to many auth failures", mlog.Field("remoteip", c.remoteIP))
625 c.writecodeline(smtp.C421ServiceUnavail, smtp.SePol7Other0, "too many auth failures", nil)
629 if !limiterConnections.Add(c.remoteIP, time.Now(), 1) {
630 c.log.Debug("refusing connection due to many open connections", mlog.Field("remoteip", c.remoteIP))
631 c.writecodeline(smtp.C421ServiceUnavail, smtp.SePol7Other0, "too many open connections from your ip or network", nil)
634 defer limiterConnections.Add(c.remoteIP, time.Now(), -1)
636 // We register and unregister the original connection, in case c.conn is replaced
637 // with a TLS connection later on.
638 mox.Connections.Register(nc, "smtp", listenerName)
639 defer mox.Connections.Unregister(nc)
643 // We include the string ESMTP. https://cr.yp.to/smtp/greeting.html recommends it.
644 // Should not be too relevant nowadays, but does not hurt and default blackbox
645 // exporter SMTP health check expects it.
646 c.writelinef("%d %s ESMTP mox %s", smtp.C220ServiceReady, c.hostname.ASCII, moxvar.Version)
651 // If another command is present, don't flush our buffered response yet. Holding
652 // off will cause us to respond with a single packet.
655 buf, err := c.r.Peek(n)
656 if err == nil && bytes.IndexByte(buf, '\n') >= 0 {
664var commands = map[string]func(c *conn, p *parser){
665 "helo": (*conn).cmdHelo,
666 "ehlo": (*conn).cmdEhlo,
667 "starttls": (*conn).cmdStarttls,
668 "auth": (*conn).cmdAuth,
669 "mail": (*conn).cmdMail,
670 "rcpt": (*conn).cmdRcpt,
671 "data": (*conn).cmdData,
672 "rset": (*conn).cmdRset,
673 "vrfy": (*conn).cmdVrfy,
674 "expn": (*conn).cmdExpn,
675 "help": (*conn).cmdHelp,
676 "noop": (*conn).cmdNoop,
677 "quit": (*conn).cmdQuit,
680func command(c *conn) {
696 if errors.As(err, &serr) {
697 c.writecodeline(serr.code, serr.secode, fmt.Sprintf("%s (%s)", serr.errmsg, mox.ReceivedID(c.cid)), serr.err)
702 // Other type of panic, we pass it on, aborting the connection.
703 c.log.Errorx("command panic", err)
708 // todo future: we could wait for either a line or shutdown, and just close the connection on shutdown.
711 t := strings.SplitN(line, " ", 2)
717 cmdl := strings.ToLower(cmd)
719 // 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
722 case <-mox.Shutdown.Done():
724 c.writecodeline(smtp.C421ServiceUnavail, smtp.SeSys3NotAccepting2, "shutting down", nil)
730 c.cmdStart = time.Now()
732 p := newParser(args, c.smtputf8, c)
733 fn, ok := commands[cmdl]
737 // Other side is likely speaking something else than SMTP, send error message and
738 // stop processing because there is a good chance whatever they sent has multiple
740 c.writecodeline(smtp.C500BadSyntax, smtp.SeProto5Syntax2, "please try again speaking smtp", nil)
744 xsmtpUserErrorf(smtp.C500BadSyntax, smtp.SeProto5BadCmdOrSeq1, "unknown command")
750// For use in metric labels.
751func (c *conn) kind() string {
758func (c *conn) xneedHello() {
759 if c.hello.IsZero() {
760 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "no ehlo/helo yet")
764// If smtp server is configured to require TLS for all mail delivery (except to TLS
765// reporting address), abort command.
766func (c *conn) xneedTLSForDelivery(rcpt smtp.Path) {
767 // For TLS reports, we allow the message in even without TLS, because there may be
769 if c.requireTLSForDelivery && !c.tls && !isTLSReportRecipient(rcpt) {
771 xsmtpUserErrorf(smtp.C530SecurityRequired, smtp.SePol7Other0, "STARTTLS required for mail delivery")
775func isTLSReportRecipient(rcpt smtp.Path) bool {
776 _, _, dest, err := mox.FindAccount(rcpt.Localpart, rcpt.IPDomain.Domain, false)
777 return err == nil && (dest.HostTLSReports || dest.DomainTLSReports)
780func (c *conn) cmdHelo(p *parser) {
784func (c *conn) cmdEhlo(p *parser) {
789func (c *conn) cmdHello(p *parser, ehlo bool) {
790 var remote dns.IPDomain
791 if c.submission && !moxvar.Pedantic {
792 // Mail clients regularly put bogus information in the hostname/ip. For submission,
793 // the value is of no use, so there is not much point in annoying the user with
794 // errors they cannot fix themselves. Except when in pedantic mode.
795 remote = dns.IPDomain{IP: c.remoteIP}
799 remote = p.xipdomain(true)
801 remote = dns.IPDomain{Domain: p.xdomain()}
803 // Verify a remote domain name has an A or AAAA record, CNAME not allowed.
../rfc/5321:722
804 cidctx := context.WithValue(mox.Context, mlog.CidKey, c.cid)
805 ctx, cancel := context.WithTimeout(cidctx, time.Minute)
806 _, _, err := c.resolver.LookupIPAddr(ctx, remote.Domain.ASCII+".")
808 if dns.IsNotFound(err) {
809 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeProto5Other0, "your ehlo domain does not resolve to an IP address")
811 // For success or temporary resolve errors, we'll just continue.
814 // Though a few paragraphs earlier is a claim additional data can occur for address
815 // literals (IP addresses), although the ABNF in that document does not allow it.
816 // We allow additional text, but only if space-separated.
817 if len(remote.IP) > 0 && p.space() {
829 // https://www.iana.org/assignments/mail-parameters/mail-parameters.xhtml
831 c.bwritelinef("250-%s", c.hostname.ASCII)
835 if !c.tls && c.tlsConfig != nil {
837 c.bwritelinef("250-STARTTLS")
838 } else if c.extRequireTLS {
841 c.bwritelinef("250-REQUIRETLS")
845 if c.tls || !c.requireTLSForAuth {
846 c.bwritelinef("250-AUTH SCRAM-SHA-256 SCRAM-SHA-1 CRAM-MD5 PLAIN LOGIN")
848 c.bwritelinef("250-AUTH ")
852 // todo future? c.writelinef("250-DSN")
859func (c *conn) cmdStarttls(p *parser) {
865 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "already speaking tls")
867 if c.account != nil {
868 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "cannot starttls after authentication")
871 // We don't want to do TLS on top of c.r because it also prints protocol traces: We
872 // don't want to log the TLS stream. So we'll do TLS on the underlying connection,
873 // but make sure any bytes already read and in the buffer are used for the TLS
876 if n := c.r.Buffered(); n > 0 {
877 conn = &moxio.PrefixConn{
878 PrefixReader: io.LimitReader(c.r, int64(n)),
883 // We add the cid to the output, to help debugging in case of a failing TLS connection.
884 c.writecodeline(smtp.C220ServiceReady, smtp.SeOther00, "go! ("+mox.ReceivedID(c.cid)+")", nil)
885 tlsConn := tls.Server(conn, c.tlsConfig)
886 cidctx := context.WithValue(mox.Context, mlog.CidKey, c.cid)
887 ctx, cancel := context.WithTimeout(cidctx, time.Minute)
889 c.log.Debug("starting tls server handshake")
890 if err := tlsConn.HandshakeContext(ctx); err != nil {
891 panic(fmt.Errorf("starttls handshake: %s (%w)", err, errIO))
894 tlsversion, ciphersuite := mox.TLSInfo(tlsConn)
895 c.log.Debug("tls server handshake done", mlog.Field("tls", tlsversion), mlog.Field("ciphersuite", ciphersuite))
897 c.tr = moxio.NewTraceReader(c.log, "RC: ", c)
898 c.tw = moxio.NewTraceWriter(c.log, "LS: ", c)
899 c.r = bufio.NewReader(c.tr)
900 c.w = bufio.NewWriter(c.tw)
907func (c *conn) cmdAuth(p *parser) {
911 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "authentication only allowed on submission ports")
913 if c.account != nil {
915 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "already authenticated")
917 if c.mailFrom != nil {
919 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "authentication not allowed during mail transaction")
922 // 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).
924 // For many failed auth attempts, slow down verification attempts.
925 // Dropping the connection could also work, but more so when we have a connection rate limiter.
927 if c.authFailed > 3 && authFailDelay > 0 {
929 mox.Sleep(mox.Context, time.Duration(c.authFailed-3)*authFailDelay)
931 c.authFailed++ // Compensated on success.
933 // On the 3rd failed authentication, start responding slowly. Successful auth will
934 // cause fast responses again.
935 if c.authFailed >= 3 {
940 var authVariant string
941 authResult := "error"
943 metrics.AuthenticationInc("submission", authVariant, authResult)
946 mox.LimiterFailedAuth.Reset(c.remoteIP, time.Now())
948 mox.LimiterFailedAuth.Add(c.remoteIP, time.Now(), 1)
954 mech := p.xsaslMech()
956 xreadInitial := func() []byte {
960 // todo future: handle max length of 12288 octets and return proper responde codes otherwise
../rfc/4954:253
964 authResult = "aborted"
965 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5Other0, "authentication aborted")
969 if !moxvar.Pedantic {
970 // Windows Mail 16005.14326.21606.0 sends two spaces between "AUTH PLAIN" and the
978 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5Syntax2, "missing initial auth base64 parameter after space")
979 } else if auth == "=" {
981 auth = "" // Base64 decode below will result in empty buffer.
984 buf, err := base64.StdEncoding.DecodeString(auth)
987 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5Syntax2, "invalid base64: %s", err)
992 xreadContinuation := func() []byte {
995 authResult = "aborted"
996 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5Other0, "authentication aborted")
998 buf, err := base64.StdEncoding.DecodeString(line)
1001 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5Syntax2, "invalid base64: %s", err)
1008 authVariant = "plain"
1012 if !c.tls && c.requireTLSForAuth {
1013 xsmtpUserErrorf(smtp.C538EncReqForAuth, smtp.SePol7EncReqForAuth11, "authentication requires tls")
1016 // Password is in line in plain text, so hide it.
1017 defer c.xtrace(mlog.LevelTraceauth)()
1018 buf := xreadInitial()
1019 c.xtrace(mlog.LevelTrace) // Restore.
1020 plain := bytes.Split(buf, []byte{0})
1021 if len(plain) != 3 {
1022 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5BadParams4, "auth data should have 3 nul-separated tokens, got %d", len(plain))
1024 authz := string(plain[0])
1025 authc := string(plain[1])
1026 password := string(plain[2])
1028 if authz != "" && authz != authc {
1029 authResult = "badcreds"
1030 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "cannot assume other role")
1033 acc, err := store.OpenEmailAuth(authc, password)
1034 if err != nil && errors.Is(err, store.ErrUnknownCredentials) {
1036 authResult = "badcreds"
1037 c.log.Info("failed authentication attempt", mlog.Field("username", authc), mlog.Field("remote", c.remoteIP))
1038 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass")
1040 xcheckf(err, "verifying credentials")
1048 c.writecodeline(smtp.C235AuthSuccess, smtp.SePol7Other0, "nice", nil)
1051 // LOGIN is obsoleted in favor of PLAIN, only implemented to support legacy
1052 // clients, see Internet-Draft (I-D):
1053 // https://datatracker.ietf.org/doc/html/draft-murchison-sasl-login-00
1055 authVariant = "login"
1059 if !c.tls && c.requireTLSForAuth {
1060 xsmtpUserErrorf(smtp.C538EncReqForAuth, smtp.SePol7EncReqForAuth11, "authentication requires tls")
1063 // Read user name. The I-D says the client should ignore the server challenge, we
1064 // send an empty one.
1065 // I-D says maximum length must be 64 bytes. We allow more, for long user names
1067 username := string(xreadInitial())
1069 // Again, client should ignore the challenge, we send the same as the example in
1071 c.writelinef("%d %s", smtp.C334ContinueAuth, base64.StdEncoding.EncodeToString([]byte("Password")))
1073 // Password is in line in plain text, so hide it.
1074 defer c.xtrace(mlog.LevelTraceauth)()
1075 password := string(xreadContinuation())
1076 c.xtrace(mlog.LevelTrace) // Restore.
1078 acc, err := store.OpenEmailAuth(username, password)
1079 if err != nil && errors.Is(err, store.ErrUnknownCredentials) {
1081 authResult = "badcreds"
1082 c.log.Info("failed authentication attempt", mlog.Field("username", username), mlog.Field("remote", c.remoteIP))
1083 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass")
1085 xcheckf(err, "verifying credentials")
1091 c.username = username
1093 c.writecodeline(smtp.C235AuthSuccess, smtp.SePol7Other0, "hello ancient smtp implementation", nil)
1096 authVariant = strings.ToLower(mech)
1101 chal := fmt.Sprintf("<%d.%d@%s>", uint64(mox.CryptoRandInt()), time.Now().UnixNano(), mox.Conf.Static.HostnameDomain.ASCII)
1102 c.writelinef("%d %s", smtp.C334ContinueAuth, base64.StdEncoding.EncodeToString([]byte(chal)))
1104 resp := xreadContinuation()
1105 t := strings.Split(string(resp), " ")
1106 if len(t) != 2 || len(t[1]) != 2*md5.Size {
1107 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5BadParams4, "malformed cram-md5 response")
1110 c.log.Debug("cram-md5 auth", mlog.Field("address", addr))
1111 acc, _, err := store.OpenEmail(addr)
1113 if errors.Is(err, store.ErrUnknownCredentials) {
1114 c.log.Info("failed authentication attempt", mlog.Field("username", addr), mlog.Field("remote", c.remoteIP))
1115 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass")
1118 xcheckf(err, "looking up address")
1122 c.log.Check(err, "closing account")
1125 var ipadhash, opadhash hash.Hash
1126 acc.WithRLock(func() {
1127 err := acc.DB.Read(context.TODO(), func(tx *bstore.Tx) error {
1128 password, err := bstore.QueryTx[store.Password](tx).Get()
1129 if err == bstore.ErrAbsent {
1130 c.log.Info("failed authentication attempt", mlog.Field("username", addr), mlog.Field("remote", c.remoteIP))
1131 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass")
1137 ipadhash = password.CRAMMD5.Ipad
1138 opadhash = password.CRAMMD5.Opad
1141 xcheckf(err, "tx read")
1143 if ipadhash == nil || opadhash == nil {
1144 c.log.Info("cram-md5 auth attempt without derived secrets set, save password again to store secrets", mlog.Field("username", addr))
1145 c.log.Info("failed authentication attempt", mlog.Field("username", addr), mlog.Field("remote", c.remoteIP))
1146 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass")
1150 ipadhash.Write([]byte(chal))
1151 opadhash.Write(ipadhash.Sum(nil))
1152 digest := fmt.Sprintf("%x", opadhash.Sum(nil))
1154 c.log.Info("failed authentication attempt", mlog.Field("username", addr), mlog.Field("remote", c.remoteIP))
1155 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass")
1162 acc = nil // Cancel cleanup.
1165 c.writecodeline(smtp.C235AuthSuccess, smtp.SePol7Other0, "nice", nil)
1167 case "SCRAM-SHA-1", "SCRAM-SHA-256":
1168 // 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?
1169 // todo: use single implementation between ../imapserver/server.go and ../smtpserver/server.go
1171 authVariant = strings.ToLower(mech)
1172 var h func() hash.Hash
1173 if authVariant == "scram-sha-1" {
1179 // Passwords cannot be retrieved or replayed from the trace.
1181 c0 := xreadInitial()
1182 ss, err := scram.NewServer(h, c0)
1183 xcheckf(err, "starting scram")
1184 c.log.Debug("scram auth", mlog.Field("authentication", ss.Authentication))
1185 acc, _, err := store.OpenEmail(ss.Authentication)
1187 // todo: we could continue scram with a generated salt, deterministically generated
1188 // from the username. that way we don't have to store anything but attackers cannot
1189 // learn if an account exists. same for absent scram saltedpassword below.
1190 c.log.Info("failed authentication attempt", mlog.Field("username", ss.Authentication), mlog.Field("remote", c.remoteIP))
1191 xsmtpUserErrorf(smtp.C454TempAuthFail, smtp.SeSys3Other0, "scram not possible")
1196 c.log.Check(err, "closing account")
1199 if ss.Authorization != "" && ss.Authorization != ss.Authentication {
1200 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "authentication with authorization for different user not supported")
1202 var xscram store.SCRAM
1203 acc.WithRLock(func() {
1204 err := acc.DB.Read(context.TODO(), func(tx *bstore.Tx) error {
1205 password, err := bstore.QueryTx[store.Password](tx).Get()
1206 if authVariant == "scram-sha-1" {
1207 xscram = password.SCRAMSHA1
1209 xscram = password.SCRAMSHA256
1211 if err == bstore.ErrAbsent || err == nil && (len(xscram.Salt) == 0 || xscram.Iterations == 0 || len(xscram.SaltedPassword) == 0) {
1212 c.log.Info("scram auth attempt without derived secrets set, save password again to store secrets", mlog.Field("address", ss.Authentication))
1213 c.log.Info("failed authentication attempt", mlog.Field("username", ss.Authentication), mlog.Field("remote", c.remoteIP))
1214 xsmtpUserErrorf(smtp.C454TempAuthFail, smtp.SeSys3Other0, "scram not possible")
1216 xcheckf(err, "fetching credentials")
1219 xcheckf(err, "read tx")
1221 s1, err := ss.ServerFirst(xscram.Iterations, xscram.Salt)
1222 xcheckf(err, "scram first server step")
1223 c.writelinef("%d %s", smtp.C334ContinueAuth, base64.StdEncoding.EncodeToString([]byte(s1))) //
../rfc/4954:187
1224 c2 := xreadContinuation()
1225 s3, err := ss.Finish(c2, xscram.SaltedPassword)
1227 c.writelinef("%d %s", smtp.C334ContinueAuth, base64.StdEncoding.EncodeToString([]byte(s3))) //
../rfc/4954:187
1230 c.readline() // Should be "*" for cancellation.
1231 if errors.Is(err, scram.ErrInvalidProof) {
1232 authResult = "badcreds"
1233 c.log.Info("failed authentication attempt", mlog.Field("username", ss.Authentication), mlog.Field("remote", c.remoteIP))
1234 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad credentials")
1236 xcheckf(err, "server final")
1240 // The message should be empty. todo: should we require it is empty?
1247 acc = nil // Cancel cleanup.
1248 c.username = ss.Authentication
1250 c.writecodeline(smtp.C235AuthSuccess, smtp.SePol7Other0, "nice", nil)
1254 xsmtpUserErrorf(smtp.C504ParamNotImpl, smtp.SeProto5BadParams4, "mechanism %s not supported", mech)
1259func (c *conn) cmdMail(p *parser) {
1260 // requirements for maximum line length:
1262 // todo future: enforce?
1264 if c.transactionBad > 10 && c.transactionGood == 0 {
1265 // If we get many bad transactions, it's probably a spammer that is guessing user names.
1266 // Useful in combination with rate limiting.
1268 c.writecodeline(smtp.C550MailboxUnavail, smtp.SeAddr1Other0, "too many failures", nil)
1274 if c.mailFrom != nil {
1276 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "already have MAIL")
1278 // Ensure clear transaction state on failure.
1289 // Allow illegal space for submission only, not for regular SMTP. Microsoft Outlook
1290 // 365 Apps for Enterprise sends it.
1291 if c.submission && !moxvar.Pedantic {
1294 rawRevPath := p.xrawReversePath()
1295 paramSeen := map[string]bool{}
1298 key := p.xparamKeyword()
1300 K := strings.ToUpper(key)
1303 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5BadParams4, "duplicate param %q", key)
1311 if size > c.maxMessageSize {
1313 ecode := smtp.SeSys3MsgLimitExceeded4
1314 if size < config.DefaultMaxMsgSize {
1315 ecode = smtp.SeMailbox2MsgLimitExceeded3
1317 xsmtpUserErrorf(smtp.C552MailboxFull, ecode, "message too large")
1319 // We won't verify the message is exactly the size the remote claims. Buf if it is
1320 // larger, we'll abort the transaction when remote crosses the boundary.
1324 v := p.xparamValue()
1325 switch strings.ToUpper(v) {
1327 c.has8bitmime = false
1329 c.has8bitmime = true
1331 xsmtpUserErrorf(smtp.C555UnrecognizedAddrParams, smtp.SeProto5BadParams4, "unrecognized parameter %q", key)
1336 // We act as if we don't trust the client to specify a mailbox. Instead, we always
1337 // check the rfc5321.mailfrom and rfc5322.from before accepting the submission.
1341 // 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
1352 xsmtpUserErrorf(smtp.C530SecurityRequired, smtp.SePol7EncNeeded10, "requiretls only allowed on tls-encrypted connections")
1353 } else if !c.extRequireTLS {
1354 xsmtpUserErrorf(smtp.C555UnrecognizedAddrParams, smtp.SeSys3NotSupported3, "REQUIRETLS not allowed for this connection")
1360 xsmtpUserErrorf(smtp.C555UnrecognizedAddrParams, smtp.SeSys3NotSupported3, "unrecognized parameter %q", key)
1364 // We now know if we have to parse the address with support for utf8.
1365 pp := newParser(rawRevPath, c.smtputf8, c)
1366 rpath := pp.xbareReversePath()
1371 // For submission, check if reverse path is allowed. I.e. authenticated account
1372 // must have the rpath configured. We do a check again on rfc5322.from during DATA.
1373 rpathAllowed := func() bool {
1378 accName, _, _, err := mox.FindAccount(rpath.Localpart, rpath.IPDomain.Domain, false)
1379 return err == nil && accName == c.account.Name
1382 if !c.submission && !rpath.IPDomain.Domain.IsZero() {
1383 // If rpath domain has null MX record or is otherwise not accepting email, reject.
1386 cidctx := context.WithValue(mox.Context, mlog.CidKey, c.cid)
1387 ctx, cancel := context.WithTimeout(cidctx, time.Minute)
1388 valid, err := checkMXRecords(ctx, c.resolver, rpath.IPDomain.Domain)
1391 c.log.Infox("temporary reject for temporary mx lookup error", err)
1392 xsmtpServerErrorf(codes{smtp.C451LocalErr, smtp.SeNet4Other0}, "cannot verify mx records for mailfrom domain")
1394 c.log.Info("permanent reject because mailfrom domain does not accept mail")
1395 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SePol7SenderHasNullMX27, "mailfrom domain not configured for mail")
1399 if c.submission && (len(rpath.IPDomain.IP) > 0 || !rpathAllowed()) {
1401 c.log.Info("submission with unconfigured mailfrom", mlog.Field("user", c.username), mlog.Field("mailfrom", rpath.String()))
1402 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SePol7DeliveryUnauth1, "must match authenticated user")
1403 } else if !c.submission && len(rpath.IPDomain.IP) > 0 {
1404 // todo future: allow if the IP is the same as this connection is coming from? does later code allow this?
1405 c.log.Info("delivery from address without domain", mlog.Field("mailfrom", rpath.String()))
1406 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SePol7Other0, "domain name required")
1409 if Localserve && strings.HasPrefix(string(rpath.Localpart), "mailfrom") {
1410 c.xlocalserveError(rpath.Localpart)
1415 c.bwritecodeline(smtp.C250Completed, smtp.SeAddr1Other0, "looking good", nil)
1419func (c *conn) cmdRcpt(p *parser) {
1422 if c.mailFrom == nil {
1424 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "missing MAIL FROM")
1430 // Allow illegal space for submission only, not for regular SMTP. Microsoft Outlook
1431 // 365 Apps for Enterprise sends it.
1432 if c.submission && !moxvar.Pedantic {
1436 if p.take("<POSTMASTER>") {
1437 fpath = smtp.Path{Localpart: "postmaster"}
1439 fpath = p.xforwardPath()
1443 key := p.xparamKeyword()
1444 // K := strings.ToUpper(key)
1447 xsmtpUserErrorf(smtp.C555UnrecognizedAddrParams, smtp.SeSys3NotSupported3, "unrecognized parameter %q", key)
1451 // Check if TLS is enabled if required. It's not great that sender/recipient
1452 // addresses may have been exposed in plaintext before we can reject delivery. The
1453 // recipient could be the tls reporting addresses, which must always be able to
1454 // receive in plain text.
1455 c.xneedTLSForDelivery(fpath)
1457 // todo future: for submission, should we do explicit verification that domains are fully qualified? also for mail from.
../rfc/6409:420
1459 if len(c.recipients) >= 100 {
1461 xsmtpUserErrorf(smtp.C452StorageFull, smtp.SeProto5TooManyRcpts3, "max of 100 recipients reached")
1464 // We don't want to allow delivery to multiple recipients with a null reverse path.
1465 // Why would anyone send like that? Null reverse path is intended for delivery
1466 // notifications, they should go to a single recipient.
1467 if !c.submission && len(c.recipients) > 0 && c.mailFrom.IsZero() {
1468 xsmtpUserErrorf(smtp.C452StorageFull, smtp.SeProto5TooManyRcpts3, "only one recipient allowed with null reverse address")
1471 // Do not accept multiple recipients if remote does not pass SPF. Because we don't
1472 // want to generate DSNs to unverified domains. This is the moment we
1473 // can refuse individual recipients, DATA will be too late. Because mail
1474 // servers must handle a max recipient limit gracefully and still send to the
1475 // recipients that are accepted, this should not cause problems. Though we are in
1476 // violation because the limit must be >= 100.
1480 if !c.submission && len(c.recipients) == 1 && !Localserve {
1481 // note: because of check above, mailFrom cannot be the null address.
1483 d := c.mailFrom.IPDomain.Domain
1485 // todo: use this spf result for DATA.
1486 spfArgs := spf.Args{
1487 RemoteIP: c.remoteIP,
1488 MailFromLocalpart: c.mailFrom.Localpart,
1490 HelloDomain: c.hello,
1492 LocalHostname: c.hostname,
1494 cidctx := context.WithValue(mox.Context, mlog.CidKey, c.cid)
1495 spfctx, spfcancel := context.WithTimeout(cidctx, time.Minute)
1497 receivedSPF, _, _, _, err := spf.Verify(spfctx, c.resolver, spfArgs)
1500 c.log.Errorx("spf verify for multiple recipients", err)
1502 pass = receivedSPF.Identity == spf.ReceivedMailFrom && receivedSPF.Result == spf.StatusPass
1505 xsmtpUserErrorf(smtp.C452StorageFull, smtp.SeProto5TooManyRcpts3, "only one recipient allowed without spf pass")
1510 if strings.HasPrefix(string(fpath.Localpart), "rcptto") {
1511 c.xlocalserveError(fpath.Localpart)
1514 // If account or destination doesn't exist, it will be handled during delivery. For
1515 // submissions, which is the common case, we'll deliver to the logged in user,
1516 // which is typically the mox user.
1517 acc, _ := mox.Conf.Account("mox")
1518 dest := acc.Destinations["mox@localhost"]
1519 c.recipients = append(c.recipients, rcptAccount{fpath, true, "mox", dest, "mox@localhost"})
1520 } else if len(fpath.IPDomain.IP) > 0 {
1522 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeAddr1UnknownDestMailbox1, "not accepting email for ip")
1524 c.recipients = append(c.recipients, rcptAccount{fpath, false, "", config.Destination{}, ""})
1525 } else if accountName, canonical, addr, err := mox.FindAccount(fpath.Localpart, fpath.IPDomain.Domain, true); err == nil {
1527 c.recipients = append(c.recipients, rcptAccount{fpath, true, accountName, addr, canonical})
1528 } else if errors.Is(err, mox.ErrDomainNotFound) {
1530 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeAddr1UnknownDestMailbox1, "not accepting email for domain")
1532 // We'll be delivering this email.
1533 c.recipients = append(c.recipients, rcptAccount{fpath, false, "", config.Destination{}, ""})
1534 } else if errors.Is(err, mox.ErrAccountNotFound) {
1536 // For submission, we're transparent about which user exists. Should be fine for the typical small-scale deploy.
1538 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeAddr1UnknownDestMailbox1, "no such user")
1540 // We pretend to accept. We don't want to let remote know the user does not exist
1541 // until after DATA. Because then remote has committed to sending a message.
1542 // note: not local for !c.submission is the signal this address is in error.
1543 c.recipients = append(c.recipients, rcptAccount{fpath, false, "", config.Destination{}, ""})
1545 c.log.Errorx("looking up account for delivery", err, mlog.Field("rcptto", fpath))
1546 xsmtpServerErrorf(codes{smtp.C451LocalErr, smtp.SeSys3Other0}, "error processing")
1548 c.bwritecodeline(smtp.C250Completed, smtp.SeAddr1Other0, "now on the list", nil)
1552func (c *conn) cmdData(p *parser) {
1555 if c.mailFrom == nil {
1557 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "missing MAIL FROM")
1559 if len(c.recipients) == 0 {
1561 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "missing RCPT TO")
1567 // 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.
1569 // Entire delivery should be done within 30 minutes, or we abort.
1570 cidctx := context.WithValue(mox.Context, mlog.CidKey, c.cid)
1571 cmdctx, cmdcancel := context.WithTimeout(cidctx, 30*time.Minute)
1573 // Deadline is taken into account by Read and Write.
1574 c.deadline, _ = cmdctx.Deadline()
1576 c.deadline = time.Time{}
1580 c.writelinef("354 see you at the bare dot")
1582 // Mark as tracedata.
1583 defer c.xtrace(mlog.LevelTracedata)()
1585 // We read the data into a temporary file. We limit the size and do basic analysis while reading.
1586 dataFile, err := store.CreateMessageTemp("smtp-deliver")
1588 xsmtpServerErrorf(errCodes(smtp.C451LocalErr, smtp.SeSys3Other0, err), "creating temporary file for message: %s", err)
1590 defer store.CloseRemoveTempFile(c.log, dataFile, "smtpserver delivered message")
1591 msgWriter := message.NewWriter(dataFile)
1592 dr := smtp.NewDataReader(c.r)
1593 n, err := io.Copy(&limitWriter{maxSize: c.maxMessageSize, w: msgWriter}, dr)
1594 c.xtrace(mlog.LevelTrace) // Restore.
1596 if errors.Is(err, errMessageTooLarge) {
1598 ecode := smtp.SeSys3MsgLimitExceeded4
1599 if n < config.DefaultMaxMsgSize {
1600 ecode = smtp.SeMailbox2MsgLimitExceeded3
1602 c.writecodeline(smtp.C451LocalErr, ecode, fmt.Sprintf("error copying data to file (%s)", mox.ReceivedID(c.cid)), err)
1603 panic(fmt.Errorf("remote sent too much DATA: %w", errIO))
1606 // Something is failing on our side. We want to let remote know. So write an error response,
1607 // then discard the remaining data so the remote client is more likely to see our
1608 // response. Our write is synchronous, there is a risk no window/buffer space is
1609 // available and our write blocks us from reading remaining data, leading to
1610 // deadlock. We have a timeout on our connection writes though, so worst case we'll
1611 // abort the connection due to expiration.
1612 c.writecodeline(smtp.C451LocalErr, smtp.SeSys3Other0, fmt.Sprintf("error copying data to file (%s)", mox.ReceivedID(c.cid)), err)
1613 io.Copy(io.Discard, dr)
1617 // Basic sanity checks on messages before we send them out to the world. Just
1618 // trying to be strict in what we do to others and liberal in what we accept.
1620 if !msgWriter.HaveBody {
1622 xsmtpUserErrorf(smtp.C554TransactionFailed, smtp.SeMsg6Other0, "message requires both header and body section")
1624 // Check only for pedantic mode because ios mail will attempt to send smtputf8 with
1625 // non-ascii in message from localpart without using 8bitmime.
1626 if moxvar.Pedantic && msgWriter.Has8bit && !c.has8bitmime {
1628 xsmtpUserErrorf(smtp.C500BadSyntax, smtp.SeMsg6Other0, "message with non-us-ascii requires 8bitmime extension")
1632 if Localserve && moxvar.Pedantic {
1633 // Require that message can be parsed fully.
1634 p, err := message.Parse(c.log, false, dataFile)
1636 err = p.Walk(c.log, nil)
1640 xsmtpUserErrorf(smtp.C554TransactionFailed, smtp.SeMsg6Other0, "malformed message: %v", err)
1644 // Prepare "Received" header.
1648 var iprevStatus iprev.Status // Only for delivery, not submission.
1649 var iprevAuthentic bool
1651 // Hide internal hosts.
1652 // todo future: make this a config option, where admins specify ip ranges that they don't want exposed. also see
../rfc/5321:4321
1653 recvFrom = message.HeaderCommentDomain(mox.Conf.Static.HostnameDomain, c.smtputf8)
1655 if len(c.hello.IP) > 0 {
1656 recvFrom = smtp.AddressLiteral(c.hello.IP)
1658 // ASCII-only version added after the extended-domain syntax below, because the
1659 // comment belongs to "BY" which comes immediately after "FROM".
1660 recvFrom = c.hello.Domain.XName(c.smtputf8)
1662 iprevctx, iprevcancel := context.WithTimeout(cmdctx, time.Minute)
1664 var revNames []string
1665 iprevStatus, revName, revNames, iprevAuthentic, err = iprev.Lookup(iprevctx, c.resolver, c.remoteIP)
1668 c.log.Infox("reverse-forward lookup", err, mlog.Field("remoteip", c.remoteIP))
1670 c.log.Debug("dns iprev check", mlog.Field("addr", c.remoteIP), mlog.Field("status", iprevStatus))
1674 } else if len(revNames) > 0 {
1677 name = strings.TrimSuffix(name, ".")
1679 if name != "" && name != c.hello.Domain.XName(c.smtputf8) {
1680 recvFrom += name + " "
1682 recvFrom += smtp.AddressLiteral(c.remoteIP) + ")"
1683 if c.smtputf8 && c.hello.Domain.Unicode != "" {
1684 recvFrom += " (" + c.hello.Domain.ASCII + ")"
1687 recvBy := mox.Conf.Static.HostnameDomain.XName(c.smtputf8)
1688 recvBy += " (" + smtp.AddressLiteral(c.localIP) + ")" // todo: hide ip if internal?
1689 if c.smtputf8 && mox.Conf.Static.HostnameDomain.Unicode != "" {
1690 // This syntax is part of "VIA".
1691 recvBy += " (" + mox.Conf.Static.HostnameDomain.ASCII + ")"
1704 if c.account != nil {
1709 // Assume transaction does not succeed. If it does, we'll compensate.
1712 recvHdrFor := func(rcptTo string) string {
1713 recvHdr := &message.HeaderWriter{}
1714 // For additional Received-header clauses, see:
1715 // https://www.iana.org/assignments/mail-parameters/mail-parameters.xhtml#table-mail-parameters-8
1717 if c.requireTLS != nil && *c.requireTLS {
1719 withComment = " (requiretls)"
1721 recvHdr.Add(" ", "Received:", "from", recvFrom, "by", recvBy, "via", "tcp", "with", with+withComment, "id", mox.ReceivedID(c.cid)) //
../rfc/5321:3158
1723 tlsConn := c.conn.(*tls.Conn)
1724 tlsComment := message.TLSReceivedComment(c.log, tlsConn.ConnectionState())
1725 recvHdr.Add(" ", tlsComment...)
1727 recvHdr.Add(" ", "for", "<"+rcptTo+">;", time.Now().Format(message.RFC5322Z))
1728 return recvHdr.String()
1731 // Submission is easiest because user is trusted. Far fewer checks to make. So
1732 // handle it first, and leave the rest of the function for handling wild west
1733 // internet traffic.
1735 c.submit(cmdctx, recvHdrFor, msgWriter, dataFile)
1737 c.deliver(cmdctx, recvHdrFor, msgWriter, iprevStatus, iprevAuthentic, dataFile)
1741// Check if a message has unambiguous "TLS-Required: No" header. Messages must not
1742// contain multiple TLS-Required headers. The only valid value is "no". But we'll
1743// accept multiple headers as long as all they are all "no".
1745func hasTLSRequiredNo(h textproto.MIMEHeader) bool {
1746 l := h.Values("Tls-Required")
1750 for _, v := range l {
1751 if !strings.EqualFold(v, "no") {
1758// submit is used for mail from authenticated users that we will try to deliver.
1759func (c *conn) submit(ctx context.Context, recvHdrFor func(string) string, msgWriter *message.Writer, dataFile *os.File) {
1760 // Similar between ../smtpserver/server.go:/submit\( and ../webmail/webmail.go:/MessageSubmit\(
1762 var msgPrefix []byte
1764 // Check that user is only sending email as one of its configured identities. Not
1768 msgFrom, header, err := message.From(c.log, true, dataFile)
1770 metricSubmission.WithLabelValues("badmessage").Inc()
1771 c.log.Infox("parsing message From address", err, mlog.Field("user", c.username))
1772 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeMsg6Other0, "cannot parse header or From address: %v", err)
1774 accName, _, _, err := mox.FindAccount(msgFrom.Localpart, msgFrom.Domain, true)
1775 if err != nil || accName != c.account.Name {
1778 err = mox.ErrAccountNotFound
1780 metricSubmission.WithLabelValues("badfrom").Inc()
1781 c.log.Infox("verifying message From address", err, mlog.Field("user", c.username), mlog.Field("msgfrom", msgFrom))
1782 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SePol7DeliveryUnauth1, "must match authenticated user")
1785 // TLS-Required: No header makes us not enforce recipient domain's TLS policy.
1788 if c.requireTLS == nil && hasTLSRequiredNo(header) {
1793 // Outgoing messages should not have a Return-Path header. The final receiving mail
1794 // server will add it.
1796 if header.Values("Return-Path") != nil {
1797 metricSubmission.WithLabelValues("badheader").Inc()
1798 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeMsg6Other0, "message must not have Return-Path header")
1801 // Add Message-Id header if missing.
1803 messageID := header.Get("Message-Id")
1804 if messageID == "" {
1805 messageID = mox.MessageIDGen(c.smtputf8)
1806 msgPrefix = append(msgPrefix, fmt.Sprintf("Message-Id: <%s>\r\n", messageID)...)
1810 if header.Get("Date") == "" {
1811 msgPrefix = append(msgPrefix, "Date: "+time.Now().Format(message.RFC5322Z)+"\r\n"...)
1814 // Check outoging message rate limit.
1815 err = c.account.DB.Read(ctx, func(tx *bstore.Tx) error {
1816 rcpts := make([]smtp.Path, len(c.recipients))
1817 for i, r := range c.recipients {
1820 msglimit, rcptlimit, err := c.account.SendLimitReached(tx, rcpts)
1821 xcheckf(err, "checking sender limit")
1823 metricSubmission.WithLabelValues("messagelimiterror").Inc()
1824 xsmtpUserErrorf(smtp.C451LocalErr, smtp.SePol7DeliveryUnauth1, "max number of messages (%d) over past 24h reached, try increasing per-account setting MaxOutgoingMessagesPerDay", msglimit)
1825 } else if rcptlimit >= 0 {
1826 metricSubmission.WithLabelValues("recipientlimiterror").Inc()
1827 xsmtpUserErrorf(smtp.C451LocalErr, smtp.SePol7DeliveryUnauth1, "max number of new/first-time recipients (%d) over past 24h reached, try increasing per-account setting MaxFirstTimeRecipientsPerDay", rcptlimit)
1831 xcheckf(err, "read-only transaction")
1833 // 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.
1835 // Add DKIM signatures.
1836 confDom, ok := mox.Conf.Domain(msgFrom.Domain)
1838 c.log.Error("domain disappeared", mlog.Field("domain", msgFrom.Domain))
1839 xsmtpServerErrorf(codes{smtp.C451LocalErr, smtp.SeSys3Other0}, "internal error")
1842 dkimConfig := confDom.DKIM
1843 if len(dkimConfig.Sign) > 0 {
1844 if canonical, err := mox.CanonicalLocalpart(msgFrom.Localpart, confDom); err != nil {
1845 c.log.Errorx("determining canonical localpart for dkim signing", err, mlog.Field("localpart", msgFrom.Localpart))
1846 } else if dkimHeaders, err := dkim.Sign(ctx, canonical, msgFrom.Domain, dkimConfig, c.smtputf8, store.FileMsgReader(msgPrefix, dataFile)); err != nil {
1847 c.log.Errorx("dkim sign for domain", err, mlog.Field("domain", msgFrom.Domain))
1848 metricServerErrors.WithLabelValues("dkimsign").Inc()
1850 msgPrefix = append(msgPrefix, []byte(dkimHeaders)...)
1854 authResults := message.AuthResults{
1855 Hostname: mox.Conf.Static.HostnameDomain.XName(c.smtputf8),
1856 Comment: mox.Conf.Static.HostnameDomain.ASCIIExtra(c.smtputf8),
1857 Methods: []message.AuthMethod{
1861 Props: []message.AuthProp{
1862 message.MakeAuthProp("smtp", "mailfrom", c.mailFrom.XString(c.smtputf8), true, c.mailFrom.ASCIIExtra(c.smtputf8)),
1867 msgPrefix = append(msgPrefix, []byte(authResults.Header())...)
1869 // We always deliver through the queue. It would be more efficient to deliver
1870 // directly, but we don't want to circumvent all the anti-spam measures. Accounts
1871 // on a single mox instance should be allowed to block each other.
1872 for _, rcptAcc := range c.recipients {
1874 code, timeout := localserveNeedsError(rcptAcc.rcptTo.Localpart)
1876 c.log.Info("timing out submission due to special localpart")
1877 mox.Sleep(mox.Context, time.Hour)
1878 xsmtpServerErrorf(codes{smtp.C451LocalErr, smtp.SeSys3Other0}, "timing out submission due to special localpart")
1879 } else if code != 0 {
1880 c.log.Info("failure due to special localpart", mlog.Field("code", code))
1881 xsmtpServerErrorf(codes{code, smtp.SeOther00}, "failure with code %d due to special localpart", code)
1885 xmsgPrefix := append([]byte(recvHdrFor(rcptAcc.rcptTo.String())), msgPrefix...)
1887 msgSize := int64(len(xmsgPrefix)) + msgWriter.Size
1888 qm := queue.MakeMsg(c.account.Name, *c.mailFrom, rcptAcc.rcptTo, msgWriter.Has8bit, c.smtputf8, msgSize, messageID, xmsgPrefix, c.requireTLS)
1889 if err := queue.Add(ctx, c.log, &qm, dataFile); err != nil {
1890 // Aborting the transaction is not great. But continuing and generating DSNs will
1891 // probably result in errors as well...
1892 metricSubmission.WithLabelValues("queueerror").Inc()
1893 c.log.Errorx("queuing message", err)
1894 xsmtpServerErrorf(errCodes(smtp.C451LocalErr, smtp.SeSys3Other0, err), "error delivering message: %v", err)
1896 metricSubmission.WithLabelValues("ok").Inc()
1897 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))
1899 err := c.account.DB.Insert(ctx, &store.Outgoing{Recipient: rcptAcc.rcptTo.XString(true)})
1900 xcheckf(err, "adding outgoing message")
1904 c.transactionBad-- // Compensate for early earlier pessimistic increase.
1907 c.writecodeline(smtp.C250Completed, smtp.SeMailbox2Other0, "it is done", nil)
1910func ipmasked(ip net.IP) (string, string, string) {
1911 if ip.To4() != nil {
1913 m2 := ip.Mask(net.CIDRMask(26, 32)).String()
1914 m3 := ip.Mask(net.CIDRMask(21, 32)).String()
1917 m1 := ip.Mask(net.CIDRMask(64, 128)).String()
1918 m2 := ip.Mask(net.CIDRMask(48, 128)).String()
1919 m3 := ip.Mask(net.CIDRMask(32, 128)).String()
1923func localserveNeedsError(lp smtp.Localpart) (code int, timeout bool) {
1925 if strings.HasSuffix(s, "temperror") {
1926 return smtp.C451LocalErr, false
1927 } else if strings.HasSuffix(s, "permerror") {
1928 return smtp.C550MailboxUnavail, false
1929 } else if strings.HasSuffix(s, "timeout") {
1936 v, err := strconv.ParseInt(s, 10, 32)
1940 if v < 400 || v > 600 {
1943 return int(v), false
1946func (c *conn) xlocalserveError(lp smtp.Localpart) {
1947 code, timeout := localserveNeedsError(lp)
1949 c.log.Info("timing out due to special localpart")
1950 mox.Sleep(mox.Context, time.Hour)
1951 xsmtpServerErrorf(codes{smtp.C451LocalErr, smtp.SeSys3Other0}, "timing out command due to special localpart")
1952 } else if code != 0 {
1953 c.log.Info("failure due to special localpart", mlog.Field("code", code))
1954 metricDelivery.WithLabelValues("delivererror", "localserve").Inc()
1955 xsmtpServerErrorf(codes{code, smtp.SeOther00}, "failure with code %d due to special localpart", code)
1959// deliver is called for incoming messages from external, typically untrusted
1960// sources. i.e. not submitted by authenticated users.
1961func (c *conn) deliver(ctx context.Context, recvHdrFor func(string) string, msgWriter *message.Writer, iprevStatus iprev.Status, iprevAuthentic bool, dataFile *os.File) {
1962 // 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.
1964 msgFrom, headers, err := message.From(c.log, false, dataFile)
1966 c.log.Infox("parsing message for From address", err)
1970 if len(headers.Values("Received")) > 100 {
1971 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeNet4Loop6, "loop detected, more than 100 Received headers")
1974 // TLS-Required: No header makes us not enforce recipient domain's TLS policy.
1975 // Since we only deliver locally at the moment, this won't influence our behaviour.
1976 // Once we forward, it would our delivery attempts.
1979 if c.requireTLS == nil && hasTLSRequiredNo(headers) {
1984 // We'll be building up an Authentication-Results header.
1985 authResults := message.AuthResults{
1986 Hostname: mox.Conf.Static.HostnameDomain.XName(c.smtputf8),
1989 commentAuthentic := func(v bool) string {
1991 return "with dnssec"
1993 return "without dnssec"
1996 // Reverse IP lookup results.
1997 // todo future: how useful is this?
1999 authResults.Methods = append(authResults.Methods, message.AuthMethod{
2001 Result: string(iprevStatus),
2002 Comment: commentAuthentic(iprevAuthentic),
2003 Props: []message.AuthProp{
2004 message.MakeAuthProp("policy", "iprev", c.remoteIP.String(), false, ""),
2008 // SPF and DKIM verification in parallel.
2009 var wg sync.WaitGroup
2013 var dkimResults []dkim.Result
2017 x := recover() // Should not happen, but don't take program down if it does.
2019 c.log.Error("dkim verify panic", mlog.Field("err", x))
2021 metrics.PanicInc(metrics.Dkimverify)
2025 // We always evaluate all signatures. We want to build up reputation for each
2026 // domain in the signature.
2027 const ignoreTestMode = false
2028 // todo future: longer timeout? we have to read through the entire email, which can be large, possibly multiple times.
2029 dkimctx, dkimcancel := context.WithTimeout(ctx, time.Minute)
2031 // todo future: we could let user configure which dkim headers they require
2032 dkimResults, dkimErr = dkim.Verify(dkimctx, c.resolver, c.smtputf8, dkim.DefaultPolicy, dataFile, ignoreTestMode)
2038 var receivedSPF spf.Received
2039 var spfDomain dns.Domain
2041 var spfAuthentic bool
2043 spfArgs := spf.Args{
2044 RemoteIP: c.remoteIP,
2045 MailFromLocalpart: c.mailFrom.Localpart,
2046 MailFromDomain: c.mailFrom.IPDomain.Domain, // Can be empty.
2047 HelloDomain: c.hello,
2049 LocalHostname: c.hostname,
2054 x := recover() // Should not happen, but don't take program down if it does.
2056 c.log.Error("spf verify panic", mlog.Field("err", x))
2058 metrics.PanicInc(metrics.Spfverify)
2062 spfctx, spfcancel := context.WithTimeout(ctx, time.Minute)
2064 receivedSPF, spfDomain, spfExpl, spfAuthentic, spfErr = spf.Verify(spfctx, c.resolver, spfArgs)
2067 c.log.Infox("spf verify", spfErr)
2071 // Wait for DKIM and SPF validation to finish.
2074 // Give immediate response if all recipients are unknown.
2076 for _, r := range c.recipients {
2081 if nunknown == len(c.recipients) {
2082 // During RCPT TO we found that the address does not exist.
2083 c.log.Info("deliver attempt to unknown user(s)", mlog.Field("recipients", c.recipients))
2085 // Crude attempt to slow down someone trying to guess names. Would work better
2086 // with connection rate limiter.
2087 if unknownRecipientsDelay > 0 {
2088 mox.Sleep(ctx, unknownRecipientsDelay)
2091 // 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.
2092 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeAddr1UnknownDestMailbox1, "no such user(s)")
2095 // Add DKIM results to Authentication-Results header.
2096 authResAddDKIM := func(result, comment, reason string, props []message.AuthProp) {
2097 dm := message.AuthMethod{
2104 authResults.Methods = append(authResults.Methods, dm)
2107 c.log.Errorx("dkim verify", dkimErr)
2108 authResAddDKIM("none", "", dkimErr.Error(), nil)
2109 } else if len(dkimResults) == 0 {
2110 c.log.Info("no dkim-signature header", mlog.Field("mailfrom", c.mailFrom))
2111 authResAddDKIM("none", "", "no dkim signatures", nil)
2113 for i, r := range dkimResults {
2114 var domain, selector dns.Domain
2115 var identity *dkim.Identity
2117 var props []message.AuthProp
2119 if r.Record != nil && r.Record.PublicKey != nil {
2120 if pubkey, ok := r.Record.PublicKey.(*rsa.PublicKey); ok {
2121 comment = fmt.Sprintf("%d bit rsa, ", pubkey.N.BitLen())
2125 sig := base64.StdEncoding.EncodeToString(r.Sig.Signature)
2126 sig = sig[:12] // Must be at least 8 characters and unique among the signatures.
2127 props = []message.AuthProp{
2128 message.MakeAuthProp("header", "d", r.Sig.Domain.XName(c.smtputf8), true, r.Sig.Domain.ASCIIExtra(c.smtputf8)),
2129 message.MakeAuthProp("header", "s", r.Sig.Selector.XName(c.smtputf8), true, r.Sig.Selector.ASCIIExtra(c.smtputf8)),
2130 message.MakeAuthProp("header", "a", r.Sig.Algorithm(), false, ""),
2133 domain = r.Sig.Domain
2134 selector = r.Sig.Selector
2135 if r.Sig.Identity != nil {
2136 props = append(props, message.MakeAuthProp("header", "i", r.Sig.Identity.String(), true, ""))
2137 identity = r.Sig.Identity
2139 if r.RecordAuthentic {
2140 comment += "with dnssec"
2142 comment += "without dnssec"
2147 errmsg = r.Err.Error()
2149 authResAddDKIM(string(r.Status), comment, errmsg, props)
2150 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))
2154 var spfIdentity *dns.Domain
2155 var mailFromValidation = store.ValidationUnknown
2156 var ehloValidation = store.ValidationUnknown
2157 switch receivedSPF.Identity {
2158 case spf.ReceivedHELO:
2159 if len(spfArgs.HelloDomain.IP) == 0 {
2160 spfIdentity = &spfArgs.HelloDomain.Domain
2162 ehloValidation = store.SPFValidation(receivedSPF.Result)
2163 case spf.ReceivedMailFrom:
2164 spfIdentity = &spfArgs.MailFromDomain
2165 mailFromValidation = store.SPFValidation(receivedSPF.Result)
2167 var props []message.AuthProp
2168 if spfIdentity != nil {
2169 props = []message.AuthProp{message.MakeAuthProp("smtp", string(receivedSPF.Identity), spfIdentity.XName(c.smtputf8), true, spfIdentity.ASCIIExtra(c.smtputf8))}
2171 var spfComment string
2173 spfComment = "with dnssec"
2175 spfComment = "without dnssec"
2177 authResults.Methods = append(authResults.Methods, message.AuthMethod{
2179 Result: string(receivedSPF.Result),
2180 Comment: spfComment,
2183 switch receivedSPF.Result {
2184 case spf.StatusPass:
2185 c.log.Debug("spf pass", mlog.Field("ip", spfArgs.RemoteIP), mlog.Field("mailfromdomain", spfArgs.MailFromDomain.ASCII)) // todo: log the domain that was actually verified.
2186 case spf.StatusFail:
2189 for _, b := range []byte(spfExpl) {
2190 if b < ' ' || b >= 0x7f {
2196 if len(spfExpl) > 800 {
2197 spfExpl = spfExpl[:797] + "..."
2199 spfExpl = "remote claims: " + spfExpl
2203 spfExpl = fmt.Sprintf("your ip %s is not on the SPF allowlist for domain %s", spfArgs.RemoteIP, spfDomain.ASCII)
2205 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?
2206 case spf.StatusTemperror:
2207 c.log.Infox("spf temperror", spfErr)
2208 case spf.StatusPermerror:
2209 c.log.Infox("spf permerror", spfErr)
2210 case spf.StatusNone, spf.StatusNeutral, spf.StatusSoftfail:
2212 c.log.Error("unknown spf status, treating as None/Neutral", mlog.Field("status", receivedSPF.Result))
2213 receivedSPF.Result = spf.StatusNone
2218 var dmarcResult dmarc.Result
2219 const applyRandomPercentage = true
2220 // dmarcMethod is added to authResults when delivering to recipients: accounts can
2221 // have different policy override rules.
2222 var dmarcMethod message.AuthMethod
2223 var msgFromValidation = store.ValidationNone
2224 if msgFrom.IsZero() {
2225 dmarcResult.Status = dmarc.StatusNone
2226 dmarcMethod = message.AuthMethod{
2228 Result: string(dmarcResult.Status),
2231 msgFromValidation = alignment(ctx, msgFrom.Domain, dkimResults, receivedSPF.Result, spfIdentity)
2233 // We are doing the DMARC evaluation now. But we only store it for inclusion in an
2234 // aggregate report when we actually use it. We use an evaluation for each
2235 // recipient, with each a potentially different result due to mailing
2236 // list/forwarding configuration. If we reject a message due to being spam, we
2237 // don't want to spend any resources for the sender domain, and we don't want to
2238 // give the sender any more information about us, so we won't record the
2240 // todo future: also not send for first-time senders? they could be spammers getting through our filter, don't want to give them insights either. though we currently would have no reasonable way to decide if they are still reputationless at the time we are composing/sending aggregate reports.
2242 dmarcctx, dmarccancel := context.WithTimeout(ctx, time.Minute)
2244 dmarcUse, dmarcResult = dmarc.Verify(dmarcctx, c.resolver, msgFrom.Domain, dkimResults, receivedSPF.Result, spfIdentity, applyRandomPercentage)
2247 if dmarcResult.RecordAuthentic {
2248 comment = "with dnssec"
2250 comment = "without dnssec"
2252 dmarcMethod = message.AuthMethod{
2254 Result: string(dmarcResult.Status),
2256 Props: []message.AuthProp{
2258 message.MakeAuthProp("header", "from", msgFrom.Domain.ASCII, true, msgFrom.Domain.ASCIIExtra(c.smtputf8)),
2262 if dmarcResult.Status == dmarc.StatusPass && msgFromValidation == store.ValidationRelaxed {
2263 msgFromValidation = store.ValidationDMARC
2266 // todo future: consider enforcing an spf (soft)fail if there is no dmarc policy or the dmarc policy is none.
../rfc/7489:1507
2268 c.log.Debug("dmarc verification", mlog.Field("result", dmarcResult.Status), mlog.Field("domain", msgFrom.Domain))
2270 // Prepare for analyzing content, calculating reputation.
2271 ipmasked1, ipmasked2, ipmasked3 := ipmasked(c.remoteIP)
2272 var verifiedDKIMDomains []string
2273 dkimSeen := map[string]bool{}
2274 for _, r := range dkimResults {
2275 // A message can have multiple signatures for the same identity. For example when
2276 // signing the message multiple times with different algorithms (rsa and ed25519).
2277 if r.Status != dkim.StatusPass {
2280 d := r.Sig.Domain.Name()
2283 verifiedDKIMDomains = append(verifiedDKIMDomains, d)
2287 // When we deliver, we try to remove from rejects mailbox based on message-id.
2288 // We'll parse it when we need it, but it is the same for each recipient.
2289 var messageID string
2290 var parsedMessageID bool
2292 // We build up a DSN for each failed recipient. If we have recipients in dsnMsg
2293 // after processing, we queue the DSN. Unless all recipients failed, in which case
2294 // we may just fail the mail transaction instead (could be common for failure to
2295 // deliver to a single recipient, e.g. for junk mail).
2297 type deliverError struct {
2304 var deliverErrors []deliverError
2305 addError := func(rcptAcc rcptAccount, code int, secode string, userError bool, errmsg string) {
2306 e := deliverError{rcptAcc.rcptTo, code, secode, userError, errmsg}
2307 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))
2308 deliverErrors = append(deliverErrors, e)
2311 // For each recipient, do final spam analysis and delivery.
2312 for _, rcptAcc := range c.recipients {
2313 log := c.log.Fields(mlog.Field("mailfrom", c.mailFrom), mlog.Field("rcptto", rcptAcc.rcptTo))
2315 // If this is not a valid local user, we send back a DSN. This can only happen when
2316 // there are also valid recipients, and only when remote is SPF-verified, so the DSN
2317 // should not cause backscatter.
2318 // In case of serious errors, we abort the transaction. We may have already
2319 // delivered some messages. Perhaps it would be better to continue with other
2320 // deliveries, and return an error at the end? Though the failure conditions will
2321 // probably prevent any other successful deliveries too...
2324 metricDelivery.WithLabelValues("unknownuser", "").Inc()
2325 addError(rcptAcc, smtp.C550MailboxUnavail, smtp.SeAddr1UnknownDestMailbox1, true, "no such user")
2329 acc, err := store.OpenAccount(rcptAcc.accountName)
2331 log.Errorx("open account", err, mlog.Field("account", rcptAcc.accountName))
2332 metricDelivery.WithLabelValues("accounterror", "").Inc()
2333 addError(rcptAcc, smtp.C451LocalErr, smtp.SeSys3Other0, false, "error processing")
2339 log.Check(err, "closing account after delivery")
2343 // We don't want to let a single IP or network deliver too many messages to an
2344 // account. They may fill up the mailbox, either with messages that have to be
2345 // purged, or by filling the disk. We check both cases for IP's and networks.
2346 var rateError bool // Whether returned error represents a rate error.
2347 err = acc.DB.Read(ctx, func(tx *bstore.Tx) (retErr error) {
2350 log.Debugx("checking message and size delivery rates", retErr, mlog.Field("duration", time.Since(now)))
2353 checkCount := func(msg store.Message, window time.Duration, limit int) {
2357 q := bstore.QueryTx[store.Message](tx)
2358 q.FilterNonzero(msg)
2359 q.FilterGreater("Received", now.Add(-window))
2360 q.FilterEqual("Expunged", false)
2368 retErr = fmt.Errorf("more than %d messages in past %s from your ip/network", limit, window)
2372 checkSize := func(msg store.Message, window time.Duration, limit int64) {
2376 q := bstore.QueryTx[store.Message](tx)
2377 q.FilterNonzero(msg)
2378 q.FilterGreater("Received", now.Add(-window))
2379 q.FilterEqual("Expunged", false)
2380 size := msgWriter.Size
2381 err := q.ForEach(func(v store.Message) error {
2391 retErr = fmt.Errorf("more than %d bytes in past %s from your ip/network", limit, window)
2395 // todo future: make these configurable
2396 // todo: should we have a limit for forwarded messages? they are stored with empty RemoteIPMasked*
2398 const day = 24 * time.Hour
2399 checkCount(store.Message{RemoteIPMasked1: ipmasked1}, time.Minute, limitIPMasked1MessagesPerMinute)
2400 checkCount(store.Message{RemoteIPMasked1: ipmasked1}, day, 20*500)
2401 checkCount(store.Message{RemoteIPMasked2: ipmasked2}, time.Minute, 1500)
2402 checkCount(store.Message{RemoteIPMasked2: ipmasked2}, day, 20*1500)
2403 checkCount(store.Message{RemoteIPMasked3: ipmasked3}, time.Minute, 4500)
2404 checkCount(store.Message{RemoteIPMasked3: ipmasked3}, day, 20*4500)
2406 const MB = 1024 * 1024
2407 checkSize(store.Message{RemoteIPMasked1: ipmasked1}, time.Minute, limitIPMasked1SizePerMinute)
2408 checkSize(store.Message{RemoteIPMasked1: ipmasked1}, day, 3*1000*MB)
2409 checkSize(store.Message{RemoteIPMasked2: ipmasked2}, time.Minute, 3000*MB)
2410 checkSize(store.Message{RemoteIPMasked2: ipmasked2}, day, 3*3000*MB)
2411 checkSize(store.Message{RemoteIPMasked3: ipmasked3}, time.Minute, 9000*MB)
2412 checkSize(store.Message{RemoteIPMasked3: ipmasked3}, day, 3*9000*MB)
2416 if err != nil && !rateError {
2417 log.Errorx("checking delivery rates", err)
2418 metricDelivery.WithLabelValues("checkrates", "").Inc()
2419 addError(rcptAcc, smtp.C451LocalErr, smtp.SeSys3Other0, false, "error processing")
2421 } else if err != nil {
2422 log.Debugx("refusing due to high delivery rate", err)
2423 metricDelivery.WithLabelValues("highrate", "").Inc()
2425 addError(rcptAcc, smtp.C452StorageFull, smtp.SeMailbox2Full2, true, err.Error())
2430 Received: time.Now(),
2431 RemoteIP: c.remoteIP.String(),
2432 RemoteIPMasked1: ipmasked1,
2433 RemoteIPMasked2: ipmasked2,
2434 RemoteIPMasked3: ipmasked3,
2435 EHLODomain: c.hello.Domain.Name(),
2436 MailFrom: c.mailFrom.String(),
2437 MailFromLocalpart: c.mailFrom.Localpart,
2438 MailFromDomain: c.mailFrom.IPDomain.Domain.Name(),
2439 RcptToLocalpart: rcptAcc.rcptTo.Localpart,
2440 RcptToDomain: rcptAcc.rcptTo.IPDomain.Domain.Name(),
2441 MsgFromLocalpart: msgFrom.Localpart,
2442 MsgFromDomain: msgFrom.Domain.Name(),
2443 MsgFromOrgDomain: publicsuffix.Lookup(ctx, msgFrom.Domain).Name(),
2444 EHLOValidated: ehloValidation == store.ValidationPass,
2445 MailFromValidated: mailFromValidation == store.ValidationPass,
2446 MsgFromValidated: msgFromValidation == store.ValidationStrict || msgFromValidation == store.ValidationDMARC || msgFromValidation == store.ValidationRelaxed,
2447 EHLOValidation: ehloValidation,
2448 MailFromValidation: mailFromValidation,
2449 MsgFromValidation: msgFromValidation,
2450 DKIMDomains: verifiedDKIMDomains,
2451 Size: msgWriter.Size,
2454 tlsState := c.conn.(*tls.Conn).ConnectionState()
2455 m.ReceivedTLSVersion = tlsState.Version
2456 m.ReceivedTLSCipherSuite = tlsState.CipherSuite
2457 if c.requireTLS != nil {
2458 m.ReceivedRequireTLS = *c.requireTLS
2461 m.ReceivedTLSVersion = 1 // Signals plain text delivery.
2464 d := delivery{&m, dataFile, rcptAcc, acc, msgFrom, c.dnsBLs, dmarcUse, dmarcResult, dkimResults, iprevStatus}
2465 a := analyze(ctx, log, c.resolver, d)
2467 // Any DMARC result override is stored in the evaluation for outgoing DMARC
2468 // aggregate reports, and added to the Authentication-Results message header.
2469 // We want to tell the sender that we have an override, e.g. for mailing lists, so
2470 // they don't overestimate the potential damage of switching from p=none to
2472 var dmarcOverrides []string
2473 if a.dmarcOverrideReason != "" {
2474 dmarcOverrides = []string{a.dmarcOverrideReason}
2476 if dmarcResult.Record != nil && !dmarcUse {
2477 dmarcOverrides = append(dmarcOverrides, string(dmarcrpt.PolicyOverrideSampledOut))
2480 // Add per-recipient DMARC method to Authentication-Results. Each account can have
2481 // their own override rules, e.g. based on configured mailing lists/forwards.
2483 rcptDMARCMethod := dmarcMethod
2484 if len(dmarcOverrides) > 0 {
2485 if rcptDMARCMethod.Comment != "" {
2486 rcptDMARCMethod.Comment += ", "
2488 rcptDMARCMethod.Comment += "override " + strings.Join(dmarcOverrides, ",")
2490 rcptAuthResults := authResults
2491 rcptAuthResults.Methods = append([]message.AuthMethod{}, authResults.Methods...)
2492 rcptAuthResults.Methods = append(rcptAuthResults.Methods, rcptDMARCMethod)
2494 // Prepend reason as message header, for easy display in mail clients.
2497 xmox = "X-Mox-Reason: " + a.reason + "\r\n"
2503 m.MsgPrefix = []byte(
2507 rcptAuthResults.Header() +
2508 receivedSPF.Header() +
2509 recvHdrFor(rcptAcc.rcptTo.String()),
2511 m.Size += int64(len(m.MsgPrefix))
2513 // Store DMARC evaluation for inclusion in an aggregate report. Only if there is at
2514 // least one reporting address: We don't want to needlessly store a row in a
2515 // database for each delivery attempt. If we reject a message for being junk, we
2516 // are also not going to send it a DMARC report. The DMARC check is done early in
2517 // the analysis, we will report on rejects because of DMARC, because it could be
2518 // valuable feedback about forwarded or mailing list messages.
2520 if !mox.Conf.Static.NoOutgoingDMARCReports && dmarcResult.Record != nil && len(dmarcResult.Record.AggregateReportAddresses) > 0 && (a.accept && !m.IsReject || a.reason == reasonDMARCPolicy) {
2521 // Disposition holds our decision on whether to accept the message. Not what the
2522 // DMARC evaluation resulted in. We can override, e.g. because of mailing lists,
2523 // forwarding, or local policy.
2524 // We treat quarantine as reject, so never claim to quarantine.
2526 disposition := dmarcrpt.DispositionNone
2528 disposition = dmarcrpt.DispositionReject
2531 // unknownDomain returns whether the sender is domain with which this account has
2532 // not had positive interaction.
2533 unknownDomain := func() (unknown bool) {
2534 err := acc.DB.Read(ctx, func(tx *bstore.Tx) (err error) {
2535 // See if we received a non-junk message from this organizational domain.
2536 q := bstore.QueryTx[store.Message](tx)
2537 q.FilterNonzero(store.Message{MsgFromOrgDomain: m.MsgFromOrgDomain})
2538 q.FilterEqual("Notjunk", true)
2539 q.FilterEqual("IsReject", false)
2540 exists, err := q.Exists()
2542 return fmt.Errorf("querying for non-junk message from organizational domain: %v", err)
2548 // See if we sent a message to this organizational domain.
2549 qr := bstore.QueryTx[store.Recipient](tx)
2550 qr.FilterNonzero(store.Recipient{OrgDomain: m.MsgFromOrgDomain})
2551 exists, err = qr.Exists()
2553 return fmt.Errorf("querying for message sent to organizational domain: %v", err)
2561 log.Errorx("checking if sender is unknown domain, for dmarc aggregate report evaluation", err)
2566 r := dmarcResult.Record
2567 addresses := make([]string, len(r.AggregateReportAddresses))
2568 for i, a := range r.AggregateReportAddresses {
2569 addresses[i] = a.String()
2571 sp := dmarcrpt.Disposition(r.SubdomainPolicy)
2572 if r.SubdomainPolicy == dmarc.PolicyEmpty {
2573 sp = dmarcrpt.Disposition(r.Policy)
2575 eval := dmarcdb.Evaluation{
2576 // Evaluated and IntervalHours set by AddEvaluation.
2577 PolicyDomain: dmarcResult.Domain.Name(),
2579 // Optional evaluations don't cause a report to be sent, but will be included.
2580 // Useful for automated inter-mailer messages, we don't want to get in a reporting
2581 // loop. We also don't want to be used for sending reports to unsuspecting domains
2582 // we have no relation with.
2583 // todo: would it make sense to also mark some percentage of mailing-list-policy-overrides optional? to lower the load on mail servers of folks sending to large mailing lists.
2584 Optional: rcptAcc.destination.DMARCReports || rcptAcc.destination.HostTLSReports || rcptAcc.destination.DomainTLSReports || a.reason == reasonDMARCPolicy && unknownDomain(),
2586 Addresses: addresses,
2588 PolicyPublished: dmarcrpt.PolicyPublished{
2589 Domain: dmarcResult.Domain.Name(),
2590 ADKIM: dmarcrpt.Alignment(r.ADKIM),
2591 ASPF: dmarcrpt.Alignment(r.ASPF),
2592 Policy: dmarcrpt.Disposition(r.Policy),
2593 SubdomainPolicy: sp,
2594 Percentage: r.Percentage,
2595 // We don't save ReportingOptions, we don't do per-message failure reporting.
2597 SourceIP: c.remoteIP.String(),
2598 Disposition: disposition,
2599 AlignedDKIMPass: dmarcResult.AlignedDKIMPass,
2600 AlignedSPFPass: dmarcResult.AlignedSPFPass,
2601 EnvelopeTo: rcptAcc.rcptTo.IPDomain.String(),
2602 EnvelopeFrom: c.mailFrom.IPDomain.String(),
2603 HeaderFrom: msgFrom.Domain.Name(),
2606 for _, s := range dmarcOverrides {
2607 reason := dmarcrpt.PolicyOverrideReason{Type: dmarcrpt.PolicyOverride(s)}
2608 eval.OverrideReasons = append(eval.OverrideReasons, reason)
2611 // We'll include all signatures for the organizational domain, even if they weren't
2612 // relevant due to strict alignment requirement.
2613 for _, dkimResult := range dkimResults {
2614 if dkimResult.Sig == nil || publicsuffix.Lookup(ctx, msgFrom.Domain) != publicsuffix.Lookup(ctx, dkimResult.Sig.Domain) {
2617 r := dmarcrpt.DKIMAuthResult{
2618 Domain: dkimResult.Sig.Domain.Name(),
2619 Selector: dkimResult.Sig.Selector.ASCII,
2620 Result: dmarcrpt.DKIMResult(dkimResult.Status),
2622 eval.DKIMResults = append(eval.DKIMResults, r)
2625 switch receivedSPF.Identity {
2626 case spf.ReceivedHELO:
2627 spfAuthResult := dmarcrpt.SPFAuthResult{
2628 Domain: spfArgs.HelloDomain.String(), // Can be unicode and also IP.
2629 Scope: dmarcrpt.SPFDomainScopeHelo,
2630 Result: dmarcrpt.SPFResult(receivedSPF.Result),
2632 eval.SPFResults = []dmarcrpt.SPFAuthResult{spfAuthResult}
2633 case spf.ReceivedMailFrom:
2634 spfAuthResult := dmarcrpt.SPFAuthResult{
2635 Domain: spfArgs.MailFromDomain.Name(), // Can be unicode.
2636 Scope: dmarcrpt.SPFDomainScopeMailFrom,
2637 Result: dmarcrpt.SPFResult(receivedSPF.Result),
2639 eval.SPFResults = []dmarcrpt.SPFAuthResult{spfAuthResult}
2642 err := dmarcdb.AddEvaluation(ctx, dmarcResult.Record.AggregateReportingInterval, &eval)
2643 log.Check(err, "adding dmarc evaluation to database for aggregate report")
2647 conf, _ := acc.Conf()
2648 if conf.RejectsMailbox != "" {
2649 present, _, messagehash, err := rejectPresent(log, acc, conf.RejectsMailbox, &m, dataFile)
2651 log.Errorx("checking whether reject is already present", err)
2652 } else if !present {
2654 m.Seen = true // We don't want to draw attention.
2655 // Regular automatic junk flags configuration applies to these messages. The
2656 // default is to treat these as neutral, so they won't cause outright rejections
2657 // due to reputation for later delivery attempts.
2658 m.MessageHash = messagehash
2659 acc.WithWLock(func() {
2662 if !conf.KeepRejects {
2663 hasSpace, err = acc.TidyRejectsMailbox(c.log, conf.RejectsMailbox)
2666 log.Errorx("tidying rejects mailbox", err)
2667 } else if hasSpace {
2668 if err := acc.DeliverMailbox(log, conf.RejectsMailbox, &m, dataFile); err != nil {
2669 log.Errorx("delivering spammy mail to rejects mailbox", err)
2671 log.Info("delivered spammy mail to rejects mailbox")
2674 log.Info("not storing spammy mail to full rejects mailbox")
2678 log.Info("reject message is already present, ignoring")
2682 log.Info("incoming message rejected", mlog.Field("reason", a.reason), mlog.Field("msgfrom", msgFrom))
2683 metricDelivery.WithLabelValues("reject", a.reason).Inc()
2685 addError(rcptAcc, a.code, a.secode, a.userError, a.errmsg)
2689 delayFirstTime := true
2690 if a.dmarcReport != nil {
2692 if err := dmarcdb.AddReport(ctx, a.dmarcReport, msgFrom.Domain); err != nil {
2693 log.Errorx("saving dmarc aggregate report in database", err)
2695 log.Info("dmarc aggregate report processed")
2697 delayFirstTime = false
2700 if a.tlsReport != nil {
2701 // todo future: add rate limiting to prevent DoS attacks.
2702 if err := tlsrptdb.AddReport(ctx, msgFrom.Domain, c.mailFrom.String(), rcptAcc.destination.HostTLSReports, a.tlsReport); err != nil {
2703 log.Errorx("saving TLSRPT report in database", err)
2705 log.Info("tlsrpt report processed")
2707 delayFirstTime = false
2711 // If this is a first-time sender and not a forwarded message, wait before actually
2712 // delivering. If this turns out to be a spammer, we've kept one of their
2713 // connections busy.
2714 if delayFirstTime && !m.IsForward && a.reason == reasonNoBadSignals && c.firstTimeSenderDelay > 0 {
2715 log.Debug("delaying before delivering from sender without reputation", mlog.Field("delay", c.firstTimeSenderDelay))
2716 mox.Sleep(mox.Context, c.firstTimeSenderDelay)
2719 // Gather the message-id before we deliver and the file may be consumed.
2720 if !parsedMessageID {
2721 if p, err := message.Parse(c.log, false, store.FileMsgReader(m.MsgPrefix, dataFile)); err != nil {
2722 log.Infox("parsing message for message-id", err)
2723 } else if header, err := p.Header(); err != nil {
2724 log.Infox("parsing message header for message-id", err)
2726 messageID = header.Get("Message-Id")
2731 code, timeout := localserveNeedsError(rcptAcc.rcptTo.Localpart)
2733 c.log.Info("timing out due to special localpart")
2734 mox.Sleep(mox.Context, time.Hour)
2735 xsmtpServerErrorf(codes{smtp.C451LocalErr, smtp.SeOther00}, "timing out delivery due to special localpart")
2736 } else if code != 0 {
2737 c.log.Info("failure due to special localpart", mlog.Field("code", code))
2738 metricDelivery.WithLabelValues("delivererror", "localserve").Inc()
2739 addError(rcptAcc, code, smtp.SeOther00, false, fmt.Sprintf("failure with code %d due to special localpart", code))
2742 acc.WithWLock(func() {
2743 if err := acc.DeliverMailbox(log, a.mailbox, &m, dataFile); err != nil {
2744 log.Errorx("delivering", err)
2745 metricDelivery.WithLabelValues("delivererror", a.reason).Inc()
2746 addError(rcptAcc, smtp.C451LocalErr, smtp.SeSys3Other0, false, "error processing")
2749 metricDelivery.WithLabelValues("delivered", a.reason).Inc()
2750 log.Info("incoming message delivered", mlog.Field("reason", a.reason), mlog.Field("msgfrom", msgFrom))
2752 conf, _ := acc.Conf()
2753 if conf.RejectsMailbox != "" && m.MessageID != "" {
2754 if err := acc.RejectsRemove(log, conf.RejectsMailbox, m.MessageID); err != nil {
2755 log.Errorx("removing message from rejects mailbox", err, mlog.Field("messageid", messageID))
2761 log.Check(err, "closing account after delivering")
2765 // If all recipients failed to deliver, return an error.
2766 if len(c.recipients) == len(deliverErrors) {
2768 e0 := deliverErrors[0]
2769 var serverError bool
2772 for _, e := range deliverErrors {
2773 serverError = serverError || !e.userError
2774 if e.code != e0.code || e.secode != e0.secode {
2777 msgs = append(msgs, e.errmsg)
2783 xsmtpErrorf(e0.code, e0.secode, !serverError, "%s", strings.Join(msgs, "\n"))
2786 // Not all failures had the same error. We'll return each error on a separate line.
2788 for _, e := range deliverErrors {
2789 s := fmt.Sprintf("%d %d.%s %s", e.code, e.code/100, e.secode, e.errmsg)
2790 lines = append(lines, s)
2792 code := smtp.C451LocalErr
2793 secode := smtp.SeSys3Other0
2795 code = smtp.C554TransactionFailed
2797 lines = append(lines, "multiple errors")
2798 xsmtpErrorf(code, secode, !serverError, strings.Join(lines, "\n"))
2800 // Generate one DSN for all failed recipients.
2801 if len(deliverErrors) > 0 {
2803 dsnMsg := dsn.Message{
2804 SMTPUTF8: c.smtputf8,
2805 From: smtp.Path{Localpart: "postmaster", IPDomain: deliverErrors[0].rcptTo.IPDomain},
2807 Subject: "mail delivery failure",
2808 References: messageID,
2810 // Per-message details.
2811 ReportingMTA: mox.Conf.Static.HostnameDomain.ASCII,
2812 ReceivedFromMTA: smtp.Ehlo{Name: c.hello, ConnIP: c.remoteIP},
2816 if len(deliverErrors) > 1 {
2817 dsnMsg.TextBody = "Multiple delivery failures occurred.\n\n"
2820 for _, e := range deliverErrors {
2822 if e.code/100 == 4 {
2825 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))
2826 rcpt := dsn.Recipient{
2827 FinalRecipient: e.rcptTo,
2829 Status: fmt.Sprintf("%d.%s", e.code/100, e.secode),
2830 LastAttemptDate: now,
2832 dsnMsg.Recipients = append(dsnMsg.Recipients, rcpt)
2835 header, err := message.ReadHeaders(bufio.NewReader(&moxio.AtReader{R: dataFile}))
2837 c.log.Errorx("reading headers of incoming message for dsn, continuing dsn without headers", err)
2839 dsnMsg.Original = header
2842 c.log.Error("not queueing dsn for incoming delivery due to localserve")
2843 } else if err := queueDSN(context.TODO(), c, *c.mailFrom, dsnMsg, c.requireTLS != nil && *c.requireTLS); err != nil {
2844 metricServerErrors.WithLabelValues("queuedsn").Inc()
2845 c.log.Errorx("queuing DSN for incoming delivery, no DSN sent", err)
2850 c.transactionBad-- // Compensate for early earlier pessimistic increase.
2852 c.writecodeline(smtp.C250Completed, smtp.SeMailbox2Other0, "it is done", nil)
2855// ecode returns either ecode, or a more specific error based on err.
2856// For example, ecode can be turned from an "other system" error into a "mail
2857// system full" if the error indicates no disk space is available.
2858func errCodes(code int, ecode string, err error) codes {
2860 case moxio.IsStorageSpace(err):
2862 case smtp.SeMailbox2Other0:
2863 if code == smtp.C451LocalErr {
2864 code = smtp.C452StorageFull
2866 ecode = smtp.SeMailbox2Full2
2867 case smtp.SeSys3Other0:
2868 if code == smtp.C451LocalErr {
2869 code = smtp.C452StorageFull
2871 ecode = smtp.SeSys3StorageFull1
2874 return codes{code, ecode}
2878func (c *conn) cmdRset(p *parser) {
2883 c.bwritecodeline(smtp.C250Completed, smtp.SeOther00, "all clear", nil)
2887func (c *conn) cmdVrfy(p *parser) {
2888 // No EHLO/HELO needed.
2899 // todo future: we could support vrfy and expn for submission? though would need to see if its rfc defines it.
2902 xsmtpUserErrorf(smtp.C252WithoutVrfy, smtp.SePol7Other0, "no verify but will try delivery")
2906func (c *conn) cmdExpn(p *parser) {
2907 // No EHLO/HELO needed.
2919 xsmtpUserErrorf(smtp.C252WithoutVrfy, smtp.SePol7Other0, "no expand but will try delivery")
2923func (c *conn) cmdHelp(p *parser) {
2924 // Let's not strictly parse the request for help. We are ignoring the text anyway.
2927 c.bwritecodeline(smtp.C214Help, smtp.SeOther00, "see rfc 5321 (smtp)", nil)
2931func (c *conn) cmdNoop(p *parser) {
2932 // No idea why, but if an argument follows, it must adhere to the string ABNF production...
2939 c.bwritecodeline(smtp.C250Completed, smtp.SeOther00, "alrighty", nil)
2943func (c *conn) cmdQuit(p *parser) {
2947 c.writecodeline(smtp.C221Closing, smtp.SeOther00, "okay thanks bye", nil)