Files
Stalwart/crates/directory/src/lib.rs

357 lines
9.6 KiB
Rust

/*
* 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 core::cache::CachedDirectory;
use std::{borrow::Cow, fmt::Debug, sync::Arc};
use ahash::AHashMap;
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 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> {
#[serde(default, skip)]
pub id: u32,
#[serde(rename = "type")]
pub typ: Type,
#[serde(default)]
pub quota: u32,
pub name: String,
#[serde(default)]
pub secrets: Vec<String>,
#[serde(default)]
pub emails: Vec<String>,
#[serde(default)]
#[serde(rename = "memberOf")]
pub member_of: Vec<T>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
}
#[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,
#[serde(rename = "resource")]
Resource = 2,
#[serde(rename = "location")]
Location = 3,
#[serde(rename = "superuser")]
Superuser = 4,
#[serde(rename = "list")]
List = 5,
#[serde(rename = "other")]
Other = 6,
}
#[derive(Debug)]
pub enum DirectoryError {
Ldap(LdapError),
Store(store::Error),
Imap(ImapError),
Smtp(mail_send::Error),
Pool(String),
Management(ManagementError),
TimedOut,
Unsupported,
}
#[derive(Debug, PartialEq, Eq)]
pub enum ManagementError {
MissingField(PrincipalField),
AlreadyExists {
field: PrincipalField,
value: String,
},
NotFound(String),
}
pub enum DirectoryInner {
Internal(Store),
Ldap(LdapDirectory),
Sql(SqlDirectory),
Imap(ImapDirectory),
Smtp(SmtpDirectory),
Memory(MemoryDirectory),
}
pub enum QueryBy<'x> {
Name(&'x str),
Id(u32),
Credentials(&'x Credentials<String>),
}
impl<T: serde::Serialize + serde::de::DeserializeOwned> Principal<T> {
pub fn name(&self) -> &str {
&self.name
}
pub fn has_name(&self) -> bool {
!self.name.is_empty()
}
pub fn description(&self) -> Option<&str> {
self.description.as_deref()
}
}
impl Debug for Directory {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Directory").finish()
}
}
impl Type {
pub fn to_jmap(&self) -> &'static str {
match self {
Self::Individual | Self::Superuser => "individual",
Self::Group => "group",
Self::Resource => "resource",
Self::Location => "location",
Self::Other => "other",
Self::List => "list",
}
}
}
#[derive(Debug, Default)]
pub enum AddressMapping {
Enable,
Custom {
regex: regex::Regex,
mapping: DynValue<String>,
},
#[default]
Disable,
}
#[derive(Default, Clone, Debug)]
pub struct Directories {
pub directories: AHashMap<String, Arc<Directory>>,
pub lookups: AHashMap<String, Lookup>,
}
#[derive(Clone, Debug)]
pub enum Lookup {
DomainExists(Arc<Directory>),
EmailExists(Arc<Directory>),
}
pub type Result<T> = std::result::Result<T, DirectoryError>;
impl From<PoolError<LdapError>> for DirectoryError {
fn from(error: PoolError<LdapError>) -> Self {
match error {
PoolError::Backend(error) => error.into(),
PoolError::Timeout(_) => DirectoryError::timeout("ldap"),
error => DirectoryError::Pool(error.to_string()),
}
}
}
impl From<PoolError<ImapError>> for DirectoryError {
fn from(error: PoolError<ImapError>) -> Self {
match error {
PoolError::Backend(error) => error.into(),
PoolError::Timeout(_) => DirectoryError::timeout("imap"),
error => DirectoryError::Pool(error.to_string()),
}
}
}
impl From<PoolError<mail_send::Error>> for DirectoryError {
fn from(error: PoolError<mail_send::Error>) -> Self {
match error {
PoolError::Backend(error) => error.into(),
PoolError::Timeout(_) => DirectoryError::timeout("smtp"),
error => DirectoryError::Pool(error.to_string()),
}
}
}
impl From<LdapError> for DirectoryError {
fn from(error: LdapError) -> Self {
tracing::warn!(
context = "directory",
event = "error",
protocol = "ldap",
reason = %error,
"LDAP directory error"
);
DirectoryError::Ldap(error)
}
}
impl From<store::Error> for DirectoryError {
fn from(error: store::Error) -> Self {
tracing::warn!(
context = "directory",
event = "error",
protocol = "store",
reason = %error,
"Directory error"
);
DirectoryError::Store(error)
}
}
impl From<ImapError> for DirectoryError {
fn from(error: ImapError) -> Self {
tracing::warn!(
context = "directory",
event = "error",
protocol = "imap",
reason = %error,
"IMAP directory error"
);
DirectoryError::Imap(error)
}
}
impl From<mail_send::Error> for DirectoryError {
fn from(error: mail_send::Error) -> Self {
tracing::warn!(
context = "directory",
event = "error",
protocol = "smtp",
reason = %error,
"SMTP directory error"
);
DirectoryError::Smtp(error)
}
}
impl DirectoryError {
pub fn unsupported(protocol: &str, method: &str) -> Self {
tracing::warn!(
context = "directory",
event = "error",
protocol = protocol,
method = method,
"Method not supported by directory"
);
DirectoryError::Unsupported
}
pub fn timeout(protocol: &str) -> Self {
tracing::warn!(
context = "directory",
event = "error",
protocol = protocol,
"Directory timed out"
);
DirectoryError::TimedOut
}
}
impl AddressMapping {
pub fn to_subaddress<'x, 'y: 'x>(&'x self, address: &'y str) -> Cow<'x, str> {
match self {
AddressMapping::Enable => {
if let Some((local_part, domain_part)) = address.rsplit_once('@') {
if let Some((local_part, _)) = local_part.split_once('+') {
return format!("{}@{}", local_part, domain_part).into();
}
}
}
AddressMapping::Custom { regex, mapping } => {
let mut regex_capture = Vec::new();
for captures in regex.captures_iter(address) {
for capture in captures.iter() {
regex_capture.push(capture.map_or("", |m| m.as_str()).to_string());
}
}
if !regex_capture.is_empty() {
return mapping.apply(regex_capture, &());
}
}
AddressMapping::Disable => (),
}
address.into()
}
pub fn to_catch_all<'x, 'y: 'x>(&'x self, address: &'y str) -> Option<Cow<'x, str>> {
match self {
AddressMapping::Enable => address
.rsplit_once('@')
.map(|(_, domain_part)| format!("@{}", domain_part))
.map(Cow::Owned),
AddressMapping::Custom { regex, mapping } => {
let mut regex_capture = Vec::new();
for captures in regex.captures_iter(address) {
for capture in captures.iter() {
regex_capture.push(capture.map_or("", |m| m.as_str()).to_string());
}
}
if !regex_capture.is_empty() {
Some(mapping.apply(regex_capture, &()))
} else {
None
}
}
AddressMapping::Disable => None,
}
}
}
impl PartialEq for DirectoryError {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::Store(l0), Self::Store(r0)) => l0 == r0,
(Self::Pool(l0), Self::Pool(r0)) => l0 == r0,
(Self::Management(l0), Self::Management(r0)) => l0 == r0,
_ => false,
}
}
}