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