1// Package smtpserver implements an SMTP server for submission and incoming delivery of mail messages.
10 cryptorand "crypto/rand"
33 "golang.org/x/exp/maps"
34 "golang.org/x/text/unicode/norm"
36 "github.com/prometheus/client_golang/prometheus"
37 "github.com/prometheus/client_golang/prometheus/promauto"
39 "github.com/mjl-/bstore"
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"
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")
69// If set, regular delivery/submit is sidestepped, email is accepted and
70// delivered to the account named mox.
73var limiterConnectionRate, limiterConnections *ratelimit.Limiter
75// For delivery rate limiting. Variable because changed during tests.
76var limitIPMasked1MessagesPerMinute int = 500
77var limitIPMasked1SizePerMinute int64 = 1000 * 1024 * 1024
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
84 // Also called by tests, so they don't trigger the rate limiter.
90 // todo future: make these configurable
91 limiterConnectionRate = &ratelimit.Limiter{
92 WindowLimits: []ratelimit.WindowLimit{
95 Limits: [...]int64{300, 900, 2700},
99 limiterConnections = &ratelimit.Limiter{
100 WindowLimits: []ratelimit.WindowLimit{
102 Window: time.Duration(math.MaxInt64), // All of time.
103 Limits: [...]int64{30, 90, 270},
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.
119 secode string // Enhanced code, but without the leading major int from code.
123 metricConnection = promauto.NewCounterVec(
124 prometheus.CounterOpts{
125 Name: "mox_smtpserver_connection_total",
126 Help: "Incoming SMTP connections.",
129 "kind", // "deliver" or "submit"
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},
139 "kind", // "deliver" or "submit"
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.",
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.",
165 metricServerErrors = promauto.NewCounterVec(
166 prometheus.CounterOpts{
167 Name: "mox_smtpserver_errors_total",
168 Help: "SMTP server errors, known values: dkimsign, queuedsn.",
176var jitterRand = mox.NewPseudoRand()
178func durationDefault(delay *time.Duration, def time.Duration) time.Duration {
185// Listen initializes network listeners for incoming SMTP connection.
186// The listeners are stored for a later call to Serve.
188 names := maps.Keys(mox.Conf.Static.Listeners)
190 for _, name := range names {
191 listener := mox.Conf.Static.Listeners[name]
193 var tlsConfig, tlsConfigDelivery *tls.Config
194 if listener.TLS != nil {
195 tlsConfig = listener.TLS.Config
196 // For SMTP delivery, if we get a TLS handshake for an SNI hostname that we don't
197 // allow, we'll fallback to a certificate for the listener hostname instead of
198 // causing the connection to fail. May improve interoperability.
199 tlsConfigDelivery = listener.TLS.ConfigFallback
202 maxMsgSize := listener.SMTPMaxMessageSize
204 maxMsgSize = config.DefaultMaxMsgSize
207 if listener.SMTP.Enabled {
208 hostname := mox.Conf.Static.HostnameDomain
209 if listener.Hostname != "" {
210 hostname = listener.HostnameDomain
212 port := config.Port(listener.SMTP.Port, 25)
213 for _, ip := range listener.IPs {
214 firstTimeSenderDelay := durationDefault(listener.SMTP.FirstTimeSenderDelay, firstTimeSenderDelayDefault)
215 listen1("smtp", name, ip, port, hostname, tlsConfigDelivery, false, false, maxMsgSize, false, listener.SMTP.RequireSTARTTLS, !listener.SMTP.NoRequireTLS, listener.SMTP.DNSBLZones, firstTimeSenderDelay)
218 if listener.Submission.Enabled {
219 hostname := mox.Conf.Static.HostnameDomain
220 if listener.Hostname != "" {
221 hostname = listener.HostnameDomain
223 port := config.Port(listener.Submission.Port, 587)
224 for _, ip := range listener.IPs {
225 listen1("submission", name, ip, port, hostname, tlsConfig, true, false, maxMsgSize, !listener.Submission.NoRequireSTARTTLS, !listener.Submission.NoRequireSTARTTLS, true, nil, 0)
229 if listener.Submissions.Enabled {
230 hostname := mox.Conf.Static.HostnameDomain
231 if listener.Hostname != "" {
232 hostname = listener.HostnameDomain
234 port := config.Port(listener.Submissions.Port, 465)
235 for _, ip := range listener.IPs {
236 listen1("submissions", name, ip, port, hostname, tlsConfig, true, true, maxMsgSize, true, true, true, nil, 0)
244func listen1(protocol, name, ip string, port int, hostname dns.Domain, tlsConfig *tls.Config, submission, xtls bool, maxMessageSize int64, requireTLSForAuth, requireTLSForDelivery, requireTLS bool, dnsBLs []dns.Domain, firstTimeSenderDelay time.Duration) {
245 log := mlog.New("smtpserver", nil)
246 addr := net.JoinHostPort(ip, fmt.Sprintf("%d", port))
247 if os.Getuid() == 0 {
248 log.Print("listening for smtp",
249 slog.String("listener", name),
250 slog.String("address", addr),
251 slog.String("protocol", protocol))
253 network := mox.Network(ip)
254 ln, err := mox.Listen(network, addr)
256 log.Fatalx("smtp: listen for smtp", err, slog.String("protocol", protocol), slog.String("listener", name))
259 ln = tls.NewListener(ln, tlsConfig)
264 conn, err := ln.Accept()
266 log.Infox("smtp: accept", err, slog.String("protocol", protocol), slog.String("listener", name))
270 // Package is set on the resolver by the dkim/spf/dmarc/etc packages.
271 resolver := dns.StrictResolver{Log: log.Logger}
272 go serve(name, mox.Cid(), hostname, tlsConfig, conn, resolver, submission, xtls, maxMessageSize, requireTLSForAuth, requireTLSForDelivery, requireTLS, dnsBLs, firstTimeSenderDelay)
276 servers = append(servers, serve)
279// Serve starts serving on all listeners, launching a goroutine per listener.
281 for _, serve := range servers {
289 // OrigConn is the original (TCP) connection. We'll read from/write to conn, which
290 // can be wrapped in a tls.Server. We close origConn instead of conn because
291 // closing the TLS connection would send a TLS close notification, which may block
292 // for 5s if the server isn't reading it (because it is also sending it).
297 extRequireTLS bool // Whether to announce and allow the REQUIRETLS extension.
298 resolver dns.Resolver
301 tr *moxio.TraceReader // Kept for changing trace level during cmd/auth/data.
302 tw *moxio.TraceWriter
303 slow bool // If set, reads are done with a 1 second sleep, and writes are done 1 byte at a time, to keep spammers busy.
304 lastlog time.Time // Used for printing the delta time since the previous logging for this connection.
306 tlsConfig *tls.Config
312 requireTLSForAuth bool
313 requireTLSForDelivery bool // If set, delivery is only allowed with TLS (STARTTLS), except if delivery is to a TLS reporting address.
314 cmd string // Current command.
315 cmdStart time.Time // Start of current command.
316 ncmds int // Number of commands processed. Used to abort connection when first incoming command is unknown/invalid.
318 firstTimeSenderDelay time.Duration
320 // If non-zero, taken into account during Read and Write. Set while processing DATA
321 // command, we don't want the entire delivery to take too long.
324 hello dns.IPDomain // Claimed remote name. Can be ip address for ehlo.
325 ehlo bool // If set, we had EHLO instead of HELO.
327 authFailed int // Number of failed auth attempts. For slowing down remote with many failures.
328 username string // Only when authenticated.
329 account *store.Account // Only when authenticated.
331 // We track good/bad message transactions to disconnect spammers trying to guess addresses.
335 // Message transaction.
337 requireTLS *bool // MAIL FROM with REQUIRETLS set.
338 futureRelease time.Time // MAIL FROM with HOLDFOR or HOLDUNTIL.
339 futureReleaseRequest string // For use in DSNs, either "for;" or "until;" plus original value.
../rfc/4865:305
340 has8bitmime bool // If MAIL FROM parameter BODY=8BITMIME was sent. Required for SMTPUTF8.
341 smtputf8 bool // todo future: we should keep track of this per recipient. perhaps only a specific recipient requires smtputf8, e.g. due to a utf8 localpart.
342 msgsmtputf8 bool // Is SMTPUTF8 required for the received message. Default to the same value as `smtputf8`, but is re-evaluated after the whole message (envelope and data) is received.
343 recipients []recipient
346type rcptAccount struct {
348 destination config.Destination
349 canonicalAddress string // Optional catchall part stripped and/or lowercased.
352type rcptAlias struct {
354 canonicalAddress string // Optional catchall part stripped and/or lowercased.
357type recipient struct {
360 // If account and alias are both not set, this is not for a local address. This is
361 // normal for submission, where messages are added to the queue. For incoming
362 // deliveries, this will result in an error.
363 account *rcptAccount // If set, recipient address is for this local account.
364 alias *rcptAlias // If set, for a local alias.
367func isClosed(err error) bool {
368 return errors.Is(err, errIO) || moxio.IsClosed(err)
371// completely reset connection state as if greeting has just been sent.
373func (c *conn) reset() {
375 c.hello = dns.IPDomain{}
377 if c.account != nil {
378 err := c.account.Close()
379 c.log.Check(err, "closing account")
385// for rset command, and a few more cases that reset the mail transaction state.
387func (c *conn) rset() {
390 c.futureRelease = time.Time{}
391 c.futureReleaseRequest = ""
392 c.has8bitmime = false
394 c.msgsmtputf8 = false
398func (c *conn) earliestDeadline(d time.Duration) time.Time {
399 e := time.Now().Add(d)
400 if !c.deadline.IsZero() && c.deadline.Before(e) {
406func (c *conn) xcheckAuth() {
407 if c.submission && c.account == nil {
409 xsmtpUserErrorf(smtp.C530SecurityRequired, smtp.SePol7Other0, "authentication required")
413func (c *conn) xtrace(level slog.Level) func() {
419 c.tr.SetTrace(mlog.LevelTrace)
420 c.tw.SetTrace(mlog.LevelTrace)
424// setSlow marks the connection slow (or now), so reads are done with 3 second
425// delay for each read, and writes are done at 1 byte per second, to try to slow
427func (c *conn) setSlow(on bool) {
429 c.log.Debug("connection changed to slow")
430 } else if !on && c.slow {
431 c.log.Debug("connection restored to regular pace")
436// Write writes to the connection. It panics on i/o errors, which is handled by the
437// connection command loop.
438func (c *conn) Write(buf []byte) (int, error) {
444 // We set a single deadline for Write and Read. This may be a TLS connection.
445 // SetDeadline works on the underlying connection. If we wouldn't touch the read
446 // deadline, and only set the write deadline and do a bunch of writes, the TLS
447 // library would still have to do reads on the underlying connection, and may reach
448 // a read deadline that was set for some earlier read.
449 // We have one deadline for the whole write. In case of slow writing, we'll write
450 // the last chunk in one go, so remote smtp clients don't abort the connection for
452 deadline := c.earliestDeadline(30 * time.Second)
453 if err := c.conn.SetDeadline(deadline); err != nil {
454 c.log.Errorx("setting deadline for write", err)
459 nn, err := c.conn.Write(buf[:chunk])
461 panic(fmt.Errorf("write: %s (%w)", err, errIO))
465 if len(buf) > 0 && badClientDelay > 0 {
466 mox.Sleep(mox.Context, badClientDelay)
468 // Make sure we don't take too long, otherwise the remote SMTP client may close the
470 if time.Until(deadline) < 2*badClientDelay {
478// Read reads from the connection. It panics on i/o errors, which is handled by the
479// connection command loop.
480func (c *conn) Read(buf []byte) (int, error) {
481 if c.slow && badClientDelay > 0 {
482 mox.Sleep(mox.Context, badClientDelay)
486 // See comment about Deadline instead of individual read/write deadlines at Write.
487 if err := c.conn.SetDeadline(c.earliestDeadline(30 * time.Second)); err != nil {
488 c.log.Errorx("setting deadline for read", err)
491 n, err := c.conn.Read(buf)
493 panic(fmt.Errorf("read: %s (%w)", err, errIO))
498// Cache of line buffers for reading commands.
500var bufpool = moxio.NewBufpool(8, 2*1024)
502func (c *conn) readline() string {
503 line, err := bufpool.Readline(c.log, c.r)
504 if err != nil && errors.Is(err, moxio.ErrLineTooLong) {
505 c.writecodeline(smtp.C500BadSyntax, smtp.SeProto5Other0, "line too long, smtp max is 512, we reached 2048", nil)
506 panic(fmt.Errorf("%s (%w)", err, errIO))
507 } else if err != nil {
508 panic(fmt.Errorf("%s (%w)", err, errIO))
513// Buffered-write command response line to connection with codes and msg.
514// Err is not sent to remote but is used for logging and can be empty.
515func (c *conn) bwritecodeline(code int, secode string, msg string, err error) {
518 ecode = fmt.Sprintf("%d.%s", code/100, secode)
520 metricCommands.WithLabelValues(c.kind(), c.cmd, fmt.Sprintf("%d", code), ecode).Observe(float64(time.Since(c.cmdStart)) / float64(time.Second))
521 c.log.Debugx("smtp command result", err,
522 slog.String("kind", c.kind()),
523 slog.String("cmd", c.cmd),
524 slog.Int("code", code),
525 slog.String("ecode", ecode),
526 slog.Duration("duration", time.Since(c.cmdStart)))
533 // Separate by newline and wrap long lines.
534 lines := strings.Split(msg, "\n")
535 for i, line := range lines {
537 var prelen = 3 + 1 + len(ecode) + len(sep)
538 for prelen+len(line) > 510 {
540 for ; e > 400 && line[e] != ' '; e-- {
542 // todo future: understand if ecode should be on each line. won't hurt. at least as long as we don't do expn or vrfy.
543 c.bwritelinef("%d-%s%s%s", code, ecode, sep, line[:e])
547 if i < len(lines)-1 {
550 c.bwritelinef("%d%s%s%s%s", code, spdash, ecode, sep, line)
554// Buffered-write a formatted response line to connection.
555func (c *conn) bwritelinef(format string, args ...any) {
556 msg := fmt.Sprintf(format, args...)
557 fmt.Fprint(c.w, msg+"\r\n")
560// Flush pending buffered writes to connection.
561func (c *conn) xflush() {
562 c.w.Flush() // Errors will have caused a panic in Write.
565// Write (with flush) a response line with codes and message. err is not written, used for logging and can be nil.
566func (c *conn) writecodeline(code int, secode string, msg string, err error) {
567 c.bwritecodeline(code, secode, msg, err)
571// Write (with flush) a formatted response line to connection.
572func (c *conn) writelinef(format string, args ...any) {
573 c.bwritelinef(format, args...)
577var cleanClose struct{} // Sentinel value for panic/recover indicating clean close of connection.
579func serve(listenerName string, cid int64, hostname dns.Domain, tlsConfig *tls.Config, nc net.Conn, resolver dns.Resolver, submission, tls bool, maxMessageSize int64, requireTLSForAuth, requireTLSForDelivery, requireTLS bool, dnsBLs []dns.Domain, firstTimeSenderDelay time.Duration) {
580 var localIP, remoteIP net.IP
581 if a, ok := nc.LocalAddr().(*net.TCPAddr); ok {
584 // For net.Pipe, during tests.
585 localIP = net.ParseIP("127.0.0.10")
587 if a, ok := nc.RemoteAddr().(*net.TCPAddr); ok {
590 // For net.Pipe, during tests.
591 remoteIP = net.ParseIP("127.0.0.10")
598 submission: submission,
600 extRequireTLS: requireTLS,
603 tlsConfig: tlsConfig,
607 maxMessageSize: maxMessageSize,
608 requireTLSForAuth: requireTLSForAuth,
609 requireTLSForDelivery: requireTLSForDelivery,
611 firstTimeSenderDelay: firstTimeSenderDelay,
613 var logmutex sync.Mutex
614 c.log = mlog.New("smtpserver", nil).WithFunc(func() []slog.Attr {
616 defer logmutex.Unlock()
619 slog.Int64("cid", c.cid),
620 slog.Duration("delta", now.Sub(c.lastlog)),
623 if c.username != "" {
624 l = append(l, slog.String("username", c.username))
628 c.tr = moxio.NewTraceReader(c.log, "RC: ", c)
629 c.tw = moxio.NewTraceWriter(c.log, "LS: ", c)
630 c.r = bufio.NewReader(c.tr)
631 c.w = bufio.NewWriter(c.tw)
633 metricConnection.WithLabelValues(c.kind()).Inc()
634 c.log.Info("new connection",
635 slog.Any("remote", c.conn.RemoteAddr()),
636 slog.Any("local", c.conn.LocalAddr()),
637 slog.Bool("submission", submission),
638 slog.Bool("tls", tls),
639 slog.String("listener", listenerName))
642 c.origConn.Close() // Close actual TCP socket, regardless of TLS on top.
643 c.conn.Close() // If TLS, will try to write alert notification to already closed socket, returning error quickly.
645 if c.account != nil {
646 err := c.account.Close()
647 c.log.Check(err, "closing account")
652 if x == nil || x == cleanClose {
653 c.log.Info("connection closed")
654 } else if err, ok := x.(error); ok && isClosed(err) {
655 c.log.Infox("connection closed", err)
657 c.log.Error("unhandled panic", slog.Any("err", x))
659 metrics.PanicInc(metrics.Smtpserver)
664 case <-mox.Shutdown.Done():
666 c.writecodeline(smtp.C421ServiceUnavail, smtp.SeSys3NotAccepting2, "shutting down", nil)
671 if !limiterConnectionRate.Add(c.remoteIP, time.Now(), 1) {
672 c.writecodeline(smtp.C421ServiceUnavail, smtp.SePol7Other0, "connection rate from your ip or network too high, slow down please", nil)
676 // If remote IP/network resulted in too many authentication failures, refuse to serve.
677 if submission && !mox.LimiterFailedAuth.CanAdd(c.remoteIP, time.Now(), 1) {
678 metrics.AuthenticationRatelimitedInc("submission")
679 c.log.Debug("refusing connection due to many auth failures", slog.Any("remoteip", c.remoteIP))
680 c.writecodeline(smtp.C421ServiceUnavail, smtp.SePol7Other0, "too many auth failures", nil)
684 if !limiterConnections.Add(c.remoteIP, time.Now(), 1) {
685 c.log.Debug("refusing connection due to many open connections", slog.Any("remoteip", c.remoteIP))
686 c.writecodeline(smtp.C421ServiceUnavail, smtp.SePol7Other0, "too many open connections from your ip or network", nil)
689 defer limiterConnections.Add(c.remoteIP, time.Now(), -1)
691 // We register and unregister the original connection, in case c.conn is replaced
692 // with a TLS connection later on.
693 mox.Connections.Register(nc, "smtp", listenerName)
694 defer mox.Connections.Unregister(nc)
698 // We include the string ESMTP. https://cr.yp.to/smtp/greeting.html recommends it.
699 // Should not be too relevant nowadays, but does not hurt and default blackbox
700 // exporter SMTP health check expects it.
701 c.writelinef("%d %s ESMTP mox %s", smtp.C220ServiceReady, c.hostname.ASCII, moxvar.Version)
706 // If another command is present, don't flush our buffered response yet. Holding
707 // off will cause us to respond with a single packet.
710 buf, err := c.r.Peek(n)
711 if err == nil && bytes.IndexByte(buf, '\n') >= 0 {
719var commands = map[string]func(c *conn, p *parser){
720 "helo": (*conn).cmdHelo,
721 "ehlo": (*conn).cmdEhlo,
722 "starttls": (*conn).cmdStarttls,
723 "auth": (*conn).cmdAuth,
724 "mail": (*conn).cmdMail,
725 "rcpt": (*conn).cmdRcpt,
726 "data": (*conn).cmdData,
727 "rset": (*conn).cmdRset,
728 "vrfy": (*conn).cmdVrfy,
729 "expn": (*conn).cmdExpn,
730 "help": (*conn).cmdHelp,
731 "noop": (*conn).cmdNoop,
732 "quit": (*conn).cmdQuit,
735func command(c *conn) {
751 if errors.As(err, &serr) {
752 c.writecodeline(serr.code, serr.secode, fmt.Sprintf("%s (%s)", serr.errmsg, mox.ReceivedID(c.cid)), serr.err)
757 // Other type of panic, we pass it on, aborting the connection.
758 c.log.Errorx("command panic", err)
763 // todo future: we could wait for either a line or shutdown, and just close the connection on shutdown.
766 t := strings.SplitN(line, " ", 2)
772 cmdl := strings.ToLower(cmd)
774 // todo future: should we return an error for lines that are too long? perhaps for submission or in a pedantic mode. we would have to take extensions for MAIL into account.
../rfc/5321:3500 ../rfc/5321:3552
777 case <-mox.Shutdown.Done():
779 c.writecodeline(smtp.C421ServiceUnavail, smtp.SeSys3NotAccepting2, "shutting down", nil)
785 c.cmdStart = time.Now()
787 p := newParser(args, c.smtputf8, c)
788 fn, ok := commands[cmdl]
792 // Other side is likely speaking something else than SMTP, send error message and
793 // stop processing because there is a good chance whatever they sent has multiple
795 c.writecodeline(smtp.C500BadSyntax, smtp.SeProto5Syntax2, "please try again speaking smtp", nil)
799 xsmtpUserErrorf(smtp.C500BadSyntax, smtp.SeProto5BadCmdOrSeq1, "unknown command")
805// For use in metric labels.
806func (c *conn) kind() string {
813func (c *conn) xneedHello() {
814 if c.hello.IsZero() {
815 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "no ehlo/helo yet")
819// If smtp server is configured to require TLS for all mail delivery (except to TLS
820// reporting address), abort command.
821func (c *conn) xneedTLSForDelivery(rcpt smtp.Path) {
822 // For TLS reports, we allow the message in even without TLS, because there may be
824 if c.requireTLSForDelivery && !c.tls && !isTLSReportRecipient(rcpt) {
826 xsmtpUserErrorf(smtp.C530SecurityRequired, smtp.SePol7Other0, "STARTTLS required for mail delivery")
830func isTLSReportRecipient(rcpt smtp.Path) bool {
831 _, _, _, dest, err := mox.LookupAddress(rcpt.Localpart, rcpt.IPDomain.Domain, false, false)
832 return err == nil && (dest.HostTLSReports || dest.DomainTLSReports)
835func (c *conn) cmdHelo(p *parser) {
839func (c *conn) cmdEhlo(p *parser) {
844func (c *conn) cmdHello(p *parser, ehlo bool) {
845 var remote dns.IPDomain
846 if c.submission && !mox.Pedantic {
847 // Mail clients regularly put bogus information in the hostname/ip. For submission,
848 // the value is of no use, so there is not much point in annoying the user with
849 // errors they cannot fix themselves. Except when in pedantic mode.
850 remote = dns.IPDomain{IP: c.remoteIP}
854 remote = p.xipdomain(true)
856 remote = dns.IPDomain{Domain: p.xdomain()}
858 // Verify a remote domain name has an A or AAAA record, CNAME not allowed.
../rfc/5321:722
859 cidctx := context.WithValue(mox.Context, mlog.CidKey, c.cid)
860 ctx, cancel := context.WithTimeout(cidctx, time.Minute)
861 _, _, err := c.resolver.LookupIPAddr(ctx, remote.Domain.ASCII+".")
863 if dns.IsNotFound(err) {
864 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeProto5Other0, "your ehlo domain does not resolve to an IP address")
866 // For success or temporary resolve errors, we'll just continue.
869 // Though a few paragraphs earlier is a claim additional data can occur for address
870 // literals (IP addresses), although the ABNF in that document does not allow it.
871 // We allow additional text, but only if space-separated.
872 if len(remote.IP) > 0 && p.space() {
884 // https://www.iana.org/assignments/mail-parameters/mail-parameters.xhtml
886 c.bwritelinef("250-%s", c.hostname.ASCII)
890 if !c.tls && c.tlsConfig != nil {
892 c.bwritelinef("250-STARTTLS")
893 } else if c.extRequireTLS {
896 c.bwritelinef("250-REQUIRETLS")
900 if c.tls || !c.requireTLSForAuth {
901 // We always mention the SCRAM PLUS variants, even if TLS is not active: It is a
902 // hint to the client that a TLS connection can use TLS channel binding during
903 // authentication. The client should select the bare variant when TLS isn't
904 // present, and also not indicate the server supports the PLUS variant in that
905 // case, or it would trigger the mechanism downgrade detection.
906 c.bwritelinef("250-AUTH SCRAM-SHA-256-PLUS SCRAM-SHA-256 SCRAM-SHA-1-PLUS SCRAM-SHA-1 CRAM-MD5 PLAIN LOGIN")
908 c.bwritelinef("250-AUTH ")
912 c.bwritelinef("250-FUTURERELEASE %d %s", queue.FutureReleaseIntervalMax/time.Second, t.Format(time.RFC3339))
915 // todo future? c.writelinef("250-DSN")
923func (c *conn) cmdStarttls(p *parser) {
929 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "already speaking tls")
931 if c.account != nil {
932 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "cannot starttls after authentication")
934 if c.tlsConfig == nil {
935 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "starttls not offered")
938 // We don't want to do TLS on top of c.r because it also prints protocol traces: We
939 // don't want to log the TLS stream. So we'll do TLS on the underlying connection,
940 // but make sure any bytes already read and in the buffer are used for the TLS
943 if n := c.r.Buffered(); n > 0 {
944 conn = &moxio.PrefixConn{
945 PrefixReader: io.LimitReader(c.r, int64(n)),
950 // We add the cid to the output, to help debugging in case of a failing TLS connection.
951 c.writecodeline(smtp.C220ServiceReady, smtp.SeOther00, "go! ("+mox.ReceivedID(c.cid)+")", nil)
952 tlsConn := tls.Server(conn, c.tlsConfig)
953 cidctx := context.WithValue(mox.Context, mlog.CidKey, c.cid)
954 ctx, cancel := context.WithTimeout(cidctx, time.Minute)
956 c.log.Debug("starting tls server handshake")
957 if err := tlsConn.HandshakeContext(ctx); err != nil {
958 panic(fmt.Errorf("starttls handshake: %s (%w)", err, errIO))
961 tlsversion, ciphersuite := moxio.TLSInfo(tlsConn)
962 c.log.Debug("tls server handshake done", slog.String("tls", tlsversion), slog.String("ciphersuite", ciphersuite))
964 c.tr = moxio.NewTraceReader(c.log, "RC: ", c)
965 c.tw = moxio.NewTraceWriter(c.log, "LS: ", c)
966 c.r = bufio.NewReader(c.tr)
967 c.w = bufio.NewWriter(c.tw)
974func (c *conn) cmdAuth(p *parser) {
978 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "authentication only allowed on submission ports")
980 if c.account != nil {
982 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "already authenticated")
984 if c.mailFrom != nil {
986 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "authentication not allowed during mail transaction")
989 // If authentication fails due to missing derived secrets, we don't hold it against
990 // the connection. There is no way to indicate server support for an authentication
991 // mechanism, but that a mechanism won't work for an account.
992 var missingDerivedSecrets bool
994 // For many failed auth attempts, slow down verification attempts.
995 // Dropping the connection could also work, but more so when we have a connection rate limiter.
997 if c.authFailed > 3 && authFailDelay > 0 {
999 mox.Sleep(mox.Context, time.Duration(c.authFailed-3)*authFailDelay)
1001 c.authFailed++ // Compensated on success.
1003 if missingDerivedSecrets {
1006 // On the 3rd failed authentication, start responding slowly. Successful auth will
1007 // cause fast responses again.
1008 if c.authFailed >= 3 {
1013 var authVariant string
1014 authResult := "error"
1016 metrics.AuthenticationInc("submission", authVariant, authResult)
1017 if authResult == "ok" {
1018 mox.LimiterFailedAuth.Reset(c.remoteIP, time.Now())
1019 } else if !missingDerivedSecrets {
1020 mox.LimiterFailedAuth.Add(c.remoteIP, time.Now(), 1)
1026 mech := p.xsaslMech()
1028 // Read the first parameter, either as initial parameter or by sending a
1029 // continuation with the optional encChal (must already be base64-encoded).
1030 xreadInitial := func(encChal string) []byte {
1034 // todo future: handle max length of 12288 octets and return proper responde codes otherwise
../rfc/4954:253
1038 authResult = "aborted"
1039 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5Other0, "authentication aborted")
1044 // Windows Mail 16005.14326.21606.0 sends two spaces between "AUTH PLAIN" and the
1049 auth = p.remainder()
1052 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5Syntax2, "missing initial auth base64 parameter after space")
1053 } else if auth == "=" {
1055 auth = "" // Base64 decode below will result in empty buffer.
1058 buf, err := base64.StdEncoding.DecodeString(auth)
1061 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5Syntax2, "invalid base64: %s", err)
1066 xreadContinuation := func() []byte {
1067 line := c.readline()
1069 authResult = "aborted"
1070 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5Other0, "authentication aborted")
1072 buf, err := base64.StdEncoding.DecodeString(line)
1075 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5Syntax2, "invalid base64: %s", err)
1082 authVariant = "plain"
1086 if !c.tls && c.requireTLSForAuth {
1087 xsmtpUserErrorf(smtp.C538EncReqForAuth, smtp.SePol7EncReqForAuth11, "authentication requires tls")
1090 // Password is in line in plain text, so hide it.
1091 defer c.xtrace(mlog.LevelTraceauth)()
1092 buf := xreadInitial("")
1093 c.xtrace(mlog.LevelTrace) // Restore.
1094 plain := bytes.Split(buf, []byte{0})
1095 if len(plain) != 3 {
1096 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5BadParams4, "auth data should have 3 nul-separated tokens, got %d", len(plain))
1098 authz := norm.NFC.String(string(plain[0]))
1099 authc := norm.NFC.String(string(plain[1]))
1100 password := string(plain[2])
1102 if authz != "" && authz != authc {
1103 authResult = "badcreds"
1104 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "cannot assume other role")
1107 acc, err := store.OpenEmailAuth(c.log, authc, password)
1108 if err != nil && errors.Is(err, store.ErrUnknownCredentials) {
1110 authResult = "badcreds"
1111 c.log.Info("failed authentication attempt", slog.String("username", authc), slog.Any("remote", c.remoteIP))
1112 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass")
1114 xcheckf(err, "verifying credentials")
1122 c.writecodeline(smtp.C235AuthSuccess, smtp.SePol7Other0, "nice", nil)
1125 // LOGIN is obsoleted in favor of PLAIN, only implemented to support legacy
1126 // clients, see Internet-Draft (I-D):
1127 // https://datatracker.ietf.org/doc/html/draft-murchison-sasl-login-00
1129 authVariant = "login"
1133 if !c.tls && c.requireTLSForAuth {
1134 xsmtpUserErrorf(smtp.C538EncReqForAuth, smtp.SePol7EncReqForAuth11, "authentication requires tls")
1137 // Read user name. The I-D says the client should ignore the server challenge, but
1138 // also that some clients may require challenge "Username:" instead of "User
1139 // Name". We can't sent both... Servers most commonly return "Username:" and
1140 // "Password:", so we do the same.
1141 // I-D says maximum length must be 64 bytes. We allow more, for long user names
1143 encChal := base64.StdEncoding.EncodeToString([]byte("Username:"))
1144 username := string(xreadInitial(encChal))
1145 username = norm.NFC.String(username)
1147 // Again, client should ignore the challenge, we send the same as the example in
1149 c.writelinef("%d %s", smtp.C334ContinueAuth, base64.StdEncoding.EncodeToString([]byte("Password:")))
1151 // Password is in line in plain text, so hide it.
1152 defer c.xtrace(mlog.LevelTraceauth)()
1153 password := string(xreadContinuation())
1154 c.xtrace(mlog.LevelTrace) // Restore.
1156 acc, err := store.OpenEmailAuth(c.log, username, password)
1157 if err != nil && errors.Is(err, store.ErrUnknownCredentials) {
1159 authResult = "badcreds"
1160 c.log.Info("failed authentication attempt", slog.String("username", username), slog.Any("remote", c.remoteIP))
1161 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass")
1163 xcheckf(err, "verifying credentials")
1169 c.username = username
1171 c.writecodeline(smtp.C235AuthSuccess, smtp.SePol7Other0, "hello ancient smtp implementation", nil)
1174 authVariant = strings.ToLower(mech)
1179 chal := fmt.Sprintf("<%d.%d@%s>", uint64(mox.CryptoRandInt()), time.Now().UnixNano(), mox.Conf.Static.HostnameDomain.ASCII)
1180 c.writelinef("%d %s", smtp.C334ContinueAuth, base64.StdEncoding.EncodeToString([]byte(chal)))
1182 resp := xreadContinuation()
1183 t := strings.Split(string(resp), " ")
1184 if len(t) != 2 || len(t[1]) != 2*md5.Size {
1185 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5BadParams4, "malformed cram-md5 response")
1187 addr := norm.NFC.String(t[0])
1188 c.log.Debug("cram-md5 auth", slog.String("address", addr))
1189 acc, _, err := store.OpenEmail(c.log, addr)
1190 if err != nil && errors.Is(err, store.ErrUnknownCredentials) {
1191 c.log.Info("failed authentication attempt", slog.String("username", addr), slog.Any("remote", c.remoteIP))
1192 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass")
1194 xcheckf(err, "looking up address")
1198 c.log.Check(err, "closing account")
1201 var ipadhash, opadhash hash.Hash
1202 acc.WithRLock(func() {
1203 err := acc.DB.Read(context.TODO(), func(tx *bstore.Tx) error {
1204 password, err := bstore.QueryTx[store.Password](tx).Get()
1205 if err == bstore.ErrAbsent {
1206 c.log.Info("failed authentication attempt", slog.String("username", addr), slog.Any("remote", c.remoteIP))
1207 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass")
1213 ipadhash = password.CRAMMD5.Ipad
1214 opadhash = password.CRAMMD5.Opad
1217 xcheckf(err, "tx read")
1219 if ipadhash == nil || opadhash == nil {
1220 missingDerivedSecrets = true
1221 c.log.Info("cram-md5 auth attempt without derived secrets set, save password again to store secrets", slog.String("username", addr))
1222 c.log.Info("failed authentication attempt", slog.String("username", addr), slog.Any("remote", c.remoteIP))
1223 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass")
1227 ipadhash.Write([]byte(chal))
1228 opadhash.Write(ipadhash.Sum(nil))
1229 digest := fmt.Sprintf("%x", opadhash.Sum(nil))
1231 c.log.Info("failed authentication attempt", slog.String("username", addr), slog.Any("remote", c.remoteIP))
1232 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass")
1239 acc = nil // Cancel cleanup.
1242 c.writecodeline(smtp.C235AuthSuccess, smtp.SePol7Other0, "nice", nil)
1244 case "SCRAM-SHA-256-PLUS", "SCRAM-SHA-256", "SCRAM-SHA-1-PLUS", "SCRAM-SHA-1":
1245 // todo: improve handling of errors during scram. e.g. invalid parameters. should we abort the imap command, or continue until the end and respond with a scram-level error?
1246 // todo: use single implementation between ../imapserver/server.go and ../smtpserver/server.go
1248 // Passwords cannot be retrieved or replayed from the trace.
1250 authVariant = strings.ToLower(mech)
1251 var h func() hash.Hash
1252 switch authVariant {
1253 case "scram-sha-1", "scram-sha-1-plus":
1255 case "scram-sha-256", "scram-sha-256-plus":
1258 xsmtpServerErrorf(codes{smtp.C554TransactionFailed, smtp.SeSys3Other0}, "missing scram auth method case")
1261 var cs *tls.ConnectionState
1262 channelBindingRequired := strings.HasSuffix(authVariant, "-plus")
1263 if channelBindingRequired && !c.tls {
1265 xsmtpUserErrorf(smtp.C538EncReqForAuth, smtp.SePol7EncReqForAuth11, "scram plus mechanism requires tls connection")
1268 xcs := c.conn.(*tls.Conn).ConnectionState()
1271 c0 := xreadInitial("")
1272 ss, err := scram.NewServer(h, c0, cs, channelBindingRequired)
1274 c.log.Infox("scram protocol error", err, slog.Any("remote", c.remoteIP))
1275 xsmtpUserErrorf(smtp.C455BadParams, smtp.SePol7Other0, "scram protocol error: %s", err)
1277 authc := norm.NFC.String(ss.Authentication)
1278 c.log.Debug("scram auth", slog.String("authentication", authc))
1279 acc, _, err := store.OpenEmail(c.log, authc)
1281 // todo: we could continue scram with a generated salt, deterministically generated
1282 // from the username. that way we don't have to store anything but attackers cannot
1283 // learn if an account exists. same for absent scram saltedpassword below.
1284 c.log.Info("failed authentication attempt", slog.String("username", authc), slog.Any("remote", c.remoteIP))
1285 xsmtpUserErrorf(smtp.C454TempAuthFail, smtp.SeSys3Other0, "scram not possible")
1290 c.log.Check(err, "closing account")
1293 if ss.Authorization != "" && ss.Authorization != ss.Authentication {
1294 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "authentication with authorization for different user not supported")
1296 var xscram store.SCRAM
1297 acc.WithRLock(func() {
1298 err := acc.DB.Read(context.TODO(), func(tx *bstore.Tx) error {
1299 password, err := bstore.QueryTx[store.Password](tx).Get()
1300 if err == bstore.ErrAbsent {
1301 c.log.Info("failed authentication attempt", slog.String("username", authc), slog.Any("remote", c.remoteIP))
1302 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass")
1304 xcheckf(err, "fetching credentials")
1305 switch authVariant {
1306 case "scram-sha-1", "scram-sha-1-plus":
1307 xscram = password.SCRAMSHA1
1308 case "scram-sha-256", "scram-sha-256-plus":
1309 xscram = password.SCRAMSHA256
1311 xsmtpServerErrorf(codes{smtp.C554TransactionFailed, smtp.SeSys3Other0}, "missing scram auth credentials case")
1313 if len(xscram.Salt) == 0 || xscram.Iterations == 0 || len(xscram.SaltedPassword) == 0 {
1314 missingDerivedSecrets = true
1315 c.log.Info("scram auth attempt without derived secrets set, save password again to store secrets", slog.String("address", authc))
1316 c.log.Info("failed authentication attempt", slog.String("username", authc), slog.Any("remote", c.remoteIP))
1317 xsmtpUserErrorf(smtp.C454TempAuthFail, smtp.SeSys3Other0, "scram not possible")
1321 xcheckf(err, "read tx")
1323 s1, err := ss.ServerFirst(xscram.Iterations, xscram.Salt)
1324 xcheckf(err, "scram first server step")
1325 c.writelinef("%d %s", smtp.C334ContinueAuth, base64.StdEncoding.EncodeToString([]byte(s1))) //
../rfc/4954:187
1326 c2 := xreadContinuation()
1327 s3, err := ss.Finish(c2, xscram.SaltedPassword)
1329 c.writelinef("%d %s", smtp.C334ContinueAuth, base64.StdEncoding.EncodeToString([]byte(s3))) //
../rfc/4954:187
1332 c.readline() // Should be "*" for cancellation.
1333 if errors.Is(err, scram.ErrInvalidProof) {
1334 authResult = "badcreds"
1335 c.log.Info("failed authentication attempt", slog.String("username", authc), slog.Any("remote", c.remoteIP))
1336 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad credentials")
1337 } else if errors.Is(err, scram.ErrChannelBindingsDontMatch) {
1338 authResult = "badchanbind"
1339 c.log.Warn("bad channel binding during authentication, potential mitm", slog.String("username", authc), slog.Any("remote", c.remoteIP))
1340 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7MsgIntegrity7, "channel bindings do not match, potential mitm")
1341 } else if errors.Is(err, scram.ErrInvalidEncoding) {
1342 c.log.Infox("bad scram protocol message", err, slog.String("username", authc), slog.Any("remote", c.remoteIP))
1343 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7Other0, "bad scram protocol message")
1345 xcheckf(err, "server final")
1349 // The message should be empty. todo: should we require it is empty?
1356 acc = nil // Cancel cleanup.
1359 c.writecodeline(smtp.C235AuthSuccess, smtp.SePol7Other0, "nice", nil)
1363 xsmtpUserErrorf(smtp.C504ParamNotImpl, smtp.SeProto5BadParams4, "mechanism %s not supported", mech)
1368func (c *conn) cmdMail(p *parser) {
1369 // requirements for maximum line length:
1371 // todo future: enforce? doesn't really seem worth it...
1373 if c.transactionBad > 10 && c.transactionGood == 0 {
1374 // If we get many bad transactions, it's probably a spammer that is guessing user names.
1375 // Useful in combination with rate limiting.
1377 c.writecodeline(smtp.C550MailboxUnavail, smtp.SeAddr1Other0, "too many failures", nil)
1383 if c.mailFrom != nil {
1385 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "already have MAIL")
1387 // Ensure clear transaction state on failure.
1398 // Microsoft Outlook 365 Apps for Enterprise sends it with submission. For delivery
1399 // it is mostly used by spammers, but has been seen with legitimate senders too.
1403 rawRevPath := p.xrawReversePath()
1404 paramSeen := map[string]bool{}
1407 key := p.xparamKeyword()
1409 K := strings.ToUpper(key)
1412 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5BadParams4, "duplicate param %q", key)
1420 if size > c.maxMessageSize {
1422 ecode := smtp.SeSys3MsgLimitExceeded4
1423 if size < config.DefaultMaxMsgSize {
1424 ecode = smtp.SeMailbox2MsgLimitExceeded3
1426 xsmtpUserErrorf(smtp.C552MailboxFull, ecode, "message too large")
1428 // We won't verify the message is exactly the size the remote claims. Buf if it is
1429 // larger, we'll abort the transaction when remote crosses the boundary.
1433 v := p.xparamValue()
1434 switch strings.ToUpper(v) {
1436 c.has8bitmime = false
1438 c.has8bitmime = true
1440 xsmtpUserErrorf(smtp.C555UnrecognizedAddrParams, smtp.SeProto5BadParams4, "unrecognized parameter %q", key)
1445 // We act as if we don't trust the client to specify a mailbox. Instead, we always
1446 // check the rfc5321.mailfrom and rfc5322.from before accepting the submission.
1450 // todo future: should we accept utf-8-addr-xtext if there is no smtputf8, and utf-8 if there is? need to find a spec
../rfc/6533:259
1458 c.msgsmtputf8 = true
1462 xsmtpUserErrorf(smtp.C530SecurityRequired, smtp.SePol7EncNeeded10, "requiretls only allowed on tls-encrypted connections")
1463 } else if !c.extRequireTLS {
1464 xsmtpUserErrorf(smtp.C555UnrecognizedAddrParams, smtp.SeSys3NotSupported3, "REQUIRETLS not allowed for this connection")
1468 case "HOLDFOR", "HOLDUNTIL":
1471 xsmtpUserErrorf(smtp.C555UnrecognizedAddrParams, smtp.SeSys3NotSupported3, "unrecognized parameter %q", key)
1473 if K == "HOLDFOR" && paramSeen["HOLDUNTIL"] || K == "HOLDUNTIL" && paramSeen["HOLDFOR"] {
1475 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5BadParams4, "cannot use both HOLDUNTIL and HOLFOR")
1479 // semantic errors as syntax errors
1482 if n > int64(queue.FutureReleaseIntervalMax/time.Second) {
1484 xsmtpUserErrorf(smtp.C554TransactionFailed, smtp.SeProto5BadParams4, "future release interval too far in the future")
1486 c.futureRelease = time.Now().Add(time.Duration(n) * time.Second)
1487 c.futureReleaseRequest = fmt.Sprintf("for;%d", n)
1489 t, s := p.xdatetimeutc()
1490 ival := time.Until(t)
1492 // Likely a mistake by the user.
1493 xsmtpUserErrorf(smtp.C554TransactionFailed, smtp.SeProto5BadParams4, "requested future release time is in the past")
1494 } else if ival > queue.FutureReleaseIntervalMax {
1496 xsmtpUserErrorf(smtp.C554TransactionFailed, smtp.SeProto5BadParams4, "requested future release time is too far in the future")
1499 c.futureReleaseRequest = "until;" + s
1503 xsmtpUserErrorf(smtp.C555UnrecognizedAddrParams, smtp.SeSys3NotSupported3, "unrecognized parameter %q", key)
1507 // We now know if we have to parse the address with support for utf8.
1508 pp := newParser(rawRevPath, c.smtputf8, c)
1509 rpath := pp.xbareReversePath()
1514 // For submission, check if reverse path is allowed. I.e. authenticated account
1515 // must have the rpath configured. We do a check again on rfc5322.from during DATA.
1516 rpathAllowed := func() bool {
1521 accName, _, _, _, err := mox.LookupAddress(rpath.Localpart, rpath.IPDomain.Domain, false, false)
1522 return err == nil && accName == c.account.Name
1525 if !c.submission && !rpath.IPDomain.Domain.IsZero() {
1526 // If rpath domain has null MX record or is otherwise not accepting email, reject.
1529 cidctx := context.WithValue(mox.Context, mlog.CidKey, c.cid)
1530 ctx, cancel := context.WithTimeout(cidctx, time.Minute)
1531 valid, err := checkMXRecords(ctx, c.resolver, rpath.IPDomain.Domain)
1534 c.log.Infox("temporary reject for temporary mx lookup error", err)
1535 xsmtpServerErrorf(codes{smtp.C451LocalErr, smtp.SeNet4Other0}, "cannot verify mx records for mailfrom domain")
1537 c.log.Info("permanent reject because mailfrom domain does not accept mail")
1538 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SePol7SenderHasNullMX27, "mailfrom domain not configured for mail")
1542 if c.submission && (len(rpath.IPDomain.IP) > 0 || !rpathAllowed()) {
1544 c.log.Info("submission with unconfigured mailfrom", slog.String("user", c.username), slog.String("mailfrom", rpath.String()))
1545 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SePol7DeliveryUnauth1, "must match authenticated user")
1546 } else if !c.submission && len(rpath.IPDomain.IP) > 0 {
1547 // todo future: allow if the IP is the same as this connection is coming from? does later code allow this?
1548 c.log.Info("delivery from address without domain", slog.String("mailfrom", rpath.String()))
1549 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SePol7Other0, "domain name required")
1552 if Localserve && strings.HasPrefix(string(rpath.Localpart), "mailfrom") {
1553 c.xlocalserveError(rpath.Localpart)
1558 c.bwritecodeline(smtp.C250Completed, smtp.SeAddr1Other0, "looking good", nil)
1562func (c *conn) cmdRcpt(p *parser) {
1565 if c.mailFrom == nil {
1567 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "missing MAIL FROM")
1573 // Microsoft Outlook 365 Apps for Enterprise sends it with submission. For delivery
1574 // it is mostly used by spammers, but has been seen with legitimate senders too.
1579 if p.take("<POSTMASTER>") {
1580 fpath = smtp.Path{Localpart: "postmaster"}
1582 fpath = p.xforwardPath()
1586 key := p.xparamKeyword()
1587 // K := strings.ToUpper(key)
1590 xsmtpUserErrorf(smtp.C555UnrecognizedAddrParams, smtp.SeSys3NotSupported3, "unrecognized parameter %q", key)
1594 // Check if TLS is enabled if required. It's not great that sender/recipient
1595 // addresses may have been exposed in plaintext before we can reject delivery. The
1596 // recipient could be the tls reporting addresses, which must always be able to
1597 // receive in plain text.
1598 c.xneedTLSForDelivery(fpath)
1600 // todo future: for submission, should we do explicit verification that domains are fully qualified? also for mail from.
../rfc/6409:420
1602 if len(c.recipients) >= rcptToLimit {
1604 xsmtpUserErrorf(smtp.C452StorageFull, smtp.SeProto5TooManyRcpts3, "max of %d recipients reached", rcptToLimit)
1607 // We don't want to allow delivery to multiple recipients with a null reverse path.
1608 // Why would anyone send like that? Null reverse path is intended for delivery
1609 // notifications, they should go to a single recipient.
1610 if !c.submission && len(c.recipients) > 0 && c.mailFrom.IsZero() {
1611 xsmtpUserErrorf(smtp.C452StorageFull, smtp.SeProto5TooManyRcpts3, "only one recipient allowed with null reverse address")
1614 // Do not accept multiple recipients if remote does not pass SPF. Because we don't
1615 // want to generate DSNs to unverified domains. This is the moment we
1616 // can refuse individual recipients, DATA will be too late. Because mail
1617 // servers must handle a max recipient limit gracefully and still send to the
1618 // recipients that are accepted, this should not cause problems. Though we are in
1619 // violation because the limit must be >= 100.
1623 if !c.submission && len(c.recipients) == 1 && !Localserve {
1624 // note: because of check above, mailFrom cannot be the null address.
1626 d := c.mailFrom.IPDomain.Domain
1628 // todo: use this spf result for DATA.
1629 spfArgs := spf.Args{
1630 RemoteIP: c.remoteIP,
1631 MailFromLocalpart: c.mailFrom.Localpart,
1633 HelloDomain: c.hello,
1635 LocalHostname: c.hostname,
1637 cidctx := context.WithValue(mox.Context, mlog.CidKey, c.cid)
1638 spfctx, spfcancel := context.WithTimeout(cidctx, time.Minute)
1640 receivedSPF, _, _, _, err := spf.Verify(spfctx, c.log.Logger, c.resolver, spfArgs)
1643 c.log.Errorx("spf verify for multiple recipients", err)
1645 pass = receivedSPF.Identity == spf.ReceivedMailFrom && receivedSPF.Result == spf.StatusPass
1648 xsmtpUserErrorf(smtp.C452StorageFull, smtp.SeProto5TooManyRcpts3, "only one recipient allowed without spf pass")
1652 if Localserve && strings.HasPrefix(string(fpath.Localpart), "rcptto") {
1653 c.xlocalserveError(fpath.Localpart)
1656 if len(fpath.IPDomain.IP) > 0 {
1658 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeAddr1UnknownDestMailbox1, "not accepting email for ip")
1660 c.recipients = append(c.recipients, recipient{fpath, nil, nil})
1661 } else if accountName, alias, canonical, addr, err := mox.LookupAddress(fpath.Localpart, fpath.IPDomain.Domain, true, true); err == nil {
1664 c.recipients = append(c.recipients, recipient{fpath, nil, &rcptAlias{*alias, canonical}})
1666 c.recipients = append(c.recipients, recipient{fpath, &rcptAccount{accountName, addr, canonical}, nil})
1669 } else if Localserve {
1670 // If the address isn't known, and we are in localserve, deliver to the mox user.
1671 // If account or destination doesn't exist, it will be handled during delivery. For
1672 // submissions, which is the common case, we'll deliver to the logged in user,
1673 // which is typically the mox user.
1674 acc, _ := mox.Conf.Account("mox")
1675 dest := acc.Destinations["mox@localhost"]
1676 c.recipients = append(c.recipients, recipient{fpath, &rcptAccount{"mox", dest, "mox@localhost"}, nil})
1677 } else if errors.Is(err, mox.ErrDomainNotFound) {
1679 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeAddr1UnknownDestMailbox1, "not accepting email for domain")
1681 // We'll be delivering this email.
1682 c.recipients = append(c.recipients, recipient{fpath, nil, nil})
1683 } else if errors.Is(err, mox.ErrAddressNotFound) {
1685 // For submission, we're transparent about which user exists. Should be fine for the typical small-scale deploy.
1687 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeAddr1UnknownDestMailbox1, "no such user")
1689 // We pretend to accept. We don't want to let remote know the user does not exist
1690 // until after DATA. Because then remote has committed to sending a message.
1691 // note: not local for !c.submission is the signal this address is in error.
1692 c.recipients = append(c.recipients, recipient{fpath, nil, nil})
1694 c.log.Errorx("looking up account for delivery", err, slog.Any("rcptto", fpath))
1695 xsmtpServerErrorf(codes{smtp.C451LocalErr, smtp.SeSys3Other0}, "error processing")
1697 c.bwritecodeline(smtp.C250Completed, smtp.SeAddr1Other0, "now on the list", nil)
1701func (c *conn) isSMTPUTF8Required(part *message.Part) bool {
1702 hasNonASCII := func(r io.Reader) bool {
1703 br := bufio.NewReader(r)
1705 b, err := br.ReadByte()
1709 xcheckf(err, "read header")
1710 if b > unicode.MaxASCII {
1716 var hasNonASCIIPartHeader func(p *message.Part) bool
1717 hasNonASCIIPartHeader = func(p *message.Part) bool {
1718 if hasNonASCII(p.HeaderReader()) {
1721 for _, pp := range p.Parts {
1722 if hasNonASCIIPartHeader(&pp) {
1729 // Check "MAIL FROM".
1730 if hasNonASCII(strings.NewReader(string(c.mailFrom.Localpart))) {
1733 // Check all "RCPT TO".
1734 for _, rcpt := range c.recipients {
1735 if hasNonASCII(strings.NewReader(string(rcpt.addr.Localpart))) {
1739 // Check header in all message parts.
1740 return hasNonASCIIPartHeader(part)
1744func (c *conn) cmdData(p *parser) {
1747 if c.mailFrom == nil {
1749 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "missing MAIL FROM")
1751 if len(c.recipients) == 0 {
1753 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "missing RCPT TO")
1759 // todo future: we could start a reader for a single line. we would then create a context that would be canceled on i/o errors.
1761 // Entire delivery should be done within 30 minutes, or we abort.
1762 cidctx := context.WithValue(mox.Context, mlog.CidKey, c.cid)
1763 cmdctx, cmdcancel := context.WithTimeout(cidctx, 30*time.Minute)
1765 // Deadline is taken into account by Read and Write.
1766 c.deadline, _ = cmdctx.Deadline()
1768 c.deadline = time.Time{}
1772 c.writelinef("354 see you at the bare dot")
1774 // Mark as tracedata.
1775 defer c.xtrace(mlog.LevelTracedata)()
1777 // We read the data into a temporary file. We limit the size and do basic analysis while reading.
1778 dataFile, err := store.CreateMessageTemp(c.log, "smtp-deliver")
1780 xsmtpServerErrorf(errCodes(smtp.C451LocalErr, smtp.SeSys3Other0, err), "creating temporary file for message: %s", err)
1782 defer store.CloseRemoveTempFile(c.log, dataFile, "smtpserver delivered message")
1783 msgWriter := message.NewWriter(dataFile)
1784 dr := smtp.NewDataReader(c.r)
1785 n, err := io.Copy(&limitWriter{maxSize: c.maxMessageSize, w: msgWriter}, dr)
1786 c.xtrace(mlog.LevelTrace) // Restore.
1788 if errors.Is(err, errMessageTooLarge) {
1790 ecode := smtp.SeSys3MsgLimitExceeded4
1791 if n < config.DefaultMaxMsgSize {
1792 ecode = smtp.SeMailbox2MsgLimitExceeded3
1794 c.writecodeline(smtp.C451LocalErr, ecode, fmt.Sprintf("error copying data to file (%s)", mox.ReceivedID(c.cid)), err)
1795 panic(fmt.Errorf("remote sent too much DATA: %w", errIO))
1798 if errors.Is(err, smtp.ErrCRLF) {
1799 c.writecodeline(smtp.C500BadSyntax, smtp.SeProto5Syntax2, fmt.Sprintf("invalid bare \\r or \\n, may be smtp smuggling (%s)", mox.ReceivedID(c.cid)), err)
1803 // Something is failing on our side. We want to let remote know. So write an error response,
1804 // then discard the remaining data so the remote client is more likely to see our
1805 // response. Our write is synchronous, there is a risk no window/buffer space is
1806 // available and our write blocks us from reading remaining data, leading to
1807 // deadlock. We have a timeout on our connection writes though, so worst case we'll
1808 // abort the connection due to expiration.
1809 c.writecodeline(smtp.C451LocalErr, smtp.SeSys3Other0, fmt.Sprintf("error copying data to file (%s)", mox.ReceivedID(c.cid)), err)
1810 io.Copy(io.Discard, dr)
1814 // Basic sanity checks on messages before we send them out to the world. Just
1815 // trying to be strict in what we do to others and liberal in what we accept.
1817 if !msgWriter.HaveBody {
1819 xsmtpUserErrorf(smtp.C554TransactionFailed, smtp.SeMsg6Other0, "message requires both header and body section")
1821 // Check only for pedantic mode because ios mail will attempt to send smtputf8 with
1822 // non-ascii in message from localpart without using 8bitmime.
1823 if mox.Pedantic && msgWriter.Has8bit && !c.has8bitmime {
1825 xsmtpUserErrorf(smtp.C500BadSyntax, smtp.SeMsg6Other0, "message with non-us-ascii requires 8bitmime extension")
1829 if Localserve && mox.Pedantic {
1830 // Require that message can be parsed fully.
1831 p, err := message.Parse(c.log.Logger, false, dataFile)
1833 err = p.Walk(c.log.Logger, nil)
1837 xsmtpUserErrorf(smtp.C554TransactionFailed, smtp.SeMsg6Other0, "malformed message: %v", err)
1841 // Now that we have all the whole message (envelope + data), we can check if the SMTPUTF8 extension is required.
1842 var part *message.Part
1843 if c.smtputf8 || c.submission || mox.Pedantic {
1844 // Try to parse the message.
1845 // Do nothing if something bad happen during Parse and Walk, just keep the current value for c.msgsmtputf8.
1846 p, err := message.Parse(c.log.Logger, true, dataFile)
1848 // Message parsed without error. Keep the result to avoid parsing the message again.
1850 err = part.Walk(c.log.Logger, nil)
1852 c.msgsmtputf8 = c.isSMTPUTF8Required(part)
1855 if c.smtputf8 != c.msgsmtputf8 {
1856 c.log.Debug("smtputf8 flag changed", slog.Bool("smtputf8", c.smtputf8), slog.Bool("msgsmtputf8", c.msgsmtputf8))
1859 if !c.smtputf8 && c.msgsmtputf8 && mox.Pedantic {
1860 metricSubmission.WithLabelValues("missingsmtputf8").Inc()
1861 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeMsg6Other0, "smtputf8 extension is required but was not added to the MAIL command")
1864 // Prepare "Received" header.
1868 var iprevStatus iprev.Status // Only for delivery, not submission.
1869 var iprevAuthentic bool
1871 // Hide internal hosts.
1872 // todo future: make this a config option, where admins specify ip ranges that they don't want exposed. also see
../rfc/5321:4321
1873 recvFrom = message.HeaderCommentDomain(mox.Conf.Static.HostnameDomain, c.msgsmtputf8)
1875 if len(c.hello.IP) > 0 {
1876 recvFrom = smtp.AddressLiteral(c.hello.IP)
1878 // ASCII-only version added after the extended-domain syntax below, because the
1879 // comment belongs to "BY" which comes immediately after "FROM".
1880 recvFrom = c.hello.Domain.XName(c.msgsmtputf8)
1882 iprevctx, iprevcancel := context.WithTimeout(cmdctx, time.Minute)
1884 var revNames []string
1885 iprevStatus, revName, revNames, iprevAuthentic, err = iprev.Lookup(iprevctx, c.resolver, c.remoteIP)
1888 c.log.Infox("reverse-forward lookup", err, slog.Any("remoteip", c.remoteIP))
1890 c.log.Debug("dns iprev check", slog.Any("addr", c.remoteIP), slog.Any("status", iprevStatus))
1894 } else if len(revNames) > 0 {
1897 name = strings.TrimSuffix(name, ".")
1899 if name != "" && name != c.hello.Domain.XName(c.msgsmtputf8) {
1900 recvFrom += name + " "
1902 recvFrom += smtp.AddressLiteral(c.remoteIP) + ")"
1903 if c.msgsmtputf8 && c.hello.Domain.Unicode != "" {
1904 recvFrom += " (" + c.hello.Domain.ASCII + ")"
1907 recvBy := mox.Conf.Static.HostnameDomain.XName(c.msgsmtputf8)
1908 recvBy += " (" + smtp.AddressLiteral(c.localIP) + ")" // todo: hide ip if internal?
1909 if c.msgsmtputf8 && mox.Conf.Static.HostnameDomain.Unicode != "" {
1910 // This syntax is part of "VIA".
1911 recvBy += " (" + mox.Conf.Static.HostnameDomain.ASCII + ")"
1924 if c.account != nil {
1929 // Assume transaction does not succeed. If it does, we'll compensate.
1932 recvHdrFor := func(rcptTo string) string {
1933 recvHdr := &message.HeaderWriter{}
1934 // For additional Received-header clauses, see:
1935 // https://www.iana.org/assignments/mail-parameters/mail-parameters.xhtml#table-mail-parameters-8
1937 if c.requireTLS != nil && *c.requireTLS {
1939 withComment = " (requiretls)"
1941 recvHdr.Add(" ", "Received:", "from", recvFrom, "by", recvBy, "via", "tcp", "with", with+withComment, "id", mox.ReceivedID(c.cid)) //
../rfc/5321:3158
1943 tlsConn := c.conn.(*tls.Conn)
1944 tlsComment := mox.TLSReceivedComment(c.log, tlsConn.ConnectionState())
1945 recvHdr.Add(" ", tlsComment...)
1947 // We leave out an empty "for" clause. This is empty for messages submitted to
1948 // multiple recipients, so the message stays identical and a single smtp
1949 // transaction can deliver, only transferring the data once.
1951 recvHdr.Add(" ", "for", "<"+rcptTo+">;")
1953 recvHdr.Add(" ", time.Now().Format(message.RFC5322Z))
1954 return recvHdr.String()
1957 // Submission is easiest because user is trusted. Far fewer checks to make. So
1958 // handle it first, and leave the rest of the function for handling wild west
1959 // internet traffic.
1961 c.submit(cmdctx, recvHdrFor, msgWriter, dataFile, part)
1963 c.deliver(cmdctx, recvHdrFor, msgWriter, iprevStatus, iprevAuthentic, dataFile)
1967// Check if a message has unambiguous "TLS-Required: No" header. Messages must not
1968// contain multiple TLS-Required headers. The only valid value is "no". But we'll
1969// accept multiple headers as long as all they are all "no".
1971func hasTLSRequiredNo(h textproto.MIMEHeader) bool {
1972 l := h.Values("Tls-Required")
1976 for _, v := range l {
1977 if !strings.EqualFold(v, "no") {
1984// submit is used for mail from authenticated users that we will try to deliver.
1985func (c *conn) submit(ctx context.Context, recvHdrFor func(string) string, msgWriter *message.Writer, dataFile *os.File, part *message.Part) {
1986 // Similar between ../smtpserver/server.go:/submit\( and ../webmail/api.go:/MessageSubmit\( and ../webapisrv/server.go:/Send\(
1988 var msgPrefix []byte
1990 // Check that user is only sending email as one of its configured identities. Not
1994 msgFrom, _, header, err := message.From(c.log.Logger, true, dataFile, part)
1996 metricSubmission.WithLabelValues("badmessage").Inc()
1997 c.log.Infox("parsing message From address", err, slog.String("user", c.username))
1998 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeMsg6Other0, "cannot parse header or From address: %v", err)
2000 if !mox.AllowMsgFrom(c.account.Name, msgFrom) {
2002 metricSubmission.WithLabelValues("badfrom").Inc()
2003 c.log.Infox("verifying message from address", mox.ErrAddressNotFound, slog.String("user", c.username), slog.Any("msgfrom", msgFrom))
2004 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SePol7DeliveryUnauth1, "message from address must belong to authenticated user")
2007 // TLS-Required: No header makes us not enforce recipient domain's TLS policy.
2010 if c.requireTLS == nil && hasTLSRequiredNo(header) {
2015 // Outgoing messages should not have a Return-Path header. The final receiving mail
2016 // server will add it.
2018 if mox.Pedantic && header.Values("Return-Path") != nil {
2019 metricSubmission.WithLabelValues("badheader").Inc()
2020 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeMsg6Other0, "message should not have Return-Path header")
2023 // Add Message-Id header if missing.
2025 messageID := header.Get("Message-Id")
2026 if messageID == "" {
2027 messageID = mox.MessageIDGen(c.msgsmtputf8)
2028 msgPrefix = append(msgPrefix, fmt.Sprintf("Message-Id: <%s>\r\n", messageID)...)
2032 if header.Get("Date") == "" {
2033 msgPrefix = append(msgPrefix, "Date: "+time.Now().Format(message.RFC5322Z)+"\r\n"...)
2036 // Check outgoing message rate limit.
2037 err = c.account.DB.Read(ctx, func(tx *bstore.Tx) error {
2038 rcpts := make([]smtp.Path, len(c.recipients))
2039 for i, r := range c.recipients {
2042 msglimit, rcptlimit, err := c.account.SendLimitReached(tx, rcpts)
2043 xcheckf(err, "checking sender limit")
2045 metricSubmission.WithLabelValues("messagelimiterror").Inc()
2046 xsmtpUserErrorf(smtp.C451LocalErr, smtp.SePol7DeliveryUnauth1, "max number of messages (%d) over past 24h reached, try increasing per-account setting MaxOutgoingMessagesPerDay", msglimit)
2047 } else if rcptlimit >= 0 {
2048 metricSubmission.WithLabelValues("recipientlimiterror").Inc()
2049 xsmtpUserErrorf(smtp.C451LocalErr, smtp.SePol7DeliveryUnauth1, "max number of new/first-time recipients (%d) over past 24h reached, try increasing per-account setting MaxFirstTimeRecipientsPerDay", rcptlimit)
2053 xcheckf(err, "read-only transaction")
2055 // We gather any X-Mox-Extra-* headers into the "extra" data during queueing, which
2056 // will make it into any webhook we deliver.
2057 // todo: remove the X-Mox-Extra-* headers from the message. we don't currently rewrite the message...
2058 // todo: should we not canonicalize keys?
2059 var extra map[string]string
2060 for k, vl := range header {
2061 if !strings.HasPrefix(k, "X-Mox-Extra-") {
2065 extra = map[string]string{}
2067 xk := k[len("X-Mox-Extra-"):]
2068 // We don't allow duplicate keys.
2069 if _, ok := extra[xk]; ok || len(vl) > 1 {
2070 xsmtpUserErrorf(smtp.C554TransactionFailed, smtp.SeMsg6Other0, "duplicate x-mox-extra- key %q", xk)
2072 extra[xk] = vl[len(vl)-1]
2075 // todo future: in a pedantic mode, we can parse the headers, and return an error if rcpt is only in To or Cc header, and not in the non-empty Bcc header. indicates a client that doesn't blind those bcc's.
2077 // Add DKIM signatures.
2078 confDom, ok := mox.Conf.Domain(msgFrom.Domain)
2080 c.log.Error("domain disappeared", slog.Any("domain", msgFrom.Domain))
2081 xsmtpServerErrorf(codes{smtp.C451LocalErr, smtp.SeSys3Other0}, "internal error")
2084 selectors := mox.DKIMSelectors(confDom.DKIM)
2085 if len(selectors) > 0 {
2086 canonical := mox.CanonicalLocalpart(msgFrom.Localpart, confDom)
2087 if dkimHeaders, err := dkim.Sign(ctx, c.log.Logger, canonical, msgFrom.Domain, selectors, c.msgsmtputf8, store.FileMsgReader(msgPrefix, dataFile)); err != nil {
2088 c.log.Errorx("dkim sign for domain", err, slog.Any("domain", msgFrom.Domain))
2089 metricServerErrors.WithLabelValues("dkimsign").Inc()
2091 msgPrefix = append(msgPrefix, []byte(dkimHeaders)...)
2095 authResults := message.AuthResults{
2096 Hostname: mox.Conf.Static.HostnameDomain.XName(c.msgsmtputf8),
2097 Comment: mox.Conf.Static.HostnameDomain.ASCIIExtra(c.msgsmtputf8),
2098 Methods: []message.AuthMethod{
2102 Props: []message.AuthProp{
2103 message.MakeAuthProp("smtp", "mailfrom", c.mailFrom.XString(c.msgsmtputf8), true, c.mailFrom.ASCIIExtra(c.msgsmtputf8)),
2108 msgPrefix = append(msgPrefix, []byte(authResults.Header())...)
2110 // We always deliver through the queue. It would be more efficient to deliver
2111 // directly for local accounts, but we don't want to circumvent all the anti-spam
2112 // measures. Accounts on a single mox instance should be allowed to block each
2115 accConf, _ := c.account.Conf()
2116 loginAddr, err := smtp.ParseAddress(c.username)
2117 xcheckf(err, "parsing login address")
2118 useFromID := slices.Contains(accConf.ParsedFromIDLoginAddresses, loginAddr)
2119 var localpartBase string
2123 // With submission, user can bring their own fromid.
2124 t := strings.SplitN(string(c.mailFrom.Localpart), confDom.LocalpartCatchallSeparator, 2)
2125 localpartBase = t[0]
2128 if fromID != "" && len(c.recipients) > 1 {
2129 xsmtpServerErrorf(codes{smtp.C554TransactionFailed, smtp.SeProto5TooManyRcpts3}, "cannot send to multiple recipients with chosen fromid")
2136 qml := make([]queue.Msg, len(c.recipients))
2137 for i, rcpt := range c.recipients {
2139 code, timeout := mox.LocalserveNeedsError(rcpt.addr.Localpart)
2141 c.log.Info("timing out submission due to special localpart")
2142 mox.Sleep(mox.Context, time.Hour)
2143 xsmtpServerErrorf(codes{smtp.C451LocalErr, smtp.SeSys3Other0}, "timing out submission due to special localpart")
2144 } else if code != 0 {
2145 c.log.Info("failure due to special localpart", slog.Int("code", code))
2146 xsmtpServerErrorf(codes{code, smtp.SeOther00}, "failure with code %d due to special localpart", code)
2153 fromID = xrandomID(16)
2155 fp.Localpart = smtp.Localpart(localpartBase + confDom.LocalpartCatchallSeparator + fromID)
2158 // For multiple recipients, we don't make each message prefix unique, leaving out
2159 // the "for" clause in the Received header. This allows the queue to deliver the
2160 // messages in a single smtp transaction.
2162 if len(c.recipients) == 1 {
2163 rcptTo = rcpt.addr.String()
2165 xmsgPrefix := append([]byte(recvHdrFor(rcptTo)), msgPrefix...)
2166 msgSize := int64(len(xmsgPrefix)) + msgWriter.Size
2167 qm := queue.MakeMsg(fp, rcpt.addr, msgWriter.Has8bit, c.msgsmtputf8, msgSize, messageID, xmsgPrefix, c.requireTLS, now, header.Get("Subject"))
2168 if !c.futureRelease.IsZero() {
2169 qm.NextAttempt = c.futureRelease
2170 qm.FutureReleaseRequest = c.futureReleaseRequest
2177 // todo: it would be good to have a limit on messages (count and total size) a user has in the queue. also/especially with futurerelease.
../rfc/4865:387
2178 if err := queue.Add(ctx, c.log, c.account.Name, dataFile, qml...); err != nil && errors.Is(err, queue.ErrFromID) && !genFromID {
2179 // todo: should we return this error during the "rcpt to" command?
2180 // secode is not an exact match, but seems closest.
2181 xsmtpServerErrorf(errCodes(smtp.C554TransactionFailed, smtp.SeAddr1SenderSyntax7, err), "bad fromid in smtp mail from address: %s", err)
2182 } else if err != nil {
2183 // Aborting the transaction is not great. But continuing and generating DSNs will
2184 // probably result in errors as well...
2185 metricSubmission.WithLabelValues("queueerror").Inc()
2186 c.log.Errorx("queuing message", err)
2187 xsmtpServerErrorf(errCodes(smtp.C451LocalErr, smtp.SeSys3Other0, err), "error delivering message: %v", err)
2189 metricSubmission.WithLabelValues("ok").Inc()
2190 for i, rcpt := range c.recipients {
2191 c.log.Info("messages queued for delivery",
2192 slog.Any("mailfrom", *c.mailFrom),
2193 slog.Any("rcptto", rcpt.addr),
2194 slog.Bool("smtputf8", c.smtputf8),
2195 slog.Bool("msgsmtputf8", c.msgsmtputf8),
2196 slog.Int64("msgsize", qml[i].Size))
2199 err = c.account.DB.Write(ctx, func(tx *bstore.Tx) error {
2200 for _, rcpt := range c.recipients {
2201 outgoing := store.Outgoing{Recipient: rcpt.addr.XString(true)}
2202 if err := tx.Insert(&outgoing); err != nil {
2203 return fmt.Errorf("adding outgoing message: %v", err)
2208 xcheckf(err, "adding outgoing messages")
2211 c.transactionBad-- // Compensate for early earlier pessimistic increase.
2214 c.writecodeline(smtp.C250Completed, smtp.SeMailbox2Other0, "it is done", nil)
2217func xrandomID(n int) string {
2218 return base64.RawURLEncoding.EncodeToString(xrandom(n))
2221func xrandom(n int) []byte {
2222 buf := make([]byte, n)
2223 x, err := cryptorand.Read(buf)
2224 xcheckf(err, "read random")
2226 xcheckf(errors.New("short random read"), "read random")
2231func ipmasked(ip net.IP) (string, string, string) {
2232 if ip.To4() != nil {
2234 m2 := ip.Mask(net.CIDRMask(26, 32)).String()
2235 m3 := ip.Mask(net.CIDRMask(21, 32)).String()
2238 m1 := ip.Mask(net.CIDRMask(64, 128)).String()
2239 m2 := ip.Mask(net.CIDRMask(48, 128)).String()
2240 m3 := ip.Mask(net.CIDRMask(32, 128)).String()
2244func (c *conn) xlocalserveError(lp smtp.Localpart) {
2245 code, timeout := mox.LocalserveNeedsError(lp)
2247 c.log.Info("timing out due to special localpart")
2248 mox.Sleep(mox.Context, time.Hour)
2249 xsmtpServerErrorf(codes{smtp.C451LocalErr, smtp.SeSys3Other0}, "timing out command due to special localpart")
2250 } else if code != 0 {
2251 c.log.Info("failure due to special localpart", slog.Int("code", code))
2252 metricDelivery.WithLabelValues("delivererror", "localserve").Inc()
2253 xsmtpServerErrorf(codes{code, smtp.SeOther00}, "failure with code %d due to special localpart", code)
2257// deliver is called for incoming messages from external, typically untrusted
2258// sources. i.e. not submitted by authenticated users.
2259func (c *conn) deliver(ctx context.Context, recvHdrFor func(string) string, msgWriter *message.Writer, iprevStatus iprev.Status, iprevAuthentic bool, dataFile *os.File) {
2260 // todo: in decision making process, if we run into (some) temporary errors, attempt to continue. if we decide to accept, all good. if we decide to reject, we'll make it a temporary reject.
2262 var msgFrom smtp.Address
2263 var envelope *message.Envelope
2264 var headers textproto.MIMEHeader
2266 part, err := message.Parse(c.log.Logger, false, dataFile)
2268 // todo: is it enough to check only the the content-type header? in other places we look at the content-types of the parts before considering a message a dsn. should we change other places to this simpler check?
2269 isDSN = part.MediaType == "MULTIPART" && part.MediaSubType == "REPORT" && strings.EqualFold(part.ContentTypeParams["report-type"], "delivery-status")
2270 msgFrom, envelope, headers, err = message.From(c.log.Logger, false, dataFile, &part)
2273 c.log.Infox("parsing message for From address", err)
2277 if len(headers.Values("Received")) > 100 {
2278 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeNet4Loop6, "loop detected, more than 100 Received headers")
2281 // TLS-Required: No header makes us not enforce recipient domain's TLS policy.
2282 // Since we only deliver locally at the moment, this won't influence our behaviour.
2283 // Once we forward, it would our delivery attempts.
2286 if c.requireTLS == nil && hasTLSRequiredNo(headers) {
2291 // We'll be building up an Authentication-Results header.
2292 authResults := message.AuthResults{
2293 Hostname: mox.Conf.Static.HostnameDomain.XName(c.msgsmtputf8),
2296 commentAuthentic := func(v bool) string {
2298 return "with dnssec"
2300 return "without dnssec"
2303 // Reverse IP lookup results.
2304 // todo future: how useful is this?
2306 authResults.Methods = append(authResults.Methods, message.AuthMethod{
2308 Result: string(iprevStatus),
2309 Comment: commentAuthentic(iprevAuthentic),
2310 Props: []message.AuthProp{
2311 message.MakeAuthProp("policy", "iprev", c.remoteIP.String(), false, ""),
2315 // SPF and DKIM verification in parallel.
2316 var wg sync.WaitGroup
2320 var dkimResults []dkim.Result
2324 x := recover() // Should not happen, but don't take program down if it does.
2326 c.log.Error("dkim verify panic", slog.Any("err", x))
2328 metrics.PanicInc(metrics.Dkimverify)
2332 // We always evaluate all signatures. We want to build up reputation for each
2333 // domain in the signature.
2334 const ignoreTestMode = false
2335 // todo future: longer timeout? we have to read through the entire email, which can be large, possibly multiple times.
2336 dkimctx, dkimcancel := context.WithTimeout(ctx, time.Minute)
2338 // todo future: we could let user configure which dkim headers they require
2340 // For localserve, fake dkim selector DNS records for hosted domains to give
2341 // dkim-signatures a chance to pass for deliveries from queue.
2342 resolver := c.resolver
2344 // Lookup based on message From address is an approximation.
2345 if dc, ok := mox.Conf.Domain(msgFrom.Domain); ok && len(dc.DKIM.Selectors) > 0 {
2346 txts := map[string][]string{}
2347 for name, sel := range dc.DKIM.Selectors {
2348 dkimr := dkim.Record{
2350 Hashes: []string{sel.HashEffective},
2351 PublicKey: sel.Key.Public(),
2353 if _, ok := sel.Key.(ed25519.PrivateKey); ok {
2354 dkimr.Key = "ed25519"
2355 } else if _, ok := sel.Key.(*rsa.PrivateKey); !ok {
2356 err := fmt.Errorf("unrecognized private key for DKIM selector %q: %T", name, sel.Key)
2357 xcheckf(err, "making dkim record")
2359 txt, err := dkimr.Record()
2360 xcheckf(err, "making DKIM DNS TXT record")
2361 txts[name+"._domainkey."+msgFrom.Domain.ASCII+"."] = []string{txt}
2363 resolver = dns.MockResolver{TXT: txts}
2366 dkimResults, dkimErr = dkim.Verify(dkimctx, c.log.Logger, resolver, c.msgsmtputf8, dkim.DefaultPolicy, dataFile, ignoreTestMode)
2372 var receivedSPF spf.Received
2373 var spfDomain dns.Domain
2375 var spfAuthentic bool
2377 spfArgs := spf.Args{
2378 RemoteIP: c.remoteIP,
2379 MailFromLocalpart: c.mailFrom.Localpart,
2380 MailFromDomain: c.mailFrom.IPDomain.Domain, // Can be empty.
2381 HelloDomain: c.hello,
2383 LocalHostname: c.hostname,
2388 x := recover() // Should not happen, but don't take program down if it does.
2390 c.log.Error("spf verify panic", slog.Any("err", x))
2392 metrics.PanicInc(metrics.Spfverify)
2396 spfctx, spfcancel := context.WithTimeout(ctx, time.Minute)
2398 resolver := c.resolver
2399 // For localserve, give hosted domains a chance to pass for deliveries from queue.
2400 if Localserve && c.remoteIP.IsLoopback() {
2401 // Lookup based on message From address is an approximation.
2402 if _, ok := mox.Conf.Domain(msgFrom.Domain); ok {
2403 resolver = dns.MockResolver{
2404 TXT: map[string][]string{msgFrom.Domain.ASCII + ".": {"v=spf1 ip4:127.0.0.1/8 ip6:::1 ~all"}},
2408 receivedSPF, spfDomain, spfExpl, spfAuthentic, spfErr = spf.Verify(spfctx, c.log.Logger, resolver, spfArgs)
2411 c.log.Infox("spf verify", spfErr)
2415 // Wait for DKIM and SPF validation to finish.
2418 // Give immediate response if all recipients are unknown.
2420 for _, r := range c.recipients {
2421 if r.account == nil && r.alias == nil {
2425 if nunknown == len(c.recipients) {
2426 // During RCPT TO we found that the address does not exist.
2427 c.log.Info("deliver attempt to unknown user(s)", slog.Any("recipients", c.recipients))
2429 // Crude attempt to slow down someone trying to guess names. Would work better
2430 // with connection rate limiter.
2431 if unknownRecipientsDelay > 0 {
2432 mox.Sleep(ctx, unknownRecipientsDelay)
2435 // todo future: if remote does not look like a properly configured mail system, respond with generic 451 error? to prevent any random internet system from discovering accounts. we could give proper response if spf for ehlo or mailfrom passes.
2436 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeAddr1UnknownDestMailbox1, "no such user(s)")
2439 // Add DKIM results to Authentication-Results header.
2440 authResAddDKIM := func(result, comment, reason string, props []message.AuthProp) {
2441 dm := message.AuthMethod{
2448 authResults.Methods = append(authResults.Methods, dm)
2451 c.log.Errorx("dkim verify", dkimErr)
2452 authResAddDKIM("none", "", dkimErr.Error(), nil)
2453 } else if len(dkimResults) == 0 {
2454 c.log.Info("no dkim-signature header", slog.Any("mailfrom", c.mailFrom))
2455 authResAddDKIM("none", "", "no dkim signatures", nil)
2457 for i, r := range dkimResults {
2458 var domain, selector dns.Domain
2459 var identity *dkim.Identity
2461 var props []message.AuthProp
2463 if r.Record != nil && r.Record.PublicKey != nil {
2464 if pubkey, ok := r.Record.PublicKey.(*rsa.PublicKey); ok {
2465 comment = fmt.Sprintf("%d bit rsa, ", pubkey.N.BitLen())
2469 sig := base64.StdEncoding.EncodeToString(r.Sig.Signature)
2470 sig = sig[:12] // Must be at least 8 characters and unique among the signatures.
2471 props = []message.AuthProp{
2472 message.MakeAuthProp("header", "d", r.Sig.Domain.XName(c.msgsmtputf8), true, r.Sig.Domain.ASCIIExtra(c.msgsmtputf8)),
2473 message.MakeAuthProp("header", "s", r.Sig.Selector.XName(c.msgsmtputf8), true, r.Sig.Selector.ASCIIExtra(c.msgsmtputf8)),
2474 message.MakeAuthProp("header", "a", r.Sig.Algorithm(), false, ""),
2477 domain = r.Sig.Domain
2478 selector = r.Sig.Selector
2479 if r.Sig.Identity != nil {
2480 props = append(props, message.MakeAuthProp("header", "i", r.Sig.Identity.String(), true, ""))
2481 identity = r.Sig.Identity
2483 if r.RecordAuthentic {
2484 comment += "with dnssec"
2486 comment += "without dnssec"
2491 errmsg = r.Err.Error()
2493 authResAddDKIM(string(r.Status), comment, errmsg, props)
2494 c.log.Debugx("dkim verification result", r.Err,
2495 slog.Int("index", i),
2496 slog.Any("mailfrom", c.mailFrom),
2497 slog.Any("status", r.Status),
2498 slog.Any("domain", domain),
2499 slog.Any("selector", selector),
2500 slog.Any("identity", identity))
2504 var spfIdentity *dns.Domain
2505 var mailFromValidation = store.ValidationUnknown
2506 var ehloValidation = store.ValidationUnknown
2507 switch receivedSPF.Identity {
2508 case spf.ReceivedHELO:
2509 if len(spfArgs.HelloDomain.IP) == 0 {
2510 spfIdentity = &spfArgs.HelloDomain.Domain
2512 ehloValidation = store.SPFValidation(receivedSPF.Result)
2513 case spf.ReceivedMailFrom:
2514 spfIdentity = &spfArgs.MailFromDomain
2515 mailFromValidation = store.SPFValidation(receivedSPF.Result)
2517 var props []message.AuthProp
2518 if spfIdentity != nil {
2519 props = []message.AuthProp{message.MakeAuthProp("smtp", string(receivedSPF.Identity), spfIdentity.XName(c.msgsmtputf8), true, spfIdentity.ASCIIExtra(c.msgsmtputf8))}
2521 var spfComment string
2523 spfComment = "with dnssec"
2525 spfComment = "without dnssec"
2527 authResults.Methods = append(authResults.Methods, message.AuthMethod{
2529 Result: string(receivedSPF.Result),
2530 Comment: spfComment,
2533 switch receivedSPF.Result {
2534 case spf.StatusPass:
2535 c.log.Debug("spf pass", slog.Any("ip", spfArgs.RemoteIP), slog.String("mailfromdomain", spfArgs.MailFromDomain.ASCII)) // todo: log the domain that was actually verified.
2536 case spf.StatusFail:
2539 for _, b := range []byte(spfExpl) {
2540 if b < ' ' || b >= 0x7f {
2546 if len(spfExpl) > 800 {
2547 spfExpl = spfExpl[:797] + "..."
2549 spfExpl = "remote claims: " + spfExpl
2553 spfExpl = fmt.Sprintf("your ip %s is not on the SPF allowlist for domain %s", spfArgs.RemoteIP, spfDomain.ASCII)
2555 c.log.Info("spf fail", slog.String("explanation", spfExpl)) // todo future: get this to the client. how? in smtp session in case of a reject due to dmarc fail?
2556 case spf.StatusTemperror:
2557 c.log.Infox("spf temperror", spfErr)
2558 case spf.StatusPermerror:
2559 c.log.Infox("spf permerror", spfErr)
2560 case spf.StatusNone, spf.StatusNeutral, spf.StatusSoftfail:
2562 c.log.Error("unknown spf status, treating as None/Neutral", slog.Any("status", receivedSPF.Result))
2563 receivedSPF.Result = spf.StatusNone
2568 var dmarcResult dmarc.Result
2569 const applyRandomPercentage = true
2570 // dmarcMethod is added to authResults when delivering to recipients: accounts can
2571 // have different policy override rules.
2572 var dmarcMethod message.AuthMethod
2573 var msgFromValidation = store.ValidationNone
2574 if msgFrom.IsZero() {
2575 dmarcResult.Status = dmarc.StatusNone
2576 dmarcMethod = message.AuthMethod{
2578 Result: string(dmarcResult.Status),
2581 msgFromValidation = alignment(ctx, c.log, msgFrom.Domain, dkimResults, receivedSPF.Result, spfIdentity)
2583 // We are doing the DMARC evaluation now. But we only store it for inclusion in an
2584 // aggregate report when we actually use it. We use an evaluation for each
2585 // recipient, with each a potentially different result due to mailing
2586 // list/forwarding configuration. If we reject a message due to being spam, we
2587 // don't want to spend any resources for the sender domain, and we don't want to
2588 // give the sender any more information about us, so we won't record the
2590 // todo future: also not send for first-time senders? they could be spammers getting through our filter, don't want to give them insights either. though we currently would have no reasonable way to decide if they are still reputationless at the time we are composing/sending aggregate reports.
2592 dmarcctx, dmarccancel := context.WithTimeout(ctx, time.Minute)
2594 dmarcUse, dmarcResult = dmarc.Verify(dmarcctx, c.log.Logger, c.resolver, msgFrom.Domain, dkimResults, receivedSPF.Result, spfIdentity, applyRandomPercentage)
2597 if dmarcResult.RecordAuthentic {
2598 comment = "with dnssec"
2600 comment = "without dnssec"
2602 dmarcMethod = message.AuthMethod{
2604 Result: string(dmarcResult.Status),
2606 Props: []message.AuthProp{
2608 message.MakeAuthProp("header", "from", msgFrom.Domain.ASCII, true, msgFrom.Domain.ASCIIExtra(c.msgsmtputf8)),
2612 if dmarcResult.Status == dmarc.StatusPass && msgFromValidation == store.ValidationRelaxed {
2613 msgFromValidation = store.ValidationDMARC
2616 // todo future: consider enforcing an spf (soft)fail if there is no dmarc policy or the dmarc policy is none.
../rfc/7489:1507
2618 c.log.Debug("dmarc verification", slog.Any("result", dmarcResult.Status), slog.Any("domain", msgFrom.Domain))
2620 // Prepare for analyzing content, calculating reputation.
2621 ipmasked1, ipmasked2, ipmasked3 := ipmasked(c.remoteIP)
2622 var verifiedDKIMDomains []string
2623 dkimSeen := map[string]bool{}
2624 for _, r := range dkimResults {
2625 // A message can have multiple signatures for the same identity. For example when
2626 // signing the message multiple times with different algorithms (rsa and ed25519).
2627 if r.Status != dkim.StatusPass {
2630 d := r.Sig.Domain.Name()
2633 verifiedDKIMDomains = append(verifiedDKIMDomains, d)
2637 // When we deliver, we try to remove from rejects mailbox based on message-id.
2638 // We'll parse it when we need it, but it is the same for each recipient.
2639 var messageID string
2640 var parsedMessageID bool
2642 // We build up a DSN for each failed recipient. If we have recipients in dsnMsg
2643 // after processing, we queue the DSN. Unless all recipients failed, in which case
2644 // we may just fail the mail transaction instead (could be common for failure to
2645 // deliver to a single recipient, e.g. for junk mail).
2647 type deliverError struct {
2654 var deliverErrors []deliverError
2655 addError := func(rcpt recipient, code int, secode string, userError bool, errmsg string) {
2656 e := deliverError{rcpt.addr, code, secode, userError, errmsg}
2657 c.log.Info("deliver error",
2658 slog.Any("rcptto", e.rcptTo),
2659 slog.Int("code", code),
2660 slog.String("secode", "secode"),
2661 slog.Bool("usererror", userError),
2662 slog.String("errmsg", errmsg))
2663 deliverErrors = append(deliverErrors, e)
2666 // Sort recipients: local accounts, aliases, unknown. For ensuring we don't deliver
2667 // to an alias destination that was also explicitly sent to.
2668 rcptScore := func(r recipient) int {
2669 if r.account != nil {
2671 } else if r.alias != nil {
2676 sort.SliceStable(c.recipients, func(i, j int) bool {
2677 return rcptScore(c.recipients[i]) < rcptScore(c.recipients[j])
2680 // Return whether address is a regular explicit recipient in this transaction. Used
2681 // to prevent delivering a message to an address both for alias and explicit
2682 // addressee. Relies on c.recipients being sorted as above.
2683 regularRecipient := func(addr smtp.Path) bool {
2684 for _, rcpt := range c.recipients {
2685 if rcpt.account == nil {
2687 } else if rcpt.addr.Equal(addr) {
2694 // Prepare a message, analyze it against account's junk filter.
2695 // The returned analysis has an open account that must be closed by the caller.
2696 // We call this for all alias destinations, also when we already delivered to that
2697 // recipient: It may be the only recipient that would allow the message.
2698 messageAnalyze := func(log mlog.Log, smtpRcptTo, deliverTo smtp.Path, accountName string, destination config.Destination, canonicalAddr string) (a *analysis, rerr error) {
2699 acc, err := store.OpenAccount(log, accountName)
2701 log.Errorx("open account", err, slog.Any("account", accountName))
2702 metricDelivery.WithLabelValues("accounterror", "").Inc()
2708 log.Check(err, "closing account during analysis")
2713 Received: time.Now(),
2714 RemoteIP: c.remoteIP.String(),
2715 RemoteIPMasked1: ipmasked1,
2716 RemoteIPMasked2: ipmasked2,
2717 RemoteIPMasked3: ipmasked3,
2718 EHLODomain: c.hello.Domain.Name(),
2719 MailFrom: c.mailFrom.String(),
2720 MailFromLocalpart: c.mailFrom.Localpart,
2721 MailFromDomain: c.mailFrom.IPDomain.Domain.Name(),
2722 RcptToLocalpart: smtpRcptTo.Localpart,
2723 RcptToDomain: smtpRcptTo.IPDomain.Domain.Name(),
2724 MsgFromLocalpart: msgFrom.Localpart,
2725 MsgFromDomain: msgFrom.Domain.Name(),
2726 MsgFromOrgDomain: publicsuffix.Lookup(ctx, log.Logger, msgFrom.Domain).Name(),
2727 EHLOValidated: ehloValidation == store.ValidationPass,
2728 MailFromValidated: mailFromValidation == store.ValidationPass,
2729 MsgFromValidated: msgFromValidation == store.ValidationStrict || msgFromValidation == store.ValidationDMARC || msgFromValidation == store.ValidationRelaxed,
2730 EHLOValidation: ehloValidation,
2731 MailFromValidation: mailFromValidation,
2732 MsgFromValidation: msgFromValidation,
2733 DKIMDomains: verifiedDKIMDomains,
2735 Size: msgWriter.Size,
2738 tlsState := c.conn.(*tls.Conn).ConnectionState()
2739 m.ReceivedTLSVersion = tlsState.Version
2740 m.ReceivedTLSCipherSuite = tlsState.CipherSuite
2741 if c.requireTLS != nil {
2742 m.ReceivedRequireTLS = *c.requireTLS
2745 m.ReceivedTLSVersion = 1 // Signals plain text delivery.
2748 var msgTo, msgCc []message.Address
2749 if envelope != nil {
2753 d := delivery{c.tls, &m, dataFile, smtpRcptTo, deliverTo, destination, canonicalAddr, acc, msgTo, msgCc, msgFrom, c.dnsBLs, dmarcUse, dmarcResult, dkimResults, iprevStatus, c.smtputf8}
2755 r := analyze(ctx, log, c.resolver, d)
2759 // Either deliver the message, or call addError to register the recipient as failed.
2760 // If recipient is an alias, we may be delivering to multiple address/accounts and
2761 // we will consider a message delivered if we delivered it to at least one account
2762 // (others may be over quota).
2763 processRecipient := func(rcpt recipient) {
2764 log := c.log.With(slog.Any("mailfrom", c.mailFrom), slog.Any("rcptto", rcpt.addr))
2766 // If this is not a valid local user, we send back a DSN. This can only happen when
2767 // there are also valid recipients, and only when remote is SPF-verified, so the DSN
2768 // should not cause backscatter.
2769 // In case of serious errors, we abort the transaction. We may have already
2770 // delivered some messages. Perhaps it would be better to continue with other
2771 // deliveries, and return an error at the end? Though the failure conditions will
2772 // probably prevent any other successful deliveries too...
2774 if rcpt.account == nil && rcpt.alias == nil {
2775 metricDelivery.WithLabelValues("unknownuser", "").Inc()
2776 addError(rcpt, smtp.C550MailboxUnavail, smtp.SeAddr1UnknownDestMailbox1, true, "no such user")
2780 // la holds all analysis, and message preparation, for all accounts (multiple for
2781 // aliases). Each has an open account that we we close on return.
2784 for _, a := range la {
2785 err := a.d.acc.Close()
2786 log.Check(err, "close account")
2790 // For aliases, we prepare & analyze for each recipient. We accept the message if
2791 // any recipient accepts it. Regular destination have just a single account to
2792 // check. We check all alias destinations, even if we already explicitly delivered
2793 // to them: they may be the only destination that would accept the message.
2794 var a0 *analysis // Analysis we've used for accept/reject decision.
2795 if rcpt.alias != nil {
2796 // Check if msgFrom address is acceptable. This doesn't take validation into
2797 // consideration. If the header was forged, the message may be rejected later on.
2798 if !aliasAllowedMsgFrom(rcpt.alias.alias, msgFrom) {
2799 addError(rcpt, smtp.C550MailboxUnavail, smtp.SePol7ExpnProhibited2, true, "not allowed to send to destination")
2803 la = make([]analysis, 0, len(rcpt.alias.alias.ParsedAddresses))
2804 for _, aa := range rcpt.alias.alias.ParsedAddresses {
2805 a, err := messageAnalyze(log, rcpt.addr, aa.Address.Path(), aa.AccountName, aa.Destination, rcpt.alias.canonicalAddress)
2807 addError(rcpt, smtp.C451LocalErr, smtp.SeSys3Other0, false, "error processing")
2811 if a.accept && a0 == nil {
2812 // Address that caused us to accept.
2817 // First address, for rejecting.
2821 a, err := messageAnalyze(log, rcpt.addr, rcpt.addr, rcpt.account.accountName, rcpt.account.destination, rcpt.account.canonicalAddress)
2823 addError(rcpt, smtp.C451LocalErr, smtp.SeSys3Other0, false, "error processing")
2830 if !a0.accept && a0.reason == reasonHighRate {
2831 log.Info("incoming message rejected for high rate, not storing in rejects mailbox", slog.String("reason", a0.reason), slog.Any("msgfrom", msgFrom))
2832 metricDelivery.WithLabelValues("reject", a0.reason).Inc()
2834 addError(rcpt, a0.code, a0.secode, a0.userError, a0.errmsg)
2838 // Any DMARC result override is stored in the evaluation for outgoing DMARC
2839 // aggregate reports, and added to the Authentication-Results message header.
2840 // We want to tell the sender that we have an override, e.g. for mailing lists, so
2841 // they don't overestimate the potential damage of switching from p=none to
2843 var dmarcOverrides []string
2844 if a0.dmarcOverrideReason != "" {
2845 dmarcOverrides = []string{a0.dmarcOverrideReason}
2847 if dmarcResult.Record != nil && !dmarcUse {
2848 dmarcOverrides = append(dmarcOverrides, string(dmarcrpt.PolicyOverrideSampledOut))
2851 // Add per-recipient DMARC method to Authentication-Results. Each account can have
2852 // their own override rules, e.g. based on configured mailing lists/forwards.
2854 rcptDMARCMethod := dmarcMethod
2855 if len(dmarcOverrides) > 0 {
2856 if rcptDMARCMethod.Comment != "" {
2857 rcptDMARCMethod.Comment += ", "
2859 rcptDMARCMethod.Comment += "override " + strings.Join(dmarcOverrides, ",")
2861 rcptAuthResults := authResults
2862 rcptAuthResults.Methods = append([]message.AuthMethod{}, authResults.Methods...)
2863 rcptAuthResults.Methods = append(rcptAuthResults.Methods, rcptDMARCMethod)
2865 // Prepend reason as message header, for easy viewing in mail clients.
2867 if a0.reason != "" {
2868 hw := &message.HeaderWriter{}
2869 hw.Add(" ", "X-Mox-Reason:")
2870 hw.Add(" ", a0.reason)
2871 for i, s := range a0.reasonText {
2877 // Just in case any of the strings has a newline, replace it with space to not break the message.
2878 s = strings.ReplaceAll(s, "\n", " ")
2879 s = strings.ReplaceAll(s, "\r", " ")
2881 hw.AddWrap([]byte(s), true)
2890 la[i].d.m.MsgPrefix = []byte(
2894 rcptAuthResults.Header() +
2895 receivedSPF.Header() +
2896 recvHdrFor(rcpt.addr.String()),
2898 la[i].d.m.Size += int64(len(la[i].d.m.MsgPrefix))
2901 // Store DMARC evaluation for inclusion in an aggregate report. Only if there is at
2902 // least one reporting address: We don't want to needlessly store a row in a
2903 // database for each delivery attempt. If we reject a message for being junk, we
2904 // are also not going to send it a DMARC report. The DMARC check is done early in
2905 // the analysis, we will report on rejects because of DMARC, because it could be
2906 // valuable feedback about forwarded or mailing list messages.
2908 if !mox.Conf.Static.NoOutgoingDMARCReports && dmarcResult.Record != nil && len(dmarcResult.Record.AggregateReportAddresses) > 0 && (a0.accept && !a0.d.m.IsReject || a0.reason == reasonDMARCPolicy) {
2909 // Disposition holds our decision on whether to accept the message. Not what the
2910 // DMARC evaluation resulted in. We can override, e.g. because of mailing lists,
2911 // forwarding, or local policy.
2912 // We treat quarantine as reject, so never claim to quarantine.
2914 disposition := dmarcrpt.DispositionNone
2916 disposition = dmarcrpt.DispositionReject
2919 // unknownDomain returns whether the sender is domain with which this account has
2920 // not had positive interaction.
2921 unknownDomain := func() (unknown bool) {
2922 err := a0.d.acc.DB.Read(ctx, func(tx *bstore.Tx) (err error) {
2923 // See if we received a non-junk message from this organizational domain.
2924 q := bstore.QueryTx[store.Message](tx)
2925 q.FilterNonzero(store.Message{MsgFromOrgDomain: a0.d.m.MsgFromOrgDomain})
2926 q.FilterEqual("Notjunk", true)
2927 q.FilterEqual("IsReject", false)
2928 exists, err := q.Exists()
2930 return fmt.Errorf("querying for non-junk message from organizational domain: %v", err)
2936 // See if we sent a message to this organizational domain.
2937 qr := bstore.QueryTx[store.Recipient](tx)
2938 qr.FilterNonzero(store.Recipient{OrgDomain: a0.d.m.MsgFromOrgDomain})
2939 exists, err = qr.Exists()
2941 return fmt.Errorf("querying for message sent to organizational domain: %v", err)
2949 log.Errorx("checking if sender is unknown domain, for dmarc aggregate report evaluation", err)
2954 r := dmarcResult.Record
2955 addresses := make([]string, len(r.AggregateReportAddresses))
2956 for i, a := range r.AggregateReportAddresses {
2957 addresses[i] = a.String()
2959 sp := dmarcrpt.Disposition(r.SubdomainPolicy)
2960 if r.SubdomainPolicy == dmarc.PolicyEmpty {
2961 sp = dmarcrpt.Disposition(r.Policy)
2963 eval := dmarcdb.Evaluation{
2964 // Evaluated and IntervalHours set by AddEvaluation.
2965 PolicyDomain: dmarcResult.Domain.Name(),
2967 // Optional evaluations don't cause a report to be sent, but will be included.
2968 // Useful for automated inter-mailer messages, we don't want to get in a reporting
2969 // loop. We also don't want to be used for sending reports to unsuspecting domains
2970 // we have no relation with.
2971 // todo: would it make sense to also mark some percentage of mailing-list-policy-overrides optional? to lower the load on mail servers of folks sending to large mailing lists.
2972 Optional: a0.d.destination.DMARCReports || a0.d.destination.HostTLSReports || a0.d.destination.DomainTLSReports || a0.reason == reasonDMARCPolicy && unknownDomain(),
2974 Addresses: addresses,
2976 PolicyPublished: dmarcrpt.PolicyPublished{
2977 Domain: dmarcResult.Domain.Name(),
2978 ADKIM: dmarcrpt.Alignment(r.ADKIM),
2979 ASPF: dmarcrpt.Alignment(r.ASPF),
2980 Policy: dmarcrpt.Disposition(r.Policy),
2981 SubdomainPolicy: sp,
2982 Percentage: r.Percentage,
2983 // We don't save ReportingOptions, we don't do per-message failure reporting.
2985 SourceIP: c.remoteIP.String(),
2986 Disposition: disposition,
2987 AlignedDKIMPass: dmarcResult.AlignedDKIMPass,
2988 AlignedSPFPass: dmarcResult.AlignedSPFPass,
2989 EnvelopeTo: rcpt.addr.IPDomain.String(),
2990 EnvelopeFrom: c.mailFrom.IPDomain.String(),
2991 HeaderFrom: msgFrom.Domain.Name(),
2994 for _, s := range dmarcOverrides {
2995 reason := dmarcrpt.PolicyOverrideReason{Type: dmarcrpt.PolicyOverride(s)}
2996 eval.OverrideReasons = append(eval.OverrideReasons, reason)
2999 // We'll include all signatures for the organizational domain, even if they weren't
3000 // relevant due to strict alignment requirement.
3001 for _, dkimResult := range dkimResults {
3002 if dkimResult.Sig == nil || publicsuffix.Lookup(ctx, log.Logger, msgFrom.Domain) != publicsuffix.Lookup(ctx, log.Logger, dkimResult.Sig.Domain) {
3005 r := dmarcrpt.DKIMAuthResult{
3006 Domain: dkimResult.Sig.Domain.Name(),
3007 Selector: dkimResult.Sig.Selector.ASCII,
3008 Result: dmarcrpt.DKIMResult(dkimResult.Status),
3010 eval.DKIMResults = append(eval.DKIMResults, r)
3013 switch receivedSPF.Identity {
3014 case spf.ReceivedHELO:
3015 spfAuthResult := dmarcrpt.SPFAuthResult{
3016 Domain: spfArgs.HelloDomain.String(), // Can be unicode and also IP.
3017 Scope: dmarcrpt.SPFDomainScopeHelo,
3018 Result: dmarcrpt.SPFResult(receivedSPF.Result),
3020 eval.SPFResults = []dmarcrpt.SPFAuthResult{spfAuthResult}
3021 case spf.ReceivedMailFrom:
3022 spfAuthResult := dmarcrpt.SPFAuthResult{
3023 Domain: spfArgs.MailFromDomain.Name(), // Can be unicode.
3024 Scope: dmarcrpt.SPFDomainScopeMailFrom,
3025 Result: dmarcrpt.SPFResult(receivedSPF.Result),
3027 eval.SPFResults = []dmarcrpt.SPFAuthResult{spfAuthResult}
3030 err := dmarcdb.AddEvaluation(ctx, dmarcResult.Record.AggregateReportingInterval, &eval)
3031 log.Check(err, "adding dmarc evaluation to database for aggregate report")
3035 for _, a := range la {
3036 // Don't add message if address was also explicitly present in a RCPT TO command.
3037 if rcpt.alias != nil && regularRecipient(a.d.deliverTo) {
3041 conf, _ := a.d.acc.Conf()
3042 if conf.RejectsMailbox == "" {
3045 present, _, messagehash, err := rejectPresent(log, a.d.acc, conf.RejectsMailbox, a.d.m, dataFile)
3047 log.Errorx("checking whether reject is already present", err)
3050 log.Info("reject message is already present, ignoring")
3053 a.d.m.IsReject = true
3054 a.d.m.Seen = true // We don't want to draw attention.
3055 // Regular automatic junk flags configuration applies to these messages. The
3056 // default is to treat these as neutral, so they won't cause outright rejections
3057 // due to reputation for later delivery attempts.
3058 a.d.m.MessageHash = messagehash
3059 a.d.acc.WithWLock(func() {
3062 if !conf.KeepRejects {
3063 hasSpace, err = a.d.acc.TidyRejectsMailbox(c.log, conf.RejectsMailbox)
3066 log.Errorx("tidying rejects mailbox", err)
3067 } else if hasSpace {
3068 if err := a.d.acc.DeliverMailbox(log, conf.RejectsMailbox, a.d.m, dataFile); err != nil {
3069 log.Errorx("delivering spammy mail to rejects mailbox", err)
3071 log.Info("delivered spammy mail to rejects mailbox")
3074 log.Info("not storing spammy mail to full rejects mailbox")
3079 log.Info("incoming message rejected", slog.String("reason", a0.reason), slog.Any("msgfrom", msgFrom))
3080 metricDelivery.WithLabelValues("reject", a0.reason).Inc()
3082 addError(rcpt, a0.code, a0.secode, a0.userError, a0.errmsg)
3086 delayFirstTime := true
3087 if rcpt.account != nil && a0.dmarcReport != nil {
3089 if err := dmarcdb.AddReport(ctx, a0.dmarcReport, msgFrom.Domain); err != nil {
3090 log.Errorx("saving dmarc aggregate report in database", err)
3092 log.Info("dmarc aggregate report processed")
3093 a0.d.m.Flags.Seen = true
3094 delayFirstTime = false
3097 if rcpt.account != nil && a0.tlsReport != nil {
3098 // todo future: add rate limiting to prevent DoS attacks.
3099 if err := tlsrptdb.AddReport(ctx, c.log, msgFrom.Domain, c.mailFrom.String(), a0.d.destination.HostTLSReports, a0.tlsReport); err != nil {
3100 log.Errorx("saving TLSRPT report in database", err)
3102 log.Info("tlsrpt report processed")
3103 a0.d.m.Flags.Seen = true
3104 delayFirstTime = false
3108 // If this is a first-time sender and not a forwarded/mailing list message, wait
3109 // before actually delivering. If this turns out to be a spammer, we've kept one of
3110 // their connections busy.
3111 a0conf, _ := a0.d.acc.Conf()
3112 if delayFirstTime && !a0.d.m.IsForward && !a0.d.m.IsMailingList && a0.reason == reasonNoBadSignals && !a0conf.NoFirstTimeSenderDelay && c.firstTimeSenderDelay > 0 {
3113 log.Debug("delaying before delivering from sender without reputation", slog.Duration("delay", c.firstTimeSenderDelay))
3114 mox.Sleep(mox.Context, c.firstTimeSenderDelay)
3118 code, timeout := mox.LocalserveNeedsError(rcpt.addr.Localpart)
3120 log.Info("timing out due to special localpart")
3121 mox.Sleep(mox.Context, time.Hour)
3122 xsmtpServerErrorf(codes{smtp.C451LocalErr, smtp.SeOther00}, "timing out delivery due to special localpart")
3123 } else if code != 0 {
3124 log.Info("failure due to special localpart", slog.Int("code", code))
3125 metricDelivery.WithLabelValues("delivererror", "localserve").Inc()
3126 addError(rcpt, code, smtp.SeOther00, false, fmt.Sprintf("failure with code %d due to special localpart", code))
3131 // Gather the message-id before we deliver and the file may be consumed.
3132 if !parsedMessageID {
3133 if p, err := message.Parse(c.log.Logger, false, store.FileMsgReader(a0.d.m.MsgPrefix, dataFile)); err != nil {
3134 log.Infox("parsing message for message-id", err)
3135 } else if header, err := p.Header(); err != nil {
3136 log.Infox("parsing message header for message-id", err)
3138 messageID = header.Get("Message-Id")
3140 parsedMessageID = true
3143 // Finally deliver the message to the account(s).
3144 var nerr int // Number of non-quota errors.
3145 var nfull int // Number of failed deliveries due to over quota.
3146 var ndelivered int // Number delivered to account.
3147 for _, a := range la {
3148 // Don't deliver to recipient that was explicitly present in SMTP transaction, or
3149 // is sending the message to an alias they are member of.
3150 if rcpt.alias != nil && (regularRecipient(a.d.deliverTo) || a.d.deliverTo.Equal(msgFrom.Path())) {
3155 a.d.acc.WithWLock(func() {
3156 if err := a.d.acc.DeliverMailbox(log, a.mailbox, a.d.m, dataFile); err != nil {
3157 log.Errorx("delivering", err)
3158 metricDelivery.WithLabelValues("delivererror", a0.reason).Inc()
3159 if errors.Is(err, store.ErrOverQuota) {
3162 addError(rcpt, smtp.C451LocalErr, smtp.SeSys3Other0, false, "error processing")
3169 metricDelivery.WithLabelValues("delivered", a0.reason).Inc()
3170 log.Info("incoming message delivered", slog.String("reason", a0.reason), slog.Any("msgfrom", msgFrom))
3172 conf, _ := a.d.acc.Conf()
3173 if conf.RejectsMailbox != "" && a.d.m.MessageID != "" {
3174 if err := a.d.acc.RejectsRemove(log, conf.RejectsMailbox, a.d.m.MessageID); err != nil {
3175 log.Errorx("removing message from rejects mailbox", err, slog.String("messageid", messageID))
3180 // Pass delivered messages to queue for DSN processing and/or hooks.
3182 mr := store.FileMsgReader(a.d.m.MsgPrefix, dataFile)
3183 part, err := a.d.m.LoadPart(mr)
3185 log.Errorx("loading parsed part for evaluating webhook", err)
3187 err = queue.Incoming(context.Background(), log, a.d.acc, messageID, *a.d.m, part, a.mailbox)
3188 log.Check(err, "queueing webhook for incoming delivery")
3190 } else if nerr > 0 && ndelivered == 0 {
3191 // Don't continue if we had an error and haven't delivered yet. If we only had
3192 // quota-related errors, we keep trying for an account to deliver to.
3196 if ndelivered == 0 && (nerr > 0 || nfull > 0) {
3198 addError(rcpt, smtp.C452StorageFull, smtp.SeMailbox2Full2, true, "account storage full")
3200 addError(rcpt, smtp.C451LocalErr, smtp.SeSys3Other0, false, "error processing")
3205 // For each recipient, do final spam analysis and delivery.
3206 for _, rcpt := range c.recipients {
3207 processRecipient(rcpt)
3210 // If all recipients failed to deliver, return an error.
3211 if len(c.recipients) == len(deliverErrors) {
3213 e0 := deliverErrors[0]
3214 var serverError bool
3217 for _, e := range deliverErrors {
3218 serverError = serverError || !e.userError
3219 if e.code != e0.code || e.secode != e0.secode {
3222 msgs = append(msgs, e.errmsg)
3228 xsmtpErrorf(e0.code, e0.secode, !serverError, "%s", strings.Join(msgs, "\n"))
3231 // Not all failures had the same error. We'll return each error on a separate line.
3233 for _, e := range deliverErrors {
3234 s := fmt.Sprintf("%d %d.%s %s", e.code, e.code/100, e.secode, e.errmsg)
3235 lines = append(lines, s)
3237 code := smtp.C451LocalErr
3238 secode := smtp.SeSys3Other0
3240 code = smtp.C554TransactionFailed
3242 lines = append(lines, "multiple errors")
3243 xsmtpErrorf(code, secode, !serverError, strings.Join(lines, "\n"))
3245 // Generate one DSN for all failed recipients.
3246 if len(deliverErrors) > 0 {
3248 dsnMsg := dsn.Message{
3249 SMTPUTF8: c.msgsmtputf8,
3250 From: smtp.Path{Localpart: "postmaster", IPDomain: deliverErrors[0].rcptTo.IPDomain},
3252 Subject: "mail delivery failure",
3253 MessageID: mox.MessageIDGen(false),
3254 References: messageID,
3256 // Per-message details.
3257 ReportingMTA: mox.Conf.Static.HostnameDomain.ASCII,
3258 ReceivedFromMTA: smtp.Ehlo{Name: c.hello, ConnIP: c.remoteIP},
3262 if len(deliverErrors) > 1 {
3263 dsnMsg.TextBody = "Multiple delivery failures occurred.\n\n"
3266 for _, e := range deliverErrors {
3268 if e.code/100 == 4 {
3271 dsnMsg.TextBody += fmt.Sprintf("%s delivery failure to:\n\n\t%s\n\nError:\n\n\t%s\n\n", kind, e.errmsg, e.rcptTo.XString(false))
3272 rcpt := dsn.Recipient{
3273 FinalRecipient: e.rcptTo,
3275 Status: fmt.Sprintf("%d.%s", e.code/100, e.secode),
3276 LastAttemptDate: now,
3278 dsnMsg.Recipients = append(dsnMsg.Recipients, rcpt)
3281 header, err := message.ReadHeaders(bufio.NewReader(&moxio.AtReader{R: dataFile}))
3283 c.log.Errorx("reading headers of incoming message for dsn, continuing dsn without headers", err)
3285 dsnMsg.Original = header
3288 c.log.Error("not queueing dsn for incoming delivery due to localserve")
3289 } else if err := queueDSN(context.TODO(), c.log, c, *c.mailFrom, dsnMsg, c.requireTLS != nil && *c.requireTLS); err != nil {
3290 metricServerErrors.WithLabelValues("queuedsn").Inc()
3291 c.log.Errorx("queuing DSN for incoming delivery, no DSN sent", err)
3296 c.transactionBad-- // Compensate for early earlier pessimistic increase.
3298 c.writecodeline(smtp.C250Completed, smtp.SeMailbox2Other0, "it is done", nil)
3301// Return whether msgFrom address is allowed to send a message to alias.
3302func aliasAllowedMsgFrom(alias config.Alias, msgFrom smtp.Address) bool {
3303 for _, aa := range alias.ParsedAddresses {
3304 if aa.Address == msgFrom {
3308 lp, err := smtp.ParseLocalpart(alias.LocalpartStr)
3309 xcheckf(err, "parsing alias localpart")
3310 if msgFrom == smtp.NewAddress(lp, alias.Domain) {
3311 return alias.AllowMsgFrom
3313 return alias.PostPublic
3316// ecode returns either ecode, or a more specific error based on err.
3317// For example, ecode can be turned from an "other system" error into a "mail
3318// system full" if the error indicates no disk space is available.
3319func errCodes(code int, ecode string, err error) codes {
3321 case moxio.IsStorageSpace(err):
3323 case smtp.SeMailbox2Other0:
3324 if code == smtp.C451LocalErr {
3325 code = smtp.C452StorageFull
3327 ecode = smtp.SeMailbox2Full2
3328 case smtp.SeSys3Other0:
3329 if code == smtp.C451LocalErr {
3330 code = smtp.C452StorageFull
3332 ecode = smtp.SeSys3StorageFull1
3335 return codes{code, ecode}
3339func (c *conn) cmdRset(p *parser) {
3344 c.bwritecodeline(smtp.C250Completed, smtp.SeOther00, "all clear", nil)
3348func (c *conn) cmdVrfy(p *parser) {
3349 // No EHLO/HELO needed.
3360 // todo future: we could support vrfy and expn for submission? though would need to see if its rfc defines it.
3363 xsmtpUserErrorf(smtp.C252WithoutVrfy, smtp.SePol7Other0, "no verify but will try delivery")
3367func (c *conn) cmdExpn(p *parser) {
3368 // No EHLO/HELO needed.
3379 // todo: we could implement expn for local aliases for authenticated users, when members have permission to list. would anyone use it?
3382 xsmtpUserErrorf(smtp.C252WithoutVrfy, smtp.SePol7Other0, "no expand but will try delivery")
3386func (c *conn) cmdHelp(p *parser) {
3387 // Let's not strictly parse the request for help. We are ignoring the text anyway.
3390 c.bwritecodeline(smtp.C214Help, smtp.SeOther00, "see rfc 5321 (smtp)", nil)
3394func (c *conn) cmdNoop(p *parser) {
3395 // No idea why, but if an argument follows, it must adhere to the string ABNF production...
3402 c.bwritecodeline(smtp.C250Completed, smtp.SeOther00, "alrighty", nil)
3406func (c *conn) cmdQuit(p *parser) {
3410 c.writecodeline(smtp.C221Closing, smtp.SeOther00, "okay thanks bye", nil)