mirror of
https://github.com/stalwartlabs/mail-server.git
synced 2024-11-28 09:07:32 +00:00
Use safe defaults when settings are missing
This commit is contained in:
parent
cb4d2f15ae
commit
35562bb9fd
120 changed files with 11732 additions and 2069 deletions
|
@ -4,8 +4,10 @@ use directory::Directory;
|
|||
use utils::config::{utils::AsKey, Config};
|
||||
|
||||
use crate::{
|
||||
config::smtp::{session::AddressMapping, V_RECIPIENT},
|
||||
expr::{functions::ResolveVariable, if_block::IfBlock, tokenizer::TokenMap, Variable},
|
||||
config::smtp::session::AddressMapping,
|
||||
expr::{
|
||||
functions::ResolveVariable, if_block::IfBlock, tokenizer::TokenMap, Variable, V_RECIPIENT,
|
||||
},
|
||||
Core,
|
||||
};
|
||||
|
||||
|
@ -130,7 +132,7 @@ impl AddressMapping {
|
|||
} else if let Some(if_block) = IfBlock::try_parse(
|
||||
config,
|
||||
key,
|
||||
&TokenMap::default().with_variables([
|
||||
&TokenMap::default().with_variables_map([
|
||||
("address", V_RECIPIENT),
|
||||
("email", V_RECIPIENT),
|
||||
("rcpt", V_RECIPIENT),
|
||||
|
@ -138,7 +140,7 @@ impl AddressMapping {
|
|||
) {
|
||||
AddressMapping::Custom(if_block)
|
||||
} else {
|
||||
AddressMapping::Disable
|
||||
AddressMapping::Enable
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,8 +39,12 @@ impl ImapConfig {
|
|||
timeout_idle: config
|
||||
.property_or_default("imap.timeout.idle", "30m")
|
||||
.unwrap_or_else(|| Duration::from_secs(1800)),
|
||||
rate_requests: config.property_or_default("imap.rate-limit.requests", "2000/1m"),
|
||||
rate_concurrent: config.property("imap.rate-limit.concurrent"),
|
||||
rate_requests: config
|
||||
.property_or_default::<Option<Rate>>("imap.rate-limit.requests", "2000/1m")
|
||||
.unwrap_or_default(),
|
||||
rate_concurrent: config
|
||||
.property::<Option<u64>>("imap.rate-limit.concurrent")
|
||||
.unwrap_or_default(),
|
||||
allow_plain_auth: config
|
||||
.property_or_default("imap.auth.allow-plain-text", "false")
|
||||
.unwrap_or(false),
|
||||
|
|
|
@ -144,9 +144,15 @@ impl JmapConfig {
|
|||
session_cache_ttl: config
|
||||
.property("cache.session.ttl")
|
||||
.unwrap_or(Duration::from_secs(3600)),
|
||||
rate_authenticated: config.property_or_default("jmap.rate-limit.account", "1000/1m"),
|
||||
rate_authenticate_req: config.property_or_default("authentication.rate-limit", "10/1m"),
|
||||
rate_anonymous: config.property_or_default("jmap.rate-limit.anonymous", "100/1m"),
|
||||
rate_authenticated: config
|
||||
.property_or_default::<Option<Rate>>("jmap.rate-limit.account", "1000/1m")
|
||||
.unwrap_or_default(),
|
||||
rate_authenticate_req: config
|
||||
.property_or_default::<Option<Rate>>("authentication.rate-limit", "10/1m")
|
||||
.unwrap_or_default(),
|
||||
rate_anonymous: config
|
||||
.property_or_default::<Option<Rate>>("jmap.rate-limit.anonymous", "100/1m")
|
||||
.unwrap_or_default(),
|
||||
oauth_key: config
|
||||
.value("oauth.key")
|
||||
.map(|s| s.to_string())
|
||||
|
|
|
@ -5,7 +5,7 @@ use directory::{Directories, Directory};
|
|||
use store::{BlobBackend, BlobStore, FtsStore, LookupStore, Store, Stores};
|
||||
use utils::config::Config;
|
||||
|
||||
use crate::{listener::tls::TlsManager, Core, Network};
|
||||
use crate::{expr::*, listener::tls::TlsManager, Core, Network};
|
||||
|
||||
use self::{
|
||||
imap::ImapConfig, jmap::settings::JmapConfig, manager::ConfigManager, scripts::Scripting,
|
||||
|
@ -22,6 +22,16 @@ pub mod smtp;
|
|||
pub mod storage;
|
||||
pub mod tracers;
|
||||
|
||||
pub(crate) const CONNECTION_VARS: &[u32; 7] = &[
|
||||
V_LISTENER,
|
||||
V_REMOTE_IP,
|
||||
V_REMOTE_PORT,
|
||||
V_LOCAL_IP,
|
||||
V_LOCAL_PORT,
|
||||
V_PROTOCOL,
|
||||
V_TLS,
|
||||
];
|
||||
|
||||
impl Core {
|
||||
pub async fn parse(config: &mut Config, stores: Stores, config_manager: ConfigManager) -> Self {
|
||||
let mut data = config
|
||||
|
@ -78,7 +88,7 @@ impl Core {
|
|||
}
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let directories = Directories::parse(config, &stores, data.clone()).await;
|
||||
let mut directories = Directories::parse(config, &stores, data.clone()).await;
|
||||
let directory = config
|
||||
.value_require("storage.directory")
|
||||
.map(|id| id.to_string())
|
||||
|
@ -94,6 +104,9 @@ impl Core {
|
|||
}
|
||||
})
|
||||
.unwrap_or_else(|| Arc::new(Directory::default()));
|
||||
directories
|
||||
.directories
|
||||
.insert("*".to_string(), directory.clone());
|
||||
|
||||
// If any of the stores are missing, disable all stores to avoid data loss
|
||||
if matches!(data, Store::None)
|
||||
|
|
|
@ -6,14 +6,17 @@ use crate::{
|
|||
Network,
|
||||
};
|
||||
|
||||
use super::smtp::*;
|
||||
use super::CONNECTION_VARS;
|
||||
|
||||
impl Default for Network {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
blocked_ips: Default::default(),
|
||||
hostname: IfBlock::new("localhost".to_string()),
|
||||
url: IfBlock::new("http://localhost:8080".to_string()),
|
||||
url: IfBlock::new::<()>(
|
||||
"server.http.url",
|
||||
[],
|
||||
"protocol + '://' + key_get('default', 'hostname') + ':' + local_port",
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -24,17 +27,9 @@ impl Network {
|
|||
blocked_ips: BlockedIps::parse(config),
|
||||
..Default::default()
|
||||
};
|
||||
let token_map = &TokenMap::default().with_smtp_variables(&[
|
||||
V_LISTENER,
|
||||
V_REMOTE_IP,
|
||||
V_LOCAL_IP,
|
||||
V_HELO_DOMAIN,
|
||||
]);
|
||||
let token_map = &TokenMap::default().with_variables(CONNECTION_VARS);
|
||||
|
||||
for (value, key) in [
|
||||
(&mut network.hostname, "server.hostname"),
|
||||
(&mut network.url, "server.url"),
|
||||
] {
|
||||
for (value, key) in [(&mut network.url, "server.url")] {
|
||||
if let Some(if_block) = IfBlock::try_parse(config, key, token_map) {
|
||||
*value = if_block;
|
||||
}
|
||||
|
|
|
@ -13,16 +13,16 @@ use utils::config::Config;
|
|||
|
||||
use crate::scripts::{functions::register_functions, plugins::RegisterSievePlugins};
|
||||
|
||||
use super::smtp::parse_server_hostname;
|
||||
use super::{if_block::IfBlock, smtp::SMTP_RCPT_TO_VARS, tokenizer::TokenMap};
|
||||
|
||||
pub struct Scripting {
|
||||
pub untrusted_compiler: Compiler,
|
||||
pub untrusted_runtime: Runtime,
|
||||
pub trusted_runtime: Runtime,
|
||||
pub from_addr: String,
|
||||
pub from_name: String,
|
||||
pub return_path: String,
|
||||
pub sign: Vec<String>,
|
||||
pub from_addr: IfBlock,
|
||||
pub from_name: IfBlock,
|
||||
pub return_path: IfBlock,
|
||||
pub sign: IfBlock,
|
||||
pub scripts: AHashMap<String, Arc<Sieve>>,
|
||||
pub bayes_cache: BayesTokenCache,
|
||||
pub remote_lists: RwLock<AHashMap<String, RemoteList>>,
|
||||
|
@ -182,7 +182,7 @@ impl Scripting {
|
|||
.unwrap_or("Auto: ")
|
||||
.to_string(),
|
||||
)
|
||||
.with_env_variable("name", "Stalwart JMAP")
|
||||
.with_env_variable("name", "Stalwart Mail Server")
|
||||
.with_env_variable("version", env!("CARGO_PKG_VERSION"))
|
||||
.with_env_variable("location", "MS")
|
||||
.with_env_variable("phase", "during");
|
||||
|
@ -250,13 +250,11 @@ impl Scripting {
|
|||
if let Some(value) = config.property::<Duration>("sieve.trusted.limits.duplicate-expiry") {
|
||||
trusted_runtime.set_default_duplicate_expiry(value.as_secs());
|
||||
}
|
||||
let hostname = if let Some(hostname) = config.value("sieve.trusted.hostname") {
|
||||
hostname.to_string()
|
||||
} else {
|
||||
parse_server_hostname(config)
|
||||
.and_then(|h| h.into_default_string())
|
||||
.unwrap_or_else(|| "localhost".to_string())
|
||||
};
|
||||
let hostname = config
|
||||
.value("sieve.trusted.hostname")
|
||||
.or_else(|| config.value("lookup.default.hostname"))
|
||||
.unwrap_or("localhost")
|
||||
.to_string();
|
||||
trusted_runtime.set_local_hostname(hostname.clone());
|
||||
|
||||
// Parse scripts
|
||||
|
@ -266,19 +264,12 @@ impl Scripting {
|
|||
.map(|s| s.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
{
|
||||
// Skip sub-scripts
|
||||
if config
|
||||
.property(("sieve.trusted.scripts", id.as_str(), "snippet"))
|
||||
.unwrap_or(false)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
let script = config
|
||||
match trusted_compiler.compile(
|
||||
config
|
||||
.value(("sieve.trusted.scripts", id.as_str(), "contents"))
|
||||
.unwrap();
|
||||
|
||||
match trusted_compiler.compile(script.as_bytes()) {
|
||||
.unwrap()
|
||||
.as_bytes(),
|
||||
) {
|
||||
Ok(compiled) => {
|
||||
scripts.insert(id, compiled.into());
|
||||
}
|
||||
|
@ -289,26 +280,42 @@ impl Scripting {
|
|||
}
|
||||
}
|
||||
|
||||
let token_map = TokenMap::default().with_variables(SMTP_RCPT_TO_VARS);
|
||||
|
||||
Scripting {
|
||||
untrusted_compiler,
|
||||
untrusted_runtime,
|
||||
trusted_runtime,
|
||||
from_addr: config
|
||||
.value("sieve.trusted.from-addr")
|
||||
.map(|a| a.to_string())
|
||||
.unwrap_or(format!("MAILER-DAEMON@{hostname}")),
|
||||
from_name: config
|
||||
.value("sieve.trusted.from-name")
|
||||
.unwrap_or("Mailer Daemon")
|
||||
.to_string(),
|
||||
return_path: config
|
||||
.value("sieve.trusted.return-path")
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
sign: config
|
||||
.values("sieve.trusted.sign")
|
||||
.map(|(_, v)| v.to_string())
|
||||
.collect(),
|
||||
from_addr: IfBlock::try_parse(config, "sieve.trusted.from-addr", &token_map)
|
||||
.unwrap_or_else(|| {
|
||||
IfBlock::new::<()>(
|
||||
"sieve.trusted.from-addr",
|
||||
[],
|
||||
"'MAILER-DAEMON@' + key_get('default', 'domain')",
|
||||
)
|
||||
}),
|
||||
from_name: IfBlock::try_parse(config, "sieve.trusted.from-name", &token_map)
|
||||
.unwrap_or_else(|| {
|
||||
IfBlock::new::<()>(
|
||||
"sieve.trusted.from-name",
|
||||
[],
|
||||
"'Mailer Daemon'",
|
||||
)
|
||||
}),
|
||||
return_path: IfBlock::try_parse(config, "sieve.trusted.return-path", &token_map)
|
||||
.unwrap_or_else(|| {
|
||||
IfBlock::empty(
|
||||
"sieve.trusted.return-path",
|
||||
)
|
||||
}),
|
||||
sign: IfBlock::try_parse(config, "sieve.trusted.sign", &token_map)
|
||||
.unwrap_or_else(|| {
|
||||
IfBlock::new::<()>(
|
||||
"sieve.trusted.sign",
|
||||
[],
|
||||
"['rsa_' + key_get('default', 'domain'), 'ed_' + key_get('default', 'domain')]",
|
||||
)
|
||||
}),
|
||||
scripts,
|
||||
bayes_cache: BayesTokenCache::new(
|
||||
config
|
||||
|
@ -332,10 +339,18 @@ impl Default for Scripting {
|
|||
untrusted_compiler: Compiler::new(),
|
||||
untrusted_runtime: Runtime::new(),
|
||||
trusted_runtime: Runtime::new(),
|
||||
from_addr: "MAILER-DAEMON@localhost".to_string(),
|
||||
from_name: "Mailer Daemon".to_string(),
|
||||
return_path: "".to_string(),
|
||||
sign: Vec::new(),
|
||||
from_addr: IfBlock::new::<()>(
|
||||
"sieve.trusted.from-addr",
|
||||
[],
|
||||
"'MAILER-DAEMON@' + key_get('default', 'domain')",
|
||||
),
|
||||
from_name: IfBlock::new::<()>("sieve.trusted.from-name", [], "'Mailer Daemon'"),
|
||||
return_path: IfBlock::empty("sieve.trusted.return-path"),
|
||||
sign: IfBlock::new::<()>(
|
||||
"sieve.trusted.sign",
|
||||
[],
|
||||
"['rsa_' + key_get('default', 'domain'), 'ed_' + key_get('default', 'domain')]",
|
||||
),
|
||||
scripts: AHashMap::new(),
|
||||
bayes_cache: BayesTokenCache::new(
|
||||
8192,
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
* for more details.
|
||||
*/
|
||||
|
||||
use std::{net::SocketAddr, sync::Arc};
|
||||
use std::{net::SocketAddr, sync::Arc, time::Duration};
|
||||
|
||||
use rustls::{
|
||||
crypto::ring::{default_provider, ALL_CIPHER_SUITES},
|
||||
|
@ -144,23 +144,40 @@ impl Servers {
|
|||
}
|
||||
}
|
||||
|
||||
// Set default options
|
||||
if !config.contains_key(("server.listener", id, "socket.reuse-addr")) {
|
||||
let _ = socket.set_reuseaddr(true);
|
||||
}
|
||||
|
||||
listeners.push(Listener {
|
||||
socket,
|
||||
addr,
|
||||
ttl: config
|
||||
.property_or_else(("server.listener", id, "socket.ttl"), "server.socket.ttl"),
|
||||
backlog: config.property_or_else(
|
||||
.property_or_else::<Option<u32>>(
|
||||
("server.listener", id, "socket.ttl"),
|
||||
"server.socket.ttl",
|
||||
"false",
|
||||
)
|
||||
.unwrap_or_default(),
|
||||
backlog: config
|
||||
.property_or_else::<Option<u32>>(
|
||||
("server.listener", id, "socket.backlog"),
|
||||
"server.socket.backlog",
|
||||
),
|
||||
linger: config.property_or_else(
|
||||
"1024",
|
||||
)
|
||||
.unwrap_or_default(),
|
||||
linger: config
|
||||
.property_or_else::<Option<Duration>>(
|
||||
("server.listener", id, "socket.linger"),
|
||||
"server.socket.linger",
|
||||
),
|
||||
"false",
|
||||
)
|
||||
.unwrap_or_default(),
|
||||
nodelay: config
|
||||
.property_or_else(
|
||||
("server.listener", id, "socket.nodelay"),
|
||||
"server.socket.nodelay",
|
||||
"true",
|
||||
)
|
||||
.unwrap_or(true),
|
||||
});
|
||||
|
@ -190,6 +207,7 @@ impl Servers {
|
|||
.property_or_else(
|
||||
("server.listener", id, "max-connections"),
|
||||
"server.max-connections",
|
||||
"8192",
|
||||
)
|
||||
.unwrap_or(8192),
|
||||
id: id_,
|
||||
|
@ -218,8 +236,8 @@ impl Servers {
|
|||
let id = id_.as_str();
|
||||
// Build TLS config
|
||||
let acceptor = if config
|
||||
.property_or_else(("server.listener", id, "tls.enable"), "server.tls.enable")
|
||||
.unwrap_or(false)
|
||||
.property_or_default(("server.listener", id, "tls.enable"), "true")
|
||||
.unwrap_or(true)
|
||||
{
|
||||
// Parse protocol versions
|
||||
let mut tls_v2 = true;
|
||||
|
@ -292,6 +310,7 @@ impl Servers {
|
|||
.property_or_else(
|
||||
("server.listener", id, "tls.ignore-client-order"),
|
||||
"server.tls.ignore-client-order",
|
||||
"true",
|
||||
)
|
||||
.unwrap_or(true);
|
||||
|
||||
|
@ -302,11 +321,8 @@ impl Servers {
|
|||
acme_config: acme_config.clone(),
|
||||
default_config,
|
||||
implicit: config
|
||||
.property_or_else(
|
||||
("server.listener", id, "tls.implicit"),
|
||||
"server.tls.implicit",
|
||||
)
|
||||
.unwrap_or(true),
|
||||
.property_or_default(("server.listener", id, "tls.implicit"), "false")
|
||||
.unwrap_or(false),
|
||||
}
|
||||
} else {
|
||||
TcpAcceptor::Plain
|
||||
|
|
|
@ -46,14 +46,20 @@ pub enum ServerProtocol {
|
|||
ManageSieve,
|
||||
}
|
||||
|
||||
impl ServerProtocol {
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
ServerProtocol::Smtp => "smtp",
|
||||
ServerProtocol::Lmtp => "lmtp",
|
||||
ServerProtocol::Imap => "imap",
|
||||
ServerProtocol::Http => "http",
|
||||
ServerProtocol::ManageSieve => "managesieve",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ServerProtocol {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
ServerProtocol::Smtp => write!(f, "smtp"),
|
||||
ServerProtocol::Lmtp => write!(f, "lmtp"),
|
||||
ServerProtocol::Imap => write!(f, "imap"),
|
||||
ServerProtocol::Http => write!(f, "http"),
|
||||
ServerProtocol::ManageSieve => write!(f, "managesieve"),
|
||||
}
|
||||
f.write_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -105,6 +105,11 @@ impl TlsManager {
|
|||
.map(|(_, v)| v.to_string())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// This ACME manager is the default when SNI is not available
|
||||
let default = config
|
||||
.property::<bool>(("acme", acme_id.as_str(), "default"))
|
||||
.unwrap_or_default();
|
||||
|
||||
// Add domains for self-signed certificate
|
||||
subject_names.extend(domains.iter().cloned());
|
||||
|
||||
|
@ -115,6 +120,7 @@ impl TlsManager {
|
|||
domains,
|
||||
contact,
|
||||
renew_before,
|
||||
default,
|
||||
) {
|
||||
Ok(acme_provider) => {
|
||||
acme_providers.insert(acme_id.to_string(), acme_provider);
|
||||
|
|
|
@ -11,7 +11,10 @@ use utils::config::{
|
|||
Config,
|
||||
};
|
||||
|
||||
use crate::expr::{self, if_block::IfBlock, tokenizer::TokenMap, Constant, ConstantValue};
|
||||
use crate::{
|
||||
config::CONNECTION_VARS,
|
||||
expr::{self, if_block::IfBlock, tokenizer::TokenMap, Constant, ConstantValue},
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
|
@ -83,22 +86,62 @@ impl Default for MailAuthConfig {
|
|||
fn default() -> Self {
|
||||
Self {
|
||||
dkim: DkimAuthConfig {
|
||||
verify: IfBlock::new(VerifyStrategy::Relaxed),
|
||||
sign: Default::default(),
|
||||
verify: IfBlock::new::<VerifyStrategy>("auth.dkim.verify", [], "relaxed"),
|
||||
sign: IfBlock::new::<()>(
|
||||
"auth.dkim.sign",
|
||||
[(
|
||||
"local_port != 25",
|
||||
"['rsa_' + key_get('default', 'domain'), 'ed_' + key_get('default', 'domain')]",
|
||||
)],
|
||||
"false",
|
||||
),
|
||||
},
|
||||
arc: ArcAuthConfig {
|
||||
verify: IfBlock::new(VerifyStrategy::Relaxed),
|
||||
seal: Default::default(),
|
||||
verify: IfBlock::new::<VerifyStrategy>("auth.arc.verify", [], "relaxed"),
|
||||
seal: IfBlock::new::<()>(
|
||||
"auth.arc.seal",
|
||||
[],
|
||||
"['rsa_' + key_get('default', 'domain'), 'ed_' + key_get('default', 'domain')]",
|
||||
),
|
||||
},
|
||||
spf: SpfAuthConfig {
|
||||
verify_ehlo: IfBlock::new(VerifyStrategy::Relaxed),
|
||||
verify_mail_from: IfBlock::new(VerifyStrategy::Relaxed),
|
||||
verify_ehlo: IfBlock::new::<VerifyStrategy>(
|
||||
"auth.spf.verify.ehlo",
|
||||
[("local_port == 25", "relaxed")],
|
||||
#[cfg(not(feature = "test_mode"))]
|
||||
"disable",
|
||||
#[cfg(feature = "test_mode")]
|
||||
"relaxed",
|
||||
|
||||
),
|
||||
verify_mail_from: IfBlock::new::<VerifyStrategy>(
|
||||
"auth.spf.verify.mail-from",
|
||||
[("local_port == 25", "relaxed")],
|
||||
#[cfg(not(feature = "test_mode"))]
|
||||
"disable",
|
||||
#[cfg(feature = "test_mode")]
|
||||
"relaxed",
|
||||
),
|
||||
},
|
||||
dmarc: DmarcAuthConfig {
|
||||
verify: IfBlock::new(VerifyStrategy::Relaxed),
|
||||
verify: IfBlock::new::<VerifyStrategy>(
|
||||
"auth.dmarc.verify",
|
||||
[("local_port == 25", "relaxed")],
|
||||
#[cfg(not(feature = "test_mode"))]
|
||||
"disable",
|
||||
#[cfg(feature = "test_mode")]
|
||||
"relaxed",
|
||||
),
|
||||
},
|
||||
iprev: IpRevAuthConfig {
|
||||
verify: IfBlock::new(VerifyStrategy::Relaxed),
|
||||
verify: IfBlock::new::<VerifyStrategy>(
|
||||
"auth.ipref.verify",
|
||||
[("local_port == 25", "relaxed")],
|
||||
#[cfg(not(feature = "test_mode"))]
|
||||
"disable",
|
||||
#[cfg(feature = "test_mode")]
|
||||
"relaxed",
|
||||
),
|
||||
},
|
||||
signers: Default::default(),
|
||||
sealers: Default::default(),
|
||||
|
@ -108,27 +151,19 @@ impl Default for MailAuthConfig {
|
|||
|
||||
impl MailAuthConfig {
|
||||
pub fn parse(config: &mut Config) -> Self {
|
||||
let sender_vars = TokenMap::default()
|
||||
.with_smtp_variables(&[
|
||||
V_SENDER,
|
||||
V_SENDER_DOMAIN,
|
||||
V_PRIORITY,
|
||||
V_AUTHENTICATED_AS,
|
||||
V_LISTENER,
|
||||
V_REMOTE_IP,
|
||||
V_LOCAL_IP,
|
||||
])
|
||||
let rcpt_vars = TokenMap::default()
|
||||
.with_variables(SMTP_RCPT_TO_VARS)
|
||||
.with_constants::<VerifyStrategy>();
|
||||
let conn_vars = TokenMap::default()
|
||||
.with_smtp_variables(&[V_LISTENER, V_REMOTE_IP, V_LOCAL_IP])
|
||||
.with_variables(CONNECTION_VARS)
|
||||
.with_constants::<VerifyStrategy>();
|
||||
let mut mail_auth = Self::default();
|
||||
|
||||
for (value, key, token_map) in [
|
||||
(&mut mail_auth.dkim.verify, "auth.dkim.verify", &sender_vars),
|
||||
(&mut mail_auth.dkim.sign, "auth.dkim.sign", &sender_vars),
|
||||
(&mut mail_auth.arc.verify, "auth.arc.verify", &sender_vars),
|
||||
(&mut mail_auth.arc.seal, "auth.arc.seal", &sender_vars),
|
||||
(&mut mail_auth.dkim.verify, "auth.dkim.verify", &rcpt_vars),
|
||||
(&mut mail_auth.dkim.sign, "auth.dkim.sign", &rcpt_vars),
|
||||
(&mut mail_auth.arc.verify, "auth.arc.verify", &rcpt_vars),
|
||||
(&mut mail_auth.arc.seal, "auth.arc.seal", &rcpt_vars),
|
||||
(
|
||||
&mut mail_auth.spf.verify_ehlo,
|
||||
"auth.spf.verify.ehlo",
|
||||
|
@ -139,16 +174,8 @@ impl MailAuthConfig {
|
|||
"auth.spf.verify.mail-from",
|
||||
&conn_vars,
|
||||
),
|
||||
(
|
||||
&mut mail_auth.dmarc.verify,
|
||||
"auth.dmarc.verify",
|
||||
&sender_vars,
|
||||
),
|
||||
(
|
||||
&mut mail_auth.iprev.verify,
|
||||
"auth.iprev.verify",
|
||||
&sender_vars,
|
||||
),
|
||||
(&mut mail_auth.dmarc.verify, "auth.dmarc.verify", &rcpt_vars),
|
||||
(&mut mail_auth.iprev.verify, "auth.iprev.verify", &conn_vars),
|
||||
] {
|
||||
if let Some(if_block) = IfBlock::try_parse(config, key, token_map) {
|
||||
*value = if_block;
|
||||
|
|
|
@ -7,13 +7,15 @@ pub mod resolver;
|
|||
pub mod session;
|
||||
pub mod throttle;
|
||||
|
||||
use crate::expr::{if_block::IfBlock, tokenizer::TokenMap, Expression, ExpressionItem, Token};
|
||||
use crate::expr::{tokenizer::TokenMap, Expression};
|
||||
|
||||
use self::{
|
||||
auth::MailAuthConfig, queue::QueueConfig, report::ReportConfig, resolver::Resolvers,
|
||||
session::SessionConfig,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
pub struct SmtpConfig {
|
||||
pub session: SessionConfig,
|
||||
|
@ -43,30 +45,73 @@ pub const THROTTLE_REMOTE_IP: u16 = 1 << 7;
|
|||
pub const THROTTLE_LOCAL_IP: u16 = 1 << 8;
|
||||
pub const THROTTLE_HELO_DOMAIN: u16 = 1 << 9;
|
||||
|
||||
pub const V_RECIPIENT: u32 = 0;
|
||||
pub const V_RECIPIENT_DOMAIN: u32 = 1;
|
||||
pub const V_SENDER: u32 = 2;
|
||||
pub const V_SENDER_DOMAIN: u32 = 3;
|
||||
pub const V_MX: u32 = 4;
|
||||
pub const V_HELO_DOMAIN: u32 = 5;
|
||||
pub const V_AUTHENTICATED_AS: u32 = 6;
|
||||
pub const V_LISTENER: u32 = 7;
|
||||
pub const V_REMOTE_IP: u32 = 8;
|
||||
pub const V_LOCAL_IP: u32 = 9;
|
||||
pub const V_PRIORITY: u32 = 10;
|
||||
pub(crate) const RCPT_DOMAIN_VARS: &[u32; 1] = &[V_RECIPIENT_DOMAIN];
|
||||
|
||||
pub const VARIABLES_MAP: &[(&str, u32)] = &[
|
||||
("rcpt", V_RECIPIENT),
|
||||
("rcpt_domain", V_RECIPIENT_DOMAIN),
|
||||
("sender", V_SENDER),
|
||||
("sender_domain", V_SENDER_DOMAIN),
|
||||
("mx", V_MX),
|
||||
("helo_domain", V_HELO_DOMAIN),
|
||||
("authenticated_as", V_AUTHENTICATED_AS),
|
||||
("listener", V_LISTENER),
|
||||
("remote_ip", V_REMOTE_IP),
|
||||
("local_ip", V_LOCAL_IP),
|
||||
("priority", V_PRIORITY),
|
||||
pub(crate) const SMTP_EHLO_VARS: &[u32; 8] = &[
|
||||
V_LISTENER,
|
||||
V_REMOTE_IP,
|
||||
V_REMOTE_PORT,
|
||||
V_LOCAL_IP,
|
||||
V_LOCAL_PORT,
|
||||
V_PROTOCOL,
|
||||
V_TLS,
|
||||
V_HELO_DOMAIN,
|
||||
];
|
||||
pub(crate) const SMTP_MAIL_FROM_VARS: &[u32; 10] = &[
|
||||
V_LISTENER,
|
||||
V_REMOTE_IP,
|
||||
V_REMOTE_PORT,
|
||||
V_LOCAL_IP,
|
||||
V_LOCAL_PORT,
|
||||
V_PROTOCOL,
|
||||
V_TLS,
|
||||
V_SENDER,
|
||||
V_SENDER_DOMAIN,
|
||||
V_AUTHENTICATED_AS,
|
||||
];
|
||||
pub(crate) const SMTP_RCPT_TO_VARS: &[u32; 15] = &[
|
||||
V_SENDER,
|
||||
V_SENDER_DOMAIN,
|
||||
V_RECIPIENTS,
|
||||
V_RECIPIENT,
|
||||
V_RECIPIENT_DOMAIN,
|
||||
V_AUTHENTICATED_AS,
|
||||
V_LISTENER,
|
||||
V_REMOTE_IP,
|
||||
V_REMOTE_PORT,
|
||||
V_LOCAL_IP,
|
||||
V_LOCAL_PORT,
|
||||
V_PROTOCOL,
|
||||
V_TLS,
|
||||
V_PRIORITY,
|
||||
V_HELO_DOMAIN,
|
||||
];
|
||||
pub(crate) const SMTP_QUEUE_HOST_VARS: &[u32; 9] = &[
|
||||
V_SENDER,
|
||||
V_SENDER_DOMAIN,
|
||||
V_RECIPIENT_DOMAIN,
|
||||
V_RECIPIENT,
|
||||
V_RECIPIENTS,
|
||||
V_MX,
|
||||
V_PRIORITY,
|
||||
V_REMOTE_IP,
|
||||
V_LOCAL_IP,
|
||||
];
|
||||
pub(crate) const SMTP_QUEUE_RCPT_VARS: &[u32; 5] = &[
|
||||
V_RECIPIENT_DOMAIN,
|
||||
V_RECIPIENTS,
|
||||
V_SENDER,
|
||||
V_SENDER_DOMAIN,
|
||||
V_PRIORITY,
|
||||
];
|
||||
pub(crate) const SMTP_QUEUE_SENDER_VARS: &[u32; 3] = &[V_SENDER, V_SENDER_DOMAIN, V_PRIORITY];
|
||||
pub(crate) const SMTP_QUEUE_MX_VARS: &[u32; 6] = &[
|
||||
V_RECIPIENT_DOMAIN,
|
||||
V_RECIPIENTS,
|
||||
V_SENDER,
|
||||
V_SENDER_DOMAIN,
|
||||
V_PRIORITY,
|
||||
V_MX,
|
||||
];
|
||||
|
||||
impl SmtpConfig {
|
||||
|
@ -80,23 +125,3 @@ impl SmtpConfig {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TokenMap {
|
||||
pub fn with_smtp_variables(mut self, variables: &[u32]) -> Self {
|
||||
for (name, idx) in VARIABLES_MAP {
|
||||
if variables.contains(idx) {
|
||||
self.tokens.insert(name, Token::Variable(*idx));
|
||||
}
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn parse_server_hostname(config: &mut Config) -> Option<IfBlock> {
|
||||
IfBlock::try_parse(
|
||||
config,
|
||||
"server.hostname",
|
||||
&TokenMap::default().with_smtp_variables(&[V_LISTENER, V_REMOTE_IP, V_LOCAL_IP]),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
use std::time::Duration;
|
||||
|
||||
use ahash::AHashMap;
|
||||
use mail_auth::IpLookupStrategy;
|
||||
use mail_send::Credentials;
|
||||
|
@ -10,7 +8,7 @@ use utils::config::{
|
|||
|
||||
use crate::{
|
||||
config::server::ServerProtocol,
|
||||
expr::{if_block::IfBlock, Constant, ConstantValue, Expression, Variable},
|
||||
expr::{if_block::IfBlock, *},
|
||||
};
|
||||
|
||||
use self::throttle::{parse_throttle, parse_throttle_key};
|
||||
|
@ -121,38 +119,80 @@ pub enum RequireOptional {
|
|||
impl Default for QueueConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
retry: IfBlock::new(Duration::from_secs(5 * 60)),
|
||||
notify: IfBlock::new(Duration::from_secs(86400)),
|
||||
expire: IfBlock::new(Duration::from_secs(5 * 86400)),
|
||||
hostname: IfBlock::new("localhost".to_string()),
|
||||
next_hop: Default::default(),
|
||||
max_mx: IfBlock::new(5),
|
||||
max_multihomed: IfBlock::new(2),
|
||||
ip_strategy: IfBlock::new(IpLookupStrategy::Ipv4thenIpv6),
|
||||
retry: IfBlock::new::<()>(
|
||||
"queue.schedule.retry",
|
||||
[],
|
||||
"[2m, 5m, 10m, 15m, 30m, 1h, 2h]",
|
||||
),
|
||||
notify: IfBlock::new::<()>("queue.schedule.notify", [], "[1d, 3d]"),
|
||||
expire: IfBlock::new::<()>("queue.schedule.expire", [], "5d"),
|
||||
hostname: IfBlock::new::<()>(
|
||||
"queue.outbound.hostname",
|
||||
[],
|
||||
"key_get('default', 'hostname')",
|
||||
),
|
||||
next_hop: IfBlock::new::<()>(
|
||||
"queue.outbound.next-hop",
|
||||
#[cfg(not(feature = "test_mode"))]
|
||||
[("is_local_domain('*', rcpt_domain)", "'local'")],
|
||||
#[cfg(feature = "test_mode")]
|
||||
[],
|
||||
"false",
|
||||
),
|
||||
max_mx: IfBlock::new::<()>("queue.outbound.limits.mx", [], "5"),
|
||||
max_multihomed: IfBlock::new::<()>("queue.outbound.limits.multihomed", [], "2"),
|
||||
ip_strategy: IfBlock::new::<IpLookupStrategy>(
|
||||
"queue.outbound.ip-strategy",
|
||||
[],
|
||||
"ipv4_then_ipv6",
|
||||
),
|
||||
source_ip: QueueOutboundSourceIp {
|
||||
ipv4: Default::default(),
|
||||
ipv6: Default::default(),
|
||||
ipv4: IfBlock::empty("queue.outbound.source-ip.v4"),
|
||||
ipv6: IfBlock::empty("queue.outbound.source-ip.v6"),
|
||||
},
|
||||
tls: QueueOutboundTls {
|
||||
dane: IfBlock::new(RequireOptional::Optional),
|
||||
mta_sts: IfBlock::new(RequireOptional::Optional),
|
||||
start: IfBlock::new(RequireOptional::Optional),
|
||||
invalid_certs: IfBlock::new(false),
|
||||
dane: IfBlock::new::<RequireOptional>("queue.outbound.tls.dane", [], "optional"),
|
||||
mta_sts: IfBlock::new::<RequireOptional>(
|
||||
"queue.outbound.tls.mta-sts",
|
||||
[],
|
||||
"optional",
|
||||
),
|
||||
start: IfBlock::new::<RequireOptional>(
|
||||
"queue.outbound.tls.starttls",
|
||||
[],
|
||||
#[cfg(not(feature = "test_mode"))]
|
||||
"require",
|
||||
#[cfg(feature = "test_mode")]
|
||||
"optional",
|
||||
),
|
||||
invalid_certs: IfBlock::new::<()>(
|
||||
"queue.outbound.tls.allow-invalid-certs",
|
||||
[],
|
||||
"false",
|
||||
),
|
||||
},
|
||||
dsn: Dsn {
|
||||
name: IfBlock::new("Mail Delivery Subsystem".to_string()),
|
||||
address: IfBlock::new("MAILER-DAEMON@localhost".to_string()),
|
||||
sign: Default::default(),
|
||||
name: IfBlock::new::<()>("report.dsn.from-name", [], "'Mail Delivery Subsystem'"),
|
||||
address: IfBlock::new::<()>(
|
||||
"report.dsn.from-address",
|
||||
[],
|
||||
"'MAILER-DAEMON@' + key_get('default', 'domain')",
|
||||
),
|
||||
sign: IfBlock::new::<()>(
|
||||
"report.dsn.sign",
|
||||
[],
|
||||
"['rsa_' + key_get('default', 'domain'), 'ed_' + key_get('default', 'domain')]",
|
||||
),
|
||||
},
|
||||
timeout: QueueOutboundTimeout {
|
||||
connect: IfBlock::new(Duration::from_secs(5 * 60)),
|
||||
greeting: IfBlock::new(Duration::from_secs(5 * 60)),
|
||||
tls: IfBlock::new(Duration::from_secs(3 * 60)),
|
||||
ehlo: IfBlock::new(Duration::from_secs(5 * 60)),
|
||||
mail: IfBlock::new(Duration::from_secs(5 * 60)),
|
||||
rcpt: IfBlock::new(Duration::from_secs(5 * 60)),
|
||||
data: IfBlock::new(Duration::from_secs(10 * 60)),
|
||||
mta_sts: IfBlock::new(Duration::from_secs(10 * 60)),
|
||||
connect: IfBlock::new::<()>("queue.outbound.timeouts.connect", [], "5m"),
|
||||
greeting: IfBlock::new::<()>("queue.outbound.timeouts.greeting", [], "5m"),
|
||||
tls: IfBlock::new::<()>("queue.outbound.timeouts.tls", [], "3m"),
|
||||
ehlo: IfBlock::new::<()>("queue.outbound.timeouts.ehlo", [], "5m"),
|
||||
mail: IfBlock::new::<()>("queue.outbound.timeouts.mail-from", [], "5m"),
|
||||
rcpt: IfBlock::new::<()>("queue.outbound.timeouts.rcpt-to", [], "5m"),
|
||||
data: IfBlock::new::<()>("queue.outbound.timeouts.data", [], "10m"),
|
||||
mta_sts: IfBlock::new::<()>("queue.outbound.timeouts.mta-sts", [], "10m"),
|
||||
},
|
||||
throttle: QueueThrottle {
|
||||
sender: Default::default(),
|
||||
|
@ -172,39 +212,14 @@ impl Default for QueueConfig {
|
|||
impl QueueConfig {
|
||||
pub fn parse(config: &mut Config) -> Self {
|
||||
let mut queue = QueueConfig::default();
|
||||
let rcpt_vars = TokenMap::default().with_smtp_variables(&[
|
||||
V_RECIPIENT_DOMAIN,
|
||||
V_SENDER,
|
||||
V_SENDER_DOMAIN,
|
||||
V_PRIORITY,
|
||||
]);
|
||||
let sender_vars =
|
||||
TokenMap::default().with_smtp_variables(&[V_SENDER, V_SENDER_DOMAIN, V_PRIORITY]);
|
||||
let mx_vars = TokenMap::default().with_smtp_variables(&[
|
||||
V_RECIPIENT_DOMAIN,
|
||||
V_SENDER,
|
||||
V_SENDER_DOMAIN,
|
||||
V_PRIORITY,
|
||||
V_MX,
|
||||
]);
|
||||
let host_vars = TokenMap::default().with_smtp_variables(&[
|
||||
V_RECIPIENT_DOMAIN,
|
||||
V_SENDER,
|
||||
V_SENDER_DOMAIN,
|
||||
V_PRIORITY,
|
||||
V_LOCAL_IP,
|
||||
V_REMOTE_IP,
|
||||
V_MX,
|
||||
]);
|
||||
let rcpt_vars = TokenMap::default().with_variables(SMTP_QUEUE_RCPT_VARS);
|
||||
let sender_vars = TokenMap::default().with_variables(SMTP_QUEUE_SENDER_VARS);
|
||||
let mx_vars = TokenMap::default().with_variables(SMTP_QUEUE_MX_VARS);
|
||||
let host_vars = TokenMap::default().with_variables(SMTP_QUEUE_HOST_VARS);
|
||||
let ip_strategy_vars = sender_vars.clone().with_constants::<IpLookupStrategy>();
|
||||
let dane_vars = mx_vars.clone().with_constants::<RequireOptional>();
|
||||
let mta_sts_vars = rcpt_vars.clone().with_constants::<RequireOptional>();
|
||||
|
||||
// Parse default server hostname
|
||||
if let Some(hostname) = parse_server_hostname(config) {
|
||||
queue.hostname = hostname.into_default("queue.outbound.hostname");
|
||||
}
|
||||
|
||||
for (value, key, token_map) in [
|
||||
(&mut queue.retry, "queue.schedule.retry", &host_vars),
|
||||
(&mut queue.notify, "queue.schedule.notify", &rcpt_vars),
|
||||
|
@ -368,15 +383,7 @@ fn parse_queue_throttle(config: &mut Config) -> QueueThrottle {
|
|||
let all_throttles = parse_throttle(
|
||||
config,
|
||||
"queue.throttle",
|
||||
&TokenMap::default().with_smtp_variables(&[
|
||||
V_RECIPIENT_DOMAIN,
|
||||
V_SENDER,
|
||||
V_SENDER_DOMAIN,
|
||||
V_PRIORITY,
|
||||
V_MX,
|
||||
V_REMOTE_IP,
|
||||
V_LOCAL_IP,
|
||||
]),
|
||||
&TokenMap::default().with_variables(SMTP_QUEUE_HOST_VARS),
|
||||
THROTTLE_RCPT_DOMAIN
|
||||
| THROTTLE_SENDER
|
||||
| THROTTLE_SENDER_DOMAIN
|
||||
|
@ -487,22 +494,18 @@ fn parse_queue_quota_item(config: &mut Config, prefix: impl AsKey) -> Option<Que
|
|||
expr: Expression::try_parse(
|
||||
config,
|
||||
(prefix.as_str(), "match"),
|
||||
&TokenMap::default().with_smtp_variables(&[
|
||||
V_RECIPIENT,
|
||||
V_RECIPIENT_DOMAIN,
|
||||
V_SENDER,
|
||||
V_SENDER_DOMAIN,
|
||||
V_PRIORITY,
|
||||
]),
|
||||
&TokenMap::default().with_variables(SMTP_QUEUE_HOST_VARS),
|
||||
)
|
||||
.unwrap_or_default(),
|
||||
keys,
|
||||
size: config
|
||||
.property::<usize>((prefix.as_str(), "size"))
|
||||
.filter(|&v| v > 0),
|
||||
.property::<Option<usize>>((prefix.as_str(), "size"))
|
||||
.filter(|&v| v.as_ref().map_or(false, |v| *v > 0))
|
||||
.unwrap_or_default(),
|
||||
messages: config
|
||||
.property::<usize>((prefix.as_str(), "messages"))
|
||||
.filter(|&v| v > 0),
|
||||
.property::<Option<usize>>((prefix.as_str(), "messages"))
|
||||
.filter(|&v| v.as_ref().map_or(false, |v| *v > 0))
|
||||
.unwrap_or_default(),
|
||||
};
|
||||
|
||||
// Validate
|
||||
|
|
|
@ -63,41 +63,17 @@ pub enum AggregateFrequency {
|
|||
|
||||
impl ReportConfig {
|
||||
pub fn parse(config: &mut Config) -> Self {
|
||||
let sender_vars = TokenMap::default().with_smtp_variables(&[
|
||||
V_SENDER,
|
||||
V_SENDER_DOMAIN,
|
||||
V_PRIORITY,
|
||||
V_AUTHENTICATED_AS,
|
||||
V_LISTENER,
|
||||
V_REMOTE_IP,
|
||||
V_LOCAL_IP,
|
||||
]);
|
||||
let rcpt_vars = TokenMap::default().with_smtp_variables(&[
|
||||
V_SENDER,
|
||||
V_SENDER_DOMAIN,
|
||||
V_PRIORITY,
|
||||
V_REMOTE_IP,
|
||||
V_LOCAL_IP,
|
||||
V_RECIPIENT_DOMAIN,
|
||||
]);
|
||||
|
||||
let default_hostname_if_block = parse_server_hostname(config);
|
||||
let default_hostname = default_hostname_if_block
|
||||
.as_ref()
|
||||
.and_then(|i| i.default_string())
|
||||
.unwrap_or("localhost")
|
||||
.to_string();
|
||||
let sender_vars = TokenMap::default().with_variables(SMTP_MAIL_FROM_VARS);
|
||||
let rcpt_vars = TokenMap::default().with_variables(SMTP_RCPT_TO_VARS);
|
||||
|
||||
Self {
|
||||
submitter: IfBlock::try_parse(
|
||||
config,
|
||||
"report.submitter",
|
||||
&TokenMap::default().with_smtp_variables(&[V_RECIPIENT_DOMAIN]),
|
||||
&TokenMap::default().with_variables(RCPT_DOMAIN_VARS),
|
||||
)
|
||||
.unwrap_or_else(|| {
|
||||
default_hostname_if_block
|
||||
.map(|i| i.into_default("report.submitter"))
|
||||
.unwrap_or_else(|| IfBlock::new("localhost".to_string()))
|
||||
IfBlock::new::<()>("report.submitter", [], "key_get('default', 'hostname')")
|
||||
}),
|
||||
analysis: ReportAnalysis {
|
||||
addresses: config
|
||||
|
@ -106,40 +82,52 @@ impl ReportConfig {
|
|||
.map(|(_, m)| m)
|
||||
.collect(),
|
||||
forward: config.property("report.analysis.forward").unwrap_or(true),
|
||||
store: config.property("report.analysis.store"),
|
||||
store: config
|
||||
.property_or_default::<Option<Duration>>("report.analysis.store", "30d")
|
||||
.unwrap_or_default(),
|
||||
},
|
||||
dkim: Report::parse(config, "dkim", &default_hostname, &sender_vars),
|
||||
spf: Report::parse(config, "spf", &default_hostname, &sender_vars),
|
||||
dmarc: Report::parse(config, "dmarc", &default_hostname, &sender_vars),
|
||||
dkim: Report::parse(config, "dkim", &rcpt_vars),
|
||||
spf: Report::parse(config, "spf", &sender_vars),
|
||||
dmarc: Report::parse(config, "dmarc", &rcpt_vars),
|
||||
dmarc_aggregate: AggregateReport::parse(
|
||||
config,
|
||||
"dmarc",
|
||||
&default_hostname,
|
||||
&sender_vars.with_constants::<AggregateFrequency>(),
|
||||
&rcpt_vars.with_constants::<AggregateFrequency>(),
|
||||
),
|
||||
tls: AggregateReport::parse(
|
||||
config,
|
||||
"tls",
|
||||
&default_hostname,
|
||||
&rcpt_vars.with_constants::<AggregateFrequency>(),
|
||||
&TokenMap::default()
|
||||
.with_variables(SMTP_QUEUE_HOST_VARS)
|
||||
.with_constants::<AggregateFrequency>(),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Report {
|
||||
pub fn parse(
|
||||
config: &mut Config,
|
||||
id: &str,
|
||||
default_hostname: &str,
|
||||
token_map: &TokenMap,
|
||||
) -> Self {
|
||||
pub fn parse(config: &mut Config, id: &str, token_map: &TokenMap) -> Self {
|
||||
let mut report = Self {
|
||||
name: IfBlock::new(format!("{} Reporting", id.to_ascii_uppercase())),
|
||||
address: IfBlock::new(format!("MAILER-DAEMON@{default_hostname}")),
|
||||
subject: IfBlock::new(format!("{} Report", id.to_ascii_uppercase())),
|
||||
sign: Default::default(),
|
||||
send: Default::default(),
|
||||
name: IfBlock::new::<()>(format!("report.{id}.from-name"), [], "'Report Subsystem'"),
|
||||
address: IfBlock::new::<()>(
|
||||
format!("report.{id}.from-address"),
|
||||
[],
|
||||
format!("'noreply-{id}@' + key_get('default', 'domain')"),
|
||||
),
|
||||
subject: IfBlock::new::<()>(
|
||||
format!("report.{id}.subject"),
|
||||
[],
|
||||
format!(
|
||||
"'{} Authentication Failure Report'",
|
||||
id.to_ascii_uppercase()
|
||||
),
|
||||
),
|
||||
sign: IfBlock::new::<()>(
|
||||
format!("report.{id}.sign"),
|
||||
[],
|
||||
"['rsa_' + key_get('default', 'domain'), 'ed_' + key_get('default', 'domain')]",
|
||||
),
|
||||
send: IfBlock::new::<()>(format!("report.{id}.send"), [], "[1, 1d]"),
|
||||
};
|
||||
for (value, key) in [
|
||||
(&mut report.name, "from-name"),
|
||||
|
@ -158,22 +146,37 @@ impl Report {
|
|||
}
|
||||
|
||||
impl AggregateReport {
|
||||
pub fn parse(
|
||||
config: &mut Config,
|
||||
id: &str,
|
||||
default_hostname: &str,
|
||||
token_map: &TokenMap,
|
||||
) -> Self {
|
||||
let rcpt_vars = TokenMap::default().with_smtp_variables(&[V_RECIPIENT_DOMAIN]);
|
||||
pub fn parse(config: &mut Config, id: &str, token_map: &TokenMap) -> Self {
|
||||
let rcpt_vars = TokenMap::default().with_variables(RCPT_DOMAIN_VARS);
|
||||
|
||||
let mut report = Self {
|
||||
name: IfBlock::new(format!("{} Aggregate Report", id.to_ascii_uppercase())),
|
||||
address: IfBlock::new(format!("noreply-{id}@{default_hostname}")),
|
||||
org_name: Default::default(),
|
||||
contact_info: Default::default(),
|
||||
send: IfBlock::new(AggregateFrequency::Never),
|
||||
sign: Default::default(),
|
||||
max_size: IfBlock::new(25 * 1024 * 1024),
|
||||
name: IfBlock::new::<()>(
|
||||
format!("report.{id}.aggregate.from-name"),
|
||||
[],
|
||||
format!("'{} Aggregate Report'", id.to_ascii_uppercase()),
|
||||
),
|
||||
address: IfBlock::new::<()>(
|
||||
format!("report.{id}.aggregate.from-address"),
|
||||
[],
|
||||
format!("'noreply-{id}@' + key_get('default', 'domain')"),
|
||||
),
|
||||
org_name: IfBlock::new::<()>(
|
||||
format!("report.{id}.aggregate.org-name"),
|
||||
[],
|
||||
"key_get('default', 'domain')",
|
||||
),
|
||||
contact_info: IfBlock::empty(format!("report.{id}.aggregate.contact-info")),
|
||||
send: IfBlock::new::<AggregateFrequency>(
|
||||
format!("report.{id}.aggregate.send"),
|
||||
[],
|
||||
"daily",
|
||||
),
|
||||
sign: IfBlock::new::<()>(
|
||||
format!("report.{id}.aggregate.sign"),
|
||||
[],
|
||||
"['rsa_' + key_get('default', 'domain'), 'ed_' + key_get('default', 'domain')]",
|
||||
),
|
||||
max_size: IfBlock::new::<()>(format!("report.{id}.aggregate.max-size"), [], "26214400"),
|
||||
};
|
||||
|
||||
for (value, key, token_map) in [
|
||||
|
@ -200,45 +203,7 @@ impl AggregateReport {
|
|||
|
||||
impl Default for ReportConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
submitter: IfBlock::new("localhost".to_string()),
|
||||
analysis: ReportAnalysis {
|
||||
addresses: Default::default(),
|
||||
forward: true,
|
||||
store: None,
|
||||
},
|
||||
dkim: Default::default(),
|
||||
spf: Default::default(),
|
||||
dmarc: Default::default(),
|
||||
dmarc_aggregate: Default::default(),
|
||||
tls: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Report {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
name: IfBlock::new("Mail Delivery Subsystem".to_string()),
|
||||
address: IfBlock::new("MAILER-DAEMON@localhost".to_string()),
|
||||
subject: IfBlock::new("Report".to_string()),
|
||||
sign: Default::default(),
|
||||
send: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for AggregateReport {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
name: IfBlock::new("Reporting Subsystem".to_string()),
|
||||
address: IfBlock::new("no-replyN@localhost".to_string()),
|
||||
org_name: Default::default(),
|
||||
contact_info: Default::default(),
|
||||
send: IfBlock::new(AggregateFrequency::Never),
|
||||
sign: Default::default(),
|
||||
max_size: IfBlock::new(25 * 1024 * 1024),
|
||||
}
|
||||
Self::parse(&mut Config::default())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -70,10 +70,7 @@ pub struct Policy {
|
|||
|
||||
impl Resolvers {
|
||||
pub async fn parse(config: &mut Config) -> Self {
|
||||
let (resolver_config, mut opts) = match config
|
||||
.value_require("resolver.type")
|
||||
.unwrap_or("system")
|
||||
{
|
||||
let (resolver_config, mut opts) = match config.value("resolver.type").unwrap_or("system") {
|
||||
"cloudflare" => (ResolverConfig::cloudflare(), ResolverOpts::default()),
|
||||
"cloudflare-tls" => (ResolverConfig::cloudflare_tls(), ResolverOpts::default()),
|
||||
"quad9" => (ResolverConfig::quad9(), ResolverOpts::default()),
|
||||
|
|
|
@ -6,7 +6,10 @@ use std::{
|
|||
use smtp_proto::*;
|
||||
use utils::config::{utils::ParseValue, Config};
|
||||
|
||||
use crate::expr::{if_block::IfBlock, tokenizer::TokenMap, Constant, ConstantValue, Variable};
|
||||
use crate::{
|
||||
config::CONNECTION_VARS,
|
||||
expr::{if_block::IfBlock, tokenizer::TokenMap, *},
|
||||
};
|
||||
|
||||
use self::throttle::parse_throttle;
|
||||
|
||||
|
@ -37,6 +40,7 @@ pub struct SessionThrottle {
|
|||
|
||||
#[derive(Clone)]
|
||||
pub struct Connect {
|
||||
pub hostname: IfBlock,
|
||||
pub script: IfBlock,
|
||||
pub greeting: IfBlock,
|
||||
}
|
||||
|
@ -67,7 +71,6 @@ pub struct Auth {
|
|||
pub directory: IfBlock,
|
||||
pub mechanisms: IfBlock,
|
||||
pub require: IfBlock,
|
||||
pub allow_plain_text: IfBlock,
|
||||
pub must_match_sender: IfBlock,
|
||||
pub errors_max: IfBlock,
|
||||
pub errors_wait: IfBlock,
|
||||
|
@ -160,33 +163,10 @@ pub enum MilterVersion {
|
|||
|
||||
impl SessionConfig {
|
||||
pub fn parse(config: &mut Config) -> Self {
|
||||
let has_conn_vars =
|
||||
TokenMap::default().with_smtp_variables(&[V_LISTENER, V_REMOTE_IP, V_LOCAL_IP]);
|
||||
let has_ehlo_hars = TokenMap::default().with_smtp_variables(&[
|
||||
V_LISTENER,
|
||||
V_REMOTE_IP,
|
||||
V_LOCAL_IP,
|
||||
V_HELO_DOMAIN,
|
||||
]);
|
||||
let has_sender_vars = TokenMap::default().with_smtp_variables(&[
|
||||
V_LISTENER,
|
||||
V_REMOTE_IP,
|
||||
V_LOCAL_IP,
|
||||
V_SENDER,
|
||||
V_SENDER_DOMAIN,
|
||||
V_AUTHENTICATED_AS,
|
||||
]);
|
||||
let has_rcpt_vars = TokenMap::default().with_smtp_variables(&[
|
||||
V_SENDER,
|
||||
V_SENDER_DOMAIN,
|
||||
V_RECIPIENT,
|
||||
V_RECIPIENT_DOMAIN,
|
||||
V_AUTHENTICATED_AS,
|
||||
V_LISTENER,
|
||||
V_REMOTE_IP,
|
||||
V_LOCAL_IP,
|
||||
V_HELO_DOMAIN,
|
||||
]);
|
||||
let has_conn_vars = TokenMap::default().with_variables(CONNECTION_VARS);
|
||||
let has_ehlo_hars = TokenMap::default().with_variables(SMTP_EHLO_VARS);
|
||||
let has_sender_vars = TokenMap::default().with_variables(SMTP_MAIL_FROM_VARS);
|
||||
let has_rcpt_vars = TokenMap::default().with_variables(SMTP_RCPT_TO_VARS);
|
||||
let mt_priority_vars = has_sender_vars.clone().with_constants::<MtPriority>();
|
||||
let mechanisms_vars = has_ehlo_hars.clone().with_constants::<Mechanism>();
|
||||
|
||||
|
@ -222,6 +202,11 @@ impl SessionConfig {
|
|||
"session.connect.script",
|
||||
&has_conn_vars,
|
||||
),
|
||||
(
|
||||
&mut session.connect.hostname,
|
||||
"session.connect.hostname",
|
||||
&has_conn_vars,
|
||||
),
|
||||
(
|
||||
&mut session.connect.greeting,
|
||||
"session.connect.greeting",
|
||||
|
@ -317,11 +302,6 @@ impl SessionConfig {
|
|||
"session.auth.errors.wait",
|
||||
&has_ehlo_hars,
|
||||
),
|
||||
(
|
||||
&mut session.auth.allow_plain_text,
|
||||
"session.auth.allow-plain-text",
|
||||
&has_ehlo_hars,
|
||||
),
|
||||
(
|
||||
&mut session.auth.must_match_sender,
|
||||
"session.auth.must-match-sender",
|
||||
|
@ -438,18 +418,7 @@ impl SessionThrottle {
|
|||
let all_throttles = parse_throttle(
|
||||
config,
|
||||
"session.throttle",
|
||||
&TokenMap::default().with_smtp_variables(&[
|
||||
V_SENDER,
|
||||
V_SENDER_DOMAIN,
|
||||
V_RECIPIENT,
|
||||
V_RECIPIENT_DOMAIN,
|
||||
V_AUTHENTICATED_AS,
|
||||
V_LISTENER,
|
||||
V_REMOTE_IP,
|
||||
V_LOCAL_IP,
|
||||
V_PRIORITY,
|
||||
V_HELO_DOMAIN,
|
||||
]),
|
||||
&TokenMap::default().with_variables(SMTP_RCPT_TO_VARS),
|
||||
THROTTLE_LISTENER
|
||||
| THROTTLE_REMOTE_IP
|
||||
| THROTTLE_LOCAL_IP
|
||||
|
@ -500,7 +469,9 @@ fn parse_pipe(config: &mut Config, id: &str, token_map: &TokenMap) -> Option<Pip
|
|||
command: IfBlock::try_parse(config, ("session.data.pipe", id, "command"), token_map)?,
|
||||
arguments: IfBlock::try_parse(config, ("session.data.pipe", id, "arguments"), token_map)?,
|
||||
timeout: IfBlock::try_parse(config, ("session.data.pipe", id, "timeout"), token_map)
|
||||
.unwrap_or_else(|| IfBlock::new(Duration::from_secs(30))),
|
||||
.unwrap_or_else(|| {
|
||||
IfBlock::new::<()>(format!("session.data.pipe.{id}.timeout"), [], "30s")
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -511,7 +482,9 @@ fn parse_milter(config: &mut Config, id: &str, token_map: &TokenMap) -> Option<M
|
|||
let port = config.property_require(("session.data.milter", id, "port"))?;
|
||||
Some(Milter {
|
||||
enable: IfBlock::try_parse(config, ("session.data.milter", id, "enable"), token_map)
|
||||
.unwrap_or_default(),
|
||||
.unwrap_or_else(|| {
|
||||
IfBlock::new::<()>(format!("session.data.milter.{id}.enable"), [], "false")
|
||||
}),
|
||||
addrs: format!("{}:{}", hostname, port)
|
||||
.to_socket_addrs()
|
||||
.map_err(|err| {
|
||||
|
@ -573,72 +546,165 @@ fn parse_milter(config: &mut Config, id: &str, token_map: &TokenMap) -> Option<M
|
|||
impl Default for SessionConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
timeout: IfBlock::new(Duration::from_secs(15 * 60)),
|
||||
duration: IfBlock::new(Duration::from_secs(5 * 60)),
|
||||
transfer_limit: IfBlock::new(250 * 1024 * 1024),
|
||||
timeout: IfBlock::new::<()>("session.timeout", [], "5m"),
|
||||
duration: IfBlock::new::<()>("session.duration", [], "10m"),
|
||||
transfer_limit: IfBlock::new::<()>("session.transfer-limit", [], "262144000"),
|
||||
throttle: SessionThrottle {
|
||||
connect: Default::default(),
|
||||
mail_from: Default::default(),
|
||||
rcpt_to: Default::default(),
|
||||
},
|
||||
connect: Connect {
|
||||
script: Default::default(),
|
||||
greeting: IfBlock::new("Stalwart ESMTP at your service".to_string()),
|
||||
hostname: IfBlock::new::<()>(
|
||||
"server.connect.hostname",
|
||||
[],
|
||||
"key_get('default', 'hostname')",
|
||||
),
|
||||
script: IfBlock::empty("session.connect.script"),
|
||||
greeting: IfBlock::new::<()>(
|
||||
"session.connect.greeting",
|
||||
[],
|
||||
"'Stalwart ESMTP at your service'",
|
||||
),
|
||||
},
|
||||
ehlo: Ehlo {
|
||||
script: Default::default(),
|
||||
require: IfBlock::new(true),
|
||||
reject_non_fqdn: IfBlock::new(true),
|
||||
script: IfBlock::empty("session.ehlo.script"),
|
||||
require: IfBlock::new::<()>("session.ehlo.require", [], "true"),
|
||||
reject_non_fqdn: IfBlock::new::<()>(
|
||||
"session.ehlo.reject-non-fqdn",
|
||||
[("local_port == 25", "true")],
|
||||
"false",
|
||||
),
|
||||
},
|
||||
auth: Auth {
|
||||
directory: Default::default(),
|
||||
mechanisms: Default::default(),
|
||||
require: IfBlock::new(false),
|
||||
allow_plain_text: IfBlock::new(false),
|
||||
must_match_sender: IfBlock::new(true),
|
||||
errors_max: IfBlock::new(3),
|
||||
errors_wait: IfBlock::new(Duration::from_secs(30)),
|
||||
directory: IfBlock::new::<()>(
|
||||
"session.auth.directory",
|
||||
#[cfg(feature = "test_mode")]
|
||||
[],
|
||||
#[cfg(not(feature = "test_mode"))]
|
||||
[("local_port != 25", "'*'")],
|
||||
"false",
|
||||
),
|
||||
mechanisms: IfBlock::new::<Mechanism>(
|
||||
"session.auth.mechanisms",
|
||||
[("local_port != 25 && is_tls", "[plain, login]")],
|
||||
"false",
|
||||
),
|
||||
require: IfBlock::new::<()>(
|
||||
"session.auth.require",
|
||||
#[cfg(feature = "test_mode")]
|
||||
[],
|
||||
#[cfg(not(feature = "test_mode"))]
|
||||
[("local_port != 25", "'*'")],
|
||||
"false",
|
||||
),
|
||||
must_match_sender: IfBlock::new::<()>("session.auth.must-match-sender", [], "true"),
|
||||
errors_max: IfBlock::new::<()>("session.auth.errors.total", [], "3"),
|
||||
errors_wait: IfBlock::new::<()>("session.auth.errors.wait", [], "5s"),
|
||||
},
|
||||
mail: Mail {
|
||||
script: Default::default(),
|
||||
rewrite: Default::default(),
|
||||
script: IfBlock::empty("session.mail.script"),
|
||||
rewrite: IfBlock::empty("session.mail.rewrite"),
|
||||
},
|
||||
rcpt: Rcpt {
|
||||
script: Default::default(),
|
||||
relay: IfBlock::new(false),
|
||||
directory: Default::default(),
|
||||
rewrite: Default::default(),
|
||||
errors_max: IfBlock::new(10),
|
||||
errors_wait: IfBlock::new(Duration::from_secs(30)),
|
||||
max_recipients: IfBlock::new(100),
|
||||
catch_all: AddressMapping::Disable,
|
||||
subaddressing: AddressMapping::Disable,
|
||||
script: IfBlock::empty("session.rcpt."),
|
||||
relay: IfBlock::new::<()>(
|
||||
"session.rcpt.relay",
|
||||
[("!is_empty(authenticated_as)", "true")],
|
||||
"false",
|
||||
),
|
||||
directory: IfBlock::new::<()>(
|
||||
"session.rcpt.directory",
|
||||
[],
|
||||
#[cfg(feature = "test_mode")]
|
||||
"false",
|
||||
#[cfg(not(feature = "test_mode"))]
|
||||
"'*'",
|
||||
),
|
||||
rewrite: IfBlock::empty("session.rcpt.rewrite"),
|
||||
errors_max: IfBlock::new::<()>("session.rcpt.errors.total", [], "5"),
|
||||
errors_wait: IfBlock::new::<()>("session.rcpt.errors.wait", [], "5s"),
|
||||
max_recipients: IfBlock::new::<()>("session.rcpt.max-recipients", [], "100"),
|
||||
catch_all: AddressMapping::Enable,
|
||||
subaddressing: AddressMapping::Enable,
|
||||
},
|
||||
data: Data {
|
||||
script: Default::default(),
|
||||
script: IfBlock::empty("session.data.script"),
|
||||
pipe_commands: Default::default(),
|
||||
milters: Default::default(),
|
||||
max_messages: IfBlock::new(10),
|
||||
max_message_size: IfBlock::new(25 * 1024 * 1024),
|
||||
max_received_headers: IfBlock::new(50),
|
||||
add_received: IfBlock::new(true),
|
||||
add_received_spf: IfBlock::new(true),
|
||||
add_return_path: IfBlock::new(true),
|
||||
add_auth_results: IfBlock::new(true),
|
||||
add_message_id: IfBlock::new(true),
|
||||
add_date: IfBlock::new(true),
|
||||
max_messages: IfBlock::new::<()>("session.data.limits.messages", [], "10"),
|
||||
max_message_size: IfBlock::new::<()>("session.data.limits.size", [], "104857600"),
|
||||
max_received_headers: IfBlock::new::<()>(
|
||||
"session.data.limits.received-headers",
|
||||
[],
|
||||
"50",
|
||||
),
|
||||
add_received: IfBlock::new::<()>(
|
||||
"session.data.add-headers.received",
|
||||
[("local_port == 25", "true")],
|
||||
"false",
|
||||
),
|
||||
add_received_spf: IfBlock::new::<()>(
|
||||
"session.data.add-headers.received-spf",
|
||||
[("local_port == 25", "true")],
|
||||
"false",
|
||||
),
|
||||
add_return_path: IfBlock::new::<()>(
|
||||
"session.data.add-headers.return-path",
|
||||
[("local_port == 25", "true")],
|
||||
"false",
|
||||
),
|
||||
add_auth_results: IfBlock::new::<()>(
|
||||
"session.data.add-headers.auth-results",
|
||||
[("local_port == 25", "true")],
|
||||
"false",
|
||||
),
|
||||
add_message_id: IfBlock::new::<()>(
|
||||
"session.data.add-headers.message-id",
|
||||
[("local_port == 25", "true")],
|
||||
"false",
|
||||
),
|
||||
add_date: IfBlock::new::<()>(
|
||||
"session.data.add-headers.date",
|
||||
[("local_port == 25", "true")],
|
||||
"false",
|
||||
),
|
||||
},
|
||||
extensions: Extensions {
|
||||
pipelining: IfBlock::new(true),
|
||||
chunking: IfBlock::new(true),
|
||||
requiretls: IfBlock::new(true),
|
||||
dsn: IfBlock::new(false),
|
||||
vrfy: IfBlock::new(false),
|
||||
expn: IfBlock::new(false),
|
||||
no_soliciting: IfBlock::new(false),
|
||||
future_release: Default::default(),
|
||||
deliver_by: Default::default(),
|
||||
mt_priority: Default::default(),
|
||||
pipelining: IfBlock::new::<()>("session.extensions.pipelining", [], "true"),
|
||||
chunking: IfBlock::new::<()>("session.extensions.chunking", [], "true"),
|
||||
requiretls: IfBlock::new::<()>("session.extensions.requiretls", [], "true"),
|
||||
dsn: IfBlock::new::<()>(
|
||||
"session.extensions.dsn",
|
||||
[("!is_empty(authenticated_as)", "true")],
|
||||
"false",
|
||||
),
|
||||
vrfy: IfBlock::new::<()>(
|
||||
"session.extensions.vrfy",
|
||||
[("!is_empty(authenticated_as)", "true")],
|
||||
"false",
|
||||
),
|
||||
expn: IfBlock::new::<()>(
|
||||
"session.extensions.expn",
|
||||
[("!is_empty(authenticated_as)", "true")],
|
||||
"false",
|
||||
),
|
||||
no_soliciting: IfBlock::new::<()>("session.extensions.no-soliciting", [], "''"),
|
||||
future_release: IfBlock::new::<()>(
|
||||
"session.extensions.future-release",
|
||||
[("!is_empty(authenticated_as)", "7d")],
|
||||
"false",
|
||||
),
|
||||
deliver_by: IfBlock::new::<()>(
|
||||
"session.extensions.deliver-by",
|
||||
[("!is_empty(authenticated_as)", "15d")],
|
||||
"false",
|
||||
),
|
||||
mt_priority: IfBlock::new::<MtPriority>(
|
||||
"session.extensions.mt-priority",
|
||||
[("!is_empty(authenticated_as)", "mixer")],
|
||||
"false",
|
||||
),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -96,11 +96,13 @@ fn parse_throttle_item(
|
|||
.unwrap_or_default(),
|
||||
keys,
|
||||
concurrency: config
|
||||
.property::<u64>((prefix.as_str(), "concurrency"))
|
||||
.filter(|&v| v > 0),
|
||||
.property::<Option<u64>>((prefix.as_str(), "concurrency"))
|
||||
.filter(|&v| v.as_ref().map_or(false, |v| *v > 0))
|
||||
.unwrap_or_default(),
|
||||
rate: config
|
||||
.property::<Rate>((prefix.as_str(), "rate"))
|
||||
.filter(|v| v.requests > 0),
|
||||
.property::<Option<Rate>>((prefix.as_str(), "rate"))
|
||||
.filter(|v| v.as_ref().map_or(false, |r| r.requests > 0))
|
||||
.unwrap_or_default(),
|
||||
};
|
||||
|
||||
// Validate
|
||||
|
|
|
@ -5,6 +5,7 @@ use tracing::Level;
|
|||
use tracing_appender::rolling::RollingFileAppender;
|
||||
use utils::config::Config;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Tracer {
|
||||
Stdout {
|
||||
level: Level,
|
||||
|
@ -24,11 +25,13 @@ pub enum Tracer {
|
|||
},
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum OtelTracer {
|
||||
Gprc(TonicExporterBuilder),
|
||||
Http(HttpExporterBuilder),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Tracers {
|
||||
pub tracers: Vec<Tracer>,
|
||||
}
|
||||
|
|
|
@ -28,17 +28,17 @@ use crate::expr::{Constant, Expression};
|
|||
use super::{
|
||||
parser::ExpressionParser,
|
||||
tokenizer::{TokenMap, Tokenizer},
|
||||
ExpressionItem,
|
||||
ConstantValue, ExpressionItem,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
#[derive(Debug, Clone)]
|
||||
#[cfg_attr(feature = "test_mode", derive(PartialEq, Eq))]
|
||||
pub struct IfThen {
|
||||
pub expr: Expression,
|
||||
pub then: Expression,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
#[derive(Debug, Clone)]
|
||||
#[cfg_attr(feature = "test_mode", derive(PartialEq, Eq))]
|
||||
pub struct IfBlock {
|
||||
pub key: String,
|
||||
|
@ -47,11 +47,35 @@ pub struct IfBlock {
|
|||
}
|
||||
|
||||
impl IfBlock {
|
||||
pub fn new<T: Into<Constant>>(value: T) -> Self {
|
||||
pub fn new<T: ConstantValue>(
|
||||
key: impl Into<String>,
|
||||
if_thens: impl IntoIterator<Item = (&'static str, &'static str)>,
|
||||
default: impl AsRef<str>,
|
||||
) -> Self {
|
||||
let token_map = TokenMap::default()
|
||||
.with_all_variables()
|
||||
.with_constants::<T>();
|
||||
|
||||
Self {
|
||||
key: String::new(),
|
||||
if_then: Vec::new(),
|
||||
default: Expression::from(value),
|
||||
key: key.into(),
|
||||
if_then: if_thens
|
||||
.into_iter()
|
||||
.map(|(if_, then)| IfThen {
|
||||
expr: Expression::parse(&token_map, if_),
|
||||
then: Expression::parse(&token_map, then),
|
||||
})
|
||||
.collect(),
|
||||
default: Expression::parse(&token_map, default.as_ref()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn empty(key: impl Into<String>) -> Self {
|
||||
Self {
|
||||
key: key.into(),
|
||||
if_then: Default::default(),
|
||||
default: Expression {
|
||||
items: Default::default(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -78,6 +102,12 @@ impl Expression {
|
|||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn parse(token_map: &TokenMap, expr: &str) -> Self {
|
||||
ExpressionParser::new(Tokenizer::new(expr, token_map))
|
||||
.parse()
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl IfBlock {
|
||||
|
@ -91,7 +121,10 @@ impl IfBlock {
|
|||
// Parse conditions
|
||||
let mut if_block = IfBlock {
|
||||
key,
|
||||
..Default::default()
|
||||
if_then: Default::default(),
|
||||
default: Expression {
|
||||
items: Default::default(),
|
||||
},
|
||||
};
|
||||
|
||||
// Try first with a single value
|
||||
|
@ -181,7 +214,6 @@ impl IfBlock {
|
|||
}
|
||||
|
||||
if !found_if {
|
||||
config.new_missing_property(if_block.key);
|
||||
None
|
||||
} else if !found_then {
|
||||
config.new_parse_error(
|
||||
|
|
|
@ -27,6 +27,42 @@ use std::{
|
|||
time::Duration,
|
||||
};
|
||||
|
||||
pub const V_RECIPIENT: u32 = 0;
|
||||
pub const V_RECIPIENT_DOMAIN: u32 = 1;
|
||||
pub const V_SENDER: u32 = 2;
|
||||
pub const V_SENDER_DOMAIN: u32 = 3;
|
||||
pub const V_MX: u32 = 4;
|
||||
pub const V_HELO_DOMAIN: u32 = 5;
|
||||
pub const V_AUTHENTICATED_AS: u32 = 6;
|
||||
pub const V_LISTENER: u32 = 7;
|
||||
pub const V_REMOTE_IP: u32 = 8;
|
||||
pub const V_REMOTE_PORT: u32 = 9;
|
||||
pub const V_LOCAL_IP: u32 = 10;
|
||||
pub const V_LOCAL_PORT: u32 = 11;
|
||||
pub const V_PRIORITY: u32 = 12;
|
||||
pub const V_PROTOCOL: u32 = 13;
|
||||
pub const V_TLS: u32 = 14;
|
||||
pub const V_RECIPIENTS: u32 = 15;
|
||||
|
||||
pub const VARIABLES_MAP: &[(&str, u32)] = &[
|
||||
("rcpt", V_RECIPIENT),
|
||||
("rcpt_domain", V_RECIPIENT_DOMAIN),
|
||||
("sender", V_SENDER),
|
||||
("sender_domain", V_SENDER_DOMAIN),
|
||||
("mx", V_MX),
|
||||
("helo_domain", V_HELO_DOMAIN),
|
||||
("authenticated_as", V_AUTHENTICATED_AS),
|
||||
("listener", V_LISTENER),
|
||||
("remote_ip", V_REMOTE_IP),
|
||||
("local_ip", V_LOCAL_IP),
|
||||
("priority", V_PRIORITY),
|
||||
("local_port", V_LOCAL_PORT),
|
||||
("remote_port", V_REMOTE_PORT),
|
||||
("protocol", V_PROTOCOL),
|
||||
("is_tls", V_TLS),
|
||||
("recipients", V_RECIPIENTS),
|
||||
];
|
||||
|
||||
use regex::Regex;
|
||||
use utils::config::{utils::ParseValue, Rate};
|
||||
|
||||
|
@ -67,7 +103,7 @@ pub enum Variable<'x> {
|
|||
|
||||
impl Default for Variable<'_> {
|
||||
fn default() -> Self {
|
||||
Variable::Integer(0)
|
||||
Variable::String("".into())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -185,6 +221,12 @@ impl From<i32> for Variable<'_> {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<u16> for Variable<'_> {
|
||||
fn from(value: u16) -> Self {
|
||||
Variable::Integer(value as i64)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<i16> for Variable<'_> {
|
||||
fn from(value: i16) -> Self {
|
||||
Variable::Integer(value as i64)
|
||||
|
@ -292,12 +334,32 @@ impl PartialEq for Token {
|
|||
|
||||
impl Eq for Token {}
|
||||
|
||||
pub struct NoConstants;
|
||||
|
||||
pub trait ConstantValue:
|
||||
ParseValue + for<'x> TryFrom<Variable<'x>> + Into<Constant> + Sized
|
||||
{
|
||||
fn add_constants(token_map: &mut TokenMap);
|
||||
}
|
||||
|
||||
impl ConstantValue for () {
|
||||
fn add_constants(_: &mut TokenMap) {}
|
||||
}
|
||||
|
||||
impl From<()> for Constant {
|
||||
fn from(_: ()) -> Self {
|
||||
Constant::Integer(0)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> TryFrom<Variable<'x>> for () {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(_: Variable<'x>) -> Result<Self, Self::Error> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl ConstantValue for Duration {
|
||||
fn add_constants(_: &mut TokenMap) {}
|
||||
}
|
||||
|
|
|
@ -29,7 +29,7 @@ use utils::config::utils::ParseValue;
|
|||
|
||||
use super::{
|
||||
functions::{ASYNC_FUNCTIONS, FUNCTIONS},
|
||||
BinaryOperator, Constant, ConstantValue, Token, UnaryOperator,
|
||||
*,
|
||||
};
|
||||
|
||||
pub struct Tokenizer<'x> {
|
||||
|
@ -348,7 +348,37 @@ impl<'x> Tokenizer<'x> {
|
|||
}
|
||||
|
||||
impl TokenMap {
|
||||
pub fn with_variables<I>(mut self, vars: I) -> Self
|
||||
pub fn with_all_variables(self) -> Self {
|
||||
self.with_variables(&[
|
||||
V_RECIPIENT,
|
||||
V_RECIPIENT_DOMAIN,
|
||||
V_SENDER,
|
||||
V_SENDER_DOMAIN,
|
||||
V_MX,
|
||||
V_HELO_DOMAIN,
|
||||
V_AUTHENTICATED_AS,
|
||||
V_LISTENER,
|
||||
V_REMOTE_IP,
|
||||
V_REMOTE_PORT,
|
||||
V_LOCAL_IP,
|
||||
V_LOCAL_PORT,
|
||||
V_PRIORITY,
|
||||
V_PROTOCOL,
|
||||
V_TLS,
|
||||
])
|
||||
}
|
||||
|
||||
pub fn with_variables(mut self, variables: &[u32]) -> Self {
|
||||
for (name, idx) in VARIABLES_MAP {
|
||||
if variables.contains(idx) {
|
||||
self.tokens.insert(name, Token::Variable(*idx));
|
||||
}
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_variables_map<I>(mut self, vars: I) -> Self
|
||||
where
|
||||
I: IntoIterator<Item = (&'static str, u32)>,
|
||||
{
|
||||
|
|
|
@ -42,7 +42,7 @@ pub static DAEMON_NAME: &str = concat!("Stalwart Mail Server v", env!("CARGO_PKG
|
|||
|
||||
pub type SharedCore = Arc<ArcSwap<Core>>;
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
#[derive(Clone, Default)]
|
||||
pub struct Core {
|
||||
pub storage: Storage,
|
||||
pub sieve: Scripting,
|
||||
|
@ -56,7 +56,6 @@ pub struct Core {
|
|||
#[derive(Clone)]
|
||||
pub struct Network {
|
||||
pub blocked_ips: BlockedIps,
|
||||
pub hostname: IfBlock,
|
||||
pub url: IfBlock,
|
||||
}
|
||||
|
||||
|
@ -249,7 +248,7 @@ impl Tracers {
|
|||
| Tracer::Otel { level, .. } => level,
|
||||
};
|
||||
|
||||
if tracer_level > level {
|
||||
if tracer_level < level {
|
||||
level = tracer_level;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -49,6 +49,7 @@ pub struct AcmeProvider {
|
|||
pub contact: Vec<String>,
|
||||
renew_before: chrono::Duration,
|
||||
account_key: ArcSwap<Vec<u8>>,
|
||||
default: bool,
|
||||
}
|
||||
|
||||
pub struct AcmeResolver {
|
||||
|
@ -73,6 +74,7 @@ impl AcmeProvider {
|
|||
domains: Vec<String>,
|
||||
contact: Vec<String>,
|
||||
renew_before: Duration,
|
||||
default: bool,
|
||||
) -> utils::config::Result<Self> {
|
||||
Ok(AcmeProvider {
|
||||
id,
|
||||
|
@ -90,6 +92,7 @@ impl AcmeProvider {
|
|||
renew_before: chrono::Duration::from_std(renew_before).unwrap(),
|
||||
domains,
|
||||
account_key: Default::default(),
|
||||
default,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -139,6 +142,7 @@ impl Clone for AcmeProvider {
|
|||
contact: self.contact.clone(),
|
||||
renew_before: self.renew_before,
|
||||
account_key: ArcSwap::from_pointee(self.account_key.load().as_ref().clone()),
|
||||
default: self.default,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -45,6 +45,12 @@ impl Core {
|
|||
cert.clone(),
|
||||
);
|
||||
}
|
||||
|
||||
// Add default certificate
|
||||
if provider.default {
|
||||
certificates.insert("*".to_string(), cert);
|
||||
}
|
||||
|
||||
self.tls.certificates.store(certificates.into());
|
||||
|
||||
// Remove auth keys
|
||||
|
|
|
@ -70,7 +70,7 @@ impl BlockedIps {
|
|||
ip_addresses: RwLock::new(ip_addresses),
|
||||
has_networks: !ip_networks.is_empty(),
|
||||
ip_networks,
|
||||
limiter_rate: config.property::<Rate>("authentication.fail2ban"),
|
||||
limiter_rate: config.property_or_default::<Rate>("authentication.fail2ban", "100/1d"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -79,7 +79,7 @@ impl Server {
|
|||
tls = is_tls,
|
||||
"Starting listener"
|
||||
);
|
||||
let local_ip = listener.addr.ip();
|
||||
let local_addr = listener.addr;
|
||||
|
||||
// Obtain TCP options
|
||||
let opts = SocketOpts {
|
||||
|
@ -131,7 +131,7 @@ impl Server {
|
|||
.proxied_address()
|
||||
.map(|addr| addr.source)
|
||||
.unwrap_or(remote_addr);
|
||||
if let Some(session) = instance.build_session(stream, local_ip, remote_addr, &core) {
|
||||
if let Some(session) = instance.build_session(stream, local_addr, remote_addr, &core) {
|
||||
// Spawn session
|
||||
manager.spawn(session, is_tls, enable_acme);
|
||||
}
|
||||
|
@ -146,7 +146,7 @@ impl Server {
|
|||
}
|
||||
}
|
||||
});
|
||||
} else if let Some(session) = instance.build_session(stream, local_ip, remote_addr, &core) {
|
||||
} else if let Some(session) = instance.build_session(stream, local_addr, remote_addr, &core) {
|
||||
// Set socket options
|
||||
opts.apply(&session.stream);
|
||||
|
||||
|
@ -183,7 +183,7 @@ trait BuildSession {
|
|||
fn build_session<T: SessionStream>(
|
||||
&self,
|
||||
stream: T,
|
||||
local_ip: IpAddr,
|
||||
local_addr: SocketAddr,
|
||||
remote_addr: SocketAddr,
|
||||
core: &Core,
|
||||
) -> Option<SessionData<T>>;
|
||||
|
@ -193,7 +193,7 @@ impl BuildSession for Arc<ServerInstance> {
|
|||
fn build_session<T: SessionStream>(
|
||||
&self,
|
||||
stream: T,
|
||||
local_ip: IpAddr,
|
||||
local_addr: SocketAddr,
|
||||
remote_addr: SocketAddr,
|
||||
core: &Core,
|
||||
) -> Option<SessionData<T>> {
|
||||
|
@ -231,9 +231,11 @@ impl BuildSession for Arc<ServerInstance> {
|
|||
remote.ip = remote_ip.to_string(),
|
||||
remote.port = remote_port,
|
||||
),
|
||||
local_ip,
|
||||
local_ip: local_addr.ip(),
|
||||
local_port: local_addr.port(),
|
||||
remote_ip,
|
||||
remote_port,
|
||||
protocol: self.protocol,
|
||||
instance: self.clone(),
|
||||
}
|
||||
.into()
|
||||
|
|
|
@ -33,11 +33,8 @@ use tokio_rustls::{Accept, TlsAcceptor};
|
|||
use utils::config::ipmask::IpAddrMask;
|
||||
|
||||
use crate::{
|
||||
config::{
|
||||
server::ServerProtocol,
|
||||
smtp::{V_LISTENER, V_LOCAL_IP, V_REMOTE_IP},
|
||||
},
|
||||
expr::functions::ResolveVariable,
|
||||
config::server::ServerProtocol,
|
||||
expr::{functions::ResolveVariable, *},
|
||||
};
|
||||
|
||||
use self::limiter::{ConcurrencyLimiter, InFlight};
|
||||
|
@ -83,8 +80,10 @@ where
|
|||
pub struct SessionData<T: SessionStream> {
|
||||
pub stream: T,
|
||||
pub local_ip: IpAddr,
|
||||
pub local_port: u16,
|
||||
pub remote_ip: IpAddr,
|
||||
pub remote_port: u16,
|
||||
pub protocol: ServerProtocol,
|
||||
pub span: tracing::Span,
|
||||
pub in_flight: InFlight,
|
||||
pub instance: Arc<ServerInstance>,
|
||||
|
@ -117,8 +116,10 @@ pub trait SessionManager: Sync + Send + 'static + Clone {
|
|||
let session = SessionData {
|
||||
stream,
|
||||
local_ip: session.local_ip,
|
||||
local_port: session.local_port,
|
||||
remote_ip: session.remote_ip,
|
||||
remote_port: session.remote_port,
|
||||
protocol: session.protocol,
|
||||
span: session.span,
|
||||
in_flight: session.in_flight,
|
||||
instance: session.instance,
|
||||
|
@ -157,21 +158,16 @@ pub trait SessionManager: Sync + Send + 'static + Clone {
|
|||
fn shutdown(&self) -> impl std::future::Future<Output = ()> + Send;
|
||||
}
|
||||
|
||||
impl ResolveVariable for ServerInstance {
|
||||
fn resolve_variable(&self, variable: u32) -> crate::expr::Variable<'_> {
|
||||
match variable {
|
||||
V_LISTENER => self.id.as_str().into(),
|
||||
_ => crate::expr::Variable::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: SessionStream> ResolveVariable for SessionData<T> {
|
||||
fn resolve_variable(&self, variable: u32) -> crate::expr::Variable<'_> {
|
||||
match variable {
|
||||
V_REMOTE_IP => self.remote_ip.to_string().into(),
|
||||
V_REMOTE_PORT => self.remote_port.into(),
|
||||
V_LOCAL_IP => self.local_ip.to_string().into(),
|
||||
V_LOCAL_PORT => self.local_port.into(),
|
||||
V_LISTENER => self.instance.id.as_str().into(),
|
||||
V_PROTOCOL => self.protocol.as_str().into(),
|
||||
V_TLS => self.stream.is_tls().into(),
|
||||
_ => crate::expr::Variable::default(),
|
||||
}
|
||||
}
|
||||
|
@ -180,7 +176,17 @@ impl<T: SessionStream> ResolveVariable for SessionData<T> {
|
|||
impl Debug for TcpAcceptor {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Tls { .. } => f.debug_tuple("Tls").finish(),
|
||||
Self::Tls {
|
||||
acme_config,
|
||||
default_config,
|
||||
implicit,
|
||||
..
|
||||
} => f
|
||||
.debug_struct("Tls")
|
||||
.field("acme_config", acme_config)
|
||||
.field("default_config", default_config)
|
||||
.field("implicit", implicit)
|
||||
.finish(),
|
||||
Self::Plain => write!(f, "Plain"),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -224,6 +224,7 @@ pub fn exec_classify(ctx: PluginContext<'_>) -> Variable {
|
|||
context = "sieve:bayes_classify",
|
||||
event = "skip-classify",
|
||||
reason = "Not enough training data",
|
||||
min_learns = classifier.min_learns,
|
||||
spam_learns = %spam_learns,
|
||||
ham_learns = %ham_learns);
|
||||
return Variable::default();
|
||||
|
|
|
@ -57,7 +57,7 @@ impl SmtpDirectory {
|
|||
is_lmtp,
|
||||
credentials: None,
|
||||
local_host: config
|
||||
.value("server.hostname")
|
||||
.value("lookup.default.hostname")
|
||||
.unwrap_or("[127.0.0.1]")
|
||||
.to_string(),
|
||||
say_ehlo: false,
|
||||
|
|
|
@ -24,8 +24,7 @@
|
|||
use std::{net::IpAddr, sync::Arc};
|
||||
|
||||
use common::{
|
||||
config::smtp::{V_LISTENER, V_LOCAL_IP, V_REMOTE_IP},
|
||||
expr::functions::ResolveVariable,
|
||||
expr::{functions::ResolveVariable, *},
|
||||
listener::{ServerInstance, SessionData, SessionManager, SessionStream},
|
||||
Core,
|
||||
};
|
||||
|
@ -57,7 +56,9 @@ use super::{HtmlResponse, HttpRequest, HttpResponse, JmapSessionManager, JsonRes
|
|||
pub struct HttpSessionData {
|
||||
pub instance: Arc<ServerInstance>,
|
||||
pub local_ip: IpAddr,
|
||||
pub local_port: u16,
|
||||
pub remote_ip: IpAddr,
|
||||
pub remote_port: u16,
|
||||
pub is_tls: bool,
|
||||
}
|
||||
|
||||
|
@ -366,7 +367,9 @@ impl JMAP {
|
|||
HttpSessionData {
|
||||
instance,
|
||||
local_ip: session.local_ip,
|
||||
local_port: session.local_port,
|
||||
remote_ip,
|
||||
remote_port: session.remote_port,
|
||||
is_tls,
|
||||
},
|
||||
)
|
||||
|
@ -423,7 +426,11 @@ impl ResolveVariable for HttpSessionData {
|
|||
fn resolve_variable(&self, variable: u32) -> common::expr::Variable<'_> {
|
||||
match variable {
|
||||
V_REMOTE_IP => self.remote_ip.to_string().into(),
|
||||
V_REMOTE_PORT => self.remote_port.into(),
|
||||
V_LOCAL_IP => self.local_ip.to_string().into(),
|
||||
V_LOCAL_PORT => self.local_port.into(),
|
||||
V_TLS => self.is_tls.into(),
|
||||
V_PROTOCOL => if self.is_tls { "https" } else { "http" }.into(),
|
||||
V_LISTENER => self.instance.id.as_str().into(),
|
||||
_ => common::expr::Variable::default(),
|
||||
}
|
||||
|
@ -434,7 +441,14 @@ impl HttpSessionData {
|
|||
pub async fn resolve_url(&self, core: &Core) -> String {
|
||||
core.eval_if(&core.network.url, self)
|
||||
.await
|
||||
.unwrap_or_else(|| format!("http{}://localhost", if self.is_tls { "s" } else { "" }))
|
||||
.unwrap_or_else(|| {
|
||||
format!(
|
||||
"http{}://{}:{}",
|
||||
if self.is_tls { "s" } else { "" },
|
||||
self.local_ip,
|
||||
self.local_port
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -149,6 +149,7 @@ pub struct Session<T: AsyncWrite + AsyncRead> {
|
|||
pub struct SessionData {
|
||||
pub local_ip: IpAddr,
|
||||
pub local_ip_str: String,
|
||||
pub local_port: u16,
|
||||
pub remote_ip: IpAddr,
|
||||
pub remote_ip_str: String,
|
||||
pub remote_port: u16,
|
||||
|
@ -200,7 +201,6 @@ pub struct SessionParameters {
|
|||
pub auth_require: bool,
|
||||
pub auth_errors_max: usize,
|
||||
pub auth_errors_wait: Duration,
|
||||
pub auth_plain_text: bool,
|
||||
pub auth_match_sender: bool,
|
||||
|
||||
// Rcpt parameters
|
||||
|
@ -219,9 +219,10 @@ pub struct SessionParameters {
|
|||
}
|
||||
|
||||
impl SessionData {
|
||||
pub fn new(local_ip: IpAddr, remote_ip: IpAddr, remote_port: u16) -> Self {
|
||||
pub fn new(local_ip: IpAddr, local_port: u16, remote_ip: IpAddr, remote_port: u16) -> Self {
|
||||
SessionData {
|
||||
local_ip,
|
||||
local_port,
|
||||
remote_ip,
|
||||
local_ip_str: local_ip.to_string(),
|
||||
remote_ip_str: remote_ip.to_string(),
|
||||
|
@ -337,7 +338,6 @@ impl Session<common::listener::stream::NullIo> {
|
|||
auth_require: Default::default(),
|
||||
auth_errors_max: Default::default(),
|
||||
auth_errors_wait: Default::default(),
|
||||
auth_plain_text: false,
|
||||
rcpt_errors_max: Default::default(),
|
||||
rcpt_errors_wait: Default::default(),
|
||||
rcpt_max: Default::default(),
|
||||
|
@ -395,6 +395,7 @@ impl SessionData {
|
|||
local_ip_str: "127.0.0.1".to_string(),
|
||||
remote_ip_str: "127.0.0.1".to_string(),
|
||||
remote_port: 0,
|
||||
local_port: 0,
|
||||
helo_domain: "localhost".into(),
|
||||
mail_from,
|
||||
rcpt_to,
|
||||
|
|
|
@ -23,12 +23,11 @@
|
|||
|
||||
use std::time::Duration;
|
||||
|
||||
use common::config::smtp::auth::VerifyStrategy;
|
||||
use tokio::io::{AsyncRead, AsyncWrite};
|
||||
use common::{config::smtp::auth::VerifyStrategy, listener::SessionStream};
|
||||
|
||||
use super::Session;
|
||||
|
||||
impl<T: AsyncRead + AsyncWrite> Session<T> {
|
||||
impl<T: SessionStream> Session<T> {
|
||||
pub async fn eval_session_params(&mut self) {
|
||||
let c = &self.core.core.smtp.session;
|
||||
self.data.bytes_left = self
|
||||
|
@ -111,12 +110,6 @@ impl<T: AsyncRead + AsyncWrite> Session<T> {
|
|||
.eval_if(&ac.errors_wait, self)
|
||||
.await
|
||||
.unwrap_or_else(|| Duration::from_secs(30));
|
||||
self.params.auth_plain_text = self
|
||||
.core
|
||||
.core
|
||||
.eval_if(&ac.allow_plain_text, self)
|
||||
.await
|
||||
.unwrap_or(false);
|
||||
self.params.auth_match_sender = self
|
||||
.core
|
||||
.core
|
||||
|
|
|
@ -23,11 +23,10 @@
|
|||
|
||||
use common::{
|
||||
config::smtp::{queue::QueueQuota, *},
|
||||
expr::functions::ResolveVariable,
|
||||
listener::limiter::ConcurrencyLimiter,
|
||||
expr::{functions::ResolveVariable, *},
|
||||
listener::{limiter::ConcurrencyLimiter, SessionStream},
|
||||
};
|
||||
use dashmap::mapref::entry::Entry;
|
||||
use tokio::io::{AsyncRead, AsyncWrite};
|
||||
use utils::config::Rate;
|
||||
|
||||
use std::hash::{BuildHasher, Hash, Hasher};
|
||||
|
@ -210,7 +209,7 @@ impl NewKey for Throttle {
|
|||
}
|
||||
}
|
||||
|
||||
impl<T: AsyncRead + AsyncWrite> Session<T> {
|
||||
impl<T: SessionStream> Session<T> {
|
||||
pub async fn is_allowed(&mut self) -> bool {
|
||||
let throttles = if !self.data.rcpt_to.is_empty() {
|
||||
&self.core.core.smtp.session.throttle.rcpt_to
|
||||
|
|
|
@ -21,11 +21,10 @@
|
|||
* for more details.
|
||||
*/
|
||||
|
||||
use common::AuthResult;
|
||||
use common::{listener::SessionStream, AuthResult};
|
||||
use mail_parser::decoders::base64::base64_decode;
|
||||
use mail_send::Credentials;
|
||||
use smtp_proto::{IntoString, AUTH_LOGIN, AUTH_OAUTHBEARER, AUTH_PLAIN, AUTH_XOAUTH2};
|
||||
use tokio::io::{AsyncRead, AsyncWrite};
|
||||
|
||||
use crate::core::Session;
|
||||
|
||||
|
@ -65,7 +64,7 @@ impl SaslToken {
|
|||
}
|
||||
}
|
||||
|
||||
impl<T: AsyncWrite + AsyncRead + Unpin> Session<T> {
|
||||
impl<T: SessionStream> Session<T> {
|
||||
pub async fn handle_sasl_response(
|
||||
&mut self,
|
||||
token: &mut SaslToken,
|
||||
|
|
|
@ -199,15 +199,10 @@ impl<T: SessionStream> Session<T> {
|
|||
.await
|
||||
.unwrap_or_default()
|
||||
.into();
|
||||
if response.auth_mechanisms != 0 {
|
||||
if !self.stream.is_tls() && !self.params.auth_plain_text {
|
||||
response.auth_mechanisms &= !(AUTH_PLAIN | AUTH_LOGIN);
|
||||
}
|
||||
if response.auth_mechanisms != 0 {
|
||||
response.capabilities |= EXT_AUTH;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Future release
|
||||
if let Some(value) = self
|
||||
|
|
|
@ -22,11 +22,8 @@
|
|||
*/
|
||||
|
||||
use common::{
|
||||
config::{
|
||||
server::ServerProtocol,
|
||||
smtp::{session::Mechanism, *},
|
||||
},
|
||||
expr::{self, functions::ResolveVariable},
|
||||
config::{server::ServerProtocol, smtp::session::Mechanism},
|
||||
expr::{self, functions::ResolveVariable, *},
|
||||
listener::SessionStream,
|
||||
};
|
||||
use smtp_proto::{
|
||||
|
@ -111,11 +108,6 @@ impl<T: SessionStream> Session<T> {
|
|||
self.write(b"503 5.5.1 AUTH not allowed.\r\n").await?;
|
||||
} else if !self.data.authenticated_as.is_empty() {
|
||||
self.write(b"503 5.5.1 Already authenticated.\r\n").await?;
|
||||
} else if mechanism & (AUTH_LOGIN | AUTH_PLAIN) != 0
|
||||
&& !self.stream.is_tls()
|
||||
&& !self.params.auth_plain_text
|
||||
{
|
||||
self.write(b"503 5.5.1 Clear text authentication without TLS is forbidden.\r\n").await?;
|
||||
} else if let Some(mut token) =
|
||||
SaslToken::from_mechanism(mechanism & auth)
|
||||
{
|
||||
|
@ -407,7 +399,7 @@ impl<T: AsyncWrite + AsyncRead + Unpin> Session<T> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<T: AsyncRead + AsyncWrite> ResolveVariable for Session<T> {
|
||||
impl<T: SessionStream> ResolveVariable for Session<T> {
|
||||
fn resolve_variable(&self, variable: u32) -> expr::Variable<'_> {
|
||||
match variable {
|
||||
V_RECIPIENT => self
|
||||
|
@ -424,6 +416,13 @@ impl<T: AsyncRead + AsyncWrite> ResolveVariable for Session<T> {
|
|||
.map(|r| r.domain.as_str())
|
||||
.unwrap_or_default()
|
||||
.into(),
|
||||
V_RECIPIENTS => self
|
||||
.data
|
||||
.rcpt_to
|
||||
.iter()
|
||||
.map(|r| Variable::String(r.address_lcase.as_str().into()))
|
||||
.collect::<Vec<_>>()
|
||||
.into(),
|
||||
V_SENDER => self
|
||||
.data
|
||||
.mail_from
|
||||
|
@ -442,8 +441,12 @@ impl<T: AsyncRead + AsyncWrite> ResolveVariable for Session<T> {
|
|||
V_AUTHENTICATED_AS => self.data.authenticated_as.as_str().into(),
|
||||
V_LISTENER => self.instance.id.as_str().into(),
|
||||
V_REMOTE_IP => self.data.remote_ip_str.as_str().into(),
|
||||
V_REMOTE_PORT => self.data.remote_port.into(),
|
||||
V_LOCAL_IP => self.data.local_ip_str.as_str().into(),
|
||||
V_LOCAL_PORT => self.data.local_port.into(),
|
||||
V_TLS => self.stream.is_tls().into(),
|
||||
V_PRIORITY => self.data.priority.to_string().into(),
|
||||
V_PROTOCOL => self.instance.protocol.as_str().into(),
|
||||
_ => expr::Variable::default(),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -46,7 +46,12 @@ impl SessionManager for SmtpSessionManager {
|
|||
span: session.span,
|
||||
stream: session.stream,
|
||||
in_flight: vec![session.in_flight],
|
||||
data: SessionData::new(session.local_ip, session.remote_ip, session.remote_port),
|
||||
data: SessionData::new(
|
||||
session.local_ip,
|
||||
session.local_port,
|
||||
session.remote_ip,
|
||||
session.remote_port,
|
||||
),
|
||||
params: SessionParameters::default(),
|
||||
};
|
||||
|
||||
|
@ -89,11 +94,13 @@ impl<T: SessionStream> Session<T> {
|
|||
pub async fn init_conn(&mut self) -> bool {
|
||||
self.eval_session_params().await;
|
||||
|
||||
let config = &self.core.core.smtp.session.connect;
|
||||
|
||||
// Sieve filtering
|
||||
if let Some(script) = self
|
||||
.core
|
||||
.core
|
||||
.eval_if::<String, _>(&self.core.core.smtp.session.connect.script, self)
|
||||
.eval_if::<String, _>(&config.script, self)
|
||||
.await
|
||||
.and_then(|name| self.core.core.get_sieve_script(&name))
|
||||
{
|
||||
|
@ -115,7 +122,7 @@ impl<T: SessionStream> Session<T> {
|
|||
self.hostname = self
|
||||
.core
|
||||
.core
|
||||
.eval_if::<String, _>(&self.core.core.network.hostname, self)
|
||||
.eval_if::<String, _>(&config.hostname, self)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
if self.hostname.is_empty() {
|
||||
|
@ -124,14 +131,14 @@ impl<T: SessionStream> Session<T> {
|
|||
event = "hostname",
|
||||
"No hostname configured, using 'localhost'."
|
||||
);
|
||||
self.hostname = "locahost".to_string();
|
||||
self.hostname = "localhost".to_string();
|
||||
}
|
||||
|
||||
// Obtain greeting
|
||||
let greeting = self
|
||||
.core
|
||||
.core
|
||||
.eval_if::<String, _>(&self.core.core.smtp.session.connect.greeting, self)
|
||||
.eval_if::<String, _>(&config.greeting, self)
|
||||
.await
|
||||
.filter(|g| !g.is_empty())
|
||||
.map(|g| format!("220 {}\r\n", g))
|
||||
|
|
|
@ -21,13 +21,13 @@
|
|||
* for more details.
|
||||
*/
|
||||
|
||||
use common::listener::SessionStream;
|
||||
use directory::DirectoryError;
|
||||
use tokio::io::{AsyncRead, AsyncWrite};
|
||||
|
||||
use crate::core::Session;
|
||||
use std::fmt::Write;
|
||||
|
||||
impl<T: AsyncWrite + AsyncRead + Unpin> Session<T> {
|
||||
impl<T: SessionStream> Session<T> {
|
||||
pub async fn handle_vrfy(&mut self, address: String) -> Result<(), ()> {
|
||||
match self
|
||||
.core
|
||||
|
|
|
@ -726,7 +726,15 @@ impl DeliveryAttempt {
|
|||
.core
|
||||
.eval_if::<String, _>(&queue_config.hostname, &envelope)
|
||||
.await
|
||||
.unwrap_or_else(|| "localhost".to_string());
|
||||
.filter(|s| !s.is_empty())
|
||||
.unwrap_or_else(|| {
|
||||
tracing::warn!(parent: &span,
|
||||
context = "queue",
|
||||
event = "ehlo",
|
||||
"No outbound hostname configured, using 'local.host'."
|
||||
);
|
||||
"local.host".to_string()
|
||||
});
|
||||
let params = SessionParams {
|
||||
span: &span,
|
||||
core: &core,
|
||||
|
|
|
@ -26,7 +26,7 @@ use std::{
|
|||
sync::Arc,
|
||||
};
|
||||
|
||||
use common::{config::smtp::V_MX, expr::functions::ResolveVariable};
|
||||
use common::expr::{functions::ResolveVariable, V_MX};
|
||||
use mail_auth::{IpLookupStrategy, MX};
|
||||
use rand::{seq::SliceRandom, Rng};
|
||||
|
||||
|
|
|
@ -28,8 +28,7 @@ use std::{
|
|||
};
|
||||
|
||||
use common::{
|
||||
config::smtp::*,
|
||||
expr::{self, functions::ResolveVariable},
|
||||
expr::{self, functions::ResolveVariable, *},
|
||||
listener::limiter::{ConcurrencyLimiter, InFlight},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
@ -245,6 +244,13 @@ impl<'x> ResolveVariable for QueueEnvelope<'x> {
|
|||
V_SENDER => self.message.return_path_lcase.as_str().into(),
|
||||
V_SENDER_DOMAIN => self.message.return_path_domain.as_str().into(),
|
||||
V_RECIPIENT_DOMAIN => self.domain.into(),
|
||||
V_RECIPIENTS => self
|
||||
.message
|
||||
.recipients
|
||||
.iter()
|
||||
.map(|r| Variable::from(r.address_lcase.as_str()))
|
||||
.collect::<Vec<_>>()
|
||||
.into(),
|
||||
V_MX => self.mx.into(),
|
||||
V_PRIORITY => self.message.priority.into(),
|
||||
V_REMOTE_IP => self.remote_ip.to_string().into(),
|
||||
|
@ -259,6 +265,12 @@ impl ResolveVariable for Message {
|
|||
match variable {
|
||||
V_SENDER => self.return_path_lcase.as_str().into(),
|
||||
V_SENDER_DOMAIN => self.return_path_domain.as_str().into(),
|
||||
V_RECIPIENTS => self
|
||||
.recipients
|
||||
.iter()
|
||||
.map(|r| Variable::from(r.address_lcase.as_str()))
|
||||
.collect::<Vec<_>>()
|
||||
.into(),
|
||||
V_PRIORITY => self.priority.into(),
|
||||
_ => "".into(),
|
||||
}
|
||||
|
|
|
@ -21,15 +21,15 @@
|
|||
* for more details.
|
||||
*/
|
||||
|
||||
use common::listener::SessionStream;
|
||||
use mail_auth::{
|
||||
common::verify::VerifySignature, AuthenticatedMessage, AuthenticationResults, DkimOutput,
|
||||
};
|
||||
use tokio::io::{AsyncRead, AsyncWrite};
|
||||
use utils::config::Rate;
|
||||
|
||||
use crate::core::Session;
|
||||
|
||||
impl<T: AsyncWrite + AsyncRead + Unpin> Session<T> {
|
||||
impl<T: SessionStream> Session<T> {
|
||||
pub async fn send_dkim_report(
|
||||
&self,
|
||||
rcpt: &str,
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
use std::collections::hash_map::Entry;
|
||||
|
||||
use ahash::AHashMap;
|
||||
use common::config::smtp::report::AggregateFrequency;
|
||||
use common::{config::smtp::report::AggregateFrequency, listener::SessionStream};
|
||||
use mail_auth::{
|
||||
common::verify::VerifySignature,
|
||||
dmarc::{self, URI},
|
||||
|
@ -36,7 +36,6 @@ use store::{
|
|||
write::{now, BatchBuilder, Bincode, QueueClass, ReportEvent, ValueClass},
|
||||
Deserialize, IterateParams, Serialize, ValueKey,
|
||||
};
|
||||
use tokio::io::{AsyncRead, AsyncWrite};
|
||||
use utils::config::Rate;
|
||||
|
||||
use crate::{
|
||||
|
@ -53,7 +52,7 @@ pub struct DmarcFormat {
|
|||
pub records: Vec<Record>,
|
||||
}
|
||||
|
||||
impl<T: AsyncWrite + AsyncRead + Unpin> Session<T> {
|
||||
impl<T: SessionStream> Session<T> {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn send_dmarc_report(
|
||||
&self,
|
||||
|
|
|
@ -21,13 +21,13 @@
|
|||
* for more details.
|
||||
*/
|
||||
|
||||
use common::listener::SessionStream;
|
||||
use mail_auth::{report::AuthFailureType, AuthenticationResults, SpfOutput};
|
||||
use tokio::io::{AsyncRead, AsyncWrite};
|
||||
use utils::config::Rate;
|
||||
|
||||
use crate::core::Session;
|
||||
|
||||
impl<T: AsyncWrite + AsyncRead + Unpin> Session<T> {
|
||||
impl<T: SessionStream> Session<T> {
|
||||
pub async fn send_spf_report(
|
||||
&self,
|
||||
rcpt: &str,
|
||||
|
|
|
@ -55,8 +55,8 @@ impl SMTP {
|
|||
.filter(params.message.as_deref().map_or(b"", |m| &m[..]))
|
||||
.with_vars_env(params.variables)
|
||||
.with_envelope_list(params.envelope)
|
||||
.with_user_address(&self.core.sieve.from_addr)
|
||||
.with_user_full_name(&self.core.sieve.from_name);
|
||||
.with_user_address(¶ms.from_addr)
|
||||
.with_user_full_name(¶ms.from_name);
|
||||
let mut input = Input::script("__script", script);
|
||||
let mut messages: Vec<Vec<u8>> = Vec::new();
|
||||
|
||||
|
@ -149,10 +149,10 @@ impl SMTP {
|
|||
message_id,
|
||||
} => {
|
||||
// Build message
|
||||
let return_path_lcase = self.core.sieve.return_path.to_lowercase();
|
||||
let return_path_lcase = params.return_path.to_lowercase();
|
||||
let return_path_domain = return_path_lcase.domain_part().to_string();
|
||||
let mut message = self.new_message(
|
||||
self.core.sieve.return_path.clone(),
|
||||
params.return_path.clone(),
|
||||
return_path_lcase,
|
||||
return_path_domain,
|
||||
);
|
||||
|
@ -265,9 +265,9 @@ impl SMTP {
|
|||
instance.message().raw_message().into()
|
||||
};
|
||||
if let Some(raw_message) = raw_message {
|
||||
let headers = if !self.core.sieve.sign.is_empty() {
|
||||
let headers = if !params.sign.is_empty() {
|
||||
let mut headers = Vec::new();
|
||||
for dkim in &self.core.sieve.sign {
|
||||
for dkim in ¶ms.sign {
|
||||
if let Some(dkim) = self.core.get_dkim_signer(dkim) {
|
||||
match dkim.sign(raw_message) {
|
||||
Ok(signature) => {
|
||||
|
|
|
@ -139,6 +139,7 @@ impl<T: SessionStream> Session<T> {
|
|||
pub async fn run_script(&self, script: Arc<Sieve>, params: ScriptParameters) -> ScriptResult {
|
||||
let core = self.core.clone();
|
||||
let span = self.span.clone();
|
||||
let params = params.with_envelope(&self.core.core, self).await;
|
||||
|
||||
let handle = Handle::current();
|
||||
self.core
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
use std::{borrow::Cow, sync::Arc};
|
||||
|
||||
use ahash::AHashMap;
|
||||
use common::scripts::ScriptModification;
|
||||
use common::{expr::functions::ResolveVariable, scripts::ScriptModification, Core};
|
||||
use sieve::{runtime::Variable, Envelope};
|
||||
|
||||
pub mod envelope;
|
||||
|
@ -48,6 +48,10 @@ pub struct ScriptParameters {
|
|||
message: Option<Arc<Vec<u8>>>,
|
||||
variables: AHashMap<Cow<'static, str>, Variable>,
|
||||
envelope: Vec<(Envelope, Variable)>,
|
||||
from_addr: String,
|
||||
from_name: String,
|
||||
return_path: String,
|
||||
sign: Vec<String>,
|
||||
#[cfg(feature = "test_mode")]
|
||||
expected_variables: Option<AHashMap<String, Variable>>,
|
||||
}
|
||||
|
@ -60,9 +64,29 @@ impl ScriptParameters {
|
|||
message: None,
|
||||
#[cfg(feature = "test_mode")]
|
||||
expected_variables: None,
|
||||
from_addr: Default::default(),
|
||||
from_name: Default::default(),
|
||||
return_path: Default::default(),
|
||||
sign: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn with_envelope(mut self, core: &Core, vars: &impl ResolveVariable) -> Self {
|
||||
for (variable, expr) in [
|
||||
(&mut self.from_addr, &core.sieve.from_addr),
|
||||
(&mut self.from_name, &core.sieve.from_name),
|
||||
(&mut self.return_path, &core.sieve.return_path),
|
||||
] {
|
||||
if let Some(value) = core.eval_if(expr, vars).await {
|
||||
*variable = value;
|
||||
}
|
||||
}
|
||||
if let Some(value) = core.eval_if(&core.sieve.sign, vars).await {
|
||||
self.sign = value;
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_message(self, message: Arc<Vec<u8>>) -> Self {
|
||||
Self {
|
||||
message: message.into(),
|
||||
|
|
|
@ -60,7 +60,10 @@ impl FdbStore {
|
|||
})
|
||||
.ok()?;
|
||||
|
||||
if let Some(value) = config.property::<Duration>((&prefix, "transaction.timeout")) {
|
||||
if let Some(value) = config
|
||||
.property::<Option<Duration>>((&prefix, "transaction.timeout"))
|
||||
.unwrap_or_default()
|
||||
{
|
||||
db.set_option(DatabaseOption::TransactionTimeout(value.as_millis() as i32))
|
||||
.map_err(|err| {
|
||||
config.new_build_error(
|
||||
|
@ -80,7 +83,10 @@ impl FdbStore {
|
|||
})
|
||||
.ok()?;
|
||||
}
|
||||
if let Some(value) = config.property::<Duration>((&prefix, "transaction.max-retry-delay")) {
|
||||
if let Some(value) = config
|
||||
.property::<Option<Duration>>((&prefix, "transaction.max-retry-delay"))
|
||||
.unwrap_or_default()
|
||||
{
|
||||
db.set_option(DatabaseOption::TransactionMaxRetryDelay(
|
||||
value.as_millis() as i32
|
||||
))
|
||||
|
|
|
@ -93,7 +93,13 @@ impl Stores {
|
|||
}
|
||||
|
||||
if has_others {
|
||||
if value == "true" {
|
||||
Value::Integer(1.into())
|
||||
} else if value == "false" {
|
||||
Value::Integer(0.into())
|
||||
} else {
|
||||
Value::Text(value.to_string().into())
|
||||
}
|
||||
} else if has_floats {
|
||||
value
|
||||
.parse()
|
||||
|
|
|
@ -49,7 +49,8 @@ impl MysqlStore {
|
|||
.max_allowed_packet(config.property((&prefix, "max-allowed-packet")))
|
||||
.wait_timeout(
|
||||
config
|
||||
.property::<Duration>((&prefix, "timeout"))
|
||||
.property::<Option<Duration>>((&prefix, "timeout"))
|
||||
.unwrap_or_default()
|
||||
.map(|t| t.as_secs() as usize),
|
||||
);
|
||||
if let Some(port) = config.property((&prefix, "port")) {
|
||||
|
@ -68,10 +69,16 @@ impl MysqlStore {
|
|||
// Configure connection pool
|
||||
let mut pool_min = PoolConstraints::default().min();
|
||||
let mut pool_max = PoolConstraints::default().max();
|
||||
if let Some(n_size) = config.property::<usize>((&prefix, "pool.min-connections")) {
|
||||
if let Some(n_size) = config
|
||||
.property::<usize>((&prefix, "pool.min-connections"))
|
||||
.filter(|&n| n > 0)
|
||||
{
|
||||
pool_min = n_size;
|
||||
}
|
||||
if let Some(n_size) = config.property::<usize>((&prefix, "pool.max-connections")) {
|
||||
if let Some(n_size) = config
|
||||
.property::<usize>((&prefix, "pool.max-connections"))
|
||||
.filter(|&n| n > 0)
|
||||
{
|
||||
pool_max = n_size;
|
||||
}
|
||||
opts = opts.pool_opts(
|
||||
|
|
|
@ -21,6 +21,8 @@
|
|||
* for more details.
|
||||
*/
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::{
|
||||
backend::postgres::tls::MakeRustlsConnect, SUBSPACE_BITMAPS, SUBSPACE_BLOBS, SUBSPACE_COUNTERS,
|
||||
SUBSPACE_INDEXES, SUBSPACE_LOGS, SUBSPACE_VALUES,
|
||||
|
@ -46,7 +48,9 @@ impl PostgresStore {
|
|||
cfg.user = config.value((&prefix, "user")).map(|s| s.to_string());
|
||||
cfg.password = config.value((&prefix, "password")).map(|s| s.to_string());
|
||||
cfg.port = config.property((&prefix, "port"));
|
||||
cfg.connect_timeout = config.property((&prefix, "timeout"));
|
||||
cfg.connect_timeout = config
|
||||
.property::<Option<Duration>>((&prefix, "timeout"))
|
||||
.unwrap_or_default();
|
||||
cfg.manager = Some(ManagerConfig {
|
||||
recycling_method: RecyclingMethod::Fast,
|
||||
});
|
||||
|
|
|
@ -67,7 +67,8 @@ impl RedisStore {
|
|||
return None;
|
||||
}
|
||||
|
||||
Some(match config.value_require((&prefix, "redis-type"))? {
|
||||
Some(
|
||||
match config.value((&prefix, "redis-type")).unwrap_or("single") {
|
||||
"single" => {
|
||||
let client = Client::open(urls.into_iter().next().unwrap())
|
||||
.map_err(|err| {
|
||||
|
@ -79,7 +80,7 @@ impl RedisStore {
|
|||
.ok()?;
|
||||
let timeout = config
|
||||
.property_or_default((&prefix, "timeout"), "10s")
|
||||
.unwrap_or_else(|| Duration::from_secs(10));
|
||||
.unwrap_or_default();
|
||||
|
||||
Self {
|
||||
pool: RedisPool::Single(
|
||||
|
@ -105,10 +106,16 @@ impl RedisStore {
|
|||
if let Some(value) = config.property((&prefix, "retry.total")) {
|
||||
builder = builder.retries(value);
|
||||
}
|
||||
if let Some(value) = config.property::<Duration>((&prefix, "retry.max-wait")) {
|
||||
if let Some(value) = config
|
||||
.property::<Option<Duration>>((&prefix, "retry.max-wait"))
|
||||
.unwrap_or_default()
|
||||
{
|
||||
builder = builder.max_retry_wait(value.as_millis() as u64);
|
||||
}
|
||||
if let Some(value) = config.property::<Duration>((&prefix, "retry.min-wait")) {
|
||||
if let Some(value) = config
|
||||
.property::<Option<Duration>>((&prefix, "retry.min-wait"))
|
||||
.unwrap_or_default()
|
||||
{
|
||||
builder = builder.min_retry_wait(value.as_millis() as u64);
|
||||
}
|
||||
if let Some(true) = config.property::<bool>((&prefix, "read-from-replicas")) {
|
||||
|
@ -125,7 +132,7 @@ impl RedisStore {
|
|||
})
|
||||
.ok()?;
|
||||
let timeout = config
|
||||
.property_or_default((&prefix, "timeout"), "10s")
|
||||
.property_or_default::<Duration>((&prefix, "timeout"), "10s")
|
||||
.unwrap_or_else(|| Duration::from_secs(10));
|
||||
|
||||
Self {
|
||||
|
@ -150,7 +157,8 @@ impl RedisStore {
|
|||
config.new_parse_error((&prefix, "redis-type"), err);
|
||||
return None;
|
||||
}
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -168,12 +176,19 @@ fn build_pool<M: Manager>(
|
|||
)
|
||||
.create_timeout(
|
||||
config
|
||||
.property_or_default::<Duration>((prefix, "pool.create-timeout"), "30s")
|
||||
.unwrap_or_else(|| Duration::from_secs(30))
|
||||
.into(),
|
||||
.property_or_default::<Option<Duration>>((prefix, "pool.create-timeout"), "30s")
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
.wait_timeout(
|
||||
config
|
||||
.property_or_default::<Option<Duration>>((prefix, "pool.wait-timeout"), "30s")
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
.recycle_timeout(
|
||||
config
|
||||
.property_or_default::<Option<Duration>>((prefix, "pool.recycle-timeout"), "30s")
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
.wait_timeout(config.property_or_default((prefix, "pool.wait-timeout"), "30s"))
|
||||
.recycle_timeout(config.property_or_default((prefix, "pool.recycle-timeout"), "30s"))
|
||||
.build()
|
||||
.map_err(|err| {
|
||||
format!(
|
||||
|
|
|
@ -207,17 +207,18 @@ impl<'x, 'y> TomlParser<'x, 'y> {
|
|||
|
||||
#[allow(clippy::while_let_on_iterator)]
|
||||
fn key(&mut self, mut key: String, in_curly: bool) -> Result<(String, char)> {
|
||||
let start_key_len = key.len();
|
||||
while let Some(ch) = self.iter.next() {
|
||||
match ch {
|
||||
'=' => {
|
||||
if !key.is_empty() {
|
||||
if start_key_len != key.len() {
|
||||
return Ok((key, ch));
|
||||
} else {
|
||||
return Err(format!("Empty key at line: {}", self.line));
|
||||
}
|
||||
}
|
||||
',' | '}' if in_curly => {
|
||||
if !key.is_empty() {
|
||||
if start_key_len != key.len() {
|
||||
return Ok((key, ch));
|
||||
} else {
|
||||
return Err(format!("Empty key at line: {}", self.line));
|
||||
|
@ -236,7 +237,7 @@ impl<'x, 'y> TomlParser<'x, 'y> {
|
|||
}
|
||||
'\n' => {
|
||||
return Err(format!(
|
||||
"Unexpected end of line at line: {}",
|
||||
"Unexpected end of line while parsing quoted key at line: {}",
|
||||
self.line
|
||||
));
|
||||
}
|
||||
|
@ -249,7 +250,14 @@ impl<'x, 'y> TomlParser<'x, 'y> {
|
|||
}
|
||||
' ' | '\t' | '\r' => (),
|
||||
'\n' => {
|
||||
return Err(format!("Unexpected end of line at line: {}", self.line));
|
||||
if start_key_len == key.len() {
|
||||
self.line += 1;
|
||||
} else {
|
||||
return Err(format!(
|
||||
"Unexpected end of line while parsing key {:?} at line: {}",
|
||||
key, self.line
|
||||
));
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
return Err(format!(
|
||||
|
|
|
@ -60,13 +60,7 @@ impl Config {
|
|||
let key = key.as_key();
|
||||
let value = match self.keys.get(&key) {
|
||||
Some(value) => value.as_str(),
|
||||
None => {
|
||||
self.warnings.insert(
|
||||
key.clone(),
|
||||
ConfigWarning::AppliedDefault(default.to_string()),
|
||||
);
|
||||
default
|
||||
}
|
||||
None => default,
|
||||
};
|
||||
match T::parse_value(value) {
|
||||
Ok(value) => Some(value),
|
||||
|
@ -80,16 +74,13 @@ impl Config {
|
|||
pub fn property_or_else<T: ParseValue>(
|
||||
&mut self,
|
||||
key: impl AsKey,
|
||||
default: impl AsKey,
|
||||
or_else: impl AsKey,
|
||||
default: &str,
|
||||
) -> Option<T> {
|
||||
let key = key.as_key();
|
||||
let value = match self.value_or_else(key.as_str(), default.clone()) {
|
||||
let value = match self.value_or_else(key.as_str(), or_else.clone()) {
|
||||
Some(value) => value,
|
||||
None => {
|
||||
self.warnings
|
||||
.insert(default.as_key(), ConfigWarning::Missing);
|
||||
return None;
|
||||
}
|
||||
None => default,
|
||||
};
|
||||
|
||||
match T::parse_value(value) {
|
||||
|
@ -216,10 +207,10 @@ impl Config {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn value_or_else(&self, key: impl AsKey, default: impl AsKey) -> Option<&str> {
|
||||
pub fn value_or_else(&self, key: impl AsKey, or_else: impl AsKey) -> Option<&str> {
|
||||
self.keys
|
||||
.get(&key.as_key())
|
||||
.or_else(|| self.keys.get(&default.as_key()))
|
||||
.or_else(|| self.keys.get(&or_else.as_key()))
|
||||
.map(|s| s.as_str())
|
||||
}
|
||||
|
||||
|
@ -260,17 +251,6 @@ impl Config {
|
|||
self.keys.remove(key)
|
||||
}
|
||||
|
||||
pub fn value_or_warn(&mut self, key: impl AsKey) -> Option<&str> {
|
||||
let key = key.as_key();
|
||||
match self.keys.get(&key) {
|
||||
Some(value) => Some(value.as_str()),
|
||||
None => {
|
||||
self.warnings.insert(key, ConfigWarning::Missing);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_parse_error(&mut self, key: impl AsKey, details: impl Into<String>) {
|
||||
self.errors
|
||||
.insert(key.as_key(), ConfigError::Parse(details.into()));
|
||||
|
@ -523,6 +503,12 @@ impl ParseValue for Rate {
|
|||
}
|
||||
}
|
||||
|
||||
impl ParseValue for () {
|
||||
fn parse_value(_: &str) -> super::Result<Self> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub trait AsKey: Clone {
|
||||
fn as_key(&self) -> String;
|
||||
fn as_prefix(&self) -> String;
|
||||
|
|
|
@ -66,11 +66,18 @@ impl From<&str> for PublicSuffix {
|
|||
impl PublicSuffix {
|
||||
#[allow(unused_variables)]
|
||||
pub async fn parse(config: &mut Config, key: &str) -> PublicSuffix {
|
||||
let values = config
|
||||
let mut values = config
|
||||
.values(key)
|
||||
.map(|(_, s)| s.to_string())
|
||||
.collect::<Vec<_>>();
|
||||
let has_values = !values.is_empty();
|
||||
if values.is_empty() {
|
||||
values = vec![
|
||||
"https://publicsuffix.org/list/public_suffix_list.dat".to_string(),
|
||||
"https://raw.githubusercontent.com/publicsuffix/list/master/public_suffix_list.dat"
|
||||
.to_string(),
|
||||
]
|
||||
}
|
||||
|
||||
for (idx, value) in values.into_iter().enumerate() {
|
||||
let bytes = if value.starts_with("https://") || value.starts_with("http://") {
|
||||
let result = match reqwest::get(&value).await {
|
||||
|
@ -157,14 +164,7 @@ impl PublicSuffix {
|
|||
}
|
||||
|
||||
#[cfg(not(feature = "test_mode"))]
|
||||
config.new_build_error(
|
||||
key,
|
||||
if has_values {
|
||||
"Failed to parse public suffixes from any source."
|
||||
} else {
|
||||
"No public suffixes list was specified."
|
||||
},
|
||||
);
|
||||
config.new_build_error(key, "Failed to parse public suffixes from any source.");
|
||||
|
||||
PublicSuffix::default()
|
||||
}
|
||||
|
|
94
resources/config/build.py
Normal file
94
resources/config/build.py
Normal file
|
@ -0,0 +1,94 @@
|
|||
import os
|
||||
|
||||
# Define the scripts and their component files
|
||||
scripts = {
|
||||
"spam-filter": [
|
||||
"config.sieve",
|
||||
"prelude.sieve",
|
||||
"from.sieve",
|
||||
"recipient.sieve",
|
||||
"subject.sieve",
|
||||
"replyto.sieve",
|
||||
"date.sieve",
|
||||
"messageid.sieve",
|
||||
"received.sieve",
|
||||
"headers.sieve",
|
||||
"bounce.sieve",
|
||||
"html.sieve",
|
||||
"mime.sieve",
|
||||
"dmarc.sieve",
|
||||
"ip.sieve",
|
||||
"helo.sieve",
|
||||
"replies_in.sieve",
|
||||
"spamtrap.sieve",
|
||||
"bayes_classify.sieve",
|
||||
"url.sieve",
|
||||
"rbl.sieve",
|
||||
"pyzor.sieve",
|
||||
"composites.sieve",
|
||||
"scores.sieve",
|
||||
"reputation.sieve",
|
||||
"epilogue.sieve"
|
||||
],
|
||||
"track-replies": [
|
||||
"config.sieve",
|
||||
"replies_out.sieve"
|
||||
],
|
||||
"greylist": [
|
||||
"config.sieve",
|
||||
"greylist.sieve"
|
||||
]
|
||||
}
|
||||
script_names = {
|
||||
"spam-filter" : "Spam Filter",
|
||||
"track-replies" : "Track Replies",
|
||||
"greylist" : "Greylisting"
|
||||
}
|
||||
|
||||
maps = ["scores.map",
|
||||
"allow_dmarc.list",
|
||||
"allow_domains.list",
|
||||
"allow_spf_dkim.list",
|
||||
"domains_disposable.list",
|
||||
"domains_free.list",
|
||||
"mime_types.map",
|
||||
"url_redirectors.list"]
|
||||
|
||||
|
||||
def read_and_concatenate(files):
|
||||
content = ""
|
||||
for file in files:
|
||||
with open(os.path.join("./spamfilter/scripts", file), "r", encoding="utf-8") as f:
|
||||
content += "\n#### Script " + file + " ####\n\n"
|
||||
content += f.read() + "\n"
|
||||
return content
|
||||
|
||||
def read_file(file):
|
||||
with open(file, "r", encoding="utf-8") as f:
|
||||
return f.read() + "\n"
|
||||
|
||||
def build_spam_filters(scripts):
|
||||
spam_filter = read_file("./spamfilter/settings.toml")
|
||||
for script_name, file_list in scripts.items():
|
||||
script_content = read_and_concatenate(file_list).replace("'''", "\\'\\'\\'")
|
||||
script_description = script_names[script_name]
|
||||
spam_filter += f"[sieve.trusted.scripts.{script_name}]\nname = \"{script_description}\"\ncontents = '''\n{script_content}'''\n\n"
|
||||
|
||||
spam_filter += "\n[lookup]\n"
|
||||
for map in maps :
|
||||
with open(os.path.join("./spamfilter/maps", map), "r", encoding="utf-8") as f:
|
||||
spam_filter += f.read() + "\n"
|
||||
|
||||
return spam_filter
|
||||
|
||||
def main():
|
||||
spam_filter = build_spam_filters(scripts)
|
||||
with open("spamfilter.toml", "w", encoding="utf-8") as toml_file:
|
||||
toml_file.write(spam_filter)
|
||||
config = read_file("./minimal.toml") + read_file("./security.toml") + spam_filter
|
||||
with open("config.toml", "w", encoding="utf-8") as toml_file:
|
||||
toml_file.write(config)
|
||||
print("Stalwart TOML configuration files have been generated.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
|
@ -1,35 +0,0 @@
|
|||
#############################################
|
||||
# Cache configuration
|
||||
#############################################
|
||||
|
||||
[cache]
|
||||
capacity = 512
|
||||
shard = 32
|
||||
|
||||
[cache.session]
|
||||
ttl = "1h"
|
||||
|
||||
[cache.account]
|
||||
size = 2048
|
||||
|
||||
[cache.mailbox]
|
||||
size = 2048
|
||||
|
||||
[cache.thread]
|
||||
size = 2048
|
||||
|
||||
[cache.bayes]
|
||||
capacity = 8192
|
||||
|
||||
[cache.bayes.ttl]
|
||||
positive = "1h"
|
||||
negative = "1h"
|
||||
|
||||
[cache.resolver]
|
||||
txt = 2048
|
||||
mx = 1024
|
||||
ipv4 = 1024
|
||||
ipv6 = 1024
|
||||
ptr = 1024
|
||||
tlsa = 1024
|
||||
mta-sts = 1024
|
|
@ -1,37 +0,0 @@
|
|||
#############################################
|
||||
# Server configuration
|
||||
#############################################
|
||||
|
||||
[server]
|
||||
hostname = "%{HOST}%"
|
||||
max-connections = 8192
|
||||
|
||||
#[server.proxy]
|
||||
#trusted-networks = ["127.0.0.0/8", "::1", "10.0.0.0/8"]
|
||||
|
||||
[authentication]
|
||||
fail2ban = "100/1d"
|
||||
rate-limit = "10/1m"
|
||||
|
||||
[server.run-as]
|
||||
user = "stalwart-mail"
|
||||
group = "stalwart-mail"
|
||||
|
||||
[server.socket]
|
||||
nodelay = true
|
||||
reuse-addr = true
|
||||
#reuse-port = true
|
||||
backlog = 1024
|
||||
#ttl = 3600
|
||||
#send-buffer-size = 65535
|
||||
#recv-buffer-size = 65535
|
||||
#linger = 1
|
||||
#tos = 1
|
||||
|
||||
[global]
|
||||
#thread-pool = 8
|
||||
|
||||
[server.http]
|
||||
#headers = ["Access-Control-Allow-Origin: *",
|
||||
# "Access-Control-Allow-Methods: POST, GET, PATCH, PUT, DELETE, HEAD, OPTIONS",
|
||||
# "Access-Control-Allow-Headers: Authorization, Content-Type, Accept, X-Requested-With"]
|
|
@ -1,73 +0,0 @@
|
|||
#############################################
|
||||
# Sieve untrusted runtime configuration
|
||||
#############################################
|
||||
|
||||
[sieve.untrusted]
|
||||
disable-capabilities = []
|
||||
notification-uris = ["mailto"]
|
||||
protected-headers = ["Original-Subject", "Original-From", "Received", "Auto-Submitted"]
|
||||
|
||||
[sieve.untrusted.limits]
|
||||
name-length = 512
|
||||
max-scripts = 256
|
||||
script-size = 102400
|
||||
string-length = 4096
|
||||
variable-name-length = 32
|
||||
variable-size = 4096
|
||||
nested-blocks = 15
|
||||
nested-tests = 15
|
||||
nested-foreverypart = 3
|
||||
match-variables = 30
|
||||
local-variables = 128
|
||||
header-size = 1024
|
||||
includes = 3
|
||||
nested-includes = 3
|
||||
cpu = 5000
|
||||
redirects = 1
|
||||
received-headers = 10
|
||||
outgoing-messages = 3
|
||||
|
||||
[sieve.untrusted.vacation]
|
||||
default-subject = "Automated reply"
|
||||
subject-prefix = "Auto: "
|
||||
|
||||
[sieve.untrusted.default-expiry]
|
||||
vacation = "30d"
|
||||
duplicate = "7d"
|
||||
|
||||
#############################################
|
||||
# Sieve trusted runtime configuration
|
||||
#############################################
|
||||
|
||||
[sieve.trusted]
|
||||
from-name = "Automated Message"
|
||||
from-addr = "no-reply@%{DEFAULT_DOMAIN}%"
|
||||
return-path = ""
|
||||
#hostname = "%{HOST}%"
|
||||
no-capability-check = true
|
||||
sign = ["rsa"]
|
||||
|
||||
[sieve.trusted.limits]
|
||||
redirects = 3
|
||||
out-messages = 5
|
||||
received-headers = 50
|
||||
cpu = 1048576
|
||||
nested-includes = 5
|
||||
duplicate-expiry = "7d"
|
||||
|
||||
[sieve.trusted.scripts]
|
||||
#connect = '''require ["variables", "extlists", "reject"];
|
||||
# if string :list "${env.remote_ip}" "default/blocked-ips" {
|
||||
# reject "Your IP '${env.remote_ip}' is not welcomed here.";
|
||||
# }'''
|
||||
|
||||
#ehlo = '''require ["variables", "extlists", "reject"];
|
||||
# if string :list "${env.helo_domain}" "default/blocked-domains" {
|
||||
# reject "551 5.1.1 Your domain '${env.helo_domain}' has been blacklisted.";
|
||||
# }'''
|
||||
|
||||
#mail = '''require ["variables", "envelope", "reject"];
|
||||
# if envelope :localpart :is "from" "known_spammer" {
|
||||
# reject "We do not accept SPAM.";
|
||||
# }'''
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
#############################################
|
||||
# Storage configuration
|
||||
#############################################
|
||||
|
||||
[storage]
|
||||
data = "%{DEFAULT_STORE}%"
|
||||
fts = "%{DEFAULT_STORE}%"
|
||||
blob = "%{DEFAULT_STORE}%"
|
||||
lookup = "%{DEFAULT_STORE}%"
|
||||
directory = "%{DEFAULT_DIRECTORY}%"
|
||||
|
||||
[storage.encryption]
|
||||
enable = true
|
||||
append = false
|
||||
|
||||
[storage.full-text]
|
||||
default-language = "en"
|
||||
|
||||
[storage.cluster]
|
||||
node-id = 1
|
|
@ -1,30 +0,0 @@
|
|||
#############################################
|
||||
# TLS default configuration
|
||||
#############################################
|
||||
|
||||
[server.tls]
|
||||
enable = true
|
||||
implicit = false
|
||||
timeout = "1m"
|
||||
certificate = "default"
|
||||
#acme = "letsencrypt"
|
||||
#protocols = ["TLSv1.2", "TLSv1.3"]
|
||||
#ciphers = [ "TLS13_AES_256_GCM_SHA384", "TLS13_AES_128_GCM_SHA256",
|
||||
# "TLS13_CHACHA20_POLY1305_SHA256", "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
|
||||
# "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256",
|
||||
# "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
|
||||
# "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256"]
|
||||
ignore-client-order = true
|
||||
|
||||
[acme."letsencrypt"]
|
||||
directory = "https://acme-v02.api.letsencrypt.org/directory"
|
||||
#directory = "https://acme-staging-v02.api.letsencrypt.org/directory"
|
||||
contact = ["postmaster@%{DEFAULT_DOMAIN}%"]
|
||||
cache = "%{BASE_PATH}%/etc/acme"
|
||||
port = 443
|
||||
renew-before = "30d"
|
||||
|
||||
[certificate."default"]
|
||||
sni-subjects = []
|
||||
cert = "file://__CERT_PATH__"
|
||||
private-key = "file://__PK_PATH__"
|
|
@ -1,24 +0,0 @@
|
|||
#############################################
|
||||
# Tracing & logging configuration
|
||||
#############################################
|
||||
|
||||
[tracing."stdout"]
|
||||
method = "stdout"
|
||||
level = "trace"
|
||||
enable = false
|
||||
|
||||
[tracing."ot"]
|
||||
method = "open-telemetry"
|
||||
transport = "http"
|
||||
endpoint = "https://127.0.0.1/otel"
|
||||
headers = ["Authorization: <place_auth_here>"]
|
||||
level = "debug"
|
||||
enable = false
|
||||
|
||||
[tracing."log"]
|
||||
method = "log"
|
||||
path = "%{BASE_PATH}%/logs"
|
||||
prefix = "stalwart.log"
|
||||
rotate = "daily"
|
||||
level = "info"
|
||||
enable = true
|
|
@ -1,52 +0,0 @@
|
|||
#############################################
|
||||
# Stalwart Mail Server Configuration File
|
||||
#############################################
|
||||
|
||||
[macros]
|
||||
host = "__HOST__"
|
||||
default_domain = "__DOMAIN__"
|
||||
base_path = "__BASE_PATH__"
|
||||
default_directory = "__DIRECTORY__"
|
||||
default_store = "__STORE__"
|
||||
|
||||
[include]
|
||||
files = [ "%{BASE_PATH}%/etc/common/server.toml",
|
||||
"%{BASE_PATH}%/etc/common/tls.toml",
|
||||
"%{BASE_PATH}%/etc/common/store.toml",
|
||||
"%{BASE_PATH}%/etc/common/tracing.toml",
|
||||
"%{BASE_PATH}%/etc/common/sieve.toml",
|
||||
"%{BASE_PATH}%/etc/common/cache.toml",
|
||||
"%{BASE_PATH}%/etc/directory/imap.toml",
|
||||
"%{BASE_PATH}%/etc/directory/internal.toml",
|
||||
"%{BASE_PATH}%/etc/directory/ldap.toml",
|
||||
"%{BASE_PATH}%/etc/directory/lmtp.toml",
|
||||
"%{BASE_PATH}%/etc/directory/memory.toml",
|
||||
"%{BASE_PATH}%/etc/directory/sql.toml",
|
||||
"%{BASE_PATH}%/etc/store/elasticsearch.toml",
|
||||
"%{BASE_PATH}%/etc/store/fs.toml",
|
||||
"%{BASE_PATH}%/etc/store/foundationdb.toml",
|
||||
"%{BASE_PATH}%/etc/store/mysql.toml",
|
||||
"%{BASE_PATH}%/etc/store/postgresql.toml",
|
||||
"%{BASE_PATH}%/etc/store/redis.toml",
|
||||
"%{BASE_PATH}%/etc/store/rocksdb.toml",
|
||||
"%{BASE_PATH}%/etc/store/s3.toml",
|
||||
"%{BASE_PATH}%/etc/store/sqlite.toml",
|
||||
"%{BASE_PATH}%/etc/imap/listener.toml",
|
||||
"%{BASE_PATH}%/etc/imap/settings.toml",
|
||||
"%{BASE_PATH}%/etc/jmap/auth.toml",
|
||||
"%{BASE_PATH}%/etc/jmap/listener.toml",
|
||||
"%{BASE_PATH}%/etc/jmap/oauth.toml",
|
||||
"%{BASE_PATH}%/etc/jmap/protocol.toml",
|
||||
"%{BASE_PATH}%/etc/jmap/push.toml",
|
||||
"%{BASE_PATH}%/etc/jmap/ratelimit.toml",
|
||||
"%{BASE_PATH}%/etc/jmap/websockets.toml",
|
||||
"%{BASE_PATH}%/etc/smtp/auth.toml",
|
||||
"%{BASE_PATH}%/etc/smtp/listener.toml",
|
||||
"%{BASE_PATH}%/etc/smtp/milter.toml",
|
||||
"%{BASE_PATH}%/etc/smtp/queue.toml",
|
||||
"%{BASE_PATH}%/etc/smtp/remote.toml",
|
||||
"%{BASE_PATH}%/etc/smtp/report.toml",
|
||||
"%{BASE_PATH}%/etc/smtp/resolver.toml",
|
||||
"%{BASE_PATH}%/etc/smtp/session.toml",
|
||||
"%{BASE_PATH}%/etc/smtp/signature.toml",
|
||||
"%{BASE_PATH}%/etc/smtp/spamfilter.toml" ]
|
|
@ -1,29 +0,0 @@
|
|||
#############################################
|
||||
# IMAP Directory configuration
|
||||
#############################################
|
||||
|
||||
[directory."imap"]
|
||||
type = "imap"
|
||||
host = "127.0.0.1"
|
||||
port = 993
|
||||
disable = true
|
||||
|
||||
[directory."imap".pool]
|
||||
max-connections = 10
|
||||
|
||||
[directory."imap".pool.timeout]
|
||||
create = "30s"
|
||||
wait = "30s"
|
||||
recycle = "30s"
|
||||
|
||||
[directory."imap".tls]
|
||||
enable = true
|
||||
allow-invalid-certs = true
|
||||
|
||||
[directory."imap".cache]
|
||||
entries = 500
|
||||
ttl = {positive = '1h', negative = '10m'}
|
||||
|
||||
[directory."imap".lookup]
|
||||
domains = ["%{DEFAULT_DOMAIN}%"]
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
#############################################
|
||||
# Internal Directory configuration
|
||||
#############################################
|
||||
|
||||
[directory."internal"]
|
||||
type = "internal"
|
||||
store = "%{DEFAULT_STORE}%"
|
||||
disable = true
|
||||
|
||||
[directory."internal".options]
|
||||
catch-all = true
|
||||
#catch-all = [ { if = "matches('(.+)@(.+)$', address)", then = "'info@' + $2" },
|
||||
# { else = false } ]
|
||||
subaddressing = true
|
||||
#subaddressing = [ { if = "matches('^([^.]+)\\.([^.]+)@(.+)$', address)", then = "$2 + '@' + $3" },
|
||||
# { else = false } ]
|
||||
|
||||
[directory."internal".cache]
|
||||
entries = 500
|
||||
ttl = {positive = '1h', negative = '10m'}
|
|
@ -1,60 +0,0 @@
|
|||
#############################################
|
||||
# LDAP Directory configuration
|
||||
#############################################
|
||||
|
||||
[directory."ldap"]
|
||||
type = "ldap"
|
||||
url = "ldap://localhost:389"
|
||||
base-dn = "dc=example,dc=org"
|
||||
timeout = "30s"
|
||||
disable = true
|
||||
|
||||
[directory."ldap".bind]
|
||||
dn = "cn=serviceuser,ou=svcaccts,dc=example,dc=org"
|
||||
secret = "mysecret"
|
||||
|
||||
[directory."ldap".bind.auth]
|
||||
enable = false
|
||||
dn = "cn=?,ou=svcaccts,dc=example,dc=org"
|
||||
|
||||
[directory."ldap".tls]
|
||||
enable = false
|
||||
allow-invalid-certs = false
|
||||
|
||||
[directory."ldap".cache]
|
||||
entries = 500
|
||||
ttl = {positive = '1h', negative = '10m'}
|
||||
|
||||
[directory."ldap".options]
|
||||
catch-all = true
|
||||
#catch-all = [ { if = "matches('(.+)@(.+)$', address)", then = "'info@' + $2" },
|
||||
# { else = false } ]
|
||||
subaddressing = true
|
||||
#subaddressing = [ { if = "matches('^([^.]+)\\.([^.]+)@(.+)$', address)", then = "$2 + '@' + $3" },
|
||||
# { else = false } ]
|
||||
|
||||
[directory."ldap".pool]
|
||||
max-connections = 10
|
||||
|
||||
[directory."ldap".pool.timeout]
|
||||
create = "30s"
|
||||
wait = "30s"
|
||||
recycle = "30s"
|
||||
|
||||
[directory."ldap".filter]
|
||||
name = "(&(|(objectClass=posixAccount)(objectClass=posixGroup))(uid=?))"
|
||||
email = "(&(|(objectClass=posixAccount)(objectClass=posixGroup))(|(mail=?)(mailAlias=?)(mailList=?)))"
|
||||
verify = "(&(|(objectClass=posixAccount)(objectClass=posixGroup))(|(mail=*?*)(mailAlias=*?*)))"
|
||||
expand = "(&(|(objectClass=posixAccount)(objectClass=posixGroup))(mailList=?))"
|
||||
domains = "(&(|(objectClass=posixAccount)(objectClass=posixGroup))(|(mail=*@?)(mailAlias=*@?)))"
|
||||
|
||||
[directory."ldap".attributes]
|
||||
name = "uid"
|
||||
class = "objectClass"
|
||||
description = ["principalName", "description"]
|
||||
secret = "userPassword"
|
||||
groups = ["memberOf", "otherGroups"]
|
||||
email = "mail"
|
||||
email-alias = "mailAlias"
|
||||
quota = "diskQuota"
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
#############################################
|
||||
# LMTP Directory configuration
|
||||
#############################################
|
||||
|
||||
[directory."lmtp"]
|
||||
type = "lmtp"
|
||||
host = "127.0.0.1"
|
||||
port = 11200
|
||||
disable = true
|
||||
|
||||
[directory."lmtp".limits]
|
||||
auth-errors = 3
|
||||
rcpt = 5
|
||||
|
||||
[directory."lmtp".pool]
|
||||
max-connections = 10
|
||||
|
||||
[directory."lmtp".pool.timeout]
|
||||
create = "30s"
|
||||
wait = "30s"
|
||||
recycle = "30s"
|
||||
|
||||
[directory."lmtp".tls]
|
||||
enable = false
|
||||
allow-invalid-certs = true
|
||||
|
||||
[directory."lmtp".cache]
|
||||
entries = 500
|
||||
ttl = {positive = '1h', negative = '10m'}
|
||||
|
||||
[directory."lmtp".lookup]
|
||||
domains = ["%{DEFAULT_DOMAIN}%"]
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
#############################################
|
||||
# In-Memory Directory configuration
|
||||
#############################################
|
||||
|
||||
[directory."memory"]
|
||||
type = "memory"
|
||||
disable = true
|
||||
|
||||
[directory."memory".options]
|
||||
catch-all = true
|
||||
#catch-all = [ { if = "matches('(.+)@(.+)$', address)", then = "'info@' + $2" },
|
||||
# { else = false } ]
|
||||
subaddressing = true
|
||||
#subaddressing = [ { if = "matches('^([^.]+)\\.([^.]+)@(.+)$', address)", then = "$2 + '@' + $3" },
|
||||
# { else = false } ]
|
||||
|
||||
[[directory."memory".principals]]
|
||||
name = "admin"
|
||||
class = "admin"
|
||||
description = "Superuser"
|
||||
secret = "changeme"
|
||||
email = ["postmaster@%{DEFAULT_DOMAIN}%"]
|
||||
|
||||
[[directory."memory".principals]]
|
||||
name = "john"
|
||||
class = "individual"
|
||||
description = "John Doe"
|
||||
secret = "12345"
|
||||
email = ["john@%{DEFAULT_DOMAIN}%", "jdoe@%{DEFAULT_DOMAIN}%", "john.doe@%{DEFAULT_DOMAIN}%"]
|
||||
email-list = ["info@%{DEFAULT_DOMAIN}%"]
|
||||
member-of = ["sales"]
|
||||
|
||||
[[directory."memory".principals]]
|
||||
name = "jane"
|
||||
class = "individual"
|
||||
description = "Jane Doe"
|
||||
secret = "abcde"
|
||||
email = ["jane@%{DEFAULT_DOMAIN}%", "jane.doe@%{DEFAULT_DOMAIN}%"]
|
||||
email-list = ["info@%{DEFAULT_DOMAIN}%"]
|
||||
member-of = ["sales", "support"]
|
||||
|
||||
[[directory."memory".principals]]
|
||||
name = "bill"
|
||||
class = "individual"
|
||||
description = "Bill Foobar"
|
||||
secret = "$2y$05$bvIG6Nmid91Mu9RcmmWZfO5HJIMCT8riNW0hEp8f6/FuA2/mHZFpe"
|
||||
quota = 50000000
|
||||
email = ["bill@%{DEFAULT_DOMAIN}%", "bill.foobar@%{DEFAULT_DOMAIN}%"]
|
||||
email-list = ["info@%{DEFAULT_DOMAIN}%"]
|
||||
|
||||
[[directory."memory".principals]]
|
||||
name = "sales"
|
||||
class = "group"
|
||||
description = "Sales Team"
|
||||
|
||||
[[directory."memory".principals]]
|
||||
name = "support"
|
||||
class = "group"
|
||||
description = "Support Team"
|
|
@ -1,26 +0,0 @@
|
|||
#############################################
|
||||
# SQL Directory configuration
|
||||
#############################################
|
||||
|
||||
[directory."sql"]
|
||||
type = "sql"
|
||||
store = "__SQL_STORE__"
|
||||
disable = true
|
||||
|
||||
[directory."sql".options]
|
||||
catch-all = true
|
||||
#catch-all = [ { if = "matches('(.+)@(.+)$', address)", then = "'info@' + $2" },
|
||||
# { else = false } ]
|
||||
subaddressing = true
|
||||
#subaddressing = [ { if = "matches('^([^.]+)\\.([^.]+)@(.+)$', address)", then = "$2 + '@' + $3" },
|
||||
# { else = false } ]
|
||||
|
||||
[directory."sql".cache]
|
||||
entries = 500
|
||||
ttl = {positive = '1h', negative = '10m'}
|
||||
|
||||
[directory."sql".columns]
|
||||
class = "type"
|
||||
secret = "secret"
|
||||
description = "description"
|
||||
quota = "quota"
|
|
@ -1,16 +0,0 @@
|
|||
#############################################
|
||||
# IMAP server listeners configuration
|
||||
#############################################
|
||||
|
||||
[server.listener."imap"]
|
||||
bind = ["[::]:143"]
|
||||
protocol = "imap"
|
||||
|
||||
[server.listener."imaptls"]
|
||||
bind = ["[::]:993"]
|
||||
protocol = "imap"
|
||||
tls.implicit = true
|
||||
|
||||
[server.listener."sieve"]
|
||||
bind = ["[::]:4190"]
|
||||
protocol = "managesieve"
|
|
@ -1,22 +0,0 @@
|
|||
#############################################
|
||||
# IMAP server settings
|
||||
#############################################
|
||||
|
||||
[imap.request]
|
||||
max-size = 52428800
|
||||
|
||||
[imap.auth]
|
||||
max-failures = 3
|
||||
allow-plain-text = false
|
||||
|
||||
[imap.folders.name]
|
||||
shared = "Shared Folders"
|
||||
|
||||
[imap.timeout]
|
||||
authenticated = "30m"
|
||||
anonymous = "1m"
|
||||
idle = "30m"
|
||||
|
||||
[imap.rate-limit]
|
||||
requests = "2000/1m"
|
||||
concurrent = 6
|
|
@ -1,6 +0,0 @@
|
|||
#############################################
|
||||
# JMAP authentication & session configuration
|
||||
#############################################
|
||||
|
||||
[jmap.session.purge]
|
||||
frequency = "15 * *"
|
|
@ -1,14 +0,0 @@
|
|||
#############################################
|
||||
# JMAP server listener configuration
|
||||
#############################################
|
||||
|
||||
[server.listener."jmap"]
|
||||
protocol = "jmap"
|
||||
bind = ["[::]:443"]
|
||||
url = "https://%{HOST}%"
|
||||
|
||||
[server.listener."jmap".tls]
|
||||
implicit = true
|
||||
|
||||
#bind = ["[::]:8080"]
|
||||
#url = "https://%{HOST}%:8080"
|
|
@ -1,16 +0,0 @@
|
|||
#############################################
|
||||
# JMAP OAuth server configuration
|
||||
#############################################
|
||||
|
||||
[oauth]
|
||||
key = "__OAUTH_KEY__"
|
||||
|
||||
[oauth.auth]
|
||||
max-attempts = 3
|
||||
|
||||
[oauth.expiry]
|
||||
user-code = "30m"
|
||||
auth-code = "10m"
|
||||
token = "1h"
|
||||
refresh-token = "30d"
|
||||
refresh-token-renew = "4d"
|
|
@ -1,43 +0,0 @@
|
|||
#############################################
|
||||
# JMAP protocol configuration
|
||||
#############################################
|
||||
|
||||
[jmap.protocol.get]
|
||||
max-objects = 500
|
||||
|
||||
[jmap.protocol.set]
|
||||
max-objects = 500
|
||||
|
||||
[jmap.protocol.request]
|
||||
max-concurrent = 4
|
||||
max-size = 10000000
|
||||
max-calls = 16
|
||||
|
||||
[jmap.protocol.query]
|
||||
max-results = 5000
|
||||
|
||||
[jmap.protocol.upload]
|
||||
max-size = 50000000
|
||||
max-concurrent = 4
|
||||
ttl = "1h"
|
||||
|
||||
[jmap.protocol.upload.quota]
|
||||
files = 1000
|
||||
size = 50000000
|
||||
|
||||
[jmap.protocol.changes]
|
||||
max-results = 5000
|
||||
|
||||
[jmap.mailbox]
|
||||
max-depth = 10
|
||||
max-name-length = 255
|
||||
|
||||
[jmap.email]
|
||||
max-attachment-size = 50000000
|
||||
max-size = 75000000
|
||||
|
||||
[jmap.email.parse]
|
||||
max-items = 10
|
||||
|
||||
[jmap.principal]
|
||||
allow-lookups = true
|
|
@ -1,21 +0,0 @@
|
|||
#############################################
|
||||
# JMAP Push & EventSource configuration
|
||||
#############################################
|
||||
|
||||
[jmap.push]
|
||||
max-total = 100
|
||||
throttle = "1ms"
|
||||
|
||||
[jmap.push.attempts]
|
||||
interval = "1m"
|
||||
max = 3
|
||||
|
||||
[jmap.push.retry]
|
||||
interval = "1s"
|
||||
|
||||
[jmap.push.timeout]
|
||||
request = "10s"
|
||||
verify = "1s"
|
||||
|
||||
[jmap.event-source]
|
||||
throttle = "1s"
|
|
@ -1,9 +0,0 @@
|
|||
|
||||
#############################################
|
||||
# JMAP server rate limiter configuration
|
||||
#############################################
|
||||
|
||||
[jmap.rate-limit]
|
||||
account = "1000/1m"
|
||||
anonymous = "100/1m"
|
||||
use-forwarded = false
|
|
@ -1,8 +0,0 @@
|
|||
#############################################
|
||||
# JMAP WebSockets server configuration
|
||||
#############################################
|
||||
|
||||
[jmap.web-sockets]
|
||||
throttle = "1s"
|
||||
timeout = "10m"
|
||||
heartbeat = "1m"
|
72
resources/config/minimal.toml
Normal file
72
resources/config/minimal.toml
Normal file
|
@ -0,0 +1,72 @@
|
|||
#############################################
|
||||
# Stalwart Mail Server Configuration File
|
||||
#############################################
|
||||
|
||||
[server.listener."smtp"]
|
||||
bind = ["[::]:25"]
|
||||
protocol = "smtp"
|
||||
|
||||
[server.listener."submission"]
|
||||
bind = ["[::]:587"]
|
||||
protocol = "smtp"
|
||||
|
||||
[server.listener."submissions"]
|
||||
bind = ["[::]:465"]
|
||||
protocol = "smtp"
|
||||
tls.implicit = true
|
||||
|
||||
[server.listener."imap"]
|
||||
bind = ["[::]:143"]
|
||||
protocol = "imap"
|
||||
|
||||
[server.listener."imaptls"]
|
||||
bind = ["[::]:993"]
|
||||
protocol = "imap"
|
||||
tls.implicit = true
|
||||
|
||||
[server.listener."sieve"]
|
||||
bind = ["[::]:4190"]
|
||||
protocol = "managesieve"
|
||||
|
||||
[server.listener."https"]
|
||||
protocol = "http"
|
||||
bind = ["[::]:443"]
|
||||
tls.implicit = true
|
||||
|
||||
[storage]
|
||||
data = "rocksdb"
|
||||
fts = "rocksdb"
|
||||
blob = "rocksdb"
|
||||
lookup = "rocksdb"
|
||||
directory = "internal"
|
||||
|
||||
[store."rocksdb"]
|
||||
type = "rocksdb"
|
||||
path = "%{env:STALWART_PATH}%/data"
|
||||
compression = "lz4"
|
||||
|
||||
[directory."internal"]
|
||||
type = "internal"
|
||||
store = "rocksdb"
|
||||
|
||||
[lookup.default]
|
||||
domain = "%{env:DOMAIN}%"
|
||||
hostname = "%{env:HOSTNAME}%"
|
||||
|
||||
[oauth]
|
||||
key = "%{env:OAUTH_KEY}%"
|
||||
|
||||
[tracer."stdout"]
|
||||
type = "stdout"
|
||||
level = "info"
|
||||
ansi = false
|
||||
enable = true
|
||||
|
||||
#[server.run-as]
|
||||
#user = "stalwart-mail"
|
||||
#group = "stalwart-mail"
|
||||
|
||||
[server.http]
|
||||
headers = ["Access-Control-Allow-Origin: *",
|
||||
"Access-Control-Allow-Methods: POST, GET, PATCH, PUT, DELETE, HEAD, OPTIONS",
|
||||
"Access-Control-Allow-Headers: Authorization, Content-Type, Accept, X-Requested-With"]
|
19
resources/config/security.toml
Normal file
19
resources/config/security.toml
Normal file
|
@ -0,0 +1,19 @@
|
|||
[[queue.quota]]
|
||||
messages = 100000
|
||||
size = 10737418240 # 10gb
|
||||
enable = true
|
||||
|
||||
[[queue.throttle]]
|
||||
key = ["rcpt_domain"]
|
||||
concurrency = 5
|
||||
enable = true
|
||||
|
||||
[[session.throttle]]
|
||||
key = ["remote_ip"]
|
||||
concurrency = 5
|
||||
enable = true
|
||||
|
||||
[[session.throttle]]
|
||||
key = ["sender_domain", "rcpt"]
|
||||
rate = "25/1h"
|
||||
enable = true
|
|
@ -1,28 +0,0 @@
|
|||
|
||||
#############################################
|
||||
# SMTP DMARC, DKIM, SPF, ARC & IpRev
|
||||
#############################################
|
||||
|
||||
[auth.iprev]
|
||||
verify = [ { if = "listener = 'smtp'", then = "relaxed" },
|
||||
{ else = "disable" } ]
|
||||
|
||||
[auth.dkim]
|
||||
verify = "relaxed"
|
||||
sign = [ { if = "listener != 'smtp'", then = "['rsa']" },
|
||||
{ else = false } ]
|
||||
|
||||
[auth.spf.verify]
|
||||
ehlo = [ { if = "listener = 'smtp'", then = "relaxed" },
|
||||
{ else = "disable" } ]
|
||||
mail-from = [ { if = "listener = 'smtp'", then = "relaxed" },
|
||||
{ else = "disable" } ]
|
||||
|
||||
[auth.arc]
|
||||
verify = "relaxed"
|
||||
seal = "['rsa']"
|
||||
|
||||
[auth.dmarc]
|
||||
verify = [ { if = "listener = 'smtp'", then = "relaxed" },
|
||||
{ else = "disable" } ]
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
#############################################
|
||||
# SMTP server listener configuration
|
||||
#############################################
|
||||
|
||||
[server.listener."smtp"]
|
||||
bind = ["[::]:25"]
|
||||
#greeting = "Stalwart SMTP at your service"
|
||||
protocol = "smtp"
|
||||
|
||||
[server.listener."submission"]
|
||||
bind = ["[::]:587"]
|
||||
protocol = "smtp"
|
||||
|
||||
[server.listener."submissions"]
|
||||
bind = ["[::]:465"]
|
||||
protocol = "smtp"
|
||||
tls.implicit = true
|
||||
|
||||
#[server.listener."management"]
|
||||
#bind = ["127.0.0.1:8080"]
|
||||
#protocol = "http"
|
|
@ -1,26 +0,0 @@
|
|||
#############################################
|
||||
# SMTP inbound Milter configuration
|
||||
#############################################
|
||||
|
||||
#[session.data.milter."rspamd"]
|
||||
#enable = [ { if = "listener = 'smtp'", then = true },
|
||||
# { else = false } ]
|
||||
#hostname = "127.0.0.1"
|
||||
#port = 11332
|
||||
#tls = false
|
||||
#allow-invalid-certs = false
|
||||
|
||||
#[session.data.milter."rspamd".timeout]
|
||||
#connect = "30s"
|
||||
#command = "30s"
|
||||
#data = "60s"
|
||||
|
||||
#[session.data.milter."rspamd".options]
|
||||
#tempfail-on-error = true
|
||||
#max-response-size = 52428800 # 50mb
|
||||
#version = 6
|
||||
|
||||
#[session.data.pipe."spam-assassin"]
|
||||
#command = "spamc"
|
||||
#arguments = []
|
||||
#timeout = "10s"
|
|
@ -1,49 +0,0 @@
|
|||
#############################################
|
||||
# SMTP server queue configuration
|
||||
#############################################
|
||||
|
||||
[queue.schedule]
|
||||
retry = "[2m, 5m, 10m, 15m, 30m, 1h, 2h]"
|
||||
notify = "[1d, 3d]"
|
||||
expire = "5d"
|
||||
|
||||
[queue.outbound]
|
||||
#hostname = "%{HOST}%"
|
||||
next-hop = [ { if = "is_local_domain('%{DEFAULT_DIRECTORY}%', rcpt_domain)", then = "'local'" },
|
||||
{ else = false } ]
|
||||
ip-strategy = "ipv4_then_ipv6"
|
||||
|
||||
[queue.outbound.tls]
|
||||
dane = "optional"
|
||||
mta-sts = "optional"
|
||||
starttls = "require"
|
||||
allow-invalid-certs = false
|
||||
|
||||
#[queue.outbound.source-ip]
|
||||
#v4 = "['10.0.0.10', '10.0.0.11']"
|
||||
#v6 = "['a::b', 'a::c']"
|
||||
|
||||
[queue.outbound.limits]
|
||||
mx = 7
|
||||
multihomed = 2
|
||||
|
||||
[queue.outbound.timeouts]
|
||||
connect = "3m"
|
||||
greeting = "3m"
|
||||
tls = "2m"
|
||||
ehlo = "3m"
|
||||
mail-from = "3m"
|
||||
rcpt-to = "3m"
|
||||
data = "10m"
|
||||
mta-sts = "2m"
|
||||
|
||||
[[queue.quota]]
|
||||
#match = "sender_domain = 'foobar.org'"
|
||||
#key = ["rcpt"]
|
||||
messages = 100000
|
||||
size = 10737418240 # 10gb
|
||||
|
||||
[[queue.throttle]]
|
||||
key = ["rcpt_domain"]
|
||||
#rate = "100/1h"
|
||||
concurrency = 5
|
|
@ -1,18 +0,0 @@
|
|||
#############################################
|
||||
# SMTP remote servers configuration
|
||||
#############################################
|
||||
|
||||
[remote."local"]
|
||||
address = "127.0.0.1"
|
||||
port = 11200
|
||||
protocol = "lmtp"
|
||||
concurrency = 10
|
||||
timeout = "1m"
|
||||
|
||||
[remote."local".tls]
|
||||
implicit = false
|
||||
allow-invalid-certs = true
|
||||
|
||||
#[remote."local".auth]
|
||||
#username = ""
|
||||
#secret = ""
|
|
@ -1,55 +0,0 @@
|
|||
#############################################
|
||||
# SMTP reporting configuration
|
||||
#############################################
|
||||
|
||||
[report]
|
||||
#submitter = "'%{HOST}%'"
|
||||
|
||||
[report.analysis]
|
||||
addresses = ["dmarc@*", "abuse@*", "postmaster@*"]
|
||||
forward = true
|
||||
store = "30d"
|
||||
|
||||
[report.dsn]
|
||||
from-name = "'Mail Delivery Subsystem'"
|
||||
from-address = "'MAILER-DAEMON@%{DEFAULT_DOMAIN}%'"
|
||||
sign = "['rsa']"
|
||||
|
||||
[report.dkim]
|
||||
from-name = "'Report Subsystem'"
|
||||
from-address = "'noreply-dkim@%{DEFAULT_DOMAIN}%'"
|
||||
subject = "'DKIM Authentication Failure Report'"
|
||||
sign = "['rsa']"
|
||||
send = "[1, 1d]"
|
||||
|
||||
[report.spf]
|
||||
from-name = "'Report Subsystem'"
|
||||
from-address = "'noreply-spf@%{DEFAULT_DOMAIN}%'"
|
||||
subject = "'SPF Authentication Failure Report'"
|
||||
send = "[1, 1d]"
|
||||
sign = "['rsa']"
|
||||
|
||||
[report.dmarc]
|
||||
from-name = "'Report Subsystem'"
|
||||
from-address = "'noreply-dmarc@%{DEFAULT_DOMAIN}%'"
|
||||
subject = "'DMARC Authentication Failure Report'"
|
||||
send = "[1, 1d]"
|
||||
sign = "['rsa']"
|
||||
|
||||
[report.dmarc.aggregate]
|
||||
from-name = "'DMARC Report'"
|
||||
from-address = "'noreply-dmarc@%{DEFAULT_DOMAIN}%'"
|
||||
org-name = "'%{DEFAULT_DOMAIN}%'"
|
||||
#contact-info = ""
|
||||
send = "daily"
|
||||
max-size = 26214400 # 25mb
|
||||
sign = "['rsa']"
|
||||
|
||||
[report.tls.aggregate]
|
||||
from-name = "'TLS Report'"
|
||||
from-address = "'noreply-tls@%{DEFAULT_DOMAIN}%'"
|
||||
org-name = "'%{DEFAULT_DOMAIN}%'"
|
||||
#contact-info = ""
|
||||
send = "daily"
|
||||
max-size = 26214400 # 25 mb
|
||||
sign = "['rsa']"
|
|
@ -1,13 +0,0 @@
|
|||
#############################################
|
||||
# SMTP server resolver configuration
|
||||
#############################################
|
||||
|
||||
[resolver]
|
||||
type = "system"
|
||||
#preserve-intermediates = true
|
||||
concurrency = 2
|
||||
timeout = "5s"
|
||||
attempts = 2
|
||||
try-tcp-on-error = true
|
||||
public-suffix = ["https://publicsuffix.org/list/public_suffix_list.dat",
|
||||
"file://%{BASE_PATH}%/etc/spamfilter/maps/suffix_list.dat.gz"]
|
|
@ -1,100 +0,0 @@
|
|||
#############################################
|
||||
# SMTP inbound session configuration
|
||||
#############################################
|
||||
|
||||
[session]
|
||||
timeout = "5m"
|
||||
transfer-limit = 262144000 # 250 MB
|
||||
duration = "10m"
|
||||
|
||||
[session.connect]
|
||||
#script = "'connect'"
|
||||
|
||||
[session.ehlo]
|
||||
require = true
|
||||
reject-non-fqdn = [ { if = "listener = 'smtp'", then = true},
|
||||
{ else = false } ]
|
||||
#script = "'ehlo'"
|
||||
|
||||
[session.extensions]
|
||||
pipelining = true
|
||||
chunking = true
|
||||
requiretls = true
|
||||
no-soliciting = ""
|
||||
dsn = [ { if = "!is_empty(authenticated_as)", then = true},
|
||||
{ else = false } ]
|
||||
expn = [ { if = "!is_empty(authenticated_as)", then = true},
|
||||
{ else = false } ]
|
||||
vrfy = [ { if = "!is_empty(authenticated_as)", then = true},
|
||||
{ else = false } ]
|
||||
future-release = [ { if = "!is_empty(authenticated_as)", then = "7d"},
|
||||
{ else = false } ]
|
||||
deliver-by = [ { if = "!is_empty(authenticated_as)", then = "15d"},
|
||||
{ else = false } ]
|
||||
mt-priority = [ { if = "!is_empty(authenticated_as)", then = "mixer"},
|
||||
{ else = false } ]
|
||||
|
||||
[session.auth]
|
||||
mechanisms = [ { if = "listener != 'smtp'", then = "[plain, login]"},
|
||||
{ else = false } ]
|
||||
directory = [ { if = "listener != 'smtp'", then = "'%{DEFAULT_DIRECTORY}%'" },
|
||||
{ else = false } ]
|
||||
require = [ { if = "listener != 'smtp'", then = true},
|
||||
{ else = false } ]
|
||||
allow-plain-text = false
|
||||
|
||||
[session.auth.errors]
|
||||
total = 3
|
||||
wait = "5s"
|
||||
|
||||
[session.mail]
|
||||
#script = "mail-from"
|
||||
#rewrite = [ { if = "listener != 'smtp' & matches('^([^.]+)@([^.]+)\\.(.+)$', rcpt)", then = "$1 + '@' + $3" },
|
||||
# { else = false } ]
|
||||
|
||||
[session.rcpt]
|
||||
#script = "greylist"
|
||||
relay = [ { if = "!is_empty(authenticated_as)", then = true },
|
||||
{ else = false } ]
|
||||
#rewrite = [ { if = "is_local_domain('%{DEFAULT_DIRECTORY}%', rcpt_domain) & matches('^([^.]+)\\.([^.]+)@(.+)$', rcpt)", then = "$1 + '+' + $2 + '@' + $3" },
|
||||
# { else = false } ]
|
||||
max-recipients = 25
|
||||
directory = "'%{DEFAULT_DIRECTORY}%'"
|
||||
|
||||
[session.rcpt.errors]
|
||||
total = 5
|
||||
wait = "5s"
|
||||
|
||||
[session.data]
|
||||
script = [ { if = "is_empty(authenticated_as)", then = "'spam-filter'"},
|
||||
{ else = "'track-replies'" } ]
|
||||
|
||||
[session.data.limits]
|
||||
messages = 10
|
||||
size = 104857600
|
||||
received-headers = 50
|
||||
|
||||
[session.data.add-headers]
|
||||
received = [ { if = "listener = 'smtp'", then = true },
|
||||
{ else = false } ]
|
||||
received-spf = [ { if = "listener = 'smtp'", then = true },
|
||||
{ else = false } ]
|
||||
auth-results = [ { if = "listener = 'smtp'", then = true },
|
||||
{ else = false } ]
|
||||
message-id = [ { if = "listener = 'smtp'", then = false },
|
||||
{ else = true } ]
|
||||
date = [ { if = "listener = 'smtp'", then = false },
|
||||
{ else = true } ]
|
||||
return-path = false
|
||||
|
||||
[[session.throttle]]
|
||||
#match = "remote_ip = '10.0.0.1'"
|
||||
key = ["remote_ip"]
|
||||
concurrency = 5
|
||||
#rate = "5/1h"
|
||||
enable = true
|
||||
|
||||
[[session.throttle]]
|
||||
key = ["sender_domain", "rcpt"]
|
||||
rate = "25/1h"
|
||||
enable = true
|
|
@ -1,18 +0,0 @@
|
|||
#############################################
|
||||
# SMTP DKIM & ARC signatures
|
||||
#############################################
|
||||
|
||||
[signature."rsa"]
|
||||
#public-key = "file://%{BASE_PATH}%/etc/dkim/%{DEFAULT_DOMAIN}%.cert"
|
||||
private-key = "file://%{BASE_PATH}%/etc/dkim/%{DEFAULT_DOMAIN}%.key"
|
||||
domain = "%{DEFAULT_DOMAIN}%"
|
||||
selector = "stalwart"
|
||||
headers = ["From", "To", "Date", "Subject", "Message-ID"]
|
||||
algorithm = "rsa-sha256"
|
||||
canonicalization = "relaxed/relaxed"
|
||||
#expire = "10d"
|
||||
#third-party = ""
|
||||
#third-party-algo = ""
|
||||
#auid = ""
|
||||
set-body-length = false
|
||||
report = true
|
|
@ -1,62 +0,0 @@
|
|||
#############################################
|
||||
# SMTP Spam & Phishing filter configuration
|
||||
#############################################
|
||||
|
||||
[spam.header]
|
||||
add-spam = true
|
||||
add-spam-result = true
|
||||
is-spam = "X-Spam-Status: Yes"
|
||||
|
||||
[spam.autolearn]
|
||||
enable = true
|
||||
balance = 0.9
|
||||
|
||||
[spam.autolearn.ham]
|
||||
replies = true
|
||||
threshold = -0.5
|
||||
|
||||
[spam.autolearn.spam]
|
||||
threshold = 6.0
|
||||
|
||||
[spam.threshold]
|
||||
spam = 5.0
|
||||
discard = 0
|
||||
reject = 0
|
||||
|
||||
[spam.data]
|
||||
directory = ""
|
||||
lookup = ""
|
||||
|
||||
[sieve.trusted.scripts]
|
||||
spam-filter = ["file://%{BASE_PATH}%/etc/spamfilter/scripts/config.sieve",
|
||||
"file://%{BASE_PATH}%/etc/spamfilter/scripts/prelude.sieve",
|
||||
"file://%{BASE_PATH}%/etc/spamfilter/scripts/from.sieve",
|
||||
"file://%{BASE_PATH}%/etc/spamfilter/scripts/recipient.sieve",
|
||||
"file://%{BASE_PATH}%/etc/spamfilter/scripts/subject.sieve",
|
||||
"file://%{BASE_PATH}%/etc/spamfilter/scripts/replyto.sieve",
|
||||
"file://%{BASE_PATH}%/etc/spamfilter/scripts/date.sieve",
|
||||
"file://%{BASE_PATH}%/etc/spamfilter/scripts/messageid.sieve",
|
||||
"file://%{BASE_PATH}%/etc/spamfilter/scripts/received.sieve",
|
||||
"file://%{BASE_PATH}%/etc/spamfilter/scripts/headers.sieve",
|
||||
"file://%{BASE_PATH}%/etc/spamfilter/scripts/bounce.sieve",
|
||||
"file://%{BASE_PATH}%/etc/spamfilter/scripts/html.sieve",
|
||||
"file://%{BASE_PATH}%/etc/spamfilter/scripts/mime.sieve",
|
||||
"file://%{BASE_PATH}%/etc/spamfilter/scripts/dmarc.sieve",
|
||||
"file://%{BASE_PATH}%/etc/spamfilter/scripts/ip.sieve",
|
||||
"file://%{BASE_PATH}%/etc/spamfilter/scripts/helo.sieve",
|
||||
"file://%{BASE_PATH}%/etc/spamfilter/scripts/replies_in.sieve",
|
||||
"file://%{BASE_PATH}%/etc/spamfilter/scripts/spamtrap.sieve",
|
||||
"file://%{BASE_PATH}%/etc/spamfilter/scripts/bayes_classify.sieve",
|
||||
"file://%{BASE_PATH}%/etc/spamfilter/scripts/url.sieve",
|
||||
"file://%{BASE_PATH}%/etc/spamfilter/scripts/rbl.sieve",
|
||||
"file://%{BASE_PATH}%/etc/spamfilter/scripts/pyzor.sieve",
|
||||
"file://%{BASE_PATH}%/etc/spamfilter/scripts/composites.sieve",
|
||||
"file://%{BASE_PATH}%/etc/spamfilter/scripts/scores.sieve",
|
||||
"file://%{BASE_PATH}%/etc/spamfilter/scripts/reputation.sieve",
|
||||
"file://%{BASE_PATH}%/etc/spamfilter/scripts/epilogue.sieve"]
|
||||
|
||||
track-replies = ["file://%{BASE_PATH}%/etc/spamfilter/scripts/config.sieve",
|
||||
"file://%{BASE_PATH}%/etc/spamfilter/scripts/replies_out.sieve"]
|
||||
|
||||
greylist = ["file://%{BASE_PATH}%/etc/spamfilter/scripts/config.sieve",
|
||||
"file://%{BASE_PATH}%/etc/spamfilter/scripts/greylist.sieve"]
|
10331
resources/config/spamfilter.toml
Normal file
10331
resources/config/spamfilter.toml
Normal file
File diff suppressed because it is too large
Load diff
|
@ -26,7 +26,6 @@ spam-dmarc = {"18f.gov",
|
|||
"adp.com",
|
||||
"advice.hmrc.gov.uk",
|
||||
"aerocivil.gov.co",
|
||||
"aerocivil.gov.co",
|
||||
"afreximbank.com",
|
||||
"agingstats.gov",
|
||||
"agro.ru",
|
||||
|
|
|
@ -150,7 +150,6 @@ spam-spdk = {"1cfresh.com",
|
|||
"paypal.ca",
|
||||
"paypal.cn",
|
||||
"paypal.com",
|
||||
"paypal.com",
|
||||
"paypal.co.uk",
|
||||
"paypal.de",
|
||||
"paypal.es",
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
spam-trap = {}
|
Binary file not shown.
|
@ -1,35 +1,35 @@
|
|||
# Whether to add an X-Spam-Status header
|
||||
let "ADD_HEADER_SPAM" "%{cfg:spam.header.add-spam}%";
|
||||
let "ADD_HEADER_SPAM" "key_get('spam-config', 'add-spam')";
|
||||
|
||||
# Whether to add an X-Spam-Result header
|
||||
let "ADD_HEADER_SPAM_RESULT" "%{cfg:spam.header.add-spam-result}%";
|
||||
let "ADD_HEADER_SPAM_RESULT" "key_get('spam-config', 'add-spam-result')";
|
||||
|
||||
# Whether message replies from authenticated users should be learned as ham
|
||||
let "AUTOLEARN_REPLIES_HAM" "%{cfg:spam.autolearn.ham.replies}%";
|
||||
let "AUTOLEARN_REPLIES_HAM" "key_get('spam-config', 'learn-ham-replies')";
|
||||
|
||||
# Whether the bayes classifier should be trained automatically
|
||||
let "AUTOLEARN_ENABLE" "%{cfg:spam.autolearn.enable}%";
|
||||
let "AUTOLEARN_ENABLE" "key_get('spam-config', 'learn-enable')";
|
||||
|
||||
# When to learn ham (score >= threshold)
|
||||
let "AUTOLEARN_HAM_THRESHOLD" "%{cfg:spam.autolearn.ham.threshold}%";
|
||||
let "AUTOLEARN_HAM_THRESHOLD" "key_get('spam-config', 'learn-ham-threshold')";
|
||||
|
||||
# When to learn spam (score <= threshold)
|
||||
let "AUTOLEARN_SPAM_THRESHOLD" "%{cfg:spam.autolearn.spam.threshold}%";
|
||||
let "AUTOLEARN_SPAM_THRESHOLD" "key_get('spam-config', 'learn-spam-threshold')";
|
||||
|
||||
# Keep difference for spam/ham learns for at least this value
|
||||
let "AUTOLEARN_SPAM_HAM_BALANCE" "%{cfg:spam.autolearn.balance}%";
|
||||
let "AUTOLEARN_SPAM_HAM_BALANCE" "key_get('spam-config', 'learn-balance')";
|
||||
|
||||
# If ADD_HEADER_SPAM is enabled, mark as SPAM messages with a score above this threshold
|
||||
let "SCORE_SPAM_THRESHOLD" "%{cfg:spam.threshold.spam}%";
|
||||
let "SCORE_SPAM_THRESHOLD" "key_get('spam-config', 'threshold-spam')";
|
||||
|
||||
# Discard messages with a score above this threshold
|
||||
let "SCORE_DISCARD_THRESHOLD" "%{cfg:spam.threshold.discard}%";
|
||||
let "SCORE_DISCARD_THRESHOLD" "key_get('spam-config', 'threshold-discard')";
|
||||
|
||||
# Reject messages with a score above this threshold
|
||||
let "SCORE_REJECT_THRESHOLD" "%{cfg:spam.threshold.reject}%";
|
||||
let "SCORE_REJECT_THRESHOLD" "key_get('spam-config', 'threshold-reject')";
|
||||
|
||||
# Directory name to use for local domain lookups (leave empty for default)
|
||||
let "DOMAIN_DIRECTORY" "%{cfg:spam.data.directory}%";
|
||||
let "DOMAIN_DIRECTORY" "key_get('spam-config', 'directory')";
|
||||
|
||||
# Store to use for Bayes tokens and ids (leave empty for default)
|
||||
let "SPAM_DB" "%{cfg:spam.data.lookup}%";
|
||||
let "SPAM_DB" "key_get('spam-config', 'lookup')";
|
||||
|
|
20
resources/config/spamfilter/settings.toml
Normal file
20
resources/config/spamfilter/settings.toml
Normal file
|
@ -0,0 +1,20 @@
|
|||
[spam.header]
|
||||
is-spam = "X-Spam-Status: Yes"
|
||||
|
||||
[lookup.spam-config]
|
||||
add-spam = true
|
||||
add-spam-result = true
|
||||
learn-enable = true
|
||||
learn-balance = "0.9"
|
||||
learn-ham-replies = true
|
||||
learn-ham-threshold = "-0.5"
|
||||
learn-spam-threshold = "6.0"
|
||||
threshold-spam = "5.0"
|
||||
threshold-discard = "0.0"
|
||||
threshold-reject = "0.0"
|
||||
directory = ""
|
||||
lookup = ""
|
||||
|
||||
[session.data]
|
||||
script = [ { if = "is_empty(authenticated_as)", then = "'spam-filter'"},
|
||||
{ else = "'track-replies'" } ]
|
|
@ -1,18 +0,0 @@
|
|||
#############################################
|
||||
# ElasticSearch FTS Store configuration
|
||||
#############################################
|
||||
|
||||
[store."elasticsearch"]
|
||||
type = "elasticsearch"
|
||||
url = "https://localhost:9200"
|
||||
user = "elastic"
|
||||
password = "myelasticpassword"
|
||||
#cloud-id = "my-cloud-id"
|
||||
disable = true
|
||||
|
||||
[store."elasticsearch".tls]
|
||||
allow-invalid-certs = true
|
||||
|
||||
[store."elasticsearch".index]
|
||||
shards = 3
|
||||
replicas = 0
|
|
@ -1,20 +0,0 @@
|
|||
#############################################
|
||||
# FoundationDB Store configuration
|
||||
#############################################
|
||||
|
||||
[store."foundationdb"]
|
||||
type = "foundationdb"
|
||||
#cluster-file = "/etc/foundationdb/fdb.cluster"
|
||||
disable = true
|
||||
|
||||
#[store."foundationdb".transaction]
|
||||
#timeout = "5s"
|
||||
#retry-limit = 10
|
||||
#max-retry-delay = "1s"
|
||||
|
||||
#[store."foundationdb".ids]
|
||||
#machine = "stalwart"
|
||||
#data-center = "my-datacenter"
|
||||
|
||||
[store."foundationdb".purge]
|
||||
frequency = "0 3 *"
|
|
@ -1,12 +0,0 @@
|
|||
#############################################
|
||||
# File System Blob Store configuration
|
||||
#############################################
|
||||
|
||||
[store."fs"]
|
||||
type = "fs"
|
||||
path = "%{BASE_PATH}%/data/blobs"
|
||||
depth = 2
|
||||
disable = true
|
||||
|
||||
[store."fs".purge]
|
||||
frequency = "0 3 *"
|
|
@ -1,37 +0,0 @@
|
|||
#############################################
|
||||
# MySQL Store configuration
|
||||
#############################################
|
||||
|
||||
[store."mysql"]
|
||||
type = "mysql"
|
||||
host = "localhost"
|
||||
port = 3307
|
||||
database = "stalwart"
|
||||
user = "root"
|
||||
password = "password"
|
||||
disable = true
|
||||
#max-allowed-packet = 1073741824
|
||||
timeout = "15s"
|
||||
|
||||
#[store."mysql".pool]
|
||||
#max-connections = 10
|
||||
#min-connections = 5
|
||||
|
||||
#[store."mysql".init]
|
||||
#execute = [
|
||||
# "CREATE TABLE IF NOT EXISTS accounts (name VARCHAR(32) PRIMARY KEY, secret VARCHAR(1024), description VARCHAR(1024), type VARCHAR(32) NOT NULL, quota INTEGER DEFAULT 0, active BOOLEAN DEFAULT 1)",
|
||||
# "CREATE TABLE IF NOT EXISTS group_members (name VARCHAR(32) NOT NULL, member_of VARCHAR(32) NOT NULL, PRIMARY KEY (name, member_of))",
|
||||
# "CREATE TABLE IF NOT EXISTS emails (name VARCHAR(32) NOT NULL, address VARCHAR(128) NOT NULL, type VARCHAR(32), PRIMARY KEY (name, address))"
|
||||
#]
|
||||
|
||||
[store."mysql".query]
|
||||
name = "SELECT name, type, secret, description, quota FROM accounts WHERE name = ? AND active = true"
|
||||
members = "SELECT member_of FROM group_members WHERE name = ?"
|
||||
recipients = "SELECT name FROM emails WHERE address = ? ORDER BY name ASC"
|
||||
emails = "SELECT address FROM emails WHERE name = ? AND type != 'list' ORDER BY type DESC, address ASC"
|
||||
verify = "SELECT address FROM emails WHERE address LIKE CONCAT('%', ?, '%') AND type = 'primary' ORDER BY address LIMIT 5"
|
||||
expand = "SELECT p.address FROM emails AS p JOIN emails AS l ON p.name = l.name WHERE p.type = 'primary' AND l.address = ? AND l.type = 'list' ORDER BY p.address LIMIT 50"
|
||||
domains = "SELECT 1 FROM emails WHERE address LIKE CONCAT('%@', ?) LIMIT 1"
|
||||
|
||||
[store."mysql".purge]
|
||||
frequency = "0 3 *"
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue