mirror of
https://github.com/stalwartlabs/mail-server.git
synced 2024-10-23 06:57:26 +00:00
v0.3.2
This commit is contained in:
parent
4f2f673baa
commit
3cea77b65e
58 changed files with 1696 additions and 368 deletions
2
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
2
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
|
@ -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
|
||||
|
|
53
CHANGELOG
53
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).
|
||||
# Change Log
|
||||
|
||||
stalwart-mail v0.3.0
|
||||
================================
|
||||
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
|
||||
|
||||
|
|
67
Cargo.lock
generated
67
Cargo.lock
generated
|
@ -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]]
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"] }
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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?;
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "imap"
|
||||
version = "0.3.1"
|
||||
version = "0.3.2"
|
||||
edition = "2021"
|
||||
resolver = "2"
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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\"");
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "jmap"
|
||||
version = "0.3.1"
|
||||
version = "0.3.2"
|
||||
edition = "2021"
|
||||
resolver = "2"
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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")]
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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}")
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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")?,
|
||||
},
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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<'_>,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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() => {
|
||||
|
|
|
@ -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() {
|
||||
|
|
148
crates/utils/src/config/dynvalue.rs
Normal file
148
crates/utils/src/config/dynvalue.rs
Normal file
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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]]
|
||||
|
|
|
@ -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__"
|
||||
|
||||
|
|
88
tests/resources/smtp/config/rules-dynvalue.toml
Normal file
88
tests/resources/smtp/config/rules-dynvalue.toml
Normal file
|
@ -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"
|
||||
|
|
@ -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:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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},
|
||||
|
|
|
@ -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},
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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(&[]));
|
||||
|
|
169
tests/src/smtp/inbound/rewrite.rs
Normal file
169
tests/src/smtp/inbound/rewrite.rs
Normal file
|
@ -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"
|
||||
);
|
||||
}
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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},
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in a new issue