1// Package smtpserver implements an SMTP server for submission and incoming delivery of mail messages.
10 cryptorand "crypto/rand"
34 "golang.org/x/exp/maps"
35 "golang.org/x/text/unicode/norm"
37 "github.com/prometheus/client_golang/prometheus"
38 "github.com/prometheus/client_golang/prometheus/promauto"
40 "github.com/mjl-/bstore"
42 "github.com/mjl-/mox/config"
43 "github.com/mjl-/mox/dkim"
44 "github.com/mjl-/mox/dmarc"
45 "github.com/mjl-/mox/dmarcdb"
46 "github.com/mjl-/mox/dmarcrpt"
47 "github.com/mjl-/mox/dns"
48 "github.com/mjl-/mox/dsn"
49 "github.com/mjl-/mox/iprev"
50 "github.com/mjl-/mox/message"
51 "github.com/mjl-/mox/metrics"
52 "github.com/mjl-/mox/mlog"
53 "github.com/mjl-/mox/mox-"
54 "github.com/mjl-/mox/moxio"
55 "github.com/mjl-/mox/moxvar"
56 "github.com/mjl-/mox/publicsuffix"
57 "github.com/mjl-/mox/queue"
58 "github.com/mjl-/mox/ratelimit"
59 "github.com/mjl-/mox/scram"
60 "github.com/mjl-/mox/smtp"
61 "github.com/mjl-/mox/spf"
62 "github.com/mjl-/mox/store"
63 "github.com/mjl-/mox/tlsrpt"
64 "github.com/mjl-/mox/tlsrptdb"
67// We use panic and recover for error handling while executing commands.
68// These errors signal the connection must be closed.
69var errIO = errors.New("io error")
71// If set, regular delivery/submit is sidestepped, email is accepted and
72// delivered to the account named mox.
75var limiterConnectionRate, limiterConnections *ratelimit.Limiter
77// For delivery rate limiting. Variable because changed during tests.
78var limitIPMasked1MessagesPerMinute int = 500
79var limitIPMasked1SizePerMinute int64 = 1000 * 1024 * 1024
81// Maximum number of RCPT TO commands (i.e. recipients) for a single message
82// delivery. Must be at least 100. Announced in LIMIT extension.
83const rcptToLimit = 1000
86 // Also called by tests, so they don't trigger the rate limiter.
92 // todo future: make these configurable
93 limiterConnectionRate = &ratelimit.Limiter{
94 WindowLimits: []ratelimit.WindowLimit{
97 Limits: [...]int64{300, 900, 2700},
101 limiterConnections = &ratelimit.Limiter{
102 WindowLimits: []ratelimit.WindowLimit{
104 Window: time.Duration(math.MaxInt64), // All of time.
105 Limits: [...]int64{30, 90, 270},
112 // Delays for bad/suspicious behaviour. Zero during tests.
113 badClientDelay = time.Second // Before reads and after 1-byte writes for probably spammers.
114 authFailDelay = time.Second // Response to authentication failure.
115 unknownRecipientsDelay = 5 * time.Second // Response when all recipients are unknown.
116 firstTimeSenderDelayDefault = 15 * time.Second // Before accepting message from first-time sender.
121 secode string // Enhanced code, but without the leading major int from code.
125 metricConnection = promauto.NewCounterVec(
126 prometheus.CounterOpts{
127 Name: "mox_smtpserver_connection_total",
128 Help: "Incoming SMTP connections.",
131 "kind", // "deliver" or "submit"
134 metricCommands = promauto.NewHistogramVec(
135 prometheus.HistogramOpts{
136 Name: "mox_smtpserver_command_duration_seconds",
137 Help: "SMTP server command duration and result codes in seconds.",
138 Buckets: []float64{0.001, 0.005, 0.01, 0.05, 0.100, 0.5, 1, 5, 10, 20, 30, 60, 120},
141 "kind", // "deliver" or "submit"
147 metricDelivery = promauto.NewCounterVec(
148 prometheus.CounterOpts{
149 Name: "mox_smtpserver_delivery_total",
150 Help: "SMTP incoming message delivery from external source, not submission. Result values: delivered, reject, unknownuser, accounterror, delivererror. Reason indicates why a message was rejected/accepted.",
157 // Similar between ../webmail/webmail.go:/metricSubmission and ../smtpserver/server.go:/metricSubmission and ../webapisrv/server.go:/metricSubmission
158 metricSubmission = promauto.NewCounterVec(
159 prometheus.CounterOpts{
160 Name: "mox_smtpserver_submission_total",
161 Help: "SMTP server incoming submission results, known values (those ending with error are server errors): ok, badmessage, badfrom, badheader, messagelimiterror, recipientlimiterror, localserveerror, queueerror.",
167 metricServerErrors = promauto.NewCounterVec(
168 prometheus.CounterOpts{
169 Name: "mox_smtpserver_errors_total",
170 Help: "SMTP server errors, known values: dkimsign, queuedsn.",
176 metricDeliveryStarttls = promauto.NewCounter(
177 prometheus.CounterOpts{
178 Name: "mox_smtpserver_delivery_starttls_total",
179 Help: "Total number of STARTTLS handshakes for incoming deliveries.",
182 metricDeliveryStarttlsErrors = promauto.NewCounterVec(
183 prometheus.CounterOpts{
184 Name: "mox_smtpserver_delivery_starttls_errors_total",
185 Help: "Errors with TLS handshake during STARTTLS for incoming deliveries.",
188 "reason", // "eof", "sslv2", "unsupportedversions", "nottls", "alert-<num>-<msg>", "other"
193var jitterRand = mox.NewPseudoRand()
195func durationDefault(delay *time.Duration, def time.Duration) time.Duration {
202// Listen initializes network listeners for incoming SMTP connection.
203// The listeners are stored for a later call to Serve.
205 names := maps.Keys(mox.Conf.Static.Listeners)
207 for _, name := range names {
208 listener := mox.Conf.Static.Listeners[name]
210 var tlsConfig, tlsConfigDelivery *tls.Config
211 if listener.TLS != nil {
212 tlsConfig = listener.TLS.Config
213 // For SMTP delivery, if we get a TLS handshake for an SNI hostname that we don't
214 // allow, we'll fallback to a certificate for the listener hostname instead of
215 // causing the connection to fail. May improve interoperability.
216 tlsConfigDelivery = listener.TLS.ConfigFallback
219 maxMsgSize := listener.SMTPMaxMessageSize
221 maxMsgSize = config.DefaultMaxMsgSize
224 if listener.SMTP.Enabled {
225 hostname := mox.Conf.Static.HostnameDomain
226 if listener.Hostname != "" {
227 hostname = listener.HostnameDomain
229 port := config.Port(listener.SMTP.Port, 25)
230 for _, ip := range listener.IPs {
231 firstTimeSenderDelay := durationDefault(listener.SMTP.FirstTimeSenderDelay, firstTimeSenderDelayDefault)
232 if tlsConfigDelivery != nil {
233 tlsConfigDelivery = tlsConfigDelivery.Clone()
234 // Default setting is currently to have session tickets disabled, to work around
235 // TLS interoperability issues with incoming deliveries from Microsoft. See
236 // https://github.com/golang/go/issues/70232.
237 tlsConfigDelivery.SessionTicketsDisabled = listener.SMTP.TLSSessionTicketsDisabled == nil || *listener.SMTP.TLSSessionTicketsDisabled
239 listen1("smtp", name, ip, port, hostname, tlsConfigDelivery, false, false, maxMsgSize, false, listener.SMTP.RequireSTARTTLS, !listener.SMTP.NoRequireTLS, listener.SMTP.DNSBLZones, firstTimeSenderDelay)
242 if listener.Submission.Enabled {
243 hostname := mox.Conf.Static.HostnameDomain
244 if listener.Hostname != "" {
245 hostname = listener.HostnameDomain
247 port := config.Port(listener.Submission.Port, 587)
248 for _, ip := range listener.IPs {
249 listen1("submission", name, ip, port, hostname, tlsConfig, true, false, maxMsgSize, !listener.Submission.NoRequireSTARTTLS, !listener.Submission.NoRequireSTARTTLS, true, nil, 0)
253 if listener.Submissions.Enabled {
254 hostname := mox.Conf.Static.HostnameDomain
255 if listener.Hostname != "" {
256 hostname = listener.HostnameDomain
258 port := config.Port(listener.Submissions.Port, 465)
259 for _, ip := range listener.IPs {
260 listen1("submissions", name, ip, port, hostname, tlsConfig, true, true, maxMsgSize, true, true, true, nil, 0)
268func listen1(protocol, name, ip string, port int, hostname dns.Domain, tlsConfig *tls.Config, submission, xtls bool, maxMessageSize int64, requireTLSForAuth, requireTLSForDelivery, requireTLS bool, dnsBLs []dns.Domain, firstTimeSenderDelay time.Duration) {
269 log := mlog.New("smtpserver", nil)
270 addr := net.JoinHostPort(ip, fmt.Sprintf("%d", port))
271 if os.Getuid() == 0 {
272 log.Print("listening for smtp",
273 slog.String("listener", name),
274 slog.String("address", addr),
275 slog.String("protocol", protocol))
277 network := mox.Network(ip)
278 ln, err := mox.Listen(network, addr)
280 log.Fatalx("smtp: listen for smtp", err, slog.String("protocol", protocol), slog.String("listener", name))
283 // Each listener gets its own copy of the config, so session keys between different
284 // ports on same listener aren't shared. We rotate session keys explicitly in this
285 // base TLS config because each connection clones the TLS config before using. The
286 // base TLS config would never get automatically managed/rotated session keys.
287 if tlsConfig != nil {
288 tlsConfig = tlsConfig.Clone()
289 mox.StartTLSSessionTicketKeyRefresher(mox.Shutdown, log, tlsConfig)
294 conn, err := ln.Accept()
296 log.Infox("smtp: accept", err, slog.String("protocol", protocol), slog.String("listener", name))
300 // Package is set on the resolver by the dkim/spf/dmarc/etc packages.
301 resolver := dns.StrictResolver{Log: log.Logger}
302 go serve(name, mox.Cid(), hostname, tlsConfig, conn, resolver, submission, xtls, 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 resolver dns.Resolver
331 tr *moxio.TraceReader // Kept for changing trace level during cmd/auth/data.
332 tw *moxio.TraceWriter
333 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.
334 lastlog time.Time // Used for printing the delta time since the previous logging for this connection.
336 baseTLSConfig *tls.Config
342 requireTLSForAuth bool
343 requireTLSForDelivery bool // If set, delivery is only allowed with TLS (STARTTLS), except if delivery is to a TLS reporting address.
344 cmd string // Current command.
345 cmdStart time.Time // Start of current command.
346 ncmds int // Number of commands processed. Used to abort connection when first incoming command is unknown/invalid.
348 firstTimeSenderDelay time.Duration
350 // If non-zero, taken into account during Read and Write. Set while processing DATA
351 // command, we don't want the entire delivery to take too long.
354 hello dns.IPDomain // Claimed remote name. Can be ip address for ehlo.
355 ehlo bool // If set, we had EHLO instead of HELO.
357 authFailed int // Number of failed auth attempts. For slowing down remote with many failures.
358 authSASL bool // Whether SASL authentication was done.
359 authTLS bool // Whether we did TLS client cert authentication.
360 username string // Only when authenticated.
361 account *store.Account // Only when authenticated.
363 // We track good/bad message transactions to disconnect spammers trying to guess addresses.
367 // Message transaction.
369 requireTLS *bool // MAIL FROM with REQUIRETLS set.
370 futureRelease time.Time // MAIL FROM with HOLDFOR or HOLDUNTIL.
371 futureReleaseRequest string // For use in DSNs, either "for;" or "until;" plus original value.
../rfc/4865:305
372 has8bitmime bool // If MAIL FROM parameter BODY=8BITMIME was sent. Required for SMTPUTF8.
373 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.
374 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.
375 recipients []recipient
378type rcptAccount struct {
380 Destination config.Destination
381 CanonicalAddress string // Optional catchall part stripped and/or lowercased.
384type rcptAlias struct {
386 CanonicalAddress string // Optional catchall part stripped and/or lowercased.
389type recipient struct {
392 // If account and alias are both not set, this is not for a local address. This is
393 // normal for submission, where messages are added to the queue. For incoming
394 // deliveries, this will result in an error.
395 Account *rcptAccount // If set, recipient address is for this local account.
396 Alias *rcptAlias // If set, for a local alias.
399func isClosed(err error) bool {
400 return errors.Is(err, errIO) || moxio.IsClosed(err)
403// makeTLSConfig makes a new tls config that is bound to the connection for
404// possible client certificate authentication in case of submission.
405func (c *conn) makeTLSConfig() *tls.Config {
407 return c.baseTLSConfig
410 // We clone the config so we can set VerifyPeerCertificate below to a method bound
411 // to this connection. Earlier, we set session keys explicitly on the base TLS
412 // config, so they can be used for this connection too.
413 tlsConf := c.baseTLSConfig.Clone()
415 // Allow client certificate authentication, for use with the sasl "external"
416 // authentication mechanism.
417 tlsConf.ClientAuth = tls.RequestClientCert
419 // We verify the client certificate during the handshake. The TLS handshake is
420 // initiated explicitly for incoming connections and during starttls, so we can
421 // immediately extract the account name and address used for authentication.
422 tlsConf.VerifyPeerCertificate = c.tlsClientAuthVerifyPeerCert
427// tlsClientAuthVerifyPeerCert can be used as tls.Config.VerifyPeerCertificate, and
428// sets authentication-related fields on conn. This is not called on resumed TLS
430func (c *conn) tlsClientAuthVerifyPeerCert(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
431 if len(rawCerts) == 0 {
435 // If we had too many authentication failures from this IP, don't attempt
436 // authentication. If this is a new incoming connetion, it is closed after the TLS
438 if !mox.LimiterFailedAuth.CanAdd(c.remoteIP, time.Now(), 1) {
442 cert, err := x509.ParseCertificate(rawCerts[0])
444 c.log.Debugx("parsing tls client certificate", err)
447 if err := c.tlsClientAuthVerifyPeerCertParsed(cert); err != nil {
448 c.log.Debugx("verifying tls client certificate", err)
449 return fmt.Errorf("verifying client certificate: %w", err)
454// tlsClientAuthVerifyPeerCertParsed verifies a client certificate. Called both for
455// fresh and resumed TLS connections.
456func (c *conn) tlsClientAuthVerifyPeerCertParsed(cert *x509.Certificate) error {
457 if c.account != nil {
458 return fmt.Errorf("cannot authenticate with tls client certificate after previous authentication")
461 authResult := "error"
463 metrics.AuthenticationInc("submission", "tlsclientauth", authResult)
464 if authResult == "ok" {
465 mox.LimiterFailedAuth.Reset(c.remoteIP, time.Now())
467 mox.LimiterFailedAuth.Add(c.remoteIP, time.Now(), 1)
471 // For many failed auth attempts, slow down verification attempts.
472 if c.authFailed > 3 && authFailDelay > 0 {
473 mox.Sleep(mox.Context, time.Duration(c.authFailed-3)*authFailDelay)
475 c.authFailed++ // Compensated on success.
477 // On the 3rd failed authentication, start responding slowly. Successful auth will
478 // cause fast responses again.
479 if c.authFailed >= 3 {
484 shabuf := sha256.Sum256(cert.RawSubjectPublicKeyInfo)
485 fp := base64.RawURLEncoding.EncodeToString(shabuf[:])
486 pubKey, err := store.TLSPublicKeyGet(context.TODO(), fp)
488 if err == bstore.ErrAbsent {
489 authResult = "badcreds"
491 return fmt.Errorf("looking up tls public key with fingerprint %s: %v", fp, err)
494 // Verify account exists and still matches address.
495 acc, _, err := store.OpenEmail(c.log, pubKey.LoginAddress)
497 return fmt.Errorf("opening account for address %s for public key %s: %w", pubKey.LoginAddress, fp, err)
502 c.log.Check(err, "close account")
505 if acc.Name != pubKey.Account {
506 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)
512 acc = nil // Prevent cleanup by defer.
513 c.username = pubKey.LoginAddress
515 c.log.Debug("tls client authenticated with client certificate",
516 slog.String("fingerprint", fp),
517 slog.String("username", c.username),
518 slog.String("account", c.account.Name),
519 slog.Any("remote", c.remoteIP))
523// xtlsHandshakeAndAuthenticate performs the TLS handshake, and verifies a client
524// certificate if present.
525func (c *conn) xtlsHandshakeAndAuthenticate(conn net.Conn) {
526 tlsConn := tls.Server(conn, c.makeTLSConfig())
529 cidctx := context.WithValue(mox.Context, mlog.CidKey, c.cid)
530 ctx, cancel := context.WithTimeout(cidctx, time.Minute)
532 c.log.Debug("starting tls server handshake")
534 metricDeliveryStarttls.Inc()
536 if err := tlsConn.HandshakeContext(ctx); err != nil {
538 // Errors from crypto/tls mostly aren't typed. We'll have to look for strings...
540 if errors.Is(err, io.EOF) {
542 } else if alert, ok := mox.AsTLSAlert(err); ok {
543 reason = tlsrpt.FormatAlert(alert)
546 if strings.Contains(s, "tls: client offered only unsupported versions") {
547 reason = "unsupportedversions"
548 } else if strings.Contains(s, "tls: first record does not look like a TLS handshake") {
550 } else if strings.Contains(s, "tls: unsupported SSLv2 handshake received") {
554 metricDeliveryStarttlsErrors.WithLabelValues(reason).Inc()
556 panic(fmt.Errorf("tls handshake: %s (%w)", err, errIO))
560 cs := tlsConn.ConnectionState()
561 if cs.DidResume && len(cs.PeerCertificates) > 0 {
562 // Verify client after session resumption.
563 err := c.tlsClientAuthVerifyPeerCertParsed(cs.PeerCertificates[0])
565 panic(fmt.Errorf("tls verify client certificate after resumption: %s (%w)", err, errIO))
569 attrs := []slog.Attr{
570 slog.Any("version", tlsVersion(cs.Version)),
571 slog.String("ciphersuite", tls.CipherSuiteName(cs.CipherSuite)),
572 slog.String("sni", cs.ServerName),
573 slog.Bool("resumed", cs.DidResume),
574 slog.Int("clientcerts", len(cs.PeerCertificates)),
576 if c.account != nil {
577 attrs = append(attrs,
578 slog.String("account", c.account.Name),
579 slog.String("username", c.username),
582 c.log.Debug("tls handshake completed", attrs...)
585type tlsVersion uint16
587func (v tlsVersion) String() string {
588 return strings.ReplaceAll(strings.ToLower(tls.VersionName(uint16(v))), " ", "-")
591// completely reset connection state as if greeting has just been sent.
593func (c *conn) reset() {
595 c.hello = dns.IPDomain{}
598 if c.account != nil {
599 err := c.account.Close()
600 c.log.Check(err, "closing account")
608// for rset command, and a few more cases that reset the mail transaction state.
610func (c *conn) rset() {
613 c.futureRelease = time.Time{}
614 c.futureReleaseRequest = ""
615 c.has8bitmime = false
617 c.msgsmtputf8 = false
621func (c *conn) earliestDeadline(d time.Duration) time.Time {
622 e := time.Now().Add(d)
623 if !c.deadline.IsZero() && c.deadline.Before(e) {
629func (c *conn) xcheckAuth() {
630 if c.submission && c.account == nil {
632 xsmtpUserErrorf(smtp.C530SecurityRequired, smtp.SePol7Other0, "authentication required")
636func (c *conn) xtrace(level slog.Level) func() {
642 c.tr.SetTrace(mlog.LevelTrace)
643 c.tw.SetTrace(mlog.LevelTrace)
647// setSlow marks the connection slow (or now), so reads are done with 3 second
648// delay for each read, and writes are done at 1 byte per second, to try to slow
650func (c *conn) setSlow(on bool) {
652 c.log.Debug("connection changed to slow")
653 } else if !on && c.slow {
654 c.log.Debug("connection restored to regular pace")
659// Write writes to the connection. It panics on i/o errors, which is handled by the
660// connection command loop.
661func (c *conn) Write(buf []byte) (int, error) {
667 // We set a single deadline for Write and Read. This may be a TLS connection.
668 // SetDeadline works on the underlying connection. If we wouldn't touch the read
669 // deadline, and only set the write deadline and do a bunch of writes, the TLS
670 // library would still have to do reads on the underlying connection, and may reach
671 // a read deadline that was set for some earlier read.
672 // We have one deadline for the whole write. In case of slow writing, we'll write
673 // the last chunk in one go, so remote smtp clients don't abort the connection for
675 deadline := c.earliestDeadline(30 * time.Second)
676 if err := c.conn.SetDeadline(deadline); err != nil {
677 c.log.Errorx("setting deadline for write", err)
682 nn, err := c.conn.Write(buf[:chunk])
684 panic(fmt.Errorf("write: %s (%w)", err, errIO))
688 if len(buf) > 0 && badClientDelay > 0 {
689 mox.Sleep(mox.Context, badClientDelay)
691 // Make sure we don't take too long, otherwise the remote SMTP client may close the
693 if time.Until(deadline) < 5*badClientDelay {
701// Read reads from the connection. It panics on i/o errors, which is handled by the
702// connection command loop.
703func (c *conn) Read(buf []byte) (int, error) {
704 if c.slow && badClientDelay > 0 {
705 mox.Sleep(mox.Context, badClientDelay)
709 // See comment about Deadline instead of individual read/write deadlines at Write.
710 if err := c.conn.SetDeadline(c.earliestDeadline(30 * time.Second)); err != nil {
711 c.log.Errorx("setting deadline for read", err)
714 n, err := c.conn.Read(buf)
716 panic(fmt.Errorf("read: %s (%w)", err, errIO))
721// Cache of line buffers for reading commands.
723var bufpool = moxio.NewBufpool(8, 2*1024)
725func (c *conn) readline() string {
726 line, err := bufpool.Readline(c.log, c.r)
727 if err != nil && errors.Is(err, moxio.ErrLineTooLong) {
728 c.writecodeline(smtp.C500BadSyntax, smtp.SeProto5Other0, "line too long, smtp max is 512, we reached 2048", nil)
729 panic(fmt.Errorf("%s (%w)", err, errIO))
730 } else if err != nil {
731 panic(fmt.Errorf("%s (%w)", err, errIO))
736// Buffered-write command response line to connection with codes and msg.
737// Err is not sent to remote but is used for logging and can be empty.
738func (c *conn) bwritecodeline(code int, secode string, msg string, err error) {
741 ecode = fmt.Sprintf("%d.%s", code/100, secode)
743 metricCommands.WithLabelValues(c.kind(), c.cmd, fmt.Sprintf("%d", code), ecode).Observe(float64(time.Since(c.cmdStart)) / float64(time.Second))
744 c.log.Debugx("smtp command result", err,
745 slog.String("kind", c.kind()),
746 slog.String("cmd", c.cmd),
747 slog.Int("code", code),
748 slog.String("ecode", ecode),
749 slog.Duration("duration", time.Since(c.cmdStart)))
756 // Separate by newline and wrap long lines.
757 lines := strings.Split(msg, "\n")
758 for i, line := range lines {
760 var prelen = 3 + 1 + len(ecode) + len(sep)
761 for prelen+len(line) > 510 {
763 for ; e > 400 && line[e] != ' '; e-- {
765 // 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.
766 c.bwritelinef("%d-%s%s%s", code, ecode, sep, line[:e])
770 if i < len(lines)-1 {
773 c.bwritelinef("%d%s%s%s%s", code, spdash, ecode, sep, line)
777// Buffered-write a formatted response line to connection.
778func (c *conn) bwritelinef(format string, args ...any) {
779 msg := fmt.Sprintf(format, args...)
780 fmt.Fprint(c.w, msg+"\r\n")
783// Flush pending buffered writes to connection.
784func (c *conn) xflush() {
785 c.w.Flush() // Errors will have caused a panic in Write.
788// Write (with flush) a response line with codes and message. err is not written, used for logging and can be nil.
789func (c *conn) writecodeline(code int, secode string, msg string, err error) {
790 c.bwritecodeline(code, secode, msg, err)
794// Write (with flush) a formatted response line to connection.
795func (c *conn) writelinef(format string, args ...any) {
796 c.bwritelinef(format, args...)
800var cleanClose struct{} // Sentinel value for panic/recover indicating clean close of connection.
802func serve(listenerName string, cid int64, hostname dns.Domain, tlsConfig *tls.Config, nc net.Conn, resolver dns.Resolver, submission, xtls bool, maxMessageSize int64, requireTLSForAuth, requireTLSForDelivery, requireTLS bool, dnsBLs []dns.Domain, firstTimeSenderDelay time.Duration) {
803 var localIP, remoteIP net.IP
804 if a, ok := nc.LocalAddr().(*net.TCPAddr); ok {
807 // For net.Pipe, during tests.
808 localIP = net.ParseIP("127.0.0.10")
810 if a, ok := nc.RemoteAddr().(*net.TCPAddr); ok {
813 // For net.Pipe, during tests.
814 remoteIP = net.ParseIP("127.0.0.10")
821 submission: submission,
823 extRequireTLS: requireTLS,
826 baseTLSConfig: tlsConfig,
830 maxMessageSize: maxMessageSize,
831 requireTLSForAuth: requireTLSForAuth,
832 requireTLSForDelivery: requireTLSForDelivery,
834 firstTimeSenderDelay: firstTimeSenderDelay,
836 var logmutex sync.Mutex
837 c.log = mlog.New("smtpserver", nil).WithFunc(func() []slog.Attr {
839 defer logmutex.Unlock()
842 slog.Int64("cid", c.cid),
843 slog.Duration("delta", now.Sub(c.lastlog)),
846 if c.username != "" {
847 l = append(l, slog.String("username", c.username))
851 c.tr = moxio.NewTraceReader(c.log, "RC: ", c)
852 c.r = bufio.NewReader(c.tr)
853 c.tw = moxio.NewTraceWriter(c.log, "LS: ", c)
854 c.w = bufio.NewWriter(c.tw)
856 metricConnection.WithLabelValues(c.kind()).Inc()
857 c.log.Info("new connection",
858 slog.Any("remote", c.conn.RemoteAddr()),
859 slog.Any("local", c.conn.LocalAddr()),
860 slog.Bool("submission", submission),
861 slog.Bool("tls", xtls),
862 slog.String("listener", listenerName))
865 c.origConn.Close() // Close actual TCP socket, regardless of TLS on top.
866 c.conn.Close() // If TLS, will try to write alert notification to already closed socket, returning error quickly.
868 if c.account != nil {
869 err := c.account.Close()
870 c.log.Check(err, "closing account")
875 if x == nil || x == cleanClose {
876 c.log.Info("connection closed")
877 } else if err, ok := x.(error); ok && isClosed(err) {
878 c.log.Infox("connection closed", err)
880 c.log.Error("unhandled panic", slog.Any("err", x))
882 metrics.PanicInc(metrics.Smtpserver)
887 // Start TLS on connection. We perform the handshake explicitly, so we can set a
888 // timeout, do client certificate authentication, log TLS details afterwards.
889 c.xtlsHandshakeAndAuthenticate(c.conn)
893 case <-mox.Shutdown.Done():
895 c.writecodeline(smtp.C421ServiceUnavail, smtp.SeSys3NotAccepting2, "shutting down", nil)
900 if !limiterConnectionRate.Add(c.remoteIP, time.Now(), 1) {
901 c.writecodeline(smtp.C421ServiceUnavail, smtp.SePol7Other0, "connection rate from your ip or network too high, slow down please", nil)
905 // If remote IP/network resulted in too many authentication failures, refuse to serve.
906 if submission && !mox.LimiterFailedAuth.CanAdd(c.remoteIP, time.Now(), 1) {
907 metrics.AuthenticationRatelimitedInc("submission")
908 c.log.Debug("refusing connection due to many auth failures", slog.Any("remoteip", c.remoteIP))
909 c.writecodeline(smtp.C421ServiceUnavail, smtp.SePol7Other0, "too many auth failures", nil)
913 if !limiterConnections.Add(c.remoteIP, time.Now(), 1) {
914 c.log.Debug("refusing connection due to many open connections", slog.Any("remoteip", c.remoteIP))
915 c.writecodeline(smtp.C421ServiceUnavail, smtp.SePol7Other0, "too many open connections from your ip or network", nil)
918 defer limiterConnections.Add(c.remoteIP, time.Now(), -1)
920 // We register and unregister the original connection, in case c.conn is replaced
921 // with a TLS connection later on.
922 mox.Connections.Register(nc, "smtp", listenerName)
923 defer mox.Connections.Unregister(nc)
927 // We include the string ESMTP. https://cr.yp.to/smtp/greeting.html recommends it.
928 // Should not be too relevant nowadays, but does not hurt and default blackbox
929 // exporter SMTP health check expects it.
930 c.writelinef("%d %s ESMTP mox %s", smtp.C220ServiceReady, c.hostname.ASCII, moxvar.Version)
935 // If another command is present, don't flush our buffered response yet. Holding
936 // off will cause us to respond with a single packet.
939 buf, err := c.r.Peek(n)
940 if err == nil && bytes.IndexByte(buf, '\n') >= 0 {
948var commands = map[string]func(c *conn, p *parser){
949 "helo": (*conn).cmdHelo,
950 "ehlo": (*conn).cmdEhlo,
951 "starttls": (*conn).cmdStarttls,
952 "auth": (*conn).cmdAuth,
953 "mail": (*conn).cmdMail,
954 "rcpt": (*conn).cmdRcpt,
955 "data": (*conn).cmdData,
956 "rset": (*conn).cmdRset,
957 "vrfy": (*conn).cmdVrfy,
958 "expn": (*conn).cmdExpn,
959 "help": (*conn).cmdHelp,
960 "noop": (*conn).cmdNoop,
961 "quit": (*conn).cmdQuit,
964func command(c *conn) {
980 if errors.As(err, &serr) {
981 c.writecodeline(serr.code, serr.secode, fmt.Sprintf("%s (%s)", serr.errmsg, mox.ReceivedID(c.cid)), serr.err)
983 c.log.Errorx("smtp error", serr.err, slog.Int("code", serr.code), slog.String("secode", serr.secode))
987 // Other type of panic, we pass it on, aborting the connection.
988 c.log.Errorx("command panic", err)
993 // todo future: we could wait for either a line or shutdown, and just close the connection on shutdown.
996 t := strings.SplitN(line, " ", 2)
1002 cmdl := strings.ToLower(cmd)
1004 // 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
1007 case <-mox.Shutdown.Done():
1009 c.writecodeline(smtp.C421ServiceUnavail, smtp.SeSys3NotAccepting2, "shutting down", nil)
1015 c.cmdStart = time.Now()
1017 p := newParser(args, c.smtputf8, c)
1018 fn, ok := commands[cmdl]
1022 // Other side is likely speaking something else than SMTP, send error message and
1023 // stop processing because there is a good chance whatever they sent has multiple
1025 c.writecodeline(smtp.C500BadSyntax, smtp.SeProto5Syntax2, "please try again speaking smtp", nil)
1029 xsmtpUserErrorf(smtp.C500BadSyntax, smtp.SeProto5BadCmdOrSeq1, "unknown command")
1035// For use in metric labels.
1036func (c *conn) kind() string {
1043func (c *conn) xneedHello() {
1044 if c.hello.IsZero() {
1045 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "no ehlo/helo yet")
1049// If smtp server is configured to require TLS for all mail delivery (except to TLS
1050// reporting address), abort command.
1051func (c *conn) xneedTLSForDelivery(rcpt smtp.Path) {
1052 // For TLS reports, we allow the message in even without TLS, because there may be
1054 if c.requireTLSForDelivery && !c.tls && !isTLSReportRecipient(rcpt) {
1056 xsmtpUserErrorf(smtp.C530SecurityRequired, smtp.SePol7Other0, "STARTTLS required for mail delivery")
1060func isTLSReportRecipient(rcpt smtp.Path) bool {
1061 _, _, _, dest, err := mox.LookupAddress(rcpt.Localpart, rcpt.IPDomain.Domain, false, false)
1062 return err == nil && (dest.HostTLSReports || dest.DomainTLSReports)
1065func (c *conn) cmdHelo(p *parser) {
1066 c.cmdHello(p, false)
1069func (c *conn) cmdEhlo(p *parser) {
1074func (c *conn) cmdHello(p *parser, ehlo bool) {
1075 var remote dns.IPDomain
1076 if c.submission && !mox.Pedantic {
1077 // Mail clients regularly put bogus information in the hostname/ip. For submission,
1078 // the value is of no use, so there is not much point in annoying the user with
1079 // errors they cannot fix themselves. Except when in pedantic mode.
1080 remote = dns.IPDomain{IP: c.remoteIP}
1084 remote = p.xipdomain(true)
1086 remote = dns.IPDomain{Domain: p.xdomain()}
1088 // Verify a remote domain name has an A or AAAA record, CNAME not allowed.
../rfc/5321:722
1089 cidctx := context.WithValue(mox.Context, mlog.CidKey, c.cid)
1090 ctx, cancel := context.WithTimeout(cidctx, time.Minute)
1091 _, _, err := c.resolver.LookupIPAddr(ctx, remote.Domain.ASCII+".")
1093 if dns.IsNotFound(err) {
1094 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeProto5Other0, "your ehlo domain does not resolve to an IP address")
1096 // For success or temporary resolve errors, we'll just continue.
1099 // Though a few paragraphs earlier is a claim additional data can occur for address
1100 // literals (IP addresses), although the ABNF in that document does not allow it.
1101 // We allow additional text, but only if space-separated.
1102 if len(remote.IP) > 0 && p.space() {
1114 // https://www.iana.org/assignments/mail-parameters/mail-parameters.xhtml
1116 c.bwritelinef("250-%s", c.hostname.ASCII)
1120 if !c.tls && c.baseTLSConfig != nil {
1122 c.bwritelinef("250-STARTTLS")
1123 } else if c.extRequireTLS {
1126 c.bwritelinef("250-REQUIRETLS")
1131 if c.tls || !c.requireTLSForAuth {
1132 // We always mention the SCRAM PLUS variants, even if TLS is not active: It is a
1133 // hint to the client that a TLS connection can use TLS channel binding during
1134 // authentication. The client should select the bare variant when TLS isn't
1135 // present, and also not indicate the server supports the PLUS variant in that
1136 // case, or it would trigger the mechanism downgrade detection.
1137 mechs = "SCRAM-SHA-256-PLUS SCRAM-SHA-256 SCRAM-SHA-1-PLUS SCRAM-SHA-1 CRAM-MD5 PLAIN LOGIN"
1139 if c.tls && len(c.conn.(*tls.Conn).ConnectionState().PeerCertificates) > 0 {
1140 mechs = "EXTERNAL " + mechs
1142 c.bwritelinef("250-AUTH %s", mechs)
1145 c.bwritelinef("250-FUTURERELEASE %d %s", queue.FutureReleaseIntervalMax/time.Second, t.Format(time.RFC3339))
1148 // todo future? c.writelinef("250-DSN")
1156func (c *conn) cmdStarttls(p *parser) {
1162 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "already speaking tls")
1164 if c.account != nil {
1165 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "cannot starttls after authentication")
1167 if c.baseTLSConfig == nil {
1168 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "starttls not offered")
1171 // We don't want to do TLS on top of c.r because it also prints protocol traces: We
1172 // don't want to log the TLS stream. So we'll do TLS on the underlying connection,
1173 // but make sure any bytes already read and in the buffer are used for the TLS
1176 if n := c.r.Buffered(); n > 0 {
1177 conn = &moxio.PrefixConn{
1178 PrefixReader: io.LimitReader(c.r, int64(n)),
1183 // We add the cid to the output, to help debugging in case of a failing TLS connection.
1184 c.writecodeline(smtp.C220ServiceReady, smtp.SeOther00, "go! ("+mox.ReceivedID(c.cid)+")", nil)
1186 c.xtlsHandshakeAndAuthenticate(conn)
1193func (c *conn) cmdAuth(p *parser) {
1197 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "authentication only allowed on submission ports")
1201 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "already authenticated")
1203 if c.mailFrom != nil {
1205 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "authentication not allowed during mail transaction")
1208 // If authentication fails due to missing derived secrets, we don't hold it against
1209 // the connection. There is no way to indicate server support for an authentication
1210 // mechanism, but that a mechanism won't work for an account.
1211 var missingDerivedSecrets bool
1213 // For many failed auth attempts, slow down verification attempts.
1214 // Dropping the connection could also work, but more so when we have a connection rate limiter.
1216 if c.authFailed > 3 && authFailDelay > 0 {
1218 mox.Sleep(mox.Context, time.Duration(c.authFailed-3)*authFailDelay)
1220 c.authFailed++ // Compensated on success.
1222 if missingDerivedSecrets {
1225 // On the 3rd failed authentication, start responding slowly. Successful auth will
1226 // cause fast responses again.
1227 if c.authFailed >= 3 {
1232 var authVariant string // Only known strings, used in metrics.
1233 authResult := "error"
1235 metrics.AuthenticationInc("submission", authVariant, authResult)
1236 if authResult == "ok" {
1237 mox.LimiterFailedAuth.Reset(c.remoteIP, time.Now())
1238 } else if !missingDerivedSecrets {
1239 mox.LimiterFailedAuth.Add(c.remoteIP, time.Now(), 1)
1245 mech := p.xsaslMech()
1247 // Read the first parameter, either as initial parameter or by sending a
1248 // continuation with the optional encChal (must already be base64-encoded).
1249 xreadInitial := func(encChal string) []byte {
1253 // todo future: handle max length of 12288 octets and return proper responde codes otherwise
../rfc/4954:253
1257 authResult = "aborted"
1258 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5Other0, "authentication aborted")
1263 // Windows Mail 16005.14326.21606.0 sends two spaces between "AUTH PLAIN" and the
1268 auth = p.remainder()
1271 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5Syntax2, "missing initial auth base64 parameter after space")
1272 } else if auth == "=" {
1274 auth = "" // Base64 decode below will result in empty buffer.
1277 buf, err := base64.StdEncoding.DecodeString(auth)
1280 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5Syntax2, "invalid base64: %s", err)
1285 xreadContinuation := func() []byte {
1286 line := c.readline()
1288 authResult = "aborted"
1289 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5Other0, "authentication aborted")
1291 buf, err := base64.StdEncoding.DecodeString(line)
1294 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5Syntax2, "invalid base64: %s", err)
1299 // The various authentication mechanisms set account and username. We may already
1300 // have an account and username from TLS client authentication. Afterwards, we
1301 // check that the account is the same.
1302 var account *store.Account
1306 err := account.Close()
1307 c.log.Check(err, "close account")
1313 authVariant = "plain"
1317 if !c.tls && c.requireTLSForAuth {
1318 xsmtpUserErrorf(smtp.C538EncReqForAuth, smtp.SePol7EncReqForAuth11, "authentication requires tls")
1321 // Password is in line in plain text, so hide it.
1322 defer c.xtrace(mlog.LevelTraceauth)()
1323 buf := xreadInitial("")
1324 c.xtrace(mlog.LevelTrace) // Restore.
1325 plain := bytes.Split(buf, []byte{0})
1326 if len(plain) != 3 {
1327 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5BadParams4, "auth data should have 3 nul-separated tokens, got %d", len(plain))
1329 authz := norm.NFC.String(string(plain[0]))
1330 username = norm.NFC.String(string(plain[1]))
1331 password := string(plain[2])
1333 if authz != "" && authz != username {
1334 authResult = "badcreds"
1335 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "cannot assume other role")
1339 account, err = store.OpenEmailAuth(c.log, username, password)
1340 if err != nil && errors.Is(err, store.ErrUnknownCredentials) {
1342 authResult = "badcreds"
1343 c.log.Info("failed authentication attempt", slog.String("username", username), slog.Any("remote", c.remoteIP))
1344 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass")
1346 xcheckf(err, "verifying credentials")
1349 // LOGIN is obsoleted in favor of PLAIN, only implemented to support legacy
1350 // clients, see Internet-Draft (I-D):
1351 // https://datatracker.ietf.org/doc/html/draft-murchison-sasl-login-00
1353 authVariant = "login"
1357 if !c.tls && c.requireTLSForAuth {
1358 xsmtpUserErrorf(smtp.C538EncReqForAuth, smtp.SePol7EncReqForAuth11, "authentication requires tls")
1361 // Read user name. The I-D says the client should ignore the server challenge, but
1362 // also that some clients may require challenge "Username:" instead of "User
1363 // Name". We can't sent both... Servers most commonly return "Username:" and
1364 // "Password:", so we do the same.
1365 // I-D says maximum length must be 64 bytes. We allow more, for long user names
1367 encChal := base64.StdEncoding.EncodeToString([]byte("Username:"))
1368 username = string(xreadInitial(encChal))
1369 username = norm.NFC.String(username)
1371 // Again, client should ignore the challenge, we send the same as the example in
1373 c.writelinef("%d %s", smtp.C334ContinueAuth, base64.StdEncoding.EncodeToString([]byte("Password:")))
1375 // Password is in line in plain text, so hide it.
1376 defer c.xtrace(mlog.LevelTraceauth)()
1377 password := string(xreadContinuation())
1378 c.xtrace(mlog.LevelTrace) // Restore.
1381 account, err = store.OpenEmailAuth(c.log, username, password)
1382 if err != nil && errors.Is(err, store.ErrUnknownCredentials) {
1384 authResult = "badcreds"
1385 c.log.Info("failed authentication attempt", slog.String("username", username), slog.Any("remote", c.remoteIP))
1386 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass")
1388 xcheckf(err, "verifying credentials")
1391 authVariant = strings.ToLower(mech)
1396 chal := fmt.Sprintf("<%d.%d@%s>", uint64(mox.CryptoRandInt()), time.Now().UnixNano(), mox.Conf.Static.HostnameDomain.ASCII)
1397 c.writelinef("%d %s", smtp.C334ContinueAuth, base64.StdEncoding.EncodeToString([]byte(chal)))
1399 resp := xreadContinuation()
1400 t := strings.Split(string(resp), " ")
1401 if len(t) != 2 || len(t[1]) != 2*md5.Size {
1402 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5BadParams4, "malformed cram-md5 response")
1404 username = norm.NFC.String(t[0])
1405 c.log.Debug("cram-md5 auth", slog.String("username", username))
1407 account, _, err = store.OpenEmail(c.log, username)
1408 if err != nil && errors.Is(err, store.ErrUnknownCredentials) {
1409 c.log.Info("failed authentication attempt", slog.String("username", username), slog.Any("remote", c.remoteIP))
1410 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass")
1412 xcheckf(err, "looking up address")
1413 var ipadhash, opadhash hash.Hash
1414 account.WithRLock(func() {
1415 err := account.DB.Read(context.TODO(), func(tx *bstore.Tx) error {
1416 password, err := bstore.QueryTx[store.Password](tx).Get()
1417 if err == bstore.ErrAbsent {
1418 c.log.Info("failed authentication attempt", slog.String("username", username), slog.Any("remote", c.remoteIP))
1419 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass")
1425 ipadhash = password.CRAMMD5.Ipad
1426 opadhash = password.CRAMMD5.Opad
1429 xcheckf(err, "tx read")
1431 if ipadhash == nil || opadhash == nil {
1432 missingDerivedSecrets = true
1433 c.log.Info("cram-md5 auth attempt without derived secrets set, save password again to store secrets", slog.String("username", username))
1434 c.log.Info("failed authentication attempt", slog.String("username", username), slog.Any("remote", c.remoteIP))
1435 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass")
1439 ipadhash.Write([]byte(chal))
1440 opadhash.Write(ipadhash.Sum(nil))
1441 digest := fmt.Sprintf("%x", opadhash.Sum(nil))
1443 c.log.Info("failed authentication attempt", slog.String("username", username), slog.Any("remote", c.remoteIP))
1444 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass")
1447 case "SCRAM-SHA-256-PLUS", "SCRAM-SHA-256", "SCRAM-SHA-1-PLUS", "SCRAM-SHA-1":
1448 // 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?
1449 // todo: use single implementation between ../imapserver/server.go and ../smtpserver/server.go
1451 // Passwords cannot be retrieved or replayed from the trace.
1453 authVariant = strings.ToLower(mech)
1454 var h func() hash.Hash
1455 switch authVariant {
1456 case "scram-sha-1", "scram-sha-1-plus":
1458 case "scram-sha-256", "scram-sha-256-plus":
1461 xsmtpServerErrorf(codes{smtp.C554TransactionFailed, smtp.SeSys3Other0}, "missing scram auth method case")
1464 var cs *tls.ConnectionState
1465 channelBindingRequired := strings.HasSuffix(authVariant, "-plus")
1466 if channelBindingRequired && !c.tls {
1468 xsmtpUserErrorf(smtp.C538EncReqForAuth, smtp.SePol7EncReqForAuth11, "scram plus mechanism requires tls connection")
1471 xcs := c.conn.(*tls.Conn).ConnectionState()
1474 c0 := xreadInitial("")
1475 ss, err := scram.NewServer(h, c0, cs, channelBindingRequired)
1477 c.log.Infox("scram protocol error", err, slog.Any("remote", c.remoteIP))
1478 xsmtpUserErrorf(smtp.C455BadParams, smtp.SePol7Other0, "scram protocol error: %s", err)
1480 username = norm.NFC.String(ss.Authentication)
1481 c.log.Debug("scram auth", slog.String("authentication", username))
1482 account, _, err = store.OpenEmail(c.log, username)
1484 // todo: we could continue scram with a generated salt, deterministically generated
1485 // from the username. that way we don't have to store anything but attackers cannot
1486 // learn if an account exists. same for absent scram saltedpassword below.
1487 c.log.Info("failed authentication attempt", slog.String("username", username), slog.Any("remote", c.remoteIP))
1488 xsmtpUserErrorf(smtp.C454TempAuthFail, smtp.SeSys3Other0, "scram not possible")
1490 if ss.Authorization != "" && ss.Authorization != username {
1491 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "authentication with authorization for different user not supported")
1493 var xscram store.SCRAM
1494 account.WithRLock(func() {
1495 err := account.DB.Read(context.TODO(), func(tx *bstore.Tx) error {
1496 password, err := bstore.QueryTx[store.Password](tx).Get()
1497 if err == bstore.ErrAbsent {
1498 c.log.Info("failed authentication attempt", slog.String("username", username), slog.Any("remote", c.remoteIP))
1499 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass")
1501 xcheckf(err, "fetching credentials")
1502 switch authVariant {
1503 case "scram-sha-1", "scram-sha-1-plus":
1504 xscram = password.SCRAMSHA1
1505 case "scram-sha-256", "scram-sha-256-plus":
1506 xscram = password.SCRAMSHA256
1508 xsmtpServerErrorf(codes{smtp.C554TransactionFailed, smtp.SeSys3Other0}, "missing scram auth credentials case")
1510 if len(xscram.Salt) == 0 || xscram.Iterations == 0 || len(xscram.SaltedPassword) == 0 {
1511 missingDerivedSecrets = true
1512 c.log.Info("scram auth attempt without derived secrets set, save password again to store secrets", slog.String("address", username))
1513 c.log.Info("failed authentication attempt", slog.String("username", username), slog.Any("remote", c.remoteIP))
1514 xsmtpUserErrorf(smtp.C454TempAuthFail, smtp.SeSys3Other0, "scram not possible")
1518 xcheckf(err, "read tx")
1520 s1, err := ss.ServerFirst(xscram.Iterations, xscram.Salt)
1521 xcheckf(err, "scram first server step")
1522 c.writelinef("%d %s", smtp.C334ContinueAuth, base64.StdEncoding.EncodeToString([]byte(s1))) //
../rfc/4954:187
1523 c2 := xreadContinuation()
1524 s3, err := ss.Finish(c2, xscram.SaltedPassword)
1526 c.writelinef("%d %s", smtp.C334ContinueAuth, base64.StdEncoding.EncodeToString([]byte(s3))) //
../rfc/4954:187
1529 c.readline() // Should be "*" for cancellation.
1530 if errors.Is(err, scram.ErrInvalidProof) {
1531 authResult = "badcreds"
1532 c.log.Info("failed authentication attempt", slog.String("username", username), slog.Any("remote", c.remoteIP))
1533 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad credentials")
1534 } else if errors.Is(err, scram.ErrChannelBindingsDontMatch) {
1535 authResult = "badchanbind"
1536 c.log.Warn("bad channel binding during authentication, potential mitm", slog.String("username", username), slog.Any("remote", c.remoteIP))
1537 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7MsgIntegrity7, "channel bindings do not match, potential mitm")
1538 } else if errors.Is(err, scram.ErrInvalidEncoding) {
1539 c.log.Infox("bad scram protocol message", err, slog.String("username", username), slog.Any("remote", c.remoteIP))
1540 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7Other0, "bad scram protocol message")
1542 xcheckf(err, "server final")
1546 // The message should be empty. todo: should we require it is empty?
1550 authVariant = strings.ToLower(mech)
1553 buf := xreadInitial("")
1554 username = string(buf)
1558 xsmtpUserErrorf(smtp.C538EncReqForAuth, smtp.SePol7EncReqForAuth11, "tls required for tls client certificate authentication")
1560 if c.account == nil {
1561 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "missing client certificate, required for tls client certificate authentication")
1565 username = c.username
1568 account, _, err = store.OpenEmail(c.log, username)
1569 xcheckf(err, "looking up username from tls client authentication")
1573 xsmtpUserErrorf(smtp.C504ParamNotImpl, smtp.SeProto5BadParams4, "mechanism %s not supported", mech)
1576 // We may already have TLS credentials. We allow an additional SASL authentication,
1577 // possibly with different username, but the account must be the same.
1578 if c.account != nil {
1579 if account != c.account {
1580 c.log.Debug("sasl authentication for different account than tls client authentication, aborting connection",
1581 slog.String("saslmechanism", authVariant),
1582 slog.String("saslaccount", account.Name),
1583 slog.String("tlsaccount", c.account.Name),
1584 slog.String("saslusername", username),
1585 slog.String("tlsusername", c.username),
1587 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "authentication failed, tls client certificate public key belongs to another account")
1588 } else if username != c.username {
1589 c.log.Debug("sasl authentication for different username than tls client certificate authentication, switching to sasl username",
1590 slog.String("saslmechanism", authVariant),
1591 slog.String("saslusername", username),
1592 slog.String("tlsusername", c.username),
1593 slog.String("account", c.account.Name),
1598 account = nil // Prevent cleanup.
1600 c.username = username
1607 c.writecodeline(smtp.C235AuthSuccess, smtp.SePol7Other0, "nice", nil)
1611func (c *conn) cmdMail(p *parser) {
1612 // requirements for maximum line length:
1614 // todo future: enforce? doesn't really seem worth it...
1616 if c.transactionBad > 10 && c.transactionGood == 0 {
1617 // If we get many bad transactions, it's probably a spammer that is guessing user names.
1618 // Useful in combination with rate limiting.
1620 c.writecodeline(smtp.C550MailboxUnavail, smtp.SeAddr1Other0, "too many failures", nil)
1626 if c.mailFrom != nil {
1628 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "already have MAIL")
1630 // Ensure clear transaction state on failure.
1641 // Microsoft Outlook 365 Apps for Enterprise sends it with submission. For delivery
1642 // it is mostly used by spammers, but has been seen with legitimate senders too.
1646 rawRevPath := p.xrawReversePath()
1647 paramSeen := map[string]bool{}
1650 key := p.xparamKeyword()
1652 K := strings.ToUpper(key)
1655 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5BadParams4, "duplicate param %q", key)
1663 if size > c.maxMessageSize {
1665 ecode := smtp.SeSys3MsgLimitExceeded4
1666 if size < config.DefaultMaxMsgSize {
1667 ecode = smtp.SeMailbox2MsgLimitExceeded3
1669 xsmtpUserErrorf(smtp.C552MailboxFull, ecode, "message too large")
1671 // We won't verify the message is exactly the size the remote claims. Buf if it is
1672 // larger, we'll abort the transaction when remote crosses the boundary.
1676 v := p.xparamValue()
1677 switch strings.ToUpper(v) {
1679 c.has8bitmime = false
1681 c.has8bitmime = true
1683 xsmtpUserErrorf(smtp.C555UnrecognizedAddrParams, smtp.SeProto5BadParams4, "unrecognized parameter %q", key)
1688 // We act as if we don't trust the client to specify a mailbox. Instead, we always
1689 // check the rfc5321.mailfrom and rfc5322.from before accepting the submission.
1693 // 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
1701 c.msgsmtputf8 = true
1705 xsmtpUserErrorf(smtp.C530SecurityRequired, smtp.SePol7EncNeeded10, "requiretls only allowed on tls-encrypted connections")
1706 } else if !c.extRequireTLS {
1707 xsmtpUserErrorf(smtp.C555UnrecognizedAddrParams, smtp.SeSys3NotSupported3, "REQUIRETLS not allowed for this connection")
1711 case "HOLDFOR", "HOLDUNTIL":
1714 xsmtpUserErrorf(smtp.C555UnrecognizedAddrParams, smtp.SeSys3NotSupported3, "unrecognized parameter %q", key)
1716 if K == "HOLDFOR" && paramSeen["HOLDUNTIL"] || K == "HOLDUNTIL" && paramSeen["HOLDFOR"] {
1718 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5BadParams4, "cannot use both HOLDUNTIL and HOLFOR")
1722 // semantic errors as syntax errors
1725 if n > int64(queue.FutureReleaseIntervalMax/time.Second) {
1727 xsmtpUserErrorf(smtp.C554TransactionFailed, smtp.SeProto5BadParams4, "future release interval too far in the future")
1729 c.futureRelease = time.Now().Add(time.Duration(n) * time.Second)
1730 c.futureReleaseRequest = fmt.Sprintf("for;%d", n)
1732 t, s := p.xdatetimeutc()
1733 ival := time.Until(t)
1735 // Likely a mistake by the user.
1736 xsmtpUserErrorf(smtp.C554TransactionFailed, smtp.SeProto5BadParams4, "requested future release time is in the past")
1737 } else if ival > queue.FutureReleaseIntervalMax {
1739 xsmtpUserErrorf(smtp.C554TransactionFailed, smtp.SeProto5BadParams4, "requested future release time is too far in the future")
1742 c.futureReleaseRequest = "until;" + s
1746 xsmtpUserErrorf(smtp.C555UnrecognizedAddrParams, smtp.SeSys3NotSupported3, "unrecognized parameter %q", key)
1750 // We now know if we have to parse the address with support for utf8.
1751 pp := newParser(rawRevPath, c.smtputf8, c)
1752 rpath := pp.xbareReversePath()
1757 // For submission, check if reverse path is allowed. I.e. authenticated account
1758 // must have the rpath configured. We do a check again on rfc5322.from during DATA.
1759 // Mail clients may use the alias address as smtp mail from address, so we allow it
1760 // for such aliases.
1761 rpathAllowed := func() bool {
1767 from := smtp.NewAddress(rpath.Localpart, rpath.IPDomain.Domain)
1768 return mox.AllowMsgFrom(c.account.Name, from)
1771 if !c.submission && !rpath.IPDomain.Domain.IsZero() {
1772 // If rpath domain has null MX record or is otherwise not accepting email, reject.
1775 cidctx := context.WithValue(mox.Context, mlog.CidKey, c.cid)
1776 ctx, cancel := context.WithTimeout(cidctx, time.Minute)
1777 valid, err := checkMXRecords(ctx, c.resolver, rpath.IPDomain.Domain)
1780 c.log.Infox("temporary reject for temporary mx lookup error", err)
1781 xsmtpServerErrorf(codes{smtp.C451LocalErr, smtp.SeNet4Other0}, "cannot verify mx records for mailfrom domain")
1783 c.log.Info("permanent reject because mailfrom domain does not accept mail")
1784 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SePol7SenderHasNullMX27, "mailfrom domain not configured for mail")
1788 if c.submission && (len(rpath.IPDomain.IP) > 0 || !rpathAllowed()) {
1790 c.log.Info("submission with unconfigured mailfrom", slog.String("user", c.username), slog.String("mailfrom", rpath.String()))
1791 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SePol7DeliveryUnauth1, "must match authenticated user")
1792 } else if !c.submission && len(rpath.IPDomain.IP) > 0 {
1793 // todo future: allow if the IP is the same as this connection is coming from? does later code allow this?
1794 c.log.Info("delivery from address without domain", slog.String("mailfrom", rpath.String()))
1795 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SePol7Other0, "domain name required")
1798 if Localserve && strings.HasPrefix(string(rpath.Localpart), "mailfrom") {
1799 c.xlocalserveError(rpath.Localpart)
1804 c.bwritecodeline(smtp.C250Completed, smtp.SeAddr1Other0, "looking good", nil)
1808func (c *conn) cmdRcpt(p *parser) {
1811 if c.mailFrom == nil {
1813 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "missing MAIL FROM")
1819 // Microsoft Outlook 365 Apps for Enterprise sends it with submission. For delivery
1820 // it is mostly used by spammers, but has been seen with legitimate senders too.
1825 if p.take("<POSTMASTER>") {
1826 fpath = smtp.Path{Localpart: "postmaster"}
1828 fpath = p.xforwardPath()
1832 key := p.xparamKeyword()
1833 // K := strings.ToUpper(key)
1836 xsmtpUserErrorf(smtp.C555UnrecognizedAddrParams, smtp.SeSys3NotSupported3, "unrecognized parameter %q", key)
1840 // Check if TLS is enabled if required. It's not great that sender/recipient
1841 // addresses may have been exposed in plaintext before we can reject delivery. The
1842 // recipient could be the tls reporting addresses, which must always be able to
1843 // receive in plain text.
1844 c.xneedTLSForDelivery(fpath)
1846 // todo future: for submission, should we do explicit verification that domains are fully qualified? also for mail from.
../rfc/6409:420
1848 if len(c.recipients) >= rcptToLimit {
1850 xsmtpUserErrorf(smtp.C452StorageFull, smtp.SeProto5TooManyRcpts3, "max of %d recipients reached", rcptToLimit)
1853 // We don't want to allow delivery to multiple recipients with a null reverse path.
1854 // Why would anyone send like that? Null reverse path is intended for delivery
1855 // notifications, they should go to a single recipient.
1856 if !c.submission && len(c.recipients) > 0 && c.mailFrom.IsZero() {
1857 xsmtpUserErrorf(smtp.C452StorageFull, smtp.SeProto5TooManyRcpts3, "only one recipient allowed with null reverse address")
1860 // Do not accept multiple recipients if remote does not pass SPF. Because we don't
1861 // want to generate DSNs to unverified domains. This is the moment we
1862 // can refuse individual recipients, DATA will be too late. Because mail
1863 // servers must handle a max recipient limit gracefully and still send to the
1864 // recipients that are accepted, this should not cause problems. Though we are in
1865 // violation because the limit must be >= 100.
1869 if !c.submission && len(c.recipients) == 1 && !Localserve {
1870 // note: because of check above, mailFrom cannot be the null address.
1872 d := c.mailFrom.IPDomain.Domain
1874 // todo: use this spf result for DATA.
1875 spfArgs := spf.Args{
1876 RemoteIP: c.remoteIP,
1877 MailFromLocalpart: c.mailFrom.Localpart,
1879 HelloDomain: c.hello,
1881 LocalHostname: c.hostname,
1883 cidctx := context.WithValue(mox.Context, mlog.CidKey, c.cid)
1884 spfctx, spfcancel := context.WithTimeout(cidctx, time.Minute)
1886 receivedSPF, _, _, _, err := spf.Verify(spfctx, c.log.Logger, c.resolver, spfArgs)
1889 c.log.Errorx("spf verify for multiple recipients", err)
1891 pass = receivedSPF.Identity == spf.ReceivedMailFrom && receivedSPF.Result == spf.StatusPass
1894 xsmtpUserErrorf(smtp.C452StorageFull, smtp.SeProto5TooManyRcpts3, "only one recipient allowed without spf pass")
1898 if Localserve && strings.HasPrefix(string(fpath.Localpart), "rcptto") {
1899 c.xlocalserveError(fpath.Localpart)
1902 if len(fpath.IPDomain.IP) > 0 {
1904 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeAddr1UnknownDestMailbox1, "not accepting email for ip")
1906 c.recipients = append(c.recipients, recipient{fpath, nil, nil})
1907 } else if accountName, alias, canonical, addr, err := mox.LookupAddress(fpath.Localpart, fpath.IPDomain.Domain, true, true); err == nil {
1910 c.recipients = append(c.recipients, recipient{fpath, nil, &rcptAlias{*alias, canonical}})
1912 c.recipients = append(c.recipients, recipient{fpath, &rcptAccount{accountName, addr, canonical}, nil})
1915 } else if Localserve {
1916 // If the address isn't known, and we are in localserve, deliver to the mox user.
1917 // If account or destination doesn't exist, it will be handled during delivery. For
1918 // submissions, which is the common case, we'll deliver to the logged in user,
1919 // which is typically the mox user.
1920 acc, _ := mox.Conf.Account("mox")
1921 dest := acc.Destinations["mox@localhost"]
1922 c.recipients = append(c.recipients, recipient{fpath, &rcptAccount{"mox", dest, "mox@localhost"}, nil})
1923 } else if errors.Is(err, mox.ErrDomainNotFound) {
1925 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeAddr1UnknownDestMailbox1, "not accepting email for domain")
1927 // We'll be delivering this email.
1928 c.recipients = append(c.recipients, recipient{fpath, nil, nil})
1929 } else if errors.Is(err, mox.ErrAddressNotFound) {
1931 // For submission, we're transparent about which user exists. Should be fine for the typical small-scale deploy.
1933 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeAddr1UnknownDestMailbox1, "no such user")
1935 // We pretend to accept. We don't want to let remote know the user does not exist
1936 // until after DATA. Because then remote has committed to sending a message.
1937 // note: not local for !c.submission is the signal this address is in error.
1938 c.recipients = append(c.recipients, recipient{fpath, nil, nil})
1940 c.log.Errorx("looking up account for delivery", err, slog.Any("rcptto", fpath))
1941 xsmtpServerErrorf(codes{smtp.C451LocalErr, smtp.SeSys3Other0}, "error processing")
1943 c.bwritecodeline(smtp.C250Completed, smtp.SeAddr1Other0, "now on the list", nil)
1946func hasNonASCII(s string) bool {
1947 for _, c := range []byte(s) {
1948 if c > unicode.MaxASCII {
1956func (c *conn) isSMTPUTF8Required(part *message.Part) bool {
1957 // Check "MAIL FROM".
1958 if hasNonASCII(string(c.mailFrom.Localpart)) {
1961 // Check all "RCPT TO".
1962 for _, rcpt := range c.recipients {
1963 if hasNonASCII(string(rcpt.Addr.Localpart)) {
1968 // Check header in all message parts.
1969 smtputf8, err := part.NeedsSMTPUTF8()
1970 xcheckf(err, "checking if smtputf8 is required")
1975func (c *conn) cmdData(p *parser) {
1978 if c.mailFrom == nil {
1980 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "missing MAIL FROM")
1982 if len(c.recipients) == 0 {
1984 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "missing RCPT TO")
1990 // 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.
1992 // Entire delivery should be done within 30 minutes, or we abort.
1993 cidctx := context.WithValue(mox.Context, mlog.CidKey, c.cid)
1994 cmdctx, cmdcancel := context.WithTimeout(cidctx, 30*time.Minute)
1996 // Deadline is taken into account by Read and Write.
1997 c.deadline, _ = cmdctx.Deadline()
1999 c.deadline = time.Time{}
2003 c.writelinef("354 see you at the bare dot")
2005 // Mark as tracedata.
2006 defer c.xtrace(mlog.LevelTracedata)()
2008 // We read the data into a temporary file. We limit the size and do basic analysis while reading.
2009 dataFile, err := store.CreateMessageTemp(c.log, "smtp-deliver")
2011 xsmtpServerErrorf(errCodes(smtp.C451LocalErr, smtp.SeSys3Other0, err), "creating temporary file for message: %s", err)
2013 defer store.CloseRemoveTempFile(c.log, dataFile, "smtpserver delivered message")
2014 msgWriter := message.NewWriter(dataFile)
2015 dr := smtp.NewDataReader(c.r)
2016 n, err := io.Copy(&limitWriter{maxSize: c.maxMessageSize, w: msgWriter}, dr)
2017 c.xtrace(mlog.LevelTrace) // Restore.
2019 if errors.Is(err, errMessageTooLarge) {
2021 ecode := smtp.SeSys3MsgLimitExceeded4
2022 if n < config.DefaultMaxMsgSize {
2023 ecode = smtp.SeMailbox2MsgLimitExceeded3
2025 c.writecodeline(smtp.C451LocalErr, ecode, fmt.Sprintf("error copying data to file (%s)", mox.ReceivedID(c.cid)), err)
2026 panic(fmt.Errorf("remote sent too much DATA: %w", errIO))
2029 if errors.Is(err, smtp.ErrCRLF) {
2030 c.writecodeline(smtp.C500BadSyntax, smtp.SeProto5Syntax2, fmt.Sprintf("invalid bare \\r or \\n, may be smtp smuggling (%s)", mox.ReceivedID(c.cid)), err)
2034 // Something is failing on our side. We want to let remote know. So write an error response,
2035 // then discard the remaining data so the remote client is more likely to see our
2036 // response. Our write is synchronous, there is a risk no window/buffer space is
2037 // available and our write blocks us from reading remaining data, leading to
2038 // deadlock. We have a timeout on our connection writes though, so worst case we'll
2039 // abort the connection due to expiration.
2040 c.writecodeline(smtp.C451LocalErr, smtp.SeSys3Other0, fmt.Sprintf("error copying data to file (%s)", mox.ReceivedID(c.cid)), err)
2041 io.Copy(io.Discard, dr)
2045 // Basic sanity checks on messages before we send them out to the world. Just
2046 // trying to be strict in what we do to others and liberal in what we accept.
2048 if !msgWriter.HaveBody {
2050 xsmtpUserErrorf(smtp.C554TransactionFailed, smtp.SeMsg6Other0, "message requires both header and body section")
2052 // Check only for pedantic mode because ios mail will attempt to send smtputf8 with
2053 // non-ascii in message from localpart without using 8bitmime.
2054 if mox.Pedantic && msgWriter.Has8bit && !c.has8bitmime {
2056 xsmtpUserErrorf(smtp.C500BadSyntax, smtp.SeMsg6Other0, "message with non-us-ascii requires 8bitmime extension")
2060 if Localserve && mox.Pedantic {
2061 // Require that message can be parsed fully.
2062 p, err := message.Parse(c.log.Logger, false, dataFile)
2064 err = p.Walk(c.log.Logger, nil)
2068 xsmtpUserErrorf(smtp.C554TransactionFailed, smtp.SeMsg6Other0, "malformed message: %v", err)
2072 // Now that we have all the whole message (envelope + data), we can check if the SMTPUTF8 extension is required.
2073 var part *message.Part
2074 if c.smtputf8 || c.submission || mox.Pedantic {
2075 // Try to parse the message.
2076 // Do nothing if something bad happen during Parse and Walk, just keep the current value for c.msgsmtputf8.
2077 p, err := message.Parse(c.log.Logger, true, dataFile)
2079 // Message parsed without error. Keep the result to avoid parsing the message again.
2081 err = part.Walk(c.log.Logger, nil)
2083 c.msgsmtputf8 = c.isSMTPUTF8Required(part)
2087 c.log.Debugx("parsing message for smtputf8 check", err)
2089 if c.smtputf8 != c.msgsmtputf8 {
2090 c.log.Debug("smtputf8 flag changed", slog.Bool("smtputf8", c.smtputf8), slog.Bool("msgsmtputf8", c.msgsmtputf8))
2093 if !c.smtputf8 && c.msgsmtputf8 && mox.Pedantic {
2094 metricSubmission.WithLabelValues("missingsmtputf8").Inc()
2095 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeMsg6Other0, "smtputf8 extension is required but was not added to the MAIL command")
2098 // Prepare "Received" header.
2102 var iprevStatus iprev.Status // Only for delivery, not submission.
2103 var iprevAuthentic bool
2105 // Hide internal hosts.
2106 // todo future: make this a config option, where admins specify ip ranges that they don't want exposed. also see
../rfc/5321:4321
2107 recvFrom = message.HeaderCommentDomain(mox.Conf.Static.HostnameDomain, c.msgsmtputf8)
2109 if len(c.hello.IP) > 0 {
2110 recvFrom = smtp.AddressLiteral(c.hello.IP)
2112 // ASCII-only version added after the extended-domain syntax below, because the
2113 // comment belongs to "BY" which comes immediately after "FROM".
2114 recvFrom = c.hello.Domain.XName(c.msgsmtputf8)
2116 iprevctx, iprevcancel := context.WithTimeout(cmdctx, time.Minute)
2118 var revNames []string
2119 iprevStatus, revName, revNames, iprevAuthentic, err = iprev.Lookup(iprevctx, c.resolver, c.remoteIP)
2122 c.log.Infox("reverse-forward lookup", err, slog.Any("remoteip", c.remoteIP))
2124 c.log.Debug("dns iprev check", slog.Any("addr", c.remoteIP), slog.Any("status", iprevStatus))
2128 } else if len(revNames) > 0 {
2131 name = strings.TrimSuffix(name, ".")
2133 if name != "" && name != c.hello.Domain.XName(c.msgsmtputf8) {
2134 recvFrom += name + " "
2136 recvFrom += smtp.AddressLiteral(c.remoteIP) + ")"
2137 if c.msgsmtputf8 && c.hello.Domain.Unicode != "" {
2138 recvFrom += " (" + c.hello.Domain.ASCII + ")"
2141 recvBy := mox.Conf.Static.HostnameDomain.XName(c.msgsmtputf8)
2142 recvBy += " (" + smtp.AddressLiteral(c.localIP) + ")" // todo: hide ip if internal?
2143 if c.msgsmtputf8 && mox.Conf.Static.HostnameDomain.Unicode != "" {
2144 // This syntax is part of "VIA".
2145 recvBy += " (" + mox.Conf.Static.HostnameDomain.ASCII + ")"
2158 if c.account != nil {
2163 // Assume transaction does not succeed. If it does, we'll compensate.
2166 recvHdrFor := func(rcptTo string) string {
2167 recvHdr := &message.HeaderWriter{}
2168 // For additional Received-header clauses, see:
2169 // https://www.iana.org/assignments/mail-parameters/mail-parameters.xhtml#table-mail-parameters-8
2171 if c.requireTLS != nil && *c.requireTLS {
2173 withComment = " (requiretls)"
2175 recvHdr.Add(" ", "Received:", "from", recvFrom, "by", recvBy, "via", "tcp", "with", with+withComment, "id", mox.ReceivedID(c.cid)) //
../rfc/5321:3158
2177 tlsConn := c.conn.(*tls.Conn)
2178 tlsComment := mox.TLSReceivedComment(c.log, tlsConn.ConnectionState())
2179 recvHdr.Add(" ", tlsComment...)
2181 // We leave out an empty "for" clause. This is empty for messages submitted to
2182 // multiple recipients, so the message stays identical and a single smtp
2183 // transaction can deliver, only transferring the data once.
2185 recvHdr.Add(" ", "for", "<"+rcptTo+">;")
2187 recvHdr.Add(" ", time.Now().Format(message.RFC5322Z))
2188 return recvHdr.String()
2191 // Submission is easiest because user is trusted. Far fewer checks to make. So
2192 // handle it first, and leave the rest of the function for handling wild west
2193 // internet traffic.
2195 c.submit(cmdctx, recvHdrFor, msgWriter, dataFile, part)
2197 c.deliver(cmdctx, recvHdrFor, msgWriter, iprevStatus, iprevAuthentic, dataFile)
2201// Check if a message has unambiguous "TLS-Required: No" header. Messages must not
2202// contain multiple TLS-Required headers. The only valid value is "no". But we'll
2203// accept multiple headers as long as all they are all "no".
2205func hasTLSRequiredNo(h textproto.MIMEHeader) bool {
2206 l := h.Values("Tls-Required")
2210 for _, v := range l {
2211 if !strings.EqualFold(v, "no") {
2218// submit is used for mail from authenticated users that we will try to deliver.
2219func (c *conn) submit(ctx context.Context, recvHdrFor func(string) string, msgWriter *message.Writer, dataFile *os.File, part *message.Part) {
2220 // Similar between ../smtpserver/server.go:/submit\( and ../webmail/api.go:/MessageSubmit\( and ../webapisrv/server.go:/Send\(
2222 var msgPrefix []byte
2224 // Check that user is only sending email as one of its configured identities. Not
2228 msgFrom, _, header, err := message.From(c.log.Logger, true, dataFile, part)
2230 metricSubmission.WithLabelValues("badmessage").Inc()
2231 c.log.Infox("parsing message From address", err, slog.String("user", c.username))
2232 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeMsg6Other0, "cannot parse header or From address: %v", err)
2234 if !mox.AllowMsgFrom(c.account.Name, msgFrom) {
2236 metricSubmission.WithLabelValues("badfrom").Inc()
2237 c.log.Infox("verifying message from address", mox.ErrAddressNotFound, slog.String("user", c.username), slog.Any("msgfrom", msgFrom))
2238 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SePol7DeliveryUnauth1, "message from address must belong to authenticated user")
2241 // TLS-Required: No header makes us not enforce recipient domain's TLS policy.
2244 if c.requireTLS == nil && hasTLSRequiredNo(header) {
2249 // Outgoing messages should not have a Return-Path header. The final receiving mail
2250 // server will add it.
2252 if mox.Pedantic && header.Values("Return-Path") != nil {
2253 metricSubmission.WithLabelValues("badheader").Inc()
2254 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeMsg6Other0, "message should not have Return-Path header")
2257 // Add Message-Id header if missing.
2259 messageID := header.Get("Message-Id")
2260 if messageID == "" {
2261 messageID = mox.MessageIDGen(c.msgsmtputf8)
2262 msgPrefix = append(msgPrefix, fmt.Sprintf("Message-Id: <%s>\r\n", messageID)...)
2266 if header.Get("Date") == "" {
2267 msgPrefix = append(msgPrefix, "Date: "+time.Now().Format(message.RFC5322Z)+"\r\n"...)
2270 // Check outgoing message rate limit.
2271 err = c.account.DB.Read(ctx, func(tx *bstore.Tx) error {
2272 rcpts := make([]smtp.Path, len(c.recipients))
2273 for i, r := range c.recipients {
2276 msglimit, rcptlimit, err := c.account.SendLimitReached(tx, rcpts)
2277 xcheckf(err, "checking sender limit")
2279 metricSubmission.WithLabelValues("messagelimiterror").Inc()
2280 xsmtpUserErrorf(smtp.C451LocalErr, smtp.SePol7DeliveryUnauth1, "max number of messages (%d) over past 24h reached, try increasing per-account setting MaxOutgoingMessagesPerDay", msglimit)
2281 } else if rcptlimit >= 0 {
2282 metricSubmission.WithLabelValues("recipientlimiterror").Inc()
2283 xsmtpUserErrorf(smtp.C451LocalErr, smtp.SePol7DeliveryUnauth1, "max number of new/first-time recipients (%d) over past 24h reached, try increasing per-account setting MaxFirstTimeRecipientsPerDay", rcptlimit)
2287 xcheckf(err, "read-only transaction")
2289 // We gather any X-Mox-Extra-* headers into the "extra" data during queueing, which
2290 // will make it into any webhook we deliver.
2291 // todo: remove the X-Mox-Extra-* headers from the message. we don't currently rewrite the message...
2292 // todo: should we not canonicalize keys?
2293 var extra map[string]string
2294 for k, vl := range header {
2295 if !strings.HasPrefix(k, "X-Mox-Extra-") {
2299 extra = map[string]string{}
2301 xk := k[len("X-Mox-Extra-"):]
2302 // We don't allow duplicate keys.
2303 if _, ok := extra[xk]; ok || len(vl) > 1 {
2304 xsmtpUserErrorf(smtp.C554TransactionFailed, smtp.SeMsg6Other0, "duplicate x-mox-extra- key %q", xk)
2306 extra[xk] = vl[len(vl)-1]
2309 // 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.
2311 // Add DKIM signatures.
2312 confDom, ok := mox.Conf.Domain(msgFrom.Domain)
2314 c.log.Error("domain disappeared", slog.Any("domain", msgFrom.Domain))
2315 xsmtpServerErrorf(codes{smtp.C451LocalErr, smtp.SeSys3Other0}, "internal error")
2318 selectors := mox.DKIMSelectors(confDom.DKIM)
2319 if len(selectors) > 0 {
2320 canonical := mox.CanonicalLocalpart(msgFrom.Localpart, confDom)
2321 if dkimHeaders, err := dkim.Sign(ctx, c.log.Logger, canonical, msgFrom.Domain, selectors, c.msgsmtputf8, store.FileMsgReader(msgPrefix, dataFile)); err != nil {
2322 c.log.Errorx("dkim sign for domain", err, slog.Any("domain", msgFrom.Domain))
2323 metricServerErrors.WithLabelValues("dkimsign").Inc()
2325 msgPrefix = append(msgPrefix, []byte(dkimHeaders)...)
2329 authResults := message.AuthResults{
2330 Hostname: mox.Conf.Static.HostnameDomain.XName(c.msgsmtputf8),
2331 Comment: mox.Conf.Static.HostnameDomain.ASCIIExtra(c.msgsmtputf8),
2332 Methods: []message.AuthMethod{
2336 Props: []message.AuthProp{
2337 message.MakeAuthProp("smtp", "mailfrom", c.mailFrom.XString(c.msgsmtputf8), true, c.mailFrom.ASCIIExtra(c.msgsmtputf8)),
2342 msgPrefix = append(msgPrefix, []byte(authResults.Header())...)
2344 // We always deliver through the queue. It would be more efficient to deliver
2345 // directly for local accounts, but we don't want to circumvent all the anti-spam
2346 // measures. Accounts on a single mox instance should be allowed to block each
2349 accConf, _ := c.account.Conf()
2350 loginAddr, err := smtp.ParseAddress(c.username)
2351 xcheckf(err, "parsing login address")
2352 useFromID := slices.Contains(accConf.ParsedFromIDLoginAddresses, loginAddr)
2353 var localpartBase string
2357 // With submission, user can bring their own fromid.
2358 t := strings.SplitN(string(c.mailFrom.Localpart), confDom.LocalpartCatchallSeparator, 2)
2359 localpartBase = t[0]
2362 if fromID != "" && len(c.recipients) > 1 {
2363 xsmtpServerErrorf(codes{smtp.C554TransactionFailed, smtp.SeProto5TooManyRcpts3}, "cannot send to multiple recipients with chosen fromid")
2370 qml := make([]queue.Msg, len(c.recipients))
2371 for i, rcpt := range c.recipients {
2373 code, timeout := mox.LocalserveNeedsError(rcpt.Addr.Localpart)
2375 c.log.Info("timing out submission due to special localpart")
2376 mox.Sleep(mox.Context, time.Hour)
2377 xsmtpServerErrorf(codes{smtp.C451LocalErr, smtp.SeSys3Other0}, "timing out submission due to special localpart")
2378 } else if code != 0 {
2379 c.log.Info("failure due to special localpart", slog.Int("code", code))
2380 xsmtpServerErrorf(codes{code, smtp.SeOther00}, "failure with code %d due to special localpart", code)
2387 fromID = xrandomID(16)
2389 fp.Localpart = smtp.Localpart(localpartBase + confDom.LocalpartCatchallSeparator + fromID)
2392 // For multiple recipients, we don't make each message prefix unique, leaving out
2393 // the "for" clause in the Received header. This allows the queue to deliver the
2394 // messages in a single smtp transaction.
2396 if len(c.recipients) == 1 {
2397 rcptTo = rcpt.Addr.String()
2399 xmsgPrefix := append([]byte(recvHdrFor(rcptTo)), msgPrefix...)
2400 msgSize := int64(len(xmsgPrefix)) + msgWriter.Size
2401 qm := queue.MakeMsg(fp, rcpt.Addr, msgWriter.Has8bit, c.msgsmtputf8, msgSize, messageID, xmsgPrefix, c.requireTLS, now, header.Get("Subject"))
2402 if !c.futureRelease.IsZero() {
2403 qm.NextAttempt = c.futureRelease
2404 qm.FutureReleaseRequest = c.futureReleaseRequest
2411 // 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
2412 if err := queue.Add(ctx, c.log, c.account.Name, dataFile, qml...); err != nil && errors.Is(err, queue.ErrFromID) && !genFromID {
2413 // todo: should we return this error during the "rcpt to" command?
2414 // secode is not an exact match, but seems closest.
2415 xsmtpServerErrorf(errCodes(smtp.C554TransactionFailed, smtp.SeAddr1SenderSyntax7, err), "bad fromid in smtp mail from address: %s", err)
2416 } else if err != nil {
2417 // Aborting the transaction is not great. But continuing and generating DSNs will
2418 // probably result in errors as well...
2419 metricSubmission.WithLabelValues("queueerror").Inc()
2420 c.log.Errorx("queuing message", err)
2421 xsmtpServerErrorf(errCodes(smtp.C451LocalErr, smtp.SeSys3Other0, err), "error delivering message: %v", err)
2423 metricSubmission.WithLabelValues("ok").Inc()
2424 for i, rcpt := range c.recipients {
2425 c.log.Info("messages queued for delivery",
2426 slog.Any("mailfrom", *c.mailFrom),
2427 slog.Any("rcptto", rcpt.Addr),
2428 slog.Bool("smtputf8", c.smtputf8),
2429 slog.Bool("msgsmtputf8", c.msgsmtputf8),
2430 slog.Int64("msgsize", qml[i].Size))
2433 err = c.account.DB.Write(ctx, func(tx *bstore.Tx) error {
2434 for _, rcpt := range c.recipients {
2435 outgoing := store.Outgoing{Recipient: rcpt.Addr.XString(true)}
2436 if err := tx.Insert(&outgoing); err != nil {
2437 return fmt.Errorf("adding outgoing message: %v", err)
2442 xcheckf(err, "adding outgoing messages")
2445 c.transactionBad-- // Compensate for early earlier pessimistic increase.
2448 c.writecodeline(smtp.C250Completed, smtp.SeMailbox2Other0, "it is done", nil)
2451func xrandomID(n int) string {
2452 return base64.RawURLEncoding.EncodeToString(xrandom(n))
2455func xrandom(n int) []byte {
2456 buf := make([]byte, n)
2457 x, err := cryptorand.Read(buf)
2458 xcheckf(err, "read random")
2460 xcheckf(errors.New("short random read"), "read random")
2465func ipmasked(ip net.IP) (string, string, string) {
2466 if ip.To4() != nil {
2468 m2 := ip.Mask(net.CIDRMask(26, 32)).String()
2469 m3 := ip.Mask(net.CIDRMask(21, 32)).String()
2472 m1 := ip.Mask(net.CIDRMask(64, 128)).String()
2473 m2 := ip.Mask(net.CIDRMask(48, 128)).String()
2474 m3 := ip.Mask(net.CIDRMask(32, 128)).String()
2478func (c *conn) xlocalserveError(lp smtp.Localpart) {
2479 code, timeout := mox.LocalserveNeedsError(lp)
2481 c.log.Info("timing out due to special localpart")
2482 mox.Sleep(mox.Context, time.Hour)
2483 xsmtpServerErrorf(codes{smtp.C451LocalErr, smtp.SeSys3Other0}, "timing out command due to special localpart")
2484 } else if code != 0 {
2485 c.log.Info("failure due to special localpart", slog.Int("code", code))
2486 metricDelivery.WithLabelValues("delivererror", "localserve").Inc()
2487 xsmtpServerErrorf(codes{code, smtp.SeOther00}, "failure with code %d due to special localpart", code)
2491// deliver is called for incoming messages from external, typically untrusted
2492// sources. i.e. not submitted by authenticated users.
2493func (c *conn) deliver(ctx context.Context, recvHdrFor func(string) string, msgWriter *message.Writer, iprevStatus iprev.Status, iprevAuthentic bool, dataFile *os.File) {
2494 // 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.
2496 var msgFrom smtp.Address
2497 var envelope *message.Envelope
2498 var headers textproto.MIMEHeader
2500 part, err := message.Parse(c.log.Logger, false, dataFile)
2502 // 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?
2503 isDSN = part.MediaType == "MULTIPART" && part.MediaSubType == "REPORT" && strings.EqualFold(part.ContentTypeParams["report-type"], "delivery-status")
2504 msgFrom, envelope, headers, err = message.From(c.log.Logger, false, dataFile, &part)
2507 c.log.Infox("parsing message for From address", err)
2511 if len(headers.Values("Received")) > 100 {
2512 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeNet4Loop6, "loop detected, more than 100 Received headers")
2515 // TLS-Required: No header makes us not enforce recipient domain's TLS policy.
2516 // Since we only deliver locally at the moment, this won't influence our behaviour.
2517 // Once we forward, it would our delivery attempts.
2520 if c.requireTLS == nil && hasTLSRequiredNo(headers) {
2525 // We'll be building up an Authentication-Results header.
2526 authResults := message.AuthResults{
2527 Hostname: mox.Conf.Static.HostnameDomain.XName(c.msgsmtputf8),
2530 commentAuthentic := func(v bool) string {
2532 return "with dnssec"
2534 return "without dnssec"
2537 // Reverse IP lookup results.
2538 // todo future: how useful is this?
2540 authResults.Methods = append(authResults.Methods, message.AuthMethod{
2542 Result: string(iprevStatus),
2543 Comment: commentAuthentic(iprevAuthentic),
2544 Props: []message.AuthProp{
2545 message.MakeAuthProp("policy", "iprev", c.remoteIP.String(), false, ""),
2549 // SPF and DKIM verification in parallel.
2550 var wg sync.WaitGroup
2554 var dkimResults []dkim.Result
2558 x := recover() // Should not happen, but don't take program down if it does.
2560 c.log.Error("dkim verify panic", slog.Any("err", x))
2562 metrics.PanicInc(metrics.Dkimverify)
2566 // We always evaluate all signatures. We want to build up reputation for each
2567 // domain in the signature.
2568 const ignoreTestMode = false
2569 // todo future: longer timeout? we have to read through the entire email, which can be large, possibly multiple times.
2570 dkimctx, dkimcancel := context.WithTimeout(ctx, time.Minute)
2572 // todo future: we could let user configure which dkim headers they require
2574 // For localserve, fake dkim selector DNS records for hosted domains to give
2575 // dkim-signatures a chance to pass for deliveries from queue.
2576 resolver := c.resolver
2578 // Lookup based on message From address is an approximation.
2579 if dc, ok := mox.Conf.Domain(msgFrom.Domain); ok && len(dc.DKIM.Selectors) > 0 {
2580 txts := map[string][]string{}
2581 for name, sel := range dc.DKIM.Selectors {
2582 dkimr := dkim.Record{
2584 Hashes: []string{sel.HashEffective},
2585 PublicKey: sel.Key.Public(),
2587 if _, ok := sel.Key.(ed25519.PrivateKey); ok {
2588 dkimr.Key = "ed25519"
2589 } else if _, ok := sel.Key.(*rsa.PrivateKey); !ok {
2590 err := fmt.Errorf("unrecognized private key for DKIM selector %q: %T", name, sel.Key)
2591 xcheckf(err, "making dkim record")
2593 txt, err := dkimr.Record()
2594 xcheckf(err, "making DKIM DNS TXT record")
2595 txts[name+"._domainkey."+msgFrom.Domain.ASCII+"."] = []string{txt}
2597 resolver = dns.MockResolver{TXT: txts}
2600 dkimResults, dkimErr = dkim.Verify(dkimctx, c.log.Logger, resolver, c.msgsmtputf8, dkim.DefaultPolicy, dataFile, ignoreTestMode)
2606 var receivedSPF spf.Received
2607 var spfDomain dns.Domain
2609 var spfAuthentic bool
2611 spfArgs := spf.Args{
2612 RemoteIP: c.remoteIP,
2613 MailFromLocalpart: c.mailFrom.Localpart,
2614 MailFromDomain: c.mailFrom.IPDomain.Domain, // Can be empty.
2615 HelloDomain: c.hello,
2617 LocalHostname: c.hostname,
2622 x := recover() // Should not happen, but don't take program down if it does.
2624 c.log.Error("spf verify panic", slog.Any("err", x))
2626 metrics.PanicInc(metrics.Spfverify)
2630 spfctx, spfcancel := context.WithTimeout(ctx, time.Minute)
2632 resolver := c.resolver
2633 // For localserve, give hosted domains a chance to pass for deliveries from queue.
2634 if Localserve && c.remoteIP.IsLoopback() {
2635 // Lookup based on message From address is an approximation.
2636 if _, ok := mox.Conf.Domain(msgFrom.Domain); ok {
2637 resolver = dns.MockResolver{
2638 TXT: map[string][]string{msgFrom.Domain.ASCII + ".": {"v=spf1 ip4:127.0.0.1/8 ip6:::1 ~all"}},
2642 receivedSPF, spfDomain, spfExpl, spfAuthentic, spfErr = spf.Verify(spfctx, c.log.Logger, resolver, spfArgs)
2645 c.log.Infox("spf verify", spfErr)
2649 // Wait for DKIM and SPF validation to finish.
2652 // Give immediate response if all recipients are unknown.
2654 for _, r := range c.recipients {
2655 if r.Account == nil && r.Alias == nil {
2659 if nunknown == len(c.recipients) {
2660 // During RCPT TO we found that the address does not exist.
2661 c.log.Info("deliver attempt to unknown user(s)", slog.Any("recipients", c.recipients))
2663 // Crude attempt to slow down someone trying to guess names. Would work better
2664 // with connection rate limiter.
2665 if unknownRecipientsDelay > 0 {
2666 mox.Sleep(ctx, unknownRecipientsDelay)
2669 // 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.
2670 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeAddr1UnknownDestMailbox1, "no such user(s)")
2673 // Add DKIM results to Authentication-Results header.
2674 authResAddDKIM := func(result, comment, reason string, props []message.AuthProp) {
2675 dm := message.AuthMethod{
2682 authResults.Methods = append(authResults.Methods, dm)
2685 c.log.Errorx("dkim verify", dkimErr)
2686 authResAddDKIM("none", "", dkimErr.Error(), nil)
2687 } else if len(dkimResults) == 0 {
2688 c.log.Info("no dkim-signature header", slog.Any("mailfrom", c.mailFrom))
2689 authResAddDKIM("none", "", "no dkim signatures", nil)
2691 for i, r := range dkimResults {
2692 var domain, selector dns.Domain
2693 var identity *dkim.Identity
2695 var props []message.AuthProp
2697 if r.Record != nil && r.Record.PublicKey != nil {
2698 if pubkey, ok := r.Record.PublicKey.(*rsa.PublicKey); ok {
2699 comment = fmt.Sprintf("%d bit rsa, ", pubkey.N.BitLen())
2703 sig := base64.StdEncoding.EncodeToString(r.Sig.Signature)
2704 sig = sig[:12] // Must be at least 8 characters and unique among the signatures.
2705 props = []message.AuthProp{
2706 message.MakeAuthProp("header", "d", r.Sig.Domain.XName(c.msgsmtputf8), true, r.Sig.Domain.ASCIIExtra(c.msgsmtputf8)),
2707 message.MakeAuthProp("header", "s", r.Sig.Selector.XName(c.msgsmtputf8), true, r.Sig.Selector.ASCIIExtra(c.msgsmtputf8)),
2708 message.MakeAuthProp("header", "a", r.Sig.Algorithm(), false, ""),
2711 domain = r.Sig.Domain
2712 selector = r.Sig.Selector
2713 if r.Sig.Identity != nil {
2714 props = append(props, message.MakeAuthProp("header", "i", r.Sig.Identity.String(), true, ""))
2715 identity = r.Sig.Identity
2717 if r.RecordAuthentic {
2718 comment += "with dnssec"
2720 comment += "without dnssec"
2725 errmsg = r.Err.Error()
2727 authResAddDKIM(string(r.Status), comment, errmsg, props)
2728 c.log.Debugx("dkim verification result", r.Err,
2729 slog.Int("index", i),
2730 slog.Any("mailfrom", c.mailFrom),
2731 slog.Any("status", r.Status),
2732 slog.Any("domain", domain),
2733 slog.Any("selector", selector),
2734 slog.Any("identity", identity))
2738 var spfIdentity *dns.Domain
2739 var mailFromValidation = store.ValidationUnknown
2740 var ehloValidation = store.ValidationUnknown
2741 switch receivedSPF.Identity {
2742 case spf.ReceivedHELO:
2743 if len(spfArgs.HelloDomain.IP) == 0 {
2744 spfIdentity = &spfArgs.HelloDomain.Domain
2746 ehloValidation = store.SPFValidation(receivedSPF.Result)
2747 case spf.ReceivedMailFrom:
2748 spfIdentity = &spfArgs.MailFromDomain
2749 mailFromValidation = store.SPFValidation(receivedSPF.Result)
2751 var props []message.AuthProp
2752 if spfIdentity != nil {
2753 props = []message.AuthProp{message.MakeAuthProp("smtp", string(receivedSPF.Identity), spfIdentity.XName(c.msgsmtputf8), true, spfIdentity.ASCIIExtra(c.msgsmtputf8))}
2755 var spfComment string
2757 spfComment = "with dnssec"
2759 spfComment = "without dnssec"
2761 authResults.Methods = append(authResults.Methods, message.AuthMethod{
2763 Result: string(receivedSPF.Result),
2764 Comment: spfComment,
2767 switch receivedSPF.Result {
2768 case spf.StatusPass:
2769 c.log.Debug("spf pass", slog.Any("ip", spfArgs.RemoteIP), slog.String("mailfromdomain", spfArgs.MailFromDomain.ASCII)) // todo: log the domain that was actually verified.
2770 case spf.StatusFail:
2773 for _, b := range []byte(spfExpl) {
2774 if b < ' ' || b >= 0x7f {
2780 if len(spfExpl) > 800 {
2781 spfExpl = spfExpl[:797] + "..."
2783 spfExpl = "remote claims: " + spfExpl
2787 spfExpl = fmt.Sprintf("your ip %s is not on the SPF allowlist for domain %s", spfArgs.RemoteIP, spfDomain.ASCII)
2789 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?
2790 case spf.StatusTemperror:
2791 c.log.Infox("spf temperror", spfErr)
2792 case spf.StatusPermerror:
2793 c.log.Infox("spf permerror", spfErr)
2794 case spf.StatusNone, spf.StatusNeutral, spf.StatusSoftfail:
2796 c.log.Error("unknown spf status, treating as None/Neutral", slog.Any("status", receivedSPF.Result))
2797 receivedSPF.Result = spf.StatusNone
2802 var dmarcResult dmarc.Result
2803 const applyRandomPercentage = true
2804 // dmarcMethod is added to authResults when delivering to recipients: accounts can
2805 // have different policy override rules.
2806 var dmarcMethod message.AuthMethod
2807 var msgFromValidation = store.ValidationNone
2808 if msgFrom.IsZero() {
2809 dmarcResult.Status = dmarc.StatusNone
2810 dmarcMethod = message.AuthMethod{
2812 Result: string(dmarcResult.Status),
2815 msgFromValidation = alignment(ctx, c.log, msgFrom.Domain, dkimResults, receivedSPF.Result, spfIdentity)
2817 // We are doing the DMARC evaluation now. But we only store it for inclusion in an
2818 // aggregate report when we actually use it. We use an evaluation for each
2819 // recipient, with each a potentially different result due to mailing
2820 // list/forwarding configuration. If we reject a message due to being spam, we
2821 // don't want to spend any resources for the sender domain, and we don't want to
2822 // give the sender any more information about us, so we won't record the
2824 // 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.
2826 dmarcctx, dmarccancel := context.WithTimeout(ctx, time.Minute)
2828 dmarcUse, dmarcResult = dmarc.Verify(dmarcctx, c.log.Logger, c.resolver, msgFrom.Domain, dkimResults, receivedSPF.Result, spfIdentity, applyRandomPercentage)
2831 if dmarcResult.RecordAuthentic {
2832 comment = "with dnssec"
2834 comment = "without dnssec"
2836 dmarcMethod = message.AuthMethod{
2838 Result: string(dmarcResult.Status),
2840 Props: []message.AuthProp{
2842 message.MakeAuthProp("header", "from", msgFrom.Domain.ASCII, true, msgFrom.Domain.ASCIIExtra(c.msgsmtputf8)),
2846 if dmarcResult.Status == dmarc.StatusPass && msgFromValidation == store.ValidationRelaxed {
2847 msgFromValidation = store.ValidationDMARC
2850 // todo future: consider enforcing an spf (soft)fail if there is no dmarc policy or the dmarc policy is none.
../rfc/7489:1507
2852 c.log.Debug("dmarc verification", slog.Any("result", dmarcResult.Status), slog.Any("domain", msgFrom.Domain))
2854 // Prepare for analyzing content, calculating reputation.
2855 ipmasked1, ipmasked2, ipmasked3 := ipmasked(c.remoteIP)
2856 var verifiedDKIMDomains []string
2857 dkimSeen := map[string]bool{}
2858 for _, r := range dkimResults {
2859 // A message can have multiple signatures for the same identity. For example when
2860 // signing the message multiple times with different algorithms (rsa and ed25519).
2861 if r.Status != dkim.StatusPass {
2864 d := r.Sig.Domain.Name()
2867 verifiedDKIMDomains = append(verifiedDKIMDomains, d)
2871 // When we deliver, we try to remove from rejects mailbox based on message-id.
2872 // We'll parse it when we need it, but it is the same for each recipient.
2873 var messageID string
2874 var parsedMessageID bool
2876 // We build up a DSN for each failed recipient. If we have recipients in dsnMsg
2877 // after processing, we queue the DSN. Unless all recipients failed, in which case
2878 // we may just fail the mail transaction instead (could be common for failure to
2879 // deliver to a single recipient, e.g. for junk mail).
2881 type deliverError struct {
2888 var deliverErrors []deliverError
2889 addError := func(rcpt recipient, code int, secode string, userError bool, errmsg string) {
2890 e := deliverError{rcpt.Addr, code, secode, userError, errmsg}
2891 c.log.Info("deliver error",
2892 slog.Any("rcptto", e.rcptTo),
2893 slog.Int("code", code),
2894 slog.String("secode", "secode"),
2895 slog.Bool("usererror", userError),
2896 slog.String("errmsg", errmsg))
2897 deliverErrors = append(deliverErrors, e)
2900 // Sort recipients: local accounts, aliases, unknown. For ensuring we don't deliver
2901 // to an alias destination that was also explicitly sent to.
2902 rcptScore := func(r recipient) int {
2903 if r.Account != nil {
2905 } else if r.Alias != nil {
2910 sort.SliceStable(c.recipients, func(i, j int) bool {
2911 return rcptScore(c.recipients[i]) < rcptScore(c.recipients[j])
2914 // Return whether address is a regular explicit recipient in this transaction. Used
2915 // to prevent delivering a message to an address both for alias and explicit
2916 // addressee. Relies on c.recipients being sorted as above.
2917 regularRecipient := func(addr smtp.Path) bool {
2918 for _, rcpt := range c.recipients {
2919 if rcpt.Account == nil {
2921 } else if rcpt.Addr.Equal(addr) {
2928 // Prepare a message, analyze it against account's junk filter.
2929 // The returned analysis has an open account that must be closed by the caller.
2930 // We call this for all alias destinations, also when we already delivered to that
2931 // recipient: It may be the only recipient that would allow the message.
2932 messageAnalyze := func(log mlog.Log, smtpRcptTo, deliverTo smtp.Path, accountName string, destination config.Destination, canonicalAddr string) (a *analysis, rerr error) {
2933 acc, err := store.OpenAccount(log, accountName)
2935 log.Errorx("open account", err, slog.Any("account", accountName))
2936 metricDelivery.WithLabelValues("accounterror", "").Inc()
2942 log.Check(err, "closing account during analysis")
2947 Received: time.Now(),
2948 RemoteIP: c.remoteIP.String(),
2949 RemoteIPMasked1: ipmasked1,
2950 RemoteIPMasked2: ipmasked2,
2951 RemoteIPMasked3: ipmasked3,
2952 EHLODomain: c.hello.Domain.Name(),
2953 MailFrom: c.mailFrom.String(),
2954 MailFromLocalpart: c.mailFrom.Localpart,
2955 MailFromDomain: c.mailFrom.IPDomain.Domain.Name(),
2956 RcptToLocalpart: smtpRcptTo.Localpart,
2957 RcptToDomain: smtpRcptTo.IPDomain.Domain.Name(),
2958 MsgFromLocalpart: msgFrom.Localpart,
2959 MsgFromDomain: msgFrom.Domain.Name(),
2960 MsgFromOrgDomain: publicsuffix.Lookup(ctx, log.Logger, msgFrom.Domain).Name(),
2961 EHLOValidated: ehloValidation == store.ValidationPass,
2962 MailFromValidated: mailFromValidation == store.ValidationPass,
2963 MsgFromValidated: msgFromValidation == store.ValidationStrict || msgFromValidation == store.ValidationDMARC || msgFromValidation == store.ValidationRelaxed,
2964 EHLOValidation: ehloValidation,
2965 MailFromValidation: mailFromValidation,
2966 MsgFromValidation: msgFromValidation,
2967 DKIMDomains: verifiedDKIMDomains,
2969 Size: msgWriter.Size,
2972 tlsState := c.conn.(*tls.Conn).ConnectionState()
2973 m.ReceivedTLSVersion = tlsState.Version
2974 m.ReceivedTLSCipherSuite = tlsState.CipherSuite
2975 if c.requireTLS != nil {
2976 m.ReceivedRequireTLS = *c.requireTLS
2979 m.ReceivedTLSVersion = 1 // Signals plain text delivery.
2982 var msgTo, msgCc []message.Address
2983 if envelope != nil {
2987 d := delivery{c.tls, &m, dataFile, smtpRcptTo, deliverTo, destination, canonicalAddr, acc, msgTo, msgCc, msgFrom, c.dnsBLs, dmarcUse, dmarcResult, dkimResults, iprevStatus, c.smtputf8}
2989 r := analyze(ctx, log, c.resolver, d)
2993 // Either deliver the message, or call addError to register the recipient as failed.
2994 // If recipient is an alias, we may be delivering to multiple address/accounts and
2995 // we will consider a message delivered if we delivered it to at least one account
2996 // (others may be over quota).
2997 processRecipient := func(rcpt recipient) {
2998 log := c.log.With(slog.Any("mailfrom", c.mailFrom), slog.Any("rcptto", rcpt.Addr))
3000 // If this is not a valid local user, we send back a DSN. This can only happen when
3001 // there are also valid recipients, and only when remote is SPF-verified, so the DSN
3002 // should not cause backscatter.
3003 // In case of serious errors, we abort the transaction. We may have already
3004 // delivered some messages. Perhaps it would be better to continue with other
3005 // deliveries, and return an error at the end? Though the failure conditions will
3006 // probably prevent any other successful deliveries too...
3008 if rcpt.Account == nil && rcpt.Alias == nil {
3009 metricDelivery.WithLabelValues("unknownuser", "").Inc()
3010 addError(rcpt, smtp.C550MailboxUnavail, smtp.SeAddr1UnknownDestMailbox1, true, "no such user")
3014 // la holds all analysis, and message preparation, for all accounts (multiple for
3015 // aliases). Each has an open account that we we close on return.
3018 for _, a := range la {
3019 err := a.d.acc.Close()
3020 log.Check(err, "close account")
3024 // For aliases, we prepare & analyze for each recipient. We accept the message if
3025 // any recipient accepts it. Regular destination have just a single account to
3026 // check. We check all alias destinations, even if we already explicitly delivered
3027 // to them: they may be the only destination that would accept the message.
3028 var a0 *analysis // Analysis we've used for accept/reject decision.
3029 if rcpt.Alias != nil {
3030 // Check if msgFrom address is acceptable. This doesn't take validation into
3031 // consideration. If the header was forged, the message may be rejected later on.
3032 if !aliasAllowedMsgFrom(rcpt.Alias.Alias, msgFrom) {
3033 addError(rcpt, smtp.C550MailboxUnavail, smtp.SePol7ExpnProhibited2, true, "not allowed to send to destination")
3037 la = make([]analysis, 0, len(rcpt.Alias.Alias.ParsedAddresses))
3038 for _, aa := range rcpt.Alias.Alias.ParsedAddresses {
3039 a, err := messageAnalyze(log, rcpt.Addr, aa.Address.Path(), aa.AccountName, aa.Destination, rcpt.Alias.CanonicalAddress)
3041 addError(rcpt, smtp.C451LocalErr, smtp.SeSys3Other0, false, "error processing")
3045 if a.accept && a0 == nil {
3046 // Address that caused us to accept.
3051 // First address, for rejecting.
3055 a, err := messageAnalyze(log, rcpt.Addr, rcpt.Addr, rcpt.Account.AccountName, rcpt.Account.Destination, rcpt.Account.CanonicalAddress)
3057 addError(rcpt, smtp.C451LocalErr, smtp.SeSys3Other0, false, "error processing")
3064 if !a0.accept && a0.reason == reasonHighRate {
3065 log.Info("incoming message rejected for high rate, not storing in rejects mailbox", slog.String("reason", a0.reason), slog.Any("msgfrom", msgFrom))
3066 metricDelivery.WithLabelValues("reject", a0.reason).Inc()
3068 addError(rcpt, a0.code, a0.secode, a0.userError, a0.errmsg)
3072 // Any DMARC result override is stored in the evaluation for outgoing DMARC
3073 // aggregate reports, and added to the Authentication-Results message header.
3074 // We want to tell the sender that we have an override, e.g. for mailing lists, so
3075 // they don't overestimate the potential damage of switching from p=none to
3077 var dmarcOverrides []string
3078 if a0.dmarcOverrideReason != "" {
3079 dmarcOverrides = []string{a0.dmarcOverrideReason}
3081 if dmarcResult.Record != nil && !dmarcUse {
3082 dmarcOverrides = append(dmarcOverrides, string(dmarcrpt.PolicyOverrideSampledOut))
3085 // Add per-recipient DMARC method to Authentication-Results. Each account can have
3086 // their own override rules, e.g. based on configured mailing lists/forwards.
3088 rcptDMARCMethod := dmarcMethod
3089 if len(dmarcOverrides) > 0 {
3090 if rcptDMARCMethod.Comment != "" {
3091 rcptDMARCMethod.Comment += ", "
3093 rcptDMARCMethod.Comment += "override " + strings.Join(dmarcOverrides, ",")
3095 rcptAuthResults := authResults
3096 rcptAuthResults.Methods = append([]message.AuthMethod{}, authResults.Methods...)
3097 rcptAuthResults.Methods = append(rcptAuthResults.Methods, rcptDMARCMethod)
3099 // Prepend reason as message header, for easy viewing in mail clients.
3101 if a0.reason != "" {
3102 hw := &message.HeaderWriter{}
3103 hw.Add(" ", "X-Mox-Reason:")
3104 hw.Add(" ", a0.reason)
3105 for i, s := range a0.reasonText {
3111 // Just in case any of the strings has a newline, replace it with space to not break the message.
3112 s = strings.ReplaceAll(s, "\n", " ")
3113 s = strings.ReplaceAll(s, "\r", " ")
3115 hw.AddWrap([]byte(s), true)
3124 la[i].d.m.MsgPrefix = []byte(
3128 rcptAuthResults.Header() +
3129 receivedSPF.Header() +
3130 recvHdrFor(rcpt.Addr.String()),
3132 la[i].d.m.Size += int64(len(la[i].d.m.MsgPrefix))
3135 // Store DMARC evaluation for inclusion in an aggregate report. Only if there is at
3136 // least one reporting address: We don't want to needlessly store a row in a
3137 // database for each delivery attempt. If we reject a message for being junk, we
3138 // are also not going to send it a DMARC report. The DMARC check is done early in
3139 // the analysis, we will report on rejects because of DMARC, because it could be
3140 // valuable feedback about forwarded or mailing list messages.
3142 if !mox.Conf.Static.NoOutgoingDMARCReports && dmarcResult.Record != nil && len(dmarcResult.Record.AggregateReportAddresses) > 0 && (a0.accept && !a0.d.m.IsReject || a0.reason == reasonDMARCPolicy) {
3143 // Disposition holds our decision on whether to accept the message. Not what the
3144 // DMARC evaluation resulted in. We can override, e.g. because of mailing lists,
3145 // forwarding, or local policy.
3146 // We treat quarantine as reject, so never claim to quarantine.
3148 disposition := dmarcrpt.DispositionNone
3150 disposition = dmarcrpt.DispositionReject
3153 // unknownDomain returns whether the sender is domain with which this account has
3154 // not had positive interaction.
3155 unknownDomain := func() (unknown bool) {
3156 err := a0.d.acc.DB.Read(ctx, func(tx *bstore.Tx) (err error) {
3157 // See if we received a non-junk message from this organizational domain.
3158 q := bstore.QueryTx[store.Message](tx)
3159 q.FilterNonzero(store.Message{MsgFromOrgDomain: a0.d.m.MsgFromOrgDomain})
3160 q.FilterEqual("Notjunk", true)
3161 q.FilterEqual("IsReject", false)
3162 exists, err := q.Exists()
3164 return fmt.Errorf("querying for non-junk message from organizational domain: %v", err)
3170 // See if we sent a message to this organizational domain.
3171 qr := bstore.QueryTx[store.Recipient](tx)
3172 qr.FilterNonzero(store.Recipient{OrgDomain: a0.d.m.MsgFromOrgDomain})
3173 exists, err = qr.Exists()
3175 return fmt.Errorf("querying for message sent to organizational domain: %v", err)
3183 log.Errorx("checking if sender is unknown domain, for dmarc aggregate report evaluation", err)
3188 r := dmarcResult.Record
3189 addresses := make([]string, len(r.AggregateReportAddresses))
3190 for i, a := range r.AggregateReportAddresses {
3191 addresses[i] = a.String()
3193 sp := dmarcrpt.Disposition(r.SubdomainPolicy)
3194 if r.SubdomainPolicy == dmarc.PolicyEmpty {
3195 sp = dmarcrpt.Disposition(r.Policy)
3197 eval := dmarcdb.Evaluation{
3198 // Evaluated and IntervalHours set by AddEvaluation.
3199 PolicyDomain: dmarcResult.Domain.Name(),
3201 // Optional evaluations don't cause a report to be sent, but will be included.
3202 // Useful for automated inter-mailer messages, we don't want to get in a reporting
3203 // loop. We also don't want to be used for sending reports to unsuspecting domains
3204 // we have no relation with.
3205 // 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.
3206 Optional: a0.d.destination.DMARCReports || a0.d.destination.HostTLSReports || a0.d.destination.DomainTLSReports || a0.reason == reasonDMARCPolicy && unknownDomain(),
3208 Addresses: addresses,
3210 PolicyPublished: dmarcrpt.PolicyPublished{
3211 Domain: dmarcResult.Domain.Name(),
3212 ADKIM: dmarcrpt.Alignment(r.ADKIM),
3213 ASPF: dmarcrpt.Alignment(r.ASPF),
3214 Policy: dmarcrpt.Disposition(r.Policy),
3215 SubdomainPolicy: sp,
3216 Percentage: r.Percentage,
3217 // We don't save ReportingOptions, we don't do per-message failure reporting.
3219 SourceIP: c.remoteIP.String(),
3220 Disposition: disposition,
3221 AlignedDKIMPass: dmarcResult.AlignedDKIMPass,
3222 AlignedSPFPass: dmarcResult.AlignedSPFPass,
3223 EnvelopeTo: rcpt.Addr.IPDomain.String(),
3224 EnvelopeFrom: c.mailFrom.IPDomain.String(),
3225 HeaderFrom: msgFrom.Domain.Name(),
3228 for _, s := range dmarcOverrides {
3229 reason := dmarcrpt.PolicyOverrideReason{Type: dmarcrpt.PolicyOverride(s)}
3230 eval.OverrideReasons = append(eval.OverrideReasons, reason)
3233 // We'll include all signatures for the organizational domain, even if they weren't
3234 // relevant due to strict alignment requirement.
3235 for _, dkimResult := range dkimResults {
3236 if dkimResult.Sig == nil || publicsuffix.Lookup(ctx, log.Logger, msgFrom.Domain) != publicsuffix.Lookup(ctx, log.Logger, dkimResult.Sig.Domain) {
3239 r := dmarcrpt.DKIMAuthResult{
3240 Domain: dkimResult.Sig.Domain.Name(),
3241 Selector: dkimResult.Sig.Selector.ASCII,
3242 Result: dmarcrpt.DKIMResult(dkimResult.Status),
3244 eval.DKIMResults = append(eval.DKIMResults, r)
3247 switch receivedSPF.Identity {
3248 case spf.ReceivedHELO:
3249 spfAuthResult := dmarcrpt.SPFAuthResult{
3250 Domain: spfArgs.HelloDomain.String(), // Can be unicode and also IP.
3251 Scope: dmarcrpt.SPFDomainScopeHelo,
3252 Result: dmarcrpt.SPFResult(receivedSPF.Result),
3254 eval.SPFResults = []dmarcrpt.SPFAuthResult{spfAuthResult}
3255 case spf.ReceivedMailFrom:
3256 spfAuthResult := dmarcrpt.SPFAuthResult{
3257 Domain: spfArgs.MailFromDomain.Name(), // Can be unicode.
3258 Scope: dmarcrpt.SPFDomainScopeMailFrom,
3259 Result: dmarcrpt.SPFResult(receivedSPF.Result),
3261 eval.SPFResults = []dmarcrpt.SPFAuthResult{spfAuthResult}
3264 err := dmarcdb.AddEvaluation(ctx, dmarcResult.Record.AggregateReportingInterval, &eval)
3265 log.Check(err, "adding dmarc evaluation to database for aggregate report")
3269 for _, a := range la {
3270 // Don't add message if address was also explicitly present in a RCPT TO command.
3271 if rcpt.Alias != nil && regularRecipient(a.d.deliverTo) {
3275 conf, _ := a.d.acc.Conf()
3276 if conf.RejectsMailbox == "" {
3279 present, _, messagehash, err := rejectPresent(log, a.d.acc, conf.RejectsMailbox, a.d.m, dataFile)
3281 log.Errorx("checking whether reject is already present", err)
3284 log.Info("reject message is already present, ignoring")
3287 a.d.m.IsReject = true
3288 a.d.m.Seen = true // We don't want to draw attention.
3289 // Regular automatic junk flags configuration applies to these messages. The
3290 // default is to treat these as neutral, so they won't cause outright rejections
3291 // due to reputation for later delivery attempts.
3292 a.d.m.MessageHash = messagehash
3293 a.d.acc.WithWLock(func() {
3296 if !conf.KeepRejects {
3297 hasSpace, err = a.d.acc.TidyRejectsMailbox(c.log, conf.RejectsMailbox)
3300 log.Errorx("tidying rejects mailbox", err)
3301 } else if hasSpace {
3302 if err := a.d.acc.DeliverMailbox(log, conf.RejectsMailbox, a.d.m, dataFile); err != nil {
3303 log.Errorx("delivering spammy mail to rejects mailbox", err)
3305 log.Info("delivered spammy mail to rejects mailbox")
3308 log.Info("not storing spammy mail to full rejects mailbox")
3313 log.Info("incoming message rejected", slog.String("reason", a0.reason), slog.Any("msgfrom", msgFrom))
3314 metricDelivery.WithLabelValues("reject", a0.reason).Inc()
3316 addError(rcpt, a0.code, a0.secode, a0.userError, a0.errmsg)
3320 delayFirstTime := true
3321 if rcpt.Account != nil && a0.dmarcReport != nil {
3323 if err := dmarcdb.AddReport(ctx, a0.dmarcReport, msgFrom.Domain); err != nil {
3324 log.Errorx("saving dmarc aggregate report in database", err)
3326 log.Info("dmarc aggregate report processed")
3327 a0.d.m.Flags.Seen = true
3328 delayFirstTime = false
3331 if rcpt.Account != nil && a0.tlsReport != nil {
3332 // todo future: add rate limiting to prevent DoS attacks.
3333 if err := tlsrptdb.AddReport(ctx, c.log, msgFrom.Domain, c.mailFrom.String(), a0.d.destination.HostTLSReports, a0.tlsReport); err != nil {
3334 log.Errorx("saving TLSRPT report in database", err)
3336 log.Info("tlsrpt report processed")
3337 a0.d.m.Flags.Seen = true
3338 delayFirstTime = false
3342 // If this is a first-time sender and not a forwarded/mailing list message, wait
3343 // before actually delivering. If this turns out to be a spammer, we've kept one of
3344 // their connections busy.
3345 a0conf, _ := a0.d.acc.Conf()
3346 if delayFirstTime && !a0.d.m.IsForward && !a0.d.m.IsMailingList && a0.reason == reasonNoBadSignals && !a0conf.NoFirstTimeSenderDelay && c.firstTimeSenderDelay > 0 {
3347 log.Debug("delaying before delivering from sender without reputation", slog.Duration("delay", c.firstTimeSenderDelay))
3348 mox.Sleep(mox.Context, c.firstTimeSenderDelay)
3352 code, timeout := mox.LocalserveNeedsError(rcpt.Addr.Localpart)
3354 log.Info("timing out due to special localpart")
3355 mox.Sleep(mox.Context, time.Hour)
3356 xsmtpServerErrorf(codes{smtp.C451LocalErr, smtp.SeOther00}, "timing out delivery due to special localpart")
3357 } else if code != 0 {
3358 log.Info("failure due to special localpart", slog.Int("code", code))
3359 metricDelivery.WithLabelValues("delivererror", "localserve").Inc()
3360 addError(rcpt, code, smtp.SeOther00, false, fmt.Sprintf("failure with code %d due to special localpart", code))
3365 // Gather the message-id before we deliver and the file may be consumed.
3366 if !parsedMessageID {
3367 if p, err := message.Parse(c.log.Logger, false, store.FileMsgReader(a0.d.m.MsgPrefix, dataFile)); err != nil {
3368 log.Infox("parsing message for message-id", err)
3369 } else if header, err := p.Header(); err != nil {
3370 log.Infox("parsing message header for message-id", err)
3372 messageID = header.Get("Message-Id")
3374 parsedMessageID = true
3377 // Finally deliver the message to the account(s).
3378 var nerr int // Number of non-quota errors.
3379 var nfull int // Number of failed deliveries due to over quota.
3380 var ndelivered int // Number delivered to account.
3381 for _, a := range la {
3382 // Don't deliver to recipient that was explicitly present in SMTP transaction, or
3383 // is sending the message to an alias they are member of.
3384 if rcpt.Alias != nil && (regularRecipient(a.d.deliverTo) || a.d.deliverTo.Equal(msgFrom.Path())) {
3389 a.d.acc.WithWLock(func() {
3390 if err := a.d.acc.DeliverMailbox(log, a.mailbox, a.d.m, dataFile); err != nil {
3391 log.Errorx("delivering", err)
3392 metricDelivery.WithLabelValues("delivererror", a0.reason).Inc()
3393 if errors.Is(err, store.ErrOverQuota) {
3396 addError(rcpt, smtp.C451LocalErr, smtp.SeSys3Other0, false, "error processing")
3403 metricDelivery.WithLabelValues("delivered", a0.reason).Inc()
3404 log.Info("incoming message delivered", slog.String("reason", a0.reason), slog.Any("msgfrom", msgFrom))
3406 conf, _ := a.d.acc.Conf()
3407 if conf.RejectsMailbox != "" && a.d.m.MessageID != "" {
3408 if err := a.d.acc.RejectsRemove(log, conf.RejectsMailbox, a.d.m.MessageID); err != nil {
3409 log.Errorx("removing message from rejects mailbox", err, slog.String("messageid", messageID))
3414 // Pass delivered messages to queue for DSN processing and/or hooks.
3416 mr := store.FileMsgReader(a.d.m.MsgPrefix, dataFile)
3417 part, err := a.d.m.LoadPart(mr)
3419 log.Errorx("loading parsed part for evaluating webhook", err)
3421 err = queue.Incoming(context.Background(), log, a.d.acc, messageID, *a.d.m, part, a.mailbox)
3422 log.Check(err, "queueing webhook for incoming delivery")
3424 } else if nerr > 0 && ndelivered == 0 {
3425 // Don't continue if we had an error and haven't delivered yet. If we only had
3426 // quota-related errors, we keep trying for an account to deliver to.
3430 if ndelivered == 0 && (nerr > 0 || nfull > 0) {
3432 addError(rcpt, smtp.C452StorageFull, smtp.SeMailbox2Full2, true, "account storage full")
3434 addError(rcpt, smtp.C451LocalErr, smtp.SeSys3Other0, false, "error processing")
3439 // For each recipient, do final spam analysis and delivery.
3440 for _, rcpt := range c.recipients {
3441 processRecipient(rcpt)
3444 // If all recipients failed to deliver, return an error.
3445 if len(c.recipients) == len(deliverErrors) {
3447 e0 := deliverErrors[0]
3448 var serverError bool
3451 for _, e := range deliverErrors {
3452 serverError = serverError || !e.userError
3453 if e.code != e0.code || e.secode != e0.secode {
3456 msgs = append(msgs, e.errmsg)
3462 xsmtpErrorf(e0.code, e0.secode, !serverError, "%s", strings.Join(msgs, "\n"))
3465 // Not all failures had the same error. We'll return each error on a separate line.
3467 for _, e := range deliverErrors {
3468 s := fmt.Sprintf("%d %d.%s %s", e.code, e.code/100, e.secode, e.errmsg)
3469 lines = append(lines, s)
3471 code := smtp.C451LocalErr
3472 secode := smtp.SeSys3Other0
3474 code = smtp.C554TransactionFailed
3476 lines = append(lines, "multiple errors")
3477 xsmtpErrorf(code, secode, !serverError, "%s", strings.Join(lines, "\n"))
3479 // Generate one DSN for all failed recipients.
3480 if len(deliverErrors) > 0 {
3482 dsnMsg := dsn.Message{
3483 SMTPUTF8: c.msgsmtputf8,
3484 From: smtp.Path{Localpart: "postmaster", IPDomain: deliverErrors[0].rcptTo.IPDomain},
3486 Subject: "mail delivery failure",
3487 MessageID: mox.MessageIDGen(false),
3488 References: messageID,
3490 // Per-message details.
3491 ReportingMTA: mox.Conf.Static.HostnameDomain.ASCII,
3492 ReceivedFromMTA: smtp.Ehlo{Name: c.hello, ConnIP: c.remoteIP},
3496 if len(deliverErrors) > 1 {
3497 dsnMsg.TextBody = "Multiple delivery failures occurred.\n\n"
3500 for _, e := range deliverErrors {
3502 if e.code/100 == 4 {
3505 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))
3506 rcpt := dsn.Recipient{
3507 FinalRecipient: e.rcptTo,
3509 Status: fmt.Sprintf("%d.%s", e.code/100, e.secode),
3510 LastAttemptDate: now,
3512 dsnMsg.Recipients = append(dsnMsg.Recipients, rcpt)
3515 header, err := message.ReadHeaders(bufio.NewReader(&moxio.AtReader{R: dataFile}))
3517 c.log.Errorx("reading headers of incoming message for dsn, continuing dsn without headers", err)
3519 dsnMsg.Original = header
3522 c.log.Error("not queueing dsn for incoming delivery due to localserve")
3523 } else if err := queueDSN(context.TODO(), c.log, c, *c.mailFrom, dsnMsg, c.requireTLS != nil && *c.requireTLS); err != nil {
3524 metricServerErrors.WithLabelValues("queuedsn").Inc()
3525 c.log.Errorx("queuing DSN for incoming delivery, no DSN sent", err)
3530 c.transactionBad-- // Compensate for early earlier pessimistic increase.
3532 c.writecodeline(smtp.C250Completed, smtp.SeMailbox2Other0, "it is done", nil)
3535// Return whether msgFrom address is allowed to send a message to alias.
3536func aliasAllowedMsgFrom(alias config.Alias, msgFrom smtp.Address) bool {
3537 for _, aa := range alias.ParsedAddresses {
3538 if aa.Address == msgFrom {
3542 lp, err := smtp.ParseLocalpart(alias.LocalpartStr)
3543 xcheckf(err, "parsing alias localpart")
3544 if msgFrom == smtp.NewAddress(lp, alias.Domain) {
3545 return alias.AllowMsgFrom
3547 return alias.PostPublic
3550// ecode returns either ecode, or a more specific error based on err.
3551// For example, ecode can be turned from an "other system" error into a "mail
3552// system full" if the error indicates no disk space is available.
3553func errCodes(code int, ecode string, err error) codes {
3555 case moxio.IsStorageSpace(err):
3557 case smtp.SeMailbox2Other0:
3558 if code == smtp.C451LocalErr {
3559 code = smtp.C452StorageFull
3561 ecode = smtp.SeMailbox2Full2
3562 case smtp.SeSys3Other0:
3563 if code == smtp.C451LocalErr {
3564 code = smtp.C452StorageFull
3566 ecode = smtp.SeSys3StorageFull1
3569 return codes{code, ecode}
3573func (c *conn) cmdRset(p *parser) {
3578 c.bwritecodeline(smtp.C250Completed, smtp.SeOther00, "all clear", nil)
3582func (c *conn) cmdVrfy(p *parser) {
3583 // No EHLO/HELO needed.
3594 // todo future: we could support vrfy and expn for submission? though would need to see if its rfc defines it.
3597 xsmtpUserErrorf(smtp.C252WithoutVrfy, smtp.SePol7Other0, "no verify but will try delivery")
3601func (c *conn) cmdExpn(p *parser) {
3602 // No EHLO/HELO needed.
3613 // todo: we could implement expn for local aliases for authenticated users, when members have permission to list. would anyone use it?
3616 xsmtpUserErrorf(smtp.C252WithoutVrfy, smtp.SePol7Other0, "no expand but will try delivery")
3620func (c *conn) cmdHelp(p *parser) {
3621 // Let's not strictly parse the request for help. We are ignoring the text anyway.
3624 c.bwritecodeline(smtp.C214Help, smtp.SeOther00, "see rfc 5321 (smtp)", nil)
3628func (c *conn) cmdNoop(p *parser) {
3629 // No idea why, but if an argument follows, it must adhere to the string ABNF production...
3636 c.bwritecodeline(smtp.C250Completed, smtp.SeOther00, "alrighty", nil)
3640func (c *conn) cmdQuit(p *parser) {
3644 c.writecodeline(smtp.C221Closing, smtp.SeOther00, "okay thanks bye", nil)