diff --git a/Cargo.lock b/Cargo.lock index 1b28dd10..7f6bbf53 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7470,6 +7470,7 @@ dependencies = [ "parking_lot", "pem", "privdrop", + "psl", "rand 0.8.5", "rcgen 0.13.1", "regex", diff --git a/crates/common/src/addresses.rs b/crates/common/src/addresses.rs index d56385b1..b6d05cf0 100644 --- a/crates/common/src/addresses.rs +++ b/crates/common/src/addresses.rs @@ -6,7 +6,7 @@ use std::borrow::Cow; -use directory::Directory; +use directory::{backend::RcptType, Directory}; use utils::config::{utils::AsKey, Config}; use crate::{ @@ -18,12 +18,12 @@ use crate::{ }; impl Server { - pub async fn email_to_ids( + pub async fn email_to_id( &self, directory: &Directory, email: &str, session_id: u64, - ) -> trc::Result> { + ) -> trc::Result> { let mut address = self .core .smtp @@ -34,9 +34,9 @@ impl Server { .await; for _ in 0..2 { - let result = directory.email_to_ids(address.as_ref()).await?; + let result = directory.email_to_id(address.as_ref()).await?; - if !result.is_empty() { + if result.is_some() { return Ok(result); } else if let Some(catch_all) = self .core @@ -53,7 +53,7 @@ impl Server { } } - Ok(vec![]) + Ok(None) } pub async fn rcpt( @@ -61,7 +61,7 @@ impl Server { directory: &Directory, email: &str, session_id: u64, - ) -> trc::Result { + ) -> trc::Result { // Expand subaddress let mut address = self .core @@ -73,8 +73,9 @@ impl Server { .await; for _ in 0..2 { - if directory.rcpt(address.as_ref()).await? { - return Ok(true); + let rcpt_type = directory.rcpt(address.as_ref()).await?; + if rcpt_type != RcptType::Invalid { + return Ok(rcpt_type); } else if let Some(catch_all) = self .core .smtp @@ -90,7 +91,7 @@ impl Server { } } - Ok(false) + Ok(RcptType::Invalid) } pub async fn vrfy( diff --git a/crates/common/src/expr/functions/asynch.rs b/crates/common/src/expr/functions/asynch.rs index 162929cc..55b36164 100644 --- a/crates/common/src/expr/functions/asynch.rs +++ b/crates/common/src/expr/functions/asynch.rs @@ -1,5 +1,6 @@ use std::{cmp::Ordering, net::IpAddr, vec::IntoIter}; +use directory::backend::RcptType; use mail_auth::IpLookupStrategy; use store::{Deserialize, Rows, Value}; use trc::AddContext; @@ -36,7 +37,7 @@ impl Server { .rcpt(address.as_ref()) .await .caused_by(trc::location!()) - .map(|v| v.into()) + .map(|v| (v != RcptType::Invalid).into()) } F_KEY_GET => { let store = params.next_as_string(); diff --git a/crates/directory/src/backend/imap/lookup.rs b/crates/directory/src/backend/imap/lookup.rs index 49fd6fbf..b6e33499 100644 --- a/crates/directory/src/backend/imap/lookup.rs +++ b/crates/directory/src/backend/imap/lookup.rs @@ -7,7 +7,7 @@ use mail_send::Credentials; use smtp_proto::{AUTH_CRAM_MD5, AUTH_LOGIN, AUTH_OAUTHBEARER, AUTH_PLAIN, AUTH_XOAUTH2}; -use crate::{IntoError, Principal, QueryBy}; +use crate::{backend::RcptType, IntoError, Principal, QueryBy}; use super::{ImapDirectory, ImapError}; @@ -60,11 +60,11 @@ impl ImapDirectory { } } - pub async fn email_to_ids(&self, _address: &str) -> trc::Result> { + pub async fn email_to_id(&self, _address: &str) -> trc::Result> { Err(trc::StoreEvent::NotSupported.caused_by(trc::location!())) } - pub async fn rcpt(&self, _address: &str) -> trc::Result { + pub async fn rcpt(&self, _address: &str) -> trc::Result { Err(trc::StoreEvent::NotSupported.caused_by(trc::location!())) } diff --git a/crates/directory/src/backend/internal/lookup.rs b/crates/directory/src/backend/internal/lookup.rs index d718dad9..a9a43958 100644 --- a/crates/directory/src/backend/internal/lookup.rs +++ b/crates/directory/src/backend/internal/lookup.rs @@ -11,7 +11,7 @@ use store::{ }; use trc::AddContext; -use crate::{Principal, QueryBy, Type}; +use crate::{backend::RcptType, Principal, QueryBy, Type}; use super::{manage::ManageDirectory, PrincipalField, PrincipalInfo}; @@ -22,11 +22,12 @@ pub trait DirectoryStore: Sync + Send { by: QueryBy<'_>, return_member_of: bool, ) -> trc::Result>; - async fn email_to_ids(&self, email: &str) -> trc::Result>; + async fn email_to_id(&self, address: &str) -> trc::Result>; async fn is_local_domain(&self, domain: &str) -> trc::Result; - async fn rcpt(&self, address: &str) -> trc::Result; + async fn rcpt(&self, address: &str) -> trc::Result; async fn vrfy(&self, address: &str) -> trc::Result>; async fn expn(&self, address: &str) -> trc::Result>; + async fn expn_by_id(&self, id: u32) -> trc::Result>; } impl DirectoryStore for Store { @@ -77,21 +78,12 @@ impl DirectoryStore for Store { Ok(None) } - async fn email_to_ids(&self, email: &str) -> trc::Result> { - if let Some(ptype) = self - .get_value::(ValueKey::from(ValueClass::Directory( - DirectoryClass::EmailToId(email.as_bytes().to_vec()), - ))) - .await? - { - if ptype.typ != Type::List { - Ok(vec![ptype.id]) - } else { - self.get_members(ptype.id).await - } - } else { - Ok(Vec::new()) - } + async fn email_to_id(&self, address: &str) -> trc::Result> { + self.get_value::(ValueKey::from(ValueClass::Directory( + DirectoryClass::EmailToId(address.as_bytes().to_vec()), + ))) + .await + .map(|ptype| ptype.map(|ptype| ptype.id)) } async fn is_local_domain(&self, domain: &str) -> trc::Result { @@ -102,12 +94,21 @@ impl DirectoryStore for Store { .map(|p| p.map_or(false, |p| p.typ == Type::Domain)) } - async fn rcpt(&self, address: &str) -> trc::Result { - self.get_value::<()>(ValueKey::from(ValueClass::Directory( - DirectoryClass::EmailToId(address.as_bytes().to_vec()), - ))) - .await - .map(|ids| ids.is_some()) + async fn rcpt(&self, address: &str) -> trc::Result { + if let Some(pinfo) = self + .get_value::(ValueKey::from(ValueClass::Directory( + DirectoryClass::EmailToId(address.as_bytes().to_vec()), + ))) + .await? + { + if pinfo.typ != Type::List { + Ok(RcptType::Mailbox) + } else { + self.expn_by_id(pinfo.id).await.map(RcptType::List) + } + } else { + Ok(RcptType::Invalid) + } } async fn vrfy(&self, address: &str) -> trc::Result> { @@ -143,7 +144,6 @@ impl DirectoryStore for Store { } async fn expn(&self, address: &str) -> trc::Result> { - let mut results = Vec::new(); if let Some(ptype) = self .get_value::(ValueKey::from(ValueClass::Directory( DirectoryClass::EmailToId(address.as_bytes().to_vec()), @@ -151,17 +151,32 @@ impl DirectoryStore for Store { .await? .filter(|p| p.typ == Type::List) { - for account_id in self.get_members(ptype.id).await? { - if let Some(email) = self - .get_principal(account_id) - .await? - .and_then(|mut p| p.take_str(PrincipalField::Emails)) - { - results.push(email); - } + self.expn_by_id(ptype.id).await + } else { + Ok(vec![]) + } + } + + async fn expn_by_id(&self, list_id: u32) -> trc::Result> { + let mut results = Vec::new(); + for account_id in self.get_members(list_id).await? { + if let Some(email) = self + .get_principal(account_id) + .await? + .and_then(|mut p| p.take_str(PrincipalField::Emails)) + { + results.push(email); } } + if let Some(emails) = self + .get_principal(list_id) + .await? + .and_then(|mut p| p.take_str_array(PrincipalField::ExternalMembers)) + { + results.extend(emails); + } + Ok(results) } } diff --git a/crates/directory/src/backend/internal/manage.rs b/crates/directory/src/backend/internal/manage.rs index 5241d1b9..52556676 100644 --- a/crates/directory/src/backend/internal/manage.rs +++ b/crates/directory/src/backend/internal/manage.rs @@ -14,10 +14,11 @@ use store::{ Deserialize, IterateParams, Serialize, Store, ValueKey, U32_LEN, }; use trc::AddContext; +use utils::sanitize_email; use crate::{ - Permission, Permissions, Principal, QueryBy, Type, MAX_TYPE_ID, ROLE_ADMIN, ROLE_TENANT_ADMIN, - ROLE_USER, + backend::RcptType, Permission, Permissions, Principal, QueryBy, Type, MAX_TYPE_ID, ROLE_ADMIN, + ROLE_TENANT_ADMIN, ROLE_USER, }; use super::{ @@ -387,7 +388,7 @@ impl ManageDirectory for Store { if principal.typ != Type::OauthClient { for email in principal.iter_mut_str(PrincipalField::Emails) { *email = email.to_lowercase(); - if self.rcpt(email).await.caused_by(trc::location!())? { + if self.rcpt(email).await.caused_by(trc::location!())? != RcptType::Invalid { return Err(err_exists(PrincipalField::Emails, email.to_string())); } if let Some(domain) = email.split('@').nth(1) { @@ -1423,25 +1424,62 @@ impl ManageDirectory for Store { .inner .retain_int(change.field, |v| *v != permission); } - (PrincipalAction::Set, PrincipalField::Urls, PrincipalValue::StringList(urls)) => { - if !urls.is_empty() { - principal.inner.set(change.field, urls); + ( + PrincipalAction::Set, + PrincipalField::Urls | PrincipalField::ExternalMembers, + PrincipalValue::StringList(mut items), + ) => { + if matches!(change.field, PrincipalField::ExternalMembers) { + items = items + .into_iter() + .map(|item| { + sanitize_email(&item).ok_or_else(|| { + error( + "Invalid email address", + format!( + "Invalid value {:?} for {}", + item, + change.field.as_str() + ) + .into(), + ) + }) + }) + .collect::>()?; + } + + if !items.is_empty() { + principal.inner.set(change.field, items); } else { principal.inner.remove(change.field); } } - (PrincipalAction::AddItem, PrincipalField::Urls, PrincipalValue::String(url)) => { - if !principal.inner.has_str_value(change.field, &url) { - principal.inner.append_str(change.field, url); + ( + PrincipalAction::AddItem, + PrincipalField::Urls | PrincipalField::ExternalMembers, + PrincipalValue::String(mut item), + ) => { + if matches!(change.field, PrincipalField::ExternalMembers) { + item = sanitize_email(&item).ok_or_else(|| { + error( + "Invalid email address", + format!("Invalid value {:?} for {}", item, change.field.as_str()) + .into(), + ) + })? + } + + if !principal.inner.has_str_value(change.field, &item) { + principal.inner.append_str(change.field, item); } } ( PrincipalAction::RemoveItem, - PrincipalField::Urls, - PrincipalValue::String(url), + PrincipalField::Urls | PrincipalField::ExternalMembers, + PrincipalValue::String(item), ) => { - if principal.inner.has_str_value(change.field, &url) { - principal.inner.retain_str(change.field, |v| *v != url); + if principal.inner.has_str_value(change.field, &item) { + principal.inner.retain_str(change.field, |v| *v != item); } } @@ -1835,7 +1873,7 @@ impl ValidateDirectory for Store { tenant_id: Option, create_if_missing: bool, ) -> trc::Result<()> { - if self.rcpt(email).await.caused_by(trc::location!())? { + if self.rcpt(email).await.caused_by(trc::location!())? != RcptType::Invalid { Err(err_exists(PrincipalField::Emails, email.to_string())) } else if let Some(domain) = email.split('@').nth(1) { match self diff --git a/crates/directory/src/backend/internal/mod.rs b/crates/directory/src/backend/internal/mod.rs index 29be134e..8130a21d 100644 --- a/crates/directory/src/backend/internal/mod.rs +++ b/crates/directory/src/backend/internal/mod.rs @@ -412,6 +412,7 @@ pub enum PrincipalField { DisabledPermissions, Picture, Urls, + ExternalMembers, } #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] @@ -491,6 +492,7 @@ impl PrincipalField { PrincipalField::UsedQuota => 13, PrincipalField::Picture => 14, PrincipalField::Urls => 15, + PrincipalField::ExternalMembers => 16, } } @@ -512,6 +514,7 @@ impl PrincipalField { 13 => Some(PrincipalField::UsedQuota), 14 => Some(PrincipalField::Picture), 15 => Some(PrincipalField::Urls), + 16 => Some(PrincipalField::ExternalMembers), _ => None, } } @@ -534,6 +537,7 @@ impl PrincipalField { PrincipalField::DisabledPermissions => "disabledPermissions", PrincipalField::Picture => "picture", PrincipalField::Urls => "urls", + PrincipalField::ExternalMembers => "externalMembers", } } @@ -555,6 +559,7 @@ impl PrincipalField { "disabledPermissions" => Some(PrincipalField::DisabledPermissions), "picture" => Some(PrincipalField::Picture), "urls" => Some(PrincipalField::Urls), + "externalMembers" => Some(PrincipalField::ExternalMembers), _ => None, } } diff --git a/crates/directory/src/backend/ldap/config.rs b/crates/directory/src/backend/ldap/config.rs index c8811f2c..e818138c 100644 --- a/crates/directory/src/backend/ldap/config.rs +++ b/crates/directory/src/backend/ldap/config.rs @@ -52,9 +52,6 @@ impl LdapDirectory { base_dn: config.value_require((&prefix, "base-dn"))?.to_string(), filter_name: LdapFilter::from_config(config, (&prefix, "filter.name")), filter_email: LdapFilter::from_config(config, (&prefix, "filter.email")), - 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")), attr_name: config .values((&prefix, "attributes.name")) .map(|(_, v)| v.to_string()) diff --git a/crates/directory/src/backend/ldap/lookup.rs b/crates/directory/src/backend/ldap/lookup.rs index c68c70d8..f0e5c0de 100644 --- a/crates/directory/src/backend/ldap/lookup.rs +++ b/crates/directory/src/backend/ldap/lookup.rs @@ -9,10 +9,13 @@ use mail_send::Credentials; use trc::AddContext; use crate::{ - backend::internal::{ - lookup::DirectoryStore, - manage::{self, ManageDirectory, UpdatePrincipal}, - PrincipalField, + backend::{ + internal::{ + lookup::DirectoryStore, + manage::{self, ManageDirectory, UpdatePrincipal}, + PrincipalField, + }, + RcptType, }, IntoError, Principal, QueryBy, Type, ROLE_ADMIN, ROLE_USER, }; @@ -212,7 +215,7 @@ impl LdapDirectory { Ok(Some(principal)) } - pub async fn email_to_ids(&self, address: &str) -> trc::Result> { + pub async fn email_to_id(&self, address: &str) -> trc::Result> { let filter = self.mappings.filter_email.build(address.as_ref()); let rs = self .pool @@ -240,29 +243,28 @@ impl LdapDirectory { .collect::>() ); - 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 { + 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( - self.data_store - .get_or_create_principal_id(name, Type::Individual) - .await?, - ); - break 'outer; + return self + .data_store + .get_or_create_principal_id(name, Type::Individual) + .await + .map(Some); } } } } - Ok(ids) + Ok(None) } - pub async fn rcpt(&self, address: &str) -> trc::Result { + pub async fn rcpt(&self, address: &str) -> trc::Result { let filter = self.mappings.filter_email.build(address.as_ref()); - self.pool + let result = self + .pool .get() .await .map_err(|err| err.into_error().caused_by(trc::location!()))? @@ -277,7 +279,11 @@ impl LdapDirectory { .next() .await .map(|entry| { - let success = entry.is_some(); + let result = if entry.is_some() { + RcptType::Mailbox + } else { + RcptType::Invalid + }; trc::event!( Store(trc::StoreEvent::LdapQuery), @@ -285,131 +291,33 @@ impl LdapDirectory { Result = entry.map(|e| trc::Value::from(format!("{e:?}"))) ); - success + result }) - .map_err(|err| err.into_error().caused_by(trc::location!())) + .map_err(|err| err.into_error().caused_by(trc::location!()))?; + + if result != RcptType::Invalid { + Ok(result) + } else { + self.data_store.rcpt(address).await.map(|result| { + if matches!(result, RcptType::List(_)) { + result + } else { + RcptType::Invalid + } + }) + } } pub async fn vrfy(&self, address: &str) -> trc::Result> { - let filter = self.mappings.filter_verify.build(address); - let mut stream = self - .pool - .get() - .await - .map_err(|err| err.into_error().caused_by(trc::location!()))? - .streaming_search( - &self.mappings.base_dn, - Scope::Subtree, - &filter, - &self.mappings.attr_email_address, - ) - .await - .map_err(|err| err.into_error().caused_by(trc::location!()))?; - - let mut emails = Vec::new(); - while let Some(entry) = stream - .next() - .await - .map_err(|err| err.into_error().caused_by(trc::location!()))? - { - 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()); - } - } - } - } - } - - trc::event!( - Store(trc::StoreEvent::LdapQuery), - Details = filter, - Result = emails - .iter() - .map(|e| trc::Value::from(e.clone())) - .collect::>() - ); - - Ok(emails) + self.data_store.vrfy(address).await } pub async fn expn(&self, address: &str) -> trc::Result> { - let filter = self.mappings.filter_expand.build(address); - let mut stream = self - .pool - .get() - .await - .map_err(|err| err.into_error().caused_by(trc::location!()))? - .streaming_search( - &self.mappings.base_dn, - Scope::Subtree, - &filter, - &self.mappings.attr_email_address, - ) - .await - .map_err(|err| err.into_error().caused_by(trc::location!()))?; - - let mut emails = Vec::new(); - while let Some(entry) = stream - .next() - .await - .map_err(|err| err.into_error().caused_by(trc::location!()))? - { - 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()); - } - } - } - } - } - - trc::event!( - Store(trc::StoreEvent::LdapQuery), - Details = filter, - Result = emails - .iter() - .map(|e| trc::Value::from(e.clone())) - .collect::>() - ); - - Ok(emails) + self.data_store.expn(address).await } pub async fn is_local_domain(&self, domain: &str) -> trc::Result { - let filter = self.mappings.filter_domains.build(domain); - self.pool - .get() - .await - .map_err(|err| err.into_error().caused_by(trc::location!()))? - .streaming_search( - &self.mappings.base_dn, - Scope::Subtree, - &filter, - Vec::::new(), - ) - .await - .map_err(|err| err.into_error().caused_by(trc::location!()))? - .next() - .await - .map(|entry| { - let success = entry.is_some(); - - trc::event!( - Store(trc::StoreEvent::LdapQuery), - Details = filter, - Result = entry.map(|e| trc::Value::from(format!("{e:?}"))) - ); - - success - }) - .map_err(|err| err.into_error().caused_by(trc::location!())) + self.data_store.is_local_domain(domain).await } } diff --git a/crates/directory/src/backend/ldap/mod.rs b/crates/directory/src/backend/ldap/mod.rs index 1037b2c8..a72e4148 100644 --- a/crates/directory/src/backend/ldap/mod.rs +++ b/crates/directory/src/backend/ldap/mod.rs @@ -24,9 +24,6 @@ pub struct LdapMappings { base_dn: String, filter_name: LdapFilter, filter_email: LdapFilter, - filter_verify: LdapFilter, - filter_expand: LdapFilter, - filter_domains: LdapFilter, attr_name: Vec, attr_type: Vec, attr_groups: Vec, diff --git a/crates/directory/src/backend/memory/lookup.rs b/crates/directory/src/backend/memory/lookup.rs index 60270097..3474069a 100644 --- a/crates/directory/src/backend/memory/lookup.rs +++ b/crates/directory/src/backend/memory/lookup.rs @@ -6,7 +6,10 @@ use mail_send::Credentials; -use crate::{backend::internal::PrincipalField, Principal, QueryBy}; +use crate::{ + backend::{internal::PrincipalField, RcptType}, + Principal, QueryBy, +}; use super::{EmailType, MemoryDirectory}; @@ -48,25 +51,19 @@ impl MemoryDirectory { Ok(None) } - pub async fn email_to_ids(&self, address: &str) -> trc::Result> { - Ok(self - .emails_to_ids - .get(address) - .map(|names| { - names - .iter() - .map(|t| match t { - EmailType::Primary(uid) | EmailType::Alias(uid) | EmailType::List(uid) => { - *uid - } - }) - .collect::>() - }) - .unwrap_or_default()) + pub async fn email_to_id(&self, address: &str) -> trc::Result> { + Ok(self.emails_to_ids.get(address).and_then(|names| { + names + .iter() + .map(|t| match t { + EmailType::Primary(uid) | EmailType::Alias(uid) | EmailType::List(uid) => *uid, + }) + .next() + })) } - pub async fn rcpt(&self, address: &str) -> trc::Result { - Ok(self.emails_to_ids.contains_key(address)) + pub async fn rcpt(&self, address: &str) -> trc::Result { + Ok(self.emails_to_ids.contains_key(address).into()) } pub async fn vrfy(&self, address: &str) -> trc::Result> { diff --git a/crates/directory/src/backend/mod.rs b/crates/directory/src/backend/mod.rs index d94eb44c..35fc3c2a 100644 --- a/crates/directory/src/backend/mod.rs +++ b/crates/directory/src/backend/mod.rs @@ -12,3 +12,21 @@ pub mod memory; pub mod oidc; pub mod smtp; pub mod sql; + +#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)] +pub enum RcptType { + Mailbox, + List(Vec), + #[default] + Invalid, +} + +impl From for RcptType { + fn from(value: bool) -> Self { + if value { + RcptType::Mailbox + } else { + RcptType::Invalid + } + } +} diff --git a/crates/directory/src/backend/oidc/lookup.rs b/crates/directory/src/backend/oidc/lookup.rs index de3119cd..51ceb140 100644 --- a/crates/directory/src/backend/oidc/lookup.rs +++ b/crates/directory/src/backend/oidc/lookup.rs @@ -21,6 +21,7 @@ use crate::{ PrincipalField, }, oidc::{Authentication, EndpointType}, + RcptType, }, Principal, QueryBy, Type, ROLE_USER, }; @@ -140,11 +141,11 @@ impl OpenIdDirectory { } } - pub async fn email_to_ids(&self, address: &str) -> trc::Result> { - self.data_store.email_to_ids(address).await + pub async fn email_to_id(&self, address: &str) -> trc::Result> { + self.data_store.email_to_id(address).await } - pub async fn rcpt(&self, address: &str) -> trc::Result { + pub async fn rcpt(&self, address: &str) -> trc::Result { self.data_store.rcpt(address).await } diff --git a/crates/directory/src/backend/smtp/lookup.rs b/crates/directory/src/backend/smtp/lookup.rs index 62285426..8bdef979 100644 --- a/crates/directory/src/backend/smtp/lookup.rs +++ b/crates/directory/src/backend/smtp/lookup.rs @@ -7,7 +7,7 @@ use mail_send::{smtp::AssertReply, Credentials}; use smtp_proto::Severity; -use crate::{IntoError, Principal, QueryBy}; +use crate::{backend::RcptType, IntoError, Principal, QueryBy}; use super::{SmtpClient, SmtpDirectory}; @@ -25,11 +25,11 @@ impl SmtpDirectory { } } - pub async fn email_to_ids(&self, _address: &str) -> trc::Result> { + pub async fn email_to_id(&self, _address: &str) -> trc::Result> { Err(trc::StoreEvent::NotSupported.caused_by(trc::location!())) } - pub async fn rcpt(&self, address: &str) -> trc::Result { + pub async fn rcpt(&self, address: &str) -> trc::Result { let mut conn = self .pool .get() @@ -57,9 +57,9 @@ impl SmtpDirectory { conn.num_rcpts = 0; conn.sent_mail_from = false; } - Ok(true) + Ok(RcptType::Mailbox) } - Severity::PermanentNegativeCompletion => Ok(false), + Severity::PermanentNegativeCompletion => Ok(RcptType::Invalid), _ => Err(trc::StoreEvent::UnexpectedError .ctx(trc::Key::Code, reply.code()) .ctx(trc::Key::Details, reply.message)), diff --git a/crates/directory/src/backend/sql/config.rs b/crates/directory/src/backend/sql/config.rs index dc69cdbf..c224ab99 100644 --- a/crates/directory/src/backend/sql/config.rs +++ b/crates/directory/src/backend/sql/config.rs @@ -32,9 +32,13 @@ impl SqlDirectory { .unwrap_or_default() .to_string(), column_secret: config - .values((&prefix, "columns.secret")) - .map(|(_, v)| v.to_string()) - .collect(), + .value((&prefix, "columns.secret")) + .unwrap_or_default() + .to_string(), + column_email: config + .value((&prefix, "columns.email")) + .unwrap_or_default() + .to_string(), column_quota: config .value((&prefix, "columns.quota")) .unwrap_or_default() @@ -49,11 +53,9 @@ impl SqlDirectory { for (query_id, query) in [ ("name", &mut mappings.query_name), ("members", &mut mappings.query_members), - ("recipients", &mut mappings.query_recipients), ("emails", &mut mappings.query_emails), - ("verify", &mut mappings.query_verify), - ("expand", &mut mappings.query_expand), - ("domains", &mut mappings.query_domains), + ("recipients", &mut mappings.query_recipients), + ("secrets", &mut mappings.query_secrets), ] { *query = config .value(("store", store_id.as_str(), "query", query_id)) diff --git a/crates/directory/src/backend/sql/lookup.rs b/crates/directory/src/backend/sql/lookup.rs index 71aaa054..80888eca 100644 --- a/crates/directory/src/backend/sql/lookup.rs +++ b/crates/directory/src/backend/sql/lookup.rs @@ -9,10 +9,13 @@ use store::{NamedRows, Rows, Value}; use trc::AddContext; use crate::{ - backend::internal::{ - lookup::DirectoryStore, - manage::{self, ManageDirectory, UpdatePrincipal}, - PrincipalField, PrincipalValue, + backend::{ + internal::{ + lookup::DirectoryStore, + manage::{self, ManageDirectory, UpdatePrincipal}, + PrincipalField, PrincipalValue, + }, + RcptType, }, Principal, QueryBy, Type, ROLE_ADMIN, ROLE_USER, }; @@ -143,6 +146,23 @@ impl SqlDirectory { ); } + // Obtain secrets + if !self.mappings.query_secrets.is_empty() { + external_principal.set( + PrincipalField::Secrets, + PrincipalValue::StringList( + self.store + .query::( + &self.mappings.query_secrets, + vec![external_principal.name().into()], + ) + .await + .caused_by(trc::location!())? + .into(), + ), + ); + } + // Obtain account ID if not available let mut principal = if let Some(stored_principal) = stored_principal { stored_principal @@ -176,66 +196,59 @@ impl SqlDirectory { Ok(Some(principal)) } - pub async fn email_to_ids(&self, address: &str) -> trc::Result> { + pub async fn email_to_id(&self, address: &str) -> trc::Result> { let names = self .store .query::(&self.mappings.query_recipients, vec![address.into()]) .await .caused_by(trc::location!())?; - 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( - self.data_store - .get_or_create_principal_id(name, Type::Individual) - .await - .caused_by(trc::location!())?, - ); + return self + .data_store + .get_or_create_principal_id(name, Type::Individual) + .await + .caused_by(trc::location!()) + .map(Some); } } - Ok(ids) + Ok(None) } - pub async fn rcpt(&self, address: &str) -> trc::Result { - self.store + pub async fn rcpt(&self, address: &str) -> trc::Result { + let result = self + .store .query::( &self.mappings.query_recipients, vec![address.to_string().into()], ) - .await - .map_err(Into::into) + .await?; + + if result { + Ok(RcptType::Mailbox) + } else { + self.data_store.rcpt(address).await.map(|result| { + if matches!(result, RcptType::List(_)) { + result + } else { + RcptType::Invalid + } + }) + } } pub async fn vrfy(&self, address: &str) -> trc::Result> { - self.store - .query::( - &self.mappings.query_verify, - vec![address.to_string().into()], - ) - .await - .map(Into::into) - .map_err(Into::into) + self.data_store.vrfy(address).await } pub async fn expn(&self, address: &str) -> trc::Result> { - self.store - .query::( - &self.mappings.query_expand, - vec![address.to_string().into()], - ) - .await - .map(Into::into) - .map_err(Into::into) + self.data_store.expn(address).await } pub async fn is_local_domain(&self, domain: &str) -> trc::Result { - self.store - .query::(&self.mappings.query_domains, vec![domain.into()]) - .await - .map_err(Into::into) + self.data_store.is_local_domain(domain).await } } @@ -250,13 +263,9 @@ impl SqlMappings { if let Some(row) = rows.rows.into_iter().next() { for (name, value) in rows.names.into_iter().zip(row.values) { - if self - .column_secret - .iter() - .any(|c| name.eq_ignore_ascii_case(c)) - { - if let Value::Text(secret) = value { - principal.append_str(PrincipalField::Secrets, secret.into_owned()); + if name.eq_ignore_ascii_case(&self.column_secret) { + if let Value::Text(text) = value { + principal.set(PrincipalField::Secrets, text.into_owned()); } } else if name.eq_ignore_ascii_case(&self.column_type) { match value.to_str().as_ref() { @@ -274,6 +283,10 @@ impl SqlMappings { if let Value::Text(text) = value { principal.set(PrincipalField::Description, text.into_owned()); } + } else if name.eq_ignore_ascii_case(&self.column_email) { + if let Value::Text(text) = value { + principal.set(PrincipalField::Emails, text.into_owned()); + } } else if name.eq_ignore_ascii_case(&self.column_quota) { if let Value::Integer(quota) = value { principal.set(PrincipalField::Quota, quota as u64); diff --git a/crates/directory/src/backend/sql/mod.rs b/crates/directory/src/backend/sql/mod.rs index a4f08678..f518a211 100644 --- a/crates/directory/src/backend/sql/mod.rs +++ b/crates/directory/src/backend/sql/mod.rs @@ -19,13 +19,12 @@ pub struct SqlDirectory { pub(crate) struct SqlMappings { query_name: String, query_members: String, - query_recipients: String, query_emails: String, - query_domains: String, - query_verify: String, - query_expand: String, + query_recipients: String, + query_secrets: String, column_description: String, - column_secret: Vec, + column_secret: String, + column_email: String, column_quota: String, column_type: String, } diff --git a/crates/directory/src/core/cache.rs b/crates/directory/src/core/cache.rs index 2e4219cb..229229e0 100644 --- a/crates/directory/src/core/cache.rs +++ b/crates/directory/src/core/cache.rs @@ -13,6 +13,8 @@ use std::{ use parking_lot::Mutex; use utils::config::{utils::AsKey, Config}; +use crate::backend::RcptType; + pub struct CachedDirectory { cached_domains: Mutex>, cached_rcpts: Mutex>, @@ -52,15 +54,15 @@ impl CachedDirectory { }) } - pub fn get_rcpt(&self, address: &str) -> Option { - self.cached_rcpts.lock().get(address) + pub fn get_rcpt(&self, address: &str) -> Option { + self.cached_rcpts.lock().get(address).map(Into::into) } - pub fn set_rcpt(&self, address: &str, exists: bool) { - if exists { - self.cached_rcpts.lock().insert_pos(address.to_string()); - } else { - self.cached_rcpts.lock().insert_neg(address.to_string()); + pub fn set_rcpt(&self, address: &str, exists: &RcptType) { + match exists { + RcptType::Mailbox => self.cached_rcpts.lock().insert_pos(address.to_string()), + RcptType::Invalid => self.cached_rcpts.lock().insert_neg(address.to_string()), + RcptType::List(_) => {} } } diff --git a/crates/directory/src/core/dispatch.rs b/crates/directory/src/core/dispatch.rs index 02a63899..94c519a5 100644 --- a/crates/directory/src/core/dispatch.rs +++ b/crates/directory/src/core/dispatch.rs @@ -7,7 +7,8 @@ use trc::AddContext; use crate::{ - backend::internal::lookup::DirectoryStore, Directory, DirectoryInner, Principal, QueryBy, + backend::{internal::lookup::DirectoryStore, RcptType}, + Directory, DirectoryInner, Principal, QueryBy, }; impl Directory { @@ -29,16 +30,16 @@ impl Directory { .caused_by(trc::location!()) } - pub async fn email_to_ids(&self, email: &str) -> trc::Result> { + pub async fn email_to_id(&self, address: &str) -> trc::Result> { match &self.store { - DirectoryInner::Internal(store) => store.email_to_ids(email).await, - DirectoryInner::Ldap(store) => store.email_to_ids(email).await, - DirectoryInner::Sql(store) => store.email_to_ids(email).await, - DirectoryInner::Imap(store) => store.email_to_ids(email).await, - DirectoryInner::Smtp(store) => store.email_to_ids(email).await, - DirectoryInner::Memory(store) => store.email_to_ids(email).await, + DirectoryInner::Internal(store) => store.email_to_id(address).await, + DirectoryInner::Ldap(store) => store.email_to_id(address).await, + DirectoryInner::Sql(store) => store.email_to_id(address).await, + DirectoryInner::Imap(store) => store.email_to_id(address).await, + DirectoryInner::Smtp(store) => store.email_to_id(address).await, + DirectoryInner::Memory(store) => store.email_to_id(address).await, #[cfg(feature = "enterprise")] - DirectoryInner::OpenId(store) => store.email_to_ids(email).await, + DirectoryInner::OpenId(store) => store.email_to_id(address).await, } .caused_by(trc::location!()) } @@ -71,7 +72,7 @@ impl Directory { Ok(result) } - pub async fn rcpt(&self, email: &str) -> trc::Result { + pub async fn rcpt(&self, email: &str) -> trc::Result { // Check cache if let Some(cache) = &self.cache { if let Some(result) = cache.get_rcpt(email) { @@ -93,7 +94,7 @@ impl Directory { // Update cache if let Some(cache) = &self.cache { - cache.set_rcpt(email, result); + cache.set_rcpt(email, &result); } Ok(result) diff --git a/crates/directory/src/core/principal.rs b/crates/directory/src/core/principal.rs index ecd8350b..29665477 100644 --- a/crates/directory/src/core/principal.rs +++ b/crates/directory/src/core/principal.rs @@ -844,16 +844,19 @@ impl<'de> serde::Deserialize<'de> for Principal { | PrincipalField::Lists | PrincipalField::EnabledPermissions | PrincipalField::DisabledPermissions - | PrincipalField::Urls => match map.next_value::()? { - StringOrMany::One(v) => PrincipalValue::StringList(vec![v]), - StringOrMany::Many(v) => { - if !v.is_empty() { - PrincipalValue::StringList(v) - } else { - continue; + | PrincipalField::Urls + | PrincipalField::ExternalMembers => { + match map.next_value::()? { + StringOrMany::One(v) => PrincipalValue::StringList(vec![v]), + StringOrMany::Many(v) => { + if !v.is_empty() { + PrincipalValue::StringList(v) + } else { + continue; + } } } - }, + } PrincipalField::UsedQuota => { // consume and ignore map.next_value::()?; diff --git a/crates/imap-proto/Cargo.toml b/crates/imap-proto/Cargo.toml index 97f1069d..2ba5b823 100644 --- a/crates/imap-proto/Cargo.toml +++ b/crates/imap-proto/Cargo.toml @@ -6,7 +6,7 @@ resolver = "2" [dependencies] jmap_proto = { path = "../jmap-proto" } -store = { path = "../store", features = ["sqlite"] } +store = { path = "../store" } mail-parser = { version = "0.9", features = ["full_encoding", "serde_support", "ludicrous_mode"] } ahash = { version = "0.8" } chrono = { version = "0.4"} diff --git a/crates/jmap/src/api/autoconfig.rs b/crates/jmap/src/api/autoconfig.rs index 27d61ac4..dcda1736 100644 --- a/crates/jmap/src/api/autoconfig.rs +++ b/crates/jmap/src/api/autoconfig.rs @@ -10,6 +10,7 @@ use common::{manager::webadmin::Resource, Server}; use directory::{backend::internal::PrincipalField, QueryBy}; use quick_xml::events::Event; use quick_xml::Reader; +use trc::AddContext; use utils::url_params::UrlParams; use crate::api::http::ToHttpResponse; @@ -193,13 +194,13 @@ impl Autoconfig for Server { // Find the account name by e-mail address let mut account_name = emailaddress.to_string(); - for id in self + if let Some(id) = self .core .storage .directory - .email_to_ids(emailaddress) + .email_to_id(emailaddress) .await - .unwrap_or_default() + .caused_by(trc::location!())? { if let Ok(Some(mut principal)) = self .core @@ -209,7 +210,6 @@ impl Autoconfig for Server { .await { account_name = principal.take_str(PrincipalField::Name).unwrap_or_default(); - break; } } diff --git a/crates/jmap/src/api/management/principal.rs b/crates/jmap/src/api/management/principal.rs index 4e2bbbf6..3513bd52 100644 --- a/crates/jmap/src/api/management/principal.rs +++ b/crates/jmap/src/api/management/principal.rs @@ -422,7 +422,8 @@ impl PrincipalManager for Server { | PrincipalField::MemberOf | PrincipalField::Members | PrincipalField::Lists - | PrincipalField::Urls => (), + | PrincipalField::Urls + | PrincipalField::ExternalMembers => (), PrincipalField::Tenant => { // Tenants are not allowed to change their tenantId if access_token.tenant.is_some() { diff --git a/crates/jmap/src/identity/get.rs b/crates/jmap/src/identity/get.rs index 6b934472..1f255d7e 100644 --- a/crates/jmap/src/identity/get.rs +++ b/crates/jmap/src/identity/get.rs @@ -16,10 +16,10 @@ use store::{ write::{BatchBuilder, F_VALUE}, }; use trc::AddContext; +use utils::sanitize_email; use crate::{changes::state::StateManager, JmapMethods}; -use super::set::sanitize_email; use std::future::Future; pub trait IdentityGet: Sync + Send { diff --git a/crates/jmap/src/identity/set.rs b/crates/jmap/src/identity/set.rs index 9e82dcb8..cfc65b24 100644 --- a/crates/jmap/src/identity/set.rs +++ b/crates/jmap/src/identity/set.rs @@ -19,6 +19,7 @@ use jmap_proto::{ }; use std::future::Future; use store::write::{log::ChangeLogBuilder, BatchBuilder, F_CLEAR, F_VALUE}; +use utils::sanitize_email; use crate::{changes::write::ChangeLog, JmapMethods}; @@ -256,39 +257,3 @@ fn validate_identity_value( } }) } - -// Basic email sanitizer -pub fn sanitize_email(email: &str) -> Option { - let mut result = String::with_capacity(email.len()); - let mut found_local = false; - let mut found_domain = false; - let mut last_ch = char::from(0); - - for ch in email.chars() { - if !ch.is_whitespace() { - if ch == '@' { - if !result.is_empty() && !found_local { - found_local = true; - } else { - return None; - } - } else if ch == '.' { - if !(last_ch.is_alphanumeric() || last_ch == '-' || last_ch == '_') { - return None; - } else if found_local { - found_domain = true; - } - } - last_ch = ch; - for ch in ch.to_lowercase() { - result.push(ch); - } - } - } - - if found_domain && last_ch != '.' { - Some(result) - } else { - None - } -} diff --git a/crates/jmap/src/principal/query.rs b/crates/jmap/src/principal/query.rs index fa9793ee..c826929c 100644 --- a/crates/jmap/src/principal/query.rs +++ b/crates/jmap/src/principal/query.rs @@ -60,8 +60,8 @@ impl PrincipalQuery for Server { } Filter::Email(email) => { let mut ids = RoaringBitmap::new(); - for id in self - .email_to_ids(&self.core.storage.directory, &email, session.session_id) + if let Some(id) = self + .email_to_id(&self.core.storage.directory, &email, session.session_id) .await? { ids.insert(id); diff --git a/crates/jmap/src/services/ingest.rs b/crates/jmap/src/services/ingest.rs index aadeb15b..71c91e3a 100644 --- a/crates/jmap/src/services/ingest.rs +++ b/crates/jmap/src/services/ingest.rs @@ -69,47 +69,54 @@ impl MailDelivery for Server { }; // Obtain the UIDs for each recipient - let mut recipients = Vec::with_capacity(message.recipients.len()); - let mut deliver_names = AHashMap::with_capacity(message.recipients.len()); - for rcpt in &message.recipients { - match self - .email_to_ids(&self.core.storage.directory, rcpt, message.session_id) + let mut uids: AHashMap = AHashMap::with_capacity(message.recipients.len()); + let mut results = Vec::with_capacity(message.recipients.len()); + for rcpt in message.recipients { + let uid = match self + .email_to_id(&self.core.storage.directory, &rcpt, message.session_id) .await { - Ok(uids) => { - for uid in &uids { - deliver_names.insert(*uid, (DeliveryResult::Success, rcpt)); - } - recipients.push(uids); + Ok(Some(uid)) => uid, + Ok(None) => { + // Something went wrong + results.push(DeliveryResult::PermanentFailure { + code: [5, 5, 0], + reason: "Mailbox not found.".into(), + }); + continue; } Err(err) => { trc::error!(err .details("Failed to lookup recipient.") - .ctx(trc::Key::To, rcpt.to_string()) + .ctx(trc::Key::To, rcpt) .span_id(message.session_id) .caused_by(trc::location!())); - recipients.push(vec![]); + results.push(DeliveryResult::TemporaryFailure { + reason: "Address lookup failed.".into(), + }); + continue; } + }; + if let Some(result) = uids.get(&uid).and_then(|pos| results.get(*pos)) { + results.push(result.clone()); + continue; } - } - // Deliver to each recipient - for (uid, (status, rcpt)) in &mut deliver_names { // Obtain access token - let result = match self.get_cached_access_token(*uid).await.and_then(|token| { + let result = match self.get_cached_access_token(uid).await.and_then(|token| { token .assert_has_permission(Permission::EmailReceive) .map(|_| token) }) { Ok(access_token) => { // Check if there is an active sieve script - match self.sieve_script_get_active(*uid).await { + match self.sieve_script_get_active(uid).await { Ok(Some(active_script)) => { self.sieve_script_ingest( &access_token, &raw_message, &message.sender_address, - rcpt, + &rcpt, message.session_id, active_script, ) @@ -137,12 +144,12 @@ impl MailDelivery for Server { Err(err) => Err(err), }; - match result { + let result = match result { Ok(ingested_message) => { // 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) @@ -150,27 +157,29 @@ impl MailDelivery for Server { ) .await; } + + DeliveryResult::Success } Err(err) => { - match err.as_ref() { + let result = match err.as_ref() { trc::EventType::Limit(trc::LimitEvent::Quota) => { - *status = DeliveryResult::TemporaryFailure { + DeliveryResult::TemporaryFailure { reason: "Mailbox over quota.".into(), } } trc::EventType::Limit(trc::LimitEvent::TenantQuota) => { - *status = DeliveryResult::TemporaryFailure { + DeliveryResult::TemporaryFailure { reason: "Organization over quota.".into(), } } trc::EventType::Security(trc::SecurityEvent::Unauthorized) => { - *status = DeliveryResult::PermanentFailure { + DeliveryResult::PermanentFailure { code: [5, 5, 0], reason: "This account is not authorized to receive email.".into(), } } trc::EventType::MessageIngest(trc::MessageIngestEvent::Error) => { - *status = DeliveryResult::PermanentFailure { + DeliveryResult::PermanentFailure { code: err .value(trc::Key::Code) .and_then(|v| v.to_uint()) @@ -185,62 +194,25 @@ impl MailDelivery for Server { .into(), } } - _ => { - *status = DeliveryResult::TemporaryFailure { - reason: "Transient server failure.".into(), - } - } - } + _ => DeliveryResult::TemporaryFailure { + reason: "Transient server failure.".into(), + }, + }; trc::error!(err .ctx(trc::Key::To, rcpt.to_string()) .span_id(message.session_id)); + + result } - } + }; + + // Cache response for UID to avoid duplicate deliveries + uids.insert(uid, results.len()); + + results.push(result); } - // Build result - recipients - .into_iter() - .map(|names| { - match names.len() { - 1 => { - // Delivery to single recipient - deliver_names.get(&names[0]).unwrap().0.clone() - } - 0 => { - // Something went wrong - DeliveryResult::TemporaryFailure { - reason: "Address lookup failed.".into(), - } - } - _ => { - // Delivery to list, count number of successes and failures - let mut success = 0; - let mut temp_failures = 0; - for uid in names { - match deliver_names.get(&uid).unwrap().0 { - DeliveryResult::Success => success += 1, - DeliveryResult::TemporaryFailure { .. } => temp_failures += 1, - DeliveryResult::PermanentFailure { .. } => {} - } - } - if success > temp_failures { - DeliveryResult::Success - } else if temp_failures > 0 { - DeliveryResult::TemporaryFailure { - reason: "Delivery to one or more recipients failed temporarily." - .into(), - } - } else { - DeliveryResult::PermanentFailure { - code: [5, 5, 0], - reason: "Delivery to all recipients failed.".into(), - } - } - } - } - }) - .collect() + results } } diff --git a/crates/jmap/src/submission/set.rs b/crates/jmap/src/submission/set.rs index c92ae0da..5c3ff8ec 100644 --- a/crates/jmap/src/submission/set.rs +++ b/crates/jmap/src/submission/set.rs @@ -38,11 +38,11 @@ use smtp::{ }; use smtp_proto::{request::parser::Rfc5321Parser, MailFrom, RcptTo}; use store::write::{assert::HashedValue, log::ChangeLogBuilder, now, BatchBuilder, Bincode}; -use utils::map::vec_map::VecMap; +use utils::{map::vec_map::VecMap, sanitize_email}; use crate::{ blob::download::BlobDownload, changes::write::ChangeLog, email::metadata::MessageMetadata, - identity::set::sanitize_email, JmapMethods, + JmapMethods, }; use std::future::Future; diff --git a/crates/smtp/src/core/mod.rs b/crates/smtp/src/core/mod.rs index 16483b4b..22d5c2b2 100644 --- a/crates/smtp/src/core/mod.rs +++ b/crates/smtp/src/core/mod.rs @@ -82,6 +82,7 @@ pub struct SessionData { pub mail_from: Option, pub rcpt_to: Vec, pub rcpt_errors: usize, + pub rcpt_oks: usize, pub message: Vec, pub authenticated_as: Option>, @@ -101,7 +102,7 @@ pub struct SessionData { pub dnsbl_error: Option>, } -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct SessionAddress { pub address: String, pub address_lcase: String, @@ -164,6 +165,7 @@ impl SessionData { priority: 0, valid_until: Instant::now(), rcpt_errors: 0, + rcpt_oks: 0, message: Vec::with_capacity(0), auth_errors: 0, messages_sent: 0, @@ -310,6 +312,7 @@ impl SessionData { mail_from, rcpt_to, rcpt_errors: 0, + rcpt_oks: 0, message, authenticated_as: Some(Arc::new(AccessToken::from_id(0))), auth_errors: 0, diff --git a/crates/smtp/src/inbound/rcpt.rs b/crates/smtp/src/inbound/rcpt.rs index 03c831ce..44213655 100644 --- a/crates/smtp/src/inbound/rcpt.rs +++ b/crates/smtp/src/inbound/rcpt.rs @@ -5,6 +5,7 @@ */ use common::{config::smtp::session::Stage, listener::SessionStream, scripts::ScriptModification}; +use directory::backend::RcptType; use smtp_proto::{ RcptTo, RCPT_NOTIFY_DELAY, RCPT_NOTIFY_FAILURE, RCPT_NOTIFY_NEVER, RCPT_NOTIFY_SUCCESS, }; @@ -71,6 +72,7 @@ impl Session { SpanId = self.data.session_id, To = rcpt.address_lcase, ); + self.data.rcpt_oks += 1; return self.write(b"250 2.1.5 OK\r\n").await; } self.data.rcpt_to.push(rcpt); @@ -177,12 +179,14 @@ impl Session { To = rcpt.address_lcase.clone(), ); self.data.rcpt_to.pop(); + self.data.rcpt_oks += 1; return self.write(b"250 2.1.5 OK\r\n").await; } } // Verify address let rcpt = self.data.rcpt_to.last().unwrap(); + let mut rcpt_members = None; if let Some(directory) = self .server .eval_if::( @@ -194,43 +198,43 @@ impl Session { .and_then(|name| self.server.get_directory(&name)) { match directory.is_local_domain(&rcpt.domain).await { - Ok(is_local_domain) => { - if is_local_domain { - match self - .server - .rcpt(directory, &rcpt.address_lcase, self.data.session_id) - .await - { - Ok(is_local_address) => { - if !is_local_address { - trc::event!( - Smtp(SmtpEvent::MailboxDoesNotExist), - SpanId = self.data.session_id, - To = rcpt.address_lcase.clone(), - ); - - let rcpt_to = self.data.rcpt_to.pop().unwrap().address_lcase; - return self - .rcpt_error( - b"550 5.1.2 Mailbox does not exist.\r\n", - rcpt_to, - ) - .await; - } - } - Err(err) => { - trc::error!(err - .span_id(self.data.session_id) - .caused_by(trc::location!()) - .details("Failed to verify address.")); - - self.data.rcpt_to.pop(); - return self - .write(b"451 4.4.3 Unable to verify address at this time.\r\n") - .await; - } + Ok(true) => { + match self + .server + .rcpt(directory, &rcpt.address_lcase, self.data.session_id) + .await + { + Ok(RcptType::Mailbox) => {} + Ok(RcptType::List(members)) => { + rcpt_members = Some(members); } - } else if !self + Ok(RcptType::Invalid) => { + trc::event!( + Smtp(SmtpEvent::MailboxDoesNotExist), + SpanId = self.data.session_id, + To = rcpt.address_lcase.clone(), + ); + + let rcpt_to = self.data.rcpt_to.pop().unwrap().address_lcase; + return self + .rcpt_error(b"550 5.1.2 Mailbox does not exist.\r\n", rcpt_to) + .await; + } + Err(err) => { + trc::error!(err + .span_id(self.data.session_id) + .caused_by(trc::location!()) + .details("Failed to verify address.")); + + self.data.rcpt_to.pop(); + return self + .write(b"451 4.4.3 Unable to verify address at this time.\r\n") + .await; + } + } + } + Ok(false) => { + if !self .server .eval_if( &self.server.core.smtp.session.rcpt.relay, @@ -305,6 +309,23 @@ impl Session { .await; } + // Expand list + if let Some(members) = rcpt_members { + let list_addr = self.data.rcpt_to.pop().unwrap(); + let orcpt = format!("rfc822;{}", list_addr.address_lcase); + for member in members { + let mut member_addr = SessionAddress::new(member); + if !self.data.rcpt_to.contains(&member_addr) + && member_addr.address_lcase != list_addr.address_lcase + { + member_addr.dsn_info = orcpt.clone().into(); + member_addr.flags = list_addr.flags; + self.data.rcpt_to.push(member_addr); + } + } + } + + self.data.rcpt_oks += 1; self.write(b"250 2.1.5 OK\r\n").await } diff --git a/crates/smtp/src/inbound/session.rs b/crates/smtp/src/inbound/session.rs index 413ccfec..58653636 100644 --- a/crates/smtp/src/inbound/session.rs +++ b/crates/smtp/src/inbound/session.rs @@ -338,15 +338,15 @@ impl Session { State::Data(receiver) => { if self.data.message.len() + bytes.len() < self.params.max_message_size { if receiver.ingest(&mut iter, &mut self.data.message) { - let num_rcpts = self.data.rcpt_to.len(); let message = self.queue_message().await; + let num_responses = if self.instance.protocol == ServerProtocol::Smtp { + 1 + } else { + self.data.rcpt_oks + }; if !message.is_empty() { - if self.instance.protocol == ServerProtocol::Smtp { + for _ in 0..num_responses { self.write(message.as_ref()).await?; - } else { - for _ in 0..num_rcpts { - self.write(message.as_ref()).await?; - } } self.reset(); state = State::default(); @@ -365,15 +365,16 @@ impl Session { if receiver.ingest(&mut iter, &mut self.data.message) { if self.can_send_data().await? { if receiver.is_last { - let num_rcpts = self.data.rcpt_to.len(); let message = self.queue_message().await; if !message.is_empty() { - if self.instance.protocol == ServerProtocol::Smtp { + let num_responses = + if self.instance.protocol == ServerProtocol::Smtp { + 1 + } else { + self.data.rcpt_oks + }; + for _ in 0..num_responses { self.write(message.as_ref()).await?; - } else { - for _ in 0..num_rcpts { - self.write(message.as_ref()).await?; - } } self.reset(); } else { @@ -464,6 +465,7 @@ impl Session { self.data.priority = 0; self.data.delivery_by = 0; self.data.future_release = 0; + self.data.rcpt_oks = 0; } #[inline(always)] diff --git a/crates/store/src/config.rs b/crates/store/src/config.rs index c32ce314..26620727 100644 --- a/crates/store/src/config.rs +++ b/crates/store/src/config.rs @@ -11,7 +11,7 @@ use utils::config::{cron::SimpleCron, utils::ParseValue, Config}; use crate::{ backend::fs::FsStore, write::purge::{PurgeSchedule, PurgeStore}, - BlobStore, CompressionAlgo, FtsStore, LookupStore, QueryStore, Store, Stores, + BlobStore, CompressionAlgo, LookupStore, QueryStore, Store, Stores, }; #[cfg(feature = "s3")] @@ -201,7 +201,7 @@ impl Stores { "elasticsearch" => { if let Some(db) = ElasticSearchStore::open(config, prefix) .await - .map(FtsStore::from) + .map(crate::FtsStore::from) { self.fts_stores.insert(store_id, db); } @@ -378,6 +378,7 @@ impl Stores { } } +#[allow(dead_code)] trait IsActiveStore { fn is_active_store(&self, id: &str) -> bool; } diff --git a/crates/utils/Cargo.toml b/crates/utils/Cargo.toml index f6596345..03aceaf1 100644 --- a/crates/utils/Cargo.toml +++ b/crates/utils/Cargo.toml @@ -34,6 +34,7 @@ blake3 = "1.3.3" lru-cache = "0.1.2" http-body-util = "0.1.0" form_urlencoded = "1.1.0" +psl = "2" [target.'cfg(unix)'.dependencies] privdrop = "0.5.3" diff --git a/crates/utils/src/lib.rs b/crates/utils/src/lib.rs index 4e92531a..f6da62a3 100644 --- a/crates/utils/src/lib.rs +++ b/crates/utils/src/lib.rs @@ -248,3 +248,39 @@ impl ServerCertVerifier for DummyVerifier { ] } } + +// Basic email sanitizer +pub fn sanitize_email(email: &str) -> Option { + let mut result = String::with_capacity(email.len()); + let mut found_local = false; + let mut found_domain = false; + let mut last_ch = char::from(0); + + for ch in email.chars() { + if !ch.is_whitespace() { + if ch == '@' { + if !result.is_empty() && !found_local { + found_local = true; + } else { + return None; + } + } else if ch == '.' { + if !(last_ch.is_alphanumeric() || last_ch == '-' || last_ch == '_') { + return None; + } else if found_local { + found_domain = true; + } + } + last_ch = ch; + for ch in ch.to_lowercase() { + result.push(ch); + } + } + } + + if found_domain && last_ch != '.' && psl::domain(result.as_bytes()).is_some() { + Some(result) + } else { + None + } +} diff --git a/tests/src/directory/internal.rs b/tests/src/directory/internal.rs index b57dc7b9..78599867 100644 --- a/tests/src/directory/internal.rs +++ b/tests/src/directory/internal.rs @@ -6,10 +6,13 @@ use ahash::AHashSet; use directory::{ - backend::internal::{ - lookup::DirectoryStore, - manage::{self, ManageDirectory, UpdatePrincipal}, - PrincipalField, PrincipalUpdate, PrincipalValue, + backend::{ + internal::{ + lookup::DirectoryStore, + manage::{self, ManageDirectory, UpdatePrincipal}, + PrincipalField, PrincipalUpdate, PrincipalValue, + }, + RcptType, }, Principal, QueryBy, Type, }; @@ -117,10 +120,13 @@ async fn internal_directory() { .await, Ok(()) ); - assert!(store.rcpt("john@example.org").await.unwrap()); assert_eq!( - store.email_to_ids("john@example.org").await.unwrap(), - vec![john_id] + store.rcpt("john@example.org").await.unwrap(), + RcptType::Mailbox + ); + assert_eq!( + store.email_to_id("john@example.org").await.unwrap(), + Some(john_id) ); // Using non-existent domain should fail @@ -154,11 +160,17 @@ async fn internal_directory() { .await .unwrap(); - assert!(store.rcpt("jane@example.org").await.unwrap()); - assert!(!store.rcpt("jane@otherdomain.org").await.unwrap()); assert_eq!( - store.email_to_ids("jane@example.org").await.unwrap(), - vec![jane_id] + store.rcpt("jane@example.org").await.unwrap(), + RcptType::Mailbox + ); + assert_eq!( + store.rcpt("jane@otherdomain.org").await.unwrap(), + RcptType::Invalid + ); + assert_eq!( + store.email_to_id("jane@example.org").await.unwrap(), + Some(jane_id) ); assert_eq!(store.vrfy("jane").await.unwrap(), vec!["jane@example.org"]); assert_eq!( @@ -239,21 +251,31 @@ async fn internal_directory() { PrincipalUpdate::set( PrincipalField::Members, PrincipalValue::StringList(vec!["john".to_string(), "jane".to_string()]), + ), + PrincipalUpdate::set( + PrincipalField::ExternalMembers, + PrincipalValue::StringList(vec![ + "mike@other.org".to_string(), + "lucy@foobar.net".to_string() + ]), ) ])) .await, Ok(()) ); - assert!(store.rcpt("list@example.org").await.unwrap()); - assert_eq!( - store - .email_to_ids("list@example.org") - .await - .unwrap() - .into_iter() - .collect::>(), - [john_id, jane_id].into_iter().collect::>(), - ); + + assert_list_members( + &store, + "list@example.org", + [ + "john@example.org", + "mike@other.org", + "lucy@foobar.net", + "jane@example.org", + ], + ) + .await; + assert_eq!( store .query(QueryBy::Name("list"), true) @@ -276,10 +298,15 @@ async fn internal_directory() { .unwrap() .into_iter() .collect::>(), - ["john@example.org", "jane@example.org"] - .into_iter() - .map(|s| s.to_string()) - .collect::>() + [ + "john@example.org", + "mike@other.org", + "lucy@foobar.net", + "jane@example.org" + ] + .into_iter() + .map(|s| s.to_string()) + .collect::>() ); // Create groups @@ -445,8 +472,14 @@ async fn internal_directory() { } ); assert_eq!(store.get_principal_id("john").await.unwrap(), None); - assert!(!store.rcpt("john@example.org").await.unwrap()); - assert!(store.rcpt("john.doe@example.org").await.unwrap()); + assert_eq!( + store.rcpt("john@example.org").await.unwrap(), + RcptType::Invalid + ); + assert_eq!( + store.rcpt("john.doe@example.org").await.unwrap(), + RcptType::Mailbox + ); // Remove a member from a mailing list and then add it back assert_eq!( @@ -460,10 +493,12 @@ async fn internal_directory() { .await, Ok(()) ); - assert_eq!( - store.email_to_ids("list@example.org").await.unwrap(), - vec![jane_id] - ); + assert_list_members( + &store, + "list@example.org", + ["jane@example.org", "mike@other.org", "lucy@foobar.net"], + ) + .await; assert_eq!( store .update_principal(UpdatePrincipal::by_name("list").with_updates(vec![ @@ -475,15 +510,17 @@ async fn internal_directory() { .await, Ok(()) ); - assert_eq!( - store - .email_to_ids("list@example.org") - .await - .unwrap() - .into_iter() - .collect::>(), - [john_id, jane_id].into_iter().collect::>() - ); + assert_list_members( + &store, + "list@example.org", + [ + "john.doe@example.org", + "jane@example.org", + "mike@other.org", + "lucy@foobar.net", + ], + ) + .await; // Field validation assert_eq!( @@ -619,10 +656,13 @@ async fn internal_directory() { store.delete_principal(QueryBy::Id(john_id)).await.unwrap(); assert_eq!(store.get_principal_id("john.doe").await.unwrap(), None); assert_eq!( - store.email_to_ids("john.doe@example.org").await.unwrap(), - Vec::::new() + store.email_to_id("john.doe@example.org").await.unwrap(), + None + ); + assert_eq!( + store.rcpt("john.doe@example.org").await.unwrap(), + RcptType::Invalid ); - assert!(!store.rcpt("john.doe@example.org").await.unwrap()); assert_eq!( store .list_principals( @@ -672,10 +712,13 @@ async fn internal_directory() { // Make sure Jane's records are still there assert_eq!(store.get_principal_id("jane").await.unwrap(), Some(jane_id)); assert_eq!( - store.email_to_ids("jane@example.org").await.unwrap(), - vec![jane_id] + store.email_to_id("jane@example.org").await.unwrap(), + Some(jane_id) + ); + assert_eq!( + store.rcpt("jane@example.org").await.unwrap(), + RcptType::Mailbox ); - assert!(store.rcpt("jane@example.org").await.unwrap()); assert_eq!( store .get_bitmap(BitmapKey { @@ -885,3 +928,22 @@ impl TestInternalDirectory for Store { } } } + +async fn assert_list_members( + store: &Store, + list_addr: &str, + members: impl IntoIterator, +) { + match store.rcpt(list_addr).await.unwrap() { + RcptType::List(items) => { + assert_eq!( + items.into_iter().collect::>(), + members + .into_iter() + .map(|s| s.to_string()) + .collect::>() + ); + } + other => panic!("invalid {other:?}"), + } +} diff --git a/tests/src/directory/ldap.rs b/tests/src/directory/ldap.rs index 4948977d..ea2d5e39 100644 --- a/tests/src/directory/ldap.rs +++ b/tests/src/directory/ldap.rs @@ -6,10 +6,15 @@ use std::fmt::Debug; -use directory::{backend::internal::manage::ManageDirectory, QueryBy, Type, ROLE_USER}; +use directory::{ + backend::{internal::manage::ManageDirectory, RcptType}, + QueryBy, Type, ROLE_USER, +}; use mail_send::Credentials; -use crate::directory::{map_account_ids, DirectoryTest, IntoTestPrincipal, TestPrincipal}; +use crate::directory::{ + map_account_id, map_account_ids, DirectoryTest, IntoTestPrincipal, TestPrincipal, +}; #[tokio::test] async fn ldap_directory() { @@ -149,41 +154,29 @@ async fn ldap_directory() { ); // Ids by email - compare_sorted( - core.email_to_ids(&handle, "jane@example.org", 0) + assert_eq!( + core.email_to_id(&handle, "jane@example.org", 0) .await .unwrap(), - map_account_ids(base_store, vec!["jane"]).await, - ); - compare_sorted( - core.email_to_ids(&handle, "jane+alias@example.org", 0) - .await - .unwrap(), - map_account_ids(base_store, vec!["jane"]).await, - ); - compare_sorted( - core.email_to_ids(&handle, "info@example.org", 0) - .await - .unwrap(), - map_account_ids(base_store, vec!["bill", "jane", "john"]).await, - ); - compare_sorted( - core.email_to_ids(&handle, "info+alias@example.org", 0) - .await - .unwrap(), - map_account_ids(base_store, vec!["bill", "jane", "john"]).await, - ); - compare_sorted( - core.email_to_ids(&handle, "unknown@example.org", 0) - .await - .unwrap(), - Vec::::new(), + Some(map_account_id(base_store, "jane").await), ); assert_eq!( - core.email_to_ids(&handle, "anything@catchall.org", 0) + core.email_to_id(&handle, "jane+alias@example.org", 0) .await .unwrap(), - map_account_ids(base_store, vec!["robert"]).await + Some(map_account_id(base_store, "jane").await), + ); + assert_eq!( + core.email_to_id(&handle, "unknown@example.org", 0) + .await + .unwrap(), + None, + ); + assert_eq!( + core.email_to_id(&handle, "anything@catchall.org", 0) + .await + .unwrap(), + Some(map_account_id(base_store, "robert").await) ); // Domain validation @@ -191,21 +184,36 @@ async fn ldap_directory() { assert!(!handle.is_local_domain("other.org").await.unwrap()); // RCPT TO - assert!(core.rcpt(&handle, "jane@example.org", 0).await.unwrap()); - assert!(core.rcpt(&handle, "info@example.org", 0).await.unwrap()); - assert!(core - .rcpt(&handle, "jane+alias@example.org", 0) - .await - .unwrap()); - assert!(core - .rcpt(&handle, "info+alias@example.org", 0) - .await - .unwrap()); - assert!(core - .rcpt(&handle, "random_user@catchall.org", 0) - .await - .unwrap()); - assert!(!core.rcpt(&handle, "invalid@example.org", 0).await.unwrap()); + assert_eq!( + core.rcpt(&handle, "jane@example.org", 0).await.unwrap(), + RcptType::Mailbox + ); + assert_eq!( + core.rcpt(&handle, "info@example.org", 0).await.unwrap(), + RcptType::Mailbox + ); + assert_eq!( + core.rcpt(&handle, "jane+alias@example.org", 0) + .await + .unwrap(), + RcptType::Mailbox + ); + assert_eq!( + core.rcpt(&handle, "info+alias@example.org", 0) + .await + .unwrap(), + RcptType::Mailbox + ); + assert_eq!( + core.rcpt(&handle, "random_user@catchall.org", 0) + .await + .unwrap(), + RcptType::Mailbox + ); + assert_eq!( + core.rcpt(&handle, "invalid@example.org", 0).await.unwrap(), + RcptType::Invalid + ); // VRFY compare_sorted( @@ -214,7 +222,10 @@ async fn ldap_directory() { ); compare_sorted( core.vrfy(&handle, "john", 0).await.unwrap(), - vec!["john@example.org".to_string()], + vec![ + "john@example.org".to_string(), + "john.doe@example.org".to_string(), + ], ); compare_sorted( core.vrfy(&handle, "jane+alias@example", 0).await.unwrap(), @@ -230,7 +241,8 @@ async fn ldap_directory() { ); // EXPN - compare_sorted( + // Now handled by the internal directory + /*compare_sorted( core.expn(&handle, "info@example.org", 0).await.unwrap(), vec![ "bill@example.org".to_string(), @@ -241,7 +253,7 @@ async fn ldap_directory() { compare_sorted( core.expn(&handle, "john@example.org", 0).await.unwrap(), Vec::::new(), - ); + );*/ } fn compare_sorted(v1: Vec, v2: Vec) { diff --git a/tests/src/directory/mod.rs b/tests/src/directory/mod.rs index 4f4dca79..67aca0f8 100644 --- a/tests/src/directory/mod.rs +++ b/tests/src/directory/mod.rs @@ -701,13 +701,15 @@ async 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_principal_id(name.as_ref()) - .await - .unwrap() - .unwrap(), - ); + ids.push(map_account_id(store, name).await); } ids } + +async fn map_account_id(store: &Store, name: impl AsRef) -> u32 { + store + .get_principal_id(name.as_ref()) + .await + .unwrap() + .unwrap() +} diff --git a/tests/src/directory/smtp.rs b/tests/src/directory/smtp.rs index cb5fdc72..4e2711ab 100644 --- a/tests/src/directory/smtp.rs +++ b/tests/src/directory/smtp.rs @@ -7,7 +7,7 @@ use std::sync::Arc; use common::listener::limiter::{ConcurrencyLimiter, InFlight}; -use directory::QueryBy; +use directory::{backend::RcptType, QueryBy}; use mail_parser::decoders::base64::base64_decode; use mail_send::Credentials; use tokio::{ @@ -78,7 +78,9 @@ async fn lmtp_directory() { for (item, expected) in &tests { let result: LookupResult = match item { - Item::IsAccount(v) => core.rcpt(&handle, v, 0).await.unwrap().into(), + Item::IsAccount(v) => { + (core.rcpt(&handle, v, 0).await.unwrap() == RcptType::Mailbox).into() + } Item::Authenticate(v) => handle .query(QueryBy::Credentials(v), true) .await @@ -122,7 +124,9 @@ async fn lmtp_directory() { requests.push(( tokio::spawn(async move { let result: LookupResult = match &item { - Item::IsAccount(v) => core.rcpt(&handle, v, 0).await.unwrap().into(), + Item::IsAccount(v) => { + (core.rcpt(&handle, v, 0).await.unwrap() == RcptType::Mailbox).into() + } Item::Authenticate(v) => handle .query(QueryBy::Credentials(v), true) .await @@ -182,7 +186,9 @@ async fn lmtp_directory() { requests.push(( tokio::spawn(async move { let result: LookupResult = match &item { - Item::IsAccount(v) => core.rcpt(&handle, v, 0).await.unwrap().into(), + Item::IsAccount(v) => { + (core.rcpt(&handle, v, 0).await.unwrap() == RcptType::Mailbox).into() + } _ => unreachable!(), }; diff --git a/tests/src/directory/sql.rs b/tests/src/directory/sql.rs index 99488582..72b9156a 100644 --- a/tests/src/directory/sql.rs +++ b/tests/src/directory/sql.rs @@ -4,11 +4,18 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use directory::{backend::internal::manage::ManageDirectory, QueryBy, Type, ROLE_USER}; +use directory::{ + backend::{internal::manage::ManageDirectory, RcptType}, + QueryBy, Type, ROLE_USER, +}; use mail_send::Credentials; + +#[allow(unused_imports)] use store::{LookupStore, Store}; -use crate::directory::{map_account_ids, DirectoryTest, IntoTestPrincipal, TestPrincipal}; +use crate::directory::{ + map_account_id, map_account_ids, DirectoryTest, IntoTestPrincipal, TestPrincipal, +}; use super::DirectoryStore; @@ -243,40 +250,28 @@ async fn sql_directory() { // Ids by email assert_eq!( - core.email_to_ids(&handle, "jane@example.org", 0) + core.email_to_id(&handle, "jane@example.org", 0) .await .unwrap(), - map_account_ids(base_store, vec!["jane"]).await + Some(map_account_id(base_store, "jane").await) ); assert_eq!( - core.email_to_ids(&handle, "info@example.org", 0) + core.email_to_id(&handle, "jane+alias@example.org", 0) .await .unwrap(), - map_account_ids(base_store, vec!["bill", "jane", "john"]).await + Some(map_account_id(base_store, "jane").await) ); assert_eq!( - core.email_to_ids(&handle, "jane+alias@example.org", 0) + core.email_to_id(&handle, "unknown@example.org", 0) .await .unwrap(), - map_account_ids(base_store, vec!["jane"]).await + None ); assert_eq!( - core.email_to_ids(&handle, "info+alias@example.org", 0) + core.email_to_id(&handle, "anything@catchall.org", 0) .await .unwrap(), - map_account_ids(base_store, vec!["bill", "jane", "john"]).await - ); - assert_eq!( - core.email_to_ids(&handle, "unknown@example.org", 0) - .await - .unwrap(), - Vec::::new() - ); - assert_eq!( - core.email_to_ids(&handle, "anything@catchall.org", 0) - .await - .unwrap(), - map_account_ids(base_store, vec!["robert"]).await + Some(map_account_id(base_store, "robert").await) ); // Domain validation @@ -284,21 +279,36 @@ async fn sql_directory() { assert!(!handle.is_local_domain("other.org").await.unwrap()); // RCPT TO - assert!(core.rcpt(&handle, "jane@example.org", 0).await.unwrap()); - assert!(core.rcpt(&handle, "info@example.org", 0).await.unwrap()); - assert!(core - .rcpt(&handle, "jane+alias@example.org", 0) - .await - .unwrap()); - assert!(core - .rcpt(&handle, "info+alias@example.org", 0) - .await - .unwrap()); - assert!(core - .rcpt(&handle, "random_user@catchall.org", 0) - .await - .unwrap()); - assert!(!core.rcpt(&handle, "invalid@example.org", 0).await.unwrap()); + assert_eq!( + core.rcpt(&handle, "jane@example.org", 0).await.unwrap(), + RcptType::Mailbox + ); + assert_eq!( + core.rcpt(&handle, "info@example.org", 0).await.unwrap(), + RcptType::Mailbox + ); + assert_eq!( + core.rcpt(&handle, "jane+alias@example.org", 0) + .await + .unwrap(), + RcptType::Mailbox + ); + assert_eq!( + core.rcpt(&handle, "info+alias@example.org", 0) + .await + .unwrap(), + RcptType::Mailbox + ); + assert_eq!( + core.rcpt(&handle, "random_user@catchall.org", 0) + .await + .unwrap(), + RcptType::Mailbox + ); + assert_eq!( + core.rcpt(&handle, "invalid@example.org", 0).await.unwrap(), + RcptType::Invalid + ); // VRFY assert_eq!( @@ -307,7 +317,10 @@ async fn sql_directory() { ); assert_eq!( core.vrfy(&handle, "john", 0).await.unwrap(), - vec!["john@example.org".to_string()] + vec![ + "john.doe@example.org".to_string(), + "john@example.org".to_string(), + ] ); assert_eq!( core.vrfy(&handle, "jane+alias@example", 0).await.unwrap(), @@ -322,8 +335,8 @@ async fn sql_directory() { Vec::::new() ); - // EXPN - assert_eq!( + // EXPN (now handled by the internal store) + /*assert_eq!( core.expn(&handle, "info@example.org", 0).await.unwrap(), vec![ "bill@example.org".to_string(), @@ -334,7 +347,7 @@ async fn sql_directory() { assert_eq!( core.expn(&handle, "john@example.org", 0).await.unwrap(), Vec::::new() - ); + );*/ } } diff --git a/tests/src/jmap/delivery.rs b/tests/src/jmap/delivery.rs index 18710268..91481238 100644 --- a/tests/src/jmap/delivery.rs +++ b/tests/src/jmap/delivery.rs @@ -482,6 +482,7 @@ impl SmtpConnection { //let c = println!("-> {:?}", text); self.writer.write_all(text.as_bytes()).await.unwrap(); self.writer.write_all(b"\r\n").await.unwrap(); + self.writer.flush().await.unwrap(); } pub async fn send_raw(&mut self, text: &str) { diff --git a/tests/src/jmap/enterprise.rs b/tests/src/jmap/enterprise.rs index e446a283..63264221 100644 --- a/tests/src/jmap/enterprise.rs +++ b/tests/src/jmap/enterprise.rs @@ -80,6 +80,7 @@ test pub async fn test(params: &mut JMAPTest) { // Enable Enterprise + println!("Running Enterprise tests..."); let mut core = params.server.inner.shared_core.load_full().as_ref().clone(); let mut config = Config::new(METRICS_CONFIG).unwrap(); core.enterprise = Enterprise { diff --git a/tests/src/jmap/mod.rs b/tests/src/jmap/mod.rs index 58f4ba52..2d29f81a 100644 --- a/tests/src/jmap/mod.rs +++ b/tests/src/jmap/mod.rs @@ -343,7 +343,7 @@ type = "console" level = "{LEVEL}" multiline = false ansi = true -disabled-events = ["network.*", "telemetry.webhook-error"] +disabled-events = ["network.*", "telemetry.webhook-error", "http.request-body"] [webhook."test"] url = "http://127.0.0.1:8821/hook" @@ -370,8 +370,8 @@ pub async fn jmap_tests() { ) .await; - /*webhooks::test(&mut params).await; - email_query::test(&mut params, delete).await; + webhooks::test(&mut params).await; + /*email_query::test(&mut params, delete).await; email_get::test(&mut params).await; email_set::test(&mut params).await; email_parse::test(&mut params).await; @@ -384,9 +384,9 @@ pub async fn jmap_tests() { mailbox::test(&mut params).await; delivery::test(&mut params).await; auth_acl::test(&mut params).await; - auth_limits::test(&mut params).await;*/ + auth_limits::test(&mut params).await; auth_oauth::test(&mut params).await; - /*event_source::test(&mut params).await; + event_source::test(&mut params).await; push_subscription::test(&mut params).await; sieve_script::test(&mut params).await; vacation_response::test(&mut params).await; @@ -396,8 +396,8 @@ pub async fn jmap_tests() { crypto::test(&mut params).await; blob::test(&mut params).await; permissions::test(¶ms).await; - purge::test(&mut params).await; - enterprise::test(&mut params).await;*/ + purge::test(&mut params).await;*/ + enterprise::test(&mut params).await; if delete { params.temp_dir.delete(); diff --git a/tests/src/jmap/permissions.rs b/tests/src/jmap/permissions.rs index 3ea842f8..80061fb1 100644 --- a/tests/src/jmap/permissions.rs +++ b/tests/src/jmap/permissions.rs @@ -414,7 +414,10 @@ pub async fn test(params: &JMAPTest) { "/api/principal", &Principal::new(u32::MAX, Type::Individual) .with_field(PrincipalField::Name, "john@foobar.org") - .with_field(PrincipalField::Roles, vec!["admin".to_string()]) + .with_field( + PrincipalField::Roles, + vec!["tenant-admin".to_string(), "user".to_string()], + ) .with_field( PrincipalField::Secrets, PrincipalValue::String("tenantpass".to_string()), @@ -571,7 +574,7 @@ pub async fn test(params: &JMAPTest) { [ ( PrincipalField::Roles, - &["admin", "no-mail-for-you@foobar.com"][..], + &["tenant-admin", "no-mail-for-you@foobar.com", "user"][..], ), (PrincipalField::Members, &[][..]), (PrincipalField::EnabledPermissions, &[][..]), diff --git a/tests/src/smtp/lookup/sql.rs b/tests/src/smtp/lookup/sql.rs index a6582597..45e0ab03 100644 --- a/tests/src/smtp/lookup/sql.rs +++ b/tests/src/smtp/lookup/sql.rs @@ -11,6 +11,10 @@ use common::{ Core, }; +use directory::{ + backend::internal::{manage::ManageDirectory, PrincipalField, PrincipalValue}, + Principal, QueryBy, Type, +}; use mail_auth::MX; use store::Stores; use utils::config::Config; @@ -136,7 +140,7 @@ async fn lookup_sql() { handle .create_test_user_with_email("mike@foobar.net", "098765", "Mike") .await; - handle + /*handle .link_test_address("jane@foobar.org", "sales@foobar.org", "list") .await; handle @@ -147,7 +151,7 @@ async fn lookup_sql() { .await; handle .link_test_address("mike@foobar.net", "support@foobar.org", "list") - .await; + .await;*/ for query in [ "CREATE TABLE domains (name TEXT PRIMARY KEY, description TEXT);", @@ -163,6 +167,53 @@ async fn lookup_sql() { .unwrap(); } + // Create local domains + let internal_store = &test.server.core.storage.data; + for name in ["foobar.org", "foobar.net"] { + internal_store + .create_principal( + Principal::new(0, Type::Domain).with_field(PrincipalField::Name, name), + None, + None, + ) + .await + .unwrap(); + } + + // Create lists + internal_store + .create_principal( + Principal::new(0, Type::List) + .with_field(PrincipalField::Name, "support@foobar.org") + .with_field(PrincipalField::Emails, "support@foobar.org") + .with_field( + PrincipalField::ExternalMembers, + PrincipalValue::StringList(vec!["mike@foobar.net".to_string()]), + ), + None, + None, + ) + .await + .unwrap(); + internal_store + .create_principal( + Principal::new(0, Type::List) + .with_field(PrincipalField::Name, "sales@foobar.org") + .with_field(PrincipalField::Emails, "sales@foobar.org") + .with_field( + PrincipalField::ExternalMembers, + PrincipalValue::StringList(vec![ + "jane@foobar.org".to_string(), + "john@foobar.org".to_string(), + "bill@foobar.org".to_string(), + ]), + ), + None, + None, + ) + .await + .unwrap(); + // Test expression functions let token_map = TokenMap::default().with_variables(&[ V_RECIPIENT, @@ -220,6 +271,9 @@ async fn lookup_sql() { session.rcpt_to("john@foobar.org", "250").await; session.rcpt_to("bill@foobar.org", "250").await; + // Lists + session.rcpt_to("sales@foobar.org", "250").await; + // Test EXPN session .cmd("EXPN sales@foobar.org", "250") @@ -234,6 +288,24 @@ async fn lookup_sql() { session.cmd("EXPN marketing@foobar.org", "550 5.1.2").await; // Test VRFY + session + .server + .core + .storage + .directory + .query(QueryBy::Name("john@foobar.org"), true) + .await + .unwrap() + .unwrap(); + session + .server + .core + .storage + .directory + .query(QueryBy::Name("jane@foobar.org"), true) + .await + .unwrap() + .unwrap(); session .cmd("VRFY john", "250") .await diff --git a/tests/src/store/import_export.rs b/tests/src/store/import_export.rs index b90bdec8..752e90cb 100644 --- a/tests/src/store/import_export.rs +++ b/tests/src/store/import_export.rs @@ -269,10 +269,7 @@ struct KeyValue { impl Snapshot { async fn new(db: &Store) -> Self { - let is_sql = matches!( - db, - Store::SQLite(_) | Store::PostgreSQL(_) | Store::MySQL(_) - ); + let is_sql = db.is_sql(); let mut keys = AHashSet::new();