1// Package smtpserver implements an SMTP server for submission and incoming delivery of mail messages.
10 cryptorand "crypto/rand"
33 "golang.org/x/exp/maps"
34 "golang.org/x/text/unicode/norm"
36 "github.com/prometheus/client_golang/prometheus"
37 "github.com/prometheus/client_golang/prometheus/promauto"
39 "github.com/mjl-/bstore"
41 "github.com/mjl-/mox/config"
42 "github.com/mjl-/mox/dkim"
43 "github.com/mjl-/mox/dmarc"
44 "github.com/mjl-/mox/dmarcdb"
45 "github.com/mjl-/mox/dmarcrpt"
46 "github.com/mjl-/mox/dns"
47 "github.com/mjl-/mox/dsn"
48 "github.com/mjl-/mox/iprev"
49 "github.com/mjl-/mox/message"
50 "github.com/mjl-/mox/metrics"
51 "github.com/mjl-/mox/mlog"
52 "github.com/mjl-/mox/mox-"
53 "github.com/mjl-/mox/moxio"
54 "github.com/mjl-/mox/moxvar"
55 "github.com/mjl-/mox/publicsuffix"
56 "github.com/mjl-/mox/queue"
57 "github.com/mjl-/mox/ratelimit"
58 "github.com/mjl-/mox/scram"
59 "github.com/mjl-/mox/smtp"
60 "github.com/mjl-/mox/spf"
61 "github.com/mjl-/mox/store"
62 "github.com/mjl-/mox/tlsrptdb"
65// We use panic and recover for error handling while executing commands.
66// These errors signal the connection must be closed.
67var errIO = errors.New("io error")
69// If set, regular delivery/submit is sidestepped, email is accepted and
70// delivered to the account named mox.
73var limiterConnectionRate, limiterConnections *ratelimit.Limiter
75// For delivery rate limiting. Variable because changed during tests.
76var limitIPMasked1MessagesPerMinute int = 500
77var limitIPMasked1SizePerMinute int64 = 1000 * 1024 * 1024
79// Maximum number of RCPT TO commands (i.e. recipients) for a single message
80// delivery. Must be at least 100. Announced in LIMIT extension.
81const rcptToLimit = 1000
84 // Also called by tests, so they don't trigger the rate limiter.
90 // todo future: make these configurable
91 limiterConnectionRate = &ratelimit.Limiter{
92 WindowLimits: []ratelimit.WindowLimit{
95 Limits: [...]int64{300, 900, 2700},
99 limiterConnections = &ratelimit.Limiter{
100 WindowLimits: []ratelimit.WindowLimit{
102 Window: time.Duration(math.MaxInt64), // All of time.
103 Limits: [...]int64{30, 90, 270},
110 // Delays for bad/suspicious behaviour. Zero during tests.
111 badClientDelay = time.Second // Before reads and after 1-byte writes for probably spammers.
112 authFailDelay = time.Second // Response to authentication failure.
113 unknownRecipientsDelay = 5 * time.Second // Response when all recipients are unknown.
114 firstTimeSenderDelayDefault = 15 * time.Second // Before accepting message from first-time sender.
119 secode string // Enhanced code, but without the leading major int from code.
123 metricConnection = promauto.NewCounterVec(
124 prometheus.CounterOpts{
125 Name: "mox_smtpserver_connection_total",
126 Help: "Incoming SMTP connections.",
129 "kind", // "deliver" or "submit"
132 metricCommands = promauto.NewHistogramVec(
133 prometheus.HistogramOpts{
134 Name: "mox_smtpserver_command_duration_seconds",
135 Help: "SMTP server command duration and result codes in seconds.",
136 Buckets: []float64{0.001, 0.005, 0.01, 0.05, 0.100, 0.5, 1, 5, 10, 20, 30, 60, 120},
139 "kind", // "deliver" or "submit"
145 metricDelivery = promauto.NewCounterVec(
146 prometheus.CounterOpts{
147 Name: "mox_smtpserver_delivery_total",
148 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.",
155 // Similar between ../webmail/webmail.go:/metricSubmission and ../smtpserver/server.go:/metricSubmission and ../webapisrv/server.go:/metricSubmission
156 metricSubmission = promauto.NewCounterVec(
157 prometheus.CounterOpts{
158 Name: "mox_smtpserver_submission_total",
159 Help: "SMTP server incoming submission results, known values (those ending with error are server errors): ok, badmessage, badfrom, badheader, messagelimiterror, recipientlimiterror, localserveerror, queueerror.",
165 metricServerErrors = promauto.NewCounterVec(
166 prometheus.CounterOpts{
167 Name: "mox_smtpserver_errors_total",
168 Help: "SMTP server errors, known values: dkimsign, queuedsn.",
176var jitterRand = mox.NewPseudoRand()
178func durationDefault(delay *time.Duration, def time.Duration) time.Duration {
185// Listen initializes network listeners for incoming SMTP connection.
186// The listeners are stored for a later call to Serve.
188 names := maps.Keys(mox.Conf.Static.Listeners)
190 for _, name := range names {
191 listener := mox.Conf.Static.Listeners[name]
193 var tlsConfig *tls.Config
194 if listener.TLS != nil {
195 tlsConfig = listener.TLS.Config
198 maxMsgSize := listener.SMTPMaxMessageSize
200 maxMsgSize = config.DefaultMaxMsgSize
203 if listener.SMTP.Enabled {
204 hostname := mox.Conf.Static.HostnameDomain
205 if listener.Hostname != "" {
206 hostname = listener.HostnameDomain
208 port := config.Port(listener.SMTP.Port, 25)
209 for _, ip := range listener.IPs {
210 firstTimeSenderDelay := durationDefault(listener.SMTP.FirstTimeSenderDelay, firstTimeSenderDelayDefault)
211 listen1("smtp", name, ip, port, hostname, tlsConfig, false, false, maxMsgSize, false, listener.SMTP.RequireSTARTTLS, !listener.SMTP.NoRequireTLS, listener.SMTP.DNSBLZones, firstTimeSenderDelay)
214 if listener.Submission.Enabled {
215 hostname := mox.Conf.Static.HostnameDomain
216 if listener.Hostname != "" {
217 hostname = listener.HostnameDomain
219 port := config.Port(listener.Submission.Port, 587)
220 for _, ip := range listener.IPs {
221 listen1("submission", name, ip, port, hostname, tlsConfig, true, false, maxMsgSize, !listener.Submission.NoRequireSTARTTLS, !listener.Submission.NoRequireSTARTTLS, true, nil, 0)
225 if listener.Submissions.Enabled {
226 hostname := mox.Conf.Static.HostnameDomain
227 if listener.Hostname != "" {
228 hostname = listener.HostnameDomain
230 port := config.Port(listener.Submissions.Port, 465)
231 for _, ip := range listener.IPs {
232 listen1("submissions", name, ip, port, hostname, tlsConfig, true, true, maxMsgSize, true, true, true, nil, 0)
240func 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) {
241 log := mlog.New("smtpserver", nil)
242 addr := net.JoinHostPort(ip, fmt.Sprintf("%d", port))
243 if os.Getuid() == 0 {
244 log.Print("listening for smtp",
245 slog.String("listener", name),
246 slog.String("address", addr),
247 slog.String("protocol", protocol))
249 network := mox.Network(ip)
250 ln, err := mox.Listen(network, addr)
252 log.Fatalx("smtp: listen for smtp", err, slog.String("protocol", protocol), slog.String("listener", name))
255 ln = tls.NewListener(ln, tlsConfig)
260 conn, err := ln.Accept()
262 log.Infox("smtp: accept", err, slog.String("protocol", protocol), slog.String("listener", name))
266 // Package is set on the resolver by the dkim/spf/dmarc/etc packages.
267 resolver := dns.StrictResolver{Log: log.Logger}
268 go serve(name, mox.Cid(), hostname, tlsConfig, conn, resolver, submission, xtls, maxMessageSize, requireTLSForAuth, requireTLSForDelivery, requireTLS, dnsBLs, firstTimeSenderDelay)
272 servers = append(servers, serve)
275// Serve starts serving on all listeners, launching a goroutine per listener.
277 for _, serve := range servers {
285 // OrigConn is the original (TCP) connection. We'll read from/write to conn, which
286 // can be wrapped in a tls.Server. We close origConn instead of conn because
287 // closing the TLS connection would send a TLS close notification, which may block
288 // for 5s if the server isn't reading it (because it is also sending it).
293 extRequireTLS bool // Whether to announce and allow the REQUIRETLS extension.
294 resolver dns.Resolver
297 tr *moxio.TraceReader // Kept for changing trace level during cmd/auth/data.
298 tw *moxio.TraceWriter
299 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.
300 lastlog time.Time // Used for printing the delta time since the previous logging for this connection.
302 tlsConfig *tls.Config
308 requireTLSForAuth bool
309 requireTLSForDelivery bool // If set, delivery is only allowed with TLS (STARTTLS), except if delivery is to a TLS reporting address.
310 cmd string // Current command.
311 cmdStart time.Time // Start of current command.
312 ncmds int // Number of commands processed. Used to abort connection when first incoming command is unknown/invalid.
314 firstTimeSenderDelay time.Duration
316 // If non-zero, taken into account during Read and Write. Set while processing DATA
317 // command, we don't want the entire delivery to take too long.
320 hello dns.IPDomain // Claimed remote name. Can be ip address for ehlo.
321 ehlo bool // If set, we had EHLO instead of HELO.
323 authFailed int // Number of failed auth attempts. For slowing down remote with many failures.
324 username string // Only when authenticated.
325 account *store.Account // Only when authenticated.
327 // We track good/bad message transactions to disconnect spammers trying to guess addresses.
331 // Message transaction.
333 requireTLS *bool // MAIL FROM with REQUIRETLS set.
334 futureRelease time.Time // MAIL FROM with HOLDFOR or HOLDUNTIL.
335 futureReleaseRequest string // For use in DSNs, either "for;" or "until;" plus original value.
../rfc/4865:305
336 has8bitmime bool // If MAIL FROM parameter BODY=8BITMIME was sent. Required for SMTPUTF8.
337 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.
338 msgsmtputf8 bool // Is SMTPUTF8 required for the received message. Default to the same value as `smtputf8`, but is re-evaluated after the whole message (envelope and data) is received.
339 recipients []recipient
342type rcptAccount struct {
344 destination config.Destination
345 canonicalAddress string // Optional catchall part stripped and/or lowercased.
348type rcptAlias struct {
350 canonicalAddress string // Optional catchall part stripped and/or lowercased.
353type recipient struct {
356 // If account and alias are both not set, this is not for a local address. This is
357 // normal for submission, where messages are added to the queue. For incoming
358 // deliveries, this will result in an error.
359 account *rcptAccount // If set, recipient address is for this local account.
360 alias *rcptAlias // If set, for a local alias.
363func isClosed(err error) bool {
364 return errors.Is(err, errIO) || moxio.IsClosed(err)
367// completely reset connection state as if greeting has just been sent.
369func (c *conn) reset() {
371 c.hello = dns.IPDomain{}
373 if c.account != nil {
374 err := c.account.Close()
375 c.log.Check(err, "closing account")
381// for rset command, and a few more cases that reset the mail transaction state.
383func (c *conn) rset() {
386 c.futureRelease = time.Time{}
387 c.futureReleaseRequest = ""
388 c.has8bitmime = false
390 c.msgsmtputf8 = false
394func (c *conn) earliestDeadline(d time.Duration) time.Time {
395 e := time.Now().Add(d)
396 if !c.deadline.IsZero() && c.deadline.Before(e) {
402func (c *conn) xcheckAuth() {
403 if c.submission && c.account == nil {
405 xsmtpUserErrorf(smtp.C530SecurityRequired, smtp.SePol7Other0, "authentication required")
409func (c *conn) xtrace(level slog.Level) func() {
415 c.tr.SetTrace(mlog.LevelTrace)
416 c.tw.SetTrace(mlog.LevelTrace)
420// setSlow marks the connection slow (or now), so reads are done with 3 second
421// delay for each read, and writes are done at 1 byte per second, to try to slow
423func (c *conn) setSlow(on bool) {
425 c.log.Debug("connection changed to slow")
426 } else if !on && c.slow {
427 c.log.Debug("connection restored to regular pace")
432// Write writes to the connection. It panics on i/o errors, which is handled by the
433// connection command loop.
434func (c *conn) Write(buf []byte) (int, error) {
440 // We set a single deadline for Write and Read. This may be a TLS connection.
441 // SetDeadline works on the underlying connection. If we wouldn't touch the read
442 // deadline, and only set the write deadline and do a bunch of writes, the TLS
443 // library would still have to do reads on the underlying connection, and may reach
444 // a read deadline that was set for some earlier read.
445 // We have one deadline for the whole write. In case of slow writing, we'll write
446 // the last chunk in one go, so remote smtp clients don't abort the connection for
448 deadline := c.earliestDeadline(30 * time.Second)
449 if err := c.conn.SetDeadline(deadline); err != nil {
450 c.log.Errorx("setting deadline for write", err)
455 nn, err := c.conn.Write(buf[:chunk])
457 panic(fmt.Errorf("write: %s (%w)", err, errIO))
461 if len(buf) > 0 && badClientDelay > 0 {
462 mox.Sleep(mox.Context, badClientDelay)
464 // Make sure we don't take too long, otherwise the remote SMTP client may close the
466 if time.Until(deadline) < 2*badClientDelay {
474// Read reads from the connection. It panics on i/o errors, which is handled by the
475// connection command loop.
476func (c *conn) Read(buf []byte) (int, error) {
477 if c.slow && badClientDelay > 0 {
478 mox.Sleep(mox.Context, badClientDelay)
482 // See comment about Deadline instead of individual read/write deadlines at Write.
483 if err := c.conn.SetDeadline(c.earliestDeadline(30 * time.Second)); err != nil {
484 c.log.Errorx("setting deadline for read", err)
487 n, err := c.conn.Read(buf)
489 panic(fmt.Errorf("read: %s (%w)", err, errIO))
494// Cache of line buffers for reading commands.
496var bufpool = moxio.NewBufpool(8, 2*1024)
498func (c *conn) readline() string {
499 line, err := bufpool.Readline(c.log, c.r)
500 if err != nil && errors.Is(err, moxio.ErrLineTooLong) {
501 c.writecodeline(smtp.C500BadSyntax, smtp.SeProto5Other0, "line too long, smtp max is 512, we reached 2048", nil)
502 panic(fmt.Errorf("%s (%w)", err, errIO))
503 } else if err != nil {
504 panic(fmt.Errorf("%s (%w)", err, errIO))
509// Buffered-write command response line to connection with codes and msg.
510// Err is not sent to remote but is used for logging and can be empty.
511func (c *conn) bwritecodeline(code int, secode string, msg string, err error) {
514 ecode = fmt.Sprintf("%d.%s", code/100, secode)
516 metricCommands.WithLabelValues(c.kind(), c.cmd, fmt.Sprintf("%d", code), ecode).Observe(float64(time.Since(c.cmdStart)) / float64(time.Second))
517 c.log.Debugx("smtp command result", err,
518 slog.String("kind", c.kind()),
519 slog.String("cmd", c.cmd),
520 slog.Int("code", code),
521 slog.String("ecode", ecode),
522 slog.Duration("duration", time.Since(c.cmdStart)))
529 // Separate by newline and wrap long lines.
530 lines := strings.Split(msg, "\n")
531 for i, line := range lines {
533 var prelen = 3 + 1 + len(ecode) + len(sep)
534 for prelen+len(line) > 510 {
536 for ; e > 400 && line[e] != ' '; e-- {
538 // 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.
539 c.bwritelinef("%d-%s%s%s", code, ecode, sep, line[:e])
543 if i < len(lines)-1 {
546 c.bwritelinef("%d%s%s%s%s", code, spdash, ecode, sep, line)
550// Buffered-write a formatted response line to connection.
551func (c *conn) bwritelinef(format string, args ...any) {
552 msg := fmt.Sprintf(format, args...)
553 fmt.Fprint(c.w, msg+"\r\n")
556// Flush pending buffered writes to connection.
557func (c *conn) xflush() {
558 c.w.Flush() // Errors will have caused a panic in Write.
561// Write (with flush) a response line with codes and message. err is not written, used for logging and can be nil.
562func (c *conn) writecodeline(code int, secode string, msg string, err error) {
563 c.bwritecodeline(code, secode, msg, err)
567// Write (with flush) a formatted response line to connection.
568func (c *conn) writelinef(format string, args ...any) {
569 c.bwritelinef(format, args...)
573var cleanClose struct{} // Sentinel value for panic/recover indicating clean close of connection.
575func 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) {
576 var localIP, remoteIP net.IP
577 if a, ok := nc.LocalAddr().(*net.TCPAddr); ok {
580 // For net.Pipe, during tests.
581 localIP = net.ParseIP("127.0.0.10")
583 if a, ok := nc.RemoteAddr().(*net.TCPAddr); ok {
586 // For net.Pipe, during tests.
587 remoteIP = net.ParseIP("127.0.0.10")
594 submission: submission,
596 extRequireTLS: requireTLS,
599 tlsConfig: tlsConfig,
603 maxMessageSize: maxMessageSize,
604 requireTLSForAuth: requireTLSForAuth,
605 requireTLSForDelivery: requireTLSForDelivery,
607 firstTimeSenderDelay: firstTimeSenderDelay,
609 var logmutex sync.Mutex
610 c.log = mlog.New("smtpserver", nil).WithFunc(func() []slog.Attr {
612 defer logmutex.Unlock()
615 slog.Int64("cid", c.cid),
616 slog.Duration("delta", now.Sub(c.lastlog)),
619 if c.username != "" {
620 l = append(l, slog.String("username", c.username))
624 c.tr = moxio.NewTraceReader(c.log, "RC: ", c)
625 c.tw = moxio.NewTraceWriter(c.log, "LS: ", c)
626 c.r = bufio.NewReader(c.tr)
627 c.w = bufio.NewWriter(c.tw)
629 metricConnection.WithLabelValues(c.kind()).Inc()
630 c.log.Info("new connection",
631 slog.Any("remote", c.conn.RemoteAddr()),
632 slog.Any("local", c.conn.LocalAddr()),
633 slog.Bool("submission", submission),
634 slog.Bool("tls", tls),
635 slog.String("listener", listenerName))
638 c.origConn.Close() // Close actual TCP socket, regardless of TLS on top.
639 c.conn.Close() // If TLS, will try to write alert notification to already closed socket, returning error quickly.
641 if c.account != nil {
642 err := c.account.Close()
643 c.log.Check(err, "closing account")
648 if x == nil || x == cleanClose {
649 c.log.Info("connection closed")
650 } else if err, ok := x.(error); ok && isClosed(err) {
651 c.log.Infox("connection closed", err)
653 c.log.Error("unhandled panic", slog.Any("err", x))
655 metrics.PanicInc(metrics.Smtpserver)
660 case <-mox.Shutdown.Done():
662 c.writecodeline(smtp.C421ServiceUnavail, smtp.SeSys3NotAccepting2, "shutting down", nil)
667 if !limiterConnectionRate.Add(c.remoteIP, time.Now(), 1) {
668 c.writecodeline(smtp.C421ServiceUnavail, smtp.SePol7Other0, "connection rate from your ip or network too high, slow down please", nil)
672 // If remote IP/network resulted in too many authentication failures, refuse to serve.
673 if submission && !mox.LimiterFailedAuth.CanAdd(c.remoteIP, time.Now(), 1) {
674 metrics.AuthenticationRatelimitedInc("submission")
675 c.log.Debug("refusing connection due to many auth failures", slog.Any("remoteip", c.remoteIP))
676 c.writecodeline(smtp.C421ServiceUnavail, smtp.SePol7Other0, "too many auth failures", nil)
680 if !limiterConnections.Add(c.remoteIP, time.Now(), 1) {
681 c.log.Debug("refusing connection due to many open connections", slog.Any("remoteip", c.remoteIP))
682 c.writecodeline(smtp.C421ServiceUnavail, smtp.SePol7Other0, "too many open connections from your ip or network", nil)
685 defer limiterConnections.Add(c.remoteIP, time.Now(), -1)
687 // We register and unregister the original connection, in case c.conn is replaced
688 // with a TLS connection later on.
689 mox.Connections.Register(nc, "smtp", listenerName)
690 defer mox.Connections.Unregister(nc)
694 // We include the string ESMTP. https://cr.yp.to/smtp/greeting.html recommends it.
695 // Should not be too relevant nowadays, but does not hurt and default blackbox
696 // exporter SMTP health check expects it.
697 c.writelinef("%d %s ESMTP mox %s", smtp.C220ServiceReady, c.hostname.ASCII, moxvar.Version)
702 // If another command is present, don't flush our buffered response yet. Holding
703 // off will cause us to respond with a single packet.
706 buf, err := c.r.Peek(n)
707 if err == nil && bytes.IndexByte(buf, '\n') >= 0 {
715var commands = map[string]func(c *conn, p *parser){
716 "helo": (*conn).cmdHelo,
717 "ehlo": (*conn).cmdEhlo,
718 "starttls": (*conn).cmdStarttls,
719 "auth": (*conn).cmdAuth,
720 "mail": (*conn).cmdMail,
721 "rcpt": (*conn).cmdRcpt,
722 "data": (*conn).cmdData,
723 "rset": (*conn).cmdRset,
724 "vrfy": (*conn).cmdVrfy,
725 "expn": (*conn).cmdExpn,
726 "help": (*conn).cmdHelp,
727 "noop": (*conn).cmdNoop,
728 "quit": (*conn).cmdQuit,
731func command(c *conn) {
747 if errors.As(err, &serr) {
748 c.writecodeline(serr.code, serr.secode, fmt.Sprintf("%s (%s)", serr.errmsg, mox.ReceivedID(c.cid)), serr.err)
753 // Other type of panic, we pass it on, aborting the connection.
754 c.log.Errorx("command panic", err)
759 // todo future: we could wait for either a line or shutdown, and just close the connection on shutdown.
762 t := strings.SplitN(line, " ", 2)
768 cmdl := strings.ToLower(cmd)
770 // 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
773 case <-mox.Shutdown.Done():
775 c.writecodeline(smtp.C421ServiceUnavail, smtp.SeSys3NotAccepting2, "shutting down", nil)
781 c.cmdStart = time.Now()
783 p := newParser(args, c.smtputf8, c)
784 fn, ok := commands[cmdl]
788 // Other side is likely speaking something else than SMTP, send error message and
789 // stop processing because there is a good chance whatever they sent has multiple
791 c.writecodeline(smtp.C500BadSyntax, smtp.SeProto5Syntax2, "please try again speaking smtp", nil)
795 xsmtpUserErrorf(smtp.C500BadSyntax, smtp.SeProto5BadCmdOrSeq1, "unknown command")
801// For use in metric labels.
802func (c *conn) kind() string {
809func (c *conn) xneedHello() {
810 if c.hello.IsZero() {
811 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "no ehlo/helo yet")
815// If smtp server is configured to require TLS for all mail delivery (except to TLS
816// reporting address), abort command.
817func (c *conn) xneedTLSForDelivery(rcpt smtp.Path) {
818 // For TLS reports, we allow the message in even without TLS, because there may be
820 if c.requireTLSForDelivery && !c.tls && !isTLSReportRecipient(rcpt) {
822 xsmtpUserErrorf(smtp.C530SecurityRequired, smtp.SePol7Other0, "STARTTLS required for mail delivery")
826func isTLSReportRecipient(rcpt smtp.Path) bool {
827 _, _, _, dest, err := mox.LookupAddress(rcpt.Localpart, rcpt.IPDomain.Domain, false, false)
828 return err == nil && (dest.HostTLSReports || dest.DomainTLSReports)
831func (c *conn) cmdHelo(p *parser) {
835func (c *conn) cmdEhlo(p *parser) {
840func (c *conn) cmdHello(p *parser, ehlo bool) {
841 var remote dns.IPDomain
842 if c.submission && !mox.Pedantic {
843 // Mail clients regularly put bogus information in the hostname/ip. For submission,
844 // the value is of no use, so there is not much point in annoying the user with
845 // errors they cannot fix themselves. Except when in pedantic mode.
846 remote = dns.IPDomain{IP: c.remoteIP}
850 remote = p.xipdomain(true)
852 remote = dns.IPDomain{Domain: p.xdomain()}
854 // Verify a remote domain name has an A or AAAA record, CNAME not allowed.
../rfc/5321:722
855 cidctx := context.WithValue(mox.Context, mlog.CidKey, c.cid)
856 ctx, cancel := context.WithTimeout(cidctx, time.Minute)
857 _, _, err := c.resolver.LookupIPAddr(ctx, remote.Domain.ASCII+".")
859 if dns.IsNotFound(err) {
860 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeProto5Other0, "your ehlo domain does not resolve to an IP address")
862 // For success or temporary resolve errors, we'll just continue.
865 // Though a few paragraphs earlier is a claim additional data can occur for address
866 // literals (IP addresses), although the ABNF in that document does not allow it.
867 // We allow additional text, but only if space-separated.
868 if len(remote.IP) > 0 && p.space() {
880 // https://www.iana.org/assignments/mail-parameters/mail-parameters.xhtml
882 c.bwritelinef("250-%s", c.hostname.ASCII)
886 if !c.tls && c.tlsConfig != nil {
888 c.bwritelinef("250-STARTTLS")
889 } else if c.extRequireTLS {
892 c.bwritelinef("250-REQUIRETLS")
896 if c.tls || !c.requireTLSForAuth {
897 // We always mention the SCRAM PLUS variants, even if TLS is not active: It is a
898 // hint to the client that a TLS connection can use TLS channel binding during
899 // authentication. The client should select the bare variant when TLS isn't
900 // present, and also not indicate the server supports the PLUS variant in that
901 // case, or it would trigger the mechanism downgrade detection.
902 c.bwritelinef("250-AUTH SCRAM-SHA-256-PLUS SCRAM-SHA-256 SCRAM-SHA-1-PLUS SCRAM-SHA-1 CRAM-MD5 PLAIN LOGIN")
904 c.bwritelinef("250-AUTH ")
908 c.bwritelinef("250-FUTURERELEASE %d %s", queue.FutureReleaseIntervalMax/time.Second, t.Format(time.RFC3339))
911 // todo future? c.writelinef("250-DSN")
919func (c *conn) cmdStarttls(p *parser) {
925 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "already speaking tls")
927 if c.account != nil {
928 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "cannot starttls after authentication")
931 // We don't want to do TLS on top of c.r because it also prints protocol traces: We
932 // don't want to log the TLS stream. So we'll do TLS on the underlying connection,
933 // but make sure any bytes already read and in the buffer are used for the TLS
936 if n := c.r.Buffered(); n > 0 {
937 conn = &moxio.PrefixConn{
938 PrefixReader: io.LimitReader(c.r, int64(n)),
943 // We add the cid to the output, to help debugging in case of a failing TLS connection.
944 c.writecodeline(smtp.C220ServiceReady, smtp.SeOther00, "go! ("+mox.ReceivedID(c.cid)+")", nil)
945 tlsConn := tls.Server(conn, c.tlsConfig)
946 cidctx := context.WithValue(mox.Context, mlog.CidKey, c.cid)
947 ctx, cancel := context.WithTimeout(cidctx, time.Minute)
949 c.log.Debug("starting tls server handshake")
950 if err := tlsConn.HandshakeContext(ctx); err != nil {
951 panic(fmt.Errorf("starttls handshake: %s (%w)", err, errIO))
954 tlsversion, ciphersuite := moxio.TLSInfo(tlsConn)
955 c.log.Debug("tls server handshake done", slog.String("tls", tlsversion), slog.String("ciphersuite", ciphersuite))
957 c.tr = moxio.NewTraceReader(c.log, "RC: ", c)
958 c.tw = moxio.NewTraceWriter(c.log, "LS: ", c)
959 c.r = bufio.NewReader(c.tr)
960 c.w = bufio.NewWriter(c.tw)
967func (c *conn) cmdAuth(p *parser) {
971 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "authentication only allowed on submission ports")
973 if c.account != nil {
975 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "already authenticated")
977 if c.mailFrom != nil {
979 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "authentication not allowed during mail transaction")
982 // If authentication fails due to missing derived secrets, we don't hold it against
983 // the connection. There is no way to indicate server support for an authentication
984 // mechanism, but that a mechanism won't work for an account.
985 var missingDerivedSecrets bool
987 // For many failed auth attempts, slow down verification attempts.
988 // Dropping the connection could also work, but more so when we have a connection rate limiter.
990 if c.authFailed > 3 && authFailDelay > 0 {
992 mox.Sleep(mox.Context, time.Duration(c.authFailed-3)*authFailDelay)
994 c.authFailed++ // Compensated on success.
996 if missingDerivedSecrets {
999 // On the 3rd failed authentication, start responding slowly. Successful auth will
1000 // cause fast responses again.
1001 if c.authFailed >= 3 {
1006 var authVariant string
1007 authResult := "error"
1009 metrics.AuthenticationInc("submission", authVariant, authResult)
1010 if authResult == "ok" {
1011 mox.LimiterFailedAuth.Reset(c.remoteIP, time.Now())
1012 } else if !missingDerivedSecrets {
1013 mox.LimiterFailedAuth.Add(c.remoteIP, time.Now(), 1)
1019 mech := p.xsaslMech()
1021 xreadInitial := func() []byte {
1025 // todo future: handle max length of 12288 octets and return proper responde codes otherwise
../rfc/4954:253
1029 authResult = "aborted"
1030 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5Other0, "authentication aborted")
1035 // Windows Mail 16005.14326.21606.0 sends two spaces between "AUTH PLAIN" and the
1040 auth = p.remainder()
1043 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5Syntax2, "missing initial auth base64 parameter after space")
1044 } else if auth == "=" {
1046 auth = "" // Base64 decode below will result in empty buffer.
1049 buf, err := base64.StdEncoding.DecodeString(auth)
1052 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5Syntax2, "invalid base64: %s", err)
1057 xreadContinuation := func() []byte {
1058 line := c.readline()
1060 authResult = "aborted"
1061 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5Other0, "authentication aborted")
1063 buf, err := base64.StdEncoding.DecodeString(line)
1066 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5Syntax2, "invalid base64: %s", err)
1073 authVariant = "plain"
1077 if !c.tls && c.requireTLSForAuth {
1078 xsmtpUserErrorf(smtp.C538EncReqForAuth, smtp.SePol7EncReqForAuth11, "authentication requires tls")
1081 // Password is in line in plain text, so hide it.
1082 defer c.xtrace(mlog.LevelTraceauth)()
1083 buf := xreadInitial()
1084 c.xtrace(mlog.LevelTrace) // Restore.
1085 plain := bytes.Split(buf, []byte{0})
1086 if len(plain) != 3 {
1087 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5BadParams4, "auth data should have 3 nul-separated tokens, got %d", len(plain))
1089 authz := norm.NFC.String(string(plain[0]))
1090 authc := norm.NFC.String(string(plain[1]))
1091 password := string(plain[2])
1093 if authz != "" && authz != authc {
1094 authResult = "badcreds"
1095 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "cannot assume other role")
1098 acc, err := store.OpenEmailAuth(c.log, authc, password)
1099 if err != nil && errors.Is(err, store.ErrUnknownCredentials) {
1101 authResult = "badcreds"
1102 c.log.Info("failed authentication attempt", slog.String("username", authc), slog.Any("remote", c.remoteIP))
1103 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass")
1105 xcheckf(err, "verifying credentials")
1113 c.writecodeline(smtp.C235AuthSuccess, smtp.SePol7Other0, "nice", nil)
1116 // LOGIN is obsoleted in favor of PLAIN, only implemented to support legacy
1117 // clients, see Internet-Draft (I-D):
1118 // https://datatracker.ietf.org/doc/html/draft-murchison-sasl-login-00
1120 authVariant = "login"
1124 if !c.tls && c.requireTLSForAuth {
1125 xsmtpUserErrorf(smtp.C538EncReqForAuth, smtp.SePol7EncReqForAuth11, "authentication requires tls")
1128 // Read user name. The I-D says the client should ignore the server challenge, we
1129 // send an empty one.
1130 // I-D says maximum length must be 64 bytes. We allow more, for long user names
1132 username := string(xreadInitial())
1133 username = norm.NFC.String(username)
1135 // Again, client should ignore the challenge, we send the same as the example in
1137 c.writelinef("%d %s", smtp.C334ContinueAuth, base64.StdEncoding.EncodeToString([]byte("Password")))
1139 // Password is in line in plain text, so hide it.
1140 defer c.xtrace(mlog.LevelTraceauth)()
1141 password := string(xreadContinuation())
1142 c.xtrace(mlog.LevelTrace) // Restore.
1144 acc, err := store.OpenEmailAuth(c.log, username, password)
1145 if err != nil && errors.Is(err, store.ErrUnknownCredentials) {
1147 authResult = "badcreds"
1148 c.log.Info("failed authentication attempt", slog.String("username", username), slog.Any("remote", c.remoteIP))
1149 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass")
1151 xcheckf(err, "verifying credentials")
1157 c.username = username
1159 c.writecodeline(smtp.C235AuthSuccess, smtp.SePol7Other0, "hello ancient smtp implementation", nil)
1162 authVariant = strings.ToLower(mech)
1167 chal := fmt.Sprintf("<%d.%d@%s>", uint64(mox.CryptoRandInt()), time.Now().UnixNano(), mox.Conf.Static.HostnameDomain.ASCII)
1168 c.writelinef("%d %s", smtp.C334ContinueAuth, base64.StdEncoding.EncodeToString([]byte(chal)))
1170 resp := xreadContinuation()
1171 t := strings.Split(string(resp), " ")
1172 if len(t) != 2 || len(t[1]) != 2*md5.Size {
1173 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5BadParams4, "malformed cram-md5 response")
1175 addr := norm.NFC.String(t[0])
1176 c.log.Debug("cram-md5 auth", slog.String("address", addr))
1177 acc, _, err := store.OpenEmail(c.log, addr)
1179 if errors.Is(err, store.ErrUnknownCredentials) {
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")
1184 xcheckf(err, "looking up address")
1188 c.log.Check(err, "closing account")
1191 var ipadhash, opadhash hash.Hash
1192 acc.WithRLock(func() {
1193 err := acc.DB.Read(context.TODO(), func(tx *bstore.Tx) error {
1194 password, err := bstore.QueryTx[store.Password](tx).Get()
1195 if err == bstore.ErrAbsent {
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")
1203 ipadhash = password.CRAMMD5.Ipad
1204 opadhash = password.CRAMMD5.Opad
1207 xcheckf(err, "tx read")
1209 if ipadhash == nil || opadhash == nil {
1210 missingDerivedSecrets = true
1211 c.log.Info("cram-md5 auth attempt without derived secrets set, save password again to store secrets", slog.String("username", addr))
1212 c.log.Info("failed authentication attempt", slog.String("username", addr), slog.Any("remote", c.remoteIP))
1213 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass")
1217 ipadhash.Write([]byte(chal))
1218 opadhash.Write(ipadhash.Sum(nil))
1219 digest := fmt.Sprintf("%x", opadhash.Sum(nil))
1221 c.log.Info("failed authentication attempt", slog.String("username", addr), slog.Any("remote", c.remoteIP))
1222 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass")
1229 acc = nil // Cancel cleanup.
1232 c.writecodeline(smtp.C235AuthSuccess, smtp.SePol7Other0, "nice", nil)
1234 case "SCRAM-SHA-256-PLUS", "SCRAM-SHA-256", "SCRAM-SHA-1-PLUS", "SCRAM-SHA-1":
1235 // 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?
1236 // todo: use single implementation between ../imapserver/server.go and ../smtpserver/server.go
1238 // Passwords cannot be retrieved or replayed from the trace.
1240 authVariant = strings.ToLower(mech)
1241 var h func() hash.Hash
1242 switch authVariant {
1243 case "scram-sha-1", "scram-sha-1-plus":
1245 case "scram-sha-256", "scram-sha-256-plus":
1248 xsmtpServerErrorf(codes{smtp.C554TransactionFailed, smtp.SeSys3Other0}, "missing scram auth method case")
1251 var cs *tls.ConnectionState
1252 channelBindingRequired := strings.HasSuffix(authVariant, "-plus")
1253 if channelBindingRequired && !c.tls {
1255 xsmtpUserErrorf(smtp.C538EncReqForAuth, smtp.SePol7EncReqForAuth11, "scram plus mechanism requires tls connection")
1258 xcs := c.conn.(*tls.Conn).ConnectionState()
1261 c0 := xreadInitial()
1262 ss, err := scram.NewServer(h, c0, cs, channelBindingRequired)
1263 xcheckf(err, "starting scram")
1264 authc := norm.NFC.String(ss.Authentication)
1265 c.log.Debug("scram auth", slog.String("authentication", authc))
1266 acc, _, err := store.OpenEmail(c.log, authc)
1268 // todo: we could continue scram with a generated salt, deterministically generated
1269 // from the username. that way we don't have to store anything but attackers cannot
1270 // learn if an account exists. same for absent scram saltedpassword below.
1271 c.log.Info("failed authentication attempt", slog.String("username", authc), slog.Any("remote", c.remoteIP))
1272 xsmtpUserErrorf(smtp.C454TempAuthFail, smtp.SeSys3Other0, "scram not possible")
1277 c.log.Check(err, "closing account")
1280 if ss.Authorization != "" && ss.Authorization != ss.Authentication {
1281 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "authentication with authorization for different user not supported")
1283 var xscram store.SCRAM
1284 acc.WithRLock(func() {
1285 err := acc.DB.Read(context.TODO(), func(tx *bstore.Tx) error {
1286 password, err := bstore.QueryTx[store.Password](tx).Get()
1287 if err == bstore.ErrAbsent {
1288 c.log.Info("failed authentication attempt", slog.String("username", authc), slog.Any("remote", c.remoteIP))
1289 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass")
1291 xcheckf(err, "fetching credentials")
1292 switch authVariant {
1293 case "scram-sha-1", "scram-sha-1-plus":
1294 xscram = password.SCRAMSHA1
1295 case "scram-sha-256", "scram-sha-256-plus":
1296 xscram = password.SCRAMSHA256
1298 xsmtpServerErrorf(codes{smtp.C554TransactionFailed, smtp.SeSys3Other0}, "missing scram auth credentials case")
1300 if len(xscram.Salt) == 0 || xscram.Iterations == 0 || len(xscram.SaltedPassword) == 0 {
1301 missingDerivedSecrets = true
1302 c.log.Info("scram auth attempt without derived secrets set, save password again to store secrets", slog.String("address", authc))
1303 c.log.Info("failed authentication attempt", slog.String("username", authc), slog.Any("remote", c.remoteIP))
1304 xsmtpUserErrorf(smtp.C454TempAuthFail, smtp.SeSys3Other0, "scram not possible")
1308 xcheckf(err, "read tx")
1310 s1, err := ss.ServerFirst(xscram.Iterations, xscram.Salt)
1311 xcheckf(err, "scram first server step")
1312 c.writelinef("%d %s", smtp.C334ContinueAuth, base64.StdEncoding.EncodeToString([]byte(s1))) //
../rfc/4954:187
1313 c2 := xreadContinuation()
1314 s3, err := ss.Finish(c2, xscram.SaltedPassword)
1316 c.writelinef("%d %s", smtp.C334ContinueAuth, base64.StdEncoding.EncodeToString([]byte(s3))) //
../rfc/4954:187
1319 c.readline() // Should be "*" for cancellation.
1320 if errors.Is(err, scram.ErrInvalidProof) {
1321 authResult = "badcreds"
1322 c.log.Info("failed authentication attempt", slog.String("username", authc), slog.Any("remote", c.remoteIP))
1323 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad credentials")
1325 xcheckf(err, "server final")
1329 // The message should be empty. todo: should we require it is empty?
1336 acc = nil // Cancel cleanup.
1339 c.writecodeline(smtp.C235AuthSuccess, smtp.SePol7Other0, "nice", nil)
1343 xsmtpUserErrorf(smtp.C504ParamNotImpl, smtp.SeProto5BadParams4, "mechanism %s not supported", mech)
1348func (c *conn) cmdMail(p *parser) {
1349 // requirements for maximum line length:
1351 // todo future: enforce? doesn't really seem worth it...
1353 if c.transactionBad > 10 && c.transactionGood == 0 {
1354 // If we get many bad transactions, it's probably a spammer that is guessing user names.
1355 // Useful in combination with rate limiting.
1357 c.writecodeline(smtp.C550MailboxUnavail, smtp.SeAddr1Other0, "too many failures", nil)
1363 if c.mailFrom != nil {
1365 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "already have MAIL")
1367 // Ensure clear transaction state on failure.
1378 // Microsoft Outlook 365 Apps for Enterprise sends it with submission. For delivery
1379 // it is mostly used by spammers, but has been seen with legitimate senders too.
1383 rawRevPath := p.xrawReversePath()
1384 paramSeen := map[string]bool{}
1387 key := p.xparamKeyword()
1389 K := strings.ToUpper(key)
1392 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5BadParams4, "duplicate param %q", key)
1400 if size > c.maxMessageSize {
1402 ecode := smtp.SeSys3MsgLimitExceeded4
1403 if size < config.DefaultMaxMsgSize {
1404 ecode = smtp.SeMailbox2MsgLimitExceeded3
1406 xsmtpUserErrorf(smtp.C552MailboxFull, ecode, "message too large")
1408 // We won't verify the message is exactly the size the remote claims. Buf if it is
1409 // larger, we'll abort the transaction when remote crosses the boundary.
1413 v := p.xparamValue()
1414 switch strings.ToUpper(v) {
1416 c.has8bitmime = false
1418 c.has8bitmime = true
1420 xsmtpUserErrorf(smtp.C555UnrecognizedAddrParams, smtp.SeProto5BadParams4, "unrecognized parameter %q", key)
1425 // We act as if we don't trust the client to specify a mailbox. Instead, we always
1426 // check the rfc5321.mailfrom and rfc5322.from before accepting the submission.
1430 // 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
1438 c.msgsmtputf8 = true
1442 xsmtpUserErrorf(smtp.C530SecurityRequired, smtp.SePol7EncNeeded10, "requiretls only allowed on tls-encrypted connections")
1443 } else if !c.extRequireTLS {
1444 xsmtpUserErrorf(smtp.C555UnrecognizedAddrParams, smtp.SeSys3NotSupported3, "REQUIRETLS not allowed for this connection")
1448 case "HOLDFOR", "HOLDUNTIL":
1451 xsmtpUserErrorf(smtp.C555UnrecognizedAddrParams, smtp.SeSys3NotSupported3, "unrecognized parameter %q", key)
1453 if K == "HOLDFOR" && paramSeen["HOLDUNTIL"] || K == "HOLDUNTIL" && paramSeen["HOLDFOR"] {
1455 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5BadParams4, "cannot use both HOLDUNTIL and HOLFOR")
1459 // semantic errors as syntax errors
1462 if n > int64(queue.FutureReleaseIntervalMax/time.Second) {
1464 xsmtpUserErrorf(smtp.C554TransactionFailed, smtp.SeProto5BadParams4, "future release interval too far in the future")
1466 c.futureRelease = time.Now().Add(time.Duration(n) * time.Second)
1467 c.futureReleaseRequest = fmt.Sprintf("for;%d", n)
1469 t, s := p.xdatetimeutc()
1470 ival := time.Until(t)
1472 // Likely a mistake by the user.
1473 xsmtpUserErrorf(smtp.C554TransactionFailed, smtp.SeProto5BadParams4, "requested future release time is in the past")
1474 } else if ival > queue.FutureReleaseIntervalMax {
1476 xsmtpUserErrorf(smtp.C554TransactionFailed, smtp.SeProto5BadParams4, "requested future release time is too far in the future")
1479 c.futureReleaseRequest = "until;" + s
1483 xsmtpUserErrorf(smtp.C555UnrecognizedAddrParams, smtp.SeSys3NotSupported3, "unrecognized parameter %q", key)
1487 // We now know if we have to parse the address with support for utf8.
1488 pp := newParser(rawRevPath, c.smtputf8, c)
1489 rpath := pp.xbareReversePath()
1494 // For submission, check if reverse path is allowed. I.e. authenticated account
1495 // must have the rpath configured. We do a check again on rfc5322.from during DATA.
1496 rpathAllowed := func() bool {
1501 accName, _, _, _, err := mox.LookupAddress(rpath.Localpart, rpath.IPDomain.Domain, false, false)
1502 return err == nil && accName == c.account.Name
1505 if !c.submission && !rpath.IPDomain.Domain.IsZero() {
1506 // If rpath domain has null MX record or is otherwise not accepting email, reject.
1509 cidctx := context.WithValue(mox.Context, mlog.CidKey, c.cid)
1510 ctx, cancel := context.WithTimeout(cidctx, time.Minute)
1511 valid, err := checkMXRecords(ctx, c.resolver, rpath.IPDomain.Domain)
1514 c.log.Infox("temporary reject for temporary mx lookup error", err)
1515 xsmtpServerErrorf(codes{smtp.C451LocalErr, smtp.SeNet4Other0}, "cannot verify mx records for mailfrom domain")
1517 c.log.Info("permanent reject because mailfrom domain does not accept mail")
1518 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SePol7SenderHasNullMX27, "mailfrom domain not configured for mail")
1522 if c.submission && (len(rpath.IPDomain.IP) > 0 || !rpathAllowed()) {
1524 c.log.Info("submission with unconfigured mailfrom", slog.String("user", c.username), slog.String("mailfrom", rpath.String()))
1525 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SePol7DeliveryUnauth1, "must match authenticated user")
1526 } else if !c.submission && len(rpath.IPDomain.IP) > 0 {
1527 // todo future: allow if the IP is the same as this connection is coming from? does later code allow this?
1528 c.log.Info("delivery from address without domain", slog.String("mailfrom", rpath.String()))
1529 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SePol7Other0, "domain name required")
1532 if Localserve && strings.HasPrefix(string(rpath.Localpart), "mailfrom") {
1533 c.xlocalserveError(rpath.Localpart)
1538 c.bwritecodeline(smtp.C250Completed, smtp.SeAddr1Other0, "looking good", nil)
1542func (c *conn) cmdRcpt(p *parser) {
1545 if c.mailFrom == nil {
1547 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "missing MAIL FROM")
1553 // Microsoft Outlook 365 Apps for Enterprise sends it with submission. For delivery
1554 // it is mostly used by spammers, but has been seen with legitimate senders too.
1559 if p.take("<POSTMASTER>") {
1560 fpath = smtp.Path{Localpart: "postmaster"}
1562 fpath = p.xforwardPath()
1566 key := p.xparamKeyword()
1567 // K := strings.ToUpper(key)
1570 xsmtpUserErrorf(smtp.C555UnrecognizedAddrParams, smtp.SeSys3NotSupported3, "unrecognized parameter %q", key)
1574 // Check if TLS is enabled if required. It's not great that sender/recipient
1575 // addresses may have been exposed in plaintext before we can reject delivery. The
1576 // recipient could be the tls reporting addresses, which must always be able to
1577 // receive in plain text.
1578 c.xneedTLSForDelivery(fpath)
1580 // todo future: for submission, should we do explicit verification that domains are fully qualified? also for mail from.
../rfc/6409:420
1582 if len(c.recipients) >= rcptToLimit {
1584 xsmtpUserErrorf(smtp.C452StorageFull, smtp.SeProto5TooManyRcpts3, "max of %d recipients reached", rcptToLimit)
1587 // We don't want to allow delivery to multiple recipients with a null reverse path.
1588 // Why would anyone send like that? Null reverse path is intended for delivery
1589 // notifications, they should go to a single recipient.
1590 if !c.submission && len(c.recipients) > 0 && c.mailFrom.IsZero() {
1591 xsmtpUserErrorf(smtp.C452StorageFull, smtp.SeProto5TooManyRcpts3, "only one recipient allowed with null reverse address")
1594 // Do not accept multiple recipients if remote does not pass SPF. Because we don't
1595 // want to generate DSNs to unverified domains. This is the moment we
1596 // can refuse individual recipients, DATA will be too late. Because mail
1597 // servers must handle a max recipient limit gracefully and still send to the
1598 // recipients that are accepted, this should not cause problems. Though we are in
1599 // violation because the limit must be >= 100.
1603 if !c.submission && len(c.recipients) == 1 && !Localserve {
1604 // note: because of check above, mailFrom cannot be the null address.
1606 d := c.mailFrom.IPDomain.Domain
1608 // todo: use this spf result for DATA.
1609 spfArgs := spf.Args{
1610 RemoteIP: c.remoteIP,
1611 MailFromLocalpart: c.mailFrom.Localpart,
1613 HelloDomain: c.hello,
1615 LocalHostname: c.hostname,
1617 cidctx := context.WithValue(mox.Context, mlog.CidKey, c.cid)
1618 spfctx, spfcancel := context.WithTimeout(cidctx, time.Minute)
1620 receivedSPF, _, _, _, err := spf.Verify(spfctx, c.log.Logger, c.resolver, spfArgs)
1623 c.log.Errorx("spf verify for multiple recipients", err)
1625 pass = receivedSPF.Identity == spf.ReceivedMailFrom && receivedSPF.Result == spf.StatusPass
1628 xsmtpUserErrorf(smtp.C452StorageFull, smtp.SeProto5TooManyRcpts3, "only one recipient allowed without spf pass")
1632 if Localserve && strings.HasPrefix(string(fpath.Localpart), "rcptto") {
1633 c.xlocalserveError(fpath.Localpart)
1636 if len(fpath.IPDomain.IP) > 0 {
1638 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeAddr1UnknownDestMailbox1, "not accepting email for ip")
1640 c.recipients = append(c.recipients, recipient{fpath, nil, nil})
1641 } else if accountName, alias, canonical, addr, err := mox.LookupAddress(fpath.Localpart, fpath.IPDomain.Domain, true, true); err == nil {
1644 c.recipients = append(c.recipients, recipient{fpath, nil, &rcptAlias{*alias, canonical}})
1646 c.recipients = append(c.recipients, recipient{fpath, &rcptAccount{accountName, addr, canonical}, nil})
1649 } else if Localserve {
1650 // If the address isn't known, and we are in localserve, deliver to the mox user.
1651 // If account or destination doesn't exist, it will be handled during delivery. For
1652 // submissions, which is the common case, we'll deliver to the logged in user,
1653 // which is typically the mox user.
1654 acc, _ := mox.Conf.Account("mox")
1655 dest := acc.Destinations["mox@localhost"]
1656 c.recipients = append(c.recipients, recipient{fpath, &rcptAccount{"mox", dest, "mox@localhost"}, nil})
1657 } else if errors.Is(err, mox.ErrDomainNotFound) {
1659 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeAddr1UnknownDestMailbox1, "not accepting email for domain")
1661 // We'll be delivering this email.
1662 c.recipients = append(c.recipients, recipient{fpath, nil, nil})
1663 } else if errors.Is(err, mox.ErrAddressNotFound) {
1665 // For submission, we're transparent about which user exists. Should be fine for the typical small-scale deploy.
1667 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeAddr1UnknownDestMailbox1, "no such user")
1669 // We pretend to accept. We don't want to let remote know the user does not exist
1670 // until after DATA. Because then remote has committed to sending a message.
1671 // note: not local for !c.submission is the signal this address is in error.
1672 c.recipients = append(c.recipients, recipient{fpath, nil, nil})
1674 c.log.Errorx("looking up account for delivery", err, slog.Any("rcptto", fpath))
1675 xsmtpServerErrorf(codes{smtp.C451LocalErr, smtp.SeSys3Other0}, "error processing")
1677 c.bwritecodeline(smtp.C250Completed, smtp.SeAddr1Other0, "now on the list", nil)
1681func (c *conn) isSMTPUTF8Required(part *message.Part) bool {
1682 hasNonASCII := func(r io.Reader) bool {
1683 br := bufio.NewReader(r)
1685 b, err := br.ReadByte()
1689 xcheckf(err, "read header")
1690 if b > unicode.MaxASCII {
1696 var hasNonASCIIPartHeader func(p *message.Part) bool
1697 hasNonASCIIPartHeader = func(p *message.Part) bool {
1698 if hasNonASCII(p.HeaderReader()) {
1701 for _, pp := range p.Parts {
1702 if hasNonASCIIPartHeader(&pp) {
1709 // Check "MAIL FROM".
1710 if hasNonASCII(strings.NewReader(string(c.mailFrom.Localpart))) {
1713 // Check all "RCPT TO".
1714 for _, rcpt := range c.recipients {
1715 if hasNonASCII(strings.NewReader(string(rcpt.addr.Localpart))) {
1719 // Check header in all message parts.
1720 return hasNonASCIIPartHeader(part)
1724func (c *conn) cmdData(p *parser) {
1727 if c.mailFrom == nil {
1729 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "missing MAIL FROM")
1731 if len(c.recipients) == 0 {
1733 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "missing RCPT TO")
1739 // 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.
1741 // Entire delivery should be done within 30 minutes, or we abort.
1742 cidctx := context.WithValue(mox.Context, mlog.CidKey, c.cid)
1743 cmdctx, cmdcancel := context.WithTimeout(cidctx, 30*time.Minute)
1745 // Deadline is taken into account by Read and Write.
1746 c.deadline, _ = cmdctx.Deadline()
1748 c.deadline = time.Time{}
1752 c.writelinef("354 see you at the bare dot")
1754 // Mark as tracedata.
1755 defer c.xtrace(mlog.LevelTracedata)()
1757 // We read the data into a temporary file. We limit the size and do basic analysis while reading.
1758 dataFile, err := store.CreateMessageTemp(c.log, "smtp-deliver")
1760 xsmtpServerErrorf(errCodes(smtp.C451LocalErr, smtp.SeSys3Other0, err), "creating temporary file for message: %s", err)
1762 defer store.CloseRemoveTempFile(c.log, dataFile, "smtpserver delivered message")
1763 msgWriter := message.NewWriter(dataFile)
1764 dr := smtp.NewDataReader(c.r)
1765 n, err := io.Copy(&limitWriter{maxSize: c.maxMessageSize, w: msgWriter}, dr)
1766 c.xtrace(mlog.LevelTrace) // Restore.
1768 if errors.Is(err, errMessageTooLarge) {
1770 ecode := smtp.SeSys3MsgLimitExceeded4
1771 if n < config.DefaultMaxMsgSize {
1772 ecode = smtp.SeMailbox2MsgLimitExceeded3
1774 c.writecodeline(smtp.C451LocalErr, ecode, fmt.Sprintf("error copying data to file (%s)", mox.ReceivedID(c.cid)), err)
1775 panic(fmt.Errorf("remote sent too much DATA: %w", errIO))
1778 if errors.Is(err, smtp.ErrCRLF) {
1779 c.writecodeline(smtp.C500BadSyntax, smtp.SeProto5Syntax2, fmt.Sprintf("invalid bare \\r or \\n, may be smtp smuggling (%s)", mox.ReceivedID(c.cid)), err)
1783 // Something is failing on our side. We want to let remote know. So write an error response,
1784 // then discard the remaining data so the remote client is more likely to see our
1785 // response. Our write is synchronous, there is a risk no window/buffer space is
1786 // available and our write blocks us from reading remaining data, leading to
1787 // deadlock. We have a timeout on our connection writes though, so worst case we'll
1788 // abort the connection due to expiration.
1789 c.writecodeline(smtp.C451LocalErr, smtp.SeSys3Other0, fmt.Sprintf("error copying data to file (%s)", mox.ReceivedID(c.cid)), err)
1790 io.Copy(io.Discard, dr)
1794 // Basic sanity checks on messages before we send them out to the world. Just
1795 // trying to be strict in what we do to others and liberal in what we accept.
1797 if !msgWriter.HaveBody {
1799 xsmtpUserErrorf(smtp.C554TransactionFailed, smtp.SeMsg6Other0, "message requires both header and body section")
1801 // Check only for pedantic mode because ios mail will attempt to send smtputf8 with
1802 // non-ascii in message from localpart without using 8bitmime.
1803 if mox.Pedantic && msgWriter.Has8bit && !c.has8bitmime {
1805 xsmtpUserErrorf(smtp.C500BadSyntax, smtp.SeMsg6Other0, "message with non-us-ascii requires 8bitmime extension")
1809 if Localserve && mox.Pedantic {
1810 // Require that message can be parsed fully.
1811 p, err := message.Parse(c.log.Logger, false, dataFile)
1813 err = p.Walk(c.log.Logger, nil)
1817 xsmtpUserErrorf(smtp.C554TransactionFailed, smtp.SeMsg6Other0, "malformed message: %v", err)
1821 // Now that we have all the whole message (envelope + data), we can check if the SMTPUTF8 extension is required.
1822 var part *message.Part
1823 if c.smtputf8 || c.submission || mox.Pedantic {
1824 // Try to parse the message.
1825 // Do nothing if something bad happen during Parse and Walk, just keep the current value for c.msgsmtputf8.
1826 p, err := message.Parse(c.log.Logger, true, dataFile)
1828 // Message parsed without error. Keep the result to avoid parsing the message again.
1830 err = part.Walk(c.log.Logger, nil)
1832 c.msgsmtputf8 = c.isSMTPUTF8Required(part)
1835 if c.smtputf8 != c.msgsmtputf8 {
1836 c.log.Debug("smtputf8 flag changed", slog.Bool("smtputf8", c.smtputf8), slog.Bool("msgsmtputf8", c.msgsmtputf8))
1839 if !c.smtputf8 && c.msgsmtputf8 && mox.Pedantic {
1840 metricSubmission.WithLabelValues("missingsmtputf8").Inc()
1841 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeMsg6Other0, "smtputf8 extension is required but was not added to the MAIL command")
1844 // Prepare "Received" header.
1848 var iprevStatus iprev.Status // Only for delivery, not submission.
1849 var iprevAuthentic bool
1851 // Hide internal hosts.
1852 // todo future: make this a config option, where admins specify ip ranges that they don't want exposed. also see
../rfc/5321:4321
1853 recvFrom = message.HeaderCommentDomain(mox.Conf.Static.HostnameDomain, c.msgsmtputf8)
1855 if len(c.hello.IP) > 0 {
1856 recvFrom = smtp.AddressLiteral(c.hello.IP)
1858 // ASCII-only version added after the extended-domain syntax below, because the
1859 // comment belongs to "BY" which comes immediately after "FROM".
1860 recvFrom = c.hello.Domain.XName(c.msgsmtputf8)
1862 iprevctx, iprevcancel := context.WithTimeout(cmdctx, time.Minute)
1864 var revNames []string
1865 iprevStatus, revName, revNames, iprevAuthentic, err = iprev.Lookup(iprevctx, c.resolver, c.remoteIP)
1868 c.log.Infox("reverse-forward lookup", err, slog.Any("remoteip", c.remoteIP))
1870 c.log.Debug("dns iprev check", slog.Any("addr", c.remoteIP), slog.Any("status", iprevStatus))
1874 } else if len(revNames) > 0 {
1877 name = strings.TrimSuffix(name, ".")
1879 if name != "" && name != c.hello.Domain.XName(c.msgsmtputf8) {
1880 recvFrom += name + " "
1882 recvFrom += smtp.AddressLiteral(c.remoteIP) + ")"
1883 if c.msgsmtputf8 && c.hello.Domain.Unicode != "" {
1884 recvFrom += " (" + c.hello.Domain.ASCII + ")"
1887 recvBy := mox.Conf.Static.HostnameDomain.XName(c.msgsmtputf8)
1888 recvBy += " (" + smtp.AddressLiteral(c.localIP) + ")" // todo: hide ip if internal?
1889 if c.msgsmtputf8 && mox.Conf.Static.HostnameDomain.Unicode != "" {
1890 // This syntax is part of "VIA".
1891 recvBy += " (" + mox.Conf.Static.HostnameDomain.ASCII + ")"
1904 if c.account != nil {
1909 // Assume transaction does not succeed. If it does, we'll compensate.
1912 recvHdrFor := func(rcptTo string) string {
1913 recvHdr := &message.HeaderWriter{}
1914 // For additional Received-header clauses, see:
1915 // https://www.iana.org/assignments/mail-parameters/mail-parameters.xhtml#table-mail-parameters-8
1917 if c.requireTLS != nil && *c.requireTLS {
1919 withComment = " (requiretls)"
1921 recvHdr.Add(" ", "Received:", "from", recvFrom, "by", recvBy, "via", "tcp", "with", with+withComment, "id", mox.ReceivedID(c.cid)) //
../rfc/5321:3158
1923 tlsConn := c.conn.(*tls.Conn)
1924 tlsComment := mox.TLSReceivedComment(c.log, tlsConn.ConnectionState())
1925 recvHdr.Add(" ", tlsComment...)
1927 // We leave out an empty "for" clause. This is empty for messages submitted to
1928 // multiple recipients, so the message stays identical and a single smtp
1929 // transaction can deliver, only transferring the data once.
1931 recvHdr.Add(" ", "for", "<"+rcptTo+">;")
1933 recvHdr.Add(" ", time.Now().Format(message.RFC5322Z))
1934 return recvHdr.String()
1937 // Submission is easiest because user is trusted. Far fewer checks to make. So
1938 // handle it first, and leave the rest of the function for handling wild west
1939 // internet traffic.
1941 c.submit(cmdctx, recvHdrFor, msgWriter, dataFile, part)
1943 c.deliver(cmdctx, recvHdrFor, msgWriter, iprevStatus, iprevAuthentic, dataFile)
1947// Check if a message has unambiguous "TLS-Required: No" header. Messages must not
1948// contain multiple TLS-Required headers. The only valid value is "no". But we'll
1949// accept multiple headers as long as all they are all "no".
1951func hasTLSRequiredNo(h textproto.MIMEHeader) bool {
1952 l := h.Values("Tls-Required")
1956 for _, v := range l {
1957 if !strings.EqualFold(v, "no") {
1964// submit is used for mail from authenticated users that we will try to deliver.
1965func (c *conn) submit(ctx context.Context, recvHdrFor func(string) string, msgWriter *message.Writer, dataFile *os.File, part *message.Part) {
1966 // Similar between ../smtpserver/server.go:/submit\( and ../webmail/api.go:/MessageSubmit\( and ../webapisrv/server.go:/Send\(
1968 var msgPrefix []byte
1970 // Check that user is only sending email as one of its configured identities. Not
1974 msgFrom, _, header, err := message.From(c.log.Logger, true, dataFile, part)
1976 metricSubmission.WithLabelValues("badmessage").Inc()
1977 c.log.Infox("parsing message From address", err, slog.String("user", c.username))
1978 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeMsg6Other0, "cannot parse header or From address: %v", err)
1980 if !mox.AllowMsgFrom(c.account.Name, msgFrom) {
1982 metricSubmission.WithLabelValues("badfrom").Inc()
1983 c.log.Infox("verifying message from address", mox.ErrAddressNotFound, slog.String("user", c.username), slog.Any("msgfrom", msgFrom))
1984 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SePol7DeliveryUnauth1, "message from address must belong to authenticated user")
1987 // TLS-Required: No header makes us not enforce recipient domain's TLS policy.
1990 if c.requireTLS == nil && hasTLSRequiredNo(header) {
1995 // Outgoing messages should not have a Return-Path header. The final receiving mail
1996 // server will add it.
1998 if mox.Pedantic && header.Values("Return-Path") != nil {
1999 metricSubmission.WithLabelValues("badheader").Inc()
2000 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeMsg6Other0, "message should not have Return-Path header")
2003 // Add Message-Id header if missing.
2005 messageID := header.Get("Message-Id")
2006 if messageID == "" {
2007 messageID = mox.MessageIDGen(c.msgsmtputf8)
2008 msgPrefix = append(msgPrefix, fmt.Sprintf("Message-Id: <%s>\r\n", messageID)...)
2012 if header.Get("Date") == "" {
2013 msgPrefix = append(msgPrefix, "Date: "+time.Now().Format(message.RFC5322Z)+"\r\n"...)
2016 // Check outgoing message rate limit.
2017 err = c.account.DB.Read(ctx, func(tx *bstore.Tx) error {
2018 rcpts := make([]smtp.Path, len(c.recipients))
2019 for i, r := range c.recipients {
2022 msglimit, rcptlimit, err := c.account.SendLimitReached(tx, rcpts)
2023 xcheckf(err, "checking sender limit")
2025 metricSubmission.WithLabelValues("messagelimiterror").Inc()
2026 xsmtpUserErrorf(smtp.C451LocalErr, smtp.SePol7DeliveryUnauth1, "max number of messages (%d) over past 24h reached, try increasing per-account setting MaxOutgoingMessagesPerDay", msglimit)
2027 } else if rcptlimit >= 0 {
2028 metricSubmission.WithLabelValues("recipientlimiterror").Inc()
2029 xsmtpUserErrorf(smtp.C451LocalErr, smtp.SePol7DeliveryUnauth1, "max number of new/first-time recipients (%d) over past 24h reached, try increasing per-account setting MaxFirstTimeRecipientsPerDay", rcptlimit)
2033 xcheckf(err, "read-only transaction")
2035 // We gather any X-Mox-Extra-* headers into the "extra" data during queueing, which
2036 // will make it into any webhook we deliver.
2037 // todo: remove the X-Mox-Extra-* headers from the message. we don't currently rewrite the message...
2038 // todo: should we not canonicalize keys?
2039 var extra map[string]string
2040 for k, vl := range header {
2041 if !strings.HasPrefix(k, "X-Mox-Extra-") {
2045 extra = map[string]string{}
2047 xk := k[len("X-Mox-Extra-"):]
2048 // We don't allow duplicate keys.
2049 if _, ok := extra[xk]; ok || len(vl) > 1 {
2050 xsmtpUserErrorf(smtp.C554TransactionFailed, smtp.SeMsg6Other0, "duplicate x-mox-extra- key %q", xk)
2052 extra[xk] = vl[len(vl)-1]
2055 // 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.
2057 // Add DKIM signatures.
2058 confDom, ok := mox.Conf.Domain(msgFrom.Domain)
2060 c.log.Error("domain disappeared", slog.Any("domain", msgFrom.Domain))
2061 xsmtpServerErrorf(codes{smtp.C451LocalErr, smtp.SeSys3Other0}, "internal error")
2064 selectors := mox.DKIMSelectors(confDom.DKIM)
2065 if len(selectors) > 0 {
2066 canonical := mox.CanonicalLocalpart(msgFrom.Localpart, confDom)
2067 if dkimHeaders, err := dkim.Sign(ctx, c.log.Logger, canonical, msgFrom.Domain, selectors, c.msgsmtputf8, store.FileMsgReader(msgPrefix, dataFile)); err != nil {
2068 c.log.Errorx("dkim sign for domain", err, slog.Any("domain", msgFrom.Domain))
2069 metricServerErrors.WithLabelValues("dkimsign").Inc()
2071 msgPrefix = append(msgPrefix, []byte(dkimHeaders)...)
2075 authResults := message.AuthResults{
2076 Hostname: mox.Conf.Static.HostnameDomain.XName(c.msgsmtputf8),
2077 Comment: mox.Conf.Static.HostnameDomain.ASCIIExtra(c.msgsmtputf8),
2078 Methods: []message.AuthMethod{
2082 Props: []message.AuthProp{
2083 message.MakeAuthProp("smtp", "mailfrom", c.mailFrom.XString(c.msgsmtputf8), true, c.mailFrom.ASCIIExtra(c.msgsmtputf8)),
2088 msgPrefix = append(msgPrefix, []byte(authResults.Header())...)
2090 // We always deliver through the queue. It would be more efficient to deliver
2091 // directly for local accounts, but we don't want to circumvent all the anti-spam
2092 // measures. Accounts on a single mox instance should be allowed to block each
2095 accConf, _ := c.account.Conf()
2096 loginAddr, err := smtp.ParseAddress(c.username)
2097 xcheckf(err, "parsing login address")
2098 useFromID := slices.Contains(accConf.ParsedFromIDLoginAddresses, loginAddr)
2099 var localpartBase string
2103 // With submission, user can bring their own fromid.
2104 t := strings.SplitN(string(c.mailFrom.Localpart), confDom.LocalpartCatchallSeparator, 2)
2105 localpartBase = t[0]
2108 if fromID != "" && len(c.recipients) > 1 {
2109 xsmtpServerErrorf(codes{smtp.C554TransactionFailed, smtp.SeProto5TooManyRcpts3}, "cannot send to multiple recipients with chosen fromid")
2116 qml := make([]queue.Msg, len(c.recipients))
2117 for i, rcpt := range c.recipients {
2119 code, timeout := mox.LocalserveNeedsError(rcpt.addr.Localpart)
2121 c.log.Info("timing out submission due to special localpart")
2122 mox.Sleep(mox.Context, time.Hour)
2123 xsmtpServerErrorf(codes{smtp.C451LocalErr, smtp.SeSys3Other0}, "timing out submission due to special localpart")
2124 } else if code != 0 {
2125 c.log.Info("failure due to special localpart", slog.Int("code", code))
2126 xsmtpServerErrorf(codes{code, smtp.SeOther00}, "failure with code %d due to special localpart", code)
2133 fromID = xrandomID(16)
2135 fp.Localpart = smtp.Localpart(localpartBase + confDom.LocalpartCatchallSeparator + fromID)
2138 // For multiple recipients, we don't make each message prefix unique, leaving out
2139 // the "for" clause in the Received header. This allows the queue to deliver the
2140 // messages in a single smtp transaction.
2142 if len(c.recipients) == 1 {
2143 rcptTo = rcpt.addr.String()
2145 xmsgPrefix := append([]byte(recvHdrFor(rcptTo)), msgPrefix...)
2146 msgSize := int64(len(xmsgPrefix)) + msgWriter.Size
2147 qm := queue.MakeMsg(fp, rcpt.addr, msgWriter.Has8bit, c.msgsmtputf8, msgSize, messageID, xmsgPrefix, c.requireTLS, now, header.Get("Subject"))
2148 if !c.futureRelease.IsZero() {
2149 qm.NextAttempt = c.futureRelease
2150 qm.FutureReleaseRequest = c.futureReleaseRequest
2157 // 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
2158 if err := queue.Add(ctx, c.log, c.account.Name, dataFile, qml...); err != nil && errors.Is(err, queue.ErrFromID) && !genFromID {
2159 // todo: should we return this error during the "rcpt to" command?
2160 // secode is not an exact match, but seems closest.
2161 xsmtpServerErrorf(errCodes(smtp.C554TransactionFailed, smtp.SeAddr1SenderSyntax7, err), "bad fromid in smtp mail from address: %s", err)
2162 } else if err != nil {
2163 // Aborting the transaction is not great. But continuing and generating DSNs will
2164 // probably result in errors as well...
2165 metricSubmission.WithLabelValues("queueerror").Inc()
2166 c.log.Errorx("queuing message", err)
2167 xsmtpServerErrorf(errCodes(smtp.C451LocalErr, smtp.SeSys3Other0, err), "error delivering message: %v", err)
2169 metricSubmission.WithLabelValues("ok").Inc()
2170 for i, rcpt := range c.recipients {
2171 c.log.Info("messages queued for delivery",
2172 slog.Any("mailfrom", *c.mailFrom),
2173 slog.Any("rcptto", rcpt.addr),
2174 slog.Bool("smtputf8", c.smtputf8),
2175 slog.Bool("msgsmtputf8", c.msgsmtputf8),
2176 slog.Int64("msgsize", qml[i].Size))
2179 err = c.account.DB.Write(ctx, func(tx *bstore.Tx) error {
2180 for _, rcpt := range c.recipients {
2181 outgoing := store.Outgoing{Recipient: rcpt.addr.XString(true)}
2182 if err := tx.Insert(&outgoing); err != nil {
2183 return fmt.Errorf("adding outgoing message: %v", err)
2188 xcheckf(err, "adding outgoing messages")
2191 c.transactionBad-- // Compensate for early earlier pessimistic increase.
2194 c.writecodeline(smtp.C250Completed, smtp.SeMailbox2Other0, "it is done", nil)
2197func xrandomID(n int) string {
2198 return base64.RawURLEncoding.EncodeToString(xrandom(n))
2201func xrandom(n int) []byte {
2202 buf := make([]byte, n)
2203 x, err := cryptorand.Read(buf)
2204 xcheckf(err, "read random")
2206 xcheckf(errors.New("short random read"), "read random")
2211func ipmasked(ip net.IP) (string, string, string) {
2212 if ip.To4() != nil {
2214 m2 := ip.Mask(net.CIDRMask(26, 32)).String()
2215 m3 := ip.Mask(net.CIDRMask(21, 32)).String()
2218 m1 := ip.Mask(net.CIDRMask(64, 128)).String()
2219 m2 := ip.Mask(net.CIDRMask(48, 128)).String()
2220 m3 := ip.Mask(net.CIDRMask(32, 128)).String()
2224func (c *conn) xlocalserveError(lp smtp.Localpart) {
2225 code, timeout := mox.LocalserveNeedsError(lp)
2227 c.log.Info("timing out due to special localpart")
2228 mox.Sleep(mox.Context, time.Hour)
2229 xsmtpServerErrorf(codes{smtp.C451LocalErr, smtp.SeSys3Other0}, "timing out command due to special localpart")
2230 } else if code != 0 {
2231 c.log.Info("failure due to special localpart", slog.Int("code", code))
2232 metricDelivery.WithLabelValues("delivererror", "localserve").Inc()
2233 xsmtpServerErrorf(codes{code, smtp.SeOther00}, "failure with code %d due to special localpart", code)
2237// deliver is called for incoming messages from external, typically untrusted
2238// sources. i.e. not submitted by authenticated users.
2239func (c *conn) deliver(ctx context.Context, recvHdrFor func(string) string, msgWriter *message.Writer, iprevStatus iprev.Status, iprevAuthentic bool, dataFile *os.File) {
2240 // 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.
2242 var msgFrom smtp.Address
2243 var envelope *message.Envelope
2244 var headers textproto.MIMEHeader
2246 part, err := message.Parse(c.log.Logger, false, dataFile)
2248 // todo: is it enough to check only the the content-type header? in other places we look at the content-types of the parts before considering a message a dsn. should we change other places to this simpler check?
2249 isDSN = part.MediaType == "MULTIPART" && part.MediaSubType == "REPORT" && strings.EqualFold(part.ContentTypeParams["report-type"], "delivery-status")
2250 msgFrom, envelope, headers, err = message.From(c.log.Logger, false, dataFile, &part)
2253 c.log.Infox("parsing message for From address", err)
2257 if len(headers.Values("Received")) > 100 {
2258 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeNet4Loop6, "loop detected, more than 100 Received headers")
2261 // TLS-Required: No header makes us not enforce recipient domain's TLS policy.
2262 // Since we only deliver locally at the moment, this won't influence our behaviour.
2263 // Once we forward, it would our delivery attempts.
2266 if c.requireTLS == nil && hasTLSRequiredNo(headers) {
2271 // We'll be building up an Authentication-Results header.
2272 authResults := message.AuthResults{
2273 Hostname: mox.Conf.Static.HostnameDomain.XName(c.msgsmtputf8),
2276 commentAuthentic := func(v bool) string {
2278 return "with dnssec"
2280 return "without dnssec"
2283 // Reverse IP lookup results.
2284 // todo future: how useful is this?
2286 authResults.Methods = append(authResults.Methods, message.AuthMethod{
2288 Result: string(iprevStatus),
2289 Comment: commentAuthentic(iprevAuthentic),
2290 Props: []message.AuthProp{
2291 message.MakeAuthProp("policy", "iprev", c.remoteIP.String(), false, ""),
2295 // SPF and DKIM verification in parallel.
2296 var wg sync.WaitGroup
2300 var dkimResults []dkim.Result
2304 x := recover() // Should not happen, but don't take program down if it does.
2306 c.log.Error("dkim verify panic", slog.Any("err", x))
2308 metrics.PanicInc(metrics.Dkimverify)
2312 // We always evaluate all signatures. We want to build up reputation for each
2313 // domain in the signature.
2314 const ignoreTestMode = false
2315 // todo future: longer timeout? we have to read through the entire email, which can be large, possibly multiple times.
2316 dkimctx, dkimcancel := context.WithTimeout(ctx, time.Minute)
2318 // todo future: we could let user configure which dkim headers they require
2320 // For localserve, fake dkim selector DNS records for hosted domains to give
2321 // dkim-signatures a chance to pass for deliveries from queue.
2322 resolver := c.resolver
2324 // Lookup based on message From address is an approximation.
2325 if dc, ok := mox.Conf.Domain(msgFrom.Domain); ok && len(dc.DKIM.Selectors) > 0 {
2326 txts := map[string][]string{}
2327 for name, sel := range dc.DKIM.Selectors {
2328 dkimr := dkim.Record{
2330 Hashes: []string{sel.HashEffective},
2331 PublicKey: sel.Key.Public(),
2333 if _, ok := sel.Key.(ed25519.PrivateKey); ok {
2334 dkimr.Key = "ed25519"
2335 } else if _, ok := sel.Key.(*rsa.PrivateKey); !ok {
2336 err := fmt.Errorf("unrecognized private key for DKIM selector %q: %T", name, sel.Key)
2337 xcheckf(err, "making dkim record")
2339 txt, err := dkimr.Record()
2340 xcheckf(err, "making DKIM DNS TXT record")
2341 txts[name+"._domainkey."+msgFrom.Domain.ASCII+"."] = []string{txt}
2343 resolver = dns.MockResolver{TXT: txts}
2346 dkimResults, dkimErr = dkim.Verify(dkimctx, c.log.Logger, resolver, c.msgsmtputf8, dkim.DefaultPolicy, dataFile, ignoreTestMode)
2352 var receivedSPF spf.Received
2353 var spfDomain dns.Domain
2355 var spfAuthentic bool
2357 spfArgs := spf.Args{
2358 RemoteIP: c.remoteIP,
2359 MailFromLocalpart: c.mailFrom.Localpart,
2360 MailFromDomain: c.mailFrom.IPDomain.Domain, // Can be empty.
2361 HelloDomain: c.hello,
2363 LocalHostname: c.hostname,
2368 x := recover() // Should not happen, but don't take program down if it does.
2370 c.log.Error("spf verify panic", slog.Any("err", x))
2372 metrics.PanicInc(metrics.Spfverify)
2376 spfctx, spfcancel := context.WithTimeout(ctx, time.Minute)
2378 resolver := c.resolver
2379 // For localserve, give hosted domains a chance to pass for deliveries from queue.
2380 if Localserve && c.remoteIP.IsLoopback() {
2381 // Lookup based on message From address is an approximation.
2382 if _, ok := mox.Conf.Domain(msgFrom.Domain); ok {
2383 resolver = dns.MockResolver{
2384 TXT: map[string][]string{msgFrom.Domain.ASCII + ".": {"v=spf1 ip4:127.0.0.1/8 ip6:::1 ~all"}},
2388 receivedSPF, spfDomain, spfExpl, spfAuthentic, spfErr = spf.Verify(spfctx, c.log.Logger, resolver, spfArgs)
2391 c.log.Infox("spf verify", spfErr)
2395 // Wait for DKIM and SPF validation to finish.
2398 // Give immediate response if all recipients are unknown.
2400 for _, r := range c.recipients {
2401 if r.account == nil && r.alias == nil {
2405 if nunknown == len(c.recipients) {
2406 // During RCPT TO we found that the address does not exist.
2407 c.log.Info("deliver attempt to unknown user(s)", slog.Any("recipients", c.recipients))
2409 // Crude attempt to slow down someone trying to guess names. Would work better
2410 // with connection rate limiter.
2411 if unknownRecipientsDelay > 0 {
2412 mox.Sleep(ctx, unknownRecipientsDelay)
2415 // 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.
2416 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeAddr1UnknownDestMailbox1, "no such user(s)")
2419 // Add DKIM results to Authentication-Results header.
2420 authResAddDKIM := func(result, comment, reason string, props []message.AuthProp) {
2421 dm := message.AuthMethod{
2428 authResults.Methods = append(authResults.Methods, dm)
2431 c.log.Errorx("dkim verify", dkimErr)
2432 authResAddDKIM("none", "", dkimErr.Error(), nil)
2433 } else if len(dkimResults) == 0 {
2434 c.log.Info("no dkim-signature header", slog.Any("mailfrom", c.mailFrom))
2435 authResAddDKIM("none", "", "no dkim signatures", nil)
2437 for i, r := range dkimResults {
2438 var domain, selector dns.Domain
2439 var identity *dkim.Identity
2441 var props []message.AuthProp
2443 if r.Record != nil && r.Record.PublicKey != nil {
2444 if pubkey, ok := r.Record.PublicKey.(*rsa.PublicKey); ok {
2445 comment = fmt.Sprintf("%d bit rsa, ", pubkey.N.BitLen())
2449 sig := base64.StdEncoding.EncodeToString(r.Sig.Signature)
2450 sig = sig[:12] // Must be at least 8 characters and unique among the signatures.
2451 props = []message.AuthProp{
2452 message.MakeAuthProp("header", "d", r.Sig.Domain.XName(c.msgsmtputf8), true, r.Sig.Domain.ASCIIExtra(c.msgsmtputf8)),
2453 message.MakeAuthProp("header", "s", r.Sig.Selector.XName(c.msgsmtputf8), true, r.Sig.Selector.ASCIIExtra(c.msgsmtputf8)),
2454 message.MakeAuthProp("header", "a", r.Sig.Algorithm(), false, ""),
2457 domain = r.Sig.Domain
2458 selector = r.Sig.Selector
2459 if r.Sig.Identity != nil {
2460 props = append(props, message.MakeAuthProp("header", "i", r.Sig.Identity.String(), true, ""))
2461 identity = r.Sig.Identity
2463 if r.RecordAuthentic {
2464 comment += "with dnssec"
2466 comment += "without dnssec"
2471 errmsg = r.Err.Error()
2473 authResAddDKIM(string(r.Status), comment, errmsg, props)
2474 c.log.Debugx("dkim verification result", r.Err,
2475 slog.Int("index", i),
2476 slog.Any("mailfrom", c.mailFrom),
2477 slog.Any("status", r.Status),
2478 slog.Any("domain", domain),
2479 slog.Any("selector", selector),
2480 slog.Any("identity", identity))
2484 var spfIdentity *dns.Domain
2485 var mailFromValidation = store.ValidationUnknown
2486 var ehloValidation = store.ValidationUnknown
2487 switch receivedSPF.Identity {
2488 case spf.ReceivedHELO:
2489 if len(spfArgs.HelloDomain.IP) == 0 {
2490 spfIdentity = &spfArgs.HelloDomain.Domain
2492 ehloValidation = store.SPFValidation(receivedSPF.Result)
2493 case spf.ReceivedMailFrom:
2494 spfIdentity = &spfArgs.MailFromDomain
2495 mailFromValidation = store.SPFValidation(receivedSPF.Result)
2497 var props []message.AuthProp
2498 if spfIdentity != nil {
2499 props = []message.AuthProp{message.MakeAuthProp("smtp", string(receivedSPF.Identity), spfIdentity.XName(c.msgsmtputf8), true, spfIdentity.ASCIIExtra(c.msgsmtputf8))}
2501 var spfComment string
2503 spfComment = "with dnssec"
2505 spfComment = "without dnssec"
2507 authResults.Methods = append(authResults.Methods, message.AuthMethod{
2509 Result: string(receivedSPF.Result),
2510 Comment: spfComment,
2513 switch receivedSPF.Result {
2514 case spf.StatusPass:
2515 c.log.Debug("spf pass", slog.Any("ip", spfArgs.RemoteIP), slog.String("mailfromdomain", spfArgs.MailFromDomain.ASCII)) // todo: log the domain that was actually verified.
2516 case spf.StatusFail:
2519 for _, b := range []byte(spfExpl) {
2520 if b < ' ' || b >= 0x7f {
2526 if len(spfExpl) > 800 {
2527 spfExpl = spfExpl[:797] + "..."
2529 spfExpl = "remote claims: " + spfExpl
2533 spfExpl = fmt.Sprintf("your ip %s is not on the SPF allowlist for domain %s", spfArgs.RemoteIP, spfDomain.ASCII)
2535 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?
2536 case spf.StatusTemperror:
2537 c.log.Infox("spf temperror", spfErr)
2538 case spf.StatusPermerror:
2539 c.log.Infox("spf permerror", spfErr)
2540 case spf.StatusNone, spf.StatusNeutral, spf.StatusSoftfail:
2542 c.log.Error("unknown spf status, treating as None/Neutral", slog.Any("status", receivedSPF.Result))
2543 receivedSPF.Result = spf.StatusNone
2548 var dmarcResult dmarc.Result
2549 const applyRandomPercentage = true
2550 // dmarcMethod is added to authResults when delivering to recipients: accounts can
2551 // have different policy override rules.
2552 var dmarcMethod message.AuthMethod
2553 var msgFromValidation = store.ValidationNone
2554 if msgFrom.IsZero() {
2555 dmarcResult.Status = dmarc.StatusNone
2556 dmarcMethod = message.AuthMethod{
2558 Result: string(dmarcResult.Status),
2561 msgFromValidation = alignment(ctx, c.log, msgFrom.Domain, dkimResults, receivedSPF.Result, spfIdentity)
2563 // We are doing the DMARC evaluation now. But we only store it for inclusion in an
2564 // aggregate report when we actually use it. We use an evaluation for each
2565 // recipient, with each a potentially different result due to mailing
2566 // list/forwarding configuration. If we reject a message due to being spam, we
2567 // don't want to spend any resources for the sender domain, and we don't want to
2568 // give the sender any more information about us, so we won't record the
2570 // 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.
2572 dmarcctx, dmarccancel := context.WithTimeout(ctx, time.Minute)
2574 dmarcUse, dmarcResult = dmarc.Verify(dmarcctx, c.log.Logger, c.resolver, msgFrom.Domain, dkimResults, receivedSPF.Result, spfIdentity, applyRandomPercentage)
2577 if dmarcResult.RecordAuthentic {
2578 comment = "with dnssec"
2580 comment = "without dnssec"
2582 dmarcMethod = message.AuthMethod{
2584 Result: string(dmarcResult.Status),
2586 Props: []message.AuthProp{
2588 message.MakeAuthProp("header", "from", msgFrom.Domain.ASCII, true, msgFrom.Domain.ASCIIExtra(c.msgsmtputf8)),
2592 if dmarcResult.Status == dmarc.StatusPass && msgFromValidation == store.ValidationRelaxed {
2593 msgFromValidation = store.ValidationDMARC
2596 // todo future: consider enforcing an spf (soft)fail if there is no dmarc policy or the dmarc policy is none.
../rfc/7489:1507
2598 c.log.Debug("dmarc verification", slog.Any("result", dmarcResult.Status), slog.Any("domain", msgFrom.Domain))
2600 // Prepare for analyzing content, calculating reputation.
2601 ipmasked1, ipmasked2, ipmasked3 := ipmasked(c.remoteIP)
2602 var verifiedDKIMDomains []string
2603 dkimSeen := map[string]bool{}
2604 for _, r := range dkimResults {
2605 // A message can have multiple signatures for the same identity. For example when
2606 // signing the message multiple times with different algorithms (rsa and ed25519).
2607 if r.Status != dkim.StatusPass {
2610 d := r.Sig.Domain.Name()
2613 verifiedDKIMDomains = append(verifiedDKIMDomains, d)
2617 // When we deliver, we try to remove from rejects mailbox based on message-id.
2618 // We'll parse it when we need it, but it is the same for each recipient.
2619 var messageID string
2620 var parsedMessageID bool
2622 // We build up a DSN for each failed recipient. If we have recipients in dsnMsg
2623 // after processing, we queue the DSN. Unless all recipients failed, in which case
2624 // we may just fail the mail transaction instead (could be common for failure to
2625 // deliver to a single recipient, e.g. for junk mail).
2627 type deliverError struct {
2634 var deliverErrors []deliverError
2635 addError := func(rcpt recipient, code int, secode string, userError bool, errmsg string) {
2636 e := deliverError{rcpt.addr, code, secode, userError, errmsg}
2637 c.log.Info("deliver error",
2638 slog.Any("rcptto", e.rcptTo),
2639 slog.Int("code", code),
2640 slog.String("secode", "secode"),
2641 slog.Bool("usererror", userError),
2642 slog.String("errmsg", errmsg))
2643 deliverErrors = append(deliverErrors, e)
2646 // Sort recipients: local accounts, aliases, unknown. For ensuring we don't deliver
2647 // to an alias destination that was also explicitly sent to.
2648 rcptScore := func(r recipient) int {
2649 if r.account != nil {
2651 } else if r.alias != nil {
2656 sort.SliceStable(c.recipients, func(i, j int) bool {
2657 return rcptScore(c.recipients[i]) < rcptScore(c.recipients[j])
2660 // Return whether address is a regular explicit recipient in this transaction. Used
2661 // to prevent delivering a message to an address both for alias and explicit
2662 // addressee. Relies on c.recipients being sorted as above.
2663 regularRecipient := func(addr smtp.Path) bool {
2664 for _, rcpt := range c.recipients {
2665 if rcpt.account == nil {
2667 } else if rcpt.addr.Equal(addr) {
2674 // Prepare a message, analyze it against account's junk filter.
2675 // The returned analysis has an open account that must be closed by the caller.
2676 // We call this for all alias destinations, also when we already delivered to that
2677 // recipient: It may be the only recipient that would allow the message.
2678 messageAnalyze := func(log mlog.Log, smtpRcptTo, deliverTo smtp.Path, accountName string, destination config.Destination, canonicalAddr string) (a *analysis, rerr error) {
2679 acc, err := store.OpenAccount(log, accountName)
2681 log.Errorx("open account", err, slog.Any("account", accountName))
2682 metricDelivery.WithLabelValues("accounterror", "").Inc()
2688 log.Check(err, "closing account during analysis")
2693 Received: time.Now(),
2694 RemoteIP: c.remoteIP.String(),
2695 RemoteIPMasked1: ipmasked1,
2696 RemoteIPMasked2: ipmasked2,
2697 RemoteIPMasked3: ipmasked3,
2698 EHLODomain: c.hello.Domain.Name(),
2699 MailFrom: c.mailFrom.String(),
2700 MailFromLocalpart: c.mailFrom.Localpart,
2701 MailFromDomain: c.mailFrom.IPDomain.Domain.Name(),
2702 RcptToLocalpart: smtpRcptTo.Localpart,
2703 RcptToDomain: smtpRcptTo.IPDomain.Domain.Name(),
2704 MsgFromLocalpart: msgFrom.Localpart,
2705 MsgFromDomain: msgFrom.Domain.Name(),
2706 MsgFromOrgDomain: publicsuffix.Lookup(ctx, log.Logger, msgFrom.Domain).Name(),
2707 EHLOValidated: ehloValidation == store.ValidationPass,
2708 MailFromValidated: mailFromValidation == store.ValidationPass,
2709 MsgFromValidated: msgFromValidation == store.ValidationStrict || msgFromValidation == store.ValidationDMARC || msgFromValidation == store.ValidationRelaxed,
2710 EHLOValidation: ehloValidation,
2711 MailFromValidation: mailFromValidation,
2712 MsgFromValidation: msgFromValidation,
2713 DKIMDomains: verifiedDKIMDomains,
2715 Size: msgWriter.Size,
2718 tlsState := c.conn.(*tls.Conn).ConnectionState()
2719 m.ReceivedTLSVersion = tlsState.Version
2720 m.ReceivedTLSCipherSuite = tlsState.CipherSuite
2721 if c.requireTLS != nil {
2722 m.ReceivedRequireTLS = *c.requireTLS
2725 m.ReceivedTLSVersion = 1 // Signals plain text delivery.
2728 var msgTo, msgCc []message.Address
2729 if envelope != nil {
2733 d := delivery{c.tls, &m, dataFile, smtpRcptTo, deliverTo, destination, canonicalAddr, acc, msgTo, msgCc, msgFrom, c.dnsBLs, dmarcUse, dmarcResult, dkimResults, iprevStatus}
2735 r := analyze(ctx, log, c.resolver, d)
2739 // Either deliver the message, or call addError to register the recipient as failed.
2740 // If recipient is an alias, we may be delivering to multiple address/accounts and
2741 // we will consider a message delivered if we delivered it to at least one account
2742 // (others may be over quota).
2743 processRecipient := func(rcpt recipient) {
2744 log := c.log.With(slog.Any("mailfrom", c.mailFrom), slog.Any("rcptto", rcpt.addr))
2746 // If this is not a valid local user, we send back a DSN. This can only happen when
2747 // there are also valid recipients, and only when remote is SPF-verified, so the DSN
2748 // should not cause backscatter.
2749 // In case of serious errors, we abort the transaction. We may have already
2750 // delivered some messages. Perhaps it would be better to continue with other
2751 // deliveries, and return an error at the end? Though the failure conditions will
2752 // probably prevent any other successful deliveries too...
2754 if rcpt.account == nil && rcpt.alias == nil {
2755 metricDelivery.WithLabelValues("unknownuser", "").Inc()
2756 addError(rcpt, smtp.C550MailboxUnavail, smtp.SeAddr1UnknownDestMailbox1, true, "no such user")
2760 // la holds all analysis, and message preparation, for all accounts (multiple for
2761 // aliases). Each has an open account that we we close on return.
2764 for _, a := range la {
2765 err := a.d.acc.Close()
2766 log.Check(err, "close account")
2770 // For aliases, we prepare & analyze for each recipient. We accept the message if
2771 // any recipient accepts it. Regular destination have just a single account to
2772 // check. We check all alias destinations, even if we already explicitly delivered
2773 // to them: they may be the only destination that would accept the message.
2774 var a0 *analysis // Analysis we've used for accept/reject decision.
2775 if rcpt.alias != nil {
2776 // Check if msgFrom address is acceptable. This doesn't take validation into
2777 // consideration. If the header was forged, the message may be rejected later on.
2778 if !aliasAllowedMsgFrom(rcpt.alias.alias, msgFrom) {
2779 addError(rcpt, smtp.C550MailboxUnavail, smtp.SePol7ExpnProhibited2, true, "not allowed to send to destination")
2783 la = make([]analysis, 0, len(rcpt.alias.alias.ParsedAddresses))
2784 for _, aa := range rcpt.alias.alias.ParsedAddresses {
2785 a, err := messageAnalyze(log, rcpt.addr, aa.Address.Path(), aa.AccountName, aa.Destination, rcpt.alias.canonicalAddress)
2787 addError(rcpt, smtp.C451LocalErr, smtp.SeSys3Other0, false, "error processing")
2791 if a.accept && a0 == nil {
2792 // Address that caused us to accept.
2797 // First address, for rejecting.
2801 a, err := messageAnalyze(log, rcpt.addr, rcpt.addr, rcpt.account.accountName, rcpt.account.destination, rcpt.account.canonicalAddress)
2803 addError(rcpt, smtp.C451LocalErr, smtp.SeSys3Other0, false, "error processing")
2810 if !a0.accept && a0.reason == reasonHighRate {
2811 log.Info("incoming message rejected for high rate, not storing in rejects mailbox", slog.String("reason", a0.reason), slog.Any("msgfrom", msgFrom))
2812 metricDelivery.WithLabelValues("reject", a0.reason).Inc()
2814 addError(rcpt, a0.code, a0.secode, a0.userError, a0.errmsg)
2818 // Any DMARC result override is stored in the evaluation for outgoing DMARC
2819 // aggregate reports, and added to the Authentication-Results message header.
2820 // We want to tell the sender that we have an override, e.g. for mailing lists, so
2821 // they don't overestimate the potential damage of switching from p=none to
2823 var dmarcOverrides []string
2824 if a0.dmarcOverrideReason != "" {
2825 dmarcOverrides = []string{a0.dmarcOverrideReason}
2827 if dmarcResult.Record != nil && !dmarcUse {
2828 dmarcOverrides = append(dmarcOverrides, string(dmarcrpt.PolicyOverrideSampledOut))
2831 // Add per-recipient DMARC method to Authentication-Results. Each account can have
2832 // their own override rules, e.g. based on configured mailing lists/forwards.
2834 rcptDMARCMethod := dmarcMethod
2835 if len(dmarcOverrides) > 0 {
2836 if rcptDMARCMethod.Comment != "" {
2837 rcptDMARCMethod.Comment += ", "
2839 rcptDMARCMethod.Comment += "override " + strings.Join(dmarcOverrides, ",")
2841 rcptAuthResults := authResults
2842 rcptAuthResults.Methods = append([]message.AuthMethod{}, authResults.Methods...)
2843 rcptAuthResults.Methods = append(rcptAuthResults.Methods, rcptDMARCMethod)
2845 // Prepend reason as message header, for easy display in mail clients.
2847 if a0.reason != "" {
2848 xmox = "X-Mox-Reason: " + a0.reason + "\r\n"
2855 la[i].d.m.MsgPrefix = []byte(
2859 rcptAuthResults.Header() +
2860 receivedSPF.Header() +
2861 recvHdrFor(rcpt.addr.String()),
2863 la[i].d.m.Size += int64(len(la[i].d.m.MsgPrefix))
2866 // Store DMARC evaluation for inclusion in an aggregate report. Only if there is at
2867 // least one reporting address: We don't want to needlessly store a row in a
2868 // database for each delivery attempt. If we reject a message for being junk, we
2869 // are also not going to send it a DMARC report. The DMARC check is done early in
2870 // the analysis, we will report on rejects because of DMARC, because it could be
2871 // valuable feedback about forwarded or mailing list messages.
2873 if !mox.Conf.Static.NoOutgoingDMARCReports && dmarcResult.Record != nil && len(dmarcResult.Record.AggregateReportAddresses) > 0 && (a0.accept && !a0.d.m.IsReject || a0.reason == reasonDMARCPolicy) {
2874 // Disposition holds our decision on whether to accept the message. Not what the
2875 // DMARC evaluation resulted in. We can override, e.g. because of mailing lists,
2876 // forwarding, or local policy.
2877 // We treat quarantine as reject, so never claim to quarantine.
2879 disposition := dmarcrpt.DispositionNone
2881 disposition = dmarcrpt.DispositionReject
2884 // unknownDomain returns whether the sender is domain with which this account has
2885 // not had positive interaction.
2886 unknownDomain := func() (unknown bool) {
2887 err := a0.d.acc.DB.Read(ctx, func(tx *bstore.Tx) (err error) {
2888 // See if we received a non-junk message from this organizational domain.
2889 q := bstore.QueryTx[store.Message](tx)
2890 q.FilterNonzero(store.Message{MsgFromOrgDomain: a0.d.m.MsgFromOrgDomain})
2891 q.FilterEqual("Notjunk", true)
2892 q.FilterEqual("IsReject", false)
2893 exists, err := q.Exists()
2895 return fmt.Errorf("querying for non-junk message from organizational domain: %v", err)
2901 // See if we sent a message to this organizational domain.
2902 qr := bstore.QueryTx[store.Recipient](tx)
2903 qr.FilterNonzero(store.Recipient{OrgDomain: a0.d.m.MsgFromOrgDomain})
2904 exists, err = qr.Exists()
2906 return fmt.Errorf("querying for message sent to organizational domain: %v", err)
2914 log.Errorx("checking if sender is unknown domain, for dmarc aggregate report evaluation", err)
2919 r := dmarcResult.Record
2920 addresses := make([]string, len(r.AggregateReportAddresses))
2921 for i, a := range r.AggregateReportAddresses {
2922 addresses[i] = a.String()
2924 sp := dmarcrpt.Disposition(r.SubdomainPolicy)
2925 if r.SubdomainPolicy == dmarc.PolicyEmpty {
2926 sp = dmarcrpt.Disposition(r.Policy)
2928 eval := dmarcdb.Evaluation{
2929 // Evaluated and IntervalHours set by AddEvaluation.
2930 PolicyDomain: dmarcResult.Domain.Name(),
2932 // Optional evaluations don't cause a report to be sent, but will be included.
2933 // Useful for automated inter-mailer messages, we don't want to get in a reporting
2934 // loop. We also don't want to be used for sending reports to unsuspecting domains
2935 // we have no relation with.
2936 // 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.
2937 Optional: a0.d.destination.DMARCReports || a0.d.destination.HostTLSReports || a0.d.destination.DomainTLSReports || a0.reason == reasonDMARCPolicy && unknownDomain(),
2939 Addresses: addresses,
2941 PolicyPublished: dmarcrpt.PolicyPublished{
2942 Domain: dmarcResult.Domain.Name(),
2943 ADKIM: dmarcrpt.Alignment(r.ADKIM),
2944 ASPF: dmarcrpt.Alignment(r.ASPF),
2945 Policy: dmarcrpt.Disposition(r.Policy),
2946 SubdomainPolicy: sp,
2947 Percentage: r.Percentage,
2948 // We don't save ReportingOptions, we don't do per-message failure reporting.
2950 SourceIP: c.remoteIP.String(),
2951 Disposition: disposition,
2952 AlignedDKIMPass: dmarcResult.AlignedDKIMPass,
2953 AlignedSPFPass: dmarcResult.AlignedSPFPass,
2954 EnvelopeTo: rcpt.addr.IPDomain.String(),
2955 EnvelopeFrom: c.mailFrom.IPDomain.String(),
2956 HeaderFrom: msgFrom.Domain.Name(),
2959 for _, s := range dmarcOverrides {
2960 reason := dmarcrpt.PolicyOverrideReason{Type: dmarcrpt.PolicyOverride(s)}
2961 eval.OverrideReasons = append(eval.OverrideReasons, reason)
2964 // We'll include all signatures for the organizational domain, even if they weren't
2965 // relevant due to strict alignment requirement.
2966 for _, dkimResult := range dkimResults {
2967 if dkimResult.Sig == nil || publicsuffix.Lookup(ctx, log.Logger, msgFrom.Domain) != publicsuffix.Lookup(ctx, log.Logger, dkimResult.Sig.Domain) {
2970 r := dmarcrpt.DKIMAuthResult{
2971 Domain: dkimResult.Sig.Domain.Name(),
2972 Selector: dkimResult.Sig.Selector.ASCII,
2973 Result: dmarcrpt.DKIMResult(dkimResult.Status),
2975 eval.DKIMResults = append(eval.DKIMResults, r)
2978 switch receivedSPF.Identity {
2979 case spf.ReceivedHELO:
2980 spfAuthResult := dmarcrpt.SPFAuthResult{
2981 Domain: spfArgs.HelloDomain.String(), // Can be unicode and also IP.
2982 Scope: dmarcrpt.SPFDomainScopeHelo,
2983 Result: dmarcrpt.SPFResult(receivedSPF.Result),
2985 eval.SPFResults = []dmarcrpt.SPFAuthResult{spfAuthResult}
2986 case spf.ReceivedMailFrom:
2987 spfAuthResult := dmarcrpt.SPFAuthResult{
2988 Domain: spfArgs.MailFromDomain.Name(), // Can be unicode.
2989 Scope: dmarcrpt.SPFDomainScopeMailFrom,
2990 Result: dmarcrpt.SPFResult(receivedSPF.Result),
2992 eval.SPFResults = []dmarcrpt.SPFAuthResult{spfAuthResult}
2995 err := dmarcdb.AddEvaluation(ctx, dmarcResult.Record.AggregateReportingInterval, &eval)
2996 log.Check(err, "adding dmarc evaluation to database for aggregate report")
3000 for _, a := range la {
3001 // Don't add message if address was also explicitly present in a RCPT TO command.
3002 if rcpt.alias != nil && regularRecipient(a.d.deliverTo) {
3006 conf, _ := a.d.acc.Conf()
3007 if conf.RejectsMailbox == "" {
3010 present, _, messagehash, err := rejectPresent(log, a.d.acc, conf.RejectsMailbox, a.d.m, dataFile)
3012 log.Errorx("checking whether reject is already present", err)
3015 log.Info("reject message is already present, ignoring")
3018 a.d.m.IsReject = true
3019 a.d.m.Seen = true // We don't want to draw attention.
3020 // Regular automatic junk flags configuration applies to these messages. The
3021 // default is to treat these as neutral, so they won't cause outright rejections
3022 // due to reputation for later delivery attempts.
3023 a.d.m.MessageHash = messagehash
3024 a.d.acc.WithWLock(func() {
3027 if !conf.KeepRejects {
3028 hasSpace, err = a.d.acc.TidyRejectsMailbox(c.log, conf.RejectsMailbox)
3031 log.Errorx("tidying rejects mailbox", err)
3032 } else if hasSpace {
3033 if err := a.d.acc.DeliverMailbox(log, conf.RejectsMailbox, a.d.m, dataFile); err != nil {
3034 log.Errorx("delivering spammy mail to rejects mailbox", err)
3036 log.Info("delivered spammy mail to rejects mailbox")
3039 log.Info("not storing spammy mail to full rejects mailbox")
3044 log.Info("incoming message rejected", slog.String("reason", a0.reason), slog.Any("msgfrom", msgFrom))
3045 metricDelivery.WithLabelValues("reject", a0.reason).Inc()
3047 addError(rcpt, a0.code, a0.secode, a0.userError, a0.errmsg)
3051 delayFirstTime := true
3052 if rcpt.account != nil && a0.dmarcReport != nil {
3054 if err := dmarcdb.AddReport(ctx, a0.dmarcReport, msgFrom.Domain); err != nil {
3055 log.Errorx("saving dmarc aggregate report in database", err)
3057 log.Info("dmarc aggregate report processed")
3058 a0.d.m.Flags.Seen = true
3059 delayFirstTime = false
3062 if rcpt.account != nil && a0.tlsReport != nil {
3063 // todo future: add rate limiting to prevent DoS attacks.
3064 if err := tlsrptdb.AddReport(ctx, c.log, msgFrom.Domain, c.mailFrom.String(), a0.d.destination.HostTLSReports, a0.tlsReport); err != nil {
3065 log.Errorx("saving TLSRPT report in database", err)
3067 log.Info("tlsrpt report processed")
3068 a0.d.m.Flags.Seen = true
3069 delayFirstTime = false
3073 // If this is a first-time sender and not a forwarded/mailing list message, wait
3074 // before actually delivering. If this turns out to be a spammer, we've kept one of
3075 // their connections busy.
3076 a0conf, _ := a0.d.acc.Conf()
3077 if delayFirstTime && !a0.d.m.IsForward && !a0.d.m.IsMailingList && a0.reason == reasonNoBadSignals && !a0conf.NoFirstTimeSenderDelay && c.firstTimeSenderDelay > 0 {
3078 log.Debug("delaying before delivering from sender without reputation", slog.Duration("delay", c.firstTimeSenderDelay))
3079 mox.Sleep(mox.Context, c.firstTimeSenderDelay)
3083 code, timeout := mox.LocalserveNeedsError(rcpt.addr.Localpart)
3085 log.Info("timing out due to special localpart")
3086 mox.Sleep(mox.Context, time.Hour)
3087 xsmtpServerErrorf(codes{smtp.C451LocalErr, smtp.SeOther00}, "timing out delivery due to special localpart")
3088 } else if code != 0 {
3089 log.Info("failure due to special localpart", slog.Int("code", code))
3090 metricDelivery.WithLabelValues("delivererror", "localserve").Inc()
3091 addError(rcpt, code, smtp.SeOther00, false, fmt.Sprintf("failure with code %d due to special localpart", code))
3096 // Gather the message-id before we deliver and the file may be consumed.
3097 if !parsedMessageID {
3098 if p, err := message.Parse(c.log.Logger, false, store.FileMsgReader(a0.d.m.MsgPrefix, dataFile)); err != nil {
3099 log.Infox("parsing message for message-id", err)
3100 } else if header, err := p.Header(); err != nil {
3101 log.Infox("parsing message header for message-id", err)
3103 messageID = header.Get("Message-Id")
3105 parsedMessageID = true
3108 // Finally deliver the message to the account(s).
3109 var nerr int // Number of non-quota errors.
3110 var nfull int // Number of failed deliveries due to over quota.
3111 var ndelivered int // Number delivered to account.
3112 for _, a := range la {
3113 // Don't deliver to recipient that was explicitly present in SMTP transaction, or
3114 // is sending the message to an alias they are member of.
3115 if rcpt.alias != nil && (regularRecipient(a.d.deliverTo) || a.d.deliverTo.Equal(msgFrom.Path())) {
3120 a.d.acc.WithWLock(func() {
3121 if err := a.d.acc.DeliverMailbox(log, a.mailbox, a.d.m, dataFile); err != nil {
3122 log.Errorx("delivering", err)
3123 metricDelivery.WithLabelValues("delivererror", a0.reason).Inc()
3124 if errors.Is(err, store.ErrOverQuota) {
3127 addError(rcpt, smtp.C451LocalErr, smtp.SeSys3Other0, false, "error processing")
3134 metricDelivery.WithLabelValues("delivered", a0.reason).Inc()
3135 log.Info("incoming message delivered", slog.String("reason", a0.reason), slog.Any("msgfrom", msgFrom))
3137 conf, _ := a.d.acc.Conf()
3138 if conf.RejectsMailbox != "" && a.d.m.MessageID != "" {
3139 if err := a.d.acc.RejectsRemove(log, conf.RejectsMailbox, a.d.m.MessageID); err != nil {
3140 log.Errorx("removing message from rejects mailbox", err, slog.String("messageid", messageID))
3145 // Pass delivered messages to queue for DSN processing and/or hooks.
3147 mr := store.FileMsgReader(a.d.m.MsgPrefix, dataFile)
3148 part, err := a.d.m.LoadPart(mr)
3150 log.Errorx("loading parsed part for evaluating webhook", err)
3152 err = queue.Incoming(context.Background(), log, a.d.acc, messageID, *a.d.m, part, a.mailbox)
3153 log.Check(err, "queueing webhook for incoming delivery")
3155 } else if nerr > 0 && ndelivered == 0 {
3156 // Don't continue if we had an error and haven't delivered yet. If we only had
3157 // quota-related errors, we keep trying for an account to deliver to.
3161 if ndelivered == 0 && (nerr > 0 || nfull > 0) {
3163 addError(rcpt, smtp.C452StorageFull, smtp.SeMailbox2Full2, true, "account storage full")
3165 addError(rcpt, smtp.C451LocalErr, smtp.SeSys3Other0, false, "error processing")
3170 // For each recipient, do final spam analysis and delivery.
3171 for _, rcpt := range c.recipients {
3172 processRecipient(rcpt)
3175 // If all recipients failed to deliver, return an error.
3176 if len(c.recipients) == len(deliverErrors) {
3178 e0 := deliverErrors[0]
3179 var serverError bool
3182 for _, e := range deliverErrors {
3183 serverError = serverError || !e.userError
3184 if e.code != e0.code || e.secode != e0.secode {
3187 msgs = append(msgs, e.errmsg)
3193 xsmtpErrorf(e0.code, e0.secode, !serverError, "%s", strings.Join(msgs, "\n"))
3196 // Not all failures had the same error. We'll return each error on a separate line.
3198 for _, e := range deliverErrors {
3199 s := fmt.Sprintf("%d %d.%s %s", e.code, e.code/100, e.secode, e.errmsg)
3200 lines = append(lines, s)
3202 code := smtp.C451LocalErr
3203 secode := smtp.SeSys3Other0
3205 code = smtp.C554TransactionFailed
3207 lines = append(lines, "multiple errors")
3208 xsmtpErrorf(code, secode, !serverError, strings.Join(lines, "\n"))
3210 // Generate one DSN for all failed recipients.
3211 if len(deliverErrors) > 0 {
3213 dsnMsg := dsn.Message{
3214 SMTPUTF8: c.msgsmtputf8,
3215 From: smtp.Path{Localpart: "postmaster", IPDomain: deliverErrors[0].rcptTo.IPDomain},
3217 Subject: "mail delivery failure",
3218 MessageID: mox.MessageIDGen(false),
3219 References: messageID,
3221 // Per-message details.
3222 ReportingMTA: mox.Conf.Static.HostnameDomain.ASCII,
3223 ReceivedFromMTA: smtp.Ehlo{Name: c.hello, ConnIP: c.remoteIP},
3227 if len(deliverErrors) > 1 {
3228 dsnMsg.TextBody = "Multiple delivery failures occurred.\n\n"
3231 for _, e := range deliverErrors {
3233 if e.code/100 == 4 {
3236 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))
3237 rcpt := dsn.Recipient{
3238 FinalRecipient: e.rcptTo,
3240 Status: fmt.Sprintf("%d.%s", e.code/100, e.secode),
3241 LastAttemptDate: now,
3243 dsnMsg.Recipients = append(dsnMsg.Recipients, rcpt)
3246 header, err := message.ReadHeaders(bufio.NewReader(&moxio.AtReader{R: dataFile}))
3248 c.log.Errorx("reading headers of incoming message for dsn, continuing dsn without headers", err)
3250 dsnMsg.Original = header
3253 c.log.Error("not queueing dsn for incoming delivery due to localserve")
3254 } else if err := queueDSN(context.TODO(), c.log, c, *c.mailFrom, dsnMsg, c.requireTLS != nil && *c.requireTLS); err != nil {
3255 metricServerErrors.WithLabelValues("queuedsn").Inc()
3256 c.log.Errorx("queuing DSN for incoming delivery, no DSN sent", err)
3261 c.transactionBad-- // Compensate for early earlier pessimistic increase.
3263 c.writecodeline(smtp.C250Completed, smtp.SeMailbox2Other0, "it is done", nil)
3266// Return whether msgFrom address is allowed to send a message to alias.
3267func aliasAllowedMsgFrom(alias config.Alias, msgFrom smtp.Address) bool {
3268 for _, aa := range alias.ParsedAddresses {
3269 if aa.Address == msgFrom {
3273 lp, err := smtp.ParseLocalpart(alias.LocalpartStr)
3274 xcheckf(err, "parsing alias localpart")
3275 if msgFrom == smtp.NewAddress(lp, alias.Domain) {
3276 return alias.AllowMsgFrom
3278 return alias.PostPublic
3281// ecode returns either ecode, or a more specific error based on err.
3282// For example, ecode can be turned from an "other system" error into a "mail
3283// system full" if the error indicates no disk space is available.
3284func errCodes(code int, ecode string, err error) codes {
3286 case moxio.IsStorageSpace(err):
3288 case smtp.SeMailbox2Other0:
3289 if code == smtp.C451LocalErr {
3290 code = smtp.C452StorageFull
3292 ecode = smtp.SeMailbox2Full2
3293 case smtp.SeSys3Other0:
3294 if code == smtp.C451LocalErr {
3295 code = smtp.C452StorageFull
3297 ecode = smtp.SeSys3StorageFull1
3300 return codes{code, ecode}
3304func (c *conn) cmdRset(p *parser) {
3309 c.bwritecodeline(smtp.C250Completed, smtp.SeOther00, "all clear", nil)
3313func (c *conn) cmdVrfy(p *parser) {
3314 // No EHLO/HELO needed.
3325 // todo future: we could support vrfy and expn for submission? though would need to see if its rfc defines it.
3328 xsmtpUserErrorf(smtp.C252WithoutVrfy, smtp.SePol7Other0, "no verify but will try delivery")
3332func (c *conn) cmdExpn(p *parser) {
3333 // No EHLO/HELO needed.
3344 // todo: we could implement expn for local aliases for authenticated users, when members have permission to list. would anyone use it?
3347 xsmtpUserErrorf(smtp.C252WithoutVrfy, smtp.SePol7Other0, "no expand but will try delivery")
3351func (c *conn) cmdHelp(p *parser) {
3352 // Let's not strictly parse the request for help. We are ignoring the text anyway.
3355 c.bwritecodeline(smtp.C214Help, smtp.SeOther00, "see rfc 5321 (smtp)", nil)
3359func (c *conn) cmdNoop(p *parser) {
3360 // No idea why, but if an argument follows, it must adhere to the string ABNF production...
3367 c.bwritecodeline(smtp.C250Completed, smtp.SeOther00, "alrighty", nil)
3371func (c *conn) cmdQuit(p *parser) {
3375 c.writecodeline(smtp.C221Closing, smtp.SeOther00, "okay thanks bye", nil)