mirror of
https://github.com/stalwartlabs/mail-server.git
synced 2024-10-23 15:00:14 +00:00
Fixed Ipv6thenIpv4 SMTP lookups
This commit is contained in:
parent
56b1fb893d
commit
6404d27dd9
7 changed files with 409 additions and 84 deletions
|
@ -15,13 +15,11 @@ use super::{
|
|||
|
||||
const VERSION: f64 = 4.000000;
|
||||
|
||||
static IF_TRUE: [&str; 57] = [
|
||||
static IF_TRUE: [&str; 54] = [
|
||||
"Mail::SpamAssassin::Plugin::DKIM",
|
||||
"Mail::SpamAssassin::Plugin::SPF",
|
||||
"Mail::SpamAssassin::Plugin::ASN",
|
||||
"Mail::SpamAssassin::Plugin::AWL",
|
||||
"Mail::SpamAssassin::Plugin::AccessDB",
|
||||
"Mail::SpamAssassin::Plugin::AntiVirus",
|
||||
"Mail::SpamAssassin::Plugin::AskDNS",
|
||||
"Mail::SpamAssassin::Plugin::AutoLearnThreshold",
|
||||
"Mail::SpamAssassin::Plugin::Bayes",
|
||||
|
@ -44,7 +42,6 @@ static IF_TRUE: [&str; 57] = [
|
|||
"Mail::SpamAssassin::Plugin::Razor2",
|
||||
"Mail::SpamAssassin::Plugin::RelayEval",
|
||||
"Mail::SpamAssassin::Plugin::ReplaceTags",
|
||||
"Mail::SpamAssassin::Plugin::Shortcircuit",
|
||||
"Mail::SpamAssassin::Plugin::TextCat",
|
||||
"Mail::SpamAssassin::Plugin::TxRep",
|
||||
"Mail::SpamAssassin::Plugin::URIDNSBL",
|
||||
|
@ -75,7 +72,12 @@ static IF_TRUE: [&str; 57] = [
|
|||
"Mail::SpamAssassin::Conf::feature_dns_block_rule",
|
||||
];
|
||||
|
||||
static IF_FALSE: [&str; 1] = ["Mail::SpamAssassin::Plugin::WhiteListSubject"];
|
||||
static IF_FALSE: [&str; 4] = [
|
||||
"Mail::SpamAssassin::Plugin::WhiteListSubject",
|
||||
"Mail::SpamAssassin::Plugin::AccessDB",
|
||||
"Mail::SpamAssassin::Plugin::AntiVirus",
|
||||
"Mail::SpamAssassin::Plugin::Shortcircuit",
|
||||
];
|
||||
|
||||
static SUPPORTED_FUNCTIONS: [&str; 162] = [
|
||||
"check_abundant_unicode_ratio",
|
||||
|
@ -1103,8 +1105,17 @@ pub fn import_spamassassin(path: PathBuf, extension: String, do_warn: bool) {
|
|||
"set \"awl_factor\" \"0.5\";\n",
|
||||
"set \"body\" \"${body.to_text}\";\n",
|
||||
"set \"body_len\" \"${body.len()}\";\n",
|
||||
"set \"headers_raw\" \"${headers.raw}\";\n",
|
||||
"set \"thread_name\" \"${header.subject.thread_name()}\";\n",
|
||||
"set \"sent_date\" \"${header.date.date}\";\n",
|
||||
"set \"mail_from\" \"${envelope.from}\";\n",
|
||||
"if eval \"mail_from.is_empty()\" {\n",
|
||||
"\tset \"mail_from\" \"postmaster@${env.helo_domain}\";\n",
|
||||
"}\n",
|
||||
"set \"mail_from_domain\" \"${mail_from.domain_part()}\";\n",
|
||||
"set \"from\" \"${header.from.addr}\";\n",
|
||||
"set \"from_domain\" \"${from.domain_part()}\";\n",
|
||||
"set \"from_name\" \"${header.from.name.trim()}\";\n",
|
||||
"\n"
|
||||
));
|
||||
|
||||
|
@ -1115,25 +1126,6 @@ pub fn import_spamassassin(path: PathBuf, extension: String, do_warn: bool) {
|
|||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Calculate forward scores
|
||||
/*let (score_pos, score_neg) =
|
||||
rules_iter
|
||||
.clone()
|
||||
.fold((0.0, 0.0), |(acc_pos, acc_neg), rule| {
|
||||
let score = rule.score();
|
||||
if score > 0.0 {
|
||||
(acc_pos + score, acc_neg)
|
||||
} else if score < 0.0 {
|
||||
(acc_pos, acc_neg + score)
|
||||
} else {
|
||||
(acc_pos, acc_neg)
|
||||
}
|
||||
});
|
||||
let mut rule = rule.clone();
|
||||
rule.forward_score_neg = score_neg;
|
||||
rule.forward_score_pos = score_pos;*/
|
||||
|
||||
write!(&mut script, "{rule}").unwrap();
|
||||
}
|
||||
|
||||
|
@ -1297,12 +1289,12 @@ impl Display for Rule {
|
|||
match function.as_str() {
|
||||
"check_from_in_auto_welcomelist" | "check_from_in_auto_whitelist" => {
|
||||
f.write_str(concat!(
|
||||
"query :use \"spam\" :set [\"awl_score\", \"awl_count\"] \"SELECT score, count FROM awl WHERE sender = ? AND ip = ?\" [\"${env.from}\", \"%{env.remote_ip}\"];\n",
|
||||
"query :use \"spam\" :set [\"awl_score\", \"awl_count\"] \"SELECT score, count FROM awl WHERE sender = ? AND ip = ?\" [\"${from}\", \"%{env.remote_ip}\"];\n",
|
||||
"if eval \"awl_count > 0\" {\n",
|
||||
"\tquery :use \"spam\" \"UPDATE awl SET score = score + ?, count = count + 1 WHERE sender = ? AND ip = ?\" [\"%{score}\", \"${env.from}\", \"%{env.remote_ip}\"];\n",
|
||||
"\tquery :use \"spam\" \"UPDATE awl SET score = score + ?, count = count + 1 WHERE sender = ? AND ip = ?\" [\"%{score}\", \"${from}\", \"%{env.remote_ip}\"];\n",
|
||||
"\tset \"score\" \"%{score + ((awl_score / awl_count) - score) * awl_factor}\";\n",
|
||||
"} else {\n",
|
||||
"\tquery :use \"spam\" \"INSERT INTO awl (score, count, sender, ip) VALUES (?, 1, ?, ?)\" [\"%{score}\", \"${env.from}\", \"%{env.remote_ip}\"];\n",
|
||||
"\tquery :use \"spam\" \"INSERT INTO awl (score, count, sender, ip) VALUES (?, 1, ?, ?)\" [\"%{score}\", \"${from}\", \"%{env.remote_ip}\"];\n",
|
||||
"}\n\n",
|
||||
))?;
|
||||
return Ok(());
|
||||
|
@ -1410,9 +1402,16 @@ impl Display for Rule {
|
|||
| "check_dkim_signsome"
|
||||
| "check_dkim_valid_author_sig"
|
||||
| "check_access_database"
|
||||
| "check_body_8bits" => {
|
||||
| "check_body_8bits"
|
||||
| "check_shortcircuit"
|
||||
| "check_suspect_name"
|
||||
| "check_microsoft_executable"
|
||||
| "check_outlook_message_id"
|
||||
| "gated_through_received_hdr_remover"
|
||||
| "check_for_faraway_charset_in_headers" => {
|
||||
// ADSP is deprecated (see https://datatracker.ietf.org/doc/status-change-adsp-rfc5617-to-historic/)
|
||||
// check_body_8bits: Not really useful
|
||||
// check_shortcircuit: Not used
|
||||
f.write_str("if false")?;
|
||||
}
|
||||
"check_dkim_dependable" => {
|
||||
|
@ -1447,7 +1446,7 @@ impl Display for Rule {
|
|||
}
|
||||
}
|
||||
"check_dkim_valid_envelopefrom" => {
|
||||
f.write_str("if allof(string :is \"${env.dkim_result}\" \"pass\", string :is \"${envelope.from}\" \"${env.from}\")")?;
|
||||
f.write_str("if allof(string :is \"${env.dkim_result}\" \"pass\", string :is \"${mail_from}\" \"${from}\")")?;
|
||||
}
|
||||
"check_for_def_dkim_welcomelist_from"
|
||||
| "check_for_def_dkim_whitelist_from"
|
||||
|
@ -1530,10 +1529,10 @@ impl Display for Rule {
|
|||
)?;
|
||||
}
|
||||
"check_equal_from_domains" => {
|
||||
f.write_str("if not string :is \"${envelope.from.base_domain()}\" \"${header.from.base_domain()}\"")?;
|
||||
f.write_str("if not string :is \"${mail_from.subdomain_part()}\" \"${from.subdomain_part()}\"")?;
|
||||
}
|
||||
"check_for_no_rdns_dotcom_helo" => {
|
||||
f.write_str(concat!("if not string :is \"${env.iprev_result}\" [\"pass\", \"\", \"temperror\"]"))?;
|
||||
f.write_str("if not string :is \"${env.iprev_result}\" [\"pass\", \"\", \"temperror\"]")?;
|
||||
}
|
||||
"helo_ip_mismatch" => {
|
||||
f.write_str(concat!(
|
||||
|
@ -1542,7 +1541,7 @@ impl Display for Rule {
|
|||
))?;
|
||||
}
|
||||
"subject_is_all_caps" => {
|
||||
f.write_str("if eval \"thread_name.len() >= 10 && thread_name.word_count() > 1 && thread_name.is_uppercase()\"")?;
|
||||
f.write_str("if eval \"thread_name.len() >= 10 && thread_name.count_words() > 1 && thread_name.is_uppercase()\"")?;
|
||||
}
|
||||
"check_for_shifted_date" => {
|
||||
let mut params = params.iter();
|
||||
|
@ -1581,6 +1580,148 @@ impl Display for Rule {
|
|||
|
||||
f.write_str("\"")?;
|
||||
}
|
||||
"check_ratware_envelope_from" => {
|
||||
f.write_str(concat!(
|
||||
"set \"env_from_local\" \"${mail_from.local_part()}\";\n",
|
||||
"set \"to_local\" \"${header.to.addr.local_part()}\";\n",
|
||||
"set \"to_domain\" \"${header.to.addr.domain_part()}\";\n",
|
||||
"if all_of(",
|
||||
"not string :is \"${env_from_local}\" \"\", ",
|
||||
"not string :is \"${to_local}\" \"\", ",
|
||||
"not string :is \"${to_domain}\" \"\", ",
|
||||
"string :contains \"${env_from_local}\" \"${to_domain}\", ",
|
||||
"string :contains \"${env_from_local}\" \"${to_local}\")"
|
||||
))?;
|
||||
}
|
||||
"check_ratware_name_id" => {
|
||||
f.write_str(concat!(
|
||||
"if header :contains ",
|
||||
"[\"Message-Id\",\"Resent-Message-Id\",",
|
||||
"\"X-Message-Id\",\"X-Original-Message-ID\"] ",
|
||||
"\"${from}\"",
|
||||
))?;
|
||||
}
|
||||
"check_mailfrom_matches_rcvd" => {
|
||||
f.write_str(
|
||||
"if string :contains \"${env.helo_domain}\" \"${mail_from_domain}\"",
|
||||
)?;
|
||||
}
|
||||
|
||||
"check_unresolved_template" => {
|
||||
f.write_str(concat!(
|
||||
"foreveryline \"${headers_raw}\" {\n",
|
||||
"\tif allof(string :regex \"${line}\" \"%[A-Z][A-Z_-]\", ",
|
||||
"not string :regex \"${line}\" \"(?i)^(?:X-VMS-To|X-UIDL|",
|
||||
"X-Face|To|Cc|From|Subject|References|In-Reply-To|(?:X-|Resent-|",
|
||||
"X-Original-)?Message-Id):\")",
|
||||
))?;
|
||||
self.fmt_match(f, 2)?;
|
||||
f.write_str("\t\tbreak;\n\t}\n}\n\n")?;
|
||||
return Ok(());
|
||||
}
|
||||
"check_for_matching_env_and_hdr_from" => {
|
||||
f.write_str("if string :is \"${mail_from}\" \"${from}\"")?;
|
||||
}
|
||||
"check_fromname_equals_replyto" => {
|
||||
f.write_str("if string :is \"${from_name}\" \"${header.reply-to.addr}\"")?;
|
||||
}
|
||||
"check_fromname_equals_to" => {
|
||||
f.write_str(concat!(
|
||||
"foreveryline \"${header.to[*].addr[*]}\" {\n",
|
||||
"\tif string :is \"${from_name}\" \"${line}\"",
|
||||
))?;
|
||||
self.fmt_match(f, 2)?;
|
||||
f.write_str("\t\tbreak;\n\t}\n}\n\n")?;
|
||||
return Ok(());
|
||||
}
|
||||
"check_fromname_spoof" => {
|
||||
f.write_str(concat!(
|
||||
"if allof(eval \"from_name.is_email()\", ",
|
||||
"not string :is \"${from_name.domain_name_part()}\" \"${from.domain_name_part()}\")",
|
||||
))?;
|
||||
}
|
||||
"check_header_count_range" => {
|
||||
let mut params = params.iter();
|
||||
let hdr_name = params
|
||||
.next()
|
||||
.expect("missing parameters for check_header_count_range")
|
||||
.to_lowercase();
|
||||
let range_from = params
|
||||
.next()
|
||||
.and_then(param_to_num::<u32>)
|
||||
.expect("missing parameters for check_header_count_range from");
|
||||
let range_to = params
|
||||
.next()
|
||||
.and_then(param_to_num::<u32>)
|
||||
.expect("missing parameters for check_header_count_range to");
|
||||
|
||||
if range_to > 100 {
|
||||
write!(
|
||||
f,
|
||||
"if eval \"header.{hdr_name}[*].raw.count() >= {range_from}\""
|
||||
)?;
|
||||
} else {
|
||||
write!(f, "if eval \"header.{hdr_name}[*].raw.count() >= {range_from} && header.{hdr_name}[*].raw.count() < {range_to}\"")?;
|
||||
}
|
||||
}
|
||||
"check_illegal_chars" => {
|
||||
let mut params = params.iter();
|
||||
let hdr_name = params
|
||||
.next()
|
||||
.expect("missing parameters for check_illegal_chars");
|
||||
let hdr_name = if hdr_name.contains("ALL") {
|
||||
"headers_raw".to_string()
|
||||
} else {
|
||||
format!("header.{}[*].raw", hdr_name.to_lowercase())
|
||||
};
|
||||
let ratio = params
|
||||
.next()
|
||||
.and_then(param_to_num::<f64>)
|
||||
.expect("missing parameters for check_illegal_chars ratio");
|
||||
let count = params
|
||||
.next()
|
||||
.and_then(param_to_num::<u32>)
|
||||
.expect("missing parameters for check_illegal_chars count");
|
||||
writeln!(f, "set \"c_count\" \"%{{{hdr_name}.count_chars()}}\";\n")?;
|
||||
writeln!(
|
||||
f,
|
||||
"set \"i_count\" \"%{{{hdr_name}.count_control_chars()}}\";\n"
|
||||
)?;
|
||||
writeln!(
|
||||
f,
|
||||
"if eval \"i_count > {count} && i_count / c_count > {ratio}\""
|
||||
)?;
|
||||
}
|
||||
"check_freemail_from" => {
|
||||
f.write_str(concat!(
|
||||
"if anyof(address :domain :list ",
|
||||
"[\"From\", \"Resent-From\", \"Envelope-Sender\",",
|
||||
"\"Resent-Sender\", \"X-Envelope-From\"] \"sa/freemail_domains\", ",
|
||||
"envelope :list \"from\" \"sa/freemail_domains\")",
|
||||
))?;
|
||||
}
|
||||
"check_freemail_header" => {
|
||||
let header = strip_quotes(
|
||||
params
|
||||
.iter()
|
||||
.next()
|
||||
.expect("missing header name for check_freemail_header"),
|
||||
);
|
||||
if header.contains("EnvelopeFrom") {
|
||||
f.write_str(
|
||||
"if string :list \"%{mail_from_domain}\" \"sa/freemail_domains\"",
|
||||
)?;
|
||||
} else {
|
||||
writeln!(
|
||||
f,
|
||||
"if address :domain \"{}\" \"sa/freemail_domains\"",
|
||||
header
|
||||
.split_once(':')
|
||||
.map_or(header, |v| v.1)
|
||||
.to_lowercase()
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
_ => {
|
||||
write!(f, "if {function}")?;
|
||||
|
@ -1640,10 +1781,11 @@ impl Rule {
|
|||
}
|
||||
|
||||
fn param_to_num<N: FromStr>(text: impl AsRef<str>) -> Option<N> {
|
||||
let text = text.as_ref();
|
||||
strip_quotes(text.as_ref()).parse::<N>().ok()
|
||||
}
|
||||
|
||||
fn strip_quotes(text: &str) -> &str {
|
||||
text.strip_prefix('\"')
|
||||
.and_then(|v| v.strip_suffix('\"'))
|
||||
.unwrap_or(text)
|
||||
.parse::<N>()
|
||||
.ok()
|
||||
}
|
||||
|
|
|
@ -392,7 +392,7 @@ impl DeliveryAttempt {
|
|||
}
|
||||
|
||||
// Obtain source and remote IPs
|
||||
let (source_ip, remote_ips) = match core
|
||||
let resolve_result = match core
|
||||
.resolve_host(remote_host, &envelope, max_multihomed)
|
||||
.await
|
||||
{
|
||||
|
@ -556,8 +556,15 @@ impl DeliveryAttempt {
|
|||
};
|
||||
|
||||
// Try each IP address
|
||||
envelope.local_ip = source_ip.unwrap_or(no_ip);
|
||||
'next_ip: for remote_ip in remote_ips {
|
||||
'next_ip: for remote_ip in resolve_result.remote_ips {
|
||||
// Set source IP, if any
|
||||
let source_ip = if remote_ip.is_ipv4() {
|
||||
resolve_result.source_ipv4
|
||||
} else {
|
||||
resolve_result.source_ipv6
|
||||
};
|
||||
envelope.local_ip = source_ip.unwrap_or(no_ip);
|
||||
|
||||
// Throttle remote host
|
||||
let mut in_flight_host = Vec::new();
|
||||
envelope.remote_ip = remote_ip;
|
||||
|
|
|
@ -21,9 +21,9 @@
|
|||
* for more details.
|
||||
*/
|
||||
|
||||
use std::net::IpAddr;
|
||||
use std::{net::IpAddr, sync::Arc};
|
||||
|
||||
use mail_auth::MX;
|
||||
use mail_auth::{IpLookupStrategy, MX};
|
||||
use rand::{seq::SliceRandom, Rng};
|
||||
use utils::config::KeyLookup;
|
||||
|
||||
|
@ -35,16 +35,75 @@ use crate::{
|
|||
|
||||
use super::NextHop;
|
||||
|
||||
pub struct IpLookupResult {
|
||||
pub source_ipv4: Option<IpAddr>,
|
||||
pub source_ipv6: Option<IpAddr>,
|
||||
pub remote_ips: Vec<IpAddr>,
|
||||
}
|
||||
|
||||
impl SMTP {
|
||||
pub async fn ip_lookup(
|
||||
&self,
|
||||
key: &str,
|
||||
strategy: IpLookupStrategy,
|
||||
max_results: usize,
|
||||
) -> mail_auth::Result<Vec<IpAddr>> {
|
||||
let (has_ipv4, has_ipv6, v4_first) = match strategy {
|
||||
IpLookupStrategy::Ipv4Only => (true, false, false),
|
||||
IpLookupStrategy::Ipv6Only => (false, true, false),
|
||||
IpLookupStrategy::Ipv4thenIpv6 => (true, true, true),
|
||||
IpLookupStrategy::Ipv6thenIpv4 => (true, true, false),
|
||||
};
|
||||
let ipv4_addrs = if has_ipv4 {
|
||||
match self.resolvers.dns.ipv4_lookup(key).await {
|
||||
Ok(addrs) => addrs,
|
||||
Err(_) if has_ipv6 => Arc::new(Vec::new()),
|
||||
Err(err) => return Err(err),
|
||||
}
|
||||
} else {
|
||||
Arc::new(Vec::new())
|
||||
};
|
||||
|
||||
if has_ipv6 {
|
||||
let ipv6_addrs = match self.resolvers.dns.ipv6_lookup(key).await {
|
||||
Ok(addrs) => addrs,
|
||||
Err(_) if !ipv4_addrs.is_empty() => Arc::new(Vec::new()),
|
||||
Err(err) => return Err(err),
|
||||
};
|
||||
if v4_first {
|
||||
Ok(ipv4_addrs
|
||||
.iter()
|
||||
.copied()
|
||||
.map(IpAddr::from)
|
||||
.chain(ipv6_addrs.iter().copied().map(IpAddr::from))
|
||||
.take(max_results)
|
||||
.collect())
|
||||
} else {
|
||||
Ok(ipv6_addrs
|
||||
.iter()
|
||||
.copied()
|
||||
.map(IpAddr::from)
|
||||
.chain(ipv4_addrs.iter().copied().map(IpAddr::from))
|
||||
.take(max_results)
|
||||
.collect())
|
||||
}
|
||||
} else {
|
||||
Ok(ipv4_addrs
|
||||
.iter()
|
||||
.take(max_results)
|
||||
.copied()
|
||||
.map(IpAddr::from)
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn resolve_host(
|
||||
&self,
|
||||
remote_host: &NextHop<'_>,
|
||||
envelope: &impl KeyLookup<Key = EnvelopeKey>,
|
||||
max_multihomed: usize,
|
||||
) -> Result<(Option<IpAddr>, Vec<IpAddr>), Status<(), Error>> {
|
||||
) -> Result<IpLookupResult, Status<(), Error>> {
|
||||
let remote_ips = self
|
||||
.resolvers
|
||||
.dns
|
||||
.ip_lookup(
|
||||
remote_host.fqdn_hostname().as_ref(),
|
||||
*self.queue.config.ip_strategy.eval(envelope).await,
|
||||
|
@ -65,40 +124,42 @@ impl SMTP {
|
|||
}
|
||||
})?;
|
||||
|
||||
if let Some(remote_ip) = remote_ips.first() {
|
||||
let mut source_ip = None;
|
||||
if !remote_ips.is_empty() {
|
||||
let mut result = IpLookupResult {
|
||||
source_ipv4: None,
|
||||
source_ipv6: None,
|
||||
remote_ips,
|
||||
};
|
||||
|
||||
if remote_ip.is_ipv4() {
|
||||
let source_ips = self.queue.config.source_ip.ipv4.eval(envelope).await;
|
||||
match source_ips.len().cmp(&1) {
|
||||
std::cmp::Ordering::Equal => {
|
||||
source_ip = IpAddr::from(*source_ips.first().unwrap()).into();
|
||||
}
|
||||
std::cmp::Ordering::Greater => {
|
||||
source_ip = IpAddr::from(
|
||||
source_ips[rand::thread_rng().gen_range(0..source_ips.len())],
|
||||
)
|
||||
.into();
|
||||
}
|
||||
std::cmp::Ordering::Less => (),
|
||||
// Obtain source IPv4 address
|
||||
let source_ips = self.queue.config.source_ip.ipv4.eval(envelope).await;
|
||||
match source_ips.len().cmp(&1) {
|
||||
std::cmp::Ordering::Equal => {
|
||||
result.source_ipv4 = IpAddr::from(*source_ips.first().unwrap()).into();
|
||||
}
|
||||
} else {
|
||||
let source_ips = self.queue.config.source_ip.ipv6.eval(envelope).await;
|
||||
match source_ips.len().cmp(&1) {
|
||||
std::cmp::Ordering::Equal => {
|
||||
source_ip = IpAddr::from(*source_ips.first().unwrap()).into();
|
||||
}
|
||||
std::cmp::Ordering::Greater => {
|
||||
source_ip = IpAddr::from(
|
||||
source_ips[rand::thread_rng().gen_range(0..source_ips.len())],
|
||||
)
|
||||
.into();
|
||||
}
|
||||
std::cmp::Ordering::Less => (),
|
||||
std::cmp::Ordering::Greater => {
|
||||
result.source_ipv4 =
|
||||
IpAddr::from(source_ips[rand::thread_rng().gen_range(0..source_ips.len())])
|
||||
.into();
|
||||
}
|
||||
std::cmp::Ordering::Less => (),
|
||||
}
|
||||
|
||||
Ok((source_ip, remote_ips))
|
||||
// Obtain source IPv6 address
|
||||
let source_ips = self.queue.config.source_ip.ipv6.eval(envelope).await;
|
||||
match source_ips.len().cmp(&1) {
|
||||
std::cmp::Ordering::Equal => {
|
||||
result.source_ipv6 = IpAddr::from(*source_ips.first().unwrap()).into();
|
||||
}
|
||||
std::cmp::Ordering::Greater => {
|
||||
result.source_ipv6 =
|
||||
IpAddr::from(source_ips[rand::thread_rng().gen_range(0..source_ips.len())])
|
||||
.into();
|
||||
}
|
||||
std::cmp::Ordering::Less => (),
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
} else {
|
||||
Err(Status::TemporaryFailure(Error::DnsError(format!(
|
||||
"No IP addresses found for {:?}.",
|
||||
|
|
|
@ -7,18 +7,18 @@ set "address4" "jane@example.org";
|
|||
set "address5" "john@localhost";
|
||||
set "address6" "jane@localhost";
|
||||
|
||||
if not string :is "${address1.base_domain()}" "${address2.base_domain()}" {
|
||||
reject "${address1.base_domain()} != ${address2.base_domain()}";
|
||||
if not string :is "${address1.subdomain_part()}" "${address2.subdomain_part()}" {
|
||||
reject "${address1.subdomain_part()} != ${address2.subdomain_part()}";
|
||||
stop;
|
||||
}
|
||||
|
||||
if not string :is "${address3.base_domain()}" "${address4.base_domain()}" {
|
||||
reject "${address3.base_domain()} != ${address4.base_domain()}";
|
||||
if not string :is "${address3.subdomain_part()}" "${address4.subdomain_part()}" {
|
||||
reject "${address3.subdomain_part()} != ${address4.subdomain_part()}";
|
||||
stop;
|
||||
}
|
||||
|
||||
if not string :is "${address5.base_domain()}" "${address6.base_domain()}" {
|
||||
reject "${address5.base_domain()} != ${address6.base_domain()}";
|
||||
if not string :is "${address5.subdomain_part()}" "${address6.subdomain_part()}" {
|
||||
reject "${address5.subdomain_part()} != ${address6.subdomain_part()}";
|
||||
stop;
|
||||
}
|
||||
|
||||
|
|
|
@ -75,7 +75,7 @@ async fn lookup_ip() {
|
|||
|
||||
// Ipv4 strategy
|
||||
core.queue.config.ip_strategy = IfBlock::new(IpLookupStrategy::Ipv4thenIpv6);
|
||||
let (source_ips, remote_ips) = core
|
||||
let resolve_result = core
|
||||
.resolve_host(
|
||||
&NextHop::MX("mx.foobar.org"),
|
||||
&RecipientDomain::new("envelope"),
|
||||
|
@ -83,15 +83,17 @@ async fn lookup_ip() {
|
|||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(ipv4.contains(&match source_ips.unwrap() {
|
||||
assert!(ipv4.contains(&match resolve_result.source_ipv4.unwrap() {
|
||||
std::net::IpAddr::V4(v4) => v4,
|
||||
_ => unreachable!(),
|
||||
}));
|
||||
assert!(remote_ips.contains(&"172.168.0.100".parse().unwrap()));
|
||||
assert!(resolve_result
|
||||
.remote_ips
|
||||
.contains(&"172.168.0.100".parse().unwrap()));
|
||||
|
||||
// Ipv6 strategy
|
||||
core.queue.config.ip_strategy = IfBlock::new(IpLookupStrategy::Ipv6thenIpv4);
|
||||
let (source_ips, remote_ips) = core
|
||||
let resolve_result = core
|
||||
.resolve_host(
|
||||
&NextHop::MX("mx.foobar.org"),
|
||||
&RecipientDomain::new("envelope"),
|
||||
|
@ -99,11 +101,13 @@ async fn lookup_ip() {
|
|||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(ipv6.contains(&match source_ips.unwrap() {
|
||||
assert!(ipv6.contains(&match resolve_result.source_ipv6.unwrap() {
|
||||
std::net::IpAddr::V6(v6) => v6,
|
||||
_ => unreachable!(),
|
||||
}));
|
||||
assert!(remote_ips.contains(&"e:f::a".parse().unwrap()));
|
||||
assert!(resolve_result
|
||||
.remote_ips
|
||||
.contains(&"e:f::a".parse().unwrap()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
110
tests/src/smtp/outbound/ip_lookup.rs
Normal file
110
tests/src/smtp/outbound/ip_lookup.rs
Normal file
|
@ -0,0 +1,110 @@
|
|||
/*
|
||||
* Copyright (c) 2023 Stalwart Labs Ltd.
|
||||
*
|
||||
* This file is part of Stalwart Mail Server.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of
|
||||
* the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
* in the LICENSE file at the top-level directory of this distribution.
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* You can be released from the requirements of the AGPLv3 license by
|
||||
* purchasing a commercial license. Please contact licensing@stalw.art
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
use std::{
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use mail_auth::{IpLookupStrategy, MX};
|
||||
use utils::config::ServerProtocol;
|
||||
|
||||
use crate::smtp::{
|
||||
inbound::TestQueueEvent, outbound::start_test_server, session::TestSession, TestConfig,
|
||||
TestSMTP,
|
||||
};
|
||||
use smtp::{
|
||||
config::IfBlock,
|
||||
core::{Session, SMTP},
|
||||
queue::{manager::Queue, DeliveryAttempt},
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
#[serial_test::serial]
|
||||
async fn ip_lookup_strategy() {
|
||||
/*tracing::subscriber::set_global_default(
|
||||
tracing_subscriber::FmtSubscriber::builder()
|
||||
.with_max_level(tracing::Level::TRACE)
|
||||
.finish(),
|
||||
)
|
||||
.unwrap();*/
|
||||
|
||||
// Start test server
|
||||
let mut core = SMTP::test();
|
||||
core.session.config.rcpt.relay = IfBlock::new(true);
|
||||
let mut remote_qr = core.init_test_queue("smtp_iplookup_remote");
|
||||
let _rx = start_test_server(core.into(), &[ServerProtocol::Smtp]);
|
||||
|
||||
for strategy in [IpLookupStrategy::Ipv6Only, IpLookupStrategy::Ipv6thenIpv4] {
|
||||
println!("-> Strategy: {:?}", strategy);
|
||||
// Add mock DNS entries
|
||||
let mut core = SMTP::test();
|
||||
core.queue.config.ip_strategy = IfBlock::new(IpLookupStrategy::Ipv6thenIpv4);
|
||||
core.resolvers.dns.mx_add(
|
||||
"foobar.org",
|
||||
vec![MX {
|
||||
exchanges: vec!["mx.foobar.org".to_string()],
|
||||
preference: 10,
|
||||
}],
|
||||
Instant::now() + Duration::from_secs(10),
|
||||
);
|
||||
if matches!(strategy, IpLookupStrategy::Ipv6thenIpv4) {
|
||||
core.resolvers.dns.ipv4_add(
|
||||
"mx.foobar.org",
|
||||
vec!["127.0.0.1".parse().unwrap()],
|
||||
Instant::now() + Duration::from_secs(10),
|
||||
);
|
||||
}
|
||||
core.resolvers.dns.ipv6_add(
|
||||
"mx.foobar.org",
|
||||
vec!["::1".parse().unwrap()],
|
||||
Instant::now() + Duration::from_secs(10),
|
||||
);
|
||||
|
||||
// Retry on failed STARTTLS
|
||||
let mut local_qr = core.init_test_queue("smtp_iplookup_local");
|
||||
core.session.config.rcpt.relay = IfBlock::new(true);
|
||||
|
||||
let core = Arc::new(core);
|
||||
let mut queue = Queue::default();
|
||||
let mut session = Session::test(core.clone());
|
||||
session.data.remote_ip = "10.0.0.1".parse().unwrap();
|
||||
session.eval_session_params().await;
|
||||
session.ehlo("mx.test.org").await;
|
||||
session
|
||||
.send_message("john@test.org", &["bill@foobar.org"], "test:no_dkim", "250")
|
||||
.await;
|
||||
DeliveryAttempt::from(local_qr.read_event().await.unwrap_message())
|
||||
.try_deliver(core.clone(), &mut queue)
|
||||
.await;
|
||||
if matches!(strategy, IpLookupStrategy::Ipv6thenIpv4) {
|
||||
local_qr.read_event().await.unwrap_done();
|
||||
remote_qr.read_event().await.unwrap_message();
|
||||
} else {
|
||||
let status = local_qr.read_event().await.unwrap_retry().inner.domains[0]
|
||||
.status
|
||||
.to_string();
|
||||
assert!(status.contains("Connection refused"));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -32,6 +32,7 @@ use super::add_test_certs;
|
|||
|
||||
pub mod dane;
|
||||
pub mod extensions;
|
||||
pub mod ip_lookup;
|
||||
pub mod lmtp;
|
||||
pub mod mta_sts;
|
||||
pub mod smtp;
|
||||
|
|
Loading…
Reference in a new issue