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 "maps"
23 "math"
24 "net"
25 "net/textproto"
26 "os"
27 "runtime/debug"
28 "slices"
29 "sort"
30 "strings"
31 "sync"
32 "time"
33 "unicode"
34
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/publicsuffix"
56 "github.com/mjl-/mox/queue"
57 "github.com/mjl-/mox/ratelimit"
58 "github.com/mjl-/mox/scram"
59 "github.com/mjl-/mox/smtp"
60 "github.com/mjl-/mox/spf"
61 "github.com/mjl-/mox/store"
62 "github.com/mjl-/mox/tlsrpt"
63 "github.com/mjl-/mox/tlsrptdb"
64)
65
66// We use panic and recover for error handling while executing commands.
67// These errors signal the connection must be closed.
68var errIO = errors.New("io error")
69
70// If set, regular delivery/submit is sidestepped, email is accepted and
71// delivered to the account named mox.
72var Localserve bool
73
74var limiterConnectionRate, limiterConnections *ratelimit.Limiter
75
76// For delivery rate limiting. Variable because changed during tests.
77var limitIPMasked1MessagesPerMinute int = 500
78var limitIPMasked1SizePerMinute int64 = 1000 * 1024 * 1024
79
80// Maximum number of RCPT TO commands (i.e. recipients) for a single message
81// delivery. Must be at least 100. Announced in LIMIT extension.
82const rcptToLimit = 1000
83
84func init() {
85 // Also called by tests, so they don't trigger the rate limiter.
86 limitersInit()
87}
88
89func limitersInit() {
90 mox.LimitersInit()
91 // todo future: make these configurable
92 limiterConnectionRate = &ratelimit.Limiter{
93 WindowLimits: []ratelimit.WindowLimit{
94 {
95 Window: time.Minute,
96 Limits: [...]int64{300, 900, 2700},
97 },
98 },
99 }
100 limiterConnections = &ratelimit.Limiter{
101 WindowLimits: []ratelimit.WindowLimit{
102 {
103 Window: time.Duration(math.MaxInt64), // All of time.
104 Limits: [...]int64{30, 90, 270},
105 },
106 },
107 }
108}
109
110var (
111 // Delays for bad/suspicious behaviour. Zero during tests.
112 badClientDelay = time.Second // Before reads and after 1-byte writes for probably spammers.
113 authFailDelay = time.Second // Response to authentication failure.
114 unknownRecipientsDelay = 5 * time.Second // Response when all recipients are unknown.
115 firstTimeSenderDelayDefault = 15 * time.Second // Before accepting message from first-time sender.
116)
117
118type codes struct {
119 code int
120 secode string // Enhanced code, but without the leading major int from code.
121}
122
123var (
124 metricConnection = promauto.NewCounterVec(
125 prometheus.CounterOpts{
126 Name: "mox_smtpserver_connection_total",
127 Help: "Incoming SMTP connections.",
128 },
129 []string{
130 "kind", // "deliver" or "submit"
131 },
132 )
133 metricCommands = promauto.NewHistogramVec(
134 prometheus.HistogramOpts{
135 Name: "mox_smtpserver_command_duration_seconds",
136 Help: "SMTP server command duration and result codes in seconds.",
137 Buckets: []float64{0.001, 0.005, 0.01, 0.05, 0.100, 0.5, 1, 5, 10, 20, 30, 60, 120},
138 },
139 []string{
140 "kind", // "deliver" or "submit"
141 "cmd",
142 "code",
143 "ecode",
144 },
145 )
146 metricDelivery = promauto.NewCounterVec(
147 prometheus.CounterOpts{
148 Name: "mox_smtpserver_delivery_total",
149 Help: "SMTP incoming message delivery from external source, not submission. Result values: delivered, reject, unknownuser, accounterror, delivererror. Reason indicates why a message was rejected/accepted.",
150 },
151 []string{
152 "result",
153 "reason",
154 },
155 )
156 // Similar between ../webmail/webmail.go:/metricSubmission and ../smtpserver/server.go:/metricSubmission and ../webapisrv/server.go:/metricSubmission
157 metricSubmission = promauto.NewCounterVec(
158 prometheus.CounterOpts{
159 Name: "mox_smtpserver_submission_total",
160 Help: "SMTP server incoming submission results, known values (those ending with error are server errors): ok, badmessage, badfrom, badheader, messagelimiterror, recipientlimiterror, localserveerror, queueerror.",
161 },
162 []string{
163 "result",
164 },
165 )
166 metricServerErrors = promauto.NewCounterVec(
167 prometheus.CounterOpts{
168 Name: "mox_smtpserver_errors_total",
169 Help: "SMTP server errors, known values: dkimsign, queuedsn.",
170 },
171 []string{
172 "error",
173 },
174 )
175 metricDeliveryStarttls = promauto.NewCounter(
176 prometheus.CounterOpts{
177 Name: "mox_smtpserver_delivery_starttls_total",
178 Help: "Total number of STARTTLS handshakes for incoming deliveries.",
179 },
180 )
181 metricDeliveryStarttlsErrors = promauto.NewCounterVec(
182 prometheus.CounterOpts{
183 Name: "mox_smtpserver_delivery_starttls_errors_total",
184 Help: "Errors with TLS handshake during STARTTLS for incoming deliveries.",
185 },
186 []string{
187 "reason", // "eof", "sslv2", "unsupportedversions", "nottls", "alert-<num>-<msg>", "other"
188 },
189 )
190)
191
192var jitterRand = mox.NewPseudoRand()
193
194func durationDefault(delay *time.Duration, def time.Duration) time.Duration {
195 if delay == nil {
196 return def
197 }
198 return *delay
199}
200
201// Listen initializes network listeners for incoming SMTP connection.
202// The listeners are stored for a later call to Serve.
203func Listen() {
204 names := slices.Sorted(maps.Keys(mox.Conf.Static.Listeners))
205 for _, name := range names {
206 listener := mox.Conf.Static.Listeners[name]
207
208 var tlsConfig, tlsConfigDelivery *tls.Config
209 var noTLSClientAuth bool
210 if listener.TLS != nil {
211 tlsConfig = listener.TLS.Config
212 // For SMTP delivery, if we get a TLS handshake for an SNI hostname that we don't
213 // allow, we'll fallback to a certificate for the listener hostname instead of
214 // causing the connection to fail. May improve interoperability.
215 tlsConfigDelivery = listener.TLS.ConfigFallback
216 noTLSClientAuth = listener.TLS.ClientAuthDisabled
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, noTLSClientAuth, 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, noTLSClientAuth, 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, noTLSClientAuth, 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, noTLSClientAuth bool, maxMessageSize int64, requireTLSForAuth, requireTLSForDelivery, requireTLS bool, dnsBLs []dns.Domain, firstTimeSenderDelay time.Duration) {
269 log := mlog.New("smtpserver", nil)
270 addr := net.JoinHostPort(ip, fmt.Sprintf("%d", port))
271 if os.Getuid() == 0 {
272 log.Print("listening for smtp",
273 slog.String("listener", name),
274 slog.String("address", addr),
275 slog.String("protocol", protocol))
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, false, noTLSClientAuth, 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 viaHTTPS bool // Whether the connection came in via the HTTPS port (using TLS ALPN).
329 noTLSClientAuth bool
330 resolver dns.Resolver
331 // The "x" in the readers and writes indicate Read and Write errors use panic to
332 // propagate the error.
333 xbr *bufio.Reader
334 xbw *bufio.Writer
335 xtr *moxio.TraceReader // Kept for changing trace level during cmd/auth/data.
336 xtw *moxio.TraceWriter
337 slow bool // If set, reads are done with a 1 second sleep, and writes are done 1 byte at a time, to keep spammers busy.
338 lastlog time.Time // Used for printing the delta time since the previous logging for this connection.
339 submission bool // ../rfc/6409:19 applies
340 baseTLSConfig *tls.Config
341 localIP net.IP
342 remoteIP net.IP
343 hostname dns.Domain
344 log mlog.Log // Used for all synchronous logging on this connection, see logbg for logging in a separate goroutine.
345 maxMessageSize int64
346 requireTLSForAuth bool
347 requireTLSForDelivery bool // If set, delivery is only allowed with TLS (STARTTLS), except if delivery is to a TLS reporting address.
348 cmd string // Current command.
349 cmdStart time.Time // Start of current command.
350 ncmds int // Number of commands processed. Used to abort connection when first incoming command is unknown/invalid.
351 dnsBLs []dns.Domain
352 firstTimeSenderDelay time.Duration
353
354 // If non-zero, taken into account during Read and Write. Set while processing DATA
355 // command, we don't want the entire delivery to take too long.
356 deadline time.Time
357
358 hello dns.IPDomain // Claimed remote name. Can be ip address for ehlo.
359 ehlo bool // If set, we had EHLO instead of HELO.
360
361 authFailed int // Number of failed auth attempts. For slowing down remote with many failures.
362 authSASL bool // Whether SASL authentication was done.
363 authTLS bool // Whether we did TLS client cert authentication.
364 username string // Only when authenticated.
365 account *store.Account // Only when authenticated.
366
367 // We track good/bad message transactions to disconnect spammers trying to guess addresses.
368 transactionGood int
369 transactionBad int
370
371 // Message transaction.
372 mailFrom *smtp.Path
373 requireTLS *bool // MAIL FROM with REQUIRETLS set.
374 futureRelease time.Time // MAIL FROM with HOLDFOR or HOLDUNTIL.
375 futureReleaseRequest string // For use in DSNs, either "for;" or "until;" plus original value. ../rfc/4865:305
376 has8bitmime bool // If MAIL FROM parameter BODY=8BITMIME was sent. Required for SMTPUTF8.
377 smtputf8 bool // todo future: we should keep track of this per recipient. perhaps only a specific recipient requires smtputf8, e.g. due to a utf8 localpart.
378 msgsmtputf8 bool // Is SMTPUTF8 required for the received message. Default to the same value as `smtputf8`, but is re-evaluated after the whole message (envelope and data) is received.
379 recipients []recipient
380}
381
382type rcptAccount struct {
383 AccountName string
384 Destination config.Destination
385 CanonicalAddress string // Optional catchall part stripped and/or lowercased.
386}
387
388type rcptAlias struct {
389 Alias config.Alias
390 CanonicalAddress string // Optional catchall part stripped and/or lowercased.
391}
392
393type recipient struct {
394 Addr smtp.Path
395
396 // If account and alias are both not set, this is not for a local address. This is
397 // normal for submission, where messages are added to the queue. For incoming
398 // deliveries, this will result in an error.
399 Account *rcptAccount // If set, recipient address is for this local account.
400 Alias *rcptAlias // If set, for a local alias.
401}
402
403func isClosed(err error) bool {
404 return errors.Is(err, errIO) || mlog.IsClosed(err)
405}
406
407// Logbg returns a logger for logging in the background (in a goroutine), eg for
408// logging LoginAttempts. The regular c.log has a handler that evaluates fields on
409// the connection at time of logging, which may happen at the same time as
410// modifications to those fields.
411func (c *conn) logbg() mlog.Log {
412 log := mlog.New("smtpserver", nil).WithCid(c.cid)
413 if c.username != "" {
414 log = log.With(slog.String("username", c.username))
415 }
416 return log
417}
418
419// loginAttempt initializes a store.LoginAttempt, for adding to the store after
420// filling in the results and other details.
421func (c *conn) loginAttempt(useTLS bool, authMech string) store.LoginAttempt {
422 var state *tls.ConnectionState
423 if tc, ok := c.conn.(*tls.Conn); ok && useTLS {
424 v := tc.ConnectionState()
425 state = &v
426 }
427
428 return store.LoginAttempt{
429 RemoteIP: c.remoteIP.String(),
430 LocalIP: c.localIP.String(),
431 TLS: store.LoginAttemptTLS(state),
432 Protocol: "submission",
433 AuthMech: authMech,
434 Result: store.AuthError, // Replaced by caller.
435 }
436}
437
438// makeTLSConfig makes a new tls config that is bound to the connection for
439// possible client certificate authentication in case of submission.
440func (c *conn) makeTLSConfig() *tls.Config {
441 if !c.submission || c.noTLSClientAuth {
442 return c.baseTLSConfig
443 }
444
445 // We clone the config so we can set VerifyPeerCertificate below to a method bound
446 // to this connection. Earlier, we set session keys explicitly on the base TLS
447 // config, so they can be used for this connection too.
448 tlsConf := c.baseTLSConfig.Clone()
449
450 // Allow client certificate authentication, for use with the sasl "external"
451 // authentication mechanism.
452 tlsConf.ClientAuth = tls.RequestClientCert
453
454 // We verify the client certificate during the handshake. The TLS handshake is
455 // initiated explicitly for incoming connections and during starttls, so we can
456 // immediately extract the account name and address used for authentication.
457 tlsConf.VerifyPeerCertificate = c.tlsClientAuthVerifyPeerCert
458
459 return tlsConf
460}
461
462// tlsClientAuthVerifyPeerCert can be used as tls.Config.VerifyPeerCertificate, and
463// sets authentication-related fields on conn. This is not called on resumed TLS
464// connections.
465func (c *conn) tlsClientAuthVerifyPeerCert(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
466 if len(rawCerts) == 0 {
467 return nil
468 }
469
470 // If we had too many authentication failures from this IP, don't attempt
471 // authentication. If this is a new incoming connetion, it is closed after the TLS
472 // handshake.
473 if !mox.LimiterFailedAuth.CanAdd(c.remoteIP, time.Now(), 1) {
474 return nil
475 }
476
477 cert, err := x509.ParseCertificate(rawCerts[0])
478 if err != nil {
479 c.log.Debugx("parsing tls client certificate", err)
480 return err
481 }
482 if err := c.tlsClientAuthVerifyPeerCertParsed(cert); err != nil {
483 c.log.Debugx("verifying tls client certificate", err)
484 return fmt.Errorf("verifying client certificate: %w", err)
485 }
486 return nil
487}
488
489// tlsClientAuthVerifyPeerCertParsed verifies a client certificate. Called both for
490// fresh and resumed TLS connections.
491func (c *conn) tlsClientAuthVerifyPeerCertParsed(cert *x509.Certificate) error {
492 if c.account != nil {
493 return fmt.Errorf("cannot authenticate with tls client certificate after previous authentication")
494 }
495
496 la := c.loginAttempt(false, "tlsclientauth")
497 defer func() {
498 // Get TLS connection state in goroutine because we are called while performing the
499 // TLS handshake, which already has the tls connection locked.
500 conn := c.conn.(*tls.Conn)
501 logbg := c.logbg() // Evaluate attributes now, can't do it in goroutine.
502 go func() {
503 defer func() {
504 // In case of panic don't take the whole program down.
505 x := recover()
506 if x != nil {
507 c.log.Error("recover from panic", slog.Any("panic", x))
508 debug.PrintStack()
509 metrics.PanicInc(metrics.Smtpserver)
510 }
511 }()
512
513 state := conn.ConnectionState()
514 la.TLS = store.LoginAttemptTLS(&state)
515 store.LoginAttemptAdd(context.Background(), logbg, la)
516 }()
517
518 if la.Result == store.AuthSuccess {
519 mox.LimiterFailedAuth.Reset(c.remoteIP, time.Now())
520 } else {
521 mox.LimiterFailedAuth.Add(c.remoteIP, time.Now(), 1)
522 }
523 }()
524
525 // For many failed auth attempts, slow down verification attempts.
526 if c.authFailed > 3 && authFailDelay > 0 {
527 mox.Sleep(mox.Context, time.Duration(c.authFailed-3)*authFailDelay)
528 }
529 c.authFailed++ // Compensated on success.
530 defer func() {
531 // On the 3rd failed authentication, start responding slowly. Successful auth will
532 // cause fast responses again.
533 if c.authFailed >= 3 {
534 c.setSlow(true)
535 }
536 }()
537
538 shabuf := sha256.Sum256(cert.RawSubjectPublicKeyInfo)
539 fp := base64.RawURLEncoding.EncodeToString(shabuf[:])
540 la.TLSPubKeyFingerprint = fp
541 pubKey, err := store.TLSPublicKeyGet(context.TODO(), fp)
542 if err != nil {
543 if err == bstore.ErrAbsent {
544 la.Result = store.AuthBadCredentials
545 }
546 return fmt.Errorf("looking up tls public key with fingerprint %s, subject %q, issuer %q: %v", fp, cert.Subject, cert.Issuer, err)
547 }
548 la.LoginAddress = pubKey.LoginAddress
549
550 // Verify account exists and still matches address. We don't check for account
551 // login being disabled if preauth is disabled. In that case, sasl external auth
552 // will be done before credentials can be used, and login disabled will be checked
553 // then, where it will result in a more helpful error message.
554 checkLoginDisabled := !pubKey.NoIMAPPreauth
555 acc, accName, _, err := store.OpenEmail(c.log, pubKey.LoginAddress, checkLoginDisabled)
556 la.AccountName = accName
557 if err != nil {
558 if errors.Is(err, store.ErrLoginDisabled) {
559 la.Result = store.AuthLoginDisabled
560 }
561 return fmt.Errorf("opening account for address %s for public key %s: %w", pubKey.LoginAddress, fp, err)
562 }
563 defer func() {
564 if acc != nil {
565 err := acc.Close()
566 c.log.Check(err, "close account")
567 }
568 }()
569 la.AccountName = acc.Name
570 if acc.Name != pubKey.Account {
571 return fmt.Errorf("tls client public key %s is for account %s, but email address %s is for account %s", fp, pubKey.Account, pubKey.LoginAddress, acc.Name)
572 }
573
574 c.authFailed = 0
575 c.account = acc
576 acc = nil // Prevent cleanup by defer.
577 c.username = pubKey.LoginAddress
578 c.authTLS = true
579 la.Result = store.AuthSuccess
580 c.log.Debug("tls client authenticated with client certificate",
581 slog.String("fingerprint", fp),
582 slog.String("username", c.username),
583 slog.String("account", c.account.Name),
584 slog.Any("remote", c.remoteIP))
585 return nil
586}
587
588// xtlsHandshakeAndAuthenticate performs the TLS handshake, and verifies a client
589// certificate if present.
590func (c *conn) xtlsHandshakeAndAuthenticate(conn net.Conn) {
591 tlsConn := tls.Server(conn, c.makeTLSConfig())
592 c.conn = tlsConn
593
594 cidctx := context.WithValue(mox.Context, mlog.CidKey, c.cid)
595 ctx, cancel := context.WithTimeout(cidctx, time.Minute)
596 defer cancel()
597 c.log.Debug("starting tls server handshake")
598 if !c.submission {
599 metricDeliveryStarttls.Inc()
600 }
601 if err := tlsConn.HandshakeContext(ctx); err != nil {
602 if !c.submission {
603 // Errors from crypto/tls mostly aren't typed. We'll have to look for strings...
604 reason := "other"
605 if errors.Is(err, io.EOF) {
606 reason = "eof"
607 } else if alert, ok := mox.AsTLSAlert(err); ok {
608 reason = tlsrpt.FormatAlert(alert)
609 } else {
610 s := err.Error()
611 if strings.Contains(s, "tls: client offered only unsupported versions") {
612 reason = "unsupportedversions"
613 } else if strings.Contains(s, "tls: first record does not look like a TLS handshake") {
614 reason = "nottls"
615 } else if strings.Contains(s, "tls: unsupported SSLv2 handshake received") {
616 reason = "sslv2"
617 }
618 }
619 metricDeliveryStarttlsErrors.WithLabelValues(reason).Inc()
620 }
621 panic(fmt.Errorf("tls handshake: %s (%w)", err, errIO))
622 }
623 cancel()
624
625 cs := tlsConn.ConnectionState()
626 if cs.DidResume && len(cs.PeerCertificates) > 0 && !c.noTLSClientAuth {
627 // Verify client after session resumption.
628 err := c.tlsClientAuthVerifyPeerCertParsed(cs.PeerCertificates[0])
629 if err != nil {
630 panic(fmt.Errorf("tls verify client certificate after resumption: %s (%w)", err, errIO))
631 }
632 }
633
634 version, ciphersuite := moxio.TLSInfo(cs)
635 attrs := []slog.Attr{
636 slog.String("version", version),
637 slog.String("ciphersuite", ciphersuite),
638 slog.String("sni", cs.ServerName),
639 slog.Bool("resumed", cs.DidResume),
640 slog.Bool("notlsclientauth", c.noTLSClientAuth),
641 slog.Int("clientcerts", len(cs.PeerCertificates)),
642 }
643 if c.account != nil {
644 attrs = append(attrs,
645 slog.String("account", c.account.Name),
646 slog.String("username", c.username),
647 )
648 }
649 c.log.Debug("tls handshake completed", attrs...)
650}
651
652// completely reset connection state as if greeting has just been sent.
653// ../rfc/3207:210
654func (c *conn) reset() {
655 c.ehlo = false
656 c.hello = dns.IPDomain{}
657 if !c.authTLS {
658 c.username = ""
659 if c.account != nil {
660 err := c.account.Close()
661 c.log.Check(err, "closing account")
662 }
663 c.account = nil
664 }
665 c.authSASL = false
666 c.rset()
667}
668
669// for rset command, and a few more cases that reset the mail transaction state.
670// ../rfc/5321:2502
671func (c *conn) rset() {
672 c.mailFrom = nil
673 c.requireTLS = nil
674 c.futureRelease = time.Time{}
675 c.futureReleaseRequest = ""
676 c.has8bitmime = false
677 c.smtputf8 = false
678 c.msgsmtputf8 = false
679 c.recipients = nil
680}
681
682func (c *conn) earliestDeadline(d time.Duration) time.Time {
683 e := time.Now().Add(d)
684 if !c.deadline.IsZero() && c.deadline.Before(e) {
685 return c.deadline
686 }
687 return e
688}
689
690func (c *conn) xcheckAuth() {
691 if c.submission && c.account == nil {
692 // ../rfc/4954:623
693 xsmtpUserErrorf(smtp.C530SecurityRequired, smtp.SePol7Other0, "authentication required")
694 }
695}
696
697func (c *conn) xtrace(level slog.Level) func() {
698 c.xflush()
699 c.xtr.SetTrace(level)
700 c.xtw.SetTrace(level)
701 return func() {
702 c.xflush()
703 c.xtr.SetTrace(mlog.LevelTrace)
704 c.xtw.SetTrace(mlog.LevelTrace)
705 }
706}
707
708// setSlow marks the connection slow (or now), so reads are done with 3 second
709// delay for each read, and writes are done at 1 byte per second, to try to slow
710// down spammers.
711func (c *conn) setSlow(on bool) {
712 if on && !c.slow {
713 c.log.Debug("connection changed to slow")
714 } else if !on && c.slow {
715 c.log.Debug("connection restored to regular pace")
716 }
717 c.slow = on
718}
719
720// Write writes to the connection. It panics on i/o errors, which is handled by the
721// connection command loop.
722func (c *conn) Write(buf []byte) (int, error) {
723 chunk := len(buf)
724 if c.slow {
725 chunk = 1
726 }
727
728 // We set a single deadline for Write and Read. This may be a TLS connection.
729 // SetDeadline works on the underlying connection. If we wouldn't touch the read
730 // deadline, and only set the write deadline and do a bunch of writes, the TLS
731 // library would still have to do reads on the underlying connection, and may reach
732 // a read deadline that was set for some earlier read.
733 // We have one deadline for the whole write. In case of slow writing, we'll write
734 // the last chunk in one go, so remote smtp clients don't abort the connection for
735 // being slow.
736 deadline := c.earliestDeadline(30 * time.Second)
737 if err := c.conn.SetDeadline(deadline); err != nil {
738 c.log.Errorx("setting deadline for write", err)
739 }
740
741 var n int
742 for len(buf) > 0 {
743 nn, err := c.conn.Write(buf[:chunk])
744 if err != nil {
745 panic(fmt.Errorf("write: %s (%w)", err, errIO))
746 }
747 n += nn
748 buf = buf[chunk:]
749 if len(buf) > 0 && badClientDelay > 0 {
750 mox.Sleep(mox.Context, badClientDelay)
751
752 // Make sure we don't take too long, otherwise the remote SMTP client may close the
753 // connection.
754 if time.Until(deadline) < 5*badClientDelay {
755 chunk = len(buf)
756 }
757 }
758 }
759 return n, nil
760}
761
762// Read reads from the connection. It panics on i/o errors, which is handled by the
763// connection command loop.
764func (c *conn) Read(buf []byte) (int, error) {
765 if c.slow && badClientDelay > 0 {
766 mox.Sleep(mox.Context, badClientDelay)
767 }
768
769 // todo future: make deadline configurable for callers, and through config file? ../rfc/5321:3610 ../rfc/6409:492
770 // See comment about Deadline instead of individual read/write deadlines at Write.
771 if err := c.conn.SetDeadline(c.earliestDeadline(30 * time.Second)); err != nil {
772 c.log.Errorx("setting deadline for read", err)
773 }
774
775 n, err := c.conn.Read(buf)
776 if err != nil {
777 panic(fmt.Errorf("read: %s (%w)", err, errIO))
778 }
779 return n, err
780}
781
782// Cache of line buffers for reading commands.
783// Filled on demand.
784var bufpool = moxio.NewBufpool(8, 2*1024)
785
786func (c *conn) xreadline() string {
787 line, err := bufpool.Readline(c.log, c.xbr)
788 if err != nil && errors.Is(err, moxio.ErrLineTooLong) {
789 c.xwritecodeline(smtp.C500BadSyntax, smtp.SeProto5Other0, "line too long, smtp max is 512, we reached 2048", nil)
790 panic(fmt.Errorf("%s (%w)", err, errIO))
791 } else if err != nil {
792 panic(fmt.Errorf("%s (%w)", err, errIO))
793 }
794 return line
795}
796
797// Buffered-write command response line to connection with codes and msg.
798// Err is not sent to remote but is used for logging and can be empty.
799func (c *conn) xbwritecodeline(code int, secode string, msg string, err error) {
800 var ecode string
801 if secode != "" {
802 ecode = fmt.Sprintf("%d.%s", code/100, secode)
803 }
804 metricCommands.WithLabelValues(c.kind(), c.cmd, fmt.Sprintf("%d", code), ecode).Observe(float64(time.Since(c.cmdStart)) / float64(time.Second))
805 c.log.Debugx("smtp command result", err,
806 slog.String("kind", c.kind()),
807 slog.String("cmd", c.cmd),
808 slog.Int("code", code),
809 slog.String("ecode", ecode),
810 slog.Duration("duration", time.Since(c.cmdStart)))
811
812 var sep string
813 if ecode != "" {
814 sep = " "
815 }
816
817 // Separate by newline and wrap long lines.
818 lines := strings.Split(msg, "\n")
819 for i, line := range lines {
820 // ../rfc/5321:3506 ../rfc/5321:2583 ../rfc/5321:2756
821 var prelen = 3 + 1 + len(ecode) + len(sep)
822 for prelen+len(line) > 510 {
823 e := 510 - prelen
824 for ; e > 400 && line[e] != ' '; e-- {
825 }
826 // todo future: understand if ecode should be on each line. won't hurt. at least as long as we don't do expn or vrfy.
827 c.xbwritelinef("%d-%s%s%s", code, ecode, sep, line[:e])
828 line = line[e:]
829 }
830 spdash := " "
831 if i < len(lines)-1 {
832 spdash = "-"
833 }
834 c.xbwritelinef("%d%s%s%s%s", code, spdash, ecode, sep, line)
835 }
836}
837
838// Buffered-write a formatted response line to connection.
839func (c *conn) xbwritelinef(format string, args ...any) {
840 msg := fmt.Sprintf(format, args...)
841 fmt.Fprint(c.xbw, msg+"\r\n")
842}
843
844// Flush pending buffered writes to connection.
845func (c *conn) xflush() {
846 c.xbw.Flush() // Errors will have caused a panic in Write.
847}
848
849// Write (with flush) a response line with codes and message. err is not written, used for logging and can be nil.
850func (c *conn) xwritecodeline(code int, secode string, msg string, err error) {
851 c.xbwritecodeline(code, secode, msg, err)
852 c.xflush()
853}
854
855// Write (with flush) a formatted response line to connection.
856func (c *conn) xwritelinef(format string, args ...any) {
857 c.xbwritelinef(format, args...)
858 c.xflush()
859}
860
861var cleanClose struct{} // Sentinel value for panic/recover indicating clean close of connection.
862
863// ServeTLSConn serves a TLS connection.
864func ServeTLSConn(listenerName string, hostname dns.Domain, conn *tls.Conn, tlsConfig *tls.Config, submission, viaHTTPS bool, maxMsgSize int64, requireTLS bool) {
865 log := mlog.New("smtpserver", nil)
866 resolver := dns.StrictResolver{Log: log.Logger}
867 serve(listenerName, mox.Cid(), hostname, tlsConfig, conn, resolver, submission, true, viaHTTPS, true, maxMsgSize, true, true, requireTLS, nil, 0)
868}
869
870func serve(listenerName string, cid int64, hostname dns.Domain, tlsConfig *tls.Config, nc net.Conn, resolver dns.Resolver, submission, xtls, viaHTTPS, noTLSClientAuth bool, maxMessageSize int64, requireTLSForAuth, requireTLSForDelivery, requireTLS bool, dnsBLs []dns.Domain, firstTimeSenderDelay time.Duration) {
871 var localIP, remoteIP net.IP
872 if a, ok := nc.LocalAddr().(*net.TCPAddr); ok {
873 localIP = a.IP
874 } else {
875 // For net.Pipe, during tests.
876 localIP = net.ParseIP("127.0.0.10")
877 }
878 if a, ok := nc.RemoteAddr().(*net.TCPAddr); ok {
879 remoteIP = a.IP
880 } else {
881 // For net.Pipe, during tests.
882 remoteIP = net.ParseIP("127.0.0.10")
883 }
884
885 origConn := nc
886 if viaHTTPS {
887 origConn = nc.(*tls.Conn).NetConn()
888 }
889
890 c := &conn{
891 cid: cid,
892 origConn: origConn,
893 conn: nc,
894 submission: submission,
895 tls: xtls,
896 viaHTTPS: viaHTTPS,
897 noTLSClientAuth: noTLSClientAuth,
898 extRequireTLS: requireTLS,
899 resolver: resolver,
900 lastlog: time.Now(),
901 baseTLSConfig: tlsConfig,
902 localIP: localIP,
903 remoteIP: remoteIP,
904 hostname: hostname,
905 maxMessageSize: maxMessageSize,
906 requireTLSForAuth: requireTLSForAuth,
907 requireTLSForDelivery: requireTLSForDelivery,
908 dnsBLs: dnsBLs,
909 firstTimeSenderDelay: firstTimeSenderDelay,
910 }
911 var logmutex sync.Mutex
912 // Also see (and possibly update) c.logbg, for logging in a goroutine.
913 c.log = mlog.New("smtpserver", nil).WithFunc(func() []slog.Attr {
914 logmutex.Lock()
915 defer logmutex.Unlock()
916 now := time.Now()
917 l := []slog.Attr{
918 slog.Int64("cid", c.cid),
919 slog.Duration("delta", now.Sub(c.lastlog)),
920 }
921 c.lastlog = now
922 if c.username != "" {
923 l = append(l, slog.String("username", c.username))
924 }
925 return l
926 })
927 c.xtr = moxio.NewTraceReader(c.log, "RC: ", c)
928 c.xbr = bufio.NewReader(c.xtr)
929 c.xtw = moxio.NewTraceWriter(c.log, "LS: ", c)
930 c.xbw = bufio.NewWriter(c.xtw)
931
932 metricConnection.WithLabelValues(c.kind()).Inc()
933 c.log.Info("new connection",
934 slog.Any("remote", c.conn.RemoteAddr()),
935 slog.Any("local", c.conn.LocalAddr()),
936 slog.Bool("submission", submission),
937 slog.Bool("tls", xtls),
938 slog.Bool("viahttps", viaHTTPS),
939 slog.String("listener", listenerName))
940
941 defer func() {
942 err := c.origConn.Close() // Close actual TCP socket, regardless of TLS on top.
943 c.log.Check(err, "closing tcp connection")
944 c.conn.Close() // If TLS, will try to write alert notification to already closed socket, returning error quickly.
945
946 if c.account != nil {
947 err := c.account.Close()
948 c.log.Check(err, "closing account")
949 c.account = nil
950 }
951
952 x := recover()
953 if x == nil || x == cleanClose {
954 c.log.Info("connection closed")
955 } else if err, ok := x.(error); ok && isClosed(err) {
956 c.log.Infox("connection closed", err)
957 } else {
958 c.log.Error("unhandled panic", slog.Any("err", x))
959 debug.PrintStack()
960 metrics.PanicInc(metrics.Smtpserver)
961 }
962 }()
963
964 if xtls && !viaHTTPS {
965 // Start TLS on connection. We perform the handshake explicitly, so we can set a
966 // timeout, do client certificate authentication, log TLS details afterwards.
967 c.xtlsHandshakeAndAuthenticate(c.conn)
968 }
969
970 select {
971 case <-mox.Shutdown.Done():
972 // ../rfc/5321:2811 ../rfc/5321:1666 ../rfc/3463:420
973 c.xwritecodeline(smtp.C421ServiceUnavail, smtp.SeSys3NotAccepting2, "shutting down", nil)
974 return
975 default:
976 }
977
978 if !limiterConnectionRate.Add(c.remoteIP, time.Now(), 1) {
979 c.xwritecodeline(smtp.C421ServiceUnavail, smtp.SePol7Other0, "connection rate from your ip or network too high, slow down please", nil)
980 return
981 }
982
983 // If remote IP/network resulted in too many authentication failures, refuse to serve.
984 if submission && !mox.LimiterFailedAuth.CanAdd(c.remoteIP, time.Now(), 1) {
985 metrics.AuthenticationRatelimitedInc("submission")
986 c.log.Debug("refusing connection due to many auth failures", slog.Any("remoteip", c.remoteIP))
987 c.xwritecodeline(smtp.C421ServiceUnavail, smtp.SePol7Other0, "too many auth failures", nil)
988 return
989 }
990
991 if !limiterConnections.Add(c.remoteIP, time.Now(), 1) {
992 c.log.Debug("refusing connection due to many open connections", slog.Any("remoteip", c.remoteIP))
993 c.xwritecodeline(smtp.C421ServiceUnavail, smtp.SePol7Other0, "too many open connections from your ip or network", nil)
994 return
995 }
996 defer limiterConnections.Add(c.remoteIP, time.Now(), -1)
997
998 // We register and unregister the original connection, in case c.conn is replaced
999 // with a TLS connection later on.
1000 mox.Connections.Register(nc, "smtp", listenerName)
1001 defer mox.Connections.Unregister(nc)
1002
1003 // ../rfc/5321:964 ../rfc/5321:4294 about announcing software and version
1004 // Syntax: ../rfc/5321:2586
1005 // We include the string ESMTP. https://cr.yp.to/smtp/greeting.html recommends it.
1006 // Should not be too relevant nowadays, but does not hurt and default blackbox
1007 // exporter SMTP health check expects it.
1008 c.xwritelinef("%d %s ESMTP mox", smtp.C220ServiceReady, c.hostname.ASCII)
1009
1010 for {
1011 command(c)
1012
1013 // If another command is present, don't flush our buffered response yet. Holding
1014 // off will cause us to respond with a single packet.
1015 n := c.xbr.Buffered()
1016 if n > 0 {
1017 buf, err := c.xbr.Peek(n)
1018 if err == nil && bytes.IndexByte(buf, '\n') >= 0 {
1019 continue
1020 }
1021 }
1022 c.xflush()
1023 }
1024}
1025
1026var commands = map[string]func(c *conn, p *parser){
1027 "helo": (*conn).cmdHelo,
1028 "ehlo": (*conn).cmdEhlo,
1029 "starttls": (*conn).cmdStarttls,
1030 "auth": (*conn).cmdAuth,
1031 "mail": (*conn).cmdMail,
1032 "rcpt": (*conn).cmdRcpt,
1033 "data": (*conn).cmdData,
1034 "rset": (*conn).cmdRset,
1035 "vrfy": (*conn).cmdVrfy,
1036 "expn": (*conn).cmdExpn,
1037 "help": (*conn).cmdHelp,
1038 "noop": (*conn).cmdNoop,
1039 "quit": (*conn).cmdQuit,
1040}
1041
1042func command(c *conn) {
1043 defer func() {
1044 x := recover()
1045 if x == nil {
1046 return
1047 }
1048 err, ok := x.(error)
1049 if !ok {
1050 panic(x)
1051 }
1052
1053 if isClosed(err) {
1054 panic(err)
1055 }
1056
1057 var serr smtpError
1058 if errors.As(err, &serr) {
1059 c.xwritecodeline(serr.code, serr.secode, fmt.Sprintf("%s (%s)", serr.errmsg, mox.ReceivedID(c.cid)), serr.err)
1060 if serr.printStack {
1061 c.log.Errorx("smtp error", serr.err, slog.Int("code", serr.code), slog.String("secode", serr.secode))
1062 debug.PrintStack()
1063 }
1064 } else {
1065 // Other type of panic, we pass it on, aborting the connection.
1066 c.log.Errorx("command panic", err)
1067 panic(err)
1068 }
1069 }()
1070
1071 // todo future: we could wait for either a line or shutdown, and just close the connection on shutdown.
1072
1073 line := c.xreadline()
1074 t := strings.SplitN(line, " ", 2)
1075 var args string
1076 if len(t) == 2 {
1077 args = " " + t[1]
1078 }
1079 cmd := t[0]
1080 cmdl := strings.ToLower(cmd)
1081
1082 // todo future: should we return an error for lines that are too long? perhaps for submission or in a pedantic mode. we would have to take extensions for MAIL into account. ../rfc/5321:3500 ../rfc/5321:3552
1083
1084 select {
1085 case <-mox.Shutdown.Done():
1086 // ../rfc/5321:2811 ../rfc/5321:1666 ../rfc/3463:420
1087 c.xwritecodeline(smtp.C421ServiceUnavail, smtp.SeSys3NotAccepting2, "shutting down", nil)
1088 panic(errIO)
1089 default:
1090 }
1091
1092 c.cmd = cmdl
1093 c.cmdStart = time.Now()
1094
1095 p := newParser(args, c.smtputf8, c)
1096 fn, ok := commands[cmdl]
1097 if !ok {
1098 c.cmd = "(unknown)"
1099 if c.ncmds == 0 {
1100 // Other side is likely speaking something else than SMTP, send error message and
1101 // stop processing because there is a good chance whatever they sent has multiple
1102 // lines.
1103 c.xwritecodeline(smtp.C500BadSyntax, smtp.SeProto5Syntax2, "please try again speaking smtp", nil)
1104 panic(errIO)
1105 }
1106 // note: not "command not implemented", see ../rfc/5321:2934 ../rfc/5321:2539
1107 xsmtpUserErrorf(smtp.C500BadSyntax, smtp.SeProto5BadCmdOrSeq1, "unknown command")
1108 }
1109 c.ncmds++
1110 fn(c, p)
1111}
1112
1113// For use in metric labels.
1114func (c *conn) kind() string {
1115 if c.submission {
1116 return "submission"
1117 }
1118 return "smtp"
1119}
1120
1121func (c *conn) xneedHello() {
1122 if c.hello.IsZero() {
1123 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "no ehlo/helo yet")
1124 }
1125}
1126
1127// If smtp server is configured to require TLS for all mail delivery (except to TLS
1128// reporting address), abort command.
1129func (c *conn) xneedTLSForDelivery(rcpt smtp.Path) {
1130 // For TLS reports, we allow the message in even without TLS, because there may be
1131 // TLS interopability problems. ../rfc/8460:316
1132 if c.requireTLSForDelivery && !c.tls && !isTLSReportRecipient(rcpt) {
1133 // ../rfc/3207:148
1134 xsmtpUserErrorf(smtp.C530SecurityRequired, smtp.SePol7Other0, "STARTTLS required for mail delivery")
1135 }
1136}
1137
1138func isTLSReportRecipient(rcpt smtp.Path) bool {
1139 _, _, _, dest, err := mox.LookupAddress(rcpt.Localpart, rcpt.IPDomain.Domain, false, false, false)
1140 return err == nil && (dest.HostTLSReports || dest.DomainTLSReports)
1141}
1142
1143func (c *conn) cmdHelo(p *parser) {
1144 c.cmdHello(p, false)
1145}
1146
1147func (c *conn) cmdEhlo(p *parser) {
1148 c.cmdHello(p, true)
1149}
1150
1151// ../rfc/5321:1783
1152func (c *conn) cmdHello(p *parser, ehlo bool) {
1153 var remote dns.IPDomain
1154 if c.submission && !mox.Pedantic {
1155 // Mail clients regularly put bogus information in the hostname/ip. For submission,
1156 // the value is of no use, so there is not much point in annoying the user with
1157 // errors they cannot fix themselves. Except when in pedantic mode.
1158 remote = dns.IPDomain{IP: c.remoteIP}
1159 } else {
1160 p.xspace()
1161 if ehlo {
1162 remote = p.xipdomain(true)
1163 } else {
1164 remote = dns.IPDomain{Domain: p.xdomain()}
1165
1166 // Verify a remote domain name has an A or AAAA record, CNAME not allowed. ../rfc/5321:722
1167 cidctx := context.WithValue(mox.Context, mlog.CidKey, c.cid)
1168 ctx, cancel := context.WithTimeout(cidctx, time.Minute)
1169 _, _, err := c.resolver.LookupIPAddr(ctx, remote.Domain.ASCII+".")
1170 cancel()
1171 if dns.IsNotFound(err) {
1172 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeProto5Other0, "your ehlo domain does not resolve to an IP address")
1173 }
1174 // For success or temporary resolve errors, we'll just continue.
1175 }
1176 // ../rfc/5321:1827
1177 // Though a few paragraphs earlier is a claim additional data can occur for address
1178 // literals (IP addresses), although the ABNF in that document does not allow it.
1179 // We allow additional text, but only if space-separated.
1180 if len(remote.IP) > 0 && p.space() {
1181 p.remainder() // ../rfc/5321:1802 ../rfc/2821:1632
1182 }
1183 p.xend()
1184 }
1185
1186 // Reset state as if RSET command has been issued. ../rfc/5321:2093 ../rfc/5321:2453
1187 c.rset()
1188
1189 c.ehlo = ehlo
1190 c.hello = remote
1191
1192 // https://www.iana.org/assignments/mail-parameters/mail-parameters.xhtml
1193
1194 c.xbwritelinef("250-%s", c.hostname.ASCII)
1195 c.xbwritelinef("250-PIPELINING") // ../rfc/2920:108
1196 c.xbwritelinef("250-SIZE %d", c.maxMessageSize) // ../rfc/1870:70
1197 // ../rfc/3207:237
1198 if !c.tls && c.baseTLSConfig != nil {
1199 // ../rfc/3207:90
1200 c.xbwritelinef("250-STARTTLS")
1201 } else if c.extRequireTLS {
1202 // ../rfc/8689:202
1203 // ../rfc/8689:143
1204 c.xbwritelinef("250-REQUIRETLS")
1205 }
1206 if c.submission {
1207 var mechs string
1208 // ../rfc/4954:123
1209 if c.tls || !c.requireTLSForAuth {
1210 // We always mention the SCRAM PLUS variants, even if TLS is not active: It is a
1211 // hint to the client that a TLS connection can use TLS channel binding during
1212 // authentication. The client should select the bare variant when TLS isn't
1213 // present, and also not indicate the server supports the PLUS variant in that
1214 // case, or it would trigger the mechanism downgrade detection.
1215 mechs = "SCRAM-SHA-256-PLUS SCRAM-SHA-256 SCRAM-SHA-1-PLUS SCRAM-SHA-1 CRAM-MD5 PLAIN LOGIN"
1216 }
1217 if c.tls && len(c.conn.(*tls.Conn).ConnectionState().PeerCertificates) > 0 && !c.viaHTTPS && !c.noTLSClientAuth {
1218 mechs = "EXTERNAL " + mechs
1219 }
1220 c.xbwritelinef("250-AUTH %s", mechs)
1221 // ../rfc/4865:127
1222 t := time.Now().Add(queue.FutureReleaseIntervalMax).UTC() // ../rfc/4865:98
1223 c.xbwritelinef("250-FUTURERELEASE %d %s", queue.FutureReleaseIntervalMax/time.Second, t.Format(time.RFC3339))
1224 }
1225 c.xbwritelinef("250-ENHANCEDSTATUSCODES") // ../rfc/2034:71
1226 // todo future? c.writelinef("250-DSN")
1227 c.xbwritelinef("250-8BITMIME") // ../rfc/6152:86
1228 c.xbwritelinef("250-LIMITS RCPTMAX=%d", rcptToLimit) // ../rfc/9422:301
1229 c.xbwritecodeline(250, "", "SMTPUTF8", nil) // ../rfc/6531:201
1230 c.xflush()
1231}
1232
1233// ../rfc/3207:96
1234func (c *conn) cmdStarttls(p *parser) {
1235 c.xneedHello()
1236 p.xend()
1237
1238 if c.tls {
1239 // ../rfc/3207:235
1240 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "already speaking tls")
1241 }
1242 if c.account != nil {
1243 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "cannot starttls after authentication")
1244 }
1245 if c.baseTLSConfig == nil {
1246 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "starttls not offered")
1247 }
1248
1249 // We don't want to do TLS on top of c.r because it also prints protocol traces: We
1250 // don't want to log the TLS stream. So we'll do TLS on the underlying connection,
1251 // but make sure any bytes already read and in the buffer are used for the TLS
1252 // handshake.
1253 conn := c.conn
1254 if n := c.xbr.Buffered(); n > 0 {
1255 conn = &moxio.PrefixConn{
1256 PrefixReader: io.LimitReader(c.xbr, int64(n)),
1257 Conn: conn,
1258 }
1259 }
1260
1261 // We add the cid to the output, to help debugging in case of a failing TLS connection.
1262 c.xwritecodeline(smtp.C220ServiceReady, smtp.SeOther00, "go! ("+mox.ReceivedID(c.cid)+")", nil)
1263
1264 c.xtlsHandshakeAndAuthenticate(conn)
1265
1266 c.reset() // ../rfc/3207:210
1267 c.tls = true
1268}
1269
1270// ../rfc/4954:139
1271func (c *conn) cmdAuth(p *parser) {
1272 c.xneedHello()
1273
1274 if !c.submission {
1275 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "authentication only allowed on submission ports")
1276 }
1277 if c.authSASL {
1278 // ../rfc/4954:152
1279 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "already authenticated")
1280 }
1281 if c.mailFrom != nil {
1282 // ../rfc/4954:157
1283 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "authentication not allowed during mail transaction")
1284 }
1285
1286 // If authentication fails due to missing derived secrets, we don't hold it against
1287 // the connection. There is no way to indicate server support for an authentication
1288 // mechanism, but that a mechanism won't work for an account.
1289 var missingDerivedSecrets bool
1290
1291 // For many failed auth attempts, slow down verification attempts.
1292 // Dropping the connection could also work, but more so when we have a connection rate limiter.
1293 // ../rfc/4954:770
1294 if c.authFailed > 3 && authFailDelay > 0 {
1295 // ../rfc/4954:770
1296 mox.Sleep(mox.Context, time.Duration(c.authFailed-3)*authFailDelay)
1297 }
1298 c.authFailed++ // Compensated on success.
1299 defer func() {
1300 if missingDerivedSecrets {
1301 c.authFailed--
1302 }
1303 // On the 3rd failed authentication, start responding slowly. Successful auth will
1304 // cause fast responses again.
1305 if c.authFailed >= 3 {
1306 c.setSlow(true)
1307 }
1308 }()
1309
1310 la := c.loginAttempt(true, "")
1311 defer func() {
1312 store.LoginAttemptAdd(context.Background(), c.logbg(), la)
1313 if la.Result == store.AuthSuccess {
1314 mox.LimiterFailedAuth.Reset(c.remoteIP, time.Now())
1315 } else if !missingDerivedSecrets {
1316 mox.LimiterFailedAuth.Add(c.remoteIP, time.Now(), 1)
1317 }
1318 }()
1319
1320 // ../rfc/4954:699
1321 p.xspace()
1322 mech := p.xsaslMech()
1323
1324 // Read the first parameter, either as initial parameter or by sending a
1325 // continuation with the optional encChal (must already be base64-encoded).
1326 xreadInitial := func(encChal string) []byte {
1327 var auth string
1328 if p.empty() {
1329 c.xwritelinef("%d %s", smtp.C334ContinueAuth, encChal) // ../rfc/4954:205
1330 // todo future: handle max length of 12288 octets and return proper responde codes otherwise ../rfc/4954:253
1331 auth = c.xreadline()
1332 if auth == "*" {
1333 // ../rfc/4954:193
1334 la.Result = store.AuthAborted
1335 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5Other0, "authentication aborted")
1336 }
1337 } else {
1338 p.xspace()
1339 if !mox.Pedantic {
1340 // Windows Mail 16005.14326.21606.0 sends two spaces between "AUTH PLAIN" and the
1341 // base64 data.
1342 for p.space() {
1343 }
1344 }
1345 auth = p.remainder()
1346 if auth == "" {
1347 // ../rfc/4954:235
1348 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5Syntax2, "missing initial auth base64 parameter after space")
1349 } else if auth == "=" {
1350 // ../rfc/4954:214
1351 auth = "" // Base64 decode below will result in empty buffer.
1352 }
1353 }
1354 buf, err := base64.StdEncoding.DecodeString(auth)
1355 if err != nil {
1356 // ../rfc/4954:235
1357 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5Syntax2, "invalid base64: %s", err)
1358 }
1359 return buf
1360 }
1361
1362 xreadContinuation := func() []byte {
1363 line := c.xreadline()
1364 if line == "*" {
1365 la.Result = store.AuthAborted
1366 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5Other0, "authentication aborted")
1367 }
1368 buf, err := base64.StdEncoding.DecodeString(line)
1369 if err != nil {
1370 // ../rfc/4954:235
1371 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5Syntax2, "invalid base64: %s", err)
1372 }
1373 return buf
1374 }
1375
1376 // The various authentication mechanisms set account and username. We may already
1377 // have an account and username from TLS client authentication. Afterwards, we
1378 // check that the account is the same.
1379 var account *store.Account
1380 var username string
1381 defer func() {
1382 if account != nil {
1383 err := account.Close()
1384 c.log.Check(err, "close account")
1385 }
1386 }()
1387
1388 switch mech {
1389 case "PLAIN":
1390 la.AuthMech = "plain"
1391
1392 // ../rfc/4954:343
1393 // ../rfc/4954:326
1394 if !c.tls && c.requireTLSForAuth {
1395 xsmtpUserErrorf(smtp.C538EncReqForAuth, smtp.SePol7EncReqForAuth11, "authentication requires tls")
1396 }
1397
1398 // Password is in line in plain text, so hide it.
1399 defer c.xtrace(mlog.LevelTraceauth)()
1400 buf := xreadInitial("")
1401 c.xtrace(mlog.LevelTrace) // Restore.
1402 plain := bytes.Split(buf, []byte{0})
1403 if len(plain) != 3 {
1404 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5BadParams4, "auth data should have 3 nul-separated tokens, got %d", len(plain))
1405 }
1406 authz := norm.NFC.String(string(plain[0]))
1407 username = norm.NFC.String(string(plain[1]))
1408 la.LoginAddress = username
1409 password := string(plain[2])
1410
1411 if authz != "" && authz != username {
1412 la.Result = store.AuthBadCredentials
1413 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "cannot assume other role")
1414 }
1415
1416 var err error
1417 account, la.AccountName, err = store.OpenEmailAuth(c.log, username, password, false)
1418 if err != nil && errors.Is(err, store.ErrUnknownCredentials) {
1419 // ../rfc/4954:274
1420 la.Result = store.AuthBadCredentials
1421 c.log.Info("failed authentication attempt", slog.String("username", username), slog.Any("remote", c.remoteIP))
1422 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass")
1423 }
1424 xcheckf(err, "verifying credentials")
1425
1426 case "LOGIN":
1427 // LOGIN is obsoleted in favor of PLAIN, only implemented to support legacy
1428 // clients, see Internet-Draft (I-D):
1429 // https://datatracker.ietf.org/doc/html/draft-murchison-sasl-login-00
1430
1431 la.LoginAddress = "login"
1432
1433 // ../rfc/4954:343
1434 // ../rfc/4954:326
1435 if !c.tls && c.requireTLSForAuth {
1436 xsmtpUserErrorf(smtp.C538EncReqForAuth, smtp.SePol7EncReqForAuth11, "authentication requires tls")
1437 }
1438
1439 // Read user name. The I-D says the client should ignore the server challenge, but
1440 // also that some clients may require challenge "Username:" instead of "User
1441 // Name". We can't sent both... Servers most commonly return "Username:" and
1442 // "Password:", so we do the same.
1443 // I-D says maximum length must be 64 bytes. We allow more, for long user names
1444 // (domains).
1445 encChal := base64.StdEncoding.EncodeToString([]byte("Username:"))
1446 username = string(xreadInitial(encChal))
1447 username = norm.NFC.String(username)
1448 la.LoginAddress = username
1449
1450 // Again, client should ignore the challenge, we send the same as the example in
1451 // the I-D.
1452 c.xwritelinef("%d %s", smtp.C334ContinueAuth, base64.StdEncoding.EncodeToString([]byte("Password:")))
1453
1454 // Password is in line in plain text, so hide it.
1455 defer c.xtrace(mlog.LevelTraceauth)()
1456 password := string(xreadContinuation())
1457 c.xtrace(mlog.LevelTrace) // Restore.
1458
1459 var err error
1460 account, la.AccountName, err = store.OpenEmailAuth(c.log, username, password, false)
1461 if err != nil && errors.Is(err, store.ErrUnknownCredentials) {
1462 // ../rfc/4954:274
1463 la.Result = store.AuthBadCredentials
1464 c.log.Info("failed authentication attempt", slog.String("username", username), slog.Any("remote", c.remoteIP))
1465 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass")
1466 }
1467 xcheckf(err, "verifying credentials")
1468
1469 case "CRAM-MD5":
1470 la.AuthMech = strings.ToLower(mech)
1471
1472 p.xempty()
1473
1474 // ../rfc/2195:82
1475 chal := fmt.Sprintf("<%d.%d@%s>", uint64(mox.CryptoRandInt()), time.Now().UnixNano(), mox.Conf.Static.HostnameDomain.ASCII)
1476 c.xwritelinef("%d %s", smtp.C334ContinueAuth, base64.StdEncoding.EncodeToString([]byte(chal)))
1477
1478 resp := xreadContinuation()
1479 t := strings.Split(string(resp), " ")
1480 if len(t) != 2 || len(t[1]) != 2*md5.Size {
1481 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5BadParams4, "malformed cram-md5 response")
1482 }
1483 username = norm.NFC.String(t[0])
1484 la.LoginAddress = username
1485 c.log.Debug("cram-md5 auth", slog.String("username", username))
1486 var err error
1487 account, la.AccountName, _, err = store.OpenEmail(c.log, username, false)
1488 if err != nil && errors.Is(err, store.ErrUnknownCredentials) {
1489 la.Result = store.AuthBadCredentials
1490 c.log.Info("failed authentication attempt", slog.String("username", username), slog.Any("remote", c.remoteIP))
1491 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass")
1492 }
1493 xcheckf(err, "looking up address")
1494 la.AccountName = account.Name
1495 var ipadhash, opadhash hash.Hash
1496 account.WithRLock(func() {
1497 err := account.DB.Read(context.TODO(), func(tx *bstore.Tx) error {
1498 password, err := bstore.QueryTx[store.Password](tx).Get()
1499 if err == bstore.ErrAbsent {
1500 c.log.Info("failed authentication attempt", slog.String("username", username), slog.Any("remote", c.remoteIP))
1501 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass")
1502 }
1503 if err != nil {
1504 return err
1505 }
1506
1507 ipadhash = password.CRAMMD5.Ipad
1508 opadhash = password.CRAMMD5.Opad
1509 return nil
1510 })
1511 xcheckf(err, "tx read")
1512 })
1513 if ipadhash == nil || opadhash == nil {
1514 missingDerivedSecrets = true
1515 c.log.Info("cram-md5 auth attempt without derived secrets set, save password again to store secrets", slog.String("username", username))
1516 c.log.Info("failed authentication attempt", slog.String("username", username), slog.Any("remote", c.remoteIP))
1517 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass")
1518 }
1519
1520 // ../rfc/2195:138 ../rfc/2104:142
1521 ipadhash.Write([]byte(chal))
1522 opadhash.Write(ipadhash.Sum(nil))
1523 digest := fmt.Sprintf("%x", opadhash.Sum(nil))
1524 if digest != t[1] {
1525 c.log.Info("failed authentication attempt", slog.String("username", username), slog.Any("remote", c.remoteIP))
1526 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass")
1527 }
1528
1529 case "SCRAM-SHA-256-PLUS", "SCRAM-SHA-256", "SCRAM-SHA-1-PLUS", "SCRAM-SHA-1":
1530 // todo: improve handling of errors during scram. e.g. invalid parameters. should we abort the imap command, or continue until the end and respond with a scram-level error?
1531 // todo: use single implementation between ../imapserver/server.go and ../smtpserver/server.go
1532
1533 // Passwords cannot be retrieved or replayed from the trace.
1534
1535 la.AuthMech = strings.ToLower(mech)
1536 var h func() hash.Hash
1537 switch la.AuthMech {
1538 case "scram-sha-1", "scram-sha-1-plus":
1539 h = sha1.New
1540 case "scram-sha-256", "scram-sha-256-plus":
1541 h = sha256.New
1542 default:
1543 xsmtpServerErrorf(codes{smtp.C554TransactionFailed, smtp.SeSys3Other0}, "missing scram auth method case")
1544 }
1545
1546 var cs *tls.ConnectionState
1547 channelBindingRequired := strings.HasSuffix(la.AuthMech, "-plus")
1548 if channelBindingRequired && !c.tls {
1549 // ../rfc/4954:630
1550 xsmtpUserErrorf(smtp.C538EncReqForAuth, smtp.SePol7EncReqForAuth11, "scram plus mechanism requires tls connection")
1551 }
1552 if c.tls {
1553 xcs := c.conn.(*tls.Conn).ConnectionState()
1554 cs = &xcs
1555 }
1556 c0 := xreadInitial("")
1557 ss, err := scram.NewServer(h, c0, cs, channelBindingRequired)
1558 if err != nil {
1559 c.log.Infox("scram protocol error", err, slog.Any("remote", c.remoteIP))
1560 xsmtpUserErrorf(smtp.C455BadParams, smtp.SePol7Other0, "scram protocol error: %s", err)
1561 }
1562 username = ss.Authentication
1563 la.LoginAddress = username
1564 c.log.Debug("scram auth", slog.String("authentication", username))
1565 account, la.AccountName, _, err = store.OpenEmail(c.log, username, false)
1566 if err != nil {
1567 // todo: we could continue scram with a generated salt, deterministically generated
1568 // from the username. that way we don't have to store anything but attackers cannot
1569 // learn if an account exists. same for absent scram saltedpassword below.
1570 c.log.Info("failed authentication attempt", slog.String("username", username), slog.Any("remote", c.remoteIP))
1571 xsmtpUserErrorf(smtp.C454TempAuthFail, smtp.SeSys3Other0, "scram not possible")
1572 }
1573 if ss.Authorization != "" && ss.Authorization != username {
1574 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "authentication with authorization for different user not supported")
1575 }
1576 var xscram store.SCRAM
1577 account.WithRLock(func() {
1578 err := account.DB.Read(context.TODO(), func(tx *bstore.Tx) error {
1579 password, err := bstore.QueryTx[store.Password](tx).Get()
1580 if err == bstore.ErrAbsent {
1581 c.log.Info("failed authentication attempt", slog.String("username", username), slog.Any("remote", c.remoteIP))
1582 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass")
1583 }
1584 xcheckf(err, "fetching credentials")
1585 switch la.AuthMech {
1586 case "scram-sha-1", "scram-sha-1-plus":
1587 xscram = password.SCRAMSHA1
1588 case "scram-sha-256", "scram-sha-256-plus":
1589 xscram = password.SCRAMSHA256
1590 default:
1591 xsmtpServerErrorf(codes{smtp.C554TransactionFailed, smtp.SeSys3Other0}, "missing scram auth credentials case")
1592 }
1593 if len(xscram.Salt) == 0 || xscram.Iterations == 0 || len(xscram.SaltedPassword) == 0 {
1594 missingDerivedSecrets = true
1595 c.log.Info("scram auth attempt without derived secrets set, save password again to store secrets", slog.String("address", username))
1596 c.log.Info("failed authentication attempt", slog.String("username", username), slog.Any("remote", c.remoteIP))
1597 xsmtpUserErrorf(smtp.C454TempAuthFail, smtp.SeSys3Other0, "scram not possible")
1598 }
1599 return nil
1600 })
1601 xcheckf(err, "read tx")
1602 })
1603 s1, err := ss.ServerFirst(xscram.Iterations, xscram.Salt)
1604 xcheckf(err, "scram first server step")
1605 c.xwritelinef("%d %s", smtp.C334ContinueAuth, base64.StdEncoding.EncodeToString([]byte(s1))) // ../rfc/4954:187
1606 c2 := xreadContinuation()
1607 s3, err := ss.Finish(c2, xscram.SaltedPassword)
1608 if len(s3) > 0 {
1609 c.xwritelinef("%d %s", smtp.C334ContinueAuth, base64.StdEncoding.EncodeToString([]byte(s3))) // ../rfc/4954:187
1610 }
1611 if err != nil {
1612 c.xreadline() // Should be "*" for cancellation.
1613 if errors.Is(err, scram.ErrInvalidProof) {
1614 la.Result = store.AuthBadCredentials
1615 c.log.Info("failed authentication attempt", slog.String("username", username), slog.Any("remote", c.remoteIP))
1616 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad credentials")
1617 } else if errors.Is(err, scram.ErrChannelBindingsDontMatch) {
1618 la.Result = store.AuthBadChannelBinding
1619 c.log.Warn("bad channel binding during authentication, potential mitm", slog.String("username", username), slog.Any("remote", c.remoteIP))
1620 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7MsgIntegrity7, "channel bindings do not match, potential mitm")
1621 } else if errors.Is(err, scram.ErrInvalidEncoding) {
1622 la.Result = store.AuthBadProtocol
1623 c.log.Infox("bad scram protocol message", err, slog.String("username", username), slog.Any("remote", c.remoteIP))
1624 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7Other0, "bad scram protocol message")
1625 }
1626 xcheckf(err, "server final")
1627 }
1628
1629 // Client must still respond, but there is nothing to say. See ../rfc/9051:6221
1630 // The message should be empty. todo: should we require it is empty?
1631 xreadContinuation()
1632
1633 case "EXTERNAL":
1634 la.AuthMech = "external"
1635
1636 // ../rfc/4422:1618
1637 buf := xreadInitial("")
1638 username = norm.NFC.String(string(buf))
1639 la.LoginAddress = username
1640
1641 if !c.tls {
1642 // ../rfc/4954:630
1643 xsmtpUserErrorf(smtp.C538EncReqForAuth, smtp.SePol7EncReqForAuth11, "tls required for tls client certificate authentication")
1644 }
1645 if c.account == nil {
1646 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "missing client certificate, required for tls client certificate authentication")
1647 }
1648
1649 if username == "" {
1650 username = c.username
1651 la.LoginAddress = username
1652 }
1653 var err error
1654 account, la.AccountName, _, err = store.OpenEmail(c.log, username, false)
1655 xcheckf(err, "looking up username from tls client authentication")
1656
1657 default:
1658 la.AuthMech = "(unrecognized)"
1659 // ../rfc/4954:176
1660 xsmtpUserErrorf(smtp.C504ParamNotImpl, smtp.SeProto5BadParams4, "mechanism %s not supported", mech)
1661 }
1662
1663 if accConf, ok := account.Conf(); !ok {
1664 xcheckf(errors.New("cannot find account"), "get account config")
1665 } else if accConf.LoginDisabled != "" {
1666 la.Result = store.AuthLoginDisabled
1667 c.log.Info("account login disabled", slog.String("username", username))
1668 xsmtpUserErrorf(smtp.C525AccountDisabled, smtp.SePol7AccountDisabled13, "%w: %s", store.ErrLoginDisabled, accConf.LoginDisabled)
1669 }
1670
1671 // We may already have TLS credentials. We allow an additional SASL authentication,
1672 // possibly with different username, but the account must be the same.
1673 if c.account != nil {
1674 if account != c.account {
1675 c.log.Debug("sasl authentication for different account than tls client authentication, aborting connection",
1676 slog.String("saslmechanism", la.AuthMech),
1677 slog.String("saslaccount", account.Name),
1678 slog.String("tlsaccount", c.account.Name),
1679 slog.String("saslusername", username),
1680 slog.String("tlsusername", c.username),
1681 )
1682 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "authentication failed, tls client certificate public key belongs to another account")
1683 } else if username != c.username {
1684 c.log.Debug("sasl authentication for different username than tls client certificate authentication, switching to sasl username",
1685 slog.String("saslmechanism", la.AuthMech),
1686 slog.String("saslusername", username),
1687 slog.String("tlsusername", c.username),
1688 slog.String("account", c.account.Name),
1689 )
1690 }
1691 } else {
1692 c.account = account
1693 account = nil // Prevent cleanup.
1694 }
1695 c.username = username
1696
1697 la.LoginAddress = c.username
1698 la.AccountName = c.account.Name
1699 la.Result = store.AuthSuccess
1700 c.authSASL = true
1701 c.authFailed = 0
1702 c.setSlow(false)
1703 // ../rfc/4954:276
1704 c.xwritecodeline(smtp.C235AuthSuccess, smtp.SePol7Other0, "nice", nil)
1705}
1706
1707// ../rfc/5321:1879 ../rfc/5321:1025
1708func (c *conn) cmdMail(p *parser) {
1709 // requirements for maximum line length:
1710 // ../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)
1711 // todo future: enforce? doesn't really seem worth it...
1712
1713 if c.transactionBad > 10 && c.transactionGood == 0 {
1714 // If we get many bad transactions, it's probably a spammer that is guessing user names.
1715 // Useful in combination with rate limiting.
1716 // ../rfc/5321:4349
1717 c.xwritecodeline(smtp.C550MailboxUnavail, smtp.SeAddr1Other0, "too many failures", nil)
1718 panic(errIO)
1719 }
1720
1721 c.xneedHello()
1722 c.xcheckAuth()
1723 if c.mailFrom != nil {
1724 // ../rfc/5321:2507, though ../rfc/5321:1029 contradicts, implying a MAIL would also reset, but ../rfc/5321:1160 decides.
1725 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "already have MAIL")
1726 }
1727 // Ensure clear transaction state on failure.
1728 defer func() {
1729 x := recover()
1730 if x != nil {
1731 // ../rfc/5321:2514
1732 c.rset()
1733 panic(x)
1734 }
1735 }()
1736 p.xtake(" FROM:")
1737 // note: no space allowed after colon. ../rfc/5321:1093
1738 // Microsoft Outlook 365 Apps for Enterprise sends it with submission. For delivery
1739 // it is mostly used by spammers, but has been seen with legitimate senders too.
1740 if !mox.Pedantic {
1741 p.space()
1742 }
1743 rawRevPath := p.xrawReversePath()
1744 paramSeen := map[string]bool{}
1745 for p.space() {
1746 // ../rfc/5321:2273
1747 key := p.xparamKeyword()
1748
1749 K := strings.ToUpper(key)
1750 if paramSeen[K] {
1751 // e.g. ../rfc/6152:128
1752 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5BadParams4, "duplicate param %q", key)
1753 }
1754 paramSeen[K] = true
1755
1756 switch K {
1757 case "SIZE":
1758 p.xtake("=")
1759 size := p.xnumber(20, true) // ../rfc/1870:90
1760 if size > c.maxMessageSize {
1761 // ../rfc/1870:136 ../rfc/3463:382
1762 ecode := smtp.SeSys3MsgLimitExceeded4
1763 if size < config.DefaultMaxMsgSize {
1764 ecode = smtp.SeMailbox2MsgLimitExceeded3
1765 }
1766 xsmtpUserErrorf(smtp.C552MailboxFull, ecode, "message too large")
1767 }
1768 // We won't verify the message is exactly the size the remote claims. Buf if it is
1769 // larger, we'll abort the transaction when remote crosses the boundary.
1770 case "BODY":
1771 p.xtake("=")
1772 // ../rfc/6152:90
1773 v := p.xparamValue()
1774 switch strings.ToUpper(v) {
1775 case "7BIT":
1776 c.has8bitmime = false
1777 case "8BITMIME":
1778 c.has8bitmime = true
1779 default:
1780 xsmtpUserErrorf(smtp.C555UnrecognizedAddrParams, smtp.SeProto5BadParams4, "unrecognized parameter %q", key)
1781 }
1782 case "AUTH":
1783 // ../rfc/4954:455
1784
1785 // We act as if we don't trust the client to specify a mailbox. Instead, we always
1786 // check the rfc5321.mailfrom and rfc5322.from before accepting the submission.
1787 // ../rfc/4954:538
1788
1789 // ../rfc/4954:704
1790 // todo future: should we accept utf-8-addr-xtext if there is no smtputf8, and utf-8 if there is? need to find a spec ../rfc/6533:259
1791 p.xtake("=")
1792 if p.take("<") {
1793 p.xtake(">")
1794 } else {
1795 p.xtext()
1796 }
1797 case "SMTPUTF8":
1798 // ../rfc/6531:213
1799 c.smtputf8 = true
1800 c.msgsmtputf8 = true
1801 case "REQUIRETLS":
1802 // ../rfc/8689:155
1803 if !c.tls {
1804 xsmtpUserErrorf(smtp.C523EncryptionNeeded, smtp.SePol7EncNeeded10, "requiretls only allowed on tls-encrypted connections")
1805 } else if !c.extRequireTLS {
1806 xsmtpUserErrorf(smtp.C555UnrecognizedAddrParams, smtp.SeSys3NotSupported3, "REQUIRETLS not allowed for this connection")
1807 }
1808 v := true
1809 c.requireTLS = &v
1810 case "HOLDFOR", "HOLDUNTIL":
1811 // Only for submission ../rfc/4865:163
1812 if !c.submission {
1813 xsmtpUserErrorf(smtp.C555UnrecognizedAddrParams, smtp.SeSys3NotSupported3, "unrecognized parameter %q", key)
1814 }
1815 if K == "HOLDFOR" && paramSeen["HOLDUNTIL"] || K == "HOLDUNTIL" && paramSeen["HOLDFOR"] {
1816 // ../rfc/4865:260
1817 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5BadParams4, "cannot use both HOLDUNTIL and HOLFOR")
1818 }
1819 p.xtake("=")
1820 // ../rfc/4865:263 ../rfc/4865:267 We are not following the advice of treating
1821 // semantic errors as syntax errors
1822 if K == "HOLDFOR" {
1823 n := p.xnumber(9, false) // ../rfc/4865:92
1824 if n > int64(queue.FutureReleaseIntervalMax/time.Second) {
1825 // ../rfc/4865:250
1826 xsmtpUserErrorf(smtp.C554TransactionFailed, smtp.SeProto5BadParams4, "future release interval too far in the future")
1827 }
1828 c.futureRelease = time.Now().Add(time.Duration(n) * time.Second)
1829 c.futureReleaseRequest = fmt.Sprintf("for;%d", n)
1830 } else {
1831 t, s := p.xdatetimeutc()
1832 ival := time.Until(t)
1833 if ival <= 0 {
1834 // Likely a mistake by the user.
1835 xsmtpUserErrorf(smtp.C554TransactionFailed, smtp.SeProto5BadParams4, "requested future release time is in the past")
1836 } else if ival > queue.FutureReleaseIntervalMax {
1837 // ../rfc/4865:255
1838 xsmtpUserErrorf(smtp.C554TransactionFailed, smtp.SeProto5BadParams4, "requested future release time is too far in the future")
1839 }
1840 c.futureRelease = t
1841 c.futureReleaseRequest = "until;" + s
1842 }
1843 default:
1844 // ../rfc/5321:2230
1845 xsmtpUserErrorf(smtp.C555UnrecognizedAddrParams, smtp.SeSys3NotSupported3, "unrecognized parameter %q", key)
1846 }
1847 }
1848
1849 // We now know if we have to parse the address with support for utf8.
1850 pp := newParser(rawRevPath, c.smtputf8, c)
1851 rpath := pp.xbareReversePath()
1852 pp.xempty()
1853 pp = nil
1854 p.xend()
1855
1856 // For submission, check if reverse path is allowed. I.e. authenticated account
1857 // must have the rpath configured. We do a check again on rfc5322.from during DATA.
1858 // Mail clients may use the alias address as smtp mail from address, so we allow it
1859 // for such aliases.
1860 rpathAllowed := func(disabled *bool) bool {
1861 // ../rfc/6409:349
1862 if rpath.IsZero() {
1863 return true
1864 }
1865
1866 from := smtp.NewAddress(rpath.Localpart, rpath.IPDomain.Domain)
1867 ok, dis := mox.AllowMsgFrom(c.account.Name, from)
1868 *disabled = dis
1869 return ok
1870 }
1871
1872 if !c.submission && !rpath.IPDomain.Domain.IsZero() {
1873 // If rpath domain has null MX record or is otherwise not accepting email, reject.
1874 // ../rfc/7505:181
1875 // ../rfc/5321:4045
1876 cidctx := context.WithValue(mox.Context, mlog.CidKey, c.cid)
1877 ctx, cancel := context.WithTimeout(cidctx, time.Minute)
1878 valid, err := checkMXRecords(ctx, c.resolver, rpath.IPDomain.Domain)
1879 cancel()
1880 if err != nil {
1881 c.log.Infox("temporary reject for temporary mx lookup error", err)
1882 xsmtpServerErrorf(codes{smtp.C451LocalErr, smtp.SeNet4Other0}, "cannot verify mx records for mailfrom domain")
1883 } else if !valid && !(Localserve && rpath.IPDomain.Domain.ASCII == "localhost") {
1884 // We don't reject for "localhost" in Localserve mode because we only resolve
1885 // through DNS, not an /etc/hosts file, and localhost may not resolve through DNS,
1886 // depending on network environment.
1887
1888 c.log.Info("permanent reject because mailfrom domain does not accept mail")
1889 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SePol7SenderHasNullMX27, "mailfrom domain not configured for mail")
1890 }
1891 }
1892
1893 var disabled bool
1894 if c.submission && (len(rpath.IPDomain.IP) > 0 || !rpathAllowed(&disabled)) {
1895 if disabled {
1896 c.log.Info("submission with smtp mail from of disabled domain", slog.Any("domain", rpath.IPDomain.Domain))
1897 xsmtpServerErrorf(codes{smtp.C451LocalErr, smtp.SeSys3Other0}, "domain of smtp mail from is temporarily disabled")
1898 }
1899
1900 // ../rfc/6409:522
1901 c.log.Info("submission with unconfigured mailfrom", slog.String("user", c.username), slog.String("mailfrom", rpath.String()))
1902 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SePol7DeliveryUnauth1, "must match authenticated user")
1903 } else if !c.submission && len(rpath.IPDomain.IP) > 0 {
1904 // todo future: allow if the IP is the same as this connection is coming from? does later code allow this?
1905 c.log.Info("delivery from address without domain", slog.String("mailfrom", rpath.String()))
1906 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SePol7Other0, "domain name required")
1907 }
1908
1909 if Localserve && strings.HasPrefix(string(rpath.Localpart), "mailfrom") {
1910 c.xlocalserveError(rpath.Localpart)
1911 }
1912
1913 c.mailFrom = &rpath
1914
1915 c.xbwritecodeline(smtp.C250Completed, smtp.SeAddr1Other0, "looking good", nil)
1916}
1917
1918// ../rfc/5321:1916 ../rfc/5321:1054
1919func (c *conn) cmdRcpt(p *parser) {
1920 c.xneedHello()
1921 c.xcheckAuth()
1922 if c.mailFrom == nil {
1923 // ../rfc/5321:1088
1924 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "missing MAIL FROM")
1925 }
1926
1927 // ../rfc/5321:1985
1928 p.xtake(" TO:")
1929 // note: no space allowed after colon. ../rfc/5321:1093
1930 // Microsoft Outlook 365 Apps for Enterprise sends it with submission. For delivery
1931 // it is mostly used by spammers, but has been seen with legitimate senders too.
1932 if !mox.Pedantic {
1933 p.space()
1934 }
1935 var fpath smtp.Path
1936 if p.take("<POSTMASTER>") {
1937 fpath = smtp.Path{Localpart: "postmaster"}
1938 } else {
1939 fpath = p.xforwardPath()
1940 }
1941 for p.space() {
1942 // ../rfc/5321:2275
1943 key := p.xparamKeyword()
1944 // K := strings.ToUpper(key)
1945 // todo future: DSN, ../rfc/3461, with "NOTIFY"
1946 // ../rfc/5321:2230
1947 xsmtpUserErrorf(smtp.C555UnrecognizedAddrParams, smtp.SeSys3NotSupported3, "unrecognized parameter %q", key)
1948 }
1949 p.xend()
1950
1951 // Check if TLS is enabled if required. It's not great that sender/recipient
1952 // addresses may have been exposed in plaintext before we can reject delivery. The
1953 // recipient could be the tls reporting addresses, which must always be able to
1954 // receive in plain text.
1955 c.xneedTLSForDelivery(fpath)
1956
1957 // todo future: for submission, should we do explicit verification that domains are fully qualified? also for mail from. ../rfc/6409:420
1958
1959 if len(c.recipients) >= rcptToLimit {
1960 // ../rfc/5321:3535 ../rfc/5321:3571
1961 xsmtpUserErrorf(smtp.C452StorageFull, smtp.SeProto5TooManyRcpts3, "max of %d recipients reached", rcptToLimit)
1962 }
1963
1964 // We don't want to allow delivery to multiple recipients with a null reverse path.
1965 // Why would anyone send like that? Null reverse path is intended for delivery
1966 // notifications, they should go to a single recipient.
1967 if !c.submission && len(c.recipients) > 0 && c.mailFrom.IsZero() {
1968 xsmtpUserErrorf(smtp.C452StorageFull, smtp.SeProto5TooManyRcpts3, "only one recipient allowed with null reverse address")
1969 }
1970
1971 // Do not accept multiple recipients if remote does not pass SPF. Because we don't
1972 // want to generate DSNs to unverified domains. This is the moment we
1973 // can refuse individual recipients, DATA will be too late. Because mail
1974 // servers must handle a max recipient limit gracefully and still send to the
1975 // recipients that are accepted, this should not cause problems. Though we are in
1976 // violation because the limit must be >= 100.
1977 // ../rfc/5321:3598
1978 // ../rfc/5321:4045
1979 // Also see ../rfc/7489:2214
1980 if !c.submission && len(c.recipients) == 1 && !Localserve {
1981 // note: because of check above, mailFrom cannot be the null address.
1982 var pass bool
1983 d := c.mailFrom.IPDomain.Domain
1984 if !d.IsZero() {
1985 // todo: use this spf result for DATA.
1986 spfArgs := spf.Args{
1987 RemoteIP: c.remoteIP,
1988 MailFromLocalpart: c.mailFrom.Localpart,
1989 MailFromDomain: d,
1990 HelloDomain: c.hello,
1991 LocalIP: c.localIP,
1992 LocalHostname: c.hostname,
1993 }
1994 cidctx := context.WithValue(mox.Context, mlog.CidKey, c.cid)
1995 spfctx, spfcancel := context.WithTimeout(cidctx, time.Minute)
1996 defer spfcancel()
1997 receivedSPF, _, _, _, err := spf.Verify(spfctx, c.log.Logger, c.resolver, spfArgs)
1998 spfcancel()
1999 if err != nil {
2000 c.log.Errorx("spf verify for multiple recipients", err)
2001 }
2002 pass = receivedSPF.Identity == spf.ReceivedMailFrom && receivedSPF.Result == spf.StatusPass
2003 }
2004 if !pass {
2005 xsmtpUserErrorf(smtp.C452StorageFull, smtp.SeProto5TooManyRcpts3, "only one recipient allowed without spf pass")
2006 }
2007 }
2008
2009 if Localserve && strings.HasPrefix(string(fpath.Localpart), "rcptto") {
2010 c.xlocalserveError(fpath.Localpart)
2011 }
2012
2013 if len(fpath.IPDomain.IP) > 0 {
2014 if !c.submission {
2015 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeAddr1UnknownDestMailbox1, "not accepting email for ip")
2016 }
2017 c.recipients = append(c.recipients, recipient{fpath, nil, nil})
2018 } else if accountName, alias, canonical, dest, err := mox.LookupAddress(fpath.Localpart, fpath.IPDomain.Domain, true, true, true); err == nil {
2019 // note: a bare postmaster, without domain, is handled by LookupAddress. ../rfc/5321:735
2020 if alias != nil {
2021 c.recipients = append(c.recipients, recipient{fpath, nil, &rcptAlias{*alias, canonical}})
2022 } else if dest.SMTPError != "" {
2023 xsmtpServerErrorf(codes{dest.SMTPErrorCode, dest.SMTPErrorSecode}, "%s", dest.SMTPErrorMsg)
2024 } else {
2025 c.recipients = append(c.recipients, recipient{fpath, &rcptAccount{accountName, dest, canonical}, nil})
2026 }
2027
2028 } else if Localserve {
2029 // If the address isn't known, and we are in localserve, deliver to the mox user.
2030 // If account or destination doesn't exist, it will be handled during delivery. For
2031 // submissions, which is the common case, we'll deliver to the logged in user,
2032 // which is typically the mox user.
2033 acc, _ := mox.Conf.Account("mox")
2034 dest := acc.Destinations["mox@localhost"]
2035 c.recipients = append(c.recipients, recipient{fpath, &rcptAccount{"mox", dest, "mox@localhost"}, nil})
2036 } else if errors.Is(err, mox.ErrDomainDisabled) {
2037 c.log.Info("smtp recipient for temporarily disabled domain", slog.Any("domain", fpath.IPDomain.Domain))
2038 xsmtpUserErrorf(smtp.C450MailboxUnavail, smtp.SeMailbox2Disabled1, "recipient domain temporarily disabled")
2039 } else if errors.Is(err, mox.ErrDomainNotFound) {
2040 if !c.submission {
2041 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeAddr1UnknownDestMailbox1, "not accepting email for domain")
2042 }
2043 // We'll be delivering this email.
2044 c.recipients = append(c.recipients, recipient{fpath, nil, nil})
2045 } else if errors.Is(err, mox.ErrAddressNotFound) {
2046 if c.submission {
2047 // For submission, we're transparent about which user exists. Should be fine for the typical small-scale deploy.
2048 // ../rfc/5321:1071
2049 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeAddr1UnknownDestMailbox1, "no such user")
2050 }
2051 // We pretend to accept. We don't want to let remote know the user does not exist
2052 // until after DATA. Because then remote has committed to sending a message.
2053 // note: not local for !c.submission is the signal this address is in error.
2054 c.recipients = append(c.recipients, recipient{fpath, nil, nil})
2055 } else {
2056 c.log.Errorx("looking up account for delivery", err, slog.Any("rcptto", fpath))
2057 xsmtpServerErrorf(codes{smtp.C451LocalErr, smtp.SeSys3Other0}, "error processing")
2058 }
2059 c.xbwritecodeline(smtp.C250Completed, smtp.SeAddr1Other0, "now on the list", nil)
2060}
2061
2062func hasNonASCII(s string) bool {
2063 for _, c := range []byte(s) {
2064 if c > unicode.MaxASCII {
2065 return true
2066 }
2067 }
2068 return false
2069}
2070
2071// ../rfc/6531:497
2072func (c *conn) isSMTPUTF8Required(part *message.Part) bool {
2073 // Check "MAIL FROM".
2074 if hasNonASCII(string(c.mailFrom.Localpart)) {
2075 return true
2076 }
2077 // Check all "RCPT TO".
2078 for _, rcpt := range c.recipients {
2079 if hasNonASCII(string(rcpt.Addr.Localpart)) {
2080 return true
2081 }
2082 }
2083
2084 // Check header in all message parts.
2085 smtputf8, err := part.NeedsSMTPUTF8()
2086 xcheckf(err, "checking if smtputf8 is required")
2087 return smtputf8
2088}
2089
2090// ../rfc/5321:1992 ../rfc/5321:1098
2091func (c *conn) cmdData(p *parser) {
2092 c.xneedHello()
2093 c.xcheckAuth()
2094 if c.mailFrom == nil {
2095 // ../rfc/5321:1130
2096 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "missing MAIL FROM")
2097 }
2098 if len(c.recipients) == 0 {
2099 // ../rfc/5321:1130
2100 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "missing RCPT TO")
2101 }
2102
2103 // ../rfc/5321:2066
2104 p.xend()
2105
2106 // 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.
2107
2108 // Entire delivery should be done within 30 minutes, or we abort.
2109 cidctx := context.WithValue(mox.Context, mlog.CidKey, c.cid)
2110 cmdctx, cmdcancel := context.WithTimeout(cidctx, 30*time.Minute)
2111 defer cmdcancel()
2112 // Deadline is taken into account by Read and Write.
2113 c.deadline, _ = cmdctx.Deadline()
2114 defer func() {
2115 c.deadline = time.Time{}
2116 }()
2117
2118 // ../rfc/5321:1994
2119 c.xwritelinef("354 see you at the bare dot")
2120
2121 // Mark as tracedata.
2122 defer c.xtrace(mlog.LevelTracedata)()
2123
2124 // We read the data into a temporary file. We limit the size and do basic analysis while reading.
2125 dataFile, err := store.CreateMessageTemp(c.log, "smtp-deliver")
2126 if err != nil {
2127 xsmtpServerErrorf(errCodes(smtp.C451LocalErr, smtp.SeSys3Other0, err), "creating temporary file for message: %s", err)
2128 }
2129 defer store.CloseRemoveTempFile(c.log, dataFile, "smtpserver delivered message")
2130 msgWriter := message.NewWriter(dataFile)
2131 dr := smtp.NewDataReader(c.xbr)
2132 n, err := io.Copy(&limitWriter{maxSize: c.maxMessageSize, w: msgWriter}, dr)
2133 c.xtrace(mlog.LevelTrace) // Restore.
2134 if err != nil {
2135 if errors.Is(err, errMessageTooLarge) {
2136 // ../rfc/1870:136 and ../rfc/3463:382
2137 ecode := smtp.SeSys3MsgLimitExceeded4
2138 if n < config.DefaultMaxMsgSize {
2139 ecode = smtp.SeMailbox2MsgLimitExceeded3
2140 }
2141 c.xwritecodeline(smtp.C451LocalErr, ecode, fmt.Sprintf("error copying data to file (%s)", mox.ReceivedID(c.cid)), err)
2142 panic(fmt.Errorf("remote sent too much DATA: %w", errIO))
2143 }
2144
2145 if errors.Is(err, smtp.ErrCRLF) {
2146 c.xwritecodeline(smtp.C500BadSyntax, smtp.SeProto5Syntax2, fmt.Sprintf("invalid bare \\r or \\n, may be smtp smuggling (%s)", mox.ReceivedID(c.cid)), err)
2147 return
2148 }
2149
2150 // Something is failing on our side. We want to let remote know. So write an error response,
2151 // then discard the remaining data so the remote client is more likely to see our
2152 // response. Our write is synchronous, there is a risk no window/buffer space is
2153 // available and our write blocks us from reading remaining data, leading to
2154 // deadlock. We have a timeout on our connection writes though, so worst case we'll
2155 // abort the connection due to expiration.
2156 c.xwritecodeline(smtp.C451LocalErr, smtp.SeSys3Other0, fmt.Sprintf("error copying data to file (%s)", mox.ReceivedID(c.cid)), err)
2157 io.Copy(io.Discard, dr)
2158 return
2159 }
2160
2161 // Basic sanity checks on messages before we send them out to the world. Just
2162 // trying to be strict in what we do to others and liberal in what we accept.
2163 if c.submission {
2164 if !msgWriter.HaveBody {
2165 // ../rfc/6409:541
2166 xsmtpUserErrorf(smtp.C554TransactionFailed, smtp.SeMsg6Other0, "message requires both header and body section")
2167 }
2168 // Check only for pedantic mode because ios mail will attempt to send smtputf8 with
2169 // non-ascii in message from localpart without using 8bitmime.
2170 if mox.Pedantic && msgWriter.Has8bit && !c.has8bitmime {
2171 // ../rfc/5321:906
2172 xsmtpUserErrorf(smtp.C500BadSyntax, smtp.SeMsg6Other0, "message with non-us-ascii requires 8bitmime extension")
2173 }
2174 }
2175
2176 if Localserve && mox.Pedantic {
2177 // Require that message can be parsed fully.
2178 p, err := message.Parse(c.log.Logger, false, dataFile)
2179 if err == nil {
2180 err = p.Walk(c.log.Logger, nil)
2181 }
2182 if err != nil {
2183 // ../rfc/6409:541
2184 xsmtpUserErrorf(smtp.C554TransactionFailed, smtp.SeMsg6Other0, "malformed message: %v", err)
2185 }
2186 }
2187
2188 // Now that we have all the whole message (envelope + data), we can check if the SMTPUTF8 extension is required.
2189 var part *message.Part
2190 if c.smtputf8 || c.submission || mox.Pedantic {
2191 // Try to parse the message.
2192 // Do nothing if something bad happen during Parse and Walk, just keep the current value for c.msgsmtputf8.
2193 p, err := message.Parse(c.log.Logger, true, dataFile)
2194 if err == nil {
2195 // Message parsed without error. Keep the result to avoid parsing the message again.
2196 part = &p
2197 err = part.Walk(c.log.Logger, nil)
2198 if err == nil {
2199 c.msgsmtputf8 = c.isSMTPUTF8Required(part)
2200 }
2201 }
2202 if err != nil {
2203 c.log.Debugx("parsing message for smtputf8 check", err)
2204 }
2205 if c.smtputf8 != c.msgsmtputf8 {
2206 c.log.Debug("smtputf8 flag changed", slog.Bool("smtputf8", c.smtputf8), slog.Bool("msgsmtputf8", c.msgsmtputf8))
2207 }
2208 }
2209 if !c.smtputf8 && c.msgsmtputf8 && mox.Pedantic {
2210 metricSubmission.WithLabelValues("missingsmtputf8").Inc()
2211 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeMsg6Other0, "smtputf8 extension is required but was not added to the MAIL command")
2212 }
2213
2214 // Prepare "Received" header.
2215 // ../rfc/5321:2051 ../rfc/5321:3302
2216 // ../rfc/5321:3311 ../rfc/6531:578
2217 var recvFrom string
2218 var iprevStatus iprev.Status // Only for delivery, not submission.
2219 var iprevAuthentic bool
2220 if c.submission {
2221 // Hide internal hosts.
2222 // todo future: make this a config option, where admins specify ip ranges that they don't want exposed. also see ../rfc/5321:4321
2223 recvFrom = message.HeaderCommentDomain(mox.Conf.Static.HostnameDomain, c.msgsmtputf8)
2224 } else {
2225 if len(c.hello.IP) > 0 {
2226 recvFrom = smtp.AddressLiteral(c.hello.IP)
2227 } else {
2228 // ASCII-only version added after the extended-domain syntax below, because the
2229 // comment belongs to "BY" which comes immediately after "FROM".
2230 recvFrom = c.hello.Domain.XName(c.msgsmtputf8)
2231 }
2232 iprevctx, iprevcancel := context.WithTimeout(cmdctx, time.Minute)
2233 var revName string
2234 var revNames []string
2235 iprevStatus, revName, revNames, iprevAuthentic, err = iprev.Lookup(iprevctx, c.resolver, c.remoteIP)
2236 iprevcancel()
2237 if err != nil {
2238 c.log.Infox("reverse-forward lookup", err, slog.Any("remoteip", c.remoteIP))
2239 }
2240 c.log.Debug("dns iprev check", slog.Any("addr", c.remoteIP), slog.Any("status", iprevStatus))
2241 var name string
2242 if revName != "" {
2243 name = revName
2244 } else if len(revNames) > 0 {
2245 name = revNames[0]
2246 }
2247 name = strings.TrimSuffix(name, ".")
2248 recvFrom += " ("
2249 if name != "" && name != c.hello.Domain.XName(c.msgsmtputf8) {
2250 recvFrom += name + " "
2251 }
2252 recvFrom += smtp.AddressLiteral(c.remoteIP) + ")"
2253 if c.msgsmtputf8 && c.hello.Domain.Unicode != "" {
2254 recvFrom += " (" + c.hello.Domain.ASCII + ")"
2255 }
2256 }
2257 recvBy := mox.Conf.Static.HostnameDomain.XName(c.msgsmtputf8)
2258 recvBy += " (" + smtp.AddressLiteral(c.localIP) + ")" // todo: hide ip if internal?
2259 if c.msgsmtputf8 && mox.Conf.Static.HostnameDomain.Unicode != "" {
2260 // This syntax is part of "VIA".
2261 recvBy += " (" + mox.Conf.Static.HostnameDomain.ASCII + ")"
2262 }
2263
2264 // ../rfc/3848:34 ../rfc/6531:791
2265 with := "SMTP"
2266 if c.msgsmtputf8 {
2267 with = "UTF8SMTP"
2268 } else if c.ehlo {
2269 with = "ESMTP"
2270 }
2271 if c.tls {
2272 with += "S"
2273 }
2274 if c.account != nil {
2275 // ../rfc/4954:660
2276 with += "A"
2277 }
2278
2279 // Assume transaction does not succeed. If it does, we'll compensate.
2280 c.transactionBad++
2281
2282 recvHdrFor := func(rcptTo string) string {
2283 recvHdr := &message.HeaderWriter{}
2284 // For additional Received-header clauses, see:
2285 // https://www.iana.org/assignments/mail-parameters/mail-parameters.xhtml#table-mail-parameters-8
2286 withComment := ""
2287 if c.requireTLS != nil && *c.requireTLS {
2288 // Comment is actually part of ID ABNF rule. ../rfc/5321:3336
2289 withComment = " (requiretls)"
2290 }
2291 recvHdr.Add(" ", "Received:", "from", recvFrom, "by", recvBy, "via", "tcp", "with", with+withComment, "id", mox.ReceivedID(c.cid)) // ../rfc/5321:3158
2292 if c.tls {
2293 tlsConn := c.conn.(*tls.Conn)
2294 tlsComment := mox.TLSReceivedComment(c.log, tlsConn.ConnectionState())
2295 recvHdr.Add(" ", tlsComment...)
2296 }
2297 // We leave out an empty "for" clause. This is empty for messages submitted to
2298 // multiple recipients, so the message stays identical and a single smtp
2299 // transaction can deliver, only transferring the data once.
2300 if rcptTo != "" {
2301 recvHdr.Add(" ", "for", "<"+rcptTo+">;")
2302 }
2303 recvHdr.Add(" ", time.Now().Format(message.RFC5322Z))
2304 return recvHdr.String()
2305 }
2306
2307 // Submission is easiest because user is trusted. Far fewer checks to make. So
2308 // handle it first, and leave the rest of the function for handling wild west
2309 // internet traffic.
2310 if c.submission {
2311 c.submit(cmdctx, recvHdrFor, msgWriter, dataFile, part)
2312 } else {
2313 c.deliver(cmdctx, recvHdrFor, msgWriter, iprevStatus, iprevAuthentic, dataFile)
2314 }
2315}
2316
2317// Check if a message has unambiguous "TLS-Required: No" header. Messages must not
2318// contain multiple TLS-Required headers. The only valid value is "no". But we'll
2319// accept multiple headers as long as all they are all "no".
2320// ../rfc/8689:223
2321func hasTLSRequiredNo(h textproto.MIMEHeader) bool {
2322 l := h.Values("Tls-Required")
2323 if len(l) == 0 {
2324 return false
2325 }
2326 for _, v := range l {
2327 if !strings.EqualFold(v, "no") {
2328 return false
2329 }
2330 }
2331 return true
2332}
2333
2334// submit is used for mail from authenticated users that we will try to deliver.
2335func (c *conn) submit(ctx context.Context, recvHdrFor func(string) string, msgWriter *message.Writer, dataFile *os.File, part *message.Part) {
2336 // Similar between ../smtpserver/server.go:/submit\( and ../webmail/api.go:/MessageSubmit\( and ../webapisrv/server.go:/Send\(
2337
2338 var msgPrefix []byte
2339
2340 // Check that user is only sending email as one of its configured identities. Not
2341 // for other users.
2342 // We don't check the Sender field, there is no expectation of verification, ../rfc/7489:2948
2343 // and with Resent headers it seems valid to have someone else as Sender. ../rfc/5322:1578
2344 msgFrom, _, header, err := message.From(c.log.Logger, true, dataFile, part)
2345 if err != nil {
2346 metricSubmission.WithLabelValues("badmessage").Inc()
2347 c.log.Infox("parsing message From address", err, slog.String("user", c.username))
2348 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeMsg6Other0, "cannot parse header or From address: %v", err)
2349 }
2350 if ok, disabled := mox.AllowMsgFrom(c.account.Name, msgFrom); disabled {
2351 c.log.Info("submission with message from address of disabled domain", slog.Any("domain", msgFrom.Domain))
2352 xsmtpServerErrorf(codes{smtp.C451LocalErr, smtp.SeSys3Other0}, "domain of message from header is temporarily disabled")
2353 } else if !ok {
2354 // ../rfc/6409:522
2355 metricSubmission.WithLabelValues("badfrom").Inc()
2356 c.log.Infox("verifying message from address", mox.ErrAddressNotFound, slog.String("user", c.username), slog.Any("msgfrom", msgFrom))
2357 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SePol7DeliveryUnauth1, "message from address must belong to authenticated user")
2358 }
2359
2360 // TLS-Required: No header makes us not enforce recipient domain's TLS policy.
2361 // ../rfc/8689:206
2362 // Only when requiretls smtp extension wasn't used. ../rfc/8689:246
2363 if c.requireTLS == nil && hasTLSRequiredNo(header) {
2364 v := false
2365 c.requireTLS = &v
2366 }
2367
2368 // Outgoing messages should not have a Return-Path header. The final receiving mail
2369 // server will add it.
2370 // ../rfc/5321:3233
2371 if mox.Pedantic && header.Values("Return-Path") != nil {
2372 metricSubmission.WithLabelValues("badheader").Inc()
2373 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeMsg6Other0, "message should not have Return-Path header")
2374 }
2375
2376 // Add Message-Id header if missing.
2377 // ../rfc/5321:4131 ../rfc/6409:751
2378 messageID := header.Get("Message-Id")
2379 if messageID == "" {
2380 messageID = mox.MessageIDGen(c.msgsmtputf8)
2381 msgPrefix = append(msgPrefix, fmt.Sprintf("Message-Id: <%s>\r\n", messageID)...)
2382 }
2383
2384 // ../rfc/6409:745
2385 if header.Get("Date") == "" {
2386 msgPrefix = append(msgPrefix, "Date: "+time.Now().Format(message.RFC5322Z)+"\r\n"...)
2387 }
2388
2389 // Check outgoing message rate limit.
2390 err = c.account.DB.Read(ctx, func(tx *bstore.Tx) error {
2391 rcpts := make([]smtp.Path, len(c.recipients))
2392 for i, r := range c.recipients {
2393 rcpts[i] = r.Addr
2394 }
2395 msglimit, rcptlimit, err := c.account.SendLimitReached(tx, rcpts)
2396 xcheckf(err, "checking sender limit")
2397 if msglimit >= 0 {
2398 metricSubmission.WithLabelValues("messagelimiterror").Inc()
2399 xsmtpUserErrorf(smtp.C451LocalErr, smtp.SePol7DeliveryUnauth1, "max number of messages (%d) over past 24h reached, try increasing per-account setting MaxOutgoingMessagesPerDay", msglimit)
2400 } else if rcptlimit >= 0 {
2401 metricSubmission.WithLabelValues("recipientlimiterror").Inc()
2402 xsmtpUserErrorf(smtp.C451LocalErr, smtp.SePol7DeliveryUnauth1, "max number of new/first-time recipients (%d) over past 24h reached, try increasing per-account setting MaxFirstTimeRecipientsPerDay", rcptlimit)
2403 }
2404 return nil
2405 })
2406 xcheckf(err, "read-only transaction")
2407
2408 // We gather any X-Mox-Extra-* headers into the "extra" data during queueing, which
2409 // will make it into any webhook we deliver.
2410 // todo: remove the X-Mox-Extra-* headers from the message. we don't currently rewrite the message...
2411 // todo: should we not canonicalize keys?
2412 var extra map[string]string
2413 for k, vl := range header {
2414 if !strings.HasPrefix(k, "X-Mox-Extra-") {
2415 continue
2416 }
2417 if extra == nil {
2418 extra = map[string]string{}
2419 }
2420 xk := k[len("X-Mox-Extra-"):]
2421 // We don't allow duplicate keys.
2422 if _, ok := extra[xk]; ok || len(vl) > 1 {
2423 xsmtpUserErrorf(smtp.C554TransactionFailed, smtp.SeMsg6Other0, "duplicate x-mox-extra- key %q", xk)
2424 }
2425 extra[xk] = vl[len(vl)-1]
2426 }
2427
2428 // 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.
2429
2430 // Add DKIM signatures.
2431 confDom, ok := mox.Conf.Domain(msgFrom.Domain)
2432 if !ok {
2433 c.log.Error("domain disappeared", slog.Any("domain", msgFrom.Domain))
2434 xsmtpServerErrorf(codes{smtp.C451LocalErr, smtp.SeSys3Other0}, "internal error")
2435 } else if confDom.Disabled {
2436 c.log.Info("submission with message from address of disabled domain", slog.Any("domain", msgFrom.Domain))
2437 xsmtpServerErrorf(codes{smtp.C451LocalErr, smtp.SeSys3Other0}, "domain of message from header is temporarily disabled")
2438 }
2439
2440 selectors := mox.DKIMSelectors(confDom.DKIM)
2441 if len(selectors) > 0 {
2442 canonical := mox.CanonicalLocalpart(msgFrom.Localpart, confDom)
2443 if dkimHeaders, err := dkim.Sign(ctx, c.log.Logger, canonical, msgFrom.Domain, selectors, c.msgsmtputf8, store.FileMsgReader(msgPrefix, dataFile)); err != nil {
2444 c.log.Errorx("dkim sign for domain", err, slog.Any("domain", msgFrom.Domain))
2445 metricServerErrors.WithLabelValues("dkimsign").Inc()
2446 } else {
2447 msgPrefix = append(msgPrefix, []byte(dkimHeaders)...)
2448 }
2449 }
2450
2451 authResults := message.AuthResults{
2452 Hostname: mox.Conf.Static.HostnameDomain.XName(c.msgsmtputf8),
2453 Comment: mox.Conf.Static.HostnameDomain.ASCIIExtra(c.msgsmtputf8),
2454 Methods: []message.AuthMethod{
2455 {
2456 Method: "auth",
2457 Result: "pass",
2458 Props: []message.AuthProp{
2459 message.MakeAuthProp("smtp", "mailfrom", c.mailFrom.XString(c.msgsmtputf8), true, c.mailFrom.ASCIIExtra(c.msgsmtputf8)),
2460 },
2461 },
2462 },
2463 }
2464 msgPrefix = append(msgPrefix, []byte(authResults.Header())...)
2465
2466 // We always deliver through the queue. It would be more efficient to deliver
2467 // directly for local accounts, but we don't want to circumvent all the anti-spam
2468 // measures. Accounts on a single mox instance should be allowed to block each
2469 // other.
2470
2471 accConf, _ := c.account.Conf()
2472 loginAddr, err := smtp.ParseAddress(c.username)
2473 xcheckf(err, "parsing login address")
2474 useFromID := slices.Contains(accConf.ParsedFromIDLoginAddresses, loginAddr)
2475 var localpartBase string
2476 var fromID string
2477 var genFromID bool
2478 if useFromID {
2479 // With submission, user can bring their own fromid.
2480 t := strings.SplitN(string(c.mailFrom.Localpart), confDom.LocalpartCatchallSeparatorsEffective[0], 2)
2481 localpartBase = t[0]
2482 if len(t) == 2 {
2483 fromID = t[1]
2484 if fromID != "" && len(c.recipients) > 1 {
2485 xsmtpServerErrorf(codes{smtp.C554TransactionFailed, smtp.SeProto5TooManyRcpts3}, "cannot send to multiple recipients with chosen fromid")
2486 }
2487 } else {
2488 genFromID = true
2489 }
2490 }
2491 now := time.Now()
2492 qml := make([]queue.Msg, len(c.recipients))
2493 for i, rcpt := range c.recipients {
2494 if Localserve {
2495 code, timeout := mox.LocalserveNeedsError(rcpt.Addr.Localpart)
2496 if timeout {
2497 c.log.Info("timing out submission due to special localpart")
2498 mox.Sleep(mox.Context, time.Hour)
2499 xsmtpServerErrorf(codes{smtp.C451LocalErr, smtp.SeSys3Other0}, "timing out submission due to special localpart")
2500 } else if code != 0 {
2501 c.log.Info("failure due to special localpart", slog.Int("code", code))
2502 xsmtpServerErrorf(codes{code, smtp.SeOther00}, "failure with code %d due to special localpart", code)
2503 }
2504 }
2505
2506 fp := *c.mailFrom
2507 if useFromID {
2508 if genFromID {
2509 fromID = xrandomID(16)
2510 }
2511 fp.Localpart = smtp.Localpart(localpartBase + confDom.LocalpartCatchallSeparatorsEffective[0] + fromID)
2512 }
2513
2514 // For multiple recipients, we don't make each message prefix unique, leaving out
2515 // the "for" clause in the Received header. This allows the queue to deliver the
2516 // messages in a single smtp transaction.
2517 var rcptTo string
2518 if len(c.recipients) == 1 {
2519 rcptTo = rcpt.Addr.String()
2520 }
2521 xmsgPrefix := append([]byte(recvHdrFor(rcptTo)), msgPrefix...)
2522 msgSize := int64(len(xmsgPrefix)) + msgWriter.Size
2523 qm := queue.MakeMsg(fp, rcpt.Addr, msgWriter.Has8bit, c.msgsmtputf8, msgSize, messageID, xmsgPrefix, c.requireTLS, now, header.Get("Subject"))
2524 if !c.futureRelease.IsZero() {
2525 qm.NextAttempt = c.futureRelease
2526 qm.FutureReleaseRequest = c.futureReleaseRequest
2527 }
2528 qm.FromID = fromID
2529 qm.Extra = extra
2530 qml[i] = qm
2531 }
2532
2533 // 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
2534 if err := queue.Add(ctx, c.log, c.account.Name, dataFile, qml...); err != nil && errors.Is(err, queue.ErrFromID) && !genFromID {
2535 // todo: should we return this error during the "rcpt to" command?
2536 // secode is not an exact match, but seems closest.
2537 xsmtpServerErrorf(errCodes(smtp.C554TransactionFailed, smtp.SeAddr1SenderSyntax7, err), "bad fromid in smtp mail from address: %s", err)
2538 } else if err != nil {
2539 // Aborting the transaction is not great. But continuing and generating DSNs will
2540 // probably result in errors as well...
2541 metricSubmission.WithLabelValues("queueerror").Inc()
2542 c.log.Errorx("queuing message", err)
2543 xsmtpServerErrorf(errCodes(smtp.C451LocalErr, smtp.SeSys3Other0, err), "error delivering message: %v", err)
2544 }
2545 metricSubmission.WithLabelValues("ok").Inc()
2546 for i, rcpt := range c.recipients {
2547 c.log.Info("messages queued for delivery",
2548 slog.Any("mailfrom", *c.mailFrom),
2549 slog.Any("rcptto", rcpt.Addr),
2550 slog.Bool("smtputf8", c.smtputf8),
2551 slog.Bool("msgsmtputf8", c.msgsmtputf8),
2552 slog.Int64("msgsize", qml[i].Size))
2553 }
2554
2555 err = c.account.DB.Write(ctx, func(tx *bstore.Tx) error {
2556 for _, rcpt := range c.recipients {
2557 outgoing := store.Outgoing{Recipient: rcpt.Addr.XString(true)}
2558 if err := tx.Insert(&outgoing); err != nil {
2559 return fmt.Errorf("adding outgoing message: %v", err)
2560 }
2561 }
2562 return nil
2563 })
2564 xcheckf(err, "adding outgoing messages")
2565
2566 c.transactionGood++
2567 c.transactionBad-- // Compensate for early earlier pessimistic increase.
2568
2569 c.rset()
2570 c.xwritecodeline(smtp.C250Completed, smtp.SeMailbox2Other0, "it is done", nil)
2571}
2572
2573func xrandomID(n int) string {
2574 return base64.RawURLEncoding.EncodeToString(xrandom(n))
2575}
2576
2577func xrandom(n int) []byte {
2578 buf := make([]byte, n)
2579 cryptorand.Read(buf)
2580 return buf
2581}
2582
2583func ipmasked(ip net.IP) (string, string, string) {
2584 if ip.To4() != nil {
2585 m1 := ip.String()
2586 m2 := ip.Mask(net.CIDRMask(26, 32)).String()
2587 m3 := ip.Mask(net.CIDRMask(21, 32)).String()
2588 return m1, m2, m3
2589 }
2590 m1 := ip.Mask(net.CIDRMask(64, 128)).String()
2591 m2 := ip.Mask(net.CIDRMask(48, 128)).String()
2592 m3 := ip.Mask(net.CIDRMask(32, 128)).String()
2593 return m1, m2, m3
2594}
2595
2596func (c *conn) xlocalserveError(lp smtp.Localpart) {
2597 code, timeout := mox.LocalserveNeedsError(lp)
2598 if timeout {
2599 c.log.Info("timing out due to special localpart")
2600 mox.Sleep(mox.Context, time.Hour)
2601 xsmtpServerErrorf(codes{smtp.C451LocalErr, smtp.SeSys3Other0}, "timing out command due to special localpart")
2602 } else if code != 0 {
2603 c.log.Info("failure due to special localpart", slog.Int("code", code))
2604 metricDelivery.WithLabelValues("delivererror", "localserve").Inc()
2605 xsmtpServerErrorf(codes{code, smtp.SeOther00}, "failure with code %d due to special localpart", code)
2606 }
2607}
2608
2609// deliver is called for incoming messages from external, typically untrusted
2610// sources. i.e. not submitted by authenticated users.
2611func (c *conn) deliver(ctx context.Context, recvHdrFor func(string) string, msgWriter *message.Writer, iprevStatus iprev.Status, iprevAuthentic bool, dataFile *os.File) {
2612 // 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.
2613
2614 var msgFrom smtp.Address
2615 var envelope *message.Envelope
2616 var headers textproto.MIMEHeader
2617 var isDSN bool
2618 part, err := message.Parse(c.log.Logger, false, dataFile)
2619 if err == nil {
2620 // 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?
2621 isDSN = part.MediaType == "MULTIPART" && part.MediaSubType == "REPORT" && strings.EqualFold(part.ContentTypeParams["report-type"], "delivery-status")
2622 msgFrom, envelope, headers, err = message.From(c.log.Logger, false, dataFile, &part)
2623 }
2624 if err != nil {
2625 c.log.Infox("parsing message for From address", err)
2626 }
2627
2628 // Basic loop detection. ../rfc/5321:4065 ../rfc/5321:1526
2629 if len(headers.Values("Received")) > 100 {
2630 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeNet4Loop6, "loop detected, more than 100 Received headers")
2631 }
2632
2633 // TLS-Required: No header makes us not enforce recipient domain's TLS policy.
2634 // Since we only deliver locally at the moment, this won't influence our behaviour.
2635 // Once we forward, it would our delivery attempts.
2636 // ../rfc/8689:206
2637 // Only when requiretls smtp extension wasn't used. ../rfc/8689:246
2638 if c.requireTLS == nil && hasTLSRequiredNo(headers) {
2639 v := false
2640 c.requireTLS = &v
2641 }
2642
2643 // We'll be building up an Authentication-Results header.
2644 authResults := message.AuthResults{
2645 Hostname: mox.Conf.Static.HostnameDomain.XName(c.msgsmtputf8),
2646 }
2647
2648 commentAuthentic := func(v bool) string {
2649 if v {
2650 return "with dnssec"
2651 }
2652 return "without dnssec"
2653 }
2654
2655 // Reverse IP lookup results.
2656 // todo future: how useful is this?
2657 // ../rfc/5321:2481
2658 authResults.Methods = append(authResults.Methods, message.AuthMethod{
2659 Method: "iprev",
2660 Result: string(iprevStatus),
2661 Comment: commentAuthentic(iprevAuthentic),
2662 Props: []message.AuthProp{
2663 message.MakeAuthProp("policy", "iprev", c.remoteIP.String(), false, ""),
2664 },
2665 })
2666
2667 // SPF and DKIM verification in parallel.
2668 var wg sync.WaitGroup
2669
2670 // DKIM
2671 wg.Add(1)
2672 var dkimResults []dkim.Result
2673 var dkimErr error
2674 go func() {
2675 defer func() {
2676 x := recover() // Should not happen, but don't take program down if it does.
2677 if x != nil {
2678 c.log.Error("dkim verify panic", slog.Any("err", x))
2679 debug.PrintStack()
2680 metrics.PanicInc(metrics.Dkimverify)
2681 }
2682 }()
2683 defer wg.Done()
2684 // We always evaluate all signatures. We want to build up reputation for each
2685 // domain in the signature.
2686 const ignoreTestMode = false
2687 // todo future: longer timeout? we have to read through the entire email, which can be large, possibly multiple times.
2688 dkimctx, dkimcancel := context.WithTimeout(ctx, time.Minute)
2689 defer dkimcancel()
2690 // todo future: we could let user configure which dkim headers they require
2691
2692 // For localserve, fake dkim selector DNS records for hosted domains to give
2693 // dkim-signatures a chance to pass for deliveries from queue.
2694 resolver := c.resolver
2695 if Localserve {
2696 // Lookup based on message From address is an approximation.
2697 if dc, ok := mox.Conf.Domain(msgFrom.Domain); ok && len(dc.DKIM.Selectors) > 0 {
2698 txts := map[string][]string{}
2699 for name, sel := range dc.DKIM.Selectors {
2700 dkimr := dkim.Record{
2701 Version: "DKIM1",
2702 Hashes: []string{sel.HashEffective},
2703 PublicKey: sel.Key.Public(),
2704 }
2705 if _, ok := sel.Key.(ed25519.PrivateKey); ok {
2706 dkimr.Key = "ed25519"
2707 } else if _, ok := sel.Key.(*rsa.PrivateKey); !ok {
2708 err := fmt.Errorf("unrecognized private key for DKIM selector %q: %T", name, sel.Key)
2709 xcheckf(err, "making dkim record")
2710 }
2711 txt, err := dkimr.Record()
2712 xcheckf(err, "making DKIM DNS TXT record")
2713 txts[name+"._domainkey."+msgFrom.Domain.ASCII+"."] = []string{txt}
2714 }
2715 resolver = dns.MockResolver{TXT: txts}
2716 }
2717 }
2718 dkimResults, dkimErr = dkim.Verify(dkimctx, c.log.Logger, resolver, c.msgsmtputf8, dkim.DefaultPolicy, dataFile, ignoreTestMode)
2719 dkimcancel()
2720 }()
2721
2722 // SPF.
2723 // ../rfc/7208:472
2724 var receivedSPF spf.Received
2725 var spfDomain dns.Domain
2726 var spfExpl string
2727 var spfAuthentic bool
2728 var spfErr error
2729 spfArgs := spf.Args{
2730 RemoteIP: c.remoteIP,
2731 MailFromLocalpart: c.mailFrom.Localpart,
2732 MailFromDomain: c.mailFrom.IPDomain.Domain, // Can be empty.
2733 HelloDomain: c.hello,
2734 LocalIP: c.localIP,
2735 LocalHostname: c.hostname,
2736 }
2737 wg.Add(1)
2738 go func() {
2739 defer func() {
2740 x := recover() // Should not happen, but don't take program down if it does.
2741 if x != nil {
2742 c.log.Error("spf verify panic", slog.Any("err", x))
2743 debug.PrintStack()
2744 metrics.PanicInc(metrics.Spfverify)
2745 }
2746 }()
2747 defer wg.Done()
2748 spfctx, spfcancel := context.WithTimeout(ctx, time.Minute)
2749 defer spfcancel()
2750 resolver := c.resolver
2751 // For localserve, give hosted domains a chance to pass for deliveries from queue.
2752 if Localserve && c.remoteIP.IsLoopback() {
2753 // Lookup based on message From address is an approximation.
2754 if _, ok := mox.Conf.Domain(msgFrom.Domain); ok {
2755 resolver = dns.MockResolver{
2756 TXT: map[string][]string{msgFrom.Domain.ASCII + ".": {"v=spf1 ip4:127.0.0.1/8 ip6:::1 ~all"}},
2757 }
2758 }
2759 }
2760 receivedSPF, spfDomain, spfExpl, spfAuthentic, spfErr = spf.Verify(spfctx, c.log.Logger, resolver, spfArgs)
2761 spfcancel()
2762 if spfErr != nil {
2763 c.log.Infox("spf verify", spfErr)
2764 }
2765 }()
2766
2767 // Wait for DKIM and SPF validation to finish.
2768 wg.Wait()
2769
2770 // Give immediate response if all recipients are unknown.
2771 nunknown := 0
2772 for _, r := range c.recipients {
2773 if r.Account == nil && r.Alias == nil {
2774 nunknown++
2775 }
2776 }
2777 if nunknown == len(c.recipients) {
2778 // During RCPT TO we found that the address does not exist.
2779 c.log.Info("deliver attempt to unknown user(s)", slog.Any("recipients", c.recipients))
2780
2781 // Crude attempt to slow down someone trying to guess names. Would work better
2782 // with connection rate limiter.
2783 if unknownRecipientsDelay > 0 {
2784 mox.Sleep(ctx, unknownRecipientsDelay)
2785 }
2786
2787 // 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.
2788 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeAddr1UnknownDestMailbox1, "no such user(s)")
2789 }
2790
2791 // Add DKIM results to Authentication-Results header.
2792 authResAddDKIM := func(result, comment, reason string, props []message.AuthProp) {
2793 dm := message.AuthMethod{
2794 Method: "dkim",
2795 Result: result,
2796 Comment: comment,
2797 Reason: reason,
2798 Props: props,
2799 }
2800 authResults.Methods = append(authResults.Methods, dm)
2801 }
2802 if dkimErr != nil {
2803 c.log.Errorx("dkim verify", dkimErr)
2804 authResAddDKIM("none", "", dkimErr.Error(), nil)
2805 } else if len(dkimResults) == 0 {
2806 c.log.Info("no dkim-signature header", slog.Any("mailfrom", c.mailFrom))
2807 authResAddDKIM("none", "", "no dkim signatures", nil)
2808 }
2809 for i, r := range dkimResults {
2810 var domain, selector dns.Domain
2811 var identity *dkim.Identity
2812 var comment string
2813 var props []message.AuthProp
2814 if r.Sig != nil {
2815 if r.Record != nil && r.Record.PublicKey != nil {
2816 if pubkey, ok := r.Record.PublicKey.(*rsa.PublicKey); ok {
2817 comment = fmt.Sprintf("%d bit rsa, ", pubkey.N.BitLen())
2818 }
2819 }
2820
2821 sig := base64.StdEncoding.EncodeToString(r.Sig.Signature)
2822 sig = sig[:min(len(sig), 12)] // Must be at least 8 characters and unique among the signatures.
2823 props = []message.AuthProp{
2824 message.MakeAuthProp("header", "d", r.Sig.Domain.XName(c.msgsmtputf8), true, r.Sig.Domain.ASCIIExtra(c.msgsmtputf8)),
2825 message.MakeAuthProp("header", "s", r.Sig.Selector.XName(c.msgsmtputf8), true, r.Sig.Selector.ASCIIExtra(c.msgsmtputf8)),
2826 message.MakeAuthProp("header", "a", r.Sig.Algorithm(), false, ""),
2827 message.MakeAuthProp("header", "b", sig, false, ""), // ../rfc/6008:147
2828 }
2829 domain = r.Sig.Domain
2830 selector = r.Sig.Selector
2831 if r.Sig.Identity != nil {
2832 props = append(props, message.MakeAuthProp("header", "i", r.Sig.Identity.String(), true, ""))
2833 identity = r.Sig.Identity
2834 }
2835 if r.RecordAuthentic {
2836 comment += "with dnssec"
2837 } else {
2838 comment += "without dnssec"
2839 }
2840 }
2841 var errmsg string
2842 if r.Err != nil {
2843 errmsg = r.Err.Error()
2844 }
2845 authResAddDKIM(string(r.Status), comment, errmsg, props)
2846 c.log.Debugx("dkim verification result", r.Err,
2847 slog.Int("index", i),
2848 slog.Any("mailfrom", c.mailFrom),
2849 slog.Any("status", r.Status),
2850 slog.Any("domain", domain),
2851 slog.Any("selector", selector),
2852 slog.Any("identity", identity))
2853 }
2854
2855 // Add SPF results to Authentication-Results header. ../rfc/7208:2141
2856 var spfIdentity *dns.Domain
2857 var mailFromValidation = store.ValidationUnknown
2858 var ehloValidation = store.ValidationUnknown
2859 switch receivedSPF.Identity {
2860 case spf.ReceivedHELO:
2861 if len(spfArgs.HelloDomain.IP) == 0 {
2862 spfIdentity = &spfArgs.HelloDomain.Domain
2863 }
2864 ehloValidation = store.SPFValidation(receivedSPF.Result)
2865 case spf.ReceivedMailFrom:
2866 spfIdentity = &spfArgs.MailFromDomain
2867 mailFromValidation = store.SPFValidation(receivedSPF.Result)
2868 }
2869 var props []message.AuthProp
2870 if spfIdentity != nil {
2871 props = []message.AuthProp{message.MakeAuthProp("smtp", string(receivedSPF.Identity), spfIdentity.XName(c.msgsmtputf8), true, spfIdentity.ASCIIExtra(c.msgsmtputf8))}
2872 }
2873 var spfComment string
2874 if spfAuthentic {
2875 spfComment = "with dnssec"
2876 } else {
2877 spfComment = "without dnssec"
2878 }
2879 authResults.Methods = append(authResults.Methods, message.AuthMethod{
2880 Method: "spf",
2881 Result: string(receivedSPF.Result),
2882 Comment: spfComment,
2883 Props: props,
2884 })
2885 switch receivedSPF.Result {
2886 case spf.StatusPass:
2887 c.log.Debug("spf pass", slog.Any("ip", spfArgs.RemoteIP), slog.String("mailfromdomain", spfArgs.MailFromDomain.ASCII)) // todo: log the domain that was actually verified.
2888 case spf.StatusFail:
2889 if spfExpl != "" {
2890 // Filter out potentially hostile text. ../rfc/7208:2529
2891 for _, b := range []byte(spfExpl) {
2892 if b < ' ' || b >= 0x7f {
2893 spfExpl = ""
2894 break
2895 }
2896 }
2897 if spfExpl != "" {
2898 if len(spfExpl) > 800 {
2899 spfExpl = spfExpl[:797] + "..."
2900 }
2901 spfExpl = "remote claims: " + spfExpl
2902 }
2903 }
2904 if spfExpl == "" {
2905 spfExpl = fmt.Sprintf("your ip %s is not on the SPF allowlist for domain %s", spfArgs.RemoteIP, spfDomain.ASCII)
2906 }
2907 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?
2908 case spf.StatusTemperror:
2909 c.log.Infox("spf temperror", spfErr)
2910 case spf.StatusPermerror:
2911 c.log.Infox("spf permerror", spfErr)
2912 case spf.StatusNone, spf.StatusNeutral, spf.StatusSoftfail:
2913 default:
2914 c.log.Error("unknown spf status, treating as None/Neutral", slog.Any("status", receivedSPF.Result))
2915 receivedSPF.Result = spf.StatusNone
2916 }
2917
2918 // DMARC
2919 var dmarcUse bool
2920 var dmarcResult dmarc.Result
2921 const applyRandomPercentage = true
2922 // dmarcMethod is added to authResults when delivering to recipients: accounts can
2923 // have different policy override rules.
2924 var dmarcMethod message.AuthMethod
2925 var msgFromValidation = store.ValidationNone
2926 if msgFrom.IsZero() {
2927 dmarcResult.Status = dmarc.StatusNone
2928 dmarcMethod = message.AuthMethod{
2929 Method: "dmarc",
2930 Result: string(dmarcResult.Status),
2931 }
2932 } else {
2933 msgFromValidation = alignment(ctx, c.log, msgFrom.Domain, dkimResults, receivedSPF.Result, spfIdentity)
2934
2935 // We are doing the DMARC evaluation now. But we only store it for inclusion in an
2936 // aggregate report when we actually use it. We use an evaluation for each
2937 // recipient, with each a potentially different result due to mailing
2938 // list/forwarding configuration. If we reject a message due to being spam, we
2939 // don't want to spend any resources for the sender domain, and we don't want to
2940 // give the sender any more information about us, so we won't record the
2941 // evaluation.
2942 // 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.
2943
2944 dmarcctx, dmarccancel := context.WithTimeout(ctx, time.Minute)
2945 defer dmarccancel()
2946 dmarcUse, dmarcResult = dmarc.Verify(dmarcctx, c.log.Logger, c.resolver, msgFrom.Domain, dkimResults, receivedSPF.Result, spfIdentity, applyRandomPercentage)
2947 dmarccancel()
2948 var comment string
2949 if dmarcResult.RecordAuthentic {
2950 comment = "with dnssec"
2951 } else {
2952 comment = "without dnssec"
2953 }
2954 dmarcMethod = message.AuthMethod{
2955 Method: "dmarc",
2956 Result: string(dmarcResult.Status),
2957 Comment: comment,
2958 Props: []message.AuthProp{
2959 // ../rfc/7489:1489
2960 message.MakeAuthProp("header", "from", msgFrom.Domain.ASCII, true, msgFrom.Domain.ASCIIExtra(c.msgsmtputf8)),
2961 },
2962 }
2963
2964 if dmarcResult.Status == dmarc.StatusPass && msgFromValidation == store.ValidationRelaxed {
2965 msgFromValidation = store.ValidationDMARC
2966 }
2967
2968 // todo future: consider enforcing an spf (soft)fail if there is no dmarc policy or the dmarc policy is none. ../rfc/7489:1507
2969 }
2970 c.log.Debug("dmarc verification", slog.Any("result", dmarcResult.Status), slog.Any("domain", msgFrom.Domain))
2971
2972 // Prepare for analyzing content, calculating reputation.
2973 ipmasked1, ipmasked2, ipmasked3 := ipmasked(c.remoteIP)
2974 var verifiedDKIMDomains []string
2975 dkimSeen := map[string]bool{}
2976 for _, r := range dkimResults {
2977 // A message can have multiple signatures for the same identity. For example when
2978 // signing the message multiple times with different algorithms (rsa and ed25519).
2979 if r.Status != dkim.StatusPass {
2980 continue
2981 }
2982 d := r.Sig.Domain.Name()
2983 if !dkimSeen[d] {
2984 dkimSeen[d] = true
2985 verifiedDKIMDomains = append(verifiedDKIMDomains, d)
2986 }
2987 }
2988
2989 // When we deliver, we try to remove from rejects mailbox based on message-id.
2990 // We'll parse it when we need it, but it is the same for each recipient.
2991 var messageID string
2992 var parsedMessageID bool
2993
2994 // We build up a DSN for each failed recipient. If we have recipients in dsnMsg
2995 // after processing, we queue the DSN. Unless all recipients failed, in which case
2996 // we may just fail the mail transaction instead (could be common for failure to
2997 // deliver to a single recipient, e.g. for junk mail).
2998 // ../rfc/3464:436
2999 type deliverError struct {
3000 rcptTo smtp.Path
3001 code int
3002 secode string
3003 userError bool
3004 errmsg string
3005 }
3006 var deliverErrors []deliverError
3007 addError := func(rcpt recipient, code int, secode string, userError bool, errmsg string) {
3008 e := deliverError{rcpt.Addr, code, secode, userError, errmsg}
3009 c.log.Info("deliver error",
3010 slog.Any("rcptto", e.rcptTo),
3011 slog.Int("code", code),
3012 slog.String("secode", "secode"),
3013 slog.Bool("usererror", userError),
3014 slog.String("errmsg", errmsg))
3015 deliverErrors = append(deliverErrors, e)
3016 }
3017
3018 // Sort recipients: local accounts, aliases, unknown. For ensuring we don't deliver
3019 // to an alias destination that was also explicitly sent to.
3020 rcptScore := func(r recipient) int {
3021 if r.Account != nil {
3022 return 0
3023 } else if r.Alias != nil {
3024 return 1
3025 }
3026 return 2
3027 }
3028 sort.SliceStable(c.recipients, func(i, j int) bool {
3029 return rcptScore(c.recipients[i]) < rcptScore(c.recipients[j])
3030 })
3031
3032 // Return whether address is a regular explicit recipient in this transaction. Used
3033 // to prevent delivering a message to an address both for alias and explicit
3034 // addressee. Relies on c.recipients being sorted as above.
3035 regularRecipient := func(addr smtp.Path) bool {
3036 for _, rcpt := range c.recipients {
3037 if rcpt.Account == nil {
3038 break
3039 } else if rcpt.Addr.Equal(addr) {
3040 return true
3041 }
3042 }
3043 return false
3044 }
3045
3046 // Prepare a message, analyze it against account's junk filter.
3047 // The returned analysis has an open account that must be closed by the caller.
3048 // We call this for all alias destinations, also when we already delivered to that
3049 // recipient: It may be the only recipient that would allow the message.
3050 messageAnalyze := func(log mlog.Log, smtpRcptTo, deliverTo smtp.Path, accountName string, destination config.Destination, canonicalAddr string) (a *analysis, rerr error) {
3051 acc, err := store.OpenAccount(log, accountName, false)
3052 if err != nil {
3053 log.Errorx("open account", err, slog.Any("account", accountName))
3054 metricDelivery.WithLabelValues("accounterror", "").Inc()
3055 return nil, err
3056 }
3057 defer func() {
3058 if a == nil {
3059 err := acc.Close()
3060 log.Check(err, "closing account during analysis")
3061 }
3062 }()
3063
3064 m := store.Message{
3065 Received: time.Now(),
3066 RemoteIP: c.remoteIP.String(),
3067 RemoteIPMasked1: ipmasked1,
3068 RemoteIPMasked2: ipmasked2,
3069 RemoteIPMasked3: ipmasked3,
3070 EHLODomain: c.hello.Domain.Name(),
3071 MailFrom: c.mailFrom.String(),
3072 MailFromLocalpart: c.mailFrom.Localpart,
3073 MailFromDomain: c.mailFrom.IPDomain.Domain.Name(),
3074 RcptToLocalpart: smtpRcptTo.Localpart,
3075 RcptToDomain: smtpRcptTo.IPDomain.Domain.Name(),
3076 MsgFromLocalpart: msgFrom.Localpart,
3077 MsgFromDomain: msgFrom.Domain.Name(),
3078 MsgFromOrgDomain: publicsuffix.Lookup(ctx, log.Logger, msgFrom.Domain).Name(),
3079 EHLOValidated: ehloValidation == store.ValidationPass,
3080 MailFromValidated: mailFromValidation == store.ValidationPass,
3081 MsgFromValidated: msgFromValidation == store.ValidationStrict || msgFromValidation == store.ValidationDMARC || msgFromValidation == store.ValidationRelaxed,
3082 EHLOValidation: ehloValidation,
3083 MailFromValidation: mailFromValidation,
3084 MsgFromValidation: msgFromValidation,
3085 DKIMDomains: verifiedDKIMDomains,
3086 DSN: isDSN,
3087 Size: msgWriter.Size,
3088 }
3089 if c.tls {
3090 tlsState := c.conn.(*tls.Conn).ConnectionState()
3091 m.ReceivedTLSVersion = tlsState.Version
3092 m.ReceivedTLSCipherSuite = tlsState.CipherSuite
3093 if c.requireTLS != nil {
3094 m.ReceivedRequireTLS = *c.requireTLS
3095 }
3096 } else {
3097 m.ReceivedTLSVersion = 1 // Signals plain text delivery.
3098 }
3099
3100 var msgTo, msgCc []message.Address
3101 if envelope != nil {
3102 msgTo = envelope.To
3103 msgCc = envelope.CC
3104 }
3105 d := delivery{c.tls, &m, dataFile, smtpRcptTo, deliverTo, destination, canonicalAddr, acc, msgTo, msgCc, msgFrom, c.dnsBLs, dmarcUse, dmarcResult, dkimResults, iprevStatus, c.smtputf8}
3106
3107 r := analyze(ctx, log, c.resolver, d)
3108 return &r, nil
3109 }
3110
3111 // Either deliver the message, or call addError to register the recipient as failed.
3112 // If recipient is an alias, we may be delivering to multiple address/accounts and
3113 // we will consider a message delivered if we delivered it to at least one account
3114 // (others may be over quota).
3115 processRecipient := func(rcpt recipient) {
3116 log := c.log.With(slog.Any("mailfrom", c.mailFrom), slog.Any("rcptto", rcpt.Addr))
3117
3118 // If this is not a valid local user, we send back a DSN. This can only happen when
3119 // there are also valid recipients, and only when remote is SPF-verified, so the DSN
3120 // should not cause backscatter.
3121 // In case of serious errors, we abort the transaction. We may have already
3122 // delivered some messages. Perhaps it would be better to continue with other
3123 // deliveries, and return an error at the end? Though the failure conditions will
3124 // probably prevent any other successful deliveries too...
3125 // We'll continue delivering to other recipients. ../rfc/5321:3275
3126 if rcpt.Account == nil && rcpt.Alias == nil {
3127 metricDelivery.WithLabelValues("unknownuser", "").Inc()
3128 addError(rcpt, smtp.C550MailboxUnavail, smtp.SeAddr1UnknownDestMailbox1, true, "no such user")
3129 return
3130 }
3131
3132 // la holds all analysis, and message preparation, for all accounts (multiple for
3133 // aliases). Each has an open account that we we close on return.
3134 var la []analysis
3135 defer func() {
3136 for _, a := range la {
3137 err := a.d.acc.Close()
3138 log.Check(err, "close account")
3139 }
3140 }()
3141
3142 // For aliases, we prepare & analyze for each recipient. We accept the message if
3143 // any recipient accepts it. Regular destination have just a single account to
3144 // check. We check all alias destinations, even if we already explicitly delivered
3145 // to them: they may be the only destination that would accept the message.
3146 var a0 *analysis // Analysis we've used for accept/reject decision.
3147 if rcpt.Alias != nil {
3148 // Check if msgFrom address is acceptable. This doesn't take validation into
3149 // consideration. If the header was forged, the message may be rejected later on.
3150 if !aliasAllowedMsgFrom(rcpt.Alias.Alias, msgFrom) {
3151 addError(rcpt, smtp.C550MailboxUnavail, smtp.SePol7ExpnProhibited2, true, "not allowed to send to destination")
3152 return
3153 }
3154
3155 la = make([]analysis, 0, len(rcpt.Alias.Alias.ParsedAddresses))
3156 for _, aa := range rcpt.Alias.Alias.ParsedAddresses {
3157 a, err := messageAnalyze(log, rcpt.Addr, aa.Address.Path(), aa.AccountName, aa.Destination, rcpt.Alias.CanonicalAddress)
3158 if err != nil {
3159 addError(rcpt, smtp.C451LocalErr, smtp.SeSys3Other0, false, "error processing")
3160 return
3161 }
3162 la = append(la, *a)
3163 if a.accept && a0 == nil {
3164 // Address that caused us to accept.
3165 a0 = &la[len(la)-1]
3166 }
3167 }
3168 if a0 == nil {
3169 // First address, for rejecting.
3170 a0 = &la[0]
3171 }
3172 } else {
3173 a, err := messageAnalyze(log, rcpt.Addr, rcpt.Addr, rcpt.Account.AccountName, rcpt.Account.Destination, rcpt.Account.CanonicalAddress)
3174 if err != nil {
3175 addError(rcpt, smtp.C451LocalErr, smtp.SeSys3Other0, false, "error processing")
3176 return
3177 }
3178 la = []analysis{*a}
3179 a0 = &la[0]
3180 }
3181
3182 if !a0.accept && a0.reason == reasonHighRate {
3183 log.Info("incoming message rejected for high rate, not storing in rejects mailbox", slog.String("reason", a0.reason), slog.Any("msgfrom", msgFrom))
3184 metricDelivery.WithLabelValues("reject", a0.reason).Inc()
3185 c.setSlow(true)
3186 addError(rcpt, a0.code, a0.secode, a0.userError, a0.errmsg)
3187 return
3188 }
3189
3190 // Any DMARC result override is stored in the evaluation for outgoing DMARC
3191 // aggregate reports, and added to the Authentication-Results message header.
3192 // We want to tell the sender that we have an override, e.g. for mailing lists, so
3193 // they don't overestimate the potential damage of switching from p=none to
3194 // p=reject.
3195 var dmarcOverrides []string
3196 if a0.dmarcOverrideReason != "" {
3197 dmarcOverrides = []string{a0.dmarcOverrideReason}
3198 }
3199 if dmarcResult.Record != nil && !dmarcUse {
3200 dmarcOverrides = append(dmarcOverrides, string(dmarcrpt.PolicyOverrideSampledOut))
3201 }
3202
3203 // Add per-recipient DMARC method to Authentication-Results. Each account can have
3204 // their own override rules, e.g. based on configured mailing lists/forwards.
3205 // ../rfc/7489:1486
3206 rcptDMARCMethod := dmarcMethod
3207 if len(dmarcOverrides) > 0 {
3208 if rcptDMARCMethod.Comment != "" {
3209 rcptDMARCMethod.Comment += ", "
3210 }
3211 rcptDMARCMethod.Comment += "override " + strings.Join(dmarcOverrides, ",")
3212 }
3213 rcptAuthResults := authResults
3214 rcptAuthResults.Methods = slices.Clone(authResults.Methods)
3215 rcptAuthResults.Methods = append(rcptAuthResults.Methods, rcptDMARCMethod)
3216
3217 // Prepend reason as message header, for easy viewing in mail clients.
3218 var xmox string
3219 if a0.reason != "" {
3220 hw := &message.HeaderWriter{}
3221 hw.Add(" ", "X-Mox-Reason:")
3222 hw.Add(" ", a0.reason)
3223 for i, s := range a0.reasonText {
3224 if i == 0 {
3225 s = "; " + s
3226 } else {
3227 hw.Newline()
3228 }
3229 // Just in case any of the strings has a newline, replace it with space to not break the message.
3230 s = strings.ReplaceAll(s, "\n", " ")
3231 s = strings.ReplaceAll(s, "\r", " ")
3232 s += ";"
3233 hw.AddWrap([]byte(s), true)
3234 }
3235 xmox = hw.String()
3236 }
3237 xmox += a0.headers
3238
3239 for i := range la {
3240 // ../rfc/5321:3204
3241 // Received-SPF header goes before Received. ../rfc/7208:2038
3242 la[i].d.m.MsgPrefix = []byte(
3243 xmox +
3244 "Delivered-To: " + la[i].d.deliverTo.XString(c.msgsmtputf8) + "\r\n" + // ../rfc/9228:274
3245 "Return-Path: <" + c.mailFrom.String() + ">\r\n" + // ../rfc/5321:3300
3246 rcptAuthResults.Header() +
3247 receivedSPF.Header() +
3248 recvHdrFor(rcpt.Addr.String()),
3249 )
3250 la[i].d.m.Size += int64(len(la[i].d.m.MsgPrefix))
3251 }
3252
3253 // Store DMARC evaluation for inclusion in an aggregate report. Only if there is at
3254 // least one reporting address: We don't want to needlessly store a row in a
3255 // database for each delivery attempt. If we reject a message for being junk, we
3256 // are also not going to send it a DMARC report. The DMARC check is done early in
3257 // the analysis, we will report on rejects because of DMARC, because it could be
3258 // valuable feedback about forwarded or mailing list messages.
3259 // ../rfc/7489:1492
3260 if !mox.Conf.Static.NoOutgoingDMARCReports && dmarcResult.Record != nil && len(dmarcResult.Record.AggregateReportAddresses) > 0 && (a0.accept && !a0.d.m.IsReject || a0.reason == reasonDMARCPolicy) {
3261 // Disposition holds our decision on whether to accept the message. Not what the
3262 // DMARC evaluation resulted in. We can override, e.g. because of mailing lists,
3263 // forwarding, or local policy.
3264 // We treat quarantine as reject, so never claim to quarantine.
3265 // ../rfc/7489:1691
3266 disposition := dmarcrpt.DispositionNone
3267 if !a0.accept {
3268 disposition = dmarcrpt.DispositionReject
3269 }
3270
3271 // unknownDomain returns whether the sender is domain with which this account has
3272 // not had positive interaction.
3273 unknownDomain := func() (unknown bool) {
3274 err := a0.d.acc.DB.Read(ctx, func(tx *bstore.Tx) (err error) {
3275 // See if we received a non-junk message from this organizational domain.
3276 q := bstore.QueryTx[store.Message](tx)
3277 q.FilterNonzero(store.Message{MsgFromOrgDomain: a0.d.m.MsgFromOrgDomain})
3278 q.FilterEqual("Expunged", false)
3279 q.FilterEqual("Notjunk", true)
3280 q.FilterEqual("IsReject", false)
3281 exists, err := q.Exists()
3282 if err != nil {
3283 return fmt.Errorf("querying for non-junk message from organizational domain: %v", err)
3284 }
3285 if exists {
3286 return nil
3287 }
3288
3289 // See if we sent a message to this organizational domain.
3290 qr := bstore.QueryTx[store.Recipient](tx)
3291 qr.FilterNonzero(store.Recipient{OrgDomain: a0.d.m.MsgFromOrgDomain})
3292 exists, err = qr.Exists()
3293 if err != nil {
3294 return fmt.Errorf("querying for message sent to organizational domain: %v", err)
3295 }
3296 if !exists {
3297 unknown = true
3298 }
3299 return nil
3300 })
3301 if err != nil {
3302 log.Errorx("checking if sender is unknown domain, for dmarc aggregate report evaluation", err)
3303 }
3304 return
3305 }
3306
3307 r := dmarcResult.Record
3308 addresses := make([]string, len(r.AggregateReportAddresses))
3309 for i, a := range r.AggregateReportAddresses {
3310 addresses[i] = a.String()
3311 }
3312 sp := dmarcrpt.Disposition(r.SubdomainPolicy)
3313 if r.SubdomainPolicy == dmarc.PolicyEmpty {
3314 sp = dmarcrpt.Disposition(r.Policy)
3315 }
3316 eval := dmarcdb.Evaluation{
3317 // Evaluated and IntervalHours set by AddEvaluation.
3318 PolicyDomain: dmarcResult.Domain.Name(),
3319
3320 // Optional evaluations don't cause a report to be sent, but will be included.
3321 // Useful for automated inter-mailer messages, we don't want to get in a reporting
3322 // loop. We also don't want to be used for sending reports to unsuspecting domains
3323 // we have no relation with.
3324 // 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.
3325 Optional: a0.d.destination.DMARCReports || a0.d.destination.HostTLSReports || a0.d.destination.DomainTLSReports || a0.reason == reasonDMARCPolicy && unknownDomain(),
3326
3327 Addresses: addresses,
3328
3329 PolicyPublished: dmarcrpt.PolicyPublished{
3330 Domain: dmarcResult.Domain.Name(),
3331 ADKIM: dmarcrpt.Alignment(r.ADKIM),
3332 ASPF: dmarcrpt.Alignment(r.ASPF),
3333 Policy: dmarcrpt.Disposition(r.Policy),
3334 SubdomainPolicy: sp,
3335 Percentage: r.Percentage,
3336 // We don't save ReportingOptions, we don't do per-message failure reporting.
3337 },
3338 SourceIP: c.remoteIP.String(),
3339 Disposition: disposition,
3340 AlignedDKIMPass: dmarcResult.AlignedDKIMPass,
3341 AlignedSPFPass: dmarcResult.AlignedSPFPass,
3342 EnvelopeTo: rcpt.Addr.IPDomain.String(),
3343 EnvelopeFrom: c.mailFrom.IPDomain.String(),
3344 HeaderFrom: msgFrom.Domain.Name(),
3345 }
3346
3347 for _, s := range dmarcOverrides {
3348 reason := dmarcrpt.PolicyOverrideReason{Type: dmarcrpt.PolicyOverride(s)}
3349 eval.OverrideReasons = append(eval.OverrideReasons, reason)
3350 }
3351
3352 // We'll include all signatures for the organizational domain, even if they weren't
3353 // relevant due to strict alignment requirement.
3354 for _, dkimResult := range dkimResults {
3355 if dkimResult.Sig == nil || publicsuffix.Lookup(ctx, log.Logger, msgFrom.Domain) != publicsuffix.Lookup(ctx, log.Logger, dkimResult.Sig.Domain) {
3356 continue
3357 }
3358 r := dmarcrpt.DKIMAuthResult{
3359 Domain: dkimResult.Sig.Domain.Name(),
3360 Selector: dkimResult.Sig.Selector.ASCII,
3361 Result: dmarcrpt.DKIMResult(dkimResult.Status),
3362 }
3363 eval.DKIMResults = append(eval.DKIMResults, r)
3364 }
3365
3366 switch receivedSPF.Identity {
3367 case spf.ReceivedHELO:
3368 spfAuthResult := dmarcrpt.SPFAuthResult{
3369 Domain: spfArgs.HelloDomain.String(), // Can be unicode and also IP.
3370 Scope: dmarcrpt.SPFDomainScopeHelo,
3371 Result: dmarcrpt.SPFResult(receivedSPF.Result),
3372 }
3373 eval.SPFResults = []dmarcrpt.SPFAuthResult{spfAuthResult}
3374 case spf.ReceivedMailFrom:
3375 spfAuthResult := dmarcrpt.SPFAuthResult{
3376 Domain: spfArgs.MailFromDomain.Name(), // Can be unicode.
3377 Scope: dmarcrpt.SPFDomainScopeMailFrom,
3378 Result: dmarcrpt.SPFResult(receivedSPF.Result),
3379 }
3380 eval.SPFResults = []dmarcrpt.SPFAuthResult{spfAuthResult}
3381 }
3382
3383 err := dmarcdb.AddEvaluation(ctx, dmarcResult.Record.AggregateReportingInterval, &eval)
3384 log.Check(err, "adding dmarc evaluation to database for aggregate report")
3385 }
3386
3387 if !a0.accept {
3388 for _, a := range la {
3389 // Don't add message if address was also explicitly present in a RCPT TO command.
3390 if rcpt.Alias != nil && regularRecipient(a.d.deliverTo) {
3391 continue
3392 }
3393
3394 conf, _ := a.d.acc.Conf()
3395 if conf.RejectsMailbox == "" {
3396 continue
3397 }
3398 present, _, messagehash, err := rejectPresent(log, a.d.acc, conf.RejectsMailbox, a.d.m, dataFile)
3399 if err != nil {
3400 log.Errorx("checking whether reject is already present", err)
3401 continue
3402 } else if present {
3403 log.Info("reject message is already present, ignoring")
3404 continue
3405 }
3406 a.d.m.IsReject = true
3407 a.d.m.Seen = true // We don't want to draw attention.
3408 // Regular automatic junk flags configuration applies to these messages. The
3409 // default is to treat these as neutral, so they won't cause outright rejections
3410 // due to reputation for later delivery attempts.
3411 a.d.m.MessageHash = messagehash
3412 a.d.acc.WithWLock(func() {
3413 var changes []store.Change
3414 var stored bool
3415
3416 var newID int64
3417 defer func() {
3418 if newID != 0 {
3419 p := a.d.acc.MessagePath(newID)
3420 err := os.Remove(p)
3421 c.log.Check(err, "remove message after error delivering to rejects", slog.String("path", p))
3422 }
3423 }()
3424
3425 err := a.d.acc.DB.Write(context.TODO(), func(tx *bstore.Tx) error {
3426 mbrej, err := a.d.acc.MailboxFind(tx, conf.RejectsMailbox)
3427 if err != nil {
3428 return fmt.Errorf("finding rejects mailbox: %v", err)
3429 }
3430
3431 if !conf.KeepRejects && mbrej != nil {
3432 chl, hasSpace, err := a.d.acc.TidyRejectsMailbox(c.log, tx, mbrej)
3433 if err != nil {
3434 return fmt.Errorf("tidying rejects mailbox: %v", err)
3435 }
3436 changes = append(changes, chl...)
3437 if !hasSpace {
3438 log.Info("not storing spammy mail to full rejects mailbox")
3439 return nil
3440 }
3441 }
3442 if mbrej == nil {
3443 nmb, chl, _, _, err := a.d.acc.MailboxCreate(tx, conf.RejectsMailbox, store.SpecialUse{})
3444 if err != nil {
3445 return fmt.Errorf("creating rejects mailbox: %v", err)
3446 }
3447 changes = append(changes, chl...)
3448
3449 mbrej = &nmb
3450 }
3451
3452 mailbox := a.mailboxDestined
3453 if mailbox == "" {
3454 mailbox = a.mailbox
3455 }
3456 var modseq store.ModSeq
3457 mbDest, chl, err := a.d.acc.MailboxEnsure(tx, mailbox, true, store.SpecialUse{}, &modseq)
3458 if err != nil {
3459 return fmt.Errorf("ensuring destined mailbox exists: %v", err)
3460 }
3461 a.d.m.MailboxDestinedID = mbDest.ID
3462 changes = append(changes, chl...)
3463
3464 if err := a.d.acc.MessageAdd(log, tx, mbrej, a.d.m, dataFile, store.AddOpts{}); err != nil {
3465 return fmt.Errorf("delivering spammy mail to rejects mailbox: %v", err)
3466 }
3467 newID = a.d.m.ID
3468
3469 if err := tx.Update(mbrej); err != nil {
3470 return fmt.Errorf("updating rejects mailbox: %v", err)
3471 }
3472 changes = append(changes, a.d.m.ChangeAddUID(*mbrej), mbrej.ChangeCounts())
3473 stored = true
3474 return nil
3475 })
3476 if err != nil {
3477 log.Errorx("delivering to rejects mailbox", err)
3478 return
3479 } else if stored {
3480 log.Info("stored spammy mail in rejects mailbox")
3481 }
3482 newID = 0
3483
3484 store.BroadcastChanges(a.d.acc, changes)
3485 })
3486 }
3487
3488 log.Info("incoming message rejected", slog.String("reason", a0.reason), slog.Any("msgfrom", msgFrom))
3489 metricDelivery.WithLabelValues("reject", a0.reason).Inc()
3490 c.setSlow(true)
3491 addError(rcpt, a0.code, a0.secode, a0.userError, a0.errmsg)
3492 return
3493 }
3494
3495 delayFirstTime := true
3496 if rcpt.Account != nil && a0.dmarcReport != nil {
3497 // todo future: add rate limiting to prevent DoS attacks. ../rfc/7489:2570
3498 if err := dmarcdb.AddReport(ctx, a0.dmarcReport, msgFrom.Domain); err != nil {
3499 log.Errorx("saving dmarc aggregate report in database", err)
3500 } else {
3501 log.Info("dmarc aggregate report processed")
3502 a0.d.m.Flags.Seen = true
3503 delayFirstTime = false
3504 }
3505 }
3506 if rcpt.Account != nil && a0.tlsReport != nil {
3507 // todo future: add rate limiting to prevent DoS attacks.
3508 if err := tlsrptdb.AddReport(ctx, c.log, msgFrom.Domain, c.mailFrom.String(), a0.d.destination.HostTLSReports, a0.tlsReport); err != nil {
3509 log.Errorx("saving TLSRPT report in database", err)
3510 } else {
3511 log.Info("tlsrpt report processed")
3512 a0.d.m.Flags.Seen = true
3513 delayFirstTime = false
3514 }
3515 }
3516
3517 // If this is a first-time sender and not a forwarded/mailing list message, wait
3518 // before actually delivering. If this turns out to be a spammer, we've kept one of
3519 // their connections busy.
3520 a0conf, _ := a0.d.acc.Conf()
3521 if delayFirstTime && !a0.d.m.IsForward && !a0.d.m.IsMailingList && a0.reason == reasonNoBadSignals && !a0conf.NoFirstTimeSenderDelay && c.firstTimeSenderDelay > 0 {
3522 log.Debug("delaying before delivering from sender without reputation", slog.Duration("delay", c.firstTimeSenderDelay))
3523 mox.Sleep(mox.Context, c.firstTimeSenderDelay)
3524 }
3525
3526 if Localserve {
3527 code, timeout := mox.LocalserveNeedsError(rcpt.Addr.Localpart)
3528 if timeout {
3529 log.Info("timing out due to special localpart")
3530 mox.Sleep(mox.Context, time.Hour)
3531 xsmtpServerErrorf(codes{smtp.C451LocalErr, smtp.SeOther00}, "timing out delivery due to special localpart")
3532 } else if code != 0 {
3533 log.Info("failure due to special localpart", slog.Int("code", code))
3534 metricDelivery.WithLabelValues("delivererror", "localserve").Inc()
3535 addError(rcpt, code, smtp.SeOther00, false, fmt.Sprintf("failure with code %d due to special localpart", code))
3536 return
3537 }
3538 }
3539
3540 // Gather the message-id before we deliver and the file may be consumed.
3541 if !parsedMessageID {
3542 if p, err := message.Parse(c.log.Logger, false, store.FileMsgReader(a0.d.m.MsgPrefix, dataFile)); err != nil {
3543 log.Infox("parsing message for message-id", err)
3544 } else if header, err := p.Header(); err != nil {
3545 log.Infox("parsing message header for message-id", err)
3546 } else {
3547 messageID = header.Get("Message-Id")
3548 }
3549 parsedMessageID = true
3550 }
3551
3552 // Finally deliver the message to the account(s).
3553 var nerr int // Number of non-quota errors.
3554 var nfull int // Number of failed deliveries due to over quota.
3555 var ndelivered int // Number delivered to account.
3556 for _, a := range la {
3557 // Don't deliver to recipient that was explicitly present in SMTP transaction, or
3558 // is sending the message to an alias they are member of.
3559 if rcpt.Alias != nil && (regularRecipient(a.d.deliverTo) || a.d.deliverTo.Equal(msgFrom.Path())) {
3560 continue
3561 }
3562
3563 var delivered bool
3564 a.d.acc.WithWLock(func() {
3565 if err := a.d.acc.DeliverMailbox(log, a.mailbox, a.mailboxDestined, a.d.m, dataFile); err != nil {
3566 log.Errorx("delivering", err)
3567 metricDelivery.WithLabelValues("delivererror", a0.reason).Inc()
3568 if errors.Is(err, store.ErrOverQuota) {
3569 nfull++
3570 } else {
3571 addError(rcpt, smtp.C451LocalErr, smtp.SeSys3Other0, false, "error processing")
3572 nerr++
3573 }
3574 return
3575 }
3576 delivered = true
3577 ndelivered++
3578 metricDelivery.WithLabelValues("delivered", a0.reason).Inc()
3579 log.Info("incoming message delivered", slog.String("reason", a0.reason), slog.Any("msgfrom", msgFrom))
3580
3581 conf, _ := a.d.acc.Conf()
3582 if conf.RejectsMailbox != "" && a.d.m.MessageID != "" {
3583 if err := a.d.acc.RejectsRemove(log, conf.RejectsMailbox, a.d.m.MessageID); err != nil {
3584 log.Errorx("removing message from rejects mailbox", err, slog.String("messageid", messageID))
3585 }
3586 }
3587 })
3588
3589 // Pass delivered messages to queue for DSN processing and/or hooks.
3590 if delivered {
3591 mr := store.FileMsgReader(a.d.m.MsgPrefix, dataFile)
3592 part, err := a.d.m.LoadPart(mr)
3593 if err != nil {
3594 log.Errorx("loading parsed part for evaluating webhook", err)
3595 } else {
3596 err = queue.Incoming(context.Background(), log, a.d.acc, messageID, *a.d.m, part, a.mailbox)
3597 log.Check(err, "queueing webhook for incoming delivery")
3598 }
3599 } else if nerr > 0 && ndelivered == 0 {
3600 // Don't continue if we had an error and haven't delivered yet. If we only had
3601 // quota-related errors, we keep trying for an account to deliver to.
3602 break
3603 }
3604 }
3605 if ndelivered == 0 && (nerr > 0 || nfull > 0) {
3606 if nerr == 0 {
3607 addError(rcpt, smtp.C452StorageFull, smtp.SeMailbox2Full2, true, "account storage full")
3608 } else {
3609 addError(rcpt, smtp.C451LocalErr, smtp.SeSys3Other0, false, "error processing")
3610 }
3611 }
3612 }
3613
3614 // For each recipient, do final spam analysis and delivery.
3615 for _, rcpt := range c.recipients {
3616 processRecipient(rcpt)
3617 }
3618
3619 // If all recipients failed to deliver, return an error.
3620 if len(c.recipients) == len(deliverErrors) {
3621 same := true
3622 e0 := deliverErrors[0]
3623 var serverError bool
3624 var msgs []string
3625 major := 4
3626 for _, e := range deliverErrors {
3627 serverError = serverError || !e.userError
3628 if e.code != e0.code || e.secode != e0.secode {
3629 same = false
3630 }
3631 msgs = append(msgs, e.errmsg)
3632 if e.code >= 500 {
3633 major = 5
3634 }
3635 }
3636 if same {
3637 xsmtpErrorf(e0.code, e0.secode, !serverError, "%s", strings.Join(msgs, "\n"))
3638 }
3639
3640 // Not all failures had the same error. We'll return each error on a separate line.
3641 lines := []string{}
3642 for _, e := range deliverErrors {
3643 s := fmt.Sprintf("%d %d.%s %s", e.code, e.code/100, e.secode, e.errmsg)
3644 lines = append(lines, s)
3645 }
3646 code := smtp.C451LocalErr
3647 secode := smtp.SeSys3Other0
3648 if major == 5 {
3649 code = smtp.C554TransactionFailed
3650 }
3651 lines = append(lines, "multiple errors")
3652 xsmtpErrorf(code, secode, !serverError, "%s", strings.Join(lines, "\n"))
3653 }
3654 // Generate one DSN for all failed recipients.
3655 if len(deliverErrors) > 0 {
3656 now := time.Now()
3657 dsnMsg := dsn.Message{
3658 SMTPUTF8: c.msgsmtputf8,
3659 From: smtp.Path{Localpart: "postmaster", IPDomain: deliverErrors[0].rcptTo.IPDomain},
3660 To: *c.mailFrom,
3661 Subject: "mail delivery failure",
3662 MessageID: mox.MessageIDGen(false),
3663 References: messageID,
3664
3665 // Per-message details.
3666 ReportingMTA: mox.Conf.Static.HostnameDomain.ASCII,
3667 ReceivedFromMTA: smtp.Ehlo{Name: c.hello, ConnIP: c.remoteIP},
3668 ArrivalDate: now,
3669 }
3670
3671 if len(deliverErrors) > 1 {
3672 dsnMsg.TextBody = "Multiple delivery failures occurred.\n\n"
3673 }
3674
3675 for _, e := range deliverErrors {
3676 kind := "Permanent"
3677 if e.code/100 == 4 {
3678 kind = "Transient"
3679 }
3680 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))
3681 rcpt := dsn.Recipient{
3682 FinalRecipient: e.rcptTo,
3683 Action: dsn.Failed,
3684 Status: fmt.Sprintf("%d.%s", e.code/100, e.secode),
3685 LastAttemptDate: now,
3686 }
3687 dsnMsg.Recipients = append(dsnMsg.Recipients, rcpt)
3688 }
3689
3690 header, err := message.ReadHeaders(bufio.NewReader(&moxio.AtReader{R: dataFile}))
3691 if err != nil {
3692 c.log.Errorx("reading headers of incoming message for dsn, continuing dsn without headers", err)
3693 }
3694 dsnMsg.Original = header
3695
3696 if Localserve {
3697 c.log.Error("not queueing dsn for incoming delivery due to localserve")
3698 } else if err := queueDSN(context.TODO(), c.log, c, *c.mailFrom, dsnMsg, c.requireTLS != nil && *c.requireTLS); err != nil {
3699 metricServerErrors.WithLabelValues("queuedsn").Inc()
3700 c.log.Errorx("queuing DSN for incoming delivery, no DSN sent", err)
3701 }
3702 }
3703
3704 c.transactionGood++
3705 c.transactionBad-- // Compensate for early earlier pessimistic increase.
3706 c.rset()
3707 c.xwritecodeline(smtp.C250Completed, smtp.SeMailbox2Other0, "it is done", nil)
3708}
3709
3710// Return whether msgFrom address is allowed to send a message to alias.
3711func aliasAllowedMsgFrom(alias config.Alias, msgFrom smtp.Address) bool {
3712 for _, aa := range alias.ParsedAddresses {
3713 if aa.Address == msgFrom {
3714 return true
3715 }
3716 }
3717 lp, err := smtp.ParseLocalpart(alias.LocalpartStr)
3718 xcheckf(err, "parsing alias localpart")
3719 if msgFrom == smtp.NewAddress(lp, alias.Domain) {
3720 return alias.AllowMsgFrom
3721 }
3722 return alias.PostPublic
3723}
3724
3725// ecode returns either ecode, or a more specific error based on err.
3726// For example, ecode can be turned from an "other system" error into a "mail
3727// system full" if the error indicates no disk space is available.
3728func errCodes(code int, ecode string, err error) codes {
3729 switch {
3730 case moxio.IsStorageSpace(err):
3731 switch ecode {
3732 case smtp.SeMailbox2Other0:
3733 if code == smtp.C451LocalErr {
3734 code = smtp.C452StorageFull
3735 }
3736 ecode = smtp.SeMailbox2Full2
3737 case smtp.SeSys3Other0:
3738 if code == smtp.C451LocalErr {
3739 code = smtp.C452StorageFull
3740 }
3741 ecode = smtp.SeSys3StorageFull1
3742 }
3743 }
3744 return codes{code, ecode}
3745}
3746
3747// ../rfc/5321:2079
3748func (c *conn) cmdRset(p *parser) {
3749 // ../rfc/5321:2106
3750 p.xend()
3751
3752 c.rset()
3753 c.xbwritecodeline(smtp.C250Completed, smtp.SeOther00, "all clear", nil)
3754}
3755
3756// ../rfc/5321:2108 ../rfc/5321:1222
3757func (c *conn) cmdVrfy(p *parser) {
3758 // No EHLO/HELO needed.
3759 // ../rfc/5321:2448
3760
3761 // ../rfc/5321:2119 ../rfc/6531:641
3762 p.xspace()
3763 p.xstring()
3764 if p.space() {
3765 p.xtake("SMTPUTF8")
3766 }
3767 p.xend()
3768
3769 // todo future: we could support vrfy and expn for submission? though would need to see if its rfc defines it.
3770
3771 // ../rfc/5321:4239
3772 xsmtpUserErrorf(smtp.C252WithoutVrfy, smtp.SePol7Other0, "no verify but will try delivery")
3773}
3774
3775// ../rfc/5321:2135 ../rfc/5321:1272
3776func (c *conn) cmdExpn(p *parser) {
3777 // No EHLO/HELO needed.
3778 // ../rfc/5321:2448
3779
3780 // ../rfc/5321:2149 ../rfc/6531:645
3781 p.xspace()
3782 p.xstring()
3783 if p.space() {
3784 p.xtake("SMTPUTF8")
3785 }
3786 p.xend()
3787
3788 // todo: we could implement expn for local aliases for authenticated users, when members have permission to list. would anyone use it?
3789
3790 // ../rfc/5321:4239
3791 xsmtpUserErrorf(smtp.C252WithoutVrfy, smtp.SePol7Other0, "no expand but will try delivery")
3792}
3793
3794// ../rfc/5321:2151
3795func (c *conn) cmdHelp(p *parser) {
3796 // Let's not strictly parse the request for help. We are ignoring the text anyway.
3797 // ../rfc/5321:2166
3798
3799 c.xbwritecodeline(smtp.C214Help, smtp.SeOther00, "see rfc 5321 (smtp)", nil)
3800}
3801
3802// ../rfc/5321:2191
3803func (c *conn) cmdNoop(p *parser) {
3804 // No idea why, but if an argument follows, it must adhere to the string ABNF production...
3805 // ../rfc/5321:2203
3806 if p.space() {
3807 p.xstring()
3808 }
3809 p.xend()
3810
3811 c.xbwritecodeline(smtp.C250Completed, smtp.SeOther00, "alrighty", nil)
3812}
3813
3814// ../rfc/5321:2205
3815func (c *conn) cmdQuit(p *parser) {
3816 // ../rfc/5321:2226
3817 p.xend()
3818
3819 c.xwritecodeline(smtp.C221Closing, smtp.SeOther00, "okay thanks bye", nil)
3820 panic(cleanClose)
3821}
3822