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