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