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