1// Package smtpserver implements an SMTP server for submission and incoming delivery of mail messages.
29 "golang.org/x/exp/maps"
30 "golang.org/x/exp/slog"
32 "github.com/prometheus/client_golang/prometheus"
33 "github.com/prometheus/client_golang/prometheus/promauto"
35 "github.com/mjl-/bstore"
37 "github.com/mjl-/mox/config"
38 "github.com/mjl-/mox/dkim"
39 "github.com/mjl-/mox/dmarc"
40 "github.com/mjl-/mox/dmarcdb"
41 "github.com/mjl-/mox/dmarcrpt"
42 "github.com/mjl-/mox/dns"
43 "github.com/mjl-/mox/dsn"
44 "github.com/mjl-/mox/iprev"
45 "github.com/mjl-/mox/message"
46 "github.com/mjl-/mox/metrics"
47 "github.com/mjl-/mox/mlog"
48 "github.com/mjl-/mox/mox-"
49 "github.com/mjl-/mox/moxio"
50 "github.com/mjl-/mox/moxvar"
51 "github.com/mjl-/mox/publicsuffix"
52 "github.com/mjl-/mox/queue"
53 "github.com/mjl-/mox/ratelimit"
54 "github.com/mjl-/mox/scram"
55 "github.com/mjl-/mox/smtp"
56 "github.com/mjl-/mox/spf"
57 "github.com/mjl-/mox/store"
58 "github.com/mjl-/mox/tlsrptdb"
61// We use panic and recover for error handling while executing commands.
62// These errors signal the connection must be closed.
63var errIO = errors.New("io error")
65// If set, regular delivery/submit is sidestepped, email is accepted and
66// delivered to the account named mox.
69var limiterConnectionRate, limiterConnections *ratelimit.Limiter
71// For delivery rate limiting. Variable because changed during tests.
72var limitIPMasked1MessagesPerMinute int = 500
73var limitIPMasked1SizePerMinute int64 = 1000 * 1024 * 1024
76 // Also called by tests, so they don't trigger the rate limiter.
82 // todo future: make these configurable
83 limiterConnectionRate = &ratelimit.Limiter{
84 WindowLimits: []ratelimit.WindowLimit{
87 Limits: [...]int64{300, 900, 2700},
91 limiterConnections = &ratelimit.Limiter{
92 WindowLimits: []ratelimit.WindowLimit{
94 Window: time.Duration(math.MaxInt64), // All of time.
95 Limits: [...]int64{30, 90, 270},
102 // Delays for bad/suspicious behaviour. Zero during tests.
103 badClientDelay = time.Second // Before reads and after 1-byte writes for probably spammers.
104 authFailDelay = time.Second // Response to authentication failure.
105 unknownRecipientsDelay = 5 * time.Second // Response when all recipients are unknown.
106 firstTimeSenderDelayDefault = 15 * time.Second // Before accepting message from first-time sender.
111 secode string // Enhanced code, but without the leading major int from code.
115 metricConnection = promauto.NewCounterVec(
116 prometheus.CounterOpts{
117 Name: "mox_smtpserver_connection_total",
118 Help: "Incoming SMTP connections.",
121 "kind", // "deliver" or "submit"
124 metricCommands = promauto.NewHistogramVec(
125 prometheus.HistogramOpts{
126 Name: "mox_smtpserver_command_duration_seconds",
127 Help: "SMTP server command duration and result codes in seconds.",
128 Buckets: []float64{0.001, 0.005, 0.01, 0.05, 0.100, 0.5, 1, 5, 10, 20, 30, 60, 120},
131 "kind", // "deliver" or "submit"
137 metricDelivery = promauto.NewCounterVec(
138 prometheus.CounterOpts{
139 Name: "mox_smtpserver_delivery_total",
140 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.",
147 // Similar between ../webmail/webmail.go:/metricSubmission and ../smtpserver/server.go:/metricSubmission
148 metricSubmission = promauto.NewCounterVec(
149 prometheus.CounterOpts{
150 Name: "mox_smtpserver_submission_total",
151 Help: "SMTP server incoming submission results, known values (those ending with error are server errors): ok, badmessage, badfrom, badheader, messagelimiterror, recipientlimiterror, localserveerror, queueerror.",
157 metricServerErrors = promauto.NewCounterVec(
158 prometheus.CounterOpts{
159 Name: "mox_smtpserver_errors_total",
160 Help: "SMTP server errors, known values: dkimsign, queuedsn.",
168var jitterRand = mox.NewPseudoRand()
170func durationDefault(delay *time.Duration, def time.Duration) time.Duration {
177// Listen initializes network listeners for incoming SMTP connection.
178// The listeners are stored for a later call to Serve.
180 names := maps.Keys(mox.Conf.Static.Listeners)
182 for _, name := range names {
183 listener := mox.Conf.Static.Listeners[name]
185 var tlsConfig *tls.Config
186 if listener.TLS != nil {
187 tlsConfig = listener.TLS.Config
190 maxMsgSize := listener.SMTPMaxMessageSize
192 maxMsgSize = config.DefaultMaxMsgSize
195 if listener.SMTP.Enabled {
196 hostname := mox.Conf.Static.HostnameDomain
197 if listener.Hostname != "" {
198 hostname = listener.HostnameDomain
200 port := config.Port(listener.SMTP.Port, 25)
201 for _, ip := range listener.IPs {
202 firstTimeSenderDelay := durationDefault(listener.SMTP.FirstTimeSenderDelay, firstTimeSenderDelayDefault)
203 listen1("smtp", name, ip, port, hostname, tlsConfig, false, false, maxMsgSize, false, listener.SMTP.RequireSTARTTLS, !listener.SMTP.NoRequireTLS, listener.SMTP.DNSBLZones, firstTimeSenderDelay)
206 if listener.Submission.Enabled {
207 hostname := mox.Conf.Static.HostnameDomain
208 if listener.Hostname != "" {
209 hostname = listener.HostnameDomain
211 port := config.Port(listener.Submission.Port, 587)
212 for _, ip := range listener.IPs {
213 listen1("submission", name, ip, port, hostname, tlsConfig, true, false, maxMsgSize, !listener.Submission.NoRequireSTARTTLS, !listener.Submission.NoRequireSTARTTLS, true, nil, 0)
217 if listener.Submissions.Enabled {
218 hostname := mox.Conf.Static.HostnameDomain
219 if listener.Hostname != "" {
220 hostname = listener.HostnameDomain
222 port := config.Port(listener.Submissions.Port, 465)
223 for _, ip := range listener.IPs {
224 listen1("submissions", name, ip, port, hostname, tlsConfig, true, true, maxMsgSize, true, true, true, nil, 0)
232func 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) {
233 log := mlog.New("smtpserver", nil)
234 addr := net.JoinHostPort(ip, fmt.Sprintf("%d", port))
235 if os.Getuid() == 0 {
236 log.Print("listening for smtp",
237 slog.String("listener", name),
238 slog.String("address", addr),
239 slog.String("protocol", protocol))
241 network := mox.Network(ip)
242 ln, err := mox.Listen(network, addr)
244 log.Fatalx("smtp: listen for smtp", err, slog.String("protocol", protocol), slog.String("listener", name))
247 ln = tls.NewListener(ln, tlsConfig)
252 conn, err := ln.Accept()
254 log.Infox("smtp: accept", err, slog.String("protocol", protocol), slog.String("listener", name))
258 // Package is set on the resolver by the dkim/spf/dmarc/etc packages.
259 resolver := dns.StrictResolver{Log: log.Logger}
260 go serve(name, mox.Cid(), hostname, tlsConfig, conn, resolver, submission, xtls, maxMessageSize, requireTLSForAuth, requireTLSForDelivery, requireTLS, dnsBLs, firstTimeSenderDelay)
264 servers = append(servers, serve)
267// Serve starts serving on all listeners, launching a goroutine per listener.
269 for _, serve := range servers {
277 // OrigConn is the original (TCP) connection. We'll read from/write to conn, which
278 // can be wrapped in a tls.Server. We close origConn instead of conn because
279 // closing the TLS connection would send a TLS close notification, which may block
280 // for 5s if the server isn't reading it (because it is also sending it).
285 extRequireTLS bool // Whether to announce and allow the REQUIRETLS extension.
286 resolver dns.Resolver
289 tr *moxio.TraceReader // Kept for changing trace level during cmd/auth/data.
290 tw *moxio.TraceWriter
291 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.
292 lastlog time.Time // Used for printing the delta time since the previous logging for this connection.
294 tlsConfig *tls.Config
300 requireTLSForAuth bool
301 requireTLSForDelivery bool // If set, delivery is only allowed with TLS (STARTTLS), except if delivery is to a TLS reporting address.
302 cmd string // Current command.
303 cmdStart time.Time // Start of current command.
304 ncmds int // Number of commands processed. Used to abort connection when first incoming command is unknown/invalid.
306 firstTimeSenderDelay time.Duration
308 // If non-zero, taken into account during Read and Write. Set while processing DATA
309 // command, we don't want the entire delivery to take too long.
312 hello dns.IPDomain // Claimed remote name. Can be ip address for ehlo.
313 ehlo bool // If set, we had EHLO instead of HELO.
315 authFailed int // Number of failed auth attempts. For slowing down remote with many failures.
316 username string // Only when authenticated.
317 account *store.Account // Only when authenticated.
319 // We track good/bad message transactions to disconnect spammers trying to guess addresses.
323 // Message transaction.
325 requireTLS *bool // MAIL FROM with REQUIRETLS set.
326 has8bitmime bool // If MAIL FROM parameter BODY=8BITMIME was sent. Required for SMTPUTF8.
327 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.
328 recipients []rcptAccount
331type rcptAccount struct {
333 local bool // Whether recipient is a local user.
335 // Only valid for local delivery.
337 destination config.Destination
338 canonicalAddress string // Optional catchall part stripped and/or lowercased.
341func isClosed(err error) bool {
342 return errors.Is(err, errIO) || moxio.IsClosed(err)
345// completely reset connection state as if greeting has just been sent.
347func (c *conn) reset() {
349 c.hello = dns.IPDomain{}
351 if c.account != nil {
352 err := c.account.Close()
353 c.log.Check(err, "closing account")
359// for rset command, and a few more cases that reset the mail transaction state.
361func (c *conn) rset() {
364 c.has8bitmime = false
369func (c *conn) earliestDeadline(d time.Duration) time.Time {
370 e := time.Now().Add(d)
371 if !c.deadline.IsZero() && c.deadline.Before(e) {
377func (c *conn) xcheckAuth() {
378 if c.submission && c.account == nil {
380 xsmtpUserErrorf(smtp.C530SecurityRequired, smtp.SePol7Other0, "authentication required")
384func (c *conn) xtrace(level slog.Level) func() {
390 c.tr.SetTrace(mlog.LevelTrace)
391 c.tw.SetTrace(mlog.LevelTrace)
395// setSlow marks the connection slow (or now), so reads are done with 3 second
396// delay for each read, and writes are done at 1 byte per second, to try to slow
398func (c *conn) setSlow(on bool) {
400 c.log.Debug("connection changed to slow")
401 } else if !on && c.slow {
402 c.log.Debug("connection restored to regular pace")
407// Write writes to the connection. It panics on i/o errors, which is handled by the
408// connection command loop.
409func (c *conn) Write(buf []byte) (int, error) {
415 // We set a single deadline for Write and Read. This may be a TLS connection.
416 // SetDeadline works on the underlying connection. If we wouldn't touch the read
417 // deadline, and only set the write deadline and do a bunch of writes, the TLS
418 // library would still have to do reads on the underlying connection, and may reach
419 // a read deadline that was set for some earlier read.
420 // We have one deadline for the whole write. In case of slow writing, we'll write
421 // the last chunk in one go, so remote smtp clients don't abort the connection for
423 deadline := c.earliestDeadline(30 * time.Second)
424 if err := c.conn.SetDeadline(deadline); err != nil {
425 c.log.Errorx("setting deadline for write", err)
430 nn, err := c.conn.Write(buf[:chunk])
432 panic(fmt.Errorf("write: %s (%w)", err, errIO))
436 if len(buf) > 0 && badClientDelay > 0 {
437 mox.Sleep(mox.Context, badClientDelay)
439 // Make sure we don't take too long, otherwise the remote SMTP client may close the
441 if time.Until(deadline) < 2*badClientDelay {
449// Read reads from the connection. It panics on i/o errors, which is handled by the
450// connection command loop.
451func (c *conn) Read(buf []byte) (int, error) {
452 if c.slow && badClientDelay > 0 {
453 mox.Sleep(mox.Context, badClientDelay)
457 // See comment about Deadline instead of individual read/write deadlines at Write.
458 if err := c.conn.SetDeadline(c.earliestDeadline(30 * time.Second)); err != nil {
459 c.log.Errorx("setting deadline for read", err)
462 n, err := c.conn.Read(buf)
464 panic(fmt.Errorf("read: %s (%w)", err, errIO))
469// Cache of line buffers for reading commands.
471var bufpool = moxio.NewBufpool(8, 2*1024)
473func (c *conn) readline() string {
474 line, err := bufpool.Readline(c.log, c.r)
475 if err != nil && errors.Is(err, moxio.ErrLineTooLong) {
476 c.writecodeline(smtp.C500BadSyntax, smtp.SeProto5Other0, "line too long, smtp max is 512, we reached 2048", nil)
477 panic(fmt.Errorf("%s (%w)", err, errIO))
478 } else if err != nil {
479 panic(fmt.Errorf("%s (%w)", err, errIO))
484// Buffered-write command response line to connection with codes and msg.
485// Err is not sent to remote but is used for logging and can be empty.
486func (c *conn) bwritecodeline(code int, secode string, msg string, err error) {
489 ecode = fmt.Sprintf("%d.%s", code/100, secode)
491 metricCommands.WithLabelValues(c.kind(), c.cmd, fmt.Sprintf("%d", code), ecode).Observe(float64(time.Since(c.cmdStart)) / float64(time.Second))
492 c.log.Debugx("smtp command result", err,
493 slog.String("kind", c.kind()),
494 slog.String("cmd", c.cmd),
495 slog.Int("code", code),
496 slog.String("ecode", ecode),
497 slog.Duration("duration", time.Since(c.cmdStart)))
504 // Separate by newline and wrap long lines.
505 lines := strings.Split(msg, "\n")
506 for i, line := range lines {
508 var prelen = 3 + 1 + len(ecode) + len(sep)
509 for prelen+len(line) > 510 {
511 for ; e > 400 && line[e] != ' '; e-- {
513 // 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.
514 c.bwritelinef("%d-%s%s%s", code, ecode, sep, line[:e])
518 if i < len(lines)-1 {
521 c.bwritelinef("%d%s%s%s%s", code, spdash, ecode, sep, line)
525// Buffered-write a formatted response line to connection.
526func (c *conn) bwritelinef(format string, args ...any) {
527 msg := fmt.Sprintf(format, args...)
528 fmt.Fprint(c.w, msg+"\r\n")
531// Flush pending buffered writes to connection.
532func (c *conn) xflush() {
533 c.w.Flush() // Errors will have caused a panic in Write.
536// Write (with flush) a response line with codes and message. err is not written, used for logging and can be nil.
537func (c *conn) writecodeline(code int, secode string, msg string, err error) {
538 c.bwritecodeline(code, secode, msg, err)
542// Write (with flush) a formatted response line to connection.
543func (c *conn) writelinef(format string, args ...any) {
544 c.bwritelinef(format, args...)
548var cleanClose struct{} // Sentinel value for panic/recover indicating clean close of connection.
550func 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) {
551 var localIP, remoteIP net.IP
552 if a, ok := nc.LocalAddr().(*net.TCPAddr); ok {
555 // For net.Pipe, during tests.
556 localIP = net.ParseIP("127.0.0.10")
558 if a, ok := nc.RemoteAddr().(*net.TCPAddr); ok {
561 // For net.Pipe, during tests.
562 remoteIP = net.ParseIP("127.0.0.10")
569 submission: submission,
571 extRequireTLS: requireTLS,
574 tlsConfig: tlsConfig,
578 maxMessageSize: maxMessageSize,
579 requireTLSForAuth: requireTLSForAuth,
580 requireTLSForDelivery: requireTLSForDelivery,
582 firstTimeSenderDelay: firstTimeSenderDelay,
584 var logmutex sync.Mutex
585 c.log = mlog.New("smtpserver", nil).WithFunc(func() []slog.Attr {
587 defer logmutex.Unlock()
590 slog.Int64("cid", c.cid),
591 slog.Duration("delta", now.Sub(c.lastlog)),
594 if c.username != "" {
595 l = append(l, slog.String("username", c.username))
599 c.tr = moxio.NewTraceReader(c.log, "RC: ", c)
600 c.tw = moxio.NewTraceWriter(c.log, "LS: ", c)
601 c.r = bufio.NewReader(c.tr)
602 c.w = bufio.NewWriter(c.tw)
604 metricConnection.WithLabelValues(c.kind()).Inc()
605 c.log.Info("new connection",
606 slog.Any("remote", c.conn.RemoteAddr()),
607 slog.Any("local", c.conn.LocalAddr()),
608 slog.Bool("submission", submission),
609 slog.Bool("tls", tls),
610 slog.String("listener", listenerName))
613 c.origConn.Close() // Close actual TCP socket, regardless of TLS on top.
614 c.conn.Close() // If TLS, will try to write alert notification to already closed socket, returning error quickly.
616 if c.account != nil {
617 err := c.account.Close()
618 c.log.Check(err, "closing account")
623 if x == nil || x == cleanClose {
624 c.log.Info("connection closed")
625 } else if err, ok := x.(error); ok && isClosed(err) {
626 c.log.Infox("connection closed", err)
628 c.log.Error("unhandled panic", slog.Any("err", x))
630 metrics.PanicInc(metrics.Smtpserver)
635 case <-mox.Shutdown.Done():
637 c.writecodeline(smtp.C421ServiceUnavail, smtp.SeSys3NotAccepting2, "shutting down", nil)
642 if !limiterConnectionRate.Add(c.remoteIP, time.Now(), 1) {
643 c.writecodeline(smtp.C421ServiceUnavail, smtp.SePol7Other0, "connection rate from your ip or network too high, slow down please", nil)
647 // If remote IP/network resulted in too many authentication failures, refuse to serve.
648 if submission && !mox.LimiterFailedAuth.CanAdd(c.remoteIP, time.Now(), 1) {
649 metrics.AuthenticationRatelimitedInc("submission")
650 c.log.Debug("refusing connection due to many auth failures", slog.Any("remoteip", c.remoteIP))
651 c.writecodeline(smtp.C421ServiceUnavail, smtp.SePol7Other0, "too many auth failures", nil)
655 if !limiterConnections.Add(c.remoteIP, time.Now(), 1) {
656 c.log.Debug("refusing connection due to many open connections", slog.Any("remoteip", c.remoteIP))
657 c.writecodeline(smtp.C421ServiceUnavail, smtp.SePol7Other0, "too many open connections from your ip or network", nil)
660 defer limiterConnections.Add(c.remoteIP, time.Now(), -1)
662 // We register and unregister the original connection, in case c.conn is replaced
663 // with a TLS connection later on.
664 mox.Connections.Register(nc, "smtp", listenerName)
665 defer mox.Connections.Unregister(nc)
669 // We include the string ESMTP. https://cr.yp.to/smtp/greeting.html recommends it.
670 // Should not be too relevant nowadays, but does not hurt and default blackbox
671 // exporter SMTP health check expects it.
672 c.writelinef("%d %s ESMTP mox %s", smtp.C220ServiceReady, c.hostname.ASCII, moxvar.Version)
677 // If another command is present, don't flush our buffered response yet. Holding
678 // off will cause us to respond with a single packet.
681 buf, err := c.r.Peek(n)
682 if err == nil && bytes.IndexByte(buf, '\n') >= 0 {
690var commands = map[string]func(c *conn, p *parser){
691 "helo": (*conn).cmdHelo,
692 "ehlo": (*conn).cmdEhlo,
693 "starttls": (*conn).cmdStarttls,
694 "auth": (*conn).cmdAuth,
695 "mail": (*conn).cmdMail,
696 "rcpt": (*conn).cmdRcpt,
697 "data": (*conn).cmdData,
698 "rset": (*conn).cmdRset,
699 "vrfy": (*conn).cmdVrfy,
700 "expn": (*conn).cmdExpn,
701 "help": (*conn).cmdHelp,
702 "noop": (*conn).cmdNoop,
703 "quit": (*conn).cmdQuit,
706func command(c *conn) {
722 if errors.As(err, &serr) {
723 c.writecodeline(serr.code, serr.secode, fmt.Sprintf("%s (%s)", serr.errmsg, mox.ReceivedID(c.cid)), serr.err)
728 // Other type of panic, we pass it on, aborting the connection.
729 c.log.Errorx("command panic", err)
734 // todo future: we could wait for either a line or shutdown, and just close the connection on shutdown.
737 t := strings.SplitN(line, " ", 2)
743 cmdl := strings.ToLower(cmd)
745 // 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
748 case <-mox.Shutdown.Done():
750 c.writecodeline(smtp.C421ServiceUnavail, smtp.SeSys3NotAccepting2, "shutting down", nil)
756 c.cmdStart = time.Now()
758 p := newParser(args, c.smtputf8, c)
759 fn, ok := commands[cmdl]
763 // Other side is likely speaking something else than SMTP, send error message and
764 // stop processing because there is a good chance whatever they sent has multiple
766 c.writecodeline(smtp.C500BadSyntax, smtp.SeProto5Syntax2, "please try again speaking smtp", nil)
770 xsmtpUserErrorf(smtp.C500BadSyntax, smtp.SeProto5BadCmdOrSeq1, "unknown command")
776// For use in metric labels.
777func (c *conn) kind() string {
784func (c *conn) xneedHello() {
785 if c.hello.IsZero() {
786 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "no ehlo/helo yet")
790// If smtp server is configured to require TLS for all mail delivery (except to TLS
791// reporting address), abort command.
792func (c *conn) xneedTLSForDelivery(rcpt smtp.Path) {
793 // For TLS reports, we allow the message in even without TLS, because there may be
795 if c.requireTLSForDelivery && !c.tls && !isTLSReportRecipient(rcpt) {
797 xsmtpUserErrorf(smtp.C530SecurityRequired, smtp.SePol7Other0, "STARTTLS required for mail delivery")
801func isTLSReportRecipient(rcpt smtp.Path) bool {
802 _, _, dest, err := mox.FindAccount(rcpt.Localpart, rcpt.IPDomain.Domain, false)
803 return err == nil && (dest.HostTLSReports || dest.DomainTLSReports)
806func (c *conn) cmdHelo(p *parser) {
810func (c *conn) cmdEhlo(p *parser) {
815func (c *conn) cmdHello(p *parser, ehlo bool) {
816 var remote dns.IPDomain
817 if c.submission && !mox.Pedantic {
818 // Mail clients regularly put bogus information in the hostname/ip. For submission,
819 // the value is of no use, so there is not much point in annoying the user with
820 // errors they cannot fix themselves. Except when in pedantic mode.
821 remote = dns.IPDomain{IP: c.remoteIP}
825 remote = p.xipdomain(true)
827 remote = dns.IPDomain{Domain: p.xdomain()}
829 // Verify a remote domain name has an A or AAAA record, CNAME not allowed.
../rfc/5321:722
830 cidctx := context.WithValue(mox.Context, mlog.CidKey, c.cid)
831 ctx, cancel := context.WithTimeout(cidctx, time.Minute)
832 _, _, err := c.resolver.LookupIPAddr(ctx, remote.Domain.ASCII+".")
834 if dns.IsNotFound(err) {
835 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeProto5Other0, "your ehlo domain does not resolve to an IP address")
837 // For success or temporary resolve errors, we'll just continue.
840 // Though a few paragraphs earlier is a claim additional data can occur for address
841 // literals (IP addresses), although the ABNF in that document does not allow it.
842 // We allow additional text, but only if space-separated.
843 if len(remote.IP) > 0 && p.space() {
855 // https://www.iana.org/assignments/mail-parameters/mail-parameters.xhtml
857 c.bwritelinef("250-%s", c.hostname.ASCII)
861 if !c.tls && c.tlsConfig != nil {
863 c.bwritelinef("250-STARTTLS")
864 } else if c.extRequireTLS {
867 c.bwritelinef("250-REQUIRETLS")
871 if c.tls || !c.requireTLSForAuth {
872 // We always mention the SCRAM PLUS variants, even if TLS is not active: It is a
873 // hint to the client that a TLS connection can use TLS channel binding during
874 // authentication. The client should select the bare variant when TLS isn't
875 // present, and also not indicate the server supports the PLUS variant in that
876 // case, or it would trigger the mechanism downgrade detection.
877 c.bwritelinef("250-AUTH SCRAM-SHA-256-PLUS SCRAM-SHA-256 SCRAM-SHA-1-PLUS SCRAM-SHA-1 CRAM-MD5 PLAIN LOGIN")
879 c.bwritelinef("250-AUTH ")
883 // todo future? c.writelinef("250-DSN")
890func (c *conn) cmdStarttls(p *parser) {
896 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "already speaking tls")
898 if c.account != nil {
899 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "cannot starttls after authentication")
902 // We don't want to do TLS on top of c.r because it also prints protocol traces: We
903 // don't want to log the TLS stream. So we'll do TLS on the underlying connection,
904 // but make sure any bytes already read and in the buffer are used for the TLS
907 if n := c.r.Buffered(); n > 0 {
908 conn = &moxio.PrefixConn{
909 PrefixReader: io.LimitReader(c.r, int64(n)),
914 // We add the cid to the output, to help debugging in case of a failing TLS connection.
915 c.writecodeline(smtp.C220ServiceReady, smtp.SeOther00, "go! ("+mox.ReceivedID(c.cid)+")", nil)
916 tlsConn := tls.Server(conn, c.tlsConfig)
917 cidctx := context.WithValue(mox.Context, mlog.CidKey, c.cid)
918 ctx, cancel := context.WithTimeout(cidctx, time.Minute)
920 c.log.Debug("starting tls server handshake")
921 if err := tlsConn.HandshakeContext(ctx); err != nil {
922 panic(fmt.Errorf("starttls handshake: %s (%w)", err, errIO))
925 tlsversion, ciphersuite := moxio.TLSInfo(tlsConn)
926 c.log.Debug("tls server handshake done", slog.String("tls", tlsversion), slog.String("ciphersuite", ciphersuite))
928 c.tr = moxio.NewTraceReader(c.log, "RC: ", c)
929 c.tw = moxio.NewTraceWriter(c.log, "LS: ", c)
930 c.r = bufio.NewReader(c.tr)
931 c.w = bufio.NewWriter(c.tw)
938func (c *conn) cmdAuth(p *parser) {
942 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "authentication only allowed on submission ports")
944 if c.account != nil {
946 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "already authenticated")
948 if c.mailFrom != nil {
950 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "authentication not allowed during mail transaction")
953 // 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).
955 // For many failed auth attempts, slow down verification attempts.
956 // Dropping the connection could also work, but more so when we have a connection rate limiter.
958 if c.authFailed > 3 && authFailDelay > 0 {
960 mox.Sleep(mox.Context, time.Duration(c.authFailed-3)*authFailDelay)
962 c.authFailed++ // Compensated on success.
964 // On the 3rd failed authentication, start responding slowly. Successful auth will
965 // cause fast responses again.
966 if c.authFailed >= 3 {
971 var authVariant string
972 authResult := "error"
974 metrics.AuthenticationInc("submission", authVariant, authResult)
977 mox.LimiterFailedAuth.Reset(c.remoteIP, time.Now())
979 mox.LimiterFailedAuth.Add(c.remoteIP, time.Now(), 1)
985 mech := p.xsaslMech()
987 xreadInitial := func() []byte {
991 // todo future: handle max length of 12288 octets and return proper responde codes otherwise
../rfc/4954:253
995 authResult = "aborted"
996 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5Other0, "authentication aborted")
1001 // Windows Mail 16005.14326.21606.0 sends two spaces between "AUTH PLAIN" and the
1006 auth = p.remainder()
1009 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5Syntax2, "missing initial auth base64 parameter after space")
1010 } else if auth == "=" {
1012 auth = "" // Base64 decode below will result in empty buffer.
1015 buf, err := base64.StdEncoding.DecodeString(auth)
1018 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5Syntax2, "invalid base64: %s", err)
1023 xreadContinuation := func() []byte {
1024 line := c.readline()
1026 authResult = "aborted"
1027 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5Other0, "authentication aborted")
1029 buf, err := base64.StdEncoding.DecodeString(line)
1032 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5Syntax2, "invalid base64: %s", err)
1039 authVariant = "plain"
1043 if !c.tls && c.requireTLSForAuth {
1044 xsmtpUserErrorf(smtp.C538EncReqForAuth, smtp.SePol7EncReqForAuth11, "authentication requires tls")
1047 // Password is in line in plain text, so hide it.
1048 defer c.xtrace(mlog.LevelTraceauth)()
1049 buf := xreadInitial()
1050 c.xtrace(mlog.LevelTrace) // Restore.
1051 plain := bytes.Split(buf, []byte{0})
1052 if len(plain) != 3 {
1053 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5BadParams4, "auth data should have 3 nul-separated tokens, got %d", len(plain))
1055 authz := string(plain[0])
1056 authc := string(plain[1])
1057 password := string(plain[2])
1059 if authz != "" && authz != authc {
1060 authResult = "badcreds"
1061 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "cannot assume other role")
1064 acc, err := store.OpenEmailAuth(c.log, authc, password)
1065 if err != nil && errors.Is(err, store.ErrUnknownCredentials) {
1067 authResult = "badcreds"
1068 c.log.Info("failed authentication attempt", slog.String("username", authc), slog.Any("remote", c.remoteIP))
1069 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass")
1071 xcheckf(err, "verifying credentials")
1079 c.writecodeline(smtp.C235AuthSuccess, smtp.SePol7Other0, "nice", nil)
1082 // LOGIN is obsoleted in favor of PLAIN, only implemented to support legacy
1083 // clients, see Internet-Draft (I-D):
1084 // https://datatracker.ietf.org/doc/html/draft-murchison-sasl-login-00
1086 authVariant = "login"
1090 if !c.tls && c.requireTLSForAuth {
1091 xsmtpUserErrorf(smtp.C538EncReqForAuth, smtp.SePol7EncReqForAuth11, "authentication requires tls")
1094 // Read user name. The I-D says the client should ignore the server challenge, we
1095 // send an empty one.
1096 // I-D says maximum length must be 64 bytes. We allow more, for long user names
1098 username := string(xreadInitial())
1100 // Again, client should ignore the challenge, we send the same as the example in
1102 c.writelinef("%d %s", smtp.C334ContinueAuth, base64.StdEncoding.EncodeToString([]byte("Password")))
1104 // Password is in line in plain text, so hide it.
1105 defer c.xtrace(mlog.LevelTraceauth)()
1106 password := string(xreadContinuation())
1107 c.xtrace(mlog.LevelTrace) // Restore.
1109 acc, err := store.OpenEmailAuth(c.log, username, password)
1110 if err != nil && errors.Is(err, store.ErrUnknownCredentials) {
1112 authResult = "badcreds"
1113 c.log.Info("failed authentication attempt", slog.String("username", username), slog.Any("remote", c.remoteIP))
1114 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass")
1116 xcheckf(err, "verifying credentials")
1122 c.username = username
1124 c.writecodeline(smtp.C235AuthSuccess, smtp.SePol7Other0, "hello ancient smtp implementation", nil)
1127 authVariant = strings.ToLower(mech)
1132 chal := fmt.Sprintf("<%d.%d@%s>", uint64(mox.CryptoRandInt()), time.Now().UnixNano(), mox.Conf.Static.HostnameDomain.ASCII)
1133 c.writelinef("%d %s", smtp.C334ContinueAuth, base64.StdEncoding.EncodeToString([]byte(chal)))
1135 resp := xreadContinuation()
1136 t := strings.Split(string(resp), " ")
1137 if len(t) != 2 || len(t[1]) != 2*md5.Size {
1138 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5BadParams4, "malformed cram-md5 response")
1141 c.log.Debug("cram-md5 auth", slog.String("address", addr))
1142 acc, _, err := store.OpenEmail(c.log, addr)
1144 if errors.Is(err, store.ErrUnknownCredentials) {
1145 c.log.Info("failed authentication attempt", slog.String("username", addr), slog.Any("remote", c.remoteIP))
1146 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass")
1149 xcheckf(err, "looking up address")
1153 c.log.Check(err, "closing account")
1156 var ipadhash, opadhash hash.Hash
1157 acc.WithRLock(func() {
1158 err := acc.DB.Read(context.TODO(), func(tx *bstore.Tx) error {
1159 password, err := bstore.QueryTx[store.Password](tx).Get()
1160 if err == bstore.ErrAbsent {
1161 c.log.Info("failed authentication attempt", slog.String("username", addr), slog.Any("remote", c.remoteIP))
1162 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass")
1168 ipadhash = password.CRAMMD5.Ipad
1169 opadhash = password.CRAMMD5.Opad
1172 xcheckf(err, "tx read")
1174 if ipadhash == nil || opadhash == nil {
1175 c.log.Info("cram-md5 auth attempt without derived secrets set, save password again to store secrets", slog.String("username", addr))
1176 c.log.Info("failed authentication attempt", slog.String("username", addr), slog.Any("remote", c.remoteIP))
1177 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass")
1181 ipadhash.Write([]byte(chal))
1182 opadhash.Write(ipadhash.Sum(nil))
1183 digest := fmt.Sprintf("%x", opadhash.Sum(nil))
1185 c.log.Info("failed authentication attempt", slog.String("username", addr), slog.Any("remote", c.remoteIP))
1186 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass")
1193 acc = nil // Cancel cleanup.
1196 c.writecodeline(smtp.C235AuthSuccess, smtp.SePol7Other0, "nice", nil)
1198 case "SCRAM-SHA-256-PLUS", "SCRAM-SHA-256", "SCRAM-SHA-1-PLUS", "SCRAM-SHA-1":
1199 // 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?
1200 // todo: use single implementation between ../imapserver/server.go and ../smtpserver/server.go
1202 // Passwords cannot be retrieved or replayed from the trace.
1204 authVariant = strings.ToLower(mech)
1205 var h func() hash.Hash
1206 switch authVariant {
1207 case "scram-sha-1", "scram-sha-1-plus":
1209 case "scram-sha-256", "scram-sha-256-plus":
1212 xsmtpServerErrorf(codes{smtp.C554TransactionFailed, smtp.SeSys3Other0}, "missing scram auth method case")
1215 var cs *tls.ConnectionState
1216 channelBindingRequired := strings.HasSuffix(authVariant, "-plus")
1217 if channelBindingRequired && !c.tls {
1219 xsmtpUserErrorf(smtp.C538EncReqForAuth, smtp.SePol7EncReqForAuth11, "scram plus mechanism requires tls connection")
1222 xcs := c.conn.(*tls.Conn).ConnectionState()
1225 c0 := xreadInitial()
1226 ss, err := scram.NewServer(h, c0, cs, channelBindingRequired)
1227 xcheckf(err, "starting scram")
1228 c.log.Debug("scram auth", slog.String("authentication", ss.Authentication))
1229 acc, _, err := store.OpenEmail(c.log, ss.Authentication)
1231 // todo: we could continue scram with a generated salt, deterministically generated
1232 // from the username. that way we don't have to store anything but attackers cannot
1233 // learn if an account exists. same for absent scram saltedpassword below.
1234 c.log.Info("failed authentication attempt", slog.String("username", ss.Authentication), slog.Any("remote", c.remoteIP))
1235 xsmtpUserErrorf(smtp.C454TempAuthFail, smtp.SeSys3Other0, "scram not possible")
1240 c.log.Check(err, "closing account")
1243 if ss.Authorization != "" && ss.Authorization != ss.Authentication {
1244 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "authentication with authorization for different user not supported")
1246 var xscram store.SCRAM
1247 acc.WithRLock(func() {
1248 err := acc.DB.Read(context.TODO(), func(tx *bstore.Tx) error {
1249 password, err := bstore.QueryTx[store.Password](tx).Get()
1250 switch authVariant {
1251 case "scram-sha-1", "scram-sha-1-plus":
1252 xscram = password.SCRAMSHA1
1253 case "scram-sha-256", "scram-sha-256-plus":
1254 xscram = password.SCRAMSHA256
1256 xsmtpServerErrorf(codes{smtp.C554TransactionFailed, smtp.SeSys3Other0}, "missing scram auth credentials case")
1258 if err == bstore.ErrAbsent || err == nil && (len(xscram.Salt) == 0 || xscram.Iterations == 0 || len(xscram.SaltedPassword) == 0) {
1259 c.log.Info("scram auth attempt without derived secrets set, save password again to store secrets", slog.String("address", ss.Authentication))
1260 c.log.Info("failed authentication attempt", slog.String("username", ss.Authentication), slog.Any("remote", c.remoteIP))
1261 xsmtpUserErrorf(smtp.C454TempAuthFail, smtp.SeSys3Other0, "scram not possible")
1263 xcheckf(err, "fetching credentials")
1266 xcheckf(err, "read tx")
1268 s1, err := ss.ServerFirst(xscram.Iterations, xscram.Salt)
1269 xcheckf(err, "scram first server step")
1270 c.writelinef("%d %s", smtp.C334ContinueAuth, base64.StdEncoding.EncodeToString([]byte(s1))) //
../rfc/4954:187
1271 c2 := xreadContinuation()
1272 s3, err := ss.Finish(c2, xscram.SaltedPassword)
1274 c.writelinef("%d %s", smtp.C334ContinueAuth, base64.StdEncoding.EncodeToString([]byte(s3))) //
../rfc/4954:187
1277 c.readline() // Should be "*" for cancellation.
1278 if errors.Is(err, scram.ErrInvalidProof) {
1279 authResult = "badcreds"
1280 c.log.Info("failed authentication attempt", slog.String("username", ss.Authentication), slog.Any("remote", c.remoteIP))
1281 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad credentials")
1283 xcheckf(err, "server final")
1287 // The message should be empty. todo: should we require it is empty?
1294 acc = nil // Cancel cleanup.
1295 c.username = ss.Authentication
1297 c.writecodeline(smtp.C235AuthSuccess, smtp.SePol7Other0, "nice", nil)
1301 xsmtpUserErrorf(smtp.C504ParamNotImpl, smtp.SeProto5BadParams4, "mechanism %s not supported", mech)
1306func (c *conn) cmdMail(p *parser) {
1307 // requirements for maximum line length:
1309 // todo future: enforce?
1311 if c.transactionBad > 10 && c.transactionGood == 0 {
1312 // If we get many bad transactions, it's probably a spammer that is guessing user names.
1313 // Useful in combination with rate limiting.
1315 c.writecodeline(smtp.C550MailboxUnavail, smtp.SeAddr1Other0, "too many failures", nil)
1321 if c.mailFrom != nil {
1323 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "already have MAIL")
1325 // Ensure clear transaction state on failure.
1336 // Microsoft Outlook 365 Apps for Enterprise sends it with submission. For delivery
1337 // it is mostly used by spammers, but has been seen with legitimate senders too.
1341 rawRevPath := p.xrawReversePath()
1342 paramSeen := map[string]bool{}
1345 key := p.xparamKeyword()
1347 K := strings.ToUpper(key)
1350 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5BadParams4, "duplicate param %q", key)
1358 if size > c.maxMessageSize {
1360 ecode := smtp.SeSys3MsgLimitExceeded4
1361 if size < config.DefaultMaxMsgSize {
1362 ecode = smtp.SeMailbox2MsgLimitExceeded3
1364 xsmtpUserErrorf(smtp.C552MailboxFull, ecode, "message too large")
1366 // We won't verify the message is exactly the size the remote claims. Buf if it is
1367 // larger, we'll abort the transaction when remote crosses the boundary.
1371 v := p.xparamValue()
1372 switch strings.ToUpper(v) {
1374 c.has8bitmime = false
1376 c.has8bitmime = true
1378 xsmtpUserErrorf(smtp.C555UnrecognizedAddrParams, smtp.SeProto5BadParams4, "unrecognized parameter %q", key)
1383 // We act as if we don't trust the client to specify a mailbox. Instead, we always
1384 // check the rfc5321.mailfrom and rfc5322.from before accepting the submission.
1388 // 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
1399 xsmtpUserErrorf(smtp.C530SecurityRequired, smtp.SePol7EncNeeded10, "requiretls only allowed on tls-encrypted connections")
1400 } else if !c.extRequireTLS {
1401 xsmtpUserErrorf(smtp.C555UnrecognizedAddrParams, smtp.SeSys3NotSupported3, "REQUIRETLS not allowed for this connection")
1407 xsmtpUserErrorf(smtp.C555UnrecognizedAddrParams, smtp.SeSys3NotSupported3, "unrecognized parameter %q", key)
1411 // We now know if we have to parse the address with support for utf8.
1412 pp := newParser(rawRevPath, c.smtputf8, c)
1413 rpath := pp.xbareReversePath()
1418 // For submission, check if reverse path is allowed. I.e. authenticated account
1419 // must have the rpath configured. We do a check again on rfc5322.from during DATA.
1420 rpathAllowed := func() bool {
1425 accName, _, _, err := mox.FindAccount(rpath.Localpart, rpath.IPDomain.Domain, false)
1426 return err == nil && accName == c.account.Name
1429 if !c.submission && !rpath.IPDomain.Domain.IsZero() {
1430 // If rpath domain has null MX record or is otherwise not accepting email, reject.
1433 cidctx := context.WithValue(mox.Context, mlog.CidKey, c.cid)
1434 ctx, cancel := context.WithTimeout(cidctx, time.Minute)
1435 valid, err := checkMXRecords(ctx, c.resolver, rpath.IPDomain.Domain)
1438 c.log.Infox("temporary reject for temporary mx lookup error", err)
1439 xsmtpServerErrorf(codes{smtp.C451LocalErr, smtp.SeNet4Other0}, "cannot verify mx records for mailfrom domain")
1441 c.log.Info("permanent reject because mailfrom domain does not accept mail")
1442 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SePol7SenderHasNullMX27, "mailfrom domain not configured for mail")
1446 if c.submission && (len(rpath.IPDomain.IP) > 0 || !rpathAllowed()) {
1448 c.log.Info("submission with unconfigured mailfrom", slog.String("user", c.username), slog.String("mailfrom", rpath.String()))
1449 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SePol7DeliveryUnauth1, "must match authenticated user")
1450 } else if !c.submission && len(rpath.IPDomain.IP) > 0 {
1451 // todo future: allow if the IP is the same as this connection is coming from? does later code allow this?
1452 c.log.Info("delivery from address without domain", slog.String("mailfrom", rpath.String()))
1453 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SePol7Other0, "domain name required")
1456 if Localserve && strings.HasPrefix(string(rpath.Localpart), "mailfrom") {
1457 c.xlocalserveError(rpath.Localpart)
1462 c.bwritecodeline(smtp.C250Completed, smtp.SeAddr1Other0, "looking good", nil)
1466func (c *conn) cmdRcpt(p *parser) {
1469 if c.mailFrom == nil {
1471 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "missing MAIL FROM")
1477 // Microsoft Outlook 365 Apps for Enterprise sends it with submission. For delivery
1478 // it is mostly used by spammers, but has been seen with legitimate senders too.
1483 if p.take("<POSTMASTER>") {
1484 fpath = smtp.Path{Localpart: "postmaster"}
1486 fpath = p.xforwardPath()
1490 key := p.xparamKeyword()
1491 // K := strings.ToUpper(key)
1494 xsmtpUserErrorf(smtp.C555UnrecognizedAddrParams, smtp.SeSys3NotSupported3, "unrecognized parameter %q", key)
1498 // Check if TLS is enabled if required. It's not great that sender/recipient
1499 // addresses may have been exposed in plaintext before we can reject delivery. The
1500 // recipient could be the tls reporting addresses, which must always be able to
1501 // receive in plain text.
1502 c.xneedTLSForDelivery(fpath)
1504 // todo future: for submission, should we do explicit verification that domains are fully qualified? also for mail from.
../rfc/6409:420
1506 if len(c.recipients) >= 100 {
1508 xsmtpUserErrorf(smtp.C452StorageFull, smtp.SeProto5TooManyRcpts3, "max of 100 recipients reached")
1511 // We don't want to allow delivery to multiple recipients with a null reverse path.
1512 // Why would anyone send like that? Null reverse path is intended for delivery
1513 // notifications, they should go to a single recipient.
1514 if !c.submission && len(c.recipients) > 0 && c.mailFrom.IsZero() {
1515 xsmtpUserErrorf(smtp.C452StorageFull, smtp.SeProto5TooManyRcpts3, "only one recipient allowed with null reverse address")
1518 // Do not accept multiple recipients if remote does not pass SPF. Because we don't
1519 // want to generate DSNs to unverified domains. This is the moment we
1520 // can refuse individual recipients, DATA will be too late. Because mail
1521 // servers must handle a max recipient limit gracefully and still send to the
1522 // recipients that are accepted, this should not cause problems. Though we are in
1523 // violation because the limit must be >= 100.
1527 if !c.submission && len(c.recipients) == 1 && !Localserve {
1528 // note: because of check above, mailFrom cannot be the null address.
1530 d := c.mailFrom.IPDomain.Domain
1532 // todo: use this spf result for DATA.
1533 spfArgs := spf.Args{
1534 RemoteIP: c.remoteIP,
1535 MailFromLocalpart: c.mailFrom.Localpart,
1537 HelloDomain: c.hello,
1539 LocalHostname: c.hostname,
1541 cidctx := context.WithValue(mox.Context, mlog.CidKey, c.cid)
1542 spfctx, spfcancel := context.WithTimeout(cidctx, time.Minute)
1544 receivedSPF, _, _, _, err := spf.Verify(spfctx, c.log.Logger, c.resolver, spfArgs)
1547 c.log.Errorx("spf verify for multiple recipients", err)
1549 pass = receivedSPF.Identity == spf.ReceivedMailFrom && receivedSPF.Result == spf.StatusPass
1552 xsmtpUserErrorf(smtp.C452StorageFull, smtp.SeProto5TooManyRcpts3, "only one recipient allowed without spf pass")
1557 if strings.HasPrefix(string(fpath.Localpart), "rcptto") {
1558 c.xlocalserveError(fpath.Localpart)
1561 // If account or destination doesn't exist, it will be handled during delivery. For
1562 // submissions, which is the common case, we'll deliver to the logged in user,
1563 // which is typically the mox user.
1564 acc, _ := mox.Conf.Account("mox")
1565 dest := acc.Destinations["mox@localhost"]
1566 c.recipients = append(c.recipients, rcptAccount{fpath, true, "mox", dest, "mox@localhost"})
1567 } else if len(fpath.IPDomain.IP) > 0 {
1569 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeAddr1UnknownDestMailbox1, "not accepting email for ip")
1571 c.recipients = append(c.recipients, rcptAccount{fpath, false, "", config.Destination{}, ""})
1572 } else if accountName, canonical, addr, err := mox.FindAccount(fpath.Localpart, fpath.IPDomain.Domain, true); err == nil {
1574 c.recipients = append(c.recipients, rcptAccount{fpath, true, accountName, addr, canonical})
1575 } else if errors.Is(err, mox.ErrDomainNotFound) {
1577 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeAddr1UnknownDestMailbox1, "not accepting email for domain")
1579 // We'll be delivering this email.
1580 c.recipients = append(c.recipients, rcptAccount{fpath, false, "", config.Destination{}, ""})
1581 } else if errors.Is(err, mox.ErrAccountNotFound) {
1583 // For submission, we're transparent about which user exists. Should be fine for the typical small-scale deploy.
1585 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeAddr1UnknownDestMailbox1, "no such user")
1587 // We pretend to accept. We don't want to let remote know the user does not exist
1588 // until after DATA. Because then remote has committed to sending a message.
1589 // note: not local for !c.submission is the signal this address is in error.
1590 c.recipients = append(c.recipients, rcptAccount{fpath, false, "", config.Destination{}, ""})
1592 c.log.Errorx("looking up account for delivery", err, slog.Any("rcptto", fpath))
1593 xsmtpServerErrorf(codes{smtp.C451LocalErr, smtp.SeSys3Other0}, "error processing")
1595 c.bwritecodeline(smtp.C250Completed, smtp.SeAddr1Other0, "now on the list", nil)
1599func (c *conn) cmdData(p *parser) {
1602 if c.mailFrom == nil {
1604 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "missing MAIL FROM")
1606 if len(c.recipients) == 0 {
1608 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "missing RCPT TO")
1614 // 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.
1616 // Entire delivery should be done within 30 minutes, or we abort.
1617 cidctx := context.WithValue(mox.Context, mlog.CidKey, c.cid)
1618 cmdctx, cmdcancel := context.WithTimeout(cidctx, 30*time.Minute)
1620 // Deadline is taken into account by Read and Write.
1621 c.deadline, _ = cmdctx.Deadline()
1623 c.deadline = time.Time{}
1627 c.writelinef("354 see you at the bare dot")
1629 // Mark as tracedata.
1630 defer c.xtrace(mlog.LevelTracedata)()
1632 // We read the data into a temporary file. We limit the size and do basic analysis while reading.
1633 dataFile, err := store.CreateMessageTemp(c.log, "smtp-deliver")
1635 xsmtpServerErrorf(errCodes(smtp.C451LocalErr, smtp.SeSys3Other0, err), "creating temporary file for message: %s", err)
1637 defer store.CloseRemoveTempFile(c.log, dataFile, "smtpserver delivered message")
1638 msgWriter := message.NewWriter(dataFile)
1639 dr := smtp.NewDataReader(c.r)
1640 n, err := io.Copy(&limitWriter{maxSize: c.maxMessageSize, w: msgWriter}, dr)
1641 c.xtrace(mlog.LevelTrace) // Restore.
1643 if errors.Is(err, errMessageTooLarge) {
1645 ecode := smtp.SeSys3MsgLimitExceeded4
1646 if n < config.DefaultMaxMsgSize {
1647 ecode = smtp.SeMailbox2MsgLimitExceeded3
1649 c.writecodeline(smtp.C451LocalErr, ecode, fmt.Sprintf("error copying data to file (%s)", mox.ReceivedID(c.cid)), err)
1650 panic(fmt.Errorf("remote sent too much DATA: %w", errIO))
1653 if errors.Is(err, smtp.ErrCRLF) {
1654 c.writecodeline(smtp.C500BadSyntax, smtp.SeProto5Syntax2, fmt.Sprintf("invalid bare \\r or \\n, may be smtp smuggling (%s)", mox.ReceivedID(c.cid)), err)
1658 // Something is failing on our side. We want to let remote know. So write an error response,
1659 // then discard the remaining data so the remote client is more likely to see our
1660 // response. Our write is synchronous, there is a risk no window/buffer space is
1661 // available and our write blocks us from reading remaining data, leading to
1662 // deadlock. We have a timeout on our connection writes though, so worst case we'll
1663 // abort the connection due to expiration.
1664 c.writecodeline(smtp.C451LocalErr, smtp.SeSys3Other0, fmt.Sprintf("error copying data to file (%s)", mox.ReceivedID(c.cid)), err)
1665 io.Copy(io.Discard, dr)
1669 // Basic sanity checks on messages before we send them out to the world. Just
1670 // trying to be strict in what we do to others and liberal in what we accept.
1672 if !msgWriter.HaveBody {
1674 xsmtpUserErrorf(smtp.C554TransactionFailed, smtp.SeMsg6Other0, "message requires both header and body section")
1676 // Check only for pedantic mode because ios mail will attempt to send smtputf8 with
1677 // non-ascii in message from localpart without using 8bitmime.
1678 if mox.Pedantic && msgWriter.Has8bit && !c.has8bitmime {
1680 xsmtpUserErrorf(smtp.C500BadSyntax, smtp.SeMsg6Other0, "message with non-us-ascii requires 8bitmime extension")
1684 if Localserve && mox.Pedantic {
1685 // Require that message can be parsed fully.
1686 p, err := message.Parse(c.log.Logger, false, dataFile)
1688 err = p.Walk(c.log.Logger, nil)
1692 xsmtpUserErrorf(smtp.C554TransactionFailed, smtp.SeMsg6Other0, "malformed message: %v", err)
1696 // Prepare "Received" header.
1700 var iprevStatus iprev.Status // Only for delivery, not submission.
1701 var iprevAuthentic bool
1703 // Hide internal hosts.
1704 // todo future: make this a config option, where admins specify ip ranges that they don't want exposed. also see
../rfc/5321:4321
1705 recvFrom = message.HeaderCommentDomain(mox.Conf.Static.HostnameDomain, c.smtputf8)
1707 if len(c.hello.IP) > 0 {
1708 recvFrom = smtp.AddressLiteral(c.hello.IP)
1710 // ASCII-only version added after the extended-domain syntax below, because the
1711 // comment belongs to "BY" which comes immediately after "FROM".
1712 recvFrom = c.hello.Domain.XName(c.smtputf8)
1714 iprevctx, iprevcancel := context.WithTimeout(cmdctx, time.Minute)
1716 var revNames []string
1717 iprevStatus, revName, revNames, iprevAuthentic, err = iprev.Lookup(iprevctx, c.resolver, c.remoteIP)
1720 c.log.Infox("reverse-forward lookup", err, slog.Any("remoteip", c.remoteIP))
1722 c.log.Debug("dns iprev check", slog.Any("addr", c.remoteIP), slog.Any("status", iprevStatus))
1726 } else if len(revNames) > 0 {
1729 name = strings.TrimSuffix(name, ".")
1731 if name != "" && name != c.hello.Domain.XName(c.smtputf8) {
1732 recvFrom += name + " "
1734 recvFrom += smtp.AddressLiteral(c.remoteIP) + ")"
1735 if c.smtputf8 && c.hello.Domain.Unicode != "" {
1736 recvFrom += " (" + c.hello.Domain.ASCII + ")"
1739 recvBy := mox.Conf.Static.HostnameDomain.XName(c.smtputf8)
1740 recvBy += " (" + smtp.AddressLiteral(c.localIP) + ")" // todo: hide ip if internal?
1741 if c.smtputf8 && mox.Conf.Static.HostnameDomain.Unicode != "" {
1742 // This syntax is part of "VIA".
1743 recvBy += " (" + mox.Conf.Static.HostnameDomain.ASCII + ")"
1756 if c.account != nil {
1761 // Assume transaction does not succeed. If it does, we'll compensate.
1764 recvHdrFor := func(rcptTo string) string {
1765 recvHdr := &message.HeaderWriter{}
1766 // For additional Received-header clauses, see:
1767 // https://www.iana.org/assignments/mail-parameters/mail-parameters.xhtml#table-mail-parameters-8
1769 if c.requireTLS != nil && *c.requireTLS {
1771 withComment = " (requiretls)"
1773 recvHdr.Add(" ", "Received:", "from", recvFrom, "by", recvBy, "via", "tcp", "with", with+withComment, "id", mox.ReceivedID(c.cid)) //
../rfc/5321:3158
1775 tlsConn := c.conn.(*tls.Conn)
1776 tlsComment := mox.TLSReceivedComment(c.log, tlsConn.ConnectionState())
1777 recvHdr.Add(" ", tlsComment...)
1779 recvHdr.Add(" ", "for", "<"+rcptTo+">;", time.Now().Format(message.RFC5322Z))
1780 return recvHdr.String()
1783 // Submission is easiest because user is trusted. Far fewer checks to make. So
1784 // handle it first, and leave the rest of the function for handling wild west
1785 // internet traffic.
1787 c.submit(cmdctx, recvHdrFor, msgWriter, dataFile)
1789 c.deliver(cmdctx, recvHdrFor, msgWriter, iprevStatus, iprevAuthentic, dataFile)
1793// Check if a message has unambiguous "TLS-Required: No" header. Messages must not
1794// contain multiple TLS-Required headers. The only valid value is "no". But we'll
1795// accept multiple headers as long as all they are all "no".
1797func hasTLSRequiredNo(h textproto.MIMEHeader) bool {
1798 l := h.Values("Tls-Required")
1802 for _, v := range l {
1803 if !strings.EqualFold(v, "no") {
1810// submit is used for mail from authenticated users that we will try to deliver.
1811func (c *conn) submit(ctx context.Context, recvHdrFor func(string) string, msgWriter *message.Writer, dataFile *os.File) {
1812 // Similar between ../smtpserver/server.go:/submit\( and ../webmail/webmail.go:/MessageSubmit\(
1814 var msgPrefix []byte
1816 // Check that user is only sending email as one of its configured identities. Not
1820 msgFrom, _, header, err := message.From(c.log.Logger, true, dataFile)
1822 metricSubmission.WithLabelValues("badmessage").Inc()
1823 c.log.Infox("parsing message From address", err, slog.String("user", c.username))
1824 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeMsg6Other0, "cannot parse header or From address: %v", err)
1826 accName, _, _, err := mox.FindAccount(msgFrom.Localpart, msgFrom.Domain, true)
1827 if err != nil || accName != c.account.Name {
1830 err = mox.ErrAccountNotFound
1832 metricSubmission.WithLabelValues("badfrom").Inc()
1833 c.log.Infox("verifying message From address", err, slog.String("user", c.username), slog.Any("msgfrom", msgFrom))
1834 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SePol7DeliveryUnauth1, "must match authenticated user")
1837 // TLS-Required: No header makes us not enforce recipient domain's TLS policy.
1840 if c.requireTLS == nil && hasTLSRequiredNo(header) {
1845 // Outgoing messages should not have a Return-Path header. The final receiving mail
1846 // server will add it.
1848 if mox.Pedantic && header.Values("Return-Path") != nil {
1849 metricSubmission.WithLabelValues("badheader").Inc()
1850 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeMsg6Other0, "message should not have Return-Path header")
1853 // Add Message-Id header if missing.
1855 messageID := header.Get("Message-Id")
1856 if messageID == "" {
1857 messageID = mox.MessageIDGen(c.smtputf8)
1858 msgPrefix = append(msgPrefix, fmt.Sprintf("Message-Id: <%s>\r\n", messageID)...)
1862 if header.Get("Date") == "" {
1863 msgPrefix = append(msgPrefix, "Date: "+time.Now().Format(message.RFC5322Z)+"\r\n"...)
1866 // Check outoging message rate limit.
1867 err = c.account.DB.Read(ctx, func(tx *bstore.Tx) error {
1868 rcpts := make([]smtp.Path, len(c.recipients))
1869 for i, r := range c.recipients {
1872 msglimit, rcptlimit, err := c.account.SendLimitReached(tx, rcpts)
1873 xcheckf(err, "checking sender limit")
1875 metricSubmission.WithLabelValues("messagelimiterror").Inc()
1876 xsmtpUserErrorf(smtp.C451LocalErr, smtp.SePol7DeliveryUnauth1, "max number of messages (%d) over past 24h reached, try increasing per-account setting MaxOutgoingMessagesPerDay", msglimit)
1877 } else if rcptlimit >= 0 {
1878 metricSubmission.WithLabelValues("recipientlimiterror").Inc()
1879 xsmtpUserErrorf(smtp.C451LocalErr, smtp.SePol7DeliveryUnauth1, "max number of new/first-time recipients (%d) over past 24h reached, try increasing per-account setting MaxFirstTimeRecipientsPerDay", rcptlimit)
1883 xcheckf(err, "read-only transaction")
1885 // 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.
1887 // Add DKIM signatures.
1888 confDom, ok := mox.Conf.Domain(msgFrom.Domain)
1890 c.log.Error("domain disappeared", slog.Any("domain", msgFrom.Domain))
1891 xsmtpServerErrorf(codes{smtp.C451LocalErr, smtp.SeSys3Other0}, "internal error")
1894 selectors := mox.DKIMSelectors(confDom.DKIM)
1895 if len(selectors) > 0 {
1896 if canonical, err := mox.CanonicalLocalpart(msgFrom.Localpart, confDom); err != nil {
1897 c.log.Errorx("determining canonical localpart for dkim signing", err, slog.Any("localpart", msgFrom.Localpart))
1898 } else if dkimHeaders, err := dkim.Sign(ctx, c.log.Logger, canonical, msgFrom.Domain, selectors, c.smtputf8, store.FileMsgReader(msgPrefix, dataFile)); err != nil {
1899 c.log.Errorx("dkim sign for domain", err, slog.Any("domain", msgFrom.Domain))
1900 metricServerErrors.WithLabelValues("dkimsign").Inc()
1902 msgPrefix = append(msgPrefix, []byte(dkimHeaders)...)
1906 authResults := message.AuthResults{
1907 Hostname: mox.Conf.Static.HostnameDomain.XName(c.smtputf8),
1908 Comment: mox.Conf.Static.HostnameDomain.ASCIIExtra(c.smtputf8),
1909 Methods: []message.AuthMethod{
1913 Props: []message.AuthProp{
1914 message.MakeAuthProp("smtp", "mailfrom", c.mailFrom.XString(c.smtputf8), true, c.mailFrom.ASCIIExtra(c.smtputf8)),
1919 msgPrefix = append(msgPrefix, []byte(authResults.Header())...)
1921 // We always deliver through the queue. It would be more efficient to deliver
1922 // directly, but we don't want to circumvent all the anti-spam measures. Accounts
1923 // on a single mox instance should be allowed to block each other.
1924 for _, rcptAcc := range c.recipients {
1926 code, timeout := localserveNeedsError(rcptAcc.rcptTo.Localpart)
1928 c.log.Info("timing out submission due to special localpart")
1929 mox.Sleep(mox.Context, time.Hour)
1930 xsmtpServerErrorf(codes{smtp.C451LocalErr, smtp.SeSys3Other0}, "timing out submission due to special localpart")
1931 } else if code != 0 {
1932 c.log.Info("failure due to special localpart", slog.Int("code", code))
1933 xsmtpServerErrorf(codes{code, smtp.SeOther00}, "failure with code %d due to special localpart", code)
1937 xmsgPrefix := append([]byte(recvHdrFor(rcptAcc.rcptTo.String())), msgPrefix...)
1939 msgSize := int64(len(xmsgPrefix)) + msgWriter.Size
1940 qm := queue.MakeMsg(c.account.Name, *c.mailFrom, rcptAcc.rcptTo, msgWriter.Has8bit, c.smtputf8, msgSize, messageID, xmsgPrefix, c.requireTLS)
1941 if err := queue.Add(ctx, c.log, &qm, dataFile); err != nil {
1942 // Aborting the transaction is not great. But continuing and generating DSNs will
1943 // probably result in errors as well...
1944 metricSubmission.WithLabelValues("queueerror").Inc()
1945 c.log.Errorx("queuing message", err)
1946 xsmtpServerErrorf(errCodes(smtp.C451LocalErr, smtp.SeSys3Other0, err), "error delivering message: %v", err)
1948 metricSubmission.WithLabelValues("ok").Inc()
1949 c.log.Info("message queued for delivery",
1950 slog.Any("mailfrom", *c.mailFrom),
1951 slog.Any("rcptto", rcptAcc.rcptTo),
1952 slog.Bool("smtputf8", c.smtputf8),
1953 slog.Int64("msgsize", msgSize))
1955 err := c.account.DB.Insert(ctx, &store.Outgoing{Recipient: rcptAcc.rcptTo.XString(true)})
1956 xcheckf(err, "adding outgoing message")
1960 c.transactionBad-- // Compensate for early earlier pessimistic increase.
1963 c.writecodeline(smtp.C250Completed, smtp.SeMailbox2Other0, "it is done", nil)
1966func ipmasked(ip net.IP) (string, string, string) {
1967 if ip.To4() != nil {
1969 m2 := ip.Mask(net.CIDRMask(26, 32)).String()
1970 m3 := ip.Mask(net.CIDRMask(21, 32)).String()
1973 m1 := ip.Mask(net.CIDRMask(64, 128)).String()
1974 m2 := ip.Mask(net.CIDRMask(48, 128)).String()
1975 m3 := ip.Mask(net.CIDRMask(32, 128)).String()
1979func localserveNeedsError(lp smtp.Localpart) (code int, timeout bool) {
1981 if strings.HasSuffix(s, "temperror") {
1982 return smtp.C451LocalErr, false
1983 } else if strings.HasSuffix(s, "permerror") {
1984 return smtp.C550MailboxUnavail, false
1985 } else if strings.HasSuffix(s, "timeout") {
1992 v, err := strconv.ParseInt(s, 10, 32)
1996 if v < 400 || v > 600 {
1999 return int(v), false
2002func (c *conn) xlocalserveError(lp smtp.Localpart) {
2003 code, timeout := localserveNeedsError(lp)
2005 c.log.Info("timing out due to special localpart")
2006 mox.Sleep(mox.Context, time.Hour)
2007 xsmtpServerErrorf(codes{smtp.C451LocalErr, smtp.SeSys3Other0}, "timing out command due to special localpart")
2008 } else if code != 0 {
2009 c.log.Info("failure due to special localpart", slog.Int("code", code))
2010 metricDelivery.WithLabelValues("delivererror", "localserve").Inc()
2011 xsmtpServerErrorf(codes{code, smtp.SeOther00}, "failure with code %d due to special localpart", code)
2015// deliver is called for incoming messages from external, typically untrusted
2016// sources. i.e. not submitted by authenticated users.
2017func (c *conn) deliver(ctx context.Context, recvHdrFor func(string) string, msgWriter *message.Writer, iprevStatus iprev.Status, iprevAuthentic bool, dataFile *os.File) {
2018 // 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.
2020 msgFrom, envelope, headers, err := message.From(c.log.Logger, false, dataFile)
2022 c.log.Infox("parsing message for From address", err)
2026 if len(headers.Values("Received")) > 100 {
2027 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeNet4Loop6, "loop detected, more than 100 Received headers")
2030 // TLS-Required: No header makes us not enforce recipient domain's TLS policy.
2031 // Since we only deliver locally at the moment, this won't influence our behaviour.
2032 // Once we forward, it would our delivery attempts.
2035 if c.requireTLS == nil && hasTLSRequiredNo(headers) {
2040 // We'll be building up an Authentication-Results header.
2041 authResults := message.AuthResults{
2042 Hostname: mox.Conf.Static.HostnameDomain.XName(c.smtputf8),
2045 commentAuthentic := func(v bool) string {
2047 return "with dnssec"
2049 return "without dnssec"
2052 // Reverse IP lookup results.
2053 // todo future: how useful is this?
2055 authResults.Methods = append(authResults.Methods, message.AuthMethod{
2057 Result: string(iprevStatus),
2058 Comment: commentAuthentic(iprevAuthentic),
2059 Props: []message.AuthProp{
2060 message.MakeAuthProp("policy", "iprev", c.remoteIP.String(), false, ""),
2064 // SPF and DKIM verification in parallel.
2065 var wg sync.WaitGroup
2069 var dkimResults []dkim.Result
2073 x := recover() // Should not happen, but don't take program down if it does.
2075 c.log.Error("dkim verify panic", slog.Any("err", x))
2077 metrics.PanicInc(metrics.Dkimverify)
2081 // We always evaluate all signatures. We want to build up reputation for each
2082 // domain in the signature.
2083 const ignoreTestMode = false
2084 // todo future: longer timeout? we have to read through the entire email, which can be large, possibly multiple times.
2085 dkimctx, dkimcancel := context.WithTimeout(ctx, time.Minute)
2087 // todo future: we could let user configure which dkim headers they require
2088 dkimResults, dkimErr = dkim.Verify(dkimctx, c.log.Logger, c.resolver, c.smtputf8, dkim.DefaultPolicy, dataFile, ignoreTestMode)
2094 var receivedSPF spf.Received
2095 var spfDomain dns.Domain
2097 var spfAuthentic bool
2099 spfArgs := spf.Args{
2100 RemoteIP: c.remoteIP,
2101 MailFromLocalpart: c.mailFrom.Localpart,
2102 MailFromDomain: c.mailFrom.IPDomain.Domain, // Can be empty.
2103 HelloDomain: c.hello,
2105 LocalHostname: c.hostname,
2110 x := recover() // Should not happen, but don't take program down if it does.
2112 c.log.Error("spf verify panic", slog.Any("err", x))
2114 metrics.PanicInc(metrics.Spfverify)
2118 spfctx, spfcancel := context.WithTimeout(ctx, time.Minute)
2120 receivedSPF, spfDomain, spfExpl, spfAuthentic, spfErr = spf.Verify(spfctx, c.log.Logger, c.resolver, spfArgs)
2123 c.log.Infox("spf verify", spfErr)
2127 // Wait for DKIM and SPF validation to finish.
2130 // Give immediate response if all recipients are unknown.
2132 for _, r := range c.recipients {
2137 if nunknown == len(c.recipients) {
2138 // During RCPT TO we found that the address does not exist.
2139 c.log.Info("deliver attempt to unknown user(s)", slog.Any("recipients", c.recipients))
2141 // Crude attempt to slow down someone trying to guess names. Would work better
2142 // with connection rate limiter.
2143 if unknownRecipientsDelay > 0 {
2144 mox.Sleep(ctx, unknownRecipientsDelay)
2147 // 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.
2148 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeAddr1UnknownDestMailbox1, "no such user(s)")
2151 // Add DKIM results to Authentication-Results header.
2152 authResAddDKIM := func(result, comment, reason string, props []message.AuthProp) {
2153 dm := message.AuthMethod{
2160 authResults.Methods = append(authResults.Methods, dm)
2163 c.log.Errorx("dkim verify", dkimErr)
2164 authResAddDKIM("none", "", dkimErr.Error(), nil)
2165 } else if len(dkimResults) == 0 {
2166 c.log.Info("no dkim-signature header", slog.Any("mailfrom", c.mailFrom))
2167 authResAddDKIM("none", "", "no dkim signatures", nil)
2169 for i, r := range dkimResults {
2170 var domain, selector dns.Domain
2171 var identity *dkim.Identity
2173 var props []message.AuthProp
2175 if r.Record != nil && r.Record.PublicKey != nil {
2176 if pubkey, ok := r.Record.PublicKey.(*rsa.PublicKey); ok {
2177 comment = fmt.Sprintf("%d bit rsa, ", pubkey.N.BitLen())
2181 sig := base64.StdEncoding.EncodeToString(r.Sig.Signature)
2182 sig = sig[:12] // Must be at least 8 characters and unique among the signatures.
2183 props = []message.AuthProp{
2184 message.MakeAuthProp("header", "d", r.Sig.Domain.XName(c.smtputf8), true, r.Sig.Domain.ASCIIExtra(c.smtputf8)),
2185 message.MakeAuthProp("header", "s", r.Sig.Selector.XName(c.smtputf8), true, r.Sig.Selector.ASCIIExtra(c.smtputf8)),
2186 message.MakeAuthProp("header", "a", r.Sig.Algorithm(), false, ""),
2189 domain = r.Sig.Domain
2190 selector = r.Sig.Selector
2191 if r.Sig.Identity != nil {
2192 props = append(props, message.MakeAuthProp("header", "i", r.Sig.Identity.String(), true, ""))
2193 identity = r.Sig.Identity
2195 if r.RecordAuthentic {
2196 comment += "with dnssec"
2198 comment += "without dnssec"
2203 errmsg = r.Err.Error()
2205 authResAddDKIM(string(r.Status), comment, errmsg, props)
2206 c.log.Debugx("dkim verification result", r.Err,
2207 slog.Int("index", i),
2208 slog.Any("mailfrom", c.mailFrom),
2209 slog.Any("status", r.Status),
2210 slog.Any("domain", domain),
2211 slog.Any("selector", selector),
2212 slog.Any("identity", identity))
2216 var spfIdentity *dns.Domain
2217 var mailFromValidation = store.ValidationUnknown
2218 var ehloValidation = store.ValidationUnknown
2219 switch receivedSPF.Identity {
2220 case spf.ReceivedHELO:
2221 if len(spfArgs.HelloDomain.IP) == 0 {
2222 spfIdentity = &spfArgs.HelloDomain.Domain
2224 ehloValidation = store.SPFValidation(receivedSPF.Result)
2225 case spf.ReceivedMailFrom:
2226 spfIdentity = &spfArgs.MailFromDomain
2227 mailFromValidation = store.SPFValidation(receivedSPF.Result)
2229 var props []message.AuthProp
2230 if spfIdentity != nil {
2231 props = []message.AuthProp{message.MakeAuthProp("smtp", string(receivedSPF.Identity), spfIdentity.XName(c.smtputf8), true, spfIdentity.ASCIIExtra(c.smtputf8))}
2233 var spfComment string
2235 spfComment = "with dnssec"
2237 spfComment = "without dnssec"
2239 authResults.Methods = append(authResults.Methods, message.AuthMethod{
2241 Result: string(receivedSPF.Result),
2242 Comment: spfComment,
2245 switch receivedSPF.Result {
2246 case spf.StatusPass:
2247 c.log.Debug("spf pass", slog.Any("ip", spfArgs.RemoteIP), slog.String("mailfromdomain", spfArgs.MailFromDomain.ASCII)) // todo: log the domain that was actually verified.
2248 case spf.StatusFail:
2251 for _, b := range []byte(spfExpl) {
2252 if b < ' ' || b >= 0x7f {
2258 if len(spfExpl) > 800 {
2259 spfExpl = spfExpl[:797] + "..."
2261 spfExpl = "remote claims: " + spfExpl
2265 spfExpl = fmt.Sprintf("your ip %s is not on the SPF allowlist for domain %s", spfArgs.RemoteIP, spfDomain.ASCII)
2267 c.log.Info("spf fail", slog.String("explanation", spfExpl)) // todo future: get this to the client. how? in smtp session in case of a reject due to dmarc fail?
2268 case spf.StatusTemperror:
2269 c.log.Infox("spf temperror", spfErr)
2270 case spf.StatusPermerror:
2271 c.log.Infox("spf permerror", spfErr)
2272 case spf.StatusNone, spf.StatusNeutral, spf.StatusSoftfail:
2274 c.log.Error("unknown spf status, treating as None/Neutral", slog.Any("status", receivedSPF.Result))
2275 receivedSPF.Result = spf.StatusNone
2280 var dmarcResult dmarc.Result
2281 const applyRandomPercentage = true
2282 // dmarcMethod is added to authResults when delivering to recipients: accounts can
2283 // have different policy override rules.
2284 var dmarcMethod message.AuthMethod
2285 var msgFromValidation = store.ValidationNone
2286 if msgFrom.IsZero() {
2287 dmarcResult.Status = dmarc.StatusNone
2288 dmarcMethod = message.AuthMethod{
2290 Result: string(dmarcResult.Status),
2293 msgFromValidation = alignment(ctx, c.log, msgFrom.Domain, dkimResults, receivedSPF.Result, spfIdentity)
2295 // We are doing the DMARC evaluation now. But we only store it for inclusion in an
2296 // aggregate report when we actually use it. We use an evaluation for each
2297 // recipient, with each a potentially different result due to mailing
2298 // list/forwarding configuration. If we reject a message due to being spam, we
2299 // don't want to spend any resources for the sender domain, and we don't want to
2300 // give the sender any more information about us, so we won't record the
2302 // 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.
2304 dmarcctx, dmarccancel := context.WithTimeout(ctx, time.Minute)
2306 dmarcUse, dmarcResult = dmarc.Verify(dmarcctx, c.log.Logger, c.resolver, msgFrom.Domain, dkimResults, receivedSPF.Result, spfIdentity, applyRandomPercentage)
2309 if dmarcResult.RecordAuthentic {
2310 comment = "with dnssec"
2312 comment = "without dnssec"
2314 dmarcMethod = message.AuthMethod{
2316 Result: string(dmarcResult.Status),
2318 Props: []message.AuthProp{
2320 message.MakeAuthProp("header", "from", msgFrom.Domain.ASCII, true, msgFrom.Domain.ASCIIExtra(c.smtputf8)),
2324 if dmarcResult.Status == dmarc.StatusPass && msgFromValidation == store.ValidationRelaxed {
2325 msgFromValidation = store.ValidationDMARC
2328 // todo future: consider enforcing an spf (soft)fail if there is no dmarc policy or the dmarc policy is none.
../rfc/7489:1507
2330 c.log.Debug("dmarc verification", slog.Any("result", dmarcResult.Status), slog.Any("domain", msgFrom.Domain))
2332 // Prepare for analyzing content, calculating reputation.
2333 ipmasked1, ipmasked2, ipmasked3 := ipmasked(c.remoteIP)
2334 var verifiedDKIMDomains []string
2335 dkimSeen := map[string]bool{}
2336 for _, r := range dkimResults {
2337 // A message can have multiple signatures for the same identity. For example when
2338 // signing the message multiple times with different algorithms (rsa and ed25519).
2339 if r.Status != dkim.StatusPass {
2342 d := r.Sig.Domain.Name()
2345 verifiedDKIMDomains = append(verifiedDKIMDomains, d)
2349 // When we deliver, we try to remove from rejects mailbox based on message-id.
2350 // We'll parse it when we need it, but it is the same for each recipient.
2351 var messageID string
2352 var parsedMessageID bool
2354 // We build up a DSN for each failed recipient. If we have recipients in dsnMsg
2355 // after processing, we queue the DSN. Unless all recipients failed, in which case
2356 // we may just fail the mail transaction instead (could be common for failure to
2357 // deliver to a single recipient, e.g. for junk mail).
2359 type deliverError struct {
2366 var deliverErrors []deliverError
2367 addError := func(rcptAcc rcptAccount, code int, secode string, userError bool, errmsg string) {
2368 e := deliverError{rcptAcc.rcptTo, code, secode, userError, errmsg}
2369 c.log.Info("deliver error",
2370 slog.Any("rcptto", e.rcptTo),
2371 slog.Int("code", code),
2372 slog.String("secode", "secode"),
2373 slog.Bool("usererror", userError),
2374 slog.String("errmsg", errmsg))
2375 deliverErrors = append(deliverErrors, e)
2378 // For each recipient, do final spam analysis and delivery.
2379 for _, rcptAcc := range c.recipients {
2380 log := c.log.With(slog.Any("mailfrom", c.mailFrom), slog.Any("rcptto", rcptAcc.rcptTo))
2382 // If this is not a valid local user, we send back a DSN. This can only happen when
2383 // there are also valid recipients, and only when remote is SPF-verified, so the DSN
2384 // should not cause backscatter.
2385 // In case of serious errors, we abort the transaction. We may have already
2386 // delivered some messages. Perhaps it would be better to continue with other
2387 // deliveries, and return an error at the end? Though the failure conditions will
2388 // probably prevent any other successful deliveries too...
2391 metricDelivery.WithLabelValues("unknownuser", "").Inc()
2392 addError(rcptAcc, smtp.C550MailboxUnavail, smtp.SeAddr1UnknownDestMailbox1, true, "no such user")
2396 acc, err := store.OpenAccount(log, rcptAcc.accountName)
2398 log.Errorx("open account", err, slog.Any("account", rcptAcc.accountName))
2399 metricDelivery.WithLabelValues("accounterror", "").Inc()
2400 addError(rcptAcc, smtp.C451LocalErr, smtp.SeSys3Other0, false, "error processing")
2406 log.Check(err, "closing account after delivery")
2410 // We don't want to let a single IP or network deliver too many messages to an
2411 // account. They may fill up the mailbox, either with messages that have to be
2412 // purged, or by filling the disk. We check both cases for IP's and networks.
2413 var rateError bool // Whether returned error represents a rate error.
2414 err = acc.DB.Read(ctx, func(tx *bstore.Tx) (retErr error) {
2417 log.Debugx("checking message and size delivery rates", retErr, slog.Duration("duration", time.Since(now)))
2420 checkCount := func(msg store.Message, window time.Duration, limit int) {
2424 q := bstore.QueryTx[store.Message](tx)
2425 q.FilterNonzero(msg)
2426 q.FilterGreater("Received", now.Add(-window))
2427 q.FilterEqual("Expunged", false)
2435 retErr = fmt.Errorf("more than %d messages in past %s from your ip/network", limit, window)
2439 checkSize := func(msg store.Message, window time.Duration, limit int64) {
2443 q := bstore.QueryTx[store.Message](tx)
2444 q.FilterNonzero(msg)
2445 q.FilterGreater("Received", now.Add(-window))
2446 q.FilterEqual("Expunged", false)
2447 size := msgWriter.Size
2448 err := q.ForEach(func(v store.Message) error {
2458 retErr = fmt.Errorf("more than %d bytes in past %s from your ip/network", limit, window)
2462 // todo future: make these configurable
2463 // todo: should we have a limit for forwarded messages? they are stored with empty RemoteIPMasked*
2465 const day = 24 * time.Hour
2466 checkCount(store.Message{RemoteIPMasked1: ipmasked1}, time.Minute, limitIPMasked1MessagesPerMinute)
2467 checkCount(store.Message{RemoteIPMasked1: ipmasked1}, day, 20*500)
2468 checkCount(store.Message{RemoteIPMasked2: ipmasked2}, time.Minute, 1500)
2469 checkCount(store.Message{RemoteIPMasked2: ipmasked2}, day, 20*1500)
2470 checkCount(store.Message{RemoteIPMasked3: ipmasked3}, time.Minute, 4500)
2471 checkCount(store.Message{RemoteIPMasked3: ipmasked3}, day, 20*4500)
2473 const MB = 1024 * 1024
2474 checkSize(store.Message{RemoteIPMasked1: ipmasked1}, time.Minute, limitIPMasked1SizePerMinute)
2475 checkSize(store.Message{RemoteIPMasked1: ipmasked1}, day, 3*1000*MB)
2476 checkSize(store.Message{RemoteIPMasked2: ipmasked2}, time.Minute, 3000*MB)
2477 checkSize(store.Message{RemoteIPMasked2: ipmasked2}, day, 3*3000*MB)
2478 checkSize(store.Message{RemoteIPMasked3: ipmasked3}, time.Minute, 9000*MB)
2479 checkSize(store.Message{RemoteIPMasked3: ipmasked3}, day, 3*9000*MB)
2483 if err != nil && !rateError {
2484 log.Errorx("checking delivery rates", err)
2485 metricDelivery.WithLabelValues("checkrates", "").Inc()
2486 addError(rcptAcc, smtp.C451LocalErr, smtp.SeSys3Other0, false, "error processing")
2488 } else if err != nil {
2489 log.Debugx("refusing due to high delivery rate", err)
2490 metricDelivery.WithLabelValues("highrate", "").Inc()
2492 addError(rcptAcc, smtp.C452StorageFull, smtp.SeMailbox2Full2, true, err.Error())
2497 Received: time.Now(),
2498 RemoteIP: c.remoteIP.String(),
2499 RemoteIPMasked1: ipmasked1,
2500 RemoteIPMasked2: ipmasked2,
2501 RemoteIPMasked3: ipmasked3,
2502 EHLODomain: c.hello.Domain.Name(),
2503 MailFrom: c.mailFrom.String(),
2504 MailFromLocalpart: c.mailFrom.Localpart,
2505 MailFromDomain: c.mailFrom.IPDomain.Domain.Name(),
2506 RcptToLocalpart: rcptAcc.rcptTo.Localpart,
2507 RcptToDomain: rcptAcc.rcptTo.IPDomain.Domain.Name(),
2508 MsgFromLocalpart: msgFrom.Localpart,
2509 MsgFromDomain: msgFrom.Domain.Name(),
2510 MsgFromOrgDomain: publicsuffix.Lookup(ctx, log.Logger, msgFrom.Domain).Name(),
2511 EHLOValidated: ehloValidation == store.ValidationPass,
2512 MailFromValidated: mailFromValidation == store.ValidationPass,
2513 MsgFromValidated: msgFromValidation == store.ValidationStrict || msgFromValidation == store.ValidationDMARC || msgFromValidation == store.ValidationRelaxed,
2514 EHLOValidation: ehloValidation,
2515 MailFromValidation: mailFromValidation,
2516 MsgFromValidation: msgFromValidation,
2517 DKIMDomains: verifiedDKIMDomains,
2518 Size: msgWriter.Size,
2521 tlsState := c.conn.(*tls.Conn).ConnectionState()
2522 m.ReceivedTLSVersion = tlsState.Version
2523 m.ReceivedTLSCipherSuite = tlsState.CipherSuite
2524 if c.requireTLS != nil {
2525 m.ReceivedRequireTLS = *c.requireTLS
2528 m.ReceivedTLSVersion = 1 // Signals plain text delivery.
2531 var msgTo, msgCc []message.Address
2532 if envelope != nil {
2536 d := delivery{c.tls, &m, dataFile, rcptAcc, acc, msgTo, msgCc, msgFrom, c.dnsBLs, dmarcUse, dmarcResult, dkimResults, iprevStatus}
2537 a := analyze(ctx, log, c.resolver, d)
2539 // Any DMARC result override is stored in the evaluation for outgoing DMARC
2540 // aggregate reports, and added to the Authentication-Results message header.
2541 // We want to tell the sender that we have an override, e.g. for mailing lists, so
2542 // they don't overestimate the potential damage of switching from p=none to
2544 var dmarcOverrides []string
2545 if a.dmarcOverrideReason != "" {
2546 dmarcOverrides = []string{a.dmarcOverrideReason}
2548 if dmarcResult.Record != nil && !dmarcUse {
2549 dmarcOverrides = append(dmarcOverrides, string(dmarcrpt.PolicyOverrideSampledOut))
2552 // Add per-recipient DMARC method to Authentication-Results. Each account can have
2553 // their own override rules, e.g. based on configured mailing lists/forwards.
2555 rcptDMARCMethod := dmarcMethod
2556 if len(dmarcOverrides) > 0 {
2557 if rcptDMARCMethod.Comment != "" {
2558 rcptDMARCMethod.Comment += ", "
2560 rcptDMARCMethod.Comment += "override " + strings.Join(dmarcOverrides, ",")
2562 rcptAuthResults := authResults
2563 rcptAuthResults.Methods = append([]message.AuthMethod{}, authResults.Methods...)
2564 rcptAuthResults.Methods = append(rcptAuthResults.Methods, rcptDMARCMethod)
2566 // Prepend reason as message header, for easy display in mail clients.
2569 xmox = "X-Mox-Reason: " + a.reason + "\r\n"
2575 m.MsgPrefix = []byte(
2579 rcptAuthResults.Header() +
2580 receivedSPF.Header() +
2581 recvHdrFor(rcptAcc.rcptTo.String()),
2583 m.Size += int64(len(m.MsgPrefix))
2585 // Store DMARC evaluation for inclusion in an aggregate report. Only if there is at
2586 // least one reporting address: We don't want to needlessly store a row in a
2587 // database for each delivery attempt. If we reject a message for being junk, we
2588 // are also not going to send it a DMARC report. The DMARC check is done early in
2589 // the analysis, we will report on rejects because of DMARC, because it could be
2590 // valuable feedback about forwarded or mailing list messages.
2592 if !mox.Conf.Static.NoOutgoingDMARCReports && dmarcResult.Record != nil && len(dmarcResult.Record.AggregateReportAddresses) > 0 && (a.accept && !m.IsReject || a.reason == reasonDMARCPolicy) {
2593 // Disposition holds our decision on whether to accept the message. Not what the
2594 // DMARC evaluation resulted in. We can override, e.g. because of mailing lists,
2595 // forwarding, or local policy.
2596 // We treat quarantine as reject, so never claim to quarantine.
2598 disposition := dmarcrpt.DispositionNone
2600 disposition = dmarcrpt.DispositionReject
2603 // unknownDomain returns whether the sender is domain with which this account has
2604 // not had positive interaction.
2605 unknownDomain := func() (unknown bool) {
2606 err := acc.DB.Read(ctx, func(tx *bstore.Tx) (err error) {
2607 // See if we received a non-junk message from this organizational domain.
2608 q := bstore.QueryTx[store.Message](tx)
2609 q.FilterNonzero(store.Message{MsgFromOrgDomain: m.MsgFromOrgDomain})
2610 q.FilterEqual("Notjunk", true)
2611 q.FilterEqual("IsReject", false)
2612 exists, err := q.Exists()
2614 return fmt.Errorf("querying for non-junk message from organizational domain: %v", err)
2620 // See if we sent a message to this organizational domain.
2621 qr := bstore.QueryTx[store.Recipient](tx)
2622 qr.FilterNonzero(store.Recipient{OrgDomain: m.MsgFromOrgDomain})
2623 exists, err = qr.Exists()
2625 return fmt.Errorf("querying for message sent to organizational domain: %v", err)
2633 log.Errorx("checking if sender is unknown domain, for dmarc aggregate report evaluation", err)
2638 r := dmarcResult.Record
2639 addresses := make([]string, len(r.AggregateReportAddresses))
2640 for i, a := range r.AggregateReportAddresses {
2641 addresses[i] = a.String()
2643 sp := dmarcrpt.Disposition(r.SubdomainPolicy)
2644 if r.SubdomainPolicy == dmarc.PolicyEmpty {
2645 sp = dmarcrpt.Disposition(r.Policy)
2647 eval := dmarcdb.Evaluation{
2648 // Evaluated and IntervalHours set by AddEvaluation.
2649 PolicyDomain: dmarcResult.Domain.Name(),
2651 // Optional evaluations don't cause a report to be sent, but will be included.
2652 // Useful for automated inter-mailer messages, we don't want to get in a reporting
2653 // loop. We also don't want to be used for sending reports to unsuspecting domains
2654 // we have no relation with.
2655 // 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.
2656 Optional: rcptAcc.destination.DMARCReports || rcptAcc.destination.HostTLSReports || rcptAcc.destination.DomainTLSReports || a.reason == reasonDMARCPolicy && unknownDomain(),
2658 Addresses: addresses,
2660 PolicyPublished: dmarcrpt.PolicyPublished{
2661 Domain: dmarcResult.Domain.Name(),
2662 ADKIM: dmarcrpt.Alignment(r.ADKIM),
2663 ASPF: dmarcrpt.Alignment(r.ASPF),
2664 Policy: dmarcrpt.Disposition(r.Policy),
2665 SubdomainPolicy: sp,
2666 Percentage: r.Percentage,
2667 // We don't save ReportingOptions, we don't do per-message failure reporting.
2669 SourceIP: c.remoteIP.String(),
2670 Disposition: disposition,
2671 AlignedDKIMPass: dmarcResult.AlignedDKIMPass,
2672 AlignedSPFPass: dmarcResult.AlignedSPFPass,
2673 EnvelopeTo: rcptAcc.rcptTo.IPDomain.String(),
2674 EnvelopeFrom: c.mailFrom.IPDomain.String(),
2675 HeaderFrom: msgFrom.Domain.Name(),
2678 for _, s := range dmarcOverrides {
2679 reason := dmarcrpt.PolicyOverrideReason{Type: dmarcrpt.PolicyOverride(s)}
2680 eval.OverrideReasons = append(eval.OverrideReasons, reason)
2683 // We'll include all signatures for the organizational domain, even if they weren't
2684 // relevant due to strict alignment requirement.
2685 for _, dkimResult := range dkimResults {
2686 if dkimResult.Sig == nil || publicsuffix.Lookup(ctx, log.Logger, msgFrom.Domain) != publicsuffix.Lookup(ctx, log.Logger, dkimResult.Sig.Domain) {
2689 r := dmarcrpt.DKIMAuthResult{
2690 Domain: dkimResult.Sig.Domain.Name(),
2691 Selector: dkimResult.Sig.Selector.ASCII,
2692 Result: dmarcrpt.DKIMResult(dkimResult.Status),
2694 eval.DKIMResults = append(eval.DKIMResults, r)
2697 switch receivedSPF.Identity {
2698 case spf.ReceivedHELO:
2699 spfAuthResult := dmarcrpt.SPFAuthResult{
2700 Domain: spfArgs.HelloDomain.String(), // Can be unicode and also IP.
2701 Scope: dmarcrpt.SPFDomainScopeHelo,
2702 Result: dmarcrpt.SPFResult(receivedSPF.Result),
2704 eval.SPFResults = []dmarcrpt.SPFAuthResult{spfAuthResult}
2705 case spf.ReceivedMailFrom:
2706 spfAuthResult := dmarcrpt.SPFAuthResult{
2707 Domain: spfArgs.MailFromDomain.Name(), // Can be unicode.
2708 Scope: dmarcrpt.SPFDomainScopeMailFrom,
2709 Result: dmarcrpt.SPFResult(receivedSPF.Result),
2711 eval.SPFResults = []dmarcrpt.SPFAuthResult{spfAuthResult}
2714 err := dmarcdb.AddEvaluation(ctx, dmarcResult.Record.AggregateReportingInterval, &eval)
2715 log.Check(err, "adding dmarc evaluation to database for aggregate report")
2719 conf, _ := acc.Conf()
2720 if conf.RejectsMailbox != "" {
2721 present, _, messagehash, err := rejectPresent(log, acc, conf.RejectsMailbox, &m, dataFile)
2723 log.Errorx("checking whether reject is already present", err)
2724 } else if !present {
2726 m.Seen = true // We don't want to draw attention.
2727 // Regular automatic junk flags configuration applies to these messages. The
2728 // default is to treat these as neutral, so they won't cause outright rejections
2729 // due to reputation for later delivery attempts.
2730 m.MessageHash = messagehash
2731 acc.WithWLock(func() {
2734 if !conf.KeepRejects {
2735 hasSpace, err = acc.TidyRejectsMailbox(c.log, conf.RejectsMailbox)
2738 log.Errorx("tidying rejects mailbox", err)
2739 } else if hasSpace {
2740 if err := acc.DeliverMailbox(log, conf.RejectsMailbox, &m, dataFile); err != nil {
2741 log.Errorx("delivering spammy mail to rejects mailbox", err)
2743 log.Info("delivered spammy mail to rejects mailbox")
2746 log.Info("not storing spammy mail to full rejects mailbox")
2750 log.Info("reject message is already present, ignoring")
2754 log.Info("incoming message rejected", slog.String("reason", a.reason), slog.Any("msgfrom", msgFrom))
2755 metricDelivery.WithLabelValues("reject", a.reason).Inc()
2757 addError(rcptAcc, a.code, a.secode, a.userError, a.errmsg)
2761 delayFirstTime := true
2762 if a.dmarcReport != nil {
2764 if err := dmarcdb.AddReport(ctx, a.dmarcReport, msgFrom.Domain); err != nil {
2765 log.Errorx("saving dmarc aggregate report in database", err)
2767 log.Info("dmarc aggregate report processed")
2769 delayFirstTime = false
2772 if a.tlsReport != nil {
2773 // todo future: add rate limiting to prevent DoS attacks.
2774 if err := tlsrptdb.AddReport(ctx, c.log, msgFrom.Domain, c.mailFrom.String(), rcptAcc.destination.HostTLSReports, a.tlsReport); err != nil {
2775 log.Errorx("saving TLSRPT report in database", err)
2777 log.Info("tlsrpt report processed")
2779 delayFirstTime = false
2783 // If this is a first-time sender and not a forwarded message, wait before actually
2784 // delivering. If this turns out to be a spammer, we've kept one of their
2785 // connections busy.
2786 if delayFirstTime && !m.IsForward && a.reason == reasonNoBadSignals && c.firstTimeSenderDelay > 0 {
2787 log.Debug("delaying before delivering from sender without reputation", slog.Duration("delay", c.firstTimeSenderDelay))
2788 mox.Sleep(mox.Context, c.firstTimeSenderDelay)
2791 // Gather the message-id before we deliver and the file may be consumed.
2792 if !parsedMessageID {
2793 if p, err := message.Parse(c.log.Logger, false, store.FileMsgReader(m.MsgPrefix, dataFile)); err != nil {
2794 log.Infox("parsing message for message-id", err)
2795 } else if header, err := p.Header(); err != nil {
2796 log.Infox("parsing message header for message-id", err)
2798 messageID = header.Get("Message-Id")
2803 code, timeout := localserveNeedsError(rcptAcc.rcptTo.Localpart)
2805 log.Info("timing out due to special localpart")
2806 mox.Sleep(mox.Context, time.Hour)
2807 xsmtpServerErrorf(codes{smtp.C451LocalErr, smtp.SeOther00}, "timing out delivery due to special localpart")
2808 } else if code != 0 {
2809 log.Info("failure due to special localpart", slog.Int("code", code))
2810 metricDelivery.WithLabelValues("delivererror", "localserve").Inc()
2811 addError(rcptAcc, code, smtp.SeOther00, false, fmt.Sprintf("failure with code %d due to special localpart", code))
2814 acc.WithWLock(func() {
2815 if err := acc.DeliverMailbox(log, a.mailbox, &m, dataFile); err != nil {
2816 log.Errorx("delivering", err)
2817 metricDelivery.WithLabelValues("delivererror", a.reason).Inc()
2818 if errors.Is(err, store.ErrOverQuota) {
2819 addError(rcptAcc, smtp.C452StorageFull, smtp.SeMailbox2Full2, true, "account storage full")
2821 addError(rcptAcc, smtp.C451LocalErr, smtp.SeSys3Other0, false, "error processing")
2825 metricDelivery.WithLabelValues("delivered", a.reason).Inc()
2826 log.Info("incoming message delivered", slog.String("reason", a.reason), slog.Any("msgfrom", msgFrom))
2828 conf, _ := acc.Conf()
2829 if conf.RejectsMailbox != "" && m.MessageID != "" {
2830 if err := acc.RejectsRemove(log, conf.RejectsMailbox, m.MessageID); err != nil {
2831 log.Errorx("removing message from rejects mailbox", err, slog.String("messageid", messageID))
2837 log.Check(err, "closing account after delivering")
2841 // If all recipients failed to deliver, return an error.
2842 if len(c.recipients) == len(deliverErrors) {
2844 e0 := deliverErrors[0]
2845 var serverError bool
2848 for _, e := range deliverErrors {
2849 serverError = serverError || !e.userError
2850 if e.code != e0.code || e.secode != e0.secode {
2853 msgs = append(msgs, e.errmsg)
2859 xsmtpErrorf(e0.code, e0.secode, !serverError, "%s", strings.Join(msgs, "\n"))
2862 // Not all failures had the same error. We'll return each error on a separate line.
2864 for _, e := range deliverErrors {
2865 s := fmt.Sprintf("%d %d.%s %s", e.code, e.code/100, e.secode, e.errmsg)
2866 lines = append(lines, s)
2868 code := smtp.C451LocalErr
2869 secode := smtp.SeSys3Other0
2871 code = smtp.C554TransactionFailed
2873 lines = append(lines, "multiple errors")
2874 xsmtpErrorf(code, secode, !serverError, strings.Join(lines, "\n"))
2876 // Generate one DSN for all failed recipients.
2877 if len(deliverErrors) > 0 {
2879 dsnMsg := dsn.Message{
2880 SMTPUTF8: c.smtputf8,
2881 From: smtp.Path{Localpart: "postmaster", IPDomain: deliverErrors[0].rcptTo.IPDomain},
2883 Subject: "mail delivery failure",
2884 MessageID: mox.MessageIDGen(false),
2885 References: messageID,
2887 // Per-message details.
2888 ReportingMTA: mox.Conf.Static.HostnameDomain.ASCII,
2889 ReceivedFromMTA: smtp.Ehlo{Name: c.hello, ConnIP: c.remoteIP},
2893 if len(deliverErrors) > 1 {
2894 dsnMsg.TextBody = "Multiple delivery failures occurred.\n\n"
2897 for _, e := range deliverErrors {
2899 if e.code/100 == 4 {
2902 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))
2903 rcpt := dsn.Recipient{
2904 FinalRecipient: e.rcptTo,
2906 Status: fmt.Sprintf("%d.%s", e.code/100, e.secode),
2907 LastAttemptDate: now,
2909 dsnMsg.Recipients = append(dsnMsg.Recipients, rcpt)
2912 header, err := message.ReadHeaders(bufio.NewReader(&moxio.AtReader{R: dataFile}))
2914 c.log.Errorx("reading headers of incoming message for dsn, continuing dsn without headers", err)
2916 dsnMsg.Original = header
2919 c.log.Error("not queueing dsn for incoming delivery due to localserve")
2920 } else if err := queueDSN(context.TODO(), c.log, c, *c.mailFrom, dsnMsg, c.requireTLS != nil && *c.requireTLS); err != nil {
2921 metricServerErrors.WithLabelValues("queuedsn").Inc()
2922 c.log.Errorx("queuing DSN for incoming delivery, no DSN sent", err)
2927 c.transactionBad-- // Compensate for early earlier pessimistic increase.
2929 c.writecodeline(smtp.C250Completed, smtp.SeMailbox2Other0, "it is done", nil)
2932// ecode returns either ecode, or a more specific error based on err.
2933// For example, ecode can be turned from an "other system" error into a "mail
2934// system full" if the error indicates no disk space is available.
2935func errCodes(code int, ecode string, err error) codes {
2937 case moxio.IsStorageSpace(err):
2939 case smtp.SeMailbox2Other0:
2940 if code == smtp.C451LocalErr {
2941 code = smtp.C452StorageFull
2943 ecode = smtp.SeMailbox2Full2
2944 case smtp.SeSys3Other0:
2945 if code == smtp.C451LocalErr {
2946 code = smtp.C452StorageFull
2948 ecode = smtp.SeSys3StorageFull1
2951 return codes{code, ecode}
2955func (c *conn) cmdRset(p *parser) {
2960 c.bwritecodeline(smtp.C250Completed, smtp.SeOther00, "all clear", nil)
2964func (c *conn) cmdVrfy(p *parser) {
2965 // No EHLO/HELO needed.
2976 // todo future: we could support vrfy and expn for submission? though would need to see if its rfc defines it.
2979 xsmtpUserErrorf(smtp.C252WithoutVrfy, smtp.SePol7Other0, "no verify but will try delivery")
2983func (c *conn) cmdExpn(p *parser) {
2984 // No EHLO/HELO needed.
2996 xsmtpUserErrorf(smtp.C252WithoutVrfy, smtp.SePol7Other0, "no expand but will try delivery")
3000func (c *conn) cmdHelp(p *parser) {
3001 // Let's not strictly parse the request for help. We are ignoring the text anyway.
3004 c.bwritecodeline(smtp.C214Help, smtp.SeOther00, "see rfc 5321 (smtp)", nil)
3008func (c *conn) cmdNoop(p *parser) {
3009 // No idea why, but if an argument follows, it must adhere to the string ABNF production...
3016 c.bwritecodeline(smtp.C250Completed, smtp.SeOther00, "alrighty", nil)
3020func (c *conn) cmdQuit(p *parser) {
3024 c.writecodeline(smtp.C221Closing, smtp.SeOther00, "okay thanks bye", nil)