diff options
author | mdecimus <mauro@stalw.art> | 2024-03-30 18:12:40 +0100 |
---|---|---|
committer | mdecimus <mauro@stalw.art> | 2024-03-30 18:12:40 +0100 |
commit | 35562bb9fdf5fc42235e1bc075258315224d6811 (patch) | |
tree | 5f4f9761addbfc55e8b39c9e9b07e32cde655f30 | |
parent | cb4d2f15ae2cb871da9a6ec6160a1845fa52e238 (diff) |
Use safe defaults when settings are missing
120 files changed, 11733 insertions, 2070 deletions
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::<Option<Rate>>("imap.rate-limit.requests", "2000/1m") + .unwrap_or_default(), + rate_concurrent: config + .property::<Option<u64>>("imap.rate-limit.concurrent") + .unwrap_or_default(), allow_plain_auth: config .property_or_default("imap.auth.allow-plain-text", "false") .unwrap_or(false), 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::<Option<Rate>>("jmap.rate-limit.account", "1000/1m") + .unwrap_or_default(), + rate_authenticate_req: config + .property_or_default::<Option<Rate>>("authentication.rate-limit", "10/1m") + .unwrap_or_default(), + rate_anonymous: config + .property_or_default::<Option<Rate>>("jmap.rate-limit.anonymous", "100/1m") + .unwrap_or_default(), oauth_key: config .value("oauth.key") .map(|s| s.to_string()) 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<String>, + pub from_addr: IfBlock, + pub from_name: IfBlock, + pub return_path: IfBlock, + pub sign: IfBlock, pub scripts: AHashMap<String, Arc<Sieve>>, pub bayes_cache: BayesTokenCache, pub remote_lists: RwLock<AHashMap<String, RemoteList>>, @@ -182,7 +182,7 @@ impl Scripting { .unwrap_or("Auto: ") .to_string(), ) - .with_env_variable("name", "Stalwart JMAP") + .with_env_variable("name", "Stalwart Mail Server") .with_env_variable("version", env!("CARGO_PKG_VERSION")) .with_env_variable("location", "MS") .with_env_variable("phase", "during"); @@ -250,13 +250,11 @@ impl Scripting { if let Some(value) = config.property::<Duration>("sieve.trusted.limits.duplicate-expiry") { trusted_runtime.set_default_duplicate_expiry(value.as_secs()); } - let hostname = if let Some(hostname) = config.value("sieve.trusted.hostname") { - hostname.to_string() - } else { - parse_server_hostname(config) - .and_then(|h| h.into_default_string()) - .unwrap_or_else(|| "localhost".to_string()) - }; + let hostname = config + .value("sieve.trusted.hostname") + .or_else(|| config.value("lookup.default.hostname")) + .unwrap_or("localhost") + .to_string(); trusted_runtime.set_local_hostname(hostname.clone()); // Parse scripts @@ -266,19 +264,12 @@ impl Scripting { .map(|s| s.to_string()) .collect::<Vec<_>>() { - // Skip sub-scripts - if config - .property(("sieve.trusted.scripts", id.as_str(), "snippet")) - .unwrap_or(false) - { - continue; - } - - let script = config - .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::<Option<u32>>( + ("server.listener", id, "socket.ttl"), + "server.socket.ttl", + "false", + ) + .unwrap_or_default(), + backlog: config + .property_or_else::<Option<u32>>( + ("server.listener", id, "socket.backlog"), + "server.socket.backlog", + "1024", + ) + .unwrap_or_default(), + linger: config + .property_or_else::<Option<Duration>>( + ("server.listener", id, "socket.linger"), + "server.socket.linger", + "false", + ) + .unwrap_or_default(), nodelay: config .property_or_else( ("server.listener", id, "socket.nodelay"), "server.socket.nodelay", + "true", ) .unwrap_or(true), }); @@ -190,6 +207,7 @@ impl Servers { .property_or_else( ("server.listener", id, "max-connections"), "server.max-connections", + "8192", ) .unwrap_or(8192), id: id_, @@ -218,8 +236,8 @@ impl Servers { let id = id_.as_str(); // Build TLS config let acceptor = if config - .property_or_else(("server.listener", id, "tls.enable"), "server.tls.enable") - .unwrap_or(false) + .property_or_default(("server.listener", id, "tls.enable"), "true") + .unwrap_or(true) { // Parse protocol versions let mut tls_v2 = true; @@ -292,6 +310,7 @@ impl Servers { .property_or_else( ("server.listener", id, "tls.ignore-client-order"), "server.tls.ignore-client-order", + "true", ) .unwrap_or(true); @@ -302,11 +321,8 @@ impl Servers { acme_config: acme_config.clone(), default_config, implicit: config - .property_or_else( - ("server.listener", id, "tls.implicit"), - "server.tls.implicit", - ) - .unwrap_or(true), + .property_or_default(("server.listener", id, "tls.implicit"), "false") + .unwrap_or(false), } } else { TcpAcceptor::Plain 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::<Vec<_>>(); + // This ACME manager is the default when SNI is not available + let default = config + .property::<bool>(("acme", acme_id.as_str(), "default")) + .unwrap_or_default(); + // Add domains for self-signed certificate subject_names.extend(domains.iter().cloned()); @@ -115,6 +120,7 @@ impl TlsManager { domains, contact, renew_before, + default, ) { Ok(acme_provider) => { acme_providers.insert(acme_id.to_string(), acme_provider); 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::<VerifyStrategy>("auth.dkim.verify", [], "relaxed"), + sign: IfBlock::new::<()>( + "auth.dkim.sign", + [( + "local_port != 25", + "['rsa_' + key_get('default', 'domain'), 'ed_' + key_get('default', 'domain')]", + )], + "false", + ), }, arc: ArcAuthConfig { - verify: IfBlock::new(VerifyStrategy::Relaxed), - seal: Default::default(), + verify: IfBlock::new::<VerifyStrategy>("auth.arc.verify", [], "relaxed"), + seal: IfBlock::new::<()>( + "auth.arc.seal", + [], + "['rsa_' + key_get('default', 'domain'), 'ed_' + key_get('default', 'domain')]", + ), }, spf: SpfAuthConfig { - verify_ehlo: IfBlock::new(VerifyStrategy::Relaxed), - verify_mail_from: IfBlock::new(VerifyStrategy::Relaxed), + verify_ehlo: IfBlock::new::<VerifyStrategy>( + "auth.spf.verify.ehlo", + [("local_port == 25", "relaxed")], + #[cfg(not(feature = "test_mode"))] + "disable", + #[cfg(feature = "test_mode")] + "relaxed", + + ), + verify_mail_from: IfBlock::new::<VerifyStrategy>( + "auth.spf.verify.mail-from", + [("local_port == 25", "relaxed")], + #[cfg(not(feature = "test_mode"))] + "disable", + #[cfg(feature = "test_mode")] + "relaxed", + ), }, dmarc: DmarcAuthConfig { - verify: IfBlock::new(VerifyStrategy::Relaxed), + verify: IfBlock::new::<VerifyStrategy>( + "auth.dmarc.verify", + [("local_port == 25", "relaxed")], + #[cfg(not(feature = "test_mode"))] + "disable", + #[cfg(feature = "test_mode")] + "relaxed", + ), }, iprev: IpRevAuthConfig { - verify: IfBlock::new(VerifyStrategy::Relaxed), + verify: IfBlock::new::<VerifyStrategy>( + "auth.ipref.verify", + [("local_port == 25", "relaxed")], + #[cfg(not(feature = "test_mode"))] + "disable", + #[cfg(feature = "test_mode")] + "relaxed", + ), }, signers: Default::default(), sealers: Default::default(), @@ -108,27 +151,19 @@ impl Default for MailAuthConfig { impl MailAuthConfig { pub fn parse(config: &mut Config) -> Self { - let sender_vars = TokenMap::default() - .with_smtp_variables(&[ - V_SENDER, - V_SENDER_DOMAIN, - V_PRIORITY, - V_AUTHENTICATED_AS, - V_LISTENER, - V_REMOTE_IP, - V_LOCAL_IP, - ]) + let rcpt_vars = TokenMap::default() + .with_variables(SMTP_RCPT_TO_VARS) .with_constants::<VerifyStrategy>(); let conn_vars = TokenMap::default() - .with_smtp_variables(&[V_LISTENER, V_REMOTE_IP, V_LOCAL_IP]) + .with_variables(CONNECTION_VARS) .with_constants::<VerifyStrategy>(); let mut mail_auth = Self::default(); for (value, key, token_map) in [ - (&mut mail_auth.dkim.verify, "auth.dkim.verify", &sender_vars), - (&mut mail_auth.dkim.sign, "auth.dkim.sign", &sender_vars), - (&mut mail_auth.arc.verify, "auth.arc.verify", &sender_vars), - (&mut mail_auth.arc.seal, "auth.arc.seal", &sender_vars), + (&mut mail_auth.dkim.verify, "auth.dkim.verify", &rcpt_vars), + (&mut mail_auth.dkim.sign, "auth.dkim.sign", &rcpt_vars), + (&mut mail_auth.arc.verify, "auth.arc.verify", &rcpt_vars), + (&mut mail_auth.arc.seal, "auth.arc.seal", &rcpt_vars), ( &mut mail_auth.spf.verify_ehlo, "auth.spf.verify.ehlo", @@ -139,16 +174,8 @@ impl MailAuthConfig { "auth.spf.verify.mail-from", &conn_vars, ), - ( - &mut mail_auth.dmarc.verify, - "auth.dmarc.verify", - &sender_vars, - ), - ( - &mut mail_auth.iprev.verify, - "auth.iprev.verify", - &sender_vars, - ), + (&mut mail_auth.dmarc.verify, "auth.dmarc.verify", &rcpt_vars), + (&mut mail_auth.iprev.verify, "auth.iprev.verify", &conn_vars), ] { if let Some(if_block) = IfBlock::try_parse(config, key, token_map) { *value = if_block; 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> { - 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::<IpLookupStrategy>( + "queue.outbound.ip-strategy", + [], + "ipv4_then_ipv6", + ), source_ip: QueueOutboundSourceIp { - ipv4: Default::default(), - ipv6: Default::default(), + ipv4: IfBlock::empty("queue.outbound.source-ip.v4"), + ipv6: IfBlock::empty("queue.outbound.source-ip.v6"), }, tls: QueueOutboundTls { - dane: IfBlock::new(RequireOptional::Optional), - mta_sts: IfBlock::new(RequireOptional::Optional), - start: IfBlock::new(RequireOptional::Optional), - invalid_certs: IfBlock::new(false), + dane: IfBlock::new::<RequireOptional>("queue.outbound.tls.dane", [], "optional"), + mta_sts: IfBlock::new::<RequireOptional>( + "queue.outbound.tls.mta-sts", + [], + "optional", + ), + start: IfBlock::new::<RequireOptional>( + "queue.outbound.tls.starttls", + [], + #[cfg(not(feature = "test_mode"))] + "require", + #[cfg(feature = "test_mode")] + "optional", + ), + invalid_certs: IfBlock::new::<()>( + "queue.outbound.tls.allow-invalid-certs", + [], + "false", + ), }, dsn: Dsn { - name: IfBlock::new("Mail Delivery Subsystem".to_string()), - address: IfBlock::new("MAILER-DAEMON@localhost".to_string()), - sign: Default::default(), + name: IfBlock::new::<()>("report.dsn.from-name", [], "'Mail Delivery Subsystem'"), + address: IfBlock::new::<()>( + "report.dsn.from-address", + [], + "'MAILER-DAEMON@' + key_get('default', 'domain')", + ), + sign: IfBlock::new::<()>( + "report.dsn.sign", + [], + "['rsa_' + key_get('default', 'domain'), 'ed_' + key_get('default', 'domain')]", + ), }, timeout: QueueOutboundTimeout { - connect: IfBlock::new(Duration::from_secs(5 * 60)), - greeting: IfBlock::new(Duration::from_secs(5 * 60)), - tls: IfBlock::new(Duration::from_secs(3 * 60)), - ehlo: IfBlock::new(Duration::from_secs(5 * 60)), - mail: IfBlock::new(Duration::from_secs(5 * 60)), - rcpt: IfBlock::new(Duration::from_secs(5 * 60)), - data: IfBlock::new(Duration::from_secs(10 * 60)), - mta_sts: IfBlock::new(Duration::from_secs(10 * 60)), + connect: IfBlock::new::<()>("queue.outbound.timeouts.connect", [], "5m"), + greeting: IfBlock::new::<()>("queue.outbound.timeouts.greeting", [], "5m"), + tls: IfBlock::new::<()>("queue.outbound.timeouts.tls", [], "3m"), + ehlo: IfBlock::new::<()>("queue.outbound.timeouts.ehlo", [], "5m"), + mail: IfBlock::new::<()>("queue.outbound.timeouts.mail-from", [], "5m"), + rcpt: IfBlock::new::<()>("queue.outbound.timeouts.rcpt-to", [], "5m"), + data: IfBlock::new::<()>("queue.outbound.timeouts.data", [], "10m"), + mta_sts: IfBlock::new::<()>("queue.outbound.timeouts.mta-sts", [], "10m"), }, throttle: QueueThrottle { sender: Default::default(), @@ -172,39 +212,14 @@ impl Default for QueueConfig { impl QueueConfig { pub fn parse(config: &mut Config) -> Self { let mut queue = QueueConfig::default(); - let rcpt_vars = TokenMap::default().with_smtp_variables(&[ - V_RECIPIENT_DOMAIN, - V_SENDER, - V_SENDER_DOMAIN, - V_PRIORITY, - ]); - let sender_vars = - TokenMap::default().with_smtp_variables(&[V_SENDER, V_SENDER_DOMAIN, V_PRIORITY]); - let mx_vars = TokenMap::default().with_smtp_variables(&[ - V_RECIPIENT_DOMAIN, - V_SENDER, - V_SENDER_DOMAIN, - V_PRIORITY, - V_MX, - ]); - let host_vars = TokenMap::default().with_smtp_variables(&[ - V_RECIPIENT_DOMAIN, - V_SENDER, - V_SENDER_DOMAIN, - V_PRIORITY, - V_LOCAL_IP, - V_REMOTE_IP, - V_MX, - ]); + let rcpt_vars = TokenMap::default().with_variables(SMTP_QUEUE_RCPT_VARS); + let sender_vars = TokenMap::default().with_variables(SMTP_QUEUE_SENDER_VARS); + let mx_vars = TokenMap::default().with_variables(SMTP_QUEUE_MX_VARS); + let host_vars = TokenMap::default().with_variables(SMTP_QUEUE_HOST_VARS); let ip_strategy_vars = sender_vars.clone().with_constants::<IpLookupStrategy>(); let dane_vars = mx_vars.clone().with_constants::<RequireOptional>(); let mta_sts_vars = rcpt_vars.clone().with_constants::<RequireOptional>(); - // Parse default server hostname - if let Some(hostname) = parse_server_hostname(config) { - queue.hostname = hostname.into_default("queue.outbound.hostname"); - } - for (value, key, token_map) in [ (&mut queue.retry, "queue.schedule.retry", &host_vars), (&mut queue.notify, "queue.schedule.notify", &rcpt_vars), @@ -368,15 +383,7 @@ fn parse_queue_throttle(config: &mut Config) -> QueueThrottle { let all_throttles = parse_throttle( config, "queue.throttle", - &TokenMap::default().with_smtp_variables(&[ - V_RECIPIENT_DOMAIN, - V_SENDER, - V_SENDER_DOMAIN, - V_PRIORITY, - V_MX, - V_REMOTE_IP, - V_LOCAL_IP, - ]), + &TokenMap::default().with_variables(SMTP_QUEUE_HOST_VARS), THROTTLE_RCPT_DOMAIN | THROTTLE_SENDER | THROTTLE_SENDER_DOMAIN @@ -487,22 +494,18 @@ fn parse_queue_quota_item(config: &mut Config, prefix: impl AsKey) -> Option<Que expr: Expression::try_parse( config, (prefix.as_str(), "match"), - &TokenMap::default().with_smtp_variables(&[ - V_RECIPIENT, - V_RECIPIENT_DOMAIN, - V_SENDER, - V_SENDER_DOMAIN, - V_PRIORITY, - ]), + &TokenMap::default().with_variables(SMTP_QUEUE_HOST_VARS), ) .unwrap_or_default(), keys, size: config - .property::<usize>((prefix.as_str(), "size")) - .filter(|&v| v > 0), + .property::<Option<usize>>((prefix.as_str(), "size")) + .filter(|&v| v.as_ref().map_or(false, |v| *v > 0)) + .unwrap_or_default(), messages: config - .property::<usize>((prefix.as_str(), "messages")) - .filter(|&v| v > 0), + .property::<Option<usize>>((prefix.as_str(), "messages")) + .filter(|&v| v.as_ref().map_or(false, |v| *v > 0)) + .unwrap_or_default(), }; // Validate 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::<Option<Duration>>("report.analysis.store", "30d") + .unwrap_or_default(), }, - dkim: Report::parse(config, "dkim", &default_hostname, &sender_vars), - spf: Report::parse(config, "spf", &default_hostname, &sender_vars), - dmarc: Report::parse(config, "dmarc", &default_hostname, &sender_vars), + dkim: Report::parse(config, "dkim", &rcpt_vars), + spf: Report::parse(config, "spf", &sender_vars), + dmarc: Report::parse(config, "dmarc", &rcpt_vars), dmarc_aggregate: AggregateReport::parse( config, "dmarc", - &default_hostname, - &sender_vars.with_constants::<AggregateFrequency>(), + &rcpt_vars.with_constants::<AggregateFrequency>(), ), tls: AggregateReport::parse( config, "tls", - &default_hostname, - &rcpt_vars.with_constants::<AggregateFrequency>(), + &TokenMap::default() + .with_variables(SMTP_QUEUE_HOST_VARS) + .with_constants::<AggregateFrequency>(), ), } } } impl Report { - pub fn parse( - config: &mut Config, - id: &str, - default_hostname: &str, - token_map: &TokenMap, - ) -> Self { + pub fn parse(config: &mut Config, id: &str, token_map: &TokenMap) -> Self { let mut report = Self { - name: IfBlock::new(format!("{} Reporting", id.to_ascii_uppercase())), - address: IfBlock::new(format!("MAILER-DAEMON@{default_hostname}")), - subject: IfBlock::new(format!("{} Report", id.to_ascii_uppercase())), - sign: Default::default(), - send: Default::default(), + name: IfBlock::new::<()>(format!("report.{id}.from-name"), [], "'Report Subsystem'"), + address: IfBlock::new::<()>( + format!("report.{id}.from-address"), + [], + format!("'noreply-{id}@' + key_get('default', 'domain')"), + ), + subject: IfBlock::new::<()>( + format!("report.{id}.subject"), + [], + format!( + "'{} Authentication Failure Report'", + id.to_ascii_uppercase() + ), + ), + sign: IfBlock::new::<()>( + format!("report.{id}.sign"), + [], + "['rsa_' + key_get('default', 'domain'), 'ed_' + key_get('default', 'domain')]", + ), + send: IfBlock::new::<()>(format!("report.{id}.send"), [], "[1, 1d]"), }; for (value, key) in [ (&mut report.name, "from-name"), @@ -158,22 +146,37 @@ impl Report { } impl AggregateReport { - pub fn parse( - config: &mut Config, - id: &str, - default_hostname: &str, - token_map: &TokenMap, - ) -> Self { - let rcpt_vars = TokenMap::default().with_smtp_variables(&[V_RECIPIENT_DOMAIN]); + pub fn parse(config: &mut Config, id: &str, token_map: &TokenMap) -> Self { + let rcpt_vars = TokenMap::default().with_variables(RCPT_DOMAIN_VARS); let mut report = Self { - name: IfBlock::new(format!("{} Aggregate Report", id.to_ascii_uppercase())), - address: IfBlock::new(format!("noreply-{id}@{default_hostname}")), - org_name: Default::default(), - contact_info: Default::default(), - send: IfBlock::new(AggregateFrequency::Never), - sign: Default::default(), - max_size: IfBlock::new(25 * 1024 * 1024), + name: IfBlock::new::<()>( + format!("report.{id}.aggregate.from-name"), + [], + format!("'{} Aggregate Report'", id.to_ascii_uppercase()), + ), + address: IfBlock::new::<()>( + format!("report.{id}.aggregate.from-address"), + [], + format!("'noreply-{id}@' + key_get('default', 'domain')"), + ), + org_name: IfBlock::new::<()>( + format!("report.{id}.aggregate.org-name"), + [], + "key_get('default', 'domain')", + ), + contact_info: IfBlock::empty(format!("report.{id}.aggregate.contact-info")), + send: IfBlock::new::<AggregateFrequency>( + format!("report.{id}.aggregate.send"), + [], + "daily", + ), + sign: IfBlock::new::<()>( + format!("report.{id}.aggregate.sign"), + [], + "['rsa_' + key_get('default', 'domain'), 'ed_' + key_get('default', 'domain')]", + ), + max_size: IfBlock::new::<()>(format!("report.{id}.aggregate.max-size"), [], "26214400"), }; for (value, key, token_map) in [ @@ -200,45 +203,7 @@ impl AggregateReport { impl Default for ReportConfig { fn default() -> Self { - Self { - submitter: IfBlock::new("localhost".to_string()), - analysis: ReportAnalysis { - addresses: Default::default(), - forward: true, - store: None, - }, - dkim: Default::default(), - spf: Default::default(), - dmarc: Default::default(), - dmarc_aggregate: Default::default(), - tls: Default::default(), - } - } -} - -impl Default for Report { - fn default() -> Self { - Self { - name: IfBlock::new("Mail Delivery Subsystem".to_string()), - address: IfBlock::new("MAILER-DAEMON@localhost".to_string()), - subject: IfBlock::new("Report".to_string()), - sign: Default::default(), - send: Default::default(), - } - } -} - -impl Default for AggregateReport { - fn default() -> Self { - Self { - name: IfBlock::new("Reporting Subsystem".to_string()), - address: IfBlock::new("no-replyN@localhost".to_string()), - org_name: Default::default(), - contact_info: Default::default(), - send: IfBlock::new(AggregateFrequency::Never), - sign: Default::default(), - max_size: IfBlock::new(25 * 1024 * 1024), - } + Self::parse(&mut Config::default()) } } 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::<MtPriority>(); let mechanisms_vars = has_ehlo_hars.clone().with_constants::<Mechanism>(); @@ -223,6 +203,11 @@ impl SessionConfig { &has_conn_vars, ), ( + &mut session.connect.hostname, + "session.connect.hostname", + &has_conn_vars, + ), + ( &mut session.connect.greeting, "session.connect.greeting", &has_conn_vars, @@ -318,11 +303,6 @@ impl SessionConfig { &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", &has_ehlo_hars, @@ -438,18 +418,7 @@ impl SessionThrottle { let all_throttles = parse_throttle( config, "session.throttle", - &TokenMap::default().with_smtp_variables(&[ - V_SENDER, - V_SENDER_DOMAIN, - V_RECIPIENT, - V_RECIPIENT_DOMAIN, - V_AUTHENTICATED_AS, - V_LISTENER, - V_REMOTE_IP, - V_LOCAL_IP, - V_PRIORITY, - V_HELO_DOMAIN, - ]), + &TokenMap::default().with_variables(SMTP_RCPT_TO_VARS), THROTTLE_LISTENER | THROTTLE_REMOTE_IP | THROTTLE_LOCAL_IP @@ -500,7 +469,9 @@ fn parse_pipe(config: &mut Config, id: &str, token_map: &TokenMap) -> Option<Pip command: IfBlock::try_parse(config, ("session.data.pipe", id, "command"), token_map)?, arguments: IfBlock::try_parse(config, ("session.data.pipe", id, "arguments"), token_map)?, timeout: IfBlock::try_parse(config, ("session.data.pipe", id, "timeout"), token_map) - .unwrap_or_else(|| IfBlock::new(Duration::from_secs(30))), + .unwrap_or_else(|| { + IfBlock::new::<()>(format!("session.data.pipe.{id}.timeout"), [], "30s") + }), }) } @@ -511,7 +482,9 @@ fn parse_milter(config: &mut Config, id: &str, token_map: &TokenMap) -> Option<M let port = config.property_require(("session.data.milter", id, "port"))?; Some(Milter { enable: IfBlock::try_parse(config, ("session.data.milter", id, "enable"), token_map) - .unwrap_or_default(), + .unwrap_or_else(|| { + IfBlock::new::<()>(format!("session.data.milter.{id}.enable"), [], "false") + }), addrs: format!("{}:{}", hostname, port) .to_socket_addrs() .map_err(|err| { @@ -573,72 +546,165 @@ fn parse_milter(config: &mut Config, id: &str, token_map: &TokenMap) -> Option<M impl Default for SessionConfig { fn default() -> Self { Self { - timeout: IfBlock::new(Duration::from_secs(15 * 60)), - duration: IfBlock::new(Duration::from_secs(5 * 60)), - transfer_limit: IfBlock::new(250 * 1024 * 1024), + timeout: IfBlock::new::<()>("session.timeout", [], "5m"), + duration: IfBlock::new::<()>("session.duration", [], "10m"), + transfer_limit: IfBlock::new::<()>("session.transfer-limit", [], "262144000"), throttle: SessionThrottle { connect: Default::default(), mail_from: Default::default(), rcpt_to: Default::default(), }, connect: Connect { - script: Default::default(), - greeting: IfBlock::new("Stalwart ESMTP at your service".to_string()), + hostname: IfBlock::new::<()>( + "server.connect.hostname", + [], + "key_get('default', 'hostname')", + ), + script: IfBlock::empty("session.connect.script"), + greeting: IfBlock::new::<()>( + "session.connect.greeting", + [], + "'Stalwart ESMTP at your service'", + ), }, ehlo: Ehlo { - script: Default::default(), - require: IfBlock::new(true), - reject_non_fqdn: IfBlock::new(true), + script: IfBlock::empty("session.ehlo.script"), + require: IfBlock::new::<()>("session.ehlo.require", [], "true"), + reject_non_fqdn: IfBlock::new::<()>( + "session.ehlo.reject-non-fqdn", + [("local_port == 25", "true")], + "false", + ), }, auth: Auth { - directory: Default::default(), - mechanisms: Default::default(), - require: IfBlock::new(false), - allow_plain_text: IfBlock::new(false), - must_match_sender: IfBlock::new(true), - errors_max: IfBlock::new(3), - errors_wait: IfBlock::new(Duration::from_secs(30)), + directory: IfBlock::new::<()>( + "session.auth.directory", + #[cfg(feature = "test_mode")] + [], + #[cfg(not(feature = "test_mode"))] + [("local_port != 25", "'*'")], + "false", + ), + mechanisms: IfBlock::new::<Mechanism>( + "session.auth.mechanisms", + [("local_port != 25 && is_tls", "[plain, login]")], + "false", + ), + require: IfBlock::new::<()>( + "session.auth.require", + #[cfg(feature = "test_mode")] + [], + #[cfg(not(feature = "test_mode"))] + [("local_port != 25", "'*'")], + "false", + ), + must_match_sender: IfBlock::new::<()>("session.auth.must-match-sender", [], "true"), + errors_max: IfBlock::new::<()>("session.auth.errors.total", [], "3"), + errors_wait: IfBlock::new::<()>("session.auth.errors.wait", [], "5s"), }, mail: Mail { - script: Default::default(), - rewrite: Default::default(), + script: IfBlock::empty("session.mail.script"), + rewrite: IfBlock::empty("session.mail.rewrite"), }, rcpt: Rcpt { - script: Default::default(), - relay: IfBlock::new(false), - directory: Default::default(), - rewrite: Default::default(), - errors_max: IfBlock::new(10), - errors_wait: IfBlock::new(Duration::from_secs(30)), - max_recipients: IfBlock::new(100), - catch_all: AddressMapping::Disable, - subaddressing: AddressMapping::Disable, + script: IfBlock::empty("session.rcpt."), + relay: IfBlock::new::<()>( + "session.rcpt.relay", + [("!is_empty(authenticated_as)", "true")], + "false", + ), + directory: IfBlock::new::<()>( + "session.rcpt.directory", + [], + #[cfg(feature = "test_mode")] + "false", + #[cfg(not(feature = "test_mode"))] + "'*'", + ), + rewrite: IfBlock::empty("session.rcpt.rewrite"), + errors_max: IfBlock::new::<()>("session.rcpt.errors.total", [], "5"), + errors_wait: IfBlock::new::<()>("session.rcpt.errors.wait", [], "5s"), + max_recipients: IfBlock::new::<()>("session.rcpt.max-recipients", [], "100"), + catch_all: AddressMapping::Enable, + subaddressing: AddressMapping::Enable, }, data: Data { - script: Default::default(), + script: IfBlock::empty("session.data.script"), pipe_commands: Default::default(), milters: Default::default(), - max_messages: IfBlock::new(10), - max_message_size: IfBlock::new(25 * 1024 * 1024), - max_received_headers: IfBlock::new(50), - add_received: IfBlock::new(true), - add_received_spf: IfBlock::new(true), - add_return_path: IfBlock::new(true), - add_auth_results: IfBlock::new(true), - add_message_id: IfBlock::new(true), - add_date: IfBlock::new(true), + max_messages: IfBlock::new::<()>("session.data.limits.messages", [], "10"), + max_message_size: IfBlock::new::<()>("session.data.limits.size", [], "104857600"), + max_received_headers: IfBlock::new::<()>( + "session.data.limits.received-headers", + [], + "50", + ), + add_received: IfBlock::new::<()>( + "session.data.add-headers.received", + [("local_port == 25", "true")], + "false", + ), + add_received_spf: IfBlock::new::<()>( + "session.data.add-headers.received-spf", + [("local_port == 25", "true")], + "false", + ), + add_return_path: IfBlock::new::<()>( + "session.data.add-headers.return-path", + [("local_port == 25", "true")], + "false", + ), + add_auth_results: IfBlock::new::<()>( + "session.data.add-headers.auth-results", + [("local_port == 25", "true")], + "false", + ), + add_message_id: IfBlock::new::<()>( + "session.data.add-headers.message-id", + [("local_port == 25", "true")], + "false", + ), + add_date: IfBlock::new::<()>( + "session.data.add-headers.date", + [("local_port == 25", "true")], + "false", + ), }, extensions: Extensions { - pipelining: IfBlock::new(true), - chunking: IfBlock::new(true), - requiretls: IfBlock::new(true), - dsn: IfBlock::new(false), - vrfy: IfBlock::new(false), - expn: IfBlock::new(false), - no_soliciting: IfBlock::new(false), - future_release: Default::default(), - deliver_by: Default::default(), - mt_priority: Default::default(), + pipelining: IfBlock::new::<()>("session.extensions.pipelining", [], "true"), + chunking: IfBlock::new::<()>("session.extensions.chunking", [], "true"), + requiretls: IfBlock::new::<()>("session.extensions.requiretls", [], "true"), + dsn: IfBlock::new::<()>( + "session.extensions.dsn", + [("!is_empty(authenticated_as)", "true")], + "false", + ), + vrfy: IfBlock::new::<()>( + "session.extensions.vrfy", + [("!is_empty(authenticated_as)", "true")], + "false", + ), + expn: IfBlock::new::<()>( + "session.extensions.expn", + [("!is_empty(authenticated_as)", "true")], + "false", + ), + no_soliciting: IfBlock::new::<()>("session.extensions.no-soliciting", [], "''"), + future_release: IfBlock::new::<()>( + "session.extensions.future-release", + [("!is_empty(authenticated_as)", "7d")], + "false", + ), + deliver_by: IfBlock::new::<()>( + "session.extensions.deliver-by", + [("!is_empty(authenticated_as)", "15d")], + "false", + ), + mt_priority: IfBlock::new::<MtPriority>( + "session.extensions.mt-priority", + [("!is_empty(authenticated_as)", "mixer")], + "false", + ), }, } } 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::<u64>((prefix.as_str(), "concurrency")) - .filter(|&v| v > 0), + .property::<Option<u64>>((prefix.as_str(), "concurrency")) + .filter(|&v| v.as_ref().map_or(false, |v| *v > 0)) + .unwrap_or_default(), rate: config - .property::<Rate>((prefix.as_str(), "rate")) - .filter(|v| v.requests > 0), + .property::<Option<Rate>>((prefix.as_str(), "rate")) + .filter(|v| v.as_ref().map_or(false, |r| r.requests > 0)) + .unwrap_or_default(), }; // Validate 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<Tracer>, } 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<T: Into<Constant>>(value: T) -> Self { + pub fn new<T: ConstantValue>( + key: impl Into<String>, + if_thens: impl IntoIterator<Item = (&'static str, &'static str)>, + default: impl AsRef<str>, + ) -> Self { + let token_map = TokenMap::default() + .with_all_variables() + .with_constants::<T>(); + Self { - key: String::new(), - if_then: Vec::new(), - default: Expression::from(value), + key: key.into(), + if_then: if_thens + .into_iter() + .map(|(if_, then)| IfThen { + expr: Expression::parse(&token_map, if_), + then: Expression::parse(&token_map, then), + }) + .collect(), + default: Expression::parse(&token_map, default.as_ref()), + } + } + + pub fn empty(key: impl Into<String>) -> Self { + Self { + key: key.into(), + if_then: Default::default(), + default: Expression { + items: Default::default(), + }, } } @@ -78,6 +102,12 @@ impl Expression { None } } + + fn parse(token_map: &TokenMap, expr: &str) -> Self { + ExpressionParser::new(Tokenizer::new(expr, token_map)) + .parse() + .unwrap() + } } impl IfBlock { @@ -91,7 +121,10 @@ impl IfBlock { // Parse conditions let mut if_block = IfBlock { key, - ..Default::default() + if_then: Default::default(), + default: Expression { + items: Default::default(), + }, }; // Try first with a single value @@ -181,7 +214,6 @@ impl IfBlock { } if !found_if { - config.new_missing_property(if_block.key); None } else if !found_then { config.new_parse_error( 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<i32> for Variable<'_> { } } +impl From<u16> for Variable<'_> { + fn from(value: u16) -> Self { + Variable::Integer(value as i64) + } +} + impl From<i16> for Variable<'_> { fn from(value: i16) -> Self { Variable::Integer(value as i64) @@ -292,12 +334,32 @@ impl PartialEq for Token { impl Eq for Token {} +pub struct NoConstants; + pub trait ConstantValue: ParseValue + for<'x> TryFrom<Variable<'x>> + Into<Constant> + Sized { fn add_constants(token_map: &mut TokenMap); } +impl ConstantValue for () { + fn add_constants(_: &mut TokenMap) {} +} + +impl From<()> for Constant { + fn from(_: ()) -> Self { + Constant::Integer(0) + } +} + +impl<'x> TryFrom<Variable<'x>> for () { + type Error = (); + + fn try_from(_: Variable<'x>) -> Result<Self, Self::Error> { + Ok(()) + } +} + impl ConstantValue for Duration { fn add_constants(_: &mut TokenMap) {} } 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<I>(mut self, vars: I) -> Self + pub fn with_all_variables(self) -> Self { + self.with_variables(&[ + V_RECIPIENT, + V_RECIPIENT_DOMAIN, + V_SENDER, + V_SENDER_DOMAIN, + V_MX, + V_HELO_DOMAIN, + V_AUTHENTICATED_AS, + V_LISTENER, + V_REMOTE_IP, + V_REMOTE_PORT, + V_LOCAL_IP, + V_LOCAL_PORT, + V_PRIORITY, + V_PROTOCOL, + V_TLS, + ]) + } + + pub fn with_variables(mut self, variables: &[u32]) -> Self { + for (name, idx) in VARIABLES_MAP { + if variables.contains(idx) { + self.tokens.insert(name, Token::Variable(*idx)); + } + } + + self + } + + pub fn with_variables_map<I>(mut self, vars: I) -> Self where I: IntoIterator<Item = (&'static str, u32)>, { 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<ArcSwap<Core>>; -#[derive(Default, Clone)] +#[derive(Clone, Default)] pub struct Core { pub storage: Storage, pub sieve: Scripting, @@ -56,7 +56,6 @@ pub struct Core { #[derive(Clone)] pub struct Network { pub blocked_ips: BlockedIps, - pub hostname: IfBlock, pub url: IfBlock, } @@ -249,7 +248,7 @@ impl Tracers { | Tracer::Otel { level, .. } => level, }; - if tracer_level > level { + if tracer_level < level { level = tracer_level; } } 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<String>, renew_before: chrono::Duration, account_key: ArcSwap<Vec<u8>>, + default: bool, } pub struct AcmeResolver { @@ -73,6 +74,7 @@ impl AcmeProvider { domains: Vec<String>, contact: Vec<String>, renew_before: Duration, + default: bool, ) -> utils::config::Result<Self> { Ok(AcmeProvider { id, @@ -90,6 +92,7 @@ impl AcmeProvider { renew_before: chrono::Duration::from_std(renew_before).unwrap(), domains, account_key: Default::default(), + default, }) } } @@ -139,6 +142,7 @@ impl Clone for AcmeProvider { contact: self.contact.clone(), renew_before: self.renew_before, account_key: ArcSwap::from_pointee(self.account_key.load().as_ref().clone()), + default: self.default, } } } 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::<Rate>("authentication.fail2ban"), + limiter_rate: config.property_or_default::<Rate>("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<T: SessionStream>( &self, stream: T, - local_ip: IpAddr, + local_addr: SocketAddr, remote_addr: SocketAddr, core: &Core, ) -> Option<SessionData<T>>; @@ -193,7 +193,7 @@ impl BuildSession for Arc<ServerInstance> { fn build_session<T: SessionStream>( &self, stream: T, - local_ip: IpAddr, + local_addr: SocketAddr, remote_addr: SocketAddr, core: &Core, ) -> Option<SessionData<T>> { @@ -231,9 +231,11 @@ impl BuildSession for Arc<ServerInstance> { remote.ip = remote_ip.to_string(), remote.port = remote_port, ), - local_ip, + local_ip: local_addr.ip(), + local_port: local_addr.port(), remote_ip, remote_port, + protocol: self.protocol, instance: self.clone(), } .into() 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<T: SessionStream> { pub stream: T, pub local_ip: IpAddr, + pub local_port: u16, pub remote_ip: IpAddr, pub remote_port: u16, + pub protocol: ServerProtocol, pub span: tracing::Span, pub in_flight: InFlight, pub instance: Arc<ServerInstance>, @@ -117,8 +116,10 @@ pub trait SessionManager: Sync + Send + 'static + Clone { let session = SessionData { stream, local_ip: session.local_ip, + local_port: session.local_port, remote_ip: session.remote_ip, remote_port: session.remote_port, + protocol: session.protocol, span: session.span, in_flight: session.in_flight, instance: session.instance, @@ -157,21 +158,16 @@ pub trait SessionManager: Sync + Send + 'static + Clone { fn shutdown(&self) -> impl std::future::Future<Output = ()> + Send; } -impl ResolveVariable for ServerInstance { - fn resolve_variable(&self, variable: u32) -> crate::expr::Variable<'_> { - match variable { - V_LISTENER => self.id.as_str().into(), - _ => crate::expr::Variable::default(), - } - } -} - impl<T: SessionStream> ResolveVariable for SessionData<T> { fn resolve_variable(&self, variable: u32) -> crate::expr::Variable<'_> { match variable { V_REMOTE_IP => self.remote_ip.to_string().into(), + V_REMOTE_PORT => self.remote_port.into(), V_LOCAL_IP => self.local_ip.to_string().into(), + V_LOCAL_PORT => self.local_port.into(), V_LISTENER => self.instance.id.as_str().into(), + V_PROTOCOL => self.protocol.as_str().into(), + V_TLS => self.stream.is_tls().into(), _ => crate::expr::Variable::default(), } } @@ -180,7 +176,17 @@ impl<T: SessionStream> ResolveVariable for SessionData<T> { impl Debug for TcpAcceptor { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Self::Tls { .. } => f.debug_tuple("Tls").finish(), + Self::Tls { + acme_config, + default_config, + implicit, + .. + } => f + .debug_struct("Tls") + .field("acme_config", acme_config) + .field("default_config", default_config) + .field("implicit", implicit) + .finish(), Self::Plain => write!(f, "Plain"), } } 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<ServerInstance>, pub local_ip: IpAddr, + pub local_port: u16, pub remote_ip: IpAddr, + pub remote_port: u16, pub is_tls: bool, } @@ -366,7 +367,9 @@ impl JMAP { HttpSessionData { instance, local_ip: session.local_ip, + local_port: session.local_port, remote_ip, + remote_port: session.remote_port, is_tls, }, ) @@ -423,7 +426,11 @@ impl ResolveVariable for HttpSessionData { fn resolve_variable(&self, variable: u32) -> common::expr::Variable<'_> { match variable { V_REMOTE_IP => self.remote_ip.to_string().into(), + V_REMOTE_PORT => self.remote_port.into(), V_LOCAL_IP => self.local_ip.to_string().into(), + V_LOCAL_PORT => self.local_port.into(), + V_TLS => self.is_tls.into(), + V_PROTOCOL => if self.is_tls { "https" } else { "http" }.into(), V_LISTENER => self.instance.id.as_str().into(), _ => common::expr::Variable::default(), } @@ -434,7 +441,14 @@ impl HttpSessionData { pub async fn resolve_url(&self, core: &Core) -> String { core.eval_if(&core.network.url, self) .await - .unwrap_or_else(|| format!("http{}://localhost", if self.is_tls { "s" } else { "" })) + .unwrap_or_else(|| { + format!( + "http{}://{}:{}", + if self.is_tls { "s" } else { "" }, + self.local_ip, + self.local_port + ) + }) } } 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<T: AsyncWrite + AsyncRead> { pub struct SessionData { pub local_ip: IpAddr, pub local_ip_str: String, + pub local_port: u16, pub remote_ip: IpAddr, pub remote_ip_str: String, pub remote_port: u16, @@ -200,7 +201,6 @@ pub struct SessionParameters { pub auth_require: bool, pub auth_errors_max: usize, pub auth_errors_wait: Duration, - pub auth_plain_text: bool, pub auth_match_sender: bool, // Rcpt parameters @@ -219,9 +219,10 @@ pub struct SessionParameters { } impl SessionData { - pub fn new(local_ip: IpAddr, remote_ip: IpAddr, remote_port: u16) -> Self { + pub fn new(local_ip: IpAddr, local_port: u16, remote_ip: IpAddr, remote_port: u16) -> Self { SessionData { local_ip, + local_port, remote_ip, local_ip_str: local_ip.to_string(), remote_ip_str: remote_ip.to_string(), @@ -337,7 +338,6 @@ impl Session<common::listener::stream::NullIo> { auth_require: Default::default(), auth_errors_max: Default::default(), auth_errors_wait: Default::default(), - auth_plain_text: false, rcpt_errors_max: Default::default(), rcpt_errors_wait: Default::default(), rcpt_max: Default::default(), @@ -395,6 +395,7 @@ impl SessionData { local_ip_str: "127.0.0.1".to_string(), remote_ip_str: "127.0.0.1".to_string(), remote_port: 0, + local_port: 0, helo_domain: "localhost".into(), mail_from, rcpt_to, 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<T: AsyncRead + AsyncWrite> Session<T> { +impl<T: SessionStream> Session<T> { pub async fn eval_session_params(&mut self) { let c = &self.core.core.smtp.session; self.data.bytes_left = self @@ -111,12 +110,6 @@ impl<T: AsyncRead + AsyncWrite> Session<T> { .eval_if(&ac.errors_wait, self) .await .unwrap_or_else(|| Duration::from_secs(30)); - self.params.auth_plain_text = self - .core - .core - .eval_if(&ac.allow_plain_text, self) - .await - .unwrap_or(false); self.params.auth_match_sender = self .core .core 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<T: AsyncRead + AsyncWrite> Session<T> { +impl<T: SessionStream> Session<T> { pub async fn is_allowed(&mut self) -> bool { let throttles = if !self.data.rcpt_to.is_empty() { &self.core.core.smtp.session.throttle.rcpt_to 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<T: AsyncWrite + AsyncRead + Unpin> Session<T> { +impl<T: SessionStream> Session<T> { 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<T: SessionStream> Session<T> { .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<T: SessionStream> Session<T> { self.write(b"503 5.5.1 AUTH not allowed.\r\n").await?; } else if !self.data.authenticated_as.is_empty() { self.write(b"503 5.5.1 Already authenticated.\r\n").await?; - } else if mechanism & (AUTH_LOGIN | AUTH_PLAIN) != 0 - && !self.stream.is_tls() - && !self.params.auth_plain_text - { - self.write(b"503 5.5.1 Clear text authentication without TLS is forbidden.\r\n").await?; } else if let Some(mut token) = SaslToken::from_mechanism(mechanism & auth) { @@ -407,7 +399,7 @@ impl<T: AsyncWrite + AsyncRead + Unpin> Session<T> { } } -impl<T: AsyncRead + AsyncWrite> ResolveVariable for Session<T> { +impl<T: SessionStream> ResolveVariable for Session<T> { fn resolve_variable(&self, variable: u32) -> expr::Variable<'_> { match variable { V_RECIPIENT => self @@ -424,6 +416,13 @@ impl<T: AsyncRead + AsyncWrite> ResolveVariable for Session<T> { .map(|r| r.domain.as_str()) .unwrap_or_default() .into(), + V_RECIPIENTS => self + .data + .rcpt_to + .iter() + .map(|r| Variable::String(r.address_lcase.as_str().into())) + .collect::<Vec<_>>() + .into(), V_SENDER => self .data .mail_from @@ -442,8 +441,12 @@ impl<T: AsyncRead + AsyncWrite> ResolveVariable for Session<T> { V_AUTHENTICATED_AS => self.data.authenticated_as.as_str().into(), V_LISTENER => self.instance.id.as_str().into(), V_REMOTE_IP => self.data.remote_ip_str.as_str().into(), + V_REMOTE_PORT => self.data.remote_port.into(), V_LOCAL_IP => self.data.local_ip_str.as_str().into(), + V_LOCAL_PORT => self.data.local_port.into(), + V_TLS => self.stream.is_tls().into(), V_PRIORITY => self.data.priority.to_string().into(), + V_PROTOCOL => self.instance.protocol.as_str().into(), _ => expr::Variable::default(), } } 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<T: SessionStream> Session<T> { pub async fn init_conn(&mut self) -> bool { self.eval_session_params().await; + let config = &self.core.core.smtp.session.connect; + // Sieve filtering if let Some(script) = self .core .core - .eval_if::<String, _>(&self.core.core.smtp.session.connect.script, self) + .eval_if::<String, _>(&config.script, self) .await .and_then(|name| self.core.core.get_sieve_script(&name)) { @@ -115,7 +122,7 @@ impl<T: SessionStream> Session<T> { self.hostname = self .core .core - .eval_if::<String, _>(&self.core.core.network.hostname, self) + .eval_if::<String, _>(&config.hostname, self) .await .unwrap_or_default(); if self.hostname.is_empty() { @@ -124,14 +131,14 @@ impl<T: SessionStream> Session<T> { event = "hostname", "No hostname configured, using 'localhost'." ); - self.hostname = "locahost".to_string(); + self.hostname = "localhost".to_string(); } // Obtain greeting let greeting = self .core .core - .eval_if::<String, _>(&self.core.core.smtp.session.connect.greeting, self) + .eval_if::<String, _>(&config.greeting, self) .await .filter(|g| !g.is_empty()) .map(|g| format!("220 {}\r\n", g)) 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<T: AsyncWrite + AsyncRead + Unpin> Session<T> { +impl<T: SessionStream> Session<T> { 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::<String, _>(&queue_config.hostname, &envelope) .await - .unwrap_or_else(|| "localhost".to_string()); + .filter(|s| !s.is_empty()) + .unwrap_or_else(|| { + tracing::warn!(parent: &span, + context = "queue", + event = "ehlo", + "No outbound hostname configured, using 'local.host'." + ); + "local.host".to_string() + }); let params = SessionParams { span: &span, core: &core, 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::<Vec<_>>() + .into(), V_MX => self.mx.into(), V_PRIORITY => self.message.priority.into(), V_REMOTE_IP => self.remote_ip.to_string().into(), @@ -259,6 +265,12 @@ impl ResolveVariable for Message { match variable { V_SENDER => self.return_path_lcase.as_str().into(), V_SENDER_DOMAIN => self.return_path_domain.as_str().into(), + V_RECIPIENTS => self + .recipients + .iter() + .map(|r| Variable::from(r.address_lcase.as_str())) + .collect::<Vec<_>>() + .into(), V_PRIORITY => self.priority.into(), _ => "".into(), } 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<T: AsyncWrite + AsyncRead + Unpin> Session<T> { +impl<T: SessionStream> Session<T> { 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<Record>, } -impl<T: AsyncWrite + AsyncRead + Unpin> Session<T> { +impl<T: SessionStream> Session<T> { #[allow(clippy::too_many_arguments)] pub async fn send_dmarc_report( &self, 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<T: AsyncWrite + AsyncRead + Unpin> Session<T> { +impl<T: SessionStream> Session<T> { 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<u8>> = Vec::new(); @@ -149,10 +149,10 @@ impl SMTP { message_id, } => { // Build message - let return_path_lcase = self.core.sieve.return_path.to_lowercase(); + let return_path_lcase = params.return_path.to_lowercase(); let return_path_domain = return_path_lcase.domain_part().to_string(); let mut message = self.new_message( - self.core.sieve.return_path.clone(), + params.return_path.clone(), return_path_lcase, return_path_domain, ); @@ -265,9 +265,9 @@ impl SMTP { instance.message().raw_message().into() }; if let Some(raw_message) = raw_message { - let headers = if !self.core.sieve.sign.is_empty() { + let headers = if !params.sign.is_empty() { let mut headers = Vec::new(); - for dkim in &self.core.sieve.sign { + for dkim in ¶ms.sign { if let Some(dkim) = self.core.get_dkim_signer(dkim) { match dkim.sign(raw_message) { Ok(signature) => { 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<T: SessionStream> Session<T> { pub async fn run_script(&self, script: Arc<Sieve>, params: ScriptParameters) -> ScriptResult { let core = self.core.clone(); let span = self.span.clone(); + let params = params.with_envelope(&self.core.core, self).await; let handle = Handle::current(); self.core 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<Arc<Vec<u8>>>, variables: AHashMap<Cow<'static, str>, Variable>, envelope: Vec<(Envelope, Variable)>, + from_addr: String, + from_name: String, + return_path: String, + sign: Vec<String>, #[cfg(feature = "test_mode")] expected_variables: Option<AHashMap<String, Variable>>, } @@ -60,9 +64,29 @@ impl ScriptParameters { message: None, #[cfg(feature = "test_mode")] expected_variables: None, + from_addr: Default::default(), + from_name: Default::default(), + return_path: Default::default(), + sign: Default::default(), } } + pub async fn with_envelope(mut self, core: &Core, vars: &impl ResolveVariable) -> Self { + for (variable, expr) in [ + (&mut self.from_addr, &core.sieve.from_addr), + (&mut self.from_name, &core.sieve.from_name), + (&mut self.return_path, &core.sieve.return_path), + ] { + if let Some(value) = core.eval_if(expr, vars).await { + *variable = value; + } + } + if let Some(value) = core.eval_if(&core.sieve.sign, vars).await { + self.sign = value; + } + self + } + pub fn with_message(self, message: Arc<Vec<u8>>) -> Self { Self { message: message.into(), 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::<Duration>((&prefix, "transaction.timeout")) { + if let Some(value) = config + .property::<Option<Duration>>((&prefix, "transaction.timeout")) + .unwrap_or_default() + { db.set_option(DatabaseOption::TransactionTimeout(value.as_millis() as i32)) .map_err(|err| { config.new_build_error( @@ -80,7 +83,10 @@ impl FdbStore { }) .ok()?; } - if let Some(value) = config.property::<Duration>((&prefix, "transaction.max-retry-delay")) { + if let Some(value) = config + .property::<Option<Duration>>((&prefix, "transaction.max-retry-delay")) + .unwrap_or_default() + { db.set_option(DatabaseOption::TransactionMaxRetryDelay( value.as_millis() as i32 )) 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::<Duration>((&prefix, "timeout")) + .property::<Option<Duration>>((&prefix, "timeout")) + .unwrap_or_default() .map(|t| t.as_secs() as usize), ); if let Some(port) = config.property((&prefix, "port")) { @@ -68,10 +69,16 @@ impl MysqlStore { // Configure connection pool let mut pool_min = PoolConstraints::default().min(); let mut pool_max = PoolConstraints::default().max(); - if let Some(n_size) = config.property::<usize>((&prefix, "pool.min-connections")) { + if let Some(n_size) = config + .property::<usize>((&prefix, "pool.min-connections")) + .filter(|&n| n > 0) + { pool_min = n_size; } - if let Some(n_size) = config.property::<usize>((&prefix, "pool.max-connections")) { + if let Some(n_size) = config + .property::<usize>((&prefix, "pool.max-connections")) + .filter(|&n| n > 0) + { pool_max = n_size; } opts = opts.pool_opts( 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::<Option<Duration>>((&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)); - - Self { - pool: RedisPool::Single( - build_pool(config, &prefix, RedisConnectionManager { client, timeout }) + 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 }) + .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::<Option<Duration>>((&prefix, "retry.max-wait")) + .unwrap_or_default() + { + builder = builder.max_retry_wait(value.as_millis() as u64); + } + if let Some(value) = config + .property::<Option<Duration>>((&prefix, "retry.min-wait")) + .unwrap_or_default() + { + builder = builder.min_retry_wait(value.as_millis() as u64); + } + if let Some(true) = config.property::<bool>((&prefix, "read-from-replicas")) { + 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::<Duration>((&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); - } - 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::<Duration>((&prefix, "retry.max-wait")) { - builder = builder.max_retry_wait(value.as_millis() as u64); + invalid => { + let err = format!("Invalid Redis type {invalid:?}"); + config.new_parse_error((&prefix, "redis-type"), err); + return None; } - if let Some(value) = config.property::<Duration>((&prefix, "retry.min-wait")) { - builder = builder.min_retry_wait(value.as_millis() as u64); - } - if let Some(true) = config.property::<bool>((&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<M: Manager>( ) .create_timeout( config - .property_or_default::<Duration>((prefix, "pool.create-timeout"), "30s") - .unwrap_or_else(|| Duration::from_secs(30)) - .into(), + .property_or_default::<Option<Duration>>((prefix, "pool.create-timeout"), "30s") + .unwrap_or_default(), + ) + .wait_timeout( + config + .property_or_default::<Option<Duration>>((prefix, "pool.wait-timeout"), "30s") + .unwrap_or_default(), + ) + .recycle_timeout( + config + .property_or_default::<Option<Duration>>((prefix, "pool.recycle-timeout"), "30s") + .unwrap_or_default(), ) - .wait_timeout(config.property_or_default((prefix, "pool.wait-timeout"), "30s")) - .recycle_timeout(config.property_or_default((prefix, "pool.recycle-timeout"), "30s")) .build() .map_err(|err| { format!( 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<T: ParseValue>( &mut self, key: impl AsKey, - default: impl AsKey, + or_else: impl AsKey, + default: &str, ) -> Option<T> { let key = key.as_key(); - let value = match self.value_or_else(key.as_str(), default.clone()) { + let value = match self.value_or_else(key.as_str(), or_else.clone()) { Some(value) => value, - None => { - self.warnings - .insert(default.as_key(), ConfigWarning::Missing); - return None; - } + None => default, }; match T::parse_value(value) { @@ -216,10 +207,10 @@ impl Config { } } - pub fn value_or_else(&self, key: impl AsKey, default: impl AsKey) -> Option<&str> { + pub fn value_or_else(&self, key: impl AsKey, or_else: impl AsKey) -> Option<&str> { self.keys .get(&key.as_key()) - .or_else(|| self.keys.get(&default.as_key())) + .or_else(|| self.keys.get(&or_else.as_key())) .map(|s| s.as_str()) } @@ -260,17 +251,6 @@ impl Config { self.keys.remove(key) } - pub fn value_or_warn(&mut self, key: impl AsKey) -> Option<&str> { - let key = key.as_key(); - match self.keys.get(&key) { - Some(value) => Some(value.as_str()), - None => { - self.warnings.insert(key, ConfigWarning::Missing); - None - } - } - } - pub fn new_parse_error(&mut self, key: impl AsKey, details: impl Into<String>) { self.errors .insert(key.as_key(), ConfigError::Parse(details.into())); @@ -523,6 +503,12 @@ impl ParseValue for Rate { } } +impl ParseValue for () { + fn parse_value(_: &str) -> super::Result<Self> { + Ok(()) + } +} + pub trait AsKey: Clone { fn as_key(&self) -> String; fn as_prefix(&self) -> String; 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::<Vec<_>>(); - let has_values = !values.is_empty(); + if values.is_empty() { + values = vec![ + "https://publicsuffix.org/list/public_suffix_list.dat".to_string(), + "https://raw.githubusercontent.com/publicsuffix/list/master/public_suffix_list.dat" + .to_string(), + ] + } + for (idx, value) in values.into_iter().enumerate() { let bytes = if value.starts_with("https://") || value.starts_with("http://") { let result = match reqwest::get(&value).await { @@ -157,14 +164,7 @@ impl PublicSuffix { } #[cfg(not(feature = "test_mode"))] - config.new_build_error( - key, - if has_values { - "Failed to parse public suffixes from any source." - } else { - "No public suffixes list was specified." - }, - ); + config.new_build_error(key, "Failed to parse public suffixes from any source."); PublicSuffix::default() } 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: <place_auth_here>"] -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, '<img')" { + if eval "is_body_part" { + let "dimensions" "html_attr_size(token, 'width', 800) + html_attr_size(token, 'height', 600)"; + + if eval "in_anchor && dimensions >= 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, '<head')" { + let "in_head" "in_head + 1"; + } elsif eval "starts_with(token, '</head')" { + let "in_head" "in_head - 1"; + } elsif eval "starts_with(token, '<body')" { + let "in_body" "in_body + 1"; + } elsif eval "starts_with(token, '</body')" { + let "in_body" "in_body - 1"; + } elsif eval "starts_with(token, '<a ')" { + let "in_anchor" "1"; + let "in_anchor_href_ip" "0"; + let "in_anchor_href" "to_lowercase(trim(html_attr(token, 'href')))"; + + if eval "is_body_part && starts_with(in_anchor_href, 'data:') && contains(in_anchor_href, ';base64,')" { + # Has Data URI encoding + let "t.HAS_DATA_URI" "1"; + if eval "contains(in_anchor_href, 'text/')" { + # Uses Data URI encoding to obfuscate plain or HTML in base64 + let "t.DATA_URI_OBFU" "1"; + } + } elsif eval "is_ip_addr(uri_part(in_anchor_href, 'host'))" { + # HTML anchor points to an IP address + let "t.HTTP_TO_IP" "1"; + let "in_anchor_href_ip" "1"; + } + } elsif eval "in_anchor && starts_with(token, '</a')" { + let "in_anchor" "0"; + } elsif eval "starts_with(token, '<meta ')" { + if eval "eq_ignore_case(html_attr(token, 'http-equiv'), 'refresh') && + contains_ignore_case(html_attr(token, 'content'), 'url=')" { + # HTML meta refresh tag + let "t.HTML_META_REFRESH_URL" "1"; + } + } elsif eval "starts_with(token, '<link') && is_body_part && + (contains_ignore_case(html_attr(token, 'rel'), 'stylesheet') || + contains_ignore_case(html_attr(token, 'href'), '.css') )" { + let "t.EXT_CSS" "1"; + } + } + + if eval "is_body_part" { + # Check for unbalanced tags + if eval "in_head != 0 || in_body != 0" { + let "t.HTML_UNBALANCED_TAG" "1"; + } + + # Check for short HTML parts with a link to an image + if eval "has_link_to_img" { + if eval "html_char_count < 1024" { + let "t.HTML_SHORT_LINK_IMG_1" "1"; + } elsif eval "html_char_count < 1536" { + let "t.HTML_SHORT_LINK_IMG_2" "1"; + } elsif eval "html_char_count < 2048" { + let "t.HTML_SHORT_LINK_IMG_3" "1"; + } + } + + if eval "(!has_link_to_img || html_char_count >= 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 Binary files differdeleted file mode 100644 index c916d9de..00000000 --- a/resources/config/spamfilter/maps/suffix_list.dat.gz +++ /dev/null diff --git a/resources/config/spamfilter/scripts/config.sieve b/resources/config/spamfilter/scripts/config.sieve index 29467958..6499f41d 100644 --- a/resources/config/spamfilter/scripts/config.sieve +++ b/resources/config/spamfilter/scripts/config.sieve @@ -1,35 +1,35 @@ # Whether to add an X-Spam-Status header -let "ADD_HEADER_SPAM" "%{cfg:spam.header.add-spam}%"; +let "ADD_HEADER_SPAM" "key_get('spam-config', 'add-spam')"; # Whether to add an X-Spam-Result header -let "ADD_HEADER_SPAM_RESULT" "%{cfg:spam.header.add-spam-result}%"; +let "ADD_HEADER_SPAM_RESULT" "key_get('spam-config', 'add-spam-result')"; # Whether message replies from authenticated users should be learned as ham -let "AUTOLEARN_REPLIES_HAM" "%{cfg:spam.autolearn.ham.replies}%"; +let "AUTOLEARN_REPLIES_HAM" "key_get('spam-config', 'learn-ham-replies')"; # Whether the bayes classifier should be trained automatically -let "AUTOLEARN_ENABLE" "%{cfg:spam.autolearn.enable}%"; +let "AUTOLEARN_ENABLE" "key_get('spam-config', 'learn-enable')"; # When to learn ham (score >= threshold) -let "AUTOLEARN_HAM_THRESHOLD" "%{cfg:spam.autolearn.ham.threshold}%"; +let "AUTOLEARN_HAM_THRESHOLD" "key_get('spam-config', 'learn-ham-threshold')"; # When to learn spam (score <= threshold) -let "AUTOLEARN_SPAM_THRESHOLD" "%{cfg:spam.autolearn.spam.threshold}%"; +let "AUTOLEARN_SPAM_THRESHOLD" "key_get('spam-config', 'learn-spam-threshold')"; # Keep difference for spam/ham learns for at least this value -let "AUTOLEARN_SPAM_HAM_BALANCE" "%{cfg:spam.autolearn.balance}%"; +let "AUTOLEARN_SPAM_HAM_BALANCE" "key_get('spam-config', 'learn-balance')"; # If ADD_HEADER_SPAM is enabled, mark as SPAM messages with a score above this threshold -let "SCORE_SPAM_THRESHOLD" "%{cfg:spam.threshold.spam}%"; +let "SCORE_SPAM_THRESHOLD" "key_get('spam-config', 'threshold-spam')"; # Discard messages with a score above this threshold -let "SCORE_DISCARD_THRESHOLD" "%{cfg:spam.threshold.discard}%"; +let "SCORE_DISCARD_THRESHOLD" "key_get('spam-config', 'threshold-discard')"; # Reject messages with a score above this threshold -let "SCORE_REJECT_THRESHOLD" "%{cfg:spam.threshold.reject}%"; +let "SCORE_REJECT_THRESHOLD" "key_get('spam-config', 'threshold-reject')"; # Directory name to use for local domain lookups (leave empty for default) -let "DOMAIN_DIRECTORY" "%{cfg:spam.data.directory}%"; +let "DOMAIN_DIRECTORY" "key_get('spam-config', 'directory')"; # Store to use for Bayes tokens and ids (leave empty for default) -let "SPAM_DB" "%{cfg:spam.data.lookup}%"; +let "SPAM_DB" "key_get('spam-config', 'lookup')"; 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 - -# Copy config files -cp -r resources/config $BASE_DIR/etc +mkdir -p $BASE_DIR $BASE_DIR/data $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}] @@ -99,6 +98,11 @@ 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'] size = 450 @@ -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<DummyIo> { }, data: SessionData::new( "127.0.0.1".parse().unwrap(), + 0, "127.0.0.1".parse().unwrap(), 0, ), |