mirror of
https://github.com/mjl-/mox.git
synced 2024-12-26 08:23:48 +03:00
deprecate having only localparts in an Account's Destinations, it should always be a full email address
current behaviour isn't intuitive. it's not great to have to attempt parsing the strings as both localpart and email address. so we deprecate the localpart-only behaviour. when we load the config file, and it has localpart-only Destinations keys, we'll change them to full addresses in memory. when an admin causes a write of domains.conf, it'll automatically be fixed. we log an error with a deprecated notice for each localpart-only destinations key. sometime in the future, we can remove the old localpart-only destination support. will be in the release notes then. also start keeping track of update notes that need to make it in the release notes of the next release. for issue #18
This commit is contained in:
parent
5742ed1537
commit
2c07645ab4
20 changed files with 57 additions and 41 deletions
|
@ -225,9 +225,9 @@ type DKIM struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type Account 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."`
|
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 {
|
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?
|
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."`
|
} `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."`
|
||||||
|
|
|
@ -436,15 +436,16 @@ describe-static" and "mox config describe-domains":
|
||||||
Accounts:
|
Accounts:
|
||||||
x:
|
x:
|
||||||
|
|
||||||
# Default domain for addresses specified in Destinations. An address can specify a
|
# Default domain for account. Deprecated behaviour: If a destination is not a full
|
||||||
# domain override.
|
# address but only a localpart, this domain is added to form a full address.
|
||||||
Domain:
|
Domain:
|
||||||
|
|
||||||
# Free form description, e.g. full name or alternative contact info. (optional)
|
# Free form description, e.g. full name or alternative contact info. (optional)
|
||||||
Description:
|
Description:
|
||||||
|
|
||||||
# Destinations, specified as (encoded) localpart for Domain, or a full address
|
# Destinations, keys are email addresses (with IDNA domains). Deprecated
|
||||||
# including domain override.
|
# behaviour: If the address is not a full address but a localpart, it is combined
|
||||||
|
# with Domain to form a full address.
|
||||||
Destinations:
|
Destinations:
|
||||||
x:
|
x:
|
||||||
|
|
||||||
|
|
|
@ -67,7 +67,7 @@ func TestAccount(t *testing.T) {
|
||||||
test(authBad, "")
|
test(authBad, "")
|
||||||
|
|
||||||
_, dests := Account{}.Destinations(authCtx)
|
_, 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()
|
go importManage()
|
||||||
|
|
||||||
|
|
|
@ -115,7 +115,7 @@ func MakeAccountConfig(addr smtp.Address) config.Account {
|
||||||
account := config.Account{
|
account := config.Account{
|
||||||
Domain: addr.Domain.Name(),
|
Domain: addr.Domain.Name(),
|
||||||
Destinations: map[string]config.Destination{
|
Destinations: map[string]config.Destination{
|
||||||
addr.Localpart.String(): {},
|
addr.String(): {},
|
||||||
},
|
},
|
||||||
RejectsMailbox: "Rejects",
|
RejectsMailbox: "Rejects",
|
||||||
JunkFilter: &config.JunkFilter{
|
JunkFilter: &config.JunkFilter{
|
||||||
|
@ -676,13 +676,7 @@ func AddressAdd(ctx context.Context, address, account string) (rerr error) {
|
||||||
for name, d := range a.Destinations {
|
for name, d := range a.Destinations {
|
||||||
nd[name] = d
|
nd[name] = d
|
||||||
}
|
}
|
||||||
var k string
|
nd[addr.String()] = config.Destination{}
|
||||||
if addr.Domain == a.DNSDomain {
|
|
||||||
k = addr.Localpart.String()
|
|
||||||
} else {
|
|
||||||
k = addr.String()
|
|
||||||
}
|
|
||||||
nd[k] = config.Destination{}
|
|
||||||
a.Destinations = nd
|
a.Destinations = nd
|
||||||
nc.Accounts[account] = a
|
nc.Accounts[account] = a
|
||||||
|
|
||||||
|
@ -727,6 +721,7 @@ func AddressRemove(ctx context.Context, address string) (rerr error) {
|
||||||
na.Destinations = map[string]config.Destination{}
|
na.Destinations = map[string]config.Destination{}
|
||||||
var dropped bool
|
var dropped bool
|
||||||
for name, d := range a.Destinations {
|
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) {
|
if !(name == addr.Localpart.String() && a.DNSDomain == addr.Domain || name == addrStr) {
|
||||||
na.Destinations[name] = d
|
na.Destinations[name] = d
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -847,6 +847,9 @@ func prepareDynamicConfig(ctx context.Context, dynamicPath string, static config
|
||||||
}
|
}
|
||||||
c.Accounts[accName] = acc
|
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 {
|
for addrName, dest := range acc.Destinations {
|
||||||
checkMailboxNormf(dest.Mailbox, "account %q, destination %q", accName, addrName)
|
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
|
var address smtp.Address
|
||||||
localpart, err := smtp.ParseLocalpart(addrName)
|
localpart, err := smtp.ParseLocalpart(addrName)
|
||||||
if err != nil && errors.Is(err, smtp.ErrBadLocalpart) {
|
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)
|
addErrorf("unknown domain %s for account %q", acc.DNSDomain.Name(), accName)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
replaceLocalparts[addrName] = address.Pack(true)
|
||||||
}
|
}
|
||||||
addrFull := address.Pack(true)
|
addrFull := address.Pack(true)
|
||||||
if _, ok := accDests[addrFull]; ok {
|
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}
|
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.
|
// Set DMARC destinations.
|
||||||
|
|
|
@ -125,16 +125,16 @@ webhandlers".
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fatalf("parsing email address: %s", err)
|
fatalf("parsing email address: %s", err)
|
||||||
}
|
}
|
||||||
username := addr.Localpart.String()
|
accountName := addr.Localpart.String()
|
||||||
domain := addr.Domain
|
domain := addr.Domain
|
||||||
|
|
||||||
for _, c := range username {
|
for _, c := range accountName {
|
||||||
if c > 0x7f {
|
if c > 0x7f {
|
||||||
fmt.Printf(`NOTE: Username %q is not ASCII-only. It is recommended you also configure an
|
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
|
ASCII-only alias. Both for delivery of email from other systems, and for
|
||||||
logging in with IMAP.
|
logging in with IMAP.
|
||||||
|
|
||||||
`, username)
|
`, accountName)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -538,7 +538,7 @@ listed in more DNS block lists, visit:
|
||||||
"public": public,
|
"public": public,
|
||||||
"internal": internal,
|
"internal": internal,
|
||||||
}
|
}
|
||||||
sc.Postmaster.Account = username
|
sc.Postmaster.Account = accountName
|
||||||
sc.Postmaster.Mailbox = "Postmaster"
|
sc.Postmaster.Mailbox = "Postmaster"
|
||||||
|
|
||||||
mox.ConfigStaticPath = "config/mox.conf"
|
mox.ConfigStaticPath = "config/mox.conf"
|
||||||
|
@ -548,7 +548,7 @@ listed in more DNS block lists, visit:
|
||||||
|
|
||||||
accountConf := mox.MakeAccountConfig(addr)
|
accountConf := mox.MakeAccountConfig(addr)
|
||||||
const withMTASTS = true
|
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 {
|
if err != nil {
|
||||||
fatalf("making domain config: %s", err)
|
fatalf("making domain config: %s", err)
|
||||||
}
|
}
|
||||||
|
@ -558,7 +558,7 @@ listed in more DNS block lists, visit:
|
||||||
domain.Name(): confDomain,
|
domain.Name(): confDomain,
|
||||||
}
|
}
|
||||||
dc.Accounts = map[string]config.Account{
|
dc.Accounts = map[string]config.Account{
|
||||||
username: accountConf,
|
accountName: accountConf,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build config in memory, so we can easily comment out the DNSBLs config.
|
// 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
|
// This approach is a bit horrible, but it generates a convenient
|
||||||
// example that includes the comments. Though it is gone by the first
|
// example that includes the comments. Though it is gone by the first
|
||||||
// write of the file by mox.
|
// 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 {
|
var destsExample = struct {
|
||||||
Destinations map[string]config.Destination
|
Destinations map[string]config.Destination
|
||||||
}{
|
}{
|
||||||
Destinations: map[string]config.Destination{
|
Destinations: map[string]config.Destination{
|
||||||
addr.Localpart.String(): {
|
addr.String(): {
|
||||||
Rulesets: []config.Ruleset{
|
Rulesets: []config.Ruleset{
|
||||||
{
|
{
|
||||||
VerifiedDomain: "list.example.org",
|
VerifiedDomain: "list.example.org",
|
||||||
|
@ -641,7 +641,7 @@ listed in more DNS block lists, visit:
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fatalf("open account: %s", err)
|
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()
|
password := pwgen()
|
||||||
if err := acc.SetPassword(password); err != nil {
|
if err := acc.SetPassword(password); err != nil {
|
||||||
|
|
2
testdata/dsn/domains.conf
vendored
2
testdata/dsn/domains.conf
vendored
|
@ -23,4 +23,4 @@ Accounts:
|
||||||
mjl:
|
mjl:
|
||||||
Domain: mox.example
|
Domain: mox.example
|
||||||
Destinations:
|
Destinations:
|
||||||
mjl: nil
|
mjl@mox.example: nil
|
||||||
|
|
4
testdata/httpaccount/domains.conf
vendored
4
testdata/httpaccount/domains.conf
vendored
|
@ -4,7 +4,7 @@ Accounts:
|
||||||
mjl:
|
mjl:
|
||||||
Domain: mox.example
|
Domain: mox.example
|
||||||
Destinations:
|
Destinations:
|
||||||
mjl:
|
mjl@mox.example:
|
||||||
Mailbox: Inbox
|
Mailbox: Inbox
|
||||||
Rulesets:
|
Rulesets:
|
||||||
-
|
-
|
||||||
|
@ -15,7 +15,7 @@ Accounts:
|
||||||
HeadersRegexp:
|
HeadersRegexp:
|
||||||
subject: .*
|
subject: .*
|
||||||
Mailbox: Catchall
|
Mailbox: Catchall
|
||||||
other:
|
other@mox.example:
|
||||||
Mailbox: Other
|
Mailbox: Other
|
||||||
JunkFilter:
|
JunkFilter:
|
||||||
Threshold: 0.950000
|
Threshold: 0.950000
|
||||||
|
|
2
testdata/imap/domains.conf
vendored
2
testdata/imap/domains.conf
vendored
|
@ -5,7 +5,7 @@ Accounts:
|
||||||
mjl:
|
mjl:
|
||||||
Domain: mox.example
|
Domain: mox.example
|
||||||
Destinations:
|
Destinations:
|
||||||
mjl: nil
|
mjl@mox.example: nil
|
||||||
JunkFilter:
|
JunkFilter:
|
||||||
Threshold: 0.95
|
Threshold: 0.95
|
||||||
Params:
|
Params:
|
||||||
|
|
2
testdata/imaptest/config/domains.conf
vendored
2
testdata/imaptest/config/domains.conf
vendored
|
@ -5,7 +5,7 @@ Accounts:
|
||||||
mjl:
|
mjl:
|
||||||
Domain: mox.example
|
Domain: mox.example
|
||||||
Destinations:
|
Destinations:
|
||||||
mjl: nil
|
mjl@mox.example: nil
|
||||||
JunkFilter:
|
JunkFilter:
|
||||||
Threshold: 0.95
|
Threshold: 0.95
|
||||||
Params:
|
Params:
|
||||||
|
|
6
testdata/integration/config/domains.conf
vendored
6
testdata/integration/config/domains.conf
vendored
|
@ -39,7 +39,7 @@ Accounts:
|
||||||
moxtest1:
|
moxtest1:
|
||||||
Domain: mox1.example
|
Domain: mox1.example
|
||||||
Destinations:
|
Destinations:
|
||||||
moxtest1: nil
|
moxtest1@mox1.example: nil
|
||||||
JunkFilter:
|
JunkFilter:
|
||||||
Threshold: 0.9999
|
Threshold: 0.9999
|
||||||
Params:
|
Params:
|
||||||
|
@ -53,7 +53,7 @@ Accounts:
|
||||||
moxtest2:
|
moxtest2:
|
||||||
Domain: mox2.example
|
Domain: mox2.example
|
||||||
Destinations:
|
Destinations:
|
||||||
moxtest2: nil
|
moxtest2@mox2.example: nil
|
||||||
JunkFilter:
|
JunkFilter:
|
||||||
Threshold: 0.9999
|
Threshold: 0.9999
|
||||||
Params:
|
Params:
|
||||||
|
@ -67,7 +67,7 @@ Accounts:
|
||||||
moxtest3:
|
moxtest3:
|
||||||
Domain: mox3.example
|
Domain: mox3.example
|
||||||
Destinations:
|
Destinations:
|
||||||
moxtest3: nil
|
moxtest3@mox3.example: nil
|
||||||
SubjectPass:
|
SubjectPass:
|
||||||
Period: 1h
|
Period: 1h
|
||||||
RejectsMailbox: rejects
|
RejectsMailbox: rejects
|
||||||
|
|
2
testdata/queue/domains.conf
vendored
2
testdata/queue/domains.conf
vendored
|
@ -4,4 +4,4 @@ Accounts:
|
||||||
mjl:
|
mjl:
|
||||||
Domain: mox.example
|
Domain: mox.example
|
||||||
Destinations:
|
Destinations:
|
||||||
mjl: nil
|
mjl@mox.example: nil
|
||||||
|
|
2
testdata/smtp/dmarcreport/domains.conf
vendored
2
testdata/smtp/dmarcreport/domains.conf
vendored
|
@ -8,4 +8,4 @@ Accounts:
|
||||||
mjl:
|
mjl:
|
||||||
Domain: mox.example
|
Domain: mox.example
|
||||||
Destinations:
|
Destinations:
|
||||||
mjl: nil
|
mjl@mox.example: nil
|
||||||
|
|
2
testdata/smtp/domains.conf
vendored
2
testdata/smtp/domains.conf
vendored
|
@ -4,7 +4,7 @@ Accounts:
|
||||||
mjl:
|
mjl:
|
||||||
Domain: mox.example
|
Domain: mox.example
|
||||||
Destinations:
|
Destinations:
|
||||||
mjl: nil
|
mjl@mox.example: nil
|
||||||
JunkFilter:
|
JunkFilter:
|
||||||
Threshold: 0.9
|
Threshold: 0.9
|
||||||
Params:
|
Params:
|
||||||
|
|
2
testdata/smtp/junk/domains.conf
vendored
2
testdata/smtp/junk/domains.conf
vendored
|
@ -4,7 +4,7 @@ Accounts:
|
||||||
mjl:
|
mjl:
|
||||||
Domain: mox.example
|
Domain: mox.example
|
||||||
Destinations:
|
Destinations:
|
||||||
mjl: nil
|
mjl@mox.example: nil
|
||||||
RejectsMailbox: Rejects
|
RejectsMailbox: Rejects
|
||||||
JunkFilter:
|
JunkFilter:
|
||||||
# Spamminess score between 0 and 1 above which emails are rejected as spam. E.g.
|
# Spamminess score between 0 and 1 above which emails are rejected as spam. E.g.
|
||||||
|
|
2
testdata/smtp/tlsrpt/domains.conf
vendored
2
testdata/smtp/tlsrpt/domains.conf
vendored
|
@ -8,4 +8,4 @@ Accounts:
|
||||||
mjl:
|
mjl:
|
||||||
Domain: mox.example
|
Domain: mox.example
|
||||||
Destinations:
|
Destinations:
|
||||||
mjl: nil
|
mjl@mox.example: nil
|
||||||
|
|
4
testdata/store/domains.conf
vendored
4
testdata/store/domains.conf
vendored
|
@ -4,7 +4,7 @@ Accounts:
|
||||||
mjl:
|
mjl:
|
||||||
Domain: mox.example
|
Domain: mox.example
|
||||||
Destinations:
|
Destinations:
|
||||||
mjl:
|
mjl@mox.example:
|
||||||
Mailbox: Inbox
|
Mailbox: Inbox
|
||||||
Rulesets:
|
Rulesets:
|
||||||
-
|
-
|
||||||
|
@ -15,7 +15,7 @@ Accounts:
|
||||||
HeadersRegexp:
|
HeadersRegexp:
|
||||||
subject: .*
|
subject: .*
|
||||||
Mailbox: Catchall
|
Mailbox: Catchall
|
||||||
other:
|
other@mox.example:
|
||||||
Mailbox: Other
|
Mailbox: Other
|
||||||
JunkFilter:
|
JunkFilter:
|
||||||
Threshold: 0.95
|
Threshold: 0.95
|
||||||
|
|
2
testdata/web/domains.conf
vendored
2
testdata/web/domains.conf
vendored
|
@ -5,7 +5,7 @@ Accounts:
|
||||||
mjl:
|
mjl:
|
||||||
Domain: mox.example
|
Domain: mox.example
|
||||||
Destinations:
|
Destinations:
|
||||||
mjl: nil
|
mjl@mox.example: nil
|
||||||
WebDomainRedirects:
|
WebDomainRedirects:
|
||||||
redir.mox.example: mox.example
|
redir.mox.example: mox.example
|
||||||
WebHandlers:
|
WebHandlers:
|
||||||
|
|
2
testdata/webserver/domains.conf
vendored
2
testdata/webserver/domains.conf
vendored
|
@ -5,7 +5,7 @@ Accounts:
|
||||||
mjl:
|
mjl:
|
||||||
Domain: mox.example
|
Domain: mox.example
|
||||||
Destinations:
|
Destinations:
|
||||||
mjl: nil
|
mjl@mox.example: nil
|
||||||
WebDomainRedirects:
|
WebDomainRedirects:
|
||||||
redir.mox.example: mox.example
|
redir.mox.example: mox.example
|
||||||
WebHandlers:
|
WebHandlers:
|
||||||
|
|
4
updating.txt
Normal file
4
updating.txt
Normal file
|
@ -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.
|
Loading…
Reference in a new issue