From 35562bb9fdf5fc42235e1bc075258315224d6811 Mon Sep 17 00:00:00 2001 From: mdecimus Date: Sat, 30 Mar 2024 18:12:40 +0100 Subject: [PATCH] Use safe defaults when settings are missing --- crates/common/src/addresses.rs | 10 +- crates/common/src/config/imap.rs | 8 +- crates/common/src/config/jmap/settings.rs | 12 +- crates/common/src/config/mod.rs | 17 +- crates/common/src/config/network.rs | 21 +- crates/common/src/config/scripts.rs | 107 +- crates/common/src/config/server/listener.rs | 50 +- crates/common/src/config/server/mod.rs | 20 +- crates/common/src/config/server/tls.rs | 6 + crates/common/src/config/smtp/auth.rs | 95 +- crates/common/src/config/smtp/mod.rs | 113 +- crates/common/src/config/smtp/queue.rs | 157 +- crates/common/src/config/smtp/report.rs | 165 +- crates/common/src/config/smtp/resolver.rs | 5 +- crates/common/src/config/smtp/session.rs | 254 +- crates/common/src/config/smtp/throttle.rs | 10 +- crates/common/src/config/tracers.rs | 3 + crates/common/src/expr/if_block.rs | 50 +- crates/common/src/expr/mod.rs | 64 +- crates/common/src/expr/tokenizer.rs | 34 +- crates/common/src/lib.rs | 5 +- crates/common/src/listener/acme/mod.rs | 4 + crates/common/src/listener/acme/resolver.rs | 6 + crates/common/src/listener/blocked.rs | 2 +- crates/common/src/listener/listen.rs | 14 +- crates/common/src/listener/mod.rs | 36 +- crates/common/src/scripts/plugins/bayes.rs | 1 + crates/directory/src/backend/smtp/config.rs | 2 +- crates/jmap/src/api/http.rs | 20 +- crates/smtp/src/core/mod.rs | 7 +- crates/smtp/src/core/params.rs | 11 +- crates/smtp/src/core/throttle.rs | 7 +- crates/smtp/src/inbound/auth.rs | 5 +- crates/smtp/src/inbound/ehlo.rs | 7 +- crates/smtp/src/inbound/session.rs | 25 +- crates/smtp/src/inbound/spawn.rs | 17 +- crates/smtp/src/inbound/vrfy.rs | 4 +- crates/smtp/src/outbound/delivery.rs | 10 +- crates/smtp/src/outbound/lookup.rs | 2 +- crates/smtp/src/queue/mod.rs | 16 +- crates/smtp/src/reporting/dkim.rs | 4 +- crates/smtp/src/reporting/dmarc.rs | 5 +- crates/smtp/src/reporting/spf.rs | 4 +- crates/smtp/src/scripts/event_loop.rs | 12 +- crates/smtp/src/scripts/exec.rs | 1 + crates/smtp/src/scripts/mod.rs | 26 +- crates/store/src/backend/foundationdb/main.rs | 10 +- crates/store/src/backend/memory/mod.rs | 8 +- crates/store/src/backend/mysql/main.rs | 13 +- crates/store/src/backend/postgres/main.rs | 6 +- crates/store/src/backend/redis/mod.rs | 173 +- crates/utils/src/config/parser.rs | 16 +- crates/utils/src/config/utils.rs | 40 +- crates/utils/src/suffixlist.rs | 20 +- resources/config/build.py | 94 + resources/config/common/cache.toml | 35 - resources/config/common/server.toml | 37 - resources/config/common/sieve.toml | 73 - resources/config/common/store.toml | 20 - resources/config/common/tls.toml | 30 - resources/config/common/tracing.toml | 24 - resources/config/config.toml | 52 - resources/config/directory/imap.toml | 29 - resources/config/directory/internal.toml | 20 - resources/config/directory/ldap.toml | 60 - resources/config/directory/lmtp.toml | 33 - resources/config/directory/memory.toml | 59 - resources/config/directory/sql.toml | 26 - resources/config/imap/listener.toml | 16 - resources/config/imap/settings.toml | 22 - resources/config/jmap/auth.toml | 6 - resources/config/jmap/listener.toml | 14 - resources/config/jmap/oauth.toml | 16 - resources/config/jmap/protocol.toml | 43 - resources/config/jmap/push.toml | 21 - resources/config/jmap/ratelimit.toml | 9 - resources/config/jmap/websockets.toml | 8 - resources/config/minimal.toml | 72 + resources/config/security.toml | 19 + resources/config/smtp/auth.toml | 28 - resources/config/smtp/listener.toml | 21 - resources/config/smtp/milter.toml | 26 - resources/config/smtp/queue.toml | 49 - resources/config/smtp/remote.toml | 18 - resources/config/smtp/report.toml | 55 - resources/config/smtp/resolver.toml | 13 - resources/config/smtp/session.toml | 100 - resources/config/smtp/signature.toml | 18 - resources/config/smtp/spamfilter.toml | 62 - resources/config/spamfilter.toml | 10331 ++++++++++++++++ .../config/spamfilter/maps/allow_dmarc.list | 1 - .../spamfilter/maps/allow_spf_dkim.list | 1 - .../config/spamfilter/maps/spam_trap.list | 1 - .../config/spamfilter/maps/suffix_list.dat.gz | Bin 80969 -> 0 bytes .../config/spamfilter/scripts/config.sieve | 24 +- resources/config/spamfilter/settings.toml | 20 + resources/config/store/elasticsearch.toml | 18 - resources/config/store/foundationdb.toml | 20 - resources/config/store/fs.toml | 12 - resources/config/store/mysql.toml | 37 - resources/config/store/postgresql.toml | 39 - resources/config/store/redis.toml | 18 - resources/config/store/rocksdb.toml | 18 - resources/config/store/s3.toml | 18 - resources/config/store/sqlite.toml | 31 - tests/resources/scripts/create_test_env.sh | 64 +- tests/src/lib.rs | 4 +- tests/src/smtp/config.rs | 26 +- tests/src/smtp/inbound/antispam.rs | 42 +- tests/src/smtp/inbound/auth.rs | 2 +- tests/src/smtp/inbound/data.rs | 30 +- tests/src/smtp/inbound/milter.rs | 4 +- tests/src/smtp/inbound/scripts.rs | 23 +- tests/src/smtp/lookup/sql.rs | 8 +- tests/src/smtp/outbound/dane.rs | 14 +- tests/src/smtp/outbound/extensions.rs | 8 + tests/src/smtp/outbound/mod.rs | 6 +- tests/src/smtp/outbound/mta_sts.rs | 9 + tests/src/smtp/outbound/tls.rs | 3 +- tests/src/smtp/session.rs | 1 + 120 files changed, 11732 insertions(+), 2069 deletions(-) create mode 100644 resources/config/build.py delete mode 100644 resources/config/common/cache.toml delete mode 100644 resources/config/common/server.toml delete mode 100644 resources/config/common/sieve.toml delete mode 100644 resources/config/common/store.toml delete mode 100644 resources/config/common/tls.toml delete mode 100644 resources/config/common/tracing.toml delete mode 100644 resources/config/config.toml delete mode 100644 resources/config/directory/imap.toml delete mode 100644 resources/config/directory/internal.toml delete mode 100644 resources/config/directory/ldap.toml delete mode 100644 resources/config/directory/lmtp.toml delete mode 100644 resources/config/directory/memory.toml delete mode 100644 resources/config/directory/sql.toml delete mode 100644 resources/config/imap/listener.toml delete mode 100644 resources/config/imap/settings.toml delete mode 100644 resources/config/jmap/auth.toml delete mode 100644 resources/config/jmap/listener.toml delete mode 100644 resources/config/jmap/oauth.toml delete mode 100644 resources/config/jmap/protocol.toml delete mode 100644 resources/config/jmap/push.toml delete mode 100644 resources/config/jmap/ratelimit.toml delete mode 100644 resources/config/jmap/websockets.toml create mode 100644 resources/config/minimal.toml create mode 100644 resources/config/security.toml delete mode 100644 resources/config/smtp/auth.toml delete mode 100644 resources/config/smtp/listener.toml delete mode 100644 resources/config/smtp/milter.toml delete mode 100644 resources/config/smtp/queue.toml delete mode 100644 resources/config/smtp/remote.toml delete mode 100644 resources/config/smtp/report.toml delete mode 100644 resources/config/smtp/resolver.toml delete mode 100644 resources/config/smtp/session.toml delete mode 100644 resources/config/smtp/signature.toml delete mode 100644 resources/config/smtp/spamfilter.toml create mode 100644 resources/config/spamfilter.toml delete mode 100644 resources/config/spamfilter/maps/spam_trap.list delete mode 100644 resources/config/spamfilter/maps/suffix_list.dat.gz create mode 100644 resources/config/spamfilter/settings.toml delete mode 100644 resources/config/store/elasticsearch.toml delete mode 100644 resources/config/store/foundationdb.toml delete mode 100644 resources/config/store/fs.toml delete mode 100644 resources/config/store/mysql.toml delete mode 100644 resources/config/store/postgresql.toml delete mode 100644 resources/config/store/redis.toml delete mode 100644 resources/config/store/rocksdb.toml delete mode 100644 resources/config/store/s3.toml delete mode 100644 resources/config/store/sqlite.toml diff --git a/crates/common/src/addresses.rs b/crates/common/src/addresses.rs index 1f54c864..3e17d5b3 100644 --- a/crates/common/src/addresses.rs +++ b/crates/common/src/addresses.rs @@ -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 } } } diff --git a/crates/common/src/config/imap.rs b/crates/common/src/config/imap.rs index a94307cc..8ccb84aa 100644 --- a/crates/common/src/config/imap.rs +++ b/crates/common/src/config/imap.rs @@ -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::>("imap.rate-limit.requests", "2000/1m") + .unwrap_or_default(), + rate_concurrent: config + .property::>("imap.rate-limit.concurrent") + .unwrap_or_default(), allow_plain_auth: config .property_or_default("imap.auth.allow-plain-text", "false") .unwrap_or(false), diff --git a/crates/common/src/config/jmap/settings.rs b/crates/common/src/config/jmap/settings.rs index 4f0c9338..ec8383ea 100644 --- a/crates/common/src/config/jmap/settings.rs +++ b/crates/common/src/config/jmap/settings.rs @@ -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::>("jmap.rate-limit.account", "1000/1m") + .unwrap_or_default(), + rate_authenticate_req: config + .property_or_default::>("authentication.rate-limit", "10/1m") + .unwrap_or_default(), + rate_anonymous: config + .property_or_default::>("jmap.rate-limit.anonymous", "100/1m") + .unwrap_or_default(), oauth_key: config .value("oauth.key") .map(|s| s.to_string()) diff --git a/crates/common/src/config/mod.rs b/crates/common/src/config/mod.rs index c1920207..352b6627 100644 --- a/crates/common/src/config/mod.rs +++ b/crates/common/src/config/mod.rs @@ -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) diff --git a/crates/common/src/config/network.rs b/crates/common/src/config/network.rs index f26b4e6d..70918e1f 100644 --- a/crates/common/src/config/network.rs +++ b/crates/common/src/config/network.rs @@ -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; } diff --git a/crates/common/src/config/scripts.rs b/crates/common/src/config/scripts.rs index f993d364..bf9fd5a0 100644 --- a/crates/common/src/config/scripts.rs +++ b/crates/common/src/config/scripts.rs @@ -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, + pub from_addr: IfBlock, + pub from_name: IfBlock, + pub return_path: IfBlock, + pub sign: IfBlock, pub scripts: AHashMap>, pub bayes_cache: BayesTokenCache, pub remote_lists: RwLock>, @@ -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::("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::>() { - // Skip sub-scripts - if config - .property(("sieve.trusted.scripts", id.as_str(), "snippet")) - .unwrap_or(false) - { - continue; - } - - let script = config - .value(("sieve.trusted.scripts", id.as_str(), "contents")) - .unwrap(); - - match trusted_compiler.compile(script.as_bytes()) { + match trusted_compiler.compile( + config + .value(("sieve.trusted.scripts", id.as_str(), "contents")) + .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, diff --git a/crates/common/src/config/server/listener.rs b/crates/common/src/config/server/listener.rs index 1eb755d8..cb4e6b5e 100644 --- a/crates/common/src/config/server/listener.rs +++ b/crates/common/src/config/server/listener.rs @@ -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( - ("server.listener", id, "socket.backlog"), - "server.socket.backlog", - ), - linger: config.property_or_else( - ("server.listener", id, "socket.linger"), - "server.socket.linger", - ), + .property_or_else::>( + ("server.listener", id, "socket.ttl"), + "server.socket.ttl", + "false", + ) + .unwrap_or_default(), + backlog: config + .property_or_else::>( + ("server.listener", id, "socket.backlog"), + "server.socket.backlog", + "1024", + ) + .unwrap_or_default(), + linger: config + .property_or_else::>( + ("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 diff --git a/crates/common/src/config/server/mod.rs b/crates/common/src/config/server/mod.rs index 3f8e9f1d..4950cb8e 100644 --- a/crates/common/src/config/server/mod.rs +++ b/crates/common/src/config/server/mod.rs @@ -46,14 +46,20 @@ pub enum ServerProtocol { ManageSieve, } -impl Display for ServerProtocol { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl ServerProtocol { + pub fn as_str(&self) -> &'static str { 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"), + 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 { + f.write_str(self.as_str()) + } +} diff --git a/crates/common/src/config/server/tls.rs b/crates/common/src/config/server/tls.rs index 9648fb71..429b439d 100644 --- a/crates/common/src/config/server/tls.rs +++ b/crates/common/src/config/server/tls.rs @@ -105,6 +105,11 @@ impl TlsManager { .map(|(_, v)| v.to_string()) .collect::>(); + // This ACME manager is the default when SNI is not available + let default = config + .property::(("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); diff --git a/crates/common/src/config/smtp/auth.rs b/crates/common/src/config/smtp/auth.rs index dd842a01..5eabd90f 100644 --- a/crates/common/src/config/smtp/auth.rs +++ b/crates/common/src/config/smtp/auth.rs @@ -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::("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::("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::( + "auth.spf.verify.ehlo", + [("local_port == 25", "relaxed")], + #[cfg(not(feature = "test_mode"))] + "disable", + #[cfg(feature = "test_mode")] + "relaxed", + + ), + verify_mail_from: IfBlock::new::( + "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::( + "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::( + "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::(); let conn_vars = TokenMap::default() - .with_smtp_variables(&[V_LISTENER, V_REMOTE_IP, V_LOCAL_IP]) + .with_variables(CONNECTION_VARS) .with_constants::(); 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; diff --git a/crates/common/src/config/smtp/mod.rs b/crates/common/src/config/smtp/mod.rs index b173d717..5aca0155 100644 --- a/crates/common/src/config/smtp/mod.rs +++ b/crates/common/src/config/smtp/mod.rs @@ -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::try_parse( - config, - "server.hostname", - &TokenMap::default().with_smtp_variables(&[V_LISTENER, V_REMOTE_IP, V_LOCAL_IP]), - ) -} diff --git a/crates/common/src/config/smtp/queue.rs b/crates/common/src/config/smtp/queue.rs index e93c5c66..011511df 100644 --- a/crates/common/src/config/smtp/queue.rs +++ b/crates/common/src/config/smtp/queue.rs @@ -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::( + "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::("queue.outbound.tls.dane", [], "optional"), + mta_sts: IfBlock::new::( + "queue.outbound.tls.mta-sts", + [], + "optional", + ), + start: IfBlock::new::( + "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::(); let dane_vars = mx_vars.clone().with_constants::(); let mta_sts_vars = rcpt_vars.clone().with_constants::(); - // 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((prefix.as_str(), "size")) - .filter(|&v| v > 0), + .property::>((prefix.as_str(), "size")) + .filter(|&v| v.as_ref().map_or(false, |v| *v > 0)) + .unwrap_or_default(), messages: config - .property::((prefix.as_str(), "messages")) - .filter(|&v| v > 0), + .property::>((prefix.as_str(), "messages")) + .filter(|&v| v.as_ref().map_or(false, |v| *v > 0)) + .unwrap_or_default(), }; // Validate diff --git a/crates/common/src/config/smtp/report.rs b/crates/common/src/config/smtp/report.rs index f95c2516..6c0e4aa6 100644 --- a/crates/common/src/config/smtp/report.rs +++ b/crates/common/src/config/smtp/report.rs @@ -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::>("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::(), + &rcpt_vars.with_constants::(), ), tls: AggregateReport::parse( config, "tls", - &default_hostname, - &rcpt_vars.with_constants::(), + &TokenMap::default() + .with_variables(SMTP_QUEUE_HOST_VARS) + .with_constants::(), ), } } } 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::( + 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()) } } diff --git a/crates/common/src/config/smtp/resolver.rs b/crates/common/src/config/smtp/resolver.rs index 2638c9c4..d314c848 100644 --- a/crates/common/src/config/smtp/resolver.rs +++ b/crates/common/src/config/smtp/resolver.rs @@ -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()), diff --git a/crates/common/src/config/smtp/session.rs b/crates/common/src/config/smtp/session.rs index 4cf4f152..2be34db5 100644 --- a/crates/common/src/config/smtp/session.rs +++ b/crates/common/src/config/smtp/session.rs @@ -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::(); let mechanisms_vars = has_ehlo_hars.clone().with_constants::(); @@ -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(format!("session.data.pipe.{id}.timeout"), [], "30s") + }), }) } @@ -511,7 +482,9 @@ fn parse_milter(config: &mut Config, id: &str, token_map: &TokenMap) -> Option(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 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::( + "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::( + "session.extensions.mt-priority", + [("!is_empty(authenticated_as)", "mixer")], + "false", + ), }, } } diff --git a/crates/common/src/config/smtp/throttle.rs b/crates/common/src/config/smtp/throttle.rs index 0c48ad48..b8bfd835 100644 --- a/crates/common/src/config/smtp/throttle.rs +++ b/crates/common/src/config/smtp/throttle.rs @@ -96,11 +96,13 @@ fn parse_throttle_item( .unwrap_or_default(), keys, concurrency: config - .property::((prefix.as_str(), "concurrency")) - .filter(|&v| v > 0), + .property::>((prefix.as_str(), "concurrency")) + .filter(|&v| v.as_ref().map_or(false, |v| *v > 0)) + .unwrap_or_default(), rate: config - .property::((prefix.as_str(), "rate")) - .filter(|v| v.requests > 0), + .property::>((prefix.as_str(), "rate")) + .filter(|v| v.as_ref().map_or(false, |r| r.requests > 0)) + .unwrap_or_default(), }; // Validate diff --git a/crates/common/src/config/tracers.rs b/crates/common/src/config/tracers.rs index 019f2009..05787bbe 100644 --- a/crates/common/src/config/tracers.rs +++ b/crates/common/src/config/tracers.rs @@ -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, } diff --git a/crates/common/src/expr/if_block.rs b/crates/common/src/expr/if_block.rs index ea98a401..c1196fa2 100644 --- a/crates/common/src/expr/if_block.rs +++ b/crates/common/src/expr/if_block.rs @@ -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>(value: T) -> Self { + pub fn new( + key: impl Into, + if_thens: impl IntoIterator, + default: impl AsRef, + ) -> Self { + let token_map = TokenMap::default() + .with_all_variables() + .with_constants::(); + 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) -> 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( diff --git a/crates/common/src/expr/mod.rs b/crates/common/src/expr/mod.rs index 0f16d8b1..4b8ea2fb 100644 --- a/crates/common/src/expr/mod.rs +++ b/crates/common/src/expr/mod.rs @@ -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 for Variable<'_> { } } +impl From for Variable<'_> { + fn from(value: u16) -> Self { + Variable::Integer(value as i64) + } +} + impl From 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> + Into + 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> for () { + type Error = (); + + fn try_from(_: Variable<'x>) -> Result { + Ok(()) + } +} + impl ConstantValue for Duration { fn add_constants(_: &mut TokenMap) {} } diff --git a/crates/common/src/expr/tokenizer.rs b/crates/common/src/expr/tokenizer.rs index d60f4c89..9dfcac3f 100644 --- a/crates/common/src/expr/tokenizer.rs +++ b/crates/common/src/expr/tokenizer.rs @@ -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(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(mut self, vars: I) -> Self where I: IntoIterator, { diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs index aed93bfa..83c155e7 100644 --- a/crates/common/src/lib.rs +++ b/crates/common/src/lib.rs @@ -42,7 +42,7 @@ pub static DAEMON_NAME: &str = concat!("Stalwart Mail Server v", env!("CARGO_PKG pub type SharedCore = Arc>; -#[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; } } diff --git a/crates/common/src/listener/acme/mod.rs b/crates/common/src/listener/acme/mod.rs index 321f3554..73f463d2 100644 --- a/crates/common/src/listener/acme/mod.rs +++ b/crates/common/src/listener/acme/mod.rs @@ -49,6 +49,7 @@ pub struct AcmeProvider { pub contact: Vec, renew_before: chrono::Duration, account_key: ArcSwap>, + default: bool, } pub struct AcmeResolver { @@ -73,6 +74,7 @@ impl AcmeProvider { domains: Vec, contact: Vec, renew_before: Duration, + default: bool, ) -> utils::config::Result { 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, } } } diff --git a/crates/common/src/listener/acme/resolver.rs b/crates/common/src/listener/acme/resolver.rs index 793bea9b..b5843a5f 100644 --- a/crates/common/src/listener/acme/resolver.rs +++ b/crates/common/src/listener/acme/resolver.rs @@ -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 diff --git a/crates/common/src/listener/blocked.rs b/crates/common/src/listener/blocked.rs index 82e649f1..9da78d79 100644 --- a/crates/common/src/listener/blocked.rs +++ b/crates/common/src/listener/blocked.rs @@ -70,7 +70,7 @@ impl BlockedIps { ip_addresses: RwLock::new(ip_addresses), has_networks: !ip_networks.is_empty(), ip_networks, - limiter_rate: config.property::("authentication.fail2ban"), + limiter_rate: config.property_or_default::("authentication.fail2ban", "100/1d"), } } } diff --git a/crates/common/src/listener/listen.rs b/crates/common/src/listener/listen.rs index ba810aeb..9f1426ad 100644 --- a/crates/common/src/listener/listen.rs +++ b/crates/common/src/listener/listen.rs @@ -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( &self, stream: T, - local_ip: IpAddr, + local_addr: SocketAddr, remote_addr: SocketAddr, core: &Core, ) -> Option>; @@ -193,7 +193,7 @@ impl BuildSession for Arc { fn build_session( &self, stream: T, - local_ip: IpAddr, + local_addr: SocketAddr, remote_addr: SocketAddr, core: &Core, ) -> Option> { @@ -231,9 +231,11 @@ impl BuildSession for Arc { 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() diff --git a/crates/common/src/listener/mod.rs b/crates/common/src/listener/mod.rs index d2c74ff2..c0af39ec 100644 --- a/crates/common/src/listener/mod.rs +++ b/crates/common/src/listener/mod.rs @@ -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 { 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, @@ -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 + 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 ResolveVariable for SessionData { 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 ResolveVariable for SessionData { 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"), } } diff --git a/crates/common/src/scripts/plugins/bayes.rs b/crates/common/src/scripts/plugins/bayes.rs index 3173a428..c4159763 100644 --- a/crates/common/src/scripts/plugins/bayes.rs +++ b/crates/common/src/scripts/plugins/bayes.rs @@ -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(); diff --git a/crates/directory/src/backend/smtp/config.rs b/crates/directory/src/backend/smtp/config.rs index 5ef8cd45..48072628 100644 --- a/crates/directory/src/backend/smtp/config.rs +++ b/crates/directory/src/backend/smtp/config.rs @@ -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, diff --git a/crates/jmap/src/api/http.rs b/crates/jmap/src/api/http.rs index c53aed0a..0b341fe1 100644 --- a/crates/jmap/src/api/http.rs +++ b/crates/jmap/src/api/http.rs @@ -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, 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 + ) + }) } } diff --git a/crates/smtp/src/core/mod.rs b/crates/smtp/src/core/mod.rs index 5821452d..89f80d3f 100644 --- a/crates/smtp/src/core/mod.rs +++ b/crates/smtp/src/core/mod.rs @@ -149,6 +149,7 @@ pub struct Session { 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 { 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, diff --git a/crates/smtp/src/core/params.rs b/crates/smtp/src/core/params.rs index 063e7bba..fff1ed76 100644 --- a/crates/smtp/src/core/params.rs +++ b/crates/smtp/src/core/params.rs @@ -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 Session { +impl Session { 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 Session { .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 diff --git a/crates/smtp/src/core/throttle.rs b/crates/smtp/src/core/throttle.rs index 79ae1c88..6da347c2 100644 --- a/crates/smtp/src/core/throttle.rs +++ b/crates/smtp/src/core/throttle.rs @@ -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 Session { +impl Session { 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 diff --git a/crates/smtp/src/inbound/auth.rs b/crates/smtp/src/inbound/auth.rs index e466faac..a5ef6b22 100644 --- a/crates/smtp/src/inbound/auth.rs +++ b/crates/smtp/src/inbound/auth.rs @@ -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 Session { +impl Session { pub async fn handle_sasl_response( &mut self, token: &mut SaslToken, diff --git a/crates/smtp/src/inbound/ehlo.rs b/crates/smtp/src/inbound/ehlo.rs index f8b11226..1d76eba5 100644 --- a/crates/smtp/src/inbound/ehlo.rs +++ b/crates/smtp/src/inbound/ehlo.rs @@ -200,12 +200,7 @@ impl Session { .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; - } + response.capabilities |= EXT_AUTH; } } diff --git a/crates/smtp/src/inbound/session.rs b/crates/smtp/src/inbound/session.rs index 720e8903..6e8f10b5 100644 --- a/crates/smtp/src/inbound/session.rs +++ b/crates/smtp/src/inbound/session.rs @@ -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 Session { 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 Session { } } -impl ResolveVariable for Session { +impl ResolveVariable for Session { fn resolve_variable(&self, variable: u32) -> expr::Variable<'_> { match variable { V_RECIPIENT => self @@ -424,6 +416,13 @@ impl ResolveVariable for Session { .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::>() + .into(), V_SENDER => self .data .mail_from @@ -442,8 +441,12 @@ impl ResolveVariable for Session { 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(), } } diff --git a/crates/smtp/src/inbound/spawn.rs b/crates/smtp/src/inbound/spawn.rs index 1e0f3993..1cdd0bde 100644 --- a/crates/smtp/src/inbound/spawn.rs +++ b/crates/smtp/src/inbound/spawn.rs @@ -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 Session { 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::(&self.core.core.smtp.session.connect.script, self) + .eval_if::(&config.script, self) .await .and_then(|name| self.core.core.get_sieve_script(&name)) { @@ -115,7 +122,7 @@ impl Session { self.hostname = self .core .core - .eval_if::(&self.core.core.network.hostname, self) + .eval_if::(&config.hostname, self) .await .unwrap_or_default(); if self.hostname.is_empty() { @@ -124,14 +131,14 @@ impl Session { 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::(&self.core.core.smtp.session.connect.greeting, self) + .eval_if::(&config.greeting, self) .await .filter(|g| !g.is_empty()) .map(|g| format!("220 {}\r\n", g)) diff --git a/crates/smtp/src/inbound/vrfy.rs b/crates/smtp/src/inbound/vrfy.rs index 4cf024b9..f846b21b 100644 --- a/crates/smtp/src/inbound/vrfy.rs +++ b/crates/smtp/src/inbound/vrfy.rs @@ -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 Session { +impl Session { pub async fn handle_vrfy(&mut self, address: String) -> Result<(), ()> { match self .core diff --git a/crates/smtp/src/outbound/delivery.rs b/crates/smtp/src/outbound/delivery.rs index a2e1701c..f046b37f 100644 --- a/crates/smtp/src/outbound/delivery.rs +++ b/crates/smtp/src/outbound/delivery.rs @@ -726,7 +726,15 @@ impl DeliveryAttempt { .core .eval_if::(&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, diff --git a/crates/smtp/src/outbound/lookup.rs b/crates/smtp/src/outbound/lookup.rs index c8099d52..a2f70f72 100644 --- a/crates/smtp/src/outbound/lookup.rs +++ b/crates/smtp/src/outbound/lookup.rs @@ -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}; diff --git a/crates/smtp/src/queue/mod.rs b/crates/smtp/src/queue/mod.rs index 4fa0d864..b6bcc2ea 100644 --- a/crates/smtp/src/queue/mod.rs +++ b/crates/smtp/src/queue/mod.rs @@ -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::>() + .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::>() + .into(), V_PRIORITY => self.priority.into(), _ => "".into(), } diff --git a/crates/smtp/src/reporting/dkim.rs b/crates/smtp/src/reporting/dkim.rs index cb4ffa00..5aaa2c40 100644 --- a/crates/smtp/src/reporting/dkim.rs +++ b/crates/smtp/src/reporting/dkim.rs @@ -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 Session { +impl Session { pub async fn send_dkim_report( &self, rcpt: &str, diff --git a/crates/smtp/src/reporting/dmarc.rs b/crates/smtp/src/reporting/dmarc.rs index 360f1020..d9e468fa 100644 --- a/crates/smtp/src/reporting/dmarc.rs +++ b/crates/smtp/src/reporting/dmarc.rs @@ -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, } -impl Session { +impl Session { #[allow(clippy::too_many_arguments)] pub async fn send_dmarc_report( &self, diff --git a/crates/smtp/src/reporting/spf.rs b/crates/smtp/src/reporting/spf.rs index 196bb43e..8ad915cd 100644 --- a/crates/smtp/src/reporting/spf.rs +++ b/crates/smtp/src/reporting/spf.rs @@ -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 Session { +impl Session { pub async fn send_spf_report( &self, rcpt: &str, diff --git a/crates/smtp/src/scripts/event_loop.rs b/crates/smtp/src/scripts/event_loop.rs index 7d0e4563..3130a625 100644 --- a/crates/smtp/src/scripts/event_loop.rs +++ b/crates/smtp/src/scripts/event_loop.rs @@ -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::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) => { diff --git a/crates/smtp/src/scripts/exec.rs b/crates/smtp/src/scripts/exec.rs index 11a8003f..10f6086b 100644 --- a/crates/smtp/src/scripts/exec.rs +++ b/crates/smtp/src/scripts/exec.rs @@ -139,6 +139,7 @@ impl Session { pub async fn run_script(&self, script: Arc, 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 diff --git a/crates/smtp/src/scripts/mod.rs b/crates/smtp/src/scripts/mod.rs index e8e5ab94..43e65b87 100644 --- a/crates/smtp/src/scripts/mod.rs +++ b/crates/smtp/src/scripts/mod.rs @@ -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>>, variables: AHashMap, Variable>, envelope: Vec<(Envelope, Variable)>, + from_addr: String, + from_name: String, + return_path: String, + sign: Vec, #[cfg(feature = "test_mode")] expected_variables: Option>, } @@ -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>) -> Self { Self { message: message.into(), diff --git a/crates/store/src/backend/foundationdb/main.rs b/crates/store/src/backend/foundationdb/main.rs index 871f2836..54c9cceb 100644 --- a/crates/store/src/backend/foundationdb/main.rs +++ b/crates/store/src/backend/foundationdb/main.rs @@ -60,7 +60,10 @@ impl FdbStore { }) .ok()?; - if let Some(value) = config.property::((&prefix, "transaction.timeout")) { + if let Some(value) = config + .property::>((&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::((&prefix, "transaction.max-retry-delay")) { + if let Some(value) = config + .property::>((&prefix, "transaction.max-retry-delay")) + .unwrap_or_default() + { db.set_option(DatabaseOption::TransactionMaxRetryDelay( value.as_millis() as i32 )) diff --git a/crates/store/src/backend/memory/mod.rs b/crates/store/src/backend/memory/mod.rs index 9da1333d..ccd43d09 100644 --- a/crates/store/src/backend/memory/mod.rs +++ b/crates/store/src/backend/memory/mod.rs @@ -93,7 +93,13 @@ impl Stores { } if has_others { - Value::Text(value.to_string().into()) + 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() diff --git a/crates/store/src/backend/mysql/main.rs b/crates/store/src/backend/mysql/main.rs index 27a2cd5a..e41aab1e 100644 --- a/crates/store/src/backend/mysql/main.rs +++ b/crates/store/src/backend/mysql/main.rs @@ -49,7 +49,8 @@ impl MysqlStore { .max_allowed_packet(config.property((&prefix, "max-allowed-packet"))) .wait_timeout( config - .property::((&prefix, "timeout")) + .property::>((&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::((&prefix, "pool.min-connections")) { + if let Some(n_size) = config + .property::((&prefix, "pool.min-connections")) + .filter(|&n| n > 0) + { pool_min = n_size; } - if let Some(n_size) = config.property::((&prefix, "pool.max-connections")) { + if let Some(n_size) = config + .property::((&prefix, "pool.max-connections")) + .filter(|&n| n > 0) + { pool_max = n_size; } opts = opts.pool_opts( diff --git a/crates/store/src/backend/postgres/main.rs b/crates/store/src/backend/postgres/main.rs index fc2ef964..d1b8c422 100644 --- a/crates/store/src/backend/postgres/main.rs +++ b/crates/store/src/backend/postgres/main.rs @@ -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::>((&prefix, "timeout")) + .unwrap_or_default(); cfg.manager = Some(ManagerConfig { recycling_method: RecyclingMethod::Fast, }); diff --git a/crates/store/src/backend/redis/mod.rs b/crates/store/src/backend/redis/mod.rs index 9d80aad4..607b827a 100644 --- a/crates/store/src/backend/redis/mod.rs +++ b/crates/store/src/backend/redis/mod.rs @@ -67,23 +67,81 @@ impl RedisStore { return None; } - Some(match config.value_require((&prefix, "redis-type"))? { - "single" => { - let client = Client::open(urls.into_iter().next().unwrap()) - .map_err(|err| { - config.new_build_error( - prefix.as_str(), - format!("Failed to open Redis client: {err:?}"), - ) - }) - .ok()?; - let timeout = config - .property_or_default((&prefix, "timeout"), "10s") - .unwrap_or_else(|| Duration::from_secs(10)); + Some( + match config.value((&prefix, "redis-type")).unwrap_or("single") { + "single" => { + let client = Client::open(urls.into_iter().next().unwrap()) + .map_err(|err| { + config.new_build_error( + prefix.as_str(), + format!("Failed to open Redis client: {err:?}"), + ) + }) + .ok()?; + let timeout = config + .property_or_default((&prefix, "timeout"), "10s") + .unwrap_or_default(); - Self { - pool: RedisPool::Single( - build_pool(config, &prefix, RedisConnectionManager { client, timeout }) + Self { + pool: RedisPool::Single( + build_pool(config, &prefix, RedisConnectionManager { client, timeout }) + .map_err(|err| { + config.new_build_error( + prefix.as_str(), + format!("Failed to build Redis pool: {err:?}"), + ) + }) + .ok()?, + ), + } + } + "cluster" => { + let mut builder = ClusterClientBuilder::new(urls.into_iter()); + if let Some(value) = config.property((&prefix, "user")) { + builder = builder.username(value); + } + if let Some(value) = config.property((&prefix, "password")) { + builder = builder.password(value); + } + if let Some(value) = config.property((&prefix, "retry.total")) { + builder = builder.retries(value); + } + if let Some(value) = config + .property::>((&prefix, "retry.max-wait")) + .unwrap_or_default() + { + builder = builder.max_retry_wait(value.as_millis() as u64); + } + if let Some(value) = config + .property::>((&prefix, "retry.min-wait")) + .unwrap_or_default() + { + builder = builder.min_retry_wait(value.as_millis() as u64); + } + if let Some(true) = config.property::((&prefix, "read-from-replicas")) { + builder = builder.read_from_replicas(); + } + + let client = builder + .build() + .map_err(|err| { + config.new_build_error( + prefix.as_str(), + format!("Failed to open Redis client: {err:?}"), + ) + }) + .ok()?; + let timeout = config + .property_or_default::((&prefix, "timeout"), "10s") + .unwrap_or_else(|| Duration::from_secs(10)); + + Self { + pool: RedisPool::Cluster( + build_pool( + config, + &prefix, + RedisClusterConnectionManager { client, timeout }, + ) .map_err(|err| { config.new_build_error( prefix.as_str(), @@ -91,66 +149,16 @@ impl RedisStore { ) }) .ok()?, - ), + ), + } } - } - "cluster" => { - let mut builder = ClusterClientBuilder::new(urls.into_iter()); - if let Some(value) = config.property((&prefix, "user")) { - builder = builder.username(value); + invalid => { + let err = format!("Invalid Redis type {invalid:?}"); + config.new_parse_error((&prefix, "redis-type"), err); + return None; } - if let Some(value) = config.property((&prefix, "password")) { - builder = builder.password(value); - } - if let Some(value) = config.property((&prefix, "retry.total")) { - builder = builder.retries(value); - } - if let Some(value) = config.property::((&prefix, "retry.max-wait")) { - builder = builder.max_retry_wait(value.as_millis() as u64); - } - if let Some(value) = config.property::((&prefix, "retry.min-wait")) { - builder = builder.min_retry_wait(value.as_millis() as u64); - } - if let Some(true) = config.property::((&prefix, "read-from-replicas")) { - builder = builder.read_from_replicas(); - } - - let client = builder - .build() - .map_err(|err| { - config.new_build_error( - prefix.as_str(), - format!("Failed to open Redis client: {err:?}"), - ) - }) - .ok()?; - let timeout = config - .property_or_default((&prefix, "timeout"), "10s") - .unwrap_or_else(|| Duration::from_secs(10)); - - Self { - pool: RedisPool::Cluster( - build_pool( - config, - &prefix, - RedisClusterConnectionManager { client, timeout }, - ) - .map_err(|err| { - config.new_build_error( - prefix.as_str(), - format!("Failed to build Redis pool: {err:?}"), - ) - }) - .ok()?, - ), - } - } - invalid => { - let err = format!("Invalid Redis type {invalid:?}"); - config.new_parse_error((&prefix, "redis-type"), err); - return None; - } - }) + }, + ) } } @@ -168,12 +176,19 @@ fn build_pool( ) .create_timeout( config - .property_or_default::((prefix, "pool.create-timeout"), "30s") - .unwrap_or_else(|| Duration::from_secs(30)) - .into(), + .property_or_default::>((prefix, "pool.create-timeout"), "30s") + .unwrap_or_default(), + ) + .wait_timeout( + config + .property_or_default::>((prefix, "pool.wait-timeout"), "30s") + .unwrap_or_default(), + ) + .recycle_timeout( + config + .property_or_default::>((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!( diff --git a/crates/utils/src/config/parser.rs b/crates/utils/src/config/parser.rs index c0bfbe57..a6d9782c 100644 --- a/crates/utils/src/config/parser.rs +++ b/crates/utils/src/config/parser.rs @@ -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!( diff --git a/crates/utils/src/config/utils.rs b/crates/utils/src/config/utils.rs index bd850f2a..b310f0b1 100644 --- a/crates/utils/src/config/utils.rs +++ b/crates/utils/src/config/utils.rs @@ -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( &mut self, key: impl AsKey, - default: impl AsKey, + or_else: impl AsKey, + default: &str, ) -> Option { 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) { 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 { + Ok(()) + } +} + pub trait AsKey: Clone { fn as_key(&self) -> String; fn as_prefix(&self) -> String; diff --git a/crates/utils/src/suffixlist.rs b/crates/utils/src/suffixlist.rs index fddb8a00..0098e387 100644 --- a/crates/utils/src/suffixlist.rs +++ b/crates/utils/src/suffixlist.rs @@ -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::>(); - 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() } diff --git a/resources/config/build.py b/resources/config/build.py new file mode 100644 index 00000000..a9d428b1 --- /dev/null +++ b/resources/config/build.py @@ -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() diff --git a/resources/config/common/cache.toml b/resources/config/common/cache.toml deleted file mode 100644 index f08eaa74..00000000 --- a/resources/config/common/cache.toml +++ /dev/null @@ -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 diff --git a/resources/config/common/server.toml b/resources/config/common/server.toml deleted file mode 100644 index c80c226c..00000000 --- a/resources/config/common/server.toml +++ /dev/null @@ -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"] diff --git a/resources/config/common/sieve.toml b/resources/config/common/sieve.toml deleted file mode 100644 index d41cee31..00000000 --- a/resources/config/common/sieve.toml +++ /dev/null @@ -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."; -# }''' - diff --git a/resources/config/common/store.toml b/resources/config/common/store.toml deleted file mode 100644 index ea96531c..00000000 --- a/resources/config/common/store.toml +++ /dev/null @@ -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 diff --git a/resources/config/common/tls.toml b/resources/config/common/tls.toml deleted file mode 100644 index 2c5fe746..00000000 --- a/resources/config/common/tls.toml +++ /dev/null @@ -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__" diff --git a/resources/config/common/tracing.toml b/resources/config/common/tracing.toml deleted file mode 100644 index 466172c9..00000000 --- a/resources/config/common/tracing.toml +++ /dev/null @@ -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: "] -level = "debug" -enable = false - -[tracing."log"] -method = "log" -path = "%{BASE_PATH}%/logs" -prefix = "stalwart.log" -rotate = "daily" -level = "info" -enable = true diff --git a/resources/config/config.toml b/resources/config/config.toml deleted file mode 100644 index 63b23694..00000000 --- a/resources/config/config.toml +++ /dev/null @@ -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" ] diff --git a/resources/config/directory/imap.toml b/resources/config/directory/imap.toml deleted file mode 100644 index 912906a5..00000000 --- a/resources/config/directory/imap.toml +++ /dev/null @@ -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}%"] - diff --git a/resources/config/directory/internal.toml b/resources/config/directory/internal.toml deleted file mode 100644 index 0ca96d21..00000000 --- a/resources/config/directory/internal.toml +++ /dev/null @@ -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'} diff --git a/resources/config/directory/ldap.toml b/resources/config/directory/ldap.toml deleted file mode 100644 index 6e91a31a..00000000 --- a/resources/config/directory/ldap.toml +++ /dev/null @@ -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" - diff --git a/resources/config/directory/lmtp.toml b/resources/config/directory/lmtp.toml deleted file mode 100644 index bff3ba4f..00000000 --- a/resources/config/directory/lmtp.toml +++ /dev/null @@ -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}%"] - diff --git a/resources/config/directory/memory.toml b/resources/config/directory/memory.toml deleted file mode 100644 index d484d999..00000000 --- a/resources/config/directory/memory.toml +++ /dev/null @@ -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" diff --git a/resources/config/directory/sql.toml b/resources/config/directory/sql.toml deleted file mode 100644 index 04a8ea23..00000000 --- a/resources/config/directory/sql.toml +++ /dev/null @@ -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" diff --git a/resources/config/imap/listener.toml b/resources/config/imap/listener.toml deleted file mode 100644 index 8f4d6357..00000000 --- a/resources/config/imap/listener.toml +++ /dev/null @@ -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" diff --git a/resources/config/imap/settings.toml b/resources/config/imap/settings.toml deleted file mode 100644 index 873ff42a..00000000 --- a/resources/config/imap/settings.toml +++ /dev/null @@ -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 diff --git a/resources/config/jmap/auth.toml b/resources/config/jmap/auth.toml deleted file mode 100644 index eaf68d83..00000000 --- a/resources/config/jmap/auth.toml +++ /dev/null @@ -1,6 +0,0 @@ -############################################# -# JMAP authentication & session configuration -############################################# - -[jmap.session.purge] -frequency = "15 * *" diff --git a/resources/config/jmap/listener.toml b/resources/config/jmap/listener.toml deleted file mode 100644 index e3186851..00000000 --- a/resources/config/jmap/listener.toml +++ /dev/null @@ -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" diff --git a/resources/config/jmap/oauth.toml b/resources/config/jmap/oauth.toml deleted file mode 100644 index acb2e594..00000000 --- a/resources/config/jmap/oauth.toml +++ /dev/null @@ -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" diff --git a/resources/config/jmap/protocol.toml b/resources/config/jmap/protocol.toml deleted file mode 100644 index 336bc4a1..00000000 --- a/resources/config/jmap/protocol.toml +++ /dev/null @@ -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 diff --git a/resources/config/jmap/push.toml b/resources/config/jmap/push.toml deleted file mode 100644 index 21111c1a..00000000 --- a/resources/config/jmap/push.toml +++ /dev/null @@ -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" diff --git a/resources/config/jmap/ratelimit.toml b/resources/config/jmap/ratelimit.toml deleted file mode 100644 index 4fef99f1..00000000 --- a/resources/config/jmap/ratelimit.toml +++ /dev/null @@ -1,9 +0,0 @@ - -############################################# -# JMAP server rate limiter configuration -############################################# - -[jmap.rate-limit] -account = "1000/1m" -anonymous = "100/1m" -use-forwarded = false diff --git a/resources/config/jmap/websockets.toml b/resources/config/jmap/websockets.toml deleted file mode 100644 index e9018e1e..00000000 --- a/resources/config/jmap/websockets.toml +++ /dev/null @@ -1,8 +0,0 @@ -############################################# -# JMAP WebSockets server configuration -############################################# - -[jmap.web-sockets] -throttle = "1s" -timeout = "10m" -heartbeat = "1m" diff --git a/resources/config/minimal.toml b/resources/config/minimal.toml new file mode 100644 index 00000000..fea40d54 --- /dev/null +++ b/resources/config/minimal.toml @@ -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"] diff --git a/resources/config/security.toml b/resources/config/security.toml new file mode 100644 index 00000000..9d73b280 --- /dev/null +++ b/resources/config/security.toml @@ -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 diff --git a/resources/config/smtp/auth.toml b/resources/config/smtp/auth.toml deleted file mode 100644 index 2e77d92c..00000000 --- a/resources/config/smtp/auth.toml +++ /dev/null @@ -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" } ] - diff --git a/resources/config/smtp/listener.toml b/resources/config/smtp/listener.toml deleted file mode 100644 index f385fb7f..00000000 --- a/resources/config/smtp/listener.toml +++ /dev/null @@ -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" diff --git a/resources/config/smtp/milter.toml b/resources/config/smtp/milter.toml deleted file mode 100644 index 4086b33a..00000000 --- a/resources/config/smtp/milter.toml +++ /dev/null @@ -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" diff --git a/resources/config/smtp/queue.toml b/resources/config/smtp/queue.toml deleted file mode 100644 index 79f0f4d0..00000000 --- a/resources/config/smtp/queue.toml +++ /dev/null @@ -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 diff --git a/resources/config/smtp/remote.toml b/resources/config/smtp/remote.toml deleted file mode 100644 index 27a658f5..00000000 --- a/resources/config/smtp/remote.toml +++ /dev/null @@ -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 = "" diff --git a/resources/config/smtp/report.toml b/resources/config/smtp/report.toml deleted file mode 100644 index 91a9e32e..00000000 --- a/resources/config/smtp/report.toml +++ /dev/null @@ -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']" diff --git a/resources/config/smtp/resolver.toml b/resources/config/smtp/resolver.toml deleted file mode 100644 index 397b22e6..00000000 --- a/resources/config/smtp/resolver.toml +++ /dev/null @@ -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"] diff --git a/resources/config/smtp/session.toml b/resources/config/smtp/session.toml deleted file mode 100644 index b72ba077..00000000 --- a/resources/config/smtp/session.toml +++ /dev/null @@ -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 diff --git a/resources/config/smtp/signature.toml b/resources/config/smtp/signature.toml deleted file mode 100644 index ef9497b5..00000000 --- a/resources/config/smtp/signature.toml +++ /dev/null @@ -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 diff --git a/resources/config/smtp/spamfilter.toml b/resources/config/smtp/spamfilter.toml deleted file mode 100644 index 58fabecb..00000000 --- a/resources/config/smtp/spamfilter.toml +++ /dev/null @@ -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"] diff --git a/resources/config/spamfilter.toml b/resources/config/spamfilter.toml new file mode 100644 index 00000000..9242b846 --- /dev/null +++ b/resources/config/spamfilter.toml @@ -0,0 +1,10331 @@ +[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'" } ] + +[sieve.trusted.scripts.spam-filter] +name = "Spam Filter" +contents = ''' + +#### Script config.sieve #### + +# Whether to add an X-Spam-Status header +let "ADD_HEADER_SPAM" "key_get('spam-config', 'add-spam')"; + +# Whether to add an X-Spam-Result header +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" "key_get('spam-config', 'learn-ham-replies')"; + +# Whether the bayes classifier should be trained automatically +let "AUTOLEARN_ENABLE" "key_get('spam-config', 'learn-enable')"; + +# When to learn ham (score >= threshold) +let "AUTOLEARN_HAM_THRESHOLD" "key_get('spam-config', 'learn-ham-threshold')"; + +# When to learn spam (score <= 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" "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" "key_get('spam-config', 'threshold-spam')"; + +# Discard messages with a score above this threshold +let "SCORE_DISCARD_THRESHOLD" "key_get('spam-config', 'threshold-discard')"; + +# Reject messages with a score above this threshold +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" "key_get('spam-config', 'directory')"; + +# Store to use for Bayes tokens and ids (leave empty for default) +let "SPAM_DB" "key_get('spam-config', 'lookup')"; + + +#### Script prelude.sieve #### + +# Convert body to plain text +let "text_body" "body.to_text"; + +# Obtain all URLs in the body +let "body_urls" "tokenize(text_body, 'uri')"; + +# Obtain all URLs in href and src attributes +let "html_body_urls" "html_attrs(body.html, '', ['href', 'src'])"; + +# Obtain all URLs in the subject, combine them with all other URLs and remove duplicates +let "urls" "dedup(tokenize(header.subject, 'uri') + body_urls + html_body_urls)"; + +# Obtain thread name and subject +let "subject_lc" "to_lowercase(header.subject)"; +let "subject_clean" "thread_name(header.subject)"; +let "body_and_subject" "subject_clean + text_body"; + +# Obtain all recipients +let "recipients" "to_lowercase(header.to:cc:bcc[*].addr[*])"; +let "recipients_clean" "winnow(dedup(recipients))"; +let "recipients_to" "header.to[*].addr[*]"; +let "recipients_cc" "header.cc[*].addr[*]"; + +# Obtain From parts +let "from_name" "to_lowercase(trim(header.from.name))"; +let "from_addr" "to_lowercase(trim(header.from.addr))"; +let "from_local" "email_part(from_addr, 'local')"; +let "from_domain" "email_part(from_addr, 'domain')"; +let "from_domain_sld" "domain_part(from_domain, 'sld')"; + +# Obtain Reply-To address +let "rto_addr" "to_lowercase(header.reply-to.addr)"; + +# Obtain Envelope From parts +let "envfrom_local" "email_part(envelope.from, 'local')"; +let "envfrom_domain" "email_part(envelope.from, 'domain')"; +let "envfrom_domain_sld" "domain_part(envfrom_domain, 'sld')"; + +# Obtain HELO domain SLD +let "helo_domain_sld" "domain_part(env.helo_domain, 'sld')"; + +# Create score variable +let "score" "0.0"; + + +#### Script from.sieve #### + +let "from_count" "count(header.from[*].raw)"; +let "service_accounts" "['www-data', 'anonymous', 'ftp', 'apache', 'nobody', 'guest', 'nginx', 'web', 'www']"; + +if eval "from_count > 0" { + let "from_raw" "to_lowercase(header.from.raw)"; + + if eval "from_count > 1" { + let "t.MULTIPLE_FROM" "1"; + } + + if eval "is_email(from_addr)" { + if eval "contains(service_accounts, from_local)" { + let "t.FROM_SERVICE_ACCT" "1"; + } + if eval "starts_with(from_domain, 'www.')" { + let "t.WWW_DOT_DOMAIN" "1"; + } + + if eval "key_exists('spam-free', from_domain_sld)" { + let "t.FREEMAIL_FROM" "1"; + } elsif eval "key_exists('spam-disposable', from_domain_sld)" { + let "t.DISPOSABLE_FROM" "1"; + } + } else { + let "t.FROM_INVALID" "1"; + } + + if eval "is_empty(from_name)" { + let "t.FROM_NO_DN" "1"; + } elsif eval "eq_ignore_case(from_addr, from_name)" { + let "t.FROM_DN_EQ_ADDR" "1"; + } else { + if eval "!t.FROM_INVALID" { + let "t.FROM_HAS_DN" "1"; + } + + if eval "is_email(from_name)" { + let "from_name_sld" "domain_part(email_part(from_name, 'domain'), 'sld')"; + if eval "(!t.FROM_INVALID && from_domain_sld != from_name_sld) || + (!is_empty(envelope.from) && envfrom_domain_sld != from_name_sld) || + (is_empty(envelope.from) && helo_domain_sld != from_name_sld)" { + let "t.SPOOF_DISPLAY_NAME" "1"; + } else { + let "t.FROM_NEQ_DISPLAY_NAME" "1"; + } + } else { + if eval "contains(from_name, 'mr. ') || contains(from_name, 'ms. ') || contains(from_name, 'mrs. ') || contains(from_name, 'dr. ')" { + let "t.FROM_NAME_HAS_TITLE" "1"; + } + if eval "contains(header.from.name, ' ')" { + let "t.FROM_NAME_EXCESS_SPACE" "1"; + } + } + } + + if eval "is_empty(envelope.from) && + (from_local == 'postmaster' || + from_local == 'mailer-daemon' || + from_local == 'root')" { + let "t.FROM_BOUNCE" "1"; + } + + if eval "(!is_empty(envelope.from) && + eq_ignore_case(from_addr, envelope.from)) || + (t.FROM_BOUNCE && + !is_empty(from_domain) && + from_domain_sld == helo_domain_sld)" { + let "t.FROM_EQ_ENVFROM" "1"; + } elsif eval "!t.FROM_INVALID" { + let "t.FORGED_SENDER" "1"; + let "t.FROM_NEQ_ENVFROM" "1"; + } + + if eval "contains(from_local, '+')" { + let "t.TAGGED_FROM" "1"; + } + + if eval "count(recipients_to) + count(recipients_cc) == 1" { + if eval "eq_ignore_case(recipients_to[0], from_addr)" { + let "t.TO_EQ_FROM" "1"; + } elsif eval "eq_ignore_case(email_part(recipients_to[0], 'domain'), from_domain)" { + let "t.TO_DOM_EQ_FROM_DOM" "1"; + } + } + + if eval "!is_ascii(from_raw)" { + if eval "!env.param.smtputf8 && env.param.body != '8bitmime' && env.param.body != 'binarymime'" { + let "t.FROM_NEEDS_ENCODING" "1"; + } + if eval "!is_header_utf8_valid('From')" { + let "t.INVALID_FROM_8BIT" "1"; + } + } + + if eval "is_ascii(header.from) && contains(from_raw, '=?') && contains(from_raw, '?=')" { + if eval "contains(from_raw, '?q?')" { + # From header is unnecessarily encoded in quoted-printable + let "t.FROM_EXCESS_QP" "1"; + } elsif eval "contains(from_raw, '?b?')" { + # From header is unnecessarily encoded in base64 + let "t.FROM_EXCESS_BASE64" "1"; + } + } + + if eval "!is_empty(from_name) && !is_empty(from_addr) && !contains(from_raw, ' <')" { + let "t.R_NO_SPACE_IN_FROM" "1"; + } + + # Read confirmation address is different to from address + let "crt" "header.X-Confirm-Reading-To.addr"; + if eval "!is_empty(crt) && !eq_ignore_case(from_addr, crt)" { + let "t.HEADER_RCONFIRM_MISMATCH" "1"; + } +} else { + let "t.MISSING_FROM" "1"; +} + +if eval "!is_empty(envelope.from)" { + if eval "is_email(envelope.from)" { + if eval "contains(service_accounts, envfrom_local)" { + let "t.ENVFROM_SERVICE_ACCT" "1"; + } + } else { + let "t.ENVFROM_INVALID" "1"; + } + + if eval "!is_empty(envfrom_domain_sld)" { + if eval "key_exists('spam-free', envfrom_domain_sld)" { + let "t.FREEMAIL_ENVFROM" "1"; + } elsif eval "key_exists('spam-disposable', envfrom_domain_sld)" { + let "t.DISPOSABLE_ENVFROM" "1"; + } + + # Mail from no resolve to A or MX + if eval "!dns_exists(envfrom_domain, 'mx') && !dns_exists(envfrom_domain, 'ip')" { + let "t.FROMHOST_NORES_A_OR_MX" "1"; + } + } + + # Read confirmation address is different to return path + let "dnt" "header.Disposition-Notification-To.addr"; + if eval "!is_empty(dnt) && !eq_ignore_case(envelope.from, dnt)" { + let "t.HEADER_FORGED_MDN" "1"; + } +} + +if eval "!t.FROM_SERVICE_ACCT && + (contains_ignore_case(service_accounts, email_part(rto_addr, 'local')) || + contains_ignore_case(service_accounts, email_part(header.sender.addr, 'local')))" { + let "t.FROM_SERVICE_ACCT" "1"; +} + +if eval "!t.WWW_DOT_DOMAIN && + (contains_ignore_case(rto_addr, '@www.') || + contains_ignore_case(header.sender.addr, '@www.'))" { + let "t.WWW_DOT_DOMAIN" "1"; +} + + + +#### Script recipient.sieve #### + + +let "to_raw" "to_lowercase(header.to.raw)"; +if eval "!is_empty(to_raw)" { + if eval "is_ascii(header.to) && contains(to_raw, '=?') && contains(to_raw, '?=')" { + if eval "contains(to_raw, '?q?')" { + # To header is unnecessarily encoded in quoted-printable + let "t.TO_EXCESS_QP" "1"; + } elsif eval "contains(to_raw, '?b?')" { + # To header is unnecessarily encoded in base64 + let "t.TO_EXCESS_BASE64" "1"; + } + } elsif eval "!is_ascii(to_raw) && !env.param.smtputf8 && env.param.body != '8bitmime' && env.param.body != 'binarymime'" { + # To needs encoding + let "t.TO_NEEDS_ENCODING" "1"; + } +} else { + let "t.MISSING_TO" "1"; +} + +let "rcpt_count" "count(recipients_clean)"; + +if eval "rcpt_count > 0" { + if eval "rcpt_count == 1" { + let "t.RCPT_COUNT_ONE" "1"; + } elsif eval "rcpt_count == 2" { + let "t.RCPT_COUNT_TWO" "1"; + } elsif eval "rcpt_count == 3" { + let "t.RCPT_COUNT_THREE" "1"; + } elsif eval "rcpt_count <= 5" { + let "t.RCPT_COUNT_FIVE" "1"; + } elsif eval "rcpt_count <= 7" { + let "t.RCPT_COUNT_SEVEN" "1"; + } elsif eval "rcpt_count <= 12" { + let "t.RCPT_COUNT_TWELVE" "1"; + } else { + let "t.RCPT_COUNT_GT_50" "1"; + } + + let "rcpt_name" "to_lowercase(header.to:cc:bcc[*].name[*])"; + let "i" "count(recipients)"; + let "to_dn_count" "0"; + let "to_dn_eq_addr_count" "0"; + let "to_match_envrcpt" "0"; + + while "i != 0" { + let "i" "i - 1"; + let "addr" "recipients[i]"; + + if eval "!is_empty(addr)" { + let "name" "rcpt_name[i]"; + + if eval "!is_empty(name)" { + if eval "name == addr" { + let "to_dn_eq_addr_count" "to_dn_eq_addr_count + 1"; + } else { + let "to_dn_count" "to_dn_count + 1"; + if eval "name == 'recipient' || name == 'recipients'" { + let "t.TO_DN_RECIPIENTS" "1"; + } + } + } + + if eval "contains(envelope.to, addr)" { + let "to_match_envrcpt" "to_match_envrcpt + 1"; + } + + # Check if the local part is present in the subject + let "local_part" "email_part(addr, 'local')"; + if eval "!is_empty(local_part)" { + if eval "contains(subject_lc, addr)" { + let "t.RCPT_ADDR_IN_SUBJECT" "1"; + } elsif eval "len(local_part) > 3 && contains(subject_lc, local_part)" { + let "t.RCPT_LOCAL_IN_SUBJECT" "1"; + } + + if eval "contains(local_part, '+')" { + let "t.TAGGED_RCPT" "1"; + } + } + + # Check if it is an into to info + if eval "!t.INFO_TO_INFO_LU && + local_part == 'info' && + from_local == 'info' && + header.List-Unsubscribe.exists" { + let "t.INFO_TO_INFO_LU" "1"; + } + + # Check for freemail or disposable domains + let "domain" "domain_part(email_part(addr, 'domain'), 'sld')"; + if eval "!is_empty(domain)" { + if eval "key_exists('spam-free', domain)" { + if eval "!t.FREEMAIL_TO && contains_ignore_case(recipients_to, addr)" { + let "t.FREEMAIL_TO" "1"; + } elsif eval "!t.FREEMAIL_CC && contains_ignore_case(recipients_cc, addr)" { + let "t.FREEMAIL_CC" "1"; + } + } elsif eval "key_exists('spam-disposable', domain)" { + if eval "!t.DISPOSABLE_TO && contains_ignore_case(recipients_to, addr)" { + let "t.DISPOSABLE_TO" "1"; + } elsif eval "!t.DISPOSABLE_CC && contains_ignore_case(recipients_cc, addr)" { + let "t.DISPOSABLE_CC" "1"; + } + } + } + } + } + + if eval "to_dn_count == 0 && to_dn_eq_addr_count == 0" { + let "t.TO_DN_NONE" "1"; + } elsif eval "to_dn_count == rcpt_count" { + let "t.TO_DN_ALL" "1"; + } elsif eval "to_dn_count > 0" { + let "t.TO_DN_SOME" "1"; + } + + if eval "to_dn_eq_addr_count == rcpt_count" { + let "t.TO_DN_EQ_ADDR_ALL" "1"; + } elsif eval "to_dn_eq_addr_count > 0" { + let "t.TO_DN_EQ_ADDR_SOME" "1"; + } + + if eval "to_match_envrcpt == rcpt_count" { + let "t.TO_MATCH_ENVRCPT_ALL" "1"; + } else { + if eval "to_match_envrcpt > 0" { + let "t.TO_MATCH_ENVRCPT_SOME" "1"; + } + + if eval "is_empty(header.List-Unsubscribe:List-Id[*])" { + let "i" "count(envelope.to)"; + while "i != 0" { + let "i" "i - 1"; + let "env_rcpt" "envelope.to[i]"; + + if eval "!contains(recipients, env_rcpt) && env_rcpt != envelope.from" { + let "t.FORGED_RECIPIENTS" "1"; + break; + } + } + } + } + + # Message from bounce and over 1 recipient + if eval "rcpt_count > 1 && + (is_empty(envelope.from) || + envfrom_local == 'postmaster' || + envfrom_local == 'mailer-daemon')" { + let "t.RCPT_BOUNCEMOREONE" "1"; + } + + # Check for sorted recipients + if eval "rcpt_count >= 7 && sort(recipients_clean, false) == recipients_clean" { + let "t.SORTED_RECIPS" "1"; + } + + # Check for suspiciously similar recipients + if eval "!t.SORTED_RECIPS && rcpt_count => 5" { + let "i" "rcpt_count"; + let "hits" "0"; + let "combinations" "0"; + + while "i" { + let "i" "i - 1"; + let "j" "i"; + while "j" { + let "j" "j - 1"; + let "a" "recipients_clean[i]"; + let "b" "recipients_clean[j]"; + + if eval "levenshtein_distance(email_part(a, 'local'), email_part(b, 'local')) < 3" { + let "hits" "hits + 1"; + } + + let "a" "email_part(a, 'domain')"; + let "b" "email_part(b, 'domain')"; + + if eval "a != b && levenshtein_distance(a, b) < 4" { + let "hits" "hits + 1"; + } + + let "combinations" "combinations + 1"; + } + } + + if eval "hits / combinations > 0.65" { + let "t.SUSPICIOUS_RECIPS" "1"; + } + } + + # Check for spaces in recipient addresses + let "raw_to" "header.to:cc[*].raw"; + let "i" "len(raw_to)"; + while "i != 0" { + let "i" "i - 1"; + let "raw_addr" "rsplit(raw_to[i], '<')[0]"; + if eval "contains(raw_addr, '>') && (starts_with(raw_addr, ' ' ) || ends_with(raw_addr, ' >'))" { + let "t.TO_WRAPPED_IN_SPACES" "1"; + break; + } + } + +} else { + let "t.RCPT_COUNT_ZERO" "1"; + + if eval "contains(to_raw, 'undisclosed') && contains(to_raw, 'recipients')" { + let "t.R_UNDISC_RCPT" "1"; + } +} + + +#### Script subject.sieve #### + + +let "raw_subject_lc" "to_lowercase(header.subject.raw)"; +let "is_ascii_subject" "is_ascii(subject_lc)"; + +if eval "len(subject_clean) >= 10 && count(tokenize(subject_clean, 'words')) > 1 && is_uppercase(subject_clean)" { + # Subject contains mostly capital letters + let "t.SUBJ_ALL_CAPS" "1"; +} + +if eval "count_chars(subject_clean) > 200" { + # Subject is very long + let "t.LONG_SUBJ" "1"; +} + +if eval "!is_empty(tokenize(subject_lc, 'uri_strict'))" { + # Subject contains a URL + let "t.URL_IN_SUBJECT" "1"; +} + +if eval "!is_ascii(raw_subject_lc) && !env.param.smtputf8 && env.param.body != '8bitmime' && env.param.body != 'binarymime'" { + # Subject needs encoding + let "t.SUBJECT_NEEDS_ENCODING" "1"; +} + +if eval "!header.Subject.exists" { + # Missing subject header + let "t.MISSING_SUBJECT" "1"; +} elsif eval "is_empty(trim(subject_lc))" { + # Subject is empty + let "t.EMPTY_SUBJECT" "1"; +} + +if eval "is_ascii(subject_lc) && contains(raw_subject_lc, '=?') && contains(raw_subject_lc, '?=')" { + if eval "contains(raw_subject_lc, '?q?')" { + # Subject header is unnecessarily encoded in quoted-printable + let "t.SUBJ_EXCESS_QP" "1"; + } elsif eval "contains(raw_subject_lc, '?b?')" { + # Subject header is unnecessarily encoded in base64 + let "t.SUBJ_EXCESS_BASE64" "1"; + } +} + +if eval "starts_with(subject_lc, 're:') && is_empty(header.in-reply-to) && is_empty(header.references)" { + # Fake reply + let "t.FAKE_REPLY" "1"; +} + +let "subject_lc_trim" "trim_end(subject_lc)"; +if eval "subject_lc != subject_lc_trim" { + # Subject ends with space characters + let "t.SUBJECT_ENDS_SPACES" "1"; +} + +if eval "contains(subject_lc, '$') || + contains(subject_lc, '€') || + contains(subject_lc, '£') || + contains(subject_lc, 'Â¥')" { + # Subject contains currency symbols + let "t.SUBJECT_HAS_CURRENCY" "1"; +} + +if eval "ends_with(subject_lc_trim, '!')" { + # Subject ends with an exclamation mark + let "t.SUBJECT_ENDS_EXCLAIM" "1"; +} elsif eval "ends_with(subject_lc_trim, '?')" { + # Subject ends with a question mark + let "t.SUBJECT_ENDS_QUESTION" "1"; +} + +if eval "contains(subject_lc_trim, '!')" { + # Subject contains an exclamation mark + let "t.SUBJECT_HAS_EXCLAIM" "1"; +} + +if eval "contains(subject_lc_trim, '?')" { + # Subject contains a question mark + let "t.SUBJECT_HAS_QUESTION" "1"; +} + + +#### Script replyto.sieve #### + +let "rto_raw" "to_lowercase(header.reply-to.raw)"; +if eval "!is_empty(rto_raw)" { + let "rto_name" "to_lowercase(header.reply-to.name)"; + + if eval "is_email(rto_addr)" { + let "t.HAS_REPLYTO" "1"; + let "rto_domain_sld" "domain_part(email_part(rto_addr, 'domain'), 'sld')"; + + if eval "eq_ignore_case(header.reply-to, header.from)" { + let "t.REPLYTO_EQ_FROM" "1"; + } else { + if eval "rto_domain_sld == from_domain_sld" { + let "t.REPLYTO_DOM_EQ_FROM_DOM" "1"; + } else { + let "is_from_list" "!is_empty(header.List-Unsubscribe:List-Id:X-To-Get-Off-This-List:X-List:Auto-Submitted[*])"; + if eval "!is_from_list && contains_ignore_case(recipients_clean, rto_addr)" { + let "t.REPLYTO_EQ_TO_ADDR" "1"; + } else { + let "t.REPLYTO_DOM_NEQ_FROM_DOM" "1"; + } + + if eval "!is_from_list && + !eq_ignore_case(from_addr, header.to.addr) && + !(count(envelope.to) == 1 && envelope.to[0] == from_addr)" { + let "i" "count(envelope.to)"; + let "found_domain" "0"; + + while "i != 0" { + let "i" "i - 1"; + + if eval "domain_part(email_part(envelope.to[i], 'domain'), 'sld') == from_domain_sld" { + let "found_domain" "1"; + break; + } + } + + if eval "!found_domain" { + let "t.SPOOF_REPLYTO" "1"; + } + } + } + + if eval "!is_empty(rto_name) && eq_ignore_case(rto_name, header.from.name)" { + let "t.REPLYTO_DN_EQ_FROM_DN" "1"; + } + } + + if eval "rto_addr == envelope.from" { + let "t.REPLYTO_ADDR_EQ_FROM" "1"; + } + + if eval "key_exists('spam-free', rto_domain_sld)" { + let "t.FREEMAIL_REPLYTO" "1"; + if eval "rto_domain_sld != from_domain_sld && key_exists('spam-free', from_domain_sld)" { + let "t.FREEMAIL_REPLYTO_NEQ_FROM_DOM" "1"; + } + } elsif eval "key_exists('spam-disposable', rto_domain_sld)" { + let "t.DISPOSABLE_REPLYTO" "1"; + } + + } else { + let "t.REPLYTO_UNPARSEABLE" "1"; + } + + if eval "is_ascii(header.reply-to) && contains(rto_raw, '=?') && contains(rto_raw, '?=')" { + if eval "contains(rto_raw, '?q?')" { + # Reply-To header is unnecessarily encoded in quoted-printable + let "t.REPLYTO_EXCESS_QP" "1"; + } elsif eval "contains(rto_raw, '?b?')" { + # Reply-To header is unnecessarily encoded in base64 + let "t.REPLYTO_EXCESS_BASE64" "1"; + } + } + + if eval "contains(rto_name, 'mr. ') || contains(rto_name, 'ms. ') || contains(rto_name, 'mrs. ') || contains(rto_name, 'dr. ')" { + let "t.REPLYTO_EMAIL_HAS_TITLE" "1"; + } +} + + + +#### Script date.sieve #### + +if eval "header.date.exists" { + let "date" "header.date.date"; + + if eval "date != 0" { + let "date_diff" "env.now - date"; + + if eval "date_diff > 86400" { + # Older than a day + let "t.DATE_IN_PAST" "1"; + } elsif eval "-date_diff > 7200" { + # More than 2 hours in the future + let "t.DATE_IN_FUTURE" "1"; + } + } else { + let "t.INVALID_DATE" "1"; + } +} else { + let "t.MISSING_DATE" "1"; +} + + +#### Script messageid.sieve #### + +let "mid_raw" "trim(header.message-id.raw)"; + +if eval "!is_empty(mid_raw)" { + let "mid_lcase" "to_lowercase(header.message-id)"; + let "mid_rhs" "email_part(mid_lcase, 'domain')"; + + if eval "!is_empty(mid_rhs)" { + if eval "starts_with(mid_rhs, '[') && ends_with(mid_rhs, ']') && is_ip_addr(strip_suffix(strip_prefix(mid_rhs, '['), ']'))" { + let "t.MID_RHS_IP_LITERAL" "1"; + } elsif eval "is_ip_addr(mid_rhs)" { + let "t.MID_BARE_IP" "1"; + } elsif eval "!contains(mid_rhs, '.')" { + let "t.MID_RHS_NOT_FQDN" "1"; + } + + if eval "starts_with(mid_rhs, 'www.')" { + let "t.MID_RHS_WWW" "1"; + } + + if eval "!is_ascii(mid_raw) || contains(mid_raw, '(') || starts_with(mid_lcase, '@')" { + let "t.INVALID_MSGID" "1"; + } + + # From address present in Message-ID checks + let "sender" "from_addr"; + if eval "is_empty(sender)" { + let "sender" "envelope.from"; + } + if eval "!is_empty(sender)" { + if eval "contains(mid_lcase, sender)" { + let "t.MID_CONTAINS_FROM" "1"; + } else { + let "from_domain" "email_part(sender, 'domain')"; + let "mid_sld" "domain_part(mid_rhs, 'sld')"; + + if eval "mid_rhs == from_domain" { + let "t.MID_RHS_MATCH_FROM" "1"; + } elsif eval "!is_empty(mid_sld) && domain_part(from_domain, 'sld') == mid_sld" { + let "t.MID_RHS_MATCH_FROMTLD" "1"; + } + } + } + + # To/Cc addresses present in Message-ID checks + let "recipients_len" "count(recipients)"; + let "i" "0"; + + while "i < recipients_len" { + let "rcpt" "recipients[i]"; + let "i" "i + 1"; + if eval "contains(mid_lcase, rcpt)" { + let "t.MID_CONTAINS_TO" "1"; + } elsif eval "email_part(rcpt, 'domain') == mid_rhs" { + let "t.MID_RHS_MATCH_TO" "1"; + } + } + } else { + let "t.INVALID_MSGID" "1"; + } + + if eval "!starts_with(mid_raw, '<') || !contains(mid_raw, '>')" { + let "t.MID_MISSING_BRACKETS" "1"; + } + +} else { + let "t.MISSING_MID" "1"; +} + + + +#### Script received.sieve #### + +let "rcvd_raw" "header.received[*].raw"; +let "rcvd_count" "count(rcvd_raw)"; + +# Count received headers +if eval "rcvd_count == 0" { + let "t.RCVD_COUNT_ZERO" "1"; +} elsif eval "rcvd_count == 1" { + let "t.RCVD_COUNT_ONE" "1"; +} elsif eval "rcvd_count == 2" { + let "t.RCVD_COUNT_TWO" "1"; +} elsif eval "rcvd_count == 3" { + let "t.RCVD_COUNT_THREE" "1"; +} elsif eval "rcvd_count <= 5" { + let "t.RCVD_COUNT_FIVE" "1"; +} elsif eval "rcvd_count <= 7" { + let "t.RCVD_COUNT_SEVEN" "1"; +} elsif eval "rcvd_count <= 12" { + let "t.RCVD_COUNT_TWELVE" "1"; +} + +# Received from an authenticated user +if eval "!is_empty(env.authenticated_as)" { + let "t.RCVD_VIA_SMTP_AUTH" "1"; +} + +# Received headers have non-ASCII characters +if eval "!is_ascii(rcvd_raw)" { + let "t.RCVD_ILLEGAL_CHARS" "1"; +} + +let "i" "0"; +let "tls_count" "0"; +let "rcvd_from_ip" "0"; +while "i < rcvd_count" { + let "i" "i + 1"; + let "helo_domain" "received_part(i, 'from')"; + + # Check for a forged received trail + if eval "!t.FORGED_RCVD_TRAIL" { + let "iprev" "received_part(i, 'iprev')"; + + if eval "!is_empty(iprev) && !is_empty(helo_domain) && !eq_ignore_case(helo_domain, iprev)" { + let "t.FORGED_RCVD_TRAIL" "1"; + } + } + + if eval "!t.PREVIOUSLY_DELIVERED" { + let "for" "received_part(i, 'for')"; + # Recipient appears on Received trail + if eval "!is_empty(for) && contains_ignore_case(recipients, for)" { + let "t.PREVIOUSLY_DELIVERED" "1"; + } + } + + if eval "!t.RCVD_HELO_USER && eq_ignore_case(helo_domain, 'user')" { + # Received: HELO contains 'user' + let "t.RCVD_HELO_USER" "1"; + } + + if eval "!is_empty(received_part(i, 'from.ip'))" { + # Received from an IP address rather than a FQDN + let "rcvd_from_ip" "rcvd_from_ip + 1"; + } + + if eval "!is_empty(received_part(i, 'tls'))" { + # Received with TLS + let "tls_count" "tls_count + 1"; + } +} + +if eval "rcvd_from_ip >= 2 || (rcvd_from_ip == 1 && is_ip_addr(env.helo_domain))" { + # Has two or more Received headers containing bare IP addresses + let "t.RCVD_DOUBLE_IP_SPAM" "1"; +} + +if eval "rcvd_count == 0" { + # One received header in a message (currently zero but one header will be added later by the MTA) + let "t.ONCE_RECEIVED" "1"; + + # Message has been directly delivered from MUA to local MX + if eval "header.User-Agent.exists || header.X-Mailer.exists" { + let "t.DIRECT_TO_MX" "1"; + } +} + +# Received with TLS checks +if eval "rcvd_count > 0 && tls_count == rcvd_count && !is_empty(env.tls.version)" { + let "t.RCVD_TLS_ALL" "1"; +} elsif eval "!is_empty(env.tls.version)" { + let "t.RCVD_TLS_LAST" "1"; +} else { + let "t.RCVD_NO_TLS_LAST" "1"; +} + + +#### Script headers.sieve #### + +# Mailing list scores +let "ml_score" "count(header.List-Id:List-Archive:List-Owner:List-Help:List-Post:X-Loop:List-Subscribe:List-Unsubscribe[*].exists) * 0.125"; +if eval "ml_score < 1" { + if eval "header.List-Id.exists" { + let "ml_score" "ml_score + 0.50"; + } + if eval "header.List-Subscribe.exists && header.List-Unsubscribe.exists" { + let "ml_score" "ml_score + 0.25"; + } + if eval "header.Precedence.exists && (eq_ignore_case(header.Precedence, 'list') || eq_ignore_case(header.Precedence, 'bulk'))" { + let "ml_score" "ml_score + 0.25"; + } +} +if eval "ml_score >= 1" { + let "t.MAILLIST" "1"; +} + +# X-Priority +if eval "header.x-priority.exists" { + let "xp" "header.x-priority"; + if eval "xp == 0" { + let "t.HAS_X_PRIO_ZERO" "1"; + } elsif eval "xp == 1" { + let "t.HAS_X_PRIO_ONE" "1"; + } elsif eval "xp == 2" { + let "t.HAS_X_PRIO_TWO" "1"; + } elsif eval "xp <= 4" { + let "t.HAS_X_PRIO_THREE" "1"; + } elsif eval "xp >= 5" { + let "t.HAS_X_PRIO_FIVE" "1"; + } +} + +let "unique_header_names" "to_lowercase(header.Content-Type:Content-Transfer-Encoding:Date:From:Sender:Reply-To:To:Cc:Bcc:Message-ID:In-Reply-To:References:Subject[*].raw_name)"; +let "unique_header_names_len" "count(unique_header_names)"; +if eval "unique_header_names_len != count(dedup(unique_header_names))" { + let "t.MULTIPLE_UNIQUE_HEADERS" "1"; +} elsif eval "unique_header_names_len == 0" { + let "t.MISSING_ESSENTIAL_HEADERS" "1"; +} + +# Wrong case X-Mailer +if eval "header.x-mailer.exists && header.x-mailer.raw_name != 'X-Mailer'" { + let "t.XM_CASE" "1"; +} + +# Has organization header +if eval "header.organization:organisation.exists" { + let "t.HAS_ORG_HEADER" "1"; +} + +# Has X-Originating-IP header +if eval "header.X-Originating-IP.exists" { + let "t.HAS_XOIP" "1"; +} + +# Has List-Unsubscribe header +if eval "header.List-Unsubscribe.exists" { + let "t.HAS_LIST_UNSUB" "1"; +} + +# Missing version number in X-Mailer or User-Agent headers +if eval "(header.X-Mailer.exists && !has_digits(header.X-Mailer)) || (header.User-Agent.exists && !has_digits(header.User-Agent))" { + let "t.XM_UA_NO_VERSION" "1"; +} + +# Precedence is bulk +if eval "eq_ignore_case(header.Precedence, 'bulk')" { + let "t.PRECEDENCE_BULK" "1"; +} + +# Upstream SPAM filtering +if eval "contains_ignore_case(header.X-KLMS-AntiSpam-Status, 'spam')" { + # Kaspersky Security for Mail Server says this message is spam + let "t.KLMS_SPAM" "1"; +} +let "spam_hdr" "to_lowercase(header.X-Spam:X-Spam-Flag:X-Spam-Status)"; +if eval "contains(spam_hdr, 'yes') || contains(spam_hdr, 'true') || contains(spam_hdr, 'spam')" { + # Message was already marked as spam + let "t.SPAM_FLAG" "1"; +} +if eval "contains_ignore_case(header.X-UI-Filterresults:X-UI-Out-Filterresults, 'junk')" { + # United Internet says this message is spam + let "t.UNITEDINTERNET_SPAM" "1"; +} + +# Compromised hosts +if eval "header.X-PHP-Originating-Script.exists" { + let "t.HAS_X_POS" "1"; + if eval "contains(header.X-PHP-Originating-Script, 'eval()')" { + let "t.X_PHP_EVAL" "1"; + } + if eval "contains(header.X-PHP-Originating-Script, '../')" { + let "t.HIDDEN_SOURCE_OBJ" "1"; + } +} +if eval "header.X-PHP-Script.exists" { + let "t.HAS_X_PHP_SCRIPT" "1"; + if eval "contains(header.X-PHP-Script, 'eval()')" { + let "t.X_PHP_EVAL" "1"; + } + if eval "contains(header.X-PHP-Script, 'sendmail.php')" { + let "t.PHP_XPS_PATTERN" "1"; + } + if eval "contains(header.X-PHP-Script, '../')" { + let "t.HIDDEN_SOURCE_OBJ" "1"; + } +} +if eval "contains_ignore_case(header.X-Mailer, 'PHPMailer')" { + let "t.HAS_PHPMAILER_SIG" "1"; +} +if eval "header.X-Source:X-Source-Args:X-Source-Dir.exists" { + let "t.HAS_X_SOURCE" "1"; + if eval "contains(header.X-Source-Args, '../')" { + let "t.HIDDEN_SOURCE_OBJ" "1"; + } +} +if eval "contains(header.X-Authenticated-Sender, ': ')" { + let "t.HAS_X_AS" "1"; +} +if eval "contains(header.X-Get-Message-Sender-Via, 'authenticated_id:')" { + let "t.HAS_X_GMSV" "1"; +} +if eval "header.X-AntiAbuse.exists" { + let "t.HAS_X_ANTIABUSE" "1"; +} +if eval "header.X-Authentication-Warning.exists" { + let "t.HAS_XAW" "1"; +} + +# Check for empty delimiters in raw headers +let "raw_headers" "header.from:to:cc:subject:reply-to:date[*].raw"; +let "i" "count(raw_headers)"; +while "i > 0" { + let "i" "i - 1"; + if eval "!starts_with(raw_headers[i], ' ')" { + let "t.HEADER_EMPTY_DELIMITER" "1"; + break; + } +} + + +#### Script bounce.sieve #### + + +if eval "(contains(subject_lc, 'delivery') && + (contains(subject_lc, 'failed') || + contains(subject_lc, 'report') || + contains(subject_lc, 'status') || + contains(subject_lc, 'warning'))) || + (contains(subject_lc, 'failure') && + (contains(subject_lc, 'delivery') || + contains(subject_lc, 'notice') || + contains(subject_lc, 'mail') )) || + (contains(subject_lc, 'delivered') && + (contains(subject_lc, 'couldn\\'t be') || + contains(subject_lc, 'could not be') || + contains(subject_lc, 'hasn\\'t been') || + contains(subject_lc, 'has not been'))) || + contains(subject_lc, 'returned mail') || + contains(subject_lc, 'undeliverable') || + contains(subject_lc, 'undelivered')" { + # Subject contains words or phrases typical for DSN + let "t.SUBJ_BOUNCE_WORDS" "1"; +} + +if eval "is_empty(envelope.from)" { + if eval "eq_ignore_case(header.content-type, 'multipart/report') && + ( eq_ignore_case(header.content-type.attr.report-type, 'delivery-status') || + eq_ignore_case(header.content-type.attr.report-type, 'disposition-notification'))" { + let "t.BOUNCE" "1"; + } else { + let "from" "to_lowercase(header.from)"; + + if eval "contains(from, 'mdaemon') && !is_empty(header.X-MDDSN-Message)" { + let "t.BOUNCE" "1"; + } elsif eval "contains(from, 'postmaster') || contains(from, 'mailer-daemon')" { + if eval "t.SUBJ_BOUNCE_WORDS" { + let "t.BOUNCE" "1"; + } else { + foreverypart { + if eval "(eq_ignore_case(header.content-type.type, 'message') || + eq_ignore_case(header.content-type.type, 'text')) && + (eq_ignore_case(header.content-type.subtype, 'rfc822-headers') || + eq_ignore_case(header.content-type.subtype, 'rfc822'))" { + let "t.BOUNCE" "1"; + break; + } + } + } + } + } +} + + +#### Script html.sieve #### + + +# Message only has text/html MIME parts +if eval "header.content-type == 'text/html'" { + let "t.MIME_HTML_ONLY" "1"; +} + +foreverypart { + if eval "eq_ignore_case(header.content-type, 'text/html')" { + # Tokenize HTML + let "is_body_part" "is_body()"; + let "html_tokens" "tokenize(part.text, 'html')"; + let "html_tokens_len" "len(html_tokens)"; + let "html_char_count" "0"; + let "html_space_count" "0"; + let "html_img_words" "0"; + let "html_words" "0"; + let "has_link_to_img" "0"; + let "has_uri" "0"; + let "has_text" "0"; + let "in_head" "0"; + let "in_body" "0"; + let "in_anchor" "0"; + let "in_anchor_href_ip" "0"; + let "in_anchor_href" ""; + + let "i" "0"; + while "i < html_tokens_len" { + let "token" "html_tokens[i]"; + let "i" "i + 1"; + + # Tokens starting with '_' are text nodes + if eval "starts_with(token, '_')" { + if eval "in_head == 0" { + let "html_char_count" "html_char_count + count_chars(token)"; + let "html_space_count" "html_space_count + count_spaces(token)"; + + let "text" "to_lowercase(trim(strip_prefix(token, '_')))"; + let "html_words" "html_words + len(tokenize(text, 'words'))"; + + let "uris" "tokenize(text, 'uri')"; + + if eval "!is_empty(uris)" { + let "has_uri" "1"; + let "uri" "uris[0]"; + + if eval "in_anchor && !is_empty(in_anchor_href)" { + if eval "contains(text, '://') && + uri_part(uri, 'scheme') != uri_part(in_anchor_href, 'scheme')" { + # The anchor text contains a distinct scheme compared to the target URL + let "t.HTTP_TO_HTTPS" "1"; + } + if eval "(!in_anchor_href_ip && (domain_part(uri_part(uri, 'host'), 'sld') != domain_part(uri_part(in_anchor_href, 'host'), 'sld'))) || + (in_anchor_href_ip && (uri_part(uri, 'host') != uri_part(in_anchor_href, 'host')))" { + let "t.PHISHING" "1"; + } + } + } elsif eval "!is_empty(text)" { + let "has_text" "1"; + } + } + } elsif eval "starts_with(token, '= 210" { + let "has_link_to_img" "1"; + } + if eval "dimensions > 100" { + # We assume that a single picture 100x200 contains approx 3 words of text + let "html_img_words" "html_img_words + dimensions / 100"; + } + + let "img_src" "html_attr(token, 'src')"; + if eval "starts_with(img_src, 'data:') && contains(img_src, ';base64,')" { + # Has Data URI encoding + let "t.HAS_DATA_URI" "1"; + } + } + } elsif eval "starts_with(token, '= 2048) && + (html_img_words / (html_words + html_img_words) > 0.5)" { + # Message contains more images than text + let "t.HTML_TEXT_IMG_RATIO" "1"; + } + + if eval "has_uri && !has_text" { + let "t.BODY_URI_ONLY" "1"; + } + } + } +} + + + +#### Script mime.sieve #### + +if eval "!header.mime-version.exists" { + if eval "header.content-type.exists || header.content-transfer-encoding.exists" { + let "t.MISSING_MIME_VERSION" "1"; + } +} elsif eval "header.mime-version.raw_name != 'MIME-Version'" { + let "t.MV_CASE" "1"; +} + +let "has_text_part" "0"; +let "is_encrypted" "0"; +let "parts_num" "0"; +let "parts_max_len" "0"; + +if eval "header.Content-Type.exists && !header.Content-Disposition:Content-Transfer-Encoding:MIME-Version.exists && !eq_ignore_case(header.Content-Type, 'text/plain')" { + # Only Content-Type header without other MIME headers + let "t.MIME_HEADER_CTYPE_ONLY" "1"; +} + +foreverypart { + let "content_type" "to_lowercase(header.content-type)"; + let "type" "to_lowercase(header.content-type.type)"; + let "subtype" "to_lowercase(header.content-type.subtype)"; + let "cte" "header.content-transfer-encoding"; + let "part_is_attachment" "is_attachment()"; + + if eval "cte != '' && !is_lowercase(cte)" { + let "cte" "to_lowercase(cte)"; + let "t.CTE_CASE" "1"; + } + + if eval "ends_with(header.content-type.raw, ';')" { + # Content-Type header ends with a semi-colon + let "t.CT_EXTRA_SEMI" "1"; + } + + if eval "type == 'multipart'" { + if eval "subtype == 'alternative'" { + let "has_plain_part" "0"; + let "has_html_part" "0"; + + let "text_part_words" ""; + let "text_part_uris" "0"; + + let "html_part_words" ""; + let "html_part_uris" "0"; + + foreverypart { + let "ma_ct" "to_lowercase(header.content-type)"; + + if eval "!has_plain_part && ma_ct == 'text/plain'" { + let "text_part" "part.text"; + let "text_part_words" "tokenize(text_part, 'words')"; + let "text_part_uris" "count(tokenize(text_part, 'uri_strict'))"; + let "has_plain_part" "1"; + } elsif eval "!has_html_part && ma_ct == 'text/html'" { + let "html_part" "html_to_text(part.text)"; + let "html_part_words" "tokenize(html_part, 'words')"; + let "html_part_uris" "count(tokenize(html_part, 'uri_strict'))"; + let "has_html_part" "1"; + } + } + + # Multipart message mostly text/html MIME + if eval "has_html_part" { + if eval "!has_plain_part" { + let "t.MIME_MA_MISSING_TEXT" "1"; + } + } elsif eval "has_plain_part" { + let "t.MIME_MA_MISSING_HTML" "1"; + } + + # HTML and text parts are different + if eval "!t.R_PARTS_DIFFER && has_html_part && has_plain_part && + (!is_empty(text_part_words) || !is_empty(html_part_words)) && + cosine_similarity(text_part_words, html_part_words) < 0.95" { + let "t.R_PARTS_DIFFER" "1"; + } + + # Odd URI count between parts + if eval "text_part_uris != html_part_uris" { + set "t.URI_COUNT_ODD" "1"; + } + } elsif eval "subtype == 'mixed'" { + let "num_text_parts" "0"; + let "has_other_part" "0"; + + foreverypart { + if eval "eq_ignore_case(header.content-type.type, 'text') && !is_attachment()" { + let "num_text_parts" "num_text_parts + 1"; + } elsif eval "!eq_ignore_case(header.content-type.type, 'multipart')" { + let "has_other_part" "1"; + } + } + + # Found multipart/mixed without non-textual part + if eval "!has_other_part && num_text_parts < 3" { + let "t.CTYPE_MIXED_BOGUS" "1"; + } + } elsif eval "subtype == 'encrypted'" { + set "is_encrypted" "1"; + } + } else { + if eval "type == 'text'" { + # MIME text part claims to be ASCII but isn't + if eval "cte == '' || cte == '7bit'" { + if eval "!is_ascii(part.raw)" { + let "t.R_BAD_CTE_7BIT" "1"; + } + } else { + if eval "cte == 'base64'" { + if eval "is_ascii(part.text)" { + # Has text part encoded in base64 that does not contain any 8bit characters + let "t.MIME_BASE64_TEXT_BOGUS" "1"; + } else { + # Has text part encoded in base64 + let "t.MIME_BASE64_TEXT" "1"; + } + } + + if eval "subtype == 'plain' && is_empty(header.content-type.attr.charset)" { + # Charset header is missing + let "t.R_MISSING_CHARSET" "1"; + } + } + let "has_text_part" "1"; + } elsif eval "type == 'application'" { + if eval "subtype == 'pkcs7-mime'" { + let "t.ENCRYPTED_SMIME" "1"; + let "part_is_attachment" "0"; + } elsif eval "subtype == 'pkcs7-signature'" { + let "t.SIGNED_SMIME" "1"; + let "part_is_attachment" "0"; + } elsif eval "subtype == 'pgp-encrypted'" { + let "t.ENCRYPTED_PGP" "1"; + let "part_is_attachment" "0"; + } elsif eval "subtype == 'pgp-signature'" { + let "t.SIGNED_PGP" "1"; + let "part_is_attachment" "0"; + } elsif eval "subtype == 'octet-stream'" { + if eval "!is_encrypted && + !header.content-id.exists && + (!header.content-disposition.exists || + (!eq_ignore_case(header.content-disposition.type, 'attachment') && + is_empty(header.content-disposition.attr.filename)))" { + let "t.CTYPE_MISSING_DISPOSITION" "1"; + } + } + } + + # Increase part count + let "parts_num" "parts_num + 1"; + if eval "parts_num == 1" { + let "parts_len" "mime_part_len()"; + if eval "parts_len > parts_max_len" { + let "parts_max_len" "parts_len"; + } + } + } + + if eval "is_empty(type) && header.content-type.exists" { + let "t.BROKEN_CONTENT_TYPE" "1"; + } + + if eval "part_is_attachment" { + # Has a MIME attachment + let "t.HAS_ATTACHMENT" "1"; + + # Detect and compare mime type + let "detected_mime_type" "detect_file_type('mime')"; + if eval "!is_empty(detected_mime_type)" { + if eval "detected_mime_type == content_type" { + # Known content-type + let "t.MIME_GOOD" "1"; + } elsif eval "content_type != 'application/octet-stream'" { + # Known bad content-type + let "t.MIME_BAD" "1"; + } + } + } + + # Analyze attachment name + let "attach_name" "attachment_name()"; + if eval "!is_empty(attach_name)" { + if eval "has_obscured(attach_name)" { + let "t.MIME_BAD_UNICODE" "1"; + } + let "name_parts" "rsplit(to_lowercase(attach_name), '.')"; + if eval "count(name_parts) > 1" { + let "ext_type" "key_get('spam-mime', name_parts[0])"; + if eval "!is_empty(ext_type)" { + let "ext_type_double" "key_get('spam-mime', name_parts[1])"; + if eval "contains(ext_type, 'BAD')" { + # Bad extension + if eval "contains(ext_type_double, 'BAD')" { + let "t.MIME_DOUBLE_BAD_EXTENSION" "1"; + } else { + let "t.MIME_BAD_EXTENSION" "1"; + } + } + if eval "contains(ext_type, 'AR') && contains(ext_type_double, 'AR')" { + # Archive in archive + let "t.MIME_ARCHIVE_IN_ARCHIVE" "1"; + } + + if eval "contains(ext_type, '/') && + content_type != 'application/octet-stream' && + !contains(split(ext_type, '|'), content_type)" { + # Invalid attachment mime type + let "t.MIME_BAD_ATTACHMENT" "1"; + } + } + } + } + +} + +# Message contains both text and encrypted parts +if eval "has_text_part && (t.ENCRYPTED_SMIME || t.SIGNED_SMIME || t.ENCRYPTED_PGP || t.SIGNED_PGP)" { + let "t.BOGUS_ENCRYPTED_AND_TEXT" "1"; +} + +# Message contains only one short part +if eval "parts_num == 1 && parts_max_len < 64" { + let "t.SINGLE_SHORT_PART" "1"; +} elsif eval "parts_max_len == 0" { + let "t.COMPLETELY_EMPTY" "1"; +} + +# Check for mixed script in body +if eval "!is_single_script(text_body)" { + let "t.R_MIXED_CHARSET" "1"; +} + + +#### Script dmarc.sieve #### + +if eval "env.spf.result == 'pass'" { + let "t.SPF_ALLOW" "1"; +} elsif eval "env.spf.result == 'fail'" { + let "t.SPF_FAIL" "1"; +} elsif eval "env.spf.result == 'softfail'" { + let "t.SPF_SOFTFAIL" "1"; +} elsif eval "env.spf.result == 'neutral'" { + let "t.SPF_NEUTRAL" "1"; +} elsif eval "env.spf.result == 'temperror'" { + let "t.SPF_DNSFAIL" "1"; +} elsif eval "env.spf.result == 'permerror'" { + let "t.SPF_PERMFAIL" "1"; +} else { + let "t.SPF_NA" "1"; +} + +if eval "env.dkim.result == 'pass'" { + let "t.DKIM_ALLOW" "1"; +} elsif eval "env.dkim.result == 'fail'" { + let "t.DKIM_REJECT" "1"; +} elsif eval "env.dkim.result == 'temperror'" { + let "t.DKIM_TEMPFAIL" "1"; +} elsif eval "env.dkim.result == 'permerror'" { + let "t.DKIM_PERMFAIL" "1"; +} else { + let "t.DKIM_NA" "1"; +} + +if eval "env.arc.result == 'pass'" { + let "t.ARC_ALLOW" "1"; +} elsif eval "env.arc.result == 'fail'" { + let "t.ARC_REJECT" "1"; +} elsif eval "env.arc.result == 'temperror'" { + let "t.ARC_DNSFAIL" "1"; +} elsif eval "env.arc.result == 'permerror'" { + let "t.ARC_INVALID" "1"; +} else { + let "t.ARC_NA" "1"; +} + +if eval "env.dmarc.result == 'pass'" { + let "t.DMARC_POLICY_ALLOW" "1"; +} elsif eval "env.dmarc.result == 'temperror'" { + let "t.DMARC_DNSFAIL" "1"; +} elsif eval "env.dmarc.result == 'permerror'" { + let "t.DMARC_BAD_POLICY" "1"; +} elsif eval "env.dmarc.result == 'fail'" { + if eval "env.dmarc.policy == 'quarantine'" { + let "t.DMARC_POLICY_QUARANTINE" "1"; + } elsif eval "env.dmarc.policy == 'reject'" { + let "t.DMARC_POLICY_REJECT" "1"; + } else { + let "t.DMARC_POLICY_SOFTFAIL" "1"; + } +} else { + let "t.DMARC_NA" "1"; +} + +if eval "header.DKIM-Signature.exists" { + let "t.DKIM_SIGNED" "1"; + if eval "header.ARC-Seal.exists" { + let "t.ARC_SIGNED" "1"; + } +} + +# Check allowlists +if eval "key_exists('spam-dmarc', from_domain)" { + if eval "t.DMARC_POLICY_ALLOW" { + let "t.ALLOWLIST_DMARC" "1"; + } else { + let "t.BLOCKLIST_DMARC" "1"; + } +} elsif eval "key_exists('spam-spdk', from_domain)" { + let "is_dkim_pass" "contains(env.dkim.domains, from_domain) || t.ARC_ALLOW"; + + if eval "is_dkim_pass && t.SPF_ALLOW" { + let "t.ALLOWLIST_SPF_DKIM" "1"; + } elsif eval "is_dkim_pass" { + let "t.ALLOWLIST_DKIM" "1"; + if eval "!t.SPF_DNSFAIL" { + let "t.BLOCKLIST_SPF" "1"; + } + } elsif eval "t.SPF_ALLOW" { + let "t.ALLOWLIST_SPF" "1"; + if eval "!t.DKIM_TEMPFAIL" { + let "t.BLOCKLIST_DKIM" "1"; + } + } elsif eval "!t.SPF_DNSFAIL && !t.DKIM_TEMPFAIL" { + let "t.BLOCKLIST_SPF_DKIM" "1"; + } +} + + +#### Script ip.sieve #### + +# Reverse ip checks +if eval "env.iprev.result != ''" { + if eval "env.iprev.result == 'temperror'" { + let "t.RDNS_DNSFAIL" "1"; + } elsif eval "env.iprev.result == 'fail' || env.iprev.result == 'permerror'" { + let "t.RDNS_NONE" "1"; + } +} + + +#### Script helo.sieve #### + +if eval "!is_ip_addr(env.helo_domain)" { + let "helo" "env.helo_domain"; + + if eval "contains(helo, '.')" { + if eval "!is_empty(env.iprev.ptr) && !eq_ignore_case(helo, env.iprev.ptr)" { + # Helo does not match reverse IP + let "t.HELO_IPREV_MISMATCH" "1"; + } + if eval "!dns_exists(helo, 'ip') && !dns_exists(helo, 'mx')" { + # Helo no resolve to A or MX + let "t.HELO_NORES_A_OR_MX" "1"; + } + } else { + if eval "contains(helo, 'user')" { + # HELO contains 'user' + let "t.RCVD_HELO_USER" "1"; + } + + # Helo not FQDN + let "t.HELO_NOT_FQDN" "1"; + } +} else { + # Helo host is bare ip + let "t.HELO_BAREIP" "1"; + + if eval "env.helo_domain != env.remote_ip" { + # Helo A IP != hostname IP + let "t.HELO_IP_A" "1"; + } +} + + +#### Script replies_in.sieve #### + + +let "message_ids" "header.In-Reply-To:References"; + +let "i" "count(message_ids)"; +while "i > 0" { + let "i" "i - 1"; + + if eval "key_exists(SPAM_DB, 'm:' + message_ids[i])" { + let "t.TRUSTED_REPLY" "1"; + break; + } +} + + +#### Script spamtrap.sieve #### + + +# Check if the message was sent to a spam trap address +if eval "AUTOLEARN_ENABLE && key_exists('spam-trap', envelope.to)" { + eval "bayes_is_balanced(SPAM_DB, false, AUTOLEARN_SPAM_HAM_BALANCE) && bayes_train(SPAM_DB, body_and_subject, true)"; + let "t.SPAM_TRAP" "1"; + + # Disable autolearn so the classifier is not trained twice + let "AUTOLEARN_ENABLE" "0"; +} + + +#### Script bayes_classify.sieve #### + +if eval "!t.SPAM_TRAP && !t.TRUSTED_REPLY" { + + # Classification parameters + # min_token_hits: 2 + # min_tokens: 11 + # min_prob_strength: 0.05 + # min_learns: 200 + + let "bayes_result" "bayes_classify(SPAM_DB, body_and_subject, [2, 11, 0.05, 200])"; + if eval "!is_empty(bayes_result)" { + if eval "bayes_result > 0.7" { + let "t.BAYES_SPAM" "1"; + } elsif eval "bayes_result < 0.5" { + let "t.BAYES_HAM" "1"; + } + } +} + + +#### Script url.sieve #### + +if eval "(count(body_urls) == 1 || count(html_body_urls) == 1) && count(tokenize(text_body, 'words')) == 0" { + let "t.URL_ONLY" "1"; +} + +if eval "has_zwsp(urls)" { + let "t.ZERO_WIDTH_SPACE_URL" "1"; +} elsif eval "has_obscured(urls)" { + let "t.R_SUSPICIOUS_URL" "1"; +} + +let "i" "count(urls)"; +while "i > 0" { + let "i" "i - 1"; + let "url" "urls[i]"; + + # Skip non-URLs such as 'data:' and 'mailto:' + if eval "!contains(url, '://')" { + continue; + } + + let "host" "uri_part(url, 'host')"; + + if eval "!is_empty(host)" { + let "is_ip" "is_ip_addr(host)"; + let "host" "puny_decode(host)"; + let "host_lc" "to_lowercase(host)"; + let "host_sld" "domain_part(host_lc, 'sld')"; + + # Skip local and trusted domains + if eval "is_local_domain(DOMAIN_DIRECTORY, host_sld) || key_exists('spam-allow', host_sld)" { + continue; + } + + if eval "!is_ip && + (!t.REDIRECTOR_URL || !t.URL_REDIRECTOR_NESTED) && + key_exists('spam-redirect', host_sld)" { + let "t.REDIRECTOR_URL" "1"; + let "redir_count" "1"; + + while "redir_count <= 5" { + # Use a custom user-agent and a 3 second timeout + let "url_redirect" "http_header(url, 'Location', 'Mozilla/5.0 (X11; Linux i686; rv:109.0) Gecko/20100101 Firefox/118.0', 3000)"; + if eval "!is_empty(url_redirect)" { + let "url" "url_redirect"; + let "host" "uri_part(url, 'host')"; + let "is_ip" "is_ip_addr(host)"; + let "host" "puny_decode(host)"; + let "host_lc" "to_lowercase(host)"; + let "host_sld" "domain_part(host_lc, 'sld')"; + + if eval "!is_ip && key_exists('spam-redirect', host_sld)" { + let "redir_count" "redir_count + 1"; + } else { + break; + } + } else { + break; + } + } + + if eval "redir_count > 5" { + let "t.URL_REDIRECTOR_NESTED" "1"; + } + } + + let "url_lc" "to_lowercase(url)"; + let "query" "uri_part(url_lc, 'path_query')"; + if eval "!is_ip" { + if eval "!is_ascii(host)" { + let "host_cured" "cure_text(host)"; + if eval "host_lc != host_cured && dns_exists(host_cured, 'ip')" { + let "t.HOMOGRAPH_URL" "1"; + } + + if eval "!is_single_script(host)" { + let "t.MIXED_CHARSET_URL" "1"; + } + } else { + if eval "ends_with(host, 'googleusercontent.com') && starts_with(query, '/proxy/')" { + let "t.HAS_GUC_PROXY_URI" "1"; + } elsif eval "ends_with(host, 'firebasestorage.googleapis.com')" { + let "t.HAS_GOOGLE_FIREBASE_URL" "1"; + } elsif eval "starts_with(domain_part(host, 'sld'), 'google.') && contains(query, 'url?') " { + let "t.HAS_GOOGLE_REDIR" "1"; + } + } + + if eval "(contains(host_lc, 'ipfs.') || contains(query, '/ipfs')) && contains(query, '/qm')" { + # InterPlanetary File System (IPFS) gateway URL, likely malicious + let "t.HAS_IPFS_GATEWAY_URL" "1"; + } elsif eval "ends_with(host_lc, '.onion')" { + let "t.HAS_ONION_URI" "1"; + } + } else { + # URL is an ip address + let "t.R_SUSPICIOUS_URL" "1"; + } + + if eval "starts_with(query, '/wp-')" { + # Contains WordPress URIs + let "t.HAS_WP_URI" "1"; + if eval "starts_with(query, '/wp-content') | starts_with(query, '/wp-includes')" { + # URL that is pointing to a compromised WordPress installation + let "t.WP_COMPROMISED" "1"; + } + } + if eval "contains(query, '/../') && !contains(query, '/well-known') && !contains(query, '/well_known')" { + # Message contains URI with a hidden path + let "t.URI_HIDDEN_PATH" "1"; + } + + # Phishing checks (refresh OpenPhish every 12 hours, PhishTank every 6 hours) + if eval "key_exists_http('https://openphish.com/feed.txt', url, [43200, 'list'])" { + let "t.PHISHED_OPENPHISH" "1"; + } + if eval "key_exists_http('http://data.phishtank.com/data/online-valid.csv', url, [21600, 'csv', 1, ',', true])" { + let "t.PHISHED_PHISHTANK" "1"; + } + + } else { + # URL could not be parsed + let "t.R_SUSPICIOUS_URL" "1"; + } +} + + + +#### Script rbl.sieve #### + + +# Validate IP addresses +let "ip_addresses" "dedup(winnow([ env.remote_ip ] + header.received[*].rcvd.ip + header.received[*].rcvd.from.ip + header.received[*].rcvd.by.ip))"; +let "ip_addresses_len" "count(ip_addresses)"; +let "i" "0"; + +while "i < ip_addresses_len" { + let "ip_address" "ip_addresses[i]"; + let "is_from_addr" "i == 0"; + let "i" "i + 1"; + + if eval "ip_address == '127.0.0.1' || ip_address == '::1'" { + continue; + } + + # Do not check more than 10 IP addresses + if eval "i >= 10" { + break; + } + + let "ip_reverse" "ip_reverse_name(ip_address)"; + let "is_ip_v4" "len(ip_reverse) <= 15"; + + # Query SPAMHAUS + let "result" "rsplit_once(dns_query(ip_reverse + '.zen.spamhaus.org', 'ipv4')[0], '.')"; + if eval "result[0] == '127.0.0'" { + let "result" "result[1]"; + + if eval "result == 2" { + if eval "is_from_addr" { + let "t.RBL_SPAMHAUS_SBL" "1"; + } else { + let "t.RECEIVED_SPAMHAUS_SBL" "1"; + } + } elsif eval "result == 3" { + if eval "is_from_addr" { + let "t.RBL_SPAMHAUS_CSS" "1"; + } else { + let "t.RECEIVED_SPAMHAUS_CSS" "1"; + } + } elsif eval "result >= 4 && result <= 7" { + if eval "is_from_addr" { + let "t.RBL_SPAMHAUS_XBL" "1"; + } else { + let "t.RECEIVED_SPAMHAUS_XBL" "1"; + } + } elsif eval "result == 9" { + if eval "is_from_addr" { + let "t.RBL_SPAMHAUS_DROP" "1"; + } else { + let "t.RECEIVED_SPAMHAUS_PBL" "1"; + } + } elsif eval "result == 10 || result == 11" { + if eval "is_from_addr" { + let "t.RBL_SPAMHAUS_PBL" "1"; + } else { + let "t.RECEIVED_SPAMHAUS_PBL" "1"; + } + } elsif eval "result == 254" { + if eval "is_from_addr" { + let "t.RBL_SPAMHAUS_BLOCKED_OPENRESOLVER" "1"; + } else { + let "t.RECEIVED_SPAMHAUS_BLOCKED_OPENRESOLVER" "1"; + } + } elsif eval "result == 255" { + if eval "is_from_addr" { + let "t.RBL_SPAMHAUS_BLOCKED" "1"; + } else { + let "t.RECEIVED_SPAMHAUS_BLOCKED" "1"; + } + } else { + # Unrecognized result + let "t.RBL_SPAMHAUS" "1"; + } + } + + if eval "is_from_addr" { + # Query IP reputation at Mailspike + let "result" "rsplit_once(dns_query(ip_reverse + '.rep.mailspike.net', 'ipv4')[0], '.')"; + if eval "result[0] == '127.0.0'" { + let "result" "result[1]"; + + if eval "result == 10" { + let "t.RBL_MAILSPIKE_WORST" "1"; + } elsif eval "result == 11" { + let "t.RBL_MAILSPIKE_VERYBAD" "1"; + } elsif eval "result == 12" { + let "t.RBL_MAILSPIKE_BAD" "1"; + } elsif eval "result >= 13 && result <= 16" { + let "t.RWL_MAILSPIKE_NEUTRAL" "1"; + } elsif eval "result == 17" { + let "t.RWL_MAILSPIKE_POSSIBLE" "1"; + } elsif eval "result == 18" { + let "t.RWL_MAILSPIKE_GOOD" "1"; + } elsif eval "result == 19" { + let "t.RWL_MAILSPIKE_VERYGOOD" "1"; + } elsif eval "result == 20" { + let "t.RWL_MAILSPIKE_EXCELLENT" "1"; + } + } + + # Query SenderScore + if eval "dns_exists(ip_reverse + '.bl.score.senderscore.com', 'ipv4')" { + let "t.RBL_SENDERSCORE" "1"; + } + + # Query SpamEatingMonkey + if eval "is_ip_v4 && dns_exists(ip_reverse + '.bl.spameatingmonkey.net', 'ipv4')" { + let "t.RBL_SEM" "1"; + } elsif eval "!is_ip_v4 && dns_exists(ip_reverse + '.bl.ipv6.spameatingmonkey.net', 'ipv4')" { + let "t.RBL_SEM_IPV6" "1"; + } + + # Query VirusFree + if eval "dns_query(ip_reverse + '.bip.virusfree.cz', 'ipv4')[0] == '127.0.0.2'" { + let "t.RBL_VIRUSFREE_BOTNET" "1"; + } + + # Query NiX + if eval "dns_exists(ip_reverse + '.ix.dnsbl.manitu.net', 'ipv4')" { + let "t.RBL_NIXSPAM" "1"; + } + + # Query Spamcop + if eval "dns_exists(ip_reverse + '.bl.spamcop.net', 'ipv4')" { + let "t.RBL_SPAMCOP" "1"; + } + + # Query Barracuda + if eval "dns_exists(ip_reverse + '.b.barracudacentral.org', 'ipv4')" { + let "t.RBL_BARRACUDA" "1"; + } + } + + # Query Blocklist.de + if eval "dns_exists(ip_reverse + '.bl.blocklist.de', 'ipv4')" { + if eval "is_from_addr" { + let "t.RBL_BLOCKLISTDE" "1"; + } else { + let "t.RECEIVED_BLOCKLISTDE" "1"; + } + } + + # Query DNSWL + let "result" "rsplit_once(dns_query(ip_reverse + '.list.dnswl.org', 'ipv4')[0], '.')"; + if eval "starts_with(result[0], '127.')" { + let "result" "result[1]"; + + if eval "result == 0" { + let "t.RCVD_IN_DNSWL_NONE" "1"; + } elsif eval "result == 1" { + let "t.RCVD_IN_DNSWL_LOW" "1"; + } elsif eval "result == 2" { + let "t.RCVD_IN_DNSWL_MED" "1"; + } elsif eval "result == 3" { + let "t.RCVD_IN_DNSWL_HI" "1"; + } elsif eval "result == 255" { + let "t.DNSWL_BLOCKED" "1"; + } + } +} + +# Validate domain names +let "emails" "dedup(winnow(to_lowercase([from_addr, rto_addr, envelope.from] + tokenize(text_body, 'email'))))"; +let "emails_len" "count(emails)"; +let "domains" "dedup(winnow(to_lowercase([ env.helo_domain, env.iprev.ptr ] + email_part(emails, 'domain') + puny_decode(uri_part(urls, 'host')))))"; +let "domains_len" "count(domains)"; +let "i" "0"; + +while "i < domains_len" { + let "domain" "domains[i]"; + let "i" "i + 1"; + + # Skip invalid and local domain names + if eval "!contains(domain, '.') || + is_ip_addr(domain) || + is_local_domain(DOMAIN_DIRECTORY, domain_part(domain, 'sld')) || + key_exists('spam-allow', domain)" { + continue; + } + + # Do not check more than 10 domain names + if eval "i >= 10" { + break; + } + + # Query SpamHaus DBL + let "result" "rsplit_once(dns_query(domain + '.dbl.spamhaus.org', 'ipv4')[0], '.')"; + if eval "result[0] == '127.0.1'" { + let "result" "result[1]"; + + if eval "result == 2" { + let "t.DBL_SPAM" "1"; + } elsif eval "result == 4" { + let "t.DBL_PHISH" "1"; + } elsif eval "result == 5" { + let "t.DBL_MALWARE" "1"; + } elsif eval "result == 6" { + let "t.DBL_BOTNET" "1"; + } elsif eval "result == 102" { + let "t.DBL_ABUSE" "1"; + } elsif eval "result == 103" { + let "t.DBL_ABUSE_REDIR" "1"; + } elsif eval "result == 104" { + let "t.DBL_ABUSE_PHISH" "1"; + } elsif eval "result == 105" { + let "t.DBL_ABUSE_MALWARE" "1"; + } elsif eval "result == 106" { + let "t.DBL_ABUSE_BOTNET" "1"; + } elsif eval "result == 254" { + let "t.DBL_BLOCKED_OPENRESOLVER" "1"; + } elsif eval "result == 255" { + let "t.DBL_BLOCKED" "1"; + } + } + + # Query SURBL multi + let "result" "rsplit_once(dns_query(domain + '.multi.surbl.org', 'ipv4')[0], '.')"; + if eval "result[0] == '127.0.0'" { + let "result" "result[1]"; + + if eval "result == 128" { + let "t.CRACKED_SURBL" "1"; + } elsif eval "result == 64" { + let "t.ABUSE_SURBL" "1"; + } elsif eval "result == 16" { + let "t.MW_SURBL_MULTI" "1"; + } elsif eval "result == 8" { + let "t.PH_SURBL_MULTI" "1"; + } elsif eval "result == 1" { + let "t.SURBL_BLOCKED" "1"; + } + } + + # Query URIBL multi + let "result" "rsplit_once(dns_query(domain + '.multi.uribl.com', 'ipv4')[0], '.')"; + if eval "result[0] == '127.0.0'" { + let "result" "result[1]"; + + if eval "result == 1" { + let "t.URIBL_BLOCKED" "1"; + } elsif eval "result == 2" { + let "t.URIBL_BLACK" "1"; + } elsif eval "result == 4" { + let "t.URIBL_GREY" "1"; + } elsif eval "result == 8" { + let "t.URIBL_RED" "1"; + } + } + + # Query SpamEatingMonkey URIBL + if eval "dns_query(domain + '.uribl.spameatingmonkey.net', 'ipv4')[0] == '127.0.0.2'" { + let "t.SEM_URIBL" "1"; + } + + # Query SpamEatingMonkey FRESH15 + if eval "dns_query(domain + '.fresh15.spameatingmonkey.net', 'ipv4')[0] == '127.0.0.2'" { + let "t.SEM_URIBL_FRESH15" "1"; + } + +} + +# Check DKIM domains that passed validation +let "i" "count(env.dkim.domains)"; +while "i > 0" { + let "i" "i - 1"; + + # Query DNSWL + let "result" "rsplit_once(dns_query(env.dkim.domains[i] + '.dwl.dnswl.org', 'ipv4')[0], '.')"; + if eval "starts_with(result[0], '127.')" { + let "result" "result[1]"; + + if eval "result == 0" { + let "t.DWL_DNSWL_NONE" "1"; + } elsif eval "result == 1" { + let "t.DWL_DNSWL_LOW" "1"; + } elsif eval "result == 2" { + let "t.DWL_DNSWL_MED" "1"; + } elsif eval "result == 3" { + let "t.DWL_DNSWL_HI" "1"; + } elsif eval "result == 255" { + let "t.DWL_DNSWL_BLOCKED" "1"; + } + } +} + +# Validate email addresses +let "i" "0"; +while "i < emails_len" { + let "email" "emails[i]"; + let "i" "i + 1"; + + # Skip invalid and local e-mail addresses + if eval "!contains(email, '@') || is_local_domain(DOMAIN_DIRECTORY, domain_part(email_part(email, 'domain'), 'sld'))" { + continue; + } + + # Do not check more than 10 email addresses + if eval "i >= 10" { + break; + } + + # Query MSBL EBL + let "result" "rsplit_once(dns_query(hash(email, 'sha1') + '.ebl.msbl.org', 'ipv4')[0], '.')"; + if eval "result[1] == 2 || result[1] == 3" { + if eval "result[0] == '127.0.0'" { + let "t.MSBL_EBL" "1"; + } elsif eval "result[0] == '127.0.1'" { + let "t.MSBL_EBL_GREY" "1"; + } + } +} + + +# Validate URL hashes +let "i" "0"; +let "urls_len" "count(urls)"; +while "i < urls_len" { + let "url" "urls[i]"; + let "i" "i + 1"; + + # Do not check more than 10 URLs + if eval "i >= 10" { + break; + } + + # Skip URLs pointing to local or trusted domains + let "domain" "domain_part(uri_part(url, 'host'), 'sld')"; + if eval "is_local_domain(DOMAIN_DIRECTORY, domain) || + key_exists('spam-allow', domain)" { + continue; + } + + # Query SURBL HASHBL + let "result" "rsplit_once(dns_query(hash(url, 'md5') + '.hashbl.surbl.org', 'ipv4')[0], '.')"; + if eval "starts_with(result[0], '127.0.')" { + let "result" "result[1]"; + + if eval "result == 8" { + let "t.SURBL_HASHBL_PHISH" "1"; + } elsif eval "result == 16" { + let "t.SURBL_HASHBL_MALWARE" "1"; + } elsif eval "result == 64" { + let "t.SURBL_HASHBL_ABUSE" "1"; + } elsif eval "result == 128" { + let "t.SURBL_HASHBL_CRACKED" "1"; + } elsif eval "result[0] == '127.0.1'" { + let "t.SURBL_HASHBL_EMAIL" "1"; + } + } +} + + +#### Script pyzor.sieve #### + +# Check message hash against Pyzor on public.pyzor.org:24441 using a 5 second timeout +let "pyzor_response" "pyzor_check('public.pyzor.org:24441', 5)"; + +if eval "!is_empty(pyzor_response) && pyzor_response[0] == 200" { + let "count" "pyzor_response[1]"; + let "wl_count" "pyzor_response[2]"; + + if eval "count > 5 && (wl_count < 10 || wl_count / count < 0.2)" { + let "t.PYZOR" "1"; + } +} + + +#### Script composites.sieve #### + +if eval "t.MISSING_ESSENTIAL_HEADERS && t.SINGLE_SHORT_PART" { + let "t.SHORT_PART_BAD_HEADERS" "1"; +} + +if eval "t.FORGED_RECIPIENTS && t.MAILLIST" { + let "t.FORGED_RECIPIENTS_MAILLIST" "1"; +} + +if eval "t.FORGED_SENDER && t.MAILLIST" { + let "t.FORGED_SENDER_MAILLIST" "1"; +} + +if eval "t.DMARC_POLICY_ALLOW && (t.SPF_SOFTFAIL || t.SPF_FAIL || t.DKIM_REJECT)" { + let "t.DMARC_POLICY_ALLOW_WITH_FAILURES" "1"; +} + +if eval "t.DKIM_NA && t.SPF_NA && t.DMARC_NA && t.ARC_NA" { + let "t.AUTH_NA" "1"; +} + +if eval "!(t.DKIM_NA && t.SPF_NA && t.DMARC_NA && t.ARC_NA) && (t.DKIM_NA || t.DKIM_TEMPFAIL || t.DKIM_PERMFAIL) && (t.SPF_NA || t.SPF_DNSFAIL) && t.DMARC_NA && (t.ARC_NA || t.ARC_DNSFAIL)" { + let "t.AUTH_NA_OR_FAIL" "1"; +} + +if eval "(t.AUTH_NA || t.AUTH_NA_OR_FAIL) && (t.BOUNCE || t.SUBJ_BOUNCE_WORDS)" { + let "t.BOUNCE_NO_AUTH" "1"; +} + +if eval "(t.HAS_X_POS || t.HAS_PHPMAILER_SIG) && t.HAS_WP_URI && (t.PHISHING || t.CRACKED_SURBL || t.PH_SURBL_MULTI || t.DBL_PHISH || t.DBL_ABUSE_PHISH || t.URIBL_BLACK || t.PHISHED_OPENPHISH || t.PHISHED_PHISHTANK)" { + let "t.HACKED_WP_PHISHING" "1"; +} + +if eval "(t.HAS_XOIP || t.RCVD_FROM_SMTP_AUTH) && t.DCC_BULK" { + let "t.COMPROMISED_ACCT_BULK" "1"; +} + +if eval "t.DCC_BULK && (t.MISSING_TO || t.R_UNDISC_RCPT)" { + let "t.UNDISC_RCPTS_BULK" "1"; +} + +if eval "t.RECEIVED_SPAMHAUS_PBL && !t.RCVD_VIA_SMTP_AUTH" { + let "t.RCVD_UNAUTH_PBL" "1"; +} + +if eval "(t.DKIM_ALLOW || t.ARC_ALLOW) && t.RCVD_IN_DNSWL_MED" { + let "t.RCVD_DKIM_ARC_DNSWL_MED" "1"; +} + +if eval "(t.DKIM_ALLOW || t.ARC_ALLOW) && t.RCVD_IN_DNSWL_HI" { + let "t.RCVD_DKIM_ARC_DNSWL_HI" "1"; +} + +if eval "(t.HAS_X_POS || t.HAS_PHPMAILER_SIG || t.HAS_X_PHP_SCRIPT) && (t.SUBJECT_ENDS_QUESTION || t.SUBJECT_ENDS_EXCLAIM || t.MANY_INVISIBLE_PARTS)" { + let "t.AUTOGEN_PHP_SPAMMY" "1"; +} + +if eval "(t.PHISHING || t.DBL_PHISH || t.PHISHED_OPENPHISH || t.PHISHED_PHISHTANK) && (t.SUBJECT_ENDS_QUESTION || t.SUBJECT_ENDS_EXCLAIM)" { + let "t.PHISH_EMOTION" "1"; +} + +if eval "t.HAS_GUC_PROXY_URI || t.URIBL_RED || t.DBL_ABUSE_REDIR || t.HAS_ONION_URI" { + let "t.HAS_ANON_DOMAIN" "1"; +} + +if eval "(t.SPF_FAIL || t.SPF_SOFTFAIL) && (t.RCVD_COUNT_ZERO || t.RCVD_NO_TLS_LAST)" { + let "t.VIOLATED_DIRECT_SPF" "1"; +} + +if eval "(t.FREEMAIL_FROM || t.FREEMAIL_ENVFROM || t.FREEMAIL_REPLYTO) && (t.TO_DN_RECIPIENTS || t.R_UNDISC_RCPT) && (t.FROM_NAME_HAS_TITLE || t.FREEMAIL_REPLYTO_NEQ_FROM_DOM)" { + let "t.FREEMAIL_AFF" "1"; +} + +if eval "t.URL_ONLY && t.REDIRECTOR_URL" { + let "t.REDIRECTOR_URL_ONLY" "1"; +} + +if eval "t.FAKE_REPLY && t.RCVD_VIA_SMTP_AUTH && (!t.RECEIVED_SPAMHAUS_PBL || t.RECEIVED_SPAMHAUS_XBL || t.RECEIVED_SPAMHAUS_SBL)" { + let "t.THREAD_HIJACKING_FROM_INJECTOR" "1"; +} + + +#### Script scores.sieve #### + +# Add scores +let "tags" "var_names()"; +let "i" "count(tags)"; +let "spam_result" ""; +while "i > 0" { + let "i" "i - 1"; + let "tag" "tags[i]"; + let "tag_score" "key_get('spam-scores', tag)"; + + if eval "is_number(tag_score)" { + let "score" "score + tag_score"; + if eval "ADD_HEADER_SPAM_RESULT" { + if eval "!is_empty(spam_result)" { + let "spam_result" "spam_result + ',\r\n\t' + tag + ' (' + tag_score + ')'"; + } else { + let "spam_result" "spam_result + tag + ' (' + tag_score + ')'"; + } + } + } elsif eval "tag_score == 'reject'" { + let "SCORE_REJECT_THRESHOLD" "1"; + let "score" "2"; + break; + } elsif eval "tag_score == 'discard'" { + discard; + stop; + } +} + + +#### Script reputation.sieve #### + +# Obtain sender address and domain +let "rep_from" "envelope.from"; +let "rep_from_domain" "envfrom_domain_sld"; +if eval "is_empty(rep_from)" { + let "rep_from" "from_addr"; + let "rep_from_domain" "from_domain_sld"; +} +if eval "env.dmarc.result != 'pass'" { + # Do not penalize forged domains + let "rep_from" "'_' + rep_from"; + let "rep_from_domain" "'_' + rep_from_domain"; +} + +# Lookup ASN +let "asn" ""; +if eval "len(env.remote_ip.reverse) <= 15" { + let "asn" "env.remote_ip.reverse + '.origin.asn.cymru.com'"; +} else { + let "asn" "env.remote_ip.reverse + '.origin.asn6.cymru.com'"; +} +let "asn" "split(dns_query(asn, 'txt'), '|')[0]"; + +# Generate reputation tokens +let "token_ids" ""; +if eval "asn > 0" { + let "token_ids" "['i:' + env.remote_ip, 'f:' + rep_from, 'd:' + rep_from_domain, 'a:' + asn ]"; +} else { + let "token_ids" "['i:' + env.remote_ip, 'f:' + rep_from, 'd:' + rep_from_domain ]"; +} + +# Lookup reputation +let "i" "len(token_ids)"; +let "reputation" "0.0"; + +while "i > 0" { + let "i" "i - 1"; + let "token_id" "token_ids[i]"; + + # Lookup reputation + let "token_rep" "key_get(SPAM_DB, token_id)"; + + if eval "is_empty(token_rep)" { + # Set reputation + eval "key_set(SPAM_DB, token_id, [score, 1], 2592000)"; + continue; + } + + # Update reputation + let "token_score" "token_rep[0]"; + let "token_count" "token_rep[1]"; + let "updated_score" "(token_count + 1) * (score + 0.98 * token_score) / (0.98 * token_count + 1)"; + eval "key_set(SPAM_DB, token_id, [updated_score, token_count + 1], 2592000)"; + + # Assign weight + let "weight" ""; + if eval "starts_with(token_id, 'f:')" { + # Sender address has 50% weight + let "weight" "0.5"; + } elsif eval "starts_with(token_id, 'd:')" { + # Sender domain has 20% weight + let "weight" "0.2"; + } elsif eval "starts_with(token_id, 'i:')" { + # IP has 20% weight + let "weight" "0.2"; + } elsif eval "starts_with(token_id, 'a:')" { + # ASN has 10% weight + let "weight" "0.1"; + } else { + continue; + } + + let "reputation" "reputation + (token_score / token_count * weight)"; +} + +# Adjust score using a 0.5 factor +if eval "reputation > 0" { + let "score" "score + (reputation - score) * 0.5"; +} + + +#### Script epilogue.sieve #### + + +# Train the bayes classifier automatically +if eval "AUTOLEARN_ENABLE && (score >= AUTOLEARN_SPAM_THRESHOLD || score <= AUTOLEARN_HAM_THRESHOLD)" { + let "is_spam" "score >= AUTOLEARN_SPAM_THRESHOLD"; + eval "bayes_is_balanced(SPAM_DB, is_spam, AUTOLEARN_SPAM_HAM_BALANCE) && + bayes_train(SPAM_DB, body_and_subject, is_spam)"; +} + +# Process score actions +if eval "SCORE_REJECT_THRESHOLD && score >= SCORE_REJECT_THRESHOLD" { + reject "Your message has been rejected because it has an excessive spam score. If you feel this is an error, please contact the postmaster."; + stop; +} elsif eval "SCORE_DISCARD_THRESHOLD && score >= SCORE_DISCARD_THRESHOLD" { + discard; + stop; +} elsif eval "ADD_HEADER_SPAM" { + let "spam_status" ""; + if eval "score >= SCORE_SPAM_THRESHOLD" { + let "spam_status" "'Yes, score=' + score"; + } else { + let "spam_status" "'No, score=' + score"; + } + eval "add_header('X-Spam-Status', spam_status)"; + if eval "!is_empty(spam_result)" { + eval "add_header('X-Spam-Result', spam_result)"; + } +} + + +''' + +[sieve.trusted.scripts.track-replies] +name = "Track Replies" +contents = ''' + +#### Script config.sieve #### + +# Whether to add an X-Spam-Status header +let "ADD_HEADER_SPAM" "key_get('spam-config', 'add-spam')"; + +# Whether to add an X-Spam-Result header +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" "key_get('spam-config', 'learn-ham-replies')"; + +# Whether the bayes classifier should be trained automatically +let "AUTOLEARN_ENABLE" "key_get('spam-config', 'learn-enable')"; + +# When to learn ham (score >= threshold) +let "AUTOLEARN_HAM_THRESHOLD" "key_get('spam-config', 'learn-ham-threshold')"; + +# When to learn spam (score <= 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" "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" "key_get('spam-config', 'threshold-spam')"; + +# Discard messages with a score above this threshold +let "SCORE_DISCARD_THRESHOLD" "key_get('spam-config', 'threshold-discard')"; + +# Reject messages with a score above this threshold +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" "key_get('spam-config', 'directory')"; + +# Store to use for Bayes tokens and ids (leave empty for default) +let "SPAM_DB" "key_get('spam-config', 'lookup')"; + + +#### Script replies_out.sieve #### + + +# This script should be used on authenticated SMTP sessions only +let "message_id" "header.Message-ID"; + +if eval "!is_empty(message_id)" { + # Store the message ID for 30 days + eval "key_set(SPAM_DB, 'm:' + message_id, '', 2592000)"; + + if eval "AUTOLEARN_ENABLE && AUTOLEARN_REPLIES_HAM && bayes_is_balanced(SPAM_DB, false, AUTOLEARN_SPAM_HAM_BALANCE)" { + eval "bayes_train(SPAM_DB, thread_name(header.subject) + ' ' + body.to_text, false)"; + } +} + +''' + +[sieve.trusted.scripts.greylist] +name = "Greylisting" +contents = ''' + +#### Script config.sieve #### + +# Whether to add an X-Spam-Status header +let "ADD_HEADER_SPAM" "key_get('spam-config', 'add-spam')"; + +# Whether to add an X-Spam-Result header +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" "key_get('spam-config', 'learn-ham-replies')"; + +# Whether the bayes classifier should be trained automatically +let "AUTOLEARN_ENABLE" "key_get('spam-config', 'learn-enable')"; + +# When to learn ham (score >= threshold) +let "AUTOLEARN_HAM_THRESHOLD" "key_get('spam-config', 'learn-ham-threshold')"; + +# When to learn spam (score <= 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" "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" "key_get('spam-config', 'threshold-spam')"; + +# Discard messages with a score above this threshold +let "SCORE_DISCARD_THRESHOLD" "key_get('spam-config', 'threshold-discard')"; + +# Reject messages with a score above this threshold +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" "key_get('spam-config', 'directory')"; + +# Store to use for Bayes tokens and ids (leave empty for default) +let "SPAM_DB" "key_get('spam-config', 'lookup')"; + + +#### Script greylist.sieve #### + + +set "triplet" "g:${env.remote_ip}.${envelope.from}.${envelope.to}"; + +if eval "!key_exists(SPAM_DB, triplet)" { + # Greylist sender for 30 days + eval "key_set(SPAM_DB, triplet, '', 2592000)"; + reject "422 4.2.2 Greylisted, please try again in a few moments."; + stop; +} + +''' + + +[lookup] +spam-scores = {"ABUSE_SURBL" = "5.0", +"ALLOWLIST_DKIM" = "-1.0", +"ALLOWLIST_DMARC" = "-7.0", +"ALLOWLIST_SPF" = "-1.0", +"ALLOWLIST_SPF_DKIM" = "-3.0", +"ARC_ALLOW" = "-1.0", +"ARC_DNSFAIL" = "0.0", +"ARC_INVALID" = "0.5", +"ARC_NA" = "0.0", +"ARC_REJECT" = "1.0", +"ARC_SIGNED" = "0.0", +"AUTH_NA" = "1.0", +"AUTH_NA_OR_FAIL" = "1.0", +"AUTOGEN_PHP_SPAMMY" = "1.0", +"BAYES_HAM" = "-3.0", +"BAYES_SPAM" = "5.1", +"BLOCKLIST_DKIM" = "2.0", +"BLOCKLIST_DMARC" = "6.0", +"BLOCKLIST_SPF" = "1.0", +"BLOCKLIST_SPF_DKIM" = "3.0", +"BODY_URI_ONLY" = "2.0", +"BOGUS_ENCRYPTED_AND_TEXT" = "10.0", +"BOUNCE" = "-0.1", +"BOUNCE_NO_AUTH" = "1.0", +"BROKEN_CONTENT_TYPE" = "1.5", +"COMPROMISED_ACCT_BULK" = "3.0", +"CRACKED_SURBL" = "5.0", +"CTE_CASE" = "0.5", +"CTYPE_MISSING_DISPOSITION" = "4.0", +"CTYPE_MIXED_BOGUS" = "1.0", +"CT_EXTRA_SEMI" = "1.0", +"DATA_URI_OBFU" = "2.0", +"DATE_IN_FUTURE" = "4.0", +"DATE_IN_PAST" = "1.0", +"DBL_ABUSE" = "5.0", +"DBL_ABUSE_BOTNET" = "6.5", +"DBL_ABUSE_MALWARE" = "6.5", +"DBL_ABUSE_PHISH" = "6.5", +"DBL_ABUSE_REDIR" = "5.0", +"DBL_BLOCKED" = "0.0", +"DBL_BLOCKED_OPENRESOLVER" = "0.0", +"DBL_BOTNET" = "7.5", +"DBL_MALWARE" = "7.5", +"DBL_PHISH" = "7.5", +"DBL_SPAM" = "6.5", +"DCC_BULK" = "3.0", +"DIRECT_TO_MX" = "0.0", +"DISPOSABLE_CC" = "0.0", +"DISPOSABLE_ENVFROM" = "0.0", +"DISPOSABLE_FROM" = "0.0", +"DISPOSABLE_REPLYTO" = "0.0", +"DISPOSABLE_TO" = "0.0", +"DKIM_SIGNED" = "0.0", +"DMARC_BAD_POLICY" = "0.5", +"DMARC_DNSFAIL" = "0.0", +"DMARC_NA" = "0.0", +"DMARC_POLICY_ALLOW" = "-0.5", +"DMARC_POLICY_ALLOW_WITH_FAILURES" = "0.0", +"DMARC_POLICY_QUARANTINE" = "1.5", +"DMARC_POLICY_REJECT" = "2.0", +"DMARC_POLICY_SOFTFAIL" = "0.1", +"DNSWL_BLOCKED" = "0.0", +"DWL_DNSWL_BLOCKED" = "0.0", +"DWL_DNSWL_HI" = "-3.5", +"DWL_DNSWL_LOW" = "-1.0", +"DWL_DNSWL_MED" = "-2.0", +"DWL_DNSWL_NONE" = "0.0", +"EMPTY_SUBJECT" = "1.0", +"ENCRYPTED_PGP" = "-0.5", +"ENCRYPTED_SMIME" = "-0.5", +"ENVFROM_INVALID" = "2.0", +"ENVFROM_SERVICE_ACCT" = "1.0", +"EXT_CSS" = "1.0", +"FAKE_REPLY" = "1.0", +"FORGED_RCVD_TRAIL" = "1.0", +"FORGED_RECIPIENTS" = "2.0", +"FORGED_RECIPIENTS_MAILLIST" = "0.0", +"FORGED_SENDER" = "0.3", +"FORGED_SENDER_MAILLIST" = "0.0", +"FREEMAIL_AFF" = "4.0", +"FREEMAIL_CC" = "0.0", +"FREEMAIL_ENVFROM" = "0.0", +"FREEMAIL_FROM" = "0.0", +"FREEMAIL_REPLYTO" = "0.0", +"FREEMAIL_REPLYTO_NEQ_FROM_DOM" = "3.0", +"FREEMAIL_TO" = "0.0", +"FROM_DN_EQ_ADDR" = "1.0", +"FROM_EQ_ENVFROM" = "0.0", +"FROM_EXCESS_BASE64" = "1.5", +"FROM_EXCESS_QP" = "1.2", +"FROM_HAS_DN" = "0.0", +"FROM_INVALID" = "2.0", +"FROM_NAME_EXCESS_SPACE" = "1.0", +"FROM_NAME_HAS_TITLE" = "1.0", +"FROM_NEEDS_ENCODING" = "1.0", +"FROM_NEQ_DISPLAY_NAME" = "4.0", +"FROM_NEQ_ENVFROM" = "0.0", +"FROM_NO_DN" = "0.0", +"FROM_SERVICE_ACCT" = "1.0", +"HACKED_WP_PHISHING" = "4.5", +"HAS_ANON_DOMAIN" = "0.1", +"HAS_ATTACHMENT" = "0.0", +"HAS_DATA_URI" = "0.0", +"HAS_GOOGLE_FIREBASE_URL" = "2.0", +"HAS_GOOGLE_REDIR" = "1.0", +"HAS_GUC_PROXY_URI" = "1.0", +"HAS_IPFS_GATEWAY_URL" = "6.0", +"HAS_LIST_UNSUB" = "-0.01", +"HAS_ONION_URI" = "0.0", +"HAS_ORG_HEADER" = "0.0", +"HAS_PHPMAILER_SIG" = "0.0", +"HAS_REPLYTO" = "0.0", +"HAS_WP_URI" = "0.0", +"HAS_XAW" = "0.0", +"HAS_XOIP" = "0.0", +"HAS_X_ANTIABUSE" = "0.0", +"HAS_X_AS" = "0.0", +"HAS_X_GMSV" = "0.0", +"HAS_X_PHP_SCRIPT" = "0.0", +"HAS_X_POS" = "0.0", +"HAS_X_PRIO_FIVE" = "0.0", +"HAS_X_PRIO_ONE" = "0.0", +"HAS_X_PRIO_THREE" = "0.0", +"HAS_X_PRIO_TWO" = "0.0", +"HAS_X_PRIO_ZERO" = "0.0", +"HAS_X_SOURCE" = "0.0", +"HEADER_EMPTY_DELIMITER" = "1.0", +"HEADER_FORGED_MDN" = "2.0", +"HEADER_RCONFIRM_MISMATCH" = "2.0", +"FROMHOST_NORES_A_OR_MX" = "1.5", +"FROM_BOUNCE" = "0.0", +"HELO_BAREIP" = "3.0", +"HELO_IP_A" = "1.0", +"HELO_NORES_A_OR_MX" = "0.3", +"HELO_NOT_FQDN" = "2.0", +"HELO_IPREV_MISMATCH" = "1.0", +"RCPT_BOUNCEMOREONE" = "1.5", +"URL_ONLY" = "2.2", +"HIDDEN_SOURCE_OBJ" = "2.0", +"HTML_META_REFRESH_URL" = "5.0", +"HTML_SHORT_LINK_IMG_1" = "2.0", +"HTML_SHORT_LINK_IMG_2" = "1.0", +"HTML_SHORT_LINK_IMG_3" = "0.5", +"HTML_TEXT_IMG_RATIO" = "1.0", +"HTML_UNBALANCED_TAG" = "0.5", +"HTTP_TO_HTTPS" = "0.5", +"HTTP_TO_IP" = "1.0", +"INFO_TO_INFO_LU" = "2.0", +"INVALID_DATE" = "1.5", +"INVALID_FROM_8BIT" = "6.0", +"INVALID_MSGID" = "1.7", +"KLMS_SPAM" = "5.0", +"LONG_SUBJ" = "3.0", +"MAILLIST" = "-0.2", +"MANY_INVISIBLE_PARTS" = "1.0", +"MID_BARE_IP" = "2.0", +"MID_CONTAINS_FROM" = "1.0", +"MID_CONTAINS_TO" = "1.0", +"MID_MISSING_BRACKETS" = "1.0", +"MID_RHS_IP_LITERAL" = "1.0", +"MID_RHS_MATCH_FROM" = "1.0", +"MID_RHS_MATCH_FROMTLD" = "1.0", +"MID_RHS_MATCH_TO" = "1.0", +"MID_RHS_NOT_FQDN" = "0.5", +"MID_RHS_WWW" = "0.5", +"MIME_ARCHIVE_IN_ARCHIVE" = "5.0", +"MIME_BAD" = "1.0", +"MIME_BAD_ATTACHMENT" = "4.0", +"MIME_BAD_EXTENSION" = "2.0", +"MIME_BAD_UNICODE" = "8.0", +"MIME_BASE64_TEXT" = "0.1", +"MIME_BASE64_TEXT_BOGUS" = "1.0", +"MIME_DOUBLE_BAD_EXTENSION" = "2.0", +"MIME_GOOD" = "-0.1", +"MIME_HEADER_CTYPE_ONLY" = "2.0", +"MIME_HTML_ONLY" = "0.2", +"MIME_MA_MISSING_HTML" = "1.0", +"MIME_MA_MISSING_TEXT" = "2.0", +"MISSING_DATE" = "1.0", +"MISSING_FROM" = "2.0", +"MISSING_MID" = "2.5", +"MISSING_MIME_VERSION" = "2.0", +"MISSING_SUBJECT" = "2.0", +"MISSING_TO" = "2.0", +"MSBL_EBL" = "7.5", +"MSBL_EBL_GREY" = "0.5", +"MULTIPLE_FROM" = "8.0", +"MULTIPLE_UNIQUE_HEADERS" = "7.0", +"MV_CASE" = "0.5", +"MW_SURBL_MULTI" = "7.5", +"HOMOGRAPH_URL" = "5.0", +"ONCE_RECEIVED" = "0.1", +"PHISHED_OPENPHISH" = "7.0", +"PHISHED_PHISHTANK" = "7.0", +"PHISHING" = "4.0", +"PHISH_EMOTION" = "1.0", +"PHP_XPS_PATTERN" = "0.0", +"PH_SURBL_MULTI" = "7.5", +"PRECEDENCE_BULK" = "0.0", +"PREVIOUSLY_DELIVERED" = "0.0", +"PYZOR" = "3.5", +"RBL_BARRACUDA" = "4.0", +"RBL_BLOCKLISTDE" = "4.0", +"RBL_MAILSPIKE_BAD" = "1.0", +"RBL_MAILSPIKE_VERYBAD" = "1.5", +"RBL_MAILSPIKE_WORST" = "2.0", +"RBL_NIXSPAM" = "4.0", +"RBL_SEM" = "1.0", +"RBL_SEM_IPV6" = "1.0", +"RBL_SENDERSCORE" = "2.0", +"RBL_SPAMCOP" = "4.0", +"RBL_SPAMHAUS" = "0.0", +"RBL_SPAMHAUS_BLOCKED" = "0.0", +"RBL_SPAMHAUS_BLOCKED_OPENRESOLVER" = "0.0", +"RBL_SPAMHAUS_CSS" = "2.0", +"RBL_SPAMHAUS_DROP" = "7.0", +"RBL_SPAMHAUS_PBL" = "2.0", +"RBL_SPAMHAUS_SBL" = "4.0", +"RBL_SPAMHAUS_XBL" = "4.0", +"RBL_VIRUSFREE_BOTNET" = "2.0", +"RCPT_ADDR_IN_SUBJECT" = "3.0", +"RCPT_COUNT_FIVE" = "0.0", +"RCPT_COUNT_GT_50" = "0.0", +"RCPT_COUNT_ONE" = "0.0", +"RCPT_COUNT_SEVEN" = "0.0", +"RCPT_COUNT_THREE" = "0.0", +"RCPT_COUNT_TWELVE" = "0.0", +"RCPT_COUNT_TWO" = "0.0", +"RCPT_COUNT_ZERO" = "0.0", +"RCPT_LOCAL_IN_SUBJECT" = "2.0", +"RCVD_COUNT_FIVE" = "0.0", +"RCVD_COUNT_ONE" = "0.0", +"RCVD_COUNT_SEVEN" = "0.0", +"RCVD_COUNT_THREE" = "0.0", +"RCVD_COUNT_TWELVE" = "0.0", +"RCVD_COUNT_TWO" = "0.0", +"RCVD_COUNT_ZERO" = "0.0", +"RCVD_DKIM_ARC_DNSWL_HI" = "-1.0", +"RCVD_DKIM_ARC_DNSWL_MED" = "-0.5", +"RCVD_DOUBLE_IP_SPAM" = "2.0", +"RCVD_FROM_SMTP_AUTH" = "0.0", +"RCVD_HELO_USER" = "3.0", +"RCVD_ILLEGAL_CHARS" = "4.0", +"RCVD_IN_DNSWL_HI" = "-0.5", +"RCVD_IN_DNSWL_LOW" = "-0.1", +"RCVD_IN_DNSWL_MED" = "-0.2", +"RCVD_IN_DNSWL_NONE" = "0.0", +"RCVD_NO_TLS_LAST" = "0.1", +"RCVD_TLS_ALL" = "0.0", +"RCVD_TLS_LAST" = "0.0", +"RCVD_UNAUTH_PBL" = "2.0", +"RCVD_VIA_SMTP_AUTH" = "0.0", +"RDNS_DNSFAIL" = "0.0", +"RDNS_NONE" = "1.0", +"RECEIVED_BLOCKLISTDE" = "3.0", +"RECEIVED_SPAMHAUS_BLOCKED" = "0.0", +"RECEIVED_SPAMHAUS_BLOCKED_OPENRESOLVER" = "0.0", +"RECEIVED_SPAMHAUS_CSS" = "1.0", +"RECEIVED_SPAMHAUS_PBL" = "0.0", +"RECEIVED_SPAMHAUS_SBL" = "3.0", +"RECEIVED_SPAMHAUS_XBL" = "1.0", +"REDIRECTOR_URL" = "0.0", +"REDIRECTOR_URL_ONLY" = "1.0", +"REPLYTO_ADDR_EQ_FROM" = "0.0", +"REPLYTO_DN_EQ_FROM_DN" = "0.0", +"REPLYTO_DOM_EQ_FROM_DOM" = "0.0", +"REPLYTO_DOM_NEQ_FROM_DOM" = "0.0", +"REPLYTO_EMAIL_HAS_TITLE" = "2.0", +"REPLYTO_EQ_FROM" = "0.0", +"REPLYTO_EQ_TO_ADDR" = "5.0", +"REPLYTO_EXCESS_BASE64" = "1.5", +"REPLYTO_EXCESS_QP" = "1.2", +"REPLYTO_UNPARSEABLE" = "1.0", +"RWL_MAILSPIKE_EXCELLENT" = "-0.4", +"RWL_MAILSPIKE_GOOD" = "-0.1", +"RWL_MAILSPIKE_NEUTRAL" = "0.0", +"RWL_MAILSPIKE_POSSIBLE" = "0.0", +"RWL_MAILSPIKE_VERYGOOD" = "-0.2", +"R_BAD_CTE_7BIT" = "3.5", +"DKIM_ALLOW" = "-0.2", +"DKIM_NA" = "0.0", +"DKIM_PERMFAIL" = "0.0", +"DKIM_REJECT" = "1.0", +"DKIM_TEMPFAIL" = "0.0", +"R_MISSING_CHARSET" = "0.5", +"R_MIXED_CHARSET" = "5.0", +"MIXED_CHARSET_URL" = "7.0", +"R_NO_SPACE_IN_FROM" = "1.0", +"R_PARTS_DIFFER" = "1.0", +"SPF_ALLOW" = "-0.2", +"SPF_DNSFAIL" = "0.0", +"SPF_FAIL" = "1.0", +"SPF_NA" = "0.0", +"SPF_NEUTRAL" = "0.0", +"SPF_PERMFAIL" = "0.0", +"SPF_SOFTFAIL" = "0.0", +"R_SUSPICIOUS_URL" = "5.0", +"R_UNDISC_RCPT" = "3.0", +"SEM_URIBL" = "3.5", +"SEM_URIBL_FRESH15" = "3.0", +"SIGNED_PGP" = "-2.0", +"SIGNED_SMIME" = "-2.0", +"SORTED_RECIPS" = "3.5", +"SPAM_FLAG" = "5.0", +"SPAM_TRAP" = "discard", +"SPOOF_DISPLAY_NAME" = "8.0", +"SPOOF_REPLYTO" = "6.0", +"SUBJECT_ENDS_EXCLAIM" = "0.0", +"SUBJECT_ENDS_QUESTION" = "1.0", +"SUBJECT_ENDS_SPACES" = "0.5", +"SUBJECT_HAS_CURRENCY" = "1.0", +"SUBJECT_HAS_EXCLAIM" = "0.0", +"SUBJECT_HAS_QUESTION" = "0.0", +"SUBJECT_NEEDS_ENCODING" = "1.0", +"SUBJ_ALL_CAPS" = "3.0", +"SUBJ_BOUNCE_WORDS" = "0.0", +"SUBJ_EXCESS_BASE64" = "1.5", +"SUBJ_EXCESS_QP" = "1.2", +"SURBL_BLOCKED" = "0.0", +"SURBL_HASHBL_ABUSE" = "5.0", +"SURBL_HASHBL_CRACKED" = "5.0", +"SURBL_HASHBL_EMAIL" = "5.0", +"SURBL_HASHBL_MALWARE" = "6.5", +"SURBL_HASHBL_PHISH" = "6.5", +"SUSPICIOUS_RECIPS" = "1.5", +"TAGGED_FROM" = "0.0", +"TAGGED_RCPT" = "0.0", +"THREAD_HIJACKING_FROM_INJECTOR" = "2.0", +"TO_DN_ALL" = "0.0", +"TO_DN_EQ_ADDR_ALL" = "0.0", +"TO_DN_EQ_ADDR_SOME" = "0.0", +"TO_DN_NONE" = "0.0", +"TO_DN_RECIPIENTS" = "2.0", +"TO_DN_SOME" = "0.0", +"TO_DOM_EQ_FROM_DOM" = "0.0", +"TO_EQ_FROM" = "0.0", +"TO_EXCESS_BASE64" = "1.5", +"TO_EXCESS_QP" = "1.2", +"TO_MATCH_ENVRCPT_ALL" = "0.0", +"TO_MATCH_ENVRCPT_SOME" = "0.0", +"TO_NEEDS_ENCODING" = "1.0", +"TO_WRAPPED_IN_SPACES" = "2.0", +"TRUSTED_REPLY" = "-7.0", +"UNDISC_RCPTS_BULK" = "3.0", +"UNITEDINTERNET_SPAM" = "5.0", +"URIBL_BLACK" = "7.5", +"URIBL_BLOCKED" = "0.0", +"URIBL_GREY" = "1.5", +"URIBL_RED" = "3.5", +"URI_COUNT_ODD" = "1.0", +"URI_HIDDEN_PATH" = "1.0", +"URL_IN_SUBJECT" = "4.0", +"URL_REDIRECTOR_NESTED" = "1.0", +"VIOLATED_DIRECT_SPF" = "3.5", +"WP_COMPROMISED" = "0.0", +"WWW_DOT_DOMAIN" = "0.5", +"XM_CASE" = "0.5", +"XM_UA_NO_VERSION" = "0.01", +"X_PHP_EVAL" = "4.0", +"ZERO_WIDTH_SPACE_URL" = "7.0", +"SHORT_PART_BAD_HEADERS" = "7.0", +"MISSING_ESSENTIAL_HEADERS" = "7.0", +"SINGLE_SHORT_PART" = "0.0", +"COMPLETELY_EMPTY" = "7.0"} + +spam-dmarc = {"18f.gov", +"1password.com", +"2gis.com", +"4chan.org", +"4pda.ru", +"9-11commission.gov", +"911.gov", +"aberdeenshire.gov.uk", +"abilityone.gov", +"absolutbank.ru", +"access-board.gov", +"acquisition.gov", +"acus.gov", +"ada.gov", +"adf.gov", +"adidas.co.kr", +"adidas.com.au", +"adidas.com.br", +"adidas.com.hk", +"adidas.fi", +"adidas.fr", +"admongo.gov", +"adobe.dk", +"adobe.es", +"adobeawards.com", +"adp.com", +"advice.hmrc.gov.uk", +"aerocivil.gov.co", +"afreximbank.com", +"agingstats.gov", +"agro.ru", +"ahcpr.gov", +"aids.gov", +"airbnb.co.uk", +"airbnb.com", +"airbnb.com.tr", +"airbnb.cz", +"airbnb.de", +"airbnb.fi", +"airbnb.fr", +"airbnb.pl", +"airbnb.ru", +"airbnb.se", +"airnow.gov", +"airtel.in", +"alfabank.com", +"alfabank.ru", +"alfastrah.ru", +"alibaba.com", +"aliexpress.com", +"alipay.com", +"alkupone.ru", +"alzheimers.gov", +"amazon.co.uk", +"amazon.com", +"amazon.com.br", +"amberalert.gov", +"americanexpress.com", +"ameslab.gov", +"angus.gov.uk", +"anidub.com", +"annapolis.gov", +"anstaskforce.gov", +"apple.com.au", +"apple.com.cn", +"apps.gov", +"archives.gov", +"arctic.gov", +"arionbanki.is", +"asg.com", +"asic.gov.au", +"askona.ru", +"asos.com", +"assist.ru", +"atf.gov", +"avast.com", +"avg.com", +"avito.ru", +"avtoradio.ru", +"axisbank.com", +"badoo.com", +"baltbank.ru", +"bank.lv", +"banki.ru", +"bankofamerica.com", +"barclaycard.co.uk", +"barclays.co.uk", +"barclays.com", +"battle.net", +"beeline.kz", +"beeline.ru", +"benefits.gov", +"betfaq.ru", +"biglion.ru", +"binary.com", +"binbank.ru", +"bioethics.gov", +"biometrics.gov", +"biopreferred.gov", +"birminghampost.net", +"bishopsmove.com", +"bitbank.cc", +"bjs.gov", +"blizzard.com", +"blog.gov.uk", +"bls.gov", +"bluestarindia.com", +"boemre.gov", +"bolsover.gov.uk", +"bolton.gov.uk", +"booking.com", +"bookmate.com", +"books.ru", +"bournemouth.gov.uk", +"box.com", +"bpa.gov", +"brandshop.ru", +"bridgend.gov.uk", +"brighton-hove.gov.uk", +"britishembassy.gov.uk", +"broadbandmap.gov", +"bromley.gov.uk", +"bts.gov", +"business.gov", +"caerphilly.gov.uk", +"caixa.gov.br", +"cambridge-news.co.uk", +"campaign.gov.uk", +"cancer.gov", +"cannockchasedc.gov.uk", +"capitalone.co.uk", +"cardiff.gov.uk", +"carecredit.com", +"cbp.gov", +"cdfifund.gov", +"centralbedfordshire.gov.uk", +"ceredigion.gov.uk", +"cesg.gov.uk", +"cfda.gov", +"cfo.gov", +"challenge.gov", +"change.org", +"chase.com", +"chcoc.gov", +"childstats.gov", +"cio.gov", +"circle.com", +"citibank.ae", +"citibank.co.in", +"citibank.co.uk", +"citibank.com.my", +"citibank.hu", +"citibank.pl", +"cloud.gov", +"cloudflare.com", +"cms.gov", +"co-operativebank.co.uk", +"colgate.com.br", +"collegedrinkingprevention.gov", +"companies-house.gov.uk", +"comuneap.gov.it", +"conab.gov.br", +"concerts.com", +"consultant.ru", +"contact-sys.com", +"copeland.gov.uk", +"cosla.gov.uk", +"courtservice.gov.uk", +"coventry.gov.uk", +"cre.gov.uk", +"csosa.gov", +"cuidadodesalud.gov", +"culturarecreacionydeporte.gov.co", +"customs.gov.my", +"customs.gov.ua", +"cybercrime.gov", +"dailypost.co.uk", +"danskebank.dk", +"danskebank.fi", +"danskebank.ie", +"danskebank.no", +"dartford.gov.uk", +"dartmoor.gov.uk", +"dataprotection.gov.uk", +"daventrydc.gov.uk", +"dellin.ru", +"denbighshire.gov.uk", +"deutsche-bank.de", +"deutschebank.be", +"deutschebank.co.in", +"dh.gov.uk", +"dhl.com", +"dhs.gov", +"diablo3.com", +"digital.gov", +"digitalgov.gov", +"digitalliteracy.gov", +"disability.gov", +"disability.gov.uk", +"dnfsb.gov", +"docker.com", +"docusign.net", +"doe.gov", +"doioig.gov", +"dol.gov", +"doleta.gov", +"domofond.ru", +"drought.gov", +"drugabuse.gov", +"dsns.gov.ua", +"dtv.gov", +"dudley.gov.uk", +"dyslexiaida.org", +"e-boks.dk", +"e-verify.gov", +"eastdunbarton.gov.uk", +"eaststaffsbc.gov.uk", +"eastsuffolk.gov.uk", +"ebay.be", +"ebay.ca", +"ebay.ch", +"ebay.co.uk", +"ebay.com", +"ebay.com.au", +"ebay.com.cn", +"ebay.de", +"ebay.es", +"ebay.eu", +"ebay.fr", +"ebay.in", +"ebay.it", +"ebay.se", +"economy.gov.tr", +"econsumer.gov", +"ed.gov", +"eftps.gov", +"ehsni.gov.uk", +"eia.gov", +"ejob.gov.tw", +"elance.com", +"eldorado.ru", +"email-ee.co.uk", +"email.tektorg.ru", +"emarsys.com", +"ems.gov", +"energystar.gov", +"erewash.gov.uk", +"esetnod32.ru", +"essex-fire.gov.uk", +"eubank.kz", +"evernote.com", +"everychildmatters.gov.uk", +"evus.gov", +"exist.ru", +"expediamail.com", +"facebook.com", +"facebookmail.com", +"fbi.gov", +"fcc.gov", +"fco.gov.uk", +"fdic.gov", +"feb.gov", +"federalreserve.gov", +"fedex.com", +"fedramp.gov", +"fedshirevets.gov", +"feedthefuture.gov", +"fema.gov", +"ferc.gov", +"fhfa.gov", +"fhfaoig.gov", +"fife.gov.uk", +"financialresearch.gov", +"financialstability.gov", +"firstbankpb.bank", +"firstbankpb.com", +"firstnet.gov", +"firstresponder.gov", +"fishwatch.gov", +"fitness.gov", +"flagma.ua", +"flamp.ru", +"fletc.gov", +"fmc.gov", +"fmcs.gov", +"foia.gov", +"food.gov.uk", +"force.com", +"fordlibrarymuseum.gov", +"foreignassistance.gov", +"foreigntrade.gov.tr", +"franklinwi.gov", +"ftc.gov", +"ftccomplaintassistant.gov", +"gamereactor.dk", +"gap.com", +"garant.ru", +"geekbrains.ru", +"geektimes.ru", +"getsmartaboutdrugs.gov", +"gibraltar.gov.uk", +"gitlab.com", +"globalentry.gov", +"globalhealth.gov", +"globe.gov", +"gloucestershire.gov.uk", +"goes-r.gov", +"gosuslugi.ru", +"gov.uk", +"groupon.es", +"groupon.hk", +"groupon.it", +"gsa.gov", +"gsaadvantage.gov", +"gsaauctions.gov", +"gsaig.gov", +"gtbank.com", +"guideline.gov", +"guidelines.gov", +"gwynedd.gov.uk", +"habr.com", +"hambleton.gov.uk", +"harp.gov", +"hawaiicounty.gov", +"hdfcbank.com", +"hdrezka.ag", +"healthcare.gov", +"healthypeople.gov", +"hertfordshire.gov.uk", +"hh.kz", +"hh.ru", +"highland.gov.uk", +"highwaycode.gov.uk", +"hillingdon.gov.uk", +"hiv.gov", +"hmrc.gov.uk", +"homeoffice.gov.uk", +"homesales.gov", +"hotels.com", +"hounslow.gov.uk", +"howto.gov", +"hru.gov", +"huduser.gov", +"hurricanes.gov", +"iba.gov.au", +"ice.gov", +"idmanagement.gov", +"ikea.ch", +"ikea.co.uk", +"ikea.com", +"ikea.de", +"ikea.fr", +"ikea.gr", +"ikea.nl", +"ikea.pl", +"imgur.com", +"incometaxindiaefiling.gov.in", +"ing.com", +"inl.gov", +"inlandrevenue.gov.uk", +"insider.co.uk", +"insolvency.gov.uk", +"instagram.com", +"insurekidsnow.gov", +"invasivespeciesinfo.gov", +"investor.gov", +"irda.gov.in", +"itunes.com", +"jccbi.gov", +"jd.ru", +"jet.com", +"jimmycarterlibrary.gov", +"job.com", +"johnsonsbaby.co.uk", +"joybuy.com", +"jpmorgan.com", +"jpmorgansecurities.com", +"judiciary.gov.uk", +"justice.gov", +"justice.gov.az", +"justice.gov.uk", +"jyskebank.dk", +"kassy.ru", +"kent.gov.uk", +"keys.openpgp.org", +"kids.gov", +"kingston.gov.uk", +"kivra.com", +"klarna.com", +"klarna.se", +"kpk.gov.pl", +"lacoast.gov", +"landsbanki.is", +"lanl.gov", +"lbhf.gov.uk", +"lcd.gov.uk", +"learningcurve.gov.uk", +"leeds.gov.uk", +"leroymerlin.es", +"lichfielddc.gov.uk", +"lincoln.gov.uk", +"lincolnshire.gov.uk", +"linkedin.com", +"livejournal.com", +"llnl.gov", +"lloydsbank.com", +"locatorplus.gov", +"lostfilm.tv", +"louisvilleco.gov", +"love.ru", +"lufthansa-group.com", +"lufthansa.com", +"mackeeper.com", +"mailgun.net", +"mak.com", +"mandtbank.com", +"mcc.gov", +"mcga.gov.uk", +"mchenrycountyil.gov", +"mecknc.gov", +"mediamarkt.se", +"medicaid.gov", +"medicare.gov", +"medium.com", +"megafon.ru", +"megaplan.ru", +"mercadolibre.com.ar", +"mercadolivre.com.br", +"merseyfire.gov.uk", +"merthyr.gov.uk", +"meshok.ru", +"messenger.com", +"microsoft.net", +"middlesbrough.gov.uk", +"midlothian.gov.uk", +"mil.ru", +"mincit.gov.co", +"minhacienda.gov.co", +"minsvyaz.ru", +"mintic.gov.co", +"mirrorfootball.co.uk", +"mkb.ru", +"mlg.ru", +"mlg.tv", +"mns.gov.ua", +"mod.gov.az", +"molisa.gov.vn", +"mos.ru", +"mosoblbank.ru", +"mosreg.ru", +"motinfo.gov.uk", +"movavi.com", +"msha.gov", +"mspb.gov", +"msport.gov.pl", +"murfreesborotn.gov", +"mvideo.ru", +"mxtoolbox.com", +"mymoney.gov", +"myplate.gov", +"myra.gov", +"myshared.ru", +"n-kesteven.gov.uk", +"n-somerset.gov.uk", +"nads.gov.ua", +"nalog.ru", +"namus.gov", +"nasa.gov", +"nationalarchives.gov.uk", +"nationalservice.gov", +"nationsreportcard.gov", +"nbr.gov.bd", +"nbtbank.com", +"ncifcrf.gov", +"ncpw.gov", +"nctb.gov.bd", +"ne-derbyshire.gov.uk", +"nea.gov", +"nelincs.gov.uk", +"neobux.com", +"neolane.net", +"netflix.com", +"newegg.com", +"newmoney.gov", +"nga.gov", +"ngu.gov.ua", +"nhtsa.gov", +"nic.ru", +"nidw.gov.bd", +"nij.gov", +"nio.gov.uk", +"niscc.gov.uk", +"nist.gov", +"nixonlibrary.gov", +"nkh.gov.hu", +"noaa.gov", +"nordea.dk", +"nordea.com", +"nordea.fi", +"nordea.no", +"nordea.se", +"north-ayrshire.gov.uk", +"north-norfolk.gov.uk", +"northlincs.gov.uk", +"norwich.gov.uk", +"notifications.service.gov.uk", +"nottinghamcity.gov.uk", +"nrc-gateway.gov", +"nrc.gov", +"nrel.gov", +"nsf.gov", +"nsopr.gov", +"nsopw.gov", +"nwtrb.gov", +"oculus.com", +"ofcm.gov", +"office.com", +"officemag.ru", +"ok.ru", +"omb.gov", +"ombudsman.gov.tr", +"onedrive.com", +"onguardonline.gov", +"opengl.org", +"openinternet.gov", +"ordsvy.gov.uk", +"ornl.gov", +"oshrc.gov", +"osti.gov", +"oxfordshire.gov.uk", +"ozon.ru", +"paauditor.gov", +"paccar.com", +"paddle8.com", +"pandemicflu.gov", +"passport.gov.uk", +"payeer.com", +"paymentaccuracy.gov", +"paypal-community.com", +"paypal.be", +"paypal.cn", +"paypal.co.il", +"paypal.co.uk", +"paypal.com", +"paypal.com.au", +"paypal.com.br", +"paypal.com.mx", +"paypal.de", +"paypal.dk", +"paypal.es", +"paypal.fr", +"paypal.nl", +"paypal.se", +"pbgc.gov", +"pc.gov.au", +"pch.com", +"penanghill.gov.my", +"pepfar.gov", +"performance.gov", +"pinterest.co.kr", +"pinterest.com", +"pinterest.de", +"pinterest.jp", +"pinterest.se", +"pkc.gov.uk", +"planeta.ru", +"platron.ru", +"plymouth.gov.uk", +"pm.gov.uk", +"pmf.gov", +"pmi.gov", +"pncbank.com", +"pokerstars.com", +"pokerstars.fr", +"pokerstars.it", +"pokerstars.net", +"priorbank.by", +"privatbank.ua", +"prospertx.gov", +"prostocash.com", +"provident.bank", +"psbank.ru", +"psc.gov", +"punjab.gov.in", +"puzzle-english.com", +"qiwi.com", +"qiwi.ru", +"rabota.ru", +"rbkc.gov.uk", +"ready.gov", +"reaganlibrary.gov", +"redbridge.gov.uk", +"reddit.com", +"reebok.es", +"reebok.nl", +"reginfo.gov", +"regulations.gov", +"reisebank.de", +"renfrewshire.gov.uk", +"rentonwa.gov", +"reportband.gov", +"rgs.ru", +"richmond.gov.uk", +"rivers.gov", +"rkn.gov.ru", +"ros.gov.uk", +"roseltorg.ru", +"rostelecom.ru", +"roundrocktexas.gov", +"royalmail.com", +"rozetka.com.ua", +"rt.com", +"rt.ru", +"rushcliffe.gov.uk", +"rutubeinfo.ru", +"sacn.gov.uk", +"safercar.gov", +"samhsa.gov", +"sanmarcostx.gov", +"sberbank.ru", +"sberbank-ast.ru", +"sbir.gov", +"sbis.ru", +"scality.com", +"scdhhs.gov", +"science360.gov", +"sciencebase.gov", +"scijinks.gov", +"sec.gov", +"secretservice.gov", +"section508.gov", +"semnan.ac.ir", +"senate.gov", +"sendgrid.net", +"seniorcorps.gov", +"serpro.gov.br", +"service.gov.uk", +"sftool.gov", +"shetland.gov.uk", +"shropshire-cc.gov.uk", +"shutterstock.com", +"sigtarp.gov", +"sk.ru", +"skat.dk", +"skatteverket.se", +"skbbank.ru", +"skittles.com", +"skydio.com", +"skype.com", +"slideshare.com", +"smart.gov", +"smida.gov.ua", +"smokefree.gov", +"snickers.com", +"solardecathlon.gov", +"sourceforge.net", +"south-ayrshire.gov.uk", +"sovest.ru", +"spbrealty.ru", +"sportmaster.ru", +"squarespace.com", +"sravni.ru", +"srs.gov", +"staffordbc.gov.uk", +"stat.gov.az", +"stedmundsbury.gov.uk", +"sthelens.gov.uk", +"stihl.ru", +"stopalcoholabuse.gov", +"stopfraud.gov", +"studentloans.gov", +"subscribe.ru", +"suffolkcc.gov.uk", +"suncorpbank.com.au", +"sundaymirror.co.uk", +"sunlight.net", +"superjob.ru", +"surestart.gov.uk", +"sutton.gov.uk", +"swansea.gov.uk", +"swift.com", +"symantec.com", +"synologynotification.com", +"taobao.com", +"tatar.ru", +"tauntondeane.gov.uk", +"tda.gov.uk", +"tdk.gov.tr", +"tdscpc.gov.in", +"telework.gov", +"tenders.gov.au", +"textmagic.com", +"tfhrc.gov", +"thebell.io", +"thecoolspot.gov", +"thinkroadsafety.gov.uk", +"tiaabank.com", +"ticketland.ru", +"tinder.com", +"tinkoff.ru", +"tomsk.gov.ru", +"torbay.gov.uk", +"tradingstandards.gov.uk", +"treas.gov", +"trial-sport.ru", +"tsa.gov", +"tst.gov.br", +"tuba.gov.tr", +"turystyka.gov.pl", +"tutu.ru", +"twitch.tv", +"twitter.com", +"twix.com", +"uber.com", +"ucarecdn.com", +"ucrdatatool.gov", +"udall.gov", +"ukvisas.gov.uk", +"ulmart.ru", +"unicor.gov", +"uniras.gov.uk", +"ups.com", +"uralairlines.ru", +"us-cert.gov", +"usa.gov", +"usadf.gov", +"usaid.gov", +"usap.gov", +"uscg.gov", +"usconsulate.gov", +"usmission.gov", +"usphs.gov", +"uspis.gov", +"usps.com", +"usps.gov", +"ustreas.gov", +"utair.ru", +"utkonos.ru", +"vaccines.gov", +"valeofglamorgan.gov.uk", +"verizonwireless.com", +"vigoda.ru", +"visa.co.uk", +"visa.com", +"visa.com.ar", +"visa.com.br", +"visa.com.cn", +"visa.com.tw", +"visa.pl", +"vistacampus.gov", +"vk.com", +"vkrugudruzei.ru", +"vkusnyblog.ru", +"vmc.gov.in", +"voa.gov.uk", +"volunteer.gov", +"vote.gov", +"walsall.gov.uk", +"wandsworth.gov.uk", +"wartimecontracting.gov", +"warwickdc.gov.uk", +"wealden.gov.uk", +"wellingtonfl.gov", +"west-lindsey.gov.uk", +"westernunion.com", +"westernunion.ru", +"westlothian.gov.uk", +"whatsapp.com", +"whistleblowers.gov", +"wirral.gov.uk", +"wlga.gov.uk", +"womenshealth.gov", +"wrexham.gov.uk", +"wrigley.com", +"wrp.gov", +"yandex-team.ru", +"york.gov.uk", +"youla.ru", +"youth.gov", +"youthrules.gov", +"youtube.com", +"zcts.ru", +"zendesk.com", +"zionsbank.com", +"zomato.com"} + + +spam-allow = {"126.com", +"163.com", +"1gost.info", +"1stnationalbank.com", +"2o7.net", +"365online.com", +"4at1.com", +"53.com", +"5iantlavalamp.com", +"abl.com.pk", +"about.com", +"accessbankplc.com", +"adelphia.net", +"adib.ae", +"adobe.com", +"agora-inc.com", +"agoramedia.com", +"aibgb.co.uk", +"aib.ie", +"airdriesavingsbank.com", +"akamai.net", +"akamaitech.net", +"aldermore.co.uk", +"alexa.com", +"alliancebank.com.my", +"alliancefg.com", +"alliantcreditunion.com", +"alliantcreditunion.org", +"allianz.de", +"allybank.com", +"alterna.ca", +"amazon.com", +"americanexpress.ch", +"americanexpress.com", +"anadolubank.nl", +"ancestry.com", +"anz.com", +"anz.co.nz", +"aol.com", +"apache.org", +"apple.com", +"arbuthnotlatham.co.uk", +"arcamax.com", +"asb.co.nz", +"ask.com", +"astrology.com", +"atdmt.com", +"att.net", +"authorize.net", +"autorambler.ru", +"axisbank.co.in", +"axisbank.com", +"b2bbank.com", +"baaderbank.de", +"baidu.com", +"baloise.ch", +"baml.com", +"banamex.com", +"bancanetbsc.do", +"bancanetsantacruz.com.do", +"bancapulia.it", +"bancarios.com", +"bancastato.ch", +"bancatransilvania.ro", +"bancobase.com", +"bancobic.ao", +"bancobic.pt", +"bancobpi.pt", +"banco.bradesco", +"bancobrasil.com.br", +"bancochile.cl", +"bancochile.com", +"bancoestado.cl", +"bancofalabella.cl", +"bancofalabella.com.co", +"bancofalabella.pe", +"bancomer.com", +"bancopopolare.it", +"bancopostaclick.it", +"bancoposta.it", +"bancosantander.es", +"bancovotorantimcartoes.com.br", +"bank24.ru", +"bankalhabib.com", +"bankaustria.at", +"bank.barclays.co.uk", +"bankbgzbnpparibas.pl", +"bankcardservices.co.uk", +"bankcomm.com", +"bankcoop.ch", +"bankiabancapersonal.es", +"bankia.com", +"bankia.es", +"bankinter.com", +"bankinter.es", +"bankmutual.com", +"bankofamerica.com", +"bankofcanada.ca", +"bankofchina.com", +"bankofcyprus.com", +"bankofindia.co.nz", +"bankofireland.com", +"bank-of-ireland.co.uk", +"bankofirelanduk.com", +"bankofoklahoma.com", +"bankofscotland.co.uk", +"banksinarmas.com", +"bankvonroll.ch", +"bankwest.com.au", +"banque-casino.fr", +"banquepopulaire.fr", +"banquescotia.com", +"barclaycard.co.uk", +"barclaycard.de", +"barclaycard.es", +"barclays.com", +"barclays.co.uk", +"barclayspartnerfinance.com", +"barclays.sc", +"barodanzltd.co.nz", +"basler.ch", +"bbandt.com", +"bbc.co.uk", +"bcentral.com", +"bci.cl", +"bcp.com.pe", +"bcv.ch", +"bcvs.ch", +"bekb.ch", +"bellevue.ch", +"bellsouth.net", +"bendigobank.com.au", +"berliner-bank.de", +"berliner-sparkasse.de", +"bfanet.ao", +"bfi0.com", +"bgfi.com", +"bgfionline.com", +"bgzbnpparibas.pl", +"billmelater.com", +"bing.com", +"bkb.ch", +"bk.rw", +"bks.at", +"blkb.ch", +"bmocm.com", +"bmo.com", +"bmogam.com", +"bmoharris.com", +"bmoharrisprivatebankingonline.com", +"bmoinvestorline.com", +"bmonesbittburns.com", +"bnl.it", +"bnpparibas.com", +"bnpparibas.fr", +"boc.cnnz", +"bonuscard.ch", +"bpe-gruposantander.com", +"bpi.pt", +"bpostbank.be", +"bradescardonline.com.br", +"bradesco.com.br", +"bradescoseguranca.com.br", +"bridgetrack.com", +"bridgewaterbank.ca", +"bsibank.com", +"btrl.ro", +"bt-trade.ro", +"businessonline-boi.com", +"bzbank.ch", +"ca-cib.com", +"ca-egypt.com", +"cafbank.org", +"cafe24.com", +"cafonline.org", +"caisse-epargne.com", +"caisse-epargne.fr", +"caixabank.com", +"caixa.gov.br", +"cajasur.es", +"camsonline.com", +"canadiandirect.com", +"capitalone360.com", +"capitalone.com", +"capitaloneonline.co.uk", +"capitecbank.co.za", +"cariparma.it", +"carrefour-banque.fr", +"cartabcc.it", +"cartabccpos.it", +"cartasi.it", +"ca-suisse.com", +"catalunyacaixa.com", +"cbg.gm", +"cbonline.co.uk", +"cembra.ch", +"cenbank.org", +"centralbank.ae", +"charitybank.org", +"charter.net", +"chase.com", +"chebanca.it", +"chinatrust.com.tw", +"cial.ch", +"cibc.com", +"cic.ch", +"cimbclicks.com.my", +"citibank.ae", +"citibank.co.in", +"citibank.com", +"citibank.co.uk", +"citibankonline.com", +"citibusiness.com", +"citicards.com", +"citi.com", +"citi.co.nz", +"citi.eu", +"citigroup.com", +"citizensbank.ca", +"citizensbank.com", +"civibank.com", +"civibank.it", +"cjb.net", +"classmates.com", +"clickbank.net", +"closebrothers.com", +"closebrothers.co.uk", +"clubsc.ch", +"cnet.com", +"cnn.com", +"co.kg", +"colpatria.com", +"colpatria.com.co", +"comcast.net", +"com.com", +"commbank.com.au", +"commerzbank.com", +"commerzbank.de", +"com.ne.kr", +"coopbank.dk", +"co-operativebank.co.uk", +"cornerbanca.ch", +"cornercard.ch", +"cornercard.com", +"corner.ch", +"corporate-ir.net", +"cosycard.ch", +"coutts.com", +"cox.net", +"craigslist.org", +"credit-agricole.com", +"credit-agricole.fr", +"creditagricole.rs", +"credit-suisse.com", +"cs.com", +"css.ch", +"ctbcbank.com", +"ctfs.com", +"custhelp.com", +"cwbank.com", +"cwbankgroup.com", +"cwt.ca", +"cybg.com", +"danskebankas.lt", +"danskebank.com", +"danskebank.co.uk", +"danskebank.de", +"danskebank.dk", +"danskebank.ee", +"danskebank.fi", +"danskebank.ie", +"danskebank.no", +"datatrans.biz", +"datatrans.ch", +"daum.net", +"db.com", +"dbs.com", +"dd.se", +"debian.org", +"dell.com", +"demirbank.kg", +"denizbank.com", +"desjardins.ca", +"desjardins.com", +"deutschebank.be", +"deutschebank.co.nz", +"deutsche-bank.de", +"diamondbank.com", +"dibpak.com", +"directnic.com", +"directtrack.com", +"discovercard.com", +"discover.com", +"discovery.co.za", +"dnbnord.lt", +"domain.com", +"doubleclick.com", +"dresdner-bank.de", +"dsbbank.sr", +"dsbl.org", +"duncanlawrie.com", +"earthlink.net", +"easybank.at", +"easylnk.com", +"ebay.com", +"ebay.co.uk", +"ebay.de", +"ebayimg.com", +"ebaystatic.com", +"ecobank.com", +"edgesuite.net", +"ediets.com", +"edwardjones.com", +"egroups.com", +"e-gulfbank.com", +"emode.com", +"esunbank.com.tw", +"example.com", +"example.net", +"example.org", +"excite.com", +"facebook.com", +"fedex.com", +"fednetbank.com", +"fidelity.com", +"fidor.de", +"finance.com", +"finansbank.com.tr", +"finasta.lt", +"fineco.it", +"firstbankcard.com", +"firstmerit.com", +"firstnational.com", +"firstnationalmerchantsolutions.com", +"firsttrustbank.co.uk", +"flickr.com", +"fnbc.ca", +"fnb.co.za", +"fnb-online.com", +"freebsd.org", +"free.fr", +"friuladria.it", +"f-secure.com", +"garantibank.eu", +"garantibank.nl", +"garanti.com.tr", +"gazprombank.ch", +"gazprombank.ru", +"generali.es", +"genevoise.ch", +"gentoo.org", +"geocities.com", +"gkb.ch", +"gmail.com", +"gmx.net", +"go.com", +"godaddy.com", +"googleadservices.com", +"google.co.in", +"google.com", +"google.it", +"google.ru", +"granitbank.hu", +"grisoft.com", +"gtbank.com", +"halifax.co.uk", +"hallmark.com", +"handelsbanken.se", +"harrodsbank.co.uk", +"hbl.com", +"hblibank.com", +"hblibank.com.pk", +"hdfcbank.com", +"heartland.co.nz", +"hellenicbank.com", +"hinet.net", +"hkbea.com", +"hlb.com.kh", +"hlb.com.my", +"hoaresbank.co.uk", +"home.barclays", +"hongleongconnect.com.kh", +"hongleongconnect.com.vn", +"hongleongconnect.my", +"hotbar.com", +"hotmail.com", +"hotpop.com", +"hp.com", +"hsbc.com", +"hsbc.com.ar", +"hsbc.com.hk", +"hsbc.co.nz", +"hsbc.co.uk", +"hypovereinsbank.co.uk", +"hypovereinsbank.de", +"ibm.com", +"icbcnz.com", +"icicibank.co.in", +"icicibank.com", +"icicibankprivatebanking.com", +"icorner.ch", +"icscards.de", +"icscards.nl", +"incredimail.com", +"ing.be", +"ing.com", +"ing-diba.de", +"ingdirect.ca", +"ing.lu", +"ing.nl", +"ingvysyabank.com", +"interac.ca", +"investorplace.com", +"iobnet.co.in", +"isbank.com.tr", +"isbank.de", +"isbank.ge", +"isbank.iq", +"isbankkosova.com", +"itau.com.br", +"ivillage.com", +"joingevalia.com", +"jpmchase.com", +"jpmorgan.com", +"jsafrasarasin.com", +"julianhodgebank.com", +"juliusbaer.com", +"juno.com", +"jyskebank.dk", +"kantonalbank.ch", +"kernel.org", +"key.com", +"kiwibank.co.nz", +"kotak.com", +"kredytbank.pl", +"kreissparkasse-schwalm-eder.de", +"ksklb.de", +"kutxabank.es", +"laboralkutxa.com", +"lacaixa.cat", +"lacaixa.es", +"laurentianbank.ca", +"lbb.de", +"lcl.com", +"lcl.fr", +"li.ru", +"list.ru", +"liveinternet.ru", +"livejournal.com", +"lloydsbank.com", +"lloydsbankcommercial.com", +"lloydsbankinggroup.com", +"lloydstsb.ch", +"lloydstsb.co.uk", +"lombardodier.com", +"loydsbank.com", +"lycos.com", +"m7z.net", +"mac.com", +"macromedia.com", +"maerki-baumann.ch", +"mail.com", +"mail.ru", +"mailscanner.info", +"mandtbank.com", +"manulifebank.ca", +"manulifebankselect.ca", +"manulife.com", +"manulifeone.ca", +"marketwatch.com", +"mashreqbank.com", +"mastercard.com", +"maybank2u.com", +"maybank2u.com.my", +"mcafee.com", +"mchsi.com", +"mdmbank.com", +"mechanicsbank.com", +"medbank.lt", +"messagelabs.com", +"metrobankdirect.com", +"metrobankonline.co.uk", +"microsoft.com", +"migbank.com", +"migrosbank.ch", +"military.com", +"mindspring.com", +"mit.edu", +"mizuhobank.co.jp", +"mmwarburg.lu", +"monster.com", +"montepio.pt", +"morganstanley.com", +"mozilla.com", +"mps.it", +"ms.com", +"msn.com", +"mufg.jp", +"myonlineresourcecenter.com", +"myonlineservices.ch", +"myspace.com", +"nate.com", +"nationalesuisse.ch", +"nationwide-communications.co.uk", +"nationwide.co.uk", +"nationwide-service.co.uk", +"natwest.com", +"navyfederal.org", +"nbc.ca", +"netflix.com", +"netscape.com", +"netscape.net", +"netzero.net", +"newyorkfed.org", +"nibl.com.np", +"nod32.com", +"nordea.fi", +"nordea.lt", +"nordfynsbank.dk", +"norisbank.de", +"norman.com", +"notenstein.ch", +"nuvisionfederal.com", +"nytimes.com", +"oceanbank.com", +"onlinesbi.com", +"openoffice.org", +"openxmlformats.org", +"optonline.net", +"orchardbank.com", +"osdn.com", +"ostsaechsische-sparkasse-dresden.de", +"overstock.com", +"pacbell.net", +"pandasoftware.com", +"passport.com", +"paylife.at", +"paypal.be", +"paypal-brasil.com.br", +"paypal.ca", +"paypal.ch", +"paypal.co.il", +"paypal.com", +"paypal.com.au", +"paypal.com.br", +"paypal-communication.com", +"paypal-community.com", +"paypal.com.mx", +"paypal.com.pt", +"paypal.co.uk", +"paypal-customerfeedback.com", +"paypal.de", +"paypal-deutschland.de", +"paypal.dk", +"paypal.es", +"paypal-exchanges.com", +"paypal.fr", +"paypal.it", +"paypal-marketing.co.uk", +"paypal-marketing.pl", +"paypal.net", +"paypal.nl", +"paypal.no", +"paypal-notify.com", +"paypal-now.com", +"paypalobjects.com", +"paypal-opwaarderen.nl", +"paypal-pages.com", +"paypal.pt", +"paypal.ru", +"paypal.se", +"paypal-search.com", +"paypal-shopping.co.uk", +"paypal-techsupport.com", +"pbebank.com", +"pcfinancial.ca", +"peoplepc.com", +"permanenttsb.ie", +"plaxo.com", +"pnc.com", +"popolarevicenza.it", +"postbank.de", +"postepay.it", +"postfinancearena.ch", +"postfinance.ch", +"postfinance.info", +"price.ru", +"prodigy.net", +"publicislamicbank.com.my", +"rabobank.com", +"rabobank.co.nz", +"rabobank.nl", +"radaruol.com.br", +"rahnbodmer.ch", +"raiffeisenbank.rs", +"raiffeisen.ch", +"raiffeisen.hu", +"raiffeisen.li", +"raiffeisen.ru", +"rambler-co.ru", +"rambler.ru", +"raphaelsbank.com", +"rbc.com", +"rbcroyalbank.com", +"rbs.co.uk", +"rbssecure.co.uk", +"rbsworldpay.com", +"rcb.at", +"real.com", +"recordbank.be", +"redhat.com", +"rediff.com", +"regiobank.nl", +"regions.com", +"regionsnet.com", +"renasantbank.com", +"rhbgroup.com", +"rogersbank.com", +"rogers.com", +"rothschildbank.com", +"rothschild.com", +"royalbank.com", +"rr.com", +"sagepay.com", +"sagepay.co.uk", +"sainsburysbank.co.uk", +"samba.com", +"santander.cl", +"santander.com", +"santander.com.br", +"santander.com.mx", +"santandercorretora.com.br", +"santander.co.uk", +"santanderesfera.com.br", +"santandersantiago.cl", +"sarasin.ch", +"sbcglobal.net", +"sberbank.ch", +"sbs.net.nz", +"sc.com", +"schoellerbank.at", +"scotiabank.ca", +"scotiabank.com", +"scotiamocatta.com", +"scotiaonline.com", +"s.de", +"sec.gov", +"securetrustbank.com", +"service-sparkasse.de", +"serviciobancomer.com", +"sf.net", +"shawbrook.co.uk", +"shaw.ca", +"shkb.ch", +"shockwave.com", +"six-group.com", +"six-payment-services.com", +"skrill.com", +"sls-direkt.de", +"smithbarney.com", +"snb.ch", +"snsbank.nl", +"societegenerale.fr", +"sourceforge.net", +"spamcop.net", +"sparda-a.de", +"sparda-bank-hamburg.de", +"sparda-b.de", +"sparda-bw.de", +"sparda-h.de", +"sparda-hessen.de", +"sparda-m.de", +"sparda-ms.de", +"sparda-n.de", +"sparda-ostbayern.de", +"sparda-sw.de", +"sparda-verband.de", +"sparda-west.de", +"sparkasse.at", +"sparkasse-bank-malta.com", +"sparkasse-bielefeld.de", +"sparkasseblog.de", +"sparkasse-bochum.de", +"sparkasse.ch", +"sparkasse.de", +"sparkasse-gera-greiz.de", +"sparkasse-hamm.de", +"sparkasse-heidelberg.de", +"sparkasse-ingolstadt.de", +"sparkasse-mittelthueringen.de", +"speedera.net", +"sportsline.com", +"standardbank.com", +"standardbank.co.za", +"standardchartered.com.gh", +"standardchartered.com.my", +"subscribe.ru", +"sun.com", +"suncorpbank.com.au", +"suntrust.com", +"swedbank.com", +"swedbank.ee", +"swedbank.lt", +"swedbank.lu", +"swedbank.se", +"swisscanto.ch", +"swisscaution.ch", +"swissquote.ch", +"sydbank.dk", +"sympatico.ca", +"tails.nl", +"tangerine.ca", +"tcb-bank.com.tw", +"tdbank.com", +"tdcommercialbanking.com", +"telus.net", +"terra.com.br", +"tescobank.com", +"ticketmaster.com", +"tinyurl.com", +"tiscali.co.uk", +"tns-counter.ru", +"tom.com", +"tone.co.nz", +"t-online.de", +"top4top.ru", +"tsbbank.co.nz", +"tsb.co.nz", +"tsb.co.uk", +"tux.org", +"twitter.com", +"ubibanca.com", +"ubs.com", +"ulsterbankanytimebanking.co.uk", +"ulsterbank.co.uk", +"unibancoconnect.pt", +"unibanco.pt", +"unicreditbank.lt", +"unicredit.eu", +"unicreditgroup.eu", +"unicredit.it", +"unionbankcameroon.com", +"unionbank.com", +"unity.co.uk", +"uob.com.sg", +"uobgroup.com", +"uol.com.br", +"ups.com", +"usbank.com", +"valianttrust.com", +"vaudoise.ch", +"venetobanca.it", +"venetobanka.al", +"verizon.net", +"versabank.com", +"videobank.it", +"virginmoney.com", +"visa.com.ar", +"visa.com.br", +"visaeurope.ch", +"visaeurope.com", +"viseca.ch", +"volksbank.de", +"volkswagenbank.de", +"vpbank.com", +"vr.de", +"vwbank.de", +"w3.org", +"wachovia.com", +"walmart.com", +"wamu.com", +"wanadoo.fr", +"washingtonpost.com", +"weatherbug.com", +"weatherbys.co.uk", +"web.de", +"webshots.com", +"webtv.net", +"wegelin.ch", +"wellsfargo.com", +"wellsfargoemail.com", +"westernunion.ca", +"westernunion.com", +"westernunion.fr", +"westernunion.se", +"westpac.com.au", +"westpac.co.nz", +"wir.ch", +"wordpress.com", +"worldbank.org", +"worldpay.com", +"wsj.com", +"wvb.de", +"xmlsoap.org", +"yacht.nl", +"yahoo.ca", +"yahoo.co.jp", +"yahoo.co.kr", +"yahoo.com", +"yahoo.com.br", +"yahoo.co.uk", +"yahoogroups.com", +"yandex.net", +"yandex.ru", +"ybonline.co.uk", +"yimg.com", +"yopi.de", +"yorkshirebank.co.uk", +"yourbankcard.com", +"yoursite.com", +"youtube.com", +"zagbank.ca", +"zdnet.com", +"zenithbank.com", +"zkb.ch", +"zugerkb.ch", +"vistaprint.dk", +"vistaprint.com", +"anpdm.com", +"dovecot.org", +"exacttarget.com", +"github.com", +"isc.org", +"lists.isc.org", +"lists.roundcube.net", +"svn.apache.org", +"taggedmail.com", +"tumblr.com"} + +spam-spdk = {"1cfresh.com", +"4chan.org", +"6pm.com", +"about.com", +"addthis.com", +"adf.ly", +"adobe.com", +"adp.com", +"adschemist.com", +"airbnb.com", +"airtel.in", +"alibaba.com", +"aliexpress.com", +"alipay.com", +"allrecipes.com", +"amazon.ca", +"amazon.cn", +"amazon.co.jp", +"amazon.com", +"amazon.co.uk", +"amazon.de", +"amazon.es", +"amazon.fr", +"amazon.in", +"amazon.it", +"amazon.ru", +"americanexpress.com", +"ancestry.com", +"android.com", +"apple.com", +"asana.com", +"att.com", +"autohome.com.cn", +"avg.com", +"aweber.com", +"badoo.com", +"bankofamerica.com", +"basecamp.com", +"battle.net", +"bet365.com", +"biglobe.ne.jp", +"bitly.com", +"bleacherreport.com", +"blogger.com", +"bloomberg.com", +"booking.com", +"box.com", +"bt.com", +"capitalone.com", +"cdiscount.com", +"change.org", +"chase.com", +"cisco.com", +"citi.com", +"costco.com", +"craigslist.org", +"custhelp.com", +"dell.com", +"delta.com", +"diply.com", +"discovercard.com", +"disqus.com", +"dropbox.com", +"drweb.com", +"e-boks.dk", +"ebay.ca", +"ebay.com", +"ebay.com.au", +"ebay.co.uk", +"ebay.de", +"ebay.fr", +"ebay.in", +"ebay.it", +"ebay.ru", +"etsy.com", +"evernote.com", +"expedia.com", +"facebook.com", +"fedex.com", +"fidelity.com", +"fishki.net", +"flickr.com", +"flirchi.com", +"force.com", +"freepik.com", +"gap.com", +"gawker.com", +"github.com", +"gizmodo.com", +"godaddy.com", +"googleadservices.com", +"googleusercontent.com", +"groupon.com", +"hdfcbank.com", +"hgtv.com", +"hh.ru", +"hm.com", +"houzz.com", +"hubspot.com", +"icicibank.com", +"icloud.com", +"ign.com", +"imgur.com", +"immobilienscout24.de", +"indeed.com", +"indiatimes.com", +"infusionsoft.com", +"instagram.com", +"intel.com", +"irctc.co.in", +"kayak.com", +"kickstarter.com", +"kijiji.ca", +"kotaku.com", +"letsencrypt.org", +"libero.it", +"lifehacker.com", +"likes.com", +"linkedin.com", +"linux.com", +"list-manage.com", +"mackeeper.com", +"mailchimp.com", +"mashable.com", +"match.com", +"mercadolibre.com.ar", +"mercadolivre.com.br", +"messenger.com", +"microsoft.com", +"microsoftonline.com", +"minmyndighetspost.se", +"moikrug.ru", +"mts.ru", +"neobux.com", +"netflix.com", +"newegg.com", +"nhk.or.jp", +"nifty.com", +"nikkeibp.co.jp", +"nyaa.se", +"nytimes.com", +"odnoklassniki.ru", +"ofd.yandex.ru", +"ok.ru", +"olx.ua", +"overstock.com", +"ozon.ru", +"ozon.travel", +"pandora.com", +"paypal.ca", +"paypal.cn", +"paypal.com", +"paypal.co.uk", +"paypal.de", +"paypal.es", +"paypal.fr", +"paypal.it", +"paypal.ru", +"paytm.com", +"pch.com", +"pinterest.com", +"porn.com", +"priceline.com", +"quora.com", +"rakuten.co.jp", +"reddit.com", +"researchgate.net", +"salesforce.com", +"sciencedirect.com", +"shopify.com", +"skanetrafiken.se", +"skat.dk", +"skatteverket.se", +"slack.com", +"slideshare.net", +"so-net.ne.jp", +"southwest.com", +"spotify.com", +"springer.com", +"squarespace.com", +"stalker.com", +"steampowered.com", +"stumbleupon.com", +"surveymonkey.com", +"swagbucks.com", +"taboola.com", +"taleo.net", +"taobao.com", +"target.com", +"taringa.net", +"taxi.yandex.ru", +"tele2.ru", +"thekitchn.com", +"tochka.com", +"tokopedia.com", +"trello.com", +"tribunnews.com", +"trulia.com", +"tumblr.com", +"twitter.com", +"ultimate-guitar.com", +"ups.com", +"usaa.com", +"usbank.com", +"usps.com", +"verizon.com", +"verizonwireless.com", +"vimeo.com", +"vine.co", +"vk.com", +"vmware.com", +"vtb24.ru", +"wahoofitness.com", +"walmart.com", +"wav.tv", +"wellsfargo.com", +"whatsapp.com", +"wikia.com", +"wikimedia.org", +"wikipedia.org", +"wildberries.ru", +"wix.com", +"wordpress.com", +"wordpress.org", +"wp.com", +"xuite.net", +"xvideos.com", +"yelp.com", +"youtube.com", +"yts.to", +"zappos.com", +"zendesk.com", +"zippyshare.com", +"zomato.com", +"zoom.us", +"zulily.com", +"zwift.com"} + +spam-disposable = {"0815.ru", +"0clickemail.com", +"0wnd.net", +"0wnd.org", +"1054733.mail-temp.com", +"10m.email", +"10minutemail.com", +"10minutesmail.com", +"1secmail.com", +"1secmail.net", +"1secmail.org", +"20minutemail.com", +"2emailock.com", +"2prong.com", +"33mail.com", +"3d-magical-magnet.ru", +"4warding.com", +"90bit.ru", +"9ox.net", +"a-bc.net", +"afrobacon.com", +"alaki.ga", +"alivance.com", +"amilegit.com", +"amiri.net", +"anonymbox.com", +"antichef.com", +"antichef.net", +"antispam.de", +"audio.now.im", +"awex.icu", +"barenshop.ru", +"barryogorman.com", +"baxomale.ht.cx", +"beefmilk.com", +"binkmail.com", +"bio-muesli.net", +"bobmail.info", +"bofthew.com", +"brefmail.com", +"bsnow.net", +"bspamfree.org", +"bugmenot.com", +"casualdx.com", +"centermail.com", +"cetpass.com", +"chammy.info", +"choicemail1.com", +"choocho-telegram.ru", +"cool.fr.nf", +"courriel.fr.nf", +"courrieltemporaire.com", +"cuvox.de", +"dandikmail.com", +"dayrep.com", +"dcemail.com", +"deadspam.com", +"desoz.com", +"devnullmail.com", +"dfg6.kozow.com", +"dfgh.net", +"digitalsanctuary.com", +"dingbone.com", +"discardmail.com", +"discardmail.de", +"dispomail.win", +"dispomail.xyz", +"disposableaddress.com", +"disposeamail.com", +"dispostable.com", +"divismail.ru", +"dlesha.ru", +"dmaster39.ru", +"dodgit.com", +"domremonta-nv.ru", +"donemail.ru", +"dontreg.com", +"dontsendmespam.de", +"dumpandjunk.com", +"e-mail.com", +"e-mail.org", +"e4ward.com", +"edu.aiot.ze.cx", +"ekholotdeeper.ru", +"email-24x7.com", +"email60.com", +"emailate.com", +"emailay.com", +"emailias.com", +"emailmiser.com", +"emailsensei.com", +"emailtemporanea.net", +"emailtemporario.com.br", +"emailtex.com", +"emailwarden.com", +"emailx.at.hm", +"emailxfer.com", +"emz.net", +"exbts.com", +"explodemail.com", +"extremail.ru", +"eyeemail.com", +"fakeinbox.com", +"fakeinformation.com", +"fantasymail.de", +"filzmail.com", +"fls4.gleeze.com", +"fotosta.ru", +"frapmail.com", +"fudgerub.com", +"funny-mom.ru", +"furycraft.ru", +"garliclife.com", +"get2mail.fr", +"getonemail.com", +"gishpuppy.com", +"gnomi.ru", +"goplaygame.ru", +"greensloth.com", +"grr.la", +"gsrv.co.uk", +"guerillamail.com", +"guerrillamail.biz", +"guerrillamail.com", +"guerrillamail.de", +"guerrillamail.info", +"guerrillamail.net", +"guerrillamail.org", +"guerrillamailblock.com", +"h2ocoffe.ru", +"haltospam.com", +"hatespam.org", +"hidemail.de", +"hmamail.com", +"hochsitze.com", +"hulapla.de", +"hydraulics360.ru", +"i.xcode.ro", +"imails.info", +"inboxclean.com", +"inboxclean.org", +"irish2me.com", +"isdaq.com", +"iwi.net", +"jetable.com", +"jetable.fr.nf", +"jetable.net", +"jetable.org", +"justyland.ru", +"kasmail.com", +"kaspop.com", +"kemampuan.me", +"kikoxltd.com", +"killmail.com", +"killmail.net", +"kismail.ru", +"kkm35.ru", +"klassmaster.com", +"klzlk.com", +"koszmail.pl", +"kurzepost.de", +"kusrc.com", +"lackmail.ru", +"laoho.com", +"ldaho.biz", +"leeching.net", +"letthemeatspam.com", +"lhsdv.com", +"lifebyfood.com", +"lifeguru.online", +"light-marketing.ru", +"lol.ovpn.to", +"lookugly.com", +"lortemail.dk", +"lr78.com", +"madecassol78.ru", +"mail-temporaire.fr", +"mail.mezimages.net", +"mail333.com", +"mailbidon.com", +"mailblocks.com", +"mailbucket.org", +"mailcatch.com", +"maildrop.cc", +"maildx.com", +"mailed.ro", +"mailfreeonline.com", +"mailfs.com", +"mailin8r.com", +"mailinater.com", +"mailinator.com", +"mailinator.net", +"mailinator2.com", +"mailincubator.com", +"mailme.ir", +"mailme.lv", +"mailmetal.com", +"mailmetrash.com", +"mailmoat.com", +"mailnesia.com", +"mailnull.com", +"mailscrap.com", +"mailshell.com", +"mailsiphon.com", +"mailsoul.com", +"mailtrash.net", +"mailzilla.com", +"makemetheking.com", +"mbx.cc", +"mega.zik.dj", +"meinspamschutz.de", +"meltmail.com", +"messagebeamer.de", +"mineblue.ru", +"mintemail.com", +"misha-rosestoy.ru", +"moboinfo.xyz", +"moncourrier.fr.nf", +"monemail.fr.nf", +"monmail.fr.nf", +"mor19.uu.gl", +"mt2009.com", +"mvrht.com", +"mycleaninbox.net", +"mymail-in.net", +"mypartyclip.de", +"myphantomemail.com", +"mytempemail.com", +"mytrashmail.com", +"neomailbox.com", +"nepwk.com", +"nervmich.net", +"nervtmich.net", +"netmails.com", +"netmails.net", +"neverbox.com", +"newfilm24.ru", +"niepodam.pl", +"no-spam.ws", +"nomail.xl.cx", +"nomorespamemails.com", +"nospam.ze.tc", +"nospam4.us", +"nospammail.net", +"notmailinator.com", +"notsharingmy.info", +"nowmymail.com", +"nurfuerspam.de", +"objectmail.com", +"obobbo.com", +"officialrolex.ru", +"oneoffemail.com", +"onewaymail.com", +"onlinenet.info", +"oopi.org", +"ordinaryamerican.net", +"otherinbox.com", +"ovpn.to", +"owlpic.com", +"p33.org", +"pancakemail.com", +"partner1bizmoney.ru", +"pchelovodstvo-tut.ru", +"piratesdelivery.ru", +"pmlep.de", +"politikerclub.de", +"pookmail.com", +"powerbank-russia.ru", +"privacy.net", +"proxymail.eu", +"prtnx.com", +"putthisinyourspamdatabase.com", +"quickinbox.com", +"razinrocks.me", +"rcpt.at", +"reallymymail.com", +"recode.me", +"reconmail.com", +"recursor.net", +"reloadpoint.ru", +"rtrtr.com", +"s-sakamas.ru", +"s0ny.net", +"safe-mail.net", +"safersignup.de", +"safetymail.info", +"safetypost.de", +"samogonda.ru", +"sendspamhere.com", +"senseless-entertainment.com", +"sgbteamreborn.imouto.pro", +"shar-kov.ru", +"sharklasers.com", +"shiftmail.com", +"shitmail.me", +"shortmail.net", +"sibmail.com", +"slaskpost.se", +"smellfear.com", +"sneakemail.com", +"sofimail.com", +"sogetthis.com", +"soodonims.com", +"spam4.me", +"spambob.net", +"spambog.com", +"spambog.de", +"spambog.ru", +"spambooger.com", +"spambox.us", +"spambox.win", +"spambox.xyz", +"spamcannon.com", +"spamcannon.net", +"spamcon.org", +"spamcorptastic.com", +"spamcowboy.com", +"spamcowboy.net", +"spamcowboy.org", +"spamday.com", +"spamex.com", +"spamfree.eu", +"spamfree24.com", +"spamfree24.de", +"spamfree24.org", +"spamgourmet.com", +"spamgourmet.net", +"spamgourmet.org", +"spamhereplease.com", +"spamhole.com", +"spamify.com", +"spaml.de", +"spammotel.com", +"spamobox.com", +"spamslicer.com", +"spamthis.co.uk", +"speed.1s.fr", +"squizzy.net", +"sskstroy.ru", +"streetwisemail.com", +"super-auswahl.de", +"supermailer.jp", +"suremail.info", +"tecninja.xyz", +"teewars.org", +"teleosaurs.xyz", +"teleworm.com", +"temp-mail.org", +"tempe-mail.com", +"tempemail.com", +"tempemail.net", +"tempinbox.co.uk", +"tempinbox.com", +"tempmail.it", +"tempmail.top", +"tempmail.win", +"tempomail.fr", +"thankyou2010.com", +"thisisnotmyrealemail.com", +"thrott.com", +"throwawayemailaddress.com", +"tilien.com", +"titaspaharpur1.gq", +"tmailinator.com", +"tradermail.info", +"trash-mail.at", +"trash-mail.com", +"trash-mail.de", +"trash2009.com", +"trashdevil.com", +"trashemail.de", +"trashinbox.net", +"trashmail.at", +"trashmail.com", +"trashmail.de", +"trashmail.me", +"trashmail.net", +"trashmail.org", +"trashmail.ws", +"trashmailer.com", +"trashymail.com", +"trbvm.com", +"trbvn.com", +"trillianpro.com", +"tvchd.com", +"twinmail.de", +"tyldd.com", +"uggsrock.com", +"upliftnow.com", +"urhen.com", +"ussv.club", +"vapecentral.ru", +"venompen.com", +"veryrealemail.com", +"vkusup.ru", +"voemail.com", +"vssms.com", +"weammo.xyz", +"webcool.club", +"wegwerfadresse.de", +"wegwerfemail.com", +"wegwerfemail.de", +"wegwerfmail.de", +"wegwerfmail.net", +"wegwerfmail.org", +"wh4f.org", +"whyspam.me", +"willhackforfood.biz", +"willselfdestruct.com", +"wronghead.com", +"wwwnew.eu", +"xemaps.com", +"xitroo.com", +"xmaily.com", +"xoxy.net", +"xww.ro", +"yep.it", +"yevme.com", +"yopmail.com", +"yopmail.fr", +"yopmail.net", +"yuurok.com", +"zagorodnyi-domik.ru", +"zdorovpagh.ru", +"zehnminutenmail.de", +"zippymail.info", +"zoemail.net"} + +spam-free = {"0-mail.com", +"027168.com", +"0815.su", +"0sg.net", +"10mail.org", +"10minutemail.co.za", +"123.com", +"123india.com", +"123mail.cl", +"123mail.org", +"126.com", +"139.com", +"150mail.com", +"150ml.com", +"15meg4free.com", +"163.com", +"16mail.com", +"188.com", +"189.cn", +"1ce.us", +"1chuan.com", +"1funplace.com", +"1internetdrive.com", +"1mail.net", +"1me.net", +"1mum.com", +"1musicrow.com", +"1pad.de", +"1zhuan.com", +"2-mail.com", +"20email.eu", +"20mail.in", +"20mail.it", +"212.com", +"21cn.com", +"24horas.com", +"2980.com", +"2bmail.co.uk", +"2die4.com", +"2trom.com", +"3126.com", +"321media.com", +"37.com", +"3ammagazine.com", +"3dmail.com", +"3g.ua", +"3mail.ga", +"444.net", +"4email.net", +"4mg.com", +"4warding.net", +"4x4man.com", +"50mail.com", +"5iron.com", +"60minutemail.com", +"6ip.us", +"74.ru", +"7mail.ml", +"88.am", +"8mail.ml", +"97rock.com", +"99experts.com", +"9online.fr", +"a1.net", +"a45.in", +"aaamail.zzn.com", +"aapt.net.au", +"aaronkwok.net", +"abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijk.com", +"abcflash.net", +"abdulnour.com", +"aberystwyth.com", +"about.com", +"abv.bg", +"abwesend.de", +"abyssmail.com", +"acceso.or.cr", +"access4less.net", +"accountant.com", +"acdcfan.com", +"acmemail.net", +"acninc.net", +"activist.com", +"adam.com.au", +"add3000.pp.ua", +"addcom.de", +"address.com", +"adelphia.net", +"adexec.com", +"adfarrow.com", +"adoption.com", +"ados.fr", +"adrenalinefreak.com", +"advalvas.be", +"advantimo.com", +"aeiou.pt", +"africamail.com", +"africamel.net", +"agoodmail.com", +"ahaa.dk", +"ahk.jp", +"aichi.com", +"aim.com", +"aircraftmail.com", +"airforce.net", +"airforceemail.com", +"airpost.net", +"ajacied.com", +"ak47.hu", +"aknet.kg", +"albawaba.com", +"algeria.com", +"alhilal.net", +"alibaba.com", +"alice.it", +"aliyun.com", +"allergist.com", +"allmail.net", +"allracing.com", +"allsaintsfan.com", +"alpenjodel.de", +"alphafrau.de", +"alskens.dk", +"alternativagratis.com", +"alumni.com", +"alumnidirector.com", +"alvilag.hu", +"amail.com", +"amele.com", +"america.hm", +"ameritech.net", +"amnetsal.com", +"amorki.pl", +"amrer.net", +"amuro.net", +"amuromail.com", +"ananzi.co.za", +"ancestry.com", +"andylau.net", +"angelfire.com", +"angelic.com", +"animail.net", +"animalhouse.com", +"anjungcafe.com", +"annsmail.com", +"ano-mail.net", +"anonymous.to", +"anote.com", +"another.com", +"anotherdomaincyka.tk", +"anotherwin95.com", +"anti-social.com", +"antisocial.com", +"antispam24.de", +"anymoment.com", +"anytimenow.com", +"aol.co.uk", +"aol.com", +"aol.fr", +"aon.at", +"apexmail.com", +"apmail.com", +"apollo.lv", +"aport.ru", +"aport2000.ru", +"appraiser.net", +"arabia.com", +"arabtop.net", +"archaeologist.com", +"arcor.de", +"arcticmail.com", +"argentina.com", +"army.net", +"armyspy.com", +"arnet.com.ar", +"artlover.com", +"artlover.com.au", +"asdasd.nl", +"asean-mail.com", +"asheville.com", +"asia-links.com", +"asia-mail.com", +"asia.com", +"asiafind.com", +"asianavenue.com", +"asiancityweb.com", +"asiansonly.net", +"asianwired.net", +"asiapoint.net", +"ass.pp.ua", +"assala.com", +"assamesemail.com", +"astrolover.com", +"astrosfan.net", +"asurfer.com", +"atheist.com", +"athenachu.net", +"atina.cl", +"atozasia.com", +"atrus.ru", +"att.net", +"attglobal.net", +"attymail.com", +"au.ru", +"auctioneer.net", +"ausi.com", +"aussiemail.com.au", +"austin.rr.com", +"australia.edu", +"australiamail.com", +"autoescuelanerja.com", +"autograf.pl", +"autorambler.ru", +"aver.com", +"avh.hu", +"awsom.net", +"axoskate.com", +"azazazatashkent.tk", +"azmeil.tk", +"bachelorboy.com", +"bachelorgal.com", +"backpackers.com", +"backstreet-boys.com", +"bagherpour.com", +"baldmama.de", +"baldpapa.de", +"ballyfinance.com", +"bangkok.com", +"bangkok2000.com", +"bannertown.net", +"baptistmail.com", +"baptized.com", +"barcelona.com", +"bareed.ws", +"bartender.net", +"baseballmail.com", +"basketballmail.com", +"batuta.net", +"bboy.zzn.com", +"bcvibes.com", +"beddly.com", +"beeebank.com", +"beenhad.com", +"beep.ru", +"beer.com", +"beethoven.com", +"belice.com", +"bell.net", +"bellair.net", +"bellsouth.net", +"berlin.com", +"berlin.de", +"bestmail.us", +"betriebsdirektor.de", +"bettergolf.net", +"bharatmail.com", +"bigassweb.com", +"bigblue.net.au", +"bigfoot.com", +"bigfoot.de", +"bigger.com", +"biggerbadder.com", +"bigmailbox.com", +"bigmir.net", +"bigpond.com", +"bigpond.com.au", +"bigpond.net.au", +"bigstring.com", +"bikemechanics.com", +"bikeracer.com", +"bikerider.com", +"billsfan.com", +"billsfan.net", +"bin-wieder-da.de", +"birdlover.com", +"bisons.com", +"bitmail.com", +"bitpage.net", +"bizhosting.com", +"bk.ru", +"blackplanet.com", +"blader.com", +"bladesmail.net", +"blazemail.com", +"bleib-bei-mir.de", +"blockfilter.com", +"blogmyway.org", +"bluebottle.com", +"bluemail.ch", +"bluemail.dk", +"blushmail.com", +"boardermail.com", +"bodhi.lawlita.com", +"bol.com.br", +"bolando.com", +"bolt.com", +"boltonfans.com", +"bombdiggity.com", +"bootybay.de", +"boun.cr", +"bounce.net", +"bouncr.com", +"box.az", +"box.ua", +"boxemail.com", +"boxformail.in", +"boxfrog.com", +"boximail.com", +"bradfordfans.com", +"brasilia.net", +"brazilmail.com", +"breathe.com", +"brennendesreich.de", +"bresnan.net", +"brew-master.com", +"brew-meister.com", +"briefemail.com", +"bright.net", +"britneyclub.com", +"broadcast.net", +"brokenvalve.com", +"brusseler.com", +"bsdmail.com", +"btcmail.pw", +"btinternet.com", +"buerotiger.de", +"bullsfan.com", +"bumpymail.com", +"bund.us", +"burnthespam.info", +"burstmail.info", +"businessman.net", +"buyersusa.com", +"bvimailbox.com", +"byom.de", +"c2.hu", +"c3.hu", +"c4.com", +"cabacabana.com", +"cableone.net", +"caere.it", +"calidifontain.be", +"californiamail.com", +"callnetuk.com", +"callsign.net", +"caltanet.it", +"camidge.com", +"canada-11.com", +"canadianmail.com", +"canoemail.com", +"caramail.com", +"caramail.fr", +"care2.com", +"careerbuildermail.com", +"carioca.net", +"cartestraina.ro", +"casablancaresort.com", +"casema.nl", +"cash4u.com", +"casino.com", +"catchamail.com", +"catholic.org", +"catlover.com", +"cd2.com", +"cegetel.net", +"celineclub.com", +"celtic.com", +"center-mail.de", +"centermail.at", +"centermail.de", +"centermail.info", +"centoper.it", +"centralpets.com", +"centrum.cz", +"centrum.sk", +"centurytel.net", +"certifiedmail.com", +"cfl.rr.com", +"cgac.es", +"chacuo.net", +"chaiyomail.com", +"chammy.info", +"chandrasekar.net", +"charmedmail.com", +"charter.com", +"charter.net", +"chat.ru", +"chattown.com", +"cheatmail.de", +"chechnya.conf.work", +"check.com", +"check1check.com", +"cheerful.com", +"chef.net", +"chek.com", +"chello.nl", +"chemist.com", +"cheyenneweb.com", +"chez.com", +"china.com", +"chinamail.com", +"chirk.com", +"chocaholic.com.au", +"chong-mail.net", +"churchusa.com", +"cia-agent.com", +"cia.hu", +"cicciociccio.com", +"cincinow.net", +"citiz.net", +"citlink.net", +"city-of-birmingham.com", +"city-of-cambridge.com", +"city-of-edinburgh.com", +"city-of-lincoln.com", +"city-of-liverpool.com", +"city-of-manchester.com", +"city-of-oxford.com", +"city-of-swansea.com", +"city-of-westminster.com", +"city-of-westminster.net", +"city-of-york.net", +"cityoflondon.org", +"ckaazaza.tk", +"claramail.com", +"classicalfan.com", +"classicmail.co.za", +"clerk.com", +"cliffhanger.com", +"clixser.com", +"close2you.net", +"clrmail.com", +"club4x4.net", +"clubalfa.com", +"clubbers.net", +"clubducati.com", +"clubhonda.net", +"club-internet.fr", +"clubinternet.fr", +"clubmember.org", +"clubnetnoir.com", +"clubvdo.net", +"cluemail.com", +"cmail.net", +"cmpmail.com", +"cnnsimail.com", +"cntv.cn", +"codec.ro", +"coder.hu", +"coid.biz", +"coldmail.com", +"collectiblesuperstore.com", +"collector.org", +"collegeclub.com", +"collegemail.com", +"colleges.com", +"columbus.rr.com", +"columbusrr.com", +"columnist.com", +"comcast.net", +"comic.com", +"communityconnect.com", +"comprendemail.com", +"compuserve.com", +"computer4u.com", +"computermail.net", +"conexcol.com", +"conk.com", +"connect4free.net", +"consultant.com", +"consumerriot.com", +"contractor.net", +"coole-files.de", +"coolgoose.ca", +"coolgoose.com", +"coolkiwi.com", +"coolmail.com", +"coolmail.net", +"coolsite.net", +"cooperation.net", +"cooperationtogo.net", +"copacabana.com", +"cornells.com", +"corporatedirtbag.com", +"cotas.net", +"counsellor.com", +"cox.com", +"cox.net", +"coxinet.net", +"cracker.hu", +"crapmail.org", +"crazedanddazed.com", +"crazymailing.com", +"cristianemail.com", +"critterpost.com", +"croeso.com", +"crosshairs.com", +"crosswinds.net", +"crwmail.com", +"cs.com", +"csinibaba.hu", +"cuemail.com", +"curio-city.com", +"curryworld.de", +"cute-girl.com", +"cutey.com", +"cyber-africa.net", +"cyber-innovation.club", +"cyber-matrix.com", +"cyber-wizard.com", +"cyber4all.com", +"cyberbabies.com", +"cybercafemaui.com", +"cyberdude.com", +"cybergal.com", +"cybergrrl.com", +"cybermail.net", +"cybernet.it", +"cyberservices.com", +"cyberspace-asia.com", +"cybertrains.org", +"cyclefanz.com", +"cynetcity.com", +"dabsol.net", +"dadacasa.com", +"dailypioneer.com", +"dallasmail.com", +"dangerous-minds.com", +"dasdasdascyka.tk", +"dawnsonmail.com", +"dawsonmail.com", +"dbzmail.com", +"deadlymob.org", +"deagot.com", +"deal-maker.com", +"dearriba.com", +"death-star.com", +"deliveryman.com", +"deneg.net", +"depechemode.com", +"deseretmail.com", +"desilota.com", +"deskpilot.com", +"destin.com", +"detik.com", +"deutschland-net.com", +"devotedcouples.com", +"dezigner.ru", +"dfwatson.com", +"di-ve.com", +"die-besten-bilder.de", +"die-genossen.de", +"die-optimisten.net", +"diemailbox.de", +"digibel.be", +"digital-filestore.de", +"diplomats.com", +"directbox.com", +"dirtracer.com", +"discard.email", +"discard.ga", +"discard.gq", +"disciples.com", +"discofan.com", +"discovery.com", +"discoverymail.com", +"disign-revelation.com", +"dispomail.eu", +"disposable.com", +"dispose.it", +"dm.w3internet.co.uk", +"dnsmadeeasy.com", +"docmail.cz", +"doctor.com", +"dodo.com.au", +"dodsi.com", +"dog.com", +"dogit.com", +"doglover.com", +"dogsnob.net", +"doityourself.com", +"domforfb1.tk", +"domforfb2.tk", +"domforfb3.tk", +"domforfb4.tk", +"domforfb5.tk", +"domforfb6.tk", +"domforfb8.tk", +"domozmail.com", +"doneasy.com", +"dontgotmail.com", +"dontmesswithtexas.com", +"doramail.com", +"dostmail.com", +"dotcom.fr", +"dotmsg.com", +"dott.it", +"download-privat.de", +"dplanet.ch", +"dr.com", +"dropmail.me", +"dropzone.com", +"drotposta.hu", +"dublin.com", +"dublin.ie", +"dumpmail.com", +"dumpmail.de", +"dumpyemail.com", +"dunlopdriver.com", +"dunloprider.com", +"duno.com", +"duskmail.com", +"dutchmail.com", +"dwp.net", +"dygo.com", +"dyndns.org", +"e-apollo.lv", +"e-mail.com.tr", +"e-mail.dk", +"e-mail.ru", +"e-mail.ua", +"e-mailanywhere.com", +"e-tapaal.com", +"earthalliance.com", +"earthcam.net", +"earthdome.com", +"earthling.net", +"earthlink.net", +"earthonline.net", +"eastcoast.co.za", +"eastmail.com", +"easy.to", +"easypost.com", +"easytrashmail.com", +"ecardmail.com", +"echina.com", +"ecompare.com", +"edmail.com", +"edtnmail.com", +"educacao.te.pt", +"eelmail.com", +"ehmail.com", +"einrot.com", +"eintagsmail.de", +"eircom.net", +"elitemail.org", +"elvis.com", +"elvisfan.com", +"email-fake.gq", +"email-london.co.uk", +"email.biz", +"email.com", +"email.cz", +"email.ee", +"email.it", +"email.nu", +"email.org", +"email.ro", +"email.ru", +"email.su", +"email.ua", +"email2me.net", +"emailacc.com", +"emailaccount.com", +"emailasso.net", +"emailchoice.com", +"emailcorner.net", +"emailem.com", +"emailengine.net", +"emailengine.org", +"emailgo.de", +"emailgroups.net", +"emailinfive.com", +"emailit.com", +"emailplanet.com", +"emailplus.org", +"emailto.de", +"emailuser.net", +"emailx.net", +"embarqmail.com", +"emeil.in", +"emeil.ir", +"emil.com", +"eml.cc", +"eml.pp.ua", +"enel.net", +"engineer.com", +"england.com", +"england.edu", +"englandmail.com", +"epage.ru", +"ephemail.net", +"epix.net", +"eposta.hu", +"eramail.co.za", +"eresmas.com", +"eriga.lv", +"estranet.it", +"ethos.st", +"eudoramail.com", +"europamel.net", +"europe.com", +"europemail.com", +"euroseek.com", +"eurosport.com", +"every1.net", +"everyday.com.kh", +"everymail.net", +"everyone.net", +"everytg.ml", +"examnotes.net", +"excite.co.jp", +"excite.com", +"excite.it", +"execs.com", +"exemail.com.au", +"expressasia.com", +"extenda.net", +"eyepaste.com", +"ezcybersearch.com", +"ezrs.com", +"f-m.fm", +"f1fans.net", +"facebook.com", +"fahr-zur-hoelle.org", +"fake-email.pp.ua", +"fake-mail.cf", +"falseaddress.com", +"fan.com", +"fansonlymail.com", +"fantasticmail.com", +"farang.net", +"faroweb.com", +"fast-email.com", +"fast-mail.fr", +"fast-mail.org", +"fastchevy.com", +"fastem.com", +"fastemail.us", +"fastemailer.com", +"fastermail.com", +"fastest.cc", +"fastimap.com", +"fastmail.ca", +"fastmail.cn", +"fastmail.co.uk", +"fastmail.com", +"fastmail.com.au", +"fastmail.es", +"fastmail.fm", +"fastmail.im", +"fastmail.in", +"fastmail.jp", +"fastmail.mx", +"fastmail.net", +"fastmail.nl", +"fastmail.se", +"fastmail.to", +"fastmail.tw", +"fastmail.us", +"fastmailbox.net", +"fastmazda.com", +"fastmessaging.com", +"fastservice.com", +"fastsubaru.com", +"fatflap.com", +"fathersrightsne.org", +"fax.ru", +"fbi.hu", +"fea.st", +"federalcontractors.com", +"feinripptraeger.de", +"felicitymail.com", +"femenino.com", +"fetchmail.co.uk", +"fettabernett.de", +"feyenoorder.com", +"ffanet.com", +"fiberia.com", +"ficken.de", +"fightallspam.com", +"filipinolinks.com", +"financemail.net", +"financier.com", +"findmail.com", +"fire-brigade.com", +"fireman.net", +"fishburne.org", +"fishfuse.com", +"fixmail.tk", +"fizmail.com", +"flashbox.5july.org", +"flashmail.com", +"flashmail.net", +"fleckens.hu", +"flipcode.com", +"fmail.co.uk", +"fmailbox.com", +"fmgirl.com", +"fmguy.com", +"fnbmail.co.za", +"fnmail.com", +"folkfan.com", +"foodmail.com", +"footard.com", +"football.ua", +"footballmail.com", +"for-president.com", +"force9.co.uk", +"forgetmail.com", +"forpresident.com", +"fortuncity.com", +"fortunecity.com", +"forum.dk", +"foxmail.com", +"fr33mail.info", +"francemel.fr", +"free-online.net", +"free-org.com", +"free.com.pe", +"free.fr", +"freeaccess.nl", +"freeaccount.com", +"freeandsingle.com", +"freedom.usa.com", +"freedomlover.com", +"freegates.be", +"freelance-france.eu", +"freeler.nl", +"freemail.c3.hu", +"freemail.com.pk", +"freemail.de", +"freemail.et", +"freemail.gr", +"freemail.hu", +"freemail.it", +"freemail.lt", +"freemail.org.mk", +"freemails.ga", +"freemeil.gq", +"freenet.de", +"freenet.kg", +"freeola.com", +"freeola.net", +"freestart.hu", +"freesurf.fr", +"freesurf.nl", +"freeuk.com", +"freeuk.net", +"freeukisp.co.uk", +"freeweb.org", +"freewebemail.com", +"freeyellow.com", +"freezone.co.uk", +"fresnomail.com", +"freudenkinder.de", +"freundin.ru", +"friendlymail.co.uk", +"friendsfan.com", +"from-africa.com", +"from-australia.com", +"from-europe.com", +"from-holland.com", +"from-japan.net", +"from-mexico.com", +"from-outerspace.com", +"from-russia.com", +"fromalaska.com", +"fromarizona.com", +"fromarkansas.com", +"fromcalifornia.com", +"fromconnecticut.com", +"fromgeorgia.com", +"fromhawaii.net", +"fromidaho.com", +"fromindiana.com", +"fromiowa.com", +"fromkansas.com", +"fromlouisiana.com", +"frommaryland.com", +"frommassachusetts.com", +"frommiami.com", +"frommichigan.com", +"fromminnesota.com", +"frommississippi.com", +"frommissouri.com", +"fromnevada.com", +"fromnewhampshire.com", +"fromnewjersey.com", +"fromnewmexico.com", +"fromnorthcarolina.com", +"fromnorthdakota.com", +"fromohio.com", +"fromoklahoma.com", +"frompennsylvania.com", +"fromrhodeisland.com", +"fromru.com", +"fromsouthcarolina.com", +"fromtennessee.com", +"fromtexas.com", +"fromutah.com", +"fromvermont.com", +"fromvirginia.com", +"fromwashington.com", +"fromwashingtondc.com", +"fromwestvirginia.com", +"fromwisconsin.com", +"fromwyoming.com", +"front.ru", +"frontier.com", +"frontiernet.net", +"fsmail.net", +"ftml.net", +"fullmail.com", +"fuorissimo.com", +"furnitureprovider.com", +"fuse.net", +"fut.es", +"galaxyhit.com", +"gamebox.net", +"gamegeek.com", +"gamespotmail.com", +"garbage.com", +"gardener.com", +"gaybrighton.co.uk", +"gaza.net", +"gazeta.pl", +"gazibooks.com", +"gci.net", +"geek.com", +"geeklife.com", +"gelitik.in", +"gencmail.com", +"gentlemansclub.de", +"geocities.com", +"geography.net", +"geologist.com", +"geopia.com", +"germanymail.com", +"get.pp.ua", +"get1mail.com", +"getairmail.com", +"getairmail.gq", +"getonemail.net", +"ghanamail.com", +"ghostmail.com", +"ghosttexter.de", +"gigileung.org", +"girl4god.com", +"glay.org", +"glendale.net", +"globalfree.it", +"globalpagan.com", +"gmail.com", +"gmail.com.br", +"gmx.at", +"gmx.biz", +"gmx.ch", +"gmx.co.uk", +"gmx.com", +"gmx.de", +"gmx.eu", +"gmx.fr", +"gmx.info", +"gmx.li", +"gmx.net", +"gmx.org", +"gmx.us", +"go.com", +"go.ro", +"go.ru", +"gocollege.com", +"gocubs.com", +"goldmail.ru", +"goldtoolbox.com", +"golfemail.com", +"golfilla.info", +"golfmail.be", +"gonavy.net", +"goodnewsmail.com", +"goodstick.com", +"googlemail.com", +"goplay.com", +"gorillaswithdirtyarmpits.com", +"gospelfan.com", +"gotmail.com", +"gotmail.org", +"gotomy.com", +"gotti.otherinbox.com", +"gportal.hu", +"graduate.org", +"graffiti.net", +"gramszu.net", +"grandmamail.com", +"graphic-designer.com", +"grapplers.com", +"greenmail.net", +"groupmail.com", +"grr.la", +"gtmc.net", +"gua.net", +"guessmail.com", +"guju.net", +"gustr.com", +"guy.com", +"guy2.com", +"guyanafriends.com", +"h-mail.us", +"hab-verschlafen.de", +"habmalnefrage.de", +"hacccc.com", +"hackermail.com", +"hackermail.net", +"hailmail.net", +"hairdresser.net", +"hamptonroads.com", +"handbag.com", +"handleit.com", +"hanmail.net", +"happemail.com", +"happycounsel.com", +"happypuppy.com", +"harakirimail.com", +"hardcorefreak.com", +"hartbot.de", +"hawaii.rr.com", +"hawaiiantel.net", +"heerschap.com", +"heesun.net", +"hello.hu", +"hello.net.au", +"hello.to", +"helter-skelter.com", +"herediano.com", +"herp.in", +"herr-der-mails.de", +"hetnet.nl", +"hey.to", +"hidzz.com", +"highquality.com", +"highveldmail.co.za", +"hilarious.com", +"hiphopfan.com", +"hispavista.com", +"hitmail.com", +"hitthe.net", +"hkg.net", +"hockeymail.com", +"hollywoodkids.com", +"home-email.com", +"home.de", +"home.nl", +"home.ro", +"home.se", +"homemail.com", +"homestead.com", +"honduras.com", +"hongkong.com", +"hoopsmail.com", +"hopemail.biz", +"hot-shot.com", +"hot.ee", +"hotbrev.com", +"hotletter.com", +"hotmail.ca", +"hotmail.ch", +"hotmail.co.il", +"hotmail.co.uk", +"hotmail.com", +"hotmail.de", +"hotmail.es", +"hotmail.fr", +"hotmail.it", +"hotmail.kz", +"hotmail.nl", +"hotmail.ru", +"hotpop3.com", +"hotvoice.com", +"housemail.com", +"hsuchi.net", +"hu2.ru", +"hughes.net", +"humanoid.net", +"hunsa.com", +"hurting.com", +"hush.com", +"hushmail.com", +"hypernautica.com", +"i-connect.com", +"i-mail.com.au", +"i-p.com", +"i.am", +"i.ua", +"i2pmail.org", +"iamawoman.com", +"icestorm.com", +"ich-bin-verrueckt-nach-dir.de", +"ich-will-net.de", +"icloud.com", +"icmsconsultants.com", +"icq.com", +"icqmail.com", +"icrazy.com", +"idirect.com", +"ieh-mail.de", +"iespana.es", +"ig.com.br", +"ihateclowns.com", +"iinet.net.au", +"ijustdontcare.com", +"ikbenspamvrij.nl", +"ilkposta.com", +"ilovechocolate.com", +"ilovejesus.com", +"ilse.nl", +"imaginemail.com", +"imail.org", +"imail.ru", +"imap-mail.com", +"imap.cc", +"imapmail.org", +"imel.org", +"imgof.com", +"imgv.de", +"immo-gerance.info", +"imposter.co.uk", +"imstations.com", +"imstressed.com", +"in-box.net", +"iname.com", +"inbax.tk", +"inbox.com", +"inbox.net", +"inbox.ru", +"inbox.si", +"inboxalias.com", +"incamail.com", +"incredimail.com", +"index.ua", +"indexa.fr", +"india.com", +"indiatimes.com", +"indo-mail.com", +"indomail.com", +"indyracers.com", +"inerted.com", +"info-media.de", +"info-radio.ml", +"info66.com", +"infohq.com", +"infomail.es", +"infomart.or.jp", +"infospacemail.com", +"infovia.com.ar", +"inicia.es", +"inmail.sk", +"inmail24.com", +"inmano.com", +"inmynetwork.tk", +"innocent.com", +"inorbit.com", +"inoutbox.com", +"insidebaltimore.net", +"insight.rr.com", +"instantemailaddress.com", +"instantmail.fr", +"instruction.com", +"instructor.net", +"insurer.com", +"interburp.com", +"interfree.it", +"interia.pl", +"interlap.com.ar", +"intermail.co.il", +"internet-e-mail.com", +"internet-mail.org", +"internet-police.com", +"internetegypt.com", +"internetemails.net", +"internetmailing.net", +"internode.on.net", +"inwind.it", +"iobox.com", +"iobox.fi", +"iol.it", +"iowaemail.com", +"ip4.pp.ua", +"ip6.pp.ua", +"ipoo.org", +"iprimus.com.au", +"iqemail.com", +"irangate.net", +"ireland.com", +"irelandmail.com", +"irj.hu", +"iroid.com", +"isellcars.com", +"iservejesus.com", +"islamonline.net", +"isleuthmail.com", +"ismart.net", +"isp9.net", +"israelmail.com", +"ist-allein.info", +"ist-einmalig.de", +"ist-ganz-allein.de", +"ist-willig.de", +"italymail.com", +"itmom.com", +"ivebeenframed.com", +"iwmail.com", +"iwon.com", +"izadpanah.com", +"jahoopa.com", +"jakuza.hu", +"jazzandjava.com", +"jazzfan.com", +"je-recycle.info", +"jerusalemmail.com", +"jetable.de", +"jetable.pp.ua", +"jetemail.net", +"jippii.fi", +"jmail.co.za", +"job4u.com", +"jokes.com", +"journalist.com", +"jourrapide.com", +"jovem.te.pt", +"jpopmail.com", +"jsrsolutions.com", +"jubiimail.dk", +"juniormail.com", +"junkmail.com", +"juno.com", +"justemail.net", +"justicemail.com", +"kaazoo.com", +"kaffeeschluerfer.com", +"kaffeeschluerfer.de", +"kaixo.com", +"kalpoint.com", +"kansascity.com", +"karbasi.com", +"katamail.com", +"kayafmmail.co.za", +"kbjrmail.com", +"kcks.com", +"keg-party.com", +"keinpardon.de", +"keko.com.ar", +"kellychen.com", +"keromail.com", +"keyemail.com", +"kgb.hu", +"kickassmail.com", +"killermail.com", +"kimo.com", +"kinglibrary.net", +"kinki-kids.com", +"kissfans.com", +"kittymail.com", +"kitznet.at", +"kiwitown.com", +"km.ru", +"knol-power.nl", +"kommespaeter.de", +"konx.com", +"korea.com", +"koreamail.com", +"kpnmail.nl", +"krongthip.com", +"krunis.com", +"ksanmail.com", +"ksee24mail.com", +"kukamail.com", +"kulturbetrieb.info", +"kumarweb.com", +"la.com", +"ladymail.cz", +"lagerlouts.com", +"lags.us", +"lakmail.com", +"lamer.hu", +"land.ru", +"lankamail.com", +"laoeq.com", +"laposte.net", +"lass-es-geschehen.de", +"lastmail.co", +"latemodels.com", +"lavache.com", +"law.com", +"lawyer.com", +"lazyinbox.com", +"leehom.net", +"legalrc.loan", +"legislator.com", +"lenta.ru", +"leonlai.net", +"letsgomets.net", +"letterboxes.org", +"letthemeatspam.com", +"levele.hu", +"lex.bg", +"lexis-nexis-mail.com", +"libero.it", +"liberomail.com", +"lick101.com", +"liebt-dich.info", +"linktrader.com", +"linuxfreemail.com", +"linuxmail.org", +"liontrucks.com", +"liquidinformation.net", +"list.ru", +"listomail.com", +"littleapple.com", +"littleblueroom.com", +"live.at", +"live.ca", +"live.cl", +"live.cn", +"live.co.uk", +"live.co.za", +"live.com", +"live.com.ar", +"live.com.au", +"live.com.mx", +"live.com.pt", +"live.com.sg", +"live.de", +"live.dk", +"live.fr", +"live.ie", +"live.in", +"live.it", +"live.jp", +"live.nl", +"live.ru", +"live.se", +"liveradio.tk", +"liverpoolfans.com", +"llandudno.com", +"llangollen.com", +"lobbyist.com", +"localbar.com", +"locos.com", +"loh.pp.ua", +"lolfreak.net", +"london.com", +"looksmart.co.uk", +"looksmart.com", +"lopezclub.com", +"louiskoo.com", +"loveable.com", +"lovecat.com", +"lovefall.ml", +"lovefootball.com", +"lovemail.com", +"lover-boy.com", +"lovesea.gq", +"lovethebroncos.com", +"loveyouforever.de", +"lovingjesus.com", +"lowandslow.com", +"lroid.com", +"luukku.com", +"lycos.co.uk", +"lycos.com", +"lycos.es", +"lycos.ne.jp", +"m-hmail.com", +"m4.org", +"mac.com", +"macbox.com", +"macfreak.com", +"macmail.com", +"madonnafan.com", +"maennerversteherin.com", +"maennerversteherin.de", +"maffia.hu", +"magicmail.co.za", +"mail-awu.de", +"mail-box.cz", +"mail-center.com", +"mail-central.com", +"mail-easy.fr", +"mail-filter.com", +"mail-me.com", +"mail-page.com", +"mail-tester.com", +"mail.az", +"mail.be", +"mail.bulgaria.com", +"mail.by", +"mail.co.za", +"mail.com", +"mail.com.tr", +"mail.ee", +"mail.gr", +"mail.hitthebeach.com", +"mail.htl22.at", +"mail.md", +"mail.misterpinball.de", +"mail.nu", +"mail.org.uk", +"mail.pf", +"mail.pt", +"mail.ru", +"mail.sisna.com", +"mail.svenz.eu", +"mail.usa.com", +"mail.wtf", +"mail114.net", +"mail15.com", +"mail2007.com", +"mail2aaron.com", +"mail2abby.com", +"mail2abc.com", +"mail2actor.com", +"mail2admiral.com", +"mail2adorable.com", +"mail2adoration.com", +"mail2adore.com", +"mail2adventure.com", +"mail2aeolus.com", +"mail2aether.com", +"mail2affection.com", +"mail2afghanistan.com", +"mail2africa.com", +"mail2agent.com", +"mail2aha.com", +"mail2ahoy.com", +"mail2aim.com", +"mail2air.com", +"mail2airbag.com", +"mail2airforce.com", +"mail2airport.com", +"mail2alabama.com", +"mail2alan.com", +"mail2alaska.com", +"mail2albania.com", +"mail2alcoholic.com", +"mail2alec.com", +"mail2alexa.com", +"mail2algeria.com", +"mail2alicia.com", +"mail2alien.com", +"mail2allan.com", +"mail2allen.com", +"mail2allison.com", +"mail2alpha.com", +"mail2alyssa.com", +"mail2amanda.com", +"mail2amazing.com", +"mail2amber.com", +"mail2america.com", +"mail2american.com", +"mail2andorra.com", +"mail2andrea.com", +"mail2andy.com", +"mail2anesthesiologist.com", +"mail2angela.com", +"mail2angola.com", +"mail2ann.com", +"mail2anna.com", +"mail2anne.com", +"mail2anthony.com", +"mail2aphrodite.com", +"mail2apollo.com", +"mail2april.com", +"mail2aquarius.com", +"mail2arabia.com", +"mail2arabic.com", +"mail2architect.com", +"mail2ares.com", +"mail2argentina.com", +"mail2aries.com", +"mail2arizona.com", +"mail2arkansas.com", +"mail2armenia.com", +"mail2army.com", +"mail2arnold.com", +"mail2art.com", +"mail2arthur.com", +"mail2artist.com", +"mail2ashley.com", +"mail2ask.com", +"mail2astronomer.com", +"mail2athena.com", +"mail2athlete.com", +"mail2atlas.com", +"mail2atom.com", +"mail2attitude.com", +"mail2auction.com", +"mail2aunt.com", +"mail2australia.com", +"mail2austria.com", +"mail2azerbaijan.com", +"mail2baby.com", +"mail2bahamas.com", +"mail2bahrain.com", +"mail2ballerina.com", +"mail2ballplayer.com", +"mail2band.com", +"mail2bangladesh.com", +"mail2bank.com", +"mail2banker.com", +"mail2bankrupt.com", +"mail2baptist.com", +"mail2bar.com", +"mail2barbados.com", +"mail2barbara.com", +"mail2barter.com", +"mail2basketball.com", +"mail2batter.com", +"mail2beach.com", +"mail2beast.com", +"mail2beatles.com", +"mail2beauty.com", +"mail2becky.com", +"mail2beijing.com", +"mail2belgium.com", +"mail2belize.com", +"mail2ben.com", +"mail2bernard.com", +"mail2beth.com", +"mail2betty.com", +"mail2beverly.com", +"mail2beyond.com", +"mail2biker.com", +"mail2bill.com", +"mail2billionaire.com", +"mail2billy.com", +"mail2bio.com", +"mail2biologist.com", +"mail2black.com", +"mail2blackbelt.com", +"mail2blake.com", +"mail2blind.com", +"mail2blonde.com", +"mail2blues.com", +"mail2bob.com", +"mail2bobby.com", +"mail2bolivia.com", +"mail2bombay.com", +"mail2bonn.com", +"mail2bookmark.com", +"mail2boreas.com", +"mail2bosnia.com", +"mail2boston.com", +"mail2botswana.com", +"mail2bradley.com", +"mail2brazil.com", +"mail2breakfast.com", +"mail2brian.com", +"mail2bride.com", +"mail2brittany.com", +"mail2broker.com", +"mail2brook.com", +"mail2bruce.com", +"mail2brunei.com", +"mail2brunette.com", +"mail2brussels.com", +"mail2bryan.com", +"mail2bug.com", +"mail2bulgaria.com", +"mail2business.com", +"mail2buy.com", +"mail2ca.com", +"mail2california.com", +"mail2calvin.com", +"mail2cambodia.com", +"mail2cameroon.com", +"mail2canada.com", +"mail2cancer.com", +"mail2capeverde.com", +"mail2capricorn.com", +"mail2cardinal.com", +"mail2cardiologist.com", +"mail2care.com", +"mail2caroline.com", +"mail2carolyn.com", +"mail2casey.com", +"mail2cat.com", +"mail2caterer.com", +"mail2cathy.com", +"mail2catlover.com", +"mail2catwalk.com", +"mail2cell.com", +"mail2chad.com", +"mail2champaign.com", +"mail2charles.com", +"mail2chef.com", +"mail2chemist.com", +"mail2cherry.com", +"mail2chicago.com", +"mail2chile.com", +"mail2china.com", +"mail2chinese.com", +"mail2chocolate.com", +"mail2christian.com", +"mail2christie.com", +"mail2christmas.com", +"mail2christy.com", +"mail2chuck.com", +"mail2cindy.com", +"mail2clark.com", +"mail2classifieds.com", +"mail2claude.com", +"mail2cliff.com", +"mail2clinic.com", +"mail2clint.com", +"mail2close.com", +"mail2club.com", +"mail2coach.com", +"mail2coastguard.com", +"mail2colin.com", +"mail2college.com", +"mail2color.com", +"mail2colorado.com", +"mail2columbia.com", +"mail2comedian.com", +"mail2composer.com", +"mail2computer.com", +"mail2computers.com", +"mail2concert.com", +"mail2congo.com", +"mail2connect.com", +"mail2connecticut.com", +"mail2consultant.com", +"mail2convict.com", +"mail2cook.com", +"mail2cool.com", +"mail2cory.com", +"mail2costarica.com", +"mail2country.com", +"mail2courtney.com", +"mail2cowboy.com", +"mail2cowgirl.com", +"mail2craig.com", +"mail2crave.com", +"mail2crazy.com", +"mail2create.com", +"mail2croatia.com", +"mail2cry.com", +"mail2crystal.com", +"mail2cuba.com", +"mail2culture.com", +"mail2curt.com", +"mail2customs.com", +"mail2cute.com", +"mail2cutey.com", +"mail2cynthia.com", +"mail2cyprus.com", +"mail2czechrepublic.com", +"mail2dad.com", +"mail2dale.com", +"mail2dallas.com", +"mail2dan.com", +"mail2dana.com", +"mail2dance.com", +"mail2dancer.com", +"mail2danielle.com", +"mail2danny.com", +"mail2darlene.com", +"mail2darling.com", +"mail2darren.com", +"mail2daughter.com", +"mail2dave.com", +"mail2dawn.com", +"mail2dc.com", +"mail2dealer.com", +"mail2deanna.com", +"mail2dearest.com", +"mail2debbie.com", +"mail2debby.com", +"mail2deer.com", +"mail2delaware.com", +"mail2delicious.com", +"mail2demeter.com", +"mail2democrat.com", +"mail2denise.com", +"mail2denmark.com", +"mail2dennis.com", +"mail2dentist.com", +"mail2derek.com", +"mail2desert.com", +"mail2devoted.com", +"mail2devotion.com", +"mail2diamond.com", +"mail2diana.com", +"mail2diane.com", +"mail2diehard.com", +"mail2dilemma.com", +"mail2dillon.com", +"mail2dinner.com", +"mail2dinosaur.com", +"mail2dionysos.com", +"mail2diplomat.com", +"mail2director.com", +"mail2dirk.com", +"mail2disco.com", +"mail2dive.com", +"mail2diver.com", +"mail2divorced.com", +"mail2djibouti.com", +"mail2doctor.com", +"mail2doglover.com", +"mail2dominic.com", +"mail2dominica.com", +"mail2dominicanrepublic.com", +"mail2don.com", +"mail2donald.com", +"mail2donna.com", +"mail2doris.com", +"mail2dorothy.com", +"mail2doug.com", +"mail2dough.com", +"mail2douglas.com", +"mail2dow.com", +"mail2downtown.com", +"mail2dream.com", +"mail2dreamer.com", +"mail2dude.com", +"mail2dustin.com", +"mail2dyke.com", +"mail2dylan.com", +"mail2earl.com", +"mail2earth.com", +"mail2eastend.com", +"mail2eat.com", +"mail2economist.com", +"mail2ecuador.com", +"mail2eddie.com", +"mail2edgar.com", +"mail2edwin.com", +"mail2egypt.com", +"mail2electron.com", +"mail2eli.com", +"mail2elizabeth.com", +"mail2ellen.com", +"mail2elliot.com", +"mail2elsalvador.com", +"mail2elvis.com", +"mail2emergency.com", +"mail2emily.com", +"mail2engineer.com", +"mail2english.com", +"mail2environmentalist.com", +"mail2eos.com", +"mail2eric.com", +"mail2erica.com", +"mail2erin.com", +"mail2erinyes.com", +"mail2eris.com", +"mail2eritrea.com", +"mail2ernie.com", +"mail2eros.com", +"mail2estonia.com", +"mail2ethan.com", +"mail2ethiopia.com", +"mail2eu.com", +"mail2europe.com", +"mail2eurus.com", +"mail2eva.com", +"mail2evan.com", +"mail2evelyn.com", +"mail2everything.com", +"mail2exciting.com", +"mail2expert.com", +"mail2fairy.com", +"mail2faith.com", +"mail2fanatic.com", +"mail2fancy.com", +"mail2fantasy.com", +"mail2farm.com", +"mail2farmer.com", +"mail2fashion.com", +"mail2fat.com", +"mail2feeling.com", +"mail2female.com", +"mail2fever.com", +"mail2fighter.com", +"mail2fiji.com", +"mail2filmfestival.com", +"mail2films.com", +"mail2finance.com", +"mail2finland.com", +"mail2fireman.com", +"mail2firm.com", +"mail2fisherman.com", +"mail2flexible.com", +"mail2florence.com", +"mail2florida.com", +"mail2floyd.com", +"mail2fly.com", +"mail2fond.com", +"mail2fondness.com", +"mail2football.com", +"mail2footballfan.com", +"mail2found.com", +"mail2france.com", +"mail2frank.com", +"mail2frankfurt.com", +"mail2franklin.com", +"mail2fred.com", +"mail2freddie.com", +"mail2free.com", +"mail2freedom.com", +"mail2french.com", +"mail2freudian.com", +"mail2friendship.com", +"mail2from.com", +"mail2fun.com", +"mail2gabon.com", +"mail2gabriel.com", +"mail2gail.com", +"mail2galaxy.com", +"mail2gambia.com", +"mail2games.com", +"mail2gary.com", +"mail2gavin.com", +"mail2gemini.com", +"mail2gene.com", +"mail2genes.com", +"mail2geneva.com", +"mail2george.com", +"mail2georgia.com", +"mail2gerald.com", +"mail2german.com", +"mail2germany.com", +"mail2ghana.com", +"mail2gilbert.com", +"mail2gina.com", +"mail2girl.com", +"mail2glen.com", +"mail2gloria.com", +"mail2goddess.com", +"mail2gold.com", +"mail2golfclub.com", +"mail2golfer.com", +"mail2gordon.com", +"mail2government.com", +"mail2grab.com", +"mail2grace.com", +"mail2graham.com", +"mail2grandma.com", +"mail2grandpa.com", +"mail2grant.com", +"mail2greece.com", +"mail2green.com", +"mail2greg.com", +"mail2grenada.com", +"mail2gsm.com", +"mail2guard.com", +"mail2guatemala.com", +"mail2guy.com", +"mail2hades.com", +"mail2haiti.com", +"mail2hal.com", +"mail2handhelds.com", +"mail2hank.com", +"mail2hannah.com", +"mail2harold.com", +"mail2harry.com", +"mail2hawaii.com", +"mail2headhunter.com", +"mail2heal.com", +"mail2heather.com", +"mail2heaven.com", +"mail2hebe.com", +"mail2hecate.com", +"mail2heidi.com", +"mail2helen.com", +"mail2hell.com", +"mail2help.com", +"mail2helpdesk.com", +"mail2henry.com", +"mail2hephaestus.com", +"mail2hera.com", +"mail2hercules.com", +"mail2herman.com", +"mail2hermes.com", +"mail2hespera.com", +"mail2hestia.com", +"mail2highschool.com", +"mail2hindu.com", +"mail2hip.com", +"mail2hiphop.com", +"mail2holland.com", +"mail2holly.com", +"mail2hollywood.com", +"mail2homer.com", +"mail2honduras.com", +"mail2honey.com", +"mail2hongkong.com", +"mail2hope.com", +"mail2horse.com", +"mail2hot.com", +"mail2hotel.com", +"mail2houston.com", +"mail2howard.com", +"mail2hugh.com", +"mail2human.com", +"mail2hungary.com", +"mail2hungry.com", +"mail2hygeia.com", +"mail2hyperspace.com", +"mail2hypnos.com", +"mail2ian.com", +"mail2ice-cream.com", +"mail2iceland.com", +"mail2idaho.com", +"mail2idontknow.com", +"mail2illinois.com", +"mail2imam.com", +"mail2in.com", +"mail2india.com", +"mail2indian.com", +"mail2indiana.com", +"mail2indonesia.com", +"mail2infinity.com", +"mail2intense.com", +"mail2iowa.com", +"mail2iran.com", +"mail2iraq.com", +"mail2ireland.com", +"mail2irene.com", +"mail2iris.com", +"mail2irresistible.com", +"mail2irving.com", +"mail2irwin.com", +"mail2isaac.com", +"mail2israel.com", +"mail2italian.com", +"mail2italy.com", +"mail2jackie.com", +"mail2jacob.com", +"mail2jail.com", +"mail2jaime.com", +"mail2jake.com", +"mail2jamaica.com", +"mail2james.com", +"mail2jamie.com", +"mail2jan.com", +"mail2jane.com", +"mail2janet.com", +"mail2janice.com", +"mail2japan.com", +"mail2japanese.com", +"mail2jasmine.com", +"mail2jason.com", +"mail2java.com", +"mail2jay.com", +"mail2jazz.com", +"mail2jed.com", +"mail2jeffrey.com", +"mail2jennifer.com", +"mail2jenny.com", +"mail2jeremy.com", +"mail2jerry.com", +"mail2jessica.com", +"mail2jessie.com", +"mail2jesus.com", +"mail2jew.com", +"mail2jeweler.com", +"mail2jim.com", +"mail2jimmy.com", +"mail2joan.com", +"mail2joann.com", +"mail2joanna.com", +"mail2jody.com", +"mail2joe.com", +"mail2joel.com", +"mail2joey.com", +"mail2john.com", +"mail2join.com", +"mail2jon.com", +"mail2jonathan.com", +"mail2jones.com", +"mail2jordan.com", +"mail2joseph.com", +"mail2josh.com", +"mail2joy.com", +"mail2juan.com", +"mail2judge.com", +"mail2judy.com", +"mail2juggler.com", +"mail2julian.com", +"mail2julie.com", +"mail2jumbo.com", +"mail2junk.com", +"mail2justin.com", +"mail2justme.com", +"mail2k.ru", +"mail2kansas.com", +"mail2karate.com", +"mail2karen.com", +"mail2karl.com", +"mail2karma.com", +"mail2kathleen.com", +"mail2kathy.com", +"mail2katie.com", +"mail2kay.com", +"mail2kazakhstan.com", +"mail2keen.com", +"mail2keith.com", +"mail2kelly.com", +"mail2kelsey.com", +"mail2ken.com", +"mail2kendall.com", +"mail2kennedy.com", +"mail2kenneth.com", +"mail2kenny.com", +"mail2kentucky.com", +"mail2kenya.com", +"mail2kerry.com", +"mail2kevin.com", +"mail2kim.com", +"mail2kimberly.com", +"mail2king.com", +"mail2kirk.com", +"mail2kiss.com", +"mail2kosher.com", +"mail2kristin.com", +"mail2kurt.com", +"mail2kuwait.com", +"mail2kyle.com", +"mail2kyrgyzstan.com", +"mail2la.com", +"mail2lacrosse.com", +"mail2lance.com", +"mail2lao.com", +"mail2larry.com", +"mail2latvia.com", +"mail2laugh.com", +"mail2laura.com", +"mail2lauren.com", +"mail2laurie.com", +"mail2lawrence.com", +"mail2lawyer.com", +"mail2lebanon.com", +"mail2lee.com", +"mail2leo.com", +"mail2leon.com", +"mail2leonard.com", +"mail2leone.com", +"mail2leslie.com", +"mail2letter.com", +"mail2liberia.com", +"mail2libertarian.com", +"mail2libra.com", +"mail2libya.com", +"mail2liechtenstein.com", +"mail2life.com", +"mail2linda.com", +"mail2linux.com", +"mail2lionel.com", +"mail2lipstick.com", +"mail2liquid.com", +"mail2lisa.com", +"mail2lithuania.com", +"mail2litigator.com", +"mail2liz.com", +"mail2lloyd.com", +"mail2lois.com", +"mail2lola.com", +"mail2london.com", +"mail2looking.com", +"mail2lori.com", +"mail2lost.com", +"mail2lou.com", +"mail2louis.com", +"mail2louisiana.com", +"mail2lovable.com", +"mail2love.com", +"mail2lucky.com", +"mail2lucy.com", +"mail2lunch.com", +"mail2lust.com", +"mail2luxembourg.com", +"mail2luxury.com", +"mail2lyle.com", +"mail2lynn.com", +"mail2madagascar.com", +"mail2madison.com", +"mail2madrid.com", +"mail2maggie.com", +"mail2mail4.com", +"mail2maine.com", +"mail2malawi.com", +"mail2malaysia.com", +"mail2maldives.com", +"mail2mali.com", +"mail2malta.com", +"mail2mambo.com", +"mail2man.com", +"mail2mandy.com", +"mail2manhunter.com", +"mail2mankind.com", +"mail2many.com", +"mail2marc.com", +"mail2marcia.com", +"mail2margaret.com", +"mail2margie.com", +"mail2marhaba.com", +"mail2maria.com", +"mail2marilyn.com", +"mail2marines.com", +"mail2mark.com", +"mail2marriage.com", +"mail2married.com", +"mail2marries.com", +"mail2mars.com", +"mail2marsha.com", +"mail2marshallislands.com", +"mail2martha.com", +"mail2martin.com", +"mail2marty.com", +"mail2marvin.com", +"mail2mary.com", +"mail2maryland.com", +"mail2mason.com", +"mail2massachusetts.com", +"mail2matt.com", +"mail2matthew.com", +"mail2maurice.com", +"mail2mauritania.com", +"mail2mauritius.com", +"mail2max.com", +"mail2maxwell.com", +"mail2maybe.com", +"mail2mba.com", +"mail2me4u.com", +"mail2mechanic.com", +"mail2medieval.com", +"mail2megan.com", +"mail2mel.com", +"mail2melanie.com", +"mail2melissa.com", +"mail2melody.com", +"mail2member.com", +"mail2memphis.com", +"mail2methodist.com", +"mail2mexican.com", +"mail2mexico.com", +"mail2mgz.com", +"mail2miami.com", +"mail2michael.com", +"mail2michelle.com", +"mail2michigan.com", +"mail2mike.com", +"mail2milan.com", +"mail2milano.com", +"mail2mildred.com", +"mail2milkyway.com", +"mail2millennium.com", +"mail2millionaire.com", +"mail2milton.com", +"mail2mime.com", +"mail2mindreader.com", +"mail2mini.com", +"mail2minister.com", +"mail2minneapolis.com", +"mail2minnesota.com", +"mail2miracle.com", +"mail2missionary.com", +"mail2mississippi.com", +"mail2missouri.com", +"mail2mitch.com", +"mail2model.com", +"mail2mom.com", +"mail2monaco.com", +"mail2money.com", +"mail2mongolia.com", +"mail2monica.com", +"mail2montana.com", +"mail2monty.com", +"mail2moon.com", +"mail2morocco.com", +"mail2morpheus.com", +"mail2mors.com", +"mail2moscow.com", +"mail2moslem.com", +"mail2mouseketeer.com", +"mail2movies.com", +"mail2mozambique.com", +"mail2mp3.com", +"mail2mrright.com", +"mail2msright.com", +"mail2museum.com", +"mail2music.com", +"mail2musician.com", +"mail2muslim.com", +"mail2my.com", +"mail2myboat.com", +"mail2mycar.com", +"mail2mycell.com", +"mail2mygsm.com", +"mail2mylaptop.com", +"mail2mymac.com", +"mail2mypager.com", +"mail2mypalm.com", +"mail2mypc.com", +"mail2myphone.com", +"mail2myplane.com", +"mail2namibia.com", +"mail2nancy.com", +"mail2nasdaq.com", +"mail2nathan.com", +"mail2nauru.com", +"mail2navy.com", +"mail2neal.com", +"mail2nebraska.com", +"mail2ned.com", +"mail2neil.com", +"mail2nelson.com", +"mail2nemesis.com", +"mail2nepal.com", +"mail2netherlands.com", +"mail2network.com", +"mail2nevada.com", +"mail2newhampshire.com", +"mail2newjersey.com", +"mail2newmexico.com", +"mail2newyork.com", +"mail2newzealand.com", +"mail2nicaragua.com", +"mail2nick.com", +"mail2nicole.com", +"mail2niger.com", +"mail2nigeria.com", +"mail2nike.com", +"mail2no.com", +"mail2noah.com", +"mail2noel.com", +"mail2noelle.com", +"mail2normal.com", +"mail2norman.com", +"mail2northamerica.com", +"mail2northcarolina.com", +"mail2northdakota.com", +"mail2northpole.com", +"mail2norway.com", +"mail2notus.com", +"mail2noway.com", +"mail2nowhere.com", +"mail2nuclear.com", +"mail2nun.com", +"mail2ny.com", +"mail2oasis.com", +"mail2oceanographer.com", +"mail2ohio.com", +"mail2ok.com", +"mail2oklahoma.com", +"mail2oliver.com", +"mail2oman.com", +"mail2one.com", +"mail2onfire.com", +"mail2online.com", +"mail2oops.com", +"mail2open.com", +"mail2ophthalmologist.com", +"mail2optometrist.com", +"mail2oregon.com", +"mail2oscars.com", +"mail2oslo.com", +"mail2painter.com", +"mail2pakistan.com", +"mail2pan.com", +"mail2panama.com", +"mail2paraguay.com", +"mail2paralegal.com", +"mail2paris.com", +"mail2park.com", +"mail2parker.com", +"mail2party.com", +"mail2passion.com", +"mail2pat.com", +"mail2patricia.com", +"mail2patrick.com", +"mail2patty.com", +"mail2paul.com", +"mail2paula.com", +"mail2pay.com", +"mail2peace.com", +"mail2pediatrician.com", +"mail2peggy.com", +"mail2pennsylvania.com", +"mail2perry.com", +"mail2persephone.com", +"mail2persian.com", +"mail2peru.com", +"mail2pete.com", +"mail2peter.com", +"mail2pharmacist.com", +"mail2phil.com", +"mail2philippines.com", +"mail2phoenix.com", +"mail2phonecall.com", +"mail2phyllis.com", +"mail2pickup.com", +"mail2pilot.com", +"mail2pisces.com", +"mail2planet.com", +"mail2platinum.com", +"mail2plato.com", +"mail2pluto.com", +"mail2pm.com", +"mail2podiatrist.com", +"mail2poet.com", +"mail2poland.com", +"mail2policeman.com", +"mail2policewoman.com", +"mail2politician.com", +"mail2pop.com", +"mail2pope.com", +"mail2popular.com", +"mail2portugal.com", +"mail2poseidon.com", +"mail2potatohead.com", +"mail2power.com", +"mail2presbyterian.com", +"mail2president.com", +"mail2priest.com", +"mail2prince.com", +"mail2princess.com", +"mail2producer.com", +"mail2professor.com", +"mail2protect.com", +"mail2psychiatrist.com", +"mail2psycho.com", +"mail2psychologist.com", +"mail2qatar.com", +"mail2queen.com", +"mail2rabbi.com", +"mail2race.com", +"mail2racer.com", +"mail2rachel.com", +"mail2rage.com", +"mail2rainmaker.com", +"mail2ralph.com", +"mail2randy.com", +"mail2rap.com", +"mail2rare.com", +"mail2rave.com", +"mail2ray.com", +"mail2raymond.com", +"mail2realtor.com", +"mail2rebecca.com", +"mail2recruiter.com", +"mail2recycle.com", +"mail2redhead.com", +"mail2reed.com", +"mail2reggie.com", +"mail2register.com", +"mail2rent.com", +"mail2republican.com", +"mail2resort.com", +"mail2rex.com", +"mail2rhodeisland.com", +"mail2rich.com", +"mail2richard.com", +"mail2ricky.com", +"mail2ride.com", +"mail2riley.com", +"mail2rita.com", +"mail2rob.com", +"mail2robert.com", +"mail2roberta.com", +"mail2robin.com", +"mail2rock.com", +"mail2rocker.com", +"mail2rod.com", +"mail2rodney.com", +"mail2romania.com", +"mail2rome.com", +"mail2ron.com", +"mail2ronald.com", +"mail2ronnie.com", +"mail2rose.com", +"mail2rosie.com", +"mail2roy.com", +"mail2rss.org", +"mail2rudy.com", +"mail2rugby.com", +"mail2runner.com", +"mail2russell.com", +"mail2russia.com", +"mail2russian.com", +"mail2rusty.com", +"mail2ruth.com", +"mail2rwanda.com", +"mail2ryan.com", +"mail2sa.com", +"mail2sabrina.com", +"mail2safe.com", +"mail2sagittarius.com", +"mail2sail.com", +"mail2sailor.com", +"mail2sal.com", +"mail2salaam.com", +"mail2sam.com", +"mail2samantha.com", +"mail2samoa.com", +"mail2samurai.com", +"mail2sandra.com", +"mail2sandy.com", +"mail2sanfrancisco.com", +"mail2sanmarino.com", +"mail2santa.com", +"mail2sara.com", +"mail2sarah.com", +"mail2sat.com", +"mail2saturn.com", +"mail2saudi.com", +"mail2saudiarabia.com", +"mail2save.com", +"mail2savings.com", +"mail2school.com", +"mail2scientist.com", +"mail2scorpio.com", +"mail2scott.com", +"mail2sean.com", +"mail2search.com", +"mail2seattle.com", +"mail2secretagent.com", +"mail2senate.com", +"mail2senegal.com", +"mail2sensual.com", +"mail2seth.com", +"mail2sevenseas.com", +"mail2sexy.com", +"mail2seychelles.com", +"mail2shane.com", +"mail2sharon.com", +"mail2shawn.com", +"mail2ship.com", +"mail2shirley.com", +"mail2shoot.com", +"mail2shuttle.com", +"mail2sierraleone.com", +"mail2simon.com", +"mail2singapore.com", +"mail2single.com", +"mail2site.com", +"mail2skater.com", +"mail2skier.com", +"mail2sky.com", +"mail2sleek.com", +"mail2slim.com", +"mail2slovakia.com", +"mail2slovenia.com", +"mail2smile.com", +"mail2smith.com", +"mail2smooth.com", +"mail2soccer.com", +"mail2soccerfan.com", +"mail2socialist.com", +"mail2soldier.com", +"mail2somalia.com", +"mail2son.com", +"mail2song.com", +"mail2sos.com", +"mail2sound.com", +"mail2southafrica.com", +"mail2southamerica.com", +"mail2southcarolina.com", +"mail2southdakota.com", +"mail2southkorea.com", +"mail2southpole.com", +"mail2spain.com", +"mail2spanish.com", +"mail2spectrum.com", +"mail2splash.com", +"mail2sponsor.com", +"mail2sports.com", +"mail2srilanka.com", +"mail2stacy.com", +"mail2stan.com", +"mail2stanley.com", +"mail2star.com", +"mail2state.com", +"mail2stephanie.com", +"mail2steve.com", +"mail2steven.com", +"mail2stewart.com", +"mail2stlouis.com", +"mail2stock.com", +"mail2stockholm.com", +"mail2stockmarket.com", +"mail2storage.com", +"mail2store.com", +"mail2strong.com", +"mail2student.com", +"mail2studio.com", +"mail2studio54.com", +"mail2stuntman.com", +"mail2subscribe.com", +"mail2sudan.com", +"mail2superstar.com", +"mail2surfer.com", +"mail2suriname.com", +"mail2susan.com", +"mail2suzie.com", +"mail2swaziland.com", +"mail2sweden.com", +"mail2sweetheart.com", +"mail2swim.com", +"mail2swimmer.com", +"mail2swiss.com", +"mail2switzerland.com", +"mail2sydney.com", +"mail2sylvia.com", +"mail2syria.com", +"mail2taboo.com", +"mail2taiwan.com", +"mail2tajikistan.com", +"mail2tammy.com", +"mail2tango.com", +"mail2tanya.com", +"mail2tanzania.com", +"mail2tara.com", +"mail2taurus.com", +"mail2taxi.com", +"mail2taxidermist.com", +"mail2taylor.com", +"mail2taz.com", +"mail2teacher.com", +"mail2technician.com", +"mail2ted.com", +"mail2telephone.com", +"mail2tenderness.com", +"mail2tennessee.com", +"mail2tennis.com", +"mail2tennisfan.com", +"mail2terri.com", +"mail2terry.com", +"mail2test.com", +"mail2texas.com", +"mail2thailand.com", +"mail2therapy.com", +"mail2think.com", +"mail2tickets.com", +"mail2tiffany.com", +"mail2tim.com", +"mail2time.com", +"mail2timothy.com", +"mail2tina.com", +"mail2titanic.com", +"mail2toby.com", +"mail2todd.com", +"mail2togo.com", +"mail2tom.com", +"mail2tommy.com", +"mail2tonga.com", +"mail2tony.com", +"mail2touch.com", +"mail2tourist.com", +"mail2tracey.com", +"mail2tracy.com", +"mail2tramp.com", +"mail2travel.com", +"mail2traveler.com", +"mail2travis.com", +"mail2trekkie.com", +"mail2trex.com", +"mail2triallawyer.com", +"mail2trick.com", +"mail2trillionaire.com", +"mail2troy.com", +"mail2truck.com", +"mail2trump.com", +"mail2try.com", +"mail2tunisia.com", +"mail2turbo.com", +"mail2turkey.com", +"mail2turkmenistan.com", +"mail2tv.com", +"mail2tycoon.com", +"mail2tyler.com", +"mail2u4me.com", +"mail2uae.com", +"mail2uganda.com", +"mail2uk.com", +"mail2ukraine.com", +"mail2uncle.com", +"mail2unsubscribe.com", +"mail2uptown.com", +"mail2uruguay.com", +"mail2usa.com", +"mail2utah.com", +"mail2uzbekistan.com", +"mail2v.com", +"mail2vacation.com", +"mail2valentines.com", +"mail2valerie.com", +"mail2valley.com", +"mail2vamoose.com", +"mail2vanessa.com", +"mail2vanuatu.com", +"mail2venezuela.com", +"mail2venous.com", +"mail2venus.com", +"mail2vermont.com", +"mail2vickie.com", +"mail2victor.com", +"mail2victoria.com", +"mail2vienna.com", +"mail2vietnam.com", +"mail2vince.com", +"mail2virginia.com", +"mail2virgo.com", +"mail2visionary.com", +"mail2vodka.com", +"mail2volleyball.com", +"mail2waiter.com", +"mail2wallstreet.com", +"mail2wally.com", +"mail2walter.com", +"mail2warren.com", +"mail2washington.com", +"mail2wave.com", +"mail2way.com", +"mail2waycool.com", +"mail2wayne.com", +"mail2webmaster.com", +"mail2webtop.com", +"mail2webtv.com", +"mail2weird.com", +"mail2wendell.com", +"mail2wendy.com", +"mail2westend.com", +"mail2westvirginia.com", +"mail2whether.com", +"mail2whip.com", +"mail2white.com", +"mail2whitehouse.com", +"mail2whitney.com", +"mail2why.com", +"mail2wilbur.com", +"mail2wild.com", +"mail2willard.com", +"mail2willie.com", +"mail2wine.com", +"mail2winner.com", +"mail2wired.com", +"mail2wisconsin.com", +"mail2woman.com", +"mail2wonder.com", +"mail2world.com", +"mail2worship.com", +"mail2wow.com", +"mail2www.com", +"mail2wyoming.com", +"mail2xfiles.com", +"mail2xox.com", +"mail2yachtclub.com", +"mail2yahalla.com", +"mail2yemen.com", +"mail2yes.com", +"mail2yugoslavia.com", +"mail2zack.com", +"mail2zambia.com", +"mail2zenith.com", +"mail2zephir.com", +"mail2zeus.com", +"mail2zipper.com", +"mail2zoo.com", +"mail2zoologist.com", +"mail2zurich.com", +"mail3000.com", +"mail4trash.com", +"mail4u.info", +"mailandftp.com", +"mailas.com", +"mailasia.com", +"mailbolt.com", +"mailboom.com", +"mailbox.as", +"mailbox.co.za", +"mailbox.gr", +"mailbox.hu", +"mailc.net", +"mailcan.com", +"mailcat.biz", +"mailcc.com", +"mailcity.com", +"mailclub.fr", +"maildx.com", +"mailed.ro", +"mailexcite.com", +"mailfa.tk", +"mailforce.net", +"mailforspam.com", +"mailfs.com", +"mailftp.com", +"mailgenie.net", +"mailguard.me", +"mailhaven.com", +"mailhood.com", +"mailimate.com", +"mailinatar.com", +"mailinator.org", +"mailinblack.com", +"mailingaddress.org", +"mailingweb.com", +"mailisent.com", +"mailismagic.com", +"mailite.com", +"mailmate.com", +"mailme.dk", +"mailme.gq", +"mailme24.com", +"mailmight.com", +"mailnator.com", +"mailnew.com", +"mailoye.com", +"mailpanda.com", +"mailpick.biz", +"mailpost.zzn.com", +"mailpride.com", +"mailproxsy.com", +"mailpuppy.com", +"mailquack.com", +"mailrock.biz", +"mailroom.com", +"mailru.com", +"mailsac.com", +"mailsent.net", +"mailservice.ms", +"mailshuttle.com", +"mailslapping.com", +"mailstart.com", +"mailstartplus.com", +"mailsurf.com", +"mailtag.com", +"mailtemp.info", +"mailtothis.com", +"mailueberfall.de", +"mailup.net", +"mailwire.com", +"mailworks.org", +"mailzi.ru", +"mailzilla.org", +"malayalamtelevision.net", +"maltesemail.com", +"mamber.net", +"manager.de", +"mancity.net", +"mantramail.com", +"manybrain.com", +"marchmail.com", +"mariahc.com", +"marijuana.com", +"marijuana.nl", +"married-not.com", +"martindalemail.com", +"masrawy.com", +"matmail.com", +"mauimail.com", +"mauritius.com", +"maxmail.co.uk", +"mbox.com.au", +"me.com", +"meta.ua", +"medical.net.au", +"medscape.com", +"meetingmall.com", +"megapoint.com", +"mehrani.com", +"mehtaweb.com", +"meine-dateien.info", +"meine-diashow.de", +"meine-fotos.info", +"meine-urlaubsfotos.de", +"mekhong.com", +"merda.flu.cc", +"merda.igg.biz", +"merda.nut.cc", +"merda.usa.cc", +"message.hu", +"messages.to", +"metacrawler.com", +"metalfan.com", +"metta.lk", +"mexicomail.com", +"mezimages.net", +"mfsa.ru", +"mierdamail.com", +"miesto.sk", +"mighty.co.za", +"migmail.net", +"migmail.pl", +"miho-nakayama.com", +"mikrotamanet.com", +"millionaireintraining.com", +"millionairemail.com", +"milmail.com", +"mindless.com", +"mindspring.com", +"minister.com", +"misery.net", +"mittalweb.com", +"mixmail.com", +"mjfrogmail.com", +"ml1.net", +"mm.st", +"mns.ru", +"moakt.com", +"mobileninja.co.uk", +"mochamail.com", +"mohammed.com", +"mohmal.com", +"moldova.cc", +"moldova.com", +"moldovacc.com", +"momslife.com", +"monemail.com", +"money.net", +"montevideo.com.uy", +"monumentmail.com", +"moose-mail.com", +"mor19.uu.gl", +"mortaza.com", +"moscowmail.com", +"mostlysunny.com", +"motormania.com", +"movemail.com", +"movieluver.com", +"mox.pp.ua", +"mp4.it", +"mr-potatohead.com", +"msgbox.com", +"msn.cn", +"msn.com", +"msn.nl", +"mt2015.com", +"mt2016.com", +"mttestdriver.com", +"muehlacker.tk", +"munich.com", +"music.com", +"musician.org", +"musicscene.org", +"muskelshirt.de", +"muslim.com", +"muslimsonline.com", +"mutantweb.com", +"mvrht.com", +"my.com", +"my10minutemail.com", +"mybox.it", +"mycity.com", +"mydomain.com", +"mydotcomaddress.com", +"myfamily.com", +"myfastmail.com", +"mygo.com", +"myiris.com", +"mymacmail.com", +"mynamedot.com", +"mynet.com", +"mynetstore.de", +"myownemail.com", +"mypacks.net", +"mypad.com", +"myplace.com", +"myrambler.ru", +"myrealbox.com", +"myremarq.com", +"myself.com", +"myspamless.com", +"mystupidjob.com", +"mytemp.email", +"mythirdage.com", +"myway.com", +"myworldmail.com", +"n2.com", +"n2baseball.com", +"n2mail.com", +"n2soccer.com", +"n2software.com", +"nabc.biz", +"nafe.com", +"nakedgreens.com", +"name.com", +"naplesnews.net", +"naseej.com", +"nativeweb.net", +"naui.net", +"naver.com", +"navigator.lv", +"navy.org", +"naz.com", +"nchoicemail.com", +"neeva.net", +"nenter.com", +"neo.rr.com", +"nervhq.org", +"net-c.be", +"net-c.ca", +"net-c.cat", +"net-c.com", +"net-c.es", +"net-c.fr", +"net-c.it", +"net-c.lu", +"net-c.nl", +"net-c.pl", +"net-pager.net", +"net-shopping.com", +"net4b.pt", +"net4you.at", +"netbounce.com", +"netbroadcaster.com", +"netby.dk", +"netc.eu", +"netc.fr", +"netc.it", +"netc.lu", +"netc.pl", +"netcenter-vn.net", +"netcmail.com", +"netcourrier.com", +"netexecutive.com", +"netexpressway.com", +"netgenie.com", +"netian.com", +"netizen.com.ar", +"netmongol.com", +"netnoir.net", +"netpiper.com", +"netralink.com", +"netscape.net", +"netspace.net.au", +"netster.com", +"nettaxi.com", +"nettemail.com", +"netterchef.de", +"netzero.com", +"netzero.net", +"neue-dateien.de", +"neuf.fr", +"neuro.md", +"newmail.com", +"newmail.net", +"newmail.ru", +"newsboysmail.com", +"newyork.com", +"nextmail.ru", +"nexxmail.com", +"nfmail.com", +"nicebush.com", +"nicegal.com", +"nicholastse.net", +"nicolastse.com", +"nikopage.com", +"nimail.com", +"ninfan.com", +"nirvanafan.com", +"nmail.cf", +"noavar.com", +"nonpartisan.com", +"nonspam.eu", +"nonspammer.de", +"norika-fujiwara.com", +"norikomail.com", +"northgates.net", +"nowhere.org", +"ntlhelp.net", +"ntscan.com", +"null.net", +"nullbox.info", +"nur-fuer-spam.de", +"nus.edu.sg", +"nwldx.com", +"nxt.ru", +"ny.com", +"nybella.com", +"nyc.com", +"nycmail.com", +"nzoomail.com", +"o-tay.com", +"o2.co.uk", +"oaklandas-fan.com", +"oath.com", +"oceanfree.net", +"oddpost.com", +"odmail.com", +"office-dateien.de", +"office-email.com", +"offroadwarrior.com", +"oicexchange.com", +"oikrach.com", +"okbank.com", +"okhuman.com", +"okmagic.com", +"oldies104mail.com", +"olemail.com", +"olympist.net", +"olypmall.ru", +"omaninfo.com", +"omen.ru", +"onebox.com", +"onenet.com.ar", +"oneoffmail.com", +"onet.com.pl", +"onet.eu", +"onet.pl", +"oninet.pt", +"online.ie", +"online.ms", +"online.nl", +"onlinewiz.com", +"onmilwaukee.com", +"onobox.com", +"op.pl", +"opayq.com", +"openmailbox.org", +"operafan.com", +"operamail.com", +"opoczta.pl", +"optician.com", +"optonline.net", +"optusnet.com.au", +"orange.fr", +"orbitel.bg", +"orgmail.net", +"orthodontist.net", +"osite.com.br", +"oso.com", +"otakumail.com", +"our-computer.com", +"our-office.com", +"our.st", +"ourbrisbane.com", +"ournet.md", +"outgun.com", +"outlook.at", +"outlook.be", +"outlook.cl", +"outlook.co.id", +"outlook.co.il", +"outlook.co.nz", +"outlook.co.th", +"outlook.com", +"outlook.com.au", +"outlook.com.br", +"outlook.com.gr", +"outlook.com.pe", +"outlook.com.tr", +"outlook.com.vn", +"outlook.cz", +"outlook.de", +"outlook.dk", +"outlook.es", +"outlook.fr", +"outlook.hu", +"outlook.ie", +"outlook.in", +"outlook.it", +"outlook.jp", +"outlook.kr", +"outlook.lv", +"outlook.my", +"outlook.ph", +"outlook.pt", +"outlook.sa", +"outlook.sg", +"outlook.sk", +"over-the-rainbow.com", +"ownmail.net", +"ozbytes.net.au", +"ozemail.com.au", +"pacbell.net", +"pacific-ocean.com", +"pacific-re.com", +"pacificwest.com", +"pagina.de", +"pagons.org", +"pakistanmail.com", +"pakistanoye.com", +"parkjiyoon.com", +"parrot.com", +"parsmail.com", +"partlycloudy.com", +"partybombe.de", +"partyheld.de", +"partynight.at", +"passwordmail.com", +"pathfindermail.com", +"pcusers.otherinbox.com", +"pediatrician.com", +"penpen.com", +"peoplepc.com", +"peopleweb.com", +"pepbot.com", +"perfectmail.com", +"perso.be", +"personal.ro", +"personales.com", +"petlover.com", +"petml.com", +"pettypool.com", +"pezeshkpour.com", +"pfui.ru", +"phayze.com", +"phone.net", +"photographer.net", +"phpbb.uu.gl", +"phreaker.net", +"physicist.net", +"pianomail.com", +"pickupman.com", +"picusnet.com", +"pigpig.net", +"pinoymail.com", +"piracha.net", +"pisem.net", +"pjjkp.com", +"planet.nl", +"planetaccess.com", +"planetarymotion.net", +"planetearthinter.net", +"planetmail.com", +"planetmail.net", +"planetout.com", +"playersodds.com", +"playful.com", +"plus.com", +"plusmail.com.br", +"pmail.net", +"pobox.sk", +"pochta.ru", +"poczta.fm", +"poczta.onet.pl", +"poetic.com", +"pokemail.net", +"pokemonpost.com", +"pokepost.com", +"polandmail.com", +"polbox.com", +"politician.com", +"polizisten-duzer.de", +"poond.com", +"popaccount.com", +"popmail.com", +"popsmail.com", +"popstar.com", +"portugalmail.com", +"portugalmail.pt", +"post.com", +"post.cz", +"post.sk", +"posta.ro", +"postaccesslite.com", +"postafree.com", +"postfach.cc", +"postinbox.com", +"postino.ch", +"postmark.net", +"postmaster.co.uk", +"postpro.net", +"powerfan.com", +"praize.com", +"premiumservice.com", +"presidency.com", +"priest.com", +"primposta.com", +"primposta.hu", +"privy-mail.com", +"privymail.de", +"pro.hu", +"probemail.com", +"prodigy.net", +"progetplus.it", +"programist.ru", +"programmer.net", +"proinbox.com", +"promessage.com", +"prontomail.com", +"protestant.com", +"protonmail.com", +"prydirect.info", +"psv-supporter.com", +"ptd.net", +"public-files.de", +"public.usa.com", +"publicist.com", +"pulp-fiction.com", +"purpleturtle.com", +"put2.net", +"pwrby.com", +"q.com", +"qmail.com", +"qprfans.com", +"qq.com", +"quackquack.com", +"quakemail.com", +"qualityservice.com", +"quantentunnel.de", +"quickhosts.com", +"quickmail.nl", +"quicknet.nl", +"quickwebmail.com", +"quiklinks.com", +"quikmail.com", +"qv7.info", +"qwest.net", +"qwestoffice.net", +"racedriver.com", +"racefanz.com", +"racingmail.com", +"radicalz.com", +"radiku.ye.vc", +"radiologist.net", +"ragingbull.com", +"ralib.com", +"rambler.ru", +"rambler.ua", +"ranmamail.com", +"rastogi.net", +"ratt-n-roll.com", +"rattle-snake.com", +"raubtierbaendiger.de", +"ravearena.com", +"ravemail.com", +"realemail.net", +"reality-concept.club", +"reallyfast.biz", +"reallyfast.info", +"reallymymail.com", +"realtyagent.com", +"reborn.com", +"reconmail.com", +"recycler.com", +"recyclermail.com", +"rediff.com", +"rediffmail.com", +"rediffmailpro.com", +"rednecks.com", +"redseven.de", +"reggaefan.com", +"registerednurses.com", +"regspaces.tk", +"reincarnate.com", +"religious.com", +"remail.ga", +"renren.com", +"repairman.com", +"reply.hu", +"representative.com", +"rescueteam.com", +"resgedvgfed.tk", +"resumemail.com", +"rezai.com", +"rhyta.com", +"richmondhill.com", +"rickymail.com", +"rin.ru", +"riopreto.com.br", +"rn.com", +"ro.ru", +"roadrunner.com", +"roanokemail.com", +"rock.com", +"rocketmail.com", +"rocketship.com", +"rockfan.com", +"rodrun.com", +"rogers.com", +"roosh.com", +"rootprompt.org", +"royal.net", +"rr.com", +"rrohio.com", +"rsub.com", +"runbox.com", +"rushpost.com", +"ruttolibero.com", +"rvshop.com", +"s-mail.com", +"sacbeemail.com", +"saeuferleber.de", +"safrica.com", +"sagra.lu", +"sags-per-mail.de", +"sailormoon.com", +"saintly.com", +"salehi.net", +"salesperson.net", +"samerica.com", +"samilan.net", +"sammimail.com", +"sandelf.de", +"sanfranmail.com", +"sanook.com", +"sapo.pt", +"saudia.com", +"sayhi.net", +"sbcglobal.net", +"scandalmail.com", +"scarlet.nl", +"schafmail.de", +"schizo.com", +"schmusemail.de", +"schoolmail.com", +"schoolsucks.com", +"schreib-doch-mal-wieder.de", +"sci.fi", +"scientist.com", +"scotland.com", +"scotlandmail.com", +"scottishmail.co.uk", +"scubadiving.com", +"seanet.com", +"search.ua", +"searchwales.com", +"sebil.com", +"secret-police.com", +"secretary.net", +"secretservices.net", +"secure-mail.biz", +"secure-mail.cc", +"seductive.com", +"seekstoyboy.com", +"seguros.com.br", +"selfdestructingmail.com", +"send.hu", +"sendme.cz", +"sendspamhere.com", +"sent.as", +"sent.at", +"sent.com", +"sentrismail.com", +"serga.com.ar", +"servemymail.com", +"servermaps.net", +"sesmail.com", +"sexmagnet.com", +"seznam.cz", +"sfr.fr", +"shaniastuff.com", +"shared-files.de", +"sharedmailbox.org", +"sharmaweb.com", +"she.com", +"shieldedmail.com", +"shinedyoureyes.com", +"shitaway.cf", +"shitaway.ga", +"shitaway.gq", +"shitaway.ml", +"shitaway.tk", +"shitaway.usa.cc", +"shitmail.de", +"shitmail.org", +"shitware.nl", +"shortmail.com", +"shotgun.hu", +"showslow.de", +"sialkotcity.com", +"sialkotian.com", +"sialkotoye.com", +"sify.com", +"silkroad.net", +"sina.cn", +"sina.com", +"singles4jesus.com", +"singmail.com", +"singnet.com.sg", +"singpost.com", +"sinnlos-mail.de", +"siteposter.net", +"skafan.com", +"skeefmail.com", +"skim.com", +"skizo.hu", +"skrx.tk", +"sky.com", +"slamdunkfan.com", +"slave-auctions.net", +"slingshot.com", +"slippery.email", +"slipry.net", +"slotter.com", +"smap.4nmv.ru", +"smapxsmap.net", +"smashmail.de", +"smoothmail.com", +"sms.at", +"snail-mail.net", +"snakebite.com", +"snet.net", +"sniper.hu", +"snkmail.com", +"snoopymail.com", +"snowboarding.com", +"snowdonia.net", +"socceramerica.net", +"soccermail.com", +"soccermomz.com", +"social-mailer.tk", +"socialworker.net", +"sociologist.com", +"sofort-mail.de", +"sofortmail.de", +"softhome.net", +"sogou.com", +"sohu.com", +"sol.dk", +"solcon.nl", +"soldier.hu", +"solution4u.com", +"solvemail.info", +"songwriter.net", +"sonnenkinder.org", +"soodomail.com", +"soon.com", +"soulfoodcookbook.com", +"sp.nl", +"space-bank.com", +"space-man.com", +"space-ship.com", +"space-travel.com", +"space.com", +"spacemart.com", +"spacewar.com", +"spainmail.com", +"spam.2012-2016.ru", +"spamavert.com", +"spambob.com", +"spambooger.com", +"spamdecoy.net", +"spameater.com", +"spamfree24.info", +"spaminator.de", +"spaml.com", +"spamoff.de", +"spartapiet.com", +"speedemail.net", +"speedpost.net", +"speedrules.com", +"speedrulz.com", +"speedymail.org", +"sperke.net", +"spils.com", +"spinfinder.com", +"spl.at", +"spoko.pl", +"spoofmail.de", +"sportsmail.com", +"sporttruckdriver.com", +"spray.se", +"spybox.de", +"spymac.com", +"srilankan.net", +"ssl-mail.com", +"st-davids.net", +"stade.fr", +"stargateradio.com", +"starmail.com", +"starmedia.com", +"starspath.com", +"start.com.au", +"startkeys.com", +"stinkefinger.net", +"stipte.nl", +"stoned.com", +"stones.com", +"stop-my-spam.pp.ua", +"streber24.de", +"streetwisemail.com", +"strompost.com", +"strongguy.com", +"student.su", +"studentcenter.org", +"stuffmail.de", +"subram.com", +"sudolife.me", +"sudolife.net", +"sudomail.biz", +"sudomail.com", +"sudomail.net", +"sudoverse.com", +"sudoverse.net", +"sudoweb.net", +"sudoworld.com", +"sudoworld.net", +"suhabi.com", +"sukhumvit.net", +"sunpoint.net", +"sunrise-sunset.com", +"sunsgame.com", +"sunumail.sn", +"superdada.com", +"supereva.it", +"supermail.ru", +"superrito.com", +"surf3.net", +"surfree.com", +"surfy.net", +"surgical.net", +"surimail.com", +"survivormail.com", +"svk.jp", +"swbell.net", +"sweb.cz", +"swedenmail.com", +"sweetxxx.de", +"swift-mail.com", +"swiftdesk.com", +"swingeasyhithard.com", +"swingfan.com", +"swipermail.zzn.com", +"swirve.com", +"swissmail.com", +"swissmail.net", +"switchboardmail.com", +"sx172.com", +"syom.com", +"t-online.de", +"t.psh.me", +"t2mail.com", +"tafmail.com", +"takuyakimura.com", +"talk21.com", +"talkinator.com", +"tamil.com", +"tampabay.rr.com", +"tankpolice.com", +"tatanova.com", +"tbwt.com", +"tds.net", +"teachermail.net", +"teachers.org", +"teamdiscovery.com", +"teamtulsa.net", +"tech-center.com", +"tech4peace.org", +"techemail.com", +"techie.com", +"technisamail.co.za", +"technologist.com", +"techscout.com", +"techspot.com", +"tele2.at", +"tele2.nl", +"teleline.es", +"telerymd.com", +"teleworm.us", +"telfort.nl", +"telfortglasvezel.nl", +"telinco.net", +"telpage.net", +"telstra.com", +"telstra.com.au", +"temp-mail.com", +"temp-mail.de", +"temp.headstrong.de", +"tempail.com", +"tempemail.biz", +"tempmail.us", +"tempmaildemo.com", +"tempmailer.com", +"temporarioemail.com.br", +"temporaryemail.us", +"tempthe.net", +"tempymail.com", +"temtulsa.net", +"tenchiclub.com", +"tenderkiss.com", +"tennismail.com", +"terminverpennt.de", +"terra.cl", +"terra.com", +"terra.com.ar", +"terra.com.br", +"test.com", +"test.de", +"tfanus.com.er", +"tfz.net", +"thai.com", +"thaimail.com", +"thaimail.net", +"thanksnospam.info", +"the-african.com", +"the-aliens.com", +"the-american.com", +"the-animal.com", +"the-astronaut.com", +"the-beauty.com", +"the-big-apple.com", +"the-boss.com", +"the-captain.com", +"the-cowboy.com", +"the-eagles.com", +"the-fastest.net", +"the-galaxy.net", +"the-genius.com", +"the-gentleman.com", +"the-german.com", +"the-italian.com", +"the-lair.com", +"the-madman.com", +"the-marine.com", +"the-master.com", +"the-mexican.com", +"the-monkey.com", +"the-pentagon.com", +"the-professional.com", +"the-quickest.com", +"the-russian.com", +"the-spaceman.com", +"thecriminals.com", +"thedoghousemail.com", +"thedorm.com", +"theend.hu", +"theglobe.com", +"thegolfcourse.com", +"theheadoffice.com", +"theinternetemail.com", +"thelanddownunder.com", +"themail.com", +"themillionare.net", +"theplate.com", +"thepokerface.com", +"thepostmaster.net", +"theraces.com", +"therapist.net", +"thestreetfighter.com", +"thewatercooler.com", +"thewebpros.co.uk", +"thirdage.com", +"thisgirl.com", +"thraml.com", +"throwam.com", +"tidni.com", +"tiscali.co.uk", +"tiscali.it", +"tkcity.com", +"tmail.ws", +"toast.com", +"toke.com", +"tom.com", +"toolsource.com", +"toomail.biz", +"toothfairy.com", +"topletter.com", +"topmail-files.de", +"torontomail.com", +"tortenboxer.de", +"totalmail.de", +"totalmusic.net", +"tpg.com.au", +"trash-mail.ml", +"trashdevil.de", +"trashymail.net", +"trayna.com", +"trialbytrivia.com", +"trickmail.net", +"trimix.cn", +"tritium.net", +"trmailbox.com", +"tropicalstorm.com", +"truckracer.com", +"truckracers.com", +"truthmail.com", +"tsamail.co.za", +"ttml.co.in", +"turboprinz.de", +"turboprinzessin.de", +"turkey.com", +"tut.by", +"tvstar.com", +"twc.com", +"twinstarsmail.com", +"typemail.com", +"u2club.com", +"ua.fm", +"ubbi.com", +"uboot.com", +"uk2.net", +"uk2k.com", +"uk2net.com", +"uk7.net", +"uk8.net", +"ukbuilder.com", +"ukcool.com", +"ukdreamcast.com", +"ukmail.org", +"ukmax.com", +"ukr.net", +"uku.co.uk", +"ultapulta.com", +"ultrapostman.com", +"ummah.org", +"umpire.com", +"unbounded.com", +"unforgettable.com", +"uni.de", +"unican.es", +"unihome.com", +"universal.pt", +"uno.ee", +"uno.it", +"unofree.it", +"unterderbruecke.de", +"uol.com.br", +"uol.com.co", +"uol.com.ve", +"uomail.com", +"upc.nl", +"upcmail.nl", +"upf.org", +"uplipht.com", +"ureach.com", +"uroid.com", +"usa.com", +"usa.net", +"usaaccess.net", +"usermail.com", +"username.e4ward.com", +"usma.net", +"usmc.net", +"uswestmail.net", +"utanet.at", +"uymail.com", +"uyuyuy.com", +"vaasfc4.tk", +"vahoo.com", +"valemail.net", +"vampirehunter.com", +"varbizmail.com", +"vcmail.com", +"velnet.co.uk", +"velocall.com", +"verizon.net", +"verlass-mich-nicht.de", +"versatel.nl", +"veryfast.biz", +"veryrealemail.com", +"veryspeedy.net", +"vfemail.net", +"vickaentb.tk", +"videotron.ca", +"viditag.com", +"vinbazar.com", +"violinmakers.co.uk", +"vip.126.com", +"vip.163.com", +"vip.21cn.com", +"vip.citiz.net", +"vip.gr", +"vip.onet.pl", +"vip.qq.com", +"vip.sina.com", +"vipmail.ru", +"virgilio.it", +"virgin.net", +"virginbroadband.com.au", +"visitweb.com", +"visto.com", +"vivavelocity.com", +"vivianhsu.net", +"vkcode.ru", +"vnet.citiz.net", +"vnn.vn", +"vodafone.nl", +"vodafonethuis.nl", +"volcanomail.com", +"vollbio.de", +"volloeko.de", +"vomoto.com", +"vorsicht-bissig.de", +"vorsicht-scharf.de", +"vote-democrats.com", +"vote-republicans.com", +"vote4gop.org", +"votenet.com", +"vp.pl", +"vr9.com", +"vubby.com", +"w3.to", +"wahoye.com", +"walala.org", +"wales2000.net", +"walkmail.net", +"walkmail.ru", +"wam.co.za", +"wanadoo.es", +"wanadoo.fr", +"war-im-urlaub.de", +"warmmail.com", +"warpmail.net", +"warrior.hu", +"wazabi.club", +"wbdet.com", +"web-contact.info", +"web-emailbox.eu", +"web-mail.com.ar", +"web-mail.pp.ua", +"web-police.com", +"web.de", +"webave.com", +"webcity.ca", +"webcontact-france.eu", +"webdream.com", +"webindia123.com", +"webmail.co.za", +"webmail.hu", +"webmails.com", +"webname.com", +"webstation.com", +"websurfer.co.za", +"webtopmail.com", +"wee.my", +"weekonline.com", +"wefjo.grn.cc", +"weg-werf-email.de", +"wegas.ru", +"wegwerf-emails.de", +"wegwerfmail.info", +"wegwerpmailadres.nl", +"wehshee.com", +"weibsvolk.de", +"weibsvolk.org", +"weinenvorglueck.de", +"welsh-lady.com", +"westnet.com.au", +"wfgdfhj.tk", +"whale-mail.com", +"whartontx.com", +"whatiaas.com", +"whatpaas.com", +"wheelweb.com", +"whipmail.com", +"whoever.com", +"whtjddn.33mail.com", +"wickmail.net", +"wideopenwest.com", +"wildmail.com", +"wilemail.com", +"will-hier-weg.de", +"windowslive.com", +"windstream.net", +"wingnutz.com", +"winning.com", +"wir-haben-nachwuchs.de", +"wir-sind-cool.org", +"witty.com", +"wiz.cc", +"wkbwmail.com", +"wmail.cf", +"wo.com.cn", +"woh.rr.com", +"wolke7.net", +"wombles.com", +"women-at-work.org", +"wongfaye.com", +"wooow.it", +"worker.com", +"workmail.com", +"worldemail.com", +"worldnet.att.net", +"wormseo.cn", +"wosaddict.com", +"wowgirl.com", +"wowmail.com", +"wowway.com", +"wp.pl", +"wptamail.com", +"wrexham.net", +"writeme.com", +"writemeback.com", +"wrongmail.com", +"www.com", +"www.e4ward.com", +"wxs.net", +"x-mail.net", +"x-networks.net", +"x5g.com", +"xaker.ru", +"xing886.uu.gl", +"xmastime.com", +"xms.nl", +"xoom.com", +"xpressmail.zzn.com", +"xs4all.nl", +"xsecurity.org", +"xsmail.com", +"xtra.co.nz", +"xuno.com", +"xww.ro", +"xy9ce.tk", +"y7mail.com", +"ya.ru", +"yada-yada.com", +"yahoo.at", +"yahoo.be", +"yahoo.ca", +"yahoo.cn", +"yahoo.co.id", +"yahoo.co.il", +"yahoo.co.in", +"yahoo.co.jp", +"yahoo.co.kr", +"yahoo.co.nz", +"yahoo.co.th", +"yahoo.co.uk", +"yahoo.co.za", +"yahoo.com", +"yahoo.com.ar", +"yahoo.com.au", +"yahoo.com.br", +"yahoo.com.cn", +"yahoo.com.co", +"yahoo.com.hk", +"yahoo.com.mx", +"yahoo.com.my", +"yahoo.com.ph", +"yahoo.com.sg", +"yahoo.com.tr", +"yahoo.com.tw", +"yahoo.com.vn", +"yahoo.cz", +"yahoo.de", +"yahoo.dk", +"yahoo.es", +"yahoo.fi", +"yahoo.fr", +"yahoo.gr", +"yahoo.hu", +"yahoo.ie", +"yahoo.in", +"yahoo.it", +"yahoo.jp", +"yahoo.nl", +"yahoo.no", +"yahoo.pl", +"yahoo.pt", +"yahoo.ro", +"yahoo.se", +"yalla.com", +"yalla.com.lb", +"yalook.com", +"yam.com", +"yandex.com", +"yandex.ru", +"yandex.ua", +"yapped.net", +"yawmail.com", +"yeah.net", +"yebox.com", +"yehey.com", +"yepmail.net", +"yert.ye.vc", +"yesey.net", +"ymail.com", +"yogotemail.com", +"yomail.info", +"yopmail.pp.ua", +"yopolis.com", +"yopweb.com", +"youareadork.com", +"youmailr.com", +"your-house.com", +"your-mail.com", +"yourname.freeservers.com", +"yours.com", +"yoursubdomain.zzn.com", +"yourteacher.net", +"yuuhuu.net", +"yyhmail.com", +"z1p.biz", +"za.com", +"zahadum.com", +"zaktouni.fr", +"zeepost.nl", +"zetmail.com", +"zhaowei.net", +"zhouemail.510520.org", +"ziggo.nl", +"zionweb.org", +"zip.net", +"zipido.com", +"ziplip.com", +"zipmail.com", +"zipmail.com.br", +"zipmax.com", +"zmail.ru", +"zoho.com", +"zomg.info", +"zonnet.nl", +"zoominternet.net", +"zubee.com", +"zuzzurello.com", +"zwallet.com", +"zweb.in", +"zxcvbnm.com", +"zybermail.com", +"zydecofan.com", +"zzn.com", +"zzz.com"} + +spam-mime = { +"bat" = "BAD", +"chm" = "BAD", +"com" = "BAD", +"exe" = "BAD", +"hta" = "BAD|NZ", +"iso" = "BAD", +"jar" = "BAD|NZ", +"lnk" = "BAD", +"scr" = "BAD", +"htm" = "text/html|BAD", +"html" = "text/html|BAD", +"shtm" = "text/html|BAD", +"shtml" = "text/html|BAD", +"ace" = "BAD|AR", +"arj" = "BAD|AR", +"asx" = "BAD", +"cab" = "BAD|AR", +"sfx" = "BAD", +"vst" = "BAD", +"vss" = "BAD", +"ade" = "BAD", +"adp" = "BAD", +"cmd" = "BAD", +"cpl" = "BAD", +"ins" = "BAD", +"isp" = "BAD", +"js" = "BAD|NZ", +"jse" = "BAD", +"lib" = "BAD", +"mde" = "BAD", +"msc" = "BAD", +"msi" = "BAD", +"msp" = "BAD", +"mst" = "BAD", +"nsh" = "BAD", +"pif" = "BAD", +"sct" = "BAD", +"shb" = "BAD", +"sys" = "BAD", +"vb" = "BAD", +"vbe" = "BAD", +"vbs" = "BAD|NZ", +"vxd" = "BAD", +"wsc" = "BAD", +"wsh" = "BAD", +"app" = "BAD", +"asp" = "BAD", +"bas" = "BAD", +"cnt" = "BAD", +"csh" = "BAD", +"diagcab" = "BAD", +"fxp" = "BAD", +"gadget" = "BAD", +"grp" = "BAD", +"hlp" = "BAD", +"hpj" = "BAD", +"inf" = "BAD", +"its" = "BAD", +"jnlp" = "BAD", +"ksh" = "BAD", +"mad" = "BAD", +"maf" = "BAD", +"mag" = "BAD", +"mam" = "BAD", +"maq" = "BAD", +"mar" = "BAD", +"mas" = "BAD", +"mat" = "BAD", +"mau" = "BAD", +"mav" = "BAD", +"maw" = "BAD", +"mcf" = "BAD", +"mda" = "BAD", +"mdb" = "BAD", +"mdt" = "BAD", +"mdw" = "BAD", +"mdz" = "BAD", +"msh" = "BAD", +"msh1" = "BAD", +"msh2" = "BAD", +"mshxml" = "BAD", +"msh1xml" = "BAD", +"msh2xml" = "BAD", +"msu" = "BAD", +"ops" = "BAD", +"osd" = "BAD", +"pcd" = "BAD", +"pl" = "BAD", +"plg" = "BAD", +"prf" = "BAD", +"prg" = "BAD", +"printerexport" = "BAD", +"ps1" = "BAD", +"ps1xml" = "BAD", +"ps2" = "BAD", +"ps2xml" = "BAD", +"psc1" = "BAD", +"psc2" = "BAD", +"psd1" = "BAD", +"psdm1" = "BAD", +"pst" = "BAD", +"reg" = "BAD", +"scf" = "BAD", +"shs" = "BAD", +"theme" = "BAD", +"url" = "BAD", +"vbp" = "BAD", +"vsmacros" = "BAD", +"vsw" = "BAD", +"webpnp" = "BAD", +"website" = "BAD", +"ws" = "BAD", +"xbap" = "BAD", +"xll" = "BAD", +"xnk" = "BAD", +"docx" = "NZ", +"pdf" = "application/pdf|application/x-pdf|NZ", +"pptx" = "NZ", +"wsf" = "NZ", +"xlsx" = "NZ", +"7x" = "AR", +"alz" = "AR", +"bz2" = "AR", +"egg" = "AR", +"lz" = "AR", +"rar" = "AR", +"xz" = "AR", +"zip" = "AR", +"txt" = "text/plain|message/disposition-notification|text/rfc822-headers"} + + +spam-redirect = {"000d.ru", +"0845.com", +"0c.ru", +"0lv.ru", +"0pen.me", +"0rz.tw", +"10r.us", +"123url.org", +"140.uz", +"17q.com", +"1c-bitrix.ru", +"1cl.in", +"1ink.in", +"1ink.ru", +"1iny.com", +"1lik.net", +"1link.in", +"1url.com", +"1url.in", +"1-url.net", +"1-url.ru", +"2big.at", +"2dwww.com", +"2.gp", +"2it.info", +"2.ly", +"2mb.eu", +"2qu.ru", +"2sms.ru", +"2tu.me", +"2tu.us", +"2url.org", +"307.to", +"3fw.ru", +"3le.ru", +"3.ly", +"3.vu", +"3x.si", +"4.gg", +"4job.ru", +"4.ly", +"4ms.me", +"4p5.com", +"4ry.ru", +"4sq.com", +"4u.gd", +"4url.cc", +"4url.tk", +"5.gp", +"5link.tk", +"5pl.us", +"5url.net", +"5z8.info", +"6fr.ru", +"6.ly", +"6pn.com", +"6url.com", +"6yo.org", +"70.ru", +"74job.ru", +"7.ly", +"7ly.ru", +"7pisem.ru", +"7ruh.com", +"7ry.us", +"7xu.org", +"8.ly", +"8q.ro", +"9mp.com", +"9-n.org", +"9xi.ru", +"a1.tc", +"a2k.in", +"aa.cx", +"aafter.us", +"abe5.com", +"access.im", +"action-emails.ru", +"ad4.us", +"adf.ly", +"adjix.com", +"adsbeta.net", +"ad.vu", +"afx.cc", +"a.gg", +"ah.ae", +"aipro.ru", +"airs.ru", +"aka-url.com", +"alic.at", +"all.fuseurl.com", +"allshort.ru", +"all-top.ru", +"alturl.com", +"a.md", +"amzn.to", +"a.nf", +"apeurl.com", +"api.m3653.net", +"apsense.cc", +"apu.sh", +"ar.gy", +"arm.in", +"arst.ch", +"atiny.me", +"atto.co.za", +"atu.ca", +"autodesk.com", +"avast.com", +"avoo.net", +"azc.cc", +"b23.ru", +"b2l.me", +"backupurl.com", +"bacn.me", +"bai.lu", +"bcool.bz", +"bezurl.com", +"bi.gl", +"binged.it", +"bin.nu", +"bitby.net", +"bit.do", +"bit.gy", +"bit.ly", +"bitleyco.cc", +"bitly.com", +"bitrix24.ru", +"biturl.net", +"bit.uz", +"bizj.us", +"bloat.me", +"bmu.li", +"boi.re", +"bq.ro", +"bravo.ly", +"briefurl.pl", +"bsa.ly", +"bsndsy.ru", +"budurl.com", +"bun.ru", +"bu.tt", +"byst.ro", +"byyb.net", +"bz9.com", +"campaign-services.directcrm.ru", +"canurl.com", +"capello.linkatty.com", +"capourl.com", +"care2share.tk", +"cbs.so", +"cbuz.com", +"cctv.ws", +"cd.vg", +"cektkp.com", +"cha.la", +"chilp.it", +"chzb.gr", +"cjb.net", +"cjt99.tk", +"clck.ru", +"cliccami.info", +"click2.info", +"clicks.biletix.ru", +"clicks.citilink.ru", +"click.email4customers.com", +"click.emailinfo.mail.hpe.com", +"click.icptrack.com", +"click.mlsend.com", +"click-me.us", +"clickthru.ca", +"clickv.tk", +"cli.gs", +"clkit.co", +"cl.lk", +"cl.ly", +"clme.ru", +"cloakreferer.com", +"clockurl.com", +"clop.in", +"cms.im", +"cmylink.com", +"cnect.us", +"comyonet.com", +"conta.cc", +"coolestone.com", +"cort.as", +"cortas.elpais.com", +"cot.ag", +"cowurl.com", +"cp.bitrix.ru", +"cr.am", +"createurl.com", +"crks.me", +"crlf.ru", +"crop.im", +"crum.pl", +"ctvr.us", +"cug.kr", +"cut4.me", +"cut.by", +"cuthut.com", +"cutt.us", +"d2u.us", +"d8z.ru", +"dai.ly", +"da.lc", +"ddp.net", +"decenturl.com", +"delivr.com", +"dev0.ru", +"dft.ba", +"digbig.com", +"di.gd", +"digg.com", +"digidns.net", +"din.gy", +"directtrafficlink.com", +"disq.us", +"dld.bz", +"dlvr.it", +"dmanalytics1.com", +"doiop.com", +"do.my", +"dopen.us", +"dot.tk", +"dr2.biz", +"driz.ru", +"dr.tl", +"durlz.info", +"easyuri.com", +"easyurl.jp", +"easyurl.net", +"eepurl.com", +"ej.uz", +"elurl.com", +"email.account.2gis.com", +"email.mail.ostrovok.ru", +"email.mxtoolbox.com", +"email.news.ostrovok.ru", +"e.mail.ru", +"em.digium.com", +"emap.ws", +"emlstart.com", +"etdurl.com", +"eweri.com", +"exa.im", +"f1ru.net", +"fa.by", +"fanta.linkatty.com", +"fav.me", +"fbi.pp.ua", +"fb.me", +"fbshare.me", +"fff.to", +"ff.im", +"ffs.cc", +"fi.gd", +"fire.to", +"firsturl.de", +"firsturl.net", +"fishurl.ru", +"flane.info", +"flavr.be", +"flic.kr", +"flq.us", +"flx.im", +"fly2.ws", +"folo.me", +"fo.my", +"fon.gs", +"forex-trade.be", +"fqav.com", +"freak.to", +"freepl.us", +"free-redirect.tk", +"freeurl.me", +"free-url-redirection.com.ru", +"fur.ly", +"fuseurl.com", +"fuzzy.to", +"fwd4.me", +"fwds.me", +"fwib.net", +"fyad.org", +"fyn.im", +"g00.me", +"gadaf.fi", +"game-url.com", +"gentleurl.net", +"geteml.com", +"getlink.info", +"get.sh", +"get.tf", +"gho.co", +"gig140.com", +"gizmo.do", +"gl.am", +"glink.co", +"gltw.ru", +"gmetzner.de", +"gmy.su", +"gnu.su", +"go2-url.com", +"go.9nl.com", +"go9.us", +"goandgrab.info", +"gog.tc", +"go.it", +"go-links.net", +"golook.at", +"go.ly", +"good.ly", +"goo.gl", +"goo.pm", +"go.qb.by", +"goshrink.com", +"gosite.in", +"goto.pattayacitythailand.com", +"gourl.ca", +"gourl.gr", +"gourl.it", +"go-url.ru", +"go.usa.gov", +"gri.bz", +"groteck.com", +"g.ro.lt", +"gtgg.us", +"g.ua", +"gu.ma", +"gurl.es", +"haqm.com", +"hex.io", +"hhvx.com", +"hiderefer.com", +"hijw.com", +"hi.kg", +"hit.kg", +"hj.to", +"hlurl.com", +"hmm.ph", +"ho.io", +"hop.clickbank.net", +"hopclicks.com", +"ho.pe", +"hop.kz", +"href.in", +"hsblinks.com", +"htxt.it", +"hubb.me", +"huff.to", +"hulu.com", +"hurl.me", +"hurl.ws", +"huuk.net", +"hvmnd.org", +"i2h.de", +"i5.be", +"icanhaz.com", +"idek.net", +"idelink.com", +"ifree.kz", +"ih3.ru", +"ikeafamilynews.ru", +"ilix.in", +"ilnk.me", +"informer.ru", +"innogam.es", +"ino.me", +"int.kz", +"ipsha.ru", +"ir.pe", +"is.gd", +"is.gs", +"issuu.com", +"itshrunk.com", +"its.my", +"iurlz.com", +"ix.lt", +"ixr.be", +"j3w.it", +"ja.cx", +"jdem.cz", +"jewi.sh", +"jijr.com", +"jmb.tw", +"j.mp", +"jom.la", +"joo.ru", +"just.as", +"juu.cc", +"keep2.me", +"kickurl.com", +"kipq.com", +"kisaurl.com", +"ki.tl", +"kl.am", +"klck.me", +"klik.sihitam.com", +"klx.co", +"knb.im", +"kon.tl", +"kore.us", +"korta.nu", +"kqon.com", +"kr1n.ru", +"krunchd.com", +"krz.ch", +"ktzr.us", +"l24.cm", +"l3ss.me", +"l9k.net", +"lat.ms", +"lavvs.com", +"lcut.us", +"leeturl.net", +"leto.tk", +"liip.to", +"liltext.com", +"lin.io", +"link2me.ru", +"link.ac", +"linkbee.com", +"linkbun.ch", +"linkcash.biz", +"linkde.info", +"linkee.com", +"link.from.homecredit.ru", +"link.hhut.ru", +"linkl.ru", +"link.mail.1fd-system.ru", +"link.mail.e-gazeta-unp.ru", +"link.mail.e.glavbukh-mail.ru", +"link.mail.fd-online.ru", +"link.mail.glavbukh-mail.ru", +"link.mail.unp-client.ru", +"link.rengo.ru", +"link.sendsay.ru", +"linkslash.ca", +"linkunion.de", +"linkx.me", +"linkyy.com", +"linkzip.net", +"lip.tc", +"li.ru", +"list-manage1.com", +"list-manage2.com", +"list-manage.com", +"little.im", +"littleurl.net", +"liurl.cn", +"livehoster.org", +"llinks.net", +"ln0.ru", +"ln4.me", +"lnk.by", +"lnk.cm", +"lnk.co", +"lnk.gd", +"lnk.in", +"lnk.ly", +"lnk.ms", +"lnk.sk", +"lnkd.in", +"lnks.gd", +"lnks.it", +"lnkstts.com", +"lnkurl.com", +"ln-s.net", +"ln-s.ru", +"loh.ru", +"loo.gl", +"lovebyt.es", +"low.cc", +"l.pr", +"lr.tc", +"lru.jp", +"lrwk.com", +"ltos.ru", +"lt.tl", +"lul.es", +"lurl.no", +"lx2.net", +"ly9.net", +"m4u.in", +"m7a.org", +"macte.ch", +"mail.rambler.ru", +"mandrillapp.com", +"mash.to", +"mee.la", +"merky.de", +"metamark.net", +"micurl.com", +"migre.me", +"miliuner.com", +"miniurl.com", +"miniurl.net.ru", +"miniurl.pl", +"mimecast.com", +"minu.me", +"minurl.fr", +"minyurl.net", +"minyurl.org", +"mislead.in", +"miud.in", +"mixe.me", +"mj.is", +"mjt.lu", +"mke.me", +"mlcampaignru.com", +"mljt.tech", +"mlsendru.com", +"mmt.su", +"mobotix-news.com", +"mo.by", +"moby.to", +"mockurl.com", +"moourl.com", +"mp77.com", +"mrte.ch", +"mtp.pl", +"mty.in", +"mug.gs", +"murl.kz", +"mvp.im", +"mylink4u.info", +"mylink.to", +"myloc.me", +"myooo.info", +"mypaqe.com", +"mypl.us", +"mytinyurl.net", +"myurl.in", +"myurl.si", +"myxx.me", +"mzan.si", +"n3n.in", +"n3r.ru", +"nbc.co", +"nblo.gs", +"nbold.com", +"ne1.net", +"netgod.tk", +"neuf.tk", +"newhotlink.com", +"nexturl.ru", +"nicesharing.com", +"nik.im", +"niurl.com", +"nl.cr", +"nn.nf", +"no1.in", +"no.io", +"nonameno.com", +"normalurl.com", +"notlong.com", +"not.my", +"now.am", +"n.pr", +"nsfw.in", +"nutshellurl.com", +"nxy.in", +"nyti.ms", +"oc1.us", +"oeeq.com", +"oiurl.com", +"o.ly", +"omf.gd", +"om.ly", +"omoikane.net", +"on.cnn.com", +"on.mktw.net", +"oogyah.com", +"oork.com", +"opurl.us", +"orbita.co.il", +"orz.se", +"ourgplus.at", +"out.houseofgaga.ru", +"ovr.me", +"ow.ly", +"o-x.fr", +"p1.fr", +"pathto.net", +"paypal-communication.com", +"pb8.ru", +"pburl.com", +"pcw.ro", +"pduda.mobi", +"pechkincensor.ru", +"pechkinspy.ru", +"peeep.us", +"peekurl.com", +"pendek.in", +"penting.web.id", +"pfat.de", +"pho.se", +"phpm.ru", +"php-ru.info", +"pi90.com", +"picourl.ru", +"piks.nl", +"ping.fm", +"pli.gs", +"plink.es", +"plo.cc", +"ploshadka.ru", +"plugin.name", +"plusphp.com", +"p.ly", +"pnt.me", +"pobierz-film.tk", +"politi.co", +"ponyurl.com", +"poo.pr", +"post.ly", +"pot.vg", +"pp.gg", +"ppt.cc", +"pra.im", +"privacy-surf.com", +"proext.com", +"professionali.ru", +"profile.to", +"proofpoint.com", +"prourl.de", +"ptiturl.com", +"p.tl", +"pub.vitrue.com", +"punyurl.com", +"purl.org", +"pvh.me", +"pw.ly", +"pxlz.org", +"py6.ru", +"pygmyurl.com", +"pysper.com", +"q32.ru", +"qick.ws", +"qid.in", +"qkr.cc", +"qlnk.net", +"qlql.ru", +"qnh.pl", +"qr.cx", +"qr.ee", +"qrf.in", +"qr.net", +"qru.ru", +"qte.me", +"qtwk.com", +"quik.in", +"qurl.com", +"qu.tc", +"qz.bz", +"rb6.me", +"r.delphicomponent.ru", +"rdrct.us", +"rdr.to", +"read.bi", +"readthis.ca", +"reallytinyurl.com", +"redir.ec", +"redirectingat.com", +"redirects.ca", +"redirect.subscribe.ru", +"redire.ru", +"redirx.com", +"reduc.in", +"referer.us", +"retweet.cc", +"retwt.me", +"reurl.org", +"rhwm.eu", +"rickroll.it", +"r.im", +"ri.ms", +"riz.gd", +"rlu.ru", +"rmse.ru", +"rnd.ru", +"romb.su", +"r-ss.de", +"rt.nu", +"rubyurl.com", +"ru.ly", +"rurl.org", +"rurl.ru", +"rurls.ru", +"rustech.org", +"rww.tw", +"s0bu.ru", +"s0e.ru", +"s4c.in", +"s7y.us", +"s8.hk", +"safe.mn", +"saf.li", +"safelinks.protection.outlook.com", +"sami.24.gg", +"sayabit.com", +"sbrf.link.info.sberbank.ru", +"s.coop", +"scurtare-url.hi2.ro", +"sdut.us", +"securityexpert.ru", +"securl.ru", +"sendgrid.net", +"sendit.in", +"sendsay.ru", +"sendurl.info", +"sg4d.com", +"sg-url.tk", +"shadyurl.com", +"share.flocktory.com", +"shar.es", +"shink.de", +"shiturl.com", +"shli.de", +"shorl.com", +"shortb.net", +"shorten.im", +"shorten.ws", +"short.ie", +"short.im", +"shortlinks.co.uk", +"short-me.com", +"shortner.com", +"shortn.me", +"short.nr", +"short.su", +"short.to", +"shorturl.asia", +"shorturl.com", +"short-url.co.uk", +"shorturls.co.uk", +"shortz.me", +"shout.to", +"show.my", +"shrinkee.com", +"shrinkify.com", +"shrinkr.com", +"shrten.com", +"shrt.fr", +"shrtl.com", +"shrt.st", +"shrunkin.com", +"shx.in", +"simplesite.com", +"simurl.com", +"sitefwd.com", +"sk9.pl", +"slate.me", +"sli.su", +"slki.ru", +"sl.to", +"smallr.com", +"smallurl.in", +"smallurl.ru", +"smalur.com", +"s-m.co", +"smsh.me", +"smurl.ca", +"smurl.name", +"sn9.ru", +"sn.im", +"snipr.com", +"snipurl.com", +"snurl.com", +"softlinemail.ru", +"sokrati.ru", +"somexyz.com", +"song.ly", +"sorturl.net", +"so.vg", +"sp2.ro", +"spedr.com", +"speed-tester.info", +"sq6.ru", +"srclick.ru", +"srcom.net", +"srnk.net", +"srs.li", +"srtn.me", +"starts.com", +"starturl.com", +"stat-pulse.com", +"stnx.at", +"stump.ws", +"su.ly", +"su.pr", +"surl.co.uk", +"surl.hu", +"surl.me", +"surs.nl", +"susurl.com", +"swturl.com", +"t1ny.net", +"ta.gd", +"ta.gg", +"tagiturl.com", +"taourl.com", +"tbd.ly", +"t.cn", +"t.co", +"tcrn.ch", +"techto.us", +"tez.co", +"tgl.net", +"tgr.me", +"tgr.ph", +"th8.us", +"thexyz.org", +"thinfi.com", +"th.ly", +"thnlnk.com", +"thurl.in", +"tie.ly", +"tighturl.com", +"tin.cc", +"tiniuri.com", +"tiny9.com", +"tinyarro.ws", +"tiny.by", +"tiny.cc", +"tinyit.cc", +"tinylink.ca", +"tinylink.in", +"tiny.ly", +"tiny.pl", +"tiny.ps", +"tinyuri.ca", +"tinyurl.com", +"tinyurl.ru", +"tki.me", +"tldr.in", +"tl.gd", +"t.lh.com", +"tlim.ru", +"tllg.net", +"tmi.me", +"tnij.org", +"tny.com", +"tny.tc", +"to8.cc", +"togoto.us", +"to.je", +"to.ly", +"toma.ai", +"tos.co", +"totc.us", +"tourl.fr", +"toysr.us", +"tozm.com", +"tpm.ly", +"tra.kz", +"track-mail.skbkontur.ru", +"trg.li", +"trii.us", +"tr.im", +"trimurl.me", +"trk.emlbest.com", +"trk.klclick.com", +"trk.klclick1.com", +"trk.klclick2.com", +"trk.klclick3.com", +"trunc.it", +"trusteml.com", +"tty.su", +"tuckinfo.com", +"tux-pla.net", +"tvsl.eu", +"tweet.ms", +"tweez.me", +"twhub.com", +"twirl.at", +"twitclicks.com", +"twitter.com", +"twitterurl.net", +"twiturl.de", +"twiu.ru", +"twurl.cc", +"twurl.nl", +"tyny.me", +"u2s.in", +"u76.org", +"ub0.cc", +"u.info-mg.ru", +"ukril.ru", +"ulk.me", +"ulmart.ru", +"ulo.me", +"ulu.lu", +"u.mavrev.com", +"umenytt.se", +"unfake.it", +"u.nu", +"updating.me", +"ur1.ca", +"ur3.us", +"ural-tender.ru", +"url2.ru", +"url360.me", +"url3.ru", +"url4.eu", +"url4.ru", +"url4t.com", +"url5.ru", +"url66.com", +"urla.ru", +"url.az", +"url.b24.am", +"urlbit.us", +"urlborg.com", +"urlbrief.com", +"urlcantik.com", +"urlclick.ru", +"url.cn", +"urlcorta.es", +"url.co.uk", +"urlcover.com", +"urlcut.com", +"urldefender.com", +"urldefense.com", +"urldepo.ru", +"url.dflix.net", +"urlel.com", +"urlenco.de", +"urle.us", +"url.g4team.com", +"urlgator.com", +"urlgeo.me", +"urlg.in", +"url-go.com", +"urlgo.ru", +"url.ie", +"urlin.it", +"urlink.eu", +"urli.nl", +"urlite.de", +"url.lotpatrol.com", +"urlmint.com", +"url.mk.ua", +"urlms.com", +"urloid.com", +"urloo.com", +"urlot.com", +"urlredo.com", +"urlscott.com", +"urls.co.za", +"url.shinri.biz", +"urlshorteningservicefortwitter.com", +"urlshort.me", +"urls.im", +"urlsim.com", +"urlsnip.com", +"urlsp.in", +"urlsqueeze.com", +"urls.vg", +"urltwit.com", +"urlu.ms", +"urlus.ru", +"url.vsofte.ru", +"urlx.ie", +"urlxs.fr", +"ur.ly", +"url.yanclex.com", +"urlz.at", +"urlzen.com", +"url-zip.com", +"urlz.ro", +"usat.ly", +"use.my", +"u.to", +"uud.in", +"uuu.su", +"uww.me", +"uyurl.com", +"vani.sh", +"vash-repetitor.ru", +"vb.ly", +"vc8.net", +"vgn.am", +"view.my", +"vk.cc", +"vl.am", +"vll.me", +"vog.me", +"vovka.com", +"vst.tv", +"w3t.org", +"w55.de", +"wa.la", +"wapo.st", +"wapurl.co.uk", +"warble.co", +"webformyself.com", +"weblist.kharkov.ua", +"weburl.me", +"weeclix.com", +"wez.su", +"whspr.it", +"widg.me", +"wik.ro", +"wipi.es", +"wlatcy-moch.tk", +"wlink.me.uk", +"wmturls.com", +"wom.im", +"wowurl.com", +"wp.me", +"wp.nu", +"wurl.in", +"wurl.us", +"wuw.su", +"ww.tl", +"clickmeeting.com", +"grandstreamnetworks.ru", +"reg.ru", +"wz.ae", +"x2t.com", +"xaa.su", +"xav.cc", +"xcqv.com", +"xcs.me", +"xd5.net", +"xdvn.net", +"xew.co", +"xops.fr", +"xp.cm", +"xr.com", +"xrl.in", +"xrls.tk", +"xrl.us", +"x-url.com", +"xurl.es", +"xurl.jp", +"x.vu", +"xvx.su", +"xw6.com", +"xxsurl.de", +"xxw.me", +"y.ahoo.it", +"yatuc.com", +"ydn.ru", +"ye.pe", +"yep.it", +"yfrog.com", +"yhoo.it", +"yi.pe", +"yiyd.com", +"ysu.me", +"yuarel.com", +"yurl.in", +"y-url.ru", +"yurl.ru", +"z0p.de", +"z2z.ca", +"zapt.in", +"zazi.me", +"zcom.us", +"zebratelecom.ru", +"zeep.in", +"zi.ma", +"zi.mu", +"zio.in", +"zipmyurl.com", +"zolp.net", +"zrps.info", +"zti.me", +"zud.me", +"zurl.ws", +"zxc9.com", +"zzang.kr", +"zz.gd"} + + diff --git a/resources/config/spamfilter/maps/allow_dmarc.list b/resources/config/spamfilter/maps/allow_dmarc.list index 4943453b..7a973841 100644 --- a/resources/config/spamfilter/maps/allow_dmarc.list +++ b/resources/config/spamfilter/maps/allow_dmarc.list @@ -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", diff --git a/resources/config/spamfilter/maps/allow_spf_dkim.list b/resources/config/spamfilter/maps/allow_spf_dkim.list index 163593e3..25628cdc 100644 --- a/resources/config/spamfilter/maps/allow_spf_dkim.list +++ b/resources/config/spamfilter/maps/allow_spf_dkim.list @@ -150,7 +150,6 @@ spam-spdk = {"1cfresh.com", "paypal.ca", "paypal.cn", "paypal.com", -"paypal.com", "paypal.co.uk", "paypal.de", "paypal.es", diff --git a/resources/config/spamfilter/maps/spam_trap.list b/resources/config/spamfilter/maps/spam_trap.list deleted file mode 100644 index cf390a38..00000000 --- a/resources/config/spamfilter/maps/spam_trap.list +++ /dev/null @@ -1 +0,0 @@ -spam-trap = {} diff --git a/resources/config/spamfilter/maps/suffix_list.dat.gz b/resources/config/spamfilter/maps/suffix_list.dat.gz deleted file mode 100644 index c916d9decdd7302b5d41ea210b67dfe6abbe8eff..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 80969 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 Q_ct-FS07T55qqG`{ibY2NC=eLL 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≥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 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')"; diff --git a/resources/config/spamfilter/settings.toml b/resources/config/spamfilter/settings.toml new file mode 100644 index 00000000..f082c270 --- /dev/null +++ b/resources/config/spamfilter/settings.toml @@ -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'" } ] diff --git a/resources/config/store/elasticsearch.toml b/resources/config/store/elasticsearch.toml deleted file mode 100644 index f5f93bce..00000000 --- a/resources/config/store/elasticsearch.toml +++ /dev/null @@ -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 diff --git a/resources/config/store/foundationdb.toml b/resources/config/store/foundationdb.toml deleted file mode 100644 index b52705c8..00000000 --- a/resources/config/store/foundationdb.toml +++ /dev/null @@ -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 *" diff --git a/resources/config/store/fs.toml b/resources/config/store/fs.toml deleted file mode 100644 index db6d38cd..00000000 --- a/resources/config/store/fs.toml +++ /dev/null @@ -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 *" diff --git a/resources/config/store/mysql.toml b/resources/config/store/mysql.toml deleted file mode 100644 index b0fa130d..00000000 --- a/resources/config/store/mysql.toml +++ /dev/null @@ -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 *" diff --git a/resources/config/store/postgresql.toml b/resources/config/store/postgresql.toml deleted file mode 100644 index bf0d2fef..00000000 --- a/resources/config/store/postgresql.toml +++ /dev/null @@ -1,39 +0,0 @@ -############################################# -# PostgreSQL Store configuration -############################################# - -[store."postgresql"] -type = "postgresql" -host = "localhost" -port = 5432 -database = "stalwart" -user = "postgres" -password = "mysecretpassword" -timeout = "15s" -disable = true - -[store."postgresql".tls] -enable = false -allow-invalid-certs = false - -#[store."postgresql".pool] -#max-connections = 10 - -#[store."postgresql".init] -#execute = [ -# "CREATE TABLE IF NOT EXISTS accounts (name TEXT PRIMARY KEY, secret TEXT, description TEXT, type TEXT NOT NULL, quota INTEGER DEFAULT 0, active BOOLEAN DEFAULT 1)", -# "CREATE TABLE IF NOT EXISTS group_members (name TEXT NOT NULL, member_of TEXT NOT NULL, PRIMARY KEY (name, member_of))", -# "CREATE TABLE IF NOT EXISTS emails (name TEXT NOT NULL, address TEXT NOT NULL, type TEXT, PRIMARY KEY (name, address))" -#] - -[store."postgresql".query] -name = "SELECT name, type, secret, description, quota FROM accounts WHERE name = $1 AND active = true" -members = "SELECT member_of FROM group_members WHERE name = $1" -recipients = "SELECT name FROM emails WHERE address = $1 ORDER BY name ASC" -emails = "SELECT address FROM emails WHERE name = $1 AND type != 'list' ORDER BY type DESC, address ASC" -verify = "SELECT address FROM emails WHERE address LIKE '%' || $1 || '%' 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 = $1 AND l.type = 'list' ORDER BY p.address LIMIT 50" -domains = "SELECT 1 FROM emails WHERE address LIKE '%@' || $1 LIMIT 1" - -[store."postgresql".purge] -frequency = "0 3 *" diff --git a/resources/config/store/redis.toml b/resources/config/store/redis.toml deleted file mode 100644 index 92f741b8..00000000 --- a/resources/config/store/redis.toml +++ /dev/null @@ -1,18 +0,0 @@ -############################################# -# Redis Lookup Store configuration -############################################# - -[store."redis"] -type = "redis" -redis-type = "single" -urls = ["redis://127.0.0.1"] -user = "my_username" -password = "secretpassword" -timeout = "10s" -#read-from-replicas = false -disable = true - -#[store."redis".retry] -#total = 3 -#max-wait = "1s" -#min-wait = "500ms" diff --git a/resources/config/store/rocksdb.toml b/resources/config/store/rocksdb.toml deleted file mode 100644 index f2d928f5..00000000 --- a/resources/config/store/rocksdb.toml +++ /dev/null @@ -1,18 +0,0 @@ -############################################# -# RocksDB Store configuration -############################################# - -[store."rocksdb"] -type = "rocksdb" -path = "%{BASE_PATH}%/data" -disable = true - -[store."rocksdb".settings] -min-blob-size = 16834 -write-buffer-size = 134217728 - -#[store."rocksdb".pool] -#workers = 10 - -[store."rocksdb".purge] -frequency = "0 3 *" diff --git a/resources/config/store/s3.toml b/resources/config/store/s3.toml deleted file mode 100644 index c8c870c0..00000000 --- a/resources/config/store/s3.toml +++ /dev/null @@ -1,18 +0,0 @@ -############################################# -# S3/MinIO Blob Store configuration -############################################# - -[store."s3"] -type = "s3" -bucket = "stalwart" -region = "eu-central-1" -access-key = "minioadmin" -secret-key = "minioadmin" -#endpoint = "" -#security-token = "" -#profile = "" -timeout = "30s" -disable = true - -[store."s3".purge] -frequency = "0 3 *" diff --git a/resources/config/store/sqlite.toml b/resources/config/store/sqlite.toml deleted file mode 100644 index f1703c2e..00000000 --- a/resources/config/store/sqlite.toml +++ /dev/null @@ -1,31 +0,0 @@ -############################################# -# SQLite Store configuration -############################################# - -[store."sqlite"] -type = "sqlite" -path = "%{BASE_PATH}%/data/index.sqlite3" -disable = true - -#[store."sqlite".pool] -#max-connections = 10 -#workers = 10 - -#[store."sqlite".init] -#execute = [ -# "CREATE TABLE IF NOT EXISTS accounts (name TEXT PRIMARY KEY, secret TEXT, description TEXT, type TEXT NOT NULL, quota INTEGER DEFAULT 0, active BOOLEAN DEFAULT 1)", -# "CREATE TABLE IF NOT EXISTS group_members (name TEXT NOT NULL, member_of TEXT NOT NULL, PRIMARY KEY (name, member_of))", -# "CREATE TABLE IF NOT EXISTS emails (name TEXT NOT NULL, address TEXT NOT NULL, type TEXT, PRIMARY KEY (name, address))" -#] - -[store."sqlite".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 = ?" -emails = "SELECT address FROM emails WHERE name = ? AND type != 'list' ORDER BY type DESC, address ASC" -verify = "SELECT address FROM emails WHERE address LIKE '%' || ? || '%' 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 '%@' || ? LIMIT 1" - -[store."sqlite".purge] -frequency = "0 3 *" diff --git a/tests/resources/scripts/create_test_env.sh b/tests/resources/scripts/create_test_env.sh index 671c8c50..2e1ea094 100644 --- a/tests/resources/scripts/create_test_env.sh +++ b/tests/resources/scripts/create_test_env.sh @@ -2,66 +2,28 @@ BASE_DIR="/Users/me/Downloads/stalwart-test" DOMAIN="example.org" - -# Stores -#STORE="foundationdb" -#FTS_STORE="foundationdb" -#BLOB_STORE="foundationdb" -#STORE="rocksdb" -#FTS_STORE="rocksdb" -#BLOB_STORE="rocksdb" -STORE="sqlite" -FTS_STORE="sqlite" -BLOB_STORE="sqlite" -#FEATURES="foundationdb postgres mysql rocks elastic s3 redis" -FEATURES="sqlite" - -# Directories -DIRECTORY="internal" -SQL_STORE="sqlite" +FEATURES="sqlite foundationdb postgres mysql rocks elastic s3 redis" # Delete previous tests rm -rf $BASE_DIR # Create directories -mkdir -p $BASE_DIR $BASE_DIR/data $BASE_DIR/data/blobs $BASE_DIR/logs +mkdir -p $BASE_DIR $BASE_DIR/data $BASE_DIR/etc -# Copy config files -cp -r resources/config $BASE_DIR/etc - -# Copy self-signed certs -cp -r tests/resources/tls_cert.pem $BASE_DIR/etc -cp -r tests/resources/tls_privatekey.pem $BASE_DIR/etc - -# Replace stores and directories -sed -i '' -e "s|__SQL_STORE__|$SQL_STORE|g" "$BASE_DIR/etc/directory/sql.toml" -sed -i '' -e 's/disable = true//g' "$BASE_DIR/etc/directory/$DIRECTORY.toml" -sed -i '' -e 's/disable = true//g' "$BASE_DIR/etc/store/$STORE.toml" -sed -i '' -e 's/disable = true//g' "$BASE_DIR/etc/store/$FTS_STORE.toml" -sed -i '' -e 's/disable = true//g' "$BASE_DIR/etc/store/$BLOB_STORE.toml" -sed -i '' -e "s/__FTS_STORE__/$FTS_STORE/g" \ - -e "s/__BLOB_STORE__/$BLOB_STORE/g" "$BASE_DIR/etc/common/store.toml" +# Copy resources +cp -r resources/config/config.toml $BASE_DIR/etc # Replace settings -sed -i '' -e "s/__STORE__/$STORE/g" \ - -e "s/__DIRECTORY__/$DIRECTORY/g" \ - -e "s/__DOMAIN__/$DOMAIN/g" \ - -e "s/__HOST__/mail.$DOMAIN/g" \ - -e "s|__BASE_PATH__|$BASE_DIR|g" "$BASE_DIR/etc/config.toml" -sed -i '' -e "s|__CERT_PATH__|$BASE_DIR/etc/tls_cert.pem|g" \ - -e "s|__PK_PATH__|$BASE_DIR/etc/tls_privatekey.pem|g" "$BASE_DIR/etc/common/tls.toml" -sed -i '' -e 's/method = "log"/method = "stdout"/g' \ - -e 's/level = "info"/level = "trace"/g' "$BASE_DIR/etc/common/tracing.toml" -sed -i '' -e 's/%{HOST}%/127.0.0.1/g' "$BASE_DIR/etc/jmap/listener.toml" -sed -i '' -e 's/allow-plain-text = false/allow-plain-text = true/g' \ - -e 's/2000\/1m/9999999\/100m/g' \ - -e 's/concurrent = 4/concurrent = 90000/g' "$BASE_DIR/etc/imap/settings.toml" -sed -i '' -e 's/user = "stalwart-mail"//g' \ - -e 's/group = "stalwart-mail"//g' "$BASE_DIR/etc/common/server.toml" -# Generate DKIM key -mkdir -p $BASE_DIR/etc/dkim -openssl genpkey -algorithm RSA -out $BASE_DIR/etc/dkim/$DOMAIN.key +sed -i '' -e "s|%{env:STALWART_PATH}%|$BASE_DIR|g" \ + -e "s|%{env:DOMAIN}%|$DOMAIN|g" \ + -e "s|%{env:HOSTNAME}%|mail.$DOMAIN|g" \ + -e "s|%{env:OAUTH_KEY}%|12345|g" \ + -e 's/level = "info"/level = "trace"/g' "$BASE_DIR/etc/config.toml" + +#sed -i '' -e 's/allow-plain-text = false/allow-plain-text = true/g' \ +# -e 's/2000\/1m/9999999\/100m/g' \ +# -e 's/concurrent = 4/concurrent = 90000/g' "$BASE_DIR/etc/imap/settings.toml" # Create admin user SET_ADMIN_USER="admin" SET_ADMIN_PASS="secret" cargo run -p mail-server --no-default-features --features "$FEATURES" -- --config=$BASE_DIR/etc/config.toml diff --git a/tests/src/lib.rs b/tests/src/lib.rs index 569ae81a..921c6407 100644 --- a/tests/src/lib.rs +++ b/tests/src/lib.rs @@ -70,8 +70,8 @@ impl AssertConfig for utils::config::Config { } fn assert_no_warnings(self) -> Self { - if !self.missing.is_empty() { - panic!("Warnings: {:#?}", self.missing); + if !self.warnings.is_empty() { + panic!("Warnings: {:#?}", self.warnings); } self } diff --git a/tests/src/smtp/config.rs b/tests/src/smtp/config.rs index 53d89ec2..d198de88 100644 --- a/tests/src/smtp/config.rs +++ b/tests/src/smtp/config.rs @@ -63,7 +63,7 @@ fn parse_if_blocks() { // Create context and add some conditions - let token_map = TokenMap::default().with_smtp_variables(&[ + let token_map = TokenMap::default().with_variables(&[ V_RECIPIENT, V_RECIPIENT_DOMAIN, V_SENDER, @@ -83,7 +83,7 @@ fn parse_if_blocks() { IfThen { expr: Expression { items: vec![ - ExpressionItem::Variable(2), + ExpressionItem::Variable(V_SENDER), ExpressionItem::Constant(Constant::String("jdoe".to_string())), ExpressionItem::BinaryOperator(BinaryOperator::Eq) ] @@ -95,12 +95,12 @@ fn parse_if_blocks() { IfThen { expr: Expression { items: vec![ - ExpressionItem::Variable(10), + ExpressionItem::Variable(V_PRIORITY), ExpressionItem::Constant(Constant::Integer(1)), ExpressionItem::UnaryOperator(UnaryOperator::Minus), ExpressionItem::BinaryOperator(BinaryOperator::Eq), ExpressionItem::JmpIf { val: true, pos: 4 }, - ExpressionItem::Variable(0), + ExpressionItem::Variable(V_RECIPIENT), ExpressionItem::Constant(Constant::String("jane".to_string())), ExpressionItem::Function { id: 29, @@ -128,7 +128,7 @@ fn parse_if_blocks() { IfThen { expr: Expression { items: vec![ - ExpressionItem::Variable(2), + ExpressionItem::Variable(V_SENDER), ExpressionItem::Constant(Constant::String("jdoe".to_string())), ExpressionItem::BinaryOperator(BinaryOperator::Eq) ] @@ -145,12 +145,12 @@ fn parse_if_blocks() { IfThen { expr: Expression { items: vec![ - ExpressionItem::Variable(10), + ExpressionItem::Variable(V_PRIORITY), ExpressionItem::Constant(Constant::Integer(1)), ExpressionItem::UnaryOperator(UnaryOperator::Minus), ExpressionItem::BinaryOperator(BinaryOperator::Eq), ExpressionItem::JmpIf { val: true, pos: 4 }, - ExpressionItem::Variable(0), + ExpressionItem::Variable(V_RECIPIENT), ExpressionItem::Constant(Constant::String("jane".to_string())), ExpressionItem::Function { id: 29, @@ -180,7 +180,7 @@ fn parse_if_blocks() { IfThen { expr: Expression { items: vec![ - ExpressionItem::Variable(2), + ExpressionItem::Variable(V_SENDER), ExpressionItem::Constant(Constant::String("jdoe".to_string())), ExpressionItem::BinaryOperator(BinaryOperator::Eq) ] @@ -197,12 +197,12 @@ fn parse_if_blocks() { IfThen { expr: Expression { items: vec![ - ExpressionItem::Variable(10), + ExpressionItem::Variable(V_PRIORITY), ExpressionItem::Constant(Constant::Integer(1)), ExpressionItem::UnaryOperator(UnaryOperator::Minus), ExpressionItem::BinaryOperator(BinaryOperator::Eq), ExpressionItem::JmpIf { val: true, pos: 4 }, - ExpressionItem::Variable(0), + ExpressionItem::Variable(V_RECIPIENT), ExpressionItem::Constant(Constant::String("jane".to_string())), ExpressionItem::Function { id: 29, @@ -261,7 +261,7 @@ fn parse_throttles() { let throttle = parse_throttle( &mut config, "throttle", - &TokenMap::default().with_smtp_variables(&[ + &TokenMap::default().with_variables(&[ V_RECIPIENT, V_RECIPIENT_DOMAIN, V_SENDER, @@ -415,7 +415,7 @@ async fn eval_if() { let mut config = Config::new(fs::read_to_string(file).unwrap()).unwrap(); let envelope = TestEnvelope::from_config(&mut config); - let token_map = TokenMap::default().with_smtp_variables(&[ + let token_map = TokenMap::default().with_variables(&[ V_RECIPIENT, V_RECIPIENT_DOMAIN, V_SENDER, @@ -464,7 +464,7 @@ async fn eval_dynvalue() { let mut config = Config::new(fs::read_to_string(file).unwrap()).unwrap(); let envelope = TestEnvelope::from_config(&mut config); - let token_map = TokenMap::default().with_smtp_variables(&[ + let token_map = TokenMap::default().with_variables(&[ V_RECIPIENT, V_RECIPIENT_DOMAIN, V_SENDER, diff --git a/tests/src/smtp/inbound/antispam.rs b/tests/src/smtp/inbound/antispam.rs index 9327223a..2490beaf 100644 --- a/tests/src/smtp/inbound/antispam.rs +++ b/tests/src/smtp/inbound/antispam.rs @@ -30,28 +30,20 @@ use crate::smtp::{build_smtp, session::TestSession, TempDir}; const CONFIG: &str = r#" [spam.header] -add-spam = true -add-spam-result = true is-spam = "X-Spam-Status: Yes" -[spam.autolearn] -enable = true -#balance = 0.9 -balance = 0.0 - -[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] +[lookup.spam-config] +add-spam = true +add-spam-result = true +learn-enable = true +#learn-balance = "0.9" +learn-balance = "0.0" +learn-ham-replies = true +learn-ham-threshold = "-0.5" +learn-spam-threshold = "6.0" +threshold-spam = "5.0" +threshold-discard = 0 +threshold-reject = 0 directory = "" lookup = "" @@ -59,8 +51,8 @@ lookup = "" relay = true [sieve.trusted] -from-name = "Sieve Daemon" -from-addr = "sieve@foobar.org" +from-name = "'Sieve Daemon'" +from-addr = "'sieve@foobar.org'" return-path = "" hostname = "mx.foobar.org" no-capability-check = true @@ -110,13 +102,12 @@ public-suffix = "file://{LIST_PATH}/public-suffix.dat" #[tokio::test(flavor = "multi_thread")] async fn antispam() { - /*let disable = true; - tracing::subscriber::set_global_default( + /*tracing::subscriber::set_global_default( tracing_subscriber::FmtSubscriber::builder() .with_env_filter( tracing_subscriber::EnvFilter::builder() .parse( - "smtp=debug,imap=debug,jmap=debug,store=debug,utils=debug,directory=debug,common=debug", + "smtp=debug,imap=debug,jmap=debug,store=debug,utils=debug,directory=debug,common=trace", ) .unwrap(), ) @@ -212,6 +203,7 @@ async fn antispam() { config.resolve_macros().await; let stores = Stores::parse_all(&mut config).await; let core = Core::parse(&mut config, stores, Default::default()).await; + //config.assert_no_errors(); // Add mock DNS entries for (domain, ip) in [ diff --git a/tests/src/smtp/inbound/auth.rs b/tests/src/smtp/inbound/auth.rs index 0e783422..a9406319 100644 --- a/tests/src/smtp/inbound/auth.rs +++ b/tests/src/smtp/inbound/auth.rs @@ -66,7 +66,7 @@ member-of = ["sales", "support"] [session.auth] require = [{if = "remote_ip = '10.0.0.1'", then = true}, {else = false}] -mechanisms = [{if = "remote_ip = '10.0.0.1'", then = "[plain, login]"}, +mechanisms = [{if = "remote_ip = '10.0.0.1' && is_tls", then = "[plain, login]"}, {else = 0}] directory = [{if = "remote_ip = '10.0.0.1'", then = "'local'"}, {else = false}] diff --git a/tests/src/smtp/inbound/data.rs b/tests/src/smtp/inbound/data.rs index f16bc81f..71befe07 100644 --- a/tests/src/smtp/inbound/data.rs +++ b/tests/src/smtp/inbound/data.rs @@ -25,11 +25,14 @@ use common::Core; use store::Stores; use utils::config::Config; -use crate::smtp::{ - build_smtp, - inbound::TestMessage, - session::{load_test_message, TestSession, VerifyResponse}, - TempDir, TestSMTP, +use crate::{ + smtp::{ + build_smtp, + inbound::TestMessage, + session::{load_test_message, TestSession, VerifyResponse}, + TempDir, TestSMTP, + }, + AssertConfig, }; use smtp::core::{Inner, Session}; @@ -39,6 +42,7 @@ data = "sqlite" lookup = "sqlite" blob = "sqlite" fts = "sqlite" +directory = "local" [store."sqlite"] type = "sqlite" @@ -74,11 +78,6 @@ email = "mike@test.com" [session.rcpt] directory = "'local'" -[[queue.quota]] -match = "sender = 'john@doe.org'" -key = ['sender'] -messages = 1 - [session.data.limits] messages = [{if = "remote_ip = '10.0.0.1'", then = 1}, {else = 100}] @@ -98,6 +97,11 @@ date = [{if = "remote_ip = '10.0.0.3'", then = true}, return-path = [{if = "remote_ip = '10.0.0.3'", then = true}, {else = false}] +[[queue.quota]] +match = "sender = 'john@doe.org'" +key = ['sender'] +messages = 1 + [[queue.quota]] match = "rcpt_domain = 'foobar.org'" key = ['rcpt_domain'] @@ -115,9 +119,10 @@ enable = true #[tokio::test] async fn data() { // Enable logging - /*tracing::subscriber::set_global_default( + /*let disable = 1; + tracing::subscriber::set_global_default( tracing_subscriber::FmtSubscriber::builder() - .with_max_level(tracing::Level::DEBUG) + .with_max_level(tracing::Level::TRACE) .finish(), ) .unwrap();*/ @@ -128,6 +133,7 @@ async fn data() { let mut config = Config::new(tmp_dir.update_config(CONFIG)).unwrap(); let stores = Stores::parse_all(&mut config).await; let core = Core::parse(&mut config, stores, Default::default()).await; + config.assert_no_errors(); let mut qr = inner.init_test_queue(&core); // Test queue message builder diff --git a/tests/src/smtp/inbound/milter.rs b/tests/src/smtp/inbound/milter.rs index 60174000..2bfb9ffb 100644 --- a/tests/src/smtp/inbound/milter.rs +++ b/tests/src/smtp/inbound/milter.rs @@ -231,6 +231,7 @@ fn milter_address_modifications() { let mut data = SessionData::new( "127.0.0.1".parse().unwrap(), + 0, "127.0.0.1".parse().unwrap(), 0, ); @@ -335,6 +336,7 @@ fn milter_message_modifications() { let parsed_test_message = AuthenticatedMessage::parse(test_message.as_bytes()).unwrap(); let mut session_data = SessionData::new( "127.0.0.1".parse().unwrap(), + 0, "127.0.0.1".parse().unwrap(), 0, ); @@ -403,7 +405,7 @@ async fn milter_client_test() { const PORT: u16 = 7357; let mut client = MilterClient::connect( &Milter { - enable: IfBlock::default(), + enable: IfBlock::empty(""), addrs: vec![SocketAddr::from(([127, 0, 0, 1], PORT))], hostname: "localhost".to_string(), port: PORT, diff --git a/tests/src/smtp/inbound/scripts.rs b/tests/src/smtp/inbound/scripts.rs index 3974192a..aa801582 100644 --- a/tests/src/smtp/inbound/scripts.rs +++ b/tests/src/smtp/inbound/scripts.rs @@ -63,11 +63,11 @@ arguments = "['{CFG_PATH}/pipe_me.sh', 'hello', 'world']" timeout = "10s" [sieve.trusted] -from-name = "Sieve Daemon" -from-addr = "sieve@foobar.org" -return-path = "" +from-name = "'Sieve Daemon'" +from-addr = "'sieve@foobar.org'" +return-path = "''" hostname = "mx.foobar.org" -sign = ["rsa"] +sign = "['rsa']" [sieve.trusted.limits] redirects = 3 @@ -94,11 +94,20 @@ relay = true [session.data] script = "'stage_data'" +[session.data.add-headers] +received = true +received-spf = true +auth-results = true +message-id = true +date = true +return-path = false + "#; #[tokio::test] async fn sieve_scripts() { - /*tracing::subscriber::set_global_default( + /*let disable = 1; + tracing::subscriber::set_global_default( tracing_subscriber::FmtSubscriber::builder() .with_max_level(tracing::Level::TRACE) .finish(), @@ -170,7 +179,9 @@ async fn sieve_scripts() { let script = script.clone(); let params = session .build_script_parameters("data") - .set_variable("from", "john.doe@example.org"); + .set_variable("from", "john.doe@example.org") + .with_envelope(&core.core, &session) + .await; let handle = Handle::current(); let span = span.clone(); let core_ = core.clone(); diff --git a/tests/src/smtp/lookup/sql.rs b/tests/src/smtp/lookup/sql.rs index c52645c8..e2a9b901 100644 --- a/tests/src/smtp/lookup/sql.rs +++ b/tests/src/smtp/lookup/sql.rs @@ -24,8 +24,7 @@ use std::time::{Duration, Instant}; use common::{ - config::smtp::*, - expr::{tokenizer::TokenMap, Expression}, + expr::{tokenizer::TokenMap, *}, Core, }; @@ -52,6 +51,7 @@ data = "sql" blob = "sql" fts = "sql" lookup = "sql" +directory = "sql" [store."sql"] type = "sqlite" @@ -105,7 +105,7 @@ expect = "mx.foobar.org" [test."key_get"] expr = "key_get('sql', 'hello') + '-' + key_exists('sql', 'hello') + '-' + key_set('sql', 'hello', 'world') + '-' + key_get('sql', 'hello') + '-' + key_exists('sql', 'hello')" -expect = "0-0-1-world-1" +expect = "-0-1-world-1" [test."counter_get"] expr = "counter_get('sql', 'county') + '-' + counter_incr('sql', 'county', 1) + '-' + counter_incr('sql', 'county', 1) + '-' + counter_get('sql', 'county')" @@ -190,7 +190,7 @@ async fn lookup_sql() { } // Test expression functions - let token_map = TokenMap::default().with_smtp_variables(&[ + let token_map = TokenMap::default().with_variables(&[ V_RECIPIENT, V_RECIPIENT_DOMAIN, V_SENDER, diff --git a/tests/src/smtp/outbound/dane.rs b/tests/src/smtp/outbound/dane.rs index 643fcd18..fac56b5f 100644 --- a/tests/src/smtp/outbound/dane.rs +++ b/tests/src/smtp/outbound/dane.rs @@ -75,6 +75,8 @@ send = "weekly" [queue.outbound.tls] dane = "require" +starttls = "require" + "#; const REMOTE: &str = " @@ -83,12 +85,22 @@ reject-non-fqdn = false [session.rcpt] relay = true + +[session.data.add-headers] +received = true +received-spf = true +auth-results = true +message-id = true +date = true +return-path = false + "; #[tokio::test] #[serial_test::serial] async fn dane_verify() { - /*tracing::subscriber::set_global_default( + /*let disable = 1; + tracing::subscriber::set_global_default( tracing_subscriber::FmtSubscriber::builder() .with_max_level(tracing::Level::TRACE) .finish(), diff --git a/tests/src/smtp/outbound/extensions.rs b/tests/src/smtp/outbound/extensions.rs index 92c5b9f4..317fc0e2 100644 --- a/tests/src/smtp/outbound/extensions.rs +++ b/tests/src/smtp/outbound/extensions.rs @@ -54,6 +54,14 @@ size = 1500 [session.extensions] dsn = true requiretls = true + +[session.data.add-headers] +received = true +received-spf = true +auth-results = true +message-id = true +date = true +return-path = false "#; #[tokio::test] diff --git a/tests/src/smtp/outbound/mod.rs b/tests/src/smtp/outbound/mod.rs index 3b22f352..924cb9c7 100644 --- a/tests/src/smtp/outbound/mod.rs +++ b/tests/src/smtp/outbound/mod.rs @@ -51,9 +51,9 @@ pub mod throttle; pub mod tls; const CONFIG: &str = r#" -[server] -hostname = 'mx.example.org' -greeting = 'Test SMTP instance' +[session.connect] +hostname = "'mx.example.org'" +greeting = "'Test SMTP instance'" [server.listener.smtp-debug] bind = ['127.0.0.1:9925'] diff --git a/tests/src/smtp/outbound/mta_sts.rs b/tests/src/smtp/outbound/mta_sts.rs index ac8f4c53..45347800 100644 --- a/tests/src/smtp/outbound/mta_sts.rs +++ b/tests/src/smtp/outbound/mta_sts.rs @@ -62,6 +62,15 @@ reject-non-fqdn = false [session.rcpt] relay = true + +[session.data.add-headers] +received = true +received-spf = true +auth-results = true +message-id = true +date = true +return-path = false + "#; #[tokio::test] diff --git a/tests/src/smtp/outbound/tls.rs b/tests/src/smtp/outbound/tls.rs index 509f8326..0ac13609 100644 --- a/tests/src/smtp/outbound/tls.rs +++ b/tests/src/smtp/outbound/tls.rs @@ -59,7 +59,8 @@ chunking = false #[tokio::test] #[serial_test::serial] async fn starttls_optional() { - /*tracing::subscriber::set_global_default( + /*let disable = 1; + tracing::subscriber::set_global_default( tracing_subscriber::FmtSubscriber::builder() .with_max_level(tracing::Level::TRACE) .finish(), diff --git a/tests/src/smtp/session.rs b/tests/src/smtp/session.rs index 1bde62f1..5ee3a74d 100644 --- a/tests/src/smtp/session.rs +++ b/tests/src/smtp/session.rs @@ -125,6 +125,7 @@ impl TestSession for Session { }, data: SessionData::new( "127.0.0.1".parse().unwrap(), + 0, "127.0.0.1".parse().unwrap(), 0, ),