Settings hot reloading - Part 3

This commit is contained in:
mdecimus 2024-03-24 19:20:36 +01:00
parent 5756815e3e
commit d8af9b4576
303 changed files with 4201 additions and 17262 deletions

243
Cargo.lock generated
View file

@ -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"

View file

@ -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)
}
}

View file

@ -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

View file

@ -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)| {

View file

@ -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))
}
}

View 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
}
}

View file

@ -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) {

View file

@ -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,

View file

@ -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

View file

@ -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 {

View file

@ -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 {

View file

@ -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()
}
}

View file

@ -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(),

View file

@ -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);
}
}

View file

@ -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>,
}

View file

@ -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();

View file

@ -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> {

View file

@ -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()
}
}

View file

@ -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
}
}

View file

@ -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()),

View file

@ -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!(

View file

@ -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))
}
}

View file

@ -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 {

View file

@ -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()

View file

@ -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"] }

View file

@ -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
})
}
}

View file

@ -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

View file

@ -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
{

View file

@ -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)]

View file

@ -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,

View file

@ -30,7 +30,6 @@ use tokio::{
};
use tokio_rustls::server::TlsStream;
use tracing::debug;
use utils::listener::SessionStream;
use super::{Session, SessionData};

View file

@ -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),
}
}
}

View file

@ -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)

View file

@ -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
{

View file

@ -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,
};

View file

@ -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(

View file

@ -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 {

View file

@ -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(

View file

@ -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(

View file

@ -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 {

View file

@ -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() {

View file

@ -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;

View file

@ -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;

View file

@ -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 {

View file

@ -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 {

View file

@ -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 {

View file

@ -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 {

View file

@ -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
},

View file

@ -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 {

View file

@ -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 {

View file

@ -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);
}

View file

@ -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;

View file

@ -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 {

View file

@ -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;

View file

@ -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(

View file

@ -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,

View file

@ -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

View file

@ -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"] }

View file

@ -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}")

View file

@ -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)
}
}

View file

@ -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

View file

@ -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 { "" }))
}
}

View file

@ -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 }
}
}

View file

@ -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(

View file

@ -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"],
}
}
}

View file

@ -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

View file

@ -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()??,

View file

@ -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"))

View file

@ -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![

View file

@ -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

View file

@ -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(),

View file

@ -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(())
}
}

View file

@ -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,

View file

@ -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| {

View file

@ -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,

View file

@ -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| {

View file

@ -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| {

View file

@ -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",

View file

@ -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)
}

View file

@ -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
};

View file

@ -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))
}

View file

@ -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};

View file

@ -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

View file

@ -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
{

View file

@ -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)

View file

@ -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(|| {

View file

@ -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();

View file

@ -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))
}

View file

@ -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);
}

View file

@ -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)
}

View file

@ -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

View file

@ -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>;
}

View file

@ -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);
}

View file

@ -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() {

View file

@ -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);
}

View file

@ -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

View file

@ -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)?
{

View file

@ -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(),

View file

@ -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 } => {

View file

@ -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