Support for external email addresses on mailing lists (closes #152)

This commit is contained in:
mdecimus
2024-11-13 19:38:54 +13:00
parent 77de725ca8
commit b2bac5d5aa
45 changed files with 813 additions and 643 deletions

View File

@@ -6,10 +6,13 @@
use ahash::AHashSet;
use directory::{
backend::internal::{
lookup::DirectoryStore,
manage::{self, ManageDirectory, UpdatePrincipal},
PrincipalField, PrincipalUpdate, PrincipalValue,
backend::{
internal::{
lookup::DirectoryStore,
manage::{self, ManageDirectory, UpdatePrincipal},
PrincipalField, PrincipalUpdate, PrincipalValue,
},
RcptType,
},
Principal, QueryBy, Type,
};
@@ -117,10 +120,13 @@ async fn internal_directory() {
.await,
Ok(())
);
assert!(store.rcpt("john@example.org").await.unwrap());
assert_eq!(
store.email_to_ids("john@example.org").await.unwrap(),
vec![john_id]
store.rcpt("john@example.org").await.unwrap(),
RcptType::Mailbox
);
assert_eq!(
store.email_to_id("john@example.org").await.unwrap(),
Some(john_id)
);
// Using non-existent domain should fail
@@ -154,11 +160,17 @@ async fn internal_directory() {
.await
.unwrap();
assert!(store.rcpt("jane@example.org").await.unwrap());
assert!(!store.rcpt("jane@otherdomain.org").await.unwrap());
assert_eq!(
store.email_to_ids("jane@example.org").await.unwrap(),
vec![jane_id]
store.rcpt("jane@example.org").await.unwrap(),
RcptType::Mailbox
);
assert_eq!(
store.rcpt("jane@otherdomain.org").await.unwrap(),
RcptType::Invalid
);
assert_eq!(
store.email_to_id("jane@example.org").await.unwrap(),
Some(jane_id)
);
assert_eq!(store.vrfy("jane").await.unwrap(), vec!["jane@example.org"]);
assert_eq!(
@@ -239,21 +251,31 @@ async fn internal_directory() {
PrincipalUpdate::set(
PrincipalField::Members,
PrincipalValue::StringList(vec!["john".to_string(), "jane".to_string()]),
),
PrincipalUpdate::set(
PrincipalField::ExternalMembers,
PrincipalValue::StringList(vec![
"mike@other.org".to_string(),
"lucy@foobar.net".to_string()
]),
)
]))
.await,
Ok(())
);
assert!(store.rcpt("list@example.org").await.unwrap());
assert_eq!(
store
.email_to_ids("list@example.org")
.await
.unwrap()
.into_iter()
.collect::<AHashSet<_>>(),
[john_id, jane_id].into_iter().collect::<AHashSet<_>>(),
);
assert_list_members(
&store,
"list@example.org",
[
"john@example.org",
"mike@other.org",
"lucy@foobar.net",
"jane@example.org",
],
)
.await;
assert_eq!(
store
.query(QueryBy::Name("list"), true)
@@ -276,10 +298,15 @@ async fn internal_directory() {
.unwrap()
.into_iter()
.collect::<AHashSet<_>>(),
["john@example.org", "jane@example.org"]
.into_iter()
.map(|s| s.to_string())
.collect::<AHashSet<_>>()
[
"john@example.org",
"mike@other.org",
"lucy@foobar.net",
"jane@example.org"
]
.into_iter()
.map(|s| s.to_string())
.collect::<AHashSet<_>>()
);
// Create groups
@@ -445,8 +472,14 @@ async fn internal_directory() {
}
);
assert_eq!(store.get_principal_id("john").await.unwrap(), None);
assert!(!store.rcpt("john@example.org").await.unwrap());
assert!(store.rcpt("john.doe@example.org").await.unwrap());
assert_eq!(
store.rcpt("john@example.org").await.unwrap(),
RcptType::Invalid
);
assert_eq!(
store.rcpt("john.doe@example.org").await.unwrap(),
RcptType::Mailbox
);
// Remove a member from a mailing list and then add it back
assert_eq!(
@@ -460,10 +493,12 @@ async fn internal_directory() {
.await,
Ok(())
);
assert_eq!(
store.email_to_ids("list@example.org").await.unwrap(),
vec![jane_id]
);
assert_list_members(
&store,
"list@example.org",
["jane@example.org", "mike@other.org", "lucy@foobar.net"],
)
.await;
assert_eq!(
store
.update_principal(UpdatePrincipal::by_name("list").with_updates(vec![
@@ -475,15 +510,17 @@ async fn internal_directory() {
.await,
Ok(())
);
assert_eq!(
store
.email_to_ids("list@example.org")
.await
.unwrap()
.into_iter()
.collect::<AHashSet<_>>(),
[john_id, jane_id].into_iter().collect::<AHashSet<_>>()
);
assert_list_members(
&store,
"list@example.org",
[
"john.doe@example.org",
"jane@example.org",
"mike@other.org",
"lucy@foobar.net",
],
)
.await;
// Field validation
assert_eq!(
@@ -619,10 +656,13 @@ async fn internal_directory() {
store.delete_principal(QueryBy::Id(john_id)).await.unwrap();
assert_eq!(store.get_principal_id("john.doe").await.unwrap(), None);
assert_eq!(
store.email_to_ids("john.doe@example.org").await.unwrap(),
Vec::<u32>::new()
store.email_to_id("john.doe@example.org").await.unwrap(),
None
);
assert_eq!(
store.rcpt("john.doe@example.org").await.unwrap(),
RcptType::Invalid
);
assert!(!store.rcpt("john.doe@example.org").await.unwrap());
assert_eq!(
store
.list_principals(
@@ -672,10 +712,13 @@ async fn internal_directory() {
// Make sure Jane's records are still there
assert_eq!(store.get_principal_id("jane").await.unwrap(), Some(jane_id));
assert_eq!(
store.email_to_ids("jane@example.org").await.unwrap(),
vec![jane_id]
store.email_to_id("jane@example.org").await.unwrap(),
Some(jane_id)
);
assert_eq!(
store.rcpt("jane@example.org").await.unwrap(),
RcptType::Mailbox
);
assert!(store.rcpt("jane@example.org").await.unwrap());
assert_eq!(
store
.get_bitmap(BitmapKey {
@@ -885,3 +928,22 @@ impl TestInternalDirectory for Store {
}
}
}
async fn assert_list_members(
store: &Store,
list_addr: &str,
members: impl IntoIterator<Item = &str>,
) {
match store.rcpt(list_addr).await.unwrap() {
RcptType::List(items) => {
assert_eq!(
items.into_iter().collect::<AHashSet<_>>(),
members
.into_iter()
.map(|s| s.to_string())
.collect::<AHashSet<_>>()
);
}
other => panic!("invalid {other:?}"),
}
}

View File

@@ -6,10 +6,15 @@
use std::fmt::Debug;
use directory::{backend::internal::manage::ManageDirectory, QueryBy, Type, ROLE_USER};
use directory::{
backend::{internal::manage::ManageDirectory, RcptType},
QueryBy, Type, ROLE_USER,
};
use mail_send::Credentials;
use crate::directory::{map_account_ids, DirectoryTest, IntoTestPrincipal, TestPrincipal};
use crate::directory::{
map_account_id, map_account_ids, DirectoryTest, IntoTestPrincipal, TestPrincipal,
};
#[tokio::test]
async fn ldap_directory() {
@@ -149,41 +154,29 @@ async fn ldap_directory() {
);
// Ids by email
compare_sorted(
core.email_to_ids(&handle, "jane@example.org", 0)
assert_eq!(
core.email_to_id(&handle, "jane@example.org", 0)
.await
.unwrap(),
map_account_ids(base_store, vec!["jane"]).await,
);
compare_sorted(
core.email_to_ids(&handle, "jane+alias@example.org", 0)
.await
.unwrap(),
map_account_ids(base_store, vec!["jane"]).await,
);
compare_sorted(
core.email_to_ids(&handle, "info@example.org", 0)
.await
.unwrap(),
map_account_ids(base_store, vec!["bill", "jane", "john"]).await,
);
compare_sorted(
core.email_to_ids(&handle, "info+alias@example.org", 0)
.await
.unwrap(),
map_account_ids(base_store, vec!["bill", "jane", "john"]).await,
);
compare_sorted(
core.email_to_ids(&handle, "unknown@example.org", 0)
.await
.unwrap(),
Vec::<u32>::new(),
Some(map_account_id(base_store, "jane").await),
);
assert_eq!(
core.email_to_ids(&handle, "anything@catchall.org", 0)
core.email_to_id(&handle, "jane+alias@example.org", 0)
.await
.unwrap(),
map_account_ids(base_store, vec!["robert"]).await
Some(map_account_id(base_store, "jane").await),
);
assert_eq!(
core.email_to_id(&handle, "unknown@example.org", 0)
.await
.unwrap(),
None,
);
assert_eq!(
core.email_to_id(&handle, "anything@catchall.org", 0)
.await
.unwrap(),
Some(map_account_id(base_store, "robert").await)
);
// Domain validation
@@ -191,21 +184,36 @@ async fn ldap_directory() {
assert!(!handle.is_local_domain("other.org").await.unwrap());
// RCPT TO
assert!(core.rcpt(&handle, "jane@example.org", 0).await.unwrap());
assert!(core.rcpt(&handle, "info@example.org", 0).await.unwrap());
assert!(core
.rcpt(&handle, "jane+alias@example.org", 0)
.await
.unwrap());
assert!(core
.rcpt(&handle, "info+alias@example.org", 0)
.await
.unwrap());
assert!(core
.rcpt(&handle, "random_user@catchall.org", 0)
.await
.unwrap());
assert!(!core.rcpt(&handle, "invalid@example.org", 0).await.unwrap());
assert_eq!(
core.rcpt(&handle, "jane@example.org", 0).await.unwrap(),
RcptType::Mailbox
);
assert_eq!(
core.rcpt(&handle, "info@example.org", 0).await.unwrap(),
RcptType::Mailbox
);
assert_eq!(
core.rcpt(&handle, "jane+alias@example.org", 0)
.await
.unwrap(),
RcptType::Mailbox
);
assert_eq!(
core.rcpt(&handle, "info+alias@example.org", 0)
.await
.unwrap(),
RcptType::Mailbox
);
assert_eq!(
core.rcpt(&handle, "random_user@catchall.org", 0)
.await
.unwrap(),
RcptType::Mailbox
);
assert_eq!(
core.rcpt(&handle, "invalid@example.org", 0).await.unwrap(),
RcptType::Invalid
);
// VRFY
compare_sorted(
@@ -214,7 +222,10 @@ async fn ldap_directory() {
);
compare_sorted(
core.vrfy(&handle, "john", 0).await.unwrap(),
vec!["john@example.org".to_string()],
vec![
"john@example.org".to_string(),
"john.doe@example.org".to_string(),
],
);
compare_sorted(
core.vrfy(&handle, "jane+alias@example", 0).await.unwrap(),
@@ -230,7 +241,8 @@ async fn ldap_directory() {
);
// EXPN
compare_sorted(
// Now handled by the internal directory
/*compare_sorted(
core.expn(&handle, "info@example.org", 0).await.unwrap(),
vec![
"bill@example.org".to_string(),
@@ -241,7 +253,7 @@ async fn ldap_directory() {
compare_sorted(
core.expn(&handle, "john@example.org", 0).await.unwrap(),
Vec::<String>::new(),
);
);*/
}
fn compare_sorted<T: Eq + Debug>(v1: Vec<T>, v2: Vec<T>) {

View File

@@ -701,13 +701,15 @@ async fn address_mappings() {
async fn map_account_ids(store: &Store, names: Vec<impl AsRef<str>>) -> Vec<u32> {
let mut ids = Vec::with_capacity(names.len());
for name in names {
ids.push(
store
.get_principal_id(name.as_ref())
.await
.unwrap()
.unwrap(),
);
ids.push(map_account_id(store, name).await);
}
ids
}
async fn map_account_id(store: &Store, name: impl AsRef<str>) -> u32 {
store
.get_principal_id(name.as_ref())
.await
.unwrap()
.unwrap()
}

View File

@@ -7,7 +7,7 @@
use std::sync::Arc;
use common::listener::limiter::{ConcurrencyLimiter, InFlight};
use directory::QueryBy;
use directory::{backend::RcptType, QueryBy};
use mail_parser::decoders::base64::base64_decode;
use mail_send::Credentials;
use tokio::{
@@ -78,7 +78,9 @@ async fn lmtp_directory() {
for (item, expected) in &tests {
let result: LookupResult = match item {
Item::IsAccount(v) => core.rcpt(&handle, v, 0).await.unwrap().into(),
Item::IsAccount(v) => {
(core.rcpt(&handle, v, 0).await.unwrap() == RcptType::Mailbox).into()
}
Item::Authenticate(v) => handle
.query(QueryBy::Credentials(v), true)
.await
@@ -122,7 +124,9 @@ async fn lmtp_directory() {
requests.push((
tokio::spawn(async move {
let result: LookupResult = match &item {
Item::IsAccount(v) => core.rcpt(&handle, v, 0).await.unwrap().into(),
Item::IsAccount(v) => {
(core.rcpt(&handle, v, 0).await.unwrap() == RcptType::Mailbox).into()
}
Item::Authenticate(v) => handle
.query(QueryBy::Credentials(v), true)
.await
@@ -182,7 +186,9 @@ async fn lmtp_directory() {
requests.push((
tokio::spawn(async move {
let result: LookupResult = match &item {
Item::IsAccount(v) => core.rcpt(&handle, v, 0).await.unwrap().into(),
Item::IsAccount(v) => {
(core.rcpt(&handle, v, 0).await.unwrap() == RcptType::Mailbox).into()
}
_ => unreachable!(),
};

View File

@@ -4,11 +4,18 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use directory::{backend::internal::manage::ManageDirectory, QueryBy, Type, ROLE_USER};
use directory::{
backend::{internal::manage::ManageDirectory, RcptType},
QueryBy, Type, ROLE_USER,
};
use mail_send::Credentials;
#[allow(unused_imports)]
use store::{LookupStore, Store};
use crate::directory::{map_account_ids, DirectoryTest, IntoTestPrincipal, TestPrincipal};
use crate::directory::{
map_account_id, map_account_ids, DirectoryTest, IntoTestPrincipal, TestPrincipal,
};
use super::DirectoryStore;
@@ -243,40 +250,28 @@ async fn sql_directory() {
// Ids by email
assert_eq!(
core.email_to_ids(&handle, "jane@example.org", 0)
core.email_to_id(&handle, "jane@example.org", 0)
.await
.unwrap(),
map_account_ids(base_store, vec!["jane"]).await
Some(map_account_id(base_store, "jane").await)
);
assert_eq!(
core.email_to_ids(&handle, "info@example.org", 0)
core.email_to_id(&handle, "jane+alias@example.org", 0)
.await
.unwrap(),
map_account_ids(base_store, vec!["bill", "jane", "john"]).await
Some(map_account_id(base_store, "jane").await)
);
assert_eq!(
core.email_to_ids(&handle, "jane+alias@example.org", 0)
core.email_to_id(&handle, "unknown@example.org", 0)
.await
.unwrap(),
map_account_ids(base_store, vec!["jane"]).await
None
);
assert_eq!(
core.email_to_ids(&handle, "info+alias@example.org", 0)
core.email_to_id(&handle, "anything@catchall.org", 0)
.await
.unwrap(),
map_account_ids(base_store, vec!["bill", "jane", "john"]).await
);
assert_eq!(
core.email_to_ids(&handle, "unknown@example.org", 0)
.await
.unwrap(),
Vec::<u32>::new()
);
assert_eq!(
core.email_to_ids(&handle, "anything@catchall.org", 0)
.await
.unwrap(),
map_account_ids(base_store, vec!["robert"]).await
Some(map_account_id(base_store, "robert").await)
);
// Domain validation
@@ -284,21 +279,36 @@ async fn sql_directory() {
assert!(!handle.is_local_domain("other.org").await.unwrap());
// RCPT TO
assert!(core.rcpt(&handle, "jane@example.org", 0).await.unwrap());
assert!(core.rcpt(&handle, "info@example.org", 0).await.unwrap());
assert!(core
.rcpt(&handle, "jane+alias@example.org", 0)
.await
.unwrap());
assert!(core
.rcpt(&handle, "info+alias@example.org", 0)
.await
.unwrap());
assert!(core
.rcpt(&handle, "random_user@catchall.org", 0)
.await
.unwrap());
assert!(!core.rcpt(&handle, "invalid@example.org", 0).await.unwrap());
assert_eq!(
core.rcpt(&handle, "jane@example.org", 0).await.unwrap(),
RcptType::Mailbox
);
assert_eq!(
core.rcpt(&handle, "info@example.org", 0).await.unwrap(),
RcptType::Mailbox
);
assert_eq!(
core.rcpt(&handle, "jane+alias@example.org", 0)
.await
.unwrap(),
RcptType::Mailbox
);
assert_eq!(
core.rcpt(&handle, "info+alias@example.org", 0)
.await
.unwrap(),
RcptType::Mailbox
);
assert_eq!(
core.rcpt(&handle, "random_user@catchall.org", 0)
.await
.unwrap(),
RcptType::Mailbox
);
assert_eq!(
core.rcpt(&handle, "invalid@example.org", 0).await.unwrap(),
RcptType::Invalid
);
// VRFY
assert_eq!(
@@ -307,7 +317,10 @@ async fn sql_directory() {
);
assert_eq!(
core.vrfy(&handle, "john", 0).await.unwrap(),
vec!["john@example.org".to_string()]
vec![
"john.doe@example.org".to_string(),
"john@example.org".to_string(),
]
);
assert_eq!(
core.vrfy(&handle, "jane+alias@example", 0).await.unwrap(),
@@ -322,8 +335,8 @@ async fn sql_directory() {
Vec::<String>::new()
);
// EXPN
assert_eq!(
// EXPN (now handled by the internal store)
/*assert_eq!(
core.expn(&handle, "info@example.org", 0).await.unwrap(),
vec![
"bill@example.org".to_string(),
@@ -334,7 +347,7 @@ async fn sql_directory() {
assert_eq!(
core.expn(&handle, "john@example.org", 0).await.unwrap(),
Vec::<String>::new()
);
);*/
}
}