implement a catchall address for a domain

by specifying a "destination" in an account that is just "@" followed by the
domain, e.g. "@example.org". messages are only delivered to the catchall
address when no regular destination matches (taking the per-domain
catchall-separator and case-sensisitivity into account).

for issue #18
This commit is contained in:
Mechiel Lukkien
2023-03-29 21:11:43 +02:00
parent 51ad345dbb
commit b571dd4b28
16 changed files with 176 additions and 58 deletions

View File

@ -273,6 +273,7 @@ const index = async () => {
Object.entries(destinations).sort().map(t =>
dom.li(
dom.a(t[0], attr({href: '#destinations/'+t[0]})),
t[0].startsWith('@') ? ' (catchall)' : [],
),
),
),

View File

@ -1126,8 +1126,8 @@ func (Admin) Domain(ctx context.Context, domain string) dns.Domain {
return d
}
// DomainLocalparts returns the localparts and accounts configured in domain.
func (Admin) DomainLocalparts(ctx context.Context, domain string) (localpartAccounts map[smtp.Localpart]string) {
// DomainLocalparts returns the encoded localparts and accounts configured in domain.
func (Admin) DomainLocalparts(ctx context.Context, domain string) (localpartAccounts map[string]string) {
d, err := dns.ParseDomain(domain)
xcheckf(ctx, err, "parsing domain")
_, ok := mox.Conf.Domain(d)

View File

@ -509,6 +509,9 @@ const account = async (name) => {
lp, '@',
dom.a(d, attr({href: '#domains/'+d})),
]
if (lp === '') {
v.unshift('(catchall) ')
}
}
return dom.tr(
dom.td(v),
@ -568,9 +571,9 @@ const account = async (name) => {
fieldset=dom.fieldset(
dom.label(
style({display: 'inline-block'}),
'Email address or localpart',
dom.span('Email address or localpart', attr({title: 'If empty, or localpart is empty, a catchall address is configured for the domain.'})),
dom.br(),
email=dom.input(attr({required: ''})),
email=dom.input(),
),
' ',
dom.button('Add address'),
@ -748,7 +751,7 @@ const domain = async (d) => {
dom.tbody(
Object.entries(localpartAccounts).map(t =>
dom.tr(
dom.td(t[0]),
dom.td(t[0] || '(catchall)'),
dom.td(dom.a(t[1], attr({href: '#accounts/'+t[1]}))),
dom.td(
dom.button('Remove address', async function click(e) {
@ -758,7 +761,7 @@ const domain = async (d) => {
}
e.target.disabled = true
try {
await api.AddressRemove(t[0] + '@'+d)
await api.AddressRemove(t[0] + '@' + d)
} catch (err) {
console.log({err})
window.alert('Error: ' + err.message)
@ -795,9 +798,9 @@ const domain = async (d) => {
fieldset=dom.fieldset(
dom.label(
style({display: 'inline-block'}),
'Localpart',
dom.span('Localpart', attr({title: 'An empty localpart is the catchall destination/address for the domain.'})),
dom.br(),
localpart=dom.input(attr({required: ''})),
localpart=dom.input(),
),
' ',
dom.label(

View File

@ -58,7 +58,7 @@
},
{
"Name": "DomainLocalparts",
"Docs": "DomainLocalparts returns the localparts and accounts configured in domain.",
"Docs": "DomainLocalparts returns the encoded localparts and accounts configured in domain.",
"Params": [
{
"Name": "domain",
@ -3237,11 +3237,6 @@
}
]
},
{
"Name": "Localpart",
"Docs": "Localpart is a decoded local part of an email address, before the \"@\".\nFor quoted strings, values do not hold the double quote or escaping backslashes.\nAn empty string can be a valid localpart.",
"Values": null
},
{
"Name": "ResultType",
"Docs": "ResultType represents a TLS error.",
@ -3490,6 +3485,11 @@
}
]
},
{
"Name": "Localpart",
"Docs": "Localpart is a decoded local part of an email address, before the \"@\".\nFor quoted strings, values do not hold the double quote or escaping backslashes.\nAn empty string can be a valid localpart.",
"Values": null
},
{
"Name": "IP",
"Docs": "An IP is a single IP address, a slice of bytes.\nFunctions in this package accept either 4-byte (IPv4)\nor 16-byte (IPv6) slices as input.\n\nNote that in this documentation, referring to an\nIP address as an IPv4 address or an IPv6 address\nis a semantic property of the address, not just the\nlength of the byte slice: a 16-byte slice can still\nbe an IPv4 address.",