Internal directory + HTTP management API passing tests

This commit is contained in:
mdecimus 2023-12-19 12:38:46 +01:00
parent ea94de6d77
commit f7313eecaf
65 changed files with 442 additions and 206 deletions

View file

@ -70,10 +70,10 @@ async fn main() -> std::io::Result<()> {
command.exec(client).await;
}
Commands::Database(command) => command.exec(client).await,
Commands::Account(_) => todo!(),
Commands::Domain(_) => todo!(),
Commands::List(_) => todo!(),
Commands::Group(_) => todo!(),
Commands::Account(command) => command.exec(client).await,
Commands::Domain(command) => command.exec(client).await,
Commands::List(command) => command.exec(client).await,
Commands::Group(command) => command.exec(client).await,
Commands::Queue(command) => command.exec(client).await,
Commands::Report(command) => command.exec(client).await,
}

View file

@ -47,20 +47,32 @@ const RETRY_ATTEMPTS: usize = 5;
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Principal {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub id: Option<u32>,
#[serde(rename = "type")]
pub typ: Option<Type>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub quota: Option<u32>,
#[serde(rename = "usedQuota")]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub used_quota: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub secrets: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub emails: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[serde(rename = "memberOf")]
pub member_of: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
}

View file

@ -35,7 +35,7 @@ use super::{
};
#[async_trait::async_trait]
pub trait ManageDirectory {
pub trait ManageDirectory: Sized {
async fn get_account_id(&self, name: &str) -> crate::Result<Option<u32>>;
async fn get_or_create_account_id(&self, name: &str) -> crate::Result<u32>;
async fn get_account_name(&self, account_id: u32) -> crate::Result<Option<String>>;
@ -65,6 +65,7 @@ pub trait ManageDirectory {
start_from: Option<&str>,
limit: usize,
) -> crate::Result<Vec<String>>;
async fn init(self) -> crate::Result<Self>;
}
#[async_trait::async_trait]
@ -197,7 +198,7 @@ impl ManageDirectory for Store {
}
// Assign accountId
let account_id = self
principal.id = self
.assign_document_id(u32::MAX, Collection::Principal)
.await?;
@ -211,19 +212,19 @@ impl ManageDirectory for Store {
(),
)
.set(
ValueClass::Directory(DirectoryClass::Principal(account_id)),
ValueClass::Directory(DirectoryClass::Principal(principal.id)),
(&principal).serialize(),
)
.set(
ValueClass::Directory(DirectoryClass::NameToId(principal.name.into_bytes())),
PrincipalIdType::new(account_id, principal.typ.into_base_type()).serialize(),
PrincipalIdType::new(principal.id, principal.typ.into_base_type()).serialize(),
);
// Write email to id mapping
let ids = if matches!(principal.typ, Type::List) {
principal.member_of
} else {
vec![account_id]
vec![principal.id]
};
for email in principal.emails {
@ -235,7 +236,7 @@ impl ManageDirectory for Store {
self.write(batch.build()).await?;
Ok(account_id)
Ok(principal.id)
}
async fn delete_account(&self, by: QueryBy<'_>) -> crate::Result<()> {
@ -681,6 +682,41 @@ impl ManageDirectory for Store {
Ok(results)
}
async fn init(self) -> crate::Result<Self> {
if let (Ok(admin_user), Ok(admin_pass)) = (
std::env::var("SET_ADMIN_USER"),
std::env::var("SET_ADMIN_PASS"),
) {
if let Some(account_id) = self.get_account_id(&admin_user).await? {
self.update_account(
QueryBy::Id(account_id),
vec![PrincipalUpdate {
action: PrincipalAction::Set,
field: PrincipalField::Secrets,
value: PrincipalValue::StringList(vec![admin_pass]),
}],
)
.await?;
eprintln!("Successfully updated administrator password for {admin_user:?}.");
} else {
self.create_account(Principal {
typ: Type::Superuser,
quota: 0,
name: admin_user.clone(),
secrets: vec![admin_pass],
emails: vec![],
member_of: vec![],
description: "Superuser".to_string().into(),
..Default::default()
})
.await?;
eprintln!("Successfully created administrator account {admin_user:?}.");
}
std::process::exit(0);
}
Ok(self)
}
}
impl From<Principal<String>> for Principal<u32> {

View file

@ -21,23 +21,22 @@
* for more details.
*/
use ahash::AHashMap;
use store::Store;
use utils::config::{utils::AsKey, Config};
use crate::{Principal, Type};
use super::{EmailType, MemoryDirectory, NameToId};
use super::{EmailType, MemoryDirectory};
impl MemoryDirectory {
pub fn from_config(
pub async fn from_config(
config: &Config,
prefix: impl AsKey,
_: Option<Store>,
id_store: Option<Store>,
) -> utils::config::Result<Self> {
let prefix = prefix.as_key();
let mut directory = MemoryDirectory {
names_to_ids: NameToId::Internal(AHashMap::new()),
names_to_ids: id_store.into(),
..Default::default()
};
@ -45,32 +44,37 @@ impl MemoryDirectory {
let name = config
.value_require((prefix.as_str(), "principals", lookup_id, "name"))?
.to_string();
let typ =
match config.value_require((prefix.as_str(), "principals", lookup_id, "type"))? {
"individual" => Type::Individual,
"admin" => Type::Superuser,
"group" => Type::Group,
_ => Type::Individual,
};
let typ = match config.value((prefix.as_str(), "principals", lookup_id, "type")) {
Some("individual") => Type::Individual,
Some("admin") => Type::Superuser,
Some("group") => Type::Group,
_ => Type::Individual,
};
// Obtain id
let id = directory.names_to_ids.get_or_insert(&name).map_err(|err| {
format!(
"Failed to obtain id for principal {} ({}): {:?}",
name, lookup_id, err
)
})?;
let id = directory
.names_to_ids
.get_or_insert(&name)
.await
.map_err(|err| {
format!(
"Failed to obtain id for principal {} ({}): {:?}",
name, lookup_id, err
)
})?;
// Obtain group ids
let mut member_of = Vec::new();
for (_, group) in config.values((prefix.as_str(), "principals", lookup_id, "member-of"))
{
member_of.push(directory.names_to_ids.get_or_insert(group).map_err(|err| {
format!(
"Failed to obtain id for principal {} ({}): {:?}",
name, lookup_id, err
)
})?);
member_of.push(directory.names_to_ids.get_or_insert(group).await.map_err(
|err| {
format!(
"Failed to obtain id for principal {} ({}): {:?}",
name, lookup_id, err
)
},
)?);
}
// Parse email addresses

View file

@ -23,7 +23,6 @@
use ahash::{AHashMap, AHashSet};
use store::Store;
use tokio::sync::oneshot;
use crate::Principal;
@ -70,24 +69,13 @@ impl From<Option<Store>> for NameToId {
}
impl NameToId {
pub fn get_or_insert(&mut self, name: &str) -> crate::Result<u32> {
pub async fn get_or_insert(&mut self, name: &str) -> crate::Result<u32> {
match self {
Self::Internal(map) => {
let next_id = map.len() as u32;
Ok(*map.entry(name.to_string()).or_insert(next_id))
}
Self::Store(store) => {
let (tx, rx) = oneshot::channel();
let store = store.clone();
let name = name.to_string();
tokio::spawn(async move {
let _ = tx.send(store.get_or_create_account_id(&name).await);
});
match rx.blocking_recv() {
Ok(result) => result,
Err(_) => Err(crate::DirectoryError::Unsupported),
}
}
Self::Store(store) => store.get_or_create_account_id(name).await,
}
}
}

View file

@ -37,35 +37,42 @@ use ahash::AHashMap;
use crate::{
backend::{
imap::ImapDirectory, ldap::LdapDirectory, memory::MemoryDirectory, smtp::SmtpDirectory,
sql::SqlDirectory,
imap::ImapDirectory, internal::manage::ManageDirectory, ldap::LdapDirectory,
memory::MemoryDirectory, smtp::SmtpDirectory, sql::SqlDirectory,
},
AddressMapping, Directories, Directory, DirectoryInner,
AddressMapping, Directories, Directory, DirectoryInner, Lookup,
};
use super::cache::CachedDirectory;
#[async_trait::async_trait]
pub trait ConfigDirectory {
fn parse_directory(
async fn parse_directory(
&self,
stores: &Stores,
id_store: Option<&str>,
) -> utils::config::Result<Directories>;
}
#[async_trait::async_trait]
impl ConfigDirectory for Config {
fn parse_directory(
async fn parse_directory(
&self,
stores: &Stores,
id_store: Option<&str>,
) -> utils::config::Result<Directories> {
let mut config = Directories {
directories: AHashMap::new(),
lookups: AHashMap::new(),
};
let id_store = id_store.and_then(|id| stores.stores.get(id).cloned());
for id in self.sub_keys("directory") {
// Parse directory
if self.property_or_static::<bool>(("directory", id, "disable"), "false")? {
tracing::debug!("Skipping disabled directory {id:?}.");
continue;
}
let protocol = self.value_require(("directory", id, "type"))?;
let prefix = ("directory", id);
let store = match protocol {
@ -80,6 +87,16 @@ impl ConfigDirectory for Config {
self.value_require(("directory", id, "store")).unwrap(),
id
)
})?
.init()
.await
.map_err(|err| {
format!(
"Failed to initialize store {:?} for directory {:?}: {:?}.",
self.value_require(("directory", id, "store")).unwrap(),
id,
err
)
})?,
),
"ldap" => DirectoryInner::Ldap(LdapDirectory::from_config(
@ -96,31 +113,40 @@ impl ConfigDirectory for Config {
"imap" => DirectoryInner::Imap(ImapDirectory::from_config(self, prefix)?),
"smtp" => DirectoryInner::Smtp(SmtpDirectory::from_config(self, prefix, false)?),
"lmtp" => DirectoryInner::Smtp(SmtpDirectory::from_config(self, prefix, true)?),
"memory" => DirectoryInner::Memory(MemoryDirectory::from_config(
self,
prefix,
id_store.clone(),
)?),
"memory" => DirectoryInner::Memory(
MemoryDirectory::from_config(self, prefix, id_store.clone()).await?,
),
unknown => {
return Err(format!("Unknown directory type: {unknown:?}"));
}
};
config.directories.insert(
id.to_string(),
Arc::new(Directory {
store,
catch_all: AddressMapping::from_config(
self,
("directory", id, "options.catch-all"),
)?,
subaddressing: AddressMapping::from_config(
self,
("directory", id, "options.subaddressing"),
)?,
cache: CachedDirectory::try_from_config(self, ("directory", id))?,
}),
// Build directory
let directory = Arc::new(Directory {
store,
catch_all: AddressMapping::from_config(
self,
("directory", id, "options.catch-all"),
)?,
subaddressing: AddressMapping::from_config(
self,
("directory", id, "options.subaddressing"),
)?,
cache: CachedDirectory::try_from_config(self, ("directory", id))?,
});
// Add lookups
config.lookups.insert(
format!("{id}/domains"),
Lookup::DomainExists(directory.clone()),
);
config.lookups.insert(
format!("{id}/recipients"),
Lookup::EmailExists(directory.clone()),
);
// Add directory
config.directories.insert(id.to_string(), directory);
}
Ok(config)

View file

@ -172,6 +172,13 @@ pub enum AddressMapping {
#[derive(Default, Clone, Debug)]
pub struct Directories {
pub directories: AHashMap<String, Arc<Directory>>,
pub lookups: AHashMap<String, Lookup>,
}
#[derive(Clone, Debug)]
pub enum Lookup {
DomainExists(Arc<Directory>),
EmailExists(Arc<Directory>),
}
pub type Result<T> = std::result::Result<T, DirectoryError>;

View file

@ -77,7 +77,7 @@ impl JMAP {
RequestError::blank(
StatusCode::BAD_REQUEST.as_u16(),
"Invalid parameters",
"Failed to deserialize principal object",
"Failed to deserialize create request",
)
.into_http_response()
}

View file

@ -180,13 +180,7 @@ impl JMAP {
}))
.await
{
Ok(Some(mut principal)) => {
if !principal.has_name() {
principal.name = username.to_string();
}
AccessToken::new(principal).into()
}
Ok(Some(principal)) => AccessToken::new(principal).into(),
Ok(None) => {
let _ = self.is_auth_allowed_hard(remote_addr);
None

View file

@ -308,8 +308,8 @@ impl JMAP {
params.keywords,
params
.mailbox_ids
.into_iter()
.map(UidMailbox::from)
.iter()
.map(|id| UidMailbox::from(*id))
.collect(),
params.received_at.unwrap_or_else(now),
)
@ -335,6 +335,17 @@ impl JMAP {
// Request FTS index
let _ = self.housekeeper_tx.send(Event::IndexStart).await;
tracing::debug!(
context = "email_ingest",
event = "success",
account_id = ?params.account_id,
document_id = ?document_id,
mailbox_ids = ?params.mailbox_ids,
change_id = ?change_id,
blob_id = ?blob_id.hash,
size = raw_message_len,
"Ingested e-mail.");
Ok(IngestedEmail {
id,
change_id,

View file

@ -49,6 +49,7 @@ async fn main() -> std::io::Result<()> {
let stores = config.parse_stores().await.failed("Invalid configuration");
let directory = config
.parse_directory(&stores, config.value("jmap.store.data"))
.await
.failed("Invalid configuration");
let schedulers = config
.parse_purge_schedules(

View file

@ -215,7 +215,9 @@ impl ConfigCondition for Config {
})?)
}
MatchType::Lookup => {
if let Some(lookup) = ctx.stores.lookups.get(value_str) {
if let Some(lookup) = ctx.directory.lookups.get(value_str) {
ConditionMatch::Lookup(lookup.clone().into())
} else if let Some(lookup) = ctx.stores.lookups.get(value_str) {
ConditionMatch::Lookup(lookup.clone().into())
} else {
return Err(format!(

View file

@ -119,7 +119,13 @@ impl ConfigSieve for Config {
)
.with_max_header_size(10240)
.with_valid_notification_uri("mailto")
.with_valid_ext_lists(ctx.stores.lookups.keys().map(|k| k.to_string()))
.with_valid_ext_lists(
ctx.stores
.lookups
.keys()
.chain(ctx.directory.lookups.keys())
.map(|k| k.to_string()),
)
.with_functions(&mut fnc_map);
if let Some(value) = self.property("sieve.trusted.limits.redirects")? {
@ -192,9 +198,23 @@ impl ConfigSieve for Config {
.lookups
.iter()
.map(|(k, v)| (k.to_string(), v.clone().into()))
.chain(
ctx.directory
.lookups
.iter()
.map(|(k, v)| (k.to_string(), v.clone().into())),
)
.collect(),
lookup_stores: ctx.stores.lookup_stores.clone(),
directories: ctx.directory.directories.clone(),
default_directory: self
.value("sieve.trusted.default.directory")
.and_then(|id| ctx.directory.directories.get(id))
.cloned(),
default_lookup_store: self
.value("sieve.trusted.default.store")
.and_then(|id| ctx.stores.lookup_stores.get(id))
.cloned(),
from_addr: self
.value("sieve.trusted.from-addr")
.map(|a| a.to_string())

View file

@ -120,6 +120,8 @@ pub struct SieveCore {
pub directories: AHashMap<String, Arc<Directory>>,
pub lookup_stores: AHashMap<String, LookupStore>,
pub lookup: AHashMap<String, Lookup>,
pub default_lookup_store: Option<LookupStore>,
pub default_directory: Option<Arc<Directory>>,
}
pub struct Resolvers {
@ -158,7 +160,10 @@ pub struct TlsConnectors {
}
#[derive(Clone)]
pub struct Lookup(Arc<store::Lookup>);
pub enum Lookup {
Store(Arc<store::Lookup>),
Directory(directory::Lookup),
}
pub enum State {
Request(RequestReceiver),
@ -279,49 +284,79 @@ impl SessionData {
impl Lookup {
pub async fn contains(&self, item: impl Into<Value<'_>>) -> Option<bool> {
self.0
.store
.query::<bool>(&self.0.query, vec![item.into()])
.await
.ok()
match self {
Lookup::Store(lookup) => lookup
.store
.query::<bool>(&lookup.query, vec![item.into()])
.await
.ok(),
Lookup::Directory(lookup) => match lookup {
directory::Lookup::DomainExists(directory) => directory
.is_local_domain(item.into().to_str().as_ref())
.await
.ok(),
directory::Lookup::EmailExists(directory) => {
directory.rcpt(item.into().to_str().as_ref()).await.ok()
}
},
}
}
pub async fn lookup(&self, items: Vec<Value<'_>>) -> Option<Variable> {
self.0
.store
.query::<Option<Row>>(&self.0.query, items)
.await
.ok()
.map(|row| {
let mut row = row.map(|row| row.values).unwrap_or_default();
match row.len().cmp(&1) {
Ordering::Equal if !matches!(row.first(), Some(Value::Null)) => {
row.pop().map(into_sieve_value).unwrap()
match self {
Lookup::Store(lookup) => lookup
.store
.query::<Option<Row>>(&lookup.query, items)
.await
.ok()
.map(|row| {
let mut row = row.map(|row| row.values).unwrap_or_default();
match row.len().cmp(&1) {
Ordering::Equal if !matches!(row.first(), Some(Value::Null)) => {
row.pop().map(into_sieve_value).unwrap()
}
Ordering::Less => Variable::default(),
_ => Variable::Array(
row.into_iter()
.map(into_sieve_value)
.collect::<Vec<_>>()
.into(),
),
}
Ordering::Less => Variable::default(),
_ => Variable::Array(
row.into_iter()
.map(into_sieve_value)
.collect::<Vec<_>>()
.into(),
),
}
})
}),
Lookup::Directory(_) => None,
}
}
pub async fn query(&self, items: Vec<Value<'_>>) -> Option<Vec<Value<'static>>> {
self.0
.store
.query::<Option<Row>>(&self.0.query, items)
.await
.ok()
.map(|row| row.map(|row| row.values).unwrap_or_default())
match self {
Lookup::Store(lookup) => lookup
.store
.query::<Option<Row>>(&lookup.query, items)
.await
.ok()
.map(|row| row.map(|row| row.values).unwrap_or_default()),
Lookup::Directory(_) => None,
}
}
}
impl PartialEq for Lookup {
fn eq(&self, other: &Self) -> bool {
self.0.query == other.0.query
match (self, other) {
(Lookup::Store(a), Lookup::Store(b)) => a.query == b.query,
(Lookup::Directory(a), Lookup::Directory(b)) => matches!(
(a, b),
(
directory::Lookup::DomainExists(_),
directory::Lookup::DomainExists(_)
) | (
directory::Lookup::EmailExists(_),
directory::Lookup::EmailExists(_)
)
),
_ => false,
}
}
}
@ -354,15 +389,15 @@ pub fn to_store_value(value: &Variable) -> Value<'static> {
}
}
impl AsRef<LookupStore> for Lookup {
fn as_ref(&self) -> &LookupStore {
&self.0.store
impl From<Arc<store::Lookup>> for Lookup {
fn from(lookup: Arc<store::Lookup>) -> Self {
Lookup::Store(lookup)
}
}
impl From<Arc<store::Lookup>> for Lookup {
fn from(lookup: Arc<store::Lookup>) -> Self {
Self(lookup)
impl From<directory::Lookup> for Lookup {
fn from(lookup: directory::Lookup) -> Self {
Lookup::Directory(lookup)
}
}

View file

@ -36,7 +36,7 @@ use store::backend::memory::MemoryStore;
use tokio::runtime::Handle;
use crate::{
core::SMTP,
core::{Lookup, SMTP},
queue::{DomainPart, InstantFromTimestamp, Message},
};
@ -166,12 +166,17 @@ impl SMTP {
}
Recipient::List(list) => {
if let Some(list) = self.sieve.lookup.get(&list) {
if let store::LookupStore::Memory(list) = list.as_ref() {
if let MemoryStore::List(list) = list.as_ref() {
for rcpt in &list.set {
handle.block_on(
message.add_recipient(rcpt, &self.queue.config),
);
if let Lookup::Store(list) = list {
if let store::LookupStore::Memory(list) = &list.store {
if let MemoryStore::List(list) = list.as_ref() {
for rcpt in &list.set {
handle.block_on(
message.add_recipient(
rcpt,
&self.queue.config,
),
);
}
}
}
}

View file

@ -64,7 +64,7 @@ fn train(ctx: PluginContext<'_>, is_train: bool) -> Variable {
let span: &tracing::Span = ctx.span;
let store = match &ctx.arguments[0] {
Variable::String(v) if !v.is_empty() => ctx.core.sieve.lookup_stores.get(v.as_ref()),
_ => ctx.core.sieve.lookup_stores.values().next(),
_ => ctx.core.sieve.default_lookup_store.as_ref(),
};
let store = if let Some(store) = store {
@ -163,7 +163,7 @@ pub fn exec_classify(ctx: PluginContext<'_>) -> Variable {
let span = ctx.span;
let store = match &ctx.arguments[0] {
Variable::String(v) if !v.is_empty() => ctx.core.sieve.lookup_stores.get(v.as_ref()),
_ => ctx.core.sieve.lookup_stores.values().next(),
_ => ctx.core.sieve.default_lookup_store.as_ref(),
};
let store = if let Some(store) = store {
store
@ -262,7 +262,7 @@ pub fn exec_is_balanced(ctx: PluginContext<'_>) -> Variable {
let span = ctx.span;
let store = match &ctx.arguments[0] {
Variable::String(v) if !v.is_empty() => ctx.core.sieve.lookup_stores.get(v.as_ref()),
_ => ctx.core.sieve.lookup_stores.values().next(),
_ => ctx.core.sieve.default_lookup_store.as_ref(),
};
let store = if let Some(store) = store {
store

View file

@ -88,7 +88,7 @@ pub fn exec(ctx: PluginContext<'_>) -> Variable {
None
}
Variable::String(v) if !v.is_empty() => ctx.core.sieve.lookup_stores.get(v.as_ref()),
_ => ctx.core.sieve.lookup_stores.values().next(),
_ => ctx.core.sieve.default_lookup_store.as_ref(),
};
if let Some(store) = store {
@ -151,7 +151,7 @@ pub fn exec_get(ctx: PluginContext<'_>) -> Variable {
None
}
Variable::String(v) if !v.is_empty() => ctx.core.sieve.lookup_stores.get(v.as_ref()),
_ => ctx.core.sieve.lookup_stores.values().next(),
_ => ctx.core.sieve.default_lookup_store.as_ref(),
};
if let Some(store) = store {
@ -180,7 +180,7 @@ pub fn exec_get(ctx: PluginContext<'_>) -> Variable {
pub fn exec_set(ctx: PluginContext<'_>) -> Variable {
let store = match &ctx.arguments[0] {
Variable::String(v) if !v.is_empty() => ctx.core.sieve.lookup_stores.get(v.as_ref()),
_ => ctx.core.sieve.lookup_stores.values().next(),
_ => ctx.core.sieve.default_lookup_store.as_ref(),
};
if let Some(store) = store {
@ -443,7 +443,7 @@ pub fn exec_local_domain(ctx: PluginContext<'_>) -> Variable {
if !domain.is_empty() {
let directory = match &ctx.arguments[0] {
Variable::String(v) if !v.is_empty() => ctx.core.sieve.directories.get(v.as_ref()),
_ => ctx.core.sieve.directories.values().next(),
_ => ctx.core.sieve.default_directory.as_ref(),
};
if let Some(directory) = directory {

View file

@ -150,10 +150,10 @@ impl IntoRows for Option<&Row<'_>> {
}
fn into_rows(self) -> crate::Rows {
todo!()
unreachable!()
}
fn into_named_rows(self) -> crate::NamedRows {
todo!()
unreachable!()
}
}

View file

@ -73,7 +73,11 @@ impl ConfigStore for Config {
let mut config = Stores::default();
for id in self.sub_keys("store") {
// Parse directory
// Parse store
if self.property_or_static::<bool>(("store", id, "disable"), "false")? {
tracing::debug!("Skipping disabled store {id:?}.");
continue;
}
let protocol = self
.value_require(("store", id, "type"))?
.to_ascii_lowercase();

View file

@ -28,7 +28,7 @@ use roaring::RoaringBitmap;
use crate::{
write::{key::KeySerializer, AnyKey, Batch, BitmapClass, ValueClass},
BitmapKey, Deserialize, IterateParams, Key, Store, ValueKey, SUBSPACE_BITMAPS,
SUBSPACE_INDEXES, SUBSPACE_LOGS, SUBSPACE_VALUES, U32_LEN,
SUBSPACE_INDEXES, SUBSPACE_LOGS, U32_LEN,
};
impl Store {
@ -272,7 +272,7 @@ impl Store {
#[cfg(feature = "test_mode")]
pub async fn destroy(&self) {
use crate::{SUBSPACE_BLOBS, SUBSPACE_COUNTERS};
use crate::{SUBSPACE_BLOBS, SUBSPACE_COUNTERS, SUBSPACE_VALUES};
for subspace in [
SUBSPACE_VALUES,
@ -365,7 +365,7 @@ impl Store {
#[allow(unused_variables)]
pub async fn assert_is_empty(&self, blob_store: crate::BlobStore) {
use crate::{SUBSPACE_BLOBS, SUBSPACE_COUNTERS};
use crate::{SUBSPACE_BLOBS, SUBSPACE_COUNTERS, SUBSPACE_VALUES};
self.blob_expire_all().await;
self.purge_blobs(blob_store).await.unwrap();

View file

@ -47,8 +47,6 @@ pub mod key;
pub mod log;
pub mod purge;
#[cfg(not(feature = "test_mode"))]
pub(crate) const ID_ASSIGNMENT_EXPIRY: u64 = 60 * 60; // seconds
#[cfg(not(feature = "test_mode"))]
pub(crate) const MAX_COMMIT_ATTEMPTS: u32 = 10;
#[cfg(not(feature = "test_mode"))]

View file

@ -21,6 +21,8 @@
* for more details.
*/
use std::fmt::Display;
use tokio::sync::watch;
use utils::config::cron::SimpleCron;
@ -40,14 +42,22 @@ pub struct PurgeSchedule {
impl PurgeSchedule {
pub fn spawn(self, mut shutdown_rx: watch::Receiver<bool>) {
tracing::debug!("Purge task started for store {:?}.", self.store_id);
tracing::debug!(
"Purge {} task started for store {:?}.",
self.store,
self.store_id
);
tokio::spawn(async move {
loop {
if tokio::time::timeout(self.cron.time_to_next(), shutdown_rx.changed())
.await
.is_ok()
{
tracing::debug!("Purge task exiting for store {:?}.", self.store_id);
tracing::debug!(
"Purge {} task exiting for store {:?}.",
self.store,
self.store_id
);
return;
}
@ -62,11 +72,7 @@ impl PurgeSchedule {
if let Err(err) = result {
tracing::warn!(
"Purge {} task failed for store {:?}: {:?}",
match &self.store {
PurgeStore::Bitmaps(_) => "bitmaps",
PurgeStore::Blobs { .. } => "blobs",
PurgeStore::Lookup(_) => "expired keys",
},
self.store,
self.store_id,
err
);
@ -75,3 +81,13 @@ impl PurgeSchedule {
});
}
}
impl Display for PurgeStore {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PurgeStore::Bitmaps(_) => write!(f, "bitmaps"),
PurgeStore::Blobs { .. } => write!(f, "blobs"),
PurgeStore::Lookup(_) => write!(f, "expired keys"),
}
}
}

View file

@ -47,6 +47,10 @@ return-path = ""
no-capability-check = true
sign = ["rsa"]
[sieve.trusted.default]
directory = "%{DEFAULT_DIRECTORY}%"
store = "%{DEFAULT_STORE}%"
[sieve.trusted.limits]
redirects = 3
out-messages = 5

View file

@ -6,13 +6,29 @@
host = "__HOST__"
default_domain = "__DOMAIN__"
base_path = "__BASE_PATH__"
default_directory = "__DIRECTORY__"
default_store = "__STORE__"
[include]
files = [ "%{BASE_PATH}%/etc/common/server.toml",
"%{BASE_PATH}%/etc/common/tls.toml",
"%{BASE_PATH}%/etc/common/tracing.toml",
"%{BASE_PATH}%/etc/common/sieve.toml",
"%{BASE_PATH}%/etc/directory/imap.toml",
"%{BASE_PATH}%/etc/directory/internal.toml",
"%{BASE_PATH}%/etc/directory/ldap.toml",
"%{BASE_PATH}%/etc/directory/lmtp.toml",
"%{BASE_PATH}%/etc/directory/memory.toml",
"%{BASE_PATH}%/etc/directory/sql.toml",
"%{BASE_PATH}%/etc/store/elastic.toml",
"%{BASE_PATH}%/etc/store/filesystem.toml",
"%{BASE_PATH}%/etc/store/foundationdb.toml",
"%{BASE_PATH}%/etc/store/mysql.toml",
"%{BASE_PATH}%/etc/store/postgresql.toml",
"%{BASE_PATH}%/etc/store/redis.toml",
"%{BASE_PATH}%/etc/store/rocksdb.toml",
"%{BASE_PATH}%/etc/store/s3.toml",
"%{BASE_PATH}%/etc/store/sqlite.toml",
"%{BASE_PATH}%/etc/imap/listener.toml",
"%{BASE_PATH}%/etc/imap/settings.toml",
"%{BASE_PATH}%/etc/jmap/auth.toml",

View file

@ -6,6 +6,7 @@
type = "imap"
address = "127.0.0.1"
port = 993
disable = true
[directory."imap".pool]
max-connections = 10

View file

@ -4,7 +4,8 @@
[directory."internal"]
type = "internal"
store = "sqlite"
store = "%{DEFAULT_STORE}%"
disable = true
[directory."internal".options]
catch-all = true

View file

@ -6,6 +6,7 @@
type = "ldap"
address = "ldap://localhost:389"
base-dn = "dc=example,dc=org"
disable = true
[directory."ldap".bind]
dn = "cn=serviceuser,ou=svcaccts,dc=example,dc=org"

View file

@ -6,6 +6,7 @@
type = "lmtp"
address = "127.0.0.1"
port = 11200
disable = true
[directory."lmtp".limits]
auth-errors = 3

View file

@ -4,6 +4,7 @@
[directory."memory"]
type = "memory"
disable = true
[directory."memory".options]
catch-all = true

View file

@ -4,7 +4,8 @@
[directory."sql"]
type = "sql"
store = "sqlite"
store = "__SQL_STORE__"
disable = true
[directory."sql".options]
catch-all = true

View file

@ -3,7 +3,7 @@
#############################################
[jmap]
directory = "default"
directory = "%{DEFAULT_DIRECTORY}%"
[jmap.session.cache]
ttl = "1h"

View file

@ -3,9 +3,9 @@
#############################################
[jmap.store]
data = "rocksdb"
fts = "rocksdb"
blob = "rocksdb"
data = "%{DEFAULT_STORE}%"
fts = "__FTS_STORE__"
blob = "__BLOB_STORE__"
[jmap.encryption]
enable = true

View file

@ -21,4 +21,4 @@ bind = ["127.0.0.1:8080"]
protocol = "http"
[management]
directory = "default"
directory = "%{DEFAULT_DIRECTORY}%"

View file

@ -13,7 +13,7 @@ expire = "5d"
[queue.outbound]
#hostname = "%{HOST}%"
next-hop = [ { if = "rcpt-domain", in-list = "default/domains", then = "local" },
next-hop = [ { if = "rcpt-domain", in-list = "%{DEFAULT_DIRECTORY}%/domains", then = "local" },
{ else = false } ]
ip-strategy = "ipv4-then-ipv6"

View file

@ -37,7 +37,7 @@ mt-priority = [ { if = "authenticated-as", ne = "", then = "mixer"},
[session.auth]
mechanisms = [ { if = "listener", ne = "smtp", then = ["plain", "login"]},
{ else = [] } ]
directory = [ { if = "listener", ne = "smtp", then = "default" },
directory = [ { if = "listener", ne = "smtp", then = "%{DEFAULT_DIRECTORY}%" },
{ else = false } ]
require = [ { if = "listener", ne = "smtp", then = true},
{ else = false } ]
@ -58,12 +58,12 @@ wait = "5s"
#script = "greylist"
relay = [ { if = "authenticated-as", ne = "", then = true },
{ else = false } ]
#rewrite = [ { all-of = [ { if = "rcpt-domain", in-list = "default/domains" },
#rewrite = [ { all-of = [ { if = "rcpt-domain", in-list = "%{DEFAULT_DIRECTORY}%/domains" },
# { if = "rcpt", matches = "^([^.]+)\.([^.]+)@(.+)$"},
# ], then = "${1}+${2}@${3}" },
# { else = false } ]
max-recipients = 25
directory = "default"
directory = "%{DEFAULT_DIRECTORY}%"
[session.rcpt.errors]
total = 5

View file

@ -87,4 +87,5 @@ spam-filter = ["file://%{BASE_PATH}%/etc/spamfilter/scripts/config.sieve",
track-replies = ["file://%{BASE_PATH}%/etc/spamfilter/scripts/config.sieve",
"file://%{BASE_PATH}%/etc/spamfilter/scripts/replies_out.sieve"]
greylist = "file://%{BASE_PATH}%/etc/spamfilter/scripts/greylist.sieve"
greylist = ["file://%{BASE_PATH}%/etc/spamfilter/scripts/config.sieve",
"file://%{BASE_PATH}%/etc/spamfilter/scripts/greylist.sieve"]

View file

@ -1,9 +1,9 @@
set "triplet" "g:${env.remote_ip}.${envelope.from}.${envelope.to}";
if eval "!key_exists(SPAMDB, triplet)" {
if eval "!key_exists(SPAM_DB, triplet)" {
# Greylist sender for 30 days
eval "key_set(SPAMDB, triplet, '', 2592000)";
eval "key_set(SPAM_DB, triplet, '', 2592000)";
reject "422 4.2.2 Greylisted, please try again in a few moments.";
stop;
}

View file

@ -8,6 +8,7 @@ url = "https://localhost:9200"
user = "elastic"
password = "myelasticpassword"
#cloud-id = "my-cloud-id"
disable = true
[store."elastic".tls]
allow-invalid-certs = true

View file

@ -6,6 +6,7 @@
type = "fs"
path = "%{BASE_PATH}%/data/blobs"
depth = 2
disable = true
[store."fs".purge]
frequency = "0 3 *"

View file

@ -5,6 +5,7 @@
[store."foundationdb"]
type = "foundationdb"
#path = "/etc/foundationdb/fdb.cluster"
disable = true
#[store."foundationdb".transaction]
#timeout = "5s"

View file

@ -9,6 +9,7 @@ port = 3307
database = "stalwart"
user = "root"
password = "password"
disable = true
[store."mysql".timeout]
wait = "15s"

View file

@ -9,6 +9,7 @@ port = 5432
database = "stalwart"
user = "postgres"
password = "mysecretpassword"
disable = true
[store."postgresql".timeout]
connect = "15s"

View file

@ -13,3 +13,4 @@ timeout = "10s"
#max-retry-wait = "1s"
#min-retry-wait = "500ms"
#read-from-replicas = false
disable = true

View file

@ -5,6 +5,7 @@
[store."rocksdb"]
type = "rocksdb"
path = "%{BASE_PATH}%/data"
disable = true
[store."rocksdb".settings]
min-blob-size = 16834

View file

@ -12,6 +12,7 @@ secret-key = "minioadmin"
#security-token = ""
#profile = ""
timeout = "30s"
disable = true
[store."s3".purge]
frequency = "0 3 *"

View file

@ -5,6 +5,7 @@
[store."sqlite"]
type = "sqlite"
path = "%{BASE_PATH}%/data/index.sqlite3"
disable = true
#[store."sqlite".pool]
#max-connections = 10

View file

@ -3,6 +3,15 @@
BASE_DIR="/tmp/stalwart-test"
DOMAIN="example.org"
# Stores
STORE="rocksdb"
FTS_STORE="rocksdb"
BLOB_STORE="rocksdb"
# Directories
DIRECTORY="internal"
SQL_STORE="sqlite"
# Delete previous tests
rm -rf $BASE_DIR
@ -16,37 +25,46 @@ cp -r resources/config $BASE_DIR/etc
cp -r tests/resources/tls_cert.pem $BASE_DIR/etc
cp -r tests/resources/tls_privatekey.pem $BASE_DIR/etc
# Replace stores and directories
sed -i '' -e "s|__SQL_STORE__|$SQL_STORE|g" "$BASE_DIR/etc/directory/sql.toml"
sed -i '' -e 's/disable = true//g' "$BASE_DIR/etc/directory/$DIRECTORY.toml"
sed -i '' -e 's/disable = true//g' "$BASE_DIR/etc/store/$STORE.toml"
sed -i '' -e 's/disable = true//g' "$BASE_DIR/etc/store/$FTS_STORE.toml"
sed -i '' -e 's/disable = true//g' "$BASE_DIR/etc/store/$BLOB_STORE.toml"
sed -i '' -e "s/__FTS_STORE__/$FTS_STORE/g" \
-e "s/__BLOB_STORE__/$BLOB_STORE/g" "$BASE_DIR/etc/jmap/store.toml"
# Replace settings
sed -i '' -e "s/__DOMAIN__/$DOMAIN/g" -e "s/__HOST__/mail.$DOMAIN/g" -e 's/sql.toml/memory.toml/g' -e "s|__BASE_PATH__|$BASE_DIR|g" "$BASE_DIR/etc/config.toml"
sed -i '' -e "s|__CERT_PATH__|$BASE_DIR/etc/tls_cert.pem|g" -e "s|__PK_PATH__|$BASE_DIR/etc/tls_privatekey.pem|g" "$BASE_DIR/etc/common/tls.toml"
sed -i '' -e 's/method = "log"/method = "stdout"/g' -e 's/level = "info"/level = "trace"/g' "$BASE_DIR/etc/common/tracing.toml"
sed -i '' -e 's/user = "stalwart-mail"//g' -e 's/group = "stalwart-mail"//g' "$BASE_DIR/etc/common/server.toml"
sed -i '' -e "s/__STORE__/$STORE/g" \
-e "s/__DIRECTORY__/$DIRECTORY/g" \
-e "s/__DOMAIN__/$DOMAIN/g" \
-e "s/__HOST__/mail.$DOMAIN/g" \
-e "s|__BASE_PATH__|$BASE_DIR|g" "$BASE_DIR/etc/config.toml"
sed -i '' -e "s|__CERT_PATH__|$BASE_DIR/etc/tls_cert.pem|g" \
-e "s|__PK_PATH__|$BASE_DIR/etc/tls_privatekey.pem|g" "$BASE_DIR/etc/common/tls.toml"
sed -i '' -e 's/method = "log"/method = "stdout"/g' \
-e 's/level = "info"/level = "trace"/g' "$BASE_DIR/etc/common/tracing.toml"
sed -i '' -e 's/allow-plain-text = false/allow-plain-text = true/g' "$BASE_DIR/etc/imap/settings.toml"
sed -i '' -e 's/user = "stalwart-mail"//g' \
-e 's/group = "stalwart-mail"//g' "$BASE_DIR/etc/common/server.toml"
# Generate DKIM key
mkdir -p $BASE_DIR/etc/dkim
openssl genpkey -algorithm RSA -out $BASE_DIR/etc/dkim/$DOMAIN.key
# Create antispam tables
sqlite3 $BASE_DIR/data/spamfilter.sqlite3 <<EOF
CREATE TABLE IF NOT EXISTS bayes_tokens (
h1 INTEGER NOT NULL,
h2 INTEGER NOT NULL,
ws INTEGER,
wh INTEGER,
PRIMARY KEY (h1, h2)
);
CREATE TABLE IF NOT EXISTS seen_ids (
id STRING NOT NULL PRIMARY KEY,
ttl DATETIME NOT NULL
);
CREATE TABLE IF NOT EXISTS reputation (
token STRING NOT NULL PRIMARY KEY,
score FLOAT NOT NULL DEFAULT '0',
count INT(11) NOT NULL DEFAULT '0',
ttl DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
EOF
#cargo run --manifest-path=crates/main/Cargo.toml -- --config=/tmp/stalwart-test/etc/config.toml
: '
SET_ADMIN_USER="admin" SET_ADMIN_PASS="secret" cargo run --manifest-path=crates/main/Cargo.toml -- --config=/tmp/stalwart-test/etc/config.toml
cargo run --manifest-path=crates/main/Cargo.toml -- --config=/tmp/stalwart-test/etc/config.toml
cargo run --manifest-path=crates/cli/Cargo.toml -- -u https://127.0.0.1:8080 -c admin:secret domain create example.org
cargo run --manifest-path=crates/cli/Cargo.toml -- -u https://127.0.0.1:8080 -c admin:secret account create john 12345 -d "John Doe" -a john@example.org -a john.doe@example.org
cargo run --manifest-path=crates/cli/Cargo.toml -- -u https://127.0.0.1:8080 -c admin:secret account create jane abcde -d "Jane Doe" -a jane@example.org
cargo run --manifest-path=crates/cli/Cargo.toml -- -u https://127.0.0.1:8080 -c admin:secret account create bill xyz12 -d "Bill Foobar" -a bill@example.org
cargo run --manifest-path=crates/cli/Cargo.toml -- -u https://127.0.0.1:8080 -c admin:secret group create sales -d "Sales Department"
cargo run --manifest-path=crates/cli/Cargo.toml -- -u https://127.0.0.1:8080 -c admin:secret group create support -d "Technical Support"
cargo run --manifest-path=crates/cli/Cargo.toml -- -u https://127.0.0.1:8080 -c admin:secret account add-to-group john sales support
cargo run --manifest-path=crates/cli/Cargo.toml -- -u https://127.0.0.1:8080 -c admin:secret account remove-from-group john support
cargo run --manifest-path=crates/cli/Cargo.toml -- -u https://127.0.0.1:8080 -c admin:secret account add-email jane jane.doe@example.org
cargo run --manifest-path=crates/cli/Cargo.toml -- -u https://127.0.0.1:8080 -c admin:secret list create everyone everyone@example.org
cargo run --manifest-path=crates/cli/Cargo.toml -- -u https://127.0.0.1:8080 -c admin:secret list add-members everyone jane john bill
cargo run --manifest-path=crates/cli/Cargo.toml -- -u https://127.0.0.1:8080 -c admin:secret account list
'

View file

@ -165,6 +165,7 @@ async fn internal_directory() {
.await
.unwrap(),
Some(Principal {
id: 1,
name: "jane".to_string(),
description: Some("Jane Doe".to_string()),
emails: vec!["jane@example.org".to_string()],
@ -225,6 +226,7 @@ async fn internal_directory() {
.unwrap(),
Principal {
name: "list".to_string(),
id: 2,
typ: Type::List,
emails: vec!["list@example.org".to_string()],
member_of: vec!["john".to_string(), "jane".to_string()],
@ -420,6 +422,7 @@ async fn internal_directory() {
.unwrap(),
Principal {
name: "list".to_string(),
id: 2,
typ: Type::List,
emails: vec!["list@example.org".to_string()],
member_of: vec!["jane".to_string()],
@ -445,6 +448,7 @@ async fn internal_directory() {
.unwrap(),
Principal {
name: "list".to_string(),
id: 2,
typ: Type::List,
emails: vec!["list@example.org".to_string()],
member_of: vec!["jane".to_string(), "john.doe".to_string()],

View file

@ -294,7 +294,7 @@ impl DirectoryTest {
let stores = config.parse_stores().await.unwrap();
DirectoryTest {
directories: config.parse_directory(&stores, id_store).unwrap(),
directories: config.parse_directory(&stores, id_store).await.unwrap(),
stores,
temp_dir,
}

View file

@ -275,7 +275,10 @@ async fn init_imap_tests(store_id: &str, delete_if_exists: bool) -> IMAPTest {
.unwrap();
let servers = config.parse_servers().unwrap();
let stores = config.parse_stores().await.failed("Invalid configuration");
let directory = config.parse_directory(&stores, store_id.into()).unwrap();
let directory = config
.parse_directory(&stores, store_id.into())
.await
.unwrap();
// Start JMAP and SMTP servers
servers.bind(&config);

View file

@ -373,7 +373,10 @@ async fn init_jmap_tests(store_id: &str, delete_if_exists: bool) -> JMAPTest {
.unwrap();
let servers = config.parse_servers().unwrap();
let stores = config.parse_stores().await.failed("Invalid configuration");
let directory = config.parse_directory(&stores, store_id.into()).unwrap();
let directory = config
.parse_directory(&stores, store_id.into())
.await
.unwrap();
// Start JMAP and SMTP servers
servers.bind(&config);

View file

@ -714,7 +714,7 @@ impl KeyLookup for TestEnvelope {
match key {
EnvelopeKey::Priority => self.priority as i32,
EnvelopeKey::Listener => self.listener_id as i32,
_ => todo!(),
_ => unreachable!(),
}
}

View file

@ -42,6 +42,10 @@ cpu = 500000
nested-includes = 5
duplicate-expiry = "7d"
[sieve.trusted.default]
#directory = "%{DEFAULT_DIRECTORY}%"
store = "spamdb"
[store."spamdb"]
type = "sqlite"
path = "%PATH%/test_antispam.db"

View file

@ -63,6 +63,7 @@ async fn auth() {
ctx.directory = Config::new(DIRECTORY)
.unwrap()
.parse_directory(&Stores::default(), None)
.await
.unwrap();
let config = &mut core.session.config.auth;

View file

@ -81,6 +81,7 @@ async fn data() {
let directory = Config::new(DIRECTORY)
.unwrap()
.parse_directory(&Stores::default(), None)
.await
.unwrap();
let config = &mut core.session.config.rcpt;
config.directory = IfBlock::new(Some(MaybeDynValue::Static(

View file

@ -136,6 +136,7 @@ async fn dmarc() {
let directory = Config::new(DIRECTORY)
.unwrap()
.parse_directory(&Stores::default(), None)
.await
.unwrap();
let config = &mut core.session.config.rcpt;
config.directory = IfBlock::new(Some(MaybeDynValue::Static(

View file

@ -75,6 +75,7 @@ async fn rcpt() {
let directory = Config::new(DIRECTORY)
.unwrap()
.parse_directory(&Stores::default(), None)
.await
.unwrap();
let config = &mut core.session.config.rcpt;
config.directory = IfBlock::new(Some(MaybeDynValue::Static(

View file

@ -104,7 +104,10 @@ async fn address_rewrite() {
let mut core = SMTP::test();
let mut ctx = ConfigContext::new(&[]).parse_signatures();
let settings = Config::new(CONFIG).unwrap();
ctx.directory = settings.parse_directory(&Stores::default(), None).unwrap();
ctx.directory = settings
.parse_directory(&Stores::default(), None)
.await
.unwrap();
core.sieve = settings.parse_sieve(&mut ctx).unwrap();
let config = &mut core.session.config;
config.mail.script = settings

View file

@ -135,7 +135,7 @@ async fn sieve_scripts() {
)
.unwrap();
ctx.stores = config.parse_stores().await.unwrap();
ctx.directory = config.parse_directory(&ctx.stores, None).unwrap();
ctx.directory = config.parse_directory(&ctx.stores, None).await.unwrap();
let pipes = config.parse_pipes(&ctx, &[EnvelopeKey::RemoteIp]).unwrap();
core.sieve = config.parse_sieve(&mut ctx).unwrap();
let config = &mut core.session.config;

View file

@ -155,6 +155,7 @@ async fn sign_and_seal() {
let directory = Config::new(DIRECTORY)
.unwrap()
.parse_directory(&Stores::default(), None)
.await
.unwrap();
let config = &mut core.session.config.rcpt;
config.directory = IfBlock::new(Some(MaybeDynValue::Static(

View file

@ -69,6 +69,7 @@ async fn vrfy_expn() {
let directory = Config::new(DIRECTORY)
.unwrap()
.parse_directory(&Stores::default(), None)
.await
.unwrap();
let config = &mut core.session.config.rcpt;
config.directory = IfBlock::new(Some(MaybeDynValue::Static(

View file

@ -87,7 +87,7 @@ async fn lookup_sql() {
let mut ctx = ConfigContext::new(&[]);
let config = Config::new(&config_file).unwrap();
ctx.stores = config.parse_stores().await.unwrap();
ctx.directory = config.parse_directory(&ctx.stores, None).unwrap();
ctx.directory = config.parse_directory(&ctx.stores, None).await.unwrap();
// Obtain directory handle
let handle = DirectoryStore {

View file

@ -51,11 +51,9 @@ const DIRECTORY: &str = r#"
[directory."local"]
type = "memory"
[directory."local".options]
superuser-group = "superusers"
[[directory."local".principals]]
name = "admin"
type = "admin"
description = "Superuser"
secret = "secret"
member-of = ["superusers"]
@ -99,6 +97,7 @@ async fn manage_queue() {
let directory = Config::new(DIRECTORY)
.unwrap()
.parse_directory(&Stores::default(), None)
.await
.unwrap();
core.queue.config.management_lookup = directory.directories.get("local").unwrap().clone();
core.session.config.rcpt.relay = IfBlock::new(true);

View file

@ -54,11 +54,9 @@ const DIRECTORY: &str = r#"
[directory."local"]
type = "memory"
[directory."local".options]
superuser-group = "superusers"
[[directory."local".principals]]
name = "admin"
type = "admin"
description = "Superuser"
secret = "secret"
member-of = ["superusers"]
@ -86,6 +84,7 @@ async fn manage_reports() {
let directory = Config::new(DIRECTORY)
.unwrap()
.parse_directory(&Stores::default(), None)
.await
.unwrap();
core.queue.config.management_lookup = directory.directories.get("local").unwrap().clone();
let (report_tx, report_rx) = mpsc::channel(1024);

View file

@ -440,6 +440,8 @@ impl TestConfig for SieveCore {
sign: vec![],
directories: Default::default(),
lookup_stores: Default::default(),
default_lookup_store: None,
default_directory: None,
}
}
}