1// Package smtpserver implements an SMTP server for submission and incoming delivery of mail messages.
30 "golang.org/x/exp/maps"
31 "golang.org/x/text/unicode/norm"
33 "github.com/prometheus/client_golang/prometheus"
34 "github.com/prometheus/client_golang/prometheus/promauto"
36 "github.com/mjl-/bstore"
38 "github.com/mjl-/mox/config"
39 "github.com/mjl-/mox/dkim"
40 "github.com/mjl-/mox/dmarc"
41 "github.com/mjl-/mox/dmarcdb"
42 "github.com/mjl-/mox/dmarcrpt"
43 "github.com/mjl-/mox/dns"
44 "github.com/mjl-/mox/dsn"
45 "github.com/mjl-/mox/iprev"
46 "github.com/mjl-/mox/message"
47 "github.com/mjl-/mox/metrics"
48 "github.com/mjl-/mox/mlog"
49 "github.com/mjl-/mox/mox-"
50 "github.com/mjl-/mox/moxio"
51 "github.com/mjl-/mox/moxvar"
52 "github.com/mjl-/mox/publicsuffix"
53 "github.com/mjl-/mox/queue"
54 "github.com/mjl-/mox/ratelimit"
55 "github.com/mjl-/mox/scram"
56 "github.com/mjl-/mox/smtp"
57 "github.com/mjl-/mox/spf"
58 "github.com/mjl-/mox/store"
59 "github.com/mjl-/mox/tlsrptdb"
62// We use panic and recover for error handling while executing commands.
63// These errors signal the connection must be closed.
64var errIO = errors.New("io error")
66// If set, regular delivery/submit is sidestepped, email is accepted and
67// delivered to the account named mox.
70var limiterConnectionRate, limiterConnections *ratelimit.Limiter
72// For delivery rate limiting. Variable because changed during tests.
73var limitIPMasked1MessagesPerMinute int = 500
74var limitIPMasked1SizePerMinute int64 = 1000 * 1024 * 1024
76// Maximum number of RCPT TO commands (i.e. recipients) for a single message
77// delivery. Must be at least 100. Announced in LIMIT extension.
78const rcptToLimit = 1000
81 // Also called by tests, so they don't trigger the rate limiter.
87 // todo future: make these configurable
88 limiterConnectionRate = &ratelimit.Limiter{
89 WindowLimits: []ratelimit.WindowLimit{
92 Limits: [...]int64{300, 900, 2700},
96 limiterConnections = &ratelimit.Limiter{
97 WindowLimits: []ratelimit.WindowLimit{
99 Window: time.Duration(math.MaxInt64), // All of time.
100 Limits: [...]int64{30, 90, 270},
107 // Delays for bad/suspicious behaviour. Zero during tests.
108 badClientDelay = time.Second // Before reads and after 1-byte writes for probably spammers.
109 authFailDelay = time.Second // Response to authentication failure.
110 unknownRecipientsDelay = 5 * time.Second // Response when all recipients are unknown.
111 firstTimeSenderDelayDefault = 15 * time.Second // Before accepting message from first-time sender.
116 secode string // Enhanced code, but without the leading major int from code.
120 metricConnection = promauto.NewCounterVec(
121 prometheus.CounterOpts{
122 Name: "mox_smtpserver_connection_total",
123 Help: "Incoming SMTP connections.",
126 "kind", // "deliver" or "submit"
129 metricCommands = promauto.NewHistogramVec(
130 prometheus.HistogramOpts{
131 Name: "mox_smtpserver_command_duration_seconds",
132 Help: "SMTP server command duration and result codes in seconds.",
133 Buckets: []float64{0.001, 0.005, 0.01, 0.05, 0.100, 0.5, 1, 5, 10, 20, 30, 60, 120},
136 "kind", // "deliver" or "submit"
142 metricDelivery = promauto.NewCounterVec(
143 prometheus.CounterOpts{
144 Name: "mox_smtpserver_delivery_total",
145 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.",
152 // Similar between ../webmail/webmail.go:/metricSubmission and ../smtpserver/server.go:/metricSubmission
153 metricSubmission = promauto.NewCounterVec(
154 prometheus.CounterOpts{
155 Name: "mox_smtpserver_submission_total",
156 Help: "SMTP server incoming submission results, known values (those ending with error are server errors): ok, badmessage, badfrom, badheader, messagelimiterror, recipientlimiterror, localserveerror, queueerror.",
162 metricServerErrors = promauto.NewCounterVec(
163 prometheus.CounterOpts{
164 Name: "mox_smtpserver_errors_total",
165 Help: "SMTP server errors, known values: dkimsign, queuedsn.",
173var jitterRand = mox.NewPseudoRand()
175func durationDefault(delay *time.Duration, def time.Duration) time.Duration {
182// Listen initializes network listeners for incoming SMTP connection.
183// The listeners are stored for a later call to Serve.
185 names := maps.Keys(mox.Conf.Static.Listeners)
187 for _, name := range names {
188 listener := mox.Conf.Static.Listeners[name]
190 var tlsConfig *tls.Config
191 if listener.TLS != nil {
192 tlsConfig = listener.TLS.Config
195 maxMsgSize := listener.SMTPMaxMessageSize
197 maxMsgSize = config.DefaultMaxMsgSize
200 if listener.SMTP.Enabled {
201 hostname := mox.Conf.Static.HostnameDomain
202 if listener.Hostname != "" {
203 hostname = listener.HostnameDomain
205 port := config.Port(listener.SMTP.Port, 25)
206 for _, ip := range listener.IPs {
207 firstTimeSenderDelay := durationDefault(listener.SMTP.FirstTimeSenderDelay, firstTimeSenderDelayDefault)
208 listen1("smtp", name, ip, port, hostname, tlsConfig, false, false, maxMsgSize, false, listener.SMTP.RequireSTARTTLS, !listener.SMTP.NoRequireTLS, listener.SMTP.DNSBLZones, firstTimeSenderDelay)
211 if listener.Submission.Enabled {
212 hostname := mox.Conf.Static.HostnameDomain
213 if listener.Hostname != "" {
214 hostname = listener.HostnameDomain
216 port := config.Port(listener.Submission.Port, 587)
217 for _, ip := range listener.IPs {
218 listen1("submission", name, ip, port, hostname, tlsConfig, true, false, maxMsgSize, !listener.Submission.NoRequireSTARTTLS, !listener.Submission.NoRequireSTARTTLS, true, nil, 0)
222 if listener.Submissions.Enabled {
223 hostname := mox.Conf.Static.HostnameDomain
224 if listener.Hostname != "" {
225 hostname = listener.HostnameDomain
227 port := config.Port(listener.Submissions.Port, 465)
228 for _, ip := range listener.IPs {
229 listen1("submissions", name, ip, port, hostname, tlsConfig, true, true, maxMsgSize, true, true, true, nil, 0)
237func 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) {
238 log := mlog.New("smtpserver", nil)
239 addr := net.JoinHostPort(ip, fmt.Sprintf("%d", port))
240 if os.Getuid() == 0 {
241 log.Print("listening for smtp",
242 slog.String("listener", name),
243 slog.String("address", addr),
244 slog.String("protocol", protocol))
246 network := mox.Network(ip)
247 ln, err := mox.Listen(network, addr)
249 log.Fatalx("smtp: listen for smtp", err, slog.String("protocol", protocol), slog.String("listener", name))
252 ln = tls.NewListener(ln, tlsConfig)
257 conn, err := ln.Accept()
259 log.Infox("smtp: accept", err, slog.String("protocol", protocol), slog.String("listener", name))
263 // Package is set on the resolver by the dkim/spf/dmarc/etc packages.
264 resolver := dns.StrictResolver{Log: log.Logger}
265 go serve(name, mox.Cid(), hostname, tlsConfig, conn, resolver, submission, xtls, maxMessageSize, requireTLSForAuth, requireTLSForDelivery, requireTLS, dnsBLs, firstTimeSenderDelay)
269 servers = append(servers, serve)
272// Serve starts serving on all listeners, launching a goroutine per listener.
274 for _, serve := range servers {
282 // OrigConn is the original (TCP) connection. We'll read from/write to conn, which
283 // can be wrapped in a tls.Server. We close origConn instead of conn because
284 // closing the TLS connection would send a TLS close notification, which may block
285 // for 5s if the server isn't reading it (because it is also sending it).
290 extRequireTLS bool // Whether to announce and allow the REQUIRETLS extension.
291 resolver dns.Resolver
294 tr *moxio.TraceReader // Kept for changing trace level during cmd/auth/data.
295 tw *moxio.TraceWriter
296 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.
297 lastlog time.Time // Used for printing the delta time since the previous logging for this connection.
299 tlsConfig *tls.Config
305 requireTLSForAuth bool
306 requireTLSForDelivery bool // If set, delivery is only allowed with TLS (STARTTLS), except if delivery is to a TLS reporting address.
307 cmd string // Current command.
308 cmdStart time.Time // Start of current command.
309 ncmds int // Number of commands processed. Used to abort connection when first incoming command is unknown/invalid.
311 firstTimeSenderDelay time.Duration
313 // If non-zero, taken into account during Read and Write. Set while processing DATA
314 // command, we don't want the entire delivery to take too long.
317 hello dns.IPDomain // Claimed remote name. Can be ip address for ehlo.
318 ehlo bool // If set, we had EHLO instead of HELO.
320 authFailed int // Number of failed auth attempts. For slowing down remote with many failures.
321 username string // Only when authenticated.
322 account *store.Account // Only when authenticated.
324 // We track good/bad message transactions to disconnect spammers trying to guess addresses.
328 // Message transaction.
330 requireTLS *bool // MAIL FROM with REQUIRETLS set.
331 futureRelease time.Time // MAIL FROM with HOLDFOR or HOLDUNTIL.
332 futureReleaseRequest string // For use in DSNs, either "for;" or "until;" plus original value.
../rfc/4865:305
333 has8bitmime bool // If MAIL FROM parameter BODY=8BITMIME was sent. Required for SMTPUTF8.
334 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.
335 recipients []rcptAccount
338type rcptAccount struct {
340 local bool // Whether recipient is a local user.
342 // Only valid for local delivery.
344 destination config.Destination
345 canonicalAddress string // Optional catchall part stripped and/or lowercased.
348func isClosed(err error) bool {
349 return errors.Is(err, errIO) || moxio.IsClosed(err)
352// completely reset connection state as if greeting has just been sent.
354func (c *conn) reset() {
356 c.hello = dns.IPDomain{}
358 if c.account != nil {
359 err := c.account.Close()
360 c.log.Check(err, "closing account")
366// for rset command, and a few more cases that reset the mail transaction state.
368func (c *conn) rset() {
371 c.futureRelease = time.Time{}
372 c.futureReleaseRequest = ""
373 c.has8bitmime = false
378func (c *conn) earliestDeadline(d time.Duration) time.Time {
379 e := time.Now().Add(d)
380 if !c.deadline.IsZero() && c.deadline.Before(e) {
386func (c *conn) xcheckAuth() {
387 if c.submission && c.account == nil {
389 xsmtpUserErrorf(smtp.C530SecurityRequired, smtp.SePol7Other0, "authentication required")
393func (c *conn) xtrace(level slog.Level) func() {
399 c.tr.SetTrace(mlog.LevelTrace)
400 c.tw.SetTrace(mlog.LevelTrace)
404// setSlow marks the connection slow (or now), so reads are done with 3 second
405// delay for each read, and writes are done at 1 byte per second, to try to slow
407func (c *conn) setSlow(on bool) {
409 c.log.Debug("connection changed to slow")
410 } else if !on && c.slow {
411 c.log.Debug("connection restored to regular pace")
416// Write writes to the connection. It panics on i/o errors, which is handled by the
417// connection command loop.
418func (c *conn) Write(buf []byte) (int, error) {
424 // We set a single deadline for Write and Read. This may be a TLS connection.
425 // SetDeadline works on the underlying connection. If we wouldn't touch the read
426 // deadline, and only set the write deadline and do a bunch of writes, the TLS
427 // library would still have to do reads on the underlying connection, and may reach
428 // a read deadline that was set for some earlier read.
429 // We have one deadline for the whole write. In case of slow writing, we'll write
430 // the last chunk in one go, so remote smtp clients don't abort the connection for
432 deadline := c.earliestDeadline(30 * time.Second)
433 if err := c.conn.SetDeadline(deadline); err != nil {
434 c.log.Errorx("setting deadline for write", err)
439 nn, err := c.conn.Write(buf[:chunk])
441 panic(fmt.Errorf("write: %s (%w)", err, errIO))
445 if len(buf) > 0 && badClientDelay > 0 {
446 mox.Sleep(mox.Context, badClientDelay)
448 // Make sure we don't take too long, otherwise the remote SMTP client may close the
450 if time.Until(deadline) < 2*badClientDelay {
458// Read reads from the connection. It panics on i/o errors, which is handled by the
459// connection command loop.
460func (c *conn) Read(buf []byte) (int, error) {
461 if c.slow && badClientDelay > 0 {
462 mox.Sleep(mox.Context, badClientDelay)
466 // See comment about Deadline instead of individual read/write deadlines at Write.
467 if err := c.conn.SetDeadline(c.earliestDeadline(30 * time.Second)); err != nil {
468 c.log.Errorx("setting deadline for read", err)
471 n, err := c.conn.Read(buf)
473 panic(fmt.Errorf("read: %s (%w)", err, errIO))
478// Cache of line buffers for reading commands.
480var bufpool = moxio.NewBufpool(8, 2*1024)
482func (c *conn) readline() string {
483 line, err := bufpool.Readline(c.log, c.r)
484 if err != nil && errors.Is(err, moxio.ErrLineTooLong) {
485 c.writecodeline(smtp.C500BadSyntax, smtp.SeProto5Other0, "line too long, smtp max is 512, we reached 2048", nil)
486 panic(fmt.Errorf("%s (%w)", err, errIO))
487 } else if err != nil {
488 panic(fmt.Errorf("%s (%w)", err, errIO))
493// Buffered-write command response line to connection with codes and msg.
494// Err is not sent to remote but is used for logging and can be empty.
495func (c *conn) bwritecodeline(code int, secode string, msg string, err error) {
498 ecode = fmt.Sprintf("%d.%s", code/100, secode)
500 metricCommands.WithLabelValues(c.kind(), c.cmd, fmt.Sprintf("%d", code), ecode).Observe(float64(time.Since(c.cmdStart)) / float64(time.Second))
501 c.log.Debugx("smtp command result", err,
502 slog.String("kind", c.kind()),
503 slog.String("cmd", c.cmd),
504 slog.Int("code", code),
505 slog.String("ecode", ecode),
506 slog.Duration("duration", time.Since(c.cmdStart)))
513 // Separate by newline and wrap long lines.
514 lines := strings.Split(msg, "\n")
515 for i, line := range lines {
517 var prelen = 3 + 1 + len(ecode) + len(sep)
518 for prelen+len(line) > 510 {
520 for ; e > 400 && line[e] != ' '; e-- {
522 // 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.
523 c.bwritelinef("%d-%s%s%s", code, ecode, sep, line[:e])
527 if i < len(lines)-1 {
530 c.bwritelinef("%d%s%s%s%s", code, spdash, ecode, sep, line)
534// Buffered-write a formatted response line to connection.
535func (c *conn) bwritelinef(format string, args ...any) {
536 msg := fmt.Sprintf(format, args...)
537 fmt.Fprint(c.w, msg+"\r\n")
540// Flush pending buffered writes to connection.
541func (c *conn) xflush() {
542 c.w.Flush() // Errors will have caused a panic in Write.
545// Write (with flush) a response line with codes and message. err is not written, used for logging and can be nil.
546func (c *conn) writecodeline(code int, secode string, msg string, err error) {
547 c.bwritecodeline(code, secode, msg, err)
551// Write (with flush) a formatted response line to connection.
552func (c *conn) writelinef(format string, args ...any) {
553 c.bwritelinef(format, args...)
557var cleanClose struct{} // Sentinel value for panic/recover indicating clean close of connection.
559func 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) {
560 var localIP, remoteIP net.IP
561 if a, ok := nc.LocalAddr().(*net.TCPAddr); ok {
564 // For net.Pipe, during tests.
565 localIP = net.ParseIP("127.0.0.10")
567 if a, ok := nc.RemoteAddr().(*net.TCPAddr); ok {
570 // For net.Pipe, during tests.
571 remoteIP = net.ParseIP("127.0.0.10")
578 submission: submission,
580 extRequireTLS: requireTLS,
583 tlsConfig: tlsConfig,
587 maxMessageSize: maxMessageSize,
588 requireTLSForAuth: requireTLSForAuth,
589 requireTLSForDelivery: requireTLSForDelivery,
591 firstTimeSenderDelay: firstTimeSenderDelay,
593 var logmutex sync.Mutex
594 c.log = mlog.New("smtpserver", nil).WithFunc(func() []slog.Attr {
596 defer logmutex.Unlock()
599 slog.Int64("cid", c.cid),
600 slog.Duration("delta", now.Sub(c.lastlog)),
603 if c.username != "" {
604 l = append(l, slog.String("username", c.username))
608 c.tr = moxio.NewTraceReader(c.log, "RC: ", c)
609 c.tw = moxio.NewTraceWriter(c.log, "LS: ", c)
610 c.r = bufio.NewReader(c.tr)
611 c.w = bufio.NewWriter(c.tw)
613 metricConnection.WithLabelValues(c.kind()).Inc()
614 c.log.Info("new connection",
615 slog.Any("remote", c.conn.RemoteAddr()),
616 slog.Any("local", c.conn.LocalAddr()),
617 slog.Bool("submission", submission),
618 slog.Bool("tls", tls),
619 slog.String("listener", listenerName))
622 c.origConn.Close() // Close actual TCP socket, regardless of TLS on top.
623 c.conn.Close() // If TLS, will try to write alert notification to already closed socket, returning error quickly.
625 if c.account != nil {
626 err := c.account.Close()
627 c.log.Check(err, "closing account")
632 if x == nil || x == cleanClose {
633 c.log.Info("connection closed")
634 } else if err, ok := x.(error); ok && isClosed(err) {
635 c.log.Infox("connection closed", err)
637 c.log.Error("unhandled panic", slog.Any("err", x))
639 metrics.PanicInc(metrics.Smtpserver)
644 case <-mox.Shutdown.Done():
646 c.writecodeline(smtp.C421ServiceUnavail, smtp.SeSys3NotAccepting2, "shutting down", nil)
651 if !limiterConnectionRate.Add(c.remoteIP, time.Now(), 1) {
652 c.writecodeline(smtp.C421ServiceUnavail, smtp.SePol7Other0, "connection rate from your ip or network too high, slow down please", nil)
656 // If remote IP/network resulted in too many authentication failures, refuse to serve.
657 if submission && !mox.LimiterFailedAuth.CanAdd(c.remoteIP, time.Now(), 1) {
658 metrics.AuthenticationRatelimitedInc("submission")
659 c.log.Debug("refusing connection due to many auth failures", slog.Any("remoteip", c.remoteIP))
660 c.writecodeline(smtp.C421ServiceUnavail, smtp.SePol7Other0, "too many auth failures", nil)
664 if !limiterConnections.Add(c.remoteIP, time.Now(), 1) {
665 c.log.Debug("refusing connection due to many open connections", slog.Any("remoteip", c.remoteIP))
666 c.writecodeline(smtp.C421ServiceUnavail, smtp.SePol7Other0, "too many open connections from your ip or network", nil)
669 defer limiterConnections.Add(c.remoteIP, time.Now(), -1)
671 // We register and unregister the original connection, in case c.conn is replaced
672 // with a TLS connection later on.
673 mox.Connections.Register(nc, "smtp", listenerName)
674 defer mox.Connections.Unregister(nc)
678 // We include the string ESMTP. https://cr.yp.to/smtp/greeting.html recommends it.
679 // Should not be too relevant nowadays, but does not hurt and default blackbox
680 // exporter SMTP health check expects it.
681 c.writelinef("%d %s ESMTP mox %s", smtp.C220ServiceReady, c.hostname.ASCII, moxvar.Version)
686 // If another command is present, don't flush our buffered response yet. Holding
687 // off will cause us to respond with a single packet.
690 buf, err := c.r.Peek(n)
691 if err == nil && bytes.IndexByte(buf, '\n') >= 0 {
699var commands = map[string]func(c *conn, p *parser){
700 "helo": (*conn).cmdHelo,
701 "ehlo": (*conn).cmdEhlo,
702 "starttls": (*conn).cmdStarttls,
703 "auth": (*conn).cmdAuth,
704 "mail": (*conn).cmdMail,
705 "rcpt": (*conn).cmdRcpt,
706 "data": (*conn).cmdData,
707 "rset": (*conn).cmdRset,
708 "vrfy": (*conn).cmdVrfy,
709 "expn": (*conn).cmdExpn,
710 "help": (*conn).cmdHelp,
711 "noop": (*conn).cmdNoop,
712 "quit": (*conn).cmdQuit,
715func command(c *conn) {
731 if errors.As(err, &serr) {
732 c.writecodeline(serr.code, serr.secode, fmt.Sprintf("%s (%s)", serr.errmsg, mox.ReceivedID(c.cid)), serr.err)
737 // Other type of panic, we pass it on, aborting the connection.
738 c.log.Errorx("command panic", err)
743 // todo future: we could wait for either a line or shutdown, and just close the connection on shutdown.
746 t := strings.SplitN(line, " ", 2)
752 cmdl := strings.ToLower(cmd)
754 // 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
757 case <-mox.Shutdown.Done():
759 c.writecodeline(smtp.C421ServiceUnavail, smtp.SeSys3NotAccepting2, "shutting down", nil)
765 c.cmdStart = time.Now()
767 p := newParser(args, c.smtputf8, c)
768 fn, ok := commands[cmdl]
772 // Other side is likely speaking something else than SMTP, send error message and
773 // stop processing because there is a good chance whatever they sent has multiple
775 c.writecodeline(smtp.C500BadSyntax, smtp.SeProto5Syntax2, "please try again speaking smtp", nil)
779 xsmtpUserErrorf(smtp.C500BadSyntax, smtp.SeProto5BadCmdOrSeq1, "unknown command")
785// For use in metric labels.
786func (c *conn) kind() string {
793func (c *conn) xneedHello() {
794 if c.hello.IsZero() {
795 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "no ehlo/helo yet")
799// If smtp server is configured to require TLS for all mail delivery (except to TLS
800// reporting address), abort command.
801func (c *conn) xneedTLSForDelivery(rcpt smtp.Path) {
802 // For TLS reports, we allow the message in even without TLS, because there may be
804 if c.requireTLSForDelivery && !c.tls && !isTLSReportRecipient(rcpt) {
806 xsmtpUserErrorf(smtp.C530SecurityRequired, smtp.SePol7Other0, "STARTTLS required for mail delivery")
810func isTLSReportRecipient(rcpt smtp.Path) bool {
811 _, _, dest, err := mox.FindAccount(rcpt.Localpart, rcpt.IPDomain.Domain, false)
812 return err == nil && (dest.HostTLSReports || dest.DomainTLSReports)
815func (c *conn) cmdHelo(p *parser) {
819func (c *conn) cmdEhlo(p *parser) {
824func (c *conn) cmdHello(p *parser, ehlo bool) {
825 var remote dns.IPDomain
826 if c.submission && !mox.Pedantic {
827 // Mail clients regularly put bogus information in the hostname/ip. For submission,
828 // the value is of no use, so there is not much point in annoying the user with
829 // errors they cannot fix themselves. Except when in pedantic mode.
830 remote = dns.IPDomain{IP: c.remoteIP}
834 remote = p.xipdomain(true)
836 remote = dns.IPDomain{Domain: p.xdomain()}
838 // Verify a remote domain name has an A or AAAA record, CNAME not allowed.
../rfc/5321:722
839 cidctx := context.WithValue(mox.Context, mlog.CidKey, c.cid)
840 ctx, cancel := context.WithTimeout(cidctx, time.Minute)
841 _, _, err := c.resolver.LookupIPAddr(ctx, remote.Domain.ASCII+".")
843 if dns.IsNotFound(err) {
844 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeProto5Other0, "your ehlo domain does not resolve to an IP address")
846 // For success or temporary resolve errors, we'll just continue.
849 // Though a few paragraphs earlier is a claim additional data can occur for address
850 // literals (IP addresses), although the ABNF in that document does not allow it.
851 // We allow additional text, but only if space-separated.
852 if len(remote.IP) > 0 && p.space() {
864 // https://www.iana.org/assignments/mail-parameters/mail-parameters.xhtml
866 c.bwritelinef("250-%s", c.hostname.ASCII)
870 if !c.tls && c.tlsConfig != nil {
872 c.bwritelinef("250-STARTTLS")
873 } else if c.extRequireTLS {
876 c.bwritelinef("250-REQUIRETLS")
880 if c.tls || !c.requireTLSForAuth {
881 // We always mention the SCRAM PLUS variants, even if TLS is not active: It is a
882 // hint to the client that a TLS connection can use TLS channel binding during
883 // authentication. The client should select the bare variant when TLS isn't
884 // present, and also not indicate the server supports the PLUS variant in that
885 // case, or it would trigger the mechanism downgrade detection.
886 c.bwritelinef("250-AUTH SCRAM-SHA-256-PLUS SCRAM-SHA-256 SCRAM-SHA-1-PLUS SCRAM-SHA-1 CRAM-MD5 PLAIN LOGIN")
888 c.bwritelinef("250-AUTH ")
892 c.bwritelinef("250-FUTURERELEASE %d %s", queue.FutureReleaseIntervalMax/time.Second, t.Format(time.RFC3339))
895 // todo future? c.writelinef("250-DSN")
903func (c *conn) cmdStarttls(p *parser) {
909 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "already speaking tls")
911 if c.account != nil {
912 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "cannot starttls after authentication")
915 // We don't want to do TLS on top of c.r because it also prints protocol traces: We
916 // don't want to log the TLS stream. So we'll do TLS on the underlying connection,
917 // but make sure any bytes already read and in the buffer are used for the TLS
920 if n := c.r.Buffered(); n > 0 {
921 conn = &moxio.PrefixConn{
922 PrefixReader: io.LimitReader(c.r, int64(n)),
927 // We add the cid to the output, to help debugging in case of a failing TLS connection.
928 c.writecodeline(smtp.C220ServiceReady, smtp.SeOther00, "go! ("+mox.ReceivedID(c.cid)+")", nil)
929 tlsConn := tls.Server(conn, c.tlsConfig)
930 cidctx := context.WithValue(mox.Context, mlog.CidKey, c.cid)
931 ctx, cancel := context.WithTimeout(cidctx, time.Minute)
933 c.log.Debug("starting tls server handshake")
934 if err := tlsConn.HandshakeContext(ctx); err != nil {
935 panic(fmt.Errorf("starttls handshake: %s (%w)", err, errIO))
938 tlsversion, ciphersuite := moxio.TLSInfo(tlsConn)
939 c.log.Debug("tls server handshake done", slog.String("tls", tlsversion), slog.String("ciphersuite", ciphersuite))
941 c.tr = moxio.NewTraceReader(c.log, "RC: ", c)
942 c.tw = moxio.NewTraceWriter(c.log, "LS: ", c)
943 c.r = bufio.NewReader(c.tr)
944 c.w = bufio.NewWriter(c.tw)
951func (c *conn) cmdAuth(p *parser) {
955 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "authentication only allowed on submission ports")
957 if c.account != nil {
959 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "already authenticated")
961 if c.mailFrom != nil {
963 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "authentication not allowed during mail transaction")
966 // If authentication fails due to missing derived secrets, we don't hold it against
967 // the connection. There is no way to indicate server support for an authentication
968 // mechanism, but that a mechanism won't work for an account.
969 var missingDerivedSecrets bool
971 // For many failed auth attempts, slow down verification attempts.
972 // Dropping the connection could also work, but more so when we have a connection rate limiter.
974 if c.authFailed > 3 && authFailDelay > 0 {
976 mox.Sleep(mox.Context, time.Duration(c.authFailed-3)*authFailDelay)
978 c.authFailed++ // Compensated on success.
980 if missingDerivedSecrets {
983 // On the 3rd failed authentication, start responding slowly. Successful auth will
984 // cause fast responses again.
985 if c.authFailed >= 3 {
990 var authVariant string
991 authResult := "error"
993 metrics.AuthenticationInc("submission", authVariant, authResult)
994 if authResult == "ok" {
995 mox.LimiterFailedAuth.Reset(c.remoteIP, time.Now())
996 } else if !missingDerivedSecrets {
997 mox.LimiterFailedAuth.Add(c.remoteIP, time.Now(), 1)
1003 mech := p.xsaslMech()
1005 xreadInitial := func() []byte {
1009 // todo future: handle max length of 12288 octets and return proper responde codes otherwise
../rfc/4954:253
1013 authResult = "aborted"
1014 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5Other0, "authentication aborted")
1019 // Windows Mail 16005.14326.21606.0 sends two spaces between "AUTH PLAIN" and the
1024 auth = p.remainder()
1027 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5Syntax2, "missing initial auth base64 parameter after space")
1028 } else if auth == "=" {
1030 auth = "" // Base64 decode below will result in empty buffer.
1033 buf, err := base64.StdEncoding.DecodeString(auth)
1036 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5Syntax2, "invalid base64: %s", err)
1041 xreadContinuation := func() []byte {
1042 line := c.readline()
1044 authResult = "aborted"
1045 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5Other0, "authentication aborted")
1047 buf, err := base64.StdEncoding.DecodeString(line)
1050 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5Syntax2, "invalid base64: %s", err)
1057 authVariant = "plain"
1061 if !c.tls && c.requireTLSForAuth {
1062 xsmtpUserErrorf(smtp.C538EncReqForAuth, smtp.SePol7EncReqForAuth11, "authentication requires tls")
1065 // Password is in line in plain text, so hide it.
1066 defer c.xtrace(mlog.LevelTraceauth)()
1067 buf := xreadInitial()
1068 c.xtrace(mlog.LevelTrace) // Restore.
1069 plain := bytes.Split(buf, []byte{0})
1070 if len(plain) != 3 {
1071 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5BadParams4, "auth data should have 3 nul-separated tokens, got %d", len(plain))
1073 authz := norm.NFC.String(string(plain[0]))
1074 authc := norm.NFC.String(string(plain[1]))
1075 password := string(plain[2])
1077 if authz != "" && authz != authc {
1078 authResult = "badcreds"
1079 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "cannot assume other role")
1082 acc, err := store.OpenEmailAuth(c.log, authc, password)
1083 if err != nil && errors.Is(err, store.ErrUnknownCredentials) {
1085 authResult = "badcreds"
1086 c.log.Info("failed authentication attempt", slog.String("username", authc), slog.Any("remote", c.remoteIP))
1087 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass")
1089 xcheckf(err, "verifying credentials")
1097 c.writecodeline(smtp.C235AuthSuccess, smtp.SePol7Other0, "nice", nil)
1100 // LOGIN is obsoleted in favor of PLAIN, only implemented to support legacy
1101 // clients, see Internet-Draft (I-D):
1102 // https://datatracker.ietf.org/doc/html/draft-murchison-sasl-login-00
1104 authVariant = "login"
1108 if !c.tls && c.requireTLSForAuth {
1109 xsmtpUserErrorf(smtp.C538EncReqForAuth, smtp.SePol7EncReqForAuth11, "authentication requires tls")
1112 // Read user name. The I-D says the client should ignore the server challenge, we
1113 // send an empty one.
1114 // I-D says maximum length must be 64 bytes. We allow more, for long user names
1116 username := string(xreadInitial())
1117 username = norm.NFC.String(username)
1119 // Again, client should ignore the challenge, we send the same as the example in
1121 c.writelinef("%d %s", smtp.C334ContinueAuth, base64.StdEncoding.EncodeToString([]byte("Password")))
1123 // Password is in line in plain text, so hide it.
1124 defer c.xtrace(mlog.LevelTraceauth)()
1125 password := string(xreadContinuation())
1126 c.xtrace(mlog.LevelTrace) // Restore.
1128 acc, err := store.OpenEmailAuth(c.log, username, password)
1129 if err != nil && errors.Is(err, store.ErrUnknownCredentials) {
1131 authResult = "badcreds"
1132 c.log.Info("failed authentication attempt", slog.String("username", username), slog.Any("remote", c.remoteIP))
1133 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass")
1135 xcheckf(err, "verifying credentials")
1141 c.username = username
1143 c.writecodeline(smtp.C235AuthSuccess, smtp.SePol7Other0, "hello ancient smtp implementation", nil)
1146 authVariant = strings.ToLower(mech)
1151 chal := fmt.Sprintf("<%d.%d@%s>", uint64(mox.CryptoRandInt()), time.Now().UnixNano(), mox.Conf.Static.HostnameDomain.ASCII)
1152 c.writelinef("%d %s", smtp.C334ContinueAuth, base64.StdEncoding.EncodeToString([]byte(chal)))
1154 resp := xreadContinuation()
1155 t := strings.Split(string(resp), " ")
1156 if len(t) != 2 || len(t[1]) != 2*md5.Size {
1157 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5BadParams4, "malformed cram-md5 response")
1159 addr := norm.NFC.String(t[0])
1160 c.log.Debug("cram-md5 auth", slog.String("address", addr))
1161 acc, _, err := store.OpenEmail(c.log, addr)
1163 if errors.Is(err, store.ErrUnknownCredentials) {
1164 c.log.Info("failed authentication attempt", slog.String("username", addr), slog.Any("remote", c.remoteIP))
1165 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass")
1168 xcheckf(err, "looking up address")
1172 c.log.Check(err, "closing account")
1175 var ipadhash, opadhash hash.Hash
1176 acc.WithRLock(func() {
1177 err := acc.DB.Read(context.TODO(), func(tx *bstore.Tx) error {
1178 password, err := bstore.QueryTx[store.Password](tx).Get()
1179 if err == bstore.ErrAbsent {
1180 c.log.Info("failed authentication attempt", slog.String("username", addr), slog.Any("remote", c.remoteIP))
1181 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass")
1187 ipadhash = password.CRAMMD5.Ipad
1188 opadhash = password.CRAMMD5.Opad
1191 xcheckf(err, "tx read")
1193 if ipadhash == nil || opadhash == nil {
1194 missingDerivedSecrets = true
1195 c.log.Info("cram-md5 auth attempt without derived secrets set, save password again to store secrets", slog.String("username", addr))
1196 c.log.Info("failed authentication attempt", slog.String("username", addr), slog.Any("remote", c.remoteIP))
1197 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass")
1201 ipadhash.Write([]byte(chal))
1202 opadhash.Write(ipadhash.Sum(nil))
1203 digest := fmt.Sprintf("%x", opadhash.Sum(nil))
1205 c.log.Info("failed authentication attempt", slog.String("username", addr), slog.Any("remote", c.remoteIP))
1206 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass")
1213 acc = nil // Cancel cleanup.
1216 c.writecodeline(smtp.C235AuthSuccess, smtp.SePol7Other0, "nice", nil)
1218 case "SCRAM-SHA-256-PLUS", "SCRAM-SHA-256", "SCRAM-SHA-1-PLUS", "SCRAM-SHA-1":
1219 // 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?
1220 // todo: use single implementation between ../imapserver/server.go and ../smtpserver/server.go
1222 // Passwords cannot be retrieved or replayed from the trace.
1224 authVariant = strings.ToLower(mech)
1225 var h func() hash.Hash
1226 switch authVariant {
1227 case "scram-sha-1", "scram-sha-1-plus":
1229 case "scram-sha-256", "scram-sha-256-plus":
1232 xsmtpServerErrorf(codes{smtp.C554TransactionFailed, smtp.SeSys3Other0}, "missing scram auth method case")
1235 var cs *tls.ConnectionState
1236 channelBindingRequired := strings.HasSuffix(authVariant, "-plus")
1237 if channelBindingRequired && !c.tls {
1239 xsmtpUserErrorf(smtp.C538EncReqForAuth, smtp.SePol7EncReqForAuth11, "scram plus mechanism requires tls connection")
1242 xcs := c.conn.(*tls.Conn).ConnectionState()
1245 c0 := xreadInitial()
1246 ss, err := scram.NewServer(h, c0, cs, channelBindingRequired)
1247 xcheckf(err, "starting scram")
1248 authc := norm.NFC.String(ss.Authentication)
1249 c.log.Debug("scram auth", slog.String("authentication", authc))
1250 acc, _, err := store.OpenEmail(c.log, authc)
1252 // todo: we could continue scram with a generated salt, deterministically generated
1253 // from the username. that way we don't have to store anything but attackers cannot
1254 // learn if an account exists. same for absent scram saltedpassword below.
1255 c.log.Info("failed authentication attempt", slog.String("username", authc), slog.Any("remote", c.remoteIP))
1256 xsmtpUserErrorf(smtp.C454TempAuthFail, smtp.SeSys3Other0, "scram not possible")
1261 c.log.Check(err, "closing account")
1264 if ss.Authorization != "" && ss.Authorization != ss.Authentication {
1265 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "authentication with authorization for different user not supported")
1267 var xscram store.SCRAM
1268 acc.WithRLock(func() {
1269 err := acc.DB.Read(context.TODO(), func(tx *bstore.Tx) error {
1270 password, err := bstore.QueryTx[store.Password](tx).Get()
1271 if err == bstore.ErrAbsent {
1272 c.log.Info("failed authentication attempt", slog.String("username", authc), slog.Any("remote", c.remoteIP))
1273 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass")
1275 xcheckf(err, "fetching credentials")
1276 switch authVariant {
1277 case "scram-sha-1", "scram-sha-1-plus":
1278 xscram = password.SCRAMSHA1
1279 case "scram-sha-256", "scram-sha-256-plus":
1280 xscram = password.SCRAMSHA256
1282 xsmtpServerErrorf(codes{smtp.C554TransactionFailed, smtp.SeSys3Other0}, "missing scram auth credentials case")
1284 if len(xscram.Salt) == 0 || xscram.Iterations == 0 || len(xscram.SaltedPassword) == 0 {
1285 missingDerivedSecrets = true
1286 c.log.Info("scram auth attempt without derived secrets set, save password again to store secrets", slog.String("address", authc))
1287 c.log.Info("failed authentication attempt", slog.String("username", authc), slog.Any("remote", c.remoteIP))
1288 xsmtpUserErrorf(smtp.C454TempAuthFail, smtp.SeSys3Other0, "scram not possible")
1292 xcheckf(err, "read tx")
1294 s1, err := ss.ServerFirst(xscram.Iterations, xscram.Salt)
1295 xcheckf(err, "scram first server step")
1296 c.writelinef("%d %s", smtp.C334ContinueAuth, base64.StdEncoding.EncodeToString([]byte(s1))) //
../rfc/4954:187
1297 c2 := xreadContinuation()
1298 s3, err := ss.Finish(c2, xscram.SaltedPassword)
1300 c.writelinef("%d %s", smtp.C334ContinueAuth, base64.StdEncoding.EncodeToString([]byte(s3))) //
../rfc/4954:187
1303 c.readline() // Should be "*" for cancellation.
1304 if errors.Is(err, scram.ErrInvalidProof) {
1305 authResult = "badcreds"
1306 c.log.Info("failed authentication attempt", slog.String("username", authc), slog.Any("remote", c.remoteIP))
1307 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad credentials")
1309 xcheckf(err, "server final")
1313 // The message should be empty. todo: should we require it is empty?
1320 acc = nil // Cancel cleanup.
1323 c.writecodeline(smtp.C235AuthSuccess, smtp.SePol7Other0, "nice", nil)
1327 xsmtpUserErrorf(smtp.C504ParamNotImpl, smtp.SeProto5BadParams4, "mechanism %s not supported", mech)
1332func (c *conn) cmdMail(p *parser) {
1333 // requirements for maximum line length:
1335 // todo future: enforce? doesn't really seem worth it...
1337 if c.transactionBad > 10 && c.transactionGood == 0 {
1338 // If we get many bad transactions, it's probably a spammer that is guessing user names.
1339 // Useful in combination with rate limiting.
1341 c.writecodeline(smtp.C550MailboxUnavail, smtp.SeAddr1Other0, "too many failures", nil)
1347 if c.mailFrom != nil {
1349 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "already have MAIL")
1351 // Ensure clear transaction state on failure.
1362 // Microsoft Outlook 365 Apps for Enterprise sends it with submission. For delivery
1363 // it is mostly used by spammers, but has been seen with legitimate senders too.
1367 rawRevPath := p.xrawReversePath()
1368 paramSeen := map[string]bool{}
1371 key := p.xparamKeyword()
1373 K := strings.ToUpper(key)
1376 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5BadParams4, "duplicate param %q", key)
1384 if size > c.maxMessageSize {
1386 ecode := smtp.SeSys3MsgLimitExceeded4
1387 if size < config.DefaultMaxMsgSize {
1388 ecode = smtp.SeMailbox2MsgLimitExceeded3
1390 xsmtpUserErrorf(smtp.C552MailboxFull, ecode, "message too large")
1392 // We won't verify the message is exactly the size the remote claims. Buf if it is
1393 // larger, we'll abort the transaction when remote crosses the boundary.
1397 v := p.xparamValue()
1398 switch strings.ToUpper(v) {
1400 c.has8bitmime = false
1402 c.has8bitmime = true
1404 xsmtpUserErrorf(smtp.C555UnrecognizedAddrParams, smtp.SeProto5BadParams4, "unrecognized parameter %q", key)
1409 // We act as if we don't trust the client to specify a mailbox. Instead, we always
1410 // check the rfc5321.mailfrom and rfc5322.from before accepting the submission.
1414 // 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
1425 xsmtpUserErrorf(smtp.C530SecurityRequired, smtp.SePol7EncNeeded10, "requiretls only allowed on tls-encrypted connections")
1426 } else if !c.extRequireTLS {
1427 xsmtpUserErrorf(smtp.C555UnrecognizedAddrParams, smtp.SeSys3NotSupported3, "REQUIRETLS not allowed for this connection")
1431 case "HOLDFOR", "HOLDUNTIL":
1434 xsmtpUserErrorf(smtp.C555UnrecognizedAddrParams, smtp.SeSys3NotSupported3, "unrecognized parameter %q", key)
1436 if K == "HOLDFOR" && paramSeen["HOLDUNTIL"] || K == "HOLDUNTIL" && paramSeen["HOLDFOR"] {
1438 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5BadParams4, "cannot use both HOLDUNTIL and HOLFOR")
1442 // semantic errors as syntax errors
1445 if n > int64(queue.FutureReleaseIntervalMax/time.Second) {
1447 xsmtpUserErrorf(smtp.C554TransactionFailed, smtp.SeProto5BadParams4, "future release interval too far in the future")
1449 c.futureRelease = time.Now().Add(time.Duration(n) * time.Second)
1450 c.futureReleaseRequest = fmt.Sprintf("for;%d", n)
1452 t, s := p.xdatetimeutc()
1453 ival := time.Until(t)
1455 // Likely a mistake by the user.
1456 xsmtpUserErrorf(smtp.C554TransactionFailed, smtp.SeProto5BadParams4, "requested future release time is in the past")
1457 } else if ival > queue.FutureReleaseIntervalMax {
1459 xsmtpUserErrorf(smtp.C554TransactionFailed, smtp.SeProto5BadParams4, "requested future release time is too far in the future")
1462 c.futureReleaseRequest = "until;" + s
1466 xsmtpUserErrorf(smtp.C555UnrecognizedAddrParams, smtp.SeSys3NotSupported3, "unrecognized parameter %q", key)
1470 // We now know if we have to parse the address with support for utf8.
1471 pp := newParser(rawRevPath, c.smtputf8, c)
1472 rpath := pp.xbareReversePath()
1477 // For submission, check if reverse path is allowed. I.e. authenticated account
1478 // must have the rpath configured. We do a check again on rfc5322.from during DATA.
1479 rpathAllowed := func() bool {
1484 accName, _, _, err := mox.FindAccount(rpath.Localpart, rpath.IPDomain.Domain, false)
1485 return err == nil && accName == c.account.Name
1488 if !c.submission && !rpath.IPDomain.Domain.IsZero() {
1489 // If rpath domain has null MX record or is otherwise not accepting email, reject.
1492 cidctx := context.WithValue(mox.Context, mlog.CidKey, c.cid)
1493 ctx, cancel := context.WithTimeout(cidctx, time.Minute)
1494 valid, err := checkMXRecords(ctx, c.resolver, rpath.IPDomain.Domain)
1497 c.log.Infox("temporary reject for temporary mx lookup error", err)
1498 xsmtpServerErrorf(codes{smtp.C451LocalErr, smtp.SeNet4Other0}, "cannot verify mx records for mailfrom domain")
1500 c.log.Info("permanent reject because mailfrom domain does not accept mail")
1501 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SePol7SenderHasNullMX27, "mailfrom domain not configured for mail")
1505 if c.submission && (len(rpath.IPDomain.IP) > 0 || !rpathAllowed()) {
1507 c.log.Info("submission with unconfigured mailfrom", slog.String("user", c.username), slog.String("mailfrom", rpath.String()))
1508 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SePol7DeliveryUnauth1, "must match authenticated user")
1509 } else if !c.submission && len(rpath.IPDomain.IP) > 0 {
1510 // todo future: allow if the IP is the same as this connection is coming from? does later code allow this?
1511 c.log.Info("delivery from address without domain", slog.String("mailfrom", rpath.String()))
1512 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SePol7Other0, "domain name required")
1515 if Localserve && strings.HasPrefix(string(rpath.Localpart), "mailfrom") {
1516 c.xlocalserveError(rpath.Localpart)
1521 c.bwritecodeline(smtp.C250Completed, smtp.SeAddr1Other0, "looking good", nil)
1525func (c *conn) cmdRcpt(p *parser) {
1528 if c.mailFrom == nil {
1530 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "missing MAIL FROM")
1536 // Microsoft Outlook 365 Apps for Enterprise sends it with submission. For delivery
1537 // it is mostly used by spammers, but has been seen with legitimate senders too.
1542 if p.take("<POSTMASTER>") {
1543 fpath = smtp.Path{Localpart: "postmaster"}
1545 fpath = p.xforwardPath()
1549 key := p.xparamKeyword()
1550 // K := strings.ToUpper(key)
1553 xsmtpUserErrorf(smtp.C555UnrecognizedAddrParams, smtp.SeSys3NotSupported3, "unrecognized parameter %q", key)
1557 // Check if TLS is enabled if required. It's not great that sender/recipient
1558 // addresses may have been exposed in plaintext before we can reject delivery. The
1559 // recipient could be the tls reporting addresses, which must always be able to
1560 // receive in plain text.
1561 c.xneedTLSForDelivery(fpath)
1563 // todo future: for submission, should we do explicit verification that domains are fully qualified? also for mail from.
../rfc/6409:420
1565 if len(c.recipients) >= rcptToLimit {
1567 xsmtpUserErrorf(smtp.C452StorageFull, smtp.SeProto5TooManyRcpts3, "max of %d recipients reached", rcptToLimit)
1570 // We don't want to allow delivery to multiple recipients with a null reverse path.
1571 // Why would anyone send like that? Null reverse path is intended for delivery
1572 // notifications, they should go to a single recipient.
1573 if !c.submission && len(c.recipients) > 0 && c.mailFrom.IsZero() {
1574 xsmtpUserErrorf(smtp.C452StorageFull, smtp.SeProto5TooManyRcpts3, "only one recipient allowed with null reverse address")
1577 // Do not accept multiple recipients if remote does not pass SPF. Because we don't
1578 // want to generate DSNs to unverified domains. This is the moment we
1579 // can refuse individual recipients, DATA will be too late. Because mail
1580 // servers must handle a max recipient limit gracefully and still send to the
1581 // recipients that are accepted, this should not cause problems. Though we are in
1582 // violation because the limit must be >= 100.
1586 if !c.submission && len(c.recipients) == 1 && !Localserve {
1587 // note: because of check above, mailFrom cannot be the null address.
1589 d := c.mailFrom.IPDomain.Domain
1591 // todo: use this spf result for DATA.
1592 spfArgs := spf.Args{
1593 RemoteIP: c.remoteIP,
1594 MailFromLocalpart: c.mailFrom.Localpart,
1596 HelloDomain: c.hello,
1598 LocalHostname: c.hostname,
1600 cidctx := context.WithValue(mox.Context, mlog.CidKey, c.cid)
1601 spfctx, spfcancel := context.WithTimeout(cidctx, time.Minute)
1603 receivedSPF, _, _, _, err := spf.Verify(spfctx, c.log.Logger, c.resolver, spfArgs)
1606 c.log.Errorx("spf verify for multiple recipients", err)
1608 pass = receivedSPF.Identity == spf.ReceivedMailFrom && receivedSPF.Result == spf.StatusPass
1611 xsmtpUserErrorf(smtp.C452StorageFull, smtp.SeProto5TooManyRcpts3, "only one recipient allowed without spf pass")
1616 if strings.HasPrefix(string(fpath.Localpart), "rcptto") {
1617 c.xlocalserveError(fpath.Localpart)
1620 // If account or destination doesn't exist, it will be handled during delivery. For
1621 // submissions, which is the common case, we'll deliver to the logged in user,
1622 // which is typically the mox user.
1623 acc, _ := mox.Conf.Account("mox")
1624 dest := acc.Destinations["mox@localhost"]
1625 c.recipients = append(c.recipients, rcptAccount{fpath, true, "mox", dest, "mox@localhost"})
1626 } else if len(fpath.IPDomain.IP) > 0 {
1628 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeAddr1UnknownDestMailbox1, "not accepting email for ip")
1630 c.recipients = append(c.recipients, rcptAccount{fpath, false, "", config.Destination{}, ""})
1631 } else if accountName, canonical, addr, err := mox.FindAccount(fpath.Localpart, fpath.IPDomain.Domain, true); err == nil {
1633 c.recipients = append(c.recipients, rcptAccount{fpath, true, accountName, addr, canonical})
1634 } else if errors.Is(err, mox.ErrDomainNotFound) {
1636 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeAddr1UnknownDestMailbox1, "not accepting email for domain")
1638 // We'll be delivering this email.
1639 c.recipients = append(c.recipients, rcptAccount{fpath, false, "", config.Destination{}, ""})
1640 } else if errors.Is(err, mox.ErrAccountNotFound) {
1642 // For submission, we're transparent about which user exists. Should be fine for the typical small-scale deploy.
1644 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeAddr1UnknownDestMailbox1, "no such user")
1646 // We pretend to accept. We don't want to let remote know the user does not exist
1647 // until after DATA. Because then remote has committed to sending a message.
1648 // note: not local for !c.submission is the signal this address is in error.
1649 c.recipients = append(c.recipients, rcptAccount{fpath, false, "", config.Destination{}, ""})
1651 c.log.Errorx("looking up account for delivery", err, slog.Any("rcptto", fpath))
1652 xsmtpServerErrorf(codes{smtp.C451LocalErr, smtp.SeSys3Other0}, "error processing")
1654 c.bwritecodeline(smtp.C250Completed, smtp.SeAddr1Other0, "now on the list", nil)
1658func (c *conn) cmdData(p *parser) {
1661 if c.mailFrom == nil {
1663 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "missing MAIL FROM")
1665 if len(c.recipients) == 0 {
1667 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "missing RCPT TO")
1673 // 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.
1675 // Entire delivery should be done within 30 minutes, or we abort.
1676 cidctx := context.WithValue(mox.Context, mlog.CidKey, c.cid)
1677 cmdctx, cmdcancel := context.WithTimeout(cidctx, 30*time.Minute)
1679 // Deadline is taken into account by Read and Write.
1680 c.deadline, _ = cmdctx.Deadline()
1682 c.deadline = time.Time{}
1686 c.writelinef("354 see you at the bare dot")
1688 // Mark as tracedata.
1689 defer c.xtrace(mlog.LevelTracedata)()
1691 // We read the data into a temporary file. We limit the size and do basic analysis while reading.
1692 dataFile, err := store.CreateMessageTemp(c.log, "smtp-deliver")
1694 xsmtpServerErrorf(errCodes(smtp.C451LocalErr, smtp.SeSys3Other0, err), "creating temporary file for message: %s", err)
1696 defer store.CloseRemoveTempFile(c.log, dataFile, "smtpserver delivered message")
1697 msgWriter := message.NewWriter(dataFile)
1698 dr := smtp.NewDataReader(c.r)
1699 n, err := io.Copy(&limitWriter{maxSize: c.maxMessageSize, w: msgWriter}, dr)
1700 c.xtrace(mlog.LevelTrace) // Restore.
1702 if errors.Is(err, errMessageTooLarge) {
1704 ecode := smtp.SeSys3MsgLimitExceeded4
1705 if n < config.DefaultMaxMsgSize {
1706 ecode = smtp.SeMailbox2MsgLimitExceeded3
1708 c.writecodeline(smtp.C451LocalErr, ecode, fmt.Sprintf("error copying data to file (%s)", mox.ReceivedID(c.cid)), err)
1709 panic(fmt.Errorf("remote sent too much DATA: %w", errIO))
1712 if errors.Is(err, smtp.ErrCRLF) {
1713 c.writecodeline(smtp.C500BadSyntax, smtp.SeProto5Syntax2, fmt.Sprintf("invalid bare \\r or \\n, may be smtp smuggling (%s)", mox.ReceivedID(c.cid)), err)
1717 // Something is failing on our side. We want to let remote know. So write an error response,
1718 // then discard the remaining data so the remote client is more likely to see our
1719 // response. Our write is synchronous, there is a risk no window/buffer space is
1720 // available and our write blocks us from reading remaining data, leading to
1721 // deadlock. We have a timeout on our connection writes though, so worst case we'll
1722 // abort the connection due to expiration.
1723 c.writecodeline(smtp.C451LocalErr, smtp.SeSys3Other0, fmt.Sprintf("error copying data to file (%s)", mox.ReceivedID(c.cid)), err)
1724 io.Copy(io.Discard, dr)
1728 // Basic sanity checks on messages before we send them out to the world. Just
1729 // trying to be strict in what we do to others and liberal in what we accept.
1731 if !msgWriter.HaveBody {
1733 xsmtpUserErrorf(smtp.C554TransactionFailed, smtp.SeMsg6Other0, "message requires both header and body section")
1735 // Check only for pedantic mode because ios mail will attempt to send smtputf8 with
1736 // non-ascii in message from localpart without using 8bitmime.
1737 if mox.Pedantic && msgWriter.Has8bit && !c.has8bitmime {
1739 xsmtpUserErrorf(smtp.C500BadSyntax, smtp.SeMsg6Other0, "message with non-us-ascii requires 8bitmime extension")
1743 if Localserve && mox.Pedantic {
1744 // Require that message can be parsed fully.
1745 p, err := message.Parse(c.log.Logger, false, dataFile)
1747 err = p.Walk(c.log.Logger, nil)
1751 xsmtpUserErrorf(smtp.C554TransactionFailed, smtp.SeMsg6Other0, "malformed message: %v", err)
1755 // Prepare "Received" header.
1759 var iprevStatus iprev.Status // Only for delivery, not submission.
1760 var iprevAuthentic bool
1762 // Hide internal hosts.
1763 // todo future: make this a config option, where admins specify ip ranges that they don't want exposed. also see
../rfc/5321:4321
1764 recvFrom = message.HeaderCommentDomain(mox.Conf.Static.HostnameDomain, c.smtputf8)
1766 if len(c.hello.IP) > 0 {
1767 recvFrom = smtp.AddressLiteral(c.hello.IP)
1769 // ASCII-only version added after the extended-domain syntax below, because the
1770 // comment belongs to "BY" which comes immediately after "FROM".
1771 recvFrom = c.hello.Domain.XName(c.smtputf8)
1773 iprevctx, iprevcancel := context.WithTimeout(cmdctx, time.Minute)
1775 var revNames []string
1776 iprevStatus, revName, revNames, iprevAuthentic, err = iprev.Lookup(iprevctx, c.resolver, c.remoteIP)
1779 c.log.Infox("reverse-forward lookup", err, slog.Any("remoteip", c.remoteIP))
1781 c.log.Debug("dns iprev check", slog.Any("addr", c.remoteIP), slog.Any("status", iprevStatus))
1785 } else if len(revNames) > 0 {
1788 name = strings.TrimSuffix(name, ".")
1790 if name != "" && name != c.hello.Domain.XName(c.smtputf8) {
1791 recvFrom += name + " "
1793 recvFrom += smtp.AddressLiteral(c.remoteIP) + ")"
1794 if c.smtputf8 && c.hello.Domain.Unicode != "" {
1795 recvFrom += " (" + c.hello.Domain.ASCII + ")"
1798 recvBy := mox.Conf.Static.HostnameDomain.XName(c.smtputf8)
1799 recvBy += " (" + smtp.AddressLiteral(c.localIP) + ")" // todo: hide ip if internal?
1800 if c.smtputf8 && mox.Conf.Static.HostnameDomain.Unicode != "" {
1801 // This syntax is part of "VIA".
1802 recvBy += " (" + mox.Conf.Static.HostnameDomain.ASCII + ")"
1815 if c.account != nil {
1820 // Assume transaction does not succeed. If it does, we'll compensate.
1823 recvHdrFor := func(rcptTo string) string {
1824 recvHdr := &message.HeaderWriter{}
1825 // For additional Received-header clauses, see:
1826 // https://www.iana.org/assignments/mail-parameters/mail-parameters.xhtml#table-mail-parameters-8
1828 if c.requireTLS != nil && *c.requireTLS {
1830 withComment = " (requiretls)"
1832 recvHdr.Add(" ", "Received:", "from", recvFrom, "by", recvBy, "via", "tcp", "with", with+withComment, "id", mox.ReceivedID(c.cid)) //
../rfc/5321:3158
1834 tlsConn := c.conn.(*tls.Conn)
1835 tlsComment := mox.TLSReceivedComment(c.log, tlsConn.ConnectionState())
1836 recvHdr.Add(" ", tlsComment...)
1838 // We leave out an empty "for" clause. This is empty for messages submitted to
1839 // multiple recipients, so the message stays identical and a single smtp
1840 // transaction can deliver, only transferring the data once.
1842 recvHdr.Add(" ", "for", "<"+rcptTo+">;")
1844 recvHdr.Add(" ", time.Now().Format(message.RFC5322Z))
1845 return recvHdr.String()
1848 // Submission is easiest because user is trusted. Far fewer checks to make. So
1849 // handle it first, and leave the rest of the function for handling wild west
1850 // internet traffic.
1852 c.submit(cmdctx, recvHdrFor, msgWriter, dataFile)
1854 c.deliver(cmdctx, recvHdrFor, msgWriter, iprevStatus, iprevAuthentic, dataFile)
1858// Check if a message has unambiguous "TLS-Required: No" header. Messages must not
1859// contain multiple TLS-Required headers. The only valid value is "no". But we'll
1860// accept multiple headers as long as all they are all "no".
1862func hasTLSRequiredNo(h textproto.MIMEHeader) bool {
1863 l := h.Values("Tls-Required")
1867 for _, v := range l {
1868 if !strings.EqualFold(v, "no") {
1875// submit is used for mail from authenticated users that we will try to deliver.
1876func (c *conn) submit(ctx context.Context, recvHdrFor func(string) string, msgWriter *message.Writer, dataFile *os.File) {
1877 // Similar between ../smtpserver/server.go:/submit\( and ../webmail/webmail.go:/MessageSubmit\(
1879 var msgPrefix []byte
1881 // Check that user is only sending email as one of its configured identities. Not
1885 msgFrom, _, header, err := message.From(c.log.Logger, true, dataFile)
1887 metricSubmission.WithLabelValues("badmessage").Inc()
1888 c.log.Infox("parsing message From address", err, slog.String("user", c.username))
1889 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeMsg6Other0, "cannot parse header or From address: %v", err)
1891 accName, _, _, err := mox.FindAccount(msgFrom.Localpart, msgFrom.Domain, true)
1892 if err != nil || accName != c.account.Name {
1895 err = mox.ErrAccountNotFound
1897 metricSubmission.WithLabelValues("badfrom").Inc()
1898 c.log.Infox("verifying message From address", err, slog.String("user", c.username), slog.Any("msgfrom", msgFrom))
1899 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SePol7DeliveryUnauth1, "must match authenticated user")
1902 // TLS-Required: No header makes us not enforce recipient domain's TLS policy.
1905 if c.requireTLS == nil && hasTLSRequiredNo(header) {
1910 // Outgoing messages should not have a Return-Path header. The final receiving mail
1911 // server will add it.
1913 if mox.Pedantic && header.Values("Return-Path") != nil {
1914 metricSubmission.WithLabelValues("badheader").Inc()
1915 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeMsg6Other0, "message should not have Return-Path header")
1918 // Add Message-Id header if missing.
1920 messageID := header.Get("Message-Id")
1921 if messageID == "" {
1922 messageID = mox.MessageIDGen(c.smtputf8)
1923 msgPrefix = append(msgPrefix, fmt.Sprintf("Message-Id: <%s>\r\n", messageID)...)
1927 if header.Get("Date") == "" {
1928 msgPrefix = append(msgPrefix, "Date: "+time.Now().Format(message.RFC5322Z)+"\r\n"...)
1931 // Check outoging message rate limit.
1932 err = c.account.DB.Read(ctx, func(tx *bstore.Tx) error {
1933 rcpts := make([]smtp.Path, len(c.recipients))
1934 for i, r := range c.recipients {
1937 msglimit, rcptlimit, err := c.account.SendLimitReached(tx, rcpts)
1938 xcheckf(err, "checking sender limit")
1940 metricSubmission.WithLabelValues("messagelimiterror").Inc()
1941 xsmtpUserErrorf(smtp.C451LocalErr, smtp.SePol7DeliveryUnauth1, "max number of messages (%d) over past 24h reached, try increasing per-account setting MaxOutgoingMessagesPerDay", msglimit)
1942 } else if rcptlimit >= 0 {
1943 metricSubmission.WithLabelValues("recipientlimiterror").Inc()
1944 xsmtpUserErrorf(smtp.C451LocalErr, smtp.SePol7DeliveryUnauth1, "max number of new/first-time recipients (%d) over past 24h reached, try increasing per-account setting MaxFirstTimeRecipientsPerDay", rcptlimit)
1948 xcheckf(err, "read-only transaction")
1950 // 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.
1952 // Add DKIM signatures.
1953 confDom, ok := mox.Conf.Domain(msgFrom.Domain)
1955 c.log.Error("domain disappeared", slog.Any("domain", msgFrom.Domain))
1956 xsmtpServerErrorf(codes{smtp.C451LocalErr, smtp.SeSys3Other0}, "internal error")
1959 selectors := mox.DKIMSelectors(confDom.DKIM)
1960 if len(selectors) > 0 {
1961 if canonical, err := mox.CanonicalLocalpart(msgFrom.Localpart, confDom); err != nil {
1962 c.log.Errorx("determining canonical localpart for dkim signing", err, slog.Any("localpart", msgFrom.Localpart))
1963 } else if dkimHeaders, err := dkim.Sign(ctx, c.log.Logger, canonical, msgFrom.Domain, selectors, c.smtputf8, store.FileMsgReader(msgPrefix, dataFile)); err != nil {
1964 c.log.Errorx("dkim sign for domain", err, slog.Any("domain", msgFrom.Domain))
1965 metricServerErrors.WithLabelValues("dkimsign").Inc()
1967 msgPrefix = append(msgPrefix, []byte(dkimHeaders)...)
1971 authResults := message.AuthResults{
1972 Hostname: mox.Conf.Static.HostnameDomain.XName(c.smtputf8),
1973 Comment: mox.Conf.Static.HostnameDomain.ASCIIExtra(c.smtputf8),
1974 Methods: []message.AuthMethod{
1978 Props: []message.AuthProp{
1979 message.MakeAuthProp("smtp", "mailfrom", c.mailFrom.XString(c.smtputf8), true, c.mailFrom.ASCIIExtra(c.smtputf8)),
1984 msgPrefix = append(msgPrefix, []byte(authResults.Header())...)
1986 // We always deliver through the queue. It would be more efficient to deliver
1987 // directly, but we don't want to circumvent all the anti-spam measures. Accounts
1988 // on a single mox instance should be allowed to block each other.
1990 qml := make([]queue.Msg, len(c.recipients))
1991 for i, rcptAcc := range c.recipients {
1993 code, timeout := localserveNeedsError(rcptAcc.rcptTo.Localpart)
1995 c.log.Info("timing out submission due to special localpart")
1996 mox.Sleep(mox.Context, time.Hour)
1997 xsmtpServerErrorf(codes{smtp.C451LocalErr, smtp.SeSys3Other0}, "timing out submission due to special localpart")
1998 } else if code != 0 {
1999 c.log.Info("failure due to special localpart", slog.Int("code", code))
2000 xsmtpServerErrorf(codes{code, smtp.SeOther00}, "failure with code %d due to special localpart", code)
2004 // For multiple recipients, we don't make each message prefix unique, leaving out
2005 // the "for" clause in the Received header. This allows the queue to deliver the
2006 // messages in a single smtp transaction.
2008 if len(c.recipients) == 1 {
2009 rcptTo = rcptAcc.rcptTo.String()
2011 xmsgPrefix := append([]byte(recvHdrFor(rcptTo)), msgPrefix...)
2012 msgSize := int64(len(xmsgPrefix)) + msgWriter.Size
2013 qm := queue.MakeMsg(*c.mailFrom, rcptAcc.rcptTo, msgWriter.Has8bit, c.smtputf8, msgSize, messageID, xmsgPrefix, c.requireTLS, now)
2014 if !c.futureRelease.IsZero() {
2015 qm.NextAttempt = c.futureRelease
2016 qm.FutureReleaseRequest = c.futureReleaseRequest
2021 // todo: it would be good to have a limit on messages (count and total size) a user has in the queue. also/especially with futurerelease.
../rfc/4865:387
2022 if err := queue.Add(ctx, c.log, c.account.Name, dataFile, qml...); err != nil {
2023 // Aborting the transaction is not great. But continuing and generating DSNs will
2024 // probably result in errors as well...
2025 metricSubmission.WithLabelValues("queueerror").Inc()
2026 c.log.Errorx("queuing message", err)
2027 xsmtpServerErrorf(errCodes(smtp.C451LocalErr, smtp.SeSys3Other0, err), "error delivering message: %v", err)
2029 metricSubmission.WithLabelValues("ok").Inc()
2030 for i, rcptAcc := range c.recipients {
2031 c.log.Info("messages queued for delivery",
2032 slog.Any("mailfrom", *c.mailFrom),
2033 slog.Any("rcptto", rcptAcc.rcptTo),
2034 slog.Bool("smtputf8", c.smtputf8),
2035 slog.Int64("msgsize", qml[i].Size))
2038 err = c.account.DB.Write(ctx, func(tx *bstore.Tx) error {
2039 for _, rcptAcc := range c.recipients {
2040 outgoing := store.Outgoing{Recipient: rcptAcc.rcptTo.XString(true)}
2041 if err := tx.Insert(&outgoing); err != nil {
2042 return fmt.Errorf("adding outgoing message: %v", err)
2047 xcheckf(err, "adding outgoing messages")
2050 c.transactionBad-- // Compensate for early earlier pessimistic increase.
2053 c.writecodeline(smtp.C250Completed, smtp.SeMailbox2Other0, "it is done", nil)
2056func ipmasked(ip net.IP) (string, string, string) {
2057 if ip.To4() != nil {
2059 m2 := ip.Mask(net.CIDRMask(26, 32)).String()
2060 m3 := ip.Mask(net.CIDRMask(21, 32)).String()
2063 m1 := ip.Mask(net.CIDRMask(64, 128)).String()
2064 m2 := ip.Mask(net.CIDRMask(48, 128)).String()
2065 m3 := ip.Mask(net.CIDRMask(32, 128)).String()
2069func localserveNeedsError(lp smtp.Localpart) (code int, timeout bool) {
2071 if strings.HasSuffix(s, "temperror") {
2072 return smtp.C451LocalErr, false
2073 } else if strings.HasSuffix(s, "permerror") {
2074 return smtp.C550MailboxUnavail, false
2075 } else if strings.HasSuffix(s, "timeout") {
2082 v, err := strconv.ParseInt(s, 10, 32)
2086 if v < 400 || v > 600 {
2089 return int(v), false
2092func (c *conn) xlocalserveError(lp smtp.Localpart) {
2093 code, timeout := localserveNeedsError(lp)
2095 c.log.Info("timing out due to special localpart")
2096 mox.Sleep(mox.Context, time.Hour)
2097 xsmtpServerErrorf(codes{smtp.C451LocalErr, smtp.SeSys3Other0}, "timing out command due to special localpart")
2098 } else if code != 0 {
2099 c.log.Info("failure due to special localpart", slog.Int("code", code))
2100 metricDelivery.WithLabelValues("delivererror", "localserve").Inc()
2101 xsmtpServerErrorf(codes{code, smtp.SeOther00}, "failure with code %d due to special localpart", code)
2105// deliver is called for incoming messages from external, typically untrusted
2106// sources. i.e. not submitted by authenticated users.
2107func (c *conn) deliver(ctx context.Context, recvHdrFor func(string) string, msgWriter *message.Writer, iprevStatus iprev.Status, iprevAuthentic bool, dataFile *os.File) {
2108 // 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.
2110 msgFrom, envelope, headers, err := message.From(c.log.Logger, false, dataFile)
2112 c.log.Infox("parsing message for From address", err)
2116 if len(headers.Values("Received")) > 100 {
2117 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeNet4Loop6, "loop detected, more than 100 Received headers")
2120 // TLS-Required: No header makes us not enforce recipient domain's TLS policy.
2121 // Since we only deliver locally at the moment, this won't influence our behaviour.
2122 // Once we forward, it would our delivery attempts.
2125 if c.requireTLS == nil && hasTLSRequiredNo(headers) {
2130 // We'll be building up an Authentication-Results header.
2131 authResults := message.AuthResults{
2132 Hostname: mox.Conf.Static.HostnameDomain.XName(c.smtputf8),
2135 commentAuthentic := func(v bool) string {
2137 return "with dnssec"
2139 return "without dnssec"
2142 // Reverse IP lookup results.
2143 // todo future: how useful is this?
2145 authResults.Methods = append(authResults.Methods, message.AuthMethod{
2147 Result: string(iprevStatus),
2148 Comment: commentAuthentic(iprevAuthentic),
2149 Props: []message.AuthProp{
2150 message.MakeAuthProp("policy", "iprev", c.remoteIP.String(), false, ""),
2154 // SPF and DKIM verification in parallel.
2155 var wg sync.WaitGroup
2159 var dkimResults []dkim.Result
2163 x := recover() // Should not happen, but don't take program down if it does.
2165 c.log.Error("dkim verify panic", slog.Any("err", x))
2167 metrics.PanicInc(metrics.Dkimverify)
2171 // We always evaluate all signatures. We want to build up reputation for each
2172 // domain in the signature.
2173 const ignoreTestMode = false
2174 // todo future: longer timeout? we have to read through the entire email, which can be large, possibly multiple times.
2175 dkimctx, dkimcancel := context.WithTimeout(ctx, time.Minute)
2177 // todo future: we could let user configure which dkim headers they require
2178 dkimResults, dkimErr = dkim.Verify(dkimctx, c.log.Logger, c.resolver, c.smtputf8, dkim.DefaultPolicy, dataFile, ignoreTestMode)
2184 var receivedSPF spf.Received
2185 var spfDomain dns.Domain
2187 var spfAuthentic bool
2189 spfArgs := spf.Args{
2190 RemoteIP: c.remoteIP,
2191 MailFromLocalpart: c.mailFrom.Localpart,
2192 MailFromDomain: c.mailFrom.IPDomain.Domain, // Can be empty.
2193 HelloDomain: c.hello,
2195 LocalHostname: c.hostname,
2200 x := recover() // Should not happen, but don't take program down if it does.
2202 c.log.Error("spf verify panic", slog.Any("err", x))
2204 metrics.PanicInc(metrics.Spfverify)
2208 spfctx, spfcancel := context.WithTimeout(ctx, time.Minute)
2210 receivedSPF, spfDomain, spfExpl, spfAuthentic, spfErr = spf.Verify(spfctx, c.log.Logger, c.resolver, spfArgs)
2213 c.log.Infox("spf verify", spfErr)
2217 // Wait for DKIM and SPF validation to finish.
2220 // Give immediate response if all recipients are unknown.
2222 for _, r := range c.recipients {
2227 if nunknown == len(c.recipients) {
2228 // During RCPT TO we found that the address does not exist.
2229 c.log.Info("deliver attempt to unknown user(s)", slog.Any("recipients", c.recipients))
2231 // Crude attempt to slow down someone trying to guess names. Would work better
2232 // with connection rate limiter.
2233 if unknownRecipientsDelay > 0 {
2234 mox.Sleep(ctx, unknownRecipientsDelay)
2237 // 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.
2238 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeAddr1UnknownDestMailbox1, "no such user(s)")
2241 // Add DKIM results to Authentication-Results header.
2242 authResAddDKIM := func(result, comment, reason string, props []message.AuthProp) {
2243 dm := message.AuthMethod{
2250 authResults.Methods = append(authResults.Methods, dm)
2253 c.log.Errorx("dkim verify", dkimErr)
2254 authResAddDKIM("none", "", dkimErr.Error(), nil)
2255 } else if len(dkimResults) == 0 {
2256 c.log.Info("no dkim-signature header", slog.Any("mailfrom", c.mailFrom))
2257 authResAddDKIM("none", "", "no dkim signatures", nil)
2259 for i, r := range dkimResults {
2260 var domain, selector dns.Domain
2261 var identity *dkim.Identity
2263 var props []message.AuthProp
2265 if r.Record != nil && r.Record.PublicKey != nil {
2266 if pubkey, ok := r.Record.PublicKey.(*rsa.PublicKey); ok {
2267 comment = fmt.Sprintf("%d bit rsa, ", pubkey.N.BitLen())
2271 sig := base64.StdEncoding.EncodeToString(r.Sig.Signature)
2272 sig = sig[:12] // Must be at least 8 characters and unique among the signatures.
2273 props = []message.AuthProp{
2274 message.MakeAuthProp("header", "d", r.Sig.Domain.XName(c.smtputf8), true, r.Sig.Domain.ASCIIExtra(c.smtputf8)),
2275 message.MakeAuthProp("header", "s", r.Sig.Selector.XName(c.smtputf8), true, r.Sig.Selector.ASCIIExtra(c.smtputf8)),
2276 message.MakeAuthProp("header", "a", r.Sig.Algorithm(), false, ""),
2279 domain = r.Sig.Domain
2280 selector = r.Sig.Selector
2281 if r.Sig.Identity != nil {
2282 props = append(props, message.MakeAuthProp("header", "i", r.Sig.Identity.String(), true, ""))
2283 identity = r.Sig.Identity
2285 if r.RecordAuthentic {
2286 comment += "with dnssec"
2288 comment += "without dnssec"
2293 errmsg = r.Err.Error()
2295 authResAddDKIM(string(r.Status), comment, errmsg, props)
2296 c.log.Debugx("dkim verification result", r.Err,
2297 slog.Int("index", i),
2298 slog.Any("mailfrom", c.mailFrom),
2299 slog.Any("status", r.Status),
2300 slog.Any("domain", domain),
2301 slog.Any("selector", selector),
2302 slog.Any("identity", identity))
2306 var spfIdentity *dns.Domain
2307 var mailFromValidation = store.ValidationUnknown
2308 var ehloValidation = store.ValidationUnknown
2309 switch receivedSPF.Identity {
2310 case spf.ReceivedHELO:
2311 if len(spfArgs.HelloDomain.IP) == 0 {
2312 spfIdentity = &spfArgs.HelloDomain.Domain
2314 ehloValidation = store.SPFValidation(receivedSPF.Result)
2315 case spf.ReceivedMailFrom:
2316 spfIdentity = &spfArgs.MailFromDomain
2317 mailFromValidation = store.SPFValidation(receivedSPF.Result)
2319 var props []message.AuthProp
2320 if spfIdentity != nil {
2321 props = []message.AuthProp{message.MakeAuthProp("smtp", string(receivedSPF.Identity), spfIdentity.XName(c.smtputf8), true, spfIdentity.ASCIIExtra(c.smtputf8))}
2323 var spfComment string
2325 spfComment = "with dnssec"
2327 spfComment = "without dnssec"
2329 authResults.Methods = append(authResults.Methods, message.AuthMethod{
2331 Result: string(receivedSPF.Result),
2332 Comment: spfComment,
2335 switch receivedSPF.Result {
2336 case spf.StatusPass:
2337 c.log.Debug("spf pass", slog.Any("ip", spfArgs.RemoteIP), slog.String("mailfromdomain", spfArgs.MailFromDomain.ASCII)) // todo: log the domain that was actually verified.
2338 case spf.StatusFail:
2341 for _, b := range []byte(spfExpl) {
2342 if b < ' ' || b >= 0x7f {
2348 if len(spfExpl) > 800 {
2349 spfExpl = spfExpl[:797] + "..."
2351 spfExpl = "remote claims: " + spfExpl
2355 spfExpl = fmt.Sprintf("your ip %s is not on the SPF allowlist for domain %s", spfArgs.RemoteIP, spfDomain.ASCII)
2357 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?
2358 case spf.StatusTemperror:
2359 c.log.Infox("spf temperror", spfErr)
2360 case spf.StatusPermerror:
2361 c.log.Infox("spf permerror", spfErr)
2362 case spf.StatusNone, spf.StatusNeutral, spf.StatusSoftfail:
2364 c.log.Error("unknown spf status, treating as None/Neutral", slog.Any("status", receivedSPF.Result))
2365 receivedSPF.Result = spf.StatusNone
2370 var dmarcResult dmarc.Result
2371 const applyRandomPercentage = true
2372 // dmarcMethod is added to authResults when delivering to recipients: accounts can
2373 // have different policy override rules.
2374 var dmarcMethod message.AuthMethod
2375 var msgFromValidation = store.ValidationNone
2376 if msgFrom.IsZero() {
2377 dmarcResult.Status = dmarc.StatusNone
2378 dmarcMethod = message.AuthMethod{
2380 Result: string(dmarcResult.Status),
2383 msgFromValidation = alignment(ctx, c.log, msgFrom.Domain, dkimResults, receivedSPF.Result, spfIdentity)
2385 // We are doing the DMARC evaluation now. But we only store it for inclusion in an
2386 // aggregate report when we actually use it. We use an evaluation for each
2387 // recipient, with each a potentially different result due to mailing
2388 // list/forwarding configuration. If we reject a message due to being spam, we
2389 // don't want to spend any resources for the sender domain, and we don't want to
2390 // give the sender any more information about us, so we won't record the
2392 // 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.
2394 dmarcctx, dmarccancel := context.WithTimeout(ctx, time.Minute)
2396 dmarcUse, dmarcResult = dmarc.Verify(dmarcctx, c.log.Logger, c.resolver, msgFrom.Domain, dkimResults, receivedSPF.Result, spfIdentity, applyRandomPercentage)
2399 if dmarcResult.RecordAuthentic {
2400 comment = "with dnssec"
2402 comment = "without dnssec"
2404 dmarcMethod = message.AuthMethod{
2406 Result: string(dmarcResult.Status),
2408 Props: []message.AuthProp{
2410 message.MakeAuthProp("header", "from", msgFrom.Domain.ASCII, true, msgFrom.Domain.ASCIIExtra(c.smtputf8)),
2414 if dmarcResult.Status == dmarc.StatusPass && msgFromValidation == store.ValidationRelaxed {
2415 msgFromValidation = store.ValidationDMARC
2418 // todo future: consider enforcing an spf (soft)fail if there is no dmarc policy or the dmarc policy is none.
../rfc/7489:1507
2420 c.log.Debug("dmarc verification", slog.Any("result", dmarcResult.Status), slog.Any("domain", msgFrom.Domain))
2422 // Prepare for analyzing content, calculating reputation.
2423 ipmasked1, ipmasked2, ipmasked3 := ipmasked(c.remoteIP)
2424 var verifiedDKIMDomains []string
2425 dkimSeen := map[string]bool{}
2426 for _, r := range dkimResults {
2427 // A message can have multiple signatures for the same identity. For example when
2428 // signing the message multiple times with different algorithms (rsa and ed25519).
2429 if r.Status != dkim.StatusPass {
2432 d := r.Sig.Domain.Name()
2435 verifiedDKIMDomains = append(verifiedDKIMDomains, d)
2439 // When we deliver, we try to remove from rejects mailbox based on message-id.
2440 // We'll parse it when we need it, but it is the same for each recipient.
2441 var messageID string
2442 var parsedMessageID bool
2444 // We build up a DSN for each failed recipient. If we have recipients in dsnMsg
2445 // after processing, we queue the DSN. Unless all recipients failed, in which case
2446 // we may just fail the mail transaction instead (could be common for failure to
2447 // deliver to a single recipient, e.g. for junk mail).
2449 type deliverError struct {
2456 var deliverErrors []deliverError
2457 addError := func(rcptAcc rcptAccount, code int, secode string, userError bool, errmsg string) {
2458 e := deliverError{rcptAcc.rcptTo, code, secode, userError, errmsg}
2459 c.log.Info("deliver error",
2460 slog.Any("rcptto", e.rcptTo),
2461 slog.Int("code", code),
2462 slog.String("secode", "secode"),
2463 slog.Bool("usererror", userError),
2464 slog.String("errmsg", errmsg))
2465 deliverErrors = append(deliverErrors, e)
2468 // For each recipient, do final spam analysis and delivery.
2469 for _, rcptAcc := range c.recipients {
2470 log := c.log.With(slog.Any("mailfrom", c.mailFrom), slog.Any("rcptto", rcptAcc.rcptTo))
2472 // If this is not a valid local user, we send back a DSN. This can only happen when
2473 // there are also valid recipients, and only when remote is SPF-verified, so the DSN
2474 // should not cause backscatter.
2475 // In case of serious errors, we abort the transaction. We may have already
2476 // delivered some messages. Perhaps it would be better to continue with other
2477 // deliveries, and return an error at the end? Though the failure conditions will
2478 // probably prevent any other successful deliveries too...
2481 metricDelivery.WithLabelValues("unknownuser", "").Inc()
2482 addError(rcptAcc, smtp.C550MailboxUnavail, smtp.SeAddr1UnknownDestMailbox1, true, "no such user")
2486 acc, err := store.OpenAccount(log, rcptAcc.accountName)
2488 log.Errorx("open account", err, slog.Any("account", rcptAcc.accountName))
2489 metricDelivery.WithLabelValues("accounterror", "").Inc()
2490 addError(rcptAcc, smtp.C451LocalErr, smtp.SeSys3Other0, false, "error processing")
2496 log.Check(err, "closing account after delivery")
2500 // We don't want to let a single IP or network deliver too many messages to an
2501 // account. They may fill up the mailbox, either with messages that have to be
2502 // purged, or by filling the disk. We check both cases for IP's and networks.
2503 var rateError bool // Whether returned error represents a rate error.
2504 err = acc.DB.Read(ctx, func(tx *bstore.Tx) (retErr error) {
2507 log.Debugx("checking message and size delivery rates", retErr, slog.Duration("duration", time.Since(now)))
2510 checkCount := func(msg store.Message, window time.Duration, limit int) {
2514 q := bstore.QueryTx[store.Message](tx)
2515 q.FilterNonzero(msg)
2516 q.FilterGreater("Received", now.Add(-window))
2517 q.FilterEqual("Expunged", false)
2525 retErr = fmt.Errorf("more than %d messages in past %s from your ip/network", limit, window)
2529 checkSize := func(msg store.Message, window time.Duration, limit int64) {
2533 q := bstore.QueryTx[store.Message](tx)
2534 q.FilterNonzero(msg)
2535 q.FilterGreater("Received", now.Add(-window))
2536 q.FilterEqual("Expunged", false)
2537 size := msgWriter.Size
2538 err := q.ForEach(func(v store.Message) error {
2548 retErr = fmt.Errorf("more than %d bytes in past %s from your ip/network", limit, window)
2552 // todo future: make these configurable
2553 // todo: should we have a limit for forwarded messages? they are stored with empty RemoteIPMasked*
2555 const day = 24 * time.Hour
2556 checkCount(store.Message{RemoteIPMasked1: ipmasked1}, time.Minute, limitIPMasked1MessagesPerMinute)
2557 checkCount(store.Message{RemoteIPMasked1: ipmasked1}, day, 20*500)
2558 checkCount(store.Message{RemoteIPMasked2: ipmasked2}, time.Minute, 1500)
2559 checkCount(store.Message{RemoteIPMasked2: ipmasked2}, day, 20*1500)
2560 checkCount(store.Message{RemoteIPMasked3: ipmasked3}, time.Minute, 4500)
2561 checkCount(store.Message{RemoteIPMasked3: ipmasked3}, day, 20*4500)
2563 const MB = 1024 * 1024
2564 checkSize(store.Message{RemoteIPMasked1: ipmasked1}, time.Minute, limitIPMasked1SizePerMinute)
2565 checkSize(store.Message{RemoteIPMasked1: ipmasked1}, day, 3*1000*MB)
2566 checkSize(store.Message{RemoteIPMasked2: ipmasked2}, time.Minute, 3000*MB)
2567 checkSize(store.Message{RemoteIPMasked2: ipmasked2}, day, 3*3000*MB)
2568 checkSize(store.Message{RemoteIPMasked3: ipmasked3}, time.Minute, 9000*MB)
2569 checkSize(store.Message{RemoteIPMasked3: ipmasked3}, day, 3*9000*MB)
2573 if err != nil && !rateError {
2574 log.Errorx("checking delivery rates", err)
2575 metricDelivery.WithLabelValues("checkrates", "").Inc()
2576 addError(rcptAcc, smtp.C451LocalErr, smtp.SeSys3Other0, false, "error processing")
2578 } else if err != nil {
2579 log.Debugx("refusing due to high delivery rate", err)
2580 metricDelivery.WithLabelValues("highrate", "").Inc()
2582 addError(rcptAcc, smtp.C452StorageFull, smtp.SeMailbox2Full2, true, err.Error())
2587 Received: time.Now(),
2588 RemoteIP: c.remoteIP.String(),
2589 RemoteIPMasked1: ipmasked1,
2590 RemoteIPMasked2: ipmasked2,
2591 RemoteIPMasked3: ipmasked3,
2592 EHLODomain: c.hello.Domain.Name(),
2593 MailFrom: c.mailFrom.String(),
2594 MailFromLocalpart: c.mailFrom.Localpart,
2595 MailFromDomain: c.mailFrom.IPDomain.Domain.Name(),
2596 RcptToLocalpart: rcptAcc.rcptTo.Localpart,
2597 RcptToDomain: rcptAcc.rcptTo.IPDomain.Domain.Name(),
2598 MsgFromLocalpart: msgFrom.Localpart,
2599 MsgFromDomain: msgFrom.Domain.Name(),
2600 MsgFromOrgDomain: publicsuffix.Lookup(ctx, log.Logger, msgFrom.Domain).Name(),
2601 EHLOValidated: ehloValidation == store.ValidationPass,
2602 MailFromValidated: mailFromValidation == store.ValidationPass,
2603 MsgFromValidated: msgFromValidation == store.ValidationStrict || msgFromValidation == store.ValidationDMARC || msgFromValidation == store.ValidationRelaxed,
2604 EHLOValidation: ehloValidation,
2605 MailFromValidation: mailFromValidation,
2606 MsgFromValidation: msgFromValidation,
2607 DKIMDomains: verifiedDKIMDomains,
2608 Size: msgWriter.Size,
2611 tlsState := c.conn.(*tls.Conn).ConnectionState()
2612 m.ReceivedTLSVersion = tlsState.Version
2613 m.ReceivedTLSCipherSuite = tlsState.CipherSuite
2614 if c.requireTLS != nil {
2615 m.ReceivedRequireTLS = *c.requireTLS
2618 m.ReceivedTLSVersion = 1 // Signals plain text delivery.
2621 var msgTo, msgCc []message.Address
2622 if envelope != nil {
2626 d := delivery{c.tls, &m, dataFile, rcptAcc, acc, msgTo, msgCc, msgFrom, c.dnsBLs, dmarcUse, dmarcResult, dkimResults, iprevStatus}
2627 a := analyze(ctx, log, c.resolver, d)
2629 // Any DMARC result override is stored in the evaluation for outgoing DMARC
2630 // aggregate reports, and added to the Authentication-Results message header.
2631 // We want to tell the sender that we have an override, e.g. for mailing lists, so
2632 // they don't overestimate the potential damage of switching from p=none to
2634 var dmarcOverrides []string
2635 if a.dmarcOverrideReason != "" {
2636 dmarcOverrides = []string{a.dmarcOverrideReason}
2638 if dmarcResult.Record != nil && !dmarcUse {
2639 dmarcOverrides = append(dmarcOverrides, string(dmarcrpt.PolicyOverrideSampledOut))
2642 // Add per-recipient DMARC method to Authentication-Results. Each account can have
2643 // their own override rules, e.g. based on configured mailing lists/forwards.
2645 rcptDMARCMethod := dmarcMethod
2646 if len(dmarcOverrides) > 0 {
2647 if rcptDMARCMethod.Comment != "" {
2648 rcptDMARCMethod.Comment += ", "
2650 rcptDMARCMethod.Comment += "override " + strings.Join(dmarcOverrides, ",")
2652 rcptAuthResults := authResults
2653 rcptAuthResults.Methods = append([]message.AuthMethod{}, authResults.Methods...)
2654 rcptAuthResults.Methods = append(rcptAuthResults.Methods, rcptDMARCMethod)
2656 // Prepend reason as message header, for easy display in mail clients.
2659 xmox = "X-Mox-Reason: " + a.reason + "\r\n"
2665 m.MsgPrefix = []byte(
2669 rcptAuthResults.Header() +
2670 receivedSPF.Header() +
2671 recvHdrFor(rcptAcc.rcptTo.String()),
2673 m.Size += int64(len(m.MsgPrefix))
2675 // Store DMARC evaluation for inclusion in an aggregate report. Only if there is at
2676 // least one reporting address: We don't want to needlessly store a row in a
2677 // database for each delivery attempt. If we reject a message for being junk, we
2678 // are also not going to send it a DMARC report. The DMARC check is done early in
2679 // the analysis, we will report on rejects because of DMARC, because it could be
2680 // valuable feedback about forwarded or mailing list messages.
2682 if !mox.Conf.Static.NoOutgoingDMARCReports && dmarcResult.Record != nil && len(dmarcResult.Record.AggregateReportAddresses) > 0 && (a.accept && !m.IsReject || a.reason == reasonDMARCPolicy) {
2683 // Disposition holds our decision on whether to accept the message. Not what the
2684 // DMARC evaluation resulted in. We can override, e.g. because of mailing lists,
2685 // forwarding, or local policy.
2686 // We treat quarantine as reject, so never claim to quarantine.
2688 disposition := dmarcrpt.DispositionNone
2690 disposition = dmarcrpt.DispositionReject
2693 // unknownDomain returns whether the sender is domain with which this account has
2694 // not had positive interaction.
2695 unknownDomain := func() (unknown bool) {
2696 err := acc.DB.Read(ctx, func(tx *bstore.Tx) (err error) {
2697 // See if we received a non-junk message from this organizational domain.
2698 q := bstore.QueryTx[store.Message](tx)
2699 q.FilterNonzero(store.Message{MsgFromOrgDomain: m.MsgFromOrgDomain})
2700 q.FilterEqual("Notjunk", true)
2701 q.FilterEqual("IsReject", false)
2702 exists, err := q.Exists()
2704 return fmt.Errorf("querying for non-junk message from organizational domain: %v", err)
2710 // See if we sent a message to this organizational domain.
2711 qr := bstore.QueryTx[store.Recipient](tx)
2712 qr.FilterNonzero(store.Recipient{OrgDomain: m.MsgFromOrgDomain})
2713 exists, err = qr.Exists()
2715 return fmt.Errorf("querying for message sent to organizational domain: %v", err)
2723 log.Errorx("checking if sender is unknown domain, for dmarc aggregate report evaluation", err)
2728 r := dmarcResult.Record
2729 addresses := make([]string, len(r.AggregateReportAddresses))
2730 for i, a := range r.AggregateReportAddresses {
2731 addresses[i] = a.String()
2733 sp := dmarcrpt.Disposition(r.SubdomainPolicy)
2734 if r.SubdomainPolicy == dmarc.PolicyEmpty {
2735 sp = dmarcrpt.Disposition(r.Policy)
2737 eval := dmarcdb.Evaluation{
2738 // Evaluated and IntervalHours set by AddEvaluation.
2739 PolicyDomain: dmarcResult.Domain.Name(),
2741 // Optional evaluations don't cause a report to be sent, but will be included.
2742 // Useful for automated inter-mailer messages, we don't want to get in a reporting
2743 // loop. We also don't want to be used for sending reports to unsuspecting domains
2744 // we have no relation with.
2745 // 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.
2746 Optional: rcptAcc.destination.DMARCReports || rcptAcc.destination.HostTLSReports || rcptAcc.destination.DomainTLSReports || a.reason == reasonDMARCPolicy && unknownDomain(),
2748 Addresses: addresses,
2750 PolicyPublished: dmarcrpt.PolicyPublished{
2751 Domain: dmarcResult.Domain.Name(),
2752 ADKIM: dmarcrpt.Alignment(r.ADKIM),
2753 ASPF: dmarcrpt.Alignment(r.ASPF),
2754 Policy: dmarcrpt.Disposition(r.Policy),
2755 SubdomainPolicy: sp,
2756 Percentage: r.Percentage,
2757 // We don't save ReportingOptions, we don't do per-message failure reporting.
2759 SourceIP: c.remoteIP.String(),
2760 Disposition: disposition,
2761 AlignedDKIMPass: dmarcResult.AlignedDKIMPass,
2762 AlignedSPFPass: dmarcResult.AlignedSPFPass,
2763 EnvelopeTo: rcptAcc.rcptTo.IPDomain.String(),
2764 EnvelopeFrom: c.mailFrom.IPDomain.String(),
2765 HeaderFrom: msgFrom.Domain.Name(),
2768 for _, s := range dmarcOverrides {
2769 reason := dmarcrpt.PolicyOverrideReason{Type: dmarcrpt.PolicyOverride(s)}
2770 eval.OverrideReasons = append(eval.OverrideReasons, reason)
2773 // We'll include all signatures for the organizational domain, even if they weren't
2774 // relevant due to strict alignment requirement.
2775 for _, dkimResult := range dkimResults {
2776 if dkimResult.Sig == nil || publicsuffix.Lookup(ctx, log.Logger, msgFrom.Domain) != publicsuffix.Lookup(ctx, log.Logger, dkimResult.Sig.Domain) {
2779 r := dmarcrpt.DKIMAuthResult{
2780 Domain: dkimResult.Sig.Domain.Name(),
2781 Selector: dkimResult.Sig.Selector.ASCII,
2782 Result: dmarcrpt.DKIMResult(dkimResult.Status),
2784 eval.DKIMResults = append(eval.DKIMResults, r)
2787 switch receivedSPF.Identity {
2788 case spf.ReceivedHELO:
2789 spfAuthResult := dmarcrpt.SPFAuthResult{
2790 Domain: spfArgs.HelloDomain.String(), // Can be unicode and also IP.
2791 Scope: dmarcrpt.SPFDomainScopeHelo,
2792 Result: dmarcrpt.SPFResult(receivedSPF.Result),
2794 eval.SPFResults = []dmarcrpt.SPFAuthResult{spfAuthResult}
2795 case spf.ReceivedMailFrom:
2796 spfAuthResult := dmarcrpt.SPFAuthResult{
2797 Domain: spfArgs.MailFromDomain.Name(), // Can be unicode.
2798 Scope: dmarcrpt.SPFDomainScopeMailFrom,
2799 Result: dmarcrpt.SPFResult(receivedSPF.Result),
2801 eval.SPFResults = []dmarcrpt.SPFAuthResult{spfAuthResult}
2804 err := dmarcdb.AddEvaluation(ctx, dmarcResult.Record.AggregateReportingInterval, &eval)
2805 log.Check(err, "adding dmarc evaluation to database for aggregate report")
2809 conf, _ := acc.Conf()
2810 if conf.RejectsMailbox != "" {
2811 present, _, messagehash, err := rejectPresent(log, acc, conf.RejectsMailbox, &m, dataFile)
2813 log.Errorx("checking whether reject is already present", err)
2814 } else if !present {
2816 m.Seen = true // We don't want to draw attention.
2817 // Regular automatic junk flags configuration applies to these messages. The
2818 // default is to treat these as neutral, so they won't cause outright rejections
2819 // due to reputation for later delivery attempts.
2820 m.MessageHash = messagehash
2821 acc.WithWLock(func() {
2824 if !conf.KeepRejects {
2825 hasSpace, err = acc.TidyRejectsMailbox(c.log, conf.RejectsMailbox)
2828 log.Errorx("tidying rejects mailbox", err)
2829 } else if hasSpace {
2830 if err := acc.DeliverMailbox(log, conf.RejectsMailbox, &m, dataFile); err != nil {
2831 log.Errorx("delivering spammy mail to rejects mailbox", err)
2833 log.Info("delivered spammy mail to rejects mailbox")
2836 log.Info("not storing spammy mail to full rejects mailbox")
2840 log.Info("reject message is already present, ignoring")
2844 log.Info("incoming message rejected", slog.String("reason", a.reason), slog.Any("msgfrom", msgFrom))
2845 metricDelivery.WithLabelValues("reject", a.reason).Inc()
2847 addError(rcptAcc, a.code, a.secode, a.userError, a.errmsg)
2851 delayFirstTime := true
2852 if a.dmarcReport != nil {
2854 if err := dmarcdb.AddReport(ctx, a.dmarcReport, msgFrom.Domain); err != nil {
2855 log.Errorx("saving dmarc aggregate report in database", err)
2857 log.Info("dmarc aggregate report processed")
2859 delayFirstTime = false
2862 if a.tlsReport != nil {
2863 // todo future: add rate limiting to prevent DoS attacks.
2864 if err := tlsrptdb.AddReport(ctx, c.log, msgFrom.Domain, c.mailFrom.String(), rcptAcc.destination.HostTLSReports, a.tlsReport); err != nil {
2865 log.Errorx("saving TLSRPT report in database", err)
2867 log.Info("tlsrpt report processed")
2869 delayFirstTime = false
2873 // If this is a first-time sender and not a forwarded message, wait before actually
2874 // delivering. If this turns out to be a spammer, we've kept one of their
2875 // connections busy.
2876 if delayFirstTime && !m.IsForward && a.reason == reasonNoBadSignals && c.firstTimeSenderDelay > 0 {
2877 log.Debug("delaying before delivering from sender without reputation", slog.Duration("delay", c.firstTimeSenderDelay))
2878 mox.Sleep(mox.Context, c.firstTimeSenderDelay)
2881 // Gather the message-id before we deliver and the file may be consumed.
2882 if !parsedMessageID {
2883 if p, err := message.Parse(c.log.Logger, false, store.FileMsgReader(m.MsgPrefix, dataFile)); err != nil {
2884 log.Infox("parsing message for message-id", err)
2885 } else if header, err := p.Header(); err != nil {
2886 log.Infox("parsing message header for message-id", err)
2888 messageID = header.Get("Message-Id")
2893 code, timeout := localserveNeedsError(rcptAcc.rcptTo.Localpart)
2895 log.Info("timing out due to special localpart")
2896 mox.Sleep(mox.Context, time.Hour)
2897 xsmtpServerErrorf(codes{smtp.C451LocalErr, smtp.SeOther00}, "timing out delivery due to special localpart")
2898 } else if code != 0 {
2899 log.Info("failure due to special localpart", slog.Int("code", code))
2900 metricDelivery.WithLabelValues("delivererror", "localserve").Inc()
2901 addError(rcptAcc, code, smtp.SeOther00, false, fmt.Sprintf("failure with code %d due to special localpart", code))
2904 acc.WithWLock(func() {
2905 if err := acc.DeliverMailbox(log, a.mailbox, &m, dataFile); err != nil {
2906 log.Errorx("delivering", err)
2907 metricDelivery.WithLabelValues("delivererror", a.reason).Inc()
2908 if errors.Is(err, store.ErrOverQuota) {
2909 addError(rcptAcc, smtp.C452StorageFull, smtp.SeMailbox2Full2, true, "account storage full")
2911 addError(rcptAcc, smtp.C451LocalErr, smtp.SeSys3Other0, false, "error processing")
2915 metricDelivery.WithLabelValues("delivered", a.reason).Inc()
2916 log.Info("incoming message delivered", slog.String("reason", a.reason), slog.Any("msgfrom", msgFrom))
2918 conf, _ := acc.Conf()
2919 if conf.RejectsMailbox != "" && m.MessageID != "" {
2920 if err := acc.RejectsRemove(log, conf.RejectsMailbox, m.MessageID); err != nil {
2921 log.Errorx("removing message from rejects mailbox", err, slog.String("messageid", messageID))
2927 log.Check(err, "closing account after delivering")
2931 // If all recipients failed to deliver, return an error.
2932 if len(c.recipients) == len(deliverErrors) {
2934 e0 := deliverErrors[0]
2935 var serverError bool
2938 for _, e := range deliverErrors {
2939 serverError = serverError || !e.userError
2940 if e.code != e0.code || e.secode != e0.secode {
2943 msgs = append(msgs, e.errmsg)
2949 xsmtpErrorf(e0.code, e0.secode, !serverError, "%s", strings.Join(msgs, "\n"))
2952 // Not all failures had the same error. We'll return each error on a separate line.
2954 for _, e := range deliverErrors {
2955 s := fmt.Sprintf("%d %d.%s %s", e.code, e.code/100, e.secode, e.errmsg)
2956 lines = append(lines, s)
2958 code := smtp.C451LocalErr
2959 secode := smtp.SeSys3Other0
2961 code = smtp.C554TransactionFailed
2963 lines = append(lines, "multiple errors")
2964 xsmtpErrorf(code, secode, !serverError, strings.Join(lines, "\n"))
2966 // Generate one DSN for all failed recipients.
2967 if len(deliverErrors) > 0 {
2969 dsnMsg := dsn.Message{
2970 SMTPUTF8: c.smtputf8,
2971 From: smtp.Path{Localpart: "postmaster", IPDomain: deliverErrors[0].rcptTo.IPDomain},
2973 Subject: "mail delivery failure",
2974 MessageID: mox.MessageIDGen(false),
2975 References: messageID,
2977 // Per-message details.
2978 ReportingMTA: mox.Conf.Static.HostnameDomain.ASCII,
2979 ReceivedFromMTA: smtp.Ehlo{Name: c.hello, ConnIP: c.remoteIP},
2983 if len(deliverErrors) > 1 {
2984 dsnMsg.TextBody = "Multiple delivery failures occurred.\n\n"
2987 for _, e := range deliverErrors {
2989 if e.code/100 == 4 {
2992 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))
2993 rcpt := dsn.Recipient{
2994 FinalRecipient: e.rcptTo,
2996 Status: fmt.Sprintf("%d.%s", e.code/100, e.secode),
2997 LastAttemptDate: now,
2999 dsnMsg.Recipients = append(dsnMsg.Recipients, rcpt)
3002 header, err := message.ReadHeaders(bufio.NewReader(&moxio.AtReader{R: dataFile}))
3004 c.log.Errorx("reading headers of incoming message for dsn, continuing dsn without headers", err)
3006 dsnMsg.Original = header
3009 c.log.Error("not queueing dsn for incoming delivery due to localserve")
3010 } else if err := queueDSN(context.TODO(), c.log, c, *c.mailFrom, dsnMsg, c.requireTLS != nil && *c.requireTLS); err != nil {
3011 metricServerErrors.WithLabelValues("queuedsn").Inc()
3012 c.log.Errorx("queuing DSN for incoming delivery, no DSN sent", err)
3017 c.transactionBad-- // Compensate for early earlier pessimistic increase.
3019 c.writecodeline(smtp.C250Completed, smtp.SeMailbox2Other0, "it is done", nil)
3022// ecode returns either ecode, or a more specific error based on err.
3023// For example, ecode can be turned from an "other system" error into a "mail
3024// system full" if the error indicates no disk space is available.
3025func errCodes(code int, ecode string, err error) codes {
3027 case moxio.IsStorageSpace(err):
3029 case smtp.SeMailbox2Other0:
3030 if code == smtp.C451LocalErr {
3031 code = smtp.C452StorageFull
3033 ecode = smtp.SeMailbox2Full2
3034 case smtp.SeSys3Other0:
3035 if code == smtp.C451LocalErr {
3036 code = smtp.C452StorageFull
3038 ecode = smtp.SeSys3StorageFull1
3041 return codes{code, ecode}
3045func (c *conn) cmdRset(p *parser) {
3050 c.bwritecodeline(smtp.C250Completed, smtp.SeOther00, "all clear", nil)
3054func (c *conn) cmdVrfy(p *parser) {
3055 // No EHLO/HELO needed.
3066 // todo future: we could support vrfy and expn for submission? though would need to see if its rfc defines it.
3069 xsmtpUserErrorf(smtp.C252WithoutVrfy, smtp.SePol7Other0, "no verify but will try delivery")
3073func (c *conn) cmdExpn(p *parser) {
3074 // No EHLO/HELO needed.
3086 xsmtpUserErrorf(smtp.C252WithoutVrfy, smtp.SePol7Other0, "no expand but will try delivery")
3090func (c *conn) cmdHelp(p *parser) {
3091 // Let's not strictly parse the request for help. We are ignoring the text anyway.
3094 c.bwritecodeline(smtp.C214Help, smtp.SeOther00, "see rfc 5321 (smtp)", nil)
3098func (c *conn) cmdNoop(p *parser) {
3099 // No idea why, but if an argument follows, it must adhere to the string ABNF production...
3106 c.bwritecodeline(smtp.C250Completed, smtp.SeOther00, "alrighty", nil)
3110func (c *conn) cmdQuit(p *parser) {
3114 c.writecodeline(smtp.C221Closing, smtp.SeOther00, "okay thanks bye", nil)