1// Package smtpserver implements an SMTP server for submission and incoming delivery of mail messages.
2package smtpserver
3
4import (
5 "bufio"
6 "bytes"
7 "context"
8 "crypto/ed25519"
9 "crypto/md5"
10 cryptorand "crypto/rand"
11 "crypto/rsa"
12 "crypto/sha1"
13 "crypto/sha256"
14 "crypto/tls"
15 "crypto/x509"
16 "encoding/base64"
17 "errors"
18 "fmt"
19 "hash"
20 "io"
21 "log/slog"
22 "math"
23 "net"
24 "net/textproto"
25 "os"
26 "runtime/debug"
27 "slices"
28 "sort"
29 "strings"
30 "sync"
31 "time"
32 "unicode"
33
34 "golang.org/x/exp/maps"
35 "golang.org/x/text/unicode/norm"
36
37 "github.com/prometheus/client_golang/prometheus"
38 "github.com/prometheus/client_golang/prometheus/promauto"
39
40 "github.com/mjl-/bstore"
41
42 "github.com/mjl-/mox/config"
43 "github.com/mjl-/mox/dkim"
44 "github.com/mjl-/mox/dmarc"
45 "github.com/mjl-/mox/dmarcdb"
46 "github.com/mjl-/mox/dmarcrpt"
47 "github.com/mjl-/mox/dns"
48 "github.com/mjl-/mox/dsn"
49 "github.com/mjl-/mox/iprev"
50 "github.com/mjl-/mox/message"
51 "github.com/mjl-/mox/metrics"
52 "github.com/mjl-/mox/mlog"
53 "github.com/mjl-/mox/mox-"
54 "github.com/mjl-/mox/moxio"
55 "github.com/mjl-/mox/moxvar"
56 "github.com/mjl-/mox/publicsuffix"
57 "github.com/mjl-/mox/queue"
58 "github.com/mjl-/mox/ratelimit"
59 "github.com/mjl-/mox/scram"
60 "github.com/mjl-/mox/smtp"
61 "github.com/mjl-/mox/spf"
62 "github.com/mjl-/mox/store"
63 "github.com/mjl-/mox/tlsrpt"
64 "github.com/mjl-/mox/tlsrptdb"
65)
66
67// We use panic and recover for error handling while executing commands.
68// These errors signal the connection must be closed.
69var errIO = errors.New("io error")
70
71// If set, regular delivery/submit is sidestepped, email is accepted and
72// delivered to the account named mox.
73var Localserve bool
74
75var limiterConnectionRate, limiterConnections *ratelimit.Limiter
76
77// For delivery rate limiting. Variable because changed during tests.
78var limitIPMasked1MessagesPerMinute int = 500
79var limitIPMasked1SizePerMinute int64 = 1000 * 1024 * 1024
80
81// Maximum number of RCPT TO commands (i.e. recipients) for a single message
82// delivery. Must be at least 100. Announced in LIMIT extension.
83const rcptToLimit = 1000
84
85func init() {
86 // Also called by tests, so they don't trigger the rate limiter.
87 limitersInit()
88}
89
90func limitersInit() {
91 mox.LimitersInit()
92 // todo future: make these configurable
93 limiterConnectionRate = &ratelimit.Limiter{
94 WindowLimits: []ratelimit.WindowLimit{
95 {
96 Window: time.Minute,
97 Limits: [...]int64{300, 900, 2700},
98 },
99 },
100 }
101 limiterConnections = &ratelimit.Limiter{
102 WindowLimits: []ratelimit.WindowLimit{
103 {
104 Window: time.Duration(math.MaxInt64), // All of time.
105 Limits: [...]int64{30, 90, 270},
106 },
107 },
108 }
109}
110
111var (
112 // Delays for bad/suspicious behaviour. Zero during tests.
113 badClientDelay = time.Second // Before reads and after 1-byte writes for probably spammers.
114 authFailDelay = time.Second // Response to authentication failure.
115 unknownRecipientsDelay = 5 * time.Second // Response when all recipients are unknown.
116 firstTimeSenderDelayDefault = 15 * time.Second // Before accepting message from first-time sender.
117)
118
119type codes struct {
120 code int
121 secode string // Enhanced code, but without the leading major int from code.
122}
123
124var (
125 metricConnection = promauto.NewCounterVec(
126 prometheus.CounterOpts{
127 Name: "mox_smtpserver_connection_total",
128 Help: "Incoming SMTP connections.",
129 },
130 []string{
131 "kind", // "deliver" or "submit"
132 },
133 )
134 metricCommands = promauto.NewHistogramVec(
135 prometheus.HistogramOpts{
136 Name: "mox_smtpserver_command_duration_seconds",
137 Help: "SMTP server command duration and result codes in seconds.",
138 Buckets: []float64{0.001, 0.005, 0.01, 0.05, 0.100, 0.5, 1, 5, 10, 20, 30, 60, 120},
139 },
140 []string{
141 "kind", // "deliver" or "submit"
142 "cmd",
143 "code",
144 "ecode",
145 },
146 )
147 metricDelivery = promauto.NewCounterVec(
148 prometheus.CounterOpts{
149 Name: "mox_smtpserver_delivery_total",
150 Help: "SMTP incoming message delivery from external source, not submission. Result values: delivered, reject, unknownuser, accounterror, delivererror. Reason indicates why a message was rejected/accepted.",
151 },
152 []string{
153 "result",
154 "reason",
155 },
156 )
157 // Similar between ../webmail/webmail.go:/metricSubmission and ../smtpserver/server.go:/metricSubmission and ../webapisrv/server.go:/metricSubmission
158 metricSubmission = promauto.NewCounterVec(
159 prometheus.CounterOpts{
160 Name: "mox_smtpserver_submission_total",
161 Help: "SMTP server incoming submission results, known values (those ending with error are server errors): ok, badmessage, badfrom, badheader, messagelimiterror, recipientlimiterror, localserveerror, queueerror.",
162 },
163 []string{
164 "result",
165 },
166 )
167 metricServerErrors = promauto.NewCounterVec(
168 prometheus.CounterOpts{
169 Name: "mox_smtpserver_errors_total",
170 Help: "SMTP server errors, known values: dkimsign, queuedsn.",
171 },
172 []string{
173 "error",
174 },
175 )
176 metricDeliveryStarttls = promauto.NewCounter(
177 prometheus.CounterOpts{
178 Name: "mox_smtpserver_delivery_starttls_total",
179 Help: "Total number of STARTTLS handshakes for incoming deliveries.",
180 },
181 )
182 metricDeliveryStarttlsErrors = promauto.NewCounterVec(
183 prometheus.CounterOpts{
184 Name: "mox_smtpserver_delivery_starttls_errors_total",
185 Help: "Errors with TLS handshake during STARTTLS for incoming deliveries.",
186 },
187 []string{
188 "reason", // "eof", "sslv2", "unsupportedversions", "nottls", "alert-<num>-<msg>", "other"
189 },
190 )
191)
192
193var jitterRand = mox.NewPseudoRand()
194
195func durationDefault(delay *time.Duration, def time.Duration) time.Duration {
196 if delay == nil {
197 return def
198 }
199 return *delay
200}
201
202// Listen initializes network listeners for incoming SMTP connection.
203// The listeners are stored for a later call to Serve.
204func Listen() {
205 names := maps.Keys(mox.Conf.Static.Listeners)
206 sort.Strings(names)
207 for _, name := range names {
208 listener := mox.Conf.Static.Listeners[name]
209
210 var tlsConfig, tlsConfigDelivery *tls.Config
211 if listener.TLS != nil {
212 tlsConfig = listener.TLS.Config
213 // For SMTP delivery, if we get a TLS handshake for an SNI hostname that we don't
214 // allow, we'll fallback to a certificate for the listener hostname instead of
215 // causing the connection to fail. May improve interoperability.
216 tlsConfigDelivery = listener.TLS.ConfigFallback
217 }
218
219 maxMsgSize := listener.SMTPMaxMessageSize
220 if maxMsgSize == 0 {
221 maxMsgSize = config.DefaultMaxMsgSize
222 }
223
224 if listener.SMTP.Enabled {
225 hostname := mox.Conf.Static.HostnameDomain
226 if listener.Hostname != "" {
227 hostname = listener.HostnameDomain
228 }
229 port := config.Port(listener.SMTP.Port, 25)
230 for _, ip := range listener.IPs {
231 firstTimeSenderDelay := durationDefault(listener.SMTP.FirstTimeSenderDelay, firstTimeSenderDelayDefault)
232 if tlsConfigDelivery != nil {
233 tlsConfigDelivery = tlsConfigDelivery.Clone()
234 // Default setting is currently to have session tickets disabled, to work around
235 // TLS interoperability issues with incoming deliveries from Microsoft. See
236 // https://github.com/golang/go/issues/70232.
237 tlsConfigDelivery.SessionTicketsDisabled = listener.SMTP.TLSSessionTicketsDisabled == nil || *listener.SMTP.TLSSessionTicketsDisabled
238 }
239 listen1("smtp", name, ip, port, hostname, tlsConfigDelivery, false, false, maxMsgSize, false, listener.SMTP.RequireSTARTTLS, !listener.SMTP.NoRequireTLS, listener.SMTP.DNSBLZones, firstTimeSenderDelay)
240 }
241 }
242 if listener.Submission.Enabled {
243 hostname := mox.Conf.Static.HostnameDomain
244 if listener.Hostname != "" {
245 hostname = listener.HostnameDomain
246 }
247 port := config.Port(listener.Submission.Port, 587)
248 for _, ip := range listener.IPs {
249 listen1("submission", name, ip, port, hostname, tlsConfig, true, false, maxMsgSize, !listener.Submission.NoRequireSTARTTLS, !listener.Submission.NoRequireSTARTTLS, true, nil, 0)
250 }
251 }
252
253 if listener.Submissions.Enabled {
254 hostname := mox.Conf.Static.HostnameDomain
255 if listener.Hostname != "" {
256 hostname = listener.HostnameDomain
257 }
258 port := config.Port(listener.Submissions.Port, 465)
259 for _, ip := range listener.IPs {
260 listen1("submissions", name, ip, port, hostname, tlsConfig, true, true, maxMsgSize, true, true, true, nil, 0)
261 }
262 }
263 }
264}
265
266var servers []func()
267
268func listen1(protocol, name, ip string, port int, hostname dns.Domain, tlsConfig *tls.Config, submission, xtls bool, maxMessageSize int64, requireTLSForAuth, requireTLSForDelivery, requireTLS bool, dnsBLs []dns.Domain, firstTimeSenderDelay time.Duration) {
269 log := mlog.New("smtpserver", nil)
270 addr := net.JoinHostPort(ip, fmt.Sprintf("%d", port))
271 if os.Getuid() == 0 {
272 log.Print("listening for smtp",
273 slog.String("listener", name),
274 slog.String("address", addr),
275 slog.String("protocol", protocol))
276 }
277 network := mox.Network(ip)
278 ln, err := mox.Listen(network, addr)
279 if err != nil {
280 log.Fatalx("smtp: listen for smtp", err, slog.String("protocol", protocol), slog.String("listener", name))
281 }
282
283 // Each listener gets its own copy of the config, so session keys between different
284 // ports on same listener aren't shared. We rotate session keys explicitly in this
285 // base TLS config because each connection clones the TLS config before using. The
286 // base TLS config would never get automatically managed/rotated session keys.
287 if tlsConfig != nil {
288 tlsConfig = tlsConfig.Clone()
289 mox.StartTLSSessionTicketKeyRefresher(mox.Shutdown, log, tlsConfig)
290 }
291
292 serve := func() {
293 for {
294 conn, err := ln.Accept()
295 if err != nil {
296 log.Infox("smtp: accept", err, slog.String("protocol", protocol), slog.String("listener", name))
297 continue
298 }
299
300 // Package is set on the resolver by the dkim/spf/dmarc/etc packages.
301 resolver := dns.StrictResolver{Log: log.Logger}
302 go serve(name, mox.Cid(), hostname, tlsConfig, conn, resolver, submission, xtls, false, maxMessageSize, requireTLSForAuth, requireTLSForDelivery, requireTLS, dnsBLs, firstTimeSenderDelay)
303 }
304 }
305
306 servers = append(servers, serve)
307}
308
309// Serve starts serving on all listeners, launching a goroutine per listener.
310func Serve() {
311 for _, serve := range servers {
312 go serve()
313 }
314}
315
316type conn struct {
317 cid int64
318
319 // OrigConn is the original (TCP) connection. We'll read from/write to conn, which
320 // can be wrapped in a tls.Server. We close origConn instead of conn because
321 // closing the TLS connection would send a TLS close notification, which may block
322 // for 5s if the server isn't reading it (because it is also sending it).
323 origConn net.Conn
324 conn net.Conn
325
326 tls bool
327 extRequireTLS bool // Whether to announce and allow the REQUIRETLS extension.
328 viaHTTPS bool // Whether the connection came in via the HTTPS port (using TLS ALPN).
329 resolver dns.Resolver
330 r *bufio.Reader
331 w *bufio.Writer
332 tr *moxio.TraceReader // Kept for changing trace level during cmd/auth/data.
333 tw *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) || moxio.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 attrs := []slog.Attr{
632 slog.Any("version", tlsVersion(cs.Version)),
633 slog.String("ciphersuite", tls.CipherSuiteName(cs.CipherSuite)),
634 slog.String("sni", cs.ServerName),
635 slog.Bool("resumed", cs.DidResume),
636 slog.Int("clientcerts", len(cs.PeerCertificates)),
637 }
638 if c.account != nil {
639 attrs = append(attrs,
640 slog.String("account", c.account.Name),
641 slog.String("username", c.username),
642 )
643 }
644 c.log.Debug("tls handshake completed", attrs...)
645}
646
647type tlsVersion uint16
648
649func (v tlsVersion) String() string {
650 return strings.ReplaceAll(strings.ToLower(tls.VersionName(uint16(v))), " ", "-")
651}
652
653// completely reset connection state as if greeting has just been sent.
654// ../rfc/3207:210
655func (c *conn) reset() {
656 c.ehlo = false
657 c.hello = dns.IPDomain{}
658 if !c.authTLS {
659 c.username = ""
660 if c.account != nil {
661 err := c.account.Close()
662 c.log.Check(err, "closing account")
663 }
664 c.account = nil
665 }
666 c.authSASL = false
667 c.rset()
668}
669
670// for rset command, and a few more cases that reset the mail transaction state.
671// ../rfc/5321:2502
672func (c *conn) rset() {
673 c.mailFrom = nil
674 c.requireTLS = nil
675 c.futureRelease = time.Time{}
676 c.futureReleaseRequest = ""
677 c.has8bitmime = false
678 c.smtputf8 = false
679 c.msgsmtputf8 = false
680 c.recipients = nil
681}
682
683func (c *conn) earliestDeadline(d time.Duration) time.Time {
684 e := time.Now().Add(d)
685 if !c.deadline.IsZero() && c.deadline.Before(e) {
686 return c.deadline
687 }
688 return e
689}
690
691func (c *conn) xcheckAuth() {
692 if c.submission && c.account == nil {
693 // ../rfc/4954:623
694 xsmtpUserErrorf(smtp.C530SecurityRequired, smtp.SePol7Other0, "authentication required")
695 }
696}
697
698func (c *conn) xtrace(level slog.Level) func() {
699 c.xflush()
700 c.tr.SetTrace(level)
701 c.tw.SetTrace(level)
702 return func() {
703 c.xflush()
704 c.tr.SetTrace(mlog.LevelTrace)
705 c.tw.SetTrace(mlog.LevelTrace)
706 }
707}
708
709// setSlow marks the connection slow (or now), so reads are done with 3 second
710// delay for each read, and writes are done at 1 byte per second, to try to slow
711// down spammers.
712func (c *conn) setSlow(on bool) {
713 if on && !c.slow {
714 c.log.Debug("connection changed to slow")
715 } else if !on && c.slow {
716 c.log.Debug("connection restored to regular pace")
717 }
718 c.slow = on
719}
720
721// Write writes to the connection. It panics on i/o errors, which is handled by the
722// connection command loop.
723func (c *conn) Write(buf []byte) (int, error) {
724 chunk := len(buf)
725 if c.slow {
726 chunk = 1
727 }
728
729 // We set a single deadline for Write and Read. This may be a TLS connection.
730 // SetDeadline works on the underlying connection. If we wouldn't touch the read
731 // deadline, and only set the write deadline and do a bunch of writes, the TLS
732 // library would still have to do reads on the underlying connection, and may reach
733 // a read deadline that was set for some earlier read.
734 // We have one deadline for the whole write. In case of slow writing, we'll write
735 // the last chunk in one go, so remote smtp clients don't abort the connection for
736 // being slow.
737 deadline := c.earliestDeadline(30 * time.Second)
738 if err := c.conn.SetDeadline(deadline); err != nil {
739 c.log.Errorx("setting deadline for write", err)
740 }
741
742 var n int
743 for len(buf) > 0 {
744 nn, err := c.conn.Write(buf[:chunk])
745 if err != nil {
746 panic(fmt.Errorf("write: %s (%w)", err, errIO))
747 }
748 n += nn
749 buf = buf[chunk:]
750 if len(buf) > 0 && badClientDelay > 0 {
751 mox.Sleep(mox.Context, badClientDelay)
752
753 // Make sure we don't take too long, otherwise the remote SMTP client may close the
754 // connection.
755 if time.Until(deadline) < 5*badClientDelay {
756 chunk = len(buf)
757 }
758 }
759 }
760 return n, nil
761}
762
763// Read reads from the connection. It panics on i/o errors, which is handled by the
764// connection command loop.
765func (c *conn) Read(buf []byte) (int, error) {
766 if c.slow && badClientDelay > 0 {
767 mox.Sleep(mox.Context, badClientDelay)
768 }
769
770 // todo future: make deadline configurable for callers, and through config file? ../rfc/5321:3610 ../rfc/6409:492
771 // See comment about Deadline instead of individual read/write deadlines at Write.
772 if err := c.conn.SetDeadline(c.earliestDeadline(30 * time.Second)); err != nil {
773 c.log.Errorx("setting deadline for read", err)
774 }
775
776 n, err := c.conn.Read(buf)
777 if err != nil {
778 panic(fmt.Errorf("read: %s (%w)", err, errIO))
779 }
780 return n, err
781}
782
783// Cache of line buffers for reading commands.
784// Filled on demand.
785var bufpool = moxio.NewBufpool(8, 2*1024)
786
787func (c *conn) readline() string {
788 line, err := bufpool.Readline(c.log, c.r)
789 if err != nil && errors.Is(err, moxio.ErrLineTooLong) {
790 c.writecodeline(smtp.C500BadSyntax, smtp.SeProto5Other0, "line too long, smtp max is 512, we reached 2048", nil)
791 panic(fmt.Errorf("%s (%w)", err, errIO))
792 } else if err != nil {
793 panic(fmt.Errorf("%s (%w)", err, errIO))
794 }
795 return line
796}
797
798// Buffered-write command response line to connection with codes and msg.
799// Err is not sent to remote but is used for logging and can be empty.
800func (c *conn) bwritecodeline(code int, secode string, msg string, err error) {
801 var ecode string
802 if secode != "" {
803 ecode = fmt.Sprintf("%d.%s", code/100, secode)
804 }
805 metricCommands.WithLabelValues(c.kind(), c.cmd, fmt.Sprintf("%d", code), ecode).Observe(float64(time.Since(c.cmdStart)) / float64(time.Second))
806 c.log.Debugx("smtp command result", err,
807 slog.String("kind", c.kind()),
808 slog.String("cmd", c.cmd),
809 slog.Int("code", code),
810 slog.String("ecode", ecode),
811 slog.Duration("duration", time.Since(c.cmdStart)))
812
813 var sep string
814 if ecode != "" {
815 sep = " "
816 }
817
818 // Separate by newline and wrap long lines.
819 lines := strings.Split(msg, "\n")
820 for i, line := range lines {
821 // ../rfc/5321:3506 ../rfc/5321:2583 ../rfc/5321:2756
822 var prelen = 3 + 1 + len(ecode) + len(sep)
823 for prelen+len(line) > 510 {
824 e := 510 - prelen
825 for ; e > 400 && line[e] != ' '; e-- {
826 }
827 // 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.
828 c.bwritelinef("%d-%s%s%s", code, ecode, sep, line[:e])
829 line = line[e:]
830 }
831 spdash := " "
832 if i < len(lines)-1 {
833 spdash = "-"
834 }
835 c.bwritelinef("%d%s%s%s%s", code, spdash, ecode, sep, line)
836 }
837}
838
839// Buffered-write a formatted response line to connection.
840func (c *conn) bwritelinef(format string, args ...any) {
841 msg := fmt.Sprintf(format, args...)
842 fmt.Fprint(c.w, msg+"\r\n")
843}
844
845// Flush pending buffered writes to connection.
846func (c *conn) xflush() {
847 c.w.Flush() // Errors will have caused a panic in Write.
848}
849
850// Write (with flush) a response line with codes and message. err is not written, used for logging and can be nil.
851func (c *conn) writecodeline(code int, secode string, msg string, err error) {
852 c.bwritecodeline(code, secode, msg, err)
853 c.xflush()
854}
855
856// Write (with flush) a formatted response line to connection.
857func (c *conn) writelinef(format string, args ...any) {
858 c.bwritelinef(format, args...)
859 c.xflush()
860}
861
862var cleanClose struct{} // Sentinel value for panic/recover indicating clean close of connection.
863
864// ServeTLSConn serves a TLS connection.
865func ServeTLSConn(listenerName string, hostname dns.Domain, conn *tls.Conn, tlsConfig *tls.Config, submission, viaHTTPS bool, maxMsgSize int64, requireTLS bool) {
866 log := mlog.New("smtpserver", nil)
867 resolver := dns.StrictResolver{Log: log.Logger}
868 serve(listenerName, mox.Cid(), hostname, tlsConfig, conn, resolver, submission, true, viaHTTPS, maxMsgSize, true, true, requireTLS, nil, 0)
869}
870
871func 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) {
872 var localIP, remoteIP net.IP
873 if a, ok := nc.LocalAddr().(*net.TCPAddr); ok {
874 localIP = a.IP
875 } else {
876 // For net.Pipe, during tests.
877 localIP = net.ParseIP("127.0.0.10")
878 }
879 if a, ok := nc.RemoteAddr().(*net.TCPAddr); ok {
880 remoteIP = a.IP
881 } else {
882 // For net.Pipe, during tests.
883 remoteIP = net.ParseIP("127.0.0.10")
884 }
885
886 origConn := nc
887 if viaHTTPS {
888 origConn = nc.(*tls.Conn).NetConn()
889 }
890
891 c := &conn{
892 cid: cid,
893 origConn: origConn,
894 conn: nc,
895 submission: submission,
896 tls: xtls,
897 viaHTTPS: viaHTTPS,
898 extRequireTLS: requireTLS,
899 resolver: resolver,
900 lastlog: time.Now(),
901 baseTLSConfig: tlsConfig,
902 localIP: localIP,
903 remoteIP: remoteIP,
904 hostname: hostname,
905 maxMessageSize: maxMessageSize,
906 requireTLSForAuth: requireTLSForAuth,
907 requireTLSForDelivery: requireTLSForDelivery,
908 dnsBLs: dnsBLs,
909 firstTimeSenderDelay: firstTimeSenderDelay,
910 }
911 var logmutex sync.Mutex
912 // Also see (and possibly update) c.logbg, for logging in a goroutine.
913 c.log = mlog.New("smtpserver", nil).WithFunc(func() []slog.Attr {
914 logmutex.Lock()
915 defer logmutex.Unlock()
916 now := time.Now()
917 l := []slog.Attr{
918 slog.Int64("cid", c.cid),
919 slog.Duration("delta", now.Sub(c.lastlog)),
920 }
921 c.lastlog = now
922 if c.username != "" {
923 l = append(l, slog.String("username", c.username))
924 }
925 return l
926 })
927 c.tr = moxio.NewTraceReader(c.log, "RC: ", c)
928 c.r = bufio.NewReader(c.tr)
929 c.tw = moxio.NewTraceWriter(c.log, "LS: ", c)
930 c.w = bufio.NewWriter(c.tw)
931
932 metricConnection.WithLabelValues(c.kind()).Inc()
933 c.log.Info("new connection",
934 slog.Any("remote", c.conn.RemoteAddr()),
935 slog.Any("local", c.conn.LocalAddr()),
936 slog.Bool("submission", submission),
937 slog.Bool("tls", xtls),
938 slog.Bool("viahttps", viaHTTPS),
939 slog.String("listener", listenerName))
940
941 defer func() {
942 c.origConn.Close() // Close actual TCP socket, regardless of TLS on top.
943 c.conn.Close() // If TLS, will try to write alert notification to already closed socket, returning error quickly.
944
945 if c.account != nil {
946 err := c.account.Close()
947 c.log.Check(err, "closing account")
948 c.account = nil
949 }
950
951 x := recover()
952 if x == nil || x == cleanClose {
953 c.log.Info("connection closed")
954 } else if err, ok := x.(error); ok && isClosed(err) {
955 c.log.Infox("connection closed", err)
956 } else {
957 c.log.Error("unhandled panic", slog.Any("err", x))
958 debug.PrintStack()
959 metrics.PanicInc(metrics.Smtpserver)
960 }
961 }()
962
963 if xtls && !viaHTTPS {
964 // Start TLS on connection. We perform the handshake explicitly, so we can set a
965 // timeout, do client certificate authentication, log TLS details afterwards.
966 c.xtlsHandshakeAndAuthenticate(c.conn)
967 }
968
969 select {
970 case <-mox.Shutdown.Done():
971 // ../rfc/5321:2811 ../rfc/5321:1666 ../rfc/3463:420
972 c.writecodeline(smtp.C421ServiceUnavail, smtp.SeSys3NotAccepting2, "shutting down", nil)
973 return
974 default:
975 }
976
977 if !limiterConnectionRate.Add(c.remoteIP, time.Now(), 1) {
978 c.writecodeline(smtp.C421ServiceUnavail, smtp.SePol7Other0, "connection rate from your ip or network too high, slow down please", nil)
979 return
980 }
981
982 // If remote IP/network resulted in too many authentication failures, refuse to serve.
983 if submission && !mox.LimiterFailedAuth.CanAdd(c.remoteIP, time.Now(), 1) {
984 metrics.AuthenticationRatelimitedInc("submission")
985 c.log.Debug("refusing connection due to many auth failures", slog.Any("remoteip", c.remoteIP))
986 c.writecodeline(smtp.C421ServiceUnavail, smtp.SePol7Other0, "too many auth failures", nil)
987 return
988 }
989
990 if !limiterConnections.Add(c.remoteIP, time.Now(), 1) {
991 c.log.Debug("refusing connection due to many open connections", slog.Any("remoteip", c.remoteIP))
992 c.writecodeline(smtp.C421ServiceUnavail, smtp.SePol7Other0, "too many open connections from your ip or network", nil)
993 return
994 }
995 defer limiterConnections.Add(c.remoteIP, time.Now(), -1)
996
997 // We register and unregister the original connection, in case c.conn is replaced
998 // with a TLS connection later on.
999 mox.Connections.Register(nc, "smtp", listenerName)
1000 defer mox.Connections.Unregister(nc)
1001
1002 // ../rfc/5321:964 ../rfc/5321:4294 about announcing software and version
1003 // Syntax: ../rfc/5321:2586
1004 // We include the string ESMTP. https://cr.yp.to/smtp/greeting.html recommends it.
1005 // Should not be too relevant nowadays, but does not hurt and default blackbox
1006 // exporter SMTP health check expects it.
1007 c.writelinef("%d %s ESMTP mox %s", smtp.C220ServiceReady, c.hostname.ASCII, moxvar.Version)
1008
1009 for {
1010 command(c)
1011
1012 // If another command is present, don't flush our buffered response yet. Holding
1013 // off will cause us to respond with a single packet.
1014 n := c.r.Buffered()
1015 if n > 0 {
1016 buf, err := c.r.Peek(n)
1017 if err == nil && bytes.IndexByte(buf, '\n') >= 0 {
1018 continue
1019 }
1020 }
1021 c.xflush()
1022 }
1023}
1024
1025var commands = map[string]func(c *conn, p *parser){
1026 "helo": (*conn).cmdHelo,
1027 "ehlo": (*conn).cmdEhlo,
1028 "starttls": (*conn).cmdStarttls,
1029 "auth": (*conn).cmdAuth,
1030 "mail": (*conn).cmdMail,
1031 "rcpt": (*conn).cmdRcpt,
1032 "data": (*conn).cmdData,
1033 "rset": (*conn).cmdRset,
1034 "vrfy": (*conn).cmdVrfy,
1035 "expn": (*conn).cmdExpn,
1036 "help": (*conn).cmdHelp,
1037 "noop": (*conn).cmdNoop,
1038 "quit": (*conn).cmdQuit,
1039}
1040
1041func command(c *conn) {
1042 defer func() {
1043 x := recover()
1044 if x == nil {
1045 return
1046 }
1047 err, ok := x.(error)
1048 if !ok {
1049 panic(x)
1050 }
1051
1052 if isClosed(err) {
1053 panic(err)
1054 }
1055
1056 var serr smtpError
1057 if errors.As(err, &serr) {
1058 c.writecodeline(serr.code, serr.secode, fmt.Sprintf("%s (%s)", serr.errmsg, mox.ReceivedID(c.cid)), serr.err)
1059 if serr.printStack {
1060 c.log.Errorx("smtp error", serr.err, slog.Int("code", serr.code), slog.String("secode", serr.secode))
1061 debug.PrintStack()
1062 }
1063 } else {
1064 // Other type of panic, we pass it on, aborting the connection.
1065 c.log.Errorx("command panic", err)
1066 panic(err)
1067 }
1068 }()
1069
1070 // todo future: we could wait for either a line or shutdown, and just close the connection on shutdown.
1071
1072 line := c.readline()
1073 t := strings.SplitN(line, " ", 2)
1074 var args string
1075 if len(t) == 2 {
1076 args = " " + t[1]
1077 }
1078 cmd := t[0]
1079 cmdl := strings.ToLower(cmd)
1080
1081 // 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
1082
1083 select {
1084 case <-mox.Shutdown.Done():
1085 // ../rfc/5321:2811 ../rfc/5321:1666 ../rfc/3463:420
1086 c.writecodeline(smtp.C421ServiceUnavail, smtp.SeSys3NotAccepting2, "shutting down", nil)
1087 panic(errIO)
1088 default:
1089 }
1090
1091 c.cmd = cmdl
1092 c.cmdStart = time.Now()
1093
1094 p := newParser(args, c.smtputf8, c)
1095 fn, ok := commands[cmdl]
1096 if !ok {
1097 c.cmd = "(unknown)"
1098 if c.ncmds == 0 {
1099 // Other side is likely speaking something else than SMTP, send error message and
1100 // stop processing because there is a good chance whatever they sent has multiple
1101 // lines.
1102 c.writecodeline(smtp.C500BadSyntax, smtp.SeProto5Syntax2, "please try again speaking smtp", nil)
1103 panic(errIO)
1104 }
1105 // note: not "command not implemented", see ../rfc/5321:2934 ../rfc/5321:2539
1106 xsmtpUserErrorf(smtp.C500BadSyntax, smtp.SeProto5BadCmdOrSeq1, "unknown command")
1107 }
1108 c.ncmds++
1109 fn(c, p)
1110}
1111
1112// For use in metric labels.
1113func (c *conn) kind() string {
1114 if c.submission {
1115 return "submission"
1116 }
1117 return "smtp"
1118}
1119
1120func (c *conn) xneedHello() {
1121 if c.hello.IsZero() {
1122 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "no ehlo/helo yet")
1123 }
1124}
1125
1126// If smtp server is configured to require TLS for all mail delivery (except to TLS
1127// reporting address), abort command.
1128func (c *conn) xneedTLSForDelivery(rcpt smtp.Path) {
1129 // For TLS reports, we allow the message in even without TLS, because there may be
1130 // TLS interopability problems. ../rfc/8460:316
1131 if c.requireTLSForDelivery && !c.tls && !isTLSReportRecipient(rcpt) {
1132 // ../rfc/3207:148
1133 xsmtpUserErrorf(smtp.C530SecurityRequired, smtp.SePol7Other0, "STARTTLS required for mail delivery")
1134 }
1135}
1136
1137func isTLSReportRecipient(rcpt smtp.Path) bool {
1138 _, _, _, dest, err := mox.LookupAddress(rcpt.Localpart, rcpt.IPDomain.Domain, false, false, false)
1139 return err == nil && (dest.HostTLSReports || dest.DomainTLSReports)
1140}
1141
1142func (c *conn) cmdHelo(p *parser) {
1143 c.cmdHello(p, false)
1144}
1145
1146func (c *conn) cmdEhlo(p *parser) {
1147 c.cmdHello(p, true)
1148}
1149
1150// ../rfc/5321:1783
1151func (c *conn) cmdHello(p *parser, ehlo bool) {
1152 var remote dns.IPDomain
1153 if c.submission && !mox.Pedantic {
1154 // Mail clients regularly put bogus information in the hostname/ip. For submission,
1155 // the value is of no use, so there is not much point in annoying the user with
1156 // errors they cannot fix themselves. Except when in pedantic mode.
1157 remote = dns.IPDomain{IP: c.remoteIP}
1158 } else {
1159 p.xspace()
1160 if ehlo {
1161 remote = p.xipdomain(true)
1162 } else {
1163 remote = dns.IPDomain{Domain: p.xdomain()}
1164
1165 // Verify a remote domain name has an A or AAAA record, CNAME not allowed. ../rfc/5321:722
1166 cidctx := context.WithValue(mox.Context, mlog.CidKey, c.cid)
1167 ctx, cancel := context.WithTimeout(cidctx, time.Minute)
1168 _, _, err := c.resolver.LookupIPAddr(ctx, remote.Domain.ASCII+".")
1169 cancel()
1170 if dns.IsNotFound(err) {
1171 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeProto5Other0, "your ehlo domain does not resolve to an IP address")
1172 }
1173 // For success or temporary resolve errors, we'll just continue.
1174 }
1175 // ../rfc/5321:1827
1176 // Though a few paragraphs earlier is a claim additional data can occur for address
1177 // literals (IP addresses), although the ABNF in that document does not allow it.
1178 // We allow additional text, but only if space-separated.
1179 if len(remote.IP) > 0 && p.space() {
1180 p.remainder() // ../rfc/5321:1802 ../rfc/2821:1632
1181 }
1182 p.xend()
1183 }
1184
1185 // Reset state as if RSET command has been issued. ../rfc/5321:2093 ../rfc/5321:2453
1186 c.rset()
1187
1188 c.ehlo = ehlo
1189 c.hello = remote
1190
1191 // https://www.iana.org/assignments/mail-parameters/mail-parameters.xhtml
1192
1193 c.bwritelinef("250-%s", c.hostname.ASCII)
1194 c.bwritelinef("250-PIPELINING") // ../rfc/2920:108
1195 c.bwritelinef("250-SIZE %d", c.maxMessageSize) // ../rfc/1870:70
1196 // ../rfc/3207:237
1197 if !c.tls && c.baseTLSConfig != nil {
1198 // ../rfc/3207:90
1199 c.bwritelinef("250-STARTTLS")
1200 } else if c.extRequireTLS {
1201 // ../rfc/8689:202
1202 // ../rfc/8689:143
1203 c.bwritelinef("250-REQUIRETLS")
1204 }
1205 if c.submission {
1206 var mechs string
1207 // ../rfc/4954:123
1208 if c.tls || !c.requireTLSForAuth {
1209 // We always mention the SCRAM PLUS variants, even if TLS is not active: It is a
1210 // hint to the client that a TLS connection can use TLS channel binding during
1211 // authentication. The client should select the bare variant when TLS isn't
1212 // present, and also not indicate the server supports the PLUS variant in that
1213 // case, or it would trigger the mechanism downgrade detection.
1214 mechs = "SCRAM-SHA-256-PLUS SCRAM-SHA-256 SCRAM-SHA-1-PLUS SCRAM-SHA-1 CRAM-MD5 PLAIN LOGIN"
1215 }
1216 if c.tls && len(c.conn.(*tls.Conn).ConnectionState().PeerCertificates) > 0 && !c.viaHTTPS {
1217 mechs = "EXTERNAL " + mechs
1218 }
1219 c.bwritelinef("250-AUTH %s", mechs)
1220 // ../rfc/4865:127
1221 t := time.Now().Add(queue.FutureReleaseIntervalMax).UTC() // ../rfc/4865:98
1222 c.bwritelinef("250-FUTURERELEASE %d %s", queue.FutureReleaseIntervalMax/time.Second, t.Format(time.RFC3339))
1223 }
1224 c.bwritelinef("250-ENHANCEDSTATUSCODES") // ../rfc/2034:71
1225 // todo future? c.writelinef("250-DSN")
1226 c.bwritelinef("250-8BITMIME") // ../rfc/6152:86
1227 c.bwritelinef("250-LIMITS RCPTMAX=%d", rcptToLimit) // ../rfc/9422:301
1228 c.bwritecodeline(250, "", "SMTPUTF8", nil) // ../rfc/6531:201
1229 c.xflush()
1230}
1231
1232// ../rfc/3207:96
1233func (c *conn) cmdStarttls(p *parser) {
1234 c.xneedHello()
1235 p.xend()
1236
1237 if c.tls {
1238 // ../rfc/3207:235
1239 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "already speaking tls")
1240 }
1241 if c.account != nil {
1242 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "cannot starttls after authentication")
1243 }
1244 if c.baseTLSConfig == nil {
1245 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "starttls not offered")
1246 }
1247
1248 // We don't want to do TLS on top of c.r because it also prints protocol traces: We
1249 // don't want to log the TLS stream. So we'll do TLS on the underlying connection,
1250 // but make sure any bytes already read and in the buffer are used for the TLS
1251 // handshake.
1252 conn := c.conn
1253 if n := c.r.Buffered(); n > 0 {
1254 conn = &moxio.PrefixConn{
1255 PrefixReader: io.LimitReader(c.r, int64(n)),
1256 Conn: conn,
1257 }
1258 }
1259
1260 // We add the cid to the output, to help debugging in case of a failing TLS connection.
1261 c.writecodeline(smtp.C220ServiceReady, smtp.SeOther00, "go! ("+mox.ReceivedID(c.cid)+")", nil)
1262
1263 c.xtlsHandshakeAndAuthenticate(conn)
1264
1265 c.reset() // ../rfc/3207:210
1266 c.tls = true
1267}
1268
1269// ../rfc/4954:139
1270func (c *conn) cmdAuth(p *parser) {
1271 c.xneedHello()
1272
1273 if !c.submission {
1274 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "authentication only allowed on submission ports")
1275 }
1276 if c.authSASL {
1277 // ../rfc/4954:152
1278 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "already authenticated")
1279 }
1280 if c.mailFrom != nil {
1281 // ../rfc/4954:157
1282 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "authentication not allowed during mail transaction")
1283 }
1284
1285 // If authentication fails due to missing derived secrets, we don't hold it against
1286 // the connection. There is no way to indicate server support for an authentication
1287 // mechanism, but that a mechanism won't work for an account.
1288 var missingDerivedSecrets bool
1289
1290 // For many failed auth attempts, slow down verification attempts.
1291 // Dropping the connection could also work, but more so when we have a connection rate limiter.
1292 // ../rfc/4954:770
1293 if c.authFailed > 3 && authFailDelay > 0 {
1294 // ../rfc/4954:770
1295 mox.Sleep(mox.Context, time.Duration(c.authFailed-3)*authFailDelay)
1296 }
1297 c.authFailed++ // Compensated on success.
1298 defer func() {
1299 if missingDerivedSecrets {
1300 c.authFailed--
1301 }
1302 // On the 3rd failed authentication, start responding slowly. Successful auth will
1303 // cause fast responses again.
1304 if c.authFailed >= 3 {
1305 c.setSlow(true)
1306 }
1307 }()
1308
1309 la := c.loginAttempt(true, "")
1310 defer func() {
1311 store.LoginAttemptAdd(context.Background(), c.logbg(), la)
1312 if la.Result == store.AuthSuccess {
1313 mox.LimiterFailedAuth.Reset(c.remoteIP, time.Now())
1314 } else if !missingDerivedSecrets {
1315 mox.LimiterFailedAuth.Add(c.remoteIP, time.Now(), 1)
1316 }
1317 }()
1318
1319 // ../rfc/4954:699
1320 p.xspace()
1321 mech := p.xsaslMech()
1322
1323 // Read the first parameter, either as initial parameter or by sending a
1324 // continuation with the optional encChal (must already be base64-encoded).
1325 xreadInitial := func(encChal string) []byte {
1326 var auth string
1327 if p.empty() {
1328 c.writelinef("%d %s", smtp.C334ContinueAuth, encChal) // ../rfc/4954:205
1329 // todo future: handle max length of 12288 octets and return proper responde codes otherwise ../rfc/4954:253
1330 auth = c.readline()
1331 if auth == "*" {
1332 // ../rfc/4954:193
1333 la.Result = store.AuthAborted
1334 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5Other0, "authentication aborted")
1335 }
1336 } else {
1337 p.xspace()
1338 if !mox.Pedantic {
1339 // Windows Mail 16005.14326.21606.0 sends two spaces between "AUTH PLAIN" and the
1340 // base64 data.
1341 for p.space() {
1342 }
1343 }
1344 auth = p.remainder()
1345 if auth == "" {
1346 // ../rfc/4954:235
1347 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5Syntax2, "missing initial auth base64 parameter after space")
1348 } else if auth == "=" {
1349 // ../rfc/4954:214
1350 auth = "" // Base64 decode below will result in empty buffer.
1351 }
1352 }
1353 buf, err := base64.StdEncoding.DecodeString(auth)
1354 if err != nil {
1355 // ../rfc/4954:235
1356 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5Syntax2, "invalid base64: %s", err)
1357 }
1358 return buf
1359 }
1360
1361 xreadContinuation := func() []byte {
1362 line := c.readline()
1363 if line == "*" {
1364 la.Result = store.AuthAborted
1365 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5Other0, "authentication aborted")
1366 }
1367 buf, err := base64.StdEncoding.DecodeString(line)
1368 if err != nil {
1369 // ../rfc/4954:235
1370 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5Syntax2, "invalid base64: %s", err)
1371 }
1372 return buf
1373 }
1374
1375 // The various authentication mechanisms set account and username. We may already
1376 // have an account and username from TLS client authentication. Afterwards, we
1377 // check that the account is the same.
1378 var account *store.Account
1379 var username string
1380 defer func() {
1381 if account != nil {
1382 err := account.Close()
1383 c.log.Check(err, "close account")
1384 }
1385 }()
1386
1387 switch mech {
1388 case "PLAIN":
1389 la.AuthMech = "plain"
1390
1391 // ../rfc/4954:343
1392 // ../rfc/4954:326
1393 if !c.tls && c.requireTLSForAuth {
1394 xsmtpUserErrorf(smtp.C538EncReqForAuth, smtp.SePol7EncReqForAuth11, "authentication requires tls")
1395 }
1396
1397 // Password is in line in plain text, so hide it.
1398 defer c.xtrace(mlog.LevelTraceauth)()
1399 buf := xreadInitial("")
1400 c.xtrace(mlog.LevelTrace) // Restore.
1401 plain := bytes.Split(buf, []byte{0})
1402 if len(plain) != 3 {
1403 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5BadParams4, "auth data should have 3 nul-separated tokens, got %d", len(plain))
1404 }
1405 authz := norm.NFC.String(string(plain[0]))
1406 username = norm.NFC.String(string(plain[1]))
1407 la.LoginAddress = username
1408 password := string(plain[2])
1409
1410 if authz != "" && authz != username {
1411 la.Result = store.AuthBadCredentials
1412 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "cannot assume other role")
1413 }
1414
1415 var err error
1416 account, la.AccountName, err = store.OpenEmailAuth(c.log, username, password, false)
1417 if err != nil && errors.Is(err, store.ErrUnknownCredentials) {
1418 // ../rfc/4954:274
1419 la.Result = store.AuthBadCredentials
1420 c.log.Info("failed authentication attempt", slog.String("username", username), slog.Any("remote", c.remoteIP))
1421 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass")
1422 }
1423 xcheckf(err, "verifying credentials")
1424
1425 case "LOGIN":
1426 // LOGIN is obsoleted in favor of PLAIN, only implemented to support legacy
1427 // clients, see Internet-Draft (I-D):
1428 // https://datatracker.ietf.org/doc/html/draft-murchison-sasl-login-00
1429
1430 la.LoginAddress = "login"
1431
1432 // ../rfc/4954:343
1433 // ../rfc/4954:326
1434 if !c.tls && c.requireTLSForAuth {
1435 xsmtpUserErrorf(smtp.C538EncReqForAuth, smtp.SePol7EncReqForAuth11, "authentication requires tls")
1436 }
1437
1438 // Read user name. The I-D says the client should ignore the server challenge, but
1439 // also that some clients may require challenge "Username:" instead of "User
1440 // Name". We can't sent both... Servers most commonly return "Username:" and
1441 // "Password:", so we do the same.
1442 // I-D says maximum length must be 64 bytes. We allow more, for long user names
1443 // (domains).
1444 encChal := base64.StdEncoding.EncodeToString([]byte("Username:"))
1445 username = string(xreadInitial(encChal))
1446 username = norm.NFC.String(username)
1447 la.LoginAddress = username
1448
1449 // Again, client should ignore the challenge, we send the same as the example in
1450 // the I-D.
1451 c.writelinef("%d %s", smtp.C334ContinueAuth, base64.StdEncoding.EncodeToString([]byte("Password:")))
1452
1453 // Password is in line in plain text, so hide it.
1454 defer c.xtrace(mlog.LevelTraceauth)()
1455 password := string(xreadContinuation())
1456 c.xtrace(mlog.LevelTrace) // Restore.
1457
1458 var err error
1459 account, la.AccountName, err = store.OpenEmailAuth(c.log, username, password, false)
1460 if err != nil && errors.Is(err, store.ErrUnknownCredentials) {
1461 // ../rfc/4954:274
1462 la.Result = store.AuthBadCredentials
1463 c.log.Info("failed authentication attempt", slog.String("username", username), slog.Any("remote", c.remoteIP))
1464 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass")
1465 }
1466 xcheckf(err, "verifying credentials")
1467
1468 case "CRAM-MD5":
1469 la.AuthMech = strings.ToLower(mech)
1470
1471 p.xempty()
1472
1473 // ../rfc/2195:82
1474 chal := fmt.Sprintf("<%d.%d@%s>", uint64(mox.CryptoRandInt()), time.Now().UnixNano(), mox.Conf.Static.HostnameDomain.ASCII)
1475 c.writelinef("%d %s", smtp.C334ContinueAuth, base64.StdEncoding.EncodeToString([]byte(chal)))
1476
1477 resp := xreadContinuation()
1478 t := strings.Split(string(resp), " ")
1479 if len(t) != 2 || len(t[1]) != 2*md5.Size {
1480 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5BadParams4, "malformed cram-md5 response")
1481 }
1482 username = norm.NFC.String(t[0])
1483 la.LoginAddress = username
1484 c.log.Debug("cram-md5 auth", slog.String("username", username))
1485 var err error
1486 account, la.AccountName, _, err = store.OpenEmail(c.log, username, false)
1487 if err != nil && errors.Is(err, store.ErrUnknownCredentials) {
1488 la.Result = store.AuthBadCredentials
1489 c.log.Info("failed authentication attempt", slog.String("username", username), slog.Any("remote", c.remoteIP))
1490 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass")
1491 }
1492 xcheckf(err, "looking up address")
1493 la.AccountName = account.Name
1494 var ipadhash, opadhash hash.Hash
1495 account.WithRLock(func() {
1496 err := account.DB.Read(context.TODO(), func(tx *bstore.Tx) error {
1497 password, err := bstore.QueryTx[store.Password](tx).Get()
1498 if err == bstore.ErrAbsent {
1499 c.log.Info("failed authentication attempt", slog.String("username", username), slog.Any("remote", c.remoteIP))
1500 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass")
1501 }
1502 if err != nil {
1503 return err
1504 }
1505
1506 ipadhash = password.CRAMMD5.Ipad
1507 opadhash = password.CRAMMD5.Opad
1508 return nil
1509 })
1510 xcheckf(err, "tx read")
1511 })
1512 if ipadhash == nil || opadhash == nil {
1513 missingDerivedSecrets = true
1514 c.log.Info("cram-md5 auth attempt without derived secrets set, save password again to store secrets", slog.String("username", username))
1515 c.log.Info("failed authentication attempt", slog.String("username", username), slog.Any("remote", c.remoteIP))
1516 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass")
1517 }
1518
1519 // ../rfc/2195:138 ../rfc/2104:142
1520 ipadhash.Write([]byte(chal))
1521 opadhash.Write(ipadhash.Sum(nil))
1522 digest := fmt.Sprintf("%x", opadhash.Sum(nil))
1523 if digest != t[1] {
1524 c.log.Info("failed authentication attempt", slog.String("username", username), slog.Any("remote", c.remoteIP))
1525 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass")
1526 }
1527
1528 case "SCRAM-SHA-256-PLUS", "SCRAM-SHA-256", "SCRAM-SHA-1-PLUS", "SCRAM-SHA-1":
1529 // 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?
1530 // todo: use single implementation between ../imapserver/server.go and ../smtpserver/server.go
1531
1532 // Passwords cannot be retrieved or replayed from the trace.
1533
1534 la.AuthMech = strings.ToLower(mech)
1535 var h func() hash.Hash
1536 switch la.AuthMech {
1537 case "scram-sha-1", "scram-sha-1-plus":
1538 h = sha1.New
1539 case "scram-sha-256", "scram-sha-256-plus":
1540 h = sha256.New
1541 default:
1542 xsmtpServerErrorf(codes{smtp.C554TransactionFailed, smtp.SeSys3Other0}, "missing scram auth method case")
1543 }
1544
1545 var cs *tls.ConnectionState
1546 channelBindingRequired := strings.HasSuffix(la.AuthMech, "-plus")
1547 if channelBindingRequired && !c.tls {
1548 // ../rfc/4954:630
1549 xsmtpUserErrorf(smtp.C538EncReqForAuth, smtp.SePol7EncReqForAuth11, "scram plus mechanism requires tls connection")
1550 }
1551 if c.tls {
1552 xcs := c.conn.(*tls.Conn).ConnectionState()
1553 cs = &xcs
1554 }
1555 c0 := xreadInitial("")
1556 ss, err := scram.NewServer(h, c0, cs, channelBindingRequired)
1557 if err != nil {
1558 c.log.Infox("scram protocol error", err, slog.Any("remote", c.remoteIP))
1559 xsmtpUserErrorf(smtp.C455BadParams, smtp.SePol7Other0, "scram protocol error: %s", err)
1560 }
1561 username = ss.Authentication
1562 la.LoginAddress = username
1563 c.log.Debug("scram auth", slog.String("authentication", username))
1564 account, la.AccountName, _, err = store.OpenEmail(c.log, username, false)
1565 if err != nil {
1566 // todo: we could continue scram with a generated salt, deterministically generated
1567 // from the username. that way we don't have to store anything but attackers cannot
1568 // learn if an account exists. same for absent scram saltedpassword below.
1569 c.log.Info("failed authentication attempt", slog.String("username", username), slog.Any("remote", c.remoteIP))
1570 xsmtpUserErrorf(smtp.C454TempAuthFail, smtp.SeSys3Other0, "scram not possible")
1571 }
1572 if ss.Authorization != "" && ss.Authorization != username {
1573 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "authentication with authorization for different user not supported")
1574 }
1575 var xscram store.SCRAM
1576 account.WithRLock(func() {
1577 err := account.DB.Read(context.TODO(), func(tx *bstore.Tx) error {
1578 password, err := bstore.QueryTx[store.Password](tx).Get()
1579 if err == bstore.ErrAbsent {
1580 c.log.Info("failed authentication attempt", slog.String("username", username), slog.Any("remote", c.remoteIP))
1581 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass")
1582 }
1583 xcheckf(err, "fetching credentials")
1584 switch la.AuthMech {
1585 case "scram-sha-1", "scram-sha-1-plus":
1586 xscram = password.SCRAMSHA1
1587 case "scram-sha-256", "scram-sha-256-plus":
1588 xscram = password.SCRAMSHA256
1589 default:
1590 xsmtpServerErrorf(codes{smtp.C554TransactionFailed, smtp.SeSys3Other0}, "missing scram auth credentials case")
1591 }
1592 if len(xscram.Salt) == 0 || xscram.Iterations == 0 || len(xscram.SaltedPassword) == 0 {
1593 missingDerivedSecrets = true
1594 c.log.Info("scram auth attempt without derived secrets set, save password again to store secrets", slog.String("address", username))
1595 c.log.Info("failed authentication attempt", slog.String("username", username), slog.Any("remote", c.remoteIP))
1596 xsmtpUserErrorf(smtp.C454TempAuthFail, smtp.SeSys3Other0, "scram not possible")
1597 }
1598 return nil
1599 })
1600 xcheckf(err, "read tx")
1601 })
1602 s1, err := ss.ServerFirst(xscram.Iterations, xscram.Salt)
1603 xcheckf(err, "scram first server step")
1604 c.writelinef("%d %s", smtp.C334ContinueAuth, base64.StdEncoding.EncodeToString([]byte(s1))) // ../rfc/4954:187
1605 c2 := xreadContinuation()
1606 s3, err := ss.Finish(c2, xscram.SaltedPassword)
1607 if len(s3) > 0 {
1608 c.writelinef("%d %s", smtp.C334ContinueAuth, base64.StdEncoding.EncodeToString([]byte(s3))) // ../rfc/4954:187
1609 }
1610 if err != nil {
1611 c.readline() // Should be "*" for cancellation.
1612 if errors.Is(err, scram.ErrInvalidProof) {
1613 la.Result = store.AuthBadCredentials
1614 c.log.Info("failed authentication attempt", slog.String("username", username), slog.Any("remote", c.remoteIP))
1615 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad credentials")
1616 } else if errors.Is(err, scram.ErrChannelBindingsDontMatch) {
1617 la.Result = store.AuthBadChannelBinding
1618 c.log.Warn("bad channel binding during authentication, potential mitm", slog.String("username", username), slog.Any("remote", c.remoteIP))
1619 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7MsgIntegrity7, "channel bindings do not match, potential mitm")
1620 } else if errors.Is(err, scram.ErrInvalidEncoding) {
1621 la.Result = store.AuthBadProtocol
1622 c.log.Infox("bad scram protocol message", err, slog.String("username", username), slog.Any("remote", c.remoteIP))
1623 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7Other0, "bad scram protocol message")
1624 }
1625 xcheckf(err, "server final")
1626 }
1627
1628 // Client must still respond, but there is nothing to say. See ../rfc/9051:6221
1629 // The message should be empty. todo: should we require it is empty?
1630 xreadContinuation()
1631
1632 case "EXTERNAL":
1633 la.AuthMech = "external"
1634
1635 // ../rfc/4422:1618
1636 buf := xreadInitial("")
1637 username = norm.NFC.String(string(buf))
1638 la.LoginAddress = username
1639
1640 if !c.tls {
1641 // ../rfc/4954:630
1642 xsmtpUserErrorf(smtp.C538EncReqForAuth, smtp.SePol7EncReqForAuth11, "tls required for tls client certificate authentication")
1643 }
1644 if c.account == nil {
1645 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "missing client certificate, required for tls client certificate authentication")
1646 }
1647
1648 if username == "" {
1649 username = c.username
1650 la.LoginAddress = username
1651 }
1652 var err error
1653 account, la.AccountName, _, err = store.OpenEmail(c.log, username, false)
1654 xcheckf(err, "looking up username from tls client authentication")
1655
1656 default:
1657 la.AuthMech = "(unrecognized)"
1658 // ../rfc/4954:176
1659 xsmtpUserErrorf(smtp.C504ParamNotImpl, smtp.SeProto5BadParams4, "mechanism %s not supported", mech)
1660 }
1661
1662 if accConf, ok := account.Conf(); !ok {
1663 xcheckf(errors.New("cannot find account"), "get account config")
1664 } else if accConf.LoginDisabled != "" {
1665 la.Result = store.AuthLoginDisabled
1666 c.log.Info("account login disabled", slog.String("username", username))
1667 xsmtpUserErrorf(smtp.C525AccountDisabled, smtp.SePol7AccountDisabled13, "%w: %s", store.ErrLoginDisabled, accConf.LoginDisabled)
1668 }
1669
1670 // We may already have TLS credentials. We allow an additional SASL authentication,
1671 // possibly with different username, but the account must be the same.
1672 if c.account != nil {
1673 if account != c.account {
1674 c.log.Debug("sasl authentication for different account than tls client authentication, aborting connection",
1675 slog.String("saslmechanism", la.AuthMech),
1676 slog.String("saslaccount", account.Name),
1677 slog.String("tlsaccount", c.account.Name),
1678 slog.String("saslusername", username),
1679 slog.String("tlsusername", c.username),
1680 )
1681 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "authentication failed, tls client certificate public key belongs to another account")
1682 } else if username != c.username {
1683 c.log.Debug("sasl authentication for different username than tls client certificate authentication, switching to sasl username",
1684 slog.String("saslmechanism", la.AuthMech),
1685 slog.String("saslusername", username),
1686 slog.String("tlsusername", c.username),
1687 slog.String("account", c.account.Name),
1688 )
1689 }
1690 } else {
1691 c.account = account
1692 account = nil // Prevent cleanup.
1693 }
1694 c.username = username
1695
1696 la.LoginAddress = c.username
1697 la.AccountName = c.account.Name
1698 la.Result = store.AuthSuccess
1699 c.authSASL = true
1700 c.authFailed = 0
1701 c.setSlow(false)
1702 // ../rfc/4954:276
1703 c.writecodeline(smtp.C235AuthSuccess, smtp.SePol7Other0, "nice", nil)
1704}
1705
1706// ../rfc/5321:1879 ../rfc/5321:1025
1707func (c *conn) cmdMail(p *parser) {
1708 // requirements for maximum line length:
1709 // ../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)
1710 // todo future: enforce? doesn't really seem worth it...
1711
1712 if c.transactionBad > 10 && c.transactionGood == 0 {
1713 // If we get many bad transactions, it's probably a spammer that is guessing user names.
1714 // Useful in combination with rate limiting.
1715 // ../rfc/5321:4349
1716 c.writecodeline(smtp.C550MailboxUnavail, smtp.SeAddr1Other0, "too many failures", nil)
1717 panic(errIO)
1718 }
1719
1720 c.xneedHello()
1721 c.xcheckAuth()
1722 if c.mailFrom != nil {
1723 // ../rfc/5321:2507, though ../rfc/5321:1029 contradicts, implying a MAIL would also reset, but ../rfc/5321:1160 decides.
1724 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "already have MAIL")
1725 }
1726 // Ensure clear transaction state on failure.
1727 defer func() {
1728 x := recover()
1729 if x != nil {
1730 // ../rfc/5321:2514
1731 c.rset()
1732 panic(x)
1733 }
1734 }()
1735 p.xtake(" FROM:")
1736 // note: no space allowed after colon. ../rfc/5321:1093
1737 // Microsoft Outlook 365 Apps for Enterprise sends it with submission. For delivery
1738 // it is mostly used by spammers, but has been seen with legitimate senders too.
1739 if !mox.Pedantic {
1740 p.space()
1741 }
1742 rawRevPath := p.xrawReversePath()
1743 paramSeen := map[string]bool{}
1744 for p.space() {
1745 // ../rfc/5321:2273
1746 key := p.xparamKeyword()
1747
1748 K := strings.ToUpper(key)
1749 if paramSeen[K] {
1750 // e.g. ../rfc/6152:128
1751 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5BadParams4, "duplicate param %q", key)
1752 }
1753 paramSeen[K] = true
1754
1755 switch K {
1756 case "SIZE":
1757 p.xtake("=")
1758 size := p.xnumber(20, true) // ../rfc/1870:90
1759 if size > c.maxMessageSize {
1760 // ../rfc/1870:136 ../rfc/3463:382
1761 ecode := smtp.SeSys3MsgLimitExceeded4
1762 if size < config.DefaultMaxMsgSize {
1763 ecode = smtp.SeMailbox2MsgLimitExceeded3
1764 }
1765 xsmtpUserErrorf(smtp.C552MailboxFull, ecode, "message too large")
1766 }
1767 // We won't verify the message is exactly the size the remote claims. Buf if it is
1768 // larger, we'll abort the transaction when remote crosses the boundary.
1769 case "BODY":
1770 p.xtake("=")
1771 // ../rfc/6152:90
1772 v := p.xparamValue()
1773 switch strings.ToUpper(v) {
1774 case "7BIT":
1775 c.has8bitmime = false
1776 case "8BITMIME":
1777 c.has8bitmime = true
1778 default:
1779 xsmtpUserErrorf(smtp.C555UnrecognizedAddrParams, smtp.SeProto5BadParams4, "unrecognized parameter %q", key)
1780 }
1781 case "AUTH":
1782 // ../rfc/4954:455
1783
1784 // We act as if we don't trust the client to specify a mailbox. Instead, we always
1785 // check the rfc5321.mailfrom and rfc5322.from before accepting the submission.
1786 // ../rfc/4954:538
1787
1788 // ../rfc/4954:704
1789 // 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
1790 p.xtake("=")
1791 p.xtake("<")
1792 p.xtext()
1793 p.xtake(">")
1794 case "SMTPUTF8":
1795 // ../rfc/6531:213
1796 c.smtputf8 = true
1797 c.msgsmtputf8 = true
1798 case "REQUIRETLS":
1799 // ../rfc/8689:155
1800 if !c.tls {
1801 xsmtpUserErrorf(smtp.C523EncryptionNeeded, smtp.SePol7EncNeeded10, "requiretls only allowed on tls-encrypted connections")
1802 } else if !c.extRequireTLS {
1803 xsmtpUserErrorf(smtp.C555UnrecognizedAddrParams, smtp.SeSys3NotSupported3, "REQUIRETLS not allowed for this connection")
1804 }
1805 v := true
1806 c.requireTLS = &v
1807 case "HOLDFOR", "HOLDUNTIL":
1808 // Only for submission ../rfc/4865:163
1809 if !c.submission {
1810 xsmtpUserErrorf(smtp.C555UnrecognizedAddrParams, smtp.SeSys3NotSupported3, "unrecognized parameter %q", key)
1811 }
1812 if K == "HOLDFOR" && paramSeen["HOLDUNTIL"] || K == "HOLDUNTIL" && paramSeen["HOLDFOR"] {
1813 // ../rfc/4865:260
1814 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5BadParams4, "cannot use both HOLDUNTIL and HOLFOR")
1815 }
1816 p.xtake("=")
1817 // ../rfc/4865:263 ../rfc/4865:267 We are not following the advice of treating
1818 // semantic errors as syntax errors
1819 if K == "HOLDFOR" {
1820 n := p.xnumber(9, false) // ../rfc/4865:92
1821 if n > int64(queue.FutureReleaseIntervalMax/time.Second) {
1822 // ../rfc/4865:250
1823 xsmtpUserErrorf(smtp.C554TransactionFailed, smtp.SeProto5BadParams4, "future release interval too far in the future")
1824 }
1825 c.futureRelease = time.Now().Add(time.Duration(n) * time.Second)
1826 c.futureReleaseRequest = fmt.Sprintf("for;%d", n)
1827 } else {
1828 t, s := p.xdatetimeutc()
1829 ival := time.Until(t)
1830 if ival <= 0 {
1831 // Likely a mistake by the user.
1832 xsmtpUserErrorf(smtp.C554TransactionFailed, smtp.SeProto5BadParams4, "requested future release time is in the past")
1833 } else if ival > queue.FutureReleaseIntervalMax {
1834 // ../rfc/4865:255
1835 xsmtpUserErrorf(smtp.C554TransactionFailed, smtp.SeProto5BadParams4, "requested future release time is too far in the future")
1836 }
1837 c.futureRelease = t
1838 c.futureReleaseRequest = "until;" + s
1839 }
1840 default:
1841 // ../rfc/5321:2230
1842 xsmtpUserErrorf(smtp.C555UnrecognizedAddrParams, smtp.SeSys3NotSupported3, "unrecognized parameter %q", key)
1843 }
1844 }
1845
1846 // We now know if we have to parse the address with support for utf8.
1847 pp := newParser(rawRevPath, c.smtputf8, c)
1848 rpath := pp.xbareReversePath()
1849 pp.xempty()
1850 pp = nil
1851 p.xend()
1852
1853 // For submission, check if reverse path is allowed. I.e. authenticated account
1854 // must have the rpath configured. We do a check again on rfc5322.from during DATA.
1855 // Mail clients may use the alias address as smtp mail from address, so we allow it
1856 // for such aliases.
1857 rpathAllowed := func(disabled *bool) bool {
1858 // ../rfc/6409:349
1859 if rpath.IsZero() {
1860 return true
1861 }
1862
1863 from := smtp.NewAddress(rpath.Localpart, rpath.IPDomain.Domain)
1864 ok, dis := mox.AllowMsgFrom(c.account.Name, from)
1865 *disabled = dis
1866 return ok
1867 }
1868
1869 if !c.submission && !rpath.IPDomain.Domain.IsZero() {
1870 // If rpath domain has null MX record or is otherwise not accepting email, reject.
1871 // ../rfc/7505:181
1872 // ../rfc/5321:4045
1873 cidctx := context.WithValue(mox.Context, mlog.CidKey, c.cid)
1874 ctx, cancel := context.WithTimeout(cidctx, time.Minute)
1875 valid, err := checkMXRecords(ctx, c.resolver, rpath.IPDomain.Domain)
1876 cancel()
1877 if err != nil {
1878 c.log.Infox("temporary reject for temporary mx lookup error", err)
1879 xsmtpServerErrorf(codes{smtp.C451LocalErr, smtp.SeNet4Other0}, "cannot verify mx records for mailfrom domain")
1880 } else if !valid {
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.r)
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.LocalpartCatchallSeparator, 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.LocalpartCatchallSeparator + 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 = append([]message.AuthMethod{}, 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("Notjunk", true)
3276 q.FilterEqual("IsReject", false)
3277 exists, err := q.Exists()
3278 if err != nil {
3279 return fmt.Errorf("querying for non-junk message from organizational domain: %v", err)
3280 }
3281 if exists {
3282 return nil
3283 }
3284
3285 // See if we sent a message to this organizational domain.
3286 qr := bstore.QueryTx[store.Recipient](tx)
3287 qr.FilterNonzero(store.Recipient{OrgDomain: a0.d.m.MsgFromOrgDomain})
3288 exists, err = qr.Exists()
3289 if err != nil {
3290 return fmt.Errorf("querying for message sent to organizational domain: %v", err)
3291 }
3292 if !exists {
3293 unknown = true
3294 }
3295 return nil
3296 })
3297 if err != nil {
3298 log.Errorx("checking if sender is unknown domain, for dmarc aggregate report evaluation", err)
3299 }
3300 return
3301 }
3302
3303 r := dmarcResult.Record
3304 addresses := make([]string, len(r.AggregateReportAddresses))
3305 for i, a := range r.AggregateReportAddresses {
3306 addresses[i] = a.String()
3307 }
3308 sp := dmarcrpt.Disposition(r.SubdomainPolicy)
3309 if r.SubdomainPolicy == dmarc.PolicyEmpty {
3310 sp = dmarcrpt.Disposition(r.Policy)
3311 }
3312 eval := dmarcdb.Evaluation{
3313 // Evaluated and IntervalHours set by AddEvaluation.
3314 PolicyDomain: dmarcResult.Domain.Name(),
3315
3316 // Optional evaluations don't cause a report to be sent, but will be included.
3317 // Useful for automated inter-mailer messages, we don't want to get in a reporting
3318 // loop. We also don't want to be used for sending reports to unsuspecting domains
3319 // we have no relation with.
3320 // 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.
3321 Optional: a0.d.destination.DMARCReports || a0.d.destination.HostTLSReports || a0.d.destination.DomainTLSReports || a0.reason == reasonDMARCPolicy && unknownDomain(),
3322
3323 Addresses: addresses,
3324
3325 PolicyPublished: dmarcrpt.PolicyPublished{
3326 Domain: dmarcResult.Domain.Name(),
3327 ADKIM: dmarcrpt.Alignment(r.ADKIM),
3328 ASPF: dmarcrpt.Alignment(r.ASPF),
3329 Policy: dmarcrpt.Disposition(r.Policy),
3330 SubdomainPolicy: sp,
3331 Percentage: r.Percentage,
3332 // We don't save ReportingOptions, we don't do per-message failure reporting.
3333 },
3334 SourceIP: c.remoteIP.String(),
3335 Disposition: disposition,
3336 AlignedDKIMPass: dmarcResult.AlignedDKIMPass,
3337 AlignedSPFPass: dmarcResult.AlignedSPFPass,
3338 EnvelopeTo: rcpt.Addr.IPDomain.String(),
3339 EnvelopeFrom: c.mailFrom.IPDomain.String(),
3340 HeaderFrom: msgFrom.Domain.Name(),
3341 }
3342
3343 for _, s := range dmarcOverrides {
3344 reason := dmarcrpt.PolicyOverrideReason{Type: dmarcrpt.PolicyOverride(s)}
3345 eval.OverrideReasons = append(eval.OverrideReasons, reason)
3346 }
3347
3348 // We'll include all signatures for the organizational domain, even if they weren't
3349 // relevant due to strict alignment requirement.
3350 for _, dkimResult := range dkimResults {
3351 if dkimResult.Sig == nil || publicsuffix.Lookup(ctx, log.Logger, msgFrom.Domain) != publicsuffix.Lookup(ctx, log.Logger, dkimResult.Sig.Domain) {
3352 continue
3353 }
3354 r := dmarcrpt.DKIMAuthResult{
3355 Domain: dkimResult.Sig.Domain.Name(),
3356 Selector: dkimResult.Sig.Selector.ASCII,
3357 Result: dmarcrpt.DKIMResult(dkimResult.Status),
3358 }
3359 eval.DKIMResults = append(eval.DKIMResults, r)
3360 }
3361
3362 switch receivedSPF.Identity {
3363 case spf.ReceivedHELO:
3364 spfAuthResult := dmarcrpt.SPFAuthResult{
3365 Domain: spfArgs.HelloDomain.String(), // Can be unicode and also IP.
3366 Scope: dmarcrpt.SPFDomainScopeHelo,
3367 Result: dmarcrpt.SPFResult(receivedSPF.Result),
3368 }
3369 eval.SPFResults = []dmarcrpt.SPFAuthResult{spfAuthResult}
3370 case spf.ReceivedMailFrom:
3371 spfAuthResult := dmarcrpt.SPFAuthResult{
3372 Domain: spfArgs.MailFromDomain.Name(), // Can be unicode.
3373 Scope: dmarcrpt.SPFDomainScopeMailFrom,
3374 Result: dmarcrpt.SPFResult(receivedSPF.Result),
3375 }
3376 eval.SPFResults = []dmarcrpt.SPFAuthResult{spfAuthResult}
3377 }
3378
3379 err := dmarcdb.AddEvaluation(ctx, dmarcResult.Record.AggregateReportingInterval, &eval)
3380 log.Check(err, "adding dmarc evaluation to database for aggregate report")
3381 }
3382
3383 if !a0.accept {
3384 for _, a := range la {
3385 // Don't add message if address was also explicitly present in a RCPT TO command.
3386 if rcpt.Alias != nil && regularRecipient(a.d.deliverTo) {
3387 continue
3388 }
3389
3390 conf, _ := a.d.acc.Conf()
3391 if conf.RejectsMailbox == "" {
3392 continue
3393 }
3394 present, _, messagehash, err := rejectPresent(log, a.d.acc, conf.RejectsMailbox, a.d.m, dataFile)
3395 if err != nil {
3396 log.Errorx("checking whether reject is already present", err)
3397 continue
3398 } else if present {
3399 log.Info("reject message is already present, ignoring")
3400 continue
3401 }
3402 a.d.m.IsReject = true
3403 a.d.m.Seen = true // We don't want to draw attention.
3404 // Regular automatic junk flags configuration applies to these messages. The
3405 // default is to treat these as neutral, so they won't cause outright rejections
3406 // due to reputation for later delivery attempts.
3407 a.d.m.MessageHash = messagehash
3408 a.d.acc.WithWLock(func() {
3409 hasSpace := true
3410 var err error
3411 if !conf.KeepRejects {
3412 hasSpace, err = a.d.acc.TidyRejectsMailbox(c.log, conf.RejectsMailbox)
3413 }
3414 if err != nil {
3415 log.Errorx("tidying rejects mailbox", err)
3416 } else if hasSpace {
3417 if err := a.d.acc.DeliverMailbox(log, conf.RejectsMailbox, a.d.m, dataFile); err != nil {
3418 log.Errorx("delivering spammy mail to rejects mailbox", err)
3419 } else {
3420 log.Info("delivered spammy mail to rejects mailbox")
3421 }
3422 } else {
3423 log.Info("not storing spammy mail to full rejects mailbox")
3424 }
3425 })
3426 }
3427
3428 log.Info("incoming message rejected", slog.String("reason", a0.reason), slog.Any("msgfrom", msgFrom))
3429 metricDelivery.WithLabelValues("reject", a0.reason).Inc()
3430 c.setSlow(true)
3431 addError(rcpt, a0.code, a0.secode, a0.userError, a0.errmsg)
3432 return
3433 }
3434
3435 delayFirstTime := true
3436 if rcpt.Account != nil && a0.dmarcReport != nil {
3437 // todo future: add rate limiting to prevent DoS attacks. ../rfc/7489:2570
3438 if err := dmarcdb.AddReport(ctx, a0.dmarcReport, msgFrom.Domain); err != nil {
3439 log.Errorx("saving dmarc aggregate report in database", err)
3440 } else {
3441 log.Info("dmarc aggregate report processed")
3442 a0.d.m.Flags.Seen = true
3443 delayFirstTime = false
3444 }
3445 }
3446 if rcpt.Account != nil && a0.tlsReport != nil {
3447 // todo future: add rate limiting to prevent DoS attacks.
3448 if err := tlsrptdb.AddReport(ctx, c.log, msgFrom.Domain, c.mailFrom.String(), a0.d.destination.HostTLSReports, a0.tlsReport); err != nil {
3449 log.Errorx("saving TLSRPT report in database", err)
3450 } else {
3451 log.Info("tlsrpt report processed")
3452 a0.d.m.Flags.Seen = true
3453 delayFirstTime = false
3454 }
3455 }
3456
3457 // If this is a first-time sender and not a forwarded/mailing list message, wait
3458 // before actually delivering. If this turns out to be a spammer, we've kept one of
3459 // their connections busy.
3460 a0conf, _ := a0.d.acc.Conf()
3461 if delayFirstTime && !a0.d.m.IsForward && !a0.d.m.IsMailingList && a0.reason == reasonNoBadSignals && !a0conf.NoFirstTimeSenderDelay && c.firstTimeSenderDelay > 0 {
3462 log.Debug("delaying before delivering from sender without reputation", slog.Duration("delay", c.firstTimeSenderDelay))
3463 mox.Sleep(mox.Context, c.firstTimeSenderDelay)
3464 }
3465
3466 if Localserve {
3467 code, timeout := mox.LocalserveNeedsError(rcpt.Addr.Localpart)
3468 if timeout {
3469 log.Info("timing out due to special localpart")
3470 mox.Sleep(mox.Context, time.Hour)
3471 xsmtpServerErrorf(codes{smtp.C451LocalErr, smtp.SeOther00}, "timing out delivery due to special localpart")
3472 } else if code != 0 {
3473 log.Info("failure due to special localpart", slog.Int("code", code))
3474 metricDelivery.WithLabelValues("delivererror", "localserve").Inc()
3475 addError(rcpt, code, smtp.SeOther00, false, fmt.Sprintf("failure with code %d due to special localpart", code))
3476 return
3477 }
3478 }
3479
3480 // Gather the message-id before we deliver and the file may be consumed.
3481 if !parsedMessageID {
3482 if p, err := message.Parse(c.log.Logger, false, store.FileMsgReader(a0.d.m.MsgPrefix, dataFile)); err != nil {
3483 log.Infox("parsing message for message-id", err)
3484 } else if header, err := p.Header(); err != nil {
3485 log.Infox("parsing message header for message-id", err)
3486 } else {
3487 messageID = header.Get("Message-Id")
3488 }
3489 parsedMessageID = true
3490 }
3491
3492 // Finally deliver the message to the account(s).
3493 var nerr int // Number of non-quota errors.
3494 var nfull int // Number of failed deliveries due to over quota.
3495 var ndelivered int // Number delivered to account.
3496 for _, a := range la {
3497 // Don't deliver to recipient that was explicitly present in SMTP transaction, or
3498 // is sending the message to an alias they are member of.
3499 if rcpt.Alias != nil && (regularRecipient(a.d.deliverTo) || a.d.deliverTo.Equal(msgFrom.Path())) {
3500 continue
3501 }
3502
3503 var delivered bool
3504 a.d.acc.WithWLock(func() {
3505 if err := a.d.acc.DeliverMailbox(log, a.mailbox, a.d.m, dataFile); err != nil {
3506 log.Errorx("delivering", err)
3507 metricDelivery.WithLabelValues("delivererror", a0.reason).Inc()
3508 if errors.Is(err, store.ErrOverQuota) {
3509 nfull++
3510 } else {
3511 addError(rcpt, smtp.C451LocalErr, smtp.SeSys3Other0, false, "error processing")
3512 nerr++
3513 }
3514 return
3515 }
3516 delivered = true
3517 ndelivered++
3518 metricDelivery.WithLabelValues("delivered", a0.reason).Inc()
3519 log.Info("incoming message delivered", slog.String("reason", a0.reason), slog.Any("msgfrom", msgFrom))
3520
3521 conf, _ := a.d.acc.Conf()
3522 if conf.RejectsMailbox != "" && a.d.m.MessageID != "" {
3523 if err := a.d.acc.RejectsRemove(log, conf.RejectsMailbox, a.d.m.MessageID); err != nil {
3524 log.Errorx("removing message from rejects mailbox", err, slog.String("messageid", messageID))
3525 }
3526 }
3527 })
3528
3529 // Pass delivered messages to queue for DSN processing and/or hooks.
3530 if delivered {
3531 mr := store.FileMsgReader(a.d.m.MsgPrefix, dataFile)
3532 part, err := a.d.m.LoadPart(mr)
3533 if err != nil {
3534 log.Errorx("loading parsed part for evaluating webhook", err)
3535 } else {
3536 err = queue.Incoming(context.Background(), log, a.d.acc, messageID, *a.d.m, part, a.mailbox)
3537 log.Check(err, "queueing webhook for incoming delivery")
3538 }
3539 } else if nerr > 0 && ndelivered == 0 {
3540 // Don't continue if we had an error and haven't delivered yet. If we only had
3541 // quota-related errors, we keep trying for an account to deliver to.
3542 break
3543 }
3544 }
3545 if ndelivered == 0 && (nerr > 0 || nfull > 0) {
3546 if nerr == 0 {
3547 addError(rcpt, smtp.C452StorageFull, smtp.SeMailbox2Full2, true, "account storage full")
3548 } else {
3549 addError(rcpt, smtp.C451LocalErr, smtp.SeSys3Other0, false, "error processing")
3550 }
3551 }
3552 }
3553
3554 // For each recipient, do final spam analysis and delivery.
3555 for _, rcpt := range c.recipients {
3556 processRecipient(rcpt)
3557 }
3558
3559 // If all recipients failed to deliver, return an error.
3560 if len(c.recipients) == len(deliverErrors) {
3561 same := true
3562 e0 := deliverErrors[0]
3563 var serverError bool
3564 var msgs []string
3565 major := 4
3566 for _, e := range deliverErrors {
3567 serverError = serverError || !e.userError
3568 if e.code != e0.code || e.secode != e0.secode {
3569 same = false
3570 }
3571 msgs = append(msgs, e.errmsg)
3572 if e.code >= 500 {
3573 major = 5
3574 }
3575 }
3576 if same {
3577 xsmtpErrorf(e0.code, e0.secode, !serverError, "%s", strings.Join(msgs, "\n"))
3578 }
3579
3580 // Not all failures had the same error. We'll return each error on a separate line.
3581 lines := []string{}
3582 for _, e := range deliverErrors {
3583 s := fmt.Sprintf("%d %d.%s %s", e.code, e.code/100, e.secode, e.errmsg)
3584 lines = append(lines, s)
3585 }
3586 code := smtp.C451LocalErr
3587 secode := smtp.SeSys3Other0
3588 if major == 5 {
3589 code = smtp.C554TransactionFailed
3590 }
3591 lines = append(lines, "multiple errors")
3592 xsmtpErrorf(code, secode, !serverError, "%s", strings.Join(lines, "\n"))
3593 }
3594 // Generate one DSN for all failed recipients.
3595 if len(deliverErrors) > 0 {
3596 now := time.Now()
3597 dsnMsg := dsn.Message{
3598 SMTPUTF8: c.msgsmtputf8,
3599 From: smtp.Path{Localpart: "postmaster", IPDomain: deliverErrors[0].rcptTo.IPDomain},
3600 To: *c.mailFrom,
3601 Subject: "mail delivery failure",
3602 MessageID: mox.MessageIDGen(false),
3603 References: messageID,
3604
3605 // Per-message details.
3606 ReportingMTA: mox.Conf.Static.HostnameDomain.ASCII,
3607 ReceivedFromMTA: smtp.Ehlo{Name: c.hello, ConnIP: c.remoteIP},
3608 ArrivalDate: now,
3609 }
3610
3611 if len(deliverErrors) > 1 {
3612 dsnMsg.TextBody = "Multiple delivery failures occurred.\n\n"
3613 }
3614
3615 for _, e := range deliverErrors {
3616 kind := "Permanent"
3617 if e.code/100 == 4 {
3618 kind = "Transient"
3619 }
3620 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))
3621 rcpt := dsn.Recipient{
3622 FinalRecipient: e.rcptTo,
3623 Action: dsn.Failed,
3624 Status: fmt.Sprintf("%d.%s", e.code/100, e.secode),
3625 LastAttemptDate: now,
3626 }
3627 dsnMsg.Recipients = append(dsnMsg.Recipients, rcpt)
3628 }
3629
3630 header, err := message.ReadHeaders(bufio.NewReader(&moxio.AtReader{R: dataFile}))
3631 if err != nil {
3632 c.log.Errorx("reading headers of incoming message for dsn, continuing dsn without headers", err)
3633 }
3634 dsnMsg.Original = header
3635
3636 if Localserve {
3637 c.log.Error("not queueing dsn for incoming delivery due to localserve")
3638 } else if err := queueDSN(context.TODO(), c.log, c, *c.mailFrom, dsnMsg, c.requireTLS != nil && *c.requireTLS); err != nil {
3639 metricServerErrors.WithLabelValues("queuedsn").Inc()
3640 c.log.Errorx("queuing DSN for incoming delivery, no DSN sent", err)
3641 }
3642 }
3643
3644 c.transactionGood++
3645 c.transactionBad-- // Compensate for early earlier pessimistic increase.
3646 c.rset()
3647 c.writecodeline(smtp.C250Completed, smtp.SeMailbox2Other0, "it is done", nil)
3648}
3649
3650// Return whether msgFrom address is allowed to send a message to alias.
3651func aliasAllowedMsgFrom(alias config.Alias, msgFrom smtp.Address) bool {
3652 for _, aa := range alias.ParsedAddresses {
3653 if aa.Address == msgFrom {
3654 return true
3655 }
3656 }
3657 lp, err := smtp.ParseLocalpart(alias.LocalpartStr)
3658 xcheckf(err, "parsing alias localpart")
3659 if msgFrom == smtp.NewAddress(lp, alias.Domain) {
3660 return alias.AllowMsgFrom
3661 }
3662 return alias.PostPublic
3663}
3664
3665// ecode returns either ecode, or a more specific error based on err.
3666// For example, ecode can be turned from an "other system" error into a "mail
3667// system full" if the error indicates no disk space is available.
3668func errCodes(code int, ecode string, err error) codes {
3669 switch {
3670 case moxio.IsStorageSpace(err):
3671 switch ecode {
3672 case smtp.SeMailbox2Other0:
3673 if code == smtp.C451LocalErr {
3674 code = smtp.C452StorageFull
3675 }
3676 ecode = smtp.SeMailbox2Full2
3677 case smtp.SeSys3Other0:
3678 if code == smtp.C451LocalErr {
3679 code = smtp.C452StorageFull
3680 }
3681 ecode = smtp.SeSys3StorageFull1
3682 }
3683 }
3684 return codes{code, ecode}
3685}
3686
3687// ../rfc/5321:2079
3688func (c *conn) cmdRset(p *parser) {
3689 // ../rfc/5321:2106
3690 p.xend()
3691
3692 c.rset()
3693 c.bwritecodeline(smtp.C250Completed, smtp.SeOther00, "all clear", nil)
3694}
3695
3696// ../rfc/5321:2108 ../rfc/5321:1222
3697func (c *conn) cmdVrfy(p *parser) {
3698 // No EHLO/HELO needed.
3699 // ../rfc/5321:2448
3700
3701 // ../rfc/5321:2119 ../rfc/6531:641
3702 p.xspace()
3703 p.xstring()
3704 if p.space() {
3705 p.xtake("SMTPUTF8")
3706 }
3707 p.xend()
3708
3709 // todo future: we could support vrfy and expn for submission? though would need to see if its rfc defines it.
3710
3711 // ../rfc/5321:4239
3712 xsmtpUserErrorf(smtp.C252WithoutVrfy, smtp.SePol7Other0, "no verify but will try delivery")
3713}
3714
3715// ../rfc/5321:2135 ../rfc/5321:1272
3716func (c *conn) cmdExpn(p *parser) {
3717 // No EHLO/HELO needed.
3718 // ../rfc/5321:2448
3719
3720 // ../rfc/5321:2149 ../rfc/6531:645
3721 p.xspace()
3722 p.xstring()
3723 if p.space() {
3724 p.xtake("SMTPUTF8")
3725 }
3726 p.xend()
3727
3728 // todo: we could implement expn for local aliases for authenticated users, when members have permission to list. would anyone use it?
3729
3730 // ../rfc/5321:4239
3731 xsmtpUserErrorf(smtp.C252WithoutVrfy, smtp.SePol7Other0, "no expand but will try delivery")
3732}
3733
3734// ../rfc/5321:2151
3735func (c *conn) cmdHelp(p *parser) {
3736 // Let's not strictly parse the request for help. We are ignoring the text anyway.
3737 // ../rfc/5321:2166
3738
3739 c.bwritecodeline(smtp.C214Help, smtp.SeOther00, "see rfc 5321 (smtp)", nil)
3740}
3741
3742// ../rfc/5321:2191
3743func (c *conn) cmdNoop(p *parser) {
3744 // No idea why, but if an argument follows, it must adhere to the string ABNF production...
3745 // ../rfc/5321:2203
3746 if p.space() {
3747 p.xstring()
3748 }
3749 p.xend()
3750
3751 c.bwritecodeline(smtp.C250Completed, smtp.SeOther00, "alrighty", nil)
3752}
3753
3754// ../rfc/5321:2205
3755func (c *conn) cmdQuit(p *parser) {
3756 // ../rfc/5321:2226
3757 p.xend()
3758
3759 c.writecodeline(smtp.C221Closing, smtp.SeOther00, "okay thanks bye", nil)
3760 panic(cleanClose)
3761}
3762