diff --git a/Cargo.lock b/Cargo.lock index 51206d12..192527fb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2926,6 +2926,7 @@ dependencies = [ "email", "form-data", "groupware", + "hashify", "http-body-util", "http_proto", "hyper 1.8.1", diff --git a/crates/common/src/auth/access_token.rs b/crates/common/src/auth/access_token.rs index 946791a5..cad0e6c5 100644 --- a/crates/common/src/auth/access_token.rs +++ b/crates/common/src/auth/access_token.rs @@ -14,6 +14,7 @@ use crate::{ network::limiter::{ConcurrencyLimiter, LimiterResult}, }; use ahash::AHasher; +use chrono::format::Item; use registry::{ schema::{ enums::Permission, @@ -517,6 +518,7 @@ impl AccessToken { } } + #[inline(always)] pub fn credential_id(&self) -> Option { self.inner .scopes @@ -524,6 +526,11 @@ impl AccessToken { .map(|scope| scope.credential_id) } + #[inline(always)] + pub fn revision(&self) -> u64 { + self.inner.revision + } + pub fn assert_has_permissions(self, permissions: &[Permission]) -> trc::Result { for permission in permissions { if !self.has_permission(*permission) { @@ -672,14 +679,38 @@ impl AccessToken { } } - #[cfg(feature = "test_mode")] + pub fn from_permissions( + account_id: u32, + set_permissions: impl IntoIterator, + ) -> AccessToken { + let mut permissions = Permissions::new(); + for permission in set_permissions { + permissions.set(permission as usize); + } + AccessToken { + scope_idx: 0, + inner: Arc::new(AccessTokenInner { + account_id, + tenant_id: Default::default(), + member_of: Default::default(), + access_to: Default::default(), + scopes: Box::new([AccessScope::new(permissions, u32::MAX)]), + concurrent_http_requests: Default::default(), + concurrent_imap_requests: Default::default(), + concurrent_uploads: Default::default(), + revision: Default::default(), + revision_account: Default::default(), + obj_size: Default::default(), + }), + } + } + pub fn from_id(account_id: u32) -> Self { AccessToken::new(Arc::new(AccessTokenInner::from_id(account_id))) } } impl AccessTokenInner { - #[cfg(feature = "test_mode")] pub fn from_id(account_id: u32) -> Self { Self { account_id, diff --git a/crates/common/src/auth/mod.rs b/crates/common/src/auth/mod.rs index dac27789..35858e71 100644 --- a/crates/common/src/auth/mod.rs +++ b/crates/common/src/auth/mod.rs @@ -97,7 +97,7 @@ pub struct PermissionsGroup { pub merge: bool, } -#[derive(Debug, Default)] +#[derive(Debug, Default, Clone)] pub struct AccessToken { scope_idx: usize, inner: Arc, diff --git a/crates/common/src/auth/oauth/mod.rs b/crates/common/src/auth/oauth/mod.rs index 5bb0a9d8..3e5170fa 100644 --- a/crates/common/src/auth/oauth/mod.rs +++ b/crates/common/src/auth/oauth/mod.rs @@ -24,7 +24,7 @@ pub enum GrantType { RefreshToken, LiveTracing, LiveMetrics, - Troubleshoot, + Diagnose, Rsvp, } @@ -35,7 +35,7 @@ impl GrantType { GrantType::RefreshToken => "refresh_token", GrantType::LiveTracing => "live_tracing", GrantType::LiveMetrics => "live_metrics", - GrantType::Troubleshoot => "troubleshoot", + GrantType::Diagnose => "diagnose", GrantType::Rsvp => "rsvp", } } @@ -46,7 +46,7 @@ impl GrantType { GrantType::RefreshToken => 1, GrantType::LiveTracing => 2, GrantType::LiveMetrics => 3, - GrantType::Troubleshoot => 4, + GrantType::Diagnose => 4, GrantType::Rsvp => 5, } } @@ -57,7 +57,7 @@ impl GrantType { 1 => Some(GrantType::RefreshToken), 2 => Some(GrantType::LiveTracing), 3 => Some(GrantType::LiveMetrics), - 4 => Some(GrantType::Troubleshoot), + 4 => Some(GrantType::Diagnose), 5 => Some(GrantType::Rsvp), _ => None, } diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs index 07c4b88a..810ce8d1 100644 --- a/crates/common/src/lib.rs +++ b/crates/common/src/lib.rs @@ -259,6 +259,7 @@ pub struct MailboxCache { pub struct HttpAuthCache { pub account_id: u32, pub revision: u64, + pub credential_id: Option, pub expires: Instant, } diff --git a/crates/http/Cargo.toml b/crates/http/Cargo.toml index 215073f0..c59337af 100644 --- a/crates/http/Cargo.toml +++ b/crates/http/Cargo.toml @@ -45,6 +45,7 @@ rkyv = { version = "0.8.10", features = ["little_endian"] } form-data = { version = "0.6.0", features = ["sync"], default-features = false } mime = "0.3.17" compact_str = "0.9.0" +hashify = { version = "0.2" } [dev-dependencies] diff --git a/crates/http/src/auth/authenticate.rs b/crates/http/src/auth/authenticate.rs index 56de5882..5970ac0a 100644 --- a/crates/http/src/auth/authenticate.rs +++ b/crates/http/src/auth/authenticate.rs @@ -6,10 +6,10 @@ use common::auth::AccessToken; use common::{HttpAuthCache, Server, auth::AuthRequest, network::limiter::InFlight}; +use directory::Credentials; use http_proto::{HttpRequest, HttpSessionData}; use hyper::header; use mail_parser::decoders::base64::base64_decode; -use mail_send::Credentials; use std::future::Future; use std::time::{Duration, Instant}; @@ -18,7 +18,6 @@ pub trait Authenticator: Sync + Send { &self, req: &HttpRequest, session: &HttpSessionData, - allow_api_access: bool, ) -> impl Future, AccessToken)>> + Send; } @@ -27,15 +26,18 @@ impl Authenticator for Server { &self, req: &HttpRequest, session: &HttpSessionData, - allow_api_access: bool, ) -> trc::Result<(Option, AccessToken)> { if let Some((mechanism, token)) = req.authorization() { // Check if the credentials are cached if let Some(http_cache) = self.inner.cache.http_auth.get(token) { // Make sure the revision is still valid if http_cache.expires <= Instant::now() { - let access_token = self.get_access_token(http_cache.account_id).await?; - if access_token.revision == http_cache.revision { + let access_token = AccessToken::renew( + self.access_token(http_cache.account_id).await?, + http_cache.credential_id, + )?; + + if access_token.revision() == http_cache.revision { // Enforce authenticated rate limit return self .is_http_authenticated_request_allowed(&access_token) @@ -62,13 +64,10 @@ impl Authenticator for Server { self.is_http_anonymous_request_allowed(&session.remote_ip) .await?; - decode_bearer_token(token, allow_api_access).ok_or_else(|| { - trc::AuthEvent::Error - .into_err() - .details("Failed to decode Bearer token.") - .id(token.to_string()) - .caused_by(trc::location!()) - })? + Credentials::Bearer { + username: None, + token: token.to_string(), + } } else { // Enforce anonymous rate limit self.is_http_anonymous_request_allowed(&session.remote_ip) @@ -83,22 +82,20 @@ impl Authenticator for Server { // Authenticate let access_token = self - .authenticate( - &AuthRequest::from_credentials( - credentials, - session.session_id, - session.remote_ip, - ) - .with_api_access(allow_api_access), - ) + .authenticate(&AuthRequest::from_credentials( + credentials, + session.session_id, + session.remote_ip, + )) .await?; // Cache credentials self.inner.cache.http_auth.insert( - token.to_string(), + token.into(), HttpAuthCache { account_id: access_token.account_id(), - revision: access_token.revision, + revision: access_token.revision(), + credential_id: access_token.credential_id(), expires: Instant::now() + Duration::from_secs(self.core.oauth.oauth_expiry_token), }, @@ -145,26 +142,15 @@ impl HttpHeaders for HttpRequest { } } -fn decode_plain_auth(token: &str) -> Option> { +fn decode_plain_auth(token: &str) -> Option { base64_decode(token.as_bytes()) .and_then(|token| String::from_utf8(token).ok()) .and_then(|token| { token .split_once(':') - .map(|(login, secret)| Credentials::Plain { + .map(|(login, secret)| Credentials::Basic { username: login.trim().to_lowercase(), secret: secret.to_string(), }) }) } - -fn decode_bearer_token(token: &str, allow_api_access: bool) -> Option> { - if allow_api_access && let Some(token) = token.strip_prefix("api_").and_then(decode_plain_auth) - { - return Some(token); - } - - Some(Credentials::OAuthBearer { - token: token.to_string(), - }) -} diff --git a/crates/http/src/auth/oauth/auth.rs b/crates/http/src/auth/oauth/auth.rs index 9c9502b4..23074a6d 100644 --- a/crates/http/src/auth/oauth/auth.rs +++ b/crates/http/src/auth/oauth/auth.rs @@ -4,6 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use super::{DeviceAuthResponse, FormData, MAX_POST_LEN, OAuthCode, OAuthCodeRequest}; use crate::auth::oauth::OAuthStatus; use common::{ KV_OAUTH, Server, @@ -16,7 +17,6 @@ use http_proto::*; use serde::Deserialize; use serde_json::json; use std::future::Future; -use std::sync::Arc; use store::{ Serialize, dispatch::lookup::KeyValue, @@ -32,8 +32,6 @@ use store::{ }; use trc::AddContext; -use super::{DeviceAuthResponse, FormData, MAX_POST_LEN, OAuthCode, OAuthCodeRequest}; - #[derive(Debug, serde::Serialize, Deserialize)] pub struct OAuthMetadata { pub issuer: String, @@ -50,7 +48,7 @@ pub struct OAuthMetadata { pub trait OAuthApiHandler: Sync + Send { fn handle_oauth_api_request( &self, - access_token: Arc, + access_token: &AccessToken, body: Option>, ) -> impl Future> + Send; @@ -70,7 +68,7 @@ pub trait OAuthApiHandler: Sync + Send { impl OAuthApiHandler for Server { async fn handle_oauth_api_request( &self, - access_token: Arc, + access_token: &AccessToken, body: Option>, ) -> trc::Result { let request = @@ -119,9 +117,7 @@ impl OAuthApiHandler for Server { .caused_by(trc::location!())?; // Insert client code - self.core - .storage - .lookup + self.in_memory_store() .key_set( KeyValue::with_prefix(KV_OAUTH, client_code.as_bytes(), value) .expires(self.core.oauth.oauth_expiry_auth_code), @@ -152,9 +148,7 @@ impl OAuthApiHandler for Server { // Obtain code if let Some(auth_code_) = self - .core - .storage - .lookup + .in_memory_store() .key_get::>(KeyValue::<()>::build_key( KV_OAUTH, code.as_bytes(), @@ -175,16 +169,12 @@ impl OAuthApiHandler for Server { success = true; // Delete issued user code - self.core - .storage - .lookup + self.in_memory_store() .key_delete(KeyValue::<()>::build_key(KV_OAUTH, code.as_bytes())) .await?; // Update device code status - self.core - .storage - .lookup + self.in_memory_store() .key_set( KeyValue::with_prefix( KV_OAUTH, @@ -260,9 +250,7 @@ impl OAuthApiHandler for Server { .caused_by(trc::location!())?; // Insert device code - self.core - .storage - .lookup + self.in_memory_store() .key_set( KeyValue::with_prefix(KV_OAUTH, device_code.as_bytes(), oauth_code.clone()) .expires(self.core.oauth.oauth_expiry_user_code), @@ -270,9 +258,7 @@ impl OAuthApiHandler for Server { .await?; // Insert user code - self.core - .storage - .lookup + self.in_memory_store() .key_set( KeyValue::with_prefix(KV_OAUTH, user_code.as_bytes(), oauth_code) .expires(self.core.oauth.oauth_expiry_user_code), diff --git a/crates/http/src/auth/oauth/openid.rs b/crates/http/src/auth/oauth/openid.rs index 271f4169..3ce822e8 100644 --- a/crates/http/src/auth/oauth/openid.rs +++ b/crates/http/src/auth/oauth/openid.rs @@ -4,15 +4,10 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use std::future::Future; - -use common::{ - Server, - auth::{AccessToken, oauth::oidc::Userinfo}, -}; -use serde::{Deserialize, Serialize}; - +use common::{Server, auth::oauth::oidc::Userinfo}; use http_proto::*; +use serde::{Deserialize, Serialize}; +use std::future::Future; #[derive(Debug, Serialize, Deserialize)] pub struct OpenIdMetadata { @@ -34,7 +29,7 @@ pub struct OpenIdMetadata { pub trait OpenIdHandler: Sync + Send { fn handle_userinfo_request( &self, - access_token: &AccessToken, + account_id: u32, ) -> impl Future> + Send; fn handle_oidc_metadata( @@ -45,16 +40,15 @@ pub trait OpenIdHandler: Sync + Send { } impl OpenIdHandler for Server { - async fn handle_userinfo_request( - &self, - access_token: &AccessToken, - ) -> trc::Result { + async fn handle_userinfo_request(&self, account_id: u32) -> trc::Result { + let account = self.account(account_id).await?; + Ok(JsonResponse::new(Userinfo { - sub: Some(access_token.account_id.to_string()), - name: access_token.description.clone(), - preferred_username: Some(access_token.name.clone()), - email: access_token.emails.first().cloned(), - email_verified: !access_token.emails.is_empty(), + sub: Some(account_id.to_string()), + name: account.description().map(|d| d.to_string()), + preferred_username: Some(account.name().to_string()), + email: account.name().to_string().into(), + email_verified: true, ..Default::default() }) .no_cache() diff --git a/crates/http/src/auth/oauth/registration.rs b/crates/http/src/auth/oauth/registration.rs index 7b7605e7..250f73f9 100644 --- a/crates/http/src/auth/oauth/registration.rs +++ b/crates/http/src/auth/oauth/registration.rs @@ -6,24 +6,29 @@ use std::future::Future; +use super::ErrorType; +use crate::auth::authenticate::Authenticator; use common::{ Server, - auth::oauth::registration::{ClientRegistrationRequest, ClientRegistrationResponse}, -}; - -use directory::{ - Permission, QueryParams, Type, - backend::internal::{ - PrincipalField, PrincipalSet, lookup::DirectoryStore, manage::ManageDirectory, + auth::{ + BuildAccessToken, + oauth::registration::{ClientRegistrationRequest, ClientRegistrationResponse}, }, }; -use store::rand::{Rng, distr::Alphanumeric, rng}; -use trc::{AddContext, AuthEvent}; - -use crate::auth::authenticate::Authenticator; use http_proto::{request::fetch_body, *}; - -use super::ErrorType; +use registry::{ + schema::{ + enums::Permission, + prelude::{Object, Property}, + structs::OAuthClient, + }, + types::datetime::UTCDateTime, +}; +use store::{ + rand::{Rng, distr::Alphanumeric, rng}, + registry::RegistryQuery, +}; +use trc::{AddContext, AuthEvent}; pub trait ClientRegistrationHandler: Sync + Send { fn handle_oauth_registration_request( @@ -45,16 +50,18 @@ impl ClientRegistrationHandler for Server { req: &mut HttpRequest, session: HttpSessionData, ) -> trc::Result { - if !self.core.oauth.allow_anonymous_client_registration { + let tenant_id = if !self.core.oauth.allow_anonymous_client_registration { // Authenticate request - let (_, access_token) = self.authenticate_headers(req, &session, true).await?; + let (_, access_token) = self.authenticate_headers(req, &session).await?; // Validate permissions - access_token.assert_has_permission(Permission::OauthClientRegistration)?; + access_token.enforce_permission(Permission::OauthClientRegistration)?; + access_token.tenant_id() } else { self.is_http_anonymous_request_allowed(&session.remote_ip) .await?; - } + None + }; // Parse request let body = fetch_body(req, 20 * 1024, session.session_id).await; @@ -71,17 +78,18 @@ impl ClientRegistrationHandler for Server { .take(20) .map(|ch| char::from(ch.to_ascii_lowercase())) .collect::(); - self.store() - .create_principal( - PrincipalSet::new(u32::MAX, Type::OauthClient) - .with_field(PrincipalField::Name, client_id.clone()) - .with_field(PrincipalField::Urls, request.redirect_uris.clone()) - .with_opt_field(PrincipalField::Description, request.client_name.clone()) - .with_field(PrincipalField::Emails, request.contacts.clone()) - .with_opt_field(PrincipalField::Picture, request.logo_uri.clone()), - None, - None, - ) + + self.registry() + .insert(&OAuthClient { + client_id: client_id.clone(), + created_at: UTCDateTime::now(), + description: request.client_name.clone(), + contacts: request.contacts.clone(), + member_tenant_id: tenant_id.map(|id| Object::Tenant.id(id as u64)), + redirect_uris: request.redirect_uris.clone(), + logo: request.logo_uri.clone(), + ..Default::default() + }) .await .caused_by(trc::location!())?; @@ -111,15 +119,27 @@ impl ClientRegistrationHandler for Server { } // Fetch client registration - let found_registration = if let Some(client) = self - .store() - .query(QueryParams::name(client_id).with_return_member_of(false)) - .await - .caused_by(trc::location!())? - .filter(|p| p.typ() == Type::OauthClient) + let found_registration = if let Some(client_id) = self + .registry() + .query::>( + RegistryQuery::new(Object::OAuthClient).equal(Property::ClientId, client_id), + ) + .await? + .first() { if let Some(redirect_uri) = redirect_uri { - if client.urls().any(|uri| uri == redirect_uri) { + let client = self + .registry() + .object::(*client_id) + .await? + .ok_or_else(|| { + trc::StoreEvent::UnexpectedError + .into_err() + .details("OAuth client not found.") + .caused_by(trc::location!()) + .ctx(trc::Key::Id, *client_id) + })?; + if client.redirect_uris.iter().any(|uri| uri == redirect_uri) { return Ok(None); } } else { @@ -135,9 +155,10 @@ impl ClientRegistrationHandler for Server { // Check if the account is allowed to override client registration if self - .get_access_token(account_id) + .access_token(account_id) .await .caused_by(trc::location!())? + .build() .has_permission(Permission::OauthClientOverride) { return Ok(None); diff --git a/crates/http/src/auth/oauth/token.rs b/crates/http/src/auth/oauth/token.rs index 1c7db5a9..4270de57 100644 --- a/crates/http/src/auth/oauth/token.rs +++ b/crates/http/src/auth/oauth/token.rs @@ -74,9 +74,7 @@ impl TokenHandler for Server { ) { // Obtain code match self - .core - .storage - .lookup + .in_memory_store() .key_get::>(KeyValue::<()>::build_key( KV_OAUTH, code.as_bytes(), @@ -102,9 +100,7 @@ impl TokenHandler for Server { TokenResponse::error(error) } else { // Mark this token as issued - self.core - .storage - .lookup + self.in_memory_store() .key_delete(KeyValue::<()>::build_key( KV_OAUTH, code.as_bytes(), @@ -146,9 +142,7 @@ impl TokenHandler for Server { { // Obtain code if let Some(auth_code_) = self - .core - .storage - .lookup + .in_memory_store() .key_get::>(KeyValue::<()>::build_key( KV_OAUTH, device_code.as_bytes(), @@ -174,9 +168,7 @@ impl TokenHandler for Server { TokenResponse::error(error) } else { // Mark this token as issued - self.core - .storage - .lookup + self.in_memory_store() .key_delete(KeyValue::<()>::build_key( KV_OAUTH, device_code.as_bytes(), @@ -315,11 +307,8 @@ impl TokenHandler for Server { None }, id_token: if with_id_token { - // Obtain access token - let access_token = self - .get_access_token(account_id) - .await - .caused_by(trc::location!())?; + // Obtain account + let account = self.account(account_id).await.caused_by(trc::location!())?; match self.issue_id_token( account_id.to_string(), @@ -327,9 +316,9 @@ impl TokenHandler for Server { client_id, StandardClaims { nonce, - preferred_username: access_token.name.clone().into(), - email: access_token.emails.first().cloned(), - description: access_token.description.clone(), + preferred_username: account.name().to_string().into(), + email: account.name().to_string().into(), + description: account.description().map(|d| d.to_string()), }, ) { Ok(id_token) => Some(id_token), diff --git a/crates/http/src/autoconfig/mod.rs b/crates/http/src/autoconfig/mod.rs index 4932c188..6a9793f2 100644 --- a/crates/http/src/autoconfig/mod.rs +++ b/crates/http/src/autoconfig/mod.rs @@ -5,13 +5,13 @@ */ use common::{Server, manager::webadmin::Resource}; -use directory::QueryParams; use http_proto::*; use quick_xml::Reader; use quick_xml::events::Event; +use registry::schema::enums::NetworkListenerProtocol; +use registry::schema::structs::NetworkListener; use std::fmt::Write; use std::future::Future; -use trc::AddContext; use utils::url_params::UrlParams; pub trait Autoconfig: Sync + Send { @@ -23,11 +23,6 @@ pub trait Autoconfig: Sync + Send { &self, body: Option>, ) -> impl Future> + Send; - fn autoconfig_parameters<'x>( - &'x self, - emailaddress: &'x str, - fail_if_invalid: bool, - ) -> impl Future> + Send; } impl Autoconfig for Server { @@ -38,9 +33,13 @@ impl Autoconfig for Server { .get("emailaddress") .unwrap_or_default() .to_lowercase(); - let (account_name, server_name, domain) = - self.autoconfig_parameters(&emailaddress, false).await?; - let services = self.core.storage.config.get_services().await?; + let Some((_, domain)) = emailaddress.rsplit_once('@') else { + return Err(trc::ResourceEvent::BadParameters + .into_err() + .details("Missing domain in email address")); + }; + let listeners = self.registry().list::().await?; + let server_name = &self.core.network.server_name; // Build XML response let mut config = String::with_capacity(1024); @@ -53,10 +52,15 @@ impl Autoconfig for Server { &mut config, "\t\t{domain}" ); - for (protocol, port, is_tls) in services { - let tag = match protocol.as_str() { - "imap" | "pop3" => "incomingServer", - "smtp" if port != 25 => "outgoingServer", + for listener in listeners { + let listener = listener.object; + let Some(port) = listener.bind.first().map(|l| l.0.port()) else { + continue; + }; + let (protocol, tag) = match listener.protocol { + NetworkListenerProtocol::Smtp if port != 25 => ("smtp", "outgoingServer"), + NetworkListenerProtocol::Imap => ("imap", "incomingServer"), + NetworkListenerProtocol::Pop3 => ("pop3", "incomingServer"), _ => continue, }; let _ = writeln!(&mut config, "\t\t<{tag} type=\"{protocol}\">"); @@ -65,9 +69,13 @@ impl Autoconfig for Server { let _ = writeln!( &mut config, "\t\t\t{}", - if is_tls { "SSL" } else { "STARTTLS" } + if listener.tls_implicit { + "SSL" + } else { + "STARTTLS" + } ); - let _ = writeln!(&mut config, "\t\t\t{account_name}"); + let _ = writeln!(&mut config, "\t\t\t{emailaddress}"); let _ = writeln!( &mut config, "\t\t\tpassword-cleartext" @@ -83,7 +91,7 @@ impl Autoconfig for Server { ("fileShare", "webdav", "file"), ] { let _ = writeln!(&mut config, "\t<{tag} type=\"{protocol}\">"); - let _ = writeln!(&mut config, "\t\t{account_name}"); + let _ = writeln!(&mut config, "\t\t{emailaddress}"); let _ = writeln!( &mut config, "\t\thttp-basic" @@ -119,9 +127,8 @@ impl Autoconfig for Server { .details("Failed to parse autodiscover request") .ctx(trc::Key::Reason, err) })?; - let (account_name, server_name, _) = - self.autoconfig_parameters(&emailaddress, true).await?; - let services = self.core.storage.config.get_services().await?; + let listeners = self.registry().list::().await?; + let server_name = &self.core.network.server_name; // Build XML response let mut config = String::with_capacity(1024); @@ -152,31 +159,33 @@ impl Autoconfig for Server { let _ = writeln!(&mut config, "\t\t"); let _ = writeln!(&mut config, "\t\t\temail"); let _ = writeln!(&mut config, "\t\t\tsettings"); - for (protocol, port, is_tls) in services { - match protocol.as_str() { - "imap" | "pop3" => (), - "smtp" if port != 25 => (), + for listener in listeners { + let listener = listener.object; + let Some(port) = listener.bind.first().map(|l| l.0.port()) else { + continue; + }; + + let protocol = match listener.protocol { + NetworkListenerProtocol::Imap => "IMAP", + NetworkListenerProtocol::Pop3 => "POP3", + NetworkListenerProtocol::Smtp if port != 25 => "SMTP", _ => continue, - } + }; let _ = writeln!(&mut config, "\t\t\t"); - let _ = writeln!( - &mut config, - "\t\t\t\t{}", - protocol.to_uppercase() - ); + let _ = writeln!(&mut config, "\t\t\t\t{protocol}",); let _ = writeln!(&mut config, "\t\t\t\t{server_name}"); let _ = writeln!(&mut config, "\t\t\t\t{port}"); - let _ = writeln!(&mut config, "\t\t\t\t{account_name}"); + let _ = writeln!(&mut config, "\t\t\t\t{emailaddress}"); let _ = writeln!(&mut config, "\t\t\t\ton"); let _ = writeln!(&mut config, "\t\t\t\t0"); let _ = writeln!(&mut config, "\t\t\t\t0"); let _ = writeln!( &mut config, "\t\t\t\t{}", - if is_tls { "on" } else { "off" } + if listener.tls_implicit { "on" } else { "off" } ); - if is_tls { + if listener.tls_implicit { let _ = writeln!(&mut config, "\t\t\t\tTLS"); } let _ = writeln!(&mut config, "\t\t\t\toff"); @@ -192,51 +201,6 @@ impl Autoconfig for Server { .into_http_response(), ) } - - async fn autoconfig_parameters<'x>( - &'x self, - emailaddress: &'x str, - fail_if_invalid: bool, - ) -> trc::Result<(String, String, &'x str)> { - // Return EMAILADDRESS - let Some((_, domain)) = emailaddress.rsplit_once('@') else { - return if !fail_if_invalid { - Ok(( - "%EMAILADDRESS%".to_string(), - self.core.network.server_name.clone(), - &self.core.network.report_domain, - )) - } else { - Err(trc::ResourceEvent::BadParameters - .into_err() - .details("Missing domain in email address")) - }; - }; - - // Find the account name by e-mail address - let mut account_name = emailaddress.into(); - if let Some(id) = self - .core - .storage - .directory - .email_to_id(emailaddress) - .await - .caused_by(trc::location!())? - && let Ok(Some(principal)) = self - .core - .storage - .directory - .query(QueryParams::id(id).with_return_member_of(false)) - .await - && principal - .primary_email() - .is_some_and(|email| email.eq_ignore_ascii_case(emailaddress)) - { - account_name = principal.name; - } - - Ok((account_name, self.core.network.server_name.clone(), domain)) - } } fn parse_autodiscover_request(bytes: &[u8]) -> Result { diff --git a/crates/http/src/form/mod.rs b/crates/http/src/form/mod.rs index 80556099..5985a283 100644 --- a/crates/http/src/form/mod.rs +++ b/crates/http/src/form/mod.rs @@ -9,7 +9,8 @@ use chrono::Utc; use common::{ KV_RATE_LIMIT_CONTACT, Server, config::network::{ContactForm, FieldOrDefault}, - ip_to_bytes, psl, + network::ip_to_bytes, + psl, }; use email::message::delivery::{IngestMessage, IngestRecipient, LocalDeliveryStatus, MailDelivery}; use http_proto::*; @@ -48,9 +49,7 @@ impl FormHandler for Server { if let Some(rate) = &form.rate && !session.remote_ip.is_loopback() && self - .core - .storage - .lookup + .in_memory_store() .is_rate_allowed( KV_RATE_LIMIT_CONTACT, &ip_to_bytes(&session.remote_ip), diff --git a/crates/http/src/management/crypto.rs b/crates/http/src/management/crypto.rs deleted file mode 100644 index 3b4ee22d..00000000 --- a/crates/http/src/management/crypto.rs +++ /dev/null @@ -1,173 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL - */ - -use common::{Server, auth::AccessToken}; -use directory::backend::internal::manage; -use email::message::crypto::{ - ENCRYPT_TRAIN_SPAM_FILTER, EncryptMessage, EncryptMessageError, EncryptionMethod, - EncryptionParams, EncryptionType, try_parse_certs, -}; -use http_proto::*; -use mail_builder::encoders::base64::base64_encode_mime; -use mail_parser::MessageParser; -use serde_json::json; -use std::{future::Future, sync::Arc}; -use store::{ - Deserialize, Serialize, ValueKey, - write::{AlignedBytes, Archive, Archiver, BatchBuilder}, -}; -use trc::AddContext; -use types::{collection::Collection, field::PrincipalField}; - -pub trait CryptoHandler: Sync + Send { - fn handle_crypto_get( - &self, - access_token: Arc, - ) -> impl Future> + Send; - - fn handle_crypto_post( - &self, - access_token: Arc, - body: Option>, - ) -> impl Future> + Send; -} - -impl CryptoHandler for Server { - async fn handle_crypto_get(&self, access_token: Arc) -> trc::Result { - let ec = if let Some(params_) = self - .store() - .get_value::>(ValueKey::property( - access_token.account_id(), - Collection::Principal, - 0, - PrincipalField::EncryptionKeys, - )) - .await? - { - let params = params_ - .unarchive::() - .caused_by(trc::location!())?; - let algo = params.algo(); - let method = params.method(); - let allow_spam_training = params.can_train_spam_filter(); - let mut certs = Vec::new(); - certs.extend_from_slice(b"-----STALWART CERTIFICATE-----\r\n"); - let _ = base64_encode_mime(¶ms_.into_inner(), &mut certs, false); - certs.extend_from_slice(b"\r\n"); - let certs = String::from_utf8(certs).unwrap_or_default(); - - match method { - EncryptionMethod::PGP => EncryptionType::PGP { - algo, - certs, - allow_spam_training, - }, - EncryptionMethod::SMIME => EncryptionType::SMIME { - algo, - certs, - allow_spam_training, - }, - } - } else { - EncryptionType::Disabled - }; - - Ok(JsonResponse::new(json!({ - "data": ec, - })) - .into_http_response()) - } - - async fn handle_crypto_post( - &self, - access_token: Arc, - body: Option>, - ) -> trc::Result { - let request = serde_json::from_slice::(body.as_deref().unwrap_or_default()) - .map_err(|err| trc::ResourceEvent::BadParameters.into_err().reason(err))?; - - let (method, algo, mut certs, allow_spam_training) = match request { - EncryptionType::PGP { - algo, - certs, - allow_spam_training, - } => (EncryptionMethod::PGP, algo, certs, allow_spam_training), - EncryptionType::SMIME { - algo, - certs, - allow_spam_training, - } => (EncryptionMethod::SMIME, algo, certs, allow_spam_training), - EncryptionType::Disabled => { - // Disable encryption at rest - let mut batch = BatchBuilder::new(); - batch - .with_account_id(access_token.account_id()) - .with_collection(Collection::Principal) - .with_document(0) - .clear(PrincipalField::EncryptionKeys); - self.core.storage.data.write(batch.build_all()).await?; - return Ok(JsonResponse::new(json!({ - "data": (), - })) - .into_http_response()); - } - }; - if !certs.ends_with("\n") { - certs.push('\n'); - } - - // Make sure Encryption is enabled - if !self.core.jmap.encrypt { - return Err(manage::unsupported( - "Encryption-at-rest has been disabled by the system administrator", - )); - } - - // Parse certificates - let certs = try_parse_certs(method, certs.into_bytes()) - .map_err(|err| manage::error(err, None::))?; - let num_certs = certs.len(); - let params = Archiver::new(EncryptionParams { - flags: method.flags() - | algo.flags() - | if allow_spam_training { - ENCRYPT_TRAIN_SPAM_FILTER - } else { - 0 - }, - certs, - }) - .serialize() - .caused_by(trc::location!())?; - - // Try a test encryption - if let Err(EncryptMessageError::Error(message)) = MessageParser::new() - .parse("Subject: test\r\ntest\r\n".as_bytes()) - .unwrap() - .encrypt( - as Deserialize>::deserialize(params.as_slice())? - .unarchive::()?, - ) - .await - { - return Err(manage::error(message, None::)); - } - - // Save encryption params - let mut batch = BatchBuilder::new(); - batch - .with_account_id(access_token.account_id()) - .with_collection(Collection::Principal) - .with_document(0) - .set(PrincipalField::EncryptionKeys, params); - self.core.storage.data.write(batch.build_all()).await?; - - Ok(JsonResponse::new(json!({ - "data": num_certs, - })) - .into_http_response()) - } -} diff --git a/crates/http/src/management/troubleshoot.rs b/crates/http/src/management/diagnose.rs similarity index 98% rename from crates/http/src/management/troubleshoot.rs rename to crates/http/src/management/diagnose.rs index 244dda61..76550483 100644 --- a/crates/http/src/management/troubleshoot.rs +++ b/crates/http/src/management/diagnose.rs @@ -19,7 +19,6 @@ use common::{ }, psl, }; -use directory::backend::internal::manage; use http_body_util::{StreamBody, combinators::BoxBody}; use hyper::{ Method, StatusCode, @@ -46,7 +45,7 @@ use utils::url_params::UrlParams; use http_proto::{request::decode_path_element, *}; pub trait TroubleshootApi: Sync + Send { - fn handle_troubleshoot_api_request( + fn handle_diagnose_api_request( &self, req: &HttpRequest, path: Vec<&str>, @@ -56,7 +55,7 @@ pub trait TroubleshootApi: Sync + Send { } impl TroubleshootApi for Server { - async fn handle_troubleshoot_api_request( + async fn handle_diagnose_api_request( &self, req: &HttpRequest, path: Vec<&str>, @@ -74,7 +73,7 @@ impl TroubleshootApi for Server { ("token", None, &Method::GET) => { // Issue a live telemetry token valid for 60 seconds Ok(JsonResponse::new(json!({ - "data": self.encode_access_token(GrantType::Troubleshoot, account_id, "web", 60).await?, + "data": self.encode_access_token(GrantType::Diagnose, account_id, "web", 60).await?, })) .into_http_response()) } @@ -86,7 +85,7 @@ impl TroubleshootApi for Server { .unwrap_or(30), ); - let mut rx = spawn_delivery_troubleshoot( + let mut rx = spawn_delivery_diagnose( self.clone(), decode_path_element(target).to_lowercase(), timeout, @@ -109,11 +108,9 @@ impl TroubleshootApi for Server { .map_err(|err| { trc::EventType::Resource(trc::ResourceEvent::BadParameters).from_json_error(err) })?; - let response = dmarc_troubleshoot(self, request).await.ok_or_else(|| { - manage::error( - "Invalid message body", - "Failed to parse message body".into(), - ) + let response = dmarc_diagnose(self, request).await.ok_or_else(|| { + trc::EventType::Resource(trc::ResourceEvent::BadParameters) + .reason("Failed to parse message body") })?; Ok(JsonResponse::new(json!({ @@ -288,7 +285,7 @@ impl ElapsedMs for Instant { self.elapsed().as_millis() as u64 } } -fn spawn_delivery_troubleshoot( +fn spawn_delivery_diagnose( server: Server, domain_or_email: String, timeout: Duration, @@ -296,13 +293,13 @@ fn spawn_delivery_troubleshoot( let (tx, rx) = mpsc::channel(10); tokio::spawn(async move { - let _ = delivery_troubleshoot(tx, server, domain_or_email, timeout).await; + let _ = delivery_diagnose(tx, server, domain_or_email, timeout).await; }); rx } -async fn delivery_troubleshoot( +async fn delivery_diagnose( tx: mpsc::Sender, server: Server, domain_or_email: String, @@ -354,7 +351,7 @@ async fn delivery_troubleshoot( mxs: mxs .iter() .map(|mx| MX { - exchanges: mx.exchanges.clone(), + exchanges: mx.exchanges.iter().map(|e| e.to_string()).collect(), preference: mx.preference, }) .collect(), @@ -882,7 +879,7 @@ pub enum DmarcPolicy { Unspecified, } -async fn dmarc_troubleshoot( +async fn dmarc_diagnose( server: &Server, request: DmarcTroubleshootRequest, ) -> Option { @@ -1005,7 +1002,7 @@ async fn dmarc_troubleshoot( ip_rev_ptr: iprev .ptr .as_ref() - .map(|ptr| ptr.as_ref().clone()) + .map(|ptr| ptr.iter().map(|s| s.to_string()).collect()) .unwrap_or_default(), ip_rev_result: (&iprev).into(), dkim_pass, diff --git a/crates/http/src/management/dkim.rs b/crates/http/src/management/dkim.rs deleted file mode 100644 index 05cd4f29..00000000 --- a/crates/http/src/management/dkim.rs +++ /dev/null @@ -1,295 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL - */ - -use std::str::FromStr; - -use common::{Server, auth::AccessToken, config::smtp::auth::simple_pem_parse}; -use directory::{Permission, backend::internal::manage}; -use hyper::Method; -use mail_auth::{ - common::crypto::{Ed25519Key, RsaKey, Sha256}, - dkim::generate::DkimKeyPair, -}; -use mail_builder::encoders::base64::base64_encode; -use mail_parser::DateTime; -use pkcs8::Document; -use rsa::pkcs1::DecodeRsaPublicKey; -use serde::{Deserialize, Serialize}; -use serde_json::json; -use store::write::now; - -use http_proto::{request::decode_path_element, *}; -use std::future::Future; - -#[derive(Debug, Serialize, Deserialize, Copy, Clone, PartialEq, Eq)] -pub enum Algorithm { - Rsa, - Ed25519, -} - -#[derive(Debug, Serialize, Deserialize)] -struct DkimSignature { - id: Option, - algorithm: Algorithm, - domain: String, - selector: Option, -} - -pub trait DkimManagement: Sync + Send { - fn handle_manage_dkim( - &self, - req: &HttpRequest, - path: Vec<&str>, - body: Option>, - access_token: &AccessToken, - ) -> impl Future> + Send; - - fn handle_get_public_key( - &self, - path: Vec<&str>, - ) -> impl Future> + Send; - - fn handle_create_signature( - &self, - body: Option>, - ) -> impl Future> + Send; - - fn create_dkim_key( - &self, - algo: Algorithm, - id: impl AsRef + Send, - domain: impl Into + Send, - selector: impl Into + Send, - ) -> impl Future> + Send; -} - -impl DkimManagement for Server { - async fn handle_manage_dkim( - &self, - req: &HttpRequest, - path: Vec<&str>, - body: Option>, - access_token: &AccessToken, - ) -> trc::Result { - match *req.method() { - Method::GET => { - // Validate the access token - access_token.assert_has_permission(Permission::DkimSignatureGet)?; - - self.handle_get_public_key(path).await - } - Method::POST => { - // Validate the access token - access_token.assert_has_permission(Permission::DkimSignatureCreate)?; - - self.handle_create_signature(body).await - } - _ => Err(trc::ResourceEvent::NotFound.into_err()), - } - } - - async fn handle_get_public_key(&self, path: Vec<&str>) -> trc::Result { - let signature_id = match path.get(1) { - Some(signature_id) => decode_path_element(signature_id), - None => { - return Err(trc::ResourceEvent::NotFound.into_err()); - } - }; - - let (pk, algo) = match ( - self.core - .storage - .config - .get(&format!("signature.{signature_id}.private-key")) - .await, - self.core - .storage - .config - .get(&format!("signature.{signature_id}.algorithm")) - .await - .map(|algo| algo.and_then(|algo| algo.parse::().ok())), - ) { - (Ok(Some(pk)), Ok(Some(algorithm))) => (pk, algorithm), - (Err(err), _) | (_, Err(err)) => return Err(err.caused_by(trc::location!())), - _ => return Err(trc::ResourceEvent::NotFound.into_err()), - }; - - Ok(JsonResponse::new(json!({ - "data": obtain_dkim_public_key(algo, &pk)?, - })) - .into_http_response()) - } - - async fn handle_create_signature(&self, body: Option>) -> trc::Result { - let request = - match serde_json::from_slice::(body.as_deref().unwrap_or_default()) { - Ok(request) => request, - Err(err) => { - return Err( - trc::EventType::Resource(trc::ResourceEvent::BadParameters).reason(err) - ); - } - }; - - let algo_str = match request.algorithm { - Algorithm::Rsa => "rsa", - Algorithm::Ed25519 => "ed25519", - }; - let id = request - .id - .unwrap_or_else(|| format!("{algo_str}-{}", request.domain)); - let selector = request.selector.unwrap_or_else(|| { - let dt = DateTime::from_timestamp(now() as i64); - format!( - "{:04}{:02}{}", - dt.year, - dt.month, - if Algorithm::Rsa == request.algorithm { - "r" - } else { - "e" - } - ) - }); - - // Make sure the signature does not exist already - if let Some(value) = self - .core - .storage - .config - .get(&format!("signature.{id}.private-key")) - .await? - { - return Err(manage::err_exists( - format!("signature.{id}.private-key"), - value, - )); - } - - // Create signature - self.create_dkim_key(request.algorithm, id, request.domain, selector) - .await?; - - Ok(JsonResponse::new(json!({ - "data": (), - })) - .into_http_response()) - } - - async fn create_dkim_key( - &self, - algo: Algorithm, - id: impl AsRef, - domain: impl Into, - selector: impl Into, - ) -> trc::Result<()> { - let id = id.as_ref(); - let (algorithm, pk_type) = match algo { - Algorithm::Rsa => ("rsa-sha256", "RSA PRIVATE KEY"), - Algorithm::Ed25519 => ("ed25519-sha256", "PRIVATE KEY"), - }; - let mut pk = format!("-----BEGIN {pk_type}-----\n").into_bytes(); - let mut lf_count = 65; - for ch in base64_encode( - match algo { - Algorithm::Rsa => DkimKeyPair::generate_rsa(2048), - Algorithm::Ed25519 => DkimKeyPair::generate_ed25519(), - } - .map_err(|err| { - manage::error("Failed to generate key", err.to_string().into()) - .caused_by(trc::location!()) - })? - .private_key(), - ) - .unwrap_or_default() - { - pk.push(ch); - lf_count -= 1; - if lf_count == 0 { - pk.push(b'\n'); - lf_count = 65; - } - } - if lf_count != 65 { - pk.push(b'\n'); - } - pk.extend_from_slice(format!("-----END {pk_type}-----\n").as_bytes()); - - self.core - .storage - .config - .set( - [ - ( - format!("signature.{id}.private-key"), - String::from_utf8(pk).unwrap(), - ), - (format!("signature.{id}.domain"), domain.into()), - (format!("signature.{id}.selector"), selector.into()), - (format!("signature.{id}.algorithm"), algorithm.to_string()), - ( - format!("signature.{id}.canonicalization"), - "relaxed/relaxed".to_string(), - ), - (format!("signature.{id}.headers.0"), "From".to_string()), - (format!("signature.{id}.headers.1"), "To".to_string()), - (format!("signature.{id}.headers.2"), "Date".to_string()), - (format!("signature.{id}.headers.3"), "Subject".to_string()), - ( - format!("signature.{id}.headers.4"), - "Message-ID".to_string(), - ), - (format!("signature.{id}.report"), "false".to_string()), - ], - true, - ) - .await - } -} - -pub fn obtain_dkim_public_key(algo: Algorithm, pk: &str) -> trc::Result { - match simple_pem_parse(pk) { - Some(der) => match algo { - Algorithm::Rsa => match RsaKey::::from_der(&der).and_then(|key| { - Document::from_pkcs1_der(&key.public_key()) - .map_err(|err| mail_auth::Error::CryptoError(err.to_string())) - }) { - Ok(pk) => Ok( - String::from_utf8(base64_encode(pk.as_bytes()).unwrap_or_default()) - .unwrap_or_default(), - ), - Err(err) => Err(manage::error( - "Failed to read RSA DER", - err.to_string().into(), - )), - }, - Algorithm::Ed25519 => { - match Ed25519Key::from_pkcs8_maybe_unchecked_der(&der) - .map_err(|err| mail_auth::Error::CryptoError(err.to_string())) - { - Ok(pk) => Ok(String::from_utf8( - base64_encode(&pk.public_key()).unwrap_or_default(), - ) - .unwrap_or_default()), - Err(err) => Err(manage::error("Crypto error", err.to_string().into())), - } - } - }, - None => Err(manage::error("Failed to decode private key", None::)), - } -} - -impl FromStr for Algorithm { - type Err = (); - - fn from_str(s: &str) -> Result { - match s.split_once('-').map(|(algo, _)| algo) { - Some("rsa") => Ok(Algorithm::Rsa), - Some("ed25519") => Ok(Algorithm::Ed25519), - _ => Err(()), - } - } -} diff --git a/crates/http/src/management/dns.rs b/crates/http/src/management/dns.rs deleted file mode 100644 index 6a4c8d8c..00000000 --- a/crates/http/src/management/dns.rs +++ /dev/null @@ -1,290 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL - */ - -use common::{Server, auth::AccessToken}; -use directory::{ - Permission, - backend::internal::manage::{self}, -}; - -use hyper::Method; -use serde::{Deserialize, Serialize}; -use serde_json::json; -use sha1::Digest; -use utils::config::Config; -use x509_parser::parse_x509_certificate; - -use crate::management::dkim::{Algorithm, obtain_dkim_public_key}; -use http_proto::{request::decode_path_element, *}; -use std::future::Future; - -#[derive(Debug, Serialize, Deserialize)] -pub struct DnsRecord { - #[serde(rename = "type")] - typ: String, - name: String, - content: String, -} - -pub trait DnsManagement: Sync + Send { - fn handle_manage_dns( - &self, - req: &HttpRequest, - path: Vec<&str>, - access_token: &AccessToken, - ) -> impl Future> + Send; - - fn build_dns_records( - &self, - domain_name: &str, - ) -> impl Future>> + Send; -} - -impl DnsManagement for Server { - async fn handle_manage_dns( - &self, - req: &HttpRequest, - path: Vec<&str>, - access_token: &AccessToken, - ) -> trc::Result { - match ( - path.get(1).copied().unwrap_or_default(), - path.get(2), - req.method(), - ) { - ("records", Some(domain), &Method::GET) => { - // Validate the access token - access_token.assert_has_permission(Permission::DomainGet)?; - - // Obtain DNS records - let domain = decode_path_element(domain); - Ok(JsonResponse::new(json!({ - "data": self.build_dns_records(domain.as_ref()).await?, - })) - .into_http_response()) - } - _ => Err(trc::ResourceEvent::NotFound.into_err()), - } - } - - async fn build_dns_records(&self, domain_name: &str) -> trc::Result> { - // Obtain server name - let server_name = &self.core.network.server_name; - let mut records = Vec::new(); - - // Obtain DKIM keys - let mut keys = Config::default(); - let mut signature_ids = Vec::new(); - let mut has_macros = false; - for (key, value) in self.core.storage.config.list("signature.", true).await? { - match key.strip_suffix(".domain") { - Some(key_id) if value == domain_name => { - signature_ids.push(key_id.to_string()); - } - _ => (), - } - if !has_macros && value.contains("%{") { - has_macros = true; - } - keys.keys.insert(key, value); - } - - // Add MX and CNAME records - records.push(DnsRecord { - typ: "MX".to_string(), - name: format!("{domain_name}."), - content: format!("10 {server_name}."), - }); - if server_name.strip_prefix("mail.") != Some(domain_name) { - records.push(DnsRecord { - typ: "CNAME".to_string(), - name: format!("mail.{domain_name}."), - content: format!("{server_name}."), - }); - } - - // Process DKIM keys - if has_macros { - keys.resolve_macros(&["env", "file", "cfg"]).await; - keys.log_errors(); - } - for signature_id in signature_ids { - if let (Some(algo), Some(pk), Some(selector)) = ( - keys.value(format!("{signature_id}.algorithm")) - .and_then(|algo| algo.parse::().ok()), - keys.value(format!("{signature_id}.private-key")), - keys.value(format!("{signature_id}.selector")), - ) { - match obtain_dkim_public_key(algo, pk) { - Ok(public) => { - records.push(DnsRecord { - typ: "TXT".to_string(), - name: format!("{selector}._domainkey.{domain_name}.",), - content: match algo { - Algorithm::Rsa => format!("v=DKIM1; k=rsa; h=sha256; p={public}"), - Algorithm::Ed25519 => { - format!("v=DKIM1; k=ed25519; h=sha256; p={public}") - } - }, - }); - } - Err(err) => { - trc::error!(err); - } - } - } - } - - // Add SPF records - if server_name.ends_with(&format!(".{domain_name}")) || server_name == domain_name { - records.push(DnsRecord { - typ: "TXT".to_string(), - name: format!("{server_name}."), - content: "v=spf1 a ra=postmaster -all".to_string(), - }); - } - records.push(DnsRecord { - typ: "TXT".to_string(), - name: format!("{domain_name}."), - content: "v=spf1 mx ra=postmaster -all".to_string(), - }); - - let mut has_https = false; - for (protocol, port, is_tls) in self - .core - .storage - .config - .get_services() - .await - .unwrap_or_default() - { - match (protocol.as_str(), port) { - ("smtp", port @ 26..=u16::MAX) => { - records.push(DnsRecord { - typ: "SRV".to_string(), - name: format!( - "_submission{}._tcp.{domain_name}.", - if is_tls { "s" } else { "" } - ), - content: format!("0 1 {port} {server_name}."), - }); - } - ("imap" | "pop3", port @ 1..=u16::MAX) => { - records.push(DnsRecord { - typ: "SRV".to_string(), - name: format!( - "_{protocol}{}._tcp.{domain_name}.", - if is_tls { "s" } else { "" } - ), - content: format!("0 1 {port} {server_name}."), - }); - } - ("http", _) if is_tls => { - has_https = true; - for service in ["jmap", "caldavs", "carddavs"] { - records.push(DnsRecord { - typ: "SRV".to_string(), - name: format!("_{service}._tcp.{domain_name}.",), - content: format!("0 1 {port} {server_name}."), - }); - } - } - _ => (), - } - } - - if has_https { - // Add autoconfig and autodiscover records - records.push(DnsRecord { - typ: "CNAME".to_string(), - name: format!("autoconfig.{domain_name}."), - content: format!("{server_name}."), - }); - records.push(DnsRecord { - typ: "CNAME".to_string(), - name: format!("autodiscover.{domain_name}."), - content: format!("{server_name}."), - }); - - // Add MTA-STS records - if let Some(policy) = self.build_mta_sts_policy() { - records.push(DnsRecord { - typ: "CNAME".to_string(), - name: format!("mta-sts.{domain_name}."), - content: format!("{server_name}."), - }); - records.push(DnsRecord { - typ: "TXT".to_string(), - name: format!("_mta-sts.{domain_name}."), - content: format!("v=STSv1; id={}", policy.id), - }); - } - } - - // Add DMARC record - records.push(DnsRecord { - typ: "TXT".to_string(), - name: format!("_dmarc.{domain_name}."), - content: format!("v=DMARC1; p=reject; rua=mailto:postmaster@{domain_name}; ruf=mailto:postmaster@{domain_name}",), - }); - - // Add TLS reporting record - records.push(DnsRecord { - typ: "TXT".to_string(), - name: format!("_smtp._tls.{domain_name}."), - content: format!("v=TLSRPTv1; rua=mailto:postmaster@{domain_name}",), - }); - - // Add TLSA records - 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.") - || name.starts_with("autodiscover.") - { - continue; - } - - for (cert_num, cert) in key.cert.iter().enumerate() { - let parsed_cert = match parse_x509_certificate(cert) { - Ok((_, parsed_cert)) => parsed_cert, - Err(err) => { - trc::error!(manage::error( - "Failed to parse certificate", - err.to_string().into() - )); - continue; - } - }; - - let name = if !name.starts_with('.') { - format!("_25._tcp.{name}.") - } else { - format!("_25._tcp.mail.{name}.") - }; - let cu = if cert_num == 0 { 3 } else { 2 }; - - for (s, cert) in [cert, parsed_cert.subject_pki.raw].into_iter().enumerate() { - for (m, hash) in [ - format!("{:x}", sha2::Sha256::digest(cert)), - format!("{:x}", sha2::Sha512::digest(cert)), - ] - .into_iter() - .enumerate() - { - records.push(DnsRecord { - typ: "TLSA".to_string(), - name: name.clone(), - content: format!("{} {} {} {}", cu, s, m + 1, hash), - }); - } - } - } - } - - Ok(records) - } -} diff --git a/crates/http/src/management/enterprise/mod.rs b/crates/http/src/management/enterprise/mod.rs deleted file mode 100644 index 54b38bb5..00000000 --- a/crates/http/src/management/enterprise/mod.rs +++ /dev/null @@ -1,12 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC - * - * SPDX-License-Identifier: LicenseRef-SEL - * - * This file is subject to the Stalwart Enterprise License Agreement (SEL) and - * is NOT open source software. - * - */ - -pub mod telemetry; -pub mod undelete; diff --git a/crates/http/src/management/enterprise/undelete.rs b/crates/http/src/management/enterprise/undelete.rs deleted file mode 100644 index 45618b34..00000000 --- a/crates/http/src/management/enterprise/undelete.rs +++ /dev/null @@ -1,352 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC - * - * SPDX-License-Identifier: LicenseRef-SEL - * - * This file is subject to the Stalwart Enterprise License Agreement (SEL) and - * is NOT open source software. - * - */ - -use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD}; -use common::{Server, enterprise::undelete::DeletedItemType}; -use directory::backend::internal::manage::ManageDirectory; -use email::{ - mailbox::INBOX_ID, - message::ingest::{EmailIngest, IngestEmail, IngestSource}, -}; -use http_proto::{request::decode_path_element, *}; -use hyper::Method; -use mail_parser::{DateTime, MessageParser}; -use serde_json::json; -use std::future::Future; -use std::str::FromStr; -use store::write::{BatchBuilder, BlobLink, BlobOp}; -use trc::AddContext; -use types::{blob_hash::BlobHash, collection::Collection}; -use utils::url_params::UrlParams; - -#[derive(serde::Deserialize, serde::Serialize, Debug)] -pub struct UndeleteRequest { - pub hash: H, - pub collection: C, - #[serde(rename = "restoreTime")] - pub time: T, - #[serde(rename = "cancelDeletion")] - #[serde(default)] - pub cancel_deletion: Option, -} - -#[derive(serde::Serialize, serde::Deserialize, PartialEq, Eq, Debug)] -#[serde(tag = "type")] -#[serde(rename_all = "camelCase")] -pub enum UndeleteResponse { - Success, - NotFound, - Error { reason: String }, -} - -#[derive(Debug, serde::Serialize, serde::Deserialize)] -pub struct DeletedBlobResponse { - pub hash: String, - pub size: u32, - #[serde(rename = "deletedAt")] - pub deleted_at: String, - #[serde(rename = "expiresAt")] - pub expires_at: String, - pub item: DeletedItemResponse, -} - -#[derive(Debug, serde::Serialize, serde::Deserialize)] -#[serde(tag = "type")] -#[serde(rename_all = "camelCase")] -pub enum DeletedItemResponse { - Email { - from: Box, - subject: Box, - received_at: String, - }, - FileNode { - name: Box, - }, - CalendarEvent { - title: Box, - start_time: String, - }, - ContactCard { - name: Box, - }, - SieveScript { - name: Box, - }, -} - -pub trait UndeleteApi: Sync + Send { - fn handle_undelete_api_request( - &self, - req: &HttpRequest, - path: Vec<&str>, - body: Option>, - session: &HttpSessionData, - ) -> impl Future> + Send; -} - -impl UndeleteApi for Server { - async fn handle_undelete_api_request( - &self, - req: &HttpRequest, - path: Vec<&str>, - body: Option>, - session: &HttpSessionData, - ) -> trc::Result { - match (path.get(2).copied(), req.method()) { - (Some(account_name), &Method::GET) => { - let account_name = decode_path_element(account_name); - let account_id = self - .core - .storage - .data - .get_principal_id(account_name.as_ref()) - .await? - .ok_or_else(|| trc::ResourceEvent::NotFound.into_err())?; - let mut deleted = self.core.list_deleted(account_id).await?; - - let params = UrlParams::new(req.uri().query()); - let limit = params.parse::("limit").unwrap_or_default(); - let mut offset = params - .parse::("page") - .unwrap_or_default() - .saturating_sub(1) - * limit; - - // Sort ascending by deleted_at - let total = deleted.len(); - deleted.sort_by(|a, b| a.item.deleted_at.cmp(&b.item.deleted_at)); - let mut results = Vec::with_capacity(if limit > 0 { limit } else { total }); - - for blob in deleted { - if offset == 0 { - results.push(DeletedBlobResponse { - hash: URL_SAFE_NO_PAD.encode(blob.hash.as_slice()), - size: blob.item.size, - deleted_at: DateTime::from_timestamp(blob.item.deleted_at as i64) - .to_rfc3339(), - expires_at: DateTime::from_timestamp(blob.expires_at as i64) - .to_rfc3339(), - item: match blob.item.typ { - DeletedItemType::Email { - from, - subject, - received_at, - } => DeletedItemResponse::Email { - from, - subject, - received_at: DateTime::from_timestamp(received_at as i64) - .to_rfc3339(), - }, - DeletedItemType::FileNode { name } => { - DeletedItemResponse::FileNode { name } - } - DeletedItemType::CalendarEvent { title, start_time } => { - DeletedItemResponse::CalendarEvent { - title, - start_time: DateTime::from_timestamp(start_time as i64) - .to_rfc3339(), - } - } - DeletedItemType::ContactCard { name } => { - DeletedItemResponse::ContactCard { name } - } - DeletedItemType::SieveScript { name } => { - DeletedItemResponse::SieveScript { name } - } - }, - }); - if results.len() == limit { - break; - } - } else { - offset -= 1; - } - } - - Ok(JsonResponse::new(json!({ - "data":{ - "items": results, - "total": total, - }, - })) - .into_http_response()) - } - (Some(account_name), &Method::POST) => { - let account_name = decode_path_element(account_name); - let account_id = self - .core - .storage - .data - .get_principal_id(account_name.as_ref()) - .await? - .ok_or_else(|| trc::ResourceEvent::NotFound.into_err())?; - - let requests: Vec> = - match serde_json::from_slice::< - Option>>, - >(body.as_deref().unwrap_or_default()) - { - Ok(Some(requests)) => requests - .into_iter() - .map(|request| { - UndeleteRequest { - hash: BlobHash::try_from_hash_slice( - URL_SAFE_NO_PAD - .decode(request.hash.as_bytes()) - .ok()? - .as_slice(), - ) - .ok()?, - collection: Collection::from_str(request.collection.as_str()) - .ok()?, - time: DateTime::parse_rfc3339(request.time.as_str())? - .to_timestamp() - as u64, - cancel_deletion: if let Some(cancel_deletion) = - request.cancel_deletion - { - (DateTime::parse_rfc3339(cancel_deletion.as_str())? - .to_timestamp() - as u64) - .into() - } else { - None - }, - } - .into() - }) - .collect::>>() - .ok_or_else(|| trc::ResourceEvent::BadParameters.into_err())?, - Ok(None) => { - let deleted = self.core.list_deleted(account_id).await?; - let mut results = Vec::with_capacity(deleted.len()); - for blob in deleted { - results.push(UndeleteRequest { - hash: blob.hash, - collection: match blob.item.typ { - DeletedItemType::Email { .. } => Collection::Email, - DeletedItemType::FileNode { .. } => Collection::FileNode, - DeletedItemType::CalendarEvent { .. } => { - Collection::CalendarEvent - } - DeletedItemType::ContactCard { .. } => { - Collection::ContactCard - } - DeletedItemType::SieveScript { .. } => { - Collection::SieveScript - } - }, - time: blob.item.deleted_at, - cancel_deletion: blob.expires_at.into(), - }); - } - results - } - Err(_) => { - return Err(trc::ResourceEvent::BadParameters.into_err()); - } - }; - - let access_token = self - .get_access_token(account_id) - .await - .caused_by(trc::location!())?; - let mut results = Vec::with_capacity(requests.len()); - let mut batch = BatchBuilder::new(); - batch.with_account_id(account_id); - for request in requests { - match request.collection { - Collection::Email => { - match self - .blob_store() - .get_blob(request.hash.as_slice(), 0..usize::MAX) - .await? - { - Some(bytes) => { - match self - .email_ingest(IngestEmail { - raw_message: &bytes, - message: MessageParser::new().parse(&bytes), - blob_hash: Some(&request.hash), - access_token: access_token.as_ref(), - mailbox_ids: vec![INBOX_ID], - keywords: vec![], - received_at: request.time.into(), - source: IngestSource::Restore, - session_id: session.session_id, - }) - .await - { - Ok(_) => { - results.push(UndeleteResponse::Success); - if let Some(cancel_deletion) = request.cancel_deletion { - batch - .clear(BlobOp::Link { - hash: request.hash.clone(), - to: BlobLink::Temporary { - until: cancel_deletion, - }, - }) - .clear(BlobOp::Undelete { - hash: request.hash, - until: cancel_deletion, - }); - } - } - Err(mut err) - if err.matches(trc::EventType::MessageIngest( - trc::MessageIngestEvent::Error, - )) => - { - results.push(UndeleteResponse::Error { - reason: err - .take_value(trc::Key::Reason) - .and_then(|v| v.into_string()) - .unwrap() - .to_string(), - }); - } - Err(err) => { - return Err(err.caused_by(trc::location!())); - } - } - } - None => { - results.push(UndeleteResponse::NotFound); - } - } - } - _ => { - results.push(UndeleteResponse::Error { - reason: "Unsupported collection".to_string(), - }); - } - } - } - - // Commit batch - if !batch.is_empty() { - self.core - .storage - .data - .write(batch.build_all()) - .await - .caused_by(trc::location!())?; - } - - Ok(JsonResponse::new(json!({ - "data": results, - })) - .into_http_response()) - } - _ => Err(trc::ResourceEvent::NotFound.into_err()), - } - } -} diff --git a/crates/http/src/management/log.rs b/crates/http/src/management/log.rs deleted file mode 100644 index 4c0b25cf..00000000 --- a/crates/http/src/management/log.rs +++ /dev/null @@ -1,154 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL - */ - -use std::{ - fs::{self, File}, - io, - path::Path, -}; - -use chrono::DateTime; -use common::{Server, auth::AccessToken}; -use directory::{Permission, backend::internal::manage}; -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 http_proto::*; - -#[derive(Serialize)] -struct LogEntry { - timestamp: String, - level: String, - event: String, - event_id: String, - details: String, -} - -pub trait LogManagement: Sync + Send { - fn handle_view_logs( - &self, - req: &HttpRequest, - access_token: &AccessToken, - ) -> impl Future> + Send; -} - -impl LogManagement for Server { - async fn handle_view_logs( - &self, - req: &HttpRequest, - access_token: &AccessToken, - ) -> trc::Result { - // Validate the access token - access_token.assert_has_permission(Permission::LogsView)?; - - let path = self - .core - .metrics - .log_path - .clone() - .ok_or_else(|| manage::unsupported("Tracer log path not configured"))?; - - let params = UrlParams::new(req.uri().query()); - let filter = params.get("filter").unwrap_or_default().to_string(); - let page: usize = params.parse("page").unwrap_or(0); - let limit: usize = params.parse("limit").unwrap_or(100); - let offset = page.saturating_sub(1) * limit; - - // TODO: Use worker pool - let (tx, rx) = oneshot::channel(); - tokio::task::spawn_blocking(move || { - let _ = tx.send(read_log_files(path, &filter, offset, limit)); - }); - - let (total, items) = rx - .await - .map_err(|err| { - trc::EventType::Server(trc::ServerEvent::ThreadError) - .reason(err) - .caused_by(trc::location!()) - })? - .map_err(|err| { - trc::ManageEvent::Error - .reason(err) - .details("Failed to read log files") - .caused_by(trc::location!()) - })?; - - Ok(JsonResponse::new(json!({ - "data": { - "items": items, - "total": total, - }, - })) - .into_http_response()) - } -} - -fn read_log_files( - path: impl AsRef, - filter: &str, - mut offset: usize, - limit: usize, -) -> io::Result<(usize, Vec)> { - let mut logs = fs::read_dir(path)?.collect::, _>>()?; - let mut total = 0; - - // Sort the entries by file name in reverse order. - logs.sort_by_key(|b| std::cmp::Reverse(b.file_name())); - - // Iterate and print the file names. - let mut entries = Vec::with_capacity(limit); - let mut logs = logs.into_iter(); - while let Some(log) = logs.next() { - if log.file_type()?.is_file() { - let mut rev_lines = RevLines::new(File::open(log.path())?); - - while let Some(line) = rev_lines.next() { - let line = line.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; - if filter.is_empty() || line.contains(filter) { - total += 1; - if offset == 0 { - if let Some(entry) = LogEntry::from_line(&line) { - entries.push(entry); - if entries.len() == limit { - if rev_lines.next().is_some() || logs.next().is_some() { - total += limit; - } - - return Ok((total, entries)); - } - } - } else { - offset -= 1; - } - } - } - } - } - - Ok((total, entries)) -} - -impl LogEntry { - fn from_line(line: &str) -> Option { - let (timestamp, rest) = line.split_once(' ')?; - let timestamp = DateTime::parse_from_rfc3339(timestamp).ok()?; - let (level, rest) = rest.trim().split_once(' ')?; - let (event, rest) = rest.trim().split_once(" (")?; - let (event_id, details) = rest.split_once(")")?; - Some(Self { - timestamp: timestamp.to_rfc3339_opts(chrono::SecondsFormat::Secs, true), - level: level.to_string(), - event: event.to_string(), - event_id: event_id.to_string(), - details: details.trim().to_string(), - }) - } -} diff --git a/crates/http/src/management/mod.rs b/crates/http/src/management/mod.rs index 24c194b5..03884b0f 100644 --- a/crates/http/src/management/mod.rs +++ b/crates/http/src/management/mod.rs @@ -4,53 +4,24 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -pub mod crypto; -pub mod dkim; -pub mod dns; -pub mod log; -pub mod principal; -pub mod queue; -pub mod reload; -pub mod report; -pub mod settings; -pub mod spam; -pub mod stores; -pub mod troubleshoot; - // SPDX-SnippetBegin // SPDX-FileCopyrightText: 2020 Stalwart Labs LLC // SPDX-License-Identifier: LicenseRef-SEL #[cfg(feature = "enterprise")] -pub mod enterprise; - -#[cfg(feature = "enterprise")] -use enterprise::telemetry::TelemetryApi; +pub mod telemetry; // SPDX-SnippetEnd +pub mod diagnose; -use crate::auth::oauth::auth::OAuthApiHandler; +use crate::management::diagnose::TroubleshootApi; use common::{Server, auth::AccessToken}; -use crypto::CryptoHandler; -use directory::{Permission, backend::internal::manage}; -use dkim::DkimManagement; -use dns::DnsManagement; -use http_proto::{request::fetch_body, *}; -use hyper::{Method, StatusCode, header}; +use http_proto::{ + HttpRequest, HttpResponse, HttpSessionData, JsonResponse, ToHttpResponse, request::fetch_body, +}; +use hyper::{StatusCode, header}; use jmap::api::{ToJmapHttpResponse, ToRequestError}; use jmap_proto::error::request::RequestError; -use log::LogManagement; -use mail_parser::DateTime; -use principal::PrincipalManager; -use queue::QueueManagement; -use reload::ManageReload; -use report::ManageReports; +use registry::schema::enums::Permission; use serde::Serialize; -use settings::ManageSettings; -use spam::ManageSpamHandler; -use std::future::Future; -use std::{str::FromStr, sync::Arc}; -use store::write::now; -use stores::ManageStore; -use troubleshoot::TroubleshootApi; #[derive(Serialize)] #[serde(tag = "error")] @@ -80,7 +51,7 @@ pub trait ManagementApi: Sync + Send { fn handle_api_manage_request( &self, req: &mut HttpRequest, - access_token: Arc, + access_token: &AccessToken, session: &HttpSessionData, ) -> impl Future> + Send; } @@ -90,85 +61,18 @@ impl ManagementApi for Server { async fn handle_api_manage_request( &self, req: &mut HttpRequest, - access_token: Arc, + access_token: &AccessToken, session: &HttpSessionData, ) -> trc::Result { let body = fetch_body(req, 1024 * 1024, session.session_id).await; let path = req.uri().path().split('/').skip(2).collect::>(); match path.first().copied().unwrap_or_default() { - "queue" => self.handle_manage_queue(req, path, &access_token).await, - "settings" => { - self.handle_manage_settings(req, path, body, &access_token) - .await - } - "reports" => self.handle_manage_reports(req, path, &access_token).await, - "principal" => { - self.handle_manage_principal(req, path, body, &access_token) - .await - } - "dns" => self.handle_manage_dns(req, path, &access_token).await, - "store" => { - self.handle_manage_store(req, path, body, session, &access_token) - .await - } - "reload" => self.handle_manage_reload(req, path, &access_token).await, - "dkim" => { - self.handle_manage_dkim(req, path, body, &access_token) - .await - } - "update" => self.handle_manage_update(req, path, &access_token).await, - "logs" if req.method() == Method::GET => { - self.handle_view_logs(req, &access_token).await - } - "spam-filter" => { - self.handle_manage_spam(req, path, body, session, &access_token) - .await - } - "restart" if req.method() == Method::GET => { + "diagnose" => { // Validate the access token - access_token.assert_has_permission(Permission::Restart)?; + access_token.enforce_permission(Permission::Troubleshoot)?; - Err(manage::unsupported("Restart is not yet supported")) - } - "oauth" => { - // Validate the access token - access_token.assert_has_permission(Permission::AuthenticateOauth)?; - - self.handle_oauth_api_request(access_token, body).await - } - "account" => match (path.get(1).copied().unwrap_or_default(), req.method()) { - ("crypto", &Method::POST) => { - // Validate the access token - access_token.assert_has_permission(Permission::ManageEncryption)?; - - self.handle_crypto_post(access_token, body).await - } - ("crypto", &Method::GET) => { - // Validate the access token - access_token.assert_has_permission(Permission::ManageEncryption)?; - - self.handle_crypto_get(access_token).await - } - ("auth", &Method::GET) => { - // Validate the access token - access_token.assert_has_permission(Permission::ManagePasswords)?; - - self.handle_account_auth_get(access_token).await - } - ("auth", &Method::POST) => { - // Validate the access token - access_token.assert_has_permission(Permission::ManagePasswords)?; - - self.handle_account_auth_post(req, access_token, body).await - } - _ => Err(trc::ResourceEvent::NotFound.into_err()), - }, - "troubleshoot" => { - // Validate the access token - access_token.assert_has_permission(Permission::Troubleshoot)?; - - self.handle_troubleshoot_api_request(req, path, &access_token, body) + self.handle_diagnose_api_request(req, path, access_token, body) .await } // SPDX-SnippetBegin @@ -186,10 +90,12 @@ impl ManagementApi for Server { // for copyright infringement, breach of contract, and fraud. if self.core.is_enterprise_edition() { - self.handle_telemetry_api_request(req, path, &access_token) + use crate::management::telemetry::TelemetryApi; + + self.handle_telemetry_api_request(req, path, access_token) .await } else { - Err(manage::enterprise()) + Err(trc::ManageEvent::NotSupported.ctx(trc::Key::Details, "Enterprise feature")) } } // SPDX-SnippetEnd @@ -198,48 +104,6 @@ impl ManagementApi for Server { } } -pub(super) struct FutureTimestamp(u64); -pub(super) struct Timestamp(u64); - -impl FromStr for Timestamp { - type Err = (); - - fn from_str(s: &str) -> Result { - if let Some(dt) = DateTime::parse_rfc3339(s) { - Ok(Timestamp(dt.to_timestamp() as u64)) - } else { - Err(()) - } - } -} - -impl FromStr for FutureTimestamp { - type Err = (); - - fn from_str(s: &str) -> Result { - if let Some(dt) = DateTime::parse_rfc3339(s) { - let instant = dt.to_timestamp() as u64; - if instant >= now() { - return Ok(FutureTimestamp(instant)); - } - } - - Err(()) - } -} - -impl FutureTimestamp { - pub fn into_inner(self) -> u64 { - self.0 - } -} - -impl Timestamp { - pub fn into_inner(self) -> u64 { - self.0 - } -} - pub trait ToManageHttpResponse { fn into_http_response(self) -> HttpResponse; } diff --git a/crates/http/src/management/principal.rs b/crates/http/src/management/principal.rs deleted file mode 100644 index fb559f13..00000000 --- a/crates/http/src/management/principal.rs +++ /dev/null @@ -1,888 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL - */ - -use crate::management::stores::destroy_account_data; -use common::{Server, auth::AccessToken}; -use directory::{ - DirectoryInner, Permission, PrincipalData, QueryBy, QueryParams, Type, - backend::internal::{ - PrincipalAction, PrincipalField, PrincipalSet, PrincipalUpdate, PrincipalValue, - lookup::DirectoryStore, - manage::{ - self, ChangedPrincipals, ManageDirectory, PrincipalList, UpdatePrincipal, not_found, - }, - }, -}; -use http_proto::{request::decode_path_element, *}; -use hyper::{Method, header}; -use serde_json::json; -use std::future::Future; -use std::sync::Arc; -use trc::AddContext; -use utils::url_params::UrlParams; - -#[derive(Debug, serde::Serialize, serde::Deserialize)] -#[serde(tag = "type")] -#[serde(rename_all = "camelCase")] -pub enum AccountAuthRequest { - SetPassword { password: String }, - EnableOtpAuth { url: String }, - DisableOtpAuth { url: Option }, - AddAppPassword { name: String, password: String }, - RemoveAppPassword { name: Option }, -} - -#[derive(Debug, serde::Serialize, serde::Deserialize)] -pub struct AccountAuthResponse { - #[serde(rename = "otpEnabled")] - pub otp_auth: bool, - #[serde(rename = "appPasswords")] - pub app_passwords: Vec, -} - -pub trait PrincipalManager: Sync + Send { - fn handle_manage_principal( - &self, - req: &HttpRequest, - path: Vec<&str>, - body: Option>, - access_token: &AccessToken, - ) -> impl Future> + Send; - - fn handle_account_auth_get( - &self, - access_token: Arc, - ) -> impl Future> + Send; - - fn handle_account_auth_post( - &self, - req: &HttpRequest, - access_token: Arc, - body: Option>, - ) -> impl Future> + Send; - - fn assert_supported_directory(&self, override_: bool) -> trc::Result<()>; -} - -impl PrincipalManager for Server { - async fn handle_manage_principal( - &self, - req: &HttpRequest, - path: Vec<&str>, - body: Option>, - access_token: &AccessToken, - ) -> trc::Result { - match (path.get(1).copied(), req.method()) { - (None | Some("deploy"), &Method::POST) => { - // Parse principal - let principal = - serde_json::from_slice::(body.as_deref().unwrap_or_default()) - .map_err(|err| { - trc::EventType::Resource(trc::ResourceEvent::BadParameters) - .from_json_error(err) - })?; - - // Validate the access token - access_token.assert_has_permission(match principal.typ() { - Type::Individual => Permission::IndividualCreate, - Type::Group => Permission::GroupCreate, - Type::List => Permission::MailingListCreate, - Type::Domain => Permission::DomainCreate, - Type::Tenant => Permission::TenantCreate, - Type::Role => Permission::RoleCreate, - Type::ApiKey => Permission::ApiKeyCreate, - Type::OauthClient => Permission::OauthClientCreate, - Type::Resource | Type::Location | Type::Other => Permission::PrincipalCreate, - })?; - - // SPDX-SnippetBegin - // SPDX-FileCopyrightText: 2020 Stalwart Labs LLC - // SPDX-License-Identifier: LicenseRef-SEL - - #[cfg(feature = "enterprise")] - { - if (matches!(principal.typ(), Type::Tenant) - || principal.has_field(PrincipalField::Tenant)) - && !self.core.is_enterprise_edition() - { - return Err(manage::enterprise()); - } - - if matches!(principal.typ(), Type::Individual) - && self.core.is_enterprise_edition() - && !self.can_create_account().await? - { - return Err(manage::error( - "License account limit reached", - format!( - "Enterprise licensed account limit reached: {} accounts licensed.", - self.licensed_accounts() - ) - .into(), - )); - } - } - - // SPDX-SnippetEnd - - // Make sure the current directory supports updates - if matches!(principal.typ(), Type::Individual) { - self.assert_supported_directory(path.get(1).copied() == Some("deploy"))?; - } - - // Validate roles - let tenant_id = access_token.tenant.map(|t| t.id); - for name in principal - .get_str_array(PrincipalField::Roles) - .unwrap_or_default() - { - if let Some(pinfo) = self - .store() - .get_principal_info(name) - .await - .caused_by(trc::location!())? - .filter(|v| v.typ == Type::Role && v.has_tenant_access(tenant_id)) - .or_else(|| PrincipalField::Roles.map_internal_roles(name)) - { - let role_permissions = - self.get_role_permissions(pinfo.id).await?.finalize_as_ref(); - let mut allowed_permissions = role_permissions.clone(); - allowed_permissions.intersection(&access_token.permissions); - if allowed_permissions != role_permissions { - return Err(manage::error( - "Invalid role", - format!("Your account cannot grant the {name:?} role").into(), - )); - } - } - } - - // Set default report domain if missing - let report_domain = if principal.typ() == Type::Domain - && self - .core - .storage - .config - .get("report.domain") - .await - .is_ok_and(|v| v.is_none()) - { - principal.name().to_lowercase().into() - } else { - None - }; - - // Create principal - let result = self - .core - .storage - .data - .create_principal(principal, tenant_id, Some(&access_token.permissions)) - .await?; - - // Set report domain - if let Some(report_domain) = report_domain - && let Err(err) = self - .core - .storage - .config - .set([("report.domain", report_domain)], true) - .await - { - trc::error!(err.details("Failed to set report domain")); - } - - // Increment revision - self.invalidate_principal_caches(result.changed_principals) - .await; - - Ok(JsonResponse::new(json!({ - "data": result.id, - })) - .into_http_response()) - } - (None, &Method::GET) => { - // List principal ids - let params = UrlParams::new(req.uri().query()); - let filter = params.get("filter"); - let page: usize = params.parse("page").unwrap_or(0); - let limit: usize = params.parse("limit").unwrap_or(0); - let count = params.get("count").is_some(); - - // Parse types - let mut types = Vec::new(); - for typ in params - .get("types") - .or_else(|| params.get("type")) - .unwrap_or_default() - .split(',') - { - if let Some(typ) = Type::parse(typ) - && !types.contains(&typ) - { - types.push(typ); - } - } - - // Parse fields - let mut fields = Vec::new(); - for field in params.get("fields").unwrap_or_default().split(',') { - if let Some(field) = PrincipalField::try_parse(field) - && !fields.contains(&field) - { - fields.push(field); - } - } - - // Validate the access token - let validate_types = if !types.is_empty() { - types.as_slice() - } else { - &[ - Type::Individual, - Type::Group, - Type::List, - Type::Domain, - Type::Tenant, - Type::Role, - Type::Other, - Type::ApiKey, - Type::OauthClient, - ] - }; - for typ in validate_types { - access_token.assert_has_permission(match typ { - Type::Individual => Permission::IndividualList, - Type::Group => Permission::GroupList, - Type::List => Permission::MailingListList, - Type::Domain => Permission::DomainList, - Type::Tenant => Permission::TenantList, - Type::Role => Permission::RoleList, - Type::ApiKey => Permission::ApiKeyList, - Type::OauthClient => Permission::OauthClientList, - Type::Resource | Type::Location | Type::Other => Permission::PrincipalList, - })?; - } - - let mut tenant = access_token.tenant.map(|t| t.id); - - // SPDX-SnippetBegin - // SPDX-FileCopyrightText: 2020 Stalwart Labs LLC - // SPDX-License-Identifier: LicenseRef-SEL - #[cfg(feature = "enterprise")] - { - if self.core.is_enterprise_edition() { - if tenant.is_none() { - // Limit search to current tenant - if let Some(tenant_name) = params.get("tenant") { - tenant = self - .core - .storage - .data - .get_principal_info(tenant_name) - .await? - .filter(|p| p.typ == Type::Tenant) - .map(|p| p.id); - } - } - } else if types.contains(&Type::Tenant) { - return Err(manage::enterprise()); - } - } - // SPDX-SnippetEnd - - let principals = self - .store() - .list_principals( - filter, - tenant, - &types, - fields.len() != 1 - || fields.first().is_none_or(|v| v != &PrincipalField::Name), - page, - limit, - ) - .await?; - - let principals: PrincipalList = if !count { - let mut expanded = PrincipalList { - items: Vec::with_capacity(principals.items.len()), - total: principals.total, - }; - - for principal in principals.items { - expanded - .items - .push(self.store().map_principal(principal, &fields).await?); - } - - expanded - } else { - PrincipalList { - items: vec![], - total: principals.total, - } - }; - - Ok(JsonResponse::new(json!({ - "data": principals, - })) - .into_http_response()) - } - (None, &Method::DELETE) => { - // List principal ids - let params = UrlParams::new(req.uri().query()); - let filter = params.get("filter"); - let typ = params.parse::("type").ok_or_else(|| { - trc::EventType::Resource(trc::ResourceEvent::BadParameters) - .into_err() - .details("Invalid type") - })?; - if params.get("confirm") != Some("true") { - return Err(trc::EventType::Resource(trc::ResourceEvent::BadParameters) - .into_err() - .details("Missing confirmation parameter")); - } - - // Validate the access token - access_token.assert_has_permission(match typ { - Type::Individual => Permission::IndividualDelete, - Type::Group => Permission::GroupDelete, - Type::List => Permission::MailingListDelete, - Type::Domain => Permission::DomainDelete, - Type::Tenant => Permission::TenantDelete, - Type::Role => Permission::RoleDelete, - Type::ApiKey => Permission::ApiKeyDelete, - Type::OauthClient => Permission::OauthClientDelete, - Type::Resource | Type::Location | Type::Other => Permission::PrincipalDelete, - })?; - - let mut tenant = access_token.tenant.map(|t| t.id); - - // SPDX-SnippetBegin - // SPDX-FileCopyrightText: 2020 Stalwart Labs LLC - // SPDX-License-Identifier: LicenseRef-SEL - #[cfg(feature = "enterprise")] - { - if self.core.is_enterprise_edition() { - if tenant.is_none() { - // Limit search to current tenant - if let Some(tenant_name) = params.get("tenant") { - tenant = self - .core - .storage - .data - .get_principal_info(tenant_name) - .await? - .filter(|p| p.typ == Type::Tenant) - .map(|p| p.id); - } - } - } else if typ == Type::Tenant { - return Err(manage::enterprise()); - } - } - // SPDX-SnippetEnd - - let principals = self - .store() - .list_principals(filter, tenant, &[typ], false, 0, 0) - .await?; - - let found = !principals.items.is_empty(); - if found { - let server = self.clone(); - tokio::spawn(async move { - for principal in principals.items { - // Delete account - match server - .store() - .delete_principal(QueryBy::Id(principal.id())) - .await - { - Ok(changed_principals) => { - // Increment revision - server.invalidate_principal_caches(changed_principals).await; - } - Err(err) => { - trc::error!(err.details("Failed to delete principal")); - continue; - } - } - - if let Err(err) = destroy_account_data( - &server, - principal.id(), - matches!(typ, Type::Individual | Type::Group), - ) - .await - { - trc::error!(err.details("Failed to delete principal")); - } - } - }); - } - - Ok(JsonResponse::new(json!({ - "data": found, - })) - .into_http_response()) - } - (Some(name), method) => { - // Fetch, update or delete principal - let name = decode_path_element(name); - let (account_id, typ) = self - .core - .storage - .data - .get_principal_info(name.as_ref()) - .await? - .filter(|p| p.has_tenant_access(access_token.tenant.map(|t| t.id))) - .map(|p| (p.id, p.typ)) - .ok_or_else(|| not_found(name.to_string()))?; - - // SPDX-SnippetBegin - // SPDX-FileCopyrightText: 2020 Stalwart Labs LLC - // SPDX-License-Identifier: LicenseRef-SEL - - #[cfg(feature = "enterprise")] - { - if matches!(typ, Type::Tenant) && !self.core.is_enterprise_edition() { - return Err(manage::enterprise()); - } - } - - // SPDX-SnippetEnd - - match *method { - Method::GET => { - // Validate the access token - access_token.assert_has_permission(match typ { - Type::Individual => Permission::IndividualGet, - Type::Group => Permission::GroupGet, - Type::List => Permission::MailingListGet, - Type::Domain => Permission::DomainGet, - Type::Tenant => Permission::TenantGet, - Type::Role => Permission::RoleGet, - Type::ApiKey => Permission::ApiKeyGet, - Type::OauthClient => Permission::OauthClientGet, - Type::Resource | Type::Location | Type::Other => { - Permission::PrincipalGet - } - })?; - - let principal = self - .store() - .query(QueryParams::id(account_id).with_return_member_of(true)) - .await? - .ok_or_else(|| trc::ManageEvent::NotFound.into_err())?; - - // Map fields - let principal = self - .core - .storage - .data - .map_principal(principal, &[]) - .await - .caused_by(trc::location!())?; - - Ok(JsonResponse::new(json!({ - "data": principal, - })) - .into_http_response()) - } - Method::DELETE => { - // Validate the access token - access_token.assert_has_permission(match typ { - Type::Individual => Permission::IndividualDelete, - Type::Group => Permission::GroupDelete, - Type::List => Permission::MailingListDelete, - Type::Domain => Permission::DomainDelete, - Type::Tenant => Permission::TenantDelete, - Type::Role => Permission::RoleDelete, - Type::ApiKey => Permission::ApiKeyDelete, - Type::OauthClient => Permission::OauthClientDelete, - Type::Resource | Type::Location | Type::Other => { - Permission::PrincipalDelete - } - })?; - - // Delete account - let changed_principals = self - .store() - .delete_principal(QueryBy::Id(account_id)) - .await?; - - if let Err(err) = destroy_account_data( - self, - account_id, - matches!(typ, Type::Individual | Type::Group), - ) - .await - { - trc::error!(err.details("Failed to delete principal")); - } - - // Increment revision - self.invalidate_principal_caches(changed_principals).await; - - Ok(JsonResponse::new(json!({ - "data": (), - })) - .into_http_response()) - } - Method::PATCH => { - // Validate the access token - let permission_needed = match typ { - Type::Individual => Permission::IndividualUpdate, - Type::Group => Permission::GroupUpdate, - Type::List => Permission::MailingListUpdate, - Type::Domain => Permission::DomainUpdate, - Type::Tenant => Permission::TenantUpdate, - Type::Role => Permission::RoleUpdate, - Type::ApiKey => Permission::ApiKeyUpdate, - Type::OauthClient => Permission::OauthClientUpdate, - Type::Resource | Type::Location | Type::Other => { - Permission::PrincipalUpdate - } - }; - access_token.assert_has_permission(permission_needed)?; - - let changes = serde_json::from_slice::>( - body.as_deref().unwrap_or_default(), - ) - .map_err(|err| { - trc::EventType::Resource(trc::ResourceEvent::BadParameters) - .from_json_error(err) - })?; - - // Validate changes - let mut invalidate_logo_cache = false; - for change in &changes { - match change.field { - PrincipalField::Secrets - | PrincipalField::Name - | PrincipalField::Emails - | PrincipalField::Quota - | PrincipalField::UsedQuota - | PrincipalField::Description - | PrincipalField::Type - | PrincipalField::MemberOf - | PrincipalField::Members - | PrincipalField::Lists - | PrincipalField::Urls - | PrincipalField::ExternalMembers - | PrincipalField::Locale => (), - PrincipalField::Picture => { - invalidate_logo_cache |= - matches!(typ, Type::Domain | Type::Tenant); - } - PrincipalField::Tenant => { - // Tenants are not allowed to change their tenantId - if access_token.tenant.is_some() { - trc::bail!( - trc::SecurityEvent::Unauthorized - .into_err() - .details(permission_needed.name()) - .ctx( - trc::Key::Reason, - "Tenants cannot change their tenantId" - ) - ); - } - } - PrincipalField::Roles - | PrincipalField::EnabledPermissions - | PrincipalField::DisabledPermissions => { - if change.field == PrincipalField::Roles - && matches!( - change.action, - PrincipalAction::AddItem | PrincipalAction::Set - ) - { - let roles = match &change.value { - PrincipalValue::String(v) => std::slice::from_ref(v), - PrincipalValue::StringList(vec) => vec, - PrincipalValue::Integer(_) - | PrincipalValue::IntegerList(_) => continue, - }; - - // Validate roles - let tenant_id = access_token.tenant.map(|t| t.id); - for name in roles { - if let Some(pinfo) = self - .store() - .get_principal_info(name) - .await - .caused_by(trc::location!())? - .filter(|v| { - v.typ == Type::Role - && v.has_tenant_access(tenant_id) - }) - .or_else(|| { - PrincipalField::Roles.map_internal_roles(name) - }) - { - let role_permissions = self - .get_role_permissions(pinfo.id) - .await? - .finalize_as_ref(); - let mut allowed_permissions = - role_permissions.clone(); - allowed_permissions - .intersection(&access_token.permissions); - if allowed_permissions != role_permissions { - return Err(manage::error( - "Invalid role", - format!("Your account cannot grant the {name:?} role").into(), - )); - } - } - } - } - } - } - } - - // Update principal - let changed_principals = self - .core - .storage - .data - .update_principal( - UpdatePrincipal::by_id(account_id) - .with_updates(changes) - .with_tenant(access_token.tenant.map(|t| t.id)) - .with_allowed_permissions(&access_token.permissions), - ) - .await?; - - // Increment revision - self.invalidate_principal_caches(changed_principals).await; - - // Invalidate logo cache if needed - if invalidate_logo_cache { - self.inner.data.logos.lock().clear(); - } - - Ok(JsonResponse::new(json!({ - "data": (), - })) - .into_http_response()) - } - _ => Err(trc::ResourceEvent::NotFound.into_err()), - } - } - - _ => Err(trc::ResourceEvent::NotFound.into_err()), - } - } - - async fn handle_account_auth_get( - &self, - access_token: Arc, - ) -> trc::Result { - let mut response = AccountAuthResponse { - otp_auth: false, - app_passwords: Vec::new(), - }; - - if access_token.account_id() != u32::MAX { - let principal = self - .directory() - .query(QueryParams::id(access_token.account_id()).with_return_member_of(false)) - .await? - .ok_or_else(|| trc::ManageEvent::NotFound.into_err())?; - - for data in &principal.data { - match data { - PrincipalData::OtpAuth(_) => { - response.otp_auth = true; - } - PrincipalData::AppPassword(secret) => { - if let Some((app_name, _)) = - secret.strip_prefix("$app$").and_then(|s| s.split_once('$')) - { - response.app_passwords.push(app_name.into()); - } - } - _ => {} - } - } - } - - Ok(JsonResponse::new(json!({ - "data": response, - })) - .into_http_response()) - } - - async fn handle_account_auth_post( - &self, - req: &HttpRequest, - access_token: Arc, - body: Option>, - ) -> trc::Result { - // Parse request - let requests = - serde_json::from_slice::>(body.as_deref().unwrap_or_default()) - .map_err(|err| { - trc::EventType::Resource(trc::ResourceEvent::BadParameters).from_json_error(err) - })?; - - if requests.is_empty() { - return Err(trc::EventType::Resource(trc::ResourceEvent::BadParameters) - .into_err() - .details("Empty request")); - } - - // Make sure the user authenticated using Basic auth - if requests.iter().any(|r| { - matches!( - r, - AccountAuthRequest::DisableOtpAuth { .. } - | AccountAuthRequest::EnableOtpAuth { .. } - | AccountAuthRequest::SetPassword { .. } - ) - }) && req - .headers() - .get(header::AUTHORIZATION) - .and_then(|h| h.to_str().ok()) - .is_none_or(|header| !header.to_lowercase().starts_with("basic ")) - { - return Err(manage::error( - "Password changes only allowed using Basic auth", - None::, - )); - } - - // Handle Fallback admin password changes - if access_token.account_id() == u32::MAX { - match requests.into_iter().next().unwrap() { - AccountAuthRequest::SetPassword { password } => { - self.core - .storage - .config - .set( - [("authentication.fallback-admin.secret", password.to_string())], - true, - ) - .await?; - - // Increment revision - self.invalidate_principal_caches(ChangedPrincipals::from_change( - access_token.account_id(), - Type::Individual, - PrincipalField::Secrets, - )) - .await; - - return Ok(JsonResponse::new(json!({ - "data": (), - })) - .into_http_response()); - } - _ => { - return Err(manage::error( - "Fallback administrator accounts do not support 2FA or AppPasswords", - None::, - )); - } - } - } - - // Make sure the current directory supports updates - if requests.iter().any(|r| { - matches!( - r, - AccountAuthRequest::SetPassword { .. } - | AccountAuthRequest::EnableOtpAuth { .. } - | AccountAuthRequest::DisableOtpAuth { .. } - ) - }) { - self.assert_supported_directory(false)?; - } - - // Build actions - let mut actions = Vec::with_capacity(requests.len()); - for request in requests { - let (action, secret) = match request { - AccountAuthRequest::SetPassword { password } => { - actions.push(PrincipalUpdate { - action: PrincipalAction::RemoveItem, - field: PrincipalField::Secrets, - value: PrincipalValue::String(String::new()), - }); - - (PrincipalAction::AddItem, password) - } - AccountAuthRequest::EnableOtpAuth { url } => (PrincipalAction::AddItem, url), - AccountAuthRequest::DisableOtpAuth { url } => ( - PrincipalAction::RemoveItem, - url.unwrap_or_else(|| "otpauth://".into()), - ), - AccountAuthRequest::AddAppPassword { name, password } => { - (PrincipalAction::AddItem, format!("$app${name}${password}")) - } - AccountAuthRequest::RemoveAppPassword { name } => ( - PrincipalAction::RemoveItem, - format!("$app${}", name.unwrap_or_default()), - ), - }; - - actions.push(PrincipalUpdate { - action, - field: PrincipalField::Secrets, - value: PrincipalValue::String(secret), - }); - } - - // Update password - let changed_principals = self - .core - .storage - .data - .update_principal( - UpdatePrincipal::by_id(access_token.account_id()) - .with_updates(actions) - .with_tenant(access_token.tenant.map(|t| t.id)), - ) - .await?; - - // Increment revision - self.invalidate_principal_caches(changed_principals).await; - - Ok(JsonResponse::new(json!({ - "data": (), - })) - .into_http_response()) - } - - fn assert_supported_directory(&self, override_: bool) -> trc::Result<()> { - let class = match &self.core.storage.directory.store { - DirectoryInner::Internal(_) => return Ok(()), - DirectoryInner::Ldap(_) => "LDAP", - DirectoryInner::Sql(_) => "SQL", - DirectoryInner::Imap(_) => "IMAP", - DirectoryInner::Smtp(_) => "SMTP", - DirectoryInner::Memory(_) => "In-Memory", - DirectoryInner::OpenId(_) => "OpenID", - }; - - if !override_ { - Err(manage::unsupported(format!( - concat!( - "{} directory cannot be managed. ", - "Only internal directories support inserts ", - "and update operations." - ), - class - ))) - } else { - Ok(()) - } - } -} diff --git a/crates/http/src/management/queue.rs b/crates/http/src/management/queue.rs deleted file mode 100644 index ad40267b..00000000 --- a/crates/http/src/management/queue.rs +++ /dev/null @@ -1,950 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL - */ - -use super::FutureTimestamp; -use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD}; -use common::{ - Server, - auth::AccessToken, - config::smtp::queue::{ArchivedQueueExpiry, QueueExpiry, QueueName}, - ipc::QueueEvent, -}; -use directory::{Permission, Type, backend::internal::manage::ManageDirectory}; -use http_proto::{request::decode_path_element, *}; -use hyper::Method; -use mail_auth::{ - dmarc::URI, - mta_sts::ReportUri, - report::{self, tlsrpt::TlsReport}, -}; -use mail_parser::DateTime; -use serde::{Deserializer, Serializer}; -use serde_json::json; -use smtp::{ - queue::{ - self, ArchivedMessage, ArchivedStatus, ErrorDetails, QueueId, Status, spool::SmtpSpool, - }, - reporting::{dmarc::DmarcReporting, tls::TlsReporting}, -}; -use std::{future::Future, sync::atomic::Ordering}; -use store::{ - Deserialize, IterateParams, ValueKey, - write::{ - AlignedBytes, Archive, QueueClass, ReportEvent, ValueClass, key::DeserializeBigEndian, now, - }, -}; -use trc::AddContext; -use utils::url_params::UrlParams; - -#[derive(Debug, serde::Serialize, serde::Deserialize, PartialEq, Eq)] -pub struct Message { - pub id: QueueId, - - pub return_path: String, - - pub recipients: Vec, - - #[serde(deserialize_with = "deserialize_datetime")] - #[serde(serialize_with = "serialize_datetime")] - pub created: DateTime, - - pub size: u64, - - #[serde(skip_serializing_if = "is_zero")] - #[serde(default)] - pub priority: i16, - - #[serde(skip_serializing_if = "Option::is_none")] - pub env_id: Option, - - pub blob_hash: String, -} - -#[derive(Debug, serde::Serialize, serde::Deserialize, PartialEq, Eq)] -pub struct Recipient { - pub address: String, - pub queue: String, - pub status: Status, - pub retry_num: u32, - - #[serde(skip_serializing_if = "Option::is_none")] - #[serde(deserialize_with = "deserialize_maybe_datetime")] - #[serde(serialize_with = "serialize_maybe_datetime")] - pub next_retry: Option, - - #[serde(skip_serializing_if = "Option::is_none")] - #[serde(deserialize_with = "deserialize_maybe_datetime")] - #[serde(serialize_with = "serialize_maybe_datetime")] - pub next_notify: Option, - - #[serde(skip_serializing_if = "Option::is_none")] - #[serde(deserialize_with = "deserialize_maybe_datetime")] - #[serde(serialize_with = "serialize_maybe_datetime")] - pub expires: Option, - - #[serde(skip_serializing_if = "Option::is_none")] - pub orcpt: Option, -} - -#[derive(Debug, serde::Serialize, serde::Deserialize)] -#[serde(tag = "type")] -#[serde(rename_all = "camelCase")] -pub enum Report { - Tls { - id: String, - domain: String, - #[serde(deserialize_with = "deserialize_datetime")] - #[serde(serialize_with = "serialize_datetime")] - range_from: DateTime, - #[serde(deserialize_with = "deserialize_datetime")] - #[serde(serialize_with = "serialize_datetime")] - range_to: DateTime, - report: TlsReport, - rua: Vec, - }, - Dmarc { - id: String, - domain: String, - #[serde(deserialize_with = "deserialize_datetime")] - #[serde(serialize_with = "serialize_datetime")] - range_from: DateTime, - #[serde(deserialize_with = "deserialize_datetime")] - #[serde(serialize_with = "serialize_datetime")] - range_to: DateTime, - report: report::Report, - rua: Vec, - }, -} - -pub trait QueueManagement: Sync + Send { - fn handle_manage_queue( - &self, - req: &HttpRequest, - path: Vec<&str>, - access_token: &AccessToken, - ) -> impl Future> + Send; -} - -impl QueueManagement for Server { - async fn handle_manage_queue( - &self, - req: &HttpRequest, - path: Vec<&str>, - access_token: &AccessToken, - ) -> trc::Result { - let params = UrlParams::new(req.uri().query()); - let mut tenant_domains: Option> = None; - - // SPDX-SnippetBegin - // SPDX-FileCopyrightText: 2020 Stalwart Labs LLC - // SPDX-License-Identifier: LicenseRef-SEL - - // Limit to tenant domains - #[cfg(feature = "enterprise")] - if self.core.is_enterprise_edition() - && let Some(tenant) = access_token.tenant - { - tenant_domains = self - .core - .storage - .data - .list_principals(None, tenant.id.into(), &[Type::Domain], false, 0, 0) - .await - .map(|principals| { - principals - .items - .into_iter() - .map(|p| p.name) - .collect::>() - }) - .caused_by(trc::location!())? - .into(); - } - - // SPDX-SnippetEnd - - match ( - path.get(1).copied().unwrap_or_default(), - path.get(2).copied().map(decode_path_element), - req.method(), - ) { - ("messages", None, &Method::GET) => { - // Validate the access token - access_token.assert_has_permission(Permission::MessageQueueList)?; - - let result = fetch_queued_messages(self, ¶ms, &tenant_domains).await?; - - let queue_status = self.inner.data.queue_status.load(Ordering::Relaxed); - - Ok(if !result.values.is_empty() { - JsonResponse::new(json!({ - "data":{ - "items": result.values, - "total": result.total, - "status": queue_status, - }, - })) - } else { - JsonResponse::new(json!({ - "data": { - "items": result.ids, - "total": result.total, - "status": queue_status, - }, - })) - } - .into_http_response()) - } - ("messages", Some(queue_id), &Method::GET) => { - // Validate the access token - access_token.assert_has_permission(Permission::MessageQueueGet)?; - - let queue_id = queue_id.parse().unwrap_or_default(); - if let Some(message_) = self.read_message_archive(queue_id).await? { - let message = message_.unarchive::()?; - if message.is_tenant_domain(&tenant_domains) { - return Ok(JsonResponse::new(json!({ - "data": Message::from_archive(queue_id, message), - })) - .into_http_response()); - } - } - Err(trc::ResourceEvent::NotFound.into_err()) - } - ("messages", None, &Method::PATCH) => { - // Validate the access token - access_token.assert_has_permission(Permission::MessageQueueUpdate)?; - - let time = params - .parse::("at") - .map(|t| t.into_inner()) - .unwrap_or_else(now); - let result = fetch_queued_messages(self, ¶ms, &tenant_domains).await?; - - let found = !result.ids.is_empty(); - if found { - let server = self.clone(); - tokio::spawn(async move { - for id in result.ids { - if let Some(mut message) = - server.read_message(id, QueueName::default()).await - { - let mut has_changes = false; - - for recipient in &mut message.message.recipients { - if matches!( - recipient.status, - Status::Scheduled | Status::TemporaryFailure(_) - ) { - recipient.retry.due = time; - if recipient - .expiration_time(message.message.created) - .is_some_and(|expires| expires > time) - { - recipient.expires = - QueueExpiry::Attempts(recipient.retry.inner + 10); - } - has_changes = true; - } - } - - if has_changes { - message.save_changes(&server, None).await; - } - } - } - - let _ = server.inner.ipc.queue_tx.send(QueueEvent::Refresh).await; - }); - } - - Ok(JsonResponse::new(json!({ - "data": found, - })) - .into_http_response()) - } - ("messages", Some(queue_id), &Method::PATCH) => { - // Validate the access token - access_token.assert_has_permission(Permission::MessageQueueUpdate)?; - - let time = params - .parse::("at") - .map(|t| t.into_inner()) - .unwrap_or_else(now); - let item = params.get("filter"); - - if let Some(mut message) = self - .read_message(queue_id.parse().unwrap_or_default(), QueueName::default()) - .await - .filter(|message| { - tenant_domains - .as_ref() - .is_none_or(|domains| message.has_domain(domains)) - }) - { - let mut found = false; - - for recipient in &mut message.message.recipients { - if matches!( - recipient.status, - Status::Scheduled | Status::TemporaryFailure(_) - ) && item - .as_ref() - .is_none_or(|item| recipient.address().contains(item)) - { - recipient.retry.due = time; - if recipient - .expiration_time(message.message.created) - .is_some_and(|expires| expires > time) - { - recipient.expires = - QueueExpiry::Attempts(recipient.retry.inner + 10); - } - found = true; - } - } - - if found { - message.save_changes(self, None).await; - let _ = self.inner.ipc.queue_tx.send(QueueEvent::Refresh).await; - } - - Ok(JsonResponse::new(json!({ - "data": found, - })) - .into_http_response()) - } else { - Err(trc::ResourceEvent::NotFound.into_err()) - } - } - ("messages", None, &Method::DELETE) => { - // Validate the access token - access_token.assert_has_permission(Permission::MessageQueueDelete)?; - - let result = fetch_queued_messages(self, ¶ms, &tenant_domains).await?; - - let found = !result.ids.is_empty(); - if found { - let server = self.clone(); - tokio::spawn(async move { - let is_active = server.inner.data.queue_status.load(Ordering::Relaxed); - - if is_active { - let _ = server - .inner - .ipc - .queue_tx - .send(QueueEvent::Paused(true)) - .await; - } - - for id in result.ids { - if let Some(message) = - server.read_message(id, QueueName::default()).await - { - message.remove(&server, None).await; - } - } - - if is_active { - let _ = server - .inner - .ipc - .queue_tx - .send(QueueEvent::Paused(false)) - .await; - } - }); - } - - Ok(JsonResponse::new(json!({ - "data": found, - })) - .into_http_response()) - } - ("messages", Some(queue_id), &Method::DELETE) => { - // Validate the access token - access_token.assert_has_permission(Permission::MessageQueueDelete)?; - - if let Some(mut message) = self - .read_message(queue_id.parse().unwrap_or_default(), QueueName::default()) - .await - .filter(|message| { - tenant_domains - .as_ref() - .is_none_or(|domains| message.has_domain(domains)) - }) - { - let mut found = false; - if let Some(item) = params.get("filter") { - // Cancel delivery for all recipients that match - for rcpt in &mut message.message.recipients { - if rcpt.address().contains(item) { - rcpt.status = Status::PermanentFailure(ErrorDetails { - entity: "localhost".into(), - details: queue::Error::Io("Delivery canceled.".into()), - }); - found = true; - } - } - if found { - // Delete message if there are no pending deliveries - if message.message.recipients.iter().any(|recipient| { - matches!( - recipient.status, - Status::TemporaryFailure(_) | Status::Scheduled - ) - }) { - message.save_changes(self, None).await; - } else { - message.remove(self, None).await; - } - } - } else { - message.remove(self, None).await; - found = true; - } - - Ok(JsonResponse::new(json!({ - "data": found, - })) - .into_http_response()) - } else { - Err(trc::ResourceEvent::NotFound.into_err()) - } - } - ("reports", None, &Method::GET) => { - // Validate the access token - access_token.assert_has_permission(Permission::OutgoingReportList)?; - - let result = fetch_queued_reports(self, ¶ms, &tenant_domains).await?; - - Ok(JsonResponse::new(json!({ - "data": { - "items": result.ids.into_iter().map(|id| id.queue_id()).collect::>(), - "total": result.total, - }, - })) - .into_http_response()) - } - ("reports", Some(report_id), &Method::GET) => { - // Validate the access token - access_token.assert_has_permission(Permission::OutgoingReportGet)?; - - let mut result = None; - if let Some(report_id) = parse_queued_report_id(report_id.as_ref()) { - match report_id { - QueueClass::DmarcReportHeader(event) - if tenant_domains.as_ref().is_none_or(|domains| { - domains.iter().any(|dd| dd == &event.domain) - }) => - { - let mut rua = Vec::new(); - if let Some(report) = self - .generate_dmarc_aggregate_report(&event, &mut rua, None, 0) - .await? - { - result = Report::dmarc(event, report, rua).into(); - } - } - QueueClass::TlsReportHeader(event) - if tenant_domains.as_ref().is_none_or(|domains| { - domains.iter().any(|dd| dd == &event.domain) - }) => - { - let mut rua = Vec::new(); - if let Some(report) = self - .generate_tls_aggregate_report( - std::slice::from_ref(&event), - &mut rua, - None, - 0, - ) - .await? - { - result = Report::tls(event, report, rua).into(); - } - } - _ => (), - } - } - - if let Some(result) = result { - Ok(JsonResponse::new(json!({ - "data": result, - })) - .into_http_response()) - } else { - Err(trc::ResourceEvent::NotFound.into_err()) - } - } - ("reports", None, &Method::DELETE) => { - // Validate the access token - access_token.assert_has_permission(Permission::OutgoingReportDelete)?; - - let result = fetch_queued_reports(self, ¶ms, &tenant_domains).await?; - let found = !result.ids.is_empty(); - if found { - let server = self.clone(); - tokio::spawn(async move { - for id in result.ids { - match id { - QueueClass::DmarcReportHeader(event) => { - server.delete_dmarc_report(event).await; - } - QueueClass::TlsReportHeader(event) => { - server.delete_tls_report(vec![event]).await; - } - _ => (), - } - } - }); - } - - Ok(JsonResponse::new(json!({ - "data": found, - })) - .into_http_response()) - } - ("reports", Some(report_id), &Method::DELETE) => { - // Validate the access token - access_token.assert_has_permission(Permission::OutgoingReportDelete)?; - - if let Some(report_id) = parse_queued_report_id(report_id.as_ref()) { - let result = match report_id { - QueueClass::DmarcReportHeader(event) - if tenant_domains.as_ref().is_none_or(|domains| { - domains.iter().any(|dd| dd == &event.domain) - }) => - { - self.delete_dmarc_report(event).await; - true - } - QueueClass::TlsReportHeader(event) - if tenant_domains.as_ref().is_none_or(|domains| { - domains.iter().any(|dd| dd == &event.domain) - }) => - { - self.delete_tls_report(vec![event]).await; - true - } - _ => false, - }; - - Ok(JsonResponse::new(json!({ - "data": result, - })) - .into_http_response()) - } else { - Err(trc::ResourceEvent::NotFound.into_err()) - } - } - ("status", None, &Method::GET) => { - // Validate the access token - access_token.assert_has_permission(Permission::MessageQueueGet)?; - - Ok(JsonResponse::new(json!({ - "data": self.inner.data.queue_status.load(Ordering::Relaxed), - })) - .into_http_response()) - } - ("status", Some(action), &Method::PATCH) => { - // Validate the access token - access_token.assert_has_permission(Permission::MessageQueueUpdate)?; - - let prev_status = self.inner.data.queue_status.load(Ordering::Relaxed); - - let _ = self - .inner - .ipc - .queue_tx - .send(QueueEvent::Paused(action == "stop")) - .await; - - Ok(JsonResponse::new(json!({ - "data": prev_status, - })) - .into_http_response()) - } - _ => Err(trc::ResourceEvent::NotFound.into_err()), - } - } -} - -impl Message { - fn from_archive(id: u64, message: &ArchivedMessage) -> Self { - let now = now(); - - Message { - id, - return_path: message.return_path.to_string(), - created: DateTime::from_timestamp(u64::from(message.created) as i64), - size: message.size.into(), - priority: message.priority.into(), - env_id: message.env_id.as_ref().map(|id| id.to_string()), - recipients: message - .recipients - .iter() - .map(|rcpt| Recipient { - address: rcpt.address().to_string(), - queue: rcpt.queue.to_string(), - status: match &rcpt.status { - ArchivedStatus::Scheduled => Status::Scheduled, - ArchivedStatus::Completed(status) => { - Status::Completed(status.response.to_string()) - } - ArchivedStatus::TemporaryFailure(status) => { - Status::TemporaryFailure(status.to_string()) - } - ArchivedStatus::PermanentFailure(status) => { - Status::PermanentFailure(status.to_string()) - } - }, - retry_num: rcpt.retry.inner.into(), - next_retry: Some(DateTime::from_timestamp(u64::from(rcpt.retry.due) as i64)), - next_notify: if rcpt.notify.due > now { - DateTime::from_timestamp(u64::from(rcpt.notify.due) as i64).into() - } else { - None - }, - expires: if let ArchivedQueueExpiry::Ttl(time) = &rcpt.expires { - DateTime::from_timestamp((u64::from(*time) + message.created) as i64).into() - } else { - None - }, - orcpt: rcpt.orcpt.as_ref().map(|orcpt| orcpt.to_string()), - }) - .collect(), - - blob_hash: URL_SAFE_NO_PAD.encode::<&[u8]>(message.blob_hash.0.as_slice()), - } - } -} - -struct QueuedMessages { - ids: Vec, - values: Vec, - total: usize, -} - -async fn fetch_queued_messages( - server: &Server, - params: &UrlParams<'_>, - tenant_domains: &Option>, -) -> trc::Result { - let queue = params.get("queue").and_then(QueueName::new); - let text = params.get("text"); - let from = params.get("from"); - let to = params.get("to"); - let before = params - .parse::("before") - .map(|t| t.into_inner()); - let after = params - .parse::("after") - .map(|t| t.into_inner()); - let page = params.parse::("page").unwrap_or_default(); - let limit = params.parse::("limit").unwrap_or_default(); - let values = params.has_key("values"); - - let range_start = params.parse::("range-start").unwrap_or_default(); - let range_end = params.parse::("range-end").unwrap_or(u64::MAX); - let max_total = params.parse::("max-total").unwrap_or_default(); - - let mut result = QueuedMessages { - ids: Vec::new(), - values: Vec::new(), - total: 0, - }; - let from_key = ValueKey::from(ValueClass::Queue(QueueClass::Message(range_start))); - let to_key = ValueKey::from(ValueClass::Queue(QueueClass::Message(range_end))); - let has_filters = text.is_some() - || from.is_some() - || to.is_some() - || before.is_some() - || after.is_some() - || queue.is_some(); - let mut offset = page.saturating_sub(1) * limit; - let mut total_returned = 0; - - server - .core - .storage - .data - .iterate( - IterateParams::new(from_key, to_key).ascending(), - |key, value| { - let message_ = as Deserialize>::deserialize(value) - .add_context(|ctx| ctx.ctx(trc::Key::Key, key))?; - let message = message_ - .unarchive::() - .add_context(|ctx| ctx.ctx(trc::Key::Key, key))?; - let matches = tenant_domains - .as_ref() - .is_none_or(|domains| message.has_domain(domains)) - && (!has_filters - || (text - .as_ref() - .map(|text| { - message.return_path.contains(text) - || message - .recipients - .iter() - .any(|r| r.address().contains(text)) - }) - .unwrap_or_else(|| { - from.as_ref() - .is_none_or(|from| message.return_path.contains(from)) - && to.as_ref().is_none_or(|to| { - message.recipients.iter().any(|r| r.address().contains(to)) - }) - }) - && before.as_ref().is_none_or(|before| { - message - .next_delivery_event(queue) - .is_some_and(|next| next < *before) - }) - && after.as_ref().is_none_or(|after| { - message - .next_delivery_event(queue) - .is_some_and(|next| next > *after) - }) - && queue - .as_ref() - .is_none_or(|q| message.recipients.iter().any(|r| &r.queue == q)))); - - if matches { - if offset == 0 { - if limit == 0 || total_returned < limit { - let queue_id = key.deserialize_be_u64(0)?; - if values { - result.values.push(Message::from_archive(queue_id, message)); - } else { - result.ids.push(queue_id); - } - total_returned += 1; - } - } else { - offset -= 1; - } - - result.total += 1; - } - - Ok(max_total == 0 || result.total < max_total) - }, - ) - .await - .caused_by(trc::location!()) - .map(|_| result) -} - -struct QueuedReports { - ids: Vec, - total: usize, -} - -async fn fetch_queued_reports( - server: &Server, - params: &UrlParams<'_>, - tenant_domains: &Option>, -) -> trc::Result { - let domain = params.get("domain").map(|d| d.to_lowercase()); - let type_ = params.get("type").and_then(|t| match t { - "dmarc" => 0u8.into(), - "tls" => 1u8.into(), - _ => None, - }); - let page: usize = params.parse("page").unwrap_or_default(); - let limit: usize = params.parse("limit").unwrap_or_default(); - - let range_start = params.parse::("range-start").unwrap_or_default(); - let range_end = params.parse::("range-end").unwrap_or(u64::MAX); - let max_total = params.parse::("max-total").unwrap_or_default(); - - let mut result = QueuedReports { - ids: Vec::new(), - total: 0, - }; - let from_key = ValueKey::from(ValueClass::Queue(QueueClass::DmarcReportHeader( - ReportEvent { - due: range_start, - policy_hash: 0, - seq_id: 0, - domain: String::new(), - }, - ))); - let to_key = ValueKey::from(ValueClass::Queue(QueueClass::TlsReportHeader( - ReportEvent { - due: range_end, - policy_hash: 0, - seq_id: 0, - domain: String::new(), - }, - ))); - let mut offset = page.saturating_sub(1) * limit; - let mut total_returned = 0; - - server - .core - .storage - .data - .iterate( - IterateParams::new(from_key, to_key).ascending().no_values(), - |key, _| { - if type_.is_none_or(|t| t == *key.last().unwrap()) { - let event = ReportEvent::deserialize(key)?; - if tenant_domains - .as_ref() - .is_none_or(|domains| domains.iter().any(|dd| dd == &event.domain)) - && event.seq_id != 0 - && domain.as_ref().is_none_or(|d| event.domain.contains(d)) - { - if offset == 0 { - if limit == 0 || total_returned < limit { - result.ids.push(if *key.last().unwrap() == 0 { - QueueClass::DmarcReportHeader(event) - } else { - QueueClass::TlsReportHeader(event) - }); - total_returned += 1; - } - } else { - offset -= 1; - } - - result.total += 1; - } - } - - Ok(max_total == 0 || result.total < max_total) - }, - ) - .await - .caused_by(trc::location!()) - .map(|_| result) -} - -impl Report { - fn dmarc(event: ReportEvent, report: report::Report, rua: Vec) -> Self { - Self::Dmarc { - domain: event.domain.clone(), - range_from: DateTime::from_timestamp(event.seq_id as i64), - range_to: DateTime::from_timestamp(event.due as i64), - id: QueueClass::DmarcReportHeader(event).queue_id(), - report, - rua, - } - } - - fn tls(event: ReportEvent, report: TlsReport, rua: Vec) -> Self { - Self::Tls { - domain: event.domain.clone(), - range_from: DateTime::from_timestamp(event.seq_id as i64), - range_to: DateTime::from_timestamp(event.due as i64), - id: QueueClass::TlsReportHeader(event).queue_id(), - report, - rua, - } - } -} - -trait GenerateQueueId { - fn queue_id(&self) -> String; -} - -impl GenerateQueueId for QueueClass { - fn queue_id(&self) -> String { - match self { - QueueClass::DmarcReportHeader(h) => { - format!("d!{}!{}!{}!{}", h.domain, h.policy_hash, h.seq_id, h.due) - } - QueueClass::TlsReportHeader(h) => { - format!("t!{}!{}!{}!{}", h.domain, h.policy_hash, h.seq_id, h.due) - } - _ => unreachable!(), - } - } -} - -fn parse_queued_report_id(id: &str) -> Option { - let mut parts = id.split('!'); - let type_ = parts.next()?; - let event = ReportEvent { - domain: parts.next()?.to_string(), - policy_hash: parts.next().and_then(|p| p.parse::().ok())?, - seq_id: parts.next().and_then(|p| p.parse::().ok())?, - due: parts.next().and_then(|p| p.parse::().ok())?, - }; - match type_ { - "d" => Some(QueueClass::DmarcReportHeader(event)), - "t" => Some(QueueClass::TlsReportHeader(event)), - _ => None, - } -} - -fn serialize_maybe_datetime(value: &Option, serializer: S) -> Result -where - S: Serializer, -{ - match value { - Some(value) => serializer.serialize_some(&value.to_rfc3339()), - None => serializer.serialize_none(), - } -} - -fn deserialize_maybe_datetime<'de, D>(deserializer: D) -> Result, D::Error> -where - D: Deserializer<'de>, -{ - if let Some(value) = as serde::Deserialize>::deserialize(deserializer)? { - if let Some(value) = DateTime::parse_rfc3339(value) { - Ok(Some(value)) - } else { - Err(serde::de::Error::custom( - "Failed to parse RFC3339 timestamp", - )) - } - } else { - Ok(None) - } -} - -fn serialize_datetime(value: &DateTime, serializer: S) -> Result -where - S: Serializer, -{ - serializer.serialize_str(&value.to_rfc3339()) -} - -fn deserialize_datetime<'de, D>(deserializer: D) -> Result -where - D: Deserializer<'de>, -{ - use serde::Deserialize; - - if let Some(value) = DateTime::parse_rfc3339(<&str>::deserialize(deserializer)?) { - Ok(value) - } else { - Err(serde::de::Error::custom( - "Failed to parse RFC3339 timestamp", - )) - } -} - -fn is_zero(num: &i16) -> bool { - *num == 0 -} - -trait IsTenantDomain { - fn is_tenant_domain(&self, tenant_domains: &Option>) -> bool; -} -impl IsTenantDomain for ArchivedMessage { - fn is_tenant_domain(&self, tenant_domains: &Option>) -> bool { - tenant_domains - .as_ref() - .is_none_or(|domains| self.has_domain(domains)) - } -} diff --git a/crates/http/src/management/reload.rs b/crates/http/src/management/reload.rs deleted file mode 100644 index a0c64bc3..00000000 --- a/crates/http/src/management/reload.rs +++ /dev/null @@ -1,167 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL - */ - -use common::{ - Server, - auth::AccessToken, - ipc::{BroadcastEvent, HousekeeperEvent}, -}; -use registry::schema::enums::Permission; -use hyper::Method; -use serde_json::json; -use std::future::Future; -use utils::url_params::UrlParams; - -use http_proto::*; - -pub trait ManageReload: Sync + Send { - fn handle_manage_reload( - &self, - req: &HttpRequest, - path: Vec<&str>, - access_token: &AccessToken, - ) -> impl Future> + Send; - - fn handle_manage_update( - &self, - req: &HttpRequest, - path: Vec<&str>, - access_token: &AccessToken, - ) -> impl Future> + Send; -} - -impl ManageReload for Server { - async fn handle_manage_reload( - &self, - req: &HttpRequest, - path: Vec<&str>, - access_token: &AccessToken, - ) -> trc::Result { - // Validate the access token - access_token.assert_has_permission(Permission::SettingsReload)?; - - match (path.get(1).copied(), req.method()) { - (Some("lookup"), &Method::GET) => { - let result = self.reload_lookups().await?; - // Update core - if let Some(core) = result.new_core { - self.inner.shared_core.store(core.into()); - } - - Ok(JsonResponse::new(json!({ - "data": result.config, - })) - .into_http_response()) - } - (Some("certificate"), &Method::GET) => Ok(JsonResponse::new(json!({ - "data": self.reload_certificates().await?.config, - })) - .into_http_response()), - (Some("server.blocked-ip"), &Method::GET) => { - let result = self.reload_blocked_ips().await?; - - self.cluster_broadcast(BroadcastEvent::ReloadBlockedIps) - .await; - - Ok(JsonResponse::new(json!({ - "data": result.config, - })) - .into_http_response()) - } - (_, &Method::GET) => { - 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.inner.shared_core.store(core.into()); - - self.cluster_broadcast(BroadcastEvent::ReloadSettings).await; - } - - if let Some(tracers) = result.tracers { - // Update tracers - - // SPDX-SnippetBegin - // SPDX-FileCopyrightText: 2020 Stalwart Labs LLC - // SPDX-License-Identifier: LicenseRef-SEL - #[cfg(feature = "enterprise")] - tracers.update(self.inner.shared_core.load().is_enterprise_edition()); - // SPDX-SnippetEnd - #[cfg(not(feature = "enterprise"))] - tracers.update(false); - } - - // Reload settings - self.inner - .ipc - .housekeeper_tx - .send(HousekeeperEvent::ReloadSettings) - .await - .map_err(|err| { - trc::EventType::Server(trc::ServerEvent::ThreadError) - .reason(err) - .details(concat!( - "Failed to send settings reload ", - "event to housekeeper" - )) - .caused_by(trc::location!()) - })?; - } - - Ok(JsonResponse::new(json!({ - "data": result.config, - })) - .into_http_response()) - } - _ => Err(trc::ResourceEvent::NotFound.into_err()), - } - } - - async fn handle_manage_update( - &self, - req: &HttpRequest, - path: Vec<&str>, - access_token: &AccessToken, - ) -> trc::Result { - match (path.get(1).copied(), req.method()) { - (Some("spam-filter"), &Method::GET) => { - // Validate the access token - access_token.assert_has_permission(Permission::SpamFilterUpdate)?; - let params = UrlParams::new(req.uri().query()); - - let overwrite = params.has_key("overwrite"); - let force = params.has_key("force"); - - Ok(JsonResponse::new(json!({ - "data": self - .core - .storage - .config - .update_spam_rules(force, overwrite) - .await? - .map(|v| v.to_string()), - })) - .into_http_response()) - } - (Some("webadmin"), &Method::GET) => { - // Validate the access token - access_token.assert_has_permission(Permission::WebadminUpdate)?; - - self.inner - .data - .webadmin - .update_and_unpack(&self.core) - .await?; - - Ok(JsonResponse::new(json!({ - "data": (), - })) - .into_http_response()) - } - _ => Err(trc::ResourceEvent::NotFound.into_err()), - } - } -} diff --git a/crates/http/src/management/report.rs b/crates/http/src/management/report.rs deleted file mode 100644 index cdaeeb2e..00000000 --- a/crates/http/src/management/report.rs +++ /dev/null @@ -1,596 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL - */ - -use common::{Server, auth::AccessToken}; -use directory::{Permission, Type, backend::internal::manage::ManageDirectory}; -use http_proto::{request::decode_path_element, *}; -use hyper::Method; -use mail_auth::report::{ - Feedback, - tlsrpt::{FailureDetails, Policy, TlsReport}, -}; -use serde_json::json; -use smtp::reporting::analysis::IncomingReport; -use std::future::Future; -use store::{ - Deserialize, IterateParams, Key, U64_LEN, ValueKey, - write::{ - AlignedBytes, Archive, BatchBuilder, ReportClass, ValueClass, key::DeserializeBigEndian, - }, -}; -use trc::AddContext; -use utils::url_params::UrlParams; - -enum ReportType { - Dmarc, - Tls, - Arf, -} - -pub trait ManageReports: Sync + Send { - fn handle_manage_reports( - &self, - req: &HttpRequest, - path: Vec<&str>, - access_token: &AccessToken, - ) -> impl Future> + Send; -} - -impl ManageReports for Server { - async fn handle_manage_reports( - &self, - req: &HttpRequest, - path: Vec<&str>, - access_token: &AccessToken, - ) -> trc::Result { - let mut tenant_domains: Option> = None; - // SPDX-SnippetBegin - // SPDX-FileCopyrightText: 2020 Stalwart Labs LLC - // SPDX-License-Identifier: LicenseRef-SEL - - // Limit to tenant domains - #[cfg(feature = "enterprise")] - if self.core.is_enterprise_edition() - && let Some(tenant) = access_token.tenant - { - tenant_domains = self - .core - .storage - .data - .list_principals(None, tenant.id.into(), &[Type::Domain], false, 0, 0) - .await - .map(|principals| { - principals - .items - .into_iter() - .map(|p| p.name) - .collect::>() - }) - .caused_by(trc::location!())? - .into(); - } - - // SPDX-SnippetEnd - - match ( - path.get(1).copied().unwrap_or_default(), - path.get(2).copied().map(decode_path_element), - req.method(), - ) { - (class @ ("dmarc" | "tls" | "arf"), None, &Method::GET) => { - // Validate the access token - access_token.assert_has_permission(Permission::IncomingReportList)?; - - let params = UrlParams::new(req.uri().query()); - - let IncomingReports { ids, total } = - fetch_incoming_reports(self, class, ¶ms, &tenant_domains).await?; - - Ok(JsonResponse::new(json!({ - "data": { - "items": ids.into_iter().map(|(id, expires)| { - format!("{id}_{expires}") - }).collect::>(), - "total": total, - }, - })) - .into_http_response()) - } - (class @ ("dmarc" | "tls" | "arf"), Some(report_id), &Method::GET) => { - // Validate the access token - access_token.assert_has_permission(Permission::IncomingReportGet)?; - - if let Some(report_id) = parse_incoming_report_id(class, report_id.as_ref()) { - match &report_id { - ReportClass::Tls { .. } => match fetch_report::>( - self, - ValueKey::from(ValueClass::Report(report_id)), - ) - .await? - { - Some(report) - if tenant_domains - .as_ref() - .is_none_or(|domains| report.has_domain(domains)) => - { - Ok(JsonResponse::new(json!({ - "data": report, - })) - .into_http_response()) - } - _ => Err(trc::ResourceEvent::NotFound.into_err()), - }, - ReportClass::Dmarc { .. } => { - match fetch_report::>( - self, - ValueKey::from(ValueClass::Report(report_id)), - ) - .await? - { - Some(report) - if tenant_domains - .as_ref() - .is_none_or(|domains| report.has_domain(domains)) => - { - Ok(JsonResponse::new(json!({ - "data": report, - })) - .into_http_response()) - } - _ => Err(trc::ResourceEvent::NotFound.into_err()), - } - } - ReportClass::Arf { .. } => match fetch_report::>( - self, - ValueKey::from(ValueClass::Report(report_id)), - ) - .await? - { - Some(report) - if tenant_domains - .as_ref() - .is_none_or(|domains| report.has_domain(domains)) => - { - Ok(JsonResponse::new(json!({ - "data": report, - })) - .into_http_response()) - } - _ => Err(trc::ResourceEvent::NotFound.into_err()), - }, - } - } else { - Err(trc::ResourceEvent::NotFound.into_err()) - } - } - (class @ ("dmarc" | "tls" | "arf"), None, &Method::DELETE) => { - // Validate the access token - access_token.assert_has_permission(Permission::IncomingReportDelete)?; - - let params = UrlParams::new(req.uri().query()); - - let IncomingReports { ids, .. } = - fetch_incoming_reports(self, class, ¶ms, &tenant_domains).await?; - - let found = !ids.is_empty(); - if found { - let class = match class { - "dmarc" => ReportClass::Dmarc { id: 0, expires: 0 }, - "tls" => ReportClass::Tls { id: 0, expires: 0 }, - "arf" => ReportClass::Arf { id: 0, expires: 0 }, - _ => unreachable!(), - }; - let server = self.clone(); - tokio::spawn(async move { - let mut batch = BatchBuilder::new(); - - for (id, expires) in ids { - let report_id = match &class { - ReportClass::Dmarc { .. } => ReportClass::Dmarc { id, expires }, - ReportClass::Tls { .. } => ReportClass::Tls { id, expires }, - ReportClass::Arf { .. } => ReportClass::Arf { id, expires }, - }; - - batch.clear(ValueClass::Report(report_id)); - - if batch.is_large_batch() { - if let Err(err) = - server.core.storage.data.write(batch.build_all()).await - { - trc::error!(err.caused_by(trc::location!())); - } - batch = BatchBuilder::new(); - } - } - - if !batch.is_empty() - && let Err(err) = - server.core.storage.data.write(batch.build_all()).await - { - trc::error!(err.caused_by(trc::location!())); - } - }); - } - - Ok(JsonResponse::new(json!({ - "data": found, - })) - .into_http_response()) - } - (class @ ("dmarc" | "tls" | "arf"), Some(report_id), &Method::DELETE) => { - // Validate the access token - access_token.assert_has_permission(Permission::IncomingReportDelete)?; - - if let Some(report_id) = parse_incoming_report_id(class, report_id.as_ref()) { - if let Some(domains) = &tenant_domains { - let is_tenant_report = match &report_id { - ReportClass::Tls { .. } => fetch_report::>( - self, - ValueKey::from(ValueClass::Report(report_id.clone())), - ) - .await? - .is_none_or(|report| report.has_domain(domains)), - ReportClass::Dmarc { .. } => { - fetch_report::>( - self, - ValueKey::from(ValueClass::Report(report_id.clone())), - ) - .await? - .is_none_or(|report| report.has_domain(domains)) - } - - ReportClass::Arf { .. } => fetch_report::>( - self, - ValueKey::from(ValueClass::Report(report_id.clone())), - ) - .await? - .is_none_or(|report| report.has_domain(domains)), - }; - - if !is_tenant_report { - return Err(trc::ResourceEvent::NotFound.into_err()); - } - } - - let mut batch = BatchBuilder::new(); - batch.clear(ValueClass::Report(report_id)); - self.core.storage.data.write(batch.build_all()).await?; - - Ok(JsonResponse::new(json!({ - "data": true, - })) - .into_http_response()) - } else { - Err(trc::ResourceEvent::NotFound.into_err()) - } - } - _ => Err(trc::ResourceEvent::NotFound.into_err()), - } - } -} - -async fn fetch_report(server: &Server, key: impl Key) -> trc::Result> -where - T: rkyv::Archive - + for<'a> rkyv::Serialize< - rkyv::api::high::HighSerializer< - rkyv::util::AlignedVec, - rkyv::ser::allocator::ArenaHandle<'a>, - rkyv::rancor::Error, - >, - >, - T::Archived: for<'a> rkyv::bytecheck::CheckBytes> - + rkyv::Deserialize>, -{ - if let Some(tls) = server - .store() - .get_value::>(key) - .await? - { - tls.deserialize::().map(Some) - } else { - Ok(None) - } -} - -struct IncomingReports { - ids: Vec<(u64, u64)>, - total: usize, -} - -async fn fetch_incoming_reports( - server: &Server, - class: &str, - params: &UrlParams<'_>, - tenant_domains: &Option>, -) -> trc::Result { - let filter = params.get("text"); - let page: usize = params.parse::("page").unwrap_or_default(); - let limit: usize = params.parse::("limit").unwrap_or_default(); - - let range_start = params.parse::("range-start").unwrap_or_default(); - let range_end = params.parse::("range-end").unwrap_or(u64::MAX); - let max_total = params.parse::("max-total").unwrap_or_default(); - - let (from_key, to_key, typ) = match class { - "dmarc" => ( - ValueKey::from(ValueClass::Report(ReportClass::Dmarc { - id: range_start, - expires: 0, - })), - ValueKey::from(ValueClass::Report(ReportClass::Dmarc { - id: range_end, - expires: u64::MAX, - })), - ReportType::Dmarc, - ), - "tls" => ( - ValueKey::from(ValueClass::Report(ReportClass::Tls { - id: range_start, - expires: 0, - })), - ValueKey::from(ValueClass::Report(ReportClass::Tls { - id: range_end, - expires: u64::MAX, - })), - ReportType::Tls, - ), - "arf" => ( - ValueKey::from(ValueClass::Report(ReportClass::Arf { - id: range_start, - expires: 0, - })), - ValueKey::from(ValueClass::Report(ReportClass::Arf { - id: range_end, - expires: u64::MAX, - })), - ReportType::Arf, - ), - _ => unreachable!(), - }; - - let mut results = IncomingReports { - ids: Vec::new(), - total: 0, - }; - let mut offset = page.saturating_sub(1) * limit; - let mut last_id = 0; - let has_filters = filter.is_some() || tenant_domains.is_some(); - - server - .core - .storage - .data - .iterate( - IterateParams::new(from_key, to_key) - .set_values(has_filters) - .descending(), - |key, value| { - // Skip chunked records - let id = key.deserialize_be_u64(U64_LEN + 1)?; - if id == last_id { - return Ok(true); - } - last_id = id; - - // TODO: Support filtering chunked records (over 10MB) on FDB - let matches = if has_filters { - let archive = as Deserialize>::deserialize(value)?; - match typ { - ReportType::Dmarc => { - let report = archive - .deserialize::>() - .caused_by(trc::location!())?; - - filter.is_none_or(|f| report.contains(f)) - && tenant_domains - .as_ref() - .is_none_or(|domains| report.has_domain(domains)) - } - ReportType::Tls => { - let report = archive - .deserialize::>() - .caused_by(trc::location!())?; - - filter.is_none_or(|f| report.contains(f)) - && tenant_domains - .as_ref() - .is_none_or(|domains| report.has_domain(domains)) - } - ReportType::Arf => { - let report = archive - .deserialize::>() - .caused_by(trc::location!())?; - - filter.is_none_or(|f| report.contains(f)) - && tenant_domains - .as_ref() - .is_none_or(|domains| report.has_domain(domains)) - } - } - } else { - true - }; - - if matches { - if offset == 0 { - if limit == 0 || results.ids.len() < limit { - results.ids.push((id, key.deserialize_be_u64(1)?)); - } - } else { - offset -= 1; - } - - results.total += 1; - } - - Ok(max_total == 0 || results.total < max_total) - }, - ) - .await - .caused_by(trc::location!()) - .map(|_| results) -} - -fn parse_incoming_report_id(class: &str, id: &str) -> Option { - let mut parts = id.split('_'); - let id = parts.next()?.parse().ok()?; - let expires = parts.next()?.parse().ok()?; - match class { - "dmarc" => Some(ReportClass::Dmarc { id, expires }), - "tls" => Some(ReportClass::Tls { id, expires }), - "arf" => Some(ReportClass::Arf { id, expires }), - _ => None, - } -} - -impl From<&str> for ReportType { - fn from(s: &str) -> Self { - match s { - "dmarc" => Self::Dmarc, - "tls" => Self::Tls, - "arf" => Self::Arf, - _ => unreachable!(), - } - } -} - -trait Contains { - fn contains(&self, text: &str) -> bool; -} - -impl Contains for mail_auth::report::Report { - fn contains(&self, text: &str) -> bool { - self.domain().contains(text) - || self.org_name().to_lowercase().contains(text) - || self.report_id().contains(text) - || self - .extra_contact_info() - .is_some_and(|c| c.to_lowercase().contains(text)) - || self.records().iter().any(|record| record.contains(text)) - } -} - -impl Contains for mail_auth::report::Record { - fn contains(&self, filter: &str) -> bool { - self.envelope_from().contains(filter) - || self.header_from().contains(filter) - || self.envelope_to().is_some_and(|to| to.contains(filter)) - || self.dkim_auth_result().iter().any(|dkim| { - dkim.domain().contains(filter) - || dkim.selector().contains(filter) - || dkim - .human_result() - .as_ref() - .is_some_and(|r| r.contains(filter)) - }) - || self.spf_auth_result().iter().any(|spf| { - spf.domain().contains(filter) - || spf.human_result().is_some_and(|r| r.contains(filter)) - }) - || self - .source_ip() - .is_some_and(|ip| ip.to_string().contains(filter)) - } -} - -impl Contains for TlsReport { - fn contains(&self, text: &str) -> bool { - self.organization_name - .as_ref() - .is_some_and(|o| o.to_lowercase().contains(text)) - || self - .contact_info - .as_ref() - .is_some_and(|c| c.to_lowercase().contains(text)) - || self.report_id.contains(text) - || self.policies.iter().any(|p| p.contains(text)) - } -} - -impl Contains for Policy { - fn contains(&self, filter: &str) -> bool { - self.policy.policy_domain.contains(filter) - || self - .policy - .policy_string - .iter() - .any(|s| s.to_lowercase().contains(filter)) - || self - .policy - .mx_host - .iter() - .any(|s| s.to_lowercase().contains(filter)) - || self.failure_details.iter().any(|f| f.contains(filter)) - } -} - -impl Contains for FailureDetails { - fn contains(&self, filter: &str) -> bool { - self.sending_mta_ip - .is_some_and(|s| s.to_string().contains(filter)) - || self - .receiving_ip - .is_some_and(|s| s.to_string().contains(filter)) - || self - .receiving_mx_hostname - .as_ref() - .is_some_and(|s| s.contains(filter)) - || self - .receiving_mx_helo - .as_ref() - .is_some_and(|s| s.contains(filter)) - || self - .additional_information - .as_ref() - .is_some_and(|s| s.contains(filter)) - || self - .failure_reason_code - .as_ref() - .is_some_and(|s| s.contains(filter)) - } -} - -impl Contains for Feedback<'_> { - fn contains(&self, text: &str) -> bool { - // Check if any of the string fields contain the filter - self.authentication_results() - .iter() - .any(|s| s.contains(text)) - || self - .original_envelope_id() - .is_some_and(|s| s.contains(text)) - || self.original_mail_from().is_some_and(|s| s.contains(text)) - || self.original_rcpt_to().is_some_and(|s| s.contains(text)) - || self.reported_domain().iter().any(|s| s.contains(text)) - || self.reported_uri().iter().any(|s| s.contains(text)) - || self.reporting_mta().is_some_and(|s| s.contains(text)) - || self.user_agent().is_some_and(|s| s.contains(text)) - || self.dkim_adsp_dns().is_some_and(|s| s.contains(text)) - || self - .dkim_canonicalized_body() - .is_some_and(|s| s.contains(text)) - || self - .dkim_canonicalized_header() - .is_some_and(|s| s.contains(text)) - || self.dkim_domain().is_some_and(|s| s.contains(text)) - || self.dkim_identity().is_some_and(|s| s.contains(text)) - || self.dkim_selector().is_some_and(|s| s.contains(text)) - || self.dkim_selector_dns().is_some_and(|s| s.contains(text)) - || self.spf_dns().is_some_and(|s| s.contains(text)) - || self.message().is_some_and(|s| s.contains(text)) - || self.headers().is_some_and(|s| s.contains(text)) - } -} - -impl Contains for IncomingReport { - fn contains(&self, text: &str) -> bool { - self.from.to_lowercase().contains(text) - || self.to.iter().any(|to| to.to_lowercase().contains(text)) - || self.subject.to_lowercase().contains(text) - || self.report.contains(text) - } -} diff --git a/crates/http/src/management/settings.rs b/crates/http/src/management/settings.rs deleted file mode 100644 index 866b8e1c..00000000 --- a/crates/http/src/management/settings.rs +++ /dev/null @@ -1,355 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL - */ - -use common::{Server, auth::AccessToken}; -use registry::schema::enums::Permission; -use hyper::Method; -use serde_json::json; -use store::ahash::AHashMap; -use utils::{config::ConfigKey, map::vec_map::VecMap, url_params::UrlParams}; - -use http_proto::{request::decode_path_element, *}; -use std::future::Future; - -#[derive(Debug, serde::Serialize, serde::Deserialize)] -#[serde(tag = "type")] -#[serde(rename_all = "camelCase")] -pub enum UpdateSettings { - Delete { - keys: Vec, - }, - Clear { - prefix: String, - #[serde(default)] - filter: Option, - }, - Insert { - prefix: Option, - values: Vec<(String, String)>, - assert_empty: bool, - }, -} - -pub trait ManageSettings: Sync + Send { - fn handle_manage_settings( - &self, - req: &HttpRequest, - path: Vec<&str>, - body: Option>, - access_token: &AccessToken, - ) -> impl Future> + Send; -} - -impl ManageSettings for Server { - async fn handle_manage_settings( - &self, - req: &HttpRequest, - path: Vec<&str>, - body: Option>, - access_token: &AccessToken, - ) -> trc::Result { - match (path.get(1).copied(), req.method()) { - (Some("group"), &Method::GET) => { - // Validate the access token - access_token.assert_has_permission(Permission::SettingsList)?; - - // List settings - let params = UrlParams::new(req.uri().query()); - let prefix = params - .get("prefix") - .map(|p| { - if !p.ends_with('.') { - format!("{p}.") - } else { - p.to_string() - } - }) - .unwrap_or_default(); - let suffix = params - .get("suffix") - .map(|s| { - if !s.starts_with('.') { - format!(".{s}") - } else { - s.to_string() - } - }) - .unwrap_or_default(); - let field = params.get("field"); - let filter = params.get("filter").unwrap_or_default().to_lowercase(); - let limit: usize = params.parse("limit").unwrap_or(0); - let mut offset = - params.parse::("page").unwrap_or(0).saturating_sub(1) * limit; - let has_filter = !filter.is_empty(); - - let settings = self.core.storage.config.list(&prefix, true).await?; - if !suffix.is_empty() && !settings.is_empty() { - // Obtain record ids - let mut total = 0; - let mut ids = Vec::new(); - for key in settings.keys() { - if let Some(id) = key.strip_suffix(&suffix) - && !id.is_empty() - { - if !has_filter { - if offset == 0 { - if limit == 0 || ids.len() < limit { - ids.push(id); - } - } else { - offset -= 1; - } - total += 1; - } else { - ids.push(id); - } - } - } - - // Group settings by record id - let mut records = Vec::new(); - for id in ids { - let mut record = AHashMap::new(); - let prefix = format!("{id}."); - record.insert("_id".to_string(), id.to_string()); - for (k, v) in &settings { - if let Some(k) = k.strip_prefix(&prefix) { - if field.is_none_or(|field| field == k) { - record.insert(k.to_string(), v.to_string()); - } - } else if record.len() > 1 { - break; - } - } - - if has_filter { - if record - .iter() - .any(|(_, v)| v.to_lowercase().contains(&filter)) - { - if offset == 0 { - if limit == 0 || records.len() < limit { - records.push(record); - } - } else { - offset -= 1; - } - total += 1; - } - } else { - records.push(record); - } - } - - Ok(JsonResponse::new(json!({ - "data": { - "total": total, - "items": records, - }, - })) - .into_http_response()) - } else { - let mut total = 0; - let mut items = Vec::new(); - - for (k, v) in settings { - if filter.is_empty() - || k.to_lowercase().contains(&filter) - || v.to_lowercase().contains(&filter) - { - if offset == 0 { - if limit == 0 || items.len() < limit { - let k = - k.strip_prefix(&prefix).map(|k| k.to_string()).unwrap_or(k); - items.push(json!({ - "_id": k, - "_value": v, - })); - } - } else { - offset -= 1; - } - total += 1; - } - } - - Ok(JsonResponse::new(json!({ - "data": { - "total": total, - "items": items, - }, - })) - .into_http_response()) - } - } - (Some("list"), &Method::GET) => { - // Validate the access token - access_token.assert_has_permission(Permission::SettingsList)?; - - // List settings - let params = UrlParams::new(req.uri().query()); - let prefix = params - .get("prefix") - .map(|p| { - if !p.ends_with('.') { - format!("{p}.") - } else { - p.to_string() - } - }) - .unwrap_or_default(); - let limit: usize = params.parse("limit").unwrap_or(0); - let offset = params.parse::("page").unwrap_or(0).saturating_sub(1) * limit; - - let settings = self.core.storage.config.list(&prefix, true).await?; - let total = settings.len(); - let items = settings - .into_iter() - .skip(offset) - .take(if limit == 0 { total } else { limit }) - .collect::>(); - - Ok(JsonResponse::new(json!({ - "data": { - "total": total, - "items": items, - }, - })) - .into_http_response()) - } - (Some("keys"), &Method::GET) => { - // Validate the access token - access_token.assert_has_permission(Permission::SettingsList)?; - - // Obtain keys - let params = UrlParams::new(req.uri().query()); - let keys = params - .get("keys") - .map(|s| s.split(',').collect::>()) - .unwrap_or_default(); - let prefixes = params - .get("prefixes") - .map(|s| s.split(',').collect::>()) - .unwrap_or_default(); - let mut results = AHashMap::with_capacity(keys.len()); - - for key in keys { - if let Some(value) = self.core.storage.config.get(key).await? { - results.insert(key.to_string(), value); - } - } - for prefix in prefixes { - let prefix = if !prefix.ends_with('.') { - format!("{prefix}.") - } else { - prefix.to_string() - }; - results.extend(self.core.storage.config.list(&prefix, false).await?); - } - - Ok(JsonResponse::new(json!({ - "data": results, - })) - .into_http_response()) - } - (Some(prefix), &Method::DELETE) if !prefix.is_empty() => { - // Validate the access token - access_token.assert_has_permission(Permission::SettingsDelete)?; - - let prefix = decode_path_element(prefix); - - self.core.storage.config.clear(prefix.as_ref()).await?; - - Ok(JsonResponse::new(json!({ - "data": (), - })) - .into_http_response()) - } - (None, &Method::POST) => { - // Validate the access token - access_token.assert_has_permission(Permission::SettingsUpdate)?; - - let changes = serde_json::from_slice::>( - body.as_deref().unwrap_or_default(), - ) - .map_err(|err| { - trc::EventType::Resource(trc::ResourceEvent::BadParameters).from_json_error(err) - })?; - - for change in changes { - match change { - UpdateSettings::Delete { keys } => { - for key in keys { - self.core.storage.config.clear(key).await?; - } - } - UpdateSettings::Clear { prefix, filter } => { - if let Some(filter) = filter { - for (key, value) in - self.core.storage.config.list(&prefix, false).await? - { - if value.to_lowercase().contains(&filter) - || key.to_lowercase().contains(&filter) - { - self.core.storage.config.clear(key).await?; - } - } - } else { - self.core.storage.config.clear_prefix(&prefix).await?; - } - } - UpdateSettings::Insert { - prefix, - values, - assert_empty, - } => { - if assert_empty { - if let Some(prefix) = &prefix { - if !self - .core - .storage - .config - .list(&format!("{prefix}."), true) - .await? - .is_empty() - { - return Err(trc::ManageEvent::AssertFailed.into_err()); - } - } else if let Some((key, _)) = values.first() - && self.core.storage.config.get(key).await?.is_some() - { - return Err(trc::ManageEvent::AssertFailed.into_err()); - } - } - - self.core - .storage - .config - .set( - values.into_iter().map(|(key, value)| ConfigKey { - key: if let Some(prefix) = &prefix { - format!("{prefix}.{key}") - } else { - key - }, - value, - }), - true, - ) - .await?; - } - } - } - - Ok(JsonResponse::new(json!({ - "data": (), - })) - .into_http_response()) - } - _ => Err(trc::ResourceEvent::NotFound.into_err()), - } - } -} diff --git a/crates/http/src/management/spam.rs b/crates/http/src/management/spam.rs deleted file mode 100644 index c6f12279..00000000 --- a/crates/http/src/management/spam.rs +++ /dev/null @@ -1,372 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL - */ - -use common::{ - Server, - auth::AccessToken, - config::spamfilter::SpamFilterAction, - manager::{SPAM_CLASSIFIER_KEY, SPAM_TRAINER_KEY}, - psl, -}; -use directory::{ - Permission, - backend::internal::manage::{self, ManageDirectory}, -}; -use email::message::ingest::EmailIngest; -use http_proto::{request::decode_path_element, *}; -use hyper::Method; -use mail_auth::{ - AuthenticatedMessage, DmarcResult, dmarc::verify::DmarcParameters, spf::verify::SpfParameters, -}; -use mail_parser::MessageParser; -use serde::{Deserialize, Serialize}; -use serde_json::json; -use spam_filter::{ - SpamFilterInput, - analysis::{init::SpamFilterInit, score::SpamFilterAnalyzeScore}, - modules::classifier::SpamClassifier, -}; -use std::future::Future; -use std::net::IpAddr; -use store::{ahash::AHashMap, write::BatchBuilder}; - -pub trait ManageSpamHandler: Sync + Send { - fn handle_manage_spam( - &self, - req: &HttpRequest, - path: Vec<&str>, - body: Option>, - session: &HttpSessionData, - access_token: &AccessToken, - ) -> impl Future> + Send; -} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct SpamClassifyRequest { - pub message: String, - - // Session details - pub remote_ip: IpAddr, - #[serde(default)] - pub ehlo_domain: String, - #[serde(default)] - pub authenticated_as: Option, - - // TLS - #[serde(default)] - pub is_tls: bool, - - // Envelope - pub env_from: String, - pub env_from_flags: u64, - pub env_rcpt_to: Vec, -} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct SpamClassifyResponse { - pub score: f32, - pub tags: AHashMap>, - pub disposition: SpamFilterDisposition, -} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -#[serde(tag = "action")] -pub enum SpamFilterDisposition { - Allow { value: T }, - Discard, - Reject, -} - -impl ManageSpamHandler for Server { - async fn handle_manage_spam( - &self, - req: &HttpRequest, - path: Vec<&str>, - body: Option>, - session: &HttpSessionData, - access_token: &AccessToken, - ) -> trc::Result { - match (path.get(1).copied(), path.get(2).copied(), req.method()) { - (Some("upload"), Some(class @ ("ham" | "spam")), &Method::POST) => { - // Validate the access token - access_token.assert_has_permission(Permission::SpamFilterTrain)?; - - let message = - body.ok_or_else(|| manage::error("Failed to parse message.", None::))?; - let account_id = if let Some(account) = - path.get(3).copied().filter(|a| !a.is_empty()) - { - let principal = self - .store() - .get_principal_info(decode_path_element(account).as_ref()) - .await? - .ok_or_else(|| manage::not_found(account.to_string()))?; - if access_token.tenant.is_some() && principal.tenant != access_token.tenant_id() - { - return Err(manage::error( - "Account does not belong to this tenant.", - None::, - )); - } - - principal.id - } else if access_token.tenant.is_none() { - u32::MAX - } else { - return Err(manage::error( - "Account ID is required for tenants.", - None::, - )); - }; - - // Write sample - let (blob_hash, blob_hold) = - self.put_temporary_blob(account_id, &message, 60).await?; - let mut batch = BatchBuilder::new(); - batch.with_account_id(account_id).clear(blob_hold); - self.add_spam_sample( - &mut batch, - blob_hash, - class == "spam", - true, - session.session_id, - ); - self.store().write(batch.build_all()).await?; - - Ok(JsonResponse::new(json!({ - "data": (), - })) - .into_http_response()) - } - (Some("train"), request, &Method::GET) => { - // Validate the access token - access_token.assert_has_permission(Permission::SpamFilterTrain)?; - - let result = match request { - Some("start") | Some("reset") => { - if !self.inner.ipc.train_task_controller.is_running() { - let reset = matches!(request, Some("reset")); - let server = self.clone(); - tokio::spawn(async move { - if let Err(err) = server.spam_train(reset).await { - trc::error!(err.caused_by(trc::location!())); - } - }); - - true - } else { - false - } - } - Some("stop") => { - if self.inner.ipc.train_task_controller.is_running() { - self.inner.ipc.train_task_controller.stop(); - true - } else { - false - } - } - Some("delete") => { - for key in [SPAM_CLASSIFIER_KEY, SPAM_TRAINER_KEY] { - self.blob_store().delete_blob(key).await?; - } - true - } - Some("status") => self.inner.ipc.train_task_controller.is_running(), - _ => { - return Err(trc::ResourceEvent::NotFound.into_err()); - } - }; - - Ok(JsonResponse::new(json!({ - "data": result, - })) - .into_http_response()) - } - (Some("classify"), _, &Method::POST) => { - // Validate the access token - access_token.assert_has_permission(Permission::SpamFilterTest)?; - - // Parse request - let request = serde_json::from_slice::( - body.as_deref().unwrap_or_default(), - ) - .map_err(|err| { - trc::EventType::Resource(trc::ResourceEvent::BadParameters).from_json_error(err) - })?; - - // Built spam filter input - let message = MessageParser::new() - .parse(request.message.as_bytes()) - .filter(|m| m.root_part().headers().iter().any(|h| !h.name.is_other())) - .ok_or_else(|| manage::error("Failed to parse message.", None::))?; - - let remote_ip = request.remote_ip; - let ehlo_domain = request.ehlo_domain.to_lowercase(); - let mail_from = request.env_from.to_lowercase(); - let mail_from_domain = mail_from.rsplit_once('@').map(|(_, domain)| domain); - let local_host = &self.core.network.server_name; - - let spf_ehlo_result = - self.core - .smtp - .resolvers - .dns - .verify_spf(self.inner.cache.build_auth_parameters( - SpfParameters::verify_ehlo(remote_ip, &ehlo_domain, local_host), - )) - .await; - - let iprev_result = self - .core - .smtp - .resolvers - .dns - .verify_iprev(self.inner.cache.build_auth_parameters(remote_ip)) - .await; - - let spf_mail_from_result = if let Some(mail_from_domain) = mail_from_domain { - self.core - .smtp - .resolvers - .dns - .check_host(self.inner.cache.build_auth_parameters(SpfParameters::new( - remote_ip, - mail_from_domain, - &ehlo_domain, - local_host, - &mail_from, - ))) - .await - } else { - self.core - .smtp - .resolvers - .dns - .check_host(self.inner.cache.build_auth_parameters(SpfParameters::new( - remote_ip, - &ehlo_domain, - &ehlo_domain, - local_host, - &format!("postmaster@{ehlo_domain}"), - ))) - .await - }; - - let auth_message = AuthenticatedMessage::from_parsed(&message, true); - - let dkim_output = self - .core - .smtp - .resolvers - .dns - .verify_dkim(self.inner.cache.build_auth_parameters(&auth_message)) - .await; - - let arc_output = self - .core - .smtp - .resolvers - .dns - .verify_arc(self.inner.cache.build_auth_parameters(&auth_message)) - .await; - - let dmarc_output = self - .core - .smtp - .resolvers - .dns - .verify_dmarc(self.inner.cache.build_auth_parameters(DmarcParameters { - message: &auth_message, - dkim_output: &dkim_output, - rfc5321_mail_from_domain: mail_from_domain.unwrap_or(ehlo_domain.as_str()), - spf_output: &spf_mail_from_result, - domain_suffix_fn: |domain| psl::domain_str(domain).unwrap_or(domain), - })) - .await; - let dmarc_pass = matches!(dmarc_output.spf_result(), DmarcResult::Pass) - || matches!(dmarc_output.dkim_result(), DmarcResult::Pass); - let dmarc_result = if dmarc_pass { - DmarcResult::Pass - } else if dmarc_output.spf_result() != &DmarcResult::None { - dmarc_output.spf_result().clone() - } else if dmarc_output.dkim_result() != &DmarcResult::None { - dmarc_output.dkim_result().clone() - } else { - DmarcResult::None - }; - let dmarc_policy = dmarc_output.policy(); - - let asn_geo = self.lookup_asn_country(remote_ip).await; - - let input = SpamFilterInput { - message: &message, - span_id: session.session_id, - arc_result: Some(&arc_output), - spf_ehlo_result: Some(&spf_ehlo_result), - spf_mail_from_result: Some(&spf_mail_from_result), - dkim_result: dkim_output.as_slice(), - dmarc_result: Some(&dmarc_result), - dmarc_policy: Some(&dmarc_policy), - iprev_result: Some(&iprev_result), - remote_ip: request.remote_ip, - ehlo_domain: Some(ehlo_domain.as_str()), - authenticated_as: request.authenticated_as.as_deref(), - asn: asn_geo.asn.as_ref().map(|a| a.id), - country: asn_geo.country.as_ref().map(|c| c.as_str()), - is_tls: request.is_tls, - env_from: &request.env_from, - env_from_flags: request.env_from_flags, - env_rcpt_to: request.env_rcpt_to.iter().map(String::as_str).collect(), - is_test: true, - is_train: false, - }; - - // Classify - let mut ctx = self.spam_filter_init(input); - let result = self.spam_filter_classify(&mut ctx).await; - - // Build response - let mut response = SpamClassifyResponse { - score: ctx.result.score, - tags: AHashMap::with_capacity(ctx.result.tags.len()), - disposition: match result { - SpamFilterAction::Allow(value) => SpamFilterDisposition::Allow { - value: value.headers, - }, - SpamFilterAction::Discard => SpamFilterDisposition::Discard, - SpamFilterAction::Reject => SpamFilterDisposition::Reject, - SpamFilterAction::Disabled => SpamFilterDisposition::Allow { - value: String::new(), - }, - }, - }; - for tag in ctx.result.tags { - let disposition = match self.core.spam.lists.scores.get(&tag) { - Some(SpamFilterAction::Allow(score)) => { - SpamFilterDisposition::Allow { value: *score } - } - Some(SpamFilterAction::Discard) => SpamFilterDisposition::Discard, - Some(SpamFilterAction::Reject) => SpamFilterDisposition::Reject, - Some(SpamFilterAction::Disabled) | None => { - SpamFilterDisposition::Allow { value: 0.0 } - } - }; - response.tags.insert(tag, disposition); - } - - Ok(JsonResponse::new(json!({ - "data": response, - })) - .into_http_response()) - } - _ => Err(trc::ResourceEvent::NotFound.into_err()), - } - } -} diff --git a/crates/http/src/management/stores.rs b/crates/http/src/management/stores.rs deleted file mode 100644 index 5dffeafa..00000000 --- a/crates/http/src/management/stores.rs +++ /dev/null @@ -1,603 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL - */ - -use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD}; -use common::{ - auth::AccessToken, - ipc::{HousekeeperEvent, PurgeType}, - manager::webadmin::Resource, - storage::index::ObjectIndexBuilder, - *, -}; -use directory::{ - Permission, - backend::internal::manage::{self, ManageDirectory}, -}; -use email::{ - cache::MessageCacheFetch, - message::{ - ingest::EmailIngest, - metadata::{MessageData, MessageMetadata}, - }, - sieve::SieveScript, -}; -use groupware::{ - calendar::{Calendar, CalendarEvent, CalendarEventNotification}, - contact::{AddressBook, ContactCard}, - file::FileNode, -}; -use http_proto::{request::decode_path_element, *}; -use hyper::Method; -use serde_json::json; -use services::task_manager::index::ReindexIndexTask; -use std::future::Future; -use store::{ - Serialize, ValueKey, rand, - search::SearchQuery, - write::{ - AlignedBytes, Archive, Archiver, BatchBuilder, BlobLink, BlobOp, DirectoryClass, - SearchIndex, ValueClass, - }, -}; -use trc::AddContext; -use types::{ - blob_hash::BlobHash, - collection::Collection, - field::{EmailField, Field, MailboxField}, -}; -use utils::url_params::UrlParams; - -// SPDX-SnippetBegin -// SPDX-FileCopyrightText: 2020 Stalwart Labs LLC -// SPDX-License-Identifier: LicenseRef-SEL -#[cfg(feature = "enterprise")] -use super::enterprise::undelete::UndeleteApi; -// SPDX-SnippetEnd - -pub trait ManageStore: Sync + Send { - fn handle_manage_store( - &self, - req: &HttpRequest, - path: Vec<&str>, - body: Option>, - session: &HttpSessionData, - access_token: &AccessToken, - ) -> impl Future> + Send; - - fn housekeeper_request( - &self, - event: HousekeeperEvent, - ) -> impl Future> + Send; -} - -impl ManageStore for Server { - async fn handle_manage_store( - &self, - req: &HttpRequest, - path: Vec<&str>, - body: Option>, - session: &HttpSessionData, - access_token: &AccessToken, - ) -> trc::Result { - match ( - path.get(1).copied(), - path.get(2).copied(), - path.get(3).copied(), - req.method(), - ) { - (Some("blobs"), Some(blob_hash), _, &Method::GET) => { - // Validate the access token - access_token.assert_has_permission(Permission::BlobFetch)?; - - let blob_hash = URL_SAFE_NO_PAD - .decode(decode_path_element(blob_hash).as_bytes()) - .map_err(|err| { - trc::EventType::Resource(trc::ResourceEvent::BadParameters) - .from_base64_error(err) - })?; - let contents = self - .core - .storage - .blob - .get_blob(&blob_hash, 0..usize::MAX) - .await? - .ok_or_else(|| trc::ManageEvent::NotFound.into_err())?; - let params = UrlParams::new(req.uri().query()); - let offset = params.parse("offset").unwrap_or(0); - let limit = params.parse("limit").unwrap_or(usize::MAX); - let contents = if offset == 0 && limit == usize::MAX { - contents - } else { - contents - .get(offset..std::cmp::min(offset + limit, contents.len())) - .unwrap_or_default() - .to_vec() - }; - - Ok(Resource::new("application/octet-stream", contents).into_http_response()) - } - (Some("purge"), Some("blob"), _, &Method::GET) => { - // Validate the access token - access_token.assert_has_permission(Permission::PurgeBlobStore)?; - - self.housekeeper_request(HousekeeperEvent::Purge(PurgeType::Blobs { - store: self.core.storage.data.clone(), - blob_store: self.core.storage.blob.clone(), - })) - .await - } - (Some("purge"), Some("data"), id, &Method::GET) => { - // Validate the access token - access_token.assert_has_permission(Permission::PurgeDataStore)?; - - let store = if let Some(id) = id.filter(|id| *id != "default") { - if let Some(store) = self.core.storage.stores.get(id) { - store.clone() - } else { - return Err(trc::ResourceEvent::NotFound.into_err()); - } - } else { - self.core.storage.data.clone() - }; - - self.housekeeper_request(HousekeeperEvent::Purge(PurgeType::Data(store))) - .await - } - (Some("purge"), Some("in-memory"), id, &Method::GET) => { - // Validate the access token - access_token.assert_has_permission(Permission::PurgeInMemoryStore)?; - - let store = if let Some(id) = id.filter(|id| *id != "default") { - if let Some(store) = self.get_lookup_store(id) { - store.clone() - } else { - return Err(trc::ResourceEvent::NotFound.into_err()); - } - } else { - self.core.storage.lookup.clone() - }; - - let prefix = match path.get(4).copied() { - Some("acme") => vec![KV_ACME].into(), - Some("oauth") => vec![KV_OAUTH].into(), - Some("rate-rcpt") => vec![KV_RATE_LIMIT_RCPT].into(), - Some("rate-scan") => vec![KV_RATE_LIMIT_SCAN].into(), - Some("rate-loiter") => vec![KV_RATE_LIMIT_LOITER].into(), - Some("rate-auth") => vec![KV_RATE_LIMIT_AUTH].into(), - Some("rate-smtp") => vec![KV_RATE_LIMIT_SMTP].into(), - Some("rate-contact") => vec![KV_RATE_LIMIT_CONTACT].into(), - Some("rate-http-authenticated") => { - vec![KV_RATE_LIMIT_HTTP_AUTHENTICATED].into() - } - Some("rate-http-anonymous") => vec![KV_RATE_LIMIT_HTTP_ANONYMOUS].into(), - Some("rate-imap") => vec![KV_RATE_LIMIT_IMAP].into(), - Some("greylist") => vec![KV_GREYLIST].into(), - Some("lock-purge-account") => vec![KV_LOCK_PURGE_ACCOUNT].into(), - Some("lock-queue-message") => vec![KV_LOCK_QUEUE_MESSAGE].into(), - Some("lock-queue-report") => vec![KV_LOCK_QUEUE_REPORT].into(), - Some("lock-email-task") => vec![KV_LOCK_TASK].into(), - Some("lock-housekeeper") => vec![KV_LOCK_HOUSEKEEPER].into(), - _ => None, - }; - - self.housekeeper_request(HousekeeperEvent::Purge(PurgeType::Lookup { - store, - prefix, - })) - .await - } - (Some("purge"), Some("account"), id, &Method::GET) => { - // Validate the access token - access_token.assert_has_permission(Permission::PurgeAccount)?; - - let account_id = if let Some(id) = id { - self.core - .storage - .data - .get_principal_id(decode_path_element(id).as_ref()) - .await? - .ok_or_else(|| trc::ManageEvent::NotFound.into_err())? - .into() - } else { - None - }; - - self.housekeeper_request(HousekeeperEvent::Purge(PurgeType::Account { - account_id, - use_roles: false, - })) - .await - } - (Some("reindex"), Some(index), id, &Method::GET) => { - // Validate the access token - access_token.assert_has_permission(Permission::FtsReindex)?; - - let account_id = if let Some(id) = id { - self.core - .storage - .data - .get_principal_id(decode_path_element(id).as_ref()) - .await? - .ok_or_else(|| trc::ManageEvent::NotFound.into_err())? - .into() - } else { - None - }; - let tenant_id = access_token.tenant.map(|t| t.id); - let index = SearchIndex::try_from_str(index).ok_or_else(|| { - trc::ResourceEvent::BadParameters.reason("Invalid search index specified") - })?; - - let jmap = self.clone(); - tokio::spawn(async move { - if let Err(err) = jmap.reindex(index, account_id, tenant_id).await { - trc::error!(err.details("Failed to reindex FTS")); - } - }); - - Ok(JsonResponse::new(json!({ - "data": (), - })) - .into_http_response()) - } - // SPDX-SnippetBegin - // SPDX-FileCopyrightText: 2020 Stalwart Labs LLC - // SPDX-License-Identifier: LicenseRef-SEL - #[cfg(feature = "enterprise")] - (Some("undelete"), _, _, _) => { - // 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. - // Such actions may result in immediate termination of your license, legal action, - // and substantial financial penalties. Stalwart Labs LLC actively monitors for - // unauthorized modifications and will pursue all available legal remedies against - // violators to the fullest extent of the law, including but not limited to claims - // for copyright infringement, breach of contract, and fraud. - - // Validate the access token - access_token.assert_has_permission(Permission::Undelete)?; - - if self.core.is_enterprise_edition() { - self.handle_undelete_api_request(req, path, body, session) - .await - } else { - Err(manage::enterprise()) - } - } - // SPDX-SnippetEnd - (Some("uids"), Some(account_id), None, &Method::DELETE) => { - let account_id = self - .core - .storage - .data - .get_principal_id(decode_path_element(account_id).as_ref()) - .await? - .ok_or_else(|| trc::ManageEvent::NotFound.into_err())?; - - let result = reset_imap_uids(self, account_id).await?; - - Ok(JsonResponse::new(json!({ - "data": result, - })) - .into_http_response()) - } - (Some("quota"), Some(account_id), None, method @ (&Method::GET | &Method::DELETE)) => { - let account_id = self - .core - .storage - .data - .get_principal_id(decode_path_element(account_id).as_ref()) - .await? - .ok_or_else(|| trc::ManageEvent::NotFound.into_err())?; - - if method == Method::DELETE { - recalculate_quota(self, account_id).await?; - } - - let result = self.get_used_quota(account_id).await?; - - Ok(JsonResponse::new(json!({ - "data": result, - })) - .into_http_response()) - } - _ => Err(trc::ResourceEvent::NotFound.into_err()), - } - } - - async fn housekeeper_request(&self, event: HousekeeperEvent) -> trc::Result { - 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": (), - })) - .into_http_response()) - } -} - -pub async fn recalculate_quota(server: &Server, account_id: u32) -> trc::Result<()> { - let mut quota = 0; - - for collection in [ - Collection::Email, - Collection::Calendar, - Collection::CalendarEvent, - Collection::CalendarEventNotification, - Collection::AddressBook, - Collection::ContactCard, - Collection::FileNode, - ] { - server - .archives(account_id, collection, &(), |_, archive| { - match collection { - Collection::Email => { - quota += archive.unarchive::()?.size.to_native() as i64; - } - Collection::Calendar => { - quota += archive.unarchive::()?.size() as i64; - } - Collection::CalendarEvent => { - quota += archive.unarchive::()?.size() as i64; - } - Collection::CalendarEventNotification => { - quota += archive.unarchive::()?.size() as i64; - } - Collection::AddressBook => { - quota += archive.unarchive::()?.size() as i64; - } - Collection::ContactCard => { - quota += archive.unarchive::()?.size() as i64; - } - Collection::FileNode => { - quota += archive.unarchive::()?.size() as i64; - } - _ => {} - } - Ok(true) - }) - .await - .caused_by(trc::location!())?; - } - - let mut batch = BatchBuilder::new(); - batch - .clear(DirectoryClass::UsedQuota(account_id)) - .add(DirectoryClass::UsedQuota(account_id), quota); - server - .store() - .write(batch.build_all()) - .await - .caused_by(trc::location!()) - .map(|_| ()) -} - -pub async fn destroy_account_blobs(server: &Server, account_id: u32) -> trc::Result<()> { - let mut delete_keys = Vec::new(); - for (collection, field) in [ - (Collection::Email, u8::from(EmailField::Metadata)), - (Collection::FileNode, u8::from(Field::ARCHIVE)), - (Collection::SieveScript, u8::from(Field::ARCHIVE)), - ] { - server - .all_archives(account_id, collection, field, |document_id, archive| { - match collection { - Collection::Email => { - let message = archive.unarchive::()?; - delete_keys.push(( - collection, - document_id, - BlobHash::from(&message.blob_hash), - )); - } - Collection::FileNode => { - if let Some(file) = archive.unarchive::()?.file.as_ref() { - delete_keys.push(( - collection, - document_id, - BlobHash::from(&file.blob_hash), - )); - } - } - Collection::SieveScript => { - let sieve = archive.unarchive::()?; - delete_keys.push(( - collection, - document_id, - BlobHash::from(&sieve.blob_hash), - )); - } - _ => {} - } - Ok(()) - }) - .await - .caused_by(trc::location!())?; - } - - let mut batch = BatchBuilder::new(); - batch.with_account_id(account_id); - - for (collection, document_id, hash) in delete_keys { - if batch.is_large_batch() { - server - .store() - .write(batch.build_all()) - .await - .caused_by(trc::location!())?; - batch = BatchBuilder::new(); - batch.with_account_id(account_id); - } - batch - .with_collection(collection) - .with_document(document_id) - .clear(ValueClass::Blob(BlobOp::Link { - hash, - to: BlobLink::Document, - })); - } - - if !batch.is_empty() { - server - .store() - .write(batch.build_all()) - .await - .caused_by(trc::location!())?; - } - - Ok(()) -} - -pub async fn destroy_account_data( - server: &Server, - account_id: u32, - has_data: bool, -) -> trc::Result<()> { - // Unlink all accounts's blobs - if has_data { - destroy_account_blobs(server, account_id).await?; - } - - // Destroy account data - server - .store() - .danger_destroy_account(account_id) - .await - .caused_by(trc::location!())?; - - if has_data { - // Remove search index - for index in [ - SearchIndex::Email, - SearchIndex::Contacts, - SearchIndex::Calendar, - ] { - if let Err(err) = server - .core - .storage - .fts - .unindex(SearchQuery::new(index).with_account_id(account_id)) - .await - { - trc::error!(err.details("Failed to delete FTS index")); - } - } - } - - Ok(()) -} - -pub async fn reset_imap_uids(server: &Server, account_id: u32) -> trc::Result<(u32, u32)> { - let mut mailbox_count = 0; - let mut email_count = 0; - - let cache = server - .get_cached_messages(account_id) - .await - .caused_by(trc::location!())?; - - for &mailbox_id in cache.mailboxes.index.keys() { - let mailbox = server - .store() - .get_value::>(ValueKey::archive( - account_id, - Collection::Mailbox, - mailbox_id, - )) - .await - .caused_by(trc::location!())? - .ok_or_else(|| trc::ImapEvent::Error.into_err().caused_by(trc::location!()))? - .into_deserialized::() - .caused_by(trc::location!())?; - let mut new_mailbox = mailbox.inner.clone(); - new_mailbox.uid_validity = rand::random::(); - let mut batch = BatchBuilder::new(); - batch - .with_account_id(account_id) - .with_collection(Collection::Mailbox) - .with_document(mailbox_id) - .custom( - ObjectIndexBuilder::new() - .with_current(mailbox) - .with_changes(new_mailbox), - ) - .caused_by(trc::location!())? - .clear(MailboxField::UidCounter); - server - .store() - .write(batch.build_all()) - .await - .caused_by(trc::location!())?; - mailbox_count += 1; - } - - // Reset all UIDs - for message_id in cache.emails.items.iter().map(|i| i.document_id) { - let data = server - .store() - .get_value::>(ValueKey::archive( - account_id, - Collection::Email, - message_id, - )) - .await - .caused_by(trc::location!())?; - let data_ = if let Some(data) = data { - data - } else { - continue; - }; - let data = data_ - .to_unarchived::() - .caused_by(trc::location!())?; - let mut new_data = data - .deserialize::() - .caused_by(trc::location!())?; - - let ids = server - .assign_email_ids( - account_id, - new_data.mailboxes.iter().map(|m| m.mailbox_id), - false, - ) - .await - .caused_by(trc::location!())?; - - for (uid_mailbox, uid) in new_data.mailboxes.iter_mut().zip(ids) { - uid_mailbox.uid = uid; - } - - // Prepare write batch - let mut batch = BatchBuilder::new(); - batch - .with_account_id(account_id) - .with_collection(Collection::Email) - .with_document(message_id) - .assert_value(ValueClass::Property(EmailField::Archive.into()), &data) - .set( - EmailField::Archive, - Archiver::new(new_data) - .serialize() - .caused_by(trc::location!())?, - ); - server - .store() - .write(batch.build_all()) - .await - .caused_by(trc::location!())?; - email_count += 1; - } - - Ok((mailbox_count, email_count)) -} diff --git a/crates/http/src/management/enterprise/telemetry.rs b/crates/http/src/management/telemetry.rs similarity index 90% rename from crates/http/src/management/enterprise/telemetry.rs rename to crates/http/src/management/telemetry.rs index 5dd037fc..d7322649 100644 --- a/crates/http/src/management/enterprise/telemetry.rs +++ b/crates/http/src/management/telemetry.rs @@ -8,11 +8,6 @@ * */ -use std::{ - fmt::Write, - time::{Duration, Instant}, -}; - use common::{ Server, auth::{AccessToken, oauth::GrantType}, @@ -21,7 +16,6 @@ use common::{ tracers::store::TracingStore, }, }; -use directory::{Permission, backend::internal::manage}; use http_body_util::{StreamBody, combinators::BoxBody}; use http_proto::*; use hyper::{ @@ -29,8 +23,13 @@ use hyper::{ body::{Bytes, Frame}, }; use mail_parser::DateTime; +use registry::schema::enums::Permission; use serde_json::json; -use std::future::Future; +use std::{ + fmt::Write, + time::{Duration, Instant}, +}; +use std::{future::Future, str::FromStr}; use store::{ ahash::{AHashMap, AHashSet}, search::{SearchComparator, SearchField, SearchFilter, SearchQuery, TracingSearchField}, @@ -43,8 +42,6 @@ use trc::{ }; use utils::{snowflake::SnowflakeIdGenerator, url_params::UrlParams}; -use crate::management::Timestamp; - pub trait TelemetryApi: Sync + Send { fn handle_telemetry_api_request( &self, @@ -71,14 +68,17 @@ impl TelemetryApi for Server { ) { ("traces", None, &Method::GET) => { // Validate the access token - access_token.assert_has_permission(Permission::TracingList)?; + access_token.enforce_permission(Permission::TracingList)?; let page: usize = params.parse("page").unwrap_or(0); let limit: usize = params.parse("limit").unwrap_or(0); let mut tracing_query = Vec::new(); tracing_query.push(SearchFilter::And); if let Some(typ) = params.parse::("type") { - tracing_query.push(SearchFilter::eq(TracingSearchField::EventType, typ.code())); + tracing_query.push(SearchFilter::eq( + TracingSearchField::EventType, + typ.to_id() as u64, + )); } if let Some(queue_id) = params.parse::("queue_id") { tracing_query.push(SearchFilter::eq(TracingSearchField::QueueId, queue_id)); @@ -159,7 +159,10 @@ impl TelemetryApi for Server { .enterprise .as_ref() .and_then(|e| e.trace_store.as_ref()) - .ok_or_else(|| manage::unsupported("No tracing store has been configured"))? + .ok_or_else(|| { + trc::ManageEvent::NotSupported + .ctx(trc::Key::Details, "No tracing store has been configured") + })? .store; let span_ids = self @@ -222,7 +225,7 @@ impl TelemetryApi for Server { } ("traces", Some("live"), &Method::GET) => { // Validate the access token - access_token.assert_has_permission(Permission::TracingLive)?; + access_token.enforce_permission(Permission::TracingLive)?; let mut key_filters = AHashMap::new(); let mut filter = None; @@ -349,14 +352,17 @@ impl TelemetryApi for Server { } ("trace", id, &Method::GET) => { // Validate the access token - access_token.assert_has_permission(Permission::TracingGet)?; + access_token.enforce_permission(Permission::TracingGet)?; let store = &self .core .enterprise .as_ref() .and_then(|e| e.trace_store.as_ref()) - .ok_or_else(|| manage::unsupported("No tracing store has been configured"))? + .ok_or_else(|| { + trc::ManageEvent::NotSupported + .ctx(trc::Key::Details, "No tracing store has been configured") + })? .store; let mut events = Vec::new(); @@ -390,7 +396,7 @@ impl TelemetryApi for Server { } ("live", Some("tracing-token"), &Method::GET) => { // Validate the access token - access_token.assert_has_permission(Permission::TracingLive)?; + access_token.enforce_permission(Permission::TracingLive)?; // Issue a live telemetry token valid for 60 seconds Ok(JsonResponse::new(json!({ @@ -400,7 +406,7 @@ impl TelemetryApi for Server { } ("live", Some("metrics-token"), &Method::GET) => { // Validate the access token - access_token.assert_has_permission(Permission::MetricsLive)?; + access_token.enforce_permission(Permission::MetricsLive)?; // Issue a live telemetry token valid for 60 seconds Ok(JsonResponse::new(json!({ @@ -410,7 +416,7 @@ impl TelemetryApi for Server { } ("metrics", None, &Method::GET) => { // Validate the access token - access_token.assert_has_permission(Permission::MetricsList)?; + access_token.enforce_permission(Permission::MetricsList)?; let before = params .parse::("before") @@ -426,11 +432,15 @@ impl TelemetryApi for Server { .as_ref() .and_then(|e| e.metrics_store.as_ref()) .ok_or_else(|| { - manage::error( - "No metrics store has been defined", - "You need to configure a metrics store in order to use this feature." - .into(), - ) + trc::ManageEvent::Error + .ctx(trc::Key::Details, "No metrics store has been defined") + .ctx( + trc::Key::Reason, + concat!( + "You need to configure a metrics ", + "store in order to use this feature." + ), + ) })? .store .query_metrics(after, before) @@ -444,7 +454,7 @@ impl TelemetryApi for Server { timestamp, value, } => Metric::Counter { - id: id.name(), + id: id.as_str().to_string(), timestamp: DateTime::from_timestamp(timestamp as i64).to_rfc3339(), value, }, @@ -454,7 +464,7 @@ impl TelemetryApi for Server { count, sum, } => Metric::Histogram { - id: id.name(), + id: id.as_str(), timestamp: DateTime::from_timestamp(timestamp as i64).to_rfc3339(), count, sum, @@ -464,7 +474,7 @@ impl TelemetryApi for Server { timestamp, value, } => Metric::Gauge { - id: id.name(), + id: id.as_str(), timestamp: DateTime::from_timestamp(timestamp as i64).to_rfc3339(), value, }, @@ -478,7 +488,7 @@ impl TelemetryApi for Server { } ("metrics", Some("live"), &Method::GET) => { // Validate the access token - access_token.assert_has_permission(Permission::MetricsLive)?; + access_token.enforce_permission(Permission::MetricsLive)?; let interval = Duration::from_secs( params @@ -491,9 +501,9 @@ impl TelemetryApi for Server { for metric_name in params.get("metrics").unwrap_or_default().split(',') { let metric_name = metric_name.trim(); if !metric_name.is_empty() { - if let Some(event_type) = EventType::try_parse(metric_name) { + if let Some(event_type) = EventType::parse(metric_name) { event_types.insert(event_type); - } else if let Some(metric_type) = MetricType::try_parse(metric_name) { + } else if let Some(metric_type) = MetricType::parse(metric_name) { metric_types.insert(metric_type); } } @@ -536,7 +546,7 @@ impl TelemetryApi for Server { let _ = write!( &mut metrics, "{{\"id\":\"{}\",\"type\":\"counter\",\"value\":{}}}", - counter.id().name(), + counter.id().as_str(), counter.value() ); } @@ -551,7 +561,7 @@ impl TelemetryApi for Server { let _ = write!( &mut metrics, "{{\"id\":\"{}\",\"type\":\"gauge\",\"value\":{}}}", - gauge.id().name(), + gauge.id().as_str(), gauge.get() ); } @@ -566,7 +576,7 @@ impl TelemetryApi for Server { let _ = write!( &mut metrics, "{{\"id\":\"{}\",\"type\":\"histogram\",\"count\":{},\"sum\":{}}}", - histogram.id().name(), + histogram.id().as_str(), histogram.count(), histogram.sum() ); @@ -584,3 +594,23 @@ impl TelemetryApi for Server { } } } + +pub(super) struct Timestamp(u64); + +impl FromStr for Timestamp { + type Err = (); + + fn from_str(s: &str) -> Result { + if let Some(dt) = DateTime::parse_rfc3339(s) { + Ok(Timestamp(dt.to_timestamp() as u64)) + } else { + Err(()) + } + } +} + +impl Timestamp { + pub fn into_inner(self) -> u64 { + self.0 + } +} diff --git a/crates/http/src/request.rs b/crates/http/src/request.rs index 92ac5177..00840586 100644 --- a/crates/http/src/request.rs +++ b/crates/http/src/request.rs @@ -15,12 +15,10 @@ use crate::{ }, autoconfig::Autoconfig, form::FormHandler, - management::{ - ManagementApi, ToManageHttpResponse, UnauthorizedResponse, troubleshoot::TroubleshootApi, - }, + management::{ManagementApi, ToManageHttpResponse}, }; use common::{ - Inner, KV_ACME, Server, + BuildServer, Inner, KV_ACME, Server, auth::{AccessToken, oauth::GrantType}, ipc::PushEvent, manager::webadmin::Resource, @@ -90,7 +88,7 @@ impl ParseHttp for Server { ("", &Method::POST) => { // Authenticate request let (_in_flight, access_token) = - self.authenticate_headers(&req, &session, false).await?; + self.authenticate_headers(&req, &session).await?; let bytes = fetch_body( &mut req, @@ -111,7 +109,7 @@ impl ParseHttp for Server { self.core.jmap.request_max_calls, self.core.jmap.request_max_size, )?, - access_token, + &access_token, &session, ) .await @@ -120,7 +118,7 @@ impl ParseHttp for Server { ("download", &Method::GET) => { // Authenticate request let (_in_flight, access_token) = - self.authenticate_headers(&req, &session, false).await?; + self.authenticate_headers(&req, &session).await?; if let (Some(_), Some(blob_id), Some(name)) = ( path.next().and_then(|p| Id::from_str(p).ok()), @@ -149,7 +147,7 @@ impl ParseHttp for Server { ("upload", &Method::POST) => { // Authenticate request let (_in_flight, access_token) = - self.authenticate_headers(&req, &session, false).await?; + self.authenticate_headers(&req, &session).await?; if let Some(account_id) = path.next().and_then(|p| Id::from_str(p).ok()) { return match fetch_body( @@ -171,7 +169,7 @@ impl ParseHttp for Server { .and_then(|h| h.to_str().ok()) .unwrap_or("application/octet-stream"), &bytes, - access_token, + &access_token, ) .await? .into_http_response()), @@ -182,14 +180,14 @@ impl ParseHttp for Server { ("eventsource", &Method::GET) => { // Authenticate request let (_in_flight, access_token) = - self.authenticate_headers(&req, &session, false).await?; + self.authenticate_headers(&req, &session).await?; return self.handle_event_source(req, access_token).await; } ("ws", &Method::GET) => { // Authenticate request let (_in_flight, access_token) = - self.authenticate_headers(&req, &session, false).await?; + self.authenticate_headers(&req, &session).await?; return self .upgrade_websocket_connection(req, access_token, session) @@ -199,11 +197,11 @@ impl ParseHttp for Server { return if req.headers().contains_key(header::AUTHORIZATION) { // Authenticate request let (_in_flight, access_token) = - self.authenticate_headers(&req, &session, false).await?; + self.authenticate_headers(&req, &session).await?; self.handle_session_resource( ctx.resolve_response_url(self).await, - access_token, + &access_token, ) .await .map(|s| s.into_http_response()) @@ -244,7 +242,7 @@ impl ParseHttp for Server { (Some(resource), Some(method)) => { // Authenticate request let (_in_flight, access_token) = - self.authenticate_headers(&req, &session, false).await?; + self.authenticate_headers(&req, &session).await?; self.handle_dav_request(req, access_token, &session, resource, method) .await @@ -288,9 +286,7 @@ impl ParseHttp for Server { ("acme-challenge", &Method::GET) if self.has_acme_http_providers() => { if let Some(token) = path.next() { return match self - .core - .storage - .lookup + .in_memory_store() .key_get::(KeyValue::<()>::build_key(KV_ACME, token)) .await? { @@ -351,7 +347,7 @@ impl ParseHttp for Server { ("introspect", &Method::POST) => { // Authenticate request let (_in_flight, access_token) = - self.authenticate_headers(&req, &session, false).await?; + self.authenticate_headers(&req, &session).await?; return self .handle_token_introspect(&mut req, &access_token, session.session_id) @@ -360,9 +356,11 @@ impl ParseHttp for Server { ("userinfo", &Method::GET) => { // Authenticate request let (_in_flight, access_token) = - self.authenticate_headers(&req, &session, false).await?; + self.authenticate_headers(&req, &session).await?; - return self.handle_userinfo_request(&access_token).await; + return self + .handle_userinfo_request(access_token.account_id()) + .await; } ("register", &Method::POST) => { return self @@ -387,81 +385,45 @@ impl ParseHttp for Server { return Ok(JsonProblemResponse(StatusCode::NO_CONTENT).into_http_response()); } - // Authenticate user - match self.authenticate_headers(&req, &session, true).await { - Ok((_, access_token)) => { - return self - .handle_api_manage_request(&mut req, access_token, &session) - .await; + let params = UrlParams::new(req.uri().query()); + let access_token = if let Some(token) = params.get("token") { + // SPDX-SnippetBegin + // SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + // SPDX-License-Identifier: LicenseRef-SEL + #[cfg(feature = "enterprise")] + if self.core.is_enterprise_edition() { + let path = req.uri().path(); + let (grant_type, permissions) = if path.starts_with("/api/telemetry/traces") + { + (GrantType::LiveTracing, Permission::TracingLive) + } else if path.starts_with("/api/telemetry/metrics") { + (GrantType::LiveMetrics, Permission::MetricsLive) + } else if path.starts_with("/api/diagnose") { + (GrantType::Diagnose, Permission::Troubleshoot) + } else { + return Err(trc::ResourceEvent::NotFound.into_err()); + }; + AccessToken::from_permissions( + self.validate_access_token(grant_type.into(), token) + .await? + .account_id, + [permissions], + ) + } else { + self.authenticate_headers(&req, &session).await?.1 } - Err(err) => { - if err.matches(trc::EventType::Auth(trc::AuthEvent::Failed)) { - let params = UrlParams::new(req.uri().query()); - let path = req.uri().path().split('/').skip(2).collect::>(); - - let (grant_type, token) = match ( - path.first().copied(), - path.get(1).copied(), - params.get("token"), - ) { - // SPDX-SnippetBegin - // SPDX-FileCopyrightText: 2020 Stalwart Labs LLC - // SPDX-License-Identifier: LicenseRef-SEL - #[cfg(feature = "enterprise")] - (Some("telemetry"), Some("traces"), Some(token)) - if self.core.is_enterprise_edition() => - { - (GrantType::LiveTracing, token) - } - #[cfg(feature = "enterprise")] - (Some("telemetry"), Some("metrics"), Some(token)) - if self.core.is_enterprise_edition() => - { - (GrantType::LiveMetrics, token) - } - // SPDX-SnippetEnd - (Some("troubleshoot"), _, Some(token)) => { - (GrantType::Troubleshoot, token) - } - _ => return Ok(HttpResponse::unauthorized(false)), - }; - let token_info = - self.validate_access_token(grant_type.into(), token).await?; - - return match grant_type { - // SPDX-SnippetBegin - // SPDX-FileCopyrightText: 2020 Stalwart Labs LLC - // SPDX-License-Identifier: LicenseRef-SEL - #[cfg(feature = "enterprise")] - GrantType::LiveTracing | GrantType::LiveMetrics => { - use crate::management::enterprise::telemetry::TelemetryApi; - self.handle_telemetry_api_request( - &req, - path, - &AccessToken::from_id(token_info.account_id) - .with_permission(Permission::MetricsLive) - .with_permission(Permission::TracingLive), - ) - .await - } - // SPDX-SnippetEnd - GrantType::Troubleshoot => { - self.handle_troubleshoot_api_request( - &req, - path, - &AccessToken::from_id(token_info.account_id) - .with_permission(Permission::Troubleshoot), - None, - ) - .await - } - _ => unreachable!(), - }; - } - - return Err(err); + // SPDX-SnippetEnd + #[cfg(not(feature = "enterprise"))] + { + self.authenticate_headers(&req, &session).await?.1 } - } + } else { + self.authenticate_headers(&req, &session).await?.1 + }; + + return self + .handle_api_manage_request(&mut req, &access_token, &session) + .await; } "mail" => { if req.method() == Method::GET @@ -678,7 +640,7 @@ async fn handle_session(inner: Arc, session: SessionDat let server = inner.build_server(); // Obtain remote IP - let remote_ip = if !server.core.jmap.http_use_forwarded { + let remote_ip = if !server.core.network.http.use_forwarded { trc::event!( Http(trc::HttpEvent::RequestUrl), SpanId = session.session_id, @@ -797,10 +759,10 @@ async fn handle_session(inner: Arc, session: SessionDat let mut response = response.build(); // Add custom headers - if !server.core.jmap.http_headers.is_empty() { + if !server.core.network.http.response_headers.is_empty() { let headers = response.headers_mut(); - for (header, value) in &server.core.jmap.http_headers { + for (header, value) in &server.core.network.http.response_headers { headers.insert(header.clone(), value.clone()); } } @@ -814,7 +776,7 @@ async fn handle_session(inner: Arc, session: SessionDat { if http_err.is_parse() { let server = inner.build_server(); - if !server.core.jmap.http_use_forwarded { + if !server.core.network.http.use_forwarded { match server.is_scanner_fail2banned(session.remote_ip).await { Ok(true) => { trc::event!( diff --git a/crates/jmap/src/api/event_source.rs b/crates/jmap/src/api/event_source.rs index e14afda1..5f56f4ab 100644 --- a/crates/jmap/src/api/event_source.rs +++ b/crates/jmap/src/api/event_source.rs @@ -13,11 +13,8 @@ use hyper::{ body::{Bytes, Frame}, }; use jmap_proto::{response::status::PushObject, types::state::State}; +use std::time::{Duration, Instant}; use std::{future::Future, str::FromStr}; -use std::{ - sync::Arc, - time::{Duration, Instant}, -}; use types::{id::Id, type_state::DataType}; use utils::map::{bitmap::Bitmap, vec_map::VecMap}; @@ -31,7 +28,7 @@ pub trait EventSourceHandler: Sync + Send { fn handle_event_source( &self, req: HttpRequest, - access_token: Arc, + access_token: AccessToken, ) -> impl Future> + Send; } @@ -39,7 +36,7 @@ impl EventSourceHandler for Server { async fn handle_event_source( &self, req: HttpRequest, - access_token: Arc, + access_token: AccessToken, ) -> trc::Result { // Parse query let mut ping = 0; diff --git a/crates/jmap/src/api/request.rs b/crates/jmap/src/api/request.rs index 27e50711..9b411ee0 100644 --- a/crates/jmap/src/api/request.rs +++ b/crates/jmap/src/api/request.rs @@ -54,7 +54,7 @@ use jmap_proto::{ response::{Response, ResponseMethod, SetResponseMethod}, }; use std::future::Future; -use std::{sync::Arc, time::Instant}; +use std::time::Instant; use trc::JmapEvent; use types::{collection::Collection, id::Id}; @@ -62,7 +62,7 @@ pub trait RequestHandler: Sync + Send { fn handle_jmap_request<'x>( &self, request: Request<'x>, - access_token: Arc, + access_token: &AccessToken, session: &HttpSessionData, ) -> impl Future> + Send; @@ -81,7 +81,7 @@ impl RequestHandler for Server { async fn handle_jmap_request<'x>( &self, request: Request<'x>, - access_token: Arc, + access_token: &AccessToken, session: &HttpSessionData, ) -> Response<'x> { let add_created_ids = request.created_ids.is_some(); @@ -111,7 +111,7 @@ impl RequestHandler for Server { .handle_method_call( call.method, call.name, - &access_token, + access_token, &mut next_call, session, ) diff --git a/crates/jmap/src/blob/upload.rs b/crates/jmap/src/blob/upload.rs index 52940210..0d64e0fa 100644 --- a/crates/jmap/src/blob/upload.rs +++ b/crates/jmap/src/blob/upload.rs @@ -8,7 +8,6 @@ use std::sync::Arc; use super::{UploadResponse, download::BlobDownload}; use common::{Server, auth::AccessToken}; -use registry::schema::enums::Permission; use jmap_proto::{ error::set::SetError, method::upload::{ @@ -16,6 +15,7 @@ use jmap_proto::{ }, request::reference::MaybeIdReference, }; +use registry::schema::enums::Permission; use std::future::Future; use trc::AddContext; use types::id::Id; @@ -36,7 +36,7 @@ pub trait BlobUpload: Sync + Send { account_id: Id, content_type: &str, data: &[u8], - access_token: Arc, + access_token: &AccessToken, ) -> impl Future> + Send; } @@ -210,11 +210,11 @@ impl BlobUpload for Server { account_id: Id, content_type: &str, data: &[u8], - access_token: Arc, + access_token: &AccessToken, ) -> trc::Result { // Limit concurrent uploads let _in_flight = self - .is_upload_allowed(&access_token) + .is_upload_allowed(access_token) .caused_by(trc::location!())?; #[cfg(feature = "test_mode")] diff --git a/crates/jmap/src/share_notification/get.rs b/crates/jmap/src/share_notification/get.rs index aad7f98e..32b4f5e9 100644 --- a/crates/jmap/src/share_notification/get.rs +++ b/crates/jmap/src/share_notification/get.rs @@ -204,7 +204,6 @@ fn build_share_notification( Value::Str( changed_by .description() - .as_deref() .unwrap_or(changed_by.name()) .to_string() .into(), diff --git a/crates/jmap/src/websocket/stream.rs b/crates/jmap/src/websocket/stream.rs index 7d027d8d..96253aba 100644 --- a/crates/jmap/src/websocket/stream.rs +++ b/crates/jmap/src/websocket/stream.rs @@ -17,7 +17,7 @@ use jmap_proto::{ }, }; use std::future::Future; -use std::{sync::Arc, time::Instant}; +use std::time::Instant; use tokio_tungstenite::WebSocketStream; use trc::JmapEvent; use tungstenite::Message; @@ -28,7 +28,7 @@ pub trait WebSocketHandler: Sync + Send { fn handle_websocket_stream( &self, stream: WebSocketStream>, - access_token: Arc, + access_token: AccessToken, session: HttpSessionData, ) -> impl Future + Send; } @@ -38,7 +38,7 @@ impl WebSocketHandler for Server { async fn handle_websocket_stream( &self, mut stream: WebSocketStream>, - access_token: Arc, + access_token: AccessToken, session: HttpSessionData, ) { trc::event!( @@ -98,7 +98,7 @@ impl WebSocketHandler for Server { let response = self .handle_jmap_request( request.request, - access_token.clone(), + &access_token, &session, ) .await; diff --git a/crates/jmap/src/websocket/upgrade.rs b/crates/jmap/src/websocket/upgrade.rs index 14367174..d2a623a5 100644 --- a/crates/jmap/src/websocket/upgrade.rs +++ b/crates/jmap/src/websocket/upgrade.rs @@ -4,25 +4,21 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use std::sync::Arc; - +use super::stream::WebSocketHandler; use common::{Server, auth::AccessToken}; +use http_proto::*; use hyper::StatusCode; use hyper_util::rt::TokioIo; +use std::future::Future; use tokio_tungstenite::WebSocketStream; use trc::JmapEvent; use tungstenite::{handshake::derive_accept_key, protocol::Role}; -use http_proto::*; -use std::future::Future; - -use super::stream::WebSocketHandler; - pub trait WebSocketUpgrade: Sync + Send { fn upgrade_websocket_connection( &self, req: HttpRequest, - access_token: Arc, + access_token: AccessToken, session: HttpSessionData, ) -> impl Future> + Send; } @@ -31,7 +27,7 @@ impl WebSocketUpgrade for Server { async fn upgrade_websocket_connection( &self, req: HttpRequest, - access_token: Arc, + access_token: AccessToken, session: HttpSessionData, ) -> trc::Result { let headers = req.headers(); diff --git a/crates/store/src/registry/query.rs b/crates/store/src/registry/query.rs index 175ebbee..9b2c35e9 100644 --- a/crates/store/src/registry/query.rs +++ b/crates/store/src/registry/query.rs @@ -185,6 +185,12 @@ impl From for RegistryFilterValue { } } +impl From<&str> for RegistryFilterValue { + fn from(value: &str) -> Self { + RegistryFilterValue::String(value.to_string()) + } +} + impl From for RegistryFilterValue { fn from(value: u64) -> Self { RegistryFilterValue::Integer(value)