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