From f21bce722e55e8ab72b2414519a0dfa3578fbabd Mon Sep 17 00:00:00 2001 From: mdecimus Date: Thu, 14 Dec 2023 12:35:25 +0100 Subject: [PATCH] Directory refactoring --- Cargo.lock | 1 + crates/directory/Cargo.toml | 1 + .../src/{ => backend}/imap/client.rs | 2 +- .../src/{ => backend}/imap/config.rs | 4 +- crates/directory/src/backend/imap/lookup.rs | 100 ++++ .../directory/src/{ => backend}/imap/mod.rs | 0 .../directory/src/{ => backend}/imap/pool.rs | 0 .../directory/src/{ => backend}/imap/tls.rs | 0 .../directory/src/backend/internal/manage.rs | 229 ++++++++++ crates/directory/src/backend/internal/mod.rs | 225 +++++++++ .../src/{ => backend}/ldap/config.rs | 29 +- crates/directory/src/backend/ldap/lookup.rs | 431 ++++++++++++++++++ .../directory/src/{ => backend}/ldap/mod.rs | 7 +- .../directory/src/{ => backend}/ldap/pool.rs | 0 crates/directory/src/backend/memory/config.rs | 133 ++++++ .../src/{ => backend}/memory/lookup.rs | 94 ++-- .../directory/src/{ => backend}/memory/mod.rs | 12 +- crates/directory/src/backend/mod.rs | 29 ++ .../src/{ => backend}/smtp/config.rs | 4 +- .../src/{ => backend}/smtp/lookup.rs | 26 +- .../directory/src/{ => backend}/smtp/mod.rs | 0 .../directory/src/{ => backend}/smtp/pool.rs | 0 .../directory/src/{ => backend}/sql/config.rs | 4 - .../directory/src/{ => backend}/sql/lookup.rs | 186 +++++--- crates/directory/src/{ => backend}/sql/mod.rs | 1 - crates/directory/src/cache/lookup.rs | 23 +- crates/directory/src/config.rs | 12 +- crates/directory/src/imap/lookup.rs | 104 ----- crates/directory/src/ldap/lookup.rs | 399 ---------------- crates/directory/src/lib.rs | 54 ++- crates/directory/src/memory/config.rs | 154 ------- crates/imap/src/core/mailbox.rs | 13 +- crates/imap/src/op/acl.rs | 28 +- crates/install/src/main.rs | 2 +- crates/jmap/src/api/admin.rs | 78 ---- crates/jmap/src/api/http.rs | 16 +- crates/jmap/src/api/mod.rs | 1 - crates/jmap/src/api/request.rs | 2 +- crates/jmap/src/api/session.rs | 5 +- crates/jmap/src/auth/acl.rs | 27 +- crates/jmap/src/auth/authenticate.rs | 164 ++----- crates/jmap/src/auth/mod.rs | 10 +- crates/jmap/src/auth/oauth/token.rs | 16 +- crates/jmap/src/email/copy.rs | 12 +- crates/jmap/src/email/index.rs | 10 +- crates/jmap/src/email/ingest.rs | 11 +- crates/jmap/src/email/set.rs | 18 +- crates/jmap/src/identity/set.rs | 14 +- crates/jmap/src/lib.rs | 104 +---- crates/jmap/src/principal/get.rs | 23 +- crates/jmap/src/principal/query.rs | 14 +- crates/jmap/src/services/index.rs | 230 +++++----- crates/jmap/src/services/ingest.rs | 39 +- crates/jmap/src/sieve/ingest.rs | 33 +- crates/jmap/src/sieve/set.rs | 13 +- crates/jmap/src/vacation/set.rs | 11 +- crates/managesieve/src/op/putscript.rs | 11 +- crates/smtp/src/config/queue.rs | 2 +- crates/smtp/src/core/management.rs | 7 +- crates/smtp/src/inbound/auth.rs | 7 +- crates/store/src/backend/mysql/main.rs | 4 +- crates/store/src/backend/postgres/main.rs | 10 +- crates/store/src/backend/rocksdb/main.rs | 5 +- crates/store/src/backend/rocksdb/mod.rs | 3 +- crates/store/src/backend/rocksdb/write.rs | 22 +- crates/store/src/backend/sqlite/main.rs | 9 +- crates/store/src/dispatch/lookup.rs | 18 +- crates/store/src/dispatch/store.rs | 50 +- crates/store/src/lib.rs | 35 +- crates/store/src/write/key.rs | 150 +++--- crates/store/src/write/mod.rs | 13 +- resources/config/directory/memory.toml | 12 +- tests/src/directory/imap.rs | 5 +- tests/src/directory/ldap.rs | 126 +++-- tests/src/directory/mod.rs | 47 +- tests/src/directory/smtp.rs | 16 +- tests/src/directory/sql.rs | 140 +++--- tests/src/imap/mod.rs | 4 +- tests/src/jmap/auth_acl.rs | 15 +- tests/src/jmap/auth_limits.rs | 12 +- tests/src/jmap/auth_oauth.rs | 12 +- tests/src/jmap/blob.rs | 11 +- tests/src/jmap/crypto.rs | 10 +- tests/src/jmap/delivery.rs | 33 +- tests/src/jmap/email_copy.rs | 4 +- tests/src/jmap/email_get.rs | 2 +- tests/src/jmap/email_parse.rs | 2 +- tests/src/jmap/email_query.rs | 2 +- tests/src/jmap/email_query_changes.rs | 2 +- tests/src/jmap/email_search_snippet.rs | 2 +- tests/src/jmap/email_set.rs | 2 +- tests/src/jmap/email_submission.rs | 12 +- tests/src/jmap/event_source.rs | 12 +- tests/src/jmap/mailbox.rs | 13 +- tests/src/jmap/mod.rs | 5 +- tests/src/jmap/push_subscription.rs | 11 +- tests/src/jmap/quota.rs | 19 +- tests/src/jmap/sieve_script.rs | 12 +- tests/src/jmap/stress_test.rs | 8 +- tests/src/jmap/thread_get.rs | 2 +- tests/src/jmap/thread_merge.rs | 6 +- tests/src/jmap/vacation_response.rs | 12 +- tests/src/jmap/websocket.rs | 12 +- tests/src/smtp/inbound/auth.rs | 4 +- tests/src/smtp/inbound/data.rs | 8 +- tests/src/smtp/inbound/dmarc.rs | 2 +- tests/src/smtp/inbound/rcpt.rs | 8 +- tests/src/smtp/inbound/sign.rs | 2 +- tests/src/smtp/inbound/vrfy.rs | 6 +- tests/src/smtp/management/queue.rs | 2 +- tests/src/smtp/management/report.rs | 2 +- tests/src/smtp/mod.rs | 2 +- 112 files changed, 2300 insertions(+), 1823 deletions(-) rename crates/directory/src/{ => backend}/imap/client.rs (99%) rename crates/directory/src/{ => backend}/imap/config.rs (95%) create mode 100644 crates/directory/src/backend/imap/lookup.rs rename crates/directory/src/{ => backend}/imap/mod.rs (100%) rename crates/directory/src/{ => backend}/imap/pool.rs (100%) rename crates/directory/src/{ => backend}/imap/tls.rs (100%) create mode 100644 crates/directory/src/backend/internal/manage.rs create mode 100644 crates/directory/src/backend/internal/mod.rs rename crates/directory/src/{ => backend}/ldap/config.rs (93%) create mode 100644 crates/directory/src/backend/ldap/lookup.rs rename crates/directory/src/{ => backend}/ldap/mod.rs (97%) rename crates/directory/src/{ => backend}/ldap/pool.rs (100%) create mode 100644 crates/directory/src/backend/memory/config.rs rename crates/directory/src/{ => backend}/memory/lookup.rs (55%) rename crates/directory/src/{ => backend}/memory/mod.rs (85%) create mode 100644 crates/directory/src/backend/mod.rs rename crates/directory/src/{ => backend}/smtp/config.rs (95%) rename crates/directory/src/{ => backend}/smtp/lookup.rs (84%) rename crates/directory/src/{ => backend}/smtp/mod.rs (100%) rename crates/directory/src/{ => backend}/smtp/pool.rs (100%) rename crates/directory/src/{ => backend}/sql/config.rs (95%) rename crates/directory/src/{ => backend}/sql/lookup.rs (51%) rename crates/directory/src/{ => backend}/sql/mod.rs (98%) delete mode 100644 crates/directory/src/imap/lookup.rs delete mode 100644 crates/directory/src/ldap/lookup.rs delete mode 100644 crates/directory/src/memory/config.rs delete mode 100644 crates/jmap/src/api/admin.rs diff --git a/Cargo.lock b/Cargo.lock index c2eebee9..899cd522 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1423,6 +1423,7 @@ dependencies = [ "async-trait", "deadpool", "futures", + "jmap_proto", "ldap3", "lru-cache", "mail-builder", diff --git a/crates/directory/Cargo.toml b/crates/directory/Cargo.toml index 6f9d5d26..c3a86da5 100644 --- a/crates/directory/Cargo.toml +++ b/crates/directory/Cargo.toml @@ -7,6 +7,7 @@ resolver = "2" [dependencies] utils = { path = "../utils" } store = { path = "../store" } +jmap_proto = { path = "../jmap-proto" } smtp-proto = { git = "https://github.com/stalwartlabs/smtp-proto" } mail-parser = { git = "https://github.com/stalwartlabs/mail-parser", features = ["full_encoding", "serde_support", "ludicrous_mode"] } mail-send = { git = "https://github.com/stalwartlabs/mail-send", default-features = false, features = ["cram-md5", "skip-ehlo"] } diff --git a/crates/directory/src/imap/client.rs b/crates/directory/src/backend/imap/client.rs similarity index 99% rename from crates/directory/src/imap/client.rs rename to crates/directory/src/backend/imap/client.rs index a2daedc6..0ff98369 100644 --- a/crates/directory/src/imap/client.rs +++ b/crates/directory/src/backend/imap/client.rs @@ -199,7 +199,7 @@ mod test { use smtp_proto::{AUTH_OAUTHBEARER, AUTH_PLAIN, AUTH_XOAUTH, AUTH_XOAUTH2}; use std::time::Duration; - use crate::imap::ImapClient; + use crate::backend::imap::ImapClient; #[ignore] #[tokio::test] diff --git a/crates/directory/src/imap/config.rs b/crates/directory/src/backend/imap/config.rs similarity index 95% rename from crates/directory/src/imap/config.rs rename to crates/directory/src/backend/imap/config.rs index 5f4f20f1..9859a25d 100644 --- a/crates/directory/src/imap/config.rs +++ b/crates/directory/src/backend/imap/config.rs @@ -26,9 +26,9 @@ use std::sync::Arc; use mail_send::smtp::tls::build_tls_connector; use utils::config::{utils::AsKey, Config}; -use crate::{cache::CachedDirectory, config::build_pool, imap::ImapConnectionManager, Directory}; +use crate::{cache::CachedDirectory, config::build_pool, Directory}; -use super::ImapDirectory; +use super::{ImapConnectionManager, ImapDirectory}; impl ImapDirectory { pub fn from_config( diff --git a/crates/directory/src/backend/imap/lookup.rs b/crates/directory/src/backend/imap/lookup.rs new file mode 100644 index 00000000..2208884e --- /dev/null +++ b/crates/directory/src/backend/imap/lookup.rs @@ -0,0 +1,100 @@ +/* + * 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 smtp_proto::{AUTH_CRAM_MD5, AUTH_LOGIN, AUTH_OAUTHBEARER, AUTH_PLAIN, AUTH_XOAUTH2}; +use store::Store; + +use crate::{Directory, DirectoryError, Principal, QueryBy, QueryType}; + +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 { + let mut client = self.pool.get().await?; + let mechanism = match credentials { + Credentials::Plain { .. } + if (client.mechanisms & (AUTH_PLAIN | AUTH_LOGIN | AUTH_CRAM_MD5)) != 0 => + { + if client.mechanisms & AUTH_CRAM_MD5 != 0 { + AUTH_CRAM_MD5 + } else if client.mechanisms & AUTH_PLAIN != 0 { + AUTH_PLAIN + } else { + AUTH_LOGIN + } + } + Credentials::OAuthBearer { .. } if client.mechanisms & AUTH_OAUTHBEARER != 0 => { + AUTH_OAUTHBEARER + } + Credentials::XOauth2 { .. } if client.mechanisms & AUTH_XOAUTH2 != 0 => { + AUTH_XOAUTH2 + } + _ => { + tracing::warn!( + context = "remote", + event = "error", + protocol = "imap", + "IMAP server does not offer any supported auth mechanisms.", + ); + return Ok(None); + } + }; + + match client.authenticate(mechanism, credentials).await { + Ok(_) => { + client.is_valid = false; + Ok(Some(Principal::default())) + } + Err(err) => match &err { + ImapError::AuthenticationFailed => Ok(None), + _ => Err(err.into()), + }, + } + } else { + Err(DirectoryError::unsupported("imap", "query")) + } + } + + async fn email_to_ids(&self, _address: &str, _store: &Store) -> crate::Result> { + Err(DirectoryError::unsupported("imap", "email_to_ids")) + } + + async fn rcpt(&self, _address: &str) -> crate::Result { + Err(DirectoryError::unsupported("imap", "rcpt")) + } + + async fn vrfy(&self, _address: &str) -> crate::Result> { + Err(DirectoryError::unsupported("imap", "vrfy")) + } + + async fn expn(&self, _address: &str) -> crate::Result> { + Err(DirectoryError::unsupported("imap", "expn")) + } + + async fn is_local_domain(&self, domain: &str) -> crate::Result { + Ok(self.domains.contains(domain)) + } +} diff --git a/crates/directory/src/imap/mod.rs b/crates/directory/src/backend/imap/mod.rs similarity index 100% rename from crates/directory/src/imap/mod.rs rename to crates/directory/src/backend/imap/mod.rs diff --git a/crates/directory/src/imap/pool.rs b/crates/directory/src/backend/imap/pool.rs similarity index 100% rename from crates/directory/src/imap/pool.rs rename to crates/directory/src/backend/imap/pool.rs diff --git a/crates/directory/src/imap/tls.rs b/crates/directory/src/backend/imap/tls.rs similarity index 100% rename from crates/directory/src/imap/tls.rs rename to crates/directory/src/backend/imap/tls.rs diff --git a/crates/directory/src/backend/internal/manage.rs b/crates/directory/src/backend/internal/manage.rs new file mode 100644 index 00000000..71e03247 --- /dev/null +++ b/crates/directory/src/backend/internal/manage.rs @@ -0,0 +1,229 @@ +/* + * 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 jmap_proto::types::collection::Collection; +use store::{ + write::{assert::HashedValue, BatchBuilder, DirectoryValue, ValueClass}, + Serialize, Store, ValueKey, +}; + +use crate::{Principal, Type}; + +#[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_or_create_account_id(&self, name: &str) -> crate::Result; +} + +#[async_trait::async_trait] +impl ManageDirectory for Store { + async fn get_account_id(&self, name: &str) -> store::Result> { + self.get_value::(ValueKey::from(ValueClass::Directory( + DirectoryValue::NameToId(name.as_bytes().to_vec()), + ))) + .await + } + + // Used by all directories except internal + async fn get_or_create_account_id(&self, name: &str) -> crate::Result { + let mut try_count = 0; + + loop { + // Try to obtain ID + if let Some(account_id) = self.get_account_id(name).await? { + return Ok(account_id); + } + + // Assign new ID + let account_id = self + .assign_document_id(u32::MAX, Collection::Principal) + .await?; + + // Write account ID + let name_key = + ValueClass::Directory(DirectoryValue::NameToId(name.as_bytes().to_vec())); + let mut batch = BatchBuilder::new(); + batch + .with_account_id(u32::MAX) + .with_collection(Collection::Principal) + .create_document(account_id) + .assert_value(name_key.clone(), ()) + .set(name_key, account_id.serialize()) + .set( + ValueClass::Directory(DirectoryValue::Principal(account_id)), + Principal { + id: account_id, + typ: Type::Individual, + name: name.to_string(), + ..Default::default() + } + .serialize(), + ); + + match self.write(batch.build()).await { + Ok(_) => { + return Ok(account_id); + } + Err(store::Error::AssertValueFailed) if try_count < 3 => { + try_count += 1; + continue; + } + Err(err) => { + tracing::error!(event = "error", + context = "store", + error = ?err, + "Failed to generate account id"); + return Err(err.into()); + } + } + } + } + + 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 delete_account_by_id(&self, account_id: u32) -> store::Result { + let principal = if let Some(principal) = self + .get_value::(ValueKey::from(ValueClass::Directory( + DirectoryValue::Principal(account_id), + ))) + .await? + { + principal + } else { + return Ok(false); + }; + + // Unlink all account's blobs + self.blob_hash_unlink_account(account_id).await?; + + // Revoke ACLs + self.acl_revoke_all(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::Principal(account_id)) + .clear(DirectoryValue::UsedQuota(account_id)); + + for email in principal.emails { + batch.clear(DirectoryValue::EmailToId(email.as_bytes().to_vec())); + } + + self.write(batch.build()).await?; + + // Delete account data + self.purge_account(account_id).await?; + + Ok(true) + } + + 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); + } + principal.inner.name = new_name.clone(); + + 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); + } + } + + Ok(false) + } +} + +/* +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?); + } + 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 new file mode 100644 index 00000000..73f47163 --- /dev/null +++ b/crates/directory/src/backend/internal/mod.rs @@ -0,0 +1,225 @@ +/* + * 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. +*/ + +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 utils::codec::leb128::Leb128Iterator; + +use crate::{Principal, QueryBy, QueryType, 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 { + let mut serializer = KeySerializer::new( + U32_LEN * 3 + + 2 + + self.name.len() + + self.emails.iter().map(|s| s.len()).sum::() + + self.secrets.iter().map(|s| s.len()).sum::() + + self.member_of.len() * U32_LEN + + self.description.as_ref().map(|s| s.len()).unwrap_or(0), + ) + .write(1u8) + .write_leb128(self.id) + .write(self.typ as u8) + .write_leb128(self.quota) + .write_leb128(self.name.len()) + .write(self.name.as_bytes()) + .write_leb128(self.description.as_ref().map_or(0, |s| s.len())) + .write(self.description.as_deref().unwrap_or_default().as_bytes()); + + for list in [&self.secrets, &self.emails] { + serializer = serializer.write_leb128(list.len()); + for value in list { + serializer = serializer.write_leb128(value.len()).write(value.as_bytes()); + } + } + + serializer = serializer.write_leb128(self.member_of.len()); + for id in &self.member_of { + serializer = serializer.write_leb128(*id); + } + + serializer.finalize() + } +} + +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 { + let mut bytes = bytes.iter(); + if bytes.next()? != &1 { + return None; + } + + Principal { + id: bytes.next_leb128()?, + typ: Type::from_u8(*bytes.next()?), + quota: bytes.next_leb128()?, + name: deserialize_string(&mut bytes)?, + description: deserialize_string(&mut bytes).map(|v| { + if !v.is_empty() { + Some(v) + } else { + None + } + })?, + secrets: deserialize_string_list(&mut bytes)?, + emails: deserialize_string_list(&mut bytes)?, + member_of: deserialize_u32_list(&mut bytes)?, + } + .into() +} + +fn deserialize_string(bytes: &mut Iter<'_, u8>) -> Option { + let len = bytes.next_leb128()?; + let mut string = Vec::with_capacity(len); + for _ in 0..len { + string.push(*bytes.next()?); + } + String::from_utf8(string).ok() +} + +fn deserialize_string_list(bytes: &mut Iter<'_, u8>) -> Option> { + let len = bytes.next_leb128()?; + let mut list = Vec::with_capacity(len); + for _ in 0..len { + list.push(deserialize_string(bytes)?); + } + Some(list) +} + +fn deserialize_u32_list(bytes: &mut Iter<'_, u8>) -> Option> { + let len = bytes.next_leb128()?; + let mut list = Vec::with_capacity(len); + for _ in 0..len { + list.push(bytes.next_leb128()?); + } + Some(list) +} + +impl Type { + pub fn from_u8(value: u8) -> Self { + match value { + 0 => Type::Individual, + 1 => Type::Group, + 2 => Type::Resource, + 3 => Type::Location, + 4 => Type::Superuser, + 5 => Type::List, + _ => Type::Other, + } + } +} diff --git a/crates/directory/src/ldap/config.rs b/crates/directory/src/backend/ldap/config.rs similarity index 93% rename from crates/directory/src/ldap/config.rs rename to crates/directory/src/backend/ldap/config.rs index 821e18c9..4181aa6e 100644 --- a/crates/directory/src/ldap/config.rs +++ b/crates/directory/src/backend/ldap/config.rs @@ -64,16 +64,18 @@ impl LdapDirectory { filter_verify: LdapFilter::from_config(config, (&prefix, "filter.verify"))?, filter_expand: LdapFilter::from_config(config, (&prefix, "filter.expand"))?, filter_domains: LdapFilter::from_config(config, (&prefix, "filter.domains"))?, - obj_user: config - .value_require((&prefix, "object-classes.user"))? - .to_string(), - obj_group: config - .value_require((&prefix, "object-classes.group"))? - .to_string(), attr_name: config .values((&prefix, "attributes.name")) .map(|(_, v)| v.to_string()) .collect(), + attr_groups: config + .values((&prefix, "attributes.groups")) + .map(|(_, v)| v.to_string()) + .collect(), + attr_type: config + .values((&prefix, "attributes.type")) + .map(|(_, v)| v.to_string()) + .collect(), attr_description: config .values((&prefix, "attributes.description")) .map(|(_, v)| v.to_string()) @@ -82,10 +84,6 @@ impl LdapDirectory { .values((&prefix, "attributes.secret")) .map(|(_, v)| v.to_string()) .collect(), - attr_groups: config - .values((&prefix, "attributes.groups")) - .map(|(_, v)| v.to_string()) - .collect(), attr_email_address: config .values((&prefix, "attributes.email")) .map(|(_, v)| v.to_string()) @@ -94,27 +92,26 @@ impl LdapDirectory { .values((&prefix, "attributes.quota")) .map(|(_, v)| v.to_string()) .collect(), - attrs_principal: vec!["objectClass".to_string()], - attrs_email: config + attr_email_alias: config .values((&prefix, "attributes.email-alias")) .map(|(_, v)| v.to_string()) .collect(), + attrs_principal: vec!["objectClass".to_string()], }; for attr in [ &mappings.attr_name, + &mappings.attr_type, &mappings.attr_description, &mappings.attr_secret, &mappings.attr_quota, &mappings.attr_groups, + &mappings.attr_email_address, + &mappings.attr_email_alias, ] { mappings.attrs_principal.extend(attr.iter().cloned()); } - mappings - .attrs_email - .extend(mappings.attr_email_address.iter().cloned()); - let auth_bind = if config.property_or_static::((&prefix, "auth-bind.enable"), "false")? { LdapFilter::from_config(config, (&prefix, "auth-bind.dn"))?.into() diff --git a/crates/directory/src/backend/ldap/lookup.rs b/crates/directory/src/backend/ldap/lookup.rs new file mode 100644 index 00000000..d392931f --- /dev/null +++ b/crates/directory/src/backend/ldap/lookup.rs @@ -0,0 +1,431 @@ +/* + * 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 ldap3::{Ldap, LdapConnAsync, LdapError, Scope, SearchEntry}; +use mail_send::Credentials; +use store::Store; + +use crate::{ + backend::internal::manage::ManageDirectory, Directory, DirectoryError, Principal, QueryBy, + QueryType, Type, +}; + +use super::{LdapDirectory, LdapMappings}; + +#[async_trait::async_trait] +impl Directory for LdapDirectory { + 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) => { + account_name = username.to_string(); + + if let Some(principal) = self + .find_principal(&mut conn, &self.mappings.filter_name.build(username)) + .await? + { + principal + } else { + return Ok(None); + } + } + QueryType::Id(uid) => { + if let Some(username) = by.account_name(uid).await? { + account_name = username; + } else { + return Ok(None); + } + account_id = Some(uid); + + if let Some(principal) = self + .find_principal(&mut conn, &self.mappings.filter_name.build(&account_name)) + .await? + { + principal + } else { + return Ok(None); + } + } + QueryType::Credentials(credentials) => { + let (username, secret) = match credentials { + Credentials::Plain { username, secret } => (username, secret), + Credentials::OAuthBearer { token } => (token, token), + Credentials::XOauth2 { username, secret } => (username, secret), + }; + account_name = username.to_string(); + + if let Some(auth_bind) = &self.auth_bind { + let (conn, mut ldap) = LdapConnAsync::with_settings( + self.pool.manager().settings.clone(), + &self.pool.manager().address, + ) + .await?; + + ldap3::drive!(conn); + + ldap.simple_bind(&auth_bind.build(username), secret).await?; + + match self + .find_principal(&mut ldap, &self.mappings.filter_name.build(username)) + .await + { + Ok(Some(principal)) => principal, + Err(DirectoryError::Ldap(LdapError::LdapResult { result })) + if [49, 50].contains(&result.rc) => + { + return Ok(None); + } + Ok(None) => return Ok(None), + Err(err) => return Err(err), + } + } else if let Some(principal) = self + .find_principal(&mut conn, &self.mappings.filter_name.build(username)) + .await? + { + if principal.principal.verify_secret(secret).await { + principal + } else { + tracing::debug!( + context = "directory", + event = "invalid_password", + protocol = "ldap", + account = username, + "Invalid password for account" + ); + return Ok(None); + } + } else { + return Ok(None); + } + } + }; + let groups = principal.groups; + let mut principal = 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?; + } + 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('=') { + let (rs, _res) = conn + .search( + &group, + Scope::Base, + "objectClass=*", + &self.mappings.attr_name, + ) + .await? + .success()?; + 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 !group.is_empty() { + principal.member_of.push(by.account_id(group).await?); + break 'outer; + } + } + } + } + } + } else { + principal.member_of.push(by.account_id(&group).await?); + } + } + } + + Ok(Some(principal)) + } + + async fn email_to_ids(&self, address: &str, store: &Store) -> crate::Result> { + let mut rs = self + .pool + .get() + .await? + .search( + &self.mappings.base_dn, + Scope::Subtree, + &self + .mappings + .filter_email + .build(self.opt.subaddressing.to_subaddress(address).as_ref()), + &self.mappings.attr_name, + ) + .await? + .success() + .map(|(rs, _res)| rs)?; + + if rs.is_empty() { + if let Some(address) = self.opt.catch_all.to_catch_all(address) { + rs = self + .pool + .get() + .await? + .search( + &self.mappings.base_dn, + Scope::Subtree, + &self.mappings.filter_email.build(address.as_ref()), + &self.mappings.attr_name, + ) + .await? + .success() + .map(|(rs, _res)| rs)?; + } else { + return Ok(Vec::new()); + } + } + + let mut ids = Vec::with_capacity(rs.len()); + for entry in rs { + let entry = SearchEntry::construct(entry); + '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?); + break 'outer; + } + } + } + } + + Ok(ids) + } + + async fn rcpt(&self, address: &str) -> crate::Result { + match self + .pool + .get() + .await? + .streaming_search( + &self.mappings.base_dn, + Scope::Subtree, + &self + .mappings + .filter_email + .build(self.opt.subaddressing.to_subaddress(address).as_ref()), + &self.mappings.attr_email_address, + ) + .await? + .next() + .await + { + Ok(Some(_)) => Ok(true), + Ok(None) => { + if let Some(address) = self.opt.catch_all.to_catch_all(address) { + self.pool + .get() + .await? + .streaming_search( + &self.mappings.base_dn, + Scope::Subtree, + &self.mappings.filter_email.build(address.as_ref()), + &self.mappings.attr_email_address, + ) + .await? + .next() + .await + .map(|entry| entry.is_some()) + .map_err(|e| e.into()) + } else { + Ok(false) + } + } + + Err(e) => Err(e.into()), + } + } + + async fn vrfy(&self, address: &str) -> crate::Result> { + let mut stream = self + .pool + .get() + .await? + .streaming_search( + &self.mappings.base_dn, + Scope::Subtree, + &self + .mappings + .filter_verify + .build(self.opt.subaddressing.to_subaddress(address).as_ref()), + &self.mappings.attr_email_address, + ) + .await?; + + let mut emails = Vec::new(); + while let Some(entry) = stream.next().await? { + let entry = SearchEntry::construct(entry); + for attr in &self.mappings.attr_email_address { + if let Some(values) = entry.attrs.get(attr) { + for email in values { + if !email.is_empty() { + emails.push(email.to_string()); + } + } + } + } + } + + Ok(emails) + } + + async fn expn(&self, address: &str) -> crate::Result> { + let mut stream = self + .pool + .get() + .await? + .streaming_search( + &self.mappings.base_dn, + Scope::Subtree, + &self + .mappings + .filter_expand + .build(self.opt.subaddressing.to_subaddress(address).as_ref()), + &self.mappings.attr_email_address, + ) + .await?; + + let mut emails = Vec::new(); + while let Some(entry) = stream.next().await? { + let entry = SearchEntry::construct(entry); + for attr in &self.mappings.attr_email_address { + if let Some(values) = entry.attrs.get(attr) { + for email in values { + if !email.is_empty() { + emails.push(email.to_string()); + } + } + } + } + } + + Ok(emails) + } + + async fn is_local_domain(&self, domain: &str) -> crate::Result { + self.pool + .get() + .await? + .streaming_search( + &self.mappings.base_dn, + Scope::Subtree, + &self.mappings.filter_domains.build(domain), + Vec::::new(), + ) + .await? + .next() + .await + .map(|entry| entry.is_some()) + .map_err(|e| e.into()) + } +} + +impl LdapDirectory { + async fn find_principal( + &self, + conn: &mut Ldap, + filter: &str, + ) -> crate::Result> { + conn.search( + &self.mappings.base_dn, + Scope::Subtree, + filter, + &self.mappings.attrs_principal, + ) + .await? + .success() + .map(|(rs, _)| { + rs.into_iter().next().map(|entry| { + self.mappings + .entry_to_principal(SearchEntry::construct(entry)) + }) + }) + .map_err(Into::into) + } +} + +struct PrincipalWithGroups { + principal: Principal, + groups: Vec, +} + +impl LdapMappings { + fn entry_to_principal(&self, entry: SearchEntry) -> PrincipalWithGroups { + let mut groups = Vec::new(); + let mut principal = Principal::default(); + + for (attr, value) in entry.attrs { + if self.attr_name.contains(&attr) { + principal.name = value.into_iter().next().unwrap_or_default(); + } else if self.attr_secret.contains(&attr) { + principal.secrets.extend(value); + } else if self.attr_email_address.contains(&attr) { + for value in value { + if principal.emails.is_empty() { + principal.emails.push(value); + } else { + principal.emails.insert(0, value); + } + } + } else if self.attr_email_alias.contains(&attr) { + principal.emails.extend(value); + } else if let Some(idx) = self.attr_description.iter().position(|a| a == &attr) { + if principal.description.is_none() || idx == 0 { + principal.description = value.into_iter().next(); + } + } else if self.attr_groups.contains(&attr) { + groups.extend(value); + } else if self.attr_quota.contains(&attr) { + if let Ok(quota) = value.into_iter().next().unwrap_or_default().parse() { + principal.quota = quota; + } + } else if self.attr_type.contains(&attr) { + for value in value { + match value.to_ascii_lowercase().as_str() { + "admin" | "administrator" | "root" | "superuser" => { + principal.typ = Type::Superuser + } + "posixaccount" | "individual" | "person" | "inetorgperson" => { + principal.typ = Type::Individual + } + "posixgroup" | "group" => principal.typ = Type::Group, + _ => continue, + } + break; + } + } + } + + PrincipalWithGroups { principal, groups } + } +} diff --git a/crates/directory/src/ldap/mod.rs b/crates/directory/src/backend/ldap/mod.rs similarity index 97% rename from crates/directory/src/ldap/mod.rs rename to crates/directory/src/backend/ldap/mod.rs index 8f0fe882..e8c57cbb 100644 --- a/crates/directory/src/ldap/mod.rs +++ b/crates/directory/src/backend/ldap/mod.rs @@ -45,16 +45,15 @@ pub struct LdapMappings { filter_verify: LdapFilter, filter_expand: LdapFilter, filter_domains: LdapFilter, - obj_user: String, - obj_group: String, attr_name: Vec, + attr_type: Vec, + attr_groups: Vec, attr_description: Vec, attr_secret: Vec, - attr_groups: Vec, attr_email_address: Vec, + attr_email_alias: Vec, attr_quota: Vec, attrs_principal: Vec, - attrs_email: Vec, } #[derive(Debug, Default)] diff --git a/crates/directory/src/ldap/pool.rs b/crates/directory/src/backend/ldap/pool.rs similarity index 100% rename from crates/directory/src/ldap/pool.rs rename to crates/directory/src/backend/ldap/pool.rs diff --git a/crates/directory/src/backend/memory/config.rs b/crates/directory/src/backend/memory/config.rs new file mode 100644 index 00000000..cbcd692b --- /dev/null +++ b/crates/directory/src/backend/memory/config.rs @@ -0,0 +1,133 @@ +/* + * 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 std::sync::Arc; + +use utils::config::{utils::AsKey, Config}; + +use crate::{Directory, DirectoryOptions, Principal, Type}; + +use super::{EmailType, MemoryDirectory}; + +impl MemoryDirectory { + pub fn from_config( + config: &Config, + prefix: impl AsKey, + ) -> utils::config::Result> { + let prefix = prefix.as_key(); + let mut directory = MemoryDirectory { + opt: DirectoryOptions::from_config(config, prefix.clone())?, + ..Default::default() + }; + + for lookup_id in config.sub_keys((prefix.as_str(), "principals")) { + let name = config + .value_require((prefix.as_str(), "principals", lookup_id, "name"))? + .to_string(); + let typ = + match config.value_require((prefix.as_str(), "principals", lookup_id, "name"))? { + "individual" => Type::Individual, + "admin" => Type::Superuser, + "group" => Type::Group, + _ => Type::Other, + }; + + // Obtain id + let next_user_id = directory.names_to_ids.len() as u32; + let id = *directory + .names_to_ids + .entry(name.to_string()) + .or_insert(next_user_id); + + // Obtain group ids + let mut member_of = Vec::new(); + for (_, group) in config.values((prefix.as_str(), "principals", lookup_id, "member-of")) + { + let next_group_id = directory.names_to_ids.len() as u32; + member_of.push( + *directory + .names_to_ids + .entry(group.to_string()) + .or_insert(next_group_id), + ); + } + + // Parse email addresses + let mut emails = Vec::new(); + for (pos, (_, email)) in config + .values((prefix.as_str(), "principals", lookup_id, "email")) + .enumerate() + { + directory + .emails_to_ids + .entry(email.to_string()) + .or_default() + .push(if pos > 0 { + EmailType::Alias(id) + } else { + EmailType::Primary(id) + }); + + if let Some((_, domain)) = email.rsplit_once('@') { + directory.domains.insert(domain.to_lowercase()); + } + + emails.push(email.to_lowercase()); + } + + // Parse mailing lists + for (_, email) in + config.values((prefix.as_str(), "principals", lookup_id, "email-list")) + { + directory + .emails_to_ids + .entry(email.to_lowercase()) + .or_default() + .push(EmailType::List(id)); + if let Some((_, domain)) = email.rsplit_once('@') { + directory.domains.insert(domain.to_lowercase()); + } + } + + directory.principals.push(Principal { + name: name.clone(), + secrets: config + .values((prefix.as_str(), "principals", lookup_id, "secret")) + .map(|(_, v)| v.to_string()) + .collect(), + typ, + description: config + .value((prefix.as_str(), "principals", lookup_id, "description")) + .map(|v| v.to_string()), + quota: config + .property((prefix.as_str(), "principals", lookup_id, "quota"))? + .unwrap_or(0), + member_of, + id, + emails, + }); + } + + Ok(Arc::new(directory)) + } +} diff --git a/crates/directory/src/memory/lookup.rs b/crates/directory/src/backend/memory/lookup.rs similarity index 55% rename from crates/directory/src/memory/lookup.rs rename to crates/directory/src/backend/memory/lookup.rs index 54c230f1..65bb4fec 100644 --- a/crates/directory/src/memory/lookup.rs +++ b/crates/directory/src/backend/memory/lookup.rs @@ -22,65 +22,68 @@ */ use mail_send::Credentials; +use store::Store; -use crate::{Directory, Principal}; +use crate::{Directory, Principal, QueryBy, QueryType}; use super::{EmailType, MemoryDirectory}; #[async_trait::async_trait] impl Directory for MemoryDirectory { - async fn authenticate( - &self, - credentials: &Credentials, - ) -> crate::Result> { - let (username, secret) = match credentials { - Credentials::Plain { username, secret } => (username, secret), - Credentials::OAuthBearer { token } => (token, token), - Credentials::XOauth2 { username, secret } => (username, secret), - }; - match self.principals.get(username) { - Some(principal) if principal.verify_secret(secret).await => Ok(Some(principal.clone())), - _ => Ok(None), - } - } - - async fn principal(&self, name: &str) -> crate::Result> { - Ok(self.principals.get(name).cloned()) - } - - async fn emails_by_name(&self, name: &str) -> crate::Result> { - let mut result = Vec::new(); - if let Some(emails) = self.names_to_email.get(name) { - for email in emails { - match email { - EmailType::Primary(email) | EmailType::Alias(email) => { - result.push(email.clone()) + async fn query(&self, by: QueryBy<'_>) -> crate::Result> { + match by.t { + QueryType::Name(name) => { + for principal in &self.principals { + if principal.name == name { + return Ok(Some(principal.clone())); + } + } + } + QueryType::Id(uid) => { + for principal in &self.principals { + if principal.id == uid { + return Ok(Some(principal.clone())); + } + } + } + QueryType::Credentials(credentials) => { + let (username, secret) = match credentials { + Credentials::Plain { username, secret } => (username, secret), + Credentials::OAuthBearer { token } => (token, token), + Credentials::XOauth2 { username, secret } => (username, secret), + }; + + for principal in &self.principals { + if &principal.name == username { + return if principal.verify_secret(secret).await { + Ok(Some(principal.clone())) + } else { + Ok(None) + }; } - _ => {} } } } - - Ok(result) + Ok(None) } - async fn names_by_email(&self, address: &str) -> crate::Result> { + async fn email_to_ids(&self, address: &str, _: &Store) -> crate::Result> { Ok(self - .emails_to_names + .emails_to_ids .get(self.opt.subaddressing.to_subaddress(address).as_ref()) .or_else(|| { self.opt .catch_all .to_catch_all(address) - .and_then(|address| self.emails_to_names.get(address.as_ref())) + .and_then(|address| self.emails_to_ids.get(address.as_ref())) }) .map(|names| { names .iter() .map(|t| match t { - EmailType::Primary(name) - | EmailType::Alias(name) - | EmailType::List(name) => name.to_string(), + EmailType::Primary(uid) | EmailType::Alias(uid) | EmailType::List(uid) => { + *uid + } }) .collect::>() }) @@ -89,21 +92,21 @@ impl Directory for MemoryDirectory { async fn rcpt(&self, address: &str) -> crate::Result { Ok(self - .emails_to_names + .emails_to_ids .contains_key(self.opt.subaddressing.to_subaddress(address).as_ref()) || self .opt .catch_all .to_catch_all(address) .map_or(false, |address| { - self.emails_to_names.contains_key(address.as_ref()) + self.emails_to_ids.contains_key(address.as_ref()) })) } async fn vrfy(&self, address: &str) -> crate::Result> { let mut result = Vec::new(); let address = self.opt.subaddressing.to_subaddress(address); - for (key, value) in &self.emails_to_names { + for (key, value) in &self.emails_to_ids { if key.contains(address.as_ref()) && value.iter().any(|t| matches!(t, EmailType::Primary(_))) { @@ -116,13 +119,16 @@ impl Directory for MemoryDirectory { async fn expn(&self, address: &str) -> crate::Result> { let mut result = Vec::new(); let address = self.opt.subaddressing.to_subaddress(address); - for (key, value) in &self.emails_to_names { + for (key, value) in &self.emails_to_ids { if key == address.as_ref() { for item in value { - if let EmailType::List(name) = item { - for addr in self.names_to_email.get(name).unwrap() { - if let EmailType::Primary(addr) = addr { - result.push(addr.clone()) + if let EmailType::List(uid) = item { + for principal in &self.principals { + if principal.id == *uid { + if let Some(addr) = principal.emails.first() { + result.push(addr.clone()) + } + break; } } } diff --git a/crates/directory/src/memory/mod.rs b/crates/directory/src/backend/memory/mod.rs similarity index 85% rename from crates/directory/src/memory/mod.rs rename to crates/directory/src/backend/memory/mod.rs index 916333a8..dded7716 100644 --- a/crates/directory/src/memory/mod.rs +++ b/crates/directory/src/backend/memory/mod.rs @@ -30,16 +30,16 @@ pub mod lookup; #[derive(Default, Debug)] pub struct MemoryDirectory { - principals: AHashMap, - emails_to_names: AHashMap>, - names_to_email: AHashMap>, + principals: Vec, + emails_to_ids: AHashMap>, + names_to_ids: AHashMap, domains: AHashSet, opt: DirectoryOptions, } #[derive(Debug)] enum EmailType { - Primary(String), - Alias(String), - List(String), + Primary(u32), + Alias(u32), + List(u32), } diff --git a/crates/directory/src/backend/mod.rs b/crates/directory/src/backend/mod.rs new file mode 100644 index 00000000..f0388b1b --- /dev/null +++ b/crates/directory/src/backend/mod.rs @@ -0,0 +1,29 @@ +/* + * 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. +*/ + +pub mod imap; +pub mod internal; +pub mod ldap; +pub mod memory; +pub mod smtp; +pub mod sql; diff --git a/crates/directory/src/smtp/config.rs b/crates/directory/src/backend/smtp/config.rs similarity index 95% rename from crates/directory/src/smtp/config.rs rename to crates/directory/src/backend/smtp/config.rs index c0fd3835..6accb4b1 100644 --- a/crates/directory/src/smtp/config.rs +++ b/crates/directory/src/backend/smtp/config.rs @@ -26,9 +26,9 @@ use std::sync::Arc; use mail_send::{smtp::tls::build_tls_connector, SmtpClientBuilder}; use utils::config::{utils::AsKey, Config}; -use crate::{cache::CachedDirectory, config::build_pool, smtp::SmtpConnectionManager, Directory}; +use crate::{cache::CachedDirectory, config::build_pool, Directory}; -use super::SmtpDirectory; +use super::{SmtpConnectionManager, SmtpDirectory}; impl SmtpDirectory { pub fn from_config( diff --git a/crates/directory/src/smtp/lookup.rs b/crates/directory/src/backend/smtp/lookup.rs similarity index 84% rename from crates/directory/src/smtp/lookup.rs rename to crates/directory/src/backend/smtp/lookup.rs index 8ff24380..80330abd 100644 --- a/crates/directory/src/smtp/lookup.rs +++ b/crates/directory/src/backend/smtp/lookup.rs @@ -23,30 +23,24 @@ use mail_send::{smtp::AssertReply, Credentials}; use smtp_proto::Severity; +use store::Store; -use crate::{Directory, DirectoryError, Principal}; +use crate::{Directory, DirectoryError, Principal, QueryBy, QueryType}; use super::{SmtpClient, SmtpDirectory}; #[async_trait::async_trait] impl Directory for SmtpDirectory { - async fn authenticate( - &self, - credentials: &Credentials, - ) -> crate::Result> { - self.pool.get().await?.authenticate(credentials).await + async fn query(&self, query: QueryBy<'_>) -> crate::Result> { + if let QueryType::Credentials(credentials) = query.t { + self.pool.get().await?.authenticate(credentials).await + } else { + Err(DirectoryError::unsupported("smtp", "query")) + } } - async fn principal(&self, _name: &str) -> crate::Result> { - Err(DirectoryError::unsupported("smtp", "principal")) - } - - async fn emails_by_name(&self, _: &str) -> crate::Result> { - Err(DirectoryError::unsupported("smtp", "emails_by_name")) - } - - async fn names_by_email(&self, _address: &str) -> crate::Result> { - Err(DirectoryError::unsupported("smtp", "names_by_email")) + async fn email_to_ids(&self, _address: &str, _store: &Store) -> crate::Result> { + Err(DirectoryError::unsupported("smtp", "email_to_ids")) } async fn rcpt(&self, address: &str) -> crate::Result { diff --git a/crates/directory/src/smtp/mod.rs b/crates/directory/src/backend/smtp/mod.rs similarity index 100% rename from crates/directory/src/smtp/mod.rs rename to crates/directory/src/backend/smtp/mod.rs diff --git a/crates/directory/src/smtp/pool.rs b/crates/directory/src/backend/smtp/pool.rs similarity index 100% rename from crates/directory/src/smtp/pool.rs rename to crates/directory/src/backend/smtp/pool.rs diff --git a/crates/directory/src/sql/config.rs b/crates/directory/src/backend/sql/config.rs similarity index 95% rename from crates/directory/src/sql/config.rs rename to crates/directory/src/backend/sql/config.rs index fb758692..2215eb0a 100644 --- a/crates/directory/src/sql/config.rs +++ b/crates/directory/src/backend/sql/config.rs @@ -47,10 +47,6 @@ impl SqlDirectory { .clone(); let mut mappings = SqlMappings { - column_name: config - .value((&prefix, "columns.name")) - .unwrap_or_default() - .to_string(), column_description: config .value((&prefix, "columns.description")) .unwrap_or_default() diff --git a/crates/directory/src/sql/lookup.rs b/crates/directory/src/backend/sql/lookup.rs similarity index 51% rename from crates/directory/src/sql/lookup.rs rename to crates/directory/src/backend/sql/lookup.rs index 0a23bb51..51706cad 100644 --- a/crates/directory/src/sql/lookup.rs +++ b/crates/directory/src/backend/sql/lookup.rs @@ -22,73 +22,124 @@ */ use mail_send::Credentials; -use store::{NamedRows, Rows, Value}; +use store::{NamedRows, Rows, Store, Value}; -use crate::{Directory, Principal, Type}; +use crate::{ + backend::internal::manage::ManageDirectory, Directory, Principal, QueryBy, QueryType, Type, +}; use super::{SqlDirectory, SqlMappings}; #[async_trait::async_trait] impl Directory for SqlDirectory { - async fn authenticate( - &self, - credentials: &Credentials, - ) -> crate::Result> { - let (username, secret) = match credentials { - Credentials::Plain { username, secret } => (username, secret), - Credentials::OAuthBearer { token } => (token, token), - Credentials::XOauth2 { username, secret } => (username, secret), + 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) => { + 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? { + account_name = username; + } else { + return Ok(None); + } + account_id = Some(uid); + + self.store + .query::( + &self.mappings.query_name, + vec![account_name.clone().into()], + ) + .await? + } + QueryType::Credentials(credentials) => { + let (username, secret_) = match credentials { + Credentials::Plain { username, secret } => (username, secret), + Credentials::OAuthBearer { token } => (token, token), + Credentials::XOauth2 { username, secret } => (username, secret), + }; + account_name = username.to_string(); + secret = secret_.into(); + + self.store + .query::(&self.mappings.query_name, vec![username.into()]) + .await? + } }; - match self.principal(username).await { - Ok(Some(principal)) if principal.verify_secret(secret).await => Ok(Some(principal)), - Ok(_) => Ok(None), - Err(err) => Err(err), + if result.rows.is_empty() { + return Ok(None); } - } - async fn principal(&self, name: &str) -> crate::Result> { - let result = self - .store - .query::(&self.mappings.query_name, vec![name.into()]) - .await?; - if !result.rows.is_empty() { - // Map row to principal - let mut principal = self.mappings.row_to_principal(result)?; + // Map row to principal + let mut principal = self.mappings.row_to_principal(result)?; + // Validate password + if let Some(secret) = secret { + if !principal.verify_secret(secret).await { + tracing::debug!( + context = "directory", + event = "invalid_password", + protocol = "sql", + account = account_name, + "Invalid password for account" + ); + return Ok(None); + } + } + + // 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?; + } + principal.name = account_name; + + if by.has_store() { // Obtain members - principal.member_of = self - .store - .query::(&self.mappings.query_members, vec![name.into()]) - .await? - .into(); - - // Check whether the user is a superuser - if let Some(idx) = principal - .member_of - .iter() - .position(|group| group.eq_ignore_ascii_case(&self.opt.superuser_group)) - { - principal.member_of.swap_remove(idx); - principal.typ = Type::Superuser; + if !self.mappings.query_members.is_empty() { + for row in self + .store + .query::( + &self.mappings.query_members, + vec![principal.name.clone().into()], + ) + .await? + .rows + { + if let Some(Value::Text(account_id)) = row.values.first() { + principal.member_of.push(by.account_id(account_id).await?); + } + } } - Ok(Some(principal)) - } else { - Ok(None) + // Obtain emails + if !self.mappings.query_emails.is_empty() { + principal.emails = self + .store + .query::( + &self.mappings.query_emails, + vec![principal.name.clone().into()], + ) + .await? + .into(); + } } + + Ok(Some(principal)) } - async fn emails_by_name(&self, name: &str) -> crate::Result> { - self.store - .query::(&self.mappings.query_emails, vec![name.into()]) - .await - .map(Into::into) - .map_err(Into::into) - } - - async fn names_by_email(&self, address: &str) -> crate::Result> { - let ids = self + async fn email_to_ids(&self, address: &str, store: &Store) -> crate::Result> { + let mut names = self .store .query::( &self.mappings.query_recipients, @@ -101,17 +152,26 @@ impl Directory for SqlDirectory { ) .await?; - if !ids.rows.is_empty() { - Ok(ids.into()) - } else if let Some(address) = self.opt.catch_all.to_catch_all(address) { - self.store - .query::(&self.mappings.query_recipients, vec![address.into()]) - .await - .map(Into::into) - .map_err(Into::into) - } else { - Ok(vec![]) + if names.rows.is_empty() { + if let Some(address) = self.opt.catch_all.to_catch_all(address) { + names = self + .store + .query::(&self.mappings.query_recipients, vec![address.into()]) + .await?; + } else { + return Ok(vec![]); + } } + + let mut ids = Vec::with_capacity(names.rows.len()); + + for row in names.rows { + if let Some(Value::Text(name)) = row.values.first() { + ids.push(store.get_or_create_account_id(name).await?); + } + } + + Ok(ids) } async fn rcpt(&self, address: &str) -> crate::Result { @@ -185,11 +245,10 @@ impl Directory for SqlDirectory { impl SqlMappings { pub fn row_to_principal(&self, rows: NamedRows) -> crate::Result { let mut principal = Principal::default(); + if let Some(row) = rows.rows.into_iter().next() { for (name, value) in rows.names.into_iter().zip(row.values) { - if name.eq_ignore_ascii_case(&self.column_name) { - principal.name = value.into_string(); - } else if name.eq_ignore_ascii_case(&self.column_secret) { + if name.eq_ignore_ascii_case(&self.column_secret) { if let Value::Text(secret) = value { principal.secrets.push(secret.into_owned()); } @@ -197,6 +256,7 @@ impl SqlMappings { match value.to_str().as_ref() { "individual" | "person" | "user" => principal.typ = Type::Individual, "group" => principal.typ = Type::Group, + "admin" | "superuser" | "administrator" => principal.typ = Type::Superuser, _ => (), } } else if name.eq_ignore_ascii_case(&self.column_description) { diff --git a/crates/directory/src/sql/mod.rs b/crates/directory/src/backend/sql/mod.rs similarity index 98% rename from crates/directory/src/sql/mod.rs rename to crates/directory/src/backend/sql/mod.rs index 956ab768..001b5e6f 100644 --- a/crates/directory/src/sql/mod.rs +++ b/crates/directory/src/backend/sql/mod.rs @@ -43,7 +43,6 @@ pub(crate) struct SqlMappings { query_domains: String, query_verify: String, query_expand: String, - column_name: String, column_description: String, column_secret: String, column_quota: String, diff --git a/crates/directory/src/cache/lookup.rs b/crates/directory/src/cache/lookup.rs index 3a2c6088..cc31330d 100644 --- a/crates/directory/src/cache/lookup.rs +++ b/crates/directory/src/cache/lookup.rs @@ -21,31 +21,20 @@ * for more details. */ -use mail_send::Credentials; +use store::Store; -use crate::{Directory, Principal}; +use crate::{Directory, Principal, QueryBy}; use super::CachedDirectory; #[async_trait::async_trait] impl Directory for CachedDirectory { - async fn authenticate( - &self, - credentials: &Credentials, - ) -> crate::Result> { - self.inner.authenticate(credentials).await + async fn query(&self, by: QueryBy<'_>) -> crate::Result> { + self.inner.query(by).await } - async fn principal(&self, name: &str) -> crate::Result> { - self.inner.principal(name).await - } - - async fn emails_by_name(&self, name: &str) -> crate::Result> { - self.inner.emails_by_name(name).await - } - - async fn names_by_email(&self, address: &str) -> crate::Result> { - self.inner.names_by_email(address).await + async fn email_to_ids(&self, address: &str, store: &Store) -> crate::Result> { + self.inner.email_to_ids(address, store).await } async fn rcpt(&self, address: &str) -> crate::Result { diff --git a/crates/directory/src/config.rs b/crates/directory/src/config.rs index a3551185..e62e6d14 100644 --- a/crates/directory/src/config.rs +++ b/crates/directory/src/config.rs @@ -36,8 +36,11 @@ use utils::config::{ use ahash::AHashMap; use crate::{ - imap::ImapDirectory, ldap::LdapDirectory, memory::MemoryDirectory, smtp::SmtpDirectory, - sql::SqlDirectory, AddressMapping, Directories, DirectoryOptions, + backend::{ + imap::ImapDirectory, ldap::LdapDirectory, memory::MemoryDirectory, smtp::SmtpDirectory, + sql::SqlDirectory, + }, + AddressMapping, Directories, DirectoryOptions, }; pub trait ConfigDirectory { @@ -49,6 +52,7 @@ impl ConfigDirectory for Config { let mut config = Directories { directories: AHashMap::new(), }; + for id in self.sub_keys("directory") { // Parse directory let protocol = self.value_require(("directory", id, "type"))?; @@ -78,10 +82,6 @@ impl DirectoryOptions { Ok(DirectoryOptions { catch_all: AddressMapping::from_config(config, (&key, "options.catch-all"))?, subaddressing: AddressMapping::from_config(config, (&key, "options.subaddressing"))?, - superuser_group: config - .value((&key, "options.superuser-group")) - .unwrap_or("superusers") - .to_string(), }) } } diff --git a/crates/directory/src/imap/lookup.rs b/crates/directory/src/imap/lookup.rs deleted file mode 100644 index 49be21ab..00000000 --- a/crates/directory/src/imap/lookup.rs +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright (c) 2023 Stalwart Labs Ltd. - * - * This file is part of Stalwart Mail Server. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of - * the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * in the LICENSE file at the top-level directory of this distribution. - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * - * 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 smtp_proto::{AUTH_CRAM_MD5, AUTH_LOGIN, AUTH_OAUTHBEARER, AUTH_PLAIN, AUTH_XOAUTH2}; - -use crate::{Directory, DirectoryError, Principal}; - -use super::{ImapDirectory, ImapError}; - -#[async_trait::async_trait] -impl Directory for ImapDirectory { - async fn authenticate( - &self, - credentials: &Credentials, - ) -> crate::Result> { - let mut client = self.pool.get().await?; - let mechanism = match credentials { - Credentials::Plain { .. } - if (client.mechanisms & (AUTH_PLAIN | AUTH_LOGIN | AUTH_CRAM_MD5)) != 0 => - { - if client.mechanisms & AUTH_CRAM_MD5 != 0 { - AUTH_CRAM_MD5 - } else if client.mechanisms & AUTH_PLAIN != 0 { - AUTH_PLAIN - } else { - AUTH_LOGIN - } - } - Credentials::OAuthBearer { .. } if client.mechanisms & AUTH_OAUTHBEARER != 0 => { - AUTH_OAUTHBEARER - } - Credentials::XOauth2 { .. } if client.mechanisms & AUTH_XOAUTH2 != 0 => AUTH_XOAUTH2, - _ => { - tracing::warn!( - context = "remote", - event = "error", - protocol = "imap", - "IMAP server does not offer any supported auth mechanisms.", - ); - return Ok(None); - } - }; - - match client.authenticate(mechanism, credentials).await { - Ok(_) => { - client.is_valid = false; - Ok(Some(Principal::default())) - } - Err(err) => match &err { - ImapError::AuthenticationFailed => Ok(None), - _ => Err(err.into()), - }, - } - } - - async fn principal(&self, _name: &str) -> crate::Result> { - Err(DirectoryError::unsupported("imap", "principal")) - } - - async fn emails_by_name(&self, _: &str) -> crate::Result> { - Err(DirectoryError::unsupported("imap", "emails_by_name")) - } - - async fn names_by_email(&self, _address: &str) -> crate::Result> { - Err(DirectoryError::unsupported("imap", "names_by_email")) - } - - async fn rcpt(&self, _address: &str) -> crate::Result { - Err(DirectoryError::unsupported("imap", "rcpt")) - } - - async fn vrfy(&self, _address: &str) -> crate::Result> { - Err(DirectoryError::unsupported("imap", "vrfy")) - } - - async fn expn(&self, _address: &str) -> crate::Result> { - Err(DirectoryError::unsupported("imap", "expn")) - } - - async fn is_local_domain(&self, domain: &str) -> crate::Result { - Ok(self.domains.contains(domain)) - } -} diff --git a/crates/directory/src/ldap/lookup.rs b/crates/directory/src/ldap/lookup.rs deleted file mode 100644 index 0cc636f0..00000000 --- a/crates/directory/src/ldap/lookup.rs +++ /dev/null @@ -1,399 +0,0 @@ -/* - * Copyright (c) 2023 Stalwart Labs Ltd. - * - * This file is part of Stalwart Mail Server. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of - * the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * in the LICENSE file at the top-level directory of this distribution. - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * - * 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 ldap3::{Ldap, LdapConnAsync, LdapError, ResultEntry, Scope, SearchEntry}; -use mail_send::Credentials; - -use crate::{Directory, DirectoryError, Principal, Type}; - -use super::{LdapDirectory, LdapMappings}; - -#[async_trait::async_trait] -impl Directory for LdapDirectory { - async fn authenticate( - &self, - credentials: &Credentials, - ) -> crate::Result> { - let (username, secret) = match credentials { - Credentials::Plain { username, secret } => (username, secret), - Credentials::OAuthBearer { token } => (token, token), - Credentials::XOauth2 { username, secret } => (username, secret), - }; - - if let Some(auth_bind) = &self.auth_bind { - let (conn, mut ldap) = LdapConnAsync::with_settings( - self.pool.manager().settings.clone(), - &self.pool.manager().address, - ) - .await?; - - ldap3::drive!(conn); - - ldap.simple_bind(&auth_bind.build(username), secret).await?; - - match self - .find_principal(&mut ldap, &self.mappings.filter_name.build(username)) - .await - { - Err(DirectoryError::Ldap(LdapError::LdapResult { result })) - if [49, 50].contains(&result.rc) => - { - Ok(None) - } - result => result, - } - } else { - let mut conn = self.pool.get().await?; - match self - .find_principal(&mut conn, &self.mappings.filter_name.build(username)) - .await - { - Ok(Some(principal)) => { - if principal.verify_secret(secret).await { - Ok(Some(principal)) - } else { - Ok(None) - } - } - result => result, - } - } - } - - async fn principal(&self, name: &str) -> crate::Result> { - let mut conn = self.pool.get().await?; - self.find_principal(&mut conn, &self.mappings.filter_name.build(name)) - .await - } - - async fn emails_by_name(&self, name: &str) -> crate::Result> { - let (rs, _res) = self - .pool - .get() - .await? - .search( - &self.mappings.base_dn, - Scope::Subtree, - &self.mappings.filter_name.build(name), - &self.mappings.attrs_email, - ) - .await? - .success()?; - - let mut emails = Vec::new(); - for entry in rs { - let entry = SearchEntry::construct(entry); - for attr in &self.mappings.attrs_email { - if let Some(values) = entry.attrs.get(attr) { - for email in values { - if !email.is_empty() { - emails.push(email.to_string()); - } - } - } - } - } - - Ok(emails) - } - - async fn names_by_email(&self, address: &str) -> crate::Result> { - let names = self - .pool - .get() - .await? - .search( - &self.mappings.base_dn, - Scope::Subtree, - &self - .mappings - .filter_email - .build(self.opt.subaddressing.to_subaddress(address).as_ref()), - &self.mappings.attr_name, - ) - .await? - .success() - .map(|(rs, _res)| self.extract_names(rs))?; - - if !names.is_empty() { - Ok(names) - } else if let Some(address) = self.opt.catch_all.to_catch_all(address) { - self.pool - .get() - .await? - .search( - &self.mappings.base_dn, - Scope::Subtree, - &self.mappings.filter_email.build(address.as_ref()), - &self.mappings.attr_name, - ) - .await? - .success() - .map(|(rs, _res)| self.extract_names(rs)) - .map_err(|e| e.into()) - } else { - Ok(names) - } - } - - async fn rcpt(&self, address: &str) -> crate::Result { - match self - .pool - .get() - .await? - .streaming_search( - &self.mappings.base_dn, - Scope::Subtree, - &self - .mappings - .filter_email - .build(self.opt.subaddressing.to_subaddress(address).as_ref()), - &self.mappings.attr_email_address, - ) - .await? - .next() - .await - { - Ok(Some(_)) => Ok(true), - Ok(None) => { - if let Some(address) = self.opt.catch_all.to_catch_all(address) { - self.pool - .get() - .await? - .streaming_search( - &self.mappings.base_dn, - Scope::Subtree, - &self.mappings.filter_email.build(address.as_ref()), - &self.mappings.attr_email_address, - ) - .await? - .next() - .await - .map(|entry| entry.is_some()) - .map_err(|e| e.into()) - } else { - Ok(false) - } - } - - Err(e) => Err(e.into()), - } - } - - async fn vrfy(&self, address: &str) -> crate::Result> { - let mut stream = self - .pool - .get() - .await? - .streaming_search( - &self.mappings.base_dn, - Scope::Subtree, - &self - .mappings - .filter_verify - .build(self.opt.subaddressing.to_subaddress(address).as_ref()), - &self.mappings.attr_email_address, - ) - .await?; - - let mut emails = Vec::new(); - while let Some(entry) = stream.next().await? { - let entry = SearchEntry::construct(entry); - for attr in &self.mappings.attr_email_address { - if let Some(values) = entry.attrs.get(attr) { - for email in values { - if !email.is_empty() { - emails.push(email.to_string()); - } - } - } - } - } - - Ok(emails) - } - - async fn expn(&self, address: &str) -> crate::Result> { - let mut stream = self - .pool - .get() - .await? - .streaming_search( - &self.mappings.base_dn, - Scope::Subtree, - &self - .mappings - .filter_expand - .build(self.opt.subaddressing.to_subaddress(address).as_ref()), - &self.mappings.attr_email_address, - ) - .await?; - - let mut emails = Vec::new(); - while let Some(entry) = stream.next().await? { - let entry = SearchEntry::construct(entry); - for attr in &self.mappings.attr_email_address { - if let Some(values) = entry.attrs.get(attr) { - for email in values { - if !email.is_empty() { - emails.push(email.to_string()); - } - } - } - } - } - - Ok(emails) - } - - async fn is_local_domain(&self, domain: &str) -> crate::Result { - self.pool - .get() - .await? - .streaming_search( - &self.mappings.base_dn, - Scope::Subtree, - &self.mappings.filter_domains.build(domain), - Vec::::new(), - ) - .await? - .next() - .await - .map(|entry| entry.is_some()) - .map_err(|e| e.into()) - } -} - -impl LdapDirectory { - async fn find_principal( - &self, - conn: &mut Ldap, - filter: &str, - ) -> crate::Result> { - let (rs, _res) = conn - .search( - &self.mappings.base_dn, - Scope::Subtree, - filter, - &self.mappings.attrs_principal, - ) - .await? - .success()?; - - if let Some(mut principal) = rs.into_iter().next().map(|entry| { - self.mappings - .entry_to_principal(SearchEntry::construct(entry)) - }) { - // Map groups - if !principal.member_of.is_empty() { - let mut names = Vec::with_capacity(principal.member_of.len()); - for group in principal.member_of { - if group.contains('=') { - let (rs, _res) = conn - .search( - &group, - Scope::Base, - "objectClass=*", - &self.mappings.attr_name, - ) - .await? - .success()?; - 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 !group.is_empty() { - if !group - .eq_ignore_ascii_case(&self.opt.superuser_group) - { - names.push(group.to_string()); - } else { - principal.typ = Type::Superuser; - } - break 'outer; - } - } - } - } - } - } else if !group.eq_ignore_ascii_case(&self.opt.superuser_group) { - names.push(group); - } else { - principal.typ = Type::Superuser; - } - } - principal.member_of = names; - } - Ok(Some(principal)) - } else { - Ok(None) - } - } - - fn extract_names(&self, rs: Vec) -> Vec { - let mut names = Vec::with_capacity(rs.len()); - for entry in rs { - let entry = SearchEntry::construct(entry); - 'outer: for attr in &self.mappings.attr_name { - if let Some(value) = entry.attrs.get(attr).and_then(|v| v.first()) { - if !value.is_empty() { - names.push(value.to_string()); - break 'outer; - } - } - } - } - names - } -} - -impl LdapMappings { - pub fn entry_to_principal(&self, entry: SearchEntry) -> Principal { - let mut principal = Principal::default(); - for (attr, value) in entry.attrs { - if self.attr_name.contains(&attr) { - principal.name = value.into_iter().next().unwrap_or_default(); - } else if self.attr_secret.contains(&attr) { - principal.secrets.extend(value); - } else if let Some(idx) = self.attr_description.iter().position(|a| a == &attr) { - if principal.description.is_none() || idx == 0 { - principal.description = value.into_iter().next(); - } - } else if self.attr_groups.contains(&attr) { - 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; - } - } else if attr.eq_ignore_ascii_case("objectClass") { - if value.contains(&self.obj_user) { - principal.typ = Type::Individual; - } else if value.contains(&self.obj_group) { - principal.typ = Type::Group; - } - } - } - - principal - } -} diff --git a/crates/directory/src/lib.rs b/crates/directory/src/lib.rs index cfbe97f1..f5a59032 100644 --- a/crates/directory/src/lib.rs +++ b/crates/directory/src/lib.rs @@ -24,46 +24,46 @@ use std::{borrow::Cow, fmt::Debug, sync::Arc}; use ahash::AHashMap; +use backend::imap::ImapError; use deadpool::managed::PoolError; -use imap::ImapError; use ldap3::LdapError; use mail_send::Credentials; +use store::Store; use utils::config::DynValue; +pub mod backend; pub mod cache; pub mod config; -pub mod imap; -pub mod ldap; -pub mod memory; pub mod secret; -pub mod smtp; -pub mod sql; #[derive(Debug, Default, Clone, PartialEq, Eq)] pub struct Principal { + pub id: u32, + pub typ: Type, + pub quota: u32, pub name: String, pub secrets: Vec, - pub typ: Type, + pub emails: Vec, + pub member_of: Vec, pub description: Option, - pub quota: u32, - pub member_of: Vec, } #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] pub enum Type { - Individual, - Group, - Resource, - Location, + Individual = 0, + Group = 1, + Resource = 2, + Location = 3, + Superuser = 4, + List = 5, #[default] - Other, - Superuser, + Other = 6, } #[derive(Debug)] pub enum DirectoryError { Ldap(LdapError), - Sql(store::Error), + Store(store::Error), Imap(ImapError), Smtp(mail_send::Error), Pool(String), @@ -73,16 +73,26 @@ pub enum DirectoryError { #[async_trait::async_trait] pub trait Directory: Sync + Send { - async fn authenticate(&self, credentials: &Credentials) -> Result>; - async fn principal(&self, name: &str) -> Result>; - async fn emails_by_name(&self, name: &str) -> Result>; - async fn names_by_email(&self, email: &str) -> Result>; + async fn query(&self, by: QueryBy<'_>) -> Result>; + async fn email_to_ids(&self, email: &str, store: &Store) -> Result>; + async fn is_local_domain(&self, domain: &str) -> crate::Result; async fn rcpt(&self, address: &str) -> crate::Result; async fn vrfy(&self, address: &str) -> Result>; async fn expn(&self, address: &str) -> Result>; } +pub enum QueryType<'x> { + Name(&'x str), + Id(u32), + Credentials(&'x Credentials), +} + +pub struct QueryBy<'x> { + pub t: QueryType<'x>, + pub store: Option<&'x Store>, +} + impl Principal { pub fn name(&self) -> &str { &self.name @@ -111,6 +121,7 @@ impl Type { Self::Resource => "resource", Self::Location => "location", Self::Other => "other", + Self::List => "list", } } } @@ -119,7 +130,6 @@ impl Type { struct DirectoryOptions { catch_all: AddressMapping, subaddressing: AddressMapping, - superuser_group: String, } #[derive(Debug, Default)] @@ -194,7 +204,7 @@ impl From for DirectoryError { "SQL directory error" ); - DirectoryError::Sql(error) + DirectoryError::Store(error) } } diff --git a/crates/directory/src/memory/config.rs b/crates/directory/src/memory/config.rs deleted file mode 100644 index 9f1d7617..00000000 --- a/crates/directory/src/memory/config.rs +++ /dev/null @@ -1,154 +0,0 @@ -/* - * Copyright (c) 2023 Stalwart Labs Ltd. - * - * This file is part of Stalwart Mail Server. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of - * the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * in the LICENSE file at the top-level directory of this distribution. - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * - * You can be released from the requirements of the AGPLv3 license by - * purchasing a commercial license. Please contact licensing@stalw.art - * for more details. -*/ - -use std::sync::Arc; - -use utils::config::{utils::AsKey, Config}; - -use crate::{Directory, DirectoryOptions, Principal, Type}; - -use super::{EmailType, MemoryDirectory}; - -impl MemoryDirectory { - pub fn from_config( - config: &Config, - prefix: impl AsKey, - ) -> utils::config::Result> { - let prefix = prefix.as_key(); - let mut directory = MemoryDirectory { - opt: DirectoryOptions::from_config(config, prefix.clone())?, - ..Default::default() - }; - - for lookup_id in config.sub_keys((prefix.as_str(), "users")) { - let name = config - .value_require((prefix.as_str(), "users", lookup_id, "name"))? - .to_string(); - let mut typ = Type::Individual; - let mut member_of = Vec::new(); - - for (_, group) in config.values((prefix.as_str(), "users", lookup_id, "member-of")) { - if !group.eq_ignore_ascii_case(&directory.opt.superuser_group) { - member_of.push(group.to_string()); - } else { - typ = Type::Superuser; - } - } - - directory.principals.insert( - name.clone(), - Principal { - name: name.clone(), - secrets: config - .values((prefix.as_str(), "users", lookup_id, "secret")) - .map(|(_, v)| v.to_string()) - .collect(), - typ, - description: config - .value((prefix.as_str(), "users", lookup_id, "description")) - .map(|v| v.to_string()), - quota: config - .property((prefix.as_str(), "users", lookup_id, "quota"))? - .unwrap_or(0), - member_of, - }, - ); - - directory.parse_emails(config, (prefix.as_str(), "users", lookup_id), name)?; - } - - for lookup_id in config.sub_keys((prefix.as_str(), "groups")) { - let name = config - .value_require((prefix.as_str(), "groups", lookup_id, "name"))? - .to_string(); - directory.principals.insert( - name.clone(), - Principal { - name: name.clone(), - secrets: vec![], - typ: Type::Group, - description: config - .value((prefix.as_str(), "groups", lookup_id, "description")) - .map(|v| v.to_string()), - quota: config - .property((prefix.as_str(), "groups", lookup_id, "quota"))? - .unwrap_or(0), - member_of: config - .values((prefix.as_str(), "groups", lookup_id, "member-of")) - .map(|(_, v)| v.to_string()) - .collect(), - }, - ); - - directory.parse_emails(config, (prefix.as_str(), "groups", lookup_id), name)?; - } - - Ok(Arc::new(directory)) - } -} - -impl MemoryDirectory { - fn parse_emails( - &mut self, - config: &Config, - prefix: impl AsKey, - name: String, - ) -> utils::config::Result<()> { - let prefix = prefix.as_key(); - let mut emails = Vec::new(); - - for (pos, (_, email)) in config.values((prefix.as_str(), "email")).enumerate() { - self.emails_to_names - .entry(email.to_string()) - .or_default() - .push(if pos > 0 { - EmailType::Alias(name.clone()) - } else { - EmailType::Primary(name.clone()) - }); - - if let Some((_, domain)) = email.rsplit_once('@') { - self.domains.insert(domain.to_lowercase()); - } - - emails.push(if pos > 0 { - EmailType::Alias(email.to_lowercase()) - } else { - EmailType::Primary(email.to_lowercase()) - }); - } - for (_, email) in config.values((prefix.as_str(), "email-list")) { - self.emails_to_names - .entry(email.to_lowercase()) - .or_default() - .push(EmailType::List(name.clone())); - if let Some((_, domain)) = email.rsplit_once('@') { - self.domains.insert(domain.to_lowercase()); - } - emails.push(EmailType::List(email.to_lowercase())); - } - - self.names_to_email.insert(name, emails); - Ok(()) - } -} diff --git a/crates/imap/src/core/mailbox.rs b/crates/imap/src/core/mailbox.rs index a20ae26e..69d45f96 100644 --- a/crates/imap/src/core/mailbox.rs +++ b/crates/imap/src/core/mailbox.rs @@ -1,6 +1,7 @@ use std::{collections::BTreeMap, sync::atomic::Ordering}; use ahash::AHashMap; +use directory::QueryBy; use imap_proto::{protocol::list::Attribute, StatusResponse}; use jmap::{ auth::{acl::EffectiveAcl, AccessToken}, @@ -50,9 +51,11 @@ impl SessionData { session.imap.name_shared, session .jmap - .get_account_name(account_id) + .directory + .query(QueryBy::id(account_id).with_store(&session.jmap.store)) .await .unwrap_or_default() + .map(|p| p.name) .unwrap_or_else(|| Id::from(account_id).to_string()) ) .into(), @@ -316,9 +319,11 @@ impl SessionData { "{}/{}", self.imap.name_shared, self.jmap - .get_account_name(account_id) + .directory + .query(QueryBy::id(account_id).with_store(&self.jmap.store)) .await .unwrap_or_default() + .map(|p| p.name) .unwrap_or_else(|| Id::from(account_id).to_string()) ); match self @@ -401,9 +406,11 @@ impl SessionData { "{}/{}", self.imap.name_shared, self.jmap - .get_account_name(account_id) + .directory + .query(QueryBy::id(account_id).with_store(&self.jmap.store)) .await .unwrap_or_default() + .map(|p| p.name) .unwrap_or_else(|| Id::from(account_id).to_string()) ) .into() diff --git a/crates/imap/src/op/acl.rs b/crates/imap/src/op/acl.rs index 3a8b6074..555527ec 100644 --- a/crates/imap/src/op/acl.rs +++ b/crates/imap/src/op/acl.rs @@ -23,6 +23,7 @@ use std::sync::Arc; +use directory::QueryBy; use imap_proto::{ protocol::acl::{ Arguments, GetAclResponse, ListRightsResponse, ModRightsOp, MyRightsResponse, Rights, @@ -74,9 +75,14 @@ impl Session { { if let Some(account_name) = data .jmap - .get_account_name(id.document_id()) + .directory + .query( + QueryBy::id(id.document_id()) + .with_store(&data.jmap.store), + ) .await .unwrap_or_default() + .map(|p| p.name) { let mut rights = Vec::new(); @@ -245,23 +251,13 @@ impl Session { let (acl_account_id, id) = match data .jmap .directory - .principal(arguments.identifier.as_ref().unwrap()) + .query( + QueryBy::name(arguments.identifier.as_ref().unwrap()) + .with_store(&data.jmap.store), + ) .await { - Ok(Some(principal)) => { - match data.jmap.get_account_id(principal.name()).await { - Ok(account_id) => (account_id, Value::Id(Id::from(account_id))), - Err(_) => { - data.write_bytes( - StatusResponse::database_failure() - .with_tag(arguments.tag) - .into_bytes(), - ) - .await; - return; - } - } - } + Ok(Some(principal)) => (principal.id, Value::Id(Id::from(principal.id))), Ok(None) => { data.write_bytes( StatusResponse::no("Account does not exist") diff --git a/crates/install/src/main.rs b/crates/install/src/main.rs index 8c9c02da..48557674 100644 --- a/crates/install/src/main.rs +++ b/crates/install/src/main.rs @@ -749,7 +749,7 @@ fn create_databases(base_path: &Path, domain: Option<&str>) -> std::io::Result. - * - * 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 jmap_proto::types::collection::Collection; -use store::{write::BatchBuilder, Serialize}; - -use crate::{NamedKey, JMAP}; - -impl JMAP { - pub async fn delete_account(&self, account_name: &str, account_id: u32) -> store::Result<()> { - let test = true; - - // Unlink all account's blobs - self.store.blob_hash_unlink_account(account_id).await?; - - // Revoke ACLs - self.store.acl_revoke_all(account_id).await?; - - // Delete account data - self.store.purge_account(account_id).await?; - - // Remove FTS index - self.fts_store.remove_all(account_id).await?; - - // Delete account - let mut batch = BatchBuilder::new(); - batch - .with_account_id(u32::MAX) - .with_collection(Collection::Principal) - .clear(NamedKey::Name(account_name)) - .clear(NamedKey::Id::<&[u8]>(account_id)) - .clear(NamedKey::Quota::<&[u8]>(account_id)); - - self.store.write(batch.build()).await?; - - Ok(()) - } - - pub async fn rename_account( - &self, - new_account_name: &str, - account_name: &str, - account_id: u32, - ) -> store::Result<()> { - let mut batch = BatchBuilder::new(); - batch - .with_account_id(u32::MAX) - .with_collection(Collection::Principal) - .clear(NamedKey::Name(account_name)) - .set( - NamedKey::Id::<&[u8]>(account_id), - new_account_name.serialize(), - ) - .set(NamedKey::Name(new_account_name), account_id.serialize()); - self.store.write(batch.build()).await?; - Ok(()) - } -} diff --git a/crates/jmap/src/api/http.rs b/crates/jmap/src/api/http.rs index 17ee5bbe..b062b4eb 100644 --- a/crates/jmap/src/api/http.rs +++ b/crates/jmap/src/api/http.rs @@ -289,7 +289,14 @@ pub async fn parse_jmap_request( req.method(), ) { ("account", "delete", &Method::GET) => { - return if let Some(account_name) = path.next() { + 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())) @@ -316,10 +323,11 @@ pub async fn parse_jmap_request( "Expected account name", ) .into_http_response() - }; + };*/ } ("account", "rename", &Method::GET) => { - return if let (Some(account_name), Some(new_account_name)) = + todo!() + /*return if let (Some(account_name), Some(new_account_name)) = (path.next(), path.next()) { match ( @@ -362,7 +370,7 @@ pub async fn parse_jmap_request( "Expected old and new account names", ) .into_http_response() - }; + };*/ } ("blob", "purge", &Method::GET) => { return match jmap.store.purge_blobs(jmap.blob_store.clone()).await { diff --git a/crates/jmap/src/api/mod.rs b/crates/jmap/src/api/mod.rs index 927225d6..74dc6712 100644 --- a/crates/jmap/src/api/mod.rs +++ b/crates/jmap/src/api/mod.rs @@ -30,7 +30,6 @@ 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/request.rs b/crates/jmap/src/api/request.rs index aec30809..e817e2db 100644 --- a/crates/jmap/src/api/request.rs +++ b/crates/jmap/src/api/request.rs @@ -250,7 +250,7 @@ impl JMAP { set::RequestArguments::Identity => { access_token.assert_is_member(req.account_id)?; - self.identity_set(req, access_token).await?.into() + self.identity_set(req).await?.into() } set::RequestArguments::EmailSubmission(arguments) => { access_token.assert_is_member(req.account_id)?; diff --git a/crates/jmap/src/api/session.rs b/crates/jmap/src/api/session.rs index 305ab023..5797493b 100644 --- a/crates/jmap/src/api/session.rs +++ b/crates/jmap/src/api/session.rs @@ -23,6 +23,7 @@ use std::sync::Arc; +use directory::QueryBy; use jmap_proto::{ error::request::RequestError, request::capability::Capability, @@ -212,9 +213,11 @@ impl JMAP { session.add_account( (*id).into(), - self.get_account_name(*id) + self.directory + .query(QueryBy::id(*id).with_store(&self.store)) .await .unwrap_or_default() + .map(|p| p.name) .unwrap_or_else(|| Id::from(*id).to_string()), is_personal, is_readonly, diff --git a/crates/jmap/src/auth/acl.rs b/crates/jmap/src/auth/acl.rs index 5c3c9cb5..9585021f 100644 --- a/crates/jmap/src/auth/acl.rs +++ b/crates/jmap/src/auth/acl.rs @@ -21,6 +21,7 @@ * for more details. */ +use directory::QueryBy; use jmap_proto::{ error::{method::MethodError, set::SetError}, object::Object, @@ -376,13 +377,14 @@ impl JMAP { if let (Some(Value::Id(id)), Some(Value::UnsignedInt(acl_bits))) = (item.first(), item.last()) { - if let Some(account_name) = self - .get_account_name(id.document_id()) + if let Some(principal) = self + .directory + .query(QueryBy::id(id.document_id()).with_store(&self.store)) .await .unwrap_or_default() { acl_obj.append( - Property::_T(account_name), + Property::_T(principal.name), Bitmap::::from(*acl_bits) .map(|acl_item| Value::Text(acl_item.to_string())) .collect::>(), @@ -450,18 +452,13 @@ 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.principal(account_name).await { - Ok(Some(_)) => { - *item = Value::Id( - self.get_account_id(account_name) - .await - .map_err(|_| { - SetError::forbidden() - .with_property(Property::Acl) - .with_description("Temporary server failure during lookup") - })? - .into(), - ); + match self + .directory + .query(QueryBy::name(account_name).with_store(&self.store)) + .await + { + Ok(Some(principal)) => { + *item = Value::Id(principal.id.into()); } Ok(None) => { return Err(SetError::invalid_properties() diff --git a/crates/jmap/src/auth/authenticate.rs b/crates/jmap/src/auth/authenticate.rs index 8003b67d..96ad2d58 100644 --- a/crates/jmap/src/auth/authenticate.rs +++ b/crates/jmap/src/auth/authenticate.rs @@ -27,17 +27,14 @@ use std::{ time::Instant, }; +use directory::QueryBy; use hyper::header; -use jmap_proto::{ - error::{method::MethodError, request::RequestError}, - types::collection::Collection, -}; +use jmap_proto::error::request::RequestError; use mail_parser::decoders::base64::base64_decode; use mail_send::Credentials; -use store::{write::BatchBuilder, Serialize}; use utils::{listener::limiter::InFlight, map::ttl_dashmap::TtlMap}; -use crate::{NamedKey, JMAP}; +use crate::JMAP; use super::{rate_limit::RemoteAddress, AccessToken}; @@ -150,85 +147,6 @@ impl JMAP { } } - pub async fn try_get_account_id(&self, name: &str) -> Result, MethodError> { - self.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 get_account_id(&self, name: &str) -> Result { - let mut try_count = 0; - - loop { - // Try to obtain ID - if let Some(account_id) = self.try_get_account_id(name).await? { - return Ok(account_id); - } - - // Assign new ID - let account_id = self - .assign_document_id(u32::MAX, Collection::Principal) - .await?; - - // Write account ID - let mut batch = BatchBuilder::new(); - batch - .with_account_id(u32::MAX) - .with_collection(Collection::Principal) - .create_document(account_id) - .assert_value(NamedKey::Name(name), ()) - .set(NamedKey::Name(name), account_id.serialize()) - .set(NamedKey::Id::<&[u8]>(account_id), name.serialize()); - - match self.store.write(batch.build()).await { - Ok(_) => { - return Ok(account_id); - } - Err(store::Error::AssertValueFailed) if try_count < 3 => { - try_count += 1; - continue; - } - Err(err) => { - tracing::error!(event = "error", - context = "store", - error = ?err, - "Failed to generate account id"); - return Err(MethodError::ServerPartialFail); - } - } - } - } - - pub async fn map_member_of(&self, names: Vec) -> Result, MethodError> { - let mut ids = Vec::with_capacity(names.len()); - for name in names { - ids.push(self.get_account_id(&name).await?); - } - Ok(ids) - } - - pub async fn get_account_name(&self, account_id: u32) -> Result, MethodError> { - self.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 - }) - } - pub fn build_remote_addr( &self, req: &hyper::Request, @@ -254,60 +172,40 @@ impl JMAP { secret: &str, remote_addr: &RemoteAddress, ) -> Option { - let mut principal = match self + match self .directory - .authenticate(&Credentials::Plain { - username: username.to_string(), - secret: secret.to_string(), - }) - .await - { - Ok(Some(principal)) => principal, - Ok(None) => { - let _ = self.is_auth_allowed_hard(remote_addr); - return None; - } - Err(_) => { - return None; - } - }; - - if !principal.has_name() { - principal.name = username.to_string(); - } - // Obtain groups - if let (Ok(account_id), Ok(member_of)) = ( - self.get_account_id(&principal.name).await, - self.map_member_of(std::mem::take(&mut principal.member_of)) - .await, - ) { - // Create access token - self.update_access_token( - AccessToken::new(principal, account_id).with_member_of(member_of), + .query( + QueryBy::credentials(&Credentials::Plain { + username: username.to_string(), + secret: secret.to_string(), + }) + .with_store(&self.store), ) .await - } else { - None + { + Ok(Some(mut principal)) => { + if !principal.has_name() { + principal.name = username.to_string(); + } + + AccessToken::new(principal).into() + } + Ok(None) => { + let _ = self.is_auth_allowed_hard(remote_addr); + None + } + Err(_) => None, } } pub async fn get_access_token(&self, account_id: u32) -> Option { - let name = self.get_account_name(account_id).await.ok()??; - let mut principal = self.directory.principal(&name).await.ok()??; - - // Obtain groups - if let (Ok(account_id), Ok(member_of)) = ( - self.get_account_id(&principal.name).await, - self.map_member_of(std::mem::take(&mut principal.member_of)) - .await, - ) { - // Create access token - self.update_access_token( - AccessToken::new(principal, account_id).with_member_of(member_of), - ) - .await - } else { - None - } + // Create access token + self.update_access_token(AccessToken::new( + self.directory + .query(QueryBy::id(account_id).with_store(&self.store)) + .await + .ok()??, + )) + .await } } diff --git a/crates/jmap/src/auth/mod.rs b/crates/jmap/src/auth/mod.rs index 76c73d1c..ac30addd 100644 --- a/crates/jmap/src/auth/mod.rs +++ b/crates/jmap/src/auth/mod.rs @@ -56,10 +56,10 @@ pub struct AccessToken { } impl AccessToken { - pub fn new(principal: Principal, primary_id: u32) -> Self { + pub fn new(principal: Principal) -> Self { Self { - primary_id, - member_of: Vec::new(), + primary_id: principal.id, + member_of: principal.member_of, access_to: Vec::new(), name: principal.name, description: principal.description, @@ -68,10 +68,6 @@ impl AccessToken { } } - pub fn with_member_of(self, member_of: Vec) -> Self { - Self { member_of, ..self } - } - pub fn with_access_to(self, access_to: Vec<(u32, Bitmap)>) -> Self { Self { access_to, ..self } } diff --git a/crates/jmap/src/auth/oauth/token.rs b/crates/jmap/src/auth/oauth/token.rs index 43cf9231..fe42e1d6 100644 --- a/crates/jmap/src/auth/oauth/token.rs +++ b/crates/jmap/src/auth/oauth/token.rs @@ -23,6 +23,7 @@ use std::{sync::atomic, time::SystemTime}; +use directory::QueryBy; use hyper::StatusCode; use mail_builder::encoders::base64::base64_encode; use mail_parser::decoders::base64::base64_decode; @@ -179,14 +180,9 @@ impl JMAP { client_id: &str, with_refresh_token: bool, ) -> Result { - let account_name = self - .get_account_name(account_id) - .await - .map_err(|_| "Temporary lookup error")? - .ok_or("Account no longer exists")?; let password_hash = self .directory - .principal(&account_name) + .query(QueryBy::id(account_id).with_store(&self.store)) .await .map_err(|_| "Temporary lookup error")? .ok_or("Account no longer exists")? @@ -302,14 +298,10 @@ impl JMAP { } // Optain password hash - let account_name = self - .get_account_name(account_id) - .await - .map_err(|_| "Temporary lookup error")? - .ok_or("Account no longer exists")?; + let password_hash = self .directory - .principal(&account_name) + .query(QueryBy::id(account_id).with_store(&self.store)) .await .map_err(|_| "Temporary lookup error")? .ok_or("Account no longer exists")? diff --git a/crates/jmap/src/email/copy.rs b/crates/jmap/src/email/copy.rs index 57a488e9..8d4ce52e 100644 --- a/crates/jmap/src/email/copy.rs +++ b/crates/jmap/src/email/copy.rs @@ -48,14 +48,12 @@ use jmap_proto::{ }; use mail_parser::{parsers::fields::thread::thread_name, HeaderName, HeaderValue}; use store::{ - write::{BatchBuilder, F_BITMAP, F_VALUE}, + write::{BatchBuilder, ValueClass, F_BITMAP, F_VALUE}, BlobClass, }; use utils::map::vec_map::VecMap; -use crate::{ - auth::AccessToken, mailbox::UidMailbox, services::housekeeper::Event, Bincode, NamedKey, JMAP, -}; +use crate::{auth::AccessToken, mailbox::UidMailbox, services::housekeeper::Event, Bincode, JMAP}; use super::{ index::{EmailIndexBuilder, TrimTextValue, MAX_SORT_FIELD_LENGTH}, @@ -415,11 +413,7 @@ impl JMAP { .value(Property::Keywords, keywords, F_VALUE | F_BITMAP) .value(Property::Cid, changes.change_id, F_VALUE) .set( - NamedKey::IndexEmail::<&[u8]> { - account_id, - document_id: message_id, - seq: self.generate_snowflake_id()?, - }, + ValueClass::IndexEmail(self.generate_snowflake_id()?), metadata.blob_hash.clone(), ) .custom(EmailIndexBuilder::set(metadata)) diff --git a/crates/jmap/src/email/index.rs b/crates/jmap/src/email/index.rs index 09320b65..c43e7acf 100644 --- a/crates/jmap/src/email/index.rs +++ b/crates/jmap/src/email/index.rs @@ -34,11 +34,13 @@ use nlp::language::Language; use store::{ backend::MAX_TOKEN_LENGTH, fts::{index::FtsDocument, Field}, - write::{BatchBuilder, BlobOp, IntoOperations, F_BITMAP, F_CLEAR, F_INDEX, F_VALUE}, + write::{ + BatchBuilder, BlobOp, DirectoryValue, IntoOperations, F_BITMAP, F_CLEAR, F_INDEX, F_VALUE, + }, BlobHash, }; -use crate::{mailbox::UidMailbox, Bincode, NamedKey}; +use crate::{mailbox::UidMailbox, Bincode}; use super::metadata::MessageMetadata; @@ -90,7 +92,7 @@ impl IndexMessage for BatchBuilder { let account_id = self.last_account_id().unwrap(); self.value(Property::Size, message.raw_message.len() as u32, F_INDEX) .add( - NamedKey::Quota::<&[u8]>(account_id), + DirectoryValue::UsedQuota(account_id), message.raw_message.len() as i64, ); @@ -413,7 +415,7 @@ impl<'x> IntoOperations for EmailIndexBuilder<'x> { batch .value(Property::Size, metadata.size as u32, F_INDEX | options) .add( - NamedKey::Quota::<&[u8]>(account_id), + DirectoryValue::UsedQuota(account_id), if self.set { metadata.size as i64 } else { diff --git a/crates/jmap/src/email/ingest.rs b/crates/jmap/src/email/ingest.rs index 07b43850..5d885efb 100644 --- a/crates/jmap/src/email/ingest.rs +++ b/crates/jmap/src/email/ingest.rs @@ -49,7 +49,7 @@ use crate::{ email::index::{IndexMessage, MAX_ID_LENGTH}, mailbox::UidMailbox, services::housekeeper::Event, - IngestError, NamedKey, JMAP, + IngestError, JMAP, }; use super::{ @@ -317,13 +317,10 @@ impl JMAP { .value(Property::ThreadId, thread_id, F_VALUE | F_BITMAP) .custom(changes) .set( - NamedKey::IndexEmail::<&[u8]> { - account_id: params.account_id, - document_id, - seq: self - .generate_snowflake_id() + ValueClass::IndexEmail( + self.generate_snowflake_id() .map_err(|_| IngestError::Temporary)?, - }, + ), blob_id.hash.clone(), ); self.store.write(batch.build()).await.map_err(|err| { diff --git a/crates/jmap/src/email/set.rs b/crates/jmap/src/email/set.rs index 7d42962a..202e7316 100644 --- a/crates/jmap/src/email/set.rs +++ b/crates/jmap/src/email/set.rs @@ -61,7 +61,7 @@ use store::{ use crate::{ auth::AccessToken, mailbox::UidMailbox, services::housekeeper::Event, Bincode, IngestError, - NamedKey, JMAP, + JMAP, }; use super::{ @@ -1071,7 +1071,11 @@ impl JMAP { batch .with_account_id(account_id) .with_collection(Collection::Email) - .delete_document(document_id); + .delete_document(document_id) + .set( + ValueClass::IndexEmail(self.generate_snowflake_id()?), + vec![], + ); // Remove last changeId batch.value(Property::Cid, (), F_VALUE | F_CLEAR); @@ -1217,16 +1221,6 @@ impl JMAP { .delete_document(thread_id); } - // Remove message from FTS index - batch.set( - NamedKey::IndexEmail::<&[u8]> { - account_id, - document_id, - seq: self.generate_snowflake_id()?, - }, - vec![], - ); - // Commit batch match self.store.write(batch.build()).await { Ok(_) => (), diff --git a/crates/jmap/src/identity/set.rs b/crates/jmap/src/identity/set.rs index 68a26860..6f2099f4 100644 --- a/crates/jmap/src/identity/set.rs +++ b/crates/jmap/src/identity/set.rs @@ -21,6 +21,7 @@ * for more details. */ +use directory::QueryBy; use jmap_proto::{ error::{method::MethodError, set::SetError}, method::set::{RequestArguments, SetRequest, SetResponse}, @@ -34,13 +35,12 @@ use jmap_proto::{ }; use store::write::{log::ChangeLogBuilder, BatchBuilder, F_CLEAR, F_VALUE}; -use crate::{auth::AccessToken, JMAP}; +use crate::JMAP; impl JMAP { pub async fn identity_set( &self, mut request: SetRequest, - access_token: &AccessToken, ) -> Result { let account_id = request.account_id.document_id(); let mut identity_ids = self @@ -73,17 +73,13 @@ impl JMAP { // Validate email address if let Value::Text(email) = identity.get(&Property::Email) { - let account_name = if access_token.primary_id == account_id { - access_token.name.clone() - } else { - self.get_account_name(account_id).await?.unwrap_or_default() - }; - if !self .directory - .emails_by_name(&account_name) + .query(QueryBy::id(account_id).with_store(&self.store)) .await .unwrap_or_default() + .unwrap_or_default() + .emails .contains(email) { response.not_created.append( diff --git a/crates/jmap/src/lib.rs b/crates/jmap/src/lib.rs index 526ee7b4..b0d70cec 100644 --- a/crates/jmap/src/lib.rs +++ b/crates/jmap/src/lib.rs @@ -31,7 +31,7 @@ use auth::{ AccessToken, }; use dashmap::DashMap; -use directory::{Directories, Directory}; +use directory::{Directories, Directory, QueryBy}; use jmap_proto::{ error::method::MethodError, method::{ @@ -52,12 +52,8 @@ use store::{ parking_lot::Mutex, query::{sort::Pagination, Comparator, Filter, ResultSet, SortedResultSet}, roaring::RoaringBitmap, - write::{ - key::{DeserializeBigEndian, KeySerializer}, - BatchBuilder, BitmapClass, TagValue, ToBitmaps, ValueClass, - }, - BitmapKey, BlobStore, Deserialize, FtsStore, Key, Serialize, Store, Stores, ValueKey, - SUBSPACE_INDEX_VALUES, U32_LEN, U64_LEN, + write::{BatchBuilder, BitmapClass, DirectoryValue, TagValue, ToBitmaps, ValueClass}, + BitmapKey, BlobStore, Deserialize, FtsStore, Serialize, Store, Stores, ValueKey, }; use tokio::sync::mpsc; use utils::{ @@ -603,7 +599,7 @@ impl JMAP { access_token.quota as i64 } else { self.directory - .principal(&access_token.name) + .query(QueryBy::id(account_id).with_store(&self.store)) .await .map_err(|err| { tracing::error!( @@ -621,7 +617,7 @@ impl JMAP { pub async fn get_used_quota(&self, account_id: u32) -> Result { self.store - .get_counter(NamedKey::Quota::<&[u8]>(account_id)) + .get_counter(DirectoryValue::UsedQuota(account_id)) .await .map_err(|err| { tracing::error!( @@ -848,93 +844,3 @@ impl UpdateResults for QueryResponse { } } } - -pub enum NamedKey> { - Name(T), - Id(u32), - Quota(u32), - IndexEmail { - account_id: u32, - document_id: u32, - seq: u64, - }, -} - -impl> From<&NamedKey> for ValueClass { - fn from(key: &NamedKey) -> Self { - match key { - NamedKey::Name(name) => ValueClass::Subspace { - key: name.as_ref().to_vec(), - id: 0, - }, - NamedKey::Id(id) => ValueClass::Subspace { - key: KeySerializer::new(std::mem::size_of::()) - .write_leb128(*id) - .finalize(), - id: 1, - }, - NamedKey::Quota(id) => ValueClass::Subspace { - key: KeySerializer::new(std::mem::size_of::()) - .write_leb128(*id) - .finalize(), - id: 2, - }, - NamedKey::IndexEmail { - account_id, - document_id, - seq, - } => ValueClass::Subspace { - key: KeySerializer::new(std::mem::size_of::() * 4) - .write(*seq) - .write(*account_id) - .write(*document_id) - .finalize(), - id: 3, - }, - } - } -} - -impl> NamedKey { - pub fn deserialize_index_email(bytes: &[u8]) -> store::Result { - let len = bytes.len(); - Ok(NamedKey::IndexEmail { - seq: bytes.deserialize_be_u64(len - U64_LEN - (U32_LEN * 2))?, - account_id: bytes.deserialize_be_u32(len - U32_LEN * 2)?, - document_id: bytes.deserialize_be_u32(len - U32_LEN)?, - }) - } -} - -impl> From> for ValueClass { - fn from(key: NamedKey) -> Self { - (&key).into() - } -} - -impl> From> for ValueKey { - fn from(key: NamedKey) -> Self { - ValueKey { - account_id: 0, - collection: 0, - document_id: 0, - class: key.into(), - } - } -} - -impl + Sync + Send> Key for NamedKey { - fn serialize(&self, include_subspace: bool) -> Vec { - ValueKey { - account_id: 0, - collection: 0, - document_id: 0, - class: ValueClass::from(self), - } - .serialize(include_subspace) - } - - fn subspace(&self) -> u8 { - SUBSPACE_INDEX_VALUES - } -} diff --git a/crates/jmap/src/principal/get.rs b/crates/jmap/src/principal/get.rs index 95e52062..1651bcdd 100644 --- a/crates/jmap/src/principal/get.rs +++ b/crates/jmap/src/principal/get.rs @@ -21,6 +21,7 @@ * for more details. */ +use directory::QueryBy; use jmap_proto::{ error::method::MethodError, method::get::{GetRequest, GetResponse, RequestArguments}, @@ -66,18 +67,10 @@ impl JMAP { }; for id in ids { - // Obtain the principal name - let name = if let Some(name) = self.get_account_name(id.document_id()).await? { - name - } else { - response.not_found.push(id.into()); - continue; - }; - // Obtain the principal let principal = if let Some(principal) = self .directory - .principal(&name) + .query(QueryBy::id(id.document_id()).with_store(&self.store)) .await .map_err(|_| MethodError::ServerPartialFail)? { @@ -98,14 +91,10 @@ impl JMAP { .clone() .map(Value::Text) .unwrap_or(Value::Null), - Property::Email => self - .directory - .emails_by_name(&name) - .await - .map_err(|_| MethodError::ServerPartialFail)? - .into_iter() - .next() - .map(Value::Text) + Property::Email => principal + .emails + .first() + .map(|email| Value::Text(email.clone())) .unwrap_or(Value::Null), _ => Value::Null, }; diff --git a/crates/jmap/src/principal/query.rs b/crates/jmap/src/principal/query.rs index cf22142f..7578adea 100644 --- a/crates/jmap/src/principal/query.rs +++ b/crates/jmap/src/principal/query.rs @@ -21,6 +21,7 @@ * for more details. */ +use directory::QueryBy; use jmap_proto::{ error::method::MethodError, method::query::{Filter, QueryRequest, QueryResponse, RequestArguments}, @@ -48,14 +49,13 @@ impl JMAP { Filter::Name(name) => { if let Some(principal) = self .directory - .principal(&name) + .query(QueryBy::name(name.as_str()).with_store(&self.store)) .await .map_err(|_| MethodError::ServerPartialFail)? { - let account_id = self.get_account_id(&principal.name).await?; - if is_set || result_set.results.contains(account_id) { + if is_set || result_set.results.contains(principal.id) { result_set.results = - RoaringBitmap::from_sorted_iter([account_id]).unwrap(); + RoaringBitmap::from_sorted_iter([principal.id]).unwrap(); } else { result_set.results = RoaringBitmap::new(); } @@ -66,13 +66,13 @@ impl JMAP { } Filter::Email(email) => { let mut ids = RoaringBitmap::new(); - for name in self + for id in self .directory - .names_by_email(&email) + .email_to_ids(&email, &self.store) .await .map_err(|_| MethodError::ServerPartialFail)? { - ids.insert(self.get_account_id(&name).await?); + ids.insert(id); } if is_set { result_set.results = ids; diff --git a/crates/jmap/src/services/index.rs b/crates/jmap/src/services/index.rs index 1549c993..51bbd9d2 100644 --- a/crates/jmap/src/services/index.rs +++ b/crates/jmap/src/services/index.rs @@ -24,40 +24,37 @@ use jmap_proto::types::{collection::Collection, property::Property}; use store::{ fts::index::FtsDocument, - write::{BatchBuilder, ValueClass}, - IterateParams, ValueKey, + write::{key::DeserializeBigEndian, BatchBuilder, ValueClass}, + Deserialize, IterateParams, ValueKey, U32_LEN, U64_LEN, }; use crate::{ email::{index::IndexMessageText, metadata::MessageMetadata}, - Bincode, NamedKey, JMAP, + Bincode, JMAP, }; use super::housekeeper::Event; +#[derive(Debug)] +struct IndexEmail { + account_id: u32, + document_id: u32, + seq: u64, +} + impl JMAP { pub async fn fts_index_queued(&self) { let from_key = ValueKey:: { account_id: 0, collection: 0, document_id: 0, - class: NamedKey::IndexEmail::<&[u8]> { - account_id: 0, - document_id: 0, - seq: 0, - } - .into(), + class: ValueClass::IndexEmail(0), }; let to_key = ValueKey:: { account_id: u32::MAX, collection: u8::MAX, document_id: u32::MAX, - class: NamedKey::IndexEmail::<&[u8]> { - account_id: u32::MAX, - document_id: u32::MAX, - seq: u64::MAX, - } - .into(), + class: ValueClass::IndexEmail(u64::MAX), }; // Retrieve entries pending to be indexed @@ -68,10 +65,7 @@ impl JMAP { .iterate( IterateParams::new(from_key, to_key).ascending(), |key, value| { - entries.push(( - NamedKey::>::deserialize_index_email(key)?, - value.to_vec(), - )); + entries.push((IndexEmail::deserialize(key)?, value.to_vec())); Ok(true) }, ) @@ -87,124 +81,123 @@ impl JMAP { // Index entries for (key, blob_hash) in entries { - if let NamedKey::IndexEmail { - account_id, - document_id, - .. - } = &key - { - if !blob_hash.is_empty() { - match self - .get_property::>( - *account_id, - Collection::Email, - *document_id, - Property::BodyStructure, - ) - .await + if !blob_hash.is_empty() { + match self + .get_property::>( + key.account_id, + Collection::Email, + key.document_id, + Property::BodyStructure, + ) + .await + { + Ok(Some(metadata)) + if metadata.inner.blob_hash.as_slice() == blob_hash.as_slice() => { - Ok(Some(metadata)) - if metadata.inner.blob_hash.as_slice() == blob_hash.as_slice() => + // Obtain raw message + let raw_message = if let Ok(Some(raw_message)) = + self.get_blob(&metadata.inner.blob_hash, 0..u32::MAX).await { - // Obtain raw message - let raw_message = if let Ok(Some(raw_message)) = - self.get_blob(&metadata.inner.blob_hash, 0..u32::MAX).await - { - raw_message - } else { - tracing::warn!( - context = "fts_index_queued", - event = "error", - account_id = *account_id, - document_id = *document_id, - blob_hash = ?metadata.inner.blob_hash, - "Message blob not found" - ); - continue; - }; - let message = metadata.inner.contents.into_message(&raw_message); - - // Index message - let document = - FtsDocument::with_default_language(self.config.default_language) - .with_account_id(*account_id) - .with_collection(Collection::Email) - .with_document_id(*document_id) - .index_message(&message); - if let Err(err) = self.fts_store.index(document).await { - tracing::error!( - context = "fts_index_queued", - event = "error", - account_id = *account_id, - document_id = *document_id, - reason = ?err, - "Failed to index email in FTS index" - ); - continue; - } - - tracing::debug!( + raw_message + } else { + tracing::warn!( context = "fts_index_queued", - event = "index", - account_id = *account_id, - document_id = *document_id, - "Indexed document in FTS index" + event = "error", + account_id = key.account_id, + document_id = key.document_id, + blob_hash = ?metadata.inner.blob_hash, + "Message blob not found" ); - } + continue; + }; + let message = metadata.inner.contents.into_message(&raw_message); - Err(err) => { + // Index message + let document = + FtsDocument::with_default_language(self.config.default_language) + .with_account_id(key.account_id) + .with_collection(Collection::Email) + .with_document_id(key.document_id) + .index_message(&message); + if let Err(err) = self.fts_store.index(document).await { tracing::error!( context = "fts_index_queued", event = "error", - account_id = *account_id, - document_id = *document_id, + account_id = key.account_id, + document_id = key.document_id, reason = ?err, - "Failed to retrieve email metadata" - ); - break; - } - _ => { - // The message was probably deleted or overwritten - tracing::debug!( - context = "fts_index_queued", - event = "error", - account_id = *account_id, - document_id = *document_id, - "Email metadata not found" + "Failed to index email in FTS index" ); + continue; } + + tracing::debug!( + context = "fts_index_queued", + event = "index", + account_id = key.account_id, + document_id = key.document_id, + "Indexed document in FTS index" + ); } - } else { - if let Err(err) = self - .fts_store - .remove(*account_id, Collection::Email.into(), *document_id) - .await - { + + Err(err) => { tracing::error!( context = "fts_index_queued", event = "error", - account_id = *account_id, - document_id = *document_id, + account_id = key.account_id, + document_id = key.document_id, reason = ?err, - "Failed to remove document from FTS index" + "Failed to retrieve email metadata" + ); + break; + } + _ => { + // The message was probably deleted or overwritten + tracing::debug!( + context = "fts_index_queued", + event = "error", + account_id = key.account_id, + document_id = key.document_id, + "Email metadata not found" ); - continue; } - - tracing::debug!( - context = "fts_index_queued", - event = "delete", - account_id = *account_id, - document_id = *document_id, - "Deleted document from FTS index" - ); } + } else { + if let Err(err) = self + .fts_store + .remove(key.account_id, Collection::Email.into(), key.document_id) + .await + { + tracing::error!( + context = "fts_index_queued", + event = "error", + account_id = key.account_id, + document_id = key.document_id, + reason = ?err, + "Failed to remove document from FTS index" + ); + continue; + } + + tracing::debug!( + context = "fts_index_queued", + event = "delete", + account_id = key.account_id, + document_id = key.document_id, + "Deleted document from FTS index" + ); } // Remove entry from queue if let Err(err) = self .store - .write(BatchBuilder::new().clear(key).build_batch()) + .write( + BatchBuilder::new() + .with_account_id(key.account_id) + .update_document(key.document_id) + .clear(ValueClass::IndexEmail(key.seq)) + .build_batch(), + ) .await { tracing::error!( @@ -222,3 +215,14 @@ impl JMAP { } } } + +impl Deserialize for IndexEmail { + fn deserialize(bytes: &[u8]) -> store::Result { + let len = bytes.len(); + Ok(IndexEmail { + seq: bytes.deserialize_be_u64(len - U64_LEN - (U32_LEN * 2))?, + account_id: bytes.deserialize_be_u32(len - U32_LEN * 2)?, + document_id: bytes.deserialize_be_u32(len - U32_LEN)?, + }) + } +} diff --git a/crates/jmap/src/services/ingest.rs b/crates/jmap/src/services/ingest.rs index 74847632..fd140165 100644 --- a/crates/jmap/src/services/ingest.rs +++ b/crates/jmap/src/services/ingest.rs @@ -21,6 +21,7 @@ * for more details. */ +use directory::QueryBy; use jmap_proto::types::{state::StateChange, type_state::DataType}; use mail_parser::MessageParser; use store::ahash::AHashMap; @@ -46,45 +47,37 @@ 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 names = self + let uids = self .directory - .names_by_email(rcpt) + .email_to_ids(rcpt, &self.store) .await .unwrap_or_default(); - for name in &names { - deliver_names.insert(name.clone(), (DeliveryResult::Success, rcpt)); + for uid in &uids { + deliver_names.insert(*uid, (DeliveryResult::Success, rcpt)); } - recipients.push(names); + recipients.push(uids); } // Deliver to each recipient - for (name, (status, rcpt)) in &mut deliver_names { - // Obtain account id - let uid = match self.get_account_id(name).await { - Ok(uid) => uid, - Err(_) => { - *status = DeliveryResult::TemporaryFailure { - reason: "Transient server failure.".into(), - }; - continue; - } - }; - + for (uid, (status, rcpt)) in &mut deliver_names { // Check if there is an active sieve script - let result = match self.sieve_script_get_active(uid).await { + let result = match self.sieve_script_get_active(*uid).await { Ok(Some(active_script)) => { self.sieve_script_ingest( &raw_message, &message.sender_address, rcpt, - uid, - name, + *uid, active_script, ) .await } Ok(None) => { - let account_quota = match self.directory.principal(name).await { + let account_quota = match self + .directory + .query(QueryBy::id(*uid).with_store(&self.store)) + .await + { Ok(Some(p)) => p.quota as i64, Ok(None) => 0, Err(_) => { @@ -98,7 +91,7 @@ impl JMAP { self.email_ingest(IngestEmail { raw_message: &raw_message, message: MessageParser::new().parse(&raw_message), - account_id: uid, + account_id: *uid, account_quota, mailbox_ids: vec![INBOX_ID], keywords: vec![], @@ -121,7 +114,7 @@ impl JMAP { // Notify state change if ingested_message.change_id != u64::MAX { self.broadcast_state_change( - StateChange::new(uid) + StateChange::new(*uid) .with_change(DataType::EmailDelivery, ingested_message.change_id) .with_change(DataType::Email, ingested_message.change_id) .with_change(DataType::Mailbox, ingested_message.change_id) diff --git a/crates/jmap/src/sieve/ingest.rs b/crates/jmap/src/sieve/ingest.rs index ccb15b18..89e84248 100644 --- a/crates/jmap/src/sieve/ingest.rs +++ b/crates/jmap/src/sieve/ingest.rs @@ -23,6 +23,7 @@ use std::borrow::Cow; +use directory::QueryBy; use jmap_proto::types::{collection::Collection, id::Id, keyword::Keyword, property::Property}; use mail_parser::MessageParser; use sieve::{Envelope, Event, Input, Mailbox, Recipient}; @@ -55,7 +56,6 @@ impl JMAP { envelope_from: &str, envelope_to: &str, account_id: u32, - account_name: &str, mut active_script: ActiveScript, ) -> Result { // Parse message @@ -77,35 +77,26 @@ impl JMAP { // Create Sieve instance let mut instance = self.sieve_runtime.filter_parsed(message); - // Obtain mail from address - let mail_from = if let Some(email) = self - .directory - .emails_by_name(account_name) - .await - .unwrap_or_default() - .into_iter() - .next() - { - email - } else { - envelope_to.to_string() - }; - - // Set account address - instance.set_user_address(&mail_from); - // Set account name and obtain quota - let account_quota = match self.directory.principal(account_name).await { + let (account_quota, mail_from) = match self + .directory + .query(QueryBy::id(account_id).with_store(&self.store)) + .await + { Ok(Some(p)) => { instance.set_user_full_name(p.description().unwrap_or_else(|| p.name())); - p.quota as i64 + (p.quota as i64, p.emails.into_iter().next()) } - Ok(None) => 0, + Ok(None) => (0, None), Err(_) => { return Err(IngestError::Temporary); } }; + // Set account address + let mail_from = mail_from.unwrap_or_else(|| envelope_to.to_string()); + instance.set_user_address(&mail_from); + // Set envelope instance.set_envelope(Envelope::From, envelope_from); instance.set_envelope(Envelope::To, envelope_to); diff --git a/crates/jmap/src/sieve/set.rs b/crates/jmap/src/sieve/set.rs index dbf00cdd..f27818ea 100644 --- a/crates/jmap/src/sieve/set.rs +++ b/crates/jmap/src/sieve/set.rs @@ -46,11 +46,14 @@ use sieve::compiler::ErrorType; use store::{ query::Filter, rand::{distributions::Alphanumeric, thread_rng, Rng}, - write::{assert::HashedValue, log::ChangeLogBuilder, BatchBuilder, BlobOp, F_CLEAR, F_VALUE}, + write::{ + assert::HashedValue, log::ChangeLogBuilder, BatchBuilder, BlobOp, DirectoryValue, F_CLEAR, + F_VALUE, + }, BlobClass, }; -use crate::{auth::AccessToken, NamedKey, JMAP}; +use crate::{auth::AccessToken, JMAP}; struct SetContext<'x> { account_id: u32, @@ -119,7 +122,7 @@ impl JMAP { .with_account_id(account_id) .with_collection(Collection::SieveScript) .create_document(document_id) - .add(NamedKey::Quota::<&[u8]>(account_id), script_size as i64) + .add(DirectoryValue::UsedQuota(account_id), script_size as i64) .blob(blob_id.hash.clone(), BlobOp::Link, 0) .custom(builder); sieve_ids.insert(document_id); @@ -215,7 +218,7 @@ impl JMAP { std::cmp::Ordering::Equal => 0, }; if update_quota != 0 { - batch.add(NamedKey::Quota::<&[u8]>(account_id), update_quota); + batch.add(DirectoryValue::UsedQuota(account_id), update_quota); } // Update blobId @@ -393,7 +396,7 @@ impl JMAP { .value(Property::EmailIds, (), F_VALUE | F_CLEAR) .blob(blob_id.hash.clone(), BlobOp::Link, F_CLEAR) .add( - NamedKey::Quota::<&[u8]>(account_id), + DirectoryValue::UsedQuota(account_id), -(blob_id.section.as_ref().unwrap().size as i64), ) .custom(ObjectIndexBuilder::new(SCHEMA).with_current(obj)); diff --git a/crates/jmap/src/vacation/set.rs b/crates/jmap/src/vacation/set.rs index 20ed9f17..0ece164a 100644 --- a/crates/jmap/src/vacation/set.rs +++ b/crates/jmap/src/vacation/set.rs @@ -42,13 +42,16 @@ use jmap_proto::{ use mail_builder::MessageBuilder; use mail_parser::decoders::html::html_to_text; use store::{ - write::{assert::HashedValue, log::ChangeLogBuilder, BatchBuilder, BlobOp, F_CLEAR, F_VALUE}, + write::{ + assert::HashedValue, log::ChangeLogBuilder, BatchBuilder, BlobOp, DirectoryValue, F_CLEAR, + F_VALUE, + }, BlobClass, }; use crate::{ sieve::set::{ObjectBlobId, SCHEMA}, - NamedKey, JMAP, + JMAP, }; impl JMAP { @@ -286,10 +289,10 @@ impl JMAP { std::cmp::Ordering::Equal => 0, }; if quota != 0 { - batch.add(NamedKey::Quota::<&[u8]>(account_id), quota); + batch.add(DirectoryValue::UsedQuota(account_id), quota); } } else { - batch.add(NamedKey::Quota::<&[u8]>(account_id), script_size); + batch.add(DirectoryValue::UsedQuota(account_id), script_size); } }; diff --git a/crates/managesieve/src/op/putscript.rs b/crates/managesieve/src/op/putscript.rs index 87af5b4a..9ecac091 100644 --- a/crates/managesieve/src/op/putscript.rs +++ b/crates/managesieve/src/op/putscript.rs @@ -22,10 +22,7 @@ */ use imap_proto::receiver::Request; -use jmap::{ - sieve::set::{ObjectBlobId, SCHEMA}, - NamedKey, -}; +use jmap::sieve::set::{ObjectBlobId, SCHEMA}; use jmap_proto::{ object::{index::ObjectIndexBuilder, Object}, types::{blob::BlobId, collection::Collection, property::Property, value::Value}, @@ -33,7 +30,7 @@ use jmap_proto::{ use sieve::compiler::ErrorType; use store::{ query::Filter, - write::{assert::HashedValue, BatchBuilder, BlobOp, F_CLEAR}, + write::{assert::HashedValue, BatchBuilder, BlobOp, DirectoryValue, F_CLEAR}, BlobClass, }; use tokio::io::{AsyncRead, AsyncWrite}; @@ -142,7 +139,7 @@ impl Session { std::cmp::Ordering::Equal => 0, }; if update_quota != 0 { - batch.add(NamedKey::Quota::<&[u8]>(account_id), update_quota); + batch.add(DirectoryValue::UsedQuota(account_id), update_quota); } batch.custom( @@ -183,7 +180,7 @@ impl Session { .with_account_id(account_id) .with_collection(Collection::SieveScript) .create_document(document_id) - .add(NamedKey::Quota::<&[u8]>(account_id), script_size) + .add(DirectoryValue::UsedQuota(account_id), script_size) .blob(blob_id.hash.clone(), BlobOp::Link, 0) .custom( ObjectIndexBuilder::new(SCHEMA).with_changes( diff --git a/crates/smtp/src/config/queue.rs b/crates/smtp/src/config/queue.rs index 09b0470a..de950842 100644 --- a/crates/smtp/src/config/queue.rs +++ b/crates/smtp/src/config/queue.rs @@ -23,7 +23,7 @@ use std::time::Duration; -use directory::memory::MemoryDirectory; +use directory::backend::memory::MemoryDirectory; use mail_send::Credentials; use super::{ diff --git a/crates/smtp/src/core/management.rs b/crates/smtp/src/core/management.rs index 34c810e4..a9e0eb27 100644 --- a/crates/smtp/src/core/management.rs +++ b/crates/smtp/src/core/management.rs @@ -23,7 +23,7 @@ use std::{borrow::Cow, fmt::Display, net::IpAddr, sync::Arc, time::Instant}; -use directory::Type; +use directory::{QueryBy, Type}; use http_body_util::{combinators::BoxBody, BodyExt, Empty, Full}; use hyper::{ body::{self, Bytes}, @@ -255,7 +255,10 @@ impl SMTP { .queue .config .management_lookup - .authenticate(&Credentials::Plain { username, secret }) + .query(QueryBy::credentials(&Credentials::Plain { + username, + secret, + })) .await { Ok(Some(principal)) if principal.typ == Type::Superuser => { diff --git a/crates/smtp/src/inbound/auth.rs b/crates/smtp/src/inbound/auth.rs index b15db7fa..a3a6afee 100644 --- a/crates/smtp/src/inbound/auth.rs +++ b/crates/smtp/src/inbound/auth.rs @@ -21,6 +21,7 @@ * for more details. */ +use directory::QueryBy; use mail_parser::decoders::base64::base64_decode; use mail_send::Credentials; use smtp_proto::{IntoString, AUTH_LOGIN, AUTH_OAUTHBEARER, AUTH_PLAIN, AUTH_XOAUTH2}; @@ -180,8 +181,10 @@ impl Session { | Credentials::XOauth2 { username, .. } | Credentials::OAuthBearer { token: username } => username.to_string(), }; - if let Ok(is_authenticated) = - lookup.authenticate(&credentials).await.map(|r| r.is_some()) + if let Ok(is_authenticated) = lookup + .query(QueryBy::credentials(&credentials)) + .await + .map(|r| r.is_some()) { tracing::debug!( parent: &self.span, diff --git a/crates/store/src/backend/mysql/main.rs b/crates/store/src/backend/mysql/main.rs index aee2d503..e12ca6f6 100644 --- a/crates/store/src/backend/mysql/main.rs +++ b/crates/store/src/backend/mysql/main.rs @@ -26,7 +26,7 @@ use utils::config::utils::AsKey; use crate::{ SUBSPACE_BITMAPS, SUBSPACE_BLOBS, SUBSPACE_BLOB_DATA, SUBSPACE_COUNTERS, SUBSPACE_INDEXES, - SUBSPACE_INDEX_VALUES, SUBSPACE_LOGS, SUBSPACE_VALUES, + SUBSPACE_LOGS, SUBSPACE_VALUES, }; use super::MysqlStore; @@ -80,7 +80,7 @@ impl MysqlStore { pub(super) async fn create_tables(&self) -> crate::Result<()> { let mut conn = self.conn_pool.get_conn().await?; - for table in [SUBSPACE_VALUES, SUBSPACE_LOGS, SUBSPACE_INDEX_VALUES] { + for table in [SUBSPACE_VALUES, SUBSPACE_LOGS] { let table = char::from(table); conn.query_drop(&format!( "CREATE TABLE IF NOT EXISTS {table} ( diff --git a/crates/store/src/backend/postgres/main.rs b/crates/store/src/backend/postgres/main.rs index 0a4fb52a..91abcba9 100644 --- a/crates/store/src/backend/postgres/main.rs +++ b/crates/store/src/backend/postgres/main.rs @@ -23,8 +23,7 @@ use crate::{ backend::postgres::tls::MakeRustlsConnect, SUBSPACE_BITMAPS, SUBSPACE_BLOBS, - SUBSPACE_BLOB_DATA, SUBSPACE_COUNTERS, SUBSPACE_INDEXES, SUBSPACE_INDEX_VALUES, SUBSPACE_LOGS, - SUBSPACE_VALUES, + SUBSPACE_BLOB_DATA, SUBSPACE_COUNTERS, SUBSPACE_INDEXES, SUBSPACE_LOGS, SUBSPACE_VALUES, }; use super::PostgresStore; @@ -75,12 +74,7 @@ impl PostgresStore { pub(super) async fn create_tables(&self) -> crate::Result<()> { let conn = self.conn_pool.get().await?; - for table in [ - SUBSPACE_VALUES, - SUBSPACE_LOGS, - SUBSPACE_INDEX_VALUES, - SUBSPACE_BLOB_DATA, - ] { + for table in [SUBSPACE_VALUES, SUBSPACE_LOGS, SUBSPACE_BLOB_DATA] { let table = char::from(table); conn.execute( &format!( diff --git a/crates/store/src/backend/rocksdb/main.rs b/crates/store/src/backend/rocksdb/main.rs index 5eb34e10..75836006 100644 --- a/crates/store/src/backend/rocksdb/main.rs +++ b/crates/store/src/backend/rocksdb/main.rs @@ -38,8 +38,7 @@ use utils::{ use crate::{Deserialize, Error}; use super::{ - RocksDbStore, CF_BITMAPS, CF_BLOBS, CF_BLOB_DATA, CF_COUNTERS, CF_INDEXES, CF_INDEX_VALUES, - CF_LOGS, CF_VALUES, + RocksDbStore, CF_BITMAPS, CF_BLOBS, CF_BLOB_DATA, CF_COUNTERS, CF_INDEXES, CF_LOGS, CF_VALUES, }; impl RocksDbStore { @@ -80,7 +79,7 @@ impl RocksDbStore { cfs.push(ColumnFamilyDescriptor::new(CF_BLOB_DATA, cf_opts)); // Other cfs - for cf in [CF_BLOBS, CF_INDEXES, CF_INDEX_VALUES, CF_LOGS, CF_VALUES] { + for cf in [CF_BLOBS, CF_INDEXES, CF_LOGS, CF_VALUES] { let cf_opts = Options::default(); cfs.push(ColumnFamilyDescriptor::new(cf, cf_opts)); } diff --git a/crates/store/src/backend/rocksdb/mod.rs b/crates/store/src/backend/rocksdb/mod.rs index 5295f928..9dd3cc5e 100644 --- a/crates/store/src/backend/rocksdb/mod.rs +++ b/crates/store/src/backend/rocksdb/mod.rs @@ -27,7 +27,7 @@ use rocksdb::{MultiThreaded, OptimisticTransactionDB}; use crate::{ SUBSPACE_BITMAPS, SUBSPACE_BLOBS, SUBSPACE_BLOB_DATA, SUBSPACE_COUNTERS, SUBSPACE_INDEXES, - SUBSPACE_INDEX_VALUES, SUBSPACE_LOGS, SUBSPACE_VALUES, + SUBSPACE_LOGS, SUBSPACE_VALUES, }; pub mod bitmap; @@ -42,7 +42,6 @@ static CF_LOGS: &str = unsafe { std::str::from_utf8_unchecked(&[SUBSPACE_LOGS]) static CF_INDEXES: &str = unsafe { std::str::from_utf8_unchecked(&[SUBSPACE_INDEXES]) }; static CF_BLOBS: &str = unsafe { std::str::from_utf8_unchecked(&[SUBSPACE_BLOBS]) }; static CF_BLOB_DATA: &str = unsafe { std::str::from_utf8_unchecked(&[SUBSPACE_BLOB_DATA]) }; -static CF_INDEX_VALUES: &str = unsafe { std::str::from_utf8_unchecked(&[SUBSPACE_INDEX_VALUES]) }; static CF_COUNTERS: &str = unsafe { std::str::from_utf8_unchecked(&[SUBSPACE_COUNTERS]) }; impl From for crate::Error { diff --git a/crates/store/src/backend/rocksdb/write.rs b/crates/store/src/backend/rocksdb/write.rs index 62fb369a..fd003bf8 100644 --- a/crates/store/src/backend/rocksdb/write.rs +++ b/crates/store/src/backend/rocksdb/write.rs @@ -31,12 +31,11 @@ use rocksdb::{Direction, ErrorKind, IteratorMode}; use super::{ bitmap::{clear_bit, set_bit}, - RocksDbStore, CF_BITMAPS, CF_BLOBS, CF_COUNTERS, CF_INDEXES, CF_INDEX_VALUES, CF_LOGS, - CF_VALUES, + RocksDbStore, CF_BITMAPS, CF_BLOBS, CF_COUNTERS, CF_INDEXES, CF_LOGS, CF_VALUES, }; use crate::{ write::{Batch, Operation, ValueOp, MAX_COMMIT_ATTEMPTS, MAX_COMMIT_TIME}, - BitmapKey, BlobKey, IndexKey, Key, LogKey, ValueKey, SUBSPACE_INDEX_VALUES, SUBSPACE_VALUES, + BitmapKey, BlobKey, IndexKey, Key, LogKey, ValueKey, SUBSPACE_VALUES, }; impl RocksDbStore { @@ -52,7 +51,6 @@ impl RocksDbStore { let cf_indexes = db.cf_handle(CF_INDEXES).unwrap(); let cf_logs = db.cf_handle(CF_LOGS).unwrap(); let cf_blobs = db.cf_handle(CF_BLOBS).unwrap(); - let cf_index_values = db.cf_handle(CF_INDEX_VALUES).unwrap(); let cf_counters = db.cf_handle(CF_COUNTERS).unwrap(); loop { @@ -101,17 +99,12 @@ impl RocksDbStore { document_id, class, }; - let cf = match key.subspace() { - SUBSPACE_VALUES => &cf_values, - SUBSPACE_INDEX_VALUES => &cf_index_values, - _ => unreachable!(), - }; let key = key.serialize(false); if let ValueOp::Set(value) = op { - wb.put_cf(cf, &key, value); + wb.put_cf(&cf_values, &key, value); } else { - wb.delete_cf(cf, &key); + wb.delete_cf(&cf_values, &key); } } Operation::Index { field, key, set } => { @@ -187,14 +180,9 @@ impl RocksDbStore { document_id, class, }; - let cf = match key.subspace() { - SUBSPACE_VALUES => &cf_values, - SUBSPACE_INDEX_VALUES => &cf_index_values, - _ => unreachable!(), - }; let key = key.serialize(false); let matches = txn - .get_cf(cf, &key)? + .get_cf(&cf_values, &key)? .map(|value| assert_value.matches(&value)) .unwrap_or_else(|| assert_value.is_none()); if !matches { diff --git a/crates/store/src/backend/sqlite/main.rs b/crates/store/src/backend/sqlite/main.rs index e6829992..1a48cd4d 100644 --- a/crates/store/src/backend/sqlite/main.rs +++ b/crates/store/src/backend/sqlite/main.rs @@ -30,7 +30,7 @@ use utils::{ use crate::{ SUBSPACE_BITMAPS, SUBSPACE_BLOBS, SUBSPACE_BLOB_DATA, SUBSPACE_COUNTERS, SUBSPACE_INDEXES, - SUBSPACE_INDEX_VALUES, SUBSPACE_LOGS, SUBSPACE_VALUES, + SUBSPACE_LOGS, SUBSPACE_VALUES, }; use super::{pool::SqliteConnectionManager, SqliteStore}; @@ -75,12 +75,7 @@ impl SqliteStore { pub(super) fn create_tables(&self) -> crate::Result<()> { let conn = self.conn_pool.get()?; - for table in [ - SUBSPACE_VALUES, - SUBSPACE_LOGS, - SUBSPACE_INDEX_VALUES, - SUBSPACE_BLOB_DATA, - ] { + for table in [SUBSPACE_VALUES, SUBSPACE_LOGS, SUBSPACE_BLOB_DATA] { let table = char::from(table); conn.execute( &format!( diff --git a/crates/store/src/dispatch/lookup.rs b/crates/store/src/dispatch/lookup.rs index 696ce235..9587d9f2 100644 --- a/crates/store/src/dispatch/lookup.rs +++ b/crates/store/src/dispatch/lookup.rs @@ -65,7 +65,7 @@ impl LookupStore { LookupStore::Store(store) => { let (class, op) = match value { LookupValue::Value { value, expires } => ( - ValueClass::Key { key }, + ValueClass::Key(key), ValueOp::Set( KeySerializer::new(value.len() + U64_LEN) .write(if expires > 0 { @@ -77,7 +77,7 @@ impl LookupStore { .finalize(), ), ), - LookupValue::Counter { num } => (ValueClass::Key { key }, ValueOp::Add(num)), + LookupValue::Counter { num } => (ValueClass::Key(key), ValueOp::Add(num)), LookupValue::None => return Ok(()), }; @@ -102,7 +102,7 @@ impl LookupStore { account_id: 0, collection: 0, document_id: 0, - class: ValueClass::Key { key }, + class: ValueClass::Key(key), }) .await .map(|value| value.unwrap_or(LookupValue::None)), @@ -111,7 +111,7 @@ impl LookupStore { account_id: 0, collection: 0, document_id: 0, - class: ValueClass::Key { key }, + class: ValueClass::Key(key), }) .await .map(|num| LookupValue::Counter { num }), @@ -129,15 +129,13 @@ impl LookupStore { account_id: 0, collection: 0, document_id: 0, - class: ValueClass::Key { key: vec![0u8] }, + class: ValueClass::Key(vec![0u8]), }; let to_key = ValueKey { account_id: 0, collection: 0, document_id: 0, - class: ValueClass::Key { - key: vec![u8::MAX; 10], - }, + class: ValueClass::Key(vec![u8::MAX; 10]), }; let current_time = now(); @@ -154,7 +152,7 @@ impl LookupStore { let mut batch = BatchBuilder::new(); for key in expired_keys { batch.ops.push(Operation::Value { - class: ValueClass::Key { key }, + class: ValueClass::Key(key), op: ValueOp::Clear, }); if batch.ops.len() >= 1000 { @@ -168,7 +166,7 @@ impl LookupStore { } } #[cfg(feature = "redis")] - LookupStore::Redis(store) => {} + LookupStore::Redis(_) => {} LookupStore::Memory(_) => {} } diff --git a/crates/store/src/dispatch/store.rs b/crates/store/src/dispatch/store.rs index f7d8a890..64bb7f0e 100644 --- a/crates/store/src/dispatch/store.rs +++ b/crates/store/src/dispatch/store.rs @@ -28,7 +28,7 @@ use roaring::RoaringBitmap; use crate::{ write::{key::KeySerializer, AnyKey, Batch, BitmapClass, ValueClass}, BitmapKey, Deserialize, IterateParams, Key, Store, ValueKey, SUBSPACE_BITMAPS, - SUBSPACE_INDEXES, SUBSPACE_INDEX_VALUES, SUBSPACE_LOGS, SUBSPACE_VALUES, U32_LEN, + SUBSPACE_INDEXES, SUBSPACE_LOGS, SUBSPACE_VALUES, U32_LEN, }; impl Store { @@ -185,12 +185,7 @@ impl Store { } pub async fn purge_account(&self, account_id: u32) -> crate::Result<()> { - for subspace in [ - SUBSPACE_BITMAPS, - SUBSPACE_VALUES, - SUBSPACE_LOGS, - SUBSPACE_INDEXES, - ] { + for subspace in [SUBSPACE_BITMAPS, SUBSPACE_LOGS, SUBSPACE_INDEXES] { self.delete_range( AnyKey { subspace, @@ -204,37 +199,27 @@ impl Store { .await?; } - for (from_key, to_key) in [ - ( - ValueKey { - account_id: 0, - collection: 0, - document_id: 0, - class: ValueClass::Acl(account_id), - }, - ValueKey { - account_id: 0, - collection: 0, - document_id: 0, - class: ValueClass::Acl(account_id + 1), - }, - ), - ( + 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::TermIndex, ValueClass::TermIndex), + ] { + self.delete_range( ValueKey { account_id, collection: 0, document_id: 0, - class: ValueClass::ReservedId, + class: from_class, }, ValueKey { account_id: account_id + 1, collection: 0, document_id: 0, - class: ValueClass::ReservedId, + class: to_class, }, - ), - ] { - self.delete_range(from_key, to_key).await?; + ) + .await?; } Ok(()) @@ -295,7 +280,6 @@ impl Store { SUBSPACE_BITMAPS, SUBSPACE_INDEXES, SUBSPACE_BLOBS, - SUBSPACE_INDEX_VALUES, SUBSPACE_COUNTERS, SUBSPACE_BLOB_DATA, ] { @@ -389,7 +373,6 @@ impl Store { for (subspace, with_values) in [ (SUBSPACE_VALUES, true), - (SUBSPACE_INDEX_VALUES, true), (SUBSPACE_COUNTERS, false), (SUBSPACE_BLOB_DATA, true), (SUBSPACE_BITMAPS, false), @@ -434,12 +417,9 @@ impl Store { value ); } - SUBSPACE_INDEX_VALUES if key[0] >= 3 => { - // Ignore named keys - return Ok(true); - } SUBSPACE_VALUES - if key.get(0..4).unwrap_or_default() == u32::MAX.to_be_bytes() => + if key[0] >= 6 + || key.get(1..5).unwrap_or_default() == u32::MAX.to_be_bytes() => { // Ignore lastId counter and ID mappings return Ok(true); diff --git a/crates/store/src/lib.rs b/crates/store/src/lib.rs index e88ad5ae..adbf59ab 100644 --- a/crates/store/src/lib.rs +++ b/crates/store/src/lib.rs @@ -180,7 +180,6 @@ pub const SUBSPACE_LOGS: u8 = b'l'; pub const SUBSPACE_INDEXES: u8 = b'i'; pub const SUBSPACE_BLOBS: u8 = b'o'; pub const SUBSPACE_BLOB_DATA: u8 = b't'; -pub const SUBSPACE_INDEX_VALUES: u8 = b'a'; pub const SUBSPACE_COUNTERS: u8 = b'c'; pub struct IterateParams { @@ -601,6 +600,22 @@ impl From for Vec { } } +impl From for Vec { + fn from(value: Row) -> Self { + value + .values + .into_iter() + .filter_map(|v| { + if let Value::Integer(v) = v { + Some(v as u32) + } else { + None + } + }) + .collect() + } +} + impl From for Vec { fn from(value: Rows) -> Self { value @@ -610,3 +625,21 @@ impl From for Vec { .collect() } } + +impl From for Vec { + fn from(value: Rows) -> Self { + value + .rows + .into_iter() + .flat_map(|v| { + v.values.into_iter().filter_map(|v| { + if let Value::Integer(v) = v { + Some(v as u32) + } else { + None + } + }) + }) + .collect() + } +} diff --git a/crates/store/src/write/key.rs b/crates/store/src/write/key.rs index 50a32595..5cace1ad 100644 --- a/crates/store/src/write/key.rs +++ b/crates/store/src/write/key.rs @@ -26,11 +26,10 @@ use utils::codec::leb128::Leb128_; use crate::{ BitmapKey, BlobHash, BlobKey, IndexKey, IndexKeyPrefix, Key, LogKey, ValueKey, BLOB_HASH_LEN, - SUBSPACE_BITMAPS, SUBSPACE_INDEXES, SUBSPACE_INDEX_VALUES, SUBSPACE_LOGS, SUBSPACE_VALUES, - U32_LEN, U64_LEN, + SUBSPACE_BITMAPS, SUBSPACE_INDEXES, SUBSPACE_LOGS, SUBSPACE_VALUES, U32_LEN, U64_LEN, }; -use super::{AnyKey, BitmapClass, BlobOp, TagValue, ValueClass}; +use super::{AnyKey, BitmapClass, BlobOp, DirectoryValue, TagValue, ValueClass}; pub struct KeySerializer { pub buf: Vec, @@ -217,69 +216,51 @@ impl Key for LogKey { impl + Sync + Send> Key for ValueKey { fn subspace(&self) -> u8 { - if matches!( - self.class.as_ref(), - ValueClass::Property(_) | ValueClass::TermIndex - ) { - SUBSPACE_VALUES - } else { - SUBSPACE_INDEX_VALUES - } + SUBSPACE_VALUES } fn serialize(&self, include_subspace: bool) -> Vec { + let serializer = if include_subspace { + KeySerializer::new(self.class.as_ref().serialized_size() + 2).write(self.subspace()) + } else { + KeySerializer::new(self.class.as_ref().serialized_size() + 1) + }; + match self.class.as_ref() { - ValueClass::Property(field) => if include_subspace { - KeySerializer::new(U32_LEN * 2 + 3).write(crate::SUBSPACE_VALUES) - } else { - KeySerializer::new(U32_LEN * 2 + 2) - } - .write(self.account_id) - .write(self.collection) - .write(self.document_id) - .write(*field), - ValueClass::TermIndex => if include_subspace { - KeySerializer::new(U32_LEN * 2 + 3).write(crate::SUBSPACE_VALUES) - } else { - KeySerializer::new(U32_LEN * 2 + 2) - } - .write(self.account_id) - .write(self.collection) - .write(self.document_id) - .write(u8::MAX), - ValueClass::Acl(grant_account_id) => if include_subspace { - KeySerializer::new(U32_LEN * 3 + 3).write(crate::SUBSPACE_INDEX_VALUES) - } else { - KeySerializer::new(U32_LEN * 3 + 2) - } - .write(0u8) - .write(*grant_account_id) - .write(self.account_id) - .write(self.collection) - .write(self.document_id), - ValueClass::ReservedId => if include_subspace { - KeySerializer::new(U32_LEN * 2 + 2).write(crate::SUBSPACE_INDEX_VALUES) - } else { - KeySerializer::new(U32_LEN * 2 + 1) - } - .write(1u8) - .write(self.account_id) - .write(self.collection) - .write(self.document_id), - ValueClass::Key { key } => if include_subspace { - KeySerializer::new(key.len() + U64_LEN + 2).write(crate::SUBSPACE_INDEX_VALUES) - } else { - KeySerializer::new(key.len() + U64_LEN + 1) - } - .write(2u8) - .write(key.as_slice()), - ValueClass::Subspace { key, id } => if include_subspace { - KeySerializer::new(key.len() + 2).write(crate::SUBSPACE_INDEX_VALUES) - } else { - KeySerializer::new(key.len() + 1) - } - .write(3 + *id) - .write(key.as_slice()), + ValueClass::Property(field) => serializer + .write(0u8) + .write(self.account_id) + .write(self.collection) + .write_leb128(self.document_id) + .write(*field), + ValueClass::TermIndex => serializer + .write(1u8) + .write(self.account_id) + .write(self.collection) + .write_leb128(self.document_id), + ValueClass::Acl(grant_account_id) => serializer + .write(2u8) + .write(*grant_account_id) + .write(self.account_id) + .write(self.collection) + .write(self.document_id), + ValueClass::ReservedId => serializer + .write(3u8) + .write(self.account_id) + .write(self.collection) + .write(self.document_id), + ValueClass::Key(key) => serializer.write(4u8).write(key.as_slice()), + ValueClass::IndexEmail(seq) => serializer + .write(5u8) + .write(*seq) + .write(self.account_id) + .write(self.document_id), + ValueClass::Directory(directory) => match directory { + 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), + }, } .finalize() } @@ -434,3 +415,48 @@ impl + Sync + Send> Key for AnyKey { self.subspace } } + +impl ValueClass { + pub fn serialized_size(&self) -> usize { + match self { + ValueClass::Property(_) | ValueClass::TermIndex | ValueClass::ReservedId => { + U32_LEN * 2 + 3 + } + 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::Principal(_) | DirectoryValue::UsedQuota(_) => U32_LEN, + }, + ValueClass::IndexEmail { .. } => U64_LEN * 2, + } + } +} + +impl From for ValueKey { + fn from(class: ValueClass) -> Self { + ValueKey { + account_id: 0, + collection: 0, + document_id: 0, + class, + } + } +} + +impl From for ValueKey { + fn from(value: DirectoryValue) -> Self { + ValueKey { + account_id: 0, + collection: 0, + document_id: 0, + class: ValueClass::Directory(value), + } + } +} + +impl From for ValueClass { + fn from(value: DirectoryValue) -> Self { + ValueClass::Directory(value) + } +} diff --git a/crates/store/src/write/mod.rs b/crates/store/src/write/mod.rs index b9ca6c1d..132596f1 100644 --- a/crates/store/src/write/mod.rs +++ b/crates/store/src/write/mod.rs @@ -138,10 +138,19 @@ pub enum TagValue { pub enum ValueClass { Property(u8), Acl(u32), - Subspace { key: Vec, id: u8 }, - Key { key: Vec }, + Key(Vec), TermIndex, ReservedId, + Directory(DirectoryValue), + IndexEmail(u64), +} + +#[derive(Debug, PartialEq, Clone, Eq, Hash)] +pub enum DirectoryValue { + NameToId(Vec), + EmailToId(Vec), + Principal(u32), + UsedQuota(u32), } #[derive(Debug, PartialEq, Eq, Hash, Default)] diff --git a/resources/config/directory/memory.toml b/resources/config/directory/memory.toml index 03b478c9..a2dbcf82 100644 --- a/resources/config/directory/memory.toml +++ b/resources/config/directory/memory.toml @@ -12,14 +12,14 @@ subaddressing = true #subaddressing = { map = "^([^.]+)\.([^.]+)@(.+)$", to = "${2}@${3}" } superuser-group = "superusers" -[[directory."default".users]] +[[directory."default".principals]] name = "admin" description = "Superuser" secret = "changeme" email = ["postmaster@%{DEFAULT_DOMAIN}%"] member-of = ["superusers"] -[[directory."default".users]] +[[directory."default".principals]] name = "john" description = "John Doe" secret = "12345" @@ -27,7 +27,7 @@ email = ["john@%{DEFAULT_DOMAIN}%", "jdoe@%{DEFAULT_DOMAIN}%", "john.doe@%{DEFAU email-list = ["info@%{DEFAULT_DOMAIN}%"] member-of = ["sales"] -[[directory."default".users]] +[[directory."default".principals]] name = "jane" description = "Jane Doe" secret = "abcde" @@ -35,7 +35,7 @@ email = ["jane@%{DEFAULT_DOMAIN}%", "jane.doe@%{DEFAULT_DOMAIN}%"] email-list = ["info@%{DEFAULT_DOMAIN}%"] member-of = ["sales", "support"] -[[directory."default".users]] +[[directory."default".principals]] name = "bill" description = "Bill Foobar" secret = "$2y$05$bvIG6Nmid91Mu9RcmmWZfO5HJIMCT8riNW0hEp8f6/FuA2/mHZFpe" @@ -43,11 +43,11 @@ quota = 50000000 email = ["bill@%{DEFAULT_DOMAIN}%", "bill.foobar@%{DEFAULT_DOMAIN}%"] email-list = ["info@%{DEFAULT_DOMAIN}%"] -[[directory."default".groups]] +[[directory."default".principals]] name = "sales" description = "Sales Team" -[[directory."default".groups]] +[[directory."default".principals]] name = "support" description = "Support Team" diff --git a/tests/src/directory/imap.rs b/tests/src/directory/imap.rs index f0651b27..3367b184 100644 --- a/tests/src/directory/imap.rs +++ b/tests/src/directory/imap.rs @@ -23,6 +23,7 @@ use std::sync::Arc; +use directory::QueryBy; use mail_parser::decoders::base64::base64_decode; use mail_send::Credentials; use tokio::{ @@ -78,7 +79,7 @@ async fn imap_directory() { assert_eq!( &LookupResult::from( handle - .authenticate(item.as_credentials()) + .query(QueryBy::credentials(item.as_credentials())) .await .unwrap() .is_some() @@ -98,7 +99,7 @@ async fn imap_directory() { tokio::spawn(async move { LookupResult::from( handle - .authenticate(item.as_credentials()) + .query(QueryBy::credentials(item.as_credentials())) .await .unwrap() .is_some(), diff --git a/tests/src/directory/ldap.rs b/tests/src/directory/ldap.rs index 04aa86f2..1b70b863 100644 --- a/tests/src/directory/ldap.rs +++ b/tests/src/directory/ldap.rs @@ -23,10 +23,10 @@ use std::fmt::Debug; -use directory::{Principal, Type}; +use directory::{backend::internal::manage::ManageDirectory, Principal, QueryBy, Type}; use mail_send::Credentials; -use crate::directory::parse_config; +use crate::directory::{map_account_ids, parse_config, IntoSortedPrincipal}; #[tokio::test] async fn ldap_directory() { @@ -41,36 +41,52 @@ async fn ldap_directory() { // Obtain directory handle let mut config = parse_config().await; let handle = config.directories.directories.remove("ldap").unwrap(); + let base_store = config.stores.stores.get("sqlite").unwrap(); // Test authentication assert_eq!( handle - .authenticate(&Credentials::Plain { - username: "john".to_string(), - secret: "12345".to_string() - }) + .query( + QueryBy::credentials(&Credentials::Plain { + username: "john".to_string(), + secret: "12345".to_string() + }) + .with_store(base_store) + ) .await .unwrap() - .unwrap(), + .unwrap() + .into_sorted(), Principal { + id: base_store.get_account_id("john").await.unwrap().unwrap(), name: "john".to_string(), description: "John Doe".to_string().into(), secrets: vec!["12345".to_string()], typ: Type::Individual, - member_of: vec!["sales".to_string()], + member_of: map_account_ids(base_store, vec!["sales"]).await, + emails: vec![ + "john@example.org".to_string(), + "john.doe@example.org".to_string() + ], ..Default::default() } + .into_sorted() ); assert_eq!( handle - .authenticate(&Credentials::Plain { - username: "bill".to_string(), - secret: "password".to_string() - }) + .query( + QueryBy::credentials(&Credentials::Plain { + username: "bill".to_string(), + secret: "password".to_string() + }) + .with_store(base_store) + ) .await .unwrap() - .unwrap(), + .unwrap() + .into_sorted(), Principal { + id: base_store.get_account_id("bill").await.unwrap().unwrap(), name: "bill".to_string(), description: "Bill Foobar".to_string().into(), secrets: vec![ @@ -78,37 +94,53 @@ async fn ldap_directory() { ], typ: Type::Individual, quota: 500000, + emails: vec!["bill@example.org".to_string(),], ..Default::default() } + .into_sorted() ); assert!(handle - .authenticate(&Credentials::Plain { - username: "bill".to_string(), - secret: "invalid".to_string() - }) + .query( + QueryBy::credentials(&Credentials::Plain { + username: "bill".to_string(), + secret: "invalid".to_string() + }) + .with_store(base_store) + ) .await .unwrap() .is_none()); // Get user by name - let mut principal = handle.principal("jane").await.unwrap().unwrap(); - principal.member_of.sort_unstable(); assert_eq!( - principal, + handle + .query(QueryBy::name("jane").with_store(base_store)) + .await + .unwrap() + .unwrap() + .into_sorted(), Principal { + id: base_store.get_account_id("jane").await.unwrap().unwrap(), name: "jane".to_string(), description: "Jane Doe".to_string().into(), typ: Type::Individual, secrets: vec!["abcde".to_string()], - member_of: vec!["sales".to_string(), "support".to_string()], + member_of: map_account_ids(base_store, vec!["sales", "support"]).await, + emails: vec!["jane@example.org".to_string(),], ..Default::default() } + .into_sorted() ); // Get group by name assert_eq!( - handle.principal("sales").await.unwrap().unwrap(), + handle + .query(QueryBy::name("sales").with_store(base_store)) + .await + .unwrap() + .unwrap(), Principal { + id: base_store.get_account_id("sales").await.unwrap().unwrap(), name: "sales".to_string(), description: "sales".to_string().into(), typ: Type::Group, @@ -116,52 +148,48 @@ async fn ldap_directory() { } ); - // Emails by id - compare_sorted( - handle.emails_by_name("john").await.unwrap(), - vec![ - "john@example.org".to_string(), - "john.doe@example.org".to_string(), - ], - ); - compare_sorted( - handle.emails_by_name("bill").await.unwrap(), - vec!["bill@example.org".to_string()], - ); - // Ids by email compare_sorted( - handle.names_by_email("jane@example.org").await.unwrap(), - vec!["jane".to_string()], + handle + .email_to_ids("jane@example.org", base_store) + .await + .unwrap(), + map_account_ids(base_store, vec!["jane"]).await, ); compare_sorted( handle - .names_by_email("jane+alias@example.org") + .email_to_ids("jane+alias@example.org", base_store) .await .unwrap(), - vec!["jane".to_string()], - ); - compare_sorted( - handle.names_by_email("info@example.org").await.unwrap(), - vec!["john".to_string(), "jane".to_string(), "bill".to_string()], + map_account_ids(base_store, vec!["jane"]).await, ); compare_sorted( handle - .names_by_email("info+alias@example.org") + .email_to_ids("info@example.org", base_store) .await .unwrap(), - vec!["john".to_string(), "jane".to_string(), "bill".to_string()], + map_account_ids(base_store, vec!["bill", "jane", "john"]).await, ); compare_sorted( - handle.names_by_email("unknown@example.org").await.unwrap(), - Vec::::new(), + handle + .email_to_ids("info+alias@example.org", base_store) + .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(), + Vec::::new(), ); assert_eq!( handle - .names_by_email("anything@catchall.org") + .email_to_ids("anything@catchall.org", base_store) .await .unwrap(), - vec!["robert".to_string()] + map_account_ids(base_store, vec!["robert"]).await ); // Domain validation diff --git a/tests/src/directory/mod.rs b/tests/src/directory/mod.rs index 5a0731f2..c9355760 100644 --- a/tests/src/directory/mod.rs +++ b/tests/src/directory/mod.rs @@ -27,13 +27,16 @@ pub mod smtp; pub mod sql; use ::smtp::core::Lookup; -use directory::{config::ConfigDirectory, AddressMapping, Directories}; +use directory::{ + backend::internal::manage::ManageDirectory, config::ConfigDirectory, AddressMapping, + Directories, Principal, +}; use mail_send::Credentials; use rustls::ServerConfig; use rustls_pemfile::{certs, pkcs8_private_keys}; use rustls_pki_types::PrivateKeyDer; use std::{borrow::Cow, io::BufReader, path::PathBuf, sync::Arc}; -use store::{config::ConfigStore, LookupStore, Stores}; +use store::{config::ConfigStore, LookupStore, Store, Stores}; use tokio_rustls::TlsAcceptor; use crate::store::TempDir; @@ -164,10 +167,6 @@ verify = "(&(|(objectClass=posixAccount)(objectClass=posixGroup))(|(mail=*?*)(gi expand = "(&(|(objectClass=posixAccount)(objectClass=posixGroup))(sn=?))" domains = "(&(|(objectClass=posixAccount)(objectClass=posixGroup))(|(mail=*@?)(givenName=*@?)(sn=*@?)))" -[directory."ldap".object-classes] -user = "posixAccount" -group = "posixGroup" - # Glauth does not support searchable custom attributes so # 'sn' and 'givenName' are used to search for aliases/lists. @@ -179,6 +178,7 @@ groups = ["memberOf", "otherGroups"] email = "mail" email-alias = "givenName" quota = "diskQuota" +type = "objectClass" ############################################################################## @@ -225,36 +225,41 @@ type = "memory" catch-all = true subaddressing = true -[[directory."local".users]] +[[directory."local".principals]] name = "john" +type = "individual" description = "John Doe" secret = "12345" email = ["john@example.org", "jdoe@example.org", "john.doe@example.org"] email-list = ["info@example.org"] member-of = ["sales"] -[[directory."local".users]] +[[directory."local".principals]] name = "jane" +type = "individual" description = "Jane Doe" secret = "abcde" email = "jane@example.org" email-list = ["info@example.org"] member-of = ["sales", "support"] -[[directory."local".users]] +[[directory."local".principals]] name = "bill" +type = "individual" description = "Bill Foobar" secret = "$2y$05$bvIG6Nmid91Mu9RcmmWZfO5HJIMCT8riNW0hEp8f6/FuA2/mHZFpe" quota = 500000 email = "bill@example.org" email-list = ["info@example.org"] -[[directory."local".groups]] +[[directory."local".principals]] name = "sales" +type = "group" description = "Sales Team" -[[directory."local".groups]] +[[directory."local".principals]] name = "support" +type = "group" description = "Support Team" "#; @@ -607,3 +612,23 @@ fn address_mappings() { ); } } + +async fn map_account_ids(store: &Store, names: Vec>) -> Vec { + let mut ids = Vec::with_capacity(names.len()); + for name in names { + ids.push(store.get_account_id(name.as_ref()).await.unwrap().unwrap()); + } + ids +} + +trait IntoSortedPrincipal: Sized { + fn into_sorted(self) -> Self; +} + +impl IntoSortedPrincipal for Principal { + fn into_sorted(mut self) -> Self { + self.member_of.sort_unstable(); + self.emails.sort_unstable(); + self + } +} diff --git a/tests/src/directory/smtp.rs b/tests/src/directory/smtp.rs index b5595d9f..e9dc1953 100644 --- a/tests/src/directory/smtp.rs +++ b/tests/src/directory/smtp.rs @@ -23,7 +23,7 @@ use std::sync::Arc; -use directory::DirectoryError; +use directory::{DirectoryError, QueryBy}; use mail_parser::decoders::base64::base64_decode; use mail_send::Credentials; use tokio::{ @@ -96,7 +96,12 @@ async fn smtp_directory() { for (item, expected) in &tests { let result: LookupResult = match item { Item::IsAccount(v) => handle.rcpt(v).await.unwrap().into(), - Item::Authenticate(v) => handle.authenticate(v).await.unwrap().is_some().into(), + Item::Authenticate(v) => handle + .query(QueryBy::credentials(v)) + .await + .unwrap() + .is_some() + .into(), Item::Verify(v) => match handle.vrfy(v).await { Ok(v) => v.into(), Err(DirectoryError::Unsupported) => LookupResult::False, @@ -123,7 +128,12 @@ async fn smtp_directory() { tokio::spawn(async move { let result: LookupResult = match &item { Item::IsAccount(v) => handle.rcpt(v).await.unwrap().into(), - Item::Authenticate(v) => handle.authenticate(v).await.unwrap().is_some().into(), + Item::Authenticate(v) => handle + .query(QueryBy::credentials(v)) + .await + .unwrap() + .is_some() + .into(), Item::Verify(v) => match handle.vrfy(v).await { Ok(v) => v.into(), Err(DirectoryError::Unsupported) => LookupResult::False, diff --git a/tests/src/directory/sql.rs b/tests/src/directory/sql.rs index 631bc193..a414a417 100644 --- a/tests/src/directory/sql.rs +++ b/tests/src/directory/sql.rs @@ -22,12 +22,12 @@ */ use ahash::AHashMap; -use directory::{Principal, Type}; +use directory::{backend::internal::manage::ManageDirectory, Principal, QueryBy, Type}; use mail_send::Credentials; use smtp::core::Lookup; use store::{LookupStore, Store}; -use crate::directory::parse_config; +use crate::directory::{map_account_ids, parse_config}; use super::DirectoryStore; @@ -57,6 +57,7 @@ async fn sql_directory() { let store = DirectoryStore { store: config.stores.lookup_stores.remove(directory_id).unwrap(), }; + let base_store = config.stores.stores.get(directory_id).unwrap(); // Create tables store.create_test_directory().await; @@ -132,32 +133,45 @@ async fn sql_directory() { // Test authentication assert_eq!( handle - .authenticate(&Credentials::Plain { - username: "john".to_string(), - secret: "12345".to_string() - }) + .query( + QueryBy::credentials(&Credentials::Plain { + username: "john".to_string(), + secret: "12345".to_string() + }) + .with_store(base_store) + ) .await .unwrap() .unwrap(), Principal { + id: base_store.get_account_id("john").await.unwrap().unwrap(), name: "john".to_string(), description: "John Doe".to_string().into(), secrets: vec!["12345".to_string()], typ: Type::Individual, - member_of: vec!["sales".to_string()], + member_of: map_account_ids(base_store, vec!["sales"]).await, + emails: vec![ + "john@example.org".to_string(), + "jdoe@example.org".to_string(), + "john.doe@example.org".to_string() + ], ..Default::default() } ); assert_eq!( handle - .authenticate(&Credentials::Plain { - username: "bill".to_string(), - secret: "password".to_string() - }) + .query( + QueryBy::credentials(&Credentials::Plain { + username: "bill".to_string(), + secret: "password".to_string() + }) + .with_store(base_store) + ) .await .unwrap() .unwrap(), Principal { + id: base_store.get_account_id("bill").await.unwrap().unwrap(), name: "bill".to_string(), description: "Bill Foobar".to_string().into(), secrets: vec![ @@ -165,35 +179,50 @@ async fn sql_directory() { ], typ: Type::Individual, quota: 500000, + emails: vec!["bill@example.org".to_string(),], ..Default::default() } ); assert!(handle - .authenticate(&Credentials::Plain { - username: "bill".to_string(), - secret: "invalid".to_string() - }) + .query( + QueryBy::credentials(&Credentials::Plain { + username: "bill".to_string(), + secret: "invalid".to_string() + }) + .with_store(base_store) + ) .await .unwrap() .is_none()); // Get user by name assert_eq!( - handle.principal("jane").await.unwrap().unwrap(), + handle + .query(QueryBy::name("jane").with_store(base_store)) + .await + .unwrap() + .unwrap(), Principal { + id: base_store.get_account_id("jane").await.unwrap().unwrap(), name: "jane".to_string(), description: "Jane Doe".to_string().into(), typ: Type::Individual, secrets: vec!["abcde".to_string()], - member_of: vec!["sales".to_string(), "support".to_string()], + member_of: map_account_ids(base_store, vec!["sales", "support"]).await, + emails: vec!["jane@example.org".to_string(),], ..Default::default() } ); // Get group by name assert_eq!( - handle.principal("sales").await.unwrap().unwrap(), + handle + .query(QueryBy::name("sales").with_store(base_store)) + .await + .unwrap() + .unwrap(), Principal { + id: base_store.get_account_id("sales").await.unwrap().unwrap(), name: "sales".to_string(), description: "Sales Team".to_string().into(), typ: Type::Group, @@ -201,53 +230,48 @@ async fn sql_directory() { } ); - // Emails by id - assert_eq!( - handle.emails_by_name("john").await.unwrap(), - vec![ - "john@example.org".to_string(), - "jdoe@example.org".to_string(), - "john.doe@example.org".to_string(), - ] - ); - assert_eq!( - handle.emails_by_name("bill").await.unwrap(), - vec!["bill@example.org".to_string(),] - ); - // Ids by email assert_eq!( - handle.names_by_email("jane@example.org").await.unwrap(), - vec!["jane".to_string()] - ); - assert_eq!( - handle.names_by_email("info@example.org").await.unwrap(), - vec!["bill".to_string(), "jane".to_string(), "john".to_string()] + handle + .email_to_ids("jane@example.org", base_store) + .await + .unwrap(), + map_account_ids(base_store, vec!["jane"]).await ); assert_eq!( handle - .names_by_email("jane+alias@example.org") + .email_to_ids("info@example.org", base_store) .await .unwrap(), - vec!["jane".to_string()] + map_account_ids(base_store, vec!["bill", "jane", "john"]).await ); assert_eq!( handle - .names_by_email("info+alias@example.org") + .email_to_ids("jane+alias@example.org", base_store) .await .unwrap(), - vec!["bill".to_string(), "jane".to_string(), "john".to_string()] - ); - assert_eq!( - handle.names_by_email("unknown@example.org").await.unwrap(), - Vec::::new() + map_account_ids(base_store, vec!["jane"]).await ); assert_eq!( handle - .names_by_email("anything@catchall.org") + .email_to_ids("info+alias@example.org", base_store) .await .unwrap(), - vec!["robert".to_string()] + map_account_ids(base_store, vec!["bill", "jane", "john"]).await + ); + assert_eq!( + handle + .email_to_ids("unknown@example.org", base_store) + .await + .unwrap(), + Vec::::new() + ); + assert_eq!( + handle + .email_to_ids("anything@catchall.org", base_store) + .await + .unwrap(), + map_account_ids(base_store, vec!["robert"]).await ); // Domain validation @@ -317,7 +341,7 @@ impl DirectoryStore { "CREATE TABLE emails (name TEXT NOT NULL, address TEXT NOT", " NULL, type TEXT, PRIMARY KEY (name, address))" ), - "INSERT INTO accounts (name, secret, type) VALUES ('admin', 'secret', 'individual')", + "INSERT INTO accounts (name, secret, type) VALUES ('admin', 'secret', 'admin')", ] { let query = if matches!(self.store, LookupStore::Store(Store::MySQL(_))) { query.replace("TEXT", "VARCHAR(255)") @@ -333,25 +357,35 @@ impl DirectoryStore { } pub async fn create_test_user(&self, login: &str, secret: &str, name: &str) { + let account_type = if login == "admin" { + "admin" + } else { + "individual" + }; self.store .query::( if matches!(self.store, LookupStore::Store(Store::PostgreSQL(_))) { concat!( "INSERT INTO accounts (name, secret, description, ", - "type, active) VALUES ($1, $2, $3, 'individual', true) ON CONFLICT (name) DO NOTHING" + "type, active) VALUES ($1, $2, $3, $4, true) ON CONFLICT (name) DO NOTHING" ) } else if matches!(self.store, LookupStore::Store(Store::MySQL(_))) { concat!( "INSERT IGNORE INTO accounts (name, secret, description, ", - "type, active) VALUES (?, ?, ?, 'individual', true)" + "type, active) VALUES (?, ?, ?, ?, true)" ) } else { concat!( "INSERT OR IGNORE INTO accounts (name, secret, description, ", - "type, active) VALUES (?, ?, ?, 'individual', true)" + "type, active) VALUES (?, ?, ?, ?, true)" ) }, - vec![login.into(), secret.into(), name.into()], + vec![ + login.into(), + secret.into(), + name.into(), + account_type.into(), + ], ) .await .unwrap(); diff --git a/tests/src/imap/mod.rs b/tests/src/imap/mod.rs index a3240a04..5d43654e 100644 --- a/tests/src/imap/mod.rs +++ b/tests/src/imap/mod.rs @@ -40,7 +40,7 @@ use std::{path::PathBuf, sync::Arc, time::Duration}; use ::managesieve::core::ManageSieveSessionManager; use ::store::config::ConfigStore; use ahash::AHashSet; -use directory::config::ConfigDirectory; +use directory::{backend::internal::manage::ManageDirectory, config::ConfigDirectory}; use imap::core::{ImapSessionManager, IMAP}; use imap_proto::ResponseType; use jmap::{api::JmapSessionManager, services::IPC_CHANNEL_BUFFER, JMAP}; @@ -337,7 +337,7 @@ async fn init_imap_tests(store_id: &str, delete_if_exists: bool) -> IMAPTest { } // Assign Id 0 to admin (required for some tests) - jmap.get_account_id("admin").await.unwrap(); + jmap.store.get_or_create_account_id("admin").await.unwrap(); IMAPTest { jmap, diff --git a/tests/src/jmap/auth_acl.rs b/tests/src/jmap/auth_acl.rs index 216ba95d..ad2690fd 100644 --- a/tests/src/jmap/auth_acl.rs +++ b/tests/src/jmap/auth_acl.rs @@ -21,6 +21,7 @@ * for more details. */ +use directory::backend::internal::manage::ManageDirectory; use jmap::mailbox::{INBOX_ID, TRASH_ID}; use jmap_client::{ core::{ @@ -64,22 +65,26 @@ pub async fn test(params: &mut JMAPTest) { .create_test_group_with_email("sales@example.com", "Sales Group") .await; let john_id: Id = server - .get_account_id("jdoe@example.com") + .store + .get_or_create_account_id("jdoe@example.com") .await .unwrap() .into(); let jane_id: Id = server - .get_account_id("jane.smith@example.com") + .store + .get_or_create_account_id("jane.smith@example.com") .await .unwrap() .into(); let bill_id: Id = server - .get_account_id("bill@example.com") + .store + .get_or_create_account_id("bill@example.com") .await .unwrap() .into(); let sales_id: Id = server - .get_account_id("sales@example.com") + .store + .get_or_create_account_id("sales@example.com") .await .unwrap() .into(); @@ -784,7 +789,7 @@ pub async fn test(params: &mut JMAPTest) { // Destroy test account data for id in [john_id, bill_id, jane_id, sales_id] { params.client.set_default_account_id(&id.to_string()); - destroy_all_mailboxes(¶ms.client).await; + destroy_all_mailboxes(params).await; } assert_is_empty(server).await; } diff --git a/tests/src/jmap/auth_limits.rs b/tests/src/jmap/auth_limits.rs index 1677a4b4..fe809fee 100644 --- a/tests/src/jmap/auth_limits.rs +++ b/tests/src/jmap/auth_limits.rs @@ -23,6 +23,7 @@ use std::{sync::Arc, time::Duration}; +use directory::backend::internal::manage::ManageDirectory; use jmap_client::{ client::{Client, Credentials}, core::set::{SetError, SetErrorType}, @@ -43,7 +44,14 @@ pub async fn test(params: &mut JMAPTest) { .directory .create_test_user_with_email("jdoe@example.com", "12345", "John Doe") .await; - let account_id = Id::from(server.get_account_id("jdoe@example.com").await.unwrap()).to_string(); + let account_id = Id::from( + server + .store + .get_or_create_account_id("jdoe@example.com") + .await + .unwrap(), + ) + .to_string(); params .directory .link_test_address("jdoe@example.com", "john.doe@example.com", "alias") @@ -199,6 +207,6 @@ pub async fn test(params: &mut JMAPTest) { // Destroy test accounts params.client.set_default_account_id(&account_id); - destroy_all_mailboxes(¶ms.client).await; + destroy_all_mailboxes(params).await; assert_is_empty(server).await; } diff --git a/tests/src/jmap/auth_oauth.rs b/tests/src/jmap/auth_oauth.rs index beed2597..c1038003 100644 --- a/tests/src/jmap/auth_oauth.rs +++ b/tests/src/jmap/auth_oauth.rs @@ -24,6 +24,7 @@ use std::time::{Duration, Instant}; use bytes::Bytes; +use directory::backend::internal::manage::ManageDirectory; use jmap::auth::oauth::{DeviceAuthResponse, ErrorType, OAuthMetadata, TokenResponse}; use jmap_client::{ client::{Client, Credentials}, @@ -47,7 +48,14 @@ pub async fn test(params: &mut JMAPTest) { .directory .create_test_user_with_email("jdoe@example.com", "12345", "John Doe") .await; - let john_id = Id::from(server.get_account_id("jdoe@example.com").await.unwrap()).to_string(); + let john_id = Id::from( + server + .store + .get_or_create_account_id("jdoe@example.com") + .await + .unwrap(), + ) + .to_string(); // Obtain OAuth metadata let metadata: OAuthMetadata = @@ -305,7 +313,7 @@ pub async fn test(params: &mut JMAPTest) { // Destroy test accounts params.client.set_default_account_id(john_id); - destroy_all_mailboxes(¶ms.client).await; + destroy_all_mailboxes(params).await; assert_is_empty(server).await; } diff --git a/tests/src/jmap/blob.rs b/tests/src/jmap/blob.rs index 0fa691b7..490826ca 100644 --- a/tests/src/jmap/blob.rs +++ b/tests/src/jmap/blob.rs @@ -21,6 +21,7 @@ * for more details. */ +use directory::backend::internal::manage::ManageDirectory; use jmap::mailbox::INBOX_ID; use jmap_proto::types::id::Id; use serde_json::Value; @@ -36,7 +37,13 @@ pub async fn test(params: &mut JMAPTest) { .directory .create_test_user_with_email("jdoe@example.com", "12345", "John Doe") .await; - let account_id = Id::from(server.get_account_id("jdoe@example.com").await.unwrap()); + let account_id = Id::from( + server + .store + .get_or_create_account_id("jdoe@example.com") + .await + .unwrap(), + ); server.store.blob_hash_expire_all().await; @@ -488,6 +495,6 @@ pub async fn test(params: &mut JMAPTest) { // Remove test data params.client.set_default_account_id(account_id.to_string()); - destroy_all_mailboxes(¶ms.client).await; + destroy_all_mailboxes(params).await; assert_is_empty(server).await; } diff --git a/tests/src/jmap/crypto.rs b/tests/src/jmap/crypto.rs index 2c29f844..09e7a0a1 100644 --- a/tests/src/jmap/crypto.rs +++ b/tests/src/jmap/crypto.rs @@ -24,6 +24,7 @@ use std::{path::PathBuf, time::Duration}; use ahash::AHashMap; +use directory::backend::internal::manage::ManageDirectory; use jmap::email::crypto::{ try_parse_certs, Algorithm, EncryptMessage, EncryptionMethod, EncryptionParams, }; @@ -44,7 +45,14 @@ pub async fn test(params: &mut JMAPTest) { .directory .create_test_user_with_email("jdoe@example.com", "12345", "John Doe") .await; - let account_id = Id::from(server.get_account_id("jdoe@example.com").await.unwrap()).to_string(); + let account_id = Id::from( + server + .store + .get_or_create_account_id("jdoe@example.com") + .await + .unwrap(), + ) + .to_string(); // Update let mut params = AHashMap::from_iter([ diff --git a/tests/src/jmap/delivery.rs b/tests/src/jmap/delivery.rs index 6445c302..b8fcee7e 100644 --- a/tests/src/jmap/delivery.rs +++ b/tests/src/jmap/delivery.rs @@ -23,6 +23,7 @@ use std::time::Duration; +use directory::backend::internal::manage::ManageDirectory; use jmap_proto::types::{collection::Collection, id::Id}; use tokio::{ @@ -51,12 +52,30 @@ pub async fn test(params: &mut JMAPTest) { .directory .create_test_user_with_email("bill@example.com", "098765", "Bill Foobar") .await; - let account_id_1 = - Id::from(server.get_account_id("jdoe@example.com").await.unwrap()).to_string(); - let account_id_2 = - Id::from(server.get_account_id("jane@example.com").await.unwrap()).to_string(); - let account_id_3 = - Id::from(server.get_account_id("bill@example.com").await.unwrap()).to_string(); + let account_id_1 = Id::from( + server + .store + .get_or_create_account_id("jdoe@example.com") + .await + .unwrap(), + ) + .to_string(); + let account_id_2 = Id::from( + server + .store + .get_or_create_account_id("jane@example.com") + .await + .unwrap(), + ) + .to_string(); + let account_id_3 = Id::from( + server + .store + .get_or_create_account_id("bill@example.com") + .await + .unwrap(), + ) + .to_string(); params .directory .link_test_address("jdoe@example.com", "john.doe@example.com", "alias") @@ -261,7 +280,7 @@ pub async fn test(params: &mut JMAPTest) { // Remove test data for account_id in [&account_id_1, &account_id_2, &account_id_3] { params.client.set_default_account_id(account_id); - destroy_all_mailboxes(¶ms.client).await; + destroy_all_mailboxes(params).await; } assert_is_empty(server).await; } diff --git a/tests/src/jmap/email_copy.rs b/tests/src/jmap/email_copy.rs index e8c6e5ff..84d3a99e 100644 --- a/tests/src/jmap/email_copy.rs +++ b/tests/src/jmap/email_copy.rs @@ -118,8 +118,8 @@ pub async fn test(params: &mut JMAPTest) { .is_none()); // Empty store - destroy_all_mailboxes(¶ms.client).await; + destroy_all_mailboxes(params).await; params.client.set_default_account_id(Id::new(2).to_string()); - destroy_all_mailboxes(¶ms.client).await; + destroy_all_mailboxes(params).await; assert_is_empty(server).await; } diff --git a/tests/src/jmap/email_get.rs b/tests/src/jmap/email_get.rs index cb9aaf38..c10653b8 100644 --- a/tests/src/jmap/email_get.rs +++ b/tests/src/jmap/email_get.rs @@ -188,7 +188,7 @@ pub async fn test(params: &mut JMAPTest) { } } - destroy_all_mailboxes(¶ms.client).await; + destroy_all_mailboxes(params).await; assert_is_empty(server).await; } diff --git a/tests/src/jmap/email_parse.rs b/tests/src/jmap/email_parse.rs index 93b4d789..ecd829ad 100644 --- a/tests/src/jmap/email_parse.rs +++ b/tests/src/jmap/email_parse.rs @@ -252,6 +252,6 @@ pub async fn test(params: &mut JMAPTest) { panic!("Test failed, output saved to {}", test_file.display()); } - destroy_all_mailboxes(¶ms.client).await; + destroy_all_mailboxes(params).await; assert_is_empty(server).await; } diff --git a/tests/src/jmap/email_query.rs b/tests/src/jmap/email_query.rs index 6240ca7b..bbd0a3a0 100644 --- a/tests/src/jmap/email_query.rs +++ b/tests/src/jmap/email_query.rs @@ -120,7 +120,7 @@ pub async fn test(params: &mut JMAPTest, insert: bool) { .unwrap_set_email() .unwrap(); - destroy_all_mailboxes(¶ms.client).await; + destroy_all_mailboxes(params).await; assert_is_empty(server).await; } diff --git a/tests/src/jmap/email_query_changes.rs b/tests/src/jmap/email_query_changes.rs index 4786d65b..9935fc8f 100644 --- a/tests/src/jmap/email_query_changes.rs +++ b/tests/src/jmap/email_query_changes.rs @@ -276,7 +276,7 @@ pub async fn test(params: &mut JMAPTest) { states.push(new_state); } - destroy_all_mailboxes(¶ms.client).await; + destroy_all_mailboxes(params).await; // Delete virtual threads let mut batch = BatchBuilder::new(); diff --git a/tests/src/jmap/email_search_snippet.rs b/tests/src/jmap/email_search_snippet.rs index 42c09edf..f3b88e4d 100644 --- a/tests/src/jmap/email_search_snippet.rs +++ b/tests/src/jmap/email_search_snippet.rs @@ -183,6 +183,6 @@ pub async fn test(params: &mut JMAPTest) { } // Destroy test data - destroy_all_mailboxes(¶ms.client).await; + destroy_all_mailboxes(params).await; assert_is_empty(server).await; } diff --git a/tests/src/jmap/email_set.rs b/tests/src/jmap/email_set.rs index fde4f2c8..024d1413 100644 --- a/tests/src/jmap/email_set.rs +++ b/tests/src/jmap/email_set.rs @@ -46,7 +46,7 @@ pub async fn test(params: &mut JMAPTest) { create(&mut params.client, &mailbox_id).await; update(&mut params.client, &mailbox_id).await; - destroy_all_mailboxes(¶ms.client).await; + destroy_all_mailboxes(params).await; assert_is_empty(server).await; } diff --git a/tests/src/jmap/email_submission.rs b/tests/src/jmap/email_submission.rs index be96c292..f2ce9f49 100644 --- a/tests/src/jmap/email_submission.rs +++ b/tests/src/jmap/email_submission.rs @@ -22,6 +22,7 @@ */ use ahash::AHashMap; +use directory::backend::internal::manage::ManageDirectory; use jmap_client::{ core::set::{SetError, SetErrorType, SetObject}, email_submission::{query::Filter, Address, Delivered, DeliveryStatus, Displayed, UndoStatus}, @@ -96,7 +97,14 @@ pub async fn test(params: &mut JMAPTest) { .directory .create_test_user_with_email("jdoe@example.com", "12345", "John Doe") .await; - let account_id = Id::from(server.get_account_id("jdoe@example.com").await.unwrap()).to_string(); + let account_id = Id::from( + server + .store + .get_or_create_account_id("jdoe@example.com") + .await + .unwrap(), + ) + .to_string(); // Create an identity without using a valid address should fail match client @@ -474,7 +482,7 @@ pub async fn test(params: &mut JMAPTest) { { client.email_submission_destroy(&id).await.unwrap(); } - destroy_all_mailboxes(¶ms.client).await; + destroy_all_mailboxes(params).await; assert_is_empty(server).await; } diff --git a/tests/src/jmap/event_source.rs b/tests/src/jmap/event_source.rs index c31f7858..7173a705 100644 --- a/tests/src/jmap/event_source.rs +++ b/tests/src/jmap/event_source.rs @@ -26,6 +26,7 @@ use std::time::Duration; use crate::jmap::{ assert_is_empty, delivery::SmtpConnection, mailbox::destroy_all_mailboxes, test_account_login, }; +use directory::backend::internal::manage::ManageDirectory; use futures::StreamExt; use jmap::mailbox::INBOX_ID; use jmap_client::{event_source::Changes, mailbox::Role, TypeState}; @@ -45,7 +46,14 @@ pub async fn test(params: &mut JMAPTest) { .directory .create_test_user_with_email("jdoe@example.com", "12345", "John Doe") .await; - let account_id = Id::from(server.get_account_id("jdoe@example.com").await.unwrap()).to_string(); + let account_id = Id::from( + server + .store + .get_or_create_account_id("jdoe@example.com") + .await + .unwrap(), + ) + .to_string(); let client = test_account_login("jdoe@example.com", "12345").await; let mut changes = client @@ -136,7 +144,7 @@ pub async fn test(params: &mut JMAPTest) { assert_ping(&mut event_rx).await; assert_ping(&mut event_rx).await; - destroy_all_mailboxes(¶ms.client).await; + destroy_all_mailboxes(params).await; assert_is_empty(server).await; } diff --git a/tests/src/jmap/mailbox.rs b/tests/src/jmap/mailbox.rs index b7c60cdd..b92606c0 100644 --- a/tests/src/jmap/mailbox.rs +++ b/tests/src/jmap/mailbox.rs @@ -36,7 +36,7 @@ use store::ahash::AHashMap; use crate::jmap::assert_is_empty; -use super::JMAPTest; +use super::{wait_for_index, JMAPTest}; pub async fn test(params: &mut JMAPTest) { println!("Running Mailbox tests..."); @@ -607,8 +607,8 @@ pub async fn test(params: &mut JMAPTest) { ["inbox", "sent", "spam"] ); - destroy_all_mailboxes(client).await; - client.set_default_account_id(Id::from(1u64)); + destroy_all_mailboxes(params).await; + params.client.set_default_account_id(Id::from(1u64)); assert_is_empty(server).await; } @@ -657,7 +657,12 @@ fn build_create_query( } } -pub async fn destroy_all_mailboxes(client: &Client) { +pub async fn destroy_all_mailboxes(test: &JMAPTest) { + wait_for_index(&test.server).await; + destroy_all_mailboxes_no_wait(&test.client).await; +} + +pub async fn destroy_all_mailboxes_no_wait(client: &Client) { let mut request = client.build(); request.query_mailbox().arguments().sort_as_tree(true); let mut ids = request.send_query_mailbox().await.unwrap().take_ids(); diff --git a/tests/src/jmap/mod.rs b/tests/src/jmap/mod.rs index 45c86047..23b6380c 100644 --- a/tests/src/jmap/mod.rs +++ b/tests/src/jmap/mod.rs @@ -282,7 +282,7 @@ pub async fn jmap_tests() { .await; let coco = 1; //email_query::test(&mut params, delete).await; - /*email_get::test(&mut params).await; + //email_get::test(&mut params).await; email_set::test(&mut params).await; email_parse::test(&mut params).await; email_search_snippet::test(&mut params).await; @@ -291,7 +291,7 @@ pub async fn jmap_tests() { email_copy::test(&mut params).await; thread_get::test(&mut params).await; thread_merge::test(&mut params).await; - mailbox::test(&mut params).await;*/ + mailbox::test(&mut params).await; delivery::test(&mut params).await; auth_acl::test(&mut params).await; auth_limits::test(&mut params).await; @@ -404,7 +404,6 @@ async fn init_jmap_tests(store_id: &str, delete_if_exists: bool) -> JMAPTest { directory .create_test_user("admin", "secret", "Superuser") .await; - directory.add_to_group("admin", "superusers").await; if delete_if_exists { jmap.store.destroy().await; diff --git a/tests/src/jmap/push_subscription.rs b/tests/src/jmap/push_subscription.rs index 38a1c47e..2c0c1529 100644 --- a/tests/src/jmap/push_subscription.rs +++ b/tests/src/jmap/push_subscription.rs @@ -30,6 +30,7 @@ use std::{ }; use base64::{engine::general_purpose, Engine}; +use directory::backend::internal::manage::ManageDirectory; use ece::EcKeyComponents; use hyper::{body, header::CONTENT_ENCODING, server::conn::http1, service::service_fn, StatusCode}; use hyper_util::rt::TokioIo; @@ -86,7 +87,13 @@ pub async fn test(params: &mut JMAPTest) { .directory .create_test_user_with_email("jdoe@example.com", "12345", "John Doe") .await; - let account_id = Id::from(server.get_account_id("jdoe@example.com").await.unwrap()); + let account_id = Id::from( + server + .store + .get_or_create_account_id("jdoe@example.com") + .await + .unwrap(), + ); params.client.set_default_account_id(account_id); let client = test_account_login("jdoe@example.com", "12345").await; @@ -219,7 +226,7 @@ pub async fn test(params: &mut JMAPTest) { client.mailbox_destroy(&mailbox_id, true).await.unwrap(); expect_nothing(&mut event_rx).await; - destroy_all_mailboxes(¶ms.client).await; + destroy_all_mailboxes(params).await; assert_is_empty(server).await; } diff --git a/tests/src/jmap/quota.rs b/tests/src/jmap/quota.rs index b270507a..98a29ead 100644 --- a/tests/src/jmap/quota.rs +++ b/tests/src/jmap/quota.rs @@ -25,6 +25,7 @@ use crate::jmap::{ assert_is_empty, delivery::SmtpConnection, jmap_raw_request, mailbox::destroy_all_mailboxes, test_account_login, }; +use directory::backend::internal::manage::ManageDirectory; use jmap::{blob::upload::DISABLE_UPLOAD_QUOTA, mailbox::INBOX_ID}; use jmap_client::{ core::set::{SetErrorType, SetObject}, @@ -45,8 +46,20 @@ pub async fn test(params: &mut JMAPTest) { .directory .create_test_user_with_email("robert@example.com", "aabbcc", "Robert Foobar") .await; - let other_account_id = Id::from(server.get_account_id("jdoe@example.com").await.unwrap()); - let account_id = Id::from(server.get_account_id("robert@example.com").await.unwrap()); + let other_account_id = Id::from( + server + .store + .get_or_create_account_id("jdoe@example.com") + .await + .unwrap(), + ); + let account_id = Id::from( + server + .store + .get_or_create_account_id("robert@example.com") + .await + .unwrap(), + ); params .directory .set_test_quota("robert@example.com", 1024) @@ -326,7 +339,7 @@ pub async fn test(params: &mut JMAPTest) { // Remove test data for account_id in [&account_id, &other_account_id] { params.client.set_default_account_id(account_id.to_string()); - destroy_all_mailboxes(¶ms.client).await; + destroy_all_mailboxes(params).await; } assert_is_empty(server).await; } diff --git a/tests/src/jmap/sieve_script.rs b/tests/src/jmap/sieve_script.rs index 56ad8348..adedeb61 100644 --- a/tests/src/jmap/sieve_script.rs +++ b/tests/src/jmap/sieve_script.rs @@ -21,6 +21,7 @@ * for more details. */ +use directory::backend::internal::manage::ManageDirectory; use jmap_client::{ core::set::{SetError, SetErrorType}, email, mailbox, @@ -53,7 +54,14 @@ pub async fn test(params: &mut JMAPTest) { .directory .create_test_user_with_email("jdoe@example.com", "12345", "John Doe") .await; - let account_id = Id::from(server.get_account_id("jdoe@example.com").await.unwrap()).to_string(); + let account_id = Id::from( + server + .store + .get_or_create_account_id("jdoe@example.com") + .await + .unwrap(), + ) + .to_string(); client.set_default_account_id(&account_id); // Validate scripts @@ -486,7 +494,7 @@ pub async fn test(params: &mut JMAPTest) { for id in request.send_query_sieve_script().await.unwrap().take_ids() { client.sieve_script_destroy(&id).await.unwrap(); } - destroy_all_mailboxes(¶ms.client).await; + destroy_all_mailboxes(params).await; assert_is_empty(server).await; } diff --git a/tests/src/jmap/stress_test.rs b/tests/src/jmap/stress_test.rs index 24b7c7c3..9a13a4fa 100644 --- a/tests/src/jmap/stress_test.rs +++ b/tests/src/jmap/stress_test.rs @@ -23,7 +23,7 @@ use std::{sync::Arc, time::Duration}; -use crate::jmap::mailbox::destroy_all_mailboxes; +use crate::jmap::{mailbox::destroy_all_mailboxes_no_wait, wait_for_index}; use futures::future::join_all; use jmap::{mailbox::UidMailbox, JMAP}; use jmap_client::{ @@ -274,7 +274,8 @@ async fn email_tests(server: Arc, client: Arc) { } } - destroy_all_mailboxes(&client).await; + wait_for_index(&server).await; + destroy_all_mailboxes_no_wait(&client).await; assert_is_empty(server.clone()).await; } } @@ -353,7 +354,8 @@ async fn mailbox_tests(server: Arc, client: Arc) { join_all(futures).await; - destroy_all_mailboxes(&client).await; + wait_for_index(&server).await; + destroy_all_mailboxes_no_wait(&client).await; assert_is_empty(server).await; } diff --git a/tests/src/jmap/thread_get.rs b/tests/src/jmap/thread_get.rs index 2e8e42cc..6a89ffc5 100644 --- a/tests/src/jmap/thread_get.rs +++ b/tests/src/jmap/thread_get.rs @@ -68,6 +68,6 @@ pub async fn test(params: &mut JMAPTest) { expected_result ); - destroy_all_mailboxes(¶ms.client).await; + destroy_all_mailboxes(params).await; assert_is_empty(server).await; } diff --git a/tests/src/jmap/thread_merge.rs b/tests/src/jmap/thread_merge.rs index 34100a01..c719ba53 100644 --- a/tests/src/jmap/thread_merge.rs +++ b/tests/src/jmap/thread_merge.rs @@ -199,8 +199,10 @@ pub async fn test(params: &mut JMAPTest) { // Delete all messages and make sure no keys are left in the store. for (base_test_num, mailbox_ids) in all_mailboxes { for (test_num, _) in mailbox_ids.into_iter().enumerate() { - client.set_default_account_id(Id::new((base_test_num + test_num) as u64).to_string()); - destroy_all_mailboxes(client).await; + params + .client + .set_default_account_id(Id::new((base_test_num + test_num) as u64).to_string()); + destroy_all_mailboxes(params).await; } } diff --git a/tests/src/jmap/vacation_response.rs b/tests/src/jmap/vacation_response.rs index 7144b251..942e41b5 100644 --- a/tests/src/jmap/vacation_response.rs +++ b/tests/src/jmap/vacation_response.rs @@ -23,6 +23,7 @@ use chrono::{Duration, Utc}; +use directory::backend::internal::manage::ManageDirectory; use jmap_proto::types::id::Id; use std::time::Instant; @@ -47,7 +48,14 @@ pub async fn test(params: &mut JMAPTest) { .directory .create_test_user_with_email("jdoe@example.com", "12345", "John Doe") .await; - let account_id = Id::from(server.get_account_id("jdoe@example.com").await.unwrap()).to_string(); + let account_id = Id::from( + server + .store + .get_or_create_account_id("jdoe@example.com") + .await + .unwrap(), + ) + .to_string(); client.set_default_account_id(&account_id); // Start mock SMTP server @@ -175,6 +183,6 @@ pub async fn test(params: &mut JMAPTest) { // Remove test data client.vacation_response_destroy().await.unwrap(); - destroy_all_mailboxes(¶ms.client).await; + destroy_all_mailboxes(params).await; assert_is_empty(server).await; } diff --git a/tests/src/jmap/websocket.rs b/tests/src/jmap/websocket.rs index 5fb12906..80ac2e39 100644 --- a/tests/src/jmap/websocket.rs +++ b/tests/src/jmap/websocket.rs @@ -22,6 +22,7 @@ */ use ahash::AHashSet; +use directory::backend::internal::manage::ManageDirectory; use futures::StreamExt; use jmap_client::{ client_ws::WebSocketMessage, @@ -49,7 +50,14 @@ pub async fn test(params: &mut JMAPTest) { .directory .create_test_user_with_email("jdoe@example.com", "12345", "John Doe") .await; - let account_id = Id::from(server.get_account_id("jdoe@example.com").await.unwrap()).to_string(); + let account_id = Id::from( + server + .store + .get_or_create_account_id("jdoe@example.com") + .await + .unwrap(), + ) + .to_string(); let client = test_account_login("jdoe@example.com", "12345").await; let mut ws_stream = client.connect_ws().await.unwrap(); @@ -124,7 +132,7 @@ pub async fn test(params: &mut JMAPTest) { expect_nothing(&mut stream_rx).await; params.client.set_default_account_id(account_id); - destroy_all_mailboxes(¶ms.client).await; + destroy_all_mailboxes(params).await; assert_is_empty(server).await; } diff --git a/tests/src/smtp/inbound/auth.rs b/tests/src/smtp/inbound/auth.rs index a8fe7552..9510c7f5 100644 --- a/tests/src/smtp/inbound/auth.rs +++ b/tests/src/smtp/inbound/auth.rs @@ -39,7 +39,7 @@ const DIRECTORY: &str = r#" [directory."local"] type = "memory" -[[directory."local".users]] +[[directory."local".principals]] name = "john" description = "John Doe" secret = "secret" @@ -47,7 +47,7 @@ email = ["john@example.org", "jdoe@example.org", "john.doe@example.org"] email-list = ["info@example.org"] member-of = ["sales"] -[[directory."local".users]] +[[directory."local".principals]] name = "jane" description = "Jane Doe" secret = "p4ssw0rd" diff --git a/tests/src/smtp/inbound/data.rs b/tests/src/smtp/inbound/data.rs index 5d5e726e..91e9d3ee 100644 --- a/tests/src/smtp/inbound/data.rs +++ b/tests/src/smtp/inbound/data.rs @@ -39,25 +39,25 @@ const DIRECTORY: &str = r#" [directory."local"] type = "memory" -[[directory."local".users]] +[[directory."local".principals]] name = "john" description = "John Doe" secret = "secret" email = ["john@foobar.org", "jdoe@example.org", "john.doe@example.org"] -[[directory."local".users]] +[[directory."local".principals]] name = "jane" description = "Jane Doe" secret = "p4ssw0rd" email = "jane@domain.net" -[[directory."local".users]] +[[directory."local".principals]] name = "bill" description = "Bill Foobar" secret = "p4ssw0rd" email = "bill@foobar.org" -[[directory."local".users]] +[[directory."local".principals]] name = "mike" description = "Mike Foobar" secret = "p4ssw0rd" diff --git a/tests/src/smtp/inbound/dmarc.rs b/tests/src/smtp/inbound/dmarc.rs index f175ef24..2a8cafa5 100644 --- a/tests/src/smtp/inbound/dmarc.rs +++ b/tests/src/smtp/inbound/dmarc.rs @@ -53,7 +53,7 @@ const DIRECTORY: &str = r#" [directory."local"] type = "memory" -[[directory."local".users]] +[[directory."local".principals]] name = "john" description = "John Doe" secret = "secret" diff --git a/tests/src/smtp/inbound/rcpt.rs b/tests/src/smtp/inbound/rcpt.rs index 1805e27c..590e2adc 100644 --- a/tests/src/smtp/inbound/rcpt.rs +++ b/tests/src/smtp/inbound/rcpt.rs @@ -41,25 +41,25 @@ const DIRECTORY: &str = r#" [directory."local"] type = "memory" -[[directory."local".users]] +[[directory."local".principals]] name = "john" description = "John Doe" secret = "secret" email = "john@foobar.org" -[[directory."local".users]] +[[directory."local".principals]] name = "jane" description = "Jane Doe" secret = "p4ssw0rd" email = "jane@foobar.org" -[[directory."local".users]] +[[directory."local".principals]] name = "bill" description = "Bill Foobar" secret = "p4ssw0rd" email = "bill@foobar.org" -[[directory."local".users]] +[[directory."local".principals]] name = "mike" description = "Mike Foobar" secret = "p4ssw0rd" diff --git a/tests/src/smtp/inbound/sign.rs b/tests/src/smtp/inbound/sign.rs index 47135c18..158a3dcb 100644 --- a/tests/src/smtp/inbound/sign.rs +++ b/tests/src/smtp/inbound/sign.rs @@ -99,7 +99,7 @@ const DIRECTORY: &str = r#" [directory."local"] type = "memory" -[[directory."local".users]] +[[directory."local".principals]] name = "john" description = "John Doe" secret = "secret" diff --git a/tests/src/smtp/inbound/vrfy.rs b/tests/src/smtp/inbound/vrfy.rs index 12e6d5f9..610ff96a 100644 --- a/tests/src/smtp/inbound/vrfy.rs +++ b/tests/src/smtp/inbound/vrfy.rs @@ -38,21 +38,21 @@ const DIRECTORY: &str = r#" [directory."local"] type = "memory" -[[directory."local".users]] +[[directory."local".principals]] name = "john" description = "John Doe" secret = "secret" email = ["john@foobar.org"] email-list = ["sales@foobar.org"] -[[directory."local".users]] +[[directory."local".principals]] name = "jane" description = "Jane Doe" secret = "p4ssw0rd" email = "jane@foobar.org" email-list = ["sales@foobar.org"] -[[directory."local".users]] +[[directory."local".principals]] name = "bill" description = "Bill Foobar" secret = "p4ssw0rd" diff --git a/tests/src/smtp/management/queue.rs b/tests/src/smtp/management/queue.rs index eb820029..07150a94 100644 --- a/tests/src/smtp/management/queue.rs +++ b/tests/src/smtp/management/queue.rs @@ -54,7 +54,7 @@ type = "memory" [directory."local".options] superuser-group = "superusers" -[[directory."local".users]] +[[directory."local".principals]] name = "admin" description = "Superuser" secret = "secret" diff --git a/tests/src/smtp/management/report.rs b/tests/src/smtp/management/report.rs index 357059e8..b190a6bc 100644 --- a/tests/src/smtp/management/report.rs +++ b/tests/src/smtp/management/report.rs @@ -57,7 +57,7 @@ type = "memory" [directory."local".options] superuser-group = "superusers" -[[directory."local".users]] +[[directory."local".principals]] name = "admin" description = "Superuser" secret = "secret" diff --git a/tests/src/smtp/mod.rs b/tests/src/smtp/mod.rs index c0ca8b93..cf4bb95e 100644 --- a/tests/src/smtp/mod.rs +++ b/tests/src/smtp/mod.rs @@ -25,7 +25,7 @@ use std::{path::PathBuf, sync::Arc, time::Duration}; use ahash::AHashMap; use dashmap::DashMap; -use directory::memory::MemoryDirectory; +use directory::backend::memory::MemoryDirectory; use mail_auth::{ common::lru::{DnsCache, LruCache}, hickory_resolver::config::{ResolverConfig, ResolverOpts},