From 6404d27dd9699e81f680ba1f4ec76bf1ba1c95b8 Mon Sep 17 00:00:00 2001 From: mdecimus Date: Tue, 5 Sep 2023 14:22:16 +0200 Subject: [PATCH] Fixed Ipv6thenIpv4 SMTP lookups --- crates/antispam/src/import/spamassassin.rs | 212 +++++++++++++++++---- crates/smtp/src/outbound/delivery.rs | 13 +- crates/smtp/src/outbound/lookup.rs | 129 +++++++++---- tests/resources/smtp/sieve/functions.sieve | 12 +- tests/src/smtp/lookup/utils.rs | 16 +- tests/src/smtp/outbound/ip_lookup.rs | 110 +++++++++++ tests/src/smtp/outbound/mod.rs | 1 + 7 files changed, 409 insertions(+), 84 deletions(-) create mode 100644 tests/src/smtp/outbound/ip_lookup.rs diff --git a/crates/antispam/src/import/spamassassin.rs b/crates/antispam/src/import/spamassassin.rs index 7d694651..31a9098b 100644 --- a/crates/antispam/src/import/spamassassin.rs +++ b/crates/antispam/src/import/spamassassin.rs @@ -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::) + .expect("missing parameters for check_header_count_range from"); + let range_to = params + .next() + .and_then(param_to_num::) + .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::) + .expect("missing parameters for check_illegal_chars ratio"); + let count = params + .next() + .and_then(param_to_num::) + .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(text: impl AsRef) -> Option { - let text = text.as_ref(); + strip_quotes(text.as_ref()).parse::().ok() +} + +fn strip_quotes(text: &str) -> &str { text.strip_prefix('\"') .and_then(|v| v.strip_suffix('\"')) .unwrap_or(text) - .parse::() - .ok() } diff --git a/crates/smtp/src/outbound/delivery.rs b/crates/smtp/src/outbound/delivery.rs index a822e90d..0e493fb2 100644 --- a/crates/smtp/src/outbound/delivery.rs +++ b/crates/smtp/src/outbound/delivery.rs @@ -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; diff --git a/crates/smtp/src/outbound/lookup.rs b/crates/smtp/src/outbound/lookup.rs index 72778b8f..7cefec5a 100644 --- a/crates/smtp/src/outbound/lookup.rs +++ b/crates/smtp/src/outbound/lookup.rs @@ -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, + pub source_ipv6: Option, + pub remote_ips: Vec, +} + impl SMTP { + pub async fn ip_lookup( + &self, + key: &str, + strategy: IpLookupStrategy, + max_results: usize, + ) -> mail_auth::Result> { + 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, max_multihomed: usize, - ) -> Result<(Option, Vec), Status<(), Error>> { + ) -> Result> { 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 {:?}.", diff --git a/tests/resources/smtp/sieve/functions.sieve b/tests/resources/smtp/sieve/functions.sieve index ca27ba45..442046a0 100644 --- a/tests/resources/smtp/sieve/functions.sieve +++ b/tests/resources/smtp/sieve/functions.sieve @@ -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; } diff --git a/tests/src/smtp/lookup/utils.rs b/tests/src/smtp/lookup/utils.rs index 576380c4..8bdfc7f7 100644 --- a/tests/src/smtp/lookup/utils.rs +++ b/tests/src/smtp/lookup/utils.rs @@ -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] diff --git a/tests/src/smtp/outbound/ip_lookup.rs b/tests/src/smtp/outbound/ip_lookup.rs new file mode 100644 index 00000000..7d764834 --- /dev/null +++ b/tests/src/smtp/outbound/ip_lookup.rs @@ -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 . + * + * 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")); + } + } +} diff --git a/tests/src/smtp/outbound/mod.rs b/tests/src/smtp/outbound/mod.rs index 3bc3765b..bebc2cdd 100644 --- a/tests/src/smtp/outbound/mod.rs +++ b/tests/src/smtp/outbound/mod.rs @@ -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;