mirror of
https://github.com/stalwartlabs/mail-server.git
synced 2024-11-24 06:19:46 +00:00
Core refactoring
This commit is contained in:
parent
24967c1e86
commit
ce8182ae07
267 changed files with 5886 additions and 4461 deletions
3
.github/workflows/test.yml
vendored
3
.github/workflows/test.yml
vendored
|
@ -2,9 +2,6 @@ name: Test
|
|||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
push:
|
||||
tags: ["v*.*.*"]
|
||||
|
||||
jobs:
|
||||
style:
|
||||
|
|
115
Cargo.lock
generated
115
Cargo.lock
generated
|
@ -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",
|
||||
]
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>> {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
163
crates/common/src/config/inner.rs
Normal file
163
crates/common/src/config/inner.rs
Normal 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),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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>,
|
||||
|
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
335
crates/common/src/core.rs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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()))
|
||||
}
|
||||
|
|
|
@ -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>> {
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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
233
crates/common/src/ipc.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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,
|
||||
) {
|
||||
|
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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()
|
||||
);
|
||||
|
||||
|
|
|
@ -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?,
|
||||
) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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!())?;
|
||||
|
|
|
@ -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),
|
||||
)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
},
|
||||
|
|
|
@ -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),
|
||||
)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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),
|
||||
)
|
||||
|
|
|
@ -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!())?;
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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>,
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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>,
|
||||
|
|
|
@ -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.")
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>>,
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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": (),
|
||||
|
|
|
@ -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>,
|
||||
|
|
|
@ -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>,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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": (),
|
||||
|
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>,
|
||||
|
|
|
@ -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>,
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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.")
|
||||
|
|
|
@ -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
|
||||
};
|
||||
|
|
|
@ -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
Loading…
Reference in a new issue