mirror of
https://github.com/stalwartlabs/mail-server.git
synced 2024-11-24 06:19:46 +00:00
Support for external email addresses on mailing lists (closes #152)
Some checks failed
trivy / Check (push) Has been cancelled
Some checks failed
trivy / Check (push) Has been cancelled
This commit is contained in:
parent
77de725ca8
commit
b2bac5d5aa
45 changed files with 813 additions and 643 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -7470,6 +7470,7 @@ dependencies = [
|
|||
"parking_lot",
|
||||
"pem",
|
||||
"privdrop",
|
||||
"psl",
|
||||
"rand 0.8.5",
|
||||
"rcgen 0.13.1",
|
||||
"regex",
|
||||
|
|
|
@ -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<Vec<u32>> {
|
||||
) -> trc::Result<Option<u32>> {
|
||||
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<bool> {
|
||||
) -> trc::Result<RcptType> {
|
||||
// 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(
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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<Vec<u32>> {
|
||||
pub async fn email_to_id(&self, _address: &str) -> trc::Result<Option<u32>> {
|
||||
Err(trc::StoreEvent::NotSupported.caused_by(trc::location!()))
|
||||
}
|
||||
|
||||
pub async fn rcpt(&self, _address: &str) -> trc::Result<bool> {
|
||||
pub async fn rcpt(&self, _address: &str) -> trc::Result<RcptType> {
|
||||
Err(trc::StoreEvent::NotSupported.caused_by(trc::location!()))
|
||||
}
|
||||
|
||||
|
|
|
@ -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<Option<Principal>>;
|
||||
async fn email_to_ids(&self, email: &str) -> trc::Result<Vec<u32>>;
|
||||
async fn email_to_id(&self, address: &str) -> trc::Result<Option<u32>>;
|
||||
async fn is_local_domain(&self, domain: &str) -> trc::Result<bool>;
|
||||
async fn rcpt(&self, address: &str) -> trc::Result<bool>;
|
||||
async fn rcpt(&self, address: &str) -> trc::Result<RcptType>;
|
||||
async fn vrfy(&self, address: &str) -> trc::Result<Vec<String>>;
|
||||
async fn expn(&self, address: &str) -> trc::Result<Vec<String>>;
|
||||
async fn expn_by_id(&self, id: u32) -> trc::Result<Vec<String>>;
|
||||
}
|
||||
|
||||
impl DirectoryStore for Store {
|
||||
|
@ -77,21 +78,12 @@ impl DirectoryStore for Store {
|
|||
Ok(None)
|
||||
}
|
||||
|
||||
async fn email_to_ids(&self, email: &str) -> trc::Result<Vec<u32>> {
|
||||
if let Some(ptype) = self
|
||||
.get_value::<PrincipalInfo>(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<Option<u32>> {
|
||||
self.get_value::<PrincipalInfo>(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<bool> {
|
||||
|
@ -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<bool> {
|
||||
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<RcptType> {
|
||||
if let Some(pinfo) = self
|
||||
.get_value::<PrincipalInfo>(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<Vec<String>> {
|
||||
|
@ -143,7 +144,6 @@ impl DirectoryStore for Store {
|
|||
}
|
||||
|
||||
async fn expn(&self, address: &str) -> trc::Result<Vec<String>> {
|
||||
let mut results = Vec::new();
|
||||
if let Some(ptype) = self
|
||||
.get_value::<PrincipalInfo>(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<Vec<String>> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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::<trc::Result<_>>()?;
|
||||
}
|
||||
|
||||
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<u32>,
|
||||
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
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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<Vec<u32>> {
|
||||
pub async fn email_to_id(&self, address: &str) -> trc::Result<Option<u32>> {
|
||||
let filter = self.mappings.filter_email.build(address.as_ref());
|
||||
let rs = self
|
||||
.pool
|
||||
|
@ -240,29 +243,28 @@ impl LdapDirectory {
|
|||
.collect::<Vec<_>>()
|
||||
);
|
||||
|
||||
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<bool> {
|
||||
pub async fn rcpt(&self, address: &str) -> trc::Result<RcptType> {
|
||||
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<Vec<String>> {
|
||||
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::<Vec<_>>()
|
||||
);
|
||||
|
||||
Ok(emails)
|
||||
self.data_store.vrfy(address).await
|
||||
}
|
||||
|
||||
pub async fn expn(&self, address: &str) -> trc::Result<Vec<String>> {
|
||||
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::<Vec<_>>()
|
||||
);
|
||||
|
||||
Ok(emails)
|
||||
self.data_store.expn(address).await
|
||||
}
|
||||
|
||||
pub async fn is_local_domain(&self, domain: &str) -> trc::Result<bool> {
|
||||
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::<String>::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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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<String>,
|
||||
attr_type: Vec<String>,
|
||||
attr_groups: Vec<String>,
|
||||
|
|
|
@ -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<Vec<u32>> {
|
||||
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::<Vec<_>>()
|
||||
})
|
||||
.unwrap_or_default())
|
||||
pub async fn email_to_id(&self, address: &str) -> trc::Result<Option<u32>> {
|
||||
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<bool> {
|
||||
Ok(self.emails_to_ids.contains_key(address))
|
||||
pub async fn rcpt(&self, address: &str) -> trc::Result<RcptType> {
|
||||
Ok(self.emails_to_ids.contains_key(address).into())
|
||||
}
|
||||
|
||||
pub async fn vrfy(&self, address: &str) -> trc::Result<Vec<String>> {
|
||||
|
|
|
@ -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<String>),
|
||||
#[default]
|
||||
Invalid,
|
||||
}
|
||||
|
||||
impl From<bool> for RcptType {
|
||||
fn from(value: bool) -> Self {
|
||||
if value {
|
||||
RcptType::Mailbox
|
||||
} else {
|
||||
RcptType::Invalid
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<Vec<u32>> {
|
||||
self.data_store.email_to_ids(address).await
|
||||
pub async fn email_to_id(&self, address: &str) -> trc::Result<Option<u32>> {
|
||||
self.data_store.email_to_id(address).await
|
||||
}
|
||||
|
||||
pub async fn rcpt(&self, address: &str) -> trc::Result<bool> {
|
||||
pub async fn rcpt(&self, address: &str) -> trc::Result<RcptType> {
|
||||
self.data_store.rcpt(address).await
|
||||
}
|
||||
|
||||
|
|
|
@ -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<Vec<u32>> {
|
||||
pub async fn email_to_id(&self, _address: &str) -> trc::Result<Option<u32>> {
|
||||
Err(trc::StoreEvent::NotSupported.caused_by(trc::location!()))
|
||||
}
|
||||
|
||||
pub async fn rcpt(&self, address: &str) -> trc::Result<bool> {
|
||||
pub async fn rcpt(&self, address: &str) -> trc::Result<RcptType> {
|
||||
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)),
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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::<Rows>(
|
||||
&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<Vec<u32>> {
|
||||
pub async fn email_to_id(&self, address: &str) -> trc::Result<Option<u32>> {
|
||||
let names = self
|
||||
.store
|
||||
.query::<Rows>(&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<bool> {
|
||||
self.store
|
||||
pub async fn rcpt(&self, address: &str) -> trc::Result<RcptType> {
|
||||
let result = self
|
||||
.store
|
||||
.query::<bool>(
|
||||
&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<Vec<String>> {
|
||||
self.store
|
||||
.query::<Rows>(
|
||||
&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<Vec<String>> {
|
||||
self.store
|
||||
.query::<Rows>(
|
||||
&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<bool> {
|
||||
self.store
|
||||
.query::<bool>(&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);
|
||||
|
|
|
@ -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<String>,
|
||||
column_secret: String,
|
||||
column_email: String,
|
||||
column_quota: String,
|
||||
column_type: String,
|
||||
}
|
||||
|
|
|
@ -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<LookupCache<String>>,
|
||||
cached_rcpts: Mutex<LookupCache<String>>,
|
||||
|
@ -52,15 +54,15 @@ impl CachedDirectory {
|
|||
})
|
||||
}
|
||||
|
||||
pub fn get_rcpt(&self, address: &str) -> Option<bool> {
|
||||
self.cached_rcpts.lock().get(address)
|
||||
pub fn get_rcpt(&self, address: &str) -> Option<RcptType> {
|
||||
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(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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<Vec<u32>> {
|
||||
pub async fn email_to_id(&self, address: &str) -> trc::Result<Option<u32>> {
|
||||
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<bool> {
|
||||
pub async fn rcpt(&self, email: &str) -> trc::Result<RcptType> {
|
||||
// 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)
|
||||
|
|
|
@ -844,16 +844,19 @@ impl<'de> serde::Deserialize<'de> for Principal {
|
|||
| PrincipalField::Lists
|
||||
| PrincipalField::EnabledPermissions
|
||||
| PrincipalField::DisabledPermissions
|
||||
| PrincipalField::Urls => match map.next_value::<StringOrMany>()? {
|
||||
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>()? {
|
||||
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::<IgnoredAny>()?;
|
||||
|
|
|
@ -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"}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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<String> {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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<u32, usize> = 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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -82,6 +82,7 @@ pub struct SessionData {
|
|||
pub mail_from: Option<SessionAddress>,
|
||||
pub rcpt_to: Vec<SessionAddress>,
|
||||
pub rcpt_errors: usize,
|
||||
pub rcpt_oks: usize,
|
||||
pub message: Vec<u8>,
|
||||
|
||||
pub authenticated_as: Option<Arc<AccessToken>>,
|
||||
|
@ -101,7 +102,7 @@ pub struct SessionData {
|
|||
pub dnsbl_error: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
#[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,
|
||||
|
|
|
@ -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<T: SessionStream> Session<T> {
|
|||
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<T: SessionStream> Session<T> {
|
|||
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::<String, _>(
|
||||
|
@ -194,43 +198,43 @@ impl<T: SessionStream> Session<T> {
|
|||
.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<T: SessionStream> Session<T> {
|
|||
.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
|
||||
}
|
||||
|
||||
|
|
|
@ -338,15 +338,15 @@ impl<T: SessionStream> Session<T> {
|
|||
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<T: SessionStream> Session<T> {
|
|||
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<T: AsyncWrite + AsyncRead + Unpin> Session<T> {
|
|||
self.data.priority = 0;
|
||||
self.data.delivery_by = 0;
|
||||
self.data.future_release = 0;
|
||||
self.data.rcpt_oks = 0;
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -248,3 +248,39 @@ impl ServerCertVerifier for DummyVerifier {
|
|||
]
|
||||
}
|
||||
}
|
||||
|
||||
// Basic email sanitizer
|
||||
pub fn sanitize_email(email: &str) -> Option<String> {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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::<AHashSet<_>>(),
|
||||
[john_id, jane_id].into_iter().collect::<AHashSet<_>>(),
|
||||
);
|
||||
|
||||
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::<AHashSet<_>>(),
|
||||
["john@example.org", "jane@example.org"]
|
||||
.into_iter()
|
||||
.map(|s| s.to_string())
|
||||
.collect::<AHashSet<_>>()
|
||||
[
|
||||
"john@example.org",
|
||||
"mike@other.org",
|
||||
"lucy@foobar.net",
|
||||
"jane@example.org"
|
||||
]
|
||||
.into_iter()
|
||||
.map(|s| s.to_string())
|
||||
.collect::<AHashSet<_>>()
|
||||
);
|
||||
|
||||
// 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::<AHashSet<_>>(),
|
||||
[john_id, jane_id].into_iter().collect::<AHashSet<_>>()
|
||||
);
|
||||
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::<u32>::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<Item = &str>,
|
||||
) {
|
||||
match store.rcpt(list_addr).await.unwrap() {
|
||||
RcptType::List(items) => {
|
||||
assert_eq!(
|
||||
items.into_iter().collect::<AHashSet<_>>(),
|
||||
members
|
||||
.into_iter()
|
||||
.map(|s| s.to_string())
|
||||
.collect::<AHashSet<_>>()
|
||||
);
|
||||
}
|
||||
other => panic!("invalid {other:?}"),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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::<u32>::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::<String>::new(),
|
||||
);
|
||||
);*/
|
||||
}
|
||||
|
||||
fn compare_sorted<T: Eq + Debug>(v1: Vec<T>, v2: Vec<T>) {
|
||||
|
|
|
@ -701,13 +701,15 @@ async fn address_mappings() {
|
|||
async fn map_account_ids(store: &Store, names: Vec<impl AsRef<str>>) -> Vec<u32> {
|
||||
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<str>) -> u32 {
|
||||
store
|
||||
.get_principal_id(name.as_ref())
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
}
|
||||
|
|
|
@ -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!(),
|
||||
};
|
||||
|
||||
|
|
|
@ -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::<u32>::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::<String>::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::<String>::new()
|
||||
);
|
||||
);*/
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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, &[][..]),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
Loading…
Reference in a new issue