summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock13
-rw-r--r--crates/cli/src/modules/domain.rs2
-rw-r--r--crates/common/src/config/jmap/settings.rs5
-rw-r--r--crates/common/src/lib.rs13
-rw-r--r--crates/common/src/listener/acme/jose.rs9
-rw-r--r--crates/directory/Cargo.toml1
-rw-r--r--crates/directory/src/backend/imap/lookup.rs2
-rw-r--r--crates/directory/src/backend/internal/lookup.rs22
-rw-r--r--crates/directory/src/backend/internal/manage.rs270
-rw-r--r--crates/directory/src/backend/internal/mod.rs215
-rw-r--r--crates/directory/src/backend/ldap/lookup.rs59
-rw-r--r--crates/directory/src/backend/memory/config.rs72
-rw-r--r--crates/directory/src/backend/memory/lookup.rs14
-rw-r--r--crates/directory/src/backend/memory/mod.rs2
-rw-r--r--crates/directory/src/backend/smtp/lookup.rs4
-rw-r--r--crates/directory/src/backend/sql/lookup.rs49
-rw-r--r--crates/directory/src/core/dispatch.rs2
-rw-r--r--crates/directory/src/core/mod.rs1
-rw-r--r--crates/directory/src/core/principal.rs490
-rw-r--r--crates/directory/src/core/secret.rs13
-rw-r--r--crates/directory/src/lib.rs254
-rw-r--r--crates/imap-proto/src/protocol/mod.rs5
-rw-r--r--crates/imap/src/core/mailbox.rs18
-rw-r--r--crates/imap/src/core/mod.rs2
-rw-r--r--crates/imap/src/op/acl.rs29
-rw-r--r--crates/imap/src/op/append.rs4
-rw-r--r--crates/imap/src/op/authenticate.rs6
-rw-r--r--crates/imap/src/op/capability.rs7
-rw-r--r--crates/imap/src/op/copy_move.rs8
-rw-r--r--crates/imap/src/op/create.rs4
-rw-r--r--crates/imap/src/op/delete.rs4
-rw-r--r--crates/imap/src/op/enable.rs4
-rw-r--r--crates/imap/src/op/expunge.rs4
-rw-r--r--crates/imap/src/op/fetch.rs4
-rw-r--r--crates/imap/src/op/idle.rs4
-rw-r--r--crates/imap/src/op/list.rs7
-rw-r--r--crates/imap/src/op/namespace.rs4
-rw-r--r--crates/imap/src/op/rename.rs4
-rw-r--r--crates/imap/src/op/search.rs7
-rw-r--r--crates/imap/src/op/select.rs8
-rw-r--r--crates/imap/src/op/status.rs4
-rw-r--r--crates/imap/src/op/store.rs4
-rw-r--r--crates/imap/src/op/subscribe.rs4
-rw-r--r--crates/imap/src/op/thread.rs4
-rw-r--r--crates/jmap/src/api/autoconfig.rs6
-rw-r--r--crates/jmap/src/api/http.rs27
-rw-r--r--crates/jmap/src/api/management/dkim.rs18
-rw-r--r--crates/jmap/src/api/management/domain.rs19
-rw-r--r--crates/jmap/src/api/management/enterprise/telemetry.rs39
-rw-r--r--crates/jmap/src/api/management/log.rs12
-rw-r--r--crates/jmap/src/api/management/mod.rs79
-rw-r--r--crates/jmap/src/api/management/principal.rs108
-rw-r--r--crates/jmap/src/api/management/queue.rs24
-rw-r--r--crates/jmap/src/api/management/reload.rs33
-rw-r--r--crates/jmap/src/api/management/report.rs12
-rw-r--r--crates/jmap/src/api/management/settings.rs18
-rw-r--r--crates/jmap/src/api/management/sieve.rs6
-rw-r--r--crates/jmap/src/api/management/stores.rs25
-rw-r--r--crates/jmap/src/api/request.rs23
-rw-r--r--crates/jmap/src/api/session.rs4
-rw-r--r--crates/jmap/src/auth/acl.rs23
-rw-r--r--crates/jmap/src/auth/mod.rs203
-rw-r--r--crates/jmap/src/auth/oauth/auth.rs4
-rw-r--r--crates/jmap/src/auth/oauth/token.rs5
-rw-r--r--crates/jmap/src/auth/rate_limit.rs7
-rw-r--r--crates/jmap/src/blob/upload.rs5
-rw-r--r--crates/jmap/src/identity/get.rs15
-rw-r--r--crates/jmap/src/identity/set.rs8
-rw-r--r--crates/jmap/src/lib.rs2
-rw-r--r--crates/jmap/src/mailbox/set.rs5
-rw-r--r--crates/jmap/src/principal/get.rs15
-rw-r--r--crates/jmap/src/principal/query.rs4
-rw-r--r--crates/jmap/src/services/ingest.rs2
-rw-r--r--crates/jmap/src/sieve/ingest.rs12
-rw-r--r--crates/managesieve/src/op/authenticate.rs4
-rw-r--r--crates/managesieve/src/op/checkscript.rs8
-rw-r--r--crates/managesieve/src/op/deletescript.rs8
-rw-r--r--crates/managesieve/src/op/getscript.rs8
-rw-r--r--crates/managesieve/src/op/havespace.rs8
-rw-r--r--crates/managesieve/src/op/listscripts.rs8
-rw-r--r--crates/managesieve/src/op/mod.rs12
-rw-r--r--crates/managesieve/src/op/putscript.rs8
-rw-r--r--crates/managesieve/src/op/renamescript.rs8
-rw-r--r--crates/managesieve/src/op/setactive.rs8
-rw-r--r--crates/pop3/Cargo.toml1
-rw-r--r--crates/pop3/src/client.rs51
-rw-r--r--crates/pop3/src/lib.rs10
-rw-r--r--crates/pop3/src/op/authenticate.rs10
-rw-r--r--crates/pop3/src/op/delete.rs6
-rw-r--r--crates/pop3/src/op/fetch.rs6
-rw-r--r--crates/pop3/src/op/list.rs16
-rw-r--r--crates/pop3/src/op/mod.rs54
-rw-r--r--crates/smtp/src/inbound/auth.rs5
-rw-r--r--crates/store/src/backend/rocksdb/write.rs4
-rw-r--r--crates/trc/event-macro/Cargo.toml2
-rw-r--r--crates/trc/src/event/description.rs2
-rw-r--r--crates/trc/src/event/mod.rs1
-rw-r--r--crates/trc/src/ipc/bitset.rs4
-rw-r--r--crates/trc/src/lib.rs1
-rw-r--r--crates/trc/src/serializers/binary.rs2
-rw-r--r--crates/utils/proc-macros/Cargo.toml12
-rw-r--r--crates/utils/proc-macros/src/lib.rs77
-rw-r--r--crates/utils/src/map/vec_map.rs185
-rw-r--r--tests/src/directory/internal.rs77
-rw-r--r--tests/src/directory/ldap.rs30
-rw-r--r--tests/src/directory/mod.rs158
-rw-r--r--tests/src/directory/sql.rs36
-rw-r--r--tests/src/smtp/inbound/basic.rs4
-rw-r--r--tests/src/smtp/inbound/dmarc.rs4
-rw-r--r--tests/src/smtp/inbound/ehlo.rs4
-rw-r--r--tests/src/smtp/inbound/limits.rs4
-rw-r--r--tests/src/smtp/inbound/rewrite.rs1
-rw-r--r--tests/src/smtp/lookup/utils.rs4
-rw-r--r--tests/src/smtp/management/queue.rs1
-rw-r--r--tests/src/smtp/management/report.rs1
-rw-r--r--tests/src/smtp/outbound/dane.rs1
-rw-r--r--tests/src/smtp/outbound/extensions.rs1
-rw-r--r--tests/src/smtp/outbound/fallback_relay.rs1
-rw-r--r--tests/src/smtp/outbound/ip_lookup.rs1
-rw-r--r--tests/src/smtp/outbound/lmtp.rs1
-rw-r--r--tests/src/smtp/outbound/mta_sts.rs1
-rw-r--r--tests/src/smtp/outbound/smtp.rs1
-rw-r--r--tests/src/smtp/outbound/throttle.rs1
-rw-r--r--tests/src/smtp/outbound/tls.rs1
-rw-r--r--tests/src/smtp/queue/dsn.rs4
-rw-r--r--tests/src/smtp/queue/manager.rs4
-rw-r--r--tests/src/smtp/queue/retry.rs1
-rw-r--r--tests/src/smtp/reporting/analyze.rs4
128 files changed, 2404 insertions, 895 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 520bfcda..c43db489 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1667,6 +1667,7 @@ dependencies = [
"parking_lot",
"password-hash",
"pbkdf2",
+ "proc_macros",
"pwhash",
"regex",
"rustls 0.23.12",
@@ -1963,7 +1964,7 @@ version = "0.1.0"
dependencies = [
"proc-macro2",
"quote",
- "syn 1.0.109",
+ "syn 2.0.77",
]
[[package]]
@@ -4505,6 +4506,7 @@ name = "pop3"
version = "0.9.4"
dependencies = [
"common",
+ "directory",
"imap",
"jmap",
"jmap_proto",
@@ -4660,6 +4662,15 @@ dependencies = [
]
[[package]]
+name = "proc_macros"
+version = "0.1.0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.77",
+]
+
+[[package]]
name = "prometheus"
version = "0.13.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/crates/cli/src/modules/domain.rs b/crates/cli/src/modules/domain.rs
index fa859b82..6ba2b69b 100644
--- a/crates/cli/src/modules/domain.rs
+++ b/crates/cli/src/modules/domain.rs
@@ -6,7 +6,7 @@
use std::borrow::Cow;
-use prettytable::{Attr, Cell, Row, Table, format};
+use prettytable::{format, Attr, Cell, Row, Table};
use reqwest::Method;
use serde_json::Value;
diff --git a/crates/common/src/config/jmap/settings.rs b/crates/common/src/config/jmap/settings.rs
index 97899ebf..700a5464 100644
--- a/crates/common/src/config/jmap/settings.rs
+++ b/crates/common/src/config/jmap/settings.rs
@@ -83,8 +83,6 @@ pub struct JmapConfig {
pub encrypt: bool,
pub encrypt_append: bool,
- pub principal_allow_lookups: bool,
-
pub capabilities: BaseCapabilities,
pub session_purge_frequency: SimpleCron,
pub account_purge_frequency: SimpleCron,
@@ -371,9 +369,6 @@ impl JmapConfig {
push_max_total: config
.property_or_default("jmap.push.max-total", "100")
.unwrap_or(100),
- principal_allow_lookups: config
- .property("jmap.principal.allow-lookups")
- .unwrap_or(true),
encrypt: config
.property_or_default("storage.encryption.enable", "true")
.unwrap_or(true),
diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs
index efe2f250..7ff3b897 100644
--- a/crates/common/src/lib.rs
+++ b/crates/common/src/lib.rs
@@ -19,7 +19,7 @@ use config::{
storage::Storage,
telemetry::Metrics,
};
-use directory::{core::secret::verify_secret_hash, Directory, Principal, QueryBy, Type};
+use directory::{core::secret::verify_secret_hash, Directory, Principal, QueryBy};
use expr::if_block::IfBlock;
use listener::{
blocked::{AllowedIps, BlockedIps},
@@ -227,7 +227,7 @@ impl Core {
credentials: &Credentials<String>,
remote_ip: IpAddr,
return_member_of: bool,
- ) -> trc::Result<Principal<u32>> {
+ ) -> trc::Result<Principal> {
// First try to authenticate the user against the default directory
let result = match directory
.query(QueryBy::Credentials(credentials), return_member_of)
@@ -237,9 +237,9 @@ impl Core {
trc::event!(
Auth(trc::AuthEvent::Success),
AccountName = credentials.login().to_string(),
- AccountId = principal.id,
+ AccountId = principal.id(),
SpanId = session_id,
- Type = principal.typ.as_str(),
+ Type = principal.typ().as_str(),
);
return Ok(principal);
@@ -268,7 +268,6 @@ impl Core {
Auth(trc::AuthEvent::Success),
AccountName = username.clone(),
SpanId = session_id,
- Type = Type::Superuser.as_str(),
);
return Ok(Principal::fallback_admin(fallback_pass));
@@ -289,8 +288,8 @@ impl Core {
Auth(trc::AuthEvent::Success),
AccountName = username.to_string(),
SpanId = session_id,
- AccountId = principal.id,
- Type = principal.typ.as_str(),
+ AccountId = principal.id(),
+ Type = principal.typ().as_str(),
);
return Ok(principal);
diff --git a/crates/common/src/listener/acme/jose.rs b/crates/common/src/listener/acme/jose.rs
index 40626dd5..0b8ee99f 100644
--- a/crates/common/src/listener/acme/jose.rs
+++ b/crates/common/src/listener/acme/jose.rs
@@ -23,7 +23,11 @@ pub(crate) fn sign(
let combined = format!("{}.{}", &protected, &payload);
let signature = key
.sign(&SystemRandom::new(), combined.as_bytes())
- .map_err(|err| trc::EventType::Acme(trc::AcmeEvent::Error).caused_by(trc::location!()).reason(err))?;
+ .map_err(|err| {
+ trc::EventType::Acme(trc::AcmeEvent::Error)
+ .caused_by(trc::location!())
+ .reason(err)
+ })?;
let signature = URL_SAFE_NO_PAD.encode(signature.as_ref());
let body = Body {
protected,
@@ -31,7 +35,8 @@ pub(crate) fn sign(
signature,
};
- serde_json::to_string(&body).map_err(|err| trc::EventType::Acme(trc::AcmeEvent::Error).from_json_error(err))
+ serde_json::to_string(&body)
+ .map_err(|err| trc::EventType::Acme(trc::AcmeEvent::Error).from_json_error(err))
}
pub(crate) fn key_authorization(key: &EcdsaKeyPair, token: &str) -> trc::Result<String> {
diff --git a/crates/directory/Cargo.toml b/crates/directory/Cargo.toml
index 30cf4c7f..ac23f369 100644
--- a/crates/directory/Cargo.toml
+++ b/crates/directory/Cargo.toml
@@ -6,6 +6,7 @@ resolver = "2"
[dependencies]
utils = { path = "../utils" }
+proc_macros = { path = "../utils/proc-macros" }
store = { path = "../store" }
trc = { path = "../trc" }
jmap_proto = { path = "../jmap-proto" }
diff --git a/crates/directory/src/backend/imap/lookup.rs b/crates/directory/src/backend/imap/lookup.rs
index acfc1ed9..49fd6fbf 100644
--- a/crates/directory/src/backend/imap/lookup.rs
+++ b/crates/directory/src/backend/imap/lookup.rs
@@ -12,7 +12,7 @@ use crate::{IntoError, Principal, QueryBy};
use super::{ImapDirectory, ImapError};
impl ImapDirectory {
- pub async fn query(&self, query: QueryBy<'_>) -> trc::Result<Option<Principal<u32>>> {
+ pub async fn query(&self, query: QueryBy<'_>) -> trc::Result<Option<Principal>> {
if let QueryBy::Credentials(credentials) = query {
let mut client = self
.pool
diff --git a/crates/directory/src/backend/internal/lookup.rs b/crates/directory/src/backend/internal/lookup.rs
index cbe1d353..3d86b831 100644
--- a/crates/directory/src/backend/internal/lookup.rs
+++ b/crates/directory/src/backend/internal/lookup.rs
@@ -12,7 +12,7 @@ use store::{
use crate::{Principal, QueryBy, Type};
-use super::{manage::ManageDirectory, PrincipalIdType};
+use super::{manage::ManageDirectory, PrincipalField, PrincipalIdType};
#[allow(async_fn_in_trait)]
pub trait DirectoryStore: Sync + Send {
@@ -20,7 +20,7 @@ pub trait DirectoryStore: Sync + Send {
&self,
by: QueryBy<'_>,
return_member_of: bool,
- ) -> trc::Result<Option<Principal<u32>>>;
+ ) -> trc::Result<Option<Principal>>;
async fn email_to_ids(&self, email: &str) -> trc::Result<Vec<u32>>;
async fn is_local_domain(&self, domain: &str) -> trc::Result<bool>;
@@ -34,7 +34,7 @@ impl DirectoryStore for Store {
&self,
by: QueryBy<'_>,
return_member_of: bool,
- ) -> trc::Result<Option<Principal<u32>>> {
+ ) -> trc::Result<Option<Principal>> {
let (account_id, secret) = match by {
QueryBy::Name(name) => (self.get_account_id(name).await?, None),
QueryBy::Id(account_id) => (account_id.into(), None),
@@ -53,7 +53,7 @@ impl DirectoryStore for Store {
if let Some(account_id) = account_id {
match (
- self.get_value::<Principal<u32>>(ValueKey::from(ValueClass::Directory(
+ self.get_value::<Principal>(ValueKey::from(ValueClass::Directory(
DirectoryClass::Principal(account_id),
)))
.await?,
@@ -61,13 +61,19 @@ impl DirectoryStore for Store {
) {
(Some(mut principal), Some(secret)) if principal.verify_secret(secret).await? => {
if return_member_of {
- principal.member_of = self.get_member_of(principal.id).await?;
+ principal.set(
+ PrincipalField::MemberOf,
+ self.get_member_of(principal.id).await?,
+ );
}
Ok(Some(principal))
}
(Some(mut principal), None) => {
if return_member_of {
- principal.member_of = self.get_member_of(principal.id).await?;
+ principal.set(
+ PrincipalField::MemberOf,
+ self.get_member_of(principal.id).await?,
+ );
}
Ok(Some(principal))
@@ -143,11 +149,11 @@ impl DirectoryStore for Store {
let mut results = Vec::new();
for account_id in self.email_to_ids(address).await? {
if let Some(email) = self
- .get_value::<Principal<u32>>(ValueKey::from(ValueClass::Directory(
+ .get_value::<Principal>(ValueKey::from(ValueClass::Directory(
DirectoryClass::Principal(account_id),
)))
.await?
- .and_then(|p| p.emails.into_iter().next())
+ .and_then(|mut p| p.take_str(PrincipalField::Emails))
{
results.push(email);
}
diff --git a/crates/directory/src/backend/internal/manage.rs b/crates/directory/src/backend/internal/manage.rs
index 710245b5..0838b391 100644
--- a/crates/directory/src/backend/internal/manage.rs
+++ b/crates/directory/src/backend/internal/manage.rs
@@ -28,11 +28,7 @@ pub trait ManageDirectory: Sized {
async fn get_account_name(&self, account_id: u32) -> trc::Result<Option<String>>;
async fn get_member_of(&self, account_id: u32) -> trc::Result<Vec<u32>>;
async fn get_members(&self, account_id: u32) -> trc::Result<Vec<u32>>;
- async fn create_account(
- &self,
- principal: Principal<String>,
- members: Vec<String>,
- ) -> trc::Result<u32>;
+ async fn create_account(&self, principal: Principal) -> trc::Result<u32>;
async fn update_account(
&self,
by: QueryBy<'_>,
@@ -44,12 +40,12 @@ pub trait ManageDirectory: Sized {
filter: Option<&str>,
typ: Option<Type>,
) -> trc::Result<Vec<String>>;
- async fn map_group_ids(&self, principal: Principal<u32>) -> trc::Result<Principal<String>>;
+ async fn map_group_ids(&self, principal: Principal) -> trc::Result<Principal>;
async fn map_principal(
&self,
- principal: Principal<String>,
+ principal: Principal,
create_if_missing: bool,
- ) -> trc::Result<Principal<u32>>;
+ ) -> trc::Result<Principal>;
async fn map_group_names(
&self,
members: Vec<String>,
@@ -62,11 +58,11 @@ pub trait ManageDirectory: Sized {
impl ManageDirectory for Store {
async fn get_account_name(&self, account_id: u32) -> trc::Result<Option<String>> {
- self.get_value::<Principal<u32>>(ValueKey::from(ValueClass::Directory(
+ self.get_value::<Principal>(ValueKey::from(ValueClass::Directory(
DirectoryClass::Principal(account_id),
)))
.await
- .map(|v| if let Some(v) = v { Some(v.name) } else { None })
+ .map(|v| v.and_then(|mut v| v.take_str(PrincipalField::Name)))
.caused_by(trc::location!())
}
@@ -108,9 +104,9 @@ impl ManageDirectory for Store {
ValueClass::Directory(DirectoryClass::Principal(MaybeDynamicId::Dynamic(0))),
Principal {
typ: Type::Individual,
- name: name.to_string(),
..Default::default()
- },
+ }
+ .with_field(PrincipalField::Name, name.to_string()),
);
match self
@@ -133,39 +129,42 @@ impl ManageDirectory for Store {
}
}
- async fn create_account(
- &self,
- principal: Principal<String>,
- members: Vec<String>,
- ) -> trc::Result<u32> {
+ async fn create_account(&self, mut principal: Principal) -> trc::Result<u32> {
// Make sure the principal has a name
- if principal.name.is_empty() {
+ let name = principal.name().to_lowercase();
+ if name.is_empty() {
return Err(err_missing(PrincipalField::Name));
}
// Map group names
- let mut principal = self
- .map_principal(principal, false)
+ let members = self
+ .map_group_names(
+ principal
+ .take(PrincipalField::Members)
+ .map(|v| v.into_str_array())
+ .unwrap_or_default(),
+ false,
+ )
.await
.caused_by(trc::location!())?;
- let members = self
- .map_group_names(members, false)
+ let mut principal = self
+ .map_principal(principal, false)
.await
.caused_by(trc::location!())?;
// Make sure new name is not taken
- principal.name = principal.name.to_lowercase();
if self
- .get_account_id(&principal.name)
+ .get_account_id(&name)
.await
.caused_by(trc::location!())?
.is_some()
{
- return Err(err_exists(PrincipalField::Name, principal.name));
+ return Err(err_exists(PrincipalField::Name, name));
}
+ principal.set(PrincipalField::Name, name);
// Make sure the e-mail is not taken and validate domain
- for email in principal.emails.iter_mut() {
+ for email in principal.iter_mut_str(PrincipalField::Emails) {
*email = email.to_lowercase();
if self.rcpt(email).await.caused_by(trc::location!())? {
return Err(err_exists(PrincipalField::Emails, email.to_string()));
@@ -183,14 +182,14 @@ impl ManageDirectory for Store {
// Write principal
let mut batch = BatchBuilder::new();
- let ptype = DynamicPrincipalIdType(principal.typ.into_base_type());
+ let ptype = DynamicPrincipalIdType(principal.typ);
batch
.with_account_id(u32::MAX)
.with_collection(Collection::Principal)
.create_document()
.assert_value(
ValueClass::Directory(DirectoryClass::NameToId(
- principal.name.clone().into_bytes(),
+ principal.name().to_string().into_bytes(),
)),
(),
)
@@ -199,30 +198,40 @@ impl ManageDirectory for Store {
principal.clone(),
)
.set(
- ValueClass::Directory(DirectoryClass::NameToId(principal.name.into_bytes())),
+ ValueClass::Directory(DirectoryClass::NameToId(
+ principal
+ .take_str(PrincipalField::Name)
+ .unwrap()
+ .into_bytes(),
+ )),
ptype,
);
// Write email to id mapping
- for email in principal.emails {
- batch.set(
- ValueClass::Directory(DirectoryClass::EmailToId(email.into_bytes())),
- ptype,
- );
+ if let Some(emails) = principal
+ .take(PrincipalField::Emails)
+ .map(|v| v.into_str_array())
+ {
+ for email in emails {
+ batch.set(
+ ValueClass::Directory(DirectoryClass::EmailToId(email.into_bytes())),
+ ptype,
+ );
+ }
}
// Write membership
- for member_of in principal.member_of {
+ for member_of in principal.iter_int(PrincipalField::MemberOf) {
batch.set(
ValueClass::Directory(DirectoryClass::MemberOf {
principal_id: MaybeDynamicId::Dynamic(0),
- member_of: MaybeDynamicId::Static(member_of),
+ member_of: MaybeDynamicId::Static(member_of as u32),
}),
vec![],
);
batch.set(
ValueClass::Directory(DirectoryClass::Members {
- principal_id: MaybeDynamicId::Static(member_of),
+ principal_id: MaybeDynamicId::Static(member_of as u32),
has_member: MaybeDynamicId::Dynamic(0),
}),
vec![],
@@ -261,8 +270,8 @@ impl ManageDirectory for Store {
QueryBy::Credentials(_) => unreachable!(),
};
- let principal = self
- .get_value::<Principal<u32>>(ValueKey::from(ValueClass::Directory(
+ let mut principal = self
+ .get_value::<Principal>(ValueKey::from(ValueClass::Directory(
DirectoryClass::Principal(account_id),
)))
.await
@@ -288,14 +297,21 @@ impl ManageDirectory for Store {
let mut batch = BatchBuilder::new();
batch
.with_account_id(account_id)
- .clear(DirectoryClass::NameToId(principal.name.into_bytes()))
+ .clear(DirectoryClass::NameToId(
+ principal
+ .take_str(PrincipalField::Name)
+ .unwrap_or_default()
+ .into_bytes(),
+ ))
.clear(DirectoryClass::Principal(MaybeDynamicId::Static(
account_id,
)))
.clear(DirectoryClass::UsedQuota(account_id));
- for email in principal.emails {
- batch.clear(DirectoryClass::EmailToId(email.into_bytes()));
+ if let Some(emails) = principal.take_str_array(PrincipalField::Emails) {
+ for email in emails {
+ batch.clear(DirectoryClass::EmailToId(email.into_bytes()));
+ }
}
for member_id in self
@@ -352,7 +368,7 @@ impl ManageDirectory for Store {
// Fetch principal
let mut principal = self
- .get_value::<HashedValue<Principal<u32>>>(ValueKey::from(ValueClass::Directory(
+ .get_value::<HashedValue<Principal>>(ValueKey::from(ValueClass::Directory(
DirectoryClass::Principal(account_id),
)))
.await
@@ -371,8 +387,7 @@ impl ManageDirectory for Store {
// Apply changes
let mut batch = BatchBuilder::new();
- let ptype =
- PrincipalIdType::new(account_id, principal.inner.typ.into_base_type()).serialize();
+ let ptype = PrincipalIdType::new(account_id, principal.inner.typ).serialize();
let update_principal = !changes.is_empty()
&& !changes
.iter()
@@ -391,7 +406,7 @@ impl ManageDirectory for Store {
(PrincipalAction::Set, PrincipalField::Name, PrincipalValue::String(new_name)) => {
// Make sure new name is not taken
let new_name = new_name.to_lowercase();
- if principal.inner.name != new_name {
+ if principal.inner.name() != new_name {
if self
.get_account_id(&new_name)
.await
@@ -402,10 +417,10 @@ impl ManageDirectory for Store {
}
batch.clear(ValueClass::Directory(DirectoryClass::NameToId(
- principal.inner.name.as_bytes().to_vec(),
+ principal.inner.name().as_bytes().to_vec(),
)));
- principal.inner.name.clone_from(&new_name);
+ principal.inner.set(PrincipalField::Name, new_name.clone());
batch.set(
ValueClass::Directory(DirectoryClass::NameToId(new_name.into_bytes())),
@@ -413,35 +428,27 @@ impl ManageDirectory for Store {
);
}
}
- (PrincipalAction::Set, PrincipalField::Type, PrincipalValue::String(new_type)) => {
- if let Some(new_type) = Type::parse(&new_type) {
- if matches!(principal.inner.typ, Type::Individual | Type::Superuser)
- && matches!(new_type, Type::Individual | Type::Superuser)
- {
- principal.inner.typ = new_type;
- continue;
- }
- }
- return Err(trc::ManageEvent::NotSupported.caused_by(trc::location!()));
- }
(
PrincipalAction::Set,
PrincipalField::Secrets,
- PrincipalValue::StringList(secrets),
+ value @ (PrincipalValue::StringList(_) | PrincipalValue::String(_)),
) => {
- principal.inner.secrets = secrets;
+ principal.inner.set(PrincipalField::Secrets, value);
}
(
PrincipalAction::AddItem,
PrincipalField::Secrets,
PrincipalValue::String(secret),
) => {
- if !principal.inner.secrets.contains(&secret) {
- if secret.is_otp_auth() && !principal.inner.secrets.is_empty() {
+ if !principal
+ .inner
+ .has_str_value(PrincipalField::Secrets, &secret)
+ {
+ if secret.is_otp_auth() {
// Add OTP Auth URLs to the beginning of the list
- principal.inner.secrets.insert(0, secret);
+ principal.inner.prepend_str(PrincipalField::Secrets, secret);
} else {
- principal.inner.secrets.push(secret);
+ principal.inner.append_str(PrincipalField::Secrets, secret);
}
}
}
@@ -451,14 +458,17 @@ impl ManageDirectory for Store {
PrincipalValue::String(secret),
) => {
if secret.is_app_password() || secret.is_otp_auth() {
+ principal.inner.retain_str(PrincipalField::Secrets, |v| {
+ *v != secret && !v.starts_with(&secret)
+ });
+ } else if !secret.is_empty() {
principal
.inner
- .secrets
- .retain(|v| *v != secret && !v.starts_with(&secret));
- } else if !secret.is_empty() {
- principal.inner.secrets.retain(|v| *v != secret);
+ .retain_str(PrincipalField::Secrets, |v| *v != secret);
} else {
- principal.inner.secrets.retain(|v| !v.is_password());
+ principal
+ .inner
+ .retain_str(PrincipalField::Secrets, |v| !v.is_password());
}
}
(
@@ -467,13 +477,15 @@ impl ManageDirectory for Store {
PrincipalValue::String(description),
) => {
if !description.is_empty() {
- principal.inner.description = Some(description);
+ principal
+ .inner
+ .set(PrincipalField::Description, description);
} else {
- principal.inner.description = None;
+ principal.inner.remove(PrincipalField::Description);
}
}
(PrincipalAction::Set, PrincipalField::Quota, PrincipalValue::Integer(quota)) => {
- principal.inner.quota = quota;
+ principal.inner.set(PrincipalField::Quota, quota);
}
// Emails
@@ -488,7 +500,7 @@ impl ManageDirectory for Store {
.map(|v| v.to_lowercase())
.collect::<Vec<_>>();
for email in &emails {
- if !principal.inner.emails.contains(email) {
+ if !principal.inner.has_str_value(PrincipalField::Emails, email) {
if self.rcpt(email).await.caused_by(trc::location!())? {
return Err(err_exists(PrincipalField::Emails, email.to_string()));
}
@@ -510,7 +522,7 @@ impl ManageDirectory for Store {
}
}
- for email in &principal.inner.emails {
+ for email in principal.inner.iter_str(PrincipalField::Emails) {
if !emails.contains(email) {
batch.clear(ValueClass::Directory(DirectoryClass::EmailToId(
email.as_bytes().to_vec(),
@@ -518,7 +530,7 @@ impl ManageDirectory for Store {
}
}
- principal.inner.emails = emails;
+ principal.inner.set(PrincipalField::Emails, emails);
}
(
PrincipalAction::AddItem,
@@ -526,7 +538,10 @@ impl ManageDirectory for Store {
PrincipalValue::String(email),
) => {
let email = email.to_lowercase();
- if !principal.inner.emails.contains(&email) {
+ if !principal
+ .inner
+ .has_str_value(PrincipalField::Emails, &email)
+ {
if self.rcpt(&email).await.caused_by(trc::location!())? {
return Err(err_exists(PrincipalField::Emails, email));
}
@@ -545,7 +560,7 @@ impl ManageDirectory for Store {
)),
ptype.clone(),
);
- principal.inner.emails.push(email);
+ principal.inner.append_str(PrincipalField::Emails, email);
}
}
(
@@ -554,11 +569,16 @@ impl ManageDirectory for Store {
PrincipalValue::String(email),
) => {
let email = email.to_lowercase();
- if let Some(pos) = principal.inner.emails.iter().position(|v| *v == email) {
+ if principal
+ .inner
+ .has_str_value(PrincipalField::Emails, &email)
+ {
+ principal
+ .inner
+ .retain_str(PrincipalField::Emails, |v| *v != email);
batch.clear(ValueClass::Directory(DirectoryClass::EmailToId(
- email.as_bytes().to_vec(),
+ email.into_bytes(),
)));
- principal.inner.emails.remove(pos);
}
}
@@ -806,49 +826,37 @@ impl ManageDirectory for Store {
self.write(batch.build()).await.map(|_| ())
}
- async fn map_group_ids(&self, principal: Principal<u32>) -> trc::Result<Principal<String>> {
- let mut mapped = Principal {
- id: principal.id,
- typ: principal.typ,
- quota: principal.quota,
- name: principal.name,
- secrets: principal.secrets,
- emails: principal.emails,
- member_of: Vec::with_capacity(principal.member_of.len()),
- description: principal.description,
- };
-
- for account_id in principal.member_of {
- if let Some(name) = self
- .get_account_name(account_id)
- .await
- .caused_by(trc::location!())?
- {
- mapped.member_of.push(name);
+ async fn map_group_ids(&self, mut principal: Principal) -> trc::Result<Principal> {
+ if let Some(member_of) = principal.take_int_array(PrincipalField::MemberOf) {
+ for account_id in member_of {
+ if let Some(name) = self
+ .get_account_name(account_id as u32)
+ .await
+ .caused_by(trc::location!())?
+ {
+ principal.append_str(PrincipalField::MemberOf, name);
+ }
}
}
- Ok(mapped)
+ Ok(principal)
}
async fn map_principal(
&self,
- principal: Principal<String>,
+ mut principal: Principal,
create_if_missing: bool,
- ) -> trc::Result<Principal<u32>> {
- Ok(Principal {
- id: principal.id,
- typ: principal.typ,
- quota: principal.quota,
- name: principal.name,
- secrets: principal.secrets,
- emails: principal.emails,
- member_of: self
- .map_group_names(principal.member_of, create_if_missing)
- .await
- .caused_by(trc::location!())?,
- description: principal.description,
- })
+ ) -> trc::Result<Principal> {
+ if let Some(member_of) = principal.take_str_array(PrincipalField::MemberOf) {
+ principal.set(
+ PrincipalField::MemberOf,
+ self.map_group_names(member_of, create_if_missing)
+ .await
+ .caused_by(trc::location!())?,
+ );
+ }
+
+ Ok(principal)
}
async fn map_group_names(
@@ -914,21 +922,20 @@ impl ManageDirectory for Store {
for (account_id, account_name) in results {
let principal = self
- .get_value::<Principal<u32>>(ValueKey::from(ValueClass::Directory(
+ .get_value::<Principal>(ValueKey::from(ValueClass::Directory(
DirectoryClass::Principal(account_id),
)))
.await
.caused_by(trc::location!())?
.ok_or_else(|| not_found(account_id.to_string()))?;
if filters.iter().all(|f| {
- principal.name.to_lowercase().contains(f)
+ principal.name().to_lowercase().contains(f)
|| principal
- .description
+ .description()
.as_ref()
.map_or(false, |d| d.to_lowercase().contains(f))
|| principal
- .emails
- .iter()
+ .iter_str(PrincipalField::Emails)
.any(|email| email.to_lowercase().contains(f))
}) {
filtered.push(account_name);
@@ -1010,7 +1017,7 @@ impl ManageDirectory for Store {
}
}
-impl SerializeWithId for Principal<u32> {
+impl SerializeWithId for Principal {
fn serialize_with_id(&self, ids: &AssignedIds) -> trc::Result<Vec<u8>> {
let mut principal = self.clone();
principal.id = ids.last_document_id().caused_by(trc::location!())?;
@@ -1018,8 +1025,8 @@ impl SerializeWithId for Principal<u32> {
}
}
-impl From<Principal<u32>> for MaybeDynamicValue {
- fn from(principal: Principal<u32>) -> Self {
+impl From<Principal> for MaybeDynamicValue {
+ fn from(principal: Principal) -> Self {
MaybeDynamicValue::Dynamic(Box::new(principal))
}
}
@@ -1040,21 +1047,6 @@ impl From<DynamicPrincipalIdType> for MaybeDynamicValue {
}
}
-impl From<Principal<String>> for Principal<u32> {
- fn from(principal: Principal<String>) -> Self {
- Principal {
- id: principal.id,
- typ: principal.typ,
- quota: principal.quota,
- name: principal.name,
- secrets: principal.secrets,
- emails: principal.emails,
- member_of: Vec::with_capacity(0),
- description: principal.description,
- }
- }
-}
-
pub fn err_missing(field: impl Into<trc::Value>) -> trc::Error {
trc::ManageEvent::MissingParameter.ctx(trc::Key::Key, field)
}
diff --git a/crates/directory/src/backend/internal/mod.rs b/crates/directory/src/backend/internal/mod.rs
index f0421612..b8ac95df 100644
--- a/crates/directory/src/backend/internal/mod.rs
+++ b/crates/directory/src/backend/internal/mod.rs
@@ -9,45 +9,70 @@ pub mod manage;
use std::{fmt::Display, slice::Iter, str::FromStr};
+use ahash::AHashMap;
use store::{write::key::KeySerializer, Deserialize, Serialize, U32_LEN};
use utils::codec::leb128::Leb128Iterator;
use crate::{Principal, Type};
+const INT_MARKER: u8 = 1 << 7;
+
pub(super) struct PrincipalIdType {
pub account_id: u32,
pub typ: Type,
}
-impl Serialize for Principal<u32> {
+impl Serialize for Principal {
fn serialize(self) -> Vec<u8> {
(&self).serialize()
}
}
-impl Serialize for &Principal<u32> {
+impl Serialize for &Principal {
fn serialize(self) -> Vec<u8> {
let mut serializer = KeySerializer::new(
- U32_LEN * 3
+ U32_LEN * 2
+ 2
- + self.name.len()
- + self.emails.iter().map(|s| s.len()).sum::<usize>()
- + self.secrets.iter().map(|s| s.len()).sum::<usize>()
- + self.description.as_ref().map(|s| s.len()).unwrap_or(0),
+ + self
+ .fields
+ .values()
+ .map(|v| v.serialized_size() + 1)
+ .sum::<usize>(),
)
- .write(1u8)
+ .write(2u8)
.write_leb128(self.id)
.write(self.typ as u8)
- .write_leb128(self.quota)
- .write_leb128(self.name.len())
- .write(self.name.as_bytes())
- .write_leb128(self.description.as_ref().map_or(0, |s| s.len()))
- .write(self.description.as_deref().unwrap_or_default().as_bytes());
-
- for list in [&self.secrets, &self.emails] {
- serializer = serializer.write_leb128(list.len());
- for value in list {
- serializer = serializer.write_leb128(value.len()).write(value.as_bytes());
+ .write_leb128(self.fields.len());
+
+ for (k, v) in &self.fields {
+ let id = k.id();
+
+ match v {
+ PrincipalValue::String(v) => {
+ serializer = serializer
+ .write(id)
+ .write_leb128(1usize)
+ .write_leb128(v.len())
+ .write(v.as_bytes());
+ }
+ PrincipalValue::StringList(l) => {
+ serializer = serializer.write(id).write_leb128(l.len());
+ for v in l {
+ serializer = serializer.write_leb128(v.len()).write(v.as_bytes());
+ }
+ }
+ PrincipalValue::Integer(v) => {
+ serializer = serializer
+ .write(id | INT_MARKER)
+ .write_leb128(1usize)
+ .write_leb128(*v);
+ }
+ PrincipalValue::IntegerList(l) => {
+ serializer = serializer.write(id | INT_MARKER).write_leb128(l.len());
+ for v in l {
+ serializer = serializer.write_leb128(*v);
+ }
+ }
}
}
@@ -55,7 +80,7 @@ impl Serialize for &Principal<u32> {
}
}
-impl Deserialize for Principal<u32> {
+impl Deserialize for Principal {
fn deserialize(bytes: &[u8]) -> trc::Result<Self> {
deserialize(bytes).ok_or_else(|| {
trc::StoreEvent::DataCorruption
@@ -98,32 +123,89 @@ impl PrincipalIdType {
}
}
-fn deserialize(bytes: &[u8]) -> Option<Principal<u32>> {
+fn deserialize(bytes: &[u8]) -> Option<Principal> {
let mut bytes = bytes.iter();
- if bytes.next()? != &1 {
- return None;
- }
- Principal {
- id: bytes.next_leb128()?,
- typ: Type::from_u8(*bytes.next()?),
- quota: bytes.next_leb128()?,
- name: deserialize_string(&mut bytes)?,
- description: deserialize_string(&mut bytes).map(|v| {
- if !v.is_empty() {
- Some(v)
+ let version = *bytes.next()?;
+ let id = bytes.next_leb128()?;
+ let type_id = *bytes.next()?;
+ let typ = Type::from_u8(type_id);
+
+ match version {
+ 1 => {
+ // Version 1 (legacy)
+ let mut principal = Principal {
+ id,
+ typ,
+ ..Default::default()
+ };
+
+ principal.set(PrincipalField::Quota, bytes.next_leb128::<u64>()?);
+ principal.set(PrincipalField::Name, deserialize_string(&mut bytes)?);
+ if let Some(description) = deserialize_string(&mut bytes).filter(|s| !s.is_empty()) {
+ principal.set(PrincipalField::Description, description);
+ }
+ for key in [PrincipalField::Secrets, PrincipalField::Emails] {
+ for _ in 0..bytes.next_leb128::<usize>()? {
+ principal.append_str(key, deserialize_string(&mut bytes)?);
+ }
+ }
+
+ if type_id != 4 {
+ principal
} else {
- None
+ principal.into_superuser()
+ }
+ .into()
+ }
+ 2 => {
+ // Version 2
+ let num_fields = bytes.next_leb128::<usize>()?;
+
+ let mut principal = Principal {
+ id,
+ typ,
+ fields: AHashMap::with_capacity(num_fields),
+ };
+
+ for _ in 0..num_fields {
+ let id = *bytes.next()?;
+ let num_values = bytes.next_leb128::<usize>()?;
+
+ if (id & INT_MARKER) == 0 {
+ let field = PrincipalField::from_id(id)?;
+ if num_values == 1 {
+ principal.set(field, deserialize_string(&mut bytes)?);
+ } else {
+ let mut values = Vec::with_capacity(num_values);
+ for _ in 0..num_values {
+ values.push(deserialize_string(&mut bytes)?);
+ }
+ principal.set(field, values);
+ }
+ } else {
+ let field = PrincipalField::from_id(id & !INT_MARKER)?;
+ if num_values == 1 {
+ principal.set(field, bytes.next_leb128::<u64>()?);
+ } else {
+ let mut values = Vec::with_capacity(num_values);
+ for _ in 0..num_values {
+ values.push(bytes.next_leb128::<u64>()?);
+ }
+ principal.set(field, values);
+ }
+ }
}
- })?,
- secrets: deserialize_string_list(&mut bytes)?,
- emails: deserialize_string_list(&mut bytes)?,
- member_of: Vec::new(),
+
+ principal.into()
+ }
+ _ => None,
}
- .into()
}
-#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
+#[derive(
+ Debug, Clone, Copy, PartialEq, Hash, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
+)]
pub enum PrincipalField {
#[serde(rename = "name")]
Name,
@@ -166,6 +248,7 @@ pub enum PrincipalValue {
String(String),
StringList(Vec<String>),
Integer(u64),
+ IntegerList(Vec<u64>),
}
impl PrincipalUpdate {
@@ -201,6 +284,33 @@ impl Display for PrincipalField {
}
impl PrincipalField {
+ pub fn id(&self) -> u8 {
+ match self {
+ PrincipalField::Name => 0,
+ PrincipalField::Type => 1,
+ PrincipalField::Quota => 2,
+ PrincipalField::Description => 3,
+ PrincipalField::Secrets => 4,
+ PrincipalField::Emails => 5,
+ PrincipalField::MemberOf => 6,
+ PrincipalField::Members => 7,
+ }
+ }
+
+ pub fn from_id(id: u8) -> Option<Self> {
+ match id {
+ 0 => Some(PrincipalField::Name),
+ 1 => Some(PrincipalField::Type),
+ 2 => Some(PrincipalField::Quota),
+ 3 => Some(PrincipalField::Description),
+ 4 => Some(PrincipalField::Secrets),
+ 5 => Some(PrincipalField::Emails),
+ 6 => Some(PrincipalField::MemberOf),
+ 7 => Some(PrincipalField::Members),
+ _ => None,
+ }
+ }
+
pub fn as_str(&self) -> &'static str {
match self {
PrincipalField::Name => "name",
@@ -224,24 +334,16 @@ fn deserialize_string(bytes: &mut Iter<'_, u8>) -> Option<String> {
String::from_utf8(string).ok()
}
-fn deserialize_string_list(bytes: &mut Iter<'_, u8>) -> Option<Vec<String>> {
- let len = bytes.next_leb128()?;
- let mut list = Vec::with_capacity(len);
- for _ in 0..len {
- list.push(deserialize_string(bytes)?);
- }
- Some(list)
-}
-
impl Type {
pub fn parse(value: &str) -> Option<Self> {
match value {
"individual" => Some(Type::Individual),
- "superuser" => Some(Type::Superuser),
"group" => Some(Type::Group),
"resource" => Some(Type::Resource),
"location" => Some(Type::Location),
"list" => Some(Type::List),
+ "tenant" => Some(Type::Tenant),
+ "superuser" => Some(Type::Individual), // legacy
_ => None,
}
}
@@ -252,18 +354,12 @@ impl Type {
1 => Type::Group,
2 => Type::Resource,
3 => Type::Location,
- 4 => Type::Superuser,
+ 4 => Type::Individual, // legacy
5 => Type::List,
+ 7 => Type::Tenant,
_ => Type::Other,
}
}
-
- pub fn into_base_type(self) -> Self {
- match self {
- Type::Superuser => Type::Individual,
- any => any,
- }
- }
}
impl FromStr for Type {
@@ -275,7 +371,6 @@ impl FromStr for Type {
}
pub trait SpecialSecrets {
- fn is_disabled(&self) -> bool;
fn is_otp_auth(&self) -> bool;
fn is_app_password(&self) -> bool;
fn is_password(&self) -> bool;
@@ -285,10 +380,6 @@ impl<T> SpecialSecrets for T
where
T: AsRef<str>,
{
- fn is_disabled(&self) -> bool {
- self.as_ref() == "$disabled$"
- }
-
fn is_otp_auth(&self) -> bool {
self.as_ref().starts_with("otpauth://")
}
@@ -298,6 +389,6 @@ where
}
fn is_password(&self) -> bool {
- !self.is_disabled() && !self.is_otp_auth() && !self.is_app_password()
+ !self.is_otp_auth() && !self.is_app_password()
}
}
diff --git a/crates/directory/src/backend/ldap/lookup.rs b/crates/directory/src/backend/ldap/lookup.rs
index 63baa697..1576de22 100644
--- a/crates/directory/src/backend/ldap/lookup.rs
+++ b/crates/directory/src/backend/ldap/lookup.rs
@@ -7,7 +7,10 @@
use ldap3::{Ldap, LdapConnAsync, Scope, SearchEntry};
use mail_send::Credentials;
-use crate::{backend::internal::manage::ManageDirectory, IntoError, Principal, QueryBy, Type};
+use crate::{
+ backend::internal::{manage::ManageDirectory, PrincipalField},
+ IntoError, Principal, QueryBy, Type,
+};
use super::{LdapDirectory, LdapMappings};
@@ -16,7 +19,7 @@ impl LdapDirectory {
&self,
by: QueryBy<'_>,
return_member_of: bool,
- ) -> trc::Result<Option<Principal<u32>>> {
+ ) -> trc::Result<Option<Principal>> {
let mut conn = self.pool.get().await.map_err(|err| err.into_error())?;
let mut account_id = None;
let account_name;
@@ -125,11 +128,11 @@ impl LdapDirectory {
.get_or_create_account_id(&account_name)
.await?;
}
- principal.name = account_name;
+ principal.append_str(PrincipalField::Name, account_name);
// Obtain groups
- if return_member_of && !principal.member_of.is_empty() {
- for member_of in principal.member_of.iter_mut() {
+ if return_member_of && principal.has_field(PrincipalField::MemberOf) {
+ for member_of in principal.iter_mut_str(PrincipalField::MemberOf) {
if member_of.contains('=') {
let (rs, _res) = conn
.search(
@@ -163,8 +166,8 @@ impl LdapDirectory {
.await
.map(Some)
} else {
- principal.member_of.clear();
- Ok(Some(principal.into()))
+ principal.remove(PrincipalField::MemberOf);
+ Ok(Some(principal))
}
}
@@ -370,7 +373,7 @@ impl LdapDirectory {
&self,
conn: &mut Ldap,
filter: &str,
- ) -> trc::Result<Option<Principal<String>>> {
+ ) -> trc::Result<Option<Principal>> {
conn.search(
&self.mappings.base_dn,
Scope::Subtree,
@@ -400,39 +403,47 @@ impl LdapDirectory {
}
impl LdapMappings {
- fn entry_to_principal(&self, entry: SearchEntry) -> Principal<String> {
+ fn entry_to_principal(&self, entry: SearchEntry) -> Principal {
let mut principal = Principal::default();
for (attr, value) in entry.attrs {
if self.attr_name.contains(&attr) {
- principal.name = value.into_iter().next().unwrap_or_default();
+ principal.set(
+ PrincipalField::Name,
+ value.into_iter().next().unwrap_or_default(),
+ );
} else if self.attr_secret.contains(&attr) {
- principal.secrets.extend(value);
+ for item in value {
+ principal.append_str(PrincipalField::Secrets, item);
+ }
} else if self.attr_email_address.contains(&attr) {
- for value in value {
- if principal.emails.is_empty() {
- principal.emails.push(value);
- } else {
- principal.emails.insert(0, value);
- }
+ for item in value {
+ principal.prepend_str(PrincipalField::Emails, item);
}
} else if self.attr_email_alias.contains(&attr) {
- principal.emails.extend(value);
+ for item in value {
+ principal.append_str(PrincipalField::Emails, item);
+ }
} else if let Some(idx) = self.attr_description.iter().position(|a| a == &attr) {
- if principal.description.is_none() || idx == 0 {
- principal.description = value.into_iter().next();
+ if !principal.has_field(PrincipalField::Description) || idx == 0 {
+ principal.set(
+ PrincipalField::Description,
+ value.into_iter().next().unwrap_or_default(),
+ );
}
} else if self.attr_groups.contains(&attr) {
- principal.member_of.extend(value);
+ for item in value {
+ principal.append_str(PrincipalField::MemberOf, item);
+ }
} else if self.attr_quota.contains(&attr) {
- if let Ok(quota) = value.into_iter().next().unwrap_or_default().parse() {
- principal.quota = quota;
+ if let Ok(quota) = value.into_iter().next().unwrap_or_default().parse::<u64>() {
+ principal.set(PrincipalField::Quota, quota);
}
} else if self.attr_type.contains(&attr) {
for value in value {
match value.to_ascii_lowercase().as_str() {
"admin" | "administrator" | "root" | "superuser" => {
- principal.typ = Type::Superuser
+ principal = principal.into_superuser();
}
"posixaccount" | "individual" | "person" | "inetorgperson" => {
principal.typ = Type::Individual
diff --git a/crates/directory/src/backend/memory/config.rs b/crates/directory/src/backend/memory/config.rs
index d3df6d86..2ed6bf08 100644
--- a/crates/directory/src/backend/memory/config.rs
+++ b/crates/directory/src/backend/memory/config.rs
@@ -7,7 +7,10 @@
use store::Store;
use utils::config::{utils::AsKey, Config};
-use crate::{backend::internal::manage::ManageDirectory, Principal, Type};
+use crate::{
+ backend::internal::{manage::ManageDirectory, PrincipalField},
+ Principal, Type,
+};
use super::{EmailType, MemoryDirectory};
@@ -34,12 +37,13 @@ impl MemoryDirectory {
let name = config
.value_require((prefix.as_str(), "principals", lookup_id, "name"))?
.to_string();
- let typ = match config.value((prefix.as_str(), "principals", lookup_id, "class")) {
- Some("individual") => Type::Individual,
- Some("admin") => Type::Superuser,
- Some("group") => Type::Group,
- _ => Type::Individual,
- };
+ let (typ, is_superuser) =
+ match config.value((prefix.as_str(), "principals", lookup_id, "class")) {
+ Some("individual") => (Type::Individual, false),
+ Some("admin") => (Type::Individual, true),
+ Some("group") => (Type::Group, false),
+ _ => (Type::Individual, false),
+ };
// Obtain id
let id = directory
@@ -57,14 +61,30 @@ impl MemoryDirectory {
})
.ok()?;
+ // Create principal
+ let mut principal = if is_superuser {
+ Principal {
+ id,
+ typ,
+ ..Default::default()
+ }
+ .into_superuser()
+ } else {
+ Principal {
+ id,
+ typ,
+ ..Default::default()
+ }
+ };
+
// Obtain group ids
- let mut member_of = Vec::new();
for group in config
.values((prefix.as_str(), "principals", lookup_id, "member-of"))
.map(|(_, s)| s.to_string())
.collect::<Vec<_>>()
{
- member_of.push(
+ principal.append_int(
+ PrincipalField::MemberOf,
directory
.data_store
.get_or_create_account_id(&group)
@@ -83,7 +103,6 @@ impl MemoryDirectory {
}
// Parse email addresses
- let mut emails = Vec::new();
for (pos, (_, email)) in config
.values((prefix.as_str(), "principals", lookup_id, "email"))
.enumerate()
@@ -102,7 +121,7 @@ impl MemoryDirectory {
directory.domains.insert(domain.to_lowercase());
}
- emails.push(email.to_lowercase());
+ principal.append_str(PrincipalField::Emails, email.to_lowercase());
}
// Parse mailing lists
@@ -119,23 +138,20 @@ impl MemoryDirectory {
}
}
- directory.principals.push(Principal {
- name: name.clone(),
- secrets: config
- .values((prefix.as_str(), "principals", lookup_id, "secret"))
- .map(|(_, v)| v.to_string())
- .collect(),
- typ,
- description: config
- .value((prefix.as_str(), "principals", lookup_id, "description"))
- .map(|v| v.to_string()),
- quota: config
- .property((prefix.as_str(), "principals", lookup_id, "quota"))
- .unwrap_or(0),
- member_of,
- id,
- emails,
- });
+ principal.set(PrincipalField::Name, name.clone());
+ for (_, secret) in config.values((prefix.as_str(), "principals", lookup_id, "secret")) {
+ principal.append_str(PrincipalField::Secrets, secret.to_string());
+ }
+ if let Some(description) =
+ config.value((prefix.as_str(), "principals", lookup_id, "description"))
+ {
+ principal.set(PrincipalField::Description, description.to_string());
+ }
+ if let Some(quota) =
+ config.property::<u64>((prefix.as_str(), "principals", lookup_id, "quota"))
+ {
+ principal.set(PrincipalField::Quota, quota);
+ }
}
Some(directory)
diff --git a/crates/directory/src/backend/memory/lookup.rs b/crates/directory/src/backend/memory/lookup.rs
index 979f70b1..60270097 100644
--- a/crates/directory/src/backend/memory/lookup.rs
+++ b/crates/directory/src/backend/memory/lookup.rs
@@ -6,16 +6,16 @@
use mail_send::Credentials;
-use crate::{Principal, QueryBy};
+use crate::{backend::internal::PrincipalField, Principal, QueryBy};
use super::{EmailType, MemoryDirectory};
impl MemoryDirectory {
- pub async fn query(&self, by: QueryBy<'_>) -> trc::Result<Option<Principal<u32>>> {
+ pub async fn query(&self, by: QueryBy<'_>) -> trc::Result<Option<Principal>> {
match by {
QueryBy::Name(name) => {
for principal in &self.principals {
- if principal.name == name {
+ if principal.name() == name {
return Ok(Some(principal.clone()));
}
}
@@ -35,7 +35,7 @@ impl MemoryDirectory {
};
for principal in &self.principals {
- if &principal.name == username {
+ if principal.name() == username {
return if principal.verify_secret(secret).await? {
Ok(Some(principal.clone()))
} else {
@@ -87,8 +87,10 @@ impl MemoryDirectory {
if let EmailType::List(uid) = item {
for principal in &self.principals {
if principal.id == *uid {
- if let Some(addr) = principal.emails.first() {
- result.push(addr.clone())
+ if let Some(addr) =
+ principal.iter_str(PrincipalField::Emails).next()
+ {
+ result.push(addr.to_string())
}
break;
}
diff --git a/crates/directory/src/backend/memory/mod.rs b/crates/directory/src/backend/memory/mod.rs
index fde9160b..d1971e1f 100644
--- a/crates/directory/src/backend/memory/mod.rs
+++ b/crates/directory/src/backend/memory/mod.rs
@@ -14,7 +14,7 @@ pub mod lookup;
#[derive(Debug)]
pub struct MemoryDirectory {
- principals: Vec<Principal<u32>>,
+ principals: Vec<Principal>,
emails_to_ids: AHashMap<String, Vec<EmailType>>,
pub(crate) data_store: Store,
domains: AHashSet<String>,
diff --git a/crates/directory/src/backend/smtp/lookup.rs b/crates/directory/src/backend/smtp/lookup.rs
index 8bf684ba..62285426 100644
--- a/crates/directory/src/backend/smtp/lookup.rs
+++ b/crates/directory/src/backend/smtp/lookup.rs
@@ -12,7 +12,7 @@ use crate::{IntoError, Principal, QueryBy};
use super::{SmtpClient, SmtpDirectory};
impl SmtpDirectory {
- pub async fn query(&self, query: QueryBy<'_>) -> trc::Result<Option<Principal<u32>>> {
+ pub async fn query(&self, query: QueryBy<'_>) -> trc::Result<Option<Principal>> {
if let QueryBy::Credentials(credentials) = query {
self.pool
.get()
@@ -93,7 +93,7 @@ impl SmtpClient {
async fn authenticate(
&mut self,
credentials: &Credentials<String>,
- ) -> trc::Result<Option<Principal<u32>>> {
+ ) -> trc::Result<Option<Principal>> {
match self
.client
.authenticate(credentials, &self.capabilities)
diff --git a/crates/directory/src/backend/sql/lookup.rs b/crates/directory/src/backend/sql/lookup.rs
index 1638af3f..e86b12e4 100644
--- a/crates/directory/src/backend/sql/lookup.rs
+++ b/crates/directory/src/backend/sql/lookup.rs
@@ -8,7 +8,10 @@ use mail_send::Credentials;
use store::{NamedRows, Rows, Value};
use trc::AddContext;
-use crate::{backend::internal::manage::ManageDirectory, Principal, QueryBy, Type};
+use crate::{
+ backend::internal::{manage::ManageDirectory, PrincipalField, PrincipalValue},
+ Principal, QueryBy, Type,
+};
use super::{SqlDirectory, SqlMappings};
@@ -17,7 +20,7 @@ impl SqlDirectory {
&self,
by: QueryBy<'_>,
return_member_of: bool,
- ) -> trc::Result<Option<Principal<u32>>> {
+ ) -> trc::Result<Option<Principal>> {
let mut account_id = None;
let account_name;
let mut secret = None;
@@ -99,22 +102,20 @@ impl SqlDirectory {
.await
.caused_by(trc::location!())?;
}
- principal.name = account_name;
+ principal.set(PrincipalField::Name, account_name);
// Obtain members
if return_member_of && !self.mappings.query_members.is_empty() {
for row in self
.store
- .query::<Rows>(
- &self.mappings.query_members,
- vec![principal.name.clone().into()],
- )
+ .query::<Rows>(&self.mappings.query_members, vec![principal.name().into()])
.await
.caused_by(trc::location!())?
.rows
{
if let Some(Value::Text(account_id)) = row.values.first() {
- principal.member_of.push(
+ principal.append_int(
+ PrincipalField::MemberOf,
self.data_store
.get_or_create_account_id(account_id)
.await
@@ -126,15 +127,16 @@ impl SqlDirectory {
// Obtain emails
if !self.mappings.query_emails.is_empty() {
- principal.emails = self
- .store
- .query::<Rows>(
- &self.mappings.query_emails,
- vec![principal.name.clone().into()],
- )
- .await
- .caused_by(trc::location!())?
- .into();
+ principal.set(
+ PrincipalField::Emails,
+ PrincipalValue::StringList(
+ self.store
+ .query::<Rows>(&self.mappings.query_emails, vec![principal.name().into()])
+ .await
+ .caused_by(trc::location!())?
+ .into(),
+ ),
+ );
}
Ok(Some(principal))
@@ -204,7 +206,7 @@ impl SqlDirectory {
}
impl SqlMappings {
- pub fn row_to_principal(&self, rows: NamedRows) -> trc::Result<Principal<u32>> {
+ pub fn row_to_principal(&self, rows: NamedRows) -> trc::Result<Principal> {
let mut principal = Principal::default();
if let Some(row) = rows.rows.into_iter().next() {
@@ -215,22 +217,25 @@ impl SqlMappings {
.any(|c| name.eq_ignore_ascii_case(c))
{
if let Value::Text(secret) = value {
- principal.secrets.push(secret.into_owned());
+ principal.append_str(PrincipalField::Secrets, secret.into_owned());
}
} else if name.eq_ignore_ascii_case(&self.column_type) {
match value.to_str().as_ref() {
"individual" | "person" | "user" => principal.typ = Type::Individual,
"group" => principal.typ = Type::Group,
- "admin" | "superuser" | "administrator" => principal.typ = Type::Superuser,
+ "admin" | "superuser" | "administrator" => {
+ principal.typ = Type::Individual;
+ principal = principal.into_superuser();
+ }
_ => (),
}
} else if name.eq_ignore_ascii_case(&self.column_description) {
if let Value::Text(text) = value {
- principal.description = text.into_owned().into();
+ principal.set(PrincipalField::Description, text.into_owned());
}
} else if name.eq_ignore_ascii_case(&self.column_quota) {
if let Value::Integer(quota) = value {
- principal.quota = quota as u64;
+ principal.set(PrincipalField::Quota, quota as u64);
}
}
}
diff --git a/crates/directory/src/core/dispatch.rs b/crates/directory/src/core/dispatch.rs
index f907b32f..77108e3e 100644
--- a/crates/directory/src/core/dispatch.rs
+++ b/crates/directory/src/core/dispatch.rs
@@ -15,7 +15,7 @@ impl Directory {
&self,
by: QueryBy<'_>,
return_member_of: bool,
- ) -> trc::Result<Option<Principal<u32>>> {
+ ) -> trc::Result<Option<Principal>> {
match &self.store {
DirectoryInner::Internal(store) => store.query(by, return_member_of).await,
DirectoryInner::Ldap(store) => store.query(by, return_member_of).await,
diff --git a/crates/directory/src/core/mod.rs b/crates/directory/src/core/mod.rs
index ae094f1e..c9ee3326 100644
--- a/crates/directory/src/core/mod.rs
+++ b/crates/directory/src/core/mod.rs
@@ -7,4 +7,5 @@
pub mod cache;
pub mod config;
pub mod dispatch;
+pub mod principal;
pub mod secret;
diff --git a/crates/directory/src/core/principal.rs b/crates/directory/src/core/principal.rs
new file mode 100644
index 00000000..bdec5d7a
--- /dev/null
+++ b/crates/directory/src/core/principal.rs
@@ -0,0 +1,490 @@
+/*
+ * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art>
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
+ */
+
+use std::collections::hash_map::Entry;
+
+use store::U64_LEN;
+
+use crate::{
+ backend::internal::{PrincipalField, PrincipalValue},
+ Principal, Type,
+};
+
+impl Principal {
+ pub fn new(id: u32, typ: Type) -> Self {
+ Self {
+ id,
+ typ,
+ ..Default::default()
+ }
+ }
+
+ pub fn id(&self) -> u32 {
+ self.id
+ }
+
+ pub fn typ(&self) -> Type {
+ self.typ
+ }
+
+ pub fn name(&self) -> &str {
+ self.get_str(PrincipalField::Name).unwrap_or_default()
+ }
+
+ pub fn has_name(&self) -> bool {
+ self.fields.contains_key(&PrincipalField::Name)
+ }
+
+ pub fn quota(&self) -> u64 {
+ self.get_int(PrincipalField::Quota).unwrap_or_default()
+ }
+
+ pub fn description(&self) -> Option<&str> {
+ self.get_str(PrincipalField::Description)
+ }
+
+ pub fn get_str(&self, key: PrincipalField) -> Option<&str> {
+ self.fields.get(&key).and_then(|v| v.as_str())
+ }
+
+ pub fn get_int(&self, key: PrincipalField) -> Option<u64> {
+ self.fields.get(&key).and_then(|v| v.as_int())
+ }
+
+ pub fn take(&mut self, key: PrincipalField) -> Option<PrincipalValue> {
+ self.fields.remove(&key)
+ }
+
+ pub fn take_str(&mut self, key: PrincipalField) -> Option<String> {
+ self.take(key).and_then(|v| match v {
+ PrincipalValue::String(s) => Some(s),
+ PrincipalValue::StringList(l) => l.into_iter().next(),
+ PrincipalValue::Integer(i) => Some(i.to_string()),
+ PrincipalValue::IntegerList(l) => l.into_iter().next().map(|i| i.to_string()),
+ })
+ }
+
+ pub fn take_str_array(&mut self, key: PrincipalField) -> Option<Vec<String>> {
+ self.take(key).map(|v| v.into_str_array())
+ }
+
+ pub fn take_int_array(&mut self, key: PrincipalField) -> Option<Vec<u64>> {
+ self.take(key).map(|v| v.into_int_array())
+ }
+
+ pub fn iter_str(
+ &self,
+ key: PrincipalField,
+ ) -> Box<dyn Iterator<Item = &String> + Sync + Send + '_> {
+ self.fields
+ .get(&key)
+ .map(|v| v.iter_str())
+ .unwrap_or_else(|| Box::new(std::iter::empty()))
+ }
+
+ pub fn iter_mut_str(
+ &mut self,
+ key: PrincipalField,
+ ) -> Box<dyn Iterator<Item = &mut String> + Sync + Send + '_> {
+ self.fields
+ .get_mut(&key)
+ .map(|v| v.iter_mut_str())
+ .unwrap_or_else(|| Box::new(std::iter::empty()))
+ }
+
+ pub fn iter_int(
+ &self,
+ key: PrincipalField,
+ ) -> Box<dyn Iterator<Item = u64> + Sync + Send + '_> {
+ self.fields
+ .get(&key)
+ .map(|v| v.iter_int())
+ .unwrap_or_else(|| Box::new(std::iter::empty()))
+ }
+
+ pub fn iter_mut_int(
+ &mut self,
+ key: PrincipalField,
+ ) -> Box<dyn Iterator<Item = &mut u64> + Sync + Send + '_> {
+ self.fields
+ .get_mut(&key)
+ .map(|v| v.iter_mut_int())
+ .unwrap_or_else(|| Box::new(std::iter::empty()))
+ }
+
+ pub fn append_int(&mut self, key: PrincipalField, value: impl Into<u64>) -> &mut Self {
+ let value = value.into();
+ match self.fields.entry(key) {
+ Entry::Occupied(v) => {
+ let v = v.into_mut();
+
+ match v {
+ PrincipalValue::IntegerList(v) => {
+ v.push(value);
+ }
+ PrincipalValue::Integer(i) => {
+ *v = PrincipalValue::IntegerList(vec![*i, value]);
+ }
+ PrincipalValue::String(s) => {
+ *v =
+ PrincipalValue::IntegerList(vec![s.parse().unwrap_or_default(), value]);
+ }
+ PrincipalValue::StringList(l) => {
+ *v = PrincipalValue::IntegerList(
+ l.iter()
+ .map(|s| s.parse().unwrap_or_default())
+ .chain(std::iter::once(value))
+ .collect(),
+ );
+ }
+ }
+ }
+ Entry::Vacant(v) => {
+ v.insert(PrincipalValue::IntegerList(vec![value]));
+ }
+ }
+
+ self
+ }
+
+ pub fn append_str(&mut self, key: PrincipalField, value: impl Into<String>) -> &mut Self {
+ let value = value.into();
+ match self.fields.entry(key) {
+ Entry::Occupied(v) => {
+ let v = v.into_mut();
+
+ match v {
+ PrincipalValue::StringList(v) => {
+ v.push(value);
+ }
+ PrincipalValue::String(s) => {
+ *v = PrincipalValue::StringList(vec![std::mem::take(s), value]);
+ }
+ PrincipalValue::Integer(i) => {
+ *v = PrincipalValue::StringList(vec![i.to_string(), value]);
+ }
+ PrincipalValue::IntegerList(l) => {
+ *v = PrincipalValue::StringList(
+ l.iter()
+ .map(|i| i.to_string())
+ .chain(std::iter::once(value))
+ .collect(),
+ );
+ }
+ }
+ }
+ Entry::Vacant(v) => {
+ v.insert(PrincipalValue::StringList(vec![value]));
+ }
+ }
+ self
+ }
+
+ pub fn prepend_str(&mut self, key: PrincipalField, value: impl Into<String>) -> &mut Self {
+ let value = value.into();
+ match self.fields.entry(key) {
+ Entry::Occupied(v) => {
+ let v = v.into_mut();
+
+ match v {
+ PrincipalValue::StringList(v) => {
+ v.insert(0, value);
+ }
+ PrincipalValue::String(s) => {
+ *v = PrincipalValue::StringList(vec![value, std::mem::take(s)]);
+ }
+ PrincipalValue::Integer(i) => {
+ *v = PrincipalValue::StringList(vec![value, i.to_string()]);
+ }
+ PrincipalValue::IntegerList(l) => {
+ *v = PrincipalValue::StringList(
+ std::iter::once(value)
+ .chain(l.iter().map(|i| i.to_string()))
+ .collect(),
+ );
+ }
+ }
+ }
+ Entry::Vacant(v) => {
+ v.insert(PrincipalValue::StringList(vec![value]));
+ }
+ }
+ self
+ }
+
+ pub fn set(&mut self, key: PrincipalField, value: impl Into<PrincipalValue>) -> &mut Self {
+ self.fields.insert(key, value.into());
+ self
+ }
+
+ pub fn with_field(mut self, key: PrincipalField, value: impl Into<PrincipalValue>) -> Self {
+ self.set(key, value);
+ self
+ }
+
+ pub fn with_opt_field(
+ mut self,
+ key: PrincipalField,
+ value: Option<impl Into<PrincipalValue>>,
+ ) -> Self {
+ if let Some(value) = value {
+ self.set(key, value);
+ }
+ self
+ }
+
+ pub fn has_field(&self, key: PrincipalField) -> bool {
+ self.fields.contains_key(&key)
+ }
+
+ pub fn has_str_value(&self, key: PrincipalField, value: &str) -> bool {
+ self.fields.get(&key).map_or(false, |v| match v {
+ PrincipalValue::String(v) => v == value,
+ PrincipalValue::StringList(l) => l.iter().any(|v| v == value),
+ PrincipalValue::Integer(_) | PrincipalValue::IntegerList(_) => false,
+ })
+ }
+
+ pub fn has_int_value(&self, key: PrincipalField, value: u64) -> bool {
+ self.fields.get(&key).map_or(false, |v| match v {
+ PrincipalValue::Integer(v) => *v == value,
+ PrincipalValue::IntegerList(l) => l.iter().any(|v| *v == value),
+ PrincipalValue::String(_) | PrincipalValue::StringList(_) => false,
+ })
+ }
+
+ pub fn field_len(&self, key: PrincipalField) -> usize {
+ self.fields.get(&key).map_or(0, |v| match v {
+ PrincipalValue::String(_) => 1,
+ PrincipalValue::StringList(l) => l.len(),
+ PrincipalValue::Integer(_) => 1,
+ PrincipalValue::IntegerList(l) => l.len(),
+ })
+ }
+
+ pub fn remove(&mut self, key: PrincipalField) -> Option<PrincipalValue> {
+ self.fields.remove(&key)
+ }
+
+ pub fn retain_str<F>(&mut self, key: PrincipalField, mut f: F)
+ where
+ F: FnMut(&String) -> bool,
+ {
+ if let Some(value) = self.fields.get_mut(&key) {
+ match value {
+ PrincipalValue::String(s) => {
+ if !f(s) {
+ self.fields.remove(&key);
+ }
+ }
+ PrincipalValue::StringList(l) => {
+ l.retain(f);
+ if l.is_empty() {
+ self.fields.remove(&key);
+ }
+ }
+ _ => {}
+ }
+ }
+ }
+
+ pub fn retain_int<F>(&mut self, key: PrincipalField, mut f: F)
+ where
+ F: FnMut(&u64) -> bool,
+ {
+ if let Some(value) = self.fields.get_mut(&key) {
+ match value {
+ PrincipalValue::Integer(i) => {
+ if !f(i) {
+ self.fields.remove(&key);
+ }
+ }
+ PrincipalValue::IntegerList(l) => {
+ l.retain(f);
+ if l.is_empty() {
+ self.fields.remove(&key);
+ }
+ }
+ _ => {}
+ }
+ }
+ }
+
+ pub fn fallback_admin(fallback_pass: impl Into<String>) -> Self {
+ Principal {
+ id: u32::MAX,
+ typ: Type::Individual,
+ ..Default::default()
+ }
+ .with_field(PrincipalField::Name, "Fallback Administrator")
+ .with_field(
+ PrincipalField::Secrets,
+ PrincipalValue::String(fallback_pass.into()),
+ )
+ .into_superuser()
+ }
+
+ pub fn into_superuser(mut self) -> Self {
+ let todo = "add role";
+ self
+ }
+}
+
+impl PrincipalValue {
+ pub fn as_str(&self) -> Option<&str> {
+ match self {
+ PrincipalValue::String(v) => Some(v.as_str()),
+ PrincipalValue::StringList(v) => v.first().map(|s| s.as_str()),
+ _ => None,
+ }
+ }
+
+ pub fn as_int(&self) -> Option<u64> {
+ match self {
+ PrincipalValue::Integer(v) => Some(*v),
+ PrincipalValue::IntegerList(v) => v.first().copied(),
+ _ => None,
+ }
+ }
+
+ pub fn iter_str(&self) -> Box<dyn Iterator<Item = &String> + Sync + Send + '_> {
+ match self {
+ PrincipalValue::String(v) => Box::new(std::iter::once(v)),
+ PrincipalValue::StringList(v) => Box::new(v.iter()),
+ _ => Box::new(std::iter::empty()),
+ }
+ }
+
+ pub fn iter_mut_str(&mut self) -> Box<dyn Iterator<Item = &mut String> + Sync + Send + '_> {
+ match self {
+ PrincipalValue::String(v) => Box::new(std::iter::once(v)),
+ PrincipalValue::StringList(v) => Box::new(v.iter_mut()),
+ _ => Box::new(std::iter::empty()),
+ }
+ }
+
+ pub fn iter_int(&self) -> Box<dyn Iterator<Item = u64> + Sync + Send + '_> {
+ match self {
+ PrincipalValue::Integer(v) => Box::new(std::iter::once(*v)),
+ PrincipalValue::IntegerList(v) => Box::new(v.iter().copied()),
+ _ => Box::new(std::iter::empty()),
+ }
+ }
+
+ pub fn iter_mut_int(&mut self) -> Box<dyn Iterator<Item = &mut u64> + Sync + Send + '_> {
+ match self {
+ PrincipalValue::Integer(v) => Box::new(std::iter::once(v)),
+ PrincipalValue::IntegerList(v) => Box::new(v.iter_mut()),
+ _ => Box::new(std::iter::empty()),
+ }
+ }
+
+ pub fn into_array(self) -> Self {
+ match self {
+ PrincipalValue::String(v) => PrincipalValue::StringList(vec![v]),
+ PrincipalValue::Integer(v) => PrincipalValue::IntegerList(vec![v]),
+ v => v,
+ }
+ }
+
+ pub fn into_str_array(self) -> Vec<String> {
+ match self {
+ PrincipalValue::StringList(v) => v,
+ PrincipalValue::String(v) => vec![v],
+ PrincipalValue::Integer(v) => vec![v.to_string()],
+ PrincipalValue::IntegerList(v) => v.into_iter().map(|v| v.to_string()).collect(),
+ }
+ }
+
+ pub fn into_int_array(self) -> Vec<u64> {
+ match self {
+ PrincipalValue::IntegerList(v) => v,
+ PrincipalValue::Integer(v) => vec![v],
+ PrincipalValue::String(v) => vec![v.parse().unwrap_or_default()],
+ PrincipalValue::StringList(v) => v
+ .into_iter()
+ .map(|v| v.parse().unwrap_or_default())
+ .collect(),
+ }
+ }
+
+ pub fn serialized_size(&self) -> usize {
+ match self {
+ PrincipalValue::String(s) => s.len() + 2,
+ PrincipalValue::StringList(s) => s.iter().map(|s| s.len() + 2).sum(),
+ PrincipalValue::Integer(_) => U64_LEN,
+ PrincipalValue::IntegerList(l) => l.len() * U64_LEN,
+ }
+ }
+}
+
+impl From<u64> for PrincipalValue {
+ fn from(v: u64) -> Self {
+ Self::Integer(v)
+ }
+}
+
+impl From<String> for PrincipalValue {
+ fn from(v: String) -> Self {
+ Self::String(v)
+ }
+}
+
+impl From<&str> for PrincipalValue {
+ fn from(v: &str) -> Self {
+ Self::String(v.to_string())
+ }
+}
+
+impl From<Vec<String>> for PrincipalValue {
+ fn from(v: Vec<String>) -> Self {
+ Self::StringList(v)
+ }
+}
+
+impl From<Vec<u64>> for PrincipalValue {
+ fn from(v: Vec<u64>) -> Self {
+ Self::IntegerList(v)
+ }
+}
+
+impl From<u32> for PrincipalValue {
+ fn from(v: u32) -> Self {
+ Self::Integer(v as u64)
+ }
+}
+
+impl From<Vec<u32>> for PrincipalValue {
+ fn from(v: Vec<u32>) -> Self {
+ Self::IntegerList(v.into_iter().map(|v| v as u64).collect())
+ }
+}
+
+impl Type {
+ pub fn to_jmap(&self) -> &'static str {
+ match self {
+ Self::Individual => "individual",
+ Self::Group => "group",
+ Self::Resource => "resource",
+ Self::Location => "location",
+ Self::Other => "other",
+ Self::List => "list",
+ Self::Tenant => "tenant",
+ }
+ }
+
+ pub fn as_str(&self) -> &'static str {
+ match self {
+ Self::Individual => "Individual",
+ Self::Group => "Group",
+ Self::Resource => "Resource",
+ Self::Location => "Location",
+ Self::Tenant => "Tenant",
+ Self::List => "List",
+ Self::Other => "Other",
+ }
+ }
+}
diff --git a/crates/directory/src/core/secret.rs b/crates/directory/src/core/secret.rs
index 8a07c10a..cbe27d6a 100644
--- a/crates/directory/src/core/secret.rs
+++ b/crates/directory/src/core/secret.rs
@@ -18,10 +18,11 @@ use sha2::Sha512;
use tokio::sync::oneshot;
use totp_rs::TOTP;
+use crate::backend::internal::PrincipalField;
use crate::backend::internal::SpecialSecrets;
use crate::Principal;
-impl<T: serde::Serialize + serde::de::DeserializeOwned> Principal<T> {
+impl Principal {
pub async fn verify_secret(&self, mut code: &str) -> trc::Result<bool> {
let mut totp_token = None;
let mut is_totp_token_missing = false;
@@ -30,12 +31,10 @@ impl<T: serde::Serialize + serde::de::DeserializeOwned> Principal<T> {
let mut is_authenticated = false;
let mut is_app_authenticated = false;
- for secret in &self.secrets {
- if secret.is_disabled() {
- // Account is disabled, no need to check further
+ let todo = "validate authenticate permission";
- return Ok(false);
- } else if secret.is_otp_auth() {
+ for secret in self.iter_str(PrincipalField::Secrets) {
+ if secret.is_otp_auth() {
if !is_totp_verified && !is_totp_token_missing {
is_totp_required = true;
@@ -99,7 +98,7 @@ impl<T: serde::Serialize + serde::de::DeserializeOwned> Principal<T> {
} else {
if is_totp_verified {
// TOTP URL appeared after password hash in secrets list
- for secret in &self.secrets {
+ for secret in self.iter_str(PrincipalField::Secrets) {
if secret.is_password() && verify_secret_hash(secret, code).await? {
return Ok(true);
}
diff --git a/crates/directory/src/lib.rs b/crates/directory/src/lib.rs
index bb6e1e99..07d320d3 100644
--- a/crates/directory/src/lib.rs
+++ b/crates/directory/src/lib.rs
@@ -10,6 +10,7 @@ use std::{fmt::Debug, sync::Arc};
use ahash::AHashMap;
use backend::{
imap::{ImapDirectory, ImapError},
+ internal::{PrincipalField, PrincipalValue},
ldap::LdapDirectory,
memory::MemoryDirectory,
smtp::SmtpDirectory,
@@ -18,6 +19,7 @@ use backend::{
use deadpool::managed::PoolError;
use ldap3::LdapError;
use mail_send::Credentials;
+use proc_macros::EnumMethods;
use store::Store;
pub mod backend;
@@ -28,24 +30,12 @@ pub struct Directory {
pub cache: Option<CachedDirectory>,
}
-#[derive(Debug, Default, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
-pub struct Principal<T> {
- #[serde(default, skip)]
- pub id: u32,
- #[serde(rename = "type")]
- pub typ: Type,
- #[serde(default)]
- pub quota: u64,
- pub name: String,
- #[serde(default)]
- pub secrets: Vec<String>,
- #[serde(default)]
- pub emails: Vec<String>,
- #[serde(default)]
- #[serde(rename = "memberOf")]
- pub member_of: Vec<T>,
- #[serde(default, skip_serializing_if = "Option::is_none")]
- pub description: Option<String>,
+#[derive(Debug, Default, Clone, PartialEq, Eq)]
+pub struct Principal {
+ pub(crate) id: u32,
+ pub(crate) typ: Type,
+
+ pub(crate) fields: AHashMap<PrincipalField, PrincipalValue>,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
@@ -59,14 +49,178 @@ pub enum Type {
Resource = 2,
#[serde(rename = "location")]
Location = 3,
- #[serde(rename = "superuser")]
- Superuser = 4,
#[serde(rename = "list")]
List = 5,
#[serde(rename = "other")]
Other = 6,
+ #[serde(rename = "tenant")]
+ Tenant = 7,
}
+#[derive(
+ Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize, EnumMethods,
+)]
+#[serde(rename_all = "camelCase")]
+pub enum Permission {
+ // Admin
+ Impersonate,
+ UnlimitedRequests,
+ UnlimitedUploads,
+ DeleteSystemFolders,
+ MessageQueueList,
+ MessageQueueGet,
+ MessageQueueUpdate,
+ MessageQueueDelete,
+ OutgoingReportList,
+ OutgoingReportGet,
+ OutgoingReportDelete,
+ IncomingReportList,
+ IncomingReportGet,
+ IncomingReportDelete,
+ SettingsList,
+ SettingsUpdate,
+ SettingsDelete,
+ SettingsReload,
+ PrincipalList,
+ PrincipalGet,
+ PrincipalUpdate,
+ PrincipalDelete,
+ PrincipalCreate,
+ DomainList,
+ DomainGet,
+ DomainCreate,
+ DomainUpdate,
+ DomainDelete,
+ BlobFetch,
+ PurgeBlobStore,
+ PurgeDataStore,
+ PurgeLookupStore,
+ PurgeAccount,
+ Undelete,
+ DkimSignatureCreate,
+ DkimSignatureGet,
+ UpdateSpamFilter,
+ UpdateWebadmin,
+ LogsView,
+ SieveRun,
+ Restart,
+ TracingList,
+ TracingGet,
+ TracingLive,
+ MetricsList,
+ MetricsLive,
+
+ // Generic
+ Authenticate,
+ AuthenticateOauth,
+
+ // Account Management
+ ManageEncryption,
+ ManagePasswords,
+
+ // JMAP
+ JmapEmailGet,
+ JmapMailboxGet,
+ JmapThreadGet,
+ JmapIdentityGet,
+ JmapEmailSubmissionGet,
+ JmapPushSubscriptionGet,
+ JmapSieveScriptGet,
+ JmapVacationResponseGet,
+ JmapPrincipalGet,
+ JmapQuotaGet,
+ JmapBlobGet,
+ JmapEmailSet,
+ JmapMailboxSet,
+ JmapIdentitySet,
+ JmapEmailSubmissionSet,
+ JmapPushSubscriptionSet,
+ JmapSieveScriptSet,
+ JmapVacationResponseSet,
+ JmapEmailChanges,
+ JmapMailboxChanges,
+ JmapThreadChanges,
+ JmapIdentityChanges,
+ JmapEmailSubmissionChanges,
+ JmapQuotaChanges,
+ JmapEmailCopy,
+ JmapBlobCopy,
+ JmapEmailImport,
+ JmapEmailParse,
+ JmapEmailQueryChanges,
+ JmapMailboxQueryChanges,
+ JmapEmailSubmissionQueryChanges,
+ JmapSieveScriptQueryChanges,
+ JmapPrincipalQueryChanges,
+ JmapQuotaQueryChanges,
+ JmapEmailQuery,
+ JmapMailboxQuery,
+ JmapEmailSubmissionQuery,
+ JmapSieveScriptQuery,
+ JmapPrincipalQuery,
+ JmapQuotaQuery,
+ JmapSearchSnippet,
+ JmapSieveScriptValidate,
+ JmapBlobLookup,
+ JmapBlobUpload,
+ JmapEcho,
+
+ // IMAP
+ ImapAuthenticate,
+ ImapAclGet,
+ ImapAclSet,
+ ImapMyRights,
+ ImapListRights,
+ ImapAppend,
+ ImapCapability,
+ ImapId,
+ ImapCopy,
+ ImapMove,
+ ImapCreate,
+ ImapDelete,
+ ImapEnable,
+ ImapExpunge,
+ ImapFetch,
+ ImapIdle,
+ ImapList,
+ ImapLsub,
+ ImapNamespace,
+ ImapRename,
+ ImapSearch,
+ ImapSort,
+ ImapSelect,
+ ImapExamine,
+ ImapStatus,
+ ImapStore,
+ ImapSubscribe,
+ ImapThread,
+
+ // SMTP
+ SmtpAuthenticate,
+
+ // POP3
+ Pop3Authenticate,
+ Pop3List,
+ Pop3Uidl,
+ Pop3Stat,
+ Pop3Retr,
+ Pop3Dele,
+
+ // ManageSieve
+ SieveAuthenticate,
+ SieveListScripts,
+ SieveSetActive,
+ SieveGetScript,
+ SievePutScript,
+ SieveDeleteScript,
+ SieveRenameScript,
+ SieveCheckScript,
+ SieveHaveSpace,
+}
+
+pub const PERMISSION_BITMAP_SIZE: usize =
+ (Permission::COUNT + std::mem::size_of::<usize>() - 1) / std::mem::size_of::<usize>();
+
pub enum DirectoryInner {
Internal(Store),
Ldap(LdapDirectory),
@@ -82,20 +236,6 @@ pub enum QueryBy<'x> {
Credentials(&'x Credentials<String>),
}
-impl<T: serde::Serialize + serde::de::DeserializeOwned> Principal<T> {
- pub fn name(&self) -> &str {
- &self.name
- }
-
- pub fn has_name(&self) -> bool {
- !self.name.is_empty()
- }
-
- pub fn description(&self) -> Option<&str> {
- self.description.as_deref()
- }
-}
-
impl Default for Directory {
fn default() -> Self {
Self {
@@ -111,57 +251,11 @@ impl Debug for Directory {
}
}
-impl Type {
- pub fn to_jmap(&self) -> &'static str {
- match self {
- Self::Individual | Self::Superuser => "individual",
- Self::Group => "group",
- Self::Resource => "resource",
- Self::Location => "location",
- Self::Other => "other",
- Self::List => "list",
- }
- }
-
- pub fn as_str(&self) -> &'static str {
- match self {
- Self::Individual => "Individual",
- Self::Group => "Group",
- Self::Resource => "Resource",
- Self::Location => "Location",
- Self::Superuser => "Superuser",
- Self::List => "List",
- Self::Other => "Other",
- }
- }
-}
-
#[derive(Default, Clone, Debug)]
pub struct Directories {
pub directories: AHashMap<String, Arc<Directory>>,
}
-impl Principal<u32> {
- pub fn fallback_admin(fallback_pass: impl Into<String>) -> Self {
- Principal {
- id: u32::MAX,
- typ: Type::Superuser,
- quota: 0,
- name: "Fallback Administrator".to_string(),
- secrets: vec![fallback_pass.into()],
- ..Default::default()
- }
- }
-}
-
-impl<T: Ord> Principal<T> {
- pub fn into_sorted(mut self) -> Self {
- self.member_of.sort_unstable();
- self.emails.sort_unstable();
- self
- }
-}
-
trait IntoError {
fn into_error(self) -> trc::Error;
}
diff --git a/crates/imap-proto/src/protocol/mod.rs b/crates/imap-proto/src/protocol/mod.rs
index ec8caa5f..5aacec43 100644
--- a/crates/imap-proto/src/protocol/mod.rs
+++ b/crates/imap-proto/src/protocol/mod.rs
@@ -501,9 +501,12 @@ impl SerializeResponse for trc::Error {
Some(ResponseCode::NonExistent.as_str())
}
trc::EventType::Store(_) => Some(ResponseCode::ContactAdmin.as_str()),
- trc::EventType::Limit(trc::LimitEvent::Quota) => Some(ResponseCode::OverQuota.as_str()),
+ trc::EventType::Limit(trc::LimitEvent::Quota) => {
+ Some(ResponseCode::OverQuota.as_str())
+ }
trc::EventType::Limit(_) => Some(ResponseCode::Limit.as_str()),
trc::EventType::Auth(_) => Some(ResponseCode::AuthenticationFailed.as_str()),
+ trc::EventType::Security(_) => Some(ResponseCode::AuthorizationFailed.as_str()),
_ => None,
})
{
diff --git a/crates/imap/src/core/mailbox.rs b/crates/imap/src/core/mailbox.rs
index 519323d5..abed8c8a 100644
--- a/crates/imap/src/core/mailbox.rs
+++ b/crates/imap/src/core/mailbox.rs
@@ -8,7 +8,7 @@ use common::{
config::jmap::settings::SpecialUse,
listener::{limiter::InFlight, SessionStream},
};
-use directory::QueryBy;
+use directory::{backend::internal::PrincipalField, QueryBy};
use imap_proto::protocol::list::Attribute;
use jmap::{
auth::{acl::EffectiveAcl, AccessToken},
@@ -28,7 +28,7 @@ use super::{Account, AccountId, Mailbox, MailboxId, MailboxSync, Session, Sessio
impl<T: SessionStream> SessionData<T> {
pub async fn new(
session: &Session<T>,
- access_token: &AccessToken,
+ access_token: Arc<AccessToken>,
in_flight: Option<InFlight>,
) -> trc::Result<Self> {
let mut session = SessionData {
@@ -39,12 +39,14 @@ impl<T: SessionStream> SessionData<T> {
session_id: session.session_id,
mailboxes: Mutex::new(vec![]),
state: access_token.state().into(),
+ access_token,
in_flight,
};
+ let access_token = session.access_token.clone();
// Fetch mailboxes for the main account
let mut mailboxes = vec![session
- .fetch_account_mailboxes(session.account_id, None, access_token)
+ .fetch_account_mailboxes(session.account_id, None, &access_token)
.await
.caused_by(trc::location!())?];
@@ -65,11 +67,11 @@ impl<T: SessionStream> SessionData<T> {
.query(QueryBy::Id(account_id), false)
.await
.unwrap_or_default()
- .map(|p| p.name)
+ .and_then(|mut p| p.take_str(PrincipalField::Name))
.unwrap_or_else(|| Id::from(account_id).to_string())
)
.into(),
- access_token,
+ &access_token,
)
.await
.caused_by(trc::location!())?,
@@ -389,8 +391,8 @@ impl<T: SessionStream> SessionData<T> {
.directory
.query(QueryBy::Id(account_id), false)
.await
- .unwrap_or_default()
- .map(|p| p.name)
+ .caused_by(trc::location!())?
+ .and_then(|mut p| p.take_str(PrincipalField::Name))
.unwrap_or_else(|| Id::from(account_id).to_string())
);
added_accounts.push(
@@ -495,7 +497,7 @@ impl<T: SessionStream> SessionData<T> {
.query(QueryBy::Id(account_id), false)
.await
.caused_by(trc::location!())?
- .map(|p| p.name)
+ .and_then(|mut p| p.take_str(PrincipalField::Name))
.unwrap_or_else(|| Id::from(account_id).to_string())
)
.into()
diff --git a/crates/imap/src/core/mod.rs b/crates/imap/src/core/mod.rs
index 92f812ae..d6834906 100644
--- a/crates/imap/src/core/mod.rs
+++ b/crates/imap/src/core/mod.rs
@@ -82,6 +82,7 @@ pub struct Session<T: SessionStream> {
pub struct SessionData<T: SessionStream> {
pub account_id: u32,
+ pub access_token: Arc<AccessToken>,
pub jmap: JMAP,
pub imap: Arc<Inner>,
pub session_id: u64,
@@ -239,6 +240,7 @@ impl<T: SessionStream> SessionData<T> {
stream_tx: new_stream,
state: self.state,
in_flight: self.in_flight,
+ access_token: self.access_token,
}
}
}
diff --git a/crates/imap/src/op/acl.rs b/crates/imap/src/op/acl.rs
index 92d39afa..499da72b 100644
--- a/crates/imap/src/op/acl.rs
+++ b/crates/imap/src/op/acl.rs
@@ -7,7 +7,7 @@
use std::{sync::Arc, time::Instant};
use common::listener::SessionStream;
-use directory::QueryBy;
+use directory::{backend::internal::PrincipalField, Permission, QueryBy};
use imap_proto::{
protocol::acl::{
Arguments, GetAclResponse, ListRightsResponse, ModRightsOp, MyRightsResponse, Rights,
@@ -36,13 +36,16 @@ use trc::AddContext;
use utils::map::bitmap::Bitmap;
use crate::{
- core::{MailboxId, Session, SessionData},
+ core::{MailboxId, Session, SessionData, State},
op::ImapContext,
spawn_op,
};
impl<T: SessionStream> Session<T> {
pub async fn handle_get_acl(&mut self, request: Request<Command>) -> trc::Result<()> {
+ // Validate access
+ self.assert_has_permission(Permission::ImapAuthenticate)?;
+
let op_start = Instant::now();
let arguments = request.parse_acl(self.version)?;
let is_rev2 = self.version.is_rev2();
@@ -69,7 +72,7 @@ impl<T: SessionStream> Session<T> {
.query(QueryBy::Id(item.account_id), false)
.await
.imap_ctx(&arguments.tag, trc::location!())?
- .map(|p| p.name)
+ .and_then(|mut p| p.take_str(PrincipalField::Name))
{
let mut rights = Vec::new();
@@ -142,6 +145,9 @@ impl<T: SessionStream> Session<T> {
}
pub async fn handle_my_rights(&mut self, request: Request<Command>) -> trc::Result<()> {
+ // Validate access
+ self.assert_has_permission(Permission::ImapMyRights)?;
+
let op_start = Instant::now();
let arguments = request.parse_acl(self.version)?;
let data = self.state.session_data();
@@ -224,6 +230,9 @@ impl<T: SessionStream> Session<T> {
}
pub async fn handle_set_acl(&mut self, request: Request<Command>) -> trc::Result<()> {
+ // Validate access
+ self.assert_has_permission(Permission::ImapAclSet)?;
+
let op_start = Instant::now();
let command = request.command;
let arguments = request.parse_acl(self.version)?;
@@ -252,7 +261,7 @@ impl<T: SessionStream> Session<T> {
.id(arguments.tag.to_string())
.caused_by(trc::location!())
})?
- .id;
+ .id();
// Prepare changes
let mut changes = Object::with_capacity(1);
@@ -381,6 +390,9 @@ impl<T: SessionStream> Session<T> {
}
pub async fn handle_list_rights(&mut self, request: Request<Command>) -> trc::Result<()> {
+ // Validate access
+ self.assert_has_permission(Permission::ImapListRights)?;
+
let op_start = Instant::now();
let arguments = request.parse_acl(self.version)?;
@@ -415,6 +427,15 @@ impl<T: SessionStream> Session<T> {
)
.await
}
+
+ pub fn assert_has_permission(&self, permission: Permission) -> trc::Result<()> {
+ match &self.state {
+ State::Authenticated { data } | State::Selected { data, .. } => {
+ data.access_token.assert_has_permission(permission)
+ }
+ State::NotAuthenticated { .. } => Ok(()),
+ }
+ }
}
impl<T: SessionStream> SessionData<T> {
diff --git a/crates/imap/src/op/append.rs b/crates/imap/src/op/append.rs
index 70777d82..a056d842 100644
--- a/crates/imap/src/op/append.rs
+++ b/crates/imap/src/op/append.rs
@@ -6,6 +6,7 @@
use std::{sync::Arc, time::Instant};
+use directory::Permission;
use imap_proto::{
protocol::{append::Arguments, select::HighestModSeq},
receiver::Request,
@@ -25,6 +26,9 @@ use super::{ImapContext, ToModSeq};
impl<T: SessionStream> Session<T> {
pub async fn handle_append(&mut self, request: Request<Command>) -> trc::Result<()> {
+ // Validate access
+ self.assert_has_permission(Permission::ImapAppend)?;
+
let op_start = Instant::now();
let arguments = request.parse_append(self.version)?;
let (data, selected_mailbox) = self.state.session_mailbox_state();
diff --git a/crates/imap/src/op/authenticate.rs b/crates/imap/src/op/authenticate.rs
index a68254af..92ba8916 100644
--- a/crates/imap/src/op/authenticate.rs
+++ b/crates/imap/src/op/authenticate.rs
@@ -5,6 +5,7 @@
*/
use common::listener::SessionStream;
+use directory::Permission;
use imap_proto::{
protocol::{authenticate::Mechanism, capability::Capability},
receiver::{self, Request},
@@ -121,6 +122,9 @@ impl<T: SessionStream> Session<T> {
}
};
+ // Validate access
+ access_token.assert_has_permission(Permission::ImapAuthenticate)?;
+
// Cache access token
let access_token = Arc::new(access_token);
self.jmap.cache_access_token(access_token.clone());
@@ -128,7 +132,7 @@ impl<T: SessionStream> Session<T> {
// Create session
self.state = State::Authenticated {
data: Arc::new(
- SessionData::new(self, &access_token, in_flight)
+ SessionData::new(self, access_token, in_flight)
.await
.map_err(|err| err.id(tag.clone()))?,
),
diff --git a/crates/imap/src/op/capability.rs b/crates/imap/src/op/capability.rs
index e4814079..fc6c163c 100644
--- a/crates/imap/src/op/capability.rs
+++ b/crates/imap/src/op/capability.rs
@@ -8,6 +8,7 @@ use std::time::Instant;
use crate::core::Session;
use common::listener::SessionStream;
+use directory::Permission;
use imap_proto::{
protocol::{
capability::{Capability, Response},
@@ -19,6 +20,9 @@ use imap_proto::{
impl<T: SessionStream> Session<T> {
pub async fn handle_capability(&mut self, request: Request<Command>) -> trc::Result<()> {
+ // Validate access
+ self.assert_has_permission(Permission::ImapCapability)?;
+
let op_start = Instant::now();
trc::event!(
Imap(trc::ImapEvent::Capabilities),
@@ -45,6 +49,9 @@ impl<T: SessionStream> Session<T> {
}
pub async fn handle_id(&mut self, request: Request<Command>) -> trc::Result<()> {
+ // Validate access
+ self.assert_has_permission(Permission::ImapId)?;
+
let op_start = Instant::now();
trc::event!(
Imap(trc::ImapEvent::Id),
diff --git a/crates/imap/src/op/copy_move.rs b/crates/imap/src/op/copy_move.rs
index 13d45c04..84043f62 100644
--- a/crates/imap/src/op/copy_move.rs
+++ b/crates/imap/src/op/copy_move.rs
@@ -6,6 +6,7 @@
use std::{sync::Arc, time::Instant};
+use directory::Permission;
use imap_proto::{
protocol::copy_move::Arguments, receiver::Request, Command, ResponseCode, ResponseType,
StatusResponse,
@@ -38,6 +39,13 @@ impl<T: SessionStream> Session<T> {
is_move: bool,
is_uid: bool,
) -> trc::Result<()> {
+ // Validate access
+ self.assert_has_permission(if is_move {
+ Permission::ImapMove
+ } else {
+ Permission::ImapCopy
+ })?;
+
let op_start = Instant::now();
let arguments = request.parse_copy_move(self.version)?;
let (data, src_mailbox) = self.state.mailbox_state();
diff --git a/crates/imap/src/op/create.rs b/crates/imap/src/op/create.rs
index 9320b89e..edd10ea0 100644
--- a/crates/imap/src/op/create.rs
+++ b/crates/imap/src/op/create.rs
@@ -12,6 +12,7 @@ use crate::{
spawn_op,
};
use common::listener::SessionStream;
+use directory::Permission;
use imap_proto::{
protocol::{create::Arguments, list::Attribute},
receiver::Request,
@@ -30,6 +31,9 @@ use trc::AddContext;
impl<T: SessionStream> Session<T> {
pub async fn handle_create(&mut self, requests: Vec<Request<Command>>) -> trc::Result<()> {
+ // Validate access
+ self.assert_has_permission(Permission::ImapCreate)?;
+
let data = self.state.session_data();
let version = self.version;
diff --git a/crates/imap/src/op/delete.rs b/crates/imap/src/op/delete.rs
index ceb7a46f..d8d028a8 100644
--- a/crates/imap/src/op/delete.rs
+++ b/crates/imap/src/op/delete.rs
@@ -11,6 +11,7 @@ use crate::{
spawn_op,
};
use common::listener::SessionStream;
+use directory::Permission;
use imap_proto::{
protocol::delete::Arguments, receiver::Request, Command, ResponseCode, StatusResponse,
};
@@ -21,6 +22,9 @@ use super::ImapContext;
impl<T: SessionStream> Session<T> {
pub async fn handle_delete(&mut self, requests: Vec<Request<Command>>) -> trc::Result<()> {
+ // Validate access
+ self.assert_has_permission(Permission::ImapDelete)?;
+
let data = self.state.session_data();
let version = self.version;
diff --git a/crates/imap/src/op/enable.rs b/crates/imap/src/op/enable.rs
index 0ee178e4..e5dde55c 100644
--- a/crates/imap/src/op/enable.rs
+++ b/crates/imap/src/op/enable.rs
@@ -8,6 +8,7 @@ use std::time::Instant;
use crate::core::Session;
use common::listener::SessionStream;
+use directory::Permission;
use imap_proto::{
protocol::{capability::Capability, enable, ImapResponse, ProtocolVersion},
receiver::Request,
@@ -16,6 +17,9 @@ use imap_proto::{
impl<T: SessionStream> Session<T> {
pub async fn handle_enable(&mut self, request: Request<Command>) -> trc::Result<()> {
+ // Validate access
+ self.assert_has_permission(Permission::ImapEnable)?;
+
let op_start = Instant::now();
let arguments = request.parse_enable()?;
diff --git a/crates/imap/src/op/expunge.rs b/crates/imap/src/op/expunge.rs
index 3576143b..36c99003 100644
--- a/crates/imap/src/op/expunge.rs
+++ b/crates/imap/src/op/expunge.rs
@@ -7,6 +7,7 @@
use std::{sync::Arc, time::Instant};
use ahash::AHashMap;
+use directory::Permission;
use imap_proto::{
parser::parse_sequence_set,
receiver::{Request, Token},
@@ -34,6 +35,9 @@ impl<T: SessionStream> Session<T> {
request: Request<Command>,
is_uid: bool,
) -> trc::Result<()> {
+ // Validate access
+ self.assert_has_permission(Permission::ImapExpunge)?;
+
let op_start = Instant::now();
let (data, mailbox) = self.state.select_data();
diff --git a/crates/imap/src/op/fetch.rs b/crates/imap/src/op/fetch.rs
index 1b9d29b2..182b8303 100644
--- a/crates/imap/src/op/fetch.rs
+++ b/crates/imap/src/op/fetch.rs
@@ -12,6 +12,7 @@ use crate::{
};
use ahash::AHashMap;
use common::listener::SessionStream;
+use directory::Permission;
use imap_proto::{
parser::PushUnique,
protocol::{
@@ -44,6 +45,9 @@ impl<T: SessionStream> Session<T> {
request: Request<Command>,
is_uid: bool,
) -> trc::Result<()> {
+ // Validate access
+ self.assert_has_permission(Permission::ImapFetch)?;
+
let op_start = Instant::now();
let arguments = request.parse_fetch()?;
diff --git a/crates/imap/src/op/idle.rs b/crates/imap/src/op/idle.rs
index 4699b1d4..be97d88f 100644
--- a/crates/imap/src/op/idle.rs
+++ b/crates/imap/src/op/idle.rs
@@ -7,6 +7,7 @@
use std::{sync::Arc, time::Instant};
use ahash::AHashSet;
+use directory::Permission;
use imap_proto::{
protocol::{
fetch,
@@ -32,6 +33,9 @@ use crate::{
impl<T: SessionStream> Session<T> {
pub async fn handle_idle(&mut self, request: Request<Command>) -> trc::Result<()> {
+ // Validate access
+ self.assert_has_permission(Permission::ImapIdle)?;
+
let op_start = Instant::now();
let (data, mailbox, types) = match &self.state {
State::Authenticated { data, .. } => {
diff --git a/crates/imap/src/op/list.rs b/crates/imap/src/op/list.rs
index 6d7e670b..dce26b75 100644
--- a/crates/imap/src/op/list.rs
+++ b/crates/imap/src/op/list.rs
@@ -11,6 +11,7 @@ use crate::{
spawn_op,
};
use common::listener::SessionStream;
+use directory::Permission;
use imap_proto::{
protocol::{
list::{
@@ -30,8 +31,14 @@ impl<T: SessionStream> Session<T> {
let command = request.command;
let is_lsub = command == Command::Lsub;
let arguments = if !is_lsub {
+ // Validate access
+ self.assert_has_permission(Permission::ImapList)?;
+
request.parse_list(self.version)
} else {
+ // Validate access
+ self.assert_has_permission(Permission::ImapLsub)?;
+
request.parse_lsub()
}?;
diff --git a/crates/imap/src/op/namespace.rs b/crates/imap/src/op/namespace.rs
index 3935955e..0f9ed1ac 100644
--- a/crates/imap/src/op/namespace.rs
+++ b/crates/imap/src/op/namespace.rs
@@ -6,6 +6,7 @@
use crate::core::Session;
use common::listener::SessionStream;
+use directory::Permission;
use imap_proto::{
protocol::{namespace::Response, ImapResponse},
receiver::Request,
@@ -14,6 +15,9 @@ use imap_proto::{
impl<T: SessionStream> Session<T> {
pub async fn handle_namespace(&mut self, request: Request<Command>) -> trc::Result<()> {
+ // Validate access
+ self.assert_has_permission(Permission::ImapNamespace)?;
+
trc::event!(
Imap(trc::ImapEvent::Namespace),
SpanId = self.session_id,
diff --git a/crates/imap/src/op/rename.rs b/crates/imap/src/op/rename.rs
index a19f6a9b..cc8b27fb 100644
--- a/crates/imap/src/op/rename.rs
+++ b/crates/imap/src/op/rename.rs
@@ -11,6 +11,7 @@ use crate::{
spawn_op,
};
use common::listener::SessionStream;
+use directory::Permission;
use imap_proto::{
protocol::rename::Arguments, receiver::Request, Command, ResponseCode, StatusResponse,
};
@@ -29,6 +30,9 @@ use super::ImapContext;
impl<T: SessionStream> Session<T> {
pub async fn handle_rename(&mut self, request: Request<Command>) -> trc::Result<()> {
+ // Validate access
+ self.assert_has_permission(Permission::ImapRename)?;
+
let op_start = Instant::now();
let arguments = request.parse_rename(self.version)?;
let data = self.state.session_data();
diff --git a/crates/imap/src/op/search.rs b/crates/imap/src/op/search.rs
index 3b5e699a..86329275 100644
--- a/crates/imap/src/op/search.rs
+++ b/crates/imap/src/op/search.rs
@@ -7,6 +7,7 @@
use std::{sync::Arc, time::Instant};
use common::listener::SessionStream;
+use directory::Permission;
use imap_proto::{
protocol::{
search::{self, Arguments, Filter, Response, ResultOption},
@@ -43,8 +44,14 @@ impl<T: SessionStream> Session<T> {
) -> trc::Result<()> {
let op_start = Instant::now();
let mut arguments = if !is_sort {
+ // Validate access
+ self.assert_has_permission(Permission::ImapSearch)?;
+
request.parse_search(self.version)
} else {
+ // Validate access
+ self.assert_has_permission(Permission::ImapSort)?;
+
request.parse_sort()
}?;
diff --git a/crates/imap/src/op/select.rs b/crates/imap/src/op/select.rs
index 43417a0c..a36125ea 100644
--- a/crates/imap/src/op/select.rs
+++ b/crates/imap/src/op/select.rs
@@ -6,6 +6,7 @@
use std::{sync::Arc, time::Instant};
+use directory::Permission;
use imap_proto::{
protocol::{
fetch,
@@ -26,6 +27,13 @@ use super::{ImapContext, ToModSeq};
impl<T: SessionStream> Session<T> {
pub async fn handle_select(&mut self, request: Request<Command>) -> trc::Result<()> {
+ // Validate access
+ self.assert_has_permission(if request.command == Command::Select {
+ Permission::ImapSelect
+ } else {
+ Permission::ImapExamine
+ })?;
+
let op_start = Instant::now();
let is_select = request.command == Command::Select;
let command = request.command;
diff --git a/crates/imap/src/op/status.rs b/crates/imap/src/op/status.rs
index 00f197dd..ce8c6029 100644
--- a/crates/imap/src/op/status.rs
+++ b/crates/imap/src/op/status.rs
@@ -12,6 +12,7 @@ use crate::{
spawn_op,
};
use common::listener::SessionStream;
+use directory::Permission;
use imap_proto::{
parser::PushUnique,
protocol::status::{Status, StatusItem, StatusItemType},
@@ -34,6 +35,9 @@ use super::ToModSeq;
impl<T: SessionStream> Session<T> {
pub async fn handle_status(&mut self, request: Request<Command>) -> trc::Result<()> {
+ // Validate access
+ self.assert_has_permission(Permission::ImapStatus)?;
+
let op_start = Instant::now();
let arguments = request.parse_status(self.version)?;
let version = self.version;
diff --git a/crates/imap/src/op/store.rs b/crates/imap/src/op/store.rs
index 24b5b932..3a799841 100644
--- a/crates/imap/src/op/store.rs
+++ b/crates/imap/src/op/store.rs
@@ -12,6 +12,7 @@ use crate::{
};
use ahash::AHashSet;
use common::listener::SessionStream;
+use directory::Permission;
use imap_proto::{
protocol::{
fetch::{DataItem, FetchItem},
@@ -39,6 +40,9 @@ impl<T: SessionStream> Session<T> {
request: Request<Command>,
is_uid: bool,
) -> trc::Result<()> {
+ // Validate access
+ self.assert_has_permission(Permission::ImapStore)?;
+
let op_start = Instant::now();
let arguments = request.parse_store()?;
let (data, mailbox) = self.state.select_data();
diff --git a/crates/imap/src/op/subscribe.rs b/crates/imap/src/op/subscribe.rs
index 9eb648ab..995ed430 100644
--- a/crates/imap/src/op/subscribe.rs
+++ b/crates/imap/src/op/subscribe.rs
@@ -11,6 +11,7 @@ use crate::{
spawn_op,
};
use common::listener::SessionStream;
+use directory::Permission;
use imap_proto::{receiver::Request, Command, ResponseCode, StatusResponse};
use jmap::mailbox::set::{MailboxSubscribe, SCHEMA};
use jmap_proto::{
@@ -30,6 +31,9 @@ impl<T: SessionStream> Session<T> {
request: Request<Command>,
is_subscribe: bool,
) -> trc::Result<()> {
+ // Validate access
+ self.assert_has_permission(Permission::ImapSubscribe)?;
+
let op_start = Instant::now();
let arguments = request.parse_subscribe(self.version)?;
let data = self.state.session_data();
diff --git a/crates/imap/src/op/thread.rs b/crates/imap/src/op/thread.rs
index 5c9748c7..a8425652 100644
--- a/crates/imap/src/op/thread.rs
+++ b/crates/imap/src/op/thread.rs
@@ -12,6 +12,7 @@ use crate::{
};
use ahash::AHashMap;
use common::listener::SessionStream;
+use directory::Permission;
use imap_proto::{
protocol::{
thread::{Arguments, Response},
@@ -28,6 +29,9 @@ impl<T: SessionStream> Session<T> {
request: Request<Command>,
is_uid: bool,
) -> trc::Result<()> {
+ // Validate access
+ self.assert_has_permission(Permission::ImapThread)?;
+
let op_start = Instant::now();
let command = request.command;
let mut arguments = request.parse_thread()?;
diff --git a/crates/jmap/src/api/autoconfig.rs b/crates/jmap/src/api/autoconfig.rs
index c8c67ef9..11d29547 100644
--- a/crates/jmap/src/api/autoconfig.rs
+++ b/crates/jmap/src/api/autoconfig.rs
@@ -7,7 +7,7 @@
use std::fmt::Write;
use common::manager::webadmin::Resource;
-use directory::QueryBy;
+use directory::{backend::internal::PrincipalField, QueryBy};
use quick_xml::events::Event;
use quick_xml::Reader;
use utils::url_params::UrlParams;
@@ -187,14 +187,14 @@ impl JMAP {
.await
.unwrap_or_default()
{
- if let Ok(Some(principal)) = self
+ if let Ok(Some(mut principal)) = self
.core
.storage
.directory
.query(QueryBy::Id(id), false)
.await
{
- account_name = principal.name;
+ account_name = principal.take_str(PrincipalField::Name).unwrap_or_default();
break;
}
}
diff --git a/crates/jmap/src/api/http.rs b/crates/jmap/src/api/http.rs
index 4968863e..120777db 100644
--- a/crates/jmap/src/api/http.rs
+++ b/crates/jmap/src/api/http.rs
@@ -12,6 +12,7 @@ use common::{
manager::webadmin::Resource,
Core,
};
+use directory::Permission;
use http_body_util::{BodyExt, Full};
use hyper::{
body::{self, Bytes},
@@ -29,7 +30,7 @@ use jmap_proto::{
};
use crate::{
- auth::{authenticate::HttpHeaders, oauth::OAuthMetadata},
+ auth::{authenticate::HttpHeaders, oauth::OAuthMetadata, AccessToken},
blob::{DownloadResponse, UploadResponse},
services::state,
JmapInstance, JMAP,
@@ -81,7 +82,7 @@ impl JMAP {
let request = fetch_body(
&mut req,
- if !access_token.is_super_user() {
+ if !access_token.has_permission(Permission::UnlimitedUploads) {
self.core.jmap.upload_max_size
} else {
0
@@ -142,7 +143,7 @@ impl JMAP {
{
return match fetch_body(
&mut req,
- if !access_token.is_super_user() {
+ if !access_token.has_permission(Permission::UnlimitedUploads) {
self.core.jmap.upload_max_size
} else {
0
@@ -302,27 +303,29 @@ impl JMAP {
if err.matches(trc::EventType::Auth(trc::AuthEvent::Failed))
&& self.core.is_enterprise_edition()
{
- if let Some((live_path, token)) = req
+ if let Some((live_path, grant_type, token)) = req
.uri()
.path()
.strip_prefix("/api/telemetry/")
.and_then(|p| {
p.strip_prefix("traces/live/")
- .map(|t| ("traces", t))
+ .map(|t| ("traces", "live_tracing", t))
.or_else(|| {
p.strip_prefix("metrics/live/")
- .map(|t| ("metrics", t))
+ .map(|t| ("metrics", "live_metrics", t))
})
})
{
let (account_id, _, _) =
- self.validate_access_token("live_telemetry", token).await?;
+ self.validate_access_token(grant_type, token).await?;
return self
.handle_telemetry_api_request(
&req,
vec!["", live_path, "live"],
- account_id,
+ &AccessToken::from_id(account_id)
+ .with_permission(Permission::MetricsLive)
+ .with_permission(Permission::TracingLive),
)
.await;
}
@@ -893,7 +896,13 @@ impl ToRequestError for trc::Error {
trc::AuthEvent::TooManyAttempts => RequestError::too_many_auth_attempts(),
_ => RequestError::unauthorized(),
},
- trc::EventType::Security(_) => RequestError::too_many_auth_attempts(),
+ trc::EventType::Security(cause) => match cause {
+ trc::SecurityEvent::AuthenticationBan
+ | trc::SecurityEvent::BruteForceBan
+ | trc::SecurityEvent::LoiterBan
+ | trc::SecurityEvent::IpBlocked => RequestError::too_many_auth_attempts(),
+ trc::SecurityEvent::Unauthorized => RequestError::forbidden(),
+ },
trc::EventType::Resource(cause) => match cause {
trc::ResourceEvent::NotFound => RequestError::not_found(),
trc::ResourceEvent::BadParameters => RequestError::blank(
diff --git a/crates/jmap/src/api/management/dkim.rs b/crates/jmap/src/api/management/dkim.rs
index 00589441..06ddaa59 100644
--- a/crates/jmap/src/api/management/dkim.rs
+++ b/crates/jmap/src/api/management/dkim.rs
@@ -7,7 +7,7 @@
use std::str::FromStr;
use common::config::smtp::auth::simple_pem_parse;
-use directory::backend::internal::manage;
+use directory::{backend::internal::manage, Permission};
use hyper::Method;
use mail_auth::{
common::crypto::{Ed25519Key, RsaKey, Sha256},
@@ -23,6 +23,7 @@ use store::write::now;
use crate::{
api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse},
+ auth::AccessToken,
JMAP,
};
@@ -48,10 +49,21 @@ impl JMAP {
req: &HttpRequest,
path: Vec<&str>,
body: Option<Vec<u8>>,
+ access_token: &AccessToken,
) -> trc::Result<HttpResponse> {
match *req.method() {
- Method::GET => self.handle_get_public_key(path).await,
- Method::POST => self.handle_create_signature(body).await,
+ 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()),
}
}
diff --git a/crates/jmap/src/api/management/domain.rs b/crates/jmap/src/api/management/domain.rs
index e029e4c6..eba1312b 100644
--- a/crates/jmap/src/api/management/domain.rs
+++ b/crates/jmap/src/api/management/domain.rs
@@ -4,7 +4,10 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
-use directory::backend::internal::manage::{self, ManageDirectory};
+use directory::{
+ backend::internal::manage::{self, ManageDirectory},
+ Permission,
+};
use hyper::Method;
use serde::{Deserialize, Serialize};
@@ -19,6 +22,7 @@ use crate::{
management::dkim::{obtain_dkim_public_key, Algorithm},
HttpRequest, HttpResponse, JsonResponse,
},
+ auth::AccessToken,
JMAP,
};
@@ -37,9 +41,13 @@ impl JMAP {
&self,
req: &HttpRequest,
path: Vec<&str>,
+ access_token: &AccessToken,
) -> trc::Result<HttpResponse> {
match (path.get(1), req.method()) {
(None, &Method::GET) => {
+ // Validate the access token
+ access_token.assert_has_permission(Permission::DomainList)?;
+
// List domains
let params = UrlParams::new(req.uri().query());
let filter = params.get("filter");
@@ -66,6 +74,9 @@ impl JMAP {
.into_http_response())
}
(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!({
@@ -74,6 +85,9 @@ impl JMAP {
.into_http_response())
}
(Some(domain), &Method::POST) => {
+ // Validate the access token
+ access_token.assert_has_permission(Permission::DomainCreate)?;
+
// Create domain
let domain = decode_path_element(domain);
self.core
@@ -103,6 +117,9 @@ impl JMAP {
.into_http_response())
}
(Some(domain), &Method::DELETE) => {
+ // Validate the access token
+ access_token.assert_has_permission(Permission::DomainDelete)?;
+
// Delete domain
let domain = decode_path_element(domain);
self.core
diff --git a/crates/jmap/src/api/management/enterprise/telemetry.rs b/crates/jmap/src/api/management/enterprise/telemetry.rs
index 6186509c..80ea5548 100644
--- a/crates/jmap/src/api/management/enterprise/telemetry.rs
+++ b/crates/jmap/src/api/management/enterprise/telemetry.rs
@@ -17,7 +17,7 @@ use common::telemetry::{
metrics::store::{Metric, MetricsStore},
tracers::store::{TracingQuery, TracingStore},
};
-use directory::backend::internal::manage;
+use directory::{backend::internal::manage, Permission};
use http_body_util::{combinators::BoxBody, StreamBody};
use hyper::{
body::{Bytes, Frame},
@@ -38,6 +38,7 @@ use crate::{
http::ToHttpResponse, management::Timestamp, HttpRequest, HttpResponse, HttpResponseBody,
JsonResponse,
},
+ auth::AccessToken,
JMAP,
};
@@ -46,9 +47,10 @@ impl JMAP {
&self,
req: &HttpRequest,
path: Vec<&str>,
- account_id: u32,
+ access_token: &AccessToken,
) -> trc::Result<HttpResponse> {
let params = UrlParams::new(req.uri().query());
+ let account_id = access_token.primary_id();
match (
path.get(1).copied().unwrap_or_default(),
@@ -56,6 +58,9 @@ impl JMAP {
req.method(),
) {
("traces", None, &Method::GET) => {
+ // Validate the access token
+ access_token.assert_has_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();
@@ -162,6 +167,9 @@ impl JMAP {
}
}
("traces", Some("live"), &Method::GET) => {
+ // Validate the access token
+ access_token.assert_has_permission(Permission::TracingLive)?;
+
let mut key_filters = AHashMap::new();
let mut filter = None;
@@ -290,6 +298,9 @@ impl JMAP {
})
}
("trace", id, &Method::GET) => {
+ // Validate the access token
+ access_token.assert_has_permission(Permission::TracingGet)?;
+
let store = &self
.core
.enterprise
@@ -327,15 +338,32 @@ impl JMAP {
.into_http_response())
}
}
- ("live", Some("token"), &Method::GET) => {
+ ("live", Some("tracing-token"), &Method::GET) => {
+ // Validate the access token
+ access_token.assert_has_permission(Permission::TracingLive)?;
+
// Issue a live telemetry token valid for 60 seconds
Ok(JsonResponse::new(json!({
- "data": self.issue_custom_token(account_id, "live_telemetry", "web", 60).await?,
+ "data": self.issue_custom_token(account_id, "live_tracing", "web", 60).await?,
+ }))
+ .into_http_response())
+ }
+ ("live", Some("metrics-token"), &Method::GET) => {
+ // Validate the access token
+ access_token.assert_has_permission(Permission::MetricsLive)?;
+
+ // Issue a live telemetry token valid for 60 seconds
+
+ Ok(JsonResponse::new(json!({
+ "data": self.issue_custom_token(account_id, "live_metrics", "web", 60).await?,
}))
.into_http_response())
}
("metrics", None, &Method::GET) => {
+ // Validate the access token
+ access_token.assert_has_permission(Permission::MetricsList)?;
+
let before = params
.parse::<Timestamp>("before")
.map(|t| t.into_inner())
@@ -395,6 +423,9 @@ impl JMAP {
.into_http_response())
}
("metrics", Some("live"), &Method::GET) => {
+ // Validate the access token
+ access_token.assert_has_permission(Permission::MetricsLive)?;
+
let interval = Duration::from_secs(
params
.parse::<u64>("interval")
diff --git a/crates/jmap/src/api/management/log.rs b/crates/jmap/src/api/management/log.rs
index 1083a009..0bebbf48 100644
--- a/crates/jmap/src/api/management/log.rs
+++ b/crates/jmap/src/api/management/log.rs
@@ -5,7 +5,7 @@ use std::{
};
use chrono::DateTime;
-use directory::backend::internal::manage;
+use directory::{backend::internal::manage, Permission};
use rev_lines::RevLines;
use serde::Serialize;
use serde_json::json;
@@ -14,6 +14,7 @@ use utils::url_params::UrlParams;
use crate::{
api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse},
+ auth::AccessToken,
JMAP,
};
@@ -27,7 +28,14 @@ struct LogEntry {
}
impl JMAP {
- pub async fn handle_view_logs(&self, req: &HttpRequest) -> trc::Result<HttpResponse> {
+ pub async fn handle_view_logs(
+ &self,
+ req: &HttpRequest,
+ access_token: &AccessToken,
+ ) -> trc::Result<HttpResponse> {
+ // Validate the access token
+ access_token.assert_has_permission(Permission::LogsView)?;
+
let path = self
.core
.metrics
diff --git a/crates/jmap/src/api/management/mod.rs b/crates/jmap/src/api/management/mod.rs
index 1715363b..9972a86f 100644
--- a/crates/jmap/src/api/management/mod.rs
+++ b/crates/jmap/src/api/management/mod.rs
@@ -19,7 +19,7 @@ pub mod stores;
use std::{borrow::Cow, str::FromStr, sync::Arc};
-use directory::backend::internal::manage;
+use directory::{backend::internal::manage, Permission};
use hyper::Method;
use mail_parser::DateTime;
use serde::Serialize;
@@ -50,31 +50,68 @@ impl JMAP {
session: &HttpSessionData,
) -> trc::Result<HttpResponse> {
let path = req.uri().path().split('/').skip(2).collect::<Vec<_>>();
- let is_superuser = access_token.is_super_user();
match path.first().copied().unwrap_or_default() {
- "queue" if is_superuser => self.handle_manage_queue(req, path).await,
- "settings" if is_superuser => self.handle_manage_settings(req, path, body).await,
- "reports" if is_superuser => self.handle_manage_reports(req, path).await,
- "principal" if is_superuser => self.handle_manage_principal(req, path, body).await,
- "domain" if is_superuser => self.handle_manage_domain(req, path).await,
- "store" if is_superuser => self.handle_manage_store(req, path, body, session).await,
- "reload" if is_superuser => self.handle_manage_reload(req, path).await,
- "dkim" if is_superuser => self.handle_manage_dkim(req, path, body).await,
- "update" if is_superuser => self.handle_manage_update(req, path).await,
- "logs" if is_superuser && req.method() == Method::GET => {
- self.handle_view_logs(req).await
+ "queue" => self.handle_manage_queue(req, path, &access_token).await,
+ "settings" => {
+ self.handle_manage_settings(req, path, body, &access_token)
+ .await
}
- "sieve" if is_superuser => self.handle_run_sieve(req, path, body).await,
- "restart" if is_superuser && req.method() == Method::GET => {
+ "reports" => self.handle_manage_reports(req, path, &access_token).await,
+ "principal" => {
+ self.handle_manage_principal(req, path, body, &access_token)
+ .await
+ }
+ "domain" => self.handle_manage_domain(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
+ }
+ "sieve" => self.handle_run_sieve(req, path, body, &access_token).await,
+ "restart" if req.method() == Method::GET => {
+ // Validate the access token
+ access_token.assert_has_permission(Permission::Restart)?;
+
Err(manage::unsupported("Restart is not yet supported"))
}
- "oauth" => self.handle_oauth_api_request(access_token, body).await,
+ "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) => self.handle_crypto_post(access_token, body).await,
- ("crypto", &Method::GET) => self.handle_crypto_get(access_token).await,
- ("auth", &Method::GET) => self.handle_account_auth_get(access_token).await,
+ ("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()),
@@ -83,7 +120,7 @@ impl JMAP {
// SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art>
// SPDX-License-Identifier: LicenseRef-SEL
#[cfg(feature = "enterprise")]
- "telemetry" if is_superuser => {
+ "telemetry" => {
// 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.
@@ -94,7 +131,7 @@ impl JMAP {
// for copyright infringement, breach of contract, and fraud.
if self.core.is_enterprise_edition() {
- self.handle_telemetry_api_request(req, path, access_token.primary_id())
+ self.handle_telemetry_api_request(req, path, &access_token)
.await
} else {
Err(manage::enterprise())
diff --git a/crates/jmap/src/api/management/principal.rs b/crates/jmap/src/api/management/principal.rs
index e88dea9d..e47d56d4 100644
--- a/crates/jmap/src/api/management/principal.rs
+++ b/crates/jmap/src/api/management/principal.rs
@@ -12,7 +12,7 @@ use directory::{
manage::{self, ManageDirectory},
PrincipalAction, PrincipalField, PrincipalUpdate, PrincipalValue, SpecialSecrets,
},
- DirectoryInner, Principal, QueryBy, Type,
+ DirectoryInner, Permission, Principal, QueryBy, Type,
};
use hyper::{header, Method};
@@ -28,7 +28,7 @@ use crate::{
use super::decode_path_element;
#[derive(Debug, serde::Serialize, serde::Deserialize)]
-pub struct PrincipalResponse {
+pub struct PrincipalPayload {
#[serde(default)]
pub id: u32,
#[serde(rename = "type")]
@@ -68,8 +68,6 @@ pub enum AccountAuthRequest {
pub struct AccountAuthResponse {
#[serde(rename = "otpEnabled")]
pub otp_auth: bool,
- #[serde(rename = "isAdministrator")]
- pub is_admin: bool,
#[serde(rename = "appPasswords")]
pub app_passwords: Vec<String>,
}
@@ -80,43 +78,47 @@ impl JMAP {
req: &HttpRequest,
path: Vec<&str>,
body: Option<Vec<u8>>,
+ access_token: &AccessToken,
) -> trc::Result<HttpResponse> {
match (path.get(1), req.method()) {
(None, &Method::POST) => {
+ // Validate the access token
+ access_token.assert_has_permission(Permission::PrincipalCreate)?;
+
// Make sure the current directory supports updates
self.assert_supported_directory()?;
// Create principal
- let principal = serde_json::from_slice::<PrincipalResponse>(
- body.as_deref().unwrap_or_default(),
- )
- .map_err(|err| {
- trc::EventType::Resource(trc::ResourceEvent::BadParameters).from_json_error(err)
- })?;
+ let principal =
+ serde_json::from_slice::<PrincipalPayload>(body.as_deref().unwrap_or_default())
+ .map_err(|err| {
+ trc::EventType::Resource(trc::ResourceEvent::BadParameters)
+ .from_json_error(err)
+ })?;
+
+ let principal = Principal::new(principal.id, principal.typ)
+ .with_field(PrincipalField::Name, principal.name)
+ .with_field(PrincipalField::Secrets, principal.secrets)
+ .with_field(PrincipalField::Quota, principal.quota)
+ .with_field(PrincipalField::Emails, principal.emails)
+ .with_field(PrincipalField::MemberOf, principal.member_of)
+ .with_field(PrincipalField::Members, principal.members)
+ .with_opt_field(PrincipalField::Description, principal.description);
Ok(JsonResponse::new(json!({
"data": self
.core
.storage
.data
- .create_account(
- Principal {
- id: principal.id,
- typ: principal.typ,
- quota: principal.quota,
- name: principal.name,
- secrets: principal.secrets,
- emails: principal.emails,
- member_of: principal.member_of,
- description: principal.description,
- },
- principal.members,
- )
+ .create_account(principal)
.await?,
}))
.into_http_response())
}
(None, &Method::GET) => {
+ // Validate the access token
+ access_token.assert_has_permission(Permission::PrincipalList)?;
+
// List principal ids
let params = UrlParams::new(req.uri().query());
let filter = params.get("filter");
@@ -144,6 +146,20 @@ impl JMAP {
.into_http_response())
}
(Some(name), method) => {
+ // Validate the access token
+ match *method {
+ Method::GET => {
+ access_token.assert_has_permission(Permission::PrincipalGet)?;
+ }
+ Method::DELETE => {
+ access_token.assert_has_permission(Permission::PrincipalDelete)?;
+ }
+ Method::PATCH => {
+ access_token.assert_has_permission(Permission::PrincipalUpdate)?;
+ }
+ _ => {}
+ }
+
// Fetch, update or delete principal
let name = decode_path_element(name);
let account_id = self
@@ -166,19 +182,23 @@ impl JMAP {
let principal = self.core.storage.data.map_group_ids(principal).await?;
// Obtain quota usage
- let mut principal = PrincipalResponse::from(principal);
+ let mut principal = PrincipalPayload::from(principal);
principal.used_quota = self.get_used_quota(account_id).await? as u64;
// Obtain member names
for member_id in self.core.storage.data.get_members(account_id).await? {
- if let Some(member_principal) = self
+ if let Some(mut member_principal) = self
.core
.storage
.data
.query(QueryBy::Id(member_id), false)
.await?
{
- principal.members.push(member_principal.name);
+ principal.members.push(
+ member_principal
+ .take_str(PrincipalField::Name)
+ .unwrap_or_default(),
+ );
}
}
@@ -257,7 +277,6 @@ impl JMAP {
) -> trc::Result<HttpResponse> {
let mut response = AccountAuthResponse {
otp_auth: false,
- is_admin: access_token.is_super_user(),
app_passwords: Vec::new(),
};
@@ -270,7 +289,7 @@ impl JMAP {
.await?
.ok_or_else(|| trc::ManageEvent::NotFound.into_err())?;
- for secret in principal.secrets {
+ for secret in principal.iter_str(PrincipalField::Secrets) {
if secret.is_otp_auth() {
response.otp_auth = true;
} else if let Some((app_name, _)) =
@@ -327,7 +346,7 @@ impl JMAP {
}
// Handle Fallback admin password changes
- if access_token.is_super_user() && access_token.primary_id() == u32::MAX {
+ if access_token.primary_id() == u32::MAX {
match requests.into_iter().next().unwrap() {
AccountAuthRequest::SetPassword { password } => {
self.core
@@ -420,24 +439,31 @@ impl JMAP {
Err(manage::unsupported(format!(
concat!(
"{} directory cannot be managed. ",
- "Only internal directories support inserts and update operations."
+ "Only internal directories support inserts ",
+ "and update operations."
),
class
)))
}
}
-impl From<Principal<String>> for PrincipalResponse {
- fn from(principal: Principal<String>) -> Self {
- PrincipalResponse {
- id: principal.id,
- typ: principal.typ,
- quota: principal.quota,
- name: principal.name,
- emails: principal.emails,
- member_of: principal.member_of,
- description: principal.description,
- secrets: principal.secrets,
+impl From<Principal> for PrincipalPayload {
+ fn from(mut principal: Principal) -> Self {
+ PrincipalPayload {
+ id: principal.id(),
+ typ: principal.typ(),
+ quota: principal.quota(),
+ name: principal.take_str(PrincipalField::Name).unwrap_or_default(),
+ emails: principal
+ .take_str_array(PrincipalField::Emails)
+ .unwrap_or_default(),
+ member_of: principal
+ .take_str_array(PrincipalField::MemberOf)
+ .unwrap_or_default(),
+ description: principal.take_str(PrincipalField::Description),
+ secrets: principal
+ .take_str_array(PrincipalField::Secrets)
+ .unwrap_or_default(),
used_quota: 0,
members: Vec::new(),
}
diff --git a/crates/jmap/src/api/management/queue.rs b/crates/jmap/src/api/management/queue.rs
index 2723827e..46fa0616 100644
--- a/crates/jmap/src/api/management/queue.rs
+++ b/crates/jmap/src/api/management/queue.rs
@@ -5,6 +5,7 @@
*/
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
+use directory::Permission;
use hyper::Method;
use mail_auth::{
dmarc::URI,
@@ -23,6 +24,7 @@ use utils::url_params::UrlParams;
use crate::{
api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse},
+ auth::AccessToken,
JMAP,
};
@@ -105,6 +107,7 @@ impl JMAP {
&self,
req: &HttpRequest,
path: Vec<&str>,
+ access_token: &AccessToken,
) -> trc::Result<HttpResponse> {
let params = UrlParams::new(req.uri().query());
@@ -114,6 +117,9 @@ impl JMAP {
req.method(),
) {
("messages", None, &Method::GET) => {
+ // Validate the access token
+ access_token.assert_has_permission(Permission::MessageQueueList)?;
+
let text = params.get("text");
let from = params.get("from");
let to = params.get("to");
@@ -217,6 +223,9 @@ impl JMAP {
.into_http_response())
}
("messages", Some(queue_id), &Method::GET) => {
+ // Validate the access token
+ access_token.assert_has_permission(Permission::MessageQueueGet)?;
+
if let Some(message) = self
.smtp
.read_message(queue_id.parse().unwrap_or_default())
@@ -231,6 +240,9 @@ impl JMAP {
}
}
("messages", Some(queue_id), &Method::PATCH) => {
+ // Validate the access token
+ access_token.assert_has_permission(Permission::MessageQueueUpdate)?;
+
let time = params
.parse::<FutureTimestamp>("at")
.map(|t| t.into_inner())
@@ -278,6 +290,9 @@ impl JMAP {
}
}
("messages", Some(queue_id), &Method::DELETE) => {
+ // Validate the access token
+ access_token.assert_has_permission(Permission::MessageQueueDelete)?;
+
if let Some(mut message) = self
.smtp
.read_message(queue_id.parse().unwrap_or_default())
@@ -358,6 +373,9 @@ impl JMAP {
}
}
("reports", None, &Method::GET) => {
+ // Validate the access token
+ access_token.assert_has_permission(Permission::OutgoingReportList)?;
+
let domain = params.get("domain").map(|d| d.to_lowercase());
let type_ = params.get("type").and_then(|t| match t {
"dmarc" => 0u8.into(),
@@ -436,6 +454,9 @@ impl JMAP {
.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 {
@@ -473,6 +494,9 @@ impl JMAP {
}
}
("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()) {
match report_id {
QueueClass::DmarcReportHeader(event) => {
diff --git a/crates/jmap/src/api/management/reload.rs b/crates/jmap/src/api/management/reload.rs
index c8653fb1..c8410fc2 100644
--- a/crates/jmap/src/api/management/reload.rs
+++ b/crates/jmap/src/api/management/reload.rs
@@ -4,12 +4,14 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
+use directory::Permission;
use hyper::Method;
use serde_json::json;
use utils::url_params::UrlParams;
use crate::{
api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse},
+ auth::AccessToken,
services::housekeeper::Event,
JMAP,
};
@@ -19,7 +21,11 @@ impl JMAP {
&self,
req: &HttpRequest,
path: Vec<&str>,
+ access_token: &AccessToken,
) -> trc::Result<HttpResponse> {
+ // 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.core.reload_lookups().await?;
@@ -92,18 +98,27 @@ impl JMAP {
&self,
req: &HttpRequest,
path: Vec<&str>,
+ access_token: &AccessToken,
) -> trc::Result<HttpResponse> {
match (path.get(1).copied(), req.method()) {
- (Some("spam-filter"), &Method::GET) => Ok(JsonResponse::new(json!({
- "data": self
- .core
- .storage
- .config
- .update_config_resource("spam-filter")
- .await?,
- }))
- .into_http_response()),
+ (Some("spam-filter"), &Method::GET) => {
+ // Validate the access token
+ access_token.assert_has_permission(Permission::UpdateSpamFilter)?;
+
+ Ok(JsonResponse::new(json!({
+ "data": self
+ .core
+ .storage
+ .config
+ .update_config_resource("spam-filter")
+ .await?,
+ }))
+ .into_http_response())
+ }
(Some("webadmin"), &Method::GET) => {
+ // Validate the access token
+ access_token.assert_has_permission(Permission::UpdateWebadmin)?;
+
self.inner.webadmin.update_and_unpack(&self.core).await?;
Ok(JsonResponse::new(json!({
diff --git a/crates/jmap/src/api/management/report.rs b/crates/jmap/src/api/management/report.rs
index d8bd0318..a53db1f9 100644
--- a/crates/jmap/src/api/management/report.rs
+++ b/crates/jmap/src/api/management/report.rs
@@ -4,6 +4,7 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
+use directory::Permission;
use hyper::Method;
use mail_auth::report::{
tlsrpt::{FailureDetails, Policy, TlsReport},
@@ -19,6 +20,7 @@ use utils::url_params::UrlParams;
use crate::{
api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse},
+ auth::AccessToken,
JMAP,
};
@@ -35,6 +37,7 @@ impl JMAP {
&self,
req: &HttpRequest,
path: Vec<&str>,
+ access_token: &AccessToken,
) -> trc::Result<HttpResponse> {
match (
path.get(1).copied().unwrap_or_default(),
@@ -42,6 +45,9 @@ impl JMAP {
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 filter = params.get("text");
let page: usize = params.parse::<usize>("page").unwrap_or_default();
@@ -154,6 +160,9 @@ impl JMAP {
.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 self
@@ -207,6 +216,9 @@ impl JMAP {
}
}
(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()) {
let mut batch = BatchBuilder::new();
batch.clear(ValueClass::Report(report_id));
diff --git a/crates/jmap/src/api/management/settings.rs b/crates/jmap/src/api/management/settings.rs
index db0b6047..1d46799a 100644
--- a/crates/jmap/src/api/management/settings.rs
+++ b/crates/jmap/src/api/management/settings.rs
@@ -4,6 +4,7 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
+use directory::Permission;
use hyper::Method;
use serde_json::json;
use store::ahash::AHashMap;
@@ -11,6 +12,7 @@ use utils::{config::ConfigKey, map::vec_map::VecMap, url_params::UrlParams};
use crate::{
api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse},
+ auth::AccessToken,
JMAP,
};
@@ -38,9 +40,13 @@ impl JMAP {
req: &HttpRequest,
path: Vec<&str>,
body: Option<Vec<u8>>,
+ access_token: &AccessToken,
) -> trc::Result<HttpResponse> {
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
@@ -168,6 +174,9 @@ impl JMAP {
}
}
(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
@@ -200,6 +209,9 @@ impl JMAP {
.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
@@ -232,6 +244,9 @@ impl JMAP {
.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?;
@@ -242,6 +257,9 @@ impl JMAP {
.into_http_response())
}
(None, &Method::POST) => {
+ // Validate the access token
+ access_token.assert_has_permission(Permission::SettingsUpdate)?;
+
let changes = serde_json::from_slice::<Vec<UpdateSettings>>(
body.as_deref().unwrap_or_default(),
)
diff --git a/crates/jmap/src/api/management/sieve.rs b/crates/jmap/src/api/management/sieve.rs
index 1305eafa..8e56ed11 100644
--- a/crates/jmap/src/api/management/sieve.rs
+++ b/crates/jmap/src/api/management/sieve.rs
@@ -7,6 +7,7 @@
use std::time::SystemTime;
use common::{scripts::ScriptModification, IntoString};
+use directory::Permission;
use hyper::Method;
use serde_json::json;
use sieve::{runtime::Variable, Envelope};
@@ -15,6 +16,7 @@ use utils::url_params::UrlParams;
use crate::{
api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse},
+ auth::AccessToken,
JMAP,
};
@@ -41,7 +43,11 @@ impl JMAP {
req: &HttpRequest,
path: Vec<&str>,
body: Option<Vec<u8>>,
+ access_token: &AccessToken,
) -> trc::Result<HttpResponse> {
+ // Validate the access token
+ access_token.assert_has_permission(Permission::SieveRun)?;
+
let (script, script_id) = match (
path.get(1).and_then(|name| {
self.core
diff --git a/crates/jmap/src/api/management/stores.rs b/crates/jmap/src/api/management/stores.rs
index c8066441..038f7286 100644
--- a/crates/jmap/src/api/management/stores.rs
+++ b/crates/jmap/src/api/management/stores.rs
@@ -6,7 +6,10 @@
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
use common::manager::webadmin::Resource;
-use directory::backend::internal::manage::{self, ManageDirectory};
+use directory::{
+ backend::internal::manage::{self, ManageDirectory},
+ Permission,
+};
use hyper::Method;
use serde_json::json;
use utils::url_params::UrlParams;
@@ -16,6 +19,7 @@ use crate::{
http::{HttpSessionData, ToHttpResponse},
HttpRequest, HttpResponse, JsonResponse,
},
+ auth::AccessToken,
services::housekeeper::{Event, PurgeType},
JMAP,
};
@@ -29,6 +33,7 @@ impl JMAP {
path: Vec<&str>,
body: Option<Vec<u8>>,
session: &HttpSessionData,
+ access_token: &AccessToken,
) -> trc::Result<HttpResponse> {
match (
path.get(1).copied(),
@@ -37,6 +42,9 @@ impl JMAP {
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| {
@@ -69,6 +77,9 @@ impl JMAP {
.into_http_response())
}
(Some("purge"), Some("blob"), _, &Method::GET) => {
+ // Validate the access token
+ access_token.assert_has_permission(Permission::PurgeBlobStore)?;
+
self.housekeeper_request(Event::Purge(PurgeType::Blobs {
store: self.core.storage.data.clone(),
blob_store: self.core.storage.blob.clone(),
@@ -76,6 +87,9 @@ impl JMAP {
.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 {
if let Some(store) = self.core.storage.stores.get(id) {
store.clone()
@@ -90,6 +104,9 @@ impl JMAP {
.await
}
(Some("purge"), Some("lookup"), id, &Method::GET) => {
+ // Validate the access token
+ access_token.assert_has_permission(Permission::PurgeLookupStore)?;
+
let store = if let Some(id) = id {
if let Some(store) = self.core.storage.lookups.get(id) {
store.clone()
@@ -104,6 +121,9 @@ impl JMAP {
.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
@@ -133,6 +153,9 @@ impl JMAP {
// 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
diff --git a/crates/jmap/src/api/request.rs b/crates/jmap/src/api/request.rs
index f41e0b76..59f2d81a 100644
--- a/crates/jmap/src/api/request.rs
+++ b/crates/jmap/src/api/request.rs
@@ -135,6 +135,11 @@ impl JMAP {
session: &HttpSessionData,
) -> trc::Result<ResponseMethod> {
let op_start = Instant::now();
+
+ // Check permissions
+ access_token.assert_has_jmap_permission(&method)?;
+
+ // Handle method
let response = match method {
RequestMethod::Get(mut req) => match req.take_arguments() {
get::RequestArguments::Email(arguments) => {
@@ -177,15 +182,7 @@ impl JMAP {
self.vacation_response_get(req).await?.into()
}
- get::RequestArguments::Principal => {
- if self.core.jmap.principal_allow_lookups || access_token.is_super_user() {
- self.principal_get(req).await?.into()
- } else {
- return Err(trc::JmapEvent::Forbidden
- .into_err()
- .details("Principal lookups are disabled".to_string()));
- }
- }
+ get::RequestArguments::Principal => self.principal_get(req).await?.into(),
get::RequestArguments::Quota => {
access_token.assert_is_member(req.account_id)?;
@@ -225,13 +222,7 @@ impl JMAP {
self.sieve_script_query(req).await?.into()
}
query::RequestArguments::Principal => {
- if self.core.jmap.principal_allow_lookups || access_token.is_super_user() {
- self.principal_query(req, session).await?.into()
- } else {
- return Err(trc::JmapEvent::Forbidden
- .into_err()
- .details("Principal lookups are disabled".to_string()));
- }
+ self.principal_query(req, session).await?.into()
}
query::RequestArguments::Quota => {
access_token.assert_is_member(req.account_id)?;
diff --git a/crates/jmap/src/api/session.rs b/crates/jmap/src/api/session.rs
index 15c342d2..3a426e9b 100644
--- a/crates/jmap/src/api/session.rs
+++ b/crates/jmap/src/api/session.rs
@@ -6,7 +6,7 @@
use std::sync::Arc;
-use directory::QueryBy;
+use directory::{backend::internal::PrincipalField, QueryBy};
use jmap_proto::{
request::capability::{Capability, Session},
types::{acl::Acl, collection::Collection, id::Id},
@@ -52,7 +52,7 @@ impl JMAP {
.query(QueryBy::Id(*id), false)
.await
.caused_by(trc::location!())?
- .map(|p| p.name)
+ .and_then(|mut p| p.take_str(PrincipalField::Name))
.unwrap_or_else(|| Id::from(*id).to_string()),
is_personal,
is_readonly,
diff --git a/crates/jmap/src/auth/acl.rs b/crates/jmap/src/auth/acl.rs
index 490f675d..d9ec2d2b 100644
--- a/crates/jmap/src/auth/acl.rs
+++ b/crates/jmap/src/auth/acl.rs
@@ -4,7 +4,7 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
-use directory::QueryBy;
+use directory::{backend::internal::PrincipalField, QueryBy};
use jmap_proto::{
error::set::SetError,
object::Object,
@@ -67,17 +67,10 @@ impl JMAP {
}
if !collections.is_empty() {
- if let Some((_, sharing)) = access_token
+ access_token
.access_to
- .iter_mut()
- .find(|(account_id, _)| *account_id == acl_item.to_account_id)
- {
- sharing.union(&collections);
- } else {
- access_token
- .access_to
- .push((acl_item.to_account_id, collections));
- }
+ .get_mut_or_insert_with(acl_item.to_account_id, Bitmap::new)
+ .union(&collections);
}
}
}
@@ -322,7 +315,7 @@ impl JMAP {
{
let mut acl_obj = Object::with_capacity(value.len() / 2);
for item in value {
- if let Some(principal) = self
+ if let Some(mut principal) = self
.core
.storage
.directory
@@ -331,7 +324,7 @@ impl JMAP {
.unwrap_or_default()
{
acl_obj.append(
- Property::_T(principal.name),
+ Property::_T(principal.take_str(PrincipalField::Name).unwrap_or_default()),
item.grants
.map(|acl_item| Value::Text(acl_item.to_string()))
.collect::<Vec<_>>(),
@@ -402,7 +395,7 @@ impl JMAP {
{
Ok(Some(principal)) => {
acls.push(AclGrant {
- account_id: principal.id,
+ account_id: principal.id(),
grants: Bitmap::from(*grants),
});
}
@@ -443,7 +436,7 @@ impl JMAP {
{
Ok(Some(principal)) => Ok((
AclGrant {
- account_id: principal.id,
+ account_id: principal.id(),
grants: Bitmap::from(*grants),
},
acl_patch.get(2).map(|v| v.as_bool().unwrap_or(false)),
diff --git a/crates/jmap/src/auth/mod.rs b/crates/jmap/src/auth/mod.rs
index 6830f82a..3e5c9f66 100644
--- a/crates/jmap/src/auth/mod.rs
+++ b/crates/jmap/src/auth/mod.rs
@@ -14,10 +14,14 @@ use aes_gcm_siv::{
AeadInPlace, Aes256GcmSiv, KeyInit, Nonce,
};
-use directory::{Principal, Type};
-use jmap_proto::types::{collection::Collection, id::Id};
+use directory::{backend::internal::PrincipalField, Permission, Principal, PERMISSION_BITMAP_SIZE};
+use jmap_proto::{
+ request::RequestMethod,
+ types::{collection::Collection, id::Id},
+};
use store::blake3;
-use utils::map::bitmap::Bitmap;
+use trc::ipc::bitset::Bitset;
+use utils::map::{bitmap::Bitmap, vec_map::VecMap};
pub mod acl;
pub mod authenticate;
@@ -28,30 +32,45 @@ pub mod rate_limit;
pub struct AccessToken {
pub primary_id: u32,
pub member_of: Vec<u32>,
- pub access_to: Vec<(u32, Bitmap<Collection>)>,
+ pub access_to: VecMap<u32, Bitmap<Collection>>,
pub name: String,
pub description: Option<String>,
pub quota: u64,
- pub is_superuser: bool,
+ pub permissions: Bitset<PERMISSION_BITMAP_SIZE>,
}
impl AccessToken {
- pub fn new(principal: Principal<u32>) -> Self {
+ pub fn new(mut principal: Principal) -> Self {
Self {
- primary_id: principal.id,
- member_of: principal.member_of,
- access_to: Vec::new(),
- name: principal.name,
- description: principal.description,
- quota: principal.quota,
- is_superuser: principal.typ == Type::Superuser,
+ primary_id: principal.id(),
+ member_of: principal
+ .iter_int(PrincipalField::MemberOf)
+ .map(|v| v as u32)
+ .collect(),
+ access_to: VecMap::new(),
+ name: principal.take_str(PrincipalField::Name).unwrap_or_default(),
+ description: principal.take_str(PrincipalField::Description),
+ quota: principal.quota(),
+ permissions: Default::default(),
}
}
- pub fn with_access_to(self, access_to: Vec<(u32, Bitmap<Collection>)>) -> Self {
+ pub fn from_id(primary_id: u32) -> Self {
+ Self {
+ primary_id,
+ ..Default::default()
+ }
+ }
+
+ pub fn with_access_to(self, access_to: VecMap<u32, Bitmap<Collection>>) -> Self {
Self { access_to, ..self }
}
+ pub fn with_permission(mut self, permission: Permission) -> Self {
+ self.permissions.set(permission.id());
+ self
+ }
+
pub fn state(&self) -> u32 {
// Hash state
let mut s = DefaultHasher::new();
@@ -71,15 +90,44 @@ impl AccessToken {
}
pub fn is_member(&self, account_id: u32) -> bool {
- self.primary_id == account_id || self.member_of.contains(&account_id) || self.is_superuser
+ self.primary_id == account_id
+ || self.member_of.contains(&account_id)
+ || self.has_permission(Permission::Impersonate)
}
pub fn is_primary_id(&self, account_id: u32) -> bool {
self.primary_id == account_id
}
- pub fn is_super_user(&self) -> bool {
- self.is_superuser
+ #[inline(always)]
+ pub fn has_permission(&self, permission: Permission) -> bool {
+ self.permissions.get(permission.id())
+ }
+
+ pub fn assert_has_permission(&self, permission: Permission) -> trc::Result<()> {
+ if self.has_permission(permission) {
+ Ok(())
+ } else {
+ Err(trc::SecurityEvent::Unauthorized
+ .into_err()
+ .details(permission.name()))
+ }
+ }
+
+ pub fn permissions(&self) -> Vec<Permission> {
+ let mut permissions = Vec::new();
+ for (block_num, bytes) in self.permissions.inner().iter().enumerate() {
+ let mut bytes = *bytes;
+
+ while bytes != 0 {
+ let item = std::mem::size_of::<usize>() - 1 - bytes.leading_zeros() as usize;
+ bytes ^= 1 << item;
+ permissions.push(
+ Permission::from_id((block_num * std::mem::size_of::<usize>()) + item).unwrap(),
+ );
+ }
+ }
+ permissions
}
pub fn is_shared(&self, account_id: u32) -> bool {
@@ -131,6 +179,127 @@ impl AccessToken {
.details(format!("You are not an owner of account {}", account_id)))
}
}
+
+ pub fn assert_has_jmap_permission(&self, request: &RequestMethod) -> trc::Result<()> {
+ let permission = match request {
+ RequestMethod::Get(m) => match &m.arguments {
+ jmap_proto::method::get::RequestArguments::Email(_) => Permission::JmapEmailGet,
+ jmap_proto::method::get::RequestArguments::Mailbox => Permission::JmapMailboxGet,
+ jmap_proto::method::get::RequestArguments::Thread => Permission::JmapThreadGet,
+ jmap_proto::method::get::RequestArguments::Identity => Permission::JmapIdentityGet,
+ jmap_proto::method::get::RequestArguments::EmailSubmission => {
+ Permission::JmapEmailSubmissionGet
+ }
+ jmap_proto::method::get::RequestArguments::PushSubscription => {
+ Permission::JmapPushSubscriptionGet
+ }
+ jmap_proto::method::get::RequestArguments::SieveScript => {
+ Permission::JmapSieveScriptGet
+ }
+ jmap_proto::method::get::RequestArguments::VacationResponse => {
+ Permission::JmapVacationResponseGet
+ }
+ jmap_proto::method::get::RequestArguments::Principal => {
+ Permission::JmapPrincipalGet
+ }
+ jmap_proto::method::get::RequestArguments::Quota => Permission::JmapQuotaGet,
+ jmap_proto::method::get::RequestArguments::Blob(_) => Permission::JmapBlobGet,
+ },
+ RequestMethod::Set(m) => match &m.arguments {
+ jmap_proto::method::set::RequestArguments::Email => Permission::JmapEmailSet,
+ jmap_proto::method::set::RequestArguments::Mailbox(_) => Permission::JmapMailboxSet,
+ jmap_proto::method::set::RequestArguments::Identity => Permission::JmapIdentitySet,
+ jmap_proto::method::set::RequestArguments::EmailSubmission(_) => {
+ Permission::JmapEmailSubmissionSet
+ }
+ jmap_proto::method::set::RequestArguments::PushSubscription => {
+ Permission::JmapPushSubscriptionSet
+ }
+ jmap_proto::method::set::RequestArguments::SieveScript(_) => {
+ Permission::JmapSieveScriptSet
+ }
+ jmap_proto::method::set::RequestArguments::VacationResponse => {
+ Permission::JmapVacationResponseSet
+ }
+ },
+ RequestMethod::Changes(m) => match m.arguments {
+ jmap_proto::method::changes::RequestArguments::Email => {
+ Permission::JmapEmailChanges
+ }
+ jmap_proto::method::changes::RequestArguments::Mailbox => {
+ Permission::JmapMailboxChanges
+ }
+ jmap_proto::method::changes::RequestArguments::Thread => {
+ Permission::JmapThreadChanges
+ }
+ jmap_proto::method::changes::RequestArguments::Identity => {
+ Permission::JmapIdentityChanges
+ }
+ jmap_proto::method::changes::RequestArguments::EmailSubmission => {
+ Permission::JmapEmailSubmissionChanges
+ }
+ jmap_proto::method::changes::RequestArguments::Quota => {
+ Permission::JmapQuotaChanges
+ }
+ },
+ RequestMethod::Copy(m) => match m.arguments {
+ jmap_proto::method::copy::RequestArguments::Email => Permission::JmapEmailCopy,
+ },
+ RequestMethod::CopyBlob(_) => Permission::JmapBlobCopy,
+ RequestMethod::ImportEmail(_) => Permission::JmapEmailImport,
+ RequestMethod::ParseEmail(_) => Permission::JmapEmailParse,
+ RequestMethod::QueryChanges(m) => match m.arguments {
+ jmap_proto::method::query::RequestArguments::Email(_) => {
+ Permission::JmapEmailQueryChanges
+ }
+ jmap_proto::method::query::RequestArguments::Mailbox(_) => {
+ Permission::JmapMailboxQueryChanges
+ }
+ jmap_proto::method::query::RequestArguments::EmailSubmission => {
+ Permission::JmapEmailSubmissionQueryChanges
+ }
+ jmap_proto::method::query::RequestArguments::SieveScript => {
+ Permission::JmapSieveScriptQueryChanges
+ }
+ jmap_proto::method::query::RequestArguments::Principal => {
+ Permission::JmapPrincipalQueryChanges
+ }
+ jmap_proto::method::query::RequestArguments::Quota => {
+ Permission::JmapQuotaQueryChanges
+ }
+ },
+ RequestMethod::Query(m) => match m.arguments {
+ jmap_proto::method::query::RequestArguments::Email(_) => Permission::JmapEmailQuery,
+ jmap_proto::method::query::RequestArguments::Mailbox(_) => {
+ Permission::JmapMailboxQuery
+ }
+ jmap_proto::method::query::RequestArguments::EmailSubmission => {
+ Permission::JmapEmailSubmissionQuery
+ }
+ jmap_proto::method::query::RequestArguments::SieveScript => {
+ Permission::JmapSieveScriptQuery
+ }
+ jmap_proto::method::query::RequestArguments::Principal => {
+ Permission::JmapPrincipalQuery
+ }
+ jmap_proto::method::query::RequestArguments::Quota => Permission::JmapQuotaQuery,
+ },
+ RequestMethod::SearchSnippet(_) => Permission::JmapSearchSnippet,
+ RequestMethod::ValidateScript(_) => Permission::JmapSieveScriptValidate,
+ RequestMethod::LookupBlob(_) => Permission::JmapBlobLookup,
+ RequestMethod::UploadBlob(_) => Permission::JmapBlobUpload,
+ RequestMethod::Echo(_) => Permission::JmapEcho,
+ RequestMethod::Error(_) => return Ok(()),
+ };
+
+ if self.has_permission(permission) {
+ Ok(())
+ } else {
+ Err(trc::JmapEvent::Forbidden
+ .into_err()
+ .details("You are not authorized to perform this action"))
+ }
+ }
}
pub struct SymmetricEncrypt {
diff --git a/crates/jmap/src/auth/oauth/auth.rs b/crates/jmap/src/auth/oauth/auth.rs
index 38cf87e6..b25c13cf 100644
--- a/crates/jmap/src/auth/oauth/auth.rs
+++ b/crates/jmap/src/auth/oauth/auth.rs
@@ -91,8 +91,8 @@ impl JMAP {
json!({
"data": {
"code": client_code,
- "is_admin": access_token.is_super_user(),
- "is_enterprise": is_enterprise,
+ "permissions": access_token.permissions(),
+ "isEnterprise": is_enterprise,
},
})
}
diff --git a/crates/jmap/src/auth/oauth/token.rs b/crates/jmap/src/auth/oauth/token.rs
index 1817399c..590a9c4d 100644
--- a/crates/jmap/src/auth/oauth/token.rs
+++ b/crates/jmap/src/auth/oauth/token.rs
@@ -6,7 +6,7 @@
use std::time::SystemTime;
-use directory::QueryBy;
+use directory::{backend::internal::PrincipalField, QueryBy};
use hyper::StatusCode;
use mail_builder::encoders::base64::base64_encode;
use mail_parser::decoders::base64::base64_decode;
@@ -187,7 +187,8 @@ impl JMAP {
.await
.map_err(|_| "Temporary lookup error")?
.ok_or("Account no longer exists")?
- .secrets
+ .take_str_array(PrincipalField::Secrets)
+ .unwrap_or_default()
.into_iter()
.next()
.ok_or("Failed to obtain password hash")
diff --git a/crates/jmap/src/auth/rate_limit.rs b/crates/jmap/src/auth/rate_limit.rs
index b5ac3491..97548d20 100644
--- a/crates/jmap/src/auth/rate_limit.rs
+++ b/crates/jmap/src/auth/rate_limit.rs
@@ -7,6 +7,7 @@
use std::{net::IpAddr, sync::Arc};
use common::listener::limiter::{ConcurrencyLimiter, InFlight};
+use directory::Permission;
use trc::AddContext;
use crate::JMAP;
@@ -61,12 +62,12 @@ impl JMAP {
if is_rate_allowed {
if let Some(in_flight_request) = limiter.concurrent_requests.is_allowed() {
Ok(in_flight_request)
- } else if access_token.is_super_user() {
+ } else if access_token.has_permission(Permission::UnlimitedRequests) {
Ok(InFlight::default())
} else {
Err(trc::LimitEvent::ConcurrentRequest.into_err())
}
- } else if access_token.is_super_user() {
+ } else if access_token.has_permission(Permission::UnlimitedRequests) {
Ok(InFlight::default())
} else {
Err(trc::LimitEvent::TooManyRequests.into_err())
@@ -97,7 +98,7 @@ impl JMAP {
.is_allowed()
{
Ok(in_flight_request)
- } else if access_token.is_super_user() {
+ } else if access_token.has_permission(Permission::UnlimitedRequests) {
Ok(InFlight::default())
} else {
Err(trc::LimitEvent::ConcurrentUpload.into_err())
diff --git a/crates/jmap/src/blob/upload.rs b/crates/jmap/src/blob/upload.rs
index 7167f948..867b6cb7 100644
--- a/crates/jmap/src/blob/upload.rs
+++ b/crates/jmap/src/blob/upload.rs
@@ -6,6 +6,7 @@
use std::sync::Arc;
+use directory::Permission;
use jmap_proto::{
error::set::SetError,
method::upload::{
@@ -149,7 +150,7 @@ impl JMAP {
&& used.bytes + data.len() > self.core.jmap.upload_tmp_quota_size)
|| (self.core.jmap.upload_tmp_quota_amount > 0
&& used.count + 1 > self.core.jmap.upload_tmp_quota_amount))
- && !access_token.is_super_user()
+ && !access_token.has_permission(Permission::UnlimitedUploads)
{
response.not_created.append(
create_id,
@@ -209,7 +210,7 @@ impl JMAP {
&& used.bytes + data.len() > self.core.jmap.upload_tmp_quota_size)
|| (self.core.jmap.upload_tmp_quota_amount > 0
&& used.count + 1 > self.core.jmap.upload_tmp_quota_amount))
- && !access_token.is_super_user()
+ && !access_token.has_permission(Permission::UnlimitedUploads)
{
let err = Err(trc::LimitEvent::BlobQuota
.into_err()
diff --git a/crates/jmap/src/identity/get.rs b/crates/jmap/src/identity/get.rs
index 360219f1..f02d8e24 100644
--- a/crates/jmap/src/identity/get.rs
+++ b/crates/jmap/src/identity/get.rs
@@ -4,7 +4,7 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
-use directory::QueryBy;
+use directory::{backend::internal::PrincipalField, QueryBy};
use jmap_proto::{
method::get::{GetRequest, GetResponse, RequestArguments},
object::Object,
@@ -125,7 +125,8 @@ impl JMAP {
.await
.caused_by(trc::location!())?
.unwrap_or_default();
- if principal.emails.is_empty() {
+ let num_emails = principal.field_len(PrincipalField::Emails);
+ if num_emails == 0 {
return Ok(identity_ids);
}
@@ -136,14 +137,14 @@ impl JMAP {
// Create identities
let name = principal
- .description
- .unwrap_or(principal.name)
+ .description()
+ .unwrap_or(principal.name())
.trim()
.to_string();
- let has_many = principal.emails.len() > 1;
- for (idx, email) in principal.emails.into_iter().enumerate() {
+ let has_many = num_emails > 1;
+ for (idx, email) in principal.iter_str(PrincipalField::Emails).enumerate() {
let document_id = idx as u32;
- let email = sanitize_email(&email).unwrap_or_default();
+ let email = sanitize_email(email).unwrap_or_default();
if email.is_empty() {
continue;
}
diff --git a/crates/jmap/src/identity/set.rs b/crates/jmap/src/identity/set.rs
index e840f90f..3ab94e51 100644
--- a/crates/jmap/src/identity/set.rs
+++ b/crates/jmap/src/identity/set.rs
@@ -4,7 +4,7 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
-use directory::QueryBy;
+use directory::{backend::internal::PrincipalField, QueryBy};
use jmap_proto::{
error::set::SetError,
method::set::{RequestArguments, SetRequest, SetResponse},
@@ -61,11 +61,9 @@ impl JMAP {
.storage
.directory
.query(QueryBy::Id(account_id), false)
- .await
+ .await?
.unwrap_or_default()
- .unwrap_or_default()
- .emails
- .contains(email)
+ .has_str_value(PrincipalField::Emails, email)
{
response.not_created.append(
id,
diff --git a/crates/jmap/src/lib.rs b/crates/jmap/src/lib.rs
index c99ff030..5eb6beb6 100644
--- a/crates/jmap/src/lib.rs
+++ b/crates/jmap/src/lib.rs
@@ -329,7 +329,7 @@ impl JMAP {
.query(QueryBy::Id(account_id), false)
.await
.add_context(|err| err.caused_by(trc::location!()).account_id(account_id))?
- .map(|p| p.quota as i64)
+ .map(|p| p.quota() as i64)
.unwrap_or_default()
})
}
diff --git a/crates/jmap/src/mailbox/set.rs b/crates/jmap/src/mailbox/set.rs
index 3a765296..eea8fb13 100644
--- a/crates/jmap/src/mailbox/set.rs
+++ b/crates/jmap/src/mailbox/set.rs
@@ -5,6 +5,7 @@
*/
use common::config::jmap::settings::SpecialUse;
+use directory::Permission;
use jmap_proto::{
error::set::{SetError, SetErrorType},
method::set::{SetRequest, SetResponse},
@@ -295,7 +296,9 @@ impl JMAP {
) -> trc::Result<Result<bool, SetError>> {
// Internal folders cannot be deleted
#[cfg(feature = "test_mode")]
- if [INBOX_ID, TRASH_ID].contains(&document_id) && !access_token.is_super_user() {
+ if [INBOX_ID, TRASH_ID].contains(&document_id)
+ && !access_token.has_permission(Permission::DeleteSystemFolders)
+ {
return Ok(Err(SetError::forbidden().with_description(
"You are not allowed to delete Inbox, Junk or Trash folders.",
)));
diff --git a/crates/jmap/src/principal/get.rs b/crates/jmap/src/principal/get.rs
index b2ab9104..b59abd77 100644
--- a/crates/jmap/src/principal/get.rs
+++ b/crates/jmap/src/principal/get.rs
@@ -4,7 +4,7 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
-use directory::QueryBy;
+use directory::{backend::internal::PrincipalField, QueryBy};
use jmap_proto::{
method::get::{GetRequest, GetResponse, RequestArguments},
object::Object,
@@ -67,16 +67,15 @@ impl JMAP {
for property in &properties {
let value = match property {
Property::Id => Value::Id(id),
- Property::Type => Value::Text(principal.typ.to_jmap().to_string()),
- Property::Name => Value::Text(principal.name.clone()),
+ Property::Type => Value::Text(principal.typ().to_jmap().to_string()),
+ Property::Name => Value::Text(principal.name().to_string()),
Property::Description => principal
- .description
- .clone()
- .map(Value::Text)
+ .description()
+ .map(|v| Value::Text(v.to_string()))
.unwrap_or(Value::Null),
Property::Email => principal
- .emails
- .first()
+ .iter_str(PrincipalField::Emails)
+ .next()
.map(|email| Value::Text(email.clone()))
.unwrap_or(Value::Null),
_ => Value::Null,
diff --git a/crates/jmap/src/principal/query.rs b/crates/jmap/src/principal/query.rs
index ed53c4aa..0ed616ea 100644
--- a/crates/jmap/src/principal/query.rs
+++ b/crates/jmap/src/principal/query.rs
@@ -37,9 +37,9 @@ impl JMAP {
.query(QueryBy::Name(name.as_str()), false)
.await?
{
- if is_set || result_set.results.contains(principal.id) {
+ if is_set || result_set.results.contains(principal.id()) {
result_set.results =
- RoaringBitmap::from_sorted_iter([principal.id]).unwrap();
+ RoaringBitmap::from_sorted_iter([principal.id()]).unwrap();
} else {
result_set.results = RoaringBitmap::new();
}
diff --git a/crates/jmap/src/services/ingest.rs b/crates/jmap/src/services/ingest.rs
index ca2548b9..af20adb5 100644
--- a/crates/jmap/src/services/ingest.rs
+++ b/crates/jmap/src/services/ingest.rs
@@ -104,7 +104,7 @@ impl JMAP {
.query(QueryBy::Id(*uid), false)
.await
{
- Ok(Some(p)) => p.quota as i64,
+ Ok(Some(p)) => p.quota() as i64,
Ok(None) => 0,
Err(err) => {
trc::error!(err
diff --git a/crates/jmap/src/sieve/ingest.rs b/crates/jmap/src/sieve/ingest.rs
index 08ccb3a5..a107bf2f 100644
--- a/crates/jmap/src/sieve/ingest.rs
+++ b/crates/jmap/src/sieve/ingest.rs
@@ -7,7 +7,7 @@
use std::borrow::Cow;
use common::listener::stream::NullIo;
-use directory::QueryBy;
+use directory::{backend::internal::PrincipalField, QueryBy};
use jmap_proto::types::{collection::Collection, id::Id, keyword::Keyword, property::Property};
use mail_parser::MessageParser;
use sieve::{Envelope, Event, Input, Mailbox, Recipient};
@@ -72,9 +72,15 @@ impl JMAP {
.query(QueryBy::Id(account_id), false)
.await
{
- Ok(Some(p)) => {
+ Ok(Some(mut p)) => {
instance.set_user_full_name(p.description().unwrap_or_else(|| p.name()));
- (p.quota as i64, p.emails.into_iter().next())
+ (
+ p.quota() as i64,
+ p.take_str_array(PrincipalField::Emails)
+ .unwrap_or_default()
+ .into_iter()
+ .next(),
+ )
}
Ok(None) => (0, None),
Err(err) => {
diff --git a/crates/managesieve/src/op/authenticate.rs b/crates/managesieve/src/op/authenticate.rs
index 80e7b298..13198e70 100644
--- a/crates/managesieve/src/op/authenticate.rs
+++ b/crates/managesieve/src/op/authenticate.rs
@@ -5,6 +5,7 @@
*/
use common::listener::{limiter::ConcurrencyLimiter, SessionStream};
+use directory::Permission;
use imap::op::authenticate::{decode_challenge_oauth, decode_challenge_plain};
use imap_proto::{
protocol::authenticate::Mechanism,
@@ -116,6 +117,9 @@ impl<T: SessionStream> Session<T> {
}
};
+ // Validate access
+ access_token.assert_has_permission(Permission::SieveAuthenticate)?;
+
// Cache access token
let access_token = Arc::new(access_token);
self.jmap.cache_access_token(access_token.clone());
diff --git a/crates/managesieve/src/op/checkscript.rs b/crates/managesieve/src/op/checkscript.rs
index c014ea0b..58f79217 100644
--- a/crates/managesieve/src/op/checkscript.rs
+++ b/crates/managesieve/src/op/checkscript.rs
@@ -6,13 +6,17 @@
use std::time::Instant;
+use common::listener::SessionStream;
+use directory::Permission;
use imap_proto::receiver::Request;
-use tokio::io::{AsyncRead, AsyncWrite};
use crate::core::{Command, Session, StatusResponse};
-impl<T: AsyncRead + AsyncWrite> Session<T> {
+impl<T: SessionStream> Session<T> {
pub async fn handle_checkscript(&mut self, request: Request<Command>) -> trc::Result<Vec<u8>> {
+ // Validate access
+ self.assert_has_permission(Permission::SieveCheckScript)?;
+
let op_start = Instant::now();
if request.tokens.is_empty() {
diff --git a/crates/managesieve/src/op/deletescript.rs b/crates/managesieve/src/op/deletescript.rs
index 092ffa8b..bd7219a9 100644
--- a/crates/managesieve/src/op/deletescript.rs
+++ b/crates/managesieve/src/op/deletescript.rs
@@ -6,16 +6,20 @@
use std::time::Instant;
+use common::listener::SessionStream;
+use directory::Permission;
use imap_proto::receiver::Request;
use jmap_proto::types::collection::Collection;
use store::write::log::ChangeLogBuilder;
-use tokio::io::{AsyncRead, AsyncWrite};
use trc::AddContext;
use crate::core::{Command, ResponseCode, Session, StatusResponse};
-impl<T: AsyncRead + AsyncWrite> Session<T> {
+impl<T: SessionStream> Session<T> {
pub async fn handle_deletescript(&mut self, request: Request<Command>) -> trc::Result<Vec<u8>> {
+ // Validate access
+ self.assert_has_permission(Permission::SieveDeleteScript)?;
+
let op_start = Instant::now();
let name = request
diff --git a/crates/managesieve/src/op/getscript.rs b/crates/managesieve/src/op/getscript.rs
index 77406bea..e21f368d 100644
--- a/crates/managesieve/src/op/getscript.rs
+++ b/crates/managesieve/src/op/getscript.rs
@@ -6,19 +6,23 @@
use std::time::Instant;
+use common::listener::SessionStream;
+use directory::Permission;
use imap_proto::receiver::Request;
use jmap::sieve::set::ObjectBlobId;
use jmap_proto::{
object::Object,
types::{collection::Collection, property::Property, value::Value},
};
-use tokio::io::{AsyncRead, AsyncWrite};
use trc::AddContext;
use crate::core::{Command, ResponseCode, Session, StatusResponse};
-impl<T: AsyncRead + AsyncWrite> Session<T> {
+impl<T: SessionStream> Session<T> {
pub async fn handle_getscript(&mut self, request: Request<Command>) -> trc::Result<Vec<u8>> {
+ // Validate access
+ self.assert_has_permission(Permission::SieveGetScript)?;
+
let op_start = Instant::now();
let name = request
.tokens
diff --git a/crates/managesieve/src/op/havespace.rs b/crates/managesieve/src/op/havespace.rs
index 05eeb59e..056c6208 100644
--- a/crates/managesieve/src/op/havespace.rs
+++ b/crates/managesieve/src/op/havespace.rs
@@ -6,14 +6,18 @@
use std::time::Instant;
+use common::listener::SessionStream;
+use directory::Permission;
use imap_proto::receiver::Request;
-use tokio::io::{AsyncRead, AsyncWrite};
use trc::AddContext;
use crate::core::{Command, ResponseCode, Session, StatusResponse};
-impl<T: AsyncRead + AsyncWrite> Session<T> {
+impl<T: SessionStream> Session<T> {
pub async fn handle_havespace(&mut self, request: Request<Command>) -> trc::Result<Vec<u8>> {
+ // Validate access
+ self.assert_has_permission(Permission::SieveHaveSpace)?;
+
let op_start = Instant::now();
let mut tokens = request.tokens.into_iter();
let name = tokens
diff --git a/crates/managesieve/src/op/listscripts.rs b/crates/managesieve/src/op/listscripts.rs
index 5d3b004e..5659d293 100644
--- a/crates/managesieve/src/op/listscripts.rs
+++ b/crates/managesieve/src/op/listscripts.rs
@@ -6,17 +6,21 @@
use std::time::Instant;
+use common::listener::SessionStream;
+use directory::Permission;
use jmap_proto::{
object::Object,
types::{collection::Collection, property::Property, value::Value},
};
-use tokio::io::{AsyncRead, AsyncWrite};
use trc::AddContext;
use crate::core::{Session, StatusResponse};
-impl<T: AsyncRead + AsyncWrite> Session<T> {
+impl<T: SessionStream> Session<T> {
pub async fn handle_listscripts(&mut self) -> trc::Result<Vec<u8>> {
+ // Validate access
+ self.assert_has_permission(Permission::SieveListScripts)?;
+
let op_start = Instant::now();
let account_id = self.state.access_token().primary_id();
let document_ids = self
diff --git a/crates/managesieve/src/op/mod.rs b/crates/managesieve/src/op/mod.rs
index bd92df4d..e49dc19e 100644
--- a/crates/managesieve/src/op/mod.rs
+++ b/crates/managesieve/src/op/mod.rs
@@ -5,8 +5,9 @@
*/
use common::listener::SessionStream;
+use directory::Permission;
-use crate::core::{Session, StatusResponse};
+use crate::core::{Session, State, StatusResponse};
pub mod authenticate;
pub mod capability;
@@ -31,4 +32,13 @@ impl<T: SessionStream> Session<T> {
Ok(StatusResponse::ok("Begin TLS negotiation now").into_bytes())
}
+
+ pub fn assert_has_permission(&self, permission: Permission) -> trc::Result<()> {
+ match &self.state {
+ State::Authenticated { access_token, .. } => {
+ access_token.assert_has_permission(permission)
+ }
+ State::NotAuthenticated { .. } => Ok(()),
+ }
+ }
}
diff --git a/crates/managesieve/src/op/putscript.rs b/crates/managesieve/src/op/putscript.rs
index b6a7c05b..24ab413a 100644
--- a/crates/managesieve/src/op/putscript.rs
+++ b/crates/managesieve/src/op/putscript.rs
@@ -6,6 +6,8 @@
use std::time::Instant;
+use common::listener::SessionStream;
+use directory::Permission;
use imap_proto::receiver::Request;
use jmap::sieve::set::{ObjectBlobId, SCHEMA};
use jmap_proto::{
@@ -18,13 +20,15 @@ use store::{
write::{assert::HashedValue, log::LogInsert, BatchBuilder, BlobOp, DirectoryClass},
BlobClass,
};
-use tokio::io::{AsyncRead, AsyncWrite};
use trc::AddContext;
use crate::core::{Command, ResponseCode, Session, StatusResponse};
-impl<T: AsyncRead + AsyncWrite> Session<T> {
+impl<T: SessionStream> Session<T> {
pub async fn handle_putscript(&mut self, request: Request<Command>) -> trc::Result<Vec<u8>> {
+ // Validate access
+ self.assert_has_permission(Permission::SievePutScript)?;
+
let op_start = Instant::now();
let mut tokens = request.tokens.into_iter();
let name = tokens
diff --git a/crates/managesieve/src/op/renamescript.rs b/crates/managesieve/src/op/renamescript.rs
index b0c76d25..6bd84345 100644
--- a/crates/managesieve/src/op/renamescript.rs
+++ b/crates/managesieve/src/op/renamescript.rs
@@ -6,6 +6,8 @@
use std::time::Instant;
+use common::listener::SessionStream;
+use directory::Permission;
use imap_proto::receiver::Request;
use jmap::sieve::set::SCHEMA;
use jmap_proto::{
@@ -13,13 +15,15 @@ use jmap_proto::{
types::{collection::Collection, property::Property, value::Value},
};
use store::write::{assert::HashedValue, log::ChangeLogBuilder, BatchBuilder};
-use tokio::io::{AsyncRead, AsyncWrite};
use trc::AddContext;
use crate::core::{Command, ResponseCode, Session, StatusResponse};
-impl<T: AsyncRead + AsyncWrite> Session<T> {
+impl<T: SessionStream> Session<T> {
pub async fn handle_renamescript(&mut self, request: Request<Command>) -> trc::Result<Vec<u8>> {
+ // Validate access
+ self.assert_has_permission(Permission::SieveRenameScript)?;
+
let op_start = Instant::now();
let mut tokens = request.tokens.into_iter();
let name = tokens
diff --git a/crates/managesieve/src/op/setactive.rs b/crates/managesieve/src/op/setactive.rs
index 616b004d..fa88b725 100644
--- a/crates/managesieve/src/op/setactive.rs
+++ b/crates/managesieve/src/op/setactive.rs
@@ -6,16 +6,20 @@
use std::time::Instant;
+use common::listener::SessionStream;
+use directory::Permission;
use imap_proto::receiver::Request;
use jmap_proto::types::collection::Collection;
use store::write::log::ChangeLogBuilder;
-use tokio::io::{AsyncRead, AsyncWrite};
use trc::AddContext;
use crate::core::{Command, Session, StatusResponse};
-impl<T: AsyncRead + AsyncWrite> Session<T> {
+impl<T: SessionStream> Session<T> {
pub async fn handle_setactive(&mut self, request: Request<Command>) -> trc::Result<Vec<u8>> {
+ // Validate access
+ self.assert_has_permission(Permission::SieveSetActive)?;
+
let op_start = Instant::now();
let name = request
.tokens
diff --git a/crates/pop3/Cargo.toml b/crates/pop3/Cargo.toml
index d24e18c8..36bdfa3d 100644
--- a/crates/pop3/Cargo.toml
+++ b/crates/pop3/Cargo.toml
@@ -7,6 +7,7 @@ resolver = "2"
[dependencies]
store = { path = "../store" }
common = { path = "../common" }
+directory = { path = "../directory" }
jmap = { path = "../jmap" }
imap = { path = "../imap" }
utils = { path = "../utils" }
diff --git a/crates/pop3/src/client.rs b/crates/pop3/src/client.rs
index e70b1c7d..5cea1276 100644
--- a/crates/pop3/src/client.rs
+++ b/crates/pop3/src/client.rs
@@ -9,7 +9,7 @@ use mail_send::Credentials;
use trc::AddContext;
use crate::{
- protocol::{request::Error, response::Response, Command, Mechanism},
+ protocol::{request::Error, Command, Mechanism},
Session, State,
};
@@ -117,54 +117,11 @@ impl<T: SessionStream> Session<T> {
self.write_ok("NOOP").await.map(|_| SessionResult::Continue)
}
Command::Rset => self.handle_rset().await.map(|_| SessionResult::Continue),
- Command::Capa => {
- let mechanisms =
- if self.stream.is_tls() || self.jmap.core.imap.allow_plain_auth {
- vec![Mechanism::Plain, Mechanism::OAuthBearer]
- } else {
- vec![Mechanism::OAuthBearer]
- };
-
- trc::event!(
- Pop3(trc::Pop3Event::Capabilities),
- SpanId = self.session_id,
- Tls = self.stream.is_tls(),
- Strict = !self.jmap.core.imap.allow_plain_auth,
- Elapsed = trc::Value::Duration(0)
- );
-
- self.write_bytes(
- Response::Capability::<u32> {
- mechanisms,
- stls: !self.stream.is_tls(),
- }
- .serialize(),
- )
- .await
- .map(|_| SessionResult::Continue)
- }
+ Command::Capa => self.handle_capa().await.map(|_| SessionResult::Continue),
Command::Stls => {
- trc::event!(
- Pop3(trc::Pop3Event::StartTls),
- SpanId = self.session_id,
- Elapsed = trc::Value::Duration(0)
- );
-
- self.write_ok("Begin TLS negotiation now")
- .await
- .map(|_| SessionResult::UpgradeTls)
- }
- Command::Utf8 => {
- trc::event!(
- Pop3(trc::Pop3Event::Utf8),
- SpanId = self.session_id,
- Elapsed = trc::Value::Duration(0)
- );
-
- self.write_ok("UTF8 enabled")
- .await
- .map(|_| SessionResult::Continue)
+ self.handle_stls().await.map(|_| SessionResult::UpgradeTls)
}
+ Command::Utf8 => self.handle_utf8().await.map(|_| SessionResult::Continue),
Command::Auth { mechanism, params } => self
.handle_sasl(mechanism, params)
.await
diff --git a/crates/pop3/src/lib.rs b/crates/pop3/src/lib.rs
index 6c0177d9..63b2b3d0 100644
--- a/crates/pop3/src/lib.rs
+++ b/crates/pop3/src/lib.rs
@@ -8,7 +8,7 @@ use std::{net::IpAddr, sync::Arc};
use common::listener::{limiter::InFlight, ServerInstance, SessionStream};
use imap::core::{ImapInstance, Inner};
-use jmap::JMAP;
+use jmap::{auth::AccessToken, JMAP};
use mailbox::Mailbox;
use protocol::request::Parser;
@@ -51,6 +51,7 @@ pub enum State {
Authenticated {
mailbox: Mailbox,
in_flight: Option<InFlight>,
+ access_token: Arc<AccessToken>,
},
}
@@ -68,4 +69,11 @@ impl State {
_ => unreachable!(),
}
}
+
+ pub fn access_token(&self) -> &Arc<AccessToken> {
+ match self {
+ State::Authenticated { access_token, .. } => access_token,
+ _ => unreachable!(),
+ }
+ }
}
diff --git a/crates/pop3/src/op/authenticate.rs b/crates/pop3/src/op/authenticate.rs
index c3662c29..39d129a0 100644
--- a/crates/pop3/src/op/authenticate.rs
+++ b/crates/pop3/src/op/authenticate.rs
@@ -5,6 +5,7 @@
*/
use common::listener::{limiter::ConcurrencyLimiter, SessionStream};
+use directory::Permission;
use imap::op::authenticate::{decode_challenge_oauth, decode_challenge_plain};
use jmap::auth::rate_limit::ConcurrencyLimiters;
use mail_parser::decoders::base64::base64_decode;
@@ -112,6 +113,9 @@ impl<T: SessionStream> Session<T> {
}
};
+ // Validate access
+ access_token.assert_has_permission(Permission::Pop3Authenticate)?;
+
// Cache access token
let access_token = Arc::new(access_token);
self.jmap.cache_access_token(access_token.clone());
@@ -120,7 +124,11 @@ impl<T: SessionStream> Session<T> {
let mailbox = self.fetch_mailbox(access_token.primary_id()).await?;
// Create session
- self.state = State::Authenticated { in_flight, mailbox };
+ self.state = State::Authenticated {
+ in_flight,
+ mailbox,
+ access_token,
+ };
self.write_ok("Authentication successful").await
}
diff --git a/crates/pop3/src/op/delete.rs b/crates/pop3/src/op/delete.rs
index 86fbcd1c..4e7799b2 100644
--- a/crates/pop3/src/op/delete.rs
+++ b/crates/pop3/src/op/delete.rs
@@ -7,6 +7,7 @@
use std::time::Instant;
use common::listener::SessionStream;
+use directory::Permission;
use jmap_proto::types::{state::StateChange, type_state::DataType};
use store::roaring::RoaringBitmap;
use trc::AddContext;
@@ -15,6 +16,11 @@ use crate::{protocol::response::Response, Session, State};
impl<T: SessionStream> Session<T> {
pub async fn handle_dele(&mut self, msgs: Vec<u32>) -> trc::Result<()> {
+ // Validate access
+ self.state
+ .access_token()
+ .assert_has_permission(Permission::Pop3Dele)?;
+
let op_start = Instant::now();
let mailbox = self.state.mailbox_mut();
let mut response = Vec::new();
diff --git a/crates/pop3/src/op/fetch.rs b/crates/pop3/src/op/fetch.rs
index 3d395e1c..be322723 100644
--- a/crates/pop3/src/op/fetch.rs
+++ b/crates/pop3/src/op/fetch.rs
@@ -7,6 +7,7 @@
use std::time::Instant;
use common::listener::SessionStream;
+use directory::Permission;
use jmap::email::metadata::MessageMetadata;
use jmap_proto::types::{collection::Collection, property::Property};
use store::write::Bincode;
@@ -16,6 +17,11 @@ use crate::{protocol::response::Response, Session};
impl<T: SessionStream> Session<T> {
pub async fn handle_fetch(&mut self, msg: u32, lines: Option<u32>) -> trc::Result<()> {
+ // Validate access
+ self.state
+ .access_token()
+ .assert_has_permission(Permission::Pop3Retr)?;
+
let op_start = Instant::now();
let mailbox = self.state.mailbox();
if let Some(message) = mailbox.messages.get(msg.saturating_sub(1) as usize) {
diff --git a/crates/pop3/src/op/list.rs b/crates/pop3/src/op/list.rs
index a3519e37..0c004ce0 100644
--- a/crates/pop3/src/op/list.rs
+++ b/crates/pop3/src/op/list.rs
@@ -7,11 +7,17 @@
use std::time::Instant;
use common::listener::SessionStream;
+use directory::Permission;
use crate::{protocol::response::Response, Session};
impl<T: SessionStream> Session<T> {
pub async fn handle_list(&mut self, msg: Option<u32>) -> trc::Result<()> {
+ // Validate access
+ self.state
+ .access_token()
+ .assert_has_permission(Permission::Pop3List)?;
+
let op_start = Instant::now();
let mailbox = self.state.mailbox();
if let Some(msg) = msg {
@@ -48,6 +54,11 @@ impl<T: SessionStream> Session<T> {
}
pub async fn handle_uidl(&mut self, msg: Option<u32>) -> trc::Result<()> {
+ // Validate access
+ self.state
+ .access_token()
+ .assert_has_permission(Permission::Pop3Uidl)?;
+
let op_start = Instant::now();
let mailbox = self.state.mailbox();
if let Some(msg) = msg {
@@ -92,6 +103,11 @@ impl<T: SessionStream> Session<T> {
}
pub async fn handle_stat(&mut self) -> trc::Result<()> {
+ // Validate access
+ self.state
+ .access_token()
+ .assert_has_permission(Permission::Pop3Stat)?;
+
let op_start = Instant::now();
let mailbox = self.state.mailbox();
diff --git a/crates/pop3/src/op/mod.rs b/crates/pop3/src/op/mod.rs
index bb5f22d7..0488caa8 100644
--- a/crates/pop3/src/op/mod.rs
+++ b/crates/pop3/src/op/mod.rs
@@ -4,7 +4,61 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
+use common::listener::SessionStream;
+
+use crate::{
+ protocol::{response::Response, Mechanism},
+ Session,
+};
+
pub mod authenticate;
pub mod delete;
pub mod fetch;
pub mod list;
+
+impl<T: SessionStream> Session<T> {
+ pub async fn handle_capa(&mut self) -> trc::Result<()> {
+ let mechanisms = if self.stream.is_tls() || self.jmap.core.imap.allow_plain_auth {
+ vec![Mechanism::Plain, Mechanism::OAuthBearer]
+ } else {
+ vec![Mechanism::OAuthBearer]
+ };
+
+ trc::event!(
+ Pop3(trc::Pop3Event::Capabilities),
+ SpanId = self.session_id,
+ Tls = self.stream.is_tls(),
+ Strict = !self.jmap.core.imap.allow_plain_auth,
+ Elapsed = trc::Value::Duration(0)
+ );
+
+ self.write_bytes(
+ Response::Capability::<u32> {
+ mechanisms,
+ stls: !self.stream.is_tls(),
+ }
+ .serialize(),
+ )
+ .await
+ }
+
+ pub async fn handle_stls(&mut self) -> trc::Result<()> {
+ trc::event!(
+ Pop3(trc::Pop3Event::StartTls),
+ SpanId = self.session_id,
+ Elapsed = trc::Value::Duration(0)
+ );
+
+ self.write_ok("Begin TLS negotiation now").await
+ }
+
+ pub async fn handle_utf8(&mut self) -> trc::Result<()> {
+ trc::event!(
+ Pop3(trc::Pop3Event::Utf8),
+ SpanId = self.session_id,
+ Elapsed = trc::Value::Duration(0)
+ );
+
+ self.write_ok("UTF8 enabled").await
+ }
+}
diff --git a/crates/smtp/src/inbound/auth.rs b/crates/smtp/src/inbound/auth.rs
index 4e8131cd..d7ff8c24 100644
--- a/crates/smtp/src/inbound/auth.rs
+++ b/crates/smtp/src/inbound/auth.rs
@@ -5,6 +5,7 @@
*/
use common::listener::SessionStream;
+use directory::backend::internal::PrincipalField;
use mail_parser::decoders::base64::base64_decode;
use mail_send::Credentials;
use smtp_proto::{IntoString, AUTH_LOGIN, AUTH_OAUTHBEARER, AUTH_PLAIN, AUTH_XOAUTH2};
@@ -177,10 +178,10 @@ impl<T: SessionStream> Session<T> {
.await
{
Ok(principal) => {
+ let todo = "check smtp auth permissions";
self.data.authenticated_as = authenticated_as.to_lowercase();
self.data.authenticated_emails = principal
- .emails
- .into_iter()
+ .iter_str(PrincipalField::Emails)
.map(|e| e.trim().to_lowercase())
.collect();
self.eval_post_auth_params().await;
diff --git a/crates/store/src/backend/rocksdb/write.rs b/crates/store/src/backend/rocksdb/write.rs
index 1642b629..8f7dd69a 100644
--- a/crates/store/src/backend/rocksdb/write.rs
+++ b/crates/store/src/backend/rocksdb/write.rs
@@ -308,7 +308,9 @@ impl<'x> RocksDBTransaction<'x> {
if !matches {
txn.rollback()?;
- return Err(CommitError::Internal(trc::StoreEvent::AssertValueFailed.into()));
+ return Err(CommitError::Internal(
+ trc::StoreEvent::AssertValueFailed.into(),
+ ));
}
}
}
diff --git a/crates/trc/event-macro/Cargo.toml b/crates/trc/event-macro/Cargo.toml
index 52ea6407..46fe7980 100644
--- a/crates/trc/event-macro/Cargo.toml
+++ b/crates/trc/event-macro/Cargo.toml
@@ -7,6 +7,6 @@ edition = "2021"
proc-macro = true
[dependencies]
-syn = { version = "1.0", features = ["full"] }
+syn = { version = "2.0", features = ["full"] }
quote = "1.0"
proc-macro2 = "1.0"
diff --git a/crates/trc/src/event/description.rs b/crates/trc/src/event/description.rs
index c0ecb311..549a92c0 100644
--- a/crates/trc/src/event/description.rs
+++ b/crates/trc/src/event/description.rs
@@ -1784,6 +1784,7 @@ impl SecurityEvent {
SecurityEvent::BruteForceBan => "Banned due to brute force attack",
SecurityEvent::LoiterBan => "Banned due to loitering",
SecurityEvent::IpBlocked => "Blocked IP address",
+ SecurityEvent::Unauthorized => "Unauthorized access",
}
}
@@ -1797,6 +1798,7 @@ impl SecurityEvent {
}
SecurityEvent::LoiterBan => "IP address was banned due to multiple loitering events",
SecurityEvent::IpBlocked => "Rejected connection from blocked IP address",
+ SecurityEvent::Unauthorized => "Account does not have permission to access resource",
}
}
}
diff --git a/crates/trc/src/event/mod.rs b/crates/trc/src/event/mod.rs
index 761a49e3..6be92077 100644
--- a/crates/trc/src/event/mod.rs
+++ b/crates/trc/src/event/mod.rs
@@ -261,6 +261,7 @@ impl EventType {
EventType::Auth(cause) => cause.message(),
EventType::Config(_) => "Configuration error",
EventType::Resource(cause) => cause.message(),
+ EventType::Security(_) => "Insufficient permissions",
_ => "Internal server error",
}
}
diff --git a/crates/trc/src/ipc/bitset.rs b/crates/trc/src/ipc/bitset.rs
index e7ccbc22..e4d6b0f1 100644
--- a/crates/trc/src/ipc/bitset.rs
+++ b/crates/trc/src/ipc/bitset.rs
@@ -51,6 +51,10 @@ impl<const N: usize> Bitset<N> {
}
true
}
+
+ pub fn inner(&self) -> &[usize; N] {
+ &self.0
+ }
}
impl<const N: usize> Default for Bitset<N> {
diff --git a/crates/trc/src/lib.rs b/crates/trc/src/lib.rs
index c3e914ae..6b40b68b 100644
--- a/crates/trc/src/lib.rs
+++ b/crates/trc/src/lib.rs
@@ -202,6 +202,7 @@ pub enum SecurityEvent {
BruteForceBan,
LoiterBan,
IpBlocked,
+ Unauthorized,
}
#[event_type]
diff --git a/crates/trc/src/serializers/binary.rs b/crates/trc/src/serializers/binary.rs
index 68f346b1..07102ffa 100644
--- a/crates/trc/src/serializers/binary.rs
+++ b/crates/trc/src/serializers/binary.rs
@@ -858,6 +858,7 @@ impl EventType {
EventType::Security(SecurityEvent::BruteForceBan) => 549,
EventType::Security(SecurityEvent::LoiterBan) => 550,
EventType::Smtp(SmtpEvent::MailFromNotAllowed) => 551,
+ EventType::Security(SecurityEvent::Unauthorized) => 552,
}
}
@@ -1455,6 +1456,7 @@ impl EventType {
549 => Some(EventType::Security(SecurityEvent::BruteForceBan)),
550 => Some(EventType::Security(SecurityEvent::LoiterBan)),
551 => Some(EventType::Smtp(SmtpEvent::MailFromNotAllowed)),
+ 552 => Some(EventType::Security(SecurityEvent::Unauthorized)),
_ => None,
}
}
diff --git a/crates/utils/proc-macros/Cargo.toml b/crates/utils/proc-macros/Cargo.toml
new file mode 100644
index 00000000..7f1060ea
--- /dev/null
+++ b/crates/utils/proc-macros/Cargo.toml
@@ -0,0 +1,12 @@
+[package]
+name = "proc_macros"
+version = "0.1.0"
+edition = "2021"
+
+[lib]
+proc-macro = true
+
+[dependencies]
+syn = { version = "2.0", features = ["full"] }
+quote = "1.0"
+proc-macro2 = "1.0"
diff --git a/crates/utils/proc-macros/src/lib.rs b/crates/utils/proc-macros/src/lib.rs
new file mode 100644
index 00000000..77fbf969
--- /dev/null
+++ b/crates/utils/proc-macros/src/lib.rs
@@ -0,0 +1,77 @@
+/*
+ * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art>
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
+ */
+
+use proc_macro::TokenStream;
+use quote::quote;
+use syn::{parse_macro_input, Data, DeriveInput};
+
+#[proc_macro_derive(EnumMethods)]
+pub fn enum_id(input: TokenStream) -> TokenStream {
+ let input = parse_macro_input!(input as DeriveInput);
+ let name = &input.ident;
+
+ let variants = match input.data {
+ Data::Enum(ref data) => &data.variants,
+ _ => panic!("EnumMethods only works on enums"),
+ };
+
+ let variant_count = variants.len();
+ let variant_names: Vec<_> = variants.iter().map(|v| &v.ident).collect();
+ let variant_ids: Vec<usize> = (0..variant_count).collect();
+ let snake_case_names: Vec<String> = variant_names
+ .iter()
+ .map(|name| to_snake_case(&name.to_string()))
+ .collect();
+
+ let expanded = quote! {
+ impl #name {
+ pub const COUNT: usize = #variant_count;
+
+ pub const fn id(&self) -> usize {
+ match self {
+ #(#name::#variant_names => #variant_ids,)*
+ }
+ }
+
+ pub fn from_id(id: usize) -> Option<Self> {
+ match id {
+ #(#variant_ids => Some(#name::#variant_names),)*
+ _ => None,
+ }
+ }
+
+ pub fn name(&self) -> &'static str {
+ match self {
+ #(#name::#variant_names => #snake_case_names,)*
+ }
+ }
+
+ pub fn from_name(name: &str) -> Option<Self> {
+ match name {
+ #(#snake_case_names => Some(#name::#variant_names),)*
+ _ => None,
+ }
+ }
+ }
+ };
+
+ TokenStream::from(expanded)
+}
+
+fn to_snake_case(s: &str) -> String {
+ let mut result = String::new();
+ for (i, ch) in s.char_indices() {
+ if ch.is_uppercase() {
+ if i > 0 {
+ result.push('-');
+ }
+ result.push(ch.to_ascii_lowercase());
+ } else {
+ result.push(ch);
+ }
+ }
+ result
+}
diff --git a/crates/utils/src/map/vec_map.rs b/crates/utils/src/map/vec_map.rs
index d39087c9..32d8803b 100644
--- a/crates/utils/src/map/vec_map.rs
+++ b/crates/utils/src/map/vec_map.rs
@@ -4,7 +4,7 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
-use std::{borrow::Borrow, fmt};
+use std::{borrow::Borrow, cmp::Ordering, fmt, hash::Hash};
use serde::{de::DeserializeOwned, ser::SerializeMap, Deserialize, Serialize};
@@ -14,50 +14,51 @@ use serde::{de::DeserializeOwned, ser::SerializeMap, Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VecMap<K: Eq + PartialEq, V> {
- pub k: Vec<K>,
- pub v: Vec<V>,
+ inner: Vec<KeyValue<K, V>>,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub struct KeyValue<K: Eq + PartialEq, V> {
+ key: K,
+ value: V,
}
impl<K: Eq + PartialEq, V> Default for VecMap<K, V> {
fn default() -> Self {
- VecMap {
- k: Vec::new(),
- v: Vec::new(),
- }
+ VecMap { inner: Vec::new() }
}
}
impl<K: Eq + PartialEq, V> VecMap<K, V> {
pub fn new() -> Self {
- Self {
- k: Vec::new(),
- v: Vec::new(),
- }
+ Self::default()
}
pub fn with_capacity(capacity: usize) -> Self {
Self {
- k: Vec::with_capacity(capacity),
- v: Vec::with_capacity(capacity),
+ inner: Vec::with_capacity(capacity),
}
}
#[inline(always)]
pub fn set(&mut self, key: K, value: V) -> bool {
- if let Some(pos) = self.k.iter().position(|k| *k == key) {
- self.v[pos] = value;
+ if let Some(kv) = self.inner.iter_mut().find(|kv| kv.key == key) {
+ kv.value = value;
false
} else {
- self.k.push(key);
- self.v.push(value);
+ self.inner.push(KeyValue { key, value });
true
}
}
#[inline(always)]
pub fn append(&mut self, key: K, value: V) {
- self.k.push(key);
- self.v.push(value);
+ self.inner.push(KeyValue { key, value });
+ }
+
+ #[inline(always)]
+ pub fn insert(&mut self, idx: usize, key: K, value: V) {
+ self.inner.insert(idx, KeyValue { key, value });
}
#[inline(always)]
@@ -65,104 +66,158 @@ impl<K: Eq + PartialEq, V> VecMap<K, V> {
where
K: Borrow<Q> + PartialEq<Q>,
{
- self.k.iter().position(|k| k == key).map(|pos| &self.v[pos])
+ self.inner.iter().find_map(|kv| {
+ if &kv.key == key {
+ Some(&kv.value)
+ } else {
+ None
+ }
+ })
}
#[inline(always)]
pub fn get_mut(&mut self, key: &K) -> Option<&mut V> {
- self.k
- .iter_mut()
- .position(|k| k == key)
- .map(|pos| &mut self.v[pos])
+ self.inner.iter_mut().find_map(|kv| {
+ if &kv.key == key {
+ Some(&mut kv.value)
+ } else {
+ None
+ }
+ })
}
#[inline(always)]
pub fn contains_key(&self, key: &K) -> bool {
- self.k.contains(key)
+ self.inner.iter().any(|kv| kv.key == *key)
}
#[inline(always)]
pub fn remove(&mut self, key: &K) -> Option<V> {
- self.k.iter().position(|k| k == key).map(|pos| {
- self.k.swap_remove(pos);
- self.v.swap_remove(pos)
- })
+ self.inner
+ .iter()
+ .position(|kv| kv.key == *key)
+ .map(|pos| self.inner.remove(pos).value)
+ }
+
+ #[inline(always)]
+ pub fn remove_all(&mut self, key: &K) {
+ self.inner.retain(|kv| kv.key != *key);
}
#[inline(always)]
pub fn remove_entry(&mut self, key: &K) -> Option<(K, V)> {
- self.k
- .iter()
- .position(|k| k == key)
- .map(|pos| (self.k.swap_remove(pos), self.v.swap_remove(pos)))
+ self.inner.iter().position(|k| &k.key == key).map(|pos| {
+ let kv = self.inner.remove(pos);
+ (kv.key, kv.value)
+ })
}
#[inline(always)]
pub fn swap_remove(&mut self, index: usize) -> V {
- self.k.swap_remove(index);
- self.v.swap_remove(index)
+ self.inner.swap_remove(index).value
}
#[inline(always)]
pub fn is_empty(&self) -> bool {
- self.k.is_empty()
+ self.inner.is_empty()
}
#[inline(always)]
pub fn len(&self) -> usize {
- self.k.len()
+ self.inner.len()
}
#[inline(always)]
pub fn clear(&mut self) {
- self.k.clear();
- self.v.clear();
+ self.inner.clear();
}
#[inline(always)]
pub fn iter(&self) -> impl Iterator<Item = (&K, &V)> {
- self.k.iter().zip(self.v.iter())
+ self.inner.iter().map(|kv| (&kv.key, &kv.value))
+ }
+
+ #[inline(always)]
+ pub fn iter_by_key<'x, 'y: 'x>(&'x self, key: &'y K) -> impl Iterator<Item = &'x V> + 'x {
+ self.inner.iter().filter_map(move |kv| {
+ if &kv.key == key {
+ Some(&kv.value)
+ } else {
+ None
+ }
+ })
}
#[inline(always)]
pub fn iter_mut(&mut self) -> impl Iterator<Item = (&mut K, &mut V)> {
- self.k.iter_mut().zip(self.v.iter_mut())
+ self.inner.iter_mut().map(|kv| (&mut kv.key, &mut kv.value))
+ }
+
+ #[inline(always)]
+ pub fn iter_mut_by_key<'x, 'y: 'x>(
+ &'x mut self,
+ key: &'y K,
+ ) -> impl Iterator<Item = &'x mut V> + 'x {
+ self.inner.iter_mut().filter_map(move |kv| {
+ if &kv.key == key {
+ Some(&mut kv.value)
+ } else {
+ None
+ }
+ })
}
#[inline(always)]
pub fn keys(&self) -> impl Iterator<Item = &K> {
- self.k.iter()
+ self.inner.iter().map(|kv| &kv.key)
}
#[inline(always)]
pub fn values(&self) -> impl Iterator<Item = &V> {
- self.v.iter()
+ self.inner.iter().map(|kv| &kv.value)
}
#[inline(always)]
pub fn values_mut(&mut self) -> impl Iterator<Item = &mut V> {
- self.v.iter_mut()
+ self.inner.iter_mut().map(|kv| &mut kv.value)
}
pub fn get_mut_or_insert_with(&mut self, key: K, fnc: impl FnOnce() -> V) -> &mut V {
- if let Some(pos) = self.k.iter().position(|k| k == &key) {
- &mut self.v[pos]
+ if let Some(pos) = self.inner.iter().position(|kv| kv.key == key) {
+ &mut self.inner[pos].value
} else {
- self.k.push(key);
- self.v.push(fnc());
- self.v.last_mut().unwrap()
+ self.inner.push(KeyValue { key, value: fnc() });
+ &mut self.inner.last_mut().unwrap().value
}
}
+
+ pub fn with_key_value(mut self, key: K, value: V) -> Self {
+ self.append(key, value);
+ self
+ }
+
+ pub fn sort_unstable(&mut self)
+ where
+ K: Ord,
+ V: Ord,
+ {
+ self.inner.sort_unstable_by(|a, b| match a.key.cmp(&b.key) {
+ Ordering::Equal => a.value.cmp(&b.value),
+ cmp => cmp,
+ });
+ }
}
impl<K: Eq + PartialEq, V: Default> VecMap<K, V> {
pub fn get_mut_or_insert(&mut self, key: K) -> &mut V {
- if let Some(pos) = self.k.iter().position(|k| k == &key) {
- &mut self.v[pos]
+ if let Some(pos) = self.inner.iter().position(|kv| kv.key == key) {
+ &mut self.inner[pos].value
} else {
- self.k.push(key);
- self.v.push(V::default());
- self.v.last_mut().unwrap()
+ self.inner.push(KeyValue {
+ key,
+ value: V::default(),
+ });
+ &mut self.inner.last_mut().unwrap().value
}
}
}
@@ -170,20 +225,34 @@ impl<K: Eq + PartialEq, V: Default> VecMap<K, V> {
impl<K: Eq + PartialEq, V> IntoIterator for VecMap<K, V> {
type Item = (K, V);
- type IntoIter = std::iter::Zip<std::vec::IntoIter<K>, std::vec::IntoIter<V>>;
+ type IntoIter =
+ std::iter::Map<std::vec::IntoIter<KeyValue<K, V>>, fn(KeyValue<K, V>) -> (K, V)>;
fn into_iter(self) -> Self::IntoIter {
- self.k.into_iter().zip(self.v)
+ self.inner.into_iter().map(|kv| (kv.key, kv.value))
}
}
impl<'x, K: Eq + PartialEq, V> IntoIterator for &'x VecMap<K, V> {
type Item = (&'x K, &'x V);
- type IntoIter = std::iter::Zip<std::slice::Iter<'x, K>, std::slice::Iter<'x, V>>;
+ type IntoIter = std::iter::Map<
+ std::slice::Iter<'x, KeyValue<K, V>>,
+ fn(&'x KeyValue<K, V>) -> (&'x K, &'x V),
+ >;
fn into_iter(self) -> Self::IntoIter {
- self.k.iter().zip(self.v.iter())
+ self.inner.iter().map(|kv| (&kv.key, &kv.value))
+ }
+}
+
+impl<K, V> Hash for VecMap<K, V>
+where
+ K: Eq + PartialEq + Hash,
+ V: Hash,
+{
+ fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+ self.inner.hash(state);
}
}
diff --git a/tests/src/directory/internal.rs b/tests/src/directory/internal.rs
index 81b2f215..dc7acf3a 100644
--- a/tests/src/directory/internal.rs
+++ b/tests/src/directory/internal.rs
@@ -21,7 +21,7 @@ use store::{
BitmapKey, ValueKey,
};
-use crate::directory::DirectoryTest;
+use crate::directory::{DirectoryTest, IntoTestPrincipal, TestPrincipal};
#[tokio::test]
async fn internal_directory() {
@@ -33,20 +33,20 @@ async fn internal_directory() {
// A principal without name should fail
assert_eq!(
- store.create_account(Principal::default(), vec![]).await,
+ store.create_account(Principal::default()).await,
Err(manage::err_missing(PrincipalField::Name))
);
// Basic account creation
let john_id = store
.create_account(
- Principal {
+ TestPrincipal {
name: "john".to_string(),
description: Some("John Doe".to_string()),
secrets: vec!["secret".to_string(), "secret2".to_string()],
..Default::default()
- },
- vec![],
+ }
+ .into(),
)
.await
.unwrap();
@@ -55,11 +55,11 @@ async fn internal_directory() {
assert_eq!(
store
.create_account(
- Principal {
+ TestPrincipal {
name: "john".to_string(),
..Default::default()
- },
- vec![]
+ }
+ .into(),
)
.await,
Err(manage::err_exists(PrincipalField::Name, "john".to_string()))
@@ -69,12 +69,12 @@ async fn internal_directory() {
assert_eq!(
store
.create_account(
- Principal {
+ TestPrincipal {
name: "jane".to_string(),
emails: vec!["jane@example.org".to_string()],
..Default::default()
- },
- vec![]
+ }
+ .into(),
)
.await,
Err(manage::not_found("example.org".to_string()))
@@ -121,15 +121,15 @@ async fn internal_directory() {
// Create an account with an email address
let jane_id = store
.create_account(
- Principal {
+ TestPrincipal {
name: "jane".to_string(),
description: Some("Jane Doe".to_string()),
secrets: vec!["my_secret".to_string(), "my_secret2".to_string()],
emails: vec!["jane@example.org".to_string()],
quota: 123,
..Default::default()
- },
- vec![],
+ }
+ .into(),
)
.await
.unwrap();
@@ -151,8 +151,9 @@ async fn internal_directory() {
true
)
.await
- .unwrap(),
- Some(Principal {
+ .unwrap()
+ .map(|p| p.into_test()),
+ Some(TestPrincipal {
id: jane_id,
name: "jane".to_string(),
description: Some("Jane Doe".to_string()),
@@ -180,13 +181,13 @@ async fn internal_directory() {
assert_eq!(
store
.create_account(
- Principal {
+ TestPrincipal {
name: "janeth".to_string(),
description: Some("Janeth Doe".to_string()),
emails: vec!["jane@example.org".to_string()],
..Default::default()
- },
- vec![]
+ }
+ .into()
)
.await,
Err(manage::err_exists(
@@ -198,13 +199,13 @@ async fn internal_directory() {
// Create a mailing list
let list_id = store
.create_account(
- Principal {
+ TestPrincipal {
name: "list".to_string(),
typ: Type::List,
emails: vec!["list@example.org".to_string()],
..Default::default()
- },
- vec![],
+ }
+ .into(),
)
.await
.unwrap();
@@ -235,8 +236,9 @@ async fn internal_directory() {
.query(QueryBy::Name("list"), true)
.await
.unwrap()
- .unwrap(),
- Principal {
+ .unwrap()
+ .into_test(),
+ TestPrincipal {
name: "list".to_string(),
id: list_id,
typ: Type::List,
@@ -260,25 +262,25 @@ async fn internal_directory() {
// Create groups
store
.create_account(
- Principal {
+ TestPrincipal {
name: "sales".to_string(),
description: Some("Sales Team".to_string()),
typ: Type::Group,
..Default::default()
- },
- vec![],
+ }
+ .into(),
)
.await
.unwrap();
store
.create_account(
- Principal {
+ TestPrincipal {
name: "support".to_string(),
description: Some("Support Team".to_string()),
typ: Type::Group,
..Default::default()
- },
- vec![],
+ }
+ .into(),
)
.await
.unwrap();
@@ -313,8 +315,9 @@ async fn internal_directory() {
)
.await
.unwrap()
+ .into_test()
.into_sorted(),
- Principal {
+ TestPrincipal {
id: john_id,
name: "john".to_string(),
description: Some("John Doe".to_string()),
@@ -367,8 +370,9 @@ async fn internal_directory() {
)
.await
.unwrap()
+ .into_test()
.into_sorted(),
- Principal {
+ TestPrincipal {
id: john_id,
name: "john".to_string(),
description: Some("John Doe".to_string()),
@@ -398,10 +402,6 @@ async fn internal_directory() {
PrincipalValue::StringList(vec!["12345".to_string()])
),
PrincipalUpdate::set(PrincipalField::Quota, PrincipalValue::Integer(1024)),
- PrincipalUpdate::set(
- PrincipalField::Type,
- PrincipalValue::String("superuser".to_string())
- ),
PrincipalUpdate::remove_item(
PrincipalField::Emails,
PrincipalValue::String("john@example.org".to_string()),
@@ -426,15 +426,16 @@ async fn internal_directory() {
)
.await
.unwrap()
+ .into_test()
.into_sorted(),
- Principal {
+ TestPrincipal {
id: john_id,
name: "john.doe".to_string(),
description: Some("Johnny Doe".to_string()),
secrets: vec!["12345".to_string()],
emails: vec!["john.doe@example.org".to_string()],
quota: 1024,
- typ: Type::Superuser,
+ typ: Type::Individual,
member_of: vec!["list".to_string(), "sales".to_string()],
}
);
diff --git a/tests/src/directory/ldap.rs b/tests/src/directory/ldap.rs
index 2435cf88..41245794 100644
--- a/tests/src/directory/ldap.rs
+++ b/tests/src/directory/ldap.rs
@@ -6,10 +6,10 @@
use std::fmt::Debug;
-use directory::{backend::internal::manage::ManageDirectory, Principal, QueryBy, Type};
+use directory::{backend::internal::manage::ManageDirectory, QueryBy, Type};
use mail_send::Credentials;
-use crate::directory::{map_account_ids, DirectoryTest};
+use crate::directory::{map_account_ids, DirectoryTest, IntoTestPrincipal, TestPrincipal};
#[tokio::test]
async fn ldap_directory() {
@@ -40,14 +40,19 @@ async fn ldap_directory() {
.await
.unwrap()
.unwrap()
+ .into_test()
.into_sorted(),
- Principal {
+ TestPrincipal {
id: base_store.get_account_id("john").await.unwrap().unwrap(),
name: "john".to_string(),
description: "John Doe".to_string().into(),
secrets: vec!["12345".to_string()],
typ: Type::Individual,
- member_of: map_account_ids(base_store, vec!["sales"]).await,
+ member_of: map_account_ids(base_store, vec!["sales"])
+ .await
+ .into_iter()
+ .map(|v| v.to_string())
+ .collect(),
emails: vec![
"john@example.org".to_string(),
"john.doe@example.org".to_string()
@@ -68,8 +73,9 @@ async fn ldap_directory() {
.await
.unwrap()
.unwrap()
+ .into_test()
.into_sorted(),
- Principal {
+ TestPrincipal {
id: base_store.get_account_id("bill").await.unwrap().unwrap(),
name: "bill".to_string(),
description: "Bill Foobar".to_string().into(),
@@ -102,14 +108,19 @@ async fn ldap_directory() {
.await
.unwrap()
.unwrap()
+ .into_test()
.into_sorted(),
- Principal {
+ TestPrincipal {
id: base_store.get_account_id("jane").await.unwrap().unwrap(),
name: "jane".to_string(),
description: "Jane Doe".to_string().into(),
typ: Type::Individual,
secrets: vec!["abcde".to_string()],
- member_of: map_account_ids(base_store, vec!["sales", "support"]).await,
+ member_of: map_account_ids(base_store, vec!["sales", "support"])
+ .await
+ .into_iter()
+ .map(|v| v.to_string())
+ .collect(),
emails: vec!["jane@example.org".to_string(),],
..Default::default()
}
@@ -122,8 +133,9 @@ async fn ldap_directory() {
.query(QueryBy::Name("sales"), true)
.await
.unwrap()
- .unwrap(),
- Principal {
+ .unwrap()
+ .into_test(),
+ TestPrincipal {
id: base_store.get_account_id("sales").await.unwrap().unwrap(),
name: "sales".to_string(),
description: "sales".to_string().into(),
diff --git a/tests/src/directory/mod.rs b/tests/src/directory/mod.rs
index d97f8730..732e2e90 100644
--- a/tests/src/directory/mod.rs
+++ b/tests/src/directory/mod.rs
@@ -11,7 +11,10 @@ pub mod smtp;
pub mod sql;
use common::{config::smtp::session::AddressMapping, Core};
-use directory::{backend::internal::manage::ManageDirectory, Directories};
+use directory::{
+ backend::internal::{manage::ManageDirectory, PrincipalField},
+ Directories, Principal, Type,
+};
use mail_send::Credentials;
use rustls::ServerConfig;
use rustls_pemfile::{certs, pkcs8_private_keys};
@@ -254,6 +257,18 @@ pub struct DirectoryTest {
pub core: Core,
}
+#[derive(Debug, Default, Clone, PartialEq, Eq)]
+pub struct TestPrincipal {
+ pub id: u32,
+ pub typ: Type,
+ pub quota: u64,
+ pub name: String,
+ pub secrets: Vec<String>,
+ pub emails: Vec<String>,
+ pub member_of: Vec<String>,
+ pub description: Option<String>,
+}
+
impl DirectoryTest {
pub async fn new(id_store: Option<&str>) -> DirectoryTest {
let temp_dir = TempDir::new("directory_tests", true);
@@ -408,6 +423,61 @@ pub fn dummy_tls_acceptor() -> Arc<TlsAcceptor> {
)))
}
+trait IntoTestPrincipal {
+ fn into_test(self) -> TestPrincipal;
+}
+
+impl IntoTestPrincipal for Principal {
+ fn into_test(self) -> TestPrincipal {
+ TestPrincipal::from(self)
+ }
+}
+
+impl TestPrincipal {
+ pub fn into_sorted(mut self) -> Self {
+ self.member_of.sort_unstable();
+ self.emails.sort_unstable();
+ self
+ }
+}
+
+impl From<Principal> for TestPrincipal {
+ fn from(mut value: Principal) -> Self {
+ Self {
+ id: value.id(),
+ typ: value.typ(),
+ quota: value.quota(),
+ name: value.take_str(PrincipalField::Name).unwrap_or_default(),
+ secrets: value
+ .take_str_array(PrincipalField::Secrets)
+ .unwrap_or_default(),
+ emails: value
+ .take_str_array(PrincipalField::Emails)
+ .unwrap_or_default(),
+ member_of: value
+ .take_str_array(PrincipalField::MemberOf)
+ .unwrap_or_default(),
+ /*member_of: value
+ .iter_int(PrincipalField::MemberOf)
+ .map(|v| v as u32)
+ .collect(),*/
+ description: value.take_str(PrincipalField::Description),
+ }
+ }
+}
+
+impl From<TestPrincipal> for Principal {
+ fn from(value: TestPrincipal) -> Self {
+ Principal::new(value.id, value.typ)
+ .with_field(PrincipalField::Name, value.name)
+ .with_field(PrincipalField::Quota, value.quota)
+ .with_field(PrincipalField::Secrets, value.secrets)
+ .with_field(PrincipalField::Emails, value.emails)
+ .with_field(PrincipalField::MemberOf, value.member_of)
+ .with_opt_field(PrincipalField::Description, value.description)
+ }
+}
+
#[derive(Clone, PartialEq, Eq, Hash)]
pub enum Item {
IsAccount(String),
@@ -500,92 +570,6 @@ impl core::fmt::Debug for Item {
}
}
-/*
-
-// DEPRECATED - TODO: Remove
-#[tokio::test(flavor = "multi_thread")]
-#[ignore]
-async fn lookup_local() {
- const LOOKUP_CONFIG: &str = r#"
- [store."local/regex"]
- type = "memory"
- format = "regex"
- values = ["^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$",
- "^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$"]
-
- [store."local/glob"]
- type = "memory"
- format = "glob"
- values = ["*@example.org", "test@*", "localhost", "*+*@*.domain.net"]
-
- [store."local/list"]
- type = "memory"
- format = "list"
- values = ["abc", "xyz", "123"]
-
- [store."local/suffix"]
- type = "memory"
- format = "glob"
- comment = "//"
- values = ["https://publicsuffix.org/list/public_suffix_list.dat", "fallback+file://%PATH%/public_suffix_list.dat.gz"]
- "#;
-
- /*tracing::subscriber::set_global_default(
- tracing_subscriber::FmtSubscriber::builder()
- .with_max_level(tracing::Level::TRACE)
- .finish(),
- )
- .unwrap();*/
-
- let mut config = utils::config::Config::new(
- &LOOKUP_CONFIG.replace(
- "%PATH%",
- PathBuf::from(env!("CARGO_MANIFEST_DIR"))
- .parent()
- .unwrap()
- .to_path_buf()
- .join("resources")
- .join("config")
- .join("lists")
- .to_str()
- .unwrap(),
- ),
- )
- .unwrap();
-
- let lookups = Stores::parse_all(&mut config).await.lookup_stores;
-
- for (lookup, item, expect) in [
- ("glob", "user@example.org", true),
- ("glob", "test@otherdomain.org", true),
- ("glob", "localhost", true),
- ("glob", "john+doe@doefamily.domain.net", true),
- ("glob", "john@domain.net", false),
- ("glob", "example.org", false),
- ("list", "abc", true),
- ("list", "xyz", true),
- ("list", "zzz", false),
- ("regex", "user@domain.com", true),
- ("regex", "127.0.0.1", true),
- ("regex", "hello", false),
- ("suffix", "co.uk", true),
- ("suffix", "coco", false),
- ] {
- assert_eq!(
- lookups
- .get(&format!("local/{lookup}"))
- .unwrap()
- .key_get::<String>(item.as_bytes().to_vec())
- .await
- .unwrap()
- .is_some(),
- expect,
- "failed for {lookup}, item {item}"
- );
- }
-}
-*/
-
#[tokio::test]
async fn address_mappings() {
const MAPPINGS: &str = r#"
diff --git a/tests/src/directory/sql.rs b/tests/src/directory/sql.rs
index 95fa8eca..5fd22d71 100644
--- a/tests/src/directory/sql.rs
+++ b/tests/src/directory/sql.rs
@@ -4,11 +4,11 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
-use directory::{backend::internal::manage::ManageDirectory, Principal, QueryBy, Type};
+use directory::{backend::internal::manage::ManageDirectory, QueryBy, Type};
use mail_send::Credentials;
use store::{LookupStore, Store};
-use crate::directory::{map_account_ids, DirectoryTest};
+use crate::directory::{map_account_ids, DirectoryTest, IntoTestPrincipal, TestPrincipal};
use super::DirectoryStore;
@@ -110,14 +110,19 @@ async fn sql_directory() {
)
.await
.unwrap()
- .unwrap(),
- Principal {
+ .unwrap()
+ .into_test(),
+ TestPrincipal {
id: base_store.get_account_id("john").await.unwrap().unwrap(),
name: "john".to_string(),
description: "John Doe".to_string().into(),
secrets: vec!["12345".to_string()],
typ: Type::Individual,
- member_of: map_account_ids(base_store, vec!["sales"]).await,
+ member_of: map_account_ids(base_store, vec!["sales"])
+ .await
+ .into_iter()
+ .map(|v| v.to_string())
+ .collect(),
emails: vec![
"john@example.org".to_string(),
"jdoe@example.org".to_string(),
@@ -137,8 +142,9 @@ async fn sql_directory() {
)
.await
.unwrap()
- .unwrap(),
- Principal {
+ .unwrap()
+ .into_test(),
+ TestPrincipal {
id: base_store.get_account_id("bill").await.unwrap().unwrap(),
name: "bill".to_string(),
description: "Bill Foobar".to_string().into(),
@@ -169,14 +175,19 @@ async fn sql_directory() {
.query(QueryBy::Name("jane"), true)
.await
.unwrap()
- .unwrap(),
- Principal {
+ .unwrap()
+ .into_test(),
+ TestPrincipal {
id: base_store.get_account_id("jane").await.unwrap().unwrap(),
name: "jane".to_string(),
description: "Jane Doe".to_string().into(),
typ: Type::Individual,
secrets: vec!["abcde".to_string()],
- member_of: map_account_ids(base_store, vec!["sales", "support"]).await,
+ member_of: map_account_ids(base_store, vec!["sales", "support"])
+ .await
+ .into_iter()
+ .map(|v| v.to_string())
+ .collect(),
emails: vec!["jane@example.org".to_string(),],
..Default::default()
}
@@ -188,8 +199,9 @@ async fn sql_directory() {
.query(QueryBy::Name("sales"), true)
.await
.unwrap()
- .unwrap(),
- Principal {
+ .unwrap()
+ .into_test(),
+ TestPrincipal {
id: base_store.get_account_id("sales").await.unwrap().unwrap(),
name: "sales".to_string(),
description: "Sales Team".to_string().into(),
diff --git a/tests/src/smtp/inbound/basic.rs b/tests/src/smtp/inbound/basic.rs
index 55058a5a..1db73a77 100644
--- a/tests/src/smtp/inbound/basic.rs
+++ b/tests/src/smtp/inbound/basic.rs
@@ -14,8 +14,8 @@ use crate::smtp::{
#[tokio::test]
async fn basic_commands() {
- // Enable logging
- crate::enable_logging();
+ // Enable logging
+ crate::enable_logging();
let mut session = Session::test(build_smtp(Core::default(), Inner::default()));
diff --git a/tests/src/smtp/inbound/dmarc.rs b/tests/src/smtp/inbound/dmarc.rs
index fdd43bdc..bd57d035 100644
--- a/tests/src/smtp/inbound/dmarc.rs
+++ b/tests/src/smtp/inbound/dmarc.rs
@@ -92,8 +92,8 @@ verify = [{if = "sender_domain = 'test.net'", then = 'relaxed'},
#[tokio::test]
async fn dmarc() {
- // Enable logging
- crate::enable_logging();
+ // Enable logging
+ crate::enable_logging();
let mut inner = Inner::default();
let tmp_dir = TempDir::new("smtp_dmarc_test", true);
diff --git a/tests/src/smtp/inbound/ehlo.rs b/tests/src/smtp/inbound/ehlo.rs
index 8c438878..48445ddd 100644
--- a/tests/src/smtp/inbound/ehlo.rs
+++ b/tests/src/smtp/inbound/ehlo.rs
@@ -38,8 +38,8 @@ ehlo = [{if = "remote_ip = '10.0.0.2'", then = 'strict'},
#[tokio::test]
async fn ehlo() {
- // Enable logging
- crate::enable_logging();
+ // Enable logging
+ crate::enable_logging();
let mut config = Config::new(CONFIG).unwrap();
let core = Core::parse(&mut config, Default::default(), Default::default()).await;
diff --git a/tests/src/smtp/inbound/limits.rs b/tests/src/smtp/inbound/limits.rs
index 3946aebc..8f92d0eb 100644
--- a/tests/src/smtp/inbound/limits.rs
+++ b/tests/src/smtp/inbound/limits.rs
@@ -29,8 +29,8 @@ duration = [{if = "remote_ip = '10.0.0.3'", then = '500ms'},
#[tokio::test]
async fn limits() {
- // Enable logging
- crate::enable_logging();
+ // Enable logging
+ crate::enable_logging();
let mut config = Config::new(CONFIG).unwrap();
let core = Core::parse(&mut config, Default::default(), Default::default()).await;
diff --git a/tests/src/smtp/inbound/rewrite.rs b/tests/src/smtp/inbound/rewrite.rs
index ec1c7399..3ea7fe3d 100644
--- a/tests/src/smtp/inbound/rewrite.rs
+++ b/tests/src/smtp/inbound/rewrite.rs
@@ -69,7 +69,6 @@ async fn address_rewrite() {
// Enable logging
crate::enable_logging();
-
// Prepare config
let mut config = Config::new(CONFIG).unwrap();
let core = Core::parse(&mut config, Default::default(), Default::default()).await;
diff --git a/tests/src/smtp/lookup/utils.rs b/tests/src/smtp/lookup/utils.rs
index 9aa3a35a..17bf5821 100644
--- a/tests/src/smtp/lookup/utils.rs
+++ b/tests/src/smtp/lookup/utils.rs
@@ -49,8 +49,8 @@ ip-strategy = "ipv6_then_ipv4"
#[tokio::test]
async fn lookup_ip() {
- // Enable logging
- crate::enable_logging();
+ // Enable logging
+ crate::enable_logging();
let ipv6 = [
"a:b::1".parse().unwrap(),
diff --git a/tests/src/smtp/management/queue.rs b/tests/src/smtp/management/queue.rs
index 28bfb8bb..cfc4d12a 100644
--- a/tests/src/smtp/management/queue.rs
+++ b/tests/src/smtp/management/queue.rs
@@ -69,7 +69,6 @@ async fn manage_queue() {
// Enable logging
crate::enable_logging();
-
// Start remote test server
let mut remote = TestServer::new("smtp_manage_queue_remote", REMOTE, true).await;
let _rx = remote.start(&[ServerProtocol::Smtp]).await;
diff --git a/tests/src/smtp/management/report.rs b/tests/src/smtp/management/report.rs
index 86cfc87d..76052870 100644
--- a/tests/src/smtp/management/report.rs
+++ b/tests/src/smtp/management/report.rs
@@ -57,7 +57,6 @@ async fn manage_reports() {
// Enable logging
crate::enable_logging();
-
// Start reporting service
let local = TestServer::new("smtp_manage_reports", CONFIG, true).await;
let _rx = local.start(&[ServerProtocol::Http]).await;
diff --git a/tests/src/smtp/outbound/dane.rs b/tests/src/smtp/outbound/dane.rs
index e7889825..42e4a717 100644
--- a/tests/src/smtp/outbound/dane.rs
+++ b/tests/src/smtp/outbound/dane.rs
@@ -85,7 +85,6 @@ async fn dane_verify() {
// Enable logging
crate::enable_logging();
-
// Start test server
let mut remote = TestServer::new("smtp_dane_remote", REMOTE, true).await;
let _rx = remote.start(&[ServerProtocol::Smtp]).await;
diff --git a/tests/src/smtp/outbound/extensions.rs b/tests/src/smtp/outbound/extensions.rs
index d660f7f8..57bbb1f2 100644
--- a/tests/src/smtp/outbound/extensions.rs
+++ b/tests/src/smtp/outbound/extensions.rs
@@ -53,7 +53,6 @@ async fn extensions() {
// Enable logging
crate::enable_logging();
-
// Start test server
let mut remote = TestServer::new("smtp_ext_remote", REMOTE, true).await;
let _rx = remote.start(&[ServerProtocol::Smtp]).await;
diff --git a/tests/src/smtp/outbound/fallback_relay.rs b/tests/src/smtp/outbound/fallback_relay.rs
index 0e736078..76173bda 100644
--- a/tests/src/smtp/outbound/fallback_relay.rs
+++ b/tests/src/smtp/outbound/fallback_relay.rs
@@ -54,7 +54,6 @@ async fn fallback_relay() {
// Enable logging
crate::enable_logging();
-
// Start test server
let mut remote = TestServer::new("smtp_fallback_remote", REMOTE, true).await;
let _rx = remote.start(&[ServerProtocol::Smtp]).await;
diff --git a/tests/src/smtp/outbound/ip_lookup.rs b/tests/src/smtp/outbound/ip_lookup.rs
index 7d4bfd2c..23e4625c 100644
--- a/tests/src/smtp/outbound/ip_lookup.rs
+++ b/tests/src/smtp/outbound/ip_lookup.rs
@@ -33,7 +33,6 @@ async fn ip_lookup_strategy() {
// Enable logging
crate::enable_logging();
-
// Start test server
let mut remote = TestServer::new("smtp_iplookup_remote", REMOTE, true).await;
let _rx = remote.start(&[ServerProtocol::Smtp]).await;
diff --git a/tests/src/smtp/outbound/lmtp.rs b/tests/src/smtp/outbound/lmtp.rs
index 98baed03..f27c94fc 100644
--- a/tests/src/smtp/outbound/lmtp.rs
+++ b/tests/src/smtp/outbound/lmtp.rs
@@ -65,7 +65,6 @@ async fn lmtp_delivery() {
// Enable logging
crate::enable_logging();
-
// Start test server
let mut remote = TestServer::new("lmtp_delivery_remote", REMOTE, true).await;
let _rx = remote.start(&[ServerProtocol::Lmtp]).await;
diff --git a/tests/src/smtp/outbound/mta_sts.rs b/tests/src/smtp/outbound/mta_sts.rs
index a6658a54..bae82bd7 100644
--- a/tests/src/smtp/outbound/mta_sts.rs
+++ b/tests/src/smtp/outbound/mta_sts.rs
@@ -62,7 +62,6 @@ async fn mta_sts_verify() {
// Enable logging
crate::enable_logging();
-
// Start test server
let mut remote = TestServer::new("smtp_mta_sts_remote", REMOTE, true).await;
let _rx = remote.start(&[ServerProtocol::Smtp]).await;
diff --git a/tests/src/smtp/outbound/smtp.rs b/tests/src/smtp/outbound/smtp.rs
index 58e4fcd1..f24ffb1f 100644
--- a/tests/src/smtp/outbound/smtp.rs
+++ b/tests/src/smtp/outbound/smtp.rs
@@ -73,7 +73,6 @@ async fn smtp_delivery() {
// Enable logging
crate::enable_logging();
-
// Start test server
let mut remote = TestServer::new("smtp_delivery_remote", REMOTE, true).await;
let _rx = remote.start(&[ServerProtocol::Smtp]).await;
diff --git a/tests/src/smtp/outbound/throttle.rs b/tests/src/smtp/outbound/throttle.rs
index 436673b5..2ea8223a 100644
--- a/tests/src/smtp/outbound/throttle.rs
+++ b/tests/src/smtp/outbound/throttle.rs
@@ -69,7 +69,6 @@ async fn throttle_outbound() {
// Enable logging
crate::enable_logging();
-
// Build test message
let mut test_message = new_message(0);
test_message.return_path_domain = "foobar.org".to_string();
diff --git a/tests/src/smtp/outbound/tls.rs b/tests/src/smtp/outbound/tls.rs
index ae40d850..32c712ce 100644
--- a/tests/src/smtp/outbound/tls.rs
+++ b/tests/src/smtp/outbound/tls.rs
@@ -46,7 +46,6 @@ async fn starttls_optional() {
// Enable logging
crate::enable_logging();
-
// Start test server
let mut remote = TestServer::new("smtp_starttls_remote", REMOTE, true).await;
let _rx = remote.start(&[ServerProtocol::Smtp]).await;
diff --git a/tests/src/smtp/queue/dsn.rs b/tests/src/smtp/queue/dsn.rs
index 22e6d485..9397edee 100644
--- a/tests/src/smtp/queue/dsn.rs
+++ b/tests/src/smtp/queue/dsn.rs
@@ -34,8 +34,8 @@ sign = "['rsa']"
#[tokio::test]
async fn generate_dsn() {
- // Enable logging
- crate::enable_logging();
+ // Enable logging
+ crate::enable_logging();
let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
path.push("resources");
diff --git a/tests/src/smtp/queue/manager.rs b/tests/src/smtp/queue/manager.rs
index 4c142970..58b749c2 100644
--- a/tests/src/smtp/queue/manager.rs
+++ b/tests/src/smtp/queue/manager.rs
@@ -23,8 +23,8 @@ relay = true
#[tokio::test]
async fn queue_due() {
- // Enable logging
- crate::enable_logging();
+ // Enable logging
+ crate::enable_logging();
let local = TestServer::new("smtp_queue_due_test", CONFIG, true).await;
let core = local.build_smtp();
diff --git a/tests/src/smtp/queue/retry.rs b/tests/src/smtp/queue/retry.rs
index ab495013..aaaaea60 100644
--- a/tests/src/smtp/queue/retry.rs
+++ b/tests/src/smtp/queue/retry.rs
@@ -38,7 +38,6 @@ async fn queue_retry() {
// Enable logging
crate::enable_logging();
-
// Create temp dir for queue
let mut local = TestServer::new("smtp_queue_retry_test", CONFIG, true).await;
diff --git a/tests/src/smtp/reporting/analyze.rs b/tests/src/smtp/reporting/analyze.rs
index bfba3396..9a28ae1d 100644
--- a/tests/src/smtp/reporting/analyze.rs
+++ b/tests/src/smtp/reporting/analyze.rs
@@ -28,8 +28,8 @@ store = "1s"
#[tokio::test(flavor = "multi_thread")]
async fn report_analyze() {
- // Enable logging
- crate::enable_logging();
+ // Enable logging
+ crate::enable_logging();
// Create temp dir for queue
let mut local = TestServer::new("smtp_analyze_report_test", CONFIG, true).await;