Core refactoring

This commit is contained in:
mdecimus 2024-09-26 14:49:46 +02:00
parent 24967c1e86
commit ce8182ae07
267 changed files with 5886 additions and 4461 deletions

View file

@ -2,9 +2,6 @@ name: Test
on:
workflow_dispatch:
pull_request:
push:
tags: ["v*.*.*"]
jobs:
style:

115
Cargo.lock generated
View file

@ -352,9 +352,9 @@ dependencies = [
[[package]]
name = "async-trait"
version = "0.1.82"
version = "0.1.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a27b8a3a6e1a44fa4c8baf1f653e4172e81486d4941f2237e20dc2d0cf4ddff1"
checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd"
dependencies = [
"proc-macro2",
"quote",
@ -416,9 +416,9 @@ dependencies = [
[[package]]
name = "axum"
version = "0.7.5"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a6c9af12842a67734c9a2e355436e5d03b22383ed60cf13cd0c18fbfe3dcbcf"
checksum = "8f43644eed690f5374f1af436ecd6aea01cd201f6fbdf0178adaf6907afb2cec"
dependencies = [
"async-trait",
"axum-core",
@ -436,16 +436,16 @@ dependencies = [
"rustversion",
"serde",
"sync_wrapper 1.0.1",
"tower",
"tower 0.5.1",
"tower-layer",
"tower-service",
]
[[package]]
name = "axum-core"
version = "0.4.3"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a15c63fd72d41492dc4f497196f5da1fb04fb7529e631d73630d1b491e47a2e3"
checksum = "5e6b8ba012a258d63c9adfa28b9ddcf66149da6f986c5b5452e629d5ee64bf00"
dependencies = [
"async-trait",
"bytes",
@ -456,7 +456,7 @@ dependencies = [
"mime",
"pin-project-lite",
"rustversion",
"sync_wrapper 0.1.2",
"sync_wrapper 1.0.1",
"tower-layer",
"tower-service",
]
@ -956,9 +956,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.17"
version = "4.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e5a21b8495e732f1b3c364c9949b201ca7bae518c502c80256c96ad79eaf6ac"
checksum = "b0956a43b323ac1afaffc053ed5c4b7c1f1800bacd1683c353aabbb752515dd3"
dependencies = [
"clap_builder",
"clap_derive",
@ -966,9 +966,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.17"
version = "4.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8cf2dd12af7a047ad9d6da2b6b249759a22a7abc0f474c1dae1777afa4b21a73"
checksum = "4d72166dd41634086d5803a47eb71ae740e61d84709c36f3c34110173db3961b"
dependencies = [
"anstream",
"anstyle",
@ -978,9 +978,9 @@ dependencies = [
[[package]]
name = "clap_derive"
version = "4.5.13"
version = "4.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0"
checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab"
dependencies = [
"heck 0.5.0",
"proc-macro2",
@ -1043,6 +1043,7 @@ dependencies = [
"base64 0.22.1",
"bincode",
"chrono",
"dashmap",
"decancer",
"directory",
"dns-update",
@ -1051,6 +1052,7 @@ dependencies = [
"hyper 1.4.1",
"idna 1.0.2",
"imagesize",
"imap_proto",
"infer",
"jmap_proto",
"libc",
@ -2765,9 +2767,9 @@ dependencies = [
[[package]]
name = "hyper-util"
version = "0.1.8"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da62f120a8a37763efb0cf8fdf264b884c7b8b9ac8660b900c8661030c00e6ba"
checksum = "41296eb09f183ac68eec06e03cdbea2e759633d4067b2f6552fc2e009bcad08b"
dependencies = [
"bytes",
"futures-channel",
@ -2778,7 +2780,6 @@ dependencies = [
"pin-project-lite",
"socket2",
"tokio",
"tower",
"tower-service",
"tracing",
]
@ -3223,7 +3224,7 @@ dependencies = [
"nlp",
"p256",
"pkcs8",
"quick-xml 0.36.1",
"quick-xml 0.36.2",
"rand",
"rasn",
"rasn-cms",
@ -3416,9 +3417,9 @@ dependencies = [
[[package]]
name = "libc"
version = "0.2.158"
version = "0.2.159"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439"
checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5"
[[package]]
name = "libloading"
@ -3581,7 +3582,7 @@ dependencies = [
"mail-builder",
"mail-parser",
"parking_lot",
"quick-xml 0.36.1",
"quick-xml 0.36.2",
"rand",
"ring 0.17.8",
"rsa",
@ -4471,9 +4472,9 @@ dependencies = [
[[package]]
name = "pkg-config"
version = "0.3.30"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec"
checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2"
[[package]]
name = "polyval"
@ -4508,9 +4509,9 @@ dependencies = [
[[package]]
name = "portable-atomic"
version = "1.7.0"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da544ee218f0d287a911e9c99a39a8c9bc8fcad3cb8db5959940044ecfc67265"
checksum = "d30538d42559de6b034bc76fd6dd4c38961b1ee5c6c56e3808c50128fdbc22ce"
[[package]]
name = "postgres-protocol"
@ -4672,9 +4673,9 @@ dependencies = [
[[package]]
name = "prost"
version = "0.13.2"
version = "0.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b2ecbe40f08db5c006b5764a2645f7f3f141ce756412ac9e1dd6087e6d32995"
checksum = "7b0487d90e047de87f984913713b85c601c05609aad5b0df4b4573fbf69aa13f"
dependencies = [
"bytes",
"prost-derive",
@ -4682,9 +4683,9 @@ dependencies = [
[[package]]
name = "prost-derive"
version = "0.13.2"
version = "0.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acf0c195eebb4af52c752bec4f52f645da98b6e92077a04110c7f349477ae5ac"
checksum = "e9552f850d5f0964a4e4d0bf306459ac29323ddfbae05e35a7c0d35cb0803cc5"
dependencies = [
"anyhow",
"itertools 0.13.0",
@ -4771,9 +4772,9 @@ dependencies = [
[[package]]
name = "quick-xml"
version = "0.36.1"
version = "0.36.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96a05e2e8efddfa51a84ca47cec303fac86c8541b686d37cac5efc0e094417bc"
checksum = "f7649a7b4df05aed9ea7ec6f628c67c9953a43869b8bc50929569b2999d443fe"
dependencies = [
"memchr",
]
@ -5028,9 +5029,9 @@ dependencies = [
[[package]]
name = "redox_syscall"
version = "0.5.4"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0884ad60e090bf1345b93da0a5de8923c93884cd03f40dfcfddd3b4bee661853"
checksum = "355ae415ccd3a04315d3f8246e86d67689ea74d88d915576e1589a351062a13b"
dependencies = [
"bitflags 2.6.0",
]
@ -5728,9 +5729,9 @@ dependencies = [
[[package]]
name = "security-framework-sys"
version = "2.11.1"
version = "2.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75da29fe9b9b08fe9d6b22b5b4bcbc75d8db3aa31e639aa56bb62e9d46bfceaf"
checksum = "ea4a292869320c0272d7bc55a5a6aafaff59b4f63404a003887b679a2e05b4b6"
dependencies = [
"core-foundation-sys",
"libc",
@ -6026,9 +6027,9 @@ checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
[[package]]
name = "simdutf8"
version = "0.1.4"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f27f6278552951f1f2b8cf9da965d10969b2efdea95a6ec47987ab46edfe263a"
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
[[package]]
name = "siphasher"
@ -6474,18 +6475,18 @@ dependencies = [
[[package]]
name = "thiserror"
version = "1.0.63"
version = "1.0.64"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724"
checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.63"
version = "1.0.64"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261"
checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3"
dependencies = [
"proc-macro2",
"quote",
@ -6714,9 +6715,9 @@ checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41"
[[package]]
name = "toml_edit"
version = "0.22.21"
version = "0.22.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b072cee73c449a636ffd6f32bd8de3a9f7119139aff882f44943ce2986dc5cf"
checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5"
dependencies = [
"indexmap 2.5.0",
"toml_datetime",
@ -6747,7 +6748,7 @@ dependencies = [
"socket2",
"tokio",
"tokio-stream",
"tower",
"tower 0.4.13",
"tower-layer",
"tower-service",
"tracing",
@ -6788,6 +6789,20 @@ dependencies = [
"tracing",
]
[[package]]
name = "tower"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2873938d487c3cfb9aed7546dc9f2711d867c9f90c46b889989a2cb84eba6b4f"
dependencies = [
"futures-core",
"futures-util",
"pin-project-lite",
"sync_wrapper 0.1.2",
"tower-layer",
"tower-service",
]
[[package]]
name = "tower-layer"
version = "0.3.3"
@ -6985,9 +7000,9 @@ checksum = "52ea75f83c0137a9b98608359a5f1af8144876eb67bcb1ce837368e906a9f524"
[[package]]
name = "unicode-script"
version = "0.5.6"
version = "0.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad8d71f5726e5f285a935e9fe8edfd53f0491eb6e9a5774097fdabee7cd8c9cd"
checksum = "9fb421b350c9aff471779e262955939f565ec18b86c15364e6bdf0d662ca7c1f"
[[package]]
name = "unicode-security"
@ -7001,9 +7016,9 @@ dependencies = [
[[package]]
name = "unicode-width"
version = "0.1.13"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d"
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
[[package]]
name = "unicode-xid"
@ -7552,9 +7567,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "winnow"
version = "0.6.18"
version = "0.6.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f"
checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b"
dependencies = [
"memchr",
]

View file

@ -11,6 +11,7 @@ store = { path = "../store" }
trc = { path = "../trc" }
directory = { path = "../directory" }
jmap_proto = { path = "../jmap-proto" }
imap_proto = { path = "../imap-proto" }
sieve-rs = { version = "0.5" }
mail-parser = { version = "0.9", features = ["full_encoding", "ludicrous_mode"] }
mail-builder = { version = "0.3", features = ["ludicrous_mode"] }
@ -59,6 +60,7 @@ zip = "2.1"
pwhash = "1.0.0"
xxhash-rust = { version = "0.8.5", features = ["xxh3"] }
psl = "2"
dashmap = "6.0"
[target.'cfg(unix)'.dependencies]
privdrop = "0.5.3"

View file

@ -14,10 +14,10 @@ use crate::{
expr::{
functions::ResolveVariable, if_block::IfBlock, tokenizer::TokenMap, Variable, V_RECIPIENT,
},
Core,
Server,
};
impl Core {
impl Server {
pub async fn email_to_ids(
&self,
directory: &Directory,
@ -25,6 +25,7 @@ impl Core {
session_id: u64,
) -> trc::Result<Vec<u32>> {
let mut address = self
.core
.smtp
.session
.rcpt
@ -38,6 +39,7 @@ impl Core {
if !result.is_empty() {
return Ok(result);
} else if let Some(catch_all) = self
.core
.smtp
.session
.rcpt
@ -62,6 +64,7 @@ impl Core {
) -> trc::Result<bool> {
// Expand subaddress
let mut address = self
.core
.smtp
.session
.rcpt
@ -73,6 +76,7 @@ impl Core {
if directory.rcpt(address.as_ref()).await? {
return Ok(true);
} else if let Some(catch_all) = self
.core
.smtp
.session
.rcpt
@ -97,7 +101,8 @@ impl Core {
) -> trc::Result<Vec<String>> {
directory
.vrfy(
self.smtp
self.core
.smtp
.session
.rcpt
.subaddressing
@ -116,7 +121,8 @@ impl Core {
) -> trc::Result<Vec<String>> {
directory
.expn(
self.smtp
self.core
.smtp
.session
.rcpt
.subaddressing
@ -170,7 +176,7 @@ impl ResolveVariable for Address<'_> {
impl AddressMapping {
pub async fn to_subaddress<'x, 'y: 'x>(
&'x self,
core: &Core,
core: &Server,
address: &'y str,
session_id: u64,
) -> Cow<'x, str> {
@ -198,7 +204,7 @@ impl AddressMapping {
pub async fn to_catch_all<'x, 'y: 'x>(
&'x self,
core: &Core,
core: &Server,
address: &'y str,
session_id: u64,
) -> Option<Cow<'x, str>> {

View file

@ -25,11 +25,11 @@ use utils::map::{
vec_map::VecMap,
};
use crate::Core;
use crate::Server;
use super::{roles::RolePermissions, AccessToken, ResourceToken, TenantInfo};
impl Core {
impl Server {
pub async fn build_access_token(&self, mut principal: Principal) -> trc::Result<AccessToken> {
let mut role_permissions = RolePermissions::default();
@ -75,8 +75,7 @@ impl Core {
tenant = Some(TenantInfo {
id: tenant_id,
quota: self
.storage
.data
.store()
.query(QueryBy::Id(tenant_id), false)
.await
.caused_by(trc::location!())?
@ -111,12 +110,7 @@ impl Core {
}
pub async fn get_access_token(&self, account_id: u32) -> trc::Result<AccessToken> {
let err = match self
.storage
.directory
.query(QueryBy::Id(account_id), true)
.await
{
let err = match self.directory().query(QueryBy::Id(account_id), true).await {
Ok(Some(principal)) => {
return self
.update_access_token(self.build_access_token(principal).await?)
@ -129,7 +123,7 @@ impl Core {
Err(err) => Err(err),
};
match &self.jmap.fallback_admin {
match &self.core.jmap.fallback_admin {
Some((_, secret)) if account_id == u32::MAX => {
self.update_access_token(
self.build_access_token(Principal::fallback_admin(secret))
@ -150,8 +144,7 @@ impl Core {
.chain(access_token.member_of.iter().copied())
{
for acl_item in self
.storage
.data
.store()
.acl_query(AclQuery::HasAccess { grant_account_id })
.await
.caused_by(trc::location!())?
@ -191,15 +184,15 @@ impl Core {
}
pub fn cache_access_token(&self, access_token: Arc<AccessToken>) {
self.security.access_tokens.insert_with_ttl(
self.inner.data.access_tokens.insert_with_ttl(
access_token.primary_id(),
access_token,
Instant::now() + self.jmap.session_cache_ttl,
Instant::now() + self.core.jmap.session_cache_ttl,
);
}
pub async fn get_cached_access_token(&self, primary_id: u32) -> trc::Result<Arc<AccessToken>> {
if let Some(access_token) = self.security.access_tokens.get_with_ttl(&primary_id) {
if let Some(access_token) = self.inner.data.access_tokens.get_with_ttl(&primary_id) {
Ok(access_token)
} else {
// Refresh ACL token

View file

@ -13,7 +13,7 @@ use directory::{
};
use trc::AddContext;
use crate::Core;
use crate::Server;
#[derive(Debug, Clone, Default)]
pub struct RolePermissions {
@ -26,14 +26,14 @@ static ADMIN_PERMISSIONS: LazyLock<Arc<RolePermissions>> = LazyLock::new(admin_p
static TENANT_ADMIN_PERMISSIONS: LazyLock<Arc<RolePermissions>> =
LazyLock::new(tenant_admin_permissions);
impl Core {
impl Server {
pub async fn get_role_permissions(&self, role_id: u32) -> trc::Result<Arc<RolePermissions>> {
match role_id {
ROLE_USER => Ok(USER_PERMISSIONS.clone()),
ROLE_ADMIN => Ok(ADMIN_PERMISSIONS.clone()),
ROLE_TENANT_ADMIN => Ok(TENANT_ADMIN_PERMISSIONS.clone()),
role_id => {
if let Some(role_permissions) = self.security.permissions.get(&role_id) {
if let Some(role_permissions) = self.inner.data.permissions.get(&role_id) {
Ok(role_permissions.clone())
} else {
self.build_role_permissions(role_id).await
@ -81,15 +81,14 @@ impl Core {
}
role_id => {
// Try with the cache
if let Some(role_permissions) = self.security.permissions.get(&role_id) {
if let Some(role_permissions) = self.inner.data.permissions.get(&role_id) {
return_permissions.union(role_permissions.as_ref());
} else {
let mut role_permissions = RolePermissions::default();
// Obtain principal
let mut principal = self
.storage
.data
.store()
.query(QueryBy::Id(role_id), true)
.await
.caused_by(trc::location!())?
@ -133,7 +132,8 @@ impl Core {
role_ids = parent_role_ids.into_iter();
} else {
// Cache role
self.security
self.inner
.data
.permissions
.insert(role_id, Arc::new(role_permissions));
}
@ -149,7 +149,8 @@ impl Core {
// Cache role
let return_permissions = Arc::new(return_permissions);
self.security
self.inner
.data
.permissions
.insert(role_id, return_permissions.clone());
Ok(return_permissions)

View file

@ -0,0 +1,163 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::{sync::Arc, time::Duration};
use ahash::{AHashMap, AHashSet, RandomState};
use arc_swap::ArcSwap;
use dashmap::DashMap;
use mail_send::smtp::tls::build_tls_connector;
use nlp::bayes::cache::BayesTokenCache;
use parking_lot::RwLock;
use utils::{
config::Config,
lru_cache::{LruCache, LruCached},
map::ttl_dashmap::{TtlDashMap, TtlMap},
snowflake::SnowflakeIdGenerator,
};
use crate::{
listener::blocked::BlockedIps, manager::webadmin::WebAdminManager, Data,
ThrottleKeyHasherBuilder, TlsConnectors,
};
use super::server::tls::{build_self_signed_cert, parse_certificates};
impl Data {
pub fn parse(config: &mut Config) -> Self {
// Parse certificates
let mut certificates = AHashMap::new();
let mut subject_names = AHashSet::new();
parse_certificates(config, &mut certificates, &mut subject_names);
if subject_names.is_empty() {
subject_names.insert("localhost".to_string());
}
// Parse capacities
let shard_amount = config
.property::<u64>("cache.shard")
.unwrap_or(32)
.next_power_of_two() as usize;
let capacity = config.property("cache.capacity").unwrap_or(100);
// Parse id generator
let id_generator = config
.property::<u64>("cluster.node-id")
.map(SnowflakeIdGenerator::with_node_id)
.unwrap_or_default();
Data {
tls_certificates: ArcSwap::from_pointee(certificates),
tls_self_signed_cert: build_self_signed_cert(
subject_names.into_iter().collect::<Vec<_>>(),
)
.or_else(|err| {
config.new_build_error("certificate.self-signed", err);
build_self_signed_cert(vec!["localhost".to_string()])
})
.ok()
.map(Arc::new),
access_tokens: TtlDashMap::with_capacity(capacity, shard_amount),
http_auth_cache: TtlDashMap::with_capacity(capacity, shard_amount),
blocked_ips: RwLock::new(BlockedIps::parse(config).blocked_ip_addresses),
blocked_ips_version: 0.into(),
permissions: Default::default(),
permissions_version: 0.into(),
jmap_id_gen: id_generator.clone(),
queue_id_gen: id_generator.clone(),
span_id_gen: id_generator,
webadmin: WebAdminManager::new(),
config_version: 0.into(),
jmap_limiter: DashMap::with_capacity_and_hasher_and_shard_amount(
capacity,
RandomState::default(),
shard_amount,
),
imap_limiter: DashMap::with_capacity_and_hasher_and_shard_amount(
capacity,
RandomState::default(),
shard_amount,
),
account_cache: LruCache::with_capacity(
config.property("cache.account.size").unwrap_or(2048),
),
mailbox_cache: LruCache::with_capacity(
config.property("cache.mailbox.size").unwrap_or(2048),
),
threads_cache: LruCache::with_capacity(
config.property("cache.thread.size").unwrap_or(2048),
),
logos: Default::default(),
smtp_session_throttle: DashMap::with_capacity_and_hasher_and_shard_amount(
capacity,
ThrottleKeyHasherBuilder::default(),
shard_amount,
),
smtp_queue_throttle: DashMap::with_capacity_and_hasher_and_shard_amount(
capacity,
ThrottleKeyHasherBuilder::default(),
shard_amount,
),
smtp_connectors: TlsConnectors::default(),
bayes_cache: BayesTokenCache::new(
config
.property_or_default("cache.bayes.capacity", "8192")
.unwrap_or(8192),
config
.property_or_default("cache.bayes.ttl.positive", "1h")
.unwrap_or_else(|| Duration::from_secs(3600)),
config
.property_or_default("cache.bayes.ttl.negative", "1h")
.unwrap_or_else(|| Duration::from_secs(3600)),
),
remote_lists: Default::default(),
}
}
}
impl Default for Data {
fn default() -> Self {
Self {
tls_certificates: Default::default(),
tls_self_signed_cert: Default::default(),
access_tokens: Default::default(),
http_auth_cache: Default::default(),
blocked_ips: Default::default(),
blocked_ips_version: 0.into(),
permissions: Default::default(),
permissions_version: 0.into(),
remote_lists: Default::default(),
jmap_id_gen: Default::default(),
queue_id_gen: Default::default(),
span_id_gen: Default::default(),
webadmin: Default::default(),
config_version: Default::default(),
jmap_limiter: Default::default(),
imap_limiter: Default::default(),
account_cache: LruCache::with_capacity(2048),
mailbox_cache: LruCache::with_capacity(2048),
threads_cache: LruCache::with_capacity(2048),
logos: Default::default(),
smtp_session_throttle: Default::default(),
smtp_queue_throttle: Default::default(),
smtp_connectors: Default::default(),
bayes_cache: BayesTokenCache::new(
8192,
Duration::from_secs(3600),
Duration::from_secs(3600),
),
}
}
}
impl Default for TlsConnectors {
fn default() -> Self {
TlsConnectors {
pki_verify: build_tls_connector(false),
dummy_verify: build_tls_connector(true),
}
}
}

View file

@ -10,13 +10,10 @@ use arc_swap::ArcSwap;
use directory::{Directories, Directory};
use store::{BlobBackend, BlobStore, FtsStore, LookupStore, Store, Stores};
use telemetry::Metrics;
use utils::{
config::Config,
map::ttl_dashmap::{ADashMap, TtlDashMap, TtlMap},
};
use utils::config::Config;
use crate::{
expr::*, listener::tls::TlsManager, manager::config::ConfigManager, Core, Network, Security,
expr::*, listener::tls::AcmeProviders, manager::config::ConfigManager, Core, Network, Security,
};
use self::{
@ -25,6 +22,7 @@ use self::{
};
pub mod imap;
pub mod inner;
pub mod jmap;
pub mod network;
pub mod scripts;
@ -165,18 +163,8 @@ impl Core {
smtp: SmtpConfig::parse(config).await,
jmap: JmapConfig::parse(config),
imap: ImapConfig::parse(config),
tls: TlsManager::parse(config),
acme: AcmeProviders::parse(config),
metrics: Metrics::parse(config),
security: Security {
access_tokens: TtlDashMap::with_capacity(100, 32),
permissions: ADashMap::with_capacity_and_hasher_and_shard_amount(
100,
ahash::RandomState::new(),
32,
),
permissions_version: Default::default(),
logos: Default::default(),
},
storage: Storage {
data,
blob,
@ -194,7 +182,7 @@ impl Core {
}
}
pub fn into_shared(self) -> Arc<ArcSwap<Self>> {
Arc::new(ArcSwap::from_pointee(self))
pub fn into_shared(self) -> ArcSwap<Self> {
ArcSwap::from_pointee(self)
}
}

View file

@ -6,7 +6,6 @@
use crate::{
expr::{if_block::IfBlock, tokenizer::TokenMap},
listener::blocked::{AllowedIps, BlockedIps},
Network,
};
use utils::config::Config;
@ -30,8 +29,7 @@ pub(crate) const HTTP_VARS: &[u32; 11] = &[
impl Default for Network {
fn default() -> Self {
Self {
blocked_ips: Default::default(),
allowed_ips: Default::default(),
security: Default::default(),
node_id: 0,
http_response_url: IfBlock::new::<()>(
"server.http.url",
@ -47,8 +45,7 @@ impl Network {
pub fn parse(config: &mut Config) -> Self {
let mut network = Network {
node_id: config.property("cluster.node-id").unwrap_or_default(),
blocked_ips: BlockedIps::parse(config),
allowed_ips: AllowedIps::parse(config),
security: Security::parse(config),
..Default::default()
};
let token_map = &TokenMap::default().with_variables(HTTP_VARS);

View file

@ -11,8 +11,6 @@ use std::{
};
use ahash::AHashMap;
use nlp::bayes::cache::BayesTokenCache;
use parking_lot::RwLock;
use sieve::{compiler::grammar::Capability, Compiler, Runtime, Sieve};
use store::Stores;
use utils::config::Config;
@ -33,11 +31,6 @@ pub struct Scripting {
pub untrusted_scripts: AHashMap<String, Arc<Sieve>>,
}
pub struct ScriptCache {
pub bayes_cache: BayesTokenCache,
pub remote_lists: RwLock<AHashMap<String, RemoteList>>,
}
#[derive(Clone)]
pub struct RemoteList {
pub entries: HashSet<String>,
@ -364,25 +357,6 @@ impl Scripting {
}
}
impl ScriptCache {
pub fn parse(config: &mut Config) -> Self {
ScriptCache {
bayes_cache: BayesTokenCache::new(
config
.property_or_default("cache.bayes.capacity", "8192")
.unwrap_or(8192),
config
.property_or_default("cache.bayes.ttl.positive", "1h")
.unwrap_or_else(|| Duration::from_secs(3600)),
config
.property_or_default("cache.bayes.ttl.negative", "1h")
.unwrap_or_else(|| Duration::from_secs(3600)),
),
remote_lists: Default::default(),
}
}
}
impl Default for Scripting {
fn default() -> Self {
Scripting {
@ -410,19 +384,6 @@ impl Default for Scripting {
}
}
impl Default for ScriptCache {
fn default() -> Self {
Self {
bayes_cache: BayesTokenCache::new(
8192,
Duration::from_secs(3600),
Duration::from_secs(3600),
),
remote_lists: Default::default(),
}
}
}
impl Clone for Scripting {
fn clone(&self) -> Self {
Self {

View file

@ -23,18 +23,18 @@ use utils::{
use crate::{
listener::{tls::CertificateResolver, TcpAcceptor},
SharedCore,
Inner,
};
use super::{
tls::{TLS12_VERSION, TLS13_VERSION},
Listener, Server, ServerProtocol, Servers,
Listener, Listeners, ServerProtocol, TcpListener,
};
impl Servers {
impl Listeners {
pub fn parse(config: &mut Config) -> Self {
// Parse ACME managers
let mut servers = Servers {
let mut servers = Listeners {
span_id_gen: Arc::new(
config
.property::<u64>("cluster.node-id")
@ -139,7 +139,7 @@ impl Servers {
let _ = socket.set_reuseaddr(true);
}
listeners.push(Listener {
listeners.push(TcpListener {
socket,
addr,
ttl: config
@ -197,7 +197,7 @@ impl Servers {
}
let span_id_gen = self.span_id_gen.clone();
self.servers.push(Server {
self.servers.push(Listener {
max_connections: config
.property_or_else(
("server.listener", id, "max-connections"),
@ -213,8 +213,8 @@ impl Servers {
});
}
pub fn parse_tcp_acceptors(&mut self, config: &mut Config, core: SharedCore) {
let resolver = Arc::new(CertificateResolver::new(core.clone()));
pub fn parse_tcp_acceptors(&mut self, config: &mut Config, inner: Arc<Inner>) {
let resolver = Arc::new(CertificateResolver::new(inner.clone()));
for id_ in config
.sub_keys("server.listener", ".protocol")

View file

@ -17,24 +17,24 @@ pub mod listener;
pub mod tls;
#[derive(Default)]
pub struct Servers {
pub servers: Vec<Server>,
pub struct Listeners {
pub servers: Vec<Listener>,
pub tcp_acceptors: AHashMap<String, TcpAcceptor>,
pub span_id_gen: Arc<SnowflakeIdGenerator>,
}
#[derive(Debug, Default)]
pub struct Server {
pub struct Listener {
pub id: String,
pub protocol: ServerProtocol,
pub listeners: Vec<Listener>,
pub listeners: Vec<TcpListener>,
pub proxy_networks: Vec<IpAddrMask>,
pub max_connections: u64,
pub span_id_gen: Arc<SnowflakeIdGenerator>,
}
#[derive(Debug)]
pub struct Listener {
pub struct TcpListener {
pub socket: TcpSocket,
pub addr: SocketAddr,
pub backlog: Option<u32>,

View file

@ -12,7 +12,6 @@ use std::{
};
use ahash::{AHashMap, AHashSet};
use arc_swap::ArcSwap;
use base64::{engine::general_purpose::STANDARD, Engine};
use dns_update::{providers::rfc2136::DnsAddress, DnsUpdater, TsigAlgorithm};
use rcgen::generate_simple_self_signed;
@ -33,20 +32,15 @@ use x509_parser::{
use crate::listener::{
acme::{directory::LETS_ENCRYPT_PRODUCTION_DIRECTORY, AcmeProvider, ChallengeSettings},
tls::TlsManager,
tls::AcmeProviders,
};
pub static TLS13_VERSION: &[&SupportedProtocolVersion] = &[&TLS13];
pub static TLS12_VERSION: &[&SupportedProtocolVersion] = &[&TLS12];
impl TlsManager {
impl AcmeProviders {
pub fn parse(config: &mut Config) -> Self {
let mut certificates = AHashMap::new();
let mut acme_providers = AHashMap::new();
let mut subject_names = AHashSet::new();
// Parse certificates
parse_certificates(config, &mut certificates, &mut subject_names);
let mut providers = AHashMap::new();
// Parse ACME providers
'outer: for acme_id in config
@ -140,9 +134,6 @@ impl TlsManager {
.property::<bool>(("acme", acme_id, "default"))
.unwrap_or_default();
// Add domains for self-signed certificate
subject_names.extend(domains.iter().cloned());
if !domains.is_empty() {
match AcmeProvider::new(
acme_id.to_string(),
@ -154,7 +145,7 @@ impl TlsManager {
default,
) {
Ok(acme_provider) => {
acme_providers.insert(acme_id.to_string(), acme_provider);
providers.insert(acme_id.to_string(), acme_provider);
}
Err(err) => {
config.new_build_error(format!("acme.{acme_id}"), err.to_string());
@ -163,21 +154,7 @@ impl TlsManager {
}
}
if subject_names.is_empty() {
subject_names.insert("localhost".to_string());
}
TlsManager {
certificates: ArcSwap::from_pointee(certificates),
acme_providers,
self_signed_cert: build_self_signed_cert(subject_names.into_iter().collect::<Vec<_>>())
.or_else(|err| {
config.new_build_error("certificate.self-signed", err);
build_self_signed_cert(vec!["localhost".to_string()])
})
.ok()
.map(Arc::new),
}
AcmeProviders { providers }
}
}

View file

@ -24,7 +24,7 @@ use mail_auth::{
use parking_lot::Mutex;
use utils::config::{utils::ParseValue, Config};
use crate::Core;
use crate::Server;
pub struct Resolvers {
pub dns: Resolver,
@ -307,15 +307,27 @@ impl Policy {
}
}
impl Core {
impl Server {
pub fn build_mta_sts_policy(&self) -> Option<Policy> {
self.smtp.session.mta_sts_policy.clone().and_then(|policy| {
policy.try_build(self.tls.certificates.load().keys().filter(|key| {
!key.starts_with("mta-sts.")
&& !key.starts_with("autoconfig.")
&& !key.starts_with("autodiscover.")
}))
})
self.core
.smtp
.session
.mta_sts_policy
.clone()
.and_then(|policy| {
policy.try_build(
self.inner
.data
.tls_certificates
.load()
.keys()
.filter(|key| {
!key.starts_with("mta-sts.")
&& !key.starts_with("autoconfig.")
&& !key.starts_with("autodiscover.")
}),
)
})
}
}

335
crates/common/src/core.rs Normal file
View file

@ -0,0 +1,335 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::{net::IpAddr, sync::Arc};
use directory::{
backend::internal::manage::ManageDirectory, core::secret::verify_secret_hash, Directory,
Principal, QueryBy, Type,
};
use mail_send::Credentials;
use sieve::Sieve;
use store::{
write::{QueueClass, ValueClass},
BlobStore, FtsStore, IterateParams, LookupStore, Store, ValueKey,
};
use trc::AddContext;
use crate::{
config::smtp::{
auth::{ArcSealer, DkimSigner},
queue::RelayHost,
},
ImapId, Inner, MailboxState, Server,
};
impl Server {
#[inline(always)]
pub fn store(&self) -> &Store {
&self.core.storage.data
}
#[inline(always)]
pub fn blob_store(&self) -> &BlobStore {
&self.core.storage.blob
}
#[inline(always)]
pub fn fts_store(&self) -> &FtsStore {
&self.core.storage.fts
}
#[inline(always)]
pub fn lookup_store(&self) -> &LookupStore {
&self.core.storage.lookup
}
#[inline(always)]
pub fn directory(&self) -> &Directory {
&self.core.storage.directory
}
pub fn get_directory(&self, name: &str) -> Option<&Arc<Directory>> {
self.core.storage.directories.get(name)
}
pub fn get_directory_or_default(&self, name: &str, session_id: u64) -> &Arc<Directory> {
self.core.storage.directories.get(name).unwrap_or_else(|| {
if !name.is_empty() {
trc::event!(
Eval(trc::EvalEvent::DirectoryNotFound),
Id = name.to_string(),
SpanId = session_id,
);
}
&self.core.storage.directory
})
}
pub fn get_lookup_store(&self, name: &str, session_id: u64) -> &LookupStore {
self.core.storage.lookups.get(name).unwrap_or_else(|| {
if !name.is_empty() {
trc::event!(
Eval(trc::EvalEvent::StoreNotFound),
Id = name.to_string(),
SpanId = session_id,
);
}
&self.core.storage.lookup
})
}
pub fn get_arc_sealer(&self, name: &str, session_id: u64) -> Option<&ArcSealer> {
self.core
.smtp
.mail_auth
.sealers
.get(name)
.map(|s| s.as_ref())
.or_else(|| {
trc::event!(
Arc(trc::ArcEvent::SealerNotFound),
Id = name.to_string(),
SpanId = session_id,
);
None
})
}
pub fn get_dkim_signer(&self, name: &str, session_id: u64) -> Option<&DkimSigner> {
self.core
.smtp
.mail_auth
.signers
.get(name)
.map(|s| s.as_ref())
.or_else(|| {
trc::event!(
Dkim(trc::DkimEvent::SignerNotFound),
Id = name.to_string(),
SpanId = session_id,
);
None
})
}
pub fn get_trusted_sieve_script(&self, name: &str, session_id: u64) -> Option<&Arc<Sieve>> {
self.core.sieve.trusted_scripts.get(name).or_else(|| {
trc::event!(
Sieve(trc::SieveEvent::ScriptNotFound),
Id = name.to_string(),
SpanId = session_id,
);
None
})
}
pub fn get_untrusted_sieve_script(&self, name: &str, session_id: u64) -> Option<&Arc<Sieve>> {
self.core.sieve.untrusted_scripts.get(name).or_else(|| {
trc::event!(
Sieve(trc::SieveEvent::ScriptNotFound),
Id = name.to_string(),
SpanId = session_id,
);
None
})
}
pub fn get_relay_host(&self, name: &str, session_id: u64) -> Option<&RelayHost> {
self.core.smtp.queue.relay_hosts.get(name).or_else(|| {
trc::event!(
Smtp(trc::SmtpEvent::RemoteIdNotFound),
Id = name.to_string(),
SpanId = session_id,
);
None
})
}
pub async fn authenticate(
&self,
directory: &Directory,
session_id: u64,
credentials: &Credentials<String>,
remote_ip: IpAddr,
return_member_of: bool,
) -> trc::Result<Principal> {
// First try to authenticate the user against the default directory
let result = match directory
.query(QueryBy::Credentials(credentials), return_member_of)
.await
{
Ok(Some(principal)) => {
trc::event!(
Auth(trc::AuthEvent::Success),
AccountName = credentials.login().to_string(),
AccountId = principal.id(),
SpanId = session_id,
Type = principal.typ().as_str(),
);
return Ok(principal);
}
Ok(None) => Ok(()),
Err(err) => {
if err.matches(trc::EventType::Auth(trc::AuthEvent::MissingTotp)) {
return Err(err);
} else {
Err(err)
}
}
};
// Then check if the credentials match the fallback admin or master user
match (
&self.core.jmap.fallback_admin,
&self.core.jmap.master_user,
credentials,
) {
(Some((fallback_admin, fallback_pass)), _, Credentials::Plain { username, secret })
if username == fallback_admin =>
{
if verify_secret_hash(fallback_pass, secret).await? {
trc::event!(
Auth(trc::AuthEvent::Success),
AccountName = username.clone(),
SpanId = session_id,
);
return Ok(Principal::fallback_admin(fallback_pass));
}
}
(_, Some((master_user, master_pass)), Credentials::Plain { username, secret })
if username.ends_with(master_user) =>
{
if verify_secret_hash(master_pass, secret).await? {
let username = username.strip_suffix(master_user).unwrap();
let username = username.strip_suffix('%').unwrap_or(username);
if let Some(principal) = directory
.query(QueryBy::Name(username), return_member_of)
.await?
{
trc::event!(
Auth(trc::AuthEvent::Success),
AccountName = username.to_string(),
SpanId = session_id,
AccountId = principal.id(),
Type = principal.typ().as_str(),
);
return Ok(principal);
}
}
}
_ => {}
}
if let Err(err) = result {
Err(err)
} else if self.has_auth_fail2ban() {
let login = credentials.login();
if self.is_auth_fail2banned(remote_ip, login).await? {
Err(trc::SecurityEvent::AuthenticationBan
.into_err()
.ctx(trc::Key::RemoteIp, remote_ip)
.ctx(trc::Key::AccountName, login.to_string()))
} else {
Err(trc::AuthEvent::Failed
.ctx(trc::Key::RemoteIp, remote_ip)
.ctx(trc::Key::AccountName, login.to_string()))
}
} else {
Err(trc::AuthEvent::Failed
.ctx(trc::Key::RemoteIp, remote_ip)
.ctx(trc::Key::AccountName, credentials.login().to_string()))
}
}
pub async fn total_queued_messages(&self) -> trc::Result<u64> {
let mut total = 0;
self.store()
.iterate(
IterateParams::new(
ValueKey::from(ValueClass::Queue(QueueClass::Message(0))),
ValueKey::from(ValueClass::Queue(QueueClass::Message(u64::MAX))),
)
.no_values(),
|_, _| {
total += 1;
Ok(true)
},
)
.await
.map(|_| total)
}
pub async fn total_accounts(&self) -> trc::Result<u64> {
self.store()
.count_principals(None, Type::Individual.into(), None)
.await
.caused_by(trc::location!())
}
pub async fn total_domains(&self) -> trc::Result<u64> {
self.store()
.count_principals(None, Type::Domain.into(), None)
.await
.caused_by(trc::location!())
}
}
pub trait BuildServer {
fn build_server(&self) -> Server;
}
impl BuildServer for Arc<Inner> {
fn build_server(&self) -> Server {
Server {
inner: self.clone(),
core: self.shared_core.load_full(),
}
}
}
trait CredentialsUsername {
fn login(&self) -> &str;
}
impl CredentialsUsername for Credentials<String> {
fn login(&self) -> &str {
match self {
Credentials::Plain { username, .. }
| Credentials::XOauth2 { username, .. }
| Credentials::OAuthBearer { token: username } => username,
}
}
}
impl MailboxState {
pub fn map_result_id(&self, document_id: u32, is_uid: bool) -> Option<(u32, ImapId)> {
if let Some(imap_id) = self.id_to_imap.get(&document_id) {
Some((if is_uid { imap_id.uid } else { imap_id.seqnum }, *imap_id))
} else if is_uid {
self.next_state.as_ref().and_then(|s| {
s.next_state
.id_to_imap
.get(&document_id)
.map(|imap_id| (imap_id.uid, *imap_id))
})
} else {
None
}
}
}

View file

@ -20,7 +20,7 @@ use trc::{Collector, MetricType, TelemetryEvent, TOTAL_EVENT_COUNT};
use super::{AlertContent, AlertContentToken, AlertMethod};
use crate::{
expr::{functions::ResolveVariable, Variable},
Core,
Server,
};
use std::fmt::Write;
@ -33,9 +33,9 @@ pub struct AlertMessage {
struct CollectorResolver;
impl Core {
impl Server {
pub async fn process_alerts(&self) -> Option<Vec<AlertMessage>> {
let alerts = &self.enterprise.as_ref()?.metrics_alerts;
let alerts = &self.core.enterprise.as_ref()?.metrics_alerts;
if alerts.is_empty() {
return None;
}

View file

@ -25,7 +25,7 @@ use store::Store;
use trc::{AddContext, EventType, MetricType};
use utils::config::cron::SimpleCron;
use crate::{expr::Expression, manager::webadmin::Resource, Core, HttpLimitResponse};
use crate::{expr::Expression, manager::webadmin::Resource, Core, HttpLimitResponse, Server};
#[derive(Clone)]
pub struct Enterprise {
@ -87,6 +87,14 @@ pub enum AlertContentToken {
}
impl Core {
pub fn is_enterprise_edition(&self) -> bool {
self.enterprise
.as_ref()
.map_or(false, |e| !e.license.is_expired())
}
}
impl Server {
// WARNING: TAMPERING WITH THIS FUNCTION IS STRICTLY PROHIBITED
// Any attempt to modify, bypass, or disable this license validation mechanism
// constitutes a severe violation of the Stalwart Enterprise License Agreement.
@ -96,18 +104,20 @@ impl Core {
// violators to the fullest extent of the law, including but not limited to claims
// for copyright infringement, breach of contract, and fraud.
#[inline]
pub fn is_enterprise_edition(&self) -> bool {
self.enterprise
.as_ref()
.map_or(false, |e| !e.license.is_expired())
self.core.is_enterprise_edition()
}
pub fn licensed_accounts(&self) -> u32 {
self.enterprise.as_ref().map_or(0, |e| e.license.accounts)
self.core
.enterprise
.as_ref()
.map_or(0, |e| e.license.accounts)
}
pub fn log_license_details(&self) {
if let Some(enterprise) = &self.enterprise {
if let Some(enterprise) = &self.core.enterprise {
trc::event!(
Server(trc::ServerEvent::Licensing),
Details = "Stalwart Enterprise Edition license key is valid",
@ -125,15 +135,14 @@ impl Core {
if self.is_enterprise_edition() {
let domain = psl::domain_str(domain).unwrap_or(domain);
let logo = { self.security.logos.lock().get(domain).cloned() };
let logo = { self.inner.data.logos.lock().get(domain).cloned() };
if let Some(logo) = logo {
Ok(logo)
} else {
// Try fetching the logo for the domain
let logo_url = if let Some(mut principal) = self
.storage
.data
.store()
.query(QueryBy::Name(domain), false)
.await
.caused_by(trc::location!())?
@ -146,8 +155,7 @@ impl Core {
logo.into()
} else if let Some(tenant_id) = principal.get_int(PrincipalField::Tenant) {
if let Some(logo) = self
.storage
.data
.store()
.query(QueryBy::Id(tenant_id as u32), false)
.await
.caused_by(trc::location!())?
@ -199,7 +207,8 @@ impl Core {
logo = Resource::new(content_type, contents).into();
}
self.security
self.inner
.data
.logos
.lock()
.insert(domain.to_string(), logo.clone());
@ -212,7 +221,8 @@ impl Core {
}
fn default_logo_url(&self) -> Option<String> {
self.enterprise
self.core
.enterprise
.as_ref()
.and_then(|e| e.logo_url.as_ref().map(|l| l.to_string()))
}

View file

@ -9,7 +9,7 @@ use std::{borrow::Cow, cmp::Ordering, fmt::Display};
use hyper::StatusCode;
use trc::EvalEvent;
use crate::Core;
use crate::Server;
use super::{
functions::{ResolveVariable, FUNCTIONS},
@ -17,7 +17,7 @@ use super::{
BinaryOperator, Constant, Expression, ExpressionItem, UnaryOperator, Variable,
};
impl Core {
impl Server {
pub async fn eval_if<'x, R: TryFrom<Variable<'x>>, V: ResolveVariable>(
&self,
if_block: &'x IfBlock,
@ -123,7 +123,7 @@ impl IfBlock {
pub async fn eval<'x, V: ResolveVariable>(
&'x self,
resolver: &'x V,
core: &Core,
core: &Server,
session_id: u64,
) -> trc::Result<Variable<'x>> {
let mut captures = Vec::new();
@ -152,7 +152,7 @@ impl Expression {
async fn eval<'x, 'y, V: ResolveVariable>(
&'x self,
resolver: &'x V,
core: &Core,
core: &Server,
captures: &'y mut Vec<String>,
session_id: u64,
) -> trc::Result<Variable<'x>> {

View file

@ -4,11 +4,11 @@ use mail_auth::IpLookupStrategy;
use store::{Deserialize, Rows, Value};
use trc::AddContext;
use crate::Core;
use crate::Server;
use super::*;
impl Core {
impl Server {
pub(crate) async fn eval_fnc<'x>(
&self,
fnc_id: u32,
@ -168,7 +168,8 @@ impl Core {
let record_type = arguments.next_as_string();
if record_type.eq_ignore_ascii_case("ip") {
self.smtp
self.core
.smtp
.resolvers
.dns
.ip_lookup(entry.as_ref(), IpLookupStrategy::Ipv4thenIpv6, 10)
@ -182,7 +183,8 @@ impl Core {
.into()
})
} else if record_type.eq_ignore_ascii_case("mx") {
self.smtp
self.core
.smtp
.resolvers
.dns
.mx_lookup(entry.as_ref())
@ -205,7 +207,8 @@ impl Core {
.into()
})
} else if record_type.eq_ignore_ascii_case("txt") {
self.smtp
self.core
.smtp
.resolvers
.dns
.txt_raw_lookup(entry.as_ref())
@ -213,7 +216,8 @@ impl Core {
.map_err(|err| trc::Error::from(err).caused_by(trc::location!()))
.map(|result| Variable::from(String::from_utf8(result).unwrap_or_default()))
} else if record_type.eq_ignore_ascii_case("ptr") {
self.smtp
self.core
.smtp
.resolvers
.dns
.ptr_lookup(entry.parse::<IpAddr>().map_err(|err| {
@ -232,7 +236,8 @@ impl Core {
.into()
})
} else if record_type.eq_ignore_ascii_case("ipv4") {
self.smtp
self.core
.smtp
.resolvers
.dns
.ipv4_lookup(entry.as_ref())
@ -246,7 +251,8 @@ impl Core {
.into()
})
} else if record_type.eq_ignore_ascii_case("ipv6") {
self.smtp
self.core
.smtp
.resolvers
.dns
.ipv6_lookup(entry.as_ref())

View file

@ -14,7 +14,7 @@ pub mod email;
pub mod misc;
pub mod text;
pub trait ResolveVariable {
pub trait ResolveVariable: Sync + Send {
fn resolve_variable(&self, variable: u32) -> Variable<'_>;
}

233
crates/common/src/ipc.rs Normal file
View file

@ -0,0 +1,233 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::{borrow::Cow, sync::Arc, time::Instant};
use ahash::RandomState;
use jmap_proto::types::{state::StateChange, type_state::DataType};
use mail_auth::{
dmarc::Dmarc,
mta_sts::TlsRpt,
report::{tlsrpt::FailureDetails, Record},
};
use store::{BlobStore, LookupStore, Store};
use tokio::sync::{mpsc, oneshot};
use utils::{map::bitmap::Bitmap, BlobHash};
use crate::{
config::smtp::{
report::AggregateFrequency,
resolver::{Policy, Tlsa},
},
listener::limiter::ConcurrencyLimiter,
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DeliveryResult {
Success,
TemporaryFailure {
reason: Cow<'static, str>,
},
PermanentFailure {
code: [u8; 3],
reason: Cow<'static, str>,
},
}
#[derive(Debug)]
pub enum DeliveryEvent {
Ingest {
message: IngestMessage,
result_tx: oneshot::Sender<Vec<DeliveryResult>>,
},
Stop,
}
#[derive(Debug)]
pub struct IngestMessage {
pub sender_address: String,
pub recipients: Vec<String>,
pub message_blob: BlobHash,
pub message_size: usize,
pub session_id: u64,
}
pub enum HousekeeperEvent {
AcmeReschedule {
provider_id: String,
renew_at: Instant,
},
Purge(PurgeType),
ReloadSettings,
Exit,
}
pub enum PurgeType {
Data(Store),
Blobs { store: Store, blob_store: BlobStore },
Lookup(LookupStore),
Account(Option<u32>),
}
#[derive(Debug)]
pub enum StateEvent {
Subscribe {
account_id: u32,
types: Bitmap<DataType>,
tx: mpsc::Sender<StateChange>,
},
Publish {
state_change: StateChange,
},
UpdateSharedAccounts {
account_id: u32,
},
UpdateSubscriptions {
account_id: u32,
subscriptions: Vec<UpdateSubscription>,
},
Stop,
}
#[derive(Debug)]
pub enum UpdateSubscription {
Unverified {
id: u32,
url: String,
code: String,
keys: Option<EncryptionKeys>,
},
Verified(PushSubscription),
}
#[derive(Debug)]
pub struct PushSubscription {
pub id: u32,
pub url: String,
pub expires: u64,
pub types: Bitmap<DataType>,
pub keys: Option<EncryptionKeys>,
}
#[derive(Debug, Clone)]
pub struct EncryptionKeys {
pub p256dh: Vec<u8>,
pub auth: Vec<u8>,
}
#[derive(Debug)]
pub enum QueueEvent {
Reload,
OnHold(OnHold<QueueEventLock>),
Stop,
}
#[derive(Debug)]
pub struct OnHold<T> {
pub next_due: Option<u64>,
pub limiters: Vec<ConcurrencyLimiter>,
pub message: T,
}
#[derive(Debug)]
pub struct QueueEventLock {
pub due: u64,
pub queue_id: u64,
pub lock_expiry: u64,
}
#[derive(Debug)]
pub enum ReportingEvent {
Dmarc(Box<DmarcEvent>),
Tls(Box<TlsEvent>),
Stop,
}
#[derive(Debug)]
pub struct DmarcEvent {
pub domain: String,
pub report_record: Record,
pub dmarc_record: Arc<Dmarc>,
pub interval: AggregateFrequency,
}
#[derive(Debug)]
pub struct TlsEvent {
pub domain: String,
pub policy: PolicyType,
pub failure: Option<FailureDetails>,
pub tls_record: Arc<TlsRpt>,
pub interval: AggregateFrequency,
}
#[derive(Debug, Hash, PartialEq, Eq)]
pub enum PolicyType {
Tlsa(Option<Arc<Tlsa>>),
Sts(Option<Arc<Policy>>),
None,
}
pub trait ToHash {
fn to_hash(&self) -> u64;
}
impl ToHash for Dmarc {
fn to_hash(&self) -> u64 {
RandomState::with_seeds(1, 9, 7, 9).hash_one(self)
}
}
impl ToHash for PolicyType {
fn to_hash(&self) -> u64 {
RandomState::with_seeds(1, 9, 7, 9).hash_one(self)
}
}
impl From<DmarcEvent> for ReportingEvent {
fn from(value: DmarcEvent) -> Self {
ReportingEvent::Dmarc(Box::new(value))
}
}
impl From<TlsEvent> for ReportingEvent {
fn from(value: TlsEvent) -> Self {
ReportingEvent::Tls(Box::new(value))
}
}
impl From<Arc<Tlsa>> for PolicyType {
fn from(value: Arc<Tlsa>) -> Self {
PolicyType::Tlsa(Some(value))
}
}
impl From<Arc<Policy>> for PolicyType {
fn from(value: Arc<Policy>) -> Self {
PolicyType::Sts(Some(value))
}
}
impl From<&Arc<Tlsa>> for PolicyType {
fn from(value: &Arc<Tlsa>) -> Self {
PolicyType::Tlsa(Some(value.clone()))
}
}
impl From<&Arc<Policy>> for PolicyType {
fn from(value: &Arc<Policy>) -> Self {
PolicyType::Sts(Some(value.clone()))
}
}
impl From<(&Option<Arc<Policy>>, &Option<Arc<Tlsa>>)> for PolicyType {
fn from(value: (&Option<Arc<Policy>>, &Option<Arc<Tlsa>>)) -> Self {
match value {
(Some(value), _) => PolicyType::Sts(Some(value.clone())),
(_, Some(value)) => PolicyType::Tlsa(Some(value.clone())),
_ => PolicyType::None,
}
}
}

View file

@ -5,59 +5,52 @@
*/
use std::{
borrow::Cow,
collections::BTreeMap,
hash::{BuildHasher, Hasher},
net::IpAddr,
sync::{atomic::AtomicU8, Arc},
};
use ahash::AHashMap;
use ahash::{AHashMap, AHashSet, RandomState};
use arc_swap::ArcSwap;
use auth::{roles::RolePermissions, AccessToken};
use config::{
imap::ImapConfig,
jmap::settings::JmapConfig,
scripts::Scripting,
smtp::{
auth::{ArcSealer, DkimSigner},
queue::RelayHost,
SmtpConfig,
},
scripts::{RemoteList, Scripting},
smtp::SmtpConfig,
storage::Storage,
telemetry::Metrics,
};
use directory::{
backend::internal::manage::ManageDirectory, core::secret::verify_secret_hash, Directory,
Principal, QueryBy, Type,
};
use dashmap::DashMap;
use expr::if_block::IfBlock;
use futures::StreamExt;
use listener::{
blocked::{AllowedIps, BlockedIps},
tls::TlsManager,
};
use mail_send::Credentials;
use imap_proto::protocol::list::Attribute;
use ipc::{DeliveryEvent, HousekeeperEvent, QueueEvent, ReportingEvent, StateEvent};
use listener::{blocked::Security, limiter::ConcurrencyLimiter, tls::AcmeProviders};
use manager::webadmin::Resource;
use parking_lot::Mutex;
use manager::webadmin::{Resource, WebAdminManager};
use nlp::bayes::cache::BayesTokenCache;
use parking_lot::{Mutex, RwLock};
use reqwest::Response;
use sieve::Sieve;
use store::{
write::{QueueClass, ValueClass},
IterateParams, LookupStore, ValueKey,
};
use tokio::sync::{mpsc, oneshot};
use trc::AddContext;
use rustls::sign::CertifiedKey;
use tokio::sync::{mpsc, Notify};
use tokio_rustls::TlsConnector;
use utils::{
lru_cache::LruCache,
map::ttl_dashmap::{ADashMap, TtlDashMap},
BlobHash,
snowflake::SnowflakeIdGenerator,
};
pub mod addresses;
pub mod auth;
pub mod config;
pub mod core;
#[cfg(feature = "enterprise")]
pub mod enterprise;
pub mod expr;
pub mod ipc;
pub mod listener;
pub mod manager;
pub mod scripts;
@ -70,75 +63,162 @@ pub static DAEMON_NAME: &str = concat!("Stalwart Mail Server v", env!("CARGO_PKG
pub const IPC_CHANNEL_BUFFER: usize = 1024;
pub type SharedCore = Arc<ArcSwap<Core>>;
#[derive(Clone, Default)]
pub struct Server {
pub inner: Arc<Inner>,
pub core: Arc<Core>,
}
#[derive(Default)]
pub struct Inner {
pub shared_core: ArcSwap<Core>,
pub data: Data,
pub ipc: Ipc,
}
pub struct Data {
pub tls_certificates: ArcSwap<AHashMap<String, Arc<CertifiedKey>>>,
pub tls_self_signed_cert: Option<Arc<CertifiedKey>>,
pub access_tokens: TtlDashMap<u32, Arc<AccessToken>>,
pub http_auth_cache: TtlDashMap<String, u32>,
pub blocked_ips: RwLock<AHashSet<IpAddr>>,
pub blocked_ips_version: AtomicU8,
pub permissions: ADashMap<u32, Arc<RolePermissions>>,
pub permissions_version: AtomicU8,
pub bayes_cache: BayesTokenCache,
pub remote_lists: RwLock<AHashMap<String, RemoteList>>,
pub jmap_id_gen: SnowflakeIdGenerator,
pub queue_id_gen: SnowflakeIdGenerator,
pub span_id_gen: SnowflakeIdGenerator,
pub webadmin: WebAdminManager,
pub config_version: AtomicU8,
pub jmap_limiter: DashMap<u32, Arc<ConcurrencyLimiters>, RandomState>,
pub imap_limiter: DashMap<u32, Arc<ConcurrencyLimiters>, RandomState>,
pub account_cache: LruCache<AccountId, Arc<Account>>,
pub mailbox_cache: LruCache<MailboxId, Arc<MailboxState>>,
pub threads_cache: LruCache<u32, Arc<Threads>>,
pub logos: Mutex<AHashMap<String, Option<Resource<Vec<u8>>>>>,
pub smtp_session_throttle: DashMap<ThrottleKey, ConcurrencyLimiter, ThrottleKeyHasherBuilder>,
pub smtp_queue_throttle: DashMap<ThrottleKey, ConcurrencyLimiter, ThrottleKeyHasherBuilder>,
pub smtp_connectors: TlsConnectors,
}
pub struct Ipc {
pub state_tx: mpsc::Sender<StateEvent>,
pub housekeeper_tx: mpsc::Sender<HousekeeperEvent>,
pub delivery_tx: mpsc::Sender<DeliveryEvent>,
pub index_tx: Arc<Notify>,
pub queue_tx: mpsc::Sender<QueueEvent>,
pub report_tx: mpsc::Sender<ReportingEvent>,
}
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: BTreeMap<String, u32>,
pub mailbox_state: AHashMap<u32, Mailbox>,
pub state_email: Option<u64>,
pub state_mailbox: Option<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<u32>,
pub total_unseen: Option<u32>,
pub total_deleted: Option<u32>,
pub uid_validity: Option<u32>,
pub uid_next: Option<u32>,
pub size: Option<u32>,
}
#[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>>,
}
#[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 ConcurrencyLimiters {
pub concurrent_requests: ConcurrencyLimiter,
pub concurrent_uploads: ConcurrencyLimiter,
}
#[derive(Clone, Default)]
pub struct Core {
pub storage: Storage,
pub sieve: Scripting,
pub network: Network,
pub tls: TlsManager,
pub acme: AcmeProviders,
pub smtp: SmtpConfig,
pub jmap: JmapConfig,
pub imap: ImapConfig,
pub metrics: Metrics,
pub security: Security,
#[cfg(feature = "enterprise")]
pub enterprise: Option<enterprise::Enterprise>,
}
//TODO: temporary hack until OIDC is implemented
#[derive(Default)]
pub struct Security {
pub logos: Mutex<AHashMap<String, Option<Resource<Vec<u8>>>>>,
pub access_tokens: TtlDashMap<u32, Arc<AccessToken>>,
pub permissions: ADashMap<u32, Arc<RolePermissions>>,
pub permissions_version: AtomicU8,
}
#[derive(Clone)]
pub struct Network {
pub node_id: u64,
pub blocked_ips: BlockedIps,
pub allowed_ips: AllowedIps,
pub security: Security,
pub http_response_url: IfBlock,
pub http_allowed_endpoint: IfBlock,
}
#[derive(Debug)]
pub enum DeliveryEvent {
Ingest {
message: IngestMessage,
result_tx: oneshot::Sender<Vec<DeliveryResult>>,
},
Stop,
}
pub struct Ipc {
pub delivery_tx: mpsc::Sender<DeliveryEvent>,
}
#[derive(Debug)]
pub struct IngestMessage {
pub sender_address: String,
pub recipients: Vec<String>,
pub message_blob: BlobHash,
pub message_size: usize,
pub session_id: u64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DeliveryResult {
Success,
TemporaryFailure {
reason: Cow<'static, str>,
},
PermanentFailure {
code: [u8; 3],
reason: Cow<'static, str>,
},
}
pub trait IntoString: Sized {
fn into_string(self) -> String;
}
@ -150,271 +230,52 @@ impl IntoString for Vec<u8> {
}
}
impl Core {
pub fn get_directory(&self, name: &str) -> Option<&Arc<Directory>> {
self.storage.directories.get(name)
}
#[derive(Debug, Clone, Eq)]
pub struct ThrottleKey {
pub hash: [u8; 32],
}
pub fn get_directory_or_default(&self, name: &str, session_id: u64) -> &Arc<Directory> {
self.storage.directories.get(name).unwrap_or_else(|| {
if !name.is_empty() {
trc::event!(
Eval(trc::EvalEvent::DirectoryNotFound),
Id = name.to_string(),
SpanId = session_id,
);
}
&self.storage.directory
})
}
pub fn get_lookup_store(&self, name: &str, session_id: u64) -> &LookupStore {
self.storage.lookups.get(name).unwrap_or_else(|| {
if !name.is_empty() {
trc::event!(
Eval(trc::EvalEvent::StoreNotFound),
Id = name.to_string(),
SpanId = session_id,
);
}
&self.storage.lookup
})
}
pub fn get_arc_sealer(&self, name: &str, session_id: u64) -> Option<&ArcSealer> {
self.smtp
.mail_auth
.sealers
.get(name)
.map(|s| s.as_ref())
.or_else(|| {
trc::event!(
Arc(trc::ArcEvent::SealerNotFound),
Id = name.to_string(),
SpanId = session_id,
);
None
})
}
pub fn get_dkim_signer(&self, name: &str, session_id: u64) -> Option<&DkimSigner> {
self.smtp
.mail_auth
.signers
.get(name)
.map(|s| s.as_ref())
.or_else(|| {
trc::event!(
Dkim(trc::DkimEvent::SignerNotFound),
Id = name.to_string(),
SpanId = session_id,
);
None
})
}
pub fn get_trusted_sieve_script(&self, name: &str, session_id: u64) -> Option<&Arc<Sieve>> {
self.sieve.trusted_scripts.get(name).or_else(|| {
trc::event!(
Sieve(trc::SieveEvent::ScriptNotFound),
Id = name.to_string(),
SpanId = session_id,
);
None
})
}
pub fn get_untrusted_sieve_script(&self, name: &str, session_id: u64) -> Option<&Arc<Sieve>> {
self.sieve.untrusted_scripts.get(name).or_else(|| {
trc::event!(
Sieve(trc::SieveEvent::ScriptNotFound),
Id = name.to_string(),
SpanId = session_id,
);
None
})
}
pub fn get_relay_host(&self, name: &str, session_id: u64) -> Option<&RelayHost> {
self.smtp.queue.relay_hosts.get(name).or_else(|| {
trc::event!(
Smtp(trc::SmtpEvent::RemoteIdNotFound),
Id = name.to_string(),
SpanId = session_id,
);
None
})
}
pub async fn authenticate(
&self,
directory: &Directory,
session_id: u64,
credentials: &Credentials<String>,
remote_ip: IpAddr,
return_member_of: bool,
) -> trc::Result<Principal> {
// First try to authenticate the user against the default directory
let result = match directory
.query(QueryBy::Credentials(credentials), return_member_of)
.await
{
Ok(Some(principal)) => {
trc::event!(
Auth(trc::AuthEvent::Success),
AccountName = credentials.login().to_string(),
AccountId = principal.id(),
SpanId = session_id,
Type = principal.typ().as_str(),
);
return Ok(principal);
}
Ok(None) => Ok(()),
Err(err) => {
if err.matches(trc::EventType::Auth(trc::AuthEvent::MissingTotp)) {
return Err(err);
} else {
Err(err)
}
}
};
// Then check if the credentials match the fallback admin or master user
match (
&self.jmap.fallback_admin,
&self.jmap.master_user,
credentials,
) {
(Some((fallback_admin, fallback_pass)), _, Credentials::Plain { username, secret })
if username == fallback_admin =>
{
if verify_secret_hash(fallback_pass, secret).await? {
trc::event!(
Auth(trc::AuthEvent::Success),
AccountName = username.clone(),
SpanId = session_id,
);
return Ok(Principal::fallback_admin(fallback_pass));
}
}
(_, Some((master_user, master_pass)), Credentials::Plain { username, secret })
if username.ends_with(master_user) =>
{
if verify_secret_hash(master_pass, secret).await? {
let username = username.strip_suffix(master_user).unwrap();
let username = username.strip_suffix('%').unwrap_or(username);
if let Some(principal) = directory
.query(QueryBy::Name(username), return_member_of)
.await?
{
trc::event!(
Auth(trc::AuthEvent::Success),
AccountName = username.to_string(),
SpanId = session_id,
AccountId = principal.id(),
Type = principal.typ().as_str(),
);
return Ok(principal);
}
}
}
_ => {}
}
if let Err(err) = result {
Err(err)
} else if self.has_auth_fail2ban() {
let login = credentials.login();
if self.is_auth_fail2banned(remote_ip, login).await? {
Err(trc::SecurityEvent::AuthenticationBan
.into_err()
.ctx(trc::Key::RemoteIp, remote_ip)
.ctx(trc::Key::AccountName, login.to_string()))
} else {
Err(trc::AuthEvent::Failed
.ctx(trc::Key::RemoteIp, remote_ip)
.ctx(trc::Key::AccountName, login.to_string()))
}
} else {
Err(trc::AuthEvent::Failed
.ctx(trc::Key::RemoteIp, remote_ip)
.ctx(trc::Key::AccountName, credentials.login().to_string()))
}
}
pub async fn total_queued_messages(&self) -> trc::Result<u64> {
let mut total = 0;
self.storage
.data
.iterate(
IterateParams::new(
ValueKey::from(ValueClass::Queue(QueueClass::Message(0))),
ValueKey::from(ValueClass::Queue(QueueClass::Message(u64::MAX))),
)
.no_values(),
|_, _| {
total += 1;
Ok(true)
},
)
.await
.map(|_| total)
}
pub async fn total_accounts(&self) -> trc::Result<u64> {
self.storage
.data
.count_principals(None, Type::Individual.into(), None)
.await
.caused_by(trc::location!())
}
pub async fn total_domains(&self) -> trc::Result<u64> {
self.storage
.data
.count_principals(None, Type::Domain.into(), None)
.await
.caused_by(trc::location!())
impl PartialEq for ThrottleKey {
fn eq(&self, other: &Self) -> bool {
self.hash == other.hash
}
}
trait CredentialsUsername {
fn login(&self) -> &str;
}
impl CredentialsUsername for Credentials<String> {
fn login(&self) -> &str {
match self {
Credentials::Plain { username, .. }
| Credentials::XOauth2 { username, .. }
| Credentials::OAuthBearer { token: username } => username,
}
impl std::hash::Hash for ThrottleKey {
fn hash<H: Hasher>(&self, state: &mut H) {
self.hash.hash(state);
}
}
impl Clone for Security {
fn clone(&self) -> Self {
Self {
access_tokens: self.access_tokens.clone(),
permissions: self.permissions.clone(),
permissions_version: AtomicU8::new(
self.permissions_version
.load(std::sync::atomic::Ordering::Relaxed),
),
logos: Mutex::new(self.logos.lock().clone()),
}
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]) {
self.hash = u64::from_ne_bytes((&bytes[..std::mem::size_of::<u64>()]).try_into().unwrap());
}
}
#[derive(Clone, Default)]
pub struct ThrottleKeyHasherBuilder {}
impl BuildHasher for ThrottleKeyHasherBuilder {
type Hasher = ThrottleKeyHasher;
fn build_hasher(&self) -> Self::Hasher {
ThrottleKeyHasher::default()
}
}
@ -448,3 +309,23 @@ impl HttpLimitResponse for Response {
Ok(Some(bytes))
}
}
impl ConcurrencyLimiters {
pub fn is_active(&self) -> bool {
self.concurrent_requests.is_active() || self.concurrent_uploads.is_active()
}
}
#[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,
delivery_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,
}
}
}

View file

@ -8,11 +8,11 @@ use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
use trc::AddContext;
use utils::config::ConfigKey;
use crate::Core;
use crate::Server;
use super::AcmeProvider;
impl Core {
impl Server {
pub(crate) async fn load_cert(&self, provider: &AcmeProvider) -> trc::Result<Option<Vec<u8>>> {
self.read_if_exists(provider, "cert", provider.domains.as_slice())
.await
@ -68,6 +68,7 @@ impl Core {
items: &[String],
) -> trc::Result<Option<Vec<u8>>> {
if let Some(content) = self
.core
.storage
.config
.get(self.build_key(provider, class, items))
@ -94,7 +95,8 @@ impl Core {
items: &[String],
contents: impl AsRef<[u8]>,
) -> trc::Result<()> {
self.storage
self.core
.storage
.config
.set([ConfigKey {
key: self.build_key(provider, class, items),

View file

@ -16,7 +16,7 @@ use arc_swap::ArcSwap;
use dns_update::DnsUpdater;
use rustls::sign::CertifiedKey;
use crate::Core;
use crate::Server;
use self::directory::{Account, ChallengeType};
@ -80,7 +80,7 @@ impl AcmeProvider {
}
}
impl Core {
impl Server {
pub async fn init_acme(&self, provider: &AcmeProvider) -> trc::Result<Duration> {
// Load account key from cache or generate a new one
if let Some(account_key) = self.load_account(provider).await? {
@ -100,15 +100,17 @@ impl Core {
}
pub fn has_acme_tls_providers(&self) -> bool {
self.tls
.acme_providers
self.core
.acme
.providers
.values()
.any(|p| matches!(p.challenge, ChallengeSettings::TlsAlpn01))
}
pub fn has_acme_http_providers(&self) -> bool {
self.tls
.acme_providers
self.core
.acme
.providers
.values()
.any(|p| matches!(p.challenge, ChallengeSettings::Http01))
}

View file

@ -13,12 +13,12 @@ use x509_parser::parse_x509_certificate;
use crate::listener::acme::directory::Identifier;
use crate::listener::acme::ChallengeSettings;
use crate::Core;
use crate::Server;
use super::directory::{Account, AuthStatus, Directory, OrderStatus};
use super::AcmeProvider;
impl Core {
impl Server {
pub(crate) async fn process_cert(
&self,
provider: &AcmeProvider,
@ -210,8 +210,7 @@ impl Core {
match &provider.challenge {
ChallengeSettings::TlsAlpn01 => {
self.storage
.lookup
self.lookup_store()
.key_set(
format!("acme:{domain}").into_bytes(),
account.tls_alpn_key(challenge, domain.clone())?,
@ -220,8 +219,7 @@ impl Core {
.await?;
}
ChallengeSettings::Http01 => {
self.storage
.lookup
self.lookup_store()
.key_set(
format!("acme:{}", challenge.token).into_bytes(),
account.http_proof(challenge)?,
@ -289,7 +287,7 @@ impl Core {
let wait_until = Instant::now() + *propagation_timeout;
let mut did_propagate = false;
while Instant::now() < wait_until {
match self.smtp.resolvers.dns.txt_raw_lookup(&name).await {
match self.core.smtp.resolvers.dns.txt_raw_lookup(&name).await {
Ok(result) => {
let result = std::str::from_utf8(&result).unwrap_or_default();
if result.contains(&dns_proof) {

View file

@ -16,14 +16,14 @@ use rustls_pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer};
use store::write::Bincode;
use trc::AcmeEvent;
use crate::{listener::acme::directory::SerializedCert, Core};
use crate::{listener::acme::directory::SerializedCert, Server};
use super::{directory::ACME_TLS_ALPN_NAME, AcmeProvider, StaticResolver};
impl Core {
impl Server {
pub(crate) fn set_cert(&self, provider: &AcmeProvider, cert: Arc<CertifiedKey>) {
// Add certificates
let mut certificates = self.tls.certificates.load().as_ref().clone();
let mut certificates = self.inner.data.tls_certificates.load().as_ref().clone();
for domain in provider.domains.iter() {
certificates.insert(
domain
@ -39,29 +39,12 @@ impl Core {
certificates.insert("*".to_string(), cert);
}
self.tls.certificates.store(certificates.into());
self.inner.data.tls_certificates.store(certificates.into());
}
}
impl ResolvesServerCert for StaticResolver {
fn resolve(&self, _: ClientHello) -> Option<Arc<CertifiedKey>> {
self.key.clone()
}
}
pub(crate) fn build_acme_static_resolver(key: Option<Arc<CertifiedKey>>) -> Arc<ServerConfig> {
let mut challenge = ServerConfig::builder()
.with_no_client_auth()
.with_cert_resolver(Arc::new(StaticResolver { key }));
challenge.alpn_protocols.push(ACME_TLS_ALPN_NAME.to_vec());
Arc::new(challenge)
}
impl Core {
pub(crate) async fn build_acme_certificate(&self, domain: &str) -> Option<Arc<CertifiedKey>> {
match self
.storage
.lookup
.lookup_store()
.key_get::<Bincode<SerializedCert>>(format!("acme:{domain}").into_bytes())
.await
{
@ -100,6 +83,20 @@ impl Core {
}
}
impl ResolvesServerCert for StaticResolver {
fn resolve(&self, _: ClientHello) -> Option<Arc<CertifiedKey>> {
self.key.clone()
}
}
pub(crate) fn build_acme_static_resolver(key: Option<Arc<CertifiedKey>>) -> Arc<ServerConfig> {
let mut challenge = ServerConfig::builder()
.with_no_client_auth()
.with_cert_resolver(Arc::new(StaticResolver { key }));
challenge.alpn_protocols.push(ACME_TLS_ALPN_NAME.to_vec());
Arc::new(challenge)
}
pub trait IsTlsAlpnChallenge {
fn is_tls_alpn_challenge(&self) -> bool;
}

View file

@ -4,85 +4,45 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::{fmt::Debug, net::IpAddr, sync::atomic::AtomicU8};
use std::{fmt::Debug, net::IpAddr};
use ahash::AHashSet;
use parking_lot::RwLock;
use utils::config::{
ipmask::{IpAddrMask, IpAddrOrMask},
utils::ParseValue,
Config, ConfigKey, Rate,
};
use crate::Core;
use crate::Server;
#[derive(Debug, Clone)]
pub struct Security {
blocked_ip_networks: Vec<IpAddrMask>,
has_blocked_networks: bool,
allowed_ip_addresses: AHashSet<IpAddr>,
allowed_ip_networks: Vec<IpAddrMask>,
has_allowed_networks: bool,
pub struct BlockedIps {
pub ip_addresses: RwLock<AHashSet<IpAddr>>,
pub version: AtomicU8,
ip_networks: Vec<IpAddrMask>,
has_networks: bool,
auth_fail_rate: Option<Rate>,
rcpt_fail_rate: Option<Rate>,
loiter_fail_rate: Option<Rate>,
}
#[derive(Clone)]
pub struct AllowedIps {
ip_addresses: AHashSet<IpAddr>,
ip_networks: Vec<IpAddrMask>,
has_networks: bool,
}
pub const BLOCKED_IP_KEY: &str = "server.blocked-ip";
pub const BLOCKED_IP_PREFIX: &str = "server.blocked-ip.";
pub const ALLOWED_IP_KEY: &str = "server.allowed-ip";
pub const ALLOWED_IP_PREFIX: &str = "server.allowed-ip.";
impl BlockedIps {
pub fn parse(config: &mut Config) -> Self {
let mut ip_addresses = AHashSet::new();
let mut ip_networks = Vec::new();
for ip in config
.set_values(BLOCKED_IP_KEY)
.map(IpAddrOrMask::parse_value)
.collect::<Vec<_>>()
{
match ip {
Ok(IpAddrOrMask::Ip(ip)) => {
ip_addresses.insert(ip);
}
Ok(IpAddrOrMask::Mask(ip)) => {
ip_networks.push(ip);
}
Err(err) => {
config.new_parse_error(BLOCKED_IP_KEY, err);
}
}
}
BlockedIps {
ip_addresses: RwLock::new(ip_addresses),
has_networks: !ip_networks.is_empty(),
ip_networks,
auth_fail_rate: config
.property_or_default::<Option<Rate>>("server.fail2ban.authentication", "100/1d")
.unwrap_or_default(),
rcpt_fail_rate: config
.property_or_default::<Option<Rate>>("server.fail2ban.invalid-rcpt", "35/1d")
.unwrap_or_default(),
loiter_fail_rate: config
.property_or_default::<Option<Rate>>("server.fail2ban.loitering", "150/1d")
.unwrap_or_default(),
version: 0.into(),
}
}
pub struct BlockedIps {
pub blocked_ip_addresses: AHashSet<IpAddr>,
pub blocked_ip_networks: Vec<IpAddrMask>,
}
impl AllowedIps {
impl Security {
pub fn parse(config: &mut Config) -> Self {
let mut ip_addresses = AHashSet::new();
let mut ip_networks = Vec::new();
let mut allowed_ip_addresses = AHashSet::new();
let mut allowed_ip_networks = Vec::new();
for ip in config
.set_values(ALLOWED_IP_KEY)
@ -91,10 +51,10 @@ impl AllowedIps {
{
match ip {
Ok(IpAddrOrMask::Ip(ip)) => {
ip_addresses.insert(ip);
allowed_ip_addresses.insert(ip);
}
Ok(IpAddrOrMask::Mask(ip)) => {
ip_networks.push(ip);
allowed_ip_networks.push(ip);
}
Err(err) => {
config.new_parse_error(ALLOWED_IP_KEY, err);
@ -105,25 +65,37 @@ impl AllowedIps {
#[cfg(not(feature = "test_mode"))]
{
// Add loopback addresses
ip_addresses.insert(IpAddr::V4(std::net::Ipv4Addr::LOCALHOST));
ip_addresses.insert(IpAddr::V6(std::net::Ipv6Addr::LOCALHOST));
allowed_ip_addresses.insert(IpAddr::V4(std::net::Ipv4Addr::LOCALHOST));
allowed_ip_addresses.insert(IpAddr::V6(std::net::Ipv6Addr::LOCALHOST));
}
AllowedIps {
ip_addresses,
has_networks: !ip_networks.is_empty(),
ip_networks,
let blocked = BlockedIps::parse(config);
Security {
has_blocked_networks: !blocked.blocked_ip_networks.is_empty(),
blocked_ip_networks: blocked.blocked_ip_networks,
has_allowed_networks: !allowed_ip_networks.is_empty(),
allowed_ip_addresses,
allowed_ip_networks,
auth_fail_rate: config
.property_or_default::<Option<Rate>>("server.fail2ban.authentication", "100/1d")
.unwrap_or_default(),
rcpt_fail_rate: config
.property_or_default::<Option<Rate>>("server.fail2ban.invalid-rcpt", "35/1d")
.unwrap_or_default(),
loiter_fail_rate: config
.property_or_default::<Option<Rate>>("server.fail2ban.loitering", "150/1d")
.unwrap_or_default(),
}
}
}
impl Core {
impl Server {
pub async fn is_rcpt_fail2banned(&self, ip: IpAddr) -> trc::Result<bool> {
if let Some(rate) = &self.network.blocked_ips.rcpt_fail_rate {
if let Some(rate) = &self.core.network.security.rcpt_fail_rate {
let is_allowed = self.is_ip_allowed(&ip)
|| self
.storage
.lookup
.lookup_store()
.is_rate_allowed(format!("r:{ip}").as_bytes(), rate, false)
.await?
.is_none();
@ -137,11 +109,10 @@ impl Core {
}
pub async fn is_loiter_fail2banned(&self, ip: IpAddr) -> trc::Result<bool> {
if let Some(rate) = &self.network.blocked_ips.loiter_fail_rate {
if let Some(rate) = &self.core.network.security.loiter_fail_rate {
let is_allowed = self.is_ip_allowed(&ip)
|| self
.storage
.lookup
.lookup_store()
.is_rate_allowed(format!("l:{ip}").as_bytes(), rate, false)
.await?
.is_none();
@ -155,17 +126,15 @@ impl Core {
}
pub async fn is_auth_fail2banned(&self, ip: IpAddr, login: &str) -> trc::Result<bool> {
if let Some(rate) = &self.network.blocked_ips.auth_fail_rate {
if let Some(rate) = &self.core.network.security.auth_fail_rate {
let is_allowed = self.is_ip_allowed(&ip)
|| (self
.storage
.lookup
.lookup_store()
.is_rate_allowed(format!("b:{ip}").as_bytes(), rate, false)
.await?
.is_none()
&& self
.storage
.lookup
.lookup_store()
.is_rate_allowed(format!("b:{login}").as_bytes(), rate, false)
.await?
.is_none());
@ -179,10 +148,11 @@ impl Core {
async fn block_ip(&self, ip: IpAddr) -> trc::Result<()> {
// Add IP to blocked list
self.network.blocked_ips.ip_addresses.write().insert(ip);
self.inner.data.blocked_ips.write().insert(ip);
// Write blocked IP to config
self.storage
self.core
.storage
.config
.set([ConfigKey {
key: format!("{}.{}", BLOCKED_IP_KEY, ip),
@ -191,104 +161,96 @@ impl Core {
.await?;
// Increment version
self.network.blocked_ips.increment_version();
self.increment_blocked_version();
Ok(())
}
pub fn has_auth_fail2ban(&self) -> bool {
self.network.blocked_ips.auth_fail_rate.is_some()
self.core.network.security.auth_fail_rate.is_some()
}
pub fn is_ip_blocked(&self, ip: &IpAddr) -> bool {
self.network.blocked_ips.ip_addresses.read().contains(ip)
|| (self.network.blocked_ips.has_networks
self.inner.data.blocked_ips.read().contains(ip)
|| (self.core.network.security.has_blocked_networks
&& self
.core
.network
.blocked_ips
.ip_networks
.security
.blocked_ip_networks
.iter()
.any(|network| network.matches(ip)))
}
pub fn is_ip_allowed(&self, ip: &IpAddr) -> bool {
self.network.allowed_ips.ip_addresses.contains(ip)
|| (self.network.allowed_ips.has_networks
self.core.network.security.allowed_ip_addresses.contains(ip)
|| (self.core.network.security.has_allowed_networks
&& self
.core
.network
.allowed_ips
.ip_networks
.security
.allowed_ip_networks
.iter()
.any(|network| network.matches(ip)))
}
}
impl BlockedIps {
pub fn increment_version(&self) {
self.version
pub fn increment_blocked_version(&self) {
self.inner
.data
.blocked_ips_version
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
}
}
impl Default for BlockedIps {
fn default() -> Self {
impl BlockedIps {
pub fn parse(config: &mut Config) -> Self {
let mut blocked_ip_addresses = AHashSet::new();
let mut blocked_ip_networks = Vec::new();
for ip in config
.set_values(BLOCKED_IP_KEY)
.map(IpAddrOrMask::parse_value)
.collect::<Vec<_>>()
{
match ip {
Ok(IpAddrOrMask::Ip(ip)) => {
blocked_ip_addresses.insert(ip);
}
Ok(IpAddrOrMask::Mask(ip)) => {
blocked_ip_networks.push(ip);
}
Err(err) => {
config.new_parse_error(BLOCKED_IP_KEY, err);
}
}
}
Self {
ip_addresses: RwLock::new(AHashSet::new()),
ip_networks: Default::default(),
has_networks: Default::default(),
version: Default::default(),
blocked_ip_addresses,
blocked_ip_networks,
}
}
}
#[allow(clippy::derivable_impls)]
impl Default for Security {
fn default() -> Self {
// Add IPv4 and IPv6 loopback addresses
Self {
#[cfg(not(feature = "test_mode"))]
allowed_ip_addresses: AHashSet::from_iter([
IpAddr::V4(std::net::Ipv4Addr::LOCALHOST),
IpAddr::V6(std::net::Ipv6Addr::LOCALHOST),
]),
#[cfg(feature = "test_mode")]
allowed_ip_addresses: Default::default(),
allowed_ip_networks: Default::default(),
has_allowed_networks: Default::default(),
blocked_ip_networks: Default::default(),
has_blocked_networks: Default::default(),
auth_fail_rate: Default::default(),
rcpt_fail_rate: Default::default(),
loiter_fail_rate: Default::default(),
}
}
}
#[allow(clippy::derivable_impls)]
impl Default for AllowedIps {
fn default() -> Self {
// Add IPv4 and IPv6 loopback addresses
Self {
#[cfg(not(feature = "test_mode"))]
ip_addresses: AHashSet::from_iter([
IpAddr::V4(std::net::Ipv4Addr::LOCALHOST),
IpAddr::V6(std::net::Ipv6Addr::LOCALHOST),
]),
#[cfg(feature = "test_mode")]
ip_addresses: Default::default(),
ip_networks: Default::default(),
has_networks: Default::default(),
}
}
}
impl Clone for BlockedIps {
fn clone(&self) -> Self {
Self {
ip_addresses: RwLock::new(self.ip_addresses.read().clone()),
ip_networks: self.ip_networks.clone(),
has_networks: self.has_networks,
version: self
.version
.load(std::sync::atomic::Ordering::Relaxed)
.into(),
auth_fail_rate: self.auth_fail_rate.clone(),
rcpt_fail_rate: self.rcpt_fail_rate.clone(),
loiter_fail_rate: self.loiter_fail_rate.clone(),
}
}
}
impl Debug for BlockedIps {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("BlockedIps")
.field("ip_addresses", &self.ip_addresses)
.field("ip_networks", &self.ip_networks)
.field("has_networks", &self.has_networks)
.field("version", &self.version)
.field("auth_fail_rate", &self.auth_fail_rate)
.field("rcpt_fail_rate", &self.rcpt_fail_rate)
.field("loiter_fail_rate", &self.loiter_fail_rate)
.finish()
}
}

View file

@ -10,20 +10,17 @@ use std::{
time::Duration,
};
use arc_swap::ArcSwap;
use proxy_header::io::ProxiedStream;
use rustls::crypto::ring::cipher_suite::TLS13_AES_128_GCM_SHA256;
use tokio::{
net::{TcpListener, TcpStream},
sync::watch,
};
use tokio::{net::TcpStream, sync::watch};
use tokio_rustls::server::TlsStream;
use trc::{EventType, HttpEvent, ImapEvent, ManageSieveEvent, Pop3Event, SmtpEvent};
use utils::{config::Config, UnwrapFailure};
use crate::{
config::server::{Listener, Server, ServerProtocol, Servers},
Core,
config::server::{Listener, Listeners, ServerProtocol, TcpListener},
core::BuildServer,
Inner, Server,
};
use super::{
@ -31,11 +28,11 @@ use super::{
TcpAcceptor,
};
impl Server {
impl Listener {
pub fn spawn(
self,
manager: impl SessionManager,
core: Arc<ArcSwap<Core>>,
inner: Arc<Inner>,
acceptor: TcpAcceptor,
shutdown_rx: watch::Receiver<bool>,
) {
@ -95,7 +92,7 @@ impl Server {
let mut shutdown_rx = instance.shutdown_rx.clone();
let manager = manager.clone();
let instance = instance.clone();
let core = core.clone();
let inner = inner.clone();
tokio::spawn(async move {
let (span_start, span_end) = match self.protocol {
ServerProtocol::Smtp | ServerProtocol::Lmtp => (
@ -125,8 +122,8 @@ impl Server {
stream = listener.accept() => {
match stream {
Ok((stream, remote_addr)) => {
let core = core.as_ref().load_full();
let enable_acme = (is_https && core.has_acme_tls_providers()).then_some(core.clone());
let server = inner.build_server();
let enable_acme = (is_https && server.has_acme_tls_providers()).then(|| server.clone());
if has_proxies && instance.proxy_networks.iter().any(|network| network.matches(&remote_addr.ip())) {
let instance = instance.clone();
@ -142,7 +139,7 @@ impl Server {
.proxied_address()
.map(|addr| addr.source)
.unwrap_or(remote_addr);
if let Some(session) = instance.build_session(stream, local_addr, remote_addr, &core) {
if let Some(session) = instance.build_session(stream, local_addr, remote_addr, &server) {
// Spawn session
manager.spawn(session, is_tls, enable_acme, span_start, span_end);
}
@ -159,7 +156,7 @@ impl Server {
}
}
});
} else if let Some(session) = instance.build_session(stream, local_addr, remote_addr, &core) {
} else if let Some(session) = instance.build_session(stream, local_addr, remote_addr, &server) {
// Set socket options
opts.apply(&session.stream);
@ -205,7 +202,7 @@ trait BuildSession {
stream: T,
local_addr: SocketAddr,
remote_addr: SocketAddr,
core: &Core,
server: &Server,
) -> Option<SessionData<T>>;
}
@ -215,7 +212,7 @@ impl BuildSession for Arc<ServerInstance> {
stream: T,
local_addr: SocketAddr,
remote_addr: SocketAddr,
core: &Core,
server: &Server,
) -> Option<SessionData<T>> {
// Convert mapped IPv6 addresses to IPv4
let remote_ip = match remote_addr.ip() {
@ -228,7 +225,7 @@ impl BuildSession for Arc<ServerInstance> {
let remote_port = remote_addr.port();
// Check if blocked
if core.is_ip_blocked(&remote_ip) {
if server.is_ip_blocked(&remote_ip) {
trc::event!(
Security(trc::SecurityEvent::IpBlocked),
ListenerId = self.id.clone(),
@ -303,7 +300,7 @@ impl SocketOpts {
}
}
impl Servers {
impl Listeners {
pub fn bind_and_drop_priv(&self, config: &mut Config) {
// Bind as root
for server in &self.servers {
@ -332,7 +329,7 @@ impl Servers {
pub fn spawn(
mut self,
spawn: impl Fn(Server, TcpAcceptor, watch::Receiver<bool>),
spawn: impl Fn(Listener, TcpAcceptor, watch::Receiver<bool>),
) -> (watch::Sender<bool>, watch::Receiver<bool>) {
// Spawn listeners
let (shutdown_tx, shutdown_rx) = watch::channel(false);
@ -348,8 +345,8 @@ impl Servers {
}
}
impl Listener {
pub fn listen(self) -> Result<TcpListener, String> {
impl TcpListener {
pub fn listen(self) -> Result<tokio::net::TcpListener, String> {
self.socket
.listen(self.backlog.unwrap_or(1024))
.map_err(|err| format!("Failed to listen on {}: {}", self.addr, err))

View file

@ -19,7 +19,7 @@ use utils::{config::ipmask::IpAddrMask, snowflake::SnowflakeIdGenerator};
use crate::{
config::server::ServerProtocol,
expr::{functions::ResolveVariable, *},
Core,
Server,
};
use self::limiter::{ConcurrencyLimiter, InFlight};
@ -91,7 +91,7 @@ pub trait SessionManager: Sync + Send + 'static + Clone {
&self,
mut session: SessionData<T>,
is_tls: bool,
acme_core: Option<Arc<Core>>,
acme_core: Option<Server>,
span_start: EventType,
span_end: EventType,
) {

View file

@ -11,7 +11,6 @@ use std::{
};
use ahash::AHashMap;
use arc_swap::ArcSwap;
use rustls::{
server::{ClientHello, ResolvesServerCert},
sign::CertifiedKey,
@ -21,7 +20,7 @@ use rustls::{
use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt};
use tokio_rustls::{Accept, LazyConfigAcceptor};
use crate::{Core, SharedCore};
use crate::{Inner, Server};
use super::{
acme::{
@ -34,36 +33,31 @@ use super::{
pub static TLS13_VERSION: &[&SupportedProtocolVersion] = &[&TLS13];
pub static TLS12_VERSION: &[&SupportedProtocolVersion] = &[&TLS12];
#[derive(Default)]
pub struct TlsManager {
pub certificates: ArcSwap<AHashMap<String, Arc<CertifiedKey>>>,
pub acme_providers: AHashMap<String, AcmeProvider>,
pub self_signed_cert: Option<Arc<CertifiedKey>>,
#[derive(Default, Clone)]
pub struct AcmeProviders {
pub providers: AHashMap<String, AcmeProvider>,
}
#[derive(Clone)]
pub struct CertificateResolver {
pub core: SharedCore,
pub inner: Arc<Inner>,
}
impl CertificateResolver {
pub fn new(core: SharedCore) -> Self {
Self { core }
pub fn new(inner: Arc<Inner>) -> Self {
Self { inner }
}
}
impl ResolvesServerCert for CertificateResolver {
fn resolve(&self, hello: ClientHello<'_>) -> Option<Arc<CertifiedKey>> {
self.core
.as_ref()
.load()
.resolve_certificate(hello.server_name())
self.resolve_certificate(hello.server_name())
}
}
impl Core {
impl CertificateResolver {
pub(crate) fn resolve_certificate(&self, name: Option<&str>) -> Option<Arc<CertifiedKey>> {
let certs = self.tls.certificates.load();
let certs = self.inner.data.tls_certificates.load();
name.map_or_else(
|| certs.get("*"),
@ -98,7 +92,7 @@ impl Core {
Tls(trc::TlsEvent::NoCertificatesAvailable),
Total = certs.len(),
);
self.tls.self_signed_cert.as_ref()
self.inner.data.tls_self_signed_cert.as_ref()
}
})
.cloned()
@ -109,7 +103,7 @@ impl TcpAcceptor {
pub async fn accept<IO>(
&self,
stream: IO,
enable_acme: Option<Arc<Core>>,
enable_acme: Option<Server>,
instance: &ServerInstance,
) -> TcpAcceptorResult<IO>
where
@ -215,13 +209,3 @@ impl std::fmt::Debug for CertificateResolver {
f.debug_struct("CertificateResolver").finish()
}
}
impl Clone for TlsManager {
fn clone(&self) -> Self {
Self {
certificates: ArcSwap::from_pointee(self.certificates.load().as_ref().clone()),
acme_providers: self.acme_providers.clone(),
self_signed_cert: self.self_signed_cert.clone(),
}
}
}

View file

@ -4,7 +4,7 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::path::PathBuf;
use std::{path::PathBuf, sync::Arc};
use arc_swap::ArcSwap;
use pwhash::sha512_crypt;
@ -12,14 +12,16 @@ use store::{
rand::{distributions::Alphanumeric, thread_rng, Rng},
Stores,
};
use tokio::sync::{mpsc, Notify};
use utils::{
config::{Config, ConfigKey},
failed, UnwrapFailure,
};
use crate::{
config::{server::Servers, telemetry::Telemetry},
Core, SharedCore,
config::{server::Listeners, telemetry::Telemetry},
ipc::{DeliveryEvent, HousekeeperEvent, QueueEvent, ReportingEvent, StateEvent},
Core, Data, Inner, Ipc, IPC_CHANNEL_BUFFER,
};
use super::{
@ -30,8 +32,17 @@ use super::{
pub struct BootManager {
pub config: Config,
pub core: SharedCore,
pub servers: Servers,
pub inner: Arc<Inner>,
pub servers: Listeners,
pub ipc_rxs: IpcReceivers,
}
pub struct IpcReceivers {
pub state_rx: Option<mpsc::Receiver<StateEvent>>,
pub housekeeper_rx: Option<mpsc::Receiver<HousekeeperEvent>>,
pub delivery_rx: Option<mpsc::Receiver<DeliveryEvent>>,
pub queue_rx: Option<mpsc::Receiver<QueueEvent>>,
pub report_rx: Option<mpsc::Receiver<ReportingEvent>>,
}
const HELP: &str = concat!(
@ -135,7 +146,7 @@ impl BootManager {
config.resolve_macros(&["env"]).await;
// Parser servers
let mut servers = Servers::parse(&mut config);
let mut servers = Listeners::parse(&mut config);
// Bind ports and drop privileges
servers.bind_and_drop_priv(&mut config);
@ -314,6 +325,9 @@ impl BootManager {
// Parse settings
let core = Core::parse(&mut config, stores, manager).await;
// Parse data
let data = Data::parse(&mut config);
// Enable telemetry
#[cfg(feature = "enterprise")]
telemetry.enable(core.is_enterprise_edition());
@ -325,16 +339,22 @@ impl BootManager {
Version = env!("CARGO_PKG_VERSION"),
);
// Build shared core
let core = core.into_shared();
// Build shared inner
let (ipc, ipc_rxs) = build_ipc();
let inner = Arc::new(Inner {
shared_core: ArcSwap::from_pointee(core),
data,
ipc,
});
// Parse TCP acceptors
servers.parse_tcp_acceptors(&mut config, core.clone());
servers.parse_tcp_acceptors(&mut config, inner.clone());
BootManager {
core,
inner,
config,
servers,
ipc_rxs,
}
}
ImportExport::Export(path) => {
@ -363,6 +383,32 @@ impl BootManager {
}
}
pub fn build_ipc() -> (Ipc, IpcReceivers) {
// Build ipc receivers
let (delivery_tx, delivery_rx) = mpsc::channel(IPC_CHANNEL_BUFFER);
let (state_tx, state_rx) = mpsc::channel(IPC_CHANNEL_BUFFER);
let (housekeeper_tx, housekeeper_rx) = mpsc::channel(IPC_CHANNEL_BUFFER);
let (queue_tx, queue_rx) = mpsc::channel(IPC_CHANNEL_BUFFER);
let (report_tx, report_rx) = mpsc::channel(IPC_CHANNEL_BUFFER);
(
Ipc {
state_tx,
housekeeper_tx,
delivery_tx,
queue_tx,
report_tx,
index_tx: Arc::new(Notify::new()),
},
IpcReceivers {
state_rx: Some(state_rx),
housekeeper_rx: Some(housekeeper_rx),
delivery_rx: Some(delivery_rx),
queue_rx: Some(queue_rx),
report_rx: Some(report_rx),
},
)
}
fn quickstart(path: impl Into<PathBuf>) {
let path = path.into();

View file

@ -4,18 +4,18 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use ahash::AHashSet;
use ahash::AHashMap;
use arc_swap::ArcSwap;
use store::Stores;
use utils::config::{ipmask::IpAddrOrMask, utils::ParseValue, Config};
use utils::config::Config;
use crate::{
config::{
server::{tls::parse_certificates, Servers},
server::{tls::parse_certificates, Listeners},
telemetry::Telemetry,
},
listener::blocked::BLOCKED_IP_KEY,
Core,
listener::blocked::{BlockedIps, BLOCKED_IP_KEY},
Core, Server,
};
use super::config::{ConfigManager, Patterns};
@ -26,49 +26,36 @@ pub struct ReloadResult {
pub tracers: Option<Telemetry>,
}
impl Core {
impl Server {
pub async fn reload_blocked_ips(&self) -> trc::Result<ReloadResult> {
let mut ip_addresses = AHashSet::new();
let mut config = self.storage.config.build_config(BLOCKED_IP_KEY).await?;
for ip in config
.set_values(BLOCKED_IP_KEY)
.map(IpAddrOrMask::parse_value)
.collect::<Vec<_>>()
{
match ip {
Ok(IpAddrOrMask::Ip(ip)) => {
ip_addresses.insert(ip);
}
Ok(IpAddrOrMask::Mask(_)) => {}
Err(err) => {
config.new_parse_error(BLOCKED_IP_KEY, err);
}
}
}
*self.network.blocked_ips.ip_addresses.write() = ip_addresses;
let mut config = self
.core
.storage
.config
.build_config(BLOCKED_IP_KEY)
.await?;
*self.inner.data.blocked_ips.write() = BlockedIps::parse(&mut config).blocked_ip_addresses;
Ok(config.into())
}
pub async fn reload_certificates(&self) -> trc::Result<ReloadResult> {
let mut config = self.storage.config.build_config("certificate").await?;
let mut certificates = self.tls.certificates.load().as_ref().clone();
let mut config = self.core.storage.config.build_config("certificate").await?;
let mut certificates = self.inner.data.tls_certificates.load().as_ref().clone();
parse_certificates(&mut config, &mut certificates, &mut Default::default());
self.tls.certificates.store(certificates.into());
self.inner.data.tls_certificates.store(certificates.into());
Ok(config.into())
}
pub async fn reload_lookups(&self) -> trc::Result<ReloadResult> {
let mut config = self.storage.config.build_config("lookup").await?;
let mut config = self.core.storage.config.build_config("lookup").await?;
let mut stores = Stores::default();
stores.parse_memory_stores(&mut config);
let mut core = self.clone();
let mut core = self.core.as_ref().clone();
for (id, store) in stores.lookup_stores {
core.storage.lookups.insert(id, store);
}
@ -81,14 +68,14 @@ impl Core {
}
pub async fn reload(&self) -> trc::Result<ReloadResult> {
let mut config = self.storage.config.build_config("").await?;
let mut config = self.core.storage.config.build_config("").await?;
// Load stores
let mut stores = Stores {
stores: self.storage.stores.clone(),
blob_stores: self.storage.blobs.clone(),
fts_stores: self.storage.ftss.clone(),
lookup_stores: self.storage.lookups.clone(),
stores: self.core.storage.stores.clone(),
blob_stores: self.core.storage.blobs.clone(),
fts_stores: self.core.storage.ftss.clone(),
lookup_stores: self.core.storage.lookups.clone(),
purge_schedules: Default::default(),
};
stores.parse_stores(&mut config).await;
@ -103,8 +90,10 @@ impl Core {
// Build manager
let manager = ConfigManager {
cfg_local: ArcSwap::from_pointee(self.storage.config.cfg_local.load().as_ref().clone()),
cfg_local_path: self.storage.config.cfg_local_path.clone(),
cfg_local: ArcSwap::from_pointee(
self.core.storage.config.cfg_local.load().as_ref().clone(),
),
cfg_local_path: self.core.storage.config.cfg_local_path.clone(),
cfg_local_patterns: Patterns::parse(&mut config).into(),
cfg_store: config
.value("storage.data")
@ -114,26 +103,29 @@ impl Core {
};
// Parse settings and build shared core
let mut core = Core::parse(&mut config, stores, manager).await;
let core = Core::parse(&mut config, stores, manager).await;
if !config.errors.is_empty() {
return Ok(config.into());
}
// Copy ACME certificates
let mut certificates = core.tls.certificates.load().as_ref().clone();
for (cert_id, cert) in self.tls.certificates.load().iter() {
certificates
.entry(cert_id.to_string())
.or_insert(cert.clone());
// Update TLS certificates
let mut new_certificates = AHashMap::new();
parse_certificates(&mut config, &mut new_certificates, &mut Default::default());
let mut current_certificates = self.inner.data.tls_certificates.load().as_ref().clone();
for (cert_id, cert) in new_certificates {
current_certificates.insert(cert_id, cert);
}
core.tls.certificates.store(certificates.into());
core.tls
.self_signed_cert
.clone_from(&self.tls.self_signed_cert);
self.inner
.data
.tls_certificates
.store(current_certificates.into());
// Update blocked IPs
*self.inner.data.blocked_ips.write() = BlockedIps::parse(&mut config).blocked_ip_addresses;
// Parser servers
let mut servers = Servers::parse(&mut config);
servers.parse_tcp_acceptors(&mut config, core.clone().into_shared());
let mut servers = Listeners::parse(&mut config);
servers.parse_tcp_acceptors(&mut config, self.inner.clone());
Ok(if config.errors.is_empty() {
ReloadResult {

View file

@ -43,8 +43,8 @@ pub async fn exec_untrain(ctx: PluginContext<'_>) -> trc::Result<Variable> {
async fn train(ctx: PluginContext<'_>, is_train: bool) -> trc::Result<Variable> {
let store = match &ctx.arguments[0] {
Variable::String(v) if !v.is_empty() => ctx.core.storage.lookups.get(v.as_ref()),
_ => Some(&ctx.core.storage.lookup),
Variable::String(v) if !v.is_empty() => ctx.server.core.storage.lookups.get(v.as_ref()),
_ => Some(&ctx.server.core.storage.lookup),
}
.ok_or_else(|| {
trc::SieveEvent::RuntimeError
@ -80,7 +80,7 @@ async fn train(ctx: PluginContext<'_>, is_train: bool) -> trc::Result<Variable>
);
// Update weight and invalidate cache
let bayes_cache = &ctx.cache.bayes_cache;
let bayes_cache = &ctx.server.inner.data.bayes_cache;
if is_train {
for (hash, weights) in model.weights {
store
@ -129,8 +129,8 @@ async fn train(ctx: PluginContext<'_>, is_train: bool) -> trc::Result<Variable>
pub async fn exec_classify(ctx: PluginContext<'_>) -> trc::Result<Variable> {
let store = match &ctx.arguments[0] {
Variable::String(v) if !v.is_empty() => ctx.core.storage.lookups.get(v.as_ref()),
_ => Some(&ctx.core.storage.lookup),
Variable::String(v) if !v.is_empty() => ctx.server.core.storage.lookups.get(v.as_ref()),
_ => Some(&ctx.server.core.storage.lookup),
}
.ok_or_else(|| {
trc::SieveEvent::RuntimeError
@ -162,7 +162,7 @@ pub async fn exec_classify(ctx: PluginContext<'_>) -> trc::Result<Variable> {
}
// Obtain training counts
let bayes_cache = &ctx.cache.bayes_cache;
let bayes_cache = &ctx.server.inner.data.bayes_cache;
let (spam_learns, ham_learns) = bayes_cache
.get_or_update(TokenHash::default(), store)
.await
@ -219,8 +219,8 @@ pub async fn exec_is_balanced(ctx: PluginContext<'_>) -> trc::Result<Variable> {
}
let store = match &ctx.arguments[0] {
Variable::String(v) if !v.is_empty() => ctx.core.storage.lookups.get(v.as_ref()),
_ => Some(&ctx.core.storage.lookup),
Variable::String(v) if !v.is_empty() => ctx.server.core.storage.lookups.get(v.as_ref()),
_ => Some(&ctx.server.core.storage.lookup),
}
.ok_or_else(|| {
trc::SieveEvent::RuntimeError
@ -231,7 +231,7 @@ pub async fn exec_is_balanced(ctx: PluginContext<'_>) -> trc::Result<Variable> {
let learn_spam = ctx.arguments[1].to_bool();
// Obtain training counts
let bayes_cache = &ctx.cache.bayes_cache;
let bayes_cache = &ctx.server.inner.data.bayes_cache;
let (spam_learns, ham_learns) = bayes_cache
.get_or_update(TokenHash::default(), store)
.await

View file

@ -25,6 +25,7 @@ pub async fn exec(ctx: PluginContext<'_>) -> trc::Result<Variable> {
Ok(if record_type.eq_ignore_ascii_case("ip") {
match ctx
.server
.core
.smtp
.resolvers
@ -40,7 +41,15 @@ pub async fn exec(ctx: PluginContext<'_>) -> trc::Result<Variable> {
Err(err) => err.short_error().into(),
}
} else if record_type.eq_ignore_ascii_case("mx") {
match ctx.core.smtp.resolvers.dns.mx_lookup(entry.as_ref()).await {
match ctx
.server
.core
.smtp
.resolvers
.dns
.mx_lookup(entry.as_ref())
.await
{
Ok(result) => result
.iter()
.flat_map(|mx| {
@ -61,6 +70,7 @@ pub async fn exec(ctx: PluginContext<'_>) -> trc::Result<Variable> {
}
match ctx
.server
.core
.smtp
.resolvers
@ -73,7 +83,7 @@ pub async fn exec(ctx: PluginContext<'_>) -> trc::Result<Variable> {
}
} else if record_type.eq_ignore_ascii_case("ptr") {
if let Ok(addr) = entry.parse::<IpAddr>() {
match ctx.core.smtp.resolvers.dns.ptr_lookup(addr).await {
match ctx.server.core.smtp.resolvers.dns.ptr_lookup(addr).await {
Ok(result) => result
.iter()
.map(|host| Variable::from(host.to_string()))
@ -94,6 +104,7 @@ pub async fn exec(ctx: PluginContext<'_>) -> trc::Result<Variable> {
}
match ctx
.server
.core
.smtp
.resolvers
@ -110,6 +121,7 @@ pub async fn exec(ctx: PluginContext<'_>) -> trc::Result<Variable> {
}
} else if record_type.eq_ignore_ascii_case("ipv6") {
match ctx
.server
.core
.smtp
.resolvers
@ -135,6 +147,7 @@ pub async fn exec_exists(ctx: PluginContext<'_>) -> trc::Result<Variable> {
Ok(if record_type.eq_ignore_ascii_case("ip") {
match ctx
.server
.core
.smtp
.resolvers
@ -147,14 +160,22 @@ pub async fn exec_exists(ctx: PluginContext<'_>) -> trc::Result<Variable> {
Err(_) => -1,
}
} else if record_type.eq_ignore_ascii_case("mx") {
match ctx.core.smtp.resolvers.dns.mx_lookup(entry.as_ref()).await {
match ctx
.server
.core
.smtp
.resolvers
.dns
.mx_lookup(entry.as_ref())
.await
{
Ok(result) => i64::from(result.iter().any(|mx| !mx.exchanges.is_empty())),
Err(Error::DnsRecordNotFound(_)) => 0,
Err(_) => -1,
}
} else if record_type.eq_ignore_ascii_case("ptr") {
if let Ok(addr) = entry.parse::<IpAddr>() {
match ctx.core.smtp.resolvers.dns.ptr_lookup(addr).await {
match ctx.server.core.smtp.resolvers.dns.ptr_lookup(addr).await {
Ok(result) => i64::from(!result.is_empty()),
Err(Error::DnsRecordNotFound(_)) => 0,
Err(_) => -1,
@ -171,6 +192,7 @@ pub async fn exec_exists(ctx: PluginContext<'_>) -> trc::Result<Variable> {
}
match ctx
.server
.core
.smtp
.resolvers
@ -184,6 +206,7 @@ pub async fn exec_exists(ctx: PluginContext<'_>) -> trc::Result<Variable> {
}
} else if record_type.eq_ignore_ascii_case("ipv6") {
match ctx
.server
.core
.smtp
.resolvers

View file

@ -42,8 +42,8 @@ pub fn register_local_domain(plugin_id: u32, fnc_map: &mut FunctionMap) {
pub async fn exec(ctx: PluginContext<'_>) -> trc::Result<Variable> {
let store = match &ctx.arguments[0] {
Variable::String(v) if !v.is_empty() => ctx.core.storage.lookups.get(v.as_ref()),
_ => Some(&ctx.core.storage.lookup),
Variable::String(v) if !v.is_empty() => ctx.server.core.storage.lookups.get(v.as_ref()),
_ => Some(&ctx.server.core.storage.lookup),
}
.ok_or_else(|| {
trc::SieveEvent::RuntimeError
@ -76,8 +76,8 @@ pub async fn exec(ctx: PluginContext<'_>) -> trc::Result<Variable> {
pub async fn exec_get(ctx: PluginContext<'_>) -> trc::Result<Variable> {
match &ctx.arguments[0] {
Variable::String(v) if !v.is_empty() => ctx.core.storage.lookups.get(v.as_ref()),
_ => Some(&ctx.core.storage.lookup),
Variable::String(v) if !v.is_empty() => ctx.server.core.storage.lookups.get(v.as_ref()),
_ => Some(&ctx.server.core.storage.lookup),
}
.ok_or_else(|| {
trc::SieveEvent::RuntimeError
@ -97,8 +97,8 @@ pub async fn exec_set(ctx: PluginContext<'_>) -> trc::Result<Variable> {
};
match &ctx.arguments[0] {
Variable::String(v) if !v.is_empty() => ctx.core.storage.lookups.get(v.as_ref()),
_ => Some(&ctx.core.storage.lookup),
Variable::String(v) if !v.is_empty() => ctx.server.core.storage.lookups.get(v.as_ref()),
_ => Some(&ctx.server.core.storage.lookup),
}
.ok_or_else(|| {
trc::SieveEvent::RuntimeError
@ -125,7 +125,7 @@ pub async fn exec_remote(ctx: PluginContext<'_>) -> trc::Result<Variable> {
// Something went wrong, try again in one hour
const RETRY: Duration = Duration::from_secs(3600);
let mut _lock = ctx.cache.remote_lists.write();
let mut _lock = ctx.server.inner.data.remote_lists.write();
let list = _lock
.entry(ctx.arguments[0].to_string().to_string())
.or_insert_with(|| RemoteList {
@ -169,7 +169,14 @@ async fn exec_remote_(ctx: &PluginContext<'_>) -> trc::Result<Variable> {
const MAX_ENTRY_SIZE: usize = 256;
const MAX_ENTRIES: usize = 100000;
match ctx.cache.remote_lists.read().get(resource.as_ref()) {
match ctx
.server
.inner
.data
.remote_lists
.read()
.get(resource.as_ref())
{
Some(remote_list) if remote_list.expires < Instant::now() => {
return Ok(remote_list.entries.contains(item.as_ref()).into())
}
@ -256,7 +263,7 @@ async fn exec_remote_(ctx: &PluginContext<'_>) -> trc::Result<Variable> {
};
// Lock remote list for writing
let mut _lock = ctx.cache.remote_lists.write();
let mut _lock = ctx.server.inner.data.remote_lists.write();
let list = _lock
.entry(resource.to_string())
.or_insert_with(|| RemoteList {
@ -352,8 +359,10 @@ pub async fn exec_local_domain(ctx: PluginContext<'_>) -> trc::Result<Variable>
if !domain.is_empty() {
return match &ctx.arguments[0] {
Variable::String(v) if !v.is_empty() => ctx.core.storage.directories.get(v.as_ref()),
_ => Some(&ctx.core.storage.directory),
Variable::String(v) if !v.is_empty() => {
ctx.server.core.storage.directories.get(v.as_ref())
}
_ => Some(&ctx.server.core.storage.directory),
}
.ok_or_else(|| {
trc::SieveEvent::RuntimeError

View file

@ -17,7 +17,7 @@ pub mod text;
use mail_parser::Message;
use sieve::{runtime::Variable, FunctionMap, Input};
use crate::{config::scripts::ScriptCache, Core};
use crate::{Core, Server};
use super::ScriptModification;
@ -25,8 +25,7 @@ type RegisterPluginFnc = fn(u32, &mut FunctionMap) -> ();
pub struct PluginContext<'x> {
pub session_id: u64,
pub core: &'x Core,
pub cache: &'x ScriptCache,
pub server: &'x Server,
pub message: &'x Message<'x>,
pub modifications: &'x mut Vec<ScriptModification>,
pub arguments: Vec<Variable>,

View file

@ -19,8 +19,8 @@ pub fn register(plugin_id: u32, fnc_map: &mut FunctionMap) {
pub async fn exec(ctx: PluginContext<'_>) -> trc::Result<Variable> {
// Obtain store name
let store = match &ctx.arguments[0] {
Variable::String(v) if !v.is_empty() => ctx.core.storage.lookups.get(v.as_ref()),
_ => Some(&ctx.core.storage.lookup),
Variable::String(v) if !v.is_empty() => ctx.server.core.storage.lookups.get(v.as_ref()),
_ => Some(&ctx.server.core.storage.lookup),
}
.ok_or_else(|| {
trc::SieveEvent::RuntimeError

View file

@ -4,7 +4,7 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::{sync::Arc, time::SystemTime};
use std::time::SystemTime;
use opentelemetry::global::set_error_handler;
use opentelemetry_sdk::metrics::data::{
@ -13,19 +13,13 @@ use opentelemetry_sdk::metrics::data::{
};
use trc::{Collector, TelemetryEvent};
use crate::{config::telemetry::OtelMetrics, Core};
use crate::config::telemetry::OtelMetrics;
impl OtelMetrics {
pub async fn push_metrics(&self, core: Arc<Core>, start_time: SystemTime) {
pub async fn push_metrics(&self, is_enterprise: bool, start_time: SystemTime) {
let mut metrics = Vec::with_capacity(256);
let now = SystemTime::now();
#[cfg(feature = "enterprise")]
let is_enterprise = core.is_enterprise_edition();
#[cfg(not(feature = "enterprise"))]
let is_enterprise = false;
// Add counters
for counter in Collector::collect_counters(is_enterprise) {
metrics.push(Metric {

View file

@ -10,9 +10,9 @@ use prometheus::{
};
use trc::{atomics::histogram::AtomicHistogram, Collector};
use crate::Core;
use crate::Server;
impl Core {
impl Server {
pub async fn export_prometheus_metrics(&self) -> trc::Result<String> {
let mut metrics = Vec::new();

View file

@ -274,22 +274,26 @@ impl MigrateDirectory for Store {
},
),
|key, value| {
if key[0] == 2 && value[0] == 1 {
principals.push((
key.get(1..)
.and_then(|b| b.read_leb128::<u32>().map(|(v, _)| v))
.ok_or_else(|| {
trc::StoreEvent::DataCorruption
.caused_by(trc::location!())
.ctx(trc::Key::Value, key)
})?,
Principal::deserialize(value)?,
));
} else if key[0] == 3 {
let domain = std::str::from_utf8(&key[1..]).unwrap_or_default();
if !domain.is_empty() {
domains.push(domain.to_string());
match (key.first(), value.first()) {
(Some(2), Some(1)) => {
principals.push((
key.get(1..)
.and_then(|b| b.read_leb128::<u32>().map(|(v, _)| v))
.ok_or_else(|| {
trc::StoreEvent::DataCorruption
.caused_by(trc::location!())
.ctx(trc::Key::Value, key)
})?,
Principal::deserialize(value)?,
));
}
(Some(3), _) => {
let domain = std::str::from_utf8(&key[1..]).unwrap_or_default();
if !domain.is_empty() {
domains.push(domain.to_string());
}
}
_ => {}
}
Ok(true)

View file

@ -6,12 +6,14 @@
use std::{iter::Peekable, sync::Arc, vec::IntoIter};
use common::listener::{limiter::ConcurrencyLimiter, SessionResult, SessionStream};
use common::{
listener::{limiter::ConcurrencyLimiter, SessionResult, SessionStream},
ConcurrencyLimiters,
};
use imap_proto::{
receiver::{self, Request},
Command, ResponseType, StatusResponse,
};
use jmap::auth::rate_limit::ConcurrencyLimiters;
use super::{SelectedMailbox, Session, SessionData, State};
@ -255,9 +257,9 @@ impl<T: SessionStream> Session<T> {
let state = &self.state;
// Rate limit request
if let State::Authenticated { data } | State::Selected { data, .. } = state {
if let Some(rate) = &self.jmap.core.imap.rate_requests {
if let Some(rate) = &self.server.core.imap.rate_requests {
if data
.jmap
.server
.core
.storage
.lookup
@ -301,7 +303,7 @@ impl<T: SessionStream> Session<T> {
}
Command::Login => {
if let State::NotAuthenticated { .. } = state {
if self.is_tls || self.jmap.core.imap.allow_plain_auth {
if self.is_tls || self.server.core.imap.allow_plain_auth {
Ok(request)
} else {
Err(trc::ImapEvent::Error
@ -385,9 +387,11 @@ impl<T: SessionStream> Session<T> {
}
pub fn get_concurrency_limiter(&self, account_id: u32) -> Option<Arc<ConcurrencyLimiters>> {
let rate = self.jmap.core.imap.rate_concurrent?;
self.imap
.rate_limiter
let rate = self.server.core.imap.rate_concurrent?;
self.server
.inner
.data
.imap_limiter
.get(&account_id)
.map(|limiter| limiter.clone())
.unwrap_or_else(|| {
@ -395,7 +399,11 @@ impl<T: SessionStream> Session<T> {
concurrent_requests: ConcurrencyLimiter::new(rate),
concurrent_uploads: ConcurrencyLimiter::new(rate),
});
self.imap.rate_limiter.insert(account_id, limiter.clone());
self.server
.inner
.data
.imap_limiter
.insert(account_id, limiter.clone());
limiter
})
.into()

View file

@ -8,10 +8,16 @@ use common::{
auth::AccessToken,
config::jmap::settings::SpecialUse,
listener::{limiter::InFlight, SessionStream},
AccountId, Mailbox,
};
use directory::{backend::internal::PrincipalField, QueryBy};
use imap_proto::protocol::list::Attribute;
use jmap::{auth::acl::EffectiveAcl, mailbox::INBOX_ID};
use jmap::{
auth::acl::{AclMethods, EffectiveAcl},
changes::get::ChangesLookup,
mailbox::{get::MailboxGet, set::MailboxSet, INBOX_ID},
JmapMethods,
};
use jmap_proto::{
object::Object,
types::{acl::Acl, collection::Collection, id::Id, property::Property, value::Value},
@ -21,7 +27,7 @@ use store::query::log::{Change, Query};
use trc::AddContext;
use utils::lru_cache::LruCached;
use super::{Account, AccountId, Mailbox, MailboxId, MailboxSync, Session, SessionData};
use super::{Account, MailboxId, MailboxSync, Session, SessionData};
impl<T: SessionStream> SessionData<T> {
pub async fn new(
@ -31,8 +37,7 @@ impl<T: SessionStream> SessionData<T> {
) -> trc::Result<Self> {
let mut session = SessionData {
stream_tx: session.stream_tx.clone(),
jmap: session.jmap.clone(),
imap: session.imap.clone(),
server: session.server.clone(),
account_id: access_token.primary_id(),
session_id: session.session_id,
mailboxes: Mutex::new(vec![]),
@ -56,9 +61,9 @@ impl<T: SessionStream> SessionData<T> {
account_id,
format!(
"{}/{}",
session.jmap.core.jmap.shared_folder,
session.server.core.jmap.shared_folder,
session
.jmap
.server
.core
.storage
.directory
@ -88,7 +93,7 @@ impl<T: SessionStream> SessionData<T> {
access_token: &AccessToken,
) -> trc::Result<Account> {
let state_mailbox = self
.jmap
.server
.core
.storage
.data
@ -96,7 +101,7 @@ impl<T: SessionStream> SessionData<T> {
.await
.caused_by(trc::location!())?;
let state_email = self
.jmap
.server
.core
.storage
.data
@ -107,19 +112,21 @@ impl<T: SessionStream> SessionData<T> {
account_id,
primary_id: access_token.primary_id(),
};
if let Some(cached_account) =
self.imap
.cache_account
.get(&cached_account_id)
.and_then(|cached_account| {
if cached_account.state_mailbox == state_mailbox
&& cached_account.state_email == state_email
{
Some(cached_account)
} else {
None
}
})
if let Some(cached_account) = self
.server
.inner
.data
.account_cache
.get(&cached_account_id)
.and_then(|cached_account| {
if cached_account.state_mailbox == state_mailbox
&& cached_account.state_email == state_email
{
Some(cached_account)
} else {
None
}
})
{
return Ok(cached_account.as_ref().clone());
}
@ -127,12 +134,12 @@ impl<T: SessionStream> SessionData<T> {
let mailbox_ids = if access_token.is_primary_id(account_id)
|| access_token.member_of.contains(&account_id)
{
self.jmap
self.server
.mailbox_get_or_create(account_id)
.await
.caused_by(trc::location!())?
} else {
self.jmap
self.server
.shared_documents(access_token, account_id, Collection::Mailbox, Acl::Read)
.await
.caused_by(trc::location!())?
@ -142,7 +149,7 @@ impl<T: SessionStream> SessionData<T> {
let mut mailboxes = Vec::with_capacity(10);
let mut special_uses = AHashMap::new();
for (mailbox_id, values) in self
.jmap
.server
.get_properties::<Object<Value>, _, _>(
account_id,
Collection::Mailbox,
@ -189,7 +196,7 @@ impl<T: SessionStream> SessionData<T> {
let mut path = Vec::new();
let mut iter_stack = Vec::new();
let message_ids = self
.jmap
.server
.get_document_ids(account_id, Collection::Email)
.await
.caused_by(trc::location!())?;
@ -246,7 +253,7 @@ impl<T: SessionStream> SessionData<T> {
},
),
total_messages: self
.jmap
.server
.get_tag(
account_id,
Collection::Email,
@ -259,7 +266,7 @@ impl<T: SessionStream> SessionData<T> {
.unwrap_or(0)
.into(),
total_unseen: self
.jmap
.server
.mailbox_unread_tags(account_id, *mailbox_id, &message_ids)
.await
.caused_by(trc::location!())?
@ -278,7 +285,7 @@ impl<T: SessionStream> SessionData<T> {
// Map special use folder aliases to their internal ids
let effective_mailbox_id = self
.jmap
.server
.core
.jmap
.default_folders
@ -313,8 +320,10 @@ impl<T: SessionStream> SessionData<T> {
}
// Update cache
self.imap
.cache_account
self.server
.inner
.data
.account_cache
.insert(cached_account_id, Arc::new(account.clone()));
Ok(account)
@ -332,8 +341,7 @@ impl<T: SessionStream> SessionData<T> {
// Obtain access token
let access_token = self
.jmap
.core
.server
.get_cached_access_token(self.account_id)
.await
.caused_by(trc::location!())?;
@ -383,8 +391,8 @@ impl<T: SessionStream> SessionData<T> {
for account_id in added_account_ids {
let prefix = format!(
"{}/{}",
self.jmap.core.jmap.shared_folder,
self.jmap
self.server.core.jmap.shared_folder,
self.server
.core
.storage
.directory
@ -414,7 +422,7 @@ impl<T: SessionStream> SessionData<T> {
.collect::<Vec<_>>();
for (account_id, last_state) in account_states {
let changelog = self
.jmap
.server
.changes_(
account_id,
Collection::Mailbox,
@ -437,7 +445,7 @@ impl<T: SessionStream> SessionData<T> {
if has_child_changes && !has_changes && changes.is_none() {
// Only child changes, no need to re-fetch mailboxes
let state_email = self
.jmap
.server
.core
.storage
.data
@ -461,8 +469,13 @@ impl<T: SessionStream> SessionData<T> {
}
// Update cache
if let Some(cached_account_) =
self.imap.cache_account.lock().get_mut(&AccountId {
if let Some(cached_account_) = self
.server
.inner
.data
.account_cache
.lock()
.get_mut(&AccountId {
account_id,
primary_id: access_token.primary_id(),
})
@ -488,8 +501,8 @@ impl<T: SessionStream> SessionData<T> {
let mailbox_prefix = if !access_token.is_primary_id(account_id) {
format!(
"{}/{}",
self.jmap.core.jmap.shared_folder,
self.jmap
self.server.core.jmap.shared_folder,
self.server
.core
.storage
.directory
@ -613,7 +626,7 @@ impl<T: SessionStream> SessionData<T> {
let access_token = self.get_access_token().await?;
Ok(access_token.is_member(account_id)
|| self
.jmap
.server
.get_property::<Object<Value>>(
account_id,
Collection::Mailbox,

View file

@ -7,9 +7,9 @@
use std::{collections::BTreeMap, sync::Arc};
use ahash::AHashMap;
use common::listener::SessionStream;
use common::{listener::SessionStream, NextMailboxState};
use imap_proto::protocol::{expunge, select::Exists, Sequence};
use jmap::mailbox::UidMailbox;
use jmap::{mailbox::UidMailbox, JmapMethods};
use jmap_proto::{
object::Object,
types::{collection::Collection, property::Property, value::Value},
@ -20,7 +20,7 @@ use utils::lru_cache::LruCached;
use crate::core::ImapId;
use super::{ImapUidToId, MailboxId, MailboxState, NextMailboxState, SelectedMailbox, SessionData};
use super::{ImapUidToId, MailboxId, MailboxState, SelectedMailbox, SessionData};
pub(crate) const MAX_RETRIES: usize = 10;
@ -28,7 +28,7 @@ impl<T: SessionStream> SessionData<T> {
pub async fn fetch_messages(&self, mailbox: &MailboxId) -> trc::Result<MailboxState> {
// Obtain message ids
let message_ids = self
.jmap
.server
.get_tag(
mailbox.account_id,
Collection::Email,
@ -43,7 +43,7 @@ impl<T: SessionStream> SessionData<T> {
// Obtain current state
let modseq = self
.jmap
.server
.core
.storage
.data
@ -54,7 +54,7 @@ impl<T: SessionStream> SessionData<T> {
// Obtain all message ids
let mut uid_map = BTreeMap::new();
for (message_id, uid_mailbox) in self
.jmap
.server
.get_properties::<HashedValue<Vec<UidMailbox>>, _, _>(
mailbox.account_id,
Collection::Email,
@ -148,8 +148,10 @@ impl<T: SessionStream> SessionData<T> {
current_state.id_to_imap = id_to_imap;
// Update cache
self.imap
.cache_mailbox
self.server
.inner
.data
.mailbox_cache
.insert(mailbox.id, Arc::new(new_state.clone()));
// Update state
@ -207,7 +209,7 @@ impl<T: SessionStream> SessionData<T> {
pub async fn get_modseq(&self, account_id: u32) -> trc::Result<Option<u64>> {
// Obtain current modseq
self.jmap
self.server
.core
.storage
.data
@ -221,7 +223,7 @@ impl<T: SessionStream> SessionData<T> {
}
pub async fn get_uid_validity(&self, mailbox: &MailboxId) -> trc::Result<u32> {
self.jmap
self.server
.get_property::<Object<Value>>(
mailbox.account_id,
Collection::Mailbox,

View file

@ -5,29 +5,21 @@
*/
use std::{
collections::BTreeMap,
net::IpAddr,
sync::{atomic::AtomicU32, Arc},
};
use ahash::AHashMap;
use common::{
auth::AccessToken,
listener::{limiter::InFlight, ServerInstance, SessionStream},
Account, ImapId, Inner, MailboxId, MailboxState, Server,
};
use dashmap::DashMap;
use imap_proto::{
protocol::{list::Attribute, ProtocolVersion},
receiver::Receiver,
Command,
};
use jmap::{auth::rate_limit::ConcurrencyLimiters, JmapInstance, JMAP};
use imap_proto::{protocol::ProtocolVersion, receiver::Receiver, Command};
use tokio::{
io::{ReadHalf, WriteHalf},
sync::watch,
};
use trc::AddContext;
use utils::lru_cache::LruCache;
pub mod client;
pub mod mailbox;
@ -36,32 +28,17 @@ pub mod session;
#[derive(Clone)]
pub struct ImapSessionManager {
pub imap: ImapInstance,
pub inner: Arc<Inner>,
}
impl ImapSessionManager {
pub fn new(imap: ImapInstance) -> Self {
Self { imap }
pub fn new(inner: Arc<Inner>) -> Self {
Self { inner }
}
}
#[derive(Clone)]
pub struct ImapInstance {
pub jmap_instance: JmapInstance,
pub imap_inner: Arc<Inner>,
}
pub struct Inner {
pub rate_limiter: DashMap<u32, Arc<ConcurrencyLimiters>>,
pub cache_account: LruCache<AccountId, Arc<Account>>,
pub cache_mailbox: LruCache<MailboxId, Arc<MailboxState>>,
}
pub struct IMAP {}
pub struct Session<T: SessionStream> {
pub jmap: JMAP,
pub imap: Arc<Inner>,
pub server: Server,
pub instance: Arc<ServerInstance>,
pub receiver: Receiver<Command>,
pub version: ProtocolVersion,
@ -79,8 +56,7 @@ pub struct Session<T: SessionStream> {
pub struct SessionData<T: SessionStream> {
pub account_id: u32,
pub access_token: Arc<AccessToken>,
pub jmap: JMAP,
pub imap: Arc<Inner>,
pub server: Server,
pub session_id: u64,
pub mailboxes: parking_lot::Mutex<Vec<Account>>,
pub stream_tx: Arc<tokio::sync::Mutex<WriteHalf<T>>>,
@ -88,29 +64,6 @@ pub struct SessionData<T: SessionStream> {
pub in_flight: Option<InFlight>,
}
#[derive(Debug, Default, Clone)]
pub struct Mailbox {
pub has_children: bool,
pub is_subscribed: bool,
pub special_use: Option<Attribute>,
pub total_messages: Option<u32>,
pub total_unseen: Option<u32>,
pub total_deleted: Option<u32>,
pub uid_validity: Option<u32>,
pub uid_next: Option<u32>,
pub size: Option<u32>,
}
#[derive(Debug, Clone, Default)]
pub struct Account {
pub account_id: u32,
pub prefix: Option<String>,
pub mailbox_names: BTreeMap<String, u32>,
pub mailbox_state: AHashMap<u32, Mailbox>,
pub state_email: Option<u64>,
pub state_mailbox: Option<u64>,
}
pub struct SelectedMailbox {
pub id: MailboxId,
pub state: parking_lot::Mutex<MailboxState>,
@ -119,36 +72,6 @@ pub struct SelectedMailbox {
pub is_condstore: bool,
}
#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
pub struct MailboxId {
pub account_id: u32,
pub mailbox_id: u32,
}
#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
pub struct AccountId {
pub account_id: u32,
pub primary_id: u32,
}
#[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>>,
}
#[derive(Debug, Clone)]
pub struct NextMailboxState {
pub next_state: MailboxState,
pub deletions: Vec<ImapId>,
}
#[derive(Debug, Default)]
pub struct MailboxSync {
pub added: Vec<String>,
@ -166,12 +89,6 @@ pub enum SavedSearch {
None,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct ImapId {
pub uid: u32,
pub seqnum: u32,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct ImapUidToId {
pub uid: u32,
@ -217,8 +134,7 @@ impl<T: SessionStream> State<T> {
impl<T: SessionStream> SessionData<T> {
pub async fn get_access_token(&self) -> trc::Result<Arc<AccessToken>> {
self.jmap
.core
self.server
.get_cached_access_token(self.account_id)
.await
.caused_by(trc::location!())
@ -230,8 +146,7 @@ impl<T: SessionStream> SessionData<T> {
) -> SessionData<U> {
SessionData {
account_id: self.account_id,
jmap: self.jmap,
imap: self.imap,
server: self.server,
session_id: self.session_id,
mailboxes: self.mailboxes,
stream_tx: new_stream,

View file

@ -6,12 +6,14 @@
use std::sync::Arc;
use common::listener::{stream::NullIo, SessionData, SessionManager, SessionResult, SessionStream};
use common::{
core::BuildServer,
listener::{stream::NullIo, SessionData, SessionManager, SessionResult, SessionStream},
};
use imap_proto::{
protocol::{ProtocolVersion, SerializeResponse},
receiver::Receiver,
};
use jmap::JMAP;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio_rustls::server::TlsStream;
@ -51,9 +53,9 @@ impl<T: SessionStream> Session<T> {
tokio::select! {
result = tokio::time::timeout(
if !matches!(self.state, State::NotAuthenticated {..}) {
self.jmap.core.imap.timeout_auth
self.server.core.imap.timeout_auth
} else {
self.jmap.core.imap.timeout_unauth
self.server.core.imap.timeout_unauth
},
self.stream_rx.read(&mut buf)) => {
match result {
@ -138,17 +140,16 @@ impl<T: SessionStream> Session<T> {
// Split stream into read and write halves
let (stream_rx, stream_tx) = tokio::io::split(session.stream);
let jmap = JMAP::from(manager.imap.jmap_instance);
let server = manager.inner.build_server();
Ok(Session {
receiver: Receiver::with_max_request_size(jmap.core.imap.max_request_size),
receiver: Receiver::with_max_request_size(server.core.imap.max_request_size),
version: ProtocolVersion::Rev1,
state: State::NotAuthenticated { auth_failures: 0 },
is_tls,
is_condstore: false,
is_qresync: false,
jmap,
imap: manager.imap.imap_inner,
server,
instance: session.instance,
session_id: session.session_id,
in_flight: session.in_flight,
@ -196,8 +197,7 @@ impl<T: SessionStream> Session<T> {
let stream_tx = Arc::new(tokio::sync::Mutex::new(stream_tx));
Ok(Session {
jmap: self.jmap,
imap: self.imap,
server: self.server,
instance: self.instance,
receiver: self.receiver,
version: self.version,

View file

@ -4,19 +4,9 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use core::{ImapInstance, Inner, IMAP};
use std::{
collections::hash_map::RandomState,
sync::{Arc, LazyLock},
};
use std::sync::LazyLock;
use dashmap::DashMap;
use imap_proto::{protocol::capability::Capability, ResponseCode, StatusResponse};
use jmap::JmapInstance;
use utils::{
config::Config,
lru_cache::{LruCache, LruCached},
};
pub mod core;
pub mod op;
@ -39,33 +29,4 @@ pub(crate) static GREETING_WITHOUT_TLS: LazyLock<Vec<u8>> = LazyLock::new(|| {
.into_bytes()
});
impl IMAP {
pub async fn init(config: &mut Config, jmap_instance: JmapInstance) -> ImapInstance {
let shard_amount = config
.property::<u64>("cache.shard")
.unwrap_or(32)
.next_power_of_two() as usize;
let capacity = config.property("cache.capacity").unwrap_or(100);
let inner = Inner {
rate_limiter: DashMap::with_capacity_and_hasher_and_shard_amount(
capacity,
RandomState::default(),
shard_amount,
),
cache_account: LruCache::with_capacity(
config.property("cache.account.size").unwrap_or(2048),
),
cache_mailbox: LruCache::with_capacity(
config.property("cache.mailbox.size").unwrap_or(2048),
),
};
ImapInstance {
jmap_instance,
imap_inner: Arc::new(inner),
}
}
}
pub struct ImapError;

View file

@ -6,7 +6,7 @@
use std::{sync::Arc, time::Instant};
use common::{auth::AccessToken, listener::SessionStream};
use common::{auth::AccessToken, listener::SessionStream, MailboxId};
use directory::{backend::internal::PrincipalField, Permission, QueryBy};
use imap_proto::{
protocol::acl::{
@ -16,7 +16,10 @@ use imap_proto::{
Command, ResponseCode, StatusResponse,
};
use jmap::{auth::acl::EffectiveAcl, mailbox::set::SCHEMA};
use jmap::{
auth::acl::EffectiveAcl, changes::write::ChangeLog, mailbox::set::SCHEMA,
services::state::StateManager, JmapMethods,
};
use jmap_proto::{
object::{index::ObjectIndexBuilder, Object},
types::{
@ -33,7 +36,7 @@ use trc::AddContext;
use utils::map::bitmap::Bitmap;
use crate::{
core::{MailboxId, Session, SessionData, State},
core::{Session, SessionData, State},
op::ImapContext,
spawn_op,
};
@ -62,7 +65,7 @@ impl<T: SessionStream> Session<T> {
{
for item in acls {
if let Some(account_name) = data
.jmap
.server
.core
.storage
.directory
@ -244,7 +247,7 @@ impl<T: SessionStream> Session<T> {
// Obtain principal id
let acl_account_id = data
.jmap
.server
.core
.storage
.directory
@ -345,18 +348,18 @@ impl<T: SessionStream> Session<T> {
.with_current(values),
);
if !batch.is_empty() {
data.jmap
data.server
.write_batch(batch)
.await
.imap_ctx(&arguments.tag, trc::location!())?;
let mut changes = ChangeLogBuilder::new();
changes.log_update(Collection::Mailbox, mailbox_id);
let change_id = data
.jmap
.server
.commit_changes(mailbox.account_id, changes)
.await
.imap_ctx(&arguments.tag, trc::location!())?;
data.jmap
data.server
.broadcast_state_change(
StateChange::new(mailbox.account_id)
.with_change(DataType::Mailbox, change_id),
@ -365,11 +368,7 @@ impl<T: SessionStream> Session<T> {
}
// Invalidate ACLs
data.jmap
.core
.security
.access_tokens
.remove(&acl_account_id);
data.server.inner.data.access_tokens.remove(&acl_account_id);
trc::event!(
Imap(trc::ImapEvent::SetAcl),
@ -447,7 +446,7 @@ impl<T: SessionStream> SessionData<T> {
) -> trc::Result<(MailboxId, HashedValue<Object<Value>>, Arc<AccessToken>)> {
if let Some(mailbox) = self.get_mailbox_by_name(&arguments.mailbox_name) {
if let Some(values) = self
.jmap
.server
.get_property::<HashedValue<Object<Value>>>(
mailbox.account_id,
Collection::Mailbox,

View file

@ -14,11 +14,14 @@ use imap_proto::{
};
use crate::{
core::{ImapUidToId, MailboxId, SelectedMailbox, Session, SessionData},
core::{ImapUidToId, SelectedMailbox, Session, SessionData},
spawn_op,
};
use common::listener::SessionStream;
use jmap::email::ingest::{IngestEmail, IngestSource};
use common::{listener::SessionStream, MailboxId};
use jmap::{
email::ingest::{EmailIngest, IngestEmail, IngestSource},
services::state::StateManager,
};
use jmap_proto::types::{acl::Acl, keyword::Keyword, state::StateChange, type_state::DataType};
use mail_parser::MessageParser;
@ -89,8 +92,7 @@ impl<T: SessionStream> SessionData<T> {
// Obtain quota
let resource_token = self
.jmap
.core
.server
.get_cached_access_token(mailbox.account_id)
.await
.imap_ctx(&arguments.tag, trc::location!())?
@ -102,7 +104,7 @@ impl<T: SessionStream> SessionData<T> {
let mut last_change_id = None;
for message in arguments.messages {
match self
.jmap
.server
.email_ingest(IngestEmail {
raw_message: &message.message,
message: MessageParser::new().parse(&message.message),
@ -111,7 +113,7 @@ impl<T: SessionStream> SessionData<T> {
keywords: message.flags.into_iter().map(Keyword::from).collect(),
received_at: message.received_at.map(|d| d as u64),
source: IngestSource::Imap,
encrypt: self.jmap.core.jmap.encrypt && self.jmap.core.jmap.encrypt_append,
encrypt: self.server.core.jmap.encrypt && self.server.core.jmap.encrypt_append,
session_id: self.session_id,
})
.await
@ -142,7 +144,7 @@ impl<T: SessionStream> SessionData<T> {
// Broadcast changes
if let Some(change_id) = last_change_id {
self.jmap
self.server
.broadcast_state_change(
StateChange::new(account_id)
.with_change(DataType::Email, change_id)

View file

@ -11,6 +11,9 @@ use imap_proto::{
receiver::{self, Request},
Command, ResponseCode, StatusResponse,
};
use jmap::auth::{
authenticate::Authenticator, oauth::token::TokenHandler, rate_limit::RateLimiter,
};
use mail_parser::decoders::base64::base64_decode;
use mail_send::Credentials;
use std::sync::Arc;
@ -70,7 +73,7 @@ impl<T: SessionStream> Session<T> {
tag: String,
) -> trc::Result<()> {
// Throttle authentication requests
self.jmap
self.server
.is_auth_allowed_soft(&self.remote_addr)
.await
.map_err(|err| err.id(tag.clone()))?;
@ -78,17 +81,17 @@ impl<T: SessionStream> Session<T> {
// Authenticate
let access_token = match credentials {
Credentials::Plain { username, secret } | Credentials::XOauth2 { username, secret } => {
self.jmap
self.server
.authenticate_plain(&username, &secret, self.remote_addr, self.session_id)
.await
}
Credentials::OAuthBearer { token } => {
match self
.jmap
.server
.validate_access_token("access_token", &token)
.await
{
Ok((account_id, _, _)) => self.jmap.core.get_access_token(account_id).await,
Ok((account_id, _, _)) => self.server.get_access_token(account_id).await,
Err(err) => Err(err),
}
}
@ -96,7 +99,7 @@ impl<T: SessionStream> Session<T> {
.map_err(|err| {
if err.matches(trc::EventType::Auth(trc::AuthEvent::Failed)) {
let auth_failures = self.state.auth_failures();
if auth_failures < self.jmap.core.imap.max_auth_failures {
if auth_failures < self.server.core.imap.max_auth_failures {
self.state = State::NotAuthenticated {
auth_failures: auth_failures + 1,
};
@ -127,7 +130,7 @@ impl<T: SessionStream> Session<T> {
// Cache access token
let access_token = Arc::new(access_token);
self.jmap.core.cache_access_token(access_token.clone());
self.server.cache_access_token(access_token.clone());
// Create session
self.state = State::Authenticated {

View file

@ -28,7 +28,7 @@ impl<T: SessionStream> Session<T> {
Imap(trc::ImapEvent::Capabilities),
SpanId = self.session_id,
Tls = self.is_tls,
Strict = !self.jmap.core.imap.allow_plain_auth,
Strict = !self.server.core.imap.allow_plain_auth,
Elapsed = op_start.elapsed()
);

View file

@ -13,11 +13,17 @@ use imap_proto::{
};
use crate::{
core::{MailboxId, SelectedMailbox, Session, SessionData},
core::{SelectedMailbox, Session, SessionData},
spawn_op,
};
use common::listener::SessionStream;
use jmap::{email::set::TagManager, mailbox::UidMailbox};
use common::{listener::SessionStream, MailboxId};
use jmap::{
changes::write::ChangeLog,
email::{copy::EmailCopy, ingest::EmailIngest, set::TagManager},
mailbox::UidMailbox,
services::state::StateManager,
JmapMethods,
};
use jmap_proto::{
error::set::SetErrorType,
types::{
@ -200,7 +206,7 @@ impl<T: SessionStream> SessionData<T> {
for uid_mailbox in mailboxes.inner_tags_mut() {
if uid_mailbox.uid == 0 {
let assigned_uid = self
.jmap
.server
.assign_imap_uid(account_id, uid_mailbox.mailbox_id)
.await
.imap_ctx(&arguments.tag, trc::location!())?;
@ -219,13 +225,13 @@ impl<T: SessionStream> SessionData<T> {
mailboxes.update_batch(&mut batch, Property::MailboxIds);
if changelog.change_id == u64::MAX {
changelog.change_id = self
.jmap
.server
.assign_change_id(account_id)
.await
.imap_ctx(&arguments.tag, trc::location!())?;
}
batch.value(Property::Cid, changelog.change_id, F_VALUE);
self.jmap
self.server
.write_batch(batch)
.await
.imap_ctx(&arguments.tag, trc::location!())?;
@ -242,8 +248,7 @@ impl<T: SessionStream> SessionData<T> {
let mut dest_change_id = None;
let dest_account_id = dest_mailbox.account_id;
let resource_token = self
.jmap
.core
.server
.get_cached_access_token(dest_account_id)
.await
.imap_ctx(&arguments.tag, trc::location!())?
@ -251,7 +256,7 @@ impl<T: SessionStream> SessionData<T> {
let mut destroy_ids = RoaringBitmap::new();
for (id, imap_id) in ids {
match self
.jmap
.server
.copy_message(
src_account_id,
id,
@ -303,7 +308,7 @@ impl<T: SessionStream> SessionData<T> {
// Broadcast changes on destination account
if let Some(change_id) = dest_change_id {
self.jmap
self.server
.broadcast_state_change(
StateChange::new(dest_account_id)
.with_change(DataType::Email, change_id)
@ -317,11 +322,11 @@ impl<T: SessionStream> SessionData<T> {
// Write changes on source account
if !changelog.is_empty() {
let change_id = self
.jmap
.server
.commit_changes(src_mailbox.id.account_id, changelog)
.await
.imap_ctx(&arguments.tag, trc::location!())?;
self.jmap
self.server
.broadcast_state_change(
StateChange::new(src_mailbox.id.account_id)
.with_change(DataType::Email, change_id)
@ -423,7 +428,7 @@ impl<T: SessionStream> SessionData<T> {
) -> trc::Result<Option<(TagManager<UidMailbox>, u32)>> {
// Obtain mailbox tags
if let (Some(mailboxes), Some(thread_id)) = (
self.jmap
self.server
.get_property::<HashedValue<Vec<UidMailbox>>>(
account_id,
Collection::Email,
@ -431,7 +436,7 @@ impl<T: SessionStream> SessionData<T> {
Property::MailboxIds,
)
.await?,
self.jmap
self.server
.get_property::<u32>(account_id, Collection::Email, id, Property::ThreadId)
.await?,
) {

View file

@ -7,18 +7,20 @@
use std::time::Instant;
use crate::{
core::{Account, Mailbox, Session, SessionData},
core::{Session, SessionData},
op::ImapContext,
spawn_op,
};
use common::listener::SessionStream;
use common::{listener::SessionStream, Account, Mailbox};
use directory::Permission;
use imap_proto::{
protocol::{create::Arguments, list::Attribute},
receiver::Request,
Command, ResponseCode, StatusResponse,
};
use jmap::mailbox::set::SCHEMA;
use jmap::{
changes::write::ChangeLog, mailbox::set::SCHEMA, services::state::StateManager, JmapMethods,
};
use jmap_proto::{
object::{index::ObjectIndexBuilder, Object},
types::{
@ -75,7 +77,7 @@ impl<T: SessionStream> SessionData<T> {
// Build batch
let mut changes = self
.jmap
.server
.begin_changes(params.account_id)
.await
.imap_ctx(&arguments.tag, trc::location!())?;
@ -102,7 +104,7 @@ impl<T: SessionStream> SessionData<T> {
.create_document()
.custom(ObjectIndexBuilder::new(SCHEMA).with_changes(mailbox));
let mailbox_id = self
.jmap
.server
.write_batch_expect_id(batch)
.await
.imap_ctx(&arguments.tag, trc::location!())?;
@ -118,13 +120,13 @@ impl<T: SessionStream> SessionData<T> {
.with_account_id(params.account_id)
.with_collection(Collection::Mailbox)
.custom(changes);
self.jmap
self.server
.write_batch(batch)
.await
.imap_ctx(&arguments.tag, trc::location!())?;
// Broadcast changes
self.jmap
self.server
.broadcast_state_change(
StateChange::new(params.account_id).with_change(DataType::Mailbox, change_id),
)
@ -205,7 +207,7 @@ impl<T: SessionStream> SessionData<T> {
};
let effective_id = self
.jmap
.server
.core
.jmap
.default_folders
@ -271,7 +273,7 @@ impl<T: SessionStream> SessionData<T> {
return Err(trc::ImapEvent::Error
.into_err()
.details("Invalid empty path item."));
} else if path_item.len() > self.jmap.core.jmap.mailbox_name_max_len {
} else if path_item.len() > self.server.core.jmap.mailbox_name_max_len {
return Err(trc::ImapEvent::Error
.into_err()
.details("Mailbox name is too long."));
@ -279,7 +281,7 @@ impl<T: SessionStream> SessionData<T> {
path.push(path_item);
}
if path.len() > self.jmap.core.jmap.mailbox_max_depth {
if path.len() > self.server.core.jmap.mailbox_max_depth {
return Err(trc::ImapEvent::Error
.into_err()
.details("Mailbox path is too deep."));
@ -295,7 +297,7 @@ impl<T: SessionStream> SessionData<T> {
let (account_id, path) = {
let mailboxes = self.mailboxes.lock();
let first_path_item = path.first().unwrap();
let account = if first_path_item == &self.jmap.core.jmap.shared_folder {
let account = if first_path_item == &self.server.core.jmap.shared_folder {
// Shared Folders/<username>/<folder>
if path.len() < 3 {
return Err(trc::ImapEvent::Error
@ -391,7 +393,7 @@ impl<T: SessionStream> SessionData<T> {
special_use: if let Some(mailbox_role) = mailbox_role {
// Make sure role is unique
if !self
.jmap
.server
.filter(
account_id,
Collection::Mailbox,

View file

@ -15,6 +15,7 @@ use directory::Permission;
use imap_proto::{
protocol::delete::Arguments, receiver::Request, Command, ResponseCode, StatusResponse,
};
use jmap::{changes::write::ChangeLog, mailbox::set::MailboxSet, services::state::StateManager};
use jmap_proto::types::{state::StateChange, type_state::DataType};
use store::write::log::ChangeLogBuilder;
@ -76,7 +77,7 @@ impl<T: SessionStream> SessionData<T> {
.imap_ctx(&arguments.tag, trc::location!())?;
let mut changelog = ChangeLogBuilder::new();
let did_remove_emails = match self
.jmap
.server
.mailbox_destroy(account_id, mailbox_id, &mut changelog, &access_token, true)
.await
.imap_ctx(&arguments.tag, trc::location!())?
@ -93,13 +94,13 @@ impl<T: SessionStream> SessionData<T> {
// Write changes
let change_id = self
.jmap
.server
.commit_changes(account_id, changelog)
.await
.imap_ctx(&arguments.tag, trc::location!())?;
// Broadcast changes
self.jmap
self.server
.broadcast_state_change(if did_remove_emails {
StateChange::new(account_id)
.with_change(DataType::Mailbox, change_id)

View file

@ -15,9 +15,15 @@ use imap_proto::{
};
use trc::AddContext;
use crate::core::{ImapId, SavedSearch, SelectedMailbox, Session, SessionData};
use common::listener::SessionStream;
use jmap::{email::set::TagManager, mailbox::UidMailbox};
use crate::core::{SavedSearch, SelectedMailbox, Session, SessionData};
use common::{listener::SessionStream, ImapId};
use jmap::{
changes::write::ChangeLog,
email::{delete::EmailDeletion, set::TagManager},
mailbox::UidMailbox,
services::state::StateManager,
JmapMethods,
};
use jmap_proto::types::{
acl::Acl, collection::Collection, id::Id, keyword::Keyword, property::Property,
state::StateChange, type_state::DataType,
@ -118,7 +124,7 @@ impl<T: SessionStream> SessionData<T> {
// Obtain message ids
let account_id = mailbox.id.account_id;
let mut deleted_ids = self
.jmap
.server
.get_tag(
account_id,
Collection::Email,
@ -129,7 +135,7 @@ impl<T: SessionStream> SessionData<T> {
.caused_by(trc::location!())?
.unwrap_or_default()
& self
.jmap
.server
.get_tag(
account_id,
Collection::Email,
@ -167,8 +173,8 @@ impl<T: SessionStream> SessionData<T> {
// Write changes on source account
if !changelog.is_empty() {
let change_id = self.jmap.commit_changes(account_id, changelog).await?;
self.jmap
let change_id = self.server.commit_changes(account_id, changelog).await?;
self.server
.broadcast_state_change(
StateChange::new(account_id)
.with_change(DataType::Email, change_id)
@ -192,7 +198,7 @@ impl<T: SessionStream> SessionData<T> {
let mut destroy_ids = RoaringBitmap::new();
for (id, mailbox_ids) in self
.jmap
.server
.get_properties::<HashedValue<Vec<UidMailbox>>, _, _>(
account_id,
Collection::Email,
@ -208,7 +214,7 @@ impl<T: SessionStream> SessionData<T> {
if mailboxes.current().len() > 1 {
// Remove deleted flag
let (mut keywords, thread_id) = if let (Some(keywords), Some(thread_id)) = (
self.jmap
self.server
.get_property::<HashedValue<Vec<Keyword>>>(
account_id,
Collection::Email,
@ -217,7 +223,7 @@ impl<T: SessionStream> SessionData<T> {
)
.await
.caused_by(trc::location!())?,
self.jmap
self.server
.get_property::<u32>(
account_id,
Collection::Email,
@ -245,10 +251,10 @@ impl<T: SessionStream> SessionData<T> {
mailboxes.update_batch(&mut batch, Property::MailboxIds);
keywords.update_batch(&mut batch, Property::Keywords);
if changelog.change_id == u64::MAX {
changelog.change_id = self.jmap.assign_change_id(account_id).await?
changelog.change_id = self.server.assign_change_id(account_id).await?
}
batch.value(Property::Cid, changelog.change_id, F_VALUE);
match self.jmap.write_batch(batch).await {
match self.server.write_batch(batch).await {
Ok(_) => {
changelog.log_update(Collection::Email, Id::from_parts(thread_id, id));
changelog.log_child_update(Collection::Mailbox, mailbox_id.mailbox_id);
@ -268,7 +274,7 @@ impl<T: SessionStream> SessionData<T> {
if !destroy_ids.is_empty() {
// Delete message from all mailboxes
let (changes, _) = self
.jmap
.server
.emails_tombstone(account_id, destroy_ids)
.await
.caused_by(trc::location!())?;

View file

@ -26,7 +26,13 @@ use imap_proto::{
receiver::Request,
Command, ResponseCode, ResponseType, StatusResponse,
};
use jmap::email::metadata::MessageMetadata;
use jmap::{
blob::download::BlobDownload,
changes::{get::ChangesLookup, write::ChangeLog},
email::metadata::MessageMetadata,
services::state::StateManager,
JmapMethods,
};
use jmap_proto::types::{
acl::Acl, collection::Collection, id::Id, keyword::Keyword, property::Property,
state::StateChange, type_state::DataType,
@ -127,7 +133,7 @@ impl<T: SessionStream> SessionData<T> {
if let Some(changed_since) = arguments.changed_since {
// Obtain changes since the modseq.
let changelog = self
.jmap
.server
.changes_(
account_id,
Collection::Email,
@ -280,7 +286,7 @@ impl<T: SessionStream> SessionData<T> {
for (seqnum, uid, id) in ids {
// Obtain attributes and keywords
let (email, keywords) = if let (Some(email), Some(keywords)) = (
self.jmap
self.server
.get_property::<Bincode<MessageMetadata>>(
account_id,
Collection::Email,
@ -289,7 +295,7 @@ impl<T: SessionStream> SessionData<T> {
)
.await
.imap_ctx(&arguments.tag, trc::location!())?,
self.jmap
self.server
.get_property::<HashedValue<Vec<Keyword>>>(
account_id,
Collection::Email,
@ -316,7 +322,7 @@ impl<T: SessionStream> SessionData<T> {
let raw_message = if needs_blobs {
// Retrieve raw message if needed
match self
.jmap
.server
.get_blob(&email.blob_hash, 0..usize::MAX)
.await
.imap_ctx(&arguments.tag, trc::location!())?
@ -347,7 +353,7 @@ impl<T: SessionStream> SessionData<T> {
set_seen_flags && !keywords.inner.iter().any(|k| k == &Keyword::Seen);
let thread_id = if needs_thread_id || set_seen_flag {
if let Some(thread_id) = self
.jmap
.server
.get_property::<u32>(account_id, Collection::Email, id, Property::ThreadId)
.await
.imap_ctx(&arguments.tag, trc::location!())?
@ -479,7 +485,7 @@ impl<T: SessionStream> SessionData<T> {
}
Attribute::ModSeq => {
if let Ok(Some(modseq)) = self
.jmap
.server
.get_property::<u64>(account_id, Collection::Email, id, Property::Cid)
.await
{
@ -524,7 +530,7 @@ impl<T: SessionStream> SessionData<T> {
// Set Seen ids
if !set_seen_ids.is_empty() {
let mut changelog = self
.jmap
.server
.begin_changes(account_id)
.await
.imap_ctx(&arguments.tag, trc::location!())?;
@ -539,7 +545,7 @@ impl<T: SessionStream> SessionData<T> {
.value(Property::Keywords, keywords.inner, F_VALUE)
.value(Property::Keywords, Keyword::Seen, F_BITMAP)
.value(Property::Cid, changelog.change_id, F_VALUE);
match self.jmap.write_batch(batch).await {
match self.server.write_batch(batch).await {
Ok(_) => {
changelog.log_update(Collection::Email, id);
}
@ -553,12 +559,12 @@ impl<T: SessionStream> SessionData<T> {
if !changelog.is_empty() {
// Write changes
let change_id = self
.jmap
.server
.commit_changes(account_id, changelog)
.await
.imap_ctx(&arguments.tag, trc::location!())?;
modseq = change_id.into();
self.jmap
self.server
.broadcast_state_change(
StateChange::new(account_id).with_change(DataType::Email, change_id),
)

View file

@ -20,6 +20,7 @@ use imap_proto::{
};
use common::listener::SessionStream;
use jmap::{changes::get::ChangesLookup, services::state::StateManager};
use jmap_proto::types::{collection::Collection, type_state::DataType};
use store::query::log::Query;
use tokio::io::AsyncReadExt;
@ -53,7 +54,7 @@ impl<T: SessionStream> Session<T> {
// Register with state manager
let mut change_rx = self
.jmap
.server
.subscribe_state_manager(data.account_id, types)
.await
.imap_ctx(&request.tag, trc::location!())?;
@ -72,7 +73,7 @@ impl<T: SessionStream> Session<T> {
let mut buf = vec![0; 4];
loop {
tokio::select! {
result = tokio::time::timeout(self.jmap.core.imap.timeout_idle, self.stream_rx.read_exact(&mut buf)) => {
result = tokio::time::timeout(self.server.core.imap.timeout_idle, self.stream_rx.read_exact(&mut buf)) => {
match result {
Ok(Ok(bytes_read)) => {
if bytes_read > 0 {
@ -202,7 +203,7 @@ impl<T: SessionStream> SessionData<T> {
// Obtain changed messages
let changelog = self
.jmap
.server
.changes_(
mailbox.id.account_id,
Collection::Email,

View file

@ -173,10 +173,10 @@ impl<T: SessionStream> SessionData<T> {
if let Some(prefix) = &account.prefix {
if !added_shared_folder {
if !filter_subscribed
&& matches_pattern(&patterns, &self.jmap.core.jmap.shared_folder)
&& matches_pattern(&patterns, &self.server.core.jmap.shared_folder)
{
list_items.push(ListItem {
mailbox_name: self.jmap.core.jmap.shared_folder.clone(),
mailbox_name: self.server.core.jmap.shared_folder.clone(),
attributes: if include_children {
vec![Attribute::HasChildren, Attribute::NoSelect]
} else {

View file

@ -30,7 +30,7 @@ impl<T: SessionStream> Session<T> {
.serialize(
Response {
shared_prefix: if self.state.session_data().mailboxes.lock().len() > 1 {
self.jmap.core.jmap.shared_folder.clone().into()
self.server.core.jmap.shared_folder.clone().into()
} else {
None
},

View file

@ -15,7 +15,10 @@ use directory::Permission;
use imap_proto::{
protocol::rename::Arguments, receiver::Request, Command, ResponseCode, StatusResponse,
};
use jmap::{auth::acl::EffectiveAcl, mailbox::set::SCHEMA};
use jmap::{
auth::acl::EffectiveAcl, changes::write::ChangeLog, mailbox::set::SCHEMA,
services::state::StateManager, JmapMethods,
};
use jmap_proto::{
object::{index::ObjectIndexBuilder, Object},
types::{
@ -92,7 +95,7 @@ impl<T: SessionStream> SessionData<T> {
// Obtain mailbox
let mailbox = self
.jmap
.server
.get_property::<HashedValue<Object<Value>>>(
params.account_id,
Collection::Mailbox,
@ -133,7 +136,7 @@ impl<T: SessionStream> SessionData<T> {
// Build batch
let mut changes = self
.jmap
.server
.begin_changes(params.account_id)
.await
.imap_ctx(&arguments.tag, trc::location!())?;
@ -159,7 +162,7 @@ impl<T: SessionStream> SessionData<T> {
);
let mailbox_id = self
.jmap
.server
.write_batch_expect_id(batch)
.await
.imap_ctx(&arguments.tag, trc::location!())?;
@ -191,13 +194,13 @@ impl<T: SessionStream> SessionData<T> {
let change_id = changes.change_id;
batch.custom(changes);
self.jmap
self.server
.write_batch(batch)
.await
.imap_ctx(&arguments.tag, trc::location!())?;
// Broadcast changes
self.jmap
self.server
.broadcast_state_change(
StateChange::new(params.account_id).with_change(DataType::Mailbox, change_id),
)

View file

@ -6,7 +6,7 @@
use std::{sync::Arc, time::Instant};
use common::listener::SessionStream;
use common::{listener::SessionStream, ImapId};
use directory::Permission;
use imap_proto::{
protocol::{
@ -16,6 +16,7 @@ use imap_proto::{
receiver::Request,
Command, StatusResponse,
};
use jmap::{changes::get::ChangesLookup, JmapMethods};
use jmap_proto::types::{collection::Collection, id::Id, keyword::Keyword, property::Property};
use mail_parser::HeaderName;
use nlp::language::Language;
@ -29,7 +30,7 @@ use tokio::sync::watch;
use trc::AddContext;
use crate::{
core::{ImapId, MailboxState, SavedSearch, SelectedMailbox, Session, SessionData},
core::{SavedSearch, SelectedMailbox, Session, SessionData},
spawn_op,
};
@ -142,7 +143,7 @@ impl<T: SessionStream> SessionData<T> {
let mut imap_ids = Vec::with_capacity(results_len);
let is_sort = if let Some(sort) = arguments.sort {
mailbox.map_search_results(
self.jmap
self.server
.core
.storage
.data
@ -260,7 +261,7 @@ impl<T: SessionStream> SessionData<T> {
// Obtain message ids
let mut filters = Vec::with_capacity(imap_filter.len() + 1);
let message_ids = self
.jmap
.server
.get_tag(
mailbox.id.account_id,
Collection::Email,
@ -290,7 +291,7 @@ impl<T: SessionStream> SessionData<T> {
fts_filters.push(FtsFilter::has_text_detect(
Field::Body,
text,
self.jmap.core.jmap.default_language,
self.server.core.jmap.default_language,
));
}
search::Filter::Cc(text) => {
@ -350,7 +351,7 @@ impl<T: SessionStream> SessionData<T> {
fts_filters.push(FtsFilter::has_text_detect(
Field::Header(HeaderName::Subject),
text,
self.jmap.core.jmap.default_language,
self.server.core.jmap.default_language,
));
}
search::Filter::Text(text) => {
@ -378,17 +379,17 @@ impl<T: SessionStream> SessionData<T> {
fts_filters.push(FtsFilter::has_text_detect(
Field::Header(HeaderName::Subject),
&text,
self.jmap.core.jmap.default_language,
self.server.core.jmap.default_language,
));
fts_filters.push(FtsFilter::has_text_detect(
Field::Body,
&text,
self.jmap.core.jmap.default_language,
self.server.core.jmap.default_language,
));
fts_filters.push(FtsFilter::has_text_detect(
Field::Attachment,
text,
self.jmap.core.jmap.default_language,
self.server.core.jmap.default_language,
));
fts_filters.push(FtsFilter::End);
}
@ -416,7 +417,7 @@ impl<T: SessionStream> SessionData<T> {
}
filters.push(query::Filter::is_in_set(
self.jmap
self.server
.fts_filter(mailbox.id.account_id, Collection::Email, fts_filters)
.await?,
));
@ -612,7 +613,7 @@ impl<T: SessionStream> SessionData<T> {
search::Filter::ModSeq((modseq, _)) => {
let mut set = RoaringBitmap::new();
for change in self
.jmap
.server
.changes_(
mailbox.id.account_id,
Collection::Email,
@ -658,7 +659,7 @@ impl<T: SessionStream> SessionData<T> {
}
// Run query
self.jmap
self.server
.filter(mailbox.id.account_id, Collection::Email, filters)
.await
.map(|res| (res, include_highest_modseq))
@ -738,23 +739,6 @@ impl SelectedMailbox {
}
}
impl MailboxState {
pub fn map_result_id(&self, document_id: u32, is_uid: bool) -> Option<(u32, ImapId)> {
if let Some(imap_id) = self.id_to_imap.get(&document_id) {
Some((if is_uid { imap_id.uid } else { imap_id.seqnum }, *imap_id))
} else if is_uid {
self.next_state.as_ref().and_then(|s| {
s.next_state
.id_to_imap
.get(&document_id)
.map(|imap_id| (imap_id.uid, *imap_id))
})
} else {
None
}
}
}
impl SavedSearch {
pub async fn unwrap(&self) -> Option<Arc<Vec<ImapId>>> {
match self {

View file

@ -47,35 +47,39 @@ impl<T: SessionStream> Session<T> {
if let Some(mailbox) = data.get_mailbox_by_name(&arguments.mailbox_name) {
// Try obtaining the mailbox from the cache
let state = {
let modseq = data
.get_modseq(mailbox.account_id)
.await
.imap_ctx(&arguments.tag, trc::location!())?;
if let Some(cached_state) =
self.imap
.cache_mailbox
.get(&mailbox)
.and_then(|cached_state| {
if cached_state.modseq.unwrap_or(0) >= modseq.unwrap_or(0) {
Some(cached_state)
} else {
None
}
})
let state =
{
cached_state.as_ref().clone()
} else {
let new_state = Arc::new(
data.fetch_messages(&mailbox)
.await
.imap_ctx(&arguments.tag, trc::location!())?,
);
self.imap.cache_mailbox.insert(mailbox, new_state.clone());
new_state.as_ref().clone()
}
};
let modseq = data
.get_modseq(mailbox.account_id)
.await
.imap_ctx(&arguments.tag, trc::location!())?;
if let Some(cached_state) =
self.server.inner.data.mailbox_cache.get(&mailbox).and_then(
|cached_state| {
if cached_state.modseq.unwrap_or(0) >= modseq.unwrap_or(0) {
Some(cached_state)
} else {
None
}
},
)
{
cached_state.as_ref().clone()
} else {
let new_state = Arc::new(
data.fetch_messages(&mailbox)
.await
.imap_ctx(&arguments.tag, trc::location!())?,
);
self.server
.inner
.data
.mailbox_cache
.insert(mailbox, new_state.clone());
new_state.as_ref().clone()
}
};
// Synchronize messages
let closed_previous = self.state.close_mailbox();

View file

@ -7,11 +7,11 @@
use std::{sync::Arc, time::Instant};
use crate::{
core::{Mailbox, Session, SessionData},
core::{Session, SessionData},
op::ImapContext,
spawn_op,
};
use common::listener::SessionStream;
use common::{listener::SessionStream, Mailbox};
use directory::Permission;
use imap_proto::{
parser::PushUnique,
@ -19,6 +19,7 @@ use imap_proto::{
receiver::Request,
Command, ResponseCode, StatusResponse,
};
use jmap::JmapMethods;
use jmap_proto::{
object::Object,
types::{collection::Collection, id::Id, keyword::Keyword, property::Property, value::Value},
@ -86,11 +87,11 @@ impl<T: SessionStream> SessionData<T> {
mailbox
} else {
// Some IMAP clients will try to get the status of a mailbox with the NoSelect flag
return if mailbox_name == self.jmap.core.jmap.shared_folder
return if mailbox_name == self.server.core.jmap.shared_folder
|| mailbox_name
.split_once('/')
.map_or(false, |(base_name, path)| {
base_name == self.jmap.core.jmap.shared_folder && !path.contains('/')
base_name == self.server.core.jmap.shared_folder && !path.contains('/')
})
{
Ok(StatusItem {
@ -211,7 +212,7 @@ impl<T: SessionStream> SessionData<T> {
// Retrieve latest values
let mut values_update = Vec::with_capacity(items_update.len());
let mailbox_message_ids = self
.jmap
.server
.get_tag(
mailbox.account_id,
Collection::Email,
@ -222,7 +223,7 @@ impl<T: SessionStream> SessionData<T> {
.caused_by(trc::location!())?
.map(Arc::new);
let message_ids = self
.jmap
.server
.get_document_ids(mailbox.account_id, Collection::Email)
.await
.caused_by(trc::location!())?;
@ -232,7 +233,7 @@ impl<T: SessionStream> SessionData<T> {
Status::Messages => mailbox_message_ids.as_ref().map(|v| v.len()).unwrap_or(0),
Status::UidNext => {
(self
.jmap
.server
.core
.storage
.data
@ -247,7 +248,7 @@ impl<T: SessionStream> SessionData<T> {
+ 1) as u64
}
Status::UidValidity => self
.jmap
.server
.get_property::<Object<Value>>(
mailbox.account_id,
Collection::Mailbox,
@ -270,7 +271,7 @@ impl<T: SessionStream> SessionData<T> {
(&message_ids, &mailbox_message_ids)
{
if let Some(mut seen) = self
.jmap
.server
.get_tag(
mailbox.account_id,
Collection::Email,
@ -293,7 +294,7 @@ impl<T: SessionStream> SessionData<T> {
Status::Deleted => {
if let (Some(mailbox_message_ids), Some(mut deleted)) = (
&mailbox_message_ids,
self.jmap
self.server
.get_tag(
mailbox.account_id,
Collection::Email,
@ -378,7 +379,7 @@ impl<T: SessionStream> SessionData<T> {
message_ids: &Arc<RoaringBitmap>,
) -> trc::Result<u32> {
let mut total_size = 0u32;
self.jmap
self.server
.core
.storage
.data

View file

@ -22,7 +22,13 @@ use imap_proto::{
receiver::Request,
Command, ResponseCode, ResponseType, StatusResponse,
};
use jmap::{email::set::TagManager, mailbox::UidMailbox};
use jmap::{
changes::{get::ChangesLookup, write::ChangeLog},
email::set::TagManager,
mailbox::UidMailbox,
services::state::StateManager,
JmapMethods,
};
use jmap_proto::types::{
acl::Acl, collection::Collection, id::Id, keyword::Keyword, property::Property,
state::StateChange, type_state::DataType,
@ -110,7 +116,7 @@ impl<T: SessionStream> SessionData<T> {
if let Some(unchanged_since) = arguments.unchanged_since {
// Obtain changes since the modseq.
let changelog = self
.jmap
.server
.changes_(
account_id,
Collection::Email,
@ -192,7 +198,7 @@ impl<T: SessionStream> SessionData<T> {
loop {
// Obtain current keywords
let (mut keywords, thread_id) = if let (Some(keywords), Some(thread_id)) = (
self.jmap
self.server
.get_property::<HashedValue<Vec<Keyword>>>(
account_id,
Collection::Email,
@ -201,7 +207,7 @@ impl<T: SessionStream> SessionData<T> {
)
.await
.imap_ctx(response.tag.as_ref().unwrap(), trc::location!())?,
self.jmap
self.server
.get_property::<u32>(account_id, Collection::Email, *id, Property::ThreadId)
.await
.imap_ctx(response.tag.as_ref().unwrap(), trc::location!())?,
@ -253,18 +259,18 @@ impl<T: SessionStream> SessionData<T> {
keywords.update_batch(&mut batch, Property::Keywords);
if changelog.change_id == u64::MAX {
changelog.change_id = self
.jmap
.server
.assign_change_id(account_id)
.await
.imap_ctx(response.tag.as_ref().unwrap(), trc::location!())?
}
batch.value(Property::Cid, changelog.change_id, F_VALUE);
match self.jmap.write_batch(batch).await {
match self.server.write_batch(batch).await {
Ok(_) => {
// Set all current mailboxes as changed if the Seen tag changed
if seen_changed {
if let Some(mailboxes) = self
.jmap
.server
.get_property::<Vec<UidMailbox>>(
account_id,
Collection::Email,
@ -335,11 +341,11 @@ impl<T: SessionStream> SessionData<T> {
// Write changes
if !changelog.is_empty() {
let change_id = self
.jmap
.server
.commit_changes(account_id, changelog)
.await
.imap_ctx(response.tag.as_ref().unwrap(), trc::location!())?;
self.jmap
self.server
.broadcast_state_change(if !changed_mailboxes.is_empty() {
StateChange::new(account_id)
.with_change(DataType::Email, change_id)

View file

@ -13,7 +13,12 @@ use crate::{
use common::listener::SessionStream;
use directory::Permission;
use imap_proto::{receiver::Request, Command, ResponseCode, StatusResponse};
use jmap::mailbox::set::{MailboxSubscribe, SCHEMA};
use jmap::{
changes::write::ChangeLog,
mailbox::set::{MailboxSubscribe, SCHEMA},
services::state::StateManager,
JmapMethods,
};
use jmap_proto::{
object::{index::ObjectIndexBuilder, Object},
types::{
@ -100,7 +105,7 @@ impl<T: SessionStream> SessionData<T> {
// Obtain mailbox
let mailbox = self
.jmap
.server
.get_property::<HashedValue<Object<Value>>>(
account_id,
Collection::Mailbox,
@ -122,7 +127,7 @@ impl<T: SessionStream> SessionData<T> {
if let Some(value) = mailbox.inner.mailbox_subscribe(self.account_id, subscribe) {
// Build batch
let mut changes = self
.jmap
.server
.begin_changes(account_id)
.await
.imap_ctx(&tag, trc::location!())?;
@ -142,13 +147,13 @@ impl<T: SessionStream> SessionData<T> {
let change_id = changes.change_id;
batch.custom(changes);
self.jmap
self.server
.write_batch(batch)
.await
.imap_ctx(&tag, trc::location!())?;
// Broadcast changes
self.jmap
self.server
.broadcast_state_change(
StateChange::new(account_id).with_change(DataType::Mailbox, change_id),
)

View file

@ -21,6 +21,7 @@ use imap_proto::{
receiver::Request,
Command, StatusResponse,
};
use jmap::email::cache::ThreadCache;
use trc::AddContext;
impl<T: SessionStream> Session<T> {
@ -80,7 +81,7 @@ impl<T: SessionStream> SessionData<T> {
// Lock the cache
let thread_ids = self
.jmap
.server
.get_cached_thread_ids(mailbox.id.account_id, result_set.results.iter())
.await
.caused_by(trc::location!())?;

View file

@ -6,18 +6,34 @@
use std::fmt::Write;
use common::manager::webadmin::Resource;
use common::{manager::webadmin::Resource, Server};
use directory::{backend::internal::PrincipalField, QueryBy};
use quick_xml::events::Event;
use quick_xml::Reader;
use utils::url_params::UrlParams;
use crate::{api::http::ToHttpResponse, JMAP};
use crate::api::http::ToHttpResponse;
use super::{HttpRequest, HttpResponse};
use std::future::Future;
impl JMAP {
pub async fn handle_autoconfig_request(&self, req: &HttpRequest) -> trc::Result<HttpResponse> {
pub trait Autoconfig: Sync + Send {
fn handle_autoconfig_request(
&self,
req: &HttpRequest,
) -> impl Future<Output = trc::Result<HttpResponse>> + Send;
fn handle_autodiscover_request(
&self,
body: Option<Vec<u8>>,
) -> impl Future<Output = trc::Result<HttpResponse>> + Send;
fn autoconfig_parameters<'x>(
&self,
emailaddress: &'x str,
) -> impl Future<Output = trc::Result<(String, String, &'x str)>> + Send;
}
impl Autoconfig for Server {
async fn handle_autoconfig_request(&self, req: &HttpRequest) -> trc::Result<HttpResponse> {
// Obtain parameters
let params = UrlParams::new(req.uri().query());
let emailaddress = params
@ -73,7 +89,7 @@ impl JMAP {
)
}
pub async fn handle_autodiscover_request(
async fn handle_autodiscover_request(
&self,
body: Option<Vec<u8>>,
) -> trc::Result<HttpResponse> {

View file

@ -9,7 +9,7 @@ use std::{
time::{Duration, Instant},
};
use common::auth::AccessToken;
use common::{auth::AccessToken, Server};
use http_body_util::{combinators::BoxBody, StreamBody};
use hyper::{
body::{Bytes, Frame},
@ -18,9 +18,10 @@ use hyper::{
use jmap_proto::types::type_state::DataType;
use utils::map::bitmap::Bitmap;
use crate::{JMAP, LONG_SLUMBER};
use crate::{services::state::StateManager, LONG_SLUMBER};
use super::{HttpRequest, HttpResponse, HttpResponseBody, StateChangeResponse};
use std::future::Future;
struct Ping {
interval: Duration,
@ -28,8 +29,16 @@ struct Ping {
payload: Bytes,
}
impl JMAP {
pub async fn handle_event_source(
pub trait EventSourceHandler: Sync + Send {
fn handle_event_source(
&self,
req: HttpRequest,
access_token: Arc<AccessToken>,
) -> impl Future<Output = trc::Result<HttpResponse>> + Send;
}
impl EventSourceHandler for Server {
async fn handle_event_source(
&self,
req: HttpRequest,
access_token: Arc<AccessToken>,

View file

@ -8,10 +8,12 @@ use std::{borrow::Cow, net::IpAddr, sync::Arc};
use common::{
auth::AccessToken,
core::BuildServer,
expr::{functions::ResolveVariable, *},
ipc::StateEvent,
listener::{ServerInstance, SessionData, SessionManager, SessionStream},
manager::webadmin::Resource,
Core,
Inner, Server,
};
use directory::Permission;
use http_body_util::{BodyExt, Full};
@ -29,17 +31,26 @@ use jmap_proto::{
response::Response,
types::{blob::BlobId, id::Id},
};
use std::future::Future;
use crate::{
auth::{authenticate::HttpHeaders, oauth::OAuthMetadata},
blob::{DownloadResponse, UploadResponse},
services::state,
JmapInstance, JMAP,
api::management::enterprise::telemetry::TelemetryApi,
auth::{
authenticate::{Authenticator, HttpHeaders},
oauth::{auth::OAuthApiHandler, token::TokenHandler, OAuthMetadata},
rate_limit::RateLimiter,
},
blob::{download::BlobDownload, upload::BlobUpload, DownloadResponse, UploadResponse},
websocket::upgrade::WebSocketUpgrade,
};
use super::{
management::ManagementApiError, HtmlResponse, HttpRequest, HttpResponse, HttpResponseBody,
JmapSessionManager, JsonResponse,
autoconfig::Autoconfig,
event_source::EventSourceHandler,
management::{ManagementApi, ManagementApiError},
request::RequestHandler,
session::SessionHandler,
HtmlResponse, HttpRequest, HttpResponse, HttpResponseBody, JmapSessionManager, JsonResponse,
};
pub struct HttpSessionData {
@ -52,8 +63,16 @@ pub struct HttpSessionData {
pub session_id: u64,
}
impl JMAP {
pub async fn parse_http_request(
pub trait ParseHttp: Sync + Send {
fn parse_http_request(
&self,
req: HttpRequest,
session: HttpSessionData,
) -> impl Future<Output = trc::Result<HttpResponse>> + Send;
}
impl ParseHttp for Server {
async fn parse_http_request(
&self,
mut req: HttpRequest,
session: HttpSessionData,
@ -63,7 +82,7 @@ impl JMAP {
// Validate endpoint access
let ctx = HttpContext::new(&session, &req);
match ctx.has_endpoint_access(&self.core).await {
match ctx.has_endpoint_access(self).await {
StatusCode::OK => (),
status => {
// Allow loopback address to avoid lockouts
@ -198,10 +217,7 @@ impl JMAP {
self.authenticate_headers(&req, &session).await?;
return Ok(self
.handle_session_resource(
ctx.resolve_response_url(&self.core).await,
access_token,
)
.handle_session_resource(ctx.resolve_response_url(self).await, access_token)
.await?
.into_http_response());
}
@ -210,11 +226,11 @@ impl JMAP {
self.is_anonymous_allowed(&session.remote_ip).await?;
return Ok(JsonResponse::new(OAuthMetadata::new(
ctx.resolve_response_url(&self.core).await,
ctx.resolve_response_url(self).await,
))
.into_http_response());
}
("acme-challenge", &Method::GET) if self.core.has_acme_http_providers() => {
("acme-challenge", &Method::GET) if self.has_acme_http_providers() => {
if let Some(token) = path.next() {
return match self
.core
@ -230,7 +246,7 @@ impl JMAP {
}
}
("mta-sts.txt", &Method::GET) => {
if let Some(policy) = self.core.build_mta_sts_policy() {
if let Some(policy) = self.build_mta_sts_policy() {
return Ok(Resource::new("text/plain", policy.to_string().into_bytes())
.into_http_response());
} else {
@ -256,7 +272,7 @@ impl JMAP {
("device", &Method::POST) => {
self.is_anonymous_allowed(&session.remote_ip).await?;
let url = ctx.resolve_response_url(&self.core).await;
let url = ctx.resolve_response_url(self).await;
return self
.handle_device_auth(&mut req, url, session.session_id)
.await;
@ -389,7 +405,7 @@ impl JMAP {
return Ok(Resource::new(
"text/plain; version=0.0.4",
self.core.export_prometheus_metrics().await?.into_bytes(),
self.export_prometheus_metrics().await?.into_bytes(),
)
.into_http_response());
}
@ -400,13 +416,12 @@ impl JMAP {
_ => (),
},
#[cfg(feature = "enterprise")]
"logo.svg" if self.core.is_enterprise_edition() => {
"logo.svg" if self.is_enterprise_edition() => {
// SPDX-SnippetBegin
// SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art>
// SPDX-License-Identifier: LicenseRef-SEL
match self
.core
.logo_resource(
req.headers()
.get(header::HOST)
@ -425,7 +440,7 @@ impl JMAP {
}
}
let resource = self.inner.webadmin.get("logo.svg").await?;
let resource = self.inner.data.webadmin.get("logo.svg").await?;
return if !resource.is_empty() {
Ok(resource.into_http_response())
@ -439,6 +454,7 @@ impl JMAP {
let path = req.uri().path();
let resource = self
.inner
.data
.webadmin
.get(path.strip_prefix('/').unwrap_or(path))
.await?;
@ -455,166 +471,156 @@ impl JMAP {
}
}
impl JmapInstance {
async fn handle_session<T: SessionStream>(self, session: SessionData<T>) {
let _in_flight = session.in_flight;
let is_tls = session.stream.is_tls();
async fn handle_session<T: SessionStream>(inner: Arc<Inner>, session: SessionData<T>) {
let _in_flight = session.in_flight;
let is_tls = session.stream.is_tls();
if let Err(http_err) = http1::Builder::new()
.keep_alive(true)
.serve_connection(
TokioIo::new(session.stream),
service_fn(|req: hyper::Request<body::Incoming>| {
let jmap_instance = self.clone();
let instance = session.instance.clone();
if let Err(http_err) = http1::Builder::new()
.keep_alive(true)
.serve_connection(
TokioIo::new(session.stream),
service_fn(|req: hyper::Request<body::Incoming>| {
let instance = session.instance.clone();
let inner = inner.clone();
async move {
let jmap = JMAP::from(jmap_instance);
// Obtain remote IP
let remote_ip = if !jmap.core.jmap.http_use_forwarded {
trc::event!(
Http(trc::HttpEvent::RequestUrl),
SpanId = session.session_id,
Url = req.uri().to_string(),
);
session.remote_ip
} else if let Some(forwarded_for) = req
.headers()
.get(header::FORWARDED)
.and_then(|h| h.to_str().ok())
.and_then(|h| {
let h = h.to_ascii_lowercase();
h.split_once("for=").and_then(|(_, rest)| {
let mut start_ip = usize::MAX;
let mut end_ip = usize::MAX;
for (pos, ch) in rest.char_indices() {
match ch {
'0'..='9' | 'a'..='f' | ':' | '.' => {
if start_ip == usize::MAX {
start_ip = pos;
}
end_ip = pos;
}
'"' | '[' | ' ' if start_ip == usize::MAX => {}
_ => {
break;
}
}
}
rest.get(start_ip..=end_ip)
.and_then(|h| h.parse::<IpAddr>().ok())
})
})
.or_else(|| {
req.headers()
.get("X-Forwarded-For")
.and_then(|h| h.to_str().ok())
.map(|h| h.split_once(',').map_or(h, |(ip, _)| ip).trim())
.and_then(|h| h.parse::<IpAddr>().ok())
})
{
trc::event!(
Http(trc::HttpEvent::RequestUrl),
SpanId = session.session_id,
RemoteIp = forwarded_for,
Url = req.uri().to_string(),
);
forwarded_for
} else {
trc::event!(
Http(trc::HttpEvent::XForwardedMissing),
SpanId = session.session_id,
);
session.remote_ip
};
// Parse HTTP request
let response = match jmap
.parse_http_request(
req,
HttpSessionData {
instance,
local_ip: session.local_ip,
local_port: session.local_port,
remote_ip,
remote_port: session.remote_port,
is_tls,
session_id: session.session_id,
},
)
.await
{
Ok(response) => response,
Err(err) => {
let response = err.into_http_response();
trc::error!(err.span_id(session.session_id));
response
}
};
async move {
let server = inner.build_server();
// Obtain remote IP
let remote_ip = if !server.core.jmap.http_use_forwarded {
trc::event!(
Http(trc::HttpEvent::ResponseBody),
Http(trc::HttpEvent::RequestUrl),
SpanId = session.session_id,
Contents = match &response.body {
HttpResponseBody::Text(value) => trc::Value::String(value.clone()),
HttpResponseBody::Binary(_) => trc::Value::Static("[binary data]"),
HttpResponseBody::Stream(_) => trc::Value::Static("[stream]"),
_ => trc::Value::None,
},
Code = response.status.as_u16(),
Size = response.size(),
Url = req.uri().to_string(),
);
// Build response
let mut response = response.build();
session.remote_ip
} else if let Some(forwarded_for) = req
.headers()
.get(header::FORWARDED)
.and_then(|h| h.to_str().ok())
.and_then(|h| {
let h = h.to_ascii_lowercase();
h.split_once("for=").and_then(|(_, rest)| {
let mut start_ip = usize::MAX;
let mut end_ip = usize::MAX;
// Add custom headers
if !jmap.core.jmap.http_headers.is_empty() {
let headers = response.headers_mut();
for (pos, ch) in rest.char_indices() {
match ch {
'0'..='9' | 'a'..='f' | ':' | '.' => {
if start_ip == usize::MAX {
start_ip = pos;
}
end_ip = pos;
}
'"' | '[' | ' ' if start_ip == usize::MAX => {}
_ => {
break;
}
}
}
for (header, value) in &jmap.core.jmap.http_headers {
headers.insert(header.clone(), value.clone());
}
rest.get(start_ip..=end_ip)
.and_then(|h| h.parse::<IpAddr>().ok())
})
})
.or_else(|| {
req.headers()
.get("X-Forwarded-For")
.and_then(|h| h.to_str().ok())
.map(|h| h.split_once(',').map_or(h, |(ip, _)| ip).trim())
.and_then(|h| h.parse::<IpAddr>().ok())
})
{
trc::event!(
Http(trc::HttpEvent::RequestUrl),
SpanId = session.session_id,
RemoteIp = forwarded_for,
Url = req.uri().to_string(),
);
forwarded_for
} else {
trc::event!(
Http(trc::HttpEvent::XForwardedMissing),
SpanId = session.session_id,
);
session.remote_ip
};
// Parse HTTP request
let response = match server
.parse_http_request(
req,
HttpSessionData {
instance,
local_ip: session.local_ip,
local_port: session.local_port,
remote_ip,
remote_port: session.remote_port,
is_tls,
session_id: session.session_id,
},
)
.await
{
Ok(response) => response,
Err(err) => {
let response = err.into_http_response();
trc::error!(err.span_id(session.session_id));
response
}
};
Ok::<_, hyper::Error>(response)
trc::event!(
Http(trc::HttpEvent::ResponseBody),
SpanId = session.session_id,
Contents = match &response.body {
HttpResponseBody::Text(value) => trc::Value::String(value.clone()),
HttpResponseBody::Binary(_) => trc::Value::Static("[binary data]"),
HttpResponseBody::Stream(_) => trc::Value::Static("[stream]"),
_ => trc::Value::None,
},
Code = response.status.as_u16(),
Size = response.size(),
);
// Build response
let mut response = response.build();
// Add custom headers
if !server.core.jmap.http_headers.is_empty() {
let headers = response.headers_mut();
for (header, value) in &server.core.jmap.http_headers {
headers.insert(header.clone(), value.clone());
}
}
}),
)
.with_upgrades()
.await
{
trc::event!(
Http(trc::HttpEvent::Error),
SpanId = session.session_id,
Reason = http_err.to_string(),
);
}
Ok::<_, hyper::Error>(response)
}
}),
)
.with_upgrades()
.await
{
trc::event!(
Http(trc::HttpEvent::Error),
SpanId = session.session_id,
Reason = http_err.to_string(),
);
}
}
impl SessionManager for JmapSessionManager {
fn handle<T: SessionStream>(
self,
session: SessionData<T>,
) -> impl std::future::Future<Output = ()> + Send {
self.inner.handle_session(session)
fn handle<T: SessionStream>(self, session: SessionData<T>) -> impl Future<Output = ()> + Send {
handle_session(self.inner, session)
}
#[allow(clippy::manual_async_fn)]
fn shutdown(&self) -> impl std::future::Future<Output = ()> + Send {
async {
let _ = self
.inner
.jmap_inner
.state_tx
.send(state::Event::Stop)
.await;
let _ = self.inner.ipc.state_tx.send(StateEvent::Stop).await;
}
}
}
@ -629,31 +635,33 @@ impl<'x> HttpContext<'x> {
Self { session, req }
}
pub async fn resolve_response_url(&self, core: &Core) -> String {
core.eval_if(
&core.network.http_response_url,
self,
self.session.session_id,
)
.await
.unwrap_or_else(|| {
format!(
"http{}://{}:{}",
if self.session.is_tls { "s" } else { "" },
self.session.local_ip,
self.session.local_port
async fn resolve_response_url(&self, server: &Server) -> String {
server
.eval_if(
&server.core.network.http_response_url,
self,
self.session.session_id,
)
})
.await
.unwrap_or_else(|| {
format!(
"http{}://{}:{}",
if self.session.is_tls { "s" } else { "" },
self.session.local_ip,
self.session.local_port
)
})
}
pub async fn has_endpoint_access(&self, core: &Core) -> StatusCode {
core.eval_if(
&core.network.http_allowed_endpoint,
self,
self.session.session_id,
)
.await
.unwrap_or(StatusCode::OK)
async fn has_endpoint_access(&self, server: &Server) -> StatusCode {
server
.eval_if(
&server.core.network.http_allowed_endpoint,
self,
self.session.session_id,
)
.await
.unwrap_or(StatusCode::OK)
}
}

View file

@ -6,7 +6,7 @@
use std::str::FromStr;
use common::{auth::AccessToken, config::smtp::auth::simple_pem_parse};
use common::{auth::AccessToken, config::smtp::auth::simple_pem_parse, Server};
use directory::{backend::internal::manage, Permission};
use hyper::Method;
use mail_auth::{
@ -21,12 +21,10 @@ use serde::{Deserialize, Serialize};
use serde_json::json;
use store::write::now;
use crate::{
api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse},
JMAP,
};
use crate::api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse};
use super::decode_path_element;
use std::future::Future;
#[derive(Debug, Serialize, Deserialize, Copy, Clone, PartialEq, Eq)]
pub enum Algorithm {
@ -42,8 +40,36 @@ struct DkimSignature {
selector: Option<String>,
}
impl JMAP {
pub async fn handle_manage_dkim(
pub trait DkimManagement: Sync + Send {
fn handle_manage_dkim(
&self,
req: &HttpRequest,
path: Vec<&str>,
body: Option<Vec<u8>>,
access_token: &AccessToken,
) -> impl Future<Output = trc::Result<HttpResponse>> + Send;
fn handle_get_public_key(
&self,
path: Vec<&str>,
) -> impl Future<Output = trc::Result<HttpResponse>> + Send;
fn handle_create_signature(
&self,
body: Option<Vec<u8>>,
) -> impl Future<Output = trc::Result<HttpResponse>> + Send;
fn create_dkim_key(
&self,
algo: Algorithm,
id: impl AsRef<str> + Send,
domain: impl Into<String> + Send,
selector: impl Into<String> + Send,
) -> impl Future<Output = trc::Result<()>> + Send;
}
impl DkimManagement for Server {
async fn handle_manage_dkim(
&self,
req: &HttpRequest,
path: Vec<&str>,

View file

@ -4,7 +4,7 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use common::auth::AccessToken;
use common::{auth::AccessToken, Server};
use directory::{
backend::internal::manage::{self},
Permission,
@ -17,27 +17,39 @@ use sha1::Digest;
use utils::config::Config;
use x509_parser::parse_x509_certificate;
use crate::{
api::{
http::ToHttpResponse,
management::dkim::{obtain_dkim_public_key, Algorithm},
HttpRequest, HttpResponse, JsonResponse,
},
JMAP,
use crate::api::{
http::ToHttpResponse,
management::dkim::{obtain_dkim_public_key, Algorithm},
HttpRequest, HttpResponse, JsonResponse,
};
use super::decode_path_element;
use std::future::Future;
#[derive(Debug, Serialize, Deserialize)]
struct DnsRecord {
pub struct DnsRecord {
#[serde(rename = "type")]
typ: String,
name: String,
content: String,
}
impl JMAP {
pub async fn handle_manage_dns(
pub trait DnsManagement: Sync + Send {
fn handle_manage_dns(
&self,
req: &HttpRequest,
path: Vec<&str>,
access_token: &AccessToken,
) -> impl Future<Output = trc::Result<HttpResponse>> + Send;
fn build_dns_records(
&self,
domain_name: &str,
) -> impl Future<Output = trc::Result<Vec<DnsRecord>>> + Send;
}
impl DnsManagement for Server {
async fn handle_manage_dns(
&self,
req: &HttpRequest,
path: Vec<&str>,
@ -210,7 +222,7 @@ impl JMAP {
});
// Add MTA-STS records
if let Some(policy) = self.core.build_mta_sts_policy() {
if let Some(policy) = self.build_mta_sts_policy() {
records.push(DnsRecord {
typ: "CNAME".to_string(),
name: format!("mta-sts.{domain_name}."),
@ -239,7 +251,7 @@ impl JMAP {
});
// Add TLSA records
for (name, key) in self.core.tls.certificates.load().iter() {
for (name, key) in self.inner.data.tls_certificates.load().iter() {
if !name.ends_with(domain_name)
|| name.starts_with("mta-sts.")
|| name.starts_with("autoconfig.")

View file

@ -19,6 +19,7 @@ use common::{
metrics::store::{Metric, MetricsStore},
tracers::store::{TracingQuery, TracingStore},
},
Server,
};
use directory::{backend::internal::manage, Permission};
use http_body_util::{combinators::BoxBody, StreamBody};
@ -28,6 +29,7 @@ use hyper::{
};
use mail_parser::DateTime;
use serde_json::json;
use std::future::Future;
use store::ahash::{AHashMap, AHashSet};
use trc::{
ipc::{bitset::Bitset, subscriber::SubscriberBuilder},
@ -41,11 +43,20 @@ use crate::{
http::ToHttpResponse, management::Timestamp, HttpRequest, HttpResponse, HttpResponseBody,
JsonResponse,
},
JMAP,
auth::oauth::token::TokenHandler,
};
impl JMAP {
pub async fn handle_telemetry_api_request(
pub trait TelemetryApi: Sync + Send {
fn handle_telemetry_api_request(
&self,
req: &HttpRequest,
path: Vec<&str>,
access_token: &AccessToken,
) -> impl Future<Output = trc::Result<HttpResponse>> + Send;
}
impl TelemetryApi for Server {
async fn handle_telemetry_api_request(
&self,
req: &HttpRequest,
path: Vec<&str>,
@ -455,9 +466,9 @@ impl JMAP {
] {
if metric_types.contains(&metric_type) {
let value = match metric_type {
MetricType::QueueCount => self.core.total_queued_messages().await?,
MetricType::UserCount => self.core.total_accounts().await?,
MetricType::DomainCount => self.core.total_domains().await?,
MetricType::QueueCount => self.total_queued_messages().await?,
MetricType::UserCount => self.total_accounts().await?,
MetricType::DomainCount => self.total_domains().await?,
_ => unreachable!(),
};
Collector::update_gauge(metric_type, value);

View file

@ -11,12 +11,13 @@
use std::str::FromStr;
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
use common::{auth::AccessToken, enterprise::undelete::DeletedBlob};
use common::{auth::AccessToken, enterprise::undelete::DeletedBlob, Server};
use directory::backend::internal::manage::ManageDirectory;
use hyper::Method;
use jmap_proto::types::collection::Collection;
use mail_parser::{DateTime, MessageParser};
use serde_json::json;
use std::future::Future;
use store::write::{BatchBuilder, BlobOp, ValueClass};
use trc::AddContext;
use utils::{url_params::UrlParams, BlobHash};
@ -27,9 +28,10 @@ use crate::{
management::decode_path_element,
HttpRequest, HttpResponse, JsonResponse,
},
email::ingest::{IngestEmail, IngestSource},
blob::download::BlobDownload,
email::ingest::{EmailIngest, IngestEmail, IngestSource},
mailbox::INBOX_ID,
JMAP,
JmapMethods,
};
#[derive(serde::Deserialize, serde::Serialize)]
@ -52,8 +54,18 @@ pub enum UndeleteResponse {
Error { reason: String },
}
impl JMAP {
pub async fn handle_undelete_api_request(
pub trait UndeleteApi: Sync + Send {
fn handle_undelete_api_request(
&self,
req: &HttpRequest,
path: Vec<&str>,
body: Option<Vec<u8>>,
session: &HttpSessionData,
) -> impl Future<Output = trc::Result<HttpResponse>> + Send;
}
impl UndeleteApi for Server {
async fn handle_undelete_api_request(
&self,
req: &HttpRequest,
path: Vec<&str>,

View file

@ -5,18 +5,16 @@ use std::{
};
use chrono::DateTime;
use common::auth::AccessToken;
use common::{auth::AccessToken, Server};
use directory::{backend::internal::manage, Permission};
use rev_lines::RevLines;
use serde::Serialize;
use serde_json::json;
use std::future::Future;
use tokio::sync::oneshot;
use utils::url_params::UrlParams;
use crate::{
api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse},
JMAP,
};
use crate::api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse};
#[derive(Serialize)]
struct LogEntry {
@ -27,8 +25,16 @@ struct LogEntry {
details: String,
}
impl JMAP {
pub async fn handle_view_logs(
pub trait LogManagement: Sync + Send {
fn handle_view_logs(
&self,
req: &HttpRequest,
access_token: &AccessToken,
) -> impl Future<Output = trc::Result<HttpResponse>> + Send;
}
impl LogManagement for Server {
async fn handle_view_logs(
&self,
req: &HttpRequest,
access_token: &AccessToken,

View file

@ -19,15 +19,28 @@ pub mod stores;
use std::{borrow::Cow, str::FromStr, sync::Arc};
use common::auth::AccessToken;
use common::{auth::AccessToken, Server};
use directory::{backend::internal::manage, Permission};
use dkim::DkimManagement;
use dns::DnsManagement;
use enterprise::telemetry::TelemetryApi;
use hyper::Method;
use log::LogManagement;
use mail_parser::DateTime;
use principal::PrincipalManager;
use queue::QueueManagement;
use reload::ManageReload;
use report::ManageReports;
use serde::Serialize;
use settings::ManageSettings;
use sieve::SieveHandler;
use store::write::now;
use stores::ManageStore;
use crate::{auth::oauth::auth::OAuthApiHandler, email::crypto::CryptoHandler};
use super::{http::HttpSessionData, HttpRequest, HttpResponse};
use crate::JMAP;
use std::future::Future;
#[derive(Serialize)]
#[serde(tag = "error")]
@ -53,9 +66,19 @@ pub enum ManagementApiError<'x> {
},
}
impl JMAP {
pub trait ManagementApi: Sync + Send {
fn handle_api_manage_request(
&self,
req: &HttpRequest,
body: Option<Vec<u8>>,
access_token: Arc<AccessToken>,
session: &HttpSessionData,
) -> impl Future<Output = trc::Result<HttpResponse>> + Send;
}
impl ManagementApi for Server {
#[allow(unused_variables)]
pub async fn handle_api_manage_request(
async fn handle_api_manage_request(
&self,
req: &HttpRequest,
body: Option<Vec<u8>>,

View file

@ -6,7 +6,7 @@
use std::sync::{atomic::Ordering, Arc};
use common::auth::AccessToken;
use common::{auth::AccessToken, Server};
use directory::{
backend::internal::{
lookup::DirectoryStore,
@ -21,12 +21,10 @@ use serde_json::json;
use trc::AddContext;
use utils::url_params::UrlParams;
use crate::{
api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse},
JMAP,
};
use crate::api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse};
use super::decode_path_element;
use std::future::Future;
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[serde(tag = "type")]
@ -47,8 +45,32 @@ pub struct AccountAuthResponse {
pub app_passwords: Vec<String>,
}
impl JMAP {
pub async fn handle_manage_principal(
pub trait PrincipalManager: Sync + Send {
fn handle_manage_principal(
&self,
req: &HttpRequest,
path: Vec<&str>,
body: Option<Vec<u8>>,
access_token: &AccessToken,
) -> impl Future<Output = trc::Result<HttpResponse>> + Send;
fn handle_account_auth_get(
&self,
access_token: Arc<AccessToken>,
) -> impl Future<Output = trc::Result<HttpResponse>> + Send;
fn handle_account_auth_post(
&self,
req: &HttpRequest,
access_token: Arc<AccessToken>,
body: Option<Vec<u8>>,
) -> impl Future<Output = trc::Result<HttpResponse>> + Send;
fn assert_supported_directory(&self) -> trc::Result<()>;
}
impl PrincipalManager for Server {
async fn handle_manage_principal(
&self,
req: &HttpRequest,
path: Vec<&str>,
@ -297,13 +319,16 @@ impl JMAP {
}
// Remove entries from cache
self.inner.sessions.retain(|_, id| id.item != account_id);
self.inner
.data
.http_auth_cache
.retain(|_, id| id.item != account_id);
if matches!(typ, Type::Role | Type::Tenant) {
// Update permissions cache
self.core.security.permissions.clear();
self.core
.security
self.inner.data.permissions.clear();
self.inner
.data
.permissions_version
.fetch_add(1, Ordering::Relaxed);
}
@ -399,20 +424,23 @@ impl JMAP {
if expire_session {
// Remove entries from cache
self.inner.sessions.retain(|_, id| id.item != account_id);
self.inner
.data
.http_auth_cache
.retain(|_, id| id.item != account_id);
}
if is_role_change {
// Update permissions cache
self.core.security.permissions.clear();
self.core
.security
self.inner.data.permissions.clear();
self.inner
.data
.permissions_version
.fetch_add(1, Ordering::Relaxed);
}
if expire_token {
self.core.security.access_tokens.remove(&account_id);
self.inner.data.access_tokens.remove(&account_id);
}
Ok(JsonResponse::new(json!({
@ -428,7 +456,7 @@ impl JMAP {
}
}
pub async fn handle_account_auth_get(
async fn handle_account_auth_get(
&self,
access_token: Arc<AccessToken>,
) -> trc::Result<HttpResponse> {
@ -463,7 +491,7 @@ impl JMAP {
.into_http_response())
}
pub async fn handle_account_auth_post(
async fn handle_account_auth_post(
&self,
req: &HttpRequest,
access_token: Arc<AccessToken>,
@ -513,7 +541,10 @@ impl JMAP {
.await?;
// Remove entries from cache
self.inner.sessions.retain(|_, id| id.item != u32::MAX);
self.inner
.data
.http_auth_cache
.retain(|_, id| id.item != u32::MAX);
return Ok(JsonResponse::new(json!({
"data": (),
@ -578,7 +609,8 @@ impl JMAP {
// Remove entries from cache
self.inner
.sessions
.data
.http_auth_cache
.retain(|_, id| id.item != access_token.primary_id());
Ok(JsonResponse::new(json!({
@ -587,7 +619,7 @@ impl JMAP {
.into_http_response())
}
pub fn assert_supported_directory(&self) -> trc::Result<()> {
fn assert_supported_directory(&self) -> trc::Result<()> {
let class = match &self.core.storage.directory.store {
DirectoryInner::Internal(_) => return Ok(()),
DirectoryInner::Ldap(_) => "LDAP",

View file

@ -4,8 +4,10 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::future::Future;
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
use common::auth::AccessToken;
use common::{auth::AccessToken, ipc::QueueEvent, Server};
use directory::{
backend::internal::{manage::ManageDirectory, PrincipalField},
Permission, Type,
@ -19,7 +21,10 @@ use mail_auth::{
use mail_parser::DateTime;
use serde::{Deserializer, Serializer};
use serde_json::json;
use smtp::queue::{self, ErrorDetails, HostResponse, QueueId, Status};
use smtp::{
queue::{self, spool::SmtpSpool, ErrorDetails, HostResponse, QueueId, Status},
reporting::{dmarc::DmarcReporting, tls::TlsReporting},
};
use store::{
write::{key::DeserializeBigEndian, now, Bincode, QueueClass, ReportEvent, ValueClass},
Deserialize, IterateParams, ValueKey,
@ -27,10 +32,7 @@ use store::{
use trc::AddContext;
use utils::url_params::UrlParams;
use crate::{
api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse},
JMAP,
};
use crate::api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse};
use super::{decode_path_element, FutureTimestamp};
@ -106,8 +108,17 @@ pub enum Report {
},
}
impl JMAP {
pub async fn handle_manage_queue(
pub trait QueueManagement: Sync + Send {
fn handle_manage_queue(
&self,
req: &HttpRequest,
path: Vec<&str>,
access_token: &AccessToken,
) -> impl Future<Output = trc::Result<HttpResponse>> + Send;
}
impl QueueManagement for Server {
async fn handle_manage_queue(
&self,
req: &HttpRequest,
path: Vec<&str>,
@ -270,7 +281,6 @@ impl JMAP {
access_token.assert_has_permission(Permission::MessageQueueGet)?;
if let Some(message) = self
.smtp
.read_message(queue_id.parse().unwrap_or_default())
.await
.filter(|message| {
@ -298,7 +308,6 @@ impl JMAP {
let item = params.get("filter");
if let Some(mut message) = self
.smtp
.read_message(queue_id.parse().unwrap_or_default())
.await
.filter(|message| {
@ -329,9 +338,9 @@ impl JMAP {
if found {
let next_event = message.next_event().unwrap_or_default();
message
.save_changes(&self.smtp, prev_event.into(), next_event.into())
.save_changes(self, prev_event.into(), next_event.into())
.await;
let _ = self.smtp.inner.queue_tx.send(queue::Event::Reload).await;
let _ = self.inner.ipc.queue_tx.send(QueueEvent::Reload).await;
}
Ok(JsonResponse::new(json!({
@ -347,7 +356,6 @@ impl JMAP {
access_token.assert_has_permission(Permission::MessageQueueDelete)?;
if let Some(mut message) = self
.smtp
.read_message(queue_id.parse().unwrap_or_default())
.await
.filter(|message| {
@ -411,14 +419,14 @@ impl JMAP {
}) {
let next_event = message.next_event().unwrap_or_default();
message
.save_changes(&self.smtp, next_event.into(), prev_event.into())
.save_changes(self, next_event.into(), prev_event.into())
.await;
} else {
message.remove(&self.smtp, prev_event).await;
message.remove(self, prev_event).await;
}
}
} else {
message.remove(&self.smtp, prev_event).await;
message.remove(self, prev_event).await;
found = true;
}
@ -528,7 +536,6 @@ impl JMAP {
{
let mut rua = Vec::new();
if let Some(report) = self
.smtp
.generate_dmarc_aggregate_report(&event, &mut rua, None, 0)
.await?
{
@ -542,7 +549,6 @@ impl JMAP {
{
let mut rua = Vec::new();
if let Some(report) = self
.smtp
.generate_tls_aggregate_report(&[event.clone()], &mut rua, None, 0)
.await?
{
@ -573,7 +579,7 @@ impl JMAP {
.as_ref()
.map_or(true, |domains| domains.contains(&event.domain)) =>
{
self.smtp.delete_dmarc_report(event).await;
self.delete_dmarc_report(event).await;
true
}
QueueClass::TlsReportHeader(event)
@ -581,7 +587,7 @@ impl JMAP {
.as_ref()
.map_or(true, |domains| domains.contains(&event.domain)) =>
{
self.smtp.delete_tls_report(vec![event]).await;
self.delete_tls_report(vec![event]).await;
true
}
_ => false,

View file

@ -4,20 +4,36 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use common::auth::AccessToken;
use common::{auth::AccessToken, ipc::HousekeeperEvent, Server};
use directory::Permission;
use hyper::Method;
use serde_json::json;
use std::future::Future;
use utils::url_params::UrlParams;
use crate::{
api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse},
services::housekeeper::Event,
JMAP,
JmapMethods,
};
impl JMAP {
pub async fn handle_manage_reload(
pub trait ManageReload: Sync + Send {
fn handle_manage_reload(
&self,
req: &HttpRequest,
path: Vec<&str>,
access_token: &AccessToken,
) -> impl Future<Output = trc::Result<HttpResponse>> + Send;
fn handle_manage_update(
&self,
req: &HttpRequest,
path: Vec<&str>,
access_token: &AccessToken,
) -> impl Future<Output = trc::Result<HttpResponse>> + Send;
}
impl ManageReload for Server {
async fn handle_manage_reload(
&self,
req: &HttpRequest,
path: Vec<&str>,
@ -28,10 +44,10 @@ impl JMAP {
match (path.get(1).copied(), req.method()) {
(Some("lookup"), &Method::GET) => {
let result = self.core.reload_lookups().await?;
let result = self.reload_lookups().await?;
// Update core
if let Some(core) = result.new_core {
self.shared_core.store(core.into());
self.inner.shared_core.store(core.into());
}
Ok(JsonResponse::new(json!({
@ -40,13 +56,14 @@ impl JMAP {
.into_http_response())
}
(Some("certificate"), &Method::GET) => Ok(JsonResponse::new(json!({
"data": self.core.reload_certificates().await?.config,
"data": self.reload_certificates().await?.config,
}))
.into_http_response()),
(Some("server.blocked-ip"), &Method::GET) => {
let result = self.core.reload_blocked_ips().await?;
let result = self.reload_blocked_ips().await?;
// Increment version counter
self.core.network.blocked_ips.increment_version();
self.increment_blocked_version();
Ok(JsonResponse::new(json!({
"data": result.config,
@ -54,28 +71,29 @@ impl JMAP {
.into_http_response())
}
(_, &Method::GET) => {
let result = self.core.reload().await?;
let result = self.reload().await?;
if !UrlParams::new(req.uri().query()).has_key("dry-run") {
if let Some(core) = result.new_core {
// Update core
self.shared_core.store(core.into());
self.inner.shared_core.store(core.into());
// Increment version counter
self.inner.increment_config_version();
self.increment_config_version();
}
if let Some(tracers) = result.tracers {
// Update tracers
#[cfg(feature = "enterprise")]
tracers.update(self.shared_core.load().is_enterprise_edition());
tracers.update(self.inner.shared_core.load().is_enterprise_edition());
#[cfg(not(feature = "enterprise"))]
tracers.update(false);
}
// Reload settings
self.inner
.ipc
.housekeeper_tx
.send(Event::ReloadSettings)
.send(HousekeeperEvent::ReloadSettings)
.await
.map_err(|err| {
trc::EventType::Server(trc::ServerEvent::ThreadError)
@ -94,7 +112,7 @@ impl JMAP {
}
}
pub async fn handle_manage_update(
async fn handle_manage_update(
&self,
req: &HttpRequest,
path: Vec<&str>,
@ -119,7 +137,11 @@ impl JMAP {
// Validate the access token
access_token.assert_has_permission(Permission::UpdateWebadmin)?;
self.inner.webadmin.update_and_unpack(&self.core).await?;
self.inner
.data
.webadmin
.update_and_unpack(&self.core)
.await?;
Ok(JsonResponse::new(json!({
"data": (),

View file

@ -4,7 +4,9 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use common::auth::AccessToken;
use std::future::Future;
use common::{auth::AccessToken, Server};
use directory::{
backend::internal::{manage::ManageDirectory, PrincipalField},
Permission, Type,
@ -23,10 +25,7 @@ use store::{
use trc::AddContext;
use utils::url_params::UrlParams;
use crate::{
api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse},
JMAP,
};
use crate::api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse};
use super::decode_path_element;
@ -36,8 +35,17 @@ enum ReportType {
Arf,
}
impl JMAP {
pub async fn handle_manage_reports(
pub trait ManageReports: Sync + Send {
fn handle_manage_reports(
&self,
req: &HttpRequest,
path: Vec<&str>,
access_token: &AccessToken,
) -> impl Future<Output = trc::Result<HttpResponse>> + Send;
}
impl ManageReports for Server {
async fn handle_manage_reports(
&self,
req: &HttpRequest,
path: Vec<&str>,

View file

@ -4,19 +4,17 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use common::auth::AccessToken;
use common::{auth::AccessToken, Server};
use directory::Permission;
use hyper::Method;
use serde_json::json;
use store::ahash::AHashMap;
use utils::{config::ConfigKey, map::vec_map::VecMap, url_params::UrlParams};
use crate::{
api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse},
JMAP,
};
use crate::api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse};
use super::decode_path_element;
use std::future::Future;
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[serde(tag = "type")]
@ -34,8 +32,18 @@ pub enum UpdateSettings {
},
}
impl JMAP {
pub async fn handle_manage_settings(
pub trait ManageSettings: Sync + Send {
fn handle_manage_settings(
&self,
req: &HttpRequest,
path: Vec<&str>,
body: Option<Vec<u8>>,
access_token: &AccessToken,
) -> impl Future<Output = trc::Result<HttpResponse>> + Send;
}
impl ManageSettings for Server {
async fn handle_manage_settings(
&self,
req: &HttpRequest,
path: Vec<&str>,

View file

@ -6,18 +6,16 @@
use std::time::SystemTime;
use common::{auth::AccessToken, scripts::ScriptModification, IntoString};
use common::{auth::AccessToken, scripts::ScriptModification, IntoString, Server};
use directory::Permission;
use hyper::Method;
use serde_json::json;
use sieve::{runtime::Variable, Envelope};
use smtp::scripts::{ScriptParameters, ScriptResult};
use smtp::scripts::{event_loop::RunScript, ScriptParameters, ScriptResult};
use std::future::Future;
use utils::url_params::UrlParams;
use crate::{
api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse},
JMAP,
};
use crate::api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse};
#[derive(Debug, serde::Serialize)]
#[serde(tag = "action")]
@ -36,8 +34,18 @@ pub enum Response {
Discard,
}
impl JMAP {
pub async fn handle_run_sieve(
pub trait SieveHandler: Sync + Send {
fn handle_run_sieve(
&self,
req: &HttpRequest,
path: Vec<&str>,
body: Option<Vec<u8>>,
access_token: &AccessToken,
) -> impl Future<Output = trc::Result<HttpResponse>> + Send;
}
impl SieveHandler for Server {
async fn handle_run_sieve(
&self,
req: &HttpRequest,
path: Vec<&str>,
@ -103,7 +111,7 @@ impl JMAP {
}
// Run script
let result = match self.smtp.run_script(script_id, script, params, 0).await {
let result = match self.run_script(script_id, script, params, 0).await {
ScriptResult::Accept { modifications } => Response::Accept { modifications },
ScriptResult::Replace {
message,

View file

@ -5,7 +5,12 @@
*/
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
use common::{auth::AccessToken, manager::webadmin::Resource};
use common::{
auth::AccessToken,
ipc::{HousekeeperEvent, PurgeType},
manager::webadmin::Resource,
Server,
};
use directory::{
backend::internal::manage::{self, ManageDirectory},
Permission,
@ -19,14 +24,30 @@ use crate::{
http::{HttpSessionData, ToHttpResponse},
HttpRequest, HttpResponse, JsonResponse,
},
services::housekeeper::{Event, PurgeType},
JMAP,
services::index::Indexer,
};
use super::decode_path_element;
use super::{decode_path_element, enterprise::undelete::UndeleteApi};
use std::future::Future;
impl JMAP {
pub async fn handle_manage_store(
pub trait ManageStore: Sync + Send {
fn handle_manage_store(
&self,
req: &HttpRequest,
path: Vec<&str>,
body: Option<Vec<u8>>,
session: &HttpSessionData,
access_token: &AccessToken,
) -> impl Future<Output = trc::Result<HttpResponse>> + Send;
fn housekeeper_request(
&self,
event: HousekeeperEvent,
) -> impl Future<Output = trc::Result<HttpResponse>> + Send;
}
impl ManageStore for Server {
async fn handle_manage_store(
&self,
req: &HttpRequest,
path: Vec<&str>,
@ -75,7 +96,7 @@ impl JMAP {
// Validate the access token
access_token.assert_has_permission(Permission::PurgeBlobStore)?;
self.housekeeper_request(Event::Purge(PurgeType::Blobs {
self.housekeeper_request(HousekeeperEvent::Purge(PurgeType::Blobs {
store: self.core.storage.data.clone(),
blob_store: self.core.storage.blob.clone(),
}))
@ -95,7 +116,7 @@ impl JMAP {
self.core.storage.data.clone()
};
self.housekeeper_request(Event::Purge(PurgeType::Data(store)))
self.housekeeper_request(HousekeeperEvent::Purge(PurgeType::Data(store)))
.await
}
(Some("purge"), Some("lookup"), id, &Method::GET) => {
@ -112,7 +133,7 @@ impl JMAP {
self.core.storage.lookup.clone()
};
self.housekeeper_request(Event::Purge(PurgeType::Lookup(store)))
self.housekeeper_request(HousekeeperEvent::Purge(PurgeType::Lookup(store)))
.await
}
(Some("purge"), Some("account"), id, &Method::GET) => {
@ -131,7 +152,7 @@ impl JMAP {
None
};
self.housekeeper_request(Event::Purge(PurgeType::Account(account_id)))
self.housekeeper_request(HousekeeperEvent::Purge(PurgeType::Account(account_id)))
.await
}
(Some("reindex"), id, None, &Method::GET) => {
@ -192,12 +213,17 @@ impl JMAP {
}
}
async fn housekeeper_request(&self, event: Event) -> trc::Result<HttpResponse> {
self.inner.housekeeper_tx.send(event).await.map_err(|err| {
trc::EventType::Server(trc::ServerEvent::ThreadError)
.reason(err)
.details("Failed to send housekeeper event")
})?;
async fn housekeeper_request(&self, event: HousekeeperEvent) -> trc::Result<HttpResponse> {
self.inner
.ipc
.housekeeper_tx
.send(event)
.await
.map_err(|err| {
trc::EventType::Server(trc::ServerEvent::ThreadError)
.reason(err)
.details("Failed to send housekeeper event")
})?;
Ok(JsonResponse::new(json!({
"data": (),

View file

@ -4,15 +4,14 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::borrow::Cow;
use std::{borrow::Cow, sync::Arc};
use common::Inner;
use hyper::StatusCode;
use jmap_proto::types::{id::Id, state::State, type_state::DataType};
use serde::Serialize;
use utils::map::vec_map::VecMap;
use crate::JmapInstance;
pub mod autoconfig;
pub mod event_source;
pub mod http;
@ -22,11 +21,11 @@ pub mod session;
#[derive(Clone)]
pub struct JmapSessionManager {
pub inner: JmapInstance,
pub inner: Arc<Inner>,
}
impl JmapSessionManager {
pub fn new(inner: JmapInstance) -> Self {
pub fn new(inner: Arc<Inner>) -> Self {
Self { inner }
}
}

View file

@ -6,7 +6,7 @@
use std::{sync::Arc, time::Instant};
use common::auth::AccessToken;
use common::{auth::AccessToken, Server};
use jmap_proto::{
method::{
get, query,
@ -18,12 +18,51 @@ use jmap_proto::{
};
use trc::JmapEvent;
use crate::JMAP;
use crate::{
blob::{copy::BlobCopy, get::BlobOperations, upload::BlobUpload},
changes::{get::ChangesLookup, query::QueryChanges},
email::{
copy::EmailCopy, get::EmailGet, import::EmailImport, parse::EmailParse, query::EmailQuery,
set::EmailSet, snippet::EmailSearchSnippet,
},
identity::{get::IdentityGet, set::IdentitySet},
mailbox::{get::MailboxGet, query::MailboxQuery, set::MailboxSet},
principal::{get::PrincipalGet, query::PrincipalQuery},
push::{get::PushSubscriptionFetch, set::PushSubscriptionSet},
quota::{get::QuotaGet, query::QuotaQuery},
services::state::StateManager,
sieve::{
get::SieveScriptGet, query::SieveScriptQuery, set::SieveScriptSet,
validate::SieveScriptValidate,
},
submission::{get::EmailSubmissionGet, query::EmailSubmissionQuery, set::EmailSubmissionSet},
thread::get::ThreadGet,
vacation::{get::VacationResponseGet, set::VacationResponseSet},
};
use super::http::HttpSessionData;
use std::future::Future;
impl JMAP {
pub async fn handle_request(
pub trait RequestHandler: Sync + Send {
fn handle_request(
&self,
request: Request,
access_token: Arc<AccessToken>,
session: &HttpSessionData,
) -> impl Future<Output = Response> + Send;
fn handle_method_call(
&self,
method: RequestMethod,
method_name: &'static str,
access_token: &AccessToken,
next_call: &mut Option<Call<RequestMethod>>,
session: &HttpSessionData,
) -> impl Future<Output = trc::Result<ResponseMethod>> + Send;
}
impl RequestHandler for Server {
async fn handle_request(
&self,
request: Request,
access_token: Arc<AccessToken>,

View file

@ -6,18 +6,27 @@
use std::sync::Arc;
use common::auth::AccessToken;
use common::{auth::AccessToken, Server};
use directory::{backend::internal::PrincipalField, QueryBy};
use jmap_proto::{
request::capability::{Capability, Session},
types::{acl::Acl, collection::Collection, id::Id},
};
use std::future::Future;
use trc::AddContext;
use crate::JMAP;
use crate::auth::acl::AclMethods;
impl JMAP {
pub async fn handle_session_resource(
pub trait SessionHandler: Sync + Send {
fn handle_session_resource(
&self,
base_url: String,
access_token: Arc<AccessToken>,
) -> impl Future<Output = trc::Result<Session>> + Send;
}
impl SessionHandler for Server {
async fn handle_session_resource(
&self,
base_url: String,
access_token: Arc<AccessToken>,

View file

@ -4,7 +4,9 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use common::auth::AccessToken;
use std::future::Future;
use common::{auth::AccessToken, Server};
use directory::{backend::internal::PrincipalField, QueryBy};
use jmap_proto::{
error::set::SetError,
@ -25,10 +27,77 @@ use store::{
use trc::AddContext;
use utils::map::bitmap::Bitmap;
use crate::JMAP;
use crate::JmapMethods;
impl JMAP {
pub async fn shared_documents(
pub trait AclMethods: Sync + Send {
fn shared_documents(
&self,
access_token: &AccessToken,
to_account_id: u32,
to_collection: Collection,
check_acls: impl Into<Bitmap<Acl>> + Send,
) -> impl Future<Output = trc::Result<RoaringBitmap>> + Send;
fn shared_messages(
&self,
access_token: &AccessToken,
to_account_id: u32,
check_acls: impl Into<Bitmap<Acl>> + Send,
) -> impl Future<Output = trc::Result<RoaringBitmap>> + Send;
fn owned_or_shared_documents(
&self,
access_token: &AccessToken,
account_id: u32,
collection: Collection,
check_acls: impl Into<Bitmap<Acl>> + Send,
) -> impl Future<Output = trc::Result<RoaringBitmap>> + Send;
fn owned_or_shared_messages(
&self,
access_token: &AccessToken,
account_id: u32,
check_acls: impl Into<Bitmap<Acl>> + Send,
) -> impl Future<Output = trc::Result<RoaringBitmap>> + Send;
fn has_access_to_document(
&self,
access_token: &AccessToken,
to_account_id: u32,
to_collection: impl Into<u8> + Send,
to_document_id: u32,
check_acls: impl Into<Bitmap<Acl>> + Send,
) -> impl Future<Output = trc::Result<bool>> + Send;
fn acl_set(
&self,
changes: &mut Object<Value>,
current: Option<&HashedValue<Object<Value>>>,
acl_changes: MaybePatchValue,
) -> impl Future<Output = Result<(), SetError>> + Send;
fn acl_get(
&self,
value: &[AclGrant],
access_token: &AccessToken,
account_id: u32,
) -> impl Future<Output = Value> + Send;
fn refresh_acls(&self, changes: &Object<Value>, current: &Option<HashedValue<Object<Value>>>);
fn map_acl_set(
&self,
acl_set: Vec<Value>,
) -> impl Future<Output = Result<Vec<AclGrant>, SetError>> + Send;
fn map_acl_patch(
&self,
acl_patch: Vec<Value>,
) -> impl Future<Output = Result<(AclGrant, Option<bool>), SetError>> + Send;
}
impl AclMethods for Server {
async fn shared_documents(
&self,
access_token: &AccessToken,
to_account_id: u32,
@ -66,7 +135,7 @@ impl JMAP {
Ok(document_ids)
}
pub async fn shared_messages(
async fn shared_messages(
&self,
access_token: &AccessToken,
to_account_id: u32,
@ -97,7 +166,7 @@ impl JMAP {
Ok(shared_messages)
}
pub async fn owned_or_shared_documents(
async fn owned_or_shared_documents(
&self,
access_token: &AccessToken,
account_id: u32,
@ -117,7 +186,7 @@ impl JMAP {
Ok(document_ids)
}
pub async fn owned_or_shared_messages(
async fn owned_or_shared_messages(
&self,
access_token: &AccessToken,
account_id: u32,
@ -136,7 +205,7 @@ impl JMAP {
Ok(document_ids)
}
pub async fn has_access_to_document(
async fn has_access_to_document(
&self,
access_token: &AccessToken,
to_account_id: u32,
@ -179,7 +248,7 @@ impl JMAP {
Ok(false)
}
pub async fn acl_set(
async fn acl_set(
&self,
changes: &mut Object<Value>,
current: Option<&HashedValue<Object<Value>>>,
@ -251,7 +320,7 @@ impl JMAP {
Ok(())
}
pub async fn acl_get(
async fn acl_get(
&self,
value: &[AclGrant],
access_token: &AccessToken,
@ -287,13 +356,9 @@ impl JMAP {
}
}
pub fn refresh_acls(
&self,
changes: &Object<Value>,
current: &Option<HashedValue<Object<Value>>>,
) {
fn refresh_acls(&self, changes: &Object<Value>, current: &Option<HashedValue<Object<Value>>>) {
if let Value::Acl(acl_changes) = changes.get(&Property::Acl) {
let access_tokens = &self.core.security.access_tokens;
let access_tokens = &self.inner.data.access_tokens;
if let Some(Value::Acl(acl_current)) = current
.as_ref()
.and_then(|current| current.inner.properties.get(&Property::Acl))

View file

@ -6,82 +6,101 @@
use std::{net::IpAddr, sync::Arc, time::Instant};
use common::listener::limiter::InFlight;
use common::{listener::limiter::InFlight, Server};
use directory::Permission;
use hyper::header;
use mail_parser::decoders::base64::base64_decode;
use mail_send::Credentials;
use utils::map::ttl_dashmap::TtlMap;
use crate::{
api::{http::HttpSessionData, HttpRequest},
JMAP,
};
use crate::api::{http::HttpSessionData, HttpRequest};
use common::auth::AccessToken;
use std::future::Future;
impl JMAP {
pub async fn authenticate_headers(
use super::{oauth::token::TokenHandler, rate_limit::RateLimiter};
pub trait Authenticator: Sync + Send {
fn authenticate_headers(
&self,
req: &HttpRequest,
session: &HttpSessionData,
) -> impl Future<Output = trc::Result<(InFlight, Arc<AccessToken>)>> + Send;
fn cache_session(&self, session_id: String, access_token: &AccessToken);
fn authenticate_plain(
&self,
username: &str,
secret: &str,
remote_ip: IpAddr,
session_id: u64,
) -> impl Future<Output = trc::Result<AccessToken>> + Send;
}
impl Authenticator for Server {
async fn authenticate_headers(
&self,
req: &HttpRequest,
session: &HttpSessionData,
) -> trc::Result<(InFlight, Arc<AccessToken>)> {
if let Some((mechanism, token)) = req.authorization() {
let access_token = if let Some(account_id) = self.inner.sessions.get_with_ttl(token) {
self.core.get_cached_access_token(account_id).await?
} else {
let access_token = if mechanism.eq_ignore_ascii_case("basic") {
// Enforce rate limit for authentication requests
self.is_auth_allowed_soft(&session.remote_ip).await?;
let access_token =
if let Some(account_id) = self.inner.data.http_auth_cache.get_with_ttl(token) {
self.get_cached_access_token(account_id).await?
} else {
let access_token = if mechanism.eq_ignore_ascii_case("basic") {
// Enforce rate limit for authentication requests
self.is_auth_allowed_soft(&session.remote_ip).await?;
// Decode the base64 encoded credentials
if let Some((account, secret)) = base64_decode(token.as_bytes())
.and_then(|token| String::from_utf8(token).ok())
.and_then(|token| {
token.split_once(':').map(|(login, secret)| {
(login.trim().to_lowercase(), secret.to_string())
// Decode the base64 encoded credentials
if let Some((account, secret)) = base64_decode(token.as_bytes())
.and_then(|token| String::from_utf8(token).ok())
.and_then(|token| {
token.split_once(':').map(|(login, secret)| {
(login.trim().to_lowercase(), secret.to_string())
})
})
})
{
self.authenticate_plain(
&account,
&secret,
session.remote_ip,
session.session_id,
)
.await?
{
self.authenticate_plain(
&account,
&secret,
session.remote_ip,
session.session_id,
)
.await?
} else {
return Err(trc::AuthEvent::Error
.into_err()
.details("Failed to decode Basic auth request.")
.id(token.to_string())
.caused_by(trc::location!()));
}
} else if mechanism.eq_ignore_ascii_case("bearer") {
// Enforce anonymous rate limit for bearer auth requests
self.is_anonymous_allowed(&session.remote_ip).await?;
let (account_id, _, _) =
self.validate_access_token("access_token", token).await?;
self.get_access_token(account_id).await?
} else {
// Enforce anonymous rate limit
self.is_anonymous_allowed(&session.remote_ip).await?;
return Err(trc::AuthEvent::Error
.into_err()
.details("Failed to decode Basic auth request.")
.id(token.to_string())
.reason("Unsupported authentication mechanism.")
.details(token.to_string())
.caused_by(trc::location!()));
}
} else if mechanism.eq_ignore_ascii_case("bearer") {
// Enforce anonymous rate limit for bearer auth requests
self.is_anonymous_allowed(&session.remote_ip).await?;
};
let (account_id, _, _) =
self.validate_access_token("access_token", token).await?;
self.core.get_access_token(account_id).await?
} else {
// Enforce anonymous rate limit
self.is_anonymous_allowed(&session.remote_ip).await?;
return Err(trc::AuthEvent::Error
.into_err()
.reason("Unsupported authentication mechanism.")
.details(token.to_string())
.caused_by(trc::location!()));
// Cache session
let access_token = Arc::new(access_token);
self.cache_session(token.to_string(), &access_token);
self.cache_access_token(access_token.clone());
access_token
};
// Cache session
let access_token = Arc::new(access_token);
self.cache_session(token.to_string(), &access_token);
self.core.cache_access_token(access_token.clone());
access_token
};
// Enforce authenticated rate limit
self.is_account_allowed(&access_token)
.await
@ -97,15 +116,15 @@ impl JMAP {
}
}
pub fn cache_session(&self, session_id: String, access_token: &AccessToken) {
self.inner.sessions.insert_with_ttl(
fn cache_session(&self, session_id: String, access_token: &AccessToken) {
self.inner.data.http_auth_cache.insert_with_ttl(
session_id,
access_token.primary_id(),
Instant::now() + self.core.jmap.session_cache_ttl,
);
}
pub async fn authenticate_plain(
async fn authenticate_plain(
&self,
username: &str,
secret: &str,
@ -113,7 +132,6 @@ impl JMAP {
session_id: u64,
) -> trc::Result<AccessToken> {
match self
.core
.authenticate(
&self.core.storage.directory,
session_id,
@ -126,15 +144,11 @@ impl JMAP {
)
.await
{
Ok(principal) => self
.core
.build_access_token(principal)
.await
.and_then(|token| {
token
.assert_has_permission(Permission::Authenticate)
.map(|_| token)
}),
Ok(principal) => self.build_access_token(principal).await.and_then(|token| {
token
.assert_has_permission(Permission::Authenticate)
.map(|_| token)
}),
Err(err) => {
if !err.matches(trc::EventType::Auth(trc::AuthEvent::MissingTotp)) {
let _ = self.is_auth_allowed_hard(&remote_ip).await;

View file

@ -6,9 +6,10 @@
use std::sync::Arc;
use common::auth::AccessToken;
use common::{auth::AccessToken, Server};
use rand::distributions::Standard;
use serde_json::json;
use std::future::Future;
use store::{
rand::{distributions::Alphanumeric, thread_rng, Rng},
write::Bincode,
@ -18,7 +19,6 @@ use store::{
use crate::{
api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse},
auth::oauth::OAuthStatus,
JMAP,
};
use super::{
@ -26,8 +26,23 @@ use super::{
MAX_POST_LEN, USER_CODE_ALPHABET, USER_CODE_LEN,
};
impl JMAP {
pub async fn handle_oauth_api_request(
pub trait OAuthApiHandler: Sync + Send {
fn handle_oauth_api_request(
&self,
access_token: Arc<AccessToken>,
body: Option<Vec<u8>>,
) -> impl Future<Output = trc::Result<HttpResponse>> + Send;
fn handle_device_auth(
&self,
req: &mut HttpRequest,
base_url: impl AsRef<str> + Send,
session_id: u64,
) -> impl Future<Output = trc::Result<HttpResponse>> + Send;
}
impl OAuthApiHandler for Server {
async fn handle_oauth_api_request(
&self,
access_token: Arc<AccessToken>,
body: Option<Vec<u8>>,
@ -143,7 +158,7 @@ impl JMAP {
Ok(JsonResponse::new(response).into_http_response())
}
pub async fn handle_device_auth(
async fn handle_device_auth(
&self,
req: &mut HttpRequest,
base_url: impl AsRef<str>,

View file

@ -202,7 +202,7 @@ pub struct FormData {
}
impl FormData {
pub async fn from_request(
async fn from_request(
req: &mut HttpRequest,
max_len: usize,
session_id: u64,

View file

@ -6,10 +6,12 @@
use std::time::SystemTime;
use common::Server;
use directory::{backend::internal::PrincipalField, QueryBy};
use hyper::StatusCode;
use mail_builder::encoders::base64::base64_encode;
use mail_parser::decoders::base64::base64_decode;
use std::future::Future;
use store::{
blake3,
rand::{thread_rng, Rng},
@ -20,7 +22,6 @@ use utils::codec::leb128::{Leb128Iterator, Leb128Vec};
use crate::{
api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse},
auth::SymmetricEncrypt,
JMAP,
};
use super::{
@ -28,9 +29,52 @@ use super::{
MAX_POST_LEN, RANDOM_CODE_LEN,
};
impl JMAP {
pub trait TokenHandler: Sync + Send {
fn handle_token_request(
&self,
req: &mut HttpRequest,
session_id: u64,
) -> impl Future<Output = trc::Result<HttpResponse>> + Send;
fn password_hash(
&self,
account_id: u32,
) -> impl Future<Output = Result<String, &'static str>> + Send;
fn issue_token(
&self,
account_id: u32,
client_id: &str,
with_refresh_token: bool,
) -> impl Future<Output = Result<OAuthResponse, &'static str>> + Send;
fn issue_custom_token(
&self,
account_id: u32,
grant_type: &str,
client_id: &str,
expiry_in: u64,
) -> impl Future<Output = trc::Result<String>> + Send;
fn encode_access_token(
&self,
grant_type: &str,
account_id: u32,
password_hash: &str,
client_id: &str,
expiry_in: u64,
) -> Result<String, &'static str>;
fn validate_access_token(
&self,
grant_type: &str,
token_: &str,
) -> impl Future<Output = trc::Result<(u32, String, u64)>> + Send;
}
impl TokenHandler for Server {
// Token endpoint
pub async fn handle_token_request(
async fn handle_token_request(
&self,
req: &mut HttpRequest,
session_id: u64,
@ -199,7 +243,7 @@ impl JMAP {
}
}
pub async fn issue_token(
async fn issue_token(
&self,
account_id: u32,
client_id: &str,
@ -233,7 +277,7 @@ impl JMAP {
})
}
pub async fn issue_custom_token(
async fn issue_custom_token(
&self,
account_id: u32,
grant_type: &str,
@ -303,7 +347,7 @@ impl JMAP {
Ok(String::from_utf8(base64_encode(&token).unwrap_or_default()).unwrap())
}
pub async fn validate_access_token(
async fn validate_access_token(
&self,
grant_type: &str,
token_: &str,

View file

@ -6,23 +6,33 @@
use std::{net::IpAddr, sync::Arc};
use common::listener::limiter::{ConcurrencyLimiter, InFlight};
use common::{
listener::limiter::{ConcurrencyLimiter, InFlight},
ConcurrencyLimiters, Server,
};
use directory::Permission;
use trc::AddContext;
use crate::JMAP;
use common::auth::AccessToken;
use std::future::Future;
pub struct ConcurrencyLimiters {
pub concurrent_requests: ConcurrencyLimiter,
pub concurrent_uploads: ConcurrencyLimiter,
pub trait RateLimiter: Sync + Send {
fn get_concurrency_limiter(&self, account_id: u32) -> Arc<ConcurrencyLimiters>;
fn is_account_allowed(
&self,
access_token: &AccessToken,
) -> impl Future<Output = trc::Result<InFlight>> + Send;
fn is_anonymous_allowed(&self, addr: &IpAddr) -> impl Future<Output = trc::Result<()>> + Send;
fn is_upload_allowed(&self, access_token: &AccessToken) -> trc::Result<InFlight>;
fn is_auth_allowed_soft(&self, addr: &IpAddr) -> impl Future<Output = trc::Result<()>> + Send;
fn is_auth_allowed_hard(&self, addr: &IpAddr) -> impl Future<Output = trc::Result<()>> + Send;
}
impl JMAP {
pub fn get_concurrency_limiter(&self, account_id: u32) -> Arc<ConcurrencyLimiters> {
impl RateLimiter for Server {
fn get_concurrency_limiter(&self, account_id: u32) -> Arc<ConcurrencyLimiters> {
self.inner
.concurrency_limiter
.data
.jmap_limiter
.get(&account_id)
.map(|limiter| limiter.clone())
.unwrap_or_else(|| {
@ -35,13 +45,14 @@ impl JMAP {
),
});
self.inner
.concurrency_limiter
.data
.jmap_limiter
.insert(account_id, limiter.clone());
limiter
})
}
pub async fn is_account_allowed(&self, access_token: &AccessToken) -> trc::Result<InFlight> {
async fn is_account_allowed(&self, access_token: &AccessToken) -> trc::Result<InFlight> {
let limiter = self.get_concurrency_limiter(access_token.primary_id());
let is_rate_allowed = if let Some(rate) = &self.core.jmap.rate_authenticated {
self.core
@ -74,7 +85,7 @@ impl JMAP {
}
}
pub async fn is_anonymous_allowed(&self, addr: &IpAddr) -> trc::Result<()> {
async fn is_anonymous_allowed(&self, addr: &IpAddr) -> trc::Result<()> {
if let Some(rate) = &self.core.jmap.rate_anonymous {
if self
.core
@ -91,7 +102,7 @@ impl JMAP {
Ok(())
}
pub fn is_upload_allowed(&self, access_token: &AccessToken) -> trc::Result<InFlight> {
fn is_upload_allowed(&self, access_token: &AccessToken) -> trc::Result<InFlight> {
if let Some(in_flight_request) = self
.get_concurrency_limiter(access_token.primary_id())
.concurrent_uploads
@ -105,7 +116,7 @@ impl JMAP {
}
}
pub async fn is_auth_allowed_soft(&self, addr: &IpAddr) -> trc::Result<()> {
async fn is_auth_allowed_soft(&self, addr: &IpAddr) -> trc::Result<()> {
if let Some(rate) = &self.core.jmap.rate_authenticate_req {
if self
.core
@ -122,7 +133,7 @@ impl JMAP {
Ok(())
}
pub async fn is_auth_allowed_hard(&self, addr: &IpAddr) -> trc::Result<()> {
async fn is_auth_allowed_hard(&self, addr: &IpAddr) -> trc::Result<()> {
if let Some(rate) = &self.core.jmap.rate_authenticate_req {
if self
.core
@ -139,9 +150,3 @@ impl JMAP {
Ok(())
}
}
impl ConcurrencyLimiters {
pub fn is_active(&self) -> bool {
self.concurrent_requests.is_active() || self.concurrent_uploads.is_active()
}
}

View file

@ -4,23 +4,34 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use common::auth::AccessToken;
use common::{auth::AccessToken, Server};
use jmap_proto::{
error::set::{SetError, SetErrorType},
method::copy::{CopyBlobRequest, CopyBlobResponse},
types::blob::BlobId,
};
use std::future::Future;
use store::{
write::{now, BatchBuilder, BlobOp},
BlobClass, Serialize,
};
use utils::map::vec_map::VecMap;
use crate::JMAP;
use crate::JmapMethods;
impl JMAP {
pub async fn blob_copy(
use super::download::BlobDownload;
pub trait BlobCopy: Sync + Send {
fn blob_copy(
&self,
request: CopyBlobRequest,
access_token: &AccessToken,
) -> impl Future<Output = trc::Result<CopyBlobResponse>> + Send;
}
impl BlobCopy for Server {
async fn blob_copy(
&self,
request: CopyBlobRequest,
access_token: &AccessToken,

View file

@ -6,7 +6,7 @@
use std::ops::Range;
use common::auth::AccessToken;
use common::{auth::AccessToken, Server};
use jmap_proto::types::{
acl::Acl,
blob::{BlobId, BlobSection},
@ -16,15 +16,42 @@ use mail_parser::{
decoders::{base64::base64_decode, quoted_printable::quoted_printable_decode},
Encoding,
};
use std::future::Future;
use store::BlobClass;
use trc::AddContext;
use utils::BlobHash;
use crate::JMAP;
use crate::auth::acl::AclMethods;
impl JMAP {
pub trait BlobDownload: Sync + Send {
fn blob_download(
&self,
blob_id: &BlobId,
access_token: &AccessToken,
) -> impl Future<Output = trc::Result<Option<Vec<u8>>>> + Send;
fn get_blob_section(
&self,
hash: &BlobHash,
section: &BlobSection,
) -> impl Future<Output = trc::Result<Option<Vec<u8>>>> + Send;
fn get_blob(
&self,
hash: &BlobHash,
range: Range<usize>,
) -> impl Future<Output = trc::Result<Option<Vec<u8>>>> + Send;
fn has_access_blob(
&self,
blob_id: &BlobId,
access_token: &AccessToken,
) -> impl Future<Output = trc::Result<bool>> + Send;
}
impl BlobDownload for Server {
#[allow(clippy::blocks_in_conditions)]
pub async fn blob_download(
async fn blob_download(
&self,
blob_id: &BlobId,
access_token: &AccessToken,
@ -84,7 +111,7 @@ impl JMAP {
}
}
pub async fn get_blob_section(
async fn get_blob_section(
&self,
hash: &BlobHash,
section: &BlobSection,
@ -102,11 +129,7 @@ impl JMAP {
}))
}
pub async fn get_blob(
&self,
hash: &BlobHash,
range: Range<usize>,
) -> trc::Result<Option<Vec<u8>>> {
async fn get_blob(&self, hash: &BlobHash, range: Range<usize>) -> trc::Result<Option<Vec<u8>>> {
self.core
.storage
.blob
@ -115,7 +138,7 @@ impl JMAP {
.caused_by(trc::location!())
}
pub async fn has_access_blob(
async fn has_access_blob(
&self,
blob_id: &BlobId,
access_token: &AccessToken,

View file

@ -4,7 +4,7 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use common::auth::AccessToken;
use common::{auth::AccessToken, Server};
use jmap_proto::{
method::{
get::{GetRequest, GetResponse},
@ -26,10 +26,26 @@ use sha2::{Sha256, Sha512};
use store::BlobClass;
use utils::map::vec_map::VecMap;
use crate::{mailbox::UidMailbox, JMAP};
use crate::{mailbox::UidMailbox, JmapMethods};
use std::future::Future;
impl JMAP {
pub async fn blob_get(
use super::download::BlobDownload;
pub trait BlobOperations: Sync + Send {
fn blob_get(
&self,
request: GetRequest<GetArguments>,
access_token: &AccessToken,
) -> impl Future<Output = trc::Result<GetResponse>> + Send;
fn blob_lookup(
&self,
request: BlobLookupRequest,
) -> impl Future<Output = trc::Result<BlobLookupResponse>> + Send;
}
impl BlobOperations for Server {
async fn blob_get(
&self,
mut request: GetRequest<GetArguments>,
access_token: &AccessToken,
@ -148,7 +164,7 @@ impl JMAP {
Ok(response)
}
pub async fn blob_lookup(&self, request: BlobLookupRequest) -> trc::Result<BlobLookupResponse> {
async fn blob_lookup(&self, request: BlobLookupRequest) -> trc::Result<BlobLookupResponse> {
let mut include_email = false;
let mut include_mailbox = false;
let mut include_thread = false;

View file

@ -6,7 +6,7 @@
use std::sync::Arc;
use common::auth::AccessToken;
use common::{auth::AccessToken, Server};
use directory::Permission;
use jmap_proto::{
error::set::SetError,
@ -23,16 +23,40 @@ use store::{
use trc::AddContext;
use utils::BlobHash;
use crate::JMAP;
use crate::{auth::rate_limit::RateLimiter, JmapMethods};
use super::UploadResponse;
use super::{download::BlobDownload, UploadResponse};
use std::future::Future;
#[cfg(feature = "test_mode")]
pub static DISABLE_UPLOAD_QUOTA: std::sync::atomic::AtomicBool =
std::sync::atomic::AtomicBool::new(true);
impl JMAP {
pub async fn blob_upload_many(
pub trait BlobUpload: Sync + Send {
fn blob_upload_many(
&self,
request: BlobUploadRequest,
access_token: &AccessToken,
) -> impl Future<Output = trc::Result<BlobUploadResponse>> + Send;
fn blob_upload(
&self,
account_id: Id,
content_type: &str,
data: &[u8],
access_token: Arc<AccessToken>,
) -> impl Future<Output = trc::Result<UploadResponse>> + Send;
fn put_blob(
&self,
account_id: u32,
data: &[u8],
set_quota: bool,
) -> impl Future<Output = trc::Result<BlobId>> + Send;
}
impl BlobUpload for Server {
async fn blob_upload_many(
&self,
request: BlobUploadRequest,
access_token: &AccessToken,
@ -178,7 +202,7 @@ impl JMAP {
Ok(response)
}
pub async fn blob_upload(
async fn blob_upload(
&self,
account_id: Id,
content_type: &str,
@ -239,12 +263,7 @@ impl JMAP {
}
#[allow(clippy::blocks_in_conditions)]
pub async fn put_blob(
&self,
account_id: u32,
data: &[u8],
set_quota: bool,
) -> trc::Result<BlobId> {
async fn put_blob(&self, account_id: u32, data: &[u8], set_quota: bool) -> trc::Result<BlobId> {
// First reserve the hash
let hash = BlobHash::from(data);
let mut batch = BatchBuilder::new();

View file

@ -4,18 +4,32 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use common::auth::AccessToken;
use common::{auth::AccessToken, Server};
use jmap_proto::{
method::changes::{ChangesRequest, ChangesResponse, RequestArguments},
types::{collection::Collection, property::Property, state::State},
};
use std::future::Future;
use store::query::log::{Change, Changes, Query};
use trc::AddContext;
use crate::JMAP;
pub trait ChangesLookup: Sync + Send {
fn changes(
&self,
request: ChangesRequest,
access_token: &AccessToken,
) -> impl Future<Output = trc::Result<ChangesResponse>> + Send;
impl JMAP {
pub async fn changes(
fn changes_(
&self,
account_id: u32,
collection: Collection,
query: Query,
) -> impl Future<Output = trc::Result<Changes>> + Send;
}
impl ChangesLookup for Server {
async fn changes(
&self,
request: ChangesRequest,
access_token: &AccessToken,
@ -160,7 +174,7 @@ impl JMAP {
Ok(response)
}
pub async fn changes_(
async fn changes_(
&self,
account_id: u32,
collection: Collection,

View file

@ -4,17 +4,31 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use common::auth::AccessToken;
use common::{auth::AccessToken, Server};
use jmap_proto::method::{
changes::{self, ChangesRequest},
query::{self, QueryRequest},
query_changes::{AddedItem, QueryChangesRequest, QueryChangesResponse},
};
use std::future::Future;
use crate::JMAP;
use crate::{
email::query::EmailQuery, mailbox::query::MailboxQuery, quota::query::QuotaQuery,
submission::query::EmailSubmissionQuery,
};
impl JMAP {
pub async fn query_changes(
use super::get::ChangesLookup;
pub trait QueryChanges: Sync + Send {
fn query_changes(
&self,
request: QueryChangesRequest,
access_token: &AccessToken,
) -> impl Future<Output = trc::Result<QueryChangesResponse>> + Send;
}
impl QueryChanges for Server {
async fn query_changes(
&self,
request: QueryChangesRequest,
access_token: &AccessToken,

View file

@ -4,16 +4,31 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use common::Server;
use jmap_proto::types::{collection::Collection, state::State};
use std::future::Future;
use trc::AddContext;
use crate::JMAP;
impl JMAP {
pub async fn get_state(
pub trait StateManager: Sync + Send {
fn get_state(
&self,
account_id: u32,
collection: impl Into<u8>,
collection: impl Into<u8> + Send,
) -> impl Future<Output = trc::Result<State>> + Send;
fn assert_state(
&self,
account_id: u32,
collection: Collection,
if_in_state: &Option<State>,
) -> impl Future<Output = trc::Result<State>> + Send;
}
impl StateManager for Server {
async fn get_state(
&self,
account_id: u32,
collection: impl Into<u8> + Send,
) -> trc::Result<State> {
let collection = collection.into();
self.core
@ -25,7 +40,7 @@ impl JMAP {
.map(State::from)
}
pub async fn assert_state(
async fn assert_state(
&self,
account_id: u32,
collection: Collection,

View file

@ -6,28 +6,47 @@
use std::time::Duration;
use common::Server;
use jmap_proto::types::collection::Collection;
use std::future::Future;
use store::{
write::{log::ChangeLogBuilder, BatchBuilder},
LogKey,
};
use trc::AddContext;
use crate::JMAP;
pub trait ChangeLog: Sync + Send {
fn begin_changes(
&self,
account_id: u32,
) -> impl Future<Output = trc::Result<ChangeLogBuilder>> + Send;
fn assign_change_id(&self, account_id: u32) -> impl Future<Output = trc::Result<u64>> + Send;
fn generate_snowflake_id(&self) -> trc::Result<u64>;
fn commit_changes(
&self,
account_id: u32,
changes: ChangeLogBuilder,
) -> impl Future<Output = trc::Result<u64>> + Send;
fn delete_changes(
&self,
account_id: u32,
before: Duration,
) -> impl Future<Output = trc::Result<()>> + Send;
}
impl JMAP {
pub async fn begin_changes(&self, account_id: u32) -> trc::Result<ChangeLogBuilder> {
impl ChangeLog for Server {
async fn begin_changes(&self, account_id: u32) -> trc::Result<ChangeLogBuilder> {
self.assign_change_id(account_id)
.await
.map(ChangeLogBuilder::with_change_id)
}
pub async fn assign_change_id(&self, _: u32) -> trc::Result<u64> {
async fn assign_change_id(&self, _: u32) -> trc::Result<u64> {
self.generate_snowflake_id()
}
pub fn generate_snowflake_id(&self) -> trc::Result<u64> {
self.inner.snowflake_id.generate().ok_or_else(|| {
fn generate_snowflake_id(&self) -> trc::Result<u64> {
self.inner.data.jmap_id_gen.generate().ok_or_else(|| {
trc::StoreEvent::UnexpectedError
.into_err()
.caused_by(trc::location!())
@ -35,7 +54,7 @@ impl JMAP {
})
}
pub async fn commit_changes(
async fn commit_changes(
&self,
account_id: u32,
mut changes: ChangeLogBuilder,
@ -56,8 +75,8 @@ impl JMAP {
.map(|_| state)
}
pub async fn delete_changes(&self, account_id: u32, before: Duration) -> trc::Result<()> {
let reference_cid = self.inner.snowflake_id.past_id(before).ok_or_else(|| {
async fn delete_changes(&self, account_id: u32, before: Duration) -> trc::Result<()> {
let reference_cid = self.inner.data.jmap_id_gen.past_id(before).ok_or_else(|| {
trc::StoreEvent::UnexpectedError
.caused_by(trc::location!())
.ctx(trc::Key::Reason, "Failed to generate reference change id.")

View file

@ -4,25 +4,29 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::{collections::HashMap, sync::Arc};
use std::sync::Arc;
use common::{Server, Threads};
use jmap_proto::types::{collection::Collection, property::Property};
use std::future::Future;
use trc::AddContext;
use utils::lru_cache::LruCached;
use crate::JMAP;
use crate::JmapMethods;
#[derive(Debug, Default)]
pub struct Threads {
pub threads: HashMap<u32, u32>,
pub modseq: Option<u64>,
}
impl JMAP {
pub async fn get_cached_thread_ids(
pub trait ThreadCache: Sync + Send {
fn get_cached_thread_ids(
&self,
account_id: u32,
message_ids: impl Iterator<Item = u32>,
message_ids: impl Iterator<Item = u32> + Send,
) -> impl Future<Output = trc::Result<Vec<(u32, u32)>>> + Send;
}
impl ThreadCache for Server {
async fn get_cached_thread_ids(
&self,
account_id: u32,
message_ids: impl Iterator<Item = u32> + Send,
) -> trc::Result<Vec<(u32, u32)>> {
// Obtain current state
let modseq = self
@ -34,8 +38,12 @@ impl JMAP {
.caused_by(trc::location!())?;
// Lock the cache
let thread_cache = if let Some(thread_cache) =
self.inner.cache_threads.get(&account_id).and_then(|t| {
let thread_cache = if let Some(thread_cache) = self
.inner
.data
.threads_cache
.get(&account_id)
.and_then(|t| {
if t.modseq.unwrap_or(0) >= modseq.unwrap_or(0) {
Some(t)
} else {
@ -58,7 +66,8 @@ impl JMAP {
modseq,
});
self.inner
.cache_threads
.data
.threads_cache
.insert(account_id, thread_cache.clone());
thread_cache
};

View file

@ -4,7 +4,10 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use common::auth::{AccessToken, ResourceToken};
use common::{
auth::{AccessToken, ResourceToken},
Server,
};
use jmap_proto::{
error::set::SetError,
method::{
@ -42,16 +45,46 @@ use store::{
use trc::AddContext;
use utils::map::vec_map::VecMap;
use crate::{api::http::HttpSessionData, mailbox::UidMailbox, JMAP};
use crate::{
api::http::HttpSessionData,
auth::acl::AclMethods,
changes::{state::StateManager, write::ChangeLog},
mailbox::{set::MailboxSet, UidMailbox},
services::index::Indexer,
JmapMethods,
};
use std::future::Future;
use super::{
index::{EmailIndexBuilder, TrimTextValue, VisitValues, MAX_ID_LENGTH, MAX_SORT_FIELD_LENGTH},
ingest::{IngestedEmail, LogEmailInsert},
ingest::{EmailIngest, IngestedEmail, LogEmailInsert},
metadata::MessageMetadata,
};
impl JMAP {
pub async fn email_copy(
pub trait EmailCopy: Sync + Send {
fn email_copy(
&self,
request: CopyRequest<RequestArguments>,
access_token: &AccessToken,
next_call: &mut Option<Call<RequestMethod>>,
session: &HttpSessionData,
) -> impl Future<Output = trc::Result<CopyResponse>> + Send;
#[allow(clippy::too_many_arguments)]
fn copy_message(
&self,
from_account_id: u32,
from_message_id: u32,
resource_token: &ResourceToken,
mailboxes: Vec<u32>,
keywords: Vec<Keyword>,
received_at: Option<UTCDate>,
session_id: u64,
) -> impl Future<Output = trc::Result<Result<IngestedEmail, SetError>>> + Send;
}
impl EmailCopy for Server {
async fn email_copy(
&self,
request: CopyRequest<RequestArguments>,
access_token: &AccessToken,
@ -271,7 +304,7 @@ impl JMAP {
}
#[allow(clippy::too_many_arguments)]
pub async fn copy_message(
async fn copy_message(
&self,
from_account_id: u32,
from_message_id: u32,
@ -435,7 +468,7 @@ impl JMAP {
let document_id = ids.last_document_id().caused_by(trc::location!())?;
// Request FTS index
self.inner.request_fts_index();
self.request_fts_index();
// Update response
email.id = Id::from_parts(thread_id, document_id);

Some files were not shown because too many files have changed in this diff Show more