768 lines
28 KiB
Rust
768 lines
28 KiB
Rust
/*
|
|
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art>
|
|
*
|
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
|
*/
|
|
|
|
use mail_builder::headers::content_type::ContentType;
|
|
use mail_builder::headers::HeaderType;
|
|
use mail_builder::mime::{make_boundary, BodyPart, MimePart};
|
|
use mail_builder::MessageBuilder;
|
|
use mail_parser::DateTime;
|
|
use smtp_proto::{
|
|
Response, RCPT_NOTIFY_DELAY, RCPT_NOTIFY_FAILURE, RCPT_NOTIFY_NEVER, RCPT_NOTIFY_SUCCESS,
|
|
};
|
|
use std::fmt::Write;
|
|
use std::time::Duration;
|
|
use store::write::now;
|
|
|
|
use crate::core::SMTP;
|
|
use crate::outbound::client::from_error_status;
|
|
|
|
use super::{
|
|
Domain, Error, ErrorDetails, HostResponse, Message, MessageSource, QueueEnvelope, Recipient,
|
|
Status, RCPT_DSN_SENT, RCPT_STATUS_CHANGED,
|
|
};
|
|
|
|
impl SMTP {
|
|
pub async fn send_dsn(&self, message: &mut Message) {
|
|
// Send DSN events
|
|
self.log_dsn(message).await;
|
|
|
|
if !message.return_path.is_empty() {
|
|
// Build DSN
|
|
if let Some(dsn) = message.build_dsn(self).await {
|
|
let mut dsn_message = self.new_message("", "", "", message.span_id);
|
|
dsn_message
|
|
.add_recipient_parts(
|
|
&message.return_path,
|
|
&message.return_path_lcase,
|
|
&message.return_path_domain,
|
|
self,
|
|
)
|
|
.await;
|
|
|
|
// Sign message
|
|
let signature = self
|
|
.sign_message(message, &self.core.smtp.queue.dsn.sign, &dsn)
|
|
.await;
|
|
|
|
// Queue DSN
|
|
dsn_message
|
|
.queue(
|
|
signature.as_deref(),
|
|
&dsn,
|
|
message.span_id,
|
|
self,
|
|
MessageSource::Dsn,
|
|
)
|
|
.await;
|
|
}
|
|
} else {
|
|
// Handle double bounce
|
|
message.handle_double_bounce();
|
|
}
|
|
}
|
|
|
|
async fn log_dsn(&self, message: &Message) {
|
|
let now = now();
|
|
|
|
for rcpt in &message.recipients {
|
|
if rcpt.has_flag(RCPT_DSN_SENT) {
|
|
continue;
|
|
}
|
|
|
|
let domain = &message.domains[rcpt.domain_idx];
|
|
match &rcpt.status {
|
|
Status::Completed(response) => {
|
|
trc::event!(
|
|
Delivery(trc::DeliveryEvent::DsnSuccess),
|
|
SpanId = message.span_id,
|
|
To = rcpt.address_lcase.clone(),
|
|
Hostname = response.hostname.clone(),
|
|
Code = response.response.code,
|
|
Details = response.response.message.to_string(),
|
|
);
|
|
}
|
|
Status::TemporaryFailure(response) if domain.notify.due <= now => {
|
|
trc::event!(
|
|
Delivery(trc::DeliveryEvent::DsnTempFail),
|
|
SpanId = message.span_id,
|
|
To = rcpt.address_lcase.clone(),
|
|
Hostname = response.hostname.entity.clone(),
|
|
Code = response.response.code,
|
|
Details = response.response.message.to_string(),
|
|
NextRetry = trc::Value::Timestamp(domain.retry.due),
|
|
Expires = trc::Value::Timestamp(domain.expires),
|
|
Total = domain.retry.inner,
|
|
);
|
|
}
|
|
Status::PermanentFailure(response) => {
|
|
trc::event!(
|
|
Delivery(trc::DeliveryEvent::DsnPermFail),
|
|
SpanId = message.span_id,
|
|
To = rcpt.address_lcase.clone(),
|
|
Hostname = response.hostname.entity.clone(),
|
|
Code = response.response.code,
|
|
Details = response.response.message.to_string(),
|
|
Total = domain.retry.inner,
|
|
);
|
|
}
|
|
Status::Scheduled => {
|
|
// There is no status for this address, use the domain's status.
|
|
match &domain.status {
|
|
Status::PermanentFailure(_) => {
|
|
trc::event!(
|
|
Delivery(trc::DeliveryEvent::DsnPermFail),
|
|
SpanId = message.span_id,
|
|
To = rcpt.address_lcase.clone(),
|
|
Details = from_error_status(&domain.status),
|
|
Total = domain.retry.inner,
|
|
);
|
|
}
|
|
Status::TemporaryFailure(_) if domain.notify.due <= now => {
|
|
trc::event!(
|
|
Delivery(trc::DeliveryEvent::DsnTempFail),
|
|
SpanId = message.span_id,
|
|
To = rcpt.address_lcase.clone(),
|
|
Details = from_error_status(&domain.status),
|
|
NextRetry = trc::Value::Timestamp(domain.retry.due),
|
|
Expires = trc::Value::Timestamp(domain.expires),
|
|
Total = domain.retry.inner,
|
|
);
|
|
}
|
|
Status::Scheduled if domain.notify.due <= now => {
|
|
trc::event!(
|
|
Delivery(trc::DeliveryEvent::DsnTempFail),
|
|
SpanId = message.span_id,
|
|
To = rcpt.address_lcase.clone(),
|
|
Details = "Concurrency limited",
|
|
NextRetry = trc::Value::Timestamp(domain.retry.due),
|
|
Expires = trc::Value::Timestamp(domain.expires),
|
|
Total = domain.retry.inner,
|
|
);
|
|
}
|
|
_ => continue,
|
|
}
|
|
}
|
|
_ => continue,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Message {
|
|
pub async fn build_dsn(&mut self, core: &SMTP) -> Option<Vec<u8>> {
|
|
let config = &core.core.smtp.queue;
|
|
let now = now();
|
|
|
|
let mut txt_success = String::new();
|
|
let mut txt_delay = String::new();
|
|
let mut txt_failed = String::new();
|
|
let mut dsn = String::new();
|
|
|
|
for rcpt in &mut self.recipients {
|
|
if rcpt.has_flag(RCPT_DSN_SENT | RCPT_NOTIFY_NEVER) {
|
|
continue;
|
|
}
|
|
let domain = &self.domains[rcpt.domain_idx];
|
|
match &rcpt.status {
|
|
Status::Completed(response) => {
|
|
rcpt.flags |= RCPT_DSN_SENT | RCPT_STATUS_CHANGED;
|
|
if !rcpt.has_flag(RCPT_NOTIFY_SUCCESS) {
|
|
continue;
|
|
}
|
|
rcpt.write_dsn(&mut dsn);
|
|
rcpt.status.write_dsn(&mut dsn);
|
|
response.write_dsn_text(&rcpt.address, &mut txt_success);
|
|
}
|
|
Status::TemporaryFailure(response)
|
|
if domain.notify.due <= now && rcpt.has_flag(RCPT_NOTIFY_DELAY) =>
|
|
{
|
|
rcpt.write_dsn(&mut dsn);
|
|
rcpt.status.write_dsn(&mut dsn);
|
|
domain.write_dsn_will_retry_until(&mut dsn);
|
|
response.write_dsn_text(&rcpt.address, &mut txt_delay);
|
|
}
|
|
Status::PermanentFailure(response) => {
|
|
rcpt.flags |= RCPT_DSN_SENT | RCPT_STATUS_CHANGED;
|
|
if !rcpt.has_flag(RCPT_NOTIFY_FAILURE) {
|
|
continue;
|
|
}
|
|
rcpt.write_dsn(&mut dsn);
|
|
rcpt.status.write_dsn(&mut dsn);
|
|
response.write_dsn_text(&rcpt.address, &mut txt_failed);
|
|
}
|
|
Status::Scheduled => {
|
|
// There is no status for this address, use the domain's status.
|
|
match &domain.status {
|
|
Status::PermanentFailure(err) => {
|
|
rcpt.flags |= RCPT_DSN_SENT | RCPT_STATUS_CHANGED;
|
|
if !rcpt.has_flag(RCPT_NOTIFY_FAILURE) {
|
|
continue;
|
|
}
|
|
rcpt.write_dsn(&mut dsn);
|
|
domain.status.write_dsn(&mut dsn);
|
|
err.write_dsn_text(&rcpt.address, &domain.domain, &mut txt_failed);
|
|
}
|
|
Status::TemporaryFailure(err)
|
|
if domain.notify.due <= now && rcpt.has_flag(RCPT_NOTIFY_DELAY) =>
|
|
{
|
|
rcpt.write_dsn(&mut dsn);
|
|
domain.status.write_dsn(&mut dsn);
|
|
domain.write_dsn_will_retry_until(&mut dsn);
|
|
err.write_dsn_text(&rcpt.address, &domain.domain, &mut txt_delay);
|
|
}
|
|
Status::Scheduled
|
|
if domain.notify.due <= now && rcpt.has_flag(RCPT_NOTIFY_DELAY) =>
|
|
{
|
|
// This case should not happen under normal circumstances
|
|
rcpt.write_dsn(&mut dsn);
|
|
domain.status.write_dsn(&mut dsn);
|
|
domain.write_dsn_will_retry_until(&mut dsn);
|
|
Error::ConcurrencyLimited.write_dsn_text(
|
|
&rcpt.address,
|
|
&domain.domain,
|
|
&mut txt_delay,
|
|
);
|
|
}
|
|
Status::Completed(_) => {
|
|
#[cfg(feature = "test_mode")]
|
|
panic!("This should not have happened.");
|
|
}
|
|
_ => continue,
|
|
}
|
|
}
|
|
_ => continue,
|
|
}
|
|
|
|
dsn.push_str("\r\n");
|
|
}
|
|
|
|
// Build text response
|
|
let txt_len = txt_success.len() + txt_delay.len() + txt_failed.len();
|
|
if txt_len == 0 {
|
|
return None;
|
|
}
|
|
|
|
let has_success = !txt_success.is_empty();
|
|
let has_delay = !txt_delay.is_empty();
|
|
let has_failure = !txt_failed.is_empty();
|
|
|
|
let mut txt = String::with_capacity(txt_len + 128);
|
|
let (subject, is_mixed) = if has_success && !has_delay && !has_failure {
|
|
txt.push_str(
|
|
"Your message has been successfully delivered to the following recipients:\r\n\r\n",
|
|
);
|
|
("Successfully delivered message", false)
|
|
} else if has_delay && !has_success && !has_failure {
|
|
txt.push_str("There was a temporary problem delivering your message to the following recipients:\r\n\r\n");
|
|
("Warning: Delay in message delivery", false)
|
|
} else if has_failure && !has_success && !has_delay {
|
|
txt.push_str(
|
|
"Your message could not be delivered to the following recipients:\r\n\r\n",
|
|
);
|
|
("Failed to deliver message", false)
|
|
} else if has_success {
|
|
txt.push_str("Your message has been partially delivered:\r\n\r\n");
|
|
("Partially delivered message", true)
|
|
} else {
|
|
txt.push_str("Your message could not be delivered to some recipients:\r\n\r\n");
|
|
(
|
|
"Warning: Temporary and permanent failures during message delivery",
|
|
true,
|
|
)
|
|
};
|
|
|
|
if has_success {
|
|
if is_mixed {
|
|
txt.push_str(
|
|
" ----- Delivery to the following addresses was successful -----\r\n",
|
|
);
|
|
}
|
|
|
|
txt.push_str(&txt_success);
|
|
txt.push_str("\r\n");
|
|
}
|
|
|
|
if has_delay {
|
|
if is_mixed {
|
|
txt.push_str(
|
|
" ----- There was a temporary problem delivering to these addresses -----\r\n",
|
|
);
|
|
}
|
|
txt.push_str(&txt_delay);
|
|
txt.push_str("\r\n");
|
|
}
|
|
|
|
if has_failure {
|
|
if is_mixed {
|
|
txt.push_str(" ----- Delivery to the following addresses failed -----\r\n");
|
|
}
|
|
txt.push_str(&txt_failed);
|
|
txt.push_str("\r\n");
|
|
}
|
|
|
|
// Update next delay notification time
|
|
if has_delay {
|
|
let mut changes = Vec::new();
|
|
for (domain_idx, domain) in self.domains.iter().enumerate() {
|
|
if matches!(
|
|
&domain.status,
|
|
Status::TemporaryFailure(_) | Status::Scheduled
|
|
) && domain.notify.due <= now
|
|
{
|
|
let envelope = QueueEnvelope::new(self, domain_idx);
|
|
|
|
if let Some(next_notify) = core
|
|
.core
|
|
.eval_if::<Vec<Duration>, _>(&config.notify, &envelope, self.span_id)
|
|
.await
|
|
.and_then(|notify| {
|
|
notify.into_iter().nth((domain.notify.inner + 1) as usize)
|
|
})
|
|
{
|
|
changes.push((domain_idx, 1, now + next_notify.as_secs()));
|
|
} else {
|
|
changes.push((domain_idx, 0, domain.expires + 10));
|
|
}
|
|
}
|
|
}
|
|
|
|
for (domain_idx, inner, due) in changes {
|
|
let domain = &mut self.domains[domain_idx];
|
|
domain.notify.inner += inner;
|
|
domain.notify.due = due;
|
|
}
|
|
}
|
|
|
|
// Obtain hostname and sender addresses
|
|
let from_name = core
|
|
.core
|
|
.eval_if(&config.dsn.name, self, self.span_id)
|
|
.await
|
|
.unwrap_or_else(|| String::from("Mail Delivery Subsystem"));
|
|
let from_addr = core
|
|
.core
|
|
.eval_if(&config.dsn.address, self, self.span_id)
|
|
.await
|
|
.unwrap_or_else(|| String::from("MAILER-DAEMON@localhost"));
|
|
let reporting_mta = core
|
|
.core
|
|
.eval_if(&core.core.smtp.report.submitter, self, self.span_id)
|
|
.await
|
|
.unwrap_or_else(|| String::from("localhost"));
|
|
|
|
// Prepare DSN
|
|
let mut dsn_header = String::with_capacity(dsn.len() + 128);
|
|
self.write_dsn_headers(&mut dsn_header, &reporting_mta);
|
|
let dsn = dsn_header + dsn.as_str();
|
|
|
|
// Fetch up to 1024 bytes of message headers
|
|
let headers = match core
|
|
.core
|
|
.storage
|
|
.blob
|
|
.get_blob(self.blob_hash.as_slice(), 0..1024)
|
|
.await
|
|
{
|
|
Ok(Some(mut buf)) => {
|
|
let mut prev_ch = 0;
|
|
let mut last_lf = buf.len();
|
|
for (pos, &ch) in buf.iter().enumerate() {
|
|
match ch {
|
|
b'\n' => {
|
|
last_lf = pos + 1;
|
|
if prev_ch != b'\n' {
|
|
prev_ch = ch;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
b'\r' => (),
|
|
0 => break,
|
|
_ => {
|
|
prev_ch = ch;
|
|
}
|
|
}
|
|
}
|
|
if last_lf < 1024 {
|
|
buf.truncate(last_lf);
|
|
}
|
|
String::from_utf8(buf).unwrap_or_default()
|
|
}
|
|
Ok(None) => {
|
|
trc::event!(
|
|
Queue(trc::QueueEvent::BlobNotFound),
|
|
SpanId = self.span_id,
|
|
BlobId = self.blob_hash.to_hex(),
|
|
CausedBy = trc::location!()
|
|
);
|
|
|
|
String::new()
|
|
}
|
|
Err(err) => {
|
|
trc::error!(err
|
|
.span_id(self.span_id)
|
|
.details("Failed to fetch blobId")
|
|
.caused_by(trc::location!()));
|
|
|
|
String::new()
|
|
}
|
|
};
|
|
|
|
// Build message
|
|
MessageBuilder::new()
|
|
.from((from_name.as_str(), from_addr.as_str()))
|
|
.header("To", HeaderType::Text(self.return_path.as_str().into()))
|
|
.header("Auto-Submitted", HeaderType::Text("auto-generated".into()))
|
|
.message_id(format!("<{}@{}>", make_boundary("."), reporting_mta))
|
|
.subject(subject)
|
|
.body(MimePart::new(
|
|
ContentType::new("multipart/report").attribute("report-type", "delivery-status"),
|
|
BodyPart::Multipart(vec![
|
|
MimePart::new(ContentType::new("text/plain"), BodyPart::Text(txt.into())),
|
|
MimePart::new(
|
|
ContentType::new("message/delivery-status"),
|
|
BodyPart::Text(dsn.into()),
|
|
),
|
|
MimePart::new(
|
|
ContentType::new("message/rfc822"),
|
|
BodyPart::Text(headers.into()),
|
|
),
|
|
]),
|
|
))
|
|
.write_to_vec()
|
|
.unwrap_or_default()
|
|
.into()
|
|
}
|
|
|
|
fn handle_double_bounce(&mut self) {
|
|
let mut is_double_bounce = Vec::with_capacity(0);
|
|
|
|
for rcpt in &mut self.recipients {
|
|
if !rcpt.has_flag(RCPT_DSN_SENT | RCPT_NOTIFY_NEVER) {
|
|
match &rcpt.status {
|
|
Status::PermanentFailure(err) => {
|
|
rcpt.flags |= RCPT_DSN_SENT;
|
|
let mut dsn = String::new();
|
|
err.write_dsn_text(&rcpt.address, &mut dsn);
|
|
is_double_bounce.push(dsn);
|
|
}
|
|
Status::Scheduled => {
|
|
let domain = &self.domains[rcpt.domain_idx];
|
|
if let Status::PermanentFailure(err) = &domain.status {
|
|
rcpt.flags |= RCPT_DSN_SENT;
|
|
let mut dsn = String::new();
|
|
err.write_dsn_text(&rcpt.address, &domain.domain, &mut dsn);
|
|
is_double_bounce.push(dsn);
|
|
}
|
|
}
|
|
_ => (),
|
|
}
|
|
}
|
|
}
|
|
|
|
let now = now();
|
|
for domain in &mut self.domains {
|
|
if domain.notify.due <= now {
|
|
domain.notify.due = domain.expires + 10;
|
|
}
|
|
}
|
|
|
|
if !is_double_bounce.is_empty() {
|
|
trc::event!(
|
|
Delivery(trc::DeliveryEvent::DoubleBounce),
|
|
SpanId = self.span_id,
|
|
To = is_double_bounce
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
impl HostResponse<String> {
|
|
fn write_dsn_text(&self, addr: &str, dsn: &mut String) {
|
|
let _ = write!(
|
|
dsn,
|
|
"<{}> (delivered to '{}' with code {} ({}.{}.{}) '",
|
|
addr,
|
|
self.hostname,
|
|
self.response.code,
|
|
self.response.esc[0],
|
|
self.response.esc[1],
|
|
self.response.esc[2]
|
|
);
|
|
self.response.write_response(dsn);
|
|
dsn.push_str("')\r\n");
|
|
}
|
|
}
|
|
|
|
impl HostResponse<ErrorDetails> {
|
|
fn write_dsn_text(&self, addr: &str, dsn: &mut String) {
|
|
let _ = write!(dsn, "<{}> (host '{}' rejected ", addr, self.hostname.entity);
|
|
|
|
if !self.hostname.details.is_empty() {
|
|
let _ = write!(dsn, "command '{}'", self.hostname.details,);
|
|
} else {
|
|
dsn.push_str("transaction");
|
|
}
|
|
|
|
let _ = write!(
|
|
dsn,
|
|
" with code {} ({}.{}.{}) '",
|
|
self.response.code, self.response.esc[0], self.response.esc[1], self.response.esc[2]
|
|
);
|
|
self.response.write_response(dsn);
|
|
dsn.push_str("')\r\n");
|
|
}
|
|
}
|
|
|
|
impl Error {
|
|
fn write_dsn_text(&self, addr: &str, domain: &str, dsn: &mut String) {
|
|
match self {
|
|
Error::UnexpectedResponse(response) => {
|
|
response.write_dsn_text(addr, dsn);
|
|
}
|
|
Error::DnsError(err) => {
|
|
let _ = write!(dsn, "<{addr}> (failed to lookup '{domain}': {err})\r\n",);
|
|
}
|
|
Error::ConnectionError(details) => {
|
|
let _ = write!(
|
|
dsn,
|
|
"<{}> (connection to '{}' failed: {})\r\n",
|
|
addr, details.entity, details.details
|
|
);
|
|
}
|
|
Error::TlsError(details) => {
|
|
let _ = write!(
|
|
dsn,
|
|
"<{}> (TLS error from '{}': {})\r\n",
|
|
addr, details.entity, details.details
|
|
);
|
|
}
|
|
Error::DaneError(details) => {
|
|
let _ = write!(
|
|
dsn,
|
|
"<{}> (DANE failed to authenticate '{}': {})\r\n",
|
|
addr, details.entity, details.details
|
|
);
|
|
}
|
|
Error::MtaStsError(details) => {
|
|
let _ = write!(
|
|
dsn,
|
|
"<{addr}> (MTA-STS failed to authenticate '{domain}': {details})\r\n",
|
|
);
|
|
}
|
|
Error::RateLimited => {
|
|
let _ = write!(dsn, "<{addr}> (rate limited)\r\n");
|
|
}
|
|
Error::ConcurrencyLimited => {
|
|
let _ = write!(
|
|
dsn,
|
|
"<{addr}> (too many concurrent connections to remote server)\r\n",
|
|
);
|
|
}
|
|
Error::Io(err) => {
|
|
let _ = write!(dsn, "<{addr}> (queue error: {err})\r\n");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Message {
|
|
fn write_dsn_headers(&self, dsn: &mut String, reporting_mta: &str) {
|
|
let _ = write!(dsn, "Reporting-MTA: dns;{reporting_mta}\r\n");
|
|
dsn.push_str("Arrival-Date: ");
|
|
dsn.push_str(&DateTime::from_timestamp(self.created as i64).to_rfc822());
|
|
dsn.push_str("\r\n");
|
|
if let Some(env_id) = &self.env_id {
|
|
let _ = write!(dsn, "Original-Envelope-Id: {env_id}\r\n");
|
|
}
|
|
dsn.push_str("\r\n");
|
|
}
|
|
}
|
|
|
|
impl Recipient {
|
|
fn write_dsn(&self, dsn: &mut String) {
|
|
if let Some(orcpt) = &self.orcpt {
|
|
let _ = write!(dsn, "Original-Recipient: rfc822;{orcpt}\r\n");
|
|
}
|
|
let _ = write!(dsn, "Final-Recipient: rfc822;{}\r\n", self.address);
|
|
}
|
|
}
|
|
|
|
impl Domain {
|
|
fn write_dsn_will_retry_until(&self, dsn: &mut String) {
|
|
let now = now();
|
|
if self.expires > now {
|
|
dsn.push_str("Will-Retry-Until: ");
|
|
dsn.push_str(&DateTime::from_timestamp(self.expires as i64).to_rfc822());
|
|
dsn.push_str("\r\n");
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<T, E> Status<T, E> {
|
|
pub fn into_permanent(self) -> Self {
|
|
match self {
|
|
Status::TemporaryFailure(v) => Status::PermanentFailure(v),
|
|
v => v,
|
|
}
|
|
}
|
|
|
|
pub fn into_temporary(self) -> Self {
|
|
match self {
|
|
Status::PermanentFailure(err) => Status::TemporaryFailure(err),
|
|
other => other,
|
|
}
|
|
}
|
|
|
|
pub fn is_permanent(&self) -> bool {
|
|
matches!(self, Status::PermanentFailure(_))
|
|
}
|
|
|
|
fn write_dsn_action(&self, dsn: &mut String) {
|
|
dsn.push_str("Action: ");
|
|
dsn.push_str(match self {
|
|
Status::Completed(_) => "delivered",
|
|
Status::PermanentFailure(_) => "failed",
|
|
Status::TemporaryFailure(_) | Status::Scheduled => "delayed",
|
|
});
|
|
dsn.push_str("\r\n");
|
|
}
|
|
}
|
|
|
|
impl Status<HostResponse<String>, HostResponse<ErrorDetails>> {
|
|
fn write_dsn(&self, dsn: &mut String) {
|
|
self.write_dsn_action(dsn);
|
|
self.write_dsn_status(dsn);
|
|
self.write_dsn_diagnostic(dsn);
|
|
self.write_dsn_remote_mta(dsn);
|
|
}
|
|
|
|
fn write_dsn_status(&self, dsn: &mut String) {
|
|
dsn.push_str("Status: ");
|
|
if let Status::Completed(HostResponse { response, .. })
|
|
| Status::PermanentFailure(HostResponse { response, .. })
|
|
| Status::TemporaryFailure(HostResponse { response, .. }) = self
|
|
{
|
|
response.write_dsn_status(dsn);
|
|
}
|
|
dsn.push_str("\r\n");
|
|
}
|
|
|
|
fn write_dsn_remote_mta(&self, dsn: &mut String) {
|
|
dsn.push_str("Remote-MTA: dns;");
|
|
if let Status::Completed(HostResponse { hostname, .. })
|
|
| Status::PermanentFailure(HostResponse {
|
|
hostname: ErrorDetails {
|
|
entity: hostname, ..
|
|
},
|
|
..
|
|
})
|
|
| Status::TemporaryFailure(HostResponse {
|
|
hostname: ErrorDetails {
|
|
entity: hostname, ..
|
|
},
|
|
..
|
|
}) = self
|
|
{
|
|
dsn.push_str(hostname);
|
|
}
|
|
dsn.push_str("\r\n");
|
|
}
|
|
|
|
fn write_dsn_diagnostic(&self, dsn: &mut String) {
|
|
if let Status::PermanentFailure(details) | Status::TemporaryFailure(details) = self {
|
|
details.response.write_dsn_diagnostic(dsn);
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Status<(), Error> {
|
|
fn write_dsn(&self, dsn: &mut String) {
|
|
self.write_dsn_action(dsn);
|
|
self.write_dsn_status(dsn);
|
|
self.write_dsn_diagnostic(dsn);
|
|
self.write_dsn_remote_mta(dsn);
|
|
}
|
|
|
|
fn write_dsn_status(&self, dsn: &mut String) {
|
|
if let Status::PermanentFailure(err) | Status::TemporaryFailure(err) = self {
|
|
dsn.push_str("Status: ");
|
|
if let Error::UnexpectedResponse(response) = err {
|
|
response.response.write_dsn_status(dsn);
|
|
} else {
|
|
dsn.push_str(if matches!(self, Status::PermanentFailure(_)) {
|
|
"5.0.0"
|
|
} else {
|
|
"4.0.0"
|
|
});
|
|
}
|
|
dsn.push_str("\r\n");
|
|
}
|
|
}
|
|
|
|
fn write_dsn_remote_mta(&self, dsn: &mut String) {
|
|
if let Status::PermanentFailure(err) | Status::TemporaryFailure(err) = self {
|
|
match err {
|
|
Error::UnexpectedResponse(HostResponse {
|
|
hostname: details, ..
|
|
})
|
|
| Error::ConnectionError(details)
|
|
| Error::TlsError(details)
|
|
| Error::DaneError(details) => {
|
|
dsn.push_str("Remote-MTA: dns;");
|
|
dsn.push_str(&details.entity);
|
|
dsn.push_str("\r\n");
|
|
}
|
|
_ => (),
|
|
}
|
|
}
|
|
}
|
|
|
|
fn write_dsn_diagnostic(&self, dsn: &mut String) {
|
|
if let Status::PermanentFailure(Error::UnexpectedResponse(response))
|
|
| Status::TemporaryFailure(Error::UnexpectedResponse(response)) = self
|
|
{
|
|
response.response.write_dsn_diagnostic(dsn);
|
|
}
|
|
}
|
|
}
|
|
|
|
impl WriteDsn for Response<String> {
|
|
fn write_dsn_status(&self, dsn: &mut String) {
|
|
if self.esc[0] > 0 {
|
|
let _ = write!(dsn, "{}.{}.{}", self.esc[0], self.esc[1], self.esc[2]);
|
|
} else {
|
|
let _ = write!(
|
|
dsn,
|
|
"{}.{}.{}",
|
|
self.code / 100,
|
|
(self.code / 10) % 10,
|
|
self.code % 10
|
|
);
|
|
}
|
|
}
|
|
|
|
fn write_dsn_diagnostic(&self, dsn: &mut String) {
|
|
let _ = write!(dsn, "Diagnostic-Code: smtp;{} ", self.code);
|
|
self.write_response(dsn);
|
|
dsn.push_str("\r\n");
|
|
}
|
|
|
|
fn write_response(&self, dsn: &mut String) {
|
|
for ch in self.message.chars() {
|
|
if ch != '\n' && ch != '\r' {
|
|
dsn.push(ch);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
trait WriteDsn {
|
|
fn write_dsn_status(&self, dsn: &mut String);
|
|
fn write_dsn_diagnostic(&self, dsn: &mut String);
|
|
fn write_response(&self, dsn: &mut String);
|
|
}
|