summaryrefslogtreecommitdiff
path: root/crates
diff options
context:
space:
mode:
Diffstat (limited to 'crates')
-rw-r--r--crates/cli/Cargo.toml2
-rw-r--r--crates/directory/Cargo.toml1
-rw-r--r--crates/directory/src/config.rs36
-rw-r--r--crates/directory/src/ldap/lookup.rs59
-rw-r--r--crates/directory/src/lib.rs77
-rw-r--r--crates/directory/src/memory/lookup.rs27
-rw-r--r--crates/directory/src/sql/lookup.rs59
-rw-r--r--crates/imap/Cargo.toml2
-rw-r--r--crates/install/Cargo.toml2
-rw-r--r--crates/install/src/main.rs2
-rw-r--r--crates/jmap/Cargo.toml2
-rw-r--r--crates/jmap/src/api/config.rs3
-rw-r--r--crates/jmap/src/sieve/ingest.rs7
-rw-r--r--crates/main/Cargo.toml2
-rw-r--r--crates/smtp/Cargo.toml2
-rw-r--r--crates/smtp/src/config/auth.rs61
-rw-r--r--crates/smtp/src/config/if_block.rs111
-rw-r--r--crates/smtp/src/config/mod.rs27
-rw-r--r--crates/smtp/src/config/queue.rs4
-rw-r--r--crates/smtp/src/config/report.rs6
-rw-r--r--crates/smtp/src/config/session.rs12
-rw-r--r--crates/smtp/src/core/if_block.rs215
-rw-r--r--crates/smtp/src/core/mod.rs4
-rw-r--r--crates/smtp/src/core/params.rs22
-rw-r--r--crates/smtp/src/core/scripts.rs148
-rw-r--r--crates/smtp/src/inbound/data.rs22
-rw-r--r--crates/smtp/src/inbound/mail.rs48
-rw-r--r--crates/smtp/src/inbound/milter/message.rs2
-rw-r--r--crates/smtp/src/inbound/rcpt.rs121
-rw-r--r--crates/smtp/src/inbound/vrfy.rs22
-rw-r--r--crates/smtp/src/reporting/mod.rs8
-rw-r--r--crates/utils/src/config/dynvalue.rs148
-rw-r--r--crates/utils/src/config/mod.rs8
-rw-r--r--crates/utils/src/config/utils.rs17
34 files changed, 1073 insertions, 216 deletions
diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml
index 09e6274f..7fb7decf 100644
--- a/crates/cli/Cargo.toml
+++ b/crates/cli/Cargo.toml
@@ -5,7 +5,7 @@ authors = ["Stalwart Labs Ltd. <hello@stalw.art>"]
license = "AGPL-3.0-only"
repository = "https://github.com/stalwartlabs/cli"
homepage = "https://github.com/stalwartlabs/cli"
-version = "0.3.1"
+version = "0.3.2"
edition = "2021"
readme = "README.md"
resolver = "2"
diff --git a/crates/directory/Cargo.toml b/crates/directory/Cargo.toml
index 16b7336a..8db1ba83 100644
--- a/crates/directory/Cargo.toml
+++ b/crates/directory/Cargo.toml
@@ -30,6 +30,7 @@ sha1 = "0.10.5"
sha2 = "0.10.6"
md5 = "0.7.0"
futures = "0.3"
+regex = "1.7.0"
[dev-dependencies]
tokio = { version = "1.23", features = ["full"] }
diff --git a/crates/directory/src/config.rs b/crates/directory/src/config.rs
index c6833b6a..1d2885fa 100644
--- a/crates/directory/src/config.rs
+++ b/crates/directory/src/config.rs
@@ -22,6 +22,7 @@
*/
use bb8::{ManageConnection, Pool};
+use regex::Regex;
use std::{
fs::File,
io::{BufRead, BufReader},
@@ -34,7 +35,7 @@ use ahash::{AHashMap, AHashSet};
use crate::{
imap::ImapDirectory, ldap::LdapDirectory, memory::MemoryDirectory, smtp::SmtpDirectory,
- sql::SqlDirectory, DirectoryConfig, DirectoryOptions, Lookup,
+ sql::SqlDirectory, AddressMapping, DirectoryConfig, DirectoryOptions, Lookup,
};
pub trait ConfigDirectory {
@@ -141,8 +142,8 @@ impl DirectoryOptions {
pub fn from_config(config: &Config, key: impl AsKey) -> utils::config::Result<Self> {
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")?,
+ catch_all: AddressMapping::from_config(config, (&key, "options.catch-all"))?,
+ subaddressing: AddressMapping::from_config(config, (&key, "options.subaddressing"))?,
superuser_group: config
.value("options.superuser-group")
.unwrap_or("superusers")
@@ -151,6 +152,35 @@ impl DirectoryOptions {
}
}
+impl AddressMapping {
+ pub fn from_config(config: &Config, key: impl AsKey) -> utils::config::Result<Self> {
+ let key = key.as_key();
+ if let Some(value) = config.value(key.as_str()) {
+ match value {
+ "true" => Ok(AddressMapping::Enable),
+ "false" => Ok(AddressMapping::Disable),
+ _ => Err(format!(
+ "Invalid value for address mapping {key:?}: {value:?}",
+ )),
+ }
+ } else if let Some(regex) = config.value((key.as_str(), "map")) {
+ Ok(AddressMapping::Custom {
+ regex: Regex::new(regex).map_err(|err| {
+ format!(
+ "Failed to compile regular expression {:?} for key {:?}: {}.",
+ regex,
+ (&key, "map").as_key(),
+ err
+ )
+ })?,
+ mapping: config.property_require((key.as_str(), "to"))?,
+ })
+ } else {
+ Ok(AddressMapping::Disable)
+ }
+ }
+}
+
pub(crate) fn build_pool<M: ManageConnection>(
config: &Config,
prefix: &str,
diff --git a/crates/directory/src/ldap/lookup.rs b/crates/directory/src/ldap/lookup.rs
index e32b7a6b..75b7df8a 100644
--- a/crates/directory/src/ldap/lookup.rs
+++ b/crates/directory/src/ldap/lookup.rs
@@ -24,7 +24,7 @@
use ldap3::{ResultEntry, Scope, SearchEntry};
use mail_send::Credentials;
-use crate::{to_catch_all_address, unwrap_subaddress, Directory, Principal, Type};
+use crate::{Directory, Principal, Type};
use super::{LdapDirectory, LdapMappings};
@@ -101,24 +101,23 @@ impl Directory for LdapDirectory {
&self
.mappings
.filter_email
- .build(unwrap_subaddress(address, self.opt.subaddressing).as_ref()),
+ .build(self.opt.subaddressing.to_subaddress(address).as_ref()),
&self.mappings.attr_name,
)
.await?
.success()
.map(|(rs, _res)| self.extract_names(rs))?;
- if names.is_empty() && self.opt.catch_all {
+ if !names.is_empty() {
+ Ok(names)
+ } else if let Some(address) = self.opt.catch_all.to_catch_all(address) {
self.pool
.get()
.await?
.search(
&self.mappings.base_dn,
Scope::Subtree,
- &self
- .mappings
- .filter_email
- .build(&to_catch_all_address(address)),
+ &self.mappings.filter_email.build(address.as_ref()),
&self.mappings.attr_name,
)
.await?
@@ -141,7 +140,7 @@ impl Directory for LdapDirectory {
&self
.mappings
.filter_email
- .build(unwrap_subaddress(address, self.opt.subaddressing).as_ref()),
+ .build(self.opt.subaddressing.to_subaddress(address).as_ref()),
&self.mappings.attr_email_address,
)
.await?
@@ -149,25 +148,27 @@ impl Directory for LdapDirectory {
.await
{
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),
+ Ok(None) => {
+ if let Some(address) = self.opt.catch_all.to_catch_all(address) {
+ self.pool
+ .get()
+ .await?
+ .streaming_search(
+ &self.mappings.base_dn,
+ Scope::Subtree,
+ &self.mappings.filter_email.build(address.as_ref()),
+ &self.mappings.attr_email_address,
+ )
+ .await?
+ .next()
+ .await
+ .map(|entry| entry.is_some())
+ .map_err(|e| e.into())
+ } else {
+ Ok(false)
+ }
+ }
+
Err(e) => Err(e.into()),
}
}
@@ -183,7 +184,7 @@ impl Directory for LdapDirectory {
&self
.mappings
.filter_verify
- .build(unwrap_subaddress(address, self.opt.subaddressing).as_ref()),
+ .build(self.opt.subaddressing.to_subaddress(address).as_ref()),
&self.mappings.attr_email_address,
)
.await?;
@@ -216,7 +217,7 @@ impl Directory for LdapDirectory {
&self
.mappings
.filter_expand
- .build(unwrap_subaddress(address, self.opt.subaddressing).as_ref()),
+ .build(self.opt.subaddressing.to_subaddress(address).as_ref()),
&self.mappings.attr_email_address,
)
.await?;
diff --git a/crates/directory/src/lib.rs b/crates/directory/src/lib.rs
index eaff7de7..5a72405b 100644
--- a/crates/directory/src/lib.rs
+++ b/crates/directory/src/lib.rs
@@ -28,6 +28,7 @@ use bb8::RunError;
use imap::ImapError;
use ldap3::LdapError;
use mail_send::Credentials;
+use utils::config::DynValue;
pub mod cache;
pub mod config;
@@ -170,11 +171,22 @@ impl Type {
#[derive(Debug, Default)]
struct DirectoryOptions {
- catch_all: bool,
- subaddressing: bool,
+ catch_all: AddressMapping,
+ subaddressing: AddressMapping,
superuser_group: String,
}
+#[derive(Debug, Default)]
+pub enum AddressMapping {
+ Enable,
+ Custom {
+ regex: regex::Regex,
+ mapping: DynValue,
+ },
+ #[default]
+ Disable,
+}
+
#[derive(Default, Clone, Debug)]
pub struct DirectoryConfig {
pub directories: AHashMap<String, Arc<dyn Directory>>,
@@ -289,23 +301,54 @@ impl DirectoryError {
}
}
-#[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();
+impl AddressMapping {
+ pub fn to_subaddress<'x, 'y: 'x>(&'x self, address: &'y str) -> Cow<'x, str> {
+ match self {
+ AddressMapping::Enable => {
+ 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();
+ }
+ }
+ }
+ AddressMapping::Custom { regex, mapping } => {
+ let mut regex_capture = Vec::new();
+ for captures in regex.captures_iter(address) {
+ for capture in captures.iter() {
+ regex_capture.push(capture.map_or("", |m| m.as_str()).to_string());
+ }
+ }
+
+ if !regex_capture.is_empty() {
+ return mapping.apply(regex_capture);
+ }
}
+ AddressMapping::Disable => (),
}
- }
- address.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())
+ pub fn to_catch_all<'x, 'y: 'x>(&'x self, address: &'y str) -> Option<Cow<'x, str>> {
+ match self {
+ AddressMapping::Enable => address
+ .rsplit_once('@')
+ .map(|(_, domain_part)| format!("@{}", domain_part))
+ .map(Cow::Owned),
+ AddressMapping::Custom { regex, mapping } => {
+ let mut regex_capture = Vec::new();
+ for captures in regex.captures_iter(address) {
+ for capture in captures.iter() {
+ regex_capture.push(capture.map_or("", |m| m.as_str()).to_string());
+ }
+ }
+ if !regex_capture.is_empty() {
+ Some(mapping.apply(regex_capture))
+ } else {
+ None
+ }
+ }
+ AddressMapping::Disable => None,
+ }
+ }
}
diff --git a/crates/directory/src/memory/lookup.rs b/crates/directory/src/memory/lookup.rs
index 252e2ddf..82e46a61 100644
--- a/crates/directory/src/memory/lookup.rs
+++ b/crates/directory/src/memory/lookup.rs
@@ -23,7 +23,7 @@
use mail_send::Credentials;
-use crate::{to_catch_all_address, unwrap_subaddress, Directory, DirectoryError, Principal};
+use crate::{Directory, DirectoryError, Principal};
use super::{EmailType, MemoryDirectory};
@@ -67,13 +67,12 @@ impl Directory for MemoryDirectory {
async fn names_by_email(&self, address: &str) -> crate::Result<Vec<String>> {
Ok(self
.emails_to_names
- .get(unwrap_subaddress(address, self.opt.subaddressing).as_ref())
+ .get(self.opt.subaddressing.to_subaddress(address).as_ref())
.or_else(|| {
- if self.opt.catch_all {
- self.emails_to_names.get(&to_catch_all_address(address))
- } else {
- None
- }
+ self.opt
+ .catch_all
+ .to_catch_all(address)
+ .and_then(|address| self.emails_to_names.get(address.as_ref()))
})
.map(|names| {
names
@@ -91,13 +90,19 @@ impl Directory for MemoryDirectory {
async fn rcpt(&self, address: &str) -> crate::Result<bool> {
Ok(self
.emails_to_names
- .contains_key(unwrap_subaddress(address, self.opt.subaddressing).as_ref())
- || (self.opt.catch_all && self.domains.contains(&to_catch_all_address(address))))
+ .contains_key(self.opt.subaddressing.to_subaddress(address).as_ref())
+ || self
+ .opt
+ .catch_all
+ .to_catch_all(address)
+ .map_or(false, |address| {
+ self.emails_to_names.contains_key(address.as_ref())
+ }))
}
async fn vrfy(&self, address: &str) -> crate::Result<Vec<String>> {
let mut result = Vec::new();
- let address = unwrap_subaddress(address, self.opt.subaddressing);
+ let address = self.opt.subaddressing.to_subaddress(address);
for (key, value) in &self.emails_to_names {
if key.contains(address.as_ref())
&& value.iter().any(|t| matches!(t, EmailType::Primary(_)))
@@ -110,7 +115,7 @@ impl Directory for MemoryDirectory {
async fn expn(&self, address: &str) -> crate::Result<Vec<String>> {
let mut result = Vec::new();
- let address = unwrap_subaddress(address, self.opt.subaddressing);
+ let address = self.opt.subaddressing.to_subaddress(address);
for (key, value) in &self.emails_to_names {
if key == address.as_ref() {
for item in value {
diff --git a/crates/directory/src/sql/lookup.rs b/crates/directory/src/sql/lookup.rs
index 20739db3..701498b5 100644
--- a/crates/directory/src/sql/lookup.rs
+++ b/crates/directory/src/sql/lookup.rs
@@ -25,7 +25,7 @@ use futures::TryStreamExt;
use mail_send::Credentials;
use sqlx::{any::AnyRow, Column, Row};
-use crate::{to_catch_all_address, unwrap_subaddress, Directory, Principal, Type};
+use crate::{Directory, Principal, Type};
use super::{SqlDirectory, SqlMappings};
@@ -91,49 +91,54 @@ impl Directory for SqlDirectory {
}
async fn names_by_email(&self, address: &str) -> crate::Result<Vec<String>> {
- let result = sqlx::query_scalar::<_, String>(&self.mappings.query_recipients)
- .bind(unwrap_subaddress(address, self.opt.subaddressing).as_ref())
+ let ids = sqlx::query_scalar::<_, String>(&self.mappings.query_recipients)
+ .bind(self.opt.subaddressing.to_subaddress(address).as_ref())
.fetch(&self.pool)
.try_collect::<Vec<_>>()
- .await;
- match result {
- Ok(ids) if !ids.is_empty() => Ok(ids),
- Ok(_) if self.opt.catch_all => {
- sqlx::query_scalar::<_, String>(&self.mappings.query_recipients)
- .bind(to_catch_all_address(address))
- .fetch(&self.pool)
- .try_collect::<Vec<_>>()
- .await
- .map_err(Into::into)
- }
- Ok(_) => Ok(vec![]),
- Err(err) => Err(err.into()),
+ .await?;
+ if !ids.is_empty() {
+ Ok(ids)
+ } else if let Some(address) = self.opt.catch_all.to_catch_all(address) {
+ sqlx::query_scalar::<_, String>(&self.mappings.query_recipients)
+ .bind(address.as_ref())
+ .fetch(&self.pool)
+ .try_collect::<Vec<_>>()
+ .await
+ .map_err(Into::into)
+ } else {
+ Ok(ids)
}
}
async fn rcpt(&self, address: &str) -> crate::Result<bool> {
let result = sqlx::query(&self.mappings.query_recipients)
- .bind(unwrap_subaddress(address, self.opt.subaddressing).as_ref())
+ .bind(self.opt.subaddressing.to_subaddress(address).as_ref())
.fetch(&self.pool)
.try_next()
.await;
match result {
Ok(Some(_)) => Ok(true),
- Ok(None) if self.opt.catch_all => sqlx::query(&self.mappings.query_recipients)
- .bind(to_catch_all_address(address))
- .fetch(&self.pool)
- .try_next()
- .await
- .map(|id| id.is_some())
- .map_err(Into::into),
- Ok(None) => Ok(false),
+ Ok(None) => {
+ if let Some(address) = self.opt.catch_all.to_catch_all(address) {
+ sqlx::query(&self.mappings.query_recipients)
+ .bind(address.as_ref())
+ .fetch(&self.pool)
+ .try_next()
+ .await
+ .map(|id| id.is_some())
+ .map_err(Into::into)
+ } else {
+ Ok(false)
+ }
+ }
+
Err(err) => Err(err.into()),
}
}
async fn vrfy(&self, address: &str) -> crate::Result<Vec<String>> {
sqlx::query_scalar::<_, String>(&self.mappings.query_verify)
- .bind(unwrap_subaddress(address, self.opt.subaddressing).as_ref())
+ .bind(self.opt.subaddressing.to_subaddress(address).as_ref())
.fetch(&self.pool)
.try_collect::<Vec<_>>()
.await
@@ -142,7 +147,7 @@ impl Directory for SqlDirectory {
async fn expn(&self, address: &str) -> crate::Result<Vec<String>> {
sqlx::query_scalar::<_, String>(&self.mappings.query_expand)
- .bind(unwrap_subaddress(address, self.opt.subaddressing).as_ref())
+ .bind(self.opt.subaddressing.to_subaddress(address).as_ref())
.fetch(&self.pool)
.try_collect::<Vec<_>>()
.await
diff --git a/crates/imap/Cargo.toml b/crates/imap/Cargo.toml
index 57c60ed0..9ed1f597 100644
--- a/crates/imap/Cargo.toml
+++ b/crates/imap/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "imap"
-version = "0.3.1"
+version = "0.3.2"
edition = "2021"
resolver = "2"
diff --git a/crates/install/Cargo.toml b/crates/install/Cargo.toml
index 2fa24529..c32da5bb 100644
--- a/crates/install/Cargo.toml
+++ b/crates/install/Cargo.toml
@@ -5,7 +5,7 @@ authors = ["Stalwart Labs Ltd. <hello@stalw.art>"]
license = "AGPL-3.0-only"
repository = "https://github.com/stalwartlabs/mail-server"
homepage = "https://github.com/stalwartlabs/mail-server"
-version = "0.3.1"
+version = "0.3.2"
edition = "2021"
readme = "README.md"
resolver = "2"
diff --git a/crates/install/src/main.rs b/crates/install/src/main.rs
index e2d91638..53fd66ad 100644
--- a/crates/install/src/main.rs
+++ b/crates/install/src/main.rs
@@ -445,7 +445,7 @@ fn main() -> std::io::Result<()> {
}
if args.docker {
cfg_file = cfg_file
- .replace("127.0.0.1:8686", "0.0.0.0:8686")
+ .replace("127.0.0.1:8080", "0.0.0.0:8080")
.replace("[server.run-as]", "#[server.run-as]")
.replace("user = \"stalwart-mail\"", "#user = \"stalwart-mail\"")
.replace("group = \"stalwart-mail\"", "#group = \"stalwart-mail\"");
diff --git a/crates/jmap/Cargo.toml b/crates/jmap/Cargo.toml
index b1184d9b..f0f17049 100644
--- a/crates/jmap/Cargo.toml
+++ b/crates/jmap/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "jmap"
-version = "0.3.1"
+version = "0.3.2"
edition = "2021"
resolver = "2"
diff --git a/crates/jmap/src/api/config.rs b/crates/jmap/src/api/config.rs
index e826a206..8d60d7c4 100644
--- a/crates/jmap/src/api/config.rs
+++ b/crates/jmap/src/api/config.rs
@@ -105,8 +105,7 @@ impl crate::Config {
.property("jmap.rate-limit.use-forwarded")?
.unwrap_or(false),
oauth_key: settings
- .value("oauth.key")
- .map(|k| k.into())
+ .text_file_contents("oauth.key")?
.unwrap_or_else(|| {
thread_rng()
.sample_iter(Alphanumeric)
diff --git a/crates/jmap/src/sieve/ingest.rs b/crates/jmap/src/sieve/ingest.rs
index ac2d1566..bc389ecd 100644
--- a/crates/jmap/src/sieve/ingest.rs
+++ b/crates/jmap/src/sieve/ingest.rs
@@ -381,7 +381,10 @@ impl JMAP {
continue;
}
}
- Event::ListContains { .. } | Event::Execute { .. } | Event::Notify { .. } => {
+ Event::ListContains { .. }
+ | Event::Execute { .. }
+ | Event::Notify { .. }
+ | Event::SetEnvelope { .. } => {
// Not allowed
input = false.into();
}
@@ -393,8 +396,6 @@ impl JMAP {
});
input = true.into();
}
- #[allow(unreachable_patterns)]
- _ => unreachable!(),
},
#[cfg(feature = "test_mode")]
diff --git a/crates/main/Cargo.toml b/crates/main/Cargo.toml
index 17bf6292..505f910c 100644
--- a/crates/main/Cargo.toml
+++ b/crates/main/Cargo.toml
@@ -7,7 +7,7 @@ homepage = "https://stalw.art"
keywords = ["imap", "jmap", "smtp", "email", "mail", "server"]
categories = ["email"]
license = "AGPL-3.0-only"
-version = "0.3.1"
+version = "0.3.2"
edition = "2021"
resolver = "2"
diff --git a/crates/smtp/Cargo.toml b/crates/smtp/Cargo.toml
index 13403c74..9529ddff 100644
--- a/crates/smtp/Cargo.toml
+++ b/crates/smtp/Cargo.toml
@@ -7,7 +7,7 @@ homepage = "https://stalw.art/smtp"
keywords = ["smtp", "email", "mail", "server"]
categories = ["email"]
license = "AGPL-3.0-only"
-version = "0.3.1"
+version = "0.3.2"
edition = "2021"
resolver = "2"
diff --git a/crates/smtp/src/config/auth.rs b/crates/smtp/src/config/auth.rs
index c0b224bf..8ff6b2c6 100644
--- a/crates/smtp/src/config/auth.rs
+++ b/crates/smtp/src/config/auth.rs
@@ -30,7 +30,7 @@ use mail_auth::{
use mail_parser::decoders::base64::base64_decode;
use utils::config::{
utils::{AsKey, ParseValue},
- Config,
+ Config, DynValue,
};
use super::{
@@ -69,7 +69,7 @@ impl ConfigAuth for Config {
.parse_if_block("auth.dkim.verify", ctx, &envelope_sender_keys)?
.unwrap_or_else(|| IfBlock::new(VerifyStrategy::Relaxed)),
sign: self
- .parse_if_block::<Vec<String>>("auth.dkim.sign", ctx, &envelope_sender_keys)?
+ .parse_if_block::<Vec<DynValue>>("auth.dkim.sign", ctx, &envelope_sender_keys)?
.unwrap_or_default()
.map_if_block(&ctx.signers, "auth.dkim.sign", "signature")?,
},
@@ -78,7 +78,11 @@ impl ConfigAuth for Config {
.parse_if_block("auth.arc.verify", ctx, &envelope_sender_keys)?
.unwrap_or_else(|| IfBlock::new(VerifyStrategy::Relaxed)),
seal: self
- .parse_if_block::<Option<String>>("auth.arc.seal", ctx, &envelope_sender_keys)?
+ .parse_if_block::<Option<DynValue>>(
+ "auth.arc.seal",
+ ctx,
+ &envelope_sender_keys,
+ )?
.unwrap_or_default()
.map_if_block(&ctx.sealers, "auth.arc.seal", "signature")?,
},
@@ -194,22 +198,43 @@ impl ConfigAuth for Config {
(DkimSigner::RsaSha256(signer), ArcSealer::RsaSha256(sealer))
}
Algorithm::Ed25519Sha256 => {
- let public_key =
- base64_decode(&self.file_contents(("signature", id, "public-key"))?)
- .ok_or_else(|| {
- format!(
- "Failed to base64 decode public key for {}.",
- ("signature", id, "public-key",).as_key(),
- )
- })?;
- let private_key =
- base64_decode(&self.file_contents(("signature", id, "private-key"))?)
- .ok_or_else(|| {
- format!(
- "Failed to base64 decode private key for {}.",
- ("signature", id, "private-key",).as_key(),
- )
+ let mut public_key = vec![];
+ let mut private_key = vec![];
+
+ for (key, key_bytes) in [
+ (("signature", id, "public-key"), &mut public_key),
+ (("signature", id, "private-key"), &mut private_key),
+ ] {
+ let mut contents = self.file_contents(key)?.into_iter();
+ let mut base64 = vec![];
+
+ 'outer: while let Some(ch) = contents.next() {
+ if !ch.is_ascii_whitespace() {
+ if ch == b'-' {
+ for ch in contents.by_ref() {
+ if ch == b'\n' {
+ break;
+ }
+ }
+ } else {
+ base64.push(ch);
+ }
+
+ for ch in contents.by_ref() {
+ if ch == b'-' {
+ break 'outer;
+ } else if !ch.is_ascii_whitespace() {
+ base64.push(ch);
+ }
+ }
+ }
+ }
+
+ *key_bytes = base64_decode(&base64).ok_or_else(|| {
+ format!("Failed to base64 decode key for {}.", key.as_key(),)
})?;
+ }
+
let key = Ed25519Key::from_seed_and_public_key(&private_key, &public_key)
.map_err(|err| {
format!("Failed to build ED25519 key for signature {id:?}: {err}")
diff --git a/crates/smtp/src/config/if_block.rs b/crates/smtp/src/config/if_block.rs
index ab3856ed..2d01ea5c 100644
--- a/crates/smtp/src/config/if_block.rs
+++ b/crates/smtp/src/config/if_block.rs
@@ -25,10 +25,12 @@ use std::sync::Arc;
use ahash::AHashMap;
-use super::{condition::ConfigCondition, ConfigContext, EnvelopeKey, IfBlock, IfThen};
+use super::{
+ condition::ConfigCondition, ConfigContext, EnvelopeKey, IfBlock, IfThen, MaybeDynValue,
+};
use utils::config::{
utils::{AsKey, ParseValues},
- Config,
+ Config, DynValue,
};
pub trait ConfigIf {
@@ -187,6 +189,12 @@ impl<T: Default> IfBlock<Option<T>> {
}
}
+impl<T> IfBlock<Option<T>> {
+ pub fn is_empty(&self) -> bool {
+ self.default.is_none() && self.if_then.is_empty()
+ }
+}
+
impl IfBlock<Option<String>> {
pub fn map_if_block<T: ?Sized>(
self,
@@ -229,6 +237,7 @@ impl IfBlock<Option<String>> {
}
}
+/*
impl IfBlock<Vec<String>> {
pub fn map_if_block<T: ?Sized>(
self,
@@ -269,6 +278,104 @@ impl IfBlock<Vec<String>> {
Ok(result)
}
}
+*/
+
+impl IfBlock<Vec<DynValue>> {
+ pub fn map_if_block<T: ?Sized>(
+ self,
+ map: &AHashMap<String, Arc<T>>,
+ key_name: &str,
+ object_name: &str,
+ ) -> super::Result<IfBlock<Vec<MaybeDynValue<T>>>> {
+ let mut if_then = Vec::with_capacity(self.if_then.len());
+ for if_clause in self.if_then.into_iter() {
+ if_then.push(IfThen {
+ conditions: if_clause.conditions,
+ then: Self::map_value(map, if_clause.then, object_name, key_name)?,
+ });
+ }
+
+ Ok(IfBlock {
+ if_then,
+ default: Self::map_value(map, self.default, object_name, key_name)?,
+ })
+ }
+
+ fn map_value<T: ?Sized>(
+ map: &AHashMap<String, Arc<T>>,
+ values: Vec<DynValue>,
+ object_name: &str,
+ key_name: &str,
+ ) -> super::Result<Vec<MaybeDynValue<T>>> {
+ let mut result = Vec::with_capacity(values.len());
+ for value in values {
+ if let DynValue::String(value) = &value {
+ if let Some(value) = map.get(value) {
+ result.push(MaybeDynValue::Static(value.clone()));
+ } else {
+ return Err(format!(
+ "Unable to find {object_name} {value:?} declared for {key_name:?}",
+ ));
+ }
+ } else {
+ result.push(MaybeDynValue::Dynamic {
+ eval: value,
+ items: map.clone(),
+ });
+ }
+ }
+ Ok(result)
+ }
+}
+
+impl IfBlock<Option<DynValue>> {
+ pub fn map_if_block<T: ?Sized>(
+ self,
+ map: &AHashMap<String, Arc<T>>,
+ key_name: impl AsKey,
+ object_name: &str,
+ ) -> super::Result<IfBlock<Option<MaybeDynValue<T>>>> {
+ let key_name = key_name.as_key();
+ let mut if_then = Vec::with_capacity(self.if_then.len());
+ for if_clause in self.if_then.into_iter() {
+ if_then.push(IfThen {
+ conditions: if_clause.conditions,
+ then: Self::map_value(map, if_clause.then, object_name, &key_name)?,
+ });
+ }
+
+ Ok(IfBlock {
+ if_then,
+ default: Self::map_value(map, self.default, object_name, &key_name)?,
+ })
+ }
+
+ fn map_value<T: ?Sized>(
+ map: &AHashMap<String, Arc<T>>,
+ value: Option<DynValue>,
+ object_name: &str,
+ key_name: &str,
+ ) -> super::Result<Option<MaybeDynValue<T>>> {
+ if let Some(value) = value {
+ if let DynValue::String(value) = &value {
+ if let Some(value) = map.get(value) {
+ Ok(Some(MaybeDynValue::Static(value.clone())))
+ } else {
+ Err(format!(
+ "Unable to find {object_name} {value:?} declared for {key_name:?}",
+ ))
+ }
+ } else {
+ Ok(Some(MaybeDynValue::Dynamic {
+ eval: value,
+ items: map.clone(),
+ }))
+ }
+ } else {
+ Ok(None)
+ }
+ }
+}
impl<T> IfBlock<Vec<T>> {
pub fn has_empty_list(&self) -> bool {
diff --git a/crates/smtp/src/config/mod.rs b/crates/smtp/src/config/mod.rs
index f9746d4f..0b149d98 100644
--- a/crates/smtp/src/config/mod.rs
+++ b/crates/smtp/src/config/mod.rs
@@ -50,7 +50,7 @@ use mail_send::Credentials;
use regex::Regex;
use sieve::Sieve;
use smtp_proto::MtPriority;
-use utils::config::{Rate, Server, ServerProtocol};
+use utils::config::{DynValue, Rate, Server, ServerProtocol};
use crate::inbound::milter;
@@ -222,7 +222,7 @@ pub struct Extensions {
}
pub struct Auth {
- pub directory: IfBlock<Option<Arc<dyn Directory>>>,
+ pub directory: IfBlock<Option<MaybeDynValue<dyn Directory>>>,
pub mechanisms: IfBlock<u64>,
pub require: IfBlock<bool>,
pub errors_max: IfBlock<usize>,
@@ -231,12 +231,14 @@ pub struct Auth {
pub struct Mail {
pub script: IfBlock<Option<Arc<Sieve>>>,
+ pub rewrite: IfBlock<Option<DynValue>>,
}
pub struct Rcpt {
pub script: IfBlock<Option<Arc<Sieve>>>,
pub relay: IfBlock<bool>,
- pub directory: IfBlock<Option<Arc<dyn Directory>>>,
+ pub directory: IfBlock<Option<MaybeDynValue<dyn Directory>>>,
+ pub rewrite: IfBlock<Option<DynValue>>,
// Errors
pub errors_max: IfBlock<usize>,
@@ -375,10 +377,19 @@ pub enum AddressMatch {
Equals(String),
}
+#[derive(Clone)]
+pub enum MaybeDynValue<T: ?Sized> {
+ Dynamic {
+ eval: DynValue,
+ items: AHashMap<String, Arc<T>>,
+ },
+ Static(Arc<T>),
+}
+
pub struct Dsn {
pub name: IfBlock<String>,
pub address: IfBlock<String>,
- pub sign: IfBlock<Vec<Arc<DkimSigner>>>,
+ pub sign: IfBlock<Vec<MaybeDynValue<DkimSigner>>>,
}
pub struct AggregateReport {
@@ -387,7 +398,7 @@ pub struct AggregateReport {
pub org_name: IfBlock<Option<String>>,
pub contact_info: IfBlock<Option<String>>,
pub send: IfBlock<AggregateFrequency>,
- pub sign: IfBlock<Vec<Arc<DkimSigner>>>,
+ pub sign: IfBlock<Vec<MaybeDynValue<DkimSigner>>>,
pub max_size: IfBlock<usize>,
}
@@ -395,7 +406,7 @@ pub struct Report {
pub name: IfBlock<String>,
pub address: IfBlock<String>,
pub subject: IfBlock<String>,
- pub sign: IfBlock<Vec<Arc<DkimSigner>>>,
+ pub sign: IfBlock<Vec<MaybeDynValue<DkimSigner>>>,
pub send: IfBlock<Option<Rate>>,
}
@@ -481,12 +492,12 @@ pub enum ArcSealer {
pub struct DkimAuthConfig {
pub verify: IfBlock<VerifyStrategy>,
- pub sign: IfBlock<Vec<Arc<DkimSigner>>>,
+ pub sign: IfBlock<Vec<MaybeDynValue<DkimSigner>>>,
}
pub struct ArcAuthConfig {
pub verify: IfBlock<VerifyStrategy>,
- pub seal: IfBlock<Option<Arc<ArcSealer>>>,
+ pub seal: IfBlock<Option<MaybeDynValue<ArcSealer>>>,
}
pub struct SpfAuthConfig {
diff --git a/crates/smtp/src/config/queue.rs b/crates/smtp/src/config/queue.rs
index 60d7dfb8..ba9419de 100644
--- a/crates/smtp/src/config/queue.rs
+++ b/crates/smtp/src/config/queue.rs
@@ -34,7 +34,7 @@ use super::{
};
use utils::config::{
utils::{AsKey, ParseValue},
- Config,
+ Config, DynValue,
};
pub trait ConfigQueue {
@@ -189,7 +189,7 @@ impl ConfigQueue for Config {
.parse_if_block("report.dsn.from-address", ctx, &sender_envelope_keys)?
.unwrap_or_else(|| IfBlock::new(format!("MAILER-DAEMON@{default_hostname}"))),
sign: self
- .parse_if_block::<Vec<String>>("report.dsn.sign", ctx, &sender_envelope_keys)?
+ .parse_if_block::<Vec<DynValue>>("report.dsn.sign", ctx, &sender_envelope_keys)?
.unwrap_or_default()
.map_if_block(&ctx.signers, "report.dsn.sign", "signature")?,
},
diff --git a/crates/smtp/src/config/report.rs b/crates/smtp/src/config/report.rs
index 7ad93472..dce1b3e6 100644
--- a/crates/smtp/src/config/report.rs
+++ b/crates/smtp/src/config/report.rs
@@ -27,7 +27,7 @@ use super::{
};
use utils::config::{
utils::{AsKey, ParseValue},
- Config,
+ Config, DynValue,
};
pub trait ConfigReport {
@@ -120,7 +120,7 @@ impl ConfigReport for Config {
.parse_if_block(("report", id, "subject"), ctx, available_keys)?
.unwrap_or_else(|| IfBlock::new(format!("{} Report", id.to_ascii_uppercase()))),
sign: self
- .parse_if_block::<Vec<String>>(("report", id, "sign"), ctx, available_keys)?
+ .parse_if_block::<Vec<DynValue>>(("report", id, "sign"), ctx, available_keys)?
.unwrap_or_default()
.map_if_block(&ctx.signers, &("report", id, "sign").as_key(), "signature")?,
send: self
@@ -173,7 +173,7 @@ impl ConfigReport for Config {
.parse_if_block(("report", id, "aggregate.send"), ctx, available_keys)?
.unwrap_or_default(),
sign: self
- .parse_if_block::<Vec<String>>(
+ .parse_if_block::<Vec<DynValue>>(
("report", id, "aggregate.sign"),
ctx,
&rcpt_envelope_keys,
diff --git a/crates/smtp/src/config/session.rs b/crates/smtp/src/config/session.rs
index 96041ada..6eb60eb4 100644
--- a/crates/smtp/src/config/session.rs
+++ b/crates/smtp/src/config/session.rs
@@ -28,7 +28,7 @@ use smtp_proto::*;
use super::{if_block::ConfigIf, throttle::ConfigThrottle, *};
use utils::config::{
utils::{AsKey, ParseValue},
- Config,
+ Config, DynValue,
};
pub trait ConfigSession {
@@ -250,7 +250,7 @@ impl ConfigSession for Config {
Ok(Auth {
directory: self
- .parse_if_block::<Option<String>>("session.auth.directory", ctx, &available_keys)?
+ .parse_if_block::<Option<DynValue>>("session.auth.directory", ctx, &available_keys)?
.unwrap_or_default()
.map_if_block(
&ctx.directory.directories,
@@ -296,6 +296,9 @@ impl ConfigSession for Config {
.parse_if_block::<Option<String>>("session.mail.script", ctx, &available_keys)?
.unwrap_or_default()
.map_if_block(&ctx.scripts, "session.mail.script", "script")?,
+ rewrite: self
+ .parse_if_block::<Option<DynValue>>("session.mail.rewrite", ctx, &available_keys)?
+ .unwrap_or_default(),
})
}
@@ -318,7 +321,7 @@ impl ConfigSession for Config {
.parse_if_block("session.rcpt.relay", ctx, &available_keys)?
.unwrap_or_else(|| IfBlock::new(false)),
directory: self
- .parse_if_block::<Option<String>>("session.rcpt.directory", ctx, &available_keys)?
+ .parse_if_block::<Option<DynValue>>("session.rcpt.directory", ctx, &available_keys)?
.unwrap_or_default()
.map_if_block(
&ctx.directory.directories,
@@ -334,6 +337,9 @@ impl ConfigSession for Config {
max_recipients: self
.parse_if_block("session.rcpt.max-recipients", ctx, &available_keys)?
.unwrap_or_else(|| IfBlock::new(100)),
+ rewrite: self
+ .parse_if_block::<Option<DynValue>>("session.rcpt.rewrite", ctx, &available_keys)?
+ .unwrap_or_default(),
})
}
diff --git a/crates/smtp/src/core/if_block.rs b/crates/smtp/src/core/if_block.rs
index 738cb050..fbfaf03c 100644
--- a/crates/smtp/src/core/if_block.rs
+++ b/crates/smtp/src/core/if_block.rs
@@ -21,14 +21,26 @@
* for more details.
*/
-use std::net::{IpAddr, Ipv4Addr};
+use std::{
+ borrow::Cow,
+ net::{IpAddr, Ipv4Addr},
+ sync::Arc,
+};
+
+use utils::config::DynValue;
use crate::config::{
- Condition, ConditionMatch, Conditions, EnvelopeKey, IfBlock, IpAddrMask, StringMatch,
+ Condition, ConditionMatch, Conditions, EnvelopeKey, IfBlock, IpAddrMask, MaybeDynValue,
+ StringMatch,
};
use super::Envelope;
+pub struct Captures<'x, T> {
+ value: &'x T,
+ captures: Vec<String>,
+}
+
impl<T: Default> IfBlock<T> {
pub async fn eval(&self, envelope: &impl Envelope) -> &T {
for if_then in &self.if_then {
@@ -39,6 +51,22 @@ impl<T: Default> IfBlock<T> {
&self.default
}
+
+ pub async fn eval_and_capture(&self, envelope: &impl Envelope) -> Captures<'_, T> {
+ for if_then in &self.if_then {
+ if let Some(captures) = if_then.conditions.eval_and_capture(envelope).await {
+ return Captures {
+ value: &if_then.then,
+ captures,
+ };
+ }
+ }
+
+ Captures {
+ value: &self.default,
+ captures: vec![],
+ }
+ }
}
impl Conditions {
@@ -116,6 +144,97 @@ impl Conditions {
matched
}
+
+ pub async fn eval_and_capture(&self, envelope: &impl Envelope) -> Option<Vec<String>> {
+ let mut conditions = self.conditions.iter();
+ let mut matched = false;
+ let mut last_capture = vec![];
+ let mut regex_capture = vec![];
+
+ while let Some(rule) = conditions.next() {
+ match rule {
+ Condition::Match { key, value, not } => {
+ let ctx_value = envelope.key_to_string(key);
+ matched = match value {
+ ConditionMatch::String(value) => match value {
+ StringMatch::Equal(value) => value.eq(ctx_value.as_ref()),
+ StringMatch::StartsWith(value) => ctx_value.starts_with(value),
+ StringMatch::EndsWith(value) => ctx_value.ends_with(value),
+ },
+ ConditionMatch::IpAddrMask(value) => value.matches(&match key {
+ EnvelopeKey::RemoteIp => envelope.remote_ip(),
+ EnvelopeKey::LocalIp => envelope.local_ip(),
+ _ => IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)),
+ }),
+ ConditionMatch::UInt(value) => {
+ *value
+ == if key == &EnvelopeKey::Listener {
+ envelope.listener_id()
+ } else {
+ debug_assert!(false, "Invalid value for UInt context key.");
+ u16::MAX
+ }
+ }
+ ConditionMatch::Int(value) => {
+ *value
+ == if key == &EnvelopeKey::Listener {
+ envelope.priority()
+ } else {
+ debug_assert!(false, "Invalid value for UInt context key.");
+ i16::MAX
+ }
+ }
+ ConditionMatch::Lookup(lookup) => {
+ lookup.contains(ctx_value.as_ref()).await?
+ }
+ ConditionMatch::Regex(value) => {
+ regex_capture.clear();
+
+ for captures in value.captures_iter(ctx_value.as_ref()) {
+ for capture in captures.iter() {
+ regex_capture
+ .push(capture.map_or("", |m| m.as_str()).to_string());
+ }
+ }
+
+ !regex_capture.is_empty()
+ }
+ } ^ not;
+
+ // Save last capture
+ if matched {
+ last_capture = if regex_capture.is_empty() {
+ vec![ctx_value.into_owned()]
+ } else {
+ std::mem::take(&mut regex_capture)
+ };
+ }
+ }
+ Condition::JumpIfTrue { positions } => {
+ if matched {
+ //TODO use advance_by when stabilized
+ for _ in 0..*positions {
+ conditions.next();
+ }
+ }
+ }
+ Condition::JumpIfFalse { positions } => {
+ if !matched {
+ //TODO use advance_by when stabilized
+ for _ in 0..*positions {
+ conditions.next();
+ }
+ }
+ }
+ }
+ }
+
+ if matched {
+ Some(last_capture)
+ } else {
+ None
+ }
+ }
}
impl IpAddrMask {
@@ -168,3 +287,95 @@ impl IpAddrMask {
}
}
}
+
+impl<'x> Captures<'x, DynValue> {
+ pub fn into_value(self) -> Cow<'x, str> {
+ self.value.apply(self.captures)
+ }
+}
+
+impl<'x> Captures<'x, Option<DynValue>> {
+ pub fn into_value(self) -> Option<Cow<'x, str>> {
+ self.value.as_ref().map(|v| v.apply(self.captures))
+ }
+}
+
+impl<'x, T: ?Sized> Captures<'x, MaybeDynValue<T>> {
+ pub fn into_value(self) -> Option<Arc<T>> {
+ match &self.value {
+ MaybeDynValue::Dynamic { eval, items } => {
+ let r = eval.apply(self.captures);
+
+ match items.get(r.as_ref()) {
+ Some(value) => value.clone().into(),
+ None => {
+ tracing::warn!(
+ context = "eval",
+ event = "error",
+ expression = ?eval,
+ result = ?r,
+ "Failed to resolve rule: value {r:?} not found in item list",
+ );
+ None
+ }
+ }
+ }
+ MaybeDynValue::Static(value) => value.clone().into(),
+ }
+ }
+}
+
+impl<'x, T: ?Sized> Captures<'x, Vec<MaybeDynValue<T>>> {
+ pub fn into_value(self) -> Vec<Arc<T>> {
+ let mut results = Vec::with_capacity(self.value.len());
+ for value in self.value.iter() {
+ match value {
+ MaybeDynValue::Dynamic { eval, items } => {
+ let r = eval.apply_borrowed(&self.captures);
+ match items.get(r.as_ref()) {
+ Some(value) => {
+ results.push(value.clone());
+ }
+ None => {
+ tracing::warn!(
+ context = "eval",
+ event = "error",
+ expression = ?eval,
+ result = ?r,
+ "Failed to resolve rule: value {r:?} not found in item list",
+ );
+ }
+ }
+ }
+ MaybeDynValue::Static(value) => {
+ results.push(value.clone());
+ }
+ }
+ }
+ results
+ }
+}
+
+impl<'x, T: ?Sized> Captures<'x, Option<MaybeDynValue<T>>> {
+ pub fn into_value(self) -> Option<Arc<T>> {
+ match self.value.as_ref()? {
+ MaybeDynValue::Dynamic { eval, items } => {
+ let r = eval.apply(self.captures);
+ match items.get(r.as_ref()) {
+ Some(value) => value.clone().into(),
+ None => {
+ tracing::warn!(
+ context = "eval",
+ event = "error",
+ expression = ?eval,
+ result = ?r,
+ "Failed to resolve rule: value {r:?} not found in item list",
+ );
+ None
+ }
+ }
+ }
+ MaybeDynValue::Static(value) => value.clone().into(),
+ }
+ }
+}
diff --git a/crates/smtp/src/core/mod.rs b/crates/smtp/src/core/mod.rs
index 4d4d1fe5..36b959fc 100644
--- a/crates/smtp/src/core/mod.rs
+++ b/crates/smtp/src/core/mod.rs
@@ -230,13 +230,11 @@ pub struct SessionParameters {
pub auth_errors_wait: Duration,
// Rcpt parameters
- pub rcpt_script: Option<Arc<Sieve>>,
pub rcpt_relay: bool,
pub rcpt_errors_max: usize,
pub rcpt_errors_wait: Duration,
pub rcpt_max: usize,
pub rcpt_dsn: bool,
- pub rcpt_directory: Option<Arc<dyn Directory>>,
pub can_expn: bool,
pub can_vrfy: bool,
pub max_message_size: usize,
@@ -463,13 +461,11 @@ impl Session<NullIo> {
auth_require: Default::default(),
auth_errors_max: Default::default(),
auth_errors_wait: Default::default(),
- rcpt_script: Default::default(),
rcpt_relay: Default::default(),
rcpt_errors_max: Default::default(),
rcpt_errors_wait: Default::default(),
rcpt_max: Default::default(),
rcpt_dsn: Default::default(),
- rcpt_directory: Default::default(),
max_message_size: Default::default(),
iprev: crate::config::VerifyStrategy::Disable,
spf_ehlo: crate::config::VerifyStrategy::Disable,
diff --git a/crates/smtp/src/core/params.rs b/crates/smtp/src/core/params.rs
index c683e73a..2380adcc 100644
--- a/crates/smtp/src/core/params.rs
+++ b/crates/smtp/src/core/params.rs
@@ -44,7 +44,7 @@ impl<T: AsyncRead + AsyncWrite> Session<T> {
// Auth parameters
let ac = &self.core.session.config.auth;
- self.params.auth_directory = ac.directory.eval(self).await.clone();
+ self.params.auth_directory = ac.directory.eval_and_capture(self).await.into_value();
self.params.auth_require = *ac.require.eval(self).await;
self.params.auth_errors_max = *ac.errors_max.eval(self).await;
self.params.auth_errors_wait = *ac.errors_wait.eval(self).await;
@@ -53,15 +53,6 @@ impl<T: AsyncRead + AsyncWrite> Session<T> {
let ec = &self.core.session.config.extensions;
self.params.can_expn = *ec.expn.eval(self).await;
self.params.can_vrfy = *ec.vrfy.eval(self).await;
- self.params.rcpt_directory = self
- .core
- .session
- .config
- .rcpt
- .directory
- .eval(self)
- .await
- .clone();
}
pub async fn eval_post_auth_params(&mut self) {
@@ -69,25 +60,14 @@ impl<T: AsyncRead + AsyncWrite> Session<T> {
let ec = &self.core.session.config.extensions;
self.params.can_expn = *ec.expn.eval(self).await;
self.params.can_vrfy = *ec.vrfy.eval(self).await;
- self.params.rcpt_directory = self
- .core
- .session
- .config
- .rcpt
- .directory
- .eval(self)
- .await
- .clone();
}
pub async fn eval_rcpt_params(&mut self) {
let rc = &self.core.session.config.rcpt;
- self.params.rcpt_script = rc.script.eval(self).await.clone();
self.params.rcpt_relay = *rc.relay.eval(self).await;
self.params.rcpt_errors_max = *rc.errors_max.eval(self).await;
self.params.rcpt_errors_wait = *rc.errors_wait.eval(self).await;
self.params.rcpt_max = *rc.max_recipients.eval(self).await;
- self.params.rcpt_directory = rc.directory.eval(self).await.clone();
self.params.rcpt_dsn = *self.core.session.config.extensions.dsn.eval(self).await;
self.params.max_message_size = *self
diff --git a/crates/smtp/src/core/scripts.rs b/crates/smtp/src/core/scripts.rs
index febd8988..c126417a 100644
--- a/crates/smtp/src/core/scripts.rs
+++ b/crates/smtp/src/core/scripts.rs
@@ -41,11 +41,16 @@ use tokio::{
use crate::queue::{DomainPart, InstantFromTimestamp, Message};
-use super::{Session, SMTP};
+use super::{Session, SessionAddress, SessionData, SMTP};
pub enum ScriptResult {
- Accept,
- Replace(Vec<u8>),
+ Accept {
+ modifications: Vec<(Envelope, String)>,
+ },
+ Replace {
+ message: Vec<u8>,
+ modifications: Vec<(Envelope, String)>,
+ },
Reject(String),
Discard,
}
@@ -108,7 +113,9 @@ impl<T: AsyncWrite + AsyncRead + Unpin> Session<T> {
core.run_script_blocking(script, vars_env, envelope, message, handle, span)
})
.await
- .unwrap_or(ScriptResult::Accept)
+ .unwrap_or(ScriptResult::Accept {
+ modifications: vec![],
+ })
}
}
@@ -135,6 +142,7 @@ impl SMTP {
let mut messages: Vec<Vec<u8>> = Vec::new();
let mut reject_reason = None;
+ let mut modifications = vec![];
let mut keep_id = usize::MAX;
// Start event loop
@@ -416,6 +424,10 @@ impl SMTP {
messages.push(message);
input = true.into();
}
+ Event::SetEnvelope { envelope, value } => {
+ modifications.push((envelope, value));
+ input = true.into();
+ }
unsupported => {
tracing::warn!(
parent: &span,
@@ -443,7 +455,7 @@ impl SMTP {
// MAX - 1 = discard message
if keep_id == 0 {
- ScriptResult::Accept
+ ScriptResult::Accept { modifications }
} else if let Some(mut reject_reason) = reject_reason {
if !reject_reason.ends_with('\n') {
reject_reason.push_str("\r\n");
@@ -459,13 +471,129 @@ impl SMTP {
ScriptResult::Reject(format!("503 5.5.3 {reject_reason}"))
}
} else if keep_id != usize::MAX - 1 {
- messages
- .into_iter()
- .nth(keep_id - 1)
- .map(ScriptResult::Replace)
- .unwrap_or(ScriptResult::Accept)
+ if let Some(message) = messages.into_iter().nth(keep_id - 1) {
+ ScriptResult::Replace {
+ message,
+ modifications,
+ }
+ } else {
+ ScriptResult::Accept { modifications }
+ }
} else {
ScriptResult::Discard
}
}
}
+
+impl SessionData {
+ pub fn apply_sieve_modifications(&mut self, modifications: Vec<(Envelope, String)>) {
+ for (envelope, value) in modifications {
+ match envelope {
+ Envelope::From => {
+ let (address, address_lcase, domain) = if value.contains('@') {
+ let address_lcase = value.to_lowercase();
+ let domain = address_lcase.domain_part().to_string();
+ (value, address_lcase, domain)
+ } else if value.is_empty() {
+ (String::new(), String::new(), String::new())
+ } else {
+ continue;
+ };
+ if let Some(mail_from) = &mut self.mail_from {
+ mail_from.address = address;
+ mail_from.address_lcase = address_lcase;
+ mail_from.domain = domain;
+ } else {
+ self.mail_from = SessionAddress {
+ address,
+ address_lcase,
+ domain,
+ flags: 0,
+ dsn_info: None,
+ }
+ .into();
+ }
+ }
+ Envelope::To => {
+ if value.contains('@') {
+ let address_lcase = value.to_lowercase();
+ let domain = address_lcase.domain_part().to_string();
+ if let Some(rcpt_to) = self.rcpt_to.last_mut() {
+ rcpt_to.address = value;
+ rcpt_to.address_lcase = address_lcase;
+ rcpt_to.domain = domain;
+ } else {
+ self.rcpt_to.push(SessionAddress {
+ address: value,
+ address_lcase,
+ domain,
+ flags: 0,
+ dsn_info: None,
+ });
+ }
+ }
+ }
+ Envelope::ByMode => {
+ if let Some(mail_from) = &mut self.mail_from {
+ mail_from.flags &= !(MAIL_BY_NOTIFY | MAIL_BY_RETURN);
+ if value == "N" {
+ mail_from.flags |= MAIL_BY_NOTIFY;
+ } else if value == "R" {
+ mail_from.flags |= MAIL_BY_RETURN;
+ }
+ }
+ }
+ Envelope::ByTrace => {
+ if let Some(mail_from) = &mut self.mail_from {
+ if value == "T" {
+ mail_from.flags |= MAIL_BY_TRACE;
+ } else {
+ mail_from.flags &= !MAIL_BY_TRACE;
+ }
+ }
+ }
+ Envelope::Notify => {
+ if let Some(rcpt_to) = self.rcpt_to.last_mut() {
+ rcpt_to.flags &= !(RCPT_NOTIFY_DELAY
+ | RCPT_NOTIFY_FAILURE
+ | RCPT_NOTIFY_SUCCESS
+ | RCPT_NOTIFY_NEVER);
+ if value == "NEVER" {
+ rcpt_to.flags |= RCPT_NOTIFY_NEVER;
+ } else {
+ for value in value.split(',') {
+ match value.trim() {
+ "SUCCESS" => rcpt_to.flags |= RCPT_NOTIFY_SUCCESS,
+ "FAILURE" => rcpt_to.flags |= RCPT_NOTIFY_FAILURE,
+ "DELAY" => rcpt_to.flags |= RCPT_NOTIFY_DELAY,
+ _ => (),
+ }
+ }
+ }
+ }
+ }
+ Envelope::Ret => {
+ if let Some(mail_from) = &mut self.mail_from {
+ mail_from.flags &= !(MAIL_RET_FULL | MAIL_RET_HDRS);
+ if value == "FULL" {
+ mail_from.flags |= MAIL_RET_FULL;
+ } else if value == "HDRS" {
+ mail_from.flags |= MAIL_RET_HDRS;
+ }
+ }
+ }
+ Envelope::Orcpt => {
+ if let Some(rcpt_to) = self.rcpt_to.last_mut() {
+ rcpt_to.dsn_info = value.into();
+ }
+ }
+ Envelope::Envid => {
+ if let Some(mail_from) = &mut self.mail_from {
+ mail_from.dsn_info = value.into();
+ }
+ }
+ Envelope::ByTimeAbsolute | Envelope::ByTimeRelative => (),
+ }
+ }
+ }
+}
diff --git a/crates/smtp/src/inbound/data.rs b/crates/smtp/src/inbound/data.rs
index 1ec1d8ec..0d94a50a 100644
--- a/crates/smtp/src/inbound/data.rs
+++ b/crates/smtp/src/inbound/data.rs
@@ -145,7 +145,7 @@ impl<T: AsyncWrite + AsyncRead + IsTls + Unpin> Session<T> {
// Verify ARC
let arc = *ac.arc.verify.eval(self).await;
- let arc_sealer = ac.arc.seal.eval(self).await;
+ let arc_sealer = ac.arc.seal.eval_and_capture(self).await.into_value();
let arc_output = if arc.verify() || arc_sealer.is_some() {
let arc_output = self.core.resolvers.dns.verify_arc(&auth_message).await;
@@ -302,7 +302,7 @@ impl<T: AsyncWrite + AsyncRead + IsTls + Unpin> Session<T> {
"Milter filter(s) accepted message.");
self.data
- .apply_modifications(modifications, &auth_message)
+ .apply_milter_modifications(modifications, &auth_message)
.map(Arc::new)
}
Err(response) => return response,
@@ -404,9 +404,19 @@ impl<T: AsyncWrite + AsyncRead + IsTls + Unpin> Session<T> {
)
.await
{
- ScriptResult::Accept => (),
- ScriptResult::Replace(new_message) => {
- edited_message = Arc::new(new_message).into();
+ ScriptResult::Accept { modifications } => {
+ if !modifications.is_empty() {
+ self.data.apply_sieve_modifications(modifications)
+ }
+ }
+ ScriptResult::Replace {
+ message,
+ modifications,
+ } => {
+ if !modifications.is_empty() {
+ self.data.apply_sieve_modifications(modifications)
+ }
+ edited_message = Arc::new(message).into();
}
ScriptResult::Reject(message) => {
tracing::debug!(parent: &self.span,
@@ -492,7 +502,7 @@ impl<T: AsyncWrite + AsyncRead + IsTls + Unpin> Session<T> {
// DKIM sign
let raw_message = edited_message.unwrap_or(raw_message);
- for signer in ac.dkim.sign.eval(self).await.iter() {
+ for signer in ac.dkim.sign.eval_and_capture(self).await.into_value() {
match signer.sign_chained(&[headers.as_ref(), &raw_message]) {
Ok(signature) => {
signature.write_header(&mut headers);
diff --git a/crates/smtp/src/inbound/mail.rs b/crates/smtp/src/inbound/mail.rs
index e275e7f7..b0b0a5c5 100644
--- a/crates/smtp/src/inbound/mail.rs
+++ b/crates/smtp/src/inbound/mail.rs
@@ -139,14 +139,50 @@ impl<T: AsyncWrite + AsyncRead + Unpin + IsTls> Session<T> {
// Sieve filtering
if let Some(script) = self.core.session.config.mail.script.eval(self).await {
- if let ScriptResult::Reject(message) = self.run_script(script.clone(), None).await {
- tracing::debug!(parent: &self.span,
- context = "mail-from",
- event = "sieve-reject",
+ match self.run_script(script.clone(), None).await {
+ ScriptResult::Accept { modifications } => {
+ if !modifications.is_empty() {
+ tracing::debug!(parent: &self.span,
+ context = "sieve",
+ event = "modify",
+ address = &self.data.mail_from.as_ref().unwrap().address,
+ modifications = ?modifications);
+ self.data.apply_sieve_modifications(modifications)
+ }
+ }
+ ScriptResult::Reject(message) => {
+ tracing::debug!(parent: &self.span,
+ context = "sieve",
+ event = "reject",
address = &self.data.mail_from.as_ref().unwrap().address,
reason = message);
- self.data.mail_from = None;
- return self.write(message.as_bytes()).await;
+ self.data.mail_from = None;
+ return self.write(message.as_bytes()).await;
+ }
+ _ => (),
+ }
+ }
+
+ // Address rewriting
+ if let Some(new_address) = self
+ .core
+ .session
+ .config
+ .mail
+ .rewrite
+ .eval_and_capture(self)
+ .await
+ .into_value()
+ {
+ let mut mail_from = self.data.mail_from.as_mut().unwrap();
+ if new_address.contains('@') {
+ mail_from.address_lcase = new_address.to_lowercase();
+ mail_from.domain = mail_from.address_lcase.domain_part().to_string();
+ mail_from.address = new_address.into_owned();
+ } else if new_address.is_empty() {
+ mail_from.address_lcase.clear();
+ mail_from.domain.clear();
+ mail_from.address.clear();
}
}
diff --git a/crates/smtp/src/inbound/milter/message.rs b/crates/smtp/src/inbound/milter/message.rs
index eccb16b5..3792d605 100644
--- a/crates/smtp/src/inbound/milter/message.rs
+++ b/crates/smtp/src/inbound/milter/message.rs
@@ -239,7 +239,7 @@ impl<T: AsyncWrite + AsyncRead + IsTls + Unpin> Session<T> {
}
impl SessionData {
- pub fn apply_modifications(
+ pub fn apply_milter_modifications(
&mut self,
modifications: Vec<Modification>,
message: &AuthenticatedMessage<'_>,
diff --git a/crates/smtp/src/inbound/rcpt.rs b/crates/smtp/src/inbound/rcpt.rs
index 7ab3883f..84002c57 100644
--- a/crates/smtp/src/inbound/rcpt.rs
+++ b/crates/smtp/src/inbound/rcpt.rs
@@ -70,8 +70,87 @@ impl<T: AsyncWrite + AsyncRead + Unpin> Session<T> {
dsn_info: to.orcpt,
};
+ if self.data.rcpt_to.contains(&rcpt) {
+ return self.write(b"250 2.1.5 OK\r\n").await;
+ }
+ self.data.rcpt_to.push(rcpt);
+
+ // Address rewriting and Sieve filtering
+ let rcpt_script = self
+ .core
+ .session
+ .config
+ .rcpt
+ .script
+ .eval(self)
+ .await
+ .clone();
+ if rcpt_script.is_some() || !self.core.session.config.rcpt.rewrite.is_empty() {
+ // Sieve filtering
+ if let Some(script) = rcpt_script {
+ match self.run_script(script.clone(), None).await {
+ ScriptResult::Accept { modifications } => {
+ if !modifications.is_empty() {
+ tracing::debug!(parent: &self.span,
+ context = "sieve",
+ event = "modify",
+ address = self.data.rcpt_to.last().unwrap().address,
+ modifications = ?modifications);
+ self.data.apply_sieve_modifications(modifications);
+ }
+ }
+ ScriptResult::Reject(message) => {
+ tracing::debug!(parent: &self.span,
+ context = "sieve",
+ event = "reject",
+ address = self.data.rcpt_to.last().unwrap().address,
+ reason = message);
+ self.data.rcpt_to.pop();
+ return self.write(message.as_bytes()).await;
+ }
+ _ => (),
+ }
+ }
+
+ // Address rewriting
+ if let Some(new_address) = self
+ .core
+ .session
+ .config
+ .rcpt
+ .rewrite
+ .eval_and_capture(self)
+ .await
+ .into_value()
+ {
+ let mut rcpt = self.data.rcpt_to.last_mut().unwrap();
+ if new_address.contains('@') {
+ rcpt.address_lcase = new_address.to_lowercase();
+ rcpt.domain = rcpt.address_lcase.domain_part().to_string();
+ rcpt.address = new_address.into_owned();
+ }
+ }
+
+ // Check for duplicates
+ let rcpt = self.data.rcpt_to.last().unwrap();
+ if self.data.rcpt_to.iter().filter(|r| r == &rcpt).count() > 1 {
+ self.data.rcpt_to.pop();
+ return self.write(b"250 2.1.5 OK\r\n").await;
+ }
+ }
+
// Verify address
- if let Some(directory) = &self.params.rcpt_directory {
+ let rcpt = self.data.rcpt_to.last().unwrap();
+ if let Some(directory) = self
+ .core
+ .session
+ .config
+ .rcpt
+ .directory
+ .eval_and_capture(self)
+ .await
+ .into_value()
+ {
if let Ok(is_local_domain) = directory.is_local_domain(&rcpt.domain).await {
if is_local_domain {
if let Ok(is_local_address) = directory.rcpt(&rcpt.address_lcase).await {
@@ -81,6 +160,8 @@ impl<T: AsyncWrite + AsyncRead + Unpin> Session<T> {
event = "error",
address = &rcpt.address_lcase,
"Mailbox does not exist.");
+
+ self.data.rcpt_to.pop();
return self
.rcpt_error(b"550 5.1.2 Mailbox does not exist.\r\n")
.await;
@@ -91,6 +172,8 @@ impl<T: AsyncWrite + AsyncRead + Unpin> Session<T> {
event = "error",
address = &rcpt.address_lcase,
"Temporary address verification failure.");
+
+ self.data.rcpt_to.pop();
return self
.write(b"451 4.4.3 Unable to verify address at this time.\r\n")
.await;
@@ -101,6 +184,8 @@ impl<T: AsyncWrite + AsyncRead + Unpin> Session<T> {
event = "error",
address = &rcpt.address_lcase,
"Relay not allowed.");
+
+ self.data.rcpt_to.pop();
return self.rcpt_error(b"550 5.1.2 Relay not allowed.\r\n").await;
}
} else {
@@ -110,6 +195,7 @@ impl<T: AsyncWrite + AsyncRead + Unpin> Session<T> {
address = &rcpt.address_lcase,
"Temporary address verification failure.");
+ self.data.rcpt_to.pop();
return self
.write(b"451 4.4.3 Unable to verify address at this time.\r\n")
.await;
@@ -120,36 +206,21 @@ impl<T: AsyncWrite + AsyncRead + Unpin> Session<T> {
event = "error",
address = &rcpt.address_lcase,
"Relay not allowed.");
+
+ self.data.rcpt_to.pop();
return self.rcpt_error(b"550 5.1.2 Relay not allowed.\r\n").await;
}
- if !self.data.rcpt_to.contains(&rcpt) {
- self.data.rcpt_to.push(rcpt);
-
- // Sieve filtering
- if let Some(script) = &self.params.rcpt_script {
- if let ScriptResult::Reject(message) = self.run_script(script.clone(), None).await {
- tracing::debug!(parent: &self.span,
- context = "rcpt",
- event = "sieve-reject",
- address = &self.data.rcpt_to.last().unwrap().address,
- reason = message);
- self.data.rcpt_to.pop();
- return self.write(message.as_bytes()).await;
- }
- }
-
- if self.is_allowed().await {
- tracing::debug!(parent: &self.span,
+ if self.is_allowed().await {
+ tracing::debug!(parent: &self.span,
context = "rcpt",
event = "success",
address = &self.data.rcpt_to.last().unwrap().address);
- } else {
- self.data.rcpt_to.pop();
- return self
- .write(b"451 4.4.5 Rate limit exceeded, try again later.\r\n")
- .await;
- }
+ } else {
+ self.data.rcpt_to.pop();
+ return self
+ .write(b"451 4.4.5 Rate limit exceeded, try again later.\r\n")
+ .await;
}
self.write(b"250 2.1.5 OK\r\n").await
diff --git a/crates/smtp/src/inbound/vrfy.rs b/crates/smtp/src/inbound/vrfy.rs
index ce266177..ffd171a0 100644
--- a/crates/smtp/src/inbound/vrfy.rs
+++ b/crates/smtp/src/inbound/vrfy.rs
@@ -29,7 +29,16 @@ use std::fmt::Write;
impl<T: AsyncWrite + AsyncRead + Unpin> Session<T> {
pub async fn handle_vrfy(&mut self, address: String) -> Result<(), ()> {
- match &self.params.rcpt_directory {
+ match self
+ .core
+ .session
+ .config
+ .rcpt
+ .directory
+ .eval_and_capture(self)
+ .await
+ .into_value()
+ {
Some(address_lookup) if self.params.can_vrfy => {
match address_lookup.vrfy(&address.to_lowercase()).await {
Ok(values) if !values.is_empty() => {
@@ -81,7 +90,16 @@ impl<T: AsyncWrite + AsyncRead + Unpin> Session<T> {
}
pub async fn handle_expn(&mut self, address: String) -> Result<(), ()> {
- match &self.params.rcpt_directory {
+ match self
+ .core
+ .session
+ .config
+ .rcpt
+ .directory
+ .eval_and_capture(self)
+ .await
+ .into_value()
+ {
Some(address_lookup) if self.params.can_expn => {
match address_lookup.expn(&address.to_lowercase()).await {
Ok(values) if !values.is_empty() => {
diff --git a/crates/smtp/src/reporting/mod.rs b/crates/smtp/src/reporting/mod.rs
index 966c8ffb..d562a43a 100644
--- a/crates/smtp/src/reporting/mod.rs
+++ b/crates/smtp/src/reporting/mod.rs
@@ -36,7 +36,7 @@ use mail_parser::DateTime;
use tokio::io::{AsyncRead, AsyncWrite};
use crate::{
- config::{AddressMatch, AggregateFrequency, DkimSigner, IfBlock},
+ config::{AddressMatch, AggregateFrequency, DkimSigner, IfBlock, MaybeDynValue},
core::{management, Session, SMTP},
outbound::{dane::Tlsa, mta_sts::Policy},
queue::{DomainPart, Message},
@@ -129,7 +129,7 @@ impl SMTP {
from_addr: &str,
rcpts: impl Iterator<Item = impl AsRef<str>>,
report: Vec<u8>,
- sign_config: &IfBlock<Vec<Arc<DkimSigner>>>,
+ sign_config: &IfBlock<Vec<MaybeDynValue<DkimSigner>>>,
span: &tracing::Span,
deliver_now: bool,
) {
@@ -178,11 +178,11 @@ impl SMTP {
impl Message {
pub async fn sign(
&mut self,
- config: &IfBlock<Vec<Arc<DkimSigner>>>,
+ config: &IfBlock<Vec<MaybeDynValue<DkimSigner>>>,
bytes: &[u8],
span: &tracing::Span,
) -> Option<Vec<u8>> {
- let signers = config.eval(self).await;
+ let signers = config.eval_and_capture(self).await.into_value();
if !signers.is_empty() {
let mut headers = Vec::with_capacity(64);
for signer in signers.iter() {
diff --git a/crates/utils/src/config/dynvalue.rs b/crates/utils/src/config/dynvalue.rs
new file mode 100644
index 00000000..f1c473f1
--- /dev/null
+++ b/crates/utils/src/config/dynvalue.rs
@@ -0,0 +1,148 @@
+/*
+ * Copyright (c) 2023 Stalwart Labs Ltd.
+ *
+ * This file is part of Stalwart Mail Server.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ * in the LICENSE file at the top-level directory of this distribution.
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * You can be released from the requirements of the AGPLv3 license by
+ * purchasing a commercial license. Please contact licensing@stalw.art
+ * for more details.
+*/
+
+use std::borrow::Cow;
+
+use super::{
+ utils::{AsKey, ParseValue},
+ DynValue,
+};
+
+impl ParseValue for DynValue {
+ #[allow(clippy::while_let_on_iterator)]
+ fn parse_value(key: impl AsKey, value: &str) -> super::Result<Self> {
+ let mut items = vec![];
+ let mut buf = vec![];
+ let mut iter = value.as_bytes().iter().peekable();
+
+ while let Some(&ch) = iter.next() {
+ if ch == b'$' && matches!(iter.peek(), Some(b'{')) {
+ iter.next();
+ if matches!(iter.peek(), Some(ch) if ch.is_ascii_digit()) {
+ if !buf.is_empty() {
+ items.push(DynValue::String(String::from_utf8(buf).unwrap()));
+ buf = vec![];
+ }
+
+ while let Some(&ch) = iter.next() {
+ if ch.is_ascii_digit() {
+ buf.push(ch);
+ } else if ch == b'}' && !buf.is_empty() {
+ let str_num = std::str::from_utf8(&buf).unwrap();
+ items.push(DynValue::Position(str_num.parse().map_err(|_| {
+ format!(
+ "Failed to parse position {str_num:?} in value {value:?} for key {}",
+ key.as_key()
+ )
+ })?));
+ buf.clear();
+ break;
+ } else {
+ return Err(format!(
+ "Invalid dynamic string {value:?} for key {}",
+ key.as_key()
+ ));
+ }
+ }
+ } else {
+ buf.push(b'$');
+ buf.push(b'{');
+ }
+ } else {
+ buf.push(ch);
+ }
+ }
+
+ if !buf.is_empty() {
+ let item = DynValue::String(String::from_utf8(buf).unwrap());
+ if !items.is_empty() {
+ items.push(item);
+ } else {
+ return Ok(item);
+ }
+ }
+
+ Ok(match items.len() {
+ 0 => DynValue::String(String::new()),
+ 1 => items.pop().unwrap(),
+ _ => DynValue::List(items),
+ })
+ }
+}
+
+impl DynValue {
+ pub fn apply(&self, captures: Vec<String>) -> Cow<str> {
+ match self {
+ DynValue::String(value) => Cow::Borrowed(value.as_str()),
+ DynValue::Position(pos) => captures
+ .into_iter()
+ .nth(*pos)
+ .map(Cow::Owned)
+ .unwrap_or(Cow::Borrowed("")),
+ DynValue::List(items) => {
+ let mut result = String::new();
+
+ for item in items {
+ match item {
+ DynValue::String(value) => result.push_str(value),
+ DynValue::Position(pos) => {
+ if let Some(capture) = captures.get(*pos) {
+ result.push_str(capture);
+ }
+ }
+ DynValue::List(_) => unreachable!(),
+ }
+ }
+
+ Cow::Owned(result)
+ }
+ }
+ }
+
+ pub fn apply_borrowed<'x, 'y: 'x>(&'x self, captures: &'y [String]) -> Cow<'x, str> {
+ match self {
+ DynValue::String(value) => Cow::Borrowed(value.as_str()),
+ DynValue::Position(pos) => captures
+ .get(*pos)
+ .map(|v| Cow::Borrowed(v.as_str()))
+ .unwrap_or(Cow::Borrowed("")),
+ DynValue::List(items) => {
+ let mut result = String::new();
+
+ for item in items {
+ match item {
+ DynValue::String(value) => result.push_str(value),
+ DynValue::Position(pos) => {
+ if let Some(capture) = captures.get(*pos) {
+ result.push_str(capture);
+ }
+ }
+ DynValue::List(_) => unreachable!(),
+ }
+ }
+
+ Cow::Owned(result)
+ }
+ }
+ }
+}
diff --git a/crates/utils/src/config/mod.rs b/crates/utils/src/config/mod.rs
index e3ebe52e..d45cbc68 100644
--- a/crates/utils/src/config/mod.rs
+++ b/crates/utils/src/config/mod.rs
@@ -22,6 +22,7 @@
*/
pub mod certificate;
+pub mod dynvalue;
pub mod listener;
pub mod parser;
pub mod utils;
@@ -74,6 +75,13 @@ pub enum ServerProtocol {
ManageSieve,
}
+#[derive(Debug, Clone)]
+pub enum DynValue {
+ String(String),
+ Position(usize),
+ List(Vec<DynValue>),
+}
+
#[derive(Debug, Default, PartialEq, Eq, Clone)]
pub struct Rate {
pub requests: u64,
diff --git a/crates/utils/src/config/utils.rs b/crates/utils/src/config/utils.rs
index 9bf7239d..ab95862e 100644
--- a/crates/utils/src/config/utils.rs
+++ b/crates/utils/src/config/utils.rs
@@ -177,6 +177,23 @@ impl Config {
Err(format!("Property {key:?} not found in configuration file."))
}
}
+
+ pub fn text_file_contents(&self, key: impl AsKey) -> super::Result<Option<String>> {
+ let key = key.as_key();
+ if let Some(value) = self.keys.get(&key) {
+ if let Some(value) = value.strip_prefix("file://") {
+ std::fs::read_to_string(value)
+ .map_err(|err| {
+ format!("Failed to read file {value:?} for property {key:?}: {err}")
+ })
+ .map(Some)
+ } else {
+ Ok(Some(value.to_string()))
+ }
+ } else {
+ Ok(None)
+ }
+ }
}
pub trait ParseValues: Sized + Default {