1// Package smtpserver implements an SMTP server for submission and incoming delivery of mail messages.
10 cryptorand "crypto/rand"
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/publicsuffix"
56 "github.com/mjl-/mox/queue"
57 "github.com/mjl-/mox/ratelimit"
58 "github.com/mjl-/mox/scram"
59 "github.com/mjl-/mox/smtp"
60 "github.com/mjl-/mox/spf"
61 "github.com/mjl-/mox/store"
62 "github.com/mjl-/mox/tlsrpt"
63 "github.com/mjl-/mox/tlsrptdb"
66// We use panic and recover for error handling while executing commands.
67// These errors signal the connection must be closed.
68var errIO = errors.New("io error")
70// If set, regular delivery/submit is sidestepped, email is accepted and
71// delivered to the account named mox.
74var limiterConnectionRate, limiterConnections *ratelimit.Limiter
76// For delivery rate limiting. Variable because changed during tests.
77var limitIPMasked1MessagesPerMinute int = 500
78var limitIPMasked1SizePerMinute int64 = 1000 * 1024 * 1024
80// Maximum number of RCPT TO commands (i.e. recipients) for a single message
81// delivery. Must be at least 100. Announced in LIMIT extension.
82const rcptToLimit = 1000
85 // Also called by tests, so they don't trigger the rate limiter.
91 // todo future: make these configurable
92 limiterConnectionRate = &ratelimit.Limiter{
93 WindowLimits: []ratelimit.WindowLimit{
96 Limits: [...]int64{300, 900, 2700},
100 limiterConnections = &ratelimit.Limiter{
101 WindowLimits: []ratelimit.WindowLimit{
103 Window: time.Duration(math.MaxInt64), // All of time.
104 Limits: [...]int64{30, 90, 270},
111 // Delays for bad/suspicious behaviour. Zero during tests.
112 badClientDelay = time.Second // Before reads and after 1-byte writes for probably spammers.
113 authFailDelay = time.Second // Response to authentication failure.
114 unknownRecipientsDelay = 5 * time.Second // Response when all recipients are unknown.
115 firstTimeSenderDelayDefault = 15 * time.Second // Before accepting message from first-time sender.
120 secode string // Enhanced code, but without the leading major int from code.
124 metricConnection = promauto.NewCounterVec(
125 prometheus.CounterOpts{
126 Name: "mox_smtpserver_connection_total",
127 Help: "Incoming SMTP connections.",
130 "kind", // "deliver" or "submit"
133 metricCommands = promauto.NewHistogramVec(
134 prometheus.HistogramOpts{
135 Name: "mox_smtpserver_command_duration_seconds",
136 Help: "SMTP server command duration and result codes in seconds.",
137 Buckets: []float64{0.001, 0.005, 0.01, 0.05, 0.100, 0.5, 1, 5, 10, 20, 30, 60, 120},
140 "kind", // "deliver" or "submit"
146 metricDelivery = promauto.NewCounterVec(
147 prometheus.CounterOpts{
148 Name: "mox_smtpserver_delivery_total",
149 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.",
156 // Similar between ../webmail/webmail.go:/metricSubmission and ../smtpserver/server.go:/metricSubmission and ../webapisrv/server.go:/metricSubmission
157 metricSubmission = promauto.NewCounterVec(
158 prometheus.CounterOpts{
159 Name: "mox_smtpserver_submission_total",
160 Help: "SMTP server incoming submission results, known values (those ending with error are server errors): ok, badmessage, badfrom, badheader, messagelimiterror, recipientlimiterror, localserveerror, queueerror.",
166 metricServerErrors = promauto.NewCounterVec(
167 prometheus.CounterOpts{
168 Name: "mox_smtpserver_errors_total",
169 Help: "SMTP server errors, known values: dkimsign, queuedsn.",
175 metricDeliveryStarttls = promauto.NewCounter(
176 prometheus.CounterOpts{
177 Name: "mox_smtpserver_delivery_starttls_total",
178 Help: "Total number of STARTTLS handshakes for incoming deliveries.",
181 metricDeliveryStarttlsErrors = promauto.NewCounterVec(
182 prometheus.CounterOpts{
183 Name: "mox_smtpserver_delivery_starttls_errors_total",
184 Help: "Errors with TLS handshake during STARTTLS for incoming deliveries.",
187 "reason", // "eof", "sslv2", "unsupportedversions", "nottls", "alert-<num>-<msg>", "other"
192var jitterRand = mox.NewPseudoRand()
194func durationDefault(delay *time.Duration, def time.Duration) time.Duration {
201// Listen initializes network listeners for incoming SMTP connection.
202// The listeners are stored for a later call to Serve.
204 names := slices.Sorted(maps.Keys(mox.Conf.Static.Listeners))
205 for _, name := range names {
206 listener := mox.Conf.Static.Listeners[name]
208 var tlsConfig, tlsConfigDelivery *tls.Config
209 if listener.TLS != nil {
210 tlsConfig = listener.TLS.Config
211 // For SMTP delivery, if we get a TLS handshake for an SNI hostname that we don't
212 // allow, we'll fallback to a certificate for the listener hostname instead of
213 // causing the connection to fail. May improve interoperability.
214 tlsConfigDelivery = listener.TLS.ConfigFallback
217 maxMsgSize := listener.SMTPMaxMessageSize
219 maxMsgSize = config.DefaultMaxMsgSize
222 if listener.SMTP.Enabled {
223 hostname := mox.Conf.Static.HostnameDomain
224 if listener.Hostname != "" {
225 hostname = listener.HostnameDomain
227 port := config.Port(listener.SMTP.Port, 25)
228 for _, ip := range listener.IPs {
229 firstTimeSenderDelay := durationDefault(listener.SMTP.FirstTimeSenderDelay, firstTimeSenderDelayDefault)
230 if tlsConfigDelivery != nil {
231 tlsConfigDelivery = tlsConfigDelivery.Clone()
232 // Default setting is currently to have session tickets disabled, to work around
233 // TLS interoperability issues with incoming deliveries from Microsoft. See
234 // https://github.com/golang/go/issues/70232.
235 tlsConfigDelivery.SessionTicketsDisabled = listener.SMTP.TLSSessionTicketsDisabled == nil || *listener.SMTP.TLSSessionTicketsDisabled
237 listen1("smtp", name, ip, port, hostname, tlsConfigDelivery, false, false, maxMsgSize, false, listener.SMTP.RequireSTARTTLS, !listener.SMTP.NoRequireTLS, listener.SMTP.DNSBLZones, firstTimeSenderDelay)
240 if listener.Submission.Enabled {
241 hostname := mox.Conf.Static.HostnameDomain
242 if listener.Hostname != "" {
243 hostname = listener.HostnameDomain
245 port := config.Port(listener.Submission.Port, 587)
246 for _, ip := range listener.IPs {
247 listen1("submission", name, ip, port, hostname, tlsConfig, true, false, maxMsgSize, !listener.Submission.NoRequireSTARTTLS, !listener.Submission.NoRequireSTARTTLS, true, nil, 0)
251 if listener.Submissions.Enabled {
252 hostname := mox.Conf.Static.HostnameDomain
253 if listener.Hostname != "" {
254 hostname = listener.HostnameDomain
256 port := config.Port(listener.Submissions.Port, 465)
257 for _, ip := range listener.IPs {
258 listen1("submissions", name, ip, port, hostname, tlsConfig, true, true, maxMsgSize, true, true, true, nil, 0)
266func 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) {
267 log := mlog.New("smtpserver", nil)
268 addr := net.JoinHostPort(ip, fmt.Sprintf("%d", port))
269 if os.Getuid() == 0 {
270 log.Print("listening for smtp",
271 slog.String("listener", name),
272 slog.String("address", addr),
273 slog.String("protocol", protocol))
275 network := mox.Network(ip)
276 ln, err := mox.Listen(network, addr)
278 log.Fatalx("smtp: listen for smtp", err, slog.String("protocol", protocol), slog.String("listener", name))
281 // Each listener gets its own copy of the config, so session keys between different
282 // ports on same listener aren't shared. We rotate session keys explicitly in this
283 // base TLS config because each connection clones the TLS config before using. The
284 // base TLS config would never get automatically managed/rotated session keys.
285 if tlsConfig != nil {
286 tlsConfig = tlsConfig.Clone()
287 mox.StartTLSSessionTicketKeyRefresher(mox.Shutdown, log, tlsConfig)
292 conn, err := ln.Accept()
294 log.Infox("smtp: accept", err, slog.String("protocol", protocol), slog.String("listener", name))
298 // Package is set on the resolver by the dkim/spf/dmarc/etc packages.
299 resolver := dns.StrictResolver{Log: log.Logger}
300 go serve(name, mox.Cid(), hostname, tlsConfig, conn, resolver, submission, xtls, false, maxMessageSize, requireTLSForAuth, requireTLSForDelivery, requireTLS, dnsBLs, firstTimeSenderDelay)
304 servers = append(servers, serve)
307// Serve starts serving on all listeners, launching a goroutine per listener.
309 for _, serve := range servers {
317 // OrigConn is the original (TCP) connection. We'll read from/write to conn, which
318 // can be wrapped in a tls.Server. We close origConn instead of conn because
319 // closing the TLS connection would send a TLS close notification, which may block
320 // for 5s if the server isn't reading it (because it is also sending it).
325 extRequireTLS bool // Whether to announce and allow the REQUIRETLS extension.
326 viaHTTPS bool // Whether the connection came in via the HTTPS port (using TLS ALPN).
327 resolver dns.Resolver
328 // The "x" in the readers and writes indicate Read and Write errors use panic to
329 // propagate the error.
332 xtr *moxio.TraceReader // Kept for changing trace level during cmd/auth/data.
333 xtw *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) || mlog.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 version, ciphersuite := moxio.TLSInfo(cs)
632 attrs := []slog.Attr{
633 slog.String("version", version),
634 slog.String("ciphersuite", ciphersuite),
635 slog.String("sni", cs.ServerName),
636 slog.Bool("resumed", cs.DidResume),
637 slog.Int("clientcerts", len(cs.PeerCertificates)),
639 if c.account != nil {
640 attrs = append(attrs,
641 slog.String("account", c.account.Name),
642 slog.String("username", c.username),
645 c.log.Debug("tls handshake completed", attrs...)
648// completely reset connection state as if greeting has just been sent.
650func (c *conn) reset() {
652 c.hello = dns.IPDomain{}
655 if c.account != nil {
656 err := c.account.Close()
657 c.log.Check(err, "closing account")
665// for rset command, and a few more cases that reset the mail transaction state.
667func (c *conn) rset() {
670 c.futureRelease = time.Time{}
671 c.futureReleaseRequest = ""
672 c.has8bitmime = false
674 c.msgsmtputf8 = false
678func (c *conn) earliestDeadline(d time.Duration) time.Time {
679 e := time.Now().Add(d)
680 if !c.deadline.IsZero() && c.deadline.Before(e) {
686func (c *conn) xcheckAuth() {
687 if c.submission && c.account == nil {
689 xsmtpUserErrorf(smtp.C530SecurityRequired, smtp.SePol7Other0, "authentication required")
693func (c *conn) xtrace(level slog.Level) func() {
695 c.xtr.SetTrace(level)
696 c.xtw.SetTrace(level)
699 c.xtr.SetTrace(mlog.LevelTrace)
700 c.xtw.SetTrace(mlog.LevelTrace)
704// setSlow marks the connection slow (or now), so reads are done with 3 second
705// delay for each read, and writes are done at 1 byte per second, to try to slow
707func (c *conn) setSlow(on bool) {
709 c.log.Debug("connection changed to slow")
710 } else if !on && c.slow {
711 c.log.Debug("connection restored to regular pace")
716// Write writes to the connection. It panics on i/o errors, which is handled by the
717// connection command loop.
718func (c *conn) Write(buf []byte) (int, error) {
724 // We set a single deadline for Write and Read. This may be a TLS connection.
725 // SetDeadline works on the underlying connection. If we wouldn't touch the read
726 // deadline, and only set the write deadline and do a bunch of writes, the TLS
727 // library would still have to do reads on the underlying connection, and may reach
728 // a read deadline that was set for some earlier read.
729 // We have one deadline for the whole write. In case of slow writing, we'll write
730 // the last chunk in one go, so remote smtp clients don't abort the connection for
732 deadline := c.earliestDeadline(30 * time.Second)
733 if err := c.conn.SetDeadline(deadline); err != nil {
734 c.log.Errorx("setting deadline for write", err)
739 nn, err := c.conn.Write(buf[:chunk])
741 panic(fmt.Errorf("write: %s (%w)", err, errIO))
745 if len(buf) > 0 && badClientDelay > 0 {
746 mox.Sleep(mox.Context, badClientDelay)
748 // Make sure we don't take too long, otherwise the remote SMTP client may close the
750 if time.Until(deadline) < 5*badClientDelay {
758// Read reads from the connection. It panics on i/o errors, which is handled by the
759// connection command loop.
760func (c *conn) Read(buf []byte) (int, error) {
761 if c.slow && badClientDelay > 0 {
762 mox.Sleep(mox.Context, badClientDelay)
766 // See comment about Deadline instead of individual read/write deadlines at Write.
767 if err := c.conn.SetDeadline(c.earliestDeadline(30 * time.Second)); err != nil {
768 c.log.Errorx("setting deadline for read", err)
771 n, err := c.conn.Read(buf)
773 panic(fmt.Errorf("read: %s (%w)", err, errIO))
778// Cache of line buffers for reading commands.
780var bufpool = moxio.NewBufpool(8, 2*1024)
782func (c *conn) readline() string {
783 line, err := bufpool.Readline(c.log, c.xbr)
784 if err != nil && errors.Is(err, moxio.ErrLineTooLong) {
785 c.writecodeline(smtp.C500BadSyntax, smtp.SeProto5Other0, "line too long, smtp max is 512, we reached 2048", nil)
786 panic(fmt.Errorf("%s (%w)", err, errIO))
787 } else if err != nil {
788 panic(fmt.Errorf("%s (%w)", err, errIO))
793// Buffered-write command response line to connection with codes and msg.
794// Err is not sent to remote but is used for logging and can be empty.
795func (c *conn) bwritecodeline(code int, secode string, msg string, err error) {
798 ecode = fmt.Sprintf("%d.%s", code/100, secode)
800 metricCommands.WithLabelValues(c.kind(), c.cmd, fmt.Sprintf("%d", code), ecode).Observe(float64(time.Since(c.cmdStart)) / float64(time.Second))
801 c.log.Debugx("smtp command result", err,
802 slog.String("kind", c.kind()),
803 slog.String("cmd", c.cmd),
804 slog.Int("code", code),
805 slog.String("ecode", ecode),
806 slog.Duration("duration", time.Since(c.cmdStart)))
813 // Separate by newline and wrap long lines.
814 lines := strings.Split(msg, "\n")
815 for i, line := range lines {
817 var prelen = 3 + 1 + len(ecode) + len(sep)
818 for prelen+len(line) > 510 {
820 for ; e > 400 && line[e] != ' '; e-- {
822 // 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.
823 c.bwritelinef("%d-%s%s%s", code, ecode, sep, line[:e])
827 if i < len(lines)-1 {
830 c.bwritelinef("%d%s%s%s%s", code, spdash, ecode, sep, line)
834// Buffered-write a formatted response line to connection.
835func (c *conn) bwritelinef(format string, args ...any) {
836 msg := fmt.Sprintf(format, args...)
837 fmt.Fprint(c.xbw, msg+"\r\n")
840// Flush pending buffered writes to connection.
841func (c *conn) xflush() {
842 c.xbw.Flush() // Errors will have caused a panic in Write.
845// Write (with flush) a response line with codes and message. err is not written, used for logging and can be nil.
846func (c *conn) writecodeline(code int, secode string, msg string, err error) {
847 c.bwritecodeline(code, secode, msg, err)
851// Write (with flush) a formatted response line to connection.
852func (c *conn) writelinef(format string, args ...any) {
853 c.bwritelinef(format, args...)
857var cleanClose struct{} // Sentinel value for panic/recover indicating clean close of connection.
859// ServeTLSConn serves a TLS connection.
860func ServeTLSConn(listenerName string, hostname dns.Domain, conn *tls.Conn, tlsConfig *tls.Config, submission, viaHTTPS bool, maxMsgSize int64, requireTLS bool) {
861 log := mlog.New("smtpserver", nil)
862 resolver := dns.StrictResolver{Log: log.Logger}
863 serve(listenerName, mox.Cid(), hostname, tlsConfig, conn, resolver, submission, true, viaHTTPS, maxMsgSize, true, true, requireTLS, nil, 0)
866func 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) {
867 var localIP, remoteIP net.IP
868 if a, ok := nc.LocalAddr().(*net.TCPAddr); ok {
871 // For net.Pipe, during tests.
872 localIP = net.ParseIP("127.0.0.10")
874 if a, ok := nc.RemoteAddr().(*net.TCPAddr); ok {
877 // For net.Pipe, during tests.
878 remoteIP = net.ParseIP("127.0.0.10")
883 origConn = nc.(*tls.Conn).NetConn()
890 submission: submission,
893 extRequireTLS: requireTLS,
896 baseTLSConfig: tlsConfig,
900 maxMessageSize: maxMessageSize,
901 requireTLSForAuth: requireTLSForAuth,
902 requireTLSForDelivery: requireTLSForDelivery,
904 firstTimeSenderDelay: firstTimeSenderDelay,
906 var logmutex sync.Mutex
907 // Also see (and possibly update) c.logbg, for logging in a goroutine.
908 c.log = mlog.New("smtpserver", nil).WithFunc(func() []slog.Attr {
910 defer logmutex.Unlock()
913 slog.Int64("cid", c.cid),
914 slog.Duration("delta", now.Sub(c.lastlog)),
917 if c.username != "" {
918 l = append(l, slog.String("username", c.username))
922 c.xtr = moxio.NewTraceReader(c.log, "RC: ", c)
923 c.xbr = bufio.NewReader(c.xtr)
924 c.xtw = moxio.NewTraceWriter(c.log, "LS: ", c)
925 c.xbw = bufio.NewWriter(c.xtw)
927 metricConnection.WithLabelValues(c.kind()).Inc()
928 c.log.Info("new connection",
929 slog.Any("remote", c.conn.RemoteAddr()),
930 slog.Any("local", c.conn.LocalAddr()),
931 slog.Bool("submission", submission),
932 slog.Bool("tls", xtls),
933 slog.Bool("viahttps", viaHTTPS),
934 slog.String("listener", listenerName))
937 err := c.origConn.Close() // Close actual TCP socket, regardless of TLS on top.
938 c.log.Check(err, "closing tcp connection")
939 c.conn.Close() // If TLS, will try to write alert notification to already closed socket, returning error quickly.
941 if c.account != nil {
942 err := c.account.Close()
943 c.log.Check(err, "closing account")
948 if x == nil || x == cleanClose {
949 c.log.Info("connection closed")
950 } else if err, ok := x.(error); ok && isClosed(err) {
951 c.log.Infox("connection closed", err)
953 c.log.Error("unhandled panic", slog.Any("err", x))
955 metrics.PanicInc(metrics.Smtpserver)
959 if xtls && !viaHTTPS {
960 // Start TLS on connection. We perform the handshake explicitly, so we can set a
961 // timeout, do client certificate authentication, log TLS details afterwards.
962 c.xtlsHandshakeAndAuthenticate(c.conn)
966 case <-mox.Shutdown.Done():
968 c.writecodeline(smtp.C421ServiceUnavail, smtp.SeSys3NotAccepting2, "shutting down", nil)
973 if !limiterConnectionRate.Add(c.remoteIP, time.Now(), 1) {
974 c.writecodeline(smtp.C421ServiceUnavail, smtp.SePol7Other0, "connection rate from your ip or network too high, slow down please", nil)
978 // If remote IP/network resulted in too many authentication failures, refuse to serve.
979 if submission && !mox.LimiterFailedAuth.CanAdd(c.remoteIP, time.Now(), 1) {
980 metrics.AuthenticationRatelimitedInc("submission")
981 c.log.Debug("refusing connection due to many auth failures", slog.Any("remoteip", c.remoteIP))
982 c.writecodeline(smtp.C421ServiceUnavail, smtp.SePol7Other0, "too many auth failures", nil)
986 if !limiterConnections.Add(c.remoteIP, time.Now(), 1) {
987 c.log.Debug("refusing connection due to many open connections", slog.Any("remoteip", c.remoteIP))
988 c.writecodeline(smtp.C421ServiceUnavail, smtp.SePol7Other0, "too many open connections from your ip or network", nil)
991 defer limiterConnections.Add(c.remoteIP, time.Now(), -1)
993 // We register and unregister the original connection, in case c.conn is replaced
994 // with a TLS connection later on.
995 mox.Connections.Register(nc, "smtp", listenerName)
996 defer mox.Connections.Unregister(nc)
1000 // We include the string ESMTP. https://cr.yp.to/smtp/greeting.html recommends it.
1001 // Should not be too relevant nowadays, but does not hurt and default blackbox
1002 // exporter SMTP health check expects it.
1003 c.writelinef("%d %s ESMTP mox", smtp.C220ServiceReady, c.hostname.ASCII)
1008 // If another command is present, don't flush our buffered response yet. Holding
1009 // off will cause us to respond with a single packet.
1010 n := c.xbr.Buffered()
1012 buf, err := c.xbr.Peek(n)
1013 if err == nil && bytes.IndexByte(buf, '\n') >= 0 {
1021var commands = map[string]func(c *conn, p *parser){
1022 "helo": (*conn).cmdHelo,
1023 "ehlo": (*conn).cmdEhlo,
1024 "starttls": (*conn).cmdStarttls,
1025 "auth": (*conn).cmdAuth,
1026 "mail": (*conn).cmdMail,
1027 "rcpt": (*conn).cmdRcpt,
1028 "data": (*conn).cmdData,
1029 "rset": (*conn).cmdRset,
1030 "vrfy": (*conn).cmdVrfy,
1031 "expn": (*conn).cmdExpn,
1032 "help": (*conn).cmdHelp,
1033 "noop": (*conn).cmdNoop,
1034 "quit": (*conn).cmdQuit,
1037func command(c *conn) {
1043 err, ok := x.(error)
1053 if errors.As(err, &serr) {
1054 c.writecodeline(serr.code, serr.secode, fmt.Sprintf("%s (%s)", serr.errmsg, mox.ReceivedID(c.cid)), serr.err)
1055 if serr.printStack {
1056 c.log.Errorx("smtp error", serr.err, slog.Int("code", serr.code), slog.String("secode", serr.secode))
1060 // Other type of panic, we pass it on, aborting the connection.
1061 c.log.Errorx("command panic", err)
1066 // todo future: we could wait for either a line or shutdown, and just close the connection on shutdown.
1068 line := c.readline()
1069 t := strings.SplitN(line, " ", 2)
1075 cmdl := strings.ToLower(cmd)
1077 // 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
1080 case <-mox.Shutdown.Done():
1082 c.writecodeline(smtp.C421ServiceUnavail, smtp.SeSys3NotAccepting2, "shutting down", nil)
1088 c.cmdStart = time.Now()
1090 p := newParser(args, c.smtputf8, c)
1091 fn, ok := commands[cmdl]
1095 // Other side is likely speaking something else than SMTP, send error message and
1096 // stop processing because there is a good chance whatever they sent has multiple
1098 c.writecodeline(smtp.C500BadSyntax, smtp.SeProto5Syntax2, "please try again speaking smtp", nil)
1102 xsmtpUserErrorf(smtp.C500BadSyntax, smtp.SeProto5BadCmdOrSeq1, "unknown command")
1108// For use in metric labels.
1109func (c *conn) kind() string {
1116func (c *conn) xneedHello() {
1117 if c.hello.IsZero() {
1118 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "no ehlo/helo yet")
1122// If smtp server is configured to require TLS for all mail delivery (except to TLS
1123// reporting address), abort command.
1124func (c *conn) xneedTLSForDelivery(rcpt smtp.Path) {
1125 // For TLS reports, we allow the message in even without TLS, because there may be
1127 if c.requireTLSForDelivery && !c.tls && !isTLSReportRecipient(rcpt) {
1129 xsmtpUserErrorf(smtp.C530SecurityRequired, smtp.SePol7Other0, "STARTTLS required for mail delivery")
1133func isTLSReportRecipient(rcpt smtp.Path) bool {
1134 _, _, _, dest, err := mox.LookupAddress(rcpt.Localpart, rcpt.IPDomain.Domain, false, false, false)
1135 return err == nil && (dest.HostTLSReports || dest.DomainTLSReports)
1138func (c *conn) cmdHelo(p *parser) {
1139 c.cmdHello(p, false)
1142func (c *conn) cmdEhlo(p *parser) {
1147func (c *conn) cmdHello(p *parser, ehlo bool) {
1148 var remote dns.IPDomain
1149 if c.submission && !mox.Pedantic {
1150 // Mail clients regularly put bogus information in the hostname/ip. For submission,
1151 // the value is of no use, so there is not much point in annoying the user with
1152 // errors they cannot fix themselves. Except when in pedantic mode.
1153 remote = dns.IPDomain{IP: c.remoteIP}
1157 remote = p.xipdomain(true)
1159 remote = dns.IPDomain{Domain: p.xdomain()}
1161 // Verify a remote domain name has an A or AAAA record, CNAME not allowed.
../rfc/5321:722
1162 cidctx := context.WithValue(mox.Context, mlog.CidKey, c.cid)
1163 ctx, cancel := context.WithTimeout(cidctx, time.Minute)
1164 _, _, err := c.resolver.LookupIPAddr(ctx, remote.Domain.ASCII+".")
1166 if dns.IsNotFound(err) {
1167 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeProto5Other0, "your ehlo domain does not resolve to an IP address")
1169 // For success or temporary resolve errors, we'll just continue.
1172 // Though a few paragraphs earlier is a claim additional data can occur for address
1173 // literals (IP addresses), although the ABNF in that document does not allow it.
1174 // We allow additional text, but only if space-separated.
1175 if len(remote.IP) > 0 && p.space() {
1187 // https://www.iana.org/assignments/mail-parameters/mail-parameters.xhtml
1189 c.bwritelinef("250-%s", c.hostname.ASCII)
1193 if !c.tls && c.baseTLSConfig != nil {
1195 c.bwritelinef("250-STARTTLS")
1196 } else if c.extRequireTLS {
1199 c.bwritelinef("250-REQUIRETLS")
1204 if c.tls || !c.requireTLSForAuth {
1205 // We always mention the SCRAM PLUS variants, even if TLS is not active: It is a
1206 // hint to the client that a TLS connection can use TLS channel binding during
1207 // authentication. The client should select the bare variant when TLS isn't
1208 // present, and also not indicate the server supports the PLUS variant in that
1209 // case, or it would trigger the mechanism downgrade detection.
1210 mechs = "SCRAM-SHA-256-PLUS SCRAM-SHA-256 SCRAM-SHA-1-PLUS SCRAM-SHA-1 CRAM-MD5 PLAIN LOGIN"
1212 if c.tls && len(c.conn.(*tls.Conn).ConnectionState().PeerCertificates) > 0 && !c.viaHTTPS {
1213 mechs = "EXTERNAL " + mechs
1215 c.bwritelinef("250-AUTH %s", mechs)
1218 c.bwritelinef("250-FUTURERELEASE %d %s", queue.FutureReleaseIntervalMax/time.Second, t.Format(time.RFC3339))
1221 // todo future? c.writelinef("250-DSN")
1229func (c *conn) cmdStarttls(p *parser) {
1235 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "already speaking tls")
1237 if c.account != nil {
1238 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "cannot starttls after authentication")
1240 if c.baseTLSConfig == nil {
1241 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "starttls not offered")
1244 // We don't want to do TLS on top of c.r because it also prints protocol traces: We
1245 // don't want to log the TLS stream. So we'll do TLS on the underlying connection,
1246 // but make sure any bytes already read and in the buffer are used for the TLS
1249 if n := c.xbr.Buffered(); n > 0 {
1250 conn = &moxio.PrefixConn{
1251 PrefixReader: io.LimitReader(c.xbr, int64(n)),
1256 // We add the cid to the output, to help debugging in case of a failing TLS connection.
1257 c.writecodeline(smtp.C220ServiceReady, smtp.SeOther00, "go! ("+mox.ReceivedID(c.cid)+")", nil)
1259 c.xtlsHandshakeAndAuthenticate(conn)
1266func (c *conn) cmdAuth(p *parser) {
1270 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "authentication only allowed on submission ports")
1274 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "already authenticated")
1276 if c.mailFrom != nil {
1278 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "authentication not allowed during mail transaction")
1281 // If authentication fails due to missing derived secrets, we don't hold it against
1282 // the connection. There is no way to indicate server support for an authentication
1283 // mechanism, but that a mechanism won't work for an account.
1284 var missingDerivedSecrets bool
1286 // For many failed auth attempts, slow down verification attempts.
1287 // Dropping the connection could also work, but more so when we have a connection rate limiter.
1289 if c.authFailed > 3 && authFailDelay > 0 {
1291 mox.Sleep(mox.Context, time.Duration(c.authFailed-3)*authFailDelay)
1293 c.authFailed++ // Compensated on success.
1295 if missingDerivedSecrets {
1298 // On the 3rd failed authentication, start responding slowly. Successful auth will
1299 // cause fast responses again.
1300 if c.authFailed >= 3 {
1305 la := c.loginAttempt(true, "")
1307 store.LoginAttemptAdd(context.Background(), c.logbg(), la)
1308 if la.Result == store.AuthSuccess {
1309 mox.LimiterFailedAuth.Reset(c.remoteIP, time.Now())
1310 } else if !missingDerivedSecrets {
1311 mox.LimiterFailedAuth.Add(c.remoteIP, time.Now(), 1)
1317 mech := p.xsaslMech()
1319 // Read the first parameter, either as initial parameter or by sending a
1320 // continuation with the optional encChal (must already be base64-encoded).
1321 xreadInitial := func(encChal string) []byte {
1325 // todo future: handle max length of 12288 octets and return proper responde codes otherwise
../rfc/4954:253
1329 la.Result = store.AuthAborted
1330 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5Other0, "authentication aborted")
1335 // Windows Mail 16005.14326.21606.0 sends two spaces between "AUTH PLAIN" and the
1340 auth = p.remainder()
1343 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5Syntax2, "missing initial auth base64 parameter after space")
1344 } else if auth == "=" {
1346 auth = "" // Base64 decode below will result in empty buffer.
1349 buf, err := base64.StdEncoding.DecodeString(auth)
1352 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5Syntax2, "invalid base64: %s", err)
1357 xreadContinuation := func() []byte {
1358 line := c.readline()
1360 la.Result = store.AuthAborted
1361 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5Other0, "authentication aborted")
1363 buf, err := base64.StdEncoding.DecodeString(line)
1366 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5Syntax2, "invalid base64: %s", err)
1371 // The various authentication mechanisms set account and username. We may already
1372 // have an account and username from TLS client authentication. Afterwards, we
1373 // check that the account is the same.
1374 var account *store.Account
1378 err := account.Close()
1379 c.log.Check(err, "close account")
1385 la.AuthMech = "plain"
1389 if !c.tls && c.requireTLSForAuth {
1390 xsmtpUserErrorf(smtp.C538EncReqForAuth, smtp.SePol7EncReqForAuth11, "authentication requires tls")
1393 // Password is in line in plain text, so hide it.
1394 defer c.xtrace(mlog.LevelTraceauth)()
1395 buf := xreadInitial("")
1396 c.xtrace(mlog.LevelTrace) // Restore.
1397 plain := bytes.Split(buf, []byte{0})
1398 if len(plain) != 3 {
1399 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5BadParams4, "auth data should have 3 nul-separated tokens, got %d", len(plain))
1401 authz := norm.NFC.String(string(plain[0]))
1402 username = norm.NFC.String(string(plain[1]))
1403 la.LoginAddress = username
1404 password := string(plain[2])
1406 if authz != "" && authz != username {
1407 la.Result = store.AuthBadCredentials
1408 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "cannot assume other role")
1412 account, la.AccountName, err = store.OpenEmailAuth(c.log, username, password, false)
1413 if err != nil && errors.Is(err, store.ErrUnknownCredentials) {
1415 la.Result = store.AuthBadCredentials
1416 c.log.Info("failed authentication attempt", slog.String("username", username), slog.Any("remote", c.remoteIP))
1417 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass")
1419 xcheckf(err, "verifying credentials")
1422 // LOGIN is obsoleted in favor of PLAIN, only implemented to support legacy
1423 // clients, see Internet-Draft (I-D):
1424 // https://datatracker.ietf.org/doc/html/draft-murchison-sasl-login-00
1426 la.LoginAddress = "login"
1430 if !c.tls && c.requireTLSForAuth {
1431 xsmtpUserErrorf(smtp.C538EncReqForAuth, smtp.SePol7EncReqForAuth11, "authentication requires tls")
1434 // Read user name. The I-D says the client should ignore the server challenge, but
1435 // also that some clients may require challenge "Username:" instead of "User
1436 // Name". We can't sent both... Servers most commonly return "Username:" and
1437 // "Password:", so we do the same.
1438 // I-D says maximum length must be 64 bytes. We allow more, for long user names
1440 encChal := base64.StdEncoding.EncodeToString([]byte("Username:"))
1441 username = string(xreadInitial(encChal))
1442 username = norm.NFC.String(username)
1443 la.LoginAddress = username
1445 // Again, client should ignore the challenge, we send the same as the example in
1447 c.writelinef("%d %s", smtp.C334ContinueAuth, base64.StdEncoding.EncodeToString([]byte("Password:")))
1449 // Password is in line in plain text, so hide it.
1450 defer c.xtrace(mlog.LevelTraceauth)()
1451 password := string(xreadContinuation())
1452 c.xtrace(mlog.LevelTrace) // Restore.
1455 account, la.AccountName, err = store.OpenEmailAuth(c.log, username, password, false)
1456 if err != nil && errors.Is(err, store.ErrUnknownCredentials) {
1458 la.Result = store.AuthBadCredentials
1459 c.log.Info("failed authentication attempt", slog.String("username", username), slog.Any("remote", c.remoteIP))
1460 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass")
1462 xcheckf(err, "verifying credentials")
1465 la.AuthMech = strings.ToLower(mech)
1470 chal := fmt.Sprintf("<%d.%d@%s>", uint64(mox.CryptoRandInt()), time.Now().UnixNano(), mox.Conf.Static.HostnameDomain.ASCII)
1471 c.writelinef("%d %s", smtp.C334ContinueAuth, base64.StdEncoding.EncodeToString([]byte(chal)))
1473 resp := xreadContinuation()
1474 t := strings.Split(string(resp), " ")
1475 if len(t) != 2 || len(t[1]) != 2*md5.Size {
1476 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5BadParams4, "malformed cram-md5 response")
1478 username = norm.NFC.String(t[0])
1479 la.LoginAddress = username
1480 c.log.Debug("cram-md5 auth", slog.String("username", username))
1482 account, la.AccountName, _, err = store.OpenEmail(c.log, username, false)
1483 if err != nil && errors.Is(err, store.ErrUnknownCredentials) {
1484 la.Result = store.AuthBadCredentials
1485 c.log.Info("failed authentication attempt", slog.String("username", username), slog.Any("remote", c.remoteIP))
1486 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass")
1488 xcheckf(err, "looking up address")
1489 la.AccountName = account.Name
1490 var ipadhash, opadhash hash.Hash
1491 account.WithRLock(func() {
1492 err := account.DB.Read(context.TODO(), func(tx *bstore.Tx) error {
1493 password, err := bstore.QueryTx[store.Password](tx).Get()
1494 if err == bstore.ErrAbsent {
1495 c.log.Info("failed authentication attempt", slog.String("username", username), slog.Any("remote", c.remoteIP))
1496 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass")
1502 ipadhash = password.CRAMMD5.Ipad
1503 opadhash = password.CRAMMD5.Opad
1506 xcheckf(err, "tx read")
1508 if ipadhash == nil || opadhash == nil {
1509 missingDerivedSecrets = true
1510 c.log.Info("cram-md5 auth attempt without derived secrets set, save password again to store secrets", slog.String("username", username))
1511 c.log.Info("failed authentication attempt", slog.String("username", username), slog.Any("remote", c.remoteIP))
1512 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass")
1516 ipadhash.Write([]byte(chal))
1517 opadhash.Write(ipadhash.Sum(nil))
1518 digest := fmt.Sprintf("%x", opadhash.Sum(nil))
1520 c.log.Info("failed authentication attempt", slog.String("username", username), slog.Any("remote", c.remoteIP))
1521 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass")
1524 case "SCRAM-SHA-256-PLUS", "SCRAM-SHA-256", "SCRAM-SHA-1-PLUS", "SCRAM-SHA-1":
1525 // 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?
1526 // todo: use single implementation between ../imapserver/server.go and ../smtpserver/server.go
1528 // Passwords cannot be retrieved or replayed from the trace.
1530 la.AuthMech = strings.ToLower(mech)
1531 var h func() hash.Hash
1532 switch la.AuthMech {
1533 case "scram-sha-1", "scram-sha-1-plus":
1535 case "scram-sha-256", "scram-sha-256-plus":
1538 xsmtpServerErrorf(codes{smtp.C554TransactionFailed, smtp.SeSys3Other0}, "missing scram auth method case")
1541 var cs *tls.ConnectionState
1542 channelBindingRequired := strings.HasSuffix(la.AuthMech, "-plus")
1543 if channelBindingRequired && !c.tls {
1545 xsmtpUserErrorf(smtp.C538EncReqForAuth, smtp.SePol7EncReqForAuth11, "scram plus mechanism requires tls connection")
1548 xcs := c.conn.(*tls.Conn).ConnectionState()
1551 c0 := xreadInitial("")
1552 ss, err := scram.NewServer(h, c0, cs, channelBindingRequired)
1554 c.log.Infox("scram protocol error", err, slog.Any("remote", c.remoteIP))
1555 xsmtpUserErrorf(smtp.C455BadParams, smtp.SePol7Other0, "scram protocol error: %s", err)
1557 username = ss.Authentication
1558 la.LoginAddress = username
1559 c.log.Debug("scram auth", slog.String("authentication", username))
1560 account, la.AccountName, _, err = store.OpenEmail(c.log, username, false)
1562 // todo: we could continue scram with a generated salt, deterministically generated
1563 // from the username. that way we don't have to store anything but attackers cannot
1564 // learn if an account exists. same for absent scram saltedpassword below.
1565 c.log.Info("failed authentication attempt", slog.String("username", username), slog.Any("remote", c.remoteIP))
1566 xsmtpUserErrorf(smtp.C454TempAuthFail, smtp.SeSys3Other0, "scram not possible")
1568 if ss.Authorization != "" && ss.Authorization != username {
1569 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "authentication with authorization for different user not supported")
1571 var xscram store.SCRAM
1572 account.WithRLock(func() {
1573 err := account.DB.Read(context.TODO(), func(tx *bstore.Tx) error {
1574 password, err := bstore.QueryTx[store.Password](tx).Get()
1575 if err == bstore.ErrAbsent {
1576 c.log.Info("failed authentication attempt", slog.String("username", username), slog.Any("remote", c.remoteIP))
1577 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass")
1579 xcheckf(err, "fetching credentials")
1580 switch la.AuthMech {
1581 case "scram-sha-1", "scram-sha-1-plus":
1582 xscram = password.SCRAMSHA1
1583 case "scram-sha-256", "scram-sha-256-plus":
1584 xscram = password.SCRAMSHA256
1586 xsmtpServerErrorf(codes{smtp.C554TransactionFailed, smtp.SeSys3Other0}, "missing scram auth credentials case")
1588 if len(xscram.Salt) == 0 || xscram.Iterations == 0 || len(xscram.SaltedPassword) == 0 {
1589 missingDerivedSecrets = true
1590 c.log.Info("scram auth attempt without derived secrets set, save password again to store secrets", slog.String("address", username))
1591 c.log.Info("failed authentication attempt", slog.String("username", username), slog.Any("remote", c.remoteIP))
1592 xsmtpUserErrorf(smtp.C454TempAuthFail, smtp.SeSys3Other0, "scram not possible")
1596 xcheckf(err, "read tx")
1598 s1, err := ss.ServerFirst(xscram.Iterations, xscram.Salt)
1599 xcheckf(err, "scram first server step")
1600 c.writelinef("%d %s", smtp.C334ContinueAuth, base64.StdEncoding.EncodeToString([]byte(s1))) //
../rfc/4954:187
1601 c2 := xreadContinuation()
1602 s3, err := ss.Finish(c2, xscram.SaltedPassword)
1604 c.writelinef("%d %s", smtp.C334ContinueAuth, base64.StdEncoding.EncodeToString([]byte(s3))) //
../rfc/4954:187
1607 c.readline() // Should be "*" for cancellation.
1608 if errors.Is(err, scram.ErrInvalidProof) {
1609 la.Result = store.AuthBadCredentials
1610 c.log.Info("failed authentication attempt", slog.String("username", username), slog.Any("remote", c.remoteIP))
1611 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad credentials")
1612 } else if errors.Is(err, scram.ErrChannelBindingsDontMatch) {
1613 la.Result = store.AuthBadChannelBinding
1614 c.log.Warn("bad channel binding during authentication, potential mitm", slog.String("username", username), slog.Any("remote", c.remoteIP))
1615 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7MsgIntegrity7, "channel bindings do not match, potential mitm")
1616 } else if errors.Is(err, scram.ErrInvalidEncoding) {
1617 la.Result = store.AuthBadProtocol
1618 c.log.Infox("bad scram protocol message", err, slog.String("username", username), slog.Any("remote", c.remoteIP))
1619 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7Other0, "bad scram protocol message")
1621 xcheckf(err, "server final")
1625 // The message should be empty. todo: should we require it is empty?
1629 la.AuthMech = "external"
1632 buf := xreadInitial("")
1633 username = norm.NFC.String(string(buf))
1634 la.LoginAddress = username
1638 xsmtpUserErrorf(smtp.C538EncReqForAuth, smtp.SePol7EncReqForAuth11, "tls required for tls client certificate authentication")
1640 if c.account == nil {
1641 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "missing client certificate, required for tls client certificate authentication")
1645 username = c.username
1646 la.LoginAddress = username
1649 account, la.AccountName, _, err = store.OpenEmail(c.log, username, false)
1650 xcheckf(err, "looking up username from tls client authentication")
1653 la.AuthMech = "(unrecognized)"
1655 xsmtpUserErrorf(smtp.C504ParamNotImpl, smtp.SeProto5BadParams4, "mechanism %s not supported", mech)
1658 if accConf, ok := account.Conf(); !ok {
1659 xcheckf(errors.New("cannot find account"), "get account config")
1660 } else if accConf.LoginDisabled != "" {
1661 la.Result = store.AuthLoginDisabled
1662 c.log.Info("account login disabled", slog.String("username", username))
1663 xsmtpUserErrorf(smtp.C525AccountDisabled, smtp.SePol7AccountDisabled13, "%w: %s", store.ErrLoginDisabled, accConf.LoginDisabled)
1666 // We may already have TLS credentials. We allow an additional SASL authentication,
1667 // possibly with different username, but the account must be the same.
1668 if c.account != nil {
1669 if account != c.account {
1670 c.log.Debug("sasl authentication for different account than tls client authentication, aborting connection",
1671 slog.String("saslmechanism", la.AuthMech),
1672 slog.String("saslaccount", account.Name),
1673 slog.String("tlsaccount", c.account.Name),
1674 slog.String("saslusername", username),
1675 slog.String("tlsusername", c.username),
1677 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "authentication failed, tls client certificate public key belongs to another account")
1678 } else if username != c.username {
1679 c.log.Debug("sasl authentication for different username than tls client certificate authentication, switching to sasl username",
1680 slog.String("saslmechanism", la.AuthMech),
1681 slog.String("saslusername", username),
1682 slog.String("tlsusername", c.username),
1683 slog.String("account", c.account.Name),
1688 account = nil // Prevent cleanup.
1690 c.username = username
1692 la.LoginAddress = c.username
1693 la.AccountName = c.account.Name
1694 la.Result = store.AuthSuccess
1699 c.writecodeline(smtp.C235AuthSuccess, smtp.SePol7Other0, "nice", nil)
1703func (c *conn) cmdMail(p *parser) {
1704 // requirements for maximum line length:
1706 // todo future: enforce? doesn't really seem worth it...
1708 if c.transactionBad > 10 && c.transactionGood == 0 {
1709 // If we get many bad transactions, it's probably a spammer that is guessing user names.
1710 // Useful in combination with rate limiting.
1712 c.writecodeline(smtp.C550MailboxUnavail, smtp.SeAddr1Other0, "too many failures", nil)
1718 if c.mailFrom != nil {
1720 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "already have MAIL")
1722 // Ensure clear transaction state on failure.
1733 // Microsoft Outlook 365 Apps for Enterprise sends it with submission. For delivery
1734 // it is mostly used by spammers, but has been seen with legitimate senders too.
1738 rawRevPath := p.xrawReversePath()
1739 paramSeen := map[string]bool{}
1742 key := p.xparamKeyword()
1744 K := strings.ToUpper(key)
1747 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5BadParams4, "duplicate param %q", key)
1755 if size > c.maxMessageSize {
1757 ecode := smtp.SeSys3MsgLimitExceeded4
1758 if size < config.DefaultMaxMsgSize {
1759 ecode = smtp.SeMailbox2MsgLimitExceeded3
1761 xsmtpUserErrorf(smtp.C552MailboxFull, ecode, "message too large")
1763 // We won't verify the message is exactly the size the remote claims. Buf if it is
1764 // larger, we'll abort the transaction when remote crosses the boundary.
1768 v := p.xparamValue()
1769 switch strings.ToUpper(v) {
1771 c.has8bitmime = false
1773 c.has8bitmime = true
1775 xsmtpUserErrorf(smtp.C555UnrecognizedAddrParams, smtp.SeProto5BadParams4, "unrecognized parameter %q", key)
1780 // We act as if we don't trust the client to specify a mailbox. Instead, we always
1781 // check the rfc5321.mailfrom and rfc5322.from before accepting the submission.
1785 // 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
1793 c.msgsmtputf8 = true
1797 xsmtpUserErrorf(smtp.C523EncryptionNeeded, smtp.SePol7EncNeeded10, "requiretls only allowed on tls-encrypted connections")
1798 } else if !c.extRequireTLS {
1799 xsmtpUserErrorf(smtp.C555UnrecognizedAddrParams, smtp.SeSys3NotSupported3, "REQUIRETLS not allowed for this connection")
1803 case "HOLDFOR", "HOLDUNTIL":
1806 xsmtpUserErrorf(smtp.C555UnrecognizedAddrParams, smtp.SeSys3NotSupported3, "unrecognized parameter %q", key)
1808 if K == "HOLDFOR" && paramSeen["HOLDUNTIL"] || K == "HOLDUNTIL" && paramSeen["HOLDFOR"] {
1810 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5BadParams4, "cannot use both HOLDUNTIL and HOLFOR")
1814 // semantic errors as syntax errors
1817 if n > int64(queue.FutureReleaseIntervalMax/time.Second) {
1819 xsmtpUserErrorf(smtp.C554TransactionFailed, smtp.SeProto5BadParams4, "future release interval too far in the future")
1821 c.futureRelease = time.Now().Add(time.Duration(n) * time.Second)
1822 c.futureReleaseRequest = fmt.Sprintf("for;%d", n)
1824 t, s := p.xdatetimeutc()
1825 ival := time.Until(t)
1827 // Likely a mistake by the user.
1828 xsmtpUserErrorf(smtp.C554TransactionFailed, smtp.SeProto5BadParams4, "requested future release time is in the past")
1829 } else if ival > queue.FutureReleaseIntervalMax {
1831 xsmtpUserErrorf(smtp.C554TransactionFailed, smtp.SeProto5BadParams4, "requested future release time is too far in the future")
1834 c.futureReleaseRequest = "until;" + s
1838 xsmtpUserErrorf(smtp.C555UnrecognizedAddrParams, smtp.SeSys3NotSupported3, "unrecognized parameter %q", key)
1842 // We now know if we have to parse the address with support for utf8.
1843 pp := newParser(rawRevPath, c.smtputf8, c)
1844 rpath := pp.xbareReversePath()
1849 // For submission, check if reverse path is allowed. I.e. authenticated account
1850 // must have the rpath configured. We do a check again on rfc5322.from during DATA.
1851 // Mail clients may use the alias address as smtp mail from address, so we allow it
1852 // for such aliases.
1853 rpathAllowed := func(disabled *bool) bool {
1859 from := smtp.NewAddress(rpath.Localpart, rpath.IPDomain.Domain)
1860 ok, dis := mox.AllowMsgFrom(c.account.Name, from)
1865 if !c.submission && !rpath.IPDomain.Domain.IsZero() {
1866 // If rpath domain has null MX record or is otherwise not accepting email, reject.
1869 cidctx := context.WithValue(mox.Context, mlog.CidKey, c.cid)
1870 ctx, cancel := context.WithTimeout(cidctx, time.Minute)
1871 valid, err := checkMXRecords(ctx, c.resolver, rpath.IPDomain.Domain)
1874 c.log.Infox("temporary reject for temporary mx lookup error", err)
1875 xsmtpServerErrorf(codes{smtp.C451LocalErr, smtp.SeNet4Other0}, "cannot verify mx records for mailfrom domain")
1876 } else if !valid && !(Localserve && rpath.IPDomain.Domain.ASCII == "localhost") {
1877 // We don't reject for "localhost" in Localserve mode because we only resolve
1878 // through DNS, not an /etc/hosts file, and localhost may not resolve through DNS,
1879 // depending on network environment.
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.xbr)
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.LocalpartCatchallSeparatorsEffective[0], 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.LocalpartCatchallSeparatorsEffective[0] + 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 = slices.Clone(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("Expunged", false)
3276 q.FilterEqual("Notjunk", true)
3277 q.FilterEqual("IsReject", false)
3278 exists, err := q.Exists()
3280 return fmt.Errorf("querying for non-junk message from organizational domain: %v", err)
3286 // See if we sent a message to this organizational domain.
3287 qr := bstore.QueryTx[store.Recipient](tx)
3288 qr.FilterNonzero(store.Recipient{OrgDomain: a0.d.m.MsgFromOrgDomain})
3289 exists, err = qr.Exists()
3291 return fmt.Errorf("querying for message sent to organizational domain: %v", err)
3299 log.Errorx("checking if sender is unknown domain, for dmarc aggregate report evaluation", err)
3304 r := dmarcResult.Record
3305 addresses := make([]string, len(r.AggregateReportAddresses))
3306 for i, a := range r.AggregateReportAddresses {
3307 addresses[i] = a.String()
3309 sp := dmarcrpt.Disposition(r.SubdomainPolicy)
3310 if r.SubdomainPolicy == dmarc.PolicyEmpty {
3311 sp = dmarcrpt.Disposition(r.Policy)
3313 eval := dmarcdb.Evaluation{
3314 // Evaluated and IntervalHours set by AddEvaluation.
3315 PolicyDomain: dmarcResult.Domain.Name(),
3317 // Optional evaluations don't cause a report to be sent, but will be included.
3318 // Useful for automated inter-mailer messages, we don't want to get in a reporting
3319 // loop. We also don't want to be used for sending reports to unsuspecting domains
3320 // we have no relation with.
3321 // 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.
3322 Optional: a0.d.destination.DMARCReports || a0.d.destination.HostTLSReports || a0.d.destination.DomainTLSReports || a0.reason == reasonDMARCPolicy && unknownDomain(),
3324 Addresses: addresses,
3326 PolicyPublished: dmarcrpt.PolicyPublished{
3327 Domain: dmarcResult.Domain.Name(),
3328 ADKIM: dmarcrpt.Alignment(r.ADKIM),
3329 ASPF: dmarcrpt.Alignment(r.ASPF),
3330 Policy: dmarcrpt.Disposition(r.Policy),
3331 SubdomainPolicy: sp,
3332 Percentage: r.Percentage,
3333 // We don't save ReportingOptions, we don't do per-message failure reporting.
3335 SourceIP: c.remoteIP.String(),
3336 Disposition: disposition,
3337 AlignedDKIMPass: dmarcResult.AlignedDKIMPass,
3338 AlignedSPFPass: dmarcResult.AlignedSPFPass,
3339 EnvelopeTo: rcpt.Addr.IPDomain.String(),
3340 EnvelopeFrom: c.mailFrom.IPDomain.String(),
3341 HeaderFrom: msgFrom.Domain.Name(),
3344 for _, s := range dmarcOverrides {
3345 reason := dmarcrpt.PolicyOverrideReason{Type: dmarcrpt.PolicyOverride(s)}
3346 eval.OverrideReasons = append(eval.OverrideReasons, reason)
3349 // We'll include all signatures for the organizational domain, even if they weren't
3350 // relevant due to strict alignment requirement.
3351 for _, dkimResult := range dkimResults {
3352 if dkimResult.Sig == nil || publicsuffix.Lookup(ctx, log.Logger, msgFrom.Domain) != publicsuffix.Lookup(ctx, log.Logger, dkimResult.Sig.Domain) {
3355 r := dmarcrpt.DKIMAuthResult{
3356 Domain: dkimResult.Sig.Domain.Name(),
3357 Selector: dkimResult.Sig.Selector.ASCII,
3358 Result: dmarcrpt.DKIMResult(dkimResult.Status),
3360 eval.DKIMResults = append(eval.DKIMResults, r)
3363 switch receivedSPF.Identity {
3364 case spf.ReceivedHELO:
3365 spfAuthResult := dmarcrpt.SPFAuthResult{
3366 Domain: spfArgs.HelloDomain.String(), // Can be unicode and also IP.
3367 Scope: dmarcrpt.SPFDomainScopeHelo,
3368 Result: dmarcrpt.SPFResult(receivedSPF.Result),
3370 eval.SPFResults = []dmarcrpt.SPFAuthResult{spfAuthResult}
3371 case spf.ReceivedMailFrom:
3372 spfAuthResult := dmarcrpt.SPFAuthResult{
3373 Domain: spfArgs.MailFromDomain.Name(), // Can be unicode.
3374 Scope: dmarcrpt.SPFDomainScopeMailFrom,
3375 Result: dmarcrpt.SPFResult(receivedSPF.Result),
3377 eval.SPFResults = []dmarcrpt.SPFAuthResult{spfAuthResult}
3380 err := dmarcdb.AddEvaluation(ctx, dmarcResult.Record.AggregateReportingInterval, &eval)
3381 log.Check(err, "adding dmarc evaluation to database for aggregate report")
3385 for _, a := range la {
3386 // Don't add message if address was also explicitly present in a RCPT TO command.
3387 if rcpt.Alias != nil && regularRecipient(a.d.deliverTo) {
3391 conf, _ := a.d.acc.Conf()
3392 if conf.RejectsMailbox == "" {
3395 present, _, messagehash, err := rejectPresent(log, a.d.acc, conf.RejectsMailbox, a.d.m, dataFile)
3397 log.Errorx("checking whether reject is already present", err)
3400 log.Info("reject message is already present, ignoring")
3403 a.d.m.IsReject = true
3404 a.d.m.Seen = true // We don't want to draw attention.
3405 // Regular automatic junk flags configuration applies to these messages. The
3406 // default is to treat these as neutral, so they won't cause outright rejections
3407 // due to reputation for later delivery attempts.
3408 a.d.m.MessageHash = messagehash
3409 a.d.acc.WithWLock(func() {
3410 var changes []store.Change
3416 p := a.d.acc.MessagePath(newID)
3418 c.log.Check(err, "remove message after error delivering to rejects", slog.String("path", p))
3422 err := a.d.acc.DB.Write(context.TODO(), func(tx *bstore.Tx) error {
3423 mbrej, err := a.d.acc.MailboxFind(tx, conf.RejectsMailbox)
3425 return fmt.Errorf("finding rejects mailbox: %v", err)
3428 if !conf.KeepRejects && mbrej != nil {
3429 chl, hasSpace, err := a.d.acc.TidyRejectsMailbox(c.log, tx, mbrej)
3431 return fmt.Errorf("tidying rejects mailbox: %v", err)
3433 changes = append(changes, chl...)
3435 log.Info("not storing spammy mail to full rejects mailbox")
3440 nmb, chl, _, _, err := a.d.acc.MailboxCreate(tx, conf.RejectsMailbox, store.SpecialUse{})
3442 return fmt.Errorf("creating rejects mailbox: %v", err)
3444 changes = append(changes, chl...)
3448 a.d.m.MailboxID = mbrej.ID
3449 if err := a.d.acc.MessageAdd(log, tx, mbrej, a.d.m, dataFile, store.AddOpts{}); err != nil {
3450 return fmt.Errorf("delivering spammy mail to rejects mailbox: %v", err)
3454 if err := tx.Update(mbrej); err != nil {
3455 return fmt.Errorf("updating rejects mailbox: %v", err)
3457 changes = append(changes, a.d.m.ChangeAddUID(), mbrej.ChangeCounts())
3462 log.Errorx("delivering to rejects mailbox", err)
3465 log.Info("stored spammy mail in rejects mailbox")
3469 store.BroadcastChanges(a.d.acc, changes)
3473 log.Info("incoming message rejected", slog.String("reason", a0.reason), slog.Any("msgfrom", msgFrom))
3474 metricDelivery.WithLabelValues("reject", a0.reason).Inc()
3476 addError(rcpt, a0.code, a0.secode, a0.userError, a0.errmsg)
3480 delayFirstTime := true
3481 if rcpt.Account != nil && a0.dmarcReport != nil {
3483 if err := dmarcdb.AddReport(ctx, a0.dmarcReport, msgFrom.Domain); err != nil {
3484 log.Errorx("saving dmarc aggregate report in database", err)
3486 log.Info("dmarc aggregate report processed")
3487 a0.d.m.Flags.Seen = true
3488 delayFirstTime = false
3491 if rcpt.Account != nil && a0.tlsReport != nil {
3492 // todo future: add rate limiting to prevent DoS attacks.
3493 if err := tlsrptdb.AddReport(ctx, c.log, msgFrom.Domain, c.mailFrom.String(), a0.d.destination.HostTLSReports, a0.tlsReport); err != nil {
3494 log.Errorx("saving TLSRPT report in database", err)
3496 log.Info("tlsrpt report processed")
3497 a0.d.m.Flags.Seen = true
3498 delayFirstTime = false
3502 // If this is a first-time sender and not a forwarded/mailing list message, wait
3503 // before actually delivering. If this turns out to be a spammer, we've kept one of
3504 // their connections busy.
3505 a0conf, _ := a0.d.acc.Conf()
3506 if delayFirstTime && !a0.d.m.IsForward && !a0.d.m.IsMailingList && a0.reason == reasonNoBadSignals && !a0conf.NoFirstTimeSenderDelay && c.firstTimeSenderDelay > 0 {
3507 log.Debug("delaying before delivering from sender without reputation", slog.Duration("delay", c.firstTimeSenderDelay))
3508 mox.Sleep(mox.Context, c.firstTimeSenderDelay)
3512 code, timeout := mox.LocalserveNeedsError(rcpt.Addr.Localpart)
3514 log.Info("timing out due to special localpart")
3515 mox.Sleep(mox.Context, time.Hour)
3516 xsmtpServerErrorf(codes{smtp.C451LocalErr, smtp.SeOther00}, "timing out delivery due to special localpart")
3517 } else if code != 0 {
3518 log.Info("failure due to special localpart", slog.Int("code", code))
3519 metricDelivery.WithLabelValues("delivererror", "localserve").Inc()
3520 addError(rcpt, code, smtp.SeOther00, false, fmt.Sprintf("failure with code %d due to special localpart", code))
3525 // Gather the message-id before we deliver and the file may be consumed.
3526 if !parsedMessageID {
3527 if p, err := message.Parse(c.log.Logger, false, store.FileMsgReader(a0.d.m.MsgPrefix, dataFile)); err != nil {
3528 log.Infox("parsing message for message-id", err)
3529 } else if header, err := p.Header(); err != nil {
3530 log.Infox("parsing message header for message-id", err)
3532 messageID = header.Get("Message-Id")
3534 parsedMessageID = true
3537 // Finally deliver the message to the account(s).
3538 var nerr int // Number of non-quota errors.
3539 var nfull int // Number of failed deliveries due to over quota.
3540 var ndelivered int // Number delivered to account.
3541 for _, a := range la {
3542 // Don't deliver to recipient that was explicitly present in SMTP transaction, or
3543 // is sending the message to an alias they are member of.
3544 if rcpt.Alias != nil && (regularRecipient(a.d.deliverTo) || a.d.deliverTo.Equal(msgFrom.Path())) {
3549 a.d.acc.WithWLock(func() {
3550 if err := a.d.acc.DeliverMailbox(log, a.mailbox, a.d.m, dataFile); err != nil {
3551 log.Errorx("delivering", err)
3552 metricDelivery.WithLabelValues("delivererror", a0.reason).Inc()
3553 if errors.Is(err, store.ErrOverQuota) {
3556 addError(rcpt, smtp.C451LocalErr, smtp.SeSys3Other0, false, "error processing")
3563 metricDelivery.WithLabelValues("delivered", a0.reason).Inc()
3564 log.Info("incoming message delivered", slog.String("reason", a0.reason), slog.Any("msgfrom", msgFrom))
3566 conf, _ := a.d.acc.Conf()
3567 if conf.RejectsMailbox != "" && a.d.m.MessageID != "" {
3568 if err := a.d.acc.RejectsRemove(log, conf.RejectsMailbox, a.d.m.MessageID); err != nil {
3569 log.Errorx("removing message from rejects mailbox", err, slog.String("messageid", messageID))
3574 // Pass delivered messages to queue for DSN processing and/or hooks.
3576 mr := store.FileMsgReader(a.d.m.MsgPrefix, dataFile)
3577 part, err := a.d.m.LoadPart(mr)
3579 log.Errorx("loading parsed part for evaluating webhook", err)
3581 err = queue.Incoming(context.Background(), log, a.d.acc, messageID, *a.d.m, part, a.mailbox)
3582 log.Check(err, "queueing webhook for incoming delivery")
3584 } else if nerr > 0 && ndelivered == 0 {
3585 // Don't continue if we had an error and haven't delivered yet. If we only had
3586 // quota-related errors, we keep trying for an account to deliver to.
3590 if ndelivered == 0 && (nerr > 0 || nfull > 0) {
3592 addError(rcpt, smtp.C452StorageFull, smtp.SeMailbox2Full2, true, "account storage full")
3594 addError(rcpt, smtp.C451LocalErr, smtp.SeSys3Other0, false, "error processing")
3599 // For each recipient, do final spam analysis and delivery.
3600 for _, rcpt := range c.recipients {
3601 processRecipient(rcpt)
3604 // If all recipients failed to deliver, return an error.
3605 if len(c.recipients) == len(deliverErrors) {
3607 e0 := deliverErrors[0]
3608 var serverError bool
3611 for _, e := range deliverErrors {
3612 serverError = serverError || !e.userError
3613 if e.code != e0.code || e.secode != e0.secode {
3616 msgs = append(msgs, e.errmsg)
3622 xsmtpErrorf(e0.code, e0.secode, !serverError, "%s", strings.Join(msgs, "\n"))
3625 // Not all failures had the same error. We'll return each error on a separate line.
3627 for _, e := range deliverErrors {
3628 s := fmt.Sprintf("%d %d.%s %s", e.code, e.code/100, e.secode, e.errmsg)
3629 lines = append(lines, s)
3631 code := smtp.C451LocalErr
3632 secode := smtp.SeSys3Other0
3634 code = smtp.C554TransactionFailed
3636 lines = append(lines, "multiple errors")
3637 xsmtpErrorf(code, secode, !serverError, "%s", strings.Join(lines, "\n"))
3639 // Generate one DSN for all failed recipients.
3640 if len(deliverErrors) > 0 {
3642 dsnMsg := dsn.Message{
3643 SMTPUTF8: c.msgsmtputf8,
3644 From: smtp.Path{Localpart: "postmaster", IPDomain: deliverErrors[0].rcptTo.IPDomain},
3646 Subject: "mail delivery failure",
3647 MessageID: mox.MessageIDGen(false),
3648 References: messageID,
3650 // Per-message details.
3651 ReportingMTA: mox.Conf.Static.HostnameDomain.ASCII,
3652 ReceivedFromMTA: smtp.Ehlo{Name: c.hello, ConnIP: c.remoteIP},
3656 if len(deliverErrors) > 1 {
3657 dsnMsg.TextBody = "Multiple delivery failures occurred.\n\n"
3660 for _, e := range deliverErrors {
3662 if e.code/100 == 4 {
3665 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))
3666 rcpt := dsn.Recipient{
3667 FinalRecipient: e.rcptTo,
3669 Status: fmt.Sprintf("%d.%s", e.code/100, e.secode),
3670 LastAttemptDate: now,
3672 dsnMsg.Recipients = append(dsnMsg.Recipients, rcpt)
3675 header, err := message.ReadHeaders(bufio.NewReader(&moxio.AtReader{R: dataFile}))
3677 c.log.Errorx("reading headers of incoming message for dsn, continuing dsn without headers", err)
3679 dsnMsg.Original = header
3682 c.log.Error("not queueing dsn for incoming delivery due to localserve")
3683 } else if err := queueDSN(context.TODO(), c.log, c, *c.mailFrom, dsnMsg, c.requireTLS != nil && *c.requireTLS); err != nil {
3684 metricServerErrors.WithLabelValues("queuedsn").Inc()
3685 c.log.Errorx("queuing DSN for incoming delivery, no DSN sent", err)
3690 c.transactionBad-- // Compensate for early earlier pessimistic increase.
3692 c.writecodeline(smtp.C250Completed, smtp.SeMailbox2Other0, "it is done", nil)
3695// Return whether msgFrom address is allowed to send a message to alias.
3696func aliasAllowedMsgFrom(alias config.Alias, msgFrom smtp.Address) bool {
3697 for _, aa := range alias.ParsedAddresses {
3698 if aa.Address == msgFrom {
3702 lp, err := smtp.ParseLocalpart(alias.LocalpartStr)
3703 xcheckf(err, "parsing alias localpart")
3704 if msgFrom == smtp.NewAddress(lp, alias.Domain) {
3705 return alias.AllowMsgFrom
3707 return alias.PostPublic
3710// ecode returns either ecode, or a more specific error based on err.
3711// For example, ecode can be turned from an "other system" error into a "mail
3712// system full" if the error indicates no disk space is available.
3713func errCodes(code int, ecode string, err error) codes {
3715 case moxio.IsStorageSpace(err):
3717 case smtp.SeMailbox2Other0:
3718 if code == smtp.C451LocalErr {
3719 code = smtp.C452StorageFull
3721 ecode = smtp.SeMailbox2Full2
3722 case smtp.SeSys3Other0:
3723 if code == smtp.C451LocalErr {
3724 code = smtp.C452StorageFull
3726 ecode = smtp.SeSys3StorageFull1
3729 return codes{code, ecode}
3733func (c *conn) cmdRset(p *parser) {
3738 c.bwritecodeline(smtp.C250Completed, smtp.SeOther00, "all clear", nil)
3742func (c *conn) cmdVrfy(p *parser) {
3743 // No EHLO/HELO needed.
3754 // todo future: we could support vrfy and expn for submission? though would need to see if its rfc defines it.
3757 xsmtpUserErrorf(smtp.C252WithoutVrfy, smtp.SePol7Other0, "no verify but will try delivery")
3761func (c *conn) cmdExpn(p *parser) {
3762 // No EHLO/HELO needed.
3773 // todo: we could implement expn for local aliases for authenticated users, when members have permission to list. would anyone use it?
3776 xsmtpUserErrorf(smtp.C252WithoutVrfy, smtp.SePol7Other0, "no expand but will try delivery")
3780func (c *conn) cmdHelp(p *parser) {
3781 // Let's not strictly parse the request for help. We are ignoring the text anyway.
3784 c.bwritecodeline(smtp.C214Help, smtp.SeOther00, "see rfc 5321 (smtp)", nil)
3788func (c *conn) cmdNoop(p *parser) {
3789 // No idea why, but if an argument follows, it must adhere to the string ABNF production...
3796 c.bwritecodeline(smtp.C250Completed, smtp.SeOther00, "alrighty", nil)
3800func (c *conn) cmdQuit(p *parser) {
3804 c.writecodeline(smtp.C221Closing, smtp.SeOther00, "okay thanks bye", nil)