mirror of
https://github.com/stalwartlabs/stalwart.git
synced 2026-11-05 08:14:47 +00:00
CLI account management + Directory refactoring
This commit is contained in:
@@ -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(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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>,
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>,
|
||||
}
|
||||
|
||||
|
||||
Vendored
-64
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
-75
@@ -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!(
|
||||
@@ -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
@@ -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>;
|
||||
|
||||
Reference in New Issue
Block a user