CLI account management + Directory refactoring

This commit is contained in:
mdecimus
2023-12-18 22:25:42 +01:00
parent 566a2a0ab8
commit ea94de6d77
91 changed files with 3531 additions and 1846 deletions
+9 -18
View File
@@ -21,20 +21,15 @@
* for more details.
*/
use std::sync::Arc;
use mail_send::smtp::tls::build_tls_connector;
use utils::config::{utils::AsKey, Config};
use crate::{cache::CachedDirectory, config::build_pool, Directory};
use crate::core::config::build_pool;
use super::{ImapConnectionManager, ImapDirectory};
impl ImapDirectory {
pub fn from_config(
config: &Config,
prefix: impl AsKey,
) -> utils::config::Result<Arc<dyn Directory>> {
pub fn from_config(config: &Config, prefix: impl AsKey) -> utils::config::Result<Self> {
let prefix = prefix.as_key();
let address = config.value_require((&prefix, "address"))?;
let tls_implicit: bool = config.property_or_static((&prefix, "tls.implicit"), "false")?;
@@ -52,16 +47,12 @@ impl ImapDirectory {
mechanisms: 0.into(),
};
CachedDirectory::try_from_config(
config,
&prefix,
ImapDirectory {
pool: build_pool(config, &prefix, manager)?,
domains: config
.values((&prefix, "local-domains"))
.map(|(_, v)| v.to_lowercase())
.collect(),
},
)
Ok(ImapDirectory {
pool: build_pool(config, &prefix, manager)?,
domains: config
.values((&prefix, "lookup.domains"))
.map(|(_, v)| v.to_lowercase())
.collect(),
})
}
}
+8 -9
View File
@@ -24,13 +24,12 @@
use mail_send::Credentials;
use smtp_proto::{AUTH_CRAM_MD5, AUTH_LOGIN, AUTH_OAUTHBEARER, AUTH_PLAIN, AUTH_XOAUTH2};
use crate::{Directory, DirectoryError, Principal, QueryBy};
use crate::{DirectoryError, Principal, QueryBy};
use super::{ImapDirectory, ImapError};
#[async_trait::async_trait]
impl Directory for ImapDirectory {
async fn query(&self, query: QueryBy<'_>) -> crate::Result<Option<Principal<u32>>> {
impl ImapDirectory {
pub async fn query(&self, query: QueryBy<'_>) -> crate::Result<Option<Principal<u32>>> {
if let QueryBy::Credentials(credentials) = query {
let mut client = self.pool.get().await?;
let mechanism = match credentials {
@@ -77,23 +76,23 @@ impl Directory for ImapDirectory {
}
}
async fn email_to_ids(&self, _address: &str) -> crate::Result<Vec<u32>> {
pub async fn email_to_ids(&self, _address: &str) -> crate::Result<Vec<u32>> {
Err(DirectoryError::unsupported("imap", "email_to_ids"))
}
async fn rcpt(&self, _address: &str) -> crate::Result<bool> {
pub async fn rcpt(&self, _address: &str) -> crate::Result<bool> {
Err(DirectoryError::unsupported("imap", "rcpt"))
}
async fn vrfy(&self, _address: &str) -> crate::Result<Vec<String>> {
pub async fn vrfy(&self, _address: &str) -> crate::Result<Vec<String>> {
Err(DirectoryError::unsupported("imap", "vrfy"))
}
async fn expn(&self, _address: &str) -> crate::Result<Vec<String>> {
pub async fn expn(&self, _address: &str) -> crate::Result<Vec<String>> {
Err(DirectoryError::unsupported("imap", "expn"))
}
async fn is_local_domain(&self, domain: &str) -> crate::Result<bool> {
pub async fn is_local_domain(&self, domain: &str) -> crate::Result<bool> {
Ok(self.domains.contains(domain))
}
}
@@ -27,12 +27,23 @@ use store::{
IterateParams, Store, ValueKey,
};
use crate::{Directory, Principal, QueryBy};
use crate::{Principal, QueryBy};
use super::manage::ManageDirectory;
#[async_trait::async_trait]
impl Directory for Store {
pub trait DirectoryStore: Sync + Send {
async fn query(&self, by: QueryBy<'_>) -> crate::Result<Option<Principal<u32>>>;
async fn email_to_ids(&self, email: &str) -> crate::Result<Vec<u32>>;
async fn is_local_domain(&self, domain: &str) -> crate::Result<bool>;
async fn rcpt(&self, address: &str) -> crate::Result<bool>;
async fn vrfy(&self, address: &str) -> crate::Result<Vec<String>>;
async fn expn(&self, address: &str) -> crate::Result<Vec<String>>;
}
#[async_trait::async_trait]
impl DirectoryStore for Store {
async fn query(&self, by: QueryBy<'_>) -> crate::Result<Option<Principal<u32>>> {
let (username, secret) = match by {
QueryBy::Name(name) => (name, None),
+82 -19
View File
@@ -24,12 +24,15 @@
use jmap_proto::types::collection::Collection;
use store::{
write::{assert::HashedValue, BatchBuilder, DirectoryClass, ValueClass},
IterateParams, Serialize, Store, ValueKey,
Deserialize, IterateParams, Serialize, Store, ValueKey,
};
use crate::{Directory, DirectoryError, ManagementError, Principal, QueryBy, Type};
use crate::{DirectoryError, ManagementError, Principal, QueryBy, Type};
use super::{PrincipalAction, PrincipalField, PrincipalUpdate, PrincipalValue};
use super::{
lookup::DirectoryStore, PrincipalAction, PrincipalField, PrincipalIdType, PrincipalUpdate,
PrincipalValue,
};
#[async_trait::async_trait]
pub trait ManageDirectory {
@@ -43,11 +46,10 @@ pub trait ManageDirectory {
changes: Vec<PrincipalUpdate>,
) -> crate::Result<()>;
async fn delete_account(&self, by: QueryBy<'_>) -> crate::Result<()>;
async fn create_domain(&self, domain: &str) -> crate::Result<()>;
async fn delete_domain(&self, domain: &str) -> crate::Result<()>;
async fn list_accounts(
&self,
start_from: Option<&str>,
typ: Option<Type>,
limit: usize,
) -> crate::Result<Vec<String>>;
async fn map_group_ids(&self, principal: Principal<u32>) -> crate::Result<Principal<String>>;
@@ -56,6 +58,13 @@ pub trait ManageDirectory {
principal: Principal<String>,
create_if_missing: bool,
) -> crate::Result<Principal<u32>>;
async fn create_domain(&self, domain: &str) -> crate::Result<()>;
async fn delete_domain(&self, domain: &str) -> crate::Result<()>;
async fn list_domains(
&self,
start_from: Option<&str>,
limit: usize,
) -> crate::Result<Vec<String>>;
}
#[async_trait::async_trait]
@@ -83,10 +92,11 @@ impl ManageDirectory for Store {
}
async fn get_account_id(&self, name: &str) -> crate::Result<Option<u32>> {
self.get_value::<u32>(ValueKey::from(ValueClass::Directory(
self.get_value::<PrincipalIdType>(ValueKey::from(ValueClass::Directory(
DirectoryClass::NameToId(name.as_bytes().to_vec()),
)))
.await
.map(|v| v.map(|v| v.account_id))
.map_err(Into::into)
}
@@ -114,7 +124,10 @@ impl ManageDirectory for Store {
.with_collection(Collection::Principal)
.create_document(account_id)
.assert_value(name_key.clone(), ())
.set(name_key, account_id.serialize())
.set(
name_key,
PrincipalIdType::new(account_id, Type::Individual).serialize(),
)
.set(
ValueClass::Directory(DirectoryClass::Principal(account_id)),
Principal {
@@ -159,18 +172,20 @@ impl ManageDirectory for Store {
// Make sure new name is not taken
principal.name = principal.name.to_lowercase();
if self.get_account_id(&principal.name).await?.is_some() {
return Err(DirectoryError::Management(ManagementError::NotUniqueField(
PrincipalField::Name,
)));
return Err(DirectoryError::Management(ManagementError::AlreadyExists {
field: PrincipalField::Name,
value: principal.name,
}));
}
// Make sure the e-mail is not taken and validate domain
for email in principal.emails.iter_mut() {
*email = email.to_lowercase();
if self.rcpt(email).await? {
return Err(DirectoryError::Management(ManagementError::NotUniqueField(
PrincipalField::Emails,
)));
return Err(DirectoryError::Management(ManagementError::AlreadyExists {
field: PrincipalField::Emails,
value: email.to_string(),
}));
}
if let Some(domain) = email.split('@').nth(1) {
if !self.is_local_domain(domain).await? {
@@ -201,7 +216,7 @@ impl ManageDirectory for Store {
)
.set(
ValueClass::Directory(DirectoryClass::NameToId(principal.name.into_bytes())),
account_id.serialize(),
PrincipalIdType::new(account_id, principal.typ.into_base_type()).serialize(),
);
// Write email to id mapping
@@ -306,7 +321,10 @@ impl ManageDirectory for Store {
if principal.inner.name != new_name {
if self.get_account_id(&new_name).await?.is_some() {
return Err(DirectoryError::Management(
ManagementError::NotUniqueField(PrincipalField::Name),
ManagementError::AlreadyExists {
field: PrincipalField::Name,
value: new_name,
},
));
}
@@ -318,12 +336,14 @@ impl ManageDirectory for Store {
batch.set(
ValueClass::Directory(DirectoryClass::NameToId(new_name.into_bytes())),
account_id.serialize(),
PrincipalIdType::new(account_id, principal.inner.typ.into_base_type())
.serialize(),
);
}
}
(PrincipalAction::Set, PrincipalField::Type, PrincipalValue::Type(new_type))
if principal.inner.typ != Type::List && new_type != Type::List =>
if matches!(principal.inner.typ, Type::Individual | Type::Superuser)
&& matches!(new_type, Type::Individual | Type::Superuser) =>
{
principal.inner.typ = new_type;
}
@@ -362,7 +382,10 @@ impl ManageDirectory for Store {
if !principal.inner.emails.contains(email) {
if self.rcpt(email).await? {
return Err(DirectoryError::Management(
ManagementError::NotUniqueField(PrincipalField::Emails),
ManagementError::AlreadyExists {
field: PrincipalField::Emails,
value: email.to_string(),
},
));
}
if let Some(domain) = email.split('@').nth(1) {
@@ -434,7 +457,10 @@ impl ManageDirectory for Store {
if !principal.inner.emails.contains(&email) {
if self.rcpt(&email).await? {
return Err(DirectoryError::Management(
ManagementError::NotUniqueField(PrincipalField::Emails),
ManagementError::AlreadyExists {
field: PrincipalField::Emails,
value: email,
},
));
}
if let Some(domain) = email.split('@').nth(1) {
@@ -595,6 +621,7 @@ impl ManageDirectory for Store {
async fn list_accounts(
&self,
start_from: Option<&str>,
typ: Option<Type>,
limit: usize,
) -> crate::Result<Vec<String>> {
let from_key = ValueKey::from(ValueClass::Directory(DirectoryClass::NameToId(
@@ -605,6 +632,42 @@ impl ManageDirectory for Store {
10
])));
let mut results = Vec::with_capacity(limit);
self.iterate(
IterateParams::new(from_key, to_key)
.set_values(typ.is_some())
.ascending(),
|key, value| {
if typ.map_or(true, |t| {
PrincipalIdType::deserialize(value)
.map(|v| v.typ == t)
.unwrap_or(false)
}) {
results.push(
String::from_utf8_lossy(key.get(1..).unwrap_or_default()).into_owned(),
);
}
Ok(limit == 0 || results.len() < limit)
},
)
.await?;
Ok(results)
}
async fn list_domains(
&self,
start_from: Option<&str>,
limit: usize,
) -> crate::Result<Vec<String>> {
let from_key = ValueKey::from(ValueClass::Directory(DirectoryClass::Domain(
start_from.unwrap_or("").as_bytes().to_vec(),
)));
let to_key = ValueKey::from(ValueClass::Directory(DirectoryClass::Domain(vec![
u8::MAX;
10
])));
let mut results = Vec::with_capacity(limit);
self.iterate(
IterateParams::new(from_key, to_key).no_values().ascending(),
+67 -1
View File
@@ -24,13 +24,18 @@
pub mod lookup;
pub mod manage;
use std::slice::Iter;
use std::{fmt::Display, slice::Iter};
use store::{write::key::KeySerializer, Deserialize, Serialize, U32_LEN};
use utils::codec::leb128::Leb128Iterator;
use crate::{Principal, Type};
pub(super) struct PrincipalIdType {
pub account_id: u32,
pub typ: Type,
}
impl Serialize for Principal<u32> {
fn serialize(self) -> Vec<u8> {
(&self).serialize()
@@ -80,6 +85,35 @@ impl Deserialize for Principal<u32> {
}
}
impl Serialize for PrincipalIdType {
fn serialize(self) -> Vec<u8> {
KeySerializer::new(U32_LEN + 1)
.write_leb128(self.account_id)
.write(self.typ as u8)
.finalize()
}
}
impl Deserialize for PrincipalIdType {
fn deserialize(bytes: &[u8]) -> store::Result<Self> {
let mut bytes = bytes.iter();
Ok(PrincipalIdType {
account_id: bytes.next_leb128().ok_or_else(|| {
store::Error::InternalError("Failed to deserialize principal account id".into())
})?,
typ: Type::from_u8(*bytes.next().ok_or_else(|| {
store::Error::InternalError("Failed to deserialize principal id type".into())
})?),
})
}
}
impl PrincipalIdType {
pub fn new(account_id: u32, typ: Type) -> Self {
Self { account_id, typ }
}
}
fn deserialize(bytes: &[u8]) -> Option<Principal<u32>> {
let mut bytes = bytes.iter();
if bytes.next()? != &1 {
@@ -175,6 +209,20 @@ impl PrincipalUpdate {
}
}
impl Display for PrincipalField {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PrincipalField::Name => write!(f, "name"),
PrincipalField::Type => write!(f, "type"),
PrincipalField::Quota => write!(f, "quota"),
PrincipalField::Description => write!(f, "description"),
PrincipalField::Secrets => write!(f, "secrets"),
PrincipalField::Emails => write!(f, "emails"),
PrincipalField::MemberOf => write!(f, "memberOf"),
}
}
}
fn deserialize_string(bytes: &mut Iter<'_, u8>) -> Option<String> {
let len = bytes.next_leb128()?;
let mut string = Vec::with_capacity(len);
@@ -203,6 +251,17 @@ fn deserialize_u32_list(bytes: &mut Iter<'_, u8>) -> Option<Vec<u32>> {
}
impl Type {
pub fn parse(value: &str) -> Option<Self> {
match value {
"individual" | "superuser" => Some(Type::Individual),
"group" => Some(Type::Group),
"resource" => Some(Type::Resource),
"location" => Some(Type::Location),
"list" => Some(Type::List),
_ => None,
}
}
pub fn from_u8(value: u8) -> Self {
match value {
0 => Type::Individual,
@@ -214,4 +273,11 @@ impl Type {
_ => Type::Other,
}
}
pub fn into_base_type(self) -> Self {
match self {
Type::Superuser => Type::Individual,
any => any,
}
}
}
+12 -19
View File
@@ -21,13 +21,11 @@
* for more details.
*/
use std::sync::Arc;
use ldap3::LdapConnSettings;
use store::Store;
use utils::config::{utils::AsKey, Config};
use crate::{cache::CachedDirectory, config::build_pool, Directory, DirectoryOptions};
use crate::core::config::build_pool;
use super::{Bind, LdapConnectionManager, LdapDirectory, LdapFilter, LdapMappings};
@@ -36,7 +34,7 @@ impl LdapDirectory {
config: &Config,
prefix: impl AsKey,
id_store: Option<Store>,
) -> utils::config::Result<Arc<dyn Directory>> {
) -> utils::config::Result<Self> {
let prefix = prefix.as_key();
let bind_dn = if let Some(dn) = config.value((&prefix, "bind.dn")) {
Bind::new(
@@ -52,9 +50,9 @@ impl LdapDirectory {
config.value_require((&prefix, "address"))?.to_string(),
LdapConnSettings::new()
.set_conn_timeout(config.property_or_static((&prefix, "timeout"), "30s")?)
.set_starttls(config.property_or_static((&prefix, "tls"), "false")?)
.set_starttls(config.property_or_static((&prefix, "tls.enable"), "false")?)
.set_no_tls_verify(
config.property_or_static((&prefix, "allow-invalid-certs"), "false")?,
config.property_or_static((&prefix, "tls.allow-invalid-certs"), "false")?,
),
bind_dn,
);
@@ -115,23 +113,18 @@ impl LdapDirectory {
}
let auth_bind =
if config.property_or_static::<bool>((&prefix, "auth-bind.enable"), "false")? {
LdapFilter::from_config(config, (&prefix, "auth-bind.dn"))?.into()
if config.property_or_static::<bool>((&prefix, "bind.auth.enable"), "false")? {
LdapFilter::from_config(config, (&prefix, "bind.auth.dn"))?.into()
} else {
None
};
CachedDirectory::try_from_config(
config,
&prefix,
LdapDirectory {
mappings,
pool: build_pool(config, &prefix, manager)?,
opt: DirectoryOptions::from_config(config, prefix.as_str())?,
auth_bind,
id_store,
},
)
Ok(LdapDirectory {
mappings,
pool: build_pool(config, &prefix, manager)?,
auth_bind,
id_store,
})
}
}
+16 -75
View File
@@ -25,15 +25,12 @@ use ldap3::{Ldap, LdapConnAsync, LdapError, Scope, SearchEntry};
use mail_send::Credentials;
use store::Store;
use crate::{
backend::internal::manage::ManageDirectory, Directory, DirectoryError, Principal, QueryBy, Type,
};
use crate::{backend::internal::manage::ManageDirectory, DirectoryError, Principal, QueryBy, Type};
use super::{LdapDirectory, LdapMappings};
#[async_trait::async_trait]
impl Directory for LdapDirectory {
async fn query(&self, by: QueryBy<'_>) -> crate::Result<Option<Principal<u32>>> {
impl LdapDirectory {
pub async fn query(&self, by: QueryBy<'_>) -> crate::Result<Option<Principal<u32>>> {
let mut conn = self.pool.get().await?;
let mut account_id = None;
let account_name;
@@ -172,44 +169,21 @@ impl Directory for LdapDirectory {
}
}
async fn email_to_ids(&self, address: &str) -> crate::Result<Vec<u32>> {
let mut rs = self
pub async fn email_to_ids(&self, address: &str) -> crate::Result<Vec<u32>> {
let rs = self
.pool
.get()
.await?
.search(
&self.mappings.base_dn,
Scope::Subtree,
&self
.mappings
.filter_email
.build(self.opt.subaddressing.to_subaddress(address).as_ref()),
&self.mappings.filter_email.build(address.as_ref()),
&self.mappings.attr_name,
)
.await?
.success()
.map(|(rs, _res)| rs)?;
if rs.is_empty() {
if let Some(address) = self.opt.catch_all.to_catch_all(address) {
rs = self
.pool
.get()
.await?
.search(
&self.mappings.base_dn,
Scope::Subtree,
&self.mappings.filter_email.build(address.as_ref()),
&self.mappings.attr_name,
)
.await?
.success()
.map(|(rs, _res)| rs)?;
} else {
return Ok(Vec::new());
}
}
let mut ids = Vec::with_capacity(rs.len());
for entry in rs {
let entry = SearchEntry::construct(entry);
@@ -230,51 +204,24 @@ impl Directory for LdapDirectory {
Ok(ids)
}
async fn rcpt(&self, address: &str) -> crate::Result<bool> {
match self
.pool
pub async fn rcpt(&self, address: &str) -> crate::Result<bool> {
self.pool
.get()
.await?
.streaming_search(
&self.mappings.base_dn,
Scope::Subtree,
&self
.mappings
.filter_email
.build(self.opt.subaddressing.to_subaddress(address).as_ref()),
&self.mappings.filter_email.build(address.as_ref()),
&self.mappings.attr_email_address,
)
.await?
.next()
.await
{
Ok(Some(_)) => Ok(true),
Ok(None) => {
if let Some(address) = self.opt.catch_all.to_catch_all(address) {
self.pool
.get()
.await?
.streaming_search(
&self.mappings.base_dn,
Scope::Subtree,
&self.mappings.filter_email.build(address.as_ref()),
&self.mappings.attr_email_address,
)
.await?
.next()
.await
.map(|entry| entry.is_some())
.map_err(|e| e.into())
} else {
Ok(false)
}
}
Err(e) => Err(e.into()),
}
.map(|entry| entry.is_some())
.map_err(|e| e.into())
}
async fn vrfy(&self, address: &str) -> crate::Result<Vec<String>> {
pub async fn vrfy(&self, address: &str) -> crate::Result<Vec<String>> {
let mut stream = self
.pool
.get()
@@ -282,10 +229,7 @@ impl Directory for LdapDirectory {
.streaming_search(
&self.mappings.base_dn,
Scope::Subtree,
&self
.mappings
.filter_verify
.build(self.opt.subaddressing.to_subaddress(address).as_ref()),
&self.mappings.filter_verify.build(address),
&self.mappings.attr_email_address,
)
.await?;
@@ -307,7 +251,7 @@ impl Directory for LdapDirectory {
Ok(emails)
}
async fn expn(&self, address: &str) -> crate::Result<Vec<String>> {
pub async fn expn(&self, address: &str) -> crate::Result<Vec<String>> {
let mut stream = self
.pool
.get()
@@ -315,10 +259,7 @@ impl Directory for LdapDirectory {
.streaming_search(
&self.mappings.base_dn,
Scope::Subtree,
&self
.mappings
.filter_expand
.build(self.opt.subaddressing.to_subaddress(address).as_ref()),
&self.mappings.filter_expand.build(address),
&self.mappings.attr_email_address,
)
.await?;
@@ -340,7 +281,7 @@ impl Directory for LdapDirectory {
Ok(emails)
}
async fn is_local_domain(&self, domain: &str) -> crate::Result<bool> {
pub async fn is_local_domain(&self, domain: &str) -> crate::Result<bool> {
self.pool
.get()
.await?
-3
View File
@@ -25,8 +25,6 @@ use deadpool::managed::Pool;
use ldap3::{ldap_escape, LdapConnSettings};
use store::Store;
use crate::DirectoryOptions;
pub mod config;
pub mod lookup;
pub mod pool;
@@ -34,7 +32,6 @@ pub mod pool;
pub struct LdapDirectory {
pool: Pool<LdapConnectionManager>,
mappings: LdapMappings,
opt: DirectoryOptions,
auth_bind: Option<LdapFilter>,
id_store: Option<Store>,
}
+22 -21
View File
@@ -21,22 +21,23 @@
* for more details.
*/
use std::sync::Arc;
use ahash::AHashMap;
use store::Store;
use utils::config::{utils::AsKey, Config};
use crate::{Directory, DirectoryOptions, Principal, Type};
use crate::{Principal, Type};
use super::{EmailType, MemoryDirectory};
use super::{EmailType, MemoryDirectory, NameToId};
impl MemoryDirectory {
pub fn from_config(
config: &Config,
prefix: impl AsKey,
) -> utils::config::Result<Arc<dyn Directory>> {
_: Option<Store>,
) -> utils::config::Result<Self> {
let prefix = prefix.as_key();
let mut directory = MemoryDirectory {
opt: DirectoryOptions::from_config(config, prefix.clone())?,
names_to_ids: NameToId::Internal(AHashMap::new()),
..Default::default()
};
@@ -45,31 +46,31 @@ impl MemoryDirectory {
.value_require((prefix.as_str(), "principals", lookup_id, "name"))?
.to_string();
let typ =
match config.value_require((prefix.as_str(), "principals", lookup_id, "name"))? {
match config.value_require((prefix.as_str(), "principals", lookup_id, "type"))? {
"individual" => Type::Individual,
"admin" => Type::Superuser,
"group" => Type::Group,
_ => Type::Other,
_ => Type::Individual,
};
// Obtain id
let next_user_id = directory.names_to_ids.len() as u32;
let id = *directory
.names_to_ids
.entry(name.to_string())
.or_insert(next_user_id);
let id = directory.names_to_ids.get_or_insert(&name).map_err(|err| {
format!(
"Failed to obtain id for principal {} ({}): {:?}",
name, lookup_id, err
)
})?;
// Obtain group ids
let mut member_of = Vec::new();
for (_, group) in config.values((prefix.as_str(), "principals", lookup_id, "member-of"))
{
let next_group_id = directory.names_to_ids.len() as u32;
member_of.push(
*directory
.names_to_ids
.entry(group.to_string())
.or_insert(next_group_id),
);
member_of.push(directory.names_to_ids.get_or_insert(group).map_err(|err| {
format!(
"Failed to obtain id for principal {} ({}): {:?}",
name, lookup_id, err
)
})?);
}
// Parse email addresses
@@ -128,6 +129,6 @@ impl MemoryDirectory {
});
}
Ok(Arc::new(directory))
Ok(directory)
}
}
+12 -32
View File
@@ -23,13 +23,12 @@
use mail_send::Credentials;
use crate::{Directory, Principal, QueryBy};
use crate::{Principal, QueryBy};
use super::{EmailType, MemoryDirectory};
#[async_trait::async_trait]
impl Directory for MemoryDirectory {
async fn query(&self, by: QueryBy<'_>) -> crate::Result<Option<Principal<u32>>> {
impl MemoryDirectory {
pub async fn query(&self, by: QueryBy<'_>) -> crate::Result<Option<Principal<u32>>> {
match by {
QueryBy::Name(name) => {
for principal in &self.principals {
@@ -66,16 +65,10 @@ impl Directory for MemoryDirectory {
Ok(None)
}
async fn email_to_ids(&self, address: &str) -> crate::Result<Vec<u32>> {
pub async fn email_to_ids(&self, address: &str) -> crate::Result<Vec<u32>> {
Ok(self
.emails_to_ids
.get(self.opt.subaddressing.to_subaddress(address).as_ref())
.or_else(|| {
self.opt
.catch_all
.to_catch_all(address)
.and_then(|address| self.emails_to_ids.get(address.as_ref()))
})
.get(address)
.map(|names| {
names
.iter()
@@ -89,37 +82,24 @@ impl Directory for MemoryDirectory {
.unwrap_or_default())
}
async fn rcpt(&self, address: &str) -> crate::Result<bool> {
Ok(self
.emails_to_ids
.contains_key(self.opt.subaddressing.to_subaddress(address).as_ref())
|| self
.opt
.catch_all
.to_catch_all(address)
.map_or(false, |address| {
self.emails_to_ids.contains_key(address.as_ref())
}))
pub async fn rcpt(&self, address: &str) -> crate::Result<bool> {
Ok(self.emails_to_ids.contains_key(address))
}
async fn vrfy(&self, address: &str) -> crate::Result<Vec<String>> {
pub async fn vrfy(&self, address: &str) -> crate::Result<Vec<String>> {
let mut result = Vec::new();
let address = self.opt.subaddressing.to_subaddress(address);
for (key, value) in &self.emails_to_ids {
if key.contains(address.as_ref())
&& value.iter().any(|t| matches!(t, EmailType::Primary(_)))
{
if key.contains(address) && value.iter().any(|t| matches!(t, EmailType::Primary(_))) {
result.push(key.clone())
}
}
Ok(result)
}
async fn expn(&self, address: &str) -> crate::Result<Vec<String>> {
pub async fn expn(&self, address: &str) -> crate::Result<Vec<String>> {
let mut result = Vec::new();
let address = self.opt.subaddressing.to_subaddress(address);
for (key, value) in &self.emails_to_ids {
if key == address.as_ref() {
if key == address {
for item in value {
if let EmailType::List(uid) = item {
for principal in &self.principals {
@@ -137,7 +117,7 @@ impl Directory for MemoryDirectory {
Ok(result)
}
async fn is_local_domain(&self, domain: &str) -> crate::Result<bool> {
pub async fn is_local_domain(&self, domain: &str) -> crate::Result<bool> {
Ok(self.domains.contains(domain))
}
}
+58 -3
View File
@@ -22,8 +22,12 @@
*/
use ahash::{AHashMap, AHashSet};
use store::Store;
use tokio::sync::oneshot;
use crate::{DirectoryOptions, Principal};
use crate::Principal;
use super::internal::manage::ManageDirectory;
pub mod config;
pub mod lookup;
@@ -32,9 +36,60 @@ pub mod lookup;
pub struct MemoryDirectory {
principals: Vec<Principal<u32>>,
emails_to_ids: AHashMap<String, Vec<EmailType>>,
names_to_ids: AHashMap<String, u32>,
names_to_ids: NameToId,
domains: AHashSet<String>,
opt: DirectoryOptions,
}
pub enum NameToId {
Internal(AHashMap<String, u32>),
Store(Store),
}
impl Default for NameToId {
fn default() -> Self {
Self::Internal(AHashMap::new())
}
}
impl std::fmt::Debug for NameToId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Internal(arg0) => f.debug_tuple("Internal").field(arg0).finish(),
Self::Store(_) => f.debug_tuple("Store").finish(),
}
}
}
impl From<Option<Store>> for NameToId {
fn from(store: Option<Store>) -> Self {
match store {
Some(store) => Self::Store(store),
None => Self::Internal(AHashMap::new()),
}
}
}
impl NameToId {
pub fn get_or_insert(&mut self, name: &str) -> crate::Result<u32> {
match self {
Self::Internal(map) => {
let next_id = map.len() as u32;
Ok(*map.entry(name.to_string()).or_insert(next_id))
}
Self::Store(store) => {
let (tx, rx) = oneshot::channel();
let store = store.clone();
let name = name.to_string();
tokio::spawn(async move {
let _ = tx.send(store.get_or_create_account_id(&name).await);
});
match rx.blocking_recv() {
Ok(result) => result,
Err(_) => Err(crate::DirectoryError::Unsupported),
}
}
}
}
}
#[derive(Debug)]
+9 -15
View File
@@ -21,12 +21,10 @@
* for more details.
*/
use std::sync::Arc;
use mail_send::{smtp::tls::build_tls_connector, SmtpClientBuilder};
use utils::config::{utils::AsKey, Config};
use crate::{cache::CachedDirectory, config::build_pool, Directory};
use crate::core::config::build_pool;
use super::{SmtpConnectionManager, SmtpDirectory};
@@ -35,7 +33,7 @@ impl SmtpDirectory {
config: &Config,
prefix: impl AsKey,
is_lmtp: bool,
) -> utils::config::Result<Arc<dyn Directory>> {
) -> utils::config::Result<Self> {
let prefix = prefix.as_key();
let address = config.value_require((&prefix, "address"))?;
let tls_implicit: bool = config.property_or_static((&prefix, "tls.implicit"), "false")?;
@@ -62,16 +60,12 @@ impl SmtpDirectory {
max_auth_errors: config.property_or_static((&prefix, "limits.auth-errors"), "3")?,
};
CachedDirectory::try_from_config(
config,
&prefix,
SmtpDirectory {
pool: build_pool(config, &prefix, manager)?,
domains: config
.values((&prefix, "local-domains"))
.map(|(_, v)| v.to_lowercase())
.collect(),
},
)
Ok(SmtpDirectory {
pool: build_pool(config, &prefix, manager)?,
domains: config
.values((&prefix, "lookup.domains"))
.map(|(_, v)| v.to_lowercase())
.collect(),
})
}
}
+8 -9
View File
@@ -24,13 +24,12 @@
use mail_send::{smtp::AssertReply, Credentials};
use smtp_proto::Severity;
use crate::{Directory, DirectoryError, Principal, QueryBy};
use crate::{DirectoryError, Principal, QueryBy};
use super::{SmtpClient, SmtpDirectory};
#[async_trait::async_trait]
impl Directory for SmtpDirectory {
async fn query(&self, query: QueryBy<'_>) -> crate::Result<Option<Principal<u32>>> {
impl SmtpDirectory {
pub async fn query(&self, query: QueryBy<'_>) -> crate::Result<Option<Principal<u32>>> {
if let QueryBy::Credentials(credentials) = query {
self.pool.get().await?.authenticate(credentials).await
} else {
@@ -38,11 +37,11 @@ impl Directory for SmtpDirectory {
}
}
async fn email_to_ids(&self, _address: &str) -> crate::Result<Vec<u32>> {
pub async fn email_to_ids(&self, _address: &str) -> crate::Result<Vec<u32>> {
Err(DirectoryError::unsupported("smtp", "email_to_ids"))
}
async fn rcpt(&self, address: &str) -> crate::Result<bool> {
pub async fn rcpt(&self, address: &str) -> crate::Result<bool> {
let mut conn = self.pool.get().await?;
if !conn.sent_mail_from {
conn.client
@@ -70,7 +69,7 @@ impl Directory for SmtpDirectory {
}
}
async fn vrfy(&self, address: &str) -> crate::Result<Vec<String>> {
pub async fn vrfy(&self, address: &str) -> crate::Result<Vec<String>> {
self.pool
.get()
.await?
@@ -78,7 +77,7 @@ impl Directory for SmtpDirectory {
.await
}
async fn expn(&self, address: &str) -> crate::Result<Vec<String>> {
pub async fn expn(&self, address: &str) -> crate::Result<Vec<String>> {
self.pool
.get()
.await?
@@ -86,7 +85,7 @@ impl Directory for SmtpDirectory {
.await
}
async fn is_local_domain(&self, domain: &str) -> crate::Result<bool> {
pub async fn is_local_domain(&self, domain: &str) -> crate::Result<bool> {
Ok(self.domains.contains(domain))
}
}
+6 -15
View File
@@ -21,13 +21,9 @@
* for more details.
*/
use std::sync::Arc;
use store::{Store, Stores};
use utils::config::{utils::AsKey, Config};
use crate::{cache::CachedDirectory, Directory, DirectoryOptions};
use super::{SqlDirectory, SqlMappings};
impl SqlDirectory {
@@ -36,7 +32,7 @@ impl SqlDirectory {
prefix: impl AsKey,
stores: &Stores,
id_store: Option<Store>,
) -> utils::config::Result<Arc<dyn Directory>> {
) -> utils::config::Result<Self> {
let prefix = prefix.as_key();
let store_id = config.value_require((&prefix, "store"))?;
let store = stores
@@ -81,15 +77,10 @@ impl SqlDirectory {
}
}
CachedDirectory::try_from_config(
config,
&prefix,
SqlDirectory {
store,
mappings,
opt: DirectoryOptions::from_config(config, prefix.as_str())?,
id_store,
},
)
Ok(SqlDirectory {
store,
mappings,
id_store,
})
}
}
+16 -64
View File
@@ -24,13 +24,12 @@
use mail_send::Credentials;
use store::{NamedRows, Rows, Store, Value};
use crate::{backend::internal::manage::ManageDirectory, Directory, Principal, QueryBy, Type};
use crate::{backend::internal::manage::ManageDirectory, Principal, QueryBy, Type};
use super::{SqlDirectory, SqlMappings};
#[async_trait::async_trait]
impl Directory for SqlDirectory {
async fn query(&self, by: QueryBy<'_>) -> crate::Result<Option<Principal<u32>>> {
impl SqlDirectory {
pub async fn query(&self, by: QueryBy<'_>) -> crate::Result<Option<Principal<u32>>> {
let mut account_id = None;
let account_name;
let mut secret = None;
@@ -143,31 +142,12 @@ impl Directory for SqlDirectory {
Ok(Some(principal))
}
async fn email_to_ids(&self, address: &str) -> crate::Result<Vec<u32>> {
let mut names = self
pub async fn email_to_ids(&self, address: &str) -> crate::Result<Vec<u32>> {
let names = self
.store
.query::<Rows>(
&self.mappings.query_recipients,
vec![self
.opt
.subaddressing
.to_subaddress(address)
.into_owned()
.into()],
)
.query::<Rows>(&self.mappings.query_recipients, vec![address.into()])
.await?;
if names.rows.is_empty() {
if let Some(address) = self.opt.catch_all.to_catch_all(address) {
names = self
.store
.query::<Rows>(&self.mappings.query_recipients, vec![address.into()])
.await?;
} else {
return Ok(vec![]);
}
}
let mut ids = Vec::with_capacity(names.rows.len());
for row in names.rows {
@@ -183,67 +163,39 @@ impl Directory for SqlDirectory {
Ok(ids)
}
async fn rcpt(&self, address: &str) -> crate::Result<bool> {
if self
.store
pub async fn rcpt(&self, address: &str) -> crate::Result<bool> {
self.store
.query::<bool>(
&self.mappings.query_recipients,
vec![self
.opt
.subaddressing
.to_subaddress(address)
.into_owned()
.into()],
vec![address.to_string().into()],
)
.await?
{
Ok(true)
} else if let Some(address) = self.opt.catch_all.to_catch_all(address) {
self.store
.query::<bool>(
&self.mappings.query_recipients,
vec![address.into_owned().into()],
)
.await
.map_err(Into::into)
} else {
Ok(false)
}
.await
.map_err(Into::into)
}
async fn vrfy(&self, address: &str) -> crate::Result<Vec<String>> {
pub async fn vrfy(&self, address: &str) -> crate::Result<Vec<String>> {
self.store
.query::<Rows>(
&self.mappings.query_verify,
vec![self
.opt
.subaddressing
.to_subaddress(address)
.into_owned()
.into()],
vec![address.to_string().into()],
)
.await
.map(Into::into)
.map_err(Into::into)
}
async fn expn(&self, address: &str) -> crate::Result<Vec<String>> {
pub async fn expn(&self, address: &str) -> crate::Result<Vec<String>> {
self.store
.query::<Rows>(
&self.mappings.query_expand,
vec![self
.opt
.subaddressing
.to_subaddress(address)
.into_owned()
.into()],
vec![address.to_string().into()],
)
.await
.map(Into::into)
.map_err(Into::into)
}
async fn is_local_domain(&self, domain: &str) -> crate::Result<bool> {
pub async fn is_local_domain(&self, domain: &str) -> crate::Result<bool> {
self.store
.query::<bool>(&self.mappings.query_domains, vec![domain.into()])
.await
-3
View File
@@ -23,15 +23,12 @@
use store::{LookupStore, Store};
use crate::DirectoryOptions;
pub mod config;
pub mod lookup;
pub struct SqlDirectory {
store: LookupStore,
mappings: SqlMappings,
opt: DirectoryOptions,
id_store: Option<Store>,
}
-64
View File
@@ -1,64 +0,0 @@
/*
* Copyright (c) 2023 Stalwart Labs Ltd.
*
* This file is part of Stalwart Mail Server.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* in the LICENSE file at the top-level directory of this distribution.
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* You can be released from the requirements of the AGPLv3 license by
* purchasing a commercial license. Please contact licensing@stalw.art
* for more details.
*/
use std::{sync::Arc, time::Duration};
use parking_lot::lock_api::Mutex;
use utils::config::Config;
use crate::Directory;
use super::{lru::LookupCache, CachedDirectory};
impl<T: Directory + 'static> CachedDirectory<T> {
pub fn try_from_config(
config: &Config,
prefix: &str,
inner: T,
) -> utils::config::Result<Arc<dyn Directory>> {
if let Some(cached_entries) = config.property((prefix, "cache.entries"))? {
let cache_ttl_positive = config
.property((prefix, "cache.ttl.positive"))?
.unwrap_or(Duration::from_secs(86400));
let cache_ttl_negative = config
.property((prefix, "cache.ttl.positive"))?
.unwrap_or_else(|| Duration::from_secs(3600));
Ok(Arc::new(CachedDirectory {
inner,
cached_domains: Mutex::new(LookupCache::new(
cached_entries,
cache_ttl_positive,
cache_ttl_negative,
)),
cached_rcpts: Mutex::new(LookupCache::new(
cached_entries,
cache_ttl_positive,
cache_ttl_negative,
)),
}))
} else {
Ok(Arc::new(inner))
}
}
}
-75
View File
@@ -1,75 +0,0 @@
/*
* Copyright (c) 2023 Stalwart Labs Ltd.
*
* This file is part of Stalwart Mail Server.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* in the LICENSE file at the top-level directory of this distribution.
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* You can be released from the requirements of the AGPLv3 license by
* purchasing a commercial license. Please contact licensing@stalw.art
* for more details.
*/
use crate::{Directory, Principal, QueryBy};
use super::CachedDirectory;
#[async_trait::async_trait]
impl<T: Directory> Directory for CachedDirectory<T> {
async fn query(&self, by: QueryBy<'_>) -> crate::Result<Option<Principal<u32>>> {
self.inner.query(by).await
}
async fn email_to_ids(&self, address: &str) -> crate::Result<Vec<u32>> {
self.inner.email_to_ids(address).await
}
async fn rcpt(&self, address: &str) -> crate::Result<bool> {
if let Some(result) = {
let result = self.cached_rcpts.lock().get(address);
result
} {
Ok(result)
} else if self.inner.rcpt(address).await? {
self.cached_rcpts.lock().insert_pos(address.to_string());
Ok(true)
} else {
self.cached_rcpts.lock().insert_neg(address.to_string());
Ok(false)
}
}
async fn vrfy(&self, address: &str) -> crate::Result<Vec<String>> {
self.inner.vrfy(address).await
}
async fn expn(&self, address: &str) -> crate::Result<Vec<String>> {
self.inner.expn(address).await
}
async fn is_local_domain(&self, domain: &str) -> crate::Result<bool> {
if let Some(result) = {
let result = self.cached_domains.lock().get(domain);
result
} {
Ok(result)
} else if self.inner.is_local_domain(domain).await? {
self.cached_domains.lock().insert_pos(domain.to_string());
Ok(true)
} else {
self.cached_domains.lock().insert_neg(domain.to_string());
Ok(false)
}
}
}
@@ -27,6 +27,14 @@ use std::{
time::{Duration, Instant},
};
use parking_lot::Mutex;
use utils::config::{utils::AsKey, Config};
pub struct CachedDirectory {
cached_domains: Mutex<LookupCache<String>>,
cached_rcpts: Mutex<LookupCache<String>>,
}
#[allow(clippy::type_complexity)]
#[derive(Debug)]
pub struct LookupCache<T: Hash + Eq> {
@@ -36,6 +44,62 @@ pub struct LookupCache<T: Hash + Eq> {
ttl_neg: Duration,
}
impl CachedDirectory {
pub fn try_from_config(
config: &Config,
prefix: impl AsKey,
) -> utils::config::Result<Option<Self>> {
let prefix = prefix.as_key();
if let Some(cached_entries) = config.property((&prefix, "cache.entries"))? {
let cache_ttl_positive = config
.property((&prefix, "cache.ttl.positive"))?
.unwrap_or(Duration::from_secs(86400));
let cache_ttl_negative = config
.property((&prefix, "cache.ttl.positive"))?
.unwrap_or_else(|| Duration::from_secs(3600));
Ok(Some(CachedDirectory {
cached_domains: Mutex::new(LookupCache::new(
cached_entries,
cache_ttl_positive,
cache_ttl_negative,
)),
cached_rcpts: Mutex::new(LookupCache::new(
cached_entries,
cache_ttl_positive,
cache_ttl_negative,
)),
}))
} else {
Ok(None)
}
}
pub fn get_rcpt(&self, address: &str) -> Option<bool> {
self.cached_rcpts.lock().get(address)
}
pub fn set_rcpt(&self, address: &str, exists: bool) {
if exists {
self.cached_rcpts.lock().insert_pos(address.to_string());
} else {
self.cached_rcpts.lock().insert_neg(address.to_string());
}
}
pub fn get_domain(&self, domain: &str) -> Option<bool> {
self.cached_domains.lock().get(domain)
}
pub fn set_domain(&self, domain: &str, exists: bool) {
if exists {
self.cached_domains.lock().insert_pos(domain.to_string());
} else {
self.cached_domains.lock().insert_neg(domain.to_string());
}
}
}
impl<T: Hash + Eq> LookupCache<T> {
pub fn new(capacity: usize, ttl_pos: Duration, ttl_neg: Duration) -> Self {
Self {
@@ -26,7 +26,7 @@ use deadpool::{
Runtime,
};
use regex::Regex;
use std::time::Duration;
use std::{sync::Arc, time::Duration};
use store::Stores;
use utils::config::{
utils::{AsKey, ParseValue},
@@ -40,9 +40,11 @@ use crate::{
imap::ImapDirectory, ldap::LdapDirectory, memory::MemoryDirectory, smtp::SmtpDirectory,
sql::SqlDirectory,
},
AddressMapping, Directories, DirectoryOptions,
AddressMapping, Directories, Directory, DirectoryInner,
};
use super::cache::CachedDirectory;
pub trait ConfigDirectory {
fn parse_directory(
&self,
@@ -66,35 +68,65 @@ impl ConfigDirectory for Config {
// Parse directory
let protocol = self.value_require(("directory", id, "type"))?;
let prefix = ("directory", id);
let directory = match protocol {
"ldap" => LdapDirectory::from_config(self, prefix, id_store.clone())?,
"sql" => SqlDirectory::from_config(self, prefix, stores, id_store.clone())?,
"imap" => ImapDirectory::from_config(self, prefix)?,
"smtp" => SmtpDirectory::from_config(self, prefix, false)?,
"lmtp" => SmtpDirectory::from_config(self, prefix, true)?,
"memory" => MemoryDirectory::from_config(self, prefix)?,
let store = match protocol {
"internal" => DirectoryInner::Internal(
stores
.stores
.get(self.value_require(("directory", id, "store"))?)
.cloned()
.ok_or_else(|| {
format!(
"Failed to find store {:?} for directory {:?}.",
self.value_require(("directory", id, "store")).unwrap(),
id
)
})?,
),
"ldap" => DirectoryInner::Ldap(LdapDirectory::from_config(
self,
prefix,
id_store.clone(),
)?),
"sql" => DirectoryInner::Sql(SqlDirectory::from_config(
self,
prefix,
stores,
id_store.clone(),
)?),
"imap" => DirectoryInner::Imap(ImapDirectory::from_config(self, prefix)?),
"smtp" => DirectoryInner::Smtp(SmtpDirectory::from_config(self, prefix, false)?),
"lmtp" => DirectoryInner::Smtp(SmtpDirectory::from_config(self, prefix, true)?),
"memory" => DirectoryInner::Memory(MemoryDirectory::from_config(
self,
prefix,
id_store.clone(),
)?),
unknown => {
return Err(format!("Unknown directory type: {unknown:?}"));
}
};
config.directories.insert(id.to_string(), directory);
config.directories.insert(
id.to_string(),
Arc::new(Directory {
store,
catch_all: AddressMapping::from_config(
self,
("directory", id, "options.catch-all"),
)?,
subaddressing: AddressMapping::from_config(
self,
("directory", id, "options.subaddressing"),
)?,
cache: CachedDirectory::try_from_config(self, ("directory", id))?,
}),
);
}
Ok(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: AddressMapping::from_config(config, (&key, "options.catch-all"))?,
subaddressing: AddressMapping::from_config(config, (&key, "options.subaddressing"))?,
})
}
}
impl AddressMapping {
pub fn from_config(config: &Config, key: impl AsKey) -> utils::config::Result<Self> {
let key = key.as_key();
@@ -134,11 +166,11 @@ pub(crate) fn build_pool<M: Manager>(
.max_size(config.property_or_static((prefix, "pool.max-connections"), "10")?)
.create_timeout(
config
.property_or_static::<Duration>((prefix, "pool.create-timeout"), "30s")?
.property_or_static::<Duration>((prefix, "pool.timeout.create"), "30s")?
.into(),
)
.wait_timeout(config.property_or_static((prefix, "pool.wait-timeout"), "30s")?)
.recycle_timeout(config.property_or_static((prefix, "pool.recycle-timeout"), "30s")?)
.wait_timeout(config.property_or_static((prefix, "pool.timeout.wait"), "30s")?)
.recycle_timeout(config.property_or_static((prefix, "pool.timeout.recycle"), "30s")?)
.build()
.map_err(|err| {
format!(
+160
View File
@@ -0,0 +1,160 @@
/*
* Copyright (c) 2023 Stalwart Labs Ltd.
*
* This file is part of Stalwart Mail Server.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* in the LICENSE file at the top-level directory of this distribution.
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* You can be released from the requirements of the AGPLv3 license by
* purchasing a commercial license. Please contact licensing@stalw.art
* for more details.
*/
use crate::{
backend::internal::lookup::DirectoryStore, Directory, DirectoryInner, Principal, QueryBy,
};
impl Directory {
pub async fn query(&self, by: QueryBy<'_>) -> crate::Result<Option<Principal<u32>>> {
match &self.store {
DirectoryInner::Internal(store) => store.query(by).await,
DirectoryInner::Ldap(store) => store.query(by).await,
DirectoryInner::Sql(store) => store.query(by).await,
DirectoryInner::Imap(store) => store.query(by).await,
DirectoryInner::Smtp(store) => store.query(by).await,
DirectoryInner::Memory(store) => store.query(by).await,
}
}
pub async fn email_to_ids(&self, email: &str) -> crate::Result<Vec<u32>> {
let mut address = self.subaddressing.to_subaddress(email);
for _ in 0..2 {
let result = match &self.store {
DirectoryInner::Internal(store) => store.email_to_ids(address.as_ref()).await,
DirectoryInner::Ldap(store) => store.email_to_ids(address.as_ref()).await,
DirectoryInner::Sql(store) => store.email_to_ids(address.as_ref()).await,
DirectoryInner::Imap(store) => store.email_to_ids(address.as_ref()).await,
DirectoryInner::Smtp(store) => store.email_to_ids(address.as_ref()).await,
DirectoryInner::Memory(store) => store.email_to_ids(address.as_ref()).await,
}?;
if !result.is_empty() {
return Ok(result);
} else if let Some(catch_all) = self.catch_all.to_catch_all(email) {
address = catch_all;
} else {
break;
}
}
Ok(vec![])
}
pub async fn is_local_domain(&self, domain: &str) -> crate::Result<bool> {
// Check cache
if let Some(cache) = &self.cache {
if let Some(result) = cache.get_domain(domain) {
return Ok(result);
}
}
let result = match &self.store {
DirectoryInner::Internal(store) => store.is_local_domain(domain).await,
DirectoryInner::Ldap(store) => store.is_local_domain(domain).await,
DirectoryInner::Sql(store) => store.is_local_domain(domain).await,
DirectoryInner::Imap(store) => store.is_local_domain(domain).await,
DirectoryInner::Smtp(store) => store.is_local_domain(domain).await,
DirectoryInner::Memory(store) => store.is_local_domain(domain).await,
}?;
// Update cache
if let Some(cache) = &self.cache {
cache.set_domain(domain, result);
}
Ok(result)
}
pub async fn rcpt(&self, email: &str) -> crate::Result<bool> {
// Expand subaddress
let mut address = self.subaddressing.to_subaddress(email);
// Check cache
if let Some(cache) = &self.cache {
if let Some(result) = cache.get_rcpt(address.as_ref()) {
return Ok(result);
}
}
for _ in 0..2 {
let result = match &self.store {
DirectoryInner::Internal(store) => store.rcpt(address.as_ref()).await,
DirectoryInner::Ldap(store) => store.rcpt(address.as_ref()).await,
DirectoryInner::Sql(store) => store.rcpt(address.as_ref()).await,
DirectoryInner::Imap(store) => store.rcpt(address.as_ref()).await,
DirectoryInner::Smtp(store) => store.rcpt(address.as_ref()).await,
DirectoryInner::Memory(store) => store.rcpt(address.as_ref()).await,
}?;
if result {
// Update cache
if let Some(cache) = &self.cache {
cache.set_rcpt(address.as_ref(), true);
}
return Ok(true);
} else if let Some(catch_all) = self.catch_all.to_catch_all(email) {
// Check cache
if let Some(cache) = &self.cache {
if let Some(result) = cache.get_rcpt(catch_all.as_ref()) {
return Ok(result);
}
}
address = catch_all;
} else {
break;
}
}
// Update cache
if let Some(cache) = &self.cache {
cache.set_rcpt(address.as_ref(), false);
}
Ok(false)
}
pub async fn vrfy(&self, address: &str) -> crate::Result<Vec<String>> {
let address = self.subaddressing.to_subaddress(address);
match &self.store {
DirectoryInner::Internal(store) => store.vrfy(address.as_ref()).await,
DirectoryInner::Ldap(store) => store.vrfy(address.as_ref()).await,
DirectoryInner::Sql(store) => store.vrfy(address.as_ref()).await,
DirectoryInner::Imap(store) => store.vrfy(address.as_ref()).await,
DirectoryInner::Smtp(store) => store.vrfy(address.as_ref()).await,
DirectoryInner::Memory(store) => store.vrfy(address.as_ref()).await,
}
}
pub async fn expn(&self, address: &str) -> crate::Result<Vec<String>> {
let address = self.subaddressing.to_subaddress(address);
match &self.store {
DirectoryInner::Internal(store) => store.expn(address.as_ref()).await,
DirectoryInner::Ldap(store) => store.expn(address.as_ref()).await,
DirectoryInner::Sql(store) => store.expn(address.as_ref()).await,
DirectoryInner::Imap(store) => store.expn(address.as_ref()).await,
DirectoryInner::Smtp(store) => store.expn(address.as_ref()).await,
DirectoryInner::Memory(store) => store.expn(address.as_ref()).await,
}
}
}
@@ -21,18 +21,20 @@
* for more details.
*/
use parking_lot::Mutex;
use crate::Directory;
use self::lru::LookupCache;
use crate::{backend::memory::MemoryDirectory, AddressMapping, Directory, DirectoryInner};
pub mod cache;
pub mod config;
pub mod lookup;
pub mod lru;
pub mod dispatch;
pub mod secret;
pub struct CachedDirectory<T: Directory> {
inner: T,
cached_domains: Mutex<LookupCache<String>>,
cached_rcpts: Mutex<LookupCache<String>>,
impl Default for Directory {
fn default() -> Self {
Directory {
store: DirectoryInner::Memory(MemoryDirectory::default()),
catch_all: AddressMapping::Disable,
subaddressing: AddressMapping::Disable,
cache: None,
}
}
}
+33 -24
View File
@@ -21,19 +21,33 @@
* for more details.
*/
use core::cache::CachedDirectory;
use std::{borrow::Cow, fmt::Debug, sync::Arc};
use ahash::AHashMap;
use backend::{imap::ImapError, internal::PrincipalField};
use backend::{
imap::{ImapDirectory, ImapError},
internal::PrincipalField,
ldap::LdapDirectory,
memory::MemoryDirectory,
smtp::SmtpDirectory,
sql::SqlDirectory,
};
use deadpool::managed::PoolError;
use ldap3::LdapError;
use mail_send::Credentials;
use store::Store;
use utils::config::DynValue;
pub mod backend;
pub mod cache;
pub mod config;
pub mod secret;
pub mod core;
pub struct Directory {
store: DirectoryInner,
catch_all: AddressMapping,
subaddressing: AddressMapping,
cache: Option<CachedDirectory>,
}
#[derive(Debug, Default, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct Principal<T> {
@@ -44,7 +58,7 @@ pub struct Principal<T> {
#[serde(default)]
pub quota: u32,
pub name: String,
#[serde(default, skip_serializing)]
#[serde(default)]
pub secrets: Vec<String>,
#[serde(default)]
pub emails: Vec<String>,
@@ -58,6 +72,7 @@ pub struct Principal<T> {
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum Type {
#[serde(rename = "individual")]
#[default]
Individual = 0,
#[serde(rename = "group")]
Group = 1,
@@ -69,7 +84,6 @@ pub enum Type {
Superuser = 4,
#[serde(rename = "list")]
List = 5,
#[default]
#[serde(rename = "other")]
Other = 6,
}
@@ -89,19 +103,20 @@ pub enum DirectoryError {
#[derive(Debug, PartialEq, Eq)]
pub enum ManagementError {
MissingField(PrincipalField),
NotUniqueField(PrincipalField),
AlreadyExists {
field: PrincipalField,
value: String,
},
NotFound(String),
}
#[async_trait::async_trait]
pub trait Directory: Sync + Send {
async fn query(&self, by: QueryBy<'_>) -> Result<Option<Principal<u32>>>;
async fn email_to_ids(&self, email: &str) -> Result<Vec<u32>>;
async fn is_local_domain(&self, domain: &str) -> crate::Result<bool>;
async fn rcpt(&self, address: &str) -> crate::Result<bool>;
async fn vrfy(&self, address: &str) -> Result<Vec<String>>;
async fn expn(&self, address: &str) -> Result<Vec<String>>;
pub enum DirectoryInner {
Internal(Store),
Ldap(LdapDirectory),
Sql(SqlDirectory),
Imap(ImapDirectory),
Smtp(SmtpDirectory),
Memory(MemoryDirectory),
}
pub enum QueryBy<'x> {
@@ -124,7 +139,7 @@ impl<T: serde::Serialize + serde::de::DeserializeOwned> Principal<T> {
}
}
impl Debug for dyn Directory {
impl Debug for Directory {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Directory").finish()
}
@@ -143,12 +158,6 @@ impl Type {
}
}
#[derive(Debug, Default)]
struct DirectoryOptions {
catch_all: AddressMapping,
subaddressing: AddressMapping,
}
#[derive(Debug, Default)]
pub enum AddressMapping {
Enable,
@@ -162,7 +171,7 @@ pub enum AddressMapping {
#[derive(Default, Clone, Debug)]
pub struct Directories {
pub directories: AHashMap<String, Arc<dyn Directory>>,
pub directories: AHashMap<String, Arc<Directory>>,
}
pub type Result<T> = std::result::Result<T, DirectoryError>;