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:
Mechiel Lukkien
2024-04-24 19:15:30 +02:00
parent 1cf7477642
commit 960a51242d
34 changed files with 2766 additions and 589 deletions

View File

@ -116,11 +116,13 @@ func newTestServer(t *testing.T, configPath string, resolver dns.Resolver) *test
mox.MustLoadConfig(true, false)
dataDir := mox.ConfigDirPath(mox.Conf.Static.DataDir)
os.RemoveAll(dataDir)
var err error
ts.acc, err = store.OpenAccount(log, "mjl")
tcheck(t, err, "open account")
err = ts.acc.SetPassword(log, password0)
tcheck(t, err, "set password")
ts.switchStop = store.Switchboard()
err = queue.Init()
tcheck(t, err, "queue init")
@ -143,6 +145,23 @@ func (ts *testserver) close() {
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)) {
ts.t.Helper()
ts.runRaw(func(conn net.Conn) {
@ -194,6 +213,14 @@ func (ts *testserver) runRaw(fn func(clientConn net.Conn)) {
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
// (that is opportunistic TLS for you, "better some than none"). Let's enjoy this
// one moment where it makes life easier.
@ -508,22 +535,6 @@ func TestSpam(t *testing.T) {
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.
ts.run(func(err error, client *smtpclient.Client) {
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)
}
checkCount("Rejects", 1)
ts.checkCount("Rejects", 1)
checkEvaluationCount(t, 0) // No positive interactions yet.
})
@ -550,9 +561,9 @@ func TestSpam(t *testing.T) {
}
tcheck(t, err, "deliver")
checkCount("mjl2junk", 1) // In ruleset rejects mailbox.
checkCount("Rejects", 1) // Same as before.
checkEvaluationCount(t, 0) // This is not an actual accept.
ts.checkCount("mjl2junk", 1) // In ruleset rejects mailbox.
ts.checkCount("Rejects", 1) // Same as before.
checkEvaluationCount(t, 0) // This is not an actual accept.
})
// Mark the messages as having good reputation.
@ -571,8 +582,8 @@ func TestSpam(t *testing.T) {
tcheck(t, err, "deliver")
// Message should now be removed from Rejects mailboxes.
checkCount("Rejects", 0)
checkCount("mjl2junk", 1)
ts.checkCount("Rejects", 0)
ts.checkCount("mjl2junk", 1)
checkEvaluationCount(t, 1)
})