Added retry_num, notify_num, last_error, last_status variables to queue expressions

This commit is contained in:
mdecimus
2024-05-06 12:55:31 +02:00
parent 54dbd9ec5e
commit 5b236e00ae
17 changed files with 427 additions and 183 deletions

View File

@@ -0,0 +1,134 @@
/*
* 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::time::{Duration, Instant};
use common::config::server::ServerProtocol;
use mail_auth::MX;
use store::write::now;
use crate::smtp::{outbound::TestServer, session::TestSession};
const LOCAL: &str = r#"
[queue.outbound]
next-hop = [{if = "retry_num > 0", then = "'fallback'"},
{else = false}]
[session.rcpt]
relay = true
max-recipients = 100
[session.extensions]
dsn = true
[remote.fallback]
address = fallback.foobar.org
port = 9925
protocol = 'smtp'
concurrency = 5
[remote.fallback.tls]
implicit = false
allow-invalid-certs = true
"#;
const REMOTE: &str = r#"
[session.rcpt]
relay = true
[session.ehlo]
reject-non-fqdn = false
[session.extensions]
dsn = true
chunking = false
"#;
#[tokio::test]
#[serial_test::serial]
async fn fallback_relay() {
/*let disable = 1;
tracing::subscriber::set_global_default(
tracing_subscriber::FmtSubscriber::builder()
.with_max_level(tracing::Level::TRACE)
.finish(),
)
.unwrap();*/
// Start test server
let mut remote = TestServer::new("smtp_fallback_remote", REMOTE, true).await;
let _rx = remote.start(&[ServerProtocol::Smtp]).await;
let mut local = TestServer::new("smtp_fallback_local", LOCAL, true).await;
// Add mock DNS entries
let core = local.build_smtp();
core.core.smtp.resolvers.dns.mx_add(
"foobar.org",
vec![MX {
exchanges: vec!["_dns_error.foobar.org".to_string()],
preference: 10,
}],
Instant::now() + Duration::from_secs(10),
);
/*core.core.smtp.resolvers.dns.ipv4_add(
"unreachable.foobar.org",
vec!["127.0.0.2".parse().unwrap()],
Instant::now() + Duration::from_secs(10),
);*/
core.core.smtp.resolvers.dns.ipv4_add(
"fallback.foobar.org",
vec!["127.0.0.1".parse().unwrap()],
Instant::now() + Duration::from_secs(10),
);
let mut session = local.new_session();
session.data.remote_ip_str = "10.0.0.1".to_string();
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;
local
.qr
.expect_message_then_deliver()
.await
.try_deliver(core.clone())
.await;
let mut retry = local.qr.expect_message().await;
let prev_due = retry.domains[0].retry.due;
let next_due = now();
let queue_id = retry.id;
retry.domains[0].retry.due = next_due;
retry
.save_changes(&core, prev_due.into(), next_due.into())
.await;
local
.qr
.delivery_attempt(queue_id)
.await
.try_deliver(core.clone())
.await;
tokio::time::sleep(Duration::from_millis(100)).await;
remote.qr.expect_message().await;
}

View File

@@ -42,6 +42,7 @@ use super::{
pub mod dane;
pub mod extensions;
pub mod fallback_relay;
pub mod ip_lookup;
pub mod lmtp;
pub mod mta_sts;

View File

@@ -33,7 +33,7 @@ use crate::smtp::{
inbound::TestQueueEvent, outbound::TestServer, queue::manager::new_message,
session::TestSession,
};
use smtp::queue::{Message, QueueEnvelope};
use smtp::queue::{Domain, Message, QueueEnvelope, Schedule, Status};
const CONFIG: &str = r#"
[session.rcpt]
@@ -113,7 +113,7 @@ async fn throttle_outbound() {
for t in &throttle.sender {
core.is_allowed(
t,
&QueueEnvelope::test(&test_message, "", ""),
&QueueEnvelope::test(&test_message, 0, ""),
&mut in_flight,
&span,
)
@@ -138,7 +138,7 @@ async fn throttle_outbound() {
for t in &throttle.sender {
core.is_allowed(
t,
&QueueEnvelope::test(&test_message, "", ""),
&QueueEnvelope::test(&test_message, 0, ""),
&mut in_flight,
&span,
)
@@ -162,10 +162,17 @@ async fn throttle_outbound() {
// Expect concurrency throttle for recipient domain 'example.org'
test_message.return_path_domain = "test.net".to_string();
test_message.domains.push(Domain {
domain: "example.org".to_string(),
retry: Schedule::now(),
notify: Schedule::now(),
expires: 0,
status: Status::Scheduled,
});
for t in &throttle.rcpt {
core.is_allowed(
t,
&QueueEnvelope::test(&test_message, "example.org", ""),
&QueueEnvelope::test(&test_message, 0, ""),
&mut in_flight,
&span,
)
@@ -191,11 +198,18 @@ async fn throttle_outbound() {
local.qr.read_event().await.unwrap_on_hold();
in_flight.clear();
// Expect rate limit throttle for recipient domain 'example.org'
// Expect rate limit throttle for recipient domain 'example.net'
test_message.domains.push(Domain {
domain: "example.net".to_string(),
retry: Schedule::now(),
notify: Schedule::now(),
expires: 0,
status: Status::Scheduled,
});
for t in &throttle.rcpt {
core.is_allowed(
t,
&QueueEnvelope::test(&test_message, "example.net", ""),
&QueueEnvelope::test(&test_message, 1, ""),
&mut in_flight,
&span,
)
@@ -236,10 +250,17 @@ async fn throttle_outbound() {
vec!["127.0.0.1".parse().unwrap()],
Instant::now() + Duration::from_secs(10),
);
test_message.domains.push(Domain {
domain: "test.org".to_string(),
retry: Schedule::now(),
notify: Schedule::now(),
expires: 0,
status: Status::Scheduled,
});
for t in &throttle.host {
core.is_allowed(
t,
&QueueEnvelope::test(&test_message, "test.org", "mx.test.org"),
&QueueEnvelope::test(&test_message, 2, "mx.test.org"),
&mut in_flight,
&span,
)
@@ -276,7 +297,7 @@ async fn throttle_outbound() {
for t in &throttle.host {
core.is_allowed(
t,
&QueueEnvelope::test(&test_message, "example.net", "mx.test.net"),
&QueueEnvelope::test(&test_message, 1, "mx.test.net"),
&mut in_flight,
&span,
)
@@ -301,17 +322,18 @@ async fn throttle_outbound() {
}
pub trait TestQueueEnvelope<'x> {
fn test(message: &'x Message, domain: &'x str, mx: &'x str) -> Self;
fn test(message: &'x Message, current_domain: usize, mx: &'x str) -> Self;
}
impl<'x> TestQueueEnvelope<'x> for QueueEnvelope<'x> {
fn test(message: &'x Message, domain: &'x str, mx: &'x str) -> Self {
fn test(message: &'x Message, current_domain: usize, mx: &'x str) -> Self {
QueueEnvelope {
message,
domain,
mx,
remote_ip: IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)),
local_ip: IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)),
current_domain,
current_rcpt: 0,
}
}
}

View File

@@ -41,7 +41,8 @@ relay = true
hostname = "'badtls.foobar.org'"
[queue.outbound.tls]
starttls = "optional"
starttls = [ { if = "retry_num > 0 && last_error == 'tls'", then = "disable"},
{ else = "optional" }]
"#;
const REMOTE: &str = r#"
@@ -104,7 +105,6 @@ async fn starttls_optional() {
.try_deliver(core.clone())
.await;
let mut retry = local.qr.expect_message().await;
assert!(retry.domains[0].disable_tls);
let prev_due = retry.domains[0].retry.due;
let next_due = now();
let queue_id = retry.id;

View File

@@ -96,7 +96,6 @@ async fn generate_dsn() {
entity: "mx.domain.org".to_string(),
details: "Connection timeout".to_string(),
})),
disable_tls: false,
}],
flags: 0,
env_id: None,

View File

@@ -165,7 +165,6 @@ fn domain(domain: &str, retry: u64, notify: u64, expires: u64) -> Domain {
notify: Schedule::later(Duration::from_secs(notify)),
expires: now() + expires,
status: Status::Scheduled,
disable_tls: false,
}
}