summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormdecimus <mauro@stalw.art>2023-07-27 20:18:34 +0200
committermdecimus <mauro@stalw.art>2023-07-27 20:18:34 +0200
commit3cea77b65e2863fc9a8e5f6b492b81292dc22843 (patch)
tree132d08058163a509789d007e61cc98d36ba1dd49
parent4f2f673baa65899c134d420d4a6375258a25f67c (diff)
v0.3.2
-rw-r--r--.github/ISSUE_TEMPLATE/feature_request.yml2
-rw-r--r--CHANGELOG55
-rw-r--r--Cargo.lock67
-rw-r--r--Dockerfile4
-rw-r--r--README.md3
-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
-rw-r--r--resources/config/directory.toml6
-rw-r--r--resources/config/smtp.toml10
-rw-r--r--tests/resources/smtp/config/rules-dynvalue.toml88
-rw-r--r--tests/src/directory/mod.rs50
-rw-r--r--tests/src/smtp/config.rs237
-rw-r--r--tests/src/smtp/inbound/auth.rs4
-rw-r--r--tests/src/smtp/inbound/data.rs6
-rw-r--r--tests/src/smtp/inbound/dmarc.rs20
-rw-r--r--tests/src/smtp/inbound/milter.rs10
-rw-r--r--tests/src/smtp/inbound/mod.rs1
-rw-r--r--tests/src/smtp/inbound/rcpt.rs6
-rw-r--r--tests/src/smtp/inbound/rewrite.rs169
-rw-r--r--tests/src/smtp/inbound/sign.rs16
-rw-r--r--tests/src/smtp/inbound/vrfy.rs6
-rw-r--r--tests/src/smtp/lookup/sql.rs6
-rw-r--r--tests/src/smtp/mod.rs2
-rw-r--r--tests/src/smtp/queue/dsn.rs3
-rw-r--r--tests/src/smtp/reporting/dmarc.rs3
-rw-r--r--tests/src/smtp/reporting/tls.rs3
58 files changed, 1697 insertions, 369 deletions
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml
index 8d981279..6a033d93 100644
--- a/.github/ISSUE_TEMPLATE/feature_request.yml
+++ b/.github/ISSUE_TEMPLATE/feature_request.yml
@@ -23,7 +23,7 @@ body:
id: problem-related
attributes:
label: Is your feature request related to a problem?
- description: Wrote a clear and concise description of what the problem is.
+ description: Write a clear and concise description of what the problem is.
placeholder: Tell us what the problem is!
value: "I'm always frustrated when..."
- type: checkboxes
diff --git a/CHANGELOG b/CHANGELOG
index b5060307..8581af95 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,20 +1,51 @@
-stalwart-mail v0.3.1
-================================
-- Added: Milter filter support. Documentation is available [here](https://stalw.art/docs/smtp/filter/milter).
-- Added: Match IP address type using /0 mask (#16).
-- Fix: Support for OpenLDAP password hashing schemes between curly brackets (#8).
-- Fix: Add CA certificates to Docker runtime (#5).
-
-stalwart-mail v0.3.0
-================================
+# Change Log
+
+All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/).
+
+## [0.3.2] - 2023-07-28
+
+### Added
+- Sender and recipient address rewriting using regular expressions and sieve scripts.
+- Subaddressing and catch-all addresses using regular expressions (#10).
+- Dynamic variables in SMTP rules.
+
+### Changed
+- Added CLI to Docker container (#19).
+
+### Fixed
+- Workaround for a bug in `sqlx` that caused SQL time-outs (#15).
+- Support for ED25519 certificates in PEM files (#20).
+- Better handling of concurrent IMAP UID map modifications (#17).
+- LDAP domain lookups from SMTP rules.
+
+## [0.3.1] - 2023-07-22
+
+### Added
+- Milter filter support.
+- Match IP address type using /0 mask (#16).
+
+### Changed
+
+### Fixed
+- Support for OpenLDAP password hashing schemes between curly brackets (#8).
+- Add CA certificates to Docker runtime (#5).
+
+## [0.3.0] - 2023-07-16
+
+### Added
+- **LDAP** and **SQL** authentication.
+- **subaddressing** and **catch-all** addresses.
+- **S3-compatible** storage.
+
+### Changed
- Merged the `stalwart-jmap`, `stalwart-imap` and `stalwart-smtp` repositories into
`stalwart-mail`.
-- Added support for **LDAP** and **SQL** authentication.
-- Added support for **subaddressing** and **catch-all** addresses.
-- Added support for **S3-compatible** storage.
- Removed clustering module and replaced it with a **FoundationDB** backend option.
- Integrated Stalwart SMTP into Stalwart JMAP.
- Rewritten JMAP protocol parser.
- Rewritten store backend.
- Rewritten IMAP server to have direct access to the message store (no more IMAP proxy).
- Replaced `actix` with `hyper`.
+
+### Fixed
+
diff --git a/Cargo.lock b/Cargo.lock
index 98e266be..73d43bda 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -697,9 +697,9 @@ dependencies = [
[[package]]
name = "clap"
-version = "4.3.17"
+version = "4.3.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5b0827b011f6f8ab38590295339817b0d26f344aa4932c3ced71b45b0c54b4a9"
+checksum = "5fd304a20bff958a57f04c4e96a2e7594cc4490a0e809cbd48bb6437edaa452d"
dependencies = [
"clap_builder",
"clap_derive",
@@ -708,9 +708,9 @@ dependencies = [
[[package]]
name = "clap_builder"
-version = "4.3.17"
+version = "4.3.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9441b403be87be858db6a23edb493e7f694761acdc3343d5a0fcaafd304cbc9e"
+checksum = "01c6a3f08f1fe5662a35cfe393aec09c4df95f60ee93b7556505260f75eee9e1"
dependencies = [
"anstream",
"anstyle",
@@ -1040,6 +1040,7 @@ dependencies = [
"password-hash 0.5.0",
"pbkdf2 0.12.2",
"pwhash",
+ "regex",
"rustls 0.21.5",
"scrypt",
"sha1",
@@ -1150,9 +1151,9 @@ dependencies = [
[[package]]
name = "either"
-version = "1.8.1"
+version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91"
+checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07"
dependencies = [
"serde",
]
@@ -1921,7 +1922,7 @@ dependencies = [
[[package]]
name = "imap"
-version = "0.3.1"
+version = "0.3.2"
dependencies = [
"ahash 0.8.3",
"dashmap",
@@ -2085,7 +2086,7 @@ dependencies = [
[[package]]
name = "jmap"
-version = "0.3.1"
+version = "0.3.2"
dependencies = [
"aes-gcm",
"aes-gcm-siv",
@@ -2281,9 +2282,9 @@ dependencies = [
[[package]]
name = "libz-sys"
-version = "1.1.9"
+version = "1.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "56ee889ecc9568871456d42f603d6a0ce59ff328d291063a45cbdf0036baf6db"
+checksum = "d97137b25e321a73eef1418d1d5d2eda4d77e12813f8e6dead84bc52c5870a7b"
dependencies = [
"cc",
"pkg-config",
@@ -2391,7 +2392,7 @@ dependencies = [
[[package]]
name = "mail-server"
-version = "0.3.1"
+version = "0.3.2"
dependencies = [
"directory",
"imap",
@@ -3296,9 +3297,9 @@ dependencies = [
[[package]]
name = "quote"
-version = "1.0.31"
+version = "1.0.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5fe8a65d69dd0808184ebb5f836ab526bb259db23c657efa38711b1072ee47f0"
+checksum = "50f3b39ccfb720540debaa0164757101c08ecb8d326b15358ce76a62c7e85965"
dependencies = [
"proc-macro2",
]
@@ -3523,9 +3524,9 @@ dependencies = [
[[package]]
name = "roaring"
-version = "0.10.1"
+version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ef0fb5e826a8bde011ecae6a8539dd333884335c57ff0f003fbe27c25bbe8f71"
+checksum = "6106b5cf8587f5834158895e9715a3c6c9716c8aefab57f1f7680917191c7873"
dependencies = [
"bytemuck",
"byteorder",
@@ -3705,7 +3706,7 @@ checksum = "79ea77c539259495ce8ca47f53e66ae0330a8819f67e23ac96ca02f50e7b7d36"
dependencies = [
"log",
"ring",
- "rustls-webpki 0.101.1",
+ "rustls-webpki 0.101.2",
"sct",
]
@@ -3742,9 +3743,9 @@ dependencies = [
[[package]]
name = "rustls-webpki"
-version = "0.101.1"
+version = "0.101.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "15f36a6828982f422756984e47912a7a51dcbc2a197aa791158f8ca61cd8204e"
+checksum = "513722fd73ad80a71f72b61009ea1b584bcfa1483ca93949c8f290298837fa59"
dependencies = [
"ring",
"untrusted",
@@ -3833,9 +3834,9 @@ dependencies = [
[[package]]
name = "security-framework"
-version = "2.9.1"
+version = "2.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1fc758eb7bffce5b308734e9b0c1468893cae9ff70ebf13e7090be8dcbcc83a8"
+checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de"
dependencies = [
"bitflags 1.3.2",
"core-foundation",
@@ -3846,9 +3847,9 @@ dependencies = [
[[package]]
name = "security-framework-sys"
-version = "2.9.0"
+version = "2.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f51d0c0d83bec45f16480d0ce0058397a69e48fcdc52d1dc8855fb68acbd31a7"
+checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a"
dependencies = [
"core-foundation-sys",
"libc",
@@ -3856,9 +3857,9 @@ dependencies = [
[[package]]
name = "serde"
-version = "1.0.174"
+version = "1.0.176"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3b88756493a5bd5e5395d53baa70b194b05764ab85b59e43e4b8f4e1192fa9b1"
+checksum = "76dc28c9523c5d70816e393136b86d48909cfb27cecaa902d338c19ed47164dc"
dependencies = [
"serde_derive",
]
@@ -3874,9 +3875,9 @@ dependencies = [
[[package]]
name = "serde_derive"
-version = "1.0.174"
+version = "1.0.176"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6e5c3a298c7f978e53536f95a63bdc4c4a64550582f31a0359a9afda6aede62e"
+checksum = "a4e7b8c5dc823e3b90651ff1d3808419cd14e5ad76de04feaf37da114e7a306f"
dependencies = [
"proc-macro2",
"quote",
@@ -3885,9 +3886,9 @@ dependencies = [
[[package]]
name = "serde_json"
-version = "1.0.103"
+version = "1.0.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d03b412469450d4404fe8499a268edd7f8b79fecb074b0d812ad64ca21f4031b"
+checksum = "076066c5f1078eac5b722a31827a8832fe108bed65dfa75e233c89f8206e976c"
dependencies = [
"itoa",
"ryu",
@@ -4003,7 +4004,7 @@ checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3"
[[package]]
name = "sieve-rs"
version = "0.3.1"
-source = "git+https://github.com/stalwartlabs/sieve#0ab2dc8cd41ee5dadcc3ab5e932b9b92abc5e067"
+source = "git+https://github.com/stalwartlabs/sieve#f9c01ba6947d73855fdd645b17c9a5d347724ee3"
dependencies = [
"ahash 0.8.3",
"bincode",
@@ -4056,7 +4057,7 @@ checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9"
[[package]]
name = "smtp"
-version = "0.3.1"
+version = "0.3.2"
dependencies = [
"ahash 0.8.3",
"blake3",
@@ -4356,7 +4357,7 @@ dependencies = [
[[package]]
name = "stalwart-cli"
-version = "0.3.1"
+version = "0.3.2"
dependencies = [
"clap",
"console",
@@ -4378,7 +4379,7 @@ dependencies = [
[[package]]
name = "stalwart-install"
-version = "0.3.1"
+version = "0.3.2"
dependencies = [
"base64 0.21.2",
"clap",
@@ -5319,7 +5320,7 @@ version = "0.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b291546d5d9d1eab74f069c77749f2cb8504a12caa20f0f2de93ddbf6f411888"
dependencies = [
- "rustls-webpki 0.101.1",
+ "rustls-webpki 0.101.2",
]
[[package]]
diff --git a/Dockerfile b/Dockerfile
index 9e4745fb..5105ae97 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -17,6 +17,8 @@ RUN useradd stalwart-mail -s /sbin/nologin -M
RUN mkdir -p /opt/stalwart-mail
RUN chown stalwart-mail:stalwart-mail /opt/stalwart-mail
-EXPOSE 8080 25 587 465 8686 143 993 4190
+VOLUME [ "/opt/stalwart-mail" ]
+
+EXPOSE 8080 25 587 465 143 993 4190
ENTRYPOINT ["/bin/sh", "/usr/local/bin/entrypoint.sh"]
diff --git a/README.md b/README.md
index ff0377b5..26557b9a 100644
--- a/README.md
+++ b/README.md
@@ -24,8 +24,9 @@ Key features:
- **SMTP** server:
- Built-in [DMARC](https://datatracker.ietf.org/doc/html/rfc7489), [DKIM](https://datatracker.ietf.org/doc/html/rfc6376), [SPF](https://datatracker.ietf.org/doc/html/rfc7208) and [ARC](https://datatracker.ietf.org/doc/html/rfc8617) support for message authentication.
- Strong transport security through [DANE](https://datatracker.ietf.org/doc/html/rfc6698), [MTA-STS](https://datatracker.ietf.org/doc/html/rfc8461) and [SMTP TLS](https://datatracker.ietf.org/doc/html/rfc8460) reporting.
- - Inbound throttling and filtering with granular configuration rules, __sieve__ scripting and __milter__ integration.
+ - Inbound throttling and filtering with granular configuration rules, sieve scripting and milter integration.
- Virtual queues with delayed delivery, priority delivery, quotas, routing rules and throttling support.
+ - Envelope rewriting and message modification.
- **Flexible**:
- **LDAP** directory and **SQL** database authentication.
- Full-text search available in 17 languages.
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 {
diff --git a/resources/config/directory.toml b/resources/config/directory.toml
index e6a0470f..2899204e 100644
--- a/resources/config/directory.toml
+++ b/resources/config/directory.toml
@@ -8,7 +8,9 @@ address = "sqlite://__PATH__/data/accounts.sqlite3?mode=rwc"
[directory."sql".options]
catch-all = true
+#catch-all = { map = "(.+)@(.+)$", to = "info@${2}" }
subaddressing = true
+#subaddressing = { map = "^([^.]+)\.([^.]+)@(.+)$", to = "${2}@${3}" }
superuser-group = "superusers"
[directory."sql".pool]
@@ -52,7 +54,9 @@ ttl = {positive = '1h', negative = '10m'}
[directory."ldap".options]
catch-all = true
+#catch-all = { map = "(.+)@(.+)$", to = "info@${2}" }
subaddressing = true
+#subaddressing = { map = "^([^.]+)\.([^.]+)@(.+)$", to = "${2}@${3}" }
superuser-group = "superusers"
[directory."ldap".pool]
@@ -137,7 +141,9 @@ type = "memory"
[directory."memory".options]
catch-all = true
+#catch-all = { map = "(.+)@(.+)$", to = "info@${2}" }
subaddressing = true
+#subaddressing = { map = "^([^.]+)\.([^.]+)@(.+)$", to = "${2}@${3}" }
superuser-group = "superusers"
[[directory."memory".users]]
diff --git a/resources/config/smtp.toml b/resources/config/smtp.toml
index 6fb320e3..5cad996a 100644
--- a/resources/config/smtp.toml
+++ b/resources/config/smtp.toml
@@ -17,7 +17,7 @@ protocol = "smtp"
tls.implicit = true
[server.listener."management"]
-bind = ["127.0.0.1:8686"]
+bind = ["127.0.0.1:8080"]
protocol = "http"
[session]
@@ -66,11 +66,19 @@ wait = "5s"
[session.mail]
#script = "mail-from"
+#rewrite = [ { all-of = [ { if = "listener", ne = "smtp" },
+# { if = "rcpt", matches = "^([^.]+)@([^.]+)\.(.+)$"},
+# ], then = "${1}@${3}" },
+# { else = false } ]
[session.rcpt]
#script = "rcpt-to"
relay = [ { if = "authenticated-as", ne = "", then = true },
{ else = false } ]
+#rewrite = [ { all-of = [ { if = "rcpt-domain", in-list = "__SMTP_DIRECTORY__/domains" },
+# { if = "rcpt", matches = "^([^.]+)\.([^.]+)@(.+)$"},
+# ], then = "${1}+${2}@${3}" },
+# { else = false } ]
max-recipients = 25
directory = "__SMTP_DIRECTORY__"
diff --git a/tests/resources/smtp/config/rules-dynvalue.toml b/tests/resources/smtp/config/rules-dynvalue.toml
new file mode 100644
index 00000000..36951bac
--- /dev/null
+++ b/tests/resources/smtp/config/rules-dynvalue.toml
@@ -0,0 +1,88 @@
+
+[envelope]
+rcpt-domain = "foo.example.org"
+rcpt = "user@foo.example.org"
+sender-domain = "foo.net"
+sender = "bill@foo.net"
+local-ip = "192.168.9.3"
+remote-ip = "A:B:C::D:E"
+mx = "mx.somedomain.com"
+authenticated-as = "john@foobar.org"
+priority = -4
+listener = 123
+helo-domain = "hi-domain.net"
+
+[eval."eq"]
+test = [
+ {if = "sender", eq = "bill@foo.net", then = "${0}"},
+ {else = false}
+]
+expect = "bill@foo.net"
+
+[eval."starts-with"]
+test = [
+ {if = "rcpt-domain", starts-with = "foo", then = "${0}${{0}}"},
+ {else = false}
+]
+expect = "foo.example.org${{0}}"
+
+[eval."regex"]
+test = [
+ {if = "rcpt", matches = "^([^.]+)@([^.]+)\.(.+)$", then = "${1}+${2}@${3}"},
+ {else = false}
+]
+expect = "user+foo@example.org"
+
+[eval."regex-full"]
+test = [
+ {if = "rcpt", matches = "^([^.]+)@([^.]+)\.(.+)$", then = "${0}"},
+ {else = false}
+]
+expect = "user@foo.example.org"
+
+[eval."static-match"]
+test = [
+ {if = "authenticated-as", matches = "^([^.]+)@(.+)$", then = "hello world"},
+ {else = false}
+]
+expect = "hello world"
+
+[eval."no-match"]
+test = [
+ {if = "authenticated-as", matches = "^([^.]+)@([^.]+)\.(.+)$org", then = "${1}+${2}@${3}"},
+ {else = false}
+]
+expect = false
+
+[directory."list_mx"]
+type = "memory"
+[directory."list_mx".lookup]
+domains = ["mx"]
+
+[directory."list_foo"]
+type = "memory"
+[directory."list_foo".lookup]
+domains = ["foo"]
+
+[maybe-eval."dyn_mx"]
+test = [
+ {if = "mx", matches = "([^.]+)\.(.+)$", then = "list_${1}"},
+ {else = false}
+]
+expect = "mx"
+
+[maybe-eval."dyn_foo"]
+test = [
+ {if = "sender-domain", matches = "([^.]+)\.(.+)$", then = "list_${1}"},
+ {else = false}
+]
+expect = "foo"
+
+[maybe-eval."static_mx"]
+test = "list_mx"
+expect = "mx"
+
+[maybe-eval."static_foo"]
+test = "list_foo"
+expect = "foo"
+
diff --git a/tests/src/directory/mod.rs b/tests/src/directory/mod.rs
index 59686cad..f9b925a1 100644
--- a/tests/src/directory/mod.rs
+++ b/tests/src/directory/mod.rs
@@ -26,11 +26,11 @@ pub mod ldap;
pub mod smtp;
pub mod sql;
-use directory::{config::ConfigDirectory, DirectoryConfig};
+use directory::{config::ConfigDirectory, AddressMapping, DirectoryConfig};
use mail_send::Credentials;
use rustls::{Certificate, PrivateKey, ServerConfig};
use rustls_pemfile::{certs, pkcs8_private_keys};
-use std::{io::BufReader, sync::Arc};
+use std::{borrow::Cow, io::BufReader, sync::Arc};
use tokio_rustls::TlsAcceptor;
const CONFIG: &str = r#"
@@ -389,3 +389,49 @@ impl core::fmt::Debug for Item {
}
}
}
+
+#[test]
+fn address_mappings() {
+ const MAPPINGS: &str = r#"
+ [enable]
+ catch-all = true
+ subaddressing = true
+ expected-sub = "john.doe@example.org"
+ expected-catch = "@example.org"
+
+ [disable]
+ catch-all = false
+ subaddressing = false
+ expected-sub = "john.doe+alias@example.org"
+ expected-catch = false
+
+ [custom]
+ catch-all = { map = "(.+)@(.+)$", to = "info@${2}" }
+ subaddressing = { map = "^([^.]+)\.([^.]+)@(.+)$", to = "${2}@${3}" }
+ expected-sub = "doe+alias@example.org"
+ expected-catch = "info@example.org"
+ "#;
+
+ let config = utils::config::Config::parse(MAPPINGS).unwrap();
+ const ADDR: &str = "john.doe+alias@example.org";
+
+ for test in ["enable", "disable", "custom"] {
+ let catch_all = AddressMapping::from_config(&config, (test, "catch-all")).unwrap();
+ let subaddressing = AddressMapping::from_config(&config, (test, "subaddressing")).unwrap();
+
+ assert_eq!(
+ subaddressing.to_subaddress(ADDR),
+ config.value_require((test, "expected-sub")).unwrap(),
+ "failed subaddress for {test:?}"
+ );
+
+ assert_eq!(
+ catch_all.to_catch_all(ADDR),
+ config
+ .property_require::<Option<String>>((test, "expected-catch"))
+ .unwrap()
+ .map(Cow::Owned),
+ "failed catch-all for {test:?}"
+ );
+ }
+}
diff --git a/tests/src/smtp/config.rs b/tests/src/smtp/config.rs
index 46c029c9..a1b0b13d 100644
--- a/tests/src/smtp/config.rs
+++ b/tests/src/smtp/config.rs
@@ -21,11 +21,11 @@
* for more details.
*/
-use std::{fs, net::IpAddr, path::PathBuf, sync::Arc, time::Duration};
+use std::{borrow::Cow, fs, net::IpAddr, path::PathBuf, sync::Arc, time::Duration};
use tokio::net::TcpSocket;
-use utils::config::{Config, Listener, Rate, Server, ServerProtocol};
+use utils::config::{Config, DynValue, Listener, Rate, Server, ServerProtocol};
use ahash::{AHashMap, AHashSet};
use directory::{config::ConfigDirectory, Lookup};
@@ -41,6 +41,20 @@ use smtp::{
use super::add_test_certs;
+struct TestEnvelope {
+ pub local_ip: IpAddr,
+ pub remote_ip: IpAddr,
+ pub sender_domain: String,
+ pub sender: String,
+ pub rcpt_domain: String,
+ pub rcpt: String,
+ pub helo_domain: String,
+ pub authenticated_as: String,
+ pub mx: String,
+ pub listener_id: u16,
+ pub priority: i16,
+}
+
#[test]
fn parse_conditions() {
let mut file = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
@@ -520,18 +534,139 @@ fn parse_servers() {
}
}
-struct TestEnvelope {
- pub local_ip: IpAddr,
- pub remote_ip: IpAddr,
- pub sender_domain: String,
- pub sender: String,
- pub rcpt_domain: String,
- pub rcpt: String,
- pub helo_domain: String,
- pub authenticated_as: String,
- pub mx: String,
- pub listener_id: u16,
- pub priority: i16,
+#[tokio::test]
+async fn eval_if() {
+ let mut file = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
+ file.push("resources");
+ file.push("smtp");
+ file.push("config");
+ file.push("rules-eval.toml");
+
+ let config = Config::parse(&fs::read_to_string(file).unwrap()).unwrap();
+ let servers = vec![
+ Server {
+ id: "smtp".to_string(),
+ internal_id: 123,
+ ..Default::default()
+ },
+ Server {
+ id: "smtps".to_string(),
+ internal_id: 456,
+ ..Default::default()
+ },
+ ];
+ let mut context = ConfigContext::new(&servers);
+ context.directory = config.parse_directory().unwrap();
+ let conditions = config.parse_conditions(&context).unwrap();
+
+ let envelope = TestEnvelope::from_config(&config);
+
+ for (key, conditions) in conditions {
+ //println!("============= Testing {:?} ==================", key);
+ let (_, expected_result) = key.rsplit_once('-').unwrap();
+ assert_eq!(
+ IfBlock {
+ if_then: vec![IfThen {
+ conditions,
+ then: true
+ }],
+ default: false,
+ }
+ .eval(&envelope)
+ .await,
+ &expected_result.parse::<bool>().unwrap(),
+ "failed for {key:?}"
+ );
+ }
+}
+
+#[tokio::test]
+async fn eval_dynvalue() {
+ let mut file = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
+ file.push("resources");
+ file.push("smtp");
+ file.push("config");
+ file.push("rules-dynvalue.toml");
+
+ let config = Config::parse(&fs::read_to_string(file).unwrap()).unwrap();
+ let mut context = ConfigContext::new(&[]);
+ context.directory = config.parse_directory().unwrap();
+
+ let envelope = TestEnvelope::from_config(&config);
+
+ for test_name in config.sub_keys("eval") {
+ //println!("============= Testing {:?} ==================", key);
+ let if_block = config
+ .parse_if_block::<Option<DynValue>>(
+ ("eval", test_name, "test"),
+ &context,
+ &[
+ EnvelopeKey::Recipient,
+ EnvelopeKey::RecipientDomain,
+ EnvelopeKey::Sender,
+ EnvelopeKey::SenderDomain,
+ EnvelopeKey::AuthenticatedAs,
+ EnvelopeKey::Listener,
+ EnvelopeKey::RemoteIp,
+ EnvelopeKey::LocalIp,
+ EnvelopeKey::Priority,
+ EnvelopeKey::Mx,
+ ],
+ )
+ .unwrap()
+ .unwrap();
+ let expected = config
+ .property_require::<Option<String>>(("eval", test_name, "expect"))
+ .unwrap()
+ .map(Cow::Owned);
+
+ assert_eq!(
+ if_block.eval_and_capture(&envelope).await.into_value(),
+ expected,
+ "failed for test {test_name:?}"
+ );
+ }
+
+ for test_name in config.sub_keys("maybe-eval") {
+ //println!("============= Testing {:?} ==================", key);
+ let if_block = config
+ .parse_if_block::<Option<DynValue>>(
+ ("maybe-eval", test_name, "test"),
+ &context,
+ &[
+ EnvelopeKey::Recipient,
+ EnvelopeKey::RecipientDomain,
+ EnvelopeKey::Sender,
+ EnvelopeKey::SenderDomain,
+ EnvelopeKey::AuthenticatedAs,
+ EnvelopeKey::Listener,
+ EnvelopeKey::RemoteIp,
+ EnvelopeKey::LocalIp,
+ EnvelopeKey::Priority,
+ EnvelopeKey::Mx,
+ ],
+ )
+ .unwrap()
+ .unwrap()
+ .map_if_block(
+ &context.directory.directories,
+ ("maybe-eval", test_name, "test"),
+ "test",
+ )
+ .unwrap();
+ let expected = config
+ .value_require(("maybe-eval", test_name, "expect"))
+ .unwrap();
+
+ assert!(if_block
+ .eval_and_capture(&envelope)
+ .await
+ .into_value()
+ .unwrap()
+ .is_local_domain(expected)
+ .await
+ .unwrap());
+ }
}
impl Envelope for TestEnvelope {
@@ -580,62 +715,22 @@ impl Envelope for TestEnvelope {
}
}
-#[tokio::test]
-async fn eval_if() {
- let mut file = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
- file.push("resources");
- file.push("smtp");
- file.push("config");
- file.push("rules-eval.toml");
-
- let config = Config::parse(&fs::read_to_string(file).unwrap()).unwrap();
- let servers = vec![
- Server {
- id: "smtp".to_string(),
- internal_id: 123,
- ..Default::default()
- },
- Server {
- id: "smtps".to_string(),
- internal_id: 456,
- ..Default::default()
- },
- ];
- let mut context = ConfigContext::new(&servers);
- context.directory = config.parse_directory().unwrap();
- let conditions = config.parse_conditions(&context).unwrap();
-
- let envelope = TestEnvelope {
- local_ip: config.property_require("envelope.local-ip").unwrap(),
- remote_ip: config.property_require("envelope.remote-ip").unwrap(),
- sender_domain: config.property_require("envelope.sender-domain").unwrap(),
- sender: config.property_require("envelope.sender").unwrap(),
- rcpt_domain: config.property_require("envelope.rcpt-domain").unwrap(),
- rcpt: config.property_require("envelope.rcpt").unwrap(),
- authenticated_as: config
- .property_require("envelope.authenticated-as")
- .unwrap(),
- mx: config.property_require("envelope.mx").unwrap(),
- listener_id: config.property_require("envelope.listener").unwrap(),
- priority: config.property_require("envelope.priority").unwrap(),
- helo_domain: config.property_require("envelope.helo-domain").unwrap(),
- };
-
- for (key, conditions) in conditions {
- //println!("============= Testing {:?} ==================", key);
- let (_, expected_result) = key.rsplit_once('-').unwrap();
- assert_eq!(
- IfBlock {
- if_then: vec![IfThen {
- conditions,
- then: true
- }],
- default: false,
- }
- .eval(&envelope)
- .await,
- &expected_result.parse::<bool>().unwrap(),
- "failed for {key:?}"
- );
+impl TestEnvelope {
+ pub fn from_config(config: &Config) -> Self {
+ Self {
+ local_ip: config.property_require("envelope.local-ip").unwrap(),
+ remote_ip: config.property_require("envelope.remote-ip").unwrap(),
+ sender_domain: config.property_require("envelope.sender-domain").unwrap(),
+ sender: config.property_require("envelope.sender").unwrap(),
+ rcpt_domain: config.property_require("envelope.rcpt-domain").unwrap(),
+ rcpt: config.property_require("envelope.rcpt").unwrap(),
+ authenticated_as: config
+ .property_require("envelope.authenticated-as")
+ .unwrap(),
+ mx: config.property_require("envelope.mx").unwrap(),
+ listener_id: config.property_require("envelope.listener").unwrap(),
+ priority: config.property_require("envelope.priority").unwrap(),
+ helo_domain: config.property_require("envelope.helo-domain").unwrap(),
+ }
}
}
diff --git a/tests/src/smtp/inbound/auth.rs b/tests/src/smtp/inbound/auth.rs
index 8cf014d0..013cccd8 100644
--- a/tests/src/smtp/inbound/auth.rs
+++ b/tests/src/smtp/inbound/auth.rs
@@ -23,7 +23,7 @@
use directory::config::ConfigDirectory;
use smtp_proto::{AUTH_LOGIN, AUTH_PLAIN};
-use utils::config::Config;
+use utils::config::{Config, DynValue};
use crate::smtp::{
session::{TestSession, VerifyResponse},
@@ -68,7 +68,7 @@ async fn auth() {
.parse_if(&ctx);
config.directory = r"[{if = 'remote-ip', eq = '10.0.0.1', then = 'local'},
{else = false}]"
- .parse_if::<Option<String>>(&ctx)
+ .parse_if::<Option<DynValue>>(&ctx)
.map_if_block(&ctx.directory.directories, "", "")
.unwrap();
config.errors_max = r"[{if = 'remote-ip', eq = '10.0.0.1', then = 2},
diff --git a/tests/src/smtp/inbound/data.rs b/tests/src/smtp/inbound/data.rs
index 411cbbed..c8169160 100644
--- a/tests/src/smtp/inbound/data.rs
+++ b/tests/src/smtp/inbound/data.rs
@@ -30,7 +30,7 @@ use crate::smtp::{
ParseTestConfig, TestConfig, TestSMTP,
};
use smtp::{
- config::{ConfigContext, IfBlock},
+ config::{ConfigContext, IfBlock, MaybeDynValue},
core::{Session, SMTP},
};
@@ -81,7 +81,9 @@ async fn data() {
let mut qr = core.init_test_queue("smtp_data_test");
let directory = Config::parse(DIRECTORY).unwrap().parse_directory().unwrap();
let mut config = &mut core.session.config.rcpt;
- config.directory = IfBlock::new(Some(directory.directories.get("local").unwrap().clone()));
+ config.directory = IfBlock::new(Some(MaybeDynValue::Static(
+ directory.directories.get("local").unwrap().clone(),
+ )));
let mut config = &mut core.session.config;
config.data.add_auth_results = "[{if = 'remote-ip', eq = '10.0.0.3', then = true},
diff --git a/tests/src/smtp/inbound/dmarc.rs b/tests/src/smtp/inbound/dmarc.rs
index d070e294..eadc1eac 100644
--- a/tests/src/smtp/inbound/dmarc.rs
+++ b/tests/src/smtp/inbound/dmarc.rs
@@ -34,7 +34,7 @@ use mail_auth::{
report::DmarcResult,
spf::Spf,
};
-use utils::config::{Config, Rate};
+use utils::config::{Config, DynValue, Rate};
use crate::smtp::{
inbound::{sign::TextConfigContext, TestMessage, TestQueueEvent, TestReportingEvent},
@@ -42,7 +42,7 @@ use crate::smtp::{
ParseTestConfig, TestConfig, TestSMTP,
};
use smtp::{
- config::{AggregateFrequency, ConfigContext, IfBlock, VerifyStrategy},
+ config::{AggregateFrequency, ConfigContext, IfBlock, MaybeDynValue, VerifyStrategy},
core::{Session, SMTP},
};
@@ -134,7 +134,9 @@ async fn dmarc() {
let mut rr = core.init_test_report();
let directory = Config::parse(DIRECTORY).unwrap().parse_directory().unwrap();
let mut config = &mut core.session.config.rcpt;
- config.directory = IfBlock::new(Some(directory.directories.get("local").unwrap().clone()));
+ config.directory = IfBlock::new(Some(MaybeDynValue::Static(
+ directory.directories.get("local").unwrap().clone(),
+ )));
let mut config = &mut core.session.config;
config.data.add_auth_results = IfBlock::new(true);
@@ -166,11 +168,17 @@ async fn dmarc() {
let mut config = &mut core.report.config;
config.spf.sign = "['rsa']"
- .parse_if::<Vec<String>>(&ctx)
+ .parse_if::<Vec<DynValue>>(&ctx)
+ .map_if_block(&ctx.signers, "", "")
+ .unwrap();
+ config.dmarc.sign = "['rsa']"
+ .parse_if::<Vec<DynValue>>(&ctx)
+ .map_if_block(&ctx.signers, "", "")
+ .unwrap();
+ config.dkim.sign = "['rsa']"
+ .parse_if::<Vec<DynValue>>(&ctx)
.map_if_block(&ctx.signers, "", "")
.unwrap();
- config.dmarc.sign = config.spf.sign.clone();
- config.dkim.sign = config.spf.sign.clone();
// SPF must pass
let core = Arc::new(core);
diff --git a/tests/src/smtp/inbound/milter.rs b/tests/src/smtp/inbound/milter.rs
index d9622678..fad22580 100644
--- a/tests/src/smtp/inbound/milter.rs
+++ b/tests/src/smtp/inbound/milter.rs
@@ -212,7 +212,7 @@ fn milter_address_modifications() {
// ChangeFrom
assert!(data
- .apply_modifications(
+ .apply_milter_modifications(
vec![Modification::ChangeFrom {
sender: "<>".to_string(),
args: String::new()
@@ -227,7 +227,7 @@ fn milter_address_modifications() {
// ChangeFrom with parameters
assert!(data
- .apply_modifications(
+ .apply_milter_modifications(
vec![Modification::ChangeFrom {
sender: "john@example.org".to_string(),
args: "REQUIRETLS ENVID=abc123".to_string(), //"NOTIFY=SUCCESS,FAILURE ENVID=abc123\n".to_string()
@@ -242,7 +242,7 @@ fn milter_address_modifications() {
// Add recipients
assert!(data
- .apply_modifications(
+ .apply_milter_modifications(
vec![
Modification::AddRcpt {
recipient: "bill@example.org".to_string(),
@@ -276,7 +276,7 @@ fn milter_address_modifications() {
// Remove recipients
assert!(data
- .apply_modifications(
+ .apply_milter_modifications(
vec![
Modification::DeleteRcpt {
recipient: "bill@example.org".to_string(),
@@ -319,7 +319,7 @@ fn milter_message_modifications() {
test.result,
String::from_utf8(
session_data
- .apply_modifications(test.modifications, &parsed_test_message)
+ .apply_milter_modifications(test.modifications, &parsed_test_message)
.unwrap()
)
.unwrap()
diff --git a/tests/src/smtp/inbound/mod.rs b/tests/src/smtp/inbound/mod.rs
index eb0fad89..0032c91f 100644
--- a/tests/src/smtp/inbound/mod.rs
+++ b/tests/src/smtp/inbound/mod.rs
@@ -42,6 +42,7 @@ pub mod limits;
pub mod mail;
pub mod milter;
pub mod rcpt;
+pub mod rewrite;
pub mod scripts;
pub mod sign;
pub mod throttle;
diff --git a/tests/src/smtp/inbound/rcpt.rs b/tests/src/smtp/inbound/rcpt.rs
index 3441f183..80b50deb 100644
--- a/tests/src/smtp/inbound/rcpt.rs
+++ b/tests/src/smtp/inbound/rcpt.rs
@@ -32,7 +32,7 @@ use crate::smtp::{
ParseTestConfig, TestConfig,
};
use smtp::{
- config::{ConfigContext, IfBlock},
+ config::{ConfigContext, IfBlock, MaybeDynValue},
core::{Session, State, SMTP},
};
@@ -75,7 +75,9 @@ async fn rcpt() {
let mut config_ext = &mut core.session.config.extensions;
let directory = Config::parse(DIRECTORY).unwrap().parse_directory().unwrap();
let mut config = &mut core.session.config.rcpt;
- config.directory = IfBlock::new(Some(directory.directories.get("local").unwrap().clone()));
+ config.directory = IfBlock::new(Some(MaybeDynValue::Static(
+ directory.directories.get("local").unwrap().clone(),
+ )));
config.max_recipients = r"[{if = 'remote-ip', eq = '10.0.0.1', then = 3},
{else = 5}]"
.parse_if(&ConfigContext::new(&[]));
diff --git a/tests/src/smtp/inbound/rewrite.rs b/tests/src/smtp/inbound/rewrite.rs
new file mode 100644
index 00000000..eb895f58
--- /dev/null
+++ b/tests/src/smtp/inbound/rewrite.rs
@@ -0,0 +1,169 @@
+/*
+ * 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 crate::smtp::{inbound::sign::TextConfigContext, session::TestSession, TestConfig};
+use directory::config::ConfigDirectory;
+use smtp::{
+ config::{if_block::ConfigIf, scripts::ConfigSieve, ConfigContext, EnvelopeKey, IfBlock},
+ core::{Session, SMTP},
+};
+use utils::config::{Config, DynValue};
+
+const CONFIG: &str = r#"
+[session.mail]
+rewrite = [ { all-of = [ { if = "sender-domain", ends-with = ".foobar.net" },
+ { if = "sender", matches = "^([^.]+)@([^.]+)\.(.+)$"},
+ ], then = "${1}+${2}@${3}" },
+ { else = false } ]
+script = [ { if = "sender-domain", eq = "foobar.org", then = "mail" },
+ { else = false } ]
+
+[session.rcpt]
+rewrite = [ { all-of = [ { if = "rcpt-domain", eq = "foobar.net" },
+ { if = "rcpt", matches = "^([^.]+)\.([^.]+)@(.+)$"},
+ ], then = "${1}+${2}@${3}" },
+ { else = false } ]
+script = [ { if = "rcpt-domain", eq = "foobar.org", then = "rcpt" },
+ { else = false } ]
+
+[sieve]
+from-name = "Sieve Daemon"
+from-addr = "sieve@foobar.org"
+return-path = ""
+hostname = "mx.foobar.org"
+
+[sieve.limits]
+redirects = 3
+out-messages = 5
+received-headers = 50
+cpu = 10000
+nested-includes = 5
+duplicate-expiry = "7d"
+
+[sieve.scripts]
+mail = '''
+require ["variables", "envelope"];
+
+if allof( envelope :domain :is "from" "foobar.org",
+ envelope :localpart :contains "from" "admin" ) {
+ set "envelope.from" "MAILER-DAEMON@foobar.org";
+}
+
+'''
+
+rcpt = '''
+require ["variables", "envelope", "regex"];
+
+if allof( envelope :localpart :contains "to" ".",
+ envelope :regex "to" "(.+)@(.+)$") {
+ set :replace "." "" "to" "${1}";
+ set "envelope.to" "${to}@${2}";
+}
+
+'''
+
+"#;
+
+#[tokio::test]
+async fn address_rewrite() {
+ /*tracing::subscriber::set_global_default(
+ tracing_subscriber::FmtSubscriber::builder()
+ .with_max_level(tracing::Level::TRACE)
+ .finish(),
+ )
+ .unwrap();*/
+
+ // Prepare config
+ let available_keys = [
+ EnvelopeKey::Sender,
+ EnvelopeKey::SenderDomain,
+ EnvelopeKey::Recipient,
+ EnvelopeKey::RecipientDomain,
+ ];
+ let mut core = SMTP::test();
+ let mut ctx = ConfigContext::new(&[]).parse_signatures();
+ let settings = Config::parse(CONFIG).unwrap();
+ ctx.directory = settings.parse_directory().unwrap();
+ core.sieve = settings.parse_sieve(&mut ctx).unwrap();
+ let config = &mut core.session.config;
+ config.mail.script = settings
+ .parse_if_block::<Option<String>>("session.mail.script", &ctx, &available_keys)
+ .unwrap()
+ .unwrap_or_default()
+ .map_if_block(&ctx.scripts, "session.mail.script", "script")
+ .unwrap();
+ config.mail.rewrite = settings
+ .parse_if_block::<Option<DynValue>>("session.mail.rewrite", &ctx, &available_keys)
+ .unwrap()
+ .unwrap_or_default();
+ config.rcpt.script = settings
+ .parse_if_block::<Option<String>>("session.rcpt.script", &ctx, &available_keys)
+ .unwrap()
+ .unwrap_or_default()
+ .map_if_block(&ctx.scripts, "session.rcpt.script", "script")
+ .unwrap();
+ config.rcpt.rewrite = settings
+ .parse_if_block::<Option<DynValue>>("session.rcpt.rewrite", &ctx, &available_keys)
+ .unwrap()
+ .unwrap_or_default();
+ config.rcpt.relay = IfBlock::new(true);
+
+ // Init session
+ let mut session = Session::test(core);
+ session.data.remote_ip = "10.0.0.1".parse().unwrap();
+ session.eval_session_params().await;
+ session.ehlo("mx.doe.org").await;
+
+ // Sender rewrite using regex
+ session.mail_from("bill@doe.foobar.net", "250").await;
+ assert_eq!(
+ session.data.mail_from.as_ref().unwrap().address,
+ "bill+doe@foobar.net"
+ );
+ session.reset();
+
+ // Sender rewrite using sieve
+ session.mail_from("this_is_admin@foobar.org", "250").await;
+ assert_eq!(
+ session.data.mail_from.as_ref().unwrap().address_lcase,
+ "mailer-daemon@foobar.org"
+ );
+
+ // Recipient rewrite using regex
+ session.rcpt_to("mary.smith@foobar.net", "250").await;
+ assert_eq!(
+ session.data.rcpt_to.last().unwrap().address,
+ "mary+smith@foobar.net"
+ );
+
+ // Remove duplicates
+ session.rcpt_to("mary.smith@foobar.net", "250").await;
+ assert_eq!(session.data.rcpt_to.len(), 1);
+
+ // Recipient rewrite using sieve
+ session.rcpt_to("m.a.r.y.s.m.i.t.h@foobar.org", "250").await;
+ assert_eq!(
+ session.data.rcpt_to.last().unwrap().address,
+ "marysmith@foobar.org"
+ );
+}
diff --git a/tests/src/smtp/inbound/sign.rs b/tests/src/smtp/inbound/sign.rs
index 11c70cbe..6da52cc4 100644
--- a/tests/src/smtp/inbound/sign.rs
+++ b/tests/src/smtp/inbound/sign.rs
@@ -28,7 +28,7 @@ use mail_auth::{
common::{parse::TxtRecordParser, verify::DomainKey},
spf::Spf,
};
-use utils::config::Config;
+use utils::config::{Config, DynValue};
use crate::smtp::{
inbound::{TestMessage, TestQueueEvent},
@@ -36,7 +36,7 @@ use crate::smtp::{
ParseTestConfig, TestConfig, TestSMTP,
};
use smtp::{
- config::{auth::ConfigAuth, ConfigContext, IfBlock, VerifyStrategy},
+ config::{auth::ConfigAuth, ConfigContext, IfBlock, MaybeDynValue, VerifyStrategy},
core::{Session, SMTP},
};
@@ -81,7 +81,9 @@ report = true
[signature.ed]
public-key = '11qYAYKxCrfVS/7TyWQHOg7hcvPapiMlrwIaaPcHURo='
-private-key = 'nWGxne/9WmC6hEr0kuwsxERJxWl7MmkZcDusAxyuf2A='
+private-key = '-----BEGIN PRIVATE KEY-----
+nWGxne/9WmC6hEr0kuwsxERJxWl7MmkZcDusAxyuf2A=
+-----END PRIVATE KEY-----'
domain = 'example.com'
selector = 'ed'
headers = ['From', 'To', 'Date', 'Subject', 'Message-ID']
@@ -152,7 +154,9 @@ async fn sign_and_seal() {
let directory = Config::parse(DIRECTORY).unwrap().parse_directory().unwrap();
let mut config = &mut core.session.config.rcpt;
- config.directory = IfBlock::new(Some(directory.directories.get("local").unwrap().clone()));
+ config.directory = IfBlock::new(Some(MaybeDynValue::Static(
+ directory.directories.get("local").unwrap().clone(),
+ )));
let mut config = &mut core.session.config;
config.data.add_auth_results = IfBlock::new(true);
@@ -170,11 +174,11 @@ async fn sign_and_seal() {
config.arc.verify = config.spf.verify_ehlo.clone();
config.dmarc.verify = config.spf.verify_ehlo.clone();
config.dkim.sign = "['rsa']"
- .parse_if::<Vec<String>>(&ctx)
+ .parse_if::<Vec<DynValue>>(&ctx)
.map_if_block(&ctx.signers, "", "")
.unwrap();
config.arc.seal = "'ed'"
- .parse_if::<Option<String>>(&ctx)
+ .parse_if::<Option<DynValue>>(&ctx)
.map_if_block(&ctx.sealers, "", "")
.unwrap();
diff --git a/tests/src/smtp/inbound/vrfy.rs b/tests/src/smtp/inbound/vrfy.rs
index 3acbb6bb..d88414dc 100644
--- a/tests/src/smtp/inbound/vrfy.rs
+++ b/tests/src/smtp/inbound/vrfy.rs
@@ -29,7 +29,7 @@ use crate::smtp::{
ParseTestConfig, TestConfig,
};
use smtp::{
- config::{ConfigContext, IfBlock},
+ config::{ConfigContext, IfBlock, MaybeDynValue},
core::{Session, SMTP},
};
@@ -67,7 +67,9 @@ async fn vrfy_expn() {
let directory = Config::parse(DIRECTORY).unwrap().parse_directory().unwrap();
let mut config = &mut core.session.config.rcpt;
- config.directory = IfBlock::new(Some(directory.directories.get("local").unwrap().clone()));
+ config.directory = IfBlock::new(Some(MaybeDynValue::Static(
+ directory.directories.get("local").unwrap().clone(),
+ )));
let mut config = &mut core.session.config.extensions;
config.vrfy = r"[{if = 'remote-ip', eq = '10.0.0.1', then = true},
diff --git a/tests/src/smtp/lookup/sql.rs b/tests/src/smtp/lookup/sql.rs
index 6efe9d3c..8a672074 100644
--- a/tests/src/smtp/lookup/sql.rs
+++ b/tests/src/smtp/lookup/sql.rs
@@ -25,7 +25,7 @@ use std::time::Duration;
use directory::config::ConfigDirectory;
use smtp_proto::{AUTH_LOGIN, AUTH_PLAIN};
-use utils::config::Config;
+use utils::config::{Config, DynValue};
use crate::{
directory::sql::{create_test_directory, create_test_user_with_email, link_test_address},
@@ -115,7 +115,7 @@ async fn lookup_sql() {
// Enable AUTH
let mut config = &mut core.session.config.auth;
config.directory = r"'sql'"
- .parse_if::<Option<String>>(&ctx)
+ .parse_if::<Option<DynValue>>(&ctx)
.map_if_block(&ctx.directory.directories, "", "")
.unwrap();
config.mechanisms = IfBlock::new(AUTH_PLAIN | AUTH_LOGIN);
@@ -124,7 +124,7 @@ async fn lookup_sql() {
// Enable VRFY/EXPN/RCPT
let mut config = &mut core.session.config.rcpt;
config.directory = r"'sql'"
- .parse_if::<Option<String>>(&ctx)
+ .parse_if::<Option<DynValue>>(&ctx)
.map_if_block(&ctx.directory.directories, "", "")
.unwrap();
config.relay = IfBlock::new(false);
diff --git a/tests/src/smtp/mod.rs b/tests/src/smtp/mod.rs
index dc7539d8..6189a353 100644
--- a/tests/src/smtp/mod.rs
+++ b/tests/src/smtp/mod.rs
@@ -239,6 +239,7 @@ impl TestConfig for SessionConfig {
},
mail: Mail {
script: IfBlock::new(None),
+ rewrite: IfBlock::new(None),
},
rcpt: Rcpt {
script: IfBlock::new(None),
@@ -247,6 +248,7 @@ impl TestConfig for SessionConfig {
errors_max: IfBlock::new(3),
errors_wait: IfBlock::new(Duration::from_secs(1)),
max_recipients: IfBlock::new(3),
+ rewrite: IfBlock::new(None),
},
data: Data {
script: IfBlock::new(None),
diff --git a/tests/src/smtp/queue/dsn.rs b/tests/src/smtp/queue/dsn.rs
index f1768dad..cdc8c328 100644
--- a/tests/src/smtp/queue/dsn.rs
+++ b/tests/src/smtp/queue/dsn.rs
@@ -29,6 +29,7 @@ use std::{
use smtp_proto::{Response, RCPT_NOTIFY_DELAY, RCPT_NOTIFY_FAILURE, RCPT_NOTIFY_SUCCESS};
use tokio::{fs::File, io::AsyncReadExt};
+use utils::config::DynValue;
use crate::smtp::{
inbound::{sign::TextConfigContext, TestQueueEvent},
@@ -109,7 +110,7 @@ async fn generate_dsn() {
let ctx = ConfigContext::new(&[]).parse_signatures();
let mut config = &mut core.queue.config.dsn;
config.sign = "['rsa']"
- .parse_if::<Vec<String>>(&ctx)
+ .parse_if::<Vec<DynValue>>(&ctx)
.map_if_block(&ctx.signers, "", "")
.unwrap();
diff --git a/tests/src/smtp/reporting/dmarc.rs b/tests/src/smtp/reporting/dmarc.rs
index 779604b7..7f929476 100644
--- a/tests/src/smtp/reporting/dmarc.rs
+++ b/tests/src/smtp/reporting/dmarc.rs
@@ -32,6 +32,7 @@ use mail_auth::{
dmarc::Dmarc,
report::{ActionDisposition, Disposition, DmarcResult, Record, Report},
};
+use utils::config::DynValue;
use crate::smtp::{
inbound::{sign::TextConfigContext, TestMessage, TestQueueEvent},
@@ -66,7 +67,7 @@ async fn report_dmarc() {
config.path = IfBlock::new(temp_dir.temp_dir.clone());
config.hash = IfBlock::new(16);
config.dmarc_aggregate.sign = "['rsa']"
- .parse_if::<Vec<String>>(&ctx)
+ .parse_if::<Vec<DynValue>>(&ctx)
.map_if_block(&ctx.signers, "", "")
.unwrap();
config.dmarc_aggregate.max_size = IfBlock::new(4096);
diff --git a/tests/src/smtp/reporting/tls.rs b/tests/src/smtp/reporting/tls.rs
index fb72b8f1..2e213009 100644
--- a/tests/src/smtp/reporting/tls.rs
+++ b/tests/src/smtp/reporting/tls.rs
@@ -29,6 +29,7 @@ use mail_auth::{
mta_sts::TlsRpt,
report::tlsrpt::{FailureDetails, PolicyType, ResultType, TlsReport},
};
+use utils::config::DynValue;
use crate::smtp::{
inbound::{sign::TextConfigContext, TestMessage, TestQueueEvent},
@@ -63,7 +64,7 @@ async fn report_tls() {
config.path = IfBlock::new(temp_dir.temp_dir.clone());
config.hash = IfBlock::new(16);
config.tls.sign = "['rsa']"
- .parse_if::<Vec<String>>(&ctx)
+ .parse_if::<Vec<DynValue>>(&ctx)
.map_if_block(&ctx.signers, "", "")
.unwrap();
config.tls.max_size = IfBlock::new(4096);