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
26 changed files with 255 additions and 95 deletions

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) {
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) {
}, 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, 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,28 +1587,68 @@ const domain = async (d: string) => {
dom.div(dom.span('\u00a0'), dom.div(dom.submitbutton('Save'))),
),
),
dom.form(
style({marginTop: '1ex'}),
async function submit(e: SubmitEvent) {
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('') : []),
(() => {
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, 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'))),
),
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'))),
),
),
)
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
}