Subaddressing and catch-all addresses support.

This commit is contained in:
Mauro D 2023-06-08 14:54:09 +00:00
parent 891d39940d
commit 1ce0cee7e6
16 changed files with 280 additions and 65 deletions

View file

@ -11,7 +11,7 @@ use ahash::{AHashMap, AHashSet};
use crate::{ use crate::{
imap::ImapDirectory, ldap::LdapDirectory, memory::MemoryDirectory, smtp::SmtpDirectory, imap::ImapDirectory, ldap::LdapDirectory, memory::MemoryDirectory, smtp::SmtpDirectory,
sql::SqlDirectory, DirectoryConfig, Lookup, sql::SqlDirectory, DirectoryConfig, DirectoryOptions, Lookup,
}; };
pub trait ConfigDirectory { pub trait ConfigDirectory {
@ -114,6 +114,16 @@ impl ConfigDirectory for Config {
} }
} }
impl DirectoryOptions {
pub fn from_config(config: &Config, key: impl AsKey) -> utils::config::Result<Self> {
let key = key.as_key();
Ok(DirectoryOptions {
catch_all: config.property_or_static((&key, "options.catch-all"), "false")?,
subaddressing: config.property_or_static((&key, "options.subaddressing"), "true")?,
})
}
}
pub(crate) fn build_pool<M: ManageConnection>( pub(crate) fn build_pool<M: ManageConnection>(
config: &Config, config: &Config,
prefix: &str, prefix: &str,

View file

@ -3,7 +3,7 @@ use std::sync::Arc;
use ldap3::LdapConnSettings; use ldap3::LdapConnSettings;
use utils::config::{utils::AsKey, Config}; use utils::config::{utils::AsKey, Config};
use crate::{cache::CachedDirectory, config::build_pool, Directory}; use crate::{cache::CachedDirectory, config::build_pool, Directory, DirectoryOptions};
use super::{Bind, LdapConnectionManager, LdapDirectory, LdapFilter, LdapMappings}; use super::{Bind, LdapConnectionManager, LdapDirectory, LdapFilter, LdapMappings};
@ -105,6 +105,7 @@ impl LdapDirectory {
LdapDirectory { LdapDirectory {
mappings, mappings,
pool: build_pool(config, &prefix, manager)?, pool: build_pool(config, &prefix, manager)?,
opt: DirectoryOptions::from_config(config, prefix.as_str())?,
}, },
) )
} }

View file

@ -1,7 +1,7 @@
use ldap3::{Scope, SearchEntry}; use ldap3::{ResultEntry, Scope, SearchEntry};
use mail_send::Credentials; use mail_send::Credentials;
use crate::{Directory, Principal, Type}; use crate::{to_catch_all_address, unwrap_subaddress, Directory, Principal, Type};
use super::{LdapDirectory, LdapMappings}; use super::{LdapDirectory, LdapMappings};
@ -109,53 +109,86 @@ impl Directory for LdapDirectory {
Ok(emails) Ok(emails)
} }
async fn ids_by_email(&self, email: &str) -> crate::Result<Vec<u32>> { async fn ids_by_email(&self, address: &str) -> crate::Result<Vec<u32>> {
let (rs, _res) = self let ids = self
.pool .pool
.get() .get()
.await? .await?
.search( .search(
&self.mappings.base_dn, &self.mappings.base_dn,
Scope::Subtree, Scope::Subtree,
&self.mappings.filter_email.build(email), &self
.mappings
.filter_email
.build(unwrap_subaddress(address, self.opt.subaddressing).as_ref()),
&self.mappings.attr_id, &self.mappings.attr_id,
) )
.await? .await?
.success()?; .success()
.map(|(rs, _res)| self.extract_ids(rs))?;
let mut ids = Vec::new(); if ids.is_empty() && self.opt.catch_all {
for entry in rs { self.pool
let entry = SearchEntry::construct(entry); .get()
'outer: for attr in &self.mappings.attr_id { .await?
if let Some(values) = entry.attrs.get(attr) { .search(
for id in values { &self.mappings.base_dn,
if let Ok(id) = id.parse() { Scope::Subtree,
ids.push(id); &self
break 'outer; .mappings
} .filter_email
} .build(&to_catch_all_address(address)),
} &self.mappings.attr_id,
} )
.await?
.success()
.map(|(rs, _res)| self.extract_ids(rs))
.map_err(|e| e.into())
} else {
Ok(ids)
} }
Ok(ids)
} }
async fn rcpt(&self, address: &str) -> crate::Result<bool> { async fn rcpt(&self, address: &str) -> crate::Result<bool> {
self.pool match self
.pool
.get() .get()
.await? .await?
.streaming_search( .streaming_search(
&self.mappings.base_dn, &self.mappings.base_dn,
Scope::Subtree, Scope::Subtree,
&self.mappings.filter_email.build(address), &self
.mappings
.filter_email
.build(unwrap_subaddress(address, self.opt.subaddressing).as_ref()),
&self.mappings.attr_email_address, &self.mappings.attr_email_address,
) )
.await? .await?
.next() .next()
.await .await
.map(|entry| entry.is_some()) {
.map_err(|e| e.into()) Ok(Some(_)) => Ok(true),
Ok(None) if self.opt.catch_all => self
.pool
.get()
.await?
.streaming_search(
&self.mappings.base_dn,
Scope::Subtree,
&self
.mappings
.filter_email
.build(&to_catch_all_address(address)),
&self.mappings.attr_email_address,
)
.await?
.next()
.await
.map(|entry| entry.is_some())
.map_err(|e| e.into()),
Ok(None) => Ok(false),
Err(e) => Err(e.into()),
}
} }
async fn vrfy(&self, address: &str) -> crate::Result<Vec<String>> { async fn vrfy(&self, address: &str) -> crate::Result<Vec<String>> {
@ -166,7 +199,10 @@ impl Directory for LdapDirectory {
.streaming_search( .streaming_search(
&self.mappings.base_dn, &self.mappings.base_dn,
Scope::Subtree, Scope::Subtree,
&self.mappings.filter_verify.build(address), &self
.mappings
.filter_verify
.build(unwrap_subaddress(address, self.opt.subaddressing).as_ref()),
&self.mappings.attr_email_address, &self.mappings.attr_email_address,
) )
.await?; .await?;
@ -196,7 +232,10 @@ impl Directory for LdapDirectory {
.streaming_search( .streaming_search(
&self.mappings.base_dn, &self.mappings.base_dn,
Scope::Subtree, Scope::Subtree,
&self.mappings.filter_expand.build(address), &self
.mappings
.filter_expand
.build(unwrap_subaddress(address, self.opt.subaddressing).as_ref()),
&self.mappings.attr_email_address, &self.mappings.attr_email_address,
) )
.await?; .await?;
@ -289,6 +328,24 @@ impl LdapDirectory {
.entry_to_principal(SearchEntry::construct(entry)) .entry_to_principal(SearchEntry::construct(entry))
})) }))
} }
fn extract_ids(&self, rs: Vec<ResultEntry>) -> Vec<u32> {
let mut ids = Vec::with_capacity(rs.len());
for entry in rs {
let entry = SearchEntry::construct(entry);
'outer: for attr in &self.mappings.attr_id {
if let Some(values) = entry.attrs.get(attr) {
for id in values {
if let Ok(id) = id.parse() {
ids.push(id);
break 'outer;
}
}
}
}
}
ids
}
} }
impl LdapMappings { impl LdapMappings {

View file

@ -1,6 +1,8 @@
use bb8::Pool; use bb8::Pool;
use ldap3::{ldap_escape, LdapConnSettings}; use ldap3::{ldap_escape, LdapConnSettings};
use crate::DirectoryOptions;
pub mod config; pub mod config;
pub mod lookup; pub mod lookup;
pub mod pool; pub mod pool;
@ -8,6 +10,7 @@ pub mod pool;
pub struct LdapDirectory { pub struct LdapDirectory {
pool: Pool<LdapConnectionManager>, pool: Pool<LdapConnectionManager>,
mappings: LdapMappings, mappings: LdapMappings,
opt: DirectoryOptions,
} }
#[derive(Debug, Default)] #[derive(Debug, Default)]

View file

@ -1,4 +1,4 @@
use std::{fmt::Debug, sync::Arc}; use std::{borrow::Cow, fmt::Debug, sync::Arc};
use ahash::{AHashMap, AHashSet}; use ahash::{AHashMap, AHashSet};
use bb8::RunError; use bb8::RunError;
@ -143,6 +143,12 @@ impl Debug for Lookup {
} }
} }
#[derive(Debug, Default)]
struct DirectoryOptions {
catch_all: bool,
subaddressing: bool,
}
#[derive(Default, Clone, Debug)] #[derive(Default, Clone, Debug)]
pub struct DirectoryConfig { pub struct DirectoryConfig {
pub directories: AHashMap<String, Arc<dyn Directory>>, pub directories: AHashMap<String, Arc<dyn Directory>>,
@ -256,3 +262,24 @@ impl DirectoryError {
DirectoryError::TimedOut DirectoryError::TimedOut
} }
} }
#[inline(always)]
fn unwrap_subaddress(address: &str, allow_subaddessing: bool) -> Cow<'_, str> {
if allow_subaddessing {
if let Some((local_part, domain_part)) = address.rsplit_once('@') {
if let Some((local_part, _)) = local_part.split_once('+') {
return format!("{}@{}", local_part, domain_part).into();
}
}
}
address.into()
}
#[inline(always)]
fn to_catch_all_address(address: &str) -> String {
address
.rsplit_once('@')
.map(|(_, domain_part)| format!("@{}", domain_part))
.unwrap_or_else(|| address.into())
}

View file

@ -2,7 +2,7 @@ use std::sync::Arc;
use utils::config::{utils::AsKey, Config}; use utils::config::{utils::AsKey, Config};
use crate::{config::ConfigDirectory, Directory, Principal, Type}; use crate::{config::ConfigDirectory, Directory, DirectoryOptions, Principal, Type};
use super::{EmailType, MemoryDirectory}; use super::{EmailType, MemoryDirectory};
@ -54,19 +54,26 @@ impl MemoryDirectory {
EmailType::Primary(id) EmailType::Primary(id)
}); });
if let Some((_, domain)) = email.rsplit_once('@') {
directory.domains.insert(domain.to_lowercase());
}
emails.push(if pos > 0 { emails.push(if pos > 0 {
EmailType::Alias(email.to_string()) EmailType::Alias(email.to_lowercase())
} else { } else {
EmailType::Primary(email.to_string()) EmailType::Primary(email.to_lowercase())
}); });
} }
for (_, email) in config.values((prefix.as_str(), "users", lookup_id, "email-list")) { for (_, email) in config.values((prefix.as_str(), "users", lookup_id, "email-list")) {
directory directory
.emails_to_ids .emails_to_ids
.entry(email.to_string()) .entry(email.to_lowercase())
.or_default() .or_default()
.push(EmailType::List(id)); .push(EmailType::List(id));
emails.push(EmailType::List(email.to_string())); if let Some((_, domain)) = email.rsplit_once('@') {
directory.domains.insert(domain.to_lowercase());
}
emails.push(EmailType::List(email.to_lowercase()));
} }
directory.ids_to_email.insert(id, emails); directory.ids_to_email.insert(id, emails);
} }
@ -95,7 +102,10 @@ impl MemoryDirectory {
}); });
} }
directory.domains = config.parse_lookup_list((&prefix, "lookup.domains"))?; directory
.domains
.extend(config.parse_lookup_list((&prefix, "lookup.domains"))?);
directory.opt = DirectoryOptions::from_config(config, prefix)?;
Ok(Arc::new(directory)) Ok(Arc::new(directory))
} }

View file

@ -1,6 +1,6 @@
use mail_send::Credentials; use mail_send::Credentials;
use crate::{Directory, DirectoryError, Principal}; use crate::{to_catch_all_address, unwrap_subaddress, Directory, DirectoryError, Principal};
use super::{EmailType, MemoryDirectory}; use super::{EmailType, MemoryDirectory};
@ -66,7 +66,14 @@ impl Directory for MemoryDirectory {
async fn ids_by_email(&self, address: &str) -> crate::Result<Vec<u32>> { async fn ids_by_email(&self, address: &str) -> crate::Result<Vec<u32>> {
Ok(self Ok(self
.emails_to_ids .emails_to_ids
.get(address) .get(unwrap_subaddress(address, self.opt.subaddressing).as_ref())
.or_else(|| {
if self.opt.catch_all {
self.emails_to_ids.get(&to_catch_all_address(address))
} else {
None
}
})
.map(|ids| { .map(|ids| {
ids.iter() ids.iter()
.map(|t| match t { .map(|t| match t {
@ -78,13 +85,19 @@ impl Directory for MemoryDirectory {
} }
async fn rcpt(&self, address: &str) -> crate::Result<bool> { async fn rcpt(&self, address: &str) -> crate::Result<bool> {
Ok(self.emails_to_ids.get(address).is_some()) Ok(self
.emails_to_ids
.contains_key(unwrap_subaddress(address, self.opt.subaddressing).as_ref())
|| (self.opt.catch_all && self.domains.contains(&to_catch_all_address(address))))
} }
async fn vrfy(&self, address: &str) -> crate::Result<Vec<String>> { async fn vrfy(&self, address: &str) -> crate::Result<Vec<String>> {
let mut result = Vec::new(); let mut result = Vec::new();
let address = unwrap_subaddress(address, self.opt.subaddressing);
for (key, value) in &self.emails_to_ids { for (key, value) in &self.emails_to_ids {
if key.contains(address) && value.iter().any(|t| matches!(t, EmailType::Primary(_))) { if key.contains(address.as_ref())
&& value.iter().any(|t| matches!(t, EmailType::Primary(_)))
{
result.push(key.clone()) result.push(key.clone())
} }
} }
@ -93,8 +106,9 @@ impl Directory for MemoryDirectory {
async fn expn(&self, address: &str) -> crate::Result<Vec<String>> { async fn expn(&self, address: &str) -> crate::Result<Vec<String>> {
let mut result = Vec::new(); let mut result = Vec::new();
let address = unwrap_subaddress(address, self.opt.subaddressing);
for (key, value) in &self.emails_to_ids { for (key, value) in &self.emails_to_ids {
if key == address { if key == address.as_ref() {
for item in value { for item in value {
if let EmailType::List(id) = item { if let EmailType::List(id) = item {
for addr in self.ids_to_email.get(id).unwrap() { for addr in self.ids_to_email.get(id).unwrap() {
@ -114,13 +128,6 @@ impl Directory for MemoryDirectory {
} }
async fn is_local_domain(&self, domain: &str) -> crate::Result<bool> { async fn is_local_domain(&self, domain: &str) -> crate::Result<bool> {
Ok(if !self.domains.contains(domain) { Ok(self.domains.contains(domain))
let domain = format!("@{domain}");
self.emails_to_ids
.keys()
.any(|email| email.ends_with(&domain))
} else {
true
})
} }
} }

View file

@ -1,6 +1,6 @@
use ahash::{AHashMap, AHashSet}; use ahash::{AHashMap, AHashSet};
use crate::Principal; use crate::{DirectoryOptions, Principal};
pub mod config; pub mod config;
pub mod lookup; pub mod lookup;
@ -12,6 +12,7 @@ pub struct MemoryDirectory {
emails_to_ids: AHashMap<String, Vec<EmailType<u32>>>, emails_to_ids: AHashMap<String, Vec<EmailType<u32>>>,
ids_to_email: AHashMap<u32, Vec<EmailType<String>>>, ids_to_email: AHashMap<u32, Vec<EmailType<String>>>,
domains: AHashSet<String>, domains: AHashSet<String>,
opt: DirectoryOptions,
} }
enum EmailType<T> { enum EmailType<T> {

View file

@ -3,7 +3,7 @@ use std::sync::Arc;
use sqlx::any::{install_default_drivers, AnyPoolOptions}; use sqlx::any::{install_default_drivers, AnyPoolOptions};
use utils::config::{utils::AsKey, Config}; use utils::config::{utils::AsKey, Config};
use crate::{cache::CachedDirectory, Directory}; use crate::{cache::CachedDirectory, Directory, DirectoryOptions};
use super::{SqlDirectory, SqlMappings}; use super::{SqlDirectory, SqlMappings};
@ -93,6 +93,14 @@ impl SqlDirectory {
.to_string(), .to_string(),
}; };
CachedDirectory::try_from_config(config, &prefix, SqlDirectory { pool, mappings }) CachedDirectory::try_from_config(
config,
&prefix,
SqlDirectory {
pool,
mappings,
opt: DirectoryOptions::from_config(config, prefix.as_str())?,
},
)
} }
} }

View file

@ -1,7 +1,7 @@
use mail_send::Credentials; use mail_send::Credentials;
use sqlx::{any::AnyRow, Column, Row}; use sqlx::{any::AnyRow, Column, Row};
use crate::{Directory, Principal, Type}; use crate::{to_catch_all_address, unwrap_subaddress, Directory, Principal, Type};
use super::{SqlDirectory, SqlMappings}; use super::{SqlDirectory, SqlMappings};
@ -74,26 +74,46 @@ impl Directory for SqlDirectory {
} }
async fn ids_by_email(&self, address: &str) -> crate::Result<Vec<u32>> { async fn ids_by_email(&self, address: &str) -> crate::Result<Vec<u32>> {
sqlx::query_scalar::<_, i64>(&self.mappings.query_recipients) match sqlx::query_scalar::<_, i64>(&self.mappings.query_recipients)
.bind(address) .bind(unwrap_subaddress(address, self.opt.subaddressing).as_ref())
.fetch_all(&self.pool) .fetch_all(&self.pool)
.await .await
.map(|ids| ids.into_iter().map(|id| id as u32).collect()) {
.map_err(Into::into) Ok(ids) if !ids.is_empty() => Ok(ids.into_iter().map(|id| id as u32).collect()),
Ok(_) if self.opt.catch_all => {
sqlx::query_scalar::<_, i64>(&self.mappings.query_recipients)
.bind(to_catch_all_address(address))
.fetch_all(&self.pool)
.await
.map(|ids| ids.into_iter().map(|id| id as u32).collect())
.map_err(Into::into)
}
Ok(_) => Ok(vec![]),
Err(err) => Err(err.into()),
}
} }
async fn rcpt(&self, address: &str) -> crate::Result<bool> { async fn rcpt(&self, address: &str) -> crate::Result<bool> {
sqlx::query(&self.mappings.query_recipients) match sqlx::query(&self.mappings.query_recipients)
.bind(address) .bind(unwrap_subaddress(address, self.opt.subaddressing).as_ref())
.fetch_optional(&self.pool) .fetch_optional(&self.pool)
.await .await
.map(|id| id.is_some()) {
.map_err(Into::into) Ok(Some(_)) => Ok(true),
Ok(None) if self.opt.catch_all => sqlx::query(&self.mappings.query_recipients)
.bind(to_catch_all_address(address))
.fetch_optional(&self.pool)
.await
.map(|id| id.is_some())
.map_err(Into::into),
Ok(None) => Ok(false),
Err(err) => Err(err.into()),
}
} }
async fn vrfy(&self, address: &str) -> crate::Result<Vec<String>> { async fn vrfy(&self, address: &str) -> crate::Result<Vec<String>> {
sqlx::query_scalar::<_, String>(&self.mappings.query_verify) sqlx::query_scalar::<_, String>(&self.mappings.query_verify)
.bind(address) .bind(unwrap_subaddress(address, self.opt.subaddressing).as_ref())
.fetch_all(&self.pool) .fetch_all(&self.pool)
.await .await
.map_err(Into::into) .map_err(Into::into)
@ -101,7 +121,7 @@ impl Directory for SqlDirectory {
async fn expn(&self, address: &str) -> crate::Result<Vec<String>> { async fn expn(&self, address: &str) -> crate::Result<Vec<String>> {
sqlx::query_scalar::<_, String>(&self.mappings.query_expand) sqlx::query_scalar::<_, String>(&self.mappings.query_expand)
.bind(address) .bind(unwrap_subaddress(address, self.opt.subaddressing).as_ref())
.fetch_all(&self.pool) .fetch_all(&self.pool)
.await .await
.map_err(Into::into) .map_err(Into::into)

View file

@ -1,11 +1,14 @@
use sqlx::{Any, Pool}; use sqlx::{Any, Pool};
use crate::DirectoryOptions;
pub mod config; pub mod config;
pub mod lookup; pub mod lookup;
pub struct SqlDirectory { pub struct SqlDirectory {
pool: Pool<Any>, pool: Pool<Any>,
mappings: SqlMappings, mappings: SqlMappings,
opt: DirectoryOptions,
} }
#[derive(Debug)] #[derive(Debug)]

View file

@ -79,6 +79,15 @@ watchconfig = true
diskQuota = [500000] diskQuota = [500000]
userPassword = ["$2y$05$bvIG6Nmid91Mu9RcmmWZfO5HJIMCT8riNW0hEp8f6/FuA2/mHZFpe"] userPassword = ["$2y$05$bvIG6Nmid91Mu9RcmmWZfO5HJIMCT8riNW0hEp8f6/FuA2/mHZFpe"]
[[users]]
name = "robert"
sn = "@catchall.org"
mail = "robert@catchall.org"
uidnumber = 7
[[users.customattributes]]
principalName = ["Robect Foobar"]
userPassword = ["nopass"]
[[users]] [[users]]
name = "serviceuser" name = "serviceuser"
mail = "serviceuser@example.org" mail = "serviceuser@example.org"

View file

@ -147,10 +147,22 @@ async fn ldap_directory() {
handle.ids_by_email("jane@example.org").await.unwrap(), handle.ids_by_email("jane@example.org").await.unwrap(),
vec![3], vec![3],
); );
compare_sorted(
handle.ids_by_email("jane+alias@example.org").await.unwrap(),
vec![3],
);
compare_sorted( compare_sorted(
handle.ids_by_email("info@example.org").await.unwrap(), handle.ids_by_email("info@example.org").await.unwrap(),
vec![2, 3, 4], vec![2, 3, 4],
); );
compare_sorted(
handle.ids_by_email("info+alias@example.org").await.unwrap(),
vec![2, 3, 4],
);
compare_sorted(
handle.ids_by_email("unknown@example.org").await.unwrap(),
Vec::<u32>::new(),
);
// Domain validation // Domain validation
assert!(handle.is_local_domain("example.org").await.unwrap()); assert!(handle.is_local_domain("example.org").await.unwrap());
@ -159,6 +171,9 @@ async fn ldap_directory() {
// RCPT TO // RCPT TO
assert!(handle.rcpt("jane@example.org").await.unwrap()); assert!(handle.rcpt("jane@example.org").await.unwrap());
assert!(handle.rcpt("info@example.org").await.unwrap()); assert!(handle.rcpt("info@example.org").await.unwrap());
assert!(handle.rcpt("jane+alias@example.org").await.unwrap());
assert!(handle.rcpt("info+alias@example.org").await.unwrap());
assert!(handle.rcpt("random_user@catchall.org").await.unwrap());
assert!(!handle.rcpt("invalid@example.org").await.unwrap()); assert!(!handle.rcpt("invalid@example.org").await.unwrap());
// VRFY // VRFY
@ -170,6 +185,10 @@ async fn ldap_directory() {
handle.vrfy("john").await.unwrap(), handle.vrfy("john").await.unwrap(),
vec!["john@example.org".to_string()], vec!["john@example.org".to_string()],
); );
compare_sorted(
handle.vrfy("jane+alias@example").await.unwrap(),
vec!["jane@example.org".to_string()],
);
compare_sorted(handle.vrfy("info").await.unwrap(), Vec::<String>::new()); compare_sorted(handle.vrfy("info").await.unwrap(), Vec::<String>::new());
compare_sorted(handle.vrfy("invalid").await.unwrap(), Vec::<String>::new()); compare_sorted(handle.vrfy("invalid").await.unwrap(), Vec::<String>::new());

View file

@ -15,6 +15,10 @@ const CONFIG: &str = r#"
type = "sql" type = "sql"
address = "sqlite::memory:" address = "sqlite::memory:"
[directory."sql".options]
catch-all = true
subaddressing = true
[directory."sql".pool] [directory."sql".pool]
max-connections = 1 max-connections = 1
@ -50,6 +54,10 @@ base-dn = "dc=example,dc=org"
dn = "cn=serviceuser,ou=svcaccts,dc=example,dc=org" dn = "cn=serviceuser,ou=svcaccts,dc=example,dc=org"
secret = "mysecret" secret = "mysecret"
[directory."ldap".options]
catch-all = true
subaddressing = true
[directory."ldap".filter] [directory."ldap".filter]
login = "(&(objectClass=posixAccount)(accountStatus=active)(cn=?))" login = "(&(objectClass=posixAccount)(accountStatus=active)(cn=?))"
name = "(&(|(objectClass=posixAccount)(objectClass=posixGroup))(cn=?))" name = "(&(|(objectClass=posixAccount)(objectClass=posixGroup))(cn=?))"
@ -111,6 +119,10 @@ ttl = {positive = '10s', negative = '5s'}
[directory."local"] [directory."local"]
type = "memory" type = "memory"
[directory."local".options]
catch-all = true
subaddressing = true
[[directory."local".users]] [[directory."local".users]]
name = "john" name = "john"
description = "John Doe" description = "John Doe"

View file

@ -53,6 +53,11 @@ async fn sql_directory() {
link_test_address(handle.as_ref(), "jane", "info@example.org", "list").await; link_test_address(handle.as_ref(), "jane", "info@example.org", "list").await;
link_test_address(handle.as_ref(), "bill", "info@example.org", "list").await; link_test_address(handle.as_ref(), "bill", "info@example.org", "list").await;
// Add catch-all user
create_test_user(handle.as_ref(), "robert", "abcde", "Robert Foobar").await;
link_test_address(handle.as_ref(), "robert", "robert@catchall.org", "primary").await;
link_test_address(handle.as_ref(), "robert", "@catchall.org", "alias").await;
// Test authentication // Test authentication
assert_eq!( assert_eq!(
handle handle
@ -177,6 +182,22 @@ async fn sql_directory() {
handle.ids_by_email("info@example.org").await.unwrap(), handle.ids_by_email("info@example.org").await.unwrap(),
vec![2, 3, 4] vec![2, 3, 4]
); );
assert_eq!(
handle.ids_by_email("jane+alias@example.org").await.unwrap(),
vec![3]
);
assert_eq!(
handle.ids_by_email("info+alias@example.org").await.unwrap(),
vec![2, 3, 4]
);
assert_eq!(
handle.ids_by_email("unknown@example.org").await.unwrap(),
Vec::<u32>::new()
);
assert_eq!(
handle.ids_by_email("anything@catchall.org").await.unwrap(),
vec![7]
);
// Domain validation // Domain validation
assert!(handle.is_local_domain("example.org").await.unwrap()); assert!(handle.is_local_domain("example.org").await.unwrap());
@ -185,6 +206,9 @@ async fn sql_directory() {
// RCPT TO // RCPT TO
assert!(handle.rcpt("jane@example.org").await.unwrap()); assert!(handle.rcpt("jane@example.org").await.unwrap());
assert!(handle.rcpt("info@example.org").await.unwrap()); assert!(handle.rcpt("info@example.org").await.unwrap());
assert!(handle.rcpt("jane+alias@example.org").await.unwrap());
assert!(handle.rcpt("info+alias@example.org").await.unwrap());
assert!(handle.rcpt("random_user@catchall.org").await.unwrap());
assert!(!handle.rcpt("invalid@example.org").await.unwrap()); assert!(!handle.rcpt("invalid@example.org").await.unwrap());
// VRFY // VRFY
@ -196,6 +220,10 @@ async fn sql_directory() {
handle.vrfy("john").await.unwrap(), handle.vrfy("john").await.unwrap(),
vec!["john@example.org".to_string()] vec!["john@example.org".to_string()]
); );
assert_eq!(
handle.vrfy("jane+alias@example").await.unwrap(),
vec!["jane@example.org".to_string()]
);
assert_eq!(handle.vrfy("info").await.unwrap(), Vec::<String>::new()); assert_eq!(handle.vrfy("info").await.unwrap(), Vec::<String>::new());
assert_eq!(handle.vrfy("invalid").await.unwrap(), Vec::<String>::new()); assert_eq!(handle.vrfy("invalid").await.unwrap(), Vec::<String>::new());

View file

@ -228,7 +228,7 @@ pub async fn jmap_tests() {
let delete = true; let delete = true;
let mut params = init_jmap_tests(delete).await; let mut params = init_jmap_tests(delete).await;
/*email_query::test(params.server.clone(), &mut params.client, delete).await; email_query::test(params.server.clone(), &mut params.client, delete).await;
email_get::test(params.server.clone(), &mut params.client).await; email_get::test(params.server.clone(), &mut params.client).await;
email_set::test(params.server.clone(), &mut params.client).await; email_set::test(params.server.clone(), &mut params.client).await;
email_parse::test(params.server.clone(), &mut params.client).await; email_parse::test(params.server.clone(), &mut params.client).await;
@ -249,7 +249,7 @@ pub async fn jmap_tests() {
vacation_response::test(params.server.clone(), &mut params.client).await; vacation_response::test(params.server.clone(), &mut params.client).await;
email_submission::test(params.server.clone(), &mut params.client).await; email_submission::test(params.server.clone(), &mut params.client).await;
websocket::test(params.server.clone(), &mut params.client).await; websocket::test(params.server.clone(), &mut params.client).await;
quota::test(params.server.clone(), &mut params.client).await;*/ quota::test(params.server.clone(), &mut params.client).await;
stress_test::test(params.server.clone(), params.client).await; stress_test::test(params.server.clone(), params.client).await;
if delete { if delete {