diff --git a/config/config.go b/config/config.go index e80eaac..b5446a9 100644 --- a/config/config.go +++ b/config/config.go @@ -225,9 +225,9 @@ type DKIM struct { } type Account struct { - Domain string `sconf-doc:"Default domain for addresses specified in Destinations. An address can specify a domain override."` + Domain string `sconf-doc:"Default domain for account. Deprecated behaviour: If a destination is not a full address but only a localpart, this domain is added to form a full address."` Description string `sconf:"optional" sconf-doc:"Free form description, e.g. full name or alternative contact info."` - Destinations map[string]Destination `sconf-doc:"Destinations, specified as (encoded) localpart for Domain, or a full address including domain override."` + Destinations map[string]Destination `sconf-doc:"Destinations, keys are email addresses (with IDNA domains). Deprecated behaviour: If the address is not a full address but a localpart, it is combined with Domain to form a full address."` SubjectPass struct { Period time.Duration `sconf-doc:"How long unique values are accepted after generating, e.g. 12h."` // todo: have a reasonable default for this? } `sconf:"optional" sconf-doc:"If configured, messages classified as weakly spam are rejected with instructions to retry delivery, but this time with a signed token added to the subject. During the next delivery attempt, the signed token will bypass the spam filter. Messages with a clear spam signal, such as a known bad reputation, are rejected/delayed without a signed token."` diff --git a/config/doc.go b/config/doc.go index 02ee9c7..6b801f7 100644 --- a/config/doc.go +++ b/config/doc.go @@ -436,15 +436,16 @@ describe-static" and "mox config describe-domains": Accounts: x: - # Default domain for addresses specified in Destinations. An address can specify a - # domain override. + # Default domain for account. Deprecated behaviour: If a destination is not a full + # address but only a localpart, this domain is added to form a full address. Domain: # Free form description, e.g. full name or alternative contact info. (optional) Description: - # Destinations, specified as (encoded) localpart for Domain, or a full address - # including domain override. + # Destinations, keys are email addresses (with IDNA domains). Deprecated + # behaviour: If the address is not a full address but a localpart, it is combined + # with Domain to form a full address. Destinations: x: diff --git a/http/account_test.go b/http/account_test.go index 30b9486..ee65cb8 100644 --- a/http/account_test.go +++ b/http/account_test.go @@ -67,7 +67,7 @@ func TestAccount(t *testing.T) { test(authBad, "") _, dests := Account{}.Destinations(authCtx) - Account{}.DestinationSave(authCtx, "mjl", dests["mjl"], dests["mjl"]) // todo: save modified value and compare it afterwards + Account{}.DestinationSave(authCtx, "mjl@mox.example", dests["mjl@mox.example"], dests["mjl@mox.example"]) // todo: save modified value and compare it afterwards go importManage() diff --git a/mox-/admin.go b/mox-/admin.go index dd8ba75..efea37a 100644 --- a/mox-/admin.go +++ b/mox-/admin.go @@ -115,7 +115,7 @@ func MakeAccountConfig(addr smtp.Address) config.Account { account := config.Account{ Domain: addr.Domain.Name(), Destinations: map[string]config.Destination{ - addr.Localpart.String(): {}, + addr.String(): {}, }, RejectsMailbox: "Rejects", JunkFilter: &config.JunkFilter{ @@ -676,13 +676,7 @@ func AddressAdd(ctx context.Context, address, account string) (rerr error) { for name, d := range a.Destinations { nd[name] = d } - var k string - if addr.Domain == a.DNSDomain { - k = addr.Localpart.String() - } else { - k = addr.String() - } - nd[k] = config.Destination{} + nd[addr.String()] = config.Destination{} a.Destinations = nd nc.Accounts[account] = a @@ -727,6 +721,7 @@ func AddressRemove(ctx context.Context, address string) (rerr error) { na.Destinations = map[string]config.Destination{} var dropped bool for name, d := range a.Destinations { + // todo deprecated: remove support for localpart-only with default domain as destination address. if !(name == addr.Localpart.String() && a.DNSDomain == addr.Domain || name == addrStr) { na.Destinations[name] = d } else { diff --git a/mox-/config.go b/mox-/config.go index cc1d36a..6d91214 100644 --- a/mox-/config.go +++ b/mox-/config.go @@ -847,6 +847,9 @@ func prepareDynamicConfig(ctx context.Context, dynamicPath string, static config } c.Accounts[accName] = acc + // todo deprecated: only localpart as keys for Destinations, we are replacing them with full addresses. if domains.conf is written, we won't have to do this again. + replaceLocalparts := map[string]string{} + for addrName, dest := range acc.Destinations { checkMailboxNormf(dest.Mailbox, "account %q, destination %q", accName, addrName) @@ -906,6 +909,7 @@ func prepareDynamicConfig(ctx context.Context, dynamicPath string, static config } } + // todo deprecated: remove support for parsing destination as just a localpart instead full address. var address smtp.Address localpart, err := smtp.ParseLocalpart(addrName) if err != nil && errors.Is(err, smtp.ErrBadLocalpart) { @@ -927,6 +931,7 @@ func prepareDynamicConfig(ctx context.Context, dynamicPath string, static config addErrorf("unknown domain %s for account %q", acc.DNSDomain.Name(), accName) continue } + replaceLocalparts[addrName] = address.Pack(true) } addrFull := address.Pack(true) if _, ok := accDests[addrFull]; ok { @@ -934,6 +939,17 @@ func prepareDynamicConfig(ctx context.Context, dynamicPath string, static config } accDests[addrFull] = AccountDestination{address.Localpart, accName, dest} } + + for lp, addr := range replaceLocalparts { + dest, ok := acc.Destinations[lp] + if !ok { + addErrorf("could not find localpart %q to replace with address in destinations", lp) + } else { + log.Error("deprecated: destination with localpart-only key will be removed in the future, replace it with a full email address, by appending the default domain", mlog.Field("localpart", lp), mlog.Field("address", addr), mlog.Field("account", accName)) + acc.Destinations[addr] = dest + delete(acc.Destinations, lp) + } + } } // Set DMARC destinations. diff --git a/quickstart.go b/quickstart.go index 59508d4..ae99514 100644 --- a/quickstart.go +++ b/quickstart.go @@ -125,16 +125,16 @@ webhandlers". if err != nil { fatalf("parsing email address: %s", err) } - username := addr.Localpart.String() + accountName := addr.Localpart.String() domain := addr.Domain - for _, c := range username { + for _, c := range accountName { if c > 0x7f { fmt.Printf(`NOTE: Username %q is not ASCII-only. It is recommended you also configure an ASCII-only alias. Both for delivery of email from other systems, and for logging in with IMAP. -`, username) +`, accountName) break } } @@ -538,7 +538,7 @@ listed in more DNS block lists, visit: "public": public, "internal": internal, } - sc.Postmaster.Account = username + sc.Postmaster.Account = accountName sc.Postmaster.Mailbox = "Postmaster" mox.ConfigStaticPath = "config/mox.conf" @@ -548,7 +548,7 @@ listed in more DNS block lists, visit: accountConf := mox.MakeAccountConfig(addr) const withMTASTS = true - confDomain, keyPaths, err := mox.MakeDomainConfig(context.Background(), domain, dnshostname, username, withMTASTS) + confDomain, keyPaths, err := mox.MakeDomainConfig(context.Background(), domain, dnshostname, accountName, withMTASTS) if err != nil { fatalf("making domain config: %s", err) } @@ -558,7 +558,7 @@ listed in more DNS block lists, visit: domain.Name(): confDomain, } dc.Accounts = map[string]config.Account{ - username: accountConf, + accountName: accountConf, } // Build config in memory, so we can easily comment out the DNSBLs config. @@ -584,12 +584,12 @@ listed in more DNS block lists, visit: // This approach is a bit horrible, but it generates a convenient // example that includes the comments. Though it is gone by the first // write of the file by mox. - odests := fmt.Sprintf("\t\tDestinations:\n\t\t\t%s: nil\n", addr.Localpart.String()) + odests := fmt.Sprintf("\t\tDestinations:\n\t\t\t%s: nil\n", addr.String()) var destsExample = struct { Destinations map[string]config.Destination }{ Destinations: map[string]config.Destination{ - addr.Localpart.String(): { + addr.String(): { Rulesets: []config.Ruleset{ { VerifiedDomain: "list.example.org", @@ -641,7 +641,7 @@ listed in more DNS block lists, visit: if err != nil { fatalf("open account: %s", err) } - cleanupPaths = append(cleanupPaths, dataDir, filepath.Join(dataDir, "accounts"), filepath.Join(dataDir, "accounts", username), filepath.Join(dataDir, "accounts", username, "index.db")) + cleanupPaths = append(cleanupPaths, dataDir, filepath.Join(dataDir, "accounts"), filepath.Join(dataDir, "accounts", accountName), filepath.Join(dataDir, "accounts", accountName, "index.db")) password := pwgen() if err := acc.SetPassword(password); err != nil { diff --git a/testdata/dsn/domains.conf b/testdata/dsn/domains.conf index d988250..e480a8c 100644 --- a/testdata/dsn/domains.conf +++ b/testdata/dsn/domains.conf @@ -23,4 +23,4 @@ Accounts: mjl: Domain: mox.example Destinations: - mjl: nil + mjl@mox.example: nil diff --git a/testdata/httpaccount/domains.conf b/testdata/httpaccount/domains.conf index 8c2045f..4eb5404 100644 --- a/testdata/httpaccount/domains.conf +++ b/testdata/httpaccount/domains.conf @@ -4,7 +4,7 @@ Accounts: mjl: Domain: mox.example Destinations: - mjl: + mjl@mox.example: Mailbox: Inbox Rulesets: - @@ -15,7 +15,7 @@ Accounts: HeadersRegexp: subject: .* Mailbox: Catchall - other: + other@mox.example: Mailbox: Other JunkFilter: Threshold: 0.950000 diff --git a/testdata/imap/domains.conf b/testdata/imap/domains.conf index 66d3e18..3ba0f08 100644 --- a/testdata/imap/domains.conf +++ b/testdata/imap/domains.conf @@ -5,7 +5,7 @@ Accounts: mjl: Domain: mox.example Destinations: - mjl: nil + mjl@mox.example: nil JunkFilter: Threshold: 0.95 Params: diff --git a/testdata/imaptest/config/domains.conf b/testdata/imaptest/config/domains.conf index 66d3e18..3ba0f08 100644 --- a/testdata/imaptest/config/domains.conf +++ b/testdata/imaptest/config/domains.conf @@ -5,7 +5,7 @@ Accounts: mjl: Domain: mox.example Destinations: - mjl: nil + mjl@mox.example: nil JunkFilter: Threshold: 0.95 Params: diff --git a/testdata/integration/config/domains.conf b/testdata/integration/config/domains.conf index 00838c3..4978c31 100644 --- a/testdata/integration/config/domains.conf +++ b/testdata/integration/config/domains.conf @@ -39,7 +39,7 @@ Accounts: moxtest1: Domain: mox1.example Destinations: - moxtest1: nil + moxtest1@mox1.example: nil JunkFilter: Threshold: 0.9999 Params: @@ -53,7 +53,7 @@ Accounts: moxtest2: Domain: mox2.example Destinations: - moxtest2: nil + moxtest2@mox2.example: nil JunkFilter: Threshold: 0.9999 Params: @@ -67,7 +67,7 @@ Accounts: moxtest3: Domain: mox3.example Destinations: - moxtest3: nil + moxtest3@mox3.example: nil SubjectPass: Period: 1h RejectsMailbox: rejects diff --git a/testdata/queue/domains.conf b/testdata/queue/domains.conf index 7f8e01e..449f11e 100644 --- a/testdata/queue/domains.conf +++ b/testdata/queue/domains.conf @@ -4,4 +4,4 @@ Accounts: mjl: Domain: mox.example Destinations: - mjl: nil + mjl@mox.example: nil diff --git a/testdata/smtp/dmarcreport/domains.conf b/testdata/smtp/dmarcreport/domains.conf index 701c3e3..dacf14f 100644 --- a/testdata/smtp/dmarcreport/domains.conf +++ b/testdata/smtp/dmarcreport/domains.conf @@ -8,4 +8,4 @@ Accounts: mjl: Domain: mox.example Destinations: - mjl: nil + mjl@mox.example: nil diff --git a/testdata/smtp/domains.conf b/testdata/smtp/domains.conf index 4f2ddc1..e183c81 100644 --- a/testdata/smtp/domains.conf +++ b/testdata/smtp/domains.conf @@ -4,7 +4,7 @@ Accounts: mjl: Domain: mox.example Destinations: - mjl: nil + mjl@mox.example: nil JunkFilter: Threshold: 0.9 Params: diff --git a/testdata/smtp/junk/domains.conf b/testdata/smtp/junk/domains.conf index 450747f..f628f26 100644 --- a/testdata/smtp/junk/domains.conf +++ b/testdata/smtp/junk/domains.conf @@ -4,7 +4,7 @@ Accounts: mjl: Domain: mox.example Destinations: - mjl: nil + mjl@mox.example: nil RejectsMailbox: Rejects JunkFilter: # Spamminess score between 0 and 1 above which emails are rejected as spam. E.g. diff --git a/testdata/smtp/tlsrpt/domains.conf b/testdata/smtp/tlsrpt/domains.conf index b60cb65..40b2e9c 100644 --- a/testdata/smtp/tlsrpt/domains.conf +++ b/testdata/smtp/tlsrpt/domains.conf @@ -8,4 +8,4 @@ Accounts: mjl: Domain: mox.example Destinations: - mjl: nil + mjl@mox.example: nil diff --git a/testdata/store/domains.conf b/testdata/store/domains.conf index ea44a7b..a71ddcb 100644 --- a/testdata/store/domains.conf +++ b/testdata/store/domains.conf @@ -4,7 +4,7 @@ Accounts: mjl: Domain: mox.example Destinations: - mjl: + mjl@mox.example: Mailbox: Inbox Rulesets: - @@ -15,7 +15,7 @@ Accounts: HeadersRegexp: subject: .* Mailbox: Catchall - other: + other@mox.example: Mailbox: Other JunkFilter: Threshold: 0.95 diff --git a/testdata/web/domains.conf b/testdata/web/domains.conf index a77e266..c568924 100644 --- a/testdata/web/domains.conf +++ b/testdata/web/domains.conf @@ -5,7 +5,7 @@ Accounts: mjl: Domain: mox.example Destinations: - mjl: nil + mjl@mox.example: nil WebDomainRedirects: redir.mox.example: mox.example WebHandlers: diff --git a/testdata/webserver/domains.conf b/testdata/webserver/domains.conf index e62bb49..a6d5e46 100644 --- a/testdata/webserver/domains.conf +++ b/testdata/webserver/domains.conf @@ -5,7 +5,7 @@ Accounts: mjl: Domain: mox.example Destinations: - mjl: nil + mjl@mox.example: nil WebDomainRedirects: redir.mox.example: mox.example WebHandlers: diff --git a/updating.txt b/updating.txt new file mode 100644 index 0000000..bc34cce --- /dev/null +++ b/updating.txt @@ -0,0 +1,4 @@ +work in progress: update instructions for the next release + +- In domains.conf, for an account, the Destinations map will now always use full email addresses, no longer localparts relative to the Domain configured for the account. The old form with just a localpart is still accepted. When writing domains.conf through the cli commands or admin web pages, the destinations will automatically be written with full email addresses. In the future, support for the localpart-only form will be removed. +- If you run mox behind a NAT, you can now specify "IPsNATed: true" in the SMTP listener to skip a few DNS checks that previously would always fail due to the IPs being NATed.