mirror of
https://github.com/stalwartlabs/mail-server.git
synced 2024-09-20 15:26:17 +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::{
|
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,
|
||||||
|
|
|
@ -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())?,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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)]
|
||||||
|
|
|
@ -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())
|
||||||
|
}
|
||||||
|
|
|
@ -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))
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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> {
|
||||||
|
|
|
@ -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())?,
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)]
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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());
|
||||||
|
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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());
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Reference in a new issue