mirror of
https://github.com/mjl-/mox.git
synced 2024-12-26 16:33:47 +03:00
add aliases/lists: when sending to an alias, the message gets delivered to all members
the members must currently all be addresses of local accounts. a message sent to an alias is accepted if at least one of the members accepts it. if no members accepts it (e.g. due to bad reputation of sender), the message is rejected. if a message is submitted to both an alias addresses and to recipients that are members of the alias in an smtp transaction, the message will be delivered to such members only once. the same applies if the address in the message from-header is the address of a member: that member won't receive the message (they sent it). this prevents duplicate messages. aliases have three configuration options: - PostPublic: whether anyone can send through the alias, or only members. members-only lists can be useful inside organizations for internal communication. public lists can be useful for support addresses. - ListMembers: whether members can see the addresses of other members. this can be seen in the account web interface. in the future, we could export this in other ways, so clients can expand the list. - AllowMsgFrom: whether messages can be sent through the alias with the alias address used in the message from-header. the webmail knows it can use that address, and will use it as from-address when replying to a message sent to that address. ideas for the future: - allow external addresses as members. still with some restrictions, such as requiring a valid dkim-signature so delivery has a chance to succeed. will also need configuration of an admin that can receive any bounces. - allow specifying specific members who can sent through the list (instead of all members). for github issue #57 by hmfaysal. also relevant for #99 by naturalethic. thanks to damir & marin from sartura for discussing requirements/features.
This commit is contained in:
parent
1cf7477642
commit
960a51242d
34 changed files with 2766 additions and 589 deletions
10
README.md
10
README.md
|
@ -24,10 +24,10 @@ See Quickstart below to get started.
|
||||||
- Automatic TLS with ACME, for use with Let's Encrypt and other CA's.
|
- Automatic TLS with ACME, for use with Let's Encrypt and other CA's.
|
||||||
- DANE and MTA-STS for inbound and outbound delivery over SMTP with STARTTLS,
|
- DANE and MTA-STS for inbound and outbound delivery over SMTP with STARTTLS,
|
||||||
including REQUIRETLS and with incoming/outgoing TLSRPT reporting.
|
including REQUIRETLS and with incoming/outgoing TLSRPT reporting.
|
||||||
- Web admin interface that helps you set up your domains and accounts
|
- Web admin interface that helps you set up your domains, accounts and list
|
||||||
(instructions to create DNS records, configure
|
aliases (instructions to create DNS records, configure
|
||||||
SPF/DKIM/DMARC/TLSRPT/MTA-STS), for status information, managing
|
SPF/DKIM/DMARC/TLSRPT/MTA-STS), for status information, and modifying the
|
||||||
accounts/domains, and modifying the configuration file.
|
configuration file.
|
||||||
- Account autodiscovery (with SRV records, Microsoft-style, Thunderbird-style,
|
- Account autodiscovery (with SRV records, Microsoft-style, Thunderbird-style,
|
||||||
and Apple device management profiles) for easy account setup (though client
|
and Apple device management profiles) for easy account setup (though client
|
||||||
support is limited).
|
support is limited).
|
||||||
|
@ -135,7 +135,6 @@ https://nlnet.nl/project/Mox/.
|
||||||
|
|
||||||
## Roadmap
|
## Roadmap
|
||||||
|
|
||||||
- Aliases, for delivering to multiple local accounts.
|
|
||||||
- Calendaring with CalDAV/iCal
|
- Calendaring with CalDAV/iCal
|
||||||
- More IMAP extensions (PREVIEW, WITHIN, IMPORTANT, COMPRESS=DEFLATE,
|
- More IMAP extensions (PREVIEW, WITHIN, IMPORTANT, COMPRESS=DEFLATE,
|
||||||
CREATE-SPECIAL-USE, SAVEDATE, UNAUTHENTICATE, REPLACE, QUOTA, NOTIFY,
|
CREATE-SPECIAL-USE, SAVEDATE, UNAUTHENTICATE, REPLACE, QUOTA, NOTIFY,
|
||||||
|
@ -145,6 +144,7 @@ https://nlnet.nl/project/Mox/.
|
||||||
- Forwarding (to an external address)
|
- Forwarding (to an external address)
|
||||||
- Add special IMAP mailbox ("Queue?") that contains queued but
|
- Add special IMAP mailbox ("Queue?") that contains queued but
|
||||||
undelivered messages, updated with IMAP flags/keywords/tags and message headers.
|
undelivered messages, updated with IMAP flags/keywords/tags and message headers.
|
||||||
|
- External addresses in aliases/lists.
|
||||||
- Sieve for filtering (for now see Rulesets in the account config)
|
- Sieve for filtering (for now see Rulesets in the account config)
|
||||||
- Autoresponder (out of office/vacation)
|
- Autoresponder (out of office/vacation)
|
||||||
- OAUTH2 support, for single sign on
|
- OAUTH2 support, for single sign on
|
||||||
|
|
|
@ -282,8 +282,9 @@ type Domain struct {
|
||||||
MTASTS *MTASTS `sconf:"optional" sconf-doc:"MTA-STS is a mechanism that allows publishing a policy with requirements for WebPKI-verified SMTP STARTTLS connections for email delivered to a domain. Existence of a policy is announced in a DNS TXT record (often unprotected/unverified, MTA-STS's weak spot). If a policy exists, it is fetched with a WebPKI-verified HTTPS request. The policy can indicate that WebPKI-verified SMTP STARTTLS is required, and which MX hosts (optionally with a wildcard pattern) are allowd. MX hosts to deliver to are still taken from DNS (again, not necessarily protected/verified), but messages will only be delivered to domains matching the MX hosts from the published policy. Mail servers look up the MTA-STS policy when first delivering to a domain, then keep a cached copy, periodically checking the DNS record if a new policy is available, and fetching and caching it if so. To update a policy, first serve a new policy with an updated policy ID, then update the DNS record (not the other way around). To remove an enforced policy, publish an updated policy with mode \"none\" for a long enough period so all cached policies have been refreshed (taking DNS TTL and policy max age into account), then remove the policy from DNS, wait for TTL to expire, and stop serving the policy."`
|
MTASTS *MTASTS `sconf:"optional" sconf-doc:"MTA-STS is a mechanism that allows publishing a policy with requirements for WebPKI-verified SMTP STARTTLS connections for email delivered to a domain. Existence of a policy is announced in a DNS TXT record (often unprotected/unverified, MTA-STS's weak spot). If a policy exists, it is fetched with a WebPKI-verified HTTPS request. The policy can indicate that WebPKI-verified SMTP STARTTLS is required, and which MX hosts (optionally with a wildcard pattern) are allowd. MX hosts to deliver to are still taken from DNS (again, not necessarily protected/verified), but messages will only be delivered to domains matching the MX hosts from the published policy. Mail servers look up the MTA-STS policy when first delivering to a domain, then keep a cached copy, periodically checking the DNS record if a new policy is available, and fetching and caching it if so. To update a policy, first serve a new policy with an updated policy ID, then update the DNS record (not the other way around). To remove an enforced policy, publish an updated policy with mode \"none\" for a long enough period so all cached policies have been refreshed (taking DNS TTL and policy max age into account), then remove the policy from DNS, wait for TTL to expire, and stop serving the policy."`
|
||||||
TLSRPT *TLSRPT `sconf:"optional" sconf-doc:"With TLSRPT a domain specifies in DNS where reports about encountered SMTP TLS behaviour should be sent. Useful for monitoring. Incoming TLS reports are automatically parsed, validated, added to metrics and stored in the reporting database for later display in the admin web pages."`
|
TLSRPT *TLSRPT `sconf:"optional" sconf-doc:"With TLSRPT a domain specifies in DNS where reports about encountered SMTP TLS behaviour should be sent. Useful for monitoring. Incoming TLS reports are automatically parsed, validated, added to metrics and stored in the reporting database for later display in the admin web pages."`
|
||||||
Routes []Route `sconf:"optional" sconf-doc:"Routes for delivering outgoing messages through the queue. Each delivery attempt evaluates account routes, these domain routes and finally global routes. The transport of the first matching route is used in the delivery attempt. If no routes match, which is the default with no configured routes, messages are delivered directly from the queue."`
|
Routes []Route `sconf:"optional" sconf-doc:"Routes for delivering outgoing messages through the queue. Each delivery attempt evaluates account routes, these domain routes and finally global routes. The transport of the first matching route is used in the delivery attempt. If no routes match, which is the default with no configured routes, messages are delivered directly from the queue."`
|
||||||
|
Aliases map[string]Alias `sconf:"optional" sconf-doc:"Aliases that cause messages to be delivered to one or more locally configured addresses. Keys are localparts (encoded, as they appear in email addresses)."`
|
||||||
|
|
||||||
Domain dns.Domain `sconf:"-" json:"-"`
|
Domain dns.Domain `sconf:"-"`
|
||||||
ClientSettingsDNSDomain dns.Domain `sconf:"-" json:"-"`
|
ClientSettingsDNSDomain dns.Domain `sconf:"-" json:"-"`
|
||||||
|
|
||||||
// Set when DMARC and TLSRPT (when set) has an address with different domain (we're
|
// Set when DMARC and TLSRPT (when set) has an address with different domain (we're
|
||||||
|
@ -292,6 +293,27 @@ type Domain struct {
|
||||||
ReportsOnly bool `sconf:"-" json:"-"`
|
ReportsOnly bool `sconf:"-" json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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).
|
||||||
|
// todo: as alternative to PostPublic, allow specifying a list of addresses (dmarc-like verified) that are (the only addresses) allowed to post to the list. if msgfrom is an external address, require a valid dkim signature to prevent dmarc-policy-related issues when delivering to remote members.
|
||||||
|
// todo: add option to require messages sent to an alias have that alias as From or Reply-To address?
|
||||||
|
|
||||||
|
type Alias struct {
|
||||||
|
Addresses []string `sconf-doc:"Expanded addresses to deliver to. These must currently be of addresses of local accounts. To prevent duplicate messages, a member address that is also an explicit recipient in the SMTP transaction will only have the message delivered once. If the address in the message From header is a member, that member also won't receive the message."`
|
||||||
|
PostPublic bool `sconf:"optional" sconf-doc:"If true, anyone can send messages to the list. Otherwise only members, based on message From address, which is assumed to be DMARC-like-verified."`
|
||||||
|
ListMembers bool `sconf:"optional" sconf-doc:"If true, members can see addresses of members."`
|
||||||
|
AllowMsgFrom bool `sconf:"optional" sconf-doc:"If true, members are allowed to send messages with this alias address in the message From header."`
|
||||||
|
|
||||||
|
LocalpartStr string `sconf:"-"` // In encoded form.
|
||||||
|
Domain dns.Domain `sconf:"-"`
|
||||||
|
ParsedAddresses []AliasAddress `sconf:"-"` // Matches addresses.
|
||||||
|
}
|
||||||
|
|
||||||
|
type AliasAddress struct {
|
||||||
|
Address smtp.Address // Parsed address.
|
||||||
|
AccountName string // Looked up.
|
||||||
|
Destination Destination // Belonging to address.
|
||||||
|
}
|
||||||
|
|
||||||
type DMARC struct {
|
type DMARC struct {
|
||||||
Localpart string `sconf-doc:"Address-part before the @ that accepts DMARC reports. Must be non-internationalized. Recommended value: dmarc-reports."`
|
Localpart string `sconf-doc:"Address-part before the @ that accepts DMARC reports. Must be non-internationalized. Recommended value: dmarc-reports."`
|
||||||
Domain string `sconf:"optional" sconf-doc:"Alternative domain for reporting address, for incoming reports. Typically empty, causing the domain wherein this config exists to be used. Can be used to receive reports for domains that aren't fully hosted on this server. Configure such a domain as a hosted domain without making all the DNS changes, and configure this field with a domain that is fully hosted on this server, so the localpart and the domain of this field form a reporting address. Then only update the DMARC DNS record for the not fully hosted domain, ensuring the reporting address is specified in its \"rua\" field as shown in the suggested DNS settings. Unicode name."`
|
Domain string `sconf:"optional" sconf-doc:"Alternative domain for reporting address, for incoming reports. Typically empty, causing the domain wherein this config exists to be used. Can be used to receive reports for domains that aren't fully hosted on this server. Configure such a domain as a hosted domain without making all the DNS changes, and configure this field with a domain that is fully hosted on this server, so the localpart and the domain of this field form a reporting address. Then only update the DMARC DNS record for the not fully hosted domain, ensuring the reporting address is specified in its \"rua\" field as shown in the suggested DNS settings. Unicode name."`
|
||||||
|
@ -412,6 +434,13 @@ type Account struct {
|
||||||
NeutralMailbox *regexp.Regexp `sconf:"-" json:"-"`
|
NeutralMailbox *regexp.Regexp `sconf:"-" json:"-"`
|
||||||
NotJunkMailbox *regexp.Regexp `sconf:"-" json:"-"`
|
NotJunkMailbox *regexp.Regexp `sconf:"-" json:"-"`
|
||||||
ParsedFromIDLoginAddresses []smtp.Address `sconf:"-" json:"-"`
|
ParsedFromIDLoginAddresses []smtp.Address `sconf:"-" json:"-"`
|
||||||
|
Aliases []AddressAlias `sconf:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AddressAlias struct {
|
||||||
|
SubscriptionAddress string
|
||||||
|
Alias Alias // Without members.
|
||||||
|
MemberAddresses []string // Only if allowed to see.
|
||||||
}
|
}
|
||||||
|
|
||||||
type JunkFilter struct {
|
type JunkFilter struct {
|
||||||
|
|
|
@ -913,6 +913,31 @@ See https://pkg.go.dev/github.com/mjl-/sconf for details.
|
||||||
MinimumAttempts: 0
|
MinimumAttempts: 0
|
||||||
Transport:
|
Transport:
|
||||||
|
|
||||||
|
# Aliases that cause messages to be delivered to one or more locally configured
|
||||||
|
# addresses. Keys are localparts (encoded, as they appear in email addresses).
|
||||||
|
# (optional)
|
||||||
|
Aliases:
|
||||||
|
x:
|
||||||
|
|
||||||
|
# Expanded addresses to deliver to. These must currently be of addresses of local
|
||||||
|
# accounts. To prevent duplicate messages, a member address that is also an
|
||||||
|
# explicit recipient in the SMTP transaction will only have the message delivered
|
||||||
|
# once. If the address in the message From header is a member, that member also
|
||||||
|
# won't receive the message.
|
||||||
|
Addresses:
|
||||||
|
-
|
||||||
|
|
||||||
|
# If true, anyone can send messages to the list. Otherwise only members, based on
|
||||||
|
# message From address, which is assumed to be DMARC-like-verified. (optional)
|
||||||
|
PostPublic: false
|
||||||
|
|
||||||
|
# If true, members can see addresses of members. (optional)
|
||||||
|
ListMembers: false
|
||||||
|
|
||||||
|
# If true, members are allowed to send messages with this alias address in the
|
||||||
|
# message From header. (optional)
|
||||||
|
AllowMsgFrom: false
|
||||||
|
|
||||||
# Accounts represent mox users, each with a password and email address(es) to
|
# Accounts represent mox users, each with a password and email address(es) to
|
||||||
# which email can be delivered (possibly at different domains). Each account has
|
# which email can be delivered (possibly at different domains). Each account has
|
||||||
# its own on-disk directory holding its messages and index database. An account
|
# its own on-disk directory holding its messages and index database. An account
|
||||||
|
|
161
ctl.go
161
ctl.go
|
@ -9,6 +9,7 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
"maps"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
@ -20,6 +21,7 @@ import (
|
||||||
|
|
||||||
"github.com/mjl-/bstore"
|
"github.com/mjl-/bstore"
|
||||||
|
|
||||||
|
"github.com/mjl-/mox/config"
|
||||||
"github.com/mjl-/mox/dns"
|
"github.com/mjl-/mox/dns"
|
||||||
"github.com/mjl-/mox/message"
|
"github.com/mjl-/mox/message"
|
||||||
"github.com/mjl-/mox/metrics"
|
"github.com/mjl-/mox/metrics"
|
||||||
|
@ -1017,6 +1019,165 @@ func servectlcmd(ctx context.Context, ctl *ctl, shutdown func()) {
|
||||||
ctl.xcheck(err, "removing address")
|
ctl.xcheck(err, "removing address")
|
||||||
ctl.xwriteok()
|
ctl.xwriteok()
|
||||||
|
|
||||||
|
case "aliaslist":
|
||||||
|
/* protocol:
|
||||||
|
> "aliaslist"
|
||||||
|
> domain
|
||||||
|
< "ok" or error
|
||||||
|
< stream
|
||||||
|
*/
|
||||||
|
domain := ctl.xread()
|
||||||
|
d, err := dns.ParseDomain(domain)
|
||||||
|
ctl.xcheck(err, "parsing domain")
|
||||||
|
dc, ok := mox.Conf.Domain(d)
|
||||||
|
if !ok {
|
||||||
|
ctl.xcheck(errors.New("no such domain"), "listing aliases")
|
||||||
|
}
|
||||||
|
ctl.xwriteok()
|
||||||
|
w := ctl.writer()
|
||||||
|
for _, a := range dc.Aliases {
|
||||||
|
lp, err := smtp.ParseLocalpart(a.LocalpartStr)
|
||||||
|
ctl.xcheck(err, "parsing alias localpart")
|
||||||
|
fmt.Fprintln(w, smtp.NewAddress(lp, a.Domain).Pack(true))
|
||||||
|
}
|
||||||
|
w.xclose()
|
||||||
|
|
||||||
|
case "aliasprint":
|
||||||
|
/* protocol:
|
||||||
|
> "aliasprint"
|
||||||
|
> address
|
||||||
|
< "ok" or error
|
||||||
|
< stream
|
||||||
|
*/
|
||||||
|
address := ctl.xread()
|
||||||
|
_, alias, ok := mox.Conf.AccountDestination(address)
|
||||||
|
if !ok {
|
||||||
|
ctl.xcheck(errors.New("no such address"), "looking up alias")
|
||||||
|
} else if alias == nil {
|
||||||
|
ctl.xcheck(errors.New("address not an alias"), "looking up alias")
|
||||||
|
}
|
||||||
|
ctl.xwriteok()
|
||||||
|
w := ctl.writer()
|
||||||
|
fmt.Fprintf(w, "# postpublic %v\n", alias.PostPublic)
|
||||||
|
fmt.Fprintf(w, "# listmembers %v\n", alias.ListMembers)
|
||||||
|
fmt.Fprintf(w, "# allowmsgfrom %v\n", alias.AllowMsgFrom)
|
||||||
|
fmt.Fprintln(w, "# members:")
|
||||||
|
for _, a := range alias.Addresses {
|
||||||
|
fmt.Fprintln(w, a)
|
||||||
|
}
|
||||||
|
w.xclose()
|
||||||
|
|
||||||
|
case "aliasadd":
|
||||||
|
/* protocol:
|
||||||
|
> "aliasadd"
|
||||||
|
> address
|
||||||
|
> json alias
|
||||||
|
< "ok" or error
|
||||||
|
*/
|
||||||
|
address := ctl.xread()
|
||||||
|
line := ctl.xread()
|
||||||
|
addr, err := smtp.ParseAddress(address)
|
||||||
|
ctl.xcheck(err, "parsing address")
|
||||||
|
var alias config.Alias
|
||||||
|
xparseJSON(ctl, line, &alias)
|
||||||
|
err = mox.AliasAdd(ctx, addr, alias)
|
||||||
|
ctl.xcheck(err, "adding alias")
|
||||||
|
ctl.xwriteok()
|
||||||
|
|
||||||
|
case "aliasupdate":
|
||||||
|
/* protocol:
|
||||||
|
> "aliasupdate"
|
||||||
|
> alias
|
||||||
|
> "true" or "false" for postpublic
|
||||||
|
> "true" or "false" for listmembers
|
||||||
|
> "true" or "false" for allowmsgfrom
|
||||||
|
< "ok" or error
|
||||||
|
*/
|
||||||
|
address := ctl.xread()
|
||||||
|
postpublic := ctl.xread()
|
||||||
|
listmembers := ctl.xread()
|
||||||
|
allowmsgfrom := ctl.xread()
|
||||||
|
addr, err := smtp.ParseAddress(address)
|
||||||
|
ctl.xcheck(err, "parsing address")
|
||||||
|
err = mox.DomainSave(ctx, addr.Domain.Name(), func(d *config.Domain) error {
|
||||||
|
a, ok := d.Aliases[addr.Localpart.String()]
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("alias does not exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
switch postpublic {
|
||||||
|
case "false":
|
||||||
|
a.PostPublic = false
|
||||||
|
case "true":
|
||||||
|
a.PostPublic = true
|
||||||
|
}
|
||||||
|
switch listmembers {
|
||||||
|
case "false":
|
||||||
|
a.ListMembers = false
|
||||||
|
case "true":
|
||||||
|
a.ListMembers = true
|
||||||
|
}
|
||||||
|
switch allowmsgfrom {
|
||||||
|
case "false":
|
||||||
|
a.AllowMsgFrom = false
|
||||||
|
case "true":
|
||||||
|
a.AllowMsgFrom = true
|
||||||
|
}
|
||||||
|
|
||||||
|
d.Aliases = maps.Clone(d.Aliases)
|
||||||
|
d.Aliases[addr.Localpart.String()] = a
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
ctl.xcheck(err, "saving alias")
|
||||||
|
ctl.xwriteok()
|
||||||
|
|
||||||
|
case "aliasrm":
|
||||||
|
/* protocol:
|
||||||
|
> "aliasrm"
|
||||||
|
> alias
|
||||||
|
< "ok" or error
|
||||||
|
*/
|
||||||
|
address := ctl.xread()
|
||||||
|
addr, err := smtp.ParseAddress(address)
|
||||||
|
ctl.xcheck(err, "parsing address")
|
||||||
|
err = mox.AliasRemove(ctx, addr)
|
||||||
|
ctl.xcheck(err, "removing alias")
|
||||||
|
ctl.xwriteok()
|
||||||
|
|
||||||
|
case "aliasaddaddr":
|
||||||
|
/* protocol:
|
||||||
|
> "aliasaddaddr"
|
||||||
|
> alias
|
||||||
|
> addresses as json
|
||||||
|
< "ok" or error
|
||||||
|
*/
|
||||||
|
address := ctl.xread()
|
||||||
|
line := ctl.xread()
|
||||||
|
addr, err := smtp.ParseAddress(address)
|
||||||
|
ctl.xcheck(err, "parsing address")
|
||||||
|
var addresses []string
|
||||||
|
xparseJSON(ctl, line, &addresses)
|
||||||
|
err = mox.AliasAddressesAdd(ctx, addr, addresses)
|
||||||
|
ctl.xcheck(err, "adding addresses to alias")
|
||||||
|
ctl.xwriteok()
|
||||||
|
|
||||||
|
case "aliasrmaddr":
|
||||||
|
/* protocol:
|
||||||
|
> "aliasrmaddr"
|
||||||
|
> alias
|
||||||
|
> addresses as json
|
||||||
|
< "ok" or error
|
||||||
|
*/
|
||||||
|
address := ctl.xread()
|
||||||
|
line := ctl.xread()
|
||||||
|
addr, err := smtp.ParseAddress(address)
|
||||||
|
ctl.xcheck(err, "parsing address")
|
||||||
|
var addresses []string
|
||||||
|
xparseJSON(ctl, line, &addresses)
|
||||||
|
err = mox.AliasAddressesRemove(ctx, addr, addresses)
|
||||||
|
ctl.xcheck(err, "removing addresses to alias")
|
||||||
|
ctl.xwriteok()
|
||||||
|
|
||||||
case "loglevels":
|
case "loglevels":
|
||||||
/* protocol:
|
/* protocol:
|
||||||
> "loglevels"
|
> "loglevels"
|
||||||
|
|
36
ctl_test.go
36
ctl_test.go
|
@ -12,6 +12,7 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/mjl-/mox/config"
|
||||||
"github.com/mjl-/mox/dmarcdb"
|
"github.com/mjl-/mox/dmarcdb"
|
||||||
"github.com/mjl-/mox/dns"
|
"github.com/mjl-/mox/dns"
|
||||||
"github.com/mjl-/mox/mlog"
|
"github.com/mjl-/mox/mlog"
|
||||||
|
@ -292,6 +293,41 @@ func TestCtl(t *testing.T) {
|
||||||
ctlcmdConfigDomainRemove(ctl, dns.Domain{ASCII: "mox2.example"})
|
ctlcmdConfigDomainRemove(ctl, dns.Domain{ASCII: "mox2.example"})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// "aliasadd"
|
||||||
|
testctl(func(ctl *ctl) {
|
||||||
|
ctlcmdConfigAliasAdd(ctl, "support@mox.example", config.Alias{Addresses: []string{"mjl@mox.example"}})
|
||||||
|
})
|
||||||
|
|
||||||
|
// "aliaslist"
|
||||||
|
testctl(func(ctl *ctl) {
|
||||||
|
ctlcmdConfigAliasList(ctl, "mox.example")
|
||||||
|
})
|
||||||
|
|
||||||
|
// "aliasprint"
|
||||||
|
testctl(func(ctl *ctl) {
|
||||||
|
ctlcmdConfigAliasPrint(ctl, "support@mox.example")
|
||||||
|
})
|
||||||
|
|
||||||
|
// "aliasupdate"
|
||||||
|
testctl(func(ctl *ctl) {
|
||||||
|
ctlcmdConfigAliasUpdate(ctl, "support@mox.example", "true", "true", "true")
|
||||||
|
})
|
||||||
|
|
||||||
|
// "aliasaddaddr"
|
||||||
|
testctl(func(ctl *ctl) {
|
||||||
|
ctlcmdConfigAliasAddaddr(ctl, "support@mox.example", []string{"mjl2@mox.example"})
|
||||||
|
})
|
||||||
|
|
||||||
|
// "aliasrmaddr"
|
||||||
|
testctl(func(ctl *ctl) {
|
||||||
|
ctlcmdConfigAliasRmaddr(ctl, "support@mox.example", []string{"mjl2@mox.example"})
|
||||||
|
})
|
||||||
|
|
||||||
|
// "aliasrm"
|
||||||
|
testctl(func(ctl *ctl) {
|
||||||
|
ctlcmdConfigAliasRemove(ctl, "support@mox.example")
|
||||||
|
})
|
||||||
|
|
||||||
// "loglevels"
|
// "loglevels"
|
||||||
testctl(func(ctl *ctl) {
|
testctl(func(ctl *ctl) {
|
||||||
ctlcmdLoglevels(ctl)
|
ctlcmdLoglevels(ctl)
|
||||||
|
|
55
doc.go
55
doc.go
|
@ -68,6 +68,13 @@ any parameters. Followed by the help and usage information for each command.
|
||||||
mox config address rm address
|
mox config address rm address
|
||||||
mox config domain add domain account [localpart]
|
mox config domain add domain account [localpart]
|
||||||
mox config domain rm domain
|
mox config domain rm domain
|
||||||
|
mox config alias list domain
|
||||||
|
mox config alias print alias
|
||||||
|
mox config alias add alias@domain rcpt1@domain ...
|
||||||
|
mox config alias update alias@domain [-postpublic false|true -listmembers false|true -allowmsgfrom false|true]
|
||||||
|
mox config alias rm alias@domain
|
||||||
|
mox config alias addaddr alias@domain rcpt1@domain ...
|
||||||
|
mox config alias rmaddr alias@domain rcpt1@domain ...
|
||||||
mox config describe-sendmail >/etc/moxsubmit.conf
|
mox config describe-sendmail >/etc/moxsubmit.conf
|
||||||
mox config printservice >mox.service
|
mox config printservice >mox.service
|
||||||
mox config ensureacmehostprivatekeys
|
mox config ensureacmehostprivatekeys
|
||||||
|
@ -968,6 +975,54 @@ rejected.
|
||||||
|
|
||||||
usage: mox config domain rm domain
|
usage: mox config domain rm domain
|
||||||
|
|
||||||
|
# mox config alias list
|
||||||
|
|
||||||
|
List aliases for domain.
|
||||||
|
|
||||||
|
usage: mox config alias list domain
|
||||||
|
|
||||||
|
# mox config alias print
|
||||||
|
|
||||||
|
Print settings and members of alias.
|
||||||
|
|
||||||
|
usage: mox config alias print alias
|
||||||
|
|
||||||
|
# mox config alias add
|
||||||
|
|
||||||
|
Add new alias with one or more addresses.
|
||||||
|
|
||||||
|
usage: mox config alias add alias@domain rcpt1@domain ...
|
||||||
|
|
||||||
|
# mox config alias update
|
||||||
|
|
||||||
|
Update alias configuration.
|
||||||
|
|
||||||
|
usage: mox config alias update alias@domain [-postpublic false|true -listmembers false|true -allowmsgfrom false|true]
|
||||||
|
-allowmsgfrom string
|
||||||
|
whether alias address can be used in message from header
|
||||||
|
-listmembers string
|
||||||
|
whether list members can list members
|
||||||
|
-postpublic string
|
||||||
|
whether anyone or only list members can post
|
||||||
|
|
||||||
|
# mox config alias rm
|
||||||
|
|
||||||
|
Remove alias.
|
||||||
|
|
||||||
|
usage: mox config alias rm alias@domain
|
||||||
|
|
||||||
|
# mox config alias addaddr
|
||||||
|
|
||||||
|
Add addresses to alias.
|
||||||
|
|
||||||
|
usage: mox config alias addaddr alias@domain rcpt1@domain ...
|
||||||
|
|
||||||
|
# mox config alias rmaddr
|
||||||
|
|
||||||
|
Remove addresses from alias.
|
||||||
|
|
||||||
|
usage: mox config alias rmaddr alias@domain rcpt1@domain ...
|
||||||
|
|
||||||
# mox config describe-sendmail
|
# mox config describe-sendmail
|
||||||
|
|
||||||
Describe configuration for mox when invoked as sendmail.
|
Describe configuration for mox when invoked as sendmail.
|
||||||
|
|
149
main.go
149
main.go
|
@ -148,6 +148,14 @@ var commands = []struct {
|
||||||
{"config address rm", cmdConfigAddressRemove},
|
{"config address rm", cmdConfigAddressRemove},
|
||||||
{"config domain add", cmdConfigDomainAdd},
|
{"config domain add", cmdConfigDomainAdd},
|
||||||
{"config domain rm", cmdConfigDomainRemove},
|
{"config domain rm", cmdConfigDomainRemove},
|
||||||
|
{"config alias list", cmdConfigAliasList},
|
||||||
|
{"config alias print", cmdConfigAliasPrint},
|
||||||
|
{"config alias add", cmdConfigAliasAdd},
|
||||||
|
{"config alias update", cmdConfigAliasUpdate},
|
||||||
|
{"config alias rm", cmdConfigAliasRemove},
|
||||||
|
{"config alias addaddr", cmdConfigAliasAddaddr},
|
||||||
|
{"config alias rmaddr", cmdConfigAliasRemoveaddr},
|
||||||
|
|
||||||
{"config describe-sendmail", cmdConfigDescribeSendmail},
|
{"config describe-sendmail", cmdConfigDescribeSendmail},
|
||||||
{"config printservice", cmdConfigPrintservice},
|
{"config printservice", cmdConfigPrintservice},
|
||||||
{"config ensureacmehostprivatekeys", cmdConfigEnsureACMEHostprivatekeys},
|
{"config ensureacmehostprivatekeys", cmdConfigEnsureACMEHostprivatekeys},
|
||||||
|
@ -711,6 +719,147 @@ func ctlcmdConfigDomainRemove(ctl *ctl, d dns.Domain) {
|
||||||
fmt.Printf("domain removed, remember to remove dns records for %s\n", d)
|
fmt.Printf("domain removed, remember to remove dns records for %s\n", d)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func cmdConfigAliasList(c *cmd) {
|
||||||
|
c.params = "domain"
|
||||||
|
c.help = `List aliases for domain.`
|
||||||
|
args := c.Parse()
|
||||||
|
if len(args) != 1 {
|
||||||
|
c.Usage()
|
||||||
|
}
|
||||||
|
|
||||||
|
mustLoadConfig()
|
||||||
|
ctlcmdConfigAliasList(xctl(), args[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
func ctlcmdConfigAliasList(ctl *ctl, address string) {
|
||||||
|
ctl.xwrite("aliaslist")
|
||||||
|
ctl.xwrite(address)
|
||||||
|
ctl.xreadok()
|
||||||
|
ctl.xstreamto(os.Stdout)
|
||||||
|
}
|
||||||
|
|
||||||
|
func cmdConfigAliasPrint(c *cmd) {
|
||||||
|
c.params = "alias"
|
||||||
|
c.help = `Print settings and members of alias.`
|
||||||
|
args := c.Parse()
|
||||||
|
if len(args) != 1 {
|
||||||
|
c.Usage()
|
||||||
|
}
|
||||||
|
|
||||||
|
mustLoadConfig()
|
||||||
|
ctlcmdConfigAliasPrint(xctl(), args[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
func ctlcmdConfigAliasPrint(ctl *ctl, address string) {
|
||||||
|
ctl.xwrite("aliasprint")
|
||||||
|
ctl.xwrite(address)
|
||||||
|
ctl.xreadok()
|
||||||
|
ctl.xstreamto(os.Stdout)
|
||||||
|
}
|
||||||
|
|
||||||
|
func cmdConfigAliasAdd(c *cmd) {
|
||||||
|
c.params = "alias@domain rcpt1@domain ..."
|
||||||
|
c.help = `Add new alias with one or more addresses.`
|
||||||
|
args := c.Parse()
|
||||||
|
if len(args) < 2 {
|
||||||
|
c.Usage()
|
||||||
|
}
|
||||||
|
|
||||||
|
alias := config.Alias{Addresses: args[1:]}
|
||||||
|
|
||||||
|
mustLoadConfig()
|
||||||
|
ctlcmdConfigAliasAdd(xctl(), args[0], alias)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ctlcmdConfigAliasAdd(ctl *ctl, address string, alias config.Alias) {
|
||||||
|
ctl.xwrite("aliasadd")
|
||||||
|
ctl.xwrite(address)
|
||||||
|
xctlwriteJSON(ctl, alias)
|
||||||
|
ctl.xreadok()
|
||||||
|
}
|
||||||
|
|
||||||
|
func cmdConfigAliasUpdate(c *cmd) {
|
||||||
|
c.params = "alias@domain [-postpublic false|true -listmembers false|true -allowmsgfrom false|true]"
|
||||||
|
c.help = `Update alias configuration.`
|
||||||
|
var postpublic, listmembers, allowmsgfrom string
|
||||||
|
c.flag.StringVar(&postpublic, "postpublic", "", "whether anyone or only list members can post")
|
||||||
|
c.flag.StringVar(&listmembers, "listmembers", "", "whether list members can list members")
|
||||||
|
c.flag.StringVar(&allowmsgfrom, "allowmsgfrom", "", "whether alias address can be used in message from header")
|
||||||
|
args := c.Parse()
|
||||||
|
if len(args) != 1 {
|
||||||
|
c.Usage()
|
||||||
|
}
|
||||||
|
|
||||||
|
alias := args[0]
|
||||||
|
mustLoadConfig()
|
||||||
|
ctlcmdConfigAliasUpdate(xctl(), alias, postpublic, listmembers, allowmsgfrom)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ctlcmdConfigAliasUpdate(ctl *ctl, alias, postpublic, listmembers, allowmsgfrom string) {
|
||||||
|
ctl.xwrite("aliasupdate")
|
||||||
|
ctl.xwrite(alias)
|
||||||
|
ctl.xwrite(postpublic)
|
||||||
|
ctl.xwrite(listmembers)
|
||||||
|
ctl.xwrite(allowmsgfrom)
|
||||||
|
ctl.xreadok()
|
||||||
|
}
|
||||||
|
|
||||||
|
func cmdConfigAliasRemove(c *cmd) {
|
||||||
|
c.params = "alias@domain"
|
||||||
|
c.help = "Remove alias."
|
||||||
|
args := c.Parse()
|
||||||
|
if len(args) != 1 {
|
||||||
|
c.Usage()
|
||||||
|
}
|
||||||
|
|
||||||
|
mustLoadConfig()
|
||||||
|
ctlcmdConfigAliasRemove(xctl(), args[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
func ctlcmdConfigAliasRemove(ctl *ctl, alias string) {
|
||||||
|
ctl.xwrite("aliasrm")
|
||||||
|
ctl.xwrite(alias)
|
||||||
|
ctl.xreadok()
|
||||||
|
}
|
||||||
|
|
||||||
|
func cmdConfigAliasAddaddr(c *cmd) {
|
||||||
|
c.params = "alias@domain rcpt1@domain ..."
|
||||||
|
c.help = `Add addresses to alias.`
|
||||||
|
args := c.Parse()
|
||||||
|
if len(args) < 2 {
|
||||||
|
c.Usage()
|
||||||
|
}
|
||||||
|
|
||||||
|
mustLoadConfig()
|
||||||
|
ctlcmdConfigAliasAddaddr(xctl(), args[0], args[1:])
|
||||||
|
}
|
||||||
|
|
||||||
|
func ctlcmdConfigAliasAddaddr(ctl *ctl, alias string, addresses []string) {
|
||||||
|
ctl.xwrite("aliasaddaddr")
|
||||||
|
ctl.xwrite(alias)
|
||||||
|
xctlwriteJSON(ctl, addresses)
|
||||||
|
ctl.xreadok()
|
||||||
|
}
|
||||||
|
|
||||||
|
func cmdConfigAliasRemoveaddr(c *cmd) {
|
||||||
|
c.params = "alias@domain rcpt1@domain ..."
|
||||||
|
c.help = `Remove addresses from alias.`
|
||||||
|
args := c.Parse()
|
||||||
|
if len(args) < 2 {
|
||||||
|
c.Usage()
|
||||||
|
}
|
||||||
|
|
||||||
|
mustLoadConfig()
|
||||||
|
ctlcmdConfigAliasRmaddr(xctl(), args[0], args[1:])
|
||||||
|
}
|
||||||
|
|
||||||
|
func ctlcmdConfigAliasRmaddr(ctl *ctl, alias string, addresses []string) {
|
||||||
|
ctl.xwrite("aliasrmaddr")
|
||||||
|
ctl.xwrite(alias)
|
||||||
|
xctlwriteJSON(ctl, addresses)
|
||||||
|
ctl.xreadok()
|
||||||
|
}
|
||||||
|
|
||||||
func cmdConfigAccountAdd(c *cmd) {
|
func cmdConfigAccountAdd(c *cmd) {
|
||||||
c.params = "account address"
|
c.params = "account address"
|
||||||
c.help = `Add an account with an email address and reload the configuration.
|
c.help = `Add an account with an email address and reload the configuration.
|
||||||
|
|
110
mox-/admin.go
110
mox-/admin.go
|
@ -17,6 +17,7 @@ import (
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"slices"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
@ -605,7 +606,7 @@ func moveAwayKeys(log mlog.Log, sels map[string]config.Selector, usedKeyPaths ma
|
||||||
// can modify the config, but must clone all referencing data it changes.
|
// can modify the config, but must clone all referencing data it changes.
|
||||||
// xmodify may employ panic-based error handling. After xmodify returns, the
|
// xmodify may employ panic-based error handling. After xmodify returns, the
|
||||||
// modified config is verified, saved and takes effect.
|
// modified config is verified, saved and takes effect.
|
||||||
func DomainSave(ctx context.Context, domainName string, xmodify func(config *config.Domain)) (rerr error) {
|
func DomainSave(ctx context.Context, domainName string, xmodify func(config *config.Domain) error) (rerr error) {
|
||||||
log := pkglog.WithContext(ctx)
|
log := pkglog.WithContext(ctx)
|
||||||
defer func() {
|
defer func() {
|
||||||
if rerr != nil {
|
if rerr != nil {
|
||||||
|
@ -622,7 +623,9 @@ func DomainSave(ctx context.Context, domainName string, xmodify func(config *con
|
||||||
return fmt.Errorf("%w: domain not present", ErrRequest)
|
return fmt.Errorf("%w: domain not present", ErrRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
xmodify(&dom)
|
if err := xmodify(&dom); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// Compose new config without modifying existing data structures. If we fail, we
|
// Compose new config without modifying existing data structures. If we fail, we
|
||||||
// leave no trace.
|
// leave no trace.
|
||||||
|
@ -1031,14 +1034,17 @@ func AccountRemove(ctx context.Context, account string) (rerr error) {
|
||||||
//
|
//
|
||||||
// Must be called with config lock held.
|
// Must be called with config lock held.
|
||||||
func checkAddressAvailable(addr smtp.Address) error {
|
func checkAddressAvailable(addr smtp.Address) error {
|
||||||
if dc, ok := Conf.Dynamic.Domains[addr.Domain.Name()]; !ok {
|
dc, ok := Conf.Dynamic.Domains[addr.Domain.Name()]
|
||||||
|
if !ok {
|
||||||
return fmt.Errorf("domain does not exist")
|
return fmt.Errorf("domain does not exist")
|
||||||
} else if lp, err := CanonicalLocalpart(addr.Localpart, dc); err != nil {
|
}
|
||||||
return fmt.Errorf("canonicalizing localpart: %v", err)
|
lp := CanonicalLocalpart(addr.Localpart, dc)
|
||||||
} else if _, ok := Conf.accountDestinations[smtp.NewAddress(lp, addr.Domain).String()]; ok {
|
if _, ok := Conf.accountDestinations[smtp.NewAddress(lp, addr.Domain).String()]; ok {
|
||||||
return fmt.Errorf("canonicalized address %s already configured", smtp.NewAddress(lp, addr.Domain))
|
return fmt.Errorf("canonicalized address %s already configured", smtp.NewAddress(lp, addr.Domain))
|
||||||
} else if dc.LocalpartCatchallSeparator != "" && strings.Contains(string(addr.Localpart), dc.LocalpartCatchallSeparator) {
|
} else if dc.LocalpartCatchallSeparator != "" && strings.Contains(string(addr.Localpart), dc.LocalpartCatchallSeparator) {
|
||||||
return fmt.Errorf("localpart cannot include domain catchall separator %s", dc.LocalpartCatchallSeparator)
|
return fmt.Errorf("localpart cannot include domain catchall separator %s", dc.LocalpartCatchallSeparator)
|
||||||
|
} else if _, ok := dc.Aliases[lp.String()]; ok {
|
||||||
|
return fmt.Errorf("address in use as alias")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -1177,14 +1183,8 @@ func AddressRemove(ctx context.Context, address string) (rerr error) {
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("%w: unknown domain in fromid login address %q", ErrRequest, fa.Pack(true))
|
return fmt.Errorf("%w: unknown domain in fromid login address %q", ErrRequest, fa.Pack(true))
|
||||||
}
|
}
|
||||||
flp, err := CanonicalLocalpart(fa.Localpart, dc)
|
flp := CanonicalLocalpart(fa.Localpart, dc)
|
||||||
if err != nil {
|
alp := CanonicalLocalpart(pa.Localpart, dc)
|
||||||
return fmt.Errorf("%w: getting canonical localpart for fromid login address %q: %v", ErrRequest, fa.Localpart, err)
|
|
||||||
}
|
|
||||||
alp, err := CanonicalLocalpart(pa.Localpart, dc)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("%w: getting canonical part for address: %v", ErrRequest, err)
|
|
||||||
}
|
|
||||||
if alp != flp {
|
if alp != flp {
|
||||||
// Keep for different localpart.
|
// Keep for different localpart.
|
||||||
fromIDLoginAddresses = append(fromIDLoginAddresses, a.FromIDLoginAddresses[i])
|
fromIDLoginAddresses = append(fromIDLoginAddresses, a.FromIDLoginAddresses[i])
|
||||||
|
@ -1206,6 +1206,88 @@ func AddressRemove(ctx context.Context, address string) (rerr error) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func AliasAdd(ctx context.Context, addr smtp.Address, alias config.Alias) error {
|
||||||
|
return DomainSave(ctx, addr.Domain.Name(), func(d *config.Domain) error {
|
||||||
|
if _, ok := d.Aliases[addr.Localpart.String()]; ok {
|
||||||
|
return fmt.Errorf("%w: alias already present", ErrRequest)
|
||||||
|
}
|
||||||
|
if d.Aliases == nil {
|
||||||
|
d.Aliases = map[string]config.Alias{}
|
||||||
|
}
|
||||||
|
d.Aliases = maps.Clone(d.Aliases)
|
||||||
|
d.Aliases[addr.Localpart.String()] = alias
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func AliasUpdate(ctx context.Context, addr smtp.Address, alias config.Alias) error {
|
||||||
|
return DomainSave(ctx, addr.Domain.Name(), func(d *config.Domain) error {
|
||||||
|
a, ok := d.Aliases[addr.Localpart.String()]
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("%w: alias does not exist", ErrRequest)
|
||||||
|
}
|
||||||
|
a.PostPublic = alias.PostPublic
|
||||||
|
a.ListMembers = alias.ListMembers
|
||||||
|
a.AllowMsgFrom = alias.AllowMsgFrom
|
||||||
|
d.Aliases = maps.Clone(d.Aliases)
|
||||||
|
d.Aliases[addr.Localpart.String()] = a
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func AliasRemove(ctx context.Context, addr smtp.Address) error {
|
||||||
|
return DomainSave(ctx, addr.Domain.Name(), func(d *config.Domain) error {
|
||||||
|
_, ok := d.Aliases[addr.Localpart.String()]
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("%w: alias does not exist", ErrRequest)
|
||||||
|
}
|
||||||
|
d.Aliases = maps.Clone(d.Aliases)
|
||||||
|
delete(d.Aliases, addr.Localpart.String())
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func AliasAddressesAdd(ctx context.Context, addr smtp.Address, addresses []string) error {
|
||||||
|
if len(addresses) == 0 {
|
||||||
|
return fmt.Errorf("%w: at least one address required", ErrRequest)
|
||||||
|
}
|
||||||
|
return DomainSave(ctx, addr.Domain.Name(), func(d *config.Domain) error {
|
||||||
|
alias, ok := d.Aliases[addr.Localpart.String()]
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("%w: no such alias", ErrRequest)
|
||||||
|
}
|
||||||
|
alias.Addresses = append(slices.Clone(alias.Addresses), addresses...)
|
||||||
|
alias.ParsedAddresses = nil
|
||||||
|
d.Aliases = maps.Clone(d.Aliases)
|
||||||
|
d.Aliases[addr.Localpart.String()] = alias
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func AliasAddressesRemove(ctx context.Context, addr smtp.Address, addresses []string) error {
|
||||||
|
if len(addresses) == 0 {
|
||||||
|
return fmt.Errorf("%w: need at least one address", ErrRequest)
|
||||||
|
}
|
||||||
|
return DomainSave(ctx, addr.Domain.Name(), func(d *config.Domain) error {
|
||||||
|
alias, ok := d.Aliases[addr.Localpart.String()]
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("%w: no such alias", ErrRequest)
|
||||||
|
}
|
||||||
|
alias.Addresses = slices.DeleteFunc(slices.Clone(alias.Addresses), func(addr string) bool {
|
||||||
|
n := len(addresses)
|
||||||
|
addresses = slices.DeleteFunc(addresses, func(a string) bool { return a == addr })
|
||||||
|
return n > len(addresses)
|
||||||
|
})
|
||||||
|
if len(addresses) > 0 {
|
||||||
|
return fmt.Errorf("%w: address not found: %s", ErrRequest, strings.Join(addresses, ", "))
|
||||||
|
}
|
||||||
|
alias.ParsedAddresses = nil
|
||||||
|
d.Aliases = maps.Clone(d.Aliases)
|
||||||
|
d.Aliases[addr.Localpart.String()] = alias
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// AccountSave updates the configuration of an account. Function xmodify is called
|
// AccountSave updates the configuration of an account. Function xmodify is called
|
||||||
// with a shallow copy of the current configuration of the account. It must not
|
// with a shallow copy of the current configuration of the account. It must not
|
||||||
// change referencing fields (e.g. existing slice/map/pointer), they may still be
|
// change referencing fields (e.g. existing slice/map/pointer), they may still be
|
||||||
|
|
140
mox-/config.go
140
mox-/config.go
|
@ -80,6 +80,8 @@ type Config struct {
|
||||||
// case-insensitive, stripped of catchall separator) to account and address.
|
// case-insensitive, stripped of catchall separator) to account and address.
|
||||||
// Domains are IDNA names in utf8.
|
// Domains are IDNA names in utf8.
|
||||||
accountDestinations map[string]AccountDestination
|
accountDestinations map[string]AccountDestination
|
||||||
|
// Like accountDestinations, but for aliases.
|
||||||
|
aliases map[string]config.Alias
|
||||||
}
|
}
|
||||||
|
|
||||||
type AccountDestination struct {
|
type AccountDestination struct {
|
||||||
|
@ -152,13 +154,14 @@ func (c *Config) withDynamicLock(fn func()) {
|
||||||
|
|
||||||
// must be called with dynamic lock held.
|
// must be called with dynamic lock held.
|
||||||
func (c *Config) loadDynamic() []error {
|
func (c *Config) loadDynamic() []error {
|
||||||
d, mtime, accDests, err := ParseDynamicConfig(context.Background(), pkglog, ConfigDynamicPath, c.Static)
|
d, mtime, accDests, aliases, err := ParseDynamicConfig(context.Background(), pkglog, ConfigDynamicPath, c.Static)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
c.Dynamic = d
|
c.Dynamic = d
|
||||||
c.dynamicMtime = mtime
|
c.dynamicMtime = mtime
|
||||||
c.accountDestinations = accDests
|
c.accountDestinations = accDests
|
||||||
|
c.aliases = aliases
|
||||||
c.allowACMEHosts(pkglog, true)
|
c.allowACMEHosts(pkglog, true)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -193,10 +196,12 @@ func (c *Config) Accounts() (l []string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// DomainLocalparts returns a mapping of encoded localparts to account names for a
|
// DomainLocalparts returns a mapping of encoded localparts to account names for a
|
||||||
// domain. An empty localpart is a catchall destination for a domain.
|
// domain, and encoded localparts to aliases. An empty localpart is a catchall
|
||||||
func (c *Config) DomainLocalparts(d dns.Domain) map[string]string {
|
// destination for a domain.
|
||||||
|
func (c *Config) DomainLocalparts(d dns.Domain) (map[string]string, map[string]config.Alias) {
|
||||||
suffix := "@" + d.Name()
|
suffix := "@" + d.Name()
|
||||||
m := map[string]string{}
|
m := map[string]string{}
|
||||||
|
aliases := map[string]config.Alias{}
|
||||||
c.withDynamicLock(func() {
|
c.withDynamicLock(func() {
|
||||||
for addr, ad := range c.accountDestinations {
|
for addr, ad := range c.accountDestinations {
|
||||||
if strings.HasSuffix(addr, suffix) {
|
if strings.HasSuffix(addr, suffix) {
|
||||||
|
@ -207,8 +212,13 @@ func (c *Config) DomainLocalparts(d dns.Domain) map[string]string {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for addr, a := range c.aliases {
|
||||||
|
if strings.HasSuffix(addr, suffix) {
|
||||||
|
aliases[a.LocalpartStr] = a
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
return m
|
return m, aliases
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Config) Domain(d dns.Domain) (dom config.Domain, ok bool) {
|
func (c *Config) Domain(d dns.Domain) (dom config.Domain, ok bool) {
|
||||||
|
@ -225,9 +235,16 @@ func (c *Config) Account(name string) (acc config.Account, ok bool) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Config) AccountDestination(addr string) (accDests AccountDestination, ok bool) {
|
func (c *Config) AccountDestination(addr string) (accDest AccountDestination, alias *config.Alias, ok bool) {
|
||||||
c.withDynamicLock(func() {
|
c.withDynamicLock(func() {
|
||||||
accDests, ok = c.accountDestinations[addr]
|
accDest, ok = c.accountDestinations[addr]
|
||||||
|
if !ok {
|
||||||
|
var a config.Alias
|
||||||
|
a, ok = c.aliases[addr]
|
||||||
|
if ok {
|
||||||
|
alias = &a
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -314,7 +331,7 @@ func (c *Config) allowACMEHosts(log mlog.Log, checkACMEHosts bool) {
|
||||||
// must be called with lock held.
|
// must be called with lock held.
|
||||||
// Returns ErrConfig if the configuration is not valid.
|
// Returns ErrConfig if the configuration is not valid.
|
||||||
func writeDynamic(ctx context.Context, log mlog.Log, c config.Dynamic) error {
|
func writeDynamic(ctx context.Context, log mlog.Log, c config.Dynamic) error {
|
||||||
accDests, errs := prepareDynamicConfig(ctx, log, ConfigDynamicPath, Conf.Static, &c)
|
accDests, aliases, errs := prepareDynamicConfig(ctx, log, ConfigDynamicPath, Conf.Static, &c)
|
||||||
if len(errs) > 0 {
|
if len(errs) > 0 {
|
||||||
return fmt.Errorf("%w: %v", ErrConfig, errs[0])
|
return fmt.Errorf("%w: %v", ErrConfig, errs[0])
|
||||||
}
|
}
|
||||||
|
@ -362,6 +379,7 @@ func writeDynamic(ctx context.Context, log mlog.Log, c config.Dynamic) error {
|
||||||
Conf.DynamicLastCheck = time.Now()
|
Conf.DynamicLastCheck = time.Now()
|
||||||
Conf.Dynamic = c
|
Conf.Dynamic = c
|
||||||
Conf.accountDestinations = accDests
|
Conf.accountDestinations = accDests
|
||||||
|
Conf.aliases = aliases
|
||||||
|
|
||||||
Conf.allowACMEHosts(log, true)
|
Conf.allowACMEHosts(log, true)
|
||||||
|
|
||||||
|
@ -401,7 +419,7 @@ func LoadConfig(ctx context.Context, log mlog.Log, doLoadTLSKeyCerts, checkACMEH
|
||||||
// SetConfig sets a new config. Not to be used during normal operation.
|
// SetConfig sets a new config. Not to be used during normal operation.
|
||||||
func SetConfig(c *Config) {
|
func SetConfig(c *Config) {
|
||||||
// Cannot just assign *c to Conf, it would copy the mutex.
|
// Cannot just assign *c to Conf, it would copy the mutex.
|
||||||
Conf = Config{c.Static, sync.Mutex{}, c.Log, sync.Mutex{}, c.Dynamic, c.dynamicMtime, c.DynamicLastCheck, c.accountDestinations}
|
Conf = Config{c.Static, sync.Mutex{}, c.Log, sync.Mutex{}, c.Dynamic, c.dynamicMtime, c.DynamicLastCheck, c.accountDestinations, c.aliases}
|
||||||
|
|
||||||
// If we have non-standard CA roots, use them for all HTTPS requests.
|
// If we have non-standard CA roots, use them for all HTTPS requests.
|
||||||
if Conf.Static.TLS.CertPool != nil {
|
if Conf.Static.TLS.CertPool != nil {
|
||||||
|
@ -452,7 +470,7 @@ func ParseConfig(ctx context.Context, log mlog.Log, p string, checkOnly, doLoadT
|
||||||
}
|
}
|
||||||
|
|
||||||
pp := filepath.Join(filepath.Dir(p), "domains.conf")
|
pp := filepath.Join(filepath.Dir(p), "domains.conf")
|
||||||
c.Dynamic, c.dynamicMtime, c.accountDestinations, errs = ParseDynamicConfig(ctx, log, pp, c.Static)
|
c.Dynamic, c.dynamicMtime, c.accountDestinations, c.aliases, errs = ParseDynamicConfig(ctx, log, pp, c.Static)
|
||||||
|
|
||||||
if !checkOnly {
|
if !checkOnly {
|
||||||
c.allowACMEHosts(log, checkACMEHosts)
|
c.allowACMEHosts(log, checkACMEHosts)
|
||||||
|
@ -992,7 +1010,7 @@ func PrepareStaticConfig(ctx context.Context, log mlog.Log, configFile string, c
|
||||||
}
|
}
|
||||||
|
|
||||||
// PrepareDynamicConfig parses the dynamic config file given a static file.
|
// PrepareDynamicConfig parses the dynamic config file given a static file.
|
||||||
func ParseDynamicConfig(ctx context.Context, log mlog.Log, dynamicPath string, static config.Static) (c config.Dynamic, mtime time.Time, accDests map[string]AccountDestination, errs []error) {
|
func ParseDynamicConfig(ctx context.Context, log mlog.Log, dynamicPath string, static config.Static) (c config.Dynamic, mtime time.Time, accDests map[string]AccountDestination, aliases map[string]config.Alias, errs []error) {
|
||||||
addErrorf := func(format string, args ...any) {
|
addErrorf := func(format string, args ...any) {
|
||||||
errs = append(errs, fmt.Errorf(format, args...))
|
errs = append(errs, fmt.Errorf(format, args...))
|
||||||
}
|
}
|
||||||
|
@ -1012,11 +1030,11 @@ func ParseDynamicConfig(ctx context.Context, log mlog.Log, dynamicPath string, s
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
accDests, errs = prepareDynamicConfig(ctx, log, dynamicPath, static, &c)
|
accDests, aliases, errs = prepareDynamicConfig(ctx, log, dynamicPath, static, &c)
|
||||||
return c, fi.ModTime(), accDests, errs
|
return c, fi.ModTime(), accDests, aliases, errs
|
||||||
}
|
}
|
||||||
|
|
||||||
func prepareDynamicConfig(ctx context.Context, log mlog.Log, dynamicPath string, static config.Static, c *config.Dynamic) (accDests map[string]AccountDestination, errs []error) {
|
func prepareDynamicConfig(ctx context.Context, log mlog.Log, dynamicPath string, static config.Static, c *config.Dynamic) (accDests map[string]AccountDestination, aliases map[string]config.Alias, errs []error) {
|
||||||
addErrorf := func(format string, args ...any) {
|
addErrorf := func(format string, args ...any) {
|
||||||
errs = append(errs, fmt.Errorf(format, args...))
|
errs = append(errs, fmt.Errorf(format, args...))
|
||||||
}
|
}
|
||||||
|
@ -1037,6 +1055,7 @@ func prepareDynamicConfig(ctx context.Context, log mlog.Log, dynamicPath string,
|
||||||
checkMailboxNormf(static.Postmaster.Mailbox, "postmaster mailbox")
|
checkMailboxNormf(static.Postmaster.Mailbox, "postmaster mailbox")
|
||||||
|
|
||||||
accDests = map[string]AccountDestination{}
|
accDests = map[string]AccountDestination{}
|
||||||
|
aliases = map[string]config.Alias{}
|
||||||
|
|
||||||
// Validate host TLSRPT account/address.
|
// Validate host TLSRPT account/address.
|
||||||
if static.HostTLSRPT.Account != "" {
|
if static.HostTLSRPT.Account != "" {
|
||||||
|
@ -1287,6 +1306,9 @@ func prepareDynamicConfig(ctx context.Context, log mlog.Log, dynamicPath string,
|
||||||
acc.ParsedFromIDLoginAddresses[i] = a
|
acc.ParsedFromIDLoginAddresses[i] = a
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clear any previously derived state.
|
||||||
|
acc.Aliases = nil
|
||||||
|
|
||||||
c.Accounts[accName] = acc
|
c.Accounts[accName] = acc
|
||||||
|
|
||||||
if acc.OutgoingWebhook != nil {
|
if acc.OutgoingWebhook != nil {
|
||||||
|
@ -1445,9 +1467,8 @@ func prepareDynamicConfig(ctx context.Context, log mlog.Log, dynamicPath string,
|
||||||
origLP := address.Localpart
|
origLP := address.Localpart
|
||||||
dc := c.Domains[address.Domain.Name()]
|
dc := c.Domains[address.Domain.Name()]
|
||||||
domainHasAddress[address.Domain.Name()] = true
|
domainHasAddress[address.Domain.Name()] = true
|
||||||
if lp, err := CanonicalLocalpart(address.Localpart, dc); err != nil {
|
lp := CanonicalLocalpart(address.Localpart, dc)
|
||||||
addErrorf("canonicalizing localpart %s: %v", address.Localpart, err)
|
if dc.LocalpartCatchallSeparator != "" && strings.Contains(string(address.Localpart), dc.LocalpartCatchallSeparator) {
|
||||||
} else if dc.LocalpartCatchallSeparator != "" && strings.Contains(string(address.Localpart), dc.LocalpartCatchallSeparator) {
|
|
||||||
addErrorf("localpart of address %s includes domain catchall separator %s", address, dc.LocalpartCatchallSeparator)
|
addErrorf("localpart of address %s includes domain catchall separator %s", address, dc.LocalpartCatchallSeparator)
|
||||||
} else {
|
} else {
|
||||||
address.Localpart = lp
|
address.Localpart = lp
|
||||||
|
@ -1481,12 +1502,7 @@ func prepareDynamicConfig(ctx context.Context, log mlog.Log, dynamicPath string,
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
dc := c.Domains[a.Domain.Name()]
|
dc := c.Domains[a.Domain.Name()]
|
||||||
lp, err := CanonicalLocalpart(a.Localpart, dc)
|
a.Localpart = CanonicalLocalpart(a.Localpart, dc)
|
||||||
if err != nil {
|
|
||||||
addErrorf("canonicalizing localpart for fromid login address %q in account %q: %v", acc.FromIDLoginAddresses[i], accName, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
a.Localpart = lp
|
|
||||||
if _, ok := accDests[a.Pack(true)]; !ok {
|
if _, ok := accDests[a.Pack(true)]; !ok {
|
||||||
addErrorf("fromid login address %q for account %q does not match its destination addresses", acc.FromIDLoginAddresses[i], accName)
|
addErrorf("fromid login address %q for account %q does not match its destination addresses", acc.FromIDLoginAddresses[i], accName)
|
||||||
}
|
}
|
||||||
|
@ -1587,6 +1603,86 @@ func prepareDynamicConfig(ctx context.Context, log mlog.Log, dynamicPath string,
|
||||||
c.Domains[d] = domain
|
c.Domains[d] = domain
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Aliases, per domain. Also add references to accounts.
|
||||||
|
for d, domain := range c.Domains {
|
||||||
|
for lpstr, a := range domain.Aliases {
|
||||||
|
var err error
|
||||||
|
a.LocalpartStr = lpstr
|
||||||
|
var clp smtp.Localpart
|
||||||
|
lp, err := smtp.ParseLocalpart(lpstr)
|
||||||
|
if err != nil {
|
||||||
|
addErrorf("domain %q: parsing localpart %q for alias: %v", d, lpstr, err)
|
||||||
|
continue
|
||||||
|
} else if domain.LocalpartCatchallSeparator != "" && strings.Contains(string(lp), domain.LocalpartCatchallSeparator) {
|
||||||
|
addErrorf("domain %q: alias %q contains localpart catchall separator", d, a.LocalpartStr)
|
||||||
|
continue
|
||||||
|
} else {
|
||||||
|
clp = CanonicalLocalpart(lp, domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
addr := smtp.NewAddress(clp, domain.Domain).Pack(true)
|
||||||
|
if _, ok := aliases[addr]; ok {
|
||||||
|
addErrorf("domain %q: duplicate alias address %q", d, addr)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := accDests[addr]; ok {
|
||||||
|
addErrorf("domain %q: alias %q already present as regular address", d, addr)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if len(a.Addresses) == 0 {
|
||||||
|
// Not currently possible, Addresses isn't optional.
|
||||||
|
addErrorf("domain %q: alias %q needs at least one destination address", d, addr)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
a.ParsedAddresses = make([]config.AliasAddress, 0, len(a.Addresses))
|
||||||
|
seen := map[string]bool{}
|
||||||
|
for _, destAddr := range a.Addresses {
|
||||||
|
da, err := smtp.ParseAddress(destAddr)
|
||||||
|
if err != nil {
|
||||||
|
addErrorf("domain %q: parsing destination address %q in alias %q: %v", d, destAddr, addr, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
dastr := da.Pack(true)
|
||||||
|
accDest, ok := accDests[dastr]
|
||||||
|
if !ok {
|
||||||
|
addErrorf("domain %q: alias %q references non-existent address %q", d, addr, destAddr)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if seen[dastr] {
|
||||||
|
addErrorf("domain %q: alias %q has duplicate address %q", d, addr, destAddr)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[dastr] = true
|
||||||
|
aa := config.AliasAddress{Address: da, AccountName: accDest.Account, Destination: accDest.Destination}
|
||||||
|
a.ParsedAddresses = append(a.ParsedAddresses, aa)
|
||||||
|
}
|
||||||
|
a.Domain = domain.Domain
|
||||||
|
c.Domains[d].Aliases[lpstr] = a
|
||||||
|
aliases[addr] = a
|
||||||
|
|
||||||
|
for _, aa := range a.ParsedAddresses {
|
||||||
|
acc := c.Accounts[aa.AccountName]
|
||||||
|
var addrs []string
|
||||||
|
if a.ListMembers {
|
||||||
|
addrs = make([]string, len(a.ParsedAddresses))
|
||||||
|
for i := range a.ParsedAddresses {
|
||||||
|
addrs[i] = a.ParsedAddresses[i].Address.Pack(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Keep the non-sensitive fields.
|
||||||
|
accAlias := config.Alias{
|
||||||
|
PostPublic: a.PostPublic,
|
||||||
|
ListMembers: a.ListMembers,
|
||||||
|
AllowMsgFrom: a.AllowMsgFrom,
|
||||||
|
LocalpartStr: a.LocalpartStr,
|
||||||
|
Domain: a.Domain,
|
||||||
|
}
|
||||||
|
acc.Aliases = append(acc.Aliases, config.AddressAlias{SubscriptionAddress: aa.Address.Pack(true), Alias: accAlias, MemberAddresses: addrs})
|
||||||
|
c.Accounts[aa.AccountName] = acc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Check webserver configs.
|
// Check webserver configs.
|
||||||
if (len(c.WebDomainRedirects) > 0 || len(c.WebHandlers) > 0) && !haveWebserverListener {
|
if (len(c.WebDomainRedirects) > 0 || len(c.WebHandlers) > 0) && !haveWebserverListener {
|
||||||
addErrorf("WebDomainRedirects or WebHandlers configured but no listener with WebserverHTTP or WebserverHTTPS enabled")
|
addErrorf("WebDomainRedirects or WebHandlers configured but no listener with WebserverHTTP or WebserverHTTPS enabled")
|
||||||
|
|
|
@ -2,7 +2,6 @@ package mox
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/mjl-/mox/config"
|
"github.com/mjl-/mox/config"
|
||||||
|
@ -12,13 +11,13 @@ import (
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrDomainNotFound = errors.New("domain not found")
|
ErrDomainNotFound = errors.New("domain not found")
|
||||||
ErrAccountNotFound = errors.New("account not found")
|
ErrAddressNotFound = errors.New("address not found")
|
||||||
)
|
)
|
||||||
|
|
||||||
// FindAccount looks up the account for localpart and domain.
|
// FindAccount looks up the account for localpart and domain.
|
||||||
//
|
//
|
||||||
// Can return ErrDomainNotFound and ErrAccountNotFound.
|
// Can return ErrDomainNotFound and ErrAddressNotFound.
|
||||||
func FindAccount(localpart smtp.Localpart, domain dns.Domain, allowPostmaster bool) (accountName string, canonicalAddress string, dest config.Destination, rerr error) {
|
func LookupAddress(localpart smtp.Localpart, domain dns.Domain, allowPostmaster, allowAlias bool) (accountName string, alias *config.Alias, canonicalAddress string, dest config.Destination, rerr error) {
|
||||||
if strings.EqualFold(string(localpart), "postmaster") {
|
if strings.EqualFold(string(localpart), "postmaster") {
|
||||||
localpart = "postmaster"
|
localpart = "postmaster"
|
||||||
}
|
}
|
||||||
|
@ -39,49 +38,48 @@ func FindAccount(localpart smtp.Localpart, domain dns.Domain, allowPostmaster bo
|
||||||
// Check for special mail host addresses.
|
// Check for special mail host addresses.
|
||||||
if localpart == "postmaster" && postmasterDomain() {
|
if localpart == "postmaster" && postmasterDomain() {
|
||||||
if !allowPostmaster {
|
if !allowPostmaster {
|
||||||
return "", "", config.Destination{}, ErrAccountNotFound
|
return "", nil, "", config.Destination{}, ErrAddressNotFound
|
||||||
}
|
}
|
||||||
return Conf.Static.Postmaster.Account, "postmaster", config.Destination{Mailbox: Conf.Static.Postmaster.Mailbox}, nil
|
return Conf.Static.Postmaster.Account, nil, "postmaster", config.Destination{Mailbox: Conf.Static.Postmaster.Mailbox}, nil
|
||||||
}
|
}
|
||||||
if localpart == Conf.Static.HostTLSRPT.ParsedLocalpart && domain == Conf.Static.HostnameDomain {
|
if localpart == Conf.Static.HostTLSRPT.ParsedLocalpart && domain == Conf.Static.HostnameDomain {
|
||||||
// Get destination, should always be present.
|
// Get destination, should always be present.
|
||||||
canonical := smtp.NewAddress(localpart, domain).String()
|
canonical := smtp.NewAddress(localpart, domain).String()
|
||||||
accAddr, ok := Conf.AccountDestination(canonical)
|
accAddr, a, ok := Conf.AccountDestination(canonical)
|
||||||
if !ok {
|
if !ok || a != nil {
|
||||||
return "", "", config.Destination{}, ErrAccountNotFound
|
return "", nil, "", config.Destination{}, ErrAddressNotFound
|
||||||
}
|
}
|
||||||
return accAddr.Account, canonical, accAddr.Destination, nil
|
return accAddr.Account, nil, canonical, accAddr.Destination, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
d, ok := Conf.Domain(domain)
|
d, ok := Conf.Domain(domain)
|
||||||
if !ok || d.ReportsOnly {
|
if !ok || d.ReportsOnly {
|
||||||
// For ReportsOnly, we also return ErrDomainNotFound, so this domain isn't
|
// For ReportsOnly, we also return ErrDomainNotFound, so this domain isn't
|
||||||
// considered local/authoritative during delivery.
|
// considered local/authoritative during delivery.
|
||||||
return "", "", config.Destination{}, ErrDomainNotFound
|
return "", nil, "", config.Destination{}, ErrDomainNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
localpart, err := CanonicalLocalpart(localpart, d)
|
localpart = CanonicalLocalpart(localpart, d)
|
||||||
if err != nil {
|
|
||||||
return "", "", config.Destination{}, fmt.Errorf("%w: %s", ErrAccountNotFound, err)
|
|
||||||
}
|
|
||||||
canonical := smtp.NewAddress(localpart, domain).String()
|
canonical := smtp.NewAddress(localpart, domain).String()
|
||||||
|
|
||||||
accAddr, ok := Conf.AccountDestination(canonical)
|
accAddr, alias, ok := Conf.AccountDestination(canonical)
|
||||||
if !ok {
|
if ok && alias != nil && allowAlias {
|
||||||
if accAddr, ok = Conf.AccountDestination("@" + domain.Name()); !ok {
|
return "", alias, canonical, config.Destination{}, nil
|
||||||
|
} else if !ok {
|
||||||
|
if accAddr, alias, ok = Conf.AccountDestination("@" + domain.Name()); !ok || alias != nil {
|
||||||
if localpart == "postmaster" && allowPostmaster {
|
if localpart == "postmaster" && allowPostmaster {
|
||||||
return Conf.Static.Postmaster.Account, "postmaster", config.Destination{Mailbox: Conf.Static.Postmaster.Mailbox}, nil
|
return Conf.Static.Postmaster.Account, nil, "postmaster", config.Destination{Mailbox: Conf.Static.Postmaster.Mailbox}, nil
|
||||||
}
|
}
|
||||||
return "", "", config.Destination{}, ErrAccountNotFound
|
return "", nil, "", config.Destination{}, ErrAddressNotFound
|
||||||
}
|
}
|
||||||
canonical = "@" + domain.Name()
|
canonical = "@" + domain.Name()
|
||||||
}
|
}
|
||||||
return accAddr.Account, canonical, accAddr.Destination, nil
|
return accAddr.Account, nil, canonical, accAddr.Destination, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CanonicalLocalpart returns the canonical localpart, removing optional catchall
|
// CanonicalLocalpart returns the canonical localpart, removing optional catchall
|
||||||
// separator, and optionally lower-casing the string.
|
// separator, and optionally lower-casing the string.
|
||||||
func CanonicalLocalpart(localpart smtp.Localpart, d config.Domain) (smtp.Localpart, error) {
|
func CanonicalLocalpart(localpart smtp.Localpart, d config.Domain) smtp.Localpart {
|
||||||
if d.LocalpartCatchallSeparator != "" {
|
if d.LocalpartCatchallSeparator != "" {
|
||||||
t := strings.SplitN(string(localpart), d.LocalpartCatchallSeparator, 2)
|
t := strings.SplitN(string(localpart), d.LocalpartCatchallSeparator, 2)
|
||||||
localpart = smtp.Localpart(t[0])
|
localpart = smtp.Localpart(t[0])
|
||||||
|
@ -90,5 +88,24 @@ func CanonicalLocalpart(localpart smtp.Localpart, d config.Domain) (smtp.Localpa
|
||||||
if !d.LocalpartCaseSensitive {
|
if !d.LocalpartCaseSensitive {
|
||||||
localpart = smtp.Localpart(strings.ToLower(string(localpart)))
|
localpart = smtp.Localpart(strings.ToLower(string(localpart)))
|
||||||
}
|
}
|
||||||
return localpart, nil
|
return localpart
|
||||||
|
}
|
||||||
|
|
||||||
|
// AllowMsgFrom returns whether account is allowed to submit messages with address
|
||||||
|
// as message From header, based on configured addresses and membership of aliases
|
||||||
|
// that allow using its address.
|
||||||
|
func AllowMsgFrom(accountName string, msgFrom smtp.Address) bool {
|
||||||
|
accName, alias, _, _, err := LookupAddress(msgFrom.Localpart, msgFrom.Domain, false, true)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if alias != nil && alias.AllowMsgFrom {
|
||||||
|
for _, aa := range alias.ParsedAddresses {
|
||||||
|
if aa.AccountName == accountName {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return accName == accountName
|
||||||
}
|
}
|
||||||
|
|
292
smtpserver/alias_test.go
Normal file
292
smtpserver/alias_test.go
Normal file
|
@ -0,0 +1,292 @@
|
||||||
|
package smtpserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/mjl-/bstore"
|
||||||
|
|
||||||
|
"github.com/mjl-/mox/dns"
|
||||||
|
"github.com/mjl-/mox/smtp"
|
||||||
|
"github.com/mjl-/mox/smtpclient"
|
||||||
|
"github.com/mjl-/mox/store"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Check user can submit message with message From address they are member of.
|
||||||
|
func TestAliasSubmitMsgFrom(t *testing.T) {
|
||||||
|
ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), dns.MockResolver{})
|
||||||
|
defer ts.close()
|
||||||
|
|
||||||
|
ts.submission = true
|
||||||
|
ts.user = "mjl@mox.example"
|
||||||
|
ts.pass = password0
|
||||||
|
|
||||||
|
var msg = strings.ReplaceAll(`From: <public@mox.example>
|
||||||
|
To: <public@mox.example>
|
||||||
|
Subject: test
|
||||||
|
|
||||||
|
test email
|
||||||
|
`, "\n", "\r\n")
|
||||||
|
|
||||||
|
ts.run(func(err error, client *smtpclient.Client) {
|
||||||
|
t.Helper()
|
||||||
|
mailFrom := "mjl@mox.example"
|
||||||
|
rcptTo := "public@mox.example"
|
||||||
|
if err == nil {
|
||||||
|
err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), false, false, false)
|
||||||
|
}
|
||||||
|
ts.smtperr(err, nil)
|
||||||
|
})
|
||||||
|
|
||||||
|
msg = strings.ReplaceAll(`From: <private@mox.example>
|
||||||
|
To: <private@mox.example>
|
||||||
|
Subject: test
|
||||||
|
|
||||||
|
test email
|
||||||
|
`, "\n", "\r\n")
|
||||||
|
|
||||||
|
ts.run(func(err error, client *smtpclient.Client) {
|
||||||
|
t.Helper()
|
||||||
|
mailFrom := "mjl@mox.example"
|
||||||
|
rcptTo := "private@mox.example"
|
||||||
|
if err == nil {
|
||||||
|
err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), false, false, false)
|
||||||
|
}
|
||||||
|
ts.smtperr(err, &smtpclient.Error{Permanent: true, Code: smtp.C550MailboxUnavail, Secode: smtp.SePol7DeliveryUnauth1})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Non-member cannot submit as alias that allows it for members.
|
||||||
|
func TestAliasSubmitMsgFromDenied(t *testing.T) {
|
||||||
|
ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), dns.MockResolver{})
|
||||||
|
defer ts.close()
|
||||||
|
|
||||||
|
acc, err := store.OpenAccount(pkglog, "☺")
|
||||||
|
tcheck(t, err, "open account")
|
||||||
|
err = acc.SetPassword(pkglog, password0)
|
||||||
|
tcheck(t, err, "set password")
|
||||||
|
err = acc.Close()
|
||||||
|
tcheck(t, err, "close account")
|
||||||
|
acc.CheckClosed()
|
||||||
|
|
||||||
|
ts.submission = true
|
||||||
|
ts.user = "☺@mox.example"
|
||||||
|
ts.pass = password0
|
||||||
|
|
||||||
|
var msg = strings.ReplaceAll(`From: <public@mox.example>
|
||||||
|
To: <public@mox.example>
|
||||||
|
Subject: test
|
||||||
|
|
||||||
|
test email
|
||||||
|
`, "\n", "\r\n")
|
||||||
|
|
||||||
|
ts.run(func(err error, client *smtpclient.Client) {
|
||||||
|
t.Helper()
|
||||||
|
mailFrom := "☺@mox.example"
|
||||||
|
rcptTo := "public@mox.example"
|
||||||
|
if err == nil {
|
||||||
|
err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), true, true, false)
|
||||||
|
}
|
||||||
|
ts.smtperr(err, &smtpclient.Error{Permanent: true, Code: smtp.C550MailboxUnavail, Secode: smtp.SePol7DeliveryUnauth1})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Non-member can deliver to public list, not to private list.
|
||||||
|
func TestAliasDeliverNonMember(t *testing.T) {
|
||||||
|
resolver := dns.MockResolver{
|
||||||
|
A: map[string][]string{
|
||||||
|
"example.org.": {"127.0.0.10"}, // For mx check.
|
||||||
|
},
|
||||||
|
PTR: map[string][]string{
|
||||||
|
"127.0.0.10": {"example.org."}, // To get passed junk filter.
|
||||||
|
},
|
||||||
|
}
|
||||||
|
ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), resolver)
|
||||||
|
defer ts.close()
|
||||||
|
|
||||||
|
var msg = strings.ReplaceAll(`From: <other@example.org>
|
||||||
|
To: <private@mox.example>
|
||||||
|
|
||||||
|
test email
|
||||||
|
`, "\n", "\r\n")
|
||||||
|
|
||||||
|
ts.run(func(err error, client *smtpclient.Client) {
|
||||||
|
t.Helper()
|
||||||
|
mailFrom := "other@example.org"
|
||||||
|
rcptTo := "private@mox.example"
|
||||||
|
if err == nil {
|
||||||
|
err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), false, false, false)
|
||||||
|
}
|
||||||
|
ts.smtperr(err, &smtpclient.Error{Permanent: true, Code: smtp.C550MailboxUnavail, Secode: smtp.SePol7ExpnProhibited2})
|
||||||
|
})
|
||||||
|
|
||||||
|
msg = strings.ReplaceAll(`From: <private@mox.example>
|
||||||
|
To: <private@mox.example>
|
||||||
|
|
||||||
|
test email
|
||||||
|
`, "\n", "\r\n")
|
||||||
|
|
||||||
|
ts.run(func(err error, client *smtpclient.Client) {
|
||||||
|
t.Helper()
|
||||||
|
mailFrom := "private@example.org"
|
||||||
|
rcptTo := "private@mox.example"
|
||||||
|
if err == nil {
|
||||||
|
err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), false, false, false)
|
||||||
|
}
|
||||||
|
ts.smtperr(err, &smtpclient.Error{Permanent: true, Code: smtp.C550MailboxUnavail, Secode: smtp.SePol7ExpnProhibited2})
|
||||||
|
})
|
||||||
|
|
||||||
|
msg = strings.ReplaceAll(`From: <other@example.org>
|
||||||
|
To: <public@mox.example>
|
||||||
|
Subject: test
|
||||||
|
|
||||||
|
test email
|
||||||
|
`, "\n", "\r\n")
|
||||||
|
|
||||||
|
ts.run(func(err error, client *smtpclient.Client) {
|
||||||
|
t.Helper()
|
||||||
|
mailFrom := "other@example.org"
|
||||||
|
rcptTo := "public@mox.example"
|
||||||
|
if err == nil {
|
||||||
|
err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), false, false, false)
|
||||||
|
}
|
||||||
|
ts.smtperr(err, nil)
|
||||||
|
|
||||||
|
ts.checkCount("Inbox", 2) // Receiving for both mjl@ and móx@.
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Member can deliver to private list, but still not with alias address as message
|
||||||
|
// from. Message with alias from address as message from is allowed.
|
||||||
|
func TestAliasDeliverMember(t *testing.T) {
|
||||||
|
resolver := dns.MockResolver{
|
||||||
|
A: map[string][]string{
|
||||||
|
"mox.example.": {"127.0.0.10"}, // For mx check.
|
||||||
|
},
|
||||||
|
PTR: map[string][]string{
|
||||||
|
"127.0.0.10": {"mox.example."}, // To get passed junk filter.
|
||||||
|
},
|
||||||
|
TXT: map[string][]string{
|
||||||
|
"mox.example.": {"v=spf1 ip4:127.0.0.10 -all"}, // To allow multiple recipients in transaction.
|
||||||
|
},
|
||||||
|
}
|
||||||
|
ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), resolver)
|
||||||
|
defer ts.close()
|
||||||
|
|
||||||
|
var msg = strings.ReplaceAll(`From: <mjl@mox.example>
|
||||||
|
To: <private@mox.example>
|
||||||
|
|
||||||
|
test email
|
||||||
|
`, "\n", "\r\n")
|
||||||
|
|
||||||
|
ts.run(func(err error, client *smtpclient.Client) {
|
||||||
|
t.Helper()
|
||||||
|
mailFrom := "mjl@mox.example"
|
||||||
|
rcptTo := []string{"private@mox.example", "móx@mox.example"}
|
||||||
|
if err == nil {
|
||||||
|
_, err = client.DeliverMultiple(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), true, true, false)
|
||||||
|
// assuming there wasn't a per-recipient error
|
||||||
|
}
|
||||||
|
ts.smtperr(err, nil)
|
||||||
|
|
||||||
|
ts.checkCount("Inbox", 0) // Not receiving for mjl@ due to msgfrom, and not móx@ due to rcpt to.
|
||||||
|
})
|
||||||
|
|
||||||
|
ts.run(func(err error, client *smtpclient.Client) {
|
||||||
|
t.Helper()
|
||||||
|
mailFrom := "mjl@mox.example"
|
||||||
|
rcptTo := "private@mox.example"
|
||||||
|
if err == nil {
|
||||||
|
err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), false, false, false)
|
||||||
|
}
|
||||||
|
ts.smtperr(err, nil)
|
||||||
|
|
||||||
|
ts.checkCount("Inbox", 1) // Only receiving for móx@mox.example, not mjl@.
|
||||||
|
})
|
||||||
|
|
||||||
|
msg = strings.ReplaceAll(`From: <private@mox.example>
|
||||||
|
To: <private@mox.example>
|
||||||
|
Subject: test
|
||||||
|
|
||||||
|
test email
|
||||||
|
`, "\n", "\r\n")
|
||||||
|
|
||||||
|
ts.run(func(err error, client *smtpclient.Client) {
|
||||||
|
t.Helper()
|
||||||
|
mailFrom := "other@mox.example"
|
||||||
|
rcptTo := "private@mox.example"
|
||||||
|
if err == nil {
|
||||||
|
err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), false, false, false)
|
||||||
|
}
|
||||||
|
ts.smtperr(err, &smtpclient.Error{Permanent: true, Code: smtp.C550MailboxUnavail, Secode: smtp.SePol7ExpnProhibited2})
|
||||||
|
})
|
||||||
|
|
||||||
|
msg = strings.ReplaceAll(`From: <public@mox.example>
|
||||||
|
To: <public@mox.example>
|
||||||
|
Subject: test
|
||||||
|
|
||||||
|
test email
|
||||||
|
`, "\n", "\r\n")
|
||||||
|
|
||||||
|
ts.run(func(err error, client *smtpclient.Client) {
|
||||||
|
t.Helper()
|
||||||
|
mailFrom := "mjl@mox.example"
|
||||||
|
rcptTo := "public@mox.example"
|
||||||
|
if err == nil {
|
||||||
|
err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), false, false, false)
|
||||||
|
}
|
||||||
|
ts.smtperr(err, nil)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Message is rejected if no member accepts it.
|
||||||
|
func TestAliasDeliverReject(t *testing.T) {
|
||||||
|
resolver := dns.MockResolver{
|
||||||
|
A: map[string][]string{
|
||||||
|
"mox.example.": {"127.0.0.10"}, // For mx check.
|
||||||
|
},
|
||||||
|
PTR: map[string][]string{
|
||||||
|
"127.0.0.10": {"mox.example."}, // To get passed junk filter.
|
||||||
|
},
|
||||||
|
TXT: map[string][]string{
|
||||||
|
"mox.example.": {"v=spf1 ip4:127.0.0.10 -all"}, // To allow multiple recipients in transaction.
|
||||||
|
},
|
||||||
|
}
|
||||||
|
ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), resolver)
|
||||||
|
defer ts.close()
|
||||||
|
|
||||||
|
var msg = strings.ReplaceAll(`From: <mjl@mox.example>
|
||||||
|
To: <private@mox.example>
|
||||||
|
|
||||||
|
test email
|
||||||
|
`, "\n", "\r\n")
|
||||||
|
|
||||||
|
ts.run(func(err error, client *smtpclient.Client) {
|
||||||
|
t.Helper()
|
||||||
|
mailFrom := "mjl@mox.example"
|
||||||
|
rcptTo := "private@mox.example"
|
||||||
|
if err == nil {
|
||||||
|
err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), false, false, false)
|
||||||
|
}
|
||||||
|
ts.smtperr(err, nil)
|
||||||
|
|
||||||
|
ts.checkCount("Inbox", 1) // Only receiving for móx@mox.example, not mjl@.
|
||||||
|
})
|
||||||
|
|
||||||
|
// Mark message as junk.
|
||||||
|
q := bstore.QueryDB[store.Message](ctxbg, ts.acc.DB)
|
||||||
|
n, err := q.UpdateFields(map[string]any{"Junk": true})
|
||||||
|
tcheck(t, err, "mark as junk")
|
||||||
|
tcompare(t, n, 1)
|
||||||
|
|
||||||
|
ts.run(func(err error, client *smtpclient.Client) {
|
||||||
|
t.Helper()
|
||||||
|
mailFrom := "mjl@mox.example"
|
||||||
|
rcptTo := "private@mox.example"
|
||||||
|
if err == nil {
|
||||||
|
err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), false, false, false)
|
||||||
|
}
|
||||||
|
ts.smtperr(err, &smtpclient.Error{Code: smtp.C451LocalErr, Secode: smtp.SeSys3Other0})
|
||||||
|
})
|
||||||
|
}
|
|
@ -11,6 +11,7 @@ import (
|
||||||
|
|
||||||
"github.com/mjl-/bstore"
|
"github.com/mjl-/bstore"
|
||||||
|
|
||||||
|
"github.com/mjl-/mox/config"
|
||||||
"github.com/mjl-/mox/dkim"
|
"github.com/mjl-/mox/dkim"
|
||||||
"github.com/mjl-/mox/dmarc"
|
"github.com/mjl-/mox/dmarc"
|
||||||
"github.com/mjl-/mox/dmarcrpt"
|
"github.com/mjl-/mox/dmarcrpt"
|
||||||
|
@ -31,7 +32,10 @@ type delivery struct {
|
||||||
tls bool
|
tls bool
|
||||||
m *store.Message
|
m *store.Message
|
||||||
dataFile *os.File
|
dataFile *os.File
|
||||||
rcptAcc rcptAccount
|
smtpRcptTo smtp.Path // As used in SMTP, possibly address of alias.
|
||||||
|
deliverTo smtp.Path // To deliver to, either smtpRcptTo or an alias member address.
|
||||||
|
destination config.Destination
|
||||||
|
canonicalAddress string
|
||||||
acc *store.Account
|
acc *store.Account
|
||||||
msgTo []message.Address
|
msgTo []message.Address
|
||||||
msgCc []message.Address
|
msgCc []message.Address
|
||||||
|
@ -44,6 +48,7 @@ type delivery struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type analysis struct {
|
type analysis struct {
|
||||||
|
d delivery
|
||||||
accept bool
|
accept bool
|
||||||
mailbox string
|
mailbox string
|
||||||
code int
|
code int
|
||||||
|
@ -76,6 +81,7 @@ const (
|
||||||
reasonSubjectpass = "subjectpass"
|
reasonSubjectpass = "subjectpass"
|
||||||
reasonSubjectpassError = "subjectpass-error"
|
reasonSubjectpassError = "subjectpass-error"
|
||||||
reasonIPrev = "iprev" // No or mild junk reputation signals, and bad iprev.
|
reasonIPrev = "iprev" // No or mild junk reputation signals, and bad iprev.
|
||||||
|
reasonHighRate = "high-rate" // Too many messages, not added to rejects.
|
||||||
)
|
)
|
||||||
|
|
||||||
func isListDomain(d delivery, ld dns.Domain) bool {
|
func isListDomain(d delivery, ld dns.Domain) bool {
|
||||||
|
@ -93,14 +99,97 @@ func isListDomain(d delivery, ld dns.Domain) bool {
|
||||||
func analyze(ctx context.Context, log mlog.Log, resolver dns.Resolver, d delivery) analysis {
|
func analyze(ctx context.Context, log mlog.Log, resolver dns.Resolver, d delivery) analysis {
|
||||||
var headers string
|
var headers string
|
||||||
|
|
||||||
mailbox := d.rcptAcc.destination.Mailbox
|
// We don't want to let a single IP or network deliver too many messages to an
|
||||||
|
// account. They may fill up the mailbox, either with messages that have to be
|
||||||
|
// purged, or by filling the disk. We check both cases for IP's and networks.
|
||||||
|
var rateError bool // Whether returned error represents a rate error.
|
||||||
|
err := d.acc.DB.Read(ctx, func(tx *bstore.Tx) (retErr error) {
|
||||||
|
now := time.Now()
|
||||||
|
defer func() {
|
||||||
|
log.Debugx("checking message and size delivery rates", retErr, slog.Duration("duration", time.Since(now)))
|
||||||
|
}()
|
||||||
|
|
||||||
|
checkCount := func(msg store.Message, window time.Duration, limit int) {
|
||||||
|
if retErr != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
q := bstore.QueryTx[store.Message](tx)
|
||||||
|
q.FilterNonzero(msg)
|
||||||
|
q.FilterGreater("Received", now.Add(-window))
|
||||||
|
q.FilterEqual("Expunged", false)
|
||||||
|
n, err := q.Count()
|
||||||
|
if err != nil {
|
||||||
|
retErr = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if n >= limit {
|
||||||
|
rateError = true
|
||||||
|
retErr = fmt.Errorf("more than %d messages in past %s from your ip/network", limit, window)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkSize := func(msg store.Message, window time.Duration, limit int64) {
|
||||||
|
if retErr != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
q := bstore.QueryTx[store.Message](tx)
|
||||||
|
q.FilterNonzero(msg)
|
||||||
|
q.FilterGreater("Received", now.Add(-window))
|
||||||
|
q.FilterEqual("Expunged", false)
|
||||||
|
size := d.m.Size
|
||||||
|
err := q.ForEach(func(v store.Message) error {
|
||||||
|
size += v.Size
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
retErr = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if size > limit {
|
||||||
|
rateError = true
|
||||||
|
retErr = fmt.Errorf("more than %d bytes in past %s from your ip/network", limit, window)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo future: make these configurable
|
||||||
|
// todo: should we have a limit for forwarded messages? they are stored with empty RemoteIPMasked*
|
||||||
|
|
||||||
|
const day = 24 * time.Hour
|
||||||
|
checkCount(store.Message{RemoteIPMasked1: d.m.RemoteIPMasked1}, time.Minute, limitIPMasked1MessagesPerMinute)
|
||||||
|
checkCount(store.Message{RemoteIPMasked1: d.m.RemoteIPMasked1}, day, 20*500)
|
||||||
|
checkCount(store.Message{RemoteIPMasked2: d.m.RemoteIPMasked2}, time.Minute, 1500)
|
||||||
|
checkCount(store.Message{RemoteIPMasked2: d.m.RemoteIPMasked2}, day, 20*1500)
|
||||||
|
checkCount(store.Message{RemoteIPMasked3: d.m.RemoteIPMasked3}, time.Minute, 4500)
|
||||||
|
checkCount(store.Message{RemoteIPMasked3: d.m.RemoteIPMasked3}, day, 20*4500)
|
||||||
|
|
||||||
|
const MB = 1024 * 1024
|
||||||
|
checkSize(store.Message{RemoteIPMasked1: d.m.RemoteIPMasked1}, time.Minute, limitIPMasked1SizePerMinute)
|
||||||
|
checkSize(store.Message{RemoteIPMasked1: d.m.RemoteIPMasked1}, day, 3*1000*MB)
|
||||||
|
checkSize(store.Message{RemoteIPMasked2: d.m.RemoteIPMasked2}, time.Minute, 3000*MB)
|
||||||
|
checkSize(store.Message{RemoteIPMasked2: d.m.RemoteIPMasked2}, day, 3*3000*MB)
|
||||||
|
checkSize(store.Message{RemoteIPMasked3: d.m.RemoteIPMasked3}, time.Minute, 9000*MB)
|
||||||
|
checkSize(store.Message{RemoteIPMasked3: d.m.RemoteIPMasked3}, day, 3*9000*MB)
|
||||||
|
|
||||||
|
return retErr
|
||||||
|
})
|
||||||
|
if err != nil && !rateError {
|
||||||
|
log.Errorx("checking delivery rates", err)
|
||||||
|
metricDelivery.WithLabelValues("checkrates", "").Inc()
|
||||||
|
return analysis{d, false, "", smtp.C451LocalErr, smtp.SeSys3Other0, false, "error processing", err, nil, nil, reasonReputationError, "", headers}
|
||||||
|
} else if err != nil {
|
||||||
|
log.Debugx("refusing due to high delivery rate", err)
|
||||||
|
metricDelivery.WithLabelValues("highrate", "").Inc()
|
||||||
|
return analysis{d, false, "", smtp.C452StorageFull, smtp.SeMailbox2Full2, true, err.Error(), err, nil, nil, reasonHighRate, "", headers}
|
||||||
|
}
|
||||||
|
|
||||||
|
mailbox := d.destination.Mailbox
|
||||||
if mailbox == "" {
|
if mailbox == "" {
|
||||||
mailbox = "Inbox"
|
mailbox = "Inbox"
|
||||||
}
|
}
|
||||||
|
|
||||||
// If destination mailbox has a mailing list domain (for SPF/DKIM) configured,
|
// If destination mailbox has a mailing list domain (for SPF/DKIM) configured,
|
||||||
// check it for a pass.
|
// check it for a pass.
|
||||||
rs := store.MessageRuleset(log, d.rcptAcc.destination, d.m, d.m.MsgPrefix, d.dataFile)
|
rs := store.MessageRuleset(log, d.destination, d.m, d.m.MsgPrefix, d.dataFile)
|
||||||
if rs != nil {
|
if rs != nil {
|
||||||
mailbox = rs.Mailbox
|
mailbox = rs.Mailbox
|
||||||
}
|
}
|
||||||
|
@ -108,7 +197,7 @@ func analyze(ctx context.Context, log mlog.Log, resolver dns.Resolver, d deliver
|
||||||
// todo: on temporary failures, reject temporarily?
|
// todo: on temporary failures, reject temporarily?
|
||||||
if isListDomain(d, rs.ListAllowDNSDomain) {
|
if isListDomain(d, rs.ListAllowDNSDomain) {
|
||||||
d.m.IsMailingList = true
|
d.m.IsMailingList = true
|
||||||
return analysis{accept: true, mailbox: mailbox, reason: reasonListAllow, dmarcOverrideReason: string(dmarcrpt.PolicyOverrideMailingList), headers: headers}
|
return analysis{d: d, accept: true, mailbox: mailbox, reason: reasonListAllow, dmarcOverrideReason: string(dmarcrpt.PolicyOverrideMailingList), headers: headers}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -177,7 +266,7 @@ func analyze(ctx context.Context, log mlog.Log, resolver dns.Resolver, d deliver
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
if mberr != nil {
|
if mberr != nil {
|
||||||
return analysis{false, mailbox, smtp.C451LocalErr, smtp.SeSys3Other0, false, "error processing", err, nil, nil, reasonReputationError, dmarcOverrideReason, headers}
|
return analysis{d, false, mailbox, smtp.C451LocalErr, smtp.SeSys3Other0, false, "error processing", err, nil, nil, reasonReputationError, dmarcOverrideReason, headers}
|
||||||
}
|
}
|
||||||
d.m.MailboxID = 0 // We plan to reject, no need to set intended MailboxID.
|
d.m.MailboxID = 0 // We plan to reject, no need to set intended MailboxID.
|
||||||
}
|
}
|
||||||
|
@ -191,7 +280,7 @@ func analyze(ctx context.Context, log mlog.Log, resolver dns.Resolver, d deliver
|
||||||
d.m.Seen = true
|
d.m.Seen = true
|
||||||
log.Info("accepting reject to configured mailbox due to ruleset")
|
log.Info("accepting reject to configured mailbox due to ruleset")
|
||||||
}
|
}
|
||||||
return analysis{accept, mailbox, code, secode, err == nil, errmsg, err, nil, nil, reason, dmarcOverrideReason, headers}
|
return analysis{d, accept, mailbox, code, secode, err == nil, errmsg, err, nil, nil, reason, dmarcOverrideReason, headers}
|
||||||
}
|
}
|
||||||
|
|
||||||
if d.dmarcUse && d.dmarcResult.Reject {
|
if d.dmarcUse && d.dmarcResult.Reject {
|
||||||
|
@ -202,7 +291,7 @@ func analyze(ctx context.Context, log mlog.Log, resolver dns.Resolver, d deliver
|
||||||
// If destination is the DMARC reporting mailbox, do additional checks and keep
|
// If destination is the DMARC reporting mailbox, do additional checks and keep
|
||||||
// track of the report. We'll check reputation, defaulting to accept.
|
// track of the report. We'll check reputation, defaulting to accept.
|
||||||
var dmarcReport *dmarcrpt.Feedback
|
var dmarcReport *dmarcrpt.Feedback
|
||||||
if d.rcptAcc.destination.DMARCReports {
|
if d.destination.DMARCReports {
|
||||||
// Messages with DMARC aggregate reports must have a DMARC pass. ../rfc/7489:1866
|
// Messages with DMARC aggregate reports must have a DMARC pass. ../rfc/7489:1866
|
||||||
if d.dmarcResult.Status != dmarc.StatusPass {
|
if d.dmarcResult.Status != dmarc.StatusPass {
|
||||||
log.Info("received dmarc aggregate report without dmarc pass, not processing as dmarc report")
|
log.Info("received dmarc aggregate report without dmarc pass, not processing as dmarc report")
|
||||||
|
@ -227,7 +316,7 @@ func analyze(ctx context.Context, log mlog.Log, resolver dns.Resolver, d deliver
|
||||||
// Similar to DMARC reporting, we check for the required DKIM. We'll check
|
// Similar to DMARC reporting, we check for the required DKIM. We'll check
|
||||||
// reputation, defaulting to accept.
|
// reputation, defaulting to accept.
|
||||||
var tlsReport *tlsrpt.Report
|
var tlsReport *tlsrpt.Report
|
||||||
if d.rcptAcc.destination.HostTLSReports || d.rcptAcc.destination.DomainTLSReports {
|
if d.destination.HostTLSReports || d.destination.DomainTLSReports {
|
||||||
matchesDomain := func(sigDomain dns.Domain) bool {
|
matchesDomain := func(sigDomain dns.Domain) bool {
|
||||||
// RFC seems to require exact DKIM domain match with submitt and message From, we
|
// RFC seems to require exact DKIM domain match with submitt and message From, we
|
||||||
// also allow msgFrom to be subdomain. ../rfc/8460:322
|
// also allow msgFrom to be subdomain. ../rfc/8460:322
|
||||||
|
@ -286,7 +375,6 @@ func analyze(ctx context.Context, log mlog.Log, resolver dns.Resolver, d deliver
|
||||||
var conclusive bool
|
var conclusive bool
|
||||||
var method reputationMethod
|
var method reputationMethod
|
||||||
var reason string
|
var reason string
|
||||||
var err error
|
|
||||||
d.acc.WithRLock(func() {
|
d.acc.WithRLock(func() {
|
||||||
err = d.acc.DB.Read(ctx, func(tx *bstore.Tx) error {
|
err = d.acc.DB.Read(ctx, func(tx *bstore.Tx) error {
|
||||||
if err := assignMailbox(tx); err != nil {
|
if err := assignMailbox(tx); err != nil {
|
||||||
|
@ -308,12 +396,12 @@ func analyze(ctx context.Context, log mlog.Log, resolver dns.Resolver, d deliver
|
||||||
slog.String("method", string(method)))
|
slog.String("method", string(method)))
|
||||||
if conclusive {
|
if conclusive {
|
||||||
if !*isjunk {
|
if !*isjunk {
|
||||||
return analysis{accept: true, mailbox: mailbox, dmarcReport: dmarcReport, tlsReport: tlsReport, reason: reason, dmarcOverrideReason: dmarcOverrideReason, headers: headers}
|
return analysis{d: d, accept: true, mailbox: mailbox, dmarcReport: dmarcReport, tlsReport: tlsReport, reason: reason, dmarcOverrideReason: dmarcOverrideReason, headers: headers}
|
||||||
}
|
}
|
||||||
return reject(smtp.C451LocalErr, smtp.SeSys3Other0, "error processing", err, string(method))
|
return reject(smtp.C451LocalErr, smtp.SeSys3Other0, "error processing", err, string(method))
|
||||||
} else if dmarcReport != nil || tlsReport != nil {
|
} else if dmarcReport != nil || tlsReport != nil {
|
||||||
log.Info("accepting message with dmarc aggregate report or tls report without reputation")
|
log.Info("accepting message with dmarc aggregate report or tls report without reputation")
|
||||||
return analysis{accept: true, mailbox: mailbox, dmarcReport: dmarcReport, tlsReport: tlsReport, reason: reasonReporting, dmarcOverrideReason: dmarcOverrideReason, headers: headers}
|
return analysis{d: d, accept: true, mailbox: mailbox, dmarcReport: dmarcReport, tlsReport: tlsReport, reason: reasonReporting, dmarcOverrideReason: dmarcOverrideReason, headers: headers}
|
||||||
}
|
}
|
||||||
// If there was no previous message from sender or its domain, and we have an SPF
|
// If there was no previous message from sender or its domain, and we have an SPF
|
||||||
// (soft)fail, reject the message.
|
// (soft)fail, reject the message.
|
||||||
|
@ -340,7 +428,7 @@ func analyze(ctx context.Context, log mlog.Log, resolver dns.Resolver, d deliver
|
||||||
var subjectpassKey string
|
var subjectpassKey string
|
||||||
conf, _ := d.acc.Conf()
|
conf, _ := d.acc.Conf()
|
||||||
if conf.SubjectPass.Period > 0 {
|
if conf.SubjectPass.Period > 0 {
|
||||||
subjectpassKey, err = d.acc.Subjectpass(d.rcptAcc.canonicalAddress)
|
subjectpassKey, err = d.acc.Subjectpass(d.canonicalAddress)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorx("get key for verifying subject token", err)
|
log.Errorx("get key for verifying subject token", err)
|
||||||
return reject(smtp.C451LocalErr, smtp.SeSys3Other0, "error processing", err, reasonSubjectpassError)
|
return reject(smtp.C451LocalErr, smtp.SeSys3Other0, "error processing", err, reasonSubjectpassError)
|
||||||
|
@ -349,7 +437,7 @@ func analyze(ctx context.Context, log mlog.Log, resolver dns.Resolver, d deliver
|
||||||
pass := err == nil
|
pass := err == nil
|
||||||
log.Infox("pass by subject token", err, slog.Bool("pass", pass))
|
log.Infox("pass by subject token", err, slog.Bool("pass", pass))
|
||||||
if pass {
|
if pass {
|
||||||
return analysis{accept: true, mailbox: mailbox, reason: reasonSubjectpass, dmarcOverrideReason: dmarcOverrideReason, headers: headers}
|
return analysis{d: d, accept: true, mailbox: mailbox, reason: reasonSubjectpass, dmarcOverrideReason: dmarcOverrideReason, headers: headers}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -380,7 +468,7 @@ func analyze(ctx context.Context, log mlog.Log, resolver dns.Resolver, d deliver
|
||||||
|
|
||||||
rcptToMatch := func(l []message.Address) bool {
|
rcptToMatch := func(l []message.Address) bool {
|
||||||
// todo: we use Go's net/mail to parse message header addresses. it does not allow empty quoted strings (contrary to spec), leaving To empty. so we don't verify To address for that unusual case for now. ../rfc/5322:961 ../rfc/5322:743
|
// todo: we use Go's net/mail to parse message header addresses. it does not allow empty quoted strings (contrary to spec), leaving To empty. so we don't verify To address for that unusual case for now. ../rfc/5322:961 ../rfc/5322:743
|
||||||
if d.rcptAcc.rcptTo.Localpart == "" {
|
if d.smtpRcptTo.Localpart == "" {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
for _, a := range l {
|
for _, a := range l {
|
||||||
|
@ -389,7 +477,7 @@ func analyze(ctx context.Context, log mlog.Log, resolver dns.Resolver, d deliver
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
lp, err := smtp.ParseLocalpart(a.User)
|
lp, err := smtp.ParseLocalpart(a.User)
|
||||||
if err == nil && dom == d.rcptAcc.rcptTo.IPDomain.Domain && lp == d.rcptAcc.rcptTo.Localpart {
|
if err == nil && dom == d.smtpRcptTo.IPDomain.Domain && lp == d.smtpRcptTo.Localpart {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -413,6 +501,7 @@ func analyze(ctx context.Context, log mlog.Log, resolver dns.Resolver, d deliver
|
||||||
// providers (e.g. gmail) does not DKIM-sign Bcc headers, so junk messages can be
|
// providers (e.g. gmail) does not DKIM-sign Bcc headers, so junk messages can be
|
||||||
// sent with matching Bcc headers. We don't get here for known senders.
|
// sent with matching Bcc headers. We don't get here for known senders.
|
||||||
threshold = 0.25
|
threshold = 0.25
|
||||||
|
log.Print("msgto/cc", slog.Any("msgto", d.msgTo), slog.Any("msgcc", d.msgCc))
|
||||||
log.Info("setting junk threshold due to smtp rcpt to and message to/cc address mismatch", slog.Float64("threshold", threshold))
|
log.Info("setting junk threshold due to smtp rcpt to and message to/cc address mismatch", slog.Float64("threshold", threshold))
|
||||||
reason = reasonJunkContentStrict
|
reason = reasonJunkContentStrict
|
||||||
}
|
}
|
||||||
|
@ -463,7 +552,7 @@ func analyze(ctx context.Context, log mlog.Log, resolver dns.Resolver, d deliver
|
||||||
}
|
}
|
||||||
|
|
||||||
if accept {
|
if accept {
|
||||||
return analysis{accept: true, mailbox: mailbox, reason: reasonNoBadSignals, dmarcOverrideReason: dmarcOverrideReason, headers: headers}
|
return analysis{d: d, accept: true, mailbox: mailbox, reason: reasonNoBadSignals, dmarcOverrideReason: dmarcOverrideReason, headers: headers}
|
||||||
}
|
}
|
||||||
|
|
||||||
if subjectpassKey != "" && d.dmarcResult.Status == dmarc.StatusPass && method == methodNone && (dnsblocklisted || junkSubjectpass) {
|
if subjectpassKey != "" && d.dmarcResult.Status == dmarc.StatusPass && method == methodNone && (dnsblocklisted || junkSubjectpass) {
|
||||||
|
|
|
@ -336,19 +336,30 @@ type conn struct {
|
||||||
has8bitmime bool // If MAIL FROM parameter BODY=8BITMIME was sent. Required for SMTPUTF8.
|
has8bitmime bool // If MAIL FROM parameter BODY=8BITMIME was sent. Required for SMTPUTF8.
|
||||||
smtputf8 bool // todo future: we should keep track of this per recipient. perhaps only a specific recipient requires smtputf8, e.g. due to a utf8 localpart.
|
smtputf8 bool // todo future: we should keep track of this per recipient. perhaps only a specific recipient requires smtputf8, e.g. due to a utf8 localpart.
|
||||||
msgsmtputf8 bool // Is SMTPUTF8 required for the received message. Default to the same value as `smtputf8`, but is re-evaluated after the whole message (envelope and data) is received.
|
msgsmtputf8 bool // Is SMTPUTF8 required for the received message. Default to the same value as `smtputf8`, but is re-evaluated after the whole message (envelope and data) is received.
|
||||||
recipients []rcptAccount
|
recipients []recipient
|
||||||
}
|
}
|
||||||
|
|
||||||
type rcptAccount struct {
|
type rcptAccount struct {
|
||||||
rcptTo smtp.Path
|
|
||||||
local bool // Whether recipient is a local user.
|
|
||||||
|
|
||||||
// Only valid for local delivery.
|
|
||||||
accountName string
|
accountName string
|
||||||
destination config.Destination
|
destination config.Destination
|
||||||
canonicalAddress string // Optional catchall part stripped and/or lowercased.
|
canonicalAddress string // Optional catchall part stripped and/or lowercased.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type rcptAlias struct {
|
||||||
|
alias config.Alias
|
||||||
|
canonicalAddress string // Optional catchall part stripped and/or lowercased.
|
||||||
|
}
|
||||||
|
|
||||||
|
type recipient struct {
|
||||||
|
addr smtp.Path
|
||||||
|
|
||||||
|
// If account and alias are both not set, this is not for a local address. This is
|
||||||
|
// normal for submission, where messages are added to the queue. For incoming
|
||||||
|
// deliveries, this will result in an error.
|
||||||
|
account *rcptAccount // If set, recipient address is for this local account.
|
||||||
|
alias *rcptAlias // If set, for a local alias.
|
||||||
|
}
|
||||||
|
|
||||||
func isClosed(err error) bool {
|
func isClosed(err error) bool {
|
||||||
return errors.Is(err, errIO) || moxio.IsClosed(err)
|
return errors.Is(err, errIO) || moxio.IsClosed(err)
|
||||||
}
|
}
|
||||||
|
@ -813,7 +824,7 @@ func (c *conn) xneedTLSForDelivery(rcpt smtp.Path) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func isTLSReportRecipient(rcpt smtp.Path) bool {
|
func isTLSReportRecipient(rcpt smtp.Path) bool {
|
||||||
_, _, dest, err := mox.FindAccount(rcpt.Localpart, rcpt.IPDomain.Domain, false)
|
_, _, _, dest, err := mox.LookupAddress(rcpt.Localpart, rcpt.IPDomain.Domain, false, false)
|
||||||
return err == nil && (dest.HostTLSReports || dest.DomainTLSReports)
|
return err == nil && (dest.HostTLSReports || dest.DomainTLSReports)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1487,7 +1498,7 @@ func (c *conn) cmdMail(p *parser) {
|
||||||
if rpath.IsZero() {
|
if rpath.IsZero() {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
accName, _, _, err := mox.FindAccount(rpath.Localpart, rpath.IPDomain.Domain, false)
|
accName, _, _, _, err := mox.LookupAddress(rpath.Localpart, rpath.IPDomain.Domain, false, false)
|
||||||
return err == nil && accName == c.account.Name
|
return err == nil && accName == c.account.Name
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1626,10 +1637,15 @@ func (c *conn) cmdRcpt(p *parser) {
|
||||||
if !c.submission {
|
if !c.submission {
|
||||||
xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeAddr1UnknownDestMailbox1, "not accepting email for ip")
|
xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeAddr1UnknownDestMailbox1, "not accepting email for ip")
|
||||||
}
|
}
|
||||||
c.recipients = append(c.recipients, rcptAccount{fpath, false, "", config.Destination{}, ""})
|
c.recipients = append(c.recipients, recipient{fpath, nil, nil})
|
||||||
} else if accountName, canonical, addr, err := mox.FindAccount(fpath.Localpart, fpath.IPDomain.Domain, true); err == nil {
|
} else if accountName, alias, canonical, addr, err := mox.LookupAddress(fpath.Localpart, fpath.IPDomain.Domain, true, true); err == nil {
|
||||||
// note: a bare postmaster, without domain, is handled by FindAccount. ../rfc/5321:735
|
// note: a bare postmaster, without domain, is handled by LookupAddress. ../rfc/5321:735
|
||||||
c.recipients = append(c.recipients, rcptAccount{fpath, true, accountName, addr, canonical})
|
if alias != nil {
|
||||||
|
c.recipients = append(c.recipients, recipient{fpath, nil, &rcptAlias{*alias, canonical}})
|
||||||
|
} else {
|
||||||
|
c.recipients = append(c.recipients, recipient{fpath, &rcptAccount{accountName, addr, canonical}, nil})
|
||||||
|
}
|
||||||
|
|
||||||
} else if Localserve {
|
} else if Localserve {
|
||||||
// If the address isn't known, and we are in localserve, deliver to the mox user.
|
// If the address isn't known, and we are in localserve, deliver to the mox user.
|
||||||
// If account or destination doesn't exist, it will be handled during delivery. For
|
// If account or destination doesn't exist, it will be handled during delivery. For
|
||||||
|
@ -1637,14 +1653,14 @@ func (c *conn) cmdRcpt(p *parser) {
|
||||||
// which is typically the mox user.
|
// which is typically the mox user.
|
||||||
acc, _ := mox.Conf.Account("mox")
|
acc, _ := mox.Conf.Account("mox")
|
||||||
dest := acc.Destinations["mox@localhost"]
|
dest := acc.Destinations["mox@localhost"]
|
||||||
c.recipients = append(c.recipients, rcptAccount{fpath, true, "mox", dest, "mox@localhost"})
|
c.recipients = append(c.recipients, recipient{fpath, &rcptAccount{"mox", dest, "mox@localhost"}, nil})
|
||||||
} else if errors.Is(err, mox.ErrDomainNotFound) {
|
} else if errors.Is(err, mox.ErrDomainNotFound) {
|
||||||
if !c.submission {
|
if !c.submission {
|
||||||
xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeAddr1UnknownDestMailbox1, "not accepting email for domain")
|
xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeAddr1UnknownDestMailbox1, "not accepting email for domain")
|
||||||
}
|
}
|
||||||
// We'll be delivering this email.
|
// We'll be delivering this email.
|
||||||
c.recipients = append(c.recipients, rcptAccount{fpath, false, "", config.Destination{}, ""})
|
c.recipients = append(c.recipients, recipient{fpath, nil, nil})
|
||||||
} else if errors.Is(err, mox.ErrAccountNotFound) {
|
} else if errors.Is(err, mox.ErrAddressNotFound) {
|
||||||
if c.submission {
|
if c.submission {
|
||||||
// For submission, we're transparent about which user exists. Should be fine for the typical small-scale deploy.
|
// For submission, we're transparent about which user exists. Should be fine for the typical small-scale deploy.
|
||||||
// ../rfc/5321:1071
|
// ../rfc/5321:1071
|
||||||
|
@ -1653,7 +1669,7 @@ func (c *conn) cmdRcpt(p *parser) {
|
||||||
// We pretend to accept. We don't want to let remote know the user does not exist
|
// We pretend to accept. We don't want to let remote know the user does not exist
|
||||||
// until after DATA. Because then remote has committed to sending a message.
|
// until after DATA. Because then remote has committed to sending a message.
|
||||||
// note: not local for !c.submission is the signal this address is in error.
|
// note: not local for !c.submission is the signal this address is in error.
|
||||||
c.recipients = append(c.recipients, rcptAccount{fpath, false, "", config.Destination{}, ""})
|
c.recipients = append(c.recipients, recipient{fpath, nil, nil})
|
||||||
} else {
|
} else {
|
||||||
c.log.Errorx("looking up account for delivery", err, slog.Any("rcptto", fpath))
|
c.log.Errorx("looking up account for delivery", err, slog.Any("rcptto", fpath))
|
||||||
xsmtpServerErrorf(codes{smtp.C451LocalErr, smtp.SeSys3Other0}, "error processing")
|
xsmtpServerErrorf(codes{smtp.C451LocalErr, smtp.SeSys3Other0}, "error processing")
|
||||||
|
@ -1696,7 +1712,7 @@ func (c *conn) isSMTPUTF8Required(part *message.Part) bool {
|
||||||
}
|
}
|
||||||
// Check all "RCPT TO".
|
// Check all "RCPT TO".
|
||||||
for _, rcpt := range c.recipients {
|
for _, rcpt := range c.recipients {
|
||||||
if hasNonASCII(strings.NewReader(string(rcpt.rcptTo.Localpart))) {
|
if hasNonASCII(strings.NewReader(string(rcpt.addr.Localpart))) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1961,15 +1977,11 @@ func (c *conn) submit(ctx context.Context, recvHdrFor func(string) string, msgWr
|
||||||
c.log.Infox("parsing message From address", err, slog.String("user", c.username))
|
c.log.Infox("parsing message From address", err, slog.String("user", c.username))
|
||||||
xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeMsg6Other0, "cannot parse header or From address: %v", err)
|
xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeMsg6Other0, "cannot parse header or From address: %v", err)
|
||||||
}
|
}
|
||||||
accName, _, _, err := mox.FindAccount(msgFrom.Localpart, msgFrom.Domain, true)
|
if !mox.AllowMsgFrom(c.account.Name, msgFrom) {
|
||||||
if err != nil || accName != c.account.Name {
|
|
||||||
// ../rfc/6409:522
|
// ../rfc/6409:522
|
||||||
if err == nil {
|
|
||||||
err = mox.ErrAccountNotFound
|
|
||||||
}
|
|
||||||
metricSubmission.WithLabelValues("badfrom").Inc()
|
metricSubmission.WithLabelValues("badfrom").Inc()
|
||||||
c.log.Infox("verifying message From address", err, slog.String("user", c.username), slog.Any("msgfrom", msgFrom))
|
c.log.Infox("verifying message from address", mox.ErrAddressNotFound, slog.String("user", c.username), slog.Any("msgfrom", msgFrom))
|
||||||
xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SePol7DeliveryUnauth1, "must match authenticated user")
|
xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SePol7DeliveryUnauth1, "message from address must belong to authenticated user")
|
||||||
}
|
}
|
||||||
|
|
||||||
// TLS-Required: No header makes us not enforce recipient domain's TLS policy.
|
// TLS-Required: No header makes us not enforce recipient domain's TLS policy.
|
||||||
|
@ -2005,7 +2017,7 @@ func (c *conn) submit(ctx context.Context, recvHdrFor func(string) string, msgWr
|
||||||
err = c.account.DB.Read(ctx, func(tx *bstore.Tx) error {
|
err = c.account.DB.Read(ctx, func(tx *bstore.Tx) error {
|
||||||
rcpts := make([]smtp.Path, len(c.recipients))
|
rcpts := make([]smtp.Path, len(c.recipients))
|
||||||
for i, r := range c.recipients {
|
for i, r := range c.recipients {
|
||||||
rcpts[i] = r.rcptTo
|
rcpts[i] = r.addr
|
||||||
}
|
}
|
||||||
msglimit, rcptlimit, err := c.account.SendLimitReached(tx, rcpts)
|
msglimit, rcptlimit, err := c.account.SendLimitReached(tx, rcpts)
|
||||||
xcheckf(err, "checking sender limit")
|
xcheckf(err, "checking sender limit")
|
||||||
|
@ -2051,9 +2063,8 @@ func (c *conn) submit(ctx context.Context, recvHdrFor func(string) string, msgWr
|
||||||
|
|
||||||
selectors := mox.DKIMSelectors(confDom.DKIM)
|
selectors := mox.DKIMSelectors(confDom.DKIM)
|
||||||
if len(selectors) > 0 {
|
if len(selectors) > 0 {
|
||||||
if canonical, err := mox.CanonicalLocalpart(msgFrom.Localpart, confDom); err != nil {
|
canonical := mox.CanonicalLocalpart(msgFrom.Localpart, confDom)
|
||||||
c.log.Errorx("determining canonical localpart for dkim signing", err, slog.Any("localpart", msgFrom.Localpart))
|
if dkimHeaders, err := dkim.Sign(ctx, c.log.Logger, canonical, msgFrom.Domain, selectors, c.msgsmtputf8, store.FileMsgReader(msgPrefix, dataFile)); err != nil {
|
||||||
} else if dkimHeaders, err := dkim.Sign(ctx, c.log.Logger, canonical, msgFrom.Domain, selectors, c.msgsmtputf8, store.FileMsgReader(msgPrefix, dataFile)); err != nil {
|
|
||||||
c.log.Errorx("dkim sign for domain", err, slog.Any("domain", msgFrom.Domain))
|
c.log.Errorx("dkim sign for domain", err, slog.Any("domain", msgFrom.Domain))
|
||||||
metricServerErrors.WithLabelValues("dkimsign").Inc()
|
metricServerErrors.WithLabelValues("dkimsign").Inc()
|
||||||
} else {
|
} else {
|
||||||
|
@ -2091,9 +2102,9 @@ func (c *conn) submit(ctx context.Context, recvHdrFor func(string) string, msgWr
|
||||||
}
|
}
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
qml := make([]queue.Msg, len(c.recipients))
|
qml := make([]queue.Msg, len(c.recipients))
|
||||||
for i, rcptAcc := range c.recipients {
|
for i, rcpt := range c.recipients {
|
||||||
if Localserve {
|
if Localserve {
|
||||||
code, timeout := mox.LocalserveNeedsError(rcptAcc.rcptTo.Localpart)
|
code, timeout := mox.LocalserveNeedsError(rcpt.addr.Localpart)
|
||||||
if timeout {
|
if timeout {
|
||||||
c.log.Info("timing out submission due to special localpart")
|
c.log.Info("timing out submission due to special localpart")
|
||||||
mox.Sleep(mox.Context, time.Hour)
|
mox.Sleep(mox.Context, time.Hour)
|
||||||
|
@ -2116,11 +2127,11 @@ func (c *conn) submit(ctx context.Context, recvHdrFor func(string) string, msgWr
|
||||||
// messages in a single smtp transaction.
|
// messages in a single smtp transaction.
|
||||||
var rcptTo string
|
var rcptTo string
|
||||||
if len(c.recipients) == 1 {
|
if len(c.recipients) == 1 {
|
||||||
rcptTo = rcptAcc.rcptTo.String()
|
rcptTo = rcpt.addr.String()
|
||||||
}
|
}
|
||||||
xmsgPrefix := append([]byte(recvHdrFor(rcptTo)), msgPrefix...)
|
xmsgPrefix := append([]byte(recvHdrFor(rcptTo)), msgPrefix...)
|
||||||
msgSize := int64(len(xmsgPrefix)) + msgWriter.Size
|
msgSize := int64(len(xmsgPrefix)) + msgWriter.Size
|
||||||
qm := queue.MakeMsg(fp, rcptAcc.rcptTo, msgWriter.Has8bit, c.msgsmtputf8, msgSize, messageID, xmsgPrefix, c.requireTLS, now, header.Get("Subject"))
|
qm := queue.MakeMsg(fp, rcpt.addr, msgWriter.Has8bit, c.msgsmtputf8, msgSize, messageID, xmsgPrefix, c.requireTLS, now, header.Get("Subject"))
|
||||||
if !c.futureRelease.IsZero() {
|
if !c.futureRelease.IsZero() {
|
||||||
qm.NextAttempt = c.futureRelease
|
qm.NextAttempt = c.futureRelease
|
||||||
qm.FutureReleaseRequest = c.futureReleaseRequest
|
qm.FutureReleaseRequest = c.futureReleaseRequest
|
||||||
|
@ -2139,18 +2150,18 @@ func (c *conn) submit(ctx context.Context, recvHdrFor func(string) string, msgWr
|
||||||
xsmtpServerErrorf(errCodes(smtp.C451LocalErr, smtp.SeSys3Other0, err), "error delivering message: %v", err)
|
xsmtpServerErrorf(errCodes(smtp.C451LocalErr, smtp.SeSys3Other0, err), "error delivering message: %v", err)
|
||||||
}
|
}
|
||||||
metricSubmission.WithLabelValues("ok").Inc()
|
metricSubmission.WithLabelValues("ok").Inc()
|
||||||
for i, rcptAcc := range c.recipients {
|
for i, rcpt := range c.recipients {
|
||||||
c.log.Info("messages queued for delivery",
|
c.log.Info("messages queued for delivery",
|
||||||
slog.Any("mailfrom", *c.mailFrom),
|
slog.Any("mailfrom", *c.mailFrom),
|
||||||
slog.Any("rcptto", rcptAcc.rcptTo),
|
slog.Any("rcptto", rcpt.addr),
|
||||||
slog.Bool("smtputf8", c.smtputf8),
|
slog.Bool("smtputf8", c.smtputf8),
|
||||||
slog.Bool("msgsmtputf8", c.msgsmtputf8),
|
slog.Bool("msgsmtputf8", c.msgsmtputf8),
|
||||||
slog.Int64("msgsize", qml[i].Size))
|
slog.Int64("msgsize", qml[i].Size))
|
||||||
}
|
}
|
||||||
|
|
||||||
err = c.account.DB.Write(ctx, func(tx *bstore.Tx) error {
|
err = c.account.DB.Write(ctx, func(tx *bstore.Tx) error {
|
||||||
for _, rcptAcc := range c.recipients {
|
for _, rcpt := range c.recipients {
|
||||||
outgoing := store.Outgoing{Recipient: rcptAcc.rcptTo.XString(true)}
|
outgoing := store.Outgoing{Recipient: rcpt.addr.XString(true)}
|
||||||
if err := tx.Insert(&outgoing); err != nil {
|
if err := tx.Insert(&outgoing); err != nil {
|
||||||
return fmt.Errorf("adding outgoing message: %v", err)
|
return fmt.Errorf("adding outgoing message: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -2370,7 +2381,7 @@ func (c *conn) deliver(ctx context.Context, recvHdrFor func(string) string, msgW
|
||||||
// Give immediate response if all recipients are unknown.
|
// Give immediate response if all recipients are unknown.
|
||||||
nunknown := 0
|
nunknown := 0
|
||||||
for _, r := range c.recipients {
|
for _, r := range c.recipients {
|
||||||
if !r.local {
|
if r.account == nil && r.alias == nil {
|
||||||
nunknown++
|
nunknown++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2604,8 +2615,8 @@ func (c *conn) deliver(ctx context.Context, recvHdrFor func(string) string, msgW
|
||||||
errmsg string
|
errmsg string
|
||||||
}
|
}
|
||||||
var deliverErrors []deliverError
|
var deliverErrors []deliverError
|
||||||
addError := func(rcptAcc rcptAccount, code int, secode string, userError bool, errmsg string) {
|
addError := func(rcpt recipient, code int, secode string, userError bool, errmsg string) {
|
||||||
e := deliverError{rcptAcc.rcptTo, code, secode, userError, errmsg}
|
e := deliverError{rcpt.addr, code, secode, userError, errmsg}
|
||||||
c.log.Info("deliver error",
|
c.log.Info("deliver error",
|
||||||
slog.Any("rcptto", e.rcptTo),
|
slog.Any("rcptto", e.rcptTo),
|
||||||
slog.Int("code", code),
|
slog.Int("code", code),
|
||||||
|
@ -2615,124 +2626,52 @@ func (c *conn) deliver(ctx context.Context, recvHdrFor func(string) string, msgW
|
||||||
deliverErrors = append(deliverErrors, e)
|
deliverErrors = append(deliverErrors, e)
|
||||||
}
|
}
|
||||||
|
|
||||||
// For each recipient, do final spam analysis and delivery.
|
// Sort recipients: local accounts, aliases, unknown. For ensuring we don't deliver
|
||||||
for _, rcptAcc := range c.recipients {
|
// to an alias destination that was also explicitly sent to.
|
||||||
log := c.log.With(slog.Any("mailfrom", c.mailFrom), slog.Any("rcptto", rcptAcc.rcptTo))
|
rcptScore := func(r recipient) int {
|
||||||
|
if r.account != nil {
|
||||||
|
return 0
|
||||||
|
} else if r.alias != nil {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
sort.SliceStable(c.recipients, func(i, j int) bool {
|
||||||
|
return rcptScore(c.recipients[i]) < rcptScore(c.recipients[j])
|
||||||
|
})
|
||||||
|
|
||||||
// If this is not a valid local user, we send back a DSN. This can only happen when
|
// Return whether address is a regular explicit recipient in this transaction. Used
|
||||||
// there are also valid recipients, and only when remote is SPF-verified, so the DSN
|
// to prevent delivering a message to an address both for alias and explicit
|
||||||
// should not cause backscatter.
|
// addressee. Relies on c.recipients being sorted as above.
|
||||||
// In case of serious errors, we abort the transaction. We may have already
|
regularRecipient := func(addr smtp.Path) bool {
|
||||||
// delivered some messages. Perhaps it would be better to continue with other
|
for _, rcpt := range c.recipients {
|
||||||
// deliveries, and return an error at the end? Though the failure conditions will
|
if rcpt.account == nil {
|
||||||
// probably prevent any other successful deliveries too...
|
break
|
||||||
// We'll continue delivering to other recipients. ../rfc/5321:3275
|
} else if rcpt.addr.Equal(addr) {
|
||||||
if !rcptAcc.local {
|
return true
|
||||||
metricDelivery.WithLabelValues("unknownuser", "").Inc()
|
}
|
||||||
addError(rcptAcc, smtp.C550MailboxUnavail, smtp.SeAddr1UnknownDestMailbox1, true, "no such user")
|
}
|
||||||
continue
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
acc, err := store.OpenAccount(log, rcptAcc.accountName)
|
// Prepare a message, analyze it against account's junk filter.
|
||||||
|
// The returned analysis has an open account that must be closed by the caller.
|
||||||
|
// We call this for all alias destinations, also when we already delivered to that
|
||||||
|
// recipient: It may be the only recipient that would allow the message.
|
||||||
|
messageAnalyze := func(log mlog.Log, smtpRcptTo, deliverTo smtp.Path, accountName string, destination config.Destination, canonicalAddr string) (a *analysis, rerr error) {
|
||||||
|
acc, err := store.OpenAccount(log, accountName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorx("open account", err, slog.Any("account", rcptAcc.accountName))
|
log.Errorx("open account", err, slog.Any("account", accountName))
|
||||||
metricDelivery.WithLabelValues("accounterror", "").Inc()
|
metricDelivery.WithLabelValues("accounterror", "").Inc()
|
||||||
addError(rcptAcc, smtp.C451LocalErr, smtp.SeSys3Other0, false, "error processing")
|
return nil, err
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
if acc != nil {
|
if a == nil {
|
||||||
err := acc.Close()
|
err := acc.Close()
|
||||||
log.Check(err, "closing account after delivery")
|
log.Check(err, "closing account during analysis")
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// We don't want to let a single IP or network deliver too many messages to an
|
|
||||||
// account. They may fill up the mailbox, either with messages that have to be
|
|
||||||
// purged, or by filling the disk. We check both cases for IP's and networks.
|
|
||||||
var rateError bool // Whether returned error represents a rate error.
|
|
||||||
err = acc.DB.Read(ctx, func(tx *bstore.Tx) (retErr error) {
|
|
||||||
now := time.Now()
|
|
||||||
defer func() {
|
|
||||||
log.Debugx("checking message and size delivery rates", retErr, slog.Duration("duration", time.Since(now)))
|
|
||||||
}()
|
|
||||||
|
|
||||||
checkCount := func(msg store.Message, window time.Duration, limit int) {
|
|
||||||
if retErr != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
q := bstore.QueryTx[store.Message](tx)
|
|
||||||
q.FilterNonzero(msg)
|
|
||||||
q.FilterGreater("Received", now.Add(-window))
|
|
||||||
q.FilterEqual("Expunged", false)
|
|
||||||
n, err := q.Count()
|
|
||||||
if err != nil {
|
|
||||||
retErr = err
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if n >= limit {
|
|
||||||
rateError = true
|
|
||||||
retErr = fmt.Errorf("more than %d messages in past %s from your ip/network", limit, window)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
checkSize := func(msg store.Message, window time.Duration, limit int64) {
|
|
||||||
if retErr != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
q := bstore.QueryTx[store.Message](tx)
|
|
||||||
q.FilterNonzero(msg)
|
|
||||||
q.FilterGreater("Received", now.Add(-window))
|
|
||||||
q.FilterEqual("Expunged", false)
|
|
||||||
size := msgWriter.Size
|
|
||||||
err := q.ForEach(func(v store.Message) error {
|
|
||||||
size += v.Size
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
retErr = err
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if size > limit {
|
|
||||||
rateError = true
|
|
||||||
retErr = fmt.Errorf("more than %d bytes in past %s from your ip/network", limit, window)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// todo future: make these configurable
|
|
||||||
// todo: should we have a limit for forwarded messages? they are stored with empty RemoteIPMasked*
|
|
||||||
|
|
||||||
const day = 24 * time.Hour
|
|
||||||
checkCount(store.Message{RemoteIPMasked1: ipmasked1}, time.Minute, limitIPMasked1MessagesPerMinute)
|
|
||||||
checkCount(store.Message{RemoteIPMasked1: ipmasked1}, day, 20*500)
|
|
||||||
checkCount(store.Message{RemoteIPMasked2: ipmasked2}, time.Minute, 1500)
|
|
||||||
checkCount(store.Message{RemoteIPMasked2: ipmasked2}, day, 20*1500)
|
|
||||||
checkCount(store.Message{RemoteIPMasked3: ipmasked3}, time.Minute, 4500)
|
|
||||||
checkCount(store.Message{RemoteIPMasked3: ipmasked3}, day, 20*4500)
|
|
||||||
|
|
||||||
const MB = 1024 * 1024
|
|
||||||
checkSize(store.Message{RemoteIPMasked1: ipmasked1}, time.Minute, limitIPMasked1SizePerMinute)
|
|
||||||
checkSize(store.Message{RemoteIPMasked1: ipmasked1}, day, 3*1000*MB)
|
|
||||||
checkSize(store.Message{RemoteIPMasked2: ipmasked2}, time.Minute, 3000*MB)
|
|
||||||
checkSize(store.Message{RemoteIPMasked2: ipmasked2}, day, 3*3000*MB)
|
|
||||||
checkSize(store.Message{RemoteIPMasked3: ipmasked3}, time.Minute, 9000*MB)
|
|
||||||
checkSize(store.Message{RemoteIPMasked3: ipmasked3}, day, 3*9000*MB)
|
|
||||||
|
|
||||||
return retErr
|
|
||||||
})
|
|
||||||
if err != nil && !rateError {
|
|
||||||
log.Errorx("checking delivery rates", err)
|
|
||||||
metricDelivery.WithLabelValues("checkrates", "").Inc()
|
|
||||||
addError(rcptAcc, smtp.C451LocalErr, smtp.SeSys3Other0, false, "error processing")
|
|
||||||
continue
|
|
||||||
} else if err != nil {
|
|
||||||
log.Debugx("refusing due to high delivery rate", err)
|
|
||||||
metricDelivery.WithLabelValues("highrate", "").Inc()
|
|
||||||
c.setSlow(true)
|
|
||||||
addError(rcptAcc, smtp.C452StorageFull, smtp.SeMailbox2Full2, true, err.Error())
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
m := store.Message{
|
m := store.Message{
|
||||||
Received: time.Now(),
|
Received: time.Now(),
|
||||||
RemoteIP: c.remoteIP.String(),
|
RemoteIP: c.remoteIP.String(),
|
||||||
|
@ -2743,8 +2682,8 @@ func (c *conn) deliver(ctx context.Context, recvHdrFor func(string) string, msgW
|
||||||
MailFrom: c.mailFrom.String(),
|
MailFrom: c.mailFrom.String(),
|
||||||
MailFromLocalpart: c.mailFrom.Localpart,
|
MailFromLocalpart: c.mailFrom.Localpart,
|
||||||
MailFromDomain: c.mailFrom.IPDomain.Domain.Name(),
|
MailFromDomain: c.mailFrom.IPDomain.Domain.Name(),
|
||||||
RcptToLocalpart: rcptAcc.rcptTo.Localpart,
|
RcptToLocalpart: smtpRcptTo.Localpart,
|
||||||
RcptToDomain: rcptAcc.rcptTo.IPDomain.Domain.Name(),
|
RcptToDomain: smtpRcptTo.IPDomain.Domain.Name(),
|
||||||
MsgFromLocalpart: msgFrom.Localpart,
|
MsgFromLocalpart: msgFrom.Localpart,
|
||||||
MsgFromDomain: msgFrom.Domain.Name(),
|
MsgFromDomain: msgFrom.Domain.Name(),
|
||||||
MsgFromOrgDomain: publicsuffix.Lookup(ctx, log.Logger, msgFrom.Domain).Name(),
|
MsgFromOrgDomain: publicsuffix.Lookup(ctx, log.Logger, msgFrom.Domain).Name(),
|
||||||
|
@ -2774,8 +2713,90 @@ func (c *conn) deliver(ctx context.Context, recvHdrFor func(string) string, msgW
|
||||||
msgTo = envelope.To
|
msgTo = envelope.To
|
||||||
msgCc = envelope.CC
|
msgCc = envelope.CC
|
||||||
}
|
}
|
||||||
d := delivery{c.tls, &m, dataFile, rcptAcc, acc, msgTo, msgCc, msgFrom, c.dnsBLs, dmarcUse, dmarcResult, dkimResults, iprevStatus}
|
d := delivery{c.tls, &m, dataFile, smtpRcptTo, deliverTo, destination, canonicalAddr, acc, msgTo, msgCc, msgFrom, c.dnsBLs, dmarcUse, dmarcResult, dkimResults, iprevStatus}
|
||||||
a := analyze(ctx, log, c.resolver, d)
|
|
||||||
|
r := analyze(ctx, log, c.resolver, d)
|
||||||
|
return &r, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Either deliver the message, or call addError to register the recipient as failed.
|
||||||
|
// If recipient is an alias, we may be delivering to multiple address/accounts and
|
||||||
|
// we will consider a message delivered if we delivered it to at least one account
|
||||||
|
// (others may be over quota).
|
||||||
|
processRecipient := func(rcpt recipient) {
|
||||||
|
log := c.log.With(slog.Any("mailfrom", c.mailFrom), slog.Any("rcptto", rcpt.addr))
|
||||||
|
|
||||||
|
// If this is not a valid local user, we send back a DSN. This can only happen when
|
||||||
|
// there are also valid recipients, and only when remote is SPF-verified, so the DSN
|
||||||
|
// should not cause backscatter.
|
||||||
|
// In case of serious errors, we abort the transaction. We may have already
|
||||||
|
// delivered some messages. Perhaps it would be better to continue with other
|
||||||
|
// deliveries, and return an error at the end? Though the failure conditions will
|
||||||
|
// probably prevent any other successful deliveries too...
|
||||||
|
// We'll continue delivering to other recipients. ../rfc/5321:3275
|
||||||
|
if rcpt.account == nil && rcpt.alias == nil {
|
||||||
|
metricDelivery.WithLabelValues("unknownuser", "").Inc()
|
||||||
|
addError(rcpt, smtp.C550MailboxUnavail, smtp.SeAddr1UnknownDestMailbox1, true, "no such user")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// la holds all analysis, and message preparation, for all accounts (multiple for
|
||||||
|
// aliases). Each has an open account that we we close on return.
|
||||||
|
var la []analysis
|
||||||
|
defer func() {
|
||||||
|
for _, a := range la {
|
||||||
|
err := a.d.acc.Close()
|
||||||
|
log.Check(err, "close account")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// For aliases, we prepare & analyze for each recipient. We accept the message if
|
||||||
|
// any recipient accepts it. Regular destination have just a single account to
|
||||||
|
// check. We check all alias destinations, even if we already explicitly delivered
|
||||||
|
// to them: they may be the only destination that would accept the message.
|
||||||
|
var a0 *analysis // Analysis we've used for accept/reject decision.
|
||||||
|
if rcpt.alias != nil {
|
||||||
|
// Check if msgFrom address is acceptable. This doesn't take validation into
|
||||||
|
// consideration. If the header was forged, the message may be rejected later on.
|
||||||
|
if !aliasAllowedMsgFrom(rcpt.alias.alias, msgFrom) {
|
||||||
|
addError(rcpt, smtp.C550MailboxUnavail, smtp.SePol7ExpnProhibited2, true, "not allowed to send to destination")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
la = make([]analysis, 0, len(rcpt.alias.alias.ParsedAddresses))
|
||||||
|
for _, aa := range rcpt.alias.alias.ParsedAddresses {
|
||||||
|
a, err := messageAnalyze(log, rcpt.addr, aa.Address.Path(), aa.AccountName, aa.Destination, rcpt.alias.canonicalAddress)
|
||||||
|
if err != nil {
|
||||||
|
addError(rcpt, smtp.C451LocalErr, smtp.SeSys3Other0, false, "error processing")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
la = append(la, *a)
|
||||||
|
if a.accept && a0 == nil {
|
||||||
|
// Address that caused us to accept.
|
||||||
|
a0 = &la[len(la)-1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if a0 == nil {
|
||||||
|
// First address, for rejecting.
|
||||||
|
a0 = &la[0]
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
a, err := messageAnalyze(log, rcpt.addr, rcpt.addr, rcpt.account.accountName, rcpt.account.destination, rcpt.account.canonicalAddress)
|
||||||
|
if err != nil {
|
||||||
|
addError(rcpt, smtp.C451LocalErr, smtp.SeSys3Other0, false, "error processing")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
la = []analysis{*a}
|
||||||
|
a0 = &la[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
if !a0.accept && a0.reason == reasonHighRate {
|
||||||
|
log.Info("incoming message rejected for high rate, not storing in rejects mailbox", slog.String("reason", a0.reason), slog.Any("msgfrom", msgFrom))
|
||||||
|
metricDelivery.WithLabelValues("reject", a0.reason).Inc()
|
||||||
|
c.setSlow(true)
|
||||||
|
addError(rcpt, a0.code, a0.secode, a0.userError, a0.errmsg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Any DMARC result override is stored in the evaluation for outgoing DMARC
|
// Any DMARC result override is stored in the evaluation for outgoing DMARC
|
||||||
// aggregate reports, and added to the Authentication-Results message header.
|
// aggregate reports, and added to the Authentication-Results message header.
|
||||||
|
@ -2783,8 +2804,8 @@ func (c *conn) deliver(ctx context.Context, recvHdrFor func(string) string, msgW
|
||||||
// they don't overestimate the potential damage of switching from p=none to
|
// they don't overestimate the potential damage of switching from p=none to
|
||||||
// p=reject.
|
// p=reject.
|
||||||
var dmarcOverrides []string
|
var dmarcOverrides []string
|
||||||
if a.dmarcOverrideReason != "" {
|
if a0.dmarcOverrideReason != "" {
|
||||||
dmarcOverrides = []string{a.dmarcOverrideReason}
|
dmarcOverrides = []string{a0.dmarcOverrideReason}
|
||||||
}
|
}
|
||||||
if dmarcResult.Record != nil && !dmarcUse {
|
if dmarcResult.Record != nil && !dmarcUse {
|
||||||
dmarcOverrides = append(dmarcOverrides, string(dmarcrpt.PolicyOverrideSampledOut))
|
dmarcOverrides = append(dmarcOverrides, string(dmarcrpt.PolicyOverrideSampledOut))
|
||||||
|
@ -2806,22 +2827,24 @@ func (c *conn) deliver(ctx context.Context, recvHdrFor func(string) string, msgW
|
||||||
|
|
||||||
// Prepend reason as message header, for easy display in mail clients.
|
// Prepend reason as message header, for easy display in mail clients.
|
||||||
var xmox string
|
var xmox string
|
||||||
if a.reason != "" {
|
if a0.reason != "" {
|
||||||
xmox = "X-Mox-Reason: " + a.reason + "\r\n"
|
xmox = "X-Mox-Reason: " + a0.reason + "\r\n"
|
||||||
}
|
}
|
||||||
xmox += a.headers
|
xmox += a0.headers
|
||||||
|
|
||||||
|
for i := range la {
|
||||||
// ../rfc/5321:3204
|
// ../rfc/5321:3204
|
||||||
// Received-SPF header goes before Received. ../rfc/7208:2038
|
// Received-SPF header goes before Received. ../rfc/7208:2038
|
||||||
m.MsgPrefix = []byte(
|
la[i].d.m.MsgPrefix = []byte(
|
||||||
xmox +
|
xmox +
|
||||||
"Delivered-To: " + rcptAcc.rcptTo.XString(c.msgsmtputf8) + "\r\n" + // ../rfc/9228:274
|
"Delivered-To: " + la[i].d.deliverTo.XString(c.msgsmtputf8) + "\r\n" + // ../rfc/9228:274
|
||||||
"Return-Path: <" + c.mailFrom.String() + ">\r\n" + // ../rfc/5321:3300
|
"Return-Path: <" + c.mailFrom.String() + ">\r\n" + // ../rfc/5321:3300
|
||||||
rcptAuthResults.Header() +
|
rcptAuthResults.Header() +
|
||||||
receivedSPF.Header() +
|
receivedSPF.Header() +
|
||||||
recvHdrFor(rcptAcc.rcptTo.String()),
|
recvHdrFor(rcpt.addr.String()),
|
||||||
)
|
)
|
||||||
m.Size += int64(len(m.MsgPrefix))
|
la[i].d.m.Size += int64(len(la[i].d.m.MsgPrefix))
|
||||||
|
}
|
||||||
|
|
||||||
// Store DMARC evaluation for inclusion in an aggregate report. Only if there is at
|
// Store DMARC evaluation for inclusion in an aggregate report. Only if there is at
|
||||||
// least one reporting address: We don't want to needlessly store a row in a
|
// least one reporting address: We don't want to needlessly store a row in a
|
||||||
|
@ -2830,24 +2853,24 @@ func (c *conn) deliver(ctx context.Context, recvHdrFor func(string) string, msgW
|
||||||
// the analysis, we will report on rejects because of DMARC, because it could be
|
// the analysis, we will report on rejects because of DMARC, because it could be
|
||||||
// valuable feedback about forwarded or mailing list messages.
|
// valuable feedback about forwarded or mailing list messages.
|
||||||
// ../rfc/7489:1492
|
// ../rfc/7489:1492
|
||||||
if !mox.Conf.Static.NoOutgoingDMARCReports && dmarcResult.Record != nil && len(dmarcResult.Record.AggregateReportAddresses) > 0 && (a.accept && !m.IsReject || a.reason == reasonDMARCPolicy) {
|
if !mox.Conf.Static.NoOutgoingDMARCReports && dmarcResult.Record != nil && len(dmarcResult.Record.AggregateReportAddresses) > 0 && (a0.accept && !a0.d.m.IsReject || a0.reason == reasonDMARCPolicy) {
|
||||||
// Disposition holds our decision on whether to accept the message. Not what the
|
// Disposition holds our decision on whether to accept the message. Not what the
|
||||||
// DMARC evaluation resulted in. We can override, e.g. because of mailing lists,
|
// DMARC evaluation resulted in. We can override, e.g. because of mailing lists,
|
||||||
// forwarding, or local policy.
|
// forwarding, or local policy.
|
||||||
// We treat quarantine as reject, so never claim to quarantine.
|
// We treat quarantine as reject, so never claim to quarantine.
|
||||||
// ../rfc/7489:1691
|
// ../rfc/7489:1691
|
||||||
disposition := dmarcrpt.DispositionNone
|
disposition := dmarcrpt.DispositionNone
|
||||||
if !a.accept {
|
if !a0.accept {
|
||||||
disposition = dmarcrpt.DispositionReject
|
disposition = dmarcrpt.DispositionReject
|
||||||
}
|
}
|
||||||
|
|
||||||
// unknownDomain returns whether the sender is domain with which this account has
|
// unknownDomain returns whether the sender is domain with which this account has
|
||||||
// not had positive interaction.
|
// not had positive interaction.
|
||||||
unknownDomain := func() (unknown bool) {
|
unknownDomain := func() (unknown bool) {
|
||||||
err := acc.DB.Read(ctx, func(tx *bstore.Tx) (err error) {
|
err := a0.d.acc.DB.Read(ctx, func(tx *bstore.Tx) (err error) {
|
||||||
// See if we received a non-junk message from this organizational domain.
|
// See if we received a non-junk message from this organizational domain.
|
||||||
q := bstore.QueryTx[store.Message](tx)
|
q := bstore.QueryTx[store.Message](tx)
|
||||||
q.FilterNonzero(store.Message{MsgFromOrgDomain: m.MsgFromOrgDomain})
|
q.FilterNonzero(store.Message{MsgFromOrgDomain: a0.d.m.MsgFromOrgDomain})
|
||||||
q.FilterEqual("Notjunk", true)
|
q.FilterEqual("Notjunk", true)
|
||||||
q.FilterEqual("IsReject", false)
|
q.FilterEqual("IsReject", false)
|
||||||
exists, err := q.Exists()
|
exists, err := q.Exists()
|
||||||
|
@ -2860,7 +2883,7 @@ func (c *conn) deliver(ctx context.Context, recvHdrFor func(string) string, msgW
|
||||||
|
|
||||||
// See if we sent a message to this organizational domain.
|
// See if we sent a message to this organizational domain.
|
||||||
qr := bstore.QueryTx[store.Recipient](tx)
|
qr := bstore.QueryTx[store.Recipient](tx)
|
||||||
qr.FilterNonzero(store.Recipient{OrgDomain: m.MsgFromOrgDomain})
|
qr.FilterNonzero(store.Recipient{OrgDomain: a0.d.m.MsgFromOrgDomain})
|
||||||
exists, err = qr.Exists()
|
exists, err = qr.Exists()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("querying for message sent to organizational domain: %v", err)
|
return fmt.Errorf("querying for message sent to organizational domain: %v", err)
|
||||||
|
@ -2894,7 +2917,7 @@ func (c *conn) deliver(ctx context.Context, recvHdrFor func(string) string, msgW
|
||||||
// loop. We also don't want to be used for sending reports to unsuspecting domains
|
// loop. We also don't want to be used for sending reports to unsuspecting domains
|
||||||
// we have no relation with.
|
// we have no relation with.
|
||||||
// todo: would it make sense to also mark some percentage of mailing-list-policy-overrides optional? to lower the load on mail servers of folks sending to large mailing lists.
|
// todo: would it make sense to also mark some percentage of mailing-list-policy-overrides optional? to lower the load on mail servers of folks sending to large mailing lists.
|
||||||
Optional: rcptAcc.destination.DMARCReports || rcptAcc.destination.HostTLSReports || rcptAcc.destination.DomainTLSReports || a.reason == reasonDMARCPolicy && unknownDomain(),
|
Optional: a0.d.destination.DMARCReports || a0.d.destination.HostTLSReports || a0.d.destination.DomainTLSReports || a0.reason == reasonDMARCPolicy && unknownDomain(),
|
||||||
|
|
||||||
Addresses: addresses,
|
Addresses: addresses,
|
||||||
|
|
||||||
|
@ -2911,7 +2934,7 @@ func (c *conn) deliver(ctx context.Context, recvHdrFor func(string) string, msgW
|
||||||
Disposition: disposition,
|
Disposition: disposition,
|
||||||
AlignedDKIMPass: dmarcResult.AlignedDKIMPass,
|
AlignedDKIMPass: dmarcResult.AlignedDKIMPass,
|
||||||
AlignedSPFPass: dmarcResult.AlignedSPFPass,
|
AlignedSPFPass: dmarcResult.AlignedSPFPass,
|
||||||
EnvelopeTo: rcptAcc.rcptTo.IPDomain.String(),
|
EnvelopeTo: rcpt.addr.IPDomain.String(),
|
||||||
EnvelopeFrom: c.mailFrom.IPDomain.String(),
|
EnvelopeFrom: c.mailFrom.IPDomain.String(),
|
||||||
HeaderFrom: msgFrom.Domain.Name(),
|
HeaderFrom: msgFrom.Domain.Name(),
|
||||||
}
|
}
|
||||||
|
@ -2956,29 +2979,41 @@ func (c *conn) deliver(ctx context.Context, recvHdrFor func(string) string, msgW
|
||||||
log.Check(err, "adding dmarc evaluation to database for aggregate report")
|
log.Check(err, "adding dmarc evaluation to database for aggregate report")
|
||||||
}
|
}
|
||||||
|
|
||||||
conf, _ := acc.Conf()
|
if !a0.accept {
|
||||||
if !a.accept {
|
for _, a := range la {
|
||||||
if conf.RejectsMailbox != "" {
|
// Don't add message if address was also explicitly present in a RCPT TO command.
|
||||||
present, _, messagehash, err := rejectPresent(log, acc, conf.RejectsMailbox, &m, dataFile)
|
if rcpt.alias != nil && regularRecipient(a.d.deliverTo) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
conf, _ := a.d.acc.Conf()
|
||||||
|
if conf.RejectsMailbox == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
present, _, messagehash, err := rejectPresent(log, a.d.acc, conf.RejectsMailbox, a.d.m, dataFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorx("checking whether reject is already present", err)
|
log.Errorx("checking whether reject is already present", err)
|
||||||
} else if !present {
|
continue
|
||||||
m.IsReject = true
|
} else if present {
|
||||||
m.Seen = true // We don't want to draw attention.
|
log.Info("reject message is already present, ignoring")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
a.d.m.IsReject = true
|
||||||
|
a.d.m.Seen = true // We don't want to draw attention.
|
||||||
// Regular automatic junk flags configuration applies to these messages. The
|
// Regular automatic junk flags configuration applies to these messages. The
|
||||||
// default is to treat these as neutral, so they won't cause outright rejections
|
// default is to treat these as neutral, so they won't cause outright rejections
|
||||||
// due to reputation for later delivery attempts.
|
// due to reputation for later delivery attempts.
|
||||||
m.MessageHash = messagehash
|
a.d.m.MessageHash = messagehash
|
||||||
acc.WithWLock(func() {
|
a.d.acc.WithWLock(func() {
|
||||||
hasSpace := true
|
hasSpace := true
|
||||||
var err error
|
var err error
|
||||||
if !conf.KeepRejects {
|
if !conf.KeepRejects {
|
||||||
hasSpace, err = acc.TidyRejectsMailbox(c.log, conf.RejectsMailbox)
|
hasSpace, err = a.d.acc.TidyRejectsMailbox(c.log, conf.RejectsMailbox)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorx("tidying rejects mailbox", err)
|
log.Errorx("tidying rejects mailbox", err)
|
||||||
} else if hasSpace {
|
} else if hasSpace {
|
||||||
if err := acc.DeliverMailbox(log, conf.RejectsMailbox, &m, dataFile); err != nil {
|
if err := a.d.acc.DeliverMailbox(log, conf.RejectsMailbox, a.d.m, dataFile); err != nil {
|
||||||
log.Errorx("delivering spammy mail to rejects mailbox", err)
|
log.Errorx("delivering spammy mail to rejects mailbox", err)
|
||||||
} else {
|
} else {
|
||||||
log.Info("delivered spammy mail to rejects mailbox")
|
log.Info("delivered spammy mail to rejects mailbox")
|
||||||
|
@ -2987,36 +3022,33 @@ func (c *conn) deliver(ctx context.Context, recvHdrFor func(string) string, msgW
|
||||||
log.Info("not storing spammy mail to full rejects mailbox")
|
log.Info("not storing spammy mail to full rejects mailbox")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else {
|
|
||||||
log.Info("reject message is already present, ignoring")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Info("incoming message rejected", slog.String("reason", a.reason), slog.Any("msgfrom", msgFrom))
|
log.Info("incoming message rejected", slog.String("reason", a0.reason), slog.Any("msgfrom", msgFrom))
|
||||||
metricDelivery.WithLabelValues("reject", a.reason).Inc()
|
metricDelivery.WithLabelValues("reject", a0.reason).Inc()
|
||||||
c.setSlow(true)
|
c.setSlow(true)
|
||||||
addError(rcptAcc, a.code, a.secode, a.userError, a.errmsg)
|
addError(rcpt, a0.code, a0.secode, a0.userError, a0.errmsg)
|
||||||
continue
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
delayFirstTime := true
|
delayFirstTime := true
|
||||||
if a.dmarcReport != nil {
|
if rcpt.account != nil && a0.dmarcReport != nil {
|
||||||
// todo future: add rate limiting to prevent DoS attacks. ../rfc/7489:2570
|
// todo future: add rate limiting to prevent DoS attacks. ../rfc/7489:2570
|
||||||
if err := dmarcdb.AddReport(ctx, a.dmarcReport, msgFrom.Domain); err != nil {
|
if err := dmarcdb.AddReport(ctx, a0.dmarcReport, msgFrom.Domain); err != nil {
|
||||||
log.Errorx("saving dmarc aggregate report in database", err)
|
log.Errorx("saving dmarc aggregate report in database", err)
|
||||||
} else {
|
} else {
|
||||||
log.Info("dmarc aggregate report processed")
|
log.Info("dmarc aggregate report processed")
|
||||||
m.Flags.Seen = true
|
a0.d.m.Flags.Seen = true
|
||||||
delayFirstTime = false
|
delayFirstTime = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if a.tlsReport != nil {
|
if rcpt.account != nil && a0.tlsReport != nil {
|
||||||
// todo future: add rate limiting to prevent DoS attacks.
|
// todo future: add rate limiting to prevent DoS attacks.
|
||||||
if err := tlsrptdb.AddReport(ctx, c.log, msgFrom.Domain, c.mailFrom.String(), rcptAcc.destination.HostTLSReports, a.tlsReport); err != nil {
|
if err := tlsrptdb.AddReport(ctx, c.log, msgFrom.Domain, c.mailFrom.String(), a0.d.destination.HostTLSReports, a0.tlsReport); err != nil {
|
||||||
log.Errorx("saving TLSRPT report in database", err)
|
log.Errorx("saving TLSRPT report in database", err)
|
||||||
} else {
|
} else {
|
||||||
log.Info("tlsrpt report processed")
|
log.Info("tlsrpt report processed")
|
||||||
m.Flags.Seen = true
|
a0.d.m.Flags.Seen = true
|
||||||
delayFirstTime = false
|
delayFirstTime = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3024,24 +3056,14 @@ func (c *conn) deliver(ctx context.Context, recvHdrFor func(string) string, msgW
|
||||||
// If this is a first-time sender and not a forwarded/mailing list message, wait
|
// If this is a first-time sender and not a forwarded/mailing list message, wait
|
||||||
// before actually delivering. If this turns out to be a spammer, we've kept one of
|
// before actually delivering. If this turns out to be a spammer, we've kept one of
|
||||||
// their connections busy.
|
// their connections busy.
|
||||||
if delayFirstTime && !m.IsForward && !m.IsMailingList && a.reason == reasonNoBadSignals && !conf.NoFirstTimeSenderDelay && c.firstTimeSenderDelay > 0 {
|
a0conf, _ := a0.d.acc.Conf()
|
||||||
|
if delayFirstTime && !a0.d.m.IsForward && !a0.d.m.IsMailingList && a0.reason == reasonNoBadSignals && !a0conf.NoFirstTimeSenderDelay && c.firstTimeSenderDelay > 0 {
|
||||||
log.Debug("delaying before delivering from sender without reputation", slog.Duration("delay", c.firstTimeSenderDelay))
|
log.Debug("delaying before delivering from sender without reputation", slog.Duration("delay", c.firstTimeSenderDelay))
|
||||||
mox.Sleep(mox.Context, c.firstTimeSenderDelay)
|
mox.Sleep(mox.Context, c.firstTimeSenderDelay)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gather the message-id before we deliver and the file may be consumed.
|
|
||||||
if !parsedMessageID {
|
|
||||||
if p, err := message.Parse(c.log.Logger, false, store.FileMsgReader(m.MsgPrefix, dataFile)); err != nil {
|
|
||||||
log.Infox("parsing message for message-id", err)
|
|
||||||
} else if header, err := p.Header(); err != nil {
|
|
||||||
log.Infox("parsing message header for message-id", err)
|
|
||||||
} else {
|
|
||||||
messageID = header.Get("Message-Id")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if Localserve {
|
if Localserve {
|
||||||
code, timeout := mox.LocalserveNeedsError(rcptAcc.rcptTo.Localpart)
|
code, timeout := mox.LocalserveNeedsError(rcpt.addr.Localpart)
|
||||||
if timeout {
|
if timeout {
|
||||||
log.Info("timing out due to special localpart")
|
log.Info("timing out due to special localpart")
|
||||||
mox.Sleep(mox.Context, time.Hour)
|
mox.Sleep(mox.Context, time.Hour)
|
||||||
|
@ -3049,28 +3071,55 @@ func (c *conn) deliver(ctx context.Context, recvHdrFor func(string) string, msgW
|
||||||
} else if code != 0 {
|
} else if code != 0 {
|
||||||
log.Info("failure due to special localpart", slog.Int("code", code))
|
log.Info("failure due to special localpart", slog.Int("code", code))
|
||||||
metricDelivery.WithLabelValues("delivererror", "localserve").Inc()
|
metricDelivery.WithLabelValues("delivererror", "localserve").Inc()
|
||||||
addError(rcptAcc, code, smtp.SeOther00, false, fmt.Sprintf("failure with code %d due to special localpart", code))
|
addError(rcpt, code, smtp.SeOther00, false, fmt.Sprintf("failure with code %d due to special localpart", code))
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var delivered bool
|
|
||||||
acc.WithWLock(func() {
|
// Gather the message-id before we deliver and the file may be consumed.
|
||||||
if err := acc.DeliverMailbox(log, a.mailbox, &m, dataFile); err != nil {
|
if !parsedMessageID {
|
||||||
log.Errorx("delivering", err)
|
if p, err := message.Parse(c.log.Logger, false, store.FileMsgReader(a0.d.m.MsgPrefix, dataFile)); err != nil {
|
||||||
metricDelivery.WithLabelValues("delivererror", a.reason).Inc()
|
log.Infox("parsing message for message-id", err)
|
||||||
if errors.Is(err, store.ErrOverQuota) {
|
} else if header, err := p.Header(); err != nil {
|
||||||
addError(rcptAcc, smtp.C452StorageFull, smtp.SeMailbox2Full2, true, "account storage full")
|
log.Infox("parsing message header for message-id", err)
|
||||||
} else {
|
} else {
|
||||||
addError(rcptAcc, smtp.C451LocalErr, smtp.SeSys3Other0, false, "error processing")
|
messageID = header.Get("Message-Id")
|
||||||
|
}
|
||||||
|
parsedMessageID = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally deliver the message to the account(s).
|
||||||
|
var nerr int // Number of non-quota errors.
|
||||||
|
var nfull int // Number of failed deliveries due to over quota.
|
||||||
|
var ndelivered int // Number delivered to account.
|
||||||
|
for _, a := range la {
|
||||||
|
// Don't deliver to recipient that was explicitly present in SMTP transaction, or
|
||||||
|
// is sending the message to an alias they are member of.
|
||||||
|
if rcpt.alias != nil && (regularRecipient(a.d.deliverTo) || a.d.deliverTo.Equal(msgFrom.Path())) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var delivered bool
|
||||||
|
a.d.acc.WithWLock(func() {
|
||||||
|
if err := a.d.acc.DeliverMailbox(log, a.mailbox, a.d.m, dataFile); err != nil {
|
||||||
|
log.Errorx("delivering", err)
|
||||||
|
metricDelivery.WithLabelValues("delivererror", a0.reason).Inc()
|
||||||
|
if errors.Is(err, store.ErrOverQuota) {
|
||||||
|
nfull++
|
||||||
|
} else {
|
||||||
|
addError(rcpt, smtp.C451LocalErr, smtp.SeSys3Other0, false, "error processing")
|
||||||
|
nerr++
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
delivered = true
|
delivered = true
|
||||||
metricDelivery.WithLabelValues("delivered", a.reason).Inc()
|
ndelivered++
|
||||||
log.Info("incoming message delivered", slog.String("reason", a.reason), slog.Any("msgfrom", msgFrom))
|
metricDelivery.WithLabelValues("delivered", a0.reason).Inc()
|
||||||
|
log.Info("incoming message delivered", slog.String("reason", a0.reason), slog.Any("msgfrom", msgFrom))
|
||||||
|
|
||||||
conf, _ = acc.Conf()
|
conf, _ := a.d.acc.Conf()
|
||||||
if conf.RejectsMailbox != "" && m.MessageID != "" {
|
if conf.RejectsMailbox != "" && a.d.m.MessageID != "" {
|
||||||
if err := acc.RejectsRemove(log, conf.RejectsMailbox, m.MessageID); err != nil {
|
if err := a.d.acc.RejectsRemove(log, conf.RejectsMailbox, a.d.m.MessageID); err != nil {
|
||||||
log.Errorx("removing message from rejects mailbox", err, slog.String("messageid", messageID))
|
log.Errorx("removing message from rejects mailbox", err, slog.String("messageid", messageID))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3078,19 +3127,32 @@ func (c *conn) deliver(ctx context.Context, recvHdrFor func(string) string, msgW
|
||||||
|
|
||||||
// Pass delivered messages to queue for DSN processing and/or hooks.
|
// Pass delivered messages to queue for DSN processing and/or hooks.
|
||||||
if delivered {
|
if delivered {
|
||||||
mr := store.FileMsgReader(m.MsgPrefix, dataFile)
|
mr := store.FileMsgReader(a.d.m.MsgPrefix, dataFile)
|
||||||
part, err := m.LoadPart(mr)
|
part, err := a.d.m.LoadPart(mr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorx("loading parsed part for evaluating webhook", err)
|
log.Errorx("loading parsed part for evaluating webhook", err)
|
||||||
} else {
|
} else {
|
||||||
err = queue.Incoming(context.Background(), log, acc, messageID, m, part, a.mailbox)
|
err = queue.Incoming(context.Background(), log, a.d.acc, messageID, *a.d.m, part, a.mailbox)
|
||||||
log.Check(err, "queueing webhook for incoming delivery")
|
log.Check(err, "queueing webhook for incoming delivery")
|
||||||
}
|
}
|
||||||
|
} else if nerr > 0 && ndelivered == 0 {
|
||||||
|
// Don't continue if we had an error and haven't delivered yet. If we only had
|
||||||
|
// quota-related errors, we keep trying for an account to deliver to.
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ndelivered == 0 && (nerr > 0 || nfull > 0) {
|
||||||
|
if nerr == 0 {
|
||||||
|
addError(rcpt, smtp.C452StorageFull, smtp.SeMailbox2Full2, true, "account storage full")
|
||||||
|
} else {
|
||||||
|
addError(rcpt, smtp.C451LocalErr, smtp.SeSys3Other0, false, "error processing")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = acc.Close()
|
// For each recipient, do final spam analysis and delivery.
|
||||||
log.Check(err, "closing account after delivering")
|
for _, rcpt := range c.recipients {
|
||||||
acc = nil
|
processRecipient(rcpt)
|
||||||
}
|
}
|
||||||
|
|
||||||
// If all recipients failed to deliver, return an error.
|
// If all recipients failed to deliver, return an error.
|
||||||
|
@ -3184,6 +3246,21 @@ func (c *conn) deliver(ctx context.Context, recvHdrFor func(string) string, msgW
|
||||||
c.writecodeline(smtp.C250Completed, smtp.SeMailbox2Other0, "it is done", nil)
|
c.writecodeline(smtp.C250Completed, smtp.SeMailbox2Other0, "it is done", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Return whether msgFrom address is allowed to send a message to alias.
|
||||||
|
func aliasAllowedMsgFrom(alias config.Alias, msgFrom smtp.Address) bool {
|
||||||
|
for _, aa := range alias.ParsedAddresses {
|
||||||
|
if aa.Address == msgFrom {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lp, err := smtp.ParseLocalpart(alias.LocalpartStr)
|
||||||
|
xcheckf(err, "parsing alias localpart")
|
||||||
|
if msgFrom == smtp.NewAddress(lp, alias.Domain) {
|
||||||
|
return alias.AllowMsgFrom
|
||||||
|
}
|
||||||
|
return alias.PostPublic
|
||||||
|
}
|
||||||
|
|
||||||
// ecode returns either ecode, or a more specific error based on err.
|
// ecode returns either ecode, or a more specific error based on err.
|
||||||
// For example, ecode can be turned from an "other system" error into a "mail
|
// For example, ecode can be turned from an "other system" error into a "mail
|
||||||
// system full" if the error indicates no disk space is available.
|
// system full" if the error indicates no disk space is available.
|
||||||
|
@ -3247,6 +3324,8 @@ func (c *conn) cmdExpn(p *parser) {
|
||||||
}
|
}
|
||||||
p.xend()
|
p.xend()
|
||||||
|
|
||||||
|
// todo: we could implement expn for local aliases for authenticated users, when members have permission to list. would anyone use it?
|
||||||
|
|
||||||
// ../rfc/5321:4239
|
// ../rfc/5321:4239
|
||||||
xsmtpUserErrorf(smtp.C252WithoutVrfy, smtp.SePol7Other0, "no expand but will try delivery")
|
xsmtpUserErrorf(smtp.C252WithoutVrfy, smtp.SePol7Other0, "no expand but will try delivery")
|
||||||
}
|
}
|
||||||
|
|
|
@ -116,11 +116,13 @@ func newTestServer(t *testing.T, configPath string, resolver dns.Resolver) *test
|
||||||
mox.MustLoadConfig(true, false)
|
mox.MustLoadConfig(true, false)
|
||||||
dataDir := mox.ConfigDirPath(mox.Conf.Static.DataDir)
|
dataDir := mox.ConfigDirPath(mox.Conf.Static.DataDir)
|
||||||
os.RemoveAll(dataDir)
|
os.RemoveAll(dataDir)
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
ts.acc, err = store.OpenAccount(log, "mjl")
|
ts.acc, err = store.OpenAccount(log, "mjl")
|
||||||
tcheck(t, err, "open account")
|
tcheck(t, err, "open account")
|
||||||
err = ts.acc.SetPassword(log, password0)
|
err = ts.acc.SetPassword(log, password0)
|
||||||
tcheck(t, err, "set password")
|
tcheck(t, err, "set password")
|
||||||
|
|
||||||
ts.switchStop = store.Switchboard()
|
ts.switchStop = store.Switchboard()
|
||||||
err = queue.Init()
|
err = queue.Init()
|
||||||
tcheck(t, err, "queue init")
|
tcheck(t, err, "queue init")
|
||||||
|
@ -143,6 +145,23 @@ func (ts *testserver) close() {
|
||||||
ts.acc = nil
|
ts.acc = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ts *testserver) checkCount(mailboxName string, expect int) {
|
||||||
|
t := ts.t
|
||||||
|
t.Helper()
|
||||||
|
q := bstore.QueryDB[store.Mailbox](ctxbg, ts.acc.DB)
|
||||||
|
q.FilterNonzero(store.Mailbox{Name: mailboxName})
|
||||||
|
mb, err := q.Get()
|
||||||
|
tcheck(t, err, "get mailbox")
|
||||||
|
qm := bstore.QueryDB[store.Message](ctxbg, ts.acc.DB)
|
||||||
|
qm.FilterNonzero(store.Message{MailboxID: mb.ID})
|
||||||
|
qm.FilterEqual("Expunged", false)
|
||||||
|
n, err := qm.Count()
|
||||||
|
tcheck(t, err, "count messages in mailbox")
|
||||||
|
if n != expect {
|
||||||
|
t.Fatalf("messages in mailbox, found %d, expected %d", n, expect)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (ts *testserver) run(fn func(helloErr error, client *smtpclient.Client)) {
|
func (ts *testserver) run(fn func(helloErr error, client *smtpclient.Client)) {
|
||||||
ts.t.Helper()
|
ts.t.Helper()
|
||||||
ts.runRaw(func(conn net.Conn) {
|
ts.runRaw(func(conn net.Conn) {
|
||||||
|
@ -194,6 +213,14 @@ func (ts *testserver) runRaw(fn func(clientConn net.Conn)) {
|
||||||
fn(clientConn)
|
fn(clientConn)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ts *testserver) smtperr(err error, expErr *smtpclient.Error) {
|
||||||
|
ts.t.Helper()
|
||||||
|
var cerr smtpclient.Error
|
||||||
|
if expErr == nil && err != nil || expErr != nil && (err == nil || !errors.As(err, &cerr) || cerr.Code != expErr.Code || cerr.Secode != expErr.Secode) {
|
||||||
|
ts.t.Fatalf("got err %#v (%q), expected %#v", err, err, expErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Just a cert that appears valid. SMTP client will not verify anything about it
|
// Just a cert that appears valid. SMTP client will not verify anything about it
|
||||||
// (that is opportunistic TLS for you, "better some than none"). Let's enjoy this
|
// (that is opportunistic TLS for you, "better some than none"). Let's enjoy this
|
||||||
// one moment where it makes life easier.
|
// one moment where it makes life easier.
|
||||||
|
@ -508,22 +535,6 @@ func TestSpam(t *testing.T) {
|
||||||
tinsertmsg(t, ts.acc, "Inbox", &nm, deliverMessage)
|
tinsertmsg(t, ts.acc, "Inbox", &nm, deliverMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
checkCount := func(mailboxName string, expect int) {
|
|
||||||
t.Helper()
|
|
||||||
q := bstore.QueryDB[store.Mailbox](ctxbg, ts.acc.DB)
|
|
||||||
q.FilterNonzero(store.Mailbox{Name: mailboxName})
|
|
||||||
mb, err := q.Get()
|
|
||||||
tcheck(t, err, "get rejects mailbox")
|
|
||||||
qm := bstore.QueryDB[store.Message](ctxbg, ts.acc.DB)
|
|
||||||
qm.FilterNonzero(store.Message{MailboxID: mb.ID})
|
|
||||||
qm.FilterEqual("Expunged", false)
|
|
||||||
n, err := qm.Count()
|
|
||||||
tcheck(t, err, "count messages in rejects mailbox")
|
|
||||||
if n != expect {
|
|
||||||
t.Fatalf("messages in rejects mailbox, found %d, expected %d", n, expect)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delivery from sender with bad reputation should fail.
|
// Delivery from sender with bad reputation should fail.
|
||||||
ts.run(func(err error, client *smtpclient.Client) {
|
ts.run(func(err error, client *smtpclient.Client) {
|
||||||
mailFrom := "remote@example.org"
|
mailFrom := "remote@example.org"
|
||||||
|
@ -536,7 +547,7 @@ func TestSpam(t *testing.T) {
|
||||||
t.Fatalf("delivery by bad sender, got err %v, expected smtpclient.Error with code %d", err, smtp.C451LocalErr)
|
t.Fatalf("delivery by bad sender, got err %v, expected smtpclient.Error with code %d", err, smtp.C451LocalErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
checkCount("Rejects", 1)
|
ts.checkCount("Rejects", 1)
|
||||||
checkEvaluationCount(t, 0) // No positive interactions yet.
|
checkEvaluationCount(t, 0) // No positive interactions yet.
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -550,8 +561,8 @@ func TestSpam(t *testing.T) {
|
||||||
}
|
}
|
||||||
tcheck(t, err, "deliver")
|
tcheck(t, err, "deliver")
|
||||||
|
|
||||||
checkCount("mjl2junk", 1) // In ruleset rejects mailbox.
|
ts.checkCount("mjl2junk", 1) // In ruleset rejects mailbox.
|
||||||
checkCount("Rejects", 1) // Same as before.
|
ts.checkCount("Rejects", 1) // Same as before.
|
||||||
checkEvaluationCount(t, 0) // This is not an actual accept.
|
checkEvaluationCount(t, 0) // This is not an actual accept.
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -571,8 +582,8 @@ func TestSpam(t *testing.T) {
|
||||||
tcheck(t, err, "deliver")
|
tcheck(t, err, "deliver")
|
||||||
|
|
||||||
// Message should now be removed from Rejects mailboxes.
|
// Message should now be removed from Rejects mailboxes.
|
||||||
checkCount("Rejects", 0)
|
ts.checkCount("Rejects", 0)
|
||||||
checkCount("mjl2junk", 1)
|
ts.checkCount("mjl2junk", 1)
|
||||||
checkEvaluationCount(t, 1)
|
checkEvaluationCount(t, 1)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -2252,8 +2252,8 @@ func OpenEmail(log mlog.Log, email string) (*Account, config.Destination, error)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, config.Destination{}, fmt.Errorf("%w: %v", ErrUnknownCredentials, err)
|
return nil, config.Destination{}, fmt.Errorf("%w: %v", ErrUnknownCredentials, err)
|
||||||
}
|
}
|
||||||
accountName, _, dest, err := mox.FindAccount(addr.Localpart, addr.Domain, false)
|
accountName, _, _, dest, err := mox.LookupAddress(addr.Localpart, addr.Domain, false, false)
|
||||||
if err != nil && (errors.Is(err, mox.ErrAccountNotFound) || errors.Is(err, mox.ErrDomainNotFound)) {
|
if err != nil && (errors.Is(err, mox.ErrAddressNotFound) || errors.Is(err, mox.ErrDomainNotFound)) {
|
||||||
return nil, config.Destination{}, ErrUnknownCredentials
|
return nil, config.Destination{}, ErrUnknownCredentials
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return nil, config.Destination{}, fmt.Errorf("looking up address: %v", err)
|
return nil, config.Destination{}, fmt.Errorf("looking up address: %v", err)
|
||||||
|
|
1
testdata/ctl/domains.conf
vendored
1
testdata/ctl/domains.conf
vendored
|
@ -8,4 +8,5 @@ Accounts:
|
||||||
KeepRetiredWebhookPeriod: 1h0m0s
|
KeepRetiredWebhookPeriod: 1h0m0s
|
||||||
Domain: mox.example
|
Domain: mox.example
|
||||||
Destinations:
|
Destinations:
|
||||||
|
mjl2@mox.example: nil
|
||||||
mjl@mox.example: nil
|
mjl@mox.example: nil
|
||||||
|
|
5
testdata/httpaccount/domains.conf
vendored
5
testdata/httpaccount/domains.conf
vendored
|
@ -1,6 +1,11 @@
|
||||||
Domains:
|
Domains:
|
||||||
mox.example:
|
mox.example:
|
||||||
LocalpartCatchallSeparator: +
|
LocalpartCatchallSeparator: +
|
||||||
|
Aliases:
|
||||||
|
support:
|
||||||
|
Addresses:
|
||||||
|
- mjl☺@mox.example
|
||||||
|
AllowMsgFrom: true
|
||||||
Accounts:
|
Accounts:
|
||||||
mjl☺:
|
mjl☺:
|
||||||
Domain: mox.example
|
Domain: mox.example
|
||||||
|
|
18
testdata/smtp/domains.conf
vendored
18
testdata/smtp/domains.conf
vendored
|
@ -1,5 +1,16 @@
|
||||||
Domains:
|
Domains:
|
||||||
mox.example: nil
|
mox.example:
|
||||||
|
Aliases:
|
||||||
|
public:
|
||||||
|
Addresses:
|
||||||
|
- mjl@mox.example
|
||||||
|
- móx@mox.example
|
||||||
|
PostPublic: true
|
||||||
|
AllowMsgFrom: true
|
||||||
|
private:
|
||||||
|
Addresses:
|
||||||
|
- mjl@mox.example
|
||||||
|
- móx@mox.example
|
||||||
mox2.example: nil
|
mox2.example: nil
|
||||||
Accounts:
|
Accounts:
|
||||||
mjl:
|
mjl:
|
||||||
|
@ -21,3 +32,8 @@ Accounts:
|
||||||
TopWords: 10
|
TopWords: 10
|
||||||
IgnoreWords: 0.1
|
IgnoreWords: 0.1
|
||||||
RareWords: 2
|
RareWords: 2
|
||||||
|
# not a member of an alias.
|
||||||
|
☺:
|
||||||
|
Domain: mox.example
|
||||||
|
Destinations:
|
||||||
|
☺@mox.example: nil
|
||||||
|
|
1
testdata/webadmin/domains.conf
vendored
1
testdata/webadmin/domains.conf
vendored
|
@ -4,4 +4,5 @@ Accounts:
|
||||||
mjl:
|
mjl:
|
||||||
Domain: mox.example
|
Domain: mox.example
|
||||||
Destinations:
|
Destinations:
|
||||||
|
mjl2@mox.example: nil
|
||||||
mjl@mox.example: nil
|
mjl@mox.example: nil
|
||||||
|
|
|
@ -255,11 +255,11 @@ var api;
|
||||||
// per-outgoing-message address used for sending.
|
// per-outgoing-message address used for sending.
|
||||||
OutgoingEvent["EventUnrecognized"] = "unrecognized";
|
OutgoingEvent["EventUnrecognized"] = "unrecognized";
|
||||||
})(OutgoingEvent = api.OutgoingEvent || (api.OutgoingEvent = {}));
|
})(OutgoingEvent = api.OutgoingEvent || (api.OutgoingEvent = {}));
|
||||||
api.structTypes = { "Account": true, "AutomaticJunkFlags": true, "Destination": true, "Domain": true, "ImportProgress": true, "Incoming": true, "IncomingMeta": true, "IncomingWebhook": true, "JunkFilter": true, "NameAddress": true, "Outgoing": true, "OutgoingWebhook": true, "Route": true, "Ruleset": true, "Structure": true, "SubjectPass": true, "Suppression": true };
|
api.structTypes = { "Account": true, "Address": true, "AddressAlias": true, "Alias": true, "AliasAddress": true, "AutomaticJunkFlags": true, "Destination": true, "Domain": true, "ImportProgress": true, "Incoming": true, "IncomingMeta": true, "IncomingWebhook": true, "JunkFilter": true, "NameAddress": true, "Outgoing": true, "OutgoingWebhook": true, "Route": true, "Ruleset": true, "Structure": true, "SubjectPass": true, "Suppression": true };
|
||||||
api.stringsTypes = { "CSRFToken": true, "OutgoingEvent": true };
|
api.stringsTypes = { "CSRFToken": true, "Localpart": true, "OutgoingEvent": true };
|
||||||
api.intsTypes = {};
|
api.intsTypes = {};
|
||||||
api.types = {
|
api.types = {
|
||||||
"Account": { "Name": "Account", "Docs": "", "Fields": [{ "Name": "OutgoingWebhook", "Docs": "", "Typewords": ["nullable", "OutgoingWebhook"] }, { "Name": "IncomingWebhook", "Docs": "", "Typewords": ["nullable", "IncomingWebhook"] }, { "Name": "FromIDLoginAddresses", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "KeepRetiredMessagePeriod", "Docs": "", "Typewords": ["int64"] }, { "Name": "KeepRetiredWebhookPeriod", "Docs": "", "Typewords": ["int64"] }, { "Name": "Domain", "Docs": "", "Typewords": ["string"] }, { "Name": "Description", "Docs": "", "Typewords": ["string"] }, { "Name": "FullName", "Docs": "", "Typewords": ["string"] }, { "Name": "Destinations", "Docs": "", "Typewords": ["{}", "Destination"] }, { "Name": "SubjectPass", "Docs": "", "Typewords": ["SubjectPass"] }, { "Name": "QuotaMessageSize", "Docs": "", "Typewords": ["int64"] }, { "Name": "RejectsMailbox", "Docs": "", "Typewords": ["string"] }, { "Name": "KeepRejects", "Docs": "", "Typewords": ["bool"] }, { "Name": "AutomaticJunkFlags", "Docs": "", "Typewords": ["AutomaticJunkFlags"] }, { "Name": "JunkFilter", "Docs": "", "Typewords": ["nullable", "JunkFilter"] }, { "Name": "MaxOutgoingMessagesPerDay", "Docs": "", "Typewords": ["int32"] }, { "Name": "MaxFirstTimeRecipientsPerDay", "Docs": "", "Typewords": ["int32"] }, { "Name": "NoFirstTimeSenderDelay", "Docs": "", "Typewords": ["bool"] }, { "Name": "Routes", "Docs": "", "Typewords": ["[]", "Route"] }, { "Name": "DNSDomain", "Docs": "", "Typewords": ["Domain"] }] },
|
"Account": { "Name": "Account", "Docs": "", "Fields": [{ "Name": "OutgoingWebhook", "Docs": "", "Typewords": ["nullable", "OutgoingWebhook"] }, { "Name": "IncomingWebhook", "Docs": "", "Typewords": ["nullable", "IncomingWebhook"] }, { "Name": "FromIDLoginAddresses", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "KeepRetiredMessagePeriod", "Docs": "", "Typewords": ["int64"] }, { "Name": "KeepRetiredWebhookPeriod", "Docs": "", "Typewords": ["int64"] }, { "Name": "Domain", "Docs": "", "Typewords": ["string"] }, { "Name": "Description", "Docs": "", "Typewords": ["string"] }, { "Name": "FullName", "Docs": "", "Typewords": ["string"] }, { "Name": "Destinations", "Docs": "", "Typewords": ["{}", "Destination"] }, { "Name": "SubjectPass", "Docs": "", "Typewords": ["SubjectPass"] }, { "Name": "QuotaMessageSize", "Docs": "", "Typewords": ["int64"] }, { "Name": "RejectsMailbox", "Docs": "", "Typewords": ["string"] }, { "Name": "KeepRejects", "Docs": "", "Typewords": ["bool"] }, { "Name": "AutomaticJunkFlags", "Docs": "", "Typewords": ["AutomaticJunkFlags"] }, { "Name": "JunkFilter", "Docs": "", "Typewords": ["nullable", "JunkFilter"] }, { "Name": "MaxOutgoingMessagesPerDay", "Docs": "", "Typewords": ["int32"] }, { "Name": "MaxFirstTimeRecipientsPerDay", "Docs": "", "Typewords": ["int32"] }, { "Name": "NoFirstTimeSenderDelay", "Docs": "", "Typewords": ["bool"] }, { "Name": "Routes", "Docs": "", "Typewords": ["[]", "Route"] }, { "Name": "DNSDomain", "Docs": "", "Typewords": ["Domain"] }, { "Name": "Aliases", "Docs": "", "Typewords": ["[]", "AddressAlias"] }] },
|
||||||
"OutgoingWebhook": { "Name": "OutgoingWebhook", "Docs": "", "Fields": [{ "Name": "URL", "Docs": "", "Typewords": ["string"] }, { "Name": "Authorization", "Docs": "", "Typewords": ["string"] }, { "Name": "Events", "Docs": "", "Typewords": ["[]", "string"] }] },
|
"OutgoingWebhook": { "Name": "OutgoingWebhook", "Docs": "", "Fields": [{ "Name": "URL", "Docs": "", "Typewords": ["string"] }, { "Name": "Authorization", "Docs": "", "Typewords": ["string"] }, { "Name": "Events", "Docs": "", "Typewords": ["[]", "string"] }] },
|
||||||
"IncomingWebhook": { "Name": "IncomingWebhook", "Docs": "", "Fields": [{ "Name": "URL", "Docs": "", "Typewords": ["string"] }, { "Name": "Authorization", "Docs": "", "Typewords": ["string"] }] },
|
"IncomingWebhook": { "Name": "IncomingWebhook", "Docs": "", "Fields": [{ "Name": "URL", "Docs": "", "Typewords": ["string"] }, { "Name": "Authorization", "Docs": "", "Typewords": ["string"] }] },
|
||||||
"Destination": { "Name": "Destination", "Docs": "", "Fields": [{ "Name": "Mailbox", "Docs": "", "Typewords": ["string"] }, { "Name": "Rulesets", "Docs": "", "Typewords": ["[]", "Ruleset"] }, { "Name": "FullName", "Docs": "", "Typewords": ["string"] }] },
|
"Destination": { "Name": "Destination", "Docs": "", "Fields": [{ "Name": "Mailbox", "Docs": "", "Typewords": ["string"] }, { "Name": "Rulesets", "Docs": "", "Typewords": ["[]", "Ruleset"] }, { "Name": "FullName", "Docs": "", "Typewords": ["string"] }] },
|
||||||
|
@ -269,6 +269,10 @@ var api;
|
||||||
"AutomaticJunkFlags": { "Name": "AutomaticJunkFlags", "Docs": "", "Fields": [{ "Name": "Enabled", "Docs": "", "Typewords": ["bool"] }, { "Name": "JunkMailboxRegexp", "Docs": "", "Typewords": ["string"] }, { "Name": "NeutralMailboxRegexp", "Docs": "", "Typewords": ["string"] }, { "Name": "NotJunkMailboxRegexp", "Docs": "", "Typewords": ["string"] }] },
|
"AutomaticJunkFlags": { "Name": "AutomaticJunkFlags", "Docs": "", "Fields": [{ "Name": "Enabled", "Docs": "", "Typewords": ["bool"] }, { "Name": "JunkMailboxRegexp", "Docs": "", "Typewords": ["string"] }, { "Name": "NeutralMailboxRegexp", "Docs": "", "Typewords": ["string"] }, { "Name": "NotJunkMailboxRegexp", "Docs": "", "Typewords": ["string"] }] },
|
||||||
"JunkFilter": { "Name": "JunkFilter", "Docs": "", "Fields": [{ "Name": "Threshold", "Docs": "", "Typewords": ["float64"] }, { "Name": "Onegrams", "Docs": "", "Typewords": ["bool"] }, { "Name": "Twograms", "Docs": "", "Typewords": ["bool"] }, { "Name": "Threegrams", "Docs": "", "Typewords": ["bool"] }, { "Name": "MaxPower", "Docs": "", "Typewords": ["float64"] }, { "Name": "TopWords", "Docs": "", "Typewords": ["int32"] }, { "Name": "IgnoreWords", "Docs": "", "Typewords": ["float64"] }, { "Name": "RareWords", "Docs": "", "Typewords": ["int32"] }] },
|
"JunkFilter": { "Name": "JunkFilter", "Docs": "", "Fields": [{ "Name": "Threshold", "Docs": "", "Typewords": ["float64"] }, { "Name": "Onegrams", "Docs": "", "Typewords": ["bool"] }, { "Name": "Twograms", "Docs": "", "Typewords": ["bool"] }, { "Name": "Threegrams", "Docs": "", "Typewords": ["bool"] }, { "Name": "MaxPower", "Docs": "", "Typewords": ["float64"] }, { "Name": "TopWords", "Docs": "", "Typewords": ["int32"] }, { "Name": "IgnoreWords", "Docs": "", "Typewords": ["float64"] }, { "Name": "RareWords", "Docs": "", "Typewords": ["int32"] }] },
|
||||||
"Route": { "Name": "Route", "Docs": "", "Fields": [{ "Name": "FromDomain", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "ToDomain", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "MinimumAttempts", "Docs": "", "Typewords": ["int32"] }, { "Name": "Transport", "Docs": "", "Typewords": ["string"] }, { "Name": "FromDomainASCII", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "ToDomainASCII", "Docs": "", "Typewords": ["[]", "string"] }] },
|
"Route": { "Name": "Route", "Docs": "", "Fields": [{ "Name": "FromDomain", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "ToDomain", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "MinimumAttempts", "Docs": "", "Typewords": ["int32"] }, { "Name": "Transport", "Docs": "", "Typewords": ["string"] }, { "Name": "FromDomainASCII", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "ToDomainASCII", "Docs": "", "Typewords": ["[]", "string"] }] },
|
||||||
|
"AddressAlias": { "Name": "AddressAlias", "Docs": "", "Fields": [{ "Name": "SubscriptionAddress", "Docs": "", "Typewords": ["string"] }, { "Name": "Alias", "Docs": "", "Typewords": ["Alias"] }, { "Name": "MemberAddresses", "Docs": "", "Typewords": ["[]", "string"] }] },
|
||||||
|
"Alias": { "Name": "Alias", "Docs": "", "Fields": [{ "Name": "Addresses", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "PostPublic", "Docs": "", "Typewords": ["bool"] }, { "Name": "ListMembers", "Docs": "", "Typewords": ["bool"] }, { "Name": "AllowMsgFrom", "Docs": "", "Typewords": ["bool"] }, { "Name": "LocalpartStr", "Docs": "", "Typewords": ["string"] }, { "Name": "Domain", "Docs": "", "Typewords": ["Domain"] }, { "Name": "ParsedAddresses", "Docs": "", "Typewords": ["[]", "AliasAddress"] }] },
|
||||||
|
"AliasAddress": { "Name": "AliasAddress", "Docs": "", "Fields": [{ "Name": "Address", "Docs": "", "Typewords": ["Address"] }, { "Name": "AccountName", "Docs": "", "Typewords": ["string"] }, { "Name": "Destination", "Docs": "", "Typewords": ["Destination"] }] },
|
||||||
|
"Address": { "Name": "Address", "Docs": "", "Fields": [{ "Name": "Localpart", "Docs": "", "Typewords": ["Localpart"] }, { "Name": "Domain", "Docs": "", "Typewords": ["Domain"] }] },
|
||||||
"Suppression": { "Name": "Suppression", "Docs": "", "Fields": [{ "Name": "ID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Created", "Docs": "", "Typewords": ["timestamp"] }, { "Name": "Account", "Docs": "", "Typewords": ["string"] }, { "Name": "BaseAddress", "Docs": "", "Typewords": ["string"] }, { "Name": "OriginalAddress", "Docs": "", "Typewords": ["string"] }, { "Name": "Manual", "Docs": "", "Typewords": ["bool"] }, { "Name": "Reason", "Docs": "", "Typewords": ["string"] }] },
|
"Suppression": { "Name": "Suppression", "Docs": "", "Fields": [{ "Name": "ID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Created", "Docs": "", "Typewords": ["timestamp"] }, { "Name": "Account", "Docs": "", "Typewords": ["string"] }, { "Name": "BaseAddress", "Docs": "", "Typewords": ["string"] }, { "Name": "OriginalAddress", "Docs": "", "Typewords": ["string"] }, { "Name": "Manual", "Docs": "", "Typewords": ["bool"] }, { "Name": "Reason", "Docs": "", "Typewords": ["string"] }] },
|
||||||
"ImportProgress": { "Name": "ImportProgress", "Docs": "", "Fields": [{ "Name": "Token", "Docs": "", "Typewords": ["string"] }] },
|
"ImportProgress": { "Name": "ImportProgress", "Docs": "", "Fields": [{ "Name": "Token", "Docs": "", "Typewords": ["string"] }] },
|
||||||
"Outgoing": { "Name": "Outgoing", "Docs": "", "Fields": [{ "Name": "Version", "Docs": "", "Typewords": ["int32"] }, { "Name": "Event", "Docs": "", "Typewords": ["OutgoingEvent"] }, { "Name": "DSN", "Docs": "", "Typewords": ["bool"] }, { "Name": "Suppressing", "Docs": "", "Typewords": ["bool"] }, { "Name": "QueueMsgID", "Docs": "", "Typewords": ["int64"] }, { "Name": "FromID", "Docs": "", "Typewords": ["string"] }, { "Name": "MessageID", "Docs": "", "Typewords": ["string"] }, { "Name": "Subject", "Docs": "", "Typewords": ["string"] }, { "Name": "WebhookQueued", "Docs": "", "Typewords": ["timestamp"] }, { "Name": "SMTPCode", "Docs": "", "Typewords": ["int32"] }, { "Name": "SMTPEnhancedCode", "Docs": "", "Typewords": ["string"] }, { "Name": "Error", "Docs": "", "Typewords": ["string"] }, { "Name": "Extra", "Docs": "", "Typewords": ["{}", "string"] }] },
|
"Outgoing": { "Name": "Outgoing", "Docs": "", "Fields": [{ "Name": "Version", "Docs": "", "Typewords": ["int32"] }, { "Name": "Event", "Docs": "", "Typewords": ["OutgoingEvent"] }, { "Name": "DSN", "Docs": "", "Typewords": ["bool"] }, { "Name": "Suppressing", "Docs": "", "Typewords": ["bool"] }, { "Name": "QueueMsgID", "Docs": "", "Typewords": ["int64"] }, { "Name": "FromID", "Docs": "", "Typewords": ["string"] }, { "Name": "MessageID", "Docs": "", "Typewords": ["string"] }, { "Name": "Subject", "Docs": "", "Typewords": ["string"] }, { "Name": "WebhookQueued", "Docs": "", "Typewords": ["timestamp"] }, { "Name": "SMTPCode", "Docs": "", "Typewords": ["int32"] }, { "Name": "SMTPEnhancedCode", "Docs": "", "Typewords": ["string"] }, { "Name": "Error", "Docs": "", "Typewords": ["string"] }, { "Name": "Extra", "Docs": "", "Typewords": ["{}", "string"] }] },
|
||||||
|
@ -277,6 +281,7 @@ var api;
|
||||||
"Structure": { "Name": "Structure", "Docs": "", "Fields": [{ "Name": "ContentType", "Docs": "", "Typewords": ["string"] }, { "Name": "ContentTypeParams", "Docs": "", "Typewords": ["{}", "string"] }, { "Name": "ContentID", "Docs": "", "Typewords": ["string"] }, { "Name": "DecodedSize", "Docs": "", "Typewords": ["int64"] }, { "Name": "Parts", "Docs": "", "Typewords": ["[]", "Structure"] }] },
|
"Structure": { "Name": "Structure", "Docs": "", "Fields": [{ "Name": "ContentType", "Docs": "", "Typewords": ["string"] }, { "Name": "ContentTypeParams", "Docs": "", "Typewords": ["{}", "string"] }, { "Name": "ContentID", "Docs": "", "Typewords": ["string"] }, { "Name": "DecodedSize", "Docs": "", "Typewords": ["int64"] }, { "Name": "Parts", "Docs": "", "Typewords": ["[]", "Structure"] }] },
|
||||||
"IncomingMeta": { "Name": "IncomingMeta", "Docs": "", "Fields": [{ "Name": "MsgID", "Docs": "", "Typewords": ["int64"] }, { "Name": "MailFrom", "Docs": "", "Typewords": ["string"] }, { "Name": "MailFromValidated", "Docs": "", "Typewords": ["bool"] }, { "Name": "MsgFromValidated", "Docs": "", "Typewords": ["bool"] }, { "Name": "RcptTo", "Docs": "", "Typewords": ["string"] }, { "Name": "DKIMVerifiedDomains", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "RemoteIP", "Docs": "", "Typewords": ["string"] }, { "Name": "Received", "Docs": "", "Typewords": ["timestamp"] }, { "Name": "MailboxName", "Docs": "", "Typewords": ["string"] }, { "Name": "Automated", "Docs": "", "Typewords": ["bool"] }] },
|
"IncomingMeta": { "Name": "IncomingMeta", "Docs": "", "Fields": [{ "Name": "MsgID", "Docs": "", "Typewords": ["int64"] }, { "Name": "MailFrom", "Docs": "", "Typewords": ["string"] }, { "Name": "MailFromValidated", "Docs": "", "Typewords": ["bool"] }, { "Name": "MsgFromValidated", "Docs": "", "Typewords": ["bool"] }, { "Name": "RcptTo", "Docs": "", "Typewords": ["string"] }, { "Name": "DKIMVerifiedDomains", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "RemoteIP", "Docs": "", "Typewords": ["string"] }, { "Name": "Received", "Docs": "", "Typewords": ["timestamp"] }, { "Name": "MailboxName", "Docs": "", "Typewords": ["string"] }, { "Name": "Automated", "Docs": "", "Typewords": ["bool"] }] },
|
||||||
"CSRFToken": { "Name": "CSRFToken", "Docs": "", "Values": null },
|
"CSRFToken": { "Name": "CSRFToken", "Docs": "", "Values": null },
|
||||||
|
"Localpart": { "Name": "Localpart", "Docs": "", "Values": null },
|
||||||
"OutgoingEvent": { "Name": "OutgoingEvent", "Docs": "", "Values": [{ "Name": "EventDelivered", "Value": "delivered", "Docs": "" }, { "Name": "EventSuppressed", "Value": "suppressed", "Docs": "" }, { "Name": "EventDelayed", "Value": "delayed", "Docs": "" }, { "Name": "EventFailed", "Value": "failed", "Docs": "" }, { "Name": "EventRelayed", "Value": "relayed", "Docs": "" }, { "Name": "EventExpanded", "Value": "expanded", "Docs": "" }, { "Name": "EventCanceled", "Value": "canceled", "Docs": "" }, { "Name": "EventUnrecognized", "Value": "unrecognized", "Docs": "" }] },
|
"OutgoingEvent": { "Name": "OutgoingEvent", "Docs": "", "Values": [{ "Name": "EventDelivered", "Value": "delivered", "Docs": "" }, { "Name": "EventSuppressed", "Value": "suppressed", "Docs": "" }, { "Name": "EventDelayed", "Value": "delayed", "Docs": "" }, { "Name": "EventFailed", "Value": "failed", "Docs": "" }, { "Name": "EventRelayed", "Value": "relayed", "Docs": "" }, { "Name": "EventExpanded", "Value": "expanded", "Docs": "" }, { "Name": "EventCanceled", "Value": "canceled", "Docs": "" }, { "Name": "EventUnrecognized", "Value": "unrecognized", "Docs": "" }] },
|
||||||
};
|
};
|
||||||
api.parser = {
|
api.parser = {
|
||||||
|
@ -290,6 +295,10 @@ var api;
|
||||||
AutomaticJunkFlags: (v) => api.parse("AutomaticJunkFlags", v),
|
AutomaticJunkFlags: (v) => api.parse("AutomaticJunkFlags", v),
|
||||||
JunkFilter: (v) => api.parse("JunkFilter", v),
|
JunkFilter: (v) => api.parse("JunkFilter", v),
|
||||||
Route: (v) => api.parse("Route", v),
|
Route: (v) => api.parse("Route", v),
|
||||||
|
AddressAlias: (v) => api.parse("AddressAlias", v),
|
||||||
|
Alias: (v) => api.parse("Alias", v),
|
||||||
|
AliasAddress: (v) => api.parse("AliasAddress", v),
|
||||||
|
Address: (v) => api.parse("Address", v),
|
||||||
Suppression: (v) => api.parse("Suppression", v),
|
Suppression: (v) => api.parse("Suppression", v),
|
||||||
ImportProgress: (v) => api.parse("ImportProgress", v),
|
ImportProgress: (v) => api.parse("ImportProgress", v),
|
||||||
Outgoing: (v) => api.parse("Outgoing", v),
|
Outgoing: (v) => api.parse("Outgoing", v),
|
||||||
|
@ -298,6 +307,7 @@ var api;
|
||||||
Structure: (v) => api.parse("Structure", v),
|
Structure: (v) => api.parse("Structure", v),
|
||||||
IncomingMeta: (v) => api.parse("IncomingMeta", v),
|
IncomingMeta: (v) => api.parse("IncomingMeta", v),
|
||||||
CSRFToken: (v) => api.parse("CSRFToken", v),
|
CSRFToken: (v) => api.parse("CSRFToken", v),
|
||||||
|
Localpart: (v) => api.parse("Localpart", v),
|
||||||
OutgoingEvent: (v) => api.parse("OutgoingEvent", v),
|
OutgoingEvent: (v) => api.parse("OutgoingEvent", v),
|
||||||
};
|
};
|
||||||
// Account exports web API functions for the account web interface. All its
|
// Account exports web API functions for the account web interface. All its
|
||||||
|
@ -1371,7 +1381,10 @@ const index = async () => {
|
||||||
await check(fullNameFieldset, client.AccountSaveFullName(fullName.value));
|
await check(fullNameFieldset, client.AccountSaveFullName(fullName.value));
|
||||||
fullName.setAttribute('value', fullName.value);
|
fullName.setAttribute('value', fullName.value);
|
||||||
fullNameForm.reset();
|
fullNameForm.reset();
|
||||||
}), dom.br(), dom.h2('Addresses'), dom.ul(Object.entries(acc.Destinations || {}).length === 0 ? dom.li('(None, login disabled)') : [], Object.entries(acc.Destinations || {}).sort().map(t => dom.li(dom.a(t[0], attr.href('#destinations/' + t[0])), t[0].startsWith('@') ? ' (catchall)' : []))), dom.br(), dom.h2('Change password'), passwordForm = dom.form(passwordFieldset = dom.fieldset(dom.label(style({ display: 'inline-block' }), 'New password', dom.br(), password1 = dom.input(attr.type('password'), attr.autocomplete('new-password'), attr.required(''), function focus() {
|
}), dom.br(), dom.h2('Addresses'), dom.ul(Object.entries(acc.Destinations || {}).length === 0 ? dom.li('(None, login disabled)') : [], Object.entries(acc.Destinations || {}).sort().map(t => dom.li(dom.a(t[0], attr.href('#destinations/' + t[0])), t[0].startsWith('@') ? ' (catchall)' : []))), dom.br(), dom.h2('Aliases/lists'), dom.table(dom.thead(dom.tr(dom.th('Alias address', attr.title('Messages sent to this address will be delivered to all members of the alias/list.')), dom.th('Subscription address', attr.title('Address subscribed to the alias/list.')), dom.th('Allowed senders', attr.title('Whether only members can send through the alias/list, or anyone.')), dom.th('Send as alias address', attr.title('If enabled, messages can be sent with the alias address in the message "From" header.')), dom.th())), (acc.Aliases || []).length === 0 ? dom.tr(dom.td(attr.colspan('5'), 'None')) : [], (acc.Aliases || []).sort((a, b) => a.Alias.LocalpartStr < b.Alias.LocalpartStr ? -1 : (domainName(a.Alias.Domain) < domainName(b.Alias.Domain) ? -1 : 1)).map(a => dom.tr(dom.td(a.Alias.LocalpartStr, '@', domainName(a.Alias.Domain)), dom.td(a.SubscriptionAddress), dom.td(a.Alias.PostPublic ? 'Anyone' : 'Members only'), dom.td(a.Alias.AllowMsgFrom ? 'Yes' : 'No'), dom.td((a.MemberAddresses || []).length === 0 ? [] :
|
||||||
|
dom.clickbutton('Show members', function click() {
|
||||||
|
popup(dom.h1('Members of alias ', a.Alias.LocalpartStr, '@', domainName(a.Alias.Domain)), dom.ul((a.MemberAddresses || []).map(addr => dom.li(addr))));
|
||||||
|
}))))), dom.br(), dom.h2('Change password'), passwordForm = dom.form(passwordFieldset = dom.fieldset(dom.label(style({ display: 'inline-block' }), 'New password', dom.br(), password1 = dom.input(attr.type('password'), attr.autocomplete('new-password'), attr.required(''), function focus() {
|
||||||
passwordHint.style.display = '';
|
passwordHint.style.display = '';
|
||||||
})), ' ', dom.label(style({ display: 'inline-block' }), 'New password repeat', dom.br(), password2 = dom.input(attr.type('password'), attr.autocomplete('new-password'), attr.required(''))), ' ', dom.submitbutton('Change password')), passwordHint = dom.div(style({ display: 'none', marginTop: '.5ex' }), dom.clickbutton('Generate random password', function click(e) {
|
})), ' ', dom.label(style({ display: 'inline-block' }), 'New password repeat', dom.br(), password2 = dom.input(attr.type('password'), attr.autocomplete('new-password'), attr.required(''))), ' ', dom.submitbutton('Change password')), passwordHint = dom.div(style({ display: 'none', marginTop: '.5ex' }), dom.clickbutton('Generate random password', function click(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
|
@ -765,6 +765,40 @@ const index = async () => {
|
||||||
),
|
),
|
||||||
dom.br(),
|
dom.br(),
|
||||||
|
|
||||||
|
dom.h2('Aliases/lists'),
|
||||||
|
dom.table(
|
||||||
|
dom.thead(
|
||||||
|
dom.tr(
|
||||||
|
dom.th('Alias address', attr.title('Messages sent to this address will be delivered to all members of the alias/list.')),
|
||||||
|
dom.th('Subscription address', attr.title('Address subscribed to the alias/list.')),
|
||||||
|
dom.th('Allowed senders', attr.title('Whether only members can send through the alias/list, or anyone.')),
|
||||||
|
dom.th('Send as alias address', attr.title('If enabled, messages can be sent with the alias address in the message "From" header.')),
|
||||||
|
dom.th(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(acc.Aliases || []).length === 0 ? dom.tr(dom.td(attr.colspan('5'), 'None')) : [],
|
||||||
|
(acc.Aliases || []).sort((a, b) => a.Alias.LocalpartStr < b.Alias.LocalpartStr ? -1 : (domainName(a.Alias.Domain) < domainName(b.Alias.Domain) ? -1 : 1)).map(a =>
|
||||||
|
dom.tr(
|
||||||
|
dom.td(a.Alias.LocalpartStr, '@', domainName(a.Alias.Domain)),
|
||||||
|
dom.td(a.SubscriptionAddress),
|
||||||
|
dom.td(a.Alias.PostPublic ? 'Anyone' : 'Members only'),
|
||||||
|
dom.td(a.Alias.AllowMsgFrom ? 'Yes' : 'No'),
|
||||||
|
dom.td(
|
||||||
|
(a.MemberAddresses || []).length === 0 ? [] :
|
||||||
|
dom.clickbutton('Show members', function click() {
|
||||||
|
popup(
|
||||||
|
dom.h1('Members of alias ', a.Alias.LocalpartStr, '@', domainName(a.Alias.Domain)),
|
||||||
|
dom.ul(
|
||||||
|
(a.MemberAddresses || []).map(addr => dom.li(addr)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
dom.br(),
|
||||||
|
|
||||||
dom.h2('Change password'),
|
dom.h2('Change password'),
|
||||||
passwordForm=dom.form(
|
passwordForm=dom.form(
|
||||||
passwordFieldset=dom.fieldset(
|
passwordFieldset=dom.fieldset(
|
||||||
|
|
|
@ -26,6 +26,8 @@ import (
|
||||||
"github.com/mjl-/bstore"
|
"github.com/mjl-/bstore"
|
||||||
"github.com/mjl-/sherpa"
|
"github.com/mjl-/sherpa"
|
||||||
|
|
||||||
|
"github.com/mjl-/mox/config"
|
||||||
|
"github.com/mjl-/mox/dns"
|
||||||
"github.com/mjl-/mox/mlog"
|
"github.com/mjl-/mox/mlog"
|
||||||
"github.com/mjl-/mox/mox-"
|
"github.com/mjl-/mox/mox-"
|
||||||
"github.com/mjl-/mox/queue"
|
"github.com/mjl-/mox/queue"
|
||||||
|
@ -227,7 +229,20 @@ func TestAccount(t *testing.T) {
|
||||||
|
|
||||||
err = queue.Init() // For DB.
|
err = queue.Init() // For DB.
|
||||||
tcheck(t, err, "queue init")
|
tcheck(t, err, "queue init")
|
||||||
|
|
||||||
account, _, _, _ := api.Account(ctx)
|
account, _, _, _ := api.Account(ctx)
|
||||||
|
|
||||||
|
// Check we don't see the alias member list.
|
||||||
|
tcompare(t, len(account.Aliases), 1)
|
||||||
|
tcompare(t, account.Aliases[0], config.AddressAlias{
|
||||||
|
SubscriptionAddress: "mjl☺@mox.example",
|
||||||
|
Alias: config.Alias{
|
||||||
|
LocalpartStr: "support",
|
||||||
|
Domain: dns.Domain{ASCII: "mox.example"},
|
||||||
|
AllowMsgFrom: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
api.DestinationSave(ctx, "mjl☺@mox.example", account.Destinations["mjl☺@mox.example"], account.Destinations["mjl☺@mox.example"]) // todo: save modified value and compare it afterwards
|
api.DestinationSave(ctx, "mjl☺@mox.example", account.Destinations["mjl☺@mox.example"], account.Destinations["mjl☺@mox.example"]) // todo: save modified value and compare it afterwards
|
||||||
|
|
||||||
api.AccountSaveFullName(ctx, account.FullName+" changed") // todo: check if value was changed
|
api.AccountSaveFullName(ctx, account.FullName+" changed") // todo: check if value was changed
|
||||||
|
|
|
@ -589,6 +589,14 @@
|
||||||
"Typewords": [
|
"Typewords": [
|
||||||
"Domain"
|
"Domain"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "Aliases",
|
||||||
|
"Docs": "",
|
||||||
|
"Typewords": [
|
||||||
|
"[]",
|
||||||
|
"AddressAlias"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -933,6 +941,138 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"Name": "AddressAlias",
|
||||||
|
"Docs": "",
|
||||||
|
"Fields": [
|
||||||
|
{
|
||||||
|
"Name": "SubscriptionAddress",
|
||||||
|
"Docs": "",
|
||||||
|
"Typewords": [
|
||||||
|
"string"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "Alias",
|
||||||
|
"Docs": "Without members.",
|
||||||
|
"Typewords": [
|
||||||
|
"Alias"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "MemberAddresses",
|
||||||
|
"Docs": "Only if allowed to see.",
|
||||||
|
"Typewords": [
|
||||||
|
"[]",
|
||||||
|
"string"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "Alias",
|
||||||
|
"Docs": "",
|
||||||
|
"Fields": [
|
||||||
|
{
|
||||||
|
"Name": "Addresses",
|
||||||
|
"Docs": "",
|
||||||
|
"Typewords": [
|
||||||
|
"[]",
|
||||||
|
"string"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "PostPublic",
|
||||||
|
"Docs": "",
|
||||||
|
"Typewords": [
|
||||||
|
"bool"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "ListMembers",
|
||||||
|
"Docs": "",
|
||||||
|
"Typewords": [
|
||||||
|
"bool"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "AllowMsgFrom",
|
||||||
|
"Docs": "",
|
||||||
|
"Typewords": [
|
||||||
|
"bool"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "LocalpartStr",
|
||||||
|
"Docs": "In encoded form.",
|
||||||
|
"Typewords": [
|
||||||
|
"string"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "Domain",
|
||||||
|
"Docs": "",
|
||||||
|
"Typewords": [
|
||||||
|
"Domain"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "ParsedAddresses",
|
||||||
|
"Docs": "Matches addresses.",
|
||||||
|
"Typewords": [
|
||||||
|
"[]",
|
||||||
|
"AliasAddress"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "AliasAddress",
|
||||||
|
"Docs": "",
|
||||||
|
"Fields": [
|
||||||
|
{
|
||||||
|
"Name": "Address",
|
||||||
|
"Docs": "Parsed address.",
|
||||||
|
"Typewords": [
|
||||||
|
"Address"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "AccountName",
|
||||||
|
"Docs": "Looked up.",
|
||||||
|
"Typewords": [
|
||||||
|
"string"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "Destination",
|
||||||
|
"Docs": "Belonging to address.",
|
||||||
|
"Typewords": [
|
||||||
|
"Destination"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "Address",
|
||||||
|
"Docs": "Address is a parsed email address.",
|
||||||
|
"Fields": [
|
||||||
|
{
|
||||||
|
"Name": "Localpart",
|
||||||
|
"Docs": "",
|
||||||
|
"Typewords": [
|
||||||
|
"Localpart"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "Domain",
|
||||||
|
"Docs": "todo: shouldn't we accept an ip address here too? and merge this type into smtp.Path.",
|
||||||
|
"Typewords": [
|
||||||
|
"Domain"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"Name": "Suppression",
|
"Name": "Suppression",
|
||||||
"Docs": "Suppression is an address to which messages will not be delivered. Attempts to\ndeliver or queue will result in an immediate permanent failure to deliver.",
|
"Docs": "Suppression is an address to which messages will not be delivered. Attempts to\ndeliver or queue will result in an immediate permanent failure to deliver.",
|
||||||
|
@ -1365,6 +1505,11 @@
|
||||||
"Docs": "",
|
"Docs": "",
|
||||||
"Values": null
|
"Values": null
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"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.\nLocalparts are in Unicode NFC.",
|
||||||
|
"Values": null
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"Name": "OutgoingEvent",
|
"Name": "OutgoingEvent",
|
||||||
"Docs": "OutgoingEvent is an activity for an outgoing delivery. Either generated by the\nqueue, or through an incoming DSN (delivery status notification) message.",
|
"Docs": "OutgoingEvent is an activity for an outgoing delivery. Either generated by the\nqueue, or through an incoming DSN (delivery status notification) message.",
|
||||||
|
|
|
@ -23,6 +23,7 @@ export interface Account {
|
||||||
NoFirstTimeSenderDelay: boolean
|
NoFirstTimeSenderDelay: boolean
|
||||||
Routes?: Route[] | null
|
Routes?: Route[] | null
|
||||||
DNSDomain: Domain // Parsed form of Domain.
|
DNSDomain: Domain // Parsed form of Domain.
|
||||||
|
Aliases?: AddressAlias[] | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OutgoingWebhook {
|
export interface OutgoingWebhook {
|
||||||
|
@ -96,6 +97,34 @@ export interface Route {
|
||||||
ToDomainASCII?: string[] | null
|
ToDomainASCII?: string[] | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AddressAlias {
|
||||||
|
SubscriptionAddress: string
|
||||||
|
Alias: Alias // Without members.
|
||||||
|
MemberAddresses?: string[] | null // Only if allowed to see.
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Alias {
|
||||||
|
Addresses?: string[] | null
|
||||||
|
PostPublic: boolean
|
||||||
|
ListMembers: boolean
|
||||||
|
AllowMsgFrom: boolean
|
||||||
|
LocalpartStr: string // In encoded form.
|
||||||
|
Domain: Domain
|
||||||
|
ParsedAddresses?: AliasAddress[] | null // Matches addresses.
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AliasAddress {
|
||||||
|
Address: Address // Parsed address.
|
||||||
|
AccountName: string // Looked up.
|
||||||
|
Destination: Destination // Belonging to address.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Address is a parsed email address.
|
||||||
|
export interface Address {
|
||||||
|
Localpart: Localpart
|
||||||
|
Domain: Domain // todo: shouldn't we accept an ip address here too? and merge this type into smtp.Path.
|
||||||
|
}
|
||||||
|
|
||||||
// Suppression is an address to which messages will not be delivered. Attempts to
|
// Suppression is an address to which messages will not be delivered. Attempts to
|
||||||
// deliver or queue will result in an immediate permanent failure to deliver.
|
// deliver or queue will result in an immediate permanent failure to deliver.
|
||||||
export interface Suppression {
|
export interface Suppression {
|
||||||
|
@ -177,6 +206,12 @@ export interface IncomingMeta {
|
||||||
|
|
||||||
export type CSRFToken = string
|
export type CSRFToken = string
|
||||||
|
|
||||||
|
// Localpart is a decoded local part of an email address, before the "@".
|
||||||
|
// For quoted strings, values do not hold the double quote or escaping backslashes.
|
||||||
|
// An empty string can be a valid localpart.
|
||||||
|
// Localparts are in Unicode NFC.
|
||||||
|
export type Localpart = string
|
||||||
|
|
||||||
// OutgoingEvent is an activity for an outgoing delivery. Either generated by the
|
// OutgoingEvent is an activity for an outgoing delivery. Either generated by the
|
||||||
// queue, or through an incoming DSN (delivery status notification) message.
|
// queue, or through an incoming DSN (delivery status notification) message.
|
||||||
export enum OutgoingEvent {
|
export enum OutgoingEvent {
|
||||||
|
@ -203,11 +238,11 @@ export enum OutgoingEvent {
|
||||||
EventUnrecognized = "unrecognized",
|
EventUnrecognized = "unrecognized",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const structTypes: {[typename: string]: boolean} = {"Account":true,"AutomaticJunkFlags":true,"Destination":true,"Domain":true,"ImportProgress":true,"Incoming":true,"IncomingMeta":true,"IncomingWebhook":true,"JunkFilter":true,"NameAddress":true,"Outgoing":true,"OutgoingWebhook":true,"Route":true,"Ruleset":true,"Structure":true,"SubjectPass":true,"Suppression":true}
|
export const structTypes: {[typename: string]: boolean} = {"Account":true,"Address":true,"AddressAlias":true,"Alias":true,"AliasAddress":true,"AutomaticJunkFlags":true,"Destination":true,"Domain":true,"ImportProgress":true,"Incoming":true,"IncomingMeta":true,"IncomingWebhook":true,"JunkFilter":true,"NameAddress":true,"Outgoing":true,"OutgoingWebhook":true,"Route":true,"Ruleset":true,"Structure":true,"SubjectPass":true,"Suppression":true}
|
||||||
export const stringsTypes: {[typename: string]: boolean} = {"CSRFToken":true,"OutgoingEvent":true}
|
export const stringsTypes: {[typename: string]: boolean} = {"CSRFToken":true,"Localpart":true,"OutgoingEvent":true}
|
||||||
export const intsTypes: {[typename: string]: boolean} = {}
|
export const intsTypes: {[typename: string]: boolean} = {}
|
||||||
export const types: TypenameMap = {
|
export const types: TypenameMap = {
|
||||||
"Account": {"Name":"Account","Docs":"","Fields":[{"Name":"OutgoingWebhook","Docs":"","Typewords":["nullable","OutgoingWebhook"]},{"Name":"IncomingWebhook","Docs":"","Typewords":["nullable","IncomingWebhook"]},{"Name":"FromIDLoginAddresses","Docs":"","Typewords":["[]","string"]},{"Name":"KeepRetiredMessagePeriod","Docs":"","Typewords":["int64"]},{"Name":"KeepRetiredWebhookPeriod","Docs":"","Typewords":["int64"]},{"Name":"Domain","Docs":"","Typewords":["string"]},{"Name":"Description","Docs":"","Typewords":["string"]},{"Name":"FullName","Docs":"","Typewords":["string"]},{"Name":"Destinations","Docs":"","Typewords":["{}","Destination"]},{"Name":"SubjectPass","Docs":"","Typewords":["SubjectPass"]},{"Name":"QuotaMessageSize","Docs":"","Typewords":["int64"]},{"Name":"RejectsMailbox","Docs":"","Typewords":["string"]},{"Name":"KeepRejects","Docs":"","Typewords":["bool"]},{"Name":"AutomaticJunkFlags","Docs":"","Typewords":["AutomaticJunkFlags"]},{"Name":"JunkFilter","Docs":"","Typewords":["nullable","JunkFilter"]},{"Name":"MaxOutgoingMessagesPerDay","Docs":"","Typewords":["int32"]},{"Name":"MaxFirstTimeRecipientsPerDay","Docs":"","Typewords":["int32"]},{"Name":"NoFirstTimeSenderDelay","Docs":"","Typewords":["bool"]},{"Name":"Routes","Docs":"","Typewords":["[]","Route"]},{"Name":"DNSDomain","Docs":"","Typewords":["Domain"]}]},
|
"Account": {"Name":"Account","Docs":"","Fields":[{"Name":"OutgoingWebhook","Docs":"","Typewords":["nullable","OutgoingWebhook"]},{"Name":"IncomingWebhook","Docs":"","Typewords":["nullable","IncomingWebhook"]},{"Name":"FromIDLoginAddresses","Docs":"","Typewords":["[]","string"]},{"Name":"KeepRetiredMessagePeriod","Docs":"","Typewords":["int64"]},{"Name":"KeepRetiredWebhookPeriod","Docs":"","Typewords":["int64"]},{"Name":"Domain","Docs":"","Typewords":["string"]},{"Name":"Description","Docs":"","Typewords":["string"]},{"Name":"FullName","Docs":"","Typewords":["string"]},{"Name":"Destinations","Docs":"","Typewords":["{}","Destination"]},{"Name":"SubjectPass","Docs":"","Typewords":["SubjectPass"]},{"Name":"QuotaMessageSize","Docs":"","Typewords":["int64"]},{"Name":"RejectsMailbox","Docs":"","Typewords":["string"]},{"Name":"KeepRejects","Docs":"","Typewords":["bool"]},{"Name":"AutomaticJunkFlags","Docs":"","Typewords":["AutomaticJunkFlags"]},{"Name":"JunkFilter","Docs":"","Typewords":["nullable","JunkFilter"]},{"Name":"MaxOutgoingMessagesPerDay","Docs":"","Typewords":["int32"]},{"Name":"MaxFirstTimeRecipientsPerDay","Docs":"","Typewords":["int32"]},{"Name":"NoFirstTimeSenderDelay","Docs":"","Typewords":["bool"]},{"Name":"Routes","Docs":"","Typewords":["[]","Route"]},{"Name":"DNSDomain","Docs":"","Typewords":["Domain"]},{"Name":"Aliases","Docs":"","Typewords":["[]","AddressAlias"]}]},
|
||||||
"OutgoingWebhook": {"Name":"OutgoingWebhook","Docs":"","Fields":[{"Name":"URL","Docs":"","Typewords":["string"]},{"Name":"Authorization","Docs":"","Typewords":["string"]},{"Name":"Events","Docs":"","Typewords":["[]","string"]}]},
|
"OutgoingWebhook": {"Name":"OutgoingWebhook","Docs":"","Fields":[{"Name":"URL","Docs":"","Typewords":["string"]},{"Name":"Authorization","Docs":"","Typewords":["string"]},{"Name":"Events","Docs":"","Typewords":["[]","string"]}]},
|
||||||
"IncomingWebhook": {"Name":"IncomingWebhook","Docs":"","Fields":[{"Name":"URL","Docs":"","Typewords":["string"]},{"Name":"Authorization","Docs":"","Typewords":["string"]}]},
|
"IncomingWebhook": {"Name":"IncomingWebhook","Docs":"","Fields":[{"Name":"URL","Docs":"","Typewords":["string"]},{"Name":"Authorization","Docs":"","Typewords":["string"]}]},
|
||||||
"Destination": {"Name":"Destination","Docs":"","Fields":[{"Name":"Mailbox","Docs":"","Typewords":["string"]},{"Name":"Rulesets","Docs":"","Typewords":["[]","Ruleset"]},{"Name":"FullName","Docs":"","Typewords":["string"]}]},
|
"Destination": {"Name":"Destination","Docs":"","Fields":[{"Name":"Mailbox","Docs":"","Typewords":["string"]},{"Name":"Rulesets","Docs":"","Typewords":["[]","Ruleset"]},{"Name":"FullName","Docs":"","Typewords":["string"]}]},
|
||||||
|
@ -217,6 +252,10 @@ export const types: TypenameMap = {
|
||||||
"AutomaticJunkFlags": {"Name":"AutomaticJunkFlags","Docs":"","Fields":[{"Name":"Enabled","Docs":"","Typewords":["bool"]},{"Name":"JunkMailboxRegexp","Docs":"","Typewords":["string"]},{"Name":"NeutralMailboxRegexp","Docs":"","Typewords":["string"]},{"Name":"NotJunkMailboxRegexp","Docs":"","Typewords":["string"]}]},
|
"AutomaticJunkFlags": {"Name":"AutomaticJunkFlags","Docs":"","Fields":[{"Name":"Enabled","Docs":"","Typewords":["bool"]},{"Name":"JunkMailboxRegexp","Docs":"","Typewords":["string"]},{"Name":"NeutralMailboxRegexp","Docs":"","Typewords":["string"]},{"Name":"NotJunkMailboxRegexp","Docs":"","Typewords":["string"]}]},
|
||||||
"JunkFilter": {"Name":"JunkFilter","Docs":"","Fields":[{"Name":"Threshold","Docs":"","Typewords":["float64"]},{"Name":"Onegrams","Docs":"","Typewords":["bool"]},{"Name":"Twograms","Docs":"","Typewords":["bool"]},{"Name":"Threegrams","Docs":"","Typewords":["bool"]},{"Name":"MaxPower","Docs":"","Typewords":["float64"]},{"Name":"TopWords","Docs":"","Typewords":["int32"]},{"Name":"IgnoreWords","Docs":"","Typewords":["float64"]},{"Name":"RareWords","Docs":"","Typewords":["int32"]}]},
|
"JunkFilter": {"Name":"JunkFilter","Docs":"","Fields":[{"Name":"Threshold","Docs":"","Typewords":["float64"]},{"Name":"Onegrams","Docs":"","Typewords":["bool"]},{"Name":"Twograms","Docs":"","Typewords":["bool"]},{"Name":"Threegrams","Docs":"","Typewords":["bool"]},{"Name":"MaxPower","Docs":"","Typewords":["float64"]},{"Name":"TopWords","Docs":"","Typewords":["int32"]},{"Name":"IgnoreWords","Docs":"","Typewords":["float64"]},{"Name":"RareWords","Docs":"","Typewords":["int32"]}]},
|
||||||
"Route": {"Name":"Route","Docs":"","Fields":[{"Name":"FromDomain","Docs":"","Typewords":["[]","string"]},{"Name":"ToDomain","Docs":"","Typewords":["[]","string"]},{"Name":"MinimumAttempts","Docs":"","Typewords":["int32"]},{"Name":"Transport","Docs":"","Typewords":["string"]},{"Name":"FromDomainASCII","Docs":"","Typewords":["[]","string"]},{"Name":"ToDomainASCII","Docs":"","Typewords":["[]","string"]}]},
|
"Route": {"Name":"Route","Docs":"","Fields":[{"Name":"FromDomain","Docs":"","Typewords":["[]","string"]},{"Name":"ToDomain","Docs":"","Typewords":["[]","string"]},{"Name":"MinimumAttempts","Docs":"","Typewords":["int32"]},{"Name":"Transport","Docs":"","Typewords":["string"]},{"Name":"FromDomainASCII","Docs":"","Typewords":["[]","string"]},{"Name":"ToDomainASCII","Docs":"","Typewords":["[]","string"]}]},
|
||||||
|
"AddressAlias": {"Name":"AddressAlias","Docs":"","Fields":[{"Name":"SubscriptionAddress","Docs":"","Typewords":["string"]},{"Name":"Alias","Docs":"","Typewords":["Alias"]},{"Name":"MemberAddresses","Docs":"","Typewords":["[]","string"]}]},
|
||||||
|
"Alias": {"Name":"Alias","Docs":"","Fields":[{"Name":"Addresses","Docs":"","Typewords":["[]","string"]},{"Name":"PostPublic","Docs":"","Typewords":["bool"]},{"Name":"ListMembers","Docs":"","Typewords":["bool"]},{"Name":"AllowMsgFrom","Docs":"","Typewords":["bool"]},{"Name":"LocalpartStr","Docs":"","Typewords":["string"]},{"Name":"Domain","Docs":"","Typewords":["Domain"]},{"Name":"ParsedAddresses","Docs":"","Typewords":["[]","AliasAddress"]}]},
|
||||||
|
"AliasAddress": {"Name":"AliasAddress","Docs":"","Fields":[{"Name":"Address","Docs":"","Typewords":["Address"]},{"Name":"AccountName","Docs":"","Typewords":["string"]},{"Name":"Destination","Docs":"","Typewords":["Destination"]}]},
|
||||||
|
"Address": {"Name":"Address","Docs":"","Fields":[{"Name":"Localpart","Docs":"","Typewords":["Localpart"]},{"Name":"Domain","Docs":"","Typewords":["Domain"]}]},
|
||||||
"Suppression": {"Name":"Suppression","Docs":"","Fields":[{"Name":"ID","Docs":"","Typewords":["int64"]},{"Name":"Created","Docs":"","Typewords":["timestamp"]},{"Name":"Account","Docs":"","Typewords":["string"]},{"Name":"BaseAddress","Docs":"","Typewords":["string"]},{"Name":"OriginalAddress","Docs":"","Typewords":["string"]},{"Name":"Manual","Docs":"","Typewords":["bool"]},{"Name":"Reason","Docs":"","Typewords":["string"]}]},
|
"Suppression": {"Name":"Suppression","Docs":"","Fields":[{"Name":"ID","Docs":"","Typewords":["int64"]},{"Name":"Created","Docs":"","Typewords":["timestamp"]},{"Name":"Account","Docs":"","Typewords":["string"]},{"Name":"BaseAddress","Docs":"","Typewords":["string"]},{"Name":"OriginalAddress","Docs":"","Typewords":["string"]},{"Name":"Manual","Docs":"","Typewords":["bool"]},{"Name":"Reason","Docs":"","Typewords":["string"]}]},
|
||||||
"ImportProgress": {"Name":"ImportProgress","Docs":"","Fields":[{"Name":"Token","Docs":"","Typewords":["string"]}]},
|
"ImportProgress": {"Name":"ImportProgress","Docs":"","Fields":[{"Name":"Token","Docs":"","Typewords":["string"]}]},
|
||||||
"Outgoing": {"Name":"Outgoing","Docs":"","Fields":[{"Name":"Version","Docs":"","Typewords":["int32"]},{"Name":"Event","Docs":"","Typewords":["OutgoingEvent"]},{"Name":"DSN","Docs":"","Typewords":["bool"]},{"Name":"Suppressing","Docs":"","Typewords":["bool"]},{"Name":"QueueMsgID","Docs":"","Typewords":["int64"]},{"Name":"FromID","Docs":"","Typewords":["string"]},{"Name":"MessageID","Docs":"","Typewords":["string"]},{"Name":"Subject","Docs":"","Typewords":["string"]},{"Name":"WebhookQueued","Docs":"","Typewords":["timestamp"]},{"Name":"SMTPCode","Docs":"","Typewords":["int32"]},{"Name":"SMTPEnhancedCode","Docs":"","Typewords":["string"]},{"Name":"Error","Docs":"","Typewords":["string"]},{"Name":"Extra","Docs":"","Typewords":["{}","string"]}]},
|
"Outgoing": {"Name":"Outgoing","Docs":"","Fields":[{"Name":"Version","Docs":"","Typewords":["int32"]},{"Name":"Event","Docs":"","Typewords":["OutgoingEvent"]},{"Name":"DSN","Docs":"","Typewords":["bool"]},{"Name":"Suppressing","Docs":"","Typewords":["bool"]},{"Name":"QueueMsgID","Docs":"","Typewords":["int64"]},{"Name":"FromID","Docs":"","Typewords":["string"]},{"Name":"MessageID","Docs":"","Typewords":["string"]},{"Name":"Subject","Docs":"","Typewords":["string"]},{"Name":"WebhookQueued","Docs":"","Typewords":["timestamp"]},{"Name":"SMTPCode","Docs":"","Typewords":["int32"]},{"Name":"SMTPEnhancedCode","Docs":"","Typewords":["string"]},{"Name":"Error","Docs":"","Typewords":["string"]},{"Name":"Extra","Docs":"","Typewords":["{}","string"]}]},
|
||||||
|
@ -225,6 +264,7 @@ export const types: TypenameMap = {
|
||||||
"Structure": {"Name":"Structure","Docs":"","Fields":[{"Name":"ContentType","Docs":"","Typewords":["string"]},{"Name":"ContentTypeParams","Docs":"","Typewords":["{}","string"]},{"Name":"ContentID","Docs":"","Typewords":["string"]},{"Name":"DecodedSize","Docs":"","Typewords":["int64"]},{"Name":"Parts","Docs":"","Typewords":["[]","Structure"]}]},
|
"Structure": {"Name":"Structure","Docs":"","Fields":[{"Name":"ContentType","Docs":"","Typewords":["string"]},{"Name":"ContentTypeParams","Docs":"","Typewords":["{}","string"]},{"Name":"ContentID","Docs":"","Typewords":["string"]},{"Name":"DecodedSize","Docs":"","Typewords":["int64"]},{"Name":"Parts","Docs":"","Typewords":["[]","Structure"]}]},
|
||||||
"IncomingMeta": {"Name":"IncomingMeta","Docs":"","Fields":[{"Name":"MsgID","Docs":"","Typewords":["int64"]},{"Name":"MailFrom","Docs":"","Typewords":["string"]},{"Name":"MailFromValidated","Docs":"","Typewords":["bool"]},{"Name":"MsgFromValidated","Docs":"","Typewords":["bool"]},{"Name":"RcptTo","Docs":"","Typewords":["string"]},{"Name":"DKIMVerifiedDomains","Docs":"","Typewords":["[]","string"]},{"Name":"RemoteIP","Docs":"","Typewords":["string"]},{"Name":"Received","Docs":"","Typewords":["timestamp"]},{"Name":"MailboxName","Docs":"","Typewords":["string"]},{"Name":"Automated","Docs":"","Typewords":["bool"]}]},
|
"IncomingMeta": {"Name":"IncomingMeta","Docs":"","Fields":[{"Name":"MsgID","Docs":"","Typewords":["int64"]},{"Name":"MailFrom","Docs":"","Typewords":["string"]},{"Name":"MailFromValidated","Docs":"","Typewords":["bool"]},{"Name":"MsgFromValidated","Docs":"","Typewords":["bool"]},{"Name":"RcptTo","Docs":"","Typewords":["string"]},{"Name":"DKIMVerifiedDomains","Docs":"","Typewords":["[]","string"]},{"Name":"RemoteIP","Docs":"","Typewords":["string"]},{"Name":"Received","Docs":"","Typewords":["timestamp"]},{"Name":"MailboxName","Docs":"","Typewords":["string"]},{"Name":"Automated","Docs":"","Typewords":["bool"]}]},
|
||||||
"CSRFToken": {"Name":"CSRFToken","Docs":"","Values":null},
|
"CSRFToken": {"Name":"CSRFToken","Docs":"","Values":null},
|
||||||
|
"Localpart": {"Name":"Localpart","Docs":"","Values":null},
|
||||||
"OutgoingEvent": {"Name":"OutgoingEvent","Docs":"","Values":[{"Name":"EventDelivered","Value":"delivered","Docs":""},{"Name":"EventSuppressed","Value":"suppressed","Docs":""},{"Name":"EventDelayed","Value":"delayed","Docs":""},{"Name":"EventFailed","Value":"failed","Docs":""},{"Name":"EventRelayed","Value":"relayed","Docs":""},{"Name":"EventExpanded","Value":"expanded","Docs":""},{"Name":"EventCanceled","Value":"canceled","Docs":""},{"Name":"EventUnrecognized","Value":"unrecognized","Docs":""}]},
|
"OutgoingEvent": {"Name":"OutgoingEvent","Docs":"","Values":[{"Name":"EventDelivered","Value":"delivered","Docs":""},{"Name":"EventSuppressed","Value":"suppressed","Docs":""},{"Name":"EventDelayed","Value":"delayed","Docs":""},{"Name":"EventFailed","Value":"failed","Docs":""},{"Name":"EventRelayed","Value":"relayed","Docs":""},{"Name":"EventExpanded","Value":"expanded","Docs":""},{"Name":"EventCanceled","Value":"canceled","Docs":""},{"Name":"EventUnrecognized","Value":"unrecognized","Docs":""}]},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -239,6 +279,10 @@ export const parser = {
|
||||||
AutomaticJunkFlags: (v: any) => parse("AutomaticJunkFlags", v) as AutomaticJunkFlags,
|
AutomaticJunkFlags: (v: any) => parse("AutomaticJunkFlags", v) as AutomaticJunkFlags,
|
||||||
JunkFilter: (v: any) => parse("JunkFilter", v) as JunkFilter,
|
JunkFilter: (v: any) => parse("JunkFilter", v) as JunkFilter,
|
||||||
Route: (v: any) => parse("Route", v) as Route,
|
Route: (v: any) => parse("Route", v) as Route,
|
||||||
|
AddressAlias: (v: any) => parse("AddressAlias", v) as AddressAlias,
|
||||||
|
Alias: (v: any) => parse("Alias", v) as Alias,
|
||||||
|
AliasAddress: (v: any) => parse("AliasAddress", v) as AliasAddress,
|
||||||
|
Address: (v: any) => parse("Address", v) as Address,
|
||||||
Suppression: (v: any) => parse("Suppression", v) as Suppression,
|
Suppression: (v: any) => parse("Suppression", v) as Suppression,
|
||||||
ImportProgress: (v: any) => parse("ImportProgress", v) as ImportProgress,
|
ImportProgress: (v: any) => parse("ImportProgress", v) as ImportProgress,
|
||||||
Outgoing: (v: any) => parse("Outgoing", v) as Outgoing,
|
Outgoing: (v: any) => parse("Outgoing", v) as Outgoing,
|
||||||
|
@ -247,6 +291,7 @@ export const parser = {
|
||||||
Structure: (v: any) => parse("Structure", v) as Structure,
|
Structure: (v: any) => parse("Structure", v) as Structure,
|
||||||
IncomingMeta: (v: any) => parse("IncomingMeta", v) as IncomingMeta,
|
IncomingMeta: (v: any) => parse("IncomingMeta", v) as IncomingMeta,
|
||||||
CSRFToken: (v: any) => parse("CSRFToken", v) as CSRFToken,
|
CSRFToken: (v: any) => parse("CSRFToken", v) as CSRFToken,
|
||||||
|
Localpart: (v: any) => parse("Localpart", v) as Localpart,
|
||||||
OutgoingEvent: (v: any) => parse("OutgoingEvent", v) as OutgoingEvent,
|
OutgoingEvent: (v: any) => parse("OutgoingEvent", v) as OutgoingEvent,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1535,7 +1535,7 @@ func (Admin) DomainConfig(ctx context.Context, domain string) config.Domain {
|
||||||
}
|
}
|
||||||
|
|
||||||
// DomainLocalparts returns the encoded localparts and accounts configured in domain.
|
// DomainLocalparts returns the encoded localparts and accounts configured in domain.
|
||||||
func (Admin) DomainLocalparts(ctx context.Context, domain string) (localpartAccounts map[string]string) {
|
func (Admin) DomainLocalparts(ctx context.Context, domain string) (localpartAccounts map[string]string, localpartAliases map[string]config.Alias) {
|
||||||
d, err := dns.ParseDomain(domain)
|
d, err := dns.ParseDomain(domain)
|
||||||
xcheckuserf(ctx, err, "parsing domain")
|
xcheckuserf(ctx, err, "parsing domain")
|
||||||
_, ok := mox.Conf.Domain(d)
|
_, ok := mox.Conf.Domain(d)
|
||||||
|
@ -2430,8 +2430,9 @@ func (Admin) AccountRoutesSave(ctx context.Context, accountName string, routes [
|
||||||
|
|
||||||
// DomainRoutesSave saves routes for a domain.
|
// DomainRoutesSave saves routes for a domain.
|
||||||
func (Admin) DomainRoutesSave(ctx context.Context, domainName string, routes []config.Route) {
|
func (Admin) DomainRoutesSave(ctx context.Context, domainName string, routes []config.Route) {
|
||||||
err := mox.DomainSave(ctx, domainName, func(domain *config.Domain) {
|
err := mox.DomainSave(ctx, domainName, func(domain *config.Domain) error {
|
||||||
domain.Routes = routes
|
domain.Routes = routes
|
||||||
|
return nil
|
||||||
})
|
})
|
||||||
xcheckf(ctx, err, "saving domain routes")
|
xcheckf(ctx, err, "saving domain routes")
|
||||||
}
|
}
|
||||||
|
@ -2446,16 +2447,18 @@ func (Admin) RoutesSave(ctx context.Context, routes []config.Route) {
|
||||||
|
|
||||||
// DomainDescriptionSave saves the description for a domain.
|
// DomainDescriptionSave saves the description for a domain.
|
||||||
func (Admin) DomainDescriptionSave(ctx context.Context, domainName, descr string) {
|
func (Admin) DomainDescriptionSave(ctx context.Context, domainName, descr string) {
|
||||||
err := mox.DomainSave(ctx, domainName, func(domain *config.Domain) {
|
err := mox.DomainSave(ctx, domainName, func(domain *config.Domain) error {
|
||||||
domain.Description = descr
|
domain.Description = descr
|
||||||
|
return nil
|
||||||
})
|
})
|
||||||
xcheckf(ctx, err, "saving domain description")
|
xcheckf(ctx, err, "saving domain description")
|
||||||
}
|
}
|
||||||
|
|
||||||
// DomainClientSettingsDomainSave saves the client settings domain for a domain.
|
// DomainClientSettingsDomainSave saves the client settings domain for a domain.
|
||||||
func (Admin) DomainClientSettingsDomainSave(ctx context.Context, domainName, clientSettingsDomain string) {
|
func (Admin) DomainClientSettingsDomainSave(ctx context.Context, domainName, clientSettingsDomain string) {
|
||||||
err := mox.DomainSave(ctx, domainName, func(domain *config.Domain) {
|
err := mox.DomainSave(ctx, domainName, func(domain *config.Domain) error {
|
||||||
domain.ClientSettingsDomain = clientSettingsDomain
|
domain.ClientSettingsDomain = clientSettingsDomain
|
||||||
|
return nil
|
||||||
})
|
})
|
||||||
xcheckf(ctx, err, "saving client settings domain")
|
xcheckf(ctx, err, "saving client settings domain")
|
||||||
}
|
}
|
||||||
|
@ -2463,9 +2466,10 @@ func (Admin) DomainClientSettingsDomainSave(ctx context.Context, domainName, cli
|
||||||
// DomainLocalpartConfigSave saves the localpart catchall and case-sensitive
|
// DomainLocalpartConfigSave saves the localpart catchall and case-sensitive
|
||||||
// settings for a domain.
|
// settings for a domain.
|
||||||
func (Admin) DomainLocalpartConfigSave(ctx context.Context, domainName, localpartCatchallSeparator string, localpartCaseSensitive bool) {
|
func (Admin) DomainLocalpartConfigSave(ctx context.Context, domainName, localpartCatchallSeparator string, localpartCaseSensitive bool) {
|
||||||
err := mox.DomainSave(ctx, domainName, func(domain *config.Domain) {
|
err := mox.DomainSave(ctx, domainName, func(domain *config.Domain) error {
|
||||||
domain.LocalpartCatchallSeparator = localpartCatchallSeparator
|
domain.LocalpartCatchallSeparator = localpartCatchallSeparator
|
||||||
domain.LocalpartCaseSensitive = localpartCaseSensitive
|
domain.LocalpartCaseSensitive = localpartCaseSensitive
|
||||||
|
return nil
|
||||||
})
|
})
|
||||||
xcheckf(ctx, err, "saving localpart settings for domain")
|
xcheckf(ctx, err, "saving localpart settings for domain")
|
||||||
}
|
}
|
||||||
|
@ -2474,7 +2478,7 @@ func (Admin) DomainLocalpartConfigSave(ctx context.Context, domainName, localpar
|
||||||
// configuration for a domain. If localpart is empty, processing reports is
|
// configuration for a domain. If localpart is empty, processing reports is
|
||||||
// disabled.
|
// disabled.
|
||||||
func (Admin) DomainDMARCAddressSave(ctx context.Context, domainName, localpart, domain, account, mailbox string) {
|
func (Admin) DomainDMARCAddressSave(ctx context.Context, domainName, localpart, domain, account, mailbox string) {
|
||||||
err := mox.DomainSave(ctx, domainName, func(d *config.Domain) {
|
err := mox.DomainSave(ctx, domainName, func(d *config.Domain) error {
|
||||||
if localpart == "" {
|
if localpart == "" {
|
||||||
d.DMARC = nil
|
d.DMARC = nil
|
||||||
} else {
|
} else {
|
||||||
|
@ -2485,6 +2489,7 @@ func (Admin) DomainDMARCAddressSave(ctx context.Context, domainName, localpart,
|
||||||
Mailbox: mailbox,
|
Mailbox: mailbox,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
})
|
})
|
||||||
xcheckf(ctx, err, "saving dmarc reporting address/settings for domain")
|
xcheckf(ctx, err, "saving dmarc reporting address/settings for domain")
|
||||||
}
|
}
|
||||||
|
@ -2493,7 +2498,7 @@ func (Admin) DomainDMARCAddressSave(ctx context.Context, domainName, localpart,
|
||||||
// configuration for a domain. If localpart is empty, processing reports is
|
// configuration for a domain. If localpart is empty, processing reports is
|
||||||
// disabled.
|
// disabled.
|
||||||
func (Admin) DomainTLSRPTAddressSave(ctx context.Context, domainName, localpart, domain, account, mailbox string) {
|
func (Admin) DomainTLSRPTAddressSave(ctx context.Context, domainName, localpart, domain, account, mailbox string) {
|
||||||
err := mox.DomainSave(ctx, domainName, func(d *config.Domain) {
|
err := mox.DomainSave(ctx, domainName, func(d *config.Domain) error {
|
||||||
if localpart == "" {
|
if localpart == "" {
|
||||||
d.TLSRPT = nil
|
d.TLSRPT = nil
|
||||||
} else {
|
} else {
|
||||||
|
@ -2504,6 +2509,7 @@ func (Admin) DomainTLSRPTAddressSave(ctx context.Context, domainName, localpart,
|
||||||
Mailbox: mailbox,
|
Mailbox: mailbox,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
})
|
})
|
||||||
xcheckf(ctx, err, "saving tls reporting address/settings for domain")
|
xcheckf(ctx, err, "saving tls reporting address/settings for domain")
|
||||||
}
|
}
|
||||||
|
@ -2511,7 +2517,7 @@ func (Admin) DomainTLSRPTAddressSave(ctx context.Context, domainName, localpart,
|
||||||
// DomainMTASTSSave saves the MTASTS policy for a domain. If policyID is empty,
|
// DomainMTASTSSave saves the MTASTS policy for a domain. If policyID is empty,
|
||||||
// no MTASTS policy is served.
|
// no MTASTS policy is served.
|
||||||
func (Admin) DomainMTASTSSave(ctx context.Context, domainName, policyID string, mode mtasts.Mode, maxAge time.Duration, mx []string) {
|
func (Admin) DomainMTASTSSave(ctx context.Context, domainName, policyID string, mode mtasts.Mode, maxAge time.Duration, mx []string) {
|
||||||
err := mox.DomainSave(ctx, domainName, func(d *config.Domain) {
|
err := mox.DomainSave(ctx, domainName, func(d *config.Domain) error {
|
||||||
if policyID == "" {
|
if policyID == "" {
|
||||||
d.MTASTS = nil
|
d.MTASTS = nil
|
||||||
} else {
|
} else {
|
||||||
|
@ -2522,6 +2528,7 @@ func (Admin) DomainMTASTSSave(ctx context.Context, domainName, policyID string,
|
||||||
MX: mx,
|
MX: mx,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
})
|
})
|
||||||
xcheckf(ctx, err, "saving mtasts policy for domain")
|
xcheckf(ctx, err, "saving mtasts policy for domain")
|
||||||
}
|
}
|
||||||
|
@ -2557,7 +2564,7 @@ func (Admin) DomainDKIMSave(ctx context.Context, domainName string, selectors ma
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err := mox.DomainSave(ctx, domainName, func(d *config.Domain) {
|
err := mox.DomainSave(ctx, domainName, func(d *config.Domain) error {
|
||||||
if len(selectors) != len(d.DKIM.Selectors) {
|
if len(selectors) != len(d.DKIM.Selectors) {
|
||||||
xcheckuserf(ctx, fmt.Errorf("cannot add/remove dkim selectors with this function"), "checking selectors")
|
xcheckuserf(ctx, fmt.Errorf("cannot add/remove dkim selectors with this function"), "checking selectors")
|
||||||
}
|
}
|
||||||
|
@ -2591,6 +2598,50 @@ func (Admin) DomainDKIMSave(ctx context.Context, domainName string, selectors ma
|
||||||
Selectors: sels,
|
Selectors: sels,
|
||||||
Sign: sign,
|
Sign: sign,
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
})
|
})
|
||||||
xcheckf(ctx, err, "saving dkim selector for domain")
|
xcheckf(ctx, err, "saving dkim selector for domain")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func xparseAddress(ctx context.Context, lp, domain string) smtp.Address {
|
||||||
|
xlp, err := smtp.ParseLocalpart(lp)
|
||||||
|
xcheckuserf(ctx, err, "parsing localpart")
|
||||||
|
d, err := dns.ParseDomain(domain)
|
||||||
|
xcheckuserf(ctx, err, "parsing domain")
|
||||||
|
return smtp.NewAddress(xlp, d)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Admin) AliasAdd(ctx context.Context, aliaslp string, domainName string, alias config.Alias) {
|
||||||
|
addr := xparseAddress(ctx, aliaslp, domainName)
|
||||||
|
err := mox.AliasAdd(ctx, addr, alias)
|
||||||
|
xcheckf(ctx, err, "adding alias")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Admin) AliasUpdate(ctx context.Context, aliaslp string, domainName string, postPublic, listMembers, allowMsgFrom bool) {
|
||||||
|
addr := xparseAddress(ctx, aliaslp, domainName)
|
||||||
|
alias := config.Alias{
|
||||||
|
PostPublic: postPublic,
|
||||||
|
ListMembers: listMembers,
|
||||||
|
AllowMsgFrom: allowMsgFrom,
|
||||||
|
}
|
||||||
|
err := mox.AliasUpdate(ctx, addr, alias)
|
||||||
|
xcheckf(ctx, err, "saving alias")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Admin) AliasRemove(ctx context.Context, aliaslp string, domainName string) {
|
||||||
|
addr := xparseAddress(ctx, aliaslp, domainName)
|
||||||
|
err := mox.AliasRemove(ctx, addr)
|
||||||
|
xcheckf(ctx, err, "removing alias")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Admin) AliasAddressesAdd(ctx context.Context, aliaslp string, domainName string, addresses []string) {
|
||||||
|
addr := xparseAddress(ctx, aliaslp, domainName)
|
||||||
|
err := mox.AliasAddressesAdd(ctx, addr, addresses)
|
||||||
|
xcheckf(ctx, err, "adding address to alias")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Admin) AliasAddressesRemove(ctx context.Context, aliaslp string, domainName string, addresses []string) {
|
||||||
|
addr := xparseAddress(ctx, aliaslp, domainName)
|
||||||
|
err := mox.AliasAddressesRemove(ctx, addr, addresses)
|
||||||
|
xcheckf(ctx, err, "removing address from alias")
|
||||||
|
}
|
||||||
|
|
|
@ -337,7 +337,7 @@ var api;
|
||||||
SPFResult["SPFTemperror"] = "temperror";
|
SPFResult["SPFTemperror"] = "temperror";
|
||||||
SPFResult["SPFPermerror"] = "permerror";
|
SPFResult["SPFPermerror"] = "permerror";
|
||||||
})(SPFResult = api.SPFResult || (api.SPFResult = {}));
|
})(SPFResult = api.SPFResult || (api.SPFResult = {}));
|
||||||
api.structTypes = { "Account": true, "AuthResults": true, "AutoconfCheckResult": true, "AutodiscoverCheckResult": true, "AutodiscoverSRV": true, "AutomaticJunkFlags": true, "Canonicalization": true, "CheckResult": true, "ClientConfigs": true, "ClientConfigsEntry": true, "ConfigDomain": true, "DANECheckResult": true, "DKIM": true, "DKIMAuthResult": true, "DKIMCheckResult": true, "DKIMRecord": true, "DMARC": true, "DMARCCheckResult": true, "DMARCRecord": true, "DMARCSummary": true, "DNSSECResult": true, "DateRange": true, "Destination": true, "Directive": true, "Domain": true, "DomainFeedback": true, "Dynamic": true, "Evaluation": true, "EvaluationStat": true, "Extension": true, "FailureDetails": true, "Filter": true, "HoldRule": true, "Hook": true, "HookFilter": true, "HookResult": true, "HookRetired": true, "HookRetiredFilter": true, "HookRetiredSort": true, "HookSort": true, "IPDomain": true, "IPRevCheckResult": true, "Identifiers": true, "IncomingWebhook": true, "JunkFilter": true, "MTASTS": true, "MTASTSCheckResult": true, "MTASTSRecord": true, "MX": true, "MXCheckResult": true, "Modifier": true, "Msg": true, "MsgResult": true, "MsgRetired": true, "OutgoingWebhook": true, "Pair": true, "Policy": true, "PolicyEvaluated": true, "PolicyOverrideReason": true, "PolicyPublished": true, "PolicyRecord": true, "Record": true, "Report": true, "ReportMetadata": true, "ReportRecord": true, "Result": true, "ResultPolicy": true, "RetiredFilter": true, "RetiredSort": true, "Reverse": true, "Route": true, "Row": true, "Ruleset": true, "SMTPAuth": true, "SPFAuthResult": true, "SPFCheckResult": true, "SPFRecord": true, "SRV": true, "SRVConfCheckResult": true, "STSMX": true, "Selector": true, "Sort": true, "SubjectPass": true, "Summary": true, "SuppressAddress": true, "TLSCheckResult": true, "TLSRPT": true, "TLSRPTCheckResult": true, "TLSRPTDateRange": true, "TLSRPTRecord": true, "TLSRPTSummary": true, "TLSRPTSuppressAddress": true, "TLSReportRecord": true, "TLSResult": true, "Transport": true, "TransportDirect": true, "TransportSMTP": true, "TransportSocks": true, "URI": true, "WebForward": true, "WebHandler": true, "WebRedirect": true, "WebStatic": true, "WebserverConfig": true };
|
api.structTypes = { "Account": true, "Address": true, "AddressAlias": true, "Alias": true, "AliasAddress": true, "AuthResults": true, "AutoconfCheckResult": true, "AutodiscoverCheckResult": true, "AutodiscoverSRV": true, "AutomaticJunkFlags": true, "Canonicalization": true, "CheckResult": true, "ClientConfigs": true, "ClientConfigsEntry": true, "ConfigDomain": true, "DANECheckResult": true, "DKIM": true, "DKIMAuthResult": true, "DKIMCheckResult": true, "DKIMRecord": true, "DMARC": true, "DMARCCheckResult": true, "DMARCRecord": true, "DMARCSummary": true, "DNSSECResult": true, "DateRange": true, "Destination": true, "Directive": true, "Domain": true, "DomainFeedback": true, "Dynamic": true, "Evaluation": true, "EvaluationStat": true, "Extension": true, "FailureDetails": true, "Filter": true, "HoldRule": true, "Hook": true, "HookFilter": true, "HookResult": true, "HookRetired": true, "HookRetiredFilter": true, "HookRetiredSort": true, "HookSort": true, "IPDomain": true, "IPRevCheckResult": true, "Identifiers": true, "IncomingWebhook": true, "JunkFilter": true, "MTASTS": true, "MTASTSCheckResult": true, "MTASTSRecord": true, "MX": true, "MXCheckResult": true, "Modifier": true, "Msg": true, "MsgResult": true, "MsgRetired": true, "OutgoingWebhook": true, "Pair": true, "Policy": true, "PolicyEvaluated": true, "PolicyOverrideReason": true, "PolicyPublished": true, "PolicyRecord": true, "Record": true, "Report": true, "ReportMetadata": true, "ReportRecord": true, "Result": true, "ResultPolicy": true, "RetiredFilter": true, "RetiredSort": true, "Reverse": true, "Route": true, "Row": true, "Ruleset": true, "SMTPAuth": true, "SPFAuthResult": true, "SPFCheckResult": true, "SPFRecord": true, "SRV": true, "SRVConfCheckResult": true, "STSMX": true, "Selector": true, "Sort": true, "SubjectPass": true, "Summary": true, "SuppressAddress": true, "TLSCheckResult": true, "TLSRPT": true, "TLSRPTCheckResult": true, "TLSRPTDateRange": true, "TLSRPTRecord": true, "TLSRPTSummary": true, "TLSRPTSuppressAddress": true, "TLSReportRecord": true, "TLSResult": true, "Transport": true, "TransportDirect": true, "TransportSMTP": true, "TransportSocks": true, "URI": true, "WebForward": true, "WebHandler": true, "WebRedirect": true, "WebStatic": true, "WebserverConfig": true };
|
||||||
api.stringsTypes = { "Align": true, "Alignment": true, "CSRFToken": true, "DKIMResult": true, "DMARCPolicy": true, "DMARCResult": true, "Disposition": true, "IP": true, "Localpart": true, "Mode": true, "PolicyOverride": true, "PolicyType": true, "RUA": true, "ResultType": true, "SPFDomainScope": true, "SPFResult": true };
|
api.stringsTypes = { "Align": true, "Alignment": true, "CSRFToken": true, "DKIMResult": true, "DMARCPolicy": true, "DMARCResult": true, "Disposition": true, "IP": true, "Localpart": true, "Mode": true, "PolicyOverride": true, "PolicyType": true, "RUA": true, "ResultType": true, "SPFDomainScope": true, "SPFResult": true };
|
||||||
api.intsTypes = {};
|
api.intsTypes = {};
|
||||||
api.types = {
|
api.types = {
|
||||||
|
@ -372,7 +372,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"] }] },
|
"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"] }] },
|
"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"] }] },
|
"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": "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"] }] },
|
"ConfigDomain": { "Name": "ConfigDomain", "Docs": "", "Fields": [{ "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"] }] },
|
||||||
"DKIM": { "Name": "DKIM", "Docs": "", "Fields": [{ "Name": "Selectors", "Docs": "", "Typewords": ["{}", "Selector"] }, { "Name": "Sign", "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"] }] },
|
"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"] }] },
|
"Canonicalization": { "Name": "Canonicalization", "Docs": "", "Fields": [{ "Name": "HeaderRelaxed", "Docs": "", "Typewords": ["bool"] }, { "Name": "BodyRelaxed", "Docs": "", "Typewords": ["bool"] }] },
|
||||||
|
@ -380,14 +380,18 @@ var api;
|
||||||
"MTASTS": { "Name": "MTASTS", "Docs": "", "Fields": [{ "Name": "PolicyID", "Docs": "", "Typewords": ["string"] }, { "Name": "Mode", "Docs": "", "Typewords": ["Mode"] }, { "Name": "MaxAge", "Docs": "", "Typewords": ["int64"] }, { "Name": "MX", "Docs": "", "Typewords": ["[]", "string"] }] },
|
"MTASTS": { "Name": "MTASTS", "Docs": "", "Fields": [{ "Name": "PolicyID", "Docs": "", "Typewords": ["string"] }, { "Name": "Mode", "Docs": "", "Typewords": ["Mode"] }, { "Name": "MaxAge", "Docs": "", "Typewords": ["int64"] }, { "Name": "MX", "Docs": "", "Typewords": ["[]", "string"] }] },
|
||||||
"TLSRPT": { "Name": "TLSRPT", "Docs": "", "Fields": [{ "Name": "Localpart", "Docs": "", "Typewords": ["string"] }, { "Name": "Domain", "Docs": "", "Typewords": ["string"] }, { "Name": "Account", "Docs": "", "Typewords": ["string"] }, { "Name": "Mailbox", "Docs": "", "Typewords": ["string"] }, { "Name": "ParsedLocalpart", "Docs": "", "Typewords": ["Localpart"] }, { "Name": "DNSDomain", "Docs": "", "Typewords": ["Domain"] }] },
|
"TLSRPT": { "Name": "TLSRPT", "Docs": "", "Fields": [{ "Name": "Localpart", "Docs": "", "Typewords": ["string"] }, { "Name": "Domain", "Docs": "", "Typewords": ["string"] }, { "Name": "Account", "Docs": "", "Typewords": ["string"] }, { "Name": "Mailbox", "Docs": "", "Typewords": ["string"] }, { "Name": "ParsedLocalpart", "Docs": "", "Typewords": ["Localpart"] }, { "Name": "DNSDomain", "Docs": "", "Typewords": ["Domain"] }] },
|
||||||
"Route": { "Name": "Route", "Docs": "", "Fields": [{ "Name": "FromDomain", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "ToDomain", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "MinimumAttempts", "Docs": "", "Typewords": ["int32"] }, { "Name": "Transport", "Docs": "", "Typewords": ["string"] }, { "Name": "FromDomainASCII", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "ToDomainASCII", "Docs": "", "Typewords": ["[]", "string"] }] },
|
"Route": { "Name": "Route", "Docs": "", "Fields": [{ "Name": "FromDomain", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "ToDomain", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "MinimumAttempts", "Docs": "", "Typewords": ["int32"] }, { "Name": "Transport", "Docs": "", "Typewords": ["string"] }, { "Name": "FromDomainASCII", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "ToDomainASCII", "Docs": "", "Typewords": ["[]", "string"] }] },
|
||||||
"Account": { "Name": "Account", "Docs": "", "Fields": [{ "Name": "OutgoingWebhook", "Docs": "", "Typewords": ["nullable", "OutgoingWebhook"] }, { "Name": "IncomingWebhook", "Docs": "", "Typewords": ["nullable", "IncomingWebhook"] }, { "Name": "FromIDLoginAddresses", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "KeepRetiredMessagePeriod", "Docs": "", "Typewords": ["int64"] }, { "Name": "KeepRetiredWebhookPeriod", "Docs": "", "Typewords": ["int64"] }, { "Name": "Domain", "Docs": "", "Typewords": ["string"] }, { "Name": "Description", "Docs": "", "Typewords": ["string"] }, { "Name": "FullName", "Docs": "", "Typewords": ["string"] }, { "Name": "Destinations", "Docs": "", "Typewords": ["{}", "Destination"] }, { "Name": "SubjectPass", "Docs": "", "Typewords": ["SubjectPass"] }, { "Name": "QuotaMessageSize", "Docs": "", "Typewords": ["int64"] }, { "Name": "RejectsMailbox", "Docs": "", "Typewords": ["string"] }, { "Name": "KeepRejects", "Docs": "", "Typewords": ["bool"] }, { "Name": "AutomaticJunkFlags", "Docs": "", "Typewords": ["AutomaticJunkFlags"] }, { "Name": "JunkFilter", "Docs": "", "Typewords": ["nullable", "JunkFilter"] }, { "Name": "MaxOutgoingMessagesPerDay", "Docs": "", "Typewords": ["int32"] }, { "Name": "MaxFirstTimeRecipientsPerDay", "Docs": "", "Typewords": ["int32"] }, { "Name": "NoFirstTimeSenderDelay", "Docs": "", "Typewords": ["bool"] }, { "Name": "Routes", "Docs": "", "Typewords": ["[]", "Route"] }, { "Name": "DNSDomain", "Docs": "", "Typewords": ["Domain"] }] },
|
"Alias": { "Name": "Alias", "Docs": "", "Fields": [{ "Name": "Addresses", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "PostPublic", "Docs": "", "Typewords": ["bool"] }, { "Name": "ListMembers", "Docs": "", "Typewords": ["bool"] }, { "Name": "AllowMsgFrom", "Docs": "", "Typewords": ["bool"] }, { "Name": "LocalpartStr", "Docs": "", "Typewords": ["string"] }, { "Name": "Domain", "Docs": "", "Typewords": ["Domain"] }, { "Name": "ParsedAddresses", "Docs": "", "Typewords": ["[]", "AliasAddress"] }] },
|
||||||
"OutgoingWebhook": { "Name": "OutgoingWebhook", "Docs": "", "Fields": [{ "Name": "URL", "Docs": "", "Typewords": ["string"] }, { "Name": "Authorization", "Docs": "", "Typewords": ["string"] }, { "Name": "Events", "Docs": "", "Typewords": ["[]", "string"] }] },
|
"AliasAddress": { "Name": "AliasAddress", "Docs": "", "Fields": [{ "Name": "Address", "Docs": "", "Typewords": ["Address"] }, { "Name": "AccountName", "Docs": "", "Typewords": ["string"] }, { "Name": "Destination", "Docs": "", "Typewords": ["Destination"] }] },
|
||||||
"IncomingWebhook": { "Name": "IncomingWebhook", "Docs": "", "Fields": [{ "Name": "URL", "Docs": "", "Typewords": ["string"] }, { "Name": "Authorization", "Docs": "", "Typewords": ["string"] }] },
|
"Address": { "Name": "Address", "Docs": "", "Fields": [{ "Name": "Localpart", "Docs": "", "Typewords": ["Localpart"] }, { "Name": "Domain", "Docs": "", "Typewords": ["Domain"] }] },
|
||||||
"Destination": { "Name": "Destination", "Docs": "", "Fields": [{ "Name": "Mailbox", "Docs": "", "Typewords": ["string"] }, { "Name": "Rulesets", "Docs": "", "Typewords": ["[]", "Ruleset"] }, { "Name": "FullName", "Docs": "", "Typewords": ["string"] }] },
|
"Destination": { "Name": "Destination", "Docs": "", "Fields": [{ "Name": "Mailbox", "Docs": "", "Typewords": ["string"] }, { "Name": "Rulesets", "Docs": "", "Typewords": ["[]", "Ruleset"] }, { "Name": "FullName", "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"] }] },
|
"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"] }] },
|
||||||
|
"Account": { "Name": "Account", "Docs": "", "Fields": [{ "Name": "OutgoingWebhook", "Docs": "", "Typewords": ["nullable", "OutgoingWebhook"] }, { "Name": "IncomingWebhook", "Docs": "", "Typewords": ["nullable", "IncomingWebhook"] }, { "Name": "FromIDLoginAddresses", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "KeepRetiredMessagePeriod", "Docs": "", "Typewords": ["int64"] }, { "Name": "KeepRetiredWebhookPeriod", "Docs": "", "Typewords": ["int64"] }, { "Name": "Domain", "Docs": "", "Typewords": ["string"] }, { "Name": "Description", "Docs": "", "Typewords": ["string"] }, { "Name": "FullName", "Docs": "", "Typewords": ["string"] }, { "Name": "Destinations", "Docs": "", "Typewords": ["{}", "Destination"] }, { "Name": "SubjectPass", "Docs": "", "Typewords": ["SubjectPass"] }, { "Name": "QuotaMessageSize", "Docs": "", "Typewords": ["int64"] }, { "Name": "RejectsMailbox", "Docs": "", "Typewords": ["string"] }, { "Name": "KeepRejects", "Docs": "", "Typewords": ["bool"] }, { "Name": "AutomaticJunkFlags", "Docs": "", "Typewords": ["AutomaticJunkFlags"] }, { "Name": "JunkFilter", "Docs": "", "Typewords": ["nullable", "JunkFilter"] }, { "Name": "MaxOutgoingMessagesPerDay", "Docs": "", "Typewords": ["int32"] }, { "Name": "MaxFirstTimeRecipientsPerDay", "Docs": "", "Typewords": ["int32"] }, { "Name": "NoFirstTimeSenderDelay", "Docs": "", "Typewords": ["bool"] }, { "Name": "Routes", "Docs": "", "Typewords": ["[]", "Route"] }, { "Name": "DNSDomain", "Docs": "", "Typewords": ["Domain"] }, { "Name": "Aliases", "Docs": "", "Typewords": ["[]", "AddressAlias"] }] },
|
||||||
|
"OutgoingWebhook": { "Name": "OutgoingWebhook", "Docs": "", "Fields": [{ "Name": "URL", "Docs": "", "Typewords": ["string"] }, { "Name": "Authorization", "Docs": "", "Typewords": ["string"] }, { "Name": "Events", "Docs": "", "Typewords": ["[]", "string"] }] },
|
||||||
|
"IncomingWebhook": { "Name": "IncomingWebhook", "Docs": "", "Fields": [{ "Name": "URL", "Docs": "", "Typewords": ["string"] }, { "Name": "Authorization", "Docs": "", "Typewords": ["string"] }] },
|
||||||
"SubjectPass": { "Name": "SubjectPass", "Docs": "", "Fields": [{ "Name": "Period", "Docs": "", "Typewords": ["int64"] }] },
|
"SubjectPass": { "Name": "SubjectPass", "Docs": "", "Fields": [{ "Name": "Period", "Docs": "", "Typewords": ["int64"] }] },
|
||||||
"AutomaticJunkFlags": { "Name": "AutomaticJunkFlags", "Docs": "", "Fields": [{ "Name": "Enabled", "Docs": "", "Typewords": ["bool"] }, { "Name": "JunkMailboxRegexp", "Docs": "", "Typewords": ["string"] }, { "Name": "NeutralMailboxRegexp", "Docs": "", "Typewords": ["string"] }, { "Name": "NotJunkMailboxRegexp", "Docs": "", "Typewords": ["string"] }] },
|
"AutomaticJunkFlags": { "Name": "AutomaticJunkFlags", "Docs": "", "Fields": [{ "Name": "Enabled", "Docs": "", "Typewords": ["bool"] }, { "Name": "JunkMailboxRegexp", "Docs": "", "Typewords": ["string"] }, { "Name": "NeutralMailboxRegexp", "Docs": "", "Typewords": ["string"] }, { "Name": "NotJunkMailboxRegexp", "Docs": "", "Typewords": ["string"] }] },
|
||||||
"JunkFilter": { "Name": "JunkFilter", "Docs": "", "Fields": [{ "Name": "Threshold", "Docs": "", "Typewords": ["float64"] }, { "Name": "Onegrams", "Docs": "", "Typewords": ["bool"] }, { "Name": "Twograms", "Docs": "", "Typewords": ["bool"] }, { "Name": "Threegrams", "Docs": "", "Typewords": ["bool"] }, { "Name": "MaxPower", "Docs": "", "Typewords": ["float64"] }, { "Name": "TopWords", "Docs": "", "Typewords": ["int32"] }, { "Name": "IgnoreWords", "Docs": "", "Typewords": ["float64"] }, { "Name": "RareWords", "Docs": "", "Typewords": ["int32"] }] },
|
"JunkFilter": { "Name": "JunkFilter", "Docs": "", "Fields": [{ "Name": "Threshold", "Docs": "", "Typewords": ["float64"] }, { "Name": "Onegrams", "Docs": "", "Typewords": ["bool"] }, { "Name": "Twograms", "Docs": "", "Typewords": ["bool"] }, { "Name": "Threegrams", "Docs": "", "Typewords": ["bool"] }, { "Name": "MaxPower", "Docs": "", "Typewords": ["float64"] }, { "Name": "TopWords", "Docs": "", "Typewords": ["int32"] }, { "Name": "IgnoreWords", "Docs": "", "Typewords": ["float64"] }, { "Name": "RareWords", "Docs": "", "Typewords": ["int32"] }] },
|
||||||
|
"AddressAlias": { "Name": "AddressAlias", "Docs": "", "Fields": [{ "Name": "SubscriptionAddress", "Docs": "", "Typewords": ["string"] }, { "Name": "Alias", "Docs": "", "Typewords": ["Alias"] }, { "Name": "MemberAddresses", "Docs": "", "Typewords": ["[]", "string"] }] },
|
||||||
"PolicyRecord": { "Name": "PolicyRecord", "Docs": "", "Fields": [{ "Name": "Domain", "Docs": "", "Typewords": ["string"] }, { "Name": "Inserted", "Docs": "", "Typewords": ["timestamp"] }, { "Name": "ValidEnd", "Docs": "", "Typewords": ["timestamp"] }, { "Name": "LastUpdate", "Docs": "", "Typewords": ["timestamp"] }, { "Name": "LastUse", "Docs": "", "Typewords": ["timestamp"] }, { "Name": "Backoff", "Docs": "", "Typewords": ["bool"] }, { "Name": "RecordID", "Docs": "", "Typewords": ["string"] }, { "Name": "Version", "Docs": "", "Typewords": ["string"] }, { "Name": "Mode", "Docs": "", "Typewords": ["Mode"] }, { "Name": "MX", "Docs": "", "Typewords": ["[]", "STSMX"] }, { "Name": "MaxAgeSeconds", "Docs": "", "Typewords": ["int32"] }, { "Name": "Extensions", "Docs": "", "Typewords": ["[]", "Pair"] }, { "Name": "PolicyText", "Docs": "", "Typewords": ["string"] }] },
|
"PolicyRecord": { "Name": "PolicyRecord", "Docs": "", "Fields": [{ "Name": "Domain", "Docs": "", "Typewords": ["string"] }, { "Name": "Inserted", "Docs": "", "Typewords": ["timestamp"] }, { "Name": "ValidEnd", "Docs": "", "Typewords": ["timestamp"] }, { "Name": "LastUpdate", "Docs": "", "Typewords": ["timestamp"] }, { "Name": "LastUse", "Docs": "", "Typewords": ["timestamp"] }, { "Name": "Backoff", "Docs": "", "Typewords": ["bool"] }, { "Name": "RecordID", "Docs": "", "Typewords": ["string"] }, { "Name": "Version", "Docs": "", "Typewords": ["string"] }, { "Name": "Mode", "Docs": "", "Typewords": ["Mode"] }, { "Name": "MX", "Docs": "", "Typewords": ["[]", "STSMX"] }, { "Name": "MaxAgeSeconds", "Docs": "", "Typewords": ["int32"] }, { "Name": "Extensions", "Docs": "", "Typewords": ["[]", "Pair"] }, { "Name": "PolicyText", "Docs": "", "Typewords": ["string"] }] },
|
||||||
"TLSReportRecord": { "Name": "TLSReportRecord", "Docs": "", "Fields": [{ "Name": "ID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Domain", "Docs": "", "Typewords": ["string"] }, { "Name": "FromDomain", "Docs": "", "Typewords": ["string"] }, { "Name": "MailFrom", "Docs": "", "Typewords": ["string"] }, { "Name": "HostReport", "Docs": "", "Typewords": ["bool"] }, { "Name": "Report", "Docs": "", "Typewords": ["Report"] }] },
|
"TLSReportRecord": { "Name": "TLSReportRecord", "Docs": "", "Fields": [{ "Name": "ID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Domain", "Docs": "", "Typewords": ["string"] }, { "Name": "FromDomain", "Docs": "", "Typewords": ["string"] }, { "Name": "MailFrom", "Docs": "", "Typewords": ["string"] }, { "Name": "HostReport", "Docs": "", "Typewords": ["bool"] }, { "Name": "Report", "Docs": "", "Typewords": ["Report"] }] },
|
||||||
"Report": { "Name": "Report", "Docs": "", "Fields": [{ "Name": "OrganizationName", "Docs": "", "Typewords": ["string"] }, { "Name": "DateRange", "Docs": "", "Typewords": ["TLSRPTDateRange"] }, { "Name": "ContactInfo", "Docs": "", "Typewords": ["string"] }, { "Name": "ReportID", "Docs": "", "Typewords": ["string"] }, { "Name": "Policies", "Docs": "", "Typewords": ["[]", "Result"] }] },
|
"Report": { "Name": "Report", "Docs": "", "Fields": [{ "Name": "OrganizationName", "Docs": "", "Typewords": ["string"] }, { "Name": "DateRange", "Docs": "", "Typewords": ["TLSRPTDateRange"] }, { "Name": "ContactInfo", "Docs": "", "Typewords": ["string"] }, { "Name": "ReportID", "Docs": "", "Typewords": ["string"] }, { "Name": "Policies", "Docs": "", "Typewords": ["[]", "Result"] }] },
|
||||||
|
@ -502,14 +506,18 @@ var api;
|
||||||
MTASTS: (v) => api.parse("MTASTS", v),
|
MTASTS: (v) => api.parse("MTASTS", v),
|
||||||
TLSRPT: (v) => api.parse("TLSRPT", v),
|
TLSRPT: (v) => api.parse("TLSRPT", v),
|
||||||
Route: (v) => api.parse("Route", v),
|
Route: (v) => api.parse("Route", v),
|
||||||
|
Alias: (v) => api.parse("Alias", v),
|
||||||
|
AliasAddress: (v) => api.parse("AliasAddress", v),
|
||||||
|
Address: (v) => api.parse("Address", v),
|
||||||
|
Destination: (v) => api.parse("Destination", v),
|
||||||
|
Ruleset: (v) => api.parse("Ruleset", v),
|
||||||
Account: (v) => api.parse("Account", v),
|
Account: (v) => api.parse("Account", v),
|
||||||
OutgoingWebhook: (v) => api.parse("OutgoingWebhook", v),
|
OutgoingWebhook: (v) => api.parse("OutgoingWebhook", v),
|
||||||
IncomingWebhook: (v) => api.parse("IncomingWebhook", v),
|
IncomingWebhook: (v) => api.parse("IncomingWebhook", v),
|
||||||
Destination: (v) => api.parse("Destination", v),
|
|
||||||
Ruleset: (v) => api.parse("Ruleset", v),
|
|
||||||
SubjectPass: (v) => api.parse("SubjectPass", v),
|
SubjectPass: (v) => api.parse("SubjectPass", v),
|
||||||
AutomaticJunkFlags: (v) => api.parse("AutomaticJunkFlags", v),
|
AutomaticJunkFlags: (v) => api.parse("AutomaticJunkFlags", v),
|
||||||
JunkFilter: (v) => api.parse("JunkFilter", v),
|
JunkFilter: (v) => api.parse("JunkFilter", v),
|
||||||
|
AddressAlias: (v) => api.parse("AddressAlias", v),
|
||||||
PolicyRecord: (v) => api.parse("PolicyRecord", v),
|
PolicyRecord: (v) => api.parse("PolicyRecord", v),
|
||||||
TLSReportRecord: (v) => api.parse("TLSReportRecord", v),
|
TLSReportRecord: (v) => api.parse("TLSReportRecord", v),
|
||||||
Report: (v) => api.parse("Report", v),
|
Report: (v) => api.parse("Report", v),
|
||||||
|
@ -680,7 +688,7 @@ var api;
|
||||||
async DomainLocalparts(domain) {
|
async DomainLocalparts(domain) {
|
||||||
const fn = "DomainLocalparts";
|
const fn = "DomainLocalparts";
|
||||||
const paramTypes = [["string"]];
|
const paramTypes = [["string"]];
|
||||||
const returnTypes = [["{}", "string"]];
|
const returnTypes = [["{}", "string"], ["{}", "Alias"]];
|
||||||
const params = [domain];
|
const params = [domain];
|
||||||
return await _sherpaCall(this.baseURL, this.authState, { ...this.options }, paramTypes, returnTypes, fn, params);
|
return await _sherpaCall(this.baseURL, this.authState, { ...this.options }, paramTypes, returnTypes, fn, params);
|
||||||
}
|
}
|
||||||
|
@ -1351,6 +1359,41 @@ var api;
|
||||||
const params = [domainName, selectors, sign];
|
const params = [domainName, selectors, sign];
|
||||||
return await _sherpaCall(this.baseURL, this.authState, { ...this.options }, paramTypes, returnTypes, fn, params);
|
return await _sherpaCall(this.baseURL, this.authState, { ...this.options }, paramTypes, returnTypes, fn, params);
|
||||||
}
|
}
|
||||||
|
async AliasAdd(aliaslp, domainName, alias) {
|
||||||
|
const fn = "AliasAdd";
|
||||||
|
const paramTypes = [["string"], ["string"], ["Alias"]];
|
||||||
|
const returnTypes = [];
|
||||||
|
const params = [aliaslp, domainName, alias];
|
||||||
|
return await _sherpaCall(this.baseURL, this.authState, { ...this.options }, paramTypes, returnTypes, fn, params);
|
||||||
|
}
|
||||||
|
async AliasUpdate(aliaslp, domainName, postPublic, listMembers, allowMsgFrom) {
|
||||||
|
const fn = "AliasUpdate";
|
||||||
|
const paramTypes = [["string"], ["string"], ["bool"], ["bool"], ["bool"]];
|
||||||
|
const returnTypes = [];
|
||||||
|
const params = [aliaslp, domainName, postPublic, listMembers, allowMsgFrom];
|
||||||
|
return await _sherpaCall(this.baseURL, this.authState, { ...this.options }, paramTypes, returnTypes, fn, params);
|
||||||
|
}
|
||||||
|
async AliasRemove(aliaslp, domainName) {
|
||||||
|
const fn = "AliasRemove";
|
||||||
|
const paramTypes = [["string"], ["string"]];
|
||||||
|
const returnTypes = [];
|
||||||
|
const params = [aliaslp, domainName];
|
||||||
|
return await _sherpaCall(this.baseURL, this.authState, { ...this.options }, paramTypes, returnTypes, fn, params);
|
||||||
|
}
|
||||||
|
async AliasAddressesAdd(aliaslp, domainName, addresses) {
|
||||||
|
const fn = "AliasAddressesAdd";
|
||||||
|
const paramTypes = [["string"], ["string"], ["[]", "string"]];
|
||||||
|
const returnTypes = [];
|
||||||
|
const params = [aliaslp, domainName, addresses];
|
||||||
|
return await _sherpaCall(this.baseURL, this.authState, { ...this.options }, paramTypes, returnTypes, fn, params);
|
||||||
|
}
|
||||||
|
async AliasAddressesRemove(aliaslp, domainName, addresses) {
|
||||||
|
const fn = "AliasAddressesRemove";
|
||||||
|
const paramTypes = [["string"], ["string"], ["[]", "string"]];
|
||||||
|
const returnTypes = [];
|
||||||
|
const params = [aliaslp, domainName, addresses];
|
||||||
|
return await _sherpaCall(this.baseURL, this.authState, { ...this.options }, paramTypes, returnTypes, fn, params);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
api.Client = Client;
|
api.Client = Client;
|
||||||
api.defaultBaseURL = (function () {
|
api.defaultBaseURL = (function () {
|
||||||
|
@ -2221,7 +2264,10 @@ const account = async (name) => {
|
||||||
await check(fieldset, client.AddressAdd(address, name));
|
await check(fieldset, client.AddressAdd(address, name));
|
||||||
form.reset();
|
form.reset();
|
||||||
window.location.reload(); // todo: only reload the destinations
|
window.location.reload(); // todo: only reload the destinations
|
||||||
}, fieldset = dom.fieldset(dom.label(style({ display: 'inline-block' }), dom.span('Localpart', attr.title('The localpart is the part before the "@"-sign of an email address. If empty, a catchall address is configured for the domain.')), dom.br(), localpart = dom.input()), '@', dom.label(style({ display: 'inline-block' }), dom.span('Domain'), dom.br(), domain = dom.select((domains || []).map(d => dom.option(domainName(d), domainName(d) === config.Domain ? attr.selected('') : [])))), ' ', dom.submitbutton('Add address'))), dom.br(), dom.h2('Settings'), dom.form(fieldsetSettings = dom.fieldset(dom.label(style({ display: 'block', marginBottom: '.5ex' }), dom.span('Maximum outgoing messages per day', attr.title('Maximum number of outgoing messages for this account in a 24 hour window. This limits the damage to recipients and the reputation of this mail server in case of account compromise. Default 1000. MaxOutgoingMessagesPerDay in configuration file.')), dom.br(), maxOutgoingMessagesPerDay = dom.input(attr.type('number'), attr.required(''), attr.value('' + (config.MaxOutgoingMessagesPerDay || 1000)))), dom.label(style({ display: 'block', marginBottom: '.5ex' }), dom.span('Maximum first-time recipients per day', attr.title('Maximum number of first-time recipients in outgoing messages for this account in a 24 hour window. This limits the damage to recipients and the reputation of this mail server in case of account compromise. Default 200. MaxFirstTimeRecipientsPerDay in configuration file.')), dom.br(), maxFirstTimeRecipientsPerDay = dom.input(attr.type('number'), attr.required(''), attr.value('' + (config.MaxFirstTimeRecipientsPerDay || 200)))), dom.label(style({ display: 'block', marginBottom: '.5ex' }), dom.span('Disk usage quota: Maximum total message size ', attr.title('Default maximum total message size in bytes for the account, overriding any globally configured default maximum size if non-zero. A negative value can be used to have no limit in case there is a limit by default. Attempting to add new messages to an account beyond its maximum total size will result in an error. Useful to prevent a single account from filling storage.')), dom.br(), quotaMessageSize = dom.input(attr.value(formatQuotaSize(config.QuotaMessageSize))), ' Current usage is ', formatQuotaSize(Math.floor(diskUsage / (1024 * 1024)) * 1024 * 1024), '.'), dom.div(style({ display: 'block', marginBottom: '.5ex' }), dom.label(firstTimeSenderDelay = dom.input(attr.type('checkbox'), config.NoFirstTimeSenderDelay ? [] : attr.checked('')), ' ', dom.span('Delay deliveries from first-time senders.', attr.title('To slow down potential spammers, when the message is misclassified as non-junk. Turning off the delay can be useful when the account processes messages automatically and needs fast responses.')))), dom.submitbutton('Save')), async function submit(e) {
|
}, fieldset = dom.fieldset(dom.label(style({ display: 'inline-block' }), dom.span('Localpart', attr.title('The localpart is the part before the "@"-sign of an email address. If empty, a catchall address is configured for the domain.')), dom.br(), localpart = dom.input()), '@', dom.label(style({ display: 'inline-block' }), dom.span('Domain'), dom.br(), domain = dom.select((domains || []).map(d => dom.option(domainName(d), domainName(d) === config.Domain ? attr.selected('') : [])))), ' ', dom.submitbutton('Add address'))), dom.br(), dom.h2('Aliases/lists'), dom.table(dom.thead(dom.tr(dom.th('Alias address'), dom.th('Subscription address'), dom.th('Allowed senders', attr.title('Whether only members can send through the alias/list, or anyone.')), dom.th('Send as alias address', attr.title('If enabled, messages can be sent with the alias address in the message "From" header.')), dom.th('Members visible', attr.title('If enabled, members can see the addresses of other members.')))), (config.Aliases || []).length === 0 ? dom.tr(dom.td(attr.colspan('6'), 'None')) : [], (config.Aliases || []).sort((a, b) => a.Alias.LocalpartStr < b.Alias.LocalpartStr ? -1 : (domainName(a.Alias.Domain) < domainName(b.Alias.Domain) ? -1 : 1)).map(a => dom.tr(dom.td(dom.a(a.Alias.LocalpartStr, '@', domainName(a.Alias.Domain), attr.href('#domains/' + domainName(a.Alias.Domain) + '/alias/' + encodeURIComponent(a.Alias.LocalpartStr)))), dom.td(a.SubscriptionAddress), dom.td(a.Alias.PostPublic ? 'Anyone' : 'Members only'), dom.td(a.Alias.AllowMsgFrom ? 'Yes' : 'No'), dom.td(a.Alias.ListMembers ? 'Yes' : 'No'), dom.td(dom.clickbutton('Remove', async function click(e) {
|
||||||
|
await check(e.target, client.AliasAddressesRemove(a.Alias.LocalpartStr, domainName(a.Alias.Domain), [a.SubscriptionAddress]));
|
||||||
|
window.location.reload(); // todo: reload less
|
||||||
|
}))))), dom.br(), dom.h2('Settings'), dom.form(fieldsetSettings = dom.fieldset(dom.label(style({ display: 'block', marginBottom: '.5ex' }), dom.span('Maximum outgoing messages per day', attr.title('Maximum number of outgoing messages for this account in a 24 hour window. This limits the damage to recipients and the reputation of this mail server in case of account compromise. Default 1000. MaxOutgoingMessagesPerDay in configuration file.')), dom.br(), maxOutgoingMessagesPerDay = dom.input(attr.type('number'), attr.required(''), attr.value('' + (config.MaxOutgoingMessagesPerDay || 1000)))), dom.label(style({ display: 'block', marginBottom: '.5ex' }), dom.span('Maximum first-time recipients per day', attr.title('Maximum number of first-time recipients in outgoing messages for this account in a 24 hour window. This limits the damage to recipients and the reputation of this mail server in case of account compromise. Default 200. MaxFirstTimeRecipientsPerDay in configuration file.')), dom.br(), maxFirstTimeRecipientsPerDay = dom.input(attr.type('number'), attr.required(''), attr.value('' + (config.MaxFirstTimeRecipientsPerDay || 200)))), dom.label(style({ display: 'block', marginBottom: '.5ex' }), dom.span('Disk usage quota: Maximum total message size ', attr.title('Default maximum total message size in bytes for the account, overriding any globally configured default maximum size if non-zero. A negative value can be used to have no limit in case there is a limit by default. Attempting to add new messages to an account beyond its maximum total size will result in an error. Useful to prevent a single account from filling storage.')), dom.br(), quotaMessageSize = dom.input(attr.value(formatQuotaSize(config.QuotaMessageSize))), ' Current usage is ', formatQuotaSize(Math.floor(diskUsage / (1024 * 1024)) * 1024 * 1024), '.'), dom.div(style({ display: 'block', marginBottom: '.5ex' }), dom.label(firstTimeSenderDelay = dom.input(attr.type('checkbox'), config.NoFirstTimeSenderDelay ? [] : attr.checked('')), ' ', dom.span('Delay deliveries from first-time senders.', attr.title('To slow down potential spammers, when the message is misclassified as non-junk. Turning off the delay can be useful when the account processes messages automatically and needs fast responses.')))), dom.submitbutton('Save')), async function submit(e) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
await check(fieldsetSettings, client.AccountSettingsSave(name, parseInt(maxOutgoingMessagesPerDay.value) || 0, parseInt(maxFirstTimeRecipientsPerDay.value) || 0, xparseSize(quotaMessageSize.value), firstTimeSenderDelay.checked));
|
await check(fieldsetSettings, client.AccountSettingsSave(name, parseInt(maxOutgoingMessagesPerDay.value) || 0, parseInt(maxFirstTimeRecipientsPerDay.value) || 0, xparseSize(quotaMessageSize.value), firstTimeSenderDelay.checked));
|
||||||
|
@ -2312,7 +2358,7 @@ const formatDuration = (v, goDuration) => {
|
||||||
const domain = async (d) => {
|
const domain = async (d) => {
|
||||||
const end = new Date();
|
const end = new Date();
|
||||||
const start = new Date(new Date().getTime() - 30 * 24 * 3600 * 1000);
|
const start = new Date(new Date().getTime() - 30 * 24 * 3600 * 1000);
|
||||||
const [dmarcSummaries, tlsrptSummaries, localpartAccounts, dnsdomain, clientConfigs, accounts, domainConfig, transports] = await Promise.all([
|
const [dmarcSummaries, tlsrptSummaries, [localpartAccounts, localpartAliases], dnsdomain, clientConfigs, accounts, domainConfig, transports] = await Promise.all([
|
||||||
client.DMARCSummaries(start, end, d),
|
client.DMARCSummaries(start, end, d),
|
||||||
client.TLSRPTSummaries(start, end, d),
|
client.TLSRPTSummaries(start, end, d),
|
||||||
client.DomainLocalparts(d),
|
client.DomainLocalparts(d),
|
||||||
|
@ -2326,6 +2372,9 @@ const domain = async (d) => {
|
||||||
let addrFieldset;
|
let addrFieldset;
|
||||||
let addrLocalpart;
|
let addrLocalpart;
|
||||||
let addrAccount;
|
let addrAccount;
|
||||||
|
let aliasFieldset;
|
||||||
|
let aliasLocalpart;
|
||||||
|
let aliasAddresses;
|
||||||
let descrFieldset;
|
let descrFieldset;
|
||||||
let descrText;
|
let descrText;
|
||||||
let clientSettingsDomainFieldset;
|
let clientSettingsDomainFieldset;
|
||||||
|
@ -2401,7 +2450,23 @@ const domain = async (d) => {
|
||||||
await check(addrFieldset, client.AddressAdd(addrLocalpart.value + '@' + d, addrAccount.value));
|
await check(addrFieldset, client.AddressAdd(addrLocalpart.value + '@' + d, addrAccount.value));
|
||||||
addrForm.reset();
|
addrForm.reset();
|
||||||
window.location.reload(); // todo: only reload the addresses
|
window.location.reload(); // todo: only reload the addresses
|
||||||
}, addrFieldset = dom.fieldset(dom.label(style({ display: 'inline-block' }), dom.span('Localpart', attr.title('The localpart is the part before the "@"-sign of an address. An empty localpart is the catchall destination/address for the domain.')), dom.br(), addrLocalpart = dom.input()), '@', domainName(dnsdomain), ' ', dom.label(style({ display: 'inline-block' }), dom.span('Account', attr.title('Account to assign the address to.')), dom.br(), addrAccount = dom.select(attr.required(''), (accounts || []).map(a => dom.option(a)))), ' ', dom.submitbutton('Add address', attr.title('Address will be added and the config reloaded.')))), dom.br(), RoutesEditor('domain-specific', transports, domainConfig.Routes || [], async (routes) => await client.DomainRoutesSave(d, routes)), dom.br(), dom.h2('Settings'), dom.form(async function submit(e) {
|
}, addrFieldset = dom.fieldset(dom.label(style({ display: 'inline-block' }), dom.span('Localpart', attr.title('The localpart is the part before the "@"-sign of an address. An empty localpart is the catchall destination/address for the domain.')), dom.br(), addrLocalpart = dom.input()), '@', domainName(dnsdomain), ' ', dom.label(style({ display: 'inline-block' }), dom.span('Account', attr.title('Account to assign the address to.')), dom.br(), addrAccount = dom.select(attr.required(''), (accounts || []).map(a => dom.option(a)))), ' ', dom.submitbutton('Add address', attr.title('Address will be added and the config reloaded.')))), dom.br(), dom.h2('Aliases/lists'), dom.table(dom.thead(dom.tr(dom.th('Address'), dom.th('Allowed senders', attr.title('Whether only members can send through the alias/list, or anyone.')), dom.th('Send as alias address', attr.title('If enabled, messages can be sent with the alias address in the message "From" header.')), dom.th('Members visible', attr.title('If enabled, members can see the addresses of other members.')))), Object.values(localpartAliases).length === 0 ? dom.tr(dom.td(attr.colspan('4'), 'None')) : [], Object.values(localpartAliases).sort((a, b) => a.LocalpartStr < b.LocalpartStr ? -1 : 1).map(a => {
|
||||||
|
return dom.tr(dom.td(dom.a(a.LocalpartStr, attr.href('#domains/' + d + '/alias/' + encodeURIComponent(a.LocalpartStr)))), dom.td(a.PostPublic ? 'Anyone' : 'Members only'), dom.td(a.AllowMsgFrom ? 'Yes' : 'No'), dom.td(a.ListMembers ? 'Yes' : 'No'));
|
||||||
|
})), dom.br(), dom.h2('Add alias'), dom.form(async function submit(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
const alias = {
|
||||||
|
Addresses: aliasAddresses.value.split('\n').map(s => s.trim()).filter(s => !!s),
|
||||||
|
PostPublic: false,
|
||||||
|
ListMembers: false,
|
||||||
|
AllowMsgFrom: false,
|
||||||
|
// Ignored:
|
||||||
|
LocalpartStr: '',
|
||||||
|
Domain: dnsdomain,
|
||||||
|
};
|
||||||
|
await check(aliasFieldset, client.AliasAdd(aliasLocalpart.value, d, alias));
|
||||||
|
window.location.hash = '#domains/' + d + '/alias/' + aliasLocalpart.value;
|
||||||
|
}, aliasFieldset = dom.fieldset(style({ display: 'flex', alignItems: 'flex-start', gap: '1em' }), dom.label(dom.div('Localpart', attr.title('The localpart is the part before the "@"-sign of an address.')), aliasLocalpart = dom.input(attr.required('')), '@', domainName(dnsdomain), ' '), dom.label(dom.div('Addresses', attr.title('One members address per line, full address of form localpart@domain. At least one address required.')), aliasAddresses = dom.textarea(attr.required(''), attr.rows('1'), function focus() { aliasAddresses.setAttribute('rows', '5'); })), dom.div(dom.div('\u00a0'), dom.submitbutton('Add alias', attr.title('Alias will be added and the config reloaded.'))))), dom.br(), RoutesEditor('domain-specific', transports, domainConfig.Routes || [], async (routes) => await client.DomainRoutesSave(d, routes)), dom.br(), dom.h2('Settings'), dom.form(async function submit(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
await check(descrFieldset, client.DomainDescriptionSave(d, descrText.value));
|
await check(descrFieldset, client.DomainDescriptionSave(d, descrText.value));
|
||||||
|
@ -2571,6 +2636,44 @@ const domain = async (d) => {
|
||||||
window.location.hash = '#';
|
window.location.hash = '#';
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
const domainAlias = async (d, aliasLocalpart) => {
|
||||||
|
const domain = await client.DomainConfig(d);
|
||||||
|
const alias = (domain.Aliases || {})[aliasLocalpart];
|
||||||
|
if (!alias) {
|
||||||
|
throw new Error('alias not found');
|
||||||
|
}
|
||||||
|
let aliasFieldset;
|
||||||
|
let postPublic;
|
||||||
|
let listMembers;
|
||||||
|
let allowMsgFrom;
|
||||||
|
let addFieldset;
|
||||||
|
let addAddress;
|
||||||
|
let delFieldset;
|
||||||
|
dom._kids(page, crumbs(crumblink('Mox Admin', '#'), crumblink('Domain ' + domainString(domain.Domain), '#domains/' + d), 'Alias ' + aliasLocalpart + '@' + domainName(domain.Domain)), dom.h2('Alias'), dom.form(async function submit(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
check(aliasFieldset, client.AliasUpdate(aliasLocalpart, d, postPublic.checked, listMembers.checked, allowMsgFrom.checked));
|
||||||
|
}, aliasFieldset = dom.fieldset(style({ display: 'flex', flexDirection: 'column', gap: '.5ex' }), dom.label(postPublic = dom.input(attr.type('checkbox'), alias.PostPublic ? attr.checked('') : []), ' Public, anyone can post instead of only members'), dom.label(listMembers = dom.input(attr.type('checkbox'), alias.ListMembers ? attr.checked('') : []), ' Members can list other members'), dom.label(allowMsgFrom = dom.input(attr.type('checkbox'), alias.AllowMsgFrom ? attr.checked('') : []), ' Allow messages to use the alias address in the message From header'), dom.div(style({ marginTop: '1ex' }), dom.submitbutton('Save')))), dom.br(), dom.h2('Members'), dom.table(dom.thead(dom.tr(dom.th('Address'), dom.th('Account'), dom.th())), dom.tbody((alias.Addresses || []).map((address, index) => {
|
||||||
|
const pa = (alias.ParsedAddresses || [])[index];
|
||||||
|
return dom.tr(dom.td(address), dom.td(dom.a(pa.AccountName, attr.href('#accounts/' + pa.AccountName))), dom.td(dom.clickbutton('Remove', async function click(e) {
|
||||||
|
await check(e.target, client.AliasAddressesRemove(aliasLocalpart, d, [address]));
|
||||||
|
window.location.reload(); // todo: reload less
|
||||||
|
})));
|
||||||
|
})), dom.tfoot(dom.tr(dom.td(attr.colspan('3'), dom.form(async function submit(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
await check(addFieldset, client.AliasAddressesAdd(aliasLocalpart, d, addAddress.value.split('\n').map(s => s.trim()).filter(s => s)));
|
||||||
|
window.location.reload(); // todo: reload less
|
||||||
|
}, addFieldset = dom.fieldset(addAddress = dom.textarea(attr.required(''), attr.rows('1'), attr.placeholder('localpart@domain'), function focus() { addAddress.setAttribute('rows', '5'); }), ' ', dom.submitbutton('Add', style({ verticalAlign: 'top' })))))))), dom.br(), dom.h2('Danger'), dom.form(async function submit(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
if (!confirm('Are you sure you want to remove this alias?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await check(delFieldset, client.AliasRemove(aliasLocalpart, d));
|
||||||
|
window.location.hash = '#domains/' + d;
|
||||||
|
}, delFieldset = dom.fieldset(dom.div(dom.submitbutton('Remove alias')))));
|
||||||
|
};
|
||||||
const domainDNSRecords = async (d) => {
|
const domainDNSRecords = async (d) => {
|
||||||
const [records, dnsdomain] = await Promise.all([
|
const [records, dnsdomain] = await Promise.all([
|
||||||
client.DomainRecords(d),
|
client.DomainRecords(d),
|
||||||
|
@ -4011,6 +4114,9 @@ const init = async () => {
|
||||||
else if (t[0] === 'domains' && t.length === 2) {
|
else if (t[0] === 'domains' && t.length === 2) {
|
||||||
await domain(t[1]);
|
await domain(t[1]);
|
||||||
}
|
}
|
||||||
|
else if (t[0] === 'domains' && t.length === 4 && t[2] === 'alias') {
|
||||||
|
await domainAlias(t[1], t[3]);
|
||||||
|
}
|
||||||
else if (t[0] === 'domains' && t.length === 3 && t[2] === 'dmarc') {
|
else if (t[0] === 'domains' && t.length === 3 && t[2] === 'dmarc') {
|
||||||
await domainDMARC(t[1]);
|
await domainDMARC(t[1]);
|
||||||
}
|
}
|
||||||
|
|
|
@ -875,6 +875,37 @@ const account = async (name: string) => {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
dom.br(),
|
dom.br(),
|
||||||
|
|
||||||
|
dom.h2('Aliases/lists'),
|
||||||
|
dom.table(
|
||||||
|
dom.thead(
|
||||||
|
dom.tr(
|
||||||
|
dom.th('Alias address'),
|
||||||
|
dom.th('Subscription address'),
|
||||||
|
dom.th('Allowed senders', attr.title('Whether only members can send through the alias/list, or anyone.')),
|
||||||
|
dom.th('Send as alias address', attr.title('If enabled, messages can be sent with the alias address in the message "From" header.')),
|
||||||
|
dom.th('Members visible', attr.title('If enabled, members can see the addresses of other members.')),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(config.Aliases || []).length === 0 ? dom.tr(dom.td(attr.colspan('6'), 'None')) : [],
|
||||||
|
(config.Aliases || []).sort((a, b) => a.Alias.LocalpartStr < b.Alias.LocalpartStr ? -1 : (domainName(a.Alias.Domain) < domainName(b.Alias.Domain) ? -1 : 1)).map(a =>
|
||||||
|
dom.tr(
|
||||||
|
dom.td(dom.a(a.Alias.LocalpartStr, '@', domainName(a.Alias.Domain), attr.href('#domains/'+domainName(a.Alias.Domain)+'/alias/'+encodeURIComponent(a.Alias.LocalpartStr)))),
|
||||||
|
dom.td(a.SubscriptionAddress),
|
||||||
|
dom.td(a.Alias.PostPublic ? 'Anyone' : 'Members only'),
|
||||||
|
dom.td(a.Alias.AllowMsgFrom ? 'Yes' : 'No'),
|
||||||
|
dom.td(a.Alias.ListMembers ? 'Yes' : 'No'),
|
||||||
|
dom.td(
|
||||||
|
dom.clickbutton('Remove', async function click(e: MouseEvent) {
|
||||||
|
await check(e.target! as HTMLButtonElement, client.AliasAddressesRemove(a.Alias.LocalpartStr, domainName(a.Alias.Domain), [a.SubscriptionAddress]))
|
||||||
|
window.location.reload() // todo: reload less
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
dom.br(),
|
||||||
|
|
||||||
dom.h2('Settings'),
|
dom.h2('Settings'),
|
||||||
dom.form(
|
dom.form(
|
||||||
fieldsetSettings=dom.fieldset(
|
fieldsetSettings=dom.fieldset(
|
||||||
|
@ -1009,7 +1040,7 @@ const formatDuration = (v: number, goDuration?: boolean) => {
|
||||||
const domain = async (d: string) => {
|
const domain = async (d: string) => {
|
||||||
const end = new Date()
|
const end = new Date()
|
||||||
const start = new Date(new Date().getTime() - 30*24*3600*1000)
|
const start = new Date(new Date().getTime() - 30*24*3600*1000)
|
||||||
const [dmarcSummaries, tlsrptSummaries, localpartAccounts, dnsdomain, clientConfigs, accounts, domainConfig, transports] = await Promise.all([
|
const [dmarcSummaries, tlsrptSummaries, [localpartAccounts, localpartAliases], dnsdomain, clientConfigs, accounts, domainConfig, transports] = await Promise.all([
|
||||||
client.DMARCSummaries(start, end, d),
|
client.DMARCSummaries(start, end, d),
|
||||||
client.TLSRPTSummaries(start, end, d),
|
client.TLSRPTSummaries(start, end, d),
|
||||||
client.DomainLocalparts(d),
|
client.DomainLocalparts(d),
|
||||||
|
@ -1025,6 +1056,10 @@ const domain = async (d: string) => {
|
||||||
let addrLocalpart: HTMLInputElement
|
let addrLocalpart: HTMLInputElement
|
||||||
let addrAccount: HTMLSelectElement
|
let addrAccount: HTMLSelectElement
|
||||||
|
|
||||||
|
let aliasFieldset: HTMLFieldSetElement
|
||||||
|
let aliasLocalpart: HTMLInputElement
|
||||||
|
let aliasAddresses: HTMLTextAreaElement
|
||||||
|
|
||||||
let descrFieldset: HTMLFieldSetElement
|
let descrFieldset: HTMLFieldSetElement
|
||||||
let descrText: HTMLInputElement
|
let descrText: HTMLInputElement
|
||||||
|
|
||||||
|
@ -1247,7 +1282,6 @@ const domain = async (d: string) => {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
dom.br(),
|
dom.br(),
|
||||||
|
|
||||||
dom.h2('Add address'),
|
dom.h2('Add address'),
|
||||||
addrForm=dom.form(
|
addrForm=dom.form(
|
||||||
async function submit(e: SubmitEvent) {
|
async function submit(e: SubmitEvent) {
|
||||||
|
@ -1278,6 +1312,64 @@ const domain = async (d: string) => {
|
||||||
),
|
),
|
||||||
dom.br(),
|
dom.br(),
|
||||||
|
|
||||||
|
dom.h2('Aliases/lists'),
|
||||||
|
dom.table(
|
||||||
|
dom.thead(
|
||||||
|
dom.tr(
|
||||||
|
dom.th('Address'),
|
||||||
|
dom.th('Allowed senders', attr.title('Whether only members can send through the alias/list, or anyone.')),
|
||||||
|
dom.th('Send as alias address', attr.title('If enabled, messages can be sent with the alias address in the message "From" header.')),
|
||||||
|
dom.th('Members visible', attr.title('If enabled, members can see the addresses of other members.')),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Object.values(localpartAliases).length === 0 ? dom.tr(dom.td(attr.colspan('4'), 'None')) : [],
|
||||||
|
Object.values(localpartAliases).sort((a, b) => a.LocalpartStr < b.LocalpartStr ? -1 : 1).map(a => {
|
||||||
|
return dom.tr(
|
||||||
|
dom.td(dom.a(a.LocalpartStr, attr.href('#domains/'+d+'/alias/'+encodeURIComponent(a.LocalpartStr)))),
|
||||||
|
dom.td(a.PostPublic ? 'Anyone' : 'Members only'),
|
||||||
|
dom.td(a.AllowMsgFrom ? 'Yes' : 'No'),
|
||||||
|
dom.td(a.ListMembers ? 'Yes' : 'No'),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
dom.br(),
|
||||||
|
dom.h2('Add alias'),
|
||||||
|
dom.form(
|
||||||
|
async function submit(e: SubmitEvent) {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
const alias: api.Alias = {
|
||||||
|
Addresses: aliasAddresses.value.split('\n').map(s => s.trim()).filter(s => !!s),
|
||||||
|
PostPublic: false,
|
||||||
|
ListMembers: false,
|
||||||
|
AllowMsgFrom: false,
|
||||||
|
// Ignored:
|
||||||
|
LocalpartStr: '',
|
||||||
|
Domain: dnsdomain,
|
||||||
|
}
|
||||||
|
await check(aliasFieldset, client.AliasAdd(aliasLocalpart.value, d, alias))
|
||||||
|
window.location.hash = '#domains/'+d+'/alias/'+aliasLocalpart.value
|
||||||
|
},
|
||||||
|
aliasFieldset=dom.fieldset(
|
||||||
|
style({display: 'flex', alignItems: 'flex-start', gap: '1em'}),
|
||||||
|
dom.label(
|
||||||
|
dom.div('Localpart', attr.title('The localpart is the part before the "@"-sign of an address.')),
|
||||||
|
aliasLocalpart=dom.input(attr.required('')),
|
||||||
|
'@', domainName(dnsdomain),
|
||||||
|
' ',
|
||||||
|
),
|
||||||
|
dom.label(
|
||||||
|
dom.div('Addresses', attr.title('One members address per line, full address of form localpart@domain. At least one address required.')),
|
||||||
|
aliasAddresses=dom.textarea(attr.required(''), attr.rows('1'), function focus() { aliasAddresses.setAttribute('rows', '5') }),
|
||||||
|
),
|
||||||
|
dom.div(
|
||||||
|
dom.div('\u00a0'),
|
||||||
|
dom.submitbutton('Add alias', attr.title('Alias will be added and the config reloaded.')),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
dom.br(),
|
||||||
|
|
||||||
RoutesEditor('domain-specific', transports, domainConfig.Routes || [], async (routes: api.Route[]) => await client.DomainRoutesSave(d, routes)),
|
RoutesEditor('domain-specific', transports, domainConfig.Routes || [], async (routes: api.Route[]) => await client.DomainRoutesSave(d, routes)),
|
||||||
dom.br(),
|
dom.br(),
|
||||||
|
|
||||||
|
@ -1680,6 +1772,122 @@ const domain = async (d: string) => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const domainAlias = async (d: string, aliasLocalpart: string) => {
|
||||||
|
const domain = await client.DomainConfig(d)
|
||||||
|
const alias = (domain.Aliases || {})[aliasLocalpart]
|
||||||
|
if (!alias) {
|
||||||
|
throw new Error('alias not found')
|
||||||
|
}
|
||||||
|
|
||||||
|
let aliasFieldset: HTMLFieldSetElement
|
||||||
|
let postPublic: HTMLInputElement
|
||||||
|
let listMembers: HTMLInputElement
|
||||||
|
let allowMsgFrom: HTMLInputElement
|
||||||
|
|
||||||
|
let addFieldset: HTMLFieldSetElement
|
||||||
|
let addAddress: HTMLTextAreaElement
|
||||||
|
|
||||||
|
let delFieldset: HTMLFieldSetElement
|
||||||
|
|
||||||
|
dom._kids(page,
|
||||||
|
crumbs(
|
||||||
|
crumblink('Mox Admin', '#'),
|
||||||
|
crumblink('Domain ' + domainString(domain.Domain), '#domains/'+d),
|
||||||
|
'Alias '+aliasLocalpart+'@'+domainName(domain.Domain),
|
||||||
|
),
|
||||||
|
|
||||||
|
dom.h2('Alias'),
|
||||||
|
dom.form(
|
||||||
|
async function submit(e: SubmitEvent) {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
check(aliasFieldset, client.AliasUpdate(aliasLocalpart, d, postPublic.checked, listMembers.checked, allowMsgFrom.checked))
|
||||||
|
},
|
||||||
|
aliasFieldset=dom.fieldset(
|
||||||
|
style({display: 'flex', flexDirection: 'column', gap: '.5ex'}),
|
||||||
|
dom.label(
|
||||||
|
postPublic=dom.input(attr.type('checkbox'), alias.PostPublic ? attr.checked('') : []),
|
||||||
|
' Public, anyone can post instead of only members',
|
||||||
|
),
|
||||||
|
dom.label(
|
||||||
|
listMembers=dom.input(attr.type('checkbox'), alias.ListMembers ? attr.checked('') : []),
|
||||||
|
' Members can list other members',
|
||||||
|
),
|
||||||
|
dom.label(
|
||||||
|
allowMsgFrom=dom.input(attr.type('checkbox'), alias.AllowMsgFrom ? attr.checked('') : []),
|
||||||
|
' Allow messages to use the alias address in the message From header',
|
||||||
|
),
|
||||||
|
dom.div(style({marginTop: '1ex'}), dom.submitbutton('Save')),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
dom.br(),
|
||||||
|
|
||||||
|
dom.h2('Members'),
|
||||||
|
dom.table(
|
||||||
|
dom.thead(
|
||||||
|
dom.tr(
|
||||||
|
dom.th('Address'),
|
||||||
|
dom.th('Account'),
|
||||||
|
dom.th(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
dom.tbody(
|
||||||
|
(alias.Addresses || []).map((address, index) => {
|
||||||
|
const pa = (alias.ParsedAddresses || [])[index]
|
||||||
|
return dom.tr(
|
||||||
|
dom.td(address),
|
||||||
|
dom.td(dom.a(pa.AccountName, attr.href('#accounts/'+pa.AccountName))),
|
||||||
|
dom.td(
|
||||||
|
dom.clickbutton('Remove', async function click(e: MouseEvent) {
|
||||||
|
await check(e.target! as HTMLButtonElement, client.AliasAddressesRemove(aliasLocalpart, d, [address]))
|
||||||
|
window.location.reload() // todo: reload less
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
dom.tfoot(
|
||||||
|
dom.tr(
|
||||||
|
dom.td(
|
||||||
|
attr.colspan('3'),
|
||||||
|
dom.form(
|
||||||
|
async function submit(e: SubmitEvent) {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
await check(addFieldset, client.AliasAddressesAdd(aliasLocalpart, d, addAddress.value.split('\n').map(s => s.trim()).filter(s => s)))
|
||||||
|
window.location.reload() // todo: reload less
|
||||||
|
},
|
||||||
|
addFieldset=dom.fieldset(
|
||||||
|
addAddress=dom.textarea(attr.required(''), attr.rows('1'), attr.placeholder('localpart@domain'), function focus() { addAddress.setAttribute('rows', '5') }), ' ',
|
||||||
|
dom.submitbutton('Add', style({verticalAlign: 'top'})),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
dom.br(),
|
||||||
|
|
||||||
|
dom.h2('Danger'),
|
||||||
|
dom.form(
|
||||||
|
async function submit(e: SubmitEvent) {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
|
||||||
|
if (!confirm('Are you sure you want to remove this alias?')) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await check(delFieldset, client.AliasRemove(aliasLocalpart, d))
|
||||||
|
window.location.hash = '#domains/'+d
|
||||||
|
},
|
||||||
|
delFieldset=dom.fieldset(
|
||||||
|
dom.div(dom.submitbutton('Remove alias')),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const domainDNSRecords = async (d: string) => {
|
const domainDNSRecords = async (d: string) => {
|
||||||
const [records, dnsdomain] = await Promise.all([
|
const [records, dnsdomain] = await Promise.all([
|
||||||
client.DomainRecords(d),
|
client.DomainRecords(d),
|
||||||
|
@ -4846,6 +5054,8 @@ const init = async () => {
|
||||||
await account(t[1])
|
await account(t[1])
|
||||||
} else if (t[0] === 'domains' && t.length === 2) {
|
} else if (t[0] === 'domains' && t.length === 2) {
|
||||||
await domain(t[1])
|
await domain(t[1])
|
||||||
|
} else if (t[0] === 'domains' && t.length === 4 && t[2] === 'alias') {
|
||||||
|
await domainAlias(t[1], t[3])
|
||||||
} else if (t[0] === 'domains' && t.length === 3 && t[2] === 'dmarc') {
|
} else if (t[0] === 'domains' && t.length === 3 && t[2] === 'dmarc') {
|
||||||
await domainDMARC(t[1])
|
await domainDMARC(t[1])
|
||||||
} else if (t[0] === 'domains' && t.length === 4 && t[2] === 'dmarc' && parseInt(t[3])) {
|
} else if (t[0] === 'domains' && t.length === 4 && t[2] === 'dmarc' && parseInt(t[3])) {
|
||||||
|
|
|
@ -321,6 +321,44 @@ func TestAdmin(t *testing.T) {
|
||||||
api.DomainDKIMRemove(ctxbg, "mox.example", "testsel")
|
api.DomainDKIMRemove(ctxbg, "mox.example", "testsel")
|
||||||
tneedErrorCode(t, "user:error", func() { api.DomainDKIMRemove(ctxbg, "mox.example", "testsel") }) // Already removed.
|
tneedErrorCode(t, "user:error", func() { api.DomainDKIMRemove(ctxbg, "mox.example", "testsel") }) // Already removed.
|
||||||
tneedErrorCode(t, "user:error", func() { api.DomainDKIMRemove(ctxbg, "bogus.example", "testsel") })
|
tneedErrorCode(t, "user:error", func() { api.DomainDKIMRemove(ctxbg, "bogus.example", "testsel") })
|
||||||
|
|
||||||
|
// Aliases
|
||||||
|
alias := config.Alias{Addresses: []string{"mjl@mox.example"}}
|
||||||
|
api.AliasAdd(ctxbg, "support", "mox.example", alias)
|
||||||
|
tneedErrorCode(t, "user:error", func() { api.AliasAdd(ctxbg, "support", "mox.example", alias) }) // Already present.
|
||||||
|
tneedErrorCode(t, "user:error", func() { api.AliasAdd(ctxbg, "Support", "mox.example", alias) }) // Duplicate, canonical.
|
||||||
|
tneedErrorCode(t, "user:error", func() { api.AliasAdd(ctxbg, "support", "bogus.example", alias) }) // Unknown domain.
|
||||||
|
tneedErrorCode(t, "user:error", func() { api.AliasAdd(ctxbg, "support2", "mox.example", config.Alias{}) }) // No addresses.
|
||||||
|
|
||||||
|
api.AliasUpdate(ctxbg, "support", "mox.example", true, true, true)
|
||||||
|
tneedErrorCode(t, "user:error", func() { api.AliasUpdate(ctxbg, "bogus", "mox.example", true, true, true) }) // Unknown alias localpart.
|
||||||
|
tneedErrorCode(t, "user:error", func() { api.AliasUpdate(ctxbg, "support", "bogus.example", true, true, true) }) // Unknown alias domain.
|
||||||
|
|
||||||
|
tneedErrorCode(t, "user:error", func() {
|
||||||
|
api.AliasAddressesAdd(ctxbg, "support", "mox.example", []string{"mjl2@mox.example", "mjl2@mox.example"})
|
||||||
|
}) // Cannot add twice.
|
||||||
|
api.AliasAddressesAdd(ctxbg, "support", "mox.example", []string{"mjl2@mox.example"})
|
||||||
|
tneedErrorCode(t, "user:error", func() { api.AliasAddressesAdd(ctxbg, "support", "mox.example", []string{"mjl2@mox.example"}) }) // Already present.
|
||||||
|
tneedErrorCode(t, "user:error", func() { api.AliasAddressesAdd(ctxbg, "support", "mox.example", []string{"bogus@mox.example"}) }) // Unknown dest localpart.
|
||||||
|
tneedErrorCode(t, "user:error", func() { api.AliasAddressesAdd(ctxbg, "support", "mox.example", []string{"bogus@bogus.example"}) }) // Unknown dest domain.
|
||||||
|
tneedErrorCode(t, "user:error", func() { api.AliasAddressesAdd(ctxbg, "support2", "mox.example", []string{"mjl@mox.example"}) }) // Unknown alias localpart.
|
||||||
|
tneedErrorCode(t, "user:error", func() { api.AliasAddressesAdd(ctxbg, "support", "bogus.example", []string{"mjl@mox.example"}) }) // Unknown alias localpart.
|
||||||
|
tneedErrorCode(t, "user:error", func() { api.AliasAddressesAdd(ctxbg, "support", "mox.example", []string{"support@mox.example"}) }) // Alias cannot be destination.
|
||||||
|
|
||||||
|
tneedErrorCode(t, "user:error", func() { api.AliasAddressesRemove(ctxbg, "support", "mox.example", []string{}) }) // Need at least 1 address.
|
||||||
|
tneedErrorCode(t, "user:error", func() { api.AliasAddressesRemove(ctxbg, "support", "mox.example", []string{"bogus@mox.example"}) }) // Not a member.
|
||||||
|
tneedErrorCode(t, "user:error", func() { api.AliasAddressesRemove(ctxbg, "support", "mox.example", []string{"bogus@bogus.example"}) }) // Not member, unknown domain.
|
||||||
|
tneedErrorCode(t, "user:error", func() { api.AliasAddressesRemove(ctxbg, "support2", "mox.example", []string{"mjl@mox.example"}) }) // Unknown alias localpart.
|
||||||
|
tneedErrorCode(t, "user:error", func() { api.AliasAddressesRemove(ctxbg, "support", "bogus.example", []string{"mjl@mox.example"}) }) // Unknown alias domain.
|
||||||
|
tneedErrorCode(t, "user:error", func() {
|
||||||
|
api.AliasAddressesRemove(ctxbg, "support", "mox.example", []string{"mjl@mox.example", "mjl2@mox.example"})
|
||||||
|
}) // Cannot leave zero addresses.
|
||||||
|
api.AliasAddressesRemove(ctxbg, "support", "mox.example", []string{"mjl@mox.example"})
|
||||||
|
|
||||||
|
api.AliasRemove(ctxbg, "support", "mox.example") // Restore.
|
||||||
|
tneedErrorCode(t, "user:error", func() { api.AliasRemove(ctxbg, "support", "mox.example") }) // No longer exists.
|
||||||
|
tneedErrorCode(t, "user:error", func() { api.AliasRemove(ctxbg, "support", "bogus.example") }) // Unknown alias domain.
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCheckDomain(t *testing.T) {
|
func TestCheckDomain(t *testing.T) {
|
||||||
|
|
|
@ -159,6 +159,13 @@
|
||||||
"{}",
|
"{}",
|
||||||
"string"
|
"string"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "localpartAliases",
|
||||||
|
"Typewords": [
|
||||||
|
"{}",
|
||||||
|
"Alias"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -1898,6 +1905,139 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"Returns": []
|
"Returns": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "AliasAdd",
|
||||||
|
"Docs": "",
|
||||||
|
"Params": [
|
||||||
|
{
|
||||||
|
"Name": "aliaslp",
|
||||||
|
"Typewords": [
|
||||||
|
"string"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "domainName",
|
||||||
|
"Typewords": [
|
||||||
|
"string"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "alias",
|
||||||
|
"Typewords": [
|
||||||
|
"Alias"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Returns": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "AliasUpdate",
|
||||||
|
"Docs": "",
|
||||||
|
"Params": [
|
||||||
|
{
|
||||||
|
"Name": "aliaslp",
|
||||||
|
"Typewords": [
|
||||||
|
"string"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "domainName",
|
||||||
|
"Typewords": [
|
||||||
|
"string"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "postPublic",
|
||||||
|
"Typewords": [
|
||||||
|
"bool"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "listMembers",
|
||||||
|
"Typewords": [
|
||||||
|
"bool"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "allowMsgFrom",
|
||||||
|
"Typewords": [
|
||||||
|
"bool"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Returns": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "AliasRemove",
|
||||||
|
"Docs": "",
|
||||||
|
"Params": [
|
||||||
|
{
|
||||||
|
"Name": "aliaslp",
|
||||||
|
"Typewords": [
|
||||||
|
"string"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "domainName",
|
||||||
|
"Typewords": [
|
||||||
|
"string"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Returns": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "AliasAddressesAdd",
|
||||||
|
"Docs": "",
|
||||||
|
"Params": [
|
||||||
|
{
|
||||||
|
"Name": "aliaslp",
|
||||||
|
"Typewords": [
|
||||||
|
"string"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "domainName",
|
||||||
|
"Typewords": [
|
||||||
|
"string"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "addresses",
|
||||||
|
"Typewords": [
|
||||||
|
"[]",
|
||||||
|
"string"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Returns": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "AliasAddressesRemove",
|
||||||
|
"Docs": "",
|
||||||
|
"Params": [
|
||||||
|
{
|
||||||
|
"Name": "aliaslp",
|
||||||
|
"Typewords": [
|
||||||
|
"string"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "domainName",
|
||||||
|
"Typewords": [
|
||||||
|
"string"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "addresses",
|
||||||
|
"Typewords": [
|
||||||
|
"[]",
|
||||||
|
"string"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Returns": []
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"Sections": [],
|
"Sections": [],
|
||||||
|
@ -3234,6 +3374,21 @@
|
||||||
"[]",
|
"[]",
|
||||||
"Route"
|
"Route"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "Aliases",
|
||||||
|
"Docs": "",
|
||||||
|
"Typewords": [
|
||||||
|
"{}",
|
||||||
|
"Alias"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "Domain",
|
||||||
|
"Docs": "",
|
||||||
|
"Typewords": [
|
||||||
|
"Domain"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -3533,6 +3688,222 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"Name": "Alias",
|
||||||
|
"Docs": "",
|
||||||
|
"Fields": [
|
||||||
|
{
|
||||||
|
"Name": "Addresses",
|
||||||
|
"Docs": "",
|
||||||
|
"Typewords": [
|
||||||
|
"[]",
|
||||||
|
"string"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "PostPublic",
|
||||||
|
"Docs": "",
|
||||||
|
"Typewords": [
|
||||||
|
"bool"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "ListMembers",
|
||||||
|
"Docs": "",
|
||||||
|
"Typewords": [
|
||||||
|
"bool"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "AllowMsgFrom",
|
||||||
|
"Docs": "",
|
||||||
|
"Typewords": [
|
||||||
|
"bool"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "LocalpartStr",
|
||||||
|
"Docs": "In encoded form.",
|
||||||
|
"Typewords": [
|
||||||
|
"string"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "Domain",
|
||||||
|
"Docs": "",
|
||||||
|
"Typewords": [
|
||||||
|
"Domain"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "ParsedAddresses",
|
||||||
|
"Docs": "Matches addresses.",
|
||||||
|
"Typewords": [
|
||||||
|
"[]",
|
||||||
|
"AliasAddress"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "AliasAddress",
|
||||||
|
"Docs": "",
|
||||||
|
"Fields": [
|
||||||
|
{
|
||||||
|
"Name": "Address",
|
||||||
|
"Docs": "Parsed address.",
|
||||||
|
"Typewords": [
|
||||||
|
"Address"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "AccountName",
|
||||||
|
"Docs": "Looked up.",
|
||||||
|
"Typewords": [
|
||||||
|
"string"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "Destination",
|
||||||
|
"Docs": "Belonging to address.",
|
||||||
|
"Typewords": [
|
||||||
|
"Destination"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "Address",
|
||||||
|
"Docs": "Address is a parsed email address.",
|
||||||
|
"Fields": [
|
||||||
|
{
|
||||||
|
"Name": "Localpart",
|
||||||
|
"Docs": "",
|
||||||
|
"Typewords": [
|
||||||
|
"Localpart"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "Domain",
|
||||||
|
"Docs": "todo: shouldn't we accept an ip address here too? and merge this type into smtp.Path.",
|
||||||
|
"Typewords": [
|
||||||
|
"Domain"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "Destination",
|
||||||
|
"Docs": "",
|
||||||
|
"Fields": [
|
||||||
|
{
|
||||||
|
"Name": "Mailbox",
|
||||||
|
"Docs": "",
|
||||||
|
"Typewords": [
|
||||||
|
"string"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "Rulesets",
|
||||||
|
"Docs": "",
|
||||||
|
"Typewords": [
|
||||||
|
"[]",
|
||||||
|
"Ruleset"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "FullName",
|
||||||
|
"Docs": "",
|
||||||
|
"Typewords": [
|
||||||
|
"string"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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": "todo: once we implement ARC, we can use dkim domains that we cannot verify but that the arc-verified forwarding mail server was able to verify.",
|
||||||
|
"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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"Name": "Account",
|
"Name": "Account",
|
||||||
"Docs": "",
|
"Docs": "",
|
||||||
|
@ -3682,6 +4053,14 @@
|
||||||
"Typewords": [
|
"Typewords": [
|
||||||
"Domain"
|
"Domain"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "Aliases",
|
||||||
|
"Docs": "",
|
||||||
|
"Typewords": [
|
||||||
|
"[]",
|
||||||
|
"AddressAlias"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -3733,118 +4112,6 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"Name": "Destination",
|
|
||||||
"Docs": "",
|
|
||||||
"Fields": [
|
|
||||||
{
|
|
||||||
"Name": "Mailbox",
|
|
||||||
"Docs": "",
|
|
||||||
"Typewords": [
|
|
||||||
"string"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Name": "Rulesets",
|
|
||||||
"Docs": "",
|
|
||||||
"Typewords": [
|
|
||||||
"[]",
|
|
||||||
"Ruleset"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Name": "FullName",
|
|
||||||
"Docs": "",
|
|
||||||
"Typewords": [
|
|
||||||
"string"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"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": "todo: once we implement ARC, we can use dkim domains that we cannot verify but that the arc-verified forwarding mail server was able to verify.",
|
|
||||||
"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"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"Name": "SubjectPass",
|
"Name": "SubjectPass",
|
||||||
"Docs": "",
|
"Docs": "",
|
||||||
|
@ -3954,6 +4221,34 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"Name": "AddressAlias",
|
||||||
|
"Docs": "",
|
||||||
|
"Fields": [
|
||||||
|
{
|
||||||
|
"Name": "SubscriptionAddress",
|
||||||
|
"Docs": "",
|
||||||
|
"Typewords": [
|
||||||
|
"string"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "Alias",
|
||||||
|
"Docs": "Without members.",
|
||||||
|
"Typewords": [
|
||||||
|
"Alias"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "MemberAddresses",
|
||||||
|
"Docs": "Only if allowed to see.",
|
||||||
|
"Typewords": [
|
||||||
|
"[]",
|
||||||
|
"string"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"Name": "PolicyRecord",
|
"Name": "PolicyRecord",
|
||||||
"Docs": "PolicyRecord is a cached policy or absence of a policy.",
|
"Docs": "PolicyRecord is a cached policy or absence of a policy.",
|
||||||
|
|
155
webadmin/api.ts
155
webadmin/api.ts
|
@ -275,6 +275,8 @@ export interface ConfigDomain {
|
||||||
MTASTS?: MTASTS | null
|
MTASTS?: MTASTS | null
|
||||||
TLSRPT?: TLSRPT | null
|
TLSRPT?: TLSRPT | null
|
||||||
Routes?: Route[] | null
|
Routes?: Route[] | null
|
||||||
|
Aliases?: { [key: string]: Alias }
|
||||||
|
Domain: Domain
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DKIM {
|
export interface DKIM {
|
||||||
|
@ -333,38 +335,26 @@ export interface Route {
|
||||||
ToDomainASCII?: string[] | null
|
ToDomainASCII?: string[] | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Account {
|
export interface Alias {
|
||||||
OutgoingWebhook?: OutgoingWebhook | null
|
Addresses?: string[] | null
|
||||||
IncomingWebhook?: IncomingWebhook | null
|
PostPublic: boolean
|
||||||
FromIDLoginAddresses?: string[] | null
|
ListMembers: boolean
|
||||||
KeepRetiredMessagePeriod: number
|
AllowMsgFrom: boolean
|
||||||
KeepRetiredWebhookPeriod: number
|
LocalpartStr: string // In encoded form.
|
||||||
Domain: string
|
Domain: Domain
|
||||||
Description: string
|
ParsedAddresses?: AliasAddress[] | null // Matches addresses.
|
||||||
FullName: string
|
|
||||||
Destinations?: { [key: string]: Destination }
|
|
||||||
SubjectPass: SubjectPass
|
|
||||||
QuotaMessageSize: number
|
|
||||||
RejectsMailbox: string
|
|
||||||
KeepRejects: boolean
|
|
||||||
AutomaticJunkFlags: AutomaticJunkFlags
|
|
||||||
JunkFilter?: JunkFilter | null // todo: sane defaults for junkfilter
|
|
||||||
MaxOutgoingMessagesPerDay: number
|
|
||||||
MaxFirstTimeRecipientsPerDay: number
|
|
||||||
NoFirstTimeSenderDelay: boolean
|
|
||||||
Routes?: Route[] | null
|
|
||||||
DNSDomain: Domain // Parsed form of Domain.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OutgoingWebhook {
|
export interface AliasAddress {
|
||||||
URL: string
|
Address: Address // Parsed address.
|
||||||
Authorization: string
|
AccountName: string // Looked up.
|
||||||
Events?: string[] | null
|
Destination: Destination // Belonging to address.
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IncomingWebhook {
|
// Address is a parsed email address.
|
||||||
URL: string
|
export interface Address {
|
||||||
Authorization: string
|
Localpart: Localpart
|
||||||
|
Domain: Domain // todo: shouldn't we accept an ip address here too? and merge this type into smtp.Path.
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Destination {
|
export interface Destination {
|
||||||
|
@ -387,6 +377,41 @@ export interface Ruleset {
|
||||||
ListAllowDNSDomain: Domain
|
ListAllowDNSDomain: Domain
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Account {
|
||||||
|
OutgoingWebhook?: OutgoingWebhook | null
|
||||||
|
IncomingWebhook?: IncomingWebhook | null
|
||||||
|
FromIDLoginAddresses?: string[] | null
|
||||||
|
KeepRetiredMessagePeriod: number
|
||||||
|
KeepRetiredWebhookPeriod: number
|
||||||
|
Domain: string
|
||||||
|
Description: string
|
||||||
|
FullName: string
|
||||||
|
Destinations?: { [key: string]: Destination }
|
||||||
|
SubjectPass: SubjectPass
|
||||||
|
QuotaMessageSize: number
|
||||||
|
RejectsMailbox: string
|
||||||
|
KeepRejects: boolean
|
||||||
|
AutomaticJunkFlags: AutomaticJunkFlags
|
||||||
|
JunkFilter?: JunkFilter | null // todo: sane defaults for junkfilter
|
||||||
|
MaxOutgoingMessagesPerDay: number
|
||||||
|
MaxFirstTimeRecipientsPerDay: number
|
||||||
|
NoFirstTimeSenderDelay: boolean
|
||||||
|
Routes?: Route[] | null
|
||||||
|
DNSDomain: Domain // Parsed form of Domain.
|
||||||
|
Aliases?: AddressAlias[] | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OutgoingWebhook {
|
||||||
|
URL: string
|
||||||
|
Authorization: string
|
||||||
|
Events?: string[] | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IncomingWebhook {
|
||||||
|
URL: string
|
||||||
|
Authorization: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface SubjectPass {
|
export interface SubjectPass {
|
||||||
Period: number // todo: have a reasonable default for this?
|
Period: number // todo: have a reasonable default for this?
|
||||||
}
|
}
|
||||||
|
@ -409,6 +434,12 @@ export interface JunkFilter {
|
||||||
RareWords: number
|
RareWords: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AddressAlias {
|
||||||
|
SubscriptionAddress: string
|
||||||
|
Alias: Alias // Without members.
|
||||||
|
MemberAddresses?: string[] | null // Only if allowed to see.
|
||||||
|
}
|
||||||
|
|
||||||
// PolicyRecord is a cached policy or absence of a policy.
|
// PolicyRecord is a cached policy or absence of a policy.
|
||||||
export interface PolicyRecord {
|
export interface PolicyRecord {
|
||||||
Domain: string // Domain name, with unicode characters.
|
Domain: string // Domain name, with unicode characters.
|
||||||
|
@ -1146,7 +1177,7 @@ export enum SPFResult {
|
||||||
// be an IPv4 address.
|
// be an IPv4 address.
|
||||||
export type IP = string
|
export type IP = string
|
||||||
|
|
||||||
export const structTypes: {[typename: string]: boolean} = {"Account":true,"AuthResults":true,"AutoconfCheckResult":true,"AutodiscoverCheckResult":true,"AutodiscoverSRV":true,"AutomaticJunkFlags":true,"Canonicalization":true,"CheckResult":true,"ClientConfigs":true,"ClientConfigsEntry":true,"ConfigDomain":true,"DANECheckResult":true,"DKIM":true,"DKIMAuthResult":true,"DKIMCheckResult":true,"DKIMRecord":true,"DMARC":true,"DMARCCheckResult":true,"DMARCRecord":true,"DMARCSummary":true,"DNSSECResult":true,"DateRange":true,"Destination":true,"Directive":true,"Domain":true,"DomainFeedback":true,"Dynamic":true,"Evaluation":true,"EvaluationStat":true,"Extension":true,"FailureDetails":true,"Filter":true,"HoldRule":true,"Hook":true,"HookFilter":true,"HookResult":true,"HookRetired":true,"HookRetiredFilter":true,"HookRetiredSort":true,"HookSort":true,"IPDomain":true,"IPRevCheckResult":true,"Identifiers":true,"IncomingWebhook":true,"JunkFilter":true,"MTASTS":true,"MTASTSCheckResult":true,"MTASTSRecord":true,"MX":true,"MXCheckResult":true,"Modifier":true,"Msg":true,"MsgResult":true,"MsgRetired":true,"OutgoingWebhook":true,"Pair":true,"Policy":true,"PolicyEvaluated":true,"PolicyOverrideReason":true,"PolicyPublished":true,"PolicyRecord":true,"Record":true,"Report":true,"ReportMetadata":true,"ReportRecord":true,"Result":true,"ResultPolicy":true,"RetiredFilter":true,"RetiredSort":true,"Reverse":true,"Route":true,"Row":true,"Ruleset":true,"SMTPAuth":true,"SPFAuthResult":true,"SPFCheckResult":true,"SPFRecord":true,"SRV":true,"SRVConfCheckResult":true,"STSMX":true,"Selector":true,"Sort":true,"SubjectPass":true,"Summary":true,"SuppressAddress":true,"TLSCheckResult":true,"TLSRPT":true,"TLSRPTCheckResult":true,"TLSRPTDateRange":true,"TLSRPTRecord":true,"TLSRPTSummary":true,"TLSRPTSuppressAddress":true,"TLSReportRecord":true,"TLSResult":true,"Transport":true,"TransportDirect":true,"TransportSMTP":true,"TransportSocks":true,"URI":true,"WebForward":true,"WebHandler":true,"WebRedirect":true,"WebStatic":true,"WebserverConfig":true}
|
export const structTypes: {[typename: string]: boolean} = {"Account":true,"Address":true,"AddressAlias":true,"Alias":true,"AliasAddress":true,"AuthResults":true,"AutoconfCheckResult":true,"AutodiscoverCheckResult":true,"AutodiscoverSRV":true,"AutomaticJunkFlags":true,"Canonicalization":true,"CheckResult":true,"ClientConfigs":true,"ClientConfigsEntry":true,"ConfigDomain":true,"DANECheckResult":true,"DKIM":true,"DKIMAuthResult":true,"DKIMCheckResult":true,"DKIMRecord":true,"DMARC":true,"DMARCCheckResult":true,"DMARCRecord":true,"DMARCSummary":true,"DNSSECResult":true,"DateRange":true,"Destination":true,"Directive":true,"Domain":true,"DomainFeedback":true,"Dynamic":true,"Evaluation":true,"EvaluationStat":true,"Extension":true,"FailureDetails":true,"Filter":true,"HoldRule":true,"Hook":true,"HookFilter":true,"HookResult":true,"HookRetired":true,"HookRetiredFilter":true,"HookRetiredSort":true,"HookSort":true,"IPDomain":true,"IPRevCheckResult":true,"Identifiers":true,"IncomingWebhook":true,"JunkFilter":true,"MTASTS":true,"MTASTSCheckResult":true,"MTASTSRecord":true,"MX":true,"MXCheckResult":true,"Modifier":true,"Msg":true,"MsgResult":true,"MsgRetired":true,"OutgoingWebhook":true,"Pair":true,"Policy":true,"PolicyEvaluated":true,"PolicyOverrideReason":true,"PolicyPublished":true,"PolicyRecord":true,"Record":true,"Report":true,"ReportMetadata":true,"ReportRecord":true,"Result":true,"ResultPolicy":true,"RetiredFilter":true,"RetiredSort":true,"Reverse":true,"Route":true,"Row":true,"Ruleset":true,"SMTPAuth":true,"SPFAuthResult":true,"SPFCheckResult":true,"SPFRecord":true,"SRV":true,"SRVConfCheckResult":true,"STSMX":true,"Selector":true,"Sort":true,"SubjectPass":true,"Summary":true,"SuppressAddress":true,"TLSCheckResult":true,"TLSRPT":true,"TLSRPTCheckResult":true,"TLSRPTDateRange":true,"TLSRPTRecord":true,"TLSRPTSummary":true,"TLSRPTSuppressAddress":true,"TLSReportRecord":true,"TLSResult":true,"Transport":true,"TransportDirect":true,"TransportSMTP":true,"TransportSocks":true,"URI":true,"WebForward":true,"WebHandler":true,"WebRedirect":true,"WebStatic":true,"WebserverConfig":true}
|
||||||
export const stringsTypes: {[typename: string]: boolean} = {"Align":true,"Alignment":true,"CSRFToken":true,"DKIMResult":true,"DMARCPolicy":true,"DMARCResult":true,"Disposition":true,"IP":true,"Localpart":true,"Mode":true,"PolicyOverride":true,"PolicyType":true,"RUA":true,"ResultType":true,"SPFDomainScope":true,"SPFResult":true}
|
export const stringsTypes: {[typename: string]: boolean} = {"Align":true,"Alignment":true,"CSRFToken":true,"DKIMResult":true,"DMARCPolicy":true,"DMARCResult":true,"Disposition":true,"IP":true,"Localpart":true,"Mode":true,"PolicyOverride":true,"PolicyType":true,"RUA":true,"ResultType":true,"SPFDomainScope":true,"SPFResult":true}
|
||||||
export const intsTypes: {[typename: string]: boolean} = {}
|
export const intsTypes: {[typename: string]: boolean} = {}
|
||||||
export const types: TypenameMap = {
|
export const types: TypenameMap = {
|
||||||
|
@ -1181,7 +1212,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"]}]},
|
"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"]}]},
|
"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"]}]},
|
"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":"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"]}]},
|
"ConfigDomain": {"Name":"ConfigDomain","Docs":"","Fields":[{"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"]}]},
|
||||||
"DKIM": {"Name":"DKIM","Docs":"","Fields":[{"Name":"Selectors","Docs":"","Typewords":["{}","Selector"]},{"Name":"Sign","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"]}]},
|
"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"]}]},
|
"Canonicalization": {"Name":"Canonicalization","Docs":"","Fields":[{"Name":"HeaderRelaxed","Docs":"","Typewords":["bool"]},{"Name":"BodyRelaxed","Docs":"","Typewords":["bool"]}]},
|
||||||
|
@ -1189,14 +1220,18 @@ export const types: TypenameMap = {
|
||||||
"MTASTS": {"Name":"MTASTS","Docs":"","Fields":[{"Name":"PolicyID","Docs":"","Typewords":["string"]},{"Name":"Mode","Docs":"","Typewords":["Mode"]},{"Name":"MaxAge","Docs":"","Typewords":["int64"]},{"Name":"MX","Docs":"","Typewords":["[]","string"]}]},
|
"MTASTS": {"Name":"MTASTS","Docs":"","Fields":[{"Name":"PolicyID","Docs":"","Typewords":["string"]},{"Name":"Mode","Docs":"","Typewords":["Mode"]},{"Name":"MaxAge","Docs":"","Typewords":["int64"]},{"Name":"MX","Docs":"","Typewords":["[]","string"]}]},
|
||||||
"TLSRPT": {"Name":"TLSRPT","Docs":"","Fields":[{"Name":"Localpart","Docs":"","Typewords":["string"]},{"Name":"Domain","Docs":"","Typewords":["string"]},{"Name":"Account","Docs":"","Typewords":["string"]},{"Name":"Mailbox","Docs":"","Typewords":["string"]},{"Name":"ParsedLocalpart","Docs":"","Typewords":["Localpart"]},{"Name":"DNSDomain","Docs":"","Typewords":["Domain"]}]},
|
"TLSRPT": {"Name":"TLSRPT","Docs":"","Fields":[{"Name":"Localpart","Docs":"","Typewords":["string"]},{"Name":"Domain","Docs":"","Typewords":["string"]},{"Name":"Account","Docs":"","Typewords":["string"]},{"Name":"Mailbox","Docs":"","Typewords":["string"]},{"Name":"ParsedLocalpart","Docs":"","Typewords":["Localpart"]},{"Name":"DNSDomain","Docs":"","Typewords":["Domain"]}]},
|
||||||
"Route": {"Name":"Route","Docs":"","Fields":[{"Name":"FromDomain","Docs":"","Typewords":["[]","string"]},{"Name":"ToDomain","Docs":"","Typewords":["[]","string"]},{"Name":"MinimumAttempts","Docs":"","Typewords":["int32"]},{"Name":"Transport","Docs":"","Typewords":["string"]},{"Name":"FromDomainASCII","Docs":"","Typewords":["[]","string"]},{"Name":"ToDomainASCII","Docs":"","Typewords":["[]","string"]}]},
|
"Route": {"Name":"Route","Docs":"","Fields":[{"Name":"FromDomain","Docs":"","Typewords":["[]","string"]},{"Name":"ToDomain","Docs":"","Typewords":["[]","string"]},{"Name":"MinimumAttempts","Docs":"","Typewords":["int32"]},{"Name":"Transport","Docs":"","Typewords":["string"]},{"Name":"FromDomainASCII","Docs":"","Typewords":["[]","string"]},{"Name":"ToDomainASCII","Docs":"","Typewords":["[]","string"]}]},
|
||||||
"Account": {"Name":"Account","Docs":"","Fields":[{"Name":"OutgoingWebhook","Docs":"","Typewords":["nullable","OutgoingWebhook"]},{"Name":"IncomingWebhook","Docs":"","Typewords":["nullable","IncomingWebhook"]},{"Name":"FromIDLoginAddresses","Docs":"","Typewords":["[]","string"]},{"Name":"KeepRetiredMessagePeriod","Docs":"","Typewords":["int64"]},{"Name":"KeepRetiredWebhookPeriod","Docs":"","Typewords":["int64"]},{"Name":"Domain","Docs":"","Typewords":["string"]},{"Name":"Description","Docs":"","Typewords":["string"]},{"Name":"FullName","Docs":"","Typewords":["string"]},{"Name":"Destinations","Docs":"","Typewords":["{}","Destination"]},{"Name":"SubjectPass","Docs":"","Typewords":["SubjectPass"]},{"Name":"QuotaMessageSize","Docs":"","Typewords":["int64"]},{"Name":"RejectsMailbox","Docs":"","Typewords":["string"]},{"Name":"KeepRejects","Docs":"","Typewords":["bool"]},{"Name":"AutomaticJunkFlags","Docs":"","Typewords":["AutomaticJunkFlags"]},{"Name":"JunkFilter","Docs":"","Typewords":["nullable","JunkFilter"]},{"Name":"MaxOutgoingMessagesPerDay","Docs":"","Typewords":["int32"]},{"Name":"MaxFirstTimeRecipientsPerDay","Docs":"","Typewords":["int32"]},{"Name":"NoFirstTimeSenderDelay","Docs":"","Typewords":["bool"]},{"Name":"Routes","Docs":"","Typewords":["[]","Route"]},{"Name":"DNSDomain","Docs":"","Typewords":["Domain"]}]},
|
"Alias": {"Name":"Alias","Docs":"","Fields":[{"Name":"Addresses","Docs":"","Typewords":["[]","string"]},{"Name":"PostPublic","Docs":"","Typewords":["bool"]},{"Name":"ListMembers","Docs":"","Typewords":["bool"]},{"Name":"AllowMsgFrom","Docs":"","Typewords":["bool"]},{"Name":"LocalpartStr","Docs":"","Typewords":["string"]},{"Name":"Domain","Docs":"","Typewords":["Domain"]},{"Name":"ParsedAddresses","Docs":"","Typewords":["[]","AliasAddress"]}]},
|
||||||
"OutgoingWebhook": {"Name":"OutgoingWebhook","Docs":"","Fields":[{"Name":"URL","Docs":"","Typewords":["string"]},{"Name":"Authorization","Docs":"","Typewords":["string"]},{"Name":"Events","Docs":"","Typewords":["[]","string"]}]},
|
"AliasAddress": {"Name":"AliasAddress","Docs":"","Fields":[{"Name":"Address","Docs":"","Typewords":["Address"]},{"Name":"AccountName","Docs":"","Typewords":["string"]},{"Name":"Destination","Docs":"","Typewords":["Destination"]}]},
|
||||||
"IncomingWebhook": {"Name":"IncomingWebhook","Docs":"","Fields":[{"Name":"URL","Docs":"","Typewords":["string"]},{"Name":"Authorization","Docs":"","Typewords":["string"]}]},
|
"Address": {"Name":"Address","Docs":"","Fields":[{"Name":"Localpart","Docs":"","Typewords":["Localpart"]},{"Name":"Domain","Docs":"","Typewords":["Domain"]}]},
|
||||||
"Destination": {"Name":"Destination","Docs":"","Fields":[{"Name":"Mailbox","Docs":"","Typewords":["string"]},{"Name":"Rulesets","Docs":"","Typewords":["[]","Ruleset"]},{"Name":"FullName","Docs":"","Typewords":["string"]}]},
|
"Destination": {"Name":"Destination","Docs":"","Fields":[{"Name":"Mailbox","Docs":"","Typewords":["string"]},{"Name":"Rulesets","Docs":"","Typewords":["[]","Ruleset"]},{"Name":"FullName","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"]}]},
|
"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"]}]},
|
||||||
|
"Account": {"Name":"Account","Docs":"","Fields":[{"Name":"OutgoingWebhook","Docs":"","Typewords":["nullable","OutgoingWebhook"]},{"Name":"IncomingWebhook","Docs":"","Typewords":["nullable","IncomingWebhook"]},{"Name":"FromIDLoginAddresses","Docs":"","Typewords":["[]","string"]},{"Name":"KeepRetiredMessagePeriod","Docs":"","Typewords":["int64"]},{"Name":"KeepRetiredWebhookPeriod","Docs":"","Typewords":["int64"]},{"Name":"Domain","Docs":"","Typewords":["string"]},{"Name":"Description","Docs":"","Typewords":["string"]},{"Name":"FullName","Docs":"","Typewords":["string"]},{"Name":"Destinations","Docs":"","Typewords":["{}","Destination"]},{"Name":"SubjectPass","Docs":"","Typewords":["SubjectPass"]},{"Name":"QuotaMessageSize","Docs":"","Typewords":["int64"]},{"Name":"RejectsMailbox","Docs":"","Typewords":["string"]},{"Name":"KeepRejects","Docs":"","Typewords":["bool"]},{"Name":"AutomaticJunkFlags","Docs":"","Typewords":["AutomaticJunkFlags"]},{"Name":"JunkFilter","Docs":"","Typewords":["nullable","JunkFilter"]},{"Name":"MaxOutgoingMessagesPerDay","Docs":"","Typewords":["int32"]},{"Name":"MaxFirstTimeRecipientsPerDay","Docs":"","Typewords":["int32"]},{"Name":"NoFirstTimeSenderDelay","Docs":"","Typewords":["bool"]},{"Name":"Routes","Docs":"","Typewords":["[]","Route"]},{"Name":"DNSDomain","Docs":"","Typewords":["Domain"]},{"Name":"Aliases","Docs":"","Typewords":["[]","AddressAlias"]}]},
|
||||||
|
"OutgoingWebhook": {"Name":"OutgoingWebhook","Docs":"","Fields":[{"Name":"URL","Docs":"","Typewords":["string"]},{"Name":"Authorization","Docs":"","Typewords":["string"]},{"Name":"Events","Docs":"","Typewords":["[]","string"]}]},
|
||||||
|
"IncomingWebhook": {"Name":"IncomingWebhook","Docs":"","Fields":[{"Name":"URL","Docs":"","Typewords":["string"]},{"Name":"Authorization","Docs":"","Typewords":["string"]}]},
|
||||||
"SubjectPass": {"Name":"SubjectPass","Docs":"","Fields":[{"Name":"Period","Docs":"","Typewords":["int64"]}]},
|
"SubjectPass": {"Name":"SubjectPass","Docs":"","Fields":[{"Name":"Period","Docs":"","Typewords":["int64"]}]},
|
||||||
"AutomaticJunkFlags": {"Name":"AutomaticJunkFlags","Docs":"","Fields":[{"Name":"Enabled","Docs":"","Typewords":["bool"]},{"Name":"JunkMailboxRegexp","Docs":"","Typewords":["string"]},{"Name":"NeutralMailboxRegexp","Docs":"","Typewords":["string"]},{"Name":"NotJunkMailboxRegexp","Docs":"","Typewords":["string"]}]},
|
"AutomaticJunkFlags": {"Name":"AutomaticJunkFlags","Docs":"","Fields":[{"Name":"Enabled","Docs":"","Typewords":["bool"]},{"Name":"JunkMailboxRegexp","Docs":"","Typewords":["string"]},{"Name":"NeutralMailboxRegexp","Docs":"","Typewords":["string"]},{"Name":"NotJunkMailboxRegexp","Docs":"","Typewords":["string"]}]},
|
||||||
"JunkFilter": {"Name":"JunkFilter","Docs":"","Fields":[{"Name":"Threshold","Docs":"","Typewords":["float64"]},{"Name":"Onegrams","Docs":"","Typewords":["bool"]},{"Name":"Twograms","Docs":"","Typewords":["bool"]},{"Name":"Threegrams","Docs":"","Typewords":["bool"]},{"Name":"MaxPower","Docs":"","Typewords":["float64"]},{"Name":"TopWords","Docs":"","Typewords":["int32"]},{"Name":"IgnoreWords","Docs":"","Typewords":["float64"]},{"Name":"RareWords","Docs":"","Typewords":["int32"]}]},
|
"JunkFilter": {"Name":"JunkFilter","Docs":"","Fields":[{"Name":"Threshold","Docs":"","Typewords":["float64"]},{"Name":"Onegrams","Docs":"","Typewords":["bool"]},{"Name":"Twograms","Docs":"","Typewords":["bool"]},{"Name":"Threegrams","Docs":"","Typewords":["bool"]},{"Name":"MaxPower","Docs":"","Typewords":["float64"]},{"Name":"TopWords","Docs":"","Typewords":["int32"]},{"Name":"IgnoreWords","Docs":"","Typewords":["float64"]},{"Name":"RareWords","Docs":"","Typewords":["int32"]}]},
|
||||||
|
"AddressAlias": {"Name":"AddressAlias","Docs":"","Fields":[{"Name":"SubscriptionAddress","Docs":"","Typewords":["string"]},{"Name":"Alias","Docs":"","Typewords":["Alias"]},{"Name":"MemberAddresses","Docs":"","Typewords":["[]","string"]}]},
|
||||||
"PolicyRecord": {"Name":"PolicyRecord","Docs":"","Fields":[{"Name":"Domain","Docs":"","Typewords":["string"]},{"Name":"Inserted","Docs":"","Typewords":["timestamp"]},{"Name":"ValidEnd","Docs":"","Typewords":["timestamp"]},{"Name":"LastUpdate","Docs":"","Typewords":["timestamp"]},{"Name":"LastUse","Docs":"","Typewords":["timestamp"]},{"Name":"Backoff","Docs":"","Typewords":["bool"]},{"Name":"RecordID","Docs":"","Typewords":["string"]},{"Name":"Version","Docs":"","Typewords":["string"]},{"Name":"Mode","Docs":"","Typewords":["Mode"]},{"Name":"MX","Docs":"","Typewords":["[]","STSMX"]},{"Name":"MaxAgeSeconds","Docs":"","Typewords":["int32"]},{"Name":"Extensions","Docs":"","Typewords":["[]","Pair"]},{"Name":"PolicyText","Docs":"","Typewords":["string"]}]},
|
"PolicyRecord": {"Name":"PolicyRecord","Docs":"","Fields":[{"Name":"Domain","Docs":"","Typewords":["string"]},{"Name":"Inserted","Docs":"","Typewords":["timestamp"]},{"Name":"ValidEnd","Docs":"","Typewords":["timestamp"]},{"Name":"LastUpdate","Docs":"","Typewords":["timestamp"]},{"Name":"LastUse","Docs":"","Typewords":["timestamp"]},{"Name":"Backoff","Docs":"","Typewords":["bool"]},{"Name":"RecordID","Docs":"","Typewords":["string"]},{"Name":"Version","Docs":"","Typewords":["string"]},{"Name":"Mode","Docs":"","Typewords":["Mode"]},{"Name":"MX","Docs":"","Typewords":["[]","STSMX"]},{"Name":"MaxAgeSeconds","Docs":"","Typewords":["int32"]},{"Name":"Extensions","Docs":"","Typewords":["[]","Pair"]},{"Name":"PolicyText","Docs":"","Typewords":["string"]}]},
|
||||||
"TLSReportRecord": {"Name":"TLSReportRecord","Docs":"","Fields":[{"Name":"ID","Docs":"","Typewords":["int64"]},{"Name":"Domain","Docs":"","Typewords":["string"]},{"Name":"FromDomain","Docs":"","Typewords":["string"]},{"Name":"MailFrom","Docs":"","Typewords":["string"]},{"Name":"HostReport","Docs":"","Typewords":["bool"]},{"Name":"Report","Docs":"","Typewords":["Report"]}]},
|
"TLSReportRecord": {"Name":"TLSReportRecord","Docs":"","Fields":[{"Name":"ID","Docs":"","Typewords":["int64"]},{"Name":"Domain","Docs":"","Typewords":["string"]},{"Name":"FromDomain","Docs":"","Typewords":["string"]},{"Name":"MailFrom","Docs":"","Typewords":["string"]},{"Name":"HostReport","Docs":"","Typewords":["bool"]},{"Name":"Report","Docs":"","Typewords":["Report"]}]},
|
||||||
"Report": {"Name":"Report","Docs":"","Fields":[{"Name":"OrganizationName","Docs":"","Typewords":["string"]},{"Name":"DateRange","Docs":"","Typewords":["TLSRPTDateRange"]},{"Name":"ContactInfo","Docs":"","Typewords":["string"]},{"Name":"ReportID","Docs":"","Typewords":["string"]},{"Name":"Policies","Docs":"","Typewords":["[]","Result"]}]},
|
"Report": {"Name":"Report","Docs":"","Fields":[{"Name":"OrganizationName","Docs":"","Typewords":["string"]},{"Name":"DateRange","Docs":"","Typewords":["TLSRPTDateRange"]},{"Name":"ContactInfo","Docs":"","Typewords":["string"]},{"Name":"ReportID","Docs":"","Typewords":["string"]},{"Name":"Policies","Docs":"","Typewords":["[]","Result"]}]},
|
||||||
|
@ -1312,14 +1347,18 @@ export const parser = {
|
||||||
MTASTS: (v: any) => parse("MTASTS", v) as MTASTS,
|
MTASTS: (v: any) => parse("MTASTS", v) as MTASTS,
|
||||||
TLSRPT: (v: any) => parse("TLSRPT", v) as TLSRPT,
|
TLSRPT: (v: any) => parse("TLSRPT", v) as TLSRPT,
|
||||||
Route: (v: any) => parse("Route", v) as Route,
|
Route: (v: any) => parse("Route", v) as Route,
|
||||||
|
Alias: (v: any) => parse("Alias", v) as Alias,
|
||||||
|
AliasAddress: (v: any) => parse("AliasAddress", v) as AliasAddress,
|
||||||
|
Address: (v: any) => parse("Address", v) as Address,
|
||||||
|
Destination: (v: any) => parse("Destination", v) as Destination,
|
||||||
|
Ruleset: (v: any) => parse("Ruleset", v) as Ruleset,
|
||||||
Account: (v: any) => parse("Account", v) as Account,
|
Account: (v: any) => parse("Account", v) as Account,
|
||||||
OutgoingWebhook: (v: any) => parse("OutgoingWebhook", v) as OutgoingWebhook,
|
OutgoingWebhook: (v: any) => parse("OutgoingWebhook", v) as OutgoingWebhook,
|
||||||
IncomingWebhook: (v: any) => parse("IncomingWebhook", v) as IncomingWebhook,
|
IncomingWebhook: (v: any) => parse("IncomingWebhook", v) as IncomingWebhook,
|
||||||
Destination: (v: any) => parse("Destination", v) as Destination,
|
|
||||||
Ruleset: (v: any) => parse("Ruleset", v) as Ruleset,
|
|
||||||
SubjectPass: (v: any) => parse("SubjectPass", v) as SubjectPass,
|
SubjectPass: (v: any) => parse("SubjectPass", v) as SubjectPass,
|
||||||
AutomaticJunkFlags: (v: any) => parse("AutomaticJunkFlags", v) as AutomaticJunkFlags,
|
AutomaticJunkFlags: (v: any) => parse("AutomaticJunkFlags", v) as AutomaticJunkFlags,
|
||||||
JunkFilter: (v: any) => parse("JunkFilter", v) as JunkFilter,
|
JunkFilter: (v: any) => parse("JunkFilter", v) as JunkFilter,
|
||||||
|
AddressAlias: (v: any) => parse("AddressAlias", v) as AddressAlias,
|
||||||
PolicyRecord: (v: any) => parse("PolicyRecord", v) as PolicyRecord,
|
PolicyRecord: (v: any) => parse("PolicyRecord", v) as PolicyRecord,
|
||||||
TLSReportRecord: (v: any) => parse("TLSReportRecord", v) as TLSReportRecord,
|
TLSReportRecord: (v: any) => parse("TLSReportRecord", v) as TLSReportRecord,
|
||||||
Report: (v: any) => parse("Report", v) as Report,
|
Report: (v: any) => parse("Report", v) as Report,
|
||||||
|
@ -1501,12 +1540,12 @@ export class Client {
|
||||||
}
|
}
|
||||||
|
|
||||||
// DomainLocalparts returns the encoded localparts and accounts configured in domain.
|
// DomainLocalparts returns the encoded localparts and accounts configured in domain.
|
||||||
async DomainLocalparts(domain: string): Promise<{ [key: string]: string }> {
|
async DomainLocalparts(domain: string): Promise<[{ [key: string]: string }, { [key: string]: Alias }]> {
|
||||||
const fn: string = "DomainLocalparts"
|
const fn: string = "DomainLocalparts"
|
||||||
const paramTypes: string[][] = [["string"]]
|
const paramTypes: string[][] = [["string"]]
|
||||||
const returnTypes: string[][] = [["{}","string"]]
|
const returnTypes: string[][] = [["{}","string"],["{}","Alias"]]
|
||||||
const params: any[] = [domain]
|
const params: any[] = [domain]
|
||||||
return await _sherpaCall(this.baseURL, this.authState, { ...this.options }, paramTypes, returnTypes, fn, params) as { [key: string]: string }
|
return await _sherpaCall(this.baseURL, this.authState, { ...this.options }, paramTypes, returnTypes, fn, params) as [{ [key: string]: string }, { [key: string]: Alias }]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Accounts returns the names of all configured accounts.
|
// Accounts returns the names of all configured accounts.
|
||||||
|
@ -2253,6 +2292,46 @@ export class Client {
|
||||||
const params: any[] = [domainName, selectors, sign]
|
const params: any[] = [domainName, selectors, sign]
|
||||||
return await _sherpaCall(this.baseURL, this.authState, { ...this.options }, paramTypes, returnTypes, fn, params) as void
|
return await _sherpaCall(this.baseURL, this.authState, { ...this.options }, paramTypes, returnTypes, fn, params) as void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async AliasAdd(aliaslp: string, domainName: string, alias: Alias): Promise<void> {
|
||||||
|
const fn: string = "AliasAdd"
|
||||||
|
const paramTypes: string[][] = [["string"],["string"],["Alias"]]
|
||||||
|
const returnTypes: string[][] = []
|
||||||
|
const params: any[] = [aliaslp, domainName, alias]
|
||||||
|
return await _sherpaCall(this.baseURL, this.authState, { ...this.options }, paramTypes, returnTypes, fn, params) as void
|
||||||
|
}
|
||||||
|
|
||||||
|
async AliasUpdate(aliaslp: string, domainName: string, postPublic: boolean, listMembers: boolean, allowMsgFrom: boolean): Promise<void> {
|
||||||
|
const fn: string = "AliasUpdate"
|
||||||
|
const paramTypes: string[][] = [["string"],["string"],["bool"],["bool"],["bool"]]
|
||||||
|
const returnTypes: string[][] = []
|
||||||
|
const params: any[] = [aliaslp, domainName, postPublic, listMembers, allowMsgFrom]
|
||||||
|
return await _sherpaCall(this.baseURL, this.authState, { ...this.options }, paramTypes, returnTypes, fn, params) as void
|
||||||
|
}
|
||||||
|
|
||||||
|
async AliasRemove(aliaslp: string, domainName: string): Promise<void> {
|
||||||
|
const fn: string = "AliasRemove"
|
||||||
|
const paramTypes: string[][] = [["string"],["string"]]
|
||||||
|
const returnTypes: string[][] = []
|
||||||
|
const params: any[] = [aliaslp, domainName]
|
||||||
|
return await _sherpaCall(this.baseURL, this.authState, { ...this.options }, paramTypes, returnTypes, fn, params) as void
|
||||||
|
}
|
||||||
|
|
||||||
|
async AliasAddressesAdd(aliaslp: string, domainName: string, addresses: string[] | null): Promise<void> {
|
||||||
|
const fn: string = "AliasAddressesAdd"
|
||||||
|
const paramTypes: string[][] = [["string"],["string"],["[]","string"]]
|
||||||
|
const returnTypes: string[][] = []
|
||||||
|
const params: any[] = [aliaslp, domainName, addresses]
|
||||||
|
return await _sherpaCall(this.baseURL, this.authState, { ...this.options }, paramTypes, returnTypes, fn, params) as void
|
||||||
|
}
|
||||||
|
|
||||||
|
async AliasAddressesRemove(aliaslp: string, domainName: string, addresses: string[] | null): Promise<void> {
|
||||||
|
const fn: string = "AliasAddressesRemove"
|
||||||
|
const paramTypes: string[][] = [["string"],["string"],["[]","string"]]
|
||||||
|
const returnTypes: string[][] = []
|
||||||
|
const params: any[] = [aliaslp, domainName, addresses]
|
||||||
|
return await _sherpaCall(this.baseURL, this.authState, { ...this.options }, paramTypes, returnTypes, fn, params) as void
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const defaultBaseURL = (function() {
|
export const defaultBaseURL = (function() {
|
||||||
|
|
|
@ -431,7 +431,7 @@ func (s server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
acc, err = store.OpenEmailAuth(log, email, password)
|
acc, err = store.OpenEmailAuth(log, email, password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
mox.LimiterFailedAuth.Add(remoteIP, t0, 1)
|
mox.LimiterFailedAuth.Add(remoteIP, t0, 1)
|
||||||
if errors.Is(err, mox.ErrDomainNotFound) || errors.Is(err, mox.ErrAccountNotFound) || errors.Is(err, store.ErrUnknownCredentials) {
|
if errors.Is(err, mox.ErrDomainNotFound) || errors.Is(err, mox.ErrAddressNotFound) || errors.Is(err, store.ErrUnknownCredentials) {
|
||||||
log.Debug("bad http basic authentication credentials")
|
log.Debug("bad http basic authentication credentials")
|
||||||
metricResults.WithLabelValues(fn, "badauth").Inc()
|
metricResults.WithLabelValues(fn, "badauth").Inc()
|
||||||
authResult = "badcreds"
|
authResult = "badcreds"
|
||||||
|
@ -621,15 +621,10 @@ func (s server) Send(ctx context.Context, req webapi.SendRequest) (resp webapi.S
|
||||||
addresses := append(append(m.To, m.CC...), m.BCC...)
|
addresses := append(append(m.To, m.CC...), m.BCC...)
|
||||||
|
|
||||||
// Check if from address is allowed for account.
|
// Check if from address is allowed for account.
|
||||||
fromAccName, _, _, err := mox.FindAccount(from.Address.Localpart, from.Address.Domain, false)
|
if !mox.AllowMsgFrom(acc.Name, from.Address) {
|
||||||
if err == nil && fromAccName != acc.Name {
|
|
||||||
err = mox.ErrAccountNotFound
|
|
||||||
}
|
|
||||||
if err != nil && (errors.Is(err, mox.ErrAccountNotFound) || errors.Is(err, mox.ErrDomainNotFound)) {
|
|
||||||
metricSubmission.WithLabelValues("badfrom").Inc()
|
metricSubmission.WithLabelValues("badfrom").Inc()
|
||||||
return resp, webapi.Error{Code: "badFrom", Message: "from-address not configured for account"}
|
return resp, webapi.Error{Code: "badFrom", Message: "from-address not configured for account"}
|
||||||
}
|
}
|
||||||
xcheckf(err, "checking if from address is allowed")
|
|
||||||
|
|
||||||
if len(recipients) == 0 {
|
if len(recipients) == 0 {
|
||||||
return resp, webapi.Error{Code: "noRecipients", Message: "no recipients"}
|
return resp, webapi.Error{Code: "noRecipients", Message: "no recipients"}
|
||||||
|
|
|
@ -640,15 +640,10 @@ func (w Webmail) MessageSubmit(ctx context.Context, m SubmitMessage) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if from address is allowed for account.
|
// Check if from address is allowed for account.
|
||||||
fromAccName, _, _, err := mox.FindAccount(fromAddr.Address.Localpart, fromAddr.Address.Domain, false)
|
if !mox.AllowMsgFrom(reqInfo.Account.Name, fromAddr.Address) {
|
||||||
if err == nil && fromAccName != reqInfo.Account.Name {
|
|
||||||
err = mox.ErrAccountNotFound
|
|
||||||
}
|
|
||||||
if err != nil && (errors.Is(err, mox.ErrAccountNotFound) || errors.Is(err, mox.ErrDomainNotFound)) {
|
|
||||||
metricSubmission.WithLabelValues("badfrom").Inc()
|
metricSubmission.WithLabelValues("badfrom").Inc()
|
||||||
xcheckuserf(ctx, errors.New("address not found"), `looking up "from" address for account`)
|
xcheckuserf(ctx, errors.New("address not found"), `looking up "from" address for account`)
|
||||||
}
|
}
|
||||||
xcheckf(ctx, err, "checking if from address is allowed")
|
|
||||||
|
|
||||||
if len(recipients) == 0 {
|
if len(recipients) == 0 {
|
||||||
xcheckuserf(ctx, errors.New("no recipients"), "composing message")
|
xcheckuserf(ctx, errors.New("no recipients"), "composing message")
|
||||||
|
|
|
@ -618,7 +618,7 @@ func serveEvents(ctx context.Context, log mlog.Log, accountPath string, w http.R
|
||||||
accConf, _ := acc.Conf()
|
accConf, _ := acc.Conf()
|
||||||
loginAddr, err := smtp.ParseAddress(address)
|
loginAddr, err := smtp.ParseAddress(address)
|
||||||
xcheckf(ctx, err, "parsing login address")
|
xcheckf(ctx, err, "parsing login address")
|
||||||
_, _, dest, err := mox.FindAccount(loginAddr.Localpart, loginAddr.Domain, false)
|
_, _, _, dest, err := mox.LookupAddress(loginAddr.Localpart, loginAddr.Domain, false, false)
|
||||||
xcheckf(ctx, err, "looking up destination for login address")
|
xcheckf(ctx, err, "looking up destination for login address")
|
||||||
loginName := accConf.FullName
|
loginName := accConf.FullName
|
||||||
if dest.FullName != "" {
|
if dest.FullName != "" {
|
||||||
|
@ -643,6 +643,18 @@ func serveEvents(ctx context.Context, log mlog.Log, accountPath string, w http.R
|
||||||
}
|
}
|
||||||
addresses = append(addresses, ma)
|
addresses = append(addresses, ma)
|
||||||
}
|
}
|
||||||
|
// User is allowed to send using alias address as message From address. Webmail
|
||||||
|
// will choose it when replying to a message sent to that address.
|
||||||
|
aliasAddrs := map[MessageAddress]bool{}
|
||||||
|
for _, a := range accConf.Aliases {
|
||||||
|
if a.Alias.AllowMsgFrom {
|
||||||
|
ma := MessageAddress{User: a.Alias.LocalpartStr, Domain: a.Alias.Domain}
|
||||||
|
if !aliasAddrs[ma] {
|
||||||
|
addresses = append(addresses, ma)
|
||||||
|
}
|
||||||
|
aliasAddrs[ma] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// We implicitly start a query. We use the reqctx for the transaction, because the
|
// We implicitly start a query. We use the reqctx for the transaction, because the
|
||||||
// transaction is passed to the query, which can be canceled.
|
// transaction is passed to the query, which can be canceled.
|
||||||
|
|
|
@ -479,10 +479,10 @@ behaviour.
|
||||||
|
|
||||||
## Admin web interface
|
## Admin web interface
|
||||||
|
|
||||||
The admin web interface helps admins set up accounts, configure addresses, and
|
The admin web interface helps admins set up accounts, configure addresses,
|
||||||
set up new domains (with instructions to create DNS records, and with a check
|
aliases/lists, and set up new domains (with instructions to create DNS records,
|
||||||
to see if they are correct). Changes made through the admin web interface
|
and with a check to see if they are correct). Changes made through the admin web
|
||||||
updates the [domains.conf config file](../config/#hdr-domains-conf).
|
interface updates the [domains.conf config file](../config/#hdr-domains-conf).
|
||||||
|
|
||||||
Received DMARC and TLS reports can be viewed, and cached MTA-STS policies
|
Received DMARC and TLS reports can be viewed, and cached MTA-STS policies
|
||||||
listed.
|
listed.
|
||||||
|
|
Loading…
Reference in a new issue