diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 94d74d96..a02c3930 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -2,8 +2,6 @@ name: I think I found a bug description: File a bug report issue title: "[bug]: " labels: ["bug"] -assignees: - - mdecimus body: - type: markdown attributes: diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 6a033d93..63ea268b 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -2,8 +2,6 @@ name: I have a feature request description: File a feature request issue title: "[enhancement]: " labels: ["enhancement"] -assignees: - - mdecimus body: - type: markdown attributes: diff --git a/Cargo.lock b/Cargo.lock index 899cd522..437c9907 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1438,6 +1438,7 @@ dependencies = [ "rustls 0.22.1", "rustls-pki-types", "scrypt", + "serde", "sha1", "sha2 0.10.8", "smtp-proto", diff --git a/crates/directory/Cargo.toml b/crates/directory/Cargo.toml index c3a86da5..a03d9965 100644 --- a/crates/directory/Cargo.toml +++ b/crates/directory/Cargo.toml @@ -33,6 +33,7 @@ sha2 = "0.10.6" md5 = "0.7.0" futures = "0.3" regex = "1.7.0" +serde = { version = "1.0", features = ["derive"]} [dev-dependencies] tokio = { version = "1.23", features = ["full"] } diff --git a/crates/directory/src/backend/imap/lookup.rs b/crates/directory/src/backend/imap/lookup.rs index 2208884e..53a642f5 100644 --- a/crates/directory/src/backend/imap/lookup.rs +++ b/crates/directory/src/backend/imap/lookup.rs @@ -23,16 +23,15 @@ use mail_send::Credentials; use smtp_proto::{AUTH_CRAM_MD5, AUTH_LOGIN, AUTH_OAUTHBEARER, AUTH_PLAIN, AUTH_XOAUTH2}; -use store::Store; -use crate::{Directory, DirectoryError, Principal, QueryBy, QueryType}; +use crate::{Directory, DirectoryError, Principal, QueryBy}; use super::{ImapDirectory, ImapError}; #[async_trait::async_trait] impl Directory for ImapDirectory { - async fn query(&self, query: QueryBy<'_>) -> crate::Result> { - if let QueryType::Credentials(credentials) = query.t { + async fn query(&self, query: QueryBy<'_>) -> crate::Result>> { + if let QueryBy::Credentials(credentials) = query { let mut client = self.pool.get().await?; let mechanism = match credentials { Credentials::Plain { .. } @@ -78,7 +77,7 @@ impl Directory for ImapDirectory { } } - async fn email_to_ids(&self, _address: &str, _store: &Store) -> crate::Result> { + async fn email_to_ids(&self, _address: &str) -> crate::Result> { Err(DirectoryError::unsupported("imap", "email_to_ids")) } diff --git a/crates/directory/src/backend/internal/lookup.rs b/crates/directory/src/backend/internal/lookup.rs new file mode 100644 index 00000000..f353cba7 --- /dev/null +++ b/crates/directory/src/backend/internal/lookup.rs @@ -0,0 +1,147 @@ +/* + * Copyright (c) 2023 Stalwart Labs Ltd. + * + * This file is part of Stalwart Mail Server. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use mail_send::Credentials; +use store::{ + write::{DirectoryValue, ValueClass}, + IterateParams, Store, ValueKey, +}; + +use crate::{Directory, Principal, QueryBy}; + +use super::manage::ManageDirectory; + +#[async_trait::async_trait] +impl Directory for Store { + async fn query(&self, by: QueryBy<'_>) -> crate::Result>> { + let (username, secret) = match by { + QueryBy::Name(name) => (name, None), + QueryBy::Id(account_id) => { + return self + .get_value::>(ValueKey::from(ValueClass::Directory( + DirectoryValue::Principal(account_id), + ))) + .await + .map_err(Into::into); + } + QueryBy::Credentials(credentials) => match credentials { + Credentials::Plain { username, secret } => { + (username.as_str(), secret.as_str().into()) + } + Credentials::OAuthBearer { token } => (token.as_str(), token.as_str().into()), + Credentials::XOauth2 { username, secret } => { + (username.as_str(), secret.as_str().into()) + } + }, + }; + + if let Some(account_id) = self.get_account_id(username).await? { + match ( + self.get_value::>(ValueKey::from(ValueClass::Directory( + DirectoryValue::Principal(account_id), + ))) + .await?, + secret, + ) { + (Some(principal), Some(secret)) if principal.verify_secret(secret).await => { + Ok(Some(principal)) + } + (Some(principal), None) => Ok(Some(principal)), + _ => Ok(None), + } + } else { + Ok(None) + } + } + + async fn email_to_ids(&self, email: &str) -> crate::Result> { + self.get_value::>(ValueKey::from(ValueClass::Directory( + DirectoryValue::EmailToId(email.as_bytes().to_vec()), + ))) + .await + .map(|ids| ids.unwrap_or_default()) + .map_err(Into::into) + } + + async fn is_local_domain(&self, domain: &str) -> crate::Result { + self.get_value::<()>(ValueKey::from(ValueClass::Directory( + DirectoryValue::Domain(domain.as_bytes().to_vec()), + ))) + .await + .map(|ids| ids.is_some()) + .map_err(Into::into) + } + + async fn rcpt(&self, address: &str) -> crate::Result { + self.get_value::<()>(ValueKey::from(ValueClass::Directory( + DirectoryValue::EmailToId(address.as_bytes().to_vec()), + ))) + .await + .map(|ids| ids.is_some()) + .map_err(Into::into) + } + + async fn vrfy(&self, address: &str) -> crate::Result> { + let mut results = Vec::new(); + let address = address.split('@').next().unwrap_or(address); + if address.len() > 3 { + self.iterate( + IterateParams::new( + ValueKey::from(ValueClass::Directory(DirectoryValue::EmailToId(vec![0u8]))), + ValueKey::from(ValueClass::Directory(DirectoryValue::EmailToId( + vec![u8::MAX; 10], + ))), + ) + .no_values(), + |key, _| { + let key = + std::str::from_utf8(key.get(1..).unwrap_or_default()).unwrap_or_default(); + if key.split('@').next().unwrap_or(key).contains(address) { + results.push(key.to_string()); + } + Ok(true) + }, + ) + .await?; + } + + Ok(results) + } + + async fn expn(&self, address: &str) -> crate::Result> { + let mut results = Vec::new(); + for account_id in self.email_to_ids(address).await? { + if let Some(email) = self + .get_value::>(ValueKey::from(ValueClass::Directory( + DirectoryValue::Principal(account_id), + ))) + .await? + .and_then(|p| p.emails.into_iter().next()) + { + results.push(email); + } + } + + Ok(results) + } +} diff --git a/crates/directory/src/backend/internal/manage.rs b/crates/directory/src/backend/internal/manage.rs index 71e03247..91597b97 100644 --- a/crates/directory/src/backend/internal/manage.rs +++ b/crates/directory/src/backend/internal/manage.rs @@ -24,27 +24,70 @@ use jmap_proto::types::collection::Collection; use store::{ write::{assert::HashedValue, BatchBuilder, DirectoryValue, ValueClass}, - Serialize, Store, ValueKey, + IterateParams, Serialize, Store, ValueKey, }; -use crate::{Principal, Type}; +use crate::{Directory, DirectoryError, ManagementError, Principal, QueryBy, Type}; + +use super::{PrincipalAction, PrincipalField, PrincipalUpdate, PrincipalValue}; #[async_trait::async_trait] pub trait ManageDirectory { - async fn delete_account_by_name(&self, name: &str) -> store::Result; - async fn delete_account_by_id(&self, id: u32) -> store::Result; - async fn rename_account(&self, name: &str, new_name: String) -> store::Result; - async fn get_account_id(&self, name: &str) -> store::Result>; + async fn get_account_id(&self, name: &str) -> crate::Result>; async fn get_or_create_account_id(&self, name: &str) -> crate::Result; + async fn get_account_name(&self, account_id: u32) -> crate::Result>; + async fn create_account(&self, principal: Principal) -> crate::Result; + async fn update_account( + &self, + by: QueryBy<'_>, + changes: Vec, + ) -> crate::Result<()>; + async fn delete_account(&self, by: QueryBy<'_>) -> crate::Result<()>; + async fn create_domain(&self, domain: &str) -> crate::Result<()>; + async fn delete_domain(&self, domain: &str) -> crate::Result<()>; + async fn list_accounts( + &self, + start_from: Option<&str>, + limit: usize, + ) -> crate::Result>; + async fn map_group_ids(&self, principal: Principal) -> crate::Result>; + async fn map_group_names( + &self, + principal: Principal, + create_if_missing: bool, + ) -> crate::Result>; } #[async_trait::async_trait] impl ManageDirectory for Store { - async fn get_account_id(&self, name: &str) -> store::Result> { + async fn get_account_name(&self, account_id: u32) -> crate::Result> { + self.get_value::>(ValueKey::from(ValueClass::Directory( + DirectoryValue::Principal(account_id), + ))) + .await + .map_err(Into::into) + .map(|v| { + if let Some(v) = v { + Some(v.name) + } else { + tracing::debug!( + context = "directory", + event = "not_found", + account = account_id, + "Principal not found for account id" + ); + + None + } + }) + } + + async fn get_account_id(&self, name: &str) -> crate::Result> { self.get_value::(ValueKey::from(ValueClass::Directory( DirectoryValue::NameToId(name.as_bytes().to_vec()), ))) .await + .map_err(Into::into) } // Used by all directories except internal @@ -102,25 +145,101 @@ impl ManageDirectory for Store { } } - async fn delete_account_by_name(&self, name: &str) -> store::Result { - if let Some(account_id) = self.get_account_id(name).await? { - self.delete_account_by_id(account_id).await - } else { - Ok(false) + async fn create_account(&self, principal: Principal) -> crate::Result { + // Make sure the principal has a name + if principal.name.is_empty() { + return Err(DirectoryError::Management(ManagementError::MissingField( + PrincipalField::Name, + ))); } + + // Map group names + let mut principal = self.map_group_names(principal, false).await?; + + // Make sure new name is not taken + principal.name = principal.name.to_lowercase(); + if self.get_account_id(&principal.name).await?.is_some() { + return Err(DirectoryError::Management(ManagementError::NotUniqueField( + PrincipalField::Name, + ))); + } + + // Make sure the e-mail is not taken and validate domain + for email in principal.emails.iter_mut() { + *email = email.to_lowercase(); + if self.rcpt(email).await? { + return Err(DirectoryError::Management(ManagementError::NotUniqueField( + PrincipalField::Emails, + ))); + } + if let Some(domain) = email.split('@').nth(1) { + if !self.is_local_domain(domain).await? { + return Err(DirectoryError::Management(ManagementError::NotFound( + domain.to_string(), + ))); + } + } + } + + // Assign accountId + let account_id = self + .assign_document_id(u32::MAX, Collection::Principal) + .await?; + + // Write principal + let mut batch = BatchBuilder::new(); + batch + .assert_value( + ValueClass::Directory(DirectoryValue::NameToId( + principal.name.clone().into_bytes(), + )), + (), + ) + .set( + ValueClass::Directory(DirectoryValue::Principal(account_id)), + (&principal).serialize(), + ) + .set( + ValueClass::Directory(DirectoryValue::NameToId(principal.name.into_bytes())), + account_id.serialize(), + ); + + // Write email to id mapping + let ids = if matches!(principal.typ, Type::List) { + principal.member_of + } else { + vec![account_id] + }; + + for email in principal.emails { + batch.set( + ValueClass::Directory(DirectoryValue::EmailToId(email.into_bytes())), + (&ids).serialize(), + ); + } + + self.write(batch.build()).await?; + + Ok(account_id) } - async fn delete_account_by_id(&self, account_id: u32) -> store::Result { - let principal = if let Some(principal) = self - .get_value::(ValueKey::from(ValueClass::Directory( + async fn delete_account(&self, by: QueryBy<'_>) -> crate::Result<()> { + let account_id = match by { + QueryBy::Name(name) => self.get_account_id(name).await?.ok_or_else(|| { + DirectoryError::Management(ManagementError::NotFound(name.to_string())) + })?, + QueryBy::Id(account_id) => account_id, + QueryBy::Credentials(_) => unreachable!(), + }; + + let principal = self + .get_value::>(ValueKey::from(ValueClass::Directory( DirectoryValue::Principal(account_id), ))) .await? - { - principal - } else { - return Ok(false); - }; + .ok_or_else(|| { + DirectoryError::Management(ManagementError::NotFound(account_id.to_string())) + })?; // Unlink all account's blobs self.blob_hash_unlink_account(account_id).await?; @@ -128,102 +247,390 @@ impl ManageDirectory for Store { // Revoke ACLs self.acl_revoke_all(account_id).await?; + // Delete account data + self.purge_account(account_id).await?; + // Delete account let mut batch = BatchBuilder::new(); batch .with_account_id(account_id) - .clear(DirectoryValue::NameToId(principal.name.as_bytes().to_vec())) + .clear(DirectoryValue::NameToId(principal.name.into_bytes())) .clear(DirectoryValue::Principal(account_id)) .clear(DirectoryValue::UsedQuota(account_id)); for email in principal.emails { - batch.clear(DirectoryValue::EmailToId(email.as_bytes().to_vec())); + batch.clear(DirectoryValue::EmailToId(email.into_bytes())); } self.write(batch.build()).await?; - // Delete account data - self.purge_account(account_id).await?; - - Ok(true) + Ok(()) } - async fn rename_account(&self, name: &str, new_name: String) -> store::Result { - if let Some(account_id) = self.get_account_id(name).await? { - if let Some(mut principal) = self - .get_value::>(ValueKey::from(ValueClass::Directory( - DirectoryValue::Principal(account_id), - ))) - .await? - { - if principal.inner.name != name { - return Ok(false); + async fn update_account( + &self, + by: QueryBy<'_>, + changes: Vec, + ) -> crate::Result<()> { + let account_id = match by { + QueryBy::Name(name) => self.get_account_id(name).await?.ok_or_else(|| { + DirectoryError::Management(ManagementError::NotFound(name.to_string())) + })?, + QueryBy::Id(account_id) => account_id, + QueryBy::Credentials(_) => unreachable!(), + }; + + // Fetch principal + let mut principal = self + .get_value::>>(ValueKey::from(ValueClass::Directory( + DirectoryValue::Principal(account_id), + ))) + .await? + .ok_or_else(|| { + DirectoryError::Management(ManagementError::NotFound(account_id.to_string())) + })?; + + // Apply changes + let mut batch = BatchBuilder::new(); + let is_list = matches!(principal.inner.typ, Type::List); + let mut has_list_changes = false; + batch.assert_value( + ValueClass::Directory(DirectoryValue::Principal(account_id)), + &principal, + ); + for change in changes { + match (change.action, change.field, change.value) { + (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 self.get_account_id(&new_name).await?.is_some() { + return Err(DirectoryError::Management( + ManagementError::NotUniqueField(PrincipalField::Name), + )); + } + + batch.clear(ValueClass::Directory(DirectoryValue::NameToId( + principal.inner.name.as_bytes().to_vec(), + ))); + + principal.inner.name = new_name.clone(); + + batch.set( + ValueClass::Directory(DirectoryValue::NameToId(new_name.into_bytes())), + account_id.serialize(), + ); + } } - principal.inner.name = new_name.clone(); + (PrincipalAction::Set, PrincipalField::Type, PrincipalValue::Type(new_type)) + if principal.inner.typ != Type::List && new_type != Type::List => + { + principal.inner.typ = new_type; + } + ( + PrincipalAction::Set, + PrincipalField::Secrets, + PrincipalValue::StringList(secrets), + ) => { + principal.inner.secrets = secrets; + } + ( + PrincipalAction::Set, + PrincipalField::Description, + PrincipalValue::String(description), + ) => { + if !description.is_empty() { + principal.inner.description = Some(description); + } else { + principal.inner.description = None; + } + } + (PrincipalAction::Set, PrincipalField::Quota, PrincipalValue::Integer(quota)) => { + principal.inner.quota = quota; + } + ( + PrincipalAction::Set, + PrincipalField::Emails, + PrincipalValue::StringList(emails), + ) => { + // Validate unique emails + let emails = emails + .into_iter() + .map(|v| v.to_lowercase()) + .collect::>(); + for email in &emails { + if !principal.inner.emails.contains(email) { + if self.rcpt(email).await? { + return Err(DirectoryError::Management( + ManagementError::NotUniqueField(PrincipalField::Emails), + )); + } + if let Some(domain) = email.split('@').nth(1) { + if !self.is_local_domain(domain).await? { + return Err(DirectoryError::Management( + ManagementError::NotFound(domain.to_string()), + )); + } + } + if !is_list { + batch.set( + ValueClass::Directory(DirectoryValue::EmailToId( + email.as_bytes().to_vec(), + )), + vec![account_id].serialize(), + ); + } + } + } + if !is_list { + for email in &principal.inner.emails { + if !emails.contains(email) { + batch.clear(ValueClass::Directory(DirectoryValue::EmailToId( + email.as_bytes().to_vec(), + ))); + } + } + } - let mut batch = BatchBuilder::new(); - batch - .assert_value( - ValueClass::Directory(DirectoryValue::Principal(account_id)), - &principal, - ) - .set( - ValueClass::Directory(DirectoryValue::Principal(account_id)), - principal.inner.serialize(), - ) - .clear(ValueClass::Directory(DirectoryValue::NameToId( - name.as_bytes().to_vec(), - ))) - .set( - ValueClass::Directory(DirectoryValue::NameToId(new_name.into_bytes())), - account_id.serialize(), - ); - self.write(batch.build()).await?; - - return Ok(true); + principal.inner.emails = emails; + } + ( + PrincipalAction::Set, + PrincipalField::MemberOf, + PrincipalValue::StringList(members), + ) => { + if is_list { + has_list_changes = true; + } + principal.inner.member_of = Vec::with_capacity(members.len()); + for member in members { + let account_id = self.get_account_id(&member).await?.ok_or_else(|| { + DirectoryError::Management(ManagementError::NotFound(member)) + })?; + principal.inner.member_of.push(account_id); + } + } + ( + PrincipalAction::AddItem, + PrincipalField::MemberOf, + PrincipalValue::String(member), + ) => { + let account_id = self.get_account_id(&member).await?.ok_or_else(|| { + DirectoryError::Management(ManagementError::NotFound(member)) + })?; + if !principal.inner.member_of.contains(&account_id) { + principal.inner.member_of.push(account_id); + if is_list { + has_list_changes = true; + } + } + } + ( + PrincipalAction::AddItem, + PrincipalField::Emails, + PrincipalValue::String(email), + ) => { + let email = email.to_lowercase(); + if !principal.inner.emails.contains(&email) { + if self.rcpt(&email).await? { + return Err(DirectoryError::Management( + ManagementError::NotUniqueField(PrincipalField::Emails), + )); + } + if let Some(domain) = email.split('@').nth(1) { + if !self.is_local_domain(domain).await? { + return Err(DirectoryError::Management(ManagementError::NotFound( + domain.to_string(), + ))); + } + } + if !is_list { + batch.set( + ValueClass::Directory(DirectoryValue::EmailToId( + email.as_bytes().to_vec(), + )), + vec![account_id].serialize(), + ); + } + principal.inner.emails.push(email); + } + } + ( + PrincipalAction::RemoveItem, + PrincipalField::MemberOf, + PrincipalValue::String(member), + ) => { + if let Some(account_id) = self.get_account_id(&member).await? { + if let Some(pos) = principal + .inner + .member_of + .iter() + .position(|v| *v == account_id) + { + principal.inner.member_of.remove(pos); + if is_list { + has_list_changes = true; + } + } + } + } + ( + PrincipalAction::RemoveItem, + PrincipalField::Emails, + PrincipalValue::String(email), + ) => { + let email = email.to_lowercase(); + if let Some(pos) = principal.inner.emails.iter().position(|v| *v == email) { + if !is_list { + batch.clear(ValueClass::Directory(DirectoryValue::EmailToId( + email.as_bytes().to_vec(), + ))); + } + principal.inner.emails.remove(pos); + } + } + _ => { + return Err(DirectoryError::Unsupported); + } } } - Ok(false) + if has_list_changes { + for email in &principal.inner.emails { + batch.set( + ValueClass::Directory(DirectoryValue::EmailToId(email.as_bytes().to_vec())), + (&principal.inner.member_of).serialize(), + ); + } + } + + batch.set( + ValueClass::Directory(DirectoryValue::Principal(account_id)), + principal.inner.serialize(), + ); + + self.write(batch.build()).await?; + + Ok(()) + } + + async fn create_domain(&self, domain: &str) -> crate::Result<()> { + if !domain.contains('.') { + return Err(DirectoryError::Management(ManagementError::MissingField( + PrincipalField::Name, + ))); + } + let mut batch = BatchBuilder::new(); + batch.set( + ValueClass::Directory(DirectoryValue::Domain(domain.to_lowercase().into_bytes())), + vec![], + ); + self.write(batch.build()).await.map_err(Into::into) + } + + async fn delete_domain(&self, domain: &str) -> crate::Result<()> { + if !domain.contains('.') { + return Err(DirectoryError::Management(ManagementError::MissingField( + PrincipalField::Name, + ))); + } + let mut batch = BatchBuilder::new(); + batch.clear(ValueClass::Directory(DirectoryValue::Domain( + domain.to_lowercase().into_bytes(), + ))); + self.write(batch.build()).await.map_err(Into::into) + } + + async fn map_group_ids(&self, principal: Principal) -> crate::Result> { + 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? { + mapped.member_of.push(name); + } + } + + Ok(mapped) + } + + async fn map_group_names( + &self, + principal: Principal, + create_if_missing: bool, + ) -> crate::Result> { + 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 member in principal.member_of { + let account_id = if create_if_missing { + self.get_or_create_account_id(&member).await? + } else { + self.get_account_id(&member) + .await? + .ok_or_else(|| DirectoryError::Management(ManagementError::NotFound(member)))? + }; + mapped.member_of.push(account_id); + } + + Ok(mapped) + } + + async fn list_accounts( + &self, + start_from: Option<&str>, + limit: usize, + ) -> crate::Result> { + let from_key = ValueKey::from(ValueClass::Directory(DirectoryValue::NameToId( + start_from.unwrap_or("").as_bytes().to_vec(), + ))); + let to_key = ValueKey::from(ValueClass::Directory(DirectoryValue::NameToId(vec![ + u8::MAX; + 10 + ]))); + + let mut results = Vec::with_capacity(limit); + self.iterate( + IterateParams::new(from_key, to_key).no_values().ascending(), + |key, _| { + results + .push(String::from_utf8_lossy(key.get(1..).unwrap_or_default()).into_owned()); + Ok(limit == 0 || results.len() < limit) + }, + ) + .await?; + + Ok(results) } } -/* -pub async fn try_get_account_id(store: &Store, name: &str) -> crate::Result> { - store - .get_value::(NamedKey::Name(name)) - .await - .map_err(|err| { - tracing::error!(event = "error", - context = "store", - account_name = name, - error = ?err, - "Failed to retrieve account id"); - MethodError::ServerPartialFail - }) -} - - - -pub async fn map_member_of(store: &Store, names: Vec) -> crate::Result> { - let mut ids = Vec::with_capacity(names.len()); - for name in names { - ids.push(self.get_account_id(&name).await?); +impl From> for Principal { + fn from(principal: Principal) -> 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, + } } - Ok(ids) } - -pub async fn get_account_name(store: &Store, account_id: u32) -> crate::Result> { - store - .get_value::(NamedKey::Id::<&[u8]>(account_id)) - .await - .map_err(|err| { - tracing::error!(event = "error", - context = "store", - account_id = account_id, - error = ?err, - "Failed to retrieve account name"); - MethodError::ServerPartialFail - }) -} -*/ diff --git a/crates/directory/src/backend/internal/mod.rs b/crates/directory/src/backend/internal/mod.rs index 73f47163..5c58b92b 100644 --- a/crates/directory/src/backend/internal/mod.rs +++ b/crates/directory/src/backend/internal/mod.rs @@ -21,101 +21,23 @@ * for more details. */ +pub mod lookup; pub mod manage; use std::slice::Iter; -use mail_send::Credentials; -use store::{ - write::{key::KeySerializer, DirectoryValue, ValueClass}, - Deserialize, Serialize, Store, ValueKey, U32_LEN, -}; +use store::{write::key::KeySerializer, Deserialize, Serialize, U32_LEN}; use utils::codec::leb128::Leb128Iterator; -use crate::{Principal, QueryBy, QueryType, Type}; +use crate::{Principal, Type}; -use self::manage::ManageDirectory; - -pub struct InternalDirectory { - pub store: Store, -} - -impl<'x> QueryBy<'x> { - pub fn name(name: &'x str) -> Self { - Self { - t: QueryType::Name(name), - store: None, - } - } - - pub fn id(id: u32) -> Self { - Self { - t: QueryType::Id(id), - store: None, - } - } - - pub fn credentials(credentials: &'x Credentials) -> Self { - Self { - t: QueryType::Credentials(credentials), - store: None, - } - } - - pub fn with_store(mut self, store: &'x Store) -> Self { - self.store = Some(store); - self - } - - pub fn has_store(&self) -> bool { - self.store.is_some() - } - - pub async fn account_name(&self, account_id: u32) -> crate::Result> { - self.store - .unwrap() - .get_value::(ValueKey::from(ValueClass::Directory( - DirectoryValue::Principal(account_id), - ))) - .await - .map_err(Into::into) - .map(|v| { - if let Some(v) = v { - Some(v.name) - } else { - tracing::debug!( - context = "directory", - event = "not_found", - account = account_id, - "Principal not found for account id" - ); - - None - } - }) - } - - pub async fn account_id(&self, name: &str) -> crate::Result { - self.store - .unwrap() - .get_or_create_account_id(name) - .await - .map_err(Into::into) - } - - pub async fn account_ids>( - &self, - items: impl Iterator, - ) -> crate::Result> { - let mut ids = Vec::new(); - for item in items { - ids.push(self.account_id(item.as_ref()).await?); - } - Ok(ids) +impl Serialize for Principal { + fn serialize(self) -> Vec { + (&self).serialize() } } -impl Serialize for Principal { +impl Serialize for &Principal { fn serialize(self) -> Vec { let mut serializer = KeySerializer::new( U32_LEN * 3 @@ -151,14 +73,14 @@ impl Serialize for Principal { } } -impl Deserialize for Principal { +impl Deserialize for Principal { fn deserialize(bytes: &[u8]) -> store::Result { deserialize(bytes) .ok_or_else(|| store::Error::InternalError("Failed to deserialize principal".into())) } } -fn deserialize(bytes: &[u8]) -> Option { +fn deserialize(bytes: &[u8]) -> Option> { let mut bytes = bytes.iter(); if bytes.next()? != &1 { return None; @@ -183,6 +105,76 @@ fn deserialize(bytes: &[u8]) -> Option { .into() } +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub enum PrincipalField { + #[serde(rename = "name")] + Name, + #[serde(rename = "type")] + Type, + #[serde(rename = "quota")] + Quota, + #[serde(rename = "description")] + Description, + #[serde(rename = "secrets")] + Secrets, + #[serde(rename = "emails")] + Emails, + #[serde(rename = "memberOf")] + MemberOf, +} + +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct PrincipalUpdate { + action: PrincipalAction, + field: PrincipalField, + value: PrincipalValue, +} + +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub enum PrincipalAction { + #[serde(rename = "set")] + Set, + #[serde(rename = "addItem")] + AddItem, + #[serde(rename = "removeItem")] + RemoveItem, +} + +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +#[serde(untagged)] +pub enum PrincipalValue { + String(String), + StringList(Vec), + Integer(u32), + Type(Type), +} + +impl PrincipalUpdate { + pub fn set(field: PrincipalField, value: PrincipalValue) -> PrincipalUpdate { + PrincipalUpdate { + action: PrincipalAction::Set, + field, + value, + } + } + + pub fn add_item(field: PrincipalField, value: PrincipalValue) -> PrincipalUpdate { + PrincipalUpdate { + action: PrincipalAction::AddItem, + field, + value, + } + } + + pub fn remove_item(field: PrincipalField, value: PrincipalValue) -> PrincipalUpdate { + PrincipalUpdate { + action: PrincipalAction::RemoveItem, + field, + value, + } + } +} + fn deserialize_string(bytes: &mut Iter<'_, u8>) -> Option { let len = bytes.next_leb128()?; let mut string = Vec::with_capacity(len); diff --git a/crates/directory/src/backend/ldap/config.rs b/crates/directory/src/backend/ldap/config.rs index 4181aa6e..ce60679e 100644 --- a/crates/directory/src/backend/ldap/config.rs +++ b/crates/directory/src/backend/ldap/config.rs @@ -24,6 +24,7 @@ use std::sync::Arc; use ldap3::LdapConnSettings; +use store::Store; use utils::config::{utils::AsKey, Config}; use crate::{cache::CachedDirectory, config::build_pool, Directory, DirectoryOptions}; @@ -34,6 +35,7 @@ impl LdapDirectory { pub fn from_config( config: &Config, prefix: impl AsKey, + id_store: Option, ) -> utils::config::Result> { let prefix = prefix.as_key(); let bind_dn = if let Some(dn) = config.value((&prefix, "bind.dn")) { @@ -127,6 +129,7 @@ impl LdapDirectory { pool: build_pool(config, &prefix, manager)?, opt: DirectoryOptions::from_config(config, prefix.as_str())?, auth_bind, + id_store, }, ) } diff --git a/crates/directory/src/backend/ldap/lookup.rs b/crates/directory/src/backend/ldap/lookup.rs index d392931f..61e6146d 100644 --- a/crates/directory/src/backend/ldap/lookup.rs +++ b/crates/directory/src/backend/ldap/lookup.rs @@ -26,21 +26,20 @@ use mail_send::Credentials; use store::Store; use crate::{ - backend::internal::manage::ManageDirectory, Directory, DirectoryError, Principal, QueryBy, - QueryType, Type, + backend::internal::manage::ManageDirectory, Directory, DirectoryError, Principal, QueryBy, Type, }; use super::{LdapDirectory, LdapMappings}; #[async_trait::async_trait] impl Directory for LdapDirectory { - async fn query(&self, by: QueryBy<'_>) -> crate::Result> { + async fn query(&self, by: QueryBy<'_>) -> crate::Result>> { let mut conn = self.pool.get().await?; let mut account_id = None; let account_name; - let principal = match by.t { - QueryType::Name(username) => { + let principal = match by { + QueryBy::Name(username) => { account_name = username.to_string(); if let Some(principal) = self @@ -52,8 +51,8 @@ impl Directory for LdapDirectory { return Ok(None); } } - QueryType::Id(uid) => { - if let Some(username) = by.account_name(uid).await? { + QueryBy::Id(uid) => { + if let Some(username) = self.unwrap_id_store().get_account_name(uid).await? { account_name = username; } else { return Ok(None); @@ -69,7 +68,7 @@ impl Directory for LdapDirectory { return Ok(None); } } - QueryType::Credentials(credentials) => { + QueryBy::Credentials(credentials) => { let (username, secret) = match credentials { Credentials::Plain { username, secret } => (username, secret), Credentials::OAuthBearer { token } => (token, token), @@ -105,7 +104,7 @@ impl Directory for LdapDirectory { .find_principal(&mut conn, &self.mappings.filter_name.build(username)) .await? { - if principal.principal.verify_secret(secret).await { + if principal.verify_secret(secret).await { principal } else { tracing::debug!( @@ -122,25 +121,26 @@ impl Directory for LdapDirectory { } } }; - let groups = principal.groups; - let mut principal = principal.principal; + let mut principal = principal; // Obtain account ID if not available if let Some(account_id) = account_id { principal.id = account_id; - } else if by.has_store() { - principal.id = by.account_id(&account_name).await?; + } else if self.has_id_store() { + principal.id = self + .unwrap_id_store() + .get_or_create_account_id(&account_name) + .await?; } principal.name = account_name; // Obtain groups - if by.has_store() && !groups.is_empty() { - principal.member_of = Vec::with_capacity(groups.len()); - for group in groups { - if group.contains('=') { + if !principal.member_of.is_empty() && self.has_id_store() { + for member_of in principal.member_of.iter_mut() { + if member_of.contains('=') { let (rs, _res) = conn .search( - &group, + member_of, Scope::Base, "objectClass=*", &self.mappings.attr_name, @@ -150,25 +150,29 @@ impl Directory for LdapDirectory { for entry in rs { 'outer: for (attr, value) in SearchEntry::construct(entry).attrs { if self.mappings.attr_name.contains(&attr) { - if let Some(group) = value.first() { + if let Some(group) = value.into_iter().next() { if !group.is_empty() { - principal.member_of.push(by.account_id(group).await?); + *member_of = group; break 'outer; } } } } } - } else { - principal.member_of.push(by.account_id(&group).await?); } } - } - Ok(Some(principal)) + // Map ids + self.unwrap_id_store() + .map_group_names(principal, true) + .await + .map(Some) + } else { + Ok(Some(principal.into())) + } } - async fn email_to_ids(&self, address: &str, store: &Store) -> crate::Result> { + async fn email_to_ids(&self, address: &str) -> crate::Result> { let mut rs = self .pool .get() @@ -212,7 +216,11 @@ impl Directory for LdapDirectory { 'outer: for attr in &self.mappings.attr_name { if let Some(name) = entry.attrs.get(attr).and_then(|v| v.first()) { if !name.is_empty() { - ids.push(store.get_or_create_account_id(name).await?); + ids.push( + self.unwrap_id_store() + .get_or_create_account_id(name) + .await?, + ); break 'outer; } } @@ -355,7 +363,7 @@ impl LdapDirectory { &self, conn: &mut Ldap, filter: &str, - ) -> crate::Result> { + ) -> crate::Result>> { conn.search( &self.mappings.base_dn, Scope::Subtree, @@ -372,16 +380,18 @@ impl LdapDirectory { }) .map_err(Into::into) } -} -struct PrincipalWithGroups { - principal: Principal, - groups: Vec, + pub fn has_id_store(&self) -> bool { + self.id_store.is_some() + } + + pub fn unwrap_id_store(&self) -> &Store { + self.id_store.as_ref().unwrap() + } } impl LdapMappings { - fn entry_to_principal(&self, entry: SearchEntry) -> PrincipalWithGroups { - let mut groups = Vec::new(); + fn entry_to_principal(&self, entry: SearchEntry) -> Principal { let mut principal = Principal::default(); for (attr, value) in entry.attrs { @@ -404,7 +414,7 @@ impl LdapMappings { principal.description = value.into_iter().next(); } } else if self.attr_groups.contains(&attr) { - groups.extend(value); + principal.member_of.extend(value); } else if self.attr_quota.contains(&attr) { if let Ok(quota) = value.into_iter().next().unwrap_or_default().parse() { principal.quota = quota; @@ -426,6 +436,6 @@ impl LdapMappings { } } - PrincipalWithGroups { principal, groups } + principal } } diff --git a/crates/directory/src/backend/ldap/mod.rs b/crates/directory/src/backend/ldap/mod.rs index e8c57cbb..06e2a667 100644 --- a/crates/directory/src/backend/ldap/mod.rs +++ b/crates/directory/src/backend/ldap/mod.rs @@ -23,6 +23,7 @@ use deadpool::managed::Pool; use ldap3::{ldap_escape, LdapConnSettings}; +use store::Store; use crate::DirectoryOptions; @@ -35,6 +36,7 @@ pub struct LdapDirectory { mappings: LdapMappings, opt: DirectoryOptions, auth_bind: Option, + id_store: Option, } #[derive(Debug, Default)] diff --git a/crates/directory/src/backend/memory/lookup.rs b/crates/directory/src/backend/memory/lookup.rs index 65bb4fec..ff80cc89 100644 --- a/crates/directory/src/backend/memory/lookup.rs +++ b/crates/directory/src/backend/memory/lookup.rs @@ -22,31 +22,30 @@ */ use mail_send::Credentials; -use store::Store; -use crate::{Directory, Principal, QueryBy, QueryType}; +use crate::{Directory, Principal, QueryBy}; use super::{EmailType, MemoryDirectory}; #[async_trait::async_trait] impl Directory for MemoryDirectory { - async fn query(&self, by: QueryBy<'_>) -> crate::Result> { - match by.t { - QueryType::Name(name) => { + async fn query(&self, by: QueryBy<'_>) -> crate::Result>> { + match by { + QueryBy::Name(name) => { for principal in &self.principals { if principal.name == name { return Ok(Some(principal.clone())); } } } - QueryType::Id(uid) => { + QueryBy::Id(uid) => { for principal in &self.principals { if principal.id == uid { return Ok(Some(principal.clone())); } } } - QueryType::Credentials(credentials) => { + QueryBy::Credentials(credentials) => { let (username, secret) = match credentials { Credentials::Plain { username, secret } => (username, secret), Credentials::OAuthBearer { token } => (token, token), @@ -67,7 +66,7 @@ impl Directory for MemoryDirectory { Ok(None) } - async fn email_to_ids(&self, address: &str, _: &Store) -> crate::Result> { + async fn email_to_ids(&self, address: &str) -> crate::Result> { Ok(self .emails_to_ids .get(self.opt.subaddressing.to_subaddress(address).as_ref()) diff --git a/crates/directory/src/backend/memory/mod.rs b/crates/directory/src/backend/memory/mod.rs index dded7716..973f31ba 100644 --- a/crates/directory/src/backend/memory/mod.rs +++ b/crates/directory/src/backend/memory/mod.rs @@ -30,7 +30,7 @@ pub mod lookup; #[derive(Default, Debug)] pub struct MemoryDirectory { - principals: Vec, + principals: Vec>, emails_to_ids: AHashMap>, names_to_ids: AHashMap, domains: AHashSet, diff --git a/crates/directory/src/backend/smtp/lookup.rs b/crates/directory/src/backend/smtp/lookup.rs index 80330abd..5744f3a4 100644 --- a/crates/directory/src/backend/smtp/lookup.rs +++ b/crates/directory/src/backend/smtp/lookup.rs @@ -23,23 +23,22 @@ use mail_send::{smtp::AssertReply, Credentials}; use smtp_proto::Severity; -use store::Store; -use crate::{Directory, DirectoryError, Principal, QueryBy, QueryType}; +use crate::{Directory, DirectoryError, Principal, QueryBy}; use super::{SmtpClient, SmtpDirectory}; #[async_trait::async_trait] impl Directory for SmtpDirectory { - async fn query(&self, query: QueryBy<'_>) -> crate::Result> { - if let QueryType::Credentials(credentials) = query.t { + async fn query(&self, query: QueryBy<'_>) -> crate::Result>> { + if let QueryBy::Credentials(credentials) = query { self.pool.get().await?.authenticate(credentials).await } else { Err(DirectoryError::unsupported("smtp", "query")) } } - async fn email_to_ids(&self, _address: &str, _store: &Store) -> crate::Result> { + async fn email_to_ids(&self, _address: &str) -> crate::Result> { Err(DirectoryError::unsupported("smtp", "email_to_ids")) } @@ -96,7 +95,7 @@ impl SmtpClient { async fn authenticate( &mut self, credentials: &Credentials, - ) -> crate::Result> { + ) -> crate::Result>> { match self .client .authenticate(credentials, &self.capabilities) diff --git a/crates/directory/src/backend/sql/config.rs b/crates/directory/src/backend/sql/config.rs index 2215eb0a..2ed6eda8 100644 --- a/crates/directory/src/backend/sql/config.rs +++ b/crates/directory/src/backend/sql/config.rs @@ -23,7 +23,7 @@ use std::sync::Arc; -use store::Stores; +use store::{Store, Stores}; use utils::config::{utils::AsKey, Config}; use crate::{cache::CachedDirectory, Directory, DirectoryOptions}; @@ -35,6 +35,7 @@ impl SqlDirectory { config: &Config, prefix: impl AsKey, stores: &Stores, + id_store: Option, ) -> utils::config::Result> { let prefix = prefix.as_key(); let store_id = config.value_require((&prefix, "store"))?; @@ -87,6 +88,7 @@ impl SqlDirectory { store, mappings, opt: DirectoryOptions::from_config(config, prefix.as_str())?, + id_store, }, ) } diff --git a/crates/directory/src/backend/sql/lookup.rs b/crates/directory/src/backend/sql/lookup.rs index 51706cad..50960113 100644 --- a/crates/directory/src/backend/sql/lookup.rs +++ b/crates/directory/src/backend/sql/lookup.rs @@ -24,29 +24,27 @@ use mail_send::Credentials; use store::{NamedRows, Rows, Store, Value}; -use crate::{ - backend::internal::manage::ManageDirectory, Directory, Principal, QueryBy, QueryType, Type, -}; +use crate::{backend::internal::manage::ManageDirectory, Directory, Principal, QueryBy, Type}; use super::{SqlDirectory, SqlMappings}; #[async_trait::async_trait] impl Directory for SqlDirectory { - async fn query(&self, by: QueryBy<'_>) -> crate::Result> { + async fn query(&self, by: QueryBy<'_>) -> crate::Result>> { let mut account_id = None; let account_name; let mut secret = None; - let result = match by.t { - QueryType::Name(username) => { + let result = match by { + QueryBy::Name(username) => { account_name = username.to_string(); self.store .query::(&self.mappings.query_name, vec![username.into()]) .await? } - QueryType::Id(uid) => { - if let Some(username) = by.account_name(uid).await? { + QueryBy::Id(uid) => { + if let Some(username) = self.unwrap_id_store().get_account_name(uid).await? { account_name = username; } else { return Ok(None); @@ -60,7 +58,7 @@ impl Directory for SqlDirectory { ) .await? } - QueryType::Credentials(credentials) => { + QueryBy::Credentials(credentials) => { let (username, secret_) = match credentials { Credentials::Plain { username, secret } => (username, secret), Credentials::OAuthBearer { token } => (token, token), @@ -99,12 +97,15 @@ impl Directory for SqlDirectory { // Obtain account ID if not available if let Some(account_id) = account_id { principal.id = account_id; - } else if by.has_store() { - principal.id = by.account_id(&account_name).await?; + } else if self.has_id_store() { + principal.id = self + .unwrap_id_store() + .get_or_create_account_id(&account_name) + .await?; } principal.name = account_name; - if by.has_store() { + if self.has_id_store() { // Obtain members if !self.mappings.query_members.is_empty() { for row in self @@ -117,7 +118,11 @@ impl Directory for SqlDirectory { .rows { if let Some(Value::Text(account_id)) = row.values.first() { - principal.member_of.push(by.account_id(account_id).await?); + principal.member_of.push( + self.unwrap_id_store() + .get_or_create_account_id(account_id) + .await?, + ); } } } @@ -138,7 +143,7 @@ impl Directory for SqlDirectory { Ok(Some(principal)) } - async fn email_to_ids(&self, address: &str, store: &Store) -> crate::Result> { + async fn email_to_ids(&self, address: &str) -> crate::Result> { let mut names = self .store .query::( @@ -167,7 +172,11 @@ impl Directory for SqlDirectory { for row in names.rows { if let Some(Value::Text(name)) = row.values.first() { - ids.push(store.get_or_create_account_id(name).await?); + ids.push( + self.unwrap_id_store() + .get_or_create_account_id(name) + .await?, + ); } } @@ -242,8 +251,18 @@ impl Directory for SqlDirectory { } } +impl SqlDirectory { + pub fn has_id_store(&self) -> bool { + self.id_store.is_some() + } + + pub fn unwrap_id_store(&self) -> &Store { + self.id_store.as_ref().unwrap() + } +} + impl SqlMappings { - pub fn row_to_principal(&self, rows: NamedRows) -> crate::Result { + pub fn row_to_principal(&self, rows: NamedRows) -> crate::Result> { let mut principal = Principal::default(); if let Some(row) = rows.rows.into_iter().next() { diff --git a/crates/directory/src/backend/sql/mod.rs b/crates/directory/src/backend/sql/mod.rs index 001b5e6f..e081f8bc 100644 --- a/crates/directory/src/backend/sql/mod.rs +++ b/crates/directory/src/backend/sql/mod.rs @@ -21,7 +21,7 @@ * for more details. */ -use store::LookupStore; +use store::{LookupStore, Store}; use crate::DirectoryOptions; @@ -32,6 +32,7 @@ pub struct SqlDirectory { store: LookupStore, mappings: SqlMappings, opt: DirectoryOptions, + id_store: Option, } #[derive(Debug, Default)] diff --git a/crates/directory/src/cache/lookup.rs b/crates/directory/src/cache/lookup.rs index cc31330d..6729a769 100644 --- a/crates/directory/src/cache/lookup.rs +++ b/crates/directory/src/cache/lookup.rs @@ -21,20 +21,18 @@ * for more details. */ -use store::Store; - use crate::{Directory, Principal, QueryBy}; use super::CachedDirectory; #[async_trait::async_trait] impl Directory for CachedDirectory { - async fn query(&self, by: QueryBy<'_>) -> crate::Result> { + async fn query(&self, by: QueryBy<'_>) -> crate::Result>> { self.inner.query(by).await } - async fn email_to_ids(&self, address: &str, store: &Store) -> crate::Result> { - self.inner.email_to_ids(address, store).await + async fn email_to_ids(&self, address: &str) -> crate::Result> { + self.inner.email_to_ids(address).await } async fn rcpt(&self, address: &str) -> crate::Result { diff --git a/crates/directory/src/config.rs b/crates/directory/src/config.rs index e62e6d14..d86c633b 100644 --- a/crates/directory/src/config.rs +++ b/crates/directory/src/config.rs @@ -44,22 +44,31 @@ use crate::{ }; pub trait ConfigDirectory { - fn parse_directory(&self, stores: &Stores) -> utils::config::Result; + fn parse_directory( + &self, + stores: &Stores, + id_store: Option<&str>, + ) -> utils::config::Result; } impl ConfigDirectory for Config { - fn parse_directory(&self, stores: &Stores) -> utils::config::Result { + fn parse_directory( + &self, + stores: &Stores, + id_store: Option<&str>, + ) -> utils::config::Result { let mut config = Directories { directories: AHashMap::new(), }; + let id_store = id_store.and_then(|id| stores.stores.get(id).cloned()); for id in self.sub_keys("directory") { // Parse directory let protocol = self.value_require(("directory", id, "type"))?; let prefix = ("directory", id); let directory = match protocol { - "ldap" => LdapDirectory::from_config(self, prefix)?, - "sql" => SqlDirectory::from_config(self, prefix, stores)?, + "ldap" => LdapDirectory::from_config(self, prefix, id_store.clone())?, + "sql" => SqlDirectory::from_config(self, prefix, stores, id_store.clone())?, "imap" => ImapDirectory::from_config(self, prefix)?, "smtp" => SmtpDirectory::from_config(self, prefix, false)?, "lmtp" => SmtpDirectory::from_config(self, prefix, true)?, diff --git a/crates/directory/src/lib.rs b/crates/directory/src/lib.rs index f5a59032..95e8d13c 100644 --- a/crates/directory/src/lib.rs +++ b/crates/directory/src/lib.rs @@ -24,11 +24,10 @@ use std::{borrow::Cow, fmt::Debug, sync::Arc}; use ahash::AHashMap; -use backend::imap::ImapError; +use backend::{imap::ImapError, internal::PrincipalField}; use deadpool::managed::PoolError; use ldap3::LdapError; use mail_send::Credentials; -use store::Store; use utils::config::DynValue; pub mod backend; @@ -36,27 +35,42 @@ pub mod cache; pub mod config; pub mod secret; -#[derive(Debug, Default, Clone, PartialEq, Eq)] -pub struct Principal { +#[derive(Debug, Default, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct Principal { + #[serde(default, skip)] pub id: u32, + #[serde(rename = "type")] pub typ: Type, + #[serde(default)] pub quota: u32, pub name: String, + #[serde(default, skip_serializing)] pub secrets: Vec, + #[serde(default)] pub emails: Vec, - pub member_of: Vec, + #[serde(default)] + #[serde(rename = "memberOf")] + pub member_of: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] pub description: Option, } -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub enum Type { + #[serde(rename = "individual")] Individual = 0, + #[serde(rename = "group")] Group = 1, + #[serde(rename = "resource")] Resource = 2, + #[serde(rename = "location")] Location = 3, + #[serde(rename = "superuser")] Superuser = 4, + #[serde(rename = "list")] List = 5, #[default] + #[serde(rename = "other")] Other = 6, } @@ -67,14 +81,22 @@ pub enum DirectoryError { Imap(ImapError), Smtp(mail_send::Error), Pool(String), + Management(ManagementError), TimedOut, Unsupported, } +#[derive(Debug, PartialEq, Eq)] +pub enum ManagementError { + MissingField(PrincipalField), + NotUniqueField(PrincipalField), + NotFound(String), +} + #[async_trait::async_trait] pub trait Directory: Sync + Send { - async fn query(&self, by: QueryBy<'_>) -> Result>; - async fn email_to_ids(&self, email: &str, store: &Store) -> Result>; + async fn query(&self, by: QueryBy<'_>) -> Result>>; + async fn email_to_ids(&self, email: &str) -> Result>; async fn is_local_domain(&self, domain: &str) -> crate::Result; async fn rcpt(&self, address: &str) -> crate::Result; @@ -82,18 +104,13 @@ pub trait Directory: Sync + Send { async fn expn(&self, address: &str) -> Result>; } -pub enum QueryType<'x> { +pub enum QueryBy<'x> { Name(&'x str), Id(u32), Credentials(&'x Credentials), } -pub struct QueryBy<'x> { - pub t: QueryType<'x>, - pub store: Option<&'x Store>, -} - -impl Principal { +impl Principal { pub fn name(&self) -> &str { &self.name } @@ -310,3 +327,14 @@ impl AddressMapping { } } } + +impl PartialEq for DirectoryError { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::Store(l0), Self::Store(r0)) => l0 == r0, + (Self::Pool(l0), Self::Pool(r0)) => l0 == r0, + (Self::Management(l0), Self::Management(r0)) => l0 == r0, + _ => false, + } + } +} diff --git a/crates/directory/src/secret.rs b/crates/directory/src/secret.rs index dda26441..4cb0bb7c 100644 --- a/crates/directory/src/secret.rs +++ b/crates/directory/src/secret.rs @@ -36,7 +36,7 @@ use tokio::sync::oneshot; use crate::Principal; -impl Principal { +impl Principal { pub async fn verify_secret(&self, secret: &str) -> bool { for hashed_secret in &self.secrets { if verify_secret_hash(hashed_secret, secret).await { diff --git a/crates/imap/src/core/mailbox.rs b/crates/imap/src/core/mailbox.rs index 69d45f96..bedbdea2 100644 --- a/crates/imap/src/core/mailbox.rs +++ b/crates/imap/src/core/mailbox.rs @@ -52,7 +52,7 @@ impl SessionData { session .jmap .directory - .query(QueryBy::id(account_id).with_store(&session.jmap.store)) + .query(QueryBy::Id(account_id)) .await .unwrap_or_default() .map(|p| p.name) @@ -320,7 +320,7 @@ impl SessionData { self.imap.name_shared, self.jmap .directory - .query(QueryBy::id(account_id).with_store(&self.jmap.store)) + .query(QueryBy::Id(account_id)) .await .unwrap_or_default() .map(|p| p.name) @@ -407,7 +407,7 @@ impl SessionData { self.imap.name_shared, self.jmap .directory - .query(QueryBy::id(account_id).with_store(&self.jmap.store)) + .query(QueryBy::Id(account_id)) .await .unwrap_or_default() .map(|p| p.name) diff --git a/crates/imap/src/op/acl.rs b/crates/imap/src/op/acl.rs index 555527ec..e2795283 100644 --- a/crates/imap/src/op/acl.rs +++ b/crates/imap/src/op/acl.rs @@ -76,10 +76,7 @@ impl Session { if let Some(account_name) = data .jmap .directory - .query( - QueryBy::id(id.document_id()) - .with_store(&data.jmap.store), - ) + .query(QueryBy::Id(id.document_id())) .await .unwrap_or_default() .map(|p| p.name) @@ -251,10 +248,7 @@ impl Session { let (acl_account_id, id) = match data .jmap .directory - .query( - QueryBy::name(arguments.identifier.as_ref().unwrap()) - .with_store(&data.jmap.store), - ) + .query(QueryBy::Name(arguments.identifier.as_ref().unwrap())) .await { Ok(Some(principal)) => (principal.id, Value::Id(Id::from(principal.id))), diff --git a/crates/jmap/src/api/admin.rs b/crates/jmap/src/api/admin.rs new file mode 100644 index 00000000..a832c8c7 --- /dev/null +++ b/crates/jmap/src/api/admin.rs @@ -0,0 +1,303 @@ +/* + * Copyright (c) 2023 Stalwart Labs Ltd. + * + * This file is part of Stalwart Mail Server. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use directory::{ + backend::internal::{manage::ManageDirectory, PrincipalUpdate}, + Directory, DirectoryError, ManagementError, Principal, QueryBy, Type, +}; +use http_body_util::combinators::BoxBody; +use hyper::{body::Bytes, Method, StatusCode}; +use jmap_proto::error::request::RequestError; +use serde_json::json; + +use crate::JMAP; + +use super::{http::ToHttpResponse, HttpRequest, JsonResponse}; + +#[derive(Debug, serde::Serialize, serde::Deserialize)] +pub struct PrincipalResponse { + pub id: u32, + #[serde(rename = "type")] + pub typ: Type, + pub quota: u32, + #[serde(rename = "usedQuota")] + pub used_quota: u32, + pub name: String, + pub emails: Vec, + #[serde(rename = "memberOf")] + pub member_of: Vec, + pub description: Option, +} + +impl JMAP { + pub async fn handle_manage_request( + &self, + req: &HttpRequest, + body: Option>, + ) -> hyper::Response> { + let mut path = req.uri().path().split('/'); + path.next(); + path.next(); + + match (path.next().unwrap_or(""), path.next(), req.method()) { + ("principal", None, &Method::POST) => { + // Create principal + if let Some(principal) = + body.and_then(|body| serde_json::from_slice::>(&body).ok()) + { + match self.store.create_account(principal).await { + Ok(account_id) => JsonResponse::new(json!({ + "accountId": account_id, + "status": "success", + })) + .into_http_response(), + Err(err) => map_directory_error(err), + } + } else { + RequestError::blank( + StatusCode::BAD_REQUEST.as_u16(), + "Invalid parameters", + "Failed to deserialize principal object", + ) + .into_http_response() + } + } + ("principal", None, &Method::GET) => { + // List principal ids + let mut from_key = None; + let mut limit: usize = 0; + + if let Some(query) = req.uri().query() { + for (key, value) in form_urlencoded::parse(query.as_bytes()) { + match key.as_ref() { + "limit" => { + limit = value.parse().unwrap_or_default(); + } + "from" => { + from_key = value.into(); + } + _ => {} + } + } + } + + match self.store.list_accounts(from_key.as_deref(), limit).await { + Ok(accounts) => JsonResponse::new(json!({ + "status": "success", + "data": accounts, + })) + .into_http_response(), + Err(err) => map_directory_error(err), + } + } + ("principal", Some(name), method) => { + // Fetch, update or delete principal + let account_id = match self.store.get_account_id(name).await { + Ok(Some(account_id)) => account_id, + Ok(None) => { + return RequestError::blank( + StatusCode::NOT_FOUND.as_u16(), + "Not found", + "Account not found.", + ) + .into_http_response(); + } + Err(err) => { + return map_directory_error(err); + } + }; + + match *method { + Method::GET => { + let result = match self.store.query(QueryBy::Id(account_id)).await { + Ok(Some(principal)) => self.store.map_group_ids(principal).await, + Ok(None) => { + return RequestError::blank( + StatusCode::NOT_FOUND.as_u16(), + "Not found", + "Account not found.", + ) + .into_http_response() + } + Err(err) => Err(err), + }; + + match result { + Ok(principal) => { + // Obtain quota usage + let mut principal = PrincipalResponse::from(principal); + principal.used_quota = + self.get_used_quota(account_id).await.unwrap_or_default() + as u32; + + JsonResponse::new(json!({ + "status": "success", + "data": principal, + })) + .into_http_response() + } + Err(err) => map_directory_error(err), + } + } + Method::DELETE => { + // Remove FTS index + if let Err(err) = self.fts_store.remove_all(account_id).await { + tracing::warn!( + context = "fts", + event = "error", + reason = ?err, + "Failed to remove FTS index" + ); + return RequestError::blank( + StatusCode::INTERNAL_SERVER_ERROR.as_u16(), + "Failed to remove FTS index", + "Contact the administrator if this problem persists", + ) + .into_http_response(); + } + + // Delete account + match self.store.delete_account(QueryBy::Id(account_id)).await { + Ok(_) => JsonResponse::new(json!({ + "status": "success", + })) + .into_http_response(), + Err(err) => map_directory_error(err), + } + } + Method::PUT => { + if let Some(changes) = body.and_then(|body| { + serde_json::from_slice::>(&body).ok() + }) { + match self + .store + .update_account(QueryBy::Id(account_id), changes) + .await + { + Ok(account_id) => JsonResponse::new(json!({ + "accountId": account_id, + "status": "success", + })) + .into_http_response(), + Err(err) => map_directory_error(err), + } + } else { + RequestError::blank( + StatusCode::BAD_REQUEST.as_u16(), + "Invalid parameters", + "Failed to deserialize modify request", + ) + .into_http_response() + } + } + _ => RequestError::not_found().into_http_response(), + } + } + ("store", Some("purge"), &Method::GET) => { + match self.store.purge_blobs(self.blob_store.clone()).await { + Ok(_) => match self.store.purge_bitmaps().await { + Ok(_) => JsonResponse::new(json!({ + "status": "success", + })) + .into_http_response(), + Err(err) => RequestError::blank( + StatusCode::INTERNAL_SERVER_ERROR.as_u16(), + "Purge database failed", + err.to_string(), + ) + .into_http_response(), + }, + Err(err) => RequestError::blank( + StatusCode::INTERNAL_SERVER_ERROR.as_u16(), + "Purge blob failed", + err.to_string(), + ) + .into_http_response(), + } + } + (path_1 @ ("queue" | "report"), Some(path_2), &Method::GET) => { + self.smtp + .handle_manage_request(req.uri(), req.method(), path_1, path_2) + .await + } + _ => RequestError::not_found().into_http_response(), + } + } +} + +fn map_directory_error(err: DirectoryError) -> hyper::Response> { + match err { + DirectoryError::Management(err) => { + let response = match err { + ManagementError::MissingField(details) => json!({ + "status": "missingField", + "details": details, + }), + ManagementError::NotUniqueField(details) => json!({ + "status": "notUniqueField", + "details": details, + }), + ManagementError::NotFound(details) => json!({ + "status": "notFound", + "details": details, + }), + }; + JsonResponse::new(response).into_http_response() + } + DirectoryError::Unsupported => JsonResponse::new(json!({ + "status": "unsupported", + "details": "Requested action is unsupported", + })) + .into_http_response(), + err => { + tracing::warn!( + context = "directory", + event = "error", + reason = ?err, + "Directory error" + ); + + RequestError::blank( + StatusCode::INTERNAL_SERVER_ERROR.as_u16(), + "Database error", + "Contact the administrator if this problem persists", + ) + .into_http_response() + } + } +} + +impl From> for PrincipalResponse { + fn from(principal: Principal) -> 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, + used_quota: 0, + } + } +} diff --git a/crates/jmap/src/api/http.rs b/crates/jmap/src/api/http.rs index b062b4eb..da4928c0 100644 --- a/crates/jmap/src/api/http.rs +++ b/crates/jmap/src/api/http.rs @@ -38,7 +38,6 @@ use jmap_proto::{ response::Response, types::{blob::BlobId, id::Id}, }; -use serde_json::Value; use tokio::{ io::{AsyncRead, AsyncWrite}, net::TcpStream, @@ -274,138 +273,17 @@ pub async fn parse_jmap_request( _ => (), } } - "admin" => { // Make sure the user is a superuser - match jmap.authenticate_headers(&req, remote_ip).await { - Ok(Some((_, access_token))) if access_token.is_super_user() => (), + let body = match jmap.authenticate_headers(&req, remote_ip).await { + Ok(Some((_, access_token))) if access_token.is_super_user() => { + fetch_body(&mut req, 8192, &access_token).await + } Ok(_) => return RequestError::unauthorized().into_http_response(), Err(err) => return err.into_http_response(), - } + }; - match ( - path.next().unwrap_or(""), - path.next().unwrap_or(""), - req.method(), - ) { - ("account", "delete", &Method::GET) => { - let todo = true; - /* - - // Remove FTS index - self.fts_store.remove_all(principal.id).await?; - */ - todo!() - /*return if let Some(account_name) = path.next() { - if let Ok(Some(account_id)) = jmap.try_get_account_id(account_name).await { - match jmap.delete_account(account_name, account_id).await { - Ok(_) => JsonResponse::new(Value::String("success".into())) - .into_http_response(), - Err(err) => RequestError::blank( - StatusCode::INTERNAL_SERVER_ERROR.as_u16(), - "Account deletion failed", - err.to_string(), - ) - .into_http_response(), - } - } else { - RequestError::blank( - StatusCode::NOT_FOUND.as_u16(), - "Not found", - "Account not found.", - ) - .into_http_response() - } - } else { - RequestError::blank( - StatusCode::BAD_REQUEST.as_u16(), - "Invalid parameters", - "Expected account name", - ) - .into_http_response() - };*/ - } - ("account", "rename", &Method::GET) => { - todo!() - /*return if let (Some(account_name), Some(new_account_name)) = - (path.next(), path.next()) - { - match ( - jmap.try_get_account_id(account_name).await, - jmap.try_get_account_id(new_account_name).await, - ) { - (Ok(Some(account_id)), Ok(None)) => { - match jmap - .rename_account(new_account_name, account_name, account_id) - .await - { - Ok(_) => JsonResponse::new(Value::String("success".into())) - .into_http_response(), - Err(err) => RequestError::blank( - StatusCode::INTERNAL_SERVER_ERROR.as_u16(), - "Account rename failed", - err.to_string(), - ) - .into_http_response(), - } - } - (Ok(None), _) => RequestError::blank( - StatusCode::NOT_FOUND.as_u16(), - "Not found", - "Account not found.", - ) - .into_http_response(), - (_, Ok(Some(_))) => RequestError::blank( - StatusCode::BAD_REQUEST.as_u16(), - "Invalid parameters", - "New account name already exists.", - ) - .into_http_response(), - _ => RequestError::internal_server_error().into_http_response(), - } - } else { - RequestError::blank( - StatusCode::BAD_REQUEST.as_u16(), - "Invalid parameters", - "Expected old and new account names", - ) - .into_http_response() - };*/ - } - ("blob", "purge", &Method::GET) => { - return match jmap.store.purge_blobs(jmap.blob_store.clone()).await { - Ok(_) => { - JsonResponse::new(Value::String("success".into())).into_http_response() - } - Err(err) => RequestError::blank( - StatusCode::INTERNAL_SERVER_ERROR.as_u16(), - "Purge blob failed", - err.to_string(), - ) - .into_http_response(), - }; - } - ("db", "purge", &Method::GET) => { - return match jmap.store.purge_bitmaps().await { - Ok(_) => { - JsonResponse::new(Value::String("success".into())).into_http_response() - } - Err(err) => RequestError::blank( - StatusCode::INTERNAL_SERVER_ERROR.as_u16(), - "Purge database failed", - err.to_string(), - ) - .into_http_response(), - }; - } - (path_1 @ ("queue" | "report"), path_2, &Method::GET) => { - return jmap - .smtp - .handle_manage_request(req.uri(), req.method(), path_1, path_2) - .await; - } - _ => (), - } + return jmap.handle_manage_request(&req, body).await; } _ => (), } diff --git a/crates/jmap/src/api/mod.rs b/crates/jmap/src/api/mod.rs index 74dc6712..927225d6 100644 --- a/crates/jmap/src/api/mod.rs +++ b/crates/jmap/src/api/mod.rs @@ -30,6 +30,7 @@ use utils::map::vec_map::VecMap; use crate::JMAP; +pub mod admin; pub mod config; pub mod event_source; pub mod http; diff --git a/crates/jmap/src/api/session.rs b/crates/jmap/src/api/session.rs index 5797493b..a1c2e74f 100644 --- a/crates/jmap/src/api/session.rs +++ b/crates/jmap/src/api/session.rs @@ -214,7 +214,7 @@ impl JMAP { session.add_account( (*id).into(), self.directory - .query(QueryBy::id(*id).with_store(&self.store)) + .query(QueryBy::Id(*id)) .await .unwrap_or_default() .map(|p| p.name) diff --git a/crates/jmap/src/auth/acl.rs b/crates/jmap/src/auth/acl.rs index 9585021f..eee51652 100644 --- a/crates/jmap/src/auth/acl.rs +++ b/crates/jmap/src/auth/acl.rs @@ -379,7 +379,7 @@ impl JMAP { { if let Some(principal) = self .directory - .query(QueryBy::id(id.document_id()).with_store(&self.store)) + .query(QueryBy::Id(id.document_id())) .await .unwrap_or_default() { @@ -452,11 +452,7 @@ impl JMAP { async fn map_acl_accounts(&self, mut acl_set: Vec) -> Result, SetError> { for item in &mut acl_set { if let Value::Text(account_name) = item { - match self - .directory - .query(QueryBy::name(account_name).with_store(&self.store)) - .await - { + match self.directory.query(QueryBy::Name(account_name)).await { Ok(Some(principal)) => { *item = Value::Id(principal.id.into()); } diff --git a/crates/jmap/src/auth/authenticate.rs b/crates/jmap/src/auth/authenticate.rs index 96ad2d58..5dbfeca3 100644 --- a/crates/jmap/src/auth/authenticate.rs +++ b/crates/jmap/src/auth/authenticate.rs @@ -174,13 +174,10 @@ impl JMAP { ) -> Option { match self .directory - .query( - QueryBy::credentials(&Credentials::Plain { - username: username.to_string(), - secret: secret.to_string(), - }) - .with_store(&self.store), - ) + .query(QueryBy::Credentials(&Credentials::Plain { + username: username.to_string(), + secret: secret.to_string(), + })) .await { Ok(Some(mut principal)) => { @@ -201,10 +198,7 @@ impl JMAP { pub async fn get_access_token(&self, account_id: u32) -> Option { // Create access token self.update_access_token(AccessToken::new( - self.directory - .query(QueryBy::id(account_id).with_store(&self.store)) - .await - .ok()??, + self.directory.query(QueryBy::Id(account_id)).await.ok()??, )) .await } diff --git a/crates/jmap/src/auth/mod.rs b/crates/jmap/src/auth/mod.rs index ac30addd..eae046c0 100644 --- a/crates/jmap/src/auth/mod.rs +++ b/crates/jmap/src/auth/mod.rs @@ -56,7 +56,7 @@ pub struct AccessToken { } impl AccessToken { - pub fn new(principal: Principal) -> Self { + pub fn new(principal: Principal) -> Self { Self { primary_id: principal.id, member_of: principal.member_of, diff --git a/crates/jmap/src/auth/oauth/token.rs b/crates/jmap/src/auth/oauth/token.rs index fe42e1d6..c963f04a 100644 --- a/crates/jmap/src/auth/oauth/token.rs +++ b/crates/jmap/src/auth/oauth/token.rs @@ -182,7 +182,7 @@ impl JMAP { ) -> Result { let password_hash = self .directory - .query(QueryBy::id(account_id).with_store(&self.store)) + .query(QueryBy::Id(account_id)) .await .map_err(|_| "Temporary lookup error")? .ok_or("Account no longer exists")? @@ -301,7 +301,7 @@ impl JMAP { let password_hash = self .directory - .query(QueryBy::id(account_id).with_store(&self.store)) + .query(QueryBy::Id(account_id)) .await .map_err(|_| "Temporary lookup error")? .ok_or("Account no longer exists")? diff --git a/crates/jmap/src/identity/set.rs b/crates/jmap/src/identity/set.rs index 6f2099f4..4e0961c7 100644 --- a/crates/jmap/src/identity/set.rs +++ b/crates/jmap/src/identity/set.rs @@ -75,7 +75,7 @@ impl JMAP { if let Value::Text(email) = identity.get(&Property::Email) { if !self .directory - .query(QueryBy::id(account_id).with_store(&self.store)) + .query(QueryBy::Id(account_id)) .await .unwrap_or_default() .unwrap_or_default() diff --git a/crates/jmap/src/lib.rs b/crates/jmap/src/lib.rs index b0d70cec..ebfd006d 100644 --- a/crates/jmap/src/lib.rs +++ b/crates/jmap/src/lib.rs @@ -599,7 +599,7 @@ impl JMAP { access_token.quota as i64 } else { self.directory - .query(QueryBy::id(account_id).with_store(&self.store)) + .query(QueryBy::Id(account_id)) .await .map_err(|err| { tracing::error!( diff --git a/crates/jmap/src/principal/get.rs b/crates/jmap/src/principal/get.rs index 1651bcdd..2e5c8f49 100644 --- a/crates/jmap/src/principal/get.rs +++ b/crates/jmap/src/principal/get.rs @@ -70,7 +70,7 @@ impl JMAP { // Obtain the principal let principal = if let Some(principal) = self .directory - .query(QueryBy::id(id.document_id()).with_store(&self.store)) + .query(QueryBy::Id(id.document_id())) .await .map_err(|_| MethodError::ServerPartialFail)? { diff --git a/crates/jmap/src/principal/query.rs b/crates/jmap/src/principal/query.rs index 7578adea..15933929 100644 --- a/crates/jmap/src/principal/query.rs +++ b/crates/jmap/src/principal/query.rs @@ -49,7 +49,7 @@ impl JMAP { Filter::Name(name) => { if let Some(principal) = self .directory - .query(QueryBy::name(name.as_str()).with_store(&self.store)) + .query(QueryBy::Name(name.as_str())) .await .map_err(|_| MethodError::ServerPartialFail)? { @@ -68,7 +68,7 @@ impl JMAP { let mut ids = RoaringBitmap::new(); for id in self .directory - .email_to_ids(&email, &self.store) + .email_to_ids(&email) .await .map_err(|_| MethodError::ServerPartialFail)? { diff --git a/crates/jmap/src/services/ingest.rs b/crates/jmap/src/services/ingest.rs index fd140165..19e47d2b 100644 --- a/crates/jmap/src/services/ingest.rs +++ b/crates/jmap/src/services/ingest.rs @@ -47,11 +47,7 @@ impl JMAP { let mut recipients = Vec::with_capacity(message.recipients.len()); let mut deliver_names = AHashMap::with_capacity(message.recipients.len()); for rcpt in &message.recipients { - let uids = self - .directory - .email_to_ids(rcpt, &self.store) - .await - .unwrap_or_default(); + let uids = self.directory.email_to_ids(rcpt).await.unwrap_or_default(); for uid in &uids { deliver_names.insert(*uid, (DeliveryResult::Success, rcpt)); } @@ -73,11 +69,7 @@ impl JMAP { .await } Ok(None) => { - let account_quota = match self - .directory - .query(QueryBy::id(*uid).with_store(&self.store)) - .await - { + let account_quota = match self.directory.query(QueryBy::Id(*uid)).await { Ok(Some(p)) => p.quota as i64, Ok(None) => 0, Err(_) => { diff --git a/crates/jmap/src/sieve/ingest.rs b/crates/jmap/src/sieve/ingest.rs index 89e84248..4cd5de48 100644 --- a/crates/jmap/src/sieve/ingest.rs +++ b/crates/jmap/src/sieve/ingest.rs @@ -78,11 +78,7 @@ impl JMAP { let mut instance = self.sieve_runtime.filter_parsed(message); // Set account name and obtain quota - let (account_quota, mail_from) = match self - .directory - .query(QueryBy::id(account_id).with_store(&self.store)) - .await - { + let (account_quota, mail_from) = match self.directory.query(QueryBy::Id(account_id)).await { Ok(Some(p)) => { instance.set_user_full_name(p.description().unwrap_or_else(|| p.name())); (p.quota as i64, p.emails.into_iter().next()) diff --git a/crates/main/src/main.rs b/crates/main/src/main.rs index 7f33c389..dec0dfc6 100644 --- a/crates/main/src/main.rs +++ b/crates/main/src/main.rs @@ -48,7 +48,7 @@ async fn main() -> std::io::Result<()> { let servers = config.parse_servers().failed("Invalid configuration"); let stores = config.parse_stores().await.failed("Invalid configuration"); let directory = config - .parse_directory(&stores) + .parse_directory(&stores, config.value("jmap.store.data")) .failed("Invalid configuration"); let schedulers = config .parse_purge_schedules( diff --git a/crates/smtp/src/core/management.rs b/crates/smtp/src/core/management.rs index a9e0eb27..4bb8a782 100644 --- a/crates/smtp/src/core/management.rs +++ b/crates/smtp/src/core/management.rs @@ -255,7 +255,7 @@ impl SMTP { .queue .config .management_lookup - .query(QueryBy::credentials(&Credentials::Plain { + .query(QueryBy::Credentials(&Credentials::Plain { username, secret, })) diff --git a/crates/smtp/src/inbound/auth.rs b/crates/smtp/src/inbound/auth.rs index a3a6afee..c3ceca0e 100644 --- a/crates/smtp/src/inbound/auth.rs +++ b/crates/smtp/src/inbound/auth.rs @@ -182,7 +182,7 @@ impl Session { | Credentials::OAuthBearer { token: username } => username.to_string(), }; if let Ok(is_authenticated) = lookup - .query(QueryBy::credentials(&credentials)) + .query(QueryBy::Credentials(&credentials)) .await .map(|r| r.is_some()) { diff --git a/crates/store/src/dispatch/lookup.rs b/crates/store/src/dispatch/lookup.rs index 9587d9f2..78039676 100644 --- a/crates/store/src/dispatch/lookup.rs +++ b/crates/store/src/dispatch/lookup.rs @@ -31,6 +31,7 @@ use crate::{ }; impl LookupStore { + #[allow(unreachable_patterns)] pub async fn query( &self, query: &str, diff --git a/crates/store/src/dispatch/store.rs b/crates/store/src/dispatch/store.rs index 64bb7f0e..02801437 100644 --- a/crates/store/src/dispatch/store.rs +++ b/crates/store/src/dispatch/store.rs @@ -202,7 +202,7 @@ impl Store { for (from_class, to_class) in [ (ValueClass::Acl(account_id), ValueClass::Acl(account_id + 1)), (ValueClass::ReservedId, ValueClass::ReservedId), - (ValueClass::Property(0), ValueClass::Property(u8::MAX)), + (ValueClass::Property(0), ValueClass::Property(0)), (ValueClass::TermIndex, ValueClass::TermIndex), ] { self.delete_range( @@ -360,6 +360,7 @@ impl Store { } #[cfg(feature = "test_mode")] + #[allow(unused_variables)] pub async fn assert_is_empty(&self, blob_store: crate::BlobStore) { use crate::{SUBSPACE_BLOBS, SUBSPACE_BLOB_DATA, SUBSPACE_COUNTERS}; diff --git a/crates/store/src/lib.rs b/crates/store/src/lib.rs index adbf59ab..9e747c6a 100644 --- a/crates/store/src/lib.rs +++ b/crates/store/src/lib.rs @@ -151,7 +151,7 @@ impl Default for BlobClass { } } -#[derive(Debug)] +#[derive(Debug, PartialEq, Eq)] pub enum Error { InternalError(String), AssertValueFailed, diff --git a/crates/store/src/write/key.rs b/crates/store/src/write/key.rs index 5cace1ad..7a53e3cd 100644 --- a/crates/store/src/write/key.rs +++ b/crates/store/src/write/key.rs @@ -259,7 +259,8 @@ impl + Sync + Send> Key for ValueKey { DirectoryValue::NameToId(name) => serializer.write(6u8).write(name.as_slice()), DirectoryValue::EmailToId(email) => serializer.write(7u8).write(email.as_slice()), DirectoryValue::Principal(uid) => serializer.write(8u8).write_leb128(*uid), - DirectoryValue::UsedQuota(uid) => serializer.write(9u8).write_leb128(*uid), + DirectoryValue::Domain(name) => serializer.write(9u8).write(name.as_slice()), + DirectoryValue::UsedQuota(uid) => serializer.write(10u8).write_leb128(*uid), }, } .finalize() @@ -425,7 +426,9 @@ impl ValueClass { ValueClass::Acl(_) => U32_LEN * 3 + 2, ValueClass::Key(v) => v.len(), ValueClass::Directory(d) => match d { - DirectoryValue::NameToId(v) | DirectoryValue::EmailToId(v) => v.len(), + DirectoryValue::NameToId(v) + | DirectoryValue::EmailToId(v) + | DirectoryValue::Domain(v) => v.len(), DirectoryValue::Principal(_) | DirectoryValue::UsedQuota(_) => U32_LEN, }, ValueClass::IndexEmail { .. } => U64_LEN * 2, diff --git a/crates/store/src/write/mod.rs b/crates/store/src/write/mod.rs index 132596f1..2f93bf5e 100644 --- a/crates/store/src/write/mod.rs +++ b/crates/store/src/write/mod.rs @@ -149,6 +149,7 @@ pub enum ValueClass { pub enum DirectoryValue { NameToId(Vec), EmailToId(Vec), + Domain(Vec), Principal(u32), UsedQuota(u32), } @@ -282,7 +283,7 @@ pub trait DeserializeFrom: Sized { fn deserialize_from(bytes: &mut Iter<'_, u8>) -> Option; } -impl Serialize for Vec { +impl Serialize for &Vec { fn serialize(self) -> Vec { let mut bytes = Vec::with_capacity(self.len() * 4); bytes.push_leb128(self.len()); @@ -293,6 +294,12 @@ impl Serialize for Vec { } } +impl Serialize for Vec { + fn serialize(self) -> Vec { + (&self).serialize() + } +} + impl SerializeInto for String { fn serialize_into(&self, buf: &mut Vec) { buf.push_leb128(self.len()); @@ -476,6 +483,12 @@ impl ToBitmaps for () { } } +impl Deserialize for () { + fn deserialize(_bytes: &[u8]) -> crate::Result { + Ok(()) + } +} + pub trait IntoOperations { fn build(self, batch: &mut BatchBuilder); } diff --git a/tests/src/directory/imap.rs b/tests/src/directory/imap.rs index 3367b184..36a8b4d8 100644 --- a/tests/src/directory/imap.rs +++ b/tests/src/directory/imap.rs @@ -35,7 +35,7 @@ use tokio_rustls::TlsAcceptor; use utils::listener::limiter::{ConcurrencyLimiter, InFlight}; -use crate::directory::{parse_config, Item, LookupResult}; +use crate::directory::{DirectoryTest, Item, LookupResult}; use super::dummy_tls_acceptor; @@ -54,7 +54,7 @@ async fn imap_directory() { tokio::time::sleep(std::time::Duration::from_millis(100)).await; // Obtain directory handle - let mut config = parse_config().await; + let mut config = DirectoryTest::new(None).await; let handle = config.directories.directories.remove("imap").unwrap(); // Basic lookup @@ -79,7 +79,7 @@ async fn imap_directory() { assert_eq!( &LookupResult::from( handle - .query(QueryBy::credentials(item.as_credentials())) + .query(QueryBy::Credentials(item.as_credentials())) .await .unwrap() .is_some() @@ -99,7 +99,7 @@ async fn imap_directory() { tokio::spawn(async move { LookupResult::from( handle - .query(QueryBy::credentials(item.as_credentials())) + .query(QueryBy::Credentials(item.as_credentials())) .await .unwrap() .is_some(), diff --git a/tests/src/directory/internal.rs b/tests/src/directory/internal.rs new file mode 100644 index 00000000..37e5e7b8 --- /dev/null +++ b/tests/src/directory/internal.rs @@ -0,0 +1,590 @@ +/* + * Copyright (c) 2023 Stalwart Labs Ltd. + * + * This file is part of Stalwart Mail Server. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use directory::{ + backend::internal::{manage::ManageDirectory, PrincipalField, PrincipalUpdate, PrincipalValue}, + Directory, DirectoryError, ManagementError, Principal, QueryBy, Type, +}; +use jmap_proto::types::collection::Collection; +use mail_send::Credentials; +use store::{ + roaring::RoaringBitmap, + write::{BatchBuilder, BitmapClass, ValueClass}, + BitmapKey, ValueKey, +}; + +use crate::directory::DirectoryTest; + +#[tokio::test] +async fn internal_directory() { + let config = DirectoryTest::new(None).await; + + for (store_id, store) in config.stores.stores { + println!("Testing internal directory with store {:?}", store_id); + store.destroy().await; + + // A principal without name should fail + assert_eq!( + store.create_account(Principal::default()).await, + Err(DirectoryError::Management(ManagementError::MissingField( + PrincipalField::Name + ))) + ); + + // Basic account creation + assert_eq!( + store + .create_account(Principal { + name: "john".to_string(), + description: Some("John Doe".to_string()), + secrets: vec!["secret".to_string(), "secret2".to_string()], + ..Default::default() + }) + .await, + Ok(0) + ); + + // Two accounts with the same name should fail + assert_eq!( + store + .create_account(Principal { + name: "john".to_string(), + ..Default::default() + }) + .await, + Err(DirectoryError::Management(ManagementError::NotUniqueField( + PrincipalField::Name + ))) + ); + + // An account using a non-existent domain should fail + assert_eq!( + store + .create_account(Principal { + name: "jane".to_string(), + emails: vec!["jane@example.org".to_string()], + ..Default::default() + }) + .await, + Err(DirectoryError::Management(ManagementError::NotFound( + "example.org".to_string() + ))) + ); + + // Create a domain name + assert_eq!(store.create_domain("example.org").await, Ok(())); + assert!(store.is_local_domain("example.org").await.unwrap()); + assert!(!store.is_local_domain("otherdomain.org").await.unwrap()); + + // Add an email address + assert_eq!( + store + .update_account( + QueryBy::Name("john"), + vec![PrincipalUpdate::add_item( + PrincipalField::Emails, + PrincipalValue::String("john@example.org".to_string()), + )], + ) + .await, + Ok(()) + ); + assert!(store.rcpt("john@example.org").await.unwrap()); + assert_eq!( + store.email_to_ids("john@example.org").await.unwrap(), + vec![0] + ); + + // Using non-existent domain should fail + assert_eq!( + store + .update_account( + QueryBy::Name("john"), + vec![PrincipalUpdate::add_item( + PrincipalField::Emails, + PrincipalValue::String("john@otherdomain.org".to_string()), + )], + ) + .await, + Err(DirectoryError::Management(ManagementError::NotFound( + "otherdomain.org".to_string() + ))) + ); + + // Create an account with an email address + assert_eq!( + store + .create_account(Principal { + 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() + }) + .await, + Ok(1) + ); + assert!(store.rcpt("jane@example.org").await.unwrap()); + assert!(!store.rcpt("jane@otherdomain.org").await.unwrap()); + assert_eq!( + store.email_to_ids("jane@example.org").await.unwrap(), + vec![1] + ); + assert_eq!(store.vrfy("jane").await.unwrap(), vec!["jane@example.org"]); + assert_eq!( + store + .query(QueryBy::Credentials(&Credentials::new( + "jane".to_string(), + "my_secret".to_string() + ))) + .await + .unwrap(), + Some(Principal { + name: "jane".to_string(), + description: Some("Jane Doe".to_string()), + emails: vec!["jane@example.org".to_string()], + secrets: vec!["my_secret".to_string(), "my_secret2".to_string()], + quota: 123, + ..Default::default() + }) + ); + assert_eq!( + store + .query(QueryBy::Credentials(&Credentials::new( + "jane".to_string(), + "wrong_password".to_string() + ))) + .await + .unwrap(), + None + ); + + // Duplicate email address should fail + assert_eq!( + store + .create_account(Principal { + name: "janeth".to_string(), + description: Some("Janeth Doe".to_string()), + emails: vec!["jane@example.org".to_string()], + ..Default::default() + }) + .await, + Err(DirectoryError::Management(ManagementError::NotUniqueField( + PrincipalField::Emails + ))) + ); + + // Create a mailing list + assert_eq!( + store + .create_account(Principal { + name: "list".to_string(), + typ: Type::List, + emails: vec!["list@example.org".to_string()], + member_of: vec!["john".to_string(), "jane".to_string()], + ..Default::default() + }) + .await, + Ok(2) + ); + assert!(store.rcpt("list@example.org").await.unwrap()); + assert_eq!( + store.email_to_ids("list@example.org").await.unwrap(), + vec![0, 1] + ); + assert_eq!( + store + .map_group_ids(store.query(QueryBy::Name("list")).await.unwrap().unwrap()) + .await + .unwrap(), + Principal { + name: "list".to_string(), + typ: Type::List, + emails: vec!["list@example.org".to_string()], + member_of: vec!["john".to_string(), "jane".to_string()], + ..Default::default() + } + ); + assert_eq!( + store.expn("list@example.org").await.unwrap(), + vec!["john@example.org", "jane@example.org"] + ); + + // Create groups + assert_eq!( + store + .create_account(Principal { + name: "sales".to_string(), + description: Some("Sales Team".to_string()), + typ: Type::Group, + ..Default::default() + }) + .await, + Ok(3) + ); + assert_eq!( + store + .create_account(Principal { + name: "support".to_string(), + description: Some("Support Team".to_string()), + typ: Type::Group, + ..Default::default() + }) + .await, + Ok(4) + ); + + // Add John to the Sales and Support groups + assert_eq!( + store + .update_account( + QueryBy::Name("john"), + vec![ + PrincipalUpdate::add_item( + PrincipalField::MemberOf, + PrincipalValue::String("sales".to_string()), + ), + PrincipalUpdate::add_item( + PrincipalField::MemberOf, + PrincipalValue::String("support".to_string()), + ) + ], + ) + .await, + Ok(()) + ); + assert_eq!( + store + .map_group_ids(store.query(QueryBy::Name("john")).await.unwrap().unwrap()) + .await + .unwrap(), + Principal { + name: "john".to_string(), + description: Some("John Doe".to_string()), + secrets: vec!["secret".to_string(), "secret2".to_string()], + emails: vec!["john@example.org".to_string()], + member_of: vec!["sales".to_string(), "support".to_string()], + ..Default::default() + } + ); + + // Adding a non-existent user should fail + assert_eq!( + store + .update_account( + QueryBy::Name("john"), + vec![PrincipalUpdate::add_item( + PrincipalField::MemberOf, + PrincipalValue::String("accounting".to_string()), + )], + ) + .await, + Err(DirectoryError::Management(ManagementError::NotFound( + "accounting".to_string() + ))) + ); + + // Remove a member from a group + assert_eq!( + store + .update_account( + QueryBy::Name("john"), + vec![PrincipalUpdate::remove_item( + PrincipalField::MemberOf, + PrincipalValue::String("support".to_string()), + )], + ) + .await, + Ok(()) + ); + assert_eq!( + store + .map_group_ids(store.query(QueryBy::Name("john")).await.unwrap().unwrap()) + .await + .unwrap(), + Principal { + name: "john".to_string(), + description: Some("John Doe".to_string()), + secrets: vec!["secret".to_string(), "secret2".to_string()], + emails: vec!["john@example.org".to_string()], + member_of: vec!["sales".to_string()], + ..Default::default() + } + ); + + // Update multiple fields + assert_eq!( + store + .update_account( + QueryBy::Name("john"), + vec![ + PrincipalUpdate::set( + PrincipalField::Name, + PrincipalValue::String("john.doe".to_string()) + ), + PrincipalUpdate::set( + PrincipalField::Description, + PrincipalValue::String("Johnny Doe".to_string()) + ), + PrincipalUpdate::set( + PrincipalField::Secrets, + PrincipalValue::StringList(vec!["12345".to_string()]) + ), + PrincipalUpdate::set(PrincipalField::Quota, PrincipalValue::Integer(1024)), + PrincipalUpdate::set( + PrincipalField::Type, + PrincipalValue::Type(Type::Superuser) + ), + PrincipalUpdate::remove_item( + PrincipalField::Emails, + PrincipalValue::String("john@example.org".to_string()), + ), + PrincipalUpdate::add_item( + PrincipalField::Emails, + PrincipalValue::String("john.doe@example.org".to_string()), + ) + ], + ) + .await, + Ok(()) + ); + assert_eq!( + store + .map_group_ids( + store + .query(QueryBy::Name("john.doe")) + .await + .unwrap() + .unwrap() + ) + .await + .unwrap(), + Principal { + 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, + member_of: vec!["sales".to_string()], + ..Default::default() + } + ); + assert_eq!(store.get_account_id("john").await.unwrap(), None); + assert!(!store.rcpt("john@example.org").await.unwrap()); + assert!(store.rcpt("john.doe@example.org").await.unwrap()); + + // Remove a member from a mailing list and then add it back + assert_eq!( + store + .update_account( + QueryBy::Name("list"), + vec![PrincipalUpdate::remove_item( + PrincipalField::MemberOf, + PrincipalValue::String("john.doe".to_string()), + )], + ) + .await, + Ok(()) + ); + assert_eq!( + store + .map_group_ids(store.query(QueryBy::Name("list")).await.unwrap().unwrap()) + .await + .unwrap(), + Principal { + name: "list".to_string(), + typ: Type::List, + emails: vec!["list@example.org".to_string()], + member_of: vec!["jane".to_string()], + ..Default::default() + } + ); + assert_eq!( + store + .update_account( + QueryBy::Name("list"), + vec![PrincipalUpdate::add_item( + PrincipalField::MemberOf, + PrincipalValue::String("john.doe".to_string()), + )], + ) + .await, + Ok(()) + ); + assert_eq!( + store + .map_group_ids(store.query(QueryBy::Name("list")).await.unwrap().unwrap()) + .await + .unwrap(), + Principal { + name: "list".to_string(), + typ: Type::List, + emails: vec!["list@example.org".to_string()], + member_of: vec!["jane".to_string(), "john.doe".to_string()], + ..Default::default() + } + ); + + // Field validation + assert_eq!( + store + .update_account( + QueryBy::Name("john.doe"), + vec![PrincipalUpdate::set( + PrincipalField::Name, + PrincipalValue::String("jane".to_string()) + ),], + ) + .await, + Err(DirectoryError::Management(ManagementError::NotUniqueField( + PrincipalField::Name + ))) + ); + assert_eq!( + store + .update_account( + QueryBy::Name("john.doe"), + vec![PrincipalUpdate::add_item( + PrincipalField::Emails, + PrincipalValue::String("jane@example.org".to_string()) + ),], + ) + .await, + Err(DirectoryError::Management(ManagementError::NotUniqueField( + PrincipalField::Emails + ))) + ); + + // List accounts + assert_eq!( + store.list_accounts(None, 0).await.unwrap(), + vec!["jane", "john.doe", "list", "sales", "support"] + ); + assert_eq!( + store.list_accounts("john".into(), 2).await.unwrap(), + vec!["john.doe", "list"] + ); + + // Write records on John's and Jane's accounts + for account_id in [0, 1] { + let document_id = store + .assign_document_id(account_id, Collection::Email) + .await + .unwrap(); + store + .write( + BatchBuilder::new() + .with_account_id(account_id) + .with_collection(Collection::Email) + .create_document(document_id) + .set(ValueClass::Property(0), "hello".as_bytes()) + .build_batch(), + ) + .await + .unwrap(); + assert_eq!( + store + .get_value::(ValueKey { + account_id, + collection: Collection::Email.into(), + document_id, + class: ValueClass::Property(0) + }) + .await + .unwrap(), + Some("hello".to_string()) + ); + } + + // Delete John's account and make sure his records are gone + store.delete_account(QueryBy::Id(0)).await.unwrap(); + assert_eq!(store.get_account_id("john.doe").await.unwrap(), None); + assert_eq!( + store.email_to_ids("john.doe@example.org").await.unwrap(), + Vec::::new() + ); + assert!(!store.rcpt("john.doe@example.org").await.unwrap()); + assert_eq!( + store.list_accounts(None, 0).await.unwrap(), + vec!["jane", "list", "sales", "support"] + ); + assert_eq!( + store + .get_bitmap(BitmapKey { + account_id: 0, + collection: Collection::Email.into(), + class: BitmapClass::DocumentIds, + block_num: 0 + }) + .await + .unwrap(), + None + ); + assert_eq!( + store + .get_value::(ValueKey { + account_id: 0, + collection: Collection::Email.into(), + document_id: 0, + class: ValueClass::Property(0) + }) + .await + .unwrap(), + None + ); + + // Make sure Jane's records are still there + assert_eq!(store.get_account_id("jane").await.unwrap(), Some(1)); + assert_eq!( + store.email_to_ids("jane@example.org").await.unwrap(), + vec![1] + ); + assert!(store.rcpt("jane@example.org").await.unwrap()); + assert_eq!( + store + .get_bitmap(BitmapKey { + account_id: 1, + collection: Collection::Email.into(), + class: BitmapClass::DocumentIds, + block_num: 0 + }) + .await + .unwrap(), + Some(RoaringBitmap::from_sorted_iter([0]).unwrap()) + ); + assert_eq!( + store + .get_value::(ValueKey { + account_id: 1, + collection: Collection::Email.into(), + document_id: 0, + class: ValueClass::Property(0) + }) + .await + .unwrap(), + Some("hello".to_string()) + ); + } +} diff --git a/tests/src/directory/ldap.rs b/tests/src/directory/ldap.rs index 1b70b863..b2fa7c60 100644 --- a/tests/src/directory/ldap.rs +++ b/tests/src/directory/ldap.rs @@ -26,7 +26,7 @@ use std::fmt::Debug; use directory::{backend::internal::manage::ManageDirectory, Principal, QueryBy, Type}; use mail_send::Credentials; -use crate::directory::{map_account_ids, parse_config, IntoSortedPrincipal}; +use crate::directory::{map_account_ids, DirectoryTest, IntoSortedPrincipal}; #[tokio::test] async fn ldap_directory() { @@ -39,20 +39,17 @@ async fn ldap_directory() { .unwrap();*/ // Obtain directory handle - let mut config = parse_config().await; + let mut config = DirectoryTest::new("sqlite".into()).await; let handle = config.directories.directories.remove("ldap").unwrap(); let base_store = config.stores.stores.get("sqlite").unwrap(); // Test authentication assert_eq!( handle - .query( - QueryBy::credentials(&Credentials::Plain { - username: "john".to_string(), - secret: "12345".to_string() - }) - .with_store(base_store) - ) + .query(QueryBy::Credentials(&Credentials::Plain { + username: "john".to_string(), + secret: "12345".to_string() + })) .await .unwrap() .unwrap() @@ -74,13 +71,10 @@ async fn ldap_directory() { ); assert_eq!( handle - .query( - QueryBy::credentials(&Credentials::Plain { - username: "bill".to_string(), - secret: "password".to_string() - }) - .with_store(base_store) - ) + .query(QueryBy::Credentials(&Credentials::Plain { + username: "bill".to_string(), + secret: "password".to_string() + })) .await .unwrap() .unwrap() @@ -100,13 +94,10 @@ async fn ldap_directory() { .into_sorted() ); assert!(handle - .query( - QueryBy::credentials(&Credentials::Plain { - username: "bill".to_string(), - secret: "invalid".to_string() - }) - .with_store(base_store) - ) + .query(QueryBy::Credentials(&Credentials::Plain { + username: "bill".to_string(), + secret: "invalid".to_string() + })) .await .unwrap() .is_none()); @@ -114,7 +105,7 @@ async fn ldap_directory() { // Get user by name assert_eq!( handle - .query(QueryBy::name("jane").with_store(base_store)) + .query(QueryBy::Name("jane")) .await .unwrap() .unwrap() @@ -134,11 +125,7 @@ async fn ldap_directory() { // Get group by name assert_eq!( - handle - .query(QueryBy::name("sales").with_store(base_store)) - .await - .unwrap() - .unwrap(), + handle.query(QueryBy::Name("sales")).await.unwrap().unwrap(), Principal { id: base_store.get_account_id("sales").await.unwrap().unwrap(), name: "sales".to_string(), @@ -150,45 +137,27 @@ async fn ldap_directory() { // Ids by email compare_sorted( - handle - .email_to_ids("jane@example.org", base_store) - .await - .unwrap(), + handle.email_to_ids("jane@example.org").await.unwrap(), map_account_ids(base_store, vec!["jane"]).await, ); compare_sorted( - handle - .email_to_ids("jane+alias@example.org", base_store) - .await - .unwrap(), + handle.email_to_ids("jane+alias@example.org").await.unwrap(), map_account_ids(base_store, vec!["jane"]).await, ); compare_sorted( - handle - .email_to_ids("info@example.org", base_store) - .await - .unwrap(), + handle.email_to_ids("info@example.org").await.unwrap(), map_account_ids(base_store, vec!["bill", "jane", "john"]).await, ); compare_sorted( - handle - .email_to_ids("info+alias@example.org", base_store) - .await - .unwrap(), + handle.email_to_ids("info+alias@example.org").await.unwrap(), map_account_ids(base_store, vec!["bill", "jane", "john"]).await, ); compare_sorted( - handle - .email_to_ids("unknown@example.org", base_store) - .await - .unwrap(), + handle.email_to_ids("unknown@example.org").await.unwrap(), Vec::::new(), ); assert_eq!( - handle - .email_to_ids("anything@catchall.org", base_store) - .await - .unwrap(), + handle.email_to_ids("anything@catchall.org").await.unwrap(), map_account_ids(base_store, vec!["robert"]).await ); diff --git a/tests/src/directory/mod.rs b/tests/src/directory/mod.rs index c9355760..0530ec06 100644 --- a/tests/src/directory/mod.rs +++ b/tests/src/directory/mod.rs @@ -22,6 +22,7 @@ */ pub mod imap; +pub mod internal; pub mod ldap; pub mod smtp; pub mod sql; @@ -274,16 +275,18 @@ pub struct DirectoryTest { pub temp_dir: TempDir, } -pub async fn parse_config() -> DirectoryTest { - let temp_dir = TempDir::new("directory_tests", true); - let config_file = CONFIG.replace("{TMP}", &temp_dir.path.to_string_lossy()); - let config = utils::config::Config::new(&config_file).unwrap(); - let stores = config.parse_stores().await.unwrap(); +impl DirectoryTest { + pub async fn new(id_store: Option<&str>) -> DirectoryTest { + let temp_dir = TempDir::new("directory_tests", true); + let config_file = CONFIG.replace("{TMP}", &temp_dir.path.to_string_lossy()); + let config = utils::config::Config::new(&config_file).unwrap(); + let stores = config.parse_stores().await.unwrap(); - DirectoryTest { - directories: config.parse_directory(&stores).unwrap(), - stores, - temp_dir, + DirectoryTest { + directories: config.parse_directory(&stores, id_store).unwrap(), + stores, + temp_dir, + } } } @@ -625,7 +628,7 @@ trait IntoSortedPrincipal: Sized { fn into_sorted(self) -> Self; } -impl IntoSortedPrincipal for Principal { +impl IntoSortedPrincipal for Principal { fn into_sorted(mut self) -> Self { self.member_of.sort_unstable(); self.emails.sort_unstable(); diff --git a/tests/src/directory/smtp.rs b/tests/src/directory/smtp.rs index e9dc1953..94ff054a 100644 --- a/tests/src/directory/smtp.rs +++ b/tests/src/directory/smtp.rs @@ -35,7 +35,7 @@ use tokio_rustls::TlsAcceptor; use utils::listener::limiter::{ConcurrencyLimiter, InFlight}; -use crate::directory::{parse_config, Item, LookupResult}; +use crate::directory::{DirectoryTest, Item, LookupResult}; use super::dummy_tls_acceptor; @@ -46,7 +46,7 @@ async fn smtp_directory() { tokio::time::sleep(std::time::Duration::from_millis(100)).await; // Obtain directory handle - let mut config = parse_config().await; + let mut config = DirectoryTest::new(None).await; let handle = config.directories.directories.remove("smtp").unwrap(); // Basic lookup @@ -97,7 +97,7 @@ async fn smtp_directory() { let result: LookupResult = match item { Item::IsAccount(v) => handle.rcpt(v).await.unwrap().into(), Item::Authenticate(v) => handle - .query(QueryBy::credentials(v)) + .query(QueryBy::Credentials(v)) .await .unwrap() .is_some() @@ -129,7 +129,7 @@ async fn smtp_directory() { let result: LookupResult = match &item { Item::IsAccount(v) => handle.rcpt(v).await.unwrap().into(), Item::Authenticate(v) => handle - .query(QueryBy::credentials(v)) + .query(QueryBy::Credentials(v)) .await .unwrap() .is_some() diff --git a/tests/src/directory/sql.rs b/tests/src/directory/sql.rs index a414a417..4b81352c 100644 --- a/tests/src/directory/sql.rs +++ b/tests/src/directory/sql.rs @@ -27,7 +27,7 @@ use mail_send::Credentials; use smtp::core::Lookup; use store::{LookupStore, Store}; -use crate::directory::{map_account_ids, parse_config}; +use crate::directory::{map_account_ids, DirectoryTest}; use super::DirectoryStore; @@ -41,17 +41,17 @@ async fn sql_directory() { ) .unwrap();*/ - // Parse config - let mut config = parse_config().await; - let lookups = config - .stores - .lookups - .into_iter() - .map(|(k, v)| (k, Lookup::from(v))) - .collect::>(); - // Obtain directory handle for directory_id in ["sqlite", "postgresql", "mysql"] { + // Parse config + let mut config = DirectoryTest::new(directory_id.into()).await; + let lookups = config + .stores + .lookups + .into_iter() + .map(|(k, v)| (k, Lookup::from(v))) + .collect::>(); + println!("Testing SQL directory {:?}", directory_id); let handle = config.directories.directories.remove(directory_id).unwrap(); let store = DirectoryStore { @@ -133,13 +133,10 @@ async fn sql_directory() { // Test authentication assert_eq!( handle - .query( - QueryBy::credentials(&Credentials::Plain { - username: "john".to_string(), - secret: "12345".to_string() - }) - .with_store(base_store) - ) + .query(QueryBy::Credentials(&Credentials::Plain { + username: "john".to_string(), + secret: "12345".to_string() + })) .await .unwrap() .unwrap(), @@ -160,13 +157,10 @@ async fn sql_directory() { ); assert_eq!( handle - .query( - QueryBy::credentials(&Credentials::Plain { - username: "bill".to_string(), - secret: "password".to_string() - }) - .with_store(base_store) - ) + .query(QueryBy::Credentials(&Credentials::Plain { + username: "bill".to_string(), + secret: "password".to_string() + })) .await .unwrap() .unwrap(), @@ -184,24 +178,17 @@ async fn sql_directory() { } ); assert!(handle - .query( - QueryBy::credentials(&Credentials::Plain { - username: "bill".to_string(), - secret: "invalid".to_string() - }) - .with_store(base_store) - ) + .query(QueryBy::Credentials(&Credentials::Plain { + username: "bill".to_string(), + secret: "invalid".to_string() + })) .await .unwrap() .is_none()); // Get user by name assert_eq!( - handle - .query(QueryBy::name("jane").with_store(base_store)) - .await - .unwrap() - .unwrap(), + handle.query(QueryBy::Name("jane")).await.unwrap().unwrap(), Principal { id: base_store.get_account_id("jane").await.unwrap().unwrap(), name: "jane".to_string(), @@ -216,11 +203,7 @@ async fn sql_directory() { // Get group by name assert_eq!( - handle - .query(QueryBy::name("sales").with_store(base_store)) - .await - .unwrap() - .unwrap(), + handle.query(QueryBy::Name("sales")).await.unwrap().unwrap(), Principal { id: base_store.get_account_id("sales").await.unwrap().unwrap(), name: "sales".to_string(), @@ -232,45 +215,27 @@ async fn sql_directory() { // Ids by email assert_eq!( - handle - .email_to_ids("jane@example.org", base_store) - .await - .unwrap(), + handle.email_to_ids("jane@example.org").await.unwrap(), map_account_ids(base_store, vec!["jane"]).await ); assert_eq!( - handle - .email_to_ids("info@example.org", base_store) - .await - .unwrap(), + handle.email_to_ids("info@example.org").await.unwrap(), map_account_ids(base_store, vec!["bill", "jane", "john"]).await ); assert_eq!( - handle - .email_to_ids("jane+alias@example.org", base_store) - .await - .unwrap(), + handle.email_to_ids("jane+alias@example.org").await.unwrap(), map_account_ids(base_store, vec!["jane"]).await ); assert_eq!( - handle - .email_to_ids("info+alias@example.org", base_store) - .await - .unwrap(), + handle.email_to_ids("info+alias@example.org").await.unwrap(), map_account_ids(base_store, vec!["bill", "jane", "john"]).await ); assert_eq!( - handle - .email_to_ids("unknown@example.org", base_store) - .await - .unwrap(), + handle.email_to_ids("unknown@example.org").await.unwrap(), Vec::::new() ); assert_eq!( - handle - .email_to_ids("anything@catchall.org", base_store) - .await - .unwrap(), + handle.email_to_ids("anything@catchall.org").await.unwrap(), map_account_ids(base_store, vec!["robert"]).await ); diff --git a/tests/src/imap/mod.rs b/tests/src/imap/mod.rs index 5d43654e..89e151ea 100644 --- a/tests/src/imap/mod.rs +++ b/tests/src/imap/mod.rs @@ -273,7 +273,7 @@ async fn init_imap_tests(store_id: &str, delete_if_exists: bool) -> IMAPTest { .unwrap(); let servers = config.parse_servers().unwrap(); let stores = config.parse_stores().await.failed("Invalid configuration"); - let directory = config.parse_directory(&stores).unwrap(); + let directory = config.parse_directory(&stores, store_id.into()).unwrap(); // Start JMAP and SMTP servers servers.bind(&config); diff --git a/tests/src/jmap/mod.rs b/tests/src/jmap/mod.rs index 23b6380c..7790cb96 100644 --- a/tests/src/jmap/mod.rs +++ b/tests/src/jmap/mod.rs @@ -373,7 +373,7 @@ async fn init_jmap_tests(store_id: &str, delete_if_exists: bool) -> JMAPTest { .unwrap(); let servers = config.parse_servers().unwrap(); let stores = config.parse_stores().await.failed("Invalid configuration"); - let directory = config.parse_directory(&stores).unwrap(); + let directory = config.parse_directory(&stores, store_id.into()).unwrap(); // Start JMAP and SMTP servers servers.bind(&config); diff --git a/tests/src/smtp/inbound/auth.rs b/tests/src/smtp/inbound/auth.rs index 9510c7f5..317df09f 100644 --- a/tests/src/smtp/inbound/auth.rs +++ b/tests/src/smtp/inbound/auth.rs @@ -62,7 +62,7 @@ async fn auth() { let mut ctx = ConfigContext::new(&[]); ctx.directory = Config::new(DIRECTORY) .unwrap() - .parse_directory(&Stores::default()) + .parse_directory(&Stores::default(), None) .unwrap(); let config = &mut core.session.config.auth; diff --git a/tests/src/smtp/inbound/data.rs b/tests/src/smtp/inbound/data.rs index 91e9d3ee..12b73e5a 100644 --- a/tests/src/smtp/inbound/data.rs +++ b/tests/src/smtp/inbound/data.rs @@ -80,7 +80,7 @@ async fn data() { let mut qr = core.init_test_queue("smtp_data_test"); let directory = Config::new(DIRECTORY) .unwrap() - .parse_directory(&Stores::default()) + .parse_directory(&Stores::default(), None) .unwrap(); let config = &mut core.session.config.rcpt; config.directory = IfBlock::new(Some(MaybeDynValue::Static( diff --git a/tests/src/smtp/inbound/dmarc.rs b/tests/src/smtp/inbound/dmarc.rs index 2a8cafa5..17efd1df 100644 --- a/tests/src/smtp/inbound/dmarc.rs +++ b/tests/src/smtp/inbound/dmarc.rs @@ -135,7 +135,7 @@ async fn dmarc() { let mut rr = core.init_test_report(); let directory = Config::new(DIRECTORY) .unwrap() - .parse_directory(&Stores::default()) + .parse_directory(&Stores::default(), None) .unwrap(); let config = &mut core.session.config.rcpt; config.directory = IfBlock::new(Some(MaybeDynValue::Static( diff --git a/tests/src/smtp/inbound/rcpt.rs b/tests/src/smtp/inbound/rcpt.rs index 590e2adc..c51a1f9c 100644 --- a/tests/src/smtp/inbound/rcpt.rs +++ b/tests/src/smtp/inbound/rcpt.rs @@ -74,7 +74,7 @@ async fn rcpt() { let config_ext = &mut core.session.config.extensions; let directory = Config::new(DIRECTORY) .unwrap() - .parse_directory(&Stores::default()) + .parse_directory(&Stores::default(), None) .unwrap(); let config = &mut core.session.config.rcpt; config.directory = IfBlock::new(Some(MaybeDynValue::Static( diff --git a/tests/src/smtp/inbound/rewrite.rs b/tests/src/smtp/inbound/rewrite.rs index 93fc2c9f..e6e57d15 100644 --- a/tests/src/smtp/inbound/rewrite.rs +++ b/tests/src/smtp/inbound/rewrite.rs @@ -104,7 +104,7 @@ async fn address_rewrite() { let mut core = SMTP::test(); let mut ctx = ConfigContext::new(&[]).parse_signatures(); let settings = Config::new(CONFIG).unwrap(); - ctx.directory = settings.parse_directory(&Stores::default()).unwrap(); + ctx.directory = settings.parse_directory(&Stores::default(), None).unwrap(); core.sieve = settings.parse_sieve(&mut ctx).unwrap(); let config = &mut core.session.config; config.mail.script = settings diff --git a/tests/src/smtp/inbound/scripts.rs b/tests/src/smtp/inbound/scripts.rs index 174cd13c..6606d2a3 100644 --- a/tests/src/smtp/inbound/scripts.rs +++ b/tests/src/smtp/inbound/scripts.rs @@ -135,7 +135,7 @@ async fn sieve_scripts() { ) .unwrap(); ctx.stores = config.parse_stores().await.unwrap(); - ctx.directory = config.parse_directory(&ctx.stores).unwrap(); + ctx.directory = config.parse_directory(&ctx.stores, None).unwrap(); let pipes = config.parse_pipes(&ctx, &[EnvelopeKey::RemoteIp]).unwrap(); core.sieve = config.parse_sieve(&mut ctx).unwrap(); let config = &mut core.session.config; diff --git a/tests/src/smtp/inbound/sign.rs b/tests/src/smtp/inbound/sign.rs index 158a3dcb..f0a05593 100644 --- a/tests/src/smtp/inbound/sign.rs +++ b/tests/src/smtp/inbound/sign.rs @@ -154,7 +154,7 @@ async fn sign_and_seal() { let directory = Config::new(DIRECTORY) .unwrap() - .parse_directory(&Stores::default()) + .parse_directory(&Stores::default(), None) .unwrap(); let config = &mut core.session.config.rcpt; config.directory = IfBlock::new(Some(MaybeDynValue::Static( diff --git a/tests/src/smtp/inbound/vrfy.rs b/tests/src/smtp/inbound/vrfy.rs index 610ff96a..1df0308c 100644 --- a/tests/src/smtp/inbound/vrfy.rs +++ b/tests/src/smtp/inbound/vrfy.rs @@ -68,7 +68,7 @@ async fn vrfy_expn() { let directory = Config::new(DIRECTORY) .unwrap() - .parse_directory(&Stores::default()) + .parse_directory(&Stores::default(), None) .unwrap(); let config = &mut core.session.config.rcpt; config.directory = IfBlock::new(Some(MaybeDynValue::Static( diff --git a/tests/src/smtp/lookup/sql.rs b/tests/src/smtp/lookup/sql.rs index e542d942..feeecf20 100644 --- a/tests/src/smtp/lookup/sql.rs +++ b/tests/src/smtp/lookup/sql.rs @@ -87,7 +87,7 @@ async fn lookup_sql() { let mut ctx = ConfigContext::new(&[]); let config = Config::new(&config_file).unwrap(); ctx.stores = config.parse_stores().await.unwrap(); - ctx.directory = config.parse_directory(&ctx.stores).unwrap(); + ctx.directory = config.parse_directory(&ctx.stores, None).unwrap(); // Obtain directory handle let handle = DirectoryStore { diff --git a/tests/src/smtp/management/queue.rs b/tests/src/smtp/management/queue.rs index 07150a94..ae929f07 100644 --- a/tests/src/smtp/management/queue.rs +++ b/tests/src/smtp/management/queue.rs @@ -98,7 +98,7 @@ async fn manage_queue() { // Start local management interface let directory = Config::new(DIRECTORY) .unwrap() - .parse_directory(&Stores::default()) + .parse_directory(&Stores::default(), None) .unwrap(); core.queue.config.management_lookup = directory.directories.get("local").unwrap().clone(); core.session.config.rcpt.relay = IfBlock::new(true); diff --git a/tests/src/smtp/management/report.rs b/tests/src/smtp/management/report.rs index b190a6bc..75de989d 100644 --- a/tests/src/smtp/management/report.rs +++ b/tests/src/smtp/management/report.rs @@ -85,7 +85,7 @@ async fn manage_reports() { config.tls.max_size = IfBlock::new(1024); let directory = Config::new(DIRECTORY) .unwrap() - .parse_directory(&Stores::default()) + .parse_directory(&Stores::default(), None) .unwrap(); core.queue.config.management_lookup = directory.directories.get("local").unwrap().clone(); let (report_tx, report_rx) = mpsc::channel(1024);