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)
754 c.log.Errorx("smtp error", serr.err, slog.Int("code", serr.code), slog.String("secode", serr.secode))
758 // Other type of panic, we pass it on, aborting the connection.
759 c.log.Errorx("command panic", err)
764 // todo future: we could wait for either a line or shutdown, and just close the connection on shutdown.
767 t := strings.SplitN(line, " ", 2)
773 cmdl := strings.ToLower(cmd)
775 // 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
778 case <-mox.Shutdown.Done():
780 c.writecodeline(smtp.C421ServiceUnavail, smtp.SeSys3NotAccepting2, "shutting down", nil)
786 c.cmdStart = time.Now()
788 p := newParser(args, c.smtputf8, c)
789 fn, ok := commands[cmdl]
793 // Other side is likely speaking something else than SMTP, send error message and
794 // stop processing because there is a good chance whatever they sent has multiple
796 c.writecodeline(smtp.C500BadSyntax, smtp.SeProto5Syntax2, "please try again speaking smtp", nil)
800 xsmtpUserErrorf(smtp.C500BadSyntax, smtp.SeProto5BadCmdOrSeq1, "unknown command")
806// For use in metric labels.
807func (c *conn) kind() string {
814func (c *conn) xneedHello() {
815 if c.hello.IsZero() {
816 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "no ehlo/helo yet")
820// If smtp server is configured to require TLS for all mail delivery (except to TLS
821// reporting address), abort command.
822func (c *conn) xneedTLSForDelivery(rcpt smtp.Path) {
823 // For TLS reports, we allow the message in even without TLS, because there may be
825 if c.requireTLSForDelivery && !c.tls && !isTLSReportRecipient(rcpt) {
827 xsmtpUserErrorf(smtp.C530SecurityRequired, smtp.SePol7Other0, "STARTTLS required for mail delivery")
831func isTLSReportRecipient(rcpt smtp.Path) bool {
832 _, _, _, dest, err := mox.LookupAddress(rcpt.Localpart, rcpt.IPDomain.Domain, false, false)
833 return err == nil && (dest.HostTLSReports || dest.DomainTLSReports)
836func (c *conn) cmdHelo(p *parser) {
840func (c *conn) cmdEhlo(p *parser) {
845func (c *conn) cmdHello(p *parser, ehlo bool) {
846 var remote dns.IPDomain
847 if c.submission && !mox.Pedantic {
848 // Mail clients regularly put bogus information in the hostname/ip. For submission,
849 // the value is of no use, so there is not much point in annoying the user with
850 // errors they cannot fix themselves. Except when in pedantic mode.
851 remote = dns.IPDomain{IP: c.remoteIP}
855 remote = p.xipdomain(true)
857 remote = dns.IPDomain{Domain: p.xdomain()}
859 // Verify a remote domain name has an A or AAAA record, CNAME not allowed.
../rfc/5321:722
860 cidctx := context.WithValue(mox.Context, mlog.CidKey, c.cid)
861 ctx, cancel := context.WithTimeout(cidctx, time.Minute)
862 _, _, err := c.resolver.LookupIPAddr(ctx, remote.Domain.ASCII+".")
864 if dns.IsNotFound(err) {
865 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeProto5Other0, "your ehlo domain does not resolve to an IP address")
867 // For success or temporary resolve errors, we'll just continue.
870 // Though a few paragraphs earlier is a claim additional data can occur for address
871 // literals (IP addresses), although the ABNF in that document does not allow it.
872 // We allow additional text, but only if space-separated.
873 if len(remote.IP) > 0 && p.space() {
885 // https://www.iana.org/assignments/mail-parameters/mail-parameters.xhtml
887 c.bwritelinef("250-%s", c.hostname.ASCII)
891 if !c.tls && c.tlsConfig != nil {
893 c.bwritelinef("250-STARTTLS")
894 } else if c.extRequireTLS {
897 c.bwritelinef("250-REQUIRETLS")
901 if c.tls || !c.requireTLSForAuth {
902 // We always mention the SCRAM PLUS variants, even if TLS is not active: It is a
903 // hint to the client that a TLS connection can use TLS channel binding during
904 // authentication. The client should select the bare variant when TLS isn't
905 // present, and also not indicate the server supports the PLUS variant in that
906 // case, or it would trigger the mechanism downgrade detection.
907 c.bwritelinef("250-AUTH SCRAM-SHA-256-PLUS SCRAM-SHA-256 SCRAM-SHA-1-PLUS SCRAM-SHA-1 CRAM-MD5 PLAIN LOGIN")
909 c.bwritelinef("250-AUTH ")
913 c.bwritelinef("250-FUTURERELEASE %d %s", queue.FutureReleaseIntervalMax/time.Second, t.Format(time.RFC3339))
916 // todo future? c.writelinef("250-DSN")
924func (c *conn) cmdStarttls(p *parser) {
930 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "already speaking tls")
932 if c.account != nil {
933 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "cannot starttls after authentication")
935 if c.tlsConfig == nil {
936 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "starttls not offered")
939 // We don't want to do TLS on top of c.r because it also prints protocol traces: We
940 // don't want to log the TLS stream. So we'll do TLS on the underlying connection,
941 // but make sure any bytes already read and in the buffer are used for the TLS
944 if n := c.r.Buffered(); n > 0 {
945 conn = &moxio.PrefixConn{
946 PrefixReader: io.LimitReader(c.r, int64(n)),
951 // We add the cid to the output, to help debugging in case of a failing TLS connection.
952 c.writecodeline(smtp.C220ServiceReady, smtp.SeOther00, "go! ("+mox.ReceivedID(c.cid)+")", nil)
953 tlsConn := tls.Server(conn, c.tlsConfig)
954 cidctx := context.WithValue(mox.Context, mlog.CidKey, c.cid)
955 ctx, cancel := context.WithTimeout(cidctx, time.Minute)
957 c.log.Debug("starting tls server handshake")
958 if err := tlsConn.HandshakeContext(ctx); err != nil {
959 panic(fmt.Errorf("starttls handshake: %s (%w)", err, errIO))
962 tlsversion, ciphersuite := moxio.TLSInfo(tlsConn)
963 c.log.Debug("tls server handshake done", slog.String("tls", tlsversion), slog.String("ciphersuite", ciphersuite))
965 c.tr = moxio.NewTraceReader(c.log, "RC: ", c)
966 c.tw = moxio.NewTraceWriter(c.log, "LS: ", c)
967 c.r = bufio.NewReader(c.tr)
968 c.w = bufio.NewWriter(c.tw)
975func (c *conn) cmdAuth(p *parser) {
979 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "authentication only allowed on submission ports")
981 if c.account != nil {
983 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "already authenticated")
985 if c.mailFrom != nil {
987 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "authentication not allowed during mail transaction")
990 // If authentication fails due to missing derived secrets, we don't hold it against
991 // the connection. There is no way to indicate server support for an authentication
992 // mechanism, but that a mechanism won't work for an account.
993 var missingDerivedSecrets bool
995 // For many failed auth attempts, slow down verification attempts.
996 // Dropping the connection could also work, but more so when we have a connection rate limiter.
998 if c.authFailed > 3 && authFailDelay > 0 {
1000 mox.Sleep(mox.Context, time.Duration(c.authFailed-3)*authFailDelay)
1002 c.authFailed++ // Compensated on success.
1004 if missingDerivedSecrets {
1007 // On the 3rd failed authentication, start responding slowly. Successful auth will
1008 // cause fast responses again.
1009 if c.authFailed >= 3 {
1014 var authVariant string
1015 authResult := "error"
1017 metrics.AuthenticationInc("submission", authVariant, authResult)
1018 if authResult == "ok" {
1019 mox.LimiterFailedAuth.Reset(c.remoteIP, time.Now())
1020 } else if !missingDerivedSecrets {
1021 mox.LimiterFailedAuth.Add(c.remoteIP, time.Now(), 1)
1027 mech := p.xsaslMech()
1029 // Read the first parameter, either as initial parameter or by sending a
1030 // continuation with the optional encChal (must already be base64-encoded).
1031 xreadInitial := func(encChal string) []byte {
1035 // todo future: handle max length of 12288 octets and return proper responde codes otherwise
../rfc/4954:253
1039 authResult = "aborted"
1040 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5Other0, "authentication aborted")
1045 // Windows Mail 16005.14326.21606.0 sends two spaces between "AUTH PLAIN" and the
1050 auth = p.remainder()
1053 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5Syntax2, "missing initial auth base64 parameter after space")
1054 } else if auth == "=" {
1056 auth = "" // Base64 decode below will result in empty buffer.
1059 buf, err := base64.StdEncoding.DecodeString(auth)
1062 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5Syntax2, "invalid base64: %s", err)
1067 xreadContinuation := func() []byte {
1068 line := c.readline()
1070 authResult = "aborted"
1071 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5Other0, "authentication aborted")
1073 buf, err := base64.StdEncoding.DecodeString(line)
1076 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5Syntax2, "invalid base64: %s", err)
1083 authVariant = "plain"
1087 if !c.tls && c.requireTLSForAuth {
1088 xsmtpUserErrorf(smtp.C538EncReqForAuth, smtp.SePol7EncReqForAuth11, "authentication requires tls")
1091 // Password is in line in plain text, so hide it.
1092 defer c.xtrace(mlog.LevelTraceauth)()
1093 buf := xreadInitial("")
1094 c.xtrace(mlog.LevelTrace) // Restore.
1095 plain := bytes.Split(buf, []byte{0})
1096 if len(plain) != 3 {
1097 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5BadParams4, "auth data should have 3 nul-separated tokens, got %d", len(plain))
1099 authz := norm.NFC.String(string(plain[0]))
1100 authc := norm.NFC.String(string(plain[1]))
1101 password := string(plain[2])
1103 if authz != "" && authz != authc {
1104 authResult = "badcreds"
1105 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "cannot assume other role")
1108 acc, err := store.OpenEmailAuth(c.log, authc, password)
1109 if err != nil && errors.Is(err, store.ErrUnknownCredentials) {
1111 authResult = "badcreds"
1112 c.log.Info("failed authentication attempt", slog.String("username", authc), slog.Any("remote", c.remoteIP))
1113 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass")
1115 xcheckf(err, "verifying credentials")
1123 c.writecodeline(smtp.C235AuthSuccess, smtp.SePol7Other0, "nice", nil)
1126 // LOGIN is obsoleted in favor of PLAIN, only implemented to support legacy
1127 // clients, see Internet-Draft (I-D):
1128 // https://datatracker.ietf.org/doc/html/draft-murchison-sasl-login-00
1130 authVariant = "login"
1134 if !c.tls && c.requireTLSForAuth {
1135 xsmtpUserErrorf(smtp.C538EncReqForAuth, smtp.SePol7EncReqForAuth11, "authentication requires tls")
1138 // Read user name. The I-D says the client should ignore the server challenge, but
1139 // also that some clients may require challenge "Username:" instead of "User
1140 // Name". We can't sent both... Servers most commonly return "Username:" and
1141 // "Password:", so we do the same.
1142 // I-D says maximum length must be 64 bytes. We allow more, for long user names
1144 encChal := base64.StdEncoding.EncodeToString([]byte("Username:"))
1145 username := string(xreadInitial(encChal))
1146 username = norm.NFC.String(username)
1148 // Again, client should ignore the challenge, we send the same as the example in
1150 c.writelinef("%d %s", smtp.C334ContinueAuth, base64.StdEncoding.EncodeToString([]byte("Password:")))
1152 // Password is in line in plain text, so hide it.
1153 defer c.xtrace(mlog.LevelTraceauth)()
1154 password := string(xreadContinuation())
1155 c.xtrace(mlog.LevelTrace) // Restore.
1157 acc, err := store.OpenEmailAuth(c.log, username, password)
1158 if err != nil && errors.Is(err, store.ErrUnknownCredentials) {
1160 authResult = "badcreds"
1161 c.log.Info("failed authentication attempt", slog.String("username", username), slog.Any("remote", c.remoteIP))
1162 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass")
1164 xcheckf(err, "verifying credentials")
1170 c.username = username
1172 c.writecodeline(smtp.C235AuthSuccess, smtp.SePol7Other0, "hello ancient smtp implementation", nil)
1175 authVariant = strings.ToLower(mech)
1180 chal := fmt.Sprintf("<%d.%d@%s>", uint64(mox.CryptoRandInt()), time.Now().UnixNano(), mox.Conf.Static.HostnameDomain.ASCII)
1181 c.writelinef("%d %s", smtp.C334ContinueAuth, base64.StdEncoding.EncodeToString([]byte(chal)))
1183 resp := xreadContinuation()
1184 t := strings.Split(string(resp), " ")
1185 if len(t) != 2 || len(t[1]) != 2*md5.Size {
1186 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5BadParams4, "malformed cram-md5 response")
1188 addr := norm.NFC.String(t[0])
1189 c.log.Debug("cram-md5 auth", slog.String("address", addr))
1190 acc, _, err := store.OpenEmail(c.log, addr)
1191 if err != nil && errors.Is(err, store.ErrUnknownCredentials) {
1192 c.log.Info("failed authentication attempt", slog.String("username", addr), slog.Any("remote", c.remoteIP))
1193 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass")
1195 xcheckf(err, "looking up address")
1199 c.log.Check(err, "closing account")
1202 var ipadhash, opadhash hash.Hash
1203 acc.WithRLock(func() {
1204 err := acc.DB.Read(context.TODO(), func(tx *bstore.Tx) error {
1205 password, err := bstore.QueryTx[store.Password](tx).Get()
1206 if err == bstore.ErrAbsent {
1207 c.log.Info("failed authentication attempt", slog.String("username", addr), slog.Any("remote", c.remoteIP))
1208 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass")
1214 ipadhash = password.CRAMMD5.Ipad
1215 opadhash = password.CRAMMD5.Opad
1218 xcheckf(err, "tx read")
1220 if ipadhash == nil || opadhash == nil {
1221 missingDerivedSecrets = true
1222 c.log.Info("cram-md5 auth attempt without derived secrets set, save password again to store secrets", slog.String("username", addr))
1223 c.log.Info("failed authentication attempt", slog.String("username", addr), slog.Any("remote", c.remoteIP))
1224 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass")
1228 ipadhash.Write([]byte(chal))
1229 opadhash.Write(ipadhash.Sum(nil))
1230 digest := fmt.Sprintf("%x", opadhash.Sum(nil))
1232 c.log.Info("failed authentication attempt", slog.String("username", addr), slog.Any("remote", c.remoteIP))
1233 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass")
1240 acc = nil // Cancel cleanup.
1243 c.writecodeline(smtp.C235AuthSuccess, smtp.SePol7Other0, "nice", nil)
1245 case "SCRAM-SHA-256-PLUS", "SCRAM-SHA-256", "SCRAM-SHA-1-PLUS", "SCRAM-SHA-1":
1246 // 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?
1247 // todo: use single implementation between ../imapserver/server.go and ../smtpserver/server.go
1249 // Passwords cannot be retrieved or replayed from the trace.
1251 authVariant = strings.ToLower(mech)
1252 var h func() hash.Hash
1253 switch authVariant {
1254 case "scram-sha-1", "scram-sha-1-plus":
1256 case "scram-sha-256", "scram-sha-256-plus":
1259 xsmtpServerErrorf(codes{smtp.C554TransactionFailed, smtp.SeSys3Other0}, "missing scram auth method case")
1262 var cs *tls.ConnectionState
1263 channelBindingRequired := strings.HasSuffix(authVariant, "-plus")
1264 if channelBindingRequired && !c.tls {
1266 xsmtpUserErrorf(smtp.C538EncReqForAuth, smtp.SePol7EncReqForAuth11, "scram plus mechanism requires tls connection")
1269 xcs := c.conn.(*tls.Conn).ConnectionState()
1272 c0 := xreadInitial("")
1273 ss, err := scram.NewServer(h, c0, cs, channelBindingRequired)
1275 c.log.Infox("scram protocol error", err, slog.Any("remote", c.remoteIP))
1276 xsmtpUserErrorf(smtp.C455BadParams, smtp.SePol7Other0, "scram protocol error: %s", err)
1278 authc := norm.NFC.String(ss.Authentication)
1279 c.log.Debug("scram auth", slog.String("authentication", authc))
1280 acc, _, err := store.OpenEmail(c.log, authc)
1282 // todo: we could continue scram with a generated salt, deterministically generated
1283 // from the username. that way we don't have to store anything but attackers cannot
1284 // learn if an account exists. same for absent scram saltedpassword below.
1285 c.log.Info("failed authentication attempt", slog.String("username", authc), slog.Any("remote", c.remoteIP))
1286 xsmtpUserErrorf(smtp.C454TempAuthFail, smtp.SeSys3Other0, "scram not possible")
1291 c.log.Check(err, "closing account")
1294 if ss.Authorization != "" && ss.Authorization != ss.Authentication {
1295 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "authentication with authorization for different user not supported")
1297 var xscram store.SCRAM
1298 acc.WithRLock(func() {
1299 err := acc.DB.Read(context.TODO(), func(tx *bstore.Tx) error {
1300 password, err := bstore.QueryTx[store.Password](tx).Get()
1301 if err == bstore.ErrAbsent {
1302 c.log.Info("failed authentication attempt", slog.String("username", authc), slog.Any("remote", c.remoteIP))
1303 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass")
1305 xcheckf(err, "fetching credentials")
1306 switch authVariant {
1307 case "scram-sha-1", "scram-sha-1-plus":
1308 xscram = password.SCRAMSHA1
1309 case "scram-sha-256", "scram-sha-256-plus":
1310 xscram = password.SCRAMSHA256
1312 xsmtpServerErrorf(codes{smtp.C554TransactionFailed, smtp.SeSys3Other0}, "missing scram auth credentials case")
1314 if len(xscram.Salt) == 0 || xscram.Iterations == 0 || len(xscram.SaltedPassword) == 0 {
1315 missingDerivedSecrets = true
1316 c.log.Info("scram auth attempt without derived secrets set, save password again to store secrets", slog.String("address", authc))
1317 c.log.Info("failed authentication attempt", slog.String("username", authc), slog.Any("remote", c.remoteIP))
1318 xsmtpUserErrorf(smtp.C454TempAuthFail, smtp.SeSys3Other0, "scram not possible")
1322 xcheckf(err, "read tx")
1324 s1, err := ss.ServerFirst(xscram.Iterations, xscram.Salt)
1325 xcheckf(err, "scram first server step")
1326 c.writelinef("%d %s", smtp.C334ContinueAuth, base64.StdEncoding.EncodeToString([]byte(s1))) //
../rfc/4954:187
1327 c2 := xreadContinuation()
1328 s3, err := ss.Finish(c2, xscram.SaltedPassword)
1330 c.writelinef("%d %s", smtp.C334ContinueAuth, base64.StdEncoding.EncodeToString([]byte(s3))) //
../rfc/4954:187
1333 c.readline() // Should be "*" for cancellation.
1334 if errors.Is(err, scram.ErrInvalidProof) {
1335 authResult = "badcreds"
1336 c.log.Info("failed authentication attempt", slog.String("username", authc), slog.Any("remote", c.remoteIP))
1337 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad credentials")
1338 } else if errors.Is(err, scram.ErrChannelBindingsDontMatch) {
1339 authResult = "badchanbind"
1340 c.log.Warn("bad channel binding during authentication, potential mitm", slog.String("username", authc), slog.Any("remote", c.remoteIP))
1341 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7MsgIntegrity7, "channel bindings do not match, potential mitm")
1342 } else if errors.Is(err, scram.ErrInvalidEncoding) {
1343 c.log.Infox("bad scram protocol message", err, slog.String("username", authc), slog.Any("remote", c.remoteIP))
1344 xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7Other0, "bad scram protocol message")
1346 xcheckf(err, "server final")
1350 // The message should be empty. todo: should we require it is empty?
1357 acc = nil // Cancel cleanup.
1360 c.writecodeline(smtp.C235AuthSuccess, smtp.SePol7Other0, "nice", nil)
1364 xsmtpUserErrorf(smtp.C504ParamNotImpl, smtp.SeProto5BadParams4, "mechanism %s not supported", mech)
1369func (c *conn) cmdMail(p *parser) {
1370 // requirements for maximum line length:
1372 // todo future: enforce? doesn't really seem worth it...
1374 if c.transactionBad > 10 && c.transactionGood == 0 {
1375 // If we get many bad transactions, it's probably a spammer that is guessing user names.
1376 // Useful in combination with rate limiting.
1378 c.writecodeline(smtp.C550MailboxUnavail, smtp.SeAddr1Other0, "too many failures", nil)
1384 if c.mailFrom != nil {
1386 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "already have MAIL")
1388 // Ensure clear transaction state on failure.
1399 // Microsoft Outlook 365 Apps for Enterprise sends it with submission. For delivery
1400 // it is mostly used by spammers, but has been seen with legitimate senders too.
1404 rawRevPath := p.xrawReversePath()
1405 paramSeen := map[string]bool{}
1408 key := p.xparamKeyword()
1410 K := strings.ToUpper(key)
1413 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5BadParams4, "duplicate param %q", key)
1421 if size > c.maxMessageSize {
1423 ecode := smtp.SeSys3MsgLimitExceeded4
1424 if size < config.DefaultMaxMsgSize {
1425 ecode = smtp.SeMailbox2MsgLimitExceeded3
1427 xsmtpUserErrorf(smtp.C552MailboxFull, ecode, "message too large")
1429 // We won't verify the message is exactly the size the remote claims. Buf if it is
1430 // larger, we'll abort the transaction when remote crosses the boundary.
1434 v := p.xparamValue()
1435 switch strings.ToUpper(v) {
1437 c.has8bitmime = false
1439 c.has8bitmime = true
1441 xsmtpUserErrorf(smtp.C555UnrecognizedAddrParams, smtp.SeProto5BadParams4, "unrecognized parameter %q", key)
1446 // We act as if we don't trust the client to specify a mailbox. Instead, we always
1447 // check the rfc5321.mailfrom and rfc5322.from before accepting the submission.
1451 // 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
1459 c.msgsmtputf8 = true
1463 xsmtpUserErrorf(smtp.C530SecurityRequired, smtp.SePol7EncNeeded10, "requiretls only allowed on tls-encrypted connections")
1464 } else if !c.extRequireTLS {
1465 xsmtpUserErrorf(smtp.C555UnrecognizedAddrParams, smtp.SeSys3NotSupported3, "REQUIRETLS not allowed for this connection")
1469 case "HOLDFOR", "HOLDUNTIL":
1472 xsmtpUserErrorf(smtp.C555UnrecognizedAddrParams, smtp.SeSys3NotSupported3, "unrecognized parameter %q", key)
1474 if K == "HOLDFOR" && paramSeen["HOLDUNTIL"] || K == "HOLDUNTIL" && paramSeen["HOLDFOR"] {
1476 xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5BadParams4, "cannot use both HOLDUNTIL and HOLFOR")
1480 // semantic errors as syntax errors
1483 if n > int64(queue.FutureReleaseIntervalMax/time.Second) {
1485 xsmtpUserErrorf(smtp.C554TransactionFailed, smtp.SeProto5BadParams4, "future release interval too far in the future")
1487 c.futureRelease = time.Now().Add(time.Duration(n) * time.Second)
1488 c.futureReleaseRequest = fmt.Sprintf("for;%d", n)
1490 t, s := p.xdatetimeutc()
1491 ival := time.Until(t)
1493 // Likely a mistake by the user.
1494 xsmtpUserErrorf(smtp.C554TransactionFailed, smtp.SeProto5BadParams4, "requested future release time is in the past")
1495 } else if ival > queue.FutureReleaseIntervalMax {
1497 xsmtpUserErrorf(smtp.C554TransactionFailed, smtp.SeProto5BadParams4, "requested future release time is too far in the future")
1500 c.futureReleaseRequest = "until;" + s
1504 xsmtpUserErrorf(smtp.C555UnrecognizedAddrParams, smtp.SeSys3NotSupported3, "unrecognized parameter %q", key)
1508 // We now know if we have to parse the address with support for utf8.
1509 pp := newParser(rawRevPath, c.smtputf8, c)
1510 rpath := pp.xbareReversePath()
1515 // For submission, check if reverse path is allowed. I.e. authenticated account
1516 // must have the rpath configured. We do a check again on rfc5322.from during DATA.
1517 rpathAllowed := func() bool {
1522 accName, _, _, _, err := mox.LookupAddress(rpath.Localpart, rpath.IPDomain.Domain, false, false)
1523 return err == nil && accName == c.account.Name
1526 if !c.submission && !rpath.IPDomain.Domain.IsZero() {
1527 // If rpath domain has null MX record or is otherwise not accepting email, reject.
1530 cidctx := context.WithValue(mox.Context, mlog.CidKey, c.cid)
1531 ctx, cancel := context.WithTimeout(cidctx, time.Minute)
1532 valid, err := checkMXRecords(ctx, c.resolver, rpath.IPDomain.Domain)
1535 c.log.Infox("temporary reject for temporary mx lookup error", err)
1536 xsmtpServerErrorf(codes{smtp.C451LocalErr, smtp.SeNet4Other0}, "cannot verify mx records for mailfrom domain")
1538 c.log.Info("permanent reject because mailfrom domain does not accept mail")
1539 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SePol7SenderHasNullMX27, "mailfrom domain not configured for mail")
1543 if c.submission && (len(rpath.IPDomain.IP) > 0 || !rpathAllowed()) {
1545 c.log.Info("submission with unconfigured mailfrom", slog.String("user", c.username), slog.String("mailfrom", rpath.String()))
1546 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SePol7DeliveryUnauth1, "must match authenticated user")
1547 } else if !c.submission && len(rpath.IPDomain.IP) > 0 {
1548 // todo future: allow if the IP is the same as this connection is coming from? does later code allow this?
1549 c.log.Info("delivery from address without domain", slog.String("mailfrom", rpath.String()))
1550 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SePol7Other0, "domain name required")
1553 if Localserve && strings.HasPrefix(string(rpath.Localpart), "mailfrom") {
1554 c.xlocalserveError(rpath.Localpart)
1559 c.bwritecodeline(smtp.C250Completed, smtp.SeAddr1Other0, "looking good", nil)
1563func (c *conn) cmdRcpt(p *parser) {
1566 if c.mailFrom == nil {
1568 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "missing MAIL FROM")
1574 // Microsoft Outlook 365 Apps for Enterprise sends it with submission. For delivery
1575 // it is mostly used by spammers, but has been seen with legitimate senders too.
1580 if p.take("<POSTMASTER>") {
1581 fpath = smtp.Path{Localpart: "postmaster"}
1583 fpath = p.xforwardPath()
1587 key := p.xparamKeyword()
1588 // K := strings.ToUpper(key)
1591 xsmtpUserErrorf(smtp.C555UnrecognizedAddrParams, smtp.SeSys3NotSupported3, "unrecognized parameter %q", key)
1595 // Check if TLS is enabled if required. It's not great that sender/recipient
1596 // addresses may have been exposed in plaintext before we can reject delivery. The
1597 // recipient could be the tls reporting addresses, which must always be able to
1598 // receive in plain text.
1599 c.xneedTLSForDelivery(fpath)
1601 // todo future: for submission, should we do explicit verification that domains are fully qualified? also for mail from.
../rfc/6409:420
1603 if len(c.recipients) >= rcptToLimit {
1605 xsmtpUserErrorf(smtp.C452StorageFull, smtp.SeProto5TooManyRcpts3, "max of %d recipients reached", rcptToLimit)
1608 // We don't want to allow delivery to multiple recipients with a null reverse path.
1609 // Why would anyone send like that? Null reverse path is intended for delivery
1610 // notifications, they should go to a single recipient.
1611 if !c.submission && len(c.recipients) > 0 && c.mailFrom.IsZero() {
1612 xsmtpUserErrorf(smtp.C452StorageFull, smtp.SeProto5TooManyRcpts3, "only one recipient allowed with null reverse address")
1615 // Do not accept multiple recipients if remote does not pass SPF. Because we don't
1616 // want to generate DSNs to unverified domains. This is the moment we
1617 // can refuse individual recipients, DATA will be too late. Because mail
1618 // servers must handle a max recipient limit gracefully and still send to the
1619 // recipients that are accepted, this should not cause problems. Though we are in
1620 // violation because the limit must be >= 100.
1624 if !c.submission && len(c.recipients) == 1 && !Localserve {
1625 // note: because of check above, mailFrom cannot be the null address.
1627 d := c.mailFrom.IPDomain.Domain
1629 // todo: use this spf result for DATA.
1630 spfArgs := spf.Args{
1631 RemoteIP: c.remoteIP,
1632 MailFromLocalpart: c.mailFrom.Localpart,
1634 HelloDomain: c.hello,
1636 LocalHostname: c.hostname,
1638 cidctx := context.WithValue(mox.Context, mlog.CidKey, c.cid)
1639 spfctx, spfcancel := context.WithTimeout(cidctx, time.Minute)
1641 receivedSPF, _, _, _, err := spf.Verify(spfctx, c.log.Logger, c.resolver, spfArgs)
1644 c.log.Errorx("spf verify for multiple recipients", err)
1646 pass = receivedSPF.Identity == spf.ReceivedMailFrom && receivedSPF.Result == spf.StatusPass
1649 xsmtpUserErrorf(smtp.C452StorageFull, smtp.SeProto5TooManyRcpts3, "only one recipient allowed without spf pass")
1653 if Localserve && strings.HasPrefix(string(fpath.Localpart), "rcptto") {
1654 c.xlocalserveError(fpath.Localpart)
1657 if len(fpath.IPDomain.IP) > 0 {
1659 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeAddr1UnknownDestMailbox1, "not accepting email for ip")
1661 c.recipients = append(c.recipients, recipient{fpath, nil, nil})
1662 } else if accountName, alias, canonical, addr, err := mox.LookupAddress(fpath.Localpart, fpath.IPDomain.Domain, true, true); err == nil {
1665 c.recipients = append(c.recipients, recipient{fpath, nil, &rcptAlias{*alias, canonical}})
1667 c.recipients = append(c.recipients, recipient{fpath, &rcptAccount{accountName, addr, canonical}, nil})
1670 } else if Localserve {
1671 // If the address isn't known, and we are in localserve, deliver to the mox user.
1672 // If account or destination doesn't exist, it will be handled during delivery. For
1673 // submissions, which is the common case, we'll deliver to the logged in user,
1674 // which is typically the mox user.
1675 acc, _ := mox.Conf.Account("mox")
1676 dest := acc.Destinations["mox@localhost"]
1677 c.recipients = append(c.recipients, recipient{fpath, &rcptAccount{"mox", dest, "mox@localhost"}, nil})
1678 } else if errors.Is(err, mox.ErrDomainNotFound) {
1680 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeAddr1UnknownDestMailbox1, "not accepting email for domain")
1682 // We'll be delivering this email.
1683 c.recipients = append(c.recipients, recipient{fpath, nil, nil})
1684 } else if errors.Is(err, mox.ErrAddressNotFound) {
1686 // For submission, we're transparent about which user exists. Should be fine for the typical small-scale deploy.
1688 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeAddr1UnknownDestMailbox1, "no such user")
1690 // We pretend to accept. We don't want to let remote know the user does not exist
1691 // until after DATA. Because then remote has committed to sending a message.
1692 // note: not local for !c.submission is the signal this address is in error.
1693 c.recipients = append(c.recipients, recipient{fpath, nil, nil})
1695 c.log.Errorx("looking up account for delivery", err, slog.Any("rcptto", fpath))
1696 xsmtpServerErrorf(codes{smtp.C451LocalErr, smtp.SeSys3Other0}, "error processing")
1698 c.bwritecodeline(smtp.C250Completed, smtp.SeAddr1Other0, "now on the list", nil)
1702func (c *conn) isSMTPUTF8Required(part *message.Part) bool {
1703 hasNonASCII := func(r io.Reader) bool {
1704 br := bufio.NewReader(r)
1706 b, err := br.ReadByte()
1710 xcheckf(err, "read header")
1711 if b > unicode.MaxASCII {
1717 var hasNonASCIIPartHeader func(p *message.Part) bool
1718 hasNonASCIIPartHeader = func(p *message.Part) bool {
1719 if hasNonASCII(p.HeaderReader()) {
1722 for _, pp := range p.Parts {
1723 if hasNonASCIIPartHeader(&pp) {
1730 // Check "MAIL FROM".
1731 if hasNonASCII(strings.NewReader(string(c.mailFrom.Localpart))) {
1734 // Check all "RCPT TO".
1735 for _, rcpt := range c.recipients {
1736 if hasNonASCII(strings.NewReader(string(rcpt.Addr.Localpart))) {
1740 // Check header in all message parts.
1741 return hasNonASCIIPartHeader(part)
1745func (c *conn) cmdData(p *parser) {
1748 if c.mailFrom == nil {
1750 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "missing MAIL FROM")
1752 if len(c.recipients) == 0 {
1754 xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "missing RCPT TO")
1760 // 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.
1762 // Entire delivery should be done within 30 minutes, or we abort.
1763 cidctx := context.WithValue(mox.Context, mlog.CidKey, c.cid)
1764 cmdctx, cmdcancel := context.WithTimeout(cidctx, 30*time.Minute)
1766 // Deadline is taken into account by Read and Write.
1767 c.deadline, _ = cmdctx.Deadline()
1769 c.deadline = time.Time{}
1773 c.writelinef("354 see you at the bare dot")
1775 // Mark as tracedata.
1776 defer c.xtrace(mlog.LevelTracedata)()
1778 // We read the data into a temporary file. We limit the size and do basic analysis while reading.
1779 dataFile, err := store.CreateMessageTemp(c.log, "smtp-deliver")
1781 xsmtpServerErrorf(errCodes(smtp.C451LocalErr, smtp.SeSys3Other0, err), "creating temporary file for message: %s", err)
1783 defer store.CloseRemoveTempFile(c.log, dataFile, "smtpserver delivered message")
1784 msgWriter := message.NewWriter(dataFile)
1785 dr := smtp.NewDataReader(c.r)
1786 n, err := io.Copy(&limitWriter{maxSize: c.maxMessageSize, w: msgWriter}, dr)
1787 c.xtrace(mlog.LevelTrace) // Restore.
1789 if errors.Is(err, errMessageTooLarge) {
1791 ecode := smtp.SeSys3MsgLimitExceeded4
1792 if n < config.DefaultMaxMsgSize {
1793 ecode = smtp.SeMailbox2MsgLimitExceeded3
1795 c.writecodeline(smtp.C451LocalErr, ecode, fmt.Sprintf("error copying data to file (%s)", mox.ReceivedID(c.cid)), err)
1796 panic(fmt.Errorf("remote sent too much DATA: %w", errIO))
1799 if errors.Is(err, smtp.ErrCRLF) {
1800 c.writecodeline(smtp.C500BadSyntax, smtp.SeProto5Syntax2, fmt.Sprintf("invalid bare \\r or \\n, may be smtp smuggling (%s)", mox.ReceivedID(c.cid)), err)
1804 // Something is failing on our side. We want to let remote know. So write an error response,
1805 // then discard the remaining data so the remote client is more likely to see our
1806 // response. Our write is synchronous, there is a risk no window/buffer space is
1807 // available and our write blocks us from reading remaining data, leading to
1808 // deadlock. We have a timeout on our connection writes though, so worst case we'll
1809 // abort the connection due to expiration.
1810 c.writecodeline(smtp.C451LocalErr, smtp.SeSys3Other0, fmt.Sprintf("error copying data to file (%s)", mox.ReceivedID(c.cid)), err)
1811 io.Copy(io.Discard, dr)
1815 // Basic sanity checks on messages before we send them out to the world. Just
1816 // trying to be strict in what we do to others and liberal in what we accept.
1818 if !msgWriter.HaveBody {
1820 xsmtpUserErrorf(smtp.C554TransactionFailed, smtp.SeMsg6Other0, "message requires both header and body section")
1822 // Check only for pedantic mode because ios mail will attempt to send smtputf8 with
1823 // non-ascii in message from localpart without using 8bitmime.
1824 if mox.Pedantic && msgWriter.Has8bit && !c.has8bitmime {
1826 xsmtpUserErrorf(smtp.C500BadSyntax, smtp.SeMsg6Other0, "message with non-us-ascii requires 8bitmime extension")
1830 if Localserve && mox.Pedantic {
1831 // Require that message can be parsed fully.
1832 p, err := message.Parse(c.log.Logger, false, dataFile)
1834 err = p.Walk(c.log.Logger, nil)
1838 xsmtpUserErrorf(smtp.C554TransactionFailed, smtp.SeMsg6Other0, "malformed message: %v", err)
1842 // Now that we have all the whole message (envelope + data), we can check if the SMTPUTF8 extension is required.
1843 var part *message.Part
1844 if c.smtputf8 || c.submission || mox.Pedantic {
1845 // Try to parse the message.
1846 // Do nothing if something bad happen during Parse and Walk, just keep the current value for c.msgsmtputf8.
1847 p, err := message.Parse(c.log.Logger, true, dataFile)
1849 // Message parsed without error. Keep the result to avoid parsing the message again.
1851 err = part.Walk(c.log.Logger, nil)
1853 c.msgsmtputf8 = c.isSMTPUTF8Required(part)
1856 if c.smtputf8 != c.msgsmtputf8 {
1857 c.log.Debug("smtputf8 flag changed", slog.Bool("smtputf8", c.smtputf8), slog.Bool("msgsmtputf8", c.msgsmtputf8))
1860 if !c.smtputf8 && c.msgsmtputf8 && mox.Pedantic {
1861 metricSubmission.WithLabelValues("missingsmtputf8").Inc()
1862 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeMsg6Other0, "smtputf8 extension is required but was not added to the MAIL command")
1865 // Prepare "Received" header.
1869 var iprevStatus iprev.Status // Only for delivery, not submission.
1870 var iprevAuthentic bool
1872 // Hide internal hosts.
1873 // todo future: make this a config option, where admins specify ip ranges that they don't want exposed. also see
../rfc/5321:4321
1874 recvFrom = message.HeaderCommentDomain(mox.Conf.Static.HostnameDomain, c.msgsmtputf8)
1876 if len(c.hello.IP) > 0 {
1877 recvFrom = smtp.AddressLiteral(c.hello.IP)
1879 // ASCII-only version added after the extended-domain syntax below, because the
1880 // comment belongs to "BY" which comes immediately after "FROM".
1881 recvFrom = c.hello.Domain.XName(c.msgsmtputf8)
1883 iprevctx, iprevcancel := context.WithTimeout(cmdctx, time.Minute)
1885 var revNames []string
1886 iprevStatus, revName, revNames, iprevAuthentic, err = iprev.Lookup(iprevctx, c.resolver, c.remoteIP)
1889 c.log.Infox("reverse-forward lookup", err, slog.Any("remoteip", c.remoteIP))
1891 c.log.Debug("dns iprev check", slog.Any("addr", c.remoteIP), slog.Any("status", iprevStatus))
1895 } else if len(revNames) > 0 {
1898 name = strings.TrimSuffix(name, ".")
1900 if name != "" && name != c.hello.Domain.XName(c.msgsmtputf8) {
1901 recvFrom += name + " "
1903 recvFrom += smtp.AddressLiteral(c.remoteIP) + ")"
1904 if c.msgsmtputf8 && c.hello.Domain.Unicode != "" {
1905 recvFrom += " (" + c.hello.Domain.ASCII + ")"
1908 recvBy := mox.Conf.Static.HostnameDomain.XName(c.msgsmtputf8)
1909 recvBy += " (" + smtp.AddressLiteral(c.localIP) + ")" // todo: hide ip if internal?
1910 if c.msgsmtputf8 && mox.Conf.Static.HostnameDomain.Unicode != "" {
1911 // This syntax is part of "VIA".
1912 recvBy += " (" + mox.Conf.Static.HostnameDomain.ASCII + ")"
1925 if c.account != nil {
1930 // Assume transaction does not succeed. If it does, we'll compensate.
1933 recvHdrFor := func(rcptTo string) string {
1934 recvHdr := &message.HeaderWriter{}
1935 // For additional Received-header clauses, see:
1936 // https://www.iana.org/assignments/mail-parameters/mail-parameters.xhtml#table-mail-parameters-8
1938 if c.requireTLS != nil && *c.requireTLS {
1940 withComment = " (requiretls)"
1942 recvHdr.Add(" ", "Received:", "from", recvFrom, "by", recvBy, "via", "tcp", "with", with+withComment, "id", mox.ReceivedID(c.cid)) //
../rfc/5321:3158
1944 tlsConn := c.conn.(*tls.Conn)
1945 tlsComment := mox.TLSReceivedComment(c.log, tlsConn.ConnectionState())
1946 recvHdr.Add(" ", tlsComment...)
1948 // We leave out an empty "for" clause. This is empty for messages submitted to
1949 // multiple recipients, so the message stays identical and a single smtp
1950 // transaction can deliver, only transferring the data once.
1952 recvHdr.Add(" ", "for", "<"+rcptTo+">;")
1954 recvHdr.Add(" ", time.Now().Format(message.RFC5322Z))
1955 return recvHdr.String()
1958 // Submission is easiest because user is trusted. Far fewer checks to make. So
1959 // handle it first, and leave the rest of the function for handling wild west
1960 // internet traffic.
1962 c.submit(cmdctx, recvHdrFor, msgWriter, dataFile, part)
1964 c.deliver(cmdctx, recvHdrFor, msgWriter, iprevStatus, iprevAuthentic, dataFile)
1968// Check if a message has unambiguous "TLS-Required: No" header. Messages must not
1969// contain multiple TLS-Required headers. The only valid value is "no". But we'll
1970// accept multiple headers as long as all they are all "no".
1972func hasTLSRequiredNo(h textproto.MIMEHeader) bool {
1973 l := h.Values("Tls-Required")
1977 for _, v := range l {
1978 if !strings.EqualFold(v, "no") {
1985// submit is used for mail from authenticated users that we will try to deliver.
1986func (c *conn) submit(ctx context.Context, recvHdrFor func(string) string, msgWriter *message.Writer, dataFile *os.File, part *message.Part) {
1987 // Similar between ../smtpserver/server.go:/submit\( and ../webmail/api.go:/MessageSubmit\( and ../webapisrv/server.go:/Send\(
1989 var msgPrefix []byte
1991 // Check that user is only sending email as one of its configured identities. Not
1995 msgFrom, _, header, err := message.From(c.log.Logger, true, dataFile, part)
1997 metricSubmission.WithLabelValues("badmessage").Inc()
1998 c.log.Infox("parsing message From address", err, slog.String("user", c.username))
1999 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeMsg6Other0, "cannot parse header or From address: %v", err)
2001 if !mox.AllowMsgFrom(c.account.Name, msgFrom) {
2003 metricSubmission.WithLabelValues("badfrom").Inc()
2004 c.log.Infox("verifying message from address", mox.ErrAddressNotFound, slog.String("user", c.username), slog.Any("msgfrom", msgFrom))
2005 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SePol7DeliveryUnauth1, "message from address must belong to authenticated user")
2008 // TLS-Required: No header makes us not enforce recipient domain's TLS policy.
2011 if c.requireTLS == nil && hasTLSRequiredNo(header) {
2016 // Outgoing messages should not have a Return-Path header. The final receiving mail
2017 // server will add it.
2019 if mox.Pedantic && header.Values("Return-Path") != nil {
2020 metricSubmission.WithLabelValues("badheader").Inc()
2021 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeMsg6Other0, "message should not have Return-Path header")
2024 // Add Message-Id header if missing.
2026 messageID := header.Get("Message-Id")
2027 if messageID == "" {
2028 messageID = mox.MessageIDGen(c.msgsmtputf8)
2029 msgPrefix = append(msgPrefix, fmt.Sprintf("Message-Id: <%s>\r\n", messageID)...)
2033 if header.Get("Date") == "" {
2034 msgPrefix = append(msgPrefix, "Date: "+time.Now().Format(message.RFC5322Z)+"\r\n"...)
2037 // Check outgoing message rate limit.
2038 err = c.account.DB.Read(ctx, func(tx *bstore.Tx) error {
2039 rcpts := make([]smtp.Path, len(c.recipients))
2040 for i, r := range c.recipients {
2043 msglimit, rcptlimit, err := c.account.SendLimitReached(tx, rcpts)
2044 xcheckf(err, "checking sender limit")
2046 metricSubmission.WithLabelValues("messagelimiterror").Inc()
2047 xsmtpUserErrorf(smtp.C451LocalErr, smtp.SePol7DeliveryUnauth1, "max number of messages (%d) over past 24h reached, try increasing per-account setting MaxOutgoingMessagesPerDay", msglimit)
2048 } else if rcptlimit >= 0 {
2049 metricSubmission.WithLabelValues("recipientlimiterror").Inc()
2050 xsmtpUserErrorf(smtp.C451LocalErr, smtp.SePol7DeliveryUnauth1, "max number of new/first-time recipients (%d) over past 24h reached, try increasing per-account setting MaxFirstTimeRecipientsPerDay", rcptlimit)
2054 xcheckf(err, "read-only transaction")
2056 // We gather any X-Mox-Extra-* headers into the "extra" data during queueing, which
2057 // will make it into any webhook we deliver.
2058 // todo: remove the X-Mox-Extra-* headers from the message. we don't currently rewrite the message...
2059 // todo: should we not canonicalize keys?
2060 var extra map[string]string
2061 for k, vl := range header {
2062 if !strings.HasPrefix(k, "X-Mox-Extra-") {
2066 extra = map[string]string{}
2068 xk := k[len("X-Mox-Extra-"):]
2069 // We don't allow duplicate keys.
2070 if _, ok := extra[xk]; ok || len(vl) > 1 {
2071 xsmtpUserErrorf(smtp.C554TransactionFailed, smtp.SeMsg6Other0, "duplicate x-mox-extra- key %q", xk)
2073 extra[xk] = vl[len(vl)-1]
2076 // 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.
2078 // Add DKIM signatures.
2079 confDom, ok := mox.Conf.Domain(msgFrom.Domain)
2081 c.log.Error("domain disappeared", slog.Any("domain", msgFrom.Domain))
2082 xsmtpServerErrorf(codes{smtp.C451LocalErr, smtp.SeSys3Other0}, "internal error")
2085 selectors := mox.DKIMSelectors(confDom.DKIM)
2086 if len(selectors) > 0 {
2087 canonical := mox.CanonicalLocalpart(msgFrom.Localpart, confDom)
2088 if dkimHeaders, err := dkim.Sign(ctx, c.log.Logger, canonical, msgFrom.Domain, selectors, c.msgsmtputf8, store.FileMsgReader(msgPrefix, dataFile)); err != nil {
2089 c.log.Errorx("dkim sign for domain", err, slog.Any("domain", msgFrom.Domain))
2090 metricServerErrors.WithLabelValues("dkimsign").Inc()
2092 msgPrefix = append(msgPrefix, []byte(dkimHeaders)...)
2096 authResults := message.AuthResults{
2097 Hostname: mox.Conf.Static.HostnameDomain.XName(c.msgsmtputf8),
2098 Comment: mox.Conf.Static.HostnameDomain.ASCIIExtra(c.msgsmtputf8),
2099 Methods: []message.AuthMethod{
2103 Props: []message.AuthProp{
2104 message.MakeAuthProp("smtp", "mailfrom", c.mailFrom.XString(c.msgsmtputf8), true, c.mailFrom.ASCIIExtra(c.msgsmtputf8)),
2109 msgPrefix = append(msgPrefix, []byte(authResults.Header())...)
2111 // We always deliver through the queue. It would be more efficient to deliver
2112 // directly for local accounts, but we don't want to circumvent all the anti-spam
2113 // measures. Accounts on a single mox instance should be allowed to block each
2116 accConf, _ := c.account.Conf()
2117 loginAddr, err := smtp.ParseAddress(c.username)
2118 xcheckf(err, "parsing login address")
2119 useFromID := slices.Contains(accConf.ParsedFromIDLoginAddresses, loginAddr)
2120 var localpartBase string
2124 // With submission, user can bring their own fromid.
2125 t := strings.SplitN(string(c.mailFrom.Localpart), confDom.LocalpartCatchallSeparator, 2)
2126 localpartBase = t[0]
2129 if fromID != "" && len(c.recipients) > 1 {
2130 xsmtpServerErrorf(codes{smtp.C554TransactionFailed, smtp.SeProto5TooManyRcpts3}, "cannot send to multiple recipients with chosen fromid")
2137 qml := make([]queue.Msg, len(c.recipients))
2138 for i, rcpt := range c.recipients {
2140 code, timeout := mox.LocalserveNeedsError(rcpt.Addr.Localpart)
2142 c.log.Info("timing out submission due to special localpart")
2143 mox.Sleep(mox.Context, time.Hour)
2144 xsmtpServerErrorf(codes{smtp.C451LocalErr, smtp.SeSys3Other0}, "timing out submission due to special localpart")
2145 } else if code != 0 {
2146 c.log.Info("failure due to special localpart", slog.Int("code", code))
2147 xsmtpServerErrorf(codes{code, smtp.SeOther00}, "failure with code %d due to special localpart", code)
2154 fromID = xrandomID(16)
2156 fp.Localpart = smtp.Localpart(localpartBase + confDom.LocalpartCatchallSeparator + fromID)
2159 // For multiple recipients, we don't make each message prefix unique, leaving out
2160 // the "for" clause in the Received header. This allows the queue to deliver the
2161 // messages in a single smtp transaction.
2163 if len(c.recipients) == 1 {
2164 rcptTo = rcpt.Addr.String()
2166 xmsgPrefix := append([]byte(recvHdrFor(rcptTo)), msgPrefix...)
2167 msgSize := int64(len(xmsgPrefix)) + msgWriter.Size
2168 qm := queue.MakeMsg(fp, rcpt.Addr, msgWriter.Has8bit, c.msgsmtputf8, msgSize, messageID, xmsgPrefix, c.requireTLS, now, header.Get("Subject"))
2169 if !c.futureRelease.IsZero() {
2170 qm.NextAttempt = c.futureRelease
2171 qm.FutureReleaseRequest = c.futureReleaseRequest
2178 // 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
2179 if err := queue.Add(ctx, c.log, c.account.Name, dataFile, qml...); err != nil && errors.Is(err, queue.ErrFromID) && !genFromID {
2180 // todo: should we return this error during the "rcpt to" command?
2181 // secode is not an exact match, but seems closest.
2182 xsmtpServerErrorf(errCodes(smtp.C554TransactionFailed, smtp.SeAddr1SenderSyntax7, err), "bad fromid in smtp mail from address: %s", err)
2183 } else if err != nil {
2184 // Aborting the transaction is not great. But continuing and generating DSNs will
2185 // probably result in errors as well...
2186 metricSubmission.WithLabelValues("queueerror").Inc()
2187 c.log.Errorx("queuing message", err)
2188 xsmtpServerErrorf(errCodes(smtp.C451LocalErr, smtp.SeSys3Other0, err), "error delivering message: %v", err)
2190 metricSubmission.WithLabelValues("ok").Inc()
2191 for i, rcpt := range c.recipients {
2192 c.log.Info("messages queued for delivery",
2193 slog.Any("mailfrom", *c.mailFrom),
2194 slog.Any("rcptto", rcpt.Addr),
2195 slog.Bool("smtputf8", c.smtputf8),
2196 slog.Bool("msgsmtputf8", c.msgsmtputf8),
2197 slog.Int64("msgsize", qml[i].Size))
2200 err = c.account.DB.Write(ctx, func(tx *bstore.Tx) error {
2201 for _, rcpt := range c.recipients {
2202 outgoing := store.Outgoing{Recipient: rcpt.Addr.XString(true)}
2203 if err := tx.Insert(&outgoing); err != nil {
2204 return fmt.Errorf("adding outgoing message: %v", err)
2209 xcheckf(err, "adding outgoing messages")
2212 c.transactionBad-- // Compensate for early earlier pessimistic increase.
2215 c.writecodeline(smtp.C250Completed, smtp.SeMailbox2Other0, "it is done", nil)
2218func xrandomID(n int) string {
2219 return base64.RawURLEncoding.EncodeToString(xrandom(n))
2222func xrandom(n int) []byte {
2223 buf := make([]byte, n)
2224 x, err := cryptorand.Read(buf)
2225 xcheckf(err, "read random")
2227 xcheckf(errors.New("short random read"), "read random")
2232func ipmasked(ip net.IP) (string, string, string) {
2233 if ip.To4() != nil {
2235 m2 := ip.Mask(net.CIDRMask(26, 32)).String()
2236 m3 := ip.Mask(net.CIDRMask(21, 32)).String()
2239 m1 := ip.Mask(net.CIDRMask(64, 128)).String()
2240 m2 := ip.Mask(net.CIDRMask(48, 128)).String()
2241 m3 := ip.Mask(net.CIDRMask(32, 128)).String()
2245func (c *conn) xlocalserveError(lp smtp.Localpart) {
2246 code, timeout := mox.LocalserveNeedsError(lp)
2248 c.log.Info("timing out due to special localpart")
2249 mox.Sleep(mox.Context, time.Hour)
2250 xsmtpServerErrorf(codes{smtp.C451LocalErr, smtp.SeSys3Other0}, "timing out command due to special localpart")
2251 } else if code != 0 {
2252 c.log.Info("failure due to special localpart", slog.Int("code", code))
2253 metricDelivery.WithLabelValues("delivererror", "localserve").Inc()
2254 xsmtpServerErrorf(codes{code, smtp.SeOther00}, "failure with code %d due to special localpart", code)
2258// deliver is called for incoming messages from external, typically untrusted
2259// sources. i.e. not submitted by authenticated users.
2260func (c *conn) deliver(ctx context.Context, recvHdrFor func(string) string, msgWriter *message.Writer, iprevStatus iprev.Status, iprevAuthentic bool, dataFile *os.File) {
2261 // 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.
2263 var msgFrom smtp.Address
2264 var envelope *message.Envelope
2265 var headers textproto.MIMEHeader
2267 part, err := message.Parse(c.log.Logger, false, dataFile)
2269 // 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?
2270 isDSN = part.MediaType == "MULTIPART" && part.MediaSubType == "REPORT" && strings.EqualFold(part.ContentTypeParams["report-type"], "delivery-status")
2271 msgFrom, envelope, headers, err = message.From(c.log.Logger, false, dataFile, &part)
2274 c.log.Infox("parsing message for From address", err)
2278 if len(headers.Values("Received")) > 100 {
2279 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeNet4Loop6, "loop detected, more than 100 Received headers")
2282 // TLS-Required: No header makes us not enforce recipient domain's TLS policy.
2283 // Since we only deliver locally at the moment, this won't influence our behaviour.
2284 // Once we forward, it would our delivery attempts.
2287 if c.requireTLS == nil && hasTLSRequiredNo(headers) {
2292 // We'll be building up an Authentication-Results header.
2293 authResults := message.AuthResults{
2294 Hostname: mox.Conf.Static.HostnameDomain.XName(c.msgsmtputf8),
2297 commentAuthentic := func(v bool) string {
2299 return "with dnssec"
2301 return "without dnssec"
2304 // Reverse IP lookup results.
2305 // todo future: how useful is this?
2307 authResults.Methods = append(authResults.Methods, message.AuthMethod{
2309 Result: string(iprevStatus),
2310 Comment: commentAuthentic(iprevAuthentic),
2311 Props: []message.AuthProp{
2312 message.MakeAuthProp("policy", "iprev", c.remoteIP.String(), false, ""),
2316 // SPF and DKIM verification in parallel.
2317 var wg sync.WaitGroup
2321 var dkimResults []dkim.Result
2325 x := recover() // Should not happen, but don't take program down if it does.
2327 c.log.Error("dkim verify panic", slog.Any("err", x))
2329 metrics.PanicInc(metrics.Dkimverify)
2333 // We always evaluate all signatures. We want to build up reputation for each
2334 // domain in the signature.
2335 const ignoreTestMode = false
2336 // todo future: longer timeout? we have to read through the entire email, which can be large, possibly multiple times.
2337 dkimctx, dkimcancel := context.WithTimeout(ctx, time.Minute)
2339 // todo future: we could let user configure which dkim headers they require
2341 // For localserve, fake dkim selector DNS records for hosted domains to give
2342 // dkim-signatures a chance to pass for deliveries from queue.
2343 resolver := c.resolver
2345 // Lookup based on message From address is an approximation.
2346 if dc, ok := mox.Conf.Domain(msgFrom.Domain); ok && len(dc.DKIM.Selectors) > 0 {
2347 txts := map[string][]string{}
2348 for name, sel := range dc.DKIM.Selectors {
2349 dkimr := dkim.Record{
2351 Hashes: []string{sel.HashEffective},
2352 PublicKey: sel.Key.Public(),
2354 if _, ok := sel.Key.(ed25519.PrivateKey); ok {
2355 dkimr.Key = "ed25519"
2356 } else if _, ok := sel.Key.(*rsa.PrivateKey); !ok {
2357 err := fmt.Errorf("unrecognized private key for DKIM selector %q: %T", name, sel.Key)
2358 xcheckf(err, "making dkim record")
2360 txt, err := dkimr.Record()
2361 xcheckf(err, "making DKIM DNS TXT record")
2362 txts[name+"._domainkey."+msgFrom.Domain.ASCII+"."] = []string{txt}
2364 resolver = dns.MockResolver{TXT: txts}
2367 dkimResults, dkimErr = dkim.Verify(dkimctx, c.log.Logger, resolver, c.msgsmtputf8, dkim.DefaultPolicy, dataFile, ignoreTestMode)
2373 var receivedSPF spf.Received
2374 var spfDomain dns.Domain
2376 var spfAuthentic bool
2378 spfArgs := spf.Args{
2379 RemoteIP: c.remoteIP,
2380 MailFromLocalpart: c.mailFrom.Localpart,
2381 MailFromDomain: c.mailFrom.IPDomain.Domain, // Can be empty.
2382 HelloDomain: c.hello,
2384 LocalHostname: c.hostname,
2389 x := recover() // Should not happen, but don't take program down if it does.
2391 c.log.Error("spf verify panic", slog.Any("err", x))
2393 metrics.PanicInc(metrics.Spfverify)
2397 spfctx, spfcancel := context.WithTimeout(ctx, time.Minute)
2399 resolver := c.resolver
2400 // For localserve, give hosted domains a chance to pass for deliveries from queue.
2401 if Localserve && c.remoteIP.IsLoopback() {
2402 // Lookup based on message From address is an approximation.
2403 if _, ok := mox.Conf.Domain(msgFrom.Domain); ok {
2404 resolver = dns.MockResolver{
2405 TXT: map[string][]string{msgFrom.Domain.ASCII + ".": {"v=spf1 ip4:127.0.0.1/8 ip6:::1 ~all"}},
2409 receivedSPF, spfDomain, spfExpl, spfAuthentic, spfErr = spf.Verify(spfctx, c.log.Logger, resolver, spfArgs)
2412 c.log.Infox("spf verify", spfErr)
2416 // Wait for DKIM and SPF validation to finish.
2419 // Give immediate response if all recipients are unknown.
2421 for _, r := range c.recipients {
2422 if r.Account == nil && r.Alias == nil {
2426 if nunknown == len(c.recipients) {
2427 // During RCPT TO we found that the address does not exist.
2428 c.log.Info("deliver attempt to unknown user(s)", slog.Any("recipients", c.recipients))
2430 // Crude attempt to slow down someone trying to guess names. Would work better
2431 // with connection rate limiter.
2432 if unknownRecipientsDelay > 0 {
2433 mox.Sleep(ctx, unknownRecipientsDelay)
2436 // 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.
2437 xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeAddr1UnknownDestMailbox1, "no such user(s)")
2440 // Add DKIM results to Authentication-Results header.
2441 authResAddDKIM := func(result, comment, reason string, props []message.AuthProp) {
2442 dm := message.AuthMethod{
2449 authResults.Methods = append(authResults.Methods, dm)
2452 c.log.Errorx("dkim verify", dkimErr)
2453 authResAddDKIM("none", "", dkimErr.Error(), nil)
2454 } else if len(dkimResults) == 0 {
2455 c.log.Info("no dkim-signature header", slog.Any("mailfrom", c.mailFrom))
2456 authResAddDKIM("none", "", "no dkim signatures", nil)
2458 for i, r := range dkimResults {
2459 var domain, selector dns.Domain
2460 var identity *dkim.Identity
2462 var props []message.AuthProp
2464 if r.Record != nil && r.Record.PublicKey != nil {
2465 if pubkey, ok := r.Record.PublicKey.(*rsa.PublicKey); ok {
2466 comment = fmt.Sprintf("%d bit rsa, ", pubkey.N.BitLen())
2470 sig := base64.StdEncoding.EncodeToString(r.Sig.Signature)
2471 sig = sig[:12] // Must be at least 8 characters and unique among the signatures.
2472 props = []message.AuthProp{
2473 message.MakeAuthProp("header", "d", r.Sig.Domain.XName(c.msgsmtputf8), true, r.Sig.Domain.ASCIIExtra(c.msgsmtputf8)),
2474 message.MakeAuthProp("header", "s", r.Sig.Selector.XName(c.msgsmtputf8), true, r.Sig.Selector.ASCIIExtra(c.msgsmtputf8)),
2475 message.MakeAuthProp("header", "a", r.Sig.Algorithm(), false, ""),
2478 domain = r.Sig.Domain
2479 selector = r.Sig.Selector
2480 if r.Sig.Identity != nil {
2481 props = append(props, message.MakeAuthProp("header", "i", r.Sig.Identity.String(), true, ""))
2482 identity = r.Sig.Identity
2484 if r.RecordAuthentic {
2485 comment += "with dnssec"
2487 comment += "without dnssec"
2492 errmsg = r.Err.Error()
2494 authResAddDKIM(string(r.Status), comment, errmsg, props)
2495 c.log.Debugx("dkim verification result", r.Err,
2496 slog.Int("index", i),
2497 slog.Any("mailfrom", c.mailFrom),
2498 slog.Any("status", r.Status),
2499 slog.Any("domain", domain),
2500 slog.Any("selector", selector),
2501 slog.Any("identity", identity))
2505 var spfIdentity *dns.Domain
2506 var mailFromValidation = store.ValidationUnknown
2507 var ehloValidation = store.ValidationUnknown
2508 switch receivedSPF.Identity {
2509 case spf.ReceivedHELO:
2510 if len(spfArgs.HelloDomain.IP) == 0 {
2511 spfIdentity = &spfArgs.HelloDomain.Domain
2513 ehloValidation = store.SPFValidation(receivedSPF.Result)
2514 case spf.ReceivedMailFrom:
2515 spfIdentity = &spfArgs.MailFromDomain
2516 mailFromValidation = store.SPFValidation(receivedSPF.Result)
2518 var props []message.AuthProp
2519 if spfIdentity != nil {
2520 props = []message.AuthProp{message.MakeAuthProp("smtp", string(receivedSPF.Identity), spfIdentity.XName(c.msgsmtputf8), true, spfIdentity.ASCIIExtra(c.msgsmtputf8))}
2522 var spfComment string
2524 spfComment = "with dnssec"
2526 spfComment = "without dnssec"
2528 authResults.Methods = append(authResults.Methods, message.AuthMethod{
2530 Result: string(receivedSPF.Result),
2531 Comment: spfComment,
2534 switch receivedSPF.Result {
2535 case spf.StatusPass:
2536 c.log.Debug("spf pass", slog.Any("ip", spfArgs.RemoteIP), slog.String("mailfromdomain", spfArgs.MailFromDomain.ASCII)) // todo: log the domain that was actually verified.
2537 case spf.StatusFail:
2540 for _, b := range []byte(spfExpl) {
2541 if b < ' ' || b >= 0x7f {
2547 if len(spfExpl) > 800 {
2548 spfExpl = spfExpl[:797] + "..."
2550 spfExpl = "remote claims: " + spfExpl
2554 spfExpl = fmt.Sprintf("your ip %s is not on the SPF allowlist for domain %s", spfArgs.RemoteIP, spfDomain.ASCII)
2556 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?
2557 case spf.StatusTemperror:
2558 c.log.Infox("spf temperror", spfErr)
2559 case spf.StatusPermerror:
2560 c.log.Infox("spf permerror", spfErr)
2561 case spf.StatusNone, spf.StatusNeutral, spf.StatusSoftfail:
2563 c.log.Error("unknown spf status, treating as None/Neutral", slog.Any("status", receivedSPF.Result))
2564 receivedSPF.Result = spf.StatusNone
2569 var dmarcResult dmarc.Result
2570 const applyRandomPercentage = true
2571 // dmarcMethod is added to authResults when delivering to recipients: accounts can
2572 // have different policy override rules.
2573 var dmarcMethod message.AuthMethod
2574 var msgFromValidation = store.ValidationNone
2575 if msgFrom.IsZero() {
2576 dmarcResult.Status = dmarc.StatusNone
2577 dmarcMethod = message.AuthMethod{
2579 Result: string(dmarcResult.Status),
2582 msgFromValidation = alignment(ctx, c.log, msgFrom.Domain, dkimResults, receivedSPF.Result, spfIdentity)
2584 // We are doing the DMARC evaluation now. But we only store it for inclusion in an
2585 // aggregate report when we actually use it. We use an evaluation for each
2586 // recipient, with each a potentially different result due to mailing
2587 // list/forwarding configuration. If we reject a message due to being spam, we
2588 // don't want to spend any resources for the sender domain, and we don't want to
2589 // give the sender any more information about us, so we won't record the
2591 // 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.
2593 dmarcctx, dmarccancel := context.WithTimeout(ctx, time.Minute)
2595 dmarcUse, dmarcResult = dmarc.Verify(dmarcctx, c.log.Logger, c.resolver, msgFrom.Domain, dkimResults, receivedSPF.Result, spfIdentity, applyRandomPercentage)
2598 if dmarcResult.RecordAuthentic {
2599 comment = "with dnssec"
2601 comment = "without dnssec"
2603 dmarcMethod = message.AuthMethod{
2605 Result: string(dmarcResult.Status),
2607 Props: []message.AuthProp{
2609 message.MakeAuthProp("header", "from", msgFrom.Domain.ASCII, true, msgFrom.Domain.ASCIIExtra(c.msgsmtputf8)),
2613 if dmarcResult.Status == dmarc.StatusPass && msgFromValidation == store.ValidationRelaxed {
2614 msgFromValidation = store.ValidationDMARC
2617 // todo future: consider enforcing an spf (soft)fail if there is no dmarc policy or the dmarc policy is none.
../rfc/7489:1507
2619 c.log.Debug("dmarc verification", slog.Any("result", dmarcResult.Status), slog.Any("domain", msgFrom.Domain))
2621 // Prepare for analyzing content, calculating reputation.
2622 ipmasked1, ipmasked2, ipmasked3 := ipmasked(c.remoteIP)
2623 var verifiedDKIMDomains []string
2624 dkimSeen := map[string]bool{}
2625 for _, r := range dkimResults {
2626 // A message can have multiple signatures for the same identity. For example when
2627 // signing the message multiple times with different algorithms (rsa and ed25519).
2628 if r.Status != dkim.StatusPass {
2631 d := r.Sig.Domain.Name()
2634 verifiedDKIMDomains = append(verifiedDKIMDomains, d)
2638 // When we deliver, we try to remove from rejects mailbox based on message-id.
2639 // We'll parse it when we need it, but it is the same for each recipient.
2640 var messageID string
2641 var parsedMessageID bool
2643 // We build up a DSN for each failed recipient. If we have recipients in dsnMsg
2644 // after processing, we queue the DSN. Unless all recipients failed, in which case
2645 // we may just fail the mail transaction instead (could be common for failure to
2646 // deliver to a single recipient, e.g. for junk mail).
2648 type deliverError struct {
2655 var deliverErrors []deliverError
2656 addError := func(rcpt recipient, code int, secode string, userError bool, errmsg string) {
2657 e := deliverError{rcpt.Addr, code, secode, userError, errmsg}
2658 c.log.Info("deliver error",
2659 slog.Any("rcptto", e.rcptTo),
2660 slog.Int("code", code),
2661 slog.String("secode", "secode"),
2662 slog.Bool("usererror", userError),
2663 slog.String("errmsg", errmsg))
2664 deliverErrors = append(deliverErrors, e)
2667 // Sort recipients: local accounts, aliases, unknown. For ensuring we don't deliver
2668 // to an alias destination that was also explicitly sent to.
2669 rcptScore := func(r recipient) int {
2670 if r.Account != nil {
2672 } else if r.Alias != nil {
2677 sort.SliceStable(c.recipients, func(i, j int) bool {
2678 return rcptScore(c.recipients[i]) < rcptScore(c.recipients[j])
2681 // Return whether address is a regular explicit recipient in this transaction. Used
2682 // to prevent delivering a message to an address both for alias and explicit
2683 // addressee. Relies on c.recipients being sorted as above.
2684 regularRecipient := func(addr smtp.Path) bool {
2685 for _, rcpt := range c.recipients {
2686 if rcpt.Account == nil {
2688 } else if rcpt.Addr.Equal(addr) {
2695 // Prepare a message, analyze it against account's junk filter.
2696 // The returned analysis has an open account that must be closed by the caller.
2697 // We call this for all alias destinations, also when we already delivered to that
2698 // recipient: It may be the only recipient that would allow the message.
2699 messageAnalyze := func(log mlog.Log, smtpRcptTo, deliverTo smtp.Path, accountName string, destination config.Destination, canonicalAddr string) (a *analysis, rerr error) {
2700 acc, err := store.OpenAccount(log, accountName)
2702 log.Errorx("open account", err, slog.Any("account", accountName))
2703 metricDelivery.WithLabelValues("accounterror", "").Inc()
2709 log.Check(err, "closing account during analysis")
2714 Received: time.Now(),
2715 RemoteIP: c.remoteIP.String(),
2716 RemoteIPMasked1: ipmasked1,
2717 RemoteIPMasked2: ipmasked2,
2718 RemoteIPMasked3: ipmasked3,
2719 EHLODomain: c.hello.Domain.Name(),
2720 MailFrom: c.mailFrom.String(),
2721 MailFromLocalpart: c.mailFrom.Localpart,
2722 MailFromDomain: c.mailFrom.IPDomain.Domain.Name(),
2723 RcptToLocalpart: smtpRcptTo.Localpart,
2724 RcptToDomain: smtpRcptTo.IPDomain.Domain.Name(),
2725 MsgFromLocalpart: msgFrom.Localpart,
2726 MsgFromDomain: msgFrom.Domain.Name(),
2727 MsgFromOrgDomain: publicsuffix.Lookup(ctx, log.Logger, msgFrom.Domain).Name(),
2728 EHLOValidated: ehloValidation == store.ValidationPass,
2729 MailFromValidated: mailFromValidation == store.ValidationPass,
2730 MsgFromValidated: msgFromValidation == store.ValidationStrict || msgFromValidation == store.ValidationDMARC || msgFromValidation == store.ValidationRelaxed,
2731 EHLOValidation: ehloValidation,
2732 MailFromValidation: mailFromValidation,
2733 MsgFromValidation: msgFromValidation,
2734 DKIMDomains: verifiedDKIMDomains,
2736 Size: msgWriter.Size,
2739 tlsState := c.conn.(*tls.Conn).ConnectionState()
2740 m.ReceivedTLSVersion = tlsState.Version
2741 m.ReceivedTLSCipherSuite = tlsState.CipherSuite
2742 if c.requireTLS != nil {
2743 m.ReceivedRequireTLS = *c.requireTLS
2746 m.ReceivedTLSVersion = 1 // Signals plain text delivery.
2749 var msgTo, msgCc []message.Address
2750 if envelope != nil {
2754 d := delivery{c.tls, &m, dataFile, smtpRcptTo, deliverTo, destination, canonicalAddr, acc, msgTo, msgCc, msgFrom, c.dnsBLs, dmarcUse, dmarcResult, dkimResults, iprevStatus, c.smtputf8}
2756 r := analyze(ctx, log, c.resolver, d)
2760 // Either deliver the message, or call addError to register the recipient as failed.
2761 // If recipient is an alias, we may be delivering to multiple address/accounts and
2762 // we will consider a message delivered if we delivered it to at least one account
2763 // (others may be over quota).
2764 processRecipient := func(rcpt recipient) {
2765 log := c.log.With(slog.Any("mailfrom", c.mailFrom), slog.Any("rcptto", rcpt.Addr))
2767 // If this is not a valid local user, we send back a DSN. This can only happen when
2768 // there are also valid recipients, and only when remote is SPF-verified, so the DSN
2769 // should not cause backscatter.
2770 // In case of serious errors, we abort the transaction. We may have already
2771 // delivered some messages. Perhaps it would be better to continue with other
2772 // deliveries, and return an error at the end? Though the failure conditions will
2773 // probably prevent any other successful deliveries too...
2775 if rcpt.Account == nil && rcpt.Alias == nil {
2776 metricDelivery.WithLabelValues("unknownuser", "").Inc()
2777 addError(rcpt, smtp.C550MailboxUnavail, smtp.SeAddr1UnknownDestMailbox1, true, "no such user")
2781 // la holds all analysis, and message preparation, for all accounts (multiple for
2782 // aliases). Each has an open account that we we close on return.
2785 for _, a := range la {
2786 err := a.d.acc.Close()
2787 log.Check(err, "close account")
2791 // For aliases, we prepare & analyze for each recipient. We accept the message if
2792 // any recipient accepts it. Regular destination have just a single account to
2793 // check. We check all alias destinations, even if we already explicitly delivered
2794 // to them: they may be the only destination that would accept the message.
2795 var a0 *analysis // Analysis we've used for accept/reject decision.
2796 if rcpt.Alias != nil {
2797 // Check if msgFrom address is acceptable. This doesn't take validation into
2798 // consideration. If the header was forged, the message may be rejected later on.
2799 if !aliasAllowedMsgFrom(rcpt.Alias.Alias, msgFrom) {
2800 addError(rcpt, smtp.C550MailboxUnavail, smtp.SePol7ExpnProhibited2, true, "not allowed to send to destination")
2804 la = make([]analysis, 0, len(rcpt.Alias.Alias.ParsedAddresses))
2805 for _, aa := range rcpt.Alias.Alias.ParsedAddresses {
2806 a, err := messageAnalyze(log, rcpt.Addr, aa.Address.Path(), aa.AccountName, aa.Destination, rcpt.Alias.CanonicalAddress)
2808 addError(rcpt, smtp.C451LocalErr, smtp.SeSys3Other0, false, "error processing")
2812 if a.accept && a0 == nil {
2813 // Address that caused us to accept.
2818 // First address, for rejecting.
2822 a, err := messageAnalyze(log, rcpt.Addr, rcpt.Addr, rcpt.Account.AccountName, rcpt.Account.Destination, rcpt.Account.CanonicalAddress)
2824 addError(rcpt, smtp.C451LocalErr, smtp.SeSys3Other0, false, "error processing")
2831 if !a0.accept && a0.reason == reasonHighRate {
2832 log.Info("incoming message rejected for high rate, not storing in rejects mailbox", slog.String("reason", a0.reason), slog.Any("msgfrom", msgFrom))
2833 metricDelivery.WithLabelValues("reject", a0.reason).Inc()
2835 addError(rcpt, a0.code, a0.secode, a0.userError, a0.errmsg)
2839 // Any DMARC result override is stored in the evaluation for outgoing DMARC
2840 // aggregate reports, and added to the Authentication-Results message header.
2841 // We want to tell the sender that we have an override, e.g. for mailing lists, so
2842 // they don't overestimate the potential damage of switching from p=none to
2844 var dmarcOverrides []string
2845 if a0.dmarcOverrideReason != "" {
2846 dmarcOverrides = []string{a0.dmarcOverrideReason}
2848 if dmarcResult.Record != nil && !dmarcUse {
2849 dmarcOverrides = append(dmarcOverrides, string(dmarcrpt.PolicyOverrideSampledOut))
2852 // Add per-recipient DMARC method to Authentication-Results. Each account can have
2853 // their own override rules, e.g. based on configured mailing lists/forwards.
2855 rcptDMARCMethod := dmarcMethod
2856 if len(dmarcOverrides) > 0 {
2857 if rcptDMARCMethod.Comment != "" {
2858 rcptDMARCMethod.Comment += ", "
2860 rcptDMARCMethod.Comment += "override " + strings.Join(dmarcOverrides, ",")
2862 rcptAuthResults := authResults
2863 rcptAuthResults.Methods = append([]message.AuthMethod{}, authResults.Methods...)
2864 rcptAuthResults.Methods = append(rcptAuthResults.Methods, rcptDMARCMethod)
2866 // Prepend reason as message header, for easy viewing in mail clients.
2868 if a0.reason != "" {
2869 hw := &message.HeaderWriter{}
2870 hw.Add(" ", "X-Mox-Reason:")
2871 hw.Add(" ", a0.reason)
2872 for i, s := range a0.reasonText {
2878 // Just in case any of the strings has a newline, replace it with space to not break the message.
2879 s = strings.ReplaceAll(s, "\n", " ")
2880 s = strings.ReplaceAll(s, "\r", " ")
2882 hw.AddWrap([]byte(s), true)
2891 la[i].d.m.MsgPrefix = []byte(
2895 rcptAuthResults.Header() +
2896 receivedSPF.Header() +
2897 recvHdrFor(rcpt.Addr.String()),
2899 la[i].d.m.Size += int64(len(la[i].d.m.MsgPrefix))
2902 // Store DMARC evaluation for inclusion in an aggregate report. Only if there is at
2903 // least one reporting address: We don't want to needlessly store a row in a
2904 // database for each delivery attempt. If we reject a message for being junk, we
2905 // are also not going to send it a DMARC report. The DMARC check is done early in
2906 // the analysis, we will report on rejects because of DMARC, because it could be
2907 // valuable feedback about forwarded or mailing list messages.
2909 if !mox.Conf.Static.NoOutgoingDMARCReports && dmarcResult.Record != nil && len(dmarcResult.Record.AggregateReportAddresses) > 0 && (a0.accept && !a0.d.m.IsReject || a0.reason == reasonDMARCPolicy) {
2910 // Disposition holds our decision on whether to accept the message. Not what the
2911 // DMARC evaluation resulted in. We can override, e.g. because of mailing lists,
2912 // forwarding, or local policy.
2913 // We treat quarantine as reject, so never claim to quarantine.
2915 disposition := dmarcrpt.DispositionNone
2917 disposition = dmarcrpt.DispositionReject
2920 // unknownDomain returns whether the sender is domain with which this account has
2921 // not had positive interaction.
2922 unknownDomain := func() (unknown bool) {
2923 err := a0.d.acc.DB.Read(ctx, func(tx *bstore.Tx) (err error) {
2924 // See if we received a non-junk message from this organizational domain.
2925 q := bstore.QueryTx[store.Message](tx)
2926 q.FilterNonzero(store.Message{MsgFromOrgDomain: a0.d.m.MsgFromOrgDomain})
2927 q.FilterEqual("Notjunk", true)
2928 q.FilterEqual("IsReject", false)
2929 exists, err := q.Exists()
2931 return fmt.Errorf("querying for non-junk message from organizational domain: %v", err)
2937 // See if we sent a message to this organizational domain.
2938 qr := bstore.QueryTx[store.Recipient](tx)
2939 qr.FilterNonzero(store.Recipient{OrgDomain: a0.d.m.MsgFromOrgDomain})
2940 exists, err = qr.Exists()
2942 return fmt.Errorf("querying for message sent to organizational domain: %v", err)
2950 log.Errorx("checking if sender is unknown domain, for dmarc aggregate report evaluation", err)
2955 r := dmarcResult.Record
2956 addresses := make([]string, len(r.AggregateReportAddresses))
2957 for i, a := range r.AggregateReportAddresses {
2958 addresses[i] = a.String()
2960 sp := dmarcrpt.Disposition(r.SubdomainPolicy)
2961 if r.SubdomainPolicy == dmarc.PolicyEmpty {
2962 sp = dmarcrpt.Disposition(r.Policy)
2964 eval := dmarcdb.Evaluation{
2965 // Evaluated and IntervalHours set by AddEvaluation.
2966 PolicyDomain: dmarcResult.Domain.Name(),
2968 // Optional evaluations don't cause a report to be sent, but will be included.
2969 // Useful for automated inter-mailer messages, we don't want to get in a reporting
2970 // loop. We also don't want to be used for sending reports to unsuspecting domains
2971 // we have no relation with.
2972 // 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.
2973 Optional: a0.d.destination.DMARCReports || a0.d.destination.HostTLSReports || a0.d.destination.DomainTLSReports || a0.reason == reasonDMARCPolicy && unknownDomain(),
2975 Addresses: addresses,
2977 PolicyPublished: dmarcrpt.PolicyPublished{
2978 Domain: dmarcResult.Domain.Name(),
2979 ADKIM: dmarcrpt.Alignment(r.ADKIM),
2980 ASPF: dmarcrpt.Alignment(r.ASPF),
2981 Policy: dmarcrpt.Disposition(r.Policy),
2982 SubdomainPolicy: sp,
2983 Percentage: r.Percentage,
2984 // We don't save ReportingOptions, we don't do per-message failure reporting.
2986 SourceIP: c.remoteIP.String(),
2987 Disposition: disposition,
2988 AlignedDKIMPass: dmarcResult.AlignedDKIMPass,
2989 AlignedSPFPass: dmarcResult.AlignedSPFPass,
2990 EnvelopeTo: rcpt.Addr.IPDomain.String(),
2991 EnvelopeFrom: c.mailFrom.IPDomain.String(),
2992 HeaderFrom: msgFrom.Domain.Name(),
2995 for _, s := range dmarcOverrides {
2996 reason := dmarcrpt.PolicyOverrideReason{Type: dmarcrpt.PolicyOverride(s)}
2997 eval.OverrideReasons = append(eval.OverrideReasons, reason)
3000 // We'll include all signatures for the organizational domain, even if they weren't
3001 // relevant due to strict alignment requirement.
3002 for _, dkimResult := range dkimResults {
3003 if dkimResult.Sig == nil || publicsuffix.Lookup(ctx, log.Logger, msgFrom.Domain) != publicsuffix.Lookup(ctx, log.Logger, dkimResult.Sig.Domain) {
3006 r := dmarcrpt.DKIMAuthResult{
3007 Domain: dkimResult.Sig.Domain.Name(),
3008 Selector: dkimResult.Sig.Selector.ASCII,
3009 Result: dmarcrpt.DKIMResult(dkimResult.Status),
3011 eval.DKIMResults = append(eval.DKIMResults, r)
3014 switch receivedSPF.Identity {
3015 case spf.ReceivedHELO:
3016 spfAuthResult := dmarcrpt.SPFAuthResult{
3017 Domain: spfArgs.HelloDomain.String(), // Can be unicode and also IP.
3018 Scope: dmarcrpt.SPFDomainScopeHelo,
3019 Result: dmarcrpt.SPFResult(receivedSPF.Result),
3021 eval.SPFResults = []dmarcrpt.SPFAuthResult{spfAuthResult}
3022 case spf.ReceivedMailFrom:
3023 spfAuthResult := dmarcrpt.SPFAuthResult{
3024 Domain: spfArgs.MailFromDomain.Name(), // Can be unicode.
3025 Scope: dmarcrpt.SPFDomainScopeMailFrom,
3026 Result: dmarcrpt.SPFResult(receivedSPF.Result),
3028 eval.SPFResults = []dmarcrpt.SPFAuthResult{spfAuthResult}
3031 err := dmarcdb.AddEvaluation(ctx, dmarcResult.Record.AggregateReportingInterval, &eval)
3032 log.Check(err, "adding dmarc evaluation to database for aggregate report")
3036 for _, a := range la {
3037 // Don't add message if address was also explicitly present in a RCPT TO command.
3038 if rcpt.Alias != nil && regularRecipient(a.d.deliverTo) {
3042 conf, _ := a.d.acc.Conf()
3043 if conf.RejectsMailbox == "" {
3046 present, _, messagehash, err := rejectPresent(log, a.d.acc, conf.RejectsMailbox, a.d.m, dataFile)
3048 log.Errorx("checking whether reject is already present", err)
3051 log.Info("reject message is already present, ignoring")
3054 a.d.m.IsReject = true
3055 a.d.m.Seen = true // We don't want to draw attention.
3056 // Regular automatic junk flags configuration applies to these messages. The
3057 // default is to treat these as neutral, so they won't cause outright rejections
3058 // due to reputation for later delivery attempts.
3059 a.d.m.MessageHash = messagehash
3060 a.d.acc.WithWLock(func() {
3063 if !conf.KeepRejects {
3064 hasSpace, err = a.d.acc.TidyRejectsMailbox(c.log, conf.RejectsMailbox)
3067 log.Errorx("tidying rejects mailbox", err)
3068 } else if hasSpace {
3069 if err := a.d.acc.DeliverMailbox(log, conf.RejectsMailbox, a.d.m, dataFile); err != nil {
3070 log.Errorx("delivering spammy mail to rejects mailbox", err)
3072 log.Info("delivered spammy mail to rejects mailbox")
3075 log.Info("not storing spammy mail to full rejects mailbox")
3080 log.Info("incoming message rejected", slog.String("reason", a0.reason), slog.Any("msgfrom", msgFrom))
3081 metricDelivery.WithLabelValues("reject", a0.reason).Inc()
3083 addError(rcpt, a0.code, a0.secode, a0.userError, a0.errmsg)
3087 delayFirstTime := true
3088 if rcpt.Account != nil && a0.dmarcReport != nil {
3090 if err := dmarcdb.AddReport(ctx, a0.dmarcReport, msgFrom.Domain); err != nil {
3091 log.Errorx("saving dmarc aggregate report in database", err)
3093 log.Info("dmarc aggregate report processed")
3094 a0.d.m.Flags.Seen = true
3095 delayFirstTime = false
3098 if rcpt.Account != nil && a0.tlsReport != nil {
3099 // todo future: add rate limiting to prevent DoS attacks.
3100 if err := tlsrptdb.AddReport(ctx, c.log, msgFrom.Domain, c.mailFrom.String(), a0.d.destination.HostTLSReports, a0.tlsReport); err != nil {
3101 log.Errorx("saving TLSRPT report in database", err)
3103 log.Info("tlsrpt report processed")
3104 a0.d.m.Flags.Seen = true
3105 delayFirstTime = false
3109 // If this is a first-time sender and not a forwarded/mailing list message, wait
3110 // before actually delivering. If this turns out to be a spammer, we've kept one of
3111 // their connections busy.
3112 a0conf, _ := a0.d.acc.Conf()
3113 if delayFirstTime && !a0.d.m.IsForward && !a0.d.m.IsMailingList && a0.reason == reasonNoBadSignals && !a0conf.NoFirstTimeSenderDelay && c.firstTimeSenderDelay > 0 {
3114 log.Debug("delaying before delivering from sender without reputation", slog.Duration("delay", c.firstTimeSenderDelay))
3115 mox.Sleep(mox.Context, c.firstTimeSenderDelay)
3119 code, timeout := mox.LocalserveNeedsError(rcpt.Addr.Localpart)
3121 log.Info("timing out due to special localpart")
3122 mox.Sleep(mox.Context, time.Hour)
3123 xsmtpServerErrorf(codes{smtp.C451LocalErr, smtp.SeOther00}, "timing out delivery due to special localpart")
3124 } else if code != 0 {
3125 log.Info("failure due to special localpart", slog.Int("code", code))
3126 metricDelivery.WithLabelValues("delivererror", "localserve").Inc()
3127 addError(rcpt, code, smtp.SeOther00, false, fmt.Sprintf("failure with code %d due to special localpart", code))
3132 // Gather the message-id before we deliver and the file may be consumed.
3133 if !parsedMessageID {
3134 if p, err := message.Parse(c.log.Logger, false, store.FileMsgReader(a0.d.m.MsgPrefix, dataFile)); err != nil {
3135 log.Infox("parsing message for message-id", err)
3136 } else if header, err := p.Header(); err != nil {
3137 log.Infox("parsing message header for message-id", err)
3139 messageID = header.Get("Message-Id")
3141 parsedMessageID = true
3144 // Finally deliver the message to the account(s).
3145 var nerr int // Number of non-quota errors.
3146 var nfull int // Number of failed deliveries due to over quota.
3147 var ndelivered int // Number delivered to account.
3148 for _, a := range la {
3149 // Don't deliver to recipient that was explicitly present in SMTP transaction, or
3150 // is sending the message to an alias they are member of.
3151 if rcpt.Alias != nil && (regularRecipient(a.d.deliverTo) || a.d.deliverTo.Equal(msgFrom.Path())) {
3156 a.d.acc.WithWLock(func() {
3157 if err := a.d.acc.DeliverMailbox(log, a.mailbox, a.d.m, dataFile); err != nil {
3158 log.Errorx("delivering", err)
3159 metricDelivery.WithLabelValues("delivererror", a0.reason).Inc()
3160 if errors.Is(err, store.ErrOverQuota) {
3163 addError(rcpt, smtp.C451LocalErr, smtp.SeSys3Other0, false, "error processing")
3170 metricDelivery.WithLabelValues("delivered", a0.reason).Inc()
3171 log.Info("incoming message delivered", slog.String("reason", a0.reason), slog.Any("msgfrom", msgFrom))
3173 conf, _ := a.d.acc.Conf()
3174 if conf.RejectsMailbox != "" && a.d.m.MessageID != "" {
3175 if err := a.d.acc.RejectsRemove(log, conf.RejectsMailbox, a.d.m.MessageID); err != nil {
3176 log.Errorx("removing message from rejects mailbox", err, slog.String("messageid", messageID))
3181 // Pass delivered messages to queue for DSN processing and/or hooks.
3183 mr := store.FileMsgReader(a.d.m.MsgPrefix, dataFile)
3184 part, err := a.d.m.LoadPart(mr)
3186 log.Errorx("loading parsed part for evaluating webhook", err)
3188 err = queue.Incoming(context.Background(), log, a.d.acc, messageID, *a.d.m, part, a.mailbox)
3189 log.Check(err, "queueing webhook for incoming delivery")
3191 } else if nerr > 0 && ndelivered == 0 {
3192 // Don't continue if we had an error and haven't delivered yet. If we only had
3193 // quota-related errors, we keep trying for an account to deliver to.
3197 if ndelivered == 0 && (nerr > 0 || nfull > 0) {
3199 addError(rcpt, smtp.C452StorageFull, smtp.SeMailbox2Full2, true, "account storage full")
3201 addError(rcpt, smtp.C451LocalErr, smtp.SeSys3Other0, false, "error processing")
3206 // For each recipient, do final spam analysis and delivery.
3207 for _, rcpt := range c.recipients {
3208 processRecipient(rcpt)
3211 // If all recipients failed to deliver, return an error.
3212 if len(c.recipients) == len(deliverErrors) {
3214 e0 := deliverErrors[0]
3215 var serverError bool
3218 for _, e := range deliverErrors {
3219 serverError = serverError || !e.userError
3220 if e.code != e0.code || e.secode != e0.secode {
3223 msgs = append(msgs, e.errmsg)
3229 xsmtpErrorf(e0.code, e0.secode, !serverError, "%s", strings.Join(msgs, "\n"))
3232 // Not all failures had the same error. We'll return each error on a separate line.
3234 for _, e := range deliverErrors {
3235 s := fmt.Sprintf("%d %d.%s %s", e.code, e.code/100, e.secode, e.errmsg)
3236 lines = append(lines, s)
3238 code := smtp.C451LocalErr
3239 secode := smtp.SeSys3Other0
3241 code = smtp.C554TransactionFailed
3243 lines = append(lines, "multiple errors")
3244 xsmtpErrorf(code, secode, !serverError, strings.Join(lines, "\n"))
3246 // Generate one DSN for all failed recipients.
3247 if len(deliverErrors) > 0 {
3249 dsnMsg := dsn.Message{
3250 SMTPUTF8: c.msgsmtputf8,
3251 From: smtp.Path{Localpart: "postmaster", IPDomain: deliverErrors[0].rcptTo.IPDomain},
3253 Subject: "mail delivery failure",
3254 MessageID: mox.MessageIDGen(false),
3255 References: messageID,
3257 // Per-message details.
3258 ReportingMTA: mox.Conf.Static.HostnameDomain.ASCII,
3259 ReceivedFromMTA: smtp.Ehlo{Name: c.hello, ConnIP: c.remoteIP},
3263 if len(deliverErrors) > 1 {
3264 dsnMsg.TextBody = "Multiple delivery failures occurred.\n\n"
3267 for _, e := range deliverErrors {
3269 if e.code/100 == 4 {
3272 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))
3273 rcpt := dsn.Recipient{
3274 FinalRecipient: e.rcptTo,
3276 Status: fmt.Sprintf("%d.%s", e.code/100, e.secode),
3277 LastAttemptDate: now,
3279 dsnMsg.Recipients = append(dsnMsg.Recipients, rcpt)
3282 header, err := message.ReadHeaders(bufio.NewReader(&moxio.AtReader{R: dataFile}))
3284 c.log.Errorx("reading headers of incoming message for dsn, continuing dsn without headers", err)
3286 dsnMsg.Original = header
3289 c.log.Error("not queueing dsn for incoming delivery due to localserve")
3290 } else if err := queueDSN(context.TODO(), c.log, c, *c.mailFrom, dsnMsg, c.requireTLS != nil && *c.requireTLS); err != nil {
3291 metricServerErrors.WithLabelValues("queuedsn").Inc()
3292 c.log.Errorx("queuing DSN for incoming delivery, no DSN sent", err)
3297 c.transactionBad-- // Compensate for early earlier pessimistic increase.
3299 c.writecodeline(smtp.C250Completed, smtp.SeMailbox2Other0, "it is done", nil)
3302// Return whether msgFrom address is allowed to send a message to alias.
3303func aliasAllowedMsgFrom(alias config.Alias, msgFrom smtp.Address) bool {
3304 for _, aa := range alias.ParsedAddresses {
3305 if aa.Address == msgFrom {
3309 lp, err := smtp.ParseLocalpart(alias.LocalpartStr)
3310 xcheckf(err, "parsing alias localpart")
3311 if msgFrom == smtp.NewAddress(lp, alias.Domain) {
3312 return alias.AllowMsgFrom
3314 return alias.PostPublic
3317// ecode returns either ecode, or a more specific error based on err.
3318// For example, ecode can be turned from an "other system" error into a "mail
3319// system full" if the error indicates no disk space is available.
3320func errCodes(code int, ecode string, err error) codes {
3322 case moxio.IsStorageSpace(err):
3324 case smtp.SeMailbox2Other0:
3325 if code == smtp.C451LocalErr {
3326 code = smtp.C452StorageFull
3328 ecode = smtp.SeMailbox2Full2
3329 case smtp.SeSys3Other0:
3330 if code == smtp.C451LocalErr {
3331 code = smtp.C452StorageFull
3333 ecode = smtp.SeSys3StorageFull1
3336 return codes{code, ecode}
3340func (c *conn) cmdRset(p *parser) {
3345 c.bwritecodeline(smtp.C250Completed, smtp.SeOther00, "all clear", nil)
3349func (c *conn) cmdVrfy(p *parser) {
3350 // No EHLO/HELO needed.
3361 // todo future: we could support vrfy and expn for submission? though would need to see if its rfc defines it.
3364 xsmtpUserErrorf(smtp.C252WithoutVrfy, smtp.SePol7Other0, "no verify but will try delivery")
3368func (c *conn) cmdExpn(p *parser) {
3369 // No EHLO/HELO needed.
3380 // todo: we could implement expn for local aliases for authenticated users, when members have permission to list. would anyone use it?
3383 xsmtpUserErrorf(smtp.C252WithoutVrfy, smtp.SePol7Other0, "no expand but will try delivery")
3387func (c *conn) cmdHelp(p *parser) {
3388 // Let's not strictly parse the request for help. We are ignoring the text anyway.
3391 c.bwritecodeline(smtp.C214Help, smtp.SeOther00, "see rfc 5321 (smtp)", nil)
3395func (c *conn) cmdNoop(p *parser) {
3396 // No idea why, but if an argument follows, it must adhere to the string ABNF production...
3403 c.bwritecodeline(smtp.C250Completed, smtp.SeOther00, "alrighty", nil)
3407func (c *conn) cmdQuit(p *parser) {
3411 c.writecodeline(smtp.C221Closing, smtp.SeOther00, "okay thanks bye", nil)