summaryrefslogtreecommitdiff
path: root/crates
diff options
context:
space:
mode:
authormdecimus <mauro@stalw.art>2024-09-26 14:49:46 +0200
committermdecimus <mauro@stalw.art>2024-09-26 14:49:46 +0200
commitce8182ae07d3f5d341f0e58d3286e3ee20295c12 (patch)
tree8a6865308f5985eb68a589d64a79bd85e665e7e2 /crates
parent24967c1e86ab4333736c5e3d69ea4bb3af7f56f0 (diff)
Core refactoring
Diffstat (limited to 'crates')
-rw-r--r--crates/common/Cargo.toml2
-rw-r--r--crates/common/src/addresses.rs18
-rw-r--r--crates/common/src/auth/access_token.rs25
-rw-r--r--crates/common/src/auth/roles.rs17
-rw-r--r--crates/common/src/config/inner.rs163
-rw-r--r--crates/common/src/config/mod.rs24
-rw-r--r--crates/common/src/config/network.rs7
-rw-r--r--crates/common/src/config/scripts.rs39
-rw-r--r--crates/common/src/config/server/listener.rs16
-rw-r--r--crates/common/src/config/server/mod.rs10
-rw-r--r--crates/common/src/config/server/tls.rs33
-rw-r--r--crates/common/src/config/smtp/resolver.rs30
-rw-r--r--crates/common/src/core.rs335
-rw-r--r--crates/common/src/enterprise/alerts.rs6
-rw-r--r--crates/common/src/enterprise/mod.rs36
-rw-r--r--crates/common/src/expr/eval.rs8
-rw-r--r--crates/common/src/expr/functions/asynch.rs22
-rw-r--r--crates/common/src/expr/functions/mod.rs2
-rw-r--r--crates/common/src/ipc.rs233
-rw-r--r--crates/common/src/lib.rs529
-rw-r--r--crates/common/src/listener/acme/cache.rs8
-rw-r--r--crates/common/src/listener/acme/mod.rs14
-rw-r--r--crates/common/src/listener/acme/order.rs12
-rw-r--r--crates/common/src/listener/acme/resolver.rs41
-rw-r--r--crates/common/src/listener/blocked.rs238
-rw-r--r--crates/common/src/listener/listen.rs39
-rw-r--r--crates/common/src/listener/mod.rs4
-rw-r--r--crates/common/src/listener/tls.rs40
-rw-r--r--crates/common/src/manager/boot.rs66
-rw-r--r--crates/common/src/manager/reload.rs94
-rw-r--r--crates/common/src/scripts/plugins/bayes.rs18
-rw-r--r--crates/common/src/scripts/plugins/dns.rs31
-rw-r--r--crates/common/src/scripts/plugins/lookup.rs31
-rw-r--r--crates/common/src/scripts/plugins/mod.rs5
-rw-r--r--crates/common/src/scripts/plugins/query.rs4
-rw-r--r--crates/common/src/telemetry/metrics/otel.rs12
-rw-r--r--crates/common/src/telemetry/metrics/prometheus.rs4
-rw-r--r--crates/directory/src/backend/internal/mod.rs34
-rw-r--r--crates/imap/src/core/client.rs26
-rw-r--r--crates/imap/src/core/mailbox.rs95
-rw-r--r--crates/imap/src/core/message.rs22
-rw-r--r--crates/imap/src/core/mod.rs103
-rw-r--r--crates/imap/src/core/session.rs20
-rw-r--r--crates/imap/src/lib.rs41
-rw-r--r--crates/imap/src/op/acl.rs27
-rw-r--r--crates/imap/src/op/append.rs18
-rw-r--r--crates/imap/src/op/authenticate.rs15
-rw-r--r--crates/imap/src/op/capability.rs2
-rw-r--r--crates/imap/src/op/copy_move.rs33
-rw-r--r--crates/imap/src/op/create.rs26
-rw-r--r--crates/imap/src/op/delete.rs7
-rw-r--r--crates/imap/src/op/expunge.rs32
-rw-r--r--crates/imap/src/op/fetch.rs28
-rw-r--r--crates/imap/src/op/idle.rs7
-rw-r--r--crates/imap/src/op/list.rs4
-rw-r--r--crates/imap/src/op/namespace.rs2
-rw-r--r--crates/imap/src/op/rename.rs15
-rw-r--r--crates/imap/src/op/search.rs42
-rw-r--r--crates/imap/src/op/select.rs60
-rw-r--r--crates/imap/src/op/status.rs23
-rw-r--r--crates/imap/src/op/store.rs24
-rw-r--r--crates/imap/src/op/subscribe.rs15
-rw-r--r--crates/imap/src/op/thread.rs3
-rw-r--r--crates/jmap/src/api/autoconfig.rs26
-rw-r--r--crates/jmap/src/api/event_source.rs17
-rw-r--r--crates/jmap/src/api/http.rs368
-rw-r--r--crates/jmap/src/api/management/dkim.rs40
-rw-r--r--crates/jmap/src/api/management/dns.rs38
-rw-r--r--crates/jmap/src/api/management/enterprise/telemetry.rs23
-rw-r--r--crates/jmap/src/api/management/enterprise/undelete.rs22
-rw-r--r--crates/jmap/src/api/management/log.rs20
-rw-r--r--crates/jmap/src/api/management/mod.rs31
-rw-r--r--crates/jmap/src/api/management/principal.rs74
-rw-r--r--crates/jmap/src/api/management/queue.rs46
-rw-r--r--crates/jmap/src/api/management/reload.rs56
-rw-r--r--crates/jmap/src/api/management/report.rs22
-rw-r--r--crates/jmap/src/api/management/settings.rs22
-rw-r--r--crates/jmap/src/api/management/sieve.rs26
-rw-r--r--crates/jmap/src/api/management/stores.rs58
-rw-r--r--crates/jmap/src/api/mod.rs9
-rw-r--r--crates/jmap/src/api/request.rs47
-rw-r--r--crates/jmap/src/api/session.rs17
-rw-r--r--crates/jmap/src/auth/acl.rs97
-rw-r--r--crates/jmap/src/auth/authenticate.rs148
-rw-r--r--crates/jmap/src/auth/oauth/auth.rs25
-rw-r--r--crates/jmap/src/auth/oauth/mod.rs2
-rw-r--r--crates/jmap/src/auth/oauth/token.rs56
-rw-r--r--crates/jmap/src/auth/rate_limit.rs47
-rw-r--r--crates/jmap/src/blob/copy.rs19
-rw-r--r--crates/jmap/src/blob/download.rs45
-rw-r--r--crates/jmap/src/blob/get.rs26
-rw-r--r--crates/jmap/src/blob/upload.rs43
-rw-r--r--crates/jmap/src/changes/get.rs24
-rw-r--r--crates/jmap/src/changes/query.rs22
-rw-r--r--crates/jmap/src/changes/state.rs25
-rw-r--r--crates/jmap/src/changes/write.rs37
-rw-r--r--crates/jmap/src/email/cache.rs33
-rw-r--r--crates/jmap/src/email/copy.rs47
-rw-r--r--crates/jmap/src/email/crypto.rs26
-rw-r--r--crates/jmap/src/email/delete.rs44
-rw-r--r--crates/jmap/src/email/get.rs21
-rw-r--r--crates/jmap/src/email/import.rs23
-rw-r--r--crates/jmap/src/email/ingest.rs37
-rw-r--r--crates/jmap/src/email/parse.rs17
-rw-r--r--crates/jmap/src/email/query.rs26
-rw-r--r--crates/jmap/src/email/set.rs28
-rw-r--r--crates/jmap/src/email/snippet.rs17
-rw-r--r--crates/jmap/src/identity/get.rs22
-rw-r--r--crates/jmap/src/identity/set.rs15
-rw-r--r--crates/jmap/src/lib.rs287
-rw-r--r--crates/jmap/src/mailbox/get.rs72
-rw-r--r--crates/jmap/src/mailbox/query.rs19
-rw-r--r--crates/jmap/src/mailbox/set.rs59
-rw-r--r--crates/jmap/src/principal/get.rs15
-rw-r--r--crates/jmap/src/principal/query.rs17
-rw-r--r--crates/jmap/src/push/get.rs32
-rw-r--r--crates/jmap/src/push/manager.rs21
-rw-r--r--crates/jmap/src/push/mod.rs30
-rw-r--r--crates/jmap/src/push/set.rs17
-rw-r--r--crates/jmap/src/quota/get.rs17
-rw-r--r--crates/jmap/src/quota/query.rs15
-rw-r--r--crates/jmap/src/quota/set.rs14
-rw-r--r--crates/jmap/src/services/delivery.rs10
-rw-r--r--crates/jmap/src/services/gossip/mod.rs26
-rw-r--r--crates/jmap/src/services/gossip/ping.rs30
-rw-r--r--crates/jmap/src/services/gossip/spawn.rs7
-rw-r--r--crates/jmap/src/services/housekeeper.rs248
-rw-r--r--crates/jmap/src/services/index.rs45
-rw-r--r--crates/jmap/src/services/ingest.rs38
-rw-r--r--crates/jmap/src/services/state.rs99
-rw-r--r--crates/jmap/src/sieve/get.rs44
-rw-r--r--crates/jmap/src/sieve/ingest.rs32
-rw-r--r--crates/jmap/src/sieve/query.rs15
-rw-r--r--crates/jmap/src/sieve/set.rs53
-rw-r--r--crates/jmap/src/sieve/validate.rs17
-rw-r--r--crates/jmap/src/submission/get.rs18
-rw-r--r--crates/jmap/src/submission/query.rs15
-rw-r--r--crates/jmap/src/submission/set.rs43
-rw-r--r--crates/jmap/src/thread/get.rs15
-rw-r--r--crates/jmap/src/vacation/get.rs22
-rw-r--r--crates/jmap/src/vacation/set.rs25
-rw-r--r--crates/jmap/src/websocket/stream.rs23
-rw-r--r--crates/jmap/src/websocket/upgrade.rs23
-rw-r--r--crates/main/src/main.rs80
-rw-r--r--crates/managesieve/src/core/client.rs8
-rw-r--r--crates/managesieve/src/core/mod.rs12
-rw-r--r--crates/managesieve/src/core/session.rs20
-rw-r--r--crates/managesieve/src/op/authenticate.rs35
-rw-r--r--crates/managesieve/src/op/capability.rs6
-rw-r--r--crates/managesieve/src/op/checkscript.rs2
-rw-r--r--crates/managesieve/src/op/deletescript.rs5
-rw-r--r--crates/managesieve/src/op/getscript.rs6
-rw-r--r--crates/managesieve/src/op/havespace.rs3
-rw-r--r--crates/managesieve/src/op/listscripts.rs5
-rw-r--r--crates/managesieve/src/op/putscript.rs32
-rw-r--r--crates/managesieve/src/op/renamescript.rs8
-rw-r--r--crates/managesieve/src/op/setactive.rs5
-rw-r--r--crates/pop3/src/client.rs6
-rw-r--r--crates/pop3/src/lib.rs12
-rw-r--r--crates/pop3/src/mailbox.rs15
-rw-r--r--crates/pop3/src/op/authenticate.rs35
-rw-r--r--crates/pop3/src/op/delete.rs13
-rw-r--r--crates/pop3/src/op/fetch.rs6
-rw-r--r--crates/pop3/src/op/mod.rs4
-rw-r--r--crates/pop3/src/session.rs16
-rw-r--r--crates/smtp/src/core/mod.rs103
-rw-r--r--crates/smtp/src/core/params.rs95
-rw-r--r--crates/smtp/src/core/throttle.rs86
-rw-r--r--crates/smtp/src/inbound/auth.rs6
-rw-r--r--crates/smtp/src/inbound/data.rs109
-rw-r--r--crates/smtp/src/inbound/ehlo.rs52
-rw-r--r--crates/smtp/src/inbound/hooks/message.rs5
-rw-r--r--crates/smtp/src/inbound/mail.rs51
-rw-r--r--crates/smtp/src/inbound/milter/message.rs9
-rw-r--r--crates/smtp/src/inbound/rcpt.rs44
-rw-r--r--crates/smtp/src/inbound/session.rs5
-rw-r--r--crates/smtp/src/inbound/spawn.rs37
-rw-r--r--crates/smtp/src/inbound/vrfy.rs20
-rw-r--r--crates/smtp/src/lib.rs73
-rw-r--r--crates/smtp/src/outbound/client.rs6
-rw-r--r--crates/smtp/src/outbound/dane/dnssec.rs28
-rw-r--r--crates/smtp/src/outbound/delivery.rs434
-rw-r--r--crates/smtp/src/outbound/local.rs2
-rw-r--r--crates/smtp/src/outbound/lookup.rs39
-rw-r--r--crates/smtp/src/outbound/mod.rs13
-rw-r--r--crates/smtp/src/outbound/mta_sts/lookup.rs26
-rw-r--r--crates/smtp/src/outbound/session.rs8
-rw-r--r--crates/smtp/src/queue/dsn.rs38
-rw-r--r--crates/smtp/src/queue/manager.rs38
-rw-r--r--crates/smtp/src/queue/mod.rs19
-rw-r--r--crates/smtp/src/queue/quota.rs24
-rw-r--r--crates/smtp/src/queue/spool.rs142
-rw-r--r--crates/smtp/src/queue/throttle.rs22
-rw-r--r--crates/smtp/src/reporting/analysis.rs13
-rw-r--r--crates/smtp/src/reporting/dkim.rs15
-rw-r--r--crates/smtp/src/reporting/dmarc.rs117
-rw-r--r--crates/smtp/src/reporting/mod.rs153
-rw-r--r--crates/smtp/src/reporting/scheduler.rs93
-rw-r--r--crates/smtp/src/reporting/spf.rs15
-rw-r--r--crates/smtp/src/reporting/tls.rs65
-rw-r--r--crates/smtp/src/scripts/event_loop.rs27
-rw-r--r--crates/smtp/src/scripts/exec.rs6
-rw-r--r--crates/smtp/src/scripts/mod.rs17
-rw-r--r--crates/utils/src/snowflake.rs10
204 files changed, 5093 insertions, 3643 deletions
diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml
index 6f30e404..6dac6a47 100644
--- a/crates/common/Cargo.toml
+++ b/crates/common/Cargo.toml
@@ -11,6 +11,7 @@ store = { path = "../store" }
trc = { path = "../trc" }
directory = { path = "../directory" }
jmap_proto = { path = "../jmap-proto" }
+imap_proto = { path = "../imap-proto" }
sieve-rs = { version = "0.5" }
mail-parser = { version = "0.9", features = ["full_encoding", "ludicrous_mode"] }
mail-builder = { version = "0.3", features = ["ludicrous_mode"] }
@@ -59,6 +60,7 @@ zip = "2.1"
pwhash = "1.0.0"
xxhash-rust = { version = "0.8.5", features = ["xxh3"] }
psl = "2"
+dashmap = "6.0"
[target.'cfg(unix)'.dependencies]
privdrop = "0.5.3"
diff --git a/crates/common/src/addresses.rs b/crates/common/src/addresses.rs
index 0303b7fc..d56385b1 100644
--- a/crates/common/src/addresses.rs
+++ b/crates/common/src/addresses.rs
@@ -14,10 +14,10 @@ use crate::{
expr::{
functions::ResolveVariable, if_block::IfBlock, tokenizer::TokenMap, Variable, V_RECIPIENT,
},
- Core,
+ Server,
};
-impl Core {
+impl Server {
pub async fn email_to_ids(
&self,
directory: &Directory,
@@ -25,6 +25,7 @@ impl Core {
session_id: u64,
) -> trc::Result<Vec<u32>> {
let mut address = self
+ .core
.smtp
.session
.rcpt
@@ -38,6 +39,7 @@ impl Core {
if !result.is_empty() {
return Ok(result);
} else if let Some(catch_all) = self
+ .core
.smtp
.session
.rcpt
@@ -62,6 +64,7 @@ impl Core {
) -> trc::Result<bool> {
// Expand subaddress
let mut address = self
+ .core
.smtp
.session
.rcpt
@@ -73,6 +76,7 @@ impl Core {
if directory.rcpt(address.as_ref()).await? {
return Ok(true);
} else if let Some(catch_all) = self
+ .core
.smtp
.session
.rcpt
@@ -97,7 +101,8 @@ impl Core {
) -> trc::Result<Vec<String>> {
directory
.vrfy(
- self.smtp
+ self.core
+ .smtp
.session
.rcpt
.subaddressing
@@ -116,7 +121,8 @@ impl Core {
) -> trc::Result<Vec<String>> {
directory
.expn(
- self.smtp
+ self.core
+ .smtp
.session
.rcpt
.subaddressing
@@ -170,7 +176,7 @@ impl ResolveVariable for Address<'_> {
impl AddressMapping {
pub async fn to_subaddress<'x, 'y: 'x>(
&'x self,
- core: &Core,
+ core: &Server,
address: &'y str,
session_id: u64,
) -> Cow<'x, str> {
@@ -198,7 +204,7 @@ impl AddressMapping {
pub async fn to_catch_all<'x, 'y: 'x>(
&'x self,
- core: &Core,
+ core: &Server,
address: &'y str,
session_id: u64,
) -> Option<Cow<'x, str>> {
diff --git a/crates/common/src/auth/access_token.rs b/crates/common/src/auth/access_token.rs
index 7ff882e4..113c33d8 100644
--- a/crates/common/src/auth/access_token.rs
+++ b/crates/common/src/auth/access_token.rs
@@ -25,11 +25,11 @@ use utils::map::{
vec_map::VecMap,
};
-use crate::Core;
+use crate::Server;
use super::{roles::RolePermissions, AccessToken, ResourceToken, TenantInfo};
-impl Core {
+impl Server {
pub async fn build_access_token(&self, mut principal: Principal) -> trc::Result<AccessToken> {
let mut role_permissions = RolePermissions::default();
@@ -75,8 +75,7 @@ impl Core {
tenant = Some(TenantInfo {
id: tenant_id,
quota: self
- .storage
- .data
+ .store()
.query(QueryBy::Id(tenant_id), false)
.await
.caused_by(trc::location!())?
@@ -111,12 +110,7 @@ impl Core {
}
pub async fn get_access_token(&self, account_id: u32) -> trc::Result<AccessToken> {
- let err = match self
- .storage
- .directory
- .query(QueryBy::Id(account_id), true)
- .await
- {
+ let err = match self.directory().query(QueryBy::Id(account_id), true).await {
Ok(Some(principal)) => {
return self
.update_access_token(self.build_access_token(principal).await?)
@@ -129,7 +123,7 @@ impl Core {
Err(err) => Err(err),
};
- match &self.jmap.fallback_admin {
+ match &self.core.jmap.fallback_admin {
Some((_, secret)) if account_id == u32::MAX => {
self.update_access_token(
self.build_access_token(Principal::fallback_admin(secret))
@@ -150,8 +144,7 @@ impl Core {
.chain(access_token.member_of.iter().copied())
{
for acl_item in self
- .storage
- .data
+ .store()
.acl_query(AclQuery::HasAccess { grant_account_id })
.await
.caused_by(trc::location!())?
@@ -191,15 +184,15 @@ impl Core {
}
pub fn cache_access_token(&self, access_token: Arc<AccessToken>) {
- self.security.access_tokens.insert_with_ttl(
+ self.inner.data.access_tokens.insert_with_ttl(
access_token.primary_id(),
access_token,
- Instant::now() + self.jmap.session_cache_ttl,
+ Instant::now() + self.core.jmap.session_cache_ttl,
);
}
pub async fn get_cached_access_token(&self, primary_id: u32) -> trc::Result<Arc<AccessToken>> {
- if let Some(access_token) = self.security.access_tokens.get_with_ttl(&primary_id) {
+ if let Some(access_token) = self.inner.data.access_tokens.get_with_ttl(&primary_id) {
Ok(access_token)
} else {
// Refresh ACL token
diff --git a/crates/common/src/auth/roles.rs b/crates/common/src/auth/roles.rs
index 79a59cfd..1180ed79 100644
--- a/crates/common/src/auth/roles.rs
+++ b/crates/common/src/auth/roles.rs
@@ -13,7 +13,7 @@ use directory::{
};
use trc::AddContext;
-use crate::Core;
+use crate::Server;
#[derive(Debug, Clone, Default)]
pub struct RolePermissions {
@@ -26,14 +26,14 @@ static ADMIN_PERMISSIONS: LazyLock<Arc<RolePermissions>> = LazyLock::new(admin_p
static TENANT_ADMIN_PERMISSIONS: LazyLock<Arc<RolePermissions>> =
LazyLock::new(tenant_admin_permissions);
-impl Core {
+impl Server {
pub async fn get_role_permissions(&self, role_id: u32) -> trc::Result<Arc<RolePermissions>> {
match role_id {
ROLE_USER => Ok(USER_PERMISSIONS.clone()),
ROLE_ADMIN => Ok(ADMIN_PERMISSIONS.clone()),
ROLE_TENANT_ADMIN => Ok(TENANT_ADMIN_PERMISSIONS.clone()),
role_id => {
- if let Some(role_permissions) = self.security.permissions.get(&role_id) {
+ if let Some(role_permissions) = self.inner.data.permissions.get(&role_id) {
Ok(role_permissions.clone())
} else {
self.build_role_permissions(role_id).await
@@ -81,15 +81,14 @@ impl Core {
}
role_id => {
// Try with the cache
- if let Some(role_permissions) = self.security.permissions.get(&role_id) {
+ if let Some(role_permissions) = self.inner.data.permissions.get(&role_id) {
return_permissions.union(role_permissions.as_ref());
} else {
let mut role_permissions = RolePermissions::default();
// Obtain principal
let mut principal = self
- .storage
- .data
+ .store()
.query(QueryBy::Id(role_id), true)
.await
.caused_by(trc::location!())?
@@ -133,7 +132,8 @@ impl Core {
role_ids = parent_role_ids.into_iter();
} else {
// Cache role
- self.security
+ self.inner
+ .data
.permissions
.insert(role_id, Arc::new(role_permissions));
}
@@ -149,7 +149,8 @@ impl Core {
// Cache role
let return_permissions = Arc::new(return_permissions);
- self.security
+ self.inner
+ .data
.permissions
.insert(role_id, return_permissions.clone());
Ok(return_permissions)
diff --git a/crates/common/src/config/inner.rs b/crates/common/src/config/inner.rs
new file mode 100644
index 00000000..92528e26
--- /dev/null
+++ b/crates/common/src/config/inner.rs
@@ -0,0 +1,163 @@
+/*
+ * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art>
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
+ */
+
+use std::{sync::Arc, time::Duration};
+
+use ahash::{AHashMap, AHashSet, RandomState};
+use arc_swap::ArcSwap;
+use dashmap::DashMap;
+use mail_send::smtp::tls::build_tls_connector;
+use nlp::bayes::cache::BayesTokenCache;
+use parking_lot::RwLock;
+use utils::{
+ config::Config,
+ lru_cache::{LruCache, LruCached},
+ map::ttl_dashmap::{TtlDashMap, TtlMap},
+ snowflake::SnowflakeIdGenerator,
+};
+
+use crate::{
+ listener::blocked::BlockedIps, manager::webadmin::WebAdminManager, Data,
+ ThrottleKeyHasherBuilder, TlsConnectors,
+};
+
+use super::server::tls::{build_self_signed_cert, parse_certificates};
+
+impl Data {
+ pub fn parse(config: &mut Config) -> Self {
+ // Parse certificates
+ let mut certificates = AHashMap::new();
+ let mut subject_names = AHashSet::new();
+ parse_certificates(config, &mut certificates, &mut subject_names);
+ if subject_names.is_empty() {
+ subject_names.insert("localhost".to_string());
+ }
+
+ // Parse capacities
+ let shard_amount = config
+ .property::<u64>("cache.shard")
+ .unwrap_or(32)
+ .next_power_of_two() as usize;
+ let capacity = config.property("cache.capacity").unwrap_or(100);
+
+ // Parse id generator
+ let id_generator = config
+ .property::<u64>("cluster.node-id")
+ .map(SnowflakeIdGenerator::with_node_id)
+ .unwrap_or_default();
+
+ Data {
+ tls_certificates: ArcSwap::from_pointee(certificates),
+ tls_self_signed_cert: build_self_signed_cert(
+ subject_names.into_iter().collect::<Vec<_>>(),
+ )
+ .or_else(|err| {
+ config.new_build_error("certificate.self-signed", err);
+ build_self_signed_cert(vec!["localhost".to_string()])
+ })
+ .ok()
+ .map(Arc::new),
+ access_tokens: TtlDashMap::with_capacity(capacity, shard_amount),
+ http_auth_cache: TtlDashMap::with_capacity(capacity, shard_amount),
+ blocked_ips: RwLock::new(BlockedIps::parse(config).blocked_ip_addresses),
+ blocked_ips_version: 0.into(),
+ permissions: Default::default(),
+ permissions_version: 0.into(),
+ jmap_id_gen: id_generator.clone(),
+ queue_id_gen: id_generator.clone(),
+ span_id_gen: id_generator,
+ webadmin: WebAdminManager::new(),
+ config_version: 0.into(),
+ jmap_limiter: DashMap::with_capacity_and_hasher_and_shard_amount(
+ capacity,
+ RandomState::default(),
+ shard_amount,
+ ),
+ imap_limiter: DashMap::with_capacity_and_hasher_and_shard_amount(
+ capacity,
+ RandomState::default(),
+ shard_amount,
+ ),
+ account_cache: LruCache::with_capacity(
+ config.property("cache.account.size").unwrap_or(2048),
+ ),
+ mailbox_cache: LruCache::with_capacity(
+ config.property("cache.mailbox.size").unwrap_or(2048),
+ ),
+ threads_cache: LruCache::with_capacity(
+ config.property("cache.thread.size").unwrap_or(2048),
+ ),
+ logos: Default::default(),
+ smtp_session_throttle: DashMap::with_capacity_and_hasher_and_shard_amount(
+ capacity,
+ ThrottleKeyHasherBuilder::default(),
+ shard_amount,
+ ),
+ smtp_queue_throttle: DashMap::with_capacity_and_hasher_and_shard_amount(
+ capacity,
+ ThrottleKeyHasherBuilder::default(),
+ shard_amount,
+ ),
+ smtp_connectors: TlsConnectors::default(),
+ bayes_cache: BayesTokenCache::new(
+ config
+ .property_or_default("cache.bayes.capacity", "8192")
+ .unwrap_or(8192),
+ config
+ .property_or_default("cache.bayes.ttl.positive", "1h")
+ .unwrap_or_else(|| Duration::from_secs(3600)),
+ config
+ .property_or_default("cache.bayes.ttl.negative", "1h")
+ .unwrap_or_else(|| Duration::from_secs(3600)),
+ ),
+ remote_lists: Default::default(),
+ }
+ }
+}
+
+impl Default for Data {
+ fn default() -> Self {
+ Self {
+ tls_certificates: Default::default(),
+ tls_self_signed_cert: Default::default(),
+ access_tokens: Default::default(),
+ http_auth_cache: Default::default(),
+ blocked_ips: Default::default(),
+ blocked_ips_version: 0.into(),
+ permissions: Default::default(),
+ permissions_version: 0.into(),
+ remote_lists: Default::default(),
+ jmap_id_gen: Default::default(),
+ queue_id_gen: Default::default(),
+ span_id_gen: Default::default(),
+ webadmin: Default::default(),
+ config_version: Default::default(),
+ jmap_limiter: Default::default(),
+ imap_limiter: Default::default(),
+ account_cache: LruCache::with_capacity(2048),
+ mailbox_cache: LruCache::with_capacity(2048),
+ threads_cache: LruCache::with_capacity(2048),
+ logos: Default::default(),
+ smtp_session_throttle: Default::default(),
+ smtp_queue_throttle: Default::default(),
+ smtp_connectors: Default::default(),
+ bayes_cache: BayesTokenCache::new(
+ 8192,
+ Duration::from_secs(3600),
+ Duration::from_secs(3600),
+ ),
+ }
+ }
+}
+
+impl Default for TlsConnectors {
+ fn default() -> Self {
+ TlsConnectors {
+ pki_verify: build_tls_connector(false),
+ dummy_verify: build_tls_connector(true),
+ }
+ }
+}
diff --git a/crates/common/src/config/mod.rs b/crates/common/src/config/mod.rs
index edd5ed29..79b7c059 100644
--- a/crates/common/src/config/mod.rs
+++ b/crates/common/src/config/mod.rs
@@ -10,13 +10,10 @@ use arc_swap::ArcSwap;
use directory::{Directories, Directory};
use store::{BlobBackend, BlobStore, FtsStore, LookupStore, Store, Stores};
use telemetry::Metrics;
-use utils::{
- config::Config,
- map::ttl_dashmap::{ADashMap, TtlDashMap, TtlMap},
-};
+use utils::config::Config;
use crate::{
- expr::*, listener::tls::TlsManager, manager::config::ConfigManager, Core, Network, Security,
+ expr::*, listener::tls::AcmeProviders, manager::config::ConfigManager, Core, Network, Security,
};
use self::{
@@ -25,6 +22,7 @@ use self::{
};
pub mod imap;
+pub mod inner;
pub mod jmap;
pub mod network;
pub mod scripts;
@@ -165,18 +163,8 @@ impl Core {
smtp: SmtpConfig::parse(config).await,
jmap: JmapConfig::parse(config),
imap: ImapConfig::parse(config),
- tls: TlsManager::parse(config),
+ acme: AcmeProviders::parse(config),
metrics: Metrics::parse(config),
- security: Security {
- access_tokens: TtlDashMap::with_capacity(100, 32),
- permissions: ADashMap::with_capacity_and_hasher_and_shard_amount(
- 100,
- ahash::RandomState::new(),
- 32,
- ),
- permissions_version: Default::default(),
- logos: Default::default(),
- },
storage: Storage {
data,
blob,
@@ -194,7 +182,7 @@ impl Core {
}
}
- pub fn into_shared(self) -> Arc<ArcSwap<Self>> {
- Arc::new(ArcSwap::from_pointee(self))
+ pub fn into_shared(self) -> ArcSwap<Self> {
+ ArcSwap::from_pointee(self)
}
}
diff --git a/crates/common/src/config/network.rs b/crates/common/src/config/network.rs
index c3829dbf..b7c3faa5 100644
--- a/crates/common/src/config/network.rs
+++ b/crates/common/src/config/network.rs
@@ -6,7 +6,6 @@
use crate::{
expr::{if_block::IfBlock, tokenizer::TokenMap},
- listener::blocked::{AllowedIps, BlockedIps},
Network,
};
use utils::config::Config;
@@ -30,8 +29,7 @@ pub(crate) const HTTP_VARS: &[u32; 11] = &[
impl Default for Network {
fn default() -> Self {
Self {
- blocked_ips: Default::default(),
- allowed_ips: Default::default(),
+ security: Default::default(),
node_id: 0,
http_response_url: IfBlock::new::<()>(
"server.http.url",
@@ -47,8 +45,7 @@ impl Network {
pub fn parse(config: &mut Config) -> Self {
let mut network = Network {
node_id: config.property("cluster.node-id").unwrap_or_default(),
- blocked_ips: BlockedIps::parse(config),
- allowed_ips: AllowedIps::parse(config),
+ security: Security::parse(config),
..Default::default()
};
let token_map = &TokenMap::default().with_variables(HTTP_VARS);
diff --git a/crates/common/src/config/scripts.rs b/crates/common/src/config/scripts.rs
index 5e68e435..aa91a2f7 100644
--- a/crates/common/src/config/scripts.rs
+++ b/crates/common/src/config/scripts.rs
@@ -11,8 +11,6 @@ use std::{
};
use ahash::AHashMap;
-use nlp::bayes::cache::BayesTokenCache;
-use parking_lot::RwLock;
use sieve::{compiler::grammar::Capability, Compiler, Runtime, Sieve};
use store::Stores;
use utils::config::Config;
@@ -33,11 +31,6 @@ pub struct Scripting {
pub untrusted_scripts: AHashMap<String, Arc<Sieve>>,
}
-pub struct ScriptCache {
- pub bayes_cache: BayesTokenCache,
- pub remote_lists: RwLock<AHashMap<String, RemoteList>>,
-}
-
#[derive(Clone)]
pub struct RemoteList {
pub entries: HashSet<String>,
@@ -364,25 +357,6 @@ impl Scripting {
}
}
-impl ScriptCache {
- pub fn parse(config: &mut Config) -> Self {
- ScriptCache {
- bayes_cache: BayesTokenCache::new(
- config
- .property_or_default("cache.bayes.capacity", "8192")
- .unwrap_or(8192),
- config
- .property_or_default("cache.bayes.ttl.positive", "1h")
- .unwrap_or_else(|| Duration::from_secs(3600)),
- config
- .property_or_default("cache.bayes.ttl.negative", "1h")
- .unwrap_or_else(|| Duration::from_secs(3600)),
- ),
- remote_lists: Default::default(),
- }
- }
-}
-
impl Default for Scripting {
fn default() -> Self {
Scripting {
@@ -410,19 +384,6 @@ impl Default for Scripting {
}
}
-impl Default for ScriptCache {
- fn default() -> Self {
- Self {
- bayes_cache: BayesTokenCache::new(
- 8192,
- Duration::from_secs(3600),
- Duration::from_secs(3600),
- ),
- remote_lists: Default::default(),
- }
- }
-}
-
impl Clone for Scripting {
fn clone(&self) -> Self {
Self {
diff --git a/crates/common/src/config/server/listener.rs b/crates/common/src/config/server/listener.rs
index 130a6b91..651b841d 100644
--- a/crates/common/src/config/server/listener.rs
+++ b/crates/common/src/config/server/listener.rs
@@ -23,18 +23,18 @@ use utils::{
use crate::{
listener::{tls::CertificateResolver, TcpAcceptor},
- SharedCore,
+ Inner,
};
use super::{
tls::{TLS12_VERSION, TLS13_VERSION},
- Listener, Server, ServerProtocol, Servers,
+ Listener, Listeners, ServerProtocol, TcpListener,
};
-impl Servers {
+impl Listeners {
pub fn parse(config: &mut Config) -> Self {
// Parse ACME managers
- let mut servers = Servers {
+ let mut servers = Listeners {
span_id_gen: Arc::new(
config
.property::<u64>("cluster.node-id")
@@ -139,7 +139,7 @@ impl Servers {
let _ = socket.set_reuseaddr(true);
}
- listeners.push(Listener {
+ listeners.push(TcpListener {
socket,
addr,
ttl: config
@@ -197,7 +197,7 @@ impl Servers {
}
let span_id_gen = self.span_id_gen.clone();
- self.servers.push(Server {
+ self.servers.push(Listener {
max_connections: config
.property_or_else(
("server.listener", id, "max-connections"),
@@ -213,8 +213,8 @@ impl Servers {
});
}
- pub fn parse_tcp_acceptors(&mut self, config: &mut Config, core: SharedCore) {
- let resolver = Arc::new(CertificateResolver::new(core.clone()));
+ pub fn parse_tcp_acceptors(&mut self, config: &mut Config, inner: Arc<Inner>) {
+ let resolver = Arc::new(CertificateResolver::new(inner.clone()));
for id_ in config
.sub_keys("server.listener", ".protocol")
diff --git a/crates/common/src/config/server/mod.rs b/crates/common/src/config/server/mod.rs
index 67c12c00..9040af39 100644
--- a/crates/common/src/config/server/mod.rs
+++ b/crates/common/src/config/server/mod.rs
@@ -17,24 +17,24 @@ pub mod listener;
pub mod tls;
#[derive(Default)]
-pub struct Servers {
- pub servers: Vec<Server>,
+pub struct Listeners {
+ pub servers: Vec<Listener>,
pub tcp_acceptors: AHashMap<String, TcpAcceptor>,
pub span_id_gen: Arc<SnowflakeIdGenerator>,
}
#[derive(Debug, Default)]
-pub struct Server {
+pub struct Listener {
pub id: String,
pub protocol: ServerProtocol,
- pub listeners: Vec<Listener>,
+ pub listeners: Vec<TcpListener>,
pub proxy_networks: Vec<IpAddrMask>,
pub max_connections: u64,
pub span_id_gen: Arc<SnowflakeIdGenerator>,
}
#[derive(Debug)]
-pub struct Listener {
+pub struct TcpListener {
pub socket: TcpSocket,
pub addr: SocketAddr,
pub backlog: Option<u32>,
diff --git a/crates/common/src/config/server/tls.rs b/crates/common/src/config/server/tls.rs
index a98d8583..dfe199d0 100644
--- a/crates/common/src/config/server/tls.rs
+++ b/crates/common/src/config/server/tls.rs
@@ -12,7 +12,6 @@ use std::{
};
use ahash::{AHashMap, AHashSet};
-use arc_swap::ArcSwap;
use base64::{engine::general_purpose::STANDARD, Engine};
use dns_update::{providers::rfc2136::DnsAddress, DnsUpdater, TsigAlgorithm};
use rcgen::generate_simple_self_signed;
@@ -33,20 +32,15 @@ use x509_parser::{
use crate::listener::{
acme::{directory::LETS_ENCRYPT_PRODUCTION_DIRECTORY, AcmeProvider, ChallengeSettings},
- tls::TlsManager,
+ tls::AcmeProviders,
};
pub static TLS13_VERSION: &[&SupportedProtocolVersion] = &[&TLS13];
pub static TLS12_VERSION: &[&SupportedProtocolVersion] = &[&TLS12];
-impl TlsManager {
+impl AcmeProviders {
pub fn parse(config: &mut Config) -> Self {
- let mut certificates = AHashMap::new();
- let mut acme_providers = AHashMap::new();
- let mut subject_names = AHashSet::new();
-
- // Parse certificates
- parse_certificates(config, &mut certificates, &mut subject_names);
+ let mut providers = AHashMap::new();
// Parse ACME providers
'outer: for acme_id in config
@@ -140,9 +134,6 @@ impl TlsManager {
.property::<bool>(("acme", acme_id, "default"))
.unwrap_or_default();
- // Add domains for self-signed certificate
- subject_names.extend(domains.iter().cloned());
-
if !domains.is_empty() {
match AcmeProvider::new(
acme_id.to_string(),
@@ -154,7 +145,7 @@ impl TlsManager {
default,
) {
Ok(acme_provider) => {
- acme_providers.insert(acme_id.to_string(), acme_provider);
+ providers.insert(acme_id.to_string(), acme_provider);
}
Err(err) => {
config.new_build_error(format!("acme.{acme_id}"), err.to_string());
@@ -163,21 +154,7 @@ impl TlsManager {
}
}
- if subject_names.is_empty() {
- subject_names.insert("localhost".to_string());
- }
-
- TlsManager {
- certificates: ArcSwap::from_pointee(certificates),
- acme_providers,
- self_signed_cert: build_self_signed_cert(subject_names.into_iter().collect::<Vec<_>>())
- .or_else(|err| {
- config.new_build_error("certificate.self-signed", err);
- build_self_signed_cert(vec!["localhost".to_string()])
- })
- .ok()
- .map(Arc::new),
- }
+ AcmeProviders { providers }
}
}
diff --git a/crates/common/src/config/smtp/resolver.rs b/crates/common/src/config/smtp/resolver.rs
index ffe43328..b6e6802f 100644
--- a/crates/common/src/config/smtp/resolver.rs
+++ b/crates/common/src/config/smtp/resolver.rs
@@ -24,7 +24,7 @@ use mail_auth::{
use parking_lot::Mutex;
use utils::config::{utils::ParseValue, Config};
-use crate::Core;
+use crate::Server;
pub struct Resolvers {
pub dns: Resolver,
@@ -307,15 +307,27 @@ impl Policy {
}
}
-impl Core {
+impl Server {
pub fn build_mta_sts_policy(&self) -> Option<Policy> {
- self.smtp.session.mta_sts_policy.clone().and_then(|policy| {
- policy.try_build(self.tls.certificates.load().keys().filter(|key| {
- !key.starts_with("mta-sts.")
- && !key.starts_with("autoconfig.")
- && !key.starts_with("autodiscover.")
- }))
- })
+ self.core
+ .smtp
+ .session
+ .mta_sts_policy
+ .clone()
+ .and_then(|policy| {
+ policy.try_build(
+ self.inner
+ .data
+ .tls_certificates
+ .load()
+ .keys()
+ .filter(|key| {
+ !key.starts_with("mta-sts.")
+ && !key.starts_with("autoconfig.")
+ && !key.starts_with("autodiscover.")
+ }),
+ )
+ })
}
}
diff --git a/crates/common/src/core.rs b/crates/common/src/core.rs
new file mode 100644
index 00000000..2ebf66dd
--- /dev/null
+++ b/crates/common/src/core.rs
@@ -0,0 +1,335 @@
+/*
+ * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art>
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
+ */
+
+use std::{net::IpAddr, sync::Arc};
+
+use directory::{
+ backend::internal::manage::ManageDirectory, core::secret::verify_secret_hash, Directory,
+ Principal, QueryBy, Type,
+};
+use mail_send::Credentials;
+use sieve::Sieve;
+use store::{
+ write::{QueueClass, ValueClass},
+ BlobStore, FtsStore, IterateParams, LookupStore, Store, ValueKey,
+};
+use trc::AddContext;
+
+use crate::{
+ config::smtp::{
+ auth::{ArcSealer, DkimSigner},
+ queue::RelayHost,
+ },
+ ImapId, Inner, MailboxState, Server,
+};
+
+impl Server {
+ #[inline(always)]
+ pub fn store(&self) -> &Store {
+ &self.core.storage.data
+ }
+
+ #[inline(always)]
+ pub fn blob_store(&self) -> &BlobStore {
+ &self.core.storage.blob
+ }
+
+ #[inline(always)]
+ pub fn fts_store(&self) -> &FtsStore {
+ &self.core.storage.fts
+ }
+
+ #[inline(always)]
+ pub fn lookup_store(&self) -> &LookupStore {
+ &self.core.storage.lookup
+ }
+
+ #[inline(always)]
+ pub fn directory(&self) -> &Directory {
+ &self.core.storage.directory
+ }
+
+ pub fn get_directory(&self, name: &str) -> Option<&Arc<Directory>> {
+ self.core.storage.directories.get(name)
+ }
+
+ pub fn get_directory_or_default(&self, name: &str, session_id: u64) -> &Arc<Directory> {
+ self.core.storage.directories.get(name).unwrap_or_else(|| {
+ if !name.is_empty() {
+ trc::event!(
+ Eval(trc::EvalEvent::DirectoryNotFound),
+ Id = name.to_string(),
+ SpanId = session_id,
+ );
+ }
+
+ &self.core.storage.directory
+ })
+ }
+
+ pub fn get_lookup_store(&self, name: &str, session_id: u64) -> &LookupStore {
+ self.core.storage.lookups.get(name).unwrap_or_else(|| {
+ if !name.is_empty() {
+ trc::event!(
+ Eval(trc::EvalEvent::StoreNotFound),
+ Id = name.to_string(),
+ SpanId = session_id,
+ );
+ }
+
+ &self.core.storage.lookup
+ })
+ }
+
+ pub fn get_arc_sealer(&self, name: &str, session_id: u64) -> Option<&ArcSealer> {
+ self.core
+ .smtp
+ .mail_auth
+ .sealers
+ .get(name)
+ .map(|s| s.as_ref())
+ .or_else(|| {
+ trc::event!(
+ Arc(trc::ArcEvent::SealerNotFound),
+ Id = name.to_string(),
+ SpanId = session_id,
+ );
+
+ None
+ })
+ }
+
+ pub fn get_dkim_signer(&self, name: &str, session_id: u64) -> Option<&DkimSigner> {
+ self.core
+ .smtp
+ .mail_auth
+ .signers
+ .get(name)
+ .map(|s| s.as_ref())
+ .or_else(|| {
+ trc::event!(
+ Dkim(trc::DkimEvent::SignerNotFound),
+ Id = name.to_string(),
+ SpanId = session_id,
+ );
+
+ None
+ })
+ }
+
+ pub fn get_trusted_sieve_script(&self, name: &str, session_id: u64) -> Option<&Arc<Sieve>> {
+ self.core.sieve.trusted_scripts.get(name).or_else(|| {
+ trc::event!(
+ Sieve(trc::SieveEvent::ScriptNotFound),
+ Id = name.to_string(),
+ SpanId = session_id,
+ );
+
+ None
+ })
+ }
+
+ pub fn get_untrusted_sieve_script(&self, name: &str, session_id: u64) -> Option<&Arc<Sieve>> {
+ self.core.sieve.untrusted_scripts.get(name).or_else(|| {
+ trc::event!(
+ Sieve(trc::SieveEvent::ScriptNotFound),
+ Id = name.to_string(),
+ SpanId = session_id,
+ );
+
+ None
+ })
+ }
+
+ pub fn get_relay_host(&self, name: &str, session_id: u64) -> Option<&RelayHost> {
+ self.core.smtp.queue.relay_hosts.get(name).or_else(|| {
+ trc::event!(
+ Smtp(trc::SmtpEvent::RemoteIdNotFound),
+ Id = name.to_string(),
+ SpanId = session_id,
+ );
+
+ None
+ })
+ }
+
+ pub async fn authenticate(
+ &self,
+ directory: &Directory,
+ session_id: u64,
+ credentials: &Credentials<String>,
+ remote_ip: IpAddr,
+ return_member_of: bool,
+ ) -> trc::Result<Principal> {
+ // First try to authenticate the user against the default directory
+ let result = match directory
+ .query(QueryBy::Credentials(credentials), return_member_of)
+ .await
+ {
+ Ok(Some(principal)) => {
+ trc::event!(
+ Auth(trc::AuthEvent::Success),
+ AccountName = credentials.login().to_string(),
+ AccountId = principal.id(),
+ SpanId = session_id,
+ Type = principal.typ().as_str(),
+ );
+
+ return Ok(principal);
+ }
+ Ok(None) => Ok(()),
+ Err(err) => {
+ if err.matches(trc::EventType::Auth(trc::AuthEvent::MissingTotp)) {
+ return Err(err);
+ } else {
+ Err(err)
+ }
+ }
+ };
+
+ // Then check if the credentials match the fallback admin or master user
+ match (
+ &self.core.jmap.fallback_admin,
+ &self.core.jmap.master_user,
+ credentials,
+ ) {
+ (Some((fallback_admin, fallback_pass)), _, Credentials::Plain { username, secret })
+ if username == fallback_admin =>
+ {
+ if verify_secret_hash(fallback_pass, secret).await? {
+ trc::event!(
+ Auth(trc::AuthEvent::Success),
+ AccountName = username.clone(),
+ SpanId = session_id,
+ );
+
+ return Ok(Principal::fallback_admin(fallback_pass));
+ }
+ }
+ (_, Some((master_user, master_pass)), Credentials::Plain { username, secret })
+ if username.ends_with(master_user) =>
+ {
+ if verify_secret_hash(master_pass, secret).await? {
+ let username = username.strip_suffix(master_user).unwrap();
+ let username = username.strip_suffix('%').unwrap_or(username);
+
+ if let Some(principal) = directory
+ .query(QueryBy::Name(username), return_member_of)
+ .await?
+ {
+ trc::event!(
+ Auth(trc::AuthEvent::Success),
+ AccountName = username.to_string(),
+ SpanId = session_id,
+ AccountId = principal.id(),
+ Type = principal.typ().as_str(),
+ );
+
+ return Ok(principal);
+ }
+ }
+ }
+ _ => {}
+ }
+
+ if let Err(err) = result {
+ Err(err)
+ } else if self.has_auth_fail2ban() {
+ let login = credentials.login();
+ if self.is_auth_fail2banned(remote_ip, login).await? {
+ Err(trc::SecurityEvent::AuthenticationBan
+ .into_err()
+ .ctx(trc::Key::RemoteIp, remote_ip)
+ .ctx(trc::Key::AccountName, login.to_string()))
+ } else {
+ Err(trc::AuthEvent::Failed
+ .ctx(trc::Key::RemoteIp, remote_ip)
+ .ctx(trc::Key::AccountName, login.to_string()))
+ }
+ } else {
+ Err(trc::AuthEvent::Failed
+ .ctx(trc::Key::RemoteIp, remote_ip)
+ .ctx(trc::Key::AccountName, credentials.login().to_string()))
+ }
+ }
+
+ pub async fn total_queued_messages(&self) -> trc::Result<u64> {
+ let mut total = 0;
+ self.store()
+ .iterate(
+ IterateParams::new(
+ ValueKey::from(ValueClass::Queue(QueueClass::Message(0))),
+ ValueKey::from(ValueClass::Queue(QueueClass::Message(u64::MAX))),
+ )
+ .no_values(),
+ |_, _| {
+ total += 1;
+
+ Ok(true)
+ },
+ )
+ .await
+ .map(|_| total)
+ }
+
+ pub async fn total_accounts(&self) -> trc::Result<u64> {
+ self.store()
+ .count_principals(None, Type::Individual.into(), None)
+ .await
+ .caused_by(trc::location!())
+ }
+
+ pub async fn total_domains(&self) -> trc::Result<u64> {
+ self.store()
+ .count_principals(None, Type::Domain.into(), None)
+ .await
+ .caused_by(trc::location!())
+ }
+}
+
+pub trait BuildServer {
+ fn build_server(&self) -> Server;
+}
+
+impl BuildServer for Arc<Inner> {
+ fn build_server(&self) -> Server {
+ Server {
+ inner: self.clone(),
+ core: self.shared_core.load_full(),
+ }
+ }
+}
+
+trait CredentialsUsername {
+ fn login(&self) -> &str;
+}
+
+impl CredentialsUsername for Credentials<String> {
+ fn login(&self) -> &str {
+ match self {
+ Credentials::Plain { username, .. }
+ | Credentials::XOauth2 { username, .. }
+ | Credentials::OAuthBearer { token: username } => username,
+ }
+ }
+}
+
+impl MailboxState {
+ pub fn map_result_id(&self, document_id: u32, is_uid: bool) -> Option<(u32, ImapId)> {
+ if let Some(imap_id) = self.id_to_imap.get(&document_id) {
+ Some((if is_uid { imap_id.uid } else { imap_id.seqnum }, *imap_id))
+ } else if is_uid {
+ self.next_state.as_ref().and_then(|s| {
+ s.next_state
+ .id_to_imap
+ .get(&document_id)
+ .map(|imap_id| (imap_id.uid, *imap_id))
+ })
+ } else {
+ None
+ }
+ }
+}
diff --git a/crates/common/src/enterprise/alerts.rs b/crates/common/src/enterprise/alerts.rs
index a7d26404..6cff45e2 100644
--- a/crates/common/src/enterprise/alerts.rs
+++ b/crates/common/src/enterprise/alerts.rs
@@ -20,7 +20,7 @@ use trc::{Collector, MetricType, TelemetryEvent, TOTAL_EVENT_COUNT};
use super::{AlertContent, AlertContentToken, AlertMethod};
use crate::{
expr::{functions::ResolveVariable, Variable},
- Core,
+ Server,
};
use std::fmt::Write;
@@ -33,9 +33,9 @@ pub struct AlertMessage {
struct CollectorResolver;
-impl Core {
+impl Server {
pub async fn process_alerts(&self) -> Option<Vec<AlertMessage>> {
- let alerts = &self.enterprise.as_ref()?.metrics_alerts;
+ let alerts = &self.core.enterprise.as_ref()?.metrics_alerts;
if alerts.is_empty() {
return None;
}
diff --git a/crates/common/src/enterprise/mod.rs b/crates/common/src/enterprise/mod.rs
index 69608023..9f634564 100644
--- a/crates/common/src/enterprise/mod.rs
+++ b/crates/common/src/enterprise/mod.rs
@@ -25,7 +25,7 @@ use store::Store;
use trc::{AddContext, EventType, MetricType};
use utils::config::cron::SimpleCron;
-use crate::{expr::Expression, manager::webadmin::Resource, Core, HttpLimitResponse};
+use crate::{expr::Expression, manager::webadmin::Resource, Core, HttpLimitResponse, Server};
#[derive(Clone)]
pub struct Enterprise {
@@ -87,6 +87,14 @@ pub enum AlertContentToken {
}
impl Core {
+ pub fn is_enterprise_edition(&self) -> bool {
+ self.enterprise
+ .as_ref()
+ .map_or(false, |e| !e.license.is_expired())
+ }
+}
+
+impl Server {
// WARNING: TAMPERING WITH THIS FUNCTION IS STRICTLY PROHIBITED
// Any attempt to modify, bypass, or disable this license validation mechanism
// constitutes a severe violation of the Stalwart Enterprise License Agreement.
@@ -96,18 +104,20 @@ impl Core {
// violators to the fullest extent of the law, including but not limited to claims
// for copyright infringement, breach of contract, and fraud.
+ #[inline]
pub fn is_enterprise_edition(&self) -> bool {
- self.enterprise
- .as_ref()
- .map_or(false, |e| !e.license.is_expired())
+ self.core.is_enterprise_edition()
}
pub fn licensed_accounts(&self) -> u32 {
- self.enterprise.as_ref().map_or(0, |e| e.license.accounts)
+ self.core
+ .enterprise
+ .as_ref()
+ .map_or(0, |e| e.license.accounts)
}
pub fn log_license_details(&self) {
- if let Some(enterprise) = &self.enterprise {
+ if let Some(enterprise) = &self.core.enterprise {
trc::event!(
Server(trc::ServerEvent::Licensing),
Details = "Stalwart Enterprise Edition license key is valid",
@@ -125,15 +135,14 @@ impl Core {
if self.is_enterprise_edition() {
let domain = psl::domain_str(domain).unwrap_or(domain);
- let logo = { self.security.logos.lock().get(domain).cloned() };
+ let logo = { self.inner.data.logos.lock().get(domain).cloned() };
if let Some(logo) = logo {
Ok(logo)
} else {
// Try fetching the logo for the domain
let logo_url = if let Some(mut principal) = self
- .storage
- .data
+ .store()
.query(QueryBy::Name(domain), false)
.await
.caused_by(trc::location!())?
@@ -146,8 +155,7 @@ impl Core {
logo.into()
} else if let Some(tenant_id) = principal.get_int(PrincipalField::Tenant) {
if let Some(logo) = self
- .storage
- .data
+ .store()
.query(QueryBy::Id(tenant_id as u32), false)
.await
.caused_by(trc::location!())?
@@ -199,7 +207,8 @@ impl Core {
logo = Resource::new(content_type, contents).into();
}
- self.security
+ self.inner
+ .data
.logos
.lock()
.insert(domain.to_string(), logo.clone());
@@ -212,7 +221,8 @@ impl Core {
}
fn default_logo_url(&self) -> Option<String> {
- self.enterprise
+ self.core
+ .enterprise
.as_ref()
.and_then(|e| e.logo_url.as_ref().map(|l| l.to_string()))
}
diff --git a/crates/common/src/expr/eval.rs b/crates/common/src/expr/eval.rs
index 371a0476..0e87c203 100644
--- a/crates/common/src/expr/eval.rs
+++ b/crates/common/src/expr/eval.rs
@@ -9,7 +9,7 @@ use std::{borrow::Cow, cmp::Ordering, fmt::Display};
use hyper::StatusCode;
use trc::EvalEvent;
-use crate::Core;
+use crate::Server;
use super::{
functions::{ResolveVariable, FUNCTIONS},
@@ -17,7 +17,7 @@ use super::{
BinaryOperator, Constant, Expression, ExpressionItem, UnaryOperator, Variable,
};
-impl Core {
+impl Server {
pub async fn eval_if<'x, R: TryFrom<Variable<'x>>, V: ResolveVariable>(
&self,
if_block: &'x IfBlock,
@@ -123,7 +123,7 @@ impl IfBlock {
pub async fn eval<'x, V: ResolveVariable>(
&'x self,
resolver: &'x V,
- core: &Core,
+ core: &Server,
session_id: u64,
) -> trc::Result<Variable<'x>> {
let mut captures = Vec::new();
@@ -152,7 +152,7 @@ impl Expression {
async fn eval<'x, 'y, V: ResolveVariable>(
&'x self,
resolver: &'x V,
- core: &Core,
+ core: &Server,
captures: &'y mut Vec<String>,
session_id: u64,
) -> trc::Result<Variable<'x>> {
diff --git a/crates/common/src/expr/functions/asynch.rs b/crates/common/src/expr/functions/asynch.rs
index 7e94f7b2..162929cc 100644
--- a/crates/common/src/expr/functions/asynch.rs
+++ b/crates/common/src/expr/functions/asynch.rs
@@ -4,11 +4,11 @@ use mail_auth::IpLookupStrategy;
use store::{Deserialize, Rows, Value};
use trc::AddContext;
-use crate::Core;
+use crate::Server;
use super::*;
-impl Core {
+impl Server {
pub(crate) async fn eval_fnc<'x>(
&self,
fnc_id: u32,
@@ -168,7 +168,8 @@ impl Core {
let record_type = arguments.next_as_string();
if record_type.eq_ignore_ascii_case("ip") {
- self.smtp
+ self.core
+ .smtp
.resolvers
.dns
.ip_lookup(entry.as_ref(), IpLookupStrategy::Ipv4thenIpv6, 10)
@@ -182,7 +183,8 @@ impl Core {
.into()
})
} else if record_type.eq_ignore_ascii_case("mx") {
- self.smtp
+ self.core
+ .smtp
.resolvers
.dns
.mx_lookup(entry.as_ref())
@@ -205,7 +207,8 @@ impl Core {
.into()
})
} else if record_type.eq_ignore_ascii_case("txt") {
- self.smtp
+ self.core
+ .smtp
.resolvers
.dns
.txt_raw_lookup(entry.as_ref())
@@ -213,7 +216,8 @@ impl Core {
.map_err(|err| trc::Error::from(err).caused_by(trc::location!()))
.map(|result| Variable::from(String::from_utf8(result).unwrap_or_default()))
} else if record_type.eq_ignore_ascii_case("ptr") {
- self.smtp
+ self.core
+ .smtp
.resolvers
.dns
.ptr_lookup(entry.parse::<IpAddr>().map_err(|err| {
@@ -232,7 +236,8 @@ impl Core {
.into()
})
} else if record_type.eq_ignore_ascii_case("ipv4") {
- self.smtp
+ self.core
+ .smtp
.resolvers
.dns
.ipv4_lookup(entry.as_ref())
@@ -246,7 +251,8 @@ impl Core {
.into()
})
} else if record_type.eq_ignore_ascii_case("ipv6") {
- self.smtp
+ self.core
+ .smtp
.resolvers
.dns
.ipv6_lookup(entry.as_ref())
diff --git a/crates/common/src/expr/functions/mod.rs b/crates/common/src/expr/functions/mod.rs
index 94646b36..3d6f0c28 100644
--- a/crates/common/src/expr/functions/mod.rs
+++ b/crates/common/src/expr/functions/mod.rs
@@ -14,7 +14,7 @@ pub mod email;
pub mod misc;
pub mod text;
-pub trait ResolveVariable {
+pub trait ResolveVariable: Sync + Send {
fn resolve_variable(&self, variable: u32) -> Variable<'_>;
}
diff --git a/crates/common/src/ipc.rs b/crates/common/src/ipc.rs
new file mode 100644
index 00000000..9b64a318
--- /dev/null
+++ b/crates/common/src/ipc.rs
@@ -0,0 +1,233 @@
+/*
+ * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art>
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
+ */
+
+use std::{borrow::Cow, sync::Arc, time::Instant};
+
+use ahash::RandomState;
+use jmap_proto::types::{state::StateChange, type_state::DataType};
+use mail_auth::{
+ dmarc::Dmarc,
+ mta_sts::TlsRpt,
+ report::{tlsrpt::FailureDetails, Record},
+};
+use store::{BlobStore, LookupStore, Store};
+use tokio::sync::{mpsc, oneshot};
+use utils::{map::bitmap::Bitmap, BlobHash};
+
+use crate::{
+ config::smtp::{
+ report::AggregateFrequency,
+ resolver::{Policy, Tlsa},
+ },
+ listener::limiter::ConcurrencyLimiter,
+};
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum DeliveryResult {
+ Success,
+ TemporaryFailure {
+ reason: Cow<'static, str>,
+ },
+ PermanentFailure {
+ code: [u8; 3],
+ reason: Cow<'static, str>,
+ },
+}
+
+#[derive(Debug)]
+pub enum DeliveryEvent {
+ Ingest {
+ message: IngestMessage,
+ result_tx: oneshot::Sender<Vec<DeliveryResult>>,
+ },
+ Stop,
+}
+
+#[derive(Debug)]
+pub struct IngestMessage {
+ pub sender_address: String,
+ pub recipients: Vec<String>,
+ pub message_blob: BlobHash,
+ pub message_size: usize,
+ pub session_id: u64,
+}
+
+pub enum HousekeeperEvent {
+ AcmeReschedule {
+ provider_id: String,
+ renew_at: Instant,
+ },
+ Purge(PurgeType),
+ ReloadSettings,
+ Exit,
+}
+
+pub enum PurgeType {
+ Data(Store),
+ Blobs { store: Store, blob_store: BlobStore },
+ Lookup(LookupStore),
+ Account(Option<u32>),
+}
+
+#[derive(Debug)]
+pub enum StateEvent {
+ Subscribe {
+ account_id: u32,
+ types: Bitmap<DataType>,
+ tx: mpsc::Sender<StateChange>,
+ },
+ Publish {
+ state_change: StateChange,
+ },
+ UpdateSharedAccounts {
+ account_id: u32,
+ },
+ UpdateSubscriptions {
+ account_id: u32,
+ subscriptions: Vec<UpdateSubscription>,
+ },
+ Stop,
+}
+
+#[derive(Debug)]
+pub enum UpdateSubscription {
+ Unverified {
+ id: u32,
+ url: String,
+ code: String,
+ keys: Option<EncryptionKeys>,
+ },
+ Verified(PushSubscription),
+}
+
+#[derive(Debug)]
+pub struct PushSubscription {
+ pub id: u32,
+ pub url: String,
+ pub expires: u64,
+ pub types: Bitmap<DataType>,
+ pub keys: Option<EncryptionKeys>,
+}
+
+#[derive(Debug, Clone)]
+pub struct EncryptionKeys {
+ pub p256dh: Vec<u8>,
+ pub auth: Vec<u8>,
+}
+
+#[derive(Debug)]
+pub enum QueueEvent {
+ Reload,
+ OnHold(OnHold<QueueEventLock>),
+ Stop,
+}
+
+#[derive(Debug)]
+pub struct OnHold<T> {
+ pub next_due: Option<u64>,
+ pub limiters: Vec<ConcurrencyLimiter>,
+ pub message: T,
+}
+
+#[derive(Debug)]
+pub struct QueueEventLock {
+ pub due: u64,
+ pub queue_id: u64,
+ pub lock_expiry: u64,
+}
+
+#[derive(Debug)]
+pub enum ReportingEvent {
+ Dmarc(Box<DmarcEvent>),
+ Tls(Box<TlsEvent>),
+ Stop,
+}
+
+#[derive(Debug)]
+pub struct DmarcEvent {
+ pub domain: String,
+ pub report_record: Record,
+ pub dmarc_record: Arc<Dmarc>,
+ pub interval: AggregateFrequency,
+}
+
+#[derive(Debug)]
+pub struct TlsEvent {
+ pub domain: String,
+ pub policy: PolicyType,
+ pub failure: Option<FailureDetails>,
+ pub tls_record: Arc<TlsRpt>,
+ pub interval: AggregateFrequency,
+}
+
+#[derive(Debug, Hash, PartialEq, Eq)]
+pub enum PolicyType {
+ Tlsa(Option<Arc<Tlsa>>),
+ Sts(Option<Arc<Policy>>),
+ None,
+}
+
+pub trait ToHash {
+ fn to_hash(&self) -> u64;
+}
+
+impl ToHash for Dmarc {
+ fn to_hash(&self) -> u64 {
+ RandomState::with_seeds(1, 9, 7, 9).hash_one(self)
+ }
+}
+
+impl ToHash for PolicyType {
+ fn to_hash(&self) -> u64 {
+ RandomState::with_seeds(1, 9, 7, 9).hash_one(self)
+ }
+}
+
+impl From<DmarcEvent> for ReportingEvent {
+ fn from(value: DmarcEvent) -> Self {
+ ReportingEvent::Dmarc(Box::new(value))
+ }
+}
+
+impl From<TlsEvent> for ReportingEvent {
+ fn from(value: TlsEvent) -> Self {
+ ReportingEvent::Tls(Box::new(value))
+ }
+}
+
+impl From<Arc<Tlsa>> for PolicyType {
+ fn from(value: Arc<Tlsa>) -> Self {
+ PolicyType::Tlsa(Some(value))
+ }
+}
+
+impl From<Arc<Policy>> for PolicyType {
+ fn from(value: Arc<Policy>) -> Self {
+ PolicyType::Sts(Some(value))
+ }
+}
+
+impl From<&Arc<Tlsa>> for PolicyType {
+ fn from(value: &Arc<Tlsa>) -> Self {
+ PolicyType::Tlsa(Some(value.clone()))
+ }
+}
+
+impl From<&Arc<Policy>> for PolicyType {
+ fn from(value: &Arc<Policy>) -> Self {
+ PolicyType::Sts(Some(value.clone()))
+ }
+}
+
+impl From<(&Option<Arc<Policy>>, &Option<Arc<Tlsa>>)> for PolicyType {
+ fn from(value: (&Option<Arc<Policy>>, &Option<Arc<Tlsa>>)) -> Self {
+ match value {
+ (Some(value), _) => PolicyType::Sts(Some(value.clone())),
+ (_, Some(value)) => PolicyType::Tlsa(Some(value.clone())),
+ _ => PolicyType::None,
+ }
+ }
+}
diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs
index e8dbf60e..f983917c 100644
--- a/crates/common/src/lib.rs
+++ b/crates/common/src/lib.rs
@@ -5,59 +5,52 @@
*/
use std::{
- borrow::Cow,
+ collections::BTreeMap,
+ hash::{BuildHasher, Hasher},
net::IpAddr,
sync::{atomic::AtomicU8, Arc},
};
-use ahash::AHashMap;
+use ahash::{AHashMap, AHashSet, RandomState};
use arc_swap::ArcSwap;
use auth::{roles::RolePermissions, AccessToken};
use config::{
imap::ImapConfig,
jmap::settings::JmapConfig,
- scripts::Scripting,
- smtp::{
- auth::{ArcSealer, DkimSigner},
- queue::RelayHost,
- SmtpConfig,
- },
+ scripts::{RemoteList, Scripting},
+ smtp::SmtpConfig,
storage::Storage,
telemetry::Metrics,
};
-use directory::{
- backend::internal::manage::ManageDirectory, core::secret::verify_secret_hash, Directory,
- Principal, QueryBy, Type,
-};
+use dashmap::DashMap;
+
use expr::if_block::IfBlock;
use futures::StreamExt;
-use listener::{
- blocked::{AllowedIps, BlockedIps},
- tls::TlsManager,
-};
-use mail_send::Credentials;
+use imap_proto::protocol::list::Attribute;
+use ipc::{DeliveryEvent, HousekeeperEvent, QueueEvent, ReportingEvent, StateEvent};
+use listener::{blocked::Security, limiter::ConcurrencyLimiter, tls::AcmeProviders};
-use manager::webadmin::Resource;
-use parking_lot::Mutex;
+use manager::webadmin::{Resource, WebAdminManager};
+use nlp::bayes::cache::BayesTokenCache;
+use parking_lot::{Mutex, RwLock};
use reqwest::Response;
-use sieve::Sieve;
-use store::{
- write::{QueueClass, ValueClass},
- IterateParams, LookupStore, ValueKey,
-};
-use tokio::sync::{mpsc, oneshot};
-use trc::AddContext;
+use rustls::sign::CertifiedKey;
+use tokio::sync::{mpsc, Notify};
+use tokio_rustls::TlsConnector;
use utils::{
+ lru_cache::LruCache,
map::ttl_dashmap::{ADashMap, TtlDashMap},
- BlobHash,
+ snowflake::SnowflakeIdGenerator,
};
pub mod addresses;
pub mod auth;
pub mod config;
+pub mod core;
#[cfg(feature = "enterprise")]
pub mod enterprise;
pub mod expr;
+pub mod ipc;
pub mod listener;
pub mod manager;
pub mod scripts;
@@ -70,351 +63,219 @@ pub static DAEMON_NAME: &str = concat!("Stalwart Mail Server v", env!("CARGO_PKG
pub const IPC_CHANNEL_BUFFER: usize = 1024;
-pub type SharedCore = Arc<ArcSwap<Core>>;
-
#[derive(Clone, Default)]
-pub struct Core {
- pub storage: Storage,
- pub sieve: Scripting,
- pub network: Network,
- pub tls: TlsManager,
- pub smtp: SmtpConfig,
- pub jmap: JmapConfig,
- pub imap: ImapConfig,
- pub metrics: Metrics,
- pub security: Security,
- #[cfg(feature = "enterprise")]
- pub enterprise: Option<enterprise::Enterprise>,
+pub struct Server {
+ pub inner: Arc<Inner>,
+ pub core: Arc<Core>,
}
-//TODO: temporary hack until OIDC is implemented
#[derive(Default)]
-pub struct Security {
- pub logos: Mutex<AHashMap<String, Option<Resource<Vec<u8>>>>>,
+pub struct Inner {
+ pub shared_core: ArcSwap<Core>,
+ pub data: Data,
+ pub ipc: Ipc,
+}
+
+pub struct Data {
+ pub tls_certificates: ArcSwap<AHashMap<String, Arc<CertifiedKey>>>,
+ pub tls_self_signed_cert: Option<Arc<CertifiedKey>>,
+
pub access_tokens: TtlDashMap<u32, Arc<AccessToken>>,
+ pub http_auth_cache: TtlDashMap<String, u32>,
+
+ pub blocked_ips: RwLock<AHashSet<IpAddr>>,
+ pub blocked_ips_version: AtomicU8,
+
pub permissions: ADashMap<u32, Arc<RolePermissions>>,
pub permissions_version: AtomicU8,
-}
-#[derive(Clone)]
-pub struct Network {
- pub node_id: u64,
- pub blocked_ips: BlockedIps,
- pub allowed_ips: AllowedIps,
- pub http_response_url: IfBlock,
- pub http_allowed_endpoint: IfBlock,
-}
+ pub bayes_cache: BayesTokenCache,
+ pub remote_lists: RwLock<AHashMap<String, RemoteList>>,
-#[derive(Debug)]
-pub enum DeliveryEvent {
- Ingest {
- message: IngestMessage,
- result_tx: oneshot::Sender<Vec<DeliveryResult>>,
- },
- Stop,
+ pub jmap_id_gen: SnowflakeIdGenerator,
+ pub queue_id_gen: SnowflakeIdGenerator,
+ pub span_id_gen: SnowflakeIdGenerator,
+
+ pub webadmin: WebAdminManager,
+ pub config_version: AtomicU8,
+
+ pub jmap_limiter: DashMap<u32, Arc<ConcurrencyLimiters>, RandomState>,
+ pub imap_limiter: DashMap<u32, Arc<ConcurrencyLimiters>, RandomState>,
+
+ pub account_cache: LruCache<AccountId, Arc<Account>>,
+ pub mailbox_cache: LruCache<MailboxId, Arc<MailboxState>>,
+ pub threads_cache: LruCache<u32, Arc<Threads>>,
+
+ pub logos: Mutex<AHashMap<String, Option<Resource<Vec<u8>>>>>,
+
+ pub smtp_session_throttle: DashMap<ThrottleKey, ConcurrencyLimiter, ThrottleKeyHasherBuilder>,
+ pub smtp_queue_throttle: DashMap<ThrottleKey, ConcurrencyLimiter, ThrottleKeyHasherBuilder>,
+ pub smtp_connectors: TlsConnectors,
}
pub struct Ipc {
+ pub state_tx: mpsc::Sender<StateEvent>,
+ pub housekeeper_tx: mpsc::Sender<HousekeeperEvent>,
pub delivery_tx: mpsc::Sender<DeliveryEvent>,
+ pub index_tx: Arc<Notify>,
+ pub queue_tx: mpsc::Sender<QueueEvent>,
+ pub report_tx: mpsc::Sender<ReportingEvent>,
}
-#[derive(Debug)]
-pub struct IngestMessage {
- pub sender_address: String,
- pub recipients: Vec<String>,
- pub message_blob: BlobHash,
- pub message_size: usize,
- pub session_id: u64,
+pub struct TlsConnectors {
+ pub pki_verify: TlsConnector,
+ pub dummy_verify: TlsConnector,
}
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub enum DeliveryResult {
- Success,
- TemporaryFailure {
- reason: Cow<'static, str>,
- },
- PermanentFailure {
- code: [u8; 3],
- reason: Cow<'static, str>,
- },
+#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
+pub struct AccountId {
+ pub account_id: u32,
+ pub primary_id: u32,
}
-pub trait IntoString: Sized {
- fn into_string(self) -> String;
+#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
+pub struct MailboxId {
+ pub account_id: u32,
+ pub mailbox_id: u32,
}
-impl IntoString for Vec<u8> {
- fn into_string(self) -> String {
- String::from_utf8(self)
- .unwrap_or_else(|err| String::from_utf8_lossy(err.as_bytes()).into_owned())
- }
+#[derive(Debug, Clone, Default)]
+pub struct Account {
+ pub account_id: u32,
+ pub prefix: Option<String>,
+ pub mailbox_names: BTreeMap<String, u32>,
+ pub mailbox_state: AHashMap<u32, Mailbox>,
+ pub state_email: Option<u64>,
+ pub state_mailbox: Option<u64>,
}
-impl Core {
- pub fn get_directory(&self, name: &str) -> Option<&Arc<Directory>> {
- self.storage.directories.get(name)
- }
-
- pub fn get_directory_or_default(&self, name: &str, session_id: u64) -> &Arc<Directory> {
- self.storage.directories.get(name).unwrap_or_else(|| {
- if !name.is_empty() {
- trc::event!(
- Eval(trc::EvalEvent::DirectoryNotFound),
- Id = name.to_string(),
- SpanId = session_id,
- );
- }
+#[derive(Debug, Default, Clone)]
+pub struct Mailbox {
+ pub has_children: bool,
+ pub is_subscribed: bool,
+ pub special_use: Option<Attribute>,
+ pub total_messages: Option<u32>,
+ pub total_unseen: Option<u32>,
+ pub total_deleted: Option<u32>,
+ pub uid_validity: Option<u32>,
+ pub uid_next: Option<u32>,
+ pub size: Option<u32>,
+}
- &self.storage.directory
- })
- }
+#[derive(Debug, Clone, Default)]
+pub struct MailboxState {
+ pub uid_next: u32,
+ pub uid_validity: u32,
+ pub uid_max: u32,
+ pub id_to_imap: AHashMap<u32, ImapId>,
+ pub uid_to_id: AHashMap<u32, u32>,
+ pub total_messages: usize,
+ pub modseq: Option<u64>,
+ pub next_state: Option<Box<NextMailboxState>>,
+}
- pub fn get_lookup_store(&self, name: &str, session_id: u64) -> &LookupStore {
- self.storage.lookups.get(name).unwrap_or_else(|| {
- if !name.is_empty() {
- trc::event!(
- Eval(trc::EvalEvent::StoreNotFound),
- Id = name.to_string(),
- SpanId = session_id,
- );
- }
+#[derive(Debug, Clone)]
+pub struct NextMailboxState {
+ pub next_state: MailboxState,
+ pub deletions: Vec<ImapId>,
+}
- &self.storage.lookup
- })
- }
+#[derive(Debug, Clone, Copy, Default)]
+pub struct ImapId {
+ pub uid: u32,
+ pub seqnum: u32,
+}
- pub fn get_arc_sealer(&self, name: &str, session_id: u64) -> Option<&ArcSealer> {
- self.smtp
- .mail_auth
- .sealers
- .get(name)
- .map(|s| s.as_ref())
- .or_else(|| {
- trc::event!(
- Arc(trc::ArcEvent::SealerNotFound),
- Id = name.to_string(),
- SpanId = session_id,
- );
-
- None
- })
- }
+#[derive(Debug, Default)]
+pub struct Threads {
+ pub threads: AHashMap<u32, u32>,
+ pub modseq: Option<u64>,
+}
- pub fn get_dkim_signer(&self, name: &str, session_id: u64) -> Option<&DkimSigner> {
- self.smtp
- .mail_auth
- .signers
- .get(name)
- .map(|s| s.as_ref())
- .or_else(|| {
- trc::event!(
- Dkim(trc::DkimEvent::SignerNotFound),
- Id = name.to_string(),
- SpanId = session_id,
- );
-
- None
- })
- }
+pub struct ConcurrencyLimiters {
+ pub concurrent_requests: ConcurrencyLimiter,
+ pub concurrent_uploads: ConcurrencyLimiter,
+}
- pub fn get_trusted_sieve_script(&self, name: &str, session_id: u64) -> Option<&Arc<Sieve>> {
- self.sieve.trusted_scripts.get(name).or_else(|| {
- trc::event!(
- Sieve(trc::SieveEvent::ScriptNotFound),
- Id = name.to_string(),
- SpanId = session_id,
- );
+#[derive(Clone, Default)]
+pub struct Core {
+ pub storage: Storage,
+ pub sieve: Scripting,
+ pub network: Network,
+ pub acme: AcmeProviders,
+ pub smtp: SmtpConfig,
+ pub jmap: JmapConfig,
+ pub imap: ImapConfig,
+ pub metrics: Metrics,
+ #[cfg(feature = "enterprise")]
+ pub enterprise: Option<enterprise::Enterprise>,
+}
- None
- })
- }
+#[derive(Clone)]
+pub struct Network {
+ pub node_id: u64,
+ pub security: Security,
+ pub http_response_url: IfBlock,
+ pub http_allowed_endpoint: IfBlock,
+}
- pub fn get_untrusted_sieve_script(&self, name: &str, session_id: u64) -> Option<&Arc<Sieve>> {
- self.sieve.untrusted_scripts.get(name).or_else(|| {
- trc::event!(
- Sieve(trc::SieveEvent::ScriptNotFound),
- Id = name.to_string(),
- SpanId = session_id,
- );
+pub trait IntoString: Sized {
+ fn into_string(self) -> String;
+}
- None
- })
+impl IntoString for Vec<u8> {
+ fn into_string(self) -> String {
+ String::from_utf8(self)
+ .unwrap_or_else(|err| String::from_utf8_lossy(err.as_bytes()).into_owned())
}
+}
- pub fn get_relay_host(&self, name: &str, session_id: u64) -> Option<&RelayHost> {
- self.smtp.queue.relay_hosts.get(name).or_else(|| {
- trc::event!(
- Smtp(trc::SmtpEvent::RemoteIdNotFound),
- Id = name.to_string(),
- SpanId = session_id,
- );
+#[derive(Debug, Clone, Eq)]
+pub struct ThrottleKey {
+ pub hash: [u8; 32],
+}
- None
- })
+impl PartialEq for ThrottleKey {
+ fn eq(&self, other: &Self) -> bool {
+ self.hash == other.hash
}
+}
- pub async fn authenticate(
- &self,
- directory: &Directory,
- session_id: u64,
- credentials: &Credentials<String>,
- remote_ip: IpAddr,
- return_member_of: bool,
- ) -> trc::Result<Principal> {
- // First try to authenticate the user against the default directory
- let result = match directory
- .query(QueryBy::Credentials(credentials), return_member_of)
- .await
- {
- Ok(Some(principal)) => {
- trc::event!(
- Auth(trc::AuthEvent::Success),
- AccountName = credentials.login().to_string(),
- AccountId = principal.id(),
- SpanId = session_id,
- Type = principal.typ().as_str(),
- );
-
- return Ok(principal);
- }
- Ok(None) => Ok(()),
- Err(err) => {
- if err.matches(trc::EventType::Auth(trc::AuthEvent::MissingTotp)) {
- return Err(err);
- } else {
- Err(err)
- }
- }
- };
-
- // Then check if the credentials match the fallback admin or master user
- match (
- &self.jmap.fallback_admin,
- &self.jmap.master_user,
- credentials,
- ) {
- (Some((fallback_admin, fallback_pass)), _, Credentials::Plain { username, secret })
- if username == fallback_admin =>
- {
- if verify_secret_hash(fallback_pass, secret).await? {
- trc::event!(
- Auth(trc::AuthEvent::Success),
- AccountName = username.clone(),
- SpanId = session_id,
- );
-
- return Ok(Principal::fallback_admin(fallback_pass));
- }
- }
- (_, Some((master_user, master_pass)), Credentials::Plain { username, secret })
- if username.ends_with(master_user) =>
- {
- if verify_secret_hash(master_pass, secret).await? {
- let username = username.strip_suffix(master_user).unwrap();
- let username = username.strip_suffix('%').unwrap_or(username);
-
- if let Some(principal) = directory
- .query(QueryBy::Name(username), return_member_of)
- .await?
- {
- trc::event!(
- Auth(trc::AuthEvent::Success),
- AccountName = username.to_string(),
- SpanId = session_id,
- AccountId = principal.id(),
- Type = principal.typ().as_str(),
- );
-
- return Ok(principal);
- }
- }
- }
- _ => {}
- }
-
- if let Err(err) = result {
- Err(err)
- } else if self.has_auth_fail2ban() {
- let login = credentials.login();
- if self.is_auth_fail2banned(remote_ip, login).await? {
- Err(trc::SecurityEvent::AuthenticationBan
- .into_err()
- .ctx(trc::Key::RemoteIp, remote_ip)
- .ctx(trc::Key::AccountName, login.to_string()))
- } else {
- Err(trc::AuthEvent::Failed
- .ctx(trc::Key::RemoteIp, remote_ip)
- .ctx(trc::Key::AccountName, login.to_string()))
- }
- } else {
- Err(trc::AuthEvent::Failed
- .ctx(trc::Key::RemoteIp, remote_ip)
- .ctx(trc::Key::AccountName, credentials.login().to_string()))
- }
+impl std::hash::Hash for ThrottleKey {
+ fn hash<H: Hasher>(&self, state: &mut H) {
+ self.hash.hash(state);
}
+}
- pub async fn total_queued_messages(&self) -> trc::Result<u64> {
- let mut total = 0;
- self.storage
- .data
- .iterate(
- IterateParams::new(
- ValueKey::from(ValueClass::Queue(QueueClass::Message(0))),
- ValueKey::from(ValueClass::Queue(QueueClass::Message(u64::MAX))),
- )
- .no_values(),
- |_, _| {
- total += 1;
-
- Ok(true)
- },
- )
- .await
- .map(|_| total)
+impl AsRef<[u8]> for ThrottleKey {
+ fn as_ref(&self) -> &[u8] {
+ &self.hash
}
+}
- pub async fn total_accounts(&self) -> trc::Result<u64> {
- self.storage
- .data
- .count_principals(None, Type::Individual.into(), None)
- .await
- .caused_by(trc::location!())
+#[derive(Default)]
+pub struct ThrottleKeyHasher {
+ hash: u64,
+}
+
+impl Hasher for ThrottleKeyHasher {
+ fn finish(&self) -> u64 {
+ self.hash
}
- pub async fn total_domains(&self) -> trc::Result<u64> {
- self.storage
- .data
- .count_principals(None, Type::Domain.into(), None)
- .await
- .caused_by(trc::location!())
+ fn write(&mut self, bytes: &[u8]) {
+ self.hash = u64::from_ne_bytes((&bytes[..std::mem::size_of::<u64>()]).try_into().unwrap());
}
}
-trait CredentialsUsername {
- fn login(&self) -> &str;
-}
+#[derive(Clone, Default)]
+pub struct ThrottleKeyHasherBuilder {}
-impl CredentialsUsername for Credentials<String> {
- fn login(&self) -> &str {
- match self {
- Credentials::Plain { username, .. }
- | Credentials::XOauth2 { username, .. }
- | Credentials::OAuthBearer { token: username } => username,
- }
- }
-}
+impl BuildHasher for ThrottleKeyHasherBuilder {
+ type Hasher = ThrottleKeyHasher;
-impl Clone for Security {
- fn clone(&self) -> Self {
- Self {
- access_tokens: self.access_tokens.clone(),
- permissions: self.permissions.clone(),
- permissions_version: AtomicU8::new(
- self.permissions_version
- .load(std::sync::atomic::Ordering::Relaxed),
- ),
- logos: Mutex::new(self.logos.lock().clone()),
- }
+ fn build_hasher(&self) -> Self::Hasher {
+ ThrottleKeyHasher::default()
}
}
@@ -448,3 +309,23 @@ impl HttpLimitResponse for Response {
Ok(Some(bytes))
}
}
+
+impl ConcurrencyLimiters {
+ pub fn is_active(&self) -> bool {
+ self.concurrent_requests.is_active() || self.concurrent_uploads.is_active()
+ }
+}
+
+#[cfg(feature = "test_mode")]
+impl Default for Ipc {
+ fn default() -> Self {
+ Self {
+ state_tx: mpsc::channel(IPC_CHANNEL_BUFFER).0,
+ housekeeper_tx: mpsc::channel(IPC_CHANNEL_BUFFER).0,
+ delivery_tx: mpsc::channel(IPC_CHANNEL_BUFFER).0,
+ index_tx: Default::default(),
+ queue_tx: mpsc::channel(IPC_CHANNEL_BUFFER).0,
+ report_tx: mpsc::channel(IPC_CHANNEL_BUFFER).0,
+ }
+ }
+}
diff --git a/crates/common/src/listener/acme/cache.rs b/crates/common/src/listener/acme/cache.rs
index 5c30ee43..c7269fc7 100644
--- a/crates/common/src/listener/acme/cache.rs
+++ b/crates/common/src/listener/acme/cache.rs
@@ -8,11 +8,11 @@ use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
use trc::AddContext;
use utils::config::ConfigKey;
-use crate::Core;
+use crate::Server;
use super::AcmeProvider;
-impl Core {
+impl Server {
pub(crate) async fn load_cert(&self, provider: &AcmeProvider) -> trc::Result<Option<Vec<u8>>> {
self.read_if_exists(provider, "cert", provider.domains.as_slice())
.await
@@ -68,6 +68,7 @@ impl Core {
items: &[String],
) -> trc::Result<Option<Vec<u8>>> {
if let Some(content) = self
+ .core
.storage
.config
.get(self.build_key(provider, class, items))
@@ -94,7 +95,8 @@ impl Core {
items: &[String],
contents: impl AsRef<[u8]>,
) -> trc::Result<()> {
- self.storage
+ self.core
+ .storage
.config
.set([ConfigKey {
key: self.build_key(provider, class, items),
diff --git a/crates/common/src/listener/acme/mod.rs b/crates/common/src/listener/acme/mod.rs
index f429984d..c0faef56 100644
--- a/crates/common/src/listener/acme/mod.rs
+++ b/crates/common/src/listener/acme/mod.rs
@@ -16,7 +16,7 @@ use arc_swap::ArcSwap;
use dns_update::DnsUpdater;
use rustls::sign::CertifiedKey;
-use crate::Core;
+use crate::Server;
use self::directory::{Account, ChallengeType};
@@ -80,7 +80,7 @@ impl AcmeProvider {
}
}
-impl Core {
+impl Server {
pub async fn init_acme(&self, provider: &AcmeProvider) -> trc::Result<Duration> {
// Load account key from cache or generate a new one
if let Some(account_key) = self.load_account(provider).await? {
@@ -100,15 +100,17 @@ impl Core {
}
pub fn has_acme_tls_providers(&self) -> bool {
- self.tls
- .acme_providers
+ self.core
+ .acme
+ .providers
.values()
.any(|p| matches!(p.challenge, ChallengeSettings::TlsAlpn01))
}
pub fn has_acme_http_providers(&self) -> bool {
- self.tls
- .acme_providers
+ self.core
+ .acme
+ .providers
.values()
.any(|p| matches!(p.challenge, ChallengeSettings::Http01))
}
diff --git a/crates/common/src/listener/acme/order.rs b/crates/common/src/listener/acme/order.rs
index 983198ea..38fc1290 100644
--- a/crates/common/src/listener/acme/order.rs
+++ b/crates/common/src/listener/acme/order.rs
@@ -13,12 +13,12 @@ use x509_parser::parse_x509_certificate;
use crate::listener::acme::directory::Identifier;
use crate::listener::acme::ChallengeSettings;
-use crate::Core;
+use crate::Server;
use super::directory::{Account, AuthStatus, Directory, OrderStatus};
use super::AcmeProvider;
-impl Core {
+impl Server {
pub(crate) async fn process_cert(
&self,
provider: &AcmeProvider,
@@ -210,8 +210,7 @@ impl Core {
match &provider.challenge {
ChallengeSettings::TlsAlpn01 => {
- self.storage
- .lookup
+ self.lookup_store()
.key_set(
format!("acme:{domain}").into_bytes(),
account.tls_alpn_key(challenge, domain.clone())?,
@@ -220,8 +219,7 @@ impl Core {
.await?;
}
ChallengeSettings::Http01 => {
- self.storage
- .lookup
+ self.lookup_store()
.key_set(
format!("acme:{}", challenge.token).into_bytes(),
account.http_proof(challenge)?,
@@ -289,7 +287,7 @@ impl Core {
let wait_until = Instant::now() + *propagation_timeout;
let mut did_propagate = false;
while Instant::now() < wait_until {
- match self.smtp.resolvers.dns.txt_raw_lookup(&name).await {
+ match self.core.smtp.resolvers.dns.txt_raw_lookup(&name).await {
Ok(result) => {
let result = std::str::from_utf8(&result).unwrap_or_default();
if result.contains(&dns_proof) {
diff --git a/crates/common/src/listener/acme/resolver.rs b/crates/common/src/listener/acme/resolver.rs
index 26d91cfe..fec948fc 100644
--- a/crates/common/src/listener/acme/resolver.rs
+++ b/crates/common/src/listener/acme/resolver.rs
@@ -16,14 +16,14 @@ use rustls_pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer};
use store::write::Bincode;
use trc::AcmeEvent;
-use crate::{listener::acme::directory::SerializedCert, Core};
+use crate::{listener::acme::directory::SerializedCert, Server};
use super::{directory::ACME_TLS_ALPN_NAME, AcmeProvider, StaticResolver};
-impl Core {
+impl Server {
pub(crate) fn set_cert(&self, provider: &AcmeProvider, cert: Arc<CertifiedKey>) {
// Add certificates
- let mut certificates = self.tls.certificates.load().as_ref().clone();
+ let mut certificates = self.inner.data.tls_certificates.load().as_ref().clone();
for domain in provider.domains.iter() {
certificates.insert(
domain
@@ -39,29 +39,12 @@ impl Core {
certificates.insert("*".to_string(), cert);
}
- self.tls.certificates.store(certificates.into());
+ self.inner.data.tls_certificates.store(certificates.into());
}
-}
-
-impl ResolvesServerCert for StaticResolver {
- fn resolve(&self, _: ClientHello) -> Option<Arc<CertifiedKey>> {
- self.key.clone()
- }
-}
-
-pub(crate) fn build_acme_static_resolver(key: Option<Arc<CertifiedKey>>) -> Arc<ServerConfig> {
- let mut challenge = ServerConfig::builder()
- .with_no_client_auth()
- .with_cert_resolver(Arc::new(StaticResolver { key }));
- challenge.alpn_protocols.push(ACME_TLS_ALPN_NAME.to_vec());
- Arc::new(challenge)
-}
-impl Core {
pub(crate) async fn build_acme_certificate(&self, domain: &str) -> Option<Arc<CertifiedKey>> {
match self
- .storage
- .lookup
+ .lookup_store()
.key_get::<Bincode<SerializedCert>>(format!("acme:{domain}").into_bytes())
.await
{
@@ -100,6 +83,20 @@ impl Core {
}
}
+impl ResolvesServerCert for StaticResolver {
+ fn resolve(&self, _: ClientHello) -> Option<Arc<CertifiedKey>> {
+ self.key.clone()
+ }
+}
+
+pub(crate) fn build_acme_static_resolver(key: Option<Arc<CertifiedKey>>) -> Arc<ServerConfig> {
+ let mut challenge = ServerConfig::builder()
+ .with_no_client_auth()
+ .with_cert_resolver(Arc::new(StaticResolver { key }));
+ challenge.alpn_protocols.push(ACME_TLS_ALPN_NAME.to_vec());
+ Arc::new(challenge)
+}
+
pub trait IsTlsAlpnChallenge {
fn is_tls_alpn_challenge(&self) -> bool;
}
diff --git a/crates/common/src/listener/blocked.rs b/crates/common/src/listener/blocked.rs
index a46a3ce1..20de1951 100644
--- a/crates/common/src/listener/blocked.rs
+++ b/crates/common/src/listener/blocked.rs
@@ -4,85 +4,45 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
-use std::{fmt::Debug, net::IpAddr, sync::atomic::AtomicU8};
+use std::{fmt::Debug, net::IpAddr};
use ahash::AHashSet;
-use parking_lot::RwLock;
use utils::config::{
ipmask::{IpAddrMask, IpAddrOrMask},
utils::ParseValue,
Config, ConfigKey, Rate,
};
-use crate::Core;
+use crate::Server;
+
+#[derive(Debug, Clone)]
+pub struct Security {
+ blocked_ip_networks: Vec<IpAddrMask>,
+ has_blocked_networks: bool,
+
+ allowed_ip_addresses: AHashSet<IpAddr>,
+ allowed_ip_networks: Vec<IpAddrMask>,
+ has_allowed_networks: bool,
-pub struct BlockedIps {
- pub ip_addresses: RwLock<AHashSet<IpAddr>>,
- pub version: AtomicU8,
- ip_networks: Vec<IpAddrMask>,
- has_networks: bool,
auth_fail_rate: Option<Rate>,
rcpt_fail_rate: Option<Rate>,
loiter_fail_rate: Option<Rate>,
}
-#[derive(Clone)]
-pub struct AllowedIps {
- ip_addresses: AHashSet<IpAddr>,
- ip_networks: Vec<IpAddrMask>,
- has_networks: bool,
-}
-
pub const BLOCKED_IP_KEY: &str = "server.blocked-ip";
pub const BLOCKED_IP_PREFIX: &str = "server.blocked-ip.";
pub const ALLOWED_IP_KEY: &str = "server.allowed-ip";
pub const ALLOWED_IP_PREFIX: &str = "server.allowed-ip.";
-impl BlockedIps {
- pub fn parse(config: &mut Config) -> Self {
- let mut ip_addresses = AHashSet::new();
- let mut ip_networks = Vec::new();
-
- for ip in config
- .set_values(BLOCKED_IP_KEY)
- .map(IpAddrOrMask::parse_value)
- .collect::<Vec<_>>()
- {
- match ip {
- Ok(IpAddrOrMask::Ip(ip)) => {
- ip_addresses.insert(ip);
- }
- Ok(IpAddrOrMask::Mask(ip)) => {
- ip_networks.push(ip);
- }
- Err(err) => {
- config.new_parse_error(BLOCKED_IP_KEY, err);
- }
- }
- }
-
- BlockedIps {
- ip_addresses: RwLock::new(ip_addresses),
- has_networks: !ip_networks.is_empty(),
- ip_networks,
- auth_fail_rate: config
- .property_or_default::<Option<Rate>>("server.fail2ban.authentication", "100/1d")
- .unwrap_or_default(),
- rcpt_fail_rate: config
- .property_or_default::<Option<Rate>>("server.fail2ban.invalid-rcpt", "35/1d")
- .unwrap_or_default(),
- loiter_fail_rate: config
- .property_or_default::<Option<Rate>>("server.fail2ban.loitering", "150/1d")
- .unwrap_or_default(),
- version: 0.into(),
- }
- }
+pub struct BlockedIps {
+ pub blocked_ip_addresses: AHashSet<IpAddr>,
+ pub blocked_ip_networks: Vec<IpAddrMask>,
}
-impl AllowedIps {
+impl Security {
pub fn parse(config: &mut Config) -> Self {
- let mut ip_addresses = AHashSet::new();
- let mut ip_networks = Vec::new();
+ let mut allowed_ip_addresses = AHashSet::new();
+ let mut allowed_ip_networks = Vec::new();
for ip in config
.set_values(ALLOWED_IP_KEY)
@@ -91,10 +51,10 @@ impl AllowedIps {
{
match ip {
Ok(IpAddrOrMask::Ip(ip)) => {
- ip_addresses.insert(ip);
+ allowed_ip_addresses.insert(ip);
}
Ok(IpAddrOrMask::Mask(ip)) => {
- ip_networks.push(ip);
+ allowed_ip_networks.push(ip);
}
Err(err) => {
config.new_parse_error(ALLOWED_IP_KEY, err);
@@ -105,25 +65,37 @@ impl AllowedIps {
#[cfg(not(feature = "test_mode"))]
{
// Add loopback addresses
- ip_addresses.insert(IpAddr::V4(std::net::Ipv4Addr::LOCALHOST));
- ip_addresses.insert(IpAddr::V6(std::net::Ipv6Addr::LOCALHOST));
+ allowed_ip_addresses.insert(IpAddr::V4(std::net::Ipv4Addr::LOCALHOST));
+ allowed_ip_addresses.insert(IpAddr::V6(std::net::Ipv6Addr::LOCALHOST));
}
- AllowedIps {
- ip_addresses,
- has_networks: !ip_networks.is_empty(),
- ip_networks,
+ let blocked = BlockedIps::parse(config);
+
+ Security {
+ has_blocked_networks: !blocked.blocked_ip_networks.is_empty(),
+ blocked_ip_networks: blocked.blocked_ip_networks,
+ has_allowed_networks: !allowed_ip_networks.is_empty(),
+ allowed_ip_addresses,
+ allowed_ip_networks,
+ auth_fail_rate: config
+ .property_or_default::<Option<Rate>>("server.fail2ban.authentication", "100/1d")
+ .unwrap_or_default(),
+ rcpt_fail_rate: config
+ .property_or_default::<Option<Rate>>("server.fail2ban.invalid-rcpt", "35/1d")
+ .unwrap_or_default(),
+ loiter_fail_rate: config
+ .property_or_default::<Option<Rate>>("server.fail2ban.loitering", "150/1d")
+ .unwrap_or_default(),
}
}
}
-impl Core {
+impl Server {
pub async fn is_rcpt_fail2banned(&self, ip: IpAddr) -> trc::Result<bool> {
- if let Some(rate) = &self.network.blocked_ips.rcpt_fail_rate {
+ if let Some(rate) = &self.core.network.security.rcpt_fail_rate {
let is_allowed = self.is_ip_allowed(&ip)
|| self
- .storage
- .lookup
+ .lookup_store()
.is_rate_allowed(format!("r:{ip}").as_bytes(), rate, false)
.await?
.is_none();
@@ -137,11 +109,10 @@ impl Core {
}
pub async fn is_loiter_fail2banned(&self, ip: IpAddr) -> trc::Result<bool> {
- if let Some(rate) = &self.network.blocked_ips.loiter_fail_rate {
+ if let Some(rate) = &self.core.network.security.loiter_fail_rate {
let is_allowed = self.is_ip_allowed(&ip)
|| self
- .storage
- .lookup
+ .lookup_store()
.is_rate_allowed(format!("l:{ip}").as_bytes(), rate, false)
.await?
.is_none();
@@ -155,17 +126,15 @@ impl Core {
}
pub async fn is_auth_fail2banned(&self, ip: IpAddr, login: &str) -> trc::Result<bool> {
- if let Some(rate) = &self.network.blocked_ips.auth_fail_rate {
+ if let Some(rate) = &self.core.network.security.auth_fail_rate {
let is_allowed = self.is_ip_allowed(&ip)
|| (self
- .storage
- .lookup
+ .lookup_store()
.is_rate_allowed(format!("b:{ip}").as_bytes(), rate, false)
.await?
.is_none()
&& self
- .storage
- .lookup
+ .lookup_store()
.is_rate_allowed(format!("b:{login}").as_bytes(), rate, false)
.await?
.is_none());
@@ -179,10 +148,11 @@ impl Core {
async fn block_ip(&self, ip: IpAddr) -> trc::Result<()> {
// Add IP to blocked list
- self.network.blocked_ips.ip_addresses.write().insert(ip);
+ self.inner.data.blocked_ips.write().insert(ip);
// Write blocked IP to config
- self.storage
+ self.core
+ .storage
.config
.set([ConfigKey {
key: format!("{}.{}", BLOCKED_IP_KEY, ip),
@@ -191,104 +161,96 @@ impl Core {
.await?;
// Increment version
- self.network.blocked_ips.increment_version();
+ self.increment_blocked_version();
Ok(())
}
pub fn has_auth_fail2ban(&self) -> bool {
- self.network.blocked_ips.auth_fail_rate.is_some()
+ self.core.network.security.auth_fail_rate.is_some()
}
pub fn is_ip_blocked(&self, ip: &IpAddr) -> bool {
- self.network.blocked_ips.ip_addresses.read().contains(ip)
- || (self.network.blocked_ips.has_networks
+ self.inner.data.blocked_ips.read().contains(ip)
+ || (self.core.network.security.has_blocked_networks
&& self
+ .core
.network
- .blocked_ips
- .ip_networks
+ .security
+ .blocked_ip_networks
.iter()
.any(|network| network.matches(ip)))
}
pub fn is_ip_allowed(&self, ip: &IpAddr) -> bool {
- self.network.allowed_ips.ip_addresses.contains(ip)
- || (self.network.allowed_ips.has_networks
+ self.core.network.security.allowed_ip_addresses.contains(ip)
+ || (self.core.network.security.has_allowed_networks
&& self
+ .core
.network
- .allowed_ips
- .ip_networks
+ .security
+ .allowed_ip_networks
.iter()
.any(|network| network.matches(ip)))
}
-}
-impl BlockedIps {
- pub fn increment_version(&self) {
- self.version
+ pub fn increment_blocked_version(&self) {
+ self.inner
+ .data
+ .blocked_ips_version
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
}
}
-impl Default for BlockedIps {
- fn default() -> Self {
+impl BlockedIps {
+ pub fn parse(config: &mut Config) -> Self {
+ let mut blocked_ip_addresses = AHashSet::new();
+ let mut blocked_ip_networks = Vec::new();
+
+ for ip in config
+ .set_values(BLOCKED_IP_KEY)
+ .map(IpAddrOrMask::parse_value)
+ .collect::<Vec<_>>()
+ {
+ match ip {
+ Ok(IpAddrOrMask::Ip(ip)) => {
+ blocked_ip_addresses.insert(ip);
+ }
+ Ok(IpAddrOrMask::Mask(ip)) => {
+ blocked_ip_networks.push(ip);
+ }
+ Err(err) => {
+ config.new_parse_error(BLOCKED_IP_KEY, err);
+ }
+ }
+ }
+
Self {
- ip_addresses: RwLock::new(AHashSet::new()),
- ip_networks: Default::default(),
- has_networks: Default::default(),
- version: Default::default(),
- auth_fail_rate: Default::default(),
- rcpt_fail_rate: Default::default(),
- loiter_fail_rate: Default::default(),
+ blocked_ip_addresses,
+ blocked_ip_networks,
}
}
}
#[allow(clippy::derivable_impls)]
-impl Default for AllowedIps {
+impl Default for Security {
fn default() -> Self {
// Add IPv4 and IPv6 loopback addresses
Self {
#[cfg(not(feature = "test_mode"))]
- ip_addresses: AHashSet::from_iter([
+ allowed_ip_addresses: AHashSet::from_iter([
IpAddr::V4(std::net::Ipv4Addr::LOCALHOST),
IpAddr::V6(std::net::Ipv6Addr::LOCALHOST),
]),
#[cfg(feature = "test_mode")]
- ip_addresses: Default::default(),
- ip_networks: Default::default(),
- has_networks: Default::default(),
- }
- }
-}
-
-impl Clone for BlockedIps {
- fn clone(&self) -> Self {
- Self {
- ip_addresses: RwLock::new(self.ip_addresses.read().clone()),
- ip_networks: self.ip_networks.clone(),
- has_networks: self.has_networks,
- version: self
- .version
- .load(std::sync::atomic::Ordering::Relaxed)
- .into(),
- auth_fail_rate: self.auth_fail_rate.clone(),
- rcpt_fail_rate: self.rcpt_fail_rate.clone(),
- loiter_fail_rate: self.loiter_fail_rate.clone(),
+ allowed_ip_addresses: Default::default(),
+ allowed_ip_networks: Default::default(),
+ has_allowed_networks: Default::default(),
+ blocked_ip_networks: Default::default(),
+ has_blocked_networks: Default::default(),
+ auth_fail_rate: Default::default(),
+ rcpt_fail_rate: Default::default(),
+ loiter_fail_rate: Default::default(),
}
}
}
-
-impl Debug for BlockedIps {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- f.debug_struct("BlockedIps")
- .field("ip_addresses", &self.ip_addresses)
- .field("ip_networks", &self.ip_networks)
- .field("has_networks", &self.has_networks)
- .field("version", &self.version)
- .field("auth_fail_rate", &self.auth_fail_rate)
- .field("rcpt_fail_rate", &self.rcpt_fail_rate)
- .field("loiter_fail_rate", &self.loiter_fail_rate)
- .finish()
- }
-}
diff --git a/crates/common/src/listener/listen.rs b/crates/common/src/listener/listen.rs
index a2877ce9..fc1cd38f 100644
--- a/crates/common/src/listener/listen.rs
+++ b/crates/common/src/listener/listen.rs
@@ -10,20 +10,17 @@ use std::{
time::Duration,
};
-use arc_swap::ArcSwap;
use proxy_header::io::ProxiedStream;
use rustls::crypto::ring::cipher_suite::TLS13_AES_128_GCM_SHA256;
-use tokio::{
- net::{TcpListener, TcpStream},
- sync::watch,
-};
+use tokio::{net::TcpStream, sync::watch};
use tokio_rustls::server::TlsStream;
use trc::{EventType, HttpEvent, ImapEvent, ManageSieveEvent, Pop3Event, SmtpEvent};
use utils::{config::Config, UnwrapFailure};
use crate::{
- config::server::{Listener, Server, ServerProtocol, Servers},
- Core,
+ config::server::{Listener, Listeners, ServerProtocol, TcpListener},
+ core::BuildServer,
+ Inner, Server,
};
use super::{
@@ -31,11 +28,11 @@ use super::{
TcpAcceptor,
};
-impl Server {
+impl Listener {
pub fn spawn(
self,
manager: impl SessionManager,
- core: Arc<ArcSwap<Core>>,
+ inner: Arc<Inner>,
acceptor: TcpAcceptor,
shutdown_rx: watch::Receiver<bool>,
) {
@@ -95,7 +92,7 @@ impl Server {
let mut shutdown_rx = instance.shutdown_rx.clone();
let manager = manager.clone();
let instance = instance.clone();
- let core = core.clone();
+ let inner = inner.clone();
tokio::spawn(async move {
let (span_start, span_end) = match self.protocol {
ServerProtocol::Smtp | ServerProtocol::Lmtp => (
@@ -125,8 +122,8 @@ impl Server {
stream = listener.accept() => {
match stream {
Ok((stream, remote_addr)) => {
- let core = core.as_ref().load_full();
- let enable_acme = (is_https && core.has_acme_tls_providers()).then_some(core.clone());
+ let server = inner.build_server();
+ let enable_acme = (is_https && server.has_acme_tls_providers()).then(|| server.clone());
if has_proxies && instance.proxy_networks.iter().any(|network| network.matches(&remote_addr.ip())) {
let instance = instance.clone();
@@ -142,7 +139,7 @@ impl Server {
.proxied_address()
.map(|addr| addr.source)
.unwrap_or(remote_addr);
- if let Some(session) = instance.build_session(stream, local_addr, remote_addr, &core) {
+ if let Some(session) = instance.build_session(stream, local_addr, remote_addr, &server) {
// Spawn session
manager.spawn(session, is_tls, enable_acme, span_start, span_end);
}
@@ -159,7 +156,7 @@ impl Server {
}
}
});
- } else if let Some(session) = instance.build_session(stream, local_addr, remote_addr, &core) {
+ } else if let Some(session) = instance.build_session(stream, local_addr, remote_addr, &server) {
// Set socket options
opts.apply(&session.stream);
@@ -205,7 +202,7 @@ trait BuildSession {
stream: T,
local_addr: SocketAddr,
remote_addr: SocketAddr,
- core: &Core,
+ server: &Server,
) -> Option<SessionData<T>>;
}
@@ -215,7 +212,7 @@ impl BuildSession for Arc<ServerInstance> {
stream: T,
local_addr: SocketAddr,
remote_addr: SocketAddr,
- core: &Core,
+ server: &Server,
) -> Option<SessionData<T>> {
// Convert mapped IPv6 addresses to IPv4
let remote_ip = match remote_addr.ip() {
@@ -228,7 +225,7 @@ impl BuildSession for Arc<ServerInstance> {
let remote_port = remote_addr.port();
// Check if blocked
- if core.is_ip_blocked(&remote_ip) {
+ if server.is_ip_blocked(&remote_ip) {
trc::event!(
Security(trc::SecurityEvent::IpBlocked),
ListenerId = self.id.clone(),
@@ -303,7 +300,7 @@ impl SocketOpts {
}
}
-impl Servers {
+impl Listeners {
pub fn bind_and_drop_priv(&self, config: &mut Config) {
// Bind as root
for server in &self.servers {
@@ -332,7 +329,7 @@ impl Servers {
pub fn spawn(
mut self,
- spawn: impl Fn(Server, TcpAcceptor, watch::Receiver<bool>),
+ spawn: impl Fn(Listener, TcpAcceptor, watch::Receiver<bool>),
) -> (watch::Sender<bool>, watch::Receiver<bool>) {
// Spawn listeners
let (shutdown_tx, shutdown_rx) = watch::channel(false);
@@ -348,8 +345,8 @@ impl Servers {
}
}
-impl Listener {
- pub fn listen(self) -> Result<TcpListener, String> {
+impl TcpListener {
+ pub fn listen(self) -> Result<tokio::net::TcpListener, String> {
self.socket
.listen(self.backlog.unwrap_or(1024))
.map_err(|err| format!("Failed to listen on {}: {}", self.addr, err))
diff --git a/crates/common/src/listener/mod.rs b/crates/common/src/listener/mod.rs
index 790adaf6..6704a228 100644
--- a/crates/common/src/listener/mod.rs
+++ b/crates/common/src/listener/mod.rs
@@ -19,7 +19,7 @@ use utils::{config::ipmask::IpAddrMask, snowflake::SnowflakeIdGenerator};
use crate::{
config::server::ServerProtocol,
expr::{functions::ResolveVariable, *},
- Core,
+ Server,
};
use self::limiter::{ConcurrencyLimiter, InFlight};
@@ -91,7 +91,7 @@ pub trait SessionManager: Sync + Send + 'static + Clone {
&self,
mut session: SessionData<T>,
is_tls: bool,
- acme_core: Option<Arc<Core>>,
+ acme_core: Option<Server>,
span_start: EventType,
span_end: EventType,
) {
diff --git a/crates/common/src/listener/tls.rs b/crates/common/src/listener/tls.rs
index fc67521c..15ec20f1 100644
--- a/crates/common/src/listener/tls.rs
+++ b/crates/common/src/listener/tls.rs
@@ -11,7 +11,6 @@ use std::{
};
use ahash::AHashMap;
-use arc_swap::ArcSwap;
use rustls::{
server::{ClientHello, ResolvesServerCert},
sign::CertifiedKey,
@@ -21,7 +20,7 @@ use rustls::{
use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt};
use tokio_rustls::{Accept, LazyConfigAcceptor};
-use crate::{Core, SharedCore};
+use crate::{Inner, Server};
use super::{
acme::{
@@ -34,36 +33,31 @@ use super::{
pub static TLS13_VERSION: &[&SupportedProtocolVersion] = &[&TLS13];
pub static TLS12_VERSION: &[&SupportedProtocolVersion] = &[&TLS12];
-#[derive(Default)]
-pub struct TlsManager {
- pub certificates: ArcSwap<AHashMap<String, Arc<CertifiedKey>>>,
- pub acme_providers: AHashMap<String, AcmeProvider>,
- pub self_signed_cert: Option<Arc<CertifiedKey>>,
+#[derive(Default, Clone)]
+pub struct AcmeProviders {
+ pub providers: AHashMap<String, AcmeProvider>,
}
#[derive(Clone)]
pub struct CertificateResolver {
- pub core: SharedCore,
+ pub inner: Arc<Inner>,
}
impl CertificateResolver {
- pub fn new(core: SharedCore) -> Self {
- Self { core }
+ pub fn new(inner: Arc<Inner>) -> Self {
+ Self { inner }
}
}
impl ResolvesServerCert for CertificateResolver {
fn resolve(&self, hello: ClientHello<'_>) -> Option<Arc<CertifiedKey>> {
- self.core
- .as_ref()
- .load()
- .resolve_certificate(hello.server_name())
+ self.resolve_certificate(hello.server_name())
}
}
-impl Core {
+impl CertificateResolver {
pub(crate) fn resolve_certificate(&self, name: Option<&str>) -> Option<Arc<CertifiedKey>> {
- let certs = self.tls.certificates.load();
+ let certs = self.inner.data.tls_certificates.load();
name.map_or_else(
|| certs.get("*"),
@@ -98,7 +92,7 @@ impl Core {
Tls(trc::TlsEvent::NoCertificatesAvailable),
Total = certs.len(),
);
- self.tls.self_signed_cert.as_ref()
+ self.inner.data.tls_self_signed_cert.as_ref()
}
})
.cloned()
@@ -109,7 +103,7 @@ impl TcpAcceptor {
pub async fn accept<IO>(
&self,
stream: IO,
- enable_acme: Option<Arc<Core>>,
+ enable_acme: Option<Server>,
instance: &ServerInstance,
) -> TcpAcceptorResult<IO>
where
@@ -215,13 +209,3 @@ impl std::fmt::Debug for CertificateResolver {
f.debug_struct("CertificateResolver").finish()
}
}
-
-impl Clone for TlsManager {
- fn clone(&self) -> Self {
- Self {
- certificates: ArcSwap::from_pointee(self.certificates.load().as_ref().clone()),
- acme_providers: self.acme_providers.clone(),
- self_signed_cert: self.self_signed_cert.clone(),
- }
- }
-}
diff --git a/crates/common/src/manager/boot.rs b/crates/common/src/manager/boot.rs
index b11b5988..bf3dd8b5 100644
--- a/crates/common/src/manager/boot.rs
+++ b/crates/common/src/manager/boot.rs
@@ -4,7 +4,7 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
-use std::path::PathBuf;
+use std::{path::PathBuf, sync::Arc};
use arc_swap::ArcSwap;
use pwhash::sha512_crypt;
@@ -12,14 +12,16 @@ use store::{
rand::{distributions::Alphanumeric, thread_rng, Rng},
Stores,
};
+use tokio::sync::{mpsc, Notify};
use utils::{
config::{Config, ConfigKey},
failed, UnwrapFailure,
};
use crate::{
- config::{server::Servers, telemetry::Telemetry},
- Core, SharedCore,
+ config::{server::Listeners, telemetry::Telemetry},
+ ipc::{DeliveryEvent, HousekeeperEvent, QueueEvent, ReportingEvent, StateEvent},
+ Core, Data, Inner, Ipc, IPC_CHANNEL_BUFFER,
};
use super::{
@@ -30,8 +32,17 @@ use super::{
pub struct BootManager {
pub config: Config,
- pub core: SharedCore,
- pub servers: Servers,
+ pub inner: Arc<Inner>,
+ pub servers: Listeners,
+ pub ipc_rxs: IpcReceivers,
+}
+
+pub struct IpcReceivers {
+ pub state_rx: Option<mpsc::Receiver<StateEvent>>,
+ pub housekeeper_rx: Option<mpsc::Receiver<HousekeeperEvent>>,
+ pub delivery_rx: Option<mpsc::Receiver<DeliveryEvent>>,
+ pub queue_rx: Option<mpsc::Receiver<QueueEvent>>,
+ pub report_rx: Option<mpsc::Receiver<ReportingEvent>>,
}
const HELP: &str = concat!(
@@ -135,7 +146,7 @@ impl BootManager {
config.resolve_macros(&["env"]).await;
// Parser servers
- let mut servers = Servers::parse(&mut config);
+ let mut servers = Listeners::parse(&mut config);
// Bind ports and drop privileges
servers.bind_and_drop_priv(&mut config);
@@ -314,6 +325,9 @@ impl BootManager {
// Parse settings
let core = Core::parse(&mut config, stores, manager).await;
+ // Parse data
+ let data = Data::parse(&mut config);
+
// Enable telemetry
#[cfg(feature = "enterprise")]
telemetry.enable(core.is_enterprise_edition());
@@ -325,16 +339,22 @@ impl BootManager {
Version = env!("CARGO_PKG_VERSION"),
);
- // Build shared core
- let core = core.into_shared();
+ // Build shared inner
+ let (ipc, ipc_rxs) = build_ipc();
+ let inner = Arc::new(Inner {
+ shared_core: ArcSwap::from_pointee(core),
+ data,
+ ipc,
+ });
// Parse TCP acceptors
- servers.parse_tcp_acceptors(&mut config, core.clone());
+ servers.parse_tcp_acceptors(&mut config, inner.clone());
BootManager {
- core,
+ inner,
config,
servers,
+ ipc_rxs,
}
}
ImportExport::Export(path) => {
@@ -363,6 +383,32 @@ impl BootManager {
}
}
+pub fn build_ipc() -> (Ipc, IpcReceivers) {
+ // Build ipc receivers
+ let (delivery_tx, delivery_rx) = mpsc::channel(IPC_CHANNEL_BUFFER);
+ let (state_tx, state_rx) = mpsc::channel(IPC_CHANNEL_BUFFER);
+ let (housekeeper_tx, housekeeper_rx) = mpsc::channel(IPC_CHANNEL_BUFFER);
+ let (queue_tx, queue_rx) = mpsc::channel(IPC_CHANNEL_BUFFER);
+ let (report_tx, report_rx) = mpsc::channel(IPC_CHANNEL_BUFFER);
+ (
+ Ipc {
+ state_tx,
+ housekeeper_tx,
+ delivery_tx,
+ queue_tx,
+ report_tx,
+ index_tx: Arc::new(Notify::new()),
+ },
+ IpcReceivers {
+ state_rx: Some(state_rx),
+ housekeeper_rx: Some(housekeeper_rx),
+ delivery_rx: Some(delivery_rx),
+ queue_rx: Some(queue_rx),
+ report_rx: Some(report_rx),
+ },
+ )
+}
+
fn quickstart(path: impl Into<PathBuf>) {
let path = path.into();
diff --git a/crates/common/src/manager/reload.rs b/crates/common/src/manager/reload.rs
index f7532a36..143d4008 100644
--- a/crates/common/src/manager/reload.rs
+++ b/crates/common/src/manager/reload.rs
@@ -4,18 +4,18 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
-use ahash::AHashSet;
+use ahash::AHashMap;
use arc_swap::ArcSwap;
use store::Stores;
-use utils::config::{ipmask::IpAddrOrMask, utils::ParseValue, Config};
+use utils::config::Config;
use crate::{
config::{
- server::{tls::parse_certificates, Servers},
+ server::{tls::parse_certificates, Listeners},
telemetry::Telemetry,
},
- listener::blocked::BLOCKED_IP_KEY,
- Core,
+ listener::blocked::{BlockedIps, BLOCKED_IP_KEY},
+ Core, Server,
};
use super::config::{ConfigManager, Patterns};
@@ -26,49 +26,36 @@ pub struct ReloadResult {
pub tracers: Option<Telemetry>,
}
-impl Core {
+impl Server {
pub async fn reload_blocked_ips(&self) -> trc::Result<ReloadResult> {
- let mut ip_addresses = AHashSet::new();
- let mut config = self.storage.config.build_config(BLOCKED_IP_KEY).await?;
-
- for ip in config
- .set_values(BLOCKED_IP_KEY)
- .map(IpAddrOrMask::parse_value)
- .collect::<Vec<_>>()
- {
- match ip {
- Ok(IpAddrOrMask::Ip(ip)) => {
- ip_addresses.insert(ip);
- }
- Ok(IpAddrOrMask::Mask(_)) => {}
- Err(err) => {
- config.new_parse_error(BLOCKED_IP_KEY, err);
- }
- }
- }
-
- *self.network.blocked_ips.ip_addresses.write() = ip_addresses;
+ let mut config = self
+ .core
+ .storage
+ .config
+ .build_config(BLOCKED_IP_KEY)
+ .await?;
+ *self.inner.data.blocked_ips.write() = BlockedIps::parse(&mut config).blocked_ip_addresses;
Ok(config.into())
}
pub async fn reload_certificates(&self) -> trc::Result<ReloadResult> {
- let mut config = self.storage.config.build_config("certificate").await?;
- let mut certificates = self.tls.certificates.load().as_ref().clone();
+ let mut config = self.core.storage.config.build_config("certificate").await?;
+ let mut certificates = self.inner.data.tls_certificates.load().as_ref().clone();
parse_certificates(&mut config, &mut certificates, &mut Default::default());
- self.tls.certificates.store(certificates.into());
+ self.inner.data.tls_certificates.store(certificates.into());
Ok(config.into())
}
pub async fn reload_lookups(&self) -> trc::Result<ReloadResult> {
- let mut config = self.storage.config.build_config("lookup").await?;
+ let mut config = self.core.storage.config.build_config("lookup").await?;
let mut stores = Stores::default();
stores.parse_memory_stores(&mut config);
- let mut core = self.clone();
+ let mut core = self.core.as_ref().clone();
for (id, store) in stores.lookup_stores {
core.storage.lookups.insert(id, store);
}
@@ -81,14 +68,14 @@ impl Core {
}
pub async fn reload(&self) -> trc::Result<ReloadResult> {
- let mut config = self.storage.config.build_config("").await?;
+ let mut config = self.core.storage.config.build_config("").await?;
// Load stores
let mut stores = Stores {
- stores: self.storage.stores.clone(),
- blob_stores: self.storage.blobs.clone(),
- fts_stores: self.storage.ftss.clone(),
- lookup_stores: self.storage.lookups.clone(),
+ stores: self.core.storage.stores.clone(),
+ blob_stores: self.core.storage.blobs.clone(),
+ fts_stores: self.core.storage.ftss.clone(),
+ lookup_stores: self.core.storage.lookups.clone(),
purge_schedules: Default::default(),
};
stores.parse_stores(&mut config).await;
@@ -103,8 +90,10 @@ impl Core {
// Build manager
let manager = ConfigManager {
- cfg_local: ArcSwap::from_pointee(self.storage.config.cfg_local.load().as_ref().clone()),
- cfg_local_path: self.storage.config.cfg_local_path.clone(),
+ cfg_local: ArcSwap::from_pointee(
+ self.core.storage.config.cfg_local.load().as_ref().clone(),
+ ),
+ cfg_local_path: self.core.storage.config.cfg_local_path.clone(),
cfg_local_patterns: Patterns::parse(&mut config).into(),
cfg_store: config
.value("storage.data")
@@ -114,26 +103,29 @@ impl Core {
};
// Parse settings and build shared core
- let mut core = Core::parse(&mut config, stores, manager).await;
+ let core = Core::parse(&mut config, stores, manager).await;
if !config.errors.is_empty() {
return Ok(config.into());
}
- // Copy ACME certificates
- let mut certificates = core.tls.certificates.load().as_ref().clone();
- for (cert_id, cert) in self.tls.certificates.load().iter() {
- certificates
- .entry(cert_id.to_string())
- .or_insert(cert.clone());
+ // Update TLS certificates
+ let mut new_certificates = AHashMap::new();
+ parse_certificates(&mut config, &mut new_certificates, &mut Default::default());
+ let mut current_certificates = self.inner.data.tls_certificates.load().as_ref().clone();
+ for (cert_id, cert) in new_certificates {
+ current_certificates.insert(cert_id, cert);
}
- core.tls.certificates.store(certificates.into());
- core.tls
- .self_signed_cert
- .clone_from(&self.tls.self_signed_cert);
+ self.inner
+ .data
+ .tls_certificates
+ .store(current_certificates.into());
+
+ // Update blocked IPs
+ *self.inner.data.blocked_ips.write() = BlockedIps::parse(&mut config).blocked_ip_addresses;
// Parser servers
- let mut servers = Servers::parse(&mut config);
- servers.parse_tcp_acceptors(&mut config, core.clone().into_shared());
+ let mut servers = Listeners::parse(&mut config);
+ servers.parse_tcp_acceptors(&mut config, self.inner.clone());
Ok(if config.errors.is_empty() {
ReloadResult {
diff --git a/crates/common/src/scripts/plugins/bayes.rs b/crates/common/src/scripts/plugins/bayes.rs
index 5250cac9..814f5a97 100644
--- a/crates/common/src/scripts/plugins/bayes.rs
+++ b/crates/common/src/scripts/plugins/bayes.rs
@@ -43,8 +43,8 @@ pub async fn exec_untrain(ctx: PluginContext<'_>) -> trc::Result<Variable> {
async fn train(ctx: PluginContext<'_>, is_train: bool) -> trc::Result<Variable> {
let store = match &ctx.arguments[0] {
- Variable::String(v) if !v.is_empty() => ctx.core.storage.lookups.get(v.as_ref()),
- _ => Some(&ctx.core.storage.lookup),
+ Variable::String(v) if !v.is_empty() => ctx.server.core.storage.lookups.get(v.as_ref()),
+ _ => Some(&ctx.server.core.storage.lookup),
}
.ok_or_else(|| {
trc::SieveEvent::RuntimeError
@@ -80,7 +80,7 @@ async fn train(ctx: PluginContext<'_>, is_train: bool) -> trc::Result<Variable>
);
// Update weight and invalidate cache
- let bayes_cache = &ctx.cache.bayes_cache;
+ let bayes_cache = &ctx.server.inner.data.bayes_cache;
if is_train {
for (hash, weights) in model.weights {
store
@@ -129,8 +129,8 @@ async fn train(ctx: PluginContext<'_>, is_train: bool) -> trc::Result<Variable>
pub async fn exec_classify(ctx: PluginContext<'_>) -> trc::Result<Variable> {
let store = match &ctx.arguments[0] {
- Variable::String(v) if !v.is_empty() => ctx.core.storage.lookups.get(v.as_ref()),
- _ => Some(&ctx.core.storage.lookup),
+ Variable::String(v) if !v.is_empty() => ctx.server.core.storage.lookups.get(v.as_ref()),
+ _ => Some(&ctx.server.core.storage.lookup),
}
.ok_or_else(|| {
trc::SieveEvent::RuntimeError
@@ -162,7 +162,7 @@ pub async fn exec_classify(ctx: PluginContext<'_>) -> trc::Result<Variable> {
}
// Obtain training counts
- let bayes_cache = &ctx.cache.bayes_cache;
+ let bayes_cache = &ctx.server.inner.data.bayes_cache;
let (spam_learns, ham_learns) = bayes_cache
.get_or_update(TokenHash::default(), store)
.await
@@ -219,8 +219,8 @@ pub async fn exec_is_balanced(ctx: PluginContext<'_>) -> trc::Result<Variable> {
}
let store = match &ctx.arguments[0] {
- Variable::String(v) if !v.is_empty() => ctx.core.storage.lookups.get(v.as_ref()),
- _ => Some(&ctx.core.storage.lookup),
+ Variable::String(v) if !v.is_empty() => ctx.server.core.storage.lookups.get(v.as_ref()),
+ _ => Some(&ctx.server.core.storage.lookup),
}
.ok_or_else(|| {
trc::SieveEvent::RuntimeError
@@ -231,7 +231,7 @@ pub async fn exec_is_balanced(ctx: PluginContext<'_>) -> trc::Result<Variable> {
let learn_spam = ctx.arguments[1].to_bool();
// Obtain training counts
- let bayes_cache = &ctx.cache.bayes_cache;
+ let bayes_cache = &ctx.server.inner.data.bayes_cache;
let (spam_learns, ham_learns) = bayes_cache
.get_or_update(TokenHash::default(), store)
.await
diff --git a/crates/common/src/scripts/plugins/dns.rs b/crates/common/src/scripts/plugins/dns.rs
index cdbacb17..6e145ddd 100644
--- a/crates/common/src/scripts/plugins/dns.rs
+++ b/crates/common/src/scripts/plugins/dns.rs
@@ -25,6 +25,7 @@ pub async fn exec(ctx: PluginContext<'_>) -> trc::Result<Variable> {
Ok(if record_type.eq_ignore_ascii_case("ip") {
match ctx
+ .server
.core
.smtp
.resolvers
@@ -40,7 +41,15 @@ pub async fn exec(ctx: PluginContext<'_>) -> trc::Result<Variable> {
Err(err) => err.short_error().into(),
}
} else if record_type.eq_ignore_ascii_case("mx") {
- match ctx.core.smtp.resolvers.dns.mx_lookup(entry.as_ref()).await {
+ match ctx
+ .server
+ .core
+ .smtp
+ .resolvers
+ .dns
+ .mx_lookup(entry.as_ref())
+ .await
+ {
Ok(result) => result
.iter()
.flat_map(|mx| {
@@ -61,6 +70,7 @@ pub async fn exec(ctx: PluginContext<'_>) -> trc::Result<Variable> {
}
match ctx
+ .server
.core
.smtp
.resolvers
@@ -73,7 +83,7 @@ pub async fn exec(ctx: PluginContext<'_>) -> trc::Result<Variable> {
}
} else if record_type.eq_ignore_ascii_case("ptr") {
if let Ok(addr) = entry.parse::<IpAddr>() {
- match ctx.core.smtp.resolvers.dns.ptr_lookup(addr).await {
+ match ctx.server.core.smtp.resolvers.dns.ptr_lookup(addr).await {
Ok(result) => result
.iter()
.map(|host| Variable::from(host.to_string()))
@@ -94,6 +104,7 @@ pub async fn exec(ctx: PluginContext<'_>) -> trc::Result<Variable> {
}
match ctx
+ .server
.core
.smtp
.resolvers
@@ -110,6 +121,7 @@ pub async fn exec(ctx: PluginContext<'_>) -> trc::Result<Variable> {
}
} else if record_type.eq_ignore_ascii_case("ipv6") {
match ctx
+ .server
.core
.smtp
.resolvers
@@ -135,6 +147,7 @@ pub async fn exec_exists(ctx: PluginContext<'_>) -> trc::Result<Variable> {
Ok(if record_type.eq_ignore_ascii_case("ip") {
match ctx
+ .server
.core
.smtp
.resolvers
@@ -147,14 +160,22 @@ pub async fn exec_exists(ctx: PluginContext<'_>) -> trc::Result<Variable> {
Err(_) => -1,
}
} else if record_type.eq_ignore_ascii_case("mx") {
- match ctx.core.smtp.resolvers.dns.mx_lookup(entry.as_ref()).await {
+ match ctx
+ .server
+ .core
+ .smtp
+ .resolvers
+ .dns
+ .mx_lookup(entry.as_ref())
+ .await
+ {
Ok(result) => i64::from(result.iter().any(|mx| !mx.exchanges.is_empty())),
Err(Error::DnsRecordNotFound(_)) => 0,
Err(_) => -1,
}
} else if record_type.eq_ignore_ascii_case("ptr") {
if let Ok(addr) = entry.parse::<IpAddr>() {
- match ctx.core.smtp.resolvers.dns.ptr_lookup(addr).await {
+ match ctx.server.core.smtp.resolvers.dns.ptr_lookup(addr).await {
Ok(result) => i64::from(!result.is_empty()),
Err(Error::DnsRecordNotFound(_)) => 0,
Err(_) => -1,
@@ -171,6 +192,7 @@ pub async fn exec_exists(ctx: PluginContext<'_>) -> trc::Result<Variable> {
}
match ctx
+ .server
.core
.smtp
.resolvers
@@ -184,6 +206,7 @@ pub async fn exec_exists(ctx: PluginContext<'_>) -> trc::Result<Variable> {
}
} else if record_type.eq_ignore_ascii_case("ipv6") {
match ctx
+ .server
.core
.smtp
.resolvers
diff --git a/crates/common/src/scripts/plugins/lookup.rs b/crates/common/src/scripts/plugins/lookup.rs
index f19721c5..1d82ba93 100644
--- a/crates/common/src/scripts/plugins/lookup.rs
+++ b/crates/common/src/scripts/plugins/lookup.rs
@@ -42,8 +42,8 @@ pub fn register_local_domain(plugin_id: u32, fnc_map: &mut FunctionMap) {
pub async fn exec(ctx: PluginContext<'_>) -> trc::Result<Variable> {
let store = match &ctx.arguments[0] {
- Variable::String(v) if !v.is_empty() => ctx.core.storage.lookups.get(v.as_ref()),
- _ => Some(&ctx.core.storage.lookup),
+ Variable::String(v) if !v.is_empty() => ctx.server.core.storage.lookups.get(v.as_ref()),
+ _ => Some(&ctx.server.core.storage.lookup),
}
.ok_or_else(|| {
trc::SieveEvent::RuntimeError
@@ -76,8 +76,8 @@ pub async fn exec(ctx: PluginContext<'_>) -> trc::Result<Variable> {
pub async fn exec_get(ctx: PluginContext<'_>) -> trc::Result<Variable> {
match &ctx.arguments[0] {
- Variable::String(v) if !v.is_empty() => ctx.core.storage.lookups.get(v.as_ref()),
- _ => Some(&ctx.core.storage.lookup),
+ Variable::String(v) if !v.is_empty() => ctx.server.core.storage.lookups.get(v.as_ref()),
+ _ => Some(&ctx.server.core.storage.lookup),
}
.ok_or_else(|| {
trc::SieveEvent::RuntimeError
@@ -97,8 +97,8 @@ pub async fn exec_set(ctx: PluginContext<'_>) -> trc::Result<Variable> {
};
match &ctx.arguments[0] {
- Variable::String(v) if !v.is_empty() => ctx.core.storage.lookups.get(v.as_ref()),
- _ => Some(&ctx.core.storage.lookup),
+ Variable::String(v) if !v.is_empty() => ctx.server.core.storage.lookups.get(v.as_ref()),
+ _ => Some(&ctx.server.core.storage.lookup),
}
.ok_or_else(|| {
trc::SieveEvent::RuntimeError
@@ -125,7 +125,7 @@ pub async fn exec_remote(ctx: PluginContext<'_>) -> trc::Result<Variable> {
// Something went wrong, try again in one hour
const RETRY: Duration = Duration::from_secs(3600);
- let mut _lock = ctx.cache.remote_lists.write();
+ let mut _lock = ctx.server.inner.data.remote_lists.write();
let list = _lock
.entry(ctx.arguments[0].to_string().to_string())
.or_insert_with(|| RemoteList {
@@ -169,7 +169,14 @@ async fn exec_remote_(ctx: &PluginContext<'_>) -> trc::Result<Variable> {
const MAX_ENTRY_SIZE: usize = 256;
const MAX_ENTRIES: usize = 100000;
- match ctx.cache.remote_lists.read().get(resource.as_ref()) {
+ match ctx
+ .server
+ .inner
+ .data
+ .remote_lists
+ .read()
+ .get(resource.as_ref())
+ {
Some(remote_list) if remote_list.expires < Instant::now() => {
return Ok(remote_list.entries.contains(item.as_ref()).into())
}
@@ -256,7 +263,7 @@ async fn exec_remote_(ctx: &PluginContext<'_>) -> trc::Result<Variable> {
};
// Lock remote list for writing
- let mut _lock = ctx.cache.remote_lists.write();
+ let mut _lock = ctx.server.inner.data.remote_lists.write();
let list = _lock
.entry(resource.to_string())
.or_insert_with(|| RemoteList {
@@ -352,8 +359,10 @@ pub async fn exec_local_domain(ctx: PluginContext<'_>) -> trc::Result<Variable>
if !domain.is_empty() {
return match &ctx.arguments[0] {
- Variable::String(v) if !v.is_empty() => ctx.core.storage.directories.get(v.as_ref()),
- _ => Some(&ctx.core.storage.directory),
+ Variable::String(v) if !v.is_empty() => {
+ ctx.server.core.storage.directories.get(v.as_ref())
+ }
+ _ => Some(&ctx.server.core.storage.directory),
}
.ok_or_else(|| {
trc::SieveEvent::RuntimeError
diff --git a/crates/common/src/scripts/plugins/mod.rs b/crates/common/src/scripts/plugins/mod.rs
index f2802138..ba681eee 100644
--- a/crates/common/src/scripts/plugins/mod.rs
+++ b/crates/common/src/scripts/plugins/mod.rs
@@ -17,7 +17,7 @@ pub mod text;
use mail_parser::Message;
use sieve::{runtime::Variable, FunctionMap, Input};
-use crate::{config::scripts::ScriptCache, Core};
+use crate::{Core, Server};
use super::ScriptModification;
@@ -25,8 +25,7 @@ type RegisterPluginFnc = fn(u32, &mut FunctionMap) -> ();
pub struct PluginContext<'x> {
pub session_id: u64,
- pub core: &'x Core,
- pub cache: &'x ScriptCache,
+ pub server: &'x Server,
pub message: &'x Message<'x>,
pub modifications: &'x mut Vec<ScriptModification>,
pub arguments: Vec<Variable>,
diff --git a/crates/common/src/scripts/plugins/query.rs b/crates/common/src/scripts/plugins/query.rs
index 356808ae..15092caf 100644
--- a/crates/common/src/scripts/plugins/query.rs
+++ b/crates/common/src/scripts/plugins/query.rs
@@ -19,8 +19,8 @@ pub fn register(plugin_id: u32, fnc_map: &mut FunctionMap) {
pub async fn exec(ctx: PluginContext<'_>) -> trc::Result<Variable> {
// Obtain store name
let store = match &ctx.arguments[0] {
- Variable::String(v) if !v.is_empty() => ctx.core.storage.lookups.get(v.as_ref()),
- _ => Some(&ctx.core.storage.lookup),
+ Variable::String(v) if !v.is_empty() => ctx.server.core.storage.lookups.get(v.as_ref()),
+ _ => Some(&ctx.server.core.storage.lookup),
}
.ok_or_else(|| {
trc::SieveEvent::RuntimeError
diff --git a/crates/common/src/telemetry/metrics/otel.rs b/crates/common/src/telemetry/metrics/otel.rs
index a4e10c9b..7ce117f8 100644
--- a/crates/common/src/telemetry/metrics/otel.rs
+++ b/crates/common/src/telemetry/metrics/otel.rs
@@ -4,7 +4,7 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
-use std::{sync::Arc, time::SystemTime};
+use std::time::SystemTime;
use opentelemetry::global::set_error_handler;
use opentelemetry_sdk::metrics::data::{
@@ -13,19 +13,13 @@ use opentelemetry_sdk::metrics::data::{
};
use trc::{Collector, TelemetryEvent};
-use crate::{config::telemetry::OtelMetrics, Core};
+use crate::config::telemetry::OtelMetrics;
impl OtelMetrics {
- pub async fn push_metrics(&self, core: Arc<Core>, start_time: SystemTime) {
+ pub async fn push_metrics(&self, is_enterprise: bool, start_time: SystemTime) {
let mut metrics = Vec::with_capacity(256);
let now = SystemTime::now();
- #[cfg(feature = "enterprise")]
- let is_enterprise = core.is_enterprise_edition();
-
- #[cfg(not(feature = "enterprise"))]
- let is_enterprise = false;
-
// Add counters
for counter in Collector::collect_counters(is_enterprise) {
metrics.push(Metric {
diff --git a/crates/common/src/telemetry/metrics/prometheus.rs b/crates/common/src/telemetry/metrics/prometheus.rs
index 35ff3c01..46bcf9c7 100644
--- a/crates/common/src/telemetry/metrics/prometheus.rs
+++ b/crates/common/src/telemetry/metrics/prometheus.rs
@@ -10,9 +10,9 @@ use prometheus::{
};
use trc::{atomics::histogram::AtomicHistogram, Collector};
-use crate::Core;
+use crate::Server;
-impl Core {
+impl Server {
pub async fn export_prometheus_metrics(&self) -> trc::Result<String> {
let mut metrics = Vec::new();
diff --git a/crates/directory/src/backend/internal/mod.rs b/crates/directory/src/backend/internal/mod.rs
index ee676080..9806d184 100644
--- a/crates/directory/src/backend/internal/mod.rs
+++ b/crates/directory/src/backend/internal/mod.rs
@@ -274,22 +274,26 @@ impl MigrateDirectory for Store {
},
),
|key, value| {
- if key[0] == 2 && value[0] == 1 {
- principals.push((
- key.get(1..)
- .and_then(|b| b.read_leb128::<u32>().map(|(v, _)| v))
- .ok_or_else(|| {
- trc::StoreEvent::DataCorruption
- .caused_by(trc::location!())
- .ctx(trc::Key::Value, key)
- })?,
- Principal::deserialize(value)?,
- ));
- } else if key[0] == 3 {
- let domain = std::str::from_utf8(&key[1..]).unwrap_or_default();
- if !domain.is_empty() {
- domains.push(domain.to_string());
+ match (key.first(), value.first()) {
+ (Some(2), Some(1)) => {
+ principals.push((
+ key.get(1..)
+ .and_then(|b| b.read_leb128::<u32>().map(|(v, _)| v))
+ .ok_or_else(|| {
+ trc::StoreEvent::DataCorruption
+ .caused_by(trc::location!())
+ .ctx(trc::Key::Value, key)
+ })?,
+ Principal::deserialize(value)?,
+ ));
}
+ (Some(3), _) => {
+ let domain = std::str::from_utf8(&key[1..]).unwrap_or_default();
+ if !domain.is_empty() {
+ domains.push(domain.to_string());
+ }
+ }
+ _ => {}
}
Ok(true)
diff --git a/crates/imap/src/core/client.rs b/crates/imap/src/core/client.rs
index 1952da3c..a6bea4e1 100644
--- a/crates/imap/src/core/client.rs
+++ b/crates/imap/src/core/client.rs
@@ -6,12 +6,14 @@
use std::{iter::Peekable, sync::Arc, vec::IntoIter};
-use common::listener::{limiter::ConcurrencyLimiter, SessionResult, SessionStream};
+use common::{
+ listener::{limiter::ConcurrencyLimiter, SessionResult, SessionStream},
+ ConcurrencyLimiters,
+};
use imap_proto::{
receiver::{self, Request},
Command, ResponseType, StatusResponse,
};
-use jmap::auth::rate_limit::ConcurrencyLimiters;
use super::{SelectedMailbox, Session, SessionData, State};
@@ -255,9 +257,9 @@ impl<T: SessionStream> Session<T> {
let state = &self.state;
// Rate limit request
if let State::Authenticated { data } | State::Selected { data, .. } = state {
- if let Some(rate) = &self.jmap.core.imap.rate_requests {
+ if let Some(rate) = &self.server.core.imap.rate_requests {
if data
- .jmap
+ .server
.core
.storage
.lookup
@@ -301,7 +303,7 @@ impl<T: SessionStream> Session<T> {
}
Command::Login => {
if let State::NotAuthenticated { .. } = state {
- if self.is_tls || self.jmap.core.imap.allow_plain_auth {
+ if self.is_tls || self.server.core.imap.allow_plain_auth {
Ok(request)
} else {
Err(trc::ImapEvent::Error
@@ -385,9 +387,11 @@ impl<T: SessionStream> Session<T> {
}
pub fn get_concurrency_limiter(&self, account_id: u32) -> Option<Arc<ConcurrencyLimiters>> {
- let rate = self.jmap.core.imap.rate_concurrent?;
- self.imap
- .rate_limiter
+ let rate = self.server.core.imap.rate_concurrent?;
+ self.server
+ .inner
+ .data
+ .imap_limiter
.get(&account_id)
.map(|limiter| limiter.clone())
.unwrap_or_else(|| {
@@ -395,7 +399,11 @@ impl<T: SessionStream> Session<T> {
concurrent_requests: ConcurrencyLimiter::new(rate),
concurrent_uploads: ConcurrencyLimiter::new(rate),
});
- self.imap.rate_limiter.insert(account_id, limiter.clone());
+ self.server
+ .inner
+ .data
+ .imap_limiter
+ .insert(account_id, limiter.clone());
limiter
})
.into()
diff --git a/crates/imap/src/core/mailbox.rs b/crates/imap/src/core/mailbox.rs
index 4a780ee6..5f872df6 100644
--- a/crates/imap/src/core/mailbox.rs
+++ b/crates/imap/src/core/mailbox.rs
@@ -8,10 +8,16 @@ use common::{
auth::AccessToken,
config::jmap::settings::SpecialUse,
listener::{limiter::InFlight, SessionStream},
+ AccountId, Mailbox,
};
use directory::{backend::internal::PrincipalField, QueryBy};
use imap_proto::protocol::list::Attribute;
-use jmap::{auth::acl::EffectiveAcl, mailbox::INBOX_ID};
+use jmap::{
+ auth::acl::{AclMethods, EffectiveAcl},
+ changes::get::ChangesLookup,
+ mailbox::{get::MailboxGet, set::MailboxSet, INBOX_ID},
+ JmapMethods,
+};
use jmap_proto::{
object::Object,
types::{acl::Acl, collection::Collection, id::Id, property::Property, value::Value},
@@ -21,7 +27,7 @@ use store::query::log::{Change, Query};
use trc::AddContext;
use utils::lru_cache::LruCached;
-use super::{Account, AccountId, Mailbox, MailboxId, MailboxSync, Session, SessionData};
+use super::{Account, MailboxId, MailboxSync, Session, SessionData};
impl<T: SessionStream> SessionData<T> {
pub async fn new(
@@ -31,8 +37,7 @@ impl<T: SessionStream> SessionData<T> {
) -> trc::Result<Self> {
let mut session = SessionData {
stream_tx: session.stream_tx.clone(),
- jmap: session.jmap.clone(),
- imap: session.imap.clone(),
+ server: session.server.clone(),
account_id: access_token.primary_id(),
session_id: session.session_id,
mailboxes: Mutex::new(vec![]),
@@ -56,9 +61,9 @@ impl<T: SessionStream> SessionData<T> {
account_id,
format!(
"{}/{}",
- session.jmap.core.jmap.shared_folder,
+ session.server.core.jmap.shared_folder,
session
- .jmap
+ .server
.core
.storage
.directory
@@ -88,7 +93,7 @@ impl<T: SessionStream> SessionData<T> {
access_token: &AccessToken,
) -> trc::Result<Account> {
let state_mailbox = self
- .jmap
+ .server
.core
.storage
.data
@@ -96,7 +101,7 @@ impl<T: SessionStream> SessionData<T> {
.await
.caused_by(trc::location!())?;
let state_email = self
- .jmap
+ .server
.core
.storage
.data
@@ -107,19 +112,21 @@ impl<T: SessionStream> SessionData<T> {
account_id,
primary_id: access_token.primary_id(),
};
- if let Some(cached_account) =
- self.imap
- .cache_account
- .get(&cached_account_id)
- .and_then(|cached_account| {
- if cached_account.state_mailbox == state_mailbox
- && cached_account.state_email == state_email
- {
- Some(cached_account)
- } else {
- None
- }
- })
+ if let Some(cached_account) = self
+ .server
+ .inner
+ .data
+ .account_cache
+ .get(&cached_account_id)
+ .and_then(|cached_account| {
+ if cached_account.state_mailbox == state_mailbox
+ && cached_account.state_email == state_email
+ {
+ Some(cached_account)
+ } else {
+ None
+ }
+ })
{
return Ok(cached_account.as_ref().clone());
}
@@ -127,12 +134,12 @@ impl<T: SessionStream> SessionData<T> {
let mailbox_ids = if access_token.is_primary_id(account_id)
|| access_token.member_of.contains(&account_id)
{
- self.jmap
+ self.server
.mailbox_get_or_create(account_id)
.await
.caused_by(trc::location!())?
} else {
- self.jmap
+ self.server
.shared_documents(access_token, account_id, Collection::Mailbox, Acl::Read)
.await
.caused_by(trc::location!())?
@@ -142,7 +149,7 @@ impl<T: SessionStream> SessionData<T> {
let mut mailboxes = Vec::with_capacity(10);
let mut special_uses = AHashMap::new();
for (mailbox_id, values) in self
- .jmap
+ .server
.get_properties::<Object<Value>, _, _>(
account_id,
Collection::Mailbox,
@@ -189,7 +196,7 @@ impl<T: SessionStream> SessionData<T> {
let mut path = Vec::new();
let mut iter_stack = Vec::new();
let message_ids = self
- .jmap
+ .server
.get_document_ids(account_id, Collection::Email)
.await
.caused_by(trc::location!())?;
@@ -246,7 +253,7 @@ impl<T: SessionStream> SessionData<T> {
},
),
total_messages: self
- .jmap
+ .server
.get_tag(
account_id,
Collection::Email,
@@ -259,7 +266,7 @@ impl<T: SessionStream> SessionData<T> {
.unwrap_or(0)
.into(),
total_unseen: self
- .jmap
+ .server
.mailbox_unread_tags(account_id, *mailbox_id, &message_ids)
.await
.caused_by(trc::location!())?
@@ -278,7 +285,7 @@ impl<T: SessionStream> SessionData<T> {
// Map special use folder aliases to their internal ids
let effective_mailbox_id = self
- .jmap
+ .server
.core
.jmap
.default_folders
@@ -313,8 +320,10 @@ impl<T: SessionStream> SessionData<T> {
}
// Update cache
- self.imap
- .cache_account
+ self.server
+ .inner
+ .data
+ .account_cache
.insert(cached_account_id, Arc::new(account.clone()));
Ok(account)
@@ -332,8 +341,7 @@ impl<T: SessionStream> SessionData<T> {
// Obtain access token
let access_token = self
- .jmap
- .core
+ .server
.get_cached_access_token(self.account_id)
.await
.caused_by(trc::location!())?;
@@ -383,8 +391,8 @@ impl<T: SessionStream> SessionData<T> {
for account_id in added_account_ids {
let prefix = format!(
"{}/{}",
- self.jmap.core.jmap.shared_folder,
- self.jmap
+ self.server.core.jmap.shared_folder,
+ self.server
.core
.storage
.directory
@@ -414,7 +422,7 @@ impl<T: SessionStream> SessionData<T> {
.collect::<Vec<_>>();
for (account_id, last_state) in account_states {
let changelog = self
- .jmap
+ .server
.changes_(
account_id,
Collection::Mailbox,
@@ -437,7 +445,7 @@ impl<T: SessionStream> SessionData<T> {
if has_child_changes && !has_changes && changes.is_none() {
// Only child changes, no need to re-fetch mailboxes
let state_email = self
- .jmap
+ .server
.core
.storage
.data
@@ -461,8 +469,13 @@ impl<T: SessionStream> SessionData<T> {
}
// Update cache
- if let Some(cached_account_) =
- self.imap.cache_account.lock().get_mut(&AccountId {
+ if let Some(cached_account_) = self
+ .server
+ .inner
+ .data
+ .account_cache
+ .lock()
+ .get_mut(&AccountId {
account_id,
primary_id: access_token.primary_id(),
})
@@ -488,8 +501,8 @@ impl<T: SessionStream> SessionData<T> {
let mailbox_prefix = if !access_token.is_primary_id(account_id) {
format!(
"{}/{}",
- self.jmap.core.jmap.shared_folder,
- self.jmap
+ self.server.core.jmap.shared_folder,
+ self.server
.core
.storage
.directory
@@ -613,7 +626,7 @@ impl<T: SessionStream> SessionData<T> {
let access_token = self.get_access_token().await?;
Ok(access_token.is_member(account_id)
|| self
- .jmap
+ .server
.get_property::<Object<Value>>(
account_id,
Collection::Mailbox,
diff --git a/crates/imap/src/core/message.rs b/crates/imap/src/core/message.rs
index 2a3df644..0706eb89 100644
--- a/crates/imap/src/core/message.rs
+++ b/crates/imap/src/core/message.rs
@@ -7,9 +7,9 @@
use std::{collections::BTreeMap, sync::Arc};
use ahash::AHashMap;
-use common::listener::SessionStream;
+use common::{listener::SessionStream, NextMailboxState};
use imap_proto::protocol::{expunge, select::Exists, Sequence};
-use jmap::mailbox::UidMailbox;
+use jmap::{mailbox::UidMailbox, JmapMethods};
use jmap_proto::{
object::Object,
types::{collection::Collection, property::Property, value::Value},
@@ -20,7 +20,7 @@ use utils::lru_cache::LruCached;
use crate::core::ImapId;
-use super::{ImapUidToId, MailboxId, MailboxState, NextMailboxState, SelectedMailbox, SessionData};
+use super::{ImapUidToId, MailboxId, MailboxState, SelectedMailbox, SessionData};
pub(crate) const MAX_RETRIES: usize = 10;
@@ -28,7 +28,7 @@ impl<T: SessionStream> SessionData<T> {
pub async fn fetch_messages(&self, mailbox: &MailboxId) -> trc::Result<MailboxState> {
// Obtain message ids
let message_ids = self
- .jmap
+ .server
.get_tag(
mailbox.account_id,
Collection::Email,
@@ -43,7 +43,7 @@ impl<T: SessionStream> SessionData<T> {
// Obtain current state
let modseq = self
- .jmap
+ .server
.core
.storage
.data
@@ -54,7 +54,7 @@ impl<T: SessionStream> SessionData<T> {
// Obtain all message ids
let mut uid_map = BTreeMap::new();
for (message_id, uid_mailbox) in self
- .jmap
+ .server
.get_properties::<HashedValue<Vec<UidMailbox>>, _, _>(
mailbox.account_id,
Collection::Email,
@@ -148,8 +148,10 @@ impl<T: SessionStream> SessionData<T> {
current_state.id_to_imap = id_to_imap;
// Update cache
- self.imap
- .cache_mailbox
+ self.server
+ .inner
+ .data
+ .mailbox_cache
.insert(mailbox.id, Arc::new(new_state.clone()));
// Update state
@@ -207,7 +209,7 @@ impl<T: SessionStream> SessionData<T> {
pub async fn get_modseq(&self, account_id: u32) -> trc::Result<Option<u64>> {
// Obtain current modseq
- self.jmap
+ self.server
.core
.storage
.data
@@ -221,7 +223,7 @@ impl<T: SessionStream> SessionData<T> {
}
pub async fn get_uid_validity(&self, mailbox: &MailboxId) -> trc::Result<u32> {
- self.jmap
+ self.server
.get_property::<Object<Value>>(
mailbox.account_id,
Collection::Mailbox,
diff --git a/crates/imap/src/core/mod.rs b/crates/imap/src/core/mod.rs
index b8175d67..db92118c 100644
--- a/crates/imap/src/core/mod.rs
+++ b/crates/imap/src/core/mod.rs
@@ -5,29 +5,21 @@
*/
use std::{
- collections::BTreeMap,
net::IpAddr,
sync::{atomic::AtomicU32, Arc},
};
-use ahash::AHashMap;
use common::{
auth::AccessToken,
listener::{limiter::InFlight, ServerInstance, SessionStream},
+ Account, ImapId, Inner, MailboxId, MailboxState, Server,
};
-use dashmap::DashMap;
-use imap_proto::{
- protocol::{list::Attribute, ProtocolVersion},
- receiver::Receiver,
- Command,
-};
-use jmap::{auth::rate_limit::ConcurrencyLimiters, JmapInstance, JMAP};
+use imap_proto::{protocol::ProtocolVersion, receiver::Receiver, Command};
use tokio::{
io::{ReadHalf, WriteHalf},
sync::watch,
};
use trc::AddContext;
-use utils::lru_cache::LruCache;
pub mod client;
pub mod mailbox;
@@ -36,32 +28,17 @@ pub mod session;
#[derive(Clone)]
pub struct ImapSessionManager {
- pub imap: ImapInstance,
+ pub inner: Arc<Inner>,
}
impl ImapSessionManager {
- pub fn new(imap: ImapInstance) -> Self {
- Self { imap }
+ pub fn new(inner: Arc<Inner>) -> Self {
+ Self { inner }
}
}
-#[derive(Clone)]
-pub struct ImapInstance {
- pub jmap_instance: JmapInstance,
- pub imap_inner: Arc<Inner>,
-}
-
-pub struct Inner {
- pub rate_limiter: DashMap<u32, Arc<ConcurrencyLimiters>>,
- pub cache_account: LruCache<AccountId, Arc<Account>>,
- pub cache_mailbox: LruCache<MailboxId, Arc<MailboxState>>,
-}
-
-pub struct IMAP {}
-
pub struct Session<T: SessionStream> {
- pub jmap: JMAP,
- pub imap: Arc<Inner>,
+ pub server: Server,
pub instance: Arc<ServerInstance>,
pub receiver: Receiver<Command>,
pub version: ProtocolVersion,
@@ -79,8 +56,7 @@ pub struct Session<T: SessionStream> {
pub struct SessionData<T: SessionStream> {
pub account_id: u32,
pub access_token: Arc<AccessToken>,
- pub jmap: JMAP,
- pub imap: Arc<Inner>,
+ pub server: Server,
pub session_id: u64,
pub mailboxes: parking_lot::Mutex<Vec<Account>>,
pub stream_tx: Arc<tokio::sync::Mutex<WriteHalf<T>>>,
@@ -88,29 +64,6 @@ pub struct SessionData<T: SessionStream> {
pub in_flight: Option<InFlight>,
}
-#[derive(Debug, Default, Clone)]
-pub struct Mailbox {
- pub has_children: bool,
- pub is_subscribed: bool,
- pub special_use: Option<Attribute>,
- pub total_messages: Option<u32>,
- pub total_unseen: Option<u32>,
- pub total_deleted: Option<u32>,
- pub uid_validity: Option<u32>,
- pub uid_next: Option<u32>,
- pub size: Option<u32>,
-}
-
-#[derive(Debug, Clone, Default)]
-pub struct Account {
- pub account_id: u32,
- pub prefix: Option<String>,
- pub mailbox_names: BTreeMap<String, u32>,
- pub mailbox_state: AHashMap<u32, Mailbox>,
- pub state_email: Option<u64>,
- pub state_mailbox: Option<u64>,
-}
-
pub struct SelectedMailbox {
pub id: MailboxId,
pub state: parking_lot::Mutex<MailboxState>,
@@ -119,36 +72,6 @@ pub struct SelectedMailbox {
pub is_condstore: bool,
}
-#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
-pub struct MailboxId {
- pub account_id: u32,
- pub mailbox_id: u32,
-}
-
-#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
-pub struct AccountId {
- pub account_id: u32,
- pub primary_id: u32,
-}
-
-#[derive(Debug, Clone, Default)]
-pub struct MailboxState {
- pub uid_next: u32,
- pub uid_validity: u32,
- pub uid_max: u32,
- pub id_to_imap: AHashMap<u32, ImapId>,
- pub uid_to_id: AHashMap<u32, u32>,
- pub total_messages: usize,
- pub modseq: Option<u64>,
- pub next_state: Option<Box<NextMailboxState>>,
-}
-
-#[derive(Debug, Clone)]
-pub struct NextMailboxState {
- pub next_state: MailboxState,
- pub deletions: Vec<ImapId>,
-}
-
#[derive(Debug, Default)]
pub struct MailboxSync {
pub added: Vec<String>,
@@ -167,12 +90,6 @@ pub enum SavedSearch {
}
#[derive(Debug, Clone, Copy, Default)]
-pub struct ImapId {
- pub uid: u32,
- pub seqnum: u32,
-}
-
-#[derive(Debug, Clone, Copy, Default)]
pub struct ImapUidToId {
pub uid: u32,
pub id: u32,
@@ -217,8 +134,7 @@ impl<T: SessionStream> State<T> {
impl<T: SessionStream> SessionData<T> {
pub async fn get_access_token(&self) -> trc::Result<Arc<AccessToken>> {
- self.jmap
- .core
+ self.server
.get_cached_access_token(self.account_id)
.await
.caused_by(trc::location!())
@@ -230,8 +146,7 @@ impl<T: SessionStream> SessionData<T> {
) -> SessionData<U> {
SessionData {
account_id: self.account_id,
- jmap: self.jmap,
- imap: self.imap,
+ server: self.server,
session_id: self.session_id,
mailboxes: self.mailboxes,
stream_tx: new_stream,
diff --git a/crates/imap/src/core/session.rs b/crates/imap/src/core/session.rs
index 8284ed67..9f2bc75e 100644
--- a/crates/imap/src/core/session.rs
+++ b/crates/imap/src/core/session.rs
@@ -6,12 +6,14 @@
use std::sync::Arc;
-use common::listener::{stream::NullIo, SessionData, SessionManager, SessionResult, SessionStream};
+use common::{
+ core::BuildServer,
+ listener::{stream::NullIo, SessionData, SessionManager, SessionResult, SessionStream},
+};
use imap_proto::{
protocol::{ProtocolVersion, SerializeResponse},
receiver::Receiver,
};
-use jmap::JMAP;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio_rustls::server::TlsStream;
@@ -51,9 +53,9 @@ impl<T: SessionStream> Session<T> {
tokio::select! {
result = tokio::time::timeout(
if !matches!(self.state, State::NotAuthenticated {..}) {
- self.jmap.core.imap.timeout_auth
+ self.server.core.imap.timeout_auth
} else {
- self.jmap.core.imap.timeout_unauth
+ self.server.core.imap.timeout_unauth
},
self.stream_rx.read(&mut buf)) => {
match result {
@@ -138,17 +140,16 @@ impl<T: SessionStream> Session<T> {
// Split stream into read and write halves
let (stream_rx, stream_tx) = tokio::io::split(session.stream);
- let jmap = JMAP::from(manager.imap.jmap_instance);
+ let server = manager.inner.build_server();
Ok(Session {
- receiver: Receiver::with_max_request_size(jmap.core.imap.max_request_size),
+ receiver: Receiver::with_max_request_size(server.core.imap.max_request_size),
version: ProtocolVersion::Rev1,
state: State::NotAuthenticated { auth_failures: 0 },
is_tls,
is_condstore: false,
is_qresync: false,
- jmap,
- imap: manager.imap.imap_inner,
+ server,
instance: session.instance,
session_id: session.session_id,
in_flight: session.in_flight,
@@ -196,8 +197,7 @@ impl<T: SessionStream> Session<T> {
let stream_tx = Arc::new(tokio::sync::Mutex::new(stream_tx));
Ok(Session {
- jmap: self.jmap,
- imap: self.imap,
+ server: self.server,
instance: self.instance,
receiver: self.receiver,
version: self.version,
diff --git a/crates/imap/src/lib.rs b/crates/imap/src/lib.rs
index e4a0eccc..f6df61e0 100644
--- a/crates/imap/src/lib.rs
+++ b/crates/imap/src/lib.rs
@@ -4,19 +4,9 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
-use core::{ImapInstance, Inner, IMAP};
-use std::{
- collections::hash_map::RandomState,
- sync::{Arc, LazyLock},
-};
+use std::sync::LazyLock;
-use dashmap::DashMap;
use imap_proto::{protocol::capability::Capability, ResponseCode, StatusResponse};
-use jmap::JmapInstance;
-use utils::{
- config::Config,
- lru_cache::{LruCache, LruCached},
-};
pub mod core;
pub mod op;
@@ -39,33 +29,4 @@ pub(crate) static GREETING_WITHOUT_TLS: LazyLock<Vec<u8>> = LazyLock::new(|| {
.into_bytes()
});
-impl IMAP {
- pub async fn init(config: &mut Config, jmap_instance: JmapInstance) -> ImapInstance {
- let shard_amount = config
- .property::<u64>("cache.shard")
- .unwrap_or(32)
- .next_power_of_two() as usize;
- let capacity = config.property("cache.capacity").unwrap_or(100);
-
- let inner = Inner {
- rate_limiter: DashMap::with_capacity_and_hasher_and_shard_amount(
- capacity,
- RandomState::default(),
- shard_amount,
- ),
- cache_account: LruCache::with_capacity(
- config.property("cache.account.size").unwrap_or(2048),
- ),
- cache_mailbox: LruCache::with_capacity(
- config.property("cache.mailbox.size").unwrap_or(2048),
- ),
- };
-
- ImapInstance {
- jmap_instance,
- imap_inner: Arc::new(inner),
- }
- }
-}
-
pub struct ImapError;
diff --git a/crates/imap/src/op/acl.rs b/crates/imap/src/op/acl.rs
index b2bc1585..f52026d3 100644
--- a/crates/imap/src/op/acl.rs
+++ b/crates/imap/src/op/acl.rs
@@ -6,7 +6,7 @@
use std::{sync::Arc, time::Instant};
-use common::{auth::AccessToken, listener::SessionStream};
+use common::{auth::AccessToken, listener::SessionStream, MailboxId};
use directory::{backend::internal::PrincipalField, Permission, QueryBy};
use imap_proto::{
protocol::acl::{
@@ -16,7 +16,10 @@ use imap_proto::{
Command, ResponseCode, StatusResponse,
};
-use jmap::{auth::acl::EffectiveAcl, mailbox::set::SCHEMA};
+use jmap::{
+ auth::acl::EffectiveAcl, changes::write::ChangeLog, mailbox::set::SCHEMA,
+ services::state::StateManager, JmapMethods,
+};
use jmap_proto::{
object::{index::ObjectIndexBuilder, Object},
types::{
@@ -33,7 +36,7 @@ use trc::AddContext;
use utils::map::bitmap::Bitmap;
use crate::{
- core::{MailboxId, Session, SessionData, State},
+ core::{Session, SessionData, State},
op::ImapContext,
spawn_op,
};
@@ -62,7 +65,7 @@ impl<T: SessionStream> Session<T> {
{
for item in acls {
if let Some(account_name) = data
- .jmap
+ .server
.core
.storage
.directory
@@ -244,7 +247,7 @@ impl<T: SessionStream> Session<T> {
// Obtain principal id
let acl_account_id = data
- .jmap
+ .server
.core
.storage
.directory
@@ -345,18 +348,18 @@ impl<T: SessionStream> Session<T> {
.with_current(values),
);
if !batch.is_empty() {
- data.jmap
+ data.server
.write_batch(batch)
.await
.imap_ctx(&arguments.tag, trc::location!())?;
let mut changes = ChangeLogBuilder::new();
changes.log_update(Collection::Mailbox, mailbox_id);
let change_id = data
- .jmap
+ .server
.commit_changes(mailbox.account_id, changes)
.await
.imap_ctx(&arguments.tag, trc::location!())?;
- data.jmap
+ data.server
.broadcast_state_change(
StateChange::new(mailbox.account_id)
.with_change(DataType::Mailbox, change_id),
@@ -365,11 +368,7 @@ impl<T: SessionStream> Session<T> {
}
// Invalidate ACLs
- data.jmap
- .core
- .security
- .access_tokens
- .remove(&acl_account_id);
+ data.server.inner.data.access_tokens.remove(&acl_account_id);
trc::event!(
Imap(trc::ImapEvent::SetAcl),
@@ -447,7 +446,7 @@ impl<T: SessionStream> SessionData<T> {
) -> trc::Result<(MailboxId, HashedValue<Object<Value>>, Arc<AccessToken>)> {
if let Some(mailbox) = self.get_mailbox_by_name(&arguments.mailbox_name) {
if let Some(values) = self
- .jmap
+ .server
.get_property::<HashedValue<Object<Value>>>(
mailbox.account_id,
Collection::Mailbox,
diff --git a/crates/imap/src/op/append.rs b/crates/imap/src/op/append.rs
index 02308ef9..284b5c7d 100644
--- a/crates/imap/src/op/append.rs
+++ b/crates/imap/src/op/append.rs
@@ -14,11 +14,14 @@ use imap_proto::{
};
use crate::{
- core::{ImapUidToId, MailboxId, SelectedMailbox, Session, SessionData},
+ core::{ImapUidToId, SelectedMailbox, Session, SessionData},
spawn_op,
};
-use common::listener::SessionStream;
-use jmap::email::ingest::{IngestEmail, IngestSource};
+use common::{listener::SessionStream, MailboxId};
+use jmap::{
+ email::ingest::{EmailIngest, IngestEmail, IngestSource},
+ services::state::StateManager,
+};
use jmap_proto::types::{acl::Acl, keyword::Keyword, state::StateChange, type_state::DataType};
use mail_parser::MessageParser;
@@ -89,8 +92,7 @@ impl<T: SessionStream> SessionData<T> {
// Obtain quota
let resource_token = self
- .jmap
- .core
+ .server
.get_cached_access_token(mailbox.account_id)
.await
.imap_ctx(&arguments.tag, trc::location!())?
@@ -102,7 +104,7 @@ impl<T: SessionStream> SessionData<T> {
let mut last_change_id = None;
for message in arguments.messages {
match self
- .jmap
+ .server
.email_ingest(IngestEmail {
raw_message: &message.message,
message: MessageParser::new().parse(&message.message),
@@ -111,7 +113,7 @@ impl<T: SessionStream> SessionData<T> {
keywords: message.flags.into_iter().map(Keyword::from).collect(),
received_at: message.received_at.map(|d| d as u64),
source: IngestSource::Imap,
- encrypt: self.jmap.core.jmap.encrypt && self.jmap.core.jmap.encrypt_append,
+ encrypt: self.server.core.jmap.encrypt && self.server.core.jmap.encrypt_append,
session_id: self.session_id,
})
.await
@@ -142,7 +144,7 @@ impl<T: SessionStream> SessionData<T> {
// Broadcast changes
if let Some(change_id) = last_change_id {
- self.jmap
+ self.server
.broadcast_state_change(
StateChange::new(account_id)
.with_change(DataType::Email, change_id)
diff --git a/crates/imap/src/op/authenticate.rs b/crates/imap/src/op/authenticate.rs
index 5724c6af..83327535 100644
--- a/crates/imap/src/op/authenticate.rs
+++ b/crates/imap/src/op/authenticate.rs
@@ -11,6 +11,9 @@ use imap_proto::{
receiver::{self, Request},
Command, ResponseCode, StatusResponse,
};
+use jmap::auth::{
+ authenticate::Authenticator, oauth::token::TokenHandler, rate_limit::RateLimiter,
+};
use mail_parser::decoders::base64::base64_decode;
use mail_send::Credentials;
use std::sync::Arc;
@@ -70,7 +73,7 @@ impl<T: SessionStream> Session<T> {
tag: String,
) -> trc::Result<()> {
// Throttle authentication requests
- self.jmap
+ self.server
.is_auth_allowed_soft(&self.remote_addr)
.await
.map_err(|err| err.id(tag.clone()))?;
@@ -78,17 +81,17 @@ impl<T: SessionStream> Session<T> {
// Authenticate
let access_token = match credentials {
Credentials::Plain { username, secret } | Credentials::XOauth2 { username, secret } => {
- self.jmap
+ self.server
.authenticate_plain(&username, &secret, self.remote_addr, self.session_id)
.await
}
Credentials::OAuthBearer { token } => {
match self
- .jmap
+ .server
.validate_access_token("access_token", &token)
.await
{
- Ok((account_id, _, _)) => self.jmap.core.get_access_token(account_id).await,
+ Ok((account_id, _, _)) => self.server.get_access_token(account_id).await,
Err(err) => Err(err),
}
}
@@ -96,7 +99,7 @@ impl<T: SessionStream> Session<T> {
.map_err(|err| {
if err.matches(trc::EventType::Auth(trc::AuthEvent::Failed)) {
let auth_failures = self.state.auth_failures();
- if auth_failures < self.jmap.core.imap.max_auth_failures {
+ if auth_failures < self.server.core.imap.max_auth_failures {
self.state = State::NotAuthenticated {
auth_failures: auth_failures + 1,
};
@@ -127,7 +130,7 @@ impl<T: SessionStream> Session<T> {
// Cache access token
let access_token = Arc::new(access_token);
- self.jmap.core.cache_access_token(access_token.clone());
+ self.server.cache_access_token(access_token.clone());
// Create session
self.state = State::Authenticated {
diff --git a/crates/imap/src/op/capability.rs b/crates/imap/src/op/capability.rs
index a8f3d7f5..deebbb76 100644
--- a/crates/imap/src/op/capability.rs
+++ b/crates/imap/src/op/capability.rs
@@ -28,7 +28,7 @@ impl<T: SessionStream> Session<T> {
Imap(trc::ImapEvent::Capabilities),
SpanId = self.session_id,
Tls = self.is_tls,
- Strict = !self.jmap.core.imap.allow_plain_auth,
+ Strict = !self.server.core.imap.allow_plain_auth,
Elapsed = op_start.elapsed()
);
diff --git a/crates/imap/src/op/copy_move.rs b/crates/imap/src/op/copy_move.rs
index 43fd6795..697748fa 100644
--- a/crates/imap/src/op/copy_move.rs
+++ b/crates/imap/src/op/copy_move.rs
@@ -13,11 +13,17 @@ use imap_proto::{
};
use crate::{
- core::{MailboxId, SelectedMailbox, Session, SessionData},
+ core::{SelectedMailbox, Session, SessionData},
spawn_op,
};
-use common::listener::SessionStream;
-use jmap::{email::set::TagManager, mailbox::UidMailbox};
+use common::{listener::SessionStream, MailboxId};
+use jmap::{
+ changes::write::ChangeLog,
+ email::{copy::EmailCopy, ingest::EmailIngest, set::TagManager},
+ mailbox::UidMailbox,
+ services::state::StateManager,
+ JmapMethods,
+};
use jmap_proto::{
error::set::SetErrorType,
types::{
@@ -200,7 +206,7 @@ impl<T: SessionStream> SessionData<T> {
for uid_mailbox in mailboxes.inner_tags_mut() {
if uid_mailbox.uid == 0 {
let assigned_uid = self
- .jmap
+ .server
.assign_imap_uid(account_id, uid_mailbox.mailbox_id)
.await
.imap_ctx(&arguments.tag, trc::location!())?;
@@ -219,13 +225,13 @@ impl<T: SessionStream> SessionData<T> {
mailboxes.update_batch(&mut batch, Property::MailboxIds);
if changelog.change_id == u64::MAX {
changelog.change_id = self
- .jmap
+ .server
.assign_change_id(account_id)
.await
.imap_ctx(&arguments.tag, trc::location!())?;
}
batch.value(Property::Cid, changelog.change_id, F_VALUE);
- self.jmap
+ self.server
.write_batch(batch)
.await
.imap_ctx(&arguments.tag, trc::location!())?;
@@ -242,8 +248,7 @@ impl<T: SessionStream> SessionData<T> {
let mut dest_change_id = None;
let dest_account_id = dest_mailbox.account_id;
let resource_token = self
- .jmap
- .core
+ .server
.get_cached_access_token(dest_account_id)
.await
.imap_ctx(&arguments.tag, trc::location!())?
@@ -251,7 +256,7 @@ impl<T: SessionStream> SessionData<T> {
let mut destroy_ids = RoaringBitmap::new();
for (id, imap_id) in ids {
match self
- .jmap
+ .server
.copy_message(
src_account_id,
id,
@@ -303,7 +308,7 @@ impl<T: SessionStream> SessionData<T> {
// Broadcast changes on destination account
if let Some(change_id) = dest_change_id {
- self.jmap
+ self.server
.broadcast_state_change(
StateChange::new(dest_account_id)
.with_change(DataType::Email, change_id)
@@ -317,11 +322,11 @@ impl<T: SessionStream> SessionData<T> {
// Write changes on source account
if !changelog.is_empty() {
let change_id = self
- .jmap
+ .server
.commit_changes(src_mailbox.id.account_id, changelog)
.await
.imap_ctx(&arguments.tag, trc::location!())?;
- self.jmap
+ self.server
.broadcast_state_change(
StateChange::new(src_mailbox.id.account_id)
.with_change(DataType::Email, change_id)
@@ -423,7 +428,7 @@ impl<T: SessionStream> SessionData<T> {
) -> trc::Result<Option<(TagManager<UidMailbox>, u32)>> {
// Obtain mailbox tags
if let (Some(mailboxes), Some(thread_id)) = (
- self.jmap
+ self.server
.get_property::<HashedValue<Vec<UidMailbox>>>(
account_id,
Collection::Email,
@@ -431,7 +436,7 @@ impl<T: SessionStream> SessionData<T> {
Property::MailboxIds,
)
.await?,
- self.jmap
+ self.server
.get_property::<u32>(account_id, Collection::Email, id, Property::ThreadId)
.await?,
) {
diff --git a/crates/imap/src/op/create.rs b/crates/imap/src/op/create.rs
index edd10ea0..2889cb5f 100644
--- a/crates/imap/src/op/create.rs
+++ b/crates/imap/src/op/create.rs
@@ -7,18 +7,20 @@
use std::time::Instant;
use crate::{
- core::{Account, Mailbox, Session, SessionData},
+ core::{Session, SessionData},
op::ImapContext,
spawn_op,
};
-use common::listener::SessionStream;
+use common::{listener::SessionStream, Account, Mailbox};
use directory::Permission;
use imap_proto::{
protocol::{create::Arguments, list::Attribute},
receiver::Request,
Command, ResponseCode, StatusResponse,
};
-use jmap::mailbox::set::SCHEMA;
+use jmap::{
+ changes::write::ChangeLog, mailbox::set::SCHEMA, services::state::StateManager, JmapMethods,
+};
use jmap_proto::{
object::{index::ObjectIndexBuilder, Object},
types::{
@@ -75,7 +77,7 @@ impl<T: SessionStream> SessionData<T> {
// Build batch
let mut changes = self
- .jmap
+ .server
.begin_changes(params.account_id)
.await
.imap_ctx(&arguments.tag, trc::location!())?;
@@ -102,7 +104,7 @@ impl<T: SessionStream> SessionData<T> {
.create_document()
.custom(ObjectIndexBuilder::new(SCHEMA).with_changes(mailbox));
let mailbox_id = self
- .jmap
+ .server
.write_batch_expect_id(batch)
.await
.imap_ctx(&arguments.tag, trc::location!())?;
@@ -118,13 +120,13 @@ impl<T: SessionStream> SessionData<T> {
.with_account_id(params.account_id)
.with_collection(Collection::Mailbox)
.custom(changes);
- self.jmap
+ self.server
.write_batch(batch)
.await
.imap_ctx(&arguments.tag, trc::location!())?;
// Broadcast changes
- self.jmap
+ self.server
.broadcast_state_change(
StateChange::new(params.account_id).with_change(DataType::Mailbox, change_id),
)
@@ -205,7 +207,7 @@ impl<T: SessionStream> SessionData<T> {
};
let effective_id = self
- .jmap
+ .server
.core
.jmap
.default_folders
@@ -271,7 +273,7 @@ impl<T: SessionStream> SessionData<T> {
return Err(trc::ImapEvent::Error
.into_err()
.details("Invalid empty path item."));
- } else if path_item.len() > self.jmap.core.jmap.mailbox_name_max_len {
+ } else if path_item.len() > self.server.core.jmap.mailbox_name_max_len {
return Err(trc::ImapEvent::Error
.into_err()
.details("Mailbox name is too long."));
@@ -279,7 +281,7 @@ impl<T: SessionStream> SessionData<T> {
path.push(path_item);
}
- if path.len() > self.jmap.core.jmap.mailbox_max_depth {
+ if path.len() > self.server.core.jmap.mailbox_max_depth {
return Err(trc::ImapEvent::Error
.into_err()
.details("Mailbox path is too deep."));
@@ -295,7 +297,7 @@ impl<T: SessionStream> SessionData<T> {
let (account_id, path) = {
let mailboxes = self.mailboxes.lock();
let first_path_item = path.first().unwrap();
- let account = if first_path_item == &self.jmap.core.jmap.shared_folder {
+ let account = if first_path_item == &self.server.core.jmap.shared_folder {
// Shared Folders/<username>/<folder>
if path.len() < 3 {
return Err(trc::ImapEvent::Error
@@ -391,7 +393,7 @@ impl<T: SessionStream> SessionData<T> {
special_use: if let Some(mailbox_role) = mailbox_role {
// Make sure role is unique
if !self
- .jmap
+ .server
.filter(
account_id,
Collection::Mailbox,
diff --git a/crates/imap/src/op/delete.rs b/crates/imap/src/op/delete.rs
index d8d028a8..0d9ecec8 100644
--- a/crates/imap/src/op/delete.rs
+++ b/crates/imap/src/op/delete.rs
@@ -15,6 +15,7 @@ use directory::Permission;
use imap_proto::{
protocol::delete::Arguments, receiver::Request, Command, ResponseCode, StatusResponse,
};
+use jmap::{changes::write::ChangeLog, mailbox::set::MailboxSet, services::state::StateManager};
use jmap_proto::types::{state::StateChange, type_state::DataType};
use store::write::log::ChangeLogBuilder;
@@ -76,7 +77,7 @@ impl<T: SessionStream> SessionData<T> {
.imap_ctx(&arguments.tag, trc::location!())?;
let mut changelog = ChangeLogBuilder::new();
let did_remove_emails = match self
- .jmap
+ .server
.mailbox_destroy(account_id, mailbox_id, &mut changelog, &access_token, true)
.await
.imap_ctx(&arguments.tag, trc::location!())?
@@ -93,13 +94,13 @@ impl<T: SessionStream> SessionData<T> {
// Write changes
let change_id = self
- .jmap
+ .server
.commit_changes(account_id, changelog)
.await
.imap_ctx(&arguments.tag, trc::location!())?;
// Broadcast changes
- self.jmap
+ self.server
.broadcast_state_change(if did_remove_emails {
StateChange::new(account_id)
.with_change(DataType::Mailbox, change_id)
diff --git a/crates/imap/src/op/expunge.rs b/crates/imap/src/op/expunge.rs
index 36c99003..519c2808 100644
--- a/crates/imap/src/op/expunge.rs
+++ b/crates/imap/src/op/expunge.rs
@@ -15,9 +15,15 @@ use imap_proto::{
};
use trc::AddContext;
-use crate::core::{ImapId, SavedSearch, SelectedMailbox, Session, SessionData};
-use common::listener::SessionStream;
-use jmap::{email::set::TagManager, mailbox::UidMailbox};
+use crate::core::{SavedSearch, SelectedMailbox, Session, SessionData};
+use common::{listener::SessionStream, ImapId};
+use jmap::{
+ changes::write::ChangeLog,
+ email::{delete::EmailDeletion, set::TagManager},
+ mailbox::UidMailbox,
+ services::state::StateManager,
+ JmapMethods,
+};
use jmap_proto::types::{
acl::Acl, collection::Collection, id::Id, keyword::Keyword, property::Property,
state::StateChange, type_state::DataType,
@@ -118,7 +124,7 @@ impl<T: SessionStream> SessionData<T> {
// Obtain message ids
let account_id = mailbox.id.account_id;
let mut deleted_ids = self
- .jmap
+ .server
.get_tag(
account_id,
Collection::Email,
@@ -129,7 +135,7 @@ impl<T: SessionStream> SessionData<T> {
.caused_by(trc::location!())?
.unwrap_or_default()
& self
- .jmap
+ .server
.get_tag(
account_id,
Collection::Email,
@@ -167,8 +173,8 @@ impl<T: SessionStream> SessionData<T> {
// Write changes on source account
if !changelog.is_empty() {
- let change_id = self.jmap.commit_changes(account_id, changelog).await?;
- self.jmap
+ let change_id = self.server.commit_changes(account_id, changelog).await?;
+ self.server
.broadcast_state_change(
StateChange::new(account_id)
.with_change(DataType::Email, change_id)
@@ -192,7 +198,7 @@ impl<T: SessionStream> SessionData<T> {
let mut destroy_ids = RoaringBitmap::new();
for (id, mailbox_ids) in self
- .jmap
+ .server
.get_properties::<HashedValue<Vec<UidMailbox>>, _, _>(
account_id,
Collection::Email,
@@ -208,7 +214,7 @@ impl<T: SessionStream> SessionData<T> {
if mailboxes.current().len() > 1 {
// Remove deleted flag
let (mut keywords, thread_id) = if let (Some(keywords), Some(thread_id)) = (
- self.jmap
+ self.server
.get_property::<HashedValue<Vec<Keyword>>>(
account_id,
Collection::Email,
@@ -217,7 +223,7 @@ impl<T: SessionStream> SessionData<T> {
)
.await
.caused_by(trc::location!())?,
- self.jmap
+ self.server
.get_property::<u32>(
account_id,
Collection::Email,
@@ -245,10 +251,10 @@ impl<T: SessionStream> SessionData<T> {
mailboxes.update_batch(&mut batch, Property::MailboxIds);
keywords.update_batch(&mut batch, Property::Keywords);
if changelog.change_id == u64::MAX {
- changelog.change_id = self.jmap.assign_change_id(account_id).await?
+ changelog.change_id = self.server.assign_change_id(account_id).await?
}
batch.value(Property::Cid, changelog.change_id, F_VALUE);
- match self.jmap.write_batch(batch).await {
+ match self.server.write_batch(batch).await {
Ok(_) => {
changelog.log_update(Collection::Email, Id::from_parts(thread_id, id));
changelog.log_child_update(Collection::Mailbox, mailbox_id.mailbox_id);
@@ -268,7 +274,7 @@ impl<T: SessionStream> SessionData<T> {
if !destroy_ids.is_empty() {
// Delete message from all mailboxes
let (changes, _) = self
- .jmap
+ .server
.emails_tombstone(account_id, destroy_ids)
.await
.caused_by(trc::location!())?;
diff --git a/crates/imap/src/op/fetch.rs b/crates/imap/src/op/fetch.rs
index 182b8303..be2b1fc8 100644
--- a/crates/imap/src/op/fetch.rs
+++ b/crates/imap/src/op/fetch.rs
@@ -26,7 +26,13 @@ use imap_proto::{
receiver::Request,
Command, ResponseCode, ResponseType, StatusResponse,
};
-use jmap::email::metadata::MessageMetadata;
+use jmap::{
+ blob::download::BlobDownload,
+ changes::{get::ChangesLookup, write::ChangeLog},
+ email::metadata::MessageMetadata,
+ services::state::StateManager,
+ JmapMethods,
+};
use jmap_proto::types::{
acl::Acl, collection::Collection, id::Id, keyword::Keyword, property::Property,
state::StateChange, type_state::DataType,
@@ -127,7 +133,7 @@ impl<T: SessionStream> SessionData<T> {
if let Some(changed_since) = arguments.changed_since {
// Obtain changes since the modseq.
let changelog = self
- .jmap
+ .server
.changes_(
account_id,
Collection::Email,
@@ -280,7 +286,7 @@ impl<T: SessionStream> SessionData<T> {
for (seqnum, uid, id) in ids {
// Obtain attributes and keywords
let (email, keywords) = if let (Some(email), Some(keywords)) = (
- self.jmap
+ self.server
.get_property::<Bincode<MessageMetadata>>(
account_id,
Collection::Email,
@@ -289,7 +295,7 @@ impl<T: SessionStream> SessionData<T> {
)
.await
.imap_ctx(&arguments.tag, trc::location!())?,
- self.jmap
+ self.server
.get_property::<HashedValue<Vec<Keyword>>>(
account_id,
Collection::Email,
@@ -316,7 +322,7 @@ impl<T: SessionStream> SessionData<T> {
let raw_message = if needs_blobs {
// Retrieve raw message if needed
match self
- .jmap
+ .server
.get_blob(&email.blob_hash, 0..usize::MAX)
.await
.imap_ctx(&arguments.tag, trc::location!())?
@@ -347,7 +353,7 @@ impl<T: SessionStream> SessionData<T> {
set_seen_flags && !keywords.inner.iter().any(|k| k == &Keyword::Seen);
let thread_id = if needs_thread_id || set_seen_flag {
if let Some(thread_id) = self
- .jmap
+ .server
.get_property::<u32>(account_id, Collection::Email, id, Property::ThreadId)
.await
.imap_ctx(&arguments.tag, trc::location!())?
@@ -479,7 +485,7 @@ impl<T: SessionStream> SessionData<T> {
}
Attribute::ModSeq => {
if let Ok(Some(modseq)) = self
- .jmap
+ .server
.get_property::<u64>(account_id, Collection::Email, id, Property::Cid)
.await
{
@@ -524,7 +530,7 @@ impl<T: SessionStream> SessionData<T> {
// Set Seen ids
if !set_seen_ids.is_empty() {
let mut changelog = self
- .jmap
+ .server
.begin_changes(account_id)
.await
.imap_ctx(&arguments.tag, trc::location!())?;
@@ -539,7 +545,7 @@ impl<T: SessionStream> SessionData<T> {
.value(Property::Keywords, keywords.inner, F_VALUE)
.value(Property::Keywords, Keyword::Seen, F_BITMAP)
.value(Property::Cid, changelog.change_id, F_VALUE);
- match self.jmap.write_batch(batch).await {
+ match self.server.write_batch(batch).await {
Ok(_) => {
changelog.log_update(Collection::Email, id);
}
@@ -553,12 +559,12 @@ impl<T: SessionStream> SessionData<T> {
if !changelog.is_empty() {
// Write changes
let change_id = self
- .jmap
+ .server
.commit_changes(account_id, changelog)
.await
.imap_ctx(&arguments.tag, trc::location!())?;
modseq = change_id.into();
- self.jmap
+ self.server
.broadcast_state_change(
StateChange::new(account_id).with_change(DataType::Email, change_id),
)
diff --git a/crates/imap/src/op/idle.rs b/crates/imap/src/op/idle.rs
index 819311b9..c395bf21 100644
--- a/crates/imap/src/op/idle.rs
+++ b/crates/imap/src/op/idle.rs
@@ -20,6 +20,7 @@ use imap_proto::{
};
use common::listener::SessionStream;
+use jmap::{changes::get::ChangesLookup, services::state::StateManager};
use jmap_proto::types::{collection::Collection, type_state::DataType};
use store::query::log::Query;
use tokio::io::AsyncReadExt;
@@ -53,7 +54,7 @@ impl<T: SessionStream> Session<T> {
// Register with state manager
let mut change_rx = self
- .jmap
+ .server
.subscribe_state_manager(data.account_id, types)
.await
.imap_ctx(&request.tag, trc::location!())?;
@@ -72,7 +73,7 @@ impl<T: SessionStream> Session<T> {
let mut buf = vec![0; 4];
loop {
tokio::select! {
- result = tokio::time::timeout(self.jmap.core.imap.timeout_idle, self.stream_rx.read_exact(&mut buf)) => {
+ result = tokio::time::timeout(self.server.core.imap.timeout_idle, self.stream_rx.read_exact(&mut buf)) => {
match result {
Ok(Ok(bytes_read)) => {
if bytes_read > 0 {
@@ -202,7 +203,7 @@ impl<T: SessionStream> SessionData<T> {
// Obtain changed messages
let changelog = self
- .jmap
+ .server
.changes_(
mailbox.id.account_id,
Collection::Email,
diff --git a/crates/imap/src/op/list.rs b/crates/imap/src/op/list.rs
index dce26b75..9b2cc3f3 100644
--- a/crates/imap/src/op/list.rs
+++ b/crates/imap/src/op/list.rs
@@ -173,10 +173,10 @@ impl<T: SessionStream> SessionData<T> {
if let Some(prefix) = &account.prefix {
if !added_shared_folder {
if !filter_subscribed
- && matches_pattern(&patterns, &self.jmap.core.jmap.shared_folder)
+ && matches_pattern(&patterns, &self.server.core.jmap.shared_folder)
{
list_items.push(ListItem {
- mailbox_name: self.jmap.core.jmap.shared_folder.clone(),
+ mailbox_name: self.server.core.jmap.shared_folder.clone(),
attributes: if include_children {
vec![Attribute::HasChildren, Attribute::NoSelect]
} else {
diff --git a/crates/imap/src/op/namespace.rs b/crates/imap/src/op/namespace.rs
index 0f9ed1ac..10f0ea00 100644
--- a/crates/imap/src/op/namespace.rs
+++ b/crates/imap/src/op/namespace.rs
@@ -30,7 +30,7 @@ impl<T: SessionStream> Session<T> {
.serialize(
Response {
shared_prefix: if self.state.session_data().mailboxes.lock().len() > 1 {
- self.jmap.core.jmap.shared_folder.clone().into()
+ self.server.core.jmap.shared_folder.clone().into()
} else {
None
},
diff --git a/crates/imap/src/op/rename.rs b/crates/imap/src/op/rename.rs
index cc8b27fb..9daa7fd7 100644
--- a/crates/imap/src/op/rename.rs
+++ b/crates/imap/src/op/rename.rs
@@ -15,7 +15,10 @@ use directory::Permission;
use imap_proto::{
protocol::rename::Arguments, receiver::Request, Command, ResponseCode, StatusResponse,
};
-use jmap::{auth::acl::EffectiveAcl, mailbox::set::SCHEMA};
+use jmap::{
+ auth::acl::EffectiveAcl, changes::write::ChangeLog, mailbox::set::SCHEMA,
+ services::state::StateManager, JmapMethods,
+};
use jmap_proto::{
object::{index::ObjectIndexBuilder, Object},
types::{
@@ -92,7 +95,7 @@ impl<T: SessionStream> SessionData<T> {
// Obtain mailbox
let mailbox = self
- .jmap
+ .server
.get_property::<HashedValue<Object<Value>>>(
params.account_id,
Collection::Mailbox,
@@ -133,7 +136,7 @@ impl<T: SessionStream> SessionData<T> {
// Build batch
let mut changes = self
- .jmap
+ .server
.begin_changes(params.account_id)
.await
.imap_ctx(&arguments.tag, trc::location!())?;
@@ -159,7 +162,7 @@ impl<T: SessionStream> SessionData<T> {
);
let mailbox_id = self
- .jmap
+ .server
.write_batch_expect_id(batch)
.await
.imap_ctx(&arguments.tag, trc::location!())?;
@@ -191,13 +194,13 @@ impl<T: SessionStream> SessionData<T> {
let change_id = changes.change_id;
batch.custom(changes);
- self.jmap
+ self.server
.write_batch(batch)
.await
.imap_ctx(&arguments.tag, trc::location!())?;
// Broadcast changes
- self.jmap
+ self.server
.broadcast_state_change(
StateChange::new(params.account_id).with_change(DataType::Mailbox, change_id),
)
diff --git a/crates/imap/src/op/search.rs b/crates/imap/src/op/search.rs
index 86329275..bf8670ac 100644
--- a/crates/imap/src/op/search.rs
+++ b/crates/imap/src/op/search.rs
@@ -6,7 +6,7 @@
use std::{sync::Arc, time::Instant};
-use common::listener::SessionStream;
+use common::{listener::SessionStream, ImapId};
use directory::Permission;
use imap_proto::{
protocol::{
@@ -16,6 +16,7 @@ use imap_proto::{
receiver::Request,
Command, StatusResponse,
};
+use jmap::{changes::get::ChangesLookup, JmapMethods};
use jmap_proto::types::{collection::Collection, id::Id, keyword::Keyword, property::Property};
use mail_parser::HeaderName;
use nlp::language::Language;
@@ -29,7 +30,7 @@ use tokio::sync::watch;
use trc::AddContext;
use crate::{
- core::{ImapId, MailboxState, SavedSearch, SelectedMailbox, Session, SessionData},
+ core::{SavedSearch, SelectedMailbox, Session, SessionData},
spawn_op,
};
@@ -142,7 +143,7 @@ impl<T: SessionStream> SessionData<T> {
let mut imap_ids = Vec::with_capacity(results_len);
let is_sort = if let Some(sort) = arguments.sort {
mailbox.map_search_results(
- self.jmap
+ self.server
.core
.storage
.data
@@ -260,7 +261,7 @@ impl<T: SessionStream> SessionData<T> {
// Obtain message ids
let mut filters = Vec::with_capacity(imap_filter.len() + 1);
let message_ids = self
- .jmap
+ .server
.get_tag(
mailbox.id.account_id,
Collection::Email,
@@ -290,7 +291,7 @@ impl<T: SessionStream> SessionData<T> {
fts_filters.push(FtsFilter::has_text_detect(
Field::Body,
text,
- self.jmap.core.jmap.default_language,
+ self.server.core.jmap.default_language,
));
}
search::Filter::Cc(text) => {
@@ -350,7 +351,7 @@ impl<T: SessionStream> SessionData<T> {
fts_filters.push(FtsFilter::has_text_detect(
Field::Header(HeaderName::Subject),
text,
- self.jmap.core.jmap.default_language,
+ self.server.core.jmap.default_language,
));
}
search::Filter::Text(text) => {
@@ -378,17 +379,17 @@ impl<T: SessionStream> SessionData<T> {
fts_filters.push(FtsFilter::has_text_detect(
Field::Header(HeaderName::Subject),
&text,
- self.jmap.core.jmap.default_language,
+ self.server.core.jmap.default_language,
));
fts_filters.push(FtsFilter::has_text_detect(
Field::Body,
&text,
- self.jmap.core.jmap.default_language,
+ self.server.core.jmap.default_language,
));
fts_filters.push(FtsFilter::has_text_detect(
Field::Attachment,
text,
- self.jmap.core.jmap.default_language,
+ self.server.core.jmap.default_language,
));
fts_filters.push(FtsFilter::End);
}
@@ -416,7 +417,7 @@ impl<T: SessionStream> SessionData<T> {
}
filters.push(query::Filter::is_in_set(
- self.jmap
+ self.server
.fts_filter(mailbox.id.account_id, Collection::Email, fts_filters)
.await?,
));
@@ -612,7 +613,7 @@ impl<T: SessionStream> SessionData<T> {
search::Filter::ModSeq((modseq, _)) => {
let mut set = RoaringBitmap::new();
for change in self
- .jmap
+ .server
.changes_(
mailbox.id.account_id,
Collection::Email,
@@ -658,7 +659,7 @@ impl<T: SessionStream> SessionData<T> {
}
// Run query
- self.jmap
+ self.server
.filter(mailbox.id.account_id, Collection::Email, filters)
.await
.map(|res| (res, include_highest_modseq))
@@ -738,23 +739,6 @@ impl SelectedMailbox {
}
}
-impl MailboxState {
- pub fn map_result_id(&self, document_id: u32, is_uid: bool) -> Option<(u32, ImapId)> {
- if let Some(imap_id) = self.id_to_imap.get(&document_id) {
- Some((if is_uid { imap_id.uid } else { imap_id.seqnum }, *imap_id))
- } else if is_uid {
- self.next_state.as_ref().and_then(|s| {
- s.next_state
- .id_to_imap
- .get(&document_id)
- .map(|imap_id| (imap_id.uid, *imap_id))
- })
- } else {
- None
- }
- }
-}
-
impl SavedSearch {
pub async fn unwrap(&self) -> Option<Arc<Vec<ImapId>>> {
match self {
diff --git a/crates/imap/src/op/select.rs b/crates/imap/src/op/select.rs
index a36125ea..5d6c1853 100644
--- a/crates/imap/src/op/select.rs
+++ b/crates/imap/src/op/select.rs
@@ -47,35 +47,39 @@ impl<T: SessionStream> Session<T> {
if let Some(mailbox) = data.get_mailbox_by_name(&arguments.mailbox_name) {
// Try obtaining the mailbox from the cache
- let state = {
- let modseq = data
- .get_modseq(mailbox.account_id)
- .await
- .imap_ctx(&arguments.tag, trc::location!())?;
-
- if let Some(cached_state) =
- self.imap
- .cache_mailbox
- .get(&mailbox)
- .and_then(|cached_state| {
- if cached_state.modseq.unwrap_or(0) >= modseq.unwrap_or(0) {
- Some(cached_state)
- } else {
- None
- }
- })
+ let state =
{
- cached_state.as_ref().clone()
- } else {
- let new_state = Arc::new(
- data.fetch_messages(&mailbox)
- .await
- .imap_ctx(&arguments.tag, trc::location!())?,
- );
- self.imap.cache_mailbox.insert(mailbox, new_state.clone());
- new_state.as_ref().clone()
- }
- };
+ let modseq = data
+ .get_modseq(mailbox.account_id)
+ .await
+ .imap_ctx(&arguments.tag, trc::location!())?;
+
+ if let Some(cached_state) =
+ self.server.inner.data.mailbox_cache.get(&mailbox).and_then(
+ |cached_state| {
+ if cached_state.modseq.unwrap_or(0) >= modseq.unwrap_or(0) {
+ Some(cached_state)
+ } else {
+ None
+ }
+ },
+ )
+ {
+ cached_state.as_ref().clone()
+ } else {
+ let new_state = Arc::new(
+ data.fetch_messages(&mailbox)
+ .await
+ .imap_ctx(&arguments.tag, trc::location!())?,
+ );
+ self.server
+ .inner
+ .data
+ .mailbox_cache
+ .insert(mailbox, new_state.clone());
+ new_state.as_ref().clone()
+ }
+ };
// Synchronize messages
let closed_previous = self.state.close_mailbox();
diff --git a/crates/imap/src/op/status.rs b/crates/imap/src/op/status.rs
index ce8c6029..d06ae582 100644
--- a/crates/imap/src/op/status.rs
+++ b/crates/imap/src/op/status.rs
@@ -7,11 +7,11 @@
use std::{sync::Arc, time::Instant};
use crate::{
- core::{Mailbox, Session, SessionData},
+ core::{Session, SessionData},
op::ImapContext,
spawn_op,
};
-use common::listener::SessionStream;
+use common::{listener::SessionStream, Mailbox};
use directory::Permission;
use imap_proto::{
parser::PushUnique,
@@ -19,6 +19,7 @@ use imap_proto::{
receiver::Request,
Command, ResponseCode, StatusResponse,
};
+use jmap::JmapMethods;
use jmap_proto::{
object::Object,
types::{collection::Collection, id::Id, keyword::Keyword, property::Property, value::Value},
@@ -86,11 +87,11 @@ impl<T: SessionStream> SessionData<T> {
mailbox
} else {
// Some IMAP clients will try to get the status of a mailbox with the NoSelect flag
- return if mailbox_name == self.jmap.core.jmap.shared_folder
+ return if mailbox_name == self.server.core.jmap.shared_folder
|| mailbox_name
.split_once('/')
.map_or(false, |(base_name, path)| {
- base_name == self.jmap.core.jmap.shared_folder && !path.contains('/')
+ base_name == self.server.core.jmap.shared_folder && !path.contains('/')
})
{
Ok(StatusItem {
@@ -211,7 +212,7 @@ impl<T: SessionStream> SessionData<T> {
// Retrieve latest values
let mut values_update = Vec::with_capacity(items_update.len());
let mailbox_message_ids = self
- .jmap
+ .server
.get_tag(
mailbox.account_id,
Collection::Email,
@@ -222,7 +223,7 @@ impl<T: SessionStream> SessionData<T> {
.caused_by(trc::location!())?
.map(Arc::new);
let message_ids = self
- .jmap
+ .server
.get_document_ids(mailbox.account_id, Collection::Email)
.await
.caused_by(trc::location!())?;
@@ -232,7 +233,7 @@ impl<T: SessionStream> SessionData<T> {
Status::Messages => mailbox_message_ids.as_ref().map(|v| v.len()).unwrap_or(0),
Status::UidNext => {
(self
- .jmap
+ .server
.core
.storage
.data
@@ -247,7 +248,7 @@ impl<T: SessionStream> SessionData<T> {
+ 1) as u64
}
Status::UidValidity => self
- .jmap
+ .server
.get_property::<Object<Value>>(
mailbox.account_id,
Collection::Mailbox,
@@ -270,7 +271,7 @@ impl<T: SessionStream> SessionData<T> {
(&message_ids, &mailbox_message_ids)
{
if let Some(mut seen) = self
- .jmap
+ .server
.get_tag(
mailbox.account_id,
Collection::Email,
@@ -293,7 +294,7 @@ impl<T: SessionStream> SessionData<T> {
Status::Deleted => {
if let (Some(mailbox_message_ids), Some(mut deleted)) = (
&mailbox_message_ids,
- self.jmap
+ self.server
.get_tag(
mailbox.account_id,
Collection::Email,
@@ -378,7 +379,7 @@ impl<T: SessionStream> SessionData<T> {
message_ids: &Arc<RoaringBitmap>,
) -> trc::Result<u32> {
let mut total_size = 0u32;
- self.jmap
+ self.server
.core
.storage
.data
diff --git a/crates/imap/src/op/store.rs b/crates/imap/src/op/store.rs
index 3a799841..14a5626a 100644
--- a/crates/imap/src/op/store.rs
+++ b/crates/imap/src/op/store.rs
@@ -22,7 +22,13 @@ use imap_proto::{
receiver::Request,
Command, ResponseCode, ResponseType, StatusResponse,
};
-use jmap::{email::set::TagManager, mailbox::UidMailbox};
+use jmap::{
+ changes::{get::ChangesLookup, write::ChangeLog},
+ email::set::TagManager,
+ mailbox::UidMailbox,
+ services::state::StateManager,
+ JmapMethods,
+};
use jmap_proto::types::{
acl::Acl, collection::Collection, id::Id, keyword::Keyword, property::Property,
state::StateChange, type_state::DataType,
@@ -110,7 +116,7 @@ impl<T: SessionStream> SessionData<T> {
if let Some(unchanged_since) = arguments.unchanged_since {
// Obtain changes since the modseq.
let changelog = self
- .jmap
+ .server
.changes_(
account_id,
Collection::Email,
@@ -192,7 +198,7 @@ impl<T: SessionStream> SessionData<T> {
loop {
// Obtain current keywords
let (mut keywords, thread_id) = if let (Some(keywords), Some(thread_id)) = (
- self.jmap
+ self.server
.get_property::<HashedValue<Vec<Keyword>>>(
account_id,
Collection::Email,
@@ -201,7 +207,7 @@ impl<T: SessionStream> SessionData<T> {
)
.await
.imap_ctx(response.tag.as_ref().unwrap(), trc::location!())?,
- self.jmap
+ self.server
.get_property::<u32>(account_id, Collection::Email, *id, Property::ThreadId)
.await
.imap_ctx(response.tag.as_ref().unwrap(), trc::location!())?,
@@ -253,18 +259,18 @@ impl<T: SessionStream> SessionData<T> {
keywords.update_batch(&mut batch, Property::Keywords);
if changelog.change_id == u64::MAX {
changelog.change_id = self
- .jmap
+ .server
.assign_change_id(account_id)
.await
.imap_ctx(response.tag.as_ref().unwrap(), trc::location!())?
}
batch.value(Property::Cid, changelog.change_id, F_VALUE);
- match self.jmap.write_batch(batch).await {
+ match self.server.write_batch(batch).await {
Ok(_) => {
// Set all current mailboxes as changed if the Seen tag changed
if seen_changed {
if let Some(mailboxes) = self
- .jmap
+ .server
.get_property::<Vec<UidMailbox>>(
account_id,
Collection::Email,
@@ -335,11 +341,11 @@ impl<T: SessionStream> SessionData<T> {
// Write changes
if !changelog.is_empty() {
let change_id = self
- .jmap
+ .server
.commit_changes(account_id, changelog)
.await
.imap_ctx(response.tag.as_ref().unwrap(), trc::location!())?;
- self.jmap
+ self.server
.broadcast_state_change(if !changed_mailboxes.is_empty() {
StateChange::new(account_id)
.with_change(DataType::Email, change_id)
diff --git a/crates/imap/src/op/subscribe.rs b/crates/imap/src/op/subscribe.rs
index 995ed430..79b426d1 100644
--- a/crates/imap/src/op/subscribe.rs
+++ b/crates/imap/src/op/subscribe.rs
@@ -13,7 +13,12 @@ use crate::{
use common::listener::SessionStream;
use directory::Permission;
use imap_proto::{receiver::Request, Command, ResponseCode, StatusResponse};
-use jmap::mailbox::set::{MailboxSubscribe, SCHEMA};
+use jmap::{
+ changes::write::ChangeLog,
+ mailbox::set::{MailboxSubscribe, SCHEMA},
+ services::state::StateManager,
+ JmapMethods,
+};
use jmap_proto::{
object::{index::ObjectIndexBuilder, Object},
types::{
@@ -100,7 +105,7 @@ impl<T: SessionStream> SessionData<T> {
// Obtain mailbox
let mailbox = self
- .jmap
+ .server
.get_property::<HashedValue<Object<Value>>>(
account_id,
Collection::Mailbox,
@@ -122,7 +127,7 @@ impl<T: SessionStream> SessionData<T> {
if let Some(value) = mailbox.inner.mailbox_subscribe(self.account_id, subscribe) {
// Build batch
let mut changes = self
- .jmap
+ .server
.begin_changes(account_id)
.await
.imap_ctx(&tag, trc::location!())?;
@@ -142,13 +147,13 @@ impl<T: SessionStream> SessionData<T> {
let change_id = changes.change_id;
batch.custom(changes);
- self.jmap
+ self.server
.write_batch(batch)
.await
.imap_ctx(&tag, trc::location!())?;
// Broadcast changes
- self.jmap
+ self.server
.broadcast_state_change(
StateChange::new(account_id).with_change(DataType::Mailbox, change_id),
)
diff --git a/crates/imap/src/op/thread.rs b/crates/imap/src/op/thread.rs
index a8425652..94d8da12 100644
--- a/crates/imap/src/op/thread.rs
+++ b/crates/imap/src/op/thread.rs
@@ -21,6 +21,7 @@ use imap_proto::{
receiver::Request,
Command, StatusResponse,
};
+use jmap::email::cache::ThreadCache;
use trc::AddContext;
impl<T: SessionStream> Session<T> {
@@ -80,7 +81,7 @@ impl<T: SessionStream> SessionData<T> {
// Lock the cache
let thread_ids = self
- .jmap
+ .server
.get_cached_thread_ids(mailbox.id.account_id, result_set.results.iter())
.await
.caused_by(trc::location!())?;
diff --git a/crates/jmap/src/api/autoconfig.rs b/crates/jmap/src/api/autoconfig.rs
index 296dd5aa..27d61ac4 100644
--- a/crates/jmap/src/api/autoconfig.rs
+++ b/crates/jmap/src/api/autoconfig.rs
@@ -6,18 +6,34 @@
use std::fmt::Write;
-use common::manager::webadmin::Resource;
+use common::{manager::webadmin::Resource, Server};
use directory::{backend::internal::PrincipalField, QueryBy};
use quick_xml::events::Event;
use quick_xml::Reader;
use utils::url_params::UrlParams;
-use crate::{api::http::ToHttpResponse, JMAP};
+use crate::api::http::ToHttpResponse;
use super::{HttpRequest, HttpResponse};
+use std::future::Future;
-impl JMAP {
- pub async fn handle_autoconfig_request(&self, req: &HttpRequest) -> trc::Result<HttpResponse> {
+pub trait Autoconfig: Sync + Send {
+ fn handle_autoconfig_request(
+ &self,
+ req: &HttpRequest,
+ ) -> impl Future<Output = trc::Result<HttpResponse>> + Send;
+ fn handle_autodiscover_request(
+ &self,
+ body: Option<Vec<u8>>,
+ ) -> impl Future<Output = trc::Result<HttpResponse>> + Send;
+ fn autoconfig_parameters<'x>(
+ &self,
+ emailaddress: &'x str,
+ ) -> impl Future<Output = trc::Result<(String, String, &'x str)>> + Send;
+}
+
+impl Autoconfig for Server {
+ async fn handle_autoconfig_request(&self, req: &HttpRequest) -> trc::Result<HttpResponse> {
// Obtain parameters
let params = UrlParams::new(req.uri().query());
let emailaddress = params
@@ -73,7 +89,7 @@ impl JMAP {
)
}
- pub async fn handle_autodiscover_request(
+ async fn handle_autodiscover_request(
&self,
body: Option<Vec<u8>>,
) -> trc::Result<HttpResponse> {
diff --git a/crates/jmap/src/api/event_source.rs b/crates/jmap/src/api/event_source.rs
index 362eba1f..0a37544c 100644
--- a/crates/jmap/src/api/event_source.rs
+++ b/crates/jmap/src/api/event_source.rs
@@ -9,7 +9,7 @@ use std::{
time::{Duration, Instant},
};
-use common::auth::AccessToken;
+use common::{auth::AccessToken, Server};
use http_body_util::{combinators::BoxBody, StreamBody};
use hyper::{
body::{Bytes, Frame},
@@ -18,9 +18,10 @@ use hyper::{
use jmap_proto::types::type_state::DataType;
use utils::map::bitmap::Bitmap;
-use crate::{JMAP, LONG_SLUMBER};
+use crate::{services::state::StateManager, LONG_SLUMBER};
use super::{HttpRequest, HttpResponse, HttpResponseBody, StateChangeResponse};
+use std::future::Future;
struct Ping {
interval: Duration,
@@ -28,8 +29,16 @@ struct Ping {
payload: Bytes,
}
-impl JMAP {
- pub async fn handle_event_source(
+pub trait EventSourceHandler: Sync + Send {
+ fn handle_event_source(
+ &self,
+ req: HttpRequest,
+ access_token: Arc<AccessToken>,
+ ) -> impl Future<Output = trc::Result<HttpResponse>> + Send;
+}
+
+impl EventSourceHandler for Server {
+ async fn handle_event_source(
&self,
req: HttpRequest,
access_token: Arc<AccessToken>,
diff --git a/crates/jmap/src/api/http.rs b/crates/jmap/src/api/http.rs
index 86943ffe..4252edcb 100644
--- a/crates/jmap/src/api/http.rs
+++ b/crates/jmap/src/api/http.rs
@@ -8,10 +8,12 @@ use std::{borrow::Cow, net::IpAddr, sync::Arc};
use common::{
auth::AccessToken,
+ core::BuildServer,
expr::{functions::ResolveVariable, *},
+ ipc::StateEvent,
listener::{ServerInstance, SessionData, SessionManager, SessionStream},
manager::webadmin::Resource,
- Core,
+ Inner, Server,
};
use directory::Permission;
use http_body_util::{BodyExt, Full};
@@ -29,17 +31,26 @@ use jmap_proto::{
response::Response,
types::{blob::BlobId, id::Id},
};
+use std::future::Future;
use crate::{
- auth::{authenticate::HttpHeaders, oauth::OAuthMetadata},
- blob::{DownloadResponse, UploadResponse},
- services::state,
- JmapInstance, JMAP,
+ api::management::enterprise::telemetry::TelemetryApi,
+ auth::{
+ authenticate::{Authenticator, HttpHeaders},
+ oauth::{auth::OAuthApiHandler, token::TokenHandler, OAuthMetadata},
+ rate_limit::RateLimiter,
+ },
+ blob::{download::BlobDownload, upload::BlobUpload, DownloadResponse, UploadResponse},
+ websocket::upgrade::WebSocketUpgrade,
};
use super::{
- management::ManagementApiError, HtmlResponse, HttpRequest, HttpResponse, HttpResponseBody,
- JmapSessionManager, JsonResponse,
+ autoconfig::Autoconfig,
+ event_source::EventSourceHandler,
+ management::{ManagementApi, ManagementApiError},
+ request::RequestHandler,
+ session::SessionHandler,
+ HtmlResponse, HttpRequest, HttpResponse, HttpResponseBody, JmapSessionManager, JsonResponse,
};
pub struct HttpSessionData {
@@ -52,8 +63,16 @@ pub struct HttpSessionData {
pub session_id: u64,
}
-impl JMAP {
- pub async fn parse_http_request(
+pub trait ParseHttp: Sync + Send {
+ fn parse_http_request(
+ &self,
+ req: HttpRequest,
+ session: HttpSessionData,
+ ) -> impl Future<Output = trc::Result<HttpResponse>> + Send;
+}
+
+impl ParseHttp for Server {
+ async fn parse_http_request(
&self,
mut req: HttpRequest,
session: HttpSessionData,
@@ -63,7 +82,7 @@ impl JMAP {
// Validate endpoint access
let ctx = HttpContext::new(&session, &req);
- match ctx.has_endpoint_access(&self.core).await {
+ match ctx.has_endpoint_access(self).await {
StatusCode::OK => (),
status => {
// Allow loopback address to avoid lockouts
@@ -198,10 +217,7 @@ impl JMAP {
self.authenticate_headers(&req, &session).await?;
return Ok(self
- .handle_session_resource(
- ctx.resolve_response_url(&self.core).await,
- access_token,
- )
+ .handle_session_resource(ctx.resolve_response_url(self).await, access_token)
.await?
.into_http_response());
}
@@ -210,11 +226,11 @@ impl JMAP {
self.is_anonymous_allowed(&session.remote_ip).await?;
return Ok(JsonResponse::new(OAuthMetadata::new(
- ctx.resolve_response_url(&self.core).await,
+ ctx.resolve_response_url(self).await,
))
.into_http_response());
}
- ("acme-challenge", &Method::GET) if self.core.has_acme_http_providers() => {
+ ("acme-challenge", &Method::GET) if self.has_acme_http_providers() => {
if let Some(token) = path.next() {
return match self
.core
@@ -230,7 +246,7 @@ impl JMAP {
}
}
("mta-sts.txt", &Method::GET) => {
- if let Some(policy) = self.core.build_mta_sts_policy() {
+ if let Some(policy) = self.build_mta_sts_policy() {
return Ok(Resource::new("text/plain", policy.to_string().into_bytes())
.into_http_response());
} else {
@@ -256,7 +272,7 @@ impl JMAP {
("device", &Method::POST) => {
self.is_anonymous_allowed(&session.remote_ip).await?;
- let url = ctx.resolve_response_url(&self.core).await;
+ let url = ctx.resolve_response_url(self).await;
return self
.handle_device_auth(&mut req, url, session.session_id)
.await;
@@ -389,7 +405,7 @@ impl JMAP {
return Ok(Resource::new(
"text/plain; version=0.0.4",
- self.core.export_prometheus_metrics().await?.into_bytes(),
+ self.export_prometheus_metrics().await?.into_bytes(),
)
.into_http_response());
}
@@ -400,13 +416,12 @@ impl JMAP {
_ => (),
},
#[cfg(feature = "enterprise")]
- "logo.svg" if self.core.is_enterprise_edition() => {
+ "logo.svg" if self.is_enterprise_edition() => {
// SPDX-SnippetBegin
// SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art>
// SPDX-License-Identifier: LicenseRef-SEL
match self
- .core
.logo_resource(
req.headers()
.get(header::HOST)
@@ -425,7 +440,7 @@ impl JMAP {
}
}
- let resource = self.inner.webadmin.get("logo.svg").await?;
+ let resource = self.inner.data.webadmin.get("logo.svg").await?;
return if !resource.is_empty() {
Ok(resource.into_http_response())
@@ -439,6 +454,7 @@ impl JMAP {
let path = req.uri().path();
let resource = self
.inner
+ .data
.webadmin
.get(path.strip_prefix('/').unwrap_or(path))
.await?;
@@ -455,166 +471,156 @@ impl JMAP {
}
}
-impl JmapInstance {
- async fn handle_session<T: SessionStream>(self, session: SessionData<T>) {
- let _in_flight = session.in_flight;
- let is_tls = session.stream.is_tls();
-
- if let Err(http_err) = http1::Builder::new()
- .keep_alive(true)
- .serve_connection(
- TokioIo::new(session.stream),
- service_fn(|req: hyper::Request<body::Incoming>| {
- let jmap_instance = self.clone();
- let instance = session.instance.clone();
-
- async move {
- let jmap = JMAP::from(jmap_instance);
-
- // Obtain remote IP
- let remote_ip = if !jmap.core.jmap.http_use_forwarded {
- trc::event!(
- Http(trc::HttpEvent::RequestUrl),
- SpanId = session.session_id,
- Url = req.uri().to_string(),
- );
-
- session.remote_ip
- } else if let Some(forwarded_for) = req
- .headers()
- .get(header::FORWARDED)
- .and_then(|h| h.to_str().ok())
- .and_then(|h| {
- let h = h.to_ascii_lowercase();
- h.split_once("for=").and_then(|(_, rest)| {
- let mut start_ip = usize::MAX;
- let mut end_ip = usize::MAX;
-
- for (pos, ch) in rest.char_indices() {
- match ch {
- '0'..='9' | 'a'..='f' | ':' | '.' => {
- if start_ip == usize::MAX {
- start_ip = pos;
- }
- end_ip = pos;
- }
- '"' | '[' | ' ' if start_ip == usize::MAX => {}
- _ => {
- break;
+async fn handle_session<T: SessionStream>(inner: Arc<Inner>, session: SessionData<T>) {
+ let _in_flight = session.in_flight;
+ let is_tls = session.stream.is_tls();
+
+ if let Err(http_err) = http1::Builder::new()
+ .keep_alive(true)
+ .serve_connection(
+ TokioIo::new(session.stream),
+ service_fn(|req: hyper::Request<body::Incoming>| {
+ let instance = session.instance.clone();
+ let inner = inner.clone();
+
+ async move {
+ let server = inner.build_server();
+
+ // Obtain remote IP
+ let remote_ip = if !server.core.jmap.http_use_forwarded {
+ trc::event!(
+ Http(trc::HttpEvent::RequestUrl),
+ SpanId = session.session_id,
+ Url = req.uri().to_string(),
+ );
+
+ session.remote_ip
+ } else if let Some(forwarded_for) = req
+ .headers()
+ .get(header::FORWARDED)
+ .and_then(|h| h.to_str().ok())
+ .and_then(|h| {
+ let h = h.to_ascii_lowercase();
+ h.split_once("for=").and_then(|(_, rest)| {
+ let mut start_ip = usize::MAX;
+ let mut end_ip = usize::MAX;
+
+ for (pos, ch) in rest.char_indices() {
+ match ch {
+ '0'..='9' | 'a'..='f' | ':' | '.' => {
+ if start_ip == usize::MAX {
+ start_ip = pos;
}
+ end_ip = pos;
+ }
+ '"' | '[' | ' ' if start_ip == usize::MAX => {}
+ _ => {
+ break;
}
}
+ }
- rest.get(start_ip..=end_ip)
- .and_then(|h| h.parse::<IpAddr>().ok())
- })
- })
- .or_else(|| {
- req.headers()
- .get("X-Forwarded-For")
- .and_then(|h| h.to_str().ok())
- .map(|h| h.split_once(',').map_or(h, |(ip, _)| ip).trim())
+ rest.get(start_ip..=end_ip)
.and_then(|h| h.parse::<IpAddr>().ok())
})
- {
- trc::event!(
- Http(trc::HttpEvent::RequestUrl),
- SpanId = session.session_id,
- RemoteIp = forwarded_for,
- Url = req.uri().to_string(),
- );
-
- forwarded_for
- } else {
- trc::event!(
- Http(trc::HttpEvent::XForwardedMissing),
- SpanId = session.session_id,
- );
- session.remote_ip
- };
-
- // Parse HTTP request
- let response = match jmap
- .parse_http_request(
- req,
- HttpSessionData {
- instance,
- local_ip: session.local_ip,
- local_port: session.local_port,
- remote_ip,
- remote_port: session.remote_port,
- is_tls,
- session_id: session.session_id,
- },
- )
- .await
- {
- Ok(response) => response,
- Err(err) => {
- let response = err.into_http_response();
- trc::error!(err.span_id(session.session_id));
- response
- }
- };
+ })
+ .or_else(|| {
+ req.headers()
+ .get("X-Forwarded-For")
+ .and_then(|h| h.to_str().ok())
+ .map(|h| h.split_once(',').map_or(h, |(ip, _)| ip).trim())
+ .and_then(|h| h.parse::<IpAddr>().ok())
+ })
+ {
+ trc::event!(
+ Http(trc::HttpEvent::RequestUrl),
+ SpanId = session.session_id,
+ RemoteIp = forwarded_for,
+ Url = req.uri().to_string(),
+ );
+ forwarded_for
+ } else {
trc::event!(
- Http(trc::HttpEvent::ResponseBody),
+ Http(trc::HttpEvent::XForwardedMissing),
SpanId = session.session_id,
- Contents = match &response.body {
- HttpResponseBody::Text(value) => trc::Value::String(value.clone()),
- HttpResponseBody::Binary(_) => trc::Value::Static("[binary data]"),
- HttpResponseBody::Stream(_) => trc::Value::Static("[stream]"),
- _ => trc::Value::None,
- },
- Code = response.status.as_u16(),
- Size = response.size(),
);
+ session.remote_ip
+ };
+
+ // Parse HTTP request
+ let response = match server
+ .parse_http_request(
+ req,
+ HttpSessionData {
+ instance,
+ local_ip: session.local_ip,
+ local_port: session.local_port,
+ remote_ip,
+ remote_port: session.remote_port,
+ is_tls,
+ session_id: session.session_id,
+ },
+ )
+ .await
+ {
+ Ok(response) => response,
+ Err(err) => {
+ let response = err.into_http_response();
+ trc::error!(err.span_id(session.session_id));
+ response
+ }
+ };
+
+ trc::event!(
+ Http(trc::HttpEvent::ResponseBody),
+ SpanId = session.session_id,
+ Contents = match &response.body {
+ HttpResponseBody::Text(value) => trc::Value::String(value.clone()),
+ HttpResponseBody::Binary(_) => trc::Value::Static("[binary data]"),
+ HttpResponseBody::Stream(_) => trc::Value::Static("[stream]"),
+ _ => trc::Value::None,
+ },
+ Code = response.status.as_u16(),
+ Size = response.size(),
+ );
- // Build response
- let mut response = response.build();
+ // Build response
+ let mut response = response.build();
- // Add custom headers
- if !jmap.core.jmap.http_headers.is_empty() {
- let headers = response.headers_mut();
+ // Add custom headers
+ if !server.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());
- }
+ for (header, value) in &server.core.jmap.http_headers {
+ headers.insert(header.clone(), value.clone());
}
-
- Ok::<_, hyper::Error>(response)
}
- }),
- )
- .with_upgrades()
- .await
- {
- trc::event!(
- Http(trc::HttpEvent::Error),
- SpanId = session.session_id,
- Reason = http_err.to_string(),
- );
- }
+
+ Ok::<_, hyper::Error>(response)
+ }
+ }),
+ )
+ .with_upgrades()
+ .await
+ {
+ trc::event!(
+ Http(trc::HttpEvent::Error),
+ SpanId = session.session_id,
+ Reason = http_err.to_string(),
+ );
}
}
impl SessionManager for JmapSessionManager {
- fn handle<T: SessionStream>(
- self,
- session: SessionData<T>,
- ) -> impl std::future::Future<Output = ()> + Send {
- self.inner.handle_session(session)
+ fn handle<T: SessionStream>(self, session: SessionData<T>) -> impl Future<Output = ()> + Send {
+ handle_session(self.inner, session)
}
#[allow(clippy::manual_async_fn)]
fn shutdown(&self) -> impl std::future::Future<Output = ()> + Send {
async {
- let _ = self
- .inner
- .jmap_inner
- .state_tx
- .send(state::Event::Stop)
- .await;
+ let _ = self.inner.ipc.state_tx.send(StateEvent::Stop).await;
}
}
}
@@ -629,31 +635,33 @@ impl<'x> HttpContext<'x> {
Self { session, req }
}
- pub async fn resolve_response_url(&self, core: &Core) -> String {
- core.eval_if(
- &core.network.http_response_url,
- self,
- self.session.session_id,
- )
- .await
- .unwrap_or_else(|| {
- format!(
- "http{}://{}:{}",
- if self.session.is_tls { "s" } else { "" },
- self.session.local_ip,
- self.session.local_port
+ async fn resolve_response_url(&self, server: &Server) -> String {
+ server
+ .eval_if(
+ &server.core.network.http_response_url,
+ self,
+ self.session.session_id,
)
- })
+ .await
+ .unwrap_or_else(|| {
+ format!(
+ "http{}://{}:{}",
+ if self.session.is_tls { "s" } else { "" },
+ self.session.local_ip,
+ self.session.local_port
+ )
+ })
}
- pub async fn has_endpoint_access(&self, core: &Core) -> StatusCode {
- core.eval_if(
- &core.network.http_allowed_endpoint,
- self,
- self.session.session_id,
- )
- .await
- .unwrap_or(StatusCode::OK)
+ async fn has_endpoint_access(&self, server: &Server) -> StatusCode {
+ server
+ .eval_if(
+ &server.core.network.http_allowed_endpoint,
+ self,
+ self.session.session_id,
+ )
+ .await
+ .unwrap_or(StatusCode::OK)
}
}
diff --git a/crates/jmap/src/api/management/dkim.rs b/crates/jmap/src/api/management/dkim.rs
index 09e0c383..5fcbe4eb 100644
--- a/crates/jmap/src/api/management/dkim.rs
+++ b/crates/jmap/src/api/management/dkim.rs
@@ -6,7 +6,7 @@
use std::str::FromStr;
-use common::{auth::AccessToken, config::smtp::auth::simple_pem_parse};
+use common::{auth::AccessToken, config::smtp::auth::simple_pem_parse, Server};
use directory::{backend::internal::manage, Permission};
use hyper::Method;
use mail_auth::{
@@ -21,12 +21,10 @@ use serde::{Deserialize, Serialize};
use serde_json::json;
use store::write::now;
-use crate::{
- api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse},
- JMAP,
-};
+use crate::api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse};
use super::decode_path_element;
+use std::future::Future;
#[derive(Debug, Serialize, Deserialize, Copy, Clone, PartialEq, Eq)]
pub enum Algorithm {
@@ -42,8 +40,36 @@ struct DkimSignature {
selector: Option<String>,
}
-impl JMAP {
- pub async fn handle_manage_dkim(
+pub trait DkimManagement: Sync + Send {
+ fn handle_manage_dkim(
+ &self,
+ req: &HttpRequest,
+ path: Vec<&str>,
+ body: Option<Vec<u8>>,
+ access_token: &AccessToken,
+ ) -> impl Future<Output = trc::Result<HttpResponse>> + Send;
+
+ fn handle_get_public_key(
+ &self,
+ path: Vec<&str>,
+ ) -> impl Future<Output = trc::Result<HttpResponse>> + Send;
+
+ fn handle_create_signature(
+ &self,
+ body: Option<Vec<u8>>,
+ ) -> impl Future<Output = trc::Result<HttpResponse>> + Send;
+
+ fn create_dkim_key(
+ &self,
+ algo: Algorithm,
+ id: impl AsRef<str> + Send,
+ domain: impl Into<String> + Send,
+ selector: impl Into<String> + Send,
+ ) -> impl Future<Output = trc::Result<()>> + Send;
+}
+
+impl DkimManagement for Server {
+ async fn handle_manage_dkim(
&self,
req: &HttpRequest,
path: Vec<&str>,
diff --git a/crates/jmap/src/api/management/dns.rs b/crates/jmap/src/api/management/dns.rs
index 65e56bb1..72a7a54e 100644
--- a/crates/jmap/src/api/management/dns.rs
+++ b/crates/jmap/src/api/management/dns.rs
@@ -4,7 +4,7 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
-use common::auth::AccessToken;
+use common::{auth::AccessToken, Server};
use directory::{
backend::internal::manage::{self},
Permission,
@@ -17,27 +17,39 @@ use sha1::Digest;
use utils::config::Config;
use x509_parser::parse_x509_certificate;
-use crate::{
- api::{
- http::ToHttpResponse,
- management::dkim::{obtain_dkim_public_key, Algorithm},
- HttpRequest, HttpResponse, JsonResponse,
- },
- JMAP,
+use crate::api::{
+ http::ToHttpResponse,
+ management::dkim::{obtain_dkim_public_key, Algorithm},
+ HttpRequest, HttpResponse, JsonResponse,
};
use super::decode_path_element;
+use std::future::Future;
#[derive(Debug, Serialize, Deserialize)]
-struct DnsRecord {
+pub struct DnsRecord {
#[serde(rename = "type")]
typ: String,
name: String,
content: String,
}
-impl JMAP {
- pub async fn handle_manage_dns(
+pub trait DnsManagement: Sync + Send {
+ fn handle_manage_dns(
+ &self,
+ req: &HttpRequest,
+ path: Vec<&str>,
+ access_token: &AccessToken,
+ ) -> impl Future<Output = trc::Result<HttpResponse>> + Send;
+
+ fn build_dns_records(
+ &self,
+ domain_name: &str,
+ ) -> impl Future<Output = trc::Result<Vec<DnsRecord>>> + Send;
+}
+
+impl DnsManagement for Server {
+ async fn handle_manage_dns(
&self,
req: &HttpRequest,
path: Vec<&str>,
@@ -210,7 +222,7 @@ impl JMAP {
});
// Add MTA-STS records
- if let Some(policy) = self.core.build_mta_sts_policy() {
+ if let Some(policy) = self.build_mta_sts_policy() {
records.push(DnsRecord {
typ: "CNAME".to_string(),
name: format!("mta-sts.{domain_name}."),
@@ -239,7 +251,7 @@ impl JMAP {
});
// Add TLSA records
- for (name, key) in self.core.tls.certificates.load().iter() {
+ for (name, key) in self.inner.data.tls_certificates.load().iter() {
if !name.ends_with(domain_name)
|| name.starts_with("mta-sts.")
|| name.starts_with("autoconfig.")
diff --git a/crates/jmap/src/api/management/enterprise/telemetry.rs b/crates/jmap/src/api/management/enterprise/telemetry.rs
index b5b06e8f..6a1ff7b2 100644
--- a/crates/jmap/src/api/management/enterprise/telemetry.rs
+++ b/crates/jmap/src/api/management/enterprise/telemetry.rs
@@ -19,6 +19,7 @@ use common::{
metrics::store::{Metric, MetricsStore},
tracers::store::{TracingQuery, TracingStore},
},
+ Server,
};
use directory::{backend::internal::manage, Permission};
use http_body_util::{combinators::BoxBody, StreamBody};
@@ -28,6 +29,7 @@ use hyper::{
};
use mail_parser::DateTime;
use serde_json::json;
+use std::future::Future;
use store::ahash::{AHashMap, AHashSet};
use trc::{
ipc::{bitset::Bitset, subscriber::SubscriberBuilder},
@@ -41,11 +43,20 @@ use crate::{
http::ToHttpResponse, management::Timestamp, HttpRequest, HttpResponse, HttpResponseBody,
JsonResponse,
},
- JMAP,
+ auth::oauth::token::TokenHandler,
};
-impl JMAP {
- pub async fn handle_telemetry_api_request(
+pub trait TelemetryApi: Sync + Send {
+ fn handle_telemetry_api_request(
+ &self,
+ req: &HttpRequest,
+ path: Vec<&str>,
+ access_token: &AccessToken,
+ ) -> impl Future<Output = trc::Result<HttpResponse>> + Send;
+}
+
+impl TelemetryApi for Server {
+ async fn handle_telemetry_api_request(
&self,
req: &HttpRequest,
path: Vec<&str>,
@@ -455,9 +466,9 @@ impl JMAP {
] {
if metric_types.contains(&metric_type) {
let value = match metric_type {
- MetricType::QueueCount => self.core.total_queued_messages().await?,
- MetricType::UserCount => self.core.total_accounts().await?,
- MetricType::DomainCount => self.core.total_domains().await?,
+ MetricType::QueueCount => self.total_queued_messages().await?,
+ MetricType::UserCount => self.total_accounts().await?,
+ MetricType::DomainCount => self.total_domains().await?,
_ => unreachable!(),
};
Collector::update_gauge(metric_type, value);
diff --git a/crates/jmap/src/api/management/enterprise/undelete.rs b/crates/jmap/src/api/management/enterprise/undelete.rs
index e14de9ed..e3a40cef 100644
--- a/crates/jmap/src/api/management/enterprise/undelete.rs
+++ b/crates/jmap/src/api/management/enterprise/undelete.rs
@@ -11,12 +11,13 @@
use std::str::FromStr;
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
-use common::{auth::AccessToken, enterprise::undelete::DeletedBlob};
+use common::{auth::AccessToken, enterprise::undelete::DeletedBlob, Server};
use directory::backend::internal::manage::ManageDirectory;
use hyper::Method;
use jmap_proto::types::collection::Collection;
use mail_parser::{DateTime, MessageParser};
use serde_json::json;
+use std::future::Future;
use store::write::{BatchBuilder, BlobOp, ValueClass};
use trc::AddContext;
use utils::{url_params::UrlParams, BlobHash};
@@ -27,9 +28,10 @@ use crate::{
management::decode_path_element,
HttpRequest, HttpResponse, JsonResponse,
},
- email::ingest::{IngestEmail, IngestSource},
+ blob::download::BlobDownload,
+ email::ingest::{EmailIngest, IngestEmail, IngestSource},
mailbox::INBOX_ID,
- JMAP,
+ JmapMethods,
};
#[derive(serde::Deserialize, serde::Serialize)]
@@ -52,8 +54,18 @@ pub enum UndeleteResponse {
Error { reason: String },
}
-impl JMAP {
- pub async fn handle_undelete_api_request(
+pub trait UndeleteApi: Sync + Send {
+ fn handle_undelete_api_request(
+ &self,
+ req: &HttpRequest,
+ path: Vec<&str>,
+ body: Option<Vec<u8>>,
+ session: &HttpSessionData,
+ ) -> impl Future<Output = trc::Result<HttpResponse>> + Send;
+}
+
+impl UndeleteApi for Server {
+ async fn handle_undelete_api_request(
&self,
req: &HttpRequest,
path: Vec<&str>,
diff --git a/crates/jmap/src/api/management/log.rs b/crates/jmap/src/api/management/log.rs
index 94a87af2..ef4191e9 100644
--- a/crates/jmap/src/api/management/log.rs
+++ b/crates/jmap/src/api/management/log.rs
@@ -5,18 +5,16 @@ use std::{
};
use chrono::DateTime;
-use common::auth::AccessToken;
+use common::{auth::AccessToken, Server};
use directory::{backend::internal::manage, Permission};
use rev_lines::RevLines;
use serde::Serialize;
use serde_json::json;
+use std::future::Future;
use tokio::sync::oneshot;
use utils::url_params::UrlParams;
-use crate::{
- api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse},
- JMAP,
-};
+use crate::api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse};
#[derive(Serialize)]
struct LogEntry {
@@ -27,8 +25,16 @@ struct LogEntry {
details: String,
}
-impl JMAP {
- pub async fn handle_view_logs(
+pub trait LogManagement: Sync + Send {
+ fn handle_view_logs(
+ &self,
+ req: &HttpRequest,
+ access_token: &AccessToken,
+ ) -> impl Future<Output = trc::Result<HttpResponse>> + Send;
+}
+
+impl LogManagement for Server {
+ async fn handle_view_logs(
&self,
req: &HttpRequest,
access_token: &AccessToken,
diff --git a/crates/jmap/src/api/management/mod.rs b/crates/jmap/src/api/management/mod.rs
index 0a3dc0c2..d7a0b704 100644
--- a/crates/jmap/src/api/management/mod.rs
+++ b/crates/jmap/src/api/management/mod.rs
@@ -19,15 +19,28 @@ pub mod stores;
use std::{borrow::Cow, str::FromStr, sync::Arc};
-use common::auth::AccessToken;
+use common::{auth::AccessToken, Server};
use directory::{backend::internal::manage, Permission};
+use dkim::DkimManagement;
+use dns::DnsManagement;
+use enterprise::telemetry::TelemetryApi;
use hyper::Method;
+use log::LogManagement;
use mail_parser::DateTime;
+use principal::PrincipalManager;
+use queue::QueueManagement;
+use reload::ManageReload;
+use report::ManageReports;
use serde::Serialize;
+use settings::ManageSettings;
+use sieve::SieveHandler;
use store::write::now;
+use stores::ManageStore;
+
+use crate::{auth::oauth::auth::OAuthApiHandler, email::crypto::CryptoHandler};
use super::{http::HttpSessionData, HttpRequest, HttpResponse};
-use crate::JMAP;
+use std::future::Future;
#[derive(Serialize)]
#[serde(tag = "error")]
@@ -53,9 +66,19 @@ pub enum ManagementApiError<'x> {
},
}
-impl JMAP {
+pub trait ManagementApi: Sync + Send {
+ fn handle_api_manage_request(
+ &self,
+ req: &HttpRequest,
+ body: Option<Vec<u8>>,
+ access_token: Arc<AccessToken>,
+ session: &HttpSessionData,
+ ) -> impl Future<Output = trc::Result<HttpResponse>> + Send;
+}
+
+impl ManagementApi for Server {
#[allow(unused_variables)]
- pub async fn handle_api_manage_request(
+ async fn handle_api_manage_request(
&self,
req: &HttpRequest,
body: Option<Vec<u8>>,
diff --git a/crates/jmap/src/api/management/principal.rs b/crates/jmap/src/api/management/principal.rs
index 51994756..764414de 100644
--- a/crates/jmap/src/api/management/principal.rs
+++ b/crates/jmap/src/api/management/principal.rs
@@ -6,7 +6,7 @@
use std::sync::{atomic::Ordering, Arc};
-use common::auth::AccessToken;
+use common::{auth::AccessToken, Server};
use directory::{
backend::internal::{
lookup::DirectoryStore,
@@ -21,12 +21,10 @@ use serde_json::json;
use trc::AddContext;
use utils::url_params::UrlParams;
-use crate::{
- api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse},
- JMAP,
-};
+use crate::api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse};
use super::decode_path_element;
+use std::future::Future;
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[serde(tag = "type")]
@@ -47,8 +45,32 @@ pub struct AccountAuthResponse {
pub app_passwords: Vec<String>,
}
-impl JMAP {
- pub async fn handle_manage_principal(
+pub trait PrincipalManager: Sync + Send {
+ fn handle_manage_principal(
+ &self,
+ req: &HttpRequest,
+ path: Vec<&str>,
+ body: Option<Vec<u8>>,
+ access_token: &AccessToken,
+ ) -> impl Future<Output = trc::Result<HttpResponse>> + Send;
+
+ fn handle_account_auth_get(
+ &self,
+ access_token: Arc<AccessToken>,
+ ) -> impl Future<Output = trc::Result<HttpResponse>> + Send;
+
+ fn handle_account_auth_post(
+ &self,
+ req: &HttpRequest,
+ access_token: Arc<AccessToken>,
+ body: Option<Vec<u8>>,
+ ) -> impl Future<Output = trc::Result<HttpResponse>> + Send;
+
+ fn assert_supported_directory(&self) -> trc::Result<()>;
+}
+
+impl PrincipalManager for Server {
+ async fn handle_manage_principal(
&self,
req: &HttpRequest,
path: Vec<&str>,
@@ -297,13 +319,16 @@ impl JMAP {
}
// Remove entries from cache
- self.inner.sessions.retain(|_, id| id.item != account_id);
+ self.inner
+ .data
+ .http_auth_cache
+ .retain(|_, id| id.item != account_id);
if matches!(typ, Type::Role | Type::Tenant) {
// Update permissions cache
- self.core.security.permissions.clear();
- self.core
- .security
+ self.inner.data.permissions.clear();
+ self.inner
+ .data
.permissions_version
.fetch_add(1, Ordering::Relaxed);
}
@@ -399,20 +424,23 @@ impl JMAP {
if expire_session {
// Remove entries from cache
- self.inner.sessions.retain(|_, id| id.item != account_id);
+ self.inner
+ .data
+ .http_auth_cache
+ .retain(|_, id| id.item != account_id);
}
if is_role_change {
// Update permissions cache
- self.core.security.permissions.clear();
- self.core
- .security
+ self.inner.data.permissions.clear();
+ self.inner
+ .data
.permissions_version
.fetch_add(1, Ordering::Relaxed);
}
if expire_token {
- self.core.security.access_tokens.remove(&account_id);
+ self.inner.data.access_tokens.remove(&account_id);
}
Ok(JsonResponse::new(json!({
@@ -428,7 +456,7 @@ impl JMAP {
}
}
- pub async fn handle_account_auth_get(
+ async fn handle_account_auth_get(
&self,
access_token: Arc<AccessToken>,
) -> trc::Result<HttpResponse> {
@@ -463,7 +491,7 @@ impl JMAP {
.into_http_response())
}
- pub async fn handle_account_auth_post(
+ async fn handle_account_auth_post(
&self,
req: &HttpRequest,
access_token: Arc<AccessToken>,
@@ -513,7 +541,10 @@ impl JMAP {
.await?;
// Remove entries from cache
- self.inner.sessions.retain(|_, id| id.item != u32::MAX);
+ self.inner
+ .data
+ .http_auth_cache
+ .retain(|_, id| id.item != u32::MAX);
return Ok(JsonResponse::new(json!({
"data": (),
@@ -578,7 +609,8 @@ impl JMAP {
// Remove entries from cache
self.inner
- .sessions
+ .data
+ .http_auth_cache
.retain(|_, id| id.item != access_token.primary_id());
Ok(JsonResponse::new(json!({
@@ -587,7 +619,7 @@ impl JMAP {
.into_http_response())
}
- pub fn assert_supported_directory(&self) -> trc::Result<()> {
+ fn assert_supported_directory(&self) -> trc::Result<()> {
let class = match &self.core.storage.directory.store {
DirectoryInner::Internal(_) => return Ok(()),
DirectoryInner::Ldap(_) => "LDAP",
diff --git a/crates/jmap/src/api/management/queue.rs b/crates/jmap/src/api/management/queue.rs
index 596abf65..c19bc707 100644
--- a/crates/jmap/src/api/management/queue.rs
+++ b/crates/jmap/src/api/management/queue.rs
@@ -4,8 +4,10 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
+use std::future::Future;
+
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
-use common::auth::AccessToken;
+use common::{auth::AccessToken, ipc::QueueEvent, Server};
use directory::{
backend::internal::{manage::ManageDirectory, PrincipalField},
Permission, Type,
@@ -19,7 +21,10 @@ use mail_auth::{
use mail_parser::DateTime;
use serde::{Deserializer, Serializer};
use serde_json::json;
-use smtp::queue::{self, ErrorDetails, HostResponse, QueueId, Status};
+use smtp::{
+ queue::{self, spool::SmtpSpool, ErrorDetails, HostResponse, QueueId, Status},
+ reporting::{dmarc::DmarcReporting, tls::TlsReporting},
+};
use store::{
write::{key::DeserializeBigEndian, now, Bincode, QueueClass, ReportEvent, ValueClass},
Deserialize, IterateParams, ValueKey,
@@ -27,10 +32,7 @@ use store::{
use trc::AddContext;
use utils::url_params::UrlParams;
-use crate::{
- api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse},
- JMAP,
-};
+use crate::api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse};
use super::{decode_path_element, FutureTimestamp};
@@ -106,8 +108,17 @@ pub enum Report {
},
}
-impl JMAP {
- pub async fn handle_manage_queue(
+pub trait QueueManagement: Sync + Send {
+ fn handle_manage_queue(
+ &self,
+ req: &HttpRequest,
+ path: Vec<&str>,
+ access_token: &AccessToken,
+ ) -> impl Future<Output = trc::Result<HttpResponse>> + Send;
+}
+
+impl QueueManagement for Server {
+ async fn handle_manage_queue(
&self,
req: &HttpRequest,
path: Vec<&str>,
@@ -270,7 +281,6 @@ impl JMAP {
access_token.assert_has_permission(Permission::MessageQueueGet)?;
if let Some(message) = self
- .smtp
.read_message(queue_id.parse().unwrap_or_default())
.await
.filter(|message| {
@@ -298,7 +308,6 @@ impl JMAP {
let item = params.get("filter");
if let Some(mut message) = self
- .smtp
.read_message(queue_id.parse().unwrap_or_default())
.await
.filter(|message| {
@@ -329,9 +338,9 @@ impl JMAP {
if found {
let next_event = message.next_event().unwrap_or_default();
message
- .save_changes(&self.smtp, prev_event.into(), next_event.into())
+ .save_changes(self, prev_event.into(), next_event.into())
.await;
- let _ = self.smtp.inner.queue_tx.send(queue::Event::Reload).await;
+ let _ = self.inner.ipc.queue_tx.send(QueueEvent::Reload).await;
}
Ok(JsonResponse::new(json!({
@@ -347,7 +356,6 @@ impl JMAP {
access_token.assert_has_permission(Permission::MessageQueueDelete)?;
if let Some(mut message) = self
- .smtp
.read_message(queue_id.parse().unwrap_or_default())
.await
.filter(|message| {
@@ -411,14 +419,14 @@ impl JMAP {
}) {
let next_event = message.next_event().unwrap_or_default();
message
- .save_changes(&self.smtp, next_event.into(), prev_event.into())
+ .save_changes(self, next_event.into(), prev_event.into())
.await;
} else {
- message.remove(&self.smtp, prev_event).await;
+ message.remove(self, prev_event).await;
}
}
} else {
- message.remove(&self.smtp, prev_event).await;
+ message.remove(self, prev_event).await;
found = true;
}
@@ -528,7 +536,6 @@ impl JMAP {
{
let mut rua = Vec::new();
if let Some(report) = self
- .smtp
.generate_dmarc_aggregate_report(&event, &mut rua, None, 0)
.await?
{
@@ -542,7 +549,6 @@ impl JMAP {
{
let mut rua = Vec::new();
if let Some(report) = self
- .smtp
.generate_tls_aggregate_report(&[event.clone()], &mut rua, None, 0)
.await?
{
@@ -573,7 +579,7 @@ impl JMAP {
.as_ref()
.map_or(true, |domains| domains.contains(&event.domain)) =>
{
- self.smtp.delete_dmarc_report(event).await;
+ self.delete_dmarc_report(event).await;
true
}
QueueClass::TlsReportHeader(event)
@@ -581,7 +587,7 @@ impl JMAP {
.as_ref()
.map_or(true, |domains| domains.contains(&event.domain)) =>
{
- self.smtp.delete_tls_report(vec![event]).await;
+ self.delete_tls_report(vec![event]).await;
true
}
_ => false,
diff --git a/crates/jmap/src/api/management/reload.rs b/crates/jmap/src/api/management/reload.rs
index f396e1b6..4c4fe110 100644
--- a/crates/jmap/src/api/management/reload.rs
+++ b/crates/jmap/src/api/management/reload.rs
@@ -4,20 +4,36 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
-use common::auth::AccessToken;
+use common::{auth::AccessToken, ipc::HousekeeperEvent, Server};
use directory::Permission;
use hyper::Method;
use serde_json::json;
+use std::future::Future;
use utils::url_params::UrlParams;
use crate::{
api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse},
- services::housekeeper::Event,
- JMAP,
+ JmapMethods,
};
-impl JMAP {
- pub async fn handle_manage_reload(
+pub trait ManageReload: Sync + Send {
+ fn handle_manage_reload(
+ &self,
+ req: &HttpRequest,
+ path: Vec<&str>,
+ access_token: &AccessToken,
+ ) -> impl Future<Output = trc::Result<HttpResponse>> + Send;
+
+ fn handle_manage_update(
+ &self,
+ req: &HttpRequest,
+ path: Vec<&str>,
+ access_token: &AccessToken,
+ ) -> impl Future<Output = trc::Result<HttpResponse>> + Send;
+}
+
+impl ManageReload for Server {
+ async fn handle_manage_reload(
&self,
req: &HttpRequest,
path: Vec<&str>,
@@ -28,10 +44,10 @@ impl JMAP {
match (path.get(1).copied(), req.method()) {
(Some("lookup"), &Method::GET) => {
- let result = self.core.reload_lookups().await?;
+ let result = self.reload_lookups().await?;
// Update core
if let Some(core) = result.new_core {
- self.shared_core.store(core.into());
+ self.inner.shared_core.store(core.into());
}
Ok(JsonResponse::new(json!({
@@ -40,13 +56,14 @@ impl JMAP {
.into_http_response())
}
(Some("certificate"), &Method::GET) => Ok(JsonResponse::new(json!({
- "data": self.core.reload_certificates().await?.config,
+ "data": self.reload_certificates().await?.config,
}))
.into_http_response()),
(Some("server.blocked-ip"), &Method::GET) => {
- let result = self.core.reload_blocked_ips().await?;
+ let result = self.reload_blocked_ips().await?;
+
// Increment version counter
- self.core.network.blocked_ips.increment_version();
+ self.increment_blocked_version();
Ok(JsonResponse::new(json!({
"data": result.config,
@@ -54,28 +71,29 @@ impl JMAP {
.into_http_response())
}
(_, &Method::GET) => {
- let result = self.core.reload().await?;
+ let result = self.reload().await?;
if !UrlParams::new(req.uri().query()).has_key("dry-run") {
if let Some(core) = result.new_core {
// Update core
- self.shared_core.store(core.into());
+ self.inner.shared_core.store(core.into());
// Increment version counter
- self.inner.increment_config_version();
+ self.increment_config_version();
}
if let Some(tracers) = result.tracers {
// Update tracers
#[cfg(feature = "enterprise")]
- tracers.update(self.shared_core.load().is_enterprise_edition());
+ tracers.update(self.inner.shared_core.load().is_enterprise_edition());
#[cfg(not(feature = "enterprise"))]
tracers.update(false);
}
// Reload settings
self.inner
+ .ipc
.housekeeper_tx
- .send(Event::ReloadSettings)
+ .send(HousekeeperEvent::ReloadSettings)
.await
.map_err(|err| {
trc::EventType::Server(trc::ServerEvent::ThreadError)
@@ -94,7 +112,7 @@ impl JMAP {
}
}
- pub async fn handle_manage_update(
+ async fn handle_manage_update(
&self,
req: &HttpRequest,
path: Vec<&str>,
@@ -119,7 +137,11 @@ impl JMAP {
// Validate the access token
access_token.assert_has_permission(Permission::UpdateWebadmin)?;
- self.inner.webadmin.update_and_unpack(&self.core).await?;
+ self.inner
+ .data
+ .webadmin
+ .update_and_unpack(&self.core)
+ .await?;
Ok(JsonResponse::new(json!({
"data": (),
diff --git a/crates/jmap/src/api/management/report.rs b/crates/jmap/src/api/management/report.rs
index daa91451..5540d41b 100644
--- a/crates/jmap/src/api/management/report.rs
+++ b/crates/jmap/src/api/management/report.rs
@@ -4,7 +4,9 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
-use common::auth::AccessToken;
+use std::future::Future;
+
+use common::{auth::AccessToken, Server};
use directory::{
backend::internal::{manage::ManageDirectory, PrincipalField},
Permission, Type,
@@ -23,10 +25,7 @@ use store::{
use trc::AddContext;
use utils::url_params::UrlParams;
-use crate::{
- api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse},
- JMAP,
-};
+use crate::api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse};
use super::decode_path_element;
@@ -36,8 +35,17 @@ enum ReportType {
Arf,
}
-impl JMAP {
- pub async fn handle_manage_reports(
+pub trait ManageReports: Sync + Send {
+ fn handle_manage_reports(
+ &self,
+ req: &HttpRequest,
+ path: Vec<&str>,
+ access_token: &AccessToken,
+ ) -> impl Future<Output = trc::Result<HttpResponse>> + Send;
+}
+
+impl ManageReports for Server {
+ async fn handle_manage_reports(
&self,
req: &HttpRequest,
path: Vec<&str>,
diff --git a/crates/jmap/src/api/management/settings.rs b/crates/jmap/src/api/management/settings.rs
index 6844c503..ccabef51 100644
--- a/crates/jmap/src/api/management/settings.rs
+++ b/crates/jmap/src/api/management/settings.rs
@@ -4,19 +4,17 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
-use common::auth::AccessToken;
+use common::{auth::AccessToken, Server};
use directory::Permission;
use hyper::Method;
use serde_json::json;
use store::ahash::AHashMap;
use utils::{config::ConfigKey, map::vec_map::VecMap, url_params::UrlParams};
-use crate::{
- api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse},
- JMAP,
-};
+use crate::api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse};
use super::decode_path_element;
+use std::future::Future;
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[serde(tag = "type")]
@@ -34,8 +32,18 @@ pub enum UpdateSettings {
},
}
-impl JMAP {
- pub async fn handle_manage_settings(
+pub trait ManageSettings: Sync + Send {
+ fn handle_manage_settings(
+ &self,
+ req: &HttpRequest,
+ path: Vec<&str>,
+ body: Option<Vec<u8>>,
+ access_token: &AccessToken,
+ ) -> impl Future<Output = trc::Result<HttpResponse>> + Send;
+}
+
+impl ManageSettings for Server {
+ async fn handle_manage_settings(
&self,
req: &HttpRequest,
path: Vec<&str>,
diff --git a/crates/jmap/src/api/management/sieve.rs b/crates/jmap/src/api/management/sieve.rs
index eb918185..85159378 100644
--- a/crates/jmap/src/api/management/sieve.rs
+++ b/crates/jmap/src/api/management/sieve.rs
@@ -6,18 +6,16 @@
use std::time::SystemTime;
-use common::{auth::AccessToken, scripts::ScriptModification, IntoString};
+use common::{auth::AccessToken, scripts::ScriptModification, IntoString, Server};
use directory::Permission;
use hyper::Method;
use serde_json::json;
use sieve::{runtime::Variable, Envelope};
-use smtp::scripts::{ScriptParameters, ScriptResult};
+use smtp::scripts::{event_loop::RunScript, ScriptParameters, ScriptResult};
+use std::future::Future;
use utils::url_params::UrlParams;
-use crate::{
- api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse},
- JMAP,
-};
+use crate::api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse};
#[derive(Debug, serde::Serialize)]
#[serde(tag = "action")]
@@ -36,8 +34,18 @@ pub enum Response {
Discard,
}
-impl JMAP {
- pub async fn handle_run_sieve(
+pub trait SieveHandler: Sync + Send {
+ fn handle_run_sieve(
+ &self,
+ req: &HttpRequest,
+ path: Vec<&str>,
+ body: Option<Vec<u8>>,
+ access_token: &AccessToken,
+ ) -> impl Future<Output = trc::Result<HttpResponse>> + Send;
+}
+
+impl SieveHandler for Server {
+ async fn handle_run_sieve(
&self,
req: &HttpRequest,
path: Vec<&str>,
@@ -103,7 +111,7 @@ impl JMAP {
}
// Run script
- let result = match self.smtp.run_script(script_id, script, params, 0).await {
+ let result = match self.run_script(script_id, script, params, 0).await {
ScriptResult::Accept { modifications } => Response::Accept { modifications },
ScriptResult::Replace {
message,
diff --git a/crates/jmap/src/api/management/stores.rs b/crates/jmap/src/api/management/stores.rs
index b86c88be..0b9dea48 100644
--- a/crates/jmap/src/api/management/stores.rs
+++ b/crates/jmap/src/api/management/stores.rs
@@ -5,7 +5,12 @@
*/
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
-use common::{auth::AccessToken, manager::webadmin::Resource};
+use common::{
+ auth::AccessToken,
+ ipc::{HousekeeperEvent, PurgeType},
+ manager::webadmin::Resource,
+ Server,
+};
use directory::{
backend::internal::manage::{self, ManageDirectory},
Permission,
@@ -19,14 +24,30 @@ use crate::{
http::{HttpSessionData, ToHttpResponse},
HttpRequest, HttpResponse, JsonResponse,
},
- services::housekeeper::{Event, PurgeType},
- JMAP,
+ services::index::Indexer,
};
-use super::decode_path_element;
+use super::{decode_path_element, enterprise::undelete::UndeleteApi};
+use std::future::Future;
+
+pub trait ManageStore: Sync + Send {
+ fn handle_manage_store(
+ &self,
+ req: &HttpRequest,
+ path: Vec<&str>,
+ body: Option<Vec<u8>>,
+ session: &HttpSessionData,
+ access_token: &AccessToken,
+ ) -> impl Future<Output = trc::Result<HttpResponse>> + Send;
+
+ fn housekeeper_request(
+ &self,
+ event: HousekeeperEvent,
+ ) -> impl Future<Output = trc::Result<HttpResponse>> + Send;
+}
-impl JMAP {
- pub async fn handle_manage_store(
+impl ManageStore for Server {
+ async fn handle_manage_store(
&self,
req: &HttpRequest,
path: Vec<&str>,
@@ -75,7 +96,7 @@ impl JMAP {
// Validate the access token
access_token.assert_has_permission(Permission::PurgeBlobStore)?;
- self.housekeeper_request(Event::Purge(PurgeType::Blobs {
+ self.housekeeper_request(HousekeeperEvent::Purge(PurgeType::Blobs {
store: self.core.storage.data.clone(),
blob_store: self.core.storage.blob.clone(),
}))
@@ -95,7 +116,7 @@ impl JMAP {
self.core.storage.data.clone()
};
- self.housekeeper_request(Event::Purge(PurgeType::Data(store)))
+ self.housekeeper_request(HousekeeperEvent::Purge(PurgeType::Data(store)))
.await
}
(Some("purge"), Some("lookup"), id, &Method::GET) => {
@@ -112,7 +133,7 @@ impl JMAP {
self.core.storage.lookup.clone()
};
- self.housekeeper_request(Event::Purge(PurgeType::Lookup(store)))
+ self.housekeeper_request(HousekeeperEvent::Purge(PurgeType::Lookup(store)))
.await
}
(Some("purge"), Some("account"), id, &Method::GET) => {
@@ -131,7 +152,7 @@ impl JMAP {
None
};
- self.housekeeper_request(Event::Purge(PurgeType::Account(account_id)))
+ self.housekeeper_request(HousekeeperEvent::Purge(PurgeType::Account(account_id)))
.await
}
(Some("reindex"), id, None, &Method::GET) => {
@@ -192,12 +213,17 @@ impl JMAP {
}
}
- async fn housekeeper_request(&self, event: Event) -> trc::Result<HttpResponse> {
- self.inner.housekeeper_tx.send(event).await.map_err(|err| {
- trc::EventType::Server(trc::ServerEvent::ThreadError)
- .reason(err)
- .details("Failed to send housekeeper event")
- })?;
+ async fn housekeeper_request(&self, event: HousekeeperEvent) -> trc::Result<HttpResponse> {
+ self.inner
+ .ipc
+ .housekeeper_tx
+ .send(event)
+ .await
+ .map_err(|err| {
+ trc::EventType::Server(trc::ServerEvent::ThreadError)
+ .reason(err)
+ .details("Failed to send housekeeper event")
+ })?;
Ok(JsonResponse::new(json!({
"data": (),
diff --git a/crates/jmap/src/api/mod.rs b/crates/jmap/src/api/mod.rs
index 2d9fee0e..0ee7ef52 100644
--- a/crates/jmap/src/api/mod.rs
+++ b/crates/jmap/src/api/mod.rs
@@ -4,15 +4,14 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
-use std::borrow::Cow;
+use std::{borrow::Cow, sync::Arc};
+use common::Inner;
use hyper::StatusCode;
use jmap_proto::types::{id::Id, state::State, type_state::DataType};
use serde::Serialize;
use utils::map::vec_map::VecMap;
-use crate::JmapInstance;
-
pub mod autoconfig;
pub mod event_source;
pub mod http;
@@ -22,11 +21,11 @@ pub mod session;
#[derive(Clone)]
pub struct JmapSessionManager {
- pub inner: JmapInstance,
+ pub inner: Arc<Inner>,
}
impl JmapSessionManager {
- pub fn new(inner: JmapInstance) -> Self {
+ pub fn new(inner: Arc<Inner>) -> Self {
Self { inner }
}
}
diff --git a/crates/jmap/src/api/request.rs b/crates/jmap/src/api/request.rs
index 6d81b480..4b2a184a 100644
--- a/crates/jmap/src/api/request.rs
+++ b/crates/jmap/src/api/request.rs
@@ -6,7 +6,7 @@
use std::{sync::Arc, time::Instant};
-use common::auth::AccessToken;
+use common::{auth::AccessToken, Server};
use jmap_proto::{
method::{
get, query,
@@ -18,12 +18,51 @@ use jmap_proto::{
};
use trc::JmapEvent;
-use crate::JMAP;
+use crate::{
+ blob::{copy::BlobCopy, get::BlobOperations, upload::BlobUpload},
+ changes::{get::ChangesLookup, query::QueryChanges},
+ email::{
+ copy::EmailCopy, get::EmailGet, import::EmailImport, parse::EmailParse, query::EmailQuery,
+ set::EmailSet, snippet::EmailSearchSnippet,
+ },
+ identity::{get::IdentityGet, set::IdentitySet},
+ mailbox::{get::MailboxGet, query::MailboxQuery, set::MailboxSet},
+ principal::{get::PrincipalGet, query::PrincipalQuery},
+ push::{get::PushSubscriptionFetch, set::PushSubscriptionSet},
+ quota::{get::QuotaGet, query::QuotaQuery},
+ services::state::StateManager,
+ sieve::{
+ get::SieveScriptGet, query::SieveScriptQuery, set::SieveScriptSet,
+ validate::SieveScriptValidate,
+ },
+ submission::{get::EmailSubmissionGet, query::EmailSubmissionQuery, set::EmailSubmissionSet},
+ thread::get::ThreadGet,
+ vacation::{get::VacationResponseGet, set::VacationResponseSet},
+};
use super::http::HttpSessionData;
+use std::future::Future;
+
+pub trait RequestHandler: Sync + Send {
+ fn handle_request(
+ &self,
+ request: Request,
+ access_token: Arc<AccessToken>,
+ session: &HttpSessionData,
+ ) -> impl Future<Output = Response> + Send;
+
+ fn handle_method_call(
+ &self,
+ method: RequestMethod,
+ method_name: &'static str,
+ access_token: &AccessToken,
+ next_call: &mut Option<Call<RequestMethod>>,
+ session: &HttpSessionData,
+ ) -> impl Future<Output = trc::Result<ResponseMethod>> + Send;
+}
-impl JMAP {
- pub async fn handle_request(
+impl RequestHandler for Server {
+ async fn handle_request(
&self,
request: Request,
access_token: Arc<AccessToken>,
diff --git a/crates/jmap/src/api/session.rs b/crates/jmap/src/api/session.rs
index 1480487f..3c94a334 100644
--- a/crates/jmap/src/api/session.rs
+++ b/crates/jmap/src/api/session.rs
@@ -6,18 +6,27 @@
use std::sync::Arc;
-use common::auth::AccessToken;
+use common::{auth::AccessToken, Server};
use directory::{backend::internal::PrincipalField, QueryBy};
use jmap_proto::{
request::capability::{Capability, Session},
types::{acl::Acl, collection::Collection, id::Id},
};
+use std::future::Future;
use trc::AddContext;
-use crate::JMAP;
+use crate::auth::acl::AclMethods;
-impl JMAP {
- pub async fn handle_session_resource(
+pub trait SessionHandler: Sync + Send {
+ fn handle_session_resource(
+ &self,
+ base_url: String,
+ access_token: Arc<AccessToken>,
+ ) -> impl Future<Output = trc::Result<Session>> + Send;
+}
+
+impl SessionHandler for Server {
+ async fn handle_session_resource(
&self,
base_url: String,
access_token: Arc<AccessToken>,
diff --git a/crates/jmap/src/auth/acl.rs b/crates/jmap/src/auth/acl.rs
index c574da61..e7a40117 100644
--- a/crates/jmap/src/auth/acl.rs
+++ b/crates/jmap/src/auth/acl.rs
@@ -4,7 +4,9 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
-use common::auth::AccessToken;
+use std::future::Future;
+
+use common::{auth::AccessToken, Server};
use directory::{backend::internal::PrincipalField, QueryBy};
use jmap_proto::{
error::set::SetError,
@@ -25,10 +27,77 @@ use store::{
use trc::AddContext;
use utils::map::bitmap::Bitmap;
-use crate::JMAP;
+use crate::JmapMethods;
+
+pub trait AclMethods: Sync + Send {
+ fn shared_documents(
+ &self,
+ access_token: &AccessToken,
+ to_account_id: u32,
+ to_collection: Collection,
+ check_acls: impl Into<Bitmap<Acl>> + Send,
+ ) -> impl Future<Output = trc::Result<RoaringBitmap>> + Send;
+
+ fn shared_messages(
+ &self,
+ access_token: &AccessToken,
+ to_account_id: u32,
+ check_acls: impl Into<Bitmap<Acl>> + Send,
+ ) -> impl Future<Output = trc::Result<RoaringBitmap>> + Send;
+
+ fn owned_or_shared_documents(
+ &self,
+ access_token: &AccessToken,
+ account_id: u32,
+ collection: Collection,
+ check_acls: impl Into<Bitmap<Acl>> + Send,
+ ) -> impl Future<Output = trc::Result<RoaringBitmap>> + Send;
+
+ fn owned_or_shared_messages(
+ &self,
+ access_token: &AccessToken,
+ account_id: u32,
+ check_acls: impl Into<Bitmap<Acl>> + Send,
+ ) -> impl Future<Output = trc::Result<RoaringBitmap>> + Send;
+
+ fn has_access_to_document(
+ &self,
+ access_token: &AccessToken,
+ to_account_id: u32,
+ to_collection: impl Into<u8> + Send,
+ to_document_id: u32,
+ check_acls: impl Into<Bitmap<Acl>> + Send,
+ ) -> impl Future<Output = trc::Result<bool>> + Send;
+
+ fn acl_set(
+ &self,
+ changes: &mut Object<Value>,
+ current: Option<&HashedValue<Object<Value>>>,
+ acl_changes: MaybePatchValue,
+ ) -> impl Future<Output = Result<(), SetError>> + Send;
-impl JMAP {
- pub async fn shared_documents(
+ fn acl_get(
+ &self,
+ value: &[AclGrant],
+ access_token: &AccessToken,
+ account_id: u32,
+ ) -> impl Future<Output = Value> + Send;
+
+ fn refresh_acls(&self, changes: &Object<Value>, current: &Option<HashedValue<Object<Value>>>);
+
+ fn map_acl_set(
+ &self,
+ acl_set: Vec<Value>,
+ ) -> impl Future<Output = Result<Vec<AclGrant>, SetError>> + Send;
+
+ fn map_acl_patch(
+ &self,
+ acl_patch: Vec<Value>,
+ ) -> impl Future<Output = Result<(AclGrant, Option<bool>), SetError>> + Send;
+}
+
+impl AclMethods for Server {
+ async fn shared_documents(
&self,
access_token: &AccessToken,
to_account_id: u32,
@@ -66,7 +135,7 @@ impl JMAP {
Ok(document_ids)
}
- pub async fn shared_messages(
+ async fn shared_messages(
&self,
access_token: &AccessToken,
to_account_id: u32,
@@ -97,7 +166,7 @@ impl JMAP {
Ok(shared_messages)
}
- pub async fn owned_or_shared_documents(
+ async fn owned_or_shared_documents(
&self,
access_token: &AccessToken,
account_id: u32,
@@ -117,7 +186,7 @@ impl JMAP {
Ok(document_ids)
}
- pub async fn owned_or_shared_messages(
+ async fn owned_or_shared_messages(
&self,
access_token: &AccessToken,
account_id: u32,
@@ -136,7 +205,7 @@ impl JMAP {
Ok(document_ids)
}
- pub async fn has_access_to_document(
+ async fn has_access_to_document(
&self,
access_token: &AccessToken,
to_account_id: u32,
@@ -179,7 +248,7 @@ impl JMAP {
Ok(false)
}
- pub async fn acl_set(
+ async fn acl_set(
&self,
changes: &mut Object<Value>,
current: Option<&HashedValue<Object<Value>>>,
@@ -251,7 +320,7 @@ impl JMAP {
Ok(())
}
- pub async fn acl_get(
+ async fn acl_get(
&self,
value: &[AclGrant],
access_token: &AccessToken,
@@ -287,13 +356,9 @@ impl JMAP {
}
}
- pub fn refresh_acls(
- &self,
- changes: &Object<Value>,
- current: &Option<HashedValue<Object<Value>>>,
- ) {
+ fn refresh_acls(&self, changes: &Object<Value>, current: &Option<HashedValue<Object<Value>>>) {
if let Value::Acl(acl_changes) = changes.get(&Property::Acl) {
- let access_tokens = &self.core.security.access_tokens;
+ let access_tokens = &self.inner.data.access_tokens;
if let Some(Value::Acl(acl_current)) = current
.as_ref()
.and_then(|current| current.inner.properties.get(&Property::Acl))
diff --git a/crates/jmap/src/auth/authenticate.rs b/crates/jmap/src/auth/authenticate.rs
index 9bb5cc2d..8009ab6d 100644
--- a/crates/jmap/src/auth/authenticate.rs
+++ b/crates/jmap/src/auth/authenticate.rs
@@ -6,82 +6,101 @@
use std::{net::IpAddr, sync::Arc, time::Instant};
-use common::listener::limiter::InFlight;
+use common::{listener::limiter::InFlight, Server};
use directory::Permission;
use hyper::header;
use mail_parser::decoders::base64::base64_decode;
use mail_send::Credentials;
use utils::map::ttl_dashmap::TtlMap;
-use crate::{
- api::{http::HttpSessionData, HttpRequest},
- JMAP,
-};
+use crate::api::{http::HttpSessionData, HttpRequest};
use common::auth::AccessToken;
+use std::future::Future;
-impl JMAP {
- pub async fn authenticate_headers(
+use super::{oauth::token::TokenHandler, rate_limit::RateLimiter};
+
+pub trait Authenticator: Sync + Send {
+ fn authenticate_headers(
+ &self,
+ req: &HttpRequest,
+ session: &HttpSessionData,
+ ) -> impl Future<Output = trc::Result<(InFlight, Arc<AccessToken>)>> + Send;
+
+ fn cache_session(&self, session_id: String, access_token: &AccessToken);
+
+ fn authenticate_plain(
+ &self,
+ username: &str,
+ secret: &str,
+ remote_ip: IpAddr,
+ session_id: u64,
+ ) -> impl Future<Output = trc::Result<AccessToken>> + Send;
+}
+
+impl Authenticator for Server {
+ async fn authenticate_headers(
&self,
req: &HttpRequest,
session: &HttpSessionData,
) -> trc::Result<(InFlight, Arc<AccessToken>)> {
if let Some((mechanism, token)) = req.authorization() {
- let access_token = if let Some(account_id) = self.inner.sessions.get_with_ttl(token) {
- self.core.get_cached_access_token(account_id).await?
- } else {
- let access_token = if mechanism.eq_ignore_ascii_case("basic") {
- // Enforce rate limit for authentication requests
- self.is_auth_allowed_soft(&session.remote_ip).await?;
-
- // Decode the base64 encoded credentials
- if let Some((account, secret)) = base64_decode(token.as_bytes())
- .and_then(|token| String::from_utf8(token).ok())
- .and_then(|token| {
- token.split_once(':').map(|(login, secret)| {
- (login.trim().to_lowercase(), secret.to_string())
+ let access_token =
+ if let Some(account_id) = self.inner.data.http_auth_cache.get_with_ttl(token) {
+ self.get_cached_access_token(account_id).await?
+ } else {
+ let access_token = if mechanism.eq_ignore_ascii_case("basic") {
+ // Enforce rate limit for authentication requests
+ self.is_auth_allowed_soft(&session.remote_ip).await?;
+
+ // Decode the base64 encoded credentials
+ if let Some((account, secret)) = base64_decode(token.as_bytes())
+ .and_then(|token| String::from_utf8(token).ok())
+ .and_then(|token| {
+ token.split_once(':').map(|(login, secret)| {
+ (login.trim().to_lowercase(), secret.to_string())
+ })
})
- })
- {
- self.authenticate_plain(
- &account,
- &secret,
- session.remote_ip,
- session.session_id,
- )
- .await?
+ {
+ self.authenticate_plain(
+ &account,
+ &secret,
+ session.remote_ip,
+ session.session_id,
+ )
+ .await?
+ } else {
+ return Err(trc::AuthEvent::Error
+ .into_err()
+ .details("Failed to decode Basic auth request.")
+ .id(token.to_string())
+ .caused_by(trc::location!()));
+ }
+ } else if mechanism.eq_ignore_ascii_case("bearer") {
+ // Enforce anonymous rate limit for bearer auth requests
+ self.is_anonymous_allowed(&session.remote_ip).await?;
+
+ let (account_id, _, _) =
+ self.validate_access_token("access_token", token).await?;
+
+ self.get_access_token(account_id).await?
} else {
+ // Enforce anonymous rate limit
+ self.is_anonymous_allowed(&session.remote_ip).await?;
return Err(trc::AuthEvent::Error
.into_err()
- .details("Failed to decode Basic auth request.")
- .id(token.to_string())
+ .reason("Unsupported authentication mechanism.")
+ .details(token.to_string())
.caused_by(trc::location!()));
- }
- } else if mechanism.eq_ignore_ascii_case("bearer") {
- // Enforce anonymous rate limit for bearer auth requests
- self.is_anonymous_allowed(&session.remote_ip).await?;
+ };
- let (account_id, _, _) =
- self.validate_access_token("access_token", token).await?;
-
- self.core.get_access_token(account_id).await?
- } else {
- // Enforce anonymous rate limit
- self.is_anonymous_allowed(&session.remote_ip).await?;
- return Err(trc::AuthEvent::Error
- .into_err()
- .reason("Unsupported authentication mechanism.")
- .details(token.to_string())
- .caused_by(trc::location!()));
+ // Cache session
+ let access_token = Arc::new(access_token);
+ self.cache_session(token.to_string(), &access_token);
+ self.cache_access_token(access_token.clone());
+ access_token
};
- // Cache session
- let access_token = Arc::new(access_token);
- self.cache_session(token.to_string(), &access_token);
- self.core.cache_access_token(access_token.clone());
- access_token
- };
-
// Enforce authenticated rate limit
self.is_account_allowed(&access_token)
.await
@@ -97,15 +116,15 @@ impl JMAP {
}
}
- pub fn cache_session(&self, session_id: String, access_token: &AccessToken) {
- self.inner.sessions.insert_with_ttl(
+ fn cache_session(&self, session_id: String, access_token: &AccessToken) {
+ self.inner.data.http_auth_cache.insert_with_ttl(
session_id,
access_token.primary_id(),
Instant::now() + self.core.jmap.session_cache_ttl,
);
}
- pub async fn authenticate_plain(
+ async fn authenticate_plain(
&self,
username: &str,
secret: &str,
@@ -113,7 +132,6 @@ impl JMAP {
session_id: u64,
) -> trc::Result<AccessToken> {
match self
- .core
.authenticate(
&self.core.storage.directory,
session_id,
@@ -126,15 +144,11 @@ impl JMAP {
)
.await
{
- Ok(principal) => self
- .core
- .build_access_token(principal)
- .await
- .and_then(|token| {
- token
- .assert_has_permission(Permission::Authenticate)
- .map(|_| token)
- }),
+ Ok(principal) => self.build_access_token(principal).await.and_then(|token| {
+ token
+ .assert_has_permission(Permission::Authenticate)
+ .map(|_| token)
+ }),
Err(err) => {
if !err.matches(trc::EventType::Auth(trc::AuthEvent::MissingTotp)) {
let _ = self.is_auth_allowed_hard(&remote_ip).await;
diff --git a/crates/jmap/src/auth/oauth/auth.rs b/crates/jmap/src/auth/oauth/auth.rs
index 78f53454..4274dc17 100644
--- a/crates/jmap/src/auth/oauth/auth.rs
+++ b/crates/jmap/src/auth/oauth/auth.rs
@@ -6,9 +6,10 @@
use std::sync::Arc;
-use common::auth::AccessToken;
+use common::{auth::AccessToken, Server};
use rand::distributions::Standard;
use serde_json::json;
+use std::future::Future;
use store::{
rand::{distributions::Alphanumeric, thread_rng, Rng},
write::Bincode,
@@ -18,7 +19,6 @@ use store::{
use crate::{
api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse},
auth::oauth::OAuthStatus,
- JMAP,
};
use super::{
@@ -26,8 +26,23 @@ use super::{
MAX_POST_LEN, USER_CODE_ALPHABET, USER_CODE_LEN,
};
-impl JMAP {
- pub async fn handle_oauth_api_request(
+pub trait OAuthApiHandler: Sync + Send {
+ fn handle_oauth_api_request(
+ &self,
+ access_token: Arc<AccessToken>,
+ body: Option<Vec<u8>>,
+ ) -> impl Future<Output = trc::Result<HttpResponse>> + Send;
+
+ fn handle_device_auth(
+ &self,
+ req: &mut HttpRequest,
+ base_url: impl AsRef<str> + Send,
+ session_id: u64,
+ ) -> impl Future<Output = trc::Result<HttpResponse>> + Send;
+}
+
+impl OAuthApiHandler for Server {
+ async fn handle_oauth_api_request(
&self,
access_token: Arc<AccessToken>,
body: Option<Vec<u8>>,
@@ -143,7 +158,7 @@ impl JMAP {
Ok(JsonResponse::new(response).into_http_response())
}
- pub async fn handle_device_auth(
+ async fn handle_device_auth(
&self,
req: &mut HttpRequest,
base_url: impl AsRef<str>,
diff --git a/crates/jmap/src/auth/oauth/mod.rs b/crates/jmap/src/auth/oauth/mod.rs
index 0a83063a..7c9e7337 100644
--- a/crates/jmap/src/auth/oauth/mod.rs
+++ b/crates/jmap/src/auth/oauth/mod.rs
@@ -202,7 +202,7 @@ pub struct FormData {
}
impl FormData {
- pub async fn from_request(
+ async fn from_request(
req: &mut HttpRequest,
max_len: usize,
session_id: u64,
diff --git a/crates/jmap/src/auth/oauth/token.rs b/crates/jmap/src/auth/oauth/token.rs
index 590a9c4d..ec23b3c4 100644
--- a/crates/jmap/src/auth/oauth/token.rs
+++ b/crates/jmap/src/auth/oauth/token.rs
@@ -6,10 +6,12 @@
use std::time::SystemTime;
+use common::Server;
use directory::{backend::internal::PrincipalField, QueryBy};
use hyper::StatusCode;
use mail_builder::encoders::base64::base64_encode;
use mail_parser::decoders::base64::base64_decode;
+use std::future::Future;
use store::{
blake3,
rand::{thread_rng, Rng},
@@ -20,7 +22,6 @@ use utils::codec::leb128::{Leb128Iterator, Leb128Vec};
use crate::{
api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse},
auth::SymmetricEncrypt,
- JMAP,
};
use super::{
@@ -28,9 +29,52 @@ use super::{
MAX_POST_LEN, RANDOM_CODE_LEN,
};
-impl JMAP {
+pub trait TokenHandler: Sync + Send {
+ fn handle_token_request(
+ &self,
+ req: &mut HttpRequest,
+ session_id: u64,
+ ) -> impl Future<Output = trc::Result<HttpResponse>> + Send;
+
+ fn password_hash(
+ &self,
+ account_id: u32,
+ ) -> impl Future<Output = Result<String, &'static str>> + Send;
+
+ fn issue_token(
+ &self,
+ account_id: u32,
+ client_id: &str,
+ with_refresh_token: bool,
+ ) -> impl Future<Output = Result<OAuthResponse, &'static str>> + Send;
+
+ fn issue_custom_token(
+ &self,
+ account_id: u32,
+ grant_type: &str,
+ client_id: &str,
+ expiry_in: u64,
+ ) -> impl Future<Output = trc::Result<String>> + Send;
+
+ fn encode_access_token(
+ &self,
+ grant_type: &str,
+ account_id: u32,
+ password_hash: &str,
+ client_id: &str,
+ expiry_in: u64,
+ ) -> Result<String, &'static str>;
+
+ fn validate_access_token(
+ &self,
+ grant_type: &str,
+ token_: &str,
+ ) -> impl Future<Output = trc::Result<(u32, String, u64)>> + Send;
+}
+
+impl TokenHandler for Server {
// Token endpoint
- pub async fn handle_token_request(
+ async fn handle_token_request(
&self,
req: &mut HttpRequest,
session_id: u64,
@@ -199,7 +243,7 @@ impl JMAP {
}
}
- pub async fn issue_token(
+ async fn issue_token(
&self,
account_id: u32,
client_id: &str,
@@ -233,7 +277,7 @@ impl JMAP {
})
}
- pub async fn issue_custom_token(
+ async fn issue_custom_token(
&self,
account_id: u32,
grant_type: &str,
@@ -303,7 +347,7 @@ impl JMAP {
Ok(String::from_utf8(base64_encode(&token).unwrap_or_default()).unwrap())
}
- pub async fn validate_access_token(
+ async fn validate_access_token(
&self,
grant_type: &str,
token_: &str,
diff --git a/crates/jmap/src/auth/rate_limit.rs b/crates/jmap/src/auth/rate_limit.rs
index 25b1ec9b..6d7be622 100644
--- a/crates/jmap/src/auth/rate_limit.rs
+++ b/crates/jmap/src/auth/rate_limit.rs
@@ -6,23 +6,33 @@
use std::{net::IpAddr, sync::Arc};
-use common::listener::limiter::{ConcurrencyLimiter, InFlight};
+use common::{
+ listener::limiter::{ConcurrencyLimiter, InFlight},
+ ConcurrencyLimiters, Server,
+};
use directory::Permission;
use trc::AddContext;
-use crate::JMAP;
-
use common::auth::AccessToken;
+use std::future::Future;
-pub struct ConcurrencyLimiters {
- pub concurrent_requests: ConcurrencyLimiter,
- pub concurrent_uploads: ConcurrencyLimiter,
+pub trait RateLimiter: Sync + Send {
+ fn get_concurrency_limiter(&self, account_id: u32) -> Arc<ConcurrencyLimiters>;
+ fn is_account_allowed(
+ &self,
+ access_token: &AccessToken,
+ ) -> impl Future<Output = trc::Result<InFlight>> + Send;
+ fn is_anonymous_allowed(&self, addr: &IpAddr) -> impl Future<Output = trc::Result<()>> + Send;
+ fn is_upload_allowed(&self, access_token: &AccessToken) -> trc::Result<InFlight>;
+ fn is_auth_allowed_soft(&self, addr: &IpAddr) -> impl Future<Output = trc::Result<()>> + Send;
+ fn is_auth_allowed_hard(&self, addr: &IpAddr) -> impl Future<Output = trc::Result<()>> + Send;
}
-impl JMAP {
- pub fn get_concurrency_limiter(&self, account_id: u32) -> Arc<ConcurrencyLimiters> {
+impl RateLimiter for Server {
+ fn get_concurrency_limiter(&self, account_id: u32) -> Arc<ConcurrencyLimiters> {
self.inner
- .concurrency_limiter
+ .data
+ .jmap_limiter
.get(&account_id)
.map(|limiter| limiter.clone())
.unwrap_or_else(|| {
@@ -35,13 +45,14 @@ impl JMAP {
),
});
self.inner
- .concurrency_limiter
+ .data
+ .jmap_limiter
.insert(account_id, limiter.clone());
limiter
})
}
- pub async fn is_account_allowed(&self, access_token: &AccessToken) -> trc::Result<InFlight> {
+ async fn is_account_allowed(&self, access_token: &AccessToken) -> trc::Result<InFlight> {
let limiter = self.get_concurrency_limiter(access_token.primary_id());
let is_rate_allowed = if let Some(rate) = &self.core.jmap.rate_authenticated {
self.core
@@ -74,7 +85,7 @@ impl JMAP {
}
}
- pub async fn is_anonymous_allowed(&self, addr: &IpAddr) -> trc::Result<()> {
+ async fn is_anonymous_allowed(&self, addr: &IpAddr) -> trc::Result<()> {
if let Some(rate) = &self.core.jmap.rate_anonymous {
if self
.core
@@ -91,7 +102,7 @@ impl JMAP {
Ok(())
}
- pub fn is_upload_allowed(&self, access_token: &AccessToken) -> trc::Result<InFlight> {
+ fn is_upload_allowed(&self, access_token: &AccessToken) -> trc::Result<InFlight> {
if let Some(in_flight_request) = self
.get_concurrency_limiter(access_token.primary_id())
.concurrent_uploads
@@ -105,7 +116,7 @@ impl JMAP {
}
}
- pub async fn is_auth_allowed_soft(&self, addr: &IpAddr) -> trc::Result<()> {
+ async fn is_auth_allowed_soft(&self, addr: &IpAddr) -> trc::Result<()> {
if let Some(rate) = &self.core.jmap.rate_authenticate_req {
if self
.core
@@ -122,7 +133,7 @@ impl JMAP {
Ok(())
}
- pub async fn is_auth_allowed_hard(&self, addr: &IpAddr) -> trc::Result<()> {
+ async fn is_auth_allowed_hard(&self, addr: &IpAddr) -> trc::Result<()> {
if let Some(rate) = &self.core.jmap.rate_authenticate_req {
if self
.core
@@ -139,9 +150,3 @@ impl JMAP {
Ok(())
}
}
-
-impl ConcurrencyLimiters {
- pub fn is_active(&self) -> bool {
- self.concurrent_requests.is_active() || self.concurrent_uploads.is_active()
- }
-}
diff --git a/crates/jmap/src/blob/copy.rs b/crates/jmap/src/blob/copy.rs
index 635b6bc0..19aa6df0 100644
--- a/crates/jmap/src/blob/copy.rs
+++ b/crates/jmap/src/blob/copy.rs
@@ -4,23 +4,34 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
-use common::auth::AccessToken;
+use common::{auth::AccessToken, Server};
use jmap_proto::{
error::set::{SetError, SetErrorType},
method::copy::{CopyBlobRequest, CopyBlobResponse},
types::blob::BlobId,
};
+use std::future::Future;
use store::{
write::{now, BatchBuilder, BlobOp},
BlobClass, Serialize,
};
use utils::map::vec_map::VecMap;
-use crate::JMAP;
+use crate::JmapMethods;
-impl JMAP {
- pub async fn blob_copy(
+use super::download::BlobDownload;
+
+pub trait BlobCopy: Sync + Send {
+ fn blob_copy(
+ &self,
+ request: CopyBlobRequest,
+ access_token: &AccessToken,
+ ) -> impl Future<Output = trc::Result<CopyBlobResponse>> + Send;
+}
+
+impl BlobCopy for Server {
+ async fn blob_copy(
&self,
request: CopyBlobRequest,
access_token: &AccessToken,
diff --git a/crates/jmap/src/blob/download.rs b/crates/jmap/src/blob/download.rs
index 8b289e59..19534b07 100644
--- a/crates/jmap/src/blob/download.rs
+++ b/crates/jmap/src/blob/download.rs
@@ -6,7 +6,7 @@
use std::ops::Range;
-use common::auth::AccessToken;
+use common::{auth::AccessToken, Server};
use jmap_proto::types::{
acl::Acl,
blob::{BlobId, BlobSection},
@@ -16,15 +16,42 @@ use mail_parser::{
decoders::{base64::base64_decode, quoted_printable::quoted_printable_decode},
Encoding,
};
+use std::future::Future;
use store::BlobClass;
use trc::AddContext;
use utils::BlobHash;
-use crate::JMAP;
+use crate::auth::acl::AclMethods;
-impl JMAP {
+pub trait BlobDownload: Sync + Send {
+ fn blob_download(
+ &self,
+ blob_id: &BlobId,
+ access_token: &AccessToken,
+ ) -> impl Future<Output = trc::Result<Option<Vec<u8>>>> + Send;
+
+ fn get_blob_section(
+ &self,
+ hash: &BlobHash,
+ section: &BlobSection,
+ ) -> impl Future<Output = trc::Result<Option<Vec<u8>>>> + Send;
+
+ fn get_blob(
+ &self,
+ hash: &BlobHash,
+ range: Range<usize>,
+ ) -> impl Future<Output = trc::Result<Option<Vec<u8>>>> + Send;
+
+ fn has_access_blob(
+ &self,
+ blob_id: &BlobId,
+ access_token: &AccessToken,
+ ) -> impl Future<Output = trc::Result<bool>> + Send;
+}
+
+impl BlobDownload for Server {
#[allow(clippy::blocks_in_conditions)]
- pub async fn blob_download(
+ async fn blob_download(
&self,
blob_id: &BlobId,
access_token: &AccessToken,
@@ -84,7 +111,7 @@ impl JMAP {
}
}
- pub async fn get_blob_section(
+ async fn get_blob_section(
&self,
hash: &BlobHash,
section: &BlobSection,
@@ -102,11 +129,7 @@ impl JMAP {
}))
}
- pub async fn get_blob(
- &self,
- hash: &BlobHash,
- range: Range<usize>,
- ) -> trc::Result<Option<Vec<u8>>> {
+ async fn get_blob(&self, hash: &BlobHash, range: Range<usize>) -> trc::Result<Option<Vec<u8>>> {
self.core
.storage
.blob
@@ -115,7 +138,7 @@ impl JMAP {
.caused_by(trc::location!())
}
- pub async fn has_access_blob(
+ async fn has_access_blob(
&self,
blob_id: &BlobId,
access_token: &AccessToken,
diff --git a/crates/jmap/src/blob/get.rs b/crates/jmap/src/blob/get.rs
index 3add516d..b73e05c5 100644
--- a/crates/jmap/src/blob/get.rs
+++ b/crates/jmap/src/blob/get.rs
@@ -4,7 +4,7 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
-use common::auth::AccessToken;
+use common::{auth::AccessToken, Server};
use jmap_proto::{
method::{
get::{GetRequest, GetResponse},
@@ -26,10 +26,26 @@ use sha2::{Sha256, Sha512};
use store::BlobClass;
use utils::map::vec_map::VecMap;
-use crate::{mailbox::UidMailbox, JMAP};
+use crate::{mailbox::UidMailbox, JmapMethods};
+use std::future::Future;
-impl JMAP {
- pub async fn blob_get(
+use super::download::BlobDownload;
+
+pub trait BlobOperations: Sync + Send {
+ fn blob_get(
+ &self,
+ request: GetRequest<GetArguments>,
+ access_token: &AccessToken,
+ ) -> impl Future<Output = trc::Result<GetResponse>> + Send;
+
+ fn blob_lookup(
+ &self,
+ request: BlobLookupRequest,
+ ) -> impl Future<Output = trc::Result<BlobLookupResponse>> + Send;
+}
+
+impl BlobOperations for Server {
+ async fn blob_get(
&self,
mut request: GetRequest<GetArguments>,
access_token: &AccessToken,
@@ -148,7 +164,7 @@ impl JMAP {
Ok(response)
}
- pub async fn blob_lookup(&self, request: BlobLookupRequest) -> trc::Result<BlobLookupResponse> {
+ async fn blob_lookup(&self, request: BlobLookupRequest) -> trc::Result<BlobLookupResponse> {
let mut include_email = false;
let mut include_mailbox = false;
let mut include_thread = false;
diff --git a/crates/jmap/src/blob/upload.rs b/crates/jmap/src/blob/upload.rs
index dfa902dc..49255196 100644
--- a/crates/jmap/src/blob/upload.rs
+++ b/crates/jmap/src/blob/upload.rs
@@ -6,7 +6,7 @@
use std::sync::Arc;
-use common::auth::AccessToken;
+use common::{auth::AccessToken, Server};
use directory::Permission;
use jmap_proto::{
error::set::SetError,
@@ -23,16 +23,40 @@ use store::{
use trc::AddContext;
use utils::BlobHash;
-use crate::JMAP;
+use crate::{auth::rate_limit::RateLimiter, JmapMethods};
-use super::UploadResponse;
+use super::{download::BlobDownload, UploadResponse};
+use std::future::Future;
#[cfg(feature = "test_mode")]
pub static DISABLE_UPLOAD_QUOTA: std::sync::atomic::AtomicBool =
std::sync::atomic::AtomicBool::new(true);
-impl JMAP {
- pub async fn blob_upload_many(
+pub trait BlobUpload: Sync + Send {
+ fn blob_upload_many(
+ &self,
+ request: BlobUploadRequest,
+ access_token: &AccessToken,
+ ) -> impl Future<Output = trc::Result<BlobUploadResponse>> + Send;
+
+ fn blob_upload(
+ &self,
+ account_id: Id,
+ content_type: &str,
+ data: &[u8],
+ access_token: Arc<AccessToken>,
+ ) -> impl Future<Output = trc::Result<UploadResponse>> + Send;
+
+ fn put_blob(
+ &self,
+ account_id: u32,
+ data: &[u8],
+ set_quota: bool,
+ ) -> impl Future<Output = trc::Result<BlobId>> + Send;
+}
+
+impl BlobUpload for Server {
+ async fn blob_upload_many(
&self,
request: BlobUploadRequest,
access_token: &AccessToken,
@@ -178,7 +202,7 @@ impl JMAP {
Ok(response)
}
- pub async fn blob_upload(
+ async fn blob_upload(
&self,
account_id: Id,
content_type: &str,
@@ -239,12 +263,7 @@ impl JMAP {
}
#[allow(clippy::blocks_in_conditions)]
- pub async fn put_blob(
- &self,
- account_id: u32,
- data: &[u8],
- set_quota: bool,
- ) -> trc::Result<BlobId> {
+ async fn put_blob(&self, account_id: u32, data: &[u8], set_quota: bool) -> trc::Result<BlobId> {
// First reserve the hash
let hash = BlobHash::from(data);
let mut batch = BatchBuilder::new();
diff --git a/crates/jmap/src/changes/get.rs b/crates/jmap/src/changes/get.rs
index cd68d95a..3da6ba15 100644
--- a/crates/jmap/src/changes/get.rs
+++ b/crates/jmap/src/changes/get.rs
@@ -4,18 +4,32 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
-use common::auth::AccessToken;
+use common::{auth::AccessToken, Server};
use jmap_proto::{
method::changes::{ChangesRequest, ChangesResponse, RequestArguments},
types::{collection::Collection, property::Property, state::State},
};
+use std::future::Future;
use store::query::log::{Change, Changes, Query};
use trc::AddContext;
-use crate::JMAP;
+pub trait ChangesLookup: Sync + Send {
+ fn changes(
+ &self,
+ request: ChangesRequest,
+ access_token: &AccessToken,
+ ) -> impl Future<Output = trc::Result<ChangesResponse>> + Send;
+
+ fn changes_(
+ &self,
+ account_id: u32,
+ collection: Collection,
+ query: Query,
+ ) -> impl Future<Output = trc::Result<Changes>> + Send;
+}
-impl JMAP {
- pub async fn changes(
+impl ChangesLookup for Server {
+ async fn changes(
&self,
request: ChangesRequest,
access_token: &AccessToken,
@@ -160,7 +174,7 @@ impl JMAP {
Ok(response)
}
- pub async fn changes_(
+ async fn changes_(
&self,
account_id: u32,
collection: Collection,
diff --git a/crates/jmap/src/changes/query.rs b/crates/jmap/src/changes/query.rs
index a6d31831..bfcb1517 100644
--- a/crates/jmap/src/changes/query.rs
+++ b/crates/jmap/src/changes/query.rs
@@ -4,17 +4,31 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
-use common::auth::AccessToken;
+use common::{auth::AccessToken, Server};
use jmap_proto::method::{
changes::{self, ChangesRequest},
query::{self, QueryRequest},
query_changes::{AddedItem, QueryChangesRequest, QueryChangesResponse},
};
+use std::future::Future;
-use crate::JMAP;
+use crate::{
+ email::query::EmailQuery, mailbox::query::MailboxQuery, quota::query::QuotaQuery,
+ submission::query::EmailSubmissionQuery,
+};
+
+use super::get::ChangesLookup;
+
+pub trait QueryChanges: Sync + Send {
+ fn query_changes(
+ &self,
+ request: QueryChangesRequest,
+ access_token: &AccessToken,
+ ) -> impl Future<Output = trc::Result<QueryChangesResponse>> + Send;
+}
-impl JMAP {
- pub async fn query_changes(
+impl QueryChanges for Server {
+ async fn query_changes(
&self,
request: QueryChangesRequest,
access_token: &AccessToken,
diff --git a/crates/jmap/src/changes/state.rs b/crates/jmap/src/changes/state.rs
index 96f75c0d..266ef2bc 100644
--- a/crates/jmap/src/changes/state.rs
+++ b/crates/jmap/src/changes/state.rs
@@ -4,16 +4,31 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
+use common::Server;
use jmap_proto::types::{collection::Collection, state::State};
+use std::future::Future;
use trc::AddContext;
-use crate::JMAP;
+pub trait StateManager: Sync + Send {
+ fn get_state(
+ &self,
+ account_id: u32,
+ collection: impl Into<u8> + Send,
+ ) -> impl Future<Output = trc::Result<State>> + Send;
+
+ fn assert_state(
+ &self,
+ account_id: u32,
+ collection: Collection,
+ if_in_state: &Option<State>,
+ ) -> impl Future<Output = trc::Result<State>> + Send;
+}
-impl JMAP {
- pub async fn get_state(
+impl StateManager for Server {
+ async fn get_state(
&self,
account_id: u32,
- collection: impl Into<u8>,
+ collection: impl Into<u8> + Send,
) -> trc::Result<State> {
let collection = collection.into();
self.core
@@ -25,7 +40,7 @@ impl JMAP {
.map(State::from)
}
- pub async fn assert_state(
+ async fn assert_state(
&self,
account_id: u32,
collection: Collection,
diff --git a/crates/jmap/src/changes/write.rs b/crates/jmap/src/changes/write.rs
index a765a480..7074cc21 100644
--- a/crates/jmap/src/changes/write.rs
+++ b/crates/jmap/src/changes/write.rs
@@ -6,28 +6,47 @@
use std::time::Duration;
+use common::Server;
use jmap_proto::types::collection::Collection;
+use std::future::Future;
use store::{
write::{log::ChangeLogBuilder, BatchBuilder},
LogKey,
};
use trc::AddContext;
-use crate::JMAP;
+pub trait ChangeLog: Sync + Send {
+ fn begin_changes(
+ &self,
+ account_id: u32,
+ ) -> impl Future<Output = trc::Result<ChangeLogBuilder>> + Send;
+ fn assign_change_id(&self, account_id: u32) -> impl Future<Output = trc::Result<u64>> + Send;
+ fn generate_snowflake_id(&self) -> trc::Result<u64>;
+ fn commit_changes(
+ &self,
+ account_id: u32,
+ changes: ChangeLogBuilder,
+ ) -> impl Future<Output = trc::Result<u64>> + Send;
+ fn delete_changes(
+ &self,
+ account_id: u32,
+ before: Duration,
+ ) -> impl Future<Output = trc::Result<()>> + Send;
+}
-impl JMAP {
- pub async fn begin_changes(&self, account_id: u32) -> trc::Result<ChangeLogBuilder> {
+impl ChangeLog for Server {
+ async fn begin_changes(&self, account_id: u32) -> trc::Result<ChangeLogBuilder> {
self.assign_change_id(account_id)
.await
.map(ChangeLogBuilder::with_change_id)
}
- pub async fn assign_change_id(&self, _: u32) -> trc::Result<u64> {
+ async fn assign_change_id(&self, _: u32) -> trc::Result<u64> {
self.generate_snowflake_id()
}
- pub fn generate_snowflake_id(&self) -> trc::Result<u64> {
- self.inner.snowflake_id.generate().ok_or_else(|| {
+ fn generate_snowflake_id(&self) -> trc::Result<u64> {
+ self.inner.data.jmap_id_gen.generate().ok_or_else(|| {
trc::StoreEvent::UnexpectedError
.into_err()
.caused_by(trc::location!())
@@ -35,7 +54,7 @@ impl JMAP {
})
}
- pub async fn commit_changes(
+ async fn commit_changes(
&self,
account_id: u32,
mut changes: ChangeLogBuilder,
@@ -56,8 +75,8 @@ impl JMAP {
.map(|_| state)
}
- pub async fn delete_changes(&self, account_id: u32, before: Duration) -> trc::Result<()> {
- let reference_cid = self.inner.snowflake_id.past_id(before).ok_or_else(|| {
+ async fn delete_changes(&self, account_id: u32, before: Duration) -> trc::Result<()> {
+ let reference_cid = self.inner.data.jmap_id_gen.past_id(before).ok_or_else(|| {
trc::StoreEvent::UnexpectedError
.caused_by(trc::location!())
.ctx(trc::Key::Reason, "Failed to generate reference change id.")
diff --git a/crates/jmap/src/email/cache.rs b/crates/jmap/src/email/cache.rs
index 2d73c6a8..830ee312 100644
--- a/crates/jmap/src/email/cache.rs
+++ b/crates/jmap/src/email/cache.rs
@@ -4,25 +4,29 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
-use std::{collections::HashMap, sync::Arc};
+use std::sync::Arc;
+use common::{Server, Threads};
use jmap_proto::types::{collection::Collection, property::Property};
+use std::future::Future;
use trc::AddContext;
use utils::lru_cache::LruCached;
-use crate::JMAP;
+use crate::JmapMethods;
-#[derive(Debug, Default)]
-pub struct Threads {
- pub threads: HashMap<u32, u32>,
- pub modseq: Option<u64>,
+pub trait ThreadCache: Sync + Send {
+ fn get_cached_thread_ids(
+ &self,
+ account_id: u32,
+ message_ids: impl Iterator<Item = u32> + Send,
+ ) -> impl Future<Output = trc::Result<Vec<(u32, u32)>>> + Send;
}
-impl JMAP {
- pub async fn get_cached_thread_ids(
+impl ThreadCache for Server {
+ async fn get_cached_thread_ids(
&self,
account_id: u32,
- message_ids: impl Iterator<Item = u32>,
+ message_ids: impl Iterator<Item = u32> + Send,
) -> trc::Result<Vec<(u32, u32)>> {
// Obtain current state
let modseq = self
@@ -34,8 +38,12 @@ impl JMAP {
.caused_by(trc::location!())?;
// Lock the cache
- let thread_cache = if let Some(thread_cache) =
- self.inner.cache_threads.get(&account_id).and_then(|t| {
+ let thread_cache = if let Some(thread_cache) = self
+ .inner
+ .data
+ .threads_cache
+ .get(&account_id)
+ .and_then(|t| {
if t.modseq.unwrap_or(0) >= modseq.unwrap_or(0) {
Some(t)
} else {
@@ -58,7 +66,8 @@ impl JMAP {
modseq,
});
self.inner
- .cache_threads
+ .data
+ .threads_cache
.insert(account_id, thread_cache.clone());
thread_cache
};
diff --git a/crates/jmap/src/email/copy.rs b/crates/jmap/src/email/copy.rs
index 472df5d0..bc7aa75b 100644
--- a/crates/jmap/src/email/copy.rs
+++ b/crates/jmap/src/email/copy.rs
@@ -4,7 +4,10 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
-use common::auth::{AccessToken, ResourceToken};
+use common::{
+ auth::{AccessToken, ResourceToken},
+ Server,
+};
use jmap_proto::{
error::set::SetError,
method::{
@@ -42,16 +45,46 @@ use store::{
use trc::AddContext;
use utils::map::vec_map::VecMap;
-use crate::{api::http::HttpSessionData, mailbox::UidMailbox, JMAP};
+use crate::{
+ api::http::HttpSessionData,
+ auth::acl::AclMethods,
+ changes::{state::StateManager, write::ChangeLog},
+ mailbox::{set::MailboxSet, UidMailbox},
+ services::index::Indexer,
+ JmapMethods,
+};
+use std::future::Future;
use super::{
index::{EmailIndexBuilder, TrimTextValue, VisitValues, MAX_ID_LENGTH, MAX_SORT_FIELD_LENGTH},
- ingest::{IngestedEmail, LogEmailInsert},
+ ingest::{EmailIngest, IngestedEmail, LogEmailInsert},
metadata::MessageMetadata,
};
-impl JMAP {
- pub async fn email_copy(
+pub trait EmailCopy: Sync + Send {
+ fn email_copy(
+ &self,
+ request: CopyRequest<RequestArguments>,
+ access_token: &AccessToken,
+ next_call: &mut Option<Call<RequestMethod>>,
+ session: &HttpSessionData,
+ ) -> impl Future<Output = trc::Result<CopyResponse>> + Send;
+
+ #[allow(clippy::too_many_arguments)]
+ fn copy_message(
+ &self,
+ from_account_id: u32,
+ from_message_id: u32,
+ resource_token: &ResourceToken,
+ mailboxes: Vec<u32>,
+ keywords: Vec<Keyword>,
+ received_at: Option<UTCDate>,
+ session_id: u64,
+ ) -> impl Future<Output = trc::Result<Result<IngestedEmail, SetError>>> + Send;
+}
+
+impl EmailCopy for Server {
+ async fn email_copy(
&self,
request: CopyRequest<RequestArguments>,
access_token: &AccessToken,
@@ -271,7 +304,7 @@ impl JMAP {
}
#[allow(clippy::too_many_arguments)]
- pub async fn copy_message(
+ async fn copy_message(
&self,
from_account_id: u32,
from_message_id: u32,
@@ -435,7 +468,7 @@ impl JMAP {
let document_id = ids.last_document_id().caused_by(trc::location!())?;
// Request FTS index
- self.inner.request_fts_index();
+ self.request_fts_index();
// Update response
email.id = Id::from_parts(thread_id, document_id);
diff --git a/crates/jmap/src/email/crypto.rs b/crates/jmap/src/email/crypto.rs
index 815de86d..758177a5 100644
--- a/crates/jmap/src/email/crypto.rs
+++ b/crates/jmap/src/email/crypto.rs
@@ -4,14 +4,16 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
-use std::{borrow::Cow, collections::BTreeSet, fmt::Display, io::Cursor, sync::Arc};
+use std::{
+ borrow::Cow, collections::BTreeSet, fmt::Display, future::Future, io::Cursor, sync::Arc,
+};
use crate::{
api::{http::ToHttpResponse, HttpResponse, JsonResponse},
- JMAP,
+ JmapMethods,
};
use aes::cipher::{block_padding::Pkcs7, BlockEncryptMut, KeyIvInit};
-use common::auth::AccessToken;
+use common::{auth::AccessToken, Server};
use directory::backend::internal::manage;
use jmap_proto::types::{collection::Collection, property::Property};
use mail_builder::{encoders::base64::base64_encode_mime, mime::make_boundary};
@@ -628,11 +630,21 @@ impl ToBitmaps for &EncryptionParams {
}
}
-impl JMAP {
- pub async fn handle_crypto_get(
+pub trait CryptoHandler: Sync + Send {
+ fn handle_crypto_get(
&self,
access_token: Arc<AccessToken>,
- ) -> trc::Result<HttpResponse> {
+ ) -> impl Future<Output = trc::Result<HttpResponse>> + Send;
+
+ fn handle_crypto_post(
+ &self,
+ access_token: Arc<AccessToken>,
+ body: Option<Vec<u8>>,
+ ) -> impl Future<Output = trc::Result<HttpResponse>> + Send;
+}
+
+impl CryptoHandler for Server {
+ async fn handle_crypto_get(&self, access_token: Arc<AccessToken>) -> trc::Result<HttpResponse> {
let params = self
.get_property::<EncryptionParams>(
access_token.primary_id(),
@@ -664,7 +676,7 @@ impl JMAP {
.into_http_response())
}
- pub async fn handle_crypto_post(
+ async fn handle_crypto_post(
&self,
access_token: Arc<AccessToken>,
body: Option<Vec<u8>>,
diff --git a/crates/jmap/src/email/delete.rs b/crates/jmap/src/email/delete.rs
index 5268925e..5d587aa3 100644
--- a/crates/jmap/src/email/delete.rs
+++ b/crates/jmap/src/email/delete.rs
@@ -6,6 +6,7 @@
use std::time::Duration;
+use common::Server;
use jmap_proto::types::{
collection::Collection, id::Id, keyword::Keyword, property::Property, state::StateChange,
type_state::DataType,
@@ -23,15 +24,41 @@ use trc::{AddContext, StoreEvent};
use utils::codec::leb128::Leb128Reader;
use crate::{
+ changes::write::ChangeLog,
mailbox::{UidMailbox, JUNK_ID, TOMBSTONE_ID, TRASH_ID},
- JMAP,
+ services::state::StateManager,
+ JmapMethods,
};
use super::{index::EmailIndexBuilder, metadata::MessageMetadata};
use rand::prelude::SliceRandom;
+use std::future::Future;
-impl JMAP {
- pub async fn emails_tombstone(
+pub trait EmailDeletion: Sync + Send {
+ fn emails_tombstone(
+ &self,
+ account_id: u32,
+ document_ids: RoaringBitmap,
+ ) -> impl Future<Output = trc::Result<(ChangeLogBuilder, RoaringBitmap)>> + Send;
+
+ fn purge_accounts(&self) -> impl Future<Output = ()> + Send;
+
+ fn purge_account(&self, account_id: u32) -> impl Future<Output = ()> + Send;
+
+ fn emails_auto_expunge(
+ &self,
+ account_id: u32,
+ period: Duration,
+ ) -> impl Future<Output = trc::Result<()>> + Send;
+
+ fn emails_purge_tombstoned(
+ &self,
+ account_id: u32,
+ ) -> impl Future<Output = trc::Result<()>> + Send;
+}
+
+impl EmailDeletion for Server {
+ async fn emails_tombstone(
&self,
account_id: u32,
mut document_ids: RoaringBitmap,
@@ -208,7 +235,7 @@ impl JMAP {
Ok((changes, document_ids))
}
- pub async fn purge_accounts(&self) {
+ async fn purge_accounts(&self) {
if let Ok(Some(account_ids)) = self.get_document_ids(u32::MAX, Collection::Principal).await
{
let mut account_ids: Vec<u32> = account_ids.into_iter().collect();
@@ -222,7 +249,7 @@ impl JMAP {
}
}
- pub async fn purge_account(&self, account_id: u32) {
+ async fn purge_account(&self, account_id: u32) {
// Lock account
match self
.core
@@ -290,7 +317,7 @@ impl JMAP {
}
}
- pub async fn emails_auto_expunge(&self, account_id: u32, period: Duration) -> trc::Result<()> {
+ async fn emails_auto_expunge(&self, account_id: u32, period: Duration) -> trc::Result<()> {
let deletion_candidates = self
.get_tag(
account_id,
@@ -313,7 +340,7 @@ impl JMAP {
if deletion_candidates.is_empty() {
return Ok(());
}
- let reference_cid = self.inner.snowflake_id.past_id(period).ok_or_else(|| {
+ let reference_cid = self.inner.data.jmap_id_gen.past_id(period).ok_or_else(|| {
trc::StoreEvent::UnexpectedError
.into_err()
.caused_by(trc::location!())
@@ -364,7 +391,7 @@ impl JMAP {
Ok(())
}
- pub async fn emails_purge_tombstoned(&self, account_id: u32) -> trc::Result<()> {
+ async fn emails_purge_tombstoned(&self, account_id: u32) -> trc::Result<()> {
// Obtain tombstoned messages
let tombstoned_ids = self
.core
@@ -401,7 +428,6 @@ impl JMAP {
// Obtain tenant id
let tenant_id = self
- .core
.get_cached_access_token(account_id)
.await
.caused_by(trc::location!())?
diff --git a/crates/jmap/src/email/get.rs b/crates/jmap/src/email/get.rs
index cc891d37..921ecd5f 100644
--- a/crates/jmap/src/email/get.rs
+++ b/crates/jmap/src/email/get.rs
@@ -4,7 +4,7 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
-use common::auth::AccessToken;
+use common::{auth::AccessToken, Server};
use jmap_proto::{
method::get::{GetRequest, GetResponse},
object::{email::GetArguments, Object},
@@ -23,16 +23,29 @@ use mail_parser::HeaderName;
use store::{write::Bincode, BlobClass};
use trc::{AddContext, StoreEvent};
-use crate::{email::headers::HeaderToValue, mailbox::UidMailbox, JMAP};
+use crate::{
+ auth::acl::AclMethods, blob::download::BlobDownload, changes::state::StateManager,
+ email::headers::HeaderToValue, mailbox::UidMailbox, JmapMethods,
+};
+use std::future::Future;
use super::{
body::{ToBodyPart, TruncateBody},
+ cache::ThreadCache,
headers::IntoForm,
metadata::{MessageMetadata, MetadataPartType},
};
-impl JMAP {
- pub async fn email_get(
+pub trait EmailGet: Sync + Send {
+ fn email_get(
+ &self,
+ request: GetRequest<GetArguments>,
+ access_token: &AccessToken,
+ ) -> impl Future<Output = trc::Result<GetResponse>> + Send;
+}
+
+impl EmailGet for Server {
+ async fn email_get(
&self,
mut request: GetRequest<GetArguments>,
access_token: &AccessToken,
diff --git a/crates/jmap/src/email/import.rs b/crates/jmap/src/email/import.rs
index 1abe06cc..1d9b441d 100644
--- a/crates/jmap/src/email/import.rs
+++ b/crates/jmap/src/email/import.rs
@@ -4,7 +4,7 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
-use common::auth::AccessToken;
+use common::{auth::AccessToken, Server};
use jmap_proto::{
error::set::{SetError, SetErrorType},
method::import::{ImportEmailRequest, ImportEmailResponse},
@@ -20,12 +20,25 @@ use jmap_proto::{
use mail_parser::MessageParser;
use utils::map::vec_map::VecMap;
-use crate::{api::http::HttpSessionData, JMAP};
+use crate::{
+ api::http::HttpSessionData, auth::acl::AclMethods, blob::download::BlobDownload,
+ changes::state::StateManager, mailbox::set::MailboxSet, JmapMethods,
+};
+
+use super::ingest::{EmailIngest, IngestEmail, IngestSource};
+use std::future::Future;
-use super::ingest::{IngestEmail, IngestSource};
+pub trait EmailImport: Sync + Send {
+ fn email_import(
+ &self,
+ request: ImportEmailRequest,
+ access_token: &AccessToken,
+ session: &HttpSessionData,
+ ) -> impl Future<Output = trc::Result<ImportEmailResponse>> + Send;
+}
-impl JMAP {
- pub async fn email_import(
+impl EmailImport for Server {
+ async fn email_import(
&self,
request: ImportEmailRequest,
access_token: &AccessToken,
diff --git a/crates/jmap/src/email/ingest.rs b/crates/jmap/src/email/ingest.rs
index 66a4cced..3a608113 100644
--- a/crates/jmap/src/email/ingest.rs
+++ b/crates/jmap/src/email/ingest.rs
@@ -9,7 +9,7 @@ use std::{
time::{Duration, Instant},
};
-use common::auth::ResourceToken;
+use common::{auth::ResourceToken, Server};
use jmap_proto::{
object::Object,
types::{
@@ -22,6 +22,7 @@ use mail_parser::{
};
use rand::Rng;
+use std::future::Future;
use store::{
ahash::AHashSet,
query::Filter,
@@ -36,12 +37,16 @@ use trc::{AddContext, MessageIngestEvent};
use utils::map::vec_map::VecMap;
use crate::{
+ blob::upload::BlobUpload,
+ changes::write::ChangeLog,
email::index::{IndexMessage, VisitValues, MAX_ID_LENGTH},
mailbox::{UidMailbox, INBOX_ID, JUNK_ID},
- JMAP,
+ services::index::Indexer,
+ JmapMethods,
};
use super::{
+ cache::ThreadCache,
crypto::{EncryptMessage, EncryptMessageError, EncryptionParams},
index::{TrimTextValue, MAX_SORT_FIELD_LENGTH},
};
@@ -76,9 +81,27 @@ pub enum IngestSource {
const MAX_RETRIES: u32 = 10;
-impl JMAP {
+pub trait EmailIngest: Sync + Send {
+ fn email_ingest(
+ &self,
+ params: IngestEmail,
+ ) -> impl Future<Output = trc::Result<IngestedEmail>> + Send;
+ fn find_or_merge_thread(
+ &self,
+ account_id: u32,
+ thread_name: &str,
+ references: &[&str],
+ ) -> impl Future<Output = trc::Result<Option<u32>>> + Send;
+ fn assign_imap_uid(
+ &self,
+ account_id: u32,
+ mailbox_id: u32,
+ ) -> impl Future<Output = trc::Result<u32>> + Send;
+}
+
+impl EmailIngest for Server {
#[allow(clippy::blocks_in_conditions)]
- pub async fn email_ingest(&self, mut params: IngestEmail<'_>) -> trc::Result<IngestedEmail> {
+ async fn email_ingest(&self, mut params: IngestEmail<'_>) -> trc::Result<IngestedEmail> {
// Check quota
let start_time = Instant::now();
let account_id = params.resource.account_id;
@@ -338,7 +361,7 @@ impl JMAP {
let id = Id::from_parts(thread_id, document_id);
// Request FTS index
- self.inner.request_fts_index();
+ self.request_fts_index();
trc::event!(
MessageIngest(match params.source {
@@ -379,7 +402,7 @@ impl JMAP {
})
}
- pub async fn find_or_merge_thread(
+ async fn find_or_merge_thread(
&self,
account_id: u32,
thread_name: &str,
@@ -519,7 +542,7 @@ impl JMAP {
}
}
- pub async fn assign_imap_uid(&self, account_id: u32, mailbox_id: u32) -> trc::Result<u32> {
+ async fn assign_imap_uid(&self, account_id: u32, mailbox_id: u32) -> trc::Result<u32> {
// Increment UID next
let mut batch = BatchBuilder::new();
batch
diff --git a/crates/jmap/src/email/parse.rs b/crates/jmap/src/email/parse.rs
index 83ec8162..d0b33751 100644
--- a/crates/jmap/src/email/parse.rs
+++ b/crates/jmap/src/email/parse.rs
@@ -4,7 +4,7 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
-use common::auth::AccessToken;
+use common::{auth::AccessToken, Server};
use jmap_proto::{
method::parse::{ParseEmailRequest, ParseEmailResponse},
object::Object,
@@ -13,9 +13,10 @@ use jmap_proto::{
use mail_parser::{
decoders::html::html_to_text, parsers::preview::preview_text, MessageParser, PartType,
};
+use std::future::Future;
use utils::map::vec_map::VecMap;
-use crate::JMAP;
+use crate::blob::download::BlobDownload;
use super::{
body::{ToBodyPart, TruncateBody},
@@ -23,8 +24,16 @@ use super::{
index::PREVIEW_LENGTH,
};
-impl JMAP {
- pub async fn email_parse(
+pub trait EmailParse: Sync + Send {
+ fn email_parse(
+ &self,
+ request: ParseEmailRequest,
+ access_token: &AccessToken,
+ ) -> impl Future<Output = trc::Result<ParseEmailResponse>> + Send;
+}
+
+impl EmailParse for Server {
+ async fn email_parse(
&self,
request: ParseEmailRequest,
access_token: &AccessToken,
diff --git a/crates/jmap/src/email/query.rs b/crates/jmap/src/email/query.rs
index 56942279..1a56c33f 100644
--- a/crates/jmap/src/email/query.rs
+++ b/crates/jmap/src/email/query.rs
@@ -4,7 +4,7 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
-use common::auth::AccessToken;
+use common::{auth::AccessToken, Server};
use jmap_proto::{
method::query::{Comparator, Filter, QueryRequest, QueryResponse, SortProperty},
object::email::QueryArguments,
@@ -12,6 +12,7 @@ use jmap_proto::{
};
use mail_parser::HeaderName;
use nlp::language::Language;
+use std::future::Future;
use store::{
fts::{Field, FilterGroup, FtsFilter, IntoFilterGroup},
query::{self},
@@ -20,10 +21,27 @@ use store::{
ValueKey,
};
-use crate::JMAP;
+use crate::{auth::acl::AclMethods, JmapMethods};
-impl JMAP {
- pub async fn email_query(
+use super::cache::ThreadCache;
+
+pub trait EmailQuery: Sync + Send {
+ fn email_query(
+ &self,
+ request: QueryRequest<QueryArguments>,
+ access_token: &AccessToken,
+ ) -> impl Future<Output = trc::Result<QueryResponse>> + Send;
+
+ fn thread_keywords(
+ &self,
+ account_id: u32,
+ keyword: Keyword,
+ match_all: bool,
+ ) -> impl Future<Output = trc::Result<RoaringBitmap>> + Send;
+}
+
+impl EmailQuery for Server {
+ async fn email_query(
&self,
mut request: QueryRequest<QueryArguments>,
access_token: &AccessToken,
diff --git a/crates/jmap/src/email/set.rs b/crates/jmap/src/email/set.rs
index 62a7562f..a1a0f719 100644
--- a/crates/jmap/src/email/set.rs
+++ b/crates/jmap/src/email/set.rs
@@ -6,7 +6,7 @@
use std::{borrow::Cow, collections::HashMap, slice::IterMut};
-use common::auth::AccessToken;
+use common::{auth::AccessToken, Server};
use jmap_proto::{
error::set::{SetError, SetErrorType},
method::set::{RequestArguments, SetRequest, SetResponse},
@@ -41,15 +41,33 @@ use store::{
};
use trc::AddContext;
-use crate::{api::http::HttpSessionData, mailbox::UidMailbox, JMAP};
+use crate::{
+ api::http::HttpSessionData,
+ auth::acl::AclMethods,
+ blob::download::BlobDownload,
+ changes::{state::StateManager, write::ChangeLog},
+ mailbox::{set::MailboxSet, UidMailbox},
+ JmapMethods,
+};
+use std::future::Future;
use super::{
+ delete::EmailDeletion,
headers::{BuildHeader, ValueToHeader},
- ingest::{IngestEmail, IngestSource},
+ ingest::{EmailIngest, IngestEmail, IngestSource},
};
-impl JMAP {
- pub async fn email_set(
+pub trait EmailSet: Sync + Send {
+ fn email_set(
+ &self,
+ request: SetRequest<RequestArguments>,
+ access_token: &AccessToken,
+ session: &HttpSessionData,
+ ) -> impl Future<Output = trc::Result<SetResponse>> + Send;
+}
+
+impl EmailSet for Server {
+ async fn email_set(
&self,
mut request: SetRequest<RequestArguments>,
access_token: &AccessToken,
diff --git a/crates/jmap/src/email/snippet.rs b/crates/jmap/src/email/snippet.rs
index dbb6e67c..bace9f17 100644
--- a/crates/jmap/src/email/snippet.rs
+++ b/crates/jmap/src/email/snippet.rs
@@ -4,7 +4,7 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
-use common::auth::AccessToken;
+use common::{auth::AccessToken, Server};
use jmap_proto::{
method::{
query::Filter,
@@ -16,12 +16,21 @@ use mail_parser::{decoders::html::html_to_text, GetHeader, HeaderName, PartType}
use nlp::language::{search_snippet::generate_snippet, stemmer::Stemmer, Language};
use store::{backend::MAX_TOKEN_LENGTH, write::Bincode};
-use crate::JMAP;
+use crate::{auth::acl::AclMethods, blob::download::BlobDownload, JmapMethods};
use super::metadata::{MessageMetadata, MetadataPartType};
+use std::future::Future;
-impl JMAP {
- pub async fn email_search_snippet(
+pub trait EmailSearchSnippet: Sync + Send {
+ fn email_search_snippet(
+ &self,
+ request: GetSearchSnippetRequest,
+ access_token: &AccessToken,
+ ) -> impl Future<Output = trc::Result<GetSearchSnippetResponse>> + Send;
+}
+
+impl EmailSearchSnippet for Server {
+ async fn email_search_snippet(
&self,
request: GetSearchSnippetRequest,
access_token: &AccessToken,
diff --git a/crates/jmap/src/identity/get.rs b/crates/jmap/src/identity/get.rs
index f02d8e24..6b934472 100644
--- a/crates/jmap/src/identity/get.rs
+++ b/crates/jmap/src/identity/get.rs
@@ -4,6 +4,7 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
+use common::Server;
use directory::{backend::internal::PrincipalField, QueryBy};
use jmap_proto::{
method::get::{GetRequest, GetResponse, RequestArguments},
@@ -16,12 +17,25 @@ use store::{
};
use trc::AddContext;
-use crate::JMAP;
+use crate::{changes::state::StateManager, JmapMethods};
use super::set::sanitize_email;
+use std::future::Future;
-impl JMAP {
- pub async fn identity_get(
+pub trait IdentityGet: Sync + Send {
+ fn identity_get(
+ &self,
+ request: GetRequest<RequestArguments>,
+ ) -> impl Future<Output = trc::Result<GetResponse>> + Send;
+
+ fn identity_get_or_create(
+ &self,
+ account_id: u32,
+ ) -> impl Future<Output = trc::Result<RoaringBitmap>> + Send;
+}
+
+impl IdentityGet for Server {
+ async fn identity_get(
&self,
mut request: GetRequest<RequestArguments>,
) -> trc::Result<GetResponse> {
@@ -107,7 +121,7 @@ impl JMAP {
Ok(response)
}
- pub async fn identity_get_or_create(&self, account_id: u32) -> trc::Result<RoaringBitmap> {
+ async fn identity_get_or_create(&self, account_id: u32) -> trc::Result<RoaringBitmap> {
let mut identity_ids = self
.get_document_ids(account_id, Collection::Identity)
.await?
diff --git a/crates/jmap/src/identity/set.rs b/crates/jmap/src/identity/set.rs
index 3ab94e51..9e82dcb8 100644
--- a/crates/jmap/src/identity/set.rs
+++ b/crates/jmap/src/identity/set.rs
@@ -4,6 +4,7 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
+use common::Server;
use directory::{backend::internal::PrincipalField, QueryBy};
use jmap_proto::{
error::set::SetError,
@@ -16,12 +17,20 @@ use jmap_proto::{
value::{MaybePatchValue, Value},
},
};
+use std::future::Future;
use store::write::{log::ChangeLogBuilder, BatchBuilder, F_CLEAR, F_VALUE};
-use crate::JMAP;
+use crate::{changes::write::ChangeLog, JmapMethods};
-impl JMAP {
- pub async fn identity_set(
+pub trait IdentitySet: Sync + Send {
+ fn identity_set(
+ &self,
+ request: SetRequest<RequestArguments>,
+ ) -> impl Future<Output = trc::Result<SetResponse>> + Send;
+}
+
+impl IdentitySet for Server {
+ async fn identity_set(
&self,
mut request: SetRequest<RequestArguments>,
) -> trc::Result<SetResponse> {
diff --git a/crates/jmap/src/lib.rs b/crates/jmap/src/lib.rs
index ad83a0bd..bd75a9fe 100644
--- a/crates/jmap/src/lib.rs
+++ b/crates/jmap/src/lib.rs
@@ -4,22 +4,15 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
-use std::{
- collections::hash_map::RandomState,
- fmt::Display,
- sync::{atomic::AtomicU8, Arc},
- time::Duration,
-};
+use std::{fmt::Display, future::Future, sync::Arc, time::Duration};
-use auth::rate_limit::ConcurrencyLimiters;
+use changes::state::StateManager;
use common::{
auth::{AccessToken, ResourceToken, TenantInfo},
- manager::webadmin::WebAdminManager,
- Core, DeliveryEvent, SharedCore,
+ manager::boot::{BootManager, IpcReceivers},
+ Inner, Server,
};
-use dashmap::DashMap;
use directory::QueryBy;
-use email::cache::Threads;
use jmap_proto::{
method::{
query::{QueryRequest, QueryResponse},
@@ -28,13 +21,10 @@ use jmap_proto::{
types::{collection::Collection, property::Property},
};
use services::{
- delivery::spawn_delivery_manager,
- housekeeper::{self, init_housekeeper, spawn_housekeeper},
- index::spawn_index_task,
- state::{self, init_state_manager, spawn_state_manager},
+ delivery::spawn_delivery_manager, housekeeper::spawn_housekeeper, index::spawn_index_task,
+ state::spawn_state_manager,
};
-use smtp::core::SMTP;
use store::{
dispatch::DocumentSet,
fts::FtsFilter,
@@ -46,14 +36,7 @@ use store::{
},
BitmapKey, Deserialize, IterateParams, ValueKey, U32_LEN,
};
-use tokio::sync::{mpsc, Notify};
use trc::AddContext;
-use utils::{
- config::Config,
- lru_cache::{LruCache, LruCached},
- map::ttl_dashmap::{TtlDashMap, TtlMap},
- snowflake::SnowflakeIdGenerator,
-};
pub mod api;
pub mod auth;
@@ -74,76 +57,24 @@ pub mod websocket;
pub const LONG_SLUMBER: Duration = Duration::from_secs(60 * 60 * 24);
-#[derive(Clone)]
-pub struct JMAP {
- pub core: Arc<Core>,
- pub shared_core: SharedCore,
- pub inner: Arc<Inner>,
- pub smtp: SMTP,
+pub trait StartServices: Sync + Send {
+ fn start_services(&mut self) -> impl Future<Output = ()> + Send;
}
-#[derive(Clone)]
-pub struct JmapInstance {
- pub core: SharedCore,
- pub jmap_inner: Arc<Inner>,
- pub smtp_inner: Arc<smtp::core::Inner>,
+pub trait SpawnServices {
+ fn spawn_services(&mut self, inner: Arc<Inner>);
}
-pub struct Inner {
- pub sessions: TtlDashMap<String, u32>,
- pub snowflake_id: SnowflakeIdGenerator,
- pub webadmin: WebAdminManager,
- pub config_version: AtomicU8,
-
- pub concurrency_limiter: DashMap<u32, Arc<ConcurrencyLimiters>>,
-
- pub state_tx: mpsc::Sender<state::Event>,
- pub housekeeper_tx: mpsc::Sender<housekeeper::Event>,
- pub index_tx: Arc<Notify>,
-
- pub cache_threads: LruCache<u32, Arc<Threads>>,
-}
-
-impl JMAP {
- pub async fn init(
- config: &mut Config,
- delivery_rx: mpsc::Receiver<DeliveryEvent>,
- 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 index_tx = Arc::new(Notify::new());
- let shard_amount = config
- .property::<u64>("cache.shard")
- .unwrap_or(32)
- .next_power_of_two() as usize;
- let capacity = config.property("cache.capacity").unwrap_or(100);
-
- let inner = Inner {
- webadmin: WebAdminManager::new(),
- sessions: 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,
- ),
- state_tx,
- housekeeper_tx,
- index_tx: index_tx.clone(),
- cache_threads: LruCache::with_capacity(
- config.property("cache.thread.size").unwrap_or(2048),
- ),
- config_version: 0.into(),
- };
-
+impl StartServices for BootManager {
+ async fn start_services(&mut self) {
// Unpack webadmin
- if let Err(err) = inner.webadmin.unpack(&core.load().storage.blob).await {
+ if let Err(err) = self
+ .inner
+ .data
+ .webadmin
+ .unpack(&self.inner.shared_core.load().storage.blob)
+ .await
+ {
trc::event!(
Resource(trc::ResourceEvent::Error),
Reason = err,
@@ -151,33 +82,33 @@ impl JMAP {
);
}
- let jmap_instance = JmapInstance {
- core,
- jmap_inner: Arc::new(inner),
- smtp_inner,
- };
+ self.ipc_rxs.spawn_services(self.inner.clone());
+ }
+}
+impl SpawnServices for IpcReceivers {
+ fn spawn_services(&mut self, inner: Arc<Inner>) {
// Spawn delivery manager
- spawn_delivery_manager(jmap_instance.clone(), delivery_rx);
+ spawn_delivery_manager(inner.clone(), self.delivery_rx.take().unwrap());
// Spawn state manager
- spawn_state_manager(jmap_instance.clone(), state_rx);
+ spawn_state_manager(inner.clone(), self.state_rx.take().unwrap());
// Spawn housekeeper
- spawn_housekeeper(jmap_instance.clone(), housekeeper_rx);
+ spawn_housekeeper(inner.clone(), self.housekeeper_rx.take().unwrap());
// Spawn index task
- spawn_index_task(jmap_instance.clone(), index_tx);
-
- jmap_instance
+ spawn_index_task(inner);
}
+}
- pub async fn get_property<U>(
+impl JmapMethods for Server {
+ async fn get_property<U>(
&self,
account_id: u32,
collection: Collection,
document_id: u32,
- property: impl AsRef<Property>,
+ property: impl AsRef<Property> + Sync + Send,
) -> trc::Result<Option<U>>
where
U: Deserialize + 'static,
@@ -203,7 +134,7 @@ impl JMAP {
})
}
- pub async fn get_properties<U, I, P>(
+ async fn get_properties<U, I, P>(
&self,
account_id: u32,
collection: Collection,
@@ -212,7 +143,7 @@ impl JMAP {
) -> trc::Result<Vec<(u32, U)>>
where
I: DocumentSet + Send + Sync,
- P: AsRef<Property>,
+ P: AsRef<Property> + Sync + Send,
U: Deserialize + 'static,
{
let property: u8 = property.as_ref().into();
@@ -258,7 +189,7 @@ impl JMAP {
.map(|_| results)
}
- pub async fn get_document_ids(
+ async fn get_document_ids(
&self,
account_id: u32,
collection: Collection,
@@ -275,12 +206,12 @@ impl JMAP {
})
}
- pub async fn get_tag(
+ async fn get_tag(
&self,
account_id: u32,
collection: Collection,
- property: impl AsRef<Property>,
- value: impl Into<TagValue<u32>>,
+ property: impl AsRef<Property> + Sync + Send,
+ value: impl Into<TagValue<u32>> + Sync + Send,
) -> trc::Result<Option<RoaringBitmap>> {
let property = property.as_ref();
self.core
@@ -304,7 +235,7 @@ impl JMAP {
})
}
- pub async fn prepare_set_response<T>(
+ async fn prepare_set_response<T: Sync + Send>(
&self,
request: &SetRequest<T>,
collection: Collection,
@@ -321,7 +252,7 @@ impl JMAP {
)
}
- pub async fn get_resource_token(
+ async fn get_resource_token(
&self,
access_token: &AccessToken,
account_id: u32,
@@ -380,7 +311,7 @@ impl JMAP {
})
}
- pub async fn get_used_quota(&self, account_id: u32) -> trc::Result<i64> {
+ async fn get_used_quota(&self, account_id: u32) -> trc::Result<i64> {
self.core
.storage
.data
@@ -389,11 +320,7 @@ impl JMAP {
.add_context(|err| err.caused_by(trc::location!()).account_id(account_id))
}
- pub async fn has_available_quota(
- &self,
- quotas: &ResourceToken,
- item_size: u64,
- ) -> trc::Result<()> {
+ async fn has_available_quota(&self, quotas: &ResourceToken, item_size: u64) -> trc::Result<()> {
if quotas.quota != 0 {
let used_quota = self.get_used_quota(quotas.account_id).await? as u64;
@@ -428,7 +355,7 @@ impl JMAP {
Ok(())
}
- pub async fn filter(
+ async fn filter(
&self,
account_id: u32,
collection: Collection,
@@ -446,7 +373,7 @@ impl JMAP {
})
}
- pub async fn fts_filter<T: Into<u8> + Display + Clone + std::fmt::Debug>(
+ async fn fts_filter<T: Into<u8> + Display + Clone + std::fmt::Debug + Sync + Send>(
&self,
account_id: u32,
collection: Collection,
@@ -464,7 +391,7 @@ impl JMAP {
})
}
- pub async fn build_query_response<T>(
+ async fn build_query_response<T: Sync + Send>(
&self,
result_set: &ResultSet,
request: &QueryRequest<T>,
@@ -513,7 +440,7 @@ impl JMAP {
))
}
- pub async fn sort(
+ async fn sort(
&self,
result_set: ResultSet,
comparators: Vec<Comparator>,
@@ -539,7 +466,7 @@ impl JMAP {
Ok(response)
}
- pub async fn write_batch(&self, batch: BatchBuilder) -> trc::Result<AssignedIds> {
+ async fn write_batch(&self, batch: BatchBuilder) -> trc::Result<AssignedIds> {
self.core
.storage
.data
@@ -548,34 +475,116 @@ impl JMAP {
.caused_by(trc::location!())
}
- pub async fn write_batch_expect_id(&self, batch: BatchBuilder) -> trc::Result<u32> {
+ async fn write_batch_expect_id(&self, batch: BatchBuilder) -> trc::Result<u32> {
self.write_batch(batch)
.await
.and_then(|ids| ids.last_document_id().caused_by(trc::location!()))
}
-}
-impl Inner {
- pub fn increment_config_version(&self) {
- self.config_version
+ fn increment_config_version(&self) {
+ self.inner
+ .data
+ .config_version
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
}
}
-impl From<JmapInstance> for JMAP {
- fn from(value: JmapInstance) -> Self {
- let shared_core = value.core.clone();
- let core = value.core.load_full();
- JMAP {
- smtp: SMTP {
- core: core.clone(),
- inner: value.smtp_inner,
- },
- core,
- shared_core,
- inner: value.jmap_inner,
- }
- }
+pub trait JmapMethods: Sync + Send {
+ fn get_property<U>(
+ &self,
+ account_id: u32,
+ collection: Collection,
+ document_id: u32,
+ property: impl AsRef<Property> + Sync + Send,
+ ) -> impl Future<Output = trc::Result<Option<U>>> + Send
+ where
+ U: Deserialize + 'static;
+
+ fn get_properties<U, I, P>(
+ &self,
+ account_id: u32,
+ collection: Collection,
+ iterate: &I,
+ property: P,
+ ) -> impl Future<Output = trc::Result<Vec<(u32, U)>>> + Send
+ where
+ I: DocumentSet + Send + Sync,
+ P: AsRef<Property> + Sync + Send,
+ U: Deserialize + 'static;
+
+ fn get_document_ids(
+ &self,
+ account_id: u32,
+ collection: Collection,
+ ) -> impl Future<Output = trc::Result<Option<RoaringBitmap>>> + Send;
+
+ fn get_tag(
+ &self,
+ account_id: u32,
+ collection: Collection,
+ property: impl AsRef<Property> + Sync + Send,
+ value: impl Into<TagValue<u32>> + Sync + Send,
+ ) -> impl Future<Output = trc::Result<Option<RoaringBitmap>>> + Send;
+
+ fn prepare_set_response<T: Sync + Send>(
+ &self,
+ request: &SetRequest<T>,
+ collection: Collection,
+ ) -> impl Future<Output = trc::Result<SetResponse>> + Send;
+
+ fn get_resource_token(
+ &self,
+ access_token: &AccessToken,
+ account_id: u32,
+ ) -> impl Future<Output = trc::Result<ResourceToken>> + Send;
+
+ fn get_used_quota(&self, account_id: u32) -> impl Future<Output = trc::Result<i64>> + Send;
+
+ fn has_available_quota(
+ &self,
+ quotas: &ResourceToken,
+ item_size: u64,
+ ) -> impl Future<Output = trc::Result<()>> + Send;
+
+ fn filter(
+ &self,
+ account_id: u32,
+ collection: Collection,
+ filters: Vec<Filter>,
+ ) -> impl Future<Output = trc::Result<ResultSet>> + Send;
+
+ fn fts_filter<T: Into<u8> + Display + Clone + std::fmt::Debug + Sync + Send>(
+ &self,
+ account_id: u32,
+ collection: Collection,
+ filters: Vec<FtsFilter<T>>,
+ ) -> impl Future<Output = trc::Result<RoaringBitmap>> + Send;
+
+ fn build_query_response<T: Sync + Send>(
+ &self,
+ result_set: &ResultSet,
+ request: &QueryRequest<T>,
+ ) -> impl Future<Output = trc::Result<(QueryResponse, Option<Pagination>)>> + Send;
+
+ fn sort(
+ &self,
+ result_set: ResultSet,
+ comparators: Vec<Comparator>,
+ paginate: Pagination,
+ response: QueryResponse,
+ ) -> impl Future<Output = trc::Result<QueryResponse>> + Send;
+
+ fn write_batch(
+ &self,
+ batch: BatchBuilder,
+ ) -> impl Future<Output = trc::Result<AssignedIds>> + Send;
+
+ fn write_batch_expect_id(
+ &self,
+ batch: BatchBuilder,
+ ) -> impl Future<Output = trc::Result<u32>> + Send;
+
+ fn increment_config_version(&self);
}
trait UpdateResults: Sized {
diff --git a/crates/jmap/src/mailbox/get.rs b/crates/jmap/src/mailbox/get.rs
index 069b678d..94f93304 100644
--- a/crates/jmap/src/mailbox/get.rs
+++ b/crates/jmap/src/mailbox/get.rs
@@ -4,7 +4,7 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
-use common::auth::AccessToken;
+use common::{auth::AccessToken, Server};
use jmap_proto::{
method::get::{GetRequest, GetResponse, RequestArguments},
object::Object,
@@ -13,12 +13,58 @@ use jmap_proto::{
use store::{ahash::AHashSet, query::Filter, roaring::RoaringBitmap};
use trc::AddContext;
-use crate::{auth::acl::EffectiveAcl, JMAP};
+use crate::{
+ auth::acl::{AclMethods, EffectiveAcl},
+ changes::state::StateManager,
+ email::cache::ThreadCache,
+ JmapMethods,
+};
-use super::INBOX_ID;
+use super::{set::MailboxSet, INBOX_ID};
+use std::future::Future;
-impl JMAP {
- pub async fn mailbox_get(
+pub trait MailboxGet: Sync + Send {
+ fn mailbox_get(
+ &self,
+ request: GetRequest<RequestArguments>,
+ access_token: &AccessToken,
+ ) -> impl Future<Output = trc::Result<GetResponse>> + Send;
+
+ fn mailbox_count_threads(
+ &self,
+ account_id: u32,
+ document_ids: Option<RoaringBitmap>,
+ ) -> impl Future<Output = trc::Result<usize>> + Send;
+
+ fn mailbox_unread_tags(
+ &self,
+ account_id: u32,
+ document_id: u32,
+ message_ids: &Option<RoaringBitmap>,
+ ) -> impl Future<Output = trc::Result<Option<RoaringBitmap>>> + Send;
+
+ fn mailbox_expand_path<'x>(
+ &self,
+ account_id: u32,
+ path: &'x str,
+ exact_match: bool,
+ ) -> impl Future<Output = trc::Result<Option<ExpandPath<'x>>>> + Send;
+
+ fn mailbox_get_by_name(
+ &self,
+ account_id: u32,
+ path: &str,
+ ) -> impl Future<Output = trc::Result<Option<u32>>> + Send;
+
+ fn mailbox_get_by_role(
+ &self,
+ account_id: u32,
+ role: &str,
+ ) -> impl Future<Output = trc::Result<Option<u32>>> + Send;
+}
+
+impl MailboxGet for Server {
+ async fn mailbox_get(
&self,
mut request: GetRequest<RequestArguments>,
access_token: &AccessToken,
@@ -257,7 +303,7 @@ impl JMAP {
}
}
- pub async fn mailbox_unread_tags(
+ async fn mailbox_unread_tags(
&self,
account_id: u32,
document_id: u32,
@@ -297,7 +343,7 @@ impl JMAP {
}
}
- pub async fn mailbox_expand_path<'x>(
+ async fn mailbox_expand_path<'x>(
&self,
account_id: u32,
path: &'x str,
@@ -376,11 +422,7 @@ impl JMAP {
Ok(Some(ExpandPath { path, found_names }))
}
- pub async fn mailbox_get_by_name(
- &self,
- account_id: u32,
- path: &str,
- ) -> trc::Result<Option<u32>> {
+ async fn mailbox_get_by_name(&self, account_id: u32, path: &str) -> trc::Result<Option<u32>> {
Ok(self
.mailbox_expand_path(account_id, path, true)
.await?
@@ -403,11 +445,7 @@ impl JMAP {
}))
}
- pub async fn mailbox_get_by_role(
- &self,
- account_id: u32,
- role: &str,
- ) -> trc::Result<Option<u32>> {
+ async fn mailbox_get_by_role(&self, account_id: u32, role: &str) -> trc::Result<Option<u32>> {
self.filter(
account_id,
Collection::Mailbox,
diff --git a/crates/jmap/src/mailbox/query.rs b/crates/jmap/src/mailbox/query.rs
index 16295772..33263db9 100644
--- a/crates/jmap/src/mailbox/query.rs
+++ b/crates/jmap/src/mailbox/query.rs
@@ -4,7 +4,7 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
-use common::auth::AccessToken;
+use common::{auth::AccessToken, Server};
use jmap_proto::{
method::query::{Comparator, Filter, QueryRequest, QueryResponse, SortProperty},
object::{mailbox::QueryArguments, Object},
@@ -16,10 +16,21 @@ use store::{
roaring::RoaringBitmap,
};
-use crate::{UpdateResults, JMAP};
+use crate::{auth::acl::AclMethods, JmapMethods, UpdateResults};
+use std::future::Future;
-impl JMAP {
- pub async fn mailbox_query(
+use super::set::MailboxSet;
+
+pub trait MailboxQuery: Sync + Send {
+ fn mailbox_query(
+ &self,
+ request: QueryRequest<QueryArguments>,
+ access_token: &AccessToken,
+ ) -> impl Future<Output = trc::Result<QueryResponse>> + Send;
+}
+
+impl MailboxQuery for Server {
+ async fn mailbox_query(
&self,
mut request: QueryRequest<QueryArguments>,
access_token: &AccessToken,
diff --git a/crates/jmap/src/mailbox/set.rs b/crates/jmap/src/mailbox/set.rs
index 91ee0216..beaca13d 100644
--- a/crates/jmap/src/mailbox/set.rs
+++ b/crates/jmap/src/mailbox/set.rs
@@ -4,7 +4,7 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
-use common::{auth::AccessToken, config::jmap::settings::SpecialUse};
+use common::{auth::AccessToken, config::jmap::settings::SpecialUse, Server};
use directory::Permission;
use jmap_proto::{
error::set::{SetError, SetErrorType},
@@ -36,13 +36,19 @@ use store::{
};
use trc::AddContext;
-use crate::{auth::acl::EffectiveAcl, JMAP};
+use crate::{
+ auth::acl::{AclMethods, EffectiveAcl},
+ changes::write::ChangeLog,
+ email::delete::EmailDeletion,
+ JmapMethods,
+};
+use super::{get::MailboxGet, ARCHIVE_ID, DRAFTS_ID, SENT_ID};
#[allow(unused_imports)]
use super::{UidMailbox, INBOX_ID, JUNK_ID, TRASH_ID};
-use super::{ARCHIVE_ID, DRAFTS_ID, SENT_ID};
+use std::future::Future;
-struct SetContext<'x> {
+pub struct SetContext<'x> {
account_id: u32,
access_token: &'x AccessToken,
is_shared: bool,
@@ -69,9 +75,44 @@ pub static SCHEMA: &[IndexProperty] = &[
IndexProperty::new(Property::Acl).index_as(IndexAs::Acl),
];
-impl JMAP {
+pub trait MailboxSet: Sync + Send {
+ fn mailbox_set(
+ &self,
+ request: SetRequest<SetArguments>,
+ access_token: &AccessToken,
+ ) -> impl Future<Output = trc::Result<SetResponse>> + Send;
+
+ fn mailbox_destroy(
+ &self,
+ account_id: u32,
+ document_id: u32,
+ changes: &mut ChangeLogBuilder,
+ access_token: &AccessToken,
+ remove_emails: bool,
+ ) -> impl Future<Output = trc::Result<Result<bool, SetError>>> + Send;
+
+ fn mailbox_set_item(
+ &self,
+ changes_: Object<SetValue>,
+ update: Option<(u32, HashedValue<Object<Value>>)>,
+ ctx: &SetContext,
+ ) -> impl Future<Output = trc::Result<Result<ObjectIndexBuilder, SetError>>> + Send;
+
+ fn mailbox_get_or_create(
+ &self,
+ account_id: u32,
+ ) -> impl Future<Output = trc::Result<RoaringBitmap>> + Send;
+
+ fn mailbox_create_path(
+ &self,
+ account_id: u32,
+ path: &str,
+ ) -> impl Future<Output = trc::Result<Option<(u32, Option<u64>)>>> + Send;
+}
+
+impl MailboxSet for Server {
#[allow(clippy::blocks_in_conditions)]
- pub async fn mailbox_set(
+ async fn mailbox_set(
&self,
mut request: SetRequest<SetArguments>,
access_token: &AccessToken,
@@ -283,7 +324,7 @@ impl JMAP {
Ok(ctx.response)
}
- pub async fn mailbox_destroy(
+ async fn mailbox_destroy(
&self,
account_id: u32,
document_id: u32,
@@ -761,7 +802,7 @@ impl JMAP {
.validate())
}
- pub async fn mailbox_get_or_create(&self, account_id: u32) -> trc::Result<RoaringBitmap> {
+ async fn mailbox_get_or_create(&self, account_id: u32) -> trc::Result<RoaringBitmap> {
let mut mailbox_ids = self
.get_document_ids(account_id, Collection::Mailbox)
.await?
@@ -828,7 +869,7 @@ impl JMAP {
.map(|_| mailbox_ids)
}
- pub async fn mailbox_create_path(
+ async fn mailbox_create_path(
&self,
account_id: u32,
path: &str,
diff --git a/crates/jmap/src/principal/get.rs b/crates/jmap/src/principal/get.rs
index b59abd77..f17d1198 100644
--- a/crates/jmap/src/principal/get.rs
+++ b/crates/jmap/src/principal/get.rs
@@ -4,17 +4,26 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
+use common::Server;
use directory::{backend::internal::PrincipalField, QueryBy};
use jmap_proto::{
method::get::{GetRequest, GetResponse, RequestArguments},
object::Object,
types::{collection::Collection, property::Property, state::State, value::Value},
};
+use std::future::Future;
-use crate::JMAP;
+use crate::JmapMethods;
-impl JMAP {
- pub async fn principal_get(
+pub trait PrincipalGet: Sync + Send {
+ fn principal_get(
+ &self,
+ request: GetRequest<RequestArguments>,
+ ) -> impl Future<Output = trc::Result<GetResponse>> + Send;
+}
+
+impl PrincipalGet for Server {
+ async fn principal_get(
&self,
mut request: GetRequest<RequestArguments>,
) -> trc::Result<GetResponse> {
diff --git a/crates/jmap/src/principal/query.rs b/crates/jmap/src/principal/query.rs
index 0ed616ea..fa9793ee 100644
--- a/crates/jmap/src/principal/query.rs
+++ b/crates/jmap/src/principal/query.rs
@@ -4,6 +4,7 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
+use common::Server;
use directory::QueryBy;
use jmap_proto::{
method::query::{Filter, QueryRequest, QueryResponse, RequestArguments},
@@ -11,10 +12,19 @@ use jmap_proto::{
};
use store::{query::ResultSet, roaring::RoaringBitmap};
-use crate::{api::http::HttpSessionData, JMAP};
+use crate::{api::http::HttpSessionData, JmapMethods};
+use std::future::Future;
-impl JMAP {
- pub async fn principal_query(
+pub trait PrincipalQuery: Sync + Send {
+ fn principal_query(
+ &self,
+ request: QueryRequest<RequestArguments>,
+ session: &HttpSessionData,
+ ) -> impl Future<Output = trc::Result<QueryResponse>> + Send;
+}
+
+impl PrincipalQuery for Server {
+ async fn principal_query(
&self,
mut request: QueryRequest<RequestArguments>,
session: &HttpSessionData,
@@ -51,7 +61,6 @@ impl JMAP {
Filter::Email(email) => {
let mut ids = RoaringBitmap::new();
for id in self
- .core
.email_to_ids(&self.core.storage.directory, &email, session.session_id)
.await?
{
diff --git a/crates/jmap/src/push/get.rs b/crates/jmap/src/push/get.rs
index 175b8be6..41692f9b 100644
--- a/crates/jmap/src/push/get.rs
+++ b/crates/jmap/src/push/get.rs
@@ -5,7 +5,11 @@
*/
use base64::{engine::general_purpose, Engine};
-use common::auth::AccessToken;
+use common::{
+ auth::AccessToken,
+ ipc::{StateEvent, UpdateSubscription},
+ Server,
+};
use jmap_proto::{
method::get::{GetRequest, GetResponse, RequestArguments},
object::Object,
@@ -17,12 +21,26 @@ use store::{
};
use utils::map::bitmap::Bitmap;
-use crate::{services::state, JMAP};
+use crate::JmapMethods;
+
+use super::{EncryptionKeys, PushSubscription};
+use std::future::Future;
-use super::{EncryptionKeys, PushSubscription, UpdateSubscription};
+pub trait PushSubscriptionFetch: Sync + Send {
+ fn push_subscription_get(
+ &self,
+ request: GetRequest<RequestArguments>,
+ access_token: &AccessToken,
+ ) -> impl Future<Output = trc::Result<GetResponse>> + Send;
+
+ fn fetch_push_subscriptions(
+ &self,
+ account_id: u32,
+ ) -> impl Future<Output = trc::Result<StateEvent>> + Send;
+}
-impl JMAP {
- pub async fn push_subscription_get(
+impl PushSubscriptionFetch for Server {
+ async fn push_subscription_get(
&self,
mut request: GetRequest<RequestArguments>,
access_token: &AccessToken,
@@ -99,7 +117,7 @@ impl JMAP {
Ok(response)
}
- pub async fn fetch_push_subscriptions(&self, account_id: u32) -> trc::Result<state::Event> {
+ async fn fetch_push_subscriptions(&self, account_id: u32) -> trc::Result<StateEvent> {
let mut subscriptions = Vec::new();
let document_ids = self
.core
@@ -235,7 +253,7 @@ impl JMAP {
}
}
- Ok(state::Event::UpdateSubscriptions {
+ Ok(StateEvent::UpdateSubscriptions {
account_id,
subscriptions,
})
diff --git a/crates/jmap/src/push/manager.rs b/crates/jmap/src/push/manager.rs
index ba7acbfd..c543f400 100644
--- a/crates/jmap/src/push/manager.rs
+++ b/crates/jmap/src/push/manager.rs
@@ -5,23 +5,24 @@
*/
use base64::{engine::general_purpose, Engine};
-use common::IPC_CHANNEL_BUFFER;
+use common::{core::BuildServer, Inner, IPC_CHANNEL_BUFFER};
use jmap_proto::types::id::Id;
use store::ahash::{AHashMap, AHashSet};
use tokio::sync::mpsc;
use trc::PushSubscriptionEvent;
-use crate::{api::StateChangeResponse, JmapInstance, LONG_SLUMBER};
+use crate::{api::StateChangeResponse, LONG_SLUMBER};
use super::{ece::ece_encrypt, EncryptionKeys, Event, PushServer, PushUpdate};
use reqwest::header::{CONTENT_ENCODING, CONTENT_TYPE};
use std::{
collections::hash_map::Entry,
+ sync::Arc,
time::{Duration, Instant},
};
-pub fn spawn_push_manager(core: JmapInstance) -> mpsc::Sender<Event> {
+pub fn spawn_push_manager(inner: Arc<Inner>) -> mpsc::Sender<Event> {
let (push_tx_, mut push_rx) = mpsc::channel::<Event>(IPC_CHANNEL_BUFFER);
let push_tx = push_tx_.clone();
@@ -37,13 +38,13 @@ pub fn spawn_push_manager(core: JmapInstance) -> mpsc::Sender<Event> {
let event_or_timeout = tokio::time::timeout(retry_timeout, push_rx.recv()).await;
// Load settings
- let core_ = core.core.load_full();
- 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;
+ let server = inner.build_server();
+ let push_attempt_interval = server.core.jmap.push_attempt_interval;
+ let push_attempts_max = server.core.jmap.push_attempts_max;
+ let push_retry_interval = server.core.jmap.push_retry_interval;
+ let push_timeout = server.core.jmap.push_timeout;
+ let push_verify_timeout = server.core.jmap.push_verify_timeout;
+ let push_throttle = server.core.jmap.push_throttle;
match event_or_timeout {
Ok(Some(event)) => match event {
diff --git a/crates/jmap/src/push/mod.rs b/crates/jmap/src/push/mod.rs
index 9e6dae69..65669632 100644
--- a/crates/jmap/src/push/mod.rs
+++ b/crates/jmap/src/push/mod.rs
@@ -11,34 +11,8 @@ pub mod set;
use std::time::Instant;
-use jmap_proto::types::{id::Id, state::StateChange, type_state::DataType};
-use utils::map::bitmap::Bitmap;
-
-#[derive(Debug)]
-pub enum UpdateSubscription {
- Unverified {
- id: u32,
- url: String,
- code: String,
- keys: Option<EncryptionKeys>,
- },
- Verified(PushSubscription),
-}
-
-#[derive(Debug)]
-pub struct PushSubscription {
- pub id: u32,
- pub url: String,
- pub expires: u64,
- pub types: Bitmap<DataType>,
- pub keys: Option<EncryptionKeys>,
-}
-
-#[derive(Debug, Clone)]
-pub struct EncryptionKeys {
- pub p256dh: Vec<u8>,
- pub auth: Vec<u8>,
-}
+use common::ipc::{EncryptionKeys, PushSubscription};
+use jmap_proto::types::{id::Id, state::StateChange};
#[derive(Debug)]
pub enum Event {
diff --git a/crates/jmap/src/push/set.rs b/crates/jmap/src/push/set.rs
index 22f56f3e..d23b9436 100644
--- a/crates/jmap/src/push/set.rs
+++ b/crates/jmap/src/push/set.rs
@@ -5,7 +5,7 @@
*/
use base64::{engine::general_purpose, Engine};
-use common::auth::AccessToken;
+use common::{auth::AccessToken, Server};
use jmap_proto::{
error::set::SetError,
method::set::{RequestArguments, SetRequest, SetResponse},
@@ -19,18 +19,27 @@ use jmap_proto::{
value::{MaybePatchValue, Value},
},
};
+use std::future::Future;
use store::{
rand::{distributions::Alphanumeric, thread_rng, Rng},
write::{now, BatchBuilder, F_CLEAR, F_VALUE},
};
-use crate::JMAP;
+use crate::{services::state::StateManager, JmapMethods};
const EXPIRES_MAX: i64 = 7 * 24 * 3600; // 7 days
const VERIFICATION_CODE_LEN: usize = 32;
-impl JMAP {
- pub async fn push_subscription_set(
+pub trait PushSubscriptionSet: Sync + Send {
+ fn push_subscription_set(
+ &self,
+ request: SetRequest<RequestArguments>,
+ access_token: &AccessToken,
+ ) -> impl Future<Output = trc::Result<SetResponse>> + Send;
+}
+
+impl PushSubscriptionSet for Server {
+ async fn push_subscription_set(
&self,
mut request: SetRequest<RequestArguments>,
access_token: &AccessToken,
diff --git a/crates/jmap/src/quota/get.rs b/crates/jmap/src/quota/get.rs
index 193c77fb..a4edeb52 100644
--- a/crates/jmap/src/quota/get.rs
+++ b/crates/jmap/src/quota/get.rs
@@ -4,17 +4,26 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
-use common::auth::AccessToken;
+use common::{auth::AccessToken, Server};
use jmap_proto::{
method::get::{GetRequest, GetResponse, RequestArguments},
object::Object,
types::{id::Id, property::Property, state::State, type_state::DataType, value::Value},
};
+use std::future::Future;
-use crate::JMAP;
+use crate::JmapMethods;
-impl JMAP {
- pub async fn quota_get(
+pub trait QuotaGet: Sync + Send {
+ fn quota_get(
+ &self,
+ request: GetRequest<RequestArguments>,
+ access_token: &AccessToken,
+ ) -> impl Future<Output = trc::Result<GetResponse>> + Send;
+}
+
+impl QuotaGet for Server {
+ async fn quota_get(
&self,
mut request: GetRequest<RequestArguments>,
access_token: &AccessToken,
diff --git a/crates/jmap/src/quota/query.rs b/crates/jmap/src/quota/query.rs
index 40d65a52..dae4ce1e 100644
--- a/crates/jmap/src/quota/query.rs
+++ b/crates/jmap/src/quota/query.rs
@@ -4,16 +4,23 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
-use common::auth::AccessToken;
+use common::{auth::AccessToken, Server};
use jmap_proto::{
method::query::{QueryRequest, QueryResponse, RequestArguments},
types::{id::Id, state::State},
};
+use std::future::Future;
-use crate::JMAP;
+pub trait QuotaQuery: Sync + Send {
+ fn quota_query(
+ &self,
+ request: QueryRequest<RequestArguments>,
+ access_token: &AccessToken,
+ ) -> impl Future<Output = trc::Result<QueryResponse>> + Send;
+}
-impl JMAP {
- pub async fn quota_query(
+impl QuotaQuery for Server {
+ async fn quota_query(
&self,
request: QueryRequest<RequestArguments>,
access_token: &AccessToken,
diff --git a/crates/jmap/src/quota/set.rs b/crates/jmap/src/quota/set.rs
index 6aec092a..4a08f11c 100644
--- a/crates/jmap/src/quota/set.rs
+++ b/crates/jmap/src/quota/set.rs
@@ -8,14 +8,16 @@ use jmap_proto::{
object::index::{IndexAs, IndexProperty},
types::property::Property,
};
+use std::future::Future;
-use crate::JMAP;
-
-impl JMAP {
- pub async fn quota_set(
+pub trait QuotaSet: Sync + Send {
+ fn quota_set(
&self,
account_id: u32,
quota: &AccessToken,
- ) -> trc::Result<SetResponse> {
- }
+ ) -> impl Future<Output = trc::Result<SetResponse>> + Send;
+}
+
+impl QuotaSet for Server {
+ async fn quota_set(&self, account_id: u32, quota: &AccessToken) -> trc::Result<SetResponse> {}
}
diff --git a/crates/jmap/src/services/delivery.rs b/crates/jmap/src/services/delivery.rs
index fc732125..b3f8b674 100644
--- a/crates/jmap/src/services/delivery.rs
+++ b/crates/jmap/src/services/delivery.rs
@@ -4,18 +4,20 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
-use common::DeliveryEvent;
+use std::sync::Arc;
+
+use common::{core::BuildServer, ipc::DeliveryEvent, Inner};
use tokio::sync::mpsc;
-use crate::{JmapInstance, JMAP};
+use super::ingest::MailDelivery;
-pub fn spawn_delivery_manager(core: JmapInstance, mut delivery_rx: mpsc::Receiver<DeliveryEvent>) {
+pub fn spawn_delivery_manager(inner: Arc<Inner>, mut delivery_rx: mpsc::Receiver<DeliveryEvent>) {
tokio::spawn(async move {
while let Some(event) = delivery_rx.recv().await {
match event {
DeliveryEvent::Ingest { message, result_tx } => {
result_tx
- .send(JMAP::from(core.clone()).deliver_message(message).await)
+ .send(inner.build_server().deliver_message(message).await)
.ok();
}
DeliveryEvent::Stop => break,
diff --git a/crates/jmap/src/services/gossip/mod.rs b/crates/jmap/src/services/gossip/mod.rs
index def13548..959e1fe2 100644
--- a/crates/jmap/src/services/gossip/mod.rs
+++ b/crates/jmap/src/services/gossip/mod.rs
@@ -11,17 +11,16 @@ pub mod ping;
pub mod request;
pub mod spawn;
+use common::Inner;
use serde::{Deserialize, Serialize};
use std::{
net::{IpAddr, SocketAddr},
- sync::atomic::Ordering,
+ sync::{atomic::Ordering, Arc},
time::Instant,
};
use tokio::sync::mpsc;
use trc::ClusterEvent;
-use crate::JmapInstance;
-
use self::request::Request;
const UDP_MAX_PAYLOAD: usize = 65500;
@@ -44,7 +43,7 @@ pub struct Gossiper {
pub last_peer_pinged: usize,
// IPC
- pub core: JmapInstance,
+ pub inner: Arc<Inner>,
pub gossip_tx: mpsc::Sender<(SocketAddr, Request)>,
}
@@ -101,23 +100,26 @@ impl From<&Peer> for PeerStatus {
impl From<&Gossiper> for PeerStatus {
fn from(cluster: &Gossiper) -> Self {
- let core = cluster.core.core.load();
PeerStatus {
addr: cluster.addr,
epoch: cluster.epoch,
- gen_config: cluster
- .core
- .jmap_inner
- .config_version
+ gen_config: cluster.inner.data.config_version.load(Ordering::Relaxed),
+ gen_lists: cluster
+ .inner
+ .data
+ .blocked_ips_version
+ .load(Ordering::Relaxed),
+ gen_permissions: cluster
+ .inner
+ .data
+ .permissions_version
.load(Ordering::Relaxed),
- gen_lists: core.network.blocked_ips.version.load(Ordering::Relaxed),
- gen_permissions: core.security.permissions_version.load(Ordering::Relaxed),
}
}
}
impl Gossiper {
- pub async fn send_gossip(&self, dest: IpAddr, request: Request) {
+ async fn send_gossip(&self, dest: IpAddr, request: Request) {
if let Err(err) = self
.gossip_tx
.send((SocketAddr::new(dest, self.port), request))
diff --git a/crates/jmap/src/services/gossip/ping.rs b/crates/jmap/src/services/gossip/ping.rs
index ab30f88c..dc05a7e3 100644
--- a/crates/jmap/src/services/gossip/ping.rs
+++ b/crates/jmap/src/services/gossip/ping.rs
@@ -4,10 +4,13 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
-use smtp::queue;
+use common::{
+ core::BuildServer,
+ ipc::{HousekeeperEvent, QueueEvent},
+};
use trc::ClusterEvent;
-use crate::services::housekeeper;
+use crate::services::index::Indexer;
use super::{request::Request, Gossiper, PeerStatus};
@@ -66,13 +69,13 @@ impl Gossiper {
}
pub fn request_reload(&self) {
- let core = self.core.clone();
+ let server = self.inner.build_server();
tokio::spawn(async move {
trc::event!(Cluster(ClusterEvent::OneOrMorePeersOffline));
- core.jmap_inner.request_fts_index();
- let _ = core.smtp_inner.queue_tx.send(queue::Event::Reload).await;
+ server.request_fts_index();
+ let _ = server.inner.ipc.queue_tx.send(QueueEvent::Reload).await;
});
}
@@ -174,29 +177,30 @@ impl Gossiper {
// Reload settings
if update_permissions {
- self.core.core.load().security.permissions.clear();
+ self.inner.data.permissions.clear();
}
if update_config || update_lists {
- let core = self.core.core.clone();
- let inner = self.core.jmap_inner.clone();
+ let server = self.inner.build_server();
tokio::spawn(async move {
let result = if update_config {
- core.load().reload().await
+ server.reload().await
} else {
- core.load().reload_blocked_ips().await
+ server.reload_blocked_ips().await
};
match result {
Ok(result) => {
if let Some(new_core) = result.new_core {
// Update core
- core.store(new_core.into());
+ server.inner.shared_core.store(new_core.into());
// Reload ACME
- if inner
+ if server
+ .inner
+ .ipc
.housekeeper_tx
- .send(housekeeper::Event::ReloadSettings)
+ .send(HousekeeperEvent::ReloadSettings)
.await
.is_err()
{
diff --git a/crates/jmap/src/services/gossip/spawn.rs b/crates/jmap/src/services/gossip/spawn.rs
index 579cf40a..f5d41056 100644
--- a/crates/jmap/src/services/gossip/spawn.rs
+++ b/crates/jmap/src/services/gossip/spawn.rs
@@ -5,11 +5,10 @@
*/
use crate::auth::SymmetricEncrypt;
-use crate::JmapInstance;
use super::request::Request;
use super::{Gossiper, Peer, UDP_MAX_PAYLOAD};
-use common::IPC_CHANNEL_BUFFER;
+use common::{Inner, IPC_CHANNEL_BUFFER};
use std::net::IpAddr;
use std::time::{Duration, Instant};
use std::{net::SocketAddr, sync::Arc};
@@ -64,7 +63,7 @@ impl GossiperBuilder {
builder.into()
}
- pub async fn spawn(self, core: JmapInstance, mut shutdown_rx: watch::Receiver<bool>) {
+ pub async fn spawn(self, inner: Arc<Inner>, mut shutdown_rx: watch::Receiver<bool>) {
// Bind port
let quidnunc = Arc::new(Quidnunc {
socket: match UdpSocket::bind(SocketAddr::new(self.bind_addr, self.port)).await {
@@ -100,7 +99,7 @@ impl GossiperBuilder {
epoch: 0,
peers: self.peers,
last_peer_pinged: u32::MAX as usize,
- core,
+ inner,
gossip_tx,
};
let quidnunc_ = quidnunc.clone();
diff --git a/crates/jmap/src/services/housekeeper.rs b/crates/jmap/src/services/housekeeper.rs
index ed05f8c6..4387146a 100644
--- a/crates/jmap/src/services/housekeeper.rs
+++ b/crates/jmap/src/services/housekeeper.rs
@@ -6,10 +6,16 @@
use std::{
collections::BinaryHeap,
+ sync::{atomic::Ordering, Arc},
time::{Duration, Instant, SystemTime},
};
-use common::{config::telemetry::OtelMetrics, IPC_CHANNEL_BUFFER};
+use common::{
+ config::telemetry::OtelMetrics,
+ core::BuildServer,
+ ipc::{HousekeeperEvent, PurgeType},
+ Inner,
+};
#[cfg(feature = "enterprise")]
use common::telemetry::{
@@ -17,33 +23,13 @@ use common::telemetry::{
tracers::store::TracingStore,
};
-use smtp::core::SMTP;
-use store::{
- write::{now, purge::PurgeStore},
- BlobStore, LookupStore, Store,
-};
+use smtp::reporting::SmtpReporting;
+use store::write::{now, purge::PurgeStore};
use tokio::sync::mpsc;
-use trc::{Collector, HousekeeperEvent, MetricType};
+use trc::{Collector, MetricType};
use utils::map::ttl_dashmap::TtlMap;
-use crate::{Inner, JmapInstance, JMAP, LONG_SLUMBER};
-
-pub enum Event {
- AcmeReschedule {
- provider_id: String,
- renew_at: Instant,
- },
- Purge(PurgeType),
- ReloadSettings,
- Exit,
-}
-
-pub enum PurgeType {
- Data(Store),
- Blobs { store: Store, blob_store: BlobStore },
- Lookup(LookupStore),
- Account(Option<u32>),
-}
+use crate::{email::delete::EmailDeletion, JmapMethods, LONG_SLUMBER};
#[derive(PartialEq, Eq)]
struct Action {
@@ -75,30 +61,30 @@ struct Queue {
#[cfg(feature = "enterprise")]
const METRIC_ALERTS_INTERVAL: Duration = Duration::from_secs(5 * 60);
-pub fn spawn_housekeeper(core: JmapInstance, mut rx: mpsc::Receiver<Event>) {
+pub fn spawn_housekeeper(inner: Arc<Inner>, mut rx: mpsc::Receiver<HousekeeperEvent>) {
tokio::spawn(async move {
- trc::event!(Housekeeper(HousekeeperEvent::Start));
+ trc::event!(Housekeeper(trc::HousekeeperEvent::Start));
let start_time = SystemTime::now();
// Add all events to queue
let mut queue = Queue::default();
{
- let core_ = core.core.load_full();
+ let server = inner.build_server();
// Session purge
queue.schedule(
- Instant::now() + core_.jmap.session_purge_frequency.time_to_next(),
+ Instant::now() + server.core.jmap.session_purge_frequency.time_to_next(),
ActionClass::Session,
);
// Account purge
queue.schedule(
- Instant::now() + core_.jmap.account_purge_frequency.time_to_next(),
+ Instant::now() + server.core.jmap.account_purge_frequency.time_to_next(),
ActionClass::Account,
);
// Store purges
- for (idx, schedule) in core_.storage.purge_schedules.iter().enumerate() {
+ for (idx, schedule) in server.core.storage.purge_schedules.iter().enumerate() {
queue.schedule(
Instant::now() + schedule.cron.time_to_next(),
ActionClass::Store(idx),
@@ -106,7 +92,7 @@ pub fn spawn_housekeeper(core: JmapInstance, mut rx: mpsc::Receiver<Event>) {
}
// OTEL Push Metrics
- if let Some(otel) = &core_.metrics.otel {
+ if let Some(otel) = &server.core.metrics.otel {
OtelMetrics::enable_errors();
queue.schedule(Instant::now() + otel.interval, ActionClass::OtelMetrics);
}
@@ -115,8 +101,8 @@ pub fn spawn_housekeeper(core: JmapInstance, mut rx: mpsc::Receiver<Event>) {
queue.schedule(Instant::now(), ActionClass::CalculateMetrics);
// Add all ACME renewals to heap
- for provider in core_.tls.acme_providers.values() {
- match core_.init_acme(provider).await {
+ for provider in server.core.acme.providers.values() {
+ match server.init_acme(provider).await {
Ok(renew_at) => {
queue.schedule(
Instant::now() + renew_at,
@@ -135,7 +121,7 @@ pub fn spawn_housekeeper(core: JmapInstance, mut rx: mpsc::Receiver<Event>) {
// Enterprise Edition license management
#[cfg(feature = "enterprise")]
- if let Some(enterprise) = &core_.enterprise {
+ if let Some(enterprise) = &server.core.enterprise {
queue.schedule(
Instant::now() + enterprise.license.expires_in(),
ActionClass::ValidateLicense,
@@ -166,12 +152,11 @@ pub fn spawn_housekeeper(core: JmapInstance, mut rx: mpsc::Receiver<Event>) {
loop {
match tokio::time::timeout(queue.wake_up_time(), rx.recv()).await {
Ok(Some(event)) => match event {
- Event::ReloadSettings => {
- let core_ = core.core.load_full();
- let inner = core.jmap_inner.clone();
+ HousekeeperEvent::ReloadSettings => {
+ let server = inner.build_server();
// Reload OTEL push metrics
- match &core_.metrics.otel {
+ match &server.core.metrics.otel {
Some(otel) if !queue.has_action(&ActionClass::OtelMetrics) => {
OtelMetrics::enable_errors();
@@ -187,7 +172,7 @@ pub fn spawn_housekeeper(core: JmapInstance, mut rx: mpsc::Receiver<Event>) {
// SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art>
// SPDX-License-Identifier: LicenseRef-SEL
#[cfg(feature = "enterprise")]
- if let Some(enterprise) = &core_.enterprise {
+ if let Some(enterprise) = &server.core.enterprise {
if !queue.has_action(&ActionClass::ValidateLicense) {
queue.schedule(
Instant::now() + enterprise.license.expires_in(),
@@ -214,12 +199,14 @@ pub fn spawn_housekeeper(core: JmapInstance, mut rx: mpsc::Receiver<Event>) {
// Reload ACME certificates
tokio::spawn(async move {
- for provider in core_.tls.acme_providers.values() {
- match core_.init_acme(provider).await {
+ for provider in server.core.acme.providers.values() {
+ match server.init_acme(provider).await {
Ok(renew_at) => {
- inner
+ server
+ .inner
+ .ipc
.housekeeper_tx
- .send(Event::AcmeReschedule {
+ .send(HousekeeperEvent::AcmeReschedule {
provider_id: provider.id.clone(),
renew_at: Instant::now() + renew_at,
})
@@ -234,7 +221,7 @@ pub fn spawn_housekeeper(core: JmapInstance, mut rx: mpsc::Receiver<Event>) {
}
});
}
- Event::AcmeReschedule {
+ HousekeeperEvent::AcmeReschedule {
provider_id,
renew_at,
} => {
@@ -242,22 +229,22 @@ pub fn spawn_housekeeper(core: JmapInstance, mut rx: mpsc::Receiver<Event>) {
queue.remove_action(&action);
queue.schedule(renew_at, action);
}
- Event::Purge(purge) => match purge {
+ HousekeeperEvent::Purge(purge) => match purge {
PurgeType::Data(store) => {
// SPDX-SnippetBegin
// SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art>
// SPDX-License-Identifier: LicenseRef-SEL
#[cfg(feature = "enterprise")]
- let trace_retention = core
- .core
+ let trace_retention = inner
+ .shared_core
.load()
.enterprise
.as_ref()
.and_then(|e| e.trace_store.as_ref())
.and_then(|t| t.retention);
#[cfg(feature = "enterprise")]
- let metrics_retention = core
- .core
+ let metrics_retention = inner
+ .shared_core
.load()
.enterprise
.as_ref()
@@ -267,7 +254,7 @@ pub fn spawn_housekeeper(core: JmapInstance, mut rx: mpsc::Receiver<Event>) {
tokio::spawn(async move {
trc::event!(
- Housekeeper(HousekeeperEvent::PurgeStore),
+ Housekeeper(trc::HousekeeperEvent::PurgeStore),
Type = "data"
);
if let Err(err) = store.purge_store().await {
@@ -294,7 +281,10 @@ pub fn spawn_housekeeper(core: JmapInstance, mut rx: mpsc::Receiver<Event>) {
});
}
PurgeType::Blobs { store, blob_store } => {
- trc::event!(Housekeeper(HousekeeperEvent::PurgeStore), Type = "blob");
+ trc::event!(
+ Housekeeper(trc::HousekeeperEvent::PurgeStore),
+ Type = "blob"
+ );
tokio::spawn(async move {
if let Err(err) = store.purge_blobs(blob_store).await {
@@ -303,7 +293,10 @@ pub fn spawn_housekeeper(core: JmapInstance, mut rx: mpsc::Receiver<Event>) {
});
}
PurgeType::Lookup(store) => {
- trc::event!(Housekeeper(HousekeeperEvent::PurgeStore), Type = "lookup");
+ trc::event!(
+ Housekeeper(trc::HousekeeperEvent::PurgeStore),
+ Type = "lookup"
+ );
tokio::spawn(async move {
if let Err(err) = store.purge_lookup_store().await {
@@ -312,45 +305,44 @@ pub fn spawn_housekeeper(core: JmapInstance, mut rx: mpsc::Receiver<Event>) {
});
}
PurgeType::Account(account_id) => {
- let jmap = JMAP::from(core.clone());
+ let server = inner.build_server();
tokio::spawn(async move {
- trc::event!(Housekeeper(HousekeeperEvent::PurgeAccounts));
+ trc::event!(Housekeeper(trc::HousekeeperEvent::PurgeAccounts));
if let Some(account_id) = account_id {
- jmap.purge_account(account_id).await;
+ server.purge_account(account_id).await;
} else {
- jmap.purge_accounts().await;
+ server.purge_accounts().await;
}
});
}
},
- Event::Exit => {
- trc::event!(Housekeeper(HousekeeperEvent::Stop));
+ HousekeeperEvent::Exit => {
+ trc::event!(Housekeeper(trc::HousekeeperEvent::Stop));
return;
}
},
Ok(None) => {
- trc::event!(Housekeeper(HousekeeperEvent::Stop));
+ trc::event!(Housekeeper(trc::HousekeeperEvent::Stop));
return;
}
Err(_) => {
- let core_ = core.core.load_full();
+ let server = inner.build_server();
while let Some(event) = queue.pop() {
match event.event {
ActionClass::Acme(provider_id) => {
- let inner = core.jmap_inner.clone();
- let core = core_.clone();
+ let server = server.clone();
tokio::spawn(async move {
if let Some(provider) =
- core.tls.acme_providers.get(&provider_id)
+ server.core.acme.providers.get(&provider_id)
{
trc::event!(
Acme(trc::AcmeEvent::OrderStart),
Hostname = provider.domains.as_slice()
);
- let renew_at = match core.renew(provider).await {
+ let renew_at = match server.renew(provider).await {
Ok(renew_at) => {
trc::event!(
Acme(trc::AcmeEvent::OrderCompleted),
@@ -371,11 +363,13 @@ pub fn spawn_housekeeper(core: JmapInstance, mut rx: mpsc::Receiver<Event>) {
}
};
- inner.increment_config_version();
+ server.increment_config_version();
- inner
+ server
+ .inner
+ .ipc
.housekeeper_tx
- .send(Event::AcmeReschedule {
+ .send(HousekeeperEvent::AcmeReschedule {
provider_id: provider_id.clone(),
renew_at: Instant::now() + renew_at,
})
@@ -385,35 +379,48 @@ pub fn spawn_housekeeper(core: JmapInstance, mut rx: mpsc::Receiver<Event>) {
});
}
ActionClass::Account => {
- let jmap = JMAP::from(core.clone());
- tokio::spawn(async move {
- trc::event!(Housekeeper(HousekeeperEvent::PurgeAccounts));
- jmap.purge_accounts().await;
- });
+ let server = server.clone();
queue.schedule(
Instant::now()
- + core_.jmap.account_purge_frequency.time_to_next(),
+ + server.core.jmap.account_purge_frequency.time_to_next(),
ActionClass::Account,
);
- }
- ActionClass::Session => {
- let inner = core.jmap_inner.clone();
- let core = core_.clone();
-
tokio::spawn(async move {
- trc::event!(Housekeeper(HousekeeperEvent::PurgeSessions));
- inner.purge();
- core.security.access_tokens.cleanup();
+ trc::event!(Housekeeper(trc::HousekeeperEvent::PurgeAccounts));
+ server.purge_accounts().await;
});
+ }
+ ActionClass::Session => {
+ let server = server.clone();
queue.schedule(
Instant::now()
- + core_.jmap.session_purge_frequency.time_to_next(),
+ + server.core.jmap.session_purge_frequency.time_to_next(),
ActionClass::Session,
);
+
+ tokio::spawn(async move {
+ trc::event!(Housekeeper(trc::HousekeeperEvent::PurgeSessions));
+ server.inner.data.http_auth_cache.cleanup();
+ server
+ .inner
+ .data
+ .jmap_limiter
+ .retain(|_, limiter| limiter.is_active());
+ server.inner.data.access_tokens.cleanup();
+
+ for throttle in [
+ &server.inner.data.smtp_session_throttle,
+ &server.inner.data.smtp_queue_throttle,
+ ] {
+ throttle.retain(|_, v| {
+ v.concurrent.load(Ordering::Relaxed) > 0
+ });
+ }
+ });
}
ActionClass::Store(idx) => {
if let Some(schedule) =
- core_.storage.purge_schedules.get(idx).cloned()
+ server.core.storage.purge_schedules.get(idx).cloned()
{
queue.schedule(
Instant::now() + schedule.cron.time_to_next(),
@@ -435,7 +442,7 @@ pub fn spawn_housekeeper(core: JmapInstance, mut rx: mpsc::Receiver<Event>) {
match result {
Ok(_) => {
trc::event!(
- Housekeeper(HousekeeperEvent::PurgeStore),
+ Housekeeper(trc::HousekeeperEvent::PurgeStore),
Id = schedule.store_id
);
}
@@ -451,16 +458,22 @@ pub fn spawn_housekeeper(core: JmapInstance, mut rx: mpsc::Receiver<Event>) {
}
}
ActionClass::OtelMetrics => {
- if let Some(otel) = &core_.metrics.otel {
+ if let Some(otel) = &server.core.metrics.otel {
queue.schedule(
Instant::now() + otel.interval,
ActionClass::OtelMetrics,
);
let otel = otel.clone();
- let core = core_.clone();
+
+ #[cfg(feature = "enterprise")]
+ let is_enterprise = server.is_enterprise_edition();
+
+ #[cfg(not(feature = "enterprise"))]
+ let is_enterprise = false;
+
tokio::spawn(async move {
- otel.push_metrics(core, start_time).await;
+ otel.push_metrics(is_enterprise, start_time).await;
});
}
}
@@ -479,12 +492,12 @@ pub fn spawn_housekeeper(core: JmapInstance, mut rx: mpsc::Receiver<Event>) {
false
};
- let core = core_.clone();
+ let server = server.clone();
tokio::spawn(async move {
#[cfg(feature = "enterprise")]
- if core.is_enterprise_edition() {
+ if server.is_enterprise_edition() {
// Obtain queue size
- match core.total_queued_messages().await {
+ match server.total_queued_messages().await {
Ok(total) => {
Collector::update_gauge(
MetricType::QueueCount,
@@ -500,7 +513,7 @@ pub fn spawn_housekeeper(core: JmapInstance, mut rx: mpsc::Receiver<Event>) {
}
if update_other_metrics {
- match core.total_accounts().await {
+ match server.total_accounts().await {
Ok(total) => {
Collector::update_gauge(
MetricType::UserCount,
@@ -514,7 +527,7 @@ pub fn spawn_housekeeper(core: JmapInstance, mut rx: mpsc::Receiver<Event>) {
}
}
- match core.total_domains().await {
+ match server.total_domains().await {
Ok(total) => {
Collector::update_gauge(
MetricType::DomainCount,
@@ -556,7 +569,8 @@ pub fn spawn_housekeeper(core: JmapInstance, mut rx: mpsc::Receiver<Event>) {
// SPDX-License-Identifier: LicenseRef-SEL
#[cfg(feature = "enterprise")]
ActionClass::InternalMetrics => {
- if let Some(metrics_store) = &core_
+ if let Some(metrics_store) = &server
+ .core
.enterprise
.as_ref()
.and_then(|e| e.metrics_store.as_ref())
@@ -568,7 +582,7 @@ pub fn spawn_housekeeper(core: JmapInstance, mut rx: mpsc::Receiver<Event>) {
let metrics_store = metrics_store.store.clone();
let metrics_history = metrics_history.clone();
- let core = core_.clone();
+ let core = server.core.clone();
tokio::spawn(async move {
if let Err(err) = metrics_store
.write_metrics(core, now(), metrics_history)
@@ -582,22 +596,20 @@ pub fn spawn_housekeeper(core: JmapInstance, mut rx: mpsc::Receiver<Event>) {
#[cfg(feature = "enterprise")]
ActionClass::AlertMetrics => {
- let smtp = SMTP {
- core: core_.clone(),
- inner: core.smtp_inner.clone(),
- };
+ let server = server.clone();
tokio::spawn(async move {
- if let Some(messages) = smtp.core.process_alerts().await {
+ if let Some(messages) = server.process_alerts().await {
for message in messages {
- smtp.send_autogenerated(
- message.from,
- message.to.into_iter(),
- message.body,
- None,
- 0,
- )
- .await;
+ server
+ .send_autogenerated(
+ message.from,
+ message.to.into_iter(),
+ message.body,
+ None,
+ 0,
+ )
+ .await;
}
}
});
@@ -605,7 +617,7 @@ pub fn spawn_housekeeper(core: JmapInstance, mut rx: mpsc::Receiver<Event>) {
#[cfg(feature = "enterprise")]
ActionClass::ValidateLicense => {
- match core_.reload().await {
+ match server.reload().await {
Ok(result) => {
if let Some(new_core) = result.new_core {
if let Some(enterprise) = &new_core.enterprise {
@@ -617,10 +629,10 @@ pub fn spawn_housekeeper(core: JmapInstance, mut rx: mpsc::Receiver<Event>) {
}
// Update core
- core.core.store(new_core.into());
+ server.inner.shared_core.store(new_core.into());
// Increment version counter
- core.jmap_inner.increment_config_version();
+ server.increment_config_version();
}
}
Err(err) => {
@@ -639,7 +651,7 @@ pub fn spawn_housekeeper(core: JmapInstance, mut rx: mpsc::Receiver<Event>) {
impl Queue {
pub fn schedule(&mut self, due: Instant, event: ActionClass) {
trc::event!(
- Housekeeper(HousekeeperEvent::Schedule),
+ Housekeeper(trc::HousekeeperEvent::Schedule),
Due = trc::Value::Timestamp(
now() + due.saturating_duration_since(Instant::now()).as_secs()
),
@@ -684,15 +696,3 @@ impl PartialOrd for Action {
Some(self.cmp(other))
}
}
-
-impl Inner {
- pub fn purge(&self) {
- self.sessions.cleanup();
- self.concurrency_limiter
- .retain(|_, limiter| limiter.is_active());
- }
-}
-
-pub fn init_housekeeper() -> (mpsc::Sender<Event>, mpsc::Receiver<Event>) {
- mpsc::channel::<Event>(IPC_CHANNEL_BUFFER)
-}
diff --git a/crates/jmap/src/services/index.rs b/crates/jmap/src/services/index.rs
index 5a17fa3e..7481cbee 100644
--- a/crates/jmap/src/services/index.rs
+++ b/crates/jmap/src/services/index.rs
@@ -6,6 +6,7 @@
use std::{sync::Arc, time::Instant};
+use common::{core::BuildServer, Inner, Server};
use directory::{
backend::internal::{manage::ManageDirectory, PrincipalField},
Type,
@@ -22,17 +23,19 @@ use store::{
Deserialize, IterateParams, Serialize, ValueKey, U32_LEN, U64_LEN,
};
-use tokio::sync::Notify;
+use std::future::Future;
use trc::{AddContext, FtsIndexEvent};
use utils::{BlobHash, BLOB_HASH_LEN};
use crate::{
+ blob::download::BlobDownload,
+ changes::write::ChangeLog,
email::{index::IndexMessageText, metadata::MessageMetadata},
- Inner, JmapInstance, JMAP,
+ JmapMethods,
};
#[derive(Debug)]
-struct IndexEmail {
+pub struct IndexEmail {
account_id: u32,
document_id: u32,
seq: u64,
@@ -42,11 +45,12 @@ struct IndexEmail {
const INDEX_LOCK_EXPIRY: u64 = 60 * 5;
-pub fn spawn_index_task(core: JmapInstance, rx: Arc<Notify>) {
+pub fn spawn_index_task(inner: Arc<Inner>) {
tokio::spawn(async move {
+ let rx = inner.ipc.index_tx.clone();
loop {
// Index any queued messages
- JMAP::from(core.clone()).fts_index_queued().await;
+ inner.build_server().fts_index_queued().await;
// Wait for a signal to index more messages
rx.notified().await;
@@ -54,8 +58,19 @@ pub fn spawn_index_task(core: JmapInstance, rx: Arc<Notify>) {
});
}
-impl JMAP {
- pub async fn fts_index_queued(&self) {
+pub trait Indexer: Sync + Send {
+ fn fts_index_queued(&self) -> impl Future<Output = ()> + Send;
+ fn try_lock_index(&self, event: &IndexEmail) -> impl Future<Output = bool> + Send;
+ fn reindex(
+ &self,
+ account_id: Option<u32>,
+ tenant_id: Option<u32>,
+ ) -> impl Future<Output = trc::Result<()>> + Send;
+ fn request_fts_index(&self);
+}
+
+impl Indexer for Server {
+ async fn fts_index_queued(&self) {
let from_key = ValueKey::<ValueClass<u32>> {
account_id: 0,
collection: 0,
@@ -243,15 +258,11 @@ impl JMAP {
}
}
- pub fn request_fts_index(&self) {
- self.inner.request_fts_index();
+ fn request_fts_index(&self) {
+ self.inner.ipc.index_tx.notify_one();
}
- pub async fn reindex(
- &self,
- account_id: Option<u32>,
- tenant_id: Option<u32>,
- ) -> trc::Result<()> {
+ async fn reindex(&self, account_id: Option<u32>, tenant_id: Option<u32>) -> trc::Result<()> {
let accounts = if let Some(account_id) = account_id {
RoaringBitmap::from_sorted_iter([account_id]).unwrap()
} else {
@@ -362,12 +373,6 @@ impl JMAP {
}
}
-impl Inner {
- pub fn request_fts_index(&self) {
- self.index_tx.notify_one();
- }
-}
-
impl IndexEmail {
fn value_class(&self) -> ValueClass<MaybeDynamicId> {
ValueClass::FtsQueue(FtsQueueClass {
diff --git a/crates/jmap/src/services/ingest.rs b/crates/jmap/src/services/ingest.rs
index 07967628..aadeb15b 100644
--- a/crates/jmap/src/services/ingest.rs
+++ b/crates/jmap/src/services/ingest.rs
@@ -4,20 +4,33 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
-use common::{DeliveryResult, IngestMessage};
+use common::{
+ ipc::{DeliveryResult, IngestMessage},
+ Server,
+};
use directory::Permission;
use jmap_proto::types::{state::StateChange, type_state::DataType};
use mail_parser::MessageParser;
+use std::future::Future;
use store::ahash::AHashMap;
use crate::{
- email::ingest::{IngestEmail, IngestSource},
+ email::ingest::{EmailIngest, IngestEmail, IngestSource},
mailbox::INBOX_ID,
- JMAP,
+ sieve::{get::SieveScriptGet, ingest::SieveScriptIngest},
};
-impl JMAP {
- pub async fn deliver_message(&self, message: IngestMessage) -> Vec<DeliveryResult> {
+use super::state::StateManager;
+
+pub trait MailDelivery: Sync + Send {
+ fn deliver_message(
+ &self,
+ message: IngestMessage,
+ ) -> impl Future<Output = Vec<DeliveryResult>> + Send;
+}
+
+impl MailDelivery for Server {
+ async fn deliver_message(&self, message: IngestMessage) -> Vec<DeliveryResult> {
// Read message
let raw_message = match self
.core
@@ -60,7 +73,6 @@ impl JMAP {
let mut deliver_names = AHashMap::with_capacity(message.recipients.len());
for rcpt in &message.recipients {
match self
- .core
.email_to_ids(&self.core.storage.directory, rcpt, message.session_id)
.await
{
@@ -84,15 +96,11 @@ impl JMAP {
// Deliver to each recipient
for (uid, (status, rcpt)) in &mut deliver_names {
// Obtain access token
- let result = match self
- .core
- .get_cached_access_token(*uid)
- .await
- .and_then(|token| {
- token
- .assert_has_permission(Permission::EmailReceive)
- .map(|_| token)
- }) {
+ let result = match self.get_cached_access_token(*uid).await.and_then(|token| {
+ token
+ .assert_has_permission(Permission::EmailReceive)
+ .map(|_| token)
+ }) {
Ok(access_token) => {
// Check if there is an active sieve script
match self.sieve_script_get_active(*uid).await {
diff --git a/crates/jmap/src/services/state.rs b/crates/jmap/src/services/state.rs
index 6f448d50..670f5ff9 100644
--- a/crates/jmap/src/services/state.rs
+++ b/crates/jmap/src/services/state.rs
@@ -4,39 +4,24 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
-use std::time::{Duration, Instant, SystemTime};
+use std::{
+ sync::Arc,
+ time::{Duration, Instant, SystemTime},
+};
-use common::IPC_CHANNEL_BUFFER;
+use common::{
+ core::BuildServer,
+ ipc::{PushSubscription, StateEvent, UpdateSubscription},
+ Inner, Server, IPC_CHANNEL_BUFFER,
+};
use jmap_proto::types::{id::Id, state::StateChange, type_state::DataType};
+use std::future::Future;
use store::ahash::AHashMap;
use tokio::sync::mpsc;
use trc::ServerEvent;
use utils::map::bitmap::Bitmap;
-use crate::{
- push::{manager::spawn_push_manager, UpdateSubscription},
- JmapInstance, JMAP,
-};
-
-#[derive(Debug)]
-pub enum Event {
- Subscribe {
- account_id: u32,
- types: Bitmap<DataType>,
- tx: mpsc::Sender<StateChange>,
- },
- Publish {
- state_change: StateChange,
- },
- UpdateSharedAccounts {
- account_id: u32,
- },
- UpdateSubscriptions {
- account_id: u32,
- subscriptions: Vec<UpdateSubscription>,
- },
- Stop,
-}
+use crate::push::{get::PushSubscriptionFetch, manager::spawn_push_manager};
#[derive(Debug)]
struct Subscriber {
@@ -62,10 +47,6 @@ impl Subscriber {
const PURGE_EVERY: Duration = Duration::from_secs(3600);
const SEND_TIMEOUT: Duration = Duration::from_millis(500);
-pub fn init_state_manager() -> (mpsc::Sender<Event>, mpsc::Receiver<Event>) {
- mpsc::channel::<Event>(IPC_CHANNEL_BUFFER)
-}
-
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
enum SubscriberId {
Ipc(u32),
@@ -73,8 +54,8 @@ enum SubscriberId {
}
#[allow(clippy::unwrap_or_default)]
-pub fn spawn_state_manager(core: JmapInstance, mut change_rx: mpsc::Receiver<Event>) {
- let push_tx = spawn_push_manager(core.clone());
+pub fn spawn_state_manager(inner: Arc<Inner>, mut change_rx: mpsc::Receiver<StateEvent>) {
+ let push_tx = spawn_push_manager(inner.clone());
tokio::spawn(async move {
let mut subscribers: AHashMap<u32, AHashMap<SubscriberId, Subscriber>> =
@@ -89,7 +70,7 @@ pub fn spawn_state_manager(core: JmapInstance, mut change_rx: mpsc::Receiver<Eve
let mut purge_needed = last_purge.elapsed() >= PURGE_EVERY;
match event {
- Event::Stop => {
+ StateEvent::Stop => {
if push_tx.send(crate::push::Event::Reset).await.is_err() {
trc::event!(
Server(ServerEvent::ThreadError),
@@ -99,13 +80,9 @@ pub fn spawn_state_manager(core: JmapInstance, mut change_rx: mpsc::Receiver<Eve
}
break;
}
- Event::UpdateSharedAccounts { account_id } => {
+ StateEvent::UpdateSharedAccounts { account_id } => {
// Obtain account membership and shared mailboxes
- let acl = match JMAP::from(core.clone())
- .core
- .get_access_token(account_id)
- .await
- {
+ let acl = match inner.build_server().get_access_token(account_id).await {
Ok(result) => result,
Err(err) => {
trc::error!(err
@@ -169,7 +146,7 @@ pub fn spawn_state_manager(core: JmapInstance, mut change_rx: mpsc::Receiver<Eve
}
shared_accounts.insert(account_id, shared_account_ids);
}
- Event::Subscribe {
+ StateEvent::Subscribe {
account_id,
types,
tx,
@@ -185,7 +162,7 @@ pub fn spawn_state_manager(core: JmapInstance, mut change_rx: mpsc::Receiver<Eve
},
);
}
- Event::Publish { state_change } => {
+ StateEvent::Publish { state_change } => {
if let Some(shared_accounts) = shared_accounts_map.get(&state_change.account_id)
{
let current_time = SystemTime::now()
@@ -266,7 +243,7 @@ pub fn spawn_state_manager(core: JmapInstance, mut change_rx: mpsc::Receiver<Eve
}
}
}
- Event::UpdateSubscriptions {
+ StateEvent::UpdateSubscriptions {
account_id,
subscriptions,
} => {
@@ -280,7 +257,7 @@ pub fn spawn_state_manager(core: JmapInstance, mut change_rx: mpsc::Receiver<Eve
if let SubscriberId::Push(push_id) = subscriber_id {
if !subscriptions.iter().any(|s| {
matches!(s, UpdateSubscription::Verified(
- crate::push::PushSubscription { id, .. }
+ PushSubscription { id, .. }
) if id == push_id)
}) {
remove_ids.push(*subscriber_id);
@@ -388,18 +365,33 @@ pub fn spawn_state_manager(core: JmapInstance, mut change_rx: mpsc::Receiver<Eve
});
}
-impl JMAP {
- pub async fn subscribe_state_manager(
+pub trait StateManager: Sync + Send {
+ fn subscribe_state_manager(
+ &self,
+ account_id: u32,
+ types: Bitmap<DataType>,
+ ) -> impl Future<Output = trc::Result<mpsc::Receiver<StateChange>>> + Send;
+
+ fn broadcast_state_change(
+ &self,
+ state_change: StateChange,
+ ) -> impl Future<Output = bool> + Send;
+
+ fn update_push_subscriptions(&self, account_id: u32) -> impl Future<Output = bool> + Send;
+}
+
+impl StateManager for Server {
+ async fn subscribe_state_manager(
&self,
account_id: u32,
types: Bitmap<DataType>,
) -> trc::Result<mpsc::Receiver<StateChange>> {
let (change_tx, change_rx) = mpsc::channel::<StateChange>(IPC_CHANNEL_BUFFER);
- let state_tx = self.inner.state_tx.clone();
+ let state_tx = self.inner.ipc.state_tx.clone();
for event in [
- Event::UpdateSharedAccounts { account_id },
- Event::Subscribe {
+ StateEvent::UpdateSharedAccounts { account_id },
+ StateEvent::Subscribe {
account_id,
types,
tx: change_tx,
@@ -415,12 +407,13 @@ impl JMAP {
Ok(change_rx)
}
- pub async fn broadcast_state_change(&self, state_change: StateChange) -> bool {
+ async fn broadcast_state_change(&self, state_change: StateChange) -> bool {
match self
.inner
+ .ipc
.state_tx
.clone()
- .send(Event::Publish { state_change })
+ .send(StateEvent::Publish { state_change })
.await
{
Ok(_) => true,
@@ -436,7 +429,7 @@ impl JMAP {
}
}
- pub async fn update_push_subscriptions(&self, account_id: u32) -> bool {
+ async fn update_push_subscriptions(&self, account_id: u32) -> bool {
let push_subs = match self.fetch_push_subscriptions(account_id).await {
Ok(push_subs) => push_subs,
Err(err) => {
@@ -447,8 +440,8 @@ impl JMAP {
}
};
- let state_tx = self.inner.state_tx.clone();
- for event in [Event::UpdateSharedAccounts { account_id }, push_subs] {
+ let state_tx = self.inner.ipc.state_tx.clone();
+ for event in [StateEvent::UpdateSharedAccounts { account_id }, push_subs] {
if state_tx.send(event).await.is_err() {
trc::event!(
Server(ServerEvent::ThreadError),
diff --git a/crates/jmap/src/sieve/get.rs b/crates/jmap/src/sieve/get.rs
index 7bee50a3..d40bea1d 100644
--- a/crates/jmap/src/sieve/get.rs
+++ b/crates/jmap/src/sieve/get.rs
@@ -6,6 +6,7 @@
use std::sync::Arc;
+use common::Server;
use jmap_proto::{
method::get::{GetRequest, GetResponse, RequestArguments},
object::Object,
@@ -18,12 +19,42 @@ use store::{
BlobClass, Deserialize, Serialize,
};
-use crate::{sieve::SeenIds, JMAP};
+use crate::{
+ blob::{download::BlobDownload, upload::BlobUpload},
+ changes::state::StateManager,
+ sieve::SeenIds,
+ JmapMethods,
+};
use super::ActiveScript;
+use std::future::Future;
+
+pub trait SieveScriptGet: Sync + Send {
+ fn sieve_script_get(
+ &self,
+ request: GetRequest<RequestArguments>,
+ ) -> impl Future<Output = trc::Result<GetResponse>> + Send;
-impl JMAP {
- pub async fn sieve_script_get(
+ fn sieve_script_get_active(
+ &self,
+ account_id: u32,
+ ) -> impl Future<Output = trc::Result<Option<ActiveScript>>> + Send;
+
+ fn sieve_script_get_by_name(
+ &self,
+ account_id: u32,
+ name: &str,
+ ) -> impl Future<Output = trc::Result<Option<Sieve>>> + Send;
+
+ fn sieve_script_compile(
+ &self,
+ account_id: u32,
+ document_id: u32,
+ ) -> impl Future<Output = trc::Result<(Sieve, Object<Value>)>> + Send;
+}
+
+impl SieveScriptGet for Server {
+ async fn sieve_script_get(
&self,
mut request: GetRequest<RequestArguments>,
) -> trc::Result<GetResponse> {
@@ -115,10 +146,7 @@ impl JMAP {
Ok(response)
}
- pub async fn sieve_script_get_active(
- &self,
- account_id: u32,
- ) -> trc::Result<Option<ActiveScript>> {
+ async fn sieve_script_get_active(&self, account_id: u32) -> trc::Result<Option<ActiveScript>> {
// Find the currently active script
if let Some(document_id) = self
.filter(
@@ -156,7 +184,7 @@ impl JMAP {
}
}
- pub async fn sieve_script_get_by_name(
+ async fn sieve_script_get_by_name(
&self,
account_id: u32,
name: &str,
diff --git a/crates/jmap/src/sieve/ingest.rs b/crates/jmap/src/sieve/ingest.rs
index 16ecbe1f..08ba6051 100644
--- a/crates/jmap/src/sieve/ingest.rs
+++ b/crates/jmap/src/sieve/ingest.rs
@@ -6,7 +6,7 @@
use std::borrow::Cow;
-use common::{auth::AccessToken, listener::stream::NullIo};
+use common::{auth::AccessToken, listener::stream::NullIo, Server};
use directory::{backend::internal::PrincipalField, QueryBy};
use jmap_proto::types::{collection::Collection, id::Id, keyword::Keyword, property::Property};
use mail_parser::MessageParser;
@@ -19,13 +19,14 @@ use store::{
use trc::{AddContext, SieveEvent};
use crate::{
- email::ingest::{IngestEmail, IngestSource, IngestedEmail},
- mailbox::{INBOX_ID, TRASH_ID},
+ email::ingest::{EmailIngest, IngestEmail, IngestSource, IngestedEmail},
+ mailbox::{get::MailboxGet, set::MailboxSet, INBOX_ID, TRASH_ID},
sieve::SeenIdHash,
- JMAP,
+ JmapMethods,
};
-use super::ActiveScript;
+use super::{get::SieveScriptGet, ActiveScript};
+use std::future::Future;
struct SieveMessage<'x> {
pub raw_message: Cow<'x, [u8]>,
@@ -33,9 +34,21 @@ struct SieveMessage<'x> {
pub flags: Vec<Keyword>,
}
-impl JMAP {
+pub trait SieveScriptIngest: Sync + Send {
+ fn sieve_script_ingest(
+ &self,
+ access_token: &AccessToken,
+ raw_message: &[u8],
+ envelope_from: &str,
+ envelope_to: &str,
+ session_id: u64,
+ active_script: ActiveScript,
+ ) -> impl Future<Output = trc::Result<IngestedEmail>> + Send;
+}
+
+impl SieveScriptIngest for Server {
#[allow(clippy::blocks_in_conditions)]
- pub async fn sieve_script_ingest(
+ async fn sieve_script_ingest(
&self,
access_token: &AccessToken,
raw_message: &[u8],
@@ -124,8 +137,7 @@ impl JMAP {
}
}
sieve::Script::Global(name_) => {
- if let Some(script) =
- self.core.get_untrusted_sieve_script(name_, session_id)
+ if let Some(script) = self.get_untrusted_sieve_script(name_, session_id)
{
input = Input::script(name, script.clone());
} else {
@@ -353,7 +365,7 @@ impl JMAP {
);
Session::<NullIo>::sieve(
- self.smtp.clone(),
+ self.clone(),
SessionAddress::new(mail_from.clone()),
recipients,
message.raw_message.to_vec(),
diff --git a/crates/jmap/src/sieve/query.rs b/crates/jmap/src/sieve/query.rs
index 144ccd65..0e1c8cc9 100644
--- a/crates/jmap/src/sieve/query.rs
+++ b/crates/jmap/src/sieve/query.rs
@@ -4,18 +4,27 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
+use common::Server;
use jmap_proto::{
method::query::{
Comparator, Filter, QueryRequest, QueryResponse, RequestArguments, SortProperty,
},
types::{collection::Collection, property::Property},
};
+use std::future::Future;
use store::query::{self};
-use crate::JMAP;
+use crate::JmapMethods;
-impl JMAP {
- pub async fn sieve_script_query(
+pub trait SieveScriptQuery: Sync + Send {
+ fn sieve_script_query(
+ &self,
+ request: QueryRequest<RequestArguments>,
+ ) -> impl Future<Output = trc::Result<QueryResponse>> + Send;
+}
+
+impl SieveScriptQuery for Server {
+ async fn sieve_script_query(
&self,
mut request: QueryRequest<RequestArguments>,
) -> trc::Result<QueryResponse> {
diff --git a/crates/jmap/src/sieve/set.rs b/crates/jmap/src/sieve/set.rs
index 12f38da9..7ba9eec0 100644
--- a/crates/jmap/src/sieve/set.rs
+++ b/crates/jmap/src/sieve/set.rs
@@ -4,7 +4,10 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
-use common::auth::{AccessToken, ResourceToken};
+use common::{
+ auth::{AccessToken, ResourceToken},
+ Server,
+};
use jmap_proto::{
error::set::{SetError, SetErrorType},
method::set::{SetRequest, SetResponse},
@@ -34,9 +37,15 @@ use store::{
BlobClass,
};
-use crate::{api::http::HttpSessionData, JMAP};
+use crate::{
+ api::http::HttpSessionData,
+ blob::{download::BlobDownload, upload::BlobUpload},
+ changes::write::ChangeLog,
+ JmapMethods,
+};
+use std::future::Future;
-struct SetContext<'x> {
+pub struct SetContext<'x> {
resource_token: ResourceToken,
access_token: &'x AccessToken,
response: SetResponse,
@@ -53,8 +62,38 @@ pub static SCHEMA: &[IndexProperty] = &[
IndexProperty::new(Property::IsActive).index_as(IndexAs::Integer),
];
-impl JMAP {
- pub async fn sieve_script_set(
+pub trait SieveScriptSet: Sync + Send {
+ fn sieve_script_set(
+ &self,
+ request: SetRequest<SetArguments>,
+ access_token: &AccessToken,
+ session: &HttpSessionData,
+ ) -> impl Future<Output = trc::Result<SetResponse>> + Send;
+
+ fn sieve_script_delete(
+ &self,
+ resource_token: &ResourceToken,
+ document_id: u32,
+ fail_if_active: bool,
+ ) -> impl Future<Output = trc::Result<bool>> + Send;
+
+ fn sieve_set_item(
+ &self,
+ changes_: Object<SetValue>,
+ update: Option<(u32, HashedValue<Object<Value>>)>,
+ ctx: &SetContext,
+ session_id: u64,
+ ) -> impl Future<Output = trc::Result<Result<(ObjectIndexBuilder, Option<Vec<u8>>), SetError>>> + Send;
+
+ fn sieve_activate_script(
+ &self,
+ account_id: u32,
+ activate_id: Option<u32>,
+ ) -> impl Future<Output = trc::Result<Vec<(u32, bool)>>> + Send;
+}
+
+impl SieveScriptSet for Server {
+ async fn sieve_script_set(
&self,
mut request: SetRequest<SetArguments>,
access_token: &AccessToken,
@@ -343,7 +382,7 @@ impl JMAP {
Ok(ctx.response)
}
- pub async fn sieve_script_delete(
+ async fn sieve_script_delete(
&self,
resource_token: &ResourceToken,
document_id: u32,
@@ -581,7 +620,7 @@ impl JMAP {
.map(|obj| (obj, blob_update)))
}
- pub async fn sieve_activate_script(
+ async fn sieve_activate_script(
&self,
account_id: u32,
mut activate_id: Option<u32>,
diff --git a/crates/jmap/src/sieve/validate.rs b/crates/jmap/src/sieve/validate.rs
index 2413a572..81b19fbb 100644
--- a/crates/jmap/src/sieve/validate.rs
+++ b/crates/jmap/src/sieve/validate.rs
@@ -4,16 +4,25 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
-use common::auth::AccessToken;
+use common::{auth::AccessToken, Server};
use jmap_proto::{
error::set::{SetError, SetErrorType},
method::validate::{ValidateSieveScriptRequest, ValidateSieveScriptResponse},
};
+use std::future::Future;
-use crate::JMAP;
+use crate::blob::download::BlobDownload;
-impl JMAP {
- pub async fn sieve_script_validate(
+pub trait SieveScriptValidate: Sync + Send {
+ fn sieve_script_validate(
+ &self,
+ request: ValidateSieveScriptRequest,
+ access_token: &AccessToken,
+ ) -> impl Future<Output = trc::Result<ValidateSieveScriptResponse>> + Send;
+}
+
+impl SieveScriptValidate for Server {
+ async fn sieve_script_validate(
&self,
request: ValidateSieveScriptRequest,
access_token: &AccessToken,
diff --git a/crates/jmap/src/submission/get.rs b/crates/jmap/src/submission/get.rs
index f08cb166..ef9ca794 100644
--- a/crates/jmap/src/submission/get.rs
+++ b/crates/jmap/src/submission/get.rs
@@ -4,17 +4,26 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
+use common::Server;
use jmap_proto::{
method::get::{GetRequest, GetResponse, RequestArguments},
object::Object,
types::{collection::Collection, property::Property, value::Value},
};
-use smtp::queue;
+use smtp::queue::{self, spool::SmtpSpool};
+use std::future::Future;
-use crate::JMAP;
+use crate::{changes::state::StateManager, JmapMethods};
-impl JMAP {
- pub async fn email_submission_get(
+pub trait EmailSubmissionGet: Sync + Send {
+ fn email_submission_get(
+ &self,
+ request: GetRequest<RequestArguments>,
+ ) -> impl Future<Output = trc::Result<GetResponse>> + Send;
+}
+
+impl EmailSubmissionGet for Server {
+ async fn email_submission_get(
&self,
mut request: GetRequest<RequestArguments>,
) -> trc::Result<GetResponse> {
@@ -79,7 +88,6 @@ impl JMAP {
// Obtain queueId
let queued_message = self
- .smtp
.read_message(push.get(&Property::MessageId).as_uint().unwrap_or(u64::MAX))
.await;
diff --git a/crates/jmap/src/submission/query.rs b/crates/jmap/src/submission/query.rs
index 80ce88bc..fb1592e3 100644
--- a/crates/jmap/src/submission/query.rs
+++ b/crates/jmap/src/submission/query.rs
@@ -4,18 +4,27 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
+use common::Server;
use jmap_proto::{
method::query::{
Comparator, Filter, QueryRequest, QueryResponse, RequestArguments, SortProperty,
},
types::{collection::Collection, property::Property},
};
+use std::future::Future;
use store::query::{self};
-use crate::JMAP;
+use crate::JmapMethods;
-impl JMAP {
- pub async fn email_submission_query(
+pub trait EmailSubmissionQuery: Sync + Send {
+ fn email_submission_query(
+ &self,
+ request: QueryRequest<RequestArguments>,
+ ) -> impl Future<Output = trc::Result<QueryResponse>> + Send;
+}
+
+impl EmailSubmissionQuery for Server {
+ async fn email_submission_query(
&self,
mut request: QueryRequest<RequestArguments>,
) -> trc::Result<QueryResponse> {
diff --git a/crates/jmap/src/submission/set.rs b/crates/jmap/src/submission/set.rs
index 54e64a16..c92ae0da 100644
--- a/crates/jmap/src/submission/set.rs
+++ b/crates/jmap/src/submission/set.rs
@@ -6,7 +6,10 @@
use std::{collections::HashMap, sync::Arc};
-use common::listener::{stream::NullIo, ServerInstance};
+use common::{
+ listener::{stream::NullIo, ServerInstance},
+ Server,
+};
use jmap_proto::{
error::set::{SetError, SetErrorType},
method::set::{self, SetRequest, SetResponse},
@@ -29,12 +32,19 @@ use jmap_proto::{
},
};
use mail_parser::{HeaderName, HeaderValue};
-use smtp::core::{Session, SessionData, State};
+use smtp::{
+ core::{Session, SessionData, State},
+ queue::spool::SmtpSpool,
+};
use smtp_proto::{request::parser::Rfc5321Parser, MailFrom, RcptTo};
use store::write::{assert::HashedValue, log::ChangeLogBuilder, now, BatchBuilder, Bincode};
use utils::map::vec_map::VecMap;
-use crate::{email::metadata::MessageMetadata, identity::set::sanitize_email, JMAP};
+use crate::{
+ blob::download::BlobDownload, changes::write::ChangeLog, email::metadata::MessageMetadata,
+ identity::set::sanitize_email, JmapMethods,
+};
+use std::future::Future;
pub static SCHEMA: &[IndexProperty] = &[
IndexProperty::new(Property::UndoStatus).index_as(IndexAs::Text {
@@ -47,8 +57,25 @@ pub static SCHEMA: &[IndexProperty] = &[
IndexProperty::new(Property::SendAt).index_as(IndexAs::LongInteger),
];
-impl JMAP {
- pub async fn email_submission_set(
+pub trait EmailSubmissionSet: Sync + Send {
+ fn email_submission_set(
+ &self,
+ request: SetRequest<SetArguments>,
+ instance: &Arc<ServerInstance>,
+ next_call: &mut Option<Call<RequestMethod>>,
+ ) -> impl Future<Output = trc::Result<SetResponse>> + Send;
+
+ fn send_message(
+ &self,
+ account_id: u32,
+ response: &SetResponse,
+ instance: &Arc<ServerInstance>,
+ object: Object<SetValue>,
+ ) -> impl Future<Output = trc::Result<Result<Object<Value>, SetError>>> + Send;
+}
+
+impl EmailSubmissionSet for Server {
+ async fn email_submission_set(
&self,
mut request: SetRequest<SetArguments>,
instance: &Arc<ServerInstance>,
@@ -147,10 +174,10 @@ impl JMAP {
match undo_status {
Some(undo_status) if undo_status == "canceled" => {
- if let Some(queue_message) = self.smtp.read_message(queue_id).await {
+ if let Some(queue_message) = self.read_message(queue_id).await {
// Delete message from queue
let message_due = queue_message.next_event().unwrap_or_default();
- queue_message.remove(&self.smtp, message_due).await;
+ queue_message.remove(self, message_due).await;
// Update record
let mut batch = BatchBuilder::new();
@@ -553,7 +580,7 @@ impl JMAP {
// Begin local SMTP session
let mut session =
- Session::<NullIo>::local(self.smtp.clone(), instance.clone(), SessionData::default());
+ Session::<NullIo>::local(self.clone(), instance.clone(), SessionData::default());
// MAIL FROM
let _ = session.handle_mail_from(mail_from).await;
diff --git a/crates/jmap/src/thread/get.rs b/crates/jmap/src/thread/get.rs
index 39403806..c328da38 100644
--- a/crates/jmap/src/thread/get.rs
+++ b/crates/jmap/src/thread/get.rs
@@ -4,18 +4,27 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
+use common::Server;
use jmap_proto::{
method::get::{GetRequest, GetResponse, RequestArguments},
object::Object,
types::{collection::Collection, id::Id, property::Property},
};
+use std::future::Future;
use store::query::{sort::Pagination, Comparator, ResultSet};
use trc::AddContext;
-use crate::JMAP;
+use crate::{changes::state::StateManager, JmapMethods};
-impl JMAP {
- pub async fn thread_get(
+pub trait ThreadGet: Sync + Send {
+ fn thread_get(
+ &self,
+ request: GetRequest<RequestArguments>,
+ ) -> impl Future<Output = trc::Result<GetResponse>> + Send;
+}
+
+impl ThreadGet for Server {
+ async fn thread_get(
&self,
mut request: GetRequest<RequestArguments>,
) -> trc::Result<GetResponse> {
diff --git a/crates/jmap/src/vacation/get.rs b/crates/jmap/src/vacation/get.rs
index 087d5561..de491cba 100644
--- a/crates/jmap/src/vacation/get.rs
+++ b/crates/jmap/src/vacation/get.rs
@@ -4,18 +4,32 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
+use common::Server;
use jmap_proto::{
method::get::{GetRequest, GetResponse, RequestArguments},
object::Object,
request::reference::MaybeReference,
types::{any_id::AnyId, collection::Collection, id::Id, property::Property, value::Value},
};
+use std::future::Future;
use store::query::Filter;
-use crate::JMAP;
+use crate::{changes::state::StateManager, JmapMethods};
-impl JMAP {
- pub async fn vacation_response_get(
+pub trait VacationResponseGet: Sync + Send {
+ fn vacation_response_get(
+ &self,
+ request: GetRequest<RequestArguments>,
+ ) -> impl Future<Output = trc::Result<GetResponse>> + Send;
+
+ fn get_vacation_sieve_script_id(
+ &self,
+ account_id: u32,
+ ) -> impl Future<Output = trc::Result<Option<u32>>> + Send;
+}
+
+impl VacationResponseGet for Server {
+ async fn vacation_response_get(
&self,
mut request: GetRequest<RequestArguments>,
) -> trc::Result<GetResponse> {
@@ -100,7 +114,7 @@ impl JMAP {
Ok(response)
}
- pub async fn get_vacation_sieve_script_id(&self, account_id: u32) -> trc::Result<Option<u32>> {
+ async fn get_vacation_sieve_script_id(&self, account_id: u32) -> trc::Result<Option<u32>> {
self.filter(
account_id,
Collection::SieveScript,
diff --git a/crates/jmap/src/vacation/set.rs b/crates/jmap/src/vacation/set.rs
index 604e24f0..f924a782 100644
--- a/crates/jmap/src/vacation/set.rs
+++ b/crates/jmap/src/vacation/set.rs
@@ -6,7 +6,7 @@
use std::borrow::Cow;
-use common::auth::AccessToken;
+use common::{auth::AccessToken, Server};
use jmap_proto::{
error::set::{SetError, SetErrorType},
method::set::{RequestArguments, SetRequest, SetResponse},
@@ -22,6 +22,7 @@ use jmap_proto::{
};
use mail_builder::MessageBuilder;
use mail_parser::decoders::html::html_to_text;
+use std::future::Future;
use store::write::{
assert::HashedValue,
log::{Changes, LogInsert},
@@ -29,12 +30,26 @@ use store::write::{
};
use crate::{
- sieve::set::{ObjectBlobId, SCHEMA},
- JMAP,
+ blob::upload::BlobUpload,
+ changes::write::ChangeLog,
+ sieve::set::{ObjectBlobId, SieveScriptSet, SCHEMA},
+ JmapMethods,
};
-impl JMAP {
- pub async fn vacation_response_set(
+use super::get::VacationResponseGet;
+
+pub trait VacationResponseSet: Sync + Send {
+ fn vacation_response_set(
+ &self,
+ request: SetRequest<RequestArguments>,
+ access_token: &AccessToken,
+ ) -> impl Future<Output = trc::Result<SetResponse>> + Send;
+
+ fn build_script(&self, obj: &mut ObjectIndexBuilder) -> trc::Result<Vec<u8>>;
+}
+
+impl VacationResponseSet for Server {
+ async fn vacation_response_set(
&self,
mut request: SetRequest<RequestArguments>,
access_token: &AccessToken,
diff --git a/crates/jmap/src/websocket/stream.rs b/crates/jmap/src/websocket/stream.rs
index 20fbac55..85fa8482 100644
--- a/crates/jmap/src/websocket/stream.rs
+++ b/crates/jmap/src/websocket/stream.rs
@@ -6,7 +6,7 @@
use std::{sync::Arc, time::Instant};
-use common::auth::AccessToken;
+use common::{auth::AccessToken, Server};
use futures_util::{SinkExt, StreamExt};
use hyper::upgrade::Upgraded;
use hyper_util::rt::TokioIo;
@@ -23,12 +23,25 @@ use tungstenite::Message;
use utils::map::bitmap::Bitmap;
use crate::{
- api::http::{HttpSessionData, ToRequestError},
- JMAP,
+ api::{
+ http::{HttpSessionData, ToRequestError},
+ request::RequestHandler,
+ },
+ services::state::StateManager,
};
+use std::future::Future;
+
+pub trait WebSocketHandler: Sync + Send {
+ fn handle_websocket_stream(
+ &self,
+ stream: WebSocketStream<TokioIo<Upgraded>>,
+ access_token: Arc<AccessToken>,
+ session: HttpSessionData,
+ ) -> impl Future<Output = ()> + Send;
+}
-impl JMAP {
- pub async fn handle_websocket_stream(
+impl WebSocketHandler for Server {
+ async fn handle_websocket_stream(
&self,
mut stream: WebSocketStream<TokioIo<Upgraded>>,
access_token: Arc<AccessToken>,
diff --git a/crates/jmap/src/websocket/upgrade.rs b/crates/jmap/src/websocket/upgrade.rs
index ee9aef1a..cdafc5ad 100644
--- a/crates/jmap/src/websocket/upgrade.rs
+++ b/crates/jmap/src/websocket/upgrade.rs
@@ -6,20 +6,29 @@
use std::sync::Arc;
-use common::auth::AccessToken;
+use common::{auth::AccessToken, Server};
use hyper::StatusCode;
use hyper_util::rt::TokioIo;
use tokio_tungstenite::WebSocketStream;
use trc::JmapEvent;
use tungstenite::{handshake::derive_accept_key, protocol::Role};
-use crate::{
- api::{http::HttpSessionData, HttpRequest, HttpResponse, HttpResponseBody},
- JMAP,
-};
+use crate::api::{http::HttpSessionData, HttpRequest, HttpResponse, HttpResponseBody};
+use std::future::Future;
-impl JMAP {
- pub async fn upgrade_websocket_connection(
+use super::stream::WebSocketHandler;
+
+pub trait WebSocketUpgrade: Sync + Send {
+ fn upgrade_websocket_connection(
+ &self,
+ req: HttpRequest,
+ access_token: Arc<AccessToken>,
+ session: HttpSessionData,
+ ) -> impl Future<Output = trc::Result<HttpResponse>> + Send;
+}
+
+impl WebSocketUpgrade for Server {
+ async fn upgrade_websocket_connection(
&self,
req: HttpRequest,
access_token: Arc<AccessToken>,
diff --git a/crates/main/src/main.rs b/crates/main/src/main.rs
index 68335de2..c35c24d1 100644
--- a/crates/main/src/main.rs
+++ b/crates/main/src/main.rs
@@ -6,14 +6,13 @@
use std::time::Duration;
-use common::{config::server::ServerProtocol, manager::boot::BootManager, Ipc, IPC_CHANNEL_BUFFER};
+use common::{config::server::ServerProtocol, core::BuildServer, manager::boot::BootManager};
use directory::backend::internal::MigrateDirectory;
-use imap::core::{ImapSessionManager, IMAP};
-use jmap::{api::JmapSessionManager, services::gossip::spawn::GossiperBuilder, JMAP};
+use imap::core::ImapSessionManager;
+use jmap::{api::JmapSessionManager, services::gossip::spawn::GossiperBuilder, StartServices};
use managesieve::core::ManageSieveSessionManager;
use pop3::Pop3SessionManager;
-use smtp::core::{SmtpSessionManager, SMTP};
-use tokio::sync::mpsc;
+use smtp::{core::SmtpSessionManager, StartQueueManager};
use trc::Collector;
use utils::wait_for_shutdown;
@@ -27,72 +26,61 @@ static GLOBAL: Jemalloc = Jemalloc;
#[tokio::main]
async fn main() -> std::io::Result<()> {
// Load config and apply macros
- let init = BootManager::init().await;
-
- // Parse core
- let mut config = init.config;
- let core = init.core;
-
- // Setup IPC channels
- let (delivery_tx, delivery_rx) = mpsc::channel(IPC_CHANNEL_BUFFER);
- let ipc = Ipc { delivery_tx };
-
- // Init servers
- let smtp = SMTP::init(
- &mut config,
- core.clone(),
- ipc,
- init.servers.span_id_gen.clone(),
- )
- .await;
- let jmap = JMAP::init(&mut config, delivery_rx, core.clone(), smtp.inner.clone()).await;
- let imap = IMAP::init(&mut config, jmap.clone()).await;
- let gossiper = GossiperBuilder::try_parse(&mut config);
+ let mut init = BootManager::init().await;
+
+ // Init services
+ init.start_services().await;
+ init.start_queue_manager();
+ let gossiper = GossiperBuilder::try_parse(&mut init.config);
// Log configuration errors
- config.log_errors();
- config.log_warnings();
+ init.config.log_errors();
+ init.config.log_warnings();
+
+ {
+ let server = init.inner.build_server();
- // Log licensing information
- #[cfg(feature = "enterprise")]
- core.load().as_ref().log_license_details();
+ // Log licensing information
+ #[cfg(feature = "enterprise")]
+ server.log_license_details();
- // Migrate directory
- if let Err(err) = core.load().storage.data.migrate_directory().await {
- trc::error!(err.details("Directory migration failed"));
- std::process::exit(1);
+ // Migrate directory
+ if let Err(err) = server.store().migrate_directory().await {
+ trc::error!(err.details("Directory migration failed"));
+ std::process::exit(1);
+ }
}
// Spawn servers
let (shutdown_tx, shutdown_rx) = init.servers.spawn(|server, acceptor, shutdown_rx| {
match &server.protocol {
ServerProtocol::Smtp | ServerProtocol::Lmtp => server.spawn(
- SmtpSessionManager::new(smtp.clone()),
- core.clone(),
+ SmtpSessionManager::new(init.inner.clone()),
+ init.inner.clone(),
acceptor,
shutdown_rx,
),
ServerProtocol::Http => server.spawn(
- JmapSessionManager::new(jmap.clone()),
- core.clone(),
+ JmapSessionManager::new(init.inner.clone()),
+ init.inner.clone(),
acceptor,
shutdown_rx,
),
ServerProtocol::Imap => server.spawn(
- ImapSessionManager::new(imap.clone()),
- core.clone(),
+ ImapSessionManager::new(init.inner.clone()),
+ init.inner.clone(),
acceptor,
shutdown_rx,
),
ServerProtocol::Pop3 => server.spawn(
- Pop3SessionManager::new(imap.clone()),
- core.clone(),
+ Pop3SessionManager::new(init.inner.clone()),
+ init.inner.clone(),
acceptor,
shutdown_rx,
),
ServerProtocol::ManageSieve => server.spawn(
- ManageSieveSessionManager::new(imap.clone()),
- core.clone(),
+ ManageSieveSessionManager::new(init.inner.clone()),
+ init.inner.clone(),
acceptor,
shutdown_rx,
),
@@ -101,7 +89,7 @@ async fn main() -> std::io::Result<()> {
// Spawn gossip
if let Some(gossiper) = gossiper {
- gossiper.spawn(jmap, shutdown_rx.clone()).await;
+ gossiper.spawn(init.inner, shutdown_rx.clone()).await;
}
// Wait for shutdown signal
diff --git a/crates/managesieve/src/core/client.rs b/crates/managesieve/src/core/client.rs
index 2380e8ef..87603b7e 100644
--- a/crates/managesieve/src/core/client.rs
+++ b/crates/managesieve/src/core/client.rs
@@ -118,7 +118,7 @@ impl<T: SessionStream> Session<T> {
Command::Capability | Command::Logout | Command::Noop => Ok(command),
Command::Authenticate => {
if let State::NotAuthenticated { .. } = &self.state {
- if self.stream.is_tls() || self.jmap.core.imap.allow_plain_auth {
+ if self.stream.is_tls() || self.server.core.imap.allow_plain_auth {
Ok(command)
} else {
Err(trc::ManageSieveEvent::Error
@@ -151,9 +151,9 @@ impl<T: SessionStream> Session<T> {
| Command::CheckScript
| Command::Unauthenticate => {
if let State::Authenticated { access_token, .. } = &self.state {
- if let Some(rate) = &self.jmap.core.imap.rate_requests {
+ if let Some(rate) = &self.server.core.imap.rate_requests {
if self
- .jmap
+ .server
.core
.storage
.lookup
@@ -239,7 +239,7 @@ impl<T: AsyncWrite + AsyncRead + Unpin> Session<T> {
impl<T: AsyncWrite + AsyncRead> Session<T> {
pub async fn get_script_id(&self, account_id: u32, name: &str) -> trc::Result<u32> {
- self.jmap
+ self.server
.core
.storage
.data
diff --git a/crates/managesieve/src/core/mod.rs b/crates/managesieve/src/core/mod.rs
index 1f89b895..20f41ab4 100644
--- a/crates/managesieve/src/core/mod.rs
+++ b/crates/managesieve/src/core/mod.rs
@@ -12,15 +12,13 @@ use std::{borrow::Cow, net::IpAddr, sync::Arc};
use common::{
auth::AccessToken,
listener::{limiter::InFlight, ServerInstance},
+ Inner, Server,
};
-use imap::core::{ImapInstance, Inner};
use imap_proto::receiver::{CommandParser, Receiver};
-use jmap::JMAP;
use tokio::io::{AsyncRead, AsyncWrite};
pub struct Session<T: AsyncRead + AsyncWrite> {
- pub jmap: JMAP,
- pub imap: Arc<Inner>,
+ pub server: Server,
pub instance: Arc<ServerInstance>,
pub receiver: Receiver<Command>,
pub state: State,
@@ -51,12 +49,12 @@ impl State {
#[derive(Clone)]
pub struct ManageSieveSessionManager {
- pub imap: ImapInstance,
+ pub inner: Arc<Inner>,
}
impl ManageSieveSessionManager {
- pub fn new(imap: ImapInstance) -> Self {
- Self { imap }
+ pub fn new(inner: Arc<Inner>) -> Self {
+ Self { inner }
}
}
diff --git a/crates/managesieve/src/core/session.rs b/crates/managesieve/src/core/session.rs
index 910fe6a3..e56ed5cc 100644
--- a/crates/managesieve/src/core/session.rs
+++ b/crates/managesieve/src/core/session.rs
@@ -4,9 +4,11 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
-use common::listener::{SessionData, SessionManager, SessionResult, SessionStream};
+use common::{
+ core::BuildServer,
+ listener::{SessionData, SessionManager, SessionResult, SessionStream},
+};
use imap_proto::receiver::{self, Receiver};
-use jmap::JMAP;
use tokio_rustls::server::TlsStream;
use crate::SERVER_GREETING;
@@ -21,12 +23,11 @@ impl SessionManager for ManageSieveSessionManager {
) -> impl std::future::Future<Output = ()> + Send {
async move {
// Create session
- let jmap = JMAP::from(self.imap.jmap_instance);
+ let server = self.inner.build_server();
let mut session = Session {
- receiver: Receiver::with_max_request_size(jmap.core.imap.max_request_size)
+ receiver: Receiver::with_max_request_size(server.core.imap.max_request_size)
.with_start_state(receiver::State::Command { is_uid: false }),
- jmap,
- imap: self.imap.imap_inner,
+ server,
instance: session.instance,
state: State::NotAuthenticated { auth_failures: 0 },
session_id: session.session_id,
@@ -67,9 +68,9 @@ impl<T: SessionStream> Session<T> {
tokio::select! {
result = tokio::time::timeout(
if !matches!(self.state, State::NotAuthenticated {..}) {
- self.jmap.core.imap.timeout_auth
+ self.server.core.imap.timeout_auth
} else {
- self.jmap.core.imap.timeout_unauth
+ self.server.core.imap.timeout_unauth
},
self.read(&mut buf)) => {
match result {
@@ -142,8 +143,7 @@ impl<T: SessionStream> Session<T> {
instance: self.instance,
in_flight: self.in_flight,
session_id: self.session_id,
- jmap: self.jmap,
- imap: self.imap,
+ server: self.server,
receiver: self.receiver,
remote_addr: self.remote_addr,
})
diff --git a/crates/managesieve/src/op/authenticate.rs b/crates/managesieve/src/op/authenticate.rs
index da67f2e5..5157c04a 100644
--- a/crates/managesieve/src/op/authenticate.rs
+++ b/crates/managesieve/src/op/authenticate.rs
@@ -4,14 +4,19 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
-use common::listener::{limiter::ConcurrencyLimiter, SessionStream};
+use common::{
+ listener::{limiter::ConcurrencyLimiter, SessionStream},
+ ConcurrencyLimiters,
+};
use directory::Permission;
use imap::op::authenticate::{decode_challenge_oauth, decode_challenge_plain};
use imap_proto::{
protocol::authenticate::Mechanism,
receiver::{self, Request},
};
-use jmap::auth::rate_limit::ConcurrencyLimiters;
+use jmap::auth::{
+ authenticate::Authenticator, oauth::token::TokenHandler, rate_limit::RateLimiter,
+};
use mail_parser::decoders::base64::base64_decode;
use mail_send::Credentials;
use std::sync::Arc;
@@ -66,22 +71,22 @@ impl<T: SessionStream> Session<T> {
};
// Throttle authentication requests
- self.jmap.is_auth_allowed_soft(&self.remote_addr).await?;
+ self.server.is_auth_allowed_soft(&self.remote_addr).await?;
// Authenticate
let access_token = match credentials {
Credentials::Plain { username, secret } | Credentials::XOauth2 { username, secret } => {
- self.jmap
+ self.server
.authenticate_plain(&username, &secret, self.remote_addr, self.session_id)
.await
}
Credentials::OAuthBearer { token } => {
match self
- .jmap
+ .server
.validate_access_token("access_token", &token)
.await
{
- Ok((account_id, _, _)) => self.jmap.core.get_access_token(account_id).await,
+ Ok((account_id, _, _)) => self.server.get_access_token(account_id).await,
Err(err) => Err(err),
}
}
@@ -90,7 +95,7 @@ impl<T: SessionStream> Session<T> {
if err.matches(trc::EventType::Auth(trc::AuthEvent::Failed)) {
match &self.state {
State::NotAuthenticated { auth_failures }
- if *auth_failures < self.jmap.core.imap.max_auth_failures =>
+ if *auth_failures < self.server.core.imap.max_auth_failures =>
{
self.state = State::NotAuthenticated {
auth_failures: auth_failures + 1,
@@ -122,7 +127,7 @@ impl<T: SessionStream> Session<T> {
// Cache access token
let access_token = Arc::new(access_token);
- self.jmap.core.cache_access_token(access_token.clone());
+ self.server.cache_access_token(access_token.clone());
// Create session
self.state = State::Authenticated {
@@ -146,9 +151,11 @@ impl<T: SessionStream> Session<T> {
}
pub fn get_concurrency_limiter(&self, account_id: u32) -> Option<Arc<ConcurrencyLimiters>> {
- let rate = self.jmap.core.imap.rate_concurrent?;
- self.imap
- .rate_limiter
+ let rate = self.server.core.imap.rate_concurrent?;
+ self.server
+ .inner
+ .data
+ .imap_limiter
.get(&account_id)
.map(|limiter| limiter.clone())
.unwrap_or_else(|| {
@@ -156,7 +163,11 @@ impl<T: SessionStream> Session<T> {
concurrent_requests: ConcurrencyLimiter::new(rate),
concurrent_uploads: ConcurrencyLimiter::new(rate),
});
- self.imap.rate_limiter.insert(account_id, limiter.clone());
+ self.server
+ .inner
+ .data
+ .imap_limiter
+ .insert(account_id, limiter.clone());
limiter
})
.into()
diff --git a/crates/managesieve/src/op/capability.rs b/crates/managesieve/src/op/capability.rs
index 13f825db..33c1fb47 100644
--- a/crates/managesieve/src/op/capability.rs
+++ b/crates/managesieve/src/op/capability.rs
@@ -21,13 +21,13 @@ impl<T: SessionStream> Session<T> {
if !self.stream.is_tls() {
response.extend_from_slice(b"\"STARTTLS\"\r\n");
}
- if self.stream.is_tls() || self.jmap.core.imap.allow_plain_auth {
+ if self.stream.is_tls() || self.server.core.imap.allow_plain_auth {
response.extend_from_slice(b"\"SASL\" \"PLAIN OAUTHBEARER\"\r\n");
} else {
response.extend_from_slice(b"\"SASL\" \"OAUTHBEARER\"\r\n");
};
if let Some(sieve) =
- self.jmap
+ self.server
.core
.jmap
.capabilities
@@ -62,7 +62,7 @@ impl<T: SessionStream> Session<T> {
ManageSieve(trc::ManageSieveEvent::Capabilities),
SpanId = self.session_id,
Tls = self.stream.is_tls(),
- Strict = !self.jmap.core.imap.allow_plain_auth,
+ Strict = !self.server.core.imap.allow_plain_auth,
Elapsed = op_start.elapsed()
);
diff --git a/crates/managesieve/src/op/checkscript.rs b/crates/managesieve/src/op/checkscript.rs
index 58f79217..8e70b921 100644
--- a/crates/managesieve/src/op/checkscript.rs
+++ b/crates/managesieve/src/op/checkscript.rs
@@ -26,7 +26,7 @@ impl<T: SessionStream> Session<T> {
}
let script = request.tokens.into_iter().next().unwrap().unwrap_bytes();
- self.jmap
+ self.server
.core
.sieve
.untrusted_compiler
diff --git a/crates/managesieve/src/op/deletescript.rs b/crates/managesieve/src/op/deletescript.rs
index f620769e..1a2b218c 100644
--- a/crates/managesieve/src/op/deletescript.rs
+++ b/crates/managesieve/src/op/deletescript.rs
@@ -9,6 +9,7 @@ use std::time::Instant;
use common::listener::SessionStream;
use directory::Permission;
use imap_proto::receiver::Request;
+use jmap::{changes::write::ChangeLog, sieve::set::SieveScriptSet};
use jmap_proto::types::collection::Collection;
use store::write::log::ChangeLogBuilder;
use trc::AddContext;
@@ -37,7 +38,7 @@ impl<T: SessionStream> Session<T> {
let account_id = access_token.primary_id();
let document_id = self.get_script_id(account_id, &name).await?;
if self
- .jmap
+ .server
.sieve_script_delete(&access_token.as_resource_token(), document_id, true)
.await
.caused_by(trc::location!())?
@@ -45,7 +46,7 @@ impl<T: SessionStream> Session<T> {
// Write changes
let mut changelog = ChangeLogBuilder::new();
changelog.log_delete(Collection::SieveScript, document_id);
- self.jmap
+ self.server
.commit_changes(account_id, changelog)
.await
.caused_by(trc::location!())?;
diff --git a/crates/managesieve/src/op/getscript.rs b/crates/managesieve/src/op/getscript.rs
index e21f368d..8837521a 100644
--- a/crates/managesieve/src/op/getscript.rs
+++ b/crates/managesieve/src/op/getscript.rs
@@ -9,7 +9,7 @@ use std::time::Instant;
use common::listener::SessionStream;
use directory::Permission;
use imap_proto::receiver::Request;
-use jmap::sieve::set::ObjectBlobId;
+use jmap::{blob::download::BlobDownload, sieve::set::ObjectBlobId, JmapMethods};
use jmap_proto::{
object::Object,
types::{collection::Collection, property::Property, value::Value},
@@ -37,7 +37,7 @@ impl<T: SessionStream> Session<T> {
let account_id = self.state.access_token().primary_id();
let document_id = self.get_script_id(account_id, &name).await?;
let (blob_section, blob_hash) = self
- .jmap
+ .server
.get_property::<Object<Value>>(
account_id,
Collection::SieveScript,
@@ -61,7 +61,7 @@ impl<T: SessionStream> Session<T> {
.code(ResponseCode::TryLater)
})?;
let script = self
- .jmap
+ .server
.get_blob_section(&blob_hash, &blob_section)
.await
.caused_by(trc::location!())?
diff --git a/crates/managesieve/src/op/havespace.rs b/crates/managesieve/src/op/havespace.rs
index 056c6208..a2b1a212 100644
--- a/crates/managesieve/src/op/havespace.rs
+++ b/crates/managesieve/src/op/havespace.rs
@@ -9,6 +9,7 @@ use std::time::Instant;
use common::listener::SessionStream;
use directory::Permission;
use imap_proto::receiver::Request;
+use jmap::JmapMethods;
use trc::AddContext;
use crate::core::{Command, ResponseCode, Session, StatusResponse};
@@ -52,7 +53,7 @@ impl<T: SessionStream> Session<T> {
if access_token.quota == 0
|| size as i64
+ self
- .jmap
+ .server
.get_used_quota(account_id)
.await
.caused_by(trc::location!())?
diff --git a/crates/managesieve/src/op/listscripts.rs b/crates/managesieve/src/op/listscripts.rs
index 5659d293..35fd38ca 100644
--- a/crates/managesieve/src/op/listscripts.rs
+++ b/crates/managesieve/src/op/listscripts.rs
@@ -8,6 +8,7 @@ use std::time::Instant;
use common::listener::SessionStream;
use directory::Permission;
+use jmap::JmapMethods;
use jmap_proto::{
object::Object,
types::{collection::Collection, property::Property, value::Value},
@@ -24,7 +25,7 @@ impl<T: SessionStream> Session<T> {
let op_start = Instant::now();
let account_id = self.state.access_token().primary_id();
let document_ids = self
- .jmap
+ .server
.get_document_ids(account_id, Collection::SieveScript)
.await
.caused_by(trc::location!())?
@@ -39,7 +40,7 @@ impl<T: SessionStream> Session<T> {
for document_id in document_ids {
if let Some(script) = self
- .jmap
+ .server
.get_property::<Object<Value>>(
account_id,
Collection::SieveScript,
diff --git a/crates/managesieve/src/op/putscript.rs b/crates/managesieve/src/op/putscript.rs
index d980a768..d7a36279 100644
--- a/crates/managesieve/src/op/putscript.rs
+++ b/crates/managesieve/src/op/putscript.rs
@@ -9,7 +9,11 @@ use std::time::Instant;
use common::listener::SessionStream;
use directory::Permission;
use imap_proto::receiver::Request;
-use jmap::sieve::set::{ObjectBlobId, SCHEMA};
+use jmap::{
+ blob::upload::BlobUpload,
+ sieve::set::{ObjectBlobId, SCHEMA},
+ JmapMethods,
+};
use jmap_proto::{
object::{index::ObjectIndexBuilder, Object},
types::{blob::BlobId, collection::Collection, property::Property, value::Value},
@@ -54,19 +58,19 @@ impl<T: SessionStream> Session<T> {
// Check quota
let resource_token = self.state.access_token().as_resource_token();
let account_id = resource_token.account_id;
- self.jmap
+ self.server
.has_available_quota(&resource_token, script_bytes.len() as u64)
.await
.caused_by(trc::location!())?;
if self
- .jmap
+ .server
.get_document_ids(account_id, Collection::SieveScript)
.await
.caused_by(trc::location!())?
.map(|ids| ids.len() as usize)
.unwrap_or(0)
- > self.jmap.core.jmap.sieve_max_scripts
+ > self.server.core.jmap.sieve_max_scripts
{
return Err(trc::ManageSieveEvent::Error
.into_err()
@@ -76,7 +80,7 @@ impl<T: SessionStream> Session<T> {
// Compile script
match self
- .jmap
+ .server
.core
.sieve
.untrusted_compiler
@@ -103,7 +107,7 @@ impl<T: SessionStream> Session<T> {
if let Some(document_id) = self.validate_name(account_id, &name).await? {
// Obtain script values
let script = self
- .jmap
+ .server
.get_property::<HashedValue<Object<Value>>>(
account_id,
Collection::SieveScript,
@@ -127,7 +131,7 @@ impl<T: SessionStream> Session<T> {
// Write script blob
let blob_id = BlobId::new(
- self.jmap
+ self.server
.put_blob(account_id, &script_bytes, false)
.await
.caused_by(trc::location!())?
@@ -168,7 +172,7 @@ impl<T: SessionStream> Session<T> {
// Update tenant quota
#[cfg(feature = "enterprise")]
- if self.jmap.core.is_enterprise_edition() {
+ if self.server.core.is_enterprise_edition() {
if let Some(tenant) = resource_token.tenant {
batch.add(DirectoryClass::UsedQuota(tenant.id), update_quota);
}
@@ -183,7 +187,7 @@ impl<T: SessionStream> Session<T> {
.with_property(Property::BlobId, Value::BlobId(blob_id)),
),
);
- self.jmap
+ self.server
.write_batch(batch)
.await
.caused_by(trc::location!())?;
@@ -199,7 +203,7 @@ impl<T: SessionStream> Session<T> {
} else {
// Write script blob
let blob_id = BlobId::new(
- self.jmap
+ self.server
.put_blob(account_id, &script_bytes, false)
.await?
.hash,
@@ -236,14 +240,14 @@ impl<T: SessionStream> Session<T> {
// Update tenant quota
#[cfg(feature = "enterprise")]
- if self.jmap.core.is_enterprise_edition() {
+ if self.server.core.is_enterprise_edition() {
if let Some(tenant) = resource_token.tenant {
batch.add(DirectoryClass::UsedQuota(tenant.id), script_size);
}
}
let assigned_ids = self
- .jmap
+ .server
.write_batch(batch)
.await
.caused_by(trc::location!())?;
@@ -265,7 +269,7 @@ impl<T: SessionStream> Session<T> {
Err(trc::ManageSieveEvent::Error
.into_err()
.details("Script name cannot be empty."))
- } else if name.len() > self.jmap.core.jmap.sieve_max_script_name {
+ } else if name.len() > self.server.core.jmap.sieve_max_script_name {
Err(trc::ManageSieveEvent::Error
.into_err()
.details("Script name is too long."))
@@ -275,7 +279,7 @@ impl<T: SessionStream> Session<T> {
.details("The 'vacation' name is reserved, please use a different name."))
} else {
Ok(self
- .jmap
+ .server
.filter(
account_id,
Collection::SieveScript,
diff --git a/crates/managesieve/src/op/renamescript.rs b/crates/managesieve/src/op/renamescript.rs
index 6bd84345..8164768c 100644
--- a/crates/managesieve/src/op/renamescript.rs
+++ b/crates/managesieve/src/op/renamescript.rs
@@ -9,7 +9,7 @@ use std::time::Instant;
use common::listener::SessionStream;
use directory::Permission;
use imap_proto::receiver::Request;
-use jmap::sieve::set::SCHEMA;
+use jmap::{changes::write::ChangeLog, sieve::set::SCHEMA, JmapMethods};
use jmap_proto::{
object::{index::ObjectIndexBuilder, Object},
types::{collection::Collection, property::Property, value::Value},
@@ -62,7 +62,7 @@ impl<T: SessionStream> Session<T> {
// Obtain script values
let script = self
- .jmap
+ .server
.get_property::<HashedValue<Object<Value>>>(
account_id,
Collection::SieveScript,
@@ -92,13 +92,13 @@ impl<T: SessionStream> Session<T> {
),
);
if !batch.is_empty() {
- self.jmap
+ self.server
.write_batch(batch)
.await
.caused_by(trc::location!())?;
let mut changelog = ChangeLogBuilder::new();
changelog.log_update(Collection::SieveScript, document_id);
- self.jmap
+ self.server
.commit_changes(account_id, changelog)
.await
.caused_by(trc::location!())?;
diff --git a/crates/managesieve/src/op/setactive.rs b/crates/managesieve/src/op/setactive.rs
index fa88b725..4ea6283c 100644
--- a/crates/managesieve/src/op/setactive.rs
+++ b/crates/managesieve/src/op/setactive.rs
@@ -9,6 +9,7 @@ use std::time::Instant;
use common::listener::SessionStream;
use directory::Permission;
use imap_proto::receiver::Request;
+use jmap::{changes::write::ChangeLog, sieve::set::SieveScriptSet};
use jmap_proto::types::collection::Collection;
use store::write::log::ChangeLogBuilder;
use trc::AddContext;
@@ -35,7 +36,7 @@ impl<T: SessionStream> Session<T> {
// De/activate script
let account_id = self.state.access_token().primary_id();
let changes = self
- .jmap
+ .server
.sieve_activate_script(
account_id,
if !name.is_empty() {
@@ -53,7 +54,7 @@ impl<T: SessionStream> Session<T> {
for (document_id, _) in changes {
changelog.log_update(Collection::SieveScript, document_id);
}
- self.jmap
+ self.server
.commit_changes(account_id, changelog)
.await
.caused_by(trc::location!())?;
diff --git a/crates/pop3/src/client.rs b/crates/pop3/src/client.rs
index 5cea1276..ccc6f88c 100644
--- a/crates/pop3/src/client.rs
+++ b/crates/pop3/src/client.rs
@@ -163,7 +163,7 @@ impl<T: SessionStream> Session<T> {
| Command::Pass { .. }
| Command::Apop { .. } => {
if let State::NotAuthenticated { username, .. } = &self.state {
- if self.stream.is_tls() || self.jmap.core.imap.allow_plain_auth {
+ if self.stream.is_tls() || self.server.core.imap.allow_plain_auth {
if !matches!(command, Command::Pass { .. }) || username.is_some() {
Ok(command)
} else {
@@ -211,9 +211,9 @@ impl<T: SessionStream> Session<T> {
| Command::Stat
| Command::Rset => {
if let State::Authenticated { mailbox, .. } = &self.state {
- if let Some(rate) = &self.jmap.core.imap.rate_requests {
+ if let Some(rate) = &self.server.core.imap.rate_requests {
if self
- .jmap
+ .server
.core
.storage
.lookup
diff --git a/crates/pop3/src/lib.rs b/crates/pop3/src/lib.rs
index 5212dafe..a7429338 100644
--- a/crates/pop3/src/lib.rs
+++ b/crates/pop3/src/lib.rs
@@ -9,9 +9,8 @@ use std::{net::IpAddr, sync::Arc};
use common::{
auth::AccessToken,
listener::{limiter::InFlight, ServerInstance, SessionStream},
+ Inner, Server,
};
-use imap::core::{ImapInstance, Inner};
-use jmap::JMAP;
use mailbox::Mailbox;
use protocol::request::Parser;
@@ -25,18 +24,17 @@ static SERVER_GREETING: &str = "+OK Stalwart POP3 at your service.\r\n";
#[derive(Clone)]
pub struct Pop3SessionManager {
- pub pop3: ImapInstance,
+ pub inner: Arc<Inner>,
}
impl Pop3SessionManager {
- pub fn new(pop3: ImapInstance) -> Self {
- Self { pop3 }
+ pub fn new(inner: Arc<Inner>) -> Self {
+ Self { inner }
}
}
pub struct Session<T: SessionStream> {
- pub jmap: JMAP,
- pub imap: Arc<Inner>,
+ pub server: Server,
pub instance: Arc<ServerInstance>,
pub receiver: Parser,
pub state: State,
diff --git a/crates/pop3/src/mailbox.rs b/crates/pop3/src/mailbox.rs
index 10accd12..708bd86d 100644
--- a/crates/pop3/src/mailbox.rs
+++ b/crates/pop3/src/mailbox.rs
@@ -7,7 +7,10 @@
use std::collections::BTreeMap;
use common::listener::SessionStream;
-use jmap::mailbox::{UidMailbox, INBOX_ID};
+use jmap::{
+ mailbox::{set::MailboxSet, UidMailbox, INBOX_ID},
+ JmapMethods,
+};
use jmap_proto::{
object::Object,
types::{collection::Collection, property::Property, value::Value},
@@ -39,7 +42,7 @@ impl<T: SessionStream> Session<T> {
pub async fn fetch_mailbox(&self, account_id: u32) -> trc::Result<Mailbox> {
// Obtain message ids
let message_ids = self
- .jmap
+ .server
.get_tag(
account_id,
Collection::Email,
@@ -58,12 +61,12 @@ impl<T: SessionStream> Session<T> {
let mut message_sizes = AHashMap::new();
// Obtain UID validity
- self.jmap
+ self.server
.mailbox_get_or_create(account_id)
.await
.caused_by(trc::location!())?;
let uid_validity = self
- .jmap
+ .server
.get_property::<Object<Value>>(
account_id,
Collection::Mailbox,
@@ -83,7 +86,7 @@ impl<T: SessionStream> Session<T> {
.map(|v| v as u32)?;
// Obtain message sizes
- self.jmap
+ self.server
.core
.storage
.data
@@ -122,7 +125,7 @@ impl<T: SessionStream> Session<T> {
// Sort by UID
for (message_id, uid_mailbox) in self
- .jmap
+ .server
.get_properties::<Vec<UidMailbox>, _, _>(
account_id,
Collection::Email,
diff --git a/crates/pop3/src/op/authenticate.rs b/crates/pop3/src/op/authenticate.rs
index fe7ca09d..1924d4f2 100644
--- a/crates/pop3/src/op/authenticate.rs
+++ b/crates/pop3/src/op/authenticate.rs
@@ -4,10 +4,15 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
-use common::listener::{limiter::ConcurrencyLimiter, SessionStream};
+use common::{
+ listener::{limiter::ConcurrencyLimiter, SessionStream},
+ ConcurrencyLimiters,
+};
use directory::Permission;
use imap::op::authenticate::{decode_challenge_oauth, decode_challenge_plain};
-use jmap::auth::rate_limit::ConcurrencyLimiters;
+use jmap::auth::{
+ authenticate::Authenticator, oauth::token::TokenHandler, rate_limit::RateLimiter,
+};
use mail_parser::decoders::base64::base64_decode;
use mail_send::Credentials;
use std::sync::Arc;
@@ -60,22 +65,22 @@ impl<T: SessionStream> Session<T> {
pub async fn handle_auth(&mut self, credentials: Credentials<String>) -> trc::Result<()> {
// Throttle authentication requests
- self.jmap.is_auth_allowed_soft(&self.remote_addr).await?;
+ self.server.is_auth_allowed_soft(&self.remote_addr).await?;
// Authenticate
let access_token = match credentials {
Credentials::Plain { username, secret } | Credentials::XOauth2 { username, secret } => {
- self.jmap
+ self.server
.authenticate_plain(&username, &secret, self.remote_addr, self.session_id)
.await
}
Credentials::OAuthBearer { token } => {
match self
- .jmap
+ .server
.validate_access_token("access_token", &token)
.await
{
- Ok((account_id, _, _)) => self.jmap.core.get_access_token(account_id).await,
+ Ok((account_id, _, _)) => self.server.get_access_token(account_id).await,
Err(err) => Err(err),
}
}
@@ -86,7 +91,7 @@ impl<T: SessionStream> Session<T> {
State::NotAuthenticated {
auth_failures,
username,
- } if *auth_failures < self.jmap.core.imap.max_auth_failures => {
+ } if *auth_failures < self.server.core.imap.max_auth_failures => {
self.state = State::NotAuthenticated {
auth_failures: auth_failures + 1,
username: username.clone(),
@@ -118,7 +123,7 @@ impl<T: SessionStream> Session<T> {
// Cache access token
let access_token = Arc::new(access_token);
- self.jmap.core.cache_access_token(access_token.clone());
+ self.server.cache_access_token(access_token.clone());
// Fetch mailbox
let mailbox = self.fetch_mailbox(access_token.primary_id()).await?;
@@ -133,9 +138,11 @@ impl<T: SessionStream> Session<T> {
}
pub fn get_concurrency_limiter(&self, account_id: u32) -> Option<Arc<ConcurrencyLimiters>> {
- let rate = self.jmap.core.imap.rate_concurrent?;
- self.imap
- .rate_limiter
+ let rate = self.server.core.imap.rate_concurrent?;
+ self.server
+ .inner
+ .data
+ .imap_limiter
.get(&account_id)
.map(|limiter| limiter.clone())
.unwrap_or_else(|| {
@@ -143,7 +150,11 @@ impl<T: SessionStream> Session<T> {
concurrent_requests: ConcurrencyLimiter::new(rate),
concurrent_uploads: ConcurrencyLimiter::new(rate),
});
- self.imap.rate_limiter.insert(account_id, limiter.clone());
+ self.server
+ .inner
+ .data
+ .imap_limiter
+ .insert(account_id, limiter.clone());
limiter
})
.into()
diff --git a/crates/pop3/src/op/delete.rs b/crates/pop3/src/op/delete.rs
index 4e7799b2..e8b53c49 100644
--- a/crates/pop3/src/op/delete.rs
+++ b/crates/pop3/src/op/delete.rs
@@ -8,6 +8,9 @@ use std::time::Instant;
use common::listener::SessionStream;
use directory::Permission;
+use jmap::{
+ changes::write::ChangeLog, email::delete::EmailDeletion, services::state::StateManager,
+};
use jmap_proto::types::{state::StateChange, type_state::DataType};
use store::roaring::RoaringBitmap;
use trc::AddContext;
@@ -87,16 +90,18 @@ impl<T: SessionStream> Session<T> {
if !deleted.is_empty() {
let num_deleted = deleted.len();
let (changes, not_deleted) = self
- .jmap
+ .server
.emails_tombstone(mailbox.account_id, deleted)
.await
.caused_by(trc::location!())?;
if !changes.is_empty() {
- if let Ok(change_id) =
- self.jmap.commit_changes(mailbox.account_id, changes).await
+ if let Ok(change_id) = self
+ .server
+ .commit_changes(mailbox.account_id, changes)
+ .await
{
- self.jmap
+ self.server
.broadcast_state_change(
StateChange::new(mailbox.account_id)
.with_change(DataType::Email, change_id)
diff --git a/crates/pop3/src/op/fetch.rs b/crates/pop3/src/op/fetch.rs
index be322723..9d58cf45 100644
--- a/crates/pop3/src/op/fetch.rs
+++ b/crates/pop3/src/op/fetch.rs
@@ -8,7 +8,7 @@ use std::time::Instant;
use common::listener::SessionStream;
use directory::Permission;
-use jmap::email::metadata::MessageMetadata;
+use jmap::{blob::download::BlobDownload, email::metadata::MessageMetadata, JmapMethods};
use jmap_proto::types::{collection::Collection, property::Property};
use store::write::Bincode;
use trc::AddContext;
@@ -26,7 +26,7 @@ impl<T: SessionStream> Session<T> {
let mailbox = self.state.mailbox();
if let Some(message) = mailbox.messages.get(msg.saturating_sub(1) as usize) {
if let Some(metadata) = self
- .jmap
+ .server
.get_property::<Bincode<MessageMetadata>>(
mailbox.account_id,
Collection::Email,
@@ -37,7 +37,7 @@ impl<T: SessionStream> Session<T> {
.caused_by(trc::location!())?
{
if let Some(bytes) = self
- .jmap
+ .server
.get_blob(&metadata.inner.blob_hash, 0..usize::MAX)
.await
.caused_by(trc::location!())?
diff --git a/crates/pop3/src/op/mod.rs b/crates/pop3/src/op/mod.rs
index 0488caa8..bfc1db7d 100644
--- a/crates/pop3/src/op/mod.rs
+++ b/crates/pop3/src/op/mod.rs
@@ -18,7 +18,7 @@ pub mod list;
impl<T: SessionStream> Session<T> {
pub async fn handle_capa(&mut self) -> trc::Result<()> {
- let mechanisms = if self.stream.is_tls() || self.jmap.core.imap.allow_plain_auth {
+ let mechanisms = if self.stream.is_tls() || self.server.core.imap.allow_plain_auth {
vec![Mechanism::Plain, Mechanism::OAuthBearer]
} else {
vec![Mechanism::OAuthBearer]
@@ -28,7 +28,7 @@ impl<T: SessionStream> Session<T> {
Pop3(trc::Pop3Event::Capabilities),
SpanId = self.session_id,
Tls = self.stream.is_tls(),
- Strict = !self.jmap.core.imap.allow_plain_auth,
+ Strict = !self.server.core.imap.allow_plain_auth,
Elapsed = trc::Value::Duration(0)
);
diff --git a/crates/pop3/src/session.rs b/crates/pop3/src/session.rs
index ca0ae1d9..d016170a 100644
--- a/crates/pop3/src/session.rs
+++ b/crates/pop3/src/session.rs
@@ -6,8 +6,10 @@
use std::borrow::Cow;
-use common::listener::{SessionData, SessionManager, SessionResult, SessionStream};
-use jmap::JMAP;
+use common::{
+ core::BuildServer,
+ listener::{SessionData, SessionManager, SessionResult, SessionStream},
+};
use tokio_rustls::server::TlsStream;
use crate::{
@@ -28,8 +30,7 @@ impl SessionManager for Pop3SessionManager {
) -> impl std::future::Future<Output = ()> + Send {
async move {
let mut session = Session {
- jmap: JMAP::from(self.pop3.jmap_instance),
- imap: self.pop3.imap_inner,
+ server: self.inner.build_server(),
instance: session.instance,
receiver: Parser::default(),
state: State::NotAuthenticated {
@@ -71,9 +72,9 @@ impl<T: SessionStream> Session<T> {
tokio::select! {
result = tokio::time::timeout(
if !matches!(self.state, State::NotAuthenticated {..}) {
- self.jmap.core.imap.timeout_auth
+ self.server.core.imap.timeout_auth
} else {
- self.jmap.core.imap.timeout_unauth
+ self.server.core.imap.timeout_unauth
},
self.stream.read(&mut buf)) => {
match result {
@@ -141,8 +142,7 @@ impl<T: SessionStream> Session<T> {
.instance
.tls_accept(self.stream, self.session_id)
.await?,
- jmap: self.jmap,
- imap: self.imap,
+ server: self.server,
instance: self.instance,
receiver: self.receiver,
state: self.state,
diff --git a/crates/smtp/src/core/mod.rs b/crates/smtp/src/core/mod.rs
index d6832d7a..810521a1 100644
--- a/crates/smtp/src/core/mod.rs
+++ b/crates/smtp/src/core/mod.rs
@@ -12,86 +12,40 @@ use std::{
};
use common::{
- config::{scripts::ScriptCache, smtp::auth::VerifyStrategy},
+ config::smtp::auth::VerifyStrategy,
listener::{
limiter::{ConcurrencyLimiter, InFlight},
ServerInstance,
},
- Core, Ipc, SharedCore,
+ Inner, Server,
};
-use dashmap::DashMap;
use directory::Directory;
use mail_auth::{IprevOutput, SpfOutput};
use smtp_proto::request::receiver::{
BdatReceiver, DataReceiver, DummyDataReceiver, DummyLineReceiver, LineReceiver, RequestReceiver,
};
-use tokio::{
- io::{AsyncRead, AsyncWrite},
- sync::mpsc,
-};
-use tokio_rustls::TlsConnector;
+use tokio::io::{AsyncRead, AsyncWrite};
use utils::snowflake::SnowflakeIdGenerator;
use crate::{
inbound::auth::SaslToken,
- queue::{self, DomainPart, QueueId},
- reporting,
+ queue::{DomainPart, QueueId},
};
-use self::throttle::{ThrottleKey, ThrottleKeyHasherBuilder};
-
pub mod params;
pub mod throttle;
#[derive(Clone)]
-pub struct SmtpInstance {
- pub inner: Arc<Inner>,
- pub core: SharedCore,
-}
-
-impl SmtpInstance {
- pub fn new(core: SharedCore, inner: impl Into<Arc<Inner>>) -> Self {
- Self {
- core,
- inner: inner.into(),
- }
- }
-}
-
-#[derive(Clone)]
pub struct SmtpSessionManager {
- pub inner: SmtpInstance,
+ pub inner: Arc<Inner>,
}
impl SmtpSessionManager {
- pub fn new(inner: SmtpInstance) -> Self {
+ pub fn new(inner: Arc<Inner>) -> Self {
Self { inner }
}
}
-#[derive(Clone)]
-pub struct SMTP {
- pub core: Arc<Core>,
- pub inner: Arc<Inner>,
-}
-
-pub struct Inner {
- pub session_throttle: DashMap<ThrottleKey, ConcurrencyLimiter, ThrottleKeyHasherBuilder>,
- pub queue_throttle: DashMap<ThrottleKey, ConcurrencyLimiter, ThrottleKeyHasherBuilder>,
- pub queue_tx: mpsc::Sender<queue::Event>,
- pub report_tx: mpsc::Sender<reporting::Event>,
- pub queue_id_gen: SnowflakeIdGenerator,
- pub span_id_gen: Arc<SnowflakeIdGenerator>,
- pub connectors: TlsConnectors,
- pub ipc: Ipc,
- pub script_cache: ScriptCache,
-}
-
-pub struct TlsConnectors {
- pub pki_verify: TlsConnector,
- pub dummy_verify: TlsConnector,
-}
-
pub enum State {
Request(RequestReceiver),
Bdat(BdatReceiver),
@@ -107,7 +61,7 @@ pub struct Session<T: AsyncWrite + AsyncRead> {
pub hostname: String,
pub state: State,
pub instance: Arc<ServerInstance>,
- pub core: SMTP,
+ pub server: Server,
pub stream: T,
pub data: SessionData,
pub params: SessionParameters,
@@ -260,15 +214,6 @@ impl PartialOrd for SessionAddress {
}
}
-impl From<SmtpInstance> for SMTP {
- fn from(value: SmtpInstance) -> Self {
- SMTP {
- core: value.core.load_full(),
- inner: value.inner,
- }
- }
-}
-
static SIEVE: LazyLock<Arc<ServerInstance>> = LazyLock::new(|| {
Arc::new(ServerInstance {
id: "sieve".to_string(),
@@ -282,12 +227,16 @@ static SIEVE: LazyLock<Arc<ServerInstance>> = LazyLock::new(|| {
});
impl Session<common::listener::stream::NullIo> {
- pub fn local(core: SMTP, instance: std::sync::Arc<ServerInstance>, data: SessionData) -> Self {
+ pub fn local(
+ server: Server,
+ instance: std::sync::Arc<ServerInstance>,
+ data: SessionData,
+ ) -> Self {
Session {
hostname: "localhost".to_string(),
state: State::None,
instance,
- core,
+ server,
stream: common::listener::stream::NullIo::default(),
data,
params: SessionParameters {
@@ -315,14 +264,14 @@ impl Session<common::listener::stream::NullIo> {
}
pub fn sieve(
- core: SMTP,
+ server: Server,
mail_from: SessionAddress,
rcpt_to: Vec<SessionAddress>,
message: Vec<u8>,
session_id: u64,
) -> Self {
Self::local(
- core,
+ server,
SIEVE.clone(),
SessionData::local(mail_from.into(), rcpt_to, message, session_id),
)
@@ -398,25 +347,3 @@ impl SessionAddress {
}
}
}
-
-#[cfg(feature = "test_mode")]
-impl Default for Inner {
- fn default() -> Self {
- Self {
- session_throttle: Default::default(),
- queue_throttle: Default::default(),
- queue_tx: mpsc::channel(1).0,
- report_tx: mpsc::channel(1).0,
- queue_id_gen: Default::default(),
- span_id_gen: Arc::new(SnowflakeIdGenerator::new()),
- connectors: TlsConnectors {
- pki_verify: mail_send::smtp::tls::build_tls_connector(false),
- dummy_verify: mail_send::smtp::tls::build_tls_connector(true),
- },
- ipc: Ipc {
- delivery_tx: mpsc::channel(1).0,
- },
- script_cache: Default::default(),
- }
- }
-}
diff --git a/crates/smtp/src/core/params.rs b/crates/smtp/src/core/params.rs
index 4e555a24..5a4e873e 100644
--- a/crates/smtp/src/core/params.rs
+++ b/crates/smtp/src/core/params.rs
@@ -12,51 +12,45 @@ use super::Session;
impl<T: SessionStream> Session<T> {
pub async fn eval_session_params(&mut self) {
- let c = &self.core.core.smtp.session;
+ let c = &self.server.core.smtp.session;
self.data.bytes_left = self
- .core
- .core
+ .server
.eval_if(&c.transfer_limit, self, self.data.session_id)
.await
.unwrap_or(250 * 1024 * 1024);
self.data.valid_until += self
- .core
- .core
+ .server
.eval_if(&c.duration, self, self.data.session_id)
.await
.unwrap_or_else(|| Duration::from_secs(15 * 60));
self.params.timeout = self
- .core
- .core
+ .server
.eval_if(&c.timeout, self, self.data.session_id)
.await
.unwrap_or_else(|| Duration::from_secs(5 * 60));
self.params.spf_ehlo = self
- .core
- .core
+ .server
.eval_if(
- &self.core.core.smtp.mail_auth.spf.verify_ehlo,
+ &self.server.core.smtp.mail_auth.spf.verify_ehlo,
self,
self.data.session_id,
)
.await
.unwrap_or(VerifyStrategy::Relaxed);
self.params.spf_mail_from = self
- .core
- .core
+ .server
.eval_if(
- &self.core.core.smtp.mail_auth.spf.verify_mail_from,
+ &self.server.core.smtp.mail_auth.spf.verify_mail_from,
self,
self.data.session_id,
)
.await
.unwrap_or(VerifyStrategy::Relaxed);
self.params.iprev = self
- .core
- .core
+ .server
.eval_if(
- &self.core.core.smtp.mail_auth.iprev.verify,
+ &self.server.core.smtp.mail_auth.iprev.verify,
self,
self.data.session_id,
)
@@ -64,65 +58,56 @@ impl<T: SessionStream> Session<T> {
.unwrap_or(VerifyStrategy::Relaxed);
// Ehlo parameters
- let ec = &self.core.core.smtp.session.ehlo;
+ let ec = &self.server.core.smtp.session.ehlo;
self.params.ehlo_require = self
- .core
- .core
+ .server
.eval_if(&ec.require, self, self.data.session_id)
.await
.unwrap_or(true);
self.params.ehlo_reject_non_fqdn = self
- .core
- .core
+ .server
.eval_if(&ec.reject_non_fqdn, self, self.data.session_id)
.await
.unwrap_or(true);
// Auth parameters
- let ac = &self.core.core.smtp.session.auth;
+ let ac = &self.server.core.smtp.session.auth;
self.params.auth_directory = self
- .core
- .core
+ .server
.eval_if::<String, _>(&ac.directory, self, self.data.session_id)
.await
- .and_then(|name| self.core.core.get_directory(&name))
+ .and_then(|name| self.server.get_directory(&name))
.cloned();
self.params.auth_require = self
- .core
- .core
+ .server
.eval_if(&ac.require, self, self.data.session_id)
.await
.unwrap_or(false);
self.params.auth_errors_max = self
- .core
- .core
+ .server
.eval_if(&ac.errors_max, self, self.data.session_id)
.await
.unwrap_or(3);
self.params.auth_errors_wait = self
- .core
- .core
+ .server
.eval_if(&ac.errors_wait, self, self.data.session_id)
.await
.unwrap_or_else(|| Duration::from_secs(30));
self.params.auth_match_sender = self
- .core
- .core
+ .server
.eval_if(&ac.must_match_sender, self, self.data.session_id)
.await
.unwrap_or(true);
// VRFY/EXPN parameters
- let ec = &self.core.core.smtp.session.extensions;
+ let ec = &self.server.core.smtp.session.extensions;
self.params.can_expn = self
- .core
- .core
+ .server
.eval_if(&ec.expn, self, self.data.session_id)
.await
.unwrap_or(false);
self.params.can_vrfy = self
- .core
- .core
+ .server
.eval_if(&ec.vrfy, self, self.data.session_id)
.await
.unwrap_or(false);
@@ -130,24 +115,21 @@ impl<T: SessionStream> Session<T> {
pub async fn eval_post_auth_params(&mut self) {
// Refresh VRFY/EXPN parameters
- let ec = &self.core.core.smtp.session.extensions;
+ let ec = &self.server.core.smtp.session.extensions;
self.params.can_expn = self
- .core
- .core
+ .server
.eval_if(&ec.expn, self, self.data.session_id)
.await
.unwrap_or(false);
self.params.can_vrfy = self
- .core
- .core
+ .server
.eval_if(&ec.vrfy, self, self.data.session_id)
.await
.unwrap_or(false);
self.params.auth_match_sender = self
- .core
- .core
+ .server
.eval_if(
- &self.core.core.smtp.session.auth.must_match_sender,
+ &self.server.core.smtp.session.auth.must_match_sender,
self,
self.data.session_id,
)
@@ -156,30 +138,26 @@ impl<T: SessionStream> Session<T> {
}
pub async fn eval_rcpt_params(&mut self) {
- let rc = &self.core.core.smtp.session.rcpt;
+ let rc = &self.server.core.smtp.session.rcpt;
self.params.rcpt_errors_max = self
- .core
- .core
+ .server
.eval_if(&rc.errors_max, self, self.data.session_id)
.await
.unwrap_or(10);
self.params.rcpt_errors_wait = self
- .core
- .core
+ .server
.eval_if(&rc.errors_wait, self, self.data.session_id)
.await
.unwrap_or_else(|| Duration::from_secs(30));
self.params.rcpt_max = self
- .core
- .core
+ .server
.eval_if(&rc.max_recipients, self, self.data.session_id)
.await
.unwrap_or(100);
self.params.rcpt_dsn = self
- .core
- .core
+ .server
.eval_if(
- &self.core.core.smtp.session.extensions.dsn,
+ &self.server.core.smtp.session.extensions.dsn,
self,
self.data.session_id,
)
@@ -187,10 +165,9 @@ impl<T: SessionStream> Session<T> {
.unwrap_or(true);
self.params.max_message_size = self
- .core
- .core
+ .server
.eval_if(
- &self.core.core.smtp.session.data.max_message_size,
+ &self.server.core.smtp.session.data.max_message_size,
self,
self.data.session_id,
)
diff --git a/crates/smtp/src/core/throttle.rs b/crates/smtp/src/core/throttle.rs
index dc9fadf7..9abed15c 100644
--- a/crates/smtp/src/core/throttle.rs
+++ b/crates/smtp/src/core/throttle.rs
@@ -8,66 +8,13 @@ use common::{
config::smtp::{queue::QueueQuota, *},
expr::{functions::ResolveVariable, *},
listener::{limiter::ConcurrencyLimiter, SessionStream},
+ ThrottleKey,
};
use dashmap::mapref::entry::Entry;
use trc::SmtpEvent;
use utils::config::Rate;
-use std::{
- hash::{BuildHasher, Hash, Hasher},
- sync::atomic::Ordering,
-};
-
-use super::{Session, SMTP};
-
-#[derive(Debug, Clone, Eq)]
-pub struct ThrottleKey {
- hash: [u8; 32],
-}
-
-impl PartialEq for ThrottleKey {
- fn eq(&self, other: &Self) -> bool {
- self.hash == other.hash
- }
-}
-
-impl Hash for ThrottleKey {
- fn hash<H: Hasher>(&self, state: &mut H) {
- self.hash.hash(state);
- }
-}
-
-impl AsRef<[u8]> for ThrottleKey {
- fn as_ref(&self) -> &[u8] {
- &self.hash
- }
-}
-
-#[derive(Default)]
-pub struct ThrottleKeyHasher {
- hash: u64,
-}
-
-impl Hasher for ThrottleKeyHasher {
- fn finish(&self) -> u64 {
- self.hash
- }
-
- fn write(&mut self, bytes: &[u8]) {
- self.hash = u64::from_ne_bytes((&bytes[..std::mem::size_of::<u64>()]).try_into().unwrap());
- }
-}
-
-#[derive(Clone, Default)]
-pub struct ThrottleKeyHasherBuilder {}
-
-impl BuildHasher for ThrottleKeyHasherBuilder {
- type Hasher = ThrottleKeyHasher;
-
- fn build_hasher(&self) -> Self::Hasher {
- ThrottleKeyHasher::default()
- }
-}
+use super::Session;
pub trait NewKey: Sized {
fn new_key(&self, e: &impl ResolveVariable) -> ThrottleKey;
@@ -199,18 +146,17 @@ impl NewKey for Throttle {
impl<T: SessionStream> Session<T> {
pub async fn is_allowed(&mut self) -> bool {
let throttles = if !self.data.rcpt_to.is_empty() {
- &self.core.core.smtp.session.throttle.rcpt_to
+ &self.server.core.smtp.session.throttle.rcpt_to
} else if self.data.mail_from.is_some() {
- &self.core.core.smtp.session.throttle.mail_from
+ &self.server.core.smtp.session.throttle.mail_from
} else {
- &self.core.core.smtp.session.throttle.connect
+ &self.server.core.smtp.session.throttle.connect
};
for t in throttles {
if t.expr.is_empty()
|| self
- .core
- .core
+ .server
.eval_expr(&t.expr, self, "throttle", self.data.session_id)
.await
.unwrap_or(false)
@@ -233,7 +179,13 @@ impl<T: SessionStream> Session<T> {
// Check concurrency
if let Some(concurrency) = &t.concurrency {
- match self.core.inner.session_throttle.entry(key.clone()) {
+ match self
+ .server
+ .inner
+ .data
+ .smtp_session_throttle
+ .entry(key.clone())
+ {
Entry::Occupied(mut e) => {
let limiter = e.get_mut();
if let Some(inflight) = limiter.is_allowed() {
@@ -261,7 +213,7 @@ impl<T: SessionStream> Session<T> {
// Check rate
if let Some(rate) = &t.rate {
if self
- .core
+ .server
.core
.storage
.lookup
@@ -296,7 +248,7 @@ impl<T: SessionStream> Session<T> {
hasher.update(&rate.period.as_secs().to_ne_bytes()[..]);
hasher.update(&rate.requests.to_ne_bytes()[..]);
- self.core
+ self.server
.core
.storage
.lookup
@@ -306,11 +258,3 @@ impl<T: SessionStream> Session<T> {
.is_none()
}
}
-
-impl SMTP {
- pub fn cleanup(&self) {
- for throttle in [&self.inner.session_throttle, &self.inner.queue_throttle] {
- throttle.retain(|_, v| v.concurrent.load(Ordering::Relaxed) > 0);
- }
- }
-}
diff --git a/crates/smtp/src/inbound/auth.rs b/crates/smtp/src/inbound/auth.rs
index 419b4dab..d1ab3470 100644
--- a/crates/smtp/src/inbound/auth.rs
+++ b/crates/smtp/src/inbound/auth.rs
@@ -168,8 +168,7 @@ impl<T: SessionStream> Session<T> {
// Authenticate
let mut result = self
- .core
- .core
+ .server
.authenticate(
directory,
self.data.session_id,
@@ -182,8 +181,7 @@ impl<T: SessionStream> Session<T> {
// Validate permissions
if let Ok(principal) = &result {
match self
- .core
- .core
+ .server
.get_cached_access_token(principal.id())
.await
.caused_by(trc::location!())
diff --git a/crates/smtp/src/inbound/data.rs b/crates/smtp/src/inbound/data.rs
index 822fc734..80477eac 100644
--- a/crates/smtp/src/inbound/data.rs
+++ b/crates/smtp/src/inbound/data.rs
@@ -34,7 +34,8 @@ use utils::config::Rate;
use crate::{
core::{Session, SessionAddress, State},
inbound::milter::Modification,
- queue::{self, Message, MessageSource, QueueEnvelope, Schedule},
+ queue::{self, quota::HasQueueQuota, Message, MessageSource, QueueEnvelope, Schedule},
+ reporting::analysis::AnalyzeReport,
scripts::ScriptResult,
};
@@ -46,7 +47,7 @@ impl<T: SessionStream> Session<T> {
let raw_message = Arc::new(std::mem::take(&mut self.data.message));
let auth_message = if let Some(auth_message) = AuthenticatedMessage::parse_with_opts(
&raw_message,
- self.core.core.smtp.mail_auth.dkim.strict,
+ self.server.core.smtp.mail_auth.dkim.strict,
) {
auth_message
} else {
@@ -59,13 +60,12 @@ impl<T: SessionStream> Session<T> {
};
// Loop detection
- let dc = &self.core.core.smtp.session.data;
- let ac = &self.core.core.smtp.mail_auth;
- let rc = &self.core.core.smtp.report;
+ let dc = &self.server.core.smtp.session.data;
+ let ac = &self.server.core.smtp.mail_auth;
+ let rc = &self.server.core.smtp.report;
if auth_message.received_headers_count()
> self
- .core
- .core
+ .server
.eval_if(&dc.max_received_headers, self, self.data.session_id)
.await
.unwrap_or(50)
@@ -82,21 +82,19 @@ impl<T: SessionStream> Session<T> {
// Verify DKIM
let dkim = self
- .core
- .core
+ .server
.eval_if(&ac.dkim.verify, self, self.data.session_id)
.await
.unwrap_or(VerifyStrategy::Relaxed);
let dmarc = self
- .core
- .core
+ .server
.eval_if(&ac.dmarc.verify, self, self.data.session_id)
.await
.unwrap_or(VerifyStrategy::Relaxed);
let dkim_output = if dkim.verify() || dmarc.verify() {
let time = Instant::now();
let dkim_output = self
- .core
+ .server
.core
.smtp
.resolvers
@@ -111,8 +109,7 @@ impl<T: SessionStream> Session<T> {
// Send reports for failed signatures
if let Some(rate) = self
- .core
- .core
+ .server
.eval_if::<Rate, _>(&rc.dkim.send, self, self.data.session_id)
.await
{
@@ -155,21 +152,19 @@ impl<T: SessionStream> Session<T> {
// Verify ARC
let arc = self
- .core
- .core
+ .server
.eval_if(&ac.arc.verify, self, self.data.session_id)
.await
.unwrap_or(VerifyStrategy::Relaxed);
let arc_sealer = self
- .core
- .core
+ .server
.eval_if::<String, _>(&ac.arc.seal, self, self.data.session_id)
.await
- .and_then(|name| self.core.core.get_arc_sealer(&name, self.data.session_id));
+ .and_then(|name| self.server.get_arc_sealer(&name, self.data.session_id));
let arc_output = if arc.verify() || arc_sealer.is_some() {
let time = Instant::now();
let arc_output = self
- .core
+ .server
.core
.smtp
.resolvers
@@ -236,7 +231,7 @@ impl<T: SessionStream> Session<T> {
Some(spf_output) if dmarc.verify() => {
let time = Instant::now();
let dmarc_output = self
- .core
+ .server
.core
.smtp
.resolvers
@@ -317,7 +312,7 @@ impl<T: SessionStream> Session<T> {
// Analyze reports
if is_report {
- self.core
+ self.server
.analyze_report(raw_message.clone(), self.data.session_id);
if !rc.analysis.forward {
self.data.messages_sent += 1;
@@ -326,11 +321,16 @@ impl<T: SessionStream> Session<T> {
}
// Add Received header
- let message_id = self.core.inner.queue_id_gen.generate().unwrap_or_else(now);
+ let message_id = self
+ .server
+ .inner
+ .data
+ .queue_id_gen
+ .generate()
+ .unwrap_or_else(now);
let mut headers = Vec::with_capacity(64);
if self
- .core
- .core
+ .server
.eval_if(&dc.add_received, self, self.data.session_id)
.await
.unwrap_or(true)
@@ -340,8 +340,7 @@ impl<T: SessionStream> Session<T> {
// Add authentication results header
if self
- .core
- .core
+ .server
.eval_if(&dc.add_auth_results, self, self.data.session_id)
.await
.unwrap_or(true)
@@ -352,8 +351,7 @@ impl<T: SessionStream> Session<T> {
// Add Received-SPF header
if let Some(spf_output) = &self.data.spf_mail_from {
if self
- .core
- .core
+ .server
.eval_if(&dc.add_received_spf, self, self.data.session_id)
.await
.unwrap_or(true)
@@ -425,23 +423,20 @@ impl<T: SessionStream> Session<T> {
// Pipe message
for pipe in &dc.pipe_commands {
if let Some(command_) = self
- .core
- .core
+ .server
.eval_if::<String, _>(&pipe.command, self, self.data.session_id)
.await
{
let piped_message = edited_message.as_ref().unwrap_or(&raw_message).clone();
let timeout = self
- .core
- .core
+ .server
.eval_if(&pipe.timeout, self, self.data.session_id)
.await
.unwrap_or_else(|| Duration::from_secs(30));
let mut command = Command::new(&command_);
for argument in self
- .core
- .core
+ .server
.eval_if::<Vec<String>, _>(&pipe.arguments, self, self.data.session_id)
.await
.unwrap_or_default()
@@ -538,13 +533,11 @@ impl<T: SessionStream> Session<T> {
// Sieve filtering
if let Some((script, script_id)) = self
- .core
- .core
+ .server
.eval_if::<String, _>(&dc.script, self, self.data.session_id)
.await
.and_then(|name| {
- self.core
- .core
+ self.server
.get_trusted_sieve_script(&name, self.data.session_id)
.map(|s| (s, name))
})
@@ -642,8 +635,7 @@ impl<T: SessionStream> Session<T> {
// Add Return-Path
if self
- .core
- .core
+ .server
.eval_if(&dc.add_return_path, self, self.data.session_id)
.await
.unwrap_or(true)
@@ -656,8 +648,7 @@ impl<T: SessionStream> Session<T> {
// Add any missing headers
if !auth_message.has_date_header()
&& self
- .core
- .core
+ .server
.eval_if(&dc.add_date, self, self.data.session_id)
.await
.unwrap_or(true)
@@ -668,8 +659,7 @@ impl<T: SessionStream> Session<T> {
}
if !auth_message.has_message_id_header()
&& self
- .core
- .core
+ .server
.eval_if(&dc.add_message_id, self, self.data.session_id)
.await
.unwrap_or(true)
@@ -684,17 +674,12 @@ impl<T: SessionStream> Session<T> {
.as_deref()
.unwrap_or_else(|| raw_message.as_slice());
for signer in self
- .core
- .core
+ .server
.eval_if::<Vec<String>, _>(&ac.dkim.sign, self, self.data.session_id)
.await
.unwrap_or_default()
{
- if let Some(signer) = self
- .core
- .core
- .get_dkim_signer(&signer, self.data.session_id)
- {
+ if let Some(signer) = self.server.get_dkim_signer(&signer, self.data.session_id) {
match signer.sign_chained(&[headers.as_ref(), raw_message]) {
Ok(signature) => {
signature.write_header(&mut headers);
@@ -712,7 +697,7 @@ impl<T: SessionStream> Session<T> {
message.size = raw_message.len() + headers.len();
// Verify queue quota
- if self.core.has_quota(&mut message).await {
+ if self.server.has_quota(&mut message).await {
// Prepare webhook event
let queue_id = message.queue_id;
@@ -727,7 +712,7 @@ impl<T: SessionStream> Session<T> {
Some(&headers),
raw_message,
self.data.session_id,
- &self.core,
+ &self.server,
source,
)
.await
@@ -799,10 +784,9 @@ impl<T: SessionStream> Session<T> {
};
// Set expiration and notification times
- let config = &self.core.core.smtp.queue;
+ let config = &self.server.core.smtp.queue;
let (num_intervals, next_notify) = self
- .core
- .core
+ .server
.eval_if::<Vec<Duration>, _>(&config.notify, &envelope, self.data.session_id)
.await
.and_then(|v| (v.len(), v.into_iter().next()?).into())
@@ -813,8 +797,7 @@ impl<T: SessionStream> Session<T> {
now()
+ future_release.as_secs()
+ self
- .core
- .core
+ .server
.eval_if(&config.expire, &envelope, self.data.session_id)
.await
.unwrap_or_else(|| Duration::from_secs(5 * 86400))
@@ -827,8 +810,7 @@ impl<T: SessionStream> Session<T> {
)
} else {
let expire = self
- .core
- .core
+ .server
.eval_if(&config.expire, &envelope, self.data.session_id)
.await
.unwrap_or_else(|| Duration::from_secs(5 * 86400));
@@ -887,10 +869,9 @@ impl<T: SessionStream> Session<T> {
if !self.data.rcpt_to.is_empty() {
if self.data.messages_sent
< self
- .core
- .core
+ .server
.eval_if(
- &self.core.core.smtp.session.data.max_messages,
+ &self.server.core.smtp.session.data.max_messages,
self,
self.data.session_id,
)
diff --git a/crates/smtp/src/inbound/ehlo.rs b/crates/smtp/src/inbound/ehlo.rs
index 49580945..deeae3d1 100644
--- a/crates/smtp/src/inbound/ehlo.rs
+++ b/crates/smtp/src/inbound/ehlo.rs
@@ -42,7 +42,7 @@ impl<T: SessionStream> Session<T> {
if self.params.spf_ehlo.verify() {
let time = Instant::now();
let spf_output = self
- .core
+ .server
.core
.smtp
.resolvers
@@ -76,17 +76,15 @@ impl<T: SessionStream> Session<T> {
// Sieve filtering
if let Some((script, script_id)) = self
- .core
- .core
+ .server
.eval_if::<String, _>(
- &self.core.core.smtp.session.ehlo.script,
+ &self.server.core.smtp.session.ehlo.script,
self,
self.data.session_id,
)
.await
.and_then(|name| {
- self.core
- .core
+ self.server
.get_trusted_sieve_script(&name, self.data.session_id)
.map(|s| (s, name))
})
@@ -140,14 +138,13 @@ impl<T: SessionStream> Session<T> {
if !self.stream.is_tls() && self.instance.acceptor.is_tls() {
response.capabilities |= EXT_START_TLS;
}
- let ec = &self.core.core.smtp.session.extensions;
- let ac = &self.core.core.smtp.session.auth;
- let dc = &self.core.core.smtp.session.data;
+ let ec = &self.server.core.smtp.session.extensions;
+ let ac = &self.server.core.smtp.session.auth;
+ let dc = &self.server.core.smtp.session.data;
// Pipelining
if self
- .core
- .core
+ .server
.eval_if(&ec.pipelining, self, self.data.session_id)
.await
.unwrap_or(true)
@@ -157,8 +154,7 @@ impl<T: SessionStream> Session<T> {
// Chunking
if self
- .core
- .core
+ .server
.eval_if(&ec.chunking, self, self.data.session_id)
.await
.unwrap_or(true)
@@ -168,8 +164,7 @@ impl<T: SessionStream> Session<T> {
// Address Expansion
if self
- .core
- .core
+ .server
.eval_if(&ec.expn, self, self.data.session_id)
.await
.unwrap_or(false)
@@ -179,8 +174,7 @@ impl<T: SessionStream> Session<T> {
// Recipient Verification
if self
- .core
- .core
+ .server
.eval_if(&ec.vrfy, self, self.data.session_id)
.await
.unwrap_or(false)
@@ -190,8 +184,7 @@ impl<T: SessionStream> Session<T> {
// Require TLS
if self
- .core
- .core
+ .server
.eval_if(&ec.requiretls, self, self.data.session_id)
.await
.unwrap_or(true)
@@ -201,8 +194,7 @@ impl<T: SessionStream> Session<T> {
// DSN
if self
- .core
- .core
+ .server
.eval_if(&ec.dsn, self, self.data.session_id)
.await
.unwrap_or(false)
@@ -213,8 +205,7 @@ impl<T: SessionStream> Session<T> {
// Authentication
if self.data.authenticated_as.is_empty() {
response.auth_mechanisms = self
- .core
- .core
+ .server
.eval_if::<Mechanism, _>(&ac.mechanisms, self, self.data.session_id)
.await
.unwrap_or_default()
@@ -226,8 +217,7 @@ impl<T: SessionStream> Session<T> {
// Future release
if let Some(value) = self
- .core
- .core
+ .server
.eval_if::<Duration, _>(&ec.future_release, self, self.data.session_id)
.await
{
@@ -242,8 +232,7 @@ impl<T: SessionStream> Session<T> {
// Deliver By
if let Some(value) = self
- .core
- .core
+ .server
.eval_if::<Duration, _>(&ec.deliver_by, self, self.data.session_id)
.await
{
@@ -253,8 +242,7 @@ impl<T: SessionStream> Session<T> {
// Priority
if let Some(value) = self
- .core
- .core
+ .server
.eval_if::<MtPriority, _>(&ec.mt_priority, self, self.data.session_id)
.await
{
@@ -264,8 +252,7 @@ impl<T: SessionStream> Session<T> {
// Size
response.size = self
- .core
- .core
+ .server
.eval_if(&dc.max_message_size, self, self.data.session_id)
.await
.unwrap_or(25 * 1024 * 1024);
@@ -275,8 +262,7 @@ impl<T: SessionStream> Session<T> {
// No soliciting
if let Some(value) = self
- .core
- .core
+ .server
.eval_if::<String, _>(&ec.no_soliciting, self, self.data.session_id)
.await
{
diff --git a/crates/smtp/src/inbound/hooks/message.rs b/crates/smtp/src/inbound/hooks/message.rs
index 6ad3b44f..37720509 100644
--- a/crates/smtp/src/inbound/hooks/message.rs
+++ b/crates/smtp/src/inbound/hooks/message.rs
@@ -36,7 +36,7 @@ impl<T: SessionStream> Session<T> {
message: Option<&AuthenticatedMessage<'_>>,
queue_id: Option<QueueId>,
) -> Result<Vec<Modification>, FilterResponse> {
- let mta_hooks = &self.core.core.smtp.session.hooks;
+ let mta_hooks = &self.server.core.smtp.session.hooks;
if mta_hooks.is_empty() {
return Ok(Vec::new());
}
@@ -45,8 +45,7 @@ impl<T: SessionStream> Session<T> {
for mta_hook in mta_hooks {
if !mta_hook.run_on_stage.contains(&stage)
|| !self
- .core
- .core
+ .server
.eval_if(&mta_hook.enable, self, self.data.session_id)
.await
.unwrap_or(false)
diff --git a/crates/smtp/src/inbound/mail.rs b/crates/smtp/src/inbound/mail.rs
index 45428f9e..033b7d3d 100644
--- a/crates/smtp/src/inbound/mail.rs
+++ b/crates/smtp/src/inbound/mail.rs
@@ -54,7 +54,7 @@ impl<T: SessionStream> Session<T> {
} else if self.data.iprev.is_none() && self.params.iprev.verify() {
let time = Instant::now();
let iprev = self
- .core
+ .server
.core
.smtp
.resolvers
@@ -122,10 +122,9 @@ impl<T: SessionStream> Session<T> {
// Check whether the address is allowed
if !self
- .core
- .core
+ .server
.eval_if::<bool, _>(
- &self.core.core.smtp.session.mail.is_allowed,
+ &self.server.core.smtp.session.mail.is_allowed,
self,
self.data.session_id,
)
@@ -145,17 +144,15 @@ impl<T: SessionStream> Session<T> {
// Sieve filtering
if let Some((script, script_id)) = self
- .core
- .core
+ .server
.eval_if::<String, _>(
- &self.core.core.smtp.session.mail.script,
+ &self.server.core.smtp.session.mail.script,
self,
self.data.session_id,
)
.await
.and_then(|name| {
- self.core
- .core
+ self.server
.get_trusted_sieve_script(&name, self.data.session_id)
.map(|s| (s, name))
})
@@ -199,10 +196,9 @@ impl<T: SessionStream> Session<T> {
// Address rewriting
if let Some(new_address) = self
- .core
- .core
+ .server
.eval_if::<String, _>(
- &self.core.core.smtp.session.mail.rewrite,
+ &self.server.core.smtp.session.mail.rewrite,
self,
self.data.session_id,
)
@@ -258,12 +254,11 @@ impl<T: SessionStream> Session<T> {
}
// Validate parameters
- let config = &self.core.core.smtp.session.extensions;
- let config_data = &self.core.core.smtp.session.data;
+ let config = &self.server.core.smtp.session.extensions;
+ let config_data = &self.server.core.smtp.session.data;
if (from.flags & MAIL_REQUIRETLS) != 0
&& !self
- .core
- .core
+ .server
.eval_if(&config.requiretls, self, self.data.session_id)
.await
.unwrap_or(false)
@@ -279,8 +274,7 @@ impl<T: SessionStream> Session<T> {
}
if (from.flags & (MAIL_BY_NOTIFY | MAIL_BY_RETURN)) != 0 {
if let Some(duration) = self
- .core
- .core
+ .server
.eval_if::<Duration, _>(&config.deliver_by, self, self.data.session_id)
.await
{
@@ -320,8 +314,7 @@ impl<T: SessionStream> Session<T> {
}
if from.mt_priority != 0 {
if self
- .core
- .core
+ .server
.eval_if::<MtPriority, _>(&config.mt_priority, self, self.data.session_id)
.await
.is_some()
@@ -351,8 +344,7 @@ impl<T: SessionStream> Session<T> {
if from.size > 0
&& from.size
> self
- .core
- .core
+ .server
.eval_if(&config_data.max_message_size, self, self.data.session_id)
.await
.unwrap_or(25 * 1024 * 1024)
@@ -370,8 +362,7 @@ impl<T: SessionStream> Session<T> {
}
if from.hold_for != 0 || from.hold_until != 0 {
if let Some(max_hold) = self
- .core
- .core
+ .server
.eval_if::<Duration, _>(&config.future_release, self, self.data.session_id)
.await
{
@@ -419,8 +410,7 @@ impl<T: SessionStream> Session<T> {
}
if has_dsn
&& !self
- .core
- .core
+ .server
.eval_if(&config.dsn, self, self.data.session_id)
.await
.unwrap_or(false)
@@ -438,7 +428,7 @@ impl<T: SessionStream> Session<T> {
let time = Instant::now();
let mail_from = self.data.mail_from.as_ref().unwrap();
let spf_output = if !mail_from.address.is_empty() {
- self.core
+ self.server
.core
.smtp
.resolvers
@@ -452,7 +442,7 @@ impl<T: SessionStream> Session<T> {
)
.await
} else {
- self.core
+ self.server
.core
.smtp
.resolvers
@@ -542,10 +532,9 @@ impl<T: SessionStream> Session<T> {
// Send report
if let (Some(recipient), Some(rate)) = (
spf_output.report_address(),
- self.core
- .core
+ self.server
.eval_if::<Rate, _>(
- &self.core.core.smtp.report.spf.send,
+ &self.server.core.smtp.report.spf.send,
self,
self.data.session_id,
)
diff --git a/crates/smtp/src/inbound/milter/message.rs b/crates/smtp/src/inbound/milter/message.rs
index 1bcbde1b..0b369161 100644
--- a/crates/smtp/src/inbound/milter/message.rs
+++ b/crates/smtp/src/inbound/milter/message.rs
@@ -35,7 +35,7 @@ impl<T: SessionStream> Session<T> {
stage: Stage,
message: Option<&AuthenticatedMessage<'_>>,
) -> Result<Vec<Modification>, FilterResponse> {
- let milters = &self.core.core.smtp.session.milters;
+ let milters = &self.server.core.smtp.session.milters;
if milters.is_empty() {
return Ok(Vec::new());
}
@@ -44,8 +44,7 @@ impl<T: SessionStream> Session<T> {
for milter in milters {
if !milter.run_on_stage.contains(&stage)
|| !self
- .core
- .core
+ .server
.eval_if(&milter.enable, self, self.data.session_id)
.await
.unwrap_or(false)
@@ -170,9 +169,9 @@ impl<T: SessionStream> Session<T> {
client
.into_tls(
if !milter.tls_allow_invalid_certs {
- &self.core.inner.connectors.pki_verify
+ &self.server.inner.data.smtp_connectors.pki_verify
} else {
- &self.core.inner.connectors.dummy_verify
+ &self.server.inner.data.smtp_connectors.dummy_verify
},
&milter.hostname,
)
diff --git a/crates/smtp/src/inbound/rcpt.rs b/crates/smtp/src/inbound/rcpt.rs
index 89c7b8dc..aab0be1e 100644
--- a/crates/smtp/src/inbound/rcpt.rs
+++ b/crates/smtp/src/inbound/rcpt.rs
@@ -77,25 +77,23 @@ impl<T: SessionStream> Session<T> {
// Address rewriting and Sieve filtering
let rcpt_script = self
- .core
- .core
+ .server
.eval_if::<String, _>(
- &self.core.core.smtp.session.rcpt.script,
+ &self.server.core.smtp.session.rcpt.script,
self,
self.data.session_id,
)
.await
.and_then(|name| {
- self.core
- .core
+ self.server
.get_trusted_sieve_script(&name, self.data.session_id)
.map(|s| (s.clone(), name))
});
if rcpt_script.is_some()
- || !self.core.core.smtp.session.rcpt.rewrite.is_empty()
+ || !self.server.core.smtp.session.rcpt.rewrite.is_empty()
|| self
- .core
+ .server
.core
.smtp
.session
@@ -146,10 +144,9 @@ impl<T: SessionStream> Session<T> {
// Address rewriting
if let Some(new_address) = self
- .core
- .core
+ .server
.eval_if::<String, _>(
- &self.core.core.smtp.session.rcpt.rewrite,
+ &self.server.core.smtp.session.rcpt.rewrite,
self,
self.data.session_id,
)
@@ -187,22 +184,20 @@ impl<T: SessionStream> Session<T> {
// Verify address
let rcpt = self.data.rcpt_to.last().unwrap();
if let Some(directory) = self
- .core
- .core
+ .server
.eval_if::<String, _>(
- &self.core.core.smtp.session.rcpt.directory,
+ &self.server.core.smtp.session.rcpt.directory,
self,
self.data.session_id,
)
.await
- .and_then(|name| self.core.core.get_directory(&name))
+ .and_then(|name| self.server.get_directory(&name))
{
match directory.is_local_domain(&rcpt.domain).await {
Ok(is_local_domain) => {
if is_local_domain {
match self
- .core
- .core
+ .server
.rcpt(directory, &rcpt.address_lcase, self.data.session_id)
.await
{
@@ -233,10 +228,9 @@ impl<T: SessionStream> Session<T> {
}
}
} else if !self
- .core
- .core
+ .server
.eval_if(
- &self.core.core.smtp.session.rcpt.relay,
+ &self.server.core.smtp.session.rcpt.relay,
self,
self.data.session_id,
)
@@ -266,10 +260,9 @@ impl<T: SessionStream> Session<T> {
}
}
} else if !self
- .core
- .core
+ .server
.eval_if(
- &self.core.core.smtp.session.rcpt.relay,
+ &self.server.core.smtp.session.rcpt.relay,
self,
self.data.session_id,
)
@@ -315,12 +308,7 @@ impl<T: SessionStream> Session<T> {
if self.data.rcpt_errors < self.params.rcpt_errors_max {
Ok(())
} else {
- match self
- .core
- .core
- .is_rcpt_fail2banned(self.data.remote_ip)
- .await
- {
+ match self.server.is_rcpt_fail2banned(self.data.remote_ip).await {
Ok(true) => {
trc::event!(
Security(SecurityEvent::BruteForceBan),
diff --git a/crates/smtp/src/inbound/session.rs b/crates/smtp/src/inbound/session.rs
index b0c91672..02648b5f 100644
--- a/crates/smtp/src/inbound/session.rs
+++ b/crates/smtp/src/inbound/session.rs
@@ -84,10 +84,9 @@ impl<T: SessionStream> Session<T> {
initial_response,
} => {
let auth: u64 = self
- .core
- .core
+ .server
.eval_if::<Mechanism, _>(
- &self.core.core.smtp.session.auth.mechanisms,
+ &self.server.core.smtp.session.auth.mechanisms,
self,
self.data.session_id,
)
diff --git a/crates/smtp/src/inbound/spawn.rs b/crates/smtp/src/inbound/spawn.rs
index e6892fc1..d7e08f8b 100644
--- a/crates/smtp/src/inbound/spawn.rs
+++ b/crates/smtp/src/inbound/spawn.rs
@@ -8,6 +8,7 @@ use std::time::Instant;
use common::{
config::smtp::session::Stage,
+ core::BuildServer,
listener::{self, SessionManager, SessionStream},
};
use tokio_rustls::server::TlsStream;
@@ -15,7 +16,6 @@ use trc::{SecurityEvent, SmtpEvent};
use crate::{
core::{Session, SessionData, SessionParameters, SmtpSessionManager, State},
- queue, reporting,
scripts::ScriptResult,
};
@@ -27,7 +27,7 @@ impl SessionManager for SmtpSessionManager {
// Create session
let mut session = Session {
hostname: String::new(),
- core: self.inner.into(),
+ server: self.inner.build_server(),
instance: session.instance,
state: State::default(),
stream: session.stream,
@@ -59,19 +59,23 @@ impl SessionManager for SmtpSessionManager {
#[allow(clippy::manual_async_fn)]
fn shutdown(&self) -> impl std::future::Future<Output = ()> + Send {
async {
- let _ = self.inner.inner.queue_tx.send(queue::Event::Stop).await;
let _ = self
.inner
+ .ipc
+ .queue_tx
+ .send(common::ipc::QueueEvent::Stop)
+ .await;
+ let _ = self
.inner
+ .ipc
.report_tx
- .send(reporting::Event::Stop)
+ .send(common::ipc::ReportingEvent::Stop)
.await;
let _ = self
.inner
- .inner
.ipc
.delivery_tx
- .send(common::DeliveryEvent::Stop)
+ .send(common::ipc::DeliveryEvent::Stop)
.await;
}
}
@@ -81,17 +85,15 @@ impl<T: SessionStream> Session<T> {
pub async fn init_conn(&mut self) -> bool {
self.eval_session_params().await;
- let config = &self.core.core.smtp.session.connect;
+ let config = &self.server.core.smtp.session.connect;
// Sieve filtering
if let Some((script, script_id)) = self
- .core
- .core
+ .server
.eval_if::<String, _>(&config.script, self, self.data.session_id)
.await
.and_then(|name| {
- self.core
- .core
+ self.server
.get_trusted_sieve_script(&name, self.data.session_id)
.map(|s| (s, name))
})
@@ -123,8 +125,7 @@ impl<T: SessionStream> Session<T> {
// Obtain hostname
self.hostname = self
- .core
- .core
+ .server
.eval_if::<String, _>(&config.hostname, self, self.data.session_id)
.await
.unwrap_or_default();
@@ -138,8 +139,7 @@ impl<T: SessionStream> Session<T> {
// Obtain greeting
let greeting = self
- .core
- .core
+ .server
.eval_if::<String, _>(&config.greeting, self, self.data.session_id)
.await
.filter(|g| !g.is_empty())
@@ -194,10 +194,7 @@ impl<T: SessionStream> Session<T> {
.await
.ok();
- match self
- .core
- .core
- .is_loiter_fail2banned(self.data.remote_ip)
+ match self.server.is_loiter_fail2banned(self.data.remote_ip)
.await
{
Ok(true) => {
@@ -277,7 +274,7 @@ impl<T: SessionStream> Session<T> {
state: self.state,
data: self.data,
instance: self.instance,
- core: self.core,
+ server: self.server,
in_flight: self.in_flight,
params: self.params,
})
diff --git a/crates/smtp/src/inbound/vrfy.rs b/crates/smtp/src/inbound/vrfy.rs
index 0c23687d..a99823d0 100644
--- a/crates/smtp/src/inbound/vrfy.rs
+++ b/crates/smtp/src/inbound/vrfy.rs
@@ -13,20 +13,18 @@ use std::fmt::Write;
impl<T: SessionStream> Session<T> {
pub async fn handle_vrfy(&mut self, address: String) -> Result<(), ()> {
match self
- .core
- .core
+ .server
.eval_if::<String, _>(
- &self.core.core.smtp.session.rcpt.directory,
+ &self.server.core.smtp.session.rcpt.directory,
self,
self.data.session_id,
)
.await
- .and_then(|name| self.core.core.get_directory(&name))
+ .and_then(|name| self.server.get_directory(&name))
{
Some(directory) if self.params.can_vrfy => {
match self
- .core
- .core
+ .server
.vrfy(directory, &address.to_lowercase(), self.data.session_id)
.await
{
@@ -88,20 +86,18 @@ impl<T: SessionStream> Session<T> {
pub async fn handle_expn(&mut self, address: String) -> Result<(), ()> {
match self
- .core
- .core
+ .server
.eval_if::<String, _>(
- &self.core.core.smtp.session.rcpt.directory,
+ &self.server.core.smtp.session.rcpt.directory,
self,
self.data.session_id,
)
.await
- .and_then(|name| self.core.core.get_directory(&name))
+ .and_then(|name| self.server.get_directory(&name))
{
Some(directory) if self.params.can_expn => {
match self
- .core
- .core
+ .server
.expn(directory, &address.to_lowercase(), self.data.session_id)
.await
{
diff --git a/crates/smtp/src/lib.rs b/crates/smtp/src/lib.rs
index eaef7d5d..0713f9bb 100644
--- a/crates/smtp/src/lib.rs
+++ b/crates/smtp/src/lib.rs
@@ -4,17 +4,14 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
-use crate::core::{throttle::ThrottleKeyHasherBuilder, TlsConnectors};
-use core::{Inner, SmtpInstance, SMTP};
use std::sync::Arc;
-use common::{config::scripts::ScriptCache, Ipc, SharedCore};
-use dashmap::DashMap;
-use mail_send::smtp::tls::build_tls_connector;
+use common::{
+ manager::boot::{BootManager, IpcReceivers},
+ Inner,
+};
use queue::manager::SpawnQueue;
use reporting::scheduler::SpawnReport;
-use tokio::sync::mpsc;
-use utils::{config::Config, snowflake::SnowflakeIdGenerator};
pub mod core;
pub mod inbound;
@@ -23,54 +20,26 @@ pub mod queue;
pub mod reporting;
pub mod scripts;
-impl SMTP {
- pub async fn init(
- config: &mut Config,
- core: SharedCore,
- ipc: Ipc,
- span_id_gen: Arc<SnowflakeIdGenerator>,
- ) -> SmtpInstance {
- // Build inner
- let capacity = config.property("cache.capacity").unwrap_or(2);
- let shard = config
- .property::<u64>("cache.shard")
- .unwrap_or(32)
- .next_power_of_two() as usize;
- let (queue_tx, queue_rx) = mpsc::channel(1024);
- let (report_tx, report_rx) = mpsc::channel(1024);
- let inner = Inner {
- session_throttle: DashMap::with_capacity_and_hasher_and_shard_amount(
- capacity,
- ThrottleKeyHasherBuilder::default(),
- shard,
- ),
- queue_throttle: DashMap::with_capacity_and_hasher_and_shard_amount(
- capacity,
- ThrottleKeyHasherBuilder::default(),
- shard,
- ),
- queue_tx,
- report_tx,
- queue_id_gen: config
- .property::<u64>("cluster.node-id")
- .map(SnowflakeIdGenerator::with_node_id)
- .unwrap_or_default(),
- span_id_gen,
- connectors: TlsConnectors {
- pki_verify: build_tls_connector(false),
- dummy_verify: build_tls_connector(true),
- },
- ipc,
- script_cache: ScriptCache::parse(config),
- };
- let inner = SmtpInstance::new(core, inner);
+pub trait StartQueueManager {
+ fn start_queue_manager(&mut self);
+}
+
+pub trait SpawnQueueManager {
+ fn spawn_queue_manager(&mut self, inner: Arc<Inner>);
+}
+impl StartQueueManager for BootManager {
+ fn start_queue_manager(&mut self) {
+ self.ipc_rxs.spawn_queue_manager(self.inner.clone());
+ }
+}
+
+impl SpawnQueueManager for IpcReceivers {
+ fn spawn_queue_manager(&mut self, inner: Arc<Inner>) {
// Spawn queue manager
- queue_rx.spawn(inner.clone());
+ self.queue_rx.take().unwrap().spawn(inner.clone());
// Spawn report manager
- report_rx.spawn(inner.clone());
-
- inner
+ self.report_rx.take().unwrap().spawn(inner);
}
}
diff --git a/crates/smtp/src/outbound/client.rs b/crates/smtp/src/outbound/client.rs
index f064781a..5391cc52 100644
--- a/crates/smtp/src/outbound/client.rs
+++ b/crates/smtp/src/outbound/client.rs
@@ -177,10 +177,8 @@ impl<T: AsyncRead + AsyncWrite + Unpin> SmtpClient<T> {
params: &SessionParams<'_>,
) -> Result<(), Status<(), Error>> {
match params
- .core
- .core
- .storage
- .blob
+ .server
+ .blob_store()
.get_blob(message.blob_hash.as_slice(), 0..usize::MAX)
.await
{
diff --git a/crates/smtp/src/outbound/dane/dnssec.rs b/crates/smtp/src/outbound/dane/dnssec.rs
index 1c2f11e3..de89c1f3 100644
--- a/crates/smtp/src/outbound/dane/dnssec.rs
+++ b/crates/smtp/src/outbound/dane/dnssec.rs
@@ -4,7 +4,10 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
-use common::config::smtp::resolver::{Tlsa, TlsaEntry};
+use common::{
+ config::smtp::resolver::{Tlsa, TlsaEntry},
+ Server,
+};
use mail_auth::{
common::{lru::DnsCache, resolver::IntoFqdn},
hickory_resolver::{
@@ -16,14 +19,27 @@ use mail_auth::{
Name,
},
};
-use std::sync::Arc;
+use std::{future::Future, sync::Arc};
-use crate::core::SMTP;
+pub trait TlsaLookup: Sync + Send {
+ fn tlsa_lookup<'x>(
+ &self,
+ key: impl IntoFqdn<'x> + Sync + Send,
+ ) -> impl Future<Output = mail_auth::Result<Option<Arc<Tlsa>>>> + Send;
-impl SMTP {
- pub async fn tlsa_lookup<'x>(
+ #[cfg(feature = "test_mode")]
+ fn tlsa_add<'x>(
&self,
key: impl IntoFqdn<'x>,
+ value: impl Into<Arc<Tlsa>>,
+ valid_until: std::time::Instant,
+ );
+}
+
+impl TlsaLookup for Server {
+ async fn tlsa_lookup<'x>(
+ &self,
+ key: impl IntoFqdn<'x> + Sync + Send,
) -> mail_auth::Result<Option<Arc<Tlsa>>> {
let key = key.into_fqdn();
if let Some(value) = self.core.smtp.resolvers.cache.tlsa.get(key.as_ref()) {
@@ -102,7 +118,7 @@ impl SMTP {
}
#[cfg(feature = "test_mode")]
- pub fn tlsa_add<'x>(
+ fn tlsa_add<'x>(
&self,
key: impl IntoFqdn<'x>,
value: impl Into<Arc<Tlsa>>,
diff --git a/crates/smtp/src/outbound/delivery.rs b/crates/smtp/src/outbound/delivery.rs
index 8fe5c968..2a8694f2 100644
--- a/crates/smtp/src/outbound/delivery.rs
+++ b/crates/smtp/src/outbound/delivery.rs
@@ -5,12 +5,21 @@
*/
use crate::outbound::client::{from_error_status, from_mail_send_error, SmtpClient};
+use crate::outbound::dane::dnssec::TlsaLookup;
+use crate::outbound::lookup::DnsLookup;
+use crate::outbound::mta_sts::lookup::MtaStsLookup;
use crate::outbound::mta_sts::verify::VerifyPolicy;
use crate::outbound::{client::StartTlsResult, dane::verify::TlsaVerify};
+use crate::queue::dsn::SendDsn;
+use crate::queue::spool::SmtpSpool;
+use crate::queue::throttle::IsAllowed;
+use crate::reporting::SmtpReporting;
use common::config::{
server::ServerProtocol,
smtp::{queue::RequireOptional, report::AggregateFrequency},
};
+use common::ipc::{OnHold, PolicyType, QueueEvent, TlsEvent};
+use common::Server;
use mail_auth::{
mta_sts::TlsRpt,
report::tlsrpt::{FailureDetails, ResultType},
@@ -20,31 +29,28 @@ use std::{
net::{IpAddr, Ipv4Addr, SocketAddr},
time::{Duration, Instant},
};
-use store::write::{now, BatchBuilder, QueueClass, QueueEvent, ValueClass};
+use store::write::{now, BatchBuilder, QueueClass, ValueClass};
use trc::{DaneEvent, DeliveryEvent, MtaStsEvent, ServerEvent, TlsRptEvent};
use crate::{
- core::SMTP,
queue::{ErrorDetails, Message},
- reporting::{tls::TlsRptOptions, PolicyType, TlsEvent},
+ reporting::tls::TlsRptOptions,
};
use super::{lookup::ToNextHop, mta_sts, session::SessionParams, NextHop, TlsStrategy};
-use crate::queue::{
- throttle, DeliveryAttempt, Domain, Error, Event, OnHold, QueueEnvelope, Status,
-};
+use crate::queue::{throttle, DeliveryAttempt, Domain, Error, QueueEnvelope, Status};
impl DeliveryAttempt {
- pub async fn try_deliver(mut self, core: SMTP) {
+ pub async fn try_deliver(mut self, server: Server) {
tokio::spawn(async move {
// Lock message
- if let Some(event) = core.try_lock_event(self.event).await {
+ if let Some(event) = server.try_lock_event(self.event).await {
self.event = event;
// Fetch message
- if let Some(mut message) = core.read_message(self.event.queue_id).await {
+ if let Some(mut message) = server.read_message(self.event.queue_id).await {
// Generate span id
- message.span_id = core.inner.span_id_gen.generate().unwrap_or_else(now);
+ message.span_id = server.inner.data.span_id_gen.generate().unwrap_or_else(now);
let span_id = message.span_id;
trc::event!(
@@ -76,7 +82,7 @@ impl DeliveryAttempt {
// Attempt delivery
let start_time = Instant::now();
- self.deliver_task(core, message).await;
+ self.deliver_task(server, message).await;
trc::event!(
Delivery(DeliveryEvent::AttemptEnd),
@@ -86,12 +92,14 @@ impl DeliveryAttempt {
} else {
// Message no longer exists, delete queue event.
let mut batch = BatchBuilder::new();
- batch.clear(ValueClass::Queue(QueueClass::MessageEvent(QueueEvent {
- due: self.event.due,
- queue_id: self.event.queue_id,
- })));
+ batch.clear(ValueClass::Queue(QueueClass::MessageEvent(
+ store::write::QueueEvent {
+ due: self.event.due,
+ queue_id: self.event.queue_id,
+ },
+ )));
- if let Err(err) = core.core.storage.data.write(batch.build()).await {
+ if let Err(err) = server.store().write(batch.build()).await {
trc::error!(err
.details("Failed to delete queue event.")
.caused_by(trc::location!()));
@@ -101,13 +109,13 @@ impl DeliveryAttempt {
});
}
- async fn deliver_task(mut self, core: SMTP, mut message: Message) {
+ async fn deliver_task(mut self, server: Server, mut message: Message) {
// Check that the message still has recipients to be delivered
let has_pending_delivery = message.has_pending_delivery();
let span_id = message.span_id;
// Send any due Delivery Status Notifications
- core.send_dsn(&mut message).await;
+ server.send_dsn(&mut message).await;
if has_pending_delivery {
// Re-queue the message if its not yet due for delivery
@@ -115,9 +123,16 @@ impl DeliveryAttempt {
if due > now() {
// Save changes
message
- .save_changes(&core, self.event.due.into(), due.into())
+ .save_changes(&server, self.event.due.into(), due.into())
.await;
- if core.inner.queue_tx.send(Event::Reload).await.is_err() {
+ if server
+ .inner
+ .ipc
+ .queue_tx
+ .send(QueueEvent::Reload)
+ .await
+ .is_err()
+ {
trc::event!(
Server(ServerEvent::ThreadError),
Reason = "Channel closed.",
@@ -135,8 +150,15 @@ impl DeliveryAttempt {
);
// All message recipients expired, do not re-queue. (DSN has been already sent)
- message.remove(&core, self.event.due).await;
- if core.inner.queue_tx.send(Event::Reload).await.is_err() {
+ message.remove(&server, self.event.due).await;
+ if server
+ .inner
+ .ipc
+ .queue_tx
+ .send(QueueEvent::Reload)
+ .await
+ .is_err()
+ {
trc::event!(
Server(ServerEvent::ThreadError),
Reason = "Channel closed.",
@@ -149,8 +171,8 @@ impl DeliveryAttempt {
}
// Throttle sender
- for throttle in &core.core.smtp.queue.throttle.sender {
- if let Err(err) = core
+ for throttle in &server.core.smtp.queue.throttle.sender {
+ if let Err(err) = server
.is_allowed(throttle, &message, &mut self.in_flight, message.span_id)
.await
{
@@ -158,7 +180,7 @@ impl DeliveryAttempt {
throttle::Error::Concurrency { limiter } => {
// Save changes to disk
let next_due = message.next_event_after(now());
- message.save_changes(&core, None, None).await;
+ message.save_changes(&server, None, None).await;
trc::event!(
Delivery(DeliveryEvent::ConcurrencyLimitExceeded),
@@ -166,7 +188,7 @@ impl DeliveryAttempt {
SpanId = span_id,
);
- Event::OnHold(OnHold {
+ QueueEvent::OnHold(OnHold {
next_due,
limiters: vec![limiter],
message: self.event,
@@ -187,14 +209,14 @@ impl DeliveryAttempt {
);
message
- .save_changes(&core, self.event.due.into(), next_event.into())
+ .save_changes(&server, self.event.due.into(), next_event.into())
.await;
- Event::Reload
+ QueueEvent::Reload
}
};
- if core.inner.queue_tx.send(event).await.is_err() {
+ if server.inner.ipc.queue_tx.send(event).await.is_err() {
trc::event!(
Server(ServerEvent::ThreadError),
Reason = "Channel closed.",
@@ -206,7 +228,7 @@ impl DeliveryAttempt {
}
}
- let queue_config = &core.core.smtp.queue;
+ let queue_config = &server.core.smtp.queue;
let mut on_hold = Vec::new();
let no_ip = IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0));
let mut recipients = std::mem::take(&mut message.recipients);
@@ -232,7 +254,7 @@ impl DeliveryAttempt {
// Throttle recipient domain
let mut in_flight = Vec::new();
for throttle in &queue_config.throttle.rcpt {
- if let Err(err) = core
+ if let Err(err) = server
.is_allowed(throttle, &envelope, &mut in_flight, message.span_id)
.await
{
@@ -249,24 +271,22 @@ impl DeliveryAttempt {
}
// Obtain next hop
- let (mut remote_hosts, is_smtp) = match core
- .core
+ let (mut remote_hosts, is_smtp) = match server
.eval_if::<String, _>(&queue_config.next_hop, &envelope, message.span_id)
.await
- .and_then(|name| core.core.get_relay_host(&name, message.span_id))
+ .and_then(|name| server.get_relay_host(&name, message.span_id))
{
Some(next_hop) if next_hop.protocol == ServerProtocol::Http => {
// Deliver message locally
let delivery_result = message
.deliver_local(
recipients.iter_mut().filter(|r| r.domain_idx == domain_idx),
- &core.inner.ipc.delivery_tx,
+ &server.inner.ipc.delivery_tx,
)
.await;
// Update status for the current domain and continue with the next one
- let schedule = core
- .core
+ let schedule = server
.eval_if::<Vec<Duration>, _>(
&queue_config.retry,
&envelope,
@@ -286,23 +306,24 @@ impl DeliveryAttempt {
// Prepare TLS strategy
let mut tls_strategy = TlsStrategy {
- mta_sts: core
- .core
+ mta_sts: server
.eval_if(&queue_config.tls.mta_sts, &envelope, message.span_id)
.await
.unwrap_or(RequireOptional::Optional),
..Default::default()
};
- let allow_invalid_certs = core
- .core
+ let allow_invalid_certs = server
.eval_if(&queue_config.tls.invalid_certs, &envelope, message.span_id)
.await
.unwrap_or(false);
// Obtain TLS reporting
- let tls_report = match core
- .core
- .eval_if(&core.core.smtp.report.tls.send, &envelope, message.span_id)
+ let tls_report = match server
+ .eval_if(
+ &server.core.smtp.report.tls.send,
+ &envelope,
+ message.span_id,
+ )
.await
.unwrap_or(AggregateFrequency::Never)
{
@@ -312,7 +333,7 @@ impl DeliveryAttempt {
if is_smtp =>
{
let time = Instant::now();
- match core
+ match server
.core
.smtp
.resolvers
@@ -357,10 +378,10 @@ impl DeliveryAttempt {
// Obtain MTA-STS policy for domain
let mta_sts_policy = if tls_strategy.try_mta_sts() && is_smtp {
let time = Instant::now();
- match core
+ match server
.lookup_mta_sts_policy(
&domain.domain,
- core.core
+ server
.eval_if(&queue_config.timeout.mta_sts, &envelope, message.span_id)
.await
.unwrap_or_else(|| Duration::from_secs(10 * 60)),
@@ -390,7 +411,7 @@ impl DeliveryAttempt {
match &err {
mta_sts::Error::Dns(mail_auth::Error::DnsRecordNotFound(_)) => {
if strict {
- core.schedule_report(TlsEvent {
+ server.schedule_report(TlsEvent {
policy: PolicyType::Sts(None),
domain: domain.domain.to_string(),
failure: FailureDetails::new(ResultType::Other)
@@ -406,16 +427,17 @@ impl DeliveryAttempt {
}
mta_sts::Error::Dns(mail_auth::Error::DnsError(_)) => (),
_ => {
- core.schedule_report(TlsEvent {
- policy: PolicyType::Sts(None),
- domain: domain.domain.to_string(),
- failure: FailureDetails::new(&err)
- .with_failure_reason_code(err.to_string())
- .into(),
- tls_record: tls_report.record.clone(),
- interval: tls_report.interval,
- })
- .await;
+ server
+ .schedule_report(TlsEvent {
+ policy: PolicyType::Sts(None),
+ domain: domain.domain.to_string(),
+ failure: FailureDetails::new(&err)
+ .with_failure_reason_code(err.to_string())
+ .into(),
+ tls_record: tls_report.record.clone(),
+ interval: tls_report.interval,
+ })
+ .await;
}
}
}
@@ -463,8 +485,7 @@ impl DeliveryAttempt {
}
if strict {
- let schedule = core
- .core
+ let schedule = server
.eval_if::<Vec<Duration>, _>(
&queue_config.retry,
&envelope,
@@ -488,7 +509,14 @@ impl DeliveryAttempt {
if is_smtp && remote_hosts.is_empty() {
// Lookup MX
let time = Instant::now();
- mx_list = match core.core.smtp.resolvers.dns.mx_lookup(&domain.domain).await {
+ mx_list = match server
+ .core
+ .smtp
+ .resolvers
+ .dns
+ .mx_lookup(&domain.domain)
+ .await
+ {
Ok(mx) => mx,
Err(err) => {
trc::event!(
@@ -499,8 +527,7 @@ impl DeliveryAttempt {
Elapsed = time.elapsed(),
);
- let schedule = core
- .core
+ let schedule = server
.eval_if::<Vec<Duration>, _>(
&queue_config.retry,
&envelope,
@@ -515,7 +542,7 @@ impl DeliveryAttempt {
if let Some(remote_hosts_) = mx_list.to_remote_hosts(
&domain.domain,
- core.core
+ server
.eval_if(&queue_config.max_mx, &envelope, message.span_id)
.await
.unwrap_or(5),
@@ -539,8 +566,7 @@ impl DeliveryAttempt {
Elapsed = time.elapsed(),
);
- let schedule = core
- .core
+ let schedule = server
.eval_if::<Vec<Duration>, _>(
&queue_config.retry,
&envelope,
@@ -559,8 +585,7 @@ impl DeliveryAttempt {
}
// Try delivering message
- let max_multihomed = core
- .core
+ let max_multihomed = server
.eval_if(&queue_config.max_multihomed, &envelope, message.span_id)
.await
.unwrap_or(2);
@@ -573,17 +598,18 @@ impl DeliveryAttempt {
if !mta_sts_policy.verify(envelope.mx) {
// Report MTA-STS failed verification
if let Some(tls_report) = &tls_report {
- core.schedule_report(TlsEvent {
- policy: mta_sts_policy.into(),
- domain: domain.domain.to_string(),
- failure: FailureDetails::new(ResultType::ValidationFailure)
- .with_receiving_mx_hostname(envelope.mx)
- .with_failure_reason_code("MX not authorized by policy.")
- .into(),
- tls_record: tls_report.record.clone(),
- interval: tls_report.interval,
- })
- .await;
+ server
+ .schedule_report(TlsEvent {
+ policy: mta_sts_policy.into(),
+ domain: domain.domain.to_string(),
+ failure: FailureDetails::new(ResultType::ValidationFailure)
+ .with_receiving_mx_hostname(envelope.mx)
+ .with_failure_reason_code("MX not authorized by policy.")
+ .into(),
+ tls_record: tls_report.record.clone(),
+ interval: tls_report.interval,
+ })
+ .await;
}
trc::event!(
@@ -624,7 +650,7 @@ impl DeliveryAttempt {
// Obtain source and remote IPs
let time = Instant::now();
- let resolve_result = match core
+ let resolve_result = match server
.resolve_host(remote_host, &envelope, max_multihomed, message.span_id)
.await
{
@@ -661,13 +687,11 @@ impl DeliveryAttempt {
};
// Update TLS strategy
- tls_strategy.dane = core
- .core
+ tls_strategy.dane = server
.eval_if(&queue_config.tls.dane, &envelope, message.span_id)
.await
.unwrap_or(RequireOptional::Optional);
- tls_strategy.tls = core
- .core
+ tls_strategy.tls = server
.eval_if(&queue_config.tls.start, &envelope, message.span_id)
.await
.unwrap_or(RequireOptional::Optional);
@@ -676,7 +700,10 @@ impl DeliveryAttempt {
let dane_policy = if tls_strategy.try_dane() && is_smtp {
let time = Instant::now();
let strict = tls_strategy.is_dane_required();
- match core.tlsa_lookup(format!("_25._tcp.{}.", envelope.mx)).await {
+ match server
+ .tlsa_lookup(format!("_25._tcp.{}.", envelope.mx))
+ .await
+ {
Ok(Some(tlsa)) => {
if tlsa.has_end_entities {
trc::event!(
@@ -703,17 +730,18 @@ impl DeliveryAttempt {
// Report invalid TLSA record
if let Some(tls_report) = &tls_report {
- core.schedule_report(TlsEvent {
- policy: tlsa.into(),
- domain: domain.domain.to_string(),
- failure: FailureDetails::new(ResultType::TlsaInvalid)
- .with_receiving_mx_hostname(envelope.mx)
- .with_failure_reason_code("Invalid TLSA record.")
- .into(),
- tls_record: tls_report.record.clone(),
- interval: tls_report.interval,
- })
- .await;
+ server
+ .schedule_report(TlsEvent {
+ policy: tlsa.into(),
+ domain: domain.domain.to_string(),
+ failure: FailureDetails::new(ResultType::TlsaInvalid)
+ .with_receiving_mx_hostname(envelope.mx)
+ .with_failure_reason_code("Invalid TLSA record.")
+ .into(),
+ tls_record: tls_report.record.clone(),
+ interval: tls_report.interval,
+ })
+ .await;
}
if strict {
@@ -740,19 +768,20 @@ impl DeliveryAttempt {
if strict {
// Report DANE required
if let Some(tls_report) = &tls_report {
- core.schedule_report(TlsEvent {
- policy: PolicyType::Tlsa(None),
- domain: domain.domain.to_string(),
- failure: FailureDetails::new(ResultType::DaneRequired)
- .with_receiving_mx_hostname(envelope.mx)
- .with_failure_reason_code(
- "No TLSA DNSSEC records found.",
- )
- .into(),
- tls_record: tls_report.record.clone(),
- interval: tls_report.interval,
- })
- .await;
+ server
+ .schedule_report(TlsEvent {
+ policy: PolicyType::Tlsa(None),
+ domain: domain.domain.to_string(),
+ failure: FailureDetails::new(ResultType::DaneRequired)
+ .with_receiving_mx_hostname(envelope.mx)
+ .with_failure_reason_code(
+ "No TLSA DNSSEC records found.",
+ )
+ .into(),
+ tls_record: tls_report.record.clone(),
+ interval: tls_report.interval,
+ })
+ .await;
}
last_status =
@@ -792,19 +821,22 @@ impl DeliveryAttempt {
last_status = if not_found {
// Report DANE required
if let Some(tls_report) = &tls_report {
- core.schedule_report(TlsEvent {
- policy: PolicyType::Tlsa(None),
- domain: domain.domain.to_string(),
- failure: FailureDetails::new(ResultType::DaneRequired)
+ server
+ .schedule_report(TlsEvent {
+ policy: PolicyType::Tlsa(None),
+ domain: domain.domain.to_string(),
+ failure: FailureDetails::new(
+ ResultType::DaneRequired,
+ )
.with_receiving_mx_hostname(envelope.mx)
.with_failure_reason_code(
"No TLSA records found for MX.",
)
.into(),
- tls_record: tls_report.record.clone(),
- interval: tls_report.interval,
- })
- .await;
+ tls_record: tls_report.record.clone(),
+ interval: tls_report.interval,
+ })
+ .await;
}
Status::PermanentFailure(Error::DaneError(ErrorDetails {
@@ -837,7 +869,7 @@ impl DeliveryAttempt {
let mut in_flight_host = Vec::new();
envelope.remote_ip = remote_ip;
for throttle in &queue_config.throttle.host {
- if let Err(err) = core
+ if let Err(err) = server
.is_allowed(throttle, &envelope, &mut in_flight_host, message.span_id)
.await
{
@@ -854,8 +886,7 @@ impl DeliveryAttempt {
// Connect
let time = Instant::now();
- let conn_timeout = core
- .core
+ let conn_timeout = server
.eval_if(&queue_config.timeout.connect, &envelope, message.span_id)
.await
.unwrap_or_else(|| Duration::from_secs(5 * 60));
@@ -908,8 +939,7 @@ impl DeliveryAttempt {
};
// Obtain session parameters
- let local_hostname = core
- .core
+ let local_hostname = server
.eval_if::<String, _>(&queue_config.hostname, &envelope, message.span_id)
.await
.filter(|s| !s.is_empty())
@@ -922,28 +952,24 @@ impl DeliveryAttempt {
});
let params = SessionParams {
session_id: message.span_id,
- core: &core,
+ server: &server,
credentials: remote_host.credentials(),
is_smtp: remote_host.is_smtp(),
hostname: envelope.mx,
local_hostname: &local_hostname,
- timeout_ehlo: core
- .core
+ timeout_ehlo: server
.eval_if(&queue_config.timeout.ehlo, &envelope, message.span_id)
.await
.unwrap_or_else(|| Duration::from_secs(5 * 60)),
- timeout_mail: core
- .core
+ timeout_mail: server
.eval_if(&queue_config.timeout.mail, &envelope, message.span_id)
.await
.unwrap_or_else(|| Duration::from_secs(5 * 60)),
- timeout_rcpt: core
- .core
+ timeout_rcpt: server
.eval_if(&queue_config.timeout.rcpt, &envelope, message.span_id)
.await
.unwrap_or_else(|| Duration::from_secs(5 * 60)),
- timeout_data: core
- .core
+ timeout_data: server
.eval_if(&queue_config.timeout.data, &envelope, message.span_id)
.await
.unwrap_or_else(|| Duration::from_secs(5 * 60)),
@@ -956,15 +982,14 @@ impl DeliveryAttempt {
|| dane_policy.is_some();
let tls_connector = if allow_invalid_certs || remote_host.allow_invalid_certs()
{
- &core.inner.connectors.dummy_verify
+ &server.inner.data.smtp_connectors.dummy_verify
} else {
- &core.inner.connectors.pki_verify
+ &server.inner.data.smtp_connectors.pki_verify
};
let delivery_result = if !remote_host.implicit_tls() {
// Read greeting
- smtp_client.timeout = core
- .core
+ smtp_client.timeout = server
.eval_if(&queue_config.timeout.greeting, &envelope, message.span_id)
.await
.unwrap_or_else(|| Duration::from_secs(5 * 60));
@@ -1014,8 +1039,7 @@ impl DeliveryAttempt {
// Try starting TLS
if tls_strategy.try_start_tls() {
let time = Instant::now();
- smtp_client.timeout = core
- .core
+ smtp_client.timeout = server
.eval_if(&queue_config.timeout.tls, &envelope, message.span_id)
.await
.unwrap_or_else(|| Duration::from_secs(3 * 60));
@@ -1055,22 +1079,23 @@ impl DeliveryAttempt {
) {
// Report DANE verification failure
if let Some(tls_report) = &tls_report {
- core.schedule_report(TlsEvent {
- policy: dane_policy.into(),
- domain: domain.domain.to_string(),
- failure: FailureDetails::new(
- ResultType::ValidationFailure,
- )
- .with_receiving_mx_hostname(envelope.mx)
- .with_receiving_ip(remote_ip)
- .with_failure_reason_code(
- "No matching certificates found.",
- )
- .into(),
- tls_record: tls_report.record.clone(),
- interval: tls_report.interval,
- })
- .await;
+ server
+ .schedule_report(TlsEvent {
+ policy: dane_policy.into(),
+ domain: domain.domain.to_string(),
+ failure: FailureDetails::new(
+ ResultType::ValidationFailure,
+ )
+ .with_receiving_mx_hostname(envelope.mx)
+ .with_receiving_ip(remote_ip)
+ .with_failure_reason_code(
+ "No matching certificates found.",
+ )
+ .into(),
+ tls_record: tls_report.record.clone(),
+ interval: tls_report.interval,
+ })
+ .await;
}
last_status = status;
@@ -1080,14 +1105,15 @@ impl DeliveryAttempt {
// Report TLS success
if let Some(tls_report) = &tls_report {
- core.schedule_report(TlsEvent {
- policy: (&mta_sts_policy, &dane_policy).into(),
- domain: domain.domain.to_string(),
- failure: None,
- tls_record: tls_report.record.clone(),
- interval: tls_report.interval,
- })
- .await;
+ server
+ .schedule_report(TlsEvent {
+ policy: (&mta_sts_policy, &dane_policy).into(),
+ domain: domain.domain.to_string(),
+ failure: None,
+ tls_record: tls_report.record.clone(),
+ interval: tls_report.interval,
+ })
+ .await;
}
// Deliver message over TLS
@@ -1126,20 +1152,21 @@ impl DeliveryAttempt {
);
if let Some(tls_report) = &tls_report {
- core.schedule_report(TlsEvent {
- policy: (&mta_sts_policy, &dane_policy).into(),
- domain: domain.domain.to_string(),
- failure: FailureDetails::new(
- ResultType::StartTlsNotSupported,
- )
- .with_receiving_mx_hostname(envelope.mx)
- .with_receiving_ip(remote_ip)
- .with_failure_reason_code(reason)
- .into(),
- tls_record: tls_report.record.clone(),
- interval: tls_report.interval,
- })
- .await;
+ server
+ .schedule_report(TlsEvent {
+ policy: (&mta_sts_policy, &dane_policy).into(),
+ domain: domain.domain.to_string(),
+ failure: FailureDetails::new(
+ ResultType::StartTlsNotSupported,
+ )
+ .with_receiving_mx_hostname(envelope.mx)
+ .with_receiving_ip(remote_ip)
+ .with_failure_reason_code(reason)
+ .into(),
+ tls_record: tls_report.record.clone(),
+ interval: tls_report.interval,
+ })
+ .await;
}
if is_strict_tls {
@@ -1173,20 +1200,21 @@ impl DeliveryAttempt {
if let (Some(tls_report), mail_send::Error::Tls(error)) =
(&tls_report, &error)
{
- core.schedule_report(TlsEvent {
- policy: (&mta_sts_policy, &dane_policy).into(),
- domain: domain.domain.to_string(),
- failure: FailureDetails::new(
- ResultType::CertificateNotTrusted,
- )
- .with_receiving_mx_hostname(envelope.mx)
- .with_receiving_ip(remote_ip)
- .with_failure_reason_code(error.to_string())
- .into(),
- tls_record: tls_report.record.clone(),
- interval: tls_report.interval,
- })
- .await;
+ server
+ .schedule_report(TlsEvent {
+ policy: (&mta_sts_policy, &dane_policy).into(),
+ domain: domain.domain.to_string(),
+ failure: FailureDetails::new(
+ ResultType::CertificateNotTrusted,
+ )
+ .with_receiving_mx_hostname(envelope.mx)
+ .with_receiving_ip(remote_ip)
+ .with_failure_reason_code(error.to_string())
+ .into(),
+ tls_record: tls_report.record.clone(),
+ interval: tls_report.interval,
+ })
+ .await;
}
last_status = if is_strict_tls {
@@ -1216,8 +1244,7 @@ impl DeliveryAttempt {
}
} else {
// Start TLS
- smtp_client.timeout = core
- .core
+ smtp_client.timeout = server
.eval_if(&queue_config.timeout.tls, &envelope, message.span_id)
.await
.unwrap_or_else(|| Duration::from_secs(3 * 60));
@@ -1239,8 +1266,7 @@ impl DeliveryAttempt {
};
// Read greeting
- smtp_client.timeout = core
- .core
+ smtp_client.timeout = server
.eval_if(&queue_config.timeout.greeting, &envelope, message.span_id)
.await
.unwrap_or_else(|| Duration::from_secs(5 * 60));
@@ -1268,8 +1294,7 @@ impl DeliveryAttempt {
};
// Update status for the current domain and continue with the next one
- let schedule = core
- .core
+ let schedule = server
.eval_if::<Vec<Duration>, _>(
&queue_config.retry,
&envelope,
@@ -1283,8 +1308,7 @@ impl DeliveryAttempt {
}
// Update status
- let schedule = core
- .core
+ let schedule = server
.eval_if::<Vec<Duration>, _>(&queue_config.retry, &envelope, message.span_id)
.await
.unwrap_or_else(|| vec![Duration::from_secs(60)]);
@@ -1293,20 +1317,20 @@ impl DeliveryAttempt {
message.recipients = recipients;
// Send Delivery Status Notifications
- core.send_dsn(&mut message).await;
+ server.send_dsn(&mut message).await;
// Notify queue manager
let result = if !on_hold.is_empty() {
// Save changes to disk
let next_due = message.next_event_after(now());
- message.save_changes(&core, None, None).await;
+ message.save_changes(&server, None, None).await;
trc::event!(
Delivery(DeliveryEvent::ConcurrencyLimitExceeded),
SpanId = span_id,
);
- Event::OnHold(OnHold {
+ QueueEvent::OnHold(OnHold {
next_due,
limiters: on_hold,
message: self.event,
@@ -1322,10 +1346,10 @@ impl DeliveryAttempt {
// Save changes to disk
message
- .save_changes(&core, self.event.due.into(), due.into())
+ .save_changes(&server, self.event.due.into(), due.into())
.await;
- Event::Reload
+ QueueEvent::Reload
} else {
trc::event!(
Delivery(DeliveryEvent::Completed),
@@ -1334,11 +1358,11 @@ impl DeliveryAttempt {
);
// Delete message from queue
- message.remove(&core, self.event.due).await;
+ message.remove(&server, self.event.due).await;
- Event::Reload
+ QueueEvent::Reload
};
- if core.inner.queue_tx.send(result).await.is_err() {
+ if server.inner.ipc.queue_tx.send(result).await.is_err() {
trc::event!(
Server(ServerEvent::ThreadError),
Reason = "Channel closed.",
diff --git a/crates/smtp/src/outbound/local.rs b/crates/smtp/src/outbound/local.rs
index c4f71501..4d900967 100644
--- a/crates/smtp/src/outbound/local.rs
+++ b/crates/smtp/src/outbound/local.rs
@@ -4,7 +4,7 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
-use common::{DeliveryEvent, DeliveryResult, IngestMessage};
+use common::ipc::{DeliveryEvent, DeliveryResult, IngestMessage};
use smtp_proto::Response;
use tokio::sync::{mpsc, oneshot};
use trc::ServerEvent;
diff --git a/crates/smtp/src/outbound/lookup.rs b/crates/smtp/src/outbound/lookup.rs
index 8b8ba8f5..8b66cc86 100644
--- a/crates/smtp/src/outbound/lookup.rs
+++ b/crates/smtp/src/outbound/lookup.rs
@@ -5,18 +5,19 @@
*/
use std::{
+ future::Future,
net::{IpAddr, Ipv4Addr, Ipv6Addr},
sync::Arc,
};
-use common::expr::{functions::ResolveVariable, V_MX};
+use common::{
+ expr::{functions::ResolveVariable, V_MX},
+ Server,
+};
use mail_auth::{IpLookupStrategy, MX};
use rand::{seq::SliceRandom, Rng};
-use crate::{
- core::SMTP,
- queue::{Error, ErrorDetails, Status},
-};
+use crate::queue::{Error, ErrorDetails, Status};
use super::NextHop;
@@ -26,8 +27,25 @@ pub struct IpLookupResult {
pub remote_ips: Vec<IpAddr>,
}
-impl SMTP {
- pub async fn ip_lookup(
+pub trait DnsLookup: Sync + Send {
+ fn ip_lookup(
+ &self,
+ key: &str,
+ strategy: IpLookupStrategy,
+ max_results: usize,
+ ) -> impl Future<Output = mail_auth::Result<Vec<IpAddr>>> + Send;
+
+ fn resolve_host<'x>(
+ &'x self,
+ remote_host: &NextHop<'_>,
+ envelope: &impl ResolveVariable,
+ max_multihomed: usize,
+ session_id: u64,
+ ) -> impl Future<Output = Result<IpLookupResult, Status<(), Error>>> + Send;
+}
+
+impl DnsLookup for Server {
+ async fn ip_lookup(
&self,
key: &str,
strategy: IpLookupStrategy,
@@ -82,7 +100,7 @@ impl SMTP {
}
}
- pub async fn resolve_host<'x>(
+ async fn resolve_host<'x>(
&'x self,
remote_host: &NextHop<'_>,
envelope: &impl ResolveVariable,
@@ -92,8 +110,7 @@ impl SMTP {
let remote_ips = self
.ip_lookup(
remote_host.fqdn_hostname().as_ref(),
- self.core
- .eval_if(&self.core.smtp.queue.ip_strategy, envelope, session_id)
+ self.eval_if(&self.core.smtp.queue.ip_strategy, envelope, session_id)
.await
.unwrap_or(IpLookupStrategy::Ipv4thenIpv6),
max_multihomed,
@@ -122,7 +139,6 @@ impl SMTP {
// Obtain source IPv4 address
let source_ips = self
- .core
.eval_if::<Vec<Ipv4Addr>, _>(
&self.core.smtp.queue.source_ip.ipv4,
envelope,
@@ -144,7 +160,6 @@ impl SMTP {
// Obtain source IPv6 address
let source_ips = self
- .core
.eval_if::<Vec<Ipv6Addr>, _>(
&self.core.smtp.queue.source_ip.ipv6,
envelope,
diff --git a/crates/smtp/src/outbound/mod.rs b/crates/smtp/src/outbound/mod.rs
index 5f963f65..7bd71e2f 100644
--- a/crates/smtp/src/outbound/mod.rs
+++ b/crates/smtp/src/outbound/mod.rs
@@ -6,16 +6,17 @@
use std::borrow::Cow;
-use common::config::{
- server::ServerProtocol,
- smtp::queue::{RelayHost, RequireOptional},
+use common::{
+ config::{
+ server::ServerProtocol,
+ smtp::queue::{RelayHost, RequireOptional},
+ },
+ ipc::QueueEventLock,
};
use mail_send::Credentials;
use smtp_proto::{Response, Severity};
-use crate::queue::{
- spool::QueueEventLock, DeliveryAttempt, Error, ErrorDetails, HostResponse, Status,
-};
+use crate::queue::{DeliveryAttempt, Error, ErrorDetails, HostResponse, Status};
pub mod client;
pub mod dane;
diff --git a/crates/smtp/src/outbound/mta_sts/lookup.rs b/crates/smtp/src/outbound/mta_sts/lookup.rs
index 38fe9164..e6ee8f6b 100644
--- a/crates/smtp/src/outbound/mta_sts/lookup.rs
+++ b/crates/smtp/src/outbound/mta_sts/lookup.rs
@@ -13,11 +13,9 @@ use std::{
#[cfg(feature = "test_mode")]
pub static STS_TEST_POLICY: parking_lot::Mutex<Vec<u8>> = parking_lot::Mutex::new(Vec::new());
-use common::config::smtp::resolver::Policy;
+use common::{config::smtp::resolver::Policy, Server};
use mail_auth::{common::lru::DnsCache, mta_sts::MtaSts, report::tlsrpt::ResultType};
-use crate::core::SMTP;
-
use super::{parse::ParsePolicy, Error};
#[cfg(not(feature = "test_mode"))]
@@ -26,9 +24,25 @@ use common::HttpLimitResponse;
#[cfg(not(feature = "test_mode"))]
const MAX_POLICY_SIZE: usize = 1024 * 1024;
+pub trait MtaStsLookup: Sync + Send {
+ fn lookup_mta_sts_policy<'x>(
+ &self,
+ domain: &str,
+ timeout: Duration,
+ ) -> impl std::future::Future<Output = Result<Arc<Policy>, Error>> + Send;
+
+ #[cfg(feature = "test_mode")]
+ fn policy_add<'x>(
+ &self,
+ key: impl mail_auth::common::resolver::IntoFqdn<'x>,
+ value: Policy,
+ valid_until: std::time::Instant,
+ );
+}
+
#[allow(unused_variables)]
-impl SMTP {
- pub async fn lookup_mta_sts_policy<'x>(
+impl MtaStsLookup for Server {
+ async fn lookup_mta_sts_policy<'x>(
&self,
domain: &str,
timeout: Duration,
@@ -96,7 +110,7 @@ impl SMTP {
}
#[cfg(feature = "test_mode")]
- pub fn policy_add<'x>(
+ fn policy_add<'x>(
&self,
key: impl mail_auth::common::resolver::IntoFqdn<'x>,
value: Policy,
diff --git a/crates/smtp/src/outbound/session.rs b/crates/smtp/src/outbound/session.rs
index b2e0dfef..41bf98b6 100644
--- a/crates/smtp/src/outbound/session.rs
+++ b/crates/smtp/src/outbound/session.rs
@@ -5,6 +5,7 @@
*/
use common::config::smtp::queue::RequireOptional;
+use common::Server;
use mail_send::Credentials;
use smtp_proto::{
EhloResponse, Severity, EXT_CHUNKING, EXT_DSN, EXT_REQUIRE_TLS, EXT_SIZE, EXT_SMTP_UTF8,
@@ -17,17 +18,14 @@ use tokio::io::{AsyncRead, AsyncWrite};
use trc::DeliveryEvent;
use crate::outbound::client::{from_error_status, from_mail_send_error};
-use crate::{
- core::SMTP,
- queue::{ErrorDetails, HostResponse, RCPT_STATUS_CHANGED},
-};
+use crate::queue::{ErrorDetails, HostResponse, RCPT_STATUS_CHANGED};
use crate::queue::{Error, Message, Recipient, Status};
use super::{client::SmtpClient, TlsStrategy};
pub struct SessionParams<'x> {
- pub core: &'x SMTP,
+ pub server: &'x Server,
pub hostname: &'x str,
pub credentials: Option<&'x Credentials<String>>,
pub is_smtp: bool,
diff --git a/crates/smtp/src/queue/dsn.rs b/crates/smtp/src/queue/dsn.rs
index ce2e5f34..97fb572e 100644
--- a/crates/smtp/src/queue/dsn.rs
+++ b/crates/smtp/src/queue/dsn.rs
@@ -4,6 +4,7 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
+use common::Server;
use mail_builder::headers::content_type::ContentType;
use mail_builder::headers::HeaderType;
use mail_builder::mime::{make_boundary, BodyPart, MimePart};
@@ -13,19 +14,26 @@ use smtp_proto::{
Response, RCPT_NOTIFY_DELAY, RCPT_NOTIFY_FAILURE, RCPT_NOTIFY_NEVER, RCPT_NOTIFY_SUCCESS,
};
use std::fmt::Write;
+use std::future::Future;
use std::time::Duration;
use store::write::now;
-use crate::core::SMTP;
use crate::outbound::client::from_error_status;
+use crate::reporting::SmtpReporting;
+use super::spool::SmtpSpool;
use super::{
Domain, Error, ErrorDetails, HostResponse, Message, MessageSource, QueueEnvelope, Recipient,
Status, RCPT_DSN_SENT, RCPT_STATUS_CHANGED,
};
-impl SMTP {
- pub async fn send_dsn(&self, message: &mut Message) {
+pub trait SendDsn: Sync + Send {
+ fn send_dsn(&self, message: &mut Message) -> impl Future<Output = ()> + Send;
+ fn log_dsn(&self, message: &Message) -> impl Future<Output = ()> + Send;
+}
+
+impl SendDsn for Server {
+ async fn send_dsn(&self, message: &mut Message) {
// Send DSN events
self.log_dsn(message).await;
@@ -152,8 +160,8 @@ impl SMTP {
}
impl Message {
- pub async fn build_dsn(&mut self, core: &SMTP) -> Option<Vec<u8>> {
- let config = &core.core.smtp.queue;
+ pub async fn build_dsn(&mut self, server: &Server) -> Option<Vec<u8>> {
+ let config = &server.core.smtp.queue;
let now = now();
let mut txt_success = String::new();
@@ -314,8 +322,7 @@ impl Message {
{
let envelope = QueueEnvelope::new(self, domain_idx);
- if let Some(next_notify) = core
- .core
+ if let Some(next_notify) = server
.eval_if::<Vec<Duration>, _>(&config.notify, &envelope, self.span_id)
.await
.and_then(|notify| {
@@ -337,19 +344,16 @@ impl Message {
}
// Obtain hostname and sender addresses
- let from_name = core
- .core
+ let from_name = server
.eval_if(&config.dsn.name, self, self.span_id)
.await
.unwrap_or_else(|| String::from("Mail Delivery Subsystem"));
- let from_addr = core
- .core
+ let from_addr = server
.eval_if(&config.dsn.address, self, self.span_id)
.await
.unwrap_or_else(|| String::from("MAILER-DAEMON@localhost"));
- let reporting_mta = core
- .core
- .eval_if(&core.core.smtp.report.submitter, self, self.span_id)
+ let reporting_mta = server
+ .eval_if(&server.core.smtp.report.submitter, self, self.span_id)
.await
.unwrap_or_else(|| String::from("localhost"));
@@ -359,10 +363,8 @@ impl Message {
let dsn = dsn_header + dsn.as_str();
// Fetch up to 1024 bytes of message headers
- let headers = match core
- .core
- .storage
- .blob
+ let headers = match server
+ .blob_store()
.get_blob(self.blob_hash.as_slice(), 0..1024)
.await
{
diff --git a/crates/smtp/src/queue/manager.rs b/crates/smtp/src/queue/manager.rs
index aa2ae6ad..6f16d7d9 100644
--- a/crates/smtp/src/queue/manager.rs
+++ b/crates/smtp/src/queue/manager.rs
@@ -4,33 +4,39 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
-use std::{sync::atomic::Ordering, time::Duration};
-
+use std::{
+ sync::{atomic::Ordering, Arc},
+ time::Duration,
+};
+
+use common::{
+ core::BuildServer,
+ ipc::{OnHold, QueueEvent, QueueEventLock},
+ Inner,
+};
use store::write::now;
use tokio::sync::mpsc;
-use crate::core::{SmtpInstance, SMTP};
-
-use super::{spool::QueueEventLock, DeliveryAttempt, Event, Message, OnHold, Status};
+use super::{spool::SmtpSpool, DeliveryAttempt, Message, Status};
pub(crate) const SHORT_WAIT: Duration = Duration::from_millis(1);
pub(crate) const LONG_WAIT: Duration = Duration::from_secs(86400 * 365);
pub struct Queue {
- pub core: SmtpInstance,
+ pub core: Arc<Inner>,
pub on_hold: Vec<OnHold<QueueEventLock>>,
pub next_wake_up: Duration,
}
-impl SpawnQueue for mpsc::Receiver<Event> {
- fn spawn(mut self, core: SmtpInstance) {
+impl SpawnQueue for mpsc::Receiver<QueueEvent> {
+ fn spawn(mut self, core: Arc<Inner>) {
tokio::spawn(async move {
let mut queue = Queue::new(core);
loop {
let on_hold = match tokio::time::timeout(queue.next_wake_up, self.recv()).await {
- Ok(Some(Event::OnHold(on_hold))) => on_hold.into(),
- Ok(Some(Event::Stop)) | Ok(None) => {
+ Ok(Some(QueueEvent::OnHold(on_hold))) => on_hold.into(),
+ Ok(Some(QueueEvent::Stop)) | Ok(None) => {
break;
}
_ => None,
@@ -48,7 +54,7 @@ impl SpawnQueue for mpsc::Receiver<Event> {
}
impl Queue {
- pub fn new(core: SmtpInstance) -> Self {
+ pub fn new(core: Arc<Inner>) -> Self {
Queue {
core,
on_hold: Vec::with_capacity(128),
@@ -58,20 +64,20 @@ impl Queue {
pub async fn process_events(&mut self) {
// Deliver any concurrency limited messages
- let core = SMTP::from(self.core.clone());
+ let server = self.core.build_server();
while let Some(queue_event) = self.next_on_hold() {
DeliveryAttempt::new(queue_event)
- .try_deliver(core.clone())
+ .try_deliver(server.clone())
.await;
}
// Deliver scheduled messages
let now = now();
self.next_wake_up = LONG_WAIT;
- for queue_event in core.next_event().await {
+ for queue_event in server.next_event().await {
if queue_event.due <= now {
DeliveryAttempt::new(queue_event)
- .try_deliver(core.clone())
+ .try_deliver(server.clone())
.await;
} else {
self.next_wake_up = Duration::from_secs(queue_event.due - now);
@@ -217,5 +223,5 @@ impl Message {
}
pub trait SpawnQueue {
- fn spawn(self, core: SmtpInstance);
+ fn spawn(self, core: Arc<Inner>);
}
diff --git a/crates/smtp/src/queue/mod.rs b/crates/smtp/src/queue/mod.rs
index e1e98772..5849054c 100644
--- a/crates/smtp/src/queue/mod.rs
+++ b/crates/smtp/src/queue/mod.rs
@@ -12,15 +12,14 @@ use std::{
use common::{
expr::{self, functions::ResolveVariable, *},
- listener::limiter::{ConcurrencyLimiter, InFlight},
+ ipc::QueueEventLock,
+ listener::limiter::InFlight,
};
use serde::{Deserialize, Serialize};
use smtp_proto::Response;
use store::write::now;
use utils::BlobHash;
-use self::spool::QueueEventLock;
-
pub mod dsn;
pub mod manager;
pub mod quota;
@@ -29,20 +28,6 @@ pub mod throttle;
pub type QueueId = u64;
-#[derive(Debug)]
-pub enum Event {
- Reload,
- OnHold(OnHold<QueueEventLock>),
- Stop,
-}
-
-#[derive(Debug)]
-pub struct OnHold<T> {
- pub next_due: Option<u64>,
- pub limiters: Vec<ConcurrencyLimiter>,
- pub message: T,
-}
-
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Schedule<T> {
pub due: u64,
diff --git a/crates/smtp/src/queue/quota.rs b/crates/smtp/src/queue/quota.rs
index e7323b63..73f65380 100644
--- a/crates/smtp/src/queue/quota.rs
+++ b/crates/smtp/src/queue/quota.rs
@@ -4,19 +4,34 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
-use common::{config::smtp::queue::QueueQuota, expr::functions::ResolveVariable};
+use std::future::Future;
+
+use common::{config::smtp::queue::QueueQuota, expr::functions::ResolveVariable, Server};
use store::{
write::{BatchBuilder, QueueClass, ValueClass},
ValueKey,
};
use trc::QueueEvent;
-use crate::core::{throttle::NewKey, SMTP};
+use crate::core::throttle::NewKey;
use super::{Message, QueueEnvelope, QuotaKey, Status};
-impl SMTP {
- pub async fn has_quota(&self, message: &mut Message) -> bool {
+pub trait HasQueueQuota: Sync + Send {
+ fn has_quota(&self, message: &mut Message) -> impl Future<Output = bool> + Send;
+ fn check_quota<'x>(
+ &'x self,
+ quota: &'x QueueQuota,
+ envelope: &impl ResolveVariable,
+ size: usize,
+ id: u64,
+ refs: &mut Vec<QuotaKey>,
+ session_id: u64,
+ ) -> impl Future<Output = bool> + Send;
+}
+
+impl HasQueueQuota for Server {
+ async fn has_quota(&self, message: &mut Message) -> bool {
let mut quota_keys = Vec::new();
if !self.core.smtp.queue.quota.sender.is_empty() {
@@ -110,7 +125,6 @@ impl SMTP {
) -> bool {
if !quota.expr.is_empty()
&& self
- .core
.eval_expr(&quota.expr, envelope, "check_quota", session_id)
.await
.unwrap_or(false)
diff --git a/crates/smtp/src/queue/spool.rs b/crates/smtp/src/queue/spool.rs
index 54aecc7e..5c33cc12 100644
--- a/crates/smtp/src/queue/spool.rs
+++ b/crates/smtp/src/queue/spool.rs
@@ -5,32 +5,44 @@
*/
use crate::queue::DomainPart;
+use common::ipc::{QueueEvent, QueueEventLock};
+use common::Server;
use std::borrow::Cow;
+use std::future::Future;
use std::time::{Duration, SystemTime};
use store::write::key::DeserializeBigEndian;
-use store::write::{now, BatchBuilder, Bincode, BlobOp, QueueClass, QueueEvent, ValueClass};
+use store::write::{now, BatchBuilder, Bincode, BlobOp, QueueClass, ValueClass};
use store::{Deserialize, IterateParams, Serialize, ValueKey, U64_LEN};
use trc::ServerEvent;
use utils::BlobHash;
-use crate::core::SMTP;
-
use super::{
- Domain, Event, Message, MessageSource, QueueEnvelope, QueueId, QuotaKey, Recipient, Schedule,
- Status,
+ Domain, Message, MessageSource, QueueEnvelope, QueueId, QuotaKey, Recipient, Schedule, Status,
};
pub const LOCK_EXPIRY: u64 = 300;
-#[derive(Debug)]
-pub struct QueueEventLock {
- pub due: u64,
- pub queue_id: u64,
- pub lock_expiry: u64,
+pub trait SmtpSpool: Sync + Send {
+ fn new_message(
+ &self,
+ return_path: impl Into<String>,
+ return_path_lcase: impl Into<String>,
+ return_path_domain: impl Into<String>,
+ span_id: u64,
+ ) -> Message;
+
+ fn next_event(&self) -> impl Future<Output = Vec<QueueEventLock>> + Send;
+
+ fn try_lock_event(
+ &self,
+ event: QueueEventLock,
+ ) -> impl Future<Output = Option<QueueEventLock>> + Send;
+
+ fn read_message(&self, id: QueueId) -> impl Future<Output = Option<Message>> + Send;
}
-impl SMTP {
- pub fn new_message(
+impl SmtpSpool for Server {
+ fn new_message(
&self,
return_path: impl Into<String>,
return_path_lcase: impl Into<String>,
@@ -41,7 +53,7 @@ impl SMTP {
.duration_since(SystemTime::UNIX_EPOCH)
.map_or(0, |d| d.as_secs());
Message {
- queue_id: self.inner.queue_id_gen.generate().unwrap_or(created),
+ queue_id: self.inner.data.queue_id_gen.generate().unwrap_or(created),
span_id,
created,
return_path: return_path.into(),
@@ -58,22 +70,24 @@ impl SMTP {
}
}
- pub async fn next_event(&self) -> Vec<QueueEventLock> {
- let from_key = ValueKey::from(ValueClass::Queue(QueueClass::MessageEvent(QueueEvent {
- due: 0,
- queue_id: 0,
- })));
- let to_key = ValueKey::from(ValueClass::Queue(QueueClass::MessageEvent(QueueEvent {
- due: u64::MAX,
- queue_id: u64::MAX,
- })));
+ async fn next_event(&self) -> Vec<QueueEventLock> {
+ let from_key = ValueKey::from(ValueClass::Queue(QueueClass::MessageEvent(
+ store::write::QueueEvent {
+ due: 0,
+ queue_id: 0,
+ },
+ )));
+ let to_key = ValueKey::from(ValueClass::Queue(QueueClass::MessageEvent(
+ store::write::QueueEvent {
+ due: u64::MAX,
+ queue_id: u64::MAX,
+ },
+ )));
let mut events = Vec::new();
let now = now();
let result = self
- .core
- .storage
- .data
+ .store()
.iterate(
IterateParams::new(from_key, to_key).ascending(),
|key, value| {
@@ -107,10 +121,10 @@ impl SMTP {
events
}
- pub async fn try_lock_event(&self, mut event: QueueEventLock) -> Option<QueueEventLock> {
+ async fn try_lock_event(&self, mut event: QueueEventLock) -> Option<QueueEventLock> {
let mut batch = BatchBuilder::new();
batch.assert_value(
- ValueClass::Queue(QueueClass::MessageEvent(QueueEvent {
+ ValueClass::Queue(QueueClass::MessageEvent(store::write::QueueEvent {
due: event.due,
queue_id: event.queue_id,
})),
@@ -118,13 +132,13 @@ impl SMTP {
);
event.lock_expiry = now() + LOCK_EXPIRY;
batch.set(
- ValueClass::Queue(QueueClass::MessageEvent(QueueEvent {
+ ValueClass::Queue(QueueClass::MessageEvent(store::write::QueueEvent {
due: event.due,
queue_id: event.queue_id,
})),
event.lock_expiry.serialize(),
);
- match self.core.storage.data.write(batch.build()).await {
+ match self.store().write(batch.build()).await {
Ok(_) => Some(event),
Err(err) if err.is_assertion_failure() => {
trc::event!(
@@ -145,11 +159,9 @@ impl SMTP {
}
}
- pub async fn read_message(&self, id: QueueId) -> Option<Message> {
+ async fn read_message(&self, id: QueueId) -> Option<Message> {
match self
- .core
- .storage
- .data
+ .store()
.get_value::<Bincode<Message>>(ValueKey::from(ValueClass::Queue(QueueClass::Message(
id,
))))
@@ -174,7 +186,7 @@ impl Message {
raw_headers: Option<&[u8]>,
raw_message: &[u8],
session_id: u64,
- core: &SMTP,
+ server: &Server,
source: MessageSource,
) -> bool {
// Write blob
@@ -203,7 +215,7 @@ impl Message {
},
0u32.serialize(),
);
- if let Err(err) = core.core.storage.data.write(batch.build()).await {
+ if let Err(err) = server.store().write(batch.build()).await {
trc::error!(err
.details("Failed to write to store.")
.span_id(session_id)
@@ -211,10 +223,8 @@ impl Message {
return false;
}
- if let Err(err) = core
- .core
- .storage
- .blob
+ if let Err(err) = server
+ .blob_store()
.put_blob(self.blob_hash.as_slice(), message.as_ref())
.await
{
@@ -271,7 +281,7 @@ impl Message {
}
batch
.set(
- ValueClass::Queue(QueueClass::MessageEvent(QueueEvent {
+ ValueClass::Queue(QueueClass::MessageEvent(store::write::QueueEvent {
due: self.next_event().unwrap_or_default(),
queue_id: self.queue_id,
})),
@@ -299,7 +309,7 @@ impl Message {
Bincode::new(self).serialize(),
);
- if let Err(err) = core.core.storage.data.write(batch.build()).await {
+ if let Err(err) = server.store().write(batch.build()).await {
trc::error!(err
.details("Failed to write to store.")
.span_id(session_id)
@@ -309,7 +319,14 @@ impl Message {
}
// Queue the message
- if core.inner.queue_tx.send(Event::Reload).await.is_err() {
+ if server
+ .inner
+ .ipc
+ .queue_tx
+ .send(QueueEvent::Reload)
+ .await
+ .is_err()
+ {
trc::event!(
Server(ServerEvent::ThreadError),
Reason = "Channel closed.",
@@ -326,7 +343,7 @@ impl Message {
rcpt: impl Into<String>,
rcpt_lcase: impl Into<String>,
rcpt_domain: impl Into<String>,
- core: &SMTP,
+ server: &Server,
) {
let rcpt_domain = rcpt_domain.into();
let domain_idx =
@@ -343,10 +360,9 @@ impl Message {
status: Status::Scheduled,
});
- let expires = core
- .core
+ let expires = server
.eval_if(
- &core.core.smtp.queue.expire,
+ &server.core.smtp.queue.expire,
&QueueEnvelope::new(self, idx),
self.span_id,
)
@@ -370,17 +386,17 @@ impl Message {
});
}
- pub async fn add_recipient(&mut self, rcpt: impl Into<String>, core: &SMTP) {
+ pub async fn add_recipient(&mut self, rcpt: impl Into<String>, server: &Server) {
let rcpt = rcpt.into();
let rcpt_lcase = rcpt.to_lowercase();
let rcpt_domain = rcpt_lcase.domain_part().to_string();
- self.add_recipient_parts(rcpt, rcpt_lcase, rcpt_domain, core)
+ self.add_recipient_parts(rcpt, rcpt_lcase, rcpt_domain, server)
.await;
}
pub async fn save_changes(
mut self,
- core: &SMTP,
+ server: &Server,
prev_event: Option<u64>,
next_event: Option<u64>,
) -> bool {
@@ -395,12 +411,14 @@ impl Message {
let mut batch = BatchBuilder::new();
if let (Some(prev_event), Some(next_event)) = (prev_event, next_event) {
batch
- .clear(ValueClass::Queue(QueueClass::MessageEvent(QueueEvent {
- due: prev_event,
- queue_id: self.queue_id,
- })))
+ .clear(ValueClass::Queue(QueueClass::MessageEvent(
+ store::write::QueueEvent {
+ due: prev_event,
+ queue_id: self.queue_id,
+ },
+ )))
.set(
- ValueClass::Queue(QueueClass::MessageEvent(QueueEvent {
+ ValueClass::Queue(QueueClass::MessageEvent(store::write::QueueEvent {
due: next_event,
queue_id: self.queue_id,
})),
@@ -414,7 +432,7 @@ impl Message {
Bincode::new(self).serialize(),
);
- if let Err(err) = core.core.storage.data.write(batch.build()).await {
+ if let Err(err) = server.store().write(batch.build()).await {
trc::error!(err
.details("Failed to save changes.")
.span_id(span_id)
@@ -425,7 +443,7 @@ impl Message {
}
}
- pub async fn remove(self, core: &SMTP, prev_event: u64) -> bool {
+ pub async fn remove(self, server: &Server, prev_event: u64) -> bool {
let mut batch = BatchBuilder::new();
// Release all quotas
@@ -448,13 +466,15 @@ impl Message {
hash: self.blob_hash.clone(),
id: self.queue_id,
})
- .clear(ValueClass::Queue(QueueClass::MessageEvent(QueueEvent {
- due: prev_event,
- queue_id: self.queue_id,
- })))
+ .clear(ValueClass::Queue(QueueClass::MessageEvent(
+ store::write::QueueEvent {
+ due: prev_event,
+ queue_id: self.queue_id,
+ },
+ )))
.clear(ValueClass::Queue(QueueClass::Message(self.queue_id)));
- if let Err(err) = core.core.storage.data.write(batch.build()).await {
+ if let Err(err) = server.store().write(batch.build()).await {
trc::error!(err
.details("Failed to write to update queue.")
.span_id(self.span_id)
diff --git a/crates/smtp/src/queue/throttle.rs b/crates/smtp/src/queue/throttle.rs
index a9de94bc..e80c3a44 100644
--- a/crates/smtp/src/queue/throttle.rs
+++ b/crates/smtp/src/queue/throttle.rs
@@ -4,15 +4,18 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
+use std::future::Future;
+
use common::{
config::smtp::Throttle,
expr::functions::ResolveVariable,
listener::limiter::{ConcurrencyLimiter, InFlight},
+ Server,
};
use dashmap::mapref::entry::Entry;
use store::write::now;
-use crate::core::{throttle::NewKey, SMTP};
+use crate::core::throttle::NewKey;
use super::{Domain, Status};
@@ -22,8 +25,18 @@ pub enum Error {
Rate { retry_at: u64 },
}
-impl SMTP {
- pub async fn is_allowed<'x>(
+pub trait IsAllowed: Sync + Send {
+ fn is_allowed<'x>(
+ &'x self,
+ throttle: &'x Throttle,
+ envelope: &impl ResolveVariable,
+ in_flight: &mut Vec<InFlight>,
+ session_id: u64,
+ ) -> impl Future<Output = Result<(), Error>> + Send;
+}
+
+impl IsAllowed for Server {
+ async fn is_allowed<'x>(
&'x self,
throttle: &'x Throttle,
envelope: &impl ResolveVariable,
@@ -32,7 +45,6 @@ impl SMTP {
) -> Result<(), Error> {
if throttle.expr.is_empty()
|| self
- .core
.eval_expr(&throttle.expr, envelope, "throttle", session_id)
.await
.unwrap_or(false)
@@ -64,7 +76,7 @@ impl SMTP {
}
if let Some(concurrency) = &throttle.concurrency {
- match self.inner.queue_throttle.entry(key) {
+ match self.inner.data.smtp_queue_throttle.entry(key) {
Entry::Occupied(mut e) => {
let limiter = e.get_mut();
if let Some(inflight) = limiter.is_allowed() {
diff --git a/crates/smtp/src/reporting/analysis.rs b/crates/smtp/src/reporting/analysis.rs
index 99ded690..ee6b4230 100644
--- a/crates/smtp/src/reporting/analysis.rs
+++ b/crates/smtp/src/reporting/analysis.rs
@@ -12,6 +12,7 @@ use std::{
};
use ahash::AHashMap;
+use common::Server;
use mail_auth::{
flate2::read::GzDecoder,
report::{tlsrpt::TlsReport, ActionDisposition, DmarcResult, Feedback, Report},
@@ -25,8 +26,6 @@ use store::{
};
use trc::IncomingReportEvent;
-use crate::core::SMTP;
-
enum Compression {
None,
Gzip,
@@ -53,8 +52,12 @@ pub struct IncomingReport<T> {
pub report: T,
}
-impl SMTP {
- pub fn analyze_report(&self, message: Arc<Vec<u8>>, session_id: u64) {
+pub trait AnalyzeReport: Sync + Send {
+ fn analyze_report(&self, message: Arc<Vec<u8>>, session_id: u64);
+}
+
+impl AnalyzeReport for Server {
+ fn analyze_report(&self, message: Arc<Vec<u8>>, session_id: u64) {
let core = self.clone();
tokio::spawn(async move {
let message = if let Some(message) = MessageParser::default().parse(message.as_ref()) {
@@ -282,7 +285,7 @@ impl SMTP {
// Store report
if let Some(expires_in) = &core.core.smtp.report.analysis.store {
let expires = now() + expires_in.as_secs();
- let id = core.inner.queue_id_gen.generate().unwrap_or(expires);
+ let id = core.inner.data.queue_id_gen.generate().unwrap_or(expires);
let mut batch = BatchBuilder::new();
match report {
diff --git a/crates/smtp/src/reporting/dkim.rs b/crates/smtp/src/reporting/dkim.rs
index 9c3e4b4e..f0b4d682 100644
--- a/crates/smtp/src/reporting/dkim.rs
+++ b/crates/smtp/src/reporting/dkim.rs
@@ -11,7 +11,7 @@ use mail_auth::{
use trc::OutgoingReportEvent;
use utils::config::Rate;
-use crate::core::Session;
+use crate::{core::Session, reporting::SmtpReporting};
impl<T: SessionStream> Session<T> {
pub async fn send_dkim_report(
@@ -44,10 +44,9 @@ impl<T: SessionStream> Session<T> {
return;
}
- let config = &self.core.core.smtp.report.dkim;
+ let config = &self.server.core.smtp.report.dkim;
let from_addr = self
- .core
- .core
+ .server
.eval_if(&config.address, self, self.data.session_id)
.await
.unwrap_or_else(|| "MAILER-DAEMON@localhost".to_string());
@@ -64,8 +63,7 @@ impl<T: SessionStream> Session<T> {
.with_headers(std::str::from_utf8(message.raw_headers()).unwrap_or_default())
.write_rfc5322(
(
- self.core
- .core
+ self.server
.eval_if(&config.name, self, self.data.session_id)
.await
.unwrap_or_else(|| "Mail Delivery Subsystem".to_string())
@@ -74,8 +72,7 @@ impl<T: SessionStream> Session<T> {
),
rcpt,
&self
- .core
- .core
+ .server
.eval_if(&config.subject, self, self.data.session_id)
.await
.unwrap_or_else(|| "DKIM Report".to_string()),
@@ -91,7 +88,7 @@ impl<T: SessionStream> Session<T> {
);
// Send report
- self.core
+ self.server
.send_report(
&from_addr,
[rcpt].into_iter(),
diff --git a/crates/smtp/src/reporting/dmarc.rs b/crates/smtp/src/reporting/dmarc.rs
index 42ce6b38..5b106fea 100644
--- a/crates/smtp/src/reporting/dmarc.rs
+++ b/crates/smtp/src/reporting/dmarc.rs
@@ -4,10 +4,15 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
-use std::collections::hash_map::Entry;
+use std::{collections::hash_map::Entry, future::Future};
use ahash::AHashMap;
-use common::{config::smtp::report::AggregateFrequency, listener::SessionStream};
+use common::{
+ config::smtp::report::AggregateFrequency,
+ ipc::{DmarcEvent, ToHash},
+ listener::SessionStream,
+ Server,
+};
use mail_auth::{
common::verify::VerifySignature,
dmarc::{self, URI},
@@ -23,11 +28,12 @@ use trc::OutgoingReportEvent;
use utils::config::Rate;
use crate::{
- core::{Session, SMTP},
+ core::Session,
queue::{DomainPart, RecipientDomain},
+ reporting::SmtpReporting,
};
-use super::{scheduler::ToHash, AggregateTimestamp, DmarcEvent, ReportLock, SerializedSize};
+use super::{AggregateTimestamp, ReportLock, SerializedSize};
#[derive(Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct DmarcFormat {
@@ -48,19 +54,18 @@ impl<T: SessionStream> Session<T> {
arc_output: &Option<ArcOutput<'_>>,
) {
let dmarc_record = dmarc_output.dmarc_record_cloned().unwrap();
- let config = &self.core.core.smtp.report.dmarc;
+ let config = &self.server.core.smtp.report.dmarc;
// Send failure report
if let (Some(failure_rate), Some(report_options)) = (
- self.core
- .core
+ self.server
.eval_if::<Rate, _>(&config.send, self, self.data.session_id)
.await,
dmarc_output.failure_report(),
) {
// Verify that any external reporting addresses are authorized
let rcpts = match self
- .core
+ .server
.core
.smtp
.resolvers
@@ -113,8 +118,7 @@ impl<T: SessionStream> Session<T> {
if !rcpts.is_empty() {
let mut report = Vec::with_capacity(128);
let from_addr = self
- .core
- .core
+ .server
.eval_if(&config.address, self, self.data.session_id)
.await
.unwrap_or_else(|| "MAILER-DAEMON@localhost".to_string());
@@ -197,8 +201,7 @@ impl<T: SessionStream> Session<T> {
})
.write_rfc5322(
(
- self.core
- .core
+ self.server
.eval_if(&config.name, self, self.data.session_id)
.await
.unwrap_or_else(|| "Mail Delivery Subsystem".to_string())
@@ -207,8 +210,7 @@ impl<T: SessionStream> Session<T> {
),
&rcpts.join(", "),
&self
- .core
- .core
+ .server
.eval_if(&config.subject, self, self.data.session_id)
.await
.unwrap_or_else(|| "DMARC Report".to_string()),
@@ -227,7 +229,7 @@ impl<T: SessionStream> Session<T> {
);
// Send report
- self.core
+ self.server
.send_report(
&from_addr,
rcpts.into_iter(),
@@ -251,10 +253,9 @@ impl<T: SessionStream> Session<T> {
// Send aggregate reports
let interval = self
- .core
- .core
+ .server
.eval_if(
- &self.core.core.smtp.report.dmarc_aggregate.send,
+ &self.server.core.smtp.report.dmarc_aggregate.send,
self,
self.data.session_id,
)
@@ -289,7 +290,7 @@ impl<T: SessionStream> Session<T> {
}
// Submit DMARC report event
- self.core
+ self.server
.schedule_report(DmarcEvent {
domain: dmarc_output.into_domain(),
report_record,
@@ -300,9 +301,22 @@ impl<T: SessionStream> Session<T> {
}
}
-impl SMTP {
- pub async fn send_dmarc_aggregate_report(&self, event: ReportEvent) {
- let span_id = self.inner.span_id_gen.generate().unwrap_or_else(now);
+pub trait DmarcReporting: Sync + Send {
+ fn send_dmarc_aggregate_report(&self, event: ReportEvent) -> impl Future<Output = ()> + Send;
+ fn generate_dmarc_aggregate_report(
+ &self,
+ event: &ReportEvent,
+ rua: &mut Vec<URI>,
+ serialized_size: Option<&mut serde_json::Serializer<SerializedSize>>,
+ span_id: u64,
+ ) -> impl Future<Output = trc::Result<Option<Report>>> + Send;
+ fn delete_dmarc_report(&self, event: ReportEvent) -> impl Future<Output = ()> + Send;
+ fn schedule_dmarc(&self, event: Box<DmarcEvent>) -> impl Future<Output = ()> + Send;
+}
+
+impl DmarcReporting for Server {
+ async fn send_dmarc_aggregate_report(&self, event: ReportEvent) {
+ let span_id = self.inner.data.span_id_gen.generate().unwrap_or_else(now);
trc::event!(
OutgoingReport(OutgoingReportEvent::DmarcAggregateReport),
@@ -315,14 +329,13 @@ impl SMTP {
// Generate report
let mut serialized_size = serde_json::Serializer::new(SerializedSize::new(
- self.core
- .eval_if(
- &self.core.smtp.report.dmarc_aggregate.max_size,
- &RecipientDomain::new(event.domain.as_str()),
- span_id,
- )
- .await
- .unwrap_or(25 * 1024 * 1024),
+ self.eval_if(
+ &self.core.smtp.report.dmarc_aggregate.max_size,
+ &RecipientDomain::new(event.domain.as_str()),
+ span_id,
+ )
+ .await
+ .unwrap_or(25 * 1024 * 1024),
));
let mut rua = Vec::new();
let report = match self
@@ -392,7 +405,6 @@ impl SMTP {
// Serialize report
let config = &self.core.smtp.report.dmarc_aggregate;
let from_addr = self
- .core
.eval_if(
&config.address,
&RecipientDomain::new(event.domain.as_str()),
@@ -403,7 +415,6 @@ impl SMTP {
let mut message = Vec::with_capacity(2048);
let _ = report.write_rfc5322(
&self
- .core
.eval_if(
&self.core.smtp.report.submitter,
&RecipientDomain::new(event.domain.as_str()),
@@ -412,15 +423,14 @@ impl SMTP {
.await
.unwrap_or_else(|| "localhost".to_string()),
(
- self.core
- .eval_if(
- &config.name,
- &RecipientDomain::new(event.domain.as_str()),
- span_id,
- )
- .await
- .unwrap_or_else(|| "Mail Delivery Subsystem".to_string())
- .as_str(),
+ self.eval_if(
+ &config.name,
+ &RecipientDomain::new(event.domain.as_str()),
+ span_id,
+ )
+ .await
+ .unwrap_or_else(|| "Mail Delivery Subsystem".to_string())
+ .as_str(),
from_addr.as_str(),
),
rua.iter().map(|a| a.as_str()),
@@ -441,7 +451,7 @@ impl SMTP {
self.delete_dmarc_report(event).await;
}
- pub async fn generate_dmarc_aggregate_report(
+ async fn generate_dmarc_aggregate_report(
&self,
event: &ReportEvent,
rua: &mut Vec<URI>,
@@ -473,17 +483,15 @@ impl SMTP {
.with_date_range_end(event.due)
.with_report_id(format!("{}_{}", event.policy_hash, event.seq_id))
.with_email(
- self.core
- .eval_if(
- &config.address,
- &RecipientDomain::new(event.domain.as_str()),
- span_id,
- )
- .await
- .unwrap_or_else(|| "MAILER-DAEMON@localhost".to_string()),
+ self.eval_if(
+ &config.address,
+ &RecipientDomain::new(event.domain.as_str()),
+ span_id,
+ )
+ .await
+ .unwrap_or_else(|| "MAILER-DAEMON@localhost".to_string()),
);
if let Some(org_name) = self
- .core
.eval_if::<String, _>(
&config.org_name,
&RecipientDomain::new(event.domain.as_str()),
@@ -494,7 +502,6 @@ impl SMTP {
report = report.with_org_name(org_name);
}
if let Some(contact_info) = self
- .core
.eval_if::<String, _>(
&config.contact_info,
&RecipientDomain::new(event.domain.as_str()),
@@ -561,7 +568,7 @@ impl SMTP {
Ok(Some(report))
}
- pub async fn delete_dmarc_report(&self, event: ReportEvent) {
+ async fn delete_dmarc_report(&self, event: ReportEvent) {
let from_key = ReportEvent {
due: event.due,
policy_hash: event.policy_hash,
@@ -600,7 +607,7 @@ impl SMTP {
}
}
- pub async fn schedule_dmarc(&self, event: Box<DmarcEvent>) {
+ async fn schedule_dmarc(&self, event: Box<DmarcEvent>) {
let created = event.interval.to_timestamp();
let deliver_at = created + event.interval.as_secs();
let mut report_event = ReportEvent {
@@ -647,7 +654,7 @@ impl SMTP {
}
// Write entry
- report_event.seq_id = self.inner.queue_id_gen.generate().unwrap_or_else(now);
+ report_event.seq_id = self.inner.data.queue_id_gen.generate().unwrap_or_else(now);
builder.set(
ValueClass::Queue(QueueClass::DmarcReportEvent(report_event)),
Bincode::new(event.report_record).serialize(),
diff --git a/crates/smtp/src/reporting/mod.rs b/crates/smtp/src/reporting/mod.rs
index fedd6689..92c81777 100644
--- a/crates/smtp/src/reporting/mod.rs
+++ b/crates/smtp/src/reporting/mod.rs
@@ -4,23 +4,17 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
-use std::{io, sync::Arc, time::SystemTime};
+use std::{future::Future, io, time::SystemTime};
use common::{
- config::smtp::{
- report::{AddressMatch, AggregateFrequency},
- resolver::{Policy, Tlsa},
- },
+ config::smtp::report::{AddressMatch, AggregateFrequency},
expr::if_block::IfBlock,
- USER_AGENT,
+ ipc::ReportingEvent,
+ Server, USER_AGENT,
};
use mail_auth::{
common::headers::HeaderWriter,
- dmarc::Dmarc,
- mta_sts::TlsRpt,
- report::{
- tlsrpt::FailureDetails, AuthFailureType, DeliveryResult, Feedback, FeedbackType, Record,
- },
+ report::{AuthFailureType, DeliveryResult, Feedback, FeedbackType},
};
use mail_parser::DateTime;
@@ -28,9 +22,9 @@ use store::write::{QueueClass, ReportEvent};
use tokio::io::{AsyncRead, AsyncWrite};
use crate::{
- core::{Session, SMTP},
+ core::Session,
inbound::DkimSign,
- queue::{DomainPart, Message, MessageSource},
+ queue::{spool::SmtpSpool, DomainPart, Message, MessageSource},
};
pub mod analysis;
@@ -40,37 +34,6 @@ pub mod scheduler;
pub mod spf;
pub mod tls;
-#[derive(Debug)]
-pub enum Event {
- Dmarc(Box<DmarcEvent>),
- Tls(Box<TlsEvent>),
- Stop,
-}
-
-#[derive(Debug)]
-pub struct DmarcEvent {
- pub domain: String,
- pub report_record: Record,
- pub dmarc_record: Arc<Dmarc>,
- pub interval: AggregateFrequency,
-}
-
-#[derive(Debug)]
-pub struct TlsEvent {
- pub domain: String,
- pub policy: PolicyType,
- pub failure: Option<FailureDetails>,
- pub tls_record: Arc<TlsRpt>,
- pub interval: AggregateFrequency,
-}
-
-#[derive(Debug, Hash, PartialEq, Eq)]
-pub enum PolicyType {
- Tlsa(Option<Arc<Tlsa>>),
- Sts(Option<Arc<Policy>>),
- None,
-}
-
impl<T: AsyncWrite + AsyncRead + Unpin> Session<T> {
pub fn new_auth_failure(&self, ft: AuthFailureType, rejected: bool) -> Feedback<'_> {
Feedback::new(FeedbackType::AuthFailure)
@@ -91,7 +54,7 @@ impl<T: AsyncWrite + AsyncRead + Unpin> Session<T> {
}
pub fn is_report(&self) -> bool {
- for addr_match in &self.core.core.smtp.report.analysis.addresses {
+ for addr_match in &self.server.core.smtp.report.analysis.addresses {
for addr in &self.data.rcpt_to {
match addr_match {
AddressMatch::StartsWith(prefix) if addr.address_lcase.starts_with(prefix) => {
@@ -110,11 +73,44 @@ impl<T: AsyncWrite + AsyncRead + Unpin> Session<T> {
}
}
-impl SMTP {
- pub async fn send_report(
+pub trait SmtpReporting: Sync + Send {
+ fn send_report(
+ &self,
+ from_addr: &str,
+ rcpts: impl Iterator<Item = impl AsRef<str> + Sync + Send> + Sync + Send,
+ report: Vec<u8>,
+ sign_config: &IfBlock,
+ deliver_now: bool,
+ parent_session_id: u64,
+ ) -> impl Future<Output = ()> + Send;
+
+ fn send_autogenerated(
+ &self,
+ from_addr: impl Into<String> + Sync + Send,
+ rcpts: impl Iterator<Item = impl Into<String> + Sync + Send> + Sync + Send,
+ raw_message: Vec<u8>,
+ sign_config: Option<&IfBlock>,
+ parent_session_id: u64,
+ ) -> impl Future<Output = ()> + Send;
+
+ fn schedule_report(
+ &self,
+ report: impl Into<ReportingEvent> + Sync + Send,
+ ) -> impl Future<Output = ()> + Send;
+
+ fn sign_message(
+ &self,
+ message: &mut Message,
+ config: &IfBlock,
+ bytes: &[u8],
+ ) -> impl Future<Output = Option<Vec<u8>>> + Send;
+}
+
+impl SmtpReporting for Server {
+ async fn send_report(
&self,
from_addr: &str,
- rcpts: impl Iterator<Item = impl AsRef<str>>,
+ rcpts: impl Iterator<Item = impl AsRef<str> + Sync + Send> + Sync + Send,
report: Vec<u8>,
sign_config: &IfBlock,
deliver_now: bool,
@@ -163,10 +159,10 @@ impl SMTP {
.await;
}
- pub async fn send_autogenerated(
+ async fn send_autogenerated(
&self,
- from_addr: impl Into<String>,
- rcpts: impl Iterator<Item = impl Into<String>>,
+ from_addr: impl Into<String> + Sync + Send,
+ rcpts: impl Iterator<Item = impl Into<String> + Sync + Send> + Sync + Send,
raw_message: Vec<u8>,
sign_config: Option<&IfBlock>,
parent_session_id: u64,
@@ -205,8 +201,8 @@ impl SMTP {
.await;
}
- pub async fn schedule_report(&self, report: impl Into<Event>) {
- if self.inner.report_tx.send(report.into()).await.is_err() {
+ async fn schedule_report(&self, report: impl Into<ReportingEvent> + Sync + Send) {
+ if self.inner.ipc.report_tx.send(report.into()).await.is_err() {
trc::event!(
Server(trc::ServerEvent::ThreadError),
CausedBy = trc::location!(),
@@ -215,21 +211,20 @@ impl SMTP {
}
}
- pub async fn sign_message(
+ async fn sign_message(
&self,
message: &mut Message,
config: &IfBlock,
bytes: &[u8],
) -> Option<Vec<u8>> {
let signers = self
- .core
.eval_if::<Vec<String>, _>(config, message, message.span_id)
.await
.unwrap_or_default();
if !signers.is_empty() {
let mut headers = Vec::with_capacity(64);
for signer in signers.iter() {
- if let Some(signer) = self.core.get_dkim_signer(signer, message.span_id) {
+ if let Some(signer) = self.get_dkim_signer(signer, message.span_id) {
match signer.sign(bytes) {
Ok(signature) => {
signature.write_header(&mut headers);
@@ -300,52 +295,6 @@ impl AggregateTimestamp for AggregateFrequency {
}
}
-impl From<DmarcEvent> for Event {
- fn from(value: DmarcEvent) -> Self {
- Event::Dmarc(Box::new(value))
- }
-}
-
-impl From<TlsEvent> for Event {
- fn from(value: TlsEvent) -> Self {
- Event::Tls(Box::new(value))
- }
-}
-
-impl From<Arc<Tlsa>> for PolicyType {
- fn from(value: Arc<Tlsa>) -> Self {
- PolicyType::Tlsa(Some(value))
- }
-}
-
-impl From<Arc<Policy>> for PolicyType {
- fn from(value: Arc<Policy>) -> Self {
- PolicyType::Sts(Some(value))
- }
-}
-
-impl From<&Arc<Tlsa>> for PolicyType {
- fn from(value: &Arc<Tlsa>) -> Self {
- PolicyType::Tlsa(Some(value.clone()))
- }
-}
-
-impl From<&Arc<Policy>> for PolicyType {
- fn from(value: &Arc<Policy>) -> Self {
- PolicyType::Sts(Some(value.clone()))
- }
-}
-
-impl From<(&Option<Arc<Policy>>, &Option<Arc<Tlsa>>)> for PolicyType {
- fn from(value: (&Option<Arc<Policy>>, &Option<Arc<Tlsa>>)) -> Self {
- match value {
- (Some(value), _) => PolicyType::Sts(Some(value.clone())),
- (_, Some(value)) => PolicyType::Tlsa(Some(value.clone())),
- _ => PolicyType::None,
- }
- }
-}
-
pub struct SerializedSize {
bytes_left: usize,
}
diff --git a/crates/smtp/src/reporting/scheduler.rs b/crates/smtp/src/reporting/scheduler.rs
index 83a1c354..ba1c9fc3 100644
--- a/crates/smtp/src/reporting/scheduler.rs
+++ b/crates/smtp/src/reporting/scheduler.rs
@@ -4,34 +4,33 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
-use ahash::{AHashMap, RandomState};
-use common::Core;
-use mail_auth::dmarc::Dmarc;
+use ahash::AHashMap;
+use common::{core::BuildServer, ipc::ReportingEvent, Inner, Server};
-use std::time::{Duration, Instant, SystemTime};
+use std::{
+ future::Future,
+ sync::Arc,
+ time::{Duration, SystemTime},
+};
use store::{
write::{now, BatchBuilder, QueueClass, ReportEvent, ValueClass},
- Deserialize, IterateParams, Key, Serialize, ValueKey,
+ Deserialize, IterateParams, Key, Serialize, Store, ValueKey,
};
use tokio::sync::mpsc;
-use crate::{
- core::{SmtpInstance, SMTP},
- queue::{manager::LONG_WAIT, spool::LOCK_EXPIRY},
-};
+use crate::queue::{manager::LONG_WAIT, spool::LOCK_EXPIRY};
-use super::{Event, ReportLock};
+use super::{dmarc::DmarcReporting, tls::TlsReporting, ReportLock};
-impl SpawnReport for mpsc::Receiver<Event> {
- fn spawn(mut self, core: SmtpInstance) {
+impl SpawnReport for mpsc::Receiver<ReportingEvent> {
+ fn spawn(mut self, inner: Arc<Inner>) {
tokio::spawn(async move {
- let mut last_cleanup = Instant::now();
let mut next_wake_up;
loop {
// Read events
let now = now();
- let events = next_report_event(&core.core.load_full()).await;
+ let events = next_report_event(inner.shared_core.load().storage.data.clone()).await;
next_wake_up = events
.last()
.and_then(|e| match e {
@@ -44,15 +43,15 @@ impl SpawnReport for mpsc::Receiver<Event> {
})
.unwrap_or(LONG_WAIT);
- let core = SMTP::from(core.clone());
- let core_ = core.clone();
+ let server = inner.build_server();
+ let server_ = server.clone();
tokio::spawn(async move {
let mut tls_reports = AHashMap::new();
for report_event in events {
match report_event {
QueueClass::DmarcReportHeader(event) if event.due <= now => {
- if core_.try_lock_report(QueueClass::dmarc_lock(&event)).await {
- core_.send_dmarc_aggregate_report(event).await;
+ if server.try_lock_report(QueueClass::dmarc_lock(&event)).await {
+ server.send_dmarc_aggregate_report(event).await;
}
}
QueueClass::TlsReportHeader(event) if event.due <= now => {
@@ -66,40 +65,34 @@ impl SpawnReport for mpsc::Receiver<Event> {
}
for (_, tls_report) in tls_reports {
- if core_
+ if server
.try_lock_report(QueueClass::tls_lock(tls_report.first().unwrap()))
.await
{
- core_.send_tls_aggregate_report(tls_report).await;
+ server.send_tls_aggregate_report(tls_report).await;
}
}
});
match tokio::time::timeout(next_wake_up, self.recv()).await {
Ok(Some(event)) => match event {
- Event::Dmarc(event) => {
- core.schedule_dmarc(event).await;
+ ReportingEvent::Dmarc(event) => {
+ server_.schedule_dmarc(event).await;
}
- Event::Tls(event) => {
- core.schedule_tls(event).await;
+ ReportingEvent::Tls(event) => {
+ server_.schedule_tls(event).await;
}
- Event::Stop => break,
+ ReportingEvent::Stop => break,
},
Ok(None) => break,
- Err(_) => {
- // Cleanup expired throttles
- if last_cleanup.elapsed().as_secs() >= 86400 {
- last_cleanup = Instant::now();
- core.cleanup();
- }
- }
+ Err(_) => {}
}
}
});
}
}
-async fn next_report_event(core: &Core) -> Vec<QueueClass> {
+async fn next_report_event(store: Store) -> Vec<QueueClass> {
let from_key = ValueKey::from(ValueClass::Queue(QueueClass::DmarcReportHeader(
ReportEvent {
due: 0,
@@ -119,9 +112,7 @@ async fn next_report_event(core: &Core) -> Vec<QueueClass> {
let mut events = Vec::new();
let now = now();
- let result = core
- .storage
- .data
+ let result = store
.iterate(
IterateParams::new(from_key, to_key).ascending().no_values(),
|key, _| {
@@ -150,13 +141,15 @@ async fn next_report_event(core: &Core) -> Vec<QueueClass> {
events
}
-impl SMTP {
- pub async fn try_lock_report(&self, lock: QueueClass) -> bool {
+pub trait LockReport: Sync + Send {
+ fn try_lock_report(&self, lock: QueueClass) -> impl Future<Output = bool> + Send;
+}
+
+impl LockReport for Server {
+ async fn try_lock_report(&self, lock: QueueClass) -> bool {
let now = now();
match self
- .core
- .storage
- .data
+ .store()
.get_value::<u64>(ValueKey::from(ValueClass::Queue(lock.clone())))
.await
{
@@ -216,22 +209,6 @@ impl SMTP {
}
}
-pub trait ToHash {
- fn to_hash(&self) -> u64;
-}
-
-impl ToHash for Dmarc {
- fn to_hash(&self) -> u64 {
- RandomState::with_seeds(1, 9, 7, 9).hash_one(self)
- }
-}
-
-impl ToHash for super::PolicyType {
- fn to_hash(&self) -> u64 {
- RandomState::with_seeds(1, 9, 7, 9).hash_one(self)
- }
-}
-
pub trait ToTimestamp {
fn to_timestamp(&self) -> u64;
}
@@ -246,5 +223,5 @@ impl ToTimestamp for Duration {
}
pub trait SpawnReport {
- fn spawn(self, core: SmtpInstance);
+ fn spawn(self, core: Arc<Inner>);
}
diff --git a/crates/smtp/src/reporting/spf.rs b/crates/smtp/src/reporting/spf.rs
index 830a2906..8b7c82b7 100644
--- a/crates/smtp/src/reporting/spf.rs
+++ b/crates/smtp/src/reporting/spf.rs
@@ -9,7 +9,7 @@ use mail_auth::{report::AuthFailureType, AuthenticationResults, SpfOutput};
use trc::OutgoingReportEvent;
use utils::config::Rate;
-use crate::core::Session;
+use crate::{core::Session, reporting::SmtpReporting};
impl<T: SessionStream> Session<T> {
pub async fn send_spf_report(
@@ -35,10 +35,9 @@ impl<T: SessionStream> Session<T> {
}
// Generate report
- let config = &self.core.core.smtp.report.spf;
+ let config = &self.server.core.smtp.report.spf;
let from_addr = self
- .core
- .core
+ .server
.eval_if(&config.address, self, self.data.session_id)
.await
.unwrap_or_else(|| "MAILER-DAEMON@localhost".to_string());
@@ -64,8 +63,7 @@ impl<T: SessionStream> Session<T> {
.with_spf_dns(format!("txt : {} : v=SPF1", output.domain())) // TODO use DNS record
.write_rfc5322(
(
- self.core
- .core
+ self.server
.eval_if(&config.name, self, self.data.session_id)
.await
.unwrap_or_else(|| "Mailer Daemon".to_string())
@@ -74,8 +72,7 @@ impl<T: SessionStream> Session<T> {
),
rcpt,
&self
- .core
- .core
+ .server
.eval_if(&config.subject, self, self.data.session_id)
.await
.unwrap_or_else(|| "SPF Report".to_string()),
@@ -91,7 +88,7 @@ impl<T: SessionStream> Session<T> {
);
// Send report
- self.core
+ self.server
.send_report(
&from_addr,
[rcpt].into_iter(),
diff --git a/crates/smtp/src/reporting/tls.rs b/crates/smtp/src/reporting/tls.rs
index 5fa82a75..0430badb 100644
--- a/crates/smtp/src/reporting/tls.rs
+++ b/crates/smtp/src/reporting/tls.rs
@@ -4,7 +4,7 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
-use std::{collections::hash_map::Entry, sync::Arc, time::Duration};
+use std::{collections::hash_map::Entry, future::Future, sync::Arc, time::Duration};
use ahash::AHashMap;
use common::{
@@ -12,7 +12,8 @@ use common::{
report::AggregateFrequency,
resolver::{Mode, MxPattern},
},
- USER_AGENT,
+ ipc::{TlsEvent, ToHash},
+ Server, USER_AGENT,
};
use mail_auth::{
flate2::{write::GzEncoder, Compression},
@@ -31,9 +32,9 @@ use store::{
};
use trc::OutgoingReportEvent;
-use crate::{core::SMTP, queue::RecipientDomain};
+use crate::{queue::RecipientDomain, reporting::SmtpReporting};
-use super::{scheduler::ToHash, AggregateTimestamp, ReportLock, SerializedSize, TlsEvent};
+use super::{AggregateTimestamp, ReportLock, SerializedSize};
#[derive(Debug, Clone)]
pub struct TlsRptOptions {
@@ -51,14 +52,30 @@ pub struct TlsFormat {
#[cfg(feature = "test_mode")]
pub static TLS_HTTP_REPORT: parking_lot::Mutex<Vec<u8>> = parking_lot::Mutex::new(Vec::new());
-impl SMTP {
- pub async fn send_tls_aggregate_report(&self, events: Vec<ReportEvent>) {
+pub trait TlsReporting: Sync + Send {
+ fn send_tls_aggregate_report(
+ &self,
+ events: Vec<ReportEvent>,
+ ) -> impl Future<Output = ()> + Send;
+ fn generate_tls_aggregate_report(
+ &self,
+ events: &[ReportEvent],
+ rua: &mut Vec<ReportUri>,
+ serialized_size: Option<&mut serde_json::Serializer<SerializedSize>>,
+ span_id: u64,
+ ) -> impl Future<Output = trc::Result<Option<TlsReport>>> + Send;
+ fn schedule_tls(&self, event: Box<TlsEvent>) -> impl Future<Output = ()> + Send;
+ fn delete_tls_report(&self, events: Vec<ReportEvent>) -> impl Future<Output = ()> + Send;
+}
+
+impl TlsReporting for Server {
+ async fn send_tls_aggregate_report(&self, events: Vec<ReportEvent>) {
let (domain_name, event_from, event_to) = events
.first()
.map(|e| (e.domain.as_str(), e.seq_id, e.due))
.unwrap();
- let span_id = self.inner.span_id_gen.generate().unwrap_or_else(now);
+ let span_id = self.inner.data.span_id_gen.generate().unwrap_or_else(now);
trc::event!(
OutgoingReport(OutgoingReportEvent::TlsAggregate),
@@ -72,14 +89,13 @@ impl SMTP {
// Generate report
let mut rua = Vec::new();
let mut serialized_size = serde_json::Serializer::new(SerializedSize::new(
- self.core
- .eval_if(
- &self.core.smtp.report.tls.max_size,
- &RecipientDomain::new(domain_name),
- span_id,
- )
- .await
- .unwrap_or(25 * 1024 * 1024),
+ self.eval_if(
+ &self.core.smtp.report.tls.max_size,
+ &RecipientDomain::new(domain_name),
+ span_id,
+ )
+ .await
+ .unwrap_or(25 * 1024 * 1024),
));
let report = match self
.generate_tls_aggregate_report(&events, &mut rua, Some(&mut serialized_size), span_id)
@@ -191,7 +207,6 @@ impl SMTP {
if !rcpts.is_empty() {
let config = &self.core.smtp.report.tls;
let from_addr = self
- .core
.eval_if(&config.address, &RecipientDomain::new(domain_name), span_id)
.await
.unwrap_or_else(|| "MAILER-DAEMON@localhost".to_string());
@@ -199,7 +214,6 @@ impl SMTP {
let _ = report.write_rfc5322_from_bytes(
domain_name,
&self
- .core
.eval_if(
&self.core.smtp.report.submitter,
&RecipientDomain::new(domain_name),
@@ -208,8 +222,7 @@ impl SMTP {
.await
.unwrap_or_else(|| "localhost".to_string()),
(
- self.core
- .eval_if(&config.name, &RecipientDomain::new(domain_name), span_id)
+ self.eval_if(&config.name, &RecipientDomain::new(domain_name), span_id)
.await
.unwrap_or_else(|| "Mail Delivery Subsystem".to_string())
.as_str(),
@@ -239,7 +252,7 @@ impl SMTP {
self.delete_tls_report(events).await;
}
- pub async fn generate_tls_aggregate_report(
+ async fn generate_tls_aggregate_report(
&self,
events: &[ReportEvent],
rua: &mut Vec<ReportUri>,
@@ -253,7 +266,6 @@ impl SMTP {
let config = &self.core.smtp.report.tls;
let mut report = TlsReport {
organization_name: self
- .core
.eval_if(
&config.org_name,
&RecipientDomain::new(domain_name),
@@ -266,7 +278,6 @@ impl SMTP {
end_datetime: DateTime::from_timestamp(event_to as i64),
},
contact_info: self
- .core
.eval_if(
&config.contact_info,
&RecipientDomain::new(domain_name),
@@ -388,7 +399,7 @@ impl SMTP {
})
}
- pub async fn schedule_tls(&self, event: Box<TlsEvent>) {
+ async fn schedule_tls(&self, event: Box<TlsEvent>) {
let created = event.interval.to_timestamp();
let deliver_at = created + event.interval.as_secs();
let mut report_event = ReportEvent {
@@ -420,7 +431,7 @@ impl SMTP {
};
match event.policy {
- super::PolicyType::Tlsa(tlsa) => {
+ common::ipc::PolicyType::Tlsa(tlsa) => {
policy.policy_type = PolicyType::Tlsa;
if let Some(tlsa) = tlsa {
for entry in &tlsa.entries {
@@ -440,7 +451,7 @@ impl SMTP {
}
}
}
- super::PolicyType::Sts(sts) => {
+ common::ipc::PolicyType::Sts(sts) => {
policy.policy_type = PolicyType::Sts;
if let Some(sts) = sts {
policy.policy_string.push("version: STSv1".to_string());
@@ -489,7 +500,7 @@ impl SMTP {
}
// Write entry
- report_event.seq_id = self.inner.queue_id_gen.generate().unwrap_or_else(now);
+ report_event.seq_id = self.inner.data.queue_id_gen.generate().unwrap_or_else(now);
builder.set(
ValueClass::Queue(QueueClass::TlsReportEvent(report_event)),
Bincode::new(event.failure).serialize(),
@@ -502,7 +513,7 @@ impl SMTP {
}
}
- pub async fn delete_tls_report(&self, events: Vec<ReportEvent>) {
+ async fn delete_tls_report(&self, events: Vec<ReportEvent>) {
let mut batch = BatchBuilder::new();
for (pos, event) in events.into_iter().enumerate() {
diff --git a/crates/smtp/src/scripts/event_loop.rs b/crates/smtp/src/scripts/event_loop.rs
index 8dcc1d15..da6885cd 100644
--- a/crates/smtp/src/scripts/event_loop.rs
+++ b/crates/smtp/src/scripts/event_loop.rs
@@ -4,9 +4,9 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
-use std::{borrow::Cow, sync::Arc, time::Instant};
+use std::{borrow::Cow, future::Future, sync::Arc, time::Instant};
-use common::scripts::plugins::PluginContext;
+use common::{scripts::plugins::PluginContext, Server};
use mail_auth::common::headers::HeaderWriter;
use sieve::{
compiler::grammar::actions::action_redirect::{ByMode, ByTime, Notify, NotifyItem, Ret},
@@ -19,15 +19,24 @@ use smtp_proto::{
use trc::SieveEvent;
use crate::{
- core::SMTP,
inbound::DkimSign,
- queue::{DomainPart, MessageSource},
+ queue::{quota::HasQueueQuota, spool::SmtpSpool, DomainPart, MessageSource},
};
use super::{ScriptModification, ScriptParameters, ScriptResult};
-impl SMTP {
- pub async fn run_script(
+pub trait RunScript: Sync + Send {
+ fn run_script(
+ &self,
+ script_id: String,
+ script: Arc<Sieve>,
+ params: ScriptParameters<'_>,
+ session_id: u64,
+ ) -> impl Future<Output = ScriptResult> + Send;
+}
+
+impl RunScript for Server {
+ async fn run_script(
&self,
script_id: String,
script: Arc<Sieve>,
@@ -112,8 +121,7 @@ impl SMTP {
id,
PluginContext {
session_id,
- core: &self.core,
- cache: &self.inner.script_cache,
+ server: self,
message: instance.message(),
modifications: &mut modifications,
arguments,
@@ -264,8 +272,7 @@ impl SMTP {
let mut headers = Vec::new();
for dkim in &params.sign {
- if let Some(dkim) = self.core.get_dkim_signer(dkim, session_id)
- {
+ if let Some(dkim) = self.get_dkim_signer(dkim, session_id) {
match dkim.sign(raw_message) {
Ok(signature) => {
signature.write_header(&mut headers);
diff --git a/crates/smtp/src/scripts/exec.rs b/crates/smtp/src/scripts/exec.rs
index ec85c80b..7b92ccfa 100644
--- a/crates/smtp/src/scripts/exec.rs
+++ b/crates/smtp/src/scripts/exec.rs
@@ -13,7 +13,7 @@ use smtp_proto::*;
use crate::{core::Session, inbound::AuthResult};
-use super::{ScriptParameters, ScriptResult};
+use super::{event_loop::RunScript, ScriptParameters, ScriptResult};
impl<T: SessionStream> Session<T> {
pub fn build_script_parameters(&self, stage: &'static str) -> ScriptParameters<'_> {
@@ -124,12 +124,12 @@ impl<T: SessionStream> Session<T> {
script: Arc<Sieve>,
params: ScriptParameters<'_>,
) -> ScriptResult {
- self.core
+ self.server
.run_script(
script_id,
script,
params
- .with_envelope(&self.core.core, self, self.data.session_id)
+ .with_envelope(&self.server, self, self.data.session_id)
.await,
self.data.session_id,
)
diff --git a/crates/smtp/src/scripts/mod.rs b/crates/smtp/src/scripts/mod.rs
index ee3c3ca7..cd162731 100644
--- a/crates/smtp/src/scripts/mod.rs
+++ b/crates/smtp/src/scripts/mod.rs
@@ -7,7 +7,7 @@
use std::borrow::Cow;
use ahash::AHashMap;
-use common::{expr::functions::ResolveVariable, scripts::ScriptModification, Core};
+use common::{expr::functions::ResolveVariable, scripts::ScriptModification, Server};
use sieve::{runtime::Variable, Envelope};
pub mod envelope;
@@ -58,20 +58,23 @@ impl<'x> ScriptParameters<'x> {
pub async fn with_envelope(
mut self,
- core: &Core,
+ server: &Server,
vars: &impl ResolveVariable,
session_id: u64,
) -> Self {
for (variable, expr) in [
- (&mut self.from_addr, &core.sieve.from_addr),
- (&mut self.from_name, &core.sieve.from_name),
- (&mut self.return_path, &core.sieve.return_path),
+ (&mut self.from_addr, &server.core.sieve.from_addr),
+ (&mut self.from_name, &server.core.sieve.from_name),
+ (&mut self.return_path, &server.core.sieve.return_path),
] {
- if let Some(value) = core.eval_if(expr, vars, session_id).await {
+ if let Some(value) = server.eval_if(expr, vars, session_id).await {
*variable = value;
}
}
- if let Some(value) = core.eval_if(&core.sieve.sign, vars, session_id).await {
+ if let Some(value) = server
+ .eval_if(&server.core.sieve.sign, vars, session_id)
+ .await
+ {
self.sign = value;
}
self
diff --git a/crates/utils/src/snowflake.rs b/crates/utils/src/snowflake.rs
index a8619b89..99e8ecc7 100644
--- a/crates/utils/src/snowflake.rs
+++ b/crates/utils/src/snowflake.rs
@@ -87,3 +87,13 @@ impl Default for SnowflakeIdGenerator {
Self::new()
}
}
+
+impl Clone for SnowflakeIdGenerator {
+ fn clone(&self) -> Self {
+ Self {
+ epoch: self.epoch,
+ node_id: self.node_id,
+ sequence: 0.into(),
+ }
+ }
+}