Import upstream v0.16.22, stripped
Upstream commit: 474dd0229cb20cf513036619781ed97bd8073c3f Enterprise-only files removed or emptied: 63 Enterprise-only snippets removed: 117 in 50 files Dangling module declarations removed: 5 Cargo edits turning enterprise off: 14 Verification: clean Enterprise feature gates left for rebuilt features: 19 in 18 files Produced by tools/fork/strip.py. The full report is in docs/fork/strip-reports/ on main.
This commit is contained in:
@@ -0,0 +1,669 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::spool::SmtpSpool;
|
||||
use super::{
|
||||
Error, ErrorDetails, HostResponse, Message, MessageSource, QueueEnvelope, RCPT_DSN_SENT,
|
||||
Recipient, Status,
|
||||
};
|
||||
use crate::inbound::dkim::DkimSign;
|
||||
use crate::queue::spool::QueueParams;
|
||||
use crate::queue::{MessageWrapper, UnexpectedResponse};
|
||||
use common::Server;
|
||||
use mail_builder::MessageBuilder;
|
||||
use mail_builder::headers::HeaderType;
|
||||
use mail_builder::headers::content_type::ContentType;
|
||||
use mail_builder::mime::{BodyPart, MimePart, make_boundary};
|
||||
use mail_parser::DateTime;
|
||||
use smtp_proto::{
|
||||
RCPT_NOTIFY_DELAY, RCPT_NOTIFY_FAILURE, RCPT_NOTIFY_NEVER, RCPT_NOTIFY_SUCCESS, Response,
|
||||
};
|
||||
use std::fmt::Write;
|
||||
use std::future::Future;
|
||||
use store::write::now;
|
||||
|
||||
pub trait SendDsn: Sync + Send {
|
||||
fn send_dsn(&self, message: &mut MessageWrapper) -> impl Future<Output = ()> + Send;
|
||||
fn log_dsn(&self, message: &MessageWrapper) -> impl Future<Output = ()> + Send;
|
||||
}
|
||||
|
||||
impl SendDsn for Server {
|
||||
async fn send_dsn(&self, message: &mut MessageWrapper) {
|
||||
// Send DSN events
|
||||
self.log_dsn(message).await;
|
||||
|
||||
if !message.message.return_path.is_empty() {
|
||||
// Build DSN
|
||||
if let Some(dsn) = message.build_dsn(self).await {
|
||||
let mut dsn_message = self.new_message("", MessageSource::Dsn, message.span_id);
|
||||
dsn_message
|
||||
.expand_and_add_recipient(message.message.return_path.as_ref(), self)
|
||||
.await;
|
||||
|
||||
// Queue DSN
|
||||
let dkim_signers = self
|
||||
.eval_signers(
|
||||
&self.core.smtp.queue.dsn.sign,
|
||||
&message.message,
|
||||
message.span_id,
|
||||
)
|
||||
.await;
|
||||
dsn_message
|
||||
.queue(
|
||||
QueueParams::new(&dsn, message.span_id, self)
|
||||
.with_dkim_signers(dkim_signers),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
} else {
|
||||
// Handle double bounce
|
||||
message.handle_double_bounce();
|
||||
}
|
||||
|
||||
// Update next DSN notify times
|
||||
message.update_next_dsn(self).await;
|
||||
}
|
||||
|
||||
async fn log_dsn(&self, message: &MessageWrapper) {
|
||||
let now = now();
|
||||
|
||||
for rcpt in &message.message.recipients {
|
||||
if rcpt.has_flag(RCPT_DSN_SENT) {
|
||||
continue;
|
||||
}
|
||||
|
||||
match &rcpt.status {
|
||||
Status::Completed(response) => {
|
||||
trc::event!(
|
||||
Delivery(trc::DeliveryEvent::DsnSuccess),
|
||||
SpanId = message.span_id,
|
||||
To = rcpt.address.clone(),
|
||||
Hostname = response.hostname.clone(),
|
||||
Code = response.response.code,
|
||||
Details = response.response.message.to_string(),
|
||||
);
|
||||
}
|
||||
Status::TemporaryFailure(response) if rcpt.notify.due <= now => {
|
||||
trc::event!(
|
||||
Delivery(trc::DeliveryEvent::DsnTempFail),
|
||||
SpanId = message.span_id,
|
||||
To = rcpt.address.clone(),
|
||||
Hostname = response.entity.clone(),
|
||||
Details = response.details.to_string(),
|
||||
NextRetry = trc::Value::Timestamp(rcpt.retry.due),
|
||||
Expires = rcpt
|
||||
.expiration_time(message.message.created)
|
||||
.map(trc::Value::Timestamp),
|
||||
Total = rcpt.retry.inner,
|
||||
);
|
||||
}
|
||||
Status::PermanentFailure(response) => {
|
||||
trc::event!(
|
||||
Delivery(trc::DeliveryEvent::DsnPermFail),
|
||||
SpanId = message.span_id,
|
||||
To = rcpt.address.clone(),
|
||||
Hostname = response.entity.clone(),
|
||||
Details = response.details.to_string(),
|
||||
Total = rcpt.retry.inner,
|
||||
);
|
||||
}
|
||||
Status::Scheduled if rcpt.notify.due <= now => {
|
||||
trc::event!(
|
||||
Delivery(trc::DeliveryEvent::DsnTempFail),
|
||||
SpanId = message.span_id,
|
||||
To = rcpt.address.clone(),
|
||||
Details = "Concurrency limited",
|
||||
NextRetry = trc::Value::Timestamp(rcpt.retry.due),
|
||||
Expires = rcpt
|
||||
.expiration_time(message.message.created)
|
||||
.map(trc::Value::Timestamp),
|
||||
Total = rcpt.retry.inner,
|
||||
);
|
||||
}
|
||||
_ => continue,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const MAX_HEADER_SIZE: usize = 4096;
|
||||
|
||||
impl MessageWrapper {
|
||||
pub async fn build_dsn(&mut self, server: &Server) -> Option<Vec<u8>> {
|
||||
let config = &server.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.message.recipients {
|
||||
if rcpt.has_flag(RCPT_DSN_SENT | RCPT_NOTIFY_NEVER) {
|
||||
continue;
|
||||
}
|
||||
match &rcpt.status {
|
||||
Status::Completed(response) => {
|
||||
rcpt.flags |= RCPT_DSN_SENT;
|
||||
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 rcpt.notify.due <= now && rcpt.has_flag(RCPT_NOTIFY_DELAY) =>
|
||||
{
|
||||
rcpt.write_dsn(&mut dsn);
|
||||
rcpt.status.write_dsn(&mut dsn);
|
||||
rcpt.write_dsn_will_retry_until(self.message.created, &mut dsn);
|
||||
response.write_dsn_text(&rcpt.address, &mut txt_delay);
|
||||
}
|
||||
Status::PermanentFailure(response) => {
|
||||
rcpt.flags |= RCPT_DSN_SENT;
|
||||
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 if rcpt.notify.due <= now && rcpt.has_flag(RCPT_NOTIFY_DELAY) => {
|
||||
// This case should not happen under normal circumstances
|
||||
rcpt.write_dsn(&mut dsn);
|
||||
rcpt.status.write_dsn(&mut dsn);
|
||||
rcpt.write_dsn_will_retry_until(self.message.created, &mut dsn);
|
||||
ErrorDetails {
|
||||
entity: "localhost".into(),
|
||||
details: Error::ConcurrencyLimited,
|
||||
}
|
||||
.write_dsn_text(&rcpt.address, &mut txt_delay);
|
||||
}
|
||||
_ => continue,
|
||||
}
|
||||
|
||||
dsn.push_str("\r\n");
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
// Obtain hostname and sender addresses
|
||||
let from_name = server
|
||||
.eval_if(&config.dsn.name, &self.message, self.span_id)
|
||||
.await
|
||||
.unwrap_or_else(|| String::from("Mail Delivery Subsystem"));
|
||||
let from_addr = server
|
||||
.eval_if(&config.dsn.address, &self.message, self.span_id)
|
||||
.await
|
||||
.unwrap_or_else(|| String::from("MAILER-DAEMON@localhost"));
|
||||
let reporting_mta = server
|
||||
.eval_if(
|
||||
&server.core.smtp.report.submitter,
|
||||
&self.message,
|
||||
self.span_id,
|
||||
)
|
||||
.await
|
||||
.unwrap_or_else(|| String::from("localhost"));
|
||||
|
||||
// Prepare DSN
|
||||
let mut dsn_header = String::with_capacity(dsn.len() + 128);
|
||||
self.message
|
||||
.write_dsn_headers(&mut dsn_header, &reporting_mta);
|
||||
let dsn = dsn_header + dsn.as_str();
|
||||
|
||||
// Fetch up to MAX_HEADER_SIZE bytes of message headers
|
||||
let headers = match server
|
||||
.blob_store()
|
||||
.get_blob(self.message.blob_hash.as_slice(), 0..MAX_HEADER_SIZE)
|
||||
.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 < MAX_HEADER_SIZE {
|
||||
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.message.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.message.return_path.as_ref().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()
|
||||
}
|
||||
|
||||
pub async fn update_next_dsn(&mut self, server: &Server) {
|
||||
let now = now();
|
||||
let mut notify_changes = Vec::new();
|
||||
for (rcpt_idx, rcpt) in self.message.recipients.iter().enumerate() {
|
||||
if matches!(
|
||||
&rcpt.status,
|
||||
Status::TemporaryFailure(_) | Status::Scheduled
|
||||
) && rcpt.notify.due <= now
|
||||
{
|
||||
let envelope = QueueEnvelope::new(&self.message, rcpt);
|
||||
|
||||
let queue_id = server
|
||||
.eval_if::<String, _>(&server.core.smtp.queue.queue, &envelope, self.span_id)
|
||||
.await
|
||||
.unwrap_or_else(|| "default".to_string());
|
||||
let queue = server.get_queue_or_default(&queue_id, self.span_id);
|
||||
|
||||
if let Some(next_notify) =
|
||||
queue.notify.get((rcpt.notify.inner + 1) as usize).copied()
|
||||
{
|
||||
notify_changes.push((rcpt_idx, 1, now + next_notify));
|
||||
} else {
|
||||
notify_changes.push((rcpt_idx, 0, u64::MAX));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (rcpt_idx, inner, due) in notify_changes {
|
||||
let rcpt = &mut self.message.recipients[rcpt_idx];
|
||||
rcpt.notify.inner += inner;
|
||||
rcpt.notify.due = due;
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_double_bounce(&mut self) {
|
||||
let mut is_double_bounce = Vec::with_capacity(0);
|
||||
let now = now();
|
||||
|
||||
for rcpt in &mut self.message.recipients {
|
||||
if !rcpt.has_flag(RCPT_DSN_SENT | RCPT_NOTIFY_NEVER)
|
||||
&& let Status::PermanentFailure(err) = &rcpt.status
|
||||
{
|
||||
rcpt.flags |= RCPT_DSN_SENT;
|
||||
let mut dsn = String::new();
|
||||
err.write_dsn_text(&rcpt.address, &mut dsn);
|
||||
is_double_bounce.push(dsn);
|
||||
}
|
||||
|
||||
if rcpt.notify.due <= now {
|
||||
rcpt.notify.due = rcpt
|
||||
.expiration_time(self.message.created)
|
||||
.map(|d| d + 10)
|
||||
.unwrap_or(u64::MAX);
|
||||
}
|
||||
}
|
||||
|
||||
if !is_double_bounce.is_empty() {
|
||||
trc::event!(
|
||||
Delivery(trc::DeliveryEvent::DoubleBounce),
|
||||
SpanId = self.span_id,
|
||||
To = is_double_bounce
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl HostResponse<Box<str>> {
|
||||
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 UnexpectedResponse {
|
||||
fn write_dsn_text(&self, host: &str, addr: &str, dsn: &mut String) {
|
||||
let _ = write!(dsn, "<{addr}> (host '{host}' rejected ");
|
||||
|
||||
if !self.command.is_empty() {
|
||||
let _ = write!(dsn, "command '{}'", self.command);
|
||||
} 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 ErrorDetails {
|
||||
fn write_dsn_text(&self, addr: &str, dsn: &mut String) {
|
||||
let entity = self.entity.as_ref();
|
||||
match &self.details {
|
||||
Error::UnexpectedResponse(response) => {
|
||||
response.write_dsn_text(entity, addr, dsn);
|
||||
}
|
||||
Error::DnsError(err) => {
|
||||
let _ = write!(dsn, "<{addr}> (failed to lookup '{entity}': {err})\r\n",);
|
||||
}
|
||||
Error::ConnectionError(details) => {
|
||||
let _ = write!(
|
||||
dsn,
|
||||
"<{addr}> (connection to '{entity}' failed: {details})\r\n",
|
||||
);
|
||||
}
|
||||
Error::TlsError(details) => {
|
||||
let _ = write!(dsn, "<{addr}> (TLS error from '{entity}': {details})\r\n",);
|
||||
}
|
||||
Error::DaneError(details) => {
|
||||
let _ = write!(
|
||||
dsn,
|
||||
"<{addr}> (DANE failed to authenticate '{entity}': {details})\r\n",
|
||||
);
|
||||
}
|
||||
Error::MtaStsError(details) => {
|
||||
let _ = write!(
|
||||
dsn,
|
||||
"<{addr}> (MTA-STS failed to authenticate '{entity}': {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);
|
||||
}
|
||||
|
||||
fn write_dsn_will_retry_until(&self, created: u64, dsn: &mut String) {
|
||||
if let Some(expires) = self.expiration_time(created)
|
||||
&& expires > now()
|
||||
{
|
||||
dsn.push_str("Will-Retry-Until: ");
|
||||
dsn.push_str(&DateTime::from_timestamp(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<Box<str>>, 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: ");
|
||||
match self {
|
||||
Status::Completed(response) => {
|
||||
response.response.write_dsn_status(dsn);
|
||||
}
|
||||
Status::TemporaryFailure(err) | Status::PermanentFailure(err) => {
|
||||
if let Error::UnexpectedResponse(response) = &err.details {
|
||||
response.response.write_dsn_status(dsn);
|
||||
} else {
|
||||
dsn.push_str(if matches!(self, Status::PermanentFailure(_)) {
|
||||
"5.0.0"
|
||||
} else {
|
||||
"4.0.0"
|
||||
});
|
||||
}
|
||||
}
|
||||
Status::Scheduled => {
|
||||
dsn.push_str("4.0.0");
|
||||
}
|
||||
}
|
||||
dsn.push_str("\r\n");
|
||||
}
|
||||
|
||||
fn write_dsn_remote_mta(&self, dsn: &mut String) {
|
||||
match self {
|
||||
Status::Completed(response) => {
|
||||
dsn.push_str("Remote-MTA: dns;");
|
||||
dsn.push_str(&response.hostname);
|
||||
dsn.push_str("\r\n");
|
||||
}
|
||||
Status::TemporaryFailure(err) | Status::PermanentFailure(err) => match &err.details {
|
||||
Error::UnexpectedResponse(_)
|
||||
| Error::ConnectionError(_)
|
||||
| Error::TlsError(_)
|
||||
| Error::DaneError(_) => {
|
||||
dsn.push_str("Remote-MTA: dns;");
|
||||
dsn.push_str(&err.entity);
|
||||
dsn.push_str("\r\n");
|
||||
}
|
||||
_ => (),
|
||||
},
|
||||
Status::Scheduled => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn write_dsn_diagnostic(&self, dsn: &mut String) {
|
||||
if let Status::PermanentFailure(err) | Status::TemporaryFailure(err) = self
|
||||
&& let Error::UnexpectedResponse(response) = &err.details
|
||||
{
|
||||
response.response.write_dsn_diagnostic(dsn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WriteDsn for Response<Box<str>> {
|
||||
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);
|
||||
}
|
||||
@@ -0,0 +1,501 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::{Message, QueueId, Status, spool::SmtpSpool};
|
||||
use crate::queue::{
|
||||
Recipient,
|
||||
spool::{INFINITE_LOCK, LOCK_EXPIRY, QUEUE_REFRESH},
|
||||
};
|
||||
use ahash::AHashMap;
|
||||
use common::{
|
||||
BuildServer, Inner,
|
||||
config::smtp::queue::{QueueExpiry, QueueName},
|
||||
ipc::{QueueEvent, QueueEventStatus},
|
||||
};
|
||||
use rand::{RngExt, seq::SliceRandom};
|
||||
use std::{
|
||||
collections::hash_map::Entry,
|
||||
sync::{Arc, atomic::Ordering},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use store::write::now;
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
pub struct Queue {
|
||||
pub core: Arc<Inner>,
|
||||
pub locked: AHashMap<(QueueId, QueueName), LockedMessage>,
|
||||
pub locked_revision: u64,
|
||||
pub stats: AHashMap<QueueName, QueueStats>,
|
||||
pub next_refresh: Instant,
|
||||
pub rx: mpsc::Receiver<QueueEvent>,
|
||||
pub is_paused: bool,
|
||||
pub scan_from: u64,
|
||||
pub scan_ceiling: u64,
|
||||
pub has_pending_work: bool,
|
||||
pub pending_refresh: bool,
|
||||
pub urgent_refresh: bool,
|
||||
pub last_scan: Instant,
|
||||
pub last_full_scan: Instant,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct QueueStats {
|
||||
pub in_flight: usize,
|
||||
pub max_in_flight: usize,
|
||||
pub budget: usize,
|
||||
pub last_warning: Instant,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct LockedMessage {
|
||||
pub expires: u64,
|
||||
pub revision: u64,
|
||||
pub due: u64,
|
||||
}
|
||||
|
||||
impl SpawnQueue for mpsc::Receiver<QueueEvent> {
|
||||
fn spawn(self, core: Arc<Inner>) {
|
||||
tokio::spawn(async move {
|
||||
Queue::new(core, self).start().await;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const BACK_PRESSURE_WARN_INTERVAL: Duration = Duration::from_secs(60);
|
||||
const MIN_SCAN_INTERVAL: Duration = Duration::from_millis(100);
|
||||
const FULL_SCAN_INTERVAL: Duration = Duration::from_secs(QUEUE_REFRESH / 2);
|
||||
|
||||
impl Queue {
|
||||
pub fn new(core: Arc<Inner>, rx: mpsc::Receiver<QueueEvent>) -> Self {
|
||||
let now = Instant::now();
|
||||
|
||||
Queue {
|
||||
core,
|
||||
locked: AHashMap::with_capacity(128),
|
||||
locked_revision: 0,
|
||||
stats: AHashMap::new(),
|
||||
next_refresh: now + Duration::from_secs(1),
|
||||
is_paused: false,
|
||||
rx,
|
||||
scan_from: 0,
|
||||
scan_ceiling: u64::MAX,
|
||||
has_pending_work: false,
|
||||
pending_refresh: false,
|
||||
urgent_refresh: false,
|
||||
last_scan: now.checked_sub(MIN_SCAN_INTERVAL).unwrap_or(now),
|
||||
last_full_scan: now,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn start(&mut self) {
|
||||
trc::event!(Queue(trc::QueueEvent::Started));
|
||||
|
||||
loop {
|
||||
let mut refresh_queue;
|
||||
|
||||
match tokio::time::timeout(
|
||||
self.next_refresh.duration_since(Instant::now()),
|
||||
self.rx.recv(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(Some(event)) => {
|
||||
refresh_queue = self.handle_event(event).await;
|
||||
|
||||
while let Ok(event) = self.rx.try_recv() {
|
||||
refresh_queue = self.handle_event(event).await || refresh_queue;
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
refresh_queue = true;
|
||||
self.urgent_refresh = true;
|
||||
}
|
||||
Ok(None) => {
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
if self.is_paused {
|
||||
self.next_refresh = Instant::now() + Duration::from_secs(86400);
|
||||
continue;
|
||||
}
|
||||
|
||||
self.pending_refresh |= refresh_queue;
|
||||
if !self.pending_refresh && self.next_refresh > Instant::now() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Coalesce bursts of worker notifications into a single scan
|
||||
let scan_at = self.last_scan + MIN_SCAN_INTERVAL;
|
||||
if !self.urgent_refresh && scan_at > Instant::now() {
|
||||
self.next_refresh = scan_at;
|
||||
continue;
|
||||
}
|
||||
|
||||
if self.scan_from != 0 && self.last_full_scan.elapsed() >= FULL_SCAN_INTERVAL {
|
||||
self.scan_from = 0;
|
||||
}
|
||||
if self.scan_from == 0 {
|
||||
self.last_full_scan = Instant::now();
|
||||
}
|
||||
let scan_floor = self.scan_from;
|
||||
self.pending_refresh = false;
|
||||
self.urgent_refresh = false;
|
||||
|
||||
// Process queue events
|
||||
let server = self.core.build_server();
|
||||
let mut queue_events = server.next_event(self).await;
|
||||
self.last_scan = Instant::now();
|
||||
|
||||
if queue_events.messages.len() > 3 {
|
||||
queue_events.messages.shuffle(&mut rand::rng());
|
||||
}
|
||||
|
||||
// A truncated scan left events behind
|
||||
let now = now();
|
||||
self.has_pending_work = self.scan_ceiling != u64::MAX;
|
||||
|
||||
for queue_event in &queue_events.messages {
|
||||
// A message may hold more than one event key, dispatch it only once
|
||||
if self
|
||||
.locked
|
||||
.get(&(queue_event.queue_id, queue_event.queue_name))
|
||||
.is_some_and(|locked| locked.expires > now)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Fetch queue stats
|
||||
let stats = match self.stats.get_mut(&queue_event.queue_name) {
|
||||
Some(stats) => stats,
|
||||
None => {
|
||||
let queue_config =
|
||||
server.get_virtual_queue_or_default(&queue_event.queue_name);
|
||||
self.stats.insert(
|
||||
queue_event.queue_name,
|
||||
QueueStats::new(queue_config.threads),
|
||||
);
|
||||
self.stats.get_mut(&queue_event.queue_name).unwrap()
|
||||
}
|
||||
};
|
||||
|
||||
// Enforce concurrency limits
|
||||
if stats.has_capacity() {
|
||||
// Deliver message
|
||||
stats.in_flight += 1;
|
||||
self.locked.insert(
|
||||
(queue_event.queue_id, queue_event.queue_name),
|
||||
LockedMessage {
|
||||
expires: now + INFINITE_LOCK,
|
||||
revision: self.locked_revision,
|
||||
due: queue_event.due,
|
||||
},
|
||||
);
|
||||
queue_event.try_deliver(server.clone());
|
||||
} else {
|
||||
if stats.last_warning.elapsed() >= BACK_PRESSURE_WARN_INTERVAL {
|
||||
stats.last_warning = Instant::now();
|
||||
trc::event!(
|
||||
Queue(trc::QueueEvent::BackPressure),
|
||||
Reason = "Processing capacity for this queue exceeded.",
|
||||
QueueName = queue_event.queue_name.to_string(),
|
||||
Limit = stats.max_in_flight,
|
||||
);
|
||||
}
|
||||
self.has_pending_work = true;
|
||||
if queue_event.due < self.scan_from {
|
||||
self.scan_from = queue_event.due;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove expired locks, revisiting any event they were holding back
|
||||
let scan_ceiling = self.scan_ceiling;
|
||||
let mut dropped_due = u64::MAX;
|
||||
self.locked.retain(|_, locked| {
|
||||
let keep = locked.expires > now
|
||||
&& (locked.revision == self.locked_revision
|
||||
|| locked.due < scan_floor
|
||||
|| locked.due >= scan_ceiling);
|
||||
if !keep && locked.due < dropped_due {
|
||||
dropped_due = locked.due;
|
||||
}
|
||||
keep
|
||||
});
|
||||
|
||||
// Do not wait for the next scheduled event while there is work left over
|
||||
let mut next_refresh = queue_events.next_refresh.saturating_sub(now);
|
||||
if self.has_pending_work {
|
||||
next_refresh = std::cmp::min(next_refresh, FULL_SCAN_INTERVAL.as_secs());
|
||||
}
|
||||
let mut next_refresh = Instant::now() + Duration::from_secs(next_refresh);
|
||||
|
||||
// A released lock uncovered an event below the floor that no scan can see
|
||||
if dropped_due < self.scan_from {
|
||||
self.scan_from = dropped_due;
|
||||
self.has_pending_work = true;
|
||||
self.pending_refresh = true;
|
||||
|
||||
let scan_at = self.last_scan + MIN_SCAN_INTERVAL;
|
||||
if scan_at < next_refresh {
|
||||
next_refresh = scan_at;
|
||||
}
|
||||
}
|
||||
|
||||
self.next_refresh = next_refresh;
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_event(&mut self, event: QueueEvent) -> bool {
|
||||
match event {
|
||||
QueueEvent::WorkerDone {
|
||||
queue_id,
|
||||
queue_name,
|
||||
status,
|
||||
} => {
|
||||
let has_capacity = match self.stats.get_mut(&queue_name) {
|
||||
Some(queue_stats) => {
|
||||
queue_stats.in_flight = queue_stats.in_flight.saturating_sub(1);
|
||||
queue_stats.has_capacity()
|
||||
}
|
||||
None => true,
|
||||
};
|
||||
|
||||
match status {
|
||||
QueueEventStatus::Completed => {
|
||||
self.core.ipc.task_tx.notify_one();
|
||||
self.locked.remove(&(queue_id, queue_name));
|
||||
!self.locked.is_empty() || !has_capacity || self.has_pending_work
|
||||
}
|
||||
QueueEventStatus::Locked => {
|
||||
let expires = LOCK_EXPIRY + rand::rng().random_range(5..10);
|
||||
let due_in = Instant::now() + Duration::from_secs(expires);
|
||||
if due_in < self.next_refresh {
|
||||
self.next_refresh = due_in;
|
||||
}
|
||||
|
||||
// The event was not delivered, so it has to be visited again
|
||||
// once the remote lock expires.
|
||||
let expires = now() + expires;
|
||||
let due = match self.locked.entry((queue_id, queue_name)) {
|
||||
Entry::Occupied(mut entry) => {
|
||||
let locked = entry.get_mut();
|
||||
locked.expires = expires;
|
||||
locked.revision = self.locked_revision;
|
||||
locked.due
|
||||
}
|
||||
Entry::Vacant(entry) => {
|
||||
entry.insert(LockedMessage {
|
||||
expires,
|
||||
revision: self.locked_revision,
|
||||
due: 0,
|
||||
});
|
||||
0
|
||||
}
|
||||
};
|
||||
if due < self.scan_from {
|
||||
self.scan_from = due;
|
||||
}
|
||||
self.locked.len() > 1 || !has_capacity || self.has_pending_work
|
||||
}
|
||||
QueueEventStatus::Deferred => {
|
||||
self.locked.remove(&(queue_id, queue_name));
|
||||
self.scan_from = 0;
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
QueueEvent::Refresh => {
|
||||
self.scan_from = 0;
|
||||
self.urgent_refresh = true;
|
||||
true
|
||||
}
|
||||
QueueEvent::Paused(paused) => {
|
||||
self.core
|
||||
.data
|
||||
.queue_status
|
||||
.store(!paused, Ordering::Relaxed);
|
||||
self.is_paused = paused;
|
||||
self.scan_from = 0;
|
||||
self.urgent_refresh = !paused;
|
||||
!paused
|
||||
}
|
||||
QueueEvent::ReloadSettings => {
|
||||
let server = self.core.build_server();
|
||||
let virtual_queues = &server.core.smtp.queue.virtual_queues;
|
||||
for (name, settings) in virtual_queues {
|
||||
if let Some(stats) = self.stats.get_mut(name) {
|
||||
stats.max_in_flight = settings.threads;
|
||||
} else {
|
||||
self.stats.insert(*name, QueueStats::new(settings.threads));
|
||||
}
|
||||
}
|
||||
self.stats
|
||||
.retain(|name, stats| stats.in_flight > 0 || virtual_queues.contains_key(name));
|
||||
self.scan_from = 0;
|
||||
false
|
||||
}
|
||||
QueueEvent::Stop => {
|
||||
self.rx.close();
|
||||
self.is_paused = true;
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Message {
|
||||
pub fn next_event(&self, queue: Option<QueueName>) -> Option<u64> {
|
||||
let mut next_event = None;
|
||||
|
||||
for rcpt in &self.recipients {
|
||||
if matches!(rcpt.status, Status::Scheduled | Status::TemporaryFailure(_))
|
||||
&& queue.is_none_or(|q| rcpt.queue == q)
|
||||
{
|
||||
let mut earlier_event = std::cmp::min(rcpt.retry.due, rcpt.notify.due);
|
||||
|
||||
if let Some(expires) = rcpt.expiration_time(self.created) {
|
||||
earlier_event = std::cmp::min(earlier_event, expires);
|
||||
}
|
||||
|
||||
if let Some(next_event) = &mut next_event {
|
||||
if earlier_event < *next_event {
|
||||
*next_event = earlier_event;
|
||||
}
|
||||
} else {
|
||||
next_event = Some(earlier_event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
next_event
|
||||
}
|
||||
|
||||
pub fn next_delivery_event(&self, queue: Option<QueueName>) -> Option<u64> {
|
||||
let mut next_delivery = None;
|
||||
|
||||
for rcpt in self.recipients.iter().filter(|rcpt| {
|
||||
matches!(rcpt.status, Status::Scheduled | Status::TemporaryFailure(_))
|
||||
&& queue.is_none_or(|q| rcpt.queue == q)
|
||||
}) {
|
||||
if let Some(next_delivery) = &mut next_delivery {
|
||||
if rcpt.retry.due < *next_delivery {
|
||||
*next_delivery = rcpt.retry.due;
|
||||
}
|
||||
} else {
|
||||
next_delivery = Some(rcpt.retry.due);
|
||||
}
|
||||
}
|
||||
|
||||
next_delivery
|
||||
}
|
||||
|
||||
pub fn next_dsn(&self, queue: Option<QueueName>) -> Option<u64> {
|
||||
let mut next_dsn = None;
|
||||
|
||||
for rcpt in self.recipients.iter().filter(|rcpt| {
|
||||
matches!(rcpt.status, Status::Scheduled | Status::TemporaryFailure(_))
|
||||
&& queue.is_none_or(|q| rcpt.queue == q)
|
||||
}) {
|
||||
if let Some(next_dsn) = &mut next_dsn {
|
||||
if rcpt.notify.due < *next_dsn {
|
||||
*next_dsn = rcpt.notify.due;
|
||||
}
|
||||
} else {
|
||||
next_dsn = Some(rcpt.notify.due);
|
||||
}
|
||||
}
|
||||
|
||||
next_dsn
|
||||
}
|
||||
|
||||
pub fn expires(&self, queue: Option<QueueName>) -> Option<u64> {
|
||||
let mut expires = None;
|
||||
|
||||
for rcpt in self.recipients.iter().filter(|d| {
|
||||
matches!(d.status, Status::Scheduled | Status::TemporaryFailure(_))
|
||||
&& queue.is_none_or(|q| d.queue == q)
|
||||
}) {
|
||||
if let Some(rcpt_expires) = rcpt.expiration_time(self.created) {
|
||||
if let Some(expires) = &mut expires {
|
||||
if rcpt_expires > *expires {
|
||||
*expires = rcpt_expires;
|
||||
}
|
||||
} else {
|
||||
expires = Some(rcpt_expires)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
expires
|
||||
}
|
||||
|
||||
pub fn next_events(&self) -> AHashMap<QueueName, u64> {
|
||||
let mut next_events = AHashMap::new();
|
||||
|
||||
for rcpt in &self.recipients {
|
||||
if matches!(rcpt.status, Status::Scheduled | Status::TemporaryFailure(_)) {
|
||||
let mut earlier_event = std::cmp::min(rcpt.retry.due, rcpt.notify.due);
|
||||
|
||||
if let Some(expires) = rcpt.expiration_time(self.created) {
|
||||
earlier_event = std::cmp::min(earlier_event, expires);
|
||||
}
|
||||
|
||||
match next_events.entry(rcpt.queue) {
|
||||
Entry::Occupied(mut entry) => {
|
||||
let entry = entry.get_mut();
|
||||
if earlier_event < *entry {
|
||||
*entry = earlier_event;
|
||||
}
|
||||
}
|
||||
Entry::Vacant(entry) => {
|
||||
entry.insert(earlier_event);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
next_events
|
||||
}
|
||||
}
|
||||
|
||||
impl Recipient {
|
||||
pub fn expiration_time(&self, created: u64) -> Option<u64> {
|
||||
match self.expires {
|
||||
QueueExpiry::Ttl(time) => Some(created + time),
|
||||
QueueExpiry::Attempts(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_expired(&self, created: u64, now: u64) -> bool {
|
||||
match self.expires {
|
||||
QueueExpiry::Ttl(time) => created + time <= now,
|
||||
QueueExpiry::Attempts(count) => self.retry.inner >= count,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait SpawnQueue {
|
||||
fn spawn(self, core: Arc<Inner>);
|
||||
}
|
||||
|
||||
impl QueueStats {
|
||||
pub(crate) fn new(max_in_flight: usize) -> Self {
|
||||
QueueStats {
|
||||
in_flight: 0,
|
||||
max_in_flight,
|
||||
budget: 0,
|
||||
last_warning: Instant::now()
|
||||
.checked_sub(BACK_PRESSURE_WARN_INTERVAL)
|
||||
.unwrap_or_else(Instant::now),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn has_capacity(&self) -> bool {
|
||||
self.in_flight < self.max_in_flight
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,666 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use common::{
|
||||
config::smtp::queue::{QueueExpiry, QueueName},
|
||||
expr::{self, functions::ResolveVariable, *},
|
||||
};
|
||||
use compact_str::ToCompactString;
|
||||
use registry::schema::enums::ExpressionVariable;
|
||||
use smtp_proto::Response;
|
||||
use std::{
|
||||
fmt::Display,
|
||||
net::{IpAddr, Ipv4Addr},
|
||||
time::{Duration, Instant, SystemTime},
|
||||
};
|
||||
use store::write::now;
|
||||
use types::blob_hash::BlobHash;
|
||||
use utils::DomainPart;
|
||||
|
||||
pub mod dsn;
|
||||
pub mod manager;
|
||||
pub mod quota;
|
||||
pub mod spool;
|
||||
pub mod throttle;
|
||||
|
||||
pub type QueueId = u64;
|
||||
|
||||
#[derive(Debug, Clone, rkyv::Serialize, rkyv::Deserialize, rkyv::Archive, serde::Deserialize)]
|
||||
pub struct Schedule<T> {
|
||||
pub due: u64,
|
||||
pub inner: T,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct QueuedMessage {
|
||||
pub due: u64,
|
||||
pub queue_id: QueueId,
|
||||
pub queue_name: QueueName,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum MessageSource {
|
||||
Authenticated,
|
||||
Unauthenticated { dmarc_pass: bool },
|
||||
Dsn,
|
||||
Report,
|
||||
Autogenerated,
|
||||
}
|
||||
|
||||
impl MessageSource {
|
||||
pub fn flags(&self) -> u64 {
|
||||
match self {
|
||||
MessageSource::Authenticated => FROM_AUTHENTICATED,
|
||||
MessageSource::Unauthenticated { dmarc_pass: true } => FROM_UNAUTHENTICATED_DMARC,
|
||||
MessageSource::Unauthenticated { dmarc_pass: false } => FROM_UNAUTHENTICATED,
|
||||
MessageSource::Dsn => FROM_DSN,
|
||||
MessageSource::Report => FROM_REPORT,
|
||||
MessageSource::Autogenerated => FROM_AUTOGENERATED,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive, Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Message {
|
||||
pub created: u64,
|
||||
pub blob_hash: BlobHash,
|
||||
|
||||
pub return_path: Box<str>,
|
||||
pub recipients: Vec<Recipient>,
|
||||
|
||||
pub received_from_ip: IpAddr,
|
||||
pub received_via_port: u16,
|
||||
|
||||
pub flags: u64,
|
||||
pub env_id: Option<Box<str>>,
|
||||
pub priority: i16,
|
||||
|
||||
pub size: u64,
|
||||
pub metadata: Box<[Metadata]>,
|
||||
}
|
||||
|
||||
impl Message {
|
||||
pub fn queued_event(&self) -> trc::QueueEvent {
|
||||
if (self.flags & FROM_AUTHENTICATED) != 0 {
|
||||
trc::QueueEvent::AuthenticatedMessageQueued
|
||||
} else if (self.flags & FROM_DSN) != 0 {
|
||||
trc::QueueEvent::DsnQueued
|
||||
} else if (self.flags & FROM_REPORT) != 0 {
|
||||
trc::QueueEvent::ReportQueued
|
||||
} else if (self.flags & FROM_AUTOGENERATED) != 0 {
|
||||
trc::QueueEvent::AutogeneratedQueued
|
||||
} else {
|
||||
trc::QueueEvent::MessageQueued
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct MessageWrapper {
|
||||
pub queue_id: QueueId,
|
||||
pub queue_name: QueueName,
|
||||
pub is_multi_queue: bool,
|
||||
pub span_id: u64,
|
||||
pub message: Message,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
rkyv::Serialize,
|
||||
rkyv::Deserialize,
|
||||
rkyv::Archive,
|
||||
Debug,
|
||||
Clone,
|
||||
PartialEq,
|
||||
Eq,
|
||||
serde::Deserialize,
|
||||
)]
|
||||
pub enum Metadata {
|
||||
QueueSize { key: Box<[u8]>, id: u64 },
|
||||
QueueCount { key: Box<[u8]>, id: u64 },
|
||||
Headers { value: Box<[u8]>, id: u64 },
|
||||
}
|
||||
|
||||
#[derive(
|
||||
rkyv::Serialize,
|
||||
rkyv::Deserialize,
|
||||
rkyv::Archive,
|
||||
Debug,
|
||||
Clone,
|
||||
PartialEq,
|
||||
Eq,
|
||||
serde::Deserialize,
|
||||
)]
|
||||
pub struct Recipient {
|
||||
pub address: Box<str>,
|
||||
|
||||
pub retry: Schedule<u32>,
|
||||
pub notify: Schedule<u32>,
|
||||
pub expires: QueueExpiry,
|
||||
|
||||
pub queue: QueueName,
|
||||
pub status: Status<HostResponse<Box<str>>, ErrorDetails>,
|
||||
pub flags: u64,
|
||||
pub orcpt: Option<Box<str>>,
|
||||
}
|
||||
|
||||
pub const FROM_AUTHENTICATED: u64 = 1 << 32;
|
||||
pub const FROM_UNAUTHENTICATED: u64 = 1 << 33;
|
||||
pub const FROM_UNAUTHENTICATED_DMARC: u64 = 1 << 34;
|
||||
pub const FROM_DSN: u64 = 1 << 35;
|
||||
pub const FROM_REPORT: u64 = 1 << 36;
|
||||
pub const FROM_AUTOGENERATED: u64 = 1 << 37;
|
||||
|
||||
pub const RCPT_DSN_SENT: u64 = 1 << 32;
|
||||
|
||||
pub const RCPT_SPAM_SHIFT: u64 = 56;
|
||||
pub const RCPT_SPAM_MASK: u64 = 0xff << RCPT_SPAM_SHIFT;
|
||||
|
||||
pub const fn rcpt_spam_flag(percentage: u8) -> u64 {
|
||||
(percentage as u64 + 1) << RCPT_SPAM_SHIFT
|
||||
}
|
||||
|
||||
pub const fn rcpt_spam_percentage(flags: u64) -> Option<u8> {
|
||||
match (flags >> RCPT_SPAM_SHIFT) as u8 {
|
||||
0 => None,
|
||||
percentage => Some(percentage - 1),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Debug,
|
||||
Clone,
|
||||
PartialEq,
|
||||
Eq,
|
||||
rkyv::Serialize,
|
||||
rkyv::Deserialize,
|
||||
rkyv::Archive,
|
||||
serde::Serialize,
|
||||
serde::Deserialize,
|
||||
)]
|
||||
pub enum Status<T, E> {
|
||||
#[serde(rename = "scheduled")]
|
||||
Scheduled,
|
||||
#[serde(rename = "completed")]
|
||||
Completed(T),
|
||||
#[serde(rename = "temp_fail")]
|
||||
TemporaryFailure(E),
|
||||
#[serde(rename = "perm_fail")]
|
||||
PermanentFailure(E),
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Debug,
|
||||
Clone,
|
||||
PartialEq,
|
||||
Eq,
|
||||
rkyv::Serialize,
|
||||
rkyv::Deserialize,
|
||||
rkyv::Archive,
|
||||
serde::Deserialize,
|
||||
)]
|
||||
pub struct HostResponse<T> {
|
||||
pub hostname: T,
|
||||
pub response: Response<Box<str>>,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Debug,
|
||||
Clone,
|
||||
PartialEq,
|
||||
Eq,
|
||||
rkyv::Serialize,
|
||||
rkyv::Deserialize,
|
||||
rkyv::Archive,
|
||||
serde::Deserialize,
|
||||
Default,
|
||||
)]
|
||||
pub enum Error {
|
||||
DnsError(Box<str>),
|
||||
UnexpectedResponse(UnexpectedResponse),
|
||||
ConnectionError(Box<str>),
|
||||
TlsError(Box<str>),
|
||||
DaneError(Box<str>),
|
||||
MtaStsError(Box<str>),
|
||||
RateLimited,
|
||||
#[default]
|
||||
ConcurrencyLimited,
|
||||
Io(Box<str>),
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Debug,
|
||||
Clone,
|
||||
PartialEq,
|
||||
Eq,
|
||||
rkyv::Serialize,
|
||||
rkyv::Deserialize,
|
||||
rkyv::Archive,
|
||||
serde::Deserialize,
|
||||
)]
|
||||
pub struct UnexpectedResponse {
|
||||
pub command: Box<str>,
|
||||
pub response: Response<Box<str>>,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Debug,
|
||||
Clone,
|
||||
PartialEq,
|
||||
Eq,
|
||||
rkyv::Serialize,
|
||||
rkyv::Deserialize,
|
||||
rkyv::Archive,
|
||||
Default,
|
||||
serde::Deserialize,
|
||||
)]
|
||||
pub struct ErrorDetails {
|
||||
pub entity: Box<str>,
|
||||
pub details: Error,
|
||||
}
|
||||
|
||||
impl<T> Ord for Schedule<T> {
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
other.due.cmp(&self.due)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> PartialOrd for Schedule<T> {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> PartialEq for Schedule<T> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.due == other.due
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Eq for Schedule<T> {}
|
||||
|
||||
impl<T: Default> Schedule<T> {
|
||||
pub fn now() -> Self {
|
||||
Schedule {
|
||||
due: now(),
|
||||
inner: T::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn later(duration: u64) -> Self {
|
||||
Schedule {
|
||||
due: now() + duration,
|
||||
inner: T::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct QueueEnvelope<'x> {
|
||||
pub message: &'x Message,
|
||||
pub domain: &'x str,
|
||||
pub mx: &'x str,
|
||||
pub rcpt: &'x Recipient,
|
||||
pub remote_ip: IpAddr,
|
||||
pub local_ip: IpAddr,
|
||||
}
|
||||
|
||||
impl<'x> QueueEnvelope<'x> {
|
||||
pub fn new(message: &'x Message, rcpt: &'x Recipient) -> Self {
|
||||
Self {
|
||||
message,
|
||||
domain: rcpt.address.domain_part(),
|
||||
rcpt,
|
||||
mx: "",
|
||||
remote_ip: IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)),
|
||||
local_ip: IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> ResolveVariable for QueueEnvelope<'x> {
|
||||
fn resolve_variable(&self, variable: ExpressionVariable) -> expr::Variable<'x> {
|
||||
match variable {
|
||||
ExpressionVariable::Sender => self.message.return_path.as_ref().into(),
|
||||
ExpressionVariable::SenderDomain => self.message.return_path.domain_part().into(),
|
||||
ExpressionVariable::RcptDomain => self.domain.into(),
|
||||
ExpressionVariable::Rcpt => self.rcpt.address.as_ref().into(),
|
||||
ExpressionVariable::Recipients => self
|
||||
.message
|
||||
.recipients
|
||||
.iter()
|
||||
.map(|r| Variable::from(r.address.as_ref()))
|
||||
.collect::<Vec<_>>()
|
||||
.into(),
|
||||
ExpressionVariable::RetryNum => self.rcpt.retry.inner.into(),
|
||||
ExpressionVariable::NotifyNum => self.rcpt.notify.inner.into(),
|
||||
ExpressionVariable::ExpiresIn => match &self.rcpt.expires {
|
||||
QueueExpiry::Ttl(time) => (*time + self.message.created).saturating_sub(now()),
|
||||
QueueExpiry::Attempts(count) => {
|
||||
(count.saturating_sub(self.rcpt.retry.inner)) as u64
|
||||
}
|
||||
}
|
||||
.into(),
|
||||
ExpressionVariable::LastStatus => self.rcpt.status.to_compact_string().into(),
|
||||
ExpressionVariable::LastError => match &self.rcpt.status {
|
||||
Status::Scheduled | Status::Completed(_) => "none",
|
||||
Status::TemporaryFailure(err) | Status::PermanentFailure(err) => {
|
||||
match &err.details {
|
||||
Error::DnsError(_) => "dns",
|
||||
Error::UnexpectedResponse(_) => "unexpected-reply",
|
||||
Error::ConnectionError(_) => "connection",
|
||||
Error::TlsError(_) => "tls",
|
||||
Error::DaneError(_) => "dane",
|
||||
Error::MtaStsError(_) => "mta-sts",
|
||||
Error::RateLimited => "rate",
|
||||
Error::ConcurrencyLimited => "concurrency",
|
||||
Error::Io(_) => "io",
|
||||
}
|
||||
}
|
||||
}
|
||||
.into(),
|
||||
ExpressionVariable::QueueName => self.rcpt.queue.as_str().into(),
|
||||
ExpressionVariable::QueueAge => now().saturating_sub(self.message.created).into(),
|
||||
ExpressionVariable::Source => if (self.message.flags & FROM_AUTHENTICATED) != 0 {
|
||||
"authenticated"
|
||||
} else if (self.message.flags & FROM_UNAUTHENTICATED_DMARC) != 0 {
|
||||
"dmarc_pass"
|
||||
} else if (self.message.flags & FROM_UNAUTHENTICATED) != 0 {
|
||||
"unauthenticated"
|
||||
} else if (self.message.flags & FROM_DSN) != 0 {
|
||||
"dsn"
|
||||
} else if (self.message.flags & FROM_REPORT) != 0 {
|
||||
"report"
|
||||
} else if (self.message.flags & FROM_AUTOGENERATED) != 0 {
|
||||
"autogenerated"
|
||||
} else {
|
||||
"unknown"
|
||||
}
|
||||
.into(),
|
||||
ExpressionVariable::Mx => self.mx.into(),
|
||||
ExpressionVariable::Priority => self.message.priority.into(),
|
||||
ExpressionVariable::RemoteIp => self.remote_ip.to_compact_string().into(),
|
||||
ExpressionVariable::LocalIp => self.local_ip.to_compact_string().into(),
|
||||
ExpressionVariable::ReceivedFromIp => {
|
||||
self.message.received_from_ip.to_compact_string().into()
|
||||
}
|
||||
ExpressionVariable::ReceivedViaPort => self.message.received_via_port.into(),
|
||||
ExpressionVariable::Size => self.message.size.into(),
|
||||
_ => "".into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_global(&self, _: &str) -> Variable<'_> {
|
||||
Variable::Integer(0)
|
||||
}
|
||||
}
|
||||
|
||||
impl ResolveVariable for Message {
|
||||
fn resolve_variable(&self, variable: ExpressionVariable) -> expr::Variable<'_> {
|
||||
match variable {
|
||||
ExpressionVariable::Sender => self.return_path.as_ref().into(),
|
||||
ExpressionVariable::SenderDomain => self.return_path.domain_part().into(),
|
||||
ExpressionVariable::Recipients => self
|
||||
.recipients
|
||||
.iter()
|
||||
.map(|r| Variable::from(r.address.as_ref()))
|
||||
.collect::<Vec<_>>()
|
||||
.into(),
|
||||
ExpressionVariable::Priority => self.priority.into(),
|
||||
_ => "".into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_global(&self, _: &str) -> Variable<'_> {
|
||||
Variable::Integer(0)
|
||||
}
|
||||
}
|
||||
|
||||
impl ResolveVariable for MessageWrapper {
|
||||
fn resolve_variable(&self, variable: ExpressionVariable) -> expr::Variable<'_> {
|
||||
match variable {
|
||||
ExpressionVariable::Sender => self.message.return_path.as_ref().into(),
|
||||
ExpressionVariable::SenderDomain => self.message.return_path.domain_part().into(),
|
||||
ExpressionVariable::Recipients => self
|
||||
.message
|
||||
.recipients
|
||||
.iter()
|
||||
.map(|r| Variable::from(r.address.as_ref()))
|
||||
.collect::<Vec<_>>()
|
||||
.into(),
|
||||
ExpressionVariable::Priority => self.message.priority.into(),
|
||||
ExpressionVariable::QueueName => self.queue_name.as_str().into(),
|
||||
ExpressionVariable::QueueAge => now().saturating_sub(self.message.created).into(),
|
||||
ExpressionVariable::Source => if (self.message.flags & FROM_AUTHENTICATED) != 0 {
|
||||
"authenticated"
|
||||
} else if (self.message.flags & FROM_UNAUTHENTICATED_DMARC) != 0 {
|
||||
"dmarc_pass"
|
||||
} else if (self.message.flags & FROM_UNAUTHENTICATED) != 0 {
|
||||
"unauthenticated"
|
||||
} else if (self.message.flags & FROM_DSN) != 0 {
|
||||
"dsn"
|
||||
} else if (self.message.flags & FROM_REPORT) != 0 {
|
||||
"report"
|
||||
} else if (self.message.flags & FROM_AUTOGENERATED) != 0 {
|
||||
"autogenerated"
|
||||
} else {
|
||||
"unknown"
|
||||
}
|
||||
.into(),
|
||||
ExpressionVariable::ReceivedFromIp => {
|
||||
self.message.received_from_ip.to_compact_string().into()
|
||||
}
|
||||
ExpressionVariable::ReceivedViaPort => self.message.received_via_port.into(),
|
||||
ExpressionVariable::Size => self.message.size.into(),
|
||||
_ => "".into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_global(&self, _: &str) -> Variable<'_> {
|
||||
Variable::Integer(0)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RecipientDomain<'x>(&'x str);
|
||||
|
||||
impl<'x> RecipientDomain<'x> {
|
||||
pub fn new(domain: &'x str) -> Self {
|
||||
Self(domain)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> ResolveVariable for RecipientDomain<'x> {
|
||||
fn resolve_variable(&self, variable: ExpressionVariable) -> expr::Variable<'x> {
|
||||
match variable {
|
||||
ExpressionVariable::RcptDomain => self.0.into(),
|
||||
_ => "".into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_global(&self, _: &str) -> Variable<'_> {
|
||||
Variable::Integer(0)
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn instant_to_timestamp(now: Instant, time: Instant) -> u64 {
|
||||
SystemTime::now()
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.map_or(0, |d| d.as_secs())
|
||||
+ time.checked_duration_since(now).map_or(0, |d| d.as_secs())
|
||||
}
|
||||
|
||||
impl Recipient {
|
||||
pub fn new(address: impl AsRef<str>) -> Self {
|
||||
Recipient {
|
||||
address: address.to_lowercase_address(false).into_boxed_str(),
|
||||
status: Status::Scheduled,
|
||||
flags: 0,
|
||||
orcpt: None,
|
||||
retry: Schedule::now(),
|
||||
notify: Schedule::now(),
|
||||
expires: QueueExpiry::Attempts(0),
|
||||
queue: QueueName::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_flags(mut self, flags: u64) -> Self {
|
||||
self.flags = flags;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_orcpt(mut self, orcpt: Option<Box<str>>) -> Self {
|
||||
self.orcpt = orcpt;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn address(&self) -> &str {
|
||||
&self.address
|
||||
}
|
||||
|
||||
pub fn domain_part(&self) -> &str {
|
||||
self.address.domain_part()
|
||||
}
|
||||
}
|
||||
|
||||
impl ArchivedRecipient {
|
||||
pub fn address(&self) -> &str {
|
||||
self.address.as_ref()
|
||||
}
|
||||
|
||||
pub fn domain_part(&self) -> &str {
|
||||
self.address.domain_part()
|
||||
}
|
||||
}
|
||||
|
||||
pub trait InstantFromTimestamp {
|
||||
fn to_instant(&self) -> Instant;
|
||||
}
|
||||
|
||||
impl InstantFromTimestamp for u64 {
|
||||
fn to_instant(&self) -> Instant {
|
||||
let timestamp = *self;
|
||||
let current_timestamp = SystemTime::now()
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.map_or(0, |d| d.as_secs());
|
||||
if timestamp > current_timestamp {
|
||||
Instant::now() + Duration::from_secs(timestamp - current_timestamp)
|
||||
} else {
|
||||
Instant::now()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Error {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Error::UnexpectedResponse(response) => {
|
||||
write!(
|
||||
f,
|
||||
"Unexpected response for {}: {}",
|
||||
response.command, response.response
|
||||
)
|
||||
}
|
||||
Error::DnsError(err) => {
|
||||
write!(f, "DNS lookup failed: {err}")
|
||||
}
|
||||
Error::ConnectionError(details) => {
|
||||
write!(f, "Connection failed: {details}",)
|
||||
}
|
||||
Error::TlsError(details) => {
|
||||
write!(f, "TLS error: {details}",)
|
||||
}
|
||||
Error::DaneError(details) => {
|
||||
write!(f, "DANE authentication failure: {details}",)
|
||||
}
|
||||
Error::MtaStsError(details) => {
|
||||
write!(f, "MTA-STS auth failed: {details}")
|
||||
}
|
||||
Error::RateLimited => {
|
||||
write!(f, "Rate limited")
|
||||
}
|
||||
Error::ConcurrencyLimited => {
|
||||
write!(f, "Too many concurrent connections to remote server")
|
||||
}
|
||||
Error::Io(err) => {
|
||||
write!(f, "Queue error: {err}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ArchivedError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
ArchivedError::UnexpectedResponse(response) => {
|
||||
write!(
|
||||
f,
|
||||
"Unexpected response for {}: {}",
|
||||
response.command, response.response
|
||||
)
|
||||
}
|
||||
ArchivedError::DnsError(err) => {
|
||||
write!(f, "DNS lookup failed: {err}")
|
||||
}
|
||||
ArchivedError::ConnectionError(details) => {
|
||||
write!(f, "Connection failed: {details}",)
|
||||
}
|
||||
ArchivedError::TlsError(details) => {
|
||||
write!(f, "TLS error: {details}",)
|
||||
}
|
||||
ArchivedError::DaneError(details) => {
|
||||
write!(f, "DANE authentication failure: {details}",)
|
||||
}
|
||||
ArchivedError::MtaStsError(details) => {
|
||||
write!(f, "MTA-STS auth failed: {details}")
|
||||
}
|
||||
ArchivedError::RateLimited => {
|
||||
write!(f, "Rate limited")
|
||||
}
|
||||
ArchivedError::ConcurrencyLimited => {
|
||||
write!(f, "Too many concurrent connections to remote server")
|
||||
}
|
||||
ArchivedError::Io(err) => {
|
||||
write!(f, "Queue error: {err}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Status<HostResponse<Box<str>>, ErrorDetails> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Status::Scheduled => write!(f, "Scheduled"),
|
||||
Status::Completed(response) => write!(f, "Delivered: {}", response.response),
|
||||
Status::TemporaryFailure(err) => {
|
||||
write!(f, "Temporary Failure for {}: {}", err.entity, err.details)
|
||||
}
|
||||
Status::PermanentFailure(err) => {
|
||||
write!(f, "Permanent Failure for {}: {}", err.entity, err.details)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ArchivedErrorDetails {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "Error for {}: {}", self.entity, self.details)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
pub trait DisplayArchivedResponse {
|
||||
fn to_string(&self) -> String;
|
||||
}
|
||||
|
||||
impl DisplayArchivedResponse for ArchivedResponse<Box<str>> {
|
||||
fn to_string(&self) -> String {
|
||||
format!(
|
||||
"Code: {}, Enhanced code: {}.{}.{}, Message: {}",
|
||||
self.code, self.esc[0], self.esc[1], self.esc[2], self.message,
|
||||
)
|
||||
}
|
||||
}
|
||||
*/
|
||||
@@ -0,0 +1,230 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::{Metadata, QueueEnvelope, Status};
|
||||
use crate::{core::throttle::NewKey, queue::MessageWrapper};
|
||||
use ahash::AHashSet;
|
||||
use common::{Server, config::smtp::queue::QueueQuota, expr::functions::ResolveVariable};
|
||||
use std::future::Future;
|
||||
use store::{
|
||||
ValueKey,
|
||||
write::{BatchBuilder, QueueClass, ValueClass},
|
||||
};
|
||||
use trc::QueueEvent;
|
||||
use utils::DomainPart;
|
||||
|
||||
pub trait HasQueueQuota: Sync + Send {
|
||||
fn has_quota(
|
||||
&self,
|
||||
message: &mut MessageWrapper,
|
||||
) -> impl Future<Output = Option<Vec<Metadata>>> + Send;
|
||||
fn check_quota<'x>(
|
||||
&'x self,
|
||||
quota: &'x QueueQuota,
|
||||
envelope: &impl ResolveVariable,
|
||||
size: u64,
|
||||
id: u64,
|
||||
refs: &mut Vec<Metadata>,
|
||||
session_id: u64,
|
||||
) -> impl Future<Output = bool> + Send;
|
||||
}
|
||||
|
||||
impl HasQueueQuota for Server {
|
||||
async fn has_quota(&self, message: &mut MessageWrapper) -> Option<Vec<Metadata>> {
|
||||
let mut quota_keys = Vec::new();
|
||||
|
||||
if !self.core.smtp.queue.quota.sender.is_empty() {
|
||||
for quota in &self.core.smtp.queue.quota.sender {
|
||||
if !self
|
||||
.check_quota(
|
||||
quota,
|
||||
&message.message,
|
||||
message.message.size,
|
||||
0,
|
||||
&mut quota_keys,
|
||||
message.span_id,
|
||||
)
|
||||
.await
|
||||
{
|
||||
trc::event!(
|
||||
Queue(QueueEvent::QuotaExceeded),
|
||||
SpanId = message.span_id,
|
||||
Id = quota.id.to_string(),
|
||||
Type = "Sender"
|
||||
);
|
||||
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !self.core.smtp.queue.quota.rcpt_domain.is_empty() {
|
||||
let mut seen_domains = AHashSet::new();
|
||||
for quota in &self.core.smtp.queue.quota.rcpt_domain {
|
||||
for (rcpt_idx, rcpt) in message.message.recipients.iter().enumerate() {
|
||||
if seen_domains.insert(rcpt.address.domain_part())
|
||||
&& !self
|
||||
.check_quota(
|
||||
quota,
|
||||
&QueueEnvelope::new(&message.message, rcpt),
|
||||
message.message.size,
|
||||
((rcpt_idx + 1) << 32) as u64,
|
||||
&mut quota_keys,
|
||||
message.span_id,
|
||||
)
|
||||
.await
|
||||
{
|
||||
trc::event!(
|
||||
Queue(QueueEvent::QuotaExceeded),
|
||||
SpanId = message.span_id,
|
||||
Id = quota.id.to_string(),
|
||||
Type = "Domain"
|
||||
);
|
||||
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for quota in &self.core.smtp.queue.quota.rcpt {
|
||||
for (rcpt_idx, rcpt) in message.message.recipients.iter().enumerate() {
|
||||
if !self
|
||||
.check_quota(
|
||||
quota,
|
||||
&QueueEnvelope::new(&message.message, rcpt),
|
||||
message.message.size,
|
||||
(rcpt_idx + 1) as u64,
|
||||
&mut quota_keys,
|
||||
message.span_id,
|
||||
)
|
||||
.await
|
||||
{
|
||||
trc::event!(
|
||||
Queue(QueueEvent::QuotaExceeded),
|
||||
SpanId = message.span_id,
|
||||
Id = quota.id.to_string(),
|
||||
Type = "Recipient"
|
||||
);
|
||||
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some(quota_keys)
|
||||
}
|
||||
|
||||
async fn check_quota<'x>(
|
||||
&'x self,
|
||||
quota: &'x QueueQuota,
|
||||
envelope: &impl ResolveVariable,
|
||||
size: u64,
|
||||
id: u64,
|
||||
refs: &mut Vec<Metadata>,
|
||||
session_id: u64,
|
||||
) -> bool {
|
||||
if !quota.expr.is_empty()
|
||||
&& self
|
||||
.eval_if("a.expr, envelope, session_id)
|
||||
.await
|
||||
.unwrap_or(false)
|
||||
{
|
||||
let key = quota.new_key(envelope, "");
|
||||
if let Some(max_size) = quota.size {
|
||||
let used_size = self
|
||||
.core
|
||||
.storage
|
||||
.data
|
||||
.get_counter(ValueKey::from(ValueClass::Queue(QueueClass::QuotaSize(
|
||||
key.as_ref().to_vec(),
|
||||
))))
|
||||
.await
|
||||
.unwrap_or(0) as u64;
|
||||
if used_size + size > max_size {
|
||||
return false;
|
||||
} else {
|
||||
refs.push(Metadata::QueueSize {
|
||||
key: key.as_ref().into(),
|
||||
id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(max_messages) = quota.messages {
|
||||
let total_messages = self
|
||||
.core
|
||||
.storage
|
||||
.data
|
||||
.get_counter(ValueKey::from(ValueClass::Queue(QueueClass::QuotaCount(
|
||||
key.as_ref().to_vec(),
|
||||
))))
|
||||
.await
|
||||
.unwrap_or(0) as u64;
|
||||
if total_messages + 1 > max_messages {
|
||||
return false;
|
||||
} else {
|
||||
refs.push(Metadata::QueueCount {
|
||||
key: key.as_ref().into(),
|
||||
id,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl MessageWrapper {
|
||||
pub fn release_quota(&mut self, batch: &mut BatchBuilder) {
|
||||
if !self.message.metadata.iter().any(|metadata| {
|
||||
matches!(
|
||||
metadata,
|
||||
Metadata::QueueSize { .. } | Metadata::QueueCount { .. }
|
||||
)
|
||||
}) {
|
||||
return;
|
||||
}
|
||||
let mut quota_ids = Vec::with_capacity(self.message.recipients.len());
|
||||
|
||||
let mut seen_domains = AHashSet::new();
|
||||
for (pos, rcpt) in self.message.recipients.iter().enumerate() {
|
||||
if matches!(
|
||||
&rcpt.status,
|
||||
Status::Completed(_) | Status::PermanentFailure(_)
|
||||
) {
|
||||
if seen_domains.insert(rcpt.address.domain_part()) {
|
||||
quota_ids.push(((pos + 1) as u64) << 32);
|
||||
}
|
||||
quota_ids.push((pos + 1) as u64);
|
||||
}
|
||||
}
|
||||
|
||||
if !quota_ids.is_empty() {
|
||||
let mut metadata = Vec::new();
|
||||
for entry in std::mem::take(&mut self.message.metadata) {
|
||||
match entry {
|
||||
Metadata::QueueCount { id, key } if quota_ids.contains(&id) => {
|
||||
batch.add(
|
||||
ValueClass::Queue(QueueClass::QuotaCount(key.into_vec())),
|
||||
-1,
|
||||
);
|
||||
}
|
||||
Metadata::QueueSize { id, key } if quota_ids.contains(&id) => {
|
||||
batch.add(
|
||||
ValueClass::Queue(QueueClass::QuotaSize(key.into_vec())),
|
||||
-(self.message.size as i64),
|
||||
);
|
||||
}
|
||||
_ => {
|
||||
metadata.push(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
self.message.metadata = metadata.into_boxed_slice();
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::core::throttle::NewKey;
|
||||
use common::{
|
||||
KV_RATE_LIMIT_SMTP, Server, config::smtp::QueueRateLimiter, expr::functions::ResolveVariable,
|
||||
};
|
||||
use std::future::Future;
|
||||
use store::write::now;
|
||||
|
||||
pub trait IsAllowed: Sync + Send {
|
||||
fn is_allowed<'x>(
|
||||
&'x self,
|
||||
throttle: &'x QueueRateLimiter,
|
||||
envelope: &impl ResolveVariable,
|
||||
session_id: u64,
|
||||
) -> impl Future<Output = Result<(), u64>> + Send;
|
||||
}
|
||||
|
||||
impl IsAllowed for Server {
|
||||
async fn is_allowed<'x>(
|
||||
&'x self,
|
||||
throttle: &'x QueueRateLimiter,
|
||||
envelope: &impl ResolveVariable,
|
||||
session_id: u64,
|
||||
) -> Result<(), u64> {
|
||||
if throttle.expr.is_empty()
|
||||
|| self
|
||||
.eval_if(&throttle.expr, envelope, session_id)
|
||||
.await
|
||||
.unwrap_or(false)
|
||||
{
|
||||
let key = throttle.new_key(envelope, "outbound");
|
||||
|
||||
match self
|
||||
.in_memory_store()
|
||||
.is_rate_allowed(KV_RATE_LIMIT_SMTP, key.as_ref(), &throttle.rate, false)
|
||||
.await
|
||||
{
|
||||
Ok(Some(next_refill)) => {
|
||||
trc::event!(
|
||||
Queue(trc::QueueEvent::RateLimitExceeded),
|
||||
SpanId = session_id,
|
||||
Id = throttle.id.to_string(),
|
||||
Limit = vec![
|
||||
trc::Value::from(throttle.rate.count),
|
||||
trc::Value::from(throttle.rate.period.into_inner())
|
||||
],
|
||||
);
|
||||
|
||||
return Err(now() + next_refill);
|
||||
}
|
||||
Err(err) => {
|
||||
trc::error!(err.span_id(session_id).caused_by(trc::location!()));
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user