Support for external email addresses on mailing lists (closes #152)
Some checks failed
trivy / Check (push) Has been cancelled

This commit is contained in:
mdecimus 2024-11-13 19:38:54 +13:00
parent 77de725ca8
commit b2bac5d5aa
45 changed files with 813 additions and 643 deletions

1
Cargo.lock generated
View file

@ -7470,6 +7470,7 @@ dependencies = [
"parking_lot",
"pem",
"privdrop",
"psl",
"rand 0.8.5",
"rcgen 0.13.1",
"regex",

View file

@ -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(

View file

@ -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();

View file

@ -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!()))
}

View file

@ -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)
}
}

View file

@ -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

View file

@ -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,
}
}

View file

@ -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())

View file

@ -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
}
}

View file

@ -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>,

View file

@ -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>> {

View file

@ -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
}
}
}

View file

@ -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
}

View file

@ -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)),

View file

@ -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))

View file

@ -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);

View file

@ -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,
}

View file

@ -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(_) => {}
}
}

View file

@ -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)

View file

@ -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>()?;

View file

@ -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"}

View file

@ -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;
}
}

View file

@ -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() {

View file

@ -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 {

View file

@ -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
}
}

View file

@ -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);

View file

@ -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
}
}

View file

@ -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;

View file

@ -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,

View file

@ -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
}

View file

@ -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)]

View file

@ -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;
}

View file

@ -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"

View file

@ -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
}
}

View file

@ -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:?}"),
}
}

View file

@ -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>) {

View file

@ -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()
}

View file

@ -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!(),
};

View file

@ -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()
);
);*/
}
}

View file

@ -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) {

View file

@ -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 {

View file

@ -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(&params).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();

View file

@ -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, &[][..]),

View file

@ -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

View file

@ -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();