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