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 var noTLSClientAuth bool
210 if listener.TLS != nil {
211 tlsConfig = listener.TLS.Config
212 // For SMTP delivery, if we get a TLS handshake for an SNI hostname that we don't
213 // allow, we'll fallback to a certificate for the listener hostname instead of
214 // causing the connection to fail. May improve interoperability.
215 tlsConfigDelivery = listener.TLS.ConfigFallback
216 noTLSClientAuth = listener.TLS.ClientAuthDisabled
219 maxMsgSize := listener.SMTPMaxMessageSize
221 maxMsgSize = config.DefaultMaxMsgSize
224 if listener.SMTP.Enabled {
225 hostname := mox.Conf.Static.HostnameDomain
226 if listener.Hostname != "" {
227 hostname = listener.HostnameDomain
229 port := config.Port(listener.SMTP.Port, 25)
230 for _, ip := range listener.IPs {
231 firstTimeSenderDelay := durationDefault(listener.SMTP.FirstTimeSenderDelay, firstTimeSenderDelayDefault)
232 if tlsConfigDelivery != nil {
233 tlsConfigDelivery = tlsConfigDelivery.Clone()
234 // Default setting is currently to have session tickets disabled, to work around
235 // TLS interoperability issues with incoming deliveries from Microsoft. See
236 // https://github.com/golang/go/issues/70232.
237 tlsConfigDelivery.SessionTicketsDisabled = listener.SMTP.TLSSessionTicketsDisabled == nil || *listener.SMTP.TLSSessionTicketsDisabled
239 listen1("smtp", name, ip, port, hostname, tlsConfigDelivery, false, false, noTLSClientAuth, maxMsgSize, false, listener.SMTP.RequireSTARTTLS, !listener.SMTP.NoRequireTLS, listener.SMTP.DNSBLZones, firstTimeSenderDelay)
242 if listener.Submission.Enabled {
243 hostname := mox.Conf.Static.HostnameDomain
244 if listener.Hostname != "" {
245 hostname = listener.HostnameDomain
247 port := config.Port(listener.Submission.Port, 587)
248 for _, ip := range listener.IPs {
249 listen1("submission", name, ip, port, hostname, tlsConfig, true, false, noTLSClientAuth, maxMsgSize, !listener.Submission.NoRequireSTARTTLS, !listener.Submission.NoRequireSTARTTLS, true, nil, 0)
253 if listener.Submissions.Enabled {
254 hostname := mox.Conf.Static.HostnameDomain
255 if listener.Hostname != "" {
256 hostname = listener.HostnameDomain
258 port := config.Port(listener.Submissions.Port, 465)
259 for _, ip := range listener.IPs {
260 listen1("submissions", name, ip, port, hostname, tlsConfig, true, true, noTLSClientAuth, maxMsgSize, true, true, true, nil, 0)
268func listen1(protocol, name, ip string, port int, hostname dns.Domain, tlsConfig *tls.Config, submission, xtls, noTLSClientAuth bool, maxMessageSize int64, requireTLSForAuth, requireTLSForDelivery, requireTLS bool, dnsBLs []dns.Domain, firstTimeSenderDelay time.Duration) {
269 log := mlog.New("smtpserver", nil)
270 addr := net.JoinHostPort(ip, fmt.Sprintf("%d", port))
271 if os.Getuid() == 0 {
272 log.Print("listening for smtp",
273 slog.String("listener", name),
274 slog.String("address", addr),
275 slog.String("protocol", protocol))
277 network := mox.Network(ip)
278 ln, err := mox.Listen(network, addr)
280 log.Fatalx("smtp: listen for smtp", err, slog.String("protocol", protocol), slog.String("listener", name))
283 // Each listener gets its own copy of the config, so session keys between different
284 // ports on same listener aren't shared. We rotate session keys explicitly in this
285 // base TLS config because each connection clones the TLS config before using. The
286 // base TLS config would never get automatically managed/rotated session keys.
287 if tlsConfig != nil {
288 tlsConfig = tlsConfig.Clone()
289 mox.StartTLSSessionTicketKeyRefresher(mox.Shutdown, log, tlsConfig)
294 conn, err := ln.Accept()
296 log.Infox("smtp: accept", err, slog.String("protocol", protocol), slog.String("listener", name))
300 // Package is set on the resolver by the dkim/spf/dmarc/etc packages.
301 resolver := dns.StrictResolver{Log: log.Logger}
302 go serve(name, mox.Cid(), hostname, tlsConfig, conn, resolver, submission, xtls, false, noTLSClientAuth, maxMessageSize, requireTLSForAuth, requireTLSForDelivery, requireTLS, dnsBLs, firstTimeSenderDelay)
306 servers = append(servers, serve)
309// Serve starts serving on all listeners, launching a goroutine per listener.
311 for _, serve := range servers {
319 // OrigConn is the original (TCP) connection. We'll read from/write to conn, which
320 // can be wrapped in a tls.Server. We close origConn instead of conn because
321 // closing the TLS connection would send a TLS close notification, which may block
322 // for 5s if the server isn't reading it (because it is also sending it).
327 extRequireTLS bool // Whether to announce and allow the REQUIRETLS extension.
328 viaHTTPS bool // Whether the connection came in via the HTTPS port (using TLS ALPN).
330 resolver dns.Resolver
331 // The "x" in the readers and writes indicate Read and Write errors use panic to
332 // propagate the error.
335 xtr *moxio.TraceReader // Kept for changing trace level during cmd/auth/data.
336 xtw *moxio.TraceWriter
337 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.
338 lastlog time.Time // Used for printing the delta time since the previous logging for this connection.
340 baseTLSConfig *tls.Config
344 log mlog.Log // Used for all synchronous logging on this connection, see logbg for logging in a separate goroutine.
346 requireTLSForAuth bool
347 requireTLSForDelivery bool // If set, delivery is only allowed with TLS (STARTTLS), except if delivery is to a TLS reporting address.
348 cmd string // Current command.
349 cmdStart time.Time // Start of current command.
350 ncmds int // Number of commands processed. Used to abort connection when first incoming command is unknown/invalid.
352 firstTimeSenderDelay time.Duration
354 // If non-zero, taken into account during Read and Write. Set while processing DATA
355 // command, we don't want the entire delivery to take too long.
358 hello dns.IPDomain // Claimed remote name. Can be ip address for ehlo.
359 ehlo bool // If set, we had EHLO instead of HELO.
361 authFailed int // Number of failed auth attempts. For slowing down remote with many failures.
362 authSASL bool // Whether SASL authentication was done.
363 authTLS bool // Whether we did TLS client cert authentication.
364 username string // Only when authenticated.
365 account *store.Account // Only when authenticated.
367 // We track good/bad message transactions to disconnect spammers trying to guess addresses.
371 // Message transaction.
373 requireTLS *bool // MAIL FROM with REQUIRETLS set.
374 futureRelease time.Time // MAIL FROM with HOLDFOR or HOLDUNTIL.
375 futureReleaseRequest string // For use in DSNs, either "for;" or "until;" plus original value.
../rfc/4865:305
376 has8bitmime bool // If MAIL FROM parameter BODY=8BITMIME was sent. Required for SMTPUTF8.
377 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.
378 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.
379 recipients []recipient
382type rcptAccount struct {
384 Destination config.Destination
385 CanonicalAddress string // Optional catchall part stripped and/or lowercased.
388type rcptAlias struct {
390 CanonicalAddress string // Optional catchall part stripped and/or lowercased.
393type recipient struct {
396 // If account and alias are both not set, this is not for a local address. This is
397 // normal for submission, where messages are added to the queue. For incoming
398 // deliveries, this will result in an error.
399 Account *rcptAccount // If set, recipient address is for this local account.
400 Alias *rcptAlias // If set, for a local alias.
403func isClosed(err error) bool {
404 return errors.Is(err, errIO) || mlog.IsClosed(err)
407// Logbg returns a logger for logging in the background (in a goroutine), eg for
408// logging LoginAttempts. The regular c.log has a handler that evaluates fields on
409// the connection at time of logging, which may happen at the same time as
410// modifications to those fields.
411func (c *conn) logbg() mlog.Log {
412 log := mlog.New("smtpserver", nil).WithCid(c.cid)
413 if c.username != "" {
414 log = log.With(slog.String("username", c.username))
419// loginAttempt initializes a store.LoginAttempt, for adding to the store after
420// filling in the results and other details.
421func (c *conn) loginAttempt(useTLS bool, authMech string) store.LoginAttempt {
422 var state *tls.ConnectionState
423 if tc, ok := c.conn.(*tls.Conn); ok && useTLS {
424 v := tc.ConnectionState()
428 return store.LoginAttempt{
429 RemoteIP: c.remoteIP.String(),
430 LocalIP: c.localIP.String(),
431 TLS: store.LoginAttemptTLS(state),
432 Protocol: "submission",
434 Result: store.AuthError, // Replaced by caller.
438// makeTLSConfig makes a new tls config that is bound to the connection for
439// possible client certificate authentication in case of submission.
440func (c *conn) makeTLSConfig() *tls.Config {
441 if !c.submission || c.noTLSClientAuth {
442 return c.baseTLSConfig
445 // We clone the config so we can set VerifyPeerCertificate below to a method bound
446 // to this connection. Earlier, we set session keys explicitly on the base TLS
447 // config, so they can be used for this connection too.
448 tlsConf := c.baseTLSConfig.Clone()
450 // Allow client certificate authentication, for use with the sasl "external"
451 // authentication mechanism.
452 tlsConf.ClientAuth = tls.RequestClientCert
454 // We verify the client certificate during the handshake. The TLS handshake is
455 // initiated explicitly for incoming connections and during starttls, so we can
456 // immediately extract the account name and address used for authentication.
457 tlsConf.VerifyPeerCertificate = c.tlsClientAuthVerifyPeerCert
462// tlsClientAuthVerifyPeerCert can be used as tls.Config.VerifyPeerCertificate, and
463// sets authentication-related fields on conn. This is not called on resumed TLS
465func (c *conn) tlsClientAuthVerifyPeerCert(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
466 if len(rawCerts) == 0 {
470 // If we had too many authentication failures from this IP, don't attempt
471 // authentication. If this is a new incoming connetion, it is closed after the TLS
473 if !mox.LimiterFailedAuth.CanAdd(c.remoteIP, time.Now(), 1) {
477 cert, err := x509.ParseCertificate(rawCerts[0])
479 c.log.Debugx("parsing tls client certificate", err)
482 if err := c.tlsClientAuthVerifyPeerCertParsed(cert); err != nil {
483 c.log.Debugx("verifying tls client certificate", err)
484 return fmt.Errorf("verifying client certificate: %w", err)
489// tlsClientAuthVerifyPeerCertParsed verifies a client certificate. Called both for
490// fresh and resumed TLS connections.
491func (c *conn) tlsClientAuthVerifyPeerCertParsed(cert *x509.Certificate) error {
492 if c.account != nil {
493 return fmt.Errorf("cannot authenticate with tls client certificate after previous authentication")
496 la := c.loginAttempt(false, "tlsclientauth")
498 // Get TLS connection state in goroutine because we are called while performing the
499 // TLS handshake, which already has the tls connection locked.
500 conn := c.conn.(*tls.Conn)
501 logbg := c.logbg() // Evaluate attributes now, can't do it in goroutine.
504 // In case of panic don't take the whole program down.
507 c.log.Error("recover from panic", slog.Any("panic", x))
509 metrics.PanicInc(metrics.Smtpserver)
513 state := conn.ConnectionState()
514 la.TLS = store.LoginAttemptTLS(&state)
515 store.LoginAttemptAdd(context.Background(), logbg, la)
518 if la.Result == store.AuthSuccess {
519 mox.LimiterFailedAuth.Reset(c.remoteIP, time.Now())
521 mox.LimiterFailedAuth.Add(c.remoteIP, time.Now(), 1)
525 // For many failed auth attempts, slow down verification attempts.
526 if c.authFailed > 3 && authFailDelay > 0 {
527 mox.Sleep(mox.Context, time.Duration(c.authFailed-3)*authFailDelay)
529 c.authFailed++ // Compensated on success.
531 // On the 3rd failed authentication, start responding slowly. Successful auth will
532 // cause fast responses again.
533 if c.authFailed >= 3 {
538 shabuf := sha256.Sum256(cert.RawSubjectPublicKeyInfo)
539 fp := base64.RawURLEncoding.EncodeToString(shabuf[:])
540 la.TLSPubKeyFingerprint = fp
541 pubKey, err := store.TLSPublicKeyGet(context.TODO(), fp)
543 if err == bstore.ErrAbsent {
544 la.Result = store.AuthBadCredentials
546 return fmt.Errorf("looking up tls public key with fingerprint %s, subject %q, issuer %q: %v", fp, cert.Subject, cert.Issuer, err)
548 la.LoginAddress = pubKey.LoginAddress
550 // Verify account exists and still matches address. We don't check for account
551 // login being disabled if preauth is disabled. In that case, sasl external auth
552 // will be done before credentials can be used, and login disabled will be checked
553 // then, where it will result in a more helpful error message.
554 checkLoginDisabled := !pubKey.NoIMAPPreauth
555 acc, accName, _, err := store.OpenEmail(c.log, pubKey.LoginAddress, checkLoginDisabled)
556 la.AccountName = accName
558 if errors.Is(err, store.ErrLoginDisabled) {
559 la.Result = store.AuthLoginDisabled
561 return fmt.Errorf("opening account for address %s for public key %s: %w", pubKey.LoginAddress, fp, err)
566 c.log.Check(err, "close account")
569 la.AccountName = acc.Name
570 if acc.Name != pubKey.Account {
571 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)
576 acc = nil // Prevent cleanup by defer.
577 c.username = pubKey.LoginAddress
579 la.Result = store.AuthSuccess
580 c.log.Debug("tls client authenticated with client certificate",
581 slog.String("fingerprint", fp),
582 slog.String("username", c.username),
583 slog.String("account", c.account.Name),
584 slog.Any("remote", c.remoteIP))
588// xtlsHandshakeAndAuthenticate performs the TLS handshake, and verifies a client
589// certificate if present.
590func (c *conn) xtlsHandshakeAndAuthenticate(conn net.Conn) {
591 tlsConn := tls.Server(conn, c.makeTLSConfig())
594 cidctx := context.WithValue(mox.Context, mlog.CidKey, c.cid)
595 ctx, cancel := context.WithTimeout(cidctx, time.Minute)
597 c.log.Debug("starting tls server handshake")
599 metricDeliveryStarttls.Inc()
601 if err := tlsConn.HandshakeContext(ctx); err != nil {
603 // Errors from crypto/tls mostly aren't typed. We'll have to look for strings...
605 if errors.Is(err, io.EOF) {
607 } else if alert, ok := mox.AsTLSAlert(err); ok {
608 reason = tlsrpt.FormatAlert(alert)
611 if strings.Contains(s, "tls: client offered only unsupported versions") {
612 reason = "unsupportedversions"
613 } else if strings.Contains(s, "tls: first record does not look like a TLS handshake") {
615 } else if strings.Contains(s, "tls: unsupported SSLv2 handshake received") {
619 metricDeliveryStarttlsErrors.WithLabelValues(reason).Inc()
621 panic(fmt.Errorf("tls handshake: %s (%w)", err, errIO))
625 cs := tlsConn.ConnectionState()
626 if cs.DidResume && len(cs.PeerCertificates) > 0 && !c.noTLSClientAuth {
627 // Verify client after session resumption.
628 err := c.tlsClientAuthVerifyPeerCertParsed(cs.PeerCertificates[0])
630 panic(fmt.Errorf("tls verify client certificate after resumption: %s (%w)", err, errIO))
634 version, ciphersuite := moxio.TLSInfo(cs)
635 attrs := []slog.Attr{
636 slog.String("version", version),
637 slog.String("ciphersuite", ciphersuite),
638 slog.String("sni", cs.ServerName),
639 slog.Bool("resumed", cs.DidResume),
640 slog.Bool("notlsclientauth", c.noTLSClientAuth),
641 slog.Int("clientcerts", len(cs.PeerCertificates)),
643 if c.account != nil {
644 attrs = append(attrs,
645 slog.String("account", c.account.Name),
646 slog.String("username", c.username),
649 c.log.Debug("tls handshake completed", attrs...)
652// completely reset connection state as if greeting has just been sent.
654func (c *conn) reset() {
656 c.hello = dns.IPDomain{}
659 if c.account != nil {
660 err := c.account.Close()
661 c.log.Check(err, "closing account")
669// for rset command, and a few more cases that reset the mail transaction state.
671func (c *conn) rset() {
674 c.futureRelease = time.Time{}
675 c.futureReleaseRequest = ""
676 c.has8bitmime = false
678 c.msgsmtputf8 = false
682func (c *conn) earliestDeadline(d time.Duration) time.Time {
683 e := time.Now().Add(d)
684 if !c.deadline.IsZero() && c.deadline.Before(e) {
690func (c *conn) xcheckAuth() {
691 if c.submission && c.account == nil {
693 xsmtpUserErrorf(smtp.C530SecurityRequired, smtp.SePol7Other0, "authentication required")
697func (c *conn) xtrace(level slog.Level) func() {
699 c.xtr.SetTrace(level)
700 c.xtw.SetTrace(level)
703 c.xtr.SetTrace(mlog.LevelTrace)
704 c.xtw.SetTrace(mlog.LevelTrace)
708// setSlow marks the connection slow (or now), so reads are done with 3 second
709// delay for each read, and writes are done at 1 byte per second, to try to slow
711func (c *conn) setSlow(on bool) {
713 c.log.Debug("connection changed to slow")
714 } else if !on && c.slow {
715 c.log.Debug("connection restored to regular pace")
720// Write writes to the connection. It panics on i/o errors, which is handled by the
721// connection command loop.
722func (c *conn) Write(buf []byte) (int, error) {
728 // We set a single deadline for Write and Read. This may be a TLS connection.
729 // SetDeadline works on the underlying connection. If we wouldn't touch the read
730 // deadline, and only set the write deadline and do a bunch of writes, the TLS
731 // library would still have to do reads on the underlying connection, and may reach
732 // a read deadline that was set for some earlier read.
733 // We have one deadline for the whole write. In case of slow writing, we'll write
734 // the last chunk in one go, so remote smtp clients don't abort the connection for
736 deadline := c.earliestDeadline(30 * time.Second)
737 if err := c.conn.SetDeadline(deadline); err != nil {
738 c.log.Errorx("setting deadline for write", err)
743 nn, err := c.conn.Write(buf[:chunk])
745 panic(fmt.Errorf("write: %s (%w)", err, errIO))
749 if len(buf) > 0 && badClientDelay > 0 {
750 mox.Sleep(mox.Context, badClientDelay)
752 // Make sure we don't take too long, otherwise the remote SMTP client may close the
754 if time.Until(deadline) < 5*badClientDelay {
762// Read reads from the connection. It panics on i/o errors, which is handled by the
763// connection command loop.
764func (c *conn) Read(buf []byte) (int, error) {
765 if c.slow && badClientDelay > 0 {
766 mox.Sleep(mox.Context, badClientDelay)
770 // See comment about Deadline instead of individual read/write deadlines at Write.
771 if err := c.conn.SetDeadline(c.earliestDeadline(30 * time.Second)); err != nil {
772 c.log.Errorx("setting deadline for read", err)
775 n, err := c.conn.Read(buf)
777 panic(fmt.Errorf("read: %s (%w)", err, errIO))
782// Cache of line buffers for reading commands.
784var bufpool = moxio.NewBufpool(8, 2*1024)
786func (c *conn) xreadline() string {
787 line, err := bufpool.Readline(c.log, c.xbr)
788 if err != nil && errors.Is(err, moxio.ErrLineTooLong) {
789 c.xwritecodeline(smtp.C500BadSyntax, smtp.SeProto5Other0, "line too long, smtp max is 512, we reached 2048", nil)
790 panic(fmt.Errorf("%s (%w)", err, errIO))
791 } else if err != nil {
792 panic(fmt.Errorf("%s (%w)", err, errIO))
797// Buffered-write command response line to connection with codes and msg.
798// Err is not sent to remote but is used for logging and can be empty.
799func (c *conn) xbwritecodeline(code int, secode string, msg string, err error) {
802 ecode = fmt.Sprintf("%d.%s", code/100, secode)
804 metricCommands.WithLabelValues(c.kind(), c.cmd, fmt.Sprintf("%d", code), ecode).Observe(float64(time.Since(c.cmdStart)) / float64(time.Second))
805 c.log.Debugx("smtp command result", err,
806 slog.String("kind", c.kind()),
807 slog.String("cmd", c.cmd),
808 slog.Int("code", code),
809 slog.String("ecode", ecode),
810 slog.Duration("duration", time.Since(c.cmdStart)))
817 // Separate by newline and wrap long lines.
818 lines := strings.Split(msg, "\n")
819 for i, line := range lines {
821 var prelen = 3 + 1 + len(ecode) + len(sep)
822 for prelen+len(line) > 510 {
824 for ; e > 400 && line[e] != ' '; e-- {
826 // 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.
827 c.xbwritelinef("%d-%s%s%s", code, ecode, sep, line[:e])
831 if i < len(lines)-1 {
834 c.xbwritelinef("%d%s%s%s%s", code, spdash, ecode, sep, line)
838// Buffered-write a formatted response line to connection.
839func (c *conn) xbwritelinef(format string, args ...any) {
840 msg := fmt.Sprintf(format, args...)
841 fmt.Fprint(c.xbw, msg+"\r\n")
844// Flush pending buffered writes to connection.
845func (c *conn) xflush() {
846 c.xbw.Flush() // Errors will have caused a panic in Write.
849// Write (with flush) a response line with codes and message. err is not written, used for logging and can be nil.
850func (c *conn) xwritecodeline(code int, secode string, msg string, err error) {
851 c.xbwritecodeline(code, secode, msg, err)
855// Write (with flush) a formatted response line to connection.
856func (c *conn) xwritelinef(format string, args ...any) {
857 c.xbwritelinef(format, args...)
861var cleanClose struct{} // Sentinel value for panic/recover indicating clean close of connection.
863// ServeTLSConn serves a TLS connection.
864func ServeTLSConn(listenerName string, hostname dns.Domain, conn *tls.Conn, tlsConfig *tls.Config, submission, viaHTTPS bool, maxMsgSize int64, requireTLS bool) {
865 log := mlog.New("smtpserver", nil)
866 resolver := dns.StrictResolver{Log: log.Logger}
867 serve(listenerName, mox.Cid(), hostname, tlsConfig, conn, resolver, submission, true, viaHTTPS, true, maxMsgSize, true, true, requireTLS, nil, 0)
870func serve(listenerName string, cid int64, hostname dns.Domain, tlsConfig *tls.Config, nc net.Conn, resolver dns.Resolver, submission, xtls, viaHTTPS, noTLSClientAuth bool, maxMessageSize int64, requireTLSForAuth, requireTLSForDelivery, requireTLS bool, dnsBLs []dns.Domain, firstTimeSenderDelay time.Duration) {
871 var localIP, remoteIP net.IP
872 if a, ok := nc.LocalAddr().(*net.TCPAddr); ok {
875 // For net.Pipe, during tests.
876 localIP = net.ParseIP("127.0.0.10")
878 if a, ok := nc.RemoteAddr().(*net.TCPAddr); ok {
881 // For net.Pipe, during tests.
882 remoteIP = net.ParseIP("127.0.0.10")
887 origConn = nc.(*tls.Conn).NetConn()
894 submission: submission,
897 noTLSClientAuth: noTLSClientAuth,
898 extRequireTLS: requireTLS,
901 baseTLSConfig: tlsConfig,
905 maxMessageSize: maxMessageSize,
906 requireTLSForAuth: requireTLSForAuth,
907 requireTLSForDelivery: requireTLSForDelivery,
909 firstTimeSenderDelay: firstTimeSenderDelay,
911 var logmutex sync.Mutex
912 // Also see (and possibly update) c.logbg, for logging in a goroutine.
913 c.log = mlog.New("smtpserver", nil).WithFunc(func() []slog.Attr {
915 defer logmutex.Unlock()
918 slog.Int64("cid", c.cid),
919 slog.Duration("delta", now.Sub(c.lastlog)),
922 if c.username != "" {
923 l = append(l, slog.String("username", c.username))
927 c.xtr = moxio.NewTraceReader(c.log, "RC: ", c)
928 c.xbr = bufio.NewReader(c.xtr)
929 c.xtw = moxio.NewTraceWriter(c.log, "LS: ", c)
930 c.xbw = bufio.NewWriter(c.xtw)
932 metricConnection.WithLabelValues(c.kind()).Inc()
933 c.log.Info("new connection",
934 slog.Any("remote", c.conn.RemoteAddr()),
935 slog.Any("local", c.conn.LocalAddr()),
936 slog.Bool("submission", submission),
937 slog.Bool("tls", xtls),
938 slog.Bool("viahttps", viaHTTPS),
939 slog.String("listener", listenerName))
942 err := c.origConn.Close() // Close actual TCP socket, regardless of TLS on top.
943 c.log.Check(err, "closing tcp connection")
944 c.conn.Close() // If TLS, will try to write alert notification to already closed socket, returning error quickly.
946 if c.account != nil {
947 err := c.account.Close()
948 c.log.Check(err, "closing account")
953 if x == nil || x == cleanClose {
954 c.log.Info("connection closed")
955 } else if err, ok := x.(error); ok && isClosed(err) {
956 c.log.Infox("connection closed", err)
958 c.log.Error("unhandled panic", slog.Any("err", x))
960 metrics.PanicInc(metrics.Smtpserver)
964 if xtls && !viaHTTPS {
965 // Start TLS on connection. We perform the handshake explicitly, so we can set a
966 // timeout, do client certificate authentication, log TLS details afterwards.
967 c.xtlsHandshakeAndAuthenticate(c.conn)
971 case <-mox.Shutdown.Done():
973 c.xwritecodeline(smtp.C421ServiceUnavail, smtp.SeSys3NotAccepting2, "shutting down", nil)
978 if !limiterConnectionRate.Add(c.remoteIP, time.Now(), 1) {
979 c.xwritecodeline(smtp.C421ServiceUnavail, smtp.SePol7Other0, "connection rate from your ip or network too high, slow down please", nil)
983 // If remote IP/network resulted in too many authentication failures, refuse to serve.
984 if submission && !mox.LimiterFailedAuth.CanAdd(c.remoteIP, time.Now(), 1) {
985 metrics.AuthenticationRatelimitedInc("submission")
986 c.log.Debug("refusing connection due to many auth failures", slog.Any("remoteip", c.remoteIP))
987 c.xwritecodeline(smtp.C421ServiceUnavail, smtp.SePol7Other0, "too many auth failures", nil)
991 if !limiterConnections.Add(c.remoteIP, time.Now(), 1) {
992 c.log.Debug("refusing connection due to many open connections", slog.Any("remoteip", c.remoteIP))
993 c.xwritecodeline(smtp.C421ServiceUnavail, smtp.SePol7Other0, "too many open connections from your ip or network", nil)
996 defer limiterConnections.Add(c.remoteIP, time.Now(), -1)
998 // We register and unregister the original connection, in case c.conn is replaced
999 // with a TLS connection later on.
1000 mox.Connections.Register(nc, "smtp", listenerName)
1001 defer mox.Connections.Unregister(nc)
1005 // We include the string ESMTP. https://cr.yp.to/smtp/greeting.html recommends it.
1006 // Should not be too relevant nowadays, but does not hurt and default blackbox
1007 // exporter SMTP health check expects it.
1008 c.xwritelinef("%d %s ESMTP mox", smtp.C220ServiceReady, c.hostname.ASCII)
1013 // If another command is present, don't flush our buffered response yet. Holding
1014 // off will cause us to respond with a single packet.
1015 n := c.xbr.Buffered()
1017 buf, err := c.xbr.Peek(n)
1018 if err == nil && bytes.IndexByte(buf, '\n') >= 0 {
1026var commands = map[string]func(c *conn, p *parser){
1027 "helo": (*conn).cmdHelo,
1028 "ehlo": (*conn).cmdEhlo,
1029 "starttls": (*conn).cmdStarttls,
1030 "auth": (*conn).cmdAuth,
1031 "mail": (*conn).cmdMail,
1032 "rcpt": (*conn).cmdRcpt,
1033 "data": (*conn).cmdData,
1034 "rset": (*conn).cmdRset,
1035 "vrfy": (*conn).cmdVrfy,
1036 "expn": (*conn).cmdExpn,
1037 "help": (*conn).cmdHelp,
1038 "noop": (*conn).cmdNoop,
1039 "quit": (*conn).cmdQuit,
1042func command(c *conn) {
1048 err, ok := x.(error)
1058 if errors.As(err, &serr) {
1059 c.xwritecodeline(serr.code, serr.secode, fmt.Sprintf("%s (%s)", serr.errmsg, mox.ReceivedID(c.cid)), serr.err)
1060 if serr.printStack {
1061 c.log.Errorx("smtp error", serr.err, slog.Int("code", serr.code), slog.String("secode", serr.secode))
1065 // Other type of panic, we pass it on, aborting the connection.
1066 c.log.Errorx("command panic", err)
1071 // todo future: we could wait for either a line or shutdown, and just close the connection on shutdown.
1073 line := c.xreadline()
1074 t := strings.SplitN(line, " ", 2)
1080 cmdl := strings.ToLower(cmd)
1082 // 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
1085 case <-mox.Shutdown.Done():
1087 c.xwritecodeline(smtp.C421ServiceUnavail, smtp.SeSys3NotAccepting2, "shutting down", nil)
1093 c.cmdStart = time.Now()
1095 p := newParser(args, c.smtputf8, c)
1096 fn, ok := commands[cmdl]
1100 // Other side is likely speaking something else than SMTP, send error message and
1101 // stop processing because there is a good chance whatever they sent has multiple
1103 c.xwritecodeline(smtp.C500BadSyntax, smtp.SeProto5Syntax2, "please try again speaking smtp", nil)
1107 xsmtpUserErrorf(smtp.C500BadSyntax, smtp.SeProto5BadCmdOrSeq1, "unknown command")
1113// For use in metric labels.
1114func (c *conn) kind() string {
1121func (c *conn) xneedHello() {
1122 if c.hello.IsZero() {
1123 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "no ehlo/helo yet")
1127// If smtp server is configured to require TLS for all mail delivery (except to TLS
1128// reporting address), abort command.
1129func (c *conn) xneedTLSForDelivery(rcpt smtp.Path) {
1130 // For TLS reports, we allow the message in even without TLS, because there may be
1132 if c.requireTLSForDelivery && !c.tls && !isTLSReportRecipient(rcpt) {
1134 xsmtpUserErrorf(smtp.C530SecurityRequired, smtp.SePol7Other0, "STARTTLS required for mail delivery")
1138func isTLSReportRecipient(rcpt smtp.Path) bool {
1139 _, _, _, dest, err := mox.LookupAddress(rcpt.Localpart, rcpt.IPDomain.Domain, false, false, false)
1140 return err == nil && (dest.HostTLSReports || dest.DomainTLSReports)
1143func (c *conn) cmdHelo(p *parser) {
1144 c.cmdHello(p, false)
1147func (c *conn) cmdEhlo(p *parser) {
1152func (c *conn) cmdHello(p *parser, ehlo bool) {
1153 var remote dns.IPDomain
1154 if c.submission && !mox.Pedantic {
1155 // Mail clients regularly put bogus information in the hostname/ip. For submission,
1156 // the value is of no use, so there is not much point in annoying the user with
1157 // errors they cannot fix themselves. Except when in pedantic mode.
1158 remote = dns.IPDomain{IP: c.remoteIP}
1162 remote = p.xipdomain(true)
1164 remote = dns.IPDomain{Domain: p.xdomain()}
1166 // Verify a remote domain name has an A or AAAA record, CNAME not allowed.
../rfc/5321:722
1167 cidctx := context.WithValue(mox.Context, mlog.CidKey, c.cid)
1168 ctx, cancel := context.WithTimeout(cidctx, time.Minute)
1169 _, _, err := c.resolver.LookupIPAddr(ctx, remote.Domain.ASCII+".")
1171 if dns.IsNotFound(err) {
1172 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeProto5Other0, "your ehlo domain does not resolve to an IP address")
1174 // For success or temporary resolve errors, we'll just continue.
1177 // Though a few paragraphs earlier is a claim additional data can occur for address
1178 // literals (IP addresses), although the ABNF in that document does not allow it.
1179 // We allow additional text, but only if space-separated.
1180 if len(remote.IP) > 0 && p.space() {
1192 // https://www.iana.org/assignments/mail-parameters/mail-parameters.xhtml
1194 c.xbwritelinef("250-%s", c.hostname.ASCII)
1198 if !c.tls && c.baseTLSConfig != nil {
1200 c.xbwritelinef("250-STARTTLS")
1201 } else if c.extRequireTLS {
1204 c.xbwritelinef("250-REQUIRETLS")
1209 if c.tls || !c.requireTLSForAuth {
1210 // We always mention the SCRAM PLUS variants, even if TLS is not active: It is a
1211 // hint to the client that a TLS connection can use TLS channel binding during
1212 // authentication. The client should select the bare variant when TLS isn't
1213 // present, and also not indicate the server supports the PLUS variant in that
1214 // case, or it would trigger the mechanism downgrade detection.
1215 mechs = "SCRAM-SHA-256-PLUS SCRAM-SHA-256 SCRAM-SHA-1-PLUS SCRAM-SHA-1 CRAM-MD5 PLAIN LOGIN"
1217 if c.tls && len(c.conn.(*tls.Conn).ConnectionState().PeerCertificates) > 0 && !c.viaHTTPS && !c.noTLSClientAuth {
1218 mechs = "EXTERNAL " + mechs
1220 c.xbwritelinef("250-AUTH %s", mechs)
1223 c.xbwritelinef("250-FUTURERELEASE %d %s", queue.FutureReleaseIntervalMax/time.Second, t.Format(time.RFC3339))
1226 // todo future? c.writelinef("250-DSN")
1234func (c *conn) cmdStarttls(p *parser) {
1240 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "already speaking tls")
1242 if c.account != nil {
1243 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "cannot starttls after authentication")
1245 if c.baseTLSConfig == nil {
1246 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "starttls not offered")
1249 // We don't want to do TLS on top of c.r because it also prints protocol traces: We
1250 // don't want to log the TLS stream. So we'll do TLS on the underlying connection,
1251 // but make sure any bytes already read and in the buffer are used for the TLS
1254 if n := c.xbr.Buffered(); n > 0 {
1255 conn = &moxio.PrefixConn{
1256 PrefixReader: io.LimitReader(c.xbr, int64(n)),
1261 // We add the cid to the output, to help debugging in case of a failing TLS connection.
1262 c.xwritecodeline(smtp.C220ServiceReady, smtp.SeOther00, "go! ("+mox.ReceivedID(c.cid)+")", nil)
1264 c.xtlsHandshakeAndAuthenticate(conn)
1271func (c *conn) cmdAuth(p *parser) {
1275 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "authentication only allowed on submission ports")
1279 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "already authenticated")
1281 if c.mailFrom != nil {
1283 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "authentication not allowed during mail transaction")
1286 // If authentication fails due to missing derived secrets, we don't hold it against
1287 // the connection. There is no way to indicate server support for an authentication
1288 // mechanism, but that a mechanism won't work for an account.
1289 var missingDerivedSecrets bool
1291 // For many failed auth attempts, slow down verification attempts.
1292 // Dropping the connection could also work, but more so when we have a connection rate limiter.
1294 if c.authFailed > 3 && authFailDelay > 0 {
1296 mox.Sleep(mox.Context, time.Duration(c.authFailed-3)*authFailDelay)
1298 c.authFailed++ // Compensated on success.
1300 if missingDerivedSecrets {
1303 // On the 3rd failed authentication, start responding slowly. Successful auth will
1304 // cause fast responses again.
1305 if c.authFailed >= 3 {
1310 la := c.loginAttempt(true, "")
1312 store.LoginAttemptAdd(context.Background(), c.logbg(), la)
1313 if la.Result == store.AuthSuccess {
1314 mox.LimiterFailedAuth.Reset(c.remoteIP, time.Now())
1315 } else if !missingDerivedSecrets {
1316 mox.LimiterFailedAuth.Add(c.remoteIP, time.Now(), 1)
1322 mech := p.xsaslMech()
1324 // Read the first parameter, either as initial parameter or by sending a
1325 // continuation with the optional encChal (must already be base64-encoded).
1326 xreadInitial := func(encChal string) []byte {
1330 // todo future: handle max length of 12288 octets and return proper responde codes otherwise
../rfc/4954:253
1331 auth = c.xreadline()
1334 la.Result = store.AuthAborted
1335 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5Other0, "authentication aborted")
1340 // Windows Mail 16005.14326.21606.0 sends two spaces between "AUTH PLAIN" and the
1345 auth = p.remainder()
1348 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5Syntax2, "missing initial auth base64 parameter after space")
1349 } else if auth == "=" {
1351 auth = "" // Base64 decode below will result in empty buffer.
1354 buf, err := base64.StdEncoding.DecodeString(auth)
1357 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5Syntax2, "invalid base64: %s", err)
1362 xreadContinuation := func() []byte {
1363 line := c.xreadline()
1365 la.Result = store.AuthAborted
1366 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5Other0, "authentication aborted")
1368 buf, err := base64.StdEncoding.DecodeString(line)
1371 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5Syntax2, "invalid base64: %s", err)
1376 // The various authentication mechanisms set account and username. We may already
1377 // have an account and username from TLS client authentication. Afterwards, we
1378 // check that the account is the same.
1379 var account *store.Account
1383 err := account.Close()
1384 c.log.Check(err, "close account")
1390 la.AuthMech = "plain"
1394 if !c.tls && c.requireTLSForAuth {
1395 xsmtpUserErrorf(smtp.C538EncReqForAuth, smtp.SePol7EncReqForAuth11, "authentication requires tls")
1398 // Password is in line in plain text, so hide it.
1399 defer c.xtrace(mlog.LevelTraceauth)()
1400 buf := xreadInitial("")
1401 c.xtrace(mlog.LevelTrace) // Restore.
1402 plain := bytes.Split(buf, []byte{0})
1403 if len(plain) != 3 {
1404 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5BadParams4, "auth data should have 3 nul-separated tokens, got %d", len(plain))
1406 authz := norm.NFC.String(string(plain[0]))
1407 username = norm.NFC.String(string(plain[1]))
1408 la.LoginAddress = username
1409 password := string(plain[2])
1411 if authz != "" && authz != username {
1412 la.Result = store.AuthBadCredentials
1413 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "cannot assume other role")
1417 account, la.AccountName, err = store.OpenEmailAuth(c.log, username, password, false)
1418 if err != nil && errors.Is(err, store.ErrUnknownCredentials) {
1420 la.Result = store.AuthBadCredentials
1421 c.log.Info("failed authentication attempt", slog.String("username", username), slog.Any("remote", c.remoteIP))
1422 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass")
1424 xcheckf(err, "verifying credentials")
1427 // LOGIN is obsoleted in favor of PLAIN, only implemented to support legacy
1428 // clients, see Internet-Draft (I-D):
1429 // https://datatracker.ietf.org/doc/html/draft-murchison-sasl-login-00
1431 la.LoginAddress = "login"
1435 if !c.tls && c.requireTLSForAuth {
1436 xsmtpUserErrorf(smtp.C538EncReqForAuth, smtp.SePol7EncReqForAuth11, "authentication requires tls")
1439 // Read user name. The I-D says the client should ignore the server challenge, but
1440 // also that some clients may require challenge "Username:" instead of "User
1441 // Name". We can't sent both... Servers most commonly return "Username:" and
1442 // "Password:", so we do the same.
1443 // I-D says maximum length must be 64 bytes. We allow more, for long user names
1445 encChal := base64.StdEncoding.EncodeToString([]byte("Username:"))
1446 username = string(xreadInitial(encChal))
1447 username = norm.NFC.String(username)
1448 la.LoginAddress = username
1450 // Again, client should ignore the challenge, we send the same as the example in
1452 c.xwritelinef("%d %s", smtp.C334ContinueAuth, base64.StdEncoding.EncodeToString([]byte("Password:")))
1454 // Password is in line in plain text, so hide it.
1455 defer c.xtrace(mlog.LevelTraceauth)()
1456 password := string(xreadContinuation())
1457 c.xtrace(mlog.LevelTrace) // Restore.
1460 account, la.AccountName, err = store.OpenEmailAuth(c.log, username, password, false)
1461 if err != nil && errors.Is(err, store.ErrUnknownCredentials) {
1463 la.Result = store.AuthBadCredentials
1464 c.log.Info("failed authentication attempt", slog.String("username", username), slog.Any("remote", c.remoteIP))
1465 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass")
1467 xcheckf(err, "verifying credentials")
1470 la.AuthMech = strings.ToLower(mech)
1475 chal := fmt.Sprintf("<%d.%d@%s>", uint64(mox.CryptoRandInt()), time.Now().UnixNano(), mox.Conf.Static.HostnameDomain.ASCII)
1476 c.xwritelinef("%d %s", smtp.C334ContinueAuth, base64.StdEncoding.EncodeToString([]byte(chal)))
1478 resp := xreadContinuation()
1479 t := strings.Split(string(resp), " ")
1480 if len(t) != 2 || len(t[1]) != 2*md5.Size {
1481 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5BadParams4, "malformed cram-md5 response")
1483 username = norm.NFC.String(t[0])
1484 la.LoginAddress = username
1485 c.log.Debug("cram-md5 auth", slog.String("username", username))
1487 account, la.AccountName, _, err = store.OpenEmail(c.log, username, false)
1488 if err != nil && errors.Is(err, store.ErrUnknownCredentials) {
1489 la.Result = store.AuthBadCredentials
1490 c.log.Info("failed authentication attempt", slog.String("username", username), slog.Any("remote", c.remoteIP))
1491 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass")
1493 xcheckf(err, "looking up address")
1494 la.AccountName = account.Name
1495 var ipadhash, opadhash hash.Hash
1496 account.WithRLock(func() {
1497 err := account.DB.Read(context.TODO(), func(tx *bstore.Tx) error {
1498 password, err := bstore.QueryTx[store.Password](tx).Get()
1499 if err == bstore.ErrAbsent {
1500 c.log.Info("failed authentication attempt", slog.String("username", username), slog.Any("remote", c.remoteIP))
1501 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass")
1507 ipadhash = password.CRAMMD5.Ipad
1508 opadhash = password.CRAMMD5.Opad
1511 xcheckf(err, "tx read")
1513 if ipadhash == nil || opadhash == nil {
1514 missingDerivedSecrets = true
1515 c.log.Info("cram-md5 auth attempt without derived secrets set, save password again to store secrets", slog.String("username", username))
1516 c.log.Info("failed authentication attempt", slog.String("username", username), slog.Any("remote", c.remoteIP))
1517 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass")
1521 ipadhash.Write([]byte(chal))
1522 opadhash.Write(ipadhash.Sum(nil))
1523 digest := fmt.Sprintf("%x", opadhash.Sum(nil))
1525 c.log.Info("failed authentication attempt", slog.String("username", username), slog.Any("remote", c.remoteIP))
1526 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass")
1529 case "SCRAM-SHA-256-PLUS", "SCRAM-SHA-256", "SCRAM-SHA-1-PLUS", "SCRAM-SHA-1":
1530 // 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?
1531 // todo: use single implementation between ../imapserver/server.go and ../smtpserver/server.go
1533 // Passwords cannot be retrieved or replayed from the trace.
1535 la.AuthMech = strings.ToLower(mech)
1536 var h func() hash.Hash
1537 switch la.AuthMech {
1538 case "scram-sha-1", "scram-sha-1-plus":
1540 case "scram-sha-256", "scram-sha-256-plus":
1543 xsmtpServerErrorf(codes{smtp.C554TransactionFailed, smtp.SeSys3Other0}, "missing scram auth method case")
1546 var cs *tls.ConnectionState
1547 channelBindingRequired := strings.HasSuffix(la.AuthMech, "-plus")
1548 if channelBindingRequired && !c.tls {
1550 xsmtpUserErrorf(smtp.C538EncReqForAuth, smtp.SePol7EncReqForAuth11, "scram plus mechanism requires tls connection")
1553 xcs := c.conn.(*tls.Conn).ConnectionState()
1556 c0 := xreadInitial("")
1557 ss, err := scram.NewServer(h, c0, cs, channelBindingRequired)
1559 c.log.Infox("scram protocol error", err, slog.Any("remote", c.remoteIP))
1560 xsmtpUserErrorf(smtp.C455BadParams, smtp.SePol7Other0, "scram protocol error: %s", err)
1562 username = ss.Authentication
1563 la.LoginAddress = username
1564 c.log.Debug("scram auth", slog.String("authentication", username))
1565 account, la.AccountName, _, err = store.OpenEmail(c.log, username, false)
1567 // todo: we could continue scram with a generated salt, deterministically generated
1568 // from the username. that way we don't have to store anything but attackers cannot
1569 // learn if an account exists. same for absent scram saltedpassword below.
1570 c.log.Info("failed authentication attempt", slog.String("username", username), slog.Any("remote", c.remoteIP))
1571 xsmtpUserErrorf(smtp.C454TempAuthFail, smtp.SeSys3Other0, "scram not possible")
1573 if ss.Authorization != "" && ss.Authorization != username {
1574 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "authentication with authorization for different user not supported")
1576 var xscram store.SCRAM
1577 account.WithRLock(func() {
1578 err := account.DB.Read(context.TODO(), func(tx *bstore.Tx) error {
1579 password, err := bstore.QueryTx[store.Password](tx).Get()
1580 if err == bstore.ErrAbsent {
1581 c.log.Info("failed authentication attempt", slog.String("username", username), slog.Any("remote", c.remoteIP))
1582 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass")
1584 xcheckf(err, "fetching credentials")
1585 switch la.AuthMech {
1586 case "scram-sha-1", "scram-sha-1-plus":
1587 xscram = password.SCRAMSHA1
1588 case "scram-sha-256", "scram-sha-256-plus":
1589 xscram = password.SCRAMSHA256
1591 xsmtpServerErrorf(codes{smtp.C554TransactionFailed, smtp.SeSys3Other0}, "missing scram auth credentials case")
1593 if len(xscram.Salt) == 0 || xscram.Iterations == 0 || len(xscram.SaltedPassword) == 0 {
1594 missingDerivedSecrets = true
1595 c.log.Info("scram auth attempt without derived secrets set, save password again to store secrets", slog.String("address", username))
1596 c.log.Info("failed authentication attempt", slog.String("username", username), slog.Any("remote", c.remoteIP))
1597 xsmtpUserErrorf(smtp.C454TempAuthFail, smtp.SeSys3Other0, "scram not possible")
1601 xcheckf(err, "read tx")
1603 s1, err := ss.ServerFirst(xscram.Iterations, xscram.Salt)
1604 xcheckf(err, "scram first server step")
1605 c.xwritelinef("%d %s", smtp.C334ContinueAuth, base64.StdEncoding.EncodeToString([]byte(s1))) //
../rfc/4954:187
1606 c2 := xreadContinuation()
1607 s3, err := ss.Finish(c2, xscram.SaltedPassword)
1609 c.xwritelinef("%d %s", smtp.C334ContinueAuth, base64.StdEncoding.EncodeToString([]byte(s3))) //
../rfc/4954:187
1612 c.xreadline() // Should be "*" for cancellation.
1613 if errors.Is(err, scram.ErrInvalidProof) {
1614 la.Result = store.AuthBadCredentials
1615 c.log.Info("failed authentication attempt", slog.String("username", username), slog.Any("remote", c.remoteIP))
1616 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad credentials")
1617 } else if errors.Is(err, scram.ErrChannelBindingsDontMatch) {
1618 la.Result = store.AuthBadChannelBinding
1619 c.log.Warn("bad channel binding during authentication, potential mitm", slog.String("username", username), slog.Any("remote", c.remoteIP))
1620 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7MsgIntegrity7, "channel bindings do not match, potential mitm")
1621 } else if errors.Is(err, scram.ErrInvalidEncoding) {
1622 la.Result = store.AuthBadProtocol
1623 c.log.Infox("bad scram protocol message", err, slog.String("username", username), slog.Any("remote", c.remoteIP))
1624 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7Other0, "bad scram protocol message")
1626 xcheckf(err, "server final")
1630 // The message should be empty. todo: should we require it is empty?
1634 la.AuthMech = "external"
1637 buf := xreadInitial("")
1638 username = norm.NFC.String(string(buf))
1639 la.LoginAddress = username
1643 xsmtpUserErrorf(smtp.C538EncReqForAuth, smtp.SePol7EncReqForAuth11, "tls required for tls client certificate authentication")
1645 if c.account == nil {
1646 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "missing client certificate, required for tls client certificate authentication")
1650 username = c.username
1651 la.LoginAddress = username
1654 account, la.AccountName, _, err = store.OpenEmail(c.log, username, false)
1655 xcheckf(err, "looking up username from tls client authentication")
1658 la.AuthMech = "(unrecognized)"
1660 xsmtpUserErrorf(smtp.C504ParamNotImpl, smtp.SeProto5BadParams4, "mechanism %s not supported", mech)
1663 if accConf, ok := account.Conf(); !ok {
1664 xcheckf(errors.New("cannot find account"), "get account config")
1665 } else if accConf.LoginDisabled != "" {
1666 la.Result = store.AuthLoginDisabled
1667 c.log.Info("account login disabled", slog.String("username", username))
1668 xsmtpUserErrorf(smtp.C525AccountDisabled, smtp.SePol7AccountDisabled13, "%w: %s", store.ErrLoginDisabled, accConf.LoginDisabled)
1671 // We may already have TLS credentials. We allow an additional SASL authentication,
1672 // possibly with different username, but the account must be the same.
1673 if c.account != nil {
1674 if account != c.account {
1675 c.log.Debug("sasl authentication for different account than tls client authentication, aborting connection",
1676 slog.String("saslmechanism", la.AuthMech),
1677 slog.String("saslaccount", account.Name),
1678 slog.String("tlsaccount", c.account.Name),
1679 slog.String("saslusername", username),
1680 slog.String("tlsusername", c.username),
1682 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "authentication failed, tls client certificate public key belongs to another account")
1683 } else if username != c.username {
1684 c.log.Debug("sasl authentication for different username than tls client certificate authentication, switching to sasl username",
1685 slog.String("saslmechanism", la.AuthMech),
1686 slog.String("saslusername", username),
1687 slog.String("tlsusername", c.username),
1688 slog.String("account", c.account.Name),
1693 account = nil // Prevent cleanup.
1695 c.username = username
1697 la.LoginAddress = c.username
1698 la.AccountName = c.account.Name
1699 la.Result = store.AuthSuccess
1704 c.xwritecodeline(smtp.C235AuthSuccess, smtp.SePol7Other0, "nice", nil)
1708func (c *conn) cmdMail(p *parser) {
1709 // requirements for maximum line length:
1711 // todo future: enforce? doesn't really seem worth it...
1713 if c.transactionBad > 10 && c.transactionGood == 0 {
1714 // If we get many bad transactions, it's probably a spammer that is guessing user names.
1715 // Useful in combination with rate limiting.
1717 c.xwritecodeline(smtp.C550MailboxUnavail, smtp.SeAddr1Other0, "too many failures", nil)
1723 if c.mailFrom != nil {
1725 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "already have MAIL")
1727 // Ensure clear transaction state on failure.
1738 // Microsoft Outlook 365 Apps for Enterprise sends it with submission. For delivery
1739 // it is mostly used by spammers, but has been seen with legitimate senders too.
1743 rawRevPath := p.xrawReversePath()
1744 paramSeen := map[string]bool{}
1747 key := p.xparamKeyword()
1749 K := strings.ToUpper(key)
1752 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5BadParams4, "duplicate param %q", key)
1760 if size > c.maxMessageSize {
1762 ecode := smtp.SeSys3MsgLimitExceeded4
1763 if size < config.DefaultMaxMsgSize {
1764 ecode = smtp.SeMailbox2MsgLimitExceeded3
1766 xsmtpUserErrorf(smtp.C552MailboxFull, ecode, "message too large")
1768 // We won't verify the message is exactly the size the remote claims. Buf if it is
1769 // larger, we'll abort the transaction when remote crosses the boundary.
1773 v := p.xparamValue()
1774 switch strings.ToUpper(v) {
1776 c.has8bitmime = false
1778 c.has8bitmime = true
1780 xsmtpUserErrorf(smtp.C555UnrecognizedAddrParams, smtp.SeProto5BadParams4, "unrecognized parameter %q", key)
1785 // We act as if we don't trust the client to specify a mailbox. Instead, we always
1786 // check the rfc5321.mailfrom and rfc5322.from before accepting the submission.
1790 // 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
1798 c.msgsmtputf8 = true
1802 xsmtpUserErrorf(smtp.C523EncryptionNeeded, smtp.SePol7EncNeeded10, "requiretls only allowed on tls-encrypted connections")
1803 } else if !c.extRequireTLS {
1804 xsmtpUserErrorf(smtp.C555UnrecognizedAddrParams, smtp.SeSys3NotSupported3, "REQUIRETLS not allowed for this connection")
1808 case "HOLDFOR", "HOLDUNTIL":
1811 xsmtpUserErrorf(smtp.C555UnrecognizedAddrParams, smtp.SeSys3NotSupported3, "unrecognized parameter %q", key)
1813 if K == "HOLDFOR" && paramSeen["HOLDUNTIL"] || K == "HOLDUNTIL" && paramSeen["HOLDFOR"] {
1815 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5BadParams4, "cannot use both HOLDUNTIL and HOLFOR")
1819 // semantic errors as syntax errors
1822 if n > int64(queue.FutureReleaseIntervalMax/time.Second) {
1824 xsmtpUserErrorf(smtp.C554TransactionFailed, smtp.SeProto5BadParams4, "future release interval too far in the future")
1826 c.futureRelease = time.Now().Add(time.Duration(n) * time.Second)
1827 c.futureReleaseRequest = fmt.Sprintf("for;%d", n)
1829 t, s := p.xdatetimeutc()
1830 ival := time.Until(t)
1832 // Likely a mistake by the user.
1833 xsmtpUserErrorf(smtp.C554TransactionFailed, smtp.SeProto5BadParams4, "requested future release time is in the past")
1834 } else if ival > queue.FutureReleaseIntervalMax {
1836 xsmtpUserErrorf(smtp.C554TransactionFailed, smtp.SeProto5BadParams4, "requested future release time is too far in the future")
1839 c.futureReleaseRequest = "until;" + s
1843 xsmtpUserErrorf(smtp.C555UnrecognizedAddrParams, smtp.SeSys3NotSupported3, "unrecognized parameter %q", key)
1847 // We now know if we have to parse the address with support for utf8.
1848 pp := newParser(rawRevPath, c.smtputf8, c)
1849 rpath := pp.xbareReversePath()
1854 // For submission, check if reverse path is allowed. I.e. authenticated account
1855 // must have the rpath configured. We do a check again on rfc5322.from during DATA.
1856 // Mail clients may use the alias address as smtp mail from address, so we allow it
1857 // for such aliases.
1858 rpathAllowed := func(disabled *bool) bool {
1864 from := smtp.NewAddress(rpath.Localpart, rpath.IPDomain.Domain)
1865 ok, dis := mox.AllowMsgFrom(c.account.Name, from)
1870 if !c.submission && !rpath.IPDomain.Domain.IsZero() {
1871 // If rpath domain has null MX record or is otherwise not accepting email, reject.
1874 cidctx := context.WithValue(mox.Context, mlog.CidKey, c.cid)
1875 ctx, cancel := context.WithTimeout(cidctx, time.Minute)
1876 valid, err := checkMXRecords(ctx, c.resolver, rpath.IPDomain.Domain)
1879 c.log.Infox("temporary reject for temporary mx lookup error", err)
1880 xsmtpServerErrorf(codes{smtp.C451LocalErr, smtp.SeNet4Other0}, "cannot verify mx records for mailfrom domain")
1881 } else if !valid && !(Localserve && rpath.IPDomain.Domain.ASCII == "localhost") {
1882 // We don't reject for "localhost" in Localserve mode because we only resolve
1883 // through DNS, not an /etc/hosts file, and localhost may not resolve through DNS,
1884 // depending on network environment.
1886 c.log.Info("permanent reject because mailfrom domain does not accept mail")
1887 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SePol7SenderHasNullMX27, "mailfrom domain not configured for mail")
1892 if c.submission && (len(rpath.IPDomain.IP) > 0 || !rpathAllowed(&disabled)) {
1894 c.log.Info("submission with smtp mail from of disabled domain", slog.Any("domain", rpath.IPDomain.Domain))
1895 xsmtpServerErrorf(codes{smtp.C451LocalErr, smtp.SeSys3Other0}, "domain of smtp mail from is temporarily disabled")
1899 c.log.Info("submission with unconfigured mailfrom", slog.String("user", c.username), slog.String("mailfrom", rpath.String()))
1900 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SePol7DeliveryUnauth1, "must match authenticated user")
1901 } else if !c.submission && len(rpath.IPDomain.IP) > 0 {
1902 // todo future: allow if the IP is the same as this connection is coming from? does later code allow this?
1903 c.log.Info("delivery from address without domain", slog.String("mailfrom", rpath.String()))
1904 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SePol7Other0, "domain name required")
1907 if Localserve && strings.HasPrefix(string(rpath.Localpart), "mailfrom") {
1908 c.xlocalserveError(rpath.Localpart)
1913 c.xbwritecodeline(smtp.C250Completed, smtp.SeAddr1Other0, "looking good", nil)
1917func (c *conn) cmdRcpt(p *parser) {
1920 if c.mailFrom == nil {
1922 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "missing MAIL FROM")
1928 // Microsoft Outlook 365 Apps for Enterprise sends it with submission. For delivery
1929 // it is mostly used by spammers, but has been seen with legitimate senders too.
1934 if p.take("<POSTMASTER>") {
1935 fpath = smtp.Path{Localpart: "postmaster"}
1937 fpath = p.xforwardPath()
1941 key := p.xparamKeyword()
1942 // K := strings.ToUpper(key)
1945 xsmtpUserErrorf(smtp.C555UnrecognizedAddrParams, smtp.SeSys3NotSupported3, "unrecognized parameter %q", key)
1949 // Check if TLS is enabled if required. It's not great that sender/recipient
1950 // addresses may have been exposed in plaintext before we can reject delivery. The
1951 // recipient could be the tls reporting addresses, which must always be able to
1952 // receive in plain text.
1953 c.xneedTLSForDelivery(fpath)
1955 // todo future: for submission, should we do explicit verification that domains are fully qualified? also for mail from.
../rfc/6409:420
1957 if len(c.recipients) >= rcptToLimit {
1959 xsmtpUserErrorf(smtp.C452StorageFull, smtp.SeProto5TooManyRcpts3, "max of %d recipients reached", rcptToLimit)
1962 // We don't want to allow delivery to multiple recipients with a null reverse path.
1963 // Why would anyone send like that? Null reverse path is intended for delivery
1964 // notifications, they should go to a single recipient.
1965 if !c.submission && len(c.recipients) > 0 && c.mailFrom.IsZero() {
1966 xsmtpUserErrorf(smtp.C452StorageFull, smtp.SeProto5TooManyRcpts3, "only one recipient allowed with null reverse address")
1969 // Do not accept multiple recipients if remote does not pass SPF. Because we don't
1970 // want to generate DSNs to unverified domains. This is the moment we
1971 // can refuse individual recipients, DATA will be too late. Because mail
1972 // servers must handle a max recipient limit gracefully and still send to the
1973 // recipients that are accepted, this should not cause problems. Though we are in
1974 // violation because the limit must be >= 100.
1978 if !c.submission && len(c.recipients) == 1 && !Localserve {
1979 // note: because of check above, mailFrom cannot be the null address.
1981 d := c.mailFrom.IPDomain.Domain
1983 // todo: use this spf result for DATA.
1984 spfArgs := spf.Args{
1985 RemoteIP: c.remoteIP,
1986 MailFromLocalpart: c.mailFrom.Localpart,
1988 HelloDomain: c.hello,
1990 LocalHostname: c.hostname,
1992 cidctx := context.WithValue(mox.Context, mlog.CidKey, c.cid)
1993 spfctx, spfcancel := context.WithTimeout(cidctx, time.Minute)
1995 receivedSPF, _, _, _, err := spf.Verify(spfctx, c.log.Logger, c.resolver, spfArgs)
1998 c.log.Errorx("spf verify for multiple recipients", err)
2000 pass = receivedSPF.Identity == spf.ReceivedMailFrom && receivedSPF.Result == spf.StatusPass
2003 xsmtpUserErrorf(smtp.C452StorageFull, smtp.SeProto5TooManyRcpts3, "only one recipient allowed without spf pass")
2007 if Localserve && strings.HasPrefix(string(fpath.Localpart), "rcptto") {
2008 c.xlocalserveError(fpath.Localpart)
2011 if len(fpath.IPDomain.IP) > 0 {
2013 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeAddr1UnknownDestMailbox1, "not accepting email for ip")
2015 c.recipients = append(c.recipients, recipient{fpath, nil, nil})
2016 } else if accountName, alias, canonical, dest, err := mox.LookupAddress(fpath.Localpart, fpath.IPDomain.Domain, true, true, true); err == nil {
2019 c.recipients = append(c.recipients, recipient{fpath, nil, &rcptAlias{*alias, canonical}})
2020 } else if dest.SMTPError != "" {
2021 xsmtpServerErrorf(codes{dest.SMTPErrorCode, dest.SMTPErrorSecode}, "%s", dest.SMTPErrorMsg)
2023 c.recipients = append(c.recipients, recipient{fpath, &rcptAccount{accountName, dest, canonical}, nil})
2026 } else if Localserve {
2027 // If the address isn't known, and we are in localserve, deliver to the mox user.
2028 // If account or destination doesn't exist, it will be handled during delivery. For
2029 // submissions, which is the common case, we'll deliver to the logged in user,
2030 // which is typically the mox user.
2031 acc, _ := mox.Conf.Account("mox")
2032 dest := acc.Destinations["mox@localhost"]
2033 c.recipients = append(c.recipients, recipient{fpath, &rcptAccount{"mox", dest, "mox@localhost"}, nil})
2034 } else if errors.Is(err, mox.ErrDomainDisabled) {
2035 c.log.Info("smtp recipient for temporarily disabled domain", slog.Any("domain", fpath.IPDomain.Domain))
2036 xsmtpUserErrorf(smtp.C450MailboxUnavail, smtp.SeMailbox2Disabled1, "recipient domain temporarily disabled")
2037 } else if errors.Is(err, mox.ErrDomainNotFound) {
2039 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeAddr1UnknownDestMailbox1, "not accepting email for domain")
2041 // We'll be delivering this email.
2042 c.recipients = append(c.recipients, recipient{fpath, nil, nil})
2043 } else if errors.Is(err, mox.ErrAddressNotFound) {
2045 // For submission, we're transparent about which user exists. Should be fine for the typical small-scale deploy.
2047 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeAddr1UnknownDestMailbox1, "no such user")
2049 // We pretend to accept. We don't want to let remote know the user does not exist
2050 // until after DATA. Because then remote has committed to sending a message.
2051 // note: not local for !c.submission is the signal this address is in error.
2052 c.recipients = append(c.recipients, recipient{fpath, nil, nil})
2054 c.log.Errorx("looking up account for delivery", err, slog.Any("rcptto", fpath))
2055 xsmtpServerErrorf(codes{smtp.C451LocalErr, smtp.SeSys3Other0}, "error processing")
2057 c.xbwritecodeline(smtp.C250Completed, smtp.SeAddr1Other0, "now on the list", nil)
2060func hasNonASCII(s string) bool {
2061 for _, c := range []byte(s) {
2062 if c > unicode.MaxASCII {
2070func (c *conn) isSMTPUTF8Required(part *message.Part) bool {
2071 // Check "MAIL FROM".
2072 if hasNonASCII(string(c.mailFrom.Localpart)) {
2075 // Check all "RCPT TO".
2076 for _, rcpt := range c.recipients {
2077 if hasNonASCII(string(rcpt.Addr.Localpart)) {
2082 // Check header in all message parts.
2083 smtputf8, err := part.NeedsSMTPUTF8()
2084 xcheckf(err, "checking if smtputf8 is required")
2089func (c *conn) cmdData(p *parser) {
2092 if c.mailFrom == nil {
2094 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "missing MAIL FROM")
2096 if len(c.recipients) == 0 {
2098 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "missing RCPT TO")
2104 // 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.
2106 // Entire delivery should be done within 30 minutes, or we abort.
2107 cidctx := context.WithValue(mox.Context, mlog.CidKey, c.cid)
2108 cmdctx, cmdcancel := context.WithTimeout(cidctx, 30*time.Minute)
2110 // Deadline is taken into account by Read and Write.
2111 c.deadline, _ = cmdctx.Deadline()
2113 c.deadline = time.Time{}
2117 c.xwritelinef("354 see you at the bare dot")
2119 // Mark as tracedata.
2120 defer c.xtrace(mlog.LevelTracedata)()
2122 // We read the data into a temporary file. We limit the size and do basic analysis while reading.
2123 dataFile, err := store.CreateMessageTemp(c.log, "smtp-deliver")
2125 xsmtpServerErrorf(errCodes(smtp.C451LocalErr, smtp.SeSys3Other0, err), "creating temporary file for message: %s", err)
2127 defer store.CloseRemoveTempFile(c.log, dataFile, "smtpserver delivered message")
2128 msgWriter := message.NewWriter(dataFile)
2129 dr := smtp.NewDataReader(c.xbr)
2130 n, err := io.Copy(&limitWriter{maxSize: c.maxMessageSize, w: msgWriter}, dr)
2131 c.xtrace(mlog.LevelTrace) // Restore.
2133 if errors.Is(err, errMessageTooLarge) {
2135 ecode := smtp.SeSys3MsgLimitExceeded4
2136 if n < config.DefaultMaxMsgSize {
2137 ecode = smtp.SeMailbox2MsgLimitExceeded3
2139 c.xwritecodeline(smtp.C451LocalErr, ecode, fmt.Sprintf("error copying data to file (%s)", mox.ReceivedID(c.cid)), err)
2140 panic(fmt.Errorf("remote sent too much DATA: %w", errIO))
2143 if errors.Is(err, smtp.ErrCRLF) {
2144 c.xwritecodeline(smtp.C500BadSyntax, smtp.SeProto5Syntax2, fmt.Sprintf("invalid bare \\r or \\n, may be smtp smuggling (%s)", mox.ReceivedID(c.cid)), err)
2148 // Something is failing on our side. We want to let remote know. So write an error response,
2149 // then discard the remaining data so the remote client is more likely to see our
2150 // response. Our write is synchronous, there is a risk no window/buffer space is
2151 // available and our write blocks us from reading remaining data, leading to
2152 // deadlock. We have a timeout on our connection writes though, so worst case we'll
2153 // abort the connection due to expiration.
2154 c.xwritecodeline(smtp.C451LocalErr, smtp.SeSys3Other0, fmt.Sprintf("error copying data to file (%s)", mox.ReceivedID(c.cid)), err)
2155 io.Copy(io.Discard, dr)
2159 // Basic sanity checks on messages before we send them out to the world. Just
2160 // trying to be strict in what we do to others and liberal in what we accept.
2162 if !msgWriter.HaveBody {
2164 xsmtpUserErrorf(smtp.C554TransactionFailed, smtp.SeMsg6Other0, "message requires both header and body section")
2166 // Check only for pedantic mode because ios mail will attempt to send smtputf8 with
2167 // non-ascii in message from localpart without using 8bitmime.
2168 if mox.Pedantic && msgWriter.Has8bit && !c.has8bitmime {
2170 xsmtpUserErrorf(smtp.C500BadSyntax, smtp.SeMsg6Other0, "message with non-us-ascii requires 8bitmime extension")
2174 if Localserve && mox.Pedantic {
2175 // Require that message can be parsed fully.
2176 p, err := message.Parse(c.log.Logger, false, dataFile)
2178 err = p.Walk(c.log.Logger, nil)
2182 xsmtpUserErrorf(smtp.C554TransactionFailed, smtp.SeMsg6Other0, "malformed message: %v", err)
2186 // Now that we have all the whole message (envelope + data), we can check if the SMTPUTF8 extension is required.
2187 var part *message.Part
2188 if c.smtputf8 || c.submission || mox.Pedantic {
2189 // Try to parse the message.
2190 // Do nothing if something bad happen during Parse and Walk, just keep the current value for c.msgsmtputf8.
2191 p, err := message.Parse(c.log.Logger, true, dataFile)
2193 // Message parsed without error. Keep the result to avoid parsing the message again.
2195 err = part.Walk(c.log.Logger, nil)
2197 c.msgsmtputf8 = c.isSMTPUTF8Required(part)
2201 c.log.Debugx("parsing message for smtputf8 check", err)
2203 if c.smtputf8 != c.msgsmtputf8 {
2204 c.log.Debug("smtputf8 flag changed", slog.Bool("smtputf8", c.smtputf8), slog.Bool("msgsmtputf8", c.msgsmtputf8))
2207 if !c.smtputf8 && c.msgsmtputf8 && mox.Pedantic {
2208 metricSubmission.WithLabelValues("missingsmtputf8").Inc()
2209 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeMsg6Other0, "smtputf8 extension is required but was not added to the MAIL command")
2212 // Prepare "Received" header.
2216 var iprevStatus iprev.Status // Only for delivery, not submission.
2217 var iprevAuthentic bool
2219 // Hide internal hosts.
2220 // todo future: make this a config option, where admins specify ip ranges that they don't want exposed. also see
../rfc/5321:4321
2221 recvFrom = message.HeaderCommentDomain(mox.Conf.Static.HostnameDomain, c.msgsmtputf8)
2223 if len(c.hello.IP) > 0 {
2224 recvFrom = smtp.AddressLiteral(c.hello.IP)
2226 // ASCII-only version added after the extended-domain syntax below, because the
2227 // comment belongs to "BY" which comes immediately after "FROM".
2228 recvFrom = c.hello.Domain.XName(c.msgsmtputf8)
2230 iprevctx, iprevcancel := context.WithTimeout(cmdctx, time.Minute)
2232 var revNames []string
2233 iprevStatus, revName, revNames, iprevAuthentic, err = iprev.Lookup(iprevctx, c.resolver, c.remoteIP)
2236 c.log.Infox("reverse-forward lookup", err, slog.Any("remoteip", c.remoteIP))
2238 c.log.Debug("dns iprev check", slog.Any("addr", c.remoteIP), slog.Any("status", iprevStatus))
2242 } else if len(revNames) > 0 {
2245 name = strings.TrimSuffix(name, ".")
2247 if name != "" && name != c.hello.Domain.XName(c.msgsmtputf8) {
2248 recvFrom += name + " "
2250 recvFrom += smtp.AddressLiteral(c.remoteIP) + ")"
2251 if c.msgsmtputf8 && c.hello.Domain.Unicode != "" {
2252 recvFrom += " (" + c.hello.Domain.ASCII + ")"
2255 recvBy := mox.Conf.Static.HostnameDomain.XName(c.msgsmtputf8)
2256 recvBy += " (" + smtp.AddressLiteral(c.localIP) + ")" // todo: hide ip if internal?
2257 if c.msgsmtputf8 && mox.Conf.Static.HostnameDomain.Unicode != "" {
2258 // This syntax is part of "VIA".
2259 recvBy += " (" + mox.Conf.Static.HostnameDomain.ASCII + ")"
2272 if c.account != nil {
2277 // Assume transaction does not succeed. If it does, we'll compensate.
2280 recvHdrFor := func(rcptTo string) string {
2281 recvHdr := &message.HeaderWriter{}
2282 // For additional Received-header clauses, see:
2283 // https://www.iana.org/assignments/mail-parameters/mail-parameters.xhtml#table-mail-parameters-8
2285 if c.requireTLS != nil && *c.requireTLS {
2287 withComment = " (requiretls)"
2289 recvHdr.Add(" ", "Received:", "from", recvFrom, "by", recvBy, "via", "tcp", "with", with+withComment, "id", mox.ReceivedID(c.cid)) //
../rfc/5321:3158
2291 tlsConn := c.conn.(*tls.Conn)
2292 tlsComment := mox.TLSReceivedComment(c.log, tlsConn.ConnectionState())
2293 recvHdr.Add(" ", tlsComment...)
2295 // We leave out an empty "for" clause. This is empty for messages submitted to
2296 // multiple recipients, so the message stays identical and a single smtp
2297 // transaction can deliver, only transferring the data once.
2299 recvHdr.Add(" ", "for", "<"+rcptTo+">;")
2301 recvHdr.Add(" ", time.Now().Format(message.RFC5322Z))
2302 return recvHdr.String()
2305 // Submission is easiest because user is trusted. Far fewer checks to make. So
2306 // handle it first, and leave the rest of the function for handling wild west
2307 // internet traffic.
2309 c.submit(cmdctx, recvHdrFor, msgWriter, dataFile, part)
2311 c.deliver(cmdctx, recvHdrFor, msgWriter, iprevStatus, iprevAuthentic, dataFile)
2315// Check if a message has unambiguous "TLS-Required: No" header. Messages must not
2316// contain multiple TLS-Required headers. The only valid value is "no". But we'll
2317// accept multiple headers as long as all they are all "no".
2319func hasTLSRequiredNo(h textproto.MIMEHeader) bool {
2320 l := h.Values("Tls-Required")
2324 for _, v := range l {
2325 if !strings.EqualFold(v, "no") {
2332// submit is used for mail from authenticated users that we will try to deliver.
2333func (c *conn) submit(ctx context.Context, recvHdrFor func(string) string, msgWriter *message.Writer, dataFile *os.File, part *message.Part) {
2334 // Similar between ../smtpserver/server.go:/submit\( and ../webmail/api.go:/MessageSubmit\( and ../webapisrv/server.go:/Send\(
2336 var msgPrefix []byte
2338 // Check that user is only sending email as one of its configured identities. Not
2342 msgFrom, _, header, err := message.From(c.log.Logger, true, dataFile, part)
2344 metricSubmission.WithLabelValues("badmessage").Inc()
2345 c.log.Infox("parsing message From address", err, slog.String("user", c.username))
2346 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeMsg6Other0, "cannot parse header or From address: %v", err)
2348 if ok, disabled := mox.AllowMsgFrom(c.account.Name, msgFrom); disabled {
2349 c.log.Info("submission with message from address of disabled domain", slog.Any("domain", msgFrom.Domain))
2350 xsmtpServerErrorf(codes{smtp.C451LocalErr, smtp.SeSys3Other0}, "domain of message from header is temporarily disabled")
2353 metricSubmission.WithLabelValues("badfrom").Inc()
2354 c.log.Infox("verifying message from address", mox.ErrAddressNotFound, slog.String("user", c.username), slog.Any("msgfrom", msgFrom))
2355 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SePol7DeliveryUnauth1, "message from address must belong to authenticated user")
2358 // TLS-Required: No header makes us not enforce recipient domain's TLS policy.
2361 if c.requireTLS == nil && hasTLSRequiredNo(header) {
2366 // Outgoing messages should not have a Return-Path header. The final receiving mail
2367 // server will add it.
2369 if mox.Pedantic && header.Values("Return-Path") != nil {
2370 metricSubmission.WithLabelValues("badheader").Inc()
2371 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeMsg6Other0, "message should not have Return-Path header")
2374 // Add Message-Id header if missing.
2376 messageID := header.Get("Message-Id")
2377 if messageID == "" {
2378 messageID = mox.MessageIDGen(c.msgsmtputf8)
2379 msgPrefix = append(msgPrefix, fmt.Sprintf("Message-Id: <%s>\r\n", messageID)...)
2383 if header.Get("Date") == "" {
2384 msgPrefix = append(msgPrefix, "Date: "+time.Now().Format(message.RFC5322Z)+"\r\n"...)
2387 // Check outgoing message rate limit.
2388 err = c.account.DB.Read(ctx, func(tx *bstore.Tx) error {
2389 rcpts := make([]smtp.Path, len(c.recipients))
2390 for i, r := range c.recipients {
2393 msglimit, rcptlimit, err := c.account.SendLimitReached(tx, rcpts)
2394 xcheckf(err, "checking sender limit")
2396 metricSubmission.WithLabelValues("messagelimiterror").Inc()
2397 xsmtpUserErrorf(smtp.C451LocalErr, smtp.SePol7DeliveryUnauth1, "max number of messages (%d) over past 24h reached, try increasing per-account setting MaxOutgoingMessagesPerDay", msglimit)
2398 } else if rcptlimit >= 0 {
2399 metricSubmission.WithLabelValues("recipientlimiterror").Inc()
2400 xsmtpUserErrorf(smtp.C451LocalErr, smtp.SePol7DeliveryUnauth1, "max number of new/first-time recipients (%d) over past 24h reached, try increasing per-account setting MaxFirstTimeRecipientsPerDay", rcptlimit)
2404 xcheckf(err, "read-only transaction")
2406 // We gather any X-Mox-Extra-* headers into the "extra" data during queueing, which
2407 // will make it into any webhook we deliver.
2408 // todo: remove the X-Mox-Extra-* headers from the message. we don't currently rewrite the message...
2409 // todo: should we not canonicalize keys?
2410 var extra map[string]string
2411 for k, vl := range header {
2412 if !strings.HasPrefix(k, "X-Mox-Extra-") {
2416 extra = map[string]string{}
2418 xk := k[len("X-Mox-Extra-"):]
2419 // We don't allow duplicate keys.
2420 if _, ok := extra[xk]; ok || len(vl) > 1 {
2421 xsmtpUserErrorf(smtp.C554TransactionFailed, smtp.SeMsg6Other0, "duplicate x-mox-extra- key %q", xk)
2423 extra[xk] = vl[len(vl)-1]
2426 // 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.
2428 // Add DKIM signatures.
2429 confDom, ok := mox.Conf.Domain(msgFrom.Domain)
2431 c.log.Error("domain disappeared", slog.Any("domain", msgFrom.Domain))
2432 xsmtpServerErrorf(codes{smtp.C451LocalErr, smtp.SeSys3Other0}, "internal error")
2433 } else if confDom.Disabled {
2434 c.log.Info("submission with message from address of disabled domain", slog.Any("domain", msgFrom.Domain))
2435 xsmtpServerErrorf(codes{smtp.C451LocalErr, smtp.SeSys3Other0}, "domain of message from header is temporarily disabled")
2438 selectors := mox.DKIMSelectors(confDom.DKIM)
2439 if len(selectors) > 0 {
2440 canonical := mox.CanonicalLocalpart(msgFrom.Localpart, confDom)
2441 if dkimHeaders, err := dkim.Sign(ctx, c.log.Logger, canonical, msgFrom.Domain, selectors, c.msgsmtputf8, store.FileMsgReader(msgPrefix, dataFile)); err != nil {
2442 c.log.Errorx("dkim sign for domain", err, slog.Any("domain", msgFrom.Domain))
2443 metricServerErrors.WithLabelValues("dkimsign").Inc()
2445 msgPrefix = append(msgPrefix, []byte(dkimHeaders)...)
2449 authResults := message.AuthResults{
2450 Hostname: mox.Conf.Static.HostnameDomain.XName(c.msgsmtputf8),
2451 Comment: mox.Conf.Static.HostnameDomain.ASCIIExtra(c.msgsmtputf8),
2452 Methods: []message.AuthMethod{
2456 Props: []message.AuthProp{
2457 message.MakeAuthProp("smtp", "mailfrom", c.mailFrom.XString(c.msgsmtputf8), true, c.mailFrom.ASCIIExtra(c.msgsmtputf8)),
2462 msgPrefix = append(msgPrefix, []byte(authResults.Header())...)
2464 // We always deliver through the queue. It would be more efficient to deliver
2465 // directly for local accounts, but we don't want to circumvent all the anti-spam
2466 // measures. Accounts on a single mox instance should be allowed to block each
2469 accConf, _ := c.account.Conf()
2470 loginAddr, err := smtp.ParseAddress(c.username)
2471 xcheckf(err, "parsing login address")
2472 useFromID := slices.Contains(accConf.ParsedFromIDLoginAddresses, loginAddr)
2473 var localpartBase string
2477 // With submission, user can bring their own fromid.
2478 t := strings.SplitN(string(c.mailFrom.Localpart), confDom.LocalpartCatchallSeparatorsEffective[0], 2)
2479 localpartBase = t[0]
2482 if fromID != "" && len(c.recipients) > 1 {
2483 xsmtpServerErrorf(codes{smtp.C554TransactionFailed, smtp.SeProto5TooManyRcpts3}, "cannot send to multiple recipients with chosen fromid")
2490 qml := make([]queue.Msg, len(c.recipients))
2491 for i, rcpt := range c.recipients {
2493 code, timeout := mox.LocalserveNeedsError(rcpt.Addr.Localpart)
2495 c.log.Info("timing out submission due to special localpart")
2496 mox.Sleep(mox.Context, time.Hour)
2497 xsmtpServerErrorf(codes{smtp.C451LocalErr, smtp.SeSys3Other0}, "timing out submission due to special localpart")
2498 } else if code != 0 {
2499 c.log.Info("failure due to special localpart", slog.Int("code", code))
2500 xsmtpServerErrorf(codes{code, smtp.SeOther00}, "failure with code %d due to special localpart", code)
2507 fromID = xrandomID(16)
2509 fp.Localpart = smtp.Localpart(localpartBase + confDom.LocalpartCatchallSeparatorsEffective[0] + fromID)
2512 // For multiple recipients, we don't make each message prefix unique, leaving out
2513 // the "for" clause in the Received header. This allows the queue to deliver the
2514 // messages in a single smtp transaction.
2516 if len(c.recipients) == 1 {
2517 rcptTo = rcpt.Addr.String()
2519 xmsgPrefix := append([]byte(recvHdrFor(rcptTo)), msgPrefix...)
2520 msgSize := int64(len(xmsgPrefix)) + msgWriter.Size
2521 qm := queue.MakeMsg(fp, rcpt.Addr, msgWriter.Has8bit, c.msgsmtputf8, msgSize, messageID, xmsgPrefix, c.requireTLS, now, header.Get("Subject"))
2522 if !c.futureRelease.IsZero() {
2523 qm.NextAttempt = c.futureRelease
2524 qm.FutureReleaseRequest = c.futureReleaseRequest
2531 // 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
2532 if err := queue.Add(ctx, c.log, c.account.Name, dataFile, qml...); err != nil && errors.Is(err, queue.ErrFromID) && !genFromID {
2533 // todo: should we return this error during the "rcpt to" command?
2534 // secode is not an exact match, but seems closest.
2535 xsmtpServerErrorf(errCodes(smtp.C554TransactionFailed, smtp.SeAddr1SenderSyntax7, err), "bad fromid in smtp mail from address: %s", err)
2536 } else if err != nil {
2537 // Aborting the transaction is not great. But continuing and generating DSNs will
2538 // probably result in errors as well...
2539 metricSubmission.WithLabelValues("queueerror").Inc()
2540 c.log.Errorx("queuing message", err)
2541 xsmtpServerErrorf(errCodes(smtp.C451LocalErr, smtp.SeSys3Other0, err), "error delivering message: %v", err)
2543 metricSubmission.WithLabelValues("ok").Inc()
2544 for i, rcpt := range c.recipients {
2545 c.log.Info("messages queued for delivery",
2546 slog.Any("mailfrom", *c.mailFrom),
2547 slog.Any("rcptto", rcpt.Addr),
2548 slog.Bool("smtputf8", c.smtputf8),
2549 slog.Bool("msgsmtputf8", c.msgsmtputf8),
2550 slog.Int64("msgsize", qml[i].Size))
2553 err = c.account.DB.Write(ctx, func(tx *bstore.Tx) error {
2554 for _, rcpt := range c.recipients {
2555 outgoing := store.Outgoing{Recipient: rcpt.Addr.XString(true)}
2556 if err := tx.Insert(&outgoing); err != nil {
2557 return fmt.Errorf("adding outgoing message: %v", err)
2562 xcheckf(err, "adding outgoing messages")
2565 c.transactionBad-- // Compensate for early earlier pessimistic increase.
2568 c.xwritecodeline(smtp.C250Completed, smtp.SeMailbox2Other0, "it is done", nil)
2571func xrandomID(n int) string {
2572 return base64.RawURLEncoding.EncodeToString(xrandom(n))
2575func xrandom(n int) []byte {
2576 buf := make([]byte, n)
2577 x, err := cryptorand.Read(buf)
2578 xcheckf(err, "read random")
2580 xcheckf(errors.New("short random read"), "read random")
2585func ipmasked(ip net.IP) (string, string, string) {
2586 if ip.To4() != nil {
2588 m2 := ip.Mask(net.CIDRMask(26, 32)).String()
2589 m3 := ip.Mask(net.CIDRMask(21, 32)).String()
2592 m1 := ip.Mask(net.CIDRMask(64, 128)).String()
2593 m2 := ip.Mask(net.CIDRMask(48, 128)).String()
2594 m3 := ip.Mask(net.CIDRMask(32, 128)).String()
2598func (c *conn) xlocalserveError(lp smtp.Localpart) {
2599 code, timeout := mox.LocalserveNeedsError(lp)
2601 c.log.Info("timing out due to special localpart")
2602 mox.Sleep(mox.Context, time.Hour)
2603 xsmtpServerErrorf(codes{smtp.C451LocalErr, smtp.SeSys3Other0}, "timing out command due to special localpart")
2604 } else if code != 0 {
2605 c.log.Info("failure due to special localpart", slog.Int("code", code))
2606 metricDelivery.WithLabelValues("delivererror", "localserve").Inc()
2607 xsmtpServerErrorf(codes{code, smtp.SeOther00}, "failure with code %d due to special localpart", code)
2611// deliver is called for incoming messages from external, typically untrusted
2612// sources. i.e. not submitted by authenticated users.
2613func (c *conn) deliver(ctx context.Context, recvHdrFor func(string) string, msgWriter *message.Writer, iprevStatus iprev.Status, iprevAuthentic bool, dataFile *os.File) {
2614 // 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.
2616 var msgFrom smtp.Address
2617 var envelope *message.Envelope
2618 var headers textproto.MIMEHeader
2620 part, err := message.Parse(c.log.Logger, false, dataFile)
2622 // 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?
2623 isDSN = part.MediaType == "MULTIPART" && part.MediaSubType == "REPORT" && strings.EqualFold(part.ContentTypeParams["report-type"], "delivery-status")
2624 msgFrom, envelope, headers, err = message.From(c.log.Logger, false, dataFile, &part)
2627 c.log.Infox("parsing message for From address", err)
2631 if len(headers.Values("Received")) > 100 {
2632 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeNet4Loop6, "loop detected, more than 100 Received headers")
2635 // TLS-Required: No header makes us not enforce recipient domain's TLS policy.
2636 // Since we only deliver locally at the moment, this won't influence our behaviour.
2637 // Once we forward, it would our delivery attempts.
2640 if c.requireTLS == nil && hasTLSRequiredNo(headers) {
2645 // We'll be building up an Authentication-Results header.
2646 authResults := message.AuthResults{
2647 Hostname: mox.Conf.Static.HostnameDomain.XName(c.msgsmtputf8),
2650 commentAuthentic := func(v bool) string {
2652 return "with dnssec"
2654 return "without dnssec"
2657 // Reverse IP lookup results.
2658 // todo future: how useful is this?
2660 authResults.Methods = append(authResults.Methods, message.AuthMethod{
2662 Result: string(iprevStatus),
2663 Comment: commentAuthentic(iprevAuthentic),
2664 Props: []message.AuthProp{
2665 message.MakeAuthProp("policy", "iprev", c.remoteIP.String(), false, ""),
2669 // SPF and DKIM verification in parallel.
2670 var wg sync.WaitGroup
2674 var dkimResults []dkim.Result
2678 x := recover() // Should not happen, but don't take program down if it does.
2680 c.log.Error("dkim verify panic", slog.Any("err", x))
2682 metrics.PanicInc(metrics.Dkimverify)
2686 // We always evaluate all signatures. We want to build up reputation for each
2687 // domain in the signature.
2688 const ignoreTestMode = false
2689 // todo future: longer timeout? we have to read through the entire email, which can be large, possibly multiple times.
2690 dkimctx, dkimcancel := context.WithTimeout(ctx, time.Minute)
2692 // todo future: we could let user configure which dkim headers they require
2694 // For localserve, fake dkim selector DNS records for hosted domains to give
2695 // dkim-signatures a chance to pass for deliveries from queue.
2696 resolver := c.resolver
2698 // Lookup based on message From address is an approximation.
2699 if dc, ok := mox.Conf.Domain(msgFrom.Domain); ok && len(dc.DKIM.Selectors) > 0 {
2700 txts := map[string][]string{}
2701 for name, sel := range dc.DKIM.Selectors {
2702 dkimr := dkim.Record{
2704 Hashes: []string{sel.HashEffective},
2705 PublicKey: sel.Key.Public(),
2707 if _, ok := sel.Key.(ed25519.PrivateKey); ok {
2708 dkimr.Key = "ed25519"
2709 } else if _, ok := sel.Key.(*rsa.PrivateKey); !ok {
2710 err := fmt.Errorf("unrecognized private key for DKIM selector %q: %T", name, sel.Key)
2711 xcheckf(err, "making dkim record")
2713 txt, err := dkimr.Record()
2714 xcheckf(err, "making DKIM DNS TXT record")
2715 txts[name+"._domainkey."+msgFrom.Domain.ASCII+"."] = []string{txt}
2717 resolver = dns.MockResolver{TXT: txts}
2720 dkimResults, dkimErr = dkim.Verify(dkimctx, c.log.Logger, resolver, c.msgsmtputf8, dkim.DefaultPolicy, dataFile, ignoreTestMode)
2726 var receivedSPF spf.Received
2727 var spfDomain dns.Domain
2729 var spfAuthentic bool
2731 spfArgs := spf.Args{
2732 RemoteIP: c.remoteIP,
2733 MailFromLocalpart: c.mailFrom.Localpart,
2734 MailFromDomain: c.mailFrom.IPDomain.Domain, // Can be empty.
2735 HelloDomain: c.hello,
2737 LocalHostname: c.hostname,
2742 x := recover() // Should not happen, but don't take program down if it does.
2744 c.log.Error("spf verify panic", slog.Any("err", x))
2746 metrics.PanicInc(metrics.Spfverify)
2750 spfctx, spfcancel := context.WithTimeout(ctx, time.Minute)
2752 resolver := c.resolver
2753 // For localserve, give hosted domains a chance to pass for deliveries from queue.
2754 if Localserve && c.remoteIP.IsLoopback() {
2755 // Lookup based on message From address is an approximation.
2756 if _, ok := mox.Conf.Domain(msgFrom.Domain); ok {
2757 resolver = dns.MockResolver{
2758 TXT: map[string][]string{msgFrom.Domain.ASCII + ".": {"v=spf1 ip4:127.0.0.1/8 ip6:::1 ~all"}},
2762 receivedSPF, spfDomain, spfExpl, spfAuthentic, spfErr = spf.Verify(spfctx, c.log.Logger, resolver, spfArgs)
2765 c.log.Infox("spf verify", spfErr)
2769 // Wait for DKIM and SPF validation to finish.
2772 // Give immediate response if all recipients are unknown.
2774 for _, r := range c.recipients {
2775 if r.Account == nil && r.Alias == nil {
2779 if nunknown == len(c.recipients) {
2780 // During RCPT TO we found that the address does not exist.
2781 c.log.Info("deliver attempt to unknown user(s)", slog.Any("recipients", c.recipients))
2783 // Crude attempt to slow down someone trying to guess names. Would work better
2784 // with connection rate limiter.
2785 if unknownRecipientsDelay > 0 {
2786 mox.Sleep(ctx, unknownRecipientsDelay)
2789 // 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.
2790 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeAddr1UnknownDestMailbox1, "no such user(s)")
2793 // Add DKIM results to Authentication-Results header.
2794 authResAddDKIM := func(result, comment, reason string, props []message.AuthProp) {
2795 dm := message.AuthMethod{
2802 authResults.Methods = append(authResults.Methods, dm)
2805 c.log.Errorx("dkim verify", dkimErr)
2806 authResAddDKIM("none", "", dkimErr.Error(), nil)
2807 } else if len(dkimResults) == 0 {
2808 c.log.Info("no dkim-signature header", slog.Any("mailfrom", c.mailFrom))
2809 authResAddDKIM("none", "", "no dkim signatures", nil)
2811 for i, r := range dkimResults {
2812 var domain, selector dns.Domain
2813 var identity *dkim.Identity
2815 var props []message.AuthProp
2817 if r.Record != nil && r.Record.PublicKey != nil {
2818 if pubkey, ok := r.Record.PublicKey.(*rsa.PublicKey); ok {
2819 comment = fmt.Sprintf("%d bit rsa, ", pubkey.N.BitLen())
2823 sig := base64.StdEncoding.EncodeToString(r.Sig.Signature)
2824 sig = sig[:12] // Must be at least 8 characters and unique among the signatures.
2825 props = []message.AuthProp{
2826 message.MakeAuthProp("header", "d", r.Sig.Domain.XName(c.msgsmtputf8), true, r.Sig.Domain.ASCIIExtra(c.msgsmtputf8)),
2827 message.MakeAuthProp("header", "s", r.Sig.Selector.XName(c.msgsmtputf8), true, r.Sig.Selector.ASCIIExtra(c.msgsmtputf8)),
2828 message.MakeAuthProp("header", "a", r.Sig.Algorithm(), false, ""),
2831 domain = r.Sig.Domain
2832 selector = r.Sig.Selector
2833 if r.Sig.Identity != nil {
2834 props = append(props, message.MakeAuthProp("header", "i", r.Sig.Identity.String(), true, ""))
2835 identity = r.Sig.Identity
2837 if r.RecordAuthentic {
2838 comment += "with dnssec"
2840 comment += "without dnssec"
2845 errmsg = r.Err.Error()
2847 authResAddDKIM(string(r.Status), comment, errmsg, props)
2848 c.log.Debugx("dkim verification result", r.Err,
2849 slog.Int("index", i),
2850 slog.Any("mailfrom", c.mailFrom),
2851 slog.Any("status", r.Status),
2852 slog.Any("domain", domain),
2853 slog.Any("selector", selector),
2854 slog.Any("identity", identity))
2858 var spfIdentity *dns.Domain
2859 var mailFromValidation = store.ValidationUnknown
2860 var ehloValidation = store.ValidationUnknown
2861 switch receivedSPF.Identity {
2862 case spf.ReceivedHELO:
2863 if len(spfArgs.HelloDomain.IP) == 0 {
2864 spfIdentity = &spfArgs.HelloDomain.Domain
2866 ehloValidation = store.SPFValidation(receivedSPF.Result)
2867 case spf.ReceivedMailFrom:
2868 spfIdentity = &spfArgs.MailFromDomain
2869 mailFromValidation = store.SPFValidation(receivedSPF.Result)
2871 var props []message.AuthProp
2872 if spfIdentity != nil {
2873 props = []message.AuthProp{message.MakeAuthProp("smtp", string(receivedSPF.Identity), spfIdentity.XName(c.msgsmtputf8), true, spfIdentity.ASCIIExtra(c.msgsmtputf8))}
2875 var spfComment string
2877 spfComment = "with dnssec"
2879 spfComment = "without dnssec"
2881 authResults.Methods = append(authResults.Methods, message.AuthMethod{
2883 Result: string(receivedSPF.Result),
2884 Comment: spfComment,
2887 switch receivedSPF.Result {
2888 case spf.StatusPass:
2889 c.log.Debug("spf pass", slog.Any("ip", spfArgs.RemoteIP), slog.String("mailfromdomain", spfArgs.MailFromDomain.ASCII)) // todo: log the domain that was actually verified.
2890 case spf.StatusFail:
2893 for _, b := range []byte(spfExpl) {
2894 if b < ' ' || b >= 0x7f {
2900 if len(spfExpl) > 800 {
2901 spfExpl = spfExpl[:797] + "..."
2903 spfExpl = "remote claims: " + spfExpl
2907 spfExpl = fmt.Sprintf("your ip %s is not on the SPF allowlist for domain %s", spfArgs.RemoteIP, spfDomain.ASCII)
2909 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?
2910 case spf.StatusTemperror:
2911 c.log.Infox("spf temperror", spfErr)
2912 case spf.StatusPermerror:
2913 c.log.Infox("spf permerror", spfErr)
2914 case spf.StatusNone, spf.StatusNeutral, spf.StatusSoftfail:
2916 c.log.Error("unknown spf status, treating as None/Neutral", slog.Any("status", receivedSPF.Result))
2917 receivedSPF.Result = spf.StatusNone
2922 var dmarcResult dmarc.Result
2923 const applyRandomPercentage = true
2924 // dmarcMethod is added to authResults when delivering to recipients: accounts can
2925 // have different policy override rules.
2926 var dmarcMethod message.AuthMethod
2927 var msgFromValidation = store.ValidationNone
2928 if msgFrom.IsZero() {
2929 dmarcResult.Status = dmarc.StatusNone
2930 dmarcMethod = message.AuthMethod{
2932 Result: string(dmarcResult.Status),
2935 msgFromValidation = alignment(ctx, c.log, msgFrom.Domain, dkimResults, receivedSPF.Result, spfIdentity)
2937 // We are doing the DMARC evaluation now. But we only store it for inclusion in an
2938 // aggregate report when we actually use it. We use an evaluation for each
2939 // recipient, with each a potentially different result due to mailing
2940 // list/forwarding configuration. If we reject a message due to being spam, we
2941 // don't want to spend any resources for the sender domain, and we don't want to
2942 // give the sender any more information about us, so we won't record the
2944 // 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.
2946 dmarcctx, dmarccancel := context.WithTimeout(ctx, time.Minute)
2948 dmarcUse, dmarcResult = dmarc.Verify(dmarcctx, c.log.Logger, c.resolver, msgFrom.Domain, dkimResults, receivedSPF.Result, spfIdentity, applyRandomPercentage)
2951 if dmarcResult.RecordAuthentic {
2952 comment = "with dnssec"
2954 comment = "without dnssec"
2956 dmarcMethod = message.AuthMethod{
2958 Result: string(dmarcResult.Status),
2960 Props: []message.AuthProp{
2962 message.MakeAuthProp("header", "from", msgFrom.Domain.ASCII, true, msgFrom.Domain.ASCIIExtra(c.msgsmtputf8)),
2966 if dmarcResult.Status == dmarc.StatusPass && msgFromValidation == store.ValidationRelaxed {
2967 msgFromValidation = store.ValidationDMARC
2970 // todo future: consider enforcing an spf (soft)fail if there is no dmarc policy or the dmarc policy is none.
../rfc/7489:1507
2972 c.log.Debug("dmarc verification", slog.Any("result", dmarcResult.Status), slog.Any("domain", msgFrom.Domain))
2974 // Prepare for analyzing content, calculating reputation.
2975 ipmasked1, ipmasked2, ipmasked3 := ipmasked(c.remoteIP)
2976 var verifiedDKIMDomains []string
2977 dkimSeen := map[string]bool{}
2978 for _, r := range dkimResults {
2979 // A message can have multiple signatures for the same identity. For example when
2980 // signing the message multiple times with different algorithms (rsa and ed25519).
2981 if r.Status != dkim.StatusPass {
2984 d := r.Sig.Domain.Name()
2987 verifiedDKIMDomains = append(verifiedDKIMDomains, d)
2991 // When we deliver, we try to remove from rejects mailbox based on message-id.
2992 // We'll parse it when we need it, but it is the same for each recipient.
2993 var messageID string
2994 var parsedMessageID bool
2996 // We build up a DSN for each failed recipient. If we have recipients in dsnMsg
2997 // after processing, we queue the DSN. Unless all recipients failed, in which case
2998 // we may just fail the mail transaction instead (could be common for failure to
2999 // deliver to a single recipient, e.g. for junk mail).
3001 type deliverError struct {
3008 var deliverErrors []deliverError
3009 addError := func(rcpt recipient, code int, secode string, userError bool, errmsg string) {
3010 e := deliverError{rcpt.Addr, code, secode, userError, errmsg}
3011 c.log.Info("deliver error",
3012 slog.Any("rcptto", e.rcptTo),
3013 slog.Int("code", code),
3014 slog.String("secode", "secode"),
3015 slog.Bool("usererror", userError),
3016 slog.String("errmsg", errmsg))
3017 deliverErrors = append(deliverErrors, e)
3020 // Sort recipients: local accounts, aliases, unknown. For ensuring we don't deliver
3021 // to an alias destination that was also explicitly sent to.
3022 rcptScore := func(r recipient) int {
3023 if r.Account != nil {
3025 } else if r.Alias != nil {
3030 sort.SliceStable(c.recipients, func(i, j int) bool {
3031 return rcptScore(c.recipients[i]) < rcptScore(c.recipients[j])
3034 // Return whether address is a regular explicit recipient in this transaction. Used
3035 // to prevent delivering a message to an address both for alias and explicit
3036 // addressee. Relies on c.recipients being sorted as above.
3037 regularRecipient := func(addr smtp.Path) bool {
3038 for _, rcpt := range c.recipients {
3039 if rcpt.Account == nil {
3041 } else if rcpt.Addr.Equal(addr) {
3048 // Prepare a message, analyze it against account's junk filter.
3049 // The returned analysis has an open account that must be closed by the caller.
3050 // We call this for all alias destinations, also when we already delivered to that
3051 // recipient: It may be the only recipient that would allow the message.
3052 messageAnalyze := func(log mlog.Log, smtpRcptTo, deliverTo smtp.Path, accountName string, destination config.Destination, canonicalAddr string) (a *analysis, rerr error) {
3053 acc, err := store.OpenAccount(log, accountName, false)
3055 log.Errorx("open account", err, slog.Any("account", accountName))
3056 metricDelivery.WithLabelValues("accounterror", "").Inc()
3062 log.Check(err, "closing account during analysis")
3067 Received: time.Now(),
3068 RemoteIP: c.remoteIP.String(),
3069 RemoteIPMasked1: ipmasked1,
3070 RemoteIPMasked2: ipmasked2,
3071 RemoteIPMasked3: ipmasked3,
3072 EHLODomain: c.hello.Domain.Name(),
3073 MailFrom: c.mailFrom.String(),
3074 MailFromLocalpart: c.mailFrom.Localpart,
3075 MailFromDomain: c.mailFrom.IPDomain.Domain.Name(),
3076 RcptToLocalpart: smtpRcptTo.Localpart,
3077 RcptToDomain: smtpRcptTo.IPDomain.Domain.Name(),
3078 MsgFromLocalpart: msgFrom.Localpart,
3079 MsgFromDomain: msgFrom.Domain.Name(),
3080 MsgFromOrgDomain: publicsuffix.Lookup(ctx, log.Logger, msgFrom.Domain).Name(),
3081 EHLOValidated: ehloValidation == store.ValidationPass,
3082 MailFromValidated: mailFromValidation == store.ValidationPass,
3083 MsgFromValidated: msgFromValidation == store.ValidationStrict || msgFromValidation == store.ValidationDMARC || msgFromValidation == store.ValidationRelaxed,
3084 EHLOValidation: ehloValidation,
3085 MailFromValidation: mailFromValidation,
3086 MsgFromValidation: msgFromValidation,
3087 DKIMDomains: verifiedDKIMDomains,
3089 Size: msgWriter.Size,
3092 tlsState := c.conn.(*tls.Conn).ConnectionState()
3093 m.ReceivedTLSVersion = tlsState.Version
3094 m.ReceivedTLSCipherSuite = tlsState.CipherSuite
3095 if c.requireTLS != nil {
3096 m.ReceivedRequireTLS = *c.requireTLS
3099 m.ReceivedTLSVersion = 1 // Signals plain text delivery.
3102 var msgTo, msgCc []message.Address
3103 if envelope != nil {
3107 d := delivery{c.tls, &m, dataFile, smtpRcptTo, deliverTo, destination, canonicalAddr, acc, msgTo, msgCc, msgFrom, c.dnsBLs, dmarcUse, dmarcResult, dkimResults, iprevStatus, c.smtputf8}
3109 r := analyze(ctx, log, c.resolver, d)
3113 // Either deliver the message, or call addError to register the recipient as failed.
3114 // If recipient is an alias, we may be delivering to multiple address/accounts and
3115 // we will consider a message delivered if we delivered it to at least one account
3116 // (others may be over quota).
3117 processRecipient := func(rcpt recipient) {
3118 log := c.log.With(slog.Any("mailfrom", c.mailFrom), slog.Any("rcptto", rcpt.Addr))
3120 // If this is not a valid local user, we send back a DSN. This can only happen when
3121 // there are also valid recipients, and only when remote is SPF-verified, so the DSN
3122 // should not cause backscatter.
3123 // In case of serious errors, we abort the transaction. We may have already
3124 // delivered some messages. Perhaps it would be better to continue with other
3125 // deliveries, and return an error at the end? Though the failure conditions will
3126 // probably prevent any other successful deliveries too...
3128 if rcpt.Account == nil && rcpt.Alias == nil {
3129 metricDelivery.WithLabelValues("unknownuser", "").Inc()
3130 addError(rcpt, smtp.C550MailboxUnavail, smtp.SeAddr1UnknownDestMailbox1, true, "no such user")
3134 // la holds all analysis, and message preparation, for all accounts (multiple for
3135 // aliases). Each has an open account that we we close on return.
3138 for _, a := range la {
3139 err := a.d.acc.Close()
3140 log.Check(err, "close account")
3144 // For aliases, we prepare & analyze for each recipient. We accept the message if
3145 // any recipient accepts it. Regular destination have just a single account to
3146 // check. We check all alias destinations, even if we already explicitly delivered
3147 // to them: they may be the only destination that would accept the message.
3148 var a0 *analysis // Analysis we've used for accept/reject decision.
3149 if rcpt.Alias != nil {
3150 // Check if msgFrom address is acceptable. This doesn't take validation into
3151 // consideration. If the header was forged, the message may be rejected later on.
3152 if !aliasAllowedMsgFrom(rcpt.Alias.Alias, msgFrom) {
3153 addError(rcpt, smtp.C550MailboxUnavail, smtp.SePol7ExpnProhibited2, true, "not allowed to send to destination")
3157 la = make([]analysis, 0, len(rcpt.Alias.Alias.ParsedAddresses))
3158 for _, aa := range rcpt.Alias.Alias.ParsedAddresses {
3159 a, err := messageAnalyze(log, rcpt.Addr, aa.Address.Path(), aa.AccountName, aa.Destination, rcpt.Alias.CanonicalAddress)
3161 addError(rcpt, smtp.C451LocalErr, smtp.SeSys3Other0, false, "error processing")
3165 if a.accept && a0 == nil {
3166 // Address that caused us to accept.
3171 // First address, for rejecting.
3175 a, err := messageAnalyze(log, rcpt.Addr, rcpt.Addr, rcpt.Account.AccountName, rcpt.Account.Destination, rcpt.Account.CanonicalAddress)
3177 addError(rcpt, smtp.C451LocalErr, smtp.SeSys3Other0, false, "error processing")
3184 if !a0.accept && a0.reason == reasonHighRate {
3185 log.Info("incoming message rejected for high rate, not storing in rejects mailbox", slog.String("reason", a0.reason), slog.Any("msgfrom", msgFrom))
3186 metricDelivery.WithLabelValues("reject", a0.reason).Inc()
3188 addError(rcpt, a0.code, a0.secode, a0.userError, a0.errmsg)
3192 // Any DMARC result override is stored in the evaluation for outgoing DMARC
3193 // aggregate reports, and added to the Authentication-Results message header.
3194 // We want to tell the sender that we have an override, e.g. for mailing lists, so
3195 // they don't overestimate the potential damage of switching from p=none to
3197 var dmarcOverrides []string
3198 if a0.dmarcOverrideReason != "" {
3199 dmarcOverrides = []string{a0.dmarcOverrideReason}
3201 if dmarcResult.Record != nil && !dmarcUse {
3202 dmarcOverrides = append(dmarcOverrides, string(dmarcrpt.PolicyOverrideSampledOut))
3205 // Add per-recipient DMARC method to Authentication-Results. Each account can have
3206 // their own override rules, e.g. based on configured mailing lists/forwards.
3208 rcptDMARCMethod := dmarcMethod
3209 if len(dmarcOverrides) > 0 {
3210 if rcptDMARCMethod.Comment != "" {
3211 rcptDMARCMethod.Comment += ", "
3213 rcptDMARCMethod.Comment += "override " + strings.Join(dmarcOverrides, ",")
3215 rcptAuthResults := authResults
3216 rcptAuthResults.Methods = slices.Clone(authResults.Methods)
3217 rcptAuthResults.Methods = append(rcptAuthResults.Methods, rcptDMARCMethod)
3219 // Prepend reason as message header, for easy viewing in mail clients.
3221 if a0.reason != "" {
3222 hw := &message.HeaderWriter{}
3223 hw.Add(" ", "X-Mox-Reason:")
3224 hw.Add(" ", a0.reason)
3225 for i, s := range a0.reasonText {
3231 // Just in case any of the strings has a newline, replace it with space to not break the message.
3232 s = strings.ReplaceAll(s, "\n", " ")
3233 s = strings.ReplaceAll(s, "\r", " ")
3235 hw.AddWrap([]byte(s), true)
3244 la[i].d.m.MsgPrefix = []byte(
3248 rcptAuthResults.Header() +
3249 receivedSPF.Header() +
3250 recvHdrFor(rcpt.Addr.String()),
3252 la[i].d.m.Size += int64(len(la[i].d.m.MsgPrefix))
3255 // Store DMARC evaluation for inclusion in an aggregate report. Only if there is at
3256 // least one reporting address: We don't want to needlessly store a row in a
3257 // database for each delivery attempt. If we reject a message for being junk, we
3258 // are also not going to send it a DMARC report. The DMARC check is done early in
3259 // the analysis, we will report on rejects because of DMARC, because it could be
3260 // valuable feedback about forwarded or mailing list messages.
3262 if !mox.Conf.Static.NoOutgoingDMARCReports && dmarcResult.Record != nil && len(dmarcResult.Record.AggregateReportAddresses) > 0 && (a0.accept && !a0.d.m.IsReject || a0.reason == reasonDMARCPolicy) {
3263 // Disposition holds our decision on whether to accept the message. Not what the
3264 // DMARC evaluation resulted in. We can override, e.g. because of mailing lists,
3265 // forwarding, or local policy.
3266 // We treat quarantine as reject, so never claim to quarantine.
3268 disposition := dmarcrpt.DispositionNone
3270 disposition = dmarcrpt.DispositionReject
3273 // unknownDomain returns whether the sender is domain with which this account has
3274 // not had positive interaction.
3275 unknownDomain := func() (unknown bool) {
3276 err := a0.d.acc.DB.Read(ctx, func(tx *bstore.Tx) (err error) {
3277 // See if we received a non-junk message from this organizational domain.
3278 q := bstore.QueryTx[store.Message](tx)
3279 q.FilterNonzero(store.Message{MsgFromOrgDomain: a0.d.m.MsgFromOrgDomain})
3280 q.FilterEqual("Expunged", false)
3281 q.FilterEqual("Notjunk", true)
3282 q.FilterEqual("IsReject", false)
3283 exists, err := q.Exists()
3285 return fmt.Errorf("querying for non-junk message from organizational domain: %v", err)
3291 // See if we sent a message to this organizational domain.
3292 qr := bstore.QueryTx[store.Recipient](tx)
3293 qr.FilterNonzero(store.Recipient{OrgDomain: a0.d.m.MsgFromOrgDomain})
3294 exists, err = qr.Exists()
3296 return fmt.Errorf("querying for message sent to organizational domain: %v", err)
3304 log.Errorx("checking if sender is unknown domain, for dmarc aggregate report evaluation", err)
3309 r := dmarcResult.Record
3310 addresses := make([]string, len(r.AggregateReportAddresses))
3311 for i, a := range r.AggregateReportAddresses {
3312 addresses[i] = a.String()
3314 sp := dmarcrpt.Disposition(r.SubdomainPolicy)
3315 if r.SubdomainPolicy == dmarc.PolicyEmpty {
3316 sp = dmarcrpt.Disposition(r.Policy)
3318 eval := dmarcdb.Evaluation{
3319 // Evaluated and IntervalHours set by AddEvaluation.
3320 PolicyDomain: dmarcResult.Domain.Name(),
3322 // Optional evaluations don't cause a report to be sent, but will be included.
3323 // Useful for automated inter-mailer messages, we don't want to get in a reporting
3324 // loop. We also don't want to be used for sending reports to unsuspecting domains
3325 // we have no relation with.
3326 // 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.
3327 Optional: a0.d.destination.DMARCReports || a0.d.destination.HostTLSReports || a0.d.destination.DomainTLSReports || a0.reason == reasonDMARCPolicy && unknownDomain(),
3329 Addresses: addresses,
3331 PolicyPublished: dmarcrpt.PolicyPublished{
3332 Domain: dmarcResult.Domain.Name(),
3333 ADKIM: dmarcrpt.Alignment(r.ADKIM),
3334 ASPF: dmarcrpt.Alignment(r.ASPF),
3335 Policy: dmarcrpt.Disposition(r.Policy),
3336 SubdomainPolicy: sp,
3337 Percentage: r.Percentage,
3338 // We don't save ReportingOptions, we don't do per-message failure reporting.
3340 SourceIP: c.remoteIP.String(),
3341 Disposition: disposition,
3342 AlignedDKIMPass: dmarcResult.AlignedDKIMPass,
3343 AlignedSPFPass: dmarcResult.AlignedSPFPass,
3344 EnvelopeTo: rcpt.Addr.IPDomain.String(),
3345 EnvelopeFrom: c.mailFrom.IPDomain.String(),
3346 HeaderFrom: msgFrom.Domain.Name(),
3349 for _, s := range dmarcOverrides {
3350 reason := dmarcrpt.PolicyOverrideReason{Type: dmarcrpt.PolicyOverride(s)}
3351 eval.OverrideReasons = append(eval.OverrideReasons, reason)
3354 // We'll include all signatures for the organizational domain, even if they weren't
3355 // relevant due to strict alignment requirement.
3356 for _, dkimResult := range dkimResults {
3357 if dkimResult.Sig == nil || publicsuffix.Lookup(ctx, log.Logger, msgFrom.Domain) != publicsuffix.Lookup(ctx, log.Logger, dkimResult.Sig.Domain) {
3360 r := dmarcrpt.DKIMAuthResult{
3361 Domain: dkimResult.Sig.Domain.Name(),
3362 Selector: dkimResult.Sig.Selector.ASCII,
3363 Result: dmarcrpt.DKIMResult(dkimResult.Status),
3365 eval.DKIMResults = append(eval.DKIMResults, r)
3368 switch receivedSPF.Identity {
3369 case spf.ReceivedHELO:
3370 spfAuthResult := dmarcrpt.SPFAuthResult{
3371 Domain: spfArgs.HelloDomain.String(), // Can be unicode and also IP.
3372 Scope: dmarcrpt.SPFDomainScopeHelo,
3373 Result: dmarcrpt.SPFResult(receivedSPF.Result),
3375 eval.SPFResults = []dmarcrpt.SPFAuthResult{spfAuthResult}
3376 case spf.ReceivedMailFrom:
3377 spfAuthResult := dmarcrpt.SPFAuthResult{
3378 Domain: spfArgs.MailFromDomain.Name(), // Can be unicode.
3379 Scope: dmarcrpt.SPFDomainScopeMailFrom,
3380 Result: dmarcrpt.SPFResult(receivedSPF.Result),
3382 eval.SPFResults = []dmarcrpt.SPFAuthResult{spfAuthResult}
3385 err := dmarcdb.AddEvaluation(ctx, dmarcResult.Record.AggregateReportingInterval, &eval)
3386 log.Check(err, "adding dmarc evaluation to database for aggregate report")
3390 for _, a := range la {
3391 // Don't add message if address was also explicitly present in a RCPT TO command.
3392 if rcpt.Alias != nil && regularRecipient(a.d.deliverTo) {
3396 conf, _ := a.d.acc.Conf()
3397 if conf.RejectsMailbox == "" {
3400 present, _, messagehash, err := rejectPresent(log, a.d.acc, conf.RejectsMailbox, a.d.m, dataFile)
3402 log.Errorx("checking whether reject is already present", err)
3405 log.Info("reject message is already present, ignoring")
3408 a.d.m.IsReject = true
3409 a.d.m.Seen = true // We don't want to draw attention.
3410 // Regular automatic junk flags configuration applies to these messages. The
3411 // default is to treat these as neutral, so they won't cause outright rejections
3412 // due to reputation for later delivery attempts.
3413 a.d.m.MessageHash = messagehash
3414 a.d.acc.WithWLock(func() {
3415 var changes []store.Change
3421 p := a.d.acc.MessagePath(newID)
3423 c.log.Check(err, "remove message after error delivering to rejects", slog.String("path", p))
3427 err := a.d.acc.DB.Write(context.TODO(), func(tx *bstore.Tx) error {
3428 mbrej, err := a.d.acc.MailboxFind(tx, conf.RejectsMailbox)
3430 return fmt.Errorf("finding rejects mailbox: %v", err)
3433 if !conf.KeepRejects && mbrej != nil {
3434 chl, hasSpace, err := a.d.acc.TidyRejectsMailbox(c.log, tx, mbrej)
3436 return fmt.Errorf("tidying rejects mailbox: %v", err)
3438 changes = append(changes, chl...)
3440 log.Info("not storing spammy mail to full rejects mailbox")
3445 nmb, chl, _, _, err := a.d.acc.MailboxCreate(tx, conf.RejectsMailbox, store.SpecialUse{})
3447 return fmt.Errorf("creating rejects mailbox: %v", err)
3449 changes = append(changes, chl...)
3453 a.d.m.MailboxID = mbrej.ID
3454 if err := a.d.acc.MessageAdd(log, tx, mbrej, a.d.m, dataFile, store.AddOpts{}); err != nil {
3455 return fmt.Errorf("delivering spammy mail to rejects mailbox: %v", err)
3459 if err := tx.Update(mbrej); err != nil {
3460 return fmt.Errorf("updating rejects mailbox: %v", err)
3462 changes = append(changes, a.d.m.ChangeAddUID(*mbrej), mbrej.ChangeCounts())
3467 log.Errorx("delivering to rejects mailbox", err)
3470 log.Info("stored spammy mail in rejects mailbox")
3474 store.BroadcastChanges(a.d.acc, changes)
3478 log.Info("incoming message rejected", slog.String("reason", a0.reason), slog.Any("msgfrom", msgFrom))
3479 metricDelivery.WithLabelValues("reject", a0.reason).Inc()
3481 addError(rcpt, a0.code, a0.secode, a0.userError, a0.errmsg)
3485 delayFirstTime := true
3486 if rcpt.Account != nil && a0.dmarcReport != nil {
3488 if err := dmarcdb.AddReport(ctx, a0.dmarcReport, msgFrom.Domain); err != nil {
3489 log.Errorx("saving dmarc aggregate report in database", err)
3491 log.Info("dmarc aggregate report processed")
3492 a0.d.m.Flags.Seen = true
3493 delayFirstTime = false
3496 if rcpt.Account != nil && a0.tlsReport != nil {
3497 // todo future: add rate limiting to prevent DoS attacks.
3498 if err := tlsrptdb.AddReport(ctx, c.log, msgFrom.Domain, c.mailFrom.String(), a0.d.destination.HostTLSReports, a0.tlsReport); err != nil {
3499 log.Errorx("saving TLSRPT report in database", err)
3501 log.Info("tlsrpt report processed")
3502 a0.d.m.Flags.Seen = true
3503 delayFirstTime = false
3507 // If this is a first-time sender and not a forwarded/mailing list message, wait
3508 // before actually delivering. If this turns out to be a spammer, we've kept one of
3509 // their connections busy.
3510 a0conf, _ := a0.d.acc.Conf()
3511 if delayFirstTime && !a0.d.m.IsForward && !a0.d.m.IsMailingList && a0.reason == reasonNoBadSignals && !a0conf.NoFirstTimeSenderDelay && c.firstTimeSenderDelay > 0 {
3512 log.Debug("delaying before delivering from sender without reputation", slog.Duration("delay", c.firstTimeSenderDelay))
3513 mox.Sleep(mox.Context, c.firstTimeSenderDelay)
3517 code, timeout := mox.LocalserveNeedsError(rcpt.Addr.Localpart)
3519 log.Info("timing out due to special localpart")
3520 mox.Sleep(mox.Context, time.Hour)
3521 xsmtpServerErrorf(codes{smtp.C451LocalErr, smtp.SeOther00}, "timing out delivery due to special localpart")
3522 } else if code != 0 {
3523 log.Info("failure due to special localpart", slog.Int("code", code))
3524 metricDelivery.WithLabelValues("delivererror", "localserve").Inc()
3525 addError(rcpt, code, smtp.SeOther00, false, fmt.Sprintf("failure with code %d due to special localpart", code))
3530 // Gather the message-id before we deliver and the file may be consumed.
3531 if !parsedMessageID {
3532 if p, err := message.Parse(c.log.Logger, false, store.FileMsgReader(a0.d.m.MsgPrefix, dataFile)); err != nil {
3533 log.Infox("parsing message for message-id", err)
3534 } else if header, err := p.Header(); err != nil {
3535 log.Infox("parsing message header for message-id", err)
3537 messageID = header.Get("Message-Id")
3539 parsedMessageID = true
3542 // Finally deliver the message to the account(s).
3543 var nerr int // Number of non-quota errors.
3544 var nfull int // Number of failed deliveries due to over quota.
3545 var ndelivered int // Number delivered to account.
3546 for _, a := range la {
3547 // Don't deliver to recipient that was explicitly present in SMTP transaction, or
3548 // is sending the message to an alias they are member of.
3549 if rcpt.Alias != nil && (regularRecipient(a.d.deliverTo) || a.d.deliverTo.Equal(msgFrom.Path())) {
3554 a.d.acc.WithWLock(func() {
3555 if err := a.d.acc.DeliverMailbox(log, a.mailbox, a.d.m, dataFile); err != nil {
3556 log.Errorx("delivering", err)
3557 metricDelivery.WithLabelValues("delivererror", a0.reason).Inc()
3558 if errors.Is(err, store.ErrOverQuota) {
3561 addError(rcpt, smtp.C451LocalErr, smtp.SeSys3Other0, false, "error processing")
3568 metricDelivery.WithLabelValues("delivered", a0.reason).Inc()
3569 log.Info("incoming message delivered", slog.String("reason", a0.reason), slog.Any("msgfrom", msgFrom))
3571 conf, _ := a.d.acc.Conf()
3572 if conf.RejectsMailbox != "" && a.d.m.MessageID != "" {
3573 if err := a.d.acc.RejectsRemove(log, conf.RejectsMailbox, a.d.m.MessageID); err != nil {
3574 log.Errorx("removing message from rejects mailbox", err, slog.String("messageid", messageID))
3579 // Pass delivered messages to queue for DSN processing and/or hooks.
3581 mr := store.FileMsgReader(a.d.m.MsgPrefix, dataFile)
3582 part, err := a.d.m.LoadPart(mr)
3584 log.Errorx("loading parsed part for evaluating webhook", err)
3586 err = queue.Incoming(context.Background(), log, a.d.acc, messageID, *a.d.m, part, a.mailbox)
3587 log.Check(err, "queueing webhook for incoming delivery")
3589 } else if nerr > 0 && ndelivered == 0 {
3590 // Don't continue if we had an error and haven't delivered yet. If we only had
3591 // quota-related errors, we keep trying for an account to deliver to.
3595 if ndelivered == 0 && (nerr > 0 || nfull > 0) {
3597 addError(rcpt, smtp.C452StorageFull, smtp.SeMailbox2Full2, true, "account storage full")
3599 addError(rcpt, smtp.C451LocalErr, smtp.SeSys3Other0, false, "error processing")
3604 // For each recipient, do final spam analysis and delivery.
3605 for _, rcpt := range c.recipients {
3606 processRecipient(rcpt)
3609 // If all recipients failed to deliver, return an error.
3610 if len(c.recipients) == len(deliverErrors) {
3612 e0 := deliverErrors[0]
3613 var serverError bool
3616 for _, e := range deliverErrors {
3617 serverError = serverError || !e.userError
3618 if e.code != e0.code || e.secode != e0.secode {
3621 msgs = append(msgs, e.errmsg)
3627 xsmtpErrorf(e0.code, e0.secode, !serverError, "%s", strings.Join(msgs, "\n"))
3630 // Not all failures had the same error. We'll return each error on a separate line.
3632 for _, e := range deliverErrors {
3633 s := fmt.Sprintf("%d %d.%s %s", e.code, e.code/100, e.secode, e.errmsg)
3634 lines = append(lines, s)
3636 code := smtp.C451LocalErr
3637 secode := smtp.SeSys3Other0
3639 code = smtp.C554TransactionFailed
3641 lines = append(lines, "multiple errors")
3642 xsmtpErrorf(code, secode, !serverError, "%s", strings.Join(lines, "\n"))
3644 // Generate one DSN for all failed recipients.
3645 if len(deliverErrors) > 0 {
3647 dsnMsg := dsn.Message{
3648 SMTPUTF8: c.msgsmtputf8,
3649 From: smtp.Path{Localpart: "postmaster", IPDomain: deliverErrors[0].rcptTo.IPDomain},
3651 Subject: "mail delivery failure",
3652 MessageID: mox.MessageIDGen(false),
3653 References: messageID,
3655 // Per-message details.
3656 ReportingMTA: mox.Conf.Static.HostnameDomain.ASCII,
3657 ReceivedFromMTA: smtp.Ehlo{Name: c.hello, ConnIP: c.remoteIP},
3661 if len(deliverErrors) > 1 {
3662 dsnMsg.TextBody = "Multiple delivery failures occurred.\n\n"
3665 for _, e := range deliverErrors {
3667 if e.code/100 == 4 {
3670 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))
3671 rcpt := dsn.Recipient{
3672 FinalRecipient: e.rcptTo,
3674 Status: fmt.Sprintf("%d.%s", e.code/100, e.secode),
3675 LastAttemptDate: now,
3677 dsnMsg.Recipients = append(dsnMsg.Recipients, rcpt)
3680 header, err := message.ReadHeaders(bufio.NewReader(&moxio.AtReader{R: dataFile}))
3682 c.log.Errorx("reading headers of incoming message for dsn, continuing dsn without headers", err)
3684 dsnMsg.Original = header
3687 c.log.Error("not queueing dsn for incoming delivery due to localserve")
3688 } else if err := queueDSN(context.TODO(), c.log, c, *c.mailFrom, dsnMsg, c.requireTLS != nil && *c.requireTLS); err != nil {
3689 metricServerErrors.WithLabelValues("queuedsn").Inc()
3690 c.log.Errorx("queuing DSN for incoming delivery, no DSN sent", err)
3695 c.transactionBad-- // Compensate for early earlier pessimistic increase.
3697 c.xwritecodeline(smtp.C250Completed, smtp.SeMailbox2Other0, "it is done", nil)
3700// Return whether msgFrom address is allowed to send a message to alias.
3701func aliasAllowedMsgFrom(alias config.Alias, msgFrom smtp.Address) bool {
3702 for _, aa := range alias.ParsedAddresses {
3703 if aa.Address == msgFrom {
3707 lp, err := smtp.ParseLocalpart(alias.LocalpartStr)
3708 xcheckf(err, "parsing alias localpart")
3709 if msgFrom == smtp.NewAddress(lp, alias.Domain) {
3710 return alias.AllowMsgFrom
3712 return alias.PostPublic
3715// ecode returns either ecode, or a more specific error based on err.
3716// For example, ecode can be turned from an "other system" error into a "mail
3717// system full" if the error indicates no disk space is available.
3718func errCodes(code int, ecode string, err error) codes {
3720 case moxio.IsStorageSpace(err):
3722 case smtp.SeMailbox2Other0:
3723 if code == smtp.C451LocalErr {
3724 code = smtp.C452StorageFull
3726 ecode = smtp.SeMailbox2Full2
3727 case smtp.SeSys3Other0:
3728 if code == smtp.C451LocalErr {
3729 code = smtp.C452StorageFull
3731 ecode = smtp.SeSys3StorageFull1
3734 return codes{code, ecode}
3738func (c *conn) cmdRset(p *parser) {
3743 c.xbwritecodeline(smtp.C250Completed, smtp.SeOther00, "all clear", nil)
3747func (c *conn) cmdVrfy(p *parser) {
3748 // No EHLO/HELO needed.
3759 // todo future: we could support vrfy and expn for submission? though would need to see if its rfc defines it.
3762 xsmtpUserErrorf(smtp.C252WithoutVrfy, smtp.SePol7Other0, "no verify but will try delivery")
3766func (c *conn) cmdExpn(p *parser) {
3767 // No EHLO/HELO needed.
3778 // todo: we could implement expn for local aliases for authenticated users, when members have permission to list. would anyone use it?
3781 xsmtpUserErrorf(smtp.C252WithoutVrfy, smtp.SePol7Other0, "no expand but will try delivery")
3785func (c *conn) cmdHelp(p *parser) {
3786 // Let's not strictly parse the request for help. We are ignoring the text anyway.
3789 c.xbwritecodeline(smtp.C214Help, smtp.SeOther00, "see rfc 5321 (smtp)", nil)
3793func (c *conn) cmdNoop(p *parser) {
3794 // No idea why, but if an argument follows, it must adhere to the string ABNF production...
3801 c.xbwritecodeline(smtp.C250Completed, smtp.SeOther00, "alrighty", nil)
3805func (c *conn) cmdQuit(p *parser) {
3809 c.xwritecodeline(smtp.C221Closing, smtp.SeOther00, "okay thanks bye", nil)