mirror of
https://github.com/stalwartlabs/mail-server.git
synced 2024-11-28 00:56:45 +00:00
Settings hot reloading - Part 3
This commit is contained in:
parent
5756815e3e
commit
d8af9b4576
303 changed files with 4201 additions and 17262 deletions
243
Cargo.lock
generated
243
Cargo.lock
generated
|
@ -1005,10 +1005,10 @@ dependencies = [
|
|||
"mail-send",
|
||||
"md5",
|
||||
"nlp",
|
||||
"opentelemetry 0.22.0",
|
||||
"opentelemetry-otlp 0.15.0",
|
||||
"opentelemetry-semantic-conventions 0.14.0",
|
||||
"opentelemetry_sdk 0.22.1",
|
||||
"opentelemetry",
|
||||
"opentelemetry-otlp",
|
||||
"opentelemetry-semantic-conventions",
|
||||
"opentelemetry_sdk",
|
||||
"parking_lot",
|
||||
"pem",
|
||||
"privdrop",
|
||||
|
@ -1032,7 +1032,7 @@ dependencies = [
|
|||
"tracing",
|
||||
"tracing-appender",
|
||||
"tracing-journald",
|
||||
"tracing-opentelemetry 0.23.0",
|
||||
"tracing-opentelemetry",
|
||||
"tracing-subscriber",
|
||||
"unicode-security",
|
||||
"utils",
|
||||
|
@ -2669,6 +2669,7 @@ name = "imap"
|
|||
version = "0.6.0"
|
||||
dependencies = [
|
||||
"ahash 0.8.11",
|
||||
"common",
|
||||
"dashmap",
|
||||
"directory",
|
||||
"imap_proto",
|
||||
|
@ -2872,6 +2873,7 @@ dependencies = [
|
|||
"bincode",
|
||||
"cbc",
|
||||
"chrono",
|
||||
"common",
|
||||
"dashmap",
|
||||
"directory",
|
||||
"ece",
|
||||
|
@ -3272,6 +3274,7 @@ dependencies = [
|
|||
name = "mail-server"
|
||||
version = "0.6.0"
|
||||
dependencies = [
|
||||
"common",
|
||||
"directory",
|
||||
"imap",
|
||||
"jemallocator",
|
||||
|
@ -3291,6 +3294,7 @@ version = "0.6.0"
|
|||
dependencies = [
|
||||
"ahash 0.8.11",
|
||||
"bincode",
|
||||
"common",
|
||||
"directory",
|
||||
"imap",
|
||||
"imap_proto",
|
||||
|
@ -3774,22 +3778,6 @@ dependencies = [
|
|||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "opentelemetry"
|
||||
version = "0.21.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e32339a5dc40459130b3bd269e9892439f55b33e772d2a9d402a789baaf4e8a"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"indexmap 2.2.5",
|
||||
"js-sys",
|
||||
"once_cell",
|
||||
"pin-project-lite",
|
||||
"thiserror",
|
||||
"urlencoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "opentelemetry"
|
||||
version = "0.22.0"
|
||||
|
@ -3805,19 +3793,6 @@ dependencies = [
|
|||
"urlencoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "opentelemetry-http"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f51189ce8be654f9b5f7e70e49967ed894e84a06fc35c6c042e64ac1fc5399e"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"bytes",
|
||||
"http 0.2.12",
|
||||
"opentelemetry 0.21.0",
|
||||
"reqwest 0.11.26",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "opentelemetry-http"
|
||||
version = "0.11.0"
|
||||
|
@ -3827,31 +3802,10 @@ dependencies = [
|
|||
"async-trait",
|
||||
"bytes",
|
||||
"http 0.2.12",
|
||||
"opentelemetry 0.22.0",
|
||||
"opentelemetry",
|
||||
"reqwest 0.11.26",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "opentelemetry-otlp"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f24cda83b20ed2433c68241f918d0f6fdec8b1d43b7a9590ab4420c5095ca930"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"futures-core",
|
||||
"http 0.2.12",
|
||||
"opentelemetry 0.21.0",
|
||||
"opentelemetry-http 0.10.0",
|
||||
"opentelemetry-proto 0.4.0",
|
||||
"opentelemetry-semantic-conventions 0.13.0",
|
||||
"opentelemetry_sdk 0.21.2",
|
||||
"prost 0.11.9",
|
||||
"reqwest 0.11.26",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tonic 0.9.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "opentelemetry-otlp"
|
||||
version = "0.15.0"
|
||||
|
@ -3861,28 +3815,16 @@ dependencies = [
|
|||
"async-trait",
|
||||
"futures-core",
|
||||
"http 0.2.12",
|
||||
"opentelemetry 0.22.0",
|
||||
"opentelemetry-http 0.11.0",
|
||||
"opentelemetry-proto 0.5.0",
|
||||
"opentelemetry-semantic-conventions 0.14.0",
|
||||
"opentelemetry_sdk 0.22.1",
|
||||
"prost 0.12.3",
|
||||
"opentelemetry",
|
||||
"opentelemetry-http",
|
||||
"opentelemetry-proto",
|
||||
"opentelemetry-semantic-conventions",
|
||||
"opentelemetry_sdk",
|
||||
"prost",
|
||||
"reqwest 0.11.26",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tonic 0.11.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "opentelemetry-proto"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2e155ce5cc812ea3d1dffbd1539aed653de4bf4882d60e6e04dcf0901d674e1"
|
||||
dependencies = [
|
||||
"opentelemetry 0.21.0",
|
||||
"opentelemetry_sdk 0.21.2",
|
||||
"prost 0.11.9",
|
||||
"tonic 0.9.2",
|
||||
"tonic",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -3891,19 +3833,10 @@ version = "0.5.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a8fddc9b68f5b80dae9d6f510b88e02396f006ad48cac349411fbecc80caae4"
|
||||
dependencies = [
|
||||
"opentelemetry 0.22.0",
|
||||
"opentelemetry_sdk 0.22.1",
|
||||
"prost 0.12.3",
|
||||
"tonic 0.11.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "opentelemetry-semantic-conventions"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f5774f1ef1f982ef2a447f6ee04ec383981a3ab99c8e77a1a7b30182e65bbc84"
|
||||
dependencies = [
|
||||
"opentelemetry 0.21.0",
|
||||
"opentelemetry",
|
||||
"opentelemetry_sdk",
|
||||
"prost",
|
||||
"tonic",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -3912,28 +3845,6 @@ version = "0.14.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f9ab5bd6c42fb9349dcf28af2ba9a0667f697f9bdcca045d39f2cec5543e2910"
|
||||
|
||||
[[package]]
|
||||
name = "opentelemetry_sdk"
|
||||
version = "0.21.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2f16aec8a98a457a52664d69e0091bac3a0abd18ead9b641cb00202ba4e0efe4"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"crossbeam-channel",
|
||||
"futures-channel",
|
||||
"futures-executor",
|
||||
"futures-util",
|
||||
"glob",
|
||||
"once_cell",
|
||||
"opentelemetry 0.21.0",
|
||||
"ordered-float",
|
||||
"percent-encoding",
|
||||
"rand",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "opentelemetry_sdk"
|
||||
version = "0.22.1"
|
||||
|
@ -3947,7 +3858,7 @@ dependencies = [
|
|||
"futures-util",
|
||||
"glob",
|
||||
"once_cell",
|
||||
"opentelemetry 0.22.0",
|
||||
"opentelemetry",
|
||||
"ordered-float",
|
||||
"percent-encoding",
|
||||
"rand",
|
||||
|
@ -4379,16 +4290,6 @@ dependencies = [
|
|||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prost"
|
||||
version = "0.11.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b82eaa1d779e9a4bc1c3217db8ffbeabaae1dca241bf70183242128d48681cd"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"prost-derive 0.11.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prost"
|
||||
version = "0.12.3"
|
||||
|
@ -4396,20 +4297,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "146c289cda302b98a28d40c8b3b90498d6e526dd24ac2ecea73e4e491685b94a"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"prost-derive 0.12.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prost-derive"
|
||||
version = "0.11.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5d2d8d10f3c6ded6da8b05b5fb3b8a5082514344d56c9f871412d29b4e075b4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.10.5",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
"prost-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -5498,9 +5386,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "serial_test"
|
||||
version = "2.0.0"
|
||||
version = "3.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0e56dd856803e253c8f298af3f4d7eb0ae5e23a737252cd90bb4f3b435033b2d"
|
||||
checksum = "953ad9342b3aaca7cb43c45c097dd008d4907070394bd0751a0aa8817e5a018d"
|
||||
dependencies = [
|
||||
"dashmap",
|
||||
"futures",
|
||||
|
@ -5512,9 +5400,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "serial_test_derive"
|
||||
version = "2.0.0"
|
||||
version = "3.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91d129178576168c589c9ec973feedf7d3126c01ac2bf08795109aa35b69fb8f"
|
||||
checksum = "b93fb4adc70021ac1b47f7d45e8cc4169baaa7ea58483bc5b721d19a26202212"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
@ -5681,6 +5569,7 @@ dependencies = [
|
|||
"ahash 0.8.11",
|
||||
"bincode",
|
||||
"blake3",
|
||||
"common",
|
||||
"dashmap",
|
||||
"decancer",
|
||||
"directory",
|
||||
|
@ -6077,9 +5966,10 @@ version = "0.1.0"
|
|||
dependencies = [
|
||||
"ahash 0.8.11",
|
||||
"async-trait",
|
||||
"base64 0.21.7",
|
||||
"base64 0.22.0",
|
||||
"bytes",
|
||||
"chrono",
|
||||
"common",
|
||||
"csv",
|
||||
"dashmap",
|
||||
"directory",
|
||||
|
@ -6360,34 +6250,6 @@ dependencies = [
|
|||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tonic"
|
||||
version = "0.9.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3082666a3a6433f7f511c7192923fa1fe07c69332d3c6a2e6bb040b569199d5a"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum",
|
||||
"base64 0.21.7",
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"h2 0.3.24",
|
||||
"http 0.2.12",
|
||||
"http-body 0.4.6",
|
||||
"hyper 0.14.28",
|
||||
"hyper-timeout",
|
||||
"percent-encoding",
|
||||
"pin-project",
|
||||
"prost 0.11.9",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"tower",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tonic"
|
||||
version = "0.11.0"
|
||||
|
@ -6406,7 +6268,7 @@ dependencies = [
|
|||
"hyper-timeout",
|
||||
"percent-encoding",
|
||||
"pin-project",
|
||||
"prost 0.12.3",
|
||||
"prost",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"tower",
|
||||
|
@ -6514,24 +6376,6 @@ dependencies = [
|
|||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-opentelemetry"
|
||||
version = "0.22.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c67ac25c5407e7b961fafc6f7e9aa5958fd297aada2d20fa2ae1737357e55596"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"once_cell",
|
||||
"opentelemetry 0.21.0",
|
||||
"opentelemetry_sdk 0.21.2",
|
||||
"smallvec",
|
||||
"tracing",
|
||||
"tracing-core",
|
||||
"tracing-log",
|
||||
"tracing-subscriber",
|
||||
"web-time 0.2.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-opentelemetry"
|
||||
version = "0.23.0"
|
||||
|
@ -6540,14 +6384,14 @@ checksum = "a9be14ba1bbe4ab79e9229f7f89fab8d120b865859f10527f31c033e599d2284"
|
|||
dependencies = [
|
||||
"js-sys",
|
||||
"once_cell",
|
||||
"opentelemetry 0.22.0",
|
||||
"opentelemetry_sdk 0.22.1",
|
||||
"opentelemetry",
|
||||
"opentelemetry_sdk",
|
||||
"smallvec",
|
||||
"tracing",
|
||||
"tracing-core",
|
||||
"tracing-log",
|
||||
"tracing-subscriber",
|
||||
"web-time 1.1.0",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -6756,7 +6600,6 @@ name = "utils"
|
|||
version = "0.6.0"
|
||||
dependencies = [
|
||||
"ahash 0.8.11",
|
||||
"arc-swap",
|
||||
"base64 0.21.7",
|
||||
"blake3",
|
||||
"chrono",
|
||||
|
@ -6767,14 +6610,9 @@ dependencies = [
|
|||
"lru-cache",
|
||||
"mail-auth",
|
||||
"mail-send",
|
||||
"opentelemetry 0.21.0",
|
||||
"opentelemetry-otlp 0.14.0",
|
||||
"opentelemetry-semantic-conventions 0.13.0",
|
||||
"opentelemetry_sdk 0.21.2",
|
||||
"parking_lot",
|
||||
"pem",
|
||||
"privdrop",
|
||||
"proxy-header",
|
||||
"rand",
|
||||
"rcgen",
|
||||
"regex",
|
||||
|
@ -6789,10 +6627,7 @@ dependencies = [
|
|||
"tokio",
|
||||
"tokio-rustls 0.25.0",
|
||||
"tracing",
|
||||
"tracing-appender",
|
||||
"tracing-journald",
|
||||
"tracing-opentelemetry 0.22.0",
|
||||
"tracing-subscriber",
|
||||
"webpki-roots 0.26.1",
|
||||
"x509-parser 0.16.0",
|
||||
]
|
||||
|
@ -6950,16 +6785,6 @@ dependencies = [
|
|||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "web-time"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aa30049b1c872b72c89866d458eae9f20380ab280ffd1b1e18df2d3e2d98cfe0"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "web-time"
|
||||
version = "1.1.0"
|
||||
|
|
|
@ -15,7 +15,6 @@ impl Core {
|
|||
directory: &Directory,
|
||||
email: &str,
|
||||
) -> directory::Result<Vec<u32>> {
|
||||
let todo = "update functions using this method.";
|
||||
let mut address = self
|
||||
.smtp
|
||||
.session
|
||||
|
@ -142,8 +141,8 @@ impl AddressMapping {
|
|||
|
||||
struct Address<'x>(&'x str);
|
||||
|
||||
impl<'x> ResolveVariable<'x> for Address<'x> {
|
||||
fn resolve_variable(&self, _: u32) -> crate::expr::Variable<'x> {
|
||||
impl ResolveVariable for Address<'_> {
|
||||
fn resolve_variable(&self, _: u32) -> crate::expr::Variable {
|
||||
Variable::from(self.0)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,12 +11,6 @@ use utils::{config::Config, map::vec_map::VecMap};
|
|||
|
||||
use super::settings::JmapConfig;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct BaseCapabilities {
|
||||
pub session: VecMap<Capability, Capabilities>,
|
||||
pub account: VecMap<Capability, Capabilities>,
|
||||
}
|
||||
|
||||
impl JmapConfig {
|
||||
pub fn add_capabilites(&mut self, config: &mut Config) {
|
||||
// Add core capabilities
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
use std::{str::FromStr, time::Duration};
|
||||
|
||||
use jmap_proto::request::capability::BaseCapabilities;
|
||||
use mail_parser::HeaderName;
|
||||
use nlp::language::Language;
|
||||
use store::rand::{distributions::Alphanumeric, thread_rng, Rng};
|
||||
use utils::config::{cron::SimpleCron, utils::ParseValue, Config, Rate};
|
||||
|
||||
use super::capabilities::BaseCapabilities;
|
||||
|
||||
pub struct JmapConfig {
|
||||
pub default_language: Language,
|
||||
pub query_max_results: usize,
|
||||
|
@ -40,7 +39,6 @@ pub struct JmapConfig {
|
|||
pub rate_authenticated: Option<Rate>,
|
||||
pub rate_authenticate_req: Option<Rate>,
|
||||
pub rate_anonymous: Option<Rate>,
|
||||
pub rate_use_forwarded: bool,
|
||||
|
||||
pub event_source_throttle: Duration,
|
||||
pub push_max_total: usize,
|
||||
|
@ -66,6 +64,7 @@ pub struct JmapConfig {
|
|||
pub spam_header: Option<(HeaderName<'static>, String)>,
|
||||
|
||||
pub http_headers: Vec<(hyper::header::HeaderName, hyper::header::HeaderValue)>,
|
||||
pub http_use_forwarded: bool,
|
||||
|
||||
pub encrypt: bool,
|
||||
pub encrypt_append: bool,
|
||||
|
@ -148,9 +147,6 @@ impl JmapConfig {
|
|||
rate_authenticate_req: config
|
||||
.property_or_default_("authentication.rate-limit", "10/1m"),
|
||||
rate_anonymous: config.property_or_default_("jmap.rate-limit.anonymous", "100/1m"),
|
||||
rate_use_forwarded: config
|
||||
.property_("jmap.rate-limit.use-forwarded")
|
||||
.unwrap_or(false),
|
||||
oauth_key: config
|
||||
.value("oauth.key")
|
||||
.map(|s| s.to_string())
|
||||
|
@ -216,6 +212,9 @@ impl JmapConfig {
|
|||
)
|
||||
})
|
||||
}),
|
||||
http_use_forwarded: config
|
||||
.property_("server.http.use-x-forwarded")
|
||||
.unwrap_or(false),
|
||||
http_headers: config
|
||||
.values("server.http.headers")
|
||||
.map(|(_, v)| {
|
||||
|
|
|
@ -1,7 +1,135 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use arc_swap::ArcSwap;
|
||||
use directory::{Directories, Directory};
|
||||
use store::{BlobBackend, BlobStore, FtsStore, LookupStore, Store, Stores};
|
||||
use utils::config::Config;
|
||||
|
||||
use crate::{Core, Network};
|
||||
|
||||
use self::{
|
||||
imap::ImapConfig, jmap::settings::JmapConfig, scripts::Scripting, smtp::SmtpConfig,
|
||||
storage::Storage,
|
||||
};
|
||||
|
||||
pub mod imap;
|
||||
pub mod jmap;
|
||||
pub mod network;
|
||||
pub mod scripts;
|
||||
pub mod server;
|
||||
pub mod smtp;
|
||||
pub mod storage;
|
||||
pub mod tracers;
|
||||
|
||||
impl Core {
|
||||
pub async fn parse(config: &mut Config, stores: Stores) -> Self {
|
||||
let mut data = config
|
||||
.value_require_("storage.data")
|
||||
.map(|id| id.to_string())
|
||||
.and_then(|id| {
|
||||
if let Some(store) = stores.stores.get(&id) {
|
||||
store.clone().into()
|
||||
} else {
|
||||
config.new_parse_error("storage.data", format!("Data store {id:?} not found"));
|
||||
None
|
||||
}
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let mut blob = config
|
||||
.value_require_("storage.blob")
|
||||
.map(|id| id.to_string())
|
||||
.and_then(|id| {
|
||||
if let Some(store) = stores.blob_stores.get(&id) {
|
||||
store.clone().into()
|
||||
} else {
|
||||
config.new_parse_error("storage.blob", format!("Blob store {id:?} not found"));
|
||||
None
|
||||
}
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let mut lookup = config
|
||||
.value_require_("storage.lookup")
|
||||
.map(|id| id.to_string())
|
||||
.and_then(|id| {
|
||||
if let Some(store) = stores.lookup_stores.get(&id) {
|
||||
store.clone().into()
|
||||
} else {
|
||||
config.new_parse_error(
|
||||
"storage.lookup",
|
||||
format!("Lookup store {id:?} not found"),
|
||||
);
|
||||
None
|
||||
}
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let mut fts = config
|
||||
.value_require_("storage.fts")
|
||||
.map(|id| id.to_string())
|
||||
.and_then(|id| {
|
||||
if let Some(store) = stores.fts_stores.get(&id) {
|
||||
store.clone().into()
|
||||
} else {
|
||||
config.new_parse_error(
|
||||
"storage.fts",
|
||||
format!("Full-text store {id:?} not found"),
|
||||
);
|
||||
None
|
||||
}
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let directories = Directories::parse(config, &stores, data.clone()).await;
|
||||
let directory = config
|
||||
.value_require_("storage.directory")
|
||||
.map(|id| id.to_string())
|
||||
.and_then(|id| {
|
||||
if let Some(directory) = directories.directories.get(&id) {
|
||||
directory.clone().into()
|
||||
} else {
|
||||
config.new_parse_error(
|
||||
"storage.directory",
|
||||
format!("Directory {id:?} not found"),
|
||||
);
|
||||
None
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|| Arc::new(Directory::default()));
|
||||
|
||||
// If any of the stores are missing, disable all stores to avoid data loss
|
||||
if matches!(data, Store::None)
|
||||
|| matches!(&blob.backend, BlobBackend::Store(Store::None))
|
||||
|| matches!(lookup, LookupStore::Store(Store::None))
|
||||
|| matches!(fts, FtsStore::Store(Store::None))
|
||||
{
|
||||
data = Store::default();
|
||||
blob = BlobStore::default();
|
||||
lookup = LookupStore::default();
|
||||
fts = FtsStore::default();
|
||||
config.new_build_error(
|
||||
"storage.*",
|
||||
"One or more stores are missing, disabling all stores",
|
||||
)
|
||||
}
|
||||
|
||||
Self {
|
||||
sieve: Scripting::parse(config, &stores).await,
|
||||
network: Network::parse(config),
|
||||
smtp: SmtpConfig::parse(config).await,
|
||||
jmap: JmapConfig::parse(config),
|
||||
imap: ImapConfig::parse(config),
|
||||
storage: Storage {
|
||||
data,
|
||||
blob,
|
||||
fts,
|
||||
lookup,
|
||||
lookups: stores.lookup_stores,
|
||||
directory,
|
||||
directories: directories.directories,
|
||||
purge_schedules: stores.purge_schedules,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_shared(self) -> Arc<ArcSwap<Self>> {
|
||||
Arc::new(ArcSwap::from_pointee(self))
|
||||
}
|
||||
}
|
||||
|
|
45
crates/common/src/config/network.rs
Normal file
45
crates/common/src/config/network.rs
Normal file
|
@ -0,0 +1,45 @@
|
|||
use utils::config::Config;
|
||||
|
||||
use crate::{
|
||||
expr::{if_block::IfBlock, tokenizer::TokenMap},
|
||||
listener::blocked::BlockedIps,
|
||||
Network,
|
||||
};
|
||||
|
||||
use super::smtp::*;
|
||||
|
||||
impl Default for Network {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
blocked_ips: Default::default(),
|
||||
hostname: IfBlock::new("localhost".to_string()),
|
||||
url: IfBlock::new("http://localhost:8080".to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Network {
|
||||
pub fn parse(config: &mut Config) -> Self {
|
||||
let mut network = Network {
|
||||
blocked_ips: BlockedIps::parse(config),
|
||||
..Default::default()
|
||||
};
|
||||
let token_map = &TokenMap::default().with_smtp_variables(&[
|
||||
V_LISTENER,
|
||||
V_REMOTE_IP,
|
||||
V_LOCAL_IP,
|
||||
V_HELO_DOMAIN,
|
||||
]);
|
||||
|
||||
for (value, key) in [
|
||||
(&mut network.hostname, "server.hostname"),
|
||||
(&mut network.url, "server.url"),
|
||||
] {
|
||||
if let Some(if_block) = IfBlock::try_parse(config, key, token_map) {
|
||||
*value = if_block;
|
||||
}
|
||||
}
|
||||
|
||||
network
|
||||
}
|
||||
}
|
|
@ -28,6 +28,7 @@ use rustls::{
|
|||
server::ResolvesServerCert,
|
||||
ServerConfig, SupportedCipherSuite, ALL_VERSIONS,
|
||||
};
|
||||
|
||||
use tokio::net::TcpSocket;
|
||||
use tokio_rustls::TlsAcceptor;
|
||||
use utils::config::{
|
||||
|
@ -35,21 +36,19 @@ use utils::config::{
|
|||
Config,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
listener::{acme::directory::ACME_TLS_ALPN_NAME, tls::CertificateResolver, TcpAcceptor},
|
||||
ConfigBuilder,
|
||||
};
|
||||
use crate::listener::{acme::directory::ACME_TLS_ALPN_NAME, tls::CertificateResolver, TcpAcceptor};
|
||||
|
||||
use super::{
|
||||
tls::{TLS12_VERSION, TLS13_VERSION},
|
||||
Listener, Server, ServerProtocol,
|
||||
Listener, Server, ServerProtocol, Servers,
|
||||
};
|
||||
|
||||
impl ConfigBuilder {
|
||||
pub fn parse_servers(&mut self, config: &mut Config) {
|
||||
impl Servers {
|
||||
pub fn parse(config: &mut Config) -> Self {
|
||||
// Parse certificates and ACME managers
|
||||
self.parse_certificates(config);
|
||||
self.parse_acmes(config);
|
||||
let mut servers = Servers::default();
|
||||
servers.parse_certificates(config);
|
||||
servers.parse_acmes(config);
|
||||
|
||||
// Parse servers
|
||||
let ids = config
|
||||
|
@ -57,8 +56,9 @@ impl ConfigBuilder {
|
|||
.map(|s| s.to_string())
|
||||
.collect::<Vec<_>>();
|
||||
for id in ids {
|
||||
self.parse_server(config, id);
|
||||
servers.parse_server(config, id);
|
||||
}
|
||||
servers
|
||||
}
|
||||
|
||||
fn parse_server(&mut self, config: &mut Config, id_: String) {
|
||||
|
|
|
@ -1,13 +1,22 @@
|
|||
use std::{fmt::Display, net::SocketAddr, time::Duration};
|
||||
use std::{fmt::Display, net::SocketAddr, sync::Arc, time::Duration};
|
||||
|
||||
use ahash::AHashMap;
|
||||
use tokio::net::TcpSocket;
|
||||
use utils::config::ipmask::IpAddrMask;
|
||||
|
||||
use crate::listener::TcpAcceptor;
|
||||
use crate::listener::{acme::AcmeManager, tls::Certificate, TcpAcceptor};
|
||||
|
||||
pub mod listener;
|
||||
pub mod tls;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Servers {
|
||||
pub servers: Vec<Server>,
|
||||
pub certificates: AHashMap<String, Arc<Certificate>>,
|
||||
pub certificates_sni: AHashMap<String, Arc<Certificate>>,
|
||||
pub acme_managers: AHashMap<String, Arc<AcmeManager>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Server {
|
||||
pub id: String,
|
||||
|
|
|
@ -37,18 +37,17 @@ use rustls_pemfile::{certs, read_one, Item};
|
|||
use rustls_pki_types::{DnsName, PrivateKeyDer, ServerName};
|
||||
use utils::config::Config;
|
||||
|
||||
use crate::{
|
||||
listener::{
|
||||
acme::{directory::LETS_ENCRYPT_PRODUCTION_DIRECTORY, AcmeManager},
|
||||
tls::Certificate,
|
||||
},
|
||||
ConfigBuilder,
|
||||
use crate::listener::{
|
||||
acme::{directory::LETS_ENCRYPT_PRODUCTION_DIRECTORY, AcmeManager},
|
||||
tls::Certificate,
|
||||
};
|
||||
|
||||
use super::Servers;
|
||||
|
||||
pub static TLS13_VERSION: &[&SupportedProtocolVersion] = &[&TLS13];
|
||||
pub static TLS12_VERSION: &[&SupportedProtocolVersion] = &[&TLS12];
|
||||
|
||||
impl ConfigBuilder {
|
||||
impl Servers {
|
||||
pub fn parse_certificates(&mut self, config: &mut Config) {
|
||||
let cert_ids = config
|
||||
.sub_keys("certificate", ".cert")
|
||||
|
@ -162,7 +161,6 @@ impl ConfigBuilder {
|
|||
domains,
|
||||
contact,
|
||||
renew_before,
|
||||
self.core.storage.data.clone(),
|
||||
) {
|
||||
Ok(acme_manager) => {
|
||||
self.acme_managers
|
||||
|
|
|
@ -386,6 +386,18 @@ impl From<VerifyStrategy> for Constant {
|
|||
}
|
||||
}
|
||||
|
||||
impl VerifyStrategy {
|
||||
#[inline(always)]
|
||||
pub fn verify(&self) -> bool {
|
||||
matches!(self, VerifyStrategy::Strict | VerifyStrategy::Relaxed)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn is_strict(&self) -> bool {
|
||||
matches!(self, VerifyStrategy::Strict)
|
||||
}
|
||||
}
|
||||
|
||||
impl ParseValue for VerifyStrategy {
|
||||
fn parse_value(key: impl AsKey, value: &str) -> Result<Self, String> {
|
||||
match value {
|
||||
|
|
|
@ -68,6 +68,18 @@ pub const VARIABLES_MAP: &[(&str, u32)] = &[
|
|||
("priority", V_PRIORITY),
|
||||
];
|
||||
|
||||
impl SmtpConfig {
|
||||
pub async fn parse(config: &mut Config) -> Self {
|
||||
Self {
|
||||
session: SessionConfig::parse(config),
|
||||
queue: QueueConfig::parse(config),
|
||||
resolvers: Resolvers::parse(config).await,
|
||||
mail_auth: MailAuthConfig::parse(config),
|
||||
report: ReportConfig::parse(config),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TokenMap {
|
||||
pub fn with_smtp_variables(mut self, variables: &[u32]) -> Self {
|
||||
for (name, idx) in VARIABLES_MAP {
|
||||
|
|
|
@ -5,10 +5,13 @@ use mail_auth::IpLookupStrategy;
|
|||
use mail_send::Credentials;
|
||||
use utils::config::{
|
||||
utils::{AsKey, ParseValue},
|
||||
Config, ServerProtocol,
|
||||
Config,
|
||||
};
|
||||
|
||||
use crate::expr::{if_block::IfBlock, Constant, ConstantValue, Expression, Variable};
|
||||
use crate::{
|
||||
config::server::ServerProtocol,
|
||||
expr::{if_block::IfBlock, Constant, ConstantValue, Expression, Variable},
|
||||
};
|
||||
|
||||
use self::throttle::{parse_throttle, parse_throttle_key};
|
||||
|
||||
|
@ -293,10 +296,59 @@ impl QueueConfig {
|
|||
// Parse queue quotas and throttles
|
||||
queue.throttle = parse_queue_throttle(config);
|
||||
queue.quota = parse_queue_quota(config);
|
||||
|
||||
// Parse relay hosts
|
||||
queue.relay_hosts = config
|
||||
.sub_keys("remote", ".address")
|
||||
.map(|id| id.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.into_iter()
|
||||
.filter_map(|id| parse_relay_host(config, &id).map(|host| (id, host)))
|
||||
.collect();
|
||||
|
||||
// Add local delivery host
|
||||
queue.relay_hosts.insert(
|
||||
"local".to_string(),
|
||||
RelayHost {
|
||||
address: String::new(),
|
||||
port: 0,
|
||||
protocol: ServerProtocol::Http,
|
||||
tls_implicit: Default::default(),
|
||||
tls_allow_invalid_certs: Default::default(),
|
||||
auth: None,
|
||||
},
|
||||
);
|
||||
|
||||
queue
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_relay_host(config: &mut Config, id: &str) -> Option<RelayHost> {
|
||||
Some(RelayHost {
|
||||
address: config.property_require_(("remote", id, "address"))?,
|
||||
port: config
|
||||
.property_require_(("remote", id, "port"))
|
||||
.unwrap_or(25),
|
||||
protocol: config
|
||||
.property_require_(("remote", id, "protocol"))
|
||||
.unwrap_or(ServerProtocol::Smtp),
|
||||
auth: if let (Some(username), Some(secret)) = (
|
||||
config.value(("remote", id, "auth.username")),
|
||||
config.value(("remote", id, "auth.secret")),
|
||||
) {
|
||||
Credentials::new(username.to_string(), secret.to_string()).into()
|
||||
} else {
|
||||
None
|
||||
},
|
||||
tls_implicit: config
|
||||
.property_(("remote", id, "tls.implicit"))
|
||||
.unwrap_or(true),
|
||||
tls_allow_invalid_certs: config
|
||||
.property_(("remote", id, "tls.allow-invalid-certs"))
|
||||
.unwrap_or(false),
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_queue_throttle(config: &mut Config) -> QueueThrottle {
|
||||
// Parse throttle
|
||||
let mut throttle = QueueThrottle {
|
||||
|
@ -541,3 +593,15 @@ impl ConstantValue for IpLookupStrategy {
|
|||
.add_constant("ipv4_then_ipv6", IpLookupStrategy::Ipv4thenIpv6);
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for RelayHost {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("RelayHost")
|
||||
.field("address", &self.address)
|
||||
.field("port", &self.port)
|
||||
.field("protocol", &self.protocol)
|
||||
.field("tls_implicit", &self.tls_implicit)
|
||||
.field("tls_allow_invalid_certs", &self.tls_allow_invalid_certs)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,8 @@
|
|||
use std::time::Duration;
|
||||
|
||||
use utils::{
|
||||
config::{
|
||||
utils::{AsKey, ParseValue},
|
||||
Config,
|
||||
},
|
||||
snowflake::SnowflakeIdGenerator,
|
||||
use utils::config::{
|
||||
utils::{AsKey, ParseValue},
|
||||
Config,
|
||||
};
|
||||
|
||||
use crate::expr::{if_block::IfBlock, tokenizer::TokenMap, Constant, ConstantValue, Variable};
|
||||
|
@ -27,7 +24,6 @@ pub struct ReportAnalysis {
|
|||
pub addresses: Vec<AddressMatch>,
|
||||
pub forward: bool,
|
||||
pub store: Option<Duration>,
|
||||
pub report_id: SnowflakeIdGenerator,
|
||||
}
|
||||
|
||||
pub enum AddressMatch {
|
||||
|
@ -109,10 +105,6 @@ impl ReportConfig {
|
|||
.collect(),
|
||||
forward: config.property_("report.analysis.forward").unwrap_or(true),
|
||||
store: config.property_("report.analysis.store"),
|
||||
report_id: config
|
||||
.property_::<u64>("storage.cluster.node-id")
|
||||
.map(SnowflakeIdGenerator::with_node_id)
|
||||
.unwrap_or_default(),
|
||||
},
|
||||
dkim: Report::parse(config, "dkim", &default_hostname, &sender_vars),
|
||||
spf: Report::parse(config, "spf", &default_hostname, &sender_vars),
|
||||
|
@ -212,7 +204,6 @@ impl Default for ReportConfig {
|
|||
addresses: Default::default(),
|
||||
forward: true,
|
||||
store: None,
|
||||
report_id: SnowflakeIdGenerator::new(),
|
||||
},
|
||||
dkim: Default::default(),
|
||||
spf: Default::default(),
|
||||
|
|
|
@ -180,6 +180,8 @@ impl SessionConfig {
|
|||
V_LOCAL_IP,
|
||||
V_HELO_DOMAIN,
|
||||
]);
|
||||
let mt_priority_vars = has_sender_vars.clone().with_constants::<MtPriority>();
|
||||
let mechanisms_vars = has_ehlo_hars.clone().with_constants::<Mechanism>();
|
||||
|
||||
let mut session = SessionConfig::default();
|
||||
session.rcpt.catch_all = AddressMapping::parse(config, "session.rcpt.catch-all");
|
||||
|
@ -200,6 +202,225 @@ impl SessionConfig {
|
|||
.collect();
|
||||
session.throttle = SessionThrottle::parse(config);
|
||||
|
||||
for (value, key, token_map) in [
|
||||
(&mut session.duration, "session.duration", &has_conn_vars),
|
||||
(
|
||||
&mut session.transfer_limit,
|
||||
"session.transfer-limit",
|
||||
&has_conn_vars,
|
||||
),
|
||||
(&mut session.timeout, "session.timeout", &has_conn_vars),
|
||||
(
|
||||
&mut session.connect.script,
|
||||
"session.connect.script",
|
||||
&has_conn_vars,
|
||||
),
|
||||
(
|
||||
&mut session.connect.greeting,
|
||||
"session.connect.greeting",
|
||||
&has_conn_vars,
|
||||
),
|
||||
(
|
||||
&mut session.extensions.pipelining,
|
||||
"session.extensions.pipelining",
|
||||
&has_sender_vars,
|
||||
),
|
||||
(
|
||||
&mut session.extensions.dsn,
|
||||
"session.extensions.dsn",
|
||||
&has_sender_vars,
|
||||
),
|
||||
(
|
||||
&mut session.extensions.vrfy,
|
||||
"session.extensions.vrfy",
|
||||
&has_sender_vars,
|
||||
),
|
||||
(
|
||||
&mut session.extensions.expn,
|
||||
"session.extensions.expn",
|
||||
&has_sender_vars,
|
||||
),
|
||||
(
|
||||
&mut session.extensions.chunking,
|
||||
"session.extensions.chunking",
|
||||
&has_sender_vars,
|
||||
),
|
||||
(
|
||||
&mut session.extensions.requiretls,
|
||||
"session.extensions.requiretls",
|
||||
&has_sender_vars,
|
||||
),
|
||||
(
|
||||
&mut session.extensions.no_soliciting,
|
||||
"session.extensions.no-soliciting",
|
||||
&has_sender_vars,
|
||||
),
|
||||
(
|
||||
&mut session.extensions.future_release,
|
||||
"session.extensions.future-release",
|
||||
&has_sender_vars,
|
||||
),
|
||||
(
|
||||
&mut session.extensions.deliver_by,
|
||||
"session.extensions.deliver-by",
|
||||
&has_sender_vars,
|
||||
),
|
||||
(
|
||||
&mut session.extensions.mt_priority,
|
||||
"session.extensions.mt-priority",
|
||||
&mt_priority_vars,
|
||||
),
|
||||
(
|
||||
&mut session.ehlo.script,
|
||||
"session.ehlo.script",
|
||||
&has_conn_vars,
|
||||
),
|
||||
(
|
||||
&mut session.ehlo.require,
|
||||
"session.ehlo.require",
|
||||
&has_conn_vars,
|
||||
),
|
||||
(
|
||||
&mut session.ehlo.reject_non_fqdn,
|
||||
"session.ehlo.reject-non-fqdn",
|
||||
&has_conn_vars,
|
||||
),
|
||||
(
|
||||
&mut session.auth.directory,
|
||||
"session.auth.directory",
|
||||
&has_ehlo_hars,
|
||||
),
|
||||
(
|
||||
&mut session.auth.mechanisms,
|
||||
"session.auth.mechanisms",
|
||||
&mechanisms_vars,
|
||||
),
|
||||
(
|
||||
&mut session.auth.require,
|
||||
"session.auth.require",
|
||||
&has_ehlo_hars,
|
||||
),
|
||||
(
|
||||
&mut session.auth.errors_max,
|
||||
"session.auth.errors.max",
|
||||
&has_ehlo_hars,
|
||||
),
|
||||
(
|
||||
&mut session.auth.errors_wait,
|
||||
"session.auth.errors.wait",
|
||||
&has_ehlo_hars,
|
||||
),
|
||||
(
|
||||
&mut session.auth.allow_plain_text,
|
||||
"session.auth.allow-plain-text",
|
||||
&has_ehlo_hars,
|
||||
),
|
||||
(
|
||||
&mut session.auth.must_match_sender,
|
||||
"session.auth.must-match-sender",
|
||||
&has_ehlo_hars,
|
||||
),
|
||||
(
|
||||
&mut session.mail.script,
|
||||
"session.mail.script",
|
||||
&has_sender_vars,
|
||||
),
|
||||
(
|
||||
&mut session.mail.rewrite,
|
||||
"session.mail.rewrite",
|
||||
&has_sender_vars,
|
||||
),
|
||||
(
|
||||
&mut session.rcpt.script,
|
||||
"session.rcpt.script",
|
||||
&has_rcpt_vars,
|
||||
),
|
||||
(
|
||||
&mut session.rcpt.relay,
|
||||
"session.rcpt.relay",
|
||||
&has_rcpt_vars,
|
||||
),
|
||||
(
|
||||
&mut session.rcpt.directory,
|
||||
"session.rcpt.directory",
|
||||
&has_rcpt_vars,
|
||||
),
|
||||
(
|
||||
&mut session.rcpt.errors_max,
|
||||
"session.rcpt.errors.max",
|
||||
&has_sender_vars,
|
||||
),
|
||||
(
|
||||
&mut session.rcpt.errors_wait,
|
||||
"session.rcpt.errors.wait",
|
||||
&has_sender_vars,
|
||||
),
|
||||
(
|
||||
&mut session.rcpt.max_recipients,
|
||||
"session.rcpt.max-recipients",
|
||||
&has_sender_vars,
|
||||
),
|
||||
(
|
||||
&mut session.rcpt.rewrite,
|
||||
"session.rcpt.rewrite",
|
||||
&has_rcpt_vars,
|
||||
),
|
||||
(
|
||||
&mut session.data.script,
|
||||
"session.data.script",
|
||||
&has_rcpt_vars,
|
||||
),
|
||||
(
|
||||
&mut session.data.max_messages,
|
||||
"session.data.limits.messages",
|
||||
&has_rcpt_vars,
|
||||
),
|
||||
(
|
||||
&mut session.data.max_message_size,
|
||||
"session.data.limits.size",
|
||||
&has_rcpt_vars,
|
||||
),
|
||||
(
|
||||
&mut session.data.max_received_headers,
|
||||
"session.data.limits.received-headers",
|
||||
&has_rcpt_vars,
|
||||
),
|
||||
(
|
||||
&mut session.data.add_received,
|
||||
"session.data.add-headers.received",
|
||||
&has_rcpt_vars,
|
||||
),
|
||||
(
|
||||
&mut session.data.add_received_spf,
|
||||
"session.data.add-headers.received-spf",
|
||||
&has_rcpt_vars,
|
||||
),
|
||||
(
|
||||
&mut session.data.add_return_path,
|
||||
"session.data.add-headers.return-path",
|
||||
&has_rcpt_vars,
|
||||
),
|
||||
(
|
||||
&mut session.data.add_auth_results,
|
||||
"session.data.add-headers.auth-results",
|
||||
&has_rcpt_vars,
|
||||
),
|
||||
(
|
||||
&mut session.data.add_message_id,
|
||||
"session.data.add-headers.message-id",
|
||||
&has_rcpt_vars,
|
||||
),
|
||||
(
|
||||
&mut session.data.add_date,
|
||||
"session.data.add-headers.date",
|
||||
&has_rcpt_vars,
|
||||
),
|
||||
] {
|
||||
if let Some(if_block) = IfBlock::try_parse(config, key, token_map) {
|
||||
*value = if_block;
|
||||
}
|
||||
}
|
||||
|
||||
session
|
||||
}
|
||||
}
|
||||
|
@ -506,7 +727,11 @@ impl From<Mechanism> for Constant {
|
|||
|
||||
impl ConstantValue for Mechanism {
|
||||
fn add_constants(token_map: &mut crate::expr::tokenizer::TokenMap) {
|
||||
todo!()
|
||||
token_map
|
||||
.add_constant("login", Mechanism(AUTH_LOGIN))
|
||||
.add_constant("plain", Mechanism(AUTH_PLAIN))
|
||||
.add_constant("xoauth2", Mechanism(AUTH_XOAUTH2))
|
||||
.add_constant("oauthbearer", Mechanism(AUTH_OAUTHBEARER));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -521,3 +746,39 @@ impl From<u64> for Mechanism {
|
|||
Mechanism(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> TryFrom<Variable<'x>> for MtPriority {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: Variable<'x>) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
Variable::Integer(value) => match value {
|
||||
2 => Ok(MtPriority::Mixer),
|
||||
3 => Ok(MtPriority::Stanag4406),
|
||||
4 => Ok(MtPriority::Nsep),
|
||||
_ => Err(()),
|
||||
},
|
||||
Variable::String(value) => MtPriority::parse_value("", &value).map_err(|_| ()),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MtPriority> for Constant {
|
||||
fn from(value: MtPriority) -> Self {
|
||||
Constant::Integer(match value {
|
||||
MtPriority::Mixer => 2,
|
||||
MtPriority::Stanag4406 => 3,
|
||||
MtPriority::Nsep => 4,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ConstantValue for MtPriority {
|
||||
fn add_constants(token_map: &mut TokenMap) {
|
||||
token_map
|
||||
.add_constant("mixer", MtPriority::Mixer)
|
||||
.add_constant("stanag4406", MtPriority::Stanag4406)
|
||||
.add_constant("nsep", MtPriority::Nsep);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ use std::sync::Arc;
|
|||
|
||||
use ahash::AHashMap;
|
||||
use directory::Directory;
|
||||
use store::{BlobStore, FtsStore, LookupStore, Store};
|
||||
use store::{write::purge::PurgeSchedule, BlobStore, FtsStore, LookupStore, Store};
|
||||
|
||||
pub struct Storage {
|
||||
pub data: Store,
|
||||
|
@ -12,4 +12,5 @@ pub struct Storage {
|
|||
pub lookups: AHashMap<String, LookupStore>,
|
||||
pub directory: Arc<Directory>,
|
||||
pub directories: AHashMap<String, Arc<Directory>>,
|
||||
pub purge_schedules: Vec<PurgeSchedule>,
|
||||
}
|
||||
|
|
|
@ -32,10 +32,10 @@ use super::{
|
|||
};
|
||||
|
||||
impl Core {
|
||||
pub async fn eval_if<R: for<'x> TryFrom<Variable<'x>>, V: for<'x> ResolveVariable<'x>>(
|
||||
pub async fn eval_if<'x, R: TryFrom<Variable<'x>>, V: ResolveVariable>(
|
||||
&self,
|
||||
if_block: &IfBlock,
|
||||
resolver: &V,
|
||||
if_block: &'x IfBlock,
|
||||
resolver: &'x V,
|
||||
) -> Option<R> {
|
||||
if if_block.is_empty() {
|
||||
return None;
|
||||
|
@ -54,10 +54,10 @@ impl Core {
|
|||
}
|
||||
}
|
||||
|
||||
pub async fn eval_expr<R: for<'x> TryFrom<Variable<'x>>, V: for<'x> ResolveVariable<'x>>(
|
||||
pub async fn eval_expr<'x, R: TryFrom<Variable<'x>>, V: ResolveVariable>(
|
||||
&self,
|
||||
expr: &Expression,
|
||||
resolver: &V,
|
||||
expr: &'x Expression,
|
||||
resolver: &'x V,
|
||||
expr_id: &str,
|
||||
) -> Option<R> {
|
||||
if expr.is_empty() {
|
||||
|
@ -79,10 +79,12 @@ impl Core {
|
|||
}
|
||||
|
||||
impl IfBlock {
|
||||
pub async fn eval<'x, V>(&'x self, resolver: &V, core: &Core, property: &str) -> Variable<'x>
|
||||
where
|
||||
V: ResolveVariable<'x>,
|
||||
{
|
||||
pub async fn eval<'x, V: ResolveVariable>(
|
||||
&'x self,
|
||||
resolver: &'x V,
|
||||
core: &Core,
|
||||
property: &str,
|
||||
) -> Variable<'x> {
|
||||
let mut captures = Vec::new();
|
||||
|
||||
for if_then in &self.if_then {
|
||||
|
@ -106,16 +108,13 @@ impl IfBlock {
|
|||
}
|
||||
|
||||
impl Expression {
|
||||
async fn eval<'x, 'y, V>(
|
||||
async fn eval<'x, 'y, V: ResolveVariable>(
|
||||
&'x self,
|
||||
resolver: &V,
|
||||
resolver: &'x V,
|
||||
core: &Core,
|
||||
property: &str,
|
||||
captures: &'y mut Vec<String>,
|
||||
) -> Variable<'x>
|
||||
where
|
||||
V: ResolveVariable<'x>,
|
||||
{
|
||||
) -> Variable<'x> {
|
||||
let mut stack = Vec::new();
|
||||
let mut exprs = self.items.iter();
|
||||
|
||||
|
|
|
@ -31,8 +31,8 @@ pub mod email;
|
|||
pub mod misc;
|
||||
pub mod text;
|
||||
|
||||
pub trait ResolveVariable<'x> {
|
||||
fn resolve_variable(&self, variable: u32) -> Variable<'x>;
|
||||
pub trait ResolveVariable {
|
||||
fn resolve_variable(&self, variable: u32) -> Variable<'_>;
|
||||
}
|
||||
|
||||
impl<'x> Variable<'x> {
|
||||
|
|
|
@ -21,10 +21,14 @@
|
|||
* for more details.
|
||||
*/
|
||||
|
||||
use std::{borrow::Cow, time::Duration};
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
net::{IpAddr, Ipv4Addr, Ipv6Addr},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use regex::Regex;
|
||||
use utils::config::utils::ParseValue;
|
||||
use utils::config::{utils::ParseValue, Rate};
|
||||
|
||||
use self::tokenizer::TokenMap;
|
||||
|
||||
|
@ -318,3 +322,74 @@ impl From<Duration> for Constant {
|
|||
Constant::Integer(value.as_millis() as i64)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> TryFrom<Variable<'x>> for Rate {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: Variable<'x>) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
Variable::Array(items) if items.len() == 2 => {
|
||||
let requests = items[0].to_integer().ok_or(())?;
|
||||
let period = items[1].to_integer().ok_or(())?;
|
||||
|
||||
if requests > 0 && period > 0 {
|
||||
Ok(Rate {
|
||||
requests: requests as u64,
|
||||
period: Duration::from_millis(period as u64),
|
||||
})
|
||||
} else {
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> TryFrom<Variable<'x>> for Ipv4Addr {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: Variable<'x>) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
Variable::String(value) => value.parse().map_err(|_| ()),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> TryFrom<Variable<'x>> for Ipv6Addr {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: Variable<'x>) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
Variable::String(value) => value.parse().map_err(|_| ()),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> TryFrom<Variable<'x>> for IpAddr {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: Variable<'x>) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
Variable::String(value) => value.parse().map_err(|_| ()),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x, T: TryFrom<Variable<'x>>> TryFrom<Variable<'x>> for Vec<T>
|
||||
where
|
||||
Result<Vec<T>, ()>: FromIterator<Result<T, <T as TryFrom<Variable<'x>>>::Error>>,
|
||||
{
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: Variable<'x>) -> Result<Self, Self::Error> {
|
||||
value
|
||||
.into_array()
|
||||
.into_iter()
|
||||
.map(|v| T::try_from(v))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
use std::{net::IpAddr, sync::Arc};
|
||||
use std::{borrow::Cow, net::IpAddr, sync::Arc};
|
||||
|
||||
use ahash::AHashMap;
|
||||
use arc_swap::ArcSwap;
|
||||
use config::{
|
||||
imap::ImapConfig,
|
||||
jmap::settings::JmapConfig,
|
||||
scripts::Scripting,
|
||||
server::Server,
|
||||
smtp::{
|
||||
auth::{ArcSealer, DkimSigner},
|
||||
queue::RelayHost,
|
||||
|
@ -16,7 +15,7 @@ use config::{
|
|||
};
|
||||
use directory::{Directory, Principal, QueryBy};
|
||||
use expr::if_block::IfBlock;
|
||||
use listener::{acme::AcmeManager, blocked::BlockedIps, tls::Certificate};
|
||||
use listener::blocked::BlockedIps;
|
||||
use mail_send::Credentials;
|
||||
use opentelemetry::KeyValue;
|
||||
use opentelemetry_sdk::{
|
||||
|
@ -26,9 +25,11 @@ use opentelemetry_sdk::{
|
|||
use opentelemetry_semantic_conventions::resource::{SERVICE_NAME, SERVICE_VERSION};
|
||||
use sieve::Sieve;
|
||||
use store::LookupStore;
|
||||
use tokio::sync::oneshot;
|
||||
use tracing::{level_filters::LevelFilter, Level};
|
||||
use tracing_appender::non_blocking::WorkerGuard;
|
||||
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Layer};
|
||||
use utils::{config::Config, BlobHash};
|
||||
|
||||
pub mod addresses;
|
||||
pub mod config;
|
||||
|
@ -39,6 +40,8 @@ pub mod scripts;
|
|||
pub static USER_AGENT: &str = concat!("StalwartMail/", env!("CARGO_PKG_VERSION"),);
|
||||
pub static DAEMON_NAME: &str = concat!("Stalwart Mail Server v", env!("CARGO_PKG_VERSION"),);
|
||||
|
||||
pub type SharedCore = Arc<ArcSwap<Core>>;
|
||||
|
||||
pub struct Core {
|
||||
pub storage: Storage,
|
||||
pub sieve: Scripting,
|
||||
|
@ -54,21 +57,41 @@ pub struct Network {
|
|||
pub url: IfBlock,
|
||||
}
|
||||
|
||||
pub struct ConfigBuilder {
|
||||
pub servers: Vec<Server>,
|
||||
pub certificates: AHashMap<String, Arc<Certificate>>,
|
||||
pub certificates_sni: AHashMap<String, Arc<Certificate>>,
|
||||
pub acme_managers: AHashMap<String, Arc<AcmeManager>>,
|
||||
pub tracers: Vec<Tracer>,
|
||||
pub core: Core,
|
||||
}
|
||||
|
||||
pub enum AuthResult<T> {
|
||||
Success(T),
|
||||
Failure,
|
||||
Banned,
|
||||
}
|
||||
|
||||
#[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,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
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;
|
||||
}
|
||||
|
@ -210,14 +233,8 @@ impl Core {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct TracerResult {
|
||||
pub guards: Vec<WorkerGuard>,
|
||||
pub errors: Vec<String>,
|
||||
}
|
||||
|
||||
impl Tracers {
|
||||
pub fn enable(self) -> TracerResult {
|
||||
pub fn enable(self, config: &mut Config) -> Option<Vec<WorkerGuard>> {
|
||||
let mut layers = Vec::new();
|
||||
let mut level = Level::TRACE;
|
||||
|
||||
|
@ -234,17 +251,15 @@ impl Tracers {
|
|||
}
|
||||
}
|
||||
|
||||
let mut result = TracerResult::default();
|
||||
let mut guards = Vec::new();
|
||||
match EnvFilter::builder().parse(format!(
|
||||
"smtp={level},imap={level},jmap={level},store={level},utils={level},directory={level}"
|
||||
"smtp={level},imap={level},jmap={level},store={level},common={level},utils={level},directory={level}"
|
||||
)) {
|
||||
Ok(layer) => {
|
||||
layers.push(layer.boxed());
|
||||
}
|
||||
Err(err) => {
|
||||
result
|
||||
.errors
|
||||
.push(format!("Failed to set env filter: {err}"));
|
||||
config.new_build_error("tracer", format!("Failed to set env filter: {err}"));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -264,7 +279,7 @@ impl Tracers {
|
|||
ansi,
|
||||
} => {
|
||||
let (non_blocking, guard) = tracing_appender::non_blocking(appender);
|
||||
result.guards.push(guard);
|
||||
guards.push(guard);
|
||||
layers.push(
|
||||
tracing_subscriber::fmt::layer()
|
||||
.with_writer(non_blocking)
|
||||
|
@ -305,9 +320,10 @@ impl Tracers {
|
|||
);
|
||||
}
|
||||
Err(err) => {
|
||||
result
|
||||
.errors
|
||||
.push(format!("Failed to start OpenTelemetry: {err}"));
|
||||
config.new_build_error(
|
||||
"tracer",
|
||||
format!("Failed to start OpenTelemetry: {err}"),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -321,29 +337,35 @@ impl Tracers {
|
|||
);
|
||||
}
|
||||
Err(err) => {
|
||||
result
|
||||
.errors
|
||||
.push(format!("Failed to start Journald: {err}"));
|
||||
config.new_build_error(
|
||||
"tracer",
|
||||
format!("Failed to start Journald: {err}"),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
result
|
||||
.errors
|
||||
.push("Journald is only available on Unix systems.".to_string());
|
||||
config.new_build_error(
|
||||
"tracer",
|
||||
"Journald is only available on Unix systems.",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(err) = tracing_subscriber::registry().with(layers).try_init() {
|
||||
result
|
||||
.errors
|
||||
.push(format!("Failed to start tracing: {err}"));
|
||||
if layers.len() > 1 {
|
||||
match tracing_subscriber::registry().with(layers).try_init() {
|
||||
Ok(_) => Some(guards),
|
||||
Err(err) => {
|
||||
config.new_build_error("tracer", format!("Failed to start tracing: {err}"));
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
}
|
||||
|
|
|
@ -59,7 +59,12 @@ impl AcmeManager {
|
|||
class: &str,
|
||||
items: &[String],
|
||||
) -> Result<Option<Vec<u8>>, std::io::Error> {
|
||||
match self.store.config_get(self.build_key(class, items)).await {
|
||||
match self
|
||||
.store
|
||||
.load()
|
||||
.config_get(self.build_key(class, items))
|
||||
.await
|
||||
{
|
||||
Ok(Some(content)) => match URL_SAFE_NO_PAD.decode(content.as_bytes()) {
|
||||
Ok(contents) => Ok(Some(contents)),
|
||||
Err(err) => Err(std::io::Error::new(ErrorKind::Other, err)),
|
||||
|
@ -76,6 +81,7 @@ impl AcmeManager {
|
|||
contents: impl AsRef<[u8]>,
|
||||
) -> Result<(), std::io::Error> {
|
||||
self.store
|
||||
.load()
|
||||
.config_set([ConfigKey {
|
||||
key: self.build_key(class, items),
|
||||
value: URL_SAFE_NO_PAD.encode(contents.as_ref()),
|
||||
|
|
|
@ -56,7 +56,7 @@ pub struct AcmeManager {
|
|||
pub(crate) domains: Vec<String>,
|
||||
contact: Vec<String>,
|
||||
renew_before: chrono::Duration,
|
||||
store: Store,
|
||||
store: ArcSwap<Store>,
|
||||
account_key: ArcSwap<Vec<u8>>,
|
||||
auth_keys: Mutex<AHashMap<String, Arc<CertifiedKey>>>,
|
||||
order_in_progress: AtomicBool,
|
||||
|
@ -81,7 +81,6 @@ impl AcmeManager {
|
|||
domains: Vec<String>,
|
||||
contact: Vec<String>,
|
||||
renew_before: Duration,
|
||||
store: Store,
|
||||
) -> utils::config::Result<Self> {
|
||||
Ok(AcmeManager {
|
||||
id,
|
||||
|
@ -97,7 +96,7 @@ impl AcmeManager {
|
|||
})
|
||||
.collect(),
|
||||
renew_before: chrono::Duration::from_std(renew_before).unwrap(),
|
||||
store,
|
||||
store: ArcSwap::from_pointee(Store::None),
|
||||
account_key: ArcSwap::from_pointee(Vec::new()),
|
||||
auth_keys: Mutex::new(AHashMap::new()),
|
||||
order_in_progress: false.into(),
|
||||
|
@ -106,7 +105,10 @@ impl AcmeManager {
|
|||
})
|
||||
}
|
||||
|
||||
pub async fn init(&self) -> Result<Duration, AcmeError> {
|
||||
pub async fn init(&self, store: Store) -> Result<Duration, AcmeError> {
|
||||
// Update data store
|
||||
self.store.store(Arc::new(store));
|
||||
|
||||
// Load account key from cache or generate a new one
|
||||
if let Some(account_key) = self.load_account().await? {
|
||||
self.account_key.store(Arc::new(account_key));
|
||||
|
@ -130,14 +132,14 @@ impl AcmeManager {
|
|||
}
|
||||
|
||||
pub trait SpawnAcme {
|
||||
fn spawn(self, shutdown_rx: watch::Receiver<bool>);
|
||||
fn spawn(self, store: Store, shutdown_rx: watch::Receiver<bool>);
|
||||
}
|
||||
|
||||
impl SpawnAcme for Arc<AcmeManager> {
|
||||
fn spawn(self, mut shutdown_rx: watch::Receiver<bool>) {
|
||||
fn spawn(self, store: Store, mut shutdown_rx: watch::Receiver<bool>) {
|
||||
tokio::spawn(async move {
|
||||
let acme = self;
|
||||
let mut renew_at = match acme.init().await {
|
||||
let mut renew_at = match acme.init(store).await {
|
||||
Ok(renew_at) => renew_at,
|
||||
Err(err) => {
|
||||
tracing::error!(
|
||||
|
|
|
@ -30,17 +30,18 @@ use std::{
|
|||
use arc_swap::ArcSwap;
|
||||
use proxy_header::io::ProxiedStream;
|
||||
use rustls::crypto::ring::cipher_suite::TLS13_AES_128_GCM_SHA256;
|
||||
use store::Store;
|
||||
use tokio::{
|
||||
net::{TcpListener, TcpStream},
|
||||
sync::watch,
|
||||
};
|
||||
use tokio_rustls::server::TlsStream;
|
||||
use tracing::Span;
|
||||
use utils::{config::Config, failed, UnwrapFailure};
|
||||
use utils::{config::Config, UnwrapFailure};
|
||||
|
||||
use crate::{
|
||||
config::server::{Listener, Server},
|
||||
ConfigBuilder, Core,
|
||||
config::server::{Listener, Server, Servers},
|
||||
Core,
|
||||
};
|
||||
|
||||
use super::{
|
||||
|
@ -87,7 +88,19 @@ impl Server {
|
|||
};
|
||||
|
||||
// Bind socket
|
||||
let listener = listener.listen();
|
||||
let listener = match listener.listen() {
|
||||
Ok(listener) => listener,
|
||||
Err(err) => {
|
||||
tracing::error!(
|
||||
event = "error",
|
||||
instance = instance.id,
|
||||
protocol = ?instance.protocol,
|
||||
reason = %err,
|
||||
"Failed to bind listener"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
// Spawn listener
|
||||
let mut shutdown_rx = instance.shutdown_rx.clone();
|
||||
|
@ -278,15 +291,17 @@ impl SocketOpts {
|
|||
}
|
||||
}
|
||||
|
||||
impl ConfigBuilder {
|
||||
pub fn bind(&self, config: &Config) {
|
||||
impl Servers {
|
||||
pub fn bind_and_drop_priv(&self, config: &mut Config) {
|
||||
// Bind as root
|
||||
for server in &self.servers {
|
||||
for listener in &server.listeners {
|
||||
listener
|
||||
.socket
|
||||
.bind(listener.addr)
|
||||
.failed(&format!("Failed to bind to {}", listener.addr));
|
||||
if let Err(err) = listener.socket.bind(listener.addr) {
|
||||
config.new_build_error(
|
||||
format!("server.listener.{}", server.id),
|
||||
format!("Failed to bind to {}: {}", listener.addr, err),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -306,7 +321,8 @@ impl ConfigBuilder {
|
|||
pub fn spawn(
|
||||
self,
|
||||
spawn: impl Fn(Server, watch::Receiver<bool>),
|
||||
) -> (watch::Sender<bool>, watch::Receiver<bool>) {
|
||||
store: Store,
|
||||
) -> watch::Sender<bool> {
|
||||
// Spawn listeners
|
||||
let (shutdown_tx, shutdown_rx) = watch::channel(false);
|
||||
for server in self.servers {
|
||||
|
@ -315,18 +331,18 @@ impl ConfigBuilder {
|
|||
|
||||
// Spawn ACME managers
|
||||
for (_, acme_manager) in self.acme_managers {
|
||||
acme_manager.spawn(shutdown_rx.clone());
|
||||
acme_manager.spawn(store.clone(), shutdown_rx.clone());
|
||||
}
|
||||
|
||||
(shutdown_tx, shutdown_rx)
|
||||
shutdown_tx
|
||||
}
|
||||
}
|
||||
|
||||
impl Listener {
|
||||
pub fn listen(self) -> TcpListener {
|
||||
pub fn listen(self) -> Result<TcpListener, String> {
|
||||
self.socket
|
||||
.listen(self.backlog.unwrap_or(1024))
|
||||
.unwrap_or_else(|err| failed(&format!("Failed to listen on {}: {}", self.addr, err)))
|
||||
.map_err(|err| format!("Failed to listen on {}: {}", self.addr, err))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -32,7 +32,13 @@ use tokio::{
|
|||
use tokio_rustls::{Accept, TlsAcceptor};
|
||||
use utils::config::ipmask::IpAddrMask;
|
||||
|
||||
use crate::config::server::ServerProtocol;
|
||||
use crate::{
|
||||
config::{
|
||||
server::ServerProtocol,
|
||||
smtp::{V_LISTENER, V_LOCAL_IP, V_REMOTE_IP},
|
||||
},
|
||||
expr::functions::ResolveVariable,
|
||||
};
|
||||
|
||||
use self::{
|
||||
acme::AcmeManager,
|
||||
|
@ -144,6 +150,26 @@ pub trait SessionManager: Sync + Send + 'static + Clone {
|
|||
fn shutdown(&self) -> impl std::future::Future<Output = ()> + Send;
|
||||
}
|
||||
|
||||
impl ResolveVariable for ServerInstance {
|
||||
fn resolve_variable(&self, variable: u32) -> crate::expr::Variable<'_> {
|
||||
match variable {
|
||||
V_LISTENER => self.id.as_str().into(),
|
||||
_ => crate::expr::Variable::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: SessionStream> ResolveVariable for SessionData<T> {
|
||||
fn resolve_variable(&self, variable: u32) -> crate::expr::Variable<'_> {
|
||||
match variable {
|
||||
V_REMOTE_IP => self.remote_ip.to_string().into(),
|
||||
V_LOCAL_IP => self.local_ip.to_string().into(),
|
||||
V_LISTENER => self.instance.id.as_str().into(),
|
||||
_ => crate::expr::Variable::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for TcpAcceptor {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
|
|
|
@ -136,6 +136,15 @@ impl<T: serde::Serialize + serde::de::DeserializeOwned> Principal<T> {
|
|||
}
|
||||
}
|
||||
|
||||
impl Default for Directory {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
store: DirectoryInner::Internal(Store::None),
|
||||
cache: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for Directory {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("Directory").finish()
|
||||
|
|
|
@ -10,6 +10,7 @@ jmap = { path = "../jmap" }
|
|||
jmap_proto = { path = "../jmap-proto" }
|
||||
directory = { path = "../directory" }
|
||||
store = { path = "../store" }
|
||||
common = { path = "../common" }
|
||||
nlp = { path = "../nlp" }
|
||||
utils = { path = "../utils" }
|
||||
mail-parser = { version = "0.9", features = ["full_encoding", "ludicrous_mode"] }
|
||||
|
|
|
@ -23,14 +23,14 @@
|
|||
|
||||
use std::{iter::Peekable, sync::Arc, vec::IntoIter};
|
||||
|
||||
use common::listener::{limiter::ConcurrencyLimiter, SessionStream};
|
||||
use imap_proto::{
|
||||
receiver::{self, Request},
|
||||
Command, ResponseCode, StatusResponse,
|
||||
};
|
||||
use jmap::auth::rate_limit::ConcurrencyLimiters;
|
||||
use utils::listener::{limiter::ConcurrencyLimiter, SessionStream};
|
||||
|
||||
use super::{SelectedMailbox, Session, SessionData, State, IMAP};
|
||||
use super::{SelectedMailbox, Session, SessionData, State};
|
||||
|
||||
impl<T: SessionStream> Session<T> {
|
||||
pub async fn ingest(&mut self, bytes: &[u8]) -> crate::Result<bool> {
|
||||
|
@ -227,26 +227,26 @@ impl<T: SessionStream> Session<T> {
|
|||
let state = &self.state;
|
||||
// Rate limit request
|
||||
if let State::Authenticated { data } | State::Selected { data, .. } = state {
|
||||
match data
|
||||
.jmap
|
||||
.lookup_store
|
||||
.is_rate_allowed(
|
||||
format!("ireq:{}", data.account_id).as_bytes(),
|
||||
&self.imap.rate_requests,
|
||||
true,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(None) => {}
|
||||
Ok(Some(_)) => {
|
||||
return Err(StatusResponse::no("Too many requests")
|
||||
.with_tag(request.tag)
|
||||
.with_code(ResponseCode::Limit));
|
||||
}
|
||||
Err(_) => {
|
||||
return Err(StatusResponse::no("Internal server error")
|
||||
.with_tag(request.tag)
|
||||
.with_code(ResponseCode::ContactAdmin));
|
||||
if let Some(rate) = &self.jmap.core.imap.rate_requests {
|
||||
match data
|
||||
.jmap
|
||||
.core
|
||||
.storage
|
||||
.lookup
|
||||
.is_rate_allowed(format!("ireq:{}", data.account_id).as_bytes(), rate, true)
|
||||
.await
|
||||
{
|
||||
Ok(None) => {}
|
||||
Ok(Some(_)) => {
|
||||
return Err(StatusResponse::no("Too many requests")
|
||||
.with_tag(request.tag)
|
||||
.with_code(ResponseCode::Limit));
|
||||
}
|
||||
Err(_) => {
|
||||
return Err(StatusResponse::no("Internal server error")
|
||||
.with_tag(request.tag)
|
||||
.with_code(ResponseCode::ContactAdmin));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -273,7 +273,7 @@ impl<T: SessionStream> Session<T> {
|
|||
}
|
||||
Command::Login => {
|
||||
if let State::NotAuthenticated { .. } = state {
|
||||
if self.is_tls || self.imap.allow_plain_auth {
|
||||
if self.is_tls || self.jmap.core.imap.allow_plain_auth {
|
||||
Ok(request)
|
||||
} else {
|
||||
Err(
|
||||
|
@ -344,6 +344,23 @@ 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
|
||||
.get(&account_id)
|
||||
.map(|limiter| limiter.clone())
|
||||
.unwrap_or_else(|| {
|
||||
let limiter = Arc::new(ConcurrencyLimiters {
|
||||
concurrent_requests: ConcurrencyLimiter::new(rate),
|
||||
concurrent_uploads: ConcurrencyLimiter::new(rate),
|
||||
});
|
||||
self.imap.rate_limiter.insert(account_id, limiter.clone());
|
||||
limiter
|
||||
})
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: SessionStream> State<T> {
|
||||
|
@ -392,19 +409,3 @@ impl<T: SessionStream> State<T> {
|
|||
matches!(self, State::Selected { .. })
|
||||
}
|
||||
}
|
||||
|
||||
impl IMAP {
|
||||
pub fn get_concurrency_limiter(&self, account_id: u32) -> Arc<ConcurrencyLimiters> {
|
||||
self.rate_limiter
|
||||
.get(&account_id)
|
||||
.map(|limiter| limiter.clone())
|
||||
.unwrap_or_else(|| {
|
||||
let limiter = Arc::new(ConcurrencyLimiters {
|
||||
concurrent_requests: ConcurrencyLimiter::new(self.rate_concurrent),
|
||||
concurrent_uploads: ConcurrencyLimiter::new(self.rate_concurrent),
|
||||
});
|
||||
self.rate_limiter.insert(account_id, limiter.clone());
|
||||
limiter
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ use std::{
|
|||
};
|
||||
|
||||
use ahash::AHashMap;
|
||||
use common::listener::{limiter::InFlight, SessionStream};
|
||||
use directory::QueryBy;
|
||||
use imap_proto::{protocol::list::Attribute, StatusResponse};
|
||||
use jmap::{
|
||||
|
@ -16,10 +17,7 @@ use jmap_proto::{
|
|||
};
|
||||
use parking_lot::Mutex;
|
||||
use store::query::log::{Change, Query};
|
||||
use utils::{
|
||||
listener::{limiter::InFlight, SessionStream},
|
||||
lru_cache::LruCached,
|
||||
};
|
||||
use utils::lru_cache::LruCached;
|
||||
|
||||
use super::{Account, AccountId, Mailbox, MailboxId, MailboxSync, Session, SessionData};
|
||||
|
||||
|
@ -27,7 +25,7 @@ impl<T: SessionStream> SessionData<T> {
|
|||
pub async fn new(
|
||||
session: &Session<T>,
|
||||
access_token: &AccessToken,
|
||||
in_flight: InFlight,
|
||||
in_flight: Option<InFlight>,
|
||||
) -> crate::Result<Self> {
|
||||
let mut session = SessionData {
|
||||
stream_tx: session.stream_tx.clone(),
|
||||
|
@ -53,9 +51,11 @@ impl<T: SessionStream> SessionData<T> {
|
|||
account_id,
|
||||
format!(
|
||||
"{}/{}",
|
||||
session.imap.name_shared,
|
||||
session.jmap.core.imap.name_shared,
|
||||
session
|
||||
.jmap
|
||||
.core
|
||||
.storage
|
||||
.directory
|
||||
.query(QueryBy::Id(account_id), false)
|
||||
.await
|
||||
|
@ -90,13 +90,17 @@ impl<T: SessionStream> SessionData<T> {
|
|||
) -> crate::Result<Account> {
|
||||
let state_mailbox = self
|
||||
.jmap
|
||||
.store
|
||||
.core
|
||||
.storage
|
||||
.data
|
||||
.get_last_change_id(account_id, Collection::Mailbox)
|
||||
.await
|
||||
.map_err(|_| {})?;
|
||||
let state_email = self
|
||||
.jmap
|
||||
.store
|
||||
.core
|
||||
.storage
|
||||
.data
|
||||
.get_last_change_id(account_id, Collection::Email)
|
||||
.await
|
||||
.map_err(|_| {})?;
|
||||
|
@ -347,8 +351,10 @@ impl<T: SessionStream> SessionData<T> {
|
|||
for account_id in added_account_ids {
|
||||
let prefix = format!(
|
||||
"{}/{}",
|
||||
self.imap.name_shared,
|
||||
self.jmap.core.imap.name_shared,
|
||||
self.jmap
|
||||
.core
|
||||
.storage
|
||||
.directory
|
||||
.query(QueryBy::Id(account_id), false)
|
||||
.await
|
||||
|
@ -407,7 +413,7 @@ impl<T: SessionStream> SessionData<T> {
|
|||
// Only child changes, no need to re-fetch mailboxes
|
||||
let state_email = self
|
||||
.jmap
|
||||
.store
|
||||
.core.storage.data
|
||||
.get_last_change_id(account_id, Collection::Email)
|
||||
.await.map_err(
|
||||
|e| {
|
||||
|
@ -459,8 +465,10 @@ impl<T: SessionStream> SessionData<T> {
|
|||
let mailbox_prefix = if !access_token.is_primary_id(account_id) {
|
||||
format!(
|
||||
"{}/{}",
|
||||
self.imap.name_shared,
|
||||
self.jmap.core.imap.name_shared,
|
||||
self.jmap
|
||||
.core
|
||||
.storage
|
||||
.directory
|
||||
.query(QueryBy::Id(account_id), false)
|
||||
.await
|
||||
|
|
|
@ -24,6 +24,7 @@
|
|||
use std::{collections::BTreeMap, sync::Arc};
|
||||
|
||||
use ahash::AHashMap;
|
||||
use common::listener::SessionStream;
|
||||
use imap_proto::{
|
||||
protocol::{expunge, select::Exists, Sequence},
|
||||
StatusResponse,
|
||||
|
@ -34,7 +35,7 @@ use jmap_proto::{
|
|||
types::{collection::Collection, property::Property, value::Value},
|
||||
};
|
||||
use store::write::assert::HashedValue;
|
||||
use utils::{listener::SessionStream, lru_cache::LruCached};
|
||||
use utils::lru_cache::LruCached;
|
||||
|
||||
use crate::core::ImapId;
|
||||
|
||||
|
@ -62,7 +63,9 @@ impl<T: SessionStream> SessionData<T> {
|
|||
// Obtain current state
|
||||
let modseq = self
|
||||
.jmap
|
||||
.store
|
||||
.core
|
||||
.storage
|
||||
.data
|
||||
.get_last_change_id(mailbox.account_id, Collection::Email)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
|
@ -231,7 +234,9 @@ impl<T: SessionStream> SessionData<T> {
|
|||
// Obtain current modseq
|
||||
if let Ok(modseq) = self
|
||||
.jmap
|
||||
.store
|
||||
.core
|
||||
.storage
|
||||
.data
|
||||
.get_last_change_id(account_id, Collection::Email)
|
||||
.await
|
||||
{
|
||||
|
|
|
@ -25,10 +25,10 @@ use std::{
|
|||
collections::BTreeMap,
|
||||
net::IpAddr,
|
||||
sync::{atomic::AtomicU32, Arc},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use ahash::AHashMap;
|
||||
use common::listener::{limiter::InFlight, ServerInstance, SessionStream};
|
||||
use dashmap::DashMap;
|
||||
use imap_proto::{
|
||||
protocol::{list::Attribute, ProtocolVersion},
|
||||
|
@ -37,17 +37,13 @@ use imap_proto::{
|
|||
};
|
||||
use jmap::{
|
||||
auth::{rate_limit::ConcurrencyLimiters, AccessToken},
|
||||
JMAP,
|
||||
JmapInstance, JMAP,
|
||||
};
|
||||
use tokio::{
|
||||
io::{ReadHalf, WriteHalf},
|
||||
sync::watch,
|
||||
};
|
||||
use utils::{
|
||||
config::Rate,
|
||||
listener::{limiter::InFlight, ServerInstance, SessionStream},
|
||||
lru_cache::LruCache,
|
||||
};
|
||||
use utils::lru_cache::LruCache;
|
||||
|
||||
pub mod client;
|
||||
pub mod mailbox;
|
||||
|
@ -56,40 +52,36 @@ pub mod session;
|
|||
|
||||
#[derive(Clone)]
|
||||
pub struct ImapSessionManager {
|
||||
pub jmap: Arc<JMAP>,
|
||||
pub imap: Arc<IMAP>,
|
||||
pub imap: ImapInstance,
|
||||
}
|
||||
|
||||
impl ImapSessionManager {
|
||||
pub fn new(jmap: Arc<JMAP>, imap: Arc<IMAP>) -> Self {
|
||||
Self { jmap, imap }
|
||||
pub fn new(imap: ImapInstance) -> Self {
|
||||
Self { imap }
|
||||
}
|
||||
}
|
||||
|
||||
pub struct IMAP {
|
||||
pub max_request_size: usize,
|
||||
pub max_auth_failures: u32,
|
||||
pub name_shared: String,
|
||||
pub allow_plain_auth: bool,
|
||||
|
||||
pub timeout_auth: Duration,
|
||||
pub timeout_unauth: Duration,
|
||||
pub timeout_idle: Duration,
|
||||
#[derive(Clone)]
|
||||
pub struct ImapInstance {
|
||||
pub jmap_instance: JmapInstance,
|
||||
pub imap_inner: Arc<Inner>,
|
||||
}
|
||||
|
||||
pub struct Inner {
|
||||
pub greeting_plain: Vec<u8>,
|
||||
pub greeting_tls: Vec<u8>,
|
||||
|
||||
pub rate_limiter: DashMap<u32, Arc<ConcurrencyLimiters>>,
|
||||
pub rate_requests: Rate,
|
||||
pub rate_concurrent: u64,
|
||||
|
||||
pub cache_account: LruCache<AccountId, Arc<Account>>,
|
||||
pub cache_mailbox: LruCache<MailboxId, Arc<MailboxState>>,
|
||||
}
|
||||
|
||||
pub struct IMAP {}
|
||||
|
||||
pub struct Session<T: SessionStream> {
|
||||
pub jmap: Arc<JMAP>,
|
||||
pub imap: Arc<IMAP>,
|
||||
pub jmap: JMAP,
|
||||
pub imap: Arc<Inner>,
|
||||
pub instance: Arc<ServerInstance>,
|
||||
pub receiver: Receiver<Command>,
|
||||
pub version: ProtocolVersion,
|
||||
|
@ -106,13 +98,13 @@ pub struct Session<T: SessionStream> {
|
|||
|
||||
pub struct SessionData<T: SessionStream> {
|
||||
pub account_id: u32,
|
||||
pub jmap: Arc<JMAP>,
|
||||
pub imap: Arc<IMAP>,
|
||||
pub jmap: JMAP,
|
||||
pub imap: Arc<Inner>,
|
||||
pub span: tracing::Span,
|
||||
pub mailboxes: parking_lot::Mutex<Vec<Account>>,
|
||||
pub stream_tx: Arc<tokio::sync::Mutex<WriteHalf<T>>>,
|
||||
pub state: AtomicU32,
|
||||
pub in_flight: InFlight,
|
||||
pub in_flight: Option<InFlight>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
|
|
|
@ -23,18 +23,19 @@
|
|||
|
||||
use std::{borrow::Cow, sync::Arc};
|
||||
|
||||
use common::listener::{stream::NullIo, SessionData, SessionManager, SessionStream};
|
||||
use imap_proto::{protocol::ProtocolVersion, receiver::Receiver};
|
||||
use jmap::JMAP;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio_rustls::server::TlsStream;
|
||||
use utils::listener::{stream::NullIo, SessionManager, SessionStream};
|
||||
|
||||
use super::{ImapSessionManager, Session, State};
|
||||
|
||||
impl SessionManager for ImapSessionManager {
|
||||
#[allow(clippy::manual_async_fn)]
|
||||
fn handle<T: utils::listener::SessionStream>(
|
||||
fn handle<T: SessionStream>(
|
||||
self,
|
||||
session: utils::listener::SessionData<T>,
|
||||
session: SessionData<T>,
|
||||
) -> impl std::future::Future<Output = ()> + Send {
|
||||
async move {
|
||||
if let Ok(mut session) = Session::new(session, self).await {
|
||||
|
@ -51,10 +52,6 @@ impl SessionManager for ImapSessionManager {
|
|||
fn shutdown(&self) -> impl std::future::Future<Output = ()> + Send {
|
||||
async {}
|
||||
}
|
||||
|
||||
fn is_ip_blocked(&self, addr: &std::net::IpAddr) -> bool {
|
||||
self.jmap.directory.blocked_ips.is_blocked(addr)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: SessionStream> Session<T> {
|
||||
|
@ -66,9 +63,9 @@ impl<T: SessionStream> Session<T> {
|
|||
tokio::select! {
|
||||
result = tokio::time::timeout(
|
||||
if !matches!(self.state, State::NotAuthenticated {..}) {
|
||||
self.imap.timeout_auth
|
||||
self.jmap.core.imap.timeout_auth
|
||||
} else {
|
||||
self.imap.timeout_unauth
|
||||
self.jmap.core.imap.timeout_unauth
|
||||
},
|
||||
self.stream_rx.read(&mut buf)) => {
|
||||
match result {
|
||||
|
@ -112,14 +109,14 @@ impl<T: SessionStream> Session<T> {
|
|||
}
|
||||
|
||||
pub async fn new(
|
||||
mut session: utils::listener::SessionData<T>,
|
||||
mut session: SessionData<T>,
|
||||
manager: ImapSessionManager,
|
||||
) -> Result<Session<T>, ()> {
|
||||
// Write greeting
|
||||
let (is_tls, greeting) = if session.stream.is_tls() {
|
||||
(true, &manager.imap.greeting_tls)
|
||||
(true, &manager.imap.imap_inner.greeting_tls)
|
||||
} else {
|
||||
(false, &manager.imap.greeting_plain)
|
||||
(false, &manager.imap.imap_inner.greeting_plain)
|
||||
};
|
||||
if let Err(err) = session.stream.write_all(greeting).await {
|
||||
tracing::debug!(parent: &session.span, event = "error", reason = %err, "Failed to write greeting.");
|
||||
|
@ -129,16 +126,17 @@ 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);
|
||||
|
||||
Ok(Session {
|
||||
receiver: Receiver::with_max_request_size(manager.imap.max_request_size),
|
||||
receiver: Receiver::with_max_request_size(jmap.core.imap.max_request_size),
|
||||
version: ProtocolVersion::Rev1,
|
||||
state: State::NotAuthenticated { auth_failures: 0 },
|
||||
is_tls,
|
||||
is_condstore: false,
|
||||
is_qresync: false,
|
||||
imap: manager.imap,
|
||||
jmap: manager.jmap,
|
||||
jmap,
|
||||
imap: manager.imap.imap_inner,
|
||||
instance: session.instance,
|
||||
span: session.span,
|
||||
in_flight: session.in_flight,
|
||||
|
|
|
@ -30,7 +30,6 @@ use tokio::{
|
|||
};
|
||||
use tokio_rustls::server::TlsStream;
|
||||
use tracing::debug;
|
||||
use utils::listener::SessionStream;
|
||||
|
||||
use super::{Session, SessionData};
|
||||
|
||||
|
|
|
@ -21,12 +21,12 @@
|
|||
* for more details.
|
||||
*/
|
||||
|
||||
use core::{ImapInstance, Inner, IMAP};
|
||||
use std::{collections::hash_map::RandomState, sync::Arc};
|
||||
|
||||
use crate::core::IMAP;
|
||||
|
||||
use dashmap::DashMap;
|
||||
use imap_proto::{protocol::capability::Capability, ResponseCode, StatusResponse};
|
||||
use jmap::JmapInstance;
|
||||
use utils::{
|
||||
config::Config,
|
||||
lru_cache::{LruCache, LruCached},
|
||||
|
@ -35,30 +35,17 @@ use utils::{
|
|||
pub mod core;
|
||||
pub mod op;
|
||||
|
||||
static SERVER_GREETING: &str = concat!(
|
||||
"Stalwart IMAP4rev2 v",
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
" at your service."
|
||||
);
|
||||
static SERVER_GREETING: &str = "Stalwart IMAP4rev2 at your service.";
|
||||
|
||||
impl IMAP {
|
||||
pub async fn init(config: &Config) -> utils::config::Result<Arc<Self>> {
|
||||
pub async fn init(config: &mut Config, jmap_instance: JmapInstance) -> ImapInstance {
|
||||
let shard_amount = config
|
||||
.property::<u64>("cache.shard")?
|
||||
.property_::<u64>("cache.shard")
|
||||
.unwrap_or(32)
|
||||
.next_power_of_two() as usize;
|
||||
let capacity = config.property("cache.capacity")?.unwrap_or(100);
|
||||
let capacity = config.property_("cache.capacity").unwrap_or(100);
|
||||
|
||||
Ok(Arc::new(IMAP {
|
||||
max_request_size: config.property_or_default("imap.request.max-size", "52428800")?,
|
||||
max_auth_failures: config.property_or_default("imap.auth.max-failures", "3")?,
|
||||
name_shared: config
|
||||
.value("imap.folders.name.shared")
|
||||
.unwrap_or("Shared Folders")
|
||||
.to_string(),
|
||||
timeout_auth: config.property_or_default("imap.timeout.authenticated", "30m")?,
|
||||
timeout_unauth: config.property_or_default("imap.timeout.anonymous", "1m")?,
|
||||
timeout_idle: config.property_or_default("imap.timeout.idle", "30m")?,
|
||||
let inner = Inner {
|
||||
greeting_plain: StatusResponse::ok(SERVER_GREETING)
|
||||
.with_code(ResponseCode::Capability {
|
||||
capabilities: Capability::all_capabilities(false, false),
|
||||
|
@ -74,16 +61,18 @@ impl IMAP {
|
|||
RandomState::default(),
|
||||
shard_amount,
|
||||
),
|
||||
rate_requests: config.property_or_default("imap.rate-limit.requests", "2000/1m")?,
|
||||
rate_concurrent: config.property("imap.rate-limit.concurrent")?.unwrap_or(4),
|
||||
allow_plain_auth: config.property_or_default("imap.auth.allow-plain-text", "false")?,
|
||||
cache_account: LruCache::with_capacity(
|
||||
config.property("cache.account.size")?.unwrap_or(2048),
|
||||
config.property_("cache.account.size").unwrap_or(2048),
|
||||
),
|
||||
cache_mailbox: LruCache::with_capacity(
|
||||
config.property("cache.mailbox.size")?.unwrap_or(2048),
|
||||
config.property_("cache.mailbox.size").unwrap_or(2048),
|
||||
),
|
||||
}))
|
||||
};
|
||||
|
||||
ImapInstance {
|
||||
jmap_instance,
|
||||
imap_inner: Arc::new(inner),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -23,6 +23,7 @@
|
|||
|
||||
use std::sync::Arc;
|
||||
|
||||
use common::listener::SessionStream;
|
||||
use directory::QueryBy;
|
||||
use imap_proto::{
|
||||
protocol::acl::{
|
||||
|
@ -49,7 +50,7 @@ use jmap_proto::{
|
|||
},
|
||||
};
|
||||
use store::write::{assert::HashedValue, log::ChangeLogBuilder, BatchBuilder};
|
||||
use utils::{listener::SessionStream, map::bitmap::Bitmap};
|
||||
use utils::map::bitmap::Bitmap;
|
||||
|
||||
use crate::core::{MailboxId, Session, SessionData};
|
||||
|
||||
|
@ -73,6 +74,8 @@ impl<T: SessionStream> Session<T> {
|
|||
for item in acls {
|
||||
if let Some(account_name) = data
|
||||
.jmap
|
||||
.core
|
||||
.storage
|
||||
.directory
|
||||
.query(QueryBy::Id(item.account_id), false)
|
||||
.await
|
||||
|
@ -244,6 +247,8 @@ impl<T: SessionStream> Session<T> {
|
|||
// Obtain principal id
|
||||
let acl_account_id = match data
|
||||
.jmap
|
||||
.core
|
||||
.storage
|
||||
.directory
|
||||
.query(QueryBy::Name(arguments.identifier.as_ref().unwrap()), false)
|
||||
.await
|
||||
|
@ -399,7 +404,7 @@ impl<T: SessionStream> Session<T> {
|
|||
}
|
||||
|
||||
// Invalidate ACLs
|
||||
data.jmap.access_tokens.remove(&acl_account_id);
|
||||
data.jmap.inner.access_tokens.remove(&acl_account_id);
|
||||
|
||||
data.write_bytes(
|
||||
StatusResponse::completed(command)
|
||||
|
|
|
@ -29,12 +29,11 @@ use imap_proto::{
|
|||
Command, ResponseCode, StatusResponse,
|
||||
};
|
||||
|
||||
use crate::core::{ImapUidToId, MailboxId, SelectedMailbox, Session, SessionData};
|
||||
use common::listener::SessionStream;
|
||||
use jmap::email::ingest::IngestEmail;
|
||||
use jmap_proto::types::{acl::Acl, keyword::Keyword, state::StateChange, type_state::DataType};
|
||||
use mail_parser::MessageParser;
|
||||
use utils::listener::SessionStream;
|
||||
|
||||
use crate::core::{ImapUidToId, MailboxId, SelectedMailbox, Session, SessionData};
|
||||
|
||||
use super::ToModSeq;
|
||||
|
||||
|
@ -133,7 +132,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),
|
||||
skip_duplicates: false,
|
||||
encrypt: self.jmap.config.encrypt && self.jmap.config.encrypt_append,
|
||||
encrypt: self.jmap.core.jmap.encrypt && self.jmap.core.jmap.encrypt_append,
|
||||
})
|
||||
.await
|
||||
{
|
||||
|
|
|
@ -21,9 +21,7 @@
|
|||
* for more details.
|
||||
*/
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use directory::AuthResult;
|
||||
use common::{listener::SessionStream, AuthResult};
|
||||
use imap_proto::{
|
||||
protocol::{authenticate::Mechanism, capability::Capability},
|
||||
receiver::{self, Request},
|
||||
|
@ -31,7 +29,7 @@ use imap_proto::{
|
|||
};
|
||||
use mail_parser::decoders::base64::base64_decode;
|
||||
use mail_send::Credentials;
|
||||
use utils::listener::SessionStream;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::core::{Session, SessionData, State};
|
||||
|
||||
|
@ -154,41 +152,43 @@ impl<T: SessionStream> Session<T> {
|
|||
|
||||
if let Some(access_token) = access_token {
|
||||
// Enforce concurrency limits
|
||||
let in_flight = self
|
||||
.imap
|
||||
let in_flight = match self
|
||||
.get_concurrency_limiter(access_token.primary_id())
|
||||
.concurrent_requests
|
||||
.is_allowed();
|
||||
if let Some(in_flight) = in_flight {
|
||||
// Cache access token
|
||||
let access_token = Arc::new(access_token);
|
||||
self.jmap.cache_access_token(access_token.clone());
|
||||
.map(|limiter| limiter.concurrent_requests.is_allowed())
|
||||
{
|
||||
Some(Some(limiter)) => Some(limiter),
|
||||
None => None,
|
||||
Some(None) => {
|
||||
self.write_bytes(
|
||||
StatusResponse::bye("Too many concurrent IMAP connections.").into_bytes(),
|
||||
)
|
||||
.await?;
|
||||
tracing::debug!(parent: &self.span,
|
||||
event = "disconnect",
|
||||
"Too many concurrent connections, disconnecting.",
|
||||
);
|
||||
return Err(());
|
||||
}
|
||||
};
|
||||
|
||||
// Create session
|
||||
self.state = State::Authenticated {
|
||||
data: Arc::new(SessionData::new(self, &access_token, in_flight).await?),
|
||||
};
|
||||
self.write_bytes(
|
||||
StatusResponse::ok("Authentication successful")
|
||||
.with_code(ResponseCode::Capability {
|
||||
capabilities: Capability::all_capabilities(true, self.is_tls),
|
||||
})
|
||||
.with_tag(tag)
|
||||
.into_bytes(),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
} else {
|
||||
self.write_bytes(
|
||||
StatusResponse::bye("Too many concurrent IMAP connections.").into_bytes(),
|
||||
)
|
||||
.await?;
|
||||
tracing::debug!(parent: &self.span,
|
||||
event = "disconnect",
|
||||
"Too many concurrent connections, disconnecting.",
|
||||
);
|
||||
Err(())
|
||||
}
|
||||
// Cache access token
|
||||
let access_token = Arc::new(access_token);
|
||||
self.jmap.cache_access_token(access_token.clone());
|
||||
|
||||
// Create session
|
||||
self.state = State::Authenticated {
|
||||
data: Arc::new(SessionData::new(self, &access_token, in_flight).await?),
|
||||
};
|
||||
self.write_bytes(
|
||||
StatusResponse::ok("Authentication successful")
|
||||
.with_code(ResponseCode::Capability {
|
||||
capabilities: Capability::all_capabilities(true, self.is_tls),
|
||||
})
|
||||
.with_tag(tag)
|
||||
.into_bytes(),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
} else {
|
||||
self.write_bytes(
|
||||
StatusResponse::no("Authentication failed")
|
||||
|
@ -199,7 +199,7 @@ impl<T: SessionStream> Session<T> {
|
|||
.await?;
|
||||
|
||||
let auth_failures = self.state.auth_failures();
|
||||
if auth_failures < self.imap.max_auth_failures {
|
||||
if auth_failures < self.jmap.core.imap.max_auth_failures {
|
||||
self.state = State::NotAuthenticated {
|
||||
auth_failures: auth_failures + 1,
|
||||
};
|
||||
|
|
|
@ -21,6 +21,8 @@
|
|||
* for more details.
|
||||
*/
|
||||
|
||||
use crate::core::Session;
|
||||
use common::listener::SessionStream;
|
||||
use imap_proto::{
|
||||
protocol::{
|
||||
capability::{Capability, Response},
|
||||
|
@ -30,10 +32,6 @@ use imap_proto::{
|
|||
Command, StatusResponse,
|
||||
};
|
||||
|
||||
use utils::listener::SessionStream;
|
||||
|
||||
use crate::core::Session;
|
||||
|
||||
impl<T: SessionStream> Session<T> {
|
||||
pub async fn handle_capability(&mut self, request: Request<Command>) -> crate::OpResult {
|
||||
self.write_bytes(
|
||||
|
|
|
@ -21,11 +21,9 @@
|
|||
* for more details.
|
||||
*/
|
||||
|
||||
use imap_proto::{receiver::Request, Command, StatusResponse};
|
||||
|
||||
use utils::listener::SessionStream;
|
||||
|
||||
use crate::core::{Session, State};
|
||||
use common::listener::SessionStream;
|
||||
use imap_proto::{receiver::Request, Command, StatusResponse};
|
||||
|
||||
impl<T: SessionStream> Session<T> {
|
||||
pub async fn handle_close(&mut self, request: Request<Command>) -> crate::OpResult {
|
||||
|
|
|
@ -28,6 +28,8 @@ use imap_proto::{
|
|||
StatusResponse,
|
||||
};
|
||||
|
||||
use crate::core::{MailboxId, SelectedMailbox, Session, SessionData};
|
||||
use common::listener::SessionStream;
|
||||
use jmap::{email::set::TagManager, mailbox::UidMailbox};
|
||||
use jmap_proto::{
|
||||
error::{method::MethodError, set::SetErrorType},
|
||||
|
@ -37,9 +39,6 @@ use jmap_proto::{
|
|||
},
|
||||
};
|
||||
use store::write::{assert::HashedValue, log::ChangeLogBuilder, BatchBuilder, F_VALUE};
|
||||
use utils::listener::SessionStream;
|
||||
|
||||
use crate::core::{MailboxId, SelectedMailbox, Session, SessionData};
|
||||
|
||||
impl<T: SessionStream> Session<T> {
|
||||
pub async fn handle_copy_move(
|
||||
|
|
|
@ -21,6 +21,8 @@
|
|||
* for more details.
|
||||
*/
|
||||
|
||||
use crate::core::{Account, Mailbox, Session, SessionData};
|
||||
use common::listener::SessionStream;
|
||||
use imap_proto::{
|
||||
protocol::{create::Arguments, list::Attribute},
|
||||
receiver::Request,
|
||||
|
@ -35,9 +37,6 @@ use jmap_proto::{
|
|||
},
|
||||
};
|
||||
use store::{query::Filter, write::BatchBuilder};
|
||||
use utils::listener::SessionStream;
|
||||
|
||||
use crate::core::{Account, Mailbox, Session, SessionData};
|
||||
|
||||
impl<T: SessionStream> Session<T> {
|
||||
pub async fn handle_create(&mut self, requests: Vec<Request<Command>>) -> crate::OpResult {
|
||||
|
@ -252,13 +251,13 @@ impl<T: SessionStream> SessionData<T> {
|
|||
let path_item = path_item.trim();
|
||||
if path_item.is_empty() {
|
||||
return Err(StatusResponse::no("Invalid empty path item."));
|
||||
} else if path_item.len() > self.jmap.config.mailbox_name_max_len {
|
||||
} else if path_item.len() > self.jmap.core.jmap.mailbox_name_max_len {
|
||||
return Err(StatusResponse::no("Mailbox name is too long."));
|
||||
}
|
||||
path.push(path_item);
|
||||
}
|
||||
|
||||
if path.len() > self.jmap.config.mailbox_max_depth {
|
||||
if path.len() > self.jmap.core.jmap.mailbox_max_depth {
|
||||
return Err(StatusResponse::no("Mailbox path is too deep."));
|
||||
}
|
||||
} else {
|
||||
|
@ -272,7 +271,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.imap.name_shared {
|
||||
let account = if first_path_item == &self.jmap.core.imap.name_shared {
|
||||
// Shared Folders/<username>/<folder>
|
||||
if path.len() < 3 {
|
||||
return Err(StatusResponse::no(
|
||||
|
|
|
@ -21,12 +21,11 @@
|
|||
* for more details.
|
||||
*/
|
||||
|
||||
use crate::core::{Session, SessionData};
|
||||
use common::listener::SessionStream;
|
||||
use imap_proto::{protocol::delete::Arguments, receiver::Request, Command, StatusResponse};
|
||||
use jmap_proto::types::{state::StateChange, type_state::DataType};
|
||||
use store::write::log::ChangeLogBuilder;
|
||||
use utils::listener::SessionStream;
|
||||
|
||||
use crate::core::{Session, SessionData};
|
||||
|
||||
impl<T: SessionStream> Session<T> {
|
||||
pub async fn handle_delete(&mut self, requests: Vec<Request<Command>>) -> crate::OpResult {
|
||||
|
|
|
@ -21,16 +21,14 @@
|
|||
* for more details.
|
||||
*/
|
||||
|
||||
use crate::core::Session;
|
||||
use common::listener::SessionStream;
|
||||
use imap_proto::{
|
||||
protocol::{capability::Capability, enable, ImapResponse, ProtocolVersion},
|
||||
receiver::Request,
|
||||
Command, StatusResponse,
|
||||
};
|
||||
|
||||
use utils::listener::SessionStream;
|
||||
|
||||
use crate::core::Session;
|
||||
|
||||
impl<T: SessionStream> Session<T> {
|
||||
pub async fn handle_enable(&mut self, request: Request<Command>) -> crate::OpResult {
|
||||
match request.parse_enable() {
|
||||
|
|
|
@ -30,6 +30,8 @@ use imap_proto::{
|
|||
Command, ResponseCode, StatusResponse,
|
||||
};
|
||||
|
||||
use crate::core::{ImapId, SavedSearch, SelectedMailbox, Session, SessionData};
|
||||
use common::listener::SessionStream;
|
||||
use jmap::{email::set::TagManager, mailbox::UidMailbox};
|
||||
use jmap_proto::{
|
||||
error::method::MethodError,
|
||||
|
@ -39,9 +41,6 @@ use jmap_proto::{
|
|||
},
|
||||
};
|
||||
use store::write::{assert::HashedValue, log::ChangeLogBuilder, BatchBuilder, F_VALUE};
|
||||
use utils::listener::SessionStream;
|
||||
|
||||
use crate::core::{ImapId, SavedSearch, SelectedMailbox, Session, SessionData};
|
||||
|
||||
use super::ToModSeq;
|
||||
|
||||
|
|
|
@ -23,7 +23,9 @@
|
|||
|
||||
use std::{borrow::Cow, sync::Arc};
|
||||
|
||||
use crate::core::{SelectedMailbox, Session, SessionData};
|
||||
use ahash::AHashMap;
|
||||
use common::listener::SessionStream;
|
||||
use imap_proto::{
|
||||
parser::PushUnique,
|
||||
protocol::{
|
||||
|
@ -50,9 +52,6 @@ use store::{
|
|||
query::log::{Change, Query},
|
||||
write::{assert::HashedValue, BatchBuilder, Bincode, F_BITMAP, F_VALUE},
|
||||
};
|
||||
use utils::listener::SessionStream;
|
||||
|
||||
use crate::core::{SelectedMailbox, Session, SessionData};
|
||||
|
||||
use super::FromModSeq;
|
||||
|
||||
|
|
|
@ -35,10 +35,10 @@ use imap_proto::{
|
|||
Command, ResponseCode, StatusResponse,
|
||||
};
|
||||
|
||||
use common::listener::SessionStream;
|
||||
use jmap_proto::types::{collection::Collection, type_state::DataType};
|
||||
use store::query::log::Query;
|
||||
use tokio::io::AsyncReadExt;
|
||||
use utils::listener::SessionStream;
|
||||
use utils::map::bitmap::Bitmap;
|
||||
|
||||
use crate::core::{SelectedMailbox, Session, SessionData, State};
|
||||
|
@ -84,7 +84,7 @@ impl<T: SessionStream> Session<T> {
|
|||
let mut buf = vec![0; 1024];
|
||||
loop {
|
||||
tokio::select! {
|
||||
result = tokio::time::timeout(self.imap.timeout_idle, self.stream_rx.read(&mut buf)) => {
|
||||
result = tokio::time::timeout(self.jmap.core.imap.timeout_idle, self.stream_rx.read(&mut buf)) => {
|
||||
match result {
|
||||
Ok(Ok(bytes_read)) => {
|
||||
if bytes_read > 0 {
|
||||
|
|
|
@ -21,6 +21,8 @@
|
|||
* for more details.
|
||||
*/
|
||||
|
||||
use crate::core::{Session, SessionData};
|
||||
use common::listener::SessionStream;
|
||||
use imap_proto::{
|
||||
protocol::{
|
||||
list::{
|
||||
|
@ -32,10 +34,6 @@ use imap_proto::{
|
|||
Command, StatusResponse,
|
||||
};
|
||||
|
||||
use utils::listener::SessionStream;
|
||||
|
||||
use crate::core::{Session, SessionData};
|
||||
|
||||
impl<T: SessionStream> Session<T> {
|
||||
pub async fn handle_list(&mut self, request: Request<Command>) -> crate::OpResult {
|
||||
let command = request.command;
|
||||
|
@ -179,9 +177,11 @@ impl<T: SessionStream> SessionData<T> {
|
|||
for account in self.mailboxes.lock().iter() {
|
||||
if let Some(prefix) = &account.prefix {
|
||||
if !added_shared_folder {
|
||||
if !filter_subscribed && matches_pattern(&patterns, &self.imap.name_shared) {
|
||||
if !filter_subscribed
|
||||
&& matches_pattern(&patterns, &self.jmap.core.imap.name_shared)
|
||||
{
|
||||
list_items.push(ListItem {
|
||||
mailbox_name: self.imap.name_shared.clone(),
|
||||
mailbox_name: self.jmap.core.imap.name_shared.clone(),
|
||||
attributes: if include_children {
|
||||
vec![Attribute::HasChildren, Attribute::NoSelect]
|
||||
} else {
|
||||
|
|
|
@ -23,10 +23,9 @@
|
|||
|
||||
use imap_proto::{receiver::Request, Command};
|
||||
|
||||
use mail_send::Credentials;
|
||||
use utils::listener::SessionStream;
|
||||
|
||||
use crate::core::Session;
|
||||
use common::listener::SessionStream;
|
||||
use mail_send::Credentials;
|
||||
|
||||
impl<T: SessionStream> Session<T> {
|
||||
pub async fn handle_login(&mut self, request: Request<Command>) -> crate::OpResult {
|
||||
|
|
|
@ -21,11 +21,9 @@
|
|||
* for more details.
|
||||
*/
|
||||
|
||||
use imap_proto::{receiver::Request, Command, StatusResponse};
|
||||
|
||||
use utils::listener::SessionStream;
|
||||
|
||||
use crate::core::Session;
|
||||
use common::listener::SessionStream;
|
||||
use imap_proto::{receiver::Request, Command, StatusResponse};
|
||||
|
||||
impl<T: SessionStream> Session<T> {
|
||||
pub async fn handle_logout(&mut self, request: Request<Command>) -> crate::OpResult {
|
||||
|
|
|
@ -21,16 +21,14 @@
|
|||
* for more details.
|
||||
*/
|
||||
|
||||
use crate::core::Session;
|
||||
use common::listener::SessionStream;
|
||||
use imap_proto::{
|
||||
protocol::{namespace::Response, ImapResponse},
|
||||
receiver::Request,
|
||||
Command, StatusResponse,
|
||||
};
|
||||
|
||||
use utils::listener::SessionStream;
|
||||
|
||||
use crate::core::Session;
|
||||
|
||||
impl<T: SessionStream> Session<T> {
|
||||
pub async fn handle_namespace(&mut self, request: Request<Command>) -> crate::OpResult {
|
||||
self.write_bytes(
|
||||
|
@ -39,7 +37,7 @@ impl<T: SessionStream> Session<T> {
|
|||
.serialize(
|
||||
Response {
|
||||
shared_prefix: if self.state.session_data().mailboxes.lock().len() > 1 {
|
||||
self.imap.name_shared.clone().into()
|
||||
self.jmap.core.imap.name_shared.clone().into()
|
||||
} else {
|
||||
None
|
||||
},
|
||||
|
|
|
@ -21,11 +21,9 @@
|
|||
* for more details.
|
||||
*/
|
||||
|
||||
use imap_proto::{receiver::Request, Command, StatusResponse};
|
||||
|
||||
use utils::listener::SessionStream;
|
||||
|
||||
use crate::core::{Session, State};
|
||||
use common::listener::SessionStream;
|
||||
use imap_proto::{receiver::Request, Command, StatusResponse};
|
||||
|
||||
impl<T: SessionStream> Session<T> {
|
||||
pub async fn handle_noop(&mut self, request: Request<Command>) -> crate::OpResult {
|
||||
|
|
|
@ -23,6 +23,8 @@
|
|||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use crate::core::{Session, SessionData};
|
||||
use common::listener::SessionStream;
|
||||
use imap_proto::{
|
||||
protocol::rename::Arguments, receiver::Request, Command, ResponseCode, StatusResponse,
|
||||
};
|
||||
|
@ -36,9 +38,6 @@ use jmap_proto::{
|
|||
},
|
||||
};
|
||||
use store::write::{assert::HashedValue, BatchBuilder};
|
||||
use utils::listener::SessionStream;
|
||||
|
||||
use crate::core::{Session, SessionData};
|
||||
|
||||
impl<T: SessionStream> Session<T> {
|
||||
pub async fn handle_rename(&mut self, request: Request<Command>) -> crate::OpResult {
|
||||
|
|
|
@ -23,6 +23,7 @@
|
|||
|
||||
use std::sync::Arc;
|
||||
|
||||
use common::listener::SessionStream;
|
||||
use imap_proto::{
|
||||
protocol::{
|
||||
search::{self, Arguments, Filter, Response, ResultOption},
|
||||
|
@ -31,7 +32,6 @@ use imap_proto::{
|
|||
receiver::Request,
|
||||
Command, StatusResponse,
|
||||
};
|
||||
|
||||
use jmap_proto::types::{collection::Collection, id::Id, keyword::Keyword, property::Property};
|
||||
use mail_parser::HeaderName;
|
||||
use nlp::language::Language;
|
||||
|
@ -42,7 +42,6 @@ use store::{
|
|||
write::now,
|
||||
};
|
||||
use tokio::sync::watch;
|
||||
use utils::listener::SessionStream;
|
||||
|
||||
use crate::core::{ImapId, MailboxState, SavedSearch, SelectedMailbox, Session, SessionData};
|
||||
|
||||
|
@ -153,7 +152,9 @@ impl<T: SessionStream> SessionData<T> {
|
|||
let is_sort = if let Some(sort) = arguments.sort {
|
||||
mailbox.map_search_results(
|
||||
self.jmap
|
||||
.store
|
||||
.core
|
||||
.storage
|
||||
.data
|
||||
.sort(
|
||||
result_set,
|
||||
sort.into_iter()
|
||||
|
@ -285,7 +286,7 @@ impl<T: SessionStream> SessionData<T> {
|
|||
fts_filters.push(FtsFilter::has_text_detect(
|
||||
Field::Body,
|
||||
text,
|
||||
self.jmap.config.default_language,
|
||||
self.jmap.core.jmap.default_language,
|
||||
));
|
||||
}
|
||||
search::Filter::Cc(text) => {
|
||||
|
@ -343,7 +344,7 @@ impl<T: SessionStream> SessionData<T> {
|
|||
fts_filters.push(FtsFilter::has_text_detect(
|
||||
Field::Header(HeaderName::Subject),
|
||||
text,
|
||||
self.jmap.config.default_language,
|
||||
self.jmap.core.jmap.default_language,
|
||||
));
|
||||
}
|
||||
search::Filter::Text(text) => {
|
||||
|
@ -371,17 +372,17 @@ impl<T: SessionStream> SessionData<T> {
|
|||
fts_filters.push(FtsFilter::has_text_detect(
|
||||
Field::Header(HeaderName::Subject),
|
||||
&text,
|
||||
self.jmap.config.default_language,
|
||||
self.jmap.core.jmap.default_language,
|
||||
));
|
||||
fts_filters.push(FtsFilter::has_text_detect(
|
||||
Field::Body,
|
||||
&text,
|
||||
self.jmap.config.default_language,
|
||||
self.jmap.core.jmap.default_language,
|
||||
));
|
||||
fts_filters.push(FtsFilter::has_text_detect(
|
||||
Field::Attachment,
|
||||
text,
|
||||
self.jmap.config.default_language,
|
||||
self.jmap.core.jmap.default_language,
|
||||
));
|
||||
fts_filters.push(FtsFilter::End);
|
||||
}
|
||||
|
|
|
@ -34,10 +34,10 @@ use imap_proto::{
|
|||
Command, ResponseCode, StatusResponse,
|
||||
};
|
||||
|
||||
use jmap_proto::types::id::Id;
|
||||
use utils::{listener::SessionStream, lru_cache::LruCached};
|
||||
|
||||
use crate::core::{SavedSearch, SelectedMailbox, Session, State};
|
||||
use common::listener::SessionStream;
|
||||
use jmap_proto::types::id::Id;
|
||||
use utils::lru_cache::LruCached;
|
||||
|
||||
use super::ToModSeq;
|
||||
|
||||
|
|
|
@ -23,6 +23,8 @@
|
|||
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::core::{Mailbox, Session, SessionData};
|
||||
use common::listener::SessionStream;
|
||||
use imap_proto::{
|
||||
parser::PushUnique,
|
||||
protocol::status::{Status, StatusItem, StatusItemType},
|
||||
|
@ -39,9 +41,6 @@ use store::{
|
|||
IndexKeyPrefix, IterateParams, ValueKey,
|
||||
};
|
||||
use store::{Deserialize, U32_LEN};
|
||||
use utils::listener::SessionStream;
|
||||
|
||||
use crate::core::{Mailbox, Session, SessionData};
|
||||
|
||||
use super::ToModSeq;
|
||||
|
||||
|
@ -95,11 +94,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.imap.name_shared
|
||||
return if mailbox_name == self.jmap.core.imap.name_shared
|
||||
|| mailbox_name
|
||||
.split_once('/')
|
||||
.map_or(false, |(base_name, path)| {
|
||||
base_name == self.imap.name_shared && !path.contains('/')
|
||||
base_name == self.jmap.core.imap.name_shared && !path.contains('/')
|
||||
})
|
||||
{
|
||||
Ok(StatusItem {
|
||||
|
@ -238,7 +237,9 @@ impl<T: SessionStream> SessionData<T> {
|
|||
Status::UidNext => {
|
||||
(self
|
||||
.jmap
|
||||
.store
|
||||
.core
|
||||
.storage
|
||||
.data
|
||||
.get_counter(ValueKey {
|
||||
account_id: mailbox.account_id,
|
||||
collection: Collection::Mailbox.into(),
|
||||
|
@ -388,7 +389,9 @@ impl<T: SessionStream> SessionData<T> {
|
|||
) -> super::Result<u32> {
|
||||
let mut total_size = 0u32;
|
||||
self.jmap
|
||||
.store
|
||||
.core
|
||||
.storage
|
||||
.data
|
||||
.iterate(
|
||||
IterateParams::new(
|
||||
IndexKeyPrefix {
|
||||
|
|
|
@ -23,7 +23,9 @@
|
|||
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::core::{message::MAX_RETRIES, SelectedMailbox, Session, SessionData};
|
||||
use ahash::AHashSet;
|
||||
use common::listener::SessionStream;
|
||||
use imap_proto::{
|
||||
protocol::{
|
||||
fetch::{DataItem, FetchItem},
|
||||
|
@ -45,9 +47,6 @@ use store::{
|
|||
query::log::{Change, Query},
|
||||
write::{assert::HashedValue, log::ChangeLogBuilder, BatchBuilder, F_VALUE},
|
||||
};
|
||||
use utils::listener::SessionStream;
|
||||
|
||||
use crate::core::{message::MAX_RETRIES, SelectedMailbox, Session, SessionData};
|
||||
|
||||
use super::FromModSeq;
|
||||
|
||||
|
|
|
@ -21,6 +21,8 @@
|
|||
* for more details.
|
||||
*/
|
||||
|
||||
use crate::core::{Session, SessionData};
|
||||
use common::listener::SessionStream;
|
||||
use imap_proto::{receiver::Request, Command, ResponseCode, StatusResponse};
|
||||
use jmap::mailbox::set::{MailboxSubscribe, SCHEMA};
|
||||
use jmap_proto::{
|
||||
|
@ -32,9 +34,6 @@ use jmap_proto::{
|
|||
},
|
||||
};
|
||||
use store::write::{assert::HashedValue, BatchBuilder};
|
||||
use utils::listener::SessionStream;
|
||||
|
||||
use crate::core::{Session, SessionData};
|
||||
|
||||
impl<T: SessionStream> Session<T> {
|
||||
pub async fn handle_subscribe(
|
||||
|
|
|
@ -23,7 +23,9 @@
|
|||
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::core::{SelectedMailbox, Session, SessionData};
|
||||
use ahash::AHashMap;
|
||||
use common::listener::SessionStream;
|
||||
use imap_proto::{
|
||||
protocol::{
|
||||
thread::{Arguments, Response},
|
||||
|
@ -33,10 +35,6 @@ use imap_proto::{
|
|||
Command, StatusResponse,
|
||||
};
|
||||
|
||||
use utils::listener::SessionStream;
|
||||
|
||||
use crate::core::{SelectedMailbox, Session, SessionData};
|
||||
|
||||
impl<T: SessionStream> Session<T> {
|
||||
pub async fn handle_thread(
|
||||
&mut self,
|
||||
|
|
|
@ -26,9 +26,47 @@ use utils::map::vec_map::VecMap;
|
|||
use crate::{
|
||||
error::request::RequestError,
|
||||
parser::{json::Parser, Error, JsonObjectParser},
|
||||
types::type_state::DataType,
|
||||
response::serialize::serialize_hex,
|
||||
types::{id::Id, type_state::DataType},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
pub struct Session {
|
||||
#[serde(rename(serialize = "capabilities"))]
|
||||
capabilities: VecMap<Capability, Capabilities>,
|
||||
#[serde(rename(serialize = "accounts"))]
|
||||
accounts: VecMap<Id, Account>,
|
||||
#[serde(rename(serialize = "primaryAccounts"))]
|
||||
primary_accounts: VecMap<Capability, Id>,
|
||||
#[serde(rename(serialize = "username"))]
|
||||
username: String,
|
||||
#[serde(rename(serialize = "apiUrl"))]
|
||||
api_url: String,
|
||||
#[serde(rename(serialize = "downloadUrl"))]
|
||||
download_url: String,
|
||||
#[serde(rename(serialize = "uploadUrl"))]
|
||||
upload_url: String,
|
||||
#[serde(rename(serialize = "eventSourceUrl"))]
|
||||
event_source_url: String,
|
||||
#[serde(rename(serialize = "state"))]
|
||||
#[serde(serialize_with = "serialize_hex")]
|
||||
state: u32,
|
||||
#[serde(skip)]
|
||||
base_url: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
struct Account {
|
||||
#[serde(rename(serialize = "name"))]
|
||||
name: String,
|
||||
#[serde(rename(serialize = "isPersonal"))]
|
||||
is_personal: bool,
|
||||
#[serde(rename(serialize = "isReadOnly"))]
|
||||
is_read_only: bool,
|
||||
#[serde(rename(serialize = "accountCapabilities"))]
|
||||
account_capabilities: VecMap<Capability, Capabilities>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, serde::Serialize, Hash, PartialEq, Eq)]
|
||||
pub enum Capability {
|
||||
#[serde(rename(serialize = "urn:ietf:params:jmap:core"))]
|
||||
|
@ -158,6 +196,124 @@ pub struct BlobCapabilities {
|
|||
#[derive(Debug, Clone, Default, serde::Serialize)]
|
||||
pub struct EmptyCapabilities {}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct BaseCapabilities {
|
||||
pub session: VecMap<Capability, Capabilities>,
|
||||
pub account: VecMap<Capability, Capabilities>,
|
||||
}
|
||||
|
||||
impl Session {
|
||||
pub fn new(base_url: impl Into<String>, base_capabilities: &BaseCapabilities) -> Session {
|
||||
let base_url = base_url.into();
|
||||
let mut capabilities = base_capabilities.session.clone();
|
||||
capabilities.append(
|
||||
Capability::WebSocket,
|
||||
Capabilities::WebSocket(WebSocketCapabilities::new(&base_url)),
|
||||
);
|
||||
|
||||
Session {
|
||||
capabilities,
|
||||
accounts: VecMap::new(),
|
||||
primary_accounts: VecMap::new(),
|
||||
username: "".to_string(),
|
||||
api_url: format!("{}/jmap/", base_url),
|
||||
download_url: format!(
|
||||
"{}/jmap/download/{{accountId}}/{{blobId}}/{{name}}?accept={{type}}",
|
||||
base_url
|
||||
),
|
||||
upload_url: format!("{}/jmap/upload/{{accountId}}/", base_url),
|
||||
event_source_url: format!(
|
||||
"{}/jmap/eventsource/?types={{types}}&closeafter={{closeafter}}&ping={{ping}}",
|
||||
base_url
|
||||
),
|
||||
base_url,
|
||||
state: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_primary_account(
|
||||
&mut self,
|
||||
account_id: Id,
|
||||
username: String,
|
||||
name: String,
|
||||
capabilities: Option<&[Capability]>,
|
||||
account_capabilities: &VecMap<Capability, Capabilities>,
|
||||
) {
|
||||
self.username = username;
|
||||
|
||||
if let Some(capabilities) = capabilities {
|
||||
for capability in capabilities {
|
||||
self.primary_accounts.append(*capability, account_id);
|
||||
}
|
||||
} else {
|
||||
for capability in self.capabilities.keys() {
|
||||
self.primary_accounts.append(*capability, account_id);
|
||||
}
|
||||
}
|
||||
|
||||
self.accounts.set(
|
||||
account_id,
|
||||
Account::new(name, true, false).add_capabilities(capabilities, account_capabilities),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn add_account(
|
||||
&mut self,
|
||||
account_id: Id,
|
||||
name: String,
|
||||
is_personal: bool,
|
||||
is_read_only: bool,
|
||||
capabilities: Option<&[Capability]>,
|
||||
account_capabilities: &VecMap<Capability, Capabilities>,
|
||||
) {
|
||||
self.accounts.set(
|
||||
account_id,
|
||||
Account::new(name, is_personal, is_read_only)
|
||||
.add_capabilities(capabilities, account_capabilities),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn set_state(&mut self, state: u32) {
|
||||
self.state = state;
|
||||
}
|
||||
|
||||
pub fn api_url(&self) -> &str {
|
||||
&self.api_url
|
||||
}
|
||||
|
||||
pub fn base_url(&self) -> &str {
|
||||
&self.base_url
|
||||
}
|
||||
}
|
||||
|
||||
impl Account {
|
||||
pub fn new(name: String, is_personal: bool, is_read_only: bool) -> Account {
|
||||
Account {
|
||||
name,
|
||||
is_personal,
|
||||
is_read_only,
|
||||
account_capabilities: VecMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_capabilities(
|
||||
mut self,
|
||||
capabilities: Option<&[Capability]>,
|
||||
account_capabilities: &VecMap<Capability, Capabilities>,
|
||||
) -> Account {
|
||||
if let Some(capabilities) = capabilities {
|
||||
for capability in capabilities {
|
||||
if let Some(value) = account_capabilities.get(capability) {
|
||||
self.account_capabilities.append(*capability, value.clone());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.account_capabilities = account_capabilities.clone();
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SieveSessionCapabilities {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
|
@ -166,6 +322,15 @@ impl Default for SieveSessionCapabilities {
|
|||
}
|
||||
}
|
||||
|
||||
impl WebSocketCapabilities {
|
||||
pub fn new(base_url: &str) -> Self {
|
||||
WebSocketCapabilities {
|
||||
url: format!("ws{}/jmap/ws", base_url.strip_prefix("http").unwrap()),
|
||||
supports_push: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl JsonObjectParser for Capability {
|
||||
fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self>
|
||||
where
|
||||
|
|
|
@ -10,6 +10,7 @@ nlp = { path = "../nlp" }
|
|||
jmap_proto = { path = "../jmap-proto" }
|
||||
smtp = { path = "../smtp" }
|
||||
utils = { path = "../utils" }
|
||||
common = { path = "../common" }
|
||||
directory = { path = "../directory" }
|
||||
smtp-proto = { version = "0.1" }
|
||||
mail-parser = { version = "0.9", features = ["full_encoding", "serde_support", "ludicrous_mode"] }
|
||||
|
|
|
@ -36,7 +36,6 @@ use utils::{config::ConfigKey, url_params::UrlParams};
|
|||
|
||||
use crate::{
|
||||
auth::{oauth::OAuthCodeRequest, AccessToken},
|
||||
services::housekeeper,
|
||||
JMAP,
|
||||
};
|
||||
|
||||
|
@ -102,7 +101,9 @@ impl JMAP {
|
|||
body.and_then(|body| serde_json::from_slice::<PrincipalResponse>(&body).ok())
|
||||
{
|
||||
match self
|
||||
.store
|
||||
.core
|
||||
.storage
|
||||
.data
|
||||
.create_account(
|
||||
Principal {
|
||||
id: principal.id,
|
||||
|
@ -141,7 +142,7 @@ impl JMAP {
|
|||
let page: usize = params.parse("page").unwrap_or(0);
|
||||
let limit: usize = params.parse("limit").unwrap_or(0);
|
||||
|
||||
match self.store.list_accounts(filter, typ).await {
|
||||
match self.core.storage.data.list_accounts(filter, typ).await {
|
||||
Ok(accounts) => {
|
||||
let (total, accounts) = if limit > 0 {
|
||||
let offset = page.saturating_sub(1) * limit;
|
||||
|
@ -166,7 +167,7 @@ impl JMAP {
|
|||
}
|
||||
("principal", Some(name), method) => {
|
||||
// Fetch, update or delete principal
|
||||
let account_id = match self.store.get_account_id(name).await {
|
||||
let account_id = match self.core.storage.data.get_account_id(name).await {
|
||||
Ok(Some(account_id)) => account_id,
|
||||
Ok(None) => {
|
||||
return RequestError::blank(
|
||||
|
@ -183,8 +184,16 @@ impl JMAP {
|
|||
|
||||
match *method {
|
||||
Method::GET => {
|
||||
let result = match self.store.query(QueryBy::Id(account_id), true).await {
|
||||
Ok(Some(principal)) => self.store.map_group_ids(principal).await,
|
||||
let result = match self
|
||||
.core
|
||||
.storage
|
||||
.data
|
||||
.query(QueryBy::Id(account_id), true)
|
||||
.await
|
||||
{
|
||||
Ok(Some(principal)) => {
|
||||
self.core.storage.data.map_group_ids(principal).await
|
||||
}
|
||||
Ok(None) => {
|
||||
return RequestError::blank(
|
||||
StatusCode::NOT_FOUND.as_u16(),
|
||||
|
@ -205,11 +214,20 @@ impl JMAP {
|
|||
as u64;
|
||||
|
||||
// Obtain member names
|
||||
for member_id in
|
||||
self.store.get_members(account_id).await.unwrap_or_default()
|
||||
for member_id in self
|
||||
.core
|
||||
.storage
|
||||
.data
|
||||
.get_members(account_id)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
{
|
||||
if let Ok(Some(member_principal)) =
|
||||
self.store.query(QueryBy::Id(member_id), false).await
|
||||
if let Ok(Some(member_principal)) = self
|
||||
.core
|
||||
.storage
|
||||
.data
|
||||
.query(QueryBy::Id(member_id), false)
|
||||
.await
|
||||
{
|
||||
principal.members.push(member_principal.name);
|
||||
}
|
||||
|
@ -225,7 +243,7 @@ impl JMAP {
|
|||
}
|
||||
Method::DELETE => {
|
||||
// Remove FTS index
|
||||
if let Err(err) = self.fts_store.remove_all(account_id).await {
|
||||
if let Err(err) = self.core.storage.fts.remove_all(account_id).await {
|
||||
tracing::warn!(
|
||||
context = "fts",
|
||||
event = "error",
|
||||
|
@ -241,7 +259,13 @@ impl JMAP {
|
|||
}
|
||||
|
||||
// Delete account
|
||||
match self.store.delete_account(QueryBy::Id(account_id)).await {
|
||||
match self
|
||||
.core
|
||||
.storage
|
||||
.data
|
||||
.delete_account(QueryBy::Id(account_id))
|
||||
.await
|
||||
{
|
||||
Ok(_) => JsonResponse::new(json!({
|
||||
"data": (),
|
||||
}))
|
||||
|
@ -254,7 +278,9 @@ impl JMAP {
|
|||
serde_json::from_slice::<Vec<PrincipalUpdate>>(&body).ok()
|
||||
}) {
|
||||
match self
|
||||
.store
|
||||
.core
|
||||
.storage
|
||||
.data
|
||||
.update_account(QueryBy::Id(account_id), changes)
|
||||
.await
|
||||
{
|
||||
|
@ -283,7 +309,7 @@ impl JMAP {
|
|||
let page: usize = params.parse("page").unwrap_or(0);
|
||||
let limit: usize = params.parse("limit").unwrap_or(0);
|
||||
|
||||
match self.store.list_domains(filter).await {
|
||||
match self.core.storage.data.list_domains(filter).await {
|
||||
Ok(domains) => {
|
||||
let (total, domains) = if limit > 0 {
|
||||
let offset = page.saturating_sub(1) * limit;
|
||||
|
@ -308,7 +334,7 @@ impl JMAP {
|
|||
}
|
||||
("domain", Some(domain), &Method::POST) => {
|
||||
// Create domain
|
||||
match self.store.create_domain(domain).await {
|
||||
match self.core.storage.data.create_domain(domain).await {
|
||||
Ok(_) => JsonResponse::new(json!({
|
||||
"data": (),
|
||||
}))
|
||||
|
@ -318,7 +344,7 @@ impl JMAP {
|
|||
}
|
||||
("domain", Some(domain), &Method::DELETE) => {
|
||||
// Delete domain
|
||||
match self.store.delete_domain(domain).await {
|
||||
match self.core.storage.data.delete_domain(domain).await {
|
||||
Ok(_) => JsonResponse::new(json!({
|
||||
"data": (),
|
||||
}))
|
||||
|
@ -327,8 +353,14 @@ impl JMAP {
|
|||
}
|
||||
}
|
||||
("store", Some("maintenance"), &Method::GET) => {
|
||||
match self.store.purge_blobs(self.blob_store.clone()).await {
|
||||
Ok(_) => match self.store.purge_store().await {
|
||||
match self
|
||||
.core
|
||||
.storage
|
||||
.data
|
||||
.purge_blobs(self.core.storage.blob.clone())
|
||||
.await
|
||||
{
|
||||
Ok(_) => match self.core.storage.data.purge_store().await {
|
||||
Ok(_) => JsonResponse::new(json!({
|
||||
"data": (),
|
||||
}))
|
||||
|
@ -348,8 +380,9 @@ impl JMAP {
|
|||
.into_http_response(),
|
||||
}
|
||||
}
|
||||
("reload", Some("settings"), &Method::GET) => {
|
||||
/*("reload", Some("settings"), &Method::GET) => {
|
||||
let _ = self
|
||||
.inner
|
||||
.housekeeper_tx
|
||||
.send(housekeeper::Event::ReloadConfig)
|
||||
.await;
|
||||
|
@ -361,6 +394,7 @@ impl JMAP {
|
|||
}
|
||||
("reload", Some("certificates"), &Method::GET) => {
|
||||
let _ = self
|
||||
.inner
|
||||
.housekeeper_tx
|
||||
.send(housekeeper::Event::ReloadCertificates)
|
||||
.await;
|
||||
|
@ -369,7 +403,7 @@ impl JMAP {
|
|||
"data": (),
|
||||
}))
|
||||
.into_http_response()
|
||||
}
|
||||
}*/
|
||||
("settings", Some("group"), &Method::GET) => {
|
||||
// List settings
|
||||
let params = UrlParams::new(req.uri().query());
|
||||
|
@ -400,7 +434,7 @@ impl JMAP {
|
|||
params.parse::<usize>("page").unwrap_or(0).saturating_sub(1) * limit;
|
||||
let has_filter = !filter.is_empty();
|
||||
|
||||
match self.store.config_list(&prefix, true).await {
|
||||
match self.core.storage.data.config_list(&prefix, true).await {
|
||||
Ok(settings) => if !suffix.is_empty() && !settings.is_empty() {
|
||||
// Obtain record ids
|
||||
let mut total = 0;
|
||||
|
@ -514,7 +548,7 @@ impl JMAP {
|
|||
let limit: usize = params.parse("limit").unwrap_or(0);
|
||||
let offset = params.parse::<usize>("page").unwrap_or(0).saturating_sub(1) * limit;
|
||||
|
||||
match self.store.config_list(&prefix, true).await {
|
||||
match self.core.storage.data.config_list(&prefix, true).await {
|
||||
Ok(settings) => {
|
||||
let total = settings.len();
|
||||
let items = settings
|
||||
|
@ -554,7 +588,7 @@ impl JMAP {
|
|||
let mut results = AHashMap::with_capacity(keys.len());
|
||||
|
||||
for key in keys {
|
||||
match self.store.config_get(key).await {
|
||||
match self.core.storage.data.config_get(key).await {
|
||||
Ok(Some(value)) => {
|
||||
results.insert(key.to_string(), value);
|
||||
}
|
||||
|
@ -571,7 +605,7 @@ impl JMAP {
|
|||
} else {
|
||||
prefix.to_string()
|
||||
};
|
||||
match self.store.config_list(&prefix, false).await {
|
||||
match self.core.storage.data.config_list(&prefix, false).await {
|
||||
Ok(values) => {
|
||||
results.extend(values);
|
||||
}
|
||||
|
@ -597,7 +631,7 @@ impl JMAP {
|
|||
}
|
||||
}
|
||||
("settings", Some(prefix), &Method::DELETE) if !prefix.is_empty() => {
|
||||
match self.store.config_clear(prefix).await {
|
||||
match self.core.storage.data.config_clear(prefix).await {
|
||||
Ok(_) => JsonResponse::new(json!({
|
||||
"data": (),
|
||||
}))
|
||||
|
@ -620,15 +654,26 @@ impl JMAP {
|
|||
match change {
|
||||
UpdateSettings::Delete { keys } => {
|
||||
for key in keys {
|
||||
result = self.store.config_clear(key).await.map(|_| true);
|
||||
result = self
|
||||
.core
|
||||
.storage
|
||||
.data
|
||||
.config_clear(key)
|
||||
.await
|
||||
.map(|_| true);
|
||||
if result.is_err() {
|
||||
break 'next;
|
||||
}
|
||||
}
|
||||
}
|
||||
UpdateSettings::Clear { prefix } => {
|
||||
result =
|
||||
self.store.config_clear_prefix(&prefix).await.map(|_| true);
|
||||
result = self
|
||||
.core
|
||||
.storage
|
||||
.data
|
||||
.config_clear_prefix(&prefix)
|
||||
.await
|
||||
.map(|_| true);
|
||||
if result.is_err() {
|
||||
break;
|
||||
}
|
||||
|
@ -641,7 +686,9 @@ impl JMAP {
|
|||
if assert_empty {
|
||||
if let Some(prefix) = &prefix {
|
||||
result = self
|
||||
.store
|
||||
.core
|
||||
.storage
|
||||
.data
|
||||
.config_list(&format!("{prefix}."), true)
|
||||
.await
|
||||
.map(|items| items.is_empty());
|
||||
|
@ -651,7 +698,9 @@ impl JMAP {
|
|||
}
|
||||
} else if let Some((key, _)) = values.first() {
|
||||
result = self
|
||||
.store
|
||||
.core
|
||||
.storage
|
||||
.data
|
||||
.config_get(key)
|
||||
.await
|
||||
.map(|items| items.is_none());
|
||||
|
@ -663,7 +712,9 @@ impl JMAP {
|
|||
}
|
||||
|
||||
result = self
|
||||
.store
|
||||
.core
|
||||
.storage
|
||||
.data
|
||||
.config_set(values.into_iter().map(|(key, value)| ConfigKey {
|
||||
key: if let Some(prefix) = &prefix {
|
||||
format!("{prefix}.{key}")
|
||||
|
|
|
@ -1,187 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2023 Stalwart Labs Ltd.
|
||||
*
|
||||
* This file is part of Stalwart Mail Server.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of
|
||||
* the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
* in the LICENSE file at the top-level directory of this distribution.
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* You can be released from the requirements of the AGPLv3 license by
|
||||
* purchasing a commercial license. Please contact licensing@stalw.art
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
use std::{str::FromStr, time::Duration};
|
||||
|
||||
use nlp::language::Language;
|
||||
use store::rand::{distributions::Alphanumeric, thread_rng, Rng};
|
||||
|
||||
use super::session::BaseCapabilities;
|
||||
|
||||
impl crate::Config {
|
||||
pub fn new(settings: &utils::config::Config) -> Result<Self, String> {
|
||||
let mut config = Self {
|
||||
default_language: Language::from_iso_639(
|
||||
settings
|
||||
.value("storage.full-text.default-language")
|
||||
.unwrap_or("en"),
|
||||
)
|
||||
.unwrap_or(Language::English),
|
||||
query_max_results: settings
|
||||
.property("jmap.protocol.query.max-results")?
|
||||
.unwrap_or(5000),
|
||||
changes_max_results: settings
|
||||
.property("jmap.protocol.changes.max-results")?
|
||||
.unwrap_or(5000),
|
||||
snippet_max_results: settings
|
||||
.property("jmap.protocol.search-snippet.max-results")?
|
||||
.unwrap_or(100),
|
||||
request_max_size: settings
|
||||
.property("jmap.protocol.request.max-size")?
|
||||
.unwrap_or(10000000),
|
||||
request_max_calls: settings
|
||||
.property("jmap.protocol.request.max-calls")?
|
||||
.unwrap_or(16),
|
||||
request_max_concurrent: settings
|
||||
.property("jmap.protocol.request.max-concurrent")?
|
||||
.unwrap_or(4),
|
||||
get_max_objects: settings
|
||||
.property("jmap.protocol.get.max-objects")?
|
||||
.unwrap_or(500),
|
||||
set_max_objects: settings
|
||||
.property("jmap.protocol.set.max-objects")?
|
||||
.unwrap_or(500),
|
||||
upload_max_size: settings
|
||||
.property("jmap.protocol.upload.max-size")?
|
||||
.unwrap_or(50000000),
|
||||
upload_max_concurrent: settings
|
||||
.property("jmap.protocol.upload.max-concurrent")?
|
||||
.unwrap_or(4),
|
||||
upload_tmp_quota_size: settings
|
||||
.property("jmap.protocol.upload.quota.size")?
|
||||
.unwrap_or(50000000),
|
||||
upload_tmp_quota_amount: settings
|
||||
.property("jmap.protocol.upload.quota.files")?
|
||||
.unwrap_or(1000),
|
||||
upload_tmp_ttl: settings
|
||||
.property_or_default::<Duration>("jmap.protocol.upload.ttl", "1h")?
|
||||
.as_secs(),
|
||||
mailbox_max_depth: settings.property("jmap.mailbox.max-depth")?.unwrap_or(10),
|
||||
mailbox_name_max_len: settings
|
||||
.property("jmap.mailbox.max-name-length")?
|
||||
.unwrap_or(255),
|
||||
mail_attachments_max_size: settings
|
||||
.property("jmap.email.max-attachment-size")?
|
||||
.unwrap_or(50000000),
|
||||
mail_max_size: settings
|
||||
.property("jmap.email.max-size")?
|
||||
.unwrap_or(75000000),
|
||||
mail_parse_max_items: settings
|
||||
.property("jmap.email.parse.max-items")?
|
||||
.unwrap_or(10),
|
||||
sieve_max_script_name: settings
|
||||
.property("sieve.untrusted.limits.name-length")?
|
||||
.unwrap_or(512),
|
||||
sieve_max_scripts: settings
|
||||
.property("sieve.untrusted.limits.max-scripts")?
|
||||
.unwrap_or(256),
|
||||
capabilities: BaseCapabilities::default(),
|
||||
session_cache_ttl: settings
|
||||
.property("cache.session.ttl")?
|
||||
.unwrap_or(Duration::from_secs(3600)),
|
||||
rate_authenticated: settings
|
||||
.property_or_default("jmap.rate-limit.account", "1000/1m")?,
|
||||
rate_authenticate_req: settings
|
||||
.property_or_default("authentication.rate-limit", "10/1m")?,
|
||||
rate_anonymous: settings.property_or_default("jmap.rate-limit.anonymous", "100/1m")?,
|
||||
rate_use_forwarded: settings
|
||||
.property("jmap.rate-limit.use-forwarded")?
|
||||
.unwrap_or(false),
|
||||
oauth_key: settings
|
||||
.value("oauth.key")
|
||||
.map(|s| s.to_string())
|
||||
.unwrap_or_else(|| {
|
||||
thread_rng()
|
||||
.sample_iter(Alphanumeric)
|
||||
.take(64)
|
||||
.map(char::from)
|
||||
.collect::<String>()
|
||||
}),
|
||||
oauth_expiry_user_code: settings
|
||||
.property_or_default::<Duration>("oauth.expiry.user-code", "30m")?
|
||||
.as_secs(),
|
||||
oauth_expiry_auth_code: settings
|
||||
.property_or_default::<Duration>("oauth.expiry.auth-code", "10m")?
|
||||
.as_secs(),
|
||||
oauth_expiry_token: settings
|
||||
.property_or_default::<Duration>("oauth.expiry.token", "1h")?
|
||||
.as_secs(),
|
||||
oauth_expiry_refresh_token: settings
|
||||
.property_or_default::<Duration>("oauth.expiry.refresh-token", "30d")?
|
||||
.as_secs(),
|
||||
oauth_expiry_refresh_token_renew: settings
|
||||
.property_or_default::<Duration>("oauth.expiry.refresh-token-renew", "4d")?
|
||||
.as_secs(),
|
||||
oauth_max_auth_attempts: settings
|
||||
.property_or_default("oauth.auth.max-attempts", "3")?,
|
||||
event_source_throttle: settings
|
||||
.property_or_default("jmap.event-source.throttle", "1s")?,
|
||||
web_socket_throttle: settings.property_or_default("jmap.web-socket.throttle", "1s")?,
|
||||
web_socket_timeout: settings.property_or_default("jmap.web-socket.timeout", "10m")?,
|
||||
web_socket_heartbeat: settings
|
||||
.property_or_default("jmap.web-socket.heartbeat", "1m")?,
|
||||
push_max_total: settings.property_or_default("jmap.push.max-total", "100")?,
|
||||
principal_allow_lookups: settings
|
||||
.property("jmap.principal.allow-lookups")?
|
||||
.unwrap_or(true),
|
||||
encrypt: settings.property_or_default("storage.encryption.enable", "true")?,
|
||||
encrypt_append: settings.property_or_default("storage.encryption.append", "false")?,
|
||||
spam_header: settings.value("spam.header.is-spam").and_then(|v| {
|
||||
v.split_once(':').map(|(k, v)| {
|
||||
(
|
||||
mail_parser::HeaderName::parse(k.trim().to_string()).unwrap(),
|
||||
v.trim().to_string(),
|
||||
)
|
||||
})
|
||||
}),
|
||||
http_headers: settings
|
||||
.values("server.http.headers")
|
||||
.map(|(_, v)| {
|
||||
if let Some((k, v)) = v.split_once(':') {
|
||||
Ok((
|
||||
hyper::header::HeaderName::from_str(k.trim()).map_err(|err| {
|
||||
format!(
|
||||
"Invalid header found in property \"server.http.headers\": {}",
|
||||
err
|
||||
)
|
||||
})?,
|
||||
hyper::header::HeaderValue::from_str(v.trim()).map_err(|err| {
|
||||
format!(
|
||||
"Invalid header found in property \"server.http.headers\": {}",
|
||||
err
|
||||
)
|
||||
})?,
|
||||
))
|
||||
} else {
|
||||
Err(format!(
|
||||
"Invalid header found in property \"server.http.headers\": {}",
|
||||
v
|
||||
))
|
||||
}
|
||||
})
|
||||
.collect::<Result<Vec<_>, String>>()?,
|
||||
};
|
||||
config.add_capabilites(settings);
|
||||
Ok(config)
|
||||
}
|
||||
}
|
|
@ -106,7 +106,7 @@ impl JMAP {
|
|||
None
|
||||
};
|
||||
let mut response = StateChangeResponse::new();
|
||||
let throttle = self.config.event_source_throttle;
|
||||
let throttle = self.core.jmap.event_source_throttle;
|
||||
|
||||
// Register with state manager
|
||||
let mut change_rx = if let Some(change_rx) = self
|
||||
|
|
|
@ -23,6 +23,12 @@
|
|||
|
||||
use std::{net::IpAddr, sync::Arc};
|
||||
|
||||
use common::{
|
||||
config::smtp::{V_LISTENER, V_LOCAL_IP, V_REMOTE_IP},
|
||||
expr::functions::ResolveVariable,
|
||||
listener::{ServerInstance, SessionData, SessionManager, SessionStream},
|
||||
Core,
|
||||
};
|
||||
use http_body_util::{BodyExt, Full};
|
||||
use hyper::{
|
||||
body::{self, Bytes},
|
||||
|
@ -34,214 +40,240 @@ use hyper::{
|
|||
use hyper_util::rt::TokioIo;
|
||||
use jmap_proto::{
|
||||
error::request::{RequestError, RequestLimitError},
|
||||
request::Request,
|
||||
request::{capability::Session, Request},
|
||||
response::Response,
|
||||
types::{blob::BlobId, id::Id},
|
||||
};
|
||||
|
||||
use utils::listener::{ServerInstance, SessionData, SessionManager, SessionStream};
|
||||
|
||||
use crate::{
|
||||
auth::{oauth::OAuthMetadata, AccessToken},
|
||||
blob::{DownloadResponse, UploadResponse},
|
||||
services::state,
|
||||
websocket::upgrade::upgrade_websocket_connection,
|
||||
JMAP,
|
||||
};
|
||||
|
||||
use super::{
|
||||
session::Session, HtmlResponse, HttpRequest, HttpResponse, JmapSessionManager, JsonResponse,
|
||||
};
|
||||
use super::{HtmlResponse, HttpRequest, HttpResponse, JmapSessionManager, JsonResponse};
|
||||
|
||||
pub async fn parse_jmap_request(
|
||||
jmap: Arc<JMAP>,
|
||||
mut req: HttpRequest,
|
||||
remote_ip: IpAddr,
|
||||
instance: Arc<ServerInstance>,
|
||||
) -> HttpResponse {
|
||||
let mut path = req.uri().path().split('/');
|
||||
path.next();
|
||||
pub struct HttpSessionData {
|
||||
pub instance: Arc<ServerInstance>,
|
||||
pub local_ip: IpAddr,
|
||||
pub remote_ip: IpAddr,
|
||||
pub is_tls: bool,
|
||||
}
|
||||
|
||||
match path.next().unwrap_or("") {
|
||||
"jmap" => {
|
||||
// Authenticate request
|
||||
let (_in_flight, access_token) = match jmap.authenticate_headers(&req, remote_ip).await
|
||||
{
|
||||
Ok(Some(session)) => session,
|
||||
Ok(None) => {
|
||||
return if req.method() != Method::OPTIONS {
|
||||
RequestError::unauthorized().into_http_response()
|
||||
} else {
|
||||
().into_http_response()
|
||||
}
|
||||
}
|
||||
Err(err) => return err.into_http_response(),
|
||||
};
|
||||
impl JMAP {
|
||||
pub async fn parse_http_request(
|
||||
&self,
|
||||
mut req: HttpRequest,
|
||||
session: HttpSessionData,
|
||||
) -> HttpResponse {
|
||||
let mut path = req.uri().path().split('/');
|
||||
path.next();
|
||||
|
||||
match (path.next().unwrap_or(""), req.method()) {
|
||||
("", &Method::POST) => {
|
||||
return match fetch_body(&mut req, jmap.config.request_max_size, &access_token)
|
||||
match path.next().unwrap_or("") {
|
||||
"jmap" => {
|
||||
// Authenticate request
|
||||
let (_in_flight, access_token) =
|
||||
match self.authenticate_headers(&req, session.remote_ip).await {
|
||||
Ok(Some(session)) => session,
|
||||
Ok(None) => {
|
||||
return if req.method() != Method::OPTIONS {
|
||||
RequestError::unauthorized().into_http_response()
|
||||
} else {
|
||||
().into_http_response()
|
||||
}
|
||||
}
|
||||
Err(err) => return err.into_http_response(),
|
||||
};
|
||||
|
||||
match (path.next().unwrap_or(""), req.method()) {
|
||||
("", &Method::POST) => {
|
||||
return match fetch_body(
|
||||
&mut req,
|
||||
self.core.jmap.request_max_size,
|
||||
&access_token,
|
||||
)
|
||||
.await
|
||||
.ok_or_else(|| RequestError::limit(RequestLimitError::SizeRequest))
|
||||
.and_then(|bytes| {
|
||||
Request::parse(
|
||||
&bytes,
|
||||
jmap.config.request_max_calls,
|
||||
jmap.config.request_max_size,
|
||||
self.core.jmap.request_max_calls,
|
||||
self.core.jmap.request_max_size,
|
||||
)
|
||||
}) {
|
||||
Ok(request) => {
|
||||
//let _ = println!("<- {}", String::from_utf8_lossy(&bytes));
|
||||
Ok(request) => {
|
||||
//let _ = println!("<- {}", String::from_utf8_lossy(&bytes));
|
||||
|
||||
match jmap.handle_request(request, access_token, &instance).await {
|
||||
Ok(response) => response.into_http_response(),
|
||||
Err(err) => err.into_http_response(),
|
||||
}
|
||||
}
|
||||
Err(err) => err.into_http_response(),
|
||||
};
|
||||
}
|
||||
("download", &Method::GET) => {
|
||||
if let (Some(_), Some(blob_id), Some(name)) = (
|
||||
path.next().and_then(|p| Id::from_bytes(p.as_bytes())),
|
||||
path.next().and_then(BlobId::from_base32),
|
||||
path.next(),
|
||||
) {
|
||||
return match jmap.blob_download(&blob_id, &access_token).await {
|
||||
Ok(Some(blob)) => DownloadResponse {
|
||||
filename: name.to_string(),
|
||||
content_type: req
|
||||
.uri()
|
||||
.query()
|
||||
.and_then(|q| {
|
||||
form_urlencoded::parse(q.as_bytes())
|
||||
.find(|(k, _)| k == "accept")
|
||||
.map(|(_, v)| v.into_owned())
|
||||
})
|
||||
.unwrap_or("application/octet-stream".to_string()),
|
||||
blob,
|
||||
}
|
||||
.into_http_response(),
|
||||
Ok(None) => RequestError::not_found().into_http_response(),
|
||||
Err(_) => RequestError::internal_server_error().into_http_response(),
|
||||
};
|
||||
}
|
||||
}
|
||||
("upload", &Method::POST) => {
|
||||
if let Some(account_id) = path.next().and_then(|p| Id::from_bytes(p.as_bytes()))
|
||||
{
|
||||
return match fetch_body(
|
||||
&mut req,
|
||||
jmap.config.upload_max_size,
|
||||
&access_token,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Some(bytes) => {
|
||||
match jmap
|
||||
.blob_upload(
|
||||
account_id,
|
||||
req.headers()
|
||||
.get(CONTENT_TYPE)
|
||||
.and_then(|h| h.to_str().ok())
|
||||
.unwrap_or("application/octet-stream"),
|
||||
&bytes,
|
||||
access_token,
|
||||
)
|
||||
match self
|
||||
.handle_request(request, access_token, &session.instance)
|
||||
.await
|
||||
{
|
||||
Ok(response) => response.into_http_response(),
|
||||
Err(err) => err.into_http_response(),
|
||||
}
|
||||
}
|
||||
None => RequestError::limit(RequestLimitError::SizeUpload)
|
||||
.into_http_response(),
|
||||
Err(err) => err.into_http_response(),
|
||||
};
|
||||
}
|
||||
("download", &Method::GET) => {
|
||||
if let (Some(_), Some(blob_id), Some(name)) = (
|
||||
path.next().and_then(|p| Id::from_bytes(p.as_bytes())),
|
||||
path.next().and_then(BlobId::from_base32),
|
||||
path.next(),
|
||||
) {
|
||||
return match self.blob_download(&blob_id, &access_token).await {
|
||||
Ok(Some(blob)) => DownloadResponse {
|
||||
filename: name.to_string(),
|
||||
content_type: req
|
||||
.uri()
|
||||
.query()
|
||||
.and_then(|q| {
|
||||
form_urlencoded::parse(q.as_bytes())
|
||||
.find(|(k, _)| k == "accept")
|
||||
.map(|(_, v)| v.into_owned())
|
||||
})
|
||||
.unwrap_or("application/octet-stream".to_string()),
|
||||
blob,
|
||||
}
|
||||
.into_http_response(),
|
||||
Ok(None) => RequestError::not_found().into_http_response(),
|
||||
Err(_) => {
|
||||
RequestError::internal_server_error().into_http_response()
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
("upload", &Method::POST) => {
|
||||
if let Some(account_id) =
|
||||
path.next().and_then(|p| Id::from_bytes(p.as_bytes()))
|
||||
{
|
||||
return match fetch_body(
|
||||
&mut req,
|
||||
self.core.jmap.upload_max_size,
|
||||
&access_token,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Some(bytes) => {
|
||||
match self
|
||||
.blob_upload(
|
||||
account_id,
|
||||
req.headers()
|
||||
.get(CONTENT_TYPE)
|
||||
.and_then(|h| h.to_str().ok())
|
||||
.unwrap_or("application/octet-stream"),
|
||||
&bytes,
|
||||
access_token,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(response) => response.into_http_response(),
|
||||
Err(err) => err.into_http_response(),
|
||||
}
|
||||
}
|
||||
None => RequestError::limit(RequestLimitError::SizeUpload)
|
||||
.into_http_response(),
|
||||
};
|
||||
}
|
||||
}
|
||||
("eventsource", &Method::GET) => {
|
||||
return self.handle_event_source(req, access_token).await
|
||||
}
|
||||
("ws", &Method::GET) => {
|
||||
return self
|
||||
.upgrade_websocket_connection(
|
||||
req,
|
||||
access_token,
|
||||
session.instance.clone(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
(_, &Method::OPTIONS) => {
|
||||
return ().into_http_response();
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
("eventsource", &Method::GET) => {
|
||||
return jmap.handle_event_source(req, access_token).await
|
||||
}
|
||||
".well-known" => match (path.next().unwrap_or(""), req.method()) {
|
||||
("jmap", &Method::GET) => {
|
||||
// Authenticate request
|
||||
let (_in_flight, access_token) =
|
||||
match self.authenticate_headers(&req, session.remote_ip).await {
|
||||
Ok(Some(session)) => session,
|
||||
Ok(None) => return RequestError::unauthorized().into_http_response(),
|
||||
Err(err) => return err.into_http_response(),
|
||||
};
|
||||
|
||||
return match self
|
||||
.handle_session_resource(
|
||||
session.resolve_url(&self.core).await,
|
||||
access_token,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(session) => session.into_http_response(),
|
||||
Err(err) => err.into_http_response(),
|
||||
};
|
||||
}
|
||||
("ws", &Method::GET) => {
|
||||
return upgrade_websocket_connection(jmap, req, access_token, instance.clone())
|
||||
.await;
|
||||
("oauth-authorization-server", &Method::GET) => {
|
||||
// Limit anonymous requests
|
||||
return match self.is_anonymous_allowed(&session.remote_ip).await {
|
||||
Ok(_) => JsonResponse::new(OAuthMetadata::new(
|
||||
session.resolve_url(&self.core).await,
|
||||
))
|
||||
.into_http_response(),
|
||||
Err(err) => err.into_http_response(),
|
||||
};
|
||||
}
|
||||
(_, &Method::OPTIONS) => {
|
||||
return ().into_http_response();
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
".well-known" => match (path.next().unwrap_or(""), req.method()) {
|
||||
("jmap", &Method::GET) => {
|
||||
// Authenticate request
|
||||
let (_in_flight, access_token) =
|
||||
match jmap.authenticate_headers(&req, remote_ip).await {
|
||||
Ok(Some(session)) => session,
|
||||
Ok(None) => return RequestError::unauthorized().into_http_response(),
|
||||
Err(err) => return err.into_http_response(),
|
||||
};
|
||||
|
||||
return match jmap.handle_session_resource(instance, access_token).await {
|
||||
Ok(session) => session.into_http_response(),
|
||||
Err(err) => err.into_http_response(),
|
||||
};
|
||||
}
|
||||
("oauth-authorization-server", &Method::GET) => {
|
||||
let remote_addr = jmap.build_remote_addr(&req, remote_ip);
|
||||
// Limit anonymous requests
|
||||
return match jmap.is_anonymous_allowed(&remote_addr).await {
|
||||
Ok(_) => {
|
||||
JsonResponse::new(OAuthMetadata::new(&instance.data)).into_http_response()
|
||||
}
|
||||
Err(err) => err.into_http_response(),
|
||||
};
|
||||
}
|
||||
(_, &Method::OPTIONS) => {
|
||||
return ().into_http_response();
|
||||
}
|
||||
_ => (),
|
||||
},
|
||||
"auth" => {
|
||||
let remote_addr = jmap.build_remote_addr(&req, remote_ip);
|
||||
|
||||
match (path.next().unwrap_or(""), req.method()) {
|
||||
},
|
||||
"auth" => match (path.next().unwrap_or(""), req.method()) {
|
||||
("", &Method::GET) => {
|
||||
return match jmap.is_anonymous_allowed(&remote_addr).await {
|
||||
Ok(_) => jmap.handle_user_device_auth(&mut req).await,
|
||||
return match self.is_anonymous_allowed(&session.remote_ip).await {
|
||||
Ok(_) => self.handle_user_device_auth(&mut req).await,
|
||||
Err(err) => err.into_http_response(),
|
||||
}
|
||||
}
|
||||
("", &Method::POST) => {
|
||||
return match jmap.is_auth_allowed_soft(&remote_addr).await {
|
||||
return match self.is_auth_allowed_soft(&session.remote_ip).await {
|
||||
Ok(_) => {
|
||||
jmap.handle_user_device_auth_post(&mut req, remote_addr)
|
||||
self.handle_user_device_auth_post(&mut req, session.remote_ip)
|
||||
.await
|
||||
}
|
||||
Err(err) => err.into_http_response(),
|
||||
}
|
||||
}
|
||||
("code", &Method::GET) => {
|
||||
return match jmap.is_anonymous_allowed(&remote_addr).await {
|
||||
Ok(_) => jmap.handle_user_code_auth(&mut req).await,
|
||||
return match self.is_anonymous_allowed(&session.remote_ip).await {
|
||||
Ok(_) => self.handle_user_code_auth(&mut req).await,
|
||||
Err(err) => err.into_http_response(),
|
||||
}
|
||||
}
|
||||
("code", &Method::POST) => {
|
||||
return match jmap.is_auth_allowed_soft(&remote_addr).await {
|
||||
Ok(_) => jmap.handle_user_code_auth_post(&mut req, remote_addr).await,
|
||||
return match self.is_auth_allowed_soft(&session.remote_ip).await {
|
||||
Ok(_) => {
|
||||
self.handle_user_code_auth_post(&mut req, session.remote_ip)
|
||||
.await
|
||||
}
|
||||
Err(err) => err.into_http_response(),
|
||||
}
|
||||
}
|
||||
("device", &Method::POST) => {
|
||||
return match jmap.is_anonymous_allowed(&remote_addr).await {
|
||||
Ok(_) => jmap.handle_device_auth(&mut req, instance).await,
|
||||
return match self.is_anonymous_allowed(&session.remote_ip).await {
|
||||
Ok(_) => {
|
||||
self.handle_device_auth(&mut req, session.resolve_url(&self.core).await)
|
||||
.await
|
||||
}
|
||||
Err(err) => err.into_http_response(),
|
||||
}
|
||||
}
|
||||
("token", &Method::POST) => {
|
||||
return match jmap.is_anonymous_allowed(&remote_addr).await {
|
||||
Ok(_) => jmap.handle_token_request(&mut req).await,
|
||||
return match self.is_anonymous_allowed(&session.remote_ip).await {
|
||||
Ok(_) => self.handle_token_request(&mut req).await,
|
||||
Err(err) => err.into_http_response(),
|
||||
}
|
||||
}
|
||||
|
@ -249,116 +281,160 @@ pub async fn parse_jmap_request(
|
|||
return ().into_http_response();
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
"crypto" if jmap.config.encrypt => {
|
||||
let remote_addr = jmap.build_remote_addr(&req, remote_ip);
|
||||
|
||||
match *req.method() {
|
||||
},
|
||||
"crypto" if self.core.jmap.encrypt => match *req.method() {
|
||||
Method::GET => {
|
||||
return jmap.handle_crypto_update(&mut req, remote_addr).await;
|
||||
return self.handle_crypto_update(&mut req, session.remote_ip).await;
|
||||
}
|
||||
Method::POST => {
|
||||
return match jmap.is_auth_allowed_soft(&remote_addr).await {
|
||||
Ok(_) => jmap.handle_crypto_update(&mut req, remote_addr).await,
|
||||
return match self.is_auth_allowed_soft(&session.remote_ip).await {
|
||||
Ok(_) => self.handle_crypto_update(&mut req, session.remote_ip).await,
|
||||
Err(err) => err.into_http_response(),
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
"api" => {
|
||||
// Allow CORS preflight requests
|
||||
if req.method() == Method::OPTIONS {
|
||||
return ().into_http_response();
|
||||
}
|
||||
|
||||
// Make sure the user is a superuser
|
||||
return match jmap.authenticate_headers(&req, remote_ip).await {
|
||||
Ok(Some((_, access_token))) => {
|
||||
let body = fetch_body(&mut req, 8192, &access_token).await;
|
||||
if access_token.is_super_user() {
|
||||
jmap.handle_api_manage_request(&req, body, access_token)
|
||||
.await
|
||||
} else {
|
||||
jmap.handle_api_request(&req, body, access_token).await
|
||||
}
|
||||
},
|
||||
"api" => {
|
||||
// Allow CORS preflight requests
|
||||
if req.method() == Method::OPTIONS {
|
||||
return ().into_http_response();
|
||||
}
|
||||
Ok(None) => RequestError::unauthorized().into_http_response(),
|
||||
Err(err) => err.into_http_response(),
|
||||
};
|
||||
|
||||
// Make sure the user is a superuser
|
||||
return match self.authenticate_headers(&req, session.remote_ip).await {
|
||||
Ok(Some((_, access_token))) => {
|
||||
let body = fetch_body(&mut req, 8192, &access_token).await;
|
||||
if access_token.is_super_user() {
|
||||
self.handle_api_manage_request(&req, body, access_token)
|
||||
.await
|
||||
} else {
|
||||
self.handle_api_request(&req, body, access_token).await
|
||||
}
|
||||
}
|
||||
Ok(None) => RequestError::unauthorized().into_http_response(),
|
||||
Err(err) => err.into_http_response(),
|
||||
};
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
RequestError::not_found().into_http_response()
|
||||
}
|
||||
|
||||
async fn handle_session<T: SessionStream>(self, session: SessionData<T>) {
|
||||
let span = session.span;
|
||||
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 = self.clone();
|
||||
let span = span.clone();
|
||||
let instance = session.instance.clone();
|
||||
|
||||
async move {
|
||||
tracing::debug!(
|
||||
parent: &span,
|
||||
event = "request",
|
||||
uri = req.uri().to_string(),
|
||||
);
|
||||
|
||||
// Obtain remote IP
|
||||
let remote_ip = if !jmap.core.jmap.http_use_forwarded {
|
||||
session.remote_ip
|
||||
} else if let Some(forwarded_for) = req
|
||||
.headers()
|
||||
.get(header::FORWARDED)
|
||||
.or_else(|| req.headers().get("X-Forwarded-For"))
|
||||
.and_then(|h| h.to_str().ok())
|
||||
.and_then(|h| h.parse::<IpAddr>().ok())
|
||||
{
|
||||
forwarded_for
|
||||
} else {
|
||||
tracing::warn!(
|
||||
"Warning: No remote address found in request, using remote ip."
|
||||
);
|
||||
session.remote_ip
|
||||
};
|
||||
|
||||
// Parse HTTP request
|
||||
let mut response = jmap
|
||||
.parse_http_request(
|
||||
req,
|
||||
HttpSessionData {
|
||||
instance,
|
||||
local_ip: session.local_ip,
|
||||
remote_ip,
|
||||
is_tls,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
// Add custom headers
|
||||
if !jmap.core.jmap.http_headers.is_empty() {
|
||||
let headers = response.headers_mut();
|
||||
|
||||
for (header, value) in &jmap.core.jmap.http_headers {
|
||||
headers.insert(header.clone(), value.clone());
|
||||
}
|
||||
}
|
||||
|
||||
Ok::<_, hyper::Error>(response)
|
||||
}
|
||||
}),
|
||||
)
|
||||
.with_upgrades()
|
||||
.await
|
||||
{
|
||||
tracing::debug!(
|
||||
parent: &span,
|
||||
event = "error",
|
||||
context = "http",
|
||||
reason = %http_err,
|
||||
);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
RequestError::not_found().into_http_response()
|
||||
}
|
||||
|
||||
impl SessionManager for JmapSessionManager {
|
||||
fn handle<T: utils::listener::SessionStream>(
|
||||
fn handle<T: SessionStream>(
|
||||
self,
|
||||
session: SessionData<T>,
|
||||
) -> impl std::future::Future<Output = ()> + Send {
|
||||
handle_request(self.inner, session)
|
||||
JMAP::from(self.inner).handle_session(session)
|
||||
}
|
||||
|
||||
#[allow(clippy::manual_async_fn)]
|
||||
fn shutdown(&self) -> impl std::future::Future<Output = ()> + Send {
|
||||
async {
|
||||
let _ = self.inner.state_tx.send(state::Event::Stop).await;
|
||||
let _ = self
|
||||
.inner
|
||||
.jmap_inner
|
||||
.state_tx
|
||||
.send(state::Event::Stop)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
fn is_ip_blocked(&self, addr: &IpAddr) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_request<T: SessionStream>(jmap: Arc<JMAP>, session: SessionData<T>) {
|
||||
let span = session.span;
|
||||
let _in_flight = session.in_flight;
|
||||
impl ResolveVariable for HttpSessionData {
|
||||
fn resolve_variable(&self, variable: u32) -> common::expr::Variable<'_> {
|
||||
match variable {
|
||||
V_REMOTE_IP => self.remote_ip.to_string().into(),
|
||||
V_LOCAL_IP => self.local_ip.to_string().into(),
|
||||
V_LISTENER => self.instance.id.as_str().into(),
|
||||
_ => common::expr::Variable::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 = jmap.clone();
|
||||
let span = span.clone();
|
||||
let instance = session.instance.clone();
|
||||
|
||||
async move {
|
||||
tracing::debug!(
|
||||
parent: &span,
|
||||
event = "request",
|
||||
uri = req.uri().to_string(),
|
||||
);
|
||||
|
||||
// Parse JMAP request
|
||||
let mut response =
|
||||
parse_jmap_request(jmap.clone(), req, session.remote_ip, instance).await;
|
||||
|
||||
// Add custom headers
|
||||
if !jmap.config.http_headers.is_empty() {
|
||||
let headers = response.headers_mut();
|
||||
|
||||
for (header, value) in &jmap.config.http_headers {
|
||||
headers.insert(header.clone(), value.clone());
|
||||
}
|
||||
}
|
||||
|
||||
Ok::<_, hyper::Error>(response)
|
||||
}
|
||||
}),
|
||||
)
|
||||
.with_upgrades()
|
||||
.await
|
||||
{
|
||||
tracing::debug!(
|
||||
parent: &span,
|
||||
event = "error",
|
||||
context = "http",
|
||||
reason = %http_err,
|
||||
);
|
||||
impl HttpSessionData {
|
||||
pub async fn resolve_url(&self, core: &Core) -> String {
|
||||
core.eval_if(&core.network.url, self)
|
||||
.await
|
||||
.unwrap_or_else(|| format!("http{}://localhost", if self.is_tls { "s" } else { "" }))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -21,17 +21,14 @@
|
|||
* for more details.
|
||||
*/
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
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::JMAP;
|
||||
use crate::JmapInstance;
|
||||
|
||||
pub mod admin;
|
||||
pub mod config;
|
||||
pub mod event_source;
|
||||
pub mod http;
|
||||
pub mod request;
|
||||
|
@ -39,11 +36,11 @@ pub mod session;
|
|||
|
||||
#[derive(Clone)]
|
||||
pub struct JmapSessionManager {
|
||||
pub inner: Arc<JMAP>,
|
||||
pub inner: JmapInstance,
|
||||
}
|
||||
|
||||
impl JmapSessionManager {
|
||||
pub fn new(inner: Arc<JMAP>) -> Self {
|
||||
pub fn new(inner: JmapInstance) -> Self {
|
||||
Self { inner }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@
|
|||
|
||||
use std::sync::Arc;
|
||||
|
||||
use common::listener::ServerInstance;
|
||||
use jmap_proto::{
|
||||
error::{method::MethodError, request::RequestError},
|
||||
method::{
|
||||
|
@ -33,7 +34,6 @@ use jmap_proto::{
|
|||
response::{Response, ResponseMethod},
|
||||
types::collection::Collection,
|
||||
};
|
||||
use utils::listener::ServerInstance;
|
||||
|
||||
use crate::{auth::AccessToken, JMAP};
|
||||
|
||||
|
@ -173,7 +173,7 @@ impl JMAP {
|
|||
self.vacation_response_get(req).await?.into()
|
||||
}
|
||||
get::RequestArguments::Principal => {
|
||||
if self.config.principal_allow_lookups || access_token.is_super_user() {
|
||||
if self.core.jmap.principal_allow_lookups || access_token.is_super_user() {
|
||||
self.principal_get(req).await?.into()
|
||||
} else {
|
||||
return Err(MethodError::Forbidden(
|
||||
|
@ -220,7 +220,7 @@ impl JMAP {
|
|||
self.sieve_script_query(req).await?.into()
|
||||
}
|
||||
query::RequestArguments::Principal => {
|
||||
if self.config.principal_allow_lookups || access_token.is_super_user() {
|
||||
if self.core.jmap.principal_allow_lookups || access_token.is_super_user() {
|
||||
self.principal_query(req).await?.into()
|
||||
} else {
|
||||
return Err(MethodError::Forbidden(
|
||||
|
|
|
@ -26,170 +26,19 @@ use std::sync::Arc;
|
|||
use directory::QueryBy;
|
||||
use jmap_proto::{
|
||||
error::request::RequestError,
|
||||
request::capability::Capability,
|
||||
response::serialize::serialize_hex,
|
||||
types::{acl::Acl, collection::Collection, id::Id, type_state::DataType},
|
||||
request::capability::{Capability, Session},
|
||||
types::{acl::Acl, collection::Collection, id::Id},
|
||||
};
|
||||
use store::ahash::AHashSet;
|
||||
use utils::{listener::ServerInstance, map::vec_map::VecMap, UnwrapFailure};
|
||||
|
||||
use crate::{auth::AccessToken, JMAP};
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
pub struct Session {
|
||||
#[serde(rename(serialize = "capabilities"))]
|
||||
capabilities: VecMap<Capability, Capabilities>,
|
||||
#[serde(rename(serialize = "accounts"))]
|
||||
accounts: VecMap<Id, Account>,
|
||||
#[serde(rename(serialize = "primaryAccounts"))]
|
||||
primary_accounts: VecMap<Capability, Id>,
|
||||
#[serde(rename(serialize = "username"))]
|
||||
username: String,
|
||||
#[serde(rename(serialize = "apiUrl"))]
|
||||
api_url: String,
|
||||
#[serde(rename(serialize = "downloadUrl"))]
|
||||
download_url: String,
|
||||
#[serde(rename(serialize = "uploadUrl"))]
|
||||
upload_url: String,
|
||||
#[serde(rename(serialize = "eventSourceUrl"))]
|
||||
event_source_url: String,
|
||||
#[serde(rename(serialize = "state"))]
|
||||
#[serde(serialize_with = "serialize_hex")]
|
||||
state: u32,
|
||||
#[serde(skip)]
|
||||
base_url: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
struct Account {
|
||||
#[serde(rename(serialize = "name"))]
|
||||
name: String,
|
||||
#[serde(rename(serialize = "isPersonal"))]
|
||||
is_personal: bool,
|
||||
#[serde(rename(serialize = "isReadOnly"))]
|
||||
is_read_only: bool,
|
||||
#[serde(rename(serialize = "accountCapabilities"))]
|
||||
account_capabilities: VecMap<Capability, Capabilities>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
#[serde(untagged)]
|
||||
#[allow(dead_code)]
|
||||
pub enum Capabilities {
|
||||
Core(CoreCapabilities),
|
||||
Mail(MailCapabilities),
|
||||
Submission(SubmissionCapabilities),
|
||||
WebSocket(WebSocketCapabilities),
|
||||
SieveAccount(SieveAccountCapabilities),
|
||||
SieveSession(SieveSessionCapabilities),
|
||||
Blob(BlobCapabilities),
|
||||
Empty(EmptyCapabilities),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
pub struct CoreCapabilities {
|
||||
#[serde(rename(serialize = "maxSizeUpload"))]
|
||||
max_size_upload: usize,
|
||||
#[serde(rename(serialize = "maxConcurrentUpload"))]
|
||||
max_concurrent_upload: usize,
|
||||
#[serde(rename(serialize = "maxSizeRequest"))]
|
||||
max_size_request: usize,
|
||||
#[serde(rename(serialize = "maxConcurrentRequests"))]
|
||||
max_concurrent_requests: usize,
|
||||
#[serde(rename(serialize = "maxCallsInRequest"))]
|
||||
max_calls_in_request: usize,
|
||||
#[serde(rename(serialize = "maxObjectsInGet"))]
|
||||
max_objects_in_get: usize,
|
||||
#[serde(rename(serialize = "maxObjectsInSet"))]
|
||||
max_objects_in_set: usize,
|
||||
#[serde(rename(serialize = "collationAlgorithms"))]
|
||||
collation_algorithms: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
pub struct WebSocketCapabilities {
|
||||
#[serde(rename(serialize = "url"))]
|
||||
url: String,
|
||||
#[serde(rename(serialize = "supportsPush"))]
|
||||
supports_push: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
pub struct SieveSessionCapabilities {
|
||||
#[serde(rename(serialize = "implementation"))]
|
||||
pub implementation: &'static str,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
pub struct SieveAccountCapabilities {
|
||||
#[serde(rename(serialize = "maxSizeScriptName"))]
|
||||
pub max_script_name: usize,
|
||||
#[serde(rename(serialize = "maxSizeScript"))]
|
||||
pub max_script_size: usize,
|
||||
#[serde(rename(serialize = "maxNumberScripts"))]
|
||||
pub max_scripts: usize,
|
||||
#[serde(rename(serialize = "maxNumberRedirects"))]
|
||||
pub max_redirects: usize,
|
||||
#[serde(rename(serialize = "sieveExtensions"))]
|
||||
pub extensions: Vec<String>,
|
||||
#[serde(rename(serialize = "notificationMethods"))]
|
||||
pub notification_methods: Option<Vec<String>>,
|
||||
#[serde(rename(serialize = "externalLists"))]
|
||||
pub ext_lists: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
pub struct MailCapabilities {
|
||||
#[serde(rename(serialize = "maxMailboxesPerEmail"))]
|
||||
max_mailboxes_per_email: Option<usize>,
|
||||
#[serde(rename(serialize = "maxMailboxDepth"))]
|
||||
max_mailbox_depth: usize,
|
||||
#[serde(rename(serialize = "maxSizeMailboxName"))]
|
||||
max_size_mailbox_name: usize,
|
||||
#[serde(rename(serialize = "maxSizeAttachmentsPerEmail"))]
|
||||
max_size_attachments_per_email: usize,
|
||||
#[serde(rename(serialize = "emailQuerySortOptions"))]
|
||||
email_query_sort_options: Vec<String>,
|
||||
#[serde(rename(serialize = "mayCreateTopLevelMailbox"))]
|
||||
may_create_top_level_mailbox: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
pub struct SubmissionCapabilities {
|
||||
#[serde(rename(serialize = "maxDelayedSend"))]
|
||||
max_delayed_send: usize,
|
||||
#[serde(rename(serialize = "submissionExtensions"))]
|
||||
submission_extensions: VecMap<String, Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
pub struct BlobCapabilities {
|
||||
#[serde(rename(serialize = "maxSizeBlobSet"))]
|
||||
max_size_blob_set: usize,
|
||||
#[serde(rename(serialize = "maxDataSources"))]
|
||||
max_data_sources: usize,
|
||||
#[serde(rename(serialize = "supportedTypeNames"))]
|
||||
supported_type_names: Vec<DataType>,
|
||||
#[serde(rename(serialize = "supportedDigestAlgorithms"))]
|
||||
supported_digest_algorithms: Vec<&'static str>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, serde::Serialize)]
|
||||
pub struct EmptyCapabilities {}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct BaseCapabilities {
|
||||
pub session: VecMap<Capability, Capabilities>,
|
||||
pub account: VecMap<Capability, Capabilities>,
|
||||
}
|
||||
|
||||
impl JMAP {
|
||||
pub async fn handle_session_resource(
|
||||
&self,
|
||||
instance: Arc<ServerInstance>,
|
||||
base_url: String,
|
||||
access_token: Arc<AccessToken>,
|
||||
) -> Result<Session, RequestError> {
|
||||
let mut session = Session::new(&instance.data, &self.config.capabilities);
|
||||
let mut session = Session::new(base_url, &self.core.jmap.capabilities);
|
||||
session.set_state(access_token.state());
|
||||
session.set_primary_account(
|
||||
access_token.primary_id().into(),
|
||||
|
@ -199,7 +48,7 @@ impl JMAP {
|
|||
.clone()
|
||||
.unwrap_or_else(|| access_token.name.clone()),
|
||||
None,
|
||||
&self.config.capabilities.account,
|
||||
&self.core.jmap.capabilities.account,
|
||||
);
|
||||
|
||||
// Add secondary accounts
|
||||
|
@ -213,7 +62,9 @@ impl JMAP {
|
|||
|
||||
session.add_account(
|
||||
(*id).into(),
|
||||
self.directory
|
||||
self.core
|
||||
.storage
|
||||
.directory
|
||||
.query(QueryBy::Id(*id), false)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
|
@ -222,320 +73,10 @@ impl JMAP {
|
|||
is_personal,
|
||||
is_readonly,
|
||||
Some(&[Capability::Mail, Capability::Quota, Capability::Blob]),
|
||||
&self.config.capabilities.account,
|
||||
&self.core.jmap.capabilities.account,
|
||||
);
|
||||
}
|
||||
|
||||
Ok(session)
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::Config {
|
||||
pub fn add_capabilites(&mut self, settings: &utils::config::Config) {
|
||||
// Add core capabilities
|
||||
self.capabilities.session.append(
|
||||
Capability::Core,
|
||||
Capabilities::Core(CoreCapabilities::new(self)),
|
||||
);
|
||||
|
||||
// Add email capabilities
|
||||
self.capabilities.session.append(
|
||||
Capability::Mail,
|
||||
Capabilities::Empty(EmptyCapabilities::default()),
|
||||
);
|
||||
self.capabilities.account.append(
|
||||
Capability::Mail,
|
||||
Capabilities::Mail(MailCapabilities::new(self)),
|
||||
);
|
||||
|
||||
// Add submission capabilities
|
||||
self.capabilities.session.append(
|
||||
Capability::Submission,
|
||||
Capabilities::Empty(EmptyCapabilities::default()),
|
||||
);
|
||||
self.capabilities.account.append(
|
||||
Capability::Submission,
|
||||
Capabilities::Submission(SubmissionCapabilities {
|
||||
max_delayed_send: 86400 * 30,
|
||||
submission_extensions: VecMap::from_iter([
|
||||
("FUTURERELEASE".to_string(), Vec::new()),
|
||||
("SIZE".to_string(), Vec::new()),
|
||||
("DSN".to_string(), Vec::new()),
|
||||
("DELIVERYBY".to_string(), Vec::new()),
|
||||
("MT-PRIORITY".to_string(), vec!["MIXER".to_string()]),
|
||||
("REQUIRETLS".to_string(), vec![]),
|
||||
]),
|
||||
}),
|
||||
);
|
||||
|
||||
// Add vacation response capabilities
|
||||
self.capabilities.session.append(
|
||||
Capability::VacationResponse,
|
||||
Capabilities::Empty(EmptyCapabilities::default()),
|
||||
);
|
||||
self.capabilities.account.append(
|
||||
Capability::VacationResponse,
|
||||
Capabilities::Empty(EmptyCapabilities::default()),
|
||||
);
|
||||
|
||||
// Add Sieve capabilities
|
||||
self.capabilities.session.append(
|
||||
Capability::Sieve,
|
||||
Capabilities::SieveSession(SieveSessionCapabilities::default()),
|
||||
);
|
||||
self.capabilities.account.append(
|
||||
Capability::Sieve,
|
||||
Capabilities::SieveAccount(SieveAccountCapabilities::new(self, settings)),
|
||||
);
|
||||
|
||||
// Add Blob capabilities
|
||||
self.capabilities.session.append(
|
||||
Capability::Blob,
|
||||
Capabilities::Empty(EmptyCapabilities::default()),
|
||||
);
|
||||
self.capabilities.account.append(
|
||||
Capability::Blob,
|
||||
Capabilities::Blob(BlobCapabilities::new(self)),
|
||||
);
|
||||
|
||||
// Add Quota capabilities
|
||||
self.capabilities.session.append(
|
||||
Capability::Quota,
|
||||
Capabilities::Empty(EmptyCapabilities::default()),
|
||||
);
|
||||
self.capabilities.account.append(
|
||||
Capability::Quota,
|
||||
Capabilities::Empty(EmptyCapabilities::default()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl Session {
|
||||
pub fn new(base_url: &str, base_capabilities: &BaseCapabilities) -> Session {
|
||||
let mut capabilities = base_capabilities.session.clone();
|
||||
capabilities.append(
|
||||
Capability::WebSocket,
|
||||
Capabilities::WebSocket(WebSocketCapabilities::new(base_url)),
|
||||
);
|
||||
|
||||
Session {
|
||||
capabilities,
|
||||
accounts: VecMap::new(),
|
||||
primary_accounts: VecMap::new(),
|
||||
username: "".to_string(),
|
||||
api_url: format!("{}/jmap/", base_url),
|
||||
download_url: format!(
|
||||
"{}/jmap/download/{{accountId}}/{{blobId}}/{{name}}?accept={{type}}",
|
||||
base_url
|
||||
),
|
||||
upload_url: format!("{}/jmap/upload/{{accountId}}/", base_url),
|
||||
event_source_url: format!(
|
||||
"{}/jmap/eventsource/?types={{types}}&closeafter={{closeafter}}&ping={{ping}}",
|
||||
base_url
|
||||
),
|
||||
base_url: base_url.to_string(),
|
||||
state: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_primary_account(
|
||||
&mut self,
|
||||
account_id: Id,
|
||||
username: String,
|
||||
name: String,
|
||||
capabilities: Option<&[Capability]>,
|
||||
account_capabilities: &VecMap<Capability, Capabilities>,
|
||||
) {
|
||||
self.username = username;
|
||||
|
||||
if let Some(capabilities) = capabilities {
|
||||
for capability in capabilities {
|
||||
self.primary_accounts.append(*capability, account_id);
|
||||
}
|
||||
} else {
|
||||
for capability in self.capabilities.keys() {
|
||||
self.primary_accounts.append(*capability, account_id);
|
||||
}
|
||||
}
|
||||
|
||||
self.accounts.set(
|
||||
account_id,
|
||||
Account::new(name, true, false).add_capabilities(capabilities, account_capabilities),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn add_account(
|
||||
&mut self,
|
||||
account_id: Id,
|
||||
name: String,
|
||||
is_personal: bool,
|
||||
is_read_only: bool,
|
||||
capabilities: Option<&[Capability]>,
|
||||
account_capabilities: &VecMap<Capability, Capabilities>,
|
||||
) {
|
||||
self.accounts.set(
|
||||
account_id,
|
||||
Account::new(name, is_personal, is_read_only)
|
||||
.add_capabilities(capabilities, account_capabilities),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn set_state(&mut self, state: u32) {
|
||||
self.state = state;
|
||||
}
|
||||
|
||||
pub fn api_url(&self) -> &str {
|
||||
&self.api_url
|
||||
}
|
||||
|
||||
pub fn base_url(&self) -> &str {
|
||||
&self.base_url
|
||||
}
|
||||
}
|
||||
|
||||
impl Account {
|
||||
pub fn new(name: String, is_personal: bool, is_read_only: bool) -> Account {
|
||||
Account {
|
||||
name,
|
||||
is_personal,
|
||||
is_read_only,
|
||||
account_capabilities: VecMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_capabilities(
|
||||
mut self,
|
||||
capabilities: Option<&[Capability]>,
|
||||
account_capabilities: &VecMap<Capability, Capabilities>,
|
||||
) -> Account {
|
||||
if let Some(capabilities) = capabilities {
|
||||
for capability in capabilities {
|
||||
if let Some(value) = account_capabilities.get(capability) {
|
||||
self.account_capabilities.append(*capability, value.clone());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.account_capabilities = account_capabilities.clone();
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl CoreCapabilities {
|
||||
pub fn new(config: &crate::Config) -> Self {
|
||||
CoreCapabilities {
|
||||
max_size_upload: config.upload_max_size,
|
||||
max_concurrent_upload: config.upload_max_concurrent as usize,
|
||||
max_size_request: config.request_max_size,
|
||||
max_concurrent_requests: config.request_max_concurrent as usize,
|
||||
max_calls_in_request: config.request_max_calls,
|
||||
max_objects_in_get: config.get_max_objects,
|
||||
max_objects_in_set: config.set_max_objects,
|
||||
collation_algorithms: vec![
|
||||
"i;ascii-numeric".to_string(),
|
||||
"i;ascii-casemap".to_string(),
|
||||
"i;unicode-casemap".to_string(),
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WebSocketCapabilities {
|
||||
pub fn new(base_url: &str) -> Self {
|
||||
WebSocketCapabilities {
|
||||
url: format!("ws{}/jmap/ws", base_url.strip_prefix("http").unwrap()),
|
||||
supports_push: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SieveAccountCapabilities {
|
||||
pub fn new(config: &crate::Config, settings: &utils::config::Config) -> Self {
|
||||
let mut notification_methods = Vec::new();
|
||||
|
||||
for (_, uri) in settings.values("sieve.untrusted.notification-uris") {
|
||||
notification_methods.push(uri.to_string());
|
||||
}
|
||||
if notification_methods.is_empty() {
|
||||
notification_methods.push("mailto".to_string());
|
||||
}
|
||||
|
||||
let mut capabilities: AHashSet<sieve::compiler::grammar::Capability> =
|
||||
AHashSet::from_iter(sieve::compiler::grammar::Capability::all().iter().cloned());
|
||||
|
||||
for (_, capability) in settings.values("sieve.untrusted.disabled-capabilities") {
|
||||
capabilities.remove(&sieve::compiler::grammar::Capability::parse(capability));
|
||||
}
|
||||
|
||||
let mut extensions = capabilities
|
||||
.into_iter()
|
||||
.map(|c| c.to_string())
|
||||
.collect::<Vec<String>>();
|
||||
extensions.sort_unstable();
|
||||
|
||||
SieveAccountCapabilities {
|
||||
max_script_name: config.sieve_max_script_name,
|
||||
max_script_size: settings
|
||||
.property("sieve.untrusted.max-script-size")
|
||||
.failed("Invalid configuration file")
|
||||
.unwrap_or(1024 * 1024),
|
||||
max_scripts: config.sieve_max_scripts,
|
||||
max_redirects: settings
|
||||
.property("sieve.untrusted.max-redirects")
|
||||
.failed("Invalid configuration file")
|
||||
.unwrap_or(1),
|
||||
extensions,
|
||||
notification_methods: if !notification_methods.is_empty() {
|
||||
notification_methods.into()
|
||||
} else {
|
||||
None
|
||||
},
|
||||
ext_lists: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SieveSessionCapabilities {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
implementation: concat!("Stalwart JMAP v", env!("CARGO_PKG_VERSION"),),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MailCapabilities {
|
||||
pub fn new(config: &crate::Config) -> Self {
|
||||
MailCapabilities {
|
||||
max_mailboxes_per_email: None,
|
||||
max_mailbox_depth: config.mailbox_max_depth,
|
||||
max_size_mailbox_name: config.mailbox_name_max_len,
|
||||
max_size_attachments_per_email: config.mail_attachments_max_size,
|
||||
email_query_sort_options: [
|
||||
"receivedAt",
|
||||
"size",
|
||||
"from",
|
||||
"to",
|
||||
"subject",
|
||||
"sentAt",
|
||||
"hasKeyword",
|
||||
"allInThreadHaveKeyword",
|
||||
"someInThreadHaveKeyword",
|
||||
]
|
||||
.iter()
|
||||
.map(|s| s.to_string())
|
||||
.collect(),
|
||||
may_create_top_level_mailbox: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BlobCapabilities {
|
||||
pub fn new(config: &crate::Config) -> Self {
|
||||
BlobCapabilities {
|
||||
max_size_blob_set: (config.request_max_size * 3 / 4) - 512,
|
||||
max_data_sources: config.request_max_calls,
|
||||
supported_type_names: vec![DataType::Email, DataType::Thread, DataType::SieveScript],
|
||||
supported_digest_algorithms: vec!["sha", "sha-256", "sha-512"],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -51,7 +51,9 @@ impl JMAP {
|
|||
.chain(access_token.member_of.clone().iter())
|
||||
{
|
||||
for acl_item in self
|
||||
.store
|
||||
.core
|
||||
.storage
|
||||
.data
|
||||
.acl_query(AclQuery::HasAccess { grant_account_id })
|
||||
.await
|
||||
.map_err(|err| {
|
||||
|
@ -120,7 +122,9 @@ impl JMAP {
|
|||
.chain(access_token.member_of.clone().iter())
|
||||
{
|
||||
for acl_item in self
|
||||
.store
|
||||
.core
|
||||
.storage
|
||||
.data
|
||||
.acl_query(AclQuery::SharedWith {
|
||||
grant_account_id,
|
||||
to_account_id,
|
||||
|
@ -233,7 +237,9 @@ impl JMAP {
|
|||
.chain(access_token.member_of.clone().iter())
|
||||
{
|
||||
match self
|
||||
.store
|
||||
.core
|
||||
.storage
|
||||
.data
|
||||
.get_value::<u64>(ValueKey {
|
||||
account_id: to_account_id,
|
||||
collection: to_collection,
|
||||
|
@ -350,6 +356,8 @@ impl JMAP {
|
|||
let mut acl_obj = Object::with_capacity(value.len() / 2);
|
||||
for item in value {
|
||||
if let Some(principal) = self
|
||||
.core
|
||||
.storage
|
||||
.directory
|
||||
.query(QueryBy::Id(item.account_id), false)
|
||||
.await
|
||||
|
@ -376,7 +384,7 @@ impl JMAP {
|
|||
current: &Option<HashedValue<Object<Value>>>,
|
||||
) {
|
||||
if let Value::Acl(acl_changes) = changes.get(&Property::Acl) {
|
||||
let access_tokens = &self.access_tokens;
|
||||
let access_tokens = &self.inner.access_tokens;
|
||||
if let Some(Value::Acl(acl_current)) = current
|
||||
.as_ref()
|
||||
.and_then(|current| current.inner.properties.get(&Property::Acl))
|
||||
|
@ -419,6 +427,8 @@ impl JMAP {
|
|||
for item in acl_set.chunks_exact(2) {
|
||||
if let (Value::Text(account_name), Value::UnsignedInt(grants)) = (&item[0], &item[1]) {
|
||||
match self
|
||||
.core
|
||||
.storage
|
||||
.directory
|
||||
.query(QueryBy::Name(account_name), false)
|
||||
.await
|
||||
|
@ -458,6 +468,8 @@ impl JMAP {
|
|||
(&acl_patch[0], &acl_patch[1])
|
||||
{
|
||||
match self
|
||||
.core
|
||||
.storage
|
||||
.directory
|
||||
.query(QueryBy::Name(account_name), false)
|
||||
.await
|
||||
|
|
|
@ -23,12 +23,13 @@
|
|||
|
||||
use std::{net::IpAddr, sync::Arc, time::Instant};
|
||||
|
||||
use common::{listener::limiter::InFlight, AuthResult};
|
||||
use directory::QueryBy;
|
||||
use hyper::header;
|
||||
use jmap_proto::error::request::RequestError;
|
||||
use mail_parser::decoders::base64::base64_decode;
|
||||
use mail_send::Credentials;
|
||||
use utils::{listener::limiter::InFlight, map::ttl_dashmap::TtlMap};
|
||||
use utils::map::ttl_dashmap::TtlMap;
|
||||
|
||||
use crate::JMAP;
|
||||
|
||||
|
@ -46,13 +47,12 @@ impl JMAP {
|
|||
.and_then(|h| h.to_str().ok())
|
||||
.and_then(|h| h.split_once(' ').map(|(l, t)| (l, t.trim().to_string())))
|
||||
{
|
||||
let session = if let Some(account_id) = self.sessions.get_with_ttl(&token) {
|
||||
let session = if let Some(account_id) = self.inner.sessions.get_with_ttl(&token) {
|
||||
self.get_cached_access_token(account_id).await
|
||||
} else {
|
||||
let addr = self.build_remote_addr(req, remote_ip);
|
||||
if mechanism.eq_ignore_ascii_case("basic") {
|
||||
// Enforce rate limit for authentication requests
|
||||
self.is_auth_allowed_soft(&addr).await?;
|
||||
self.is_auth_allowed_soft(&remote_ip).await?;
|
||||
|
||||
// Decode the base64 encoded credentials
|
||||
if let Some((account, secret)) = base64_decode(token.as_bytes())
|
||||
|
@ -64,7 +64,7 @@ impl JMAP {
|
|||
})
|
||||
{
|
||||
if let AuthResult::Success(access_token) =
|
||||
self.authenticate_plain(&account, &secret, addr).await
|
||||
self.authenticate_plain(&account, &secret, remote_ip).await
|
||||
{
|
||||
Some(access_token)
|
||||
} else {
|
||||
|
@ -80,7 +80,7 @@ impl JMAP {
|
|||
}
|
||||
} else if mechanism.eq_ignore_ascii_case("bearer") {
|
||||
// Enforce anonymous rate limit for bearer auth requests
|
||||
self.is_anonymous_allowed(&addr).await?;
|
||||
self.is_anonymous_allowed(&remote_ip).await?;
|
||||
|
||||
match self.validate_access_token("access_token", &token).await {
|
||||
Ok((account_id, _, _)) => self.get_access_token(account_id).await,
|
||||
|
@ -95,7 +95,7 @@ impl JMAP {
|
|||
}
|
||||
} else {
|
||||
// Enforce anonymous rate limit
|
||||
self.is_anonymous_allowed(&addr).await?;
|
||||
self.is_anonymous_allowed(&remote_ip).await?;
|
||||
None
|
||||
}
|
||||
.map(|access_token| {
|
||||
|
@ -114,31 +114,30 @@ impl JMAP {
|
|||
}
|
||||
} else {
|
||||
// Enforce anonymous rate limit
|
||||
self.is_anonymous_allowed(&self.build_remote_addr(req, remote_ip))
|
||||
.await?;
|
||||
self.is_anonymous_allowed(&remote_ip).await?;
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cache_session(&self, session_id: String, access_token: &AccessToken) {
|
||||
self.sessions.insert_with_ttl(
|
||||
self.inner.sessions.insert_with_ttl(
|
||||
session_id,
|
||||
access_token.primary_id(),
|
||||
Instant::now() + self.config.session_cache_ttl,
|
||||
Instant::now() + self.core.jmap.session_cache_ttl,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn cache_access_token(&self, access_token: Arc<AccessToken>) {
|
||||
self.access_tokens.insert_with_ttl(
|
||||
self.inner.access_tokens.insert_with_ttl(
|
||||
access_token.primary_id(),
|
||||
access_token,
|
||||
Instant::now() + self.config.session_cache_ttl,
|
||||
Instant::now() + self.core.jmap.session_cache_ttl,
|
||||
);
|
||||
}
|
||||
|
||||
pub async fn get_cached_access_token(&self, primary_id: u32) -> Option<Arc<AccessToken>> {
|
||||
if let Some(access_token) = self.access_tokens.get_with_ttl(&primary_id) {
|
||||
if let Some(access_token) = self.inner.access_tokens.get_with_ttl(&primary_id) {
|
||||
access_token.into()
|
||||
} else {
|
||||
// Refresh ACL token
|
||||
|
@ -150,27 +149,6 @@ impl JMAP {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn build_remote_addr(
|
||||
&self,
|
||||
req: &hyper::Request<hyper::body::Incoming>,
|
||||
remote_ip: IpAddr,
|
||||
) -> IpAddr {
|
||||
if !self.config.rate_use_forwarded {
|
||||
remote_ip
|
||||
} else if let Some(forwarded_for) = req
|
||||
.headers()
|
||||
.get(header::FORWARDED)
|
||||
.or_else(|| req.headers().get("X-Forwarded-For"))
|
||||
.and_then(|h| h.to_str().ok())
|
||||
.and_then(|h| h.parse::<IpAddr>().ok())
|
||||
{
|
||||
forwarded_for
|
||||
} else {
|
||||
tracing::warn!("Warning: No remote address found in request, using remote ip.");
|
||||
remote_ip
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn authenticate_plain(
|
||||
&self,
|
||||
username: &str,
|
||||
|
@ -178,8 +156,9 @@ impl JMAP {
|
|||
remote_ip: IpAddr,
|
||||
) -> AuthResult<AccessToken> {
|
||||
match self
|
||||
.directory
|
||||
.core
|
||||
.authenticate(
|
||||
&self.core.storage.directory,
|
||||
&Credentials::Plain {
|
||||
username: username.to_string(),
|
||||
secret: secret.to_string(),
|
||||
|
@ -202,7 +181,9 @@ impl JMAP {
|
|||
pub async fn get_access_token(&self, account_id: u32) -> Option<AccessToken> {
|
||||
// Create access token
|
||||
self.update_access_token(AccessToken::new(
|
||||
self.directory
|
||||
self.core
|
||||
.storage
|
||||
.directory
|
||||
.query(QueryBy::Id(account_id), true)
|
||||
.await
|
||||
.ok()??,
|
||||
|
|
|
@ -27,12 +27,13 @@ use std::{
|
|||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use common::AuthResult;
|
||||
use hyper::StatusCode;
|
||||
use store::rand::{
|
||||
distributions::{Alphanumeric, Standard},
|
||||
thread_rng, Rng,
|
||||
};
|
||||
use utils::{listener::ServerInstance, map::ttl_dashmap::TtlMap};
|
||||
use utils::map::ttl_dashmap::TtlMap;
|
||||
|
||||
use crate::{
|
||||
api::{http::ToHttpResponse, HtmlResponse, HttpRequest, HttpResponse, JsonResponse},
|
||||
|
@ -54,7 +55,7 @@ impl JMAP {
|
|||
pub async fn handle_device_auth(
|
||||
&self,
|
||||
req: &mut HttpRequest,
|
||||
instance: Arc<ServerInstance>,
|
||||
base_url: impl AsRef<str>,
|
||||
) -> HttpResponse {
|
||||
// Parse form
|
||||
let client_id = match FormData::from_request(req, MAX_POST_LEN)
|
||||
|
@ -100,19 +101,22 @@ impl JMAP {
|
|||
client_id,
|
||||
redirect_uri: None,
|
||||
});
|
||||
let expiry = Instant::now() + Duration::from_secs(self.config.oauth_expiry_user_code);
|
||||
self.oauth_codes
|
||||
let expiry = Instant::now() + Duration::from_secs(self.core.jmap.oauth_expiry_user_code);
|
||||
self.inner
|
||||
.oauth_codes
|
||||
.insert_with_ttl(device_code.clone(), oauth_code.clone(), expiry);
|
||||
self.oauth_codes
|
||||
self.inner
|
||||
.oauth_codes
|
||||
.insert_with_ttl(user_code.clone(), oauth_code, expiry);
|
||||
|
||||
// Build response
|
||||
let base_url = base_url.as_ref();
|
||||
JsonResponse::new(DeviceAuthResponse {
|
||||
verification_uri: format!("{}/auth", instance.data),
|
||||
verification_uri_complete: format!("{}/auth/?code={}", instance.data, user_code),
|
||||
verification_uri: format!("{}/auth", base_url),
|
||||
verification_uri_complete: format!("{}/auth/?code={}", base_url, user_code),
|
||||
device_code,
|
||||
user_code,
|
||||
expires_in: self.config.oauth_expiry_user_code,
|
||||
expires_in: self.core.jmap.oauth_expiry_user_code,
|
||||
interval: 5,
|
||||
})
|
||||
.into_http_response()
|
||||
|
@ -168,9 +172,9 @@ impl JMAP {
|
|||
|
||||
let code = if let Some(oauth) = fields
|
||||
.get("code")
|
||||
.and_then(|code| self.oauth_codes.get_with_ttl(code))
|
||||
.and_then(|code| self.inner.oauth_codes.get_with_ttl(code))
|
||||
{
|
||||
if (STATUS_PENDING..STATUS_PENDING + self.config.oauth_max_auth_attempts)
|
||||
if (STATUS_PENDING..STATUS_PENDING + self.core.jmap.oauth_max_auth_attempts)
|
||||
.contains(&oauth.status.load(atomic::Ordering::Relaxed))
|
||||
{
|
||||
if let (Some(email), Some(password)) = (fields.get("email"), fields.get("password"))
|
||||
|
|
|
@ -188,9 +188,10 @@ pub struct OAuthMetadata {
|
|||
}
|
||||
|
||||
impl OAuthMetadata {
|
||||
pub fn new(base_url: &str) -> Self {
|
||||
pub fn new(base_url: impl AsRef<str>) -> Self {
|
||||
let base_url = base_url.as_ref();
|
||||
OAuthMetadata {
|
||||
issuer: base_url.to_string(),
|
||||
issuer: base_url.into(),
|
||||
authorization_endpoint: format!("{}/auth/code", base_url),
|
||||
token_endpoint: format!("{}/auth/token", base_url),
|
||||
grant_types_supported: vec![
|
||||
|
|
|
@ -65,7 +65,7 @@ impl JMAP {
|
|||
params.get("client_id"),
|
||||
params.get("redirect_uri"),
|
||||
) {
|
||||
if let Some(oauth) = self.oauth_codes.get_with_ttl(code) {
|
||||
if let Some(oauth) = self.inner.oauth_codes.get_with_ttl(code) {
|
||||
if client_id != oauth.client_id
|
||||
|| redirect_uri != oauth.redirect_uri.as_deref().unwrap_or("")
|
||||
{
|
||||
|
@ -103,7 +103,7 @@ impl JMAP {
|
|||
if let (Some(oauth), Some(client_id)) = (
|
||||
params
|
||||
.get("device_code")
|
||||
.and_then(|dc| self.oauth_codes.get_with_ttl(dc)),
|
||||
.and_then(|dc| self.inner.oauth_codes.get_with_ttl(dc)),
|
||||
params.get("client_id"),
|
||||
) {
|
||||
response = if oauth.client_id != client_id {
|
||||
|
@ -131,7 +131,7 @@ impl JMAP {
|
|||
}
|
||||
status
|
||||
if (STATUS_PENDING
|
||||
..STATUS_PENDING + self.config.oauth_max_auth_attempts)
|
||||
..STATUS_PENDING + self.core.jmap.oauth_max_auth_attempts)
|
||||
.contains(&status) =>
|
||||
{
|
||||
TokenResponse::error(ErrorType::AuthorizationPending)
|
||||
|
@ -152,7 +152,7 @@ impl JMAP {
|
|||
.issue_token(
|
||||
account_id,
|
||||
&client_id,
|
||||
time_left <= self.config.oauth_expiry_refresh_token_renew,
|
||||
time_left <= self.core.jmap.oauth_expiry_refresh_token_renew,
|
||||
)
|
||||
.await
|
||||
.map(TokenResponse::Granted)
|
||||
|
@ -184,6 +184,8 @@ impl JMAP {
|
|||
with_refresh_token: bool,
|
||||
) -> Result<OAuthResponse, &'static str> {
|
||||
let password_hash = self
|
||||
.core
|
||||
.storage
|
||||
.directory
|
||||
.query(QueryBy::Id(account_id), false)
|
||||
.await
|
||||
|
@ -200,17 +202,17 @@ impl JMAP {
|
|||
account_id,
|
||||
&password_hash,
|
||||
client_id,
|
||||
self.config.oauth_expiry_token,
|
||||
self.core.jmap.oauth_expiry_token,
|
||||
)?,
|
||||
token_type: "bearer".to_string(),
|
||||
expires_in: self.config.oauth_expiry_token,
|
||||
expires_in: self.core.jmap.oauth_expiry_token,
|
||||
refresh_token: if with_refresh_token {
|
||||
self.encode_access_token(
|
||||
"refresh_token",
|
||||
account_id,
|
||||
&password_hash,
|
||||
client_id,
|
||||
self.config.oauth_expiry_refresh_token,
|
||||
self.core.jmap.oauth_expiry_refresh_token,
|
||||
)?
|
||||
.into()
|
||||
} else {
|
||||
|
@ -232,7 +234,7 @@ impl JMAP {
|
|||
if client_id.len() > CLIENT_ID_MAX_LEN {
|
||||
return Err("ClientId is too long");
|
||||
}
|
||||
let key = self.config.oauth_key.clone();
|
||||
let key = self.core.jmap.oauth_key.clone();
|
||||
let context = format!(
|
||||
"{} {} {} {}",
|
||||
grant_type, client_id, account_id, password_hash
|
||||
|
@ -302,6 +304,8 @@ impl JMAP {
|
|||
|
||||
// Obtain password hash
|
||||
let password_hash = self
|
||||
.core
|
||||
.storage
|
||||
.directory
|
||||
.query(QueryBy::Id(account_id), false)
|
||||
.await
|
||||
|
@ -313,7 +317,7 @@ impl JMAP {
|
|||
.ok_or("Failed to obtain password hash")?;
|
||||
|
||||
// Build context
|
||||
let key = self.config.oauth_key.clone();
|
||||
let key = self.core.jmap.oauth_key.clone();
|
||||
let context = format!(
|
||||
"{} {} {} {}",
|
||||
grant_type, client_id, account_id, password_hash
|
||||
|
|
|
@ -28,6 +28,7 @@ use std::{
|
|||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use common::AuthResult;
|
||||
use http_body_util::{BodyExt, Full};
|
||||
use hyper::{body::Bytes, header, StatusCode};
|
||||
use mail_builder::encoders::base64::base64_encode;
|
||||
|
@ -122,7 +123,7 @@ impl JMAP {
|
|||
.collect::<String>();
|
||||
|
||||
// Add client code
|
||||
self.oauth_codes.insert_with_ttl(
|
||||
self.inner.oauth_codes.insert_with_ttl(
|
||||
client_code.clone(),
|
||||
Arc::new(OAuthCode {
|
||||
status: STATUS_AUTHORIZED.into(),
|
||||
|
@ -130,7 +131,7 @@ impl JMAP {
|
|||
client_id,
|
||||
redirect_uri,
|
||||
}),
|
||||
Instant::now() + Duration::from_secs(self.config.oauth_expiry_auth_code),
|
||||
Instant::now() + Duration::from_secs(self.core.jmap.oauth_expiry_auth_code),
|
||||
);
|
||||
client_code
|
||||
}
|
||||
|
@ -205,7 +206,7 @@ impl JMAP {
|
|||
let _ = write!(redirect_link, "&state={}", state);
|
||||
}
|
||||
|
||||
if auth_code.is_none() && (auth_attempts < self.config.oauth_max_auth_attempts) {
|
||||
if auth_code.is_none() && (auth_attempts < self.core.jmap.oauth_max_auth_attempts) {
|
||||
let code = String::from_utf8(
|
||||
base64_encode(
|
||||
&bincode::serialize(&(auth_attempts + 1, code_req)).unwrap_or_default(),
|
||||
|
|
|
@ -23,8 +23,8 @@
|
|||
|
||||
use std::{net::IpAddr, sync::Arc};
|
||||
|
||||
use common::listener::limiter::{ConcurrencyLimiter, InFlight};
|
||||
use jmap_proto::error::request::{RequestError, RequestLimitError};
|
||||
use utils::listener::limiter::{ConcurrencyLimiter, InFlight};
|
||||
|
||||
use crate::JMAP;
|
||||
|
||||
|
@ -37,17 +37,22 @@ pub struct ConcurrencyLimiters {
|
|||
|
||||
impl JMAP {
|
||||
pub fn get_concurrency_limiter(&self, account_id: u32) -> Arc<ConcurrencyLimiters> {
|
||||
self.concurrency_limiter
|
||||
self.inner
|
||||
.concurrency_limiter
|
||||
.get(&account_id)
|
||||
.map(|limiter| limiter.clone())
|
||||
.unwrap_or_else(|| {
|
||||
let limiter = Arc::new(ConcurrencyLimiters {
|
||||
concurrent_requests: ConcurrencyLimiter::new(
|
||||
self.config.request_max_concurrent,
|
||||
self.core.jmap.request_max_concurrent,
|
||||
),
|
||||
concurrent_uploads: ConcurrencyLimiter::new(
|
||||
self.core.jmap.upload_max_concurrent,
|
||||
),
|
||||
concurrent_uploads: ConcurrencyLimiter::new(self.config.upload_max_concurrent),
|
||||
});
|
||||
self.concurrency_limiter.insert(account_id, limiter.clone());
|
||||
self.inner
|
||||
.concurrency_limiter
|
||||
.insert(account_id, limiter.clone());
|
||||
limiter
|
||||
})
|
||||
}
|
||||
|
@ -57,18 +62,23 @@ impl JMAP {
|
|||
access_token: &AccessToken,
|
||||
) -> Result<InFlight, RequestError> {
|
||||
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
|
||||
.storage
|
||||
.lookup
|
||||
.is_rate_allowed(
|
||||
format!("j:{}", access_token.primary_id).as_bytes(),
|
||||
rate,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.map_err(|_| RequestError::internal_server_error())?
|
||||
.is_none()
|
||||
} else {
|
||||
true
|
||||
};
|
||||
|
||||
if self
|
||||
.lookup_store
|
||||
.is_rate_allowed(
|
||||
format!("j:{}", access_token.primary_id).as_bytes(),
|
||||
&self.config.rate_authenticated,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.map_err(|_| RequestError::internal_server_error())?
|
||||
.is_none()
|
||||
{
|
||||
if is_rate_allowed {
|
||||
if let Some(in_flight_request) = limiter.concurrent_requests.is_allowed() {
|
||||
Ok(in_flight_request)
|
||||
} else if access_token.is_super_user() {
|
||||
|
@ -84,21 +94,20 @@ impl JMAP {
|
|||
}
|
||||
|
||||
pub async fn is_anonymous_allowed(&self, addr: &IpAddr) -> Result<(), RequestError> {
|
||||
if self
|
||||
.lookup_store
|
||||
.is_rate_allowed(
|
||||
format!("jreq:{}", addr).as_bytes(),
|
||||
&self.config.rate_anonymous,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.map_err(|_| RequestError::internal_server_error())?
|
||||
.is_none()
|
||||
{
|
||||
Ok(())
|
||||
} else {
|
||||
Err(RequestError::too_many_requests())
|
||||
if let Some(rate) = &self.core.jmap.rate_anonymous {
|
||||
if self
|
||||
.core
|
||||
.storage
|
||||
.lookup
|
||||
.is_rate_allowed(format!("jreq:{}", addr).as_bytes(), rate, false)
|
||||
.await
|
||||
.map_err(|_| RequestError::internal_server_error())?
|
||||
.is_some()
|
||||
{
|
||||
return Err(RequestError::too_many_requests());
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn is_upload_allowed(&self, access_token: &AccessToken) -> Result<InFlight, RequestError> {
|
||||
|
@ -116,39 +125,37 @@ impl JMAP {
|
|||
}
|
||||
|
||||
pub async fn is_auth_allowed_soft(&self, addr: &IpAddr) -> Result<(), RequestError> {
|
||||
if self
|
||||
.lookup_store
|
||||
.is_rate_allowed(
|
||||
format!("jauth:{}", addr).as_bytes(),
|
||||
&self.config.rate_authenticate_req,
|
||||
true,
|
||||
)
|
||||
.await
|
||||
.map_err(|_| RequestError::internal_server_error())?
|
||||
.is_none()
|
||||
{
|
||||
Ok(())
|
||||
} else {
|
||||
Err(RequestError::too_many_auth_attempts())
|
||||
if let Some(rate) = &self.core.jmap.rate_authenticate_req {
|
||||
if self
|
||||
.core
|
||||
.storage
|
||||
.lookup
|
||||
.is_rate_allowed(format!("jauth:{}", addr).as_bytes(), rate, true)
|
||||
.await
|
||||
.map_err(|_| RequestError::internal_server_error())?
|
||||
.is_some()
|
||||
{
|
||||
return Err(RequestError::too_many_auth_attempts());
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn is_auth_allowed_hard(&self, addr: &IpAddr) -> Result<(), RequestError> {
|
||||
if self
|
||||
.lookup_store
|
||||
.is_rate_allowed(
|
||||
format!("jauth:{}", addr).as_bytes(),
|
||||
&self.config.rate_authenticate_req,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.map_err(|_| RequestError::internal_server_error())?
|
||||
.is_none()
|
||||
{
|
||||
Ok(())
|
||||
} else {
|
||||
Err(RequestError::too_many_auth_attempts())
|
||||
if let Some(rate) = &self.core.jmap.rate_authenticate_req {
|
||||
if self
|
||||
.core
|
||||
.storage
|
||||
.lookup
|
||||
.is_rate_allowed(format!("jauth:{}", addr).as_bytes(), rate, false)
|
||||
.await
|
||||
.map_err(|_| RequestError::internal_server_error())?
|
||||
.is_some()
|
||||
{
|
||||
return Err(RequestError::too_many_auth_attempts());
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -55,7 +55,7 @@ impl JMAP {
|
|||
for blob_id in request.blob_ids {
|
||||
if self.has_access_blob(&blob_id, access_token).await? {
|
||||
let mut batch = BatchBuilder::new();
|
||||
let until = now() + self.config.upload_tmp_ttl;
|
||||
let until = now() + self.core.jmap.upload_tmp_ttl;
|
||||
batch.with_account_id(account_id).set(
|
||||
BlobOp::Reserve {
|
||||
until,
|
||||
|
|
|
@ -48,7 +48,9 @@ impl JMAP {
|
|||
access_token: &AccessToken,
|
||||
) -> Result<Option<Vec<u8>>, MethodError> {
|
||||
if !self
|
||||
.store
|
||||
.core
|
||||
.storage
|
||||
.data
|
||||
.blob_has_access(&blob_id.hash, &blob_id.class)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
|
@ -129,7 +131,7 @@ impl JMAP {
|
|||
hash: &BlobHash,
|
||||
range: Range<usize>,
|
||||
) -> Result<Option<Vec<u8>>, MethodError> {
|
||||
match self.blob_store.get_blob(hash.as_ref(), range).await {
|
||||
match self.core.storage.blob.get_blob(hash.as_ref(), range).await {
|
||||
Ok(blob) => Ok(blob),
|
||||
Err(err) => {
|
||||
tracing::error!(event = "error",
|
||||
|
@ -148,7 +150,9 @@ impl JMAP {
|
|||
access_token: &AccessToken,
|
||||
) -> Result<bool, MethodError> {
|
||||
Ok(self
|
||||
.store
|
||||
.core
|
||||
.storage
|
||||
.data
|
||||
.blob_has_access(&blob_id.hash, &blob_id.class)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
|
|
|
@ -52,7 +52,7 @@ impl JMAP {
|
|||
access_token: &AccessToken,
|
||||
) -> Result<GetResponse, MethodError> {
|
||||
let ids = request
|
||||
.unwrap_blob_ids(self.config.get_max_objects)?
|
||||
.unwrap_blob_ids(self.core.jmap.get_max_objects)?
|
||||
.unwrap_or_default();
|
||||
let properties = request.unwrap_properties(&[
|
||||
Property::Id,
|
||||
|
|
|
@ -58,7 +58,7 @@ impl JMAP {
|
|||
};
|
||||
let account_id = request.account_id.document_id();
|
||||
|
||||
if request.create.len() > self.config.set_max_objects {
|
||||
if request.create.len() > self.core.jmap.set_max_objects {
|
||||
return Err(MethodError::RequestTooLarge);
|
||||
}
|
||||
|
||||
|
@ -129,14 +129,14 @@ impl JMAP {
|
|||
DataSourceObject::Value(bytes) => bytes,
|
||||
};
|
||||
|
||||
if bytes.len() + data.len() < self.config.upload_max_size {
|
||||
if bytes.len() + data.len() < self.core.jmap.upload_max_size {
|
||||
data.extend(bytes);
|
||||
} else {
|
||||
response.not_created.append(
|
||||
create_id,
|
||||
SetError::too_large().with_description(format!(
|
||||
"Upload size exceeds maximum of {} bytes.",
|
||||
self.config.upload_max_size
|
||||
self.core.jmap.upload_max_size
|
||||
)),
|
||||
);
|
||||
continue 'outer;
|
||||
|
@ -153,26 +153,33 @@ impl JMAP {
|
|||
}
|
||||
|
||||
// Enforce quota
|
||||
let used = self.store.blob_quota(account_id).await.map_err(|err| {
|
||||
tracing::error!(event = "error",
|
||||
let used = self
|
||||
.core
|
||||
.storage
|
||||
.data
|
||||
.blob_quota(account_id)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
tracing::error!(event = "error",
|
||||
context = "blob_store",
|
||||
account_id = account_id,
|
||||
error = ?err,
|
||||
"Failed to obtain blob quota");
|
||||
MethodError::ServerPartialFail
|
||||
})?;
|
||||
MethodError::ServerPartialFail
|
||||
})?;
|
||||
|
||||
if ((self.config.upload_tmp_quota_size > 0
|
||||
&& used.bytes + data.len() > self.config.upload_tmp_quota_size)
|
||||
|| (self.config.upload_tmp_quota_amount > 0
|
||||
&& used.count + 1 > self.config.upload_tmp_quota_amount))
|
||||
if ((self.core.jmap.upload_tmp_quota_size > 0
|
||||
&& used.bytes + data.len() > self.core.jmap.upload_tmp_quota_size)
|
||||
|| (self.core.jmap.upload_tmp_quota_amount > 0
|
||||
&& used.count + 1 > self.core.jmap.upload_tmp_quota_amount))
|
||||
&& !access_token.is_super_user()
|
||||
{
|
||||
response.not_created.append(
|
||||
create_id,
|
||||
SetError::over_quota().with_description(format!(
|
||||
"You have exceeded the blob upload quota of {} files or {} bytes.",
|
||||
self.config.upload_tmp_quota_amount, self.config.upload_tmp_quota_size
|
||||
self.core.jmap.upload_tmp_quota_amount,
|
||||
self.core.jmap.upload_tmp_quota_size
|
||||
)),
|
||||
);
|
||||
continue 'outer;
|
||||
|
@ -212,7 +219,9 @@ impl JMAP {
|
|||
|
||||
// Enforce quota
|
||||
let used = self
|
||||
.store
|
||||
.core
|
||||
.storage
|
||||
.data
|
||||
.blob_quota(account_id.document_id())
|
||||
.await
|
||||
.map_err(|err| {
|
||||
|
@ -224,15 +233,15 @@ impl JMAP {
|
|||
RequestError::internal_server_error()
|
||||
})?;
|
||||
|
||||
if ((self.config.upload_tmp_quota_size > 0
|
||||
&& used.bytes + data.len() > self.config.upload_tmp_quota_size)
|
||||
|| (self.config.upload_tmp_quota_amount > 0
|
||||
&& used.count + 1 > self.config.upload_tmp_quota_amount))
|
||||
if ((self.core.jmap.upload_tmp_quota_size > 0
|
||||
&& used.bytes + data.len() > self.core.jmap.upload_tmp_quota_size)
|
||||
|| (self.core.jmap.upload_tmp_quota_amount > 0
|
||||
&& used.count + 1 > self.core.jmap.upload_tmp_quota_amount))
|
||||
&& !access_token.is_super_user()
|
||||
{
|
||||
let err = Err(RequestError::over_blob_quota(
|
||||
self.config.upload_tmp_quota_amount,
|
||||
self.config.upload_tmp_quota_size,
|
||||
self.core.jmap.upload_tmp_quota_amount,
|
||||
self.core.jmap.upload_tmp_quota_size,
|
||||
));
|
||||
|
||||
#[cfg(feature = "test_mode")]
|
||||
|
@ -265,7 +274,7 @@ impl JMAP {
|
|||
// First reserve the hash
|
||||
let hash = BlobHash::from(data);
|
||||
let mut batch = BatchBuilder::new();
|
||||
let until = now() + self.config.upload_tmp_ttl;
|
||||
let until = now() + self.core.jmap.upload_tmp_ttl;
|
||||
|
||||
batch.with_account_id(account_id).set(
|
||||
BlobOp::Reserve {
|
||||
|
@ -276,16 +285,25 @@ impl JMAP {
|
|||
);
|
||||
self.write_batch(batch).await?;
|
||||
|
||||
if !self.store.blob_exists(&hash).await.map_err(|err| {
|
||||
tracing::error!(
|
||||
if !self
|
||||
.core
|
||||
.storage
|
||||
.data
|
||||
.blob_exists(&hash)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
tracing::error!(
|
||||
event = "error",
|
||||
context = "put_blob",
|
||||
error = ?err,
|
||||
"Failed to verify blob hash existence.");
|
||||
MethodError::ServerPartialFail
|
||||
})? {
|
||||
MethodError::ServerPartialFail
|
||||
})?
|
||||
{
|
||||
// Upload blob to store
|
||||
self.blob_store
|
||||
self.core
|
||||
.storage
|
||||
.blob
|
||||
.put_blob(hash.as_ref(), data)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
|
|
|
@ -69,10 +69,10 @@ impl JMAP {
|
|||
}
|
||||
};
|
||||
|
||||
let max_changes = if self.config.changes_max_results > 0
|
||||
&& self.config.changes_max_results < request.max_changes.unwrap_or(0)
|
||||
let max_changes = if self.core.jmap.changes_max_results > 0
|
||||
&& self.core.jmap.changes_max_results < request.max_changes.unwrap_or(0)
|
||||
{
|
||||
self.config.changes_max_results
|
||||
self.core.jmap.changes_max_results
|
||||
} else {
|
||||
request.max_changes.unwrap_or(0)
|
||||
};
|
||||
|
@ -182,7 +182,9 @@ impl JMAP {
|
|||
collection: Collection,
|
||||
query: Query,
|
||||
) -> Result<Changes, MethodError> {
|
||||
self.store
|
||||
self.core
|
||||
.storage
|
||||
.data
|
||||
.changes(account_id, collection, query)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
|
|
|
@ -35,7 +35,13 @@ impl JMAP {
|
|||
collection: impl Into<u8>,
|
||||
) -> Result<State, MethodError> {
|
||||
let collection = collection.into();
|
||||
match self.store.get_last_change_id(account_id, collection).await {
|
||||
match self
|
||||
.core
|
||||
.storage
|
||||
.data
|
||||
.get_last_change_id(account_id, collection)
|
||||
.await
|
||||
{
|
||||
Ok(id) => Ok(id.into()),
|
||||
Err(err) => {
|
||||
tracing::error!(event = "error",
|
||||
|
|
|
@ -35,7 +35,7 @@ impl JMAP {
|
|||
|
||||
pub async fn assign_change_id(&self, _: u32) -> Result<u64, MethodError> {
|
||||
self.generate_snowflake_id()
|
||||
/*self.store
|
||||
/*self.core.storage.data
|
||||
.assign_change_id(account_id)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
|
@ -49,7 +49,7 @@ impl JMAP {
|
|||
}
|
||||
|
||||
pub fn generate_snowflake_id(&self) -> Result<u64, MethodError> {
|
||||
self.snowflake_id.generate().ok_or_else(|| {
|
||||
self.inner.snowflake_id.generate().ok_or_else(|| {
|
||||
tracing::error!(
|
||||
event = "error",
|
||||
context = "change_log",
|
||||
|
@ -71,14 +71,19 @@ impl JMAP {
|
|||
|
||||
let mut builder = BatchBuilder::new();
|
||||
builder.with_account_id(account_id).custom(changes);
|
||||
self.store.write(builder.build()).await.map_err(|err| {
|
||||
tracing::error!(
|
||||
self.core
|
||||
.storage
|
||||
.data
|
||||
.write(builder.build())
|
||||
.await
|
||||
.map_err(|err| {
|
||||
tracing::error!(
|
||||
event = "error",
|
||||
context = "change_log",
|
||||
error = ?err,
|
||||
"Failed to write changes.");
|
||||
MethodError::ServerPartialFail
|
||||
})?;
|
||||
MethodError::ServerPartialFail
|
||||
})?;
|
||||
|
||||
Ok(state)
|
||||
}
|
||||
|
|
|
@ -46,7 +46,9 @@ impl JMAP {
|
|||
) -> Result<Vec<(u32, u32)>, MethodError> {
|
||||
// Obtain current state
|
||||
let modseq = self
|
||||
.store
|
||||
.core
|
||||
.storage
|
||||
.data
|
||||
.get_last_change_id(account_id, Collection::Thread)
|
||||
.map_err(|err| {
|
||||
tracing::error!(event = "error",
|
||||
|
@ -60,7 +62,7 @@ impl JMAP {
|
|||
|
||||
// Lock the cache
|
||||
let thread_cache = if let Some(thread_cache) =
|
||||
self.cache_threads.get(&account_id).and_then(|t| {
|
||||
self.inner.cache_threads.get(&account_id).and_then(|t| {
|
||||
if t.modseq.unwrap_or(0) >= modseq.unwrap_or(0) {
|
||||
Some(t)
|
||||
} else {
|
||||
|
@ -82,7 +84,9 @@ impl JMAP {
|
|||
.collect(),
|
||||
modseq,
|
||||
});
|
||||
self.cache_threads.insert(account_id, thread_cache.clone());
|
||||
self.inner
|
||||
.cache_threads
|
||||
.insert(account_id, thread_cache.clone());
|
||||
thread_cache
|
||||
};
|
||||
|
||||
|
|
|
@ -435,17 +435,22 @@ impl JMAP {
|
|||
.custom(EmailIndexBuilder::set(metadata))
|
||||
.custom(changes);
|
||||
|
||||
self.store.write(batch.build()).await.map_err(|err| {
|
||||
tracing::error!(
|
||||
self.core
|
||||
.storage
|
||||
.data
|
||||
.write(batch.build())
|
||||
.await
|
||||
.map_err(|err| {
|
||||
tracing::error!(
|
||||
event = "error",
|
||||
context = "email_copy",
|
||||
error = ?err,
|
||||
"Failed to write message to database.");
|
||||
MethodError::ServerPartialFail
|
||||
})?;
|
||||
MethodError::ServerPartialFail
|
||||
})?;
|
||||
|
||||
// Request FTS index
|
||||
let _ = self.housekeeper_tx.send(Event::IndexStart).await;
|
||||
let _ = self.inner.housekeeper_tx.send(Event::IndexStart).await;
|
||||
|
||||
Ok(Ok(email))
|
||||
}
|
||||
|
|
|
@ -29,6 +29,7 @@ use crate::{
|
|||
JMAP,
|
||||
};
|
||||
use aes::cipher::{block_padding::Pkcs7, BlockEncryptMut, KeyIvInit};
|
||||
use common::AuthResult;
|
||||
use jmap_proto::types::{collection::Collection, property::Property};
|
||||
use mail_builder::{encoders::base64::base64_encode_mime, mime::make_boundary};
|
||||
use mail_parser::{decoders::base64::base64_decode, Message, MessageParser, MimeHeaders};
|
||||
|
|
|
@ -53,7 +53,7 @@ impl JMAP {
|
|||
mut request: GetRequest<GetArguments>,
|
||||
access_token: &AccessToken,
|
||||
) -> Result<GetResponse, MethodError> {
|
||||
let ids = request.unwrap_ids(self.config.get_max_objects)?;
|
||||
let ids = request.unwrap_ids(self.core.jmap.get_max_objects)?;
|
||||
let properties = request.unwrap_properties(&[
|
||||
Property::Id,
|
||||
Property::BlobId,
|
||||
|
@ -108,7 +108,7 @@ impl JMAP {
|
|||
} else {
|
||||
let document_ids = message_ids
|
||||
.iter()
|
||||
.take(self.config.get_max_objects)
|
||||
.take(self.core.jmap.get_max_objects)
|
||||
.collect::<Vec<_>>();
|
||||
self.get_cached_thread_ids(account_id, document_ids.iter().copied())
|
||||
.await
|
||||
|
|
|
@ -141,7 +141,7 @@ impl JMAP {
|
|||
keywords: email.keywords,
|
||||
received_at: email.received_at.map(|r| r.into()),
|
||||
skip_duplicates: true,
|
||||
encrypt: self.config.encrypt && self.config.encrypt_append,
|
||||
encrypt: self.core.jmap.encrypt && self.core.jmap.encrypt_append,
|
||||
})
|
||||
.await
|
||||
{
|
||||
|
|
|
@ -108,7 +108,7 @@ impl JMAP {
|
|||
})?;
|
||||
|
||||
// Check for Spam headers
|
||||
if let Some((header_name, header_value)) = &self.config.spam_header {
|
||||
if let Some((header_name, header_value)) = &self.core.jmap.spam_header {
|
||||
if params.mailbox_ids == [INBOX_ID]
|
||||
&& message.root_part().headers().iter().any(|header| {
|
||||
&header.name == header_name
|
||||
|
@ -164,7 +164,9 @@ impl JMAP {
|
|||
if params.skip_duplicates
|
||||
&& !message_id.is_empty()
|
||||
&& !self
|
||||
.store
|
||||
.core
|
||||
.storage
|
||||
.data
|
||||
.filter(
|
||||
params.account_id,
|
||||
Collection::Email,
|
||||
|
@ -261,7 +263,9 @@ impl JMAP {
|
|||
|
||||
// Obtain a documentId and changeId
|
||||
let document_id = self
|
||||
.store
|
||||
.core
|
||||
.storage
|
||||
.data
|
||||
.assign_document_id(params.account_id, Collection::Email)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
|
@ -327,7 +331,9 @@ impl JMAP {
|
|||
thread_id
|
||||
} else {
|
||||
let thread_id = self
|
||||
.store
|
||||
.core
|
||||
.storage
|
||||
.data
|
||||
.assign_document_id(params.account_id, Collection::Thread)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
|
@ -371,17 +377,22 @@ impl JMAP {
|
|||
),
|
||||
blob_id.hash.clone(),
|
||||
);
|
||||
self.store.write(batch.build()).await.map_err(|err| {
|
||||
tracing::error!(
|
||||
self.core
|
||||
.storage
|
||||
.data
|
||||
.write(batch.build())
|
||||
.await
|
||||
.map_err(|err| {
|
||||
tracing::error!(
|
||||
event = "error",
|
||||
context = "email_ingest",
|
||||
error = ?err,
|
||||
"Failed to write message to database.");
|
||||
IngestError::Temporary
|
||||
})?;
|
||||
IngestError::Temporary
|
||||
})?;
|
||||
|
||||
// Request FTS index
|
||||
let _ = self.housekeeper_tx.send(Event::IndexStart).await;
|
||||
let _ = self.inner.housekeeper_tx.send(Event::IndexStart).await;
|
||||
|
||||
tracing::debug!(
|
||||
context = "email_ingest",
|
||||
|
@ -436,7 +447,9 @@ impl JMAP {
|
|||
}
|
||||
filters.push(Filter::End);
|
||||
let results = self
|
||||
.store
|
||||
.core
|
||||
.storage
|
||||
.data
|
||||
.filter(account_id, Collection::Email, filters)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
|
@ -522,7 +535,9 @@ impl JMAP {
|
|||
{
|
||||
if thread_id != old_thread_id {
|
||||
for document_id in self
|
||||
.store
|
||||
.core
|
||||
.storage
|
||||
.data
|
||||
.get_bitmap(BitmapKey {
|
||||
account_id,
|
||||
collection: Collection::Email.into(),
|
||||
|
@ -558,7 +573,7 @@ impl JMAP {
|
|||
}
|
||||
batch.custom(changes);
|
||||
|
||||
match self.store.write(batch.build()).await {
|
||||
match self.core.storage.data.write(batch.build()).await {
|
||||
Ok(_) => return Ok(Some(thread_id)),
|
||||
Err(store::Error::AssertValueFailed) if try_count < MAX_RETRIES => {
|
||||
let backoff = rand::thread_rng().gen_range(50..=300);
|
||||
|
@ -585,7 +600,9 @@ impl JMAP {
|
|||
.with_collection(Collection::Mailbox)
|
||||
.update_document(mailbox_id)
|
||||
.add_and_get(Property::EmailIds, 1);
|
||||
self.store
|
||||
self.core
|
||||
.storage
|
||||
.data
|
||||
.write(batch.build())
|
||||
.await
|
||||
.map(|v| v.expect("UID next") as u32)
|
||||
|
|
|
@ -46,7 +46,7 @@ impl JMAP {
|
|||
request: ParseEmailRequest,
|
||||
access_token: &AccessToken,
|
||||
) -> Result<ParseEmailResponse, MethodError> {
|
||||
if request.blob_ids.len() > self.config.mail_parse_max_items {
|
||||
if request.blob_ids.len() > self.core.jmap.mail_parse_max_items {
|
||||
return Err(MethodError::RequestTooLarge);
|
||||
}
|
||||
let properties = request.properties.unwrap_or_else(|| {
|
||||
|
|
|
@ -79,17 +79,17 @@ impl JMAP {
|
|||
fts_filters.push(FtsFilter::has_text_detect(
|
||||
Field::Header(HeaderName::Subject),
|
||||
&text,
|
||||
self.config.default_language,
|
||||
self.core.jmap.default_language,
|
||||
));
|
||||
fts_filters.push(FtsFilter::has_text_detect(
|
||||
Field::Body,
|
||||
&text,
|
||||
self.config.default_language,
|
||||
self.core.jmap.default_language,
|
||||
));
|
||||
fts_filters.push(FtsFilter::has_text_detect(
|
||||
Field::Attachment,
|
||||
text,
|
||||
self.config.default_language,
|
||||
self.core.jmap.default_language,
|
||||
));
|
||||
fts_filters.push(FtsFilter::End);
|
||||
}
|
||||
|
@ -116,12 +116,12 @@ impl JMAP {
|
|||
Filter::Subject(text) => fts_filters.push(FtsFilter::has_text_detect(
|
||||
Field::Header(HeaderName::Subject),
|
||||
text,
|
||||
self.config.default_language,
|
||||
self.core.jmap.default_language,
|
||||
)),
|
||||
Filter::Body(text) => fts_filters.push(FtsFilter::has_text_detect(
|
||||
Field::Body,
|
||||
text,
|
||||
self.config.default_language,
|
||||
self.core.jmap.default_language,
|
||||
)),
|
||||
Filter::Header(header) => {
|
||||
let mut header = header.into_iter();
|
||||
|
|
|
@ -599,8 +599,9 @@ impl JMAP {
|
|||
// Check attachment sizes
|
||||
if !is_multipart {
|
||||
size_attachments += parts.last().unwrap().size();
|
||||
if self.config.mail_attachments_max_size > 0
|
||||
&& size_attachments > self.config.mail_attachments_max_size
|
||||
if self.core.jmap.mail_attachments_max_size > 0
|
||||
&& size_attachments
|
||||
> self.core.jmap.mail_attachments_max_size
|
||||
{
|
||||
response.not_created.append(
|
||||
id,
|
||||
|
@ -608,7 +609,7 @@ impl JMAP {
|
|||
.with_property(property)
|
||||
.with_description(format!(
|
||||
"Message exceeds maximum size of {} bytes.",
|
||||
self.config.mail_attachments_max_size
|
||||
self.core.jmap.mail_attachments_max_size
|
||||
)),
|
||||
);
|
||||
continue 'create;
|
||||
|
@ -741,7 +742,7 @@ impl JMAP {
|
|||
keywords,
|
||||
received_at,
|
||||
skip_duplicates: false,
|
||||
encrypt: self.config.encrypt && self.config.encrypt_append,
|
||||
encrypt: self.core.jmap.encrypt && self.core.jmap.encrypt_append,
|
||||
})
|
||||
.await
|
||||
{
|
||||
|
@ -986,7 +987,7 @@ impl JMAP {
|
|||
|
||||
// Write changes
|
||||
if !batch.is_empty() {
|
||||
match self.store.write(batch.build()).await {
|
||||
match self.core.storage.data.write(batch.build()).await {
|
||||
Ok(_) => {
|
||||
// Add to updated list
|
||||
response.updated.append(id, None);
|
||||
|
@ -1240,7 +1241,7 @@ impl JMAP {
|
|||
}
|
||||
|
||||
// Commit batch
|
||||
match self.store.write(batch.build()).await {
|
||||
match self.core.storage.data.write(batch.build()).await {
|
||||
Ok(_) => (),
|
||||
Err(store::Error::AssertValueFailed) => {
|
||||
return Ok(Err(SetError::forbidden().with_description(
|
||||
|
@ -1258,7 +1259,7 @@ impl JMAP {
|
|||
}
|
||||
|
||||
// Request FTS index
|
||||
let _ = self.housekeeper_tx.send(Event::IndexStart).await;
|
||||
let _ = self.inner.housekeeper_tx.send(Event::IndexStart).await;
|
||||
|
||||
Ok(Ok(changes))
|
||||
}
|
||||
|
|
|
@ -47,14 +47,14 @@ impl JMAP {
|
|||
let mut include_term = true;
|
||||
let mut terms = vec![];
|
||||
let mut is_exact = false;
|
||||
let mut language = self.config.default_language;
|
||||
let mut language = self.core.jmap.default_language;
|
||||
|
||||
for cond in request.filter {
|
||||
match cond {
|
||||
Filter::Text(text) | Filter::Subject(text) | Filter::Body(text) => {
|
||||
if include_term {
|
||||
let (text, language_) =
|
||||
Language::detect(text, self.config.default_language);
|
||||
Language::detect(text, self.core.jmap.default_language);
|
||||
language = language_;
|
||||
if (text.starts_with('"') && text.ends_with('"'))
|
||||
|| (text.starts_with('\'') && text.ends_with('\''))
|
||||
|
@ -99,7 +99,7 @@ impl JMAP {
|
|||
not_found: vec![],
|
||||
};
|
||||
|
||||
if email_ids.len() > self.config.snippet_max_results {
|
||||
if email_ids.len() > self.core.jmap.snippet_max_results {
|
||||
return Err(MethodError::RequestTooLarge);
|
||||
}
|
||||
|
||||
|
|
|
@ -42,7 +42,7 @@ impl JMAP {
|
|||
&self,
|
||||
mut request: GetRequest<RequestArguments>,
|
||||
) -> Result<GetResponse, MethodError> {
|
||||
let ids = request.unwrap_ids(self.config.get_max_objects)?;
|
||||
let ids = request.unwrap_ids(self.core.jmap.get_max_objects)?;
|
||||
let properties = request.unwrap_properties(&[
|
||||
Property::Id,
|
||||
Property::Name,
|
||||
|
@ -60,7 +60,7 @@ impl JMAP {
|
|||
} else {
|
||||
identity_ids
|
||||
.iter()
|
||||
.take(self.config.get_max_objects)
|
||||
.take(self.core.jmap.get_max_objects)
|
||||
.map(Into::into)
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
|
@ -129,6 +129,8 @@ impl JMAP {
|
|||
|
||||
// Obtain principal
|
||||
let principal = self
|
||||
.core
|
||||
.storage
|
||||
.directory
|
||||
.query(QueryBy::Id(account_id), false)
|
||||
.await
|
||||
|
@ -181,14 +183,19 @@ impl JMAP {
|
|||
);
|
||||
identity_ids.insert(identity_id);
|
||||
}
|
||||
self.store.write(batch.build()).await.map_err(|err| {
|
||||
tracing::error!(
|
||||
self.core
|
||||
.storage
|
||||
.data
|
||||
.write(batch.build())
|
||||
.await
|
||||
.map_err(|err| {
|
||||
tracing::error!(
|
||||
event = "error",
|
||||
context = "identity_get_or_create",
|
||||
error = ?err,
|
||||
"Failed to create identities.");
|
||||
MethodError::ServerPartialFail
|
||||
})?;
|
||||
MethodError::ServerPartialFail
|
||||
})?;
|
||||
|
||||
Ok(identity_ids)
|
||||
}
|
||||
|
|
|
@ -47,7 +47,7 @@ impl JMAP {
|
|||
.get_document_ids(account_id, Collection::Identity)
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
let mut response = SetResponse::from_request(&request, self.config.set_max_objects)?;
|
||||
let mut response = SetResponse::from_request(&request, self.core.jmap.set_max_objects)?;
|
||||
let will_destroy = request.unwrap_destroy();
|
||||
|
||||
// Process creates
|
||||
|
@ -74,6 +74,8 @@ impl JMAP {
|
|||
// Validate email address
|
||||
if let Value::Text(email) = identity.get(&Property::Email) {
|
||||
if !self
|
||||
.core
|
||||
.storage
|
||||
.directory
|
||||
.query(QueryBy::Id(account_id), false)
|
||||
.await
|
||||
|
|
|
@ -23,11 +23,10 @@
|
|||
|
||||
use std::{collections::hash_map::RandomState, fmt::Display, sync::Arc, time::Duration};
|
||||
|
||||
use ::sieve::{Compiler, Runtime};
|
||||
use api::session::BaseCapabilities;
|
||||
use auth::{oauth::OAuthCode, rate_limit::ConcurrencyLimiters, AccessToken};
|
||||
use common::{Core, DeliveryEvent, SharedCore};
|
||||
use dashmap::DashMap;
|
||||
use directory::{Directories, Directory, QueryBy};
|
||||
use directory::QueryBy;
|
||||
use email::cache::Threads;
|
||||
use jmap_proto::{
|
||||
error::method::MethodError,
|
||||
|
@ -37,13 +36,12 @@ use jmap_proto::{
|
|||
},
|
||||
types::{collection::Collection, property::Property},
|
||||
};
|
||||
use mail_parser::HeaderName;
|
||||
use nlp::language::Language;
|
||||
use services::{
|
||||
delivery::spawn_delivery_manager,
|
||||
housekeeper::{self, init_housekeeper, spawn_housekeeper},
|
||||
state::{self, init_state_manager, spawn_state_manager},
|
||||
};
|
||||
|
||||
use smtp::core::SMTP;
|
||||
use store::{
|
||||
fts::FtsFilter,
|
||||
|
@ -52,17 +50,14 @@ use store::{
|
|||
write::{
|
||||
key::DeserializeBigEndian, BatchBuilder, BitmapClass, DirectoryClass, TagValue, ValueClass,
|
||||
},
|
||||
BitmapKey, BlobStore, Deserialize, FtsStore, IterateParams, LookupStore, Store, Stores,
|
||||
ValueKey, U32_LEN,
|
||||
BitmapKey, Deserialize, IterateParams, ValueKey, U32_LEN,
|
||||
};
|
||||
use tokio::sync::mpsc;
|
||||
use utils::{
|
||||
config::{Rate, Servers},
|
||||
ipc::DeliveryEvent,
|
||||
config::Config,
|
||||
lru_cache::{LruCache, LruCached},
|
||||
map::ttl_dashmap::{TtlDashMap, TtlMap},
|
||||
snowflake::SnowflakeIdGenerator,
|
||||
UnwrapFailure,
|
||||
};
|
||||
|
||||
pub mod api;
|
||||
|
@ -84,14 +79,21 @@ pub mod websocket;
|
|||
|
||||
pub const LONG_SLUMBER: Duration = Duration::from_secs(60 * 60 * 24);
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct JMAP {
|
||||
pub store: Store,
|
||||
pub blob_store: BlobStore,
|
||||
pub fts_store: FtsStore,
|
||||
pub lookup_store: LookupStore,
|
||||
pub config: Config,
|
||||
pub directory: Arc<Directory>,
|
||||
pub core: Arc<Core>,
|
||||
pub inner: Arc<Inner>,
|
||||
pub smtp: SMTP,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct JmapInstance {
|
||||
pub core: SharedCore,
|
||||
pub jmap_inner: Arc<Inner>,
|
||||
pub smtp_inner: Arc<smtp::core::Inner>,
|
||||
}
|
||||
|
||||
pub struct Inner {
|
||||
pub sessions: TtlDashMap<String, u32>,
|
||||
pub access_tokens: TtlDashMap<u32, Arc<AccessToken>>,
|
||||
pub snowflake_id: SnowflakeIdGenerator,
|
||||
|
@ -101,74 +103,8 @@ pub struct JMAP {
|
|||
|
||||
pub state_tx: mpsc::Sender<state::Event>,
|
||||
pub housekeeper_tx: mpsc::Sender<housekeeper::Event>,
|
||||
pub smtp: Arc<SMTP>,
|
||||
|
||||
pub cache_threads: LruCache<u32, Arc<Threads>>,
|
||||
|
||||
pub sieve_compiler: Compiler,
|
||||
pub sieve_runtime: Runtime<()>,
|
||||
}
|
||||
|
||||
pub struct Config {
|
||||
pub default_language: Language,
|
||||
pub query_max_results: usize,
|
||||
pub changes_max_results: usize,
|
||||
pub snippet_max_results: usize,
|
||||
|
||||
pub request_max_size: usize,
|
||||
pub request_max_calls: usize,
|
||||
pub request_max_concurrent: u64,
|
||||
|
||||
pub get_max_objects: usize,
|
||||
pub set_max_objects: usize,
|
||||
|
||||
pub upload_max_size: usize,
|
||||
pub upload_max_concurrent: u64,
|
||||
|
||||
pub upload_tmp_quota_size: usize,
|
||||
pub upload_tmp_quota_amount: usize,
|
||||
pub upload_tmp_ttl: u64,
|
||||
|
||||
pub mailbox_max_depth: usize,
|
||||
pub mailbox_name_max_len: usize,
|
||||
pub mail_attachments_max_size: usize,
|
||||
pub mail_parse_max_items: usize,
|
||||
pub mail_max_size: usize,
|
||||
|
||||
pub sieve_max_script_name: usize,
|
||||
pub sieve_max_scripts: usize,
|
||||
|
||||
pub session_cache_ttl: Duration,
|
||||
pub rate_authenticated: Rate,
|
||||
pub rate_authenticate_req: Rate,
|
||||
pub rate_anonymous: Rate,
|
||||
pub rate_use_forwarded: bool,
|
||||
|
||||
pub event_source_throttle: Duration,
|
||||
pub push_max_total: usize,
|
||||
|
||||
pub web_socket_throttle: Duration,
|
||||
pub web_socket_timeout: Duration,
|
||||
pub web_socket_heartbeat: Duration,
|
||||
|
||||
pub oauth_key: String,
|
||||
pub oauth_expiry_user_code: u64,
|
||||
pub oauth_expiry_auth_code: u64,
|
||||
pub oauth_expiry_token: u64,
|
||||
pub oauth_expiry_refresh_token: u64,
|
||||
pub oauth_expiry_refresh_token_renew: u64,
|
||||
pub oauth_max_auth_attempts: u32,
|
||||
|
||||
pub spam_header: Option<(HeaderName<'static>, String)>,
|
||||
|
||||
pub http_headers: Vec<(hyper::header::HeaderName, hyper::header::HeaderValue)>,
|
||||
|
||||
pub encrypt: bool,
|
||||
pub encrypt_append: bool,
|
||||
|
||||
pub principal_allow_lookups: bool,
|
||||
|
||||
pub capabilities: BaseCapabilities,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
@ -180,213 +116,56 @@ pub enum IngestError {
|
|||
|
||||
impl JMAP {
|
||||
pub async fn init(
|
||||
config: &utils::config::Config,
|
||||
stores: &Stores,
|
||||
directories: &Directories,
|
||||
servers: &mut Servers,
|
||||
config: &mut Config,
|
||||
delivery_rx: mpsc::Receiver<DeliveryEvent>,
|
||||
smtp: Arc<SMTP>,
|
||||
) -> Result<Arc<Self>, String> {
|
||||
core: SharedCore,
|
||||
smtp_inner: Arc<smtp::core::Inner>,
|
||||
) -> JmapInstance {
|
||||
// Init state manager and housekeeper
|
||||
let (state_tx, state_rx) = init_state_manager();
|
||||
let (housekeeper_tx, housekeeper_rx) = init_housekeeper();
|
||||
let shard_amount = config
|
||||
.property::<u64>("cache.shard")?
|
||||
.property_::<u64>("cache.shard")
|
||||
.unwrap_or(32)
|
||||
.next_power_of_two() as usize;
|
||||
let capacity = config.property("cache.capacity")?.unwrap_or(100);
|
||||
let capacity = config.property_("cache.capacity").unwrap_or(100);
|
||||
|
||||
let jmap_server = Arc::new(JMAP {
|
||||
directory: directories
|
||||
.directories
|
||||
.get(config.value_require("storage.directory")?)
|
||||
.failed(&format!(
|
||||
"Unable to find directory '{}'",
|
||||
config.value_require("storage.directory")?
|
||||
))
|
||||
.clone(),
|
||||
snowflake_id: config
|
||||
.property::<u64>("storage.cluster.node-id")?
|
||||
.map(SnowflakeIdGenerator::with_node_id)
|
||||
.unwrap_or_else(SnowflakeIdGenerator::new),
|
||||
store: stores.get_store(config, "storage.data")?,
|
||||
fts_store: stores.get_fts_store(config, "storage.fts")?,
|
||||
blob_store: stores.get_blob_store(config, "storage.blob")?,
|
||||
lookup_store: stores.get_lookup_store(config, "storage.lookup")?,
|
||||
config: Config::new(config).failed("Invalid configuration file"),
|
||||
let inner = Inner {
|
||||
sessions: TtlDashMap::with_capacity(capacity, shard_amount),
|
||||
access_tokens: TtlDashMap::with_capacity(capacity, shard_amount),
|
||||
snowflake_id: config
|
||||
.property_::<u64>("cluster.node-id")
|
||||
.map(SnowflakeIdGenerator::with_node_id)
|
||||
.unwrap_or_default(),
|
||||
concurrency_limiter: DashMap::with_capacity_and_hasher_and_shard_amount(
|
||||
capacity,
|
||||
RandomState::default(),
|
||||
shard_amount,
|
||||
),
|
||||
oauth_codes: TtlDashMap::with_capacity(capacity, shard_amount),
|
||||
cache_threads: LruCache::with_capacity(
|
||||
config.property("cache.thread.size")?.unwrap_or(2048),
|
||||
),
|
||||
state_tx,
|
||||
housekeeper_tx,
|
||||
smtp,
|
||||
sieve_compiler: Compiler::new()
|
||||
.with_max_script_size(
|
||||
config
|
||||
.property("sieve.untrusted.limits.script-size")?
|
||||
.unwrap_or(1024 * 1024),
|
||||
)
|
||||
.with_max_string_size(
|
||||
config
|
||||
.property("sieve.untrusted.limits.string-length")?
|
||||
.unwrap_or(4096),
|
||||
)
|
||||
.with_max_variable_name_size(
|
||||
config
|
||||
.property("sieve.untrusted.limits.variable-name-length")?
|
||||
.unwrap_or(32),
|
||||
)
|
||||
.with_max_nested_blocks(
|
||||
config
|
||||
.property("sieve.untrusted.limits.nested-blocks")?
|
||||
.unwrap_or(15),
|
||||
)
|
||||
.with_max_nested_tests(
|
||||
config
|
||||
.property("sieve.untrusted.limits.nested-tests")?
|
||||
.unwrap_or(15),
|
||||
)
|
||||
.with_max_nested_foreverypart(
|
||||
config
|
||||
.property("sieve.untrusted.limits.nested-foreverypart")?
|
||||
.unwrap_or(3),
|
||||
)
|
||||
.with_max_match_variables(
|
||||
config
|
||||
.property("sieve.untrusted.limits.match-variables")?
|
||||
.unwrap_or(30),
|
||||
)
|
||||
.with_max_local_variables(
|
||||
config
|
||||
.property("sieve.untrusted.limits.local-variables")?
|
||||
.unwrap_or(128),
|
||||
)
|
||||
.with_max_header_size(
|
||||
config
|
||||
.property("sieve.untrusted.limits.header-size")?
|
||||
.unwrap_or(1024),
|
||||
)
|
||||
.with_max_includes(
|
||||
config
|
||||
.property("sieve.untrusted.limits.includes")?
|
||||
.unwrap_or(3),
|
||||
),
|
||||
sieve_runtime: Runtime::new()
|
||||
.with_max_nested_includes(
|
||||
config
|
||||
.property("sieve.untrusted.limits.nested-includes")?
|
||||
.unwrap_or(3),
|
||||
)
|
||||
.with_cpu_limit(
|
||||
config
|
||||
.property("sieve.untrusted.limits.cpu")?
|
||||
.unwrap_or(5000),
|
||||
)
|
||||
.with_max_variable_size(
|
||||
config
|
||||
.property("sieve.untrusted.limits.variable-size")?
|
||||
.unwrap_or(4096),
|
||||
)
|
||||
.with_max_redirects(
|
||||
config
|
||||
.property("sieve.untrusted.limits.redirects")?
|
||||
.unwrap_or(1),
|
||||
)
|
||||
.with_max_received_headers(
|
||||
config
|
||||
.property("sieve.untrusted.limits.received-headers")?
|
||||
.unwrap_or(10),
|
||||
)
|
||||
.with_max_header_size(
|
||||
config
|
||||
.property("sieve.untrusted.limits.header-size")?
|
||||
.unwrap_or(1024),
|
||||
)
|
||||
.with_max_out_messages(
|
||||
config
|
||||
.property("sieve.untrusted.limits.outgoing-messages")?
|
||||
.unwrap_or(3),
|
||||
)
|
||||
.with_default_vacation_expiry(
|
||||
config
|
||||
.property::<Duration>("sieve.untrusted.default-expiry.vacation")?
|
||||
.unwrap_or(Duration::from_secs(30 * 86400))
|
||||
.as_secs(),
|
||||
)
|
||||
.with_default_duplicate_expiry(
|
||||
config
|
||||
.property::<Duration>("sieve.untrusted.default-expiry.duplicate")?
|
||||
.unwrap_or(Duration::from_secs(7 * 86400))
|
||||
.as_secs(),
|
||||
)
|
||||
.without_capabilities(
|
||||
config
|
||||
.values("sieve.untrusted.disable-capabilities")
|
||||
.map(|(_, v)| v),
|
||||
)
|
||||
.with_valid_notification_uris({
|
||||
let values = config
|
||||
.values("sieve.untrusted.notification-uris")
|
||||
.map(|(_, v)| v.to_string())
|
||||
.collect::<Vec<_>>();
|
||||
if !values.is_empty() {
|
||||
values
|
||||
} else {
|
||||
vec!["mailto".to_string()]
|
||||
}
|
||||
})
|
||||
.with_protected_headers({
|
||||
let values = config
|
||||
.values("sieve.untrusted.protected-headers")
|
||||
.map(|(_, v)| v.to_string())
|
||||
.collect::<Vec<_>>();
|
||||
if !values.is_empty() {
|
||||
values
|
||||
} else {
|
||||
vec![
|
||||
"Original-Subject".to_string(),
|
||||
"Original-From".to_string(),
|
||||
"Received".to_string(),
|
||||
"Auto-Submitted".to_string(),
|
||||
]
|
||||
}
|
||||
})
|
||||
.with_vacation_default_subject(
|
||||
config
|
||||
.value("sieve.untrusted.vacation.default-subject")
|
||||
.unwrap_or("Automated reply")
|
||||
.to_string(),
|
||||
)
|
||||
.with_vacation_subject_prefix(
|
||||
config
|
||||
.value("sieve.untrusted.vacation.subject-prefix")
|
||||
.unwrap_or("Auto: ")
|
||||
.to_string(),
|
||||
)
|
||||
.with_env_variable("name", "Stalwart JMAP")
|
||||
.with_env_variable("version", env!("CARGO_PKG_VERSION"))
|
||||
.with_env_variable("location", "MS")
|
||||
.with_env_variable("phase", "during"),
|
||||
});
|
||||
cache_threads: LruCache::with_capacity(
|
||||
config.property_("cache.thread.size").unwrap_or(2048),
|
||||
),
|
||||
};
|
||||
|
||||
let jmap_instance = JmapInstance {
|
||||
core,
|
||||
jmap_inner: Arc::new(inner),
|
||||
smtp_inner,
|
||||
};
|
||||
|
||||
// Spawn delivery manager
|
||||
spawn_delivery_manager(jmap_server.clone(), delivery_rx);
|
||||
spawn_delivery_manager(jmap_instance.clone(), delivery_rx);
|
||||
|
||||
// Spawn state manager
|
||||
spawn_state_manager(jmap_server.clone(), config, state_rx);
|
||||
spawn_state_manager(jmap_instance.clone(), state_rx);
|
||||
|
||||
// Spawn housekeeper
|
||||
spawn_housekeeper(jmap_server.clone(), config, servers, housekeeper_rx);
|
||||
spawn_housekeeper(jmap_instance.clone(), housekeeper_rx);
|
||||
|
||||
Ok(jmap_server)
|
||||
jmap_instance
|
||||
}
|
||||
|
||||
pub async fn assign_document_id(
|
||||
|
@ -394,7 +173,9 @@ impl JMAP {
|
|||
account_id: u32,
|
||||
collection: Collection,
|
||||
) -> Result<u32, MethodError> {
|
||||
self.store
|
||||
self.core
|
||||
.storage
|
||||
.data
|
||||
.assign_document_id(account_id, collection)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
|
@ -419,7 +200,9 @@ impl JMAP {
|
|||
{
|
||||
let property = property.as_ref();
|
||||
match self
|
||||
.store
|
||||
.core
|
||||
.storage
|
||||
.data
|
||||
.get_value::<U>(ValueKey {
|
||||
account_id,
|
||||
collection: collection.into(),
|
||||
|
@ -460,7 +243,9 @@ impl JMAP {
|
|||
let expected_results = iterate.len();
|
||||
let mut results = Vec::with_capacity(expected_results);
|
||||
|
||||
self.store
|
||||
self.core
|
||||
.storage
|
||||
.data
|
||||
.iterate(
|
||||
IterateParams::new(
|
||||
ValueKey {
|
||||
|
@ -507,7 +292,9 @@ impl JMAP {
|
|||
collection: Collection,
|
||||
) -> Result<Option<RoaringBitmap>, MethodError> {
|
||||
match self
|
||||
.store
|
||||
.core
|
||||
.storage
|
||||
.data
|
||||
.get_bitmap(BitmapKey::document_ids(account_id, collection))
|
||||
.await
|
||||
{
|
||||
|
@ -533,7 +320,9 @@ impl JMAP {
|
|||
) -> Result<Option<RoaringBitmap>, MethodError> {
|
||||
let property = property.as_ref();
|
||||
match self
|
||||
.store
|
||||
.core
|
||||
.storage
|
||||
.data
|
||||
.get_bitmap(BitmapKey {
|
||||
account_id,
|
||||
collection: collection.into(),
|
||||
|
@ -565,7 +354,7 @@ impl JMAP {
|
|||
collection: Collection,
|
||||
) -> Result<SetResponse, MethodError> {
|
||||
Ok(
|
||||
SetResponse::from_request(request, self.config.set_max_objects)?.with_state(
|
||||
SetResponse::from_request(request, self.core.jmap.set_max_objects)?.with_state(
|
||||
self.assert_state(
|
||||
request.account_id.document_id(),
|
||||
collection,
|
||||
|
@ -584,7 +373,9 @@ impl JMAP {
|
|||
Ok(if access_token.primary_id == account_id {
|
||||
access_token.quota as i64
|
||||
} else {
|
||||
self.directory
|
||||
self.core
|
||||
.storage
|
||||
.directory
|
||||
.query(QueryBy::Id(account_id), false)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
|
@ -602,7 +393,9 @@ impl JMAP {
|
|||
}
|
||||
|
||||
pub async fn get_used_quota(&self, account_id: u32) -> Result<i64, MethodError> {
|
||||
self.store
|
||||
self.core
|
||||
.storage
|
||||
.data
|
||||
.get_counter(DirectoryClass::UsedQuota(account_id))
|
||||
.await
|
||||
.map_err(|err| {
|
||||
|
@ -622,7 +415,9 @@ impl JMAP {
|
|||
collection: Collection,
|
||||
filters: Vec<Filter>,
|
||||
) -> Result<ResultSet, MethodError> {
|
||||
self.store
|
||||
self.core
|
||||
.storage
|
||||
.data
|
||||
.filter(account_id, collection, filters)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
|
@ -643,7 +438,9 @@ impl JMAP {
|
|||
collection: Collection,
|
||||
filters: Vec<FtsFilter<T>>,
|
||||
) -> Result<RoaringBitmap, MethodError> {
|
||||
self.fts_store
|
||||
self.core
|
||||
.storage
|
||||
.fts
|
||||
.query(account_id, collection, filters)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
|
@ -666,15 +463,15 @@ impl JMAP {
|
|||
let total = result_set.results.len() as usize;
|
||||
let (limit_total, limit) = if let Some(limit) = request.limit {
|
||||
if limit > 0 {
|
||||
let limit = std::cmp::min(limit, self.config.query_max_results);
|
||||
let limit = std::cmp::min(limit, self.core.jmap.query_max_results);
|
||||
(std::cmp::min(limit, total), limit)
|
||||
} else {
|
||||
(0, 0)
|
||||
}
|
||||
} else {
|
||||
(
|
||||
std::cmp::min(self.config.query_max_results, total),
|
||||
self.config.query_max_results,
|
||||
std::cmp::min(self.core.jmap.query_max_results, total),
|
||||
self.core.jmap.query_max_results,
|
||||
)
|
||||
};
|
||||
Ok((
|
||||
|
@ -718,7 +515,13 @@ impl JMAP {
|
|||
let collection = result_set.collection;
|
||||
let account_id = result_set.account_id;
|
||||
response.update_results(
|
||||
match self.store.sort(result_set, comparators, paginate).await {
|
||||
match self
|
||||
.core
|
||||
.storage
|
||||
.data
|
||||
.sort(result_set, comparators, paginate)
|
||||
.await
|
||||
{
|
||||
Ok(result) => result,
|
||||
Err(err) => {
|
||||
tracing::error!(event = "error",
|
||||
|
@ -736,7 +539,9 @@ impl JMAP {
|
|||
}
|
||||
|
||||
pub async fn write_batch(&self, batch: BatchBuilder) -> Result<(), MethodError> {
|
||||
self.store
|
||||
self.core
|
||||
.storage
|
||||
.data
|
||||
.write(batch.build())
|
||||
.await
|
||||
.map(|_| ())
|
||||
|
@ -764,6 +569,20 @@ impl JMAP {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<JmapInstance> for JMAP {
|
||||
fn from(value: JmapInstance) -> Self {
|
||||
let core = value.core.load().clone();
|
||||
JMAP {
|
||||
smtp: SMTP {
|
||||
core: core.clone(),
|
||||
inner: value.smtp_inner,
|
||||
},
|
||||
core,
|
||||
inner: value.jmap_inner,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
trait UpdateResults: Sized {
|
||||
fn update_results(&mut self, sorted_results: SortedResultSet) -> Result<(), MethodError>;
|
||||
}
|
||||
|
|
|
@ -40,7 +40,7 @@ impl JMAP {
|
|||
mut request: GetRequest<RequestArguments>,
|
||||
access_token: &AccessToken,
|
||||
) -> Result<GetResponse, MethodError> {
|
||||
let ids = request.unwrap_ids(self.config.get_max_objects)?;
|
||||
let ids = request.unwrap_ids(self.core.jmap.get_max_objects)?;
|
||||
let properties = request.unwrap_properties(&[
|
||||
Property::Id,
|
||||
Property::Name,
|
||||
|
@ -67,7 +67,7 @@ impl JMAP {
|
|||
} else {
|
||||
mailbox_ids
|
||||
.iter()
|
||||
.take(self.config.get_max_objects)
|
||||
.take(self.core.jmap.get_max_objects)
|
||||
.map(Into::into)
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
|
@ -338,7 +338,7 @@ impl JMAP {
|
|||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
if path.is_empty() || path.len() > self.config.mailbox_max_depth {
|
||||
if path.is_empty() || path.len() > self.core.jmap.mailbox_max_depth {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
|
|
|
@ -146,7 +146,7 @@ impl JMAP {
|
|||
let mut keep = false;
|
||||
let mut jmap_id = document_id + 1;
|
||||
|
||||
for _ in 0..self.config.mailbox_max_depth {
|
||||
for _ in 0..self.core.jmap.mailbox_max_depth {
|
||||
if let Some(&parent_id) = hierarchy.get(&jmap_id) {
|
||||
if parent_id == 0 {
|
||||
keep = true;
|
||||
|
@ -213,7 +213,7 @@ impl JMAP {
|
|||
let mut stack = Vec::new();
|
||||
let mut jmap_id = 0;
|
||||
|
||||
'outer: for _ in 0..(response.ids.len() * 10 * self.config.mailbox_max_depth) {
|
||||
'outer: for _ in 0..(response.ids.len() * 10 * self.core.jmap.mailbox_max_depth) {
|
||||
let (mut children, mut it) = if let Some(children) = tree.remove(&jmap_id) {
|
||||
(children, response.ids.iter())
|
||||
} else if let Some(prev) = stack.pop() {
|
||||
|
|
|
@ -136,7 +136,7 @@ impl JMAP {
|
|||
batch.create_document(document_id).custom(builder);
|
||||
changes.log_insert(Collection::Mailbox, document_id);
|
||||
ctx.mailbox_ids.insert(document_id);
|
||||
match self.store.write(batch.build()).await {
|
||||
match self.core.storage.data.write(batch.build()).await {
|
||||
Ok(_) => {
|
||||
ctx.response.created(id, document_id);
|
||||
}
|
||||
|
@ -235,7 +235,7 @@ impl JMAP {
|
|||
batch.update_document(document_id).custom(builder);
|
||||
|
||||
if !batch.is_empty() {
|
||||
match self.store.write(batch.build()).await {
|
||||
match self.core.storage.data.write(batch.build()).await {
|
||||
Ok(_) => {
|
||||
changes.log_update(Collection::Mailbox, document_id);
|
||||
}
|
||||
|
@ -403,7 +403,7 @@ impl JMAP {
|
|||
.assert_value(Property::MailboxIds, &mailbox_ids)
|
||||
.value(Property::MailboxIds, mailbox_ids.inner, F_VALUE)
|
||||
.value(Property::MailboxIds, document_id, F_BITMAP | F_CLEAR);
|
||||
match self.store.write(batch.build()).await {
|
||||
match self.core.storage.data.write(batch.build()).await {
|
||||
Ok(_) => changes.log_update(
|
||||
Collection::Email,
|
||||
Id::from_parts(thread_id, message_id),
|
||||
|
@ -497,7 +497,7 @@ impl JMAP {
|
|||
.value(Property::EmailIds, (), F_VALUE | F_CLEAR)
|
||||
.custom(ObjectIndexBuilder::new(SCHEMA).with_current(mailbox));
|
||||
|
||||
match self.store.write(batch.build()).await {
|
||||
match self.core.storage.data.write(batch.build()).await {
|
||||
Ok(_) => {
|
||||
changes.log_delete(Collection::Mailbox, document_id);
|
||||
Ok(Ok(did_remove_emails))
|
||||
|
@ -542,7 +542,7 @@ impl JMAP {
|
|||
let value = match (&property, value) {
|
||||
(Property::Name, MaybePatchValue::Value(Value::Text(value))) => {
|
||||
let value = value.trim();
|
||||
if !value.is_empty() && value.len() < self.config.mailbox_name_max_len {
|
||||
if !value.is_empty() && value.len() < self.core.jmap.mailbox_name_max_len {
|
||||
Value::Text(value.to_string())
|
||||
} else {
|
||||
return Ok(Err(SetError::invalid_properties()
|
||||
|
@ -633,7 +633,7 @@ impl JMAP {
|
|||
.map_or(u32::MAX, |(mailbox_id, _)| *mailbox_id + 1);
|
||||
let mut mailbox_parent_id = mailbox_parent_id.document_id();
|
||||
let mut success = false;
|
||||
for depth in 0..self.config.mailbox_max_depth {
|
||||
for depth in 0..self.core.jmap.mailbox_max_depth {
|
||||
if mailbox_parent_id == current_mailbox_id {
|
||||
return Ok(Err(SetError::invalid_properties()
|
||||
.with_property(Property::ParentId)
|
||||
|
@ -857,14 +857,19 @@ impl JMAP {
|
|||
);
|
||||
mailbox_ids.insert(mailbox_id);
|
||||
}
|
||||
self.store.write(batch.build()).await.map_err(|err| {
|
||||
tracing::error!(
|
||||
self.core
|
||||
.storage
|
||||
.data
|
||||
.write(batch.build())
|
||||
.await
|
||||
.map_err(|err| {
|
||||
tracing::error!(
|
||||
event = "error",
|
||||
context = "mailbox_get_or_create",
|
||||
error = ?err,
|
||||
"Failed to create mailboxes.");
|
||||
MethodError::ServerPartialFail
|
||||
})?;
|
||||
MethodError::ServerPartialFail
|
||||
})?;
|
||||
|
||||
Ok(mailbox_ids)
|
||||
}
|
||||
|
@ -903,7 +908,7 @@ impl JMAP {
|
|||
.with_collection(Collection::Mailbox);
|
||||
|
||||
for name in path {
|
||||
if name.len() > self.config.mailbox_name_max_len {
|
||||
if name.len() > self.core.jmap.mailbox_name_max_len {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
|
|
|
@ -36,7 +36,7 @@ impl JMAP {
|
|||
&self,
|
||||
mut request: GetRequest<RequestArguments>,
|
||||
) -> Result<GetResponse, MethodError> {
|
||||
let ids = request.unwrap_ids(self.config.get_max_objects)?;
|
||||
let ids = request.unwrap_ids(self.core.jmap.get_max_objects)?;
|
||||
let properties = request.unwrap_properties(&[
|
||||
Property::Id,
|
||||
Property::Type,
|
||||
|
@ -55,7 +55,7 @@ impl JMAP {
|
|||
} else {
|
||||
email_submission_ids
|
||||
.iter()
|
||||
.take(self.config.get_max_objects)
|
||||
.take(self.core.jmap.get_max_objects)
|
||||
.map(Into::into)
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
|
@ -69,6 +69,8 @@ impl JMAP {
|
|||
for id in ids {
|
||||
// Obtain the principal
|
||||
let principal = if let Some(principal) = self
|
||||
.core
|
||||
.storage
|
||||
.directory
|
||||
.query(QueryBy::Id(id.document_id()), false)
|
||||
.await
|
||||
|
|
|
@ -48,6 +48,8 @@ impl JMAP {
|
|||
match cond {
|
||||
Filter::Name(name) => {
|
||||
if let Some(principal) = self
|
||||
.core
|
||||
.storage
|
||||
.directory
|
||||
.query(QueryBy::Name(name.as_str()), false)
|
||||
.await
|
||||
|
@ -67,8 +69,8 @@ impl JMAP {
|
|||
Filter::Email(email) => {
|
||||
let mut ids = RoaringBitmap::new();
|
||||
for id in self
|
||||
.directory
|
||||
.email_to_ids(&email)
|
||||
.core
|
||||
.email_to_ids(&self.core.storage.directory, &email)
|
||||
.await
|
||||
.map_err(|_| MethodError::ServerPartialFail)?
|
||||
{
|
||||
|
|
|
@ -44,7 +44,7 @@ impl JMAP {
|
|||
mut request: GetRequest<RequestArguments>,
|
||||
access_token: &AccessToken,
|
||||
) -> Result<GetResponse, MethodError> {
|
||||
let ids = request.unwrap_ids(self.config.get_max_objects)?;
|
||||
let ids = request.unwrap_ids(self.core.jmap.get_max_objects)?;
|
||||
let properties = request.unwrap_properties(&[
|
||||
Property::Id,
|
||||
Property::DeviceClientId,
|
||||
|
@ -62,7 +62,7 @@ impl JMAP {
|
|||
} else {
|
||||
push_ids
|
||||
.iter()
|
||||
.take(self.config.get_max_objects)
|
||||
.take(self.core.jmap.get_max_objects)
|
||||
.map(Into::into)
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
|
@ -119,7 +119,9 @@ impl JMAP {
|
|||
pub async fn fetch_push_subscriptions(&self, account_id: u32) -> store::Result<state::Event> {
|
||||
let mut subscriptions = Vec::new();
|
||||
let document_ids = self
|
||||
.store
|
||||
.core
|
||||
.storage
|
||||
.data
|
||||
.get_bitmap(BitmapKey::document_ids(
|
||||
account_id,
|
||||
Collection::PushSubscription,
|
||||
|
@ -131,7 +133,9 @@ impl JMAP {
|
|||
|
||||
for document_id in document_ids {
|
||||
let mut subscription = self
|
||||
.store
|
||||
.core
|
||||
.storage
|
||||
.data
|
||||
.get_value::<Object<Value>>(ValueKey {
|
||||
account_id,
|
||||
collection: Collection::PushSubscription.into(),
|
||||
|
|
|
@ -25,9 +25,8 @@ use base64::{engine::general_purpose, Engine};
|
|||
use jmap_proto::types::id::Id;
|
||||
use store::ahash::{AHashMap, AHashSet};
|
||||
use tokio::sync::mpsc;
|
||||
use utils::{config::Config, UnwrapFailure};
|
||||
|
||||
use crate::{api::StateChangeResponse, services::IPC_CHANNEL_BUFFER, LONG_SLUMBER};
|
||||
use crate::{api::StateChangeResponse, services::IPC_CHANNEL_BUFFER, JmapInstance, LONG_SLUMBER};
|
||||
|
||||
use super::{ece::ece_encrypt, EncryptionKeys, Event, PushServer, PushUpdate};
|
||||
|
||||
|
@ -37,29 +36,10 @@ use std::{
|
|||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
pub fn spawn_push_manager(settings: &Config) -> mpsc::Sender<Event> {
|
||||
pub fn spawn_push_manager(core: JmapInstance) -> mpsc::Sender<Event> {
|
||||
let (push_tx_, mut push_rx) = mpsc::channel::<Event>(IPC_CHANNEL_BUFFER);
|
||||
let push_tx = push_tx_.clone();
|
||||
|
||||
let push_attempt_interval: Duration = settings
|
||||
.property_or_default("jmap.push.attempts.interval", "1m")
|
||||
.failed("Invalid configuration");
|
||||
let push_attempts_max: u32 = settings
|
||||
.property_or_default("jmap.push.attempts.max", "3")
|
||||
.failed("Invalid configuration");
|
||||
let push_retry_interval: Duration = settings
|
||||
.property_or_default("jmap.push.retry.interval", "1s")
|
||||
.failed("Invalid configuration");
|
||||
let push_timeout: Duration = settings
|
||||
.property_or_default("jmap.push.timeout.request", "10s")
|
||||
.failed("Invalid configuration");
|
||||
let push_verify_timeout: Duration = settings
|
||||
.property_or_default("jmap.push.timeout.verify", "1m")
|
||||
.failed("Invalid configuration");
|
||||
let push_throttle: Duration = settings
|
||||
.property_or_default("jmap.push.throttle", "1s")
|
||||
.failed("Invalid configuration");
|
||||
|
||||
tokio::spawn(async move {
|
||||
let mut subscriptions = AHashMap::default();
|
||||
let mut last_verify: AHashMap<u32, Instant> = AHashMap::default();
|
||||
|
@ -68,6 +48,15 @@ pub fn spawn_push_manager(settings: &Config) -> mpsc::Sender<Event> {
|
|||
let mut retry_ids = AHashSet::default();
|
||||
|
||||
loop {
|
||||
// Load settings
|
||||
let core_ = core.core.load();
|
||||
let push_attempt_interval = core_.jmap.push_attempt_interval;
|
||||
let push_attempts_max = core_.jmap.push_attempts_max;
|
||||
let push_retry_interval = core_.jmap.push_retry_interval;
|
||||
let push_timeout = core_.jmap.push_timeout;
|
||||
let push_verify_timeout = core_.jmap.push_verify_timeout;
|
||||
let push_throttle = core_.jmap.push_throttle;
|
||||
|
||||
match tokio::time::timeout(retry_timeout, push_rx.recv()).await {
|
||||
Ok(Some(event)) => match event {
|
||||
Event::Update { updates } => {
|
||||
|
|
|
@ -56,14 +56,14 @@ impl JMAP {
|
|||
.get_document_ids(account_id, Collection::PushSubscription)
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
let mut response = SetResponse::from_request(&request, self.config.set_max_objects)?;
|
||||
let mut response = SetResponse::from_request(&request, self.core.jmap.set_max_objects)?;
|
||||
let will_destroy = request.unwrap_destroy();
|
||||
|
||||
// Process creates
|
||||
'create: for (id, object) in request.unwrap_create() {
|
||||
let mut push = Object::with_capacity(object.properties.len());
|
||||
|
||||
if push_ids.len() as usize >= self.config.push_max_total {
|
||||
if push_ids.len() as usize >= self.core.jmap.push_max_total {
|
||||
response.not_created.append(id, SetError::forbidden().with_description(
|
||||
"There are too many subscriptions, please delete some before adding a new one.",
|
||||
));
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue