mirror of
https://github.com/stalwartlabs/mail-server.git
synced 2025-11-09 21:31:12 +08:00
253 lines
7.9 KiB
Rust
253 lines
7.9 KiB
Rust
/*
|
|
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
|
|
*
|
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
|
*/
|
|
|
|
use std::{
|
|
fs,
|
|
net::{IpAddr, Ipv4Addr},
|
|
path::PathBuf,
|
|
time::SystemTime,
|
|
};
|
|
|
|
use common::config::smtp::queue::{QueueExpiry, QueueName};
|
|
use smtp_proto::{RCPT_NOTIFY_DELAY, RCPT_NOTIFY_FAILURE, RCPT_NOTIFY_SUCCESS, Response};
|
|
use store::write::now;
|
|
use utils::BlobHash;
|
|
|
|
use crate::smtp::{QueueReceiver, TestSMTP, inbound::sign::SIGNATURES};
|
|
use smtp::queue::{
|
|
Error, ErrorDetails, HostResponse, Message, MessageWrapper, Recipient, Schedule, Status,
|
|
UnexpectedResponse, dsn::SendDsn,
|
|
};
|
|
|
|
const CONFIG: &str = r#"
|
|
[report]
|
|
submitter = "'mx.example.org'"
|
|
|
|
[session.ehlo]
|
|
reject-non-fqdn = false
|
|
|
|
[session.rcpt]
|
|
relay = true
|
|
|
|
[report.dsn]
|
|
from-name = "'Mail Delivery Subsystem'"
|
|
from-address = "'MAILER-DAEMON@example.org'"
|
|
sign = "['rsa']"
|
|
|
|
"#;
|
|
|
|
#[tokio::test]
|
|
async fn generate_dsn() {
|
|
// Enable logging
|
|
crate::enable_logging();
|
|
|
|
let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
|
path.push("resources");
|
|
path.push("smtp");
|
|
path.push("dsn");
|
|
path.push("original.txt");
|
|
let size = fs::metadata(&path).unwrap().len() as u64;
|
|
let dsn_original = fs::read_to_string(&path).unwrap();
|
|
|
|
let flags = RCPT_NOTIFY_FAILURE | RCPT_NOTIFY_DELAY | RCPT_NOTIFY_SUCCESS;
|
|
let mut message = MessageWrapper {
|
|
queue_id: 0,
|
|
span_id: 0,
|
|
is_multi_queue: false,
|
|
queue_name: QueueName::default(),
|
|
message: Message {
|
|
size,
|
|
created: SystemTime::now()
|
|
.duration_since(SystemTime::UNIX_EPOCH)
|
|
.map_or(0, |d| d.as_secs()),
|
|
return_path: "sender@foobar.org".into(),
|
|
return_path_lcase: "".into(),
|
|
return_path_domain: "foobar.org".into(),
|
|
recipients: vec![Recipient {
|
|
address: "foobar@example.org".into(),
|
|
address_lcase: "foobar@example.org".into(),
|
|
status: Status::PermanentFailure(ErrorDetails {
|
|
entity: "mx.example.org".into(),
|
|
details: Error::UnexpectedResponse(UnexpectedResponse {
|
|
command: "RCPT TO:<foobar@example.org>".into(),
|
|
response: Response {
|
|
code: 550,
|
|
esc: [5, 1, 2],
|
|
message: "User does not exist".into(),
|
|
},
|
|
}),
|
|
}),
|
|
flags: 0,
|
|
orcpt: None,
|
|
retry: Schedule::now(),
|
|
notify: Schedule::now(),
|
|
expires: QueueExpiry::Ttl(10),
|
|
queue: QueueName::default(),
|
|
}],
|
|
flags: 0,
|
|
env_id: None,
|
|
priority: 0,
|
|
blob_hash: BlobHash::generate(dsn_original.as_bytes()),
|
|
quota_keys: vec![],
|
|
received_from_ip: IpAddr::V4(Ipv4Addr::LOCALHOST),
|
|
received_via_port: 0,
|
|
},
|
|
};
|
|
|
|
// Load config
|
|
let mut local = TestSMTP::new("smtp_dsn_test", CONFIG.to_string() + SIGNATURES).await;
|
|
let core = local.build_smtp();
|
|
let qr = &mut local.queue_receiver;
|
|
|
|
// Create temp dir for queue
|
|
qr.blob_store
|
|
.put_blob(
|
|
message.message.blob_hash.as_slice(),
|
|
dsn_original.as_bytes(),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
// Disabled DSN
|
|
core.send_dsn(&mut message).await;
|
|
qr.assert_no_events();
|
|
qr.assert_queue_is_empty().await;
|
|
|
|
// Failure DSN
|
|
message.message.recipients[0].flags = flags;
|
|
core.send_dsn(&mut message).await;
|
|
let dsn_message = qr.expect_message().await;
|
|
qr.compare_dsn(dsn_message.message, "failure.eml").await;
|
|
|
|
// Success DSN
|
|
message.message.recipients.push(Recipient {
|
|
address: "jane@example.org".into(),
|
|
address_lcase: "jane@example.org".into(),
|
|
status: Status::Completed(HostResponse {
|
|
hostname: "mx2.example.org".into(),
|
|
response: Response {
|
|
code: 250,
|
|
esc: [2, 1, 5],
|
|
message: "Message accepted for delivery".into(),
|
|
},
|
|
}),
|
|
flags,
|
|
orcpt: None,
|
|
retry: Schedule::now(),
|
|
notify: Schedule::now(),
|
|
expires: QueueExpiry::Ttl(10),
|
|
queue: QueueName::default(),
|
|
});
|
|
core.send_dsn(&mut message).await;
|
|
let dsn_message = qr.expect_message().await;
|
|
qr.compare_dsn(dsn_message.message, "success.eml").await;
|
|
|
|
// Delay DSN
|
|
message.message.recipients.push(Recipient {
|
|
address: "john.doe@example.org".into(),
|
|
address_lcase: "john.doe@example.org".into(),
|
|
status: Status::TemporaryFailure(ErrorDetails {
|
|
entity: "mx.domain.org".into(),
|
|
details: Error::ConnectionError("Connection timeout".into()),
|
|
}),
|
|
flags,
|
|
orcpt: Some("jdoe@example.org".into()),
|
|
retry: Schedule::now(),
|
|
notify: Schedule::now(),
|
|
expires: QueueExpiry::Ttl(10),
|
|
queue: QueueName::default(),
|
|
});
|
|
core.send_dsn(&mut message).await;
|
|
let dsn_message = qr.expect_message().await;
|
|
qr.compare_dsn(dsn_message.message, "delay.eml").await;
|
|
|
|
// Mixed DSN
|
|
for rcpt in &mut message.message.recipients {
|
|
rcpt.flags = flags;
|
|
}
|
|
message.message.recipients.last_mut().unwrap().notify.due = now();
|
|
core.send_dsn(&mut message).await;
|
|
let dsn_message = qr.expect_message().await;
|
|
qr.compare_dsn(dsn_message.message, "mixed.eml").await;
|
|
|
|
// Load queue
|
|
let queue = qr.read_queued_messages().await;
|
|
assert_eq!(queue.len(), 4);
|
|
}
|
|
|
|
impl QueueReceiver {
|
|
async fn compare_dsn(&self, message: Message, test: &str) {
|
|
let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
|
path.push("resources");
|
|
path.push("smtp");
|
|
path.push("dsn");
|
|
path.push(test);
|
|
|
|
let bytes = self
|
|
.blob_store
|
|
.get_blob(message.blob_hash.as_slice(), 0..usize::MAX)
|
|
.await
|
|
.unwrap()
|
|
.unwrap();
|
|
|
|
let dsn = remove_ids(bytes);
|
|
let dsn_expected = fs::read_to_string(&path).unwrap();
|
|
|
|
if dsn != dsn_expected {
|
|
let mut failed = PathBuf::from(&path);
|
|
failed.set_extension("failed");
|
|
fs::write(&failed, dsn.as_bytes()).unwrap();
|
|
panic!(
|
|
"Failed for {}, output saved to {}",
|
|
path.display(),
|
|
failed.display()
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn remove_ids(message: Vec<u8>) -> String {
|
|
let old_message = String::from_utf8(message).unwrap();
|
|
let mut message = String::with_capacity(old_message.len());
|
|
let mut found_dkim = false;
|
|
let mut skip = false;
|
|
|
|
let mut boundary = "";
|
|
for line in old_message.split("\r\n") {
|
|
if skip {
|
|
if line.chars().next().unwrap().is_ascii_whitespace() {
|
|
continue;
|
|
} else {
|
|
skip = false;
|
|
}
|
|
}
|
|
if line.starts_with("Date:") || line.starts_with("Message-ID:") {
|
|
continue;
|
|
} else if !found_dkim && line.starts_with("DKIM-Signature:") {
|
|
found_dkim = true;
|
|
skip = true;
|
|
continue;
|
|
} else if line.starts_with("--") {
|
|
message.push_str(&line.replace(boundary, "mime_boundary"));
|
|
} else if let Some((_, boundary_)) = line.split_once("boundary=\"") {
|
|
boundary = boundary_.split_once('"').unwrap().0;
|
|
message.push_str(&line.replace(boundary, "mime_boundary"));
|
|
} else if line.starts_with("Arrival-Date:") {
|
|
message.push_str("Arrival-Date: <date goes here>");
|
|
} else if line.starts_with("Will-Retry-Until:") {
|
|
message.push_str("Will-Retry-Until: <date goes here>");
|
|
} else {
|
|
message.push_str(line);
|
|
}
|
|
message.push_str("\r\n");
|
|
}
|
|
|
|
if !found_dkim {
|
|
panic!("No DKIM signature found in: {old_message}");
|
|
}
|
|
|
|
message
|
|
}
|