Allow multiple localpart catch all separators, e.g. both "+" and "-", for addresses you+anything@example.com and you-anything@example.com

The original config option stays, and we still use it for the common case where
we have a single separator. The "+" is configured by default. It is optional,
just like the new option "LocalpartCatchallSeparators" (plural).

When parsing the config file, we combine LocalpartCatchallSeparator and
LocalpartCatchallSeparators into a single list
LocalpartCatchallSeparatorsEffective, which we use throughout the code.

For issue #301 by janc13
This commit is contained in:
Mechiel Lukkien 2025-03-07 14:39:58 +01:00
parent d0b241499f
commit 9a8bb1134b
No known key found for this signature in database
26 changed files with 255 additions and 95 deletions

View File

@ -737,7 +737,7 @@ func AccountRemove(ctx context.Context, account string) (rerr error) {
}
// checkAddressAvailable checks that the address after canonicalization is not
// already configured, and that its localpart does not contain the catchall
// already configured, and that its localpart does not contain a catchall
// localpart separator.
//
// Must be called with config lock held.
@ -749,9 +749,13 @@ func checkAddressAvailable(addr smtp.Address) error {
lp := mox.CanonicalLocalpart(addr.Localpart, dc)
if _, ok := mox.Conf.AccountDestinationsLocked[smtp.NewAddress(lp, addr.Domain).String()]; ok {
return fmt.Errorf("canonicalized address %s already configured", smtp.NewAddress(lp, addr.Domain))
} else if dc.LocalpartCatchallSeparator != "" && strings.Contains(string(addr.Localpart), dc.LocalpartCatchallSeparator) {
return fmt.Errorf("localpart cannot include domain catchall separator %s", dc.LocalpartCatchallSeparator)
} else if _, ok := dc.Aliases[lp.String()]; ok {
}
for _, sep := range dc.LocalpartCatchallSeparatorsEffective {
if strings.Contains(string(addr.Localpart), sep) {
return fmt.Errorf("localpart cannot include domain catchall separator %s", sep)
}
}
if _, ok := dc.Aliases[lp.String()]; ok {
return fmt.Errorf("address in use as alias")
}
return nil

View File

@ -283,6 +283,7 @@ type Domain struct {
Description string `sconf:"optional" sconf-doc:"Free-form description of domain."`
ClientSettingsDomain string `sconf:"optional" sconf-doc:"Hostname for client settings instead of the mail server hostname. E.g. mail.<domain>. For future migration to another mail operator without requiring all clients to update their settings, it is convenient to have client settings that reference a subdomain of the hosted domain instead of the hostname of the server where the mail is currently hosted. If empty, the hostname of the mail server is used for client configurations. Unicode name."`
LocalpartCatchallSeparator string `sconf:"optional" sconf-doc:"If not empty, only the string before the separator is used to for email delivery decisions. For example, if set to \"+\", you+anything@example.com will be delivered to you@example.com."`
LocalpartCatchallSeparators []string `sconf:"optional" sconf-doc:"Similar to LocalpartCatchallSeparator, but in case multiple are needed. For example both \"+\" and \"-\". Only of one LocalpartCatchallSeparator or LocalpartCatchallSeparators can be set. If set, the first separator is used to make unique addresses for outgoing SMTP connections with FromIDLoginAddresses."`
LocalpartCaseSensitive bool `sconf:"optional" sconf-doc:"If set, upper/lower case is relevant for email delivery."`
DKIM DKIM `sconf:"optional" sconf-doc:"With DKIM signing, a domain is taking responsibility for (content of) emails it sends, letting receiving mail servers build up a (hopefully positive) reputation of the domain, which can help with mail delivery."`
DMARC *DMARC `sconf:"optional" sconf-doc:"With DMARC, a domain publishes, in DNS, a policy on how other mail servers should handle incoming messages with the From-header matching this domain and/or subdomain (depending on the configured alignment). Receiving mail servers use this to build up a reputation of this domain, which can help with mail delivery. A domain can also publish an email address to which reports about DMARC verification results can be sent by verifying mail servers, useful for monitoring. Incoming DMARC reports are automatically parsed, validated, added to metrics and stored in the reporting database for later display in the admin web pages."`
@ -298,6 +299,7 @@ type Domain struct {
// hosting the reporting), and there are no destination addresses configured for
// the domain. Disables some functionality related to hosting a domain.
ReportsOnly bool `sconf:"-" json:"-"`
LocalpartCatchallSeparatorsEffective []string `sconf:"-"` // Either LocalpartCatchallSeparators, the value of LocalpartCatchallSeparator, or empty.
}
// todo: allow external addresses as members of aliases. we would add messages for them to the queue for outgoing delivery. we should require an admin addresses to which delivery failures will be delivered (locally, and to use in smtp mail from, so dsns go there). also take care to evaluate smtputf8 (if external address requires utf8 and incoming transaction didn't).

View File

@ -789,6 +789,14 @@ See https://pkg.go.dev/github.com/mjl-/sconf for details.
# delivered to you@example.com. (optional)
LocalpartCatchallSeparator:
# Similar to LocalpartCatchallSeparator, but in case multiple are needed. For
# example both "+" and "-". Only of one LocalpartCatchallSeparator or
# LocalpartCatchallSeparators can be set. If set, the first separator is used to
# make unique addresses for outgoing SMTP connections with FromIDLoginAddresses.
# (optional)
LocalpartCatchallSeparators:
-
# If set, upper/lower case is relevant for email delivery. (optional)
LocalpartCaseSensitive: false

View File

@ -1238,6 +1238,21 @@ func prepareDynamicConfig(ctx context.Context, log mlog.Log, dynamicPath string,
c.ClientSettingDomains[csd] = struct{}{}
}
if domain.LocalpartCatchallSeparator != "" && len(domain.LocalpartCatchallSeparators) != 0 {
addDomainErrorf("cannot have both LocalpartCatchallSeparator and LocalpartCatchallSeparators")
}
domain.LocalpartCatchallSeparatorsEffective = domain.LocalpartCatchallSeparators
if domain.LocalpartCatchallSeparator != "" {
domain.LocalpartCatchallSeparatorsEffective = append(domain.LocalpartCatchallSeparatorsEffective, domain.LocalpartCatchallSeparator)
}
sepSeen := map[string]bool{}
for _, sep := range domain.LocalpartCatchallSeparatorsEffective {
if sepSeen[sep] {
addDomainErrorf("duplicate localpart catchall separator %q", sep)
}
sepSeen[sep] = true
}
for _, sign := range domain.DKIM.Sign {
if _, ok := domain.DKIM.Selectors[sign]; !ok {
addDomainErrorf("unknown selector %s for signing", sign)
@ -1434,7 +1449,7 @@ func prepareDynamicConfig(ctx context.Context, log mlog.Log, dynamicPath string,
dom, ok := c.Domains[a.Domain.Name()]
if !ok {
addAccountErrorf("unknown domain in fromid login address %q", s)
} else if dom.LocalpartCatchallSeparator == "" {
} else if len(dom.LocalpartCatchallSeparatorsEffective) == 0 {
addAccountErrorf("localpart catchall separator not configured for domain for fromid login address %q", s)
}
acc.ParsedFromIDLoginAddresses[i] = a
@ -1660,9 +1675,14 @@ func prepareDynamicConfig(ctx context.Context, log mlog.Log, dynamicPath string,
dc := c.Domains[address.Domain.Name()]
domainHasAddress[address.Domain.Name()] = true
lp := CanonicalLocalpart(address.Localpart, dc)
if dc.LocalpartCatchallSeparator != "" && strings.Contains(string(address.Localpart), dc.LocalpartCatchallSeparator) {
addDestErrorf("localpart of address %s includes domain catchall separator %s", address, dc.LocalpartCatchallSeparator)
} else {
var hasSep bool
for _, sep := range dc.LocalpartCatchallSeparatorsEffective {
if strings.Contains(string(address.Localpart), sep) {
hasSep = true
addDestErrorf("localpart of address %s includes domain catchall separator %s", address, sep)
}
}
if !hasSep {
address.Localpart = lp
}
addrFull := address.Pack(true)
@ -1817,10 +1837,17 @@ func prepareDynamicConfig(ctx context.Context, log mlog.Log, dynamicPath string,
if err != nil {
addAliasErrorf("parsing alias: %v", err)
continue
} else if domain.LocalpartCatchallSeparator != "" && strings.Contains(string(lp), domain.LocalpartCatchallSeparator) {
addAliasErrorf("alias contains localpart catchall separator")
continue
} else {
var hasSep bool
for _, sep := range domain.LocalpartCatchallSeparatorsEffective {
if strings.Contains(string(lp), sep) {
addAliasErrorf("alias contains localpart catchall separator")
hasSep = true
}
}
if hasSep {
continue
}
clp = CanonicalLocalpart(lp, domain)
}

View File

@ -86,10 +86,10 @@ func LookupAddress(localpart smtp.Localpart, domain dns.Domain, allowPostmaster,
}
// CanonicalLocalpart returns the canonical localpart, removing optional catchall
// separator, and optionally lower-casing the string.
// separators, and optionally lower-casing the string.
func CanonicalLocalpart(localpart smtp.Localpart, d config.Domain) smtp.Localpart {
if d.LocalpartCatchallSeparator != "" {
t := strings.SplitN(string(localpart), d.LocalpartCatchallSeparator, 2)
for _, sep := range d.LocalpartCatchallSeparatorsEffective {
t := strings.SplitN(string(localpart), sep, 2)
localpart = smtp.Localpart(t[0])
}

View File

@ -650,8 +650,8 @@ func Incoming(ctx context.Context, log mlog.Log, acc *store.Account, messageID s
log.Debugx("parsing recipient domain in incoming message", err)
} else {
domconf, _ := mox.Conf.Domain(dom)
if domconf.LocalpartCatchallSeparator != "" {
t := strings.SplitN(string(m.RcptToLocalpart), domconf.LocalpartCatchallSeparator, 2)
if len(domconf.LocalpartCatchallSeparatorsEffective) > 0 {
t := strings.SplitN(string(m.RcptToLocalpart), domconf.LocalpartCatchallSeparatorsEffective[0], 2)
if len(t) == 2 {
fromID = t[1]
}

View File

@ -2469,7 +2469,7 @@ func (c *conn) submit(ctx context.Context, recvHdrFor func(string) string, msgWr
var genFromID bool
if useFromID {
// With submission, user can bring their own fromid.
t := strings.SplitN(string(c.mailFrom.Localpart), confDom.LocalpartCatchallSeparator, 2)
t := strings.SplitN(string(c.mailFrom.Localpart), confDom.LocalpartCatchallSeparatorsEffective[0], 2)
localpartBase = t[0]
if len(t) == 2 {
fromID = t[1]
@ -2500,7 +2500,7 @@ func (c *conn) submit(ctx context.Context, recvHdrFor func(string) string, msgWr
if genFromID {
fromID = xrandomID(16)
}
fp.Localpart = smtp.Localpart(localpartBase + confDom.LocalpartCatchallSeparator + fromID)
fp.Localpart = smtp.Localpart(localpartBase + confDom.LocalpartCatchallSeparatorsEffective[0] + fromID)
}
// For multiple recipients, we don't make each message prefix unique, leaving out

View File

@ -1494,12 +1494,13 @@ func TestCatchall(t *testing.T) {
testDeliver("mjl@mox.example", nil) // Exact match.
testDeliver("mjl+test@mox.example", nil) // Domain localpart catchall separator.
testDeliver("MJL+TEST@mox.example", nil) // Again, and case insensitive.
testDeliver("unknown@mox.example", nil) // Catchall address, to account catchall.
n, err := bstore.QueryDB[store.Message](ctxbg, ts.acc.DB).Count()
tcheck(t, err, "checking delivered messages")
tcompare(t, n, 3)
testDeliver("unknown@mox.example", nil) // Catchall address, to account catchall.
acc, err := store.OpenAccount(pkglog, "catchall", false)
tcheck(t, err, "open account")
defer func() {
@ -1509,6 +1510,13 @@ func TestCatchall(t *testing.T) {
n, err = bstore.QueryDB[store.Message](ctxbg, acc.DB).Count()
tcheck(t, err, "checking delivered messages to catchall account")
tcompare(t, n, 1)
testDeliver("mjl-test@mox2.example", nil) // Second catchall separator.
testDeliver("mjl-test+test@mox2.example", nil) // Silly, both separators in address.
testDeliver("mjl+test-test@mox2.example", nil)
n, err = bstore.QueryDB[store.Message](ctxbg, ts.acc.DB).Count()
tcheck(t, err, "checking delivered messages")
tcompare(t, n, 6)
}
// Test DKIM signing for outgoing messages.

View File

@ -1,11 +1,16 @@
Domains:
mox.example:
LocalpartCatchallSeparator: +
mox2.example:
LocalpartCatchallSeparators:
- +
- -
Accounts:
mjl:
Domain: mox.example
Destinations:
mjl@mox.example: nil
mjl@mox2.example: nil
catchall:
Domain: mox.example
Destinations:

View File

@ -2,6 +2,9 @@ Domains:
disabled.example:
Disabled: true
mox.example:
LocalpartCatchallSeparators:
- +
- -
DKIM:
Selectors:
testsel:

View File

@ -2527,9 +2527,19 @@ func (Admin) DomainClientSettingsDomainSave(ctx context.Context, domainName, cli
// DomainLocalpartConfigSave saves the localpart catchall and case-sensitive
// settings for a domain.
func (Admin) DomainLocalpartConfigSave(ctx context.Context, domainName, localpartCatchallSeparator string, localpartCaseSensitive bool) {
func (Admin) DomainLocalpartConfigSave(ctx context.Context, domainName string, localpartCatchallSeparators []string, localpartCaseSensitive bool) {
err := admin.DomainSave(ctx, domainName, func(domain *config.Domain) error {
domain.LocalpartCatchallSeparator = localpartCatchallSeparator
domain.LocalpartCatchallSeparatorsEffective = localpartCatchallSeparators
// If there is a single separator, we prefer the non-list form, it's easier to
// read/edit and should suffice for most setups.
domain.LocalpartCatchallSeparator = ""
domain.LocalpartCatchallSeparators = nil
if len(localpartCatchallSeparators) == 1 {
domain.LocalpartCatchallSeparator = localpartCatchallSeparators[0]
} else {
domain.LocalpartCatchallSeparators = localpartCatchallSeparators
}
domain.LocalpartCaseSensitive = localpartCaseSensitive
return nil
})

View File

@ -298,7 +298,7 @@ var api;
"AutoconfCheckResult": { "Name": "AutoconfCheckResult", "Docs": "", "Fields": [{ "Name": "ClientSettingsDomainIPs", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "IPs", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "Errors", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "Warnings", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "Instructions", "Docs": "", "Typewords": ["[]", "string"] }] },
"AutodiscoverCheckResult": { "Name": "AutodiscoverCheckResult", "Docs": "", "Fields": [{ "Name": "Records", "Docs": "", "Typewords": ["[]", "AutodiscoverSRV"] }, { "Name": "Errors", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "Warnings", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "Instructions", "Docs": "", "Typewords": ["[]", "string"] }] },
"AutodiscoverSRV": { "Name": "AutodiscoverSRV", "Docs": "", "Fields": [{ "Name": "Target", "Docs": "", "Typewords": ["string"] }, { "Name": "Port", "Docs": "", "Typewords": ["uint16"] }, { "Name": "Priority", "Docs": "", "Typewords": ["uint16"] }, { "Name": "Weight", "Docs": "", "Typewords": ["uint16"] }, { "Name": "IPs", "Docs": "", "Typewords": ["[]", "string"] }] },
"ConfigDomain": { "Name": "ConfigDomain", "Docs": "", "Fields": [{ "Name": "Disabled", "Docs": "", "Typewords": ["bool"] }, { "Name": "Description", "Docs": "", "Typewords": ["string"] }, { "Name": "ClientSettingsDomain", "Docs": "", "Typewords": ["string"] }, { "Name": "LocalpartCatchallSeparator", "Docs": "", "Typewords": ["string"] }, { "Name": "LocalpartCaseSensitive", "Docs": "", "Typewords": ["bool"] }, { "Name": "DKIM", "Docs": "", "Typewords": ["DKIM"] }, { "Name": "DMARC", "Docs": "", "Typewords": ["nullable", "DMARC"] }, { "Name": "MTASTS", "Docs": "", "Typewords": ["nullable", "MTASTS"] }, { "Name": "TLSRPT", "Docs": "", "Typewords": ["nullable", "TLSRPT"] }, { "Name": "Routes", "Docs": "", "Typewords": ["[]", "Route"] }, { "Name": "Aliases", "Docs": "", "Typewords": ["{}", "Alias"] }, { "Name": "Domain", "Docs": "", "Typewords": ["Domain"] }] },
"ConfigDomain": { "Name": "ConfigDomain", "Docs": "", "Fields": [{ "Name": "Disabled", "Docs": "", "Typewords": ["bool"] }, { "Name": "Description", "Docs": "", "Typewords": ["string"] }, { "Name": "ClientSettingsDomain", "Docs": "", "Typewords": ["string"] }, { "Name": "LocalpartCatchallSeparator", "Docs": "", "Typewords": ["string"] }, { "Name": "LocalpartCatchallSeparators", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "LocalpartCaseSensitive", "Docs": "", "Typewords": ["bool"] }, { "Name": "DKIM", "Docs": "", "Typewords": ["DKIM"] }, { "Name": "DMARC", "Docs": "", "Typewords": ["nullable", "DMARC"] }, { "Name": "MTASTS", "Docs": "", "Typewords": ["nullable", "MTASTS"] }, { "Name": "TLSRPT", "Docs": "", "Typewords": ["nullable", "TLSRPT"] }, { "Name": "Routes", "Docs": "", "Typewords": ["[]", "Route"] }, { "Name": "Aliases", "Docs": "", "Typewords": ["{}", "Alias"] }, { "Name": "Domain", "Docs": "", "Typewords": ["Domain"] }, { "Name": "LocalpartCatchallSeparatorsEffective", "Docs": "", "Typewords": ["[]", "string"] }] },
"DKIM": { "Name": "DKIM", "Docs": "", "Fields": [{ "Name": "Selectors", "Docs": "", "Typewords": ["{}", "Selector"] }, { "Name": "Sign", "Docs": "", "Typewords": ["[]", "string"] }] },
"Selector": { "Name": "Selector", "Docs": "", "Fields": [{ "Name": "Hash", "Docs": "", "Typewords": ["string"] }, { "Name": "HashEffective", "Docs": "", "Typewords": ["string"] }, { "Name": "Canonicalization", "Docs": "", "Typewords": ["Canonicalization"] }, { "Name": "Headers", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "HeadersEffective", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "DontSealHeaders", "Docs": "", "Typewords": ["bool"] }, { "Name": "Expiration", "Docs": "", "Typewords": ["string"] }, { "Name": "PrivateKeyFile", "Docs": "", "Typewords": ["string"] }, { "Name": "Algorithm", "Docs": "", "Typewords": ["string"] }] },
"Canonicalization": { "Name": "Canonicalization", "Docs": "", "Fields": [{ "Name": "HeaderRelaxed", "Docs": "", "Typewords": ["bool"] }, { "Name": "BodyRelaxed", "Docs": "", "Typewords": ["bool"] }] },
@ -1220,11 +1220,11 @@ var api;
}
// DomainLocalpartConfigSave saves the localpart catchall and case-sensitive
// settings for a domain.
async DomainLocalpartConfigSave(domainName, localpartCatchallSeparator, localpartCaseSensitive) {
async DomainLocalpartConfigSave(domainName, localpartCatchallSeparators, localpartCaseSensitive) {
const fn = "DomainLocalpartConfigSave";
const paramTypes = [["string"], ["string"], ["bool"]];
const paramTypes = [["string"], ["[]", "string"], ["bool"]];
const returnTypes = [];
const params = [domainName, localpartCatchallSeparator, localpartCaseSensitive];
const params = [domainName, localpartCatchallSeparators, localpartCaseSensitive];
return await _sherpaCall(this.baseURL, this.authState, { ...this.options }, paramTypes, returnTypes, fn, params);
}
// DomainDMARCAddressSave saves the DMARC reporting address/processing
@ -2372,7 +2372,6 @@ const domain = async (d) => {
let clientSettingsDomainFieldset;
let clientSettingsDomain;
let localpartFieldset;
let localpartCatchallSeparator;
let localpartCaseSensitive;
let dmarcFieldset;
let dmarcLocalpart;
@ -2469,11 +2468,39 @@ const domain = async (d) => {
e.preventDefault();
e.stopPropagation();
await check(clientSettingsDomainFieldset, client.DomainClientSettingsDomainSave(d, clientSettingsDomain.value));
}, clientSettingsDomainFieldset = dom.fieldset(style({ display: 'flex', gap: '1em' }), dom.label(attr.title('Hostname for client settings instead of the mail server hostname. E.g. mail.<domain>. For future migration to another mail operator without requiring all clients to update their settings, it is convenient to have client settings that reference a subdomain of the hosted domain instead of the hostname of the server where the mail is currently hosted. If empty, the hostname of the mail server is used for client configurations. Unicode name.'), dom.div('Client settings domain'), clientSettingsDomain = dom.input(attr.value(domainConfig.ClientSettingsDomain), style({ width: '30em' }))), dom.div(dom.span('\u00a0'), dom.div(dom.submitbutton('Save'))))), dom.form(style({ marginTop: '1ex' }), async function submit(e) {
}, clientSettingsDomainFieldset = dom.fieldset(style({ display: 'flex', gap: '1em' }), dom.label(attr.title('Hostname for client settings instead of the mail server hostname. E.g. mail.<domain>. For future migration to another mail operator without requiring all clients to update their settings, it is convenient to have client settings that reference a subdomain of the hosted domain instead of the hostname of the server where the mail is currently hosted. If empty, the hostname of the mail server is used for client configurations. Unicode name.'), dom.div('Client settings domain'), clientSettingsDomain = dom.input(attr.value(domainConfig.ClientSettingsDomain), style({ width: '30em' }))), dom.div(dom.span('\u00a0'), dom.div(dom.submitbutton('Save'))))), (() => {
let separatorViews = [];
let separatorsBox;
const addSeparatorView = (s) => {
const separator = dom.input(attr.required(''), attr.value(s), style({ width: '2em' }));
const v = {
separator: separator,
root: dom.div(separator, ' ', dom.clickbutton('Remove', function click() {
separatorViews.splice(separatorViews.indexOf(v), 1);
v.root.remove();
if (separatorViews.length === 0) {
separatorsBox.append(dom.div('(None)'));
}
})),
};
if (separatorViews.length === 0) {
dom._kids(separatorsBox);
}
separatorViews.push(v);
separatorsBox.appendChild(v.root);
};
const elem = dom.form(style({ marginTop: '1ex' }), async function submit(e) {
e.preventDefault();
e.stopPropagation();
await check(localpartFieldset, client.DomainLocalpartConfigSave(d, localpartCatchallSeparator.value, localpartCaseSensitive.checked));
}, localpartFieldset = dom.fieldset(style({ display: 'flex', gap: '1em' }), dom.label(attr.title('If set, upper/lower case is relevant for email delivery.'), dom.div('Localpart case sensitive'), localpartCaseSensitive = dom.input(attr.type('checkbox'), domainConfig.LocalpartCaseSensitive ? attr.checked('') : [])), dom.label(attr.title('If not empty, only the string before the separator is used to for email delivery decisions. For example, if set to \"+\", you+anything@example.com will be delivered to you@example.com.'), dom.div('Localpart catchall separator'), localpartCatchallSeparator = dom.input(attr.value(domainConfig.LocalpartCatchallSeparator))), dom.div(dom.span('\u00a0'), dom.div(dom.submitbutton('Save'))))), dom.br(), dom.h2('DMARC reporting address'), dom.form(style({ marginTop: '1ex' }), async function submit(e) {
await check(localpartFieldset, client.DomainLocalpartConfigSave(d, separatorViews.map(v => v.separator.value), localpartCaseSensitive.checked));
}, localpartFieldset = dom.fieldset(style({ display: 'flex', gap: '1em' }), dom.label(attr.title('If set, upper/lower case is relevant for email delivery.'), dom.div('Localpart case sensitive'), localpartCaseSensitive = dom.input(attr.type('checkbox'), domainConfig.LocalpartCaseSensitive ? attr.checked('') : [])), dom.div(dom.label(attr.title('If not empty, only the string before the separator is used for email delivery decisions. For example, if set to \"+\", you+anything@example.com will be delivered to you@example.com.'), 'Localpart catchall separators'), ' ', dom.clickbutton('Add', function click() {
addSeparatorView('');
}), separatorsBox = dom.div(style({ display: 'flex', flexDirection: 'column', gap: '.25em' }), dom.div('(None)'))), dom.div(dom.span('\u00a0'), dom.div(dom.submitbutton('Save')))));
for (const sep of (domainConfig.LocalpartCatchallSeparatorsEffective || [])) {
addSeparatorView(sep);
}
return elem;
})(), dom.br(), dom.h2('DMARC reporting address'), dom.form(style({ marginTop: '1ex' }), async function submit(e) {
e.preventDefault();
e.stopPropagation();
if (!dmarcLocalpart.value) {

View File

@ -1243,7 +1243,6 @@ const domain = async (d: string) => {
let clientSettingsDomain: HTMLInputElement
let localpartFieldset: HTMLFieldSetElement
let localpartCatchallSeparator: HTMLInputElement
let localpartCaseSensitive: HTMLInputElement
let dmarcFieldset: HTMLFieldSetElement
@ -1588,12 +1587,42 @@ const domain = async (d: string) => {
dom.div(dom.span('\u00a0'), dom.div(dom.submitbutton('Save'))),
),
),
dom.form(
(() => {
interface SeparatorView {
root: HTMLElement
separator: HTMLInputElement
}
let separatorViews: SeparatorView[] = []
let separatorsBox: HTMLDivElement
const addSeparatorView = (s: string) => {
const separator = dom.input(attr.required(''), attr.value(s), style({width: '2em'}))
const v = {
separator: separator,
root: dom.div(
separator, ' ',
dom.clickbutton('Remove', function click() {
separatorViews.splice(separatorViews.indexOf(v), 1)
v.root.remove()
if (separatorViews.length === 0) {
separatorsBox.append(dom.div('(None)'))
}
}),
),
}
if (separatorViews.length === 0) {
dom._kids(separatorsBox)
}
separatorViews.push(v)
separatorsBox.appendChild(v.root)
}
const elem = dom.form(
style({marginTop: '1ex'}),
async function submit(e: SubmitEvent) {
e.preventDefault()
e.stopPropagation()
await check(localpartFieldset, client.DomainLocalpartConfigSave(d, localpartCatchallSeparator.value, localpartCaseSensitive.checked))
await check(localpartFieldset, client.DomainLocalpartConfigSave(d, separatorViews.map(v => v.separator.value), localpartCaseSensitive.checked))
},
localpartFieldset=dom.fieldset(
style({display: 'flex', gap: '1em'}),
@ -1602,14 +1631,24 @@ const domain = async (d: string) => {
dom.div('Localpart case sensitive'),
localpartCaseSensitive=dom.input(attr.type('checkbox'), domainConfig.LocalpartCaseSensitive ? attr.checked('') : []),
),
dom.div(
dom.label(
attr.title('If not empty, only the string before the separator is used to for email delivery decisions. For example, if set to \"+\", you+anything@example.com will be delivered to you@example.com.'),
dom.div('Localpart catchall separator'),
localpartCatchallSeparator=dom.input(attr.value(domainConfig.LocalpartCatchallSeparator)),
attr.title('If not empty, only the string before the separator is used for email delivery decisions. For example, if set to \"+\", you+anything@example.com will be delivered to you@example.com.'),
'Localpart catchall separators',
), ' ',
dom.clickbutton('Add', function click() {
addSeparatorView('')
}),
separatorsBox=dom.div(style({display: 'flex', flexDirection: 'column', gap: '.25em'}), dom.div('(None)')),
),
dom.div(dom.span('\u00a0'), dom.div(dom.submitbutton('Save'))),
),
),
)
for (const sep of (domainConfig.LocalpartCatchallSeparatorsEffective || [])) {
addSeparatorView(sep)
}
return elem
})(),
dom.br(),
dom.h2('DMARC reporting address'),

View File

@ -277,9 +277,9 @@ func TestAdmin(t *testing.T) {
tneedErrorCode(t, "user:error", func() { api.DomainClientSettingsDomainSave(ctxbg, "bogus.example", "unknown.example") })
api.DomainClientSettingsDomainSave(ctxbg, "mox.example", "") // Restore.
api.DomainLocalpartConfigSave(ctxbg, "mox.example", "-", true)
tneedErrorCode(t, "user:error", func() { api.DomainLocalpartConfigSave(ctxbg, "bogus.example", "", false) })
api.DomainLocalpartConfigSave(ctxbg, "mox.example", "", false) // Restore.
api.DomainLocalpartConfigSave(ctxbg, "mox.example", []string{"-"}, true)
tneedErrorCode(t, "user:error", func() { api.DomainLocalpartConfigSave(ctxbg, "bogus.example", nil, false) })
api.DomainLocalpartConfigSave(ctxbg, "mox.example", nil, false) // Restore.
api.DomainDMARCAddressSave(ctxbg, "mox.example", "dmarc-reports", "", "mjl", "DMARC")
tneedErrorCode(t, "user:error", func() { api.DomainDMARCAddressSave(ctxbg, "bogus.example", "dmarc-reports", "", "mjl", "DMARC") })

View File

@ -1710,8 +1710,9 @@
]
},
{
"Name": "localpartCatchallSeparator",
"Name": "localpartCatchallSeparators",
"Typewords": [
"[]",
"string"
]
},
@ -3441,6 +3442,14 @@
"string"
]
},
{
"Name": "LocalpartCatchallSeparators",
"Docs": "",
"Typewords": [
"[]",
"string"
]
},
{
"Name": "LocalpartCaseSensitive",
"Docs": "",
@ -3501,6 +3510,14 @@
"Typewords": [
"Domain"
]
},
{
"Name": "LocalpartCatchallSeparatorsEffective",
"Docs": "Either LocalpartCatchallSeparators, the value of LocalpartCatchallSeparator, or empty.",
"Typewords": [
"[]",
"string"
]
}
]
},

View File

@ -270,6 +270,7 @@ export interface ConfigDomain {
Description: string
ClientSettingsDomain: string
LocalpartCatchallSeparator: string
LocalpartCatchallSeparators?: string[] | null
LocalpartCaseSensitive: boolean
DKIM: DKIM
DMARC?: DMARC | null
@ -278,6 +279,7 @@ export interface ConfigDomain {
Routes?: Route[] | null
Aliases?: { [key: string]: Alias }
Domain: Domain
LocalpartCatchallSeparatorsEffective?: string[] | null // Either LocalpartCatchallSeparators, the value of LocalpartCatchallSeparator, or empty.
}
export interface DKIM {
@ -1184,7 +1186,7 @@ export const types: TypenameMap = {
"AutoconfCheckResult": {"Name":"AutoconfCheckResult","Docs":"","Fields":[{"Name":"ClientSettingsDomainIPs","Docs":"","Typewords":["[]","string"]},{"Name":"IPs","Docs":"","Typewords":["[]","string"]},{"Name":"Errors","Docs":"","Typewords":["[]","string"]},{"Name":"Warnings","Docs":"","Typewords":["[]","string"]},{"Name":"Instructions","Docs":"","Typewords":["[]","string"]}]},
"AutodiscoverCheckResult": {"Name":"AutodiscoverCheckResult","Docs":"","Fields":[{"Name":"Records","Docs":"","Typewords":["[]","AutodiscoverSRV"]},{"Name":"Errors","Docs":"","Typewords":["[]","string"]},{"Name":"Warnings","Docs":"","Typewords":["[]","string"]},{"Name":"Instructions","Docs":"","Typewords":["[]","string"]}]},
"AutodiscoverSRV": {"Name":"AutodiscoverSRV","Docs":"","Fields":[{"Name":"Target","Docs":"","Typewords":["string"]},{"Name":"Port","Docs":"","Typewords":["uint16"]},{"Name":"Priority","Docs":"","Typewords":["uint16"]},{"Name":"Weight","Docs":"","Typewords":["uint16"]},{"Name":"IPs","Docs":"","Typewords":["[]","string"]}]},
"ConfigDomain": {"Name":"ConfigDomain","Docs":"","Fields":[{"Name":"Disabled","Docs":"","Typewords":["bool"]},{"Name":"Description","Docs":"","Typewords":["string"]},{"Name":"ClientSettingsDomain","Docs":"","Typewords":["string"]},{"Name":"LocalpartCatchallSeparator","Docs":"","Typewords":["string"]},{"Name":"LocalpartCaseSensitive","Docs":"","Typewords":["bool"]},{"Name":"DKIM","Docs":"","Typewords":["DKIM"]},{"Name":"DMARC","Docs":"","Typewords":["nullable","DMARC"]},{"Name":"MTASTS","Docs":"","Typewords":["nullable","MTASTS"]},{"Name":"TLSRPT","Docs":"","Typewords":["nullable","TLSRPT"]},{"Name":"Routes","Docs":"","Typewords":["[]","Route"]},{"Name":"Aliases","Docs":"","Typewords":["{}","Alias"]},{"Name":"Domain","Docs":"","Typewords":["Domain"]}]},
"ConfigDomain": {"Name":"ConfigDomain","Docs":"","Fields":[{"Name":"Disabled","Docs":"","Typewords":["bool"]},{"Name":"Description","Docs":"","Typewords":["string"]},{"Name":"ClientSettingsDomain","Docs":"","Typewords":["string"]},{"Name":"LocalpartCatchallSeparator","Docs":"","Typewords":["string"]},{"Name":"LocalpartCatchallSeparators","Docs":"","Typewords":["[]","string"]},{"Name":"LocalpartCaseSensitive","Docs":"","Typewords":["bool"]},{"Name":"DKIM","Docs":"","Typewords":["DKIM"]},{"Name":"DMARC","Docs":"","Typewords":["nullable","DMARC"]},{"Name":"MTASTS","Docs":"","Typewords":["nullable","MTASTS"]},{"Name":"TLSRPT","Docs":"","Typewords":["nullable","TLSRPT"]},{"Name":"Routes","Docs":"","Typewords":["[]","Route"]},{"Name":"Aliases","Docs":"","Typewords":["{}","Alias"]},{"Name":"Domain","Docs":"","Typewords":["Domain"]},{"Name":"LocalpartCatchallSeparatorsEffective","Docs":"","Typewords":["[]","string"]}]},
"DKIM": {"Name":"DKIM","Docs":"","Fields":[{"Name":"Selectors","Docs":"","Typewords":["{}","Selector"]},{"Name":"Sign","Docs":"","Typewords":["[]","string"]}]},
"Selector": {"Name":"Selector","Docs":"","Fields":[{"Name":"Hash","Docs":"","Typewords":["string"]},{"Name":"HashEffective","Docs":"","Typewords":["string"]},{"Name":"Canonicalization","Docs":"","Typewords":["Canonicalization"]},{"Name":"Headers","Docs":"","Typewords":["[]","string"]},{"Name":"HeadersEffective","Docs":"","Typewords":["[]","string"]},{"Name":"DontSealHeaders","Docs":"","Typewords":["bool"]},{"Name":"Expiration","Docs":"","Typewords":["string"]},{"Name":"PrivateKeyFile","Docs":"","Typewords":["string"]},{"Name":"Algorithm","Docs":"","Typewords":["string"]}]},
"Canonicalization": {"Name":"Canonicalization","Docs":"","Fields":[{"Name":"HeaderRelaxed","Docs":"","Typewords":["bool"]},{"Name":"BodyRelaxed","Docs":"","Typewords":["bool"]}]},
@ -2194,11 +2196,11 @@ export class Client {
// DomainLocalpartConfigSave saves the localpart catchall and case-sensitive
// settings for a domain.
async DomainLocalpartConfigSave(domainName: string, localpartCatchallSeparator: string, localpartCaseSensitive: boolean): Promise<void> {
async DomainLocalpartConfigSave(domainName: string, localpartCatchallSeparators: string[] | null, localpartCaseSensitive: boolean): Promise<void> {
const fn: string = "DomainLocalpartConfigSave"
const paramTypes: string[][] = [["string"],["string"],["bool"]]
const paramTypes: string[][] = [["string"],["[]","string"],["bool"]]
const returnTypes: string[][] = []
const params: any[] = [domainName, localpartCatchallSeparator, localpartCaseSensitive]
const params: any[] = [domainName, localpartCatchallSeparators, localpartCaseSensitive]
return await _sherpaCall(this.baseURL, this.authState, { ...this.options }, paramTypes, returnTypes, fn, params) as void
}

View File

@ -1007,10 +1007,7 @@ func (s server) Send(ctx context.Context, req webapi.SendRequest) (resp webapi.S
useFromID := slices.Contains(accConf.ParsedFromIDLoginAddresses, loginAddr)
var localpartBase string
if useFromID {
if confDom.LocalpartCatchallSeparator == "" {
xcheckuserf(errors.New(`localpart catchall separator must be configured for domain`), `composing unique "from" address`)
}
localpartBase = strings.SplitN(string(fromPath.Localpart), confDom.LocalpartCatchallSeparator, 2)[0]
localpartBase = strings.SplitN(string(fromPath.Localpart), confDom.LocalpartCatchallSeparatorsEffective[0], 2)[0]
}
fromIDs := make([]string, len(recipients))
qml := make([]queue.Msg, len(recipients))
@ -1019,7 +1016,7 @@ func (s server) Send(ctx context.Context, req webapi.SendRequest) (resp webapi.S
fp := fromPath
if useFromID {
fromIDs[i] = xrandomID(16)
fp.Localpart = smtp.Localpart(localpartBase + confDom.LocalpartCatchallSeparator + fromIDs[i])
fp.Localpart = smtp.Localpart(localpartBase + confDom.LocalpartCatchallSeparatorsEffective[0] + fromIDs[i])
}
// Don't use per-recipient unique message prefix when multiple recipients are

View File

@ -956,7 +956,7 @@ func (w Webmail) MessageSubmit(ctx context.Context, m SubmitMessage) {
fromPath := fromAddr.Address.Path()
var localpartBase string
if useFromID {
localpartBase = strings.SplitN(string(fromPath.Localpart), confDom.LocalpartCatchallSeparator, 2)[0]
localpartBase = strings.SplitN(string(fromPath.Localpart), confDom.LocalpartCatchallSeparatorsEffective[0], 2)[0]
}
qml := make([]queue.Msg, len(recipients))
now := time.Now()
@ -965,7 +965,7 @@ func (w Webmail) MessageSubmit(ctx context.Context, m SubmitMessage) {
var fromID string
if useFromID {
fromID = xrandomID(ctx, 16)
fp.Localpart = smtp.Localpart(localpartBase + confDom.LocalpartCatchallSeparator + fromID)
fp.Localpart = smtp.Localpart(localpartBase + confDom.LocalpartCatchallSeparatorsEffective[0] + fromID)
}
// Don't use per-recipient unique message prefix when multiple recipients are

View File

@ -2002,9 +2002,10 @@
"Docs": "DomainAddressConfig has the address (localpart) configuration for a domain, so\nthe webmail client can decide if an address matches the addresses of the\naccount.",
"Fields": [
{
"Name": "LocalpartCatchallSeparator",
"Name": "LocalpartCatchallSeparators",
"Docs": "Can be empty.",
"Typewords": [
"[]",
"string"
]
},

View File

@ -264,7 +264,7 @@ export interface EventStart {
// the webmail client can decide if an address matches the addresses of the
// account.
export interface DomainAddressConfig {
LocalpartCatchallSeparator: string // Can be empty.
LocalpartCatchallSeparators?: string[] | null // Can be empty.
LocalpartCaseSensitive: boolean
}
@ -621,7 +621,7 @@ export const types: TypenameMap = {
"Settings": {"Name":"Settings","Docs":"","Fields":[{"Name":"ID","Docs":"","Typewords":["uint8"]},{"Name":"Signature","Docs":"","Typewords":["string"]},{"Name":"Quoting","Docs":"","Typewords":["Quoting"]},{"Name":"ShowAddressSecurity","Docs":"","Typewords":["bool"]},{"Name":"ShowHTML","Docs":"","Typewords":["bool"]},{"Name":"NoShowShortcuts","Docs":"","Typewords":["bool"]},{"Name":"ShowHeaders","Docs":"","Typewords":["[]","string"]}]},
"Ruleset": {"Name":"Ruleset","Docs":"","Fields":[{"Name":"SMTPMailFromRegexp","Docs":"","Typewords":["string"]},{"Name":"MsgFromRegexp","Docs":"","Typewords":["string"]},{"Name":"VerifiedDomain","Docs":"","Typewords":["string"]},{"Name":"HeadersRegexp","Docs":"","Typewords":["{}","string"]},{"Name":"IsForward","Docs":"","Typewords":["bool"]},{"Name":"ListAllowDomain","Docs":"","Typewords":["string"]},{"Name":"AcceptRejectsToMailbox","Docs":"","Typewords":["string"]},{"Name":"Mailbox","Docs":"","Typewords":["string"]},{"Name":"Comment","Docs":"","Typewords":["string"]},{"Name":"VerifiedDNSDomain","Docs":"","Typewords":["Domain"]},{"Name":"ListAllowDNSDomain","Docs":"","Typewords":["Domain"]}]},
"EventStart": {"Name":"EventStart","Docs":"","Fields":[{"Name":"SSEID","Docs":"","Typewords":["int64"]},{"Name":"LoginAddress","Docs":"","Typewords":["MessageAddress"]},{"Name":"Addresses","Docs":"","Typewords":["[]","MessageAddress"]},{"Name":"DomainAddressConfigs","Docs":"","Typewords":["{}","DomainAddressConfig"]},{"Name":"MailboxName","Docs":"","Typewords":["string"]},{"Name":"Mailboxes","Docs":"","Typewords":["[]","Mailbox"]},{"Name":"RejectsMailbox","Docs":"","Typewords":["string"]},{"Name":"Settings","Docs":"","Typewords":["Settings"]},{"Name":"AccountPath","Docs":"","Typewords":["string"]},{"Name":"Version","Docs":"","Typewords":["string"]}]},
"DomainAddressConfig": {"Name":"DomainAddressConfig","Docs":"","Fields":[{"Name":"LocalpartCatchallSeparator","Docs":"","Typewords":["string"]},{"Name":"LocalpartCaseSensitive","Docs":"","Typewords":["bool"]}]},
"DomainAddressConfig": {"Name":"DomainAddressConfig","Docs":"","Fields":[{"Name":"LocalpartCatchallSeparators","Docs":"","Typewords":["[]","string"]},{"Name":"LocalpartCaseSensitive","Docs":"","Typewords":["bool"]}]},
"EventViewErr": {"Name":"EventViewErr","Docs":"","Fields":[{"Name":"ViewID","Docs":"","Typewords":["int64"]},{"Name":"RequestID","Docs":"","Typewords":["int64"]},{"Name":"Err","Docs":"","Typewords":["string"]}]},
"EventViewReset": {"Name":"EventViewReset","Docs":"","Fields":[{"Name":"ViewID","Docs":"","Typewords":["int64"]},{"Name":"RequestID","Docs":"","Typewords":["int64"]}]},
"EventViewMsgs": {"Name":"EventViewMsgs","Docs":"","Fields":[{"Name":"ViewID","Docs":"","Typewords":["int64"]},{"Name":"RequestID","Docs":"","Typewords":["int64"]},{"Name":"MessageItems","Docs":"","Typewords":["[]","[]","MessageItem"]},{"Name":"ParsedMessage","Docs":"","Typewords":["nullable","ParsedMessage"]},{"Name":"ViewEnd","Docs":"","Typewords":["bool"]}]},

View File

@ -355,6 +355,18 @@ func TestAPI(t *testing.T) {
})
// todo: check delivery of 6 messages to inbox, 1 to sent
api.MessageSubmit(ctx, SubmitMessage{
From: "mjl-altcatchall@mox.example",
To: []string{"mjl-to@mox.example", "mjl to2 <mjl+to2@mox.example>"},
Cc: []string{"mjl-cc@mox.example", "mjl cc2 <mjl+cc2@mox.example>"},
Bcc: []string{"mjl-bcc@mox.example", "mjl bcc2 <mjl+bcc2@mox.example>"},
Subject: "test email",
TextBody: "this is the content\n\ncheers,\nmox",
ReplyTo: "mjl replyto <mjl-replyto@mox.example>",
UserAgent: "moxwebmail/dev",
})
// todo: check delivery of 6 messages to inbox, 1 to sent
// Reply with attachments.
api.MessageSubmit(ctx, SubmitMessage{
From: "mjl@mox.example",

View File

@ -314,7 +314,7 @@ var api;
"Settings": { "Name": "Settings", "Docs": "", "Fields": [{ "Name": "ID", "Docs": "", "Typewords": ["uint8"] }, { "Name": "Signature", "Docs": "", "Typewords": ["string"] }, { "Name": "Quoting", "Docs": "", "Typewords": ["Quoting"] }, { "Name": "ShowAddressSecurity", "Docs": "", "Typewords": ["bool"] }, { "Name": "ShowHTML", "Docs": "", "Typewords": ["bool"] }, { "Name": "NoShowShortcuts", "Docs": "", "Typewords": ["bool"] }, { "Name": "ShowHeaders", "Docs": "", "Typewords": ["[]", "string"] }] },
"Ruleset": { "Name": "Ruleset", "Docs": "", "Fields": [{ "Name": "SMTPMailFromRegexp", "Docs": "", "Typewords": ["string"] }, { "Name": "MsgFromRegexp", "Docs": "", "Typewords": ["string"] }, { "Name": "VerifiedDomain", "Docs": "", "Typewords": ["string"] }, { "Name": "HeadersRegexp", "Docs": "", "Typewords": ["{}", "string"] }, { "Name": "IsForward", "Docs": "", "Typewords": ["bool"] }, { "Name": "ListAllowDomain", "Docs": "", "Typewords": ["string"] }, { "Name": "AcceptRejectsToMailbox", "Docs": "", "Typewords": ["string"] }, { "Name": "Mailbox", "Docs": "", "Typewords": ["string"] }, { "Name": "Comment", "Docs": "", "Typewords": ["string"] }, { "Name": "VerifiedDNSDomain", "Docs": "", "Typewords": ["Domain"] }, { "Name": "ListAllowDNSDomain", "Docs": "", "Typewords": ["Domain"] }] },
"EventStart": { "Name": "EventStart", "Docs": "", "Fields": [{ "Name": "SSEID", "Docs": "", "Typewords": ["int64"] }, { "Name": "LoginAddress", "Docs": "", "Typewords": ["MessageAddress"] }, { "Name": "Addresses", "Docs": "", "Typewords": ["[]", "MessageAddress"] }, { "Name": "DomainAddressConfigs", "Docs": "", "Typewords": ["{}", "DomainAddressConfig"] }, { "Name": "MailboxName", "Docs": "", "Typewords": ["string"] }, { "Name": "Mailboxes", "Docs": "", "Typewords": ["[]", "Mailbox"] }, { "Name": "RejectsMailbox", "Docs": "", "Typewords": ["string"] }, { "Name": "Settings", "Docs": "", "Typewords": ["Settings"] }, { "Name": "AccountPath", "Docs": "", "Typewords": ["string"] }, { "Name": "Version", "Docs": "", "Typewords": ["string"] }] },
"DomainAddressConfig": { "Name": "DomainAddressConfig", "Docs": "", "Fields": [{ "Name": "LocalpartCatchallSeparator", "Docs": "", "Typewords": ["string"] }, { "Name": "LocalpartCaseSensitive", "Docs": "", "Typewords": ["bool"] }] },
"DomainAddressConfig": { "Name": "DomainAddressConfig", "Docs": "", "Fields": [{ "Name": "LocalpartCatchallSeparators", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "LocalpartCaseSensitive", "Docs": "", "Typewords": ["bool"] }] },
"EventViewErr": { "Name": "EventViewErr", "Docs": "", "Fields": [{ "Name": "ViewID", "Docs": "", "Typewords": ["int64"] }, { "Name": "RequestID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Err", "Docs": "", "Typewords": ["string"] }] },
"EventViewReset": { "Name": "EventViewReset", "Docs": "", "Fields": [{ "Name": "ViewID", "Docs": "", "Typewords": ["int64"] }, { "Name": "RequestID", "Docs": "", "Typewords": ["int64"] }] },
"EventViewMsgs": { "Name": "EventViewMsgs", "Docs": "", "Fields": [{ "Name": "ViewID", "Docs": "", "Typewords": ["int64"] }, { "Name": "RequestID", "Docs": "", "Typewords": ["int64"] }, { "Name": "MessageItems", "Docs": "", "Typewords": ["[]", "[]", "MessageItem"] }, { "Name": "ParsedMessage", "Docs": "", "Typewords": ["nullable", "ParsedMessage"] }, { "Name": "ViewEnd", "Docs": "", "Typewords": ["bool"] }] },

View File

@ -314,7 +314,7 @@ var api;
"Settings": { "Name": "Settings", "Docs": "", "Fields": [{ "Name": "ID", "Docs": "", "Typewords": ["uint8"] }, { "Name": "Signature", "Docs": "", "Typewords": ["string"] }, { "Name": "Quoting", "Docs": "", "Typewords": ["Quoting"] }, { "Name": "ShowAddressSecurity", "Docs": "", "Typewords": ["bool"] }, { "Name": "ShowHTML", "Docs": "", "Typewords": ["bool"] }, { "Name": "NoShowShortcuts", "Docs": "", "Typewords": ["bool"] }, { "Name": "ShowHeaders", "Docs": "", "Typewords": ["[]", "string"] }] },
"Ruleset": { "Name": "Ruleset", "Docs": "", "Fields": [{ "Name": "SMTPMailFromRegexp", "Docs": "", "Typewords": ["string"] }, { "Name": "MsgFromRegexp", "Docs": "", "Typewords": ["string"] }, { "Name": "VerifiedDomain", "Docs": "", "Typewords": ["string"] }, { "Name": "HeadersRegexp", "Docs": "", "Typewords": ["{}", "string"] }, { "Name": "IsForward", "Docs": "", "Typewords": ["bool"] }, { "Name": "ListAllowDomain", "Docs": "", "Typewords": ["string"] }, { "Name": "AcceptRejectsToMailbox", "Docs": "", "Typewords": ["string"] }, { "Name": "Mailbox", "Docs": "", "Typewords": ["string"] }, { "Name": "Comment", "Docs": "", "Typewords": ["string"] }, { "Name": "VerifiedDNSDomain", "Docs": "", "Typewords": ["Domain"] }, { "Name": "ListAllowDNSDomain", "Docs": "", "Typewords": ["Domain"] }] },
"EventStart": { "Name": "EventStart", "Docs": "", "Fields": [{ "Name": "SSEID", "Docs": "", "Typewords": ["int64"] }, { "Name": "LoginAddress", "Docs": "", "Typewords": ["MessageAddress"] }, { "Name": "Addresses", "Docs": "", "Typewords": ["[]", "MessageAddress"] }, { "Name": "DomainAddressConfigs", "Docs": "", "Typewords": ["{}", "DomainAddressConfig"] }, { "Name": "MailboxName", "Docs": "", "Typewords": ["string"] }, { "Name": "Mailboxes", "Docs": "", "Typewords": ["[]", "Mailbox"] }, { "Name": "RejectsMailbox", "Docs": "", "Typewords": ["string"] }, { "Name": "Settings", "Docs": "", "Typewords": ["Settings"] }, { "Name": "AccountPath", "Docs": "", "Typewords": ["string"] }, { "Name": "Version", "Docs": "", "Typewords": ["string"] }] },
"DomainAddressConfig": { "Name": "DomainAddressConfig", "Docs": "", "Fields": [{ "Name": "LocalpartCatchallSeparator", "Docs": "", "Typewords": ["string"] }, { "Name": "LocalpartCaseSensitive", "Docs": "", "Typewords": ["bool"] }] },
"DomainAddressConfig": { "Name": "DomainAddressConfig", "Docs": "", "Fields": [{ "Name": "LocalpartCatchallSeparators", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "LocalpartCaseSensitive", "Docs": "", "Typewords": ["bool"] }] },
"EventViewErr": { "Name": "EventViewErr", "Docs": "", "Fields": [{ "Name": "ViewID", "Docs": "", "Typewords": ["int64"] }, { "Name": "RequestID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Err", "Docs": "", "Typewords": ["string"] }] },
"EventViewReset": { "Name": "EventViewReset", "Docs": "", "Fields": [{ "Name": "ViewID", "Docs": "", "Typewords": ["int64"] }, { "Name": "RequestID", "Docs": "", "Typewords": ["int64"] }] },
"EventViewMsgs": { "Name": "EventViewMsgs", "Docs": "", "Fields": [{ "Name": "ViewID", "Docs": "", "Typewords": ["int64"] }, { "Name": "RequestID", "Docs": "", "Typewords": ["int64"] }, { "Name": "MessageItems", "Docs": "", "Typewords": ["[]", "[]", "MessageItem"] }, { "Name": "ParsedMessage", "Docs": "", "Typewords": ["nullable", "ParsedMessage"] }, { "Name": "ViewEnd", "Docs": "", "Typewords": ["bool"] }] },

View File

@ -228,7 +228,7 @@ type EventStart struct {
// the webmail client can decide if an address matches the addresses of the
// account.
type DomainAddressConfig struct {
LocalpartCatchallSeparator string // Can be empty.
LocalpartCatchallSeparators []string // Can be empty.
LocalpartCaseSensitive bool
}
@ -764,7 +764,7 @@ func serveEvents(ctx context.Context, log mlog.Log, accountPath string, w http.R
domainAddressConfigs := map[string]DomainAddressConfig{}
for _, a := range addresses {
dom, _ := mox.Conf.Domain(a.Domain)
domainAddressConfigs[a.Domain.ASCII] = DomainAddressConfig{dom.LocalpartCatchallSeparator, dom.LocalpartCaseSensitive}
domainAddressConfigs[a.Domain.ASCII] = DomainAddressConfig{dom.LocalpartCatchallSeparatorsEffective, dom.LocalpartCaseSensitive}
}
// Write first event, allowing client to fill its UI with mailboxes.

View File

@ -314,7 +314,7 @@ var api;
"Settings": { "Name": "Settings", "Docs": "", "Fields": [{ "Name": "ID", "Docs": "", "Typewords": ["uint8"] }, { "Name": "Signature", "Docs": "", "Typewords": ["string"] }, { "Name": "Quoting", "Docs": "", "Typewords": ["Quoting"] }, { "Name": "ShowAddressSecurity", "Docs": "", "Typewords": ["bool"] }, { "Name": "ShowHTML", "Docs": "", "Typewords": ["bool"] }, { "Name": "NoShowShortcuts", "Docs": "", "Typewords": ["bool"] }, { "Name": "ShowHeaders", "Docs": "", "Typewords": ["[]", "string"] }] },
"Ruleset": { "Name": "Ruleset", "Docs": "", "Fields": [{ "Name": "SMTPMailFromRegexp", "Docs": "", "Typewords": ["string"] }, { "Name": "MsgFromRegexp", "Docs": "", "Typewords": ["string"] }, { "Name": "VerifiedDomain", "Docs": "", "Typewords": ["string"] }, { "Name": "HeadersRegexp", "Docs": "", "Typewords": ["{}", "string"] }, { "Name": "IsForward", "Docs": "", "Typewords": ["bool"] }, { "Name": "ListAllowDomain", "Docs": "", "Typewords": ["string"] }, { "Name": "AcceptRejectsToMailbox", "Docs": "", "Typewords": ["string"] }, { "Name": "Mailbox", "Docs": "", "Typewords": ["string"] }, { "Name": "Comment", "Docs": "", "Typewords": ["string"] }, { "Name": "VerifiedDNSDomain", "Docs": "", "Typewords": ["Domain"] }, { "Name": "ListAllowDNSDomain", "Docs": "", "Typewords": ["Domain"] }] },
"EventStart": { "Name": "EventStart", "Docs": "", "Fields": [{ "Name": "SSEID", "Docs": "", "Typewords": ["int64"] }, { "Name": "LoginAddress", "Docs": "", "Typewords": ["MessageAddress"] }, { "Name": "Addresses", "Docs": "", "Typewords": ["[]", "MessageAddress"] }, { "Name": "DomainAddressConfigs", "Docs": "", "Typewords": ["{}", "DomainAddressConfig"] }, { "Name": "MailboxName", "Docs": "", "Typewords": ["string"] }, { "Name": "Mailboxes", "Docs": "", "Typewords": ["[]", "Mailbox"] }, { "Name": "RejectsMailbox", "Docs": "", "Typewords": ["string"] }, { "Name": "Settings", "Docs": "", "Typewords": ["Settings"] }, { "Name": "AccountPath", "Docs": "", "Typewords": ["string"] }, { "Name": "Version", "Docs": "", "Typewords": ["string"] }] },
"DomainAddressConfig": { "Name": "DomainAddressConfig", "Docs": "", "Fields": [{ "Name": "LocalpartCatchallSeparator", "Docs": "", "Typewords": ["string"] }, { "Name": "LocalpartCaseSensitive", "Docs": "", "Typewords": ["bool"] }] },
"DomainAddressConfig": { "Name": "DomainAddressConfig", "Docs": "", "Fields": [{ "Name": "LocalpartCatchallSeparators", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "LocalpartCaseSensitive", "Docs": "", "Typewords": ["bool"] }] },
"EventViewErr": { "Name": "EventViewErr", "Docs": "", "Fields": [{ "Name": "ViewID", "Docs": "", "Typewords": ["int64"] }, { "Name": "RequestID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Err", "Docs": "", "Typewords": ["string"] }] },
"EventViewReset": { "Name": "EventViewReset", "Docs": "", "Fields": [{ "Name": "ViewID", "Docs": "", "Typewords": ["int64"] }, { "Name": "RequestID", "Docs": "", "Typewords": ["int64"] }] },
"EventViewMsgs": { "Name": "EventViewMsgs", "Docs": "", "Fields": [{ "Name": "ViewID", "Docs": "", "Typewords": ["int64"] }, { "Name": "RequestID", "Docs": "", "Typewords": ["int64"] }, { "Name": "MessageItems", "Docs": "", "Typewords": ["[]", "[]", "MessageItem"] }, { "Name": "ParsedMessage", "Docs": "", "Typewords": ["nullable", "ParsedMessage"] }, { "Name": "ViewEnd", "Docs": "", "Typewords": ["bool"] }] },
@ -2991,9 +2991,8 @@ const compose = (opts, listMailboxes) => {
const normalizeUser = (a) => {
let user = a.User;
const domconf = domainAddressConfigs[a.Domain.ASCII];
const localpartCatchallSeparator = domconf.LocalpartCatchallSeparator;
if (localpartCatchallSeparator) {
user = user.split(localpartCatchallSeparator)[0];
for (const sep of (domconf.LocalpartCatchallSeparators || [])) {
user = user.split(sep)[0];
}
const localpartCaseSensitive = domconf.LocalpartCaseSensitive;
if (!localpartCaseSensitive) {

View File

@ -1887,9 +1887,8 @@ const compose = (opts: ComposeOptions, listMailboxes: listMailboxes) => {
const normalizeUser = (a: api.MessageAddress) => {
let user = a.User
const domconf = domainAddressConfigs[a.Domain.ASCII]
const localpartCatchallSeparator = domconf.LocalpartCatchallSeparator
if (localpartCatchallSeparator) {
user = user.split(localpartCatchallSeparator)[0]
for (const sep of (domconf.LocalpartCatchallSeparators || [])) {
user = user.split(sep)[0]
}
const localpartCaseSensitive = domconf.LocalpartCaseSensitive
if (!localpartCaseSensitive) {