1// Package smtpserver implements an SMTP server for submission and incoming delivery of mail messages.
2package smtpserver
3
4import (
5 "bufio"
6 "bytes"
7 "context"
8 "crypto/ed25519"
9 "crypto/md5"
10 cryptorand "crypto/rand"
11 "crypto/rsa"
12 "crypto/sha1"
13 "crypto/sha256"
14 "crypto/tls"
15 "crypto/x509"
16 "encoding/base64"
17 "errors"
18 "fmt"
19 "hash"
20 "io"
21 "log/slog"
22 "math"
23 "net"
24 "net/textproto"
25 "os"
26 "runtime/debug"
27 "slices"
28 "sort"
29 "strings"
30 "sync"
31 "time"
32 "unicode"
33
34 "golang.org/x/exp/maps"
35 "golang.org/x/text/unicode/norm"
36
37 "github.com/prometheus/client_golang/prometheus"
38 "github.com/prometheus/client_golang/prometheus/promauto"
39
40 "github.com/mjl-/bstore"
41
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"
65)
66
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")
70
71// If set, regular delivery/submit is sidestepped, email is accepted and
72// delivered to the account named mox.
73var Localserve bool
74
75var limiterConnectionRate, limiterConnections *ratelimit.Limiter
76
77// For delivery rate limiting. Variable because changed during tests.
78var limitIPMasked1MessagesPerMinute int = 500
79var limitIPMasked1SizePerMinute int64 = 1000 * 1024 * 1024
80
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
84
85func init() {
86 // Also called by tests, so they don't trigger the rate limiter.
87 limitersInit()
88}
89
90func limitersInit() {
91 mox.LimitersInit()
92 // todo future: make these configurable
93 limiterConnectionRate = &ratelimit.Limiter{
94 WindowLimits: []ratelimit.WindowLimit{
95 {
96 Window: time.Minute,
97 Limits: [...]int64{300, 900, 2700},
98 },
99 },
100 }
101 limiterConnections = &ratelimit.Limiter{
102 WindowLimits: []ratelimit.WindowLimit{
103 {
104 Window: time.Duration(math.MaxInt64), // All of time.
105 Limits: [...]int64{30, 90, 270},
106 },
107 },
108 }
109}
110
111var (
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.
117)
118
119type codes struct {
120 code int
121 secode string // Enhanced code, but without the leading major int from code.
122}
123
124var (
125 metricConnection = promauto.NewCounterVec(
126 prometheus.CounterOpts{
127 Name: "mox_smtpserver_connection_total",
128 Help: "Incoming SMTP connections.",
129 },
130 []string{
131 "kind", // "deliver" or "submit"
132 },
133 )
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},
139 },
140 []string{
141 "kind", // "deliver" or "submit"
142 "cmd",
143 "code",
144 "ecode",
145 },
146 )
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.",
151 },
152 []string{
153 "result",
154 "reason",
155 },
156 )
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.",
162 },
163 []string{
164 "result",
165 },
166 )
167 metricServerErrors = promauto.NewCounterVec(
168 prometheus.CounterOpts{
169 Name: "mox_smtpserver_errors_total",
170 Help: "SMTP server errors, known values: dkimsign, queuedsn.",
171 },
172 []string{
173 "error",
174 },
175 )
176 metricDeliveryStarttls = promauto.NewCounter(
177 prometheus.CounterOpts{
178 Name: "mox_smtpserver_delivery_starttls_total",
179 Help: "Total number of STARTTLS handshakes for incoming deliveries.",
180 },
181 )
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.",
186 },
187 []string{
188 "reason", // "eof", "sslv2", "unsupportedversions", "nottls", "alert-<num>-<msg>", "other"
189 },
190 )
191)
192
193var jitterRand = mox.NewPseudoRand()
194
195func durationDefault(delay *time.Duration, def time.Duration) time.Duration {
196 if delay == nil {
197 return def
198 }
199 return *delay
200}
201
202// Listen initializes network listeners for incoming SMTP connection.
203// The listeners are stored for a later call to Serve.
204func Listen() {
205 names := maps.Keys(mox.Conf.Static.Listeners)
206 sort.Strings(names)
207 for _, name := range names {
208 listener := mox.Conf.Static.Listeners[name]
209
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
217 }
218
219 maxMsgSize := listener.SMTPMaxMessageSize
220 if maxMsgSize == 0 {
221 maxMsgSize = config.DefaultMaxMsgSize
222 }
223
224 if listener.SMTP.Enabled {
225 hostname := mox.Conf.Static.HostnameDomain
226 if listener.Hostname != "" {
227 hostname = listener.HostnameDomain
228 }
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
238 }
239 listen1("smtp", name, ip, port, hostname, tlsConfigDelivery, false, false, maxMsgSize, false, listener.SMTP.RequireSTARTTLS, !listener.SMTP.NoRequireTLS, listener.SMTP.DNSBLZones, firstTimeSenderDelay)
240 }
241 }
242 if listener.Submission.Enabled {
243 hostname := mox.Conf.Static.HostnameDomain
244 if listener.Hostname != "" {
245 hostname = listener.HostnameDomain
246 }
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)
250 }
251 }
252
253 if listener.Submissions.Enabled {
254 hostname := mox.Conf.Static.HostnameDomain
255 if listener.Hostname != "" {
256 hostname = listener.HostnameDomain
257 }
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)
261 }
262 }
263 }
264}
265
266var servers []func()
267
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))
276 }
277 network := mox.Network(ip)
278 ln, err := mox.Listen(network, addr)
279 if err != nil {
280 log.Fatalx("smtp: listen for smtp", err, slog.String("protocol", protocol), slog.String("listener", name))
281 }
282
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)
290 }
291
292 serve := func() {
293 for {
294 conn, err := ln.Accept()
295 if err != nil {
296 log.Infox("smtp: accept", err, slog.String("protocol", protocol), slog.String("listener", name))
297 continue
298 }
299
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)
303 }
304 }
305
306 servers = append(servers, serve)
307}
308
309// Serve starts serving on all listeners, launching a goroutine per listener.
310func Serve() {
311 for _, serve := range servers {
312 go serve()
313 }
314}
315
316type conn struct {
317 cid int64
318
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).
323 origConn net.Conn
324 conn net.Conn
325
326 tls bool
327 extRequireTLS bool // Whether to announce and allow the REQUIRETLS extension.
328 resolver dns.Resolver
329 r *bufio.Reader
330 w *bufio.Writer
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.
335 submission bool // ../rfc/6409:19 applies
336 baseTLSConfig *tls.Config
337 localIP net.IP
338 remoteIP net.IP
339 hostname dns.Domain
340 log mlog.Log
341 maxMessageSize int64
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.
347 dnsBLs []dns.Domain
348 firstTimeSenderDelay time.Duration
349
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.
352 deadline time.Time
353
354 hello dns.IPDomain // Claimed remote name. Can be ip address for ehlo.
355 ehlo bool // If set, we had EHLO instead of HELO.
356
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.
362
363 // We track good/bad message transactions to disconnect spammers trying to guess addresses.
364 transactionGood int
365 transactionBad int
366
367 // Message transaction.
368 mailFrom *smtp.Path
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
376}
377
378type rcptAccount struct {
379 AccountName string
380 Destination config.Destination
381 CanonicalAddress string // Optional catchall part stripped and/or lowercased.
382}
383
384type rcptAlias struct {
385 Alias config.Alias
386 CanonicalAddress string // Optional catchall part stripped and/or lowercased.
387}
388
389type recipient struct {
390 Addr smtp.Path
391
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.
397}
398
399func isClosed(err error) bool {
400 return errors.Is(err, errIO) || moxio.IsClosed(err)
401}
402
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 {
406 if !c.submission {
407 return c.baseTLSConfig
408 }
409
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()
414
415 // Allow client certificate authentication, for use with the sasl "external"
416 // authentication mechanism.
417 tlsConf.ClientAuth = tls.RequestClientCert
418
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
423
424 return tlsConf
425}
426
427// tlsClientAuthVerifyPeerCert can be used as tls.Config.VerifyPeerCertificate, and
428// sets authentication-related fields on conn. This is not called on resumed TLS
429// connections.
430func (c *conn) tlsClientAuthVerifyPeerCert(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
431 if len(rawCerts) == 0 {
432 return nil
433 }
434
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
437 // handshake.
438 if !mox.LimiterFailedAuth.CanAdd(c.remoteIP, time.Now(), 1) {
439 return nil
440 }
441
442 cert, err := x509.ParseCertificate(rawCerts[0])
443 if err != nil {
444 c.log.Debugx("parsing tls client certificate", err)
445 return err
446 }
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)
450 }
451 return nil
452}
453
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")
459 }
460
461 authResult := "error"
462 defer func() {
463 metrics.AuthenticationInc("submission", "tlsclientauth", authResult)
464 if authResult == "ok" {
465 mox.LimiterFailedAuth.Reset(c.remoteIP, time.Now())
466 } else {
467 mox.LimiterFailedAuth.Add(c.remoteIP, time.Now(), 1)
468 }
469 }()
470
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)
474 }
475 c.authFailed++ // Compensated on success.
476 defer func() {
477 // On the 3rd failed authentication, start responding slowly. Successful auth will
478 // cause fast responses again.
479 if c.authFailed >= 3 {
480 c.setSlow(true)
481 }
482 }()
483
484 shabuf := sha256.Sum256(cert.RawSubjectPublicKeyInfo)
485 fp := base64.RawURLEncoding.EncodeToString(shabuf[:])
486 pubKey, err := store.TLSPublicKeyGet(context.TODO(), fp)
487 if err != nil {
488 if err == bstore.ErrAbsent {
489 authResult = "badcreds"
490 }
491 return fmt.Errorf("looking up tls public key with fingerprint %s: %v", fp, err)
492 }
493
494 // Verify account exists and still matches address.
495 acc, _, err := store.OpenEmail(c.log, pubKey.LoginAddress)
496 if err != nil {
497 return fmt.Errorf("opening account for address %s for public key %s: %w", pubKey.LoginAddress, fp, err)
498 }
499 defer func() {
500 if acc != nil {
501 err := acc.Close()
502 c.log.Check(err, "close account")
503 }
504 }()
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)
507 }
508
509 authResult = "ok"
510 c.authFailed = 0
511 c.account = acc
512 acc = nil // Prevent cleanup by defer.
513 c.username = pubKey.LoginAddress
514 c.authTLS = true
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))
520 return nil
521}
522
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())
527 c.conn = tlsConn
528
529 cidctx := context.WithValue(mox.Context, mlog.CidKey, c.cid)
530 ctx, cancel := context.WithTimeout(cidctx, time.Minute)
531 defer cancel()
532 c.log.Debug("starting tls server handshake")
533 if !c.submission {
534 metricDeliveryStarttls.Inc()
535 }
536 if err := tlsConn.HandshakeContext(ctx); err != nil {
537 if !c.submission {
538 // Errors from crypto/tls mostly aren't typed. We'll have to look for strings...
539 reason := "other"
540 if errors.Is(err, io.EOF) {
541 reason = "eof"
542 } else if alert, ok := mox.AsTLSAlert(err); ok {
543 reason = tlsrpt.FormatAlert(alert)
544 } else {
545 s := err.Error()
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") {
549 reason = "nottls"
550 } else if strings.Contains(s, "tls: unsupported SSLv2 handshake received") {
551 reason = "sslv2"
552 }
553 }
554 metricDeliveryStarttlsErrors.WithLabelValues(reason).Inc()
555 }
556 panic(fmt.Errorf("tls handshake: %s (%w)", err, errIO))
557 }
558 cancel()
559
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])
564 if err != nil {
565 panic(fmt.Errorf("tls verify client certificate after resumption: %s (%w)", err, errIO))
566 }
567 }
568
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)),
575 }
576 if c.account != nil {
577 attrs = append(attrs,
578 slog.String("account", c.account.Name),
579 slog.String("username", c.username),
580 )
581 }
582 c.log.Debug("tls handshake completed", attrs...)
583}
584
585type tlsVersion uint16
586
587func (v tlsVersion) String() string {
588 return strings.ReplaceAll(strings.ToLower(tls.VersionName(uint16(v))), " ", "-")
589}
590
591// completely reset connection state as if greeting has just been sent.
592// ../rfc/3207:210
593func (c *conn) reset() {
594 c.ehlo = false
595 c.hello = dns.IPDomain{}
596 if !c.authTLS {
597 c.username = ""
598 if c.account != nil {
599 err := c.account.Close()
600 c.log.Check(err, "closing account")
601 }
602 c.account = nil
603 }
604 c.authSASL = false
605 c.rset()
606}
607
608// for rset command, and a few more cases that reset the mail transaction state.
609// ../rfc/5321:2502
610func (c *conn) rset() {
611 c.mailFrom = nil
612 c.requireTLS = nil
613 c.futureRelease = time.Time{}
614 c.futureReleaseRequest = ""
615 c.has8bitmime = false
616 c.smtputf8 = false
617 c.msgsmtputf8 = false
618 c.recipients = nil
619}
620
621func (c *conn) earliestDeadline(d time.Duration) time.Time {
622 e := time.Now().Add(d)
623 if !c.deadline.IsZero() && c.deadline.Before(e) {
624 return c.deadline
625 }
626 return e
627}
628
629func (c *conn) xcheckAuth() {
630 if c.submission && c.account == nil {
631 // ../rfc/4954:623
632 xsmtpUserErrorf(smtp.C530SecurityRequired, smtp.SePol7Other0, "authentication required")
633 }
634}
635
636func (c *conn) xtrace(level slog.Level) func() {
637 c.xflush()
638 c.tr.SetTrace(level)
639 c.tw.SetTrace(level)
640 return func() {
641 c.xflush()
642 c.tr.SetTrace(mlog.LevelTrace)
643 c.tw.SetTrace(mlog.LevelTrace)
644 }
645}
646
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
649// down spammers.
650func (c *conn) setSlow(on bool) {
651 if on && !c.slow {
652 c.log.Debug("connection changed to slow")
653 } else if !on && c.slow {
654 c.log.Debug("connection restored to regular pace")
655 }
656 c.slow = on
657}
658
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) {
662 chunk := len(buf)
663 if c.slow {
664 chunk = 1
665 }
666
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
674 // being slow.
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)
678 }
679
680 var n int
681 for len(buf) > 0 {
682 nn, err := c.conn.Write(buf[:chunk])
683 if err != nil {
684 panic(fmt.Errorf("write: %s (%w)", err, errIO))
685 }
686 n += nn
687 buf = buf[chunk:]
688 if len(buf) > 0 && badClientDelay > 0 {
689 mox.Sleep(mox.Context, badClientDelay)
690
691 // Make sure we don't take too long, otherwise the remote SMTP client may close the
692 // connection.
693 if time.Until(deadline) < 5*badClientDelay {
694 chunk = len(buf)
695 }
696 }
697 }
698 return n, nil
699}
700
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)
706 }
707
708 // todo future: make deadline configurable for callers, and through config file? ../rfc/5321:3610 ../rfc/6409:492
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)
712 }
713
714 n, err := c.conn.Read(buf)
715 if err != nil {
716 panic(fmt.Errorf("read: %s (%w)", err, errIO))
717 }
718 return n, err
719}
720
721// Cache of line buffers for reading commands.
722// Filled on demand.
723var bufpool = moxio.NewBufpool(8, 2*1024)
724
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))
732 }
733 return line
734}
735
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) {
739 var ecode string
740 if secode != "" {
741 ecode = fmt.Sprintf("%d.%s", code/100, secode)
742 }
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)))
750
751 var sep string
752 if ecode != "" {
753 sep = " "
754 }
755
756 // Separate by newline and wrap long lines.
757 lines := strings.Split(msg, "\n")
758 for i, line := range lines {
759 // ../rfc/5321:3506 ../rfc/5321:2583 ../rfc/5321:2756
760 var prelen = 3 + 1 + len(ecode) + len(sep)
761 for prelen+len(line) > 510 {
762 e := 510 - prelen
763 for ; e > 400 && line[e] != ' '; e-- {
764 }
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])
767 line = line[e:]
768 }
769 spdash := " "
770 if i < len(lines)-1 {
771 spdash = "-"
772 }
773 c.bwritelinef("%d%s%s%s%s", code, spdash, ecode, sep, line)
774 }
775}
776
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")
781}
782
783// Flush pending buffered writes to connection.
784func (c *conn) xflush() {
785 c.w.Flush() // Errors will have caused a panic in Write.
786}
787
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)
791 c.xflush()
792}
793
794// Write (with flush) a formatted response line to connection.
795func (c *conn) writelinef(format string, args ...any) {
796 c.bwritelinef(format, args...)
797 c.xflush()
798}
799
800var cleanClose struct{} // Sentinel value for panic/recover indicating clean close of connection.
801
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 {
805 localIP = a.IP
806 } else {
807 // For net.Pipe, during tests.
808 localIP = net.ParseIP("127.0.0.10")
809 }
810 if a, ok := nc.RemoteAddr().(*net.TCPAddr); ok {
811 remoteIP = a.IP
812 } else {
813 // For net.Pipe, during tests.
814 remoteIP = net.ParseIP("127.0.0.10")
815 }
816
817 c := &conn{
818 cid: cid,
819 origConn: nc,
820 conn: nc,
821 submission: submission,
822 tls: xtls,
823 extRequireTLS: requireTLS,
824 resolver: resolver,
825 lastlog: time.Now(),
826 baseTLSConfig: tlsConfig,
827 localIP: localIP,
828 remoteIP: remoteIP,
829 hostname: hostname,
830 maxMessageSize: maxMessageSize,
831 requireTLSForAuth: requireTLSForAuth,
832 requireTLSForDelivery: requireTLSForDelivery,
833 dnsBLs: dnsBLs,
834 firstTimeSenderDelay: firstTimeSenderDelay,
835 }
836 var logmutex sync.Mutex
837 c.log = mlog.New("smtpserver", nil).WithFunc(func() []slog.Attr {
838 logmutex.Lock()
839 defer logmutex.Unlock()
840 now := time.Now()
841 l := []slog.Attr{
842 slog.Int64("cid", c.cid),
843 slog.Duration("delta", now.Sub(c.lastlog)),
844 }
845 c.lastlog = now
846 if c.username != "" {
847 l = append(l, slog.String("username", c.username))
848 }
849 return l
850 })
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)
855
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))
863
864 defer func() {
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.
867
868 if c.account != nil {
869 err := c.account.Close()
870 c.log.Check(err, "closing account")
871 c.account = nil
872 }
873
874 x := recover()
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)
879 } else {
880 c.log.Error("unhandled panic", slog.Any("err", x))
881 debug.PrintStack()
882 metrics.PanicInc(metrics.Smtpserver)
883 }
884 }()
885
886 if xtls {
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)
890 }
891
892 select {
893 case <-mox.Shutdown.Done():
894 // ../rfc/5321:2811 ../rfc/5321:1666 ../rfc/3463:420
895 c.writecodeline(smtp.C421ServiceUnavail, smtp.SeSys3NotAccepting2, "shutting down", nil)
896 return
897 default:
898 }
899
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)
902 return
903 }
904
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)
910 return
911 }
912
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)
916 return
917 }
918 defer limiterConnections.Add(c.remoteIP, time.Now(), -1)
919
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)
924
925 // ../rfc/5321:964 ../rfc/5321:4294 about announcing software and version
926 // Syntax: ../rfc/5321:2586
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)
931
932 for {
933 command(c)
934
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.
937 n := c.r.Buffered()
938 if n > 0 {
939 buf, err := c.r.Peek(n)
940 if err == nil && bytes.IndexByte(buf, '\n') >= 0 {
941 continue
942 }
943 }
944 c.xflush()
945 }
946}
947
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,
962}
963
964func command(c *conn) {
965 defer func() {
966 x := recover()
967 if x == nil {
968 return
969 }
970 err, ok := x.(error)
971 if !ok {
972 panic(x)
973 }
974
975 if isClosed(err) {
976 panic(err)
977 }
978
979 var serr smtpError
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)
982 if serr.printStack {
983 c.log.Errorx("smtp error", serr.err, slog.Int("code", serr.code), slog.String("secode", serr.secode))
984 debug.PrintStack()
985 }
986 } else {
987 // Other type of panic, we pass it on, aborting the connection.
988 c.log.Errorx("command panic", err)
989 panic(err)
990 }
991 }()
992
993 // todo future: we could wait for either a line or shutdown, and just close the connection on shutdown.
994
995 line := c.readline()
996 t := strings.SplitN(line, " ", 2)
997 var args string
998 if len(t) == 2 {
999 args = " " + t[1]
1000 }
1001 cmd := t[0]
1002 cmdl := strings.ToLower(cmd)
1003
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
1005
1006 select {
1007 case <-mox.Shutdown.Done():
1008 // ../rfc/5321:2811 ../rfc/5321:1666 ../rfc/3463:420
1009 c.writecodeline(smtp.C421ServiceUnavail, smtp.SeSys3NotAccepting2, "shutting down", nil)
1010 panic(errIO)
1011 default:
1012 }
1013
1014 c.cmd = cmdl
1015 c.cmdStart = time.Now()
1016
1017 p := newParser(args, c.smtputf8, c)
1018 fn, ok := commands[cmdl]
1019 if !ok {
1020 c.cmd = "(unknown)"
1021 if c.ncmds == 0 {
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
1024 // lines.
1025 c.writecodeline(smtp.C500BadSyntax, smtp.SeProto5Syntax2, "please try again speaking smtp", nil)
1026 panic(errIO)
1027 }
1028 // note: not "command not implemented", see ../rfc/5321:2934 ../rfc/5321:2539
1029 xsmtpUserErrorf(smtp.C500BadSyntax, smtp.SeProto5BadCmdOrSeq1, "unknown command")
1030 }
1031 c.ncmds++
1032 fn(c, p)
1033}
1034
1035// For use in metric labels.
1036func (c *conn) kind() string {
1037 if c.submission {
1038 return "submission"
1039 }
1040 return "smtp"
1041}
1042
1043func (c *conn) xneedHello() {
1044 if c.hello.IsZero() {
1045 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "no ehlo/helo yet")
1046 }
1047}
1048
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
1053 // TLS interopability problems. ../rfc/8460:316
1054 if c.requireTLSForDelivery && !c.tls && !isTLSReportRecipient(rcpt) {
1055 // ../rfc/3207:148
1056 xsmtpUserErrorf(smtp.C530SecurityRequired, smtp.SePol7Other0, "STARTTLS required for mail delivery")
1057 }
1058}
1059
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)
1063}
1064
1065func (c *conn) cmdHelo(p *parser) {
1066 c.cmdHello(p, false)
1067}
1068
1069func (c *conn) cmdEhlo(p *parser) {
1070 c.cmdHello(p, true)
1071}
1072
1073// ../rfc/5321:1783
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}
1081 } else {
1082 p.xspace()
1083 if ehlo {
1084 remote = p.xipdomain(true)
1085 } else {
1086 remote = dns.IPDomain{Domain: p.xdomain()}
1087
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+".")
1092 cancel()
1093 if dns.IsNotFound(err) {
1094 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeProto5Other0, "your ehlo domain does not resolve to an IP address")
1095 }
1096 // For success or temporary resolve errors, we'll just continue.
1097 }
1098 // ../rfc/5321:1827
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() {
1103 p.remainder() // ../rfc/5321:1802 ../rfc/2821:1632
1104 }
1105 p.xend()
1106 }
1107
1108 // Reset state as if RSET command has been issued. ../rfc/5321:2093 ../rfc/5321:2453
1109 c.rset()
1110
1111 c.ehlo = ehlo
1112 c.hello = remote
1113
1114 // https://www.iana.org/assignments/mail-parameters/mail-parameters.xhtml
1115
1116 c.bwritelinef("250-%s", c.hostname.ASCII)
1117 c.bwritelinef("250-PIPELINING") // ../rfc/2920:108
1118 c.bwritelinef("250-SIZE %d", c.maxMessageSize) // ../rfc/1870:70
1119 // ../rfc/3207:237
1120 if !c.tls && c.baseTLSConfig != nil {
1121 // ../rfc/3207:90
1122 c.bwritelinef("250-STARTTLS")
1123 } else if c.extRequireTLS {
1124 // ../rfc/8689:202
1125 // ../rfc/8689:143
1126 c.bwritelinef("250-REQUIRETLS")
1127 }
1128 if c.submission {
1129 var mechs string
1130 // ../rfc/4954:123
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"
1138 }
1139 if c.tls && len(c.conn.(*tls.Conn).ConnectionState().PeerCertificates) > 0 {
1140 mechs = "EXTERNAL " + mechs
1141 }
1142 c.bwritelinef("250-AUTH %s", mechs)
1143 // ../rfc/4865:127
1144 t := time.Now().Add(queue.FutureReleaseIntervalMax).UTC() // ../rfc/4865:98
1145 c.bwritelinef("250-FUTURERELEASE %d %s", queue.FutureReleaseIntervalMax/time.Second, t.Format(time.RFC3339))
1146 }
1147 c.bwritelinef("250-ENHANCEDSTATUSCODES") // ../rfc/2034:71
1148 // todo future? c.writelinef("250-DSN")
1149 c.bwritelinef("250-8BITMIME") // ../rfc/6152:86
1150 c.bwritelinef("250-LIMITS RCPTMAX=%d", rcptToLimit) // ../rfc/9422:301
1151 c.bwritecodeline(250, "", "SMTPUTF8", nil) // ../rfc/6531:201
1152 c.xflush()
1153}
1154
1155// ../rfc/3207:96
1156func (c *conn) cmdStarttls(p *parser) {
1157 c.xneedHello()
1158 p.xend()
1159
1160 if c.tls {
1161 // ../rfc/3207:235
1162 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "already speaking tls")
1163 }
1164 if c.account != nil {
1165 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "cannot starttls after authentication")
1166 }
1167 if c.baseTLSConfig == nil {
1168 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "starttls not offered")
1169 }
1170
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
1174 // handshake.
1175 conn := c.conn
1176 if n := c.r.Buffered(); n > 0 {
1177 conn = &moxio.PrefixConn{
1178 PrefixReader: io.LimitReader(c.r, int64(n)),
1179 Conn: conn,
1180 }
1181 }
1182
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)
1185
1186 c.xtlsHandshakeAndAuthenticate(conn)
1187
1188 c.reset() // ../rfc/3207:210
1189 c.tls = true
1190}
1191
1192// ../rfc/4954:139
1193func (c *conn) cmdAuth(p *parser) {
1194 c.xneedHello()
1195
1196 if !c.submission {
1197 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "authentication only allowed on submission ports")
1198 }
1199 if c.authSASL {
1200 // ../rfc/4954:152
1201 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "already authenticated")
1202 }
1203 if c.mailFrom != nil {
1204 // ../rfc/4954:157
1205 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "authentication not allowed during mail transaction")
1206 }
1207
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
1212
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.
1215 // ../rfc/4954:770
1216 if c.authFailed > 3 && authFailDelay > 0 {
1217 // ../rfc/4954:770
1218 mox.Sleep(mox.Context, time.Duration(c.authFailed-3)*authFailDelay)
1219 }
1220 c.authFailed++ // Compensated on success.
1221 defer func() {
1222 if missingDerivedSecrets {
1223 c.authFailed--
1224 }
1225 // On the 3rd failed authentication, start responding slowly. Successful auth will
1226 // cause fast responses again.
1227 if c.authFailed >= 3 {
1228 c.setSlow(true)
1229 }
1230 }()
1231
1232 var authVariant string // Only known strings, used in metrics.
1233 authResult := "error"
1234 defer func() {
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)
1240 }
1241 }()
1242
1243 // ../rfc/4954:699
1244 p.xspace()
1245 mech := p.xsaslMech()
1246
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 {
1250 var auth string
1251 if p.empty() {
1252 c.writelinef("%d %s", smtp.C334ContinueAuth, encChal) // ../rfc/4954:205
1253 // todo future: handle max length of 12288 octets and return proper responde codes otherwise ../rfc/4954:253
1254 auth = c.readline()
1255 if auth == "*" {
1256 // ../rfc/4954:193
1257 authResult = "aborted"
1258 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5Other0, "authentication aborted")
1259 }
1260 } else {
1261 p.xspace()
1262 if !mox.Pedantic {
1263 // Windows Mail 16005.14326.21606.0 sends two spaces between "AUTH PLAIN" and the
1264 // base64 data.
1265 for p.space() {
1266 }
1267 }
1268 auth = p.remainder()
1269 if auth == "" {
1270 // ../rfc/4954:235
1271 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5Syntax2, "missing initial auth base64 parameter after space")
1272 } else if auth == "=" {
1273 // ../rfc/4954:214
1274 auth = "" // Base64 decode below will result in empty buffer.
1275 }
1276 }
1277 buf, err := base64.StdEncoding.DecodeString(auth)
1278 if err != nil {
1279 // ../rfc/4954:235
1280 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5Syntax2, "invalid base64: %s", err)
1281 }
1282 return buf
1283 }
1284
1285 xreadContinuation := func() []byte {
1286 line := c.readline()
1287 if line == "*" {
1288 authResult = "aborted"
1289 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5Other0, "authentication aborted")
1290 }
1291 buf, err := base64.StdEncoding.DecodeString(line)
1292 if err != nil {
1293 // ../rfc/4954:235
1294 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5Syntax2, "invalid base64: %s", err)
1295 }
1296 return buf
1297 }
1298
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
1303 var username string
1304 defer func() {
1305 if account != nil {
1306 err := account.Close()
1307 c.log.Check(err, "close account")
1308 }
1309 }()
1310
1311 switch mech {
1312 case "PLAIN":
1313 authVariant = "plain"
1314
1315 // ../rfc/4954:343
1316 // ../rfc/4954:326
1317 if !c.tls && c.requireTLSForAuth {
1318 xsmtpUserErrorf(smtp.C538EncReqForAuth, smtp.SePol7EncReqForAuth11, "authentication requires tls")
1319 }
1320
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))
1328 }
1329 authz := norm.NFC.String(string(plain[0]))
1330 username = norm.NFC.String(string(plain[1]))
1331 password := string(plain[2])
1332
1333 if authz != "" && authz != username {
1334 authResult = "badcreds"
1335 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "cannot assume other role")
1336 }
1337
1338 var err error
1339 account, err = store.OpenEmailAuth(c.log, username, password)
1340 if err != nil && errors.Is(err, store.ErrUnknownCredentials) {
1341 // ../rfc/4954:274
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")
1345 }
1346 xcheckf(err, "verifying credentials")
1347
1348 case "LOGIN":
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
1352
1353 authVariant = "login"
1354
1355 // ../rfc/4954:343
1356 // ../rfc/4954:326
1357 if !c.tls && c.requireTLSForAuth {
1358 xsmtpUserErrorf(smtp.C538EncReqForAuth, smtp.SePol7EncReqForAuth11, "authentication requires tls")
1359 }
1360
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
1366 // (domains).
1367 encChal := base64.StdEncoding.EncodeToString([]byte("Username:"))
1368 username = string(xreadInitial(encChal))
1369 username = norm.NFC.String(username)
1370
1371 // Again, client should ignore the challenge, we send the same as the example in
1372 // the I-D.
1373 c.writelinef("%d %s", smtp.C334ContinueAuth, base64.StdEncoding.EncodeToString([]byte("Password:")))
1374
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.
1379
1380 var err error
1381 account, err = store.OpenEmailAuth(c.log, username, password)
1382 if err != nil && errors.Is(err, store.ErrUnknownCredentials) {
1383 // ../rfc/4954:274
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")
1387 }
1388 xcheckf(err, "verifying credentials")
1389
1390 case "CRAM-MD5":
1391 authVariant = strings.ToLower(mech)
1392
1393 p.xempty()
1394
1395 // ../rfc/2195:82
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)))
1398
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")
1403 }
1404 username = norm.NFC.String(t[0])
1405 c.log.Debug("cram-md5 auth", slog.String("username", username))
1406 var err error
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")
1411 }
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")
1420 }
1421 if err != nil {
1422 return err
1423 }
1424
1425 ipadhash = password.CRAMMD5.Ipad
1426 opadhash = password.CRAMMD5.Opad
1427 return nil
1428 })
1429 xcheckf(err, "tx read")
1430 })
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")
1436 }
1437
1438 // ../rfc/2195:138 ../rfc/2104:142
1439 ipadhash.Write([]byte(chal))
1440 opadhash.Write(ipadhash.Sum(nil))
1441 digest := fmt.Sprintf("%x", opadhash.Sum(nil))
1442 if digest != t[1] {
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")
1445 }
1446
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
1450
1451 // Passwords cannot be retrieved or replayed from the trace.
1452
1453 authVariant = strings.ToLower(mech)
1454 var h func() hash.Hash
1455 switch authVariant {
1456 case "scram-sha-1", "scram-sha-1-plus":
1457 h = sha1.New
1458 case "scram-sha-256", "scram-sha-256-plus":
1459 h = sha256.New
1460 default:
1461 xsmtpServerErrorf(codes{smtp.C554TransactionFailed, smtp.SeSys3Other0}, "missing scram auth method case")
1462 }
1463
1464 var cs *tls.ConnectionState
1465 channelBindingRequired := strings.HasSuffix(authVariant, "-plus")
1466 if channelBindingRequired && !c.tls {
1467 // ../rfc/4954:630
1468 xsmtpUserErrorf(smtp.C538EncReqForAuth, smtp.SePol7EncReqForAuth11, "scram plus mechanism requires tls connection")
1469 }
1470 if c.tls {
1471 xcs := c.conn.(*tls.Conn).ConnectionState()
1472 cs = &xcs
1473 }
1474 c0 := xreadInitial("")
1475 ss, err := scram.NewServer(h, c0, cs, channelBindingRequired)
1476 if err != nil {
1477 c.log.Infox("scram protocol error", err, slog.Any("remote", c.remoteIP))
1478 xsmtpUserErrorf(smtp.C455BadParams, smtp.SePol7Other0, "scram protocol error: %s", err)
1479 }
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)
1483 if err != nil {
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")
1489 }
1490 if ss.Authorization != "" && ss.Authorization != username {
1491 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "authentication with authorization for different user not supported")
1492 }
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")
1500 }
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
1507 default:
1508 xsmtpServerErrorf(codes{smtp.C554TransactionFailed, smtp.SeSys3Other0}, "missing scram auth credentials case")
1509 }
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")
1515 }
1516 return nil
1517 })
1518 xcheckf(err, "read tx")
1519 })
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)
1525 if len(s3) > 0 {
1526 c.writelinef("%d %s", smtp.C334ContinueAuth, base64.StdEncoding.EncodeToString([]byte(s3))) // ../rfc/4954:187
1527 }
1528 if err != nil {
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")
1541 }
1542 xcheckf(err, "server final")
1543 }
1544
1545 // Client must still respond, but there is nothing to say. See ../rfc/9051:6221
1546 // The message should be empty. todo: should we require it is empty?
1547 xreadContinuation()
1548
1549 case "EXTERNAL":
1550 authVariant = strings.ToLower(mech)
1551
1552 // ../rfc/4422:1618
1553 buf := xreadInitial("")
1554 username = string(buf)
1555
1556 if !c.tls {
1557 // ../rfc/4954:630
1558 xsmtpUserErrorf(smtp.C538EncReqForAuth, smtp.SePol7EncReqForAuth11, "tls required for tls client certificate authentication")
1559 }
1560 if c.account == nil {
1561 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "missing client certificate, required for tls client certificate authentication")
1562 }
1563
1564 if username == "" {
1565 username = c.username
1566 }
1567 var err error
1568 account, _, err = store.OpenEmail(c.log, username)
1569 xcheckf(err, "looking up username from tls client authentication")
1570
1571 default:
1572 // ../rfc/4954:176
1573 xsmtpUserErrorf(smtp.C504ParamNotImpl, smtp.SeProto5BadParams4, "mechanism %s not supported", mech)
1574 }
1575
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),
1586 )
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),
1594 )
1595 }
1596 } else {
1597 c.account = account
1598 account = nil // Prevent cleanup.
1599 }
1600 c.username = username
1601
1602 authResult = "ok"
1603 c.authSASL = true
1604 c.authFailed = 0
1605 c.setSlow(false)
1606 // ../rfc/4954:276
1607 c.writecodeline(smtp.C235AuthSuccess, smtp.SePol7Other0, "nice", nil)
1608}
1609
1610// ../rfc/5321:1879 ../rfc/5321:1025
1611func (c *conn) cmdMail(p *parser) {
1612 // requirements for maximum line length:
1613 // ../rfc/5321:3500 (base max of 512 including crlf) ../rfc/4954:134 (+500) ../rfc/1870:92 (+26) ../rfc/6152:90 (none specified) ../rfc/6531:231 (+10)
1614 // todo future: enforce? doesn't really seem worth it...
1615
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.
1619 // ../rfc/5321:4349
1620 c.writecodeline(smtp.C550MailboxUnavail, smtp.SeAddr1Other0, "too many failures", nil)
1621 panic(errIO)
1622 }
1623
1624 c.xneedHello()
1625 c.xcheckAuth()
1626 if c.mailFrom != nil {
1627 // ../rfc/5321:2507, though ../rfc/5321:1029 contradicts, implying a MAIL would also reset, but ../rfc/5321:1160 decides.
1628 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "already have MAIL")
1629 }
1630 // Ensure clear transaction state on failure.
1631 defer func() {
1632 x := recover()
1633 if x != nil {
1634 // ../rfc/5321:2514
1635 c.rset()
1636 panic(x)
1637 }
1638 }()
1639 p.xtake(" FROM:")
1640 // note: no space allowed after colon. ../rfc/5321:1093
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.
1643 if !mox.Pedantic {
1644 p.space()
1645 }
1646 rawRevPath := p.xrawReversePath()
1647 paramSeen := map[string]bool{}
1648 for p.space() {
1649 // ../rfc/5321:2273
1650 key := p.xparamKeyword()
1651
1652 K := strings.ToUpper(key)
1653 if paramSeen[K] {
1654 // e.g. ../rfc/6152:128
1655 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5BadParams4, "duplicate param %q", key)
1656 }
1657 paramSeen[K] = true
1658
1659 switch K {
1660 case "SIZE":
1661 p.xtake("=")
1662 size := p.xnumber(20, true) // ../rfc/1870:90
1663 if size > c.maxMessageSize {
1664 // ../rfc/1870:136 ../rfc/3463:382
1665 ecode := smtp.SeSys3MsgLimitExceeded4
1666 if size < config.DefaultMaxMsgSize {
1667 ecode = smtp.SeMailbox2MsgLimitExceeded3
1668 }
1669 xsmtpUserErrorf(smtp.C552MailboxFull, ecode, "message too large")
1670 }
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.
1673 case "BODY":
1674 p.xtake("=")
1675 // ../rfc/6152:90
1676 v := p.xparamValue()
1677 switch strings.ToUpper(v) {
1678 case "7BIT":
1679 c.has8bitmime = false
1680 case "8BITMIME":
1681 c.has8bitmime = true
1682 default:
1683 xsmtpUserErrorf(smtp.C555UnrecognizedAddrParams, smtp.SeProto5BadParams4, "unrecognized parameter %q", key)
1684 }
1685 case "AUTH":
1686 // ../rfc/4954:455
1687
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.
1690 // ../rfc/4954:538
1691
1692 // ../rfc/4954:704
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
1694 p.xtake("=")
1695 p.xtake("<")
1696 p.xtext()
1697 p.xtake(">")
1698 case "SMTPUTF8":
1699 // ../rfc/6531:213
1700 c.smtputf8 = true
1701 c.msgsmtputf8 = true
1702 case "REQUIRETLS":
1703 // ../rfc/8689:155
1704 if !c.tls {
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")
1708 }
1709 v := true
1710 c.requireTLS = &v
1711 case "HOLDFOR", "HOLDUNTIL":
1712 // Only for submission ../rfc/4865:163
1713 if !c.submission {
1714 xsmtpUserErrorf(smtp.C555UnrecognizedAddrParams, smtp.SeSys3NotSupported3, "unrecognized parameter %q", key)
1715 }
1716 if K == "HOLDFOR" && paramSeen["HOLDUNTIL"] || K == "HOLDUNTIL" && paramSeen["HOLDFOR"] {
1717 // ../rfc/4865:260
1718 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5BadParams4, "cannot use both HOLDUNTIL and HOLFOR")
1719 }
1720 p.xtake("=")
1721 // ../rfc/4865:263 ../rfc/4865:267 We are not following the advice of treating
1722 // semantic errors as syntax errors
1723 if K == "HOLDFOR" {
1724 n := p.xnumber(9, false) // ../rfc/4865:92
1725 if n > int64(queue.FutureReleaseIntervalMax/time.Second) {
1726 // ../rfc/4865:250
1727 xsmtpUserErrorf(smtp.C554TransactionFailed, smtp.SeProto5BadParams4, "future release interval too far in the future")
1728 }
1729 c.futureRelease = time.Now().Add(time.Duration(n) * time.Second)
1730 c.futureReleaseRequest = fmt.Sprintf("for;%d", n)
1731 } else {
1732 t, s := p.xdatetimeutc()
1733 ival := time.Until(t)
1734 if ival <= 0 {
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 {
1738 // ../rfc/4865:255
1739 xsmtpUserErrorf(smtp.C554TransactionFailed, smtp.SeProto5BadParams4, "requested future release time is too far in the future")
1740 }
1741 c.futureRelease = t
1742 c.futureReleaseRequest = "until;" + s
1743 }
1744 default:
1745 // ../rfc/5321:2230
1746 xsmtpUserErrorf(smtp.C555UnrecognizedAddrParams, smtp.SeSys3NotSupported3, "unrecognized parameter %q", key)
1747 }
1748 }
1749
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()
1753 pp.xempty()
1754 pp = nil
1755 p.xend()
1756
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 {
1762 // ../rfc/6409:349
1763 if rpath.IsZero() {
1764 return true
1765 }
1766
1767 from := smtp.NewAddress(rpath.Localpart, rpath.IPDomain.Domain)
1768 return mox.AllowMsgFrom(c.account.Name, from)
1769 }
1770
1771 if !c.submission && !rpath.IPDomain.Domain.IsZero() {
1772 // If rpath domain has null MX record or is otherwise not accepting email, reject.
1773 // ../rfc/7505:181
1774 // ../rfc/5321:4045
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)
1778 cancel()
1779 if err != nil {
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")
1782 } else if !valid {
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")
1785 }
1786 }
1787
1788 if c.submission && (len(rpath.IPDomain.IP) > 0 || !rpathAllowed()) {
1789 // ../rfc/6409:522
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")
1796 }
1797
1798 if Localserve && strings.HasPrefix(string(rpath.Localpart), "mailfrom") {
1799 c.xlocalserveError(rpath.Localpart)
1800 }
1801
1802 c.mailFrom = &rpath
1803
1804 c.bwritecodeline(smtp.C250Completed, smtp.SeAddr1Other0, "looking good", nil)
1805}
1806
1807// ../rfc/5321:1916 ../rfc/5321:1054
1808func (c *conn) cmdRcpt(p *parser) {
1809 c.xneedHello()
1810 c.xcheckAuth()
1811 if c.mailFrom == nil {
1812 // ../rfc/5321:1088
1813 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "missing MAIL FROM")
1814 }
1815
1816 // ../rfc/5321:1985
1817 p.xtake(" TO:")
1818 // note: no space allowed after colon. ../rfc/5321:1093
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.
1821 if !mox.Pedantic {
1822 p.space()
1823 }
1824 var fpath smtp.Path
1825 if p.take("<POSTMASTER>") {
1826 fpath = smtp.Path{Localpart: "postmaster"}
1827 } else {
1828 fpath = p.xforwardPath()
1829 }
1830 for p.space() {
1831 // ../rfc/5321:2275
1832 key := p.xparamKeyword()
1833 // K := strings.ToUpper(key)
1834 // todo future: DSN, ../rfc/3461, with "NOTIFY"
1835 // ../rfc/5321:2230
1836 xsmtpUserErrorf(smtp.C555UnrecognizedAddrParams, smtp.SeSys3NotSupported3, "unrecognized parameter %q", key)
1837 }
1838 p.xend()
1839
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)
1845
1846 // todo future: for submission, should we do explicit verification that domains are fully qualified? also for mail from. ../rfc/6409:420
1847
1848 if len(c.recipients) >= rcptToLimit {
1849 // ../rfc/5321:3535 ../rfc/5321:3571
1850 xsmtpUserErrorf(smtp.C452StorageFull, smtp.SeProto5TooManyRcpts3, "max of %d recipients reached", rcptToLimit)
1851 }
1852
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")
1858 }
1859
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.
1866 // ../rfc/5321:3598
1867 // ../rfc/5321:4045
1868 // Also see ../rfc/7489:2214
1869 if !c.submission && len(c.recipients) == 1 && !Localserve {
1870 // note: because of check above, mailFrom cannot be the null address.
1871 var pass bool
1872 d := c.mailFrom.IPDomain.Domain
1873 if !d.IsZero() {
1874 // todo: use this spf result for DATA.
1875 spfArgs := spf.Args{
1876 RemoteIP: c.remoteIP,
1877 MailFromLocalpart: c.mailFrom.Localpart,
1878 MailFromDomain: d,
1879 HelloDomain: c.hello,
1880 LocalIP: c.localIP,
1881 LocalHostname: c.hostname,
1882 }
1883 cidctx := context.WithValue(mox.Context, mlog.CidKey, c.cid)
1884 spfctx, spfcancel := context.WithTimeout(cidctx, time.Minute)
1885 defer spfcancel()
1886 receivedSPF, _, _, _, err := spf.Verify(spfctx, c.log.Logger, c.resolver, spfArgs)
1887 spfcancel()
1888 if err != nil {
1889 c.log.Errorx("spf verify for multiple recipients", err)
1890 }
1891 pass = receivedSPF.Identity == spf.ReceivedMailFrom && receivedSPF.Result == spf.StatusPass
1892 }
1893 if !pass {
1894 xsmtpUserErrorf(smtp.C452StorageFull, smtp.SeProto5TooManyRcpts3, "only one recipient allowed without spf pass")
1895 }
1896 }
1897
1898 if Localserve && strings.HasPrefix(string(fpath.Localpart), "rcptto") {
1899 c.xlocalserveError(fpath.Localpart)
1900 }
1901
1902 if len(fpath.IPDomain.IP) > 0 {
1903 if !c.submission {
1904 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeAddr1UnknownDestMailbox1, "not accepting email for ip")
1905 }
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 {
1908 // note: a bare postmaster, without domain, is handled by LookupAddress. ../rfc/5321:735
1909 if alias != nil {
1910 c.recipients = append(c.recipients, recipient{fpath, nil, &rcptAlias{*alias, canonical}})
1911 } else {
1912 c.recipients = append(c.recipients, recipient{fpath, &rcptAccount{accountName, addr, canonical}, nil})
1913 }
1914
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) {
1924 if !c.submission {
1925 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeAddr1UnknownDestMailbox1, "not accepting email for domain")
1926 }
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) {
1930 if c.submission {
1931 // For submission, we're transparent about which user exists. Should be fine for the typical small-scale deploy.
1932 // ../rfc/5321:1071
1933 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeAddr1UnknownDestMailbox1, "no such user")
1934 }
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})
1939 } else {
1940 c.log.Errorx("looking up account for delivery", err, slog.Any("rcptto", fpath))
1941 xsmtpServerErrorf(codes{smtp.C451LocalErr, smtp.SeSys3Other0}, "error processing")
1942 }
1943 c.bwritecodeline(smtp.C250Completed, smtp.SeAddr1Other0, "now on the list", nil)
1944}
1945
1946func hasNonASCII(s string) bool {
1947 for _, c := range []byte(s) {
1948 if c > unicode.MaxASCII {
1949 return true
1950 }
1951 }
1952 return false
1953}
1954
1955// ../rfc/6531:497
1956func (c *conn) isSMTPUTF8Required(part *message.Part) bool {
1957 // Check "MAIL FROM".
1958 if hasNonASCII(string(c.mailFrom.Localpart)) {
1959 return true
1960 }
1961 // Check all "RCPT TO".
1962 for _, rcpt := range c.recipients {
1963 if hasNonASCII(string(rcpt.Addr.Localpart)) {
1964 return true
1965 }
1966 }
1967
1968 // Check header in all message parts.
1969 smtputf8, err := part.NeedsSMTPUTF8()
1970 xcheckf(err, "checking if smtputf8 is required")
1971 return smtputf8
1972}
1973
1974// ../rfc/5321:1992 ../rfc/5321:1098
1975func (c *conn) cmdData(p *parser) {
1976 c.xneedHello()
1977 c.xcheckAuth()
1978 if c.mailFrom == nil {
1979 // ../rfc/5321:1130
1980 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "missing MAIL FROM")
1981 }
1982 if len(c.recipients) == 0 {
1983 // ../rfc/5321:1130
1984 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "missing RCPT TO")
1985 }
1986
1987 // ../rfc/5321:2066
1988 p.xend()
1989
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.
1991
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)
1995 defer cmdcancel()
1996 // Deadline is taken into account by Read and Write.
1997 c.deadline, _ = cmdctx.Deadline()
1998 defer func() {
1999 c.deadline = time.Time{}
2000 }()
2001
2002 // ../rfc/5321:1994
2003 c.writelinef("354 see you at the bare dot")
2004
2005 // Mark as tracedata.
2006 defer c.xtrace(mlog.LevelTracedata)()
2007
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")
2010 if err != nil {
2011 xsmtpServerErrorf(errCodes(smtp.C451LocalErr, smtp.SeSys3Other0, err), "creating temporary file for message: %s", err)
2012 }
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.
2018 if err != nil {
2019 if errors.Is(err, errMessageTooLarge) {
2020 // ../rfc/1870:136 and ../rfc/3463:382
2021 ecode := smtp.SeSys3MsgLimitExceeded4
2022 if n < config.DefaultMaxMsgSize {
2023 ecode = smtp.SeMailbox2MsgLimitExceeded3
2024 }
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))
2027 }
2028
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)
2031 return
2032 }
2033
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)
2042 return
2043 }
2044
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.
2047 if c.submission {
2048 if !msgWriter.HaveBody {
2049 // ../rfc/6409:541
2050 xsmtpUserErrorf(smtp.C554TransactionFailed, smtp.SeMsg6Other0, "message requires both header and body section")
2051 }
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 {
2055 // ../rfc/5321:906
2056 xsmtpUserErrorf(smtp.C500BadSyntax, smtp.SeMsg6Other0, "message with non-us-ascii requires 8bitmime extension")
2057 }
2058 }
2059
2060 if Localserve && mox.Pedantic {
2061 // Require that message can be parsed fully.
2062 p, err := message.Parse(c.log.Logger, false, dataFile)
2063 if err == nil {
2064 err = p.Walk(c.log.Logger, nil)
2065 }
2066 if err != nil {
2067 // ../rfc/6409:541
2068 xsmtpUserErrorf(smtp.C554TransactionFailed, smtp.SeMsg6Other0, "malformed message: %v", err)
2069 }
2070 }
2071
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)
2078 if err == nil {
2079 // Message parsed without error. Keep the result to avoid parsing the message again.
2080 part = &p
2081 err = part.Walk(c.log.Logger, nil)
2082 if err == nil {
2083 c.msgsmtputf8 = c.isSMTPUTF8Required(part)
2084 }
2085 }
2086 if err != nil {
2087 c.log.Debugx("parsing message for smtputf8 check", err)
2088 }
2089 if c.smtputf8 != c.msgsmtputf8 {
2090 c.log.Debug("smtputf8 flag changed", slog.Bool("smtputf8", c.smtputf8), slog.Bool("msgsmtputf8", c.msgsmtputf8))
2091 }
2092 }
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")
2096 }
2097
2098 // Prepare "Received" header.
2099 // ../rfc/5321:2051 ../rfc/5321:3302
2100 // ../rfc/5321:3311 ../rfc/6531:578
2101 var recvFrom string
2102 var iprevStatus iprev.Status // Only for delivery, not submission.
2103 var iprevAuthentic bool
2104 if c.submission {
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)
2108 } else {
2109 if len(c.hello.IP) > 0 {
2110 recvFrom = smtp.AddressLiteral(c.hello.IP)
2111 } else {
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)
2115 }
2116 iprevctx, iprevcancel := context.WithTimeout(cmdctx, time.Minute)
2117 var revName string
2118 var revNames []string
2119 iprevStatus, revName, revNames, iprevAuthentic, err = iprev.Lookup(iprevctx, c.resolver, c.remoteIP)
2120 iprevcancel()
2121 if err != nil {
2122 c.log.Infox("reverse-forward lookup", err, slog.Any("remoteip", c.remoteIP))
2123 }
2124 c.log.Debug("dns iprev check", slog.Any("addr", c.remoteIP), slog.Any("status", iprevStatus))
2125 var name string
2126 if revName != "" {
2127 name = revName
2128 } else if len(revNames) > 0 {
2129 name = revNames[0]
2130 }
2131 name = strings.TrimSuffix(name, ".")
2132 recvFrom += " ("
2133 if name != "" && name != c.hello.Domain.XName(c.msgsmtputf8) {
2134 recvFrom += name + " "
2135 }
2136 recvFrom += smtp.AddressLiteral(c.remoteIP) + ")"
2137 if c.msgsmtputf8 && c.hello.Domain.Unicode != "" {
2138 recvFrom += " (" + c.hello.Domain.ASCII + ")"
2139 }
2140 }
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 + ")"
2146 }
2147
2148 // ../rfc/3848:34 ../rfc/6531:791
2149 with := "SMTP"
2150 if c.msgsmtputf8 {
2151 with = "UTF8SMTP"
2152 } else if c.ehlo {
2153 with = "ESMTP"
2154 }
2155 if c.tls {
2156 with += "S"
2157 }
2158 if c.account != nil {
2159 // ../rfc/4954:660
2160 with += "A"
2161 }
2162
2163 // Assume transaction does not succeed. If it does, we'll compensate.
2164 c.transactionBad++
2165
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
2170 withComment := ""
2171 if c.requireTLS != nil && *c.requireTLS {
2172 // Comment is actually part of ID ABNF rule. ../rfc/5321:3336
2173 withComment = " (requiretls)"
2174 }
2175 recvHdr.Add(" ", "Received:", "from", recvFrom, "by", recvBy, "via", "tcp", "with", with+withComment, "id", mox.ReceivedID(c.cid)) // ../rfc/5321:3158
2176 if c.tls {
2177 tlsConn := c.conn.(*tls.Conn)
2178 tlsComment := mox.TLSReceivedComment(c.log, tlsConn.ConnectionState())
2179 recvHdr.Add(" ", tlsComment...)
2180 }
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.
2184 if rcptTo != "" {
2185 recvHdr.Add(" ", "for", "<"+rcptTo+">;")
2186 }
2187 recvHdr.Add(" ", time.Now().Format(message.RFC5322Z))
2188 return recvHdr.String()
2189 }
2190
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.
2194 if c.submission {
2195 c.submit(cmdctx, recvHdrFor, msgWriter, dataFile, part)
2196 } else {
2197 c.deliver(cmdctx, recvHdrFor, msgWriter, iprevStatus, iprevAuthentic, dataFile)
2198 }
2199}
2200
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".
2204// ../rfc/8689:223
2205func hasTLSRequiredNo(h textproto.MIMEHeader) bool {
2206 l := h.Values("Tls-Required")
2207 if len(l) == 0 {
2208 return false
2209 }
2210 for _, v := range l {
2211 if !strings.EqualFold(v, "no") {
2212 return false
2213 }
2214 }
2215 return true
2216}
2217
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\(
2221
2222 var msgPrefix []byte
2223
2224 // Check that user is only sending email as one of its configured identities. Not
2225 // for other users.
2226 // We don't check the Sender field, there is no expectation of verification, ../rfc/7489:2948
2227 // and with Resent headers it seems valid to have someone else as Sender. ../rfc/5322:1578
2228 msgFrom, _, header, err := message.From(c.log.Logger, true, dataFile, part)
2229 if err != nil {
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)
2233 }
2234 if !mox.AllowMsgFrom(c.account.Name, msgFrom) {
2235 // ../rfc/6409:522
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")
2239 }
2240
2241 // TLS-Required: No header makes us not enforce recipient domain's TLS policy.
2242 // ../rfc/8689:206
2243 // Only when requiretls smtp extension wasn't used. ../rfc/8689:246
2244 if c.requireTLS == nil && hasTLSRequiredNo(header) {
2245 v := false
2246 c.requireTLS = &v
2247 }
2248
2249 // Outgoing messages should not have a Return-Path header. The final receiving mail
2250 // server will add it.
2251 // ../rfc/5321:3233
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")
2255 }
2256
2257 // Add Message-Id header if missing.
2258 // ../rfc/5321:4131 ../rfc/6409:751
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)...)
2263 }
2264
2265 // ../rfc/6409:745
2266 if header.Get("Date") == "" {
2267 msgPrefix = append(msgPrefix, "Date: "+time.Now().Format(message.RFC5322Z)+"\r\n"...)
2268 }
2269
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 {
2274 rcpts[i] = r.Addr
2275 }
2276 msglimit, rcptlimit, err := c.account.SendLimitReached(tx, rcpts)
2277 xcheckf(err, "checking sender limit")
2278 if msglimit >= 0 {
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)
2284 }
2285 return nil
2286 })
2287 xcheckf(err, "read-only transaction")
2288
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-") {
2296 continue
2297 }
2298 if extra == nil {
2299 extra = map[string]string{}
2300 }
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)
2305 }
2306 extra[xk] = vl[len(vl)-1]
2307 }
2308
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.
2310
2311 // Add DKIM signatures.
2312 confDom, ok := mox.Conf.Domain(msgFrom.Domain)
2313 if !ok {
2314 c.log.Error("domain disappeared", slog.Any("domain", msgFrom.Domain))
2315 xsmtpServerErrorf(codes{smtp.C451LocalErr, smtp.SeSys3Other0}, "internal error")
2316 }
2317
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()
2324 } else {
2325 msgPrefix = append(msgPrefix, []byte(dkimHeaders)...)
2326 }
2327 }
2328
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{
2333 {
2334 Method: "auth",
2335 Result: "pass",
2336 Props: []message.AuthProp{
2337 message.MakeAuthProp("smtp", "mailfrom", c.mailFrom.XString(c.msgsmtputf8), true, c.mailFrom.ASCIIExtra(c.msgsmtputf8)),
2338 },
2339 },
2340 },
2341 }
2342 msgPrefix = append(msgPrefix, []byte(authResults.Header())...)
2343
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
2347 // other.
2348
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
2354 var fromID string
2355 var genFromID bool
2356 if useFromID {
2357 // With submission, user can bring their own fromid.
2358 t := strings.SplitN(string(c.mailFrom.Localpart), confDom.LocalpartCatchallSeparator, 2)
2359 localpartBase = t[0]
2360 if len(t) == 2 {
2361 fromID = t[1]
2362 if fromID != "" && len(c.recipients) > 1 {
2363 xsmtpServerErrorf(codes{smtp.C554TransactionFailed, smtp.SeProto5TooManyRcpts3}, "cannot send to multiple recipients with chosen fromid")
2364 }
2365 } else {
2366 genFromID = true
2367 }
2368 }
2369 now := time.Now()
2370 qml := make([]queue.Msg, len(c.recipients))
2371 for i, rcpt := range c.recipients {
2372 if Localserve {
2373 code, timeout := mox.LocalserveNeedsError(rcpt.Addr.Localpart)
2374 if timeout {
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)
2381 }
2382 }
2383
2384 fp := *c.mailFrom
2385 if useFromID {
2386 if genFromID {
2387 fromID = xrandomID(16)
2388 }
2389 fp.Localpart = smtp.Localpart(localpartBase + confDom.LocalpartCatchallSeparator + fromID)
2390 }
2391
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.
2395 var rcptTo string
2396 if len(c.recipients) == 1 {
2397 rcptTo = rcpt.Addr.String()
2398 }
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
2405 }
2406 qm.FromID = fromID
2407 qm.Extra = extra
2408 qml[i] = qm
2409 }
2410
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)
2422 }
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))
2431 }
2432
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)
2438 }
2439 }
2440 return nil
2441 })
2442 xcheckf(err, "adding outgoing messages")
2443
2444 c.transactionGood++
2445 c.transactionBad-- // Compensate for early earlier pessimistic increase.
2446
2447 c.rset()
2448 c.writecodeline(smtp.C250Completed, smtp.SeMailbox2Other0, "it is done", nil)
2449}
2450
2451func xrandomID(n int) string {
2452 return base64.RawURLEncoding.EncodeToString(xrandom(n))
2453}
2454
2455func xrandom(n int) []byte {
2456 buf := make([]byte, n)
2457 x, err := cryptorand.Read(buf)
2458 xcheckf(err, "read random")
2459 if x != n {
2460 xcheckf(errors.New("short random read"), "read random")
2461 }
2462 return buf
2463}
2464
2465func ipmasked(ip net.IP) (string, string, string) {
2466 if ip.To4() != nil {
2467 m1 := ip.String()
2468 m2 := ip.Mask(net.CIDRMask(26, 32)).String()
2469 m3 := ip.Mask(net.CIDRMask(21, 32)).String()
2470 return m1, m2, m3
2471 }
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()
2475 return m1, m2, m3
2476}
2477
2478func (c *conn) xlocalserveError(lp smtp.Localpart) {
2479 code, timeout := mox.LocalserveNeedsError(lp)
2480 if timeout {
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)
2488 }
2489}
2490
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.
2495
2496 var msgFrom smtp.Address
2497 var envelope *message.Envelope
2498 var headers textproto.MIMEHeader
2499 var isDSN bool
2500 part, err := message.Parse(c.log.Logger, false, dataFile)
2501 if err == nil {
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)
2505 }
2506 if err != nil {
2507 c.log.Infox("parsing message for From address", err)
2508 }
2509
2510 // Basic loop detection. ../rfc/5321:4065 ../rfc/5321:1526
2511 if len(headers.Values("Received")) > 100 {
2512 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeNet4Loop6, "loop detected, more than 100 Received headers")
2513 }
2514
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.
2518 // ../rfc/8689:206
2519 // Only when requiretls smtp extension wasn't used. ../rfc/8689:246
2520 if c.requireTLS == nil && hasTLSRequiredNo(headers) {
2521 v := false
2522 c.requireTLS = &v
2523 }
2524
2525 // We'll be building up an Authentication-Results header.
2526 authResults := message.AuthResults{
2527 Hostname: mox.Conf.Static.HostnameDomain.XName(c.msgsmtputf8),
2528 }
2529
2530 commentAuthentic := func(v bool) string {
2531 if v {
2532 return "with dnssec"
2533 }
2534 return "without dnssec"
2535 }
2536
2537 // Reverse IP lookup results.
2538 // todo future: how useful is this?
2539 // ../rfc/5321:2481
2540 authResults.Methods = append(authResults.Methods, message.AuthMethod{
2541 Method: "iprev",
2542 Result: string(iprevStatus),
2543 Comment: commentAuthentic(iprevAuthentic),
2544 Props: []message.AuthProp{
2545 message.MakeAuthProp("policy", "iprev", c.remoteIP.String(), false, ""),
2546 },
2547 })
2548
2549 // SPF and DKIM verification in parallel.
2550 var wg sync.WaitGroup
2551
2552 // DKIM
2553 wg.Add(1)
2554 var dkimResults []dkim.Result
2555 var dkimErr error
2556 go func() {
2557 defer func() {
2558 x := recover() // Should not happen, but don't take program down if it does.
2559 if x != nil {
2560 c.log.Error("dkim verify panic", slog.Any("err", x))
2561 debug.PrintStack()
2562 metrics.PanicInc(metrics.Dkimverify)
2563 }
2564 }()
2565 defer wg.Done()
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)
2571 defer dkimcancel()
2572 // todo future: we could let user configure which dkim headers they require
2573
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
2577 if Localserve {
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{
2583 Version: "DKIM1",
2584 Hashes: []string{sel.HashEffective},
2585 PublicKey: sel.Key.Public(),
2586 }
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")
2592 }
2593 txt, err := dkimr.Record()
2594 xcheckf(err, "making DKIM DNS TXT record")
2595 txts[name+"._domainkey."+msgFrom.Domain.ASCII+"."] = []string{txt}
2596 }
2597 resolver = dns.MockResolver{TXT: txts}
2598 }
2599 }
2600 dkimResults, dkimErr = dkim.Verify(dkimctx, c.log.Logger, resolver, c.msgsmtputf8, dkim.DefaultPolicy, dataFile, ignoreTestMode)
2601 dkimcancel()
2602 }()
2603
2604 // SPF.
2605 // ../rfc/7208:472
2606 var receivedSPF spf.Received
2607 var spfDomain dns.Domain
2608 var spfExpl string
2609 var spfAuthentic bool
2610 var spfErr error
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,
2616 LocalIP: c.localIP,
2617 LocalHostname: c.hostname,
2618 }
2619 wg.Add(1)
2620 go func() {
2621 defer func() {
2622 x := recover() // Should not happen, but don't take program down if it does.
2623 if x != nil {
2624 c.log.Error("spf verify panic", slog.Any("err", x))
2625 debug.PrintStack()
2626 metrics.PanicInc(metrics.Spfverify)
2627 }
2628 }()
2629 defer wg.Done()
2630 spfctx, spfcancel := context.WithTimeout(ctx, time.Minute)
2631 defer spfcancel()
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"}},
2639 }
2640 }
2641 }
2642 receivedSPF, spfDomain, spfExpl, spfAuthentic, spfErr = spf.Verify(spfctx, c.log.Logger, resolver, spfArgs)
2643 spfcancel()
2644 if spfErr != nil {
2645 c.log.Infox("spf verify", spfErr)
2646 }
2647 }()
2648
2649 // Wait for DKIM and SPF validation to finish.
2650 wg.Wait()
2651
2652 // Give immediate response if all recipients are unknown.
2653 nunknown := 0
2654 for _, r := range c.recipients {
2655 if r.Account == nil && r.Alias == nil {
2656 nunknown++
2657 }
2658 }
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))
2662
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)
2667 }
2668
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)")
2671 }
2672
2673 // Add DKIM results to Authentication-Results header.
2674 authResAddDKIM := func(result, comment, reason string, props []message.AuthProp) {
2675 dm := message.AuthMethod{
2676 Method: "dkim",
2677 Result: result,
2678 Comment: comment,
2679 Reason: reason,
2680 Props: props,
2681 }
2682 authResults.Methods = append(authResults.Methods, dm)
2683 }
2684 if dkimErr != nil {
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)
2690 }
2691 for i, r := range dkimResults {
2692 var domain, selector dns.Domain
2693 var identity *dkim.Identity
2694 var comment string
2695 var props []message.AuthProp
2696 if r.Sig != nil {
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())
2700 }
2701 }
2702
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, ""),
2709 message.MakeAuthProp("header", "b", sig, false, ""), // ../rfc/6008:147
2710 }
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
2716 }
2717 if r.RecordAuthentic {
2718 comment += "with dnssec"
2719 } else {
2720 comment += "without dnssec"
2721 }
2722 }
2723 var errmsg string
2724 if r.Err != nil {
2725 errmsg = r.Err.Error()
2726 }
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))
2735 }
2736
2737 // Add SPF results to Authentication-Results header. ../rfc/7208:2141
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
2745 }
2746 ehloValidation = store.SPFValidation(receivedSPF.Result)
2747 case spf.ReceivedMailFrom:
2748 spfIdentity = &spfArgs.MailFromDomain
2749 mailFromValidation = store.SPFValidation(receivedSPF.Result)
2750 }
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))}
2754 }
2755 var spfComment string
2756 if spfAuthentic {
2757 spfComment = "with dnssec"
2758 } else {
2759 spfComment = "without dnssec"
2760 }
2761 authResults.Methods = append(authResults.Methods, message.AuthMethod{
2762 Method: "spf",
2763 Result: string(receivedSPF.Result),
2764 Comment: spfComment,
2765 Props: props,
2766 })
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:
2771 if spfExpl != "" {
2772 // Filter out potentially hostile text. ../rfc/7208:2529
2773 for _, b := range []byte(spfExpl) {
2774 if b < ' ' || b >= 0x7f {
2775 spfExpl = ""
2776 break
2777 }
2778 }
2779 if spfExpl != "" {
2780 if len(spfExpl) > 800 {
2781 spfExpl = spfExpl[:797] + "..."
2782 }
2783 spfExpl = "remote claims: " + spfExpl
2784 }
2785 }
2786 if spfExpl == "" {
2787 spfExpl = fmt.Sprintf("your ip %s is not on the SPF allowlist for domain %s", spfArgs.RemoteIP, spfDomain.ASCII)
2788 }
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:
2795 default:
2796 c.log.Error("unknown spf status, treating as None/Neutral", slog.Any("status", receivedSPF.Result))
2797 receivedSPF.Result = spf.StatusNone
2798 }
2799
2800 // DMARC
2801 var dmarcUse bool
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{
2811 Method: "dmarc",
2812 Result: string(dmarcResult.Status),
2813 }
2814 } else {
2815 msgFromValidation = alignment(ctx, c.log, msgFrom.Domain, dkimResults, receivedSPF.Result, spfIdentity)
2816
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
2823 // evaluation.
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.
2825
2826 dmarcctx, dmarccancel := context.WithTimeout(ctx, time.Minute)
2827 defer dmarccancel()
2828 dmarcUse, dmarcResult = dmarc.Verify(dmarcctx, c.log.Logger, c.resolver, msgFrom.Domain, dkimResults, receivedSPF.Result, spfIdentity, applyRandomPercentage)
2829 dmarccancel()
2830 var comment string
2831 if dmarcResult.RecordAuthentic {
2832 comment = "with dnssec"
2833 } else {
2834 comment = "without dnssec"
2835 }
2836 dmarcMethod = message.AuthMethod{
2837 Method: "dmarc",
2838 Result: string(dmarcResult.Status),
2839 Comment: comment,
2840 Props: []message.AuthProp{
2841 // ../rfc/7489:1489
2842 message.MakeAuthProp("header", "from", msgFrom.Domain.ASCII, true, msgFrom.Domain.ASCIIExtra(c.msgsmtputf8)),
2843 },
2844 }
2845
2846 if dmarcResult.Status == dmarc.StatusPass && msgFromValidation == store.ValidationRelaxed {
2847 msgFromValidation = store.ValidationDMARC
2848 }
2849
2850 // todo future: consider enforcing an spf (soft)fail if there is no dmarc policy or the dmarc policy is none. ../rfc/7489:1507
2851 }
2852 c.log.Debug("dmarc verification", slog.Any("result", dmarcResult.Status), slog.Any("domain", msgFrom.Domain))
2853
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 {
2862 continue
2863 }
2864 d := r.Sig.Domain.Name()
2865 if !dkimSeen[d] {
2866 dkimSeen[d] = true
2867 verifiedDKIMDomains = append(verifiedDKIMDomains, d)
2868 }
2869 }
2870
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
2875
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).
2880 // ../rfc/3464:436
2881 type deliverError struct {
2882 rcptTo smtp.Path
2883 code int
2884 secode string
2885 userError bool
2886 errmsg string
2887 }
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)
2898 }
2899
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 {
2904 return 0
2905 } else if r.Alias != nil {
2906 return 1
2907 }
2908 return 2
2909 }
2910 sort.SliceStable(c.recipients, func(i, j int) bool {
2911 return rcptScore(c.recipients[i]) < rcptScore(c.recipients[j])
2912 })
2913
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 {
2920 break
2921 } else if rcpt.Addr.Equal(addr) {
2922 return true
2923 }
2924 }
2925 return false
2926 }
2927
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)
2934 if err != nil {
2935 log.Errorx("open account", err, slog.Any("account", accountName))
2936 metricDelivery.WithLabelValues("accounterror", "").Inc()
2937 return nil, err
2938 }
2939 defer func() {
2940 if a == nil {
2941 err := acc.Close()
2942 log.Check(err, "closing account during analysis")
2943 }
2944 }()
2945
2946 m := store.Message{
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,
2968 DSN: isDSN,
2969 Size: msgWriter.Size,
2970 }
2971 if c.tls {
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
2977 }
2978 } else {
2979 m.ReceivedTLSVersion = 1 // Signals plain text delivery.
2980 }
2981
2982 var msgTo, msgCc []message.Address
2983 if envelope != nil {
2984 msgTo = envelope.To
2985 msgCc = envelope.CC
2986 }
2987 d := delivery{c.tls, &m, dataFile, smtpRcptTo, deliverTo, destination, canonicalAddr, acc, msgTo, msgCc, msgFrom, c.dnsBLs, dmarcUse, dmarcResult, dkimResults, iprevStatus, c.smtputf8}
2988
2989 r := analyze(ctx, log, c.resolver, d)
2990 return &r, nil
2991 }
2992
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))
2999
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...
3007 // We'll continue delivering to other recipients. ../rfc/5321:3275
3008 if rcpt.Account == nil && rcpt.Alias == nil {
3009 metricDelivery.WithLabelValues("unknownuser", "").Inc()
3010 addError(rcpt, smtp.C550MailboxUnavail, smtp.SeAddr1UnknownDestMailbox1, true, "no such user")
3011 return
3012 }
3013
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.
3016 var la []analysis
3017 defer func() {
3018 for _, a := range la {
3019 err := a.d.acc.Close()
3020 log.Check(err, "close account")
3021 }
3022 }()
3023
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")
3034 return
3035 }
3036
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)
3040 if err != nil {
3041 addError(rcpt, smtp.C451LocalErr, smtp.SeSys3Other0, false, "error processing")
3042 return
3043 }
3044 la = append(la, *a)
3045 if a.accept && a0 == nil {
3046 // Address that caused us to accept.
3047 a0 = &la[len(la)-1]
3048 }
3049 }
3050 if a0 == nil {
3051 // First address, for rejecting.
3052 a0 = &la[0]
3053 }
3054 } else {
3055 a, err := messageAnalyze(log, rcpt.Addr, rcpt.Addr, rcpt.Account.AccountName, rcpt.Account.Destination, rcpt.Account.CanonicalAddress)
3056 if err != nil {
3057 addError(rcpt, smtp.C451LocalErr, smtp.SeSys3Other0, false, "error processing")
3058 return
3059 }
3060 la = []analysis{*a}
3061 a0 = &la[0]
3062 }
3063
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()
3067 c.setSlow(true)
3068 addError(rcpt, a0.code, a0.secode, a0.userError, a0.errmsg)
3069 return
3070 }
3071
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
3076 // p=reject.
3077 var dmarcOverrides []string
3078 if a0.dmarcOverrideReason != "" {
3079 dmarcOverrides = []string{a0.dmarcOverrideReason}
3080 }
3081 if dmarcResult.Record != nil && !dmarcUse {
3082 dmarcOverrides = append(dmarcOverrides, string(dmarcrpt.PolicyOverrideSampledOut))
3083 }
3084
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.
3087 // ../rfc/7489:1486
3088 rcptDMARCMethod := dmarcMethod
3089 if len(dmarcOverrides) > 0 {
3090 if rcptDMARCMethod.Comment != "" {
3091 rcptDMARCMethod.Comment += ", "
3092 }
3093 rcptDMARCMethod.Comment += "override " + strings.Join(dmarcOverrides, ",")
3094 }
3095 rcptAuthResults := authResults
3096 rcptAuthResults.Methods = append([]message.AuthMethod{}, authResults.Methods...)
3097 rcptAuthResults.Methods = append(rcptAuthResults.Methods, rcptDMARCMethod)
3098
3099 // Prepend reason as message header, for easy viewing in mail clients.
3100 var xmox string
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 {
3106 if i == 0 {
3107 s = "; " + s
3108 } else {
3109 hw.Newline()
3110 }
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", " ")
3114 s += ";"
3115 hw.AddWrap([]byte(s), true)
3116 }
3117 xmox = hw.String()
3118 }
3119 xmox += a0.headers
3120
3121 for i := range la {
3122 // ../rfc/5321:3204
3123 // Received-SPF header goes before Received. ../rfc/7208:2038
3124 la[i].d.m.MsgPrefix = []byte(
3125 xmox +
3126 "Delivered-To: " + la[i].d.deliverTo.XString(c.msgsmtputf8) + "\r\n" + // ../rfc/9228:274
3127 "Return-Path: <" + c.mailFrom.String() + ">\r\n" + // ../rfc/5321:3300
3128 rcptAuthResults.Header() +
3129 receivedSPF.Header() +
3130 recvHdrFor(rcpt.Addr.String()),
3131 )
3132 la[i].d.m.Size += int64(len(la[i].d.m.MsgPrefix))
3133 }
3134
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.
3141 // ../rfc/7489:1492
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.
3147 // ../rfc/7489:1691
3148 disposition := dmarcrpt.DispositionNone
3149 if !a0.accept {
3150 disposition = dmarcrpt.DispositionReject
3151 }
3152
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()
3163 if err != nil {
3164 return fmt.Errorf("querying for non-junk message from organizational domain: %v", err)
3165 }
3166 if exists {
3167 return nil
3168 }
3169
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()
3174 if err != nil {
3175 return fmt.Errorf("querying for message sent to organizational domain: %v", err)
3176 }
3177 if !exists {
3178 unknown = true
3179 }
3180 return nil
3181 })
3182 if err != nil {
3183 log.Errorx("checking if sender is unknown domain, for dmarc aggregate report evaluation", err)
3184 }
3185 return
3186 }
3187
3188 r := dmarcResult.Record
3189 addresses := make([]string, len(r.AggregateReportAddresses))
3190 for i, a := range r.AggregateReportAddresses {
3191 addresses[i] = a.String()
3192 }
3193 sp := dmarcrpt.Disposition(r.SubdomainPolicy)
3194 if r.SubdomainPolicy == dmarc.PolicyEmpty {
3195 sp = dmarcrpt.Disposition(r.Policy)
3196 }
3197 eval := dmarcdb.Evaluation{
3198 // Evaluated and IntervalHours set by AddEvaluation.
3199 PolicyDomain: dmarcResult.Domain.Name(),
3200
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(),
3207
3208 Addresses: addresses,
3209
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.
3218 },
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(),
3226 }
3227
3228 for _, s := range dmarcOverrides {
3229 reason := dmarcrpt.PolicyOverrideReason{Type: dmarcrpt.PolicyOverride(s)}
3230 eval.OverrideReasons = append(eval.OverrideReasons, reason)
3231 }
3232
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) {
3237 continue
3238 }
3239 r := dmarcrpt.DKIMAuthResult{
3240 Domain: dkimResult.Sig.Domain.Name(),
3241 Selector: dkimResult.Sig.Selector.ASCII,
3242 Result: dmarcrpt.DKIMResult(dkimResult.Status),
3243 }
3244 eval.DKIMResults = append(eval.DKIMResults, r)
3245 }
3246
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),
3253 }
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),
3260 }
3261 eval.SPFResults = []dmarcrpt.SPFAuthResult{spfAuthResult}
3262 }
3263
3264 err := dmarcdb.AddEvaluation(ctx, dmarcResult.Record.AggregateReportingInterval, &eval)
3265 log.Check(err, "adding dmarc evaluation to database for aggregate report")
3266 }
3267
3268 if !a0.accept {
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) {
3272 continue
3273 }
3274
3275 conf, _ := a.d.acc.Conf()
3276 if conf.RejectsMailbox == "" {
3277 continue
3278 }
3279 present, _, messagehash, err := rejectPresent(log, a.d.acc, conf.RejectsMailbox, a.d.m, dataFile)
3280 if err != nil {
3281 log.Errorx("checking whether reject is already present", err)
3282 continue
3283 } else if present {
3284 log.Info("reject message is already present, ignoring")
3285 continue
3286 }
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() {
3294 hasSpace := true
3295 var err error
3296 if !conf.KeepRejects {
3297 hasSpace, err = a.d.acc.TidyRejectsMailbox(c.log, conf.RejectsMailbox)
3298 }
3299 if err != nil {
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)
3304 } else {
3305 log.Info("delivered spammy mail to rejects mailbox")
3306 }
3307 } else {
3308 log.Info("not storing spammy mail to full rejects mailbox")
3309 }
3310 })
3311 }
3312
3313 log.Info("incoming message rejected", slog.String("reason", a0.reason), slog.Any("msgfrom", msgFrom))
3314 metricDelivery.WithLabelValues("reject", a0.reason).Inc()
3315 c.setSlow(true)
3316 addError(rcpt, a0.code, a0.secode, a0.userError, a0.errmsg)
3317 return
3318 }
3319
3320 delayFirstTime := true
3321 if rcpt.Account != nil && a0.dmarcReport != nil {
3322 // todo future: add rate limiting to prevent DoS attacks. ../rfc/7489:2570
3323 if err := dmarcdb.AddReport(ctx, a0.dmarcReport, msgFrom.Domain); err != nil {
3324 log.Errorx("saving dmarc aggregate report in database", err)
3325 } else {
3326 log.Info("dmarc aggregate report processed")
3327 a0.d.m.Flags.Seen = true
3328 delayFirstTime = false
3329 }
3330 }
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)
3335 } else {
3336 log.Info("tlsrpt report processed")
3337 a0.d.m.Flags.Seen = true
3338 delayFirstTime = false
3339 }
3340 }
3341
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)
3349 }
3350
3351 if Localserve {
3352 code, timeout := mox.LocalserveNeedsError(rcpt.Addr.Localpart)
3353 if timeout {
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))
3361 return
3362 }
3363 }
3364
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)
3371 } else {
3372 messageID = header.Get("Message-Id")
3373 }
3374 parsedMessageID = true
3375 }
3376
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())) {
3385 continue
3386 }
3387
3388 var delivered bool
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) {
3394 nfull++
3395 } else {
3396 addError(rcpt, smtp.C451LocalErr, smtp.SeSys3Other0, false, "error processing")
3397 nerr++
3398 }
3399 return
3400 }
3401 delivered = true
3402 ndelivered++
3403 metricDelivery.WithLabelValues("delivered", a0.reason).Inc()
3404 log.Info("incoming message delivered", slog.String("reason", a0.reason), slog.Any("msgfrom", msgFrom))
3405
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))
3410 }
3411 }
3412 })
3413
3414 // Pass delivered messages to queue for DSN processing and/or hooks.
3415 if delivered {
3416 mr := store.FileMsgReader(a.d.m.MsgPrefix, dataFile)
3417 part, err := a.d.m.LoadPart(mr)
3418 if err != nil {
3419 log.Errorx("loading parsed part for evaluating webhook", err)
3420 } else {
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")
3423 }
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.
3427 break
3428 }
3429 }
3430 if ndelivered == 0 && (nerr > 0 || nfull > 0) {
3431 if nerr == 0 {
3432 addError(rcpt, smtp.C452StorageFull, smtp.SeMailbox2Full2, true, "account storage full")
3433 } else {
3434 addError(rcpt, smtp.C451LocalErr, smtp.SeSys3Other0, false, "error processing")
3435 }
3436 }
3437 }
3438
3439 // For each recipient, do final spam analysis and delivery.
3440 for _, rcpt := range c.recipients {
3441 processRecipient(rcpt)
3442 }
3443
3444 // If all recipients failed to deliver, return an error.
3445 if len(c.recipients) == len(deliverErrors) {
3446 same := true
3447 e0 := deliverErrors[0]
3448 var serverError bool
3449 var msgs []string
3450 major := 4
3451 for _, e := range deliverErrors {
3452 serverError = serverError || !e.userError
3453 if e.code != e0.code || e.secode != e0.secode {
3454 same = false
3455 }
3456 msgs = append(msgs, e.errmsg)
3457 if e.code >= 500 {
3458 major = 5
3459 }
3460 }
3461 if same {
3462 xsmtpErrorf(e0.code, e0.secode, !serverError, "%s", strings.Join(msgs, "\n"))
3463 }
3464
3465 // Not all failures had the same error. We'll return each error on a separate line.
3466 lines := []string{}
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)
3470 }
3471 code := smtp.C451LocalErr
3472 secode := smtp.SeSys3Other0
3473 if major == 5 {
3474 code = smtp.C554TransactionFailed
3475 }
3476 lines = append(lines, "multiple errors")
3477 xsmtpErrorf(code, secode, !serverError, "%s", strings.Join(lines, "\n"))
3478 }
3479 // Generate one DSN for all failed recipients.
3480 if len(deliverErrors) > 0 {
3481 now := time.Now()
3482 dsnMsg := dsn.Message{
3483 SMTPUTF8: c.msgsmtputf8,
3484 From: smtp.Path{Localpart: "postmaster", IPDomain: deliverErrors[0].rcptTo.IPDomain},
3485 To: *c.mailFrom,
3486 Subject: "mail delivery failure",
3487 MessageID: mox.MessageIDGen(false),
3488 References: messageID,
3489
3490 // Per-message details.
3491 ReportingMTA: mox.Conf.Static.HostnameDomain.ASCII,
3492 ReceivedFromMTA: smtp.Ehlo{Name: c.hello, ConnIP: c.remoteIP},
3493 ArrivalDate: now,
3494 }
3495
3496 if len(deliverErrors) > 1 {
3497 dsnMsg.TextBody = "Multiple delivery failures occurred.\n\n"
3498 }
3499
3500 for _, e := range deliverErrors {
3501 kind := "Permanent"
3502 if e.code/100 == 4 {
3503 kind = "Transient"
3504 }
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,
3508 Action: dsn.Failed,
3509 Status: fmt.Sprintf("%d.%s", e.code/100, e.secode),
3510 LastAttemptDate: now,
3511 }
3512 dsnMsg.Recipients = append(dsnMsg.Recipients, rcpt)
3513 }
3514
3515 header, err := message.ReadHeaders(bufio.NewReader(&moxio.AtReader{R: dataFile}))
3516 if err != nil {
3517 c.log.Errorx("reading headers of incoming message for dsn, continuing dsn without headers", err)
3518 }
3519 dsnMsg.Original = header
3520
3521 if Localserve {
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)
3526 }
3527 }
3528
3529 c.transactionGood++
3530 c.transactionBad-- // Compensate for early earlier pessimistic increase.
3531 c.rset()
3532 c.writecodeline(smtp.C250Completed, smtp.SeMailbox2Other0, "it is done", nil)
3533}
3534
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 {
3539 return true
3540 }
3541 }
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
3546 }
3547 return alias.PostPublic
3548}
3549
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 {
3554 switch {
3555 case moxio.IsStorageSpace(err):
3556 switch ecode {
3557 case smtp.SeMailbox2Other0:
3558 if code == smtp.C451LocalErr {
3559 code = smtp.C452StorageFull
3560 }
3561 ecode = smtp.SeMailbox2Full2
3562 case smtp.SeSys3Other0:
3563 if code == smtp.C451LocalErr {
3564 code = smtp.C452StorageFull
3565 }
3566 ecode = smtp.SeSys3StorageFull1
3567 }
3568 }
3569 return codes{code, ecode}
3570}
3571
3572// ../rfc/5321:2079
3573func (c *conn) cmdRset(p *parser) {
3574 // ../rfc/5321:2106
3575 p.xend()
3576
3577 c.rset()
3578 c.bwritecodeline(smtp.C250Completed, smtp.SeOther00, "all clear", nil)
3579}
3580
3581// ../rfc/5321:2108 ../rfc/5321:1222
3582func (c *conn) cmdVrfy(p *parser) {
3583 // No EHLO/HELO needed.
3584 // ../rfc/5321:2448
3585
3586 // ../rfc/5321:2119 ../rfc/6531:641
3587 p.xspace()
3588 p.xstring()
3589 if p.space() {
3590 p.xtake("SMTPUTF8")
3591 }
3592 p.xend()
3593
3594 // todo future: we could support vrfy and expn for submission? though would need to see if its rfc defines it.
3595
3596 // ../rfc/5321:4239
3597 xsmtpUserErrorf(smtp.C252WithoutVrfy, smtp.SePol7Other0, "no verify but will try delivery")
3598}
3599
3600// ../rfc/5321:2135 ../rfc/5321:1272
3601func (c *conn) cmdExpn(p *parser) {
3602 // No EHLO/HELO needed.
3603 // ../rfc/5321:2448
3604
3605 // ../rfc/5321:2149 ../rfc/6531:645
3606 p.xspace()
3607 p.xstring()
3608 if p.space() {
3609 p.xtake("SMTPUTF8")
3610 }
3611 p.xend()
3612
3613 // todo: we could implement expn for local aliases for authenticated users, when members have permission to list. would anyone use it?
3614
3615 // ../rfc/5321:4239
3616 xsmtpUserErrorf(smtp.C252WithoutVrfy, smtp.SePol7Other0, "no expand but will try delivery")
3617}
3618
3619// ../rfc/5321:2151
3620func (c *conn) cmdHelp(p *parser) {
3621 // Let's not strictly parse the request for help. We are ignoring the text anyway.
3622 // ../rfc/5321:2166
3623
3624 c.bwritecodeline(smtp.C214Help, smtp.SeOther00, "see rfc 5321 (smtp)", nil)
3625}
3626
3627// ../rfc/5321:2191
3628func (c *conn) cmdNoop(p *parser) {
3629 // No idea why, but if an argument follows, it must adhere to the string ABNF production...
3630 // ../rfc/5321:2203
3631 if p.space() {
3632 p.xstring()
3633 }
3634 p.xend()
3635
3636 c.bwritecodeline(smtp.C250Completed, smtp.SeOther00, "alrighty", nil)
3637}
3638
3639// ../rfc/5321:2205
3640func (c *conn) cmdQuit(p *parser) {
3641 // ../rfc/5321:2226
3642 p.xend()
3643
3644 c.writecodeline(smtp.C221Closing, smtp.SeOther00, "okay thanks bye", nil)
3645 panic(cleanClose)
3646}
3647