mirror of
https://github.com/stalwartlabs/mail-server.git
synced 2024-09-20 07:16:18 +08:00
Subaddressing and catch-all addresses support.
This commit is contained in:
parent
891d39940d
commit
1ce0cee7e6
|
@ -11,7 +11,7 @@ use ahash::{AHashMap, AHashSet};
|
|||
|
||||
use crate::{
|
||||
imap::ImapDirectory, ldap::LdapDirectory, memory::MemoryDirectory, smtp::SmtpDirectory,
|
||||
sql::SqlDirectory, DirectoryConfig, Lookup,
|
||||
sql::SqlDirectory, DirectoryConfig, DirectoryOptions, Lookup,
|
||||
};
|
||||
|
||||
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>(
|
||||
config: &Config,
|
||||
prefix: &str,
|
||||
|
|
|
@ -3,7 +3,7 @@ use std::sync::Arc;
|
|||
use ldap3::LdapConnSettings;
|
||||
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};
|
||||
|
||||
|
@ -105,6 +105,7 @@ impl LdapDirectory {
|
|||
LdapDirectory {
|
||||
mappings,
|
||||
pool: build_pool(config, &prefix, manager)?,
|
||||
opt: DirectoryOptions::from_config(config, prefix.as_str())?,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use ldap3::{Scope, SearchEntry};
|
||||
use ldap3::{ResultEntry, Scope, SearchEntry};
|
||||
use mail_send::Credentials;
|
||||
|
||||
use crate::{Directory, Principal, Type};
|
||||
use crate::{to_catch_all_address, unwrap_subaddress, Directory, Principal, Type};
|
||||
|
||||
use super::{LdapDirectory, LdapMappings};
|
||||
|
||||
|
@ -109,53 +109,86 @@ impl Directory for LdapDirectory {
|
|||
Ok(emails)
|
||||
}
|
||||
|
||||
async fn ids_by_email(&self, email: &str) -> crate::Result<Vec<u32>> {
|
||||
let (rs, _res) = self
|
||||
async fn ids_by_email(&self, address: &str) -> crate::Result<Vec<u32>> {
|
||||
let ids = self
|
||||
.pool
|
||||
.get()
|
||||
.await?
|
||||
.search(
|
||||
&self.mappings.base_dn,
|
||||
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,
|
||||
)
|
||||
.await?
|
||||
.success()?;
|
||||
.success()
|
||||
.map(|(rs, _res)| self.extract_ids(rs))?;
|
||||
|
||||
let mut ids = Vec::new();
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if ids.is_empty() && self.opt.catch_all {
|
||||
self.pool
|
||||
.get()
|
||||
.await?
|
||||
.search(
|
||||
&self.mappings.base_dn,
|
||||
Scope::Subtree,
|
||||
&self
|
||||
.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> {
|
||||
self.pool
|
||||
match self
|
||||
.pool
|
||||
.get()
|
||||
.await?
|
||||
.streaming_search(
|
||||
&self.mappings.base_dn,
|
||||
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,
|
||||
)
|
||||
.await?
|
||||
.next()
|
||||
.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>> {
|
||||
|
@ -166,7 +199,10 @@ impl Directory for LdapDirectory {
|
|||
.streaming_search(
|
||||
&self.mappings.base_dn,
|
||||
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,
|
||||
)
|
||||
.await?;
|
||||
|
@ -196,7 +232,10 @@ impl Directory for LdapDirectory {
|
|||
.streaming_search(
|
||||
&self.mappings.base_dn,
|
||||
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,
|
||||
)
|
||||
.await?;
|
||||
|
@ -289,6 +328,24 @@ impl LdapDirectory {
|
|||
.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 {
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
use bb8::Pool;
|
||||
use ldap3::{ldap_escape, LdapConnSettings};
|
||||
|
||||
use crate::DirectoryOptions;
|
||||
|
||||
pub mod config;
|
||||
pub mod lookup;
|
||||
pub mod pool;
|
||||
|
@ -8,6 +10,7 @@ pub mod pool;
|
|||
pub struct LdapDirectory {
|
||||
pool: Pool<LdapConnectionManager>,
|
||||
mappings: LdapMappings,
|
||||
opt: DirectoryOptions,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use std::{fmt::Debug, sync::Arc};
|
||||
use std::{borrow::Cow, fmt::Debug, sync::Arc};
|
||||
|
||||
use ahash::{AHashMap, AHashSet};
|
||||
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)]
|
||||
pub struct DirectoryConfig {
|
||||
pub directories: AHashMap<String, Arc<dyn Directory>>,
|
||||
|
@ -256,3 +262,24 @@ impl DirectoryError {
|
|||
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())
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ use std::sync::Arc;
|
|||
|
||||
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};
|
||||
|
||||
|
@ -54,19 +54,26 @@ impl MemoryDirectory {
|
|||
EmailType::Primary(id)
|
||||
});
|
||||
|
||||
if let Some((_, domain)) = email.rsplit_once('@') {
|
||||
directory.domains.insert(domain.to_lowercase());
|
||||
}
|
||||
|
||||
emails.push(if pos > 0 {
|
||||
EmailType::Alias(email.to_string())
|
||||
EmailType::Alias(email.to_lowercase())
|
||||
} else {
|
||||
EmailType::Primary(email.to_string())
|
||||
EmailType::Primary(email.to_lowercase())
|
||||
});
|
||||
}
|
||||
for (_, email) in config.values((prefix.as_str(), "users", lookup_id, "email-list")) {
|
||||
directory
|
||||
.emails_to_ids
|
||||
.entry(email.to_string())
|
||||
.entry(email.to_lowercase())
|
||||
.or_default()
|
||||
.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);
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use mail_send::Credentials;
|
||||
|
||||
use crate::{Directory, DirectoryError, Principal};
|
||||
use crate::{to_catch_all_address, unwrap_subaddress, Directory, DirectoryError, Principal};
|
||||
|
||||
use super::{EmailType, MemoryDirectory};
|
||||
|
||||
|
@ -66,7 +66,14 @@ impl Directory for MemoryDirectory {
|
|||
async fn ids_by_email(&self, address: &str) -> crate::Result<Vec<u32>> {
|
||||
Ok(self
|
||||
.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| {
|
||||
ids.iter()
|
||||
.map(|t| match t {
|
||||
|
@ -78,13 +85,19 @@ impl Directory for MemoryDirectory {
|
|||
}
|
||||
|
||||
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>> {
|
||||
let mut result = Vec::new();
|
||||
let address = unwrap_subaddress(address, self.opt.subaddressing);
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
@ -93,8 +106,9 @@ impl Directory for MemoryDirectory {
|
|||
|
||||
async fn expn(&self, address: &str) -> crate::Result<Vec<String>> {
|
||||
let mut result = Vec::new();
|
||||
let address = unwrap_subaddress(address, self.opt.subaddressing);
|
||||
for (key, value) in &self.emails_to_ids {
|
||||
if key == address {
|
||||
if key == address.as_ref() {
|
||||
for item in value {
|
||||
if let EmailType::List(id) = item {
|
||||
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> {
|
||||
Ok(if !self.domains.contains(domain) {
|
||||
let domain = format!("@{domain}");
|
||||
self.emails_to_ids
|
||||
.keys()
|
||||
.any(|email| email.ends_with(&domain))
|
||||
} else {
|
||||
true
|
||||
})
|
||||
Ok(self.domains.contains(domain))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use ahash::{AHashMap, AHashSet};
|
||||
|
||||
use crate::Principal;
|
||||
use crate::{DirectoryOptions, Principal};
|
||||
|
||||
pub mod config;
|
||||
pub mod lookup;
|
||||
|
@ -12,6 +12,7 @@ pub struct MemoryDirectory {
|
|||
emails_to_ids: AHashMap<String, Vec<EmailType<u32>>>,
|
||||
ids_to_email: AHashMap<u32, Vec<EmailType<String>>>,
|
||||
domains: AHashSet<String>,
|
||||
opt: DirectoryOptions,
|
||||
}
|
||||
|
||||
enum EmailType<T> {
|
||||
|
|
|
@ -3,7 +3,7 @@ use std::sync::Arc;
|
|||
use sqlx::any::{install_default_drivers, AnyPoolOptions};
|
||||
use utils::config::{utils::AsKey, Config};
|
||||
|
||||
use crate::{cache::CachedDirectory, Directory};
|
||||
use crate::{cache::CachedDirectory, Directory, DirectoryOptions};
|
||||
|
||||
use super::{SqlDirectory, SqlMappings};
|
||||
|
||||
|
@ -93,6 +93,14 @@ impl SqlDirectory {
|
|||
.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())?,
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use mail_send::Credentials;
|
||||
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};
|
||||
|
||||
|
@ -74,26 +74,46 @@ impl Directory for SqlDirectory {
|
|||
}
|
||||
|
||||
async fn ids_by_email(&self, address: &str) -> crate::Result<Vec<u32>> {
|
||||
sqlx::query_scalar::<_, i64>(&self.mappings.query_recipients)
|
||||
.bind(address)
|
||||
match sqlx::query_scalar::<_, i64>(&self.mappings.query_recipients)
|
||||
.bind(unwrap_subaddress(address, self.opt.subaddressing).as_ref())
|
||||
.fetch_all(&self.pool)
|
||||
.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> {
|
||||
sqlx::query(&self.mappings.query_recipients)
|
||||
.bind(address)
|
||||
match sqlx::query(&self.mappings.query_recipients)
|
||||
.bind(unwrap_subaddress(address, self.opt.subaddressing).as_ref())
|
||||
.fetch_optional(&self.pool)
|
||||
.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>> {
|
||||
sqlx::query_scalar::<_, String>(&self.mappings.query_verify)
|
||||
.bind(address)
|
||||
.bind(unwrap_subaddress(address, self.opt.subaddressing).as_ref())
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
|
@ -101,7 +121,7 @@ impl Directory for SqlDirectory {
|
|||
|
||||
async fn expn(&self, address: &str) -> crate::Result<Vec<String>> {
|
||||
sqlx::query_scalar::<_, String>(&self.mappings.query_expand)
|
||||
.bind(address)
|
||||
.bind(unwrap_subaddress(address, self.opt.subaddressing).as_ref())
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
use sqlx::{Any, Pool};
|
||||
|
||||
use crate::DirectoryOptions;
|
||||
|
||||
pub mod config;
|
||||
pub mod lookup;
|
||||
|
||||
pub struct SqlDirectory {
|
||||
pool: Pool<Any>,
|
||||
mappings: SqlMappings,
|
||||
opt: DirectoryOptions,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
|
|
@ -79,6 +79,15 @@ watchconfig = true
|
|||
diskQuota = [500000]
|
||||
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]]
|
||||
name = "serviceuser"
|
||||
mail = "serviceuser@example.org"
|
||||
|
|
|
@ -147,10 +147,22 @@ async fn ldap_directory() {
|
|||
handle.ids_by_email("jane@example.org").await.unwrap(),
|
||||
vec![3],
|
||||
);
|
||||
compare_sorted(
|
||||
handle.ids_by_email("jane+alias@example.org").await.unwrap(),
|
||||
vec![3],
|
||||
);
|
||||
compare_sorted(
|
||||
handle.ids_by_email("info@example.org").await.unwrap(),
|
||||
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
|
||||
assert!(handle.is_local_domain("example.org").await.unwrap());
|
||||
|
@ -159,6 +171,9 @@ async fn ldap_directory() {
|
|||
// RCPT TO
|
||||
assert!(handle.rcpt("jane@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());
|
||||
|
||||
// VRFY
|
||||
|
@ -170,6 +185,10 @@ async fn ldap_directory() {
|
|||
handle.vrfy("john").await.unwrap(),
|
||||
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("invalid").await.unwrap(), Vec::<String>::new());
|
||||
|
||||
|
|
|
@ -15,6 +15,10 @@ const CONFIG: &str = r#"
|
|||
type = "sql"
|
||||
address = "sqlite::memory:"
|
||||
|
||||
[directory."sql".options]
|
||||
catch-all = true
|
||||
subaddressing = true
|
||||
|
||||
[directory."sql".pool]
|
||||
max-connections = 1
|
||||
|
||||
|
@ -50,6 +54,10 @@ base-dn = "dc=example,dc=org"
|
|||
dn = "cn=serviceuser,ou=svcaccts,dc=example,dc=org"
|
||||
secret = "mysecret"
|
||||
|
||||
[directory."ldap".options]
|
||||
catch-all = true
|
||||
subaddressing = true
|
||||
|
||||
[directory."ldap".filter]
|
||||
login = "(&(objectClass=posixAccount)(accountStatus=active)(cn=?))"
|
||||
name = "(&(|(objectClass=posixAccount)(objectClass=posixGroup))(cn=?))"
|
||||
|
@ -111,6 +119,10 @@ ttl = {positive = '10s', negative = '5s'}
|
|||
[directory."local"]
|
||||
type = "memory"
|
||||
|
||||
[directory."local".options]
|
||||
catch-all = true
|
||||
subaddressing = true
|
||||
|
||||
[[directory."local".users]]
|
||||
name = "john"
|
||||
description = "John Doe"
|
||||
|
|
|
@ -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(), "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
|
||||
assert_eq!(
|
||||
handle
|
||||
|
@ -177,6 +182,22 @@ async fn sql_directory() {
|
|||
handle.ids_by_email("info@example.org").await.unwrap(),
|
||||
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
|
||||
assert!(handle.is_local_domain("example.org").await.unwrap());
|
||||
|
@ -185,6 +206,9 @@ async fn sql_directory() {
|
|||
// RCPT TO
|
||||
assert!(handle.rcpt("jane@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());
|
||||
|
||||
// VRFY
|
||||
|
@ -196,6 +220,10 @@ async fn sql_directory() {
|
|||
handle.vrfy("john").await.unwrap(),
|
||||
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("invalid").await.unwrap(), Vec::<String>::new());
|
||||
|
||||
|
|
|
@ -228,7 +228,7 @@ pub async fn jmap_tests() {
|
|||
|
||||
let delete = true;
|
||||
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_set::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;
|
||||
email_submission::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;
|
||||
|
||||
if delete {
|
||||
|
|
Loading…
Reference in a new issue