mirror of
https://github.com/stalwartlabs/mail-server.git
synced 2024-09-20 07:16:18 +08:00
Internal directory + HTTP management API passing tests
This commit is contained in:
parent
ea94de6d77
commit
f7313eecaf
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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>,
|
||||
}
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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!(
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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!()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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"))]
|
||||
|
|
|
@ -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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
type = "imap"
|
||||
address = "127.0.0.1"
|
||||
port = 993
|
||||
disable = true
|
||||
|
||||
[directory."imap".pool]
|
||||
max-connections = 10
|
||||
|
|
|
@ -4,7 +4,8 @@
|
|||
|
||||
[directory."internal"]
|
||||
type = "internal"
|
||||
store = "sqlite"
|
||||
store = "%{DEFAULT_STORE}%"
|
||||
disable = true
|
||||
|
||||
[directory."internal".options]
|
||||
catch-all = true
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
type = "lmtp"
|
||||
address = "127.0.0.1"
|
||||
port = 11200
|
||||
disable = true
|
||||
|
||||
[directory."lmtp".limits]
|
||||
auth-errors = 3
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
[directory."memory"]
|
||||
type = "memory"
|
||||
disable = true
|
||||
|
||||
[directory."memory".options]
|
||||
catch-all = true
|
||||
|
|
|
@ -4,7 +4,8 @@
|
|||
|
||||
[directory."sql"]
|
||||
type = "sql"
|
||||
store = "sqlite"
|
||||
store = "__SQL_STORE__"
|
||||
disable = true
|
||||
|
||||
[directory."sql".options]
|
||||
catch-all = true
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
#############################################
|
||||
|
||||
[jmap]
|
||||
directory = "default"
|
||||
directory = "%{DEFAULT_DIRECTORY}%"
|
||||
|
||||
[jmap.session.cache]
|
||||
ttl = "1h"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -21,4 +21,4 @@ bind = ["127.0.0.1:8080"]
|
|||
protocol = "http"
|
||||
|
||||
[management]
|
||||
directory = "default"
|
||||
directory = "%{DEFAULT_DIRECTORY}%"
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
type = "fs"
|
||||
path = "%{BASE_PATH}%/data/blobs"
|
||||
depth = 2
|
||||
disable = true
|
||||
|
||||
[store."fs".purge]
|
||||
frequency = "0 3 *"
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
[store."foundationdb"]
|
||||
type = "foundationdb"
|
||||
#path = "/etc/foundationdb/fdb.cluster"
|
||||
disable = true
|
||||
|
||||
#[store."foundationdb".transaction]
|
||||
#timeout = "5s"
|
||||
|
|
|
@ -9,6 +9,7 @@ port = 3307
|
|||
database = "stalwart"
|
||||
user = "root"
|
||||
password = "password"
|
||||
disable = true
|
||||
|
||||
[store."mysql".timeout]
|
||||
wait = "15s"
|
||||
|
|
|
@ -9,6 +9,7 @@ port = 5432
|
|||
database = "stalwart"
|
||||
user = "postgres"
|
||||
password = "mysecretpassword"
|
||||
disable = true
|
||||
|
||||
[store."postgresql".timeout]
|
||||
connect = "15s"
|
||||
|
|
|
@ -13,3 +13,4 @@ timeout = "10s"
|
|||
#max-retry-wait = "1s"
|
||||
#min-retry-wait = "500ms"
|
||||
#read-from-replicas = false
|
||||
disable = true
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
[store."rocksdb"]
|
||||
type = "rocksdb"
|
||||
path = "%{BASE_PATH}%/data"
|
||||
disable = true
|
||||
|
||||
[store."rocksdb".settings]
|
||||
min-blob-size = 16834
|
||||
|
|
|
@ -12,6 +12,7 @@ secret-key = "minioadmin"
|
|||
#security-token = ""
|
||||
#profile = ""
|
||||
timeout = "30s"
|
||||
disable = true
|
||||
|
||||
[store."s3".purge]
|
||||
frequency = "0 3 *"
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
[store."sqlite"]
|
||||
type = "sqlite"
|
||||
path = "%{BASE_PATH}%/data/index.sqlite3"
|
||||
disable = true
|
||||
|
||||
#[store."sqlite".pool]
|
||||
#max-connections = 10
|
||||
|
|
|
@ -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
|
||||
'
|
||||
|
|
|
@ -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()],
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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!(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -440,6 +440,8 @@ impl TestConfig for SieveCore {
|
|||
sign: vec![],
|
||||
directories: Default::default(),
|
||||
lookup_stores: Default::default(),
|
||||
default_lookup_store: None,
|
||||
default_directory: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue