From 32cf6500bd240f143eb28571a6dc5e8778dbcf07 Mon Sep 17 00:00:00 2001 From: Mechiel Lukkien Date: Sun, 28 Apr 2024 11:44:51 +0200 Subject: [PATCH] when removing an address, remove it as member from aliases unless the address is the last member, then the admin must either remove the alias first, or add new members. we don't want to accidentally remove an alias address. in the admin page for removing addresses, we warn the admin that the address will be removed from any aliases. --- mox-/admin.go | 33 +++++++++++++++++++++++++++++++++ webadmin/admin.js | 6 ++++-- webadmin/admin.ts | 6 ++++-- 3 files changed, 41 insertions(+), 4 deletions(-) diff --git a/mox-/admin.go b/mox-/admin.go index 28b466f..db33420 100644 --- a/mox-/admin.go +++ b/mox-/admin.go @@ -1117,6 +1117,9 @@ func AddressAdd(ctx context.Context, address, account string) (rerr error) { // AddressRemove removes an email address and reloads the configuration. // Address can be a catchall address for the domain of the form "@". +// +// If the address is member of an alias, remove it from from the alias, unless it +// is the last member. func AddressRemove(ctx context.Context, address string) (rerr error) { log := pkglog.WithContext(ctx) defer func() { @@ -1192,12 +1195,42 @@ func AddressRemove(ctx context.Context, address string) (rerr error) { } na.FromIDLoginAddresses = fromIDLoginAddresses + // And remove as member from aliases configured in domains. + domains := maps.Clone(Conf.Dynamic.Domains) + for _, aa := range na.Aliases { + if aa.SubscriptionAddress != address { + continue + } + + aliasAddr := fmt.Sprintf("%s@%s", aa.Alias.LocalpartStr, aa.Alias.Domain.Name()) + + dom, ok := Conf.Dynamic.Domains[aa.Alias.Domain.Name()] + if !ok { + return fmt.Errorf("cannot find domain for alias %s", aliasAddr) + } + a, ok := dom.Aliases[aa.Alias.LocalpartStr] + if !ok { + return fmt.Errorf("cannot find alias %s", aliasAddr) + } + a.Addresses = slices.Clone(a.Addresses) + a.Addresses = slices.DeleteFunc(a.Addresses, func(v string) bool { return v == address }) + if len(a.Addresses) == 0 { + return fmt.Errorf("address is last member of alias %s, add new members or remove alias first", aliasAddr) + } + a.ParsedAddresses = nil // Filled when parsing config. + dom.Aliases = maps.Clone(dom.Aliases) + dom.Aliases[aa.Alias.LocalpartStr] = a + domains[aa.Alias.Domain.Name()] = dom + } + na.Aliases = nil // Filled when parsing config. + nc := Conf.Dynamic nc.Accounts = map[string]config.Account{} for name, a := range Conf.Dynamic.Accounts { nc.Accounts[name] = a } nc.Accounts[ad.Account] = na + nc.Domains = domains if err := writeDynamic(ctx, log, nc); err != nil { return fmt.Errorf("writing domains.conf: %w", err) diff --git a/webadmin/admin.js b/webadmin/admin.js index 273506e..23aa33c 100644 --- a/webadmin/admin.js +++ b/webadmin/admin.js @@ -2253,7 +2253,9 @@ const account = async (name) => { } return dom.tr(dom.td(v), dom.td(dom.clickbutton('Remove', async function click(e) { e.preventDefault(); - if (!window.confirm('Are you sure you want to remove this address?')) { + const aliases = (config.Aliases || []).filter(aa => aa.SubscriptionAddress === k).map(aa => aa.Alias.LocalpartStr + "@" + domainName(aa.Alias.Domain)); + const aliasmsg = aliases.length > 0 ? ' Address will be removed from alias(es): ' + aliases.join(', ') : ''; + if (!window.confirm('Are you sure you want to remove this address?' + aliasmsg)) { return; } await check(e.target, client.AddressRemove(k)); @@ -2441,7 +2443,7 @@ const domain = async (d) => { }; dom._kids(page, crumbs(crumblink('Mox Admin', '#'), 'Domain ' + domainString(dnsdomain)), dom.ul(dom.li(dom.a('Required DNS records', attr.href('#domains/' + d + '/dnsrecords'))), dom.li(dom.a('Check current actual DNS records and domain configuration', attr.href('#domains/' + d + '/dnscheck')))), dom.br(), dom.h2('Client configuration'), dom.p('If autoconfig/autodiscover does not work with an email client, use the settings below for this domain. Authenticate with email address and password. ', dom.span('Explicitly configure', attr.title('To prevent authentication mechanism downgrade attempts that may result in clients sending plain text passwords to a MitM.')), ' the first supported authentication mechanism: SCRAM-SHA-256-PLUS, SCRAM-SHA-1-PLUS, SCRAM-SHA-256, SCRAM-SHA-1, CRAM-MD5.'), dom.table(dom.thead(dom.tr(dom.th('Protocol'), dom.th('Host'), dom.th('Port'), dom.th('Listener'), dom.th('Note'))), dom.tbody((clientConfigs.Entries || []).map(e => dom.tr(dom.td(e.Protocol), dom.td(domainString(e.Host)), dom.td('' + e.Port), dom.td('' + e.Listener), dom.td('' + e.Note))))), dom.br(), dom.h2('DMARC aggregate reports summary'), renderDMARCSummaries(dmarcSummaries || []), dom.br(), dom.h2('TLS reports summary'), renderTLSRPTSummaries(tlsrptSummaries || []), dom.br(), dom.h2('Addresses'), dom.table(dom.thead(dom.tr(dom.th('Address'), dom.th('Account'), dom.th('Action'))), dom.tbody(Object.entries(localpartAccounts).map(t => dom.tr(dom.td(prewrap(t[0]) || '(catchall)'), dom.td(dom.a(t[1], attr.href('#accounts/' + t[1]))), dom.td(dom.clickbutton('Remove', async function click(e) { e.preventDefault(); - if (!window.confirm('Are you sure you want to remove this address?')) { + if (!window.confirm('Are you sure you want to remove this address? If it is a member of an alias, it will be removed from the alias.')) { return; } await check(e.target, client.AddressRemove(t[0] + '@' + d)); diff --git a/webadmin/admin.ts b/webadmin/admin.ts index cc75750..c983d63 100644 --- a/webadmin/admin.ts +++ b/webadmin/admin.ts @@ -837,7 +837,9 @@ const account = async (name: string) => { dom.td( dom.clickbutton('Remove', async function click(e: MouseEvent) { e.preventDefault() - if (!window.confirm('Are you sure you want to remove this address?')) { + const aliases = (config.Aliases || []).filter(aa => aa.SubscriptionAddress === k).map(aa => aa.Alias.LocalpartStr+"@"+domainName(aa.Alias.Domain)) + const aliasmsg = aliases.length > 0 ? ' Address will be removed from alias(es): '+aliases.join(', ') : '' + if (!window.confirm('Are you sure you want to remove this address?'+aliasmsg)) { return } await check(e.target! as HTMLButtonElement, client.AddressRemove(k)) @@ -1273,7 +1275,7 @@ const domain = async (d: string) => { dom.td( dom.clickbutton('Remove', async function click(e: MouseEvent) { e.preventDefault() - if (!window.confirm('Are you sure you want to remove this address?')) { + if (!window.confirm('Are you sure you want to remove this address? If it is a member of an alias, it will be removed from the alias.')) { return } await check(e.target! as HTMLButtonElement, client.AddressRemove(t[0] + '@' + d))