Fixed Ipv6thenIpv4 SMTP lookups

This commit is contained in:
mdecimus 2023-09-05 14:22:16 +02:00
parent 56b1fb893d
commit 6404d27dd9
7 changed files with 409 additions and 84 deletions

View file

@ -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()
}

View file

@ -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;

View file

@ -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 {:?}.",

View file

@ -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;
}

View file

@ -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]

View 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"));
}
}
}

View file

@ -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;