diff --git a/.gitignore b/.gitignore index 3c52c1e..c0a3d9c 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,7 @@ /testdata/smtp/datajunk/ /testdata/smtp/sendlimit/data/ /testdata/smtp/catchall/data/ +/testdata/smtp/postmaster/ /testdata/store/data/ /testdata/train/ /cover.out diff --git a/config/config.go b/config/config.go index dddc6bc..7abbe00 100644 --- a/config/config.go +++ b/config/config.go @@ -52,7 +52,7 @@ type Static struct { Postmaster struct { Account string Mailbox string `sconf-doc:"E.g. Postmaster or Inbox."` - } `sconf-doc:"Destination for emails delivered to postmaster address."` + } `sconf-doc:"Destination for emails delivered to postmaster addresses: a plain 'postmaster' without domain, 'postmaster@' (also for each listener with SMTP enabled), and as fallback for each domain without explicitly configured postmaster destination."` DefaultMailboxes []string `sconf:"optional" sconf-doc:"Mailboxes to create when adding an account. Inbox is always created. If no mailboxes are specified, the following are automatically created: Sent, Archive, Trash, Drafts and Junk."` // All IPs that were explicitly listen on for external SMTP. Only set when there diff --git a/config/doc.go b/config/doc.go index ecda88b..cfad2d2 100644 --- a/config/doc.go +++ b/config/doc.go @@ -321,7 +321,10 @@ describe-static" and "mox config describe-domains": # Port for HTTPS webserver. (optional) Port: 0 - # Destination for emails delivered to postmaster address. + # Destination for emails delivered to postmaster addresses: a plain 'postmaster' + # without domain, 'postmaster@' (also for each listener with SMTP + # enabled), and as fallback for each domain without explicitly configured + # postmaster destination. Postmaster: Account: diff --git a/mox-/admin.go b/mox-/admin.go index 7df7f4a..c99d4ba 100644 --- a/mox-/admin.go +++ b/mox-/admin.go @@ -270,7 +270,7 @@ func MakeDomainConfig(ctx context.Context, domain, hostname dns.Domain, accountN // DomainAdd adds the domain to the domains config, rewriting domains.conf and // marking it loaded. // -// accountName is used for DMARC/TLS report. +// accountName is used for DMARC/TLS report and potentially for the postmaster address. // If the account does not exist, it is created with localpart. Localpart must be // set only if the account does not yet exist. func DomainAdd(ctx context.Context, domain dns.Domain, accountName string, localpart smtp.Localpart) (rerr error) { @@ -325,6 +325,16 @@ func DomainAdd(ctx context.Context, domain dns.Domain, accountName string, local return fmt.Errorf("account name is empty") } else if !ok { nc.Accounts[accountName] = MakeAccountConfig(smtp.Address{Localpart: localpart, Domain: domain}) + } else if accountName != Conf.Static.Postmaster.Account { + nacc := nc.Accounts[accountName] + nd := map[string]config.Destination{} + for k, v := range nacc.Destinations { + nd[k] = v + } + pmaddr := smtp.Address{Localpart: "postmaster", Domain: domain} + nd[pmaddr.String()] = config.Destination{} + nacc.Destinations = nd + nc.Accounts[accountName] = nacc } nc.Domains[domain.Name()] = confDomain diff --git a/mox-/lookup.go b/mox-/lookup.go index 228d6f1..2be8a29 100644 --- a/mox-/lookup.go +++ b/mox-/lookup.go @@ -22,8 +22,21 @@ func FindAccount(localpart smtp.Localpart, domain dns.Domain, allowPostmaster bo if strings.EqualFold(string(localpart), "postmaster") { localpart = "postmaster" } - var zerodomain dns.Domain - if localpart == "postmaster" && domain == zerodomain { + + postmasterDomain := func() bool { + var zerodomain dns.Domain + if domain == zerodomain || domain == Conf.Static.HostnameDomain { + return true + } + for _, l := range Conf.Static.Listeners { + if l.SMTP.Enabled && domain == l.HostnameDomain { + return true + } + } + return false + } + + if localpart == "postmaster" && postmasterDomain() { if !allowPostmaster { return "", "", config.Destination{}, ErrAccountNotFound } @@ -44,6 +57,9 @@ func FindAccount(localpart smtp.Localpart, domain dns.Domain, allowPostmaster bo accAddr, ok := Conf.AccountDestination(canonical) if !ok { if accAddr, ok = Conf.AccountDestination("@" + domain.Name()); !ok { + if localpart == "postmaster" && allowPostmaster { + return Conf.Static.Postmaster.Account, "postmaster", config.Destination{Mailbox: Conf.Static.Postmaster.Mailbox}, nil + } return "", "", config.Destination{}, ErrAccountNotFound } canonical = "@" + domain.Name() diff --git a/smtpserver/server_test.go b/smtpserver/server_test.go index 5d05bc9..bf2fbed 100644 --- a/smtpserver/server_test.go +++ b/smtpserver/server_test.go @@ -1097,3 +1097,37 @@ test email testSubmit("mjl@mox.example", "mjl@mox.example") testSubmit("mjl@mox.example", "mjl@mox2.example") // DKIM signature will be for mox2.example. } + +// Test to postmaster addresses. +func TestPostmaster(t *testing.T) { + resolver := dns.MockResolver{ + A: map[string][]string{ + "other.example.": {"127.0.0.10"}, // For mx check. + }, + PTR: map[string][]string{ + "127.0.0.10": {"other.example."}, + }, + } + ts := newTestServer(t, "../testdata/smtp/postmaster/mox.conf", resolver) + defer ts.close() + + testDeliver := func(rcptTo string, expErr *smtpclient.Error) { + t.Helper() + ts.run(func(err error, client *smtpclient.Client) { + t.Helper() + mailFrom := "mjl@other.example" + if err == nil { + err = client.Deliver(context.Background(), mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false) + } + var cerr smtpclient.Error + if expErr == nil && err != nil || expErr != nil && (err == nil || !errors.As(err, &cerr) || cerr.Code != expErr.Code || cerr.Secode != expErr.Secode) { + t.Fatalf("got err %#v, expected %#v", err, expErr) + } + }) + } + + testDeliver("postmaster", nil) // Plain postmaster address without domain. + testDeliver("postmaster@host.mox.example", nil) // Postmaster address with configured mail server hostname. + testDeliver("postmaster@mox.example", nil) // Postmaster address without explicitly configured destination. + testDeliver("postmaster@unknown.example", &smtpclient.Error{Code: smtp.C550MailboxUnavail, Secode: smtp.SeAddr1UnknownDestMailbox1}) +} diff --git a/testdata/smtp/postmaster/domains.conf b/testdata/smtp/postmaster/domains.conf new file mode 100644 index 0000000..449f11e --- /dev/null +++ b/testdata/smtp/postmaster/domains.conf @@ -0,0 +1,7 @@ +Domains: + mox.example: nil +Accounts: + mjl: + Domain: mox.example + Destinations: + mjl@mox.example: nil diff --git a/testdata/smtp/postmaster/mox.conf b/testdata/smtp/postmaster/mox.conf new file mode 100644 index 0000000..311852d --- /dev/null +++ b/testdata/smtp/postmaster/mox.conf @@ -0,0 +1,9 @@ +DataDir: data +User: 1000 +LogLevel: trace +Hostname: host.mox.example +Postmaster: + Account: mjl + Mailbox: postmaster +Listeners: + local: nil