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