1// Package smtpserver implements an SMTP server for submission and incoming delivery of mail messages.
10 cryptorand "crypto/rand"
34 "golang.org/x/exp/maps"
35 "golang.org/x/text/unicode/norm"
37 "github.com/prometheus/client_golang/prometheus"
38 "github.com/prometheus/client_golang/prometheus/promauto"
40 "github.com/mjl-/bstore"
42 "github.com/mjl-/mox/config"
43 "github.com/mjl-/mox/dkim"
44 "github.com/mjl-/mox/dmarc"
45 "github.com/mjl-/mox/dmarcdb"
46 "github.com/mjl-/mox/dmarcrpt"
47 "github.com/mjl-/mox/dns"
48 "github.com/mjl-/mox/dsn"
49 "github.com/mjl-/mox/iprev"
50 "github.com/mjl-/mox/message"
51 "github.com/mjl-/mox/metrics"
52 "github.com/mjl-/mox/mlog"
53 "github.com/mjl-/mox/mox-"
54 "github.com/mjl-/mox/moxio"
55 "github.com/mjl-/mox/moxvar"
56 "github.com/mjl-/mox/publicsuffix"
57 "github.com/mjl-/mox/queue"
58 "github.com/mjl-/mox/ratelimit"
59 "github.com/mjl-/mox/scram"
60 "github.com/mjl-/mox/smtp"
61 "github.com/mjl-/mox/spf"
62 "github.com/mjl-/mox/store"
63 "github.com/mjl-/mox/tlsrpt"
64 "github.com/mjl-/mox/tlsrptdb"
67// We use panic and recover for error handling while executing commands.
68// These errors signal the connection must be closed.
69var errIO = errors.New("io error")
71// If set, regular delivery/submit is sidestepped, email is accepted and
72// delivered to the account named mox.
75var limiterConnectionRate, limiterConnections *ratelimit.Limiter
77// For delivery rate limiting. Variable because changed during tests.
78var limitIPMasked1MessagesPerMinute int = 500
79var limitIPMasked1SizePerMinute int64 = 1000 * 1024 * 1024
81// Maximum number of RCPT TO commands (i.e. recipients) for a single message
82// delivery. Must be at least 100. Announced in LIMIT extension.
83const rcptToLimit = 1000
86 // Also called by tests, so they don't trigger the rate limiter.
92 // todo future: make these configurable
93 limiterConnectionRate = &ratelimit.Limiter{
94 WindowLimits: []ratelimit.WindowLimit{
97 Limits: [...]int64{300, 900, 2700},
101 limiterConnections = &ratelimit.Limiter{
102 WindowLimits: []ratelimit.WindowLimit{
104 Window: time.Duration(math.MaxInt64), // All of time.
105 Limits: [...]int64{30, 90, 270},
112 // Delays for bad/suspicious behaviour. Zero during tests.
113 badClientDelay = time.Second // Before reads and after 1-byte writes for probably spammers.
114 authFailDelay = time.Second // Response to authentication failure.
115 unknownRecipientsDelay = 5 * time.Second // Response when all recipients are unknown.
116 firstTimeSenderDelayDefault = 15 * time.Second // Before accepting message from first-time sender.
121 secode string // Enhanced code, but without the leading major int from code.
125 metricConnection = promauto.NewCounterVec(
126 prometheus.CounterOpts{
127 Name: "mox_smtpserver_connection_total",
128 Help: "Incoming SMTP connections.",
131 "kind", // "deliver" or "submit"
134 metricCommands = promauto.NewHistogramVec(
135 prometheus.HistogramOpts{
136 Name: "mox_smtpserver_command_duration_seconds",
137 Help: "SMTP server command duration and result codes in seconds.",
138 Buckets: []float64{0.001, 0.005, 0.01, 0.05, 0.100, 0.5, 1, 5, 10, 20, 30, 60, 120},
141 "kind", // "deliver" or "submit"
147 metricDelivery = promauto.NewCounterVec(
148 prometheus.CounterOpts{
149 Name: "mox_smtpserver_delivery_total",
150 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.",
157 // Similar between ../webmail/webmail.go:/metricSubmission and ../smtpserver/server.go:/metricSubmission and ../webapisrv/server.go:/metricSubmission
158 metricSubmission = promauto.NewCounterVec(
159 prometheus.CounterOpts{
160 Name: "mox_smtpserver_submission_total",
161 Help: "SMTP server incoming submission results, known values (those ending with error are server errors): ok, badmessage, badfrom, badheader, messagelimiterror, recipientlimiterror, localserveerror, queueerror.",
167 metricServerErrors = promauto.NewCounterVec(
168 prometheus.CounterOpts{
169 Name: "mox_smtpserver_errors_total",
170 Help: "SMTP server errors, known values: dkimsign, queuedsn.",
176 metricDeliveryStarttls = promauto.NewCounter(
177 prometheus.CounterOpts{
178 Name: "mox_smtpserver_delivery_starttls_total",
179 Help: "Total number of STARTTLS handshakes for incoming deliveries.",
182 metricDeliveryStarttlsErrors = promauto.NewCounterVec(
183 prometheus.CounterOpts{
184 Name: "mox_smtpserver_delivery_starttls_errors_total",
185 Help: "Errors with TLS handshake during STARTTLS for incoming deliveries.",
188 "reason", // "eof", "sslv2", "unsupportedversions", "nottls", "alert-<num>-<msg>", "other"
193var jitterRand = mox.NewPseudoRand()
195func durationDefault(delay *time.Duration, def time.Duration) time.Duration {
202// Listen initializes network listeners for incoming SMTP connection.
203// The listeners are stored for a later call to Serve.
205 names := maps.Keys(mox.Conf.Static.Listeners)
207 for _, name := range names {
208 listener := mox.Conf.Static.Listeners[name]
210 var tlsConfig, tlsConfigDelivery *tls.Config
211 if listener.TLS != nil {
212 tlsConfig = listener.TLS.Config
213 // For SMTP delivery, if we get a TLS handshake for an SNI hostname that we don't
214 // allow, we'll fallback to a certificate for the listener hostname instead of
215 // causing the connection to fail. May improve interoperability.
216 tlsConfigDelivery = listener.TLS.ConfigFallback
219 maxMsgSize := listener.SMTPMaxMessageSize
221 maxMsgSize = config.DefaultMaxMsgSize
224 if listener.SMTP.Enabled {
225 hostname := mox.Conf.Static.HostnameDomain
226 if listener.Hostname != "" {
227 hostname = listener.HostnameDomain
229 port := config.Port(listener.SMTP.Port, 25)
230 for _, ip := range listener.IPs {
231 firstTimeSenderDelay := durationDefault(listener.SMTP.FirstTimeSenderDelay, firstTimeSenderDelayDefault)
232 if tlsConfigDelivery != nil {
233 tlsConfigDelivery = tlsConfigDelivery.Clone()
234 // Default setting is currently to have session tickets disabled, to work around
235 // TLS interoperability issues with incoming deliveries from Microsoft. See
236 // https://github.com/golang/go/issues/70232.
237 tlsConfigDelivery.SessionTicketsDisabled = listener.SMTP.TLSSessionTicketsDisabled == nil || *listener.SMTP.TLSSessionTicketsDisabled
239 listen1("smtp", name, ip, port, hostname, tlsConfigDelivery, false, false, maxMsgSize, false, listener.SMTP.RequireSTARTTLS, !listener.SMTP.NoRequireTLS, listener.SMTP.DNSBLZones, firstTimeSenderDelay)
242 if listener.Submission.Enabled {
243 hostname := mox.Conf.Static.HostnameDomain
244 if listener.Hostname != "" {
245 hostname = listener.HostnameDomain
247 port := config.Port(listener.Submission.Port, 587)
248 for _, ip := range listener.IPs {
249 listen1("submission", name, ip, port, hostname, tlsConfig, true, false, maxMsgSize, !listener.Submission.NoRequireSTARTTLS, !listener.Submission.NoRequireSTARTTLS, true, nil, 0)
253 if listener.Submissions.Enabled {
254 hostname := mox.Conf.Static.HostnameDomain
255 if listener.Hostname != "" {
256 hostname = listener.HostnameDomain
258 port := config.Port(listener.Submissions.Port, 465)
259 for _, ip := range listener.IPs {
260 listen1("submissions", name, ip, port, hostname, tlsConfig, true, true, maxMsgSize, true, true, true, nil, 0)
268func 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) {
269 log := mlog.New("smtpserver", nil)
270 addr := net.JoinHostPort(ip, fmt.Sprintf("%d", port))
271 if os.Getuid() == 0 {
272 log.Print("listening for smtp",
273 slog.String("listener", name),
274 slog.String("address", addr),
275 slog.String("protocol", protocol))
277 network := mox.Network(ip)
278 ln, err := mox.Listen(network, addr)
280 log.Fatalx("smtp: listen for smtp", err, slog.String("protocol", protocol), slog.String("listener", name))
283 // Each listener gets its own copy of the config, so session keys between different
284 // ports on same listener aren't shared. We rotate session keys explicitly in this
285 // base TLS config because each connection clones the TLS config before using. The
286 // base TLS config would never get automatically managed/rotated session keys.
287 if tlsConfig != nil {
288 tlsConfig = tlsConfig.Clone()
289 mox.StartTLSSessionTicketKeyRefresher(mox.Shutdown, log, tlsConfig)
294 conn, err := ln.Accept()
296 log.Infox("smtp: accept", err, slog.String("protocol", protocol), slog.String("listener", name))
300 // Package is set on the resolver by the dkim/spf/dmarc/etc packages.
301 resolver := dns.StrictResolver{Log: log.Logger}
302 go serve(name, mox.Cid(), hostname, tlsConfig, conn, resolver, submission, xtls, false, maxMessageSize, requireTLSForAuth, requireTLSForDelivery, requireTLS, dnsBLs, firstTimeSenderDelay)
306 servers = append(servers, serve)
309// Serve starts serving on all listeners, launching a goroutine per listener.
311 for _, serve := range servers {
319 // OrigConn is the original (TCP) connection. We'll read from/write to conn, which
320 // can be wrapped in a tls.Server. We close origConn instead of conn because
321 // closing the TLS connection would send a TLS close notification, which may block
322 // for 5s if the server isn't reading it (because it is also sending it).
327 extRequireTLS bool // Whether to announce and allow the REQUIRETLS extension.
328 viaHTTPS bool // Whether the connection came in via the HTTPS port (using TLS ALPN).
329 resolver dns.Resolver
332 tr *moxio.TraceReader // Kept for changing trace level during cmd/auth/data.
333 tw *moxio.TraceWriter
334 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.
335 lastlog time.Time // Used for printing the delta time since the previous logging for this connection.
337 baseTLSConfig *tls.Config
341 log mlog.Log // Used for all synchronous logging on this connection, see logbg for logging in a separate goroutine.
343 requireTLSForAuth bool
344 requireTLSForDelivery bool // If set, delivery is only allowed with TLS (STARTTLS), except if delivery is to a TLS reporting address.
345 cmd string // Current command.
346 cmdStart time.Time // Start of current command.
347 ncmds int // Number of commands processed. Used to abort connection when first incoming command is unknown/invalid.
349 firstTimeSenderDelay time.Duration
351 // If non-zero, taken into account during Read and Write. Set while processing DATA
352 // command, we don't want the entire delivery to take too long.
355 hello dns.IPDomain // Claimed remote name. Can be ip address for ehlo.
356 ehlo bool // If set, we had EHLO instead of HELO.
358 authFailed int // Number of failed auth attempts. For slowing down remote with many failures.
359 authSASL bool // Whether SASL authentication was done.
360 authTLS bool // Whether we did TLS client cert authentication.
361 username string // Only when authenticated.
362 account *store.Account // Only when authenticated.
364 // We track good/bad message transactions to disconnect spammers trying to guess addresses.
368 // Message transaction.
370 requireTLS *bool // MAIL FROM with REQUIRETLS set.
371 futureRelease time.Time // MAIL FROM with HOLDFOR or HOLDUNTIL.
372 futureReleaseRequest string // For use in DSNs, either "for;" or "until;" plus original value.
../rfc/4865:305
373 has8bitmime bool // If MAIL FROM parameter BODY=8BITMIME was sent. Required for SMTPUTF8.
374 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.
375 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.
376 recipients []recipient
379type rcptAccount struct {
381 Destination config.Destination
382 CanonicalAddress string // Optional catchall part stripped and/or lowercased.
385type rcptAlias struct {
387 CanonicalAddress string // Optional catchall part stripped and/or lowercased.
390type recipient struct {
393 // If account and alias are both not set, this is not for a local address. This is
394 // normal for submission, where messages are added to the queue. For incoming
395 // deliveries, this will result in an error.
396 Account *rcptAccount // If set, recipient address is for this local account.
397 Alias *rcptAlias // If set, for a local alias.
400func isClosed(err error) bool {
401 return errors.Is(err, errIO) || moxio.IsClosed(err)
404// Logbg returns a logger for logging in the background (in a goroutine), eg for
405// logging LoginAttempts. The regular c.log has a handler that evaluates fields on
406// the connection at time of logging, which may happen at the same time as
407// modifications to those fields.
408func (c *conn) logbg() mlog.Log {
409 log := mlog.New("smtpserver", nil).WithCid(c.cid)
410 if c.username != "" {
411 log = log.With(slog.String("username", c.username))
416// loginAttempt initializes a store.LoginAttempt, for adding to the store after
417// filling in the results and other details.
418func (c *conn) loginAttempt(useTLS bool, authMech string) store.LoginAttempt {
419 var state *tls.ConnectionState
420 if tc, ok := c.conn.(*tls.Conn); ok && useTLS {
421 v := tc.ConnectionState()
425 return store.LoginAttempt{
426 RemoteIP: c.remoteIP.String(),
427 LocalIP: c.localIP.String(),
428 TLS: store.LoginAttemptTLS(state),
429 Protocol: "submission",
431 Result: store.AuthError, // Replaced by caller.
435// makeTLSConfig makes a new tls config that is bound to the connection for
436// possible client certificate authentication in case of submission.
437func (c *conn) makeTLSConfig() *tls.Config {
439 return c.baseTLSConfig
442 // We clone the config so we can set VerifyPeerCertificate below to a method bound
443 // to this connection. Earlier, we set session keys explicitly on the base TLS
444 // config, so they can be used for this connection too.
445 tlsConf := c.baseTLSConfig.Clone()
447 // Allow client certificate authentication, for use with the sasl "external"
448 // authentication mechanism.
449 tlsConf.ClientAuth = tls.RequestClientCert
451 // We verify the client certificate during the handshake. The TLS handshake is
452 // initiated explicitly for incoming connections and during starttls, so we can
453 // immediately extract the account name and address used for authentication.
454 tlsConf.VerifyPeerCertificate = c.tlsClientAuthVerifyPeerCert
459// tlsClientAuthVerifyPeerCert can be used as tls.Config.VerifyPeerCertificate, and
460// sets authentication-related fields on conn. This is not called on resumed TLS
462func (c *conn) tlsClientAuthVerifyPeerCert(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
463 if len(rawCerts) == 0 {
467 // If we had too many authentication failures from this IP, don't attempt
468 // authentication. If this is a new incoming connetion, it is closed after the TLS
470 if !mox.LimiterFailedAuth.CanAdd(c.remoteIP, time.Now(), 1) {
474 cert, err := x509.ParseCertificate(rawCerts[0])
476 c.log.Debugx("parsing tls client certificate", err)
479 if err := c.tlsClientAuthVerifyPeerCertParsed(cert); err != nil {
480 c.log.Debugx("verifying tls client certificate", err)
481 return fmt.Errorf("verifying client certificate: %w", err)
486// tlsClientAuthVerifyPeerCertParsed verifies a client certificate. Called both for
487// fresh and resumed TLS connections.
488func (c *conn) tlsClientAuthVerifyPeerCertParsed(cert *x509.Certificate) error {
489 if c.account != nil {
490 return fmt.Errorf("cannot authenticate with tls client certificate after previous authentication")
493 la := c.loginAttempt(false, "tlsclientauth")
495 // Get TLS connection state in goroutine because we are called while performing the
496 // TLS handshake, which already has the tls connection locked.
497 conn := c.conn.(*tls.Conn)
498 logbg := c.logbg() // Evaluate attributes now, can't do it in goroutine.
501 // In case of panic don't take the whole program down.
504 c.log.Error("recover from panic", slog.Any("panic", x))
506 metrics.PanicInc(metrics.Smtpserver)
510 state := conn.ConnectionState()
511 la.TLS = store.LoginAttemptTLS(&state)
512 store.LoginAttemptAdd(context.Background(), logbg, la)
515 if la.Result == store.AuthSuccess {
516 mox.LimiterFailedAuth.Reset(c.remoteIP, time.Now())
518 mox.LimiterFailedAuth.Add(c.remoteIP, time.Now(), 1)
522 // For many failed auth attempts, slow down verification attempts.
523 if c.authFailed > 3 && authFailDelay > 0 {
524 mox.Sleep(mox.Context, time.Duration(c.authFailed-3)*authFailDelay)
526 c.authFailed++ // Compensated on success.
528 // On the 3rd failed authentication, start responding slowly. Successful auth will
529 // cause fast responses again.
530 if c.authFailed >= 3 {
535 shabuf := sha256.Sum256(cert.RawSubjectPublicKeyInfo)
536 fp := base64.RawURLEncoding.EncodeToString(shabuf[:])
537 la.TLSPubKeyFingerprint = fp
538 pubKey, err := store.TLSPublicKeyGet(context.TODO(), fp)
540 if err == bstore.ErrAbsent {
541 la.Result = store.AuthBadCredentials
543 return fmt.Errorf("looking up tls public key with fingerprint %s: %v", fp, err)
545 la.LoginAddress = pubKey.LoginAddress
547 // Verify account exists and still matches address. We don't check for account
548 // login being disabled if preauth is disabled. In that case, sasl external auth
549 // will be done before credentials can be used, and login disabled will be checked
550 // then, where it will result in a more helpful error message.
551 checkLoginDisabled := !pubKey.NoIMAPPreauth
552 acc, accName, _, err := store.OpenEmail(c.log, pubKey.LoginAddress, checkLoginDisabled)
553 la.AccountName = accName
555 if errors.Is(err, store.ErrLoginDisabled) {
556 la.Result = store.AuthLoginDisabled
558 return fmt.Errorf("opening account for address %s for public key %s: %w", pubKey.LoginAddress, fp, err)
563 c.log.Check(err, "close account")
566 la.AccountName = acc.Name
567 if acc.Name != pubKey.Account {
568 return fmt.Errorf("tls client public key %s is for account %s, but email address %s is for account %s", fp, pubKey.Account, pubKey.LoginAddress, acc.Name)
573 acc = nil // Prevent cleanup by defer.
574 c.username = pubKey.LoginAddress
576 la.Result = store.AuthSuccess
577 c.log.Debug("tls client authenticated with client certificate",
578 slog.String("fingerprint", fp),
579 slog.String("username", c.username),
580 slog.String("account", c.account.Name),
581 slog.Any("remote", c.remoteIP))
585// xtlsHandshakeAndAuthenticate performs the TLS handshake, and verifies a client
586// certificate if present.
587func (c *conn) xtlsHandshakeAndAuthenticate(conn net.Conn) {
588 tlsConn := tls.Server(conn, c.makeTLSConfig())
591 cidctx := context.WithValue(mox.Context, mlog.CidKey, c.cid)
592 ctx, cancel := context.WithTimeout(cidctx, time.Minute)
594 c.log.Debug("starting tls server handshake")
596 metricDeliveryStarttls.Inc()
598 if err := tlsConn.HandshakeContext(ctx); err != nil {
600 // Errors from crypto/tls mostly aren't typed. We'll have to look for strings...
602 if errors.Is(err, io.EOF) {
604 } else if alert, ok := mox.AsTLSAlert(err); ok {
605 reason = tlsrpt.FormatAlert(alert)
608 if strings.Contains(s, "tls: client offered only unsupported versions") {
609 reason = "unsupportedversions"
610 } else if strings.Contains(s, "tls: first record does not look like a TLS handshake") {
612 } else if strings.Contains(s, "tls: unsupported SSLv2 handshake received") {
616 metricDeliveryStarttlsErrors.WithLabelValues(reason).Inc()
618 panic(fmt.Errorf("tls handshake: %s (%w)", err, errIO))
622 cs := tlsConn.ConnectionState()
623 if cs.DidResume && len(cs.PeerCertificates) > 0 {
624 // Verify client after session resumption.
625 err := c.tlsClientAuthVerifyPeerCertParsed(cs.PeerCertificates[0])
627 panic(fmt.Errorf("tls verify client certificate after resumption: %s (%w)", err, errIO))
631 attrs := []slog.Attr{
632 slog.Any("version", tlsVersion(cs.Version)),
633 slog.String("ciphersuite", tls.CipherSuiteName(cs.CipherSuite)),
634 slog.String("sni", cs.ServerName),
635 slog.Bool("resumed", cs.DidResume),
636 slog.Int("clientcerts", len(cs.PeerCertificates)),
638 if c.account != nil {
639 attrs = append(attrs,
640 slog.String("account", c.account.Name),
641 slog.String("username", c.username),
644 c.log.Debug("tls handshake completed", attrs...)
647type tlsVersion uint16
649func (v tlsVersion) String() string {
650 return strings.ReplaceAll(strings.ToLower(tls.VersionName(uint16(v))), " ", "-")
653// completely reset connection state as if greeting has just been sent.
655func (c *conn) reset() {
657 c.hello = dns.IPDomain{}
660 if c.account != nil {
661 err := c.account.Close()
662 c.log.Check(err, "closing account")
670// for rset command, and a few more cases that reset the mail transaction state.
672func (c *conn) rset() {
675 c.futureRelease = time.Time{}
676 c.futureReleaseRequest = ""
677 c.has8bitmime = false
679 c.msgsmtputf8 = false
683func (c *conn) earliestDeadline(d time.Duration) time.Time {
684 e := time.Now().Add(d)
685 if !c.deadline.IsZero() && c.deadline.Before(e) {
691func (c *conn) xcheckAuth() {
692 if c.submission && c.account == nil {
694 xsmtpUserErrorf(smtp.C530SecurityRequired, smtp.SePol7Other0, "authentication required")
698func (c *conn) xtrace(level slog.Level) func() {
704 c.tr.SetTrace(mlog.LevelTrace)
705 c.tw.SetTrace(mlog.LevelTrace)
709// setSlow marks the connection slow (or now), so reads are done with 3 second
710// delay for each read, and writes are done at 1 byte per second, to try to slow
712func (c *conn) setSlow(on bool) {
714 c.log.Debug("connection changed to slow")
715 } else if !on && c.slow {
716 c.log.Debug("connection restored to regular pace")
721// Write writes to the connection. It panics on i/o errors, which is handled by the
722// connection command loop.
723func (c *conn) Write(buf []byte) (int, error) {
729 // We set a single deadline for Write and Read. This may be a TLS connection.
730 // SetDeadline works on the underlying connection. If we wouldn't touch the read
731 // deadline, and only set the write deadline and do a bunch of writes, the TLS
732 // library would still have to do reads on the underlying connection, and may reach
733 // a read deadline that was set for some earlier read.
734 // We have one deadline for the whole write. In case of slow writing, we'll write
735 // the last chunk in one go, so remote smtp clients don't abort the connection for
737 deadline := c.earliestDeadline(30 * time.Second)
738 if err := c.conn.SetDeadline(deadline); err != nil {
739 c.log.Errorx("setting deadline for write", err)
744 nn, err := c.conn.Write(buf[:chunk])
746 panic(fmt.Errorf("write: %s (%w)", err, errIO))
750 if len(buf) > 0 && badClientDelay > 0 {
751 mox.Sleep(mox.Context, badClientDelay)
753 // Make sure we don't take too long, otherwise the remote SMTP client may close the
755 if time.Until(deadline) < 5*badClientDelay {
763// Read reads from the connection. It panics on i/o errors, which is handled by the
764// connection command loop.
765func (c *conn) Read(buf []byte) (int, error) {
766 if c.slow && badClientDelay > 0 {
767 mox.Sleep(mox.Context, badClientDelay)
771 // See comment about Deadline instead of individual read/write deadlines at Write.
772 if err := c.conn.SetDeadline(c.earliestDeadline(30 * time.Second)); err != nil {
773 c.log.Errorx("setting deadline for read", err)
776 n, err := c.conn.Read(buf)
778 panic(fmt.Errorf("read: %s (%w)", err, errIO))
783// Cache of line buffers for reading commands.
785var bufpool = moxio.NewBufpool(8, 2*1024)
787func (c *conn) readline() string {
788 line, err := bufpool.Readline(c.log, c.r)
789 if err != nil && errors.Is(err, moxio.ErrLineTooLong) {
790 c.writecodeline(smtp.C500BadSyntax, smtp.SeProto5Other0, "line too long, smtp max is 512, we reached 2048", nil)
791 panic(fmt.Errorf("%s (%w)", err, errIO))
792 } else if err != nil {
793 panic(fmt.Errorf("%s (%w)", err, errIO))
798// Buffered-write command response line to connection with codes and msg.
799// Err is not sent to remote but is used for logging and can be empty.
800func (c *conn) bwritecodeline(code int, secode string, msg string, err error) {
803 ecode = fmt.Sprintf("%d.%s", code/100, secode)
805 metricCommands.WithLabelValues(c.kind(), c.cmd, fmt.Sprintf("%d", code), ecode).Observe(float64(time.Since(c.cmdStart)) / float64(time.Second))
806 c.log.Debugx("smtp command result", err,
807 slog.String("kind", c.kind()),
808 slog.String("cmd", c.cmd),
809 slog.Int("code", code),
810 slog.String("ecode", ecode),
811 slog.Duration("duration", time.Since(c.cmdStart)))
818 // Separate by newline and wrap long lines.
819 lines := strings.Split(msg, "\n")
820 for i, line := range lines {
822 var prelen = 3 + 1 + len(ecode) + len(sep)
823 for prelen+len(line) > 510 {
825 for ; e > 400 && line[e] != ' '; e-- {
827 // 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.
828 c.bwritelinef("%d-%s%s%s", code, ecode, sep, line[:e])
832 if i < len(lines)-1 {
835 c.bwritelinef("%d%s%s%s%s", code, spdash, ecode, sep, line)
839// Buffered-write a formatted response line to connection.
840func (c *conn) bwritelinef(format string, args ...any) {
841 msg := fmt.Sprintf(format, args...)
842 fmt.Fprint(c.w, msg+"\r\n")
845// Flush pending buffered writes to connection.
846func (c *conn) xflush() {
847 c.w.Flush() // Errors will have caused a panic in Write.
850// Write (with flush) a response line with codes and message. err is not written, used for logging and can be nil.
851func (c *conn) writecodeline(code int, secode string, msg string, err error) {
852 c.bwritecodeline(code, secode, msg, err)
856// Write (with flush) a formatted response line to connection.
857func (c *conn) writelinef(format string, args ...any) {
858 c.bwritelinef(format, args...)
862var cleanClose struct{} // Sentinel value for panic/recover indicating clean close of connection.
864// ServeTLSConn serves a TLS connection.
865func ServeTLSConn(listenerName string, hostname dns.Domain, conn *tls.Conn, tlsConfig *tls.Config, submission, viaHTTPS bool, maxMsgSize int64, requireTLS bool) {
866 log := mlog.New("smtpserver", nil)
867 resolver := dns.StrictResolver{Log: log.Logger}
868 serve(listenerName, mox.Cid(), hostname, tlsConfig, conn, resolver, submission, true, viaHTTPS, maxMsgSize, true, true, requireTLS, nil, 0)
871func serve(listenerName string, cid int64, hostname dns.Domain, tlsConfig *tls.Config, nc net.Conn, resolver dns.Resolver, submission, xtls, viaHTTPS bool, maxMessageSize int64, requireTLSForAuth, requireTLSForDelivery, requireTLS bool, dnsBLs []dns.Domain, firstTimeSenderDelay time.Duration) {
872 var localIP, remoteIP net.IP
873 if a, ok := nc.LocalAddr().(*net.TCPAddr); ok {
876 // For net.Pipe, during tests.
877 localIP = net.ParseIP("127.0.0.10")
879 if a, ok := nc.RemoteAddr().(*net.TCPAddr); ok {
882 // For net.Pipe, during tests.
883 remoteIP = net.ParseIP("127.0.0.10")
888 origConn = nc.(*tls.Conn).NetConn()
895 submission: submission,
898 extRequireTLS: requireTLS,
901 baseTLSConfig: tlsConfig,
905 maxMessageSize: maxMessageSize,
906 requireTLSForAuth: requireTLSForAuth,
907 requireTLSForDelivery: requireTLSForDelivery,
909 firstTimeSenderDelay: firstTimeSenderDelay,
911 var logmutex sync.Mutex
912 // Also see (and possibly update) c.logbg, for logging in a goroutine.
913 c.log = mlog.New("smtpserver", nil).WithFunc(func() []slog.Attr {
915 defer logmutex.Unlock()
918 slog.Int64("cid", c.cid),
919 slog.Duration("delta", now.Sub(c.lastlog)),
922 if c.username != "" {
923 l = append(l, slog.String("username", c.username))
927 c.tr = moxio.NewTraceReader(c.log, "RC: ", c)
928 c.r = bufio.NewReader(c.tr)
929 c.tw = moxio.NewTraceWriter(c.log, "LS: ", c)
930 c.w = bufio.NewWriter(c.tw)
932 metricConnection.WithLabelValues(c.kind()).Inc()
933 c.log.Info("new connection",
934 slog.Any("remote", c.conn.RemoteAddr()),
935 slog.Any("local", c.conn.LocalAddr()),
936 slog.Bool("submission", submission),
937 slog.Bool("tls", xtls),
938 slog.Bool("viahttps", viaHTTPS),
939 slog.String("listener", listenerName))
942 c.origConn.Close() // Close actual TCP socket, regardless of TLS on top.
943 c.conn.Close() // If TLS, will try to write alert notification to already closed socket, returning error quickly.
945 if c.account != nil {
946 err := c.account.Close()
947 c.log.Check(err, "closing account")
952 if x == nil || x == cleanClose {
953 c.log.Info("connection closed")
954 } else if err, ok := x.(error); ok && isClosed(err) {
955 c.log.Infox("connection closed", err)
957 c.log.Error("unhandled panic", slog.Any("err", x))
959 metrics.PanicInc(metrics.Smtpserver)
963 if xtls && !viaHTTPS {
964 // Start TLS on connection. We perform the handshake explicitly, so we can set a
965 // timeout, do client certificate authentication, log TLS details afterwards.
966 c.xtlsHandshakeAndAuthenticate(c.conn)
970 case <-mox.Shutdown.Done():
972 c.writecodeline(smtp.C421ServiceUnavail, smtp.SeSys3NotAccepting2, "shutting down", nil)
977 if !limiterConnectionRate.Add(c.remoteIP, time.Now(), 1) {
978 c.writecodeline(smtp.C421ServiceUnavail, smtp.SePol7Other0, "connection rate from your ip or network too high, slow down please", nil)
982 // If remote IP/network resulted in too many authentication failures, refuse to serve.
983 if submission && !mox.LimiterFailedAuth.CanAdd(c.remoteIP, time.Now(), 1) {
984 metrics.AuthenticationRatelimitedInc("submission")
985 c.log.Debug("refusing connection due to many auth failures", slog.Any("remoteip", c.remoteIP))
986 c.writecodeline(smtp.C421ServiceUnavail, smtp.SePol7Other0, "too many auth failures", nil)
990 if !limiterConnections.Add(c.remoteIP, time.Now(), 1) {
991 c.log.Debug("refusing connection due to many open connections", slog.Any("remoteip", c.remoteIP))
992 c.writecodeline(smtp.C421ServiceUnavail, smtp.SePol7Other0, "too many open connections from your ip or network", nil)
995 defer limiterConnections.Add(c.remoteIP, time.Now(), -1)
997 // We register and unregister the original connection, in case c.conn is replaced
998 // with a TLS connection later on.
999 mox.Connections.Register(nc, "smtp", listenerName)
1000 defer mox.Connections.Unregister(nc)
1004 // We include the string ESMTP. https://cr.yp.to/smtp/greeting.html recommends it.
1005 // Should not be too relevant nowadays, but does not hurt and default blackbox
1006 // exporter SMTP health check expects it.
1007 c.writelinef("%d %s ESMTP mox %s", smtp.C220ServiceReady, c.hostname.ASCII, moxvar.Version)
1012 // If another command is present, don't flush our buffered response yet. Holding
1013 // off will cause us to respond with a single packet.
1016 buf, err := c.r.Peek(n)
1017 if err == nil && bytes.IndexByte(buf, '\n') >= 0 {
1025var commands = map[string]func(c *conn, p *parser){
1026 "helo": (*conn).cmdHelo,
1027 "ehlo": (*conn).cmdEhlo,
1028 "starttls": (*conn).cmdStarttls,
1029 "auth": (*conn).cmdAuth,
1030 "mail": (*conn).cmdMail,
1031 "rcpt": (*conn).cmdRcpt,
1032 "data": (*conn).cmdData,
1033 "rset": (*conn).cmdRset,
1034 "vrfy": (*conn).cmdVrfy,
1035 "expn": (*conn).cmdExpn,
1036 "help": (*conn).cmdHelp,
1037 "noop": (*conn).cmdNoop,
1038 "quit": (*conn).cmdQuit,
1041func command(c *conn) {
1047 err, ok := x.(error)
1057 if errors.As(err, &serr) {
1058 c.writecodeline(serr.code, serr.secode, fmt.Sprintf("%s (%s)", serr.errmsg, mox.ReceivedID(c.cid)), serr.err)
1059 if serr.printStack {
1060 c.log.Errorx("smtp error", serr.err, slog.Int("code", serr.code), slog.String("secode", serr.secode))
1064 // Other type of panic, we pass it on, aborting the connection.
1065 c.log.Errorx("command panic", err)
1070 // todo future: we could wait for either a line or shutdown, and just close the connection on shutdown.
1072 line := c.readline()
1073 t := strings.SplitN(line, " ", 2)
1079 cmdl := strings.ToLower(cmd)
1081 // 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
1084 case <-mox.Shutdown.Done():
1086 c.writecodeline(smtp.C421ServiceUnavail, smtp.SeSys3NotAccepting2, "shutting down", nil)
1092 c.cmdStart = time.Now()
1094 p := newParser(args, c.smtputf8, c)
1095 fn, ok := commands[cmdl]
1099 // Other side is likely speaking something else than SMTP, send error message and
1100 // stop processing because there is a good chance whatever they sent has multiple
1102 c.writecodeline(smtp.C500BadSyntax, smtp.SeProto5Syntax2, "please try again speaking smtp", nil)
1106 xsmtpUserErrorf(smtp.C500BadSyntax, smtp.SeProto5BadCmdOrSeq1, "unknown command")
1112// For use in metric labels.
1113func (c *conn) kind() string {
1120func (c *conn) xneedHello() {
1121 if c.hello.IsZero() {
1122 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "no ehlo/helo yet")
1126// If smtp server is configured to require TLS for all mail delivery (except to TLS
1127// reporting address), abort command.
1128func (c *conn) xneedTLSForDelivery(rcpt smtp.Path) {
1129 // For TLS reports, we allow the message in even without TLS, because there may be
1131 if c.requireTLSForDelivery && !c.tls && !isTLSReportRecipient(rcpt) {
1133 xsmtpUserErrorf(smtp.C530SecurityRequired, smtp.SePol7Other0, "STARTTLS required for mail delivery")
1137func isTLSReportRecipient(rcpt smtp.Path) bool {
1138 _, _, _, dest, err := mox.LookupAddress(rcpt.Localpart, rcpt.IPDomain.Domain, false, false, false)
1139 return err == nil && (dest.HostTLSReports || dest.DomainTLSReports)
1142func (c *conn) cmdHelo(p *parser) {
1143 c.cmdHello(p, false)
1146func (c *conn) cmdEhlo(p *parser) {
1151func (c *conn) cmdHello(p *parser, ehlo bool) {
1152 var remote dns.IPDomain
1153 if c.submission && !mox.Pedantic {
1154 // Mail clients regularly put bogus information in the hostname/ip. For submission,
1155 // the value is of no use, so there is not much point in annoying the user with
1156 // errors they cannot fix themselves. Except when in pedantic mode.
1157 remote = dns.IPDomain{IP: c.remoteIP}
1161 remote = p.xipdomain(true)
1163 remote = dns.IPDomain{Domain: p.xdomain()}
1165 // Verify a remote domain name has an A or AAAA record, CNAME not allowed.
../rfc/5321:722
1166 cidctx := context.WithValue(mox.Context, mlog.CidKey, c.cid)
1167 ctx, cancel := context.WithTimeout(cidctx, time.Minute)
1168 _, _, err := c.resolver.LookupIPAddr(ctx, remote.Domain.ASCII+".")
1170 if dns.IsNotFound(err) {
1171 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeProto5Other0, "your ehlo domain does not resolve to an IP address")
1173 // For success or temporary resolve errors, we'll just continue.
1176 // Though a few paragraphs earlier is a claim additional data can occur for address
1177 // literals (IP addresses), although the ABNF in that document does not allow it.
1178 // We allow additional text, but only if space-separated.
1179 if len(remote.IP) > 0 && p.space() {
1191 // https://www.iana.org/assignments/mail-parameters/mail-parameters.xhtml
1193 c.bwritelinef("250-%s", c.hostname.ASCII)
1197 if !c.tls && c.baseTLSConfig != nil {
1199 c.bwritelinef("250-STARTTLS")
1200 } else if c.extRequireTLS {
1203 c.bwritelinef("250-REQUIRETLS")
1208 if c.tls || !c.requireTLSForAuth {
1209 // We always mention the SCRAM PLUS variants, even if TLS is not active: It is a
1210 // hint to the client that a TLS connection can use TLS channel binding during
1211 // authentication. The client should select the bare variant when TLS isn't
1212 // present, and also not indicate the server supports the PLUS variant in that
1213 // case, or it would trigger the mechanism downgrade detection.
1214 mechs = "SCRAM-SHA-256-PLUS SCRAM-SHA-256 SCRAM-SHA-1-PLUS SCRAM-SHA-1 CRAM-MD5 PLAIN LOGIN"
1216 if c.tls && len(c.conn.(*tls.Conn).ConnectionState().PeerCertificates) > 0 && !c.viaHTTPS {
1217 mechs = "EXTERNAL " + mechs
1219 c.bwritelinef("250-AUTH %s", mechs)
1222 c.bwritelinef("250-FUTURERELEASE %d %s", queue.FutureReleaseIntervalMax/time.Second, t.Format(time.RFC3339))
1225 // todo future? c.writelinef("250-DSN")
1233func (c *conn) cmdStarttls(p *parser) {
1239 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "already speaking tls")
1241 if c.account != nil {
1242 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "cannot starttls after authentication")
1244 if c.baseTLSConfig == nil {
1245 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "starttls not offered")
1248 // We don't want to do TLS on top of c.r because it also prints protocol traces: We
1249 // don't want to log the TLS stream. So we'll do TLS on the underlying connection,
1250 // but make sure any bytes already read and in the buffer are used for the TLS
1253 if n := c.r.Buffered(); n > 0 {
1254 conn = &moxio.PrefixConn{
1255 PrefixReader: io.LimitReader(c.r, int64(n)),
1260 // We add the cid to the output, to help debugging in case of a failing TLS connection.
1261 c.writecodeline(smtp.C220ServiceReady, smtp.SeOther00, "go! ("+mox.ReceivedID(c.cid)+")", nil)
1263 c.xtlsHandshakeAndAuthenticate(conn)
1270func (c *conn) cmdAuth(p *parser) {
1274 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "authentication only allowed on submission ports")
1278 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "already authenticated")
1280 if c.mailFrom != nil {
1282 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "authentication not allowed during mail transaction")
1285 // If authentication fails due to missing derived secrets, we don't hold it against
1286 // the connection. There is no way to indicate server support for an authentication
1287 // mechanism, but that a mechanism won't work for an account.
1288 var missingDerivedSecrets bool
1290 // For many failed auth attempts, slow down verification attempts.
1291 // Dropping the connection could also work, but more so when we have a connection rate limiter.
1293 if c.authFailed > 3 && authFailDelay > 0 {
1295 mox.Sleep(mox.Context, time.Duration(c.authFailed-3)*authFailDelay)
1297 c.authFailed++ // Compensated on success.
1299 if missingDerivedSecrets {
1302 // On the 3rd failed authentication, start responding slowly. Successful auth will
1303 // cause fast responses again.
1304 if c.authFailed >= 3 {
1309 la := c.loginAttempt(true, "")
1311 store.LoginAttemptAdd(context.Background(), c.logbg(), la)
1312 if la.Result == store.AuthSuccess {
1313 mox.LimiterFailedAuth.Reset(c.remoteIP, time.Now())
1314 } else if !missingDerivedSecrets {
1315 mox.LimiterFailedAuth.Add(c.remoteIP, time.Now(), 1)
1321 mech := p.xsaslMech()
1323 // Read the first parameter, either as initial parameter or by sending a
1324 // continuation with the optional encChal (must already be base64-encoded).
1325 xreadInitial := func(encChal string) []byte {
1329 // todo future: handle max length of 12288 octets and return proper responde codes otherwise
../rfc/4954:253
1333 la.Result = store.AuthAborted
1334 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5Other0, "authentication aborted")
1339 // Windows Mail 16005.14326.21606.0 sends two spaces between "AUTH PLAIN" and the
1344 auth = p.remainder()
1347 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5Syntax2, "missing initial auth base64 parameter after space")
1348 } else if auth == "=" {
1350 auth = "" // Base64 decode below will result in empty buffer.
1353 buf, err := base64.StdEncoding.DecodeString(auth)
1356 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5Syntax2, "invalid base64: %s", err)
1361 xreadContinuation := func() []byte {
1362 line := c.readline()
1364 la.Result = store.AuthAborted
1365 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5Other0, "authentication aborted")
1367 buf, err := base64.StdEncoding.DecodeString(line)
1370 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5Syntax2, "invalid base64: %s", err)
1375 // The various authentication mechanisms set account and username. We may already
1376 // have an account and username from TLS client authentication. Afterwards, we
1377 // check that the account is the same.
1378 var account *store.Account
1382 err := account.Close()
1383 c.log.Check(err, "close account")
1389 la.AuthMech = "plain"
1393 if !c.tls && c.requireTLSForAuth {
1394 xsmtpUserErrorf(smtp.C538EncReqForAuth, smtp.SePol7EncReqForAuth11, "authentication requires tls")
1397 // Password is in line in plain text, so hide it.
1398 defer c.xtrace(mlog.LevelTraceauth)()
1399 buf := xreadInitial("")
1400 c.xtrace(mlog.LevelTrace) // Restore.
1401 plain := bytes.Split(buf, []byte{0})
1402 if len(plain) != 3 {
1403 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5BadParams4, "auth data should have 3 nul-separated tokens, got %d", len(plain))
1405 authz := norm.NFC.String(string(plain[0]))
1406 username = norm.NFC.String(string(plain[1]))
1407 la.LoginAddress = username
1408 password := string(plain[2])
1410 if authz != "" && authz != username {
1411 la.Result = store.AuthBadCredentials
1412 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "cannot assume other role")
1416 account, la.AccountName, err = store.OpenEmailAuth(c.log, username, password, false)
1417 if err != nil && errors.Is(err, store.ErrUnknownCredentials) {
1419 la.Result = store.AuthBadCredentials
1420 c.log.Info("failed authentication attempt", slog.String("username", username), slog.Any("remote", c.remoteIP))
1421 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass")
1423 xcheckf(err, "verifying credentials")
1426 // LOGIN is obsoleted in favor of PLAIN, only implemented to support legacy
1427 // clients, see Internet-Draft (I-D):
1428 // https://datatracker.ietf.org/doc/html/draft-murchison-sasl-login-00
1430 la.LoginAddress = "login"
1434 if !c.tls && c.requireTLSForAuth {
1435 xsmtpUserErrorf(smtp.C538EncReqForAuth, smtp.SePol7EncReqForAuth11, "authentication requires tls")
1438 // Read user name. The I-D says the client should ignore the server challenge, but
1439 // also that some clients may require challenge "Username:" instead of "User
1440 // Name". We can't sent both... Servers most commonly return "Username:" and
1441 // "Password:", so we do the same.
1442 // I-D says maximum length must be 64 bytes. We allow more, for long user names
1444 encChal := base64.StdEncoding.EncodeToString([]byte("Username:"))
1445 username = string(xreadInitial(encChal))
1446 username = norm.NFC.String(username)
1447 la.LoginAddress = username
1449 // Again, client should ignore the challenge, we send the same as the example in
1451 c.writelinef("%d %s", smtp.C334ContinueAuth, base64.StdEncoding.EncodeToString([]byte("Password:")))
1453 // Password is in line in plain text, so hide it.
1454 defer c.xtrace(mlog.LevelTraceauth)()
1455 password := string(xreadContinuation())
1456 c.xtrace(mlog.LevelTrace) // Restore.
1459 account, la.AccountName, err = store.OpenEmailAuth(c.log, username, password, false)
1460 if err != nil && errors.Is(err, store.ErrUnknownCredentials) {
1462 la.Result = store.AuthBadCredentials
1463 c.log.Info("failed authentication attempt", slog.String("username", username), slog.Any("remote", c.remoteIP))
1464 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass")
1466 xcheckf(err, "verifying credentials")
1469 la.AuthMech = strings.ToLower(mech)
1474 chal := fmt.Sprintf("<%d.%d@%s>", uint64(mox.CryptoRandInt()), time.Now().UnixNano(), mox.Conf.Static.HostnameDomain.ASCII)
1475 c.writelinef("%d %s", smtp.C334ContinueAuth, base64.StdEncoding.EncodeToString([]byte(chal)))
1477 resp := xreadContinuation()
1478 t := strings.Split(string(resp), " ")
1479 if len(t) != 2 || len(t[1]) != 2*md5.Size {
1480 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5BadParams4, "malformed cram-md5 response")
1482 username = norm.NFC.String(t[0])
1483 la.LoginAddress = username
1484 c.log.Debug("cram-md5 auth", slog.String("username", username))
1486 account, la.AccountName, _, err = store.OpenEmail(c.log, username, false)
1487 if err != nil && errors.Is(err, store.ErrUnknownCredentials) {
1488 la.Result = store.AuthBadCredentials
1489 c.log.Info("failed authentication attempt", slog.String("username", username), slog.Any("remote", c.remoteIP))
1490 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass")
1492 xcheckf(err, "looking up address")
1493 la.AccountName = account.Name
1494 var ipadhash, opadhash hash.Hash
1495 account.WithRLock(func() {
1496 err := account.DB.Read(context.TODO(), func(tx *bstore.Tx) error {
1497 password, err := bstore.QueryTx[store.Password](tx).Get()
1498 if err == bstore.ErrAbsent {
1499 c.log.Info("failed authentication attempt", slog.String("username", username), slog.Any("remote", c.remoteIP))
1500 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass")
1506 ipadhash = password.CRAMMD5.Ipad
1507 opadhash = password.CRAMMD5.Opad
1510 xcheckf(err, "tx read")
1512 if ipadhash == nil || opadhash == nil {
1513 missingDerivedSecrets = true
1514 c.log.Info("cram-md5 auth attempt without derived secrets set, save password again to store secrets", slog.String("username", username))
1515 c.log.Info("failed authentication attempt", slog.String("username", username), slog.Any("remote", c.remoteIP))
1516 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass")
1520 ipadhash.Write([]byte(chal))
1521 opadhash.Write(ipadhash.Sum(nil))
1522 digest := fmt.Sprintf("%x", opadhash.Sum(nil))
1524 c.log.Info("failed authentication attempt", slog.String("username", username), slog.Any("remote", c.remoteIP))
1525 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass")
1528 case "SCRAM-SHA-256-PLUS", "SCRAM-SHA-256", "SCRAM-SHA-1-PLUS", "SCRAM-SHA-1":
1529 // 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?
1530 // todo: use single implementation between ../imapserver/server.go and ../smtpserver/server.go
1532 // Passwords cannot be retrieved or replayed from the trace.
1534 la.AuthMech = strings.ToLower(mech)
1535 var h func() hash.Hash
1536 switch la.AuthMech {
1537 case "scram-sha-1", "scram-sha-1-plus":
1539 case "scram-sha-256", "scram-sha-256-plus":
1542 xsmtpServerErrorf(codes{smtp.C554TransactionFailed, smtp.SeSys3Other0}, "missing scram auth method case")
1545 var cs *tls.ConnectionState
1546 channelBindingRequired := strings.HasSuffix(la.AuthMech, "-plus")
1547 if channelBindingRequired && !c.tls {
1549 xsmtpUserErrorf(smtp.C538EncReqForAuth, smtp.SePol7EncReqForAuth11, "scram plus mechanism requires tls connection")
1552 xcs := c.conn.(*tls.Conn).ConnectionState()
1555 c0 := xreadInitial("")
1556 ss, err := scram.NewServer(h, c0, cs, channelBindingRequired)
1558 c.log.Infox("scram protocol error", err, slog.Any("remote", c.remoteIP))
1559 xsmtpUserErrorf(smtp.C455BadParams, smtp.SePol7Other0, "scram protocol error: %s", err)
1561 username = ss.Authentication
1562 la.LoginAddress = username
1563 c.log.Debug("scram auth", slog.String("authentication", username))
1564 account, la.AccountName, _, err = store.OpenEmail(c.log, username, false)
1566 // todo: we could continue scram with a generated salt, deterministically generated
1567 // from the username. that way we don't have to store anything but attackers cannot
1568 // learn if an account exists. same for absent scram saltedpassword below.
1569 c.log.Info("failed authentication attempt", slog.String("username", username), slog.Any("remote", c.remoteIP))
1570 xsmtpUserErrorf(smtp.C454TempAuthFail, smtp.SeSys3Other0, "scram not possible")
1572 if ss.Authorization != "" && ss.Authorization != username {
1573 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "authentication with authorization for different user not supported")
1575 var xscram store.SCRAM
1576 account.WithRLock(func() {
1577 err := account.DB.Read(context.TODO(), func(tx *bstore.Tx) error {
1578 password, err := bstore.QueryTx[store.Password](tx).Get()
1579 if err == bstore.ErrAbsent {
1580 c.log.Info("failed authentication attempt", slog.String("username", username), slog.Any("remote", c.remoteIP))
1581 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass")
1583 xcheckf(err, "fetching credentials")
1584 switch la.AuthMech {
1585 case "scram-sha-1", "scram-sha-1-plus":
1586 xscram = password.SCRAMSHA1
1587 case "scram-sha-256", "scram-sha-256-plus":
1588 xscram = password.SCRAMSHA256
1590 xsmtpServerErrorf(codes{smtp.C554TransactionFailed, smtp.SeSys3Other0}, "missing scram auth credentials case")
1592 if len(xscram.Salt) == 0 || xscram.Iterations == 0 || len(xscram.SaltedPassword) == 0 {
1593 missingDerivedSecrets = true
1594 c.log.Info("scram auth attempt without derived secrets set, save password again to store secrets", slog.String("address", username))
1595 c.log.Info("failed authentication attempt", slog.String("username", username), slog.Any("remote", c.remoteIP))
1596 xsmtpUserErrorf(smtp.C454TempAuthFail, smtp.SeSys3Other0, "scram not possible")
1600 xcheckf(err, "read tx")
1602 s1, err := ss.ServerFirst(xscram.Iterations, xscram.Salt)
1603 xcheckf(err, "scram first server step")
1604 c.writelinef("%d %s", smtp.C334ContinueAuth, base64.StdEncoding.EncodeToString([]byte(s1))) //
../rfc/4954:187
1605 c2 := xreadContinuation()
1606 s3, err := ss.Finish(c2, xscram.SaltedPassword)
1608 c.writelinef("%d %s", smtp.C334ContinueAuth, base64.StdEncoding.EncodeToString([]byte(s3))) //
../rfc/4954:187
1611 c.readline() // Should be "*" for cancellation.
1612 if errors.Is(err, scram.ErrInvalidProof) {
1613 la.Result = store.AuthBadCredentials
1614 c.log.Info("failed authentication attempt", slog.String("username", username), slog.Any("remote", c.remoteIP))
1615 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad credentials")
1616 } else if errors.Is(err, scram.ErrChannelBindingsDontMatch) {
1617 la.Result = store.AuthBadChannelBinding
1618 c.log.Warn("bad channel binding during authentication, potential mitm", slog.String("username", username), slog.Any("remote", c.remoteIP))
1619 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7MsgIntegrity7, "channel bindings do not match, potential mitm")
1620 } else if errors.Is(err, scram.ErrInvalidEncoding) {
1621 la.Result = store.AuthBadProtocol
1622 c.log.Infox("bad scram protocol message", err, slog.String("username", username), slog.Any("remote", c.remoteIP))
1623 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7Other0, "bad scram protocol message")
1625 xcheckf(err, "server final")
1629 // The message should be empty. todo: should we require it is empty?
1633 la.AuthMech = "external"
1636 buf := xreadInitial("")
1637 username = norm.NFC.String(string(buf))
1638 la.LoginAddress = username
1642 xsmtpUserErrorf(smtp.C538EncReqForAuth, smtp.SePol7EncReqForAuth11, "tls required for tls client certificate authentication")
1644 if c.account == nil {
1645 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "missing client certificate, required for tls client certificate authentication")
1649 username = c.username
1650 la.LoginAddress = username
1653 account, la.AccountName, _, err = store.OpenEmail(c.log, username, false)
1654 xcheckf(err, "looking up username from tls client authentication")
1657 la.AuthMech = "(unrecognized)"
1659 xsmtpUserErrorf(smtp.C504ParamNotImpl, smtp.SeProto5BadParams4, "mechanism %s not supported", mech)
1662 if accConf, ok := account.Conf(); !ok {
1663 xcheckf(errors.New("cannot find account"), "get account config")
1664 } else if accConf.LoginDisabled != "" {
1665 la.Result = store.AuthLoginDisabled
1666 c.log.Info("account login disabled", slog.String("username", username))
1667 xsmtpUserErrorf(smtp.C525AccountDisabled, smtp.SePol7AccountDisabled13, "%w: %s", store.ErrLoginDisabled, accConf.LoginDisabled)
1670 // We may already have TLS credentials. We allow an additional SASL authentication,
1671 // possibly with different username, but the account must be the same.
1672 if c.account != nil {
1673 if account != c.account {
1674 c.log.Debug("sasl authentication for different account than tls client authentication, aborting connection",
1675 slog.String("saslmechanism", la.AuthMech),
1676 slog.String("saslaccount", account.Name),
1677 slog.String("tlsaccount", c.account.Name),
1678 slog.String("saslusername", username),
1679 slog.String("tlsusername", c.username),
1681 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "authentication failed, tls client certificate public key belongs to another account")
1682 } else if username != c.username {
1683 c.log.Debug("sasl authentication for different username than tls client certificate authentication, switching to sasl username",
1684 slog.String("saslmechanism", la.AuthMech),
1685 slog.String("saslusername", username),
1686 slog.String("tlsusername", c.username),
1687 slog.String("account", c.account.Name),
1692 account = nil // Prevent cleanup.
1694 c.username = username
1696 la.LoginAddress = c.username
1697 la.AccountName = c.account.Name
1698 la.Result = store.AuthSuccess
1703 c.writecodeline(smtp.C235AuthSuccess, smtp.SePol7Other0, "nice", nil)
1707func (c *conn) cmdMail(p *parser) {
1708 // requirements for maximum line length:
1710 // todo future: enforce? doesn't really seem worth it...
1712 if c.transactionBad > 10 && c.transactionGood == 0 {
1713 // If we get many bad transactions, it's probably a spammer that is guessing user names.
1714 // Useful in combination with rate limiting.
1716 c.writecodeline(smtp.C550MailboxUnavail, smtp.SeAddr1Other0, "too many failures", nil)
1722 if c.mailFrom != nil {
1724 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "already have MAIL")
1726 // Ensure clear transaction state on failure.
1737 // Microsoft Outlook 365 Apps for Enterprise sends it with submission. For delivery
1738 // it is mostly used by spammers, but has been seen with legitimate senders too.
1742 rawRevPath := p.xrawReversePath()
1743 paramSeen := map[string]bool{}
1746 key := p.xparamKeyword()
1748 K := strings.ToUpper(key)
1751 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5BadParams4, "duplicate param %q", key)
1759 if size > c.maxMessageSize {
1761 ecode := smtp.SeSys3MsgLimitExceeded4
1762 if size < config.DefaultMaxMsgSize {
1763 ecode = smtp.SeMailbox2MsgLimitExceeded3
1765 xsmtpUserErrorf(smtp.C552MailboxFull, ecode, "message too large")
1767 // We won't verify the message is exactly the size the remote claims. Buf if it is
1768 // larger, we'll abort the transaction when remote crosses the boundary.
1772 v := p.xparamValue()
1773 switch strings.ToUpper(v) {
1775 c.has8bitmime = false
1777 c.has8bitmime = true
1779 xsmtpUserErrorf(smtp.C555UnrecognizedAddrParams, smtp.SeProto5BadParams4, "unrecognized parameter %q", key)
1784 // We act as if we don't trust the client to specify a mailbox. Instead, we always
1785 // check the rfc5321.mailfrom and rfc5322.from before accepting the submission.
1789 // 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
1797 c.msgsmtputf8 = true
1801 xsmtpUserErrorf(smtp.C523EncryptionNeeded, smtp.SePol7EncNeeded10, "requiretls only allowed on tls-encrypted connections")
1802 } else if !c.extRequireTLS {
1803 xsmtpUserErrorf(smtp.C555UnrecognizedAddrParams, smtp.SeSys3NotSupported3, "REQUIRETLS not allowed for this connection")
1807 case "HOLDFOR", "HOLDUNTIL":
1810 xsmtpUserErrorf(smtp.C555UnrecognizedAddrParams, smtp.SeSys3NotSupported3, "unrecognized parameter %q", key)
1812 if K == "HOLDFOR" && paramSeen["HOLDUNTIL"] || K == "HOLDUNTIL" && paramSeen["HOLDFOR"] {
1814 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5BadParams4, "cannot use both HOLDUNTIL and HOLFOR")
1818 // semantic errors as syntax errors
1821 if n > int64(queue.FutureReleaseIntervalMax/time.Second) {
1823 xsmtpUserErrorf(smtp.C554TransactionFailed, smtp.SeProto5BadParams4, "future release interval too far in the future")
1825 c.futureRelease = time.Now().Add(time.Duration(n) * time.Second)
1826 c.futureReleaseRequest = fmt.Sprintf("for;%d", n)
1828 t, s := p.xdatetimeutc()
1829 ival := time.Until(t)
1831 // Likely a mistake by the user.
1832 xsmtpUserErrorf(smtp.C554TransactionFailed, smtp.SeProto5BadParams4, "requested future release time is in the past")
1833 } else if ival > queue.FutureReleaseIntervalMax {
1835 xsmtpUserErrorf(smtp.C554TransactionFailed, smtp.SeProto5BadParams4, "requested future release time is too far in the future")
1838 c.futureReleaseRequest = "until;" + s
1842 xsmtpUserErrorf(smtp.C555UnrecognizedAddrParams, smtp.SeSys3NotSupported3, "unrecognized parameter %q", key)
1846 // We now know if we have to parse the address with support for utf8.
1847 pp := newParser(rawRevPath, c.smtputf8, c)
1848 rpath := pp.xbareReversePath()
1853 // For submission, check if reverse path is allowed. I.e. authenticated account
1854 // must have the rpath configured. We do a check again on rfc5322.from during DATA.
1855 // Mail clients may use the alias address as smtp mail from address, so we allow it
1856 // for such aliases.
1857 rpathAllowed := func(disabled *bool) bool {
1863 from := smtp.NewAddress(rpath.Localpart, rpath.IPDomain.Domain)
1864 ok, dis := mox.AllowMsgFrom(c.account.Name, from)
1869 if !c.submission && !rpath.IPDomain.Domain.IsZero() {
1870 // If rpath domain has null MX record or is otherwise not accepting email, reject.
1873 cidctx := context.WithValue(mox.Context, mlog.CidKey, c.cid)
1874 ctx, cancel := context.WithTimeout(cidctx, time.Minute)
1875 valid, err := checkMXRecords(ctx, c.resolver, rpath.IPDomain.Domain)
1878 c.log.Infox("temporary reject for temporary mx lookup error", err)
1879 xsmtpServerErrorf(codes{smtp.C451LocalErr, smtp.SeNet4Other0}, "cannot verify mx records for mailfrom domain")
1881 c.log.Info("permanent reject because mailfrom domain does not accept mail")
1882 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SePol7SenderHasNullMX27, "mailfrom domain not configured for mail")
1887 if c.submission && (len(rpath.IPDomain.IP) > 0 || !rpathAllowed(&disabled)) {
1889 c.log.Info("submission with smtp mail from of disabled domain", slog.Any("domain", rpath.IPDomain.Domain))
1890 xsmtpServerErrorf(codes{smtp.C451LocalErr, smtp.SeSys3Other0}, "domain of smtp mail from is temporarily disabled")
1894 c.log.Info("submission with unconfigured mailfrom", slog.String("user", c.username), slog.String("mailfrom", rpath.String()))
1895 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SePol7DeliveryUnauth1, "must match authenticated user")
1896 } else if !c.submission && len(rpath.IPDomain.IP) > 0 {
1897 // todo future: allow if the IP is the same as this connection is coming from? does later code allow this?
1898 c.log.Info("delivery from address without domain", slog.String("mailfrom", rpath.String()))
1899 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SePol7Other0, "domain name required")
1902 if Localserve && strings.HasPrefix(string(rpath.Localpart), "mailfrom") {
1903 c.xlocalserveError(rpath.Localpart)
1908 c.bwritecodeline(smtp.C250Completed, smtp.SeAddr1Other0, "looking good", nil)
1912func (c *conn) cmdRcpt(p *parser) {
1915 if c.mailFrom == nil {
1917 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "missing MAIL FROM")
1923 // Microsoft Outlook 365 Apps for Enterprise sends it with submission. For delivery
1924 // it is mostly used by spammers, but has been seen with legitimate senders too.
1929 if p.take("<POSTMASTER>") {
1930 fpath = smtp.Path{Localpart: "postmaster"}
1932 fpath = p.xforwardPath()
1936 key := p.xparamKeyword()
1937 // K := strings.ToUpper(key)
1940 xsmtpUserErrorf(smtp.C555UnrecognizedAddrParams, smtp.SeSys3NotSupported3, "unrecognized parameter %q", key)
1944 // Check if TLS is enabled if required. It's not great that sender/recipient
1945 // addresses may have been exposed in plaintext before we can reject delivery. The
1946 // recipient could be the tls reporting addresses, which must always be able to
1947 // receive in plain text.
1948 c.xneedTLSForDelivery(fpath)
1950 // todo future: for submission, should we do explicit verification that domains are fully qualified? also for mail from.
../rfc/6409:420
1952 if len(c.recipients) >= rcptToLimit {
1954 xsmtpUserErrorf(smtp.C452StorageFull, smtp.SeProto5TooManyRcpts3, "max of %d recipients reached", rcptToLimit)
1957 // We don't want to allow delivery to multiple recipients with a null reverse path.
1958 // Why would anyone send like that? Null reverse path is intended for delivery
1959 // notifications, they should go to a single recipient.
1960 if !c.submission && len(c.recipients) > 0 && c.mailFrom.IsZero() {
1961 xsmtpUserErrorf(smtp.C452StorageFull, smtp.SeProto5TooManyRcpts3, "only one recipient allowed with null reverse address")
1964 // Do not accept multiple recipients if remote does not pass SPF. Because we don't
1965 // want to generate DSNs to unverified domains. This is the moment we
1966 // can refuse individual recipients, DATA will be too late. Because mail
1967 // servers must handle a max recipient limit gracefully and still send to the
1968 // recipients that are accepted, this should not cause problems. Though we are in
1969 // violation because the limit must be >= 100.
1973 if !c.submission && len(c.recipients) == 1 && !Localserve {
1974 // note: because of check above, mailFrom cannot be the null address.
1976 d := c.mailFrom.IPDomain.Domain
1978 // todo: use this spf result for DATA.
1979 spfArgs := spf.Args{
1980 RemoteIP: c.remoteIP,
1981 MailFromLocalpart: c.mailFrom.Localpart,
1983 HelloDomain: c.hello,
1985 LocalHostname: c.hostname,
1987 cidctx := context.WithValue(mox.Context, mlog.CidKey, c.cid)
1988 spfctx, spfcancel := context.WithTimeout(cidctx, time.Minute)
1990 receivedSPF, _, _, _, err := spf.Verify(spfctx, c.log.Logger, c.resolver, spfArgs)
1993 c.log.Errorx("spf verify for multiple recipients", err)
1995 pass = receivedSPF.Identity == spf.ReceivedMailFrom && receivedSPF.Result == spf.StatusPass
1998 xsmtpUserErrorf(smtp.C452StorageFull, smtp.SeProto5TooManyRcpts3, "only one recipient allowed without spf pass")
2002 if Localserve && strings.HasPrefix(string(fpath.Localpart), "rcptto") {
2003 c.xlocalserveError(fpath.Localpart)
2006 if len(fpath.IPDomain.IP) > 0 {
2008 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeAddr1UnknownDestMailbox1, "not accepting email for ip")
2010 c.recipients = append(c.recipients, recipient{fpath, nil, nil})
2011 } else if accountName, alias, canonical, dest, err := mox.LookupAddress(fpath.Localpart, fpath.IPDomain.Domain, true, true, true); err == nil {
2014 c.recipients = append(c.recipients, recipient{fpath, nil, &rcptAlias{*alias, canonical}})
2015 } else if dest.SMTPError != "" {
2016 xsmtpServerErrorf(codes{dest.SMTPErrorCode, dest.SMTPErrorSecode}, "%s", dest.SMTPErrorMsg)
2018 c.recipients = append(c.recipients, recipient{fpath, &rcptAccount{accountName, dest, canonical}, nil})
2021 } else if Localserve {
2022 // If the address isn't known, and we are in localserve, deliver to the mox user.
2023 // If account or destination doesn't exist, it will be handled during delivery. For
2024 // submissions, which is the common case, we'll deliver to the logged in user,
2025 // which is typically the mox user.
2026 acc, _ := mox.Conf.Account("mox")
2027 dest := acc.Destinations["mox@localhost"]
2028 c.recipients = append(c.recipients, recipient{fpath, &rcptAccount{"mox", dest, "mox@localhost"}, nil})
2029 } else if errors.Is(err, mox.ErrDomainDisabled) {
2030 c.log.Info("smtp recipient for temporarily disabled domain", slog.Any("domain", fpath.IPDomain.Domain))
2031 xsmtpUserErrorf(smtp.C450MailboxUnavail, smtp.SeMailbox2Disabled1, "recipient domain temporarily disabled")
2032 } else if errors.Is(err, mox.ErrDomainNotFound) {
2034 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeAddr1UnknownDestMailbox1, "not accepting email for domain")
2036 // We'll be delivering this email.
2037 c.recipients = append(c.recipients, recipient{fpath, nil, nil})
2038 } else if errors.Is(err, mox.ErrAddressNotFound) {
2040 // For submission, we're transparent about which user exists. Should be fine for the typical small-scale deploy.
2042 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeAddr1UnknownDestMailbox1, "no such user")
2044 // We pretend to accept. We don't want to let remote know the user does not exist
2045 // until after DATA. Because then remote has committed to sending a message.
2046 // note: not local for !c.submission is the signal this address is in error.
2047 c.recipients = append(c.recipients, recipient{fpath, nil, nil})
2049 c.log.Errorx("looking up account for delivery", err, slog.Any("rcptto", fpath))
2050 xsmtpServerErrorf(codes{smtp.C451LocalErr, smtp.SeSys3Other0}, "error processing")
2052 c.bwritecodeline(smtp.C250Completed, smtp.SeAddr1Other0, "now on the list", nil)
2055func hasNonASCII(s string) bool {
2056 for _, c := range []byte(s) {
2057 if c > unicode.MaxASCII {
2065func (c *conn) isSMTPUTF8Required(part *message.Part) bool {
2066 // Check "MAIL FROM".
2067 if hasNonASCII(string(c.mailFrom.Localpart)) {
2070 // Check all "RCPT TO".
2071 for _, rcpt := range c.recipients {
2072 if hasNonASCII(string(rcpt.Addr.Localpart)) {
2077 // Check header in all message parts.
2078 smtputf8, err := part.NeedsSMTPUTF8()
2079 xcheckf(err, "checking if smtputf8 is required")
2084func (c *conn) cmdData(p *parser) {
2087 if c.mailFrom == nil {
2089 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "missing MAIL FROM")
2091 if len(c.recipients) == 0 {
2093 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "missing RCPT TO")
2099 // 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.
2101 // Entire delivery should be done within 30 minutes, or we abort.
2102 cidctx := context.WithValue(mox.Context, mlog.CidKey, c.cid)
2103 cmdctx, cmdcancel := context.WithTimeout(cidctx, 30*time.Minute)
2105 // Deadline is taken into account by Read and Write.
2106 c.deadline, _ = cmdctx.Deadline()
2108 c.deadline = time.Time{}
2112 c.writelinef("354 see you at the bare dot")
2114 // Mark as tracedata.
2115 defer c.xtrace(mlog.LevelTracedata)()
2117 // We read the data into a temporary file. We limit the size and do basic analysis while reading.
2118 dataFile, err := store.CreateMessageTemp(c.log, "smtp-deliver")
2120 xsmtpServerErrorf(errCodes(smtp.C451LocalErr, smtp.SeSys3Other0, err), "creating temporary file for message: %s", err)
2122 defer store.CloseRemoveTempFile(c.log, dataFile, "smtpserver delivered message")
2123 msgWriter := message.NewWriter(dataFile)
2124 dr := smtp.NewDataReader(c.r)
2125 n, err := io.Copy(&limitWriter{maxSize: c.maxMessageSize, w: msgWriter}, dr)
2126 c.xtrace(mlog.LevelTrace) // Restore.
2128 if errors.Is(err, errMessageTooLarge) {
2130 ecode := smtp.SeSys3MsgLimitExceeded4
2131 if n < config.DefaultMaxMsgSize {
2132 ecode = smtp.SeMailbox2MsgLimitExceeded3
2134 c.writecodeline(smtp.C451LocalErr, ecode, fmt.Sprintf("error copying data to file (%s)", mox.ReceivedID(c.cid)), err)
2135 panic(fmt.Errorf("remote sent too much DATA: %w", errIO))
2138 if errors.Is(err, smtp.ErrCRLF) {
2139 c.writecodeline(smtp.C500BadSyntax, smtp.SeProto5Syntax2, fmt.Sprintf("invalid bare \\r or \\n, may be smtp smuggling (%s)", mox.ReceivedID(c.cid)), err)
2143 // Something is failing on our side. We want to let remote know. So write an error response,
2144 // then discard the remaining data so the remote client is more likely to see our
2145 // response. Our write is synchronous, there is a risk no window/buffer space is
2146 // available and our write blocks us from reading remaining data, leading to
2147 // deadlock. We have a timeout on our connection writes though, so worst case we'll
2148 // abort the connection due to expiration.
2149 c.writecodeline(smtp.C451LocalErr, smtp.SeSys3Other0, fmt.Sprintf("error copying data to file (%s)", mox.ReceivedID(c.cid)), err)
2150 io.Copy(io.Discard, dr)
2154 // Basic sanity checks on messages before we send them out to the world. Just
2155 // trying to be strict in what we do to others and liberal in what we accept.
2157 if !msgWriter.HaveBody {
2159 xsmtpUserErrorf(smtp.C554TransactionFailed, smtp.SeMsg6Other0, "message requires both header and body section")
2161 // Check only for pedantic mode because ios mail will attempt to send smtputf8 with
2162 // non-ascii in message from localpart without using 8bitmime.
2163 if mox.Pedantic && msgWriter.Has8bit && !c.has8bitmime {
2165 xsmtpUserErrorf(smtp.C500BadSyntax, smtp.SeMsg6Other0, "message with non-us-ascii requires 8bitmime extension")
2169 if Localserve && mox.Pedantic {
2170 // Require that message can be parsed fully.
2171 p, err := message.Parse(c.log.Logger, false, dataFile)
2173 err = p.Walk(c.log.Logger, nil)
2177 xsmtpUserErrorf(smtp.C554TransactionFailed, smtp.SeMsg6Other0, "malformed message: %v", err)
2181 // Now that we have all the whole message (envelope + data), we can check if the SMTPUTF8 extension is required.
2182 var part *message.Part
2183 if c.smtputf8 || c.submission || mox.Pedantic {
2184 // Try to parse the message.
2185 // Do nothing if something bad happen during Parse and Walk, just keep the current value for c.msgsmtputf8.
2186 p, err := message.Parse(c.log.Logger, true, dataFile)
2188 // Message parsed without error. Keep the result to avoid parsing the message again.
2190 err = part.Walk(c.log.Logger, nil)
2192 c.msgsmtputf8 = c.isSMTPUTF8Required(part)
2196 c.log.Debugx("parsing message for smtputf8 check", err)
2198 if c.smtputf8 != c.msgsmtputf8 {
2199 c.log.Debug("smtputf8 flag changed", slog.Bool("smtputf8", c.smtputf8), slog.Bool("msgsmtputf8", c.msgsmtputf8))
2202 if !c.smtputf8 && c.msgsmtputf8 && mox.Pedantic {
2203 metricSubmission.WithLabelValues("missingsmtputf8").Inc()
2204 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeMsg6Other0, "smtputf8 extension is required but was not added to the MAIL command")
2207 // Prepare "Received" header.
2211 var iprevStatus iprev.Status // Only for delivery, not submission.
2212 var iprevAuthentic bool
2214 // Hide internal hosts.
2215 // todo future: make this a config option, where admins specify ip ranges that they don't want exposed. also see
../rfc/5321:4321
2216 recvFrom = message.HeaderCommentDomain(mox.Conf.Static.HostnameDomain, c.msgsmtputf8)
2218 if len(c.hello.IP) > 0 {
2219 recvFrom = smtp.AddressLiteral(c.hello.IP)
2221 // ASCII-only version added after the extended-domain syntax below, because the
2222 // comment belongs to "BY" which comes immediately after "FROM".
2223 recvFrom = c.hello.Domain.XName(c.msgsmtputf8)
2225 iprevctx, iprevcancel := context.WithTimeout(cmdctx, time.Minute)
2227 var revNames []string
2228 iprevStatus, revName, revNames, iprevAuthentic, err = iprev.Lookup(iprevctx, c.resolver, c.remoteIP)
2231 c.log.Infox("reverse-forward lookup", err, slog.Any("remoteip", c.remoteIP))
2233 c.log.Debug("dns iprev check", slog.Any("addr", c.remoteIP), slog.Any("status", iprevStatus))
2237 } else if len(revNames) > 0 {
2240 name = strings.TrimSuffix(name, ".")
2242 if name != "" && name != c.hello.Domain.XName(c.msgsmtputf8) {
2243 recvFrom += name + " "
2245 recvFrom += smtp.AddressLiteral(c.remoteIP) + ")"
2246 if c.msgsmtputf8 && c.hello.Domain.Unicode != "" {
2247 recvFrom += " (" + c.hello.Domain.ASCII + ")"
2250 recvBy := mox.Conf.Static.HostnameDomain.XName(c.msgsmtputf8)
2251 recvBy += " (" + smtp.AddressLiteral(c.localIP) + ")" // todo: hide ip if internal?
2252 if c.msgsmtputf8 && mox.Conf.Static.HostnameDomain.Unicode != "" {
2253 // This syntax is part of "VIA".
2254 recvBy += " (" + mox.Conf.Static.HostnameDomain.ASCII + ")"
2267 if c.account != nil {
2272 // Assume transaction does not succeed. If it does, we'll compensate.
2275 recvHdrFor := func(rcptTo string) string {
2276 recvHdr := &message.HeaderWriter{}
2277 // For additional Received-header clauses, see:
2278 // https://www.iana.org/assignments/mail-parameters/mail-parameters.xhtml#table-mail-parameters-8
2280 if c.requireTLS != nil && *c.requireTLS {
2282 withComment = " (requiretls)"
2284 recvHdr.Add(" ", "Received:", "from", recvFrom, "by", recvBy, "via", "tcp", "with", with+withComment, "id", mox.ReceivedID(c.cid)) //
../rfc/5321:3158
2286 tlsConn := c.conn.(*tls.Conn)
2287 tlsComment := mox.TLSReceivedComment(c.log, tlsConn.ConnectionState())
2288 recvHdr.Add(" ", tlsComment...)
2290 // We leave out an empty "for" clause. This is empty for messages submitted to
2291 // multiple recipients, so the message stays identical and a single smtp
2292 // transaction can deliver, only transferring the data once.
2294 recvHdr.Add(" ", "for", "<"+rcptTo+">;")
2296 recvHdr.Add(" ", time.Now().Format(message.RFC5322Z))
2297 return recvHdr.String()
2300 // Submission is easiest because user is trusted. Far fewer checks to make. So
2301 // handle it first, and leave the rest of the function for handling wild west
2302 // internet traffic.
2304 c.submit(cmdctx, recvHdrFor, msgWriter, dataFile, part)
2306 c.deliver(cmdctx, recvHdrFor, msgWriter, iprevStatus, iprevAuthentic, dataFile)
2310// Check if a message has unambiguous "TLS-Required: No" header. Messages must not
2311// contain multiple TLS-Required headers. The only valid value is "no". But we'll
2312// accept multiple headers as long as all they are all "no".
2314func hasTLSRequiredNo(h textproto.MIMEHeader) bool {
2315 l := h.Values("Tls-Required")
2319 for _, v := range l {
2320 if !strings.EqualFold(v, "no") {
2327// submit is used for mail from authenticated users that we will try to deliver.
2328func (c *conn) submit(ctx context.Context, recvHdrFor func(string) string, msgWriter *message.Writer, dataFile *os.File, part *message.Part) {
2329 // Similar between ../smtpserver/server.go:/submit\( and ../webmail/api.go:/MessageSubmit\( and ../webapisrv/server.go:/Send\(
2331 var msgPrefix []byte
2333 // Check that user is only sending email as one of its configured identities. Not
2337 msgFrom, _, header, err := message.From(c.log.Logger, true, dataFile, part)
2339 metricSubmission.WithLabelValues("badmessage").Inc()
2340 c.log.Infox("parsing message From address", err, slog.String("user", c.username))
2341 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeMsg6Other0, "cannot parse header or From address: %v", err)
2343 if ok, disabled := mox.AllowMsgFrom(c.account.Name, msgFrom); disabled {
2344 c.log.Info("submission with message from address of disabled domain", slog.Any("domain", msgFrom.Domain))
2345 xsmtpServerErrorf(codes{smtp.C451LocalErr, smtp.SeSys3Other0}, "domain of message from header is temporarily disabled")
2348 metricSubmission.WithLabelValues("badfrom").Inc()
2349 c.log.Infox("verifying message from address", mox.ErrAddressNotFound, slog.String("user", c.username), slog.Any("msgfrom", msgFrom))
2350 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SePol7DeliveryUnauth1, "message from address must belong to authenticated user")
2353 // TLS-Required: No header makes us not enforce recipient domain's TLS policy.
2356 if c.requireTLS == nil && hasTLSRequiredNo(header) {
2361 // Outgoing messages should not have a Return-Path header. The final receiving mail
2362 // server will add it.
2364 if mox.Pedantic && header.Values("Return-Path") != nil {
2365 metricSubmission.WithLabelValues("badheader").Inc()
2366 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeMsg6Other0, "message should not have Return-Path header")
2369 // Add Message-Id header if missing.
2371 messageID := header.Get("Message-Id")
2372 if messageID == "" {
2373 messageID = mox.MessageIDGen(c.msgsmtputf8)
2374 msgPrefix = append(msgPrefix, fmt.Sprintf("Message-Id: <%s>\r\n", messageID)...)
2378 if header.Get("Date") == "" {
2379 msgPrefix = append(msgPrefix, "Date: "+time.Now().Format(message.RFC5322Z)+"\r\n"...)
2382 // Check outgoing message rate limit.
2383 err = c.account.DB.Read(ctx, func(tx *bstore.Tx) error {
2384 rcpts := make([]smtp.Path, len(c.recipients))
2385 for i, r := range c.recipients {
2388 msglimit, rcptlimit, err := c.account.SendLimitReached(tx, rcpts)
2389 xcheckf(err, "checking sender limit")
2391 metricSubmission.WithLabelValues("messagelimiterror").Inc()
2392 xsmtpUserErrorf(smtp.C451LocalErr, smtp.SePol7DeliveryUnauth1, "max number of messages (%d) over past 24h reached, try increasing per-account setting MaxOutgoingMessagesPerDay", msglimit)
2393 } else if rcptlimit >= 0 {
2394 metricSubmission.WithLabelValues("recipientlimiterror").Inc()
2395 xsmtpUserErrorf(smtp.C451LocalErr, smtp.SePol7DeliveryUnauth1, "max number of new/first-time recipients (%d) over past 24h reached, try increasing per-account setting MaxFirstTimeRecipientsPerDay", rcptlimit)
2399 xcheckf(err, "read-only transaction")
2401 // We gather any X-Mox-Extra-* headers into the "extra" data during queueing, which
2402 // will make it into any webhook we deliver.
2403 // todo: remove the X-Mox-Extra-* headers from the message. we don't currently rewrite the message...
2404 // todo: should we not canonicalize keys?
2405 var extra map[string]string
2406 for k, vl := range header {
2407 if !strings.HasPrefix(k, "X-Mox-Extra-") {
2411 extra = map[string]string{}
2413 xk := k[len("X-Mox-Extra-"):]
2414 // We don't allow duplicate keys.
2415 if _, ok := extra[xk]; ok || len(vl) > 1 {
2416 xsmtpUserErrorf(smtp.C554TransactionFailed, smtp.SeMsg6Other0, "duplicate x-mox-extra- key %q", xk)
2418 extra[xk] = vl[len(vl)-1]
2421 // 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.
2423 // Add DKIM signatures.
2424 confDom, ok := mox.Conf.Domain(msgFrom.Domain)
2426 c.log.Error("domain disappeared", slog.Any("domain", msgFrom.Domain))
2427 xsmtpServerErrorf(codes{smtp.C451LocalErr, smtp.SeSys3Other0}, "internal error")
2428 } else if confDom.Disabled {
2429 c.log.Info("submission with message from address of disabled domain", slog.Any("domain", msgFrom.Domain))
2430 xsmtpServerErrorf(codes{smtp.C451LocalErr, smtp.SeSys3Other0}, "domain of message from header is temporarily disabled")
2433 selectors := mox.DKIMSelectors(confDom.DKIM)
2434 if len(selectors) > 0 {
2435 canonical := mox.CanonicalLocalpart(msgFrom.Localpart, confDom)
2436 if dkimHeaders, err := dkim.Sign(ctx, c.log.Logger, canonical, msgFrom.Domain, selectors, c.msgsmtputf8, store.FileMsgReader(msgPrefix, dataFile)); err != nil {
2437 c.log.Errorx("dkim sign for domain", err, slog.Any("domain", msgFrom.Domain))
2438 metricServerErrors.WithLabelValues("dkimsign").Inc()
2440 msgPrefix = append(msgPrefix, []byte(dkimHeaders)...)
2444 authResults := message.AuthResults{
2445 Hostname: mox.Conf.Static.HostnameDomain.XName(c.msgsmtputf8),
2446 Comment: mox.Conf.Static.HostnameDomain.ASCIIExtra(c.msgsmtputf8),
2447 Methods: []message.AuthMethod{
2451 Props: []message.AuthProp{
2452 message.MakeAuthProp("smtp", "mailfrom", c.mailFrom.XString(c.msgsmtputf8), true, c.mailFrom.ASCIIExtra(c.msgsmtputf8)),
2457 msgPrefix = append(msgPrefix, []byte(authResults.Header())...)
2459 // We always deliver through the queue. It would be more efficient to deliver
2460 // directly for local accounts, but we don't want to circumvent all the anti-spam
2461 // measures. Accounts on a single mox instance should be allowed to block each
2464 accConf, _ := c.account.Conf()
2465 loginAddr, err := smtp.ParseAddress(c.username)
2466 xcheckf(err, "parsing login address")
2467 useFromID := slices.Contains(accConf.ParsedFromIDLoginAddresses, loginAddr)
2468 var localpartBase string
2472 // With submission, user can bring their own fromid.
2473 t := strings.SplitN(string(c.mailFrom.Localpart), confDom.LocalpartCatchallSeparator, 2)
2474 localpartBase = t[0]
2477 if fromID != "" && len(c.recipients) > 1 {
2478 xsmtpServerErrorf(codes{smtp.C554TransactionFailed, smtp.SeProto5TooManyRcpts3}, "cannot send to multiple recipients with chosen fromid")
2485 qml := make([]queue.Msg, len(c.recipients))
2486 for i, rcpt := range c.recipients {
2488 code, timeout := mox.LocalserveNeedsError(rcpt.Addr.Localpart)
2490 c.log.Info("timing out submission due to special localpart")
2491 mox.Sleep(mox.Context, time.Hour)
2492 xsmtpServerErrorf(codes{smtp.C451LocalErr, smtp.SeSys3Other0}, "timing out submission due to special localpart")
2493 } else if code != 0 {
2494 c.log.Info("failure due to special localpart", slog.Int("code", code))
2495 xsmtpServerErrorf(codes{code, smtp.SeOther00}, "failure with code %d due to special localpart", code)
2502 fromID = xrandomID(16)
2504 fp.Localpart = smtp.Localpart(localpartBase + confDom.LocalpartCatchallSeparator + fromID)
2507 // For multiple recipients, we don't make each message prefix unique, leaving out
2508 // the "for" clause in the Received header. This allows the queue to deliver the
2509 // messages in a single smtp transaction.
2511 if len(c.recipients) == 1 {
2512 rcptTo = rcpt.Addr.String()
2514 xmsgPrefix := append([]byte(recvHdrFor(rcptTo)), msgPrefix...)
2515 msgSize := int64(len(xmsgPrefix)) + msgWriter.Size
2516 qm := queue.MakeMsg(fp, rcpt.Addr, msgWriter.Has8bit, c.msgsmtputf8, msgSize, messageID, xmsgPrefix, c.requireTLS, now, header.Get("Subject"))
2517 if !c.futureRelease.IsZero() {
2518 qm.NextAttempt = c.futureRelease
2519 qm.FutureReleaseRequest = c.futureReleaseRequest
2526 // 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
2527 if err := queue.Add(ctx, c.log, c.account.Name, dataFile, qml...); err != nil && errors.Is(err, queue.ErrFromID) && !genFromID {
2528 // todo: should we return this error during the "rcpt to" command?
2529 // secode is not an exact match, but seems closest.
2530 xsmtpServerErrorf(errCodes(smtp.C554TransactionFailed, smtp.SeAddr1SenderSyntax7, err), "bad fromid in smtp mail from address: %s", err)
2531 } else if err != nil {
2532 // Aborting the transaction is not great. But continuing and generating DSNs will
2533 // probably result in errors as well...
2534 metricSubmission.WithLabelValues("queueerror").Inc()
2535 c.log.Errorx("queuing message", err)
2536 xsmtpServerErrorf(errCodes(smtp.C451LocalErr, smtp.SeSys3Other0, err), "error delivering message: %v", err)
2538 metricSubmission.WithLabelValues("ok").Inc()
2539 for i, rcpt := range c.recipients {
2540 c.log.Info("messages queued for delivery",
2541 slog.Any("mailfrom", *c.mailFrom),
2542 slog.Any("rcptto", rcpt.Addr),
2543 slog.Bool("smtputf8", c.smtputf8),
2544 slog.Bool("msgsmtputf8", c.msgsmtputf8),
2545 slog.Int64("msgsize", qml[i].Size))
2548 err = c.account.DB.Write(ctx, func(tx *bstore.Tx) error {
2549 for _, rcpt := range c.recipients {
2550 outgoing := store.Outgoing{Recipient: rcpt.Addr.XString(true)}
2551 if err := tx.Insert(&outgoing); err != nil {
2552 return fmt.Errorf("adding outgoing message: %v", err)
2557 xcheckf(err, "adding outgoing messages")
2560 c.transactionBad-- // Compensate for early earlier pessimistic increase.
2563 c.writecodeline(smtp.C250Completed, smtp.SeMailbox2Other0, "it is done", nil)
2566func xrandomID(n int) string {
2567 return base64.RawURLEncoding.EncodeToString(xrandom(n))
2570func xrandom(n int) []byte {
2571 buf := make([]byte, n)
2572 x, err := cryptorand.Read(buf)
2573 xcheckf(err, "read random")
2575 xcheckf(errors.New("short random read"), "read random")
2580func ipmasked(ip net.IP) (string, string, string) {
2581 if ip.To4() != nil {
2583 m2 := ip.Mask(net.CIDRMask(26, 32)).String()
2584 m3 := ip.Mask(net.CIDRMask(21, 32)).String()
2587 m1 := ip.Mask(net.CIDRMask(64, 128)).String()
2588 m2 := ip.Mask(net.CIDRMask(48, 128)).String()
2589 m3 := ip.Mask(net.CIDRMask(32, 128)).String()
2593func (c *conn) xlocalserveError(lp smtp.Localpart) {
2594 code, timeout := mox.LocalserveNeedsError(lp)
2596 c.log.Info("timing out due to special localpart")
2597 mox.Sleep(mox.Context, time.Hour)
2598 xsmtpServerErrorf(codes{smtp.C451LocalErr, smtp.SeSys3Other0}, "timing out command due to special localpart")
2599 } else if code != 0 {
2600 c.log.Info("failure due to special localpart", slog.Int("code", code))
2601 metricDelivery.WithLabelValues("delivererror", "localserve").Inc()
2602 xsmtpServerErrorf(codes{code, smtp.SeOther00}, "failure with code %d due to special localpart", code)
2606// deliver is called for incoming messages from external, typically untrusted
2607// sources. i.e. not submitted by authenticated users.
2608func (c *conn) deliver(ctx context.Context, recvHdrFor func(string) string, msgWriter *message.Writer, iprevStatus iprev.Status, iprevAuthentic bool, dataFile *os.File) {
2609 // 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.
2611 var msgFrom smtp.Address
2612 var envelope *message.Envelope
2613 var headers textproto.MIMEHeader
2615 part, err := message.Parse(c.log.Logger, false, dataFile)
2617 // 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?
2618 isDSN = part.MediaType == "MULTIPART" && part.MediaSubType == "REPORT" && strings.EqualFold(part.ContentTypeParams["report-type"], "delivery-status")
2619 msgFrom, envelope, headers, err = message.From(c.log.Logger, false, dataFile, &part)
2622 c.log.Infox("parsing message for From address", err)
2626 if len(headers.Values("Received")) > 100 {
2627 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeNet4Loop6, "loop detected, more than 100 Received headers")
2630 // TLS-Required: No header makes us not enforce recipient domain's TLS policy.
2631 // Since we only deliver locally at the moment, this won't influence our behaviour.
2632 // Once we forward, it would our delivery attempts.
2635 if c.requireTLS == nil && hasTLSRequiredNo(headers) {
2640 // We'll be building up an Authentication-Results header.
2641 authResults := message.AuthResults{
2642 Hostname: mox.Conf.Static.HostnameDomain.XName(c.msgsmtputf8),
2645 commentAuthentic := func(v bool) string {
2647 return "with dnssec"
2649 return "without dnssec"
2652 // Reverse IP lookup results.
2653 // todo future: how useful is this?
2655 authResults.Methods = append(authResults.Methods, message.AuthMethod{
2657 Result: string(iprevStatus),
2658 Comment: commentAuthentic(iprevAuthentic),
2659 Props: []message.AuthProp{
2660 message.MakeAuthProp("policy", "iprev", c.remoteIP.String(), false, ""),
2664 // SPF and DKIM verification in parallel.
2665 var wg sync.WaitGroup
2669 var dkimResults []dkim.Result
2673 x := recover() // Should not happen, but don't take program down if it does.
2675 c.log.Error("dkim verify panic", slog.Any("err", x))
2677 metrics.PanicInc(metrics.Dkimverify)
2681 // We always evaluate all signatures. We want to build up reputation for each
2682 // domain in the signature.
2683 const ignoreTestMode = false
2684 // todo future: longer timeout? we have to read through the entire email, which can be large, possibly multiple times.
2685 dkimctx, dkimcancel := context.WithTimeout(ctx, time.Minute)
2687 // todo future: we could let user configure which dkim headers they require
2689 // For localserve, fake dkim selector DNS records for hosted domains to give
2690 // dkim-signatures a chance to pass for deliveries from queue.
2691 resolver := c.resolver
2693 // Lookup based on message From address is an approximation.
2694 if dc, ok := mox.Conf.Domain(msgFrom.Domain); ok && len(dc.DKIM.Selectors) > 0 {
2695 txts := map[string][]string{}
2696 for name, sel := range dc.DKIM.Selectors {
2697 dkimr := dkim.Record{
2699 Hashes: []string{sel.HashEffective},
2700 PublicKey: sel.Key.Public(),
2702 if _, ok := sel.Key.(ed25519.PrivateKey); ok {
2703 dkimr.Key = "ed25519"
2704 } else if _, ok := sel.Key.(*rsa.PrivateKey); !ok {
2705 err := fmt.Errorf("unrecognized private key for DKIM selector %q: %T", name, sel.Key)
2706 xcheckf(err, "making dkim record")
2708 txt, err := dkimr.Record()
2709 xcheckf(err, "making DKIM DNS TXT record")
2710 txts[name+"._domainkey."+msgFrom.Domain.ASCII+"."] = []string{txt}
2712 resolver = dns.MockResolver{TXT: txts}
2715 dkimResults, dkimErr = dkim.Verify(dkimctx, c.log.Logger, resolver, c.msgsmtputf8, dkim.DefaultPolicy, dataFile, ignoreTestMode)
2721 var receivedSPF spf.Received
2722 var spfDomain dns.Domain
2724 var spfAuthentic bool
2726 spfArgs := spf.Args{
2727 RemoteIP: c.remoteIP,
2728 MailFromLocalpart: c.mailFrom.Localpart,
2729 MailFromDomain: c.mailFrom.IPDomain.Domain, // Can be empty.
2730 HelloDomain: c.hello,
2732 LocalHostname: c.hostname,
2737 x := recover() // Should not happen, but don't take program down if it does.
2739 c.log.Error("spf verify panic", slog.Any("err", x))
2741 metrics.PanicInc(metrics.Spfverify)
2745 spfctx, spfcancel := context.WithTimeout(ctx, time.Minute)
2747 resolver := c.resolver
2748 // For localserve, give hosted domains a chance to pass for deliveries from queue.
2749 if Localserve && c.remoteIP.IsLoopback() {
2750 // Lookup based on message From address is an approximation.
2751 if _, ok := mox.Conf.Domain(msgFrom.Domain); ok {
2752 resolver = dns.MockResolver{
2753 TXT: map[string][]string{msgFrom.Domain.ASCII + ".": {"v=spf1 ip4:127.0.0.1/8 ip6:::1 ~all"}},
2757 receivedSPF, spfDomain, spfExpl, spfAuthentic, spfErr = spf.Verify(spfctx, c.log.Logger, resolver, spfArgs)
2760 c.log.Infox("spf verify", spfErr)
2764 // Wait for DKIM and SPF validation to finish.
2767 // Give immediate response if all recipients are unknown.
2769 for _, r := range c.recipients {
2770 if r.Account == nil && r.Alias == nil {
2774 if nunknown == len(c.recipients) {
2775 // During RCPT TO we found that the address does not exist.
2776 c.log.Info("deliver attempt to unknown user(s)", slog.Any("recipients", c.recipients))
2778 // Crude attempt to slow down someone trying to guess names. Would work better
2779 // with connection rate limiter.
2780 if unknownRecipientsDelay > 0 {
2781 mox.Sleep(ctx, unknownRecipientsDelay)
2784 // 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.
2785 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeAddr1UnknownDestMailbox1, "no such user(s)")
2788 // Add DKIM results to Authentication-Results header.
2789 authResAddDKIM := func(result, comment, reason string, props []message.AuthProp) {
2790 dm := message.AuthMethod{
2797 authResults.Methods = append(authResults.Methods, dm)
2800 c.log.Errorx("dkim verify", dkimErr)
2801 authResAddDKIM("none", "", dkimErr.Error(), nil)
2802 } else if len(dkimResults) == 0 {
2803 c.log.Info("no dkim-signature header", slog.Any("mailfrom", c.mailFrom))
2804 authResAddDKIM("none", "", "no dkim signatures", nil)
2806 for i, r := range dkimResults {
2807 var domain, selector dns.Domain
2808 var identity *dkim.Identity
2810 var props []message.AuthProp
2812 if r.Record != nil && r.Record.PublicKey != nil {
2813 if pubkey, ok := r.Record.PublicKey.(*rsa.PublicKey); ok {
2814 comment = fmt.Sprintf("%d bit rsa, ", pubkey.N.BitLen())
2818 sig := base64.StdEncoding.EncodeToString(r.Sig.Signature)
2819 sig = sig[:12] // Must be at least 8 characters and unique among the signatures.
2820 props = []message.AuthProp{
2821 message.MakeAuthProp("header", "d", r.Sig.Domain.XName(c.msgsmtputf8), true, r.Sig.Domain.ASCIIExtra(c.msgsmtputf8)),
2822 message.MakeAuthProp("header", "s", r.Sig.Selector.XName(c.msgsmtputf8), true, r.Sig.Selector.ASCIIExtra(c.msgsmtputf8)),
2823 message.MakeAuthProp("header", "a", r.Sig.Algorithm(), false, ""),
2826 domain = r.Sig.Domain
2827 selector = r.Sig.Selector
2828 if r.Sig.Identity != nil {
2829 props = append(props, message.MakeAuthProp("header", "i", r.Sig.Identity.String(), true, ""))
2830 identity = r.Sig.Identity
2832 if r.RecordAuthentic {
2833 comment += "with dnssec"
2835 comment += "without dnssec"
2840 errmsg = r.Err.Error()
2842 authResAddDKIM(string(r.Status), comment, errmsg, props)
2843 c.log.Debugx("dkim verification result", r.Err,
2844 slog.Int("index", i),
2845 slog.Any("mailfrom", c.mailFrom),
2846 slog.Any("status", r.Status),
2847 slog.Any("domain", domain),
2848 slog.Any("selector", selector),
2849 slog.Any("identity", identity))
2853 var spfIdentity *dns.Domain
2854 var mailFromValidation = store.ValidationUnknown
2855 var ehloValidation = store.ValidationUnknown
2856 switch receivedSPF.Identity {
2857 case spf.ReceivedHELO:
2858 if len(spfArgs.HelloDomain.IP) == 0 {
2859 spfIdentity = &spfArgs.HelloDomain.Domain
2861 ehloValidation = store.SPFValidation(receivedSPF.Result)
2862 case spf.ReceivedMailFrom:
2863 spfIdentity = &spfArgs.MailFromDomain
2864 mailFromValidation = store.SPFValidation(receivedSPF.Result)
2866 var props []message.AuthProp
2867 if spfIdentity != nil {
2868 props = []message.AuthProp{message.MakeAuthProp("smtp", string(receivedSPF.Identity), spfIdentity.XName(c.msgsmtputf8), true, spfIdentity.ASCIIExtra(c.msgsmtputf8))}
2870 var spfComment string
2872 spfComment = "with dnssec"
2874 spfComment = "without dnssec"
2876 authResults.Methods = append(authResults.Methods, message.AuthMethod{
2878 Result: string(receivedSPF.Result),
2879 Comment: spfComment,
2882 switch receivedSPF.Result {
2883 case spf.StatusPass:
2884 c.log.Debug("spf pass", slog.Any("ip", spfArgs.RemoteIP), slog.String("mailfromdomain", spfArgs.MailFromDomain.ASCII)) // todo: log the domain that was actually verified.
2885 case spf.StatusFail:
2888 for _, b := range []byte(spfExpl) {
2889 if b < ' ' || b >= 0x7f {
2895 if len(spfExpl) > 800 {
2896 spfExpl = spfExpl[:797] + "..."
2898 spfExpl = "remote claims: " + spfExpl
2902 spfExpl = fmt.Sprintf("your ip %s is not on the SPF allowlist for domain %s", spfArgs.RemoteIP, spfDomain.ASCII)
2904 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?
2905 case spf.StatusTemperror:
2906 c.log.Infox("spf temperror", spfErr)
2907 case spf.StatusPermerror:
2908 c.log.Infox("spf permerror", spfErr)
2909 case spf.StatusNone, spf.StatusNeutral, spf.StatusSoftfail:
2911 c.log.Error("unknown spf status, treating as None/Neutral", slog.Any("status", receivedSPF.Result))
2912 receivedSPF.Result = spf.StatusNone
2917 var dmarcResult dmarc.Result
2918 const applyRandomPercentage = true
2919 // dmarcMethod is added to authResults when delivering to recipients: accounts can
2920 // have different policy override rules.
2921 var dmarcMethod message.AuthMethod
2922 var msgFromValidation = store.ValidationNone
2923 if msgFrom.IsZero() {
2924 dmarcResult.Status = dmarc.StatusNone
2925 dmarcMethod = message.AuthMethod{
2927 Result: string(dmarcResult.Status),
2930 msgFromValidation = alignment(ctx, c.log, msgFrom.Domain, dkimResults, receivedSPF.Result, spfIdentity)
2932 // We are doing the DMARC evaluation now. But we only store it for inclusion in an
2933 // aggregate report when we actually use it. We use an evaluation for each
2934 // recipient, with each a potentially different result due to mailing
2935 // list/forwarding configuration. If we reject a message due to being spam, we
2936 // don't want to spend any resources for the sender domain, and we don't want to
2937 // give the sender any more information about us, so we won't record the
2939 // 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.
2941 dmarcctx, dmarccancel := context.WithTimeout(ctx, time.Minute)
2943 dmarcUse, dmarcResult = dmarc.Verify(dmarcctx, c.log.Logger, c.resolver, msgFrom.Domain, dkimResults, receivedSPF.Result, spfIdentity, applyRandomPercentage)
2946 if dmarcResult.RecordAuthentic {
2947 comment = "with dnssec"
2949 comment = "without dnssec"
2951 dmarcMethod = message.AuthMethod{
2953 Result: string(dmarcResult.Status),
2955 Props: []message.AuthProp{
2957 message.MakeAuthProp("header", "from", msgFrom.Domain.ASCII, true, msgFrom.Domain.ASCIIExtra(c.msgsmtputf8)),
2961 if dmarcResult.Status == dmarc.StatusPass && msgFromValidation == store.ValidationRelaxed {
2962 msgFromValidation = store.ValidationDMARC
2965 // todo future: consider enforcing an spf (soft)fail if there is no dmarc policy or the dmarc policy is none.
../rfc/7489:1507
2967 c.log.Debug("dmarc verification", slog.Any("result", dmarcResult.Status), slog.Any("domain", msgFrom.Domain))
2969 // Prepare for analyzing content, calculating reputation.
2970 ipmasked1, ipmasked2, ipmasked3 := ipmasked(c.remoteIP)
2971 var verifiedDKIMDomains []string
2972 dkimSeen := map[string]bool{}
2973 for _, r := range dkimResults {
2974 // A message can have multiple signatures for the same identity. For example when
2975 // signing the message multiple times with different algorithms (rsa and ed25519).
2976 if r.Status != dkim.StatusPass {
2979 d := r.Sig.Domain.Name()
2982 verifiedDKIMDomains = append(verifiedDKIMDomains, d)
2986 // When we deliver, we try to remove from rejects mailbox based on message-id.
2987 // We'll parse it when we need it, but it is the same for each recipient.
2988 var messageID string
2989 var parsedMessageID bool
2991 // We build up a DSN for each failed recipient. If we have recipients in dsnMsg
2992 // after processing, we queue the DSN. Unless all recipients failed, in which case
2993 // we may just fail the mail transaction instead (could be common for failure to
2994 // deliver to a single recipient, e.g. for junk mail).
2996 type deliverError struct {
3003 var deliverErrors []deliverError
3004 addError := func(rcpt recipient, code int, secode string, userError bool, errmsg string) {
3005 e := deliverError{rcpt.Addr, code, secode, userError, errmsg}
3006 c.log.Info("deliver error",
3007 slog.Any("rcptto", e.rcptTo),
3008 slog.Int("code", code),
3009 slog.String("secode", "secode"),
3010 slog.Bool("usererror", userError),
3011 slog.String("errmsg", errmsg))
3012 deliverErrors = append(deliverErrors, e)
3015 // Sort recipients: local accounts, aliases, unknown. For ensuring we don't deliver
3016 // to an alias destination that was also explicitly sent to.
3017 rcptScore := func(r recipient) int {
3018 if r.Account != nil {
3020 } else if r.Alias != nil {
3025 sort.SliceStable(c.recipients, func(i, j int) bool {
3026 return rcptScore(c.recipients[i]) < rcptScore(c.recipients[j])
3029 // Return whether address is a regular explicit recipient in this transaction. Used
3030 // to prevent delivering a message to an address both for alias and explicit
3031 // addressee. Relies on c.recipients being sorted as above.
3032 regularRecipient := func(addr smtp.Path) bool {
3033 for _, rcpt := range c.recipients {
3034 if rcpt.Account == nil {
3036 } else if rcpt.Addr.Equal(addr) {
3043 // Prepare a message, analyze it against account's junk filter.
3044 // The returned analysis has an open account that must be closed by the caller.
3045 // We call this for all alias destinations, also when we already delivered to that
3046 // recipient: It may be the only recipient that would allow the message.
3047 messageAnalyze := func(log mlog.Log, smtpRcptTo, deliverTo smtp.Path, accountName string, destination config.Destination, canonicalAddr string) (a *analysis, rerr error) {
3048 acc, err := store.OpenAccount(log, accountName, false)
3050 log.Errorx("open account", err, slog.Any("account", accountName))
3051 metricDelivery.WithLabelValues("accounterror", "").Inc()
3057 log.Check(err, "closing account during analysis")
3062 Received: time.Now(),
3063 RemoteIP: c.remoteIP.String(),
3064 RemoteIPMasked1: ipmasked1,
3065 RemoteIPMasked2: ipmasked2,
3066 RemoteIPMasked3: ipmasked3,
3067 EHLODomain: c.hello.Domain.Name(),
3068 MailFrom: c.mailFrom.String(),
3069 MailFromLocalpart: c.mailFrom.Localpart,
3070 MailFromDomain: c.mailFrom.IPDomain.Domain.Name(),
3071 RcptToLocalpart: smtpRcptTo.Localpart,
3072 RcptToDomain: smtpRcptTo.IPDomain.Domain.Name(),
3073 MsgFromLocalpart: msgFrom.Localpart,
3074 MsgFromDomain: msgFrom.Domain.Name(),
3075 MsgFromOrgDomain: publicsuffix.Lookup(ctx, log.Logger, msgFrom.Domain).Name(),
3076 EHLOValidated: ehloValidation == store.ValidationPass,
3077 MailFromValidated: mailFromValidation == store.ValidationPass,
3078 MsgFromValidated: msgFromValidation == store.ValidationStrict || msgFromValidation == store.ValidationDMARC || msgFromValidation == store.ValidationRelaxed,
3079 EHLOValidation: ehloValidation,
3080 MailFromValidation: mailFromValidation,
3081 MsgFromValidation: msgFromValidation,
3082 DKIMDomains: verifiedDKIMDomains,
3084 Size: msgWriter.Size,
3087 tlsState := c.conn.(*tls.Conn).ConnectionState()
3088 m.ReceivedTLSVersion = tlsState.Version
3089 m.ReceivedTLSCipherSuite = tlsState.CipherSuite
3090 if c.requireTLS != nil {
3091 m.ReceivedRequireTLS = *c.requireTLS
3094 m.ReceivedTLSVersion = 1 // Signals plain text delivery.
3097 var msgTo, msgCc []message.Address
3098 if envelope != nil {
3102 d := delivery{c.tls, &m, dataFile, smtpRcptTo, deliverTo, destination, canonicalAddr, acc, msgTo, msgCc, msgFrom, c.dnsBLs, dmarcUse, dmarcResult, dkimResults, iprevStatus, c.smtputf8}
3104 r := analyze(ctx, log, c.resolver, d)
3108 // Either deliver the message, or call addError to register the recipient as failed.
3109 // If recipient is an alias, we may be delivering to multiple address/accounts and
3110 // we will consider a message delivered if we delivered it to at least one account
3111 // (others may be over quota).
3112 processRecipient := func(rcpt recipient) {
3113 log := c.log.With(slog.Any("mailfrom", c.mailFrom), slog.Any("rcptto", rcpt.Addr))
3115 // If this is not a valid local user, we send back a DSN. This can only happen when
3116 // there are also valid recipients, and only when remote is SPF-verified, so the DSN
3117 // should not cause backscatter.
3118 // In case of serious errors, we abort the transaction. We may have already
3119 // delivered some messages. Perhaps it would be better to continue with other
3120 // deliveries, and return an error at the end? Though the failure conditions will
3121 // probably prevent any other successful deliveries too...
3123 if rcpt.Account == nil && rcpt.Alias == nil {
3124 metricDelivery.WithLabelValues("unknownuser", "").Inc()
3125 addError(rcpt, smtp.C550MailboxUnavail, smtp.SeAddr1UnknownDestMailbox1, true, "no such user")
3129 // la holds all analysis, and message preparation, for all accounts (multiple for
3130 // aliases). Each has an open account that we we close on return.
3133 for _, a := range la {
3134 err := a.d.acc.Close()
3135 log.Check(err, "close account")
3139 // For aliases, we prepare & analyze for each recipient. We accept the message if
3140 // any recipient accepts it. Regular destination have just a single account to
3141 // check. We check all alias destinations, even if we already explicitly delivered
3142 // to them: they may be the only destination that would accept the message.
3143 var a0 *analysis // Analysis we've used for accept/reject decision.
3144 if rcpt.Alias != nil {
3145 // Check if msgFrom address is acceptable. This doesn't take validation into
3146 // consideration. If the header was forged, the message may be rejected later on.
3147 if !aliasAllowedMsgFrom(rcpt.Alias.Alias, msgFrom) {
3148 addError(rcpt, smtp.C550MailboxUnavail, smtp.SePol7ExpnProhibited2, true, "not allowed to send to destination")
3152 la = make([]analysis, 0, len(rcpt.Alias.Alias.ParsedAddresses))
3153 for _, aa := range rcpt.Alias.Alias.ParsedAddresses {
3154 a, err := messageAnalyze(log, rcpt.Addr, aa.Address.Path(), aa.AccountName, aa.Destination, rcpt.Alias.CanonicalAddress)
3156 addError(rcpt, smtp.C451LocalErr, smtp.SeSys3Other0, false, "error processing")
3160 if a.accept && a0 == nil {
3161 // Address that caused us to accept.
3166 // First address, for rejecting.
3170 a, err := messageAnalyze(log, rcpt.Addr, rcpt.Addr, rcpt.Account.AccountName, rcpt.Account.Destination, rcpt.Account.CanonicalAddress)
3172 addError(rcpt, smtp.C451LocalErr, smtp.SeSys3Other0, false, "error processing")
3179 if !a0.accept && a0.reason == reasonHighRate {
3180 log.Info("incoming message rejected for high rate, not storing in rejects mailbox", slog.String("reason", a0.reason), slog.Any("msgfrom", msgFrom))
3181 metricDelivery.WithLabelValues("reject", a0.reason).Inc()
3183 addError(rcpt, a0.code, a0.secode, a0.userError, a0.errmsg)
3187 // Any DMARC result override is stored in the evaluation for outgoing DMARC
3188 // aggregate reports, and added to the Authentication-Results message header.
3189 // We want to tell the sender that we have an override, e.g. for mailing lists, so
3190 // they don't overestimate the potential damage of switching from p=none to
3192 var dmarcOverrides []string
3193 if a0.dmarcOverrideReason != "" {
3194 dmarcOverrides = []string{a0.dmarcOverrideReason}
3196 if dmarcResult.Record != nil && !dmarcUse {
3197 dmarcOverrides = append(dmarcOverrides, string(dmarcrpt.PolicyOverrideSampledOut))
3200 // Add per-recipient DMARC method to Authentication-Results. Each account can have
3201 // their own override rules, e.g. based on configured mailing lists/forwards.
3203 rcptDMARCMethod := dmarcMethod
3204 if len(dmarcOverrides) > 0 {
3205 if rcptDMARCMethod.Comment != "" {
3206 rcptDMARCMethod.Comment += ", "
3208 rcptDMARCMethod.Comment += "override " + strings.Join(dmarcOverrides, ",")
3210 rcptAuthResults := authResults
3211 rcptAuthResults.Methods = append([]message.AuthMethod{}, authResults.Methods...)
3212 rcptAuthResults.Methods = append(rcptAuthResults.Methods, rcptDMARCMethod)
3214 // Prepend reason as message header, for easy viewing in mail clients.
3216 if a0.reason != "" {
3217 hw := &message.HeaderWriter{}
3218 hw.Add(" ", "X-Mox-Reason:")
3219 hw.Add(" ", a0.reason)
3220 for i, s := range a0.reasonText {
3226 // Just in case any of the strings has a newline, replace it with space to not break the message.
3227 s = strings.ReplaceAll(s, "\n", " ")
3228 s = strings.ReplaceAll(s, "\r", " ")
3230 hw.AddWrap([]byte(s), true)
3239 la[i].d.m.MsgPrefix = []byte(
3243 rcptAuthResults.Header() +
3244 receivedSPF.Header() +
3245 recvHdrFor(rcpt.Addr.String()),
3247 la[i].d.m.Size += int64(len(la[i].d.m.MsgPrefix))
3250 // Store DMARC evaluation for inclusion in an aggregate report. Only if there is at
3251 // least one reporting address: We don't want to needlessly store a row in a
3252 // database for each delivery attempt. If we reject a message for being junk, we
3253 // are also not going to send it a DMARC report. The DMARC check is done early in
3254 // the analysis, we will report on rejects because of DMARC, because it could be
3255 // valuable feedback about forwarded or mailing list messages.
3257 if !mox.Conf.Static.NoOutgoingDMARCReports && dmarcResult.Record != nil && len(dmarcResult.Record.AggregateReportAddresses) > 0 && (a0.accept && !a0.d.m.IsReject || a0.reason == reasonDMARCPolicy) {
3258 // Disposition holds our decision on whether to accept the message. Not what the
3259 // DMARC evaluation resulted in. We can override, e.g. because of mailing lists,
3260 // forwarding, or local policy.
3261 // We treat quarantine as reject, so never claim to quarantine.
3263 disposition := dmarcrpt.DispositionNone
3265 disposition = dmarcrpt.DispositionReject
3268 // unknownDomain returns whether the sender is domain with which this account has
3269 // not had positive interaction.
3270 unknownDomain := func() (unknown bool) {
3271 err := a0.d.acc.DB.Read(ctx, func(tx *bstore.Tx) (err error) {
3272 // See if we received a non-junk message from this organizational domain.
3273 q := bstore.QueryTx[store.Message](tx)
3274 q.FilterNonzero(store.Message{MsgFromOrgDomain: a0.d.m.MsgFromOrgDomain})
3275 q.FilterEqual("Notjunk", true)
3276 q.FilterEqual("IsReject", false)
3277 exists, err := q.Exists()
3279 return fmt.Errorf("querying for non-junk message from organizational domain: %v", err)
3285 // See if we sent a message to this organizational domain.
3286 qr := bstore.QueryTx[store.Recipient](tx)
3287 qr.FilterNonzero(store.Recipient{OrgDomain: a0.d.m.MsgFromOrgDomain})
3288 exists, err = qr.Exists()
3290 return fmt.Errorf("querying for message sent to organizational domain: %v", err)
3298 log.Errorx("checking if sender is unknown domain, for dmarc aggregate report evaluation", err)
3303 r := dmarcResult.Record
3304 addresses := make([]string, len(r.AggregateReportAddresses))
3305 for i, a := range r.AggregateReportAddresses {
3306 addresses[i] = a.String()
3308 sp := dmarcrpt.Disposition(r.SubdomainPolicy)
3309 if r.SubdomainPolicy == dmarc.PolicyEmpty {
3310 sp = dmarcrpt.Disposition(r.Policy)
3312 eval := dmarcdb.Evaluation{
3313 // Evaluated and IntervalHours set by AddEvaluation.
3314 PolicyDomain: dmarcResult.Domain.Name(),
3316 // Optional evaluations don't cause a report to be sent, but will be included.
3317 // Useful for automated inter-mailer messages, we don't want to get in a reporting
3318 // loop. We also don't want to be used for sending reports to unsuspecting domains
3319 // we have no relation with.
3320 // 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.
3321 Optional: a0.d.destination.DMARCReports || a0.d.destination.HostTLSReports || a0.d.destination.DomainTLSReports || a0.reason == reasonDMARCPolicy && unknownDomain(),
3323 Addresses: addresses,
3325 PolicyPublished: dmarcrpt.PolicyPublished{
3326 Domain: dmarcResult.Domain.Name(),
3327 ADKIM: dmarcrpt.Alignment(r.ADKIM),
3328 ASPF: dmarcrpt.Alignment(r.ASPF),
3329 Policy: dmarcrpt.Disposition(r.Policy),
3330 SubdomainPolicy: sp,
3331 Percentage: r.Percentage,
3332 // We don't save ReportingOptions, we don't do per-message failure reporting.
3334 SourceIP: c.remoteIP.String(),
3335 Disposition: disposition,
3336 AlignedDKIMPass: dmarcResult.AlignedDKIMPass,
3337 AlignedSPFPass: dmarcResult.AlignedSPFPass,
3338 EnvelopeTo: rcpt.Addr.IPDomain.String(),
3339 EnvelopeFrom: c.mailFrom.IPDomain.String(),
3340 HeaderFrom: msgFrom.Domain.Name(),
3343 for _, s := range dmarcOverrides {
3344 reason := dmarcrpt.PolicyOverrideReason{Type: dmarcrpt.PolicyOverride(s)}
3345 eval.OverrideReasons = append(eval.OverrideReasons, reason)
3348 // We'll include all signatures for the organizational domain, even if they weren't
3349 // relevant due to strict alignment requirement.
3350 for _, dkimResult := range dkimResults {
3351 if dkimResult.Sig == nil || publicsuffix.Lookup(ctx, log.Logger, msgFrom.Domain) != publicsuffix.Lookup(ctx, log.Logger, dkimResult.Sig.Domain) {
3354 r := dmarcrpt.DKIMAuthResult{
3355 Domain: dkimResult.Sig.Domain.Name(),
3356 Selector: dkimResult.Sig.Selector.ASCII,
3357 Result: dmarcrpt.DKIMResult(dkimResult.Status),
3359 eval.DKIMResults = append(eval.DKIMResults, r)
3362 switch receivedSPF.Identity {
3363 case spf.ReceivedHELO:
3364 spfAuthResult := dmarcrpt.SPFAuthResult{
3365 Domain: spfArgs.HelloDomain.String(), // Can be unicode and also IP.
3366 Scope: dmarcrpt.SPFDomainScopeHelo,
3367 Result: dmarcrpt.SPFResult(receivedSPF.Result),
3369 eval.SPFResults = []dmarcrpt.SPFAuthResult{spfAuthResult}
3370 case spf.ReceivedMailFrom:
3371 spfAuthResult := dmarcrpt.SPFAuthResult{
3372 Domain: spfArgs.MailFromDomain.Name(), // Can be unicode.
3373 Scope: dmarcrpt.SPFDomainScopeMailFrom,
3374 Result: dmarcrpt.SPFResult(receivedSPF.Result),
3376 eval.SPFResults = []dmarcrpt.SPFAuthResult{spfAuthResult}
3379 err := dmarcdb.AddEvaluation(ctx, dmarcResult.Record.AggregateReportingInterval, &eval)
3380 log.Check(err, "adding dmarc evaluation to database for aggregate report")
3384 for _, a := range la {
3385 // Don't add message if address was also explicitly present in a RCPT TO command.
3386 if rcpt.Alias != nil && regularRecipient(a.d.deliverTo) {
3390 conf, _ := a.d.acc.Conf()
3391 if conf.RejectsMailbox == "" {
3394 present, _, messagehash, err := rejectPresent(log, a.d.acc, conf.RejectsMailbox, a.d.m, dataFile)
3396 log.Errorx("checking whether reject is already present", err)
3399 log.Info("reject message is already present, ignoring")
3402 a.d.m.IsReject = true
3403 a.d.m.Seen = true // We don't want to draw attention.
3404 // Regular automatic junk flags configuration applies to these messages. The
3405 // default is to treat these as neutral, so they won't cause outright rejections
3406 // due to reputation for later delivery attempts.
3407 a.d.m.MessageHash = messagehash
3408 a.d.acc.WithWLock(func() {
3411 if !conf.KeepRejects {
3412 hasSpace, err = a.d.acc.TidyRejectsMailbox(c.log, conf.RejectsMailbox)
3415 log.Errorx("tidying rejects mailbox", err)
3416 } else if hasSpace {
3417 if err := a.d.acc.DeliverMailbox(log, conf.RejectsMailbox, a.d.m, dataFile); err != nil {
3418 log.Errorx("delivering spammy mail to rejects mailbox", err)
3420 log.Info("delivered spammy mail to rejects mailbox")
3423 log.Info("not storing spammy mail to full rejects mailbox")
3428 log.Info("incoming message rejected", slog.String("reason", a0.reason), slog.Any("msgfrom", msgFrom))
3429 metricDelivery.WithLabelValues("reject", a0.reason).Inc()
3431 addError(rcpt, a0.code, a0.secode, a0.userError, a0.errmsg)
3435 delayFirstTime := true
3436 if rcpt.Account != nil && a0.dmarcReport != nil {
3438 if err := dmarcdb.AddReport(ctx, a0.dmarcReport, msgFrom.Domain); err != nil {
3439 log.Errorx("saving dmarc aggregate report in database", err)
3441 log.Info("dmarc aggregate report processed")
3442 a0.d.m.Flags.Seen = true
3443 delayFirstTime = false
3446 if rcpt.Account != nil && a0.tlsReport != nil {
3447 // todo future: add rate limiting to prevent DoS attacks.
3448 if err := tlsrptdb.AddReport(ctx, c.log, msgFrom.Domain, c.mailFrom.String(), a0.d.destination.HostTLSReports, a0.tlsReport); err != nil {
3449 log.Errorx("saving TLSRPT report in database", err)
3451 log.Info("tlsrpt report processed")
3452 a0.d.m.Flags.Seen = true
3453 delayFirstTime = false
3457 // If this is a first-time sender and not a forwarded/mailing list message, wait
3458 // before actually delivering. If this turns out to be a spammer, we've kept one of
3459 // their connections busy.
3460 a0conf, _ := a0.d.acc.Conf()
3461 if delayFirstTime && !a0.d.m.IsForward && !a0.d.m.IsMailingList && a0.reason == reasonNoBadSignals && !a0conf.NoFirstTimeSenderDelay && c.firstTimeSenderDelay > 0 {
3462 log.Debug("delaying before delivering from sender without reputation", slog.Duration("delay", c.firstTimeSenderDelay))
3463 mox.Sleep(mox.Context, c.firstTimeSenderDelay)
3467 code, timeout := mox.LocalserveNeedsError(rcpt.Addr.Localpart)
3469 log.Info("timing out due to special localpart")
3470 mox.Sleep(mox.Context, time.Hour)
3471 xsmtpServerErrorf(codes{smtp.C451LocalErr, smtp.SeOther00}, "timing out delivery due to special localpart")
3472 } else if code != 0 {
3473 log.Info("failure due to special localpart", slog.Int("code", code))
3474 metricDelivery.WithLabelValues("delivererror", "localserve").Inc()
3475 addError(rcpt, code, smtp.SeOther00, false, fmt.Sprintf("failure with code %d due to special localpart", code))
3480 // Gather the message-id before we deliver and the file may be consumed.
3481 if !parsedMessageID {
3482 if p, err := message.Parse(c.log.Logger, false, store.FileMsgReader(a0.d.m.MsgPrefix, dataFile)); err != nil {
3483 log.Infox("parsing message for message-id", err)
3484 } else if header, err := p.Header(); err != nil {
3485 log.Infox("parsing message header for message-id", err)
3487 messageID = header.Get("Message-Id")
3489 parsedMessageID = true
3492 // Finally deliver the message to the account(s).
3493 var nerr int // Number of non-quota errors.
3494 var nfull int // Number of failed deliveries due to over quota.
3495 var ndelivered int // Number delivered to account.
3496 for _, a := range la {
3497 // Don't deliver to recipient that was explicitly present in SMTP transaction, or
3498 // is sending the message to an alias they are member of.
3499 if rcpt.Alias != nil && (regularRecipient(a.d.deliverTo) || a.d.deliverTo.Equal(msgFrom.Path())) {
3504 a.d.acc.WithWLock(func() {
3505 if err := a.d.acc.DeliverMailbox(log, a.mailbox, a.d.m, dataFile); err != nil {
3506 log.Errorx("delivering", err)
3507 metricDelivery.WithLabelValues("delivererror", a0.reason).Inc()
3508 if errors.Is(err, store.ErrOverQuota) {
3511 addError(rcpt, smtp.C451LocalErr, smtp.SeSys3Other0, false, "error processing")
3518 metricDelivery.WithLabelValues("delivered", a0.reason).Inc()
3519 log.Info("incoming message delivered", slog.String("reason", a0.reason), slog.Any("msgfrom", msgFrom))
3521 conf, _ := a.d.acc.Conf()
3522 if conf.RejectsMailbox != "" && a.d.m.MessageID != "" {
3523 if err := a.d.acc.RejectsRemove(log, conf.RejectsMailbox, a.d.m.MessageID); err != nil {
3524 log.Errorx("removing message from rejects mailbox", err, slog.String("messageid", messageID))
3529 // Pass delivered messages to queue for DSN processing and/or hooks.
3531 mr := store.FileMsgReader(a.d.m.MsgPrefix, dataFile)
3532 part, err := a.d.m.LoadPart(mr)
3534 log.Errorx("loading parsed part for evaluating webhook", err)
3536 err = queue.Incoming(context.Background(), log, a.d.acc, messageID, *a.d.m, part, a.mailbox)
3537 log.Check(err, "queueing webhook for incoming delivery")
3539 } else if nerr > 0 && ndelivered == 0 {
3540 // Don't continue if we had an error and haven't delivered yet. If we only had
3541 // quota-related errors, we keep trying for an account to deliver to.
3545 if ndelivered == 0 && (nerr > 0 || nfull > 0) {
3547 addError(rcpt, smtp.C452StorageFull, smtp.SeMailbox2Full2, true, "account storage full")
3549 addError(rcpt, smtp.C451LocalErr, smtp.SeSys3Other0, false, "error processing")
3554 // For each recipient, do final spam analysis and delivery.
3555 for _, rcpt := range c.recipients {
3556 processRecipient(rcpt)
3559 // If all recipients failed to deliver, return an error.
3560 if len(c.recipients) == len(deliverErrors) {
3562 e0 := deliverErrors[0]
3563 var serverError bool
3566 for _, e := range deliverErrors {
3567 serverError = serverError || !e.userError
3568 if e.code != e0.code || e.secode != e0.secode {
3571 msgs = append(msgs, e.errmsg)
3577 xsmtpErrorf(e0.code, e0.secode, !serverError, "%s", strings.Join(msgs, "\n"))
3580 // Not all failures had the same error. We'll return each error on a separate line.
3582 for _, e := range deliverErrors {
3583 s := fmt.Sprintf("%d %d.%s %s", e.code, e.code/100, e.secode, e.errmsg)
3584 lines = append(lines, s)
3586 code := smtp.C451LocalErr
3587 secode := smtp.SeSys3Other0
3589 code = smtp.C554TransactionFailed
3591 lines = append(lines, "multiple errors")
3592 xsmtpErrorf(code, secode, !serverError, "%s", strings.Join(lines, "\n"))
3594 // Generate one DSN for all failed recipients.
3595 if len(deliverErrors) > 0 {
3597 dsnMsg := dsn.Message{
3598 SMTPUTF8: c.msgsmtputf8,
3599 From: smtp.Path{Localpart: "postmaster", IPDomain: deliverErrors[0].rcptTo.IPDomain},
3601 Subject: "mail delivery failure",
3602 MessageID: mox.MessageIDGen(false),
3603 References: messageID,
3605 // Per-message details.
3606 ReportingMTA: mox.Conf.Static.HostnameDomain.ASCII,
3607 ReceivedFromMTA: smtp.Ehlo{Name: c.hello, ConnIP: c.remoteIP},
3611 if len(deliverErrors) > 1 {
3612 dsnMsg.TextBody = "Multiple delivery failures occurred.\n\n"
3615 for _, e := range deliverErrors {
3617 if e.code/100 == 4 {
3620 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))
3621 rcpt := dsn.Recipient{
3622 FinalRecipient: e.rcptTo,
3624 Status: fmt.Sprintf("%d.%s", e.code/100, e.secode),
3625 LastAttemptDate: now,
3627 dsnMsg.Recipients = append(dsnMsg.Recipients, rcpt)
3630 header, err := message.ReadHeaders(bufio.NewReader(&moxio.AtReader{R: dataFile}))
3632 c.log.Errorx("reading headers of incoming message for dsn, continuing dsn without headers", err)
3634 dsnMsg.Original = header
3637 c.log.Error("not queueing dsn for incoming delivery due to localserve")
3638 } else if err := queueDSN(context.TODO(), c.log, c, *c.mailFrom, dsnMsg, c.requireTLS != nil && *c.requireTLS); err != nil {
3639 metricServerErrors.WithLabelValues("queuedsn").Inc()
3640 c.log.Errorx("queuing DSN for incoming delivery, no DSN sent", err)
3645 c.transactionBad-- // Compensate for early earlier pessimistic increase.
3647 c.writecodeline(smtp.C250Completed, smtp.SeMailbox2Other0, "it is done", nil)
3650// Return whether msgFrom address is allowed to send a message to alias.
3651func aliasAllowedMsgFrom(alias config.Alias, msgFrom smtp.Address) bool {
3652 for _, aa := range alias.ParsedAddresses {
3653 if aa.Address == msgFrom {
3657 lp, err := smtp.ParseLocalpart(alias.LocalpartStr)
3658 xcheckf(err, "parsing alias localpart")
3659 if msgFrom == smtp.NewAddress(lp, alias.Domain) {
3660 return alias.AllowMsgFrom
3662 return alias.PostPublic
3665// ecode returns either ecode, or a more specific error based on err.
3666// For example, ecode can be turned from an "other system" error into a "mail
3667// system full" if the error indicates no disk space is available.
3668func errCodes(code int, ecode string, err error) codes {
3670 case moxio.IsStorageSpace(err):
3672 case smtp.SeMailbox2Other0:
3673 if code == smtp.C451LocalErr {
3674 code = smtp.C452StorageFull
3676 ecode = smtp.SeMailbox2Full2
3677 case smtp.SeSys3Other0:
3678 if code == smtp.C451LocalErr {
3679 code = smtp.C452StorageFull
3681 ecode = smtp.SeSys3StorageFull1
3684 return codes{code, ecode}
3688func (c *conn) cmdRset(p *parser) {
3693 c.bwritecodeline(smtp.C250Completed, smtp.SeOther00, "all clear", nil)
3697func (c *conn) cmdVrfy(p *parser) {
3698 // No EHLO/HELO needed.
3709 // todo future: we could support vrfy and expn for submission? though would need to see if its rfc defines it.
3712 xsmtpUserErrorf(smtp.C252WithoutVrfy, smtp.SePol7Other0, "no verify but will try delivery")
3716func (c *conn) cmdExpn(p *parser) {
3717 // No EHLO/HELO needed.
3728 // todo: we could implement expn for local aliases for authenticated users, when members have permission to list. would anyone use it?
3731 xsmtpUserErrorf(smtp.C252WithoutVrfy, smtp.SePol7Other0, "no expand but will try delivery")
3735func (c *conn) cmdHelp(p *parser) {
3736 // Let's not strictly parse the request for help. We are ignoring the text anyway.
3739 c.bwritecodeline(smtp.C214Help, smtp.SeOther00, "see rfc 5321 (smtp)", nil)
3743func (c *conn) cmdNoop(p *parser) {
3744 // No idea why, but if an argument follows, it must adhere to the string ABNF production...
3751 c.bwritecodeline(smtp.C250Completed, smtp.SeOther00, "alrighty", nil)
3755func (c *conn) cmdQuit(p *parser) {
3759 c.writecodeline(smtp.C221Closing, smtp.SeOther00, "okay thanks bye", nil)