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

574 lines
15 KiB
Rust

/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::{
hash::{BuildHasher, Hasher},
net::{IpAddr, Ipv4Addr, Ipv6Addr},
sync::{
Arc,
atomic::{AtomicBool, AtomicU8},
},
time::Duration,
};
use ahash::{AHashMap, AHashSet};
use arc_swap::ArcSwap;
use auth::{AccessToken, oauth::config::OAuthConfig, roles::RolePermissions};
use config::{
dav::DavConfig,
imap::ImapConfig,
jmap::settings::JmapConfig,
network::Network,
scripts::Scripting,
smtp::{
SmtpConfig,
resolver::{Policy, Tlsa},
},
spamfilter::{IpResolver, SpamFilterConfig},
storage::Storage,
telemetry::Metrics,
};
use imap_proto::protocol::list::Attribute;
use indexmap::IndexMap;
use ipc::{HousekeeperEvent, QueueEvent, ReportingEvent, StateEvent};
use listener::{asn::AsnGeoLookupData, blocked::Security, tls::AcmeProviders};
use mail_auth::{MX, Txt};
use manager::webadmin::{Resource, WebAdminManager};
use nlp::bayes::{TokenHash, Weights};
use parking_lot::{Mutex, RwLock};
use rustls::sign::CertifiedKey;
use tokio::sync::{Notify, Semaphore, mpsc};
use tokio_rustls::TlsConnector;
use utils::{
bimap::{IdBimap, IdBimapItem},
cache::{Cache, CacheItemWeight, CacheWithTtl},
snowflake::SnowflakeIdGenerator,
};
pub mod addresses;
pub mod auth;
pub mod config;
pub mod core;
pub mod dns;
#[cfg(feature = "enterprise")]
pub mod enterprise;
pub mod expr;
pub mod ipc;
pub mod listener;
pub mod manager;
pub mod scripts;
pub mod sharing;
pub mod storage;
pub mod telemetry;
pub use psl;
pub static USER_AGENT: &str = concat!("Stalwart/", env!("CARGO_PKG_VERSION"),);
pub static DAEMON_NAME: &str = concat!("Stalwart Mail Server v", env!("CARGO_PKG_VERSION"),);
pub const LONG_1D_SLUMBER: Duration = Duration::from_secs(60 * 60 * 24);
pub const LONG_1Y_SLUMBER: Duration = Duration::from_secs(60 * 60 * 24 * 365);
pub const IPC_CHANNEL_BUFFER: usize = 1024;
pub const KV_ACME: u8 = 0;
pub const KV_OAUTH: u8 = 1;
pub const KV_RATE_LIMIT_RCPT: u8 = 2;
pub const KV_RATE_LIMIT_SCAN: u8 = 3;
pub const KV_RATE_LIMIT_LOITER: u8 = 4;
pub const KV_RATE_LIMIT_AUTH: u8 = 5;
pub const KV_RATE_LIMIT_SMTP: u8 = 6;
pub const KV_RATE_LIMIT_CONTACT: u8 = 7;
pub const KV_RATE_LIMIT_HTTP_AUTHENTICATED: u8 = 8;
pub const KV_RATE_LIMIT_HTTP_ANONYMOUS: u8 = 9;
pub const KV_RATE_LIMIT_IMAP: u8 = 10;
pub const KV_TOKEN_REVISION: u8 = 11;
pub const KV_REPUTATION_IP: u8 = 12;
pub const KV_REPUTATION_FROM: u8 = 13;
pub const KV_REPUTATION_DOMAIN: u8 = 14;
pub const KV_REPUTATION_ASN: u8 = 15;
pub const KV_GREYLIST: u8 = 16;
pub const KV_BAYES_MODEL_GLOBAL: u8 = 17;
pub const KV_BAYES_MODEL_USER: u8 = 18;
pub const KV_TRUSTED_REPLY: u8 = 19;
pub const KV_LOCK_PURGE_ACCOUNT: u8 = 20;
pub const KV_LOCK_QUEUE_MESSAGE: u8 = 21;
pub const KV_LOCK_QUEUE_REPORT: u8 = 22;
pub const KV_LOCK_EMAIL_TASK: u8 = 23;
pub const KV_LOCK_HOUSEKEEPER: u8 = 24;
pub const KV_LOCK_DAV: u8 = 25;
pub const KV_SIEVE_ID: u8 = 26;
#[derive(Clone)]
pub struct Server {
pub inner: Arc<Inner>,
pub core: Arc<Core>,
}
pub struct Inner {
pub shared_core: ArcSwap<Core>,
pub data: Data,
pub cache: Caches,
pub ipc: Ipc,
}
pub struct Data {
pub tls_certificates: ArcSwap<AHashMap<String, Arc<CertifiedKey>>>,
pub tls_self_signed_cert: Option<Arc<CertifiedKey>>,
pub blocked_ips: RwLock<AHashSet<IpAddr>>,
pub blocked_ips_version: AtomicU8,
pub asn_geo_data: AsnGeoLookupData,
pub jmap_id_gen: SnowflakeIdGenerator,
pub queue_id_gen: SnowflakeIdGenerator,
pub span_id_gen: SnowflakeIdGenerator,
pub queue_status: AtomicBool,
pub webadmin: WebAdminManager,
pub logos: Mutex<AHashMap<String, Option<Resource<Vec<u8>>>>>,
pub config_version: AtomicU8,
pub smtp_connectors: TlsConnectors,
}
pub struct Caches {
pub access_tokens: Cache<u32, Arc<AccessToken>>,
pub http_auth: Cache<String, HttpAuthCache>,
pub permissions: Cache<u32, Arc<RolePermissions>>,
pub account: Cache<AccountId, Arc<Account>>,
pub mailbox: Cache<MailboxId, Arc<MailboxState>>,
pub threads: Cache<u32, Arc<Threads>>,
pub dav: Cache<DavResourceId, Arc<DavResources>>,
pub bayes: CacheWithTtl<TokenHash, Weights>,
pub dns_txt: CacheWithTtl<String, Txt>,
pub dns_mx: CacheWithTtl<String, Arc<Vec<MX>>>,
pub dns_ptr: CacheWithTtl<IpAddr, Arc<Vec<String>>>,
pub dns_ipv4: CacheWithTtl<String, Arc<Vec<Ipv4Addr>>>,
pub dns_ipv6: CacheWithTtl<String, Arc<Vec<Ipv6Addr>>>,
pub dns_tlsa: CacheWithTtl<String, Arc<Tlsa>>,
pub dbs_mta_sts: CacheWithTtl<String, Arc<Policy>>,
pub dns_rbl: CacheWithTtl<String, Option<Arc<IpResolver>>>,
}
#[derive(Debug, Clone, Default)]
pub struct HttpAuthCache {
pub account_id: u32,
pub revision: u64,
}
pub struct Ipc {
pub state_tx: mpsc::Sender<StateEvent>,
pub housekeeper_tx: mpsc::Sender<HousekeeperEvent>,
pub index_tx: Arc<Notify>,
pub queue_tx: mpsc::Sender<QueueEvent>,
pub report_tx: mpsc::Sender<ReportingEvent>,
pub local_delivery_sm: Arc<Semaphore>,
}
pub struct TlsConnectors {
pub pki_verify: TlsConnector,
pub dummy_verify: TlsConnector,
}
#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
pub struct AccountId {
pub account_id: u32,
pub primary_id: u32,
}
#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
pub struct MailboxId {
pub account_id: u32,
pub mailbox_id: u32,
}
#[derive(Debug, Clone, Default)]
pub struct Account {
pub account_id: u32,
pub prefix: Option<String>,
pub mailbox_names: IndexMap<String, u32>,
pub mailbox_state: AHashMap<u32, Mailbox>,
pub state_email: Option<u64>,
pub state_mailbox: Option<u64>,
pub obj_size: u64,
}
#[derive(Debug, Default, Clone)]
pub struct Mailbox {
pub has_children: bool,
pub is_subscribed: bool,
pub special_use: Option<Attribute>,
pub total_messages: Option<u64>,
pub total_unseen: Option<u64>,
pub total_deleted: Option<u64>,
pub total_deleted_storage: Option<u64>,
pub uid_validity: Option<u64>,
pub uid_next: Option<u64>,
pub size: Option<u64>,
}
#[derive(Debug, Clone, Default)]
pub struct MailboxState {
pub uid_next: u32,
pub uid_validity: u32,
pub uid_max: u32,
pub id_to_imap: AHashMap<u32, ImapId>,
pub uid_to_id: AHashMap<u32, u32>,
pub total_messages: usize,
pub modseq: Option<u64>,
pub next_state: Option<Box<NextMailboxState>>,
pub obj_size: u64,
}
#[derive(Debug, Clone)]
pub struct NextMailboxState {
pub next_state: MailboxState,
pub deletions: Vec<ImapId>,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct ImapId {
pub uid: u32,
pub seqnum: u32,
}
#[derive(Debug, Default)]
pub struct Threads {
pub threads: AHashMap<u32, u32>,
pub modseq: Option<u64>,
}
pub struct NameWrapper(pub String);
#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
pub struct DavResourceId {
pub account_id: u32,
pub collection: u8,
}
#[derive(Debug, Default)]
pub struct DavResources {
pub files: IdBimap<DavResource>,
pub size: u64,
pub modseq: Option<u64>,
}
#[derive(Debug, Default)]
pub struct DavResource {
pub document_id: u32,
pub parent_id: Option<u32>,
pub name: String,
pub size: u32,
pub hierarchy_sequence: u32,
pub is_container: bool,
}
#[derive(Clone, Default)]
pub struct Core {
pub storage: Storage,
pub sieve: Scripting,
pub network: Network,
pub acme: AcmeProviders,
pub oauth: OAuthConfig,
pub smtp: SmtpConfig,
pub jmap: JmapConfig,
pub dav: DavConfig,
pub spam: SpamFilterConfig,
pub imap: ImapConfig,
pub metrics: Metrics,
#[cfg(feature = "enterprise")]
pub enterprise: Option<enterprise::Enterprise>,
}
impl CacheItemWeight for AccountId {
fn weight(&self) -> u64 {
std::mem::size_of::<AccountId>() as u64
}
}
impl CacheItemWeight for MailboxId {
fn weight(&self) -> u64 {
std::mem::size_of::<MailboxId>() as u64
}
}
impl CacheItemWeight for DavResourceId {
fn weight(&self) -> u64 {
std::mem::size_of::<DavResourceId>() as u64
}
}
impl CacheItemWeight for Threads {
fn weight(&self) -> u64 {
((self.threads.len() + 2) * std::mem::size_of::<Threads>()) as u64
}
}
impl CacheItemWeight for MailboxState {
fn weight(&self) -> u64 {
self.obj_size
}
}
impl CacheItemWeight for Account {
fn weight(&self) -> u64 {
self.obj_size
}
}
impl CacheItemWeight for HttpAuthCache {
fn weight(&self) -> u64 {
std::mem::size_of::<HttpAuthCache>() as u64
}
}
impl CacheItemWeight for DavResources {
fn weight(&self) -> u64 {
self.size
}
}
impl MailboxState {
pub fn calculate_weight(&self) -> u64 {
std::mem::size_of::<MailboxState>() as u64
+ (self.id_to_imap.len() * std::mem::size_of::<ImapId>() + std::mem::size_of::<u32>())
as u64
+ (self.uid_to_id.len() * std::mem::size_of::<u64>()) as u64
+ self.next_state.as_ref().map_or(0, |n| {
std::mem::size_of::<NextMailboxState>() as u64
+ (n.deletions.len() * std::mem::size_of::<ImapId>()) as u64
+ n.next_state.calculate_weight()
})
}
}
pub trait IntoString: Sized {
fn into_string(self) -> String;
}
impl IntoString for Vec<u8> {
fn into_string(self) -> String {
String::from_utf8(self)
.unwrap_or_else(|err| String::from_utf8_lossy(err.as_bytes()).into_owned())
}
}
#[derive(Debug, Clone, Eq)]
pub struct ThrottleKey {
pub hash: [u8; 32],
}
impl PartialEq for ThrottleKey {
fn eq(&self, other: &Self) -> bool {
self.hash == other.hash
}
}
impl std::hash::Hash for ThrottleKey {
fn hash<H: Hasher>(&self, state: &mut H) {
self.hash.hash(state);
}
}
impl AsRef<[u8]> for ThrottleKey {
fn as_ref(&self) -> &[u8] {
&self.hash
}
}
#[derive(Default)]
pub struct ThrottleKeyHasher {
hash: u64,
}
impl Hasher for ThrottleKeyHasher {
fn finish(&self) -> u64 {
self.hash
}
fn write(&mut self, bytes: &[u8]) {
debug_assert!(
bytes.len() >= std::mem::size_of::<u64>(),
"ThrottleKeyHasher: input too short {bytes:?}"
);
self.hash = bytes
.get(0..std::mem::size_of::<u64>())
.map_or(0, |b| u64::from_ne_bytes(b.try_into().unwrap()));
}
}
#[derive(Clone, Default)]
pub struct ThrottleKeyHasherBuilder {}
impl BuildHasher for ThrottleKeyHasherBuilder {
type Hasher = ThrottleKeyHasher;
fn build_hasher(&self) -> Self::Hasher {
ThrottleKeyHasher::default()
}
}
#[cfg(feature = "test_mode")]
#[allow(clippy::derivable_impls)]
impl Default for Server {
fn default() -> Self {
Self {
inner: Default::default(),
core: Default::default(),
}
}
}
#[cfg(feature = "test_mode")]
#[allow(clippy::derivable_impls)]
impl Default for Inner {
fn default() -> Self {
Self {
shared_core: Default::default(),
data: Default::default(),
ipc: Default::default(),
cache: Default::default(),
}
}
}
#[cfg(feature = "test_mode")]
#[allow(clippy::derivable_impls)]
impl Default for Caches {
fn default() -> Self {
Self {
access_tokens: Cache::new(1024, 10 * 1024 * 1024),
http_auth: Cache::new(1024, 10 * 1024 * 1024),
permissions: Cache::new(1024, 10 * 1024 * 1024),
account: Cache::new(1024, 10 * 1024 * 1024),
mailbox: Cache::new(1024, 10 * 1024 * 1024),
threads: Cache::new(1024, 10 * 1024 * 1024),
dav: Cache::new(1024, 10 * 1024 * 1024),
bayes: CacheWithTtl::new(1024, 10 * 1024 * 1024),
dns_rbl: CacheWithTtl::new(1024, 10 * 1024 * 1024),
dns_txt: CacheWithTtl::new(1024, 10 * 1024 * 1024),
dns_mx: CacheWithTtl::new(1024, 10 * 1024 * 1024),
dns_ptr: CacheWithTtl::new(1024, 10 * 1024 * 1024),
dns_ipv4: CacheWithTtl::new(1024, 10 * 1024 * 1024),
dns_ipv6: CacheWithTtl::new(1024, 10 * 1024 * 1024),
dns_tlsa: CacheWithTtl::new(1024, 10 * 1024 * 1024),
dbs_mta_sts: CacheWithTtl::new(1024, 10 * 1024 * 1024),
}
}
}
#[cfg(feature = "test_mode")]
impl Default for Ipc {
fn default() -> Self {
Self {
state_tx: mpsc::channel(IPC_CHANNEL_BUFFER).0,
housekeeper_tx: mpsc::channel(IPC_CHANNEL_BUFFER).0,
index_tx: Default::default(),
queue_tx: mpsc::channel(IPC_CHANNEL_BUFFER).0,
report_tx: mpsc::channel(IPC_CHANNEL_BUFFER).0,
local_delivery_sm: Arc::new(Semaphore::new(10)),
}
}
}
pub fn ip_to_bytes(ip: &IpAddr) -> Vec<u8> {
match ip {
IpAddr::V4(ip) => ip.octets().to_vec(),
IpAddr::V6(ip) => ip.octets().to_vec(),
}
}
pub fn ip_to_bytes_prefix(prefix: u8, ip: &IpAddr) -> Vec<u8> {
match ip {
IpAddr::V4(ip) => {
let mut buf = Vec::with_capacity(5);
buf.push(prefix);
buf.extend_from_slice(&ip.octets());
buf
}
IpAddr::V6(ip) => {
let mut buf = Vec::with_capacity(17);
buf.push(prefix);
buf.extend_from_slice(&ip.octets());
buf
}
}
}
impl DavResources {
pub fn subtree(&self, search_path: &str) -> impl Iterator<Item = &DavResource> {
let prefix = format!("{search_path}/");
self.files
.iter()
.filter(move |item| item.name.starts_with(&prefix) || item.name == search_path)
}
pub fn subtree_with_depth(
&self,
search_path: &str,
depth: usize,
) -> impl Iterator<Item = &DavResource> {
let prefix = format!("{search_path}/");
self.files.iter().filter(move |item| {
item.name
.strip_prefix(&prefix)
.is_some_and(|name| name.as_bytes().iter().filter(|&&c| c == b'/').count() <= depth)
|| item.name == search_path
})
}
pub fn tree_with_depth(&self, depth: usize) -> impl Iterator<Item = &DavResource> {
self.files.iter().filter(move |item| {
item.name.as_bytes().iter().filter(|&&c| c == b'/').count() <= depth
})
}
pub fn is_ancestor_of(&self, ancestor: u32, descendant: u32) -> bool {
let ancestor = &self.files.by_id(ancestor).unwrap().name;
let descendant = &self.files.by_id(descendant).unwrap().name;
let prefix = format!("{ancestor}/");
descendant.starts_with(&prefix) || descendant == ancestor
}
}
impl IdBimapItem for DavResource {
fn id(&self) -> &u32 {
&self.document_id
}
fn name(&self) -> &str {
&self.name
}
}
impl std::hash::Hash for DavResource {
fn hash<H: Hasher>(&self, state: &mut H) {
self.document_id.hash(state);
}
}
impl PartialEq for DavResource {
fn eq(&self, other: &Self) -> bool {
self.document_id == other.document_id
}
}
impl Eq for DavResource {}
impl std::borrow::Borrow<u32> for DavResource {
fn borrow(&self) -> &u32 {
&self.document_id
}
}