diff --git a/dkim/dkim.go b/dkim/dkim.go index 52e7617..d4f8705 100644 --- a/dkim/dkim.go +++ b/dkim/dkim.go @@ -33,6 +33,9 @@ import ( "github.com/mjl-/mox/stub" ) +// If set, signatures for top-level domain "localhost" are accepted. +var Localserve bool + var ( MetricSign stub.CounterVec = stub.CounterVecIgnore{} MetricVerify stub.HistogramVec = stub.HistogramVecIgnore{} @@ -442,7 +445,7 @@ func checkSignatureParams(ctx context.Context, log mlog.Log, sig *Sig) (hash cry if subdom.Unicode != "" { subdom.Unicode = "x." + subdom.Unicode } - if orgDom := publicsuffix.Lookup(ctx, log.Logger, subdom); subdom.ASCII == orgDom.ASCII { + if orgDom := publicsuffix.Lookup(ctx, log.Logger, subdom); subdom.ASCII == orgDom.ASCII && !(Localserve && sig.Domain.ASCII == "localhost") { return 0, false, false, fmt.Errorf("%w: %s", ErrTLD, sig.Domain) } diff --git a/doc.go b/doc.go index 3dcb845..4f374ad 100644 --- a/doc.go +++ b/doc.go @@ -764,9 +764,12 @@ automatically initialized with configuration files, an account with email address mox@localhost and password moxmoxmox, and a newly generated self-signed TLS certificate. -All incoming email to any address is accepted (if checks pass) and delivered to -the account that is submitting the message, unless the recipient localpart ends -with: +Incoming messages are delivered as normal, falling back to accepting and +delivering to the mox account for unknown addresses. +Submitted messages are added to the queue, which delivers by ignoring the +destination servers, always connecting to itself instead. + +Recipient addresses with the following localpart suffixes are handled specially: - "temperror": fail with a temporary error code - "permerror": fail with a permanent error code @@ -774,8 +777,7 @@ with: - "timeout": no response (for an hour) If the localpart begins with "mailfrom" or "rcptto", the error is returned -during those commands instead of during "data". If the localpart begins with -"queue", the submission is accepted but delivery from the queue will fail. +during those commands instead of during "data". usage: mox localserve -dir string diff --git a/localserve.go b/localserve.go index b33f351..5e19c3f 100644 --- a/localserve.go +++ b/localserve.go @@ -26,6 +26,8 @@ import ( "github.com/mjl-/sconf" "github.com/mjl-/mox/config" + "github.com/mjl-/mox/dkim" + "github.com/mjl-/mox/dns" "github.com/mjl-/mox/junk" "github.com/mjl-/mox/mlog" "github.com/mjl-/mox/mox-" @@ -49,9 +51,12 @@ automatically initialized with configuration files, an account with email address mox@localhost and password moxmoxmox, and a newly generated self-signed TLS certificate. -All incoming email to any address is accepted (if checks pass) and delivered to -the account that is submitting the message, unless the recipient localpart ends -with: +Incoming messages are delivered as normal, falling back to accepting and +delivering to the mox account for unknown addresses. +Submitted messages are added to the queue, which delivers by ignoring the +destination servers, always connecting to itself instead. + +Recipient addresses with the following localpart suffixes are handled specially: - "temperror": fail with a temporary error code - "permerror": fail with a permanent error code @@ -59,8 +64,7 @@ with: - "timeout": no response (for an hour) If the localpart begins with "mailfrom" or "rcptto", the error is returned -during those commands instead of during "data". If the localpart begins with -"queue", the submission is accepted but delivery from the queue will fail. +during those commands instead of during "data". ` golog.SetFlags(0) @@ -144,6 +148,8 @@ during those commands instead of during "data". If the localpart begins with smtpserver.Localserve = true // Tell queue it shouldn't be queuing/delivering. queue.Localserve = true + // Tell DKIM not to fail signatures for TLD localhost. + dkim.Localserve = true const mtastsdbRefresher = false const sendDMARCReports = false @@ -393,6 +399,7 @@ func writeLocalConfig(log mlog.Log, dir, ip string) (rerr error) { Destinations: map[string]config.Destination{ "mox@localhost": {}, }, + NoFirstTimeSenderDelay: true, } acc.AutomaticJunkFlags.Enabled = true acc.AutomaticJunkFlags.JunkMailboxRegexp = "^(junk|spam)" @@ -408,10 +415,25 @@ func writeLocalConfig(log mlog.Log, dir, ip string) (rerr error) { }, } + dkimKeyBuf, err := mox.MakeDKIMEd25519Key(dns.Domain{ASCII: "localserve"}, dns.Domain{ASCII: "localhost"}) + xcheck(err, "making dkim key") + dkimKeyPath := "dkim.localserve.privatekey.pkcs8.pem" + err = os.WriteFile(filepath.Join(dir, dkimKeyPath), dkimKeyBuf, 0660) + xcheck(err, "writing dkim key file") + dynamic := config.Dynamic{ Domains: map[string]config.Domain{ "localhost": { LocalpartCatchallSeparator: "+", + DKIM: config.DKIM{ + Sign: []string{"localserve"}, + Selectors: map[string]config.Selector{ + "localserve": { + Expiration: "72h", + PrivateKeyFile: dkimKeyPath, + }, + }, + }, }, }, Accounts: map[string]config.Account{ diff --git a/mox-/admin.go b/mox-/admin.go index 89d2d2b..8bc56ec 100644 --- a/mox-/admin.go +++ b/mox-/admin.go @@ -99,7 +99,7 @@ func dkimKeyNote(kind string, selector, domain dns.Domain) string { return s } -// MakeDKIMEd25519Key returns a PEM buffer containing an rsa key for use with +// MakeDKIMRSAKey returns a PEM buffer containing an rsa key for use with // DKIM. // selector and domain can be empty. If not, they are used in the note. func MakeDKIMRSAKey(selector, domain dns.Domain) ([]byte, error) { diff --git a/queue/localserve.go b/queue/localserve.go new file mode 100644 index 0000000..770f1ed --- /dev/null +++ b/queue/localserve.go @@ -0,0 +1,103 @@ +package queue + +import ( + "bytes" + "context" + "fmt" + "io" + "log/slog" + "net" + "os" + "time" + + "github.com/mjl-/mox/dns" + "github.com/mjl-/mox/dsn" + "github.com/mjl-/mox/mlog" + "github.com/mjl-/mox/mox-" + "github.com/mjl-/mox/smtpclient" + "github.com/mjl-/mox/store" +) + +// We won't be dialing remote servers. We just connect the smtp port of the first +// ip in the "local" listener, with fallback to localhost:1025 for any destination +// address and try to deliver. Our smtpserver uses a mocked dns resolver to give +// spf/dkim a chance to pass. +func deliverLocalserve(ctx context.Context, log mlog.Log, msgs []*Msg, backoff time.Duration) { + m0 := msgs[0] + + addr := "localhost:1025" + l, ok := mox.Conf.Static.Listeners["local"] + if ok && l.SMTP.Enabled { + port := 1025 + if l.SMTP.Port != 0 { + port = l.SMTP.Port + } + addr = net.JoinHostPort(l.IPs[0], fmt.Sprintf("%d", port)) + } + var d net.Dialer + dialctx, dialcancel := context.WithTimeout(ctx, 30*time.Second) + defer dialcancel() + conn, err := d.DialContext(dialctx, "tcp", addr) + dialcancel() + if err != nil { + failMsgsDB(log, msgs, m0.DialedIPs, backoff, dsn.NameIP{}, err) + return + } + defer func() { + if conn != nil { + err = conn.Close() + log.Check(err, "closing connection") + } + }() + + clientctx, clientcancel := context.WithTimeout(context.Background(), 60*time.Second) + defer clientcancel() + localhost := dns.Domain{ASCII: "localhost"} + client, err := smtpclient.New(clientctx, log.Logger, conn, smtpclient.TLSOpportunistic, false, localhost, localhost, smtpclient.Opts{}) + clientcancel() + if err != nil { + failMsgsDB(log, msgs, m0.DialedIPs, backoff, dsn.NameIP{}, err) + return + } + conn = nil // Will be closed when closing client. + defer func() { + err := client.Close() + log.Check(err, "closing smtp client") + }() + + var msgr io.ReadCloser + var size int64 + if len(m0.DSNUTF8) > 0 { + msgr = io.NopCloser(bytes.NewReader(m0.DSNUTF8)) + size = int64(len(m0.DSNUTF8)) + } else { + size = m0.Size + p := m0.MessagePath() + f, err := os.Open(p) + if err != nil { + log.Errorx("opening message for delivery", err, slog.String("remote", addr), slog.String("path", p)) + err = fmt.Errorf("opening message file: %v", err) + failMsgsDB(log, msgs, m0.DialedIPs, backoff, dsn.NameIP{}, err) + return + } + msgr = store.FileMsgReader(m0.MsgPrefix, f) + defer func() { + err := msgr.Close() + log.Check(err, "closing message after delivery attempt") + }() + } + + deliverctx, delivercancel := context.WithTimeout(context.Background(), 60*time.Second) + defer delivercancel() + requireTLS := m0.RequireTLS != nil && *m0.RequireTLS + rcpts := make([]string, len(msgs)) + for i, m := range msgs { + rcpts[i] = m.Recipient().String() + } + rcptErrs, err := client.DeliverMultiple(deliverctx, m0.Sender().String(), rcpts, size, msgr, m0.Has8bit, m0.SMTPUTF8, requireTLS) + delivercancel() + if err != nil { + log.Infox("smtp transaction for delivery failed", err) + } + processDeliveries(log, m0, msgs, addr, "localhost", backoff, rcptErrs, err) +} diff --git a/queue/queue.go b/queue/queue.go index 707bafb..567b4d1 100644 --- a/queue/queue.go +++ b/queue/queue.go @@ -28,12 +28,10 @@ import ( "github.com/mjl-/mox/config" "github.com/mjl-/mox/dns" "github.com/mjl-/mox/dsn" - "github.com/mjl-/mox/message" "github.com/mjl-/mox/metrics" "github.com/mjl-/mox/mlog" "github.com/mjl-/mox/mox-" "github.com/mjl-/mox/moxio" - "github.com/mjl-/mox/publicsuffix" "github.com/mjl-/mox/smtp" "github.com/mjl-/mox/smtpclient" "github.com/mjl-/mox/store" @@ -1484,117 +1482,7 @@ func deliver(log mlog.Log, resolver dns.Resolver, m0 Msg) { } if Localserve { - // We are not actually going to deliver. We'll deliver to the sender account. - // Unless recipients match certain special patterns, in which case we can pretend - // to cause delivery failures. Useful for testing. - - acc, err := store.OpenAccount(log, m0.SenderAccount) - if err != nil { - log.Errorx("opening sender account for immediate delivery with localserve, skipping", err) - return - } - defer func() { - err := acc.Close() - log.Check(err, "closing account") - }() - conf, _ := acc.Conf() - - p := m0.MessagePath() - msgFile, err := os.Open(p) - if err != nil { - xerr := fmt.Errorf("open message for delivery: %v", err) - failMsgsDB(qlog, msgs, m0.DialedIPs, backoff, dsn.NameIP{}, xerr) - return - } - defer func() { - err := msgFile.Close() - qlog.Check(err, "closing message after delivery attempt") - }() - - // Parse the message for a From-address, but continue on error. - fromAddr, _, _, fromErr := message.From(qlog.Logger, false, store.FileMsgReader(m0.MsgPrefix, msgFile), nil) - log.Check(fromErr, "parsing message From header") - - for _, qm := range msgs { - code, timeout := mox.LocalserveNeedsError(qm.RecipientLocalpart) - if timeout || code != 0 { - err := errors.New("simulated error due to localserve mode and special recipient localpart") - if timeout { - err = fmt.Errorf("%s: timeout", err) - } else { - err = smtpclient.Error{Permanent: code/100 == 5, Code: code, Err: err} - } - failMsgsDB(qlog, []*Msg{qm}, m0.DialedIPs, backoff, remoteMTA, err) - continue - } - - msgFromOrgDomain := publicsuffix.Lookup(ctx, qlog.Logger, fromAddr.Domain) - - dm := store.Message{ - RemoteIP: "::1", - RemoteIPMasked1: "::", - RemoteIPMasked2: "::", - RemoteIPMasked3: "::", - MailFrom: qm.Sender().XString(true), - MailFromLocalpart: qm.SenderLocalpart, - MailFromDomain: qm.SenderDomainStr, - RcptToLocalpart: qm.RecipientLocalpart, - RcptToDomain: qm.RecipientDomainStr, - MsgFromLocalpart: fromAddr.Localpart, - MsgFromDomain: fromAddr.Domain.Name(), - MsgFromOrgDomain: msgFromOrgDomain.Name(), - EHLOValidated: true, - MailFromValidated: true, - MsgFromValidated: true, - EHLOValidation: store.ValidationPass, - MailFromValidation: store.ValidationPass, - MsgFromValidation: store.ValidationDMARC, - DKIMDomains: []string{"localhost"}, - ReceivedRequireTLS: qm.RequireTLS != nil && *qm.RequireTLS, - Size: qm.Size, - MsgPrefix: qm.MsgPrefix, - } - var err error - var mb store.Mailbox - acc.WithWLock(func() { - dest := conf.Destinations[qm.Recipient().String()] - err = acc.DeliverDestination(log, dest, &dm, msgFile) - if err != nil { - err = fmt.Errorf("delivering message: %v", err) - return // Returned again outside WithWLock. - } - - mb = store.Mailbox{ID: dm.MailboxID} - if err = acc.DB.Get(context.Background(), &mb); err != nil { - err = fmt.Errorf("getting mailbox for message after delivery: %v", err) - } - }) - if err != nil { - log.Errorx("delivering from queue to original sender account failed, skipping", err) - continue - } - log.Debug("delivered from queue to original sender account") - qm.markResult(0, "", "", true) - err = DB.Write(context.Background(), func(tx *bstore.Tx) error { - return retireMsgs(qlog, tx, webhook.EventDelivered, smtp.C250Completed, "", nil, *qm) - }) - if err != nil { - log.Errorx("removing queue message from database after local delivery to sender account", err) - } else if err := removeMsgsFS(qlog, *qm); err != nil { - log.Errorx("removing queue messages from file system after local delivery to sender account", err) - } - kick() - - // Process incoming message for incoming webhook. - mr := store.FileMsgReader(dm.MsgPrefix, msgFile) - part, err := dm.LoadPart(mr) - if err != nil { - log.Errorx("loading parsed part for evaluating webhook", err) - } else { - err = Incoming(context.Background(), log, acc, m0.MessageID, dm, part, mb.Name) - log.Check(err, "queueing webhook for incoming delivery") - } - } + deliverLocalserve(ctx, qlog, msgs, backoff) return } diff --git a/queue/queue_test.go b/queue/queue_test.go index e9a7710..adbb27e 100644 --- a/queue/queue_test.go +++ b/queue/queue_test.go @@ -1284,84 +1284,6 @@ func TestQueueStart(t *testing.T) { time.Sleep(100 * time.Millisecond) // Racy... give time to finish. } -// Localserve should cause deliveries to go to sender account, with failure (DSN) -// for recipient addresses that start with "queue" and end with -// "temperror"/"permerror"/"timeout". -func TestLocalserve(t *testing.T) { - Localserve = true - defer func() { - Localserve = false - }() - - path := smtp.Path{Localpart: "mjl", IPDomain: dns.IPDomain{Domain: dns.Domain{ASCII: "mox.example"}}} - - testDeliver := func(to smtp.Path, expSuccess bool) { - t.Helper() - - _, cleanup := setup(t) - defer cleanup() - err := Init() - tcheck(t, err, "queue init") - - accret, err := store.OpenAccount(pkglog, "retired") - tcheck(t, err, "open account") - defer func() { - err := accret.Close() - tcheck(t, err, "closing account") - accret.CheckClosed() - }() - - mf := prepareFile(t) - defer os.Remove(mf.Name()) - defer mf.Close() - - // Regular message. - qm := MakeMsg(path, to, false, false, int64(len(testmsg)), "", nil, nil, time.Now(), "test") - qml := []Msg{qm} - err = Add(ctxbg, pkglog, accret.Name, mf, qml...) - tcheck(t, err, "add message to queue") - qm = qml[0] - - deliver(pkglog, nil, qm) - <-deliveryResults - // Message should be delivered to account. - n, err := bstore.QueryDB[store.Message](ctxbg, accret.DB).Count() - tcheck(t, err, "count messages in account") - tcompare(t, n, 1) - - n, err = Count(ctxbg) - tcheck(t, err, "count message queue") - tcompare(t, n, 0) - - _, err = bstore.QueryDB[MsgRetired](ctxbg, DB).Count() - tcheck(t, err, "get retired message") - - hl, err := bstore.QueryDB[Hook](ctxbg, DB).List() - tcheck(t, err, "get webhooks") - if expSuccess { - tcompare(t, len(hl), 2) - tcompare(t, hl[0].IsIncoming, false) - tcompare(t, hl[1].IsIncoming, true) - } else { - tcompare(t, len(hl), 1) - tcompare(t, hl[0].IsIncoming, false) - } - var out webhook.Outgoing - err = json.Unmarshal([]byte(hl[0].Payload), &out) - tcheck(t, err, "unmarshal outgoing webhook payload") - if expSuccess { - tcompare(t, out.Event, webhook.EventDelivered) - } else { - tcompare(t, out.Event, webhook.EventFailed) - } - } - - testDeliver(path, true) - badpath := path - badpath.Localpart = smtp.Localpart("queuepermerror") - testDeliver(badpath, false) -} - func TestListFilterSort(t *testing.T) { _, cleanup := setup(t) defer cleanup() diff --git a/queue/submit.go b/queue/submit.go index e264b46..a79c7ab 100644 --- a/queue/submit.go +++ b/queue/submit.go @@ -231,7 +231,14 @@ func deliverSubmit(qlog mlog.Log, resolver dns.Resolver, dialer smtpclient.Diale if submiterr != nil { qlog.Infox("smtp transaction for delivery failed", submiterr) } - failed = 0 // Reset, we are looking at the SMTP results below. + failed, delivered = processDeliveries(qlog, m0, msgs, addr, transport.Host, backoff, rcptErrs, submiterr) +} + +// Process failures and successful deliveries, retiring/removing messages from +// queue, queueing webhooks. +// +// Also used by deliverLocalserve. +func processDeliveries(qlog mlog.Log, m0 *Msg, msgs []*Msg, remoteAddr string, remoteHost string, backoff time.Duration, rcptErrs []smtpclient.Response, submiterr error) (failed, delivered int) { var delMsgs []Msg for i, m := range msgs { qmlog := qlog.With( @@ -246,14 +253,14 @@ func deliverSubmit(qlog mlog.Log, resolver dns.Resolver, dialer smtpclient.Diale } if err != nil { smtperr, ok := err.(smtpclient.Error) - err = fmt.Errorf("transport %s: submitting message to %s: %w", transportName, addr, err) + err = fmt.Errorf("delivering message to %s: %w", remoteAddr, err) var remoteMTA dsn.NameIP if ok { - remoteMTA.Name = transport.Host + remoteMTA.Name = remoteHost smtperr.Err = err err = smtperr } - qmlog.Errorx("submitting message", err, slog.String("remote", addr)) + qmlog.Errorx("submitting message", err, slog.String("remote", remoteAddr)) failMsgsDB(qmlog, []*Msg{m}, m0.DialedIPs, backoff, remoteMTA, err) failed++ } else { @@ -274,4 +281,5 @@ func deliverSubmit(qlog mlog.Log, resolver dns.Resolver, dialer smtpclient.Diale } kick() } + return } diff --git a/smtpserver/server.go b/smtpserver/server.go index f9ddcbb..e62b06e 100644 --- a/smtpserver/server.go +++ b/smtpserver/server.go @@ -5,6 +5,7 @@ import ( "bufio" "bytes" "context" + "crypto/ed25519" "crypto/md5" cryptorand "crypto/rand" "crypto/rsa" @@ -1617,18 +1618,11 @@ func (c *conn) cmdRcpt(p *parser) { } } - if Localserve { - if strings.HasPrefix(string(fpath.Localpart), "rcptto") { - c.xlocalserveError(fpath.Localpart) - } + if Localserve && strings.HasPrefix(string(fpath.Localpart), "rcptto") { + c.xlocalserveError(fpath.Localpart) + } - // If account or destination doesn't exist, it will be handled during delivery. For - // submissions, which is the common case, we'll deliver to the logged in user, - // which is typically the mox user. - acc, _ := mox.Conf.Account("mox") - dest := acc.Destinations["mox@localhost"] - c.recipients = append(c.recipients, rcptAccount{fpath, true, "mox", dest, "mox@localhost"}) - } else if len(fpath.IPDomain.IP) > 0 { + if len(fpath.IPDomain.IP) > 0 { if !c.submission { xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeAddr1UnknownDestMailbox1, "not accepting email for ip") } @@ -1636,6 +1630,14 @@ func (c *conn) cmdRcpt(p *parser) { } else if accountName, canonical, addr, err := mox.FindAccount(fpath.Localpart, fpath.IPDomain.Domain, true); err == nil { // note: a bare postmaster, without domain, is handled by FindAccount. ../rfc/5321:735 c.recipients = append(c.recipients, rcptAccount{fpath, true, accountName, addr, canonical}) + } else if Localserve { + // If the address isn't known, and we are in localserve, deliver to the mox user. + // If account or destination doesn't exist, it will be handled during delivery. For + // submissions, which is the common case, we'll deliver to the logged in user, + // which is typically the mox user. + acc, _ := mox.Conf.Account("mox") + dest := acc.Destinations["mox@localhost"] + c.recipients = append(c.recipients, rcptAccount{fpath, true, "mox", dest, "mox@localhost"}) } else if errors.Is(err, mox.ErrDomainNotFound) { if !c.submission { xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeAddr1UnknownDestMailbox1, "not accepting email for domain") @@ -1999,7 +2001,7 @@ func (c *conn) submit(ctx context.Context, recvHdrFor func(string) string, msgWr msgPrefix = append(msgPrefix, "Date: "+time.Now().Format(message.RFC5322Z)+"\r\n"...) } - // Check outoging message rate limit. + // Check outgoing message rate limit. err = c.account.DB.Read(ctx, func(tx *bstore.Tx) error { rcpts := make([]smtp.Path, len(c.recipients)) for i, r := range c.recipients { @@ -2286,7 +2288,34 @@ func (c *conn) deliver(ctx context.Context, recvHdrFor func(string) string, msgW dkimctx, dkimcancel := context.WithTimeout(ctx, time.Minute) defer dkimcancel() // todo future: we could let user configure which dkim headers they require - dkimResults, dkimErr = dkim.Verify(dkimctx, c.log.Logger, c.resolver, c.msgsmtputf8, dkim.DefaultPolicy, dataFile, ignoreTestMode) + + // For localserve, fake dkim selector DNS records for hosted domains to give + // dkim-signatures a chance to pass for deliveries from queue. + resolver := c.resolver + if Localserve { + // Lookup based on message From address is an approximation. + if dc, ok := mox.Conf.Domain(msgFrom.Domain); ok && len(dc.DKIM.Selectors) > 0 { + txts := map[string][]string{} + for name, sel := range dc.DKIM.Selectors { + dkimr := dkim.Record{ + Version: "DKIM1", + Hashes: []string{sel.HashEffective}, + PublicKey: sel.Key.Public(), + } + if _, ok := sel.Key.(ed25519.PrivateKey); ok { + dkimr.Key = "ed25519" + } else if _, ok := sel.Key.(*rsa.PrivateKey); !ok { + err := fmt.Errorf("unrecognized private key for DKIM selector %q: %T", name, sel.Key) + xcheckf(err, "making dkim record") + } + txt, err := dkimr.Record() + xcheckf(err, "making DKIM DNS TXT record") + txts[name+"._domainkey."+msgFrom.Domain.ASCII+"."] = []string{txt} + } + resolver = dns.MockResolver{TXT: txts} + } + } + dkimResults, dkimErr = dkim.Verify(dkimctx, c.log.Logger, resolver, c.msgsmtputf8, dkim.DefaultPolicy, dataFile, ignoreTestMode) dkimcancel() }() @@ -2318,7 +2347,17 @@ func (c *conn) deliver(ctx context.Context, recvHdrFor func(string) string, msgW defer wg.Done() spfctx, spfcancel := context.WithTimeout(ctx, time.Minute) defer spfcancel() - receivedSPF, spfDomain, spfExpl, spfAuthentic, spfErr = spf.Verify(spfctx, c.log.Logger, c.resolver, spfArgs) + resolver := c.resolver + // For localserve, give hosted domains a chance to pass for deliveries from queue. + if Localserve && c.remoteIP.IsLoopback() { + // Lookup based on message From address is an approximation. + if _, ok := mox.Conf.Domain(msgFrom.Domain); ok { + resolver = dns.MockResolver{ + TXT: map[string][]string{msgFrom.Domain.ASCII + ".": {"v=spf1 ip4:127.0.0.1/8 ip6:::1 ~all"}}, + } + } + } + receivedSPF, spfDomain, spfExpl, spfAuthentic, spfErr = spf.Verify(spfctx, c.log.Logger, resolver, spfArgs) spfcancel() if spfErr != nil { c.log.Infox("spf verify", spfErr) @@ -3001,7 +3040,7 @@ func (c *conn) deliver(ctx context.Context, recvHdrFor func(string) string, msgW } } - if Localserve && !strings.HasPrefix(string(rcptAcc.rcptTo.Localpart), "queue") { + if Localserve { code, timeout := mox.LocalserveNeedsError(rcptAcc.rcptTo.Localpart) if timeout { log.Info("timing out due to special localpart")