diff --git a/crates/directory/src/config.rs b/crates/directory/src/config.rs index 7881ead4..2d150f86 100644 --- a/crates/directory/src/config.rs +++ b/crates/directory/src/config.rs @@ -11,7 +11,7 @@ use ahash::{AHashMap, AHashSet}; use crate::{ imap::ImapDirectory, ldap::LdapDirectory, memory::MemoryDirectory, smtp::SmtpDirectory, - sql::SqlDirectory, DirectoryConfig, Lookup, + sql::SqlDirectory, DirectoryConfig, DirectoryOptions, Lookup, }; pub trait ConfigDirectory { @@ -114,6 +114,16 @@ impl ConfigDirectory for Config { } } +impl DirectoryOptions { + pub fn from_config(config: &Config, key: impl AsKey) -> utils::config::Result { + let key = key.as_key(); + Ok(DirectoryOptions { + catch_all: config.property_or_static((&key, "options.catch-all"), "false")?, + subaddressing: config.property_or_static((&key, "options.subaddressing"), "true")?, + }) + } +} + pub(crate) fn build_pool( config: &Config, prefix: &str, diff --git a/crates/directory/src/ldap/config.rs b/crates/directory/src/ldap/config.rs index cf77fbf1..07da01be 100644 --- a/crates/directory/src/ldap/config.rs +++ b/crates/directory/src/ldap/config.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use ldap3::LdapConnSettings; use utils::config::{utils::AsKey, Config}; -use crate::{cache::CachedDirectory, config::build_pool, Directory}; +use crate::{cache::CachedDirectory, config::build_pool, Directory, DirectoryOptions}; use super::{Bind, LdapConnectionManager, LdapDirectory, LdapFilter, LdapMappings}; @@ -105,6 +105,7 @@ impl LdapDirectory { LdapDirectory { mappings, pool: build_pool(config, &prefix, manager)?, + opt: DirectoryOptions::from_config(config, prefix.as_str())?, }, ) } diff --git a/crates/directory/src/ldap/lookup.rs b/crates/directory/src/ldap/lookup.rs index e37a3514..998cd244 100644 --- a/crates/directory/src/ldap/lookup.rs +++ b/crates/directory/src/ldap/lookup.rs @@ -1,7 +1,7 @@ -use ldap3::{Scope, SearchEntry}; +use ldap3::{ResultEntry, Scope, SearchEntry}; use mail_send::Credentials; -use crate::{Directory, Principal, Type}; +use crate::{to_catch_all_address, unwrap_subaddress, Directory, Principal, Type}; use super::{LdapDirectory, LdapMappings}; @@ -109,53 +109,86 @@ impl Directory for LdapDirectory { Ok(emails) } - async fn ids_by_email(&self, email: &str) -> crate::Result> { - let (rs, _res) = self + async fn ids_by_email(&self, address: &str) -> crate::Result> { + let ids = self .pool .get() .await? .search( &self.mappings.base_dn, Scope::Subtree, - &self.mappings.filter_email.build(email), + &self + .mappings + .filter_email + .build(unwrap_subaddress(address, self.opt.subaddressing).as_ref()), &self.mappings.attr_id, ) .await? - .success()?; + .success() + .map(|(rs, _res)| self.extract_ids(rs))?; - let mut ids = Vec::new(); - for entry in rs { - let entry = SearchEntry::construct(entry); - 'outer: for attr in &self.mappings.attr_id { - if let Some(values) = entry.attrs.get(attr) { - for id in values { - if let Ok(id) = id.parse() { - ids.push(id); - break 'outer; - } - } - } - } + if ids.is_empty() && self.opt.catch_all { + self.pool + .get() + .await? + .search( + &self.mappings.base_dn, + Scope::Subtree, + &self + .mappings + .filter_email + .build(&to_catch_all_address(address)), + &self.mappings.attr_id, + ) + .await? + .success() + .map(|(rs, _res)| self.extract_ids(rs)) + .map_err(|e| e.into()) + } else { + Ok(ids) } - - Ok(ids) } async fn rcpt(&self, address: &str) -> crate::Result { - self.pool + match self + .pool .get() .await? .streaming_search( &self.mappings.base_dn, Scope::Subtree, - &self.mappings.filter_email.build(address), + &self + .mappings + .filter_email + .build(unwrap_subaddress(address, self.opt.subaddressing).as_ref()), &self.mappings.attr_email_address, ) .await? .next() .await - .map(|entry| entry.is_some()) - .map_err(|e| e.into()) + { + Ok(Some(_)) => Ok(true), + Ok(None) if self.opt.catch_all => self + .pool + .get() + .await? + .streaming_search( + &self.mappings.base_dn, + Scope::Subtree, + &self + .mappings + .filter_email + .build(&to_catch_all_address(address)), + &self.mappings.attr_email_address, + ) + .await? + .next() + .await + .map(|entry| entry.is_some()) + .map_err(|e| e.into()), + Ok(None) => Ok(false), + Err(e) => Err(e.into()), + } } async fn vrfy(&self, address: &str) -> crate::Result> { @@ -166,7 +199,10 @@ impl Directory for LdapDirectory { .streaming_search( &self.mappings.base_dn, Scope::Subtree, - &self.mappings.filter_verify.build(address), + &self + .mappings + .filter_verify + .build(unwrap_subaddress(address, self.opt.subaddressing).as_ref()), &self.mappings.attr_email_address, ) .await?; @@ -196,7 +232,10 @@ impl Directory for LdapDirectory { .streaming_search( &self.mappings.base_dn, Scope::Subtree, - &self.mappings.filter_expand.build(address), + &self + .mappings + .filter_expand + .build(unwrap_subaddress(address, self.opt.subaddressing).as_ref()), &self.mappings.attr_email_address, ) .await?; @@ -289,6 +328,24 @@ impl LdapDirectory { .entry_to_principal(SearchEntry::construct(entry)) })) } + + fn extract_ids(&self, rs: Vec) -> Vec { + let mut ids = Vec::with_capacity(rs.len()); + for entry in rs { + let entry = SearchEntry::construct(entry); + 'outer: for attr in &self.mappings.attr_id { + if let Some(values) = entry.attrs.get(attr) { + for id in values { + if let Ok(id) = id.parse() { + ids.push(id); + break 'outer; + } + } + } + } + } + ids + } } impl LdapMappings { diff --git a/crates/directory/src/ldap/mod.rs b/crates/directory/src/ldap/mod.rs index 873e286e..0a09ed8d 100644 --- a/crates/directory/src/ldap/mod.rs +++ b/crates/directory/src/ldap/mod.rs @@ -1,6 +1,8 @@ use bb8::Pool; use ldap3::{ldap_escape, LdapConnSettings}; +use crate::DirectoryOptions; + pub mod config; pub mod lookup; pub mod pool; @@ -8,6 +10,7 @@ pub mod pool; pub struct LdapDirectory { pool: Pool, mappings: LdapMappings, + opt: DirectoryOptions, } #[derive(Debug, Default)] diff --git a/crates/directory/src/lib.rs b/crates/directory/src/lib.rs index 41272fd9..0a821558 100644 --- a/crates/directory/src/lib.rs +++ b/crates/directory/src/lib.rs @@ -1,4 +1,4 @@ -use std::{fmt::Debug, sync::Arc}; +use std::{borrow::Cow, fmt::Debug, sync::Arc}; use ahash::{AHashMap, AHashSet}; use bb8::RunError; @@ -143,6 +143,12 @@ impl Debug for Lookup { } } +#[derive(Debug, Default)] +struct DirectoryOptions { + catch_all: bool, + subaddressing: bool, +} + #[derive(Default, Clone, Debug)] pub struct DirectoryConfig { pub directories: AHashMap>, @@ -256,3 +262,24 @@ impl DirectoryError { DirectoryError::TimedOut } } + +#[inline(always)] +fn unwrap_subaddress(address: &str, allow_subaddessing: bool) -> Cow<'_, str> { + if allow_subaddessing { + if let Some((local_part, domain_part)) = address.rsplit_once('@') { + if let Some((local_part, _)) = local_part.split_once('+') { + return format!("{}@{}", local_part, domain_part).into(); + } + } + } + + address.into() +} + +#[inline(always)] +fn to_catch_all_address(address: &str) -> String { + address + .rsplit_once('@') + .map(|(_, domain_part)| format!("@{}", domain_part)) + .unwrap_or_else(|| address.into()) +} diff --git a/crates/directory/src/memory/config.rs b/crates/directory/src/memory/config.rs index 2dea4299..25312043 100644 --- a/crates/directory/src/memory/config.rs +++ b/crates/directory/src/memory/config.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use utils::config::{utils::AsKey, Config}; -use crate::{config::ConfigDirectory, Directory, Principal, Type}; +use crate::{config::ConfigDirectory, Directory, DirectoryOptions, Principal, Type}; use super::{EmailType, MemoryDirectory}; @@ -54,19 +54,26 @@ impl MemoryDirectory { EmailType::Primary(id) }); + if let Some((_, domain)) = email.rsplit_once('@') { + directory.domains.insert(domain.to_lowercase()); + } + emails.push(if pos > 0 { - EmailType::Alias(email.to_string()) + EmailType::Alias(email.to_lowercase()) } else { - EmailType::Primary(email.to_string()) + EmailType::Primary(email.to_lowercase()) }); } for (_, email) in config.values((prefix.as_str(), "users", lookup_id, "email-list")) { directory .emails_to_ids - .entry(email.to_string()) + .entry(email.to_lowercase()) .or_default() .push(EmailType::List(id)); - emails.push(EmailType::List(email.to_string())); + if let Some((_, domain)) = email.rsplit_once('@') { + directory.domains.insert(domain.to_lowercase()); + } + emails.push(EmailType::List(email.to_lowercase())); } directory.ids_to_email.insert(id, emails); } @@ -95,7 +102,10 @@ impl MemoryDirectory { }); } - directory.domains = config.parse_lookup_list((&prefix, "lookup.domains"))?; + directory + .domains + .extend(config.parse_lookup_list((&prefix, "lookup.domains"))?); + directory.opt = DirectoryOptions::from_config(config, prefix)?; Ok(Arc::new(directory)) } diff --git a/crates/directory/src/memory/lookup.rs b/crates/directory/src/memory/lookup.rs index 63ed8e3f..8be00063 100644 --- a/crates/directory/src/memory/lookup.rs +++ b/crates/directory/src/memory/lookup.rs @@ -1,6 +1,6 @@ use mail_send::Credentials; -use crate::{Directory, DirectoryError, Principal}; +use crate::{to_catch_all_address, unwrap_subaddress, Directory, DirectoryError, Principal}; use super::{EmailType, MemoryDirectory}; @@ -66,7 +66,14 @@ impl Directory for MemoryDirectory { async fn ids_by_email(&self, address: &str) -> crate::Result> { Ok(self .emails_to_ids - .get(address) + .get(unwrap_subaddress(address, self.opt.subaddressing).as_ref()) + .or_else(|| { + if self.opt.catch_all { + self.emails_to_ids.get(&to_catch_all_address(address)) + } else { + None + } + }) .map(|ids| { ids.iter() .map(|t| match t { @@ -78,13 +85,19 @@ impl Directory for MemoryDirectory { } async fn rcpt(&self, address: &str) -> crate::Result { - Ok(self.emails_to_ids.get(address).is_some()) + Ok(self + .emails_to_ids + .contains_key(unwrap_subaddress(address, self.opt.subaddressing).as_ref()) + || (self.opt.catch_all && self.domains.contains(&to_catch_all_address(address)))) } async fn vrfy(&self, address: &str) -> crate::Result> { let mut result = Vec::new(); + let address = unwrap_subaddress(address, self.opt.subaddressing); for (key, value) in &self.emails_to_ids { - if key.contains(address) && value.iter().any(|t| matches!(t, EmailType::Primary(_))) { + if key.contains(address.as_ref()) + && value.iter().any(|t| matches!(t, EmailType::Primary(_))) + { result.push(key.clone()) } } @@ -93,8 +106,9 @@ impl Directory for MemoryDirectory { async fn expn(&self, address: &str) -> crate::Result> { let mut result = Vec::new(); + let address = unwrap_subaddress(address, self.opt.subaddressing); for (key, value) in &self.emails_to_ids { - if key == address { + if key == address.as_ref() { for item in value { if let EmailType::List(id) = item { for addr in self.ids_to_email.get(id).unwrap() { @@ -114,13 +128,6 @@ impl Directory for MemoryDirectory { } async fn is_local_domain(&self, domain: &str) -> crate::Result { - Ok(if !self.domains.contains(domain) { - let domain = format!("@{domain}"); - self.emails_to_ids - .keys() - .any(|email| email.ends_with(&domain)) - } else { - true - }) + Ok(self.domains.contains(domain)) } } diff --git a/crates/directory/src/memory/mod.rs b/crates/directory/src/memory/mod.rs index e2dcca39..15641e53 100644 --- a/crates/directory/src/memory/mod.rs +++ b/crates/directory/src/memory/mod.rs @@ -1,6 +1,6 @@ use ahash::{AHashMap, AHashSet}; -use crate::Principal; +use crate::{DirectoryOptions, Principal}; pub mod config; pub mod lookup; @@ -12,6 +12,7 @@ pub struct MemoryDirectory { emails_to_ids: AHashMap>>, ids_to_email: AHashMap>>, domains: AHashSet, + opt: DirectoryOptions, } enum EmailType { diff --git a/crates/directory/src/sql/config.rs b/crates/directory/src/sql/config.rs index fb80cfd8..9b699a39 100644 --- a/crates/directory/src/sql/config.rs +++ b/crates/directory/src/sql/config.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use sqlx::any::{install_default_drivers, AnyPoolOptions}; use utils::config::{utils::AsKey, Config}; -use crate::{cache::CachedDirectory, Directory}; +use crate::{cache::CachedDirectory, Directory, DirectoryOptions}; use super::{SqlDirectory, SqlMappings}; @@ -93,6 +93,14 @@ impl SqlDirectory { .to_string(), }; - CachedDirectory::try_from_config(config, &prefix, SqlDirectory { pool, mappings }) + CachedDirectory::try_from_config( + config, + &prefix, + SqlDirectory { + pool, + mappings, + opt: DirectoryOptions::from_config(config, prefix.as_str())?, + }, + ) } } diff --git a/crates/directory/src/sql/lookup.rs b/crates/directory/src/sql/lookup.rs index faa45410..dfbde816 100644 --- a/crates/directory/src/sql/lookup.rs +++ b/crates/directory/src/sql/lookup.rs @@ -1,7 +1,7 @@ use mail_send::Credentials; use sqlx::{any::AnyRow, Column, Row}; -use crate::{Directory, Principal, Type}; +use crate::{to_catch_all_address, unwrap_subaddress, Directory, Principal, Type}; use super::{SqlDirectory, SqlMappings}; @@ -74,26 +74,46 @@ impl Directory for SqlDirectory { } async fn ids_by_email(&self, address: &str) -> crate::Result> { - sqlx::query_scalar::<_, i64>(&self.mappings.query_recipients) - .bind(address) + match sqlx::query_scalar::<_, i64>(&self.mappings.query_recipients) + .bind(unwrap_subaddress(address, self.opt.subaddressing).as_ref()) .fetch_all(&self.pool) .await - .map(|ids| ids.into_iter().map(|id| id as u32).collect()) - .map_err(Into::into) + { + Ok(ids) if !ids.is_empty() => Ok(ids.into_iter().map(|id| id as u32).collect()), + Ok(_) if self.opt.catch_all => { + sqlx::query_scalar::<_, i64>(&self.mappings.query_recipients) + .bind(to_catch_all_address(address)) + .fetch_all(&self.pool) + .await + .map(|ids| ids.into_iter().map(|id| id as u32).collect()) + .map_err(Into::into) + } + Ok(_) => Ok(vec![]), + Err(err) => Err(err.into()), + } } async fn rcpt(&self, address: &str) -> crate::Result { - sqlx::query(&self.mappings.query_recipients) - .bind(address) + match sqlx::query(&self.mappings.query_recipients) + .bind(unwrap_subaddress(address, self.opt.subaddressing).as_ref()) .fetch_optional(&self.pool) .await - .map(|id| id.is_some()) - .map_err(Into::into) + { + Ok(Some(_)) => Ok(true), + Ok(None) if self.opt.catch_all => sqlx::query(&self.mappings.query_recipients) + .bind(to_catch_all_address(address)) + .fetch_optional(&self.pool) + .await + .map(|id| id.is_some()) + .map_err(Into::into), + Ok(None) => Ok(false), + Err(err) => Err(err.into()), + } } async fn vrfy(&self, address: &str) -> crate::Result> { sqlx::query_scalar::<_, String>(&self.mappings.query_verify) - .bind(address) + .bind(unwrap_subaddress(address, self.opt.subaddressing).as_ref()) .fetch_all(&self.pool) .await .map_err(Into::into) @@ -101,7 +121,7 @@ impl Directory for SqlDirectory { async fn expn(&self, address: &str) -> crate::Result> { sqlx::query_scalar::<_, String>(&self.mappings.query_expand) - .bind(address) + .bind(unwrap_subaddress(address, self.opt.subaddressing).as_ref()) .fetch_all(&self.pool) .await .map_err(Into::into) diff --git a/crates/directory/src/sql/mod.rs b/crates/directory/src/sql/mod.rs index 2e6f0433..1700539b 100644 --- a/crates/directory/src/sql/mod.rs +++ b/crates/directory/src/sql/mod.rs @@ -1,11 +1,14 @@ use sqlx::{Any, Pool}; +use crate::DirectoryOptions; + pub mod config; pub mod lookup; pub struct SqlDirectory { pool: Pool, mappings: SqlMappings, + opt: DirectoryOptions, } #[derive(Debug)] diff --git a/tests/resources/ldap.cfg b/tests/resources/ldap.cfg index 349022c9..ad8945ea 100644 --- a/tests/resources/ldap.cfg +++ b/tests/resources/ldap.cfg @@ -79,6 +79,15 @@ watchconfig = true diskQuota = [500000] userPassword = ["$2y$05$bvIG6Nmid91Mu9RcmmWZfO5HJIMCT8riNW0hEp8f6/FuA2/mHZFpe"] +[[users]] + name = "robert" + sn = "@catchall.org" + mail = "robert@catchall.org" + uidnumber = 7 + [[users.customattributes]] + principalName = ["Robect Foobar"] + userPassword = ["nopass"] + [[users]] name = "serviceuser" mail = "serviceuser@example.org" diff --git a/tests/src/directory/ldap.rs b/tests/src/directory/ldap.rs index 92a541da..344693ab 100644 --- a/tests/src/directory/ldap.rs +++ b/tests/src/directory/ldap.rs @@ -147,10 +147,22 @@ async fn ldap_directory() { handle.ids_by_email("jane@example.org").await.unwrap(), vec![3], ); + compare_sorted( + handle.ids_by_email("jane+alias@example.org").await.unwrap(), + vec![3], + ); compare_sorted( handle.ids_by_email("info@example.org").await.unwrap(), vec![2, 3, 4], ); + compare_sorted( + handle.ids_by_email("info+alias@example.org").await.unwrap(), + vec![2, 3, 4], + ); + compare_sorted( + handle.ids_by_email("unknown@example.org").await.unwrap(), + Vec::::new(), + ); // Domain validation assert!(handle.is_local_domain("example.org").await.unwrap()); @@ -159,6 +171,9 @@ async fn ldap_directory() { // RCPT TO assert!(handle.rcpt("jane@example.org").await.unwrap()); assert!(handle.rcpt("info@example.org").await.unwrap()); + assert!(handle.rcpt("jane+alias@example.org").await.unwrap()); + assert!(handle.rcpt("info+alias@example.org").await.unwrap()); + assert!(handle.rcpt("random_user@catchall.org").await.unwrap()); assert!(!handle.rcpt("invalid@example.org").await.unwrap()); // VRFY @@ -170,6 +185,10 @@ async fn ldap_directory() { handle.vrfy("john").await.unwrap(), vec!["john@example.org".to_string()], ); + compare_sorted( + handle.vrfy("jane+alias@example").await.unwrap(), + vec!["jane@example.org".to_string()], + ); compare_sorted(handle.vrfy("info").await.unwrap(), Vec::::new()); compare_sorted(handle.vrfy("invalid").await.unwrap(), Vec::::new()); diff --git a/tests/src/directory/mod.rs b/tests/src/directory/mod.rs index 6760f1e6..2220fc8c 100644 --- a/tests/src/directory/mod.rs +++ b/tests/src/directory/mod.rs @@ -15,6 +15,10 @@ const CONFIG: &str = r#" type = "sql" address = "sqlite::memory:" +[directory."sql".options] +catch-all = true +subaddressing = true + [directory."sql".pool] max-connections = 1 @@ -50,6 +54,10 @@ base-dn = "dc=example,dc=org" dn = "cn=serviceuser,ou=svcaccts,dc=example,dc=org" secret = "mysecret" +[directory."ldap".options] +catch-all = true +subaddressing = true + [directory."ldap".filter] login = "(&(objectClass=posixAccount)(accountStatus=active)(cn=?))" name = "(&(|(objectClass=posixAccount)(objectClass=posixGroup))(cn=?))" @@ -111,6 +119,10 @@ ttl = {positive = '10s', negative = '5s'} [directory."local"] type = "memory" +[directory."local".options] +catch-all = true +subaddressing = true + [[directory."local".users]] name = "john" description = "John Doe" diff --git a/tests/src/directory/sql.rs b/tests/src/directory/sql.rs index f172b6d4..457f516f 100644 --- a/tests/src/directory/sql.rs +++ b/tests/src/directory/sql.rs @@ -53,6 +53,11 @@ async fn sql_directory() { link_test_address(handle.as_ref(), "jane", "info@example.org", "list").await; link_test_address(handle.as_ref(), "bill", "info@example.org", "list").await; + // Add catch-all user + create_test_user(handle.as_ref(), "robert", "abcde", "Robert Foobar").await; + link_test_address(handle.as_ref(), "robert", "robert@catchall.org", "primary").await; + link_test_address(handle.as_ref(), "robert", "@catchall.org", "alias").await; + // Test authentication assert_eq!( handle @@ -177,6 +182,22 @@ async fn sql_directory() { handle.ids_by_email("info@example.org").await.unwrap(), vec![2, 3, 4] ); + assert_eq!( + handle.ids_by_email("jane+alias@example.org").await.unwrap(), + vec![3] + ); + assert_eq!( + handle.ids_by_email("info+alias@example.org").await.unwrap(), + vec![2, 3, 4] + ); + assert_eq!( + handle.ids_by_email("unknown@example.org").await.unwrap(), + Vec::::new() + ); + assert_eq!( + handle.ids_by_email("anything@catchall.org").await.unwrap(), + vec![7] + ); // Domain validation assert!(handle.is_local_domain("example.org").await.unwrap()); @@ -185,6 +206,9 @@ async fn sql_directory() { // RCPT TO assert!(handle.rcpt("jane@example.org").await.unwrap()); assert!(handle.rcpt("info@example.org").await.unwrap()); + assert!(handle.rcpt("jane+alias@example.org").await.unwrap()); + assert!(handle.rcpt("info+alias@example.org").await.unwrap()); + assert!(handle.rcpt("random_user@catchall.org").await.unwrap()); assert!(!handle.rcpt("invalid@example.org").await.unwrap()); // VRFY @@ -196,6 +220,10 @@ async fn sql_directory() { handle.vrfy("john").await.unwrap(), vec!["john@example.org".to_string()] ); + assert_eq!( + handle.vrfy("jane+alias@example").await.unwrap(), + vec!["jane@example.org".to_string()] + ); assert_eq!(handle.vrfy("info").await.unwrap(), Vec::::new()); assert_eq!(handle.vrfy("invalid").await.unwrap(), Vec::::new()); diff --git a/tests/src/jmap/mod.rs b/tests/src/jmap/mod.rs index fe2a30f1..03ddd582 100644 --- a/tests/src/jmap/mod.rs +++ b/tests/src/jmap/mod.rs @@ -228,7 +228,7 @@ pub async fn jmap_tests() { let delete = true; let mut params = init_jmap_tests(delete).await; - /*email_query::test(params.server.clone(), &mut params.client, delete).await; + email_query::test(params.server.clone(), &mut params.client, delete).await; email_get::test(params.server.clone(), &mut params.client).await; email_set::test(params.server.clone(), &mut params.client).await; email_parse::test(params.server.clone(), &mut params.client).await; @@ -249,7 +249,7 @@ pub async fn jmap_tests() { vacation_response::test(params.server.clone(), &mut params.client).await; email_submission::test(params.server.clone(), &mut params.client).await; websocket::test(params.server.clone(), &mut params.client).await; - quota::test(params.server.clone(), &mut params.client).await;*/ + quota::test(params.server.clone(), &mut params.client).await; stress_test::test(params.server.clone(), params.client).await; if delete {