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:
2026-09-18 10:21:56 -07:00
commit 7dae9b29fd
1650 changed files with 485521 additions and 0 deletions
+125
View File
@@ -0,0 +1,125 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use sieve::Envelope;
use smtp_proto::{
MAIL_BY_NOTIFY, MAIL_BY_RETURN, MAIL_BY_TRACE, MAIL_RET_FULL, MAIL_RET_HDRS, RCPT_NOTIFY_DELAY,
RCPT_NOTIFY_FAILURE, RCPT_NOTIFY_NEVER, RCPT_NOTIFY_SUCCESS,
};
use utils::DomainPart;
use crate::core::{SessionAddress, SessionData};
impl SessionData {
pub fn apply_envelope_modification(&mut self, envelope: Envelope, value: String) {
match envelope {
Envelope::From => {
let (address, address_lcase, domain) = if value.contains('@') {
let address_lcase = value.to_lowercase();
let domain = address_lcase.domain_part().into();
(value, address_lcase, domain)
} else if value.is_empty() {
(String::new(), String::new(), String::new())
} else {
return;
};
if let Some(mail_from) = &mut self.mail_from {
mail_from.address = address;
mail_from.address_lcase = address_lcase;
mail_from.domain = domain;
} else {
self.mail_from = SessionAddress {
address,
address_lcase,
domain,
flags: 0,
dsn_info: None,
}
.into();
}
}
Envelope::To => {
if value.contains('@') {
let address_lcase = value.to_lowercase();
let domain = address_lcase.domain_part().into();
if let Some(rcpt_to) = self.rcpt_to.last_mut() {
rcpt_to.address = value;
rcpt_to.address_lcase = address_lcase;
rcpt_to.domain = domain;
} else {
self.rcpt_to.push(SessionAddress {
address: value,
address_lcase,
domain,
flags: 0,
dsn_info: None,
});
}
}
}
Envelope::ByMode => {
if let Some(mail_from) = &mut self.mail_from {
mail_from.flags &= !(MAIL_BY_NOTIFY | MAIL_BY_RETURN);
if value == "N" {
mail_from.flags |= MAIL_BY_NOTIFY;
} else if value == "R" {
mail_from.flags |= MAIL_BY_RETURN;
}
}
}
Envelope::ByTrace => {
if let Some(mail_from) = &mut self.mail_from {
if value == "T" {
mail_from.flags |= MAIL_BY_TRACE;
} else {
mail_from.flags &= !MAIL_BY_TRACE;
}
}
}
Envelope::Notify => {
if let Some(rcpt_to) = self.rcpt_to.last_mut() {
rcpt_to.flags &= !(RCPT_NOTIFY_DELAY
| RCPT_NOTIFY_FAILURE
| RCPT_NOTIFY_SUCCESS
| RCPT_NOTIFY_NEVER);
if value == "NEVER" {
rcpt_to.flags |= RCPT_NOTIFY_NEVER;
} else {
for value in value.split(',') {
match value.trim() {
"SUCCESS" => rcpt_to.flags |= RCPT_NOTIFY_SUCCESS,
"FAILURE" => rcpt_to.flags |= RCPT_NOTIFY_FAILURE,
"DELAY" => rcpt_to.flags |= RCPT_NOTIFY_DELAY,
_ => (),
}
}
}
}
}
Envelope::Ret => {
if let Some(mail_from) = &mut self.mail_from {
mail_from.flags &= !(MAIL_RET_FULL | MAIL_RET_HDRS);
if value == "FULL" {
mail_from.flags |= MAIL_RET_FULL;
} else if value == "HDRS" {
mail_from.flags |= MAIL_RET_HDRS;
}
}
}
Envelope::Orcpt => {
if let Some(rcpt_to) = self.rcpt_to.last_mut() {
rcpt_to.dsn_info = value.into();
}
}
Envelope::Envid => {
if let Some(mail_from) = &mut self.mail_from {
mail_from.dsn_info = value.into();
}
}
Envelope::ByTimeAbsolute | Envelope::ByTimeRelative => (),
}
}
}
+435
View File
@@ -0,0 +1,435 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::queue::{
MessageSource,
quota::HasQueueQuota,
spool::{QueueParams, SmtpSpool},
};
use common::{Server, config::smtp::queue::QueueExpiry, scripts::plugins::PluginContext};
use mail_parser::{Encoding, Message, MessagePart, PartType};
use sieve::{
Event, Input, MatchAs, Recipient, Sieve,
compiler::grammar::actions::action_redirect::{ByMode, ByTime, Notify, NotifyItem, Ret},
};
use smtp_proto::{
MAIL_BY_TRACE, MAIL_RET_FULL, MAIL_RET_HDRS, RCPT_NOTIFY_DELAY, RCPT_NOTIFY_FAILURE,
RCPT_NOTIFY_NEVER, RCPT_NOTIFY_SUCCESS,
};
use std::{future::Future, sync::Arc, time::Instant};
use trc::SieveEvent;
use super::{ScriptModification, ScriptParameters, ScriptResult};
pub trait RunScript: Sync + Send {
fn run_script(
&self,
script_id: String,
script: Arc<Sieve>,
params: ScriptParameters<'_>,
) -> impl Future<Output = ScriptResult> + Send;
}
impl RunScript for Server {
async fn run_script(
&self,
script_id: String,
script: Arc<Sieve>,
params: ScriptParameters<'_>,
) -> ScriptResult {
// Create filter instance
let time = Instant::now();
let mut instance = self
.core
.sieve
.trusted_runtime
.filter_parsed(params.message.unwrap_or_else(|| Message {
parts: vec![MessagePart {
headers: vec![],
is_encoding_problem: false,
body: PartType::Text("".into()),
encoding: Encoding::None,
offset_header: 0,
offset_body: 0,
offset_end: 0,
}],
raw_message: b""[..].into(),
..Default::default()
}))
.with_vars_env(params.variables)
.with_envelope_list(params.envelope)
.with_user_address(&params.from_addr)
.with_user_full_name(&params.from_name);
if let Some(spam_status) = params.spam_status {
instance.set_spam_status(spam_status);
}
let mut input = Input::script("__script", script);
let mut messages: Vec<Vec<u8>> = Vec::new();
let session_id = params.session_id;
let mut reject_reason = None;
let mut modifications = vec![];
let mut keep_id = usize::MAX;
// Start event loop
while let Some(result) = instance.run(input) {
match result {
Ok(event) => match event {
Event::IncludeScript { name, optional } => {
if let Some(script) = self.core.sieve.trusted_script(name.as_str()) {
input = Input::script(name, script.clone());
} else if optional {
input = false.into();
} else {
trc::event!(
Sieve(SieveEvent::ScriptNotFound),
Id = script_id.clone(),
SpanId = session_id,
Details = name.as_str().to_string(),
);
break;
}
}
Event::ListContains {
lists,
values,
match_as,
} => {
input = false.into();
'outer: for list in lists {
if let Some(store) = self.get_lookup_store(&list) {
for value in &values {
if let Ok(true) = store
.key_exists(if !matches!(match_as, MatchAs::Lowercase) {
value.clone()
} else {
value.to_lowercase()
})
.await
{
input = true.into();
break 'outer;
}
}
} else {
trc::event!(
Sieve(SieveEvent::ListNotFound),
Id = script_id.clone(),
SpanId = session_id,
Details = list,
);
}
}
}
Event::Function { id, arguments } => {
input = self
.core
.run_plugin(
id,
PluginContext {
session_id,
server: self,
message: instance.message(),
modifications: &mut modifications,
access_token: params.access_token,
arguments,
},
)
.await;
}
Event::Keep { message_id, .. } => {
keep_id = message_id;
input = true.into();
}
Event::Discard => {
keep_id = usize::MAX - 1;
input = true.into();
}
Event::Reject { reason, .. } => {
reject_reason = reason.into();
input = true.into();
}
Event::SendMessage {
recipient,
notify,
return_of_content,
by_time,
message_id,
} => {
// Build message
let mut message = self.new_message(
params.return_path.as_str(),
MessageSource::Autogenerated,
session_id,
);
match recipient {
Recipient::Address(rcpt) => {
message.expand_and_add_recipient(rcpt, self).await;
}
Recipient::Group(rcpt_list) => {
for rcpt in rcpt_list {
message.expand_and_add_recipient(rcpt, self).await;
}
}
Recipient::List(list) => {
trc::event!(
Sieve(SieveEvent::NotSupported),
Id = script_id.clone(),
SpanId = session_id,
Details = list,
Reason = "Sending to lists is not supported.",
);
}
}
// Set notify flags
let mut flags = 0;
match notify {
Notify::Never => {
flags = RCPT_NOTIFY_NEVER;
}
Notify::Items(items) => {
for item in items {
flags |= match item {
NotifyItem::Success => RCPT_NOTIFY_SUCCESS,
NotifyItem::Failure => RCPT_NOTIFY_FAILURE,
NotifyItem::Delay => RCPT_NOTIFY_DELAY,
};
}
}
Notify::Default => (),
}
if flags > 0 {
for rcpt in &mut message.message.recipients {
rcpt.flags |= flags;
}
}
// Set ByTime flags
match by_time {
ByTime::Relative {
rlimit,
mode,
trace,
} => {
if trace {
message.message.flags |= MAIL_BY_TRACE;
}
match mode {
ByMode::Notify => {
for domain in &mut message.message.recipients {
domain.notify.due += rlimit;
}
}
ByMode::Return => {
for domain in &mut message.message.recipients {
domain.notify.due += rlimit;
}
}
ByMode::Default => (),
}
}
ByTime::Absolute {
alimit,
mode,
trace,
} => {
if trace {
message.message.flags |= MAIL_BY_TRACE;
}
match mode {
ByMode::Notify => {
for domain in &mut message.message.recipients {
domain.notify.due = alimit as u64;
}
}
ByMode::Return => {
let expires =
(alimit as u64).saturating_sub(message.message.created);
if expires > 0 {
for domain in &mut message.message.recipients {
domain.expires = QueueExpiry::Ttl(expires);
}
}
}
ByMode::Default => (),
}
}
ByTime::None => (),
};
// Set ret
match return_of_content {
Ret::Full => {
message.message.flags |= MAIL_RET_FULL;
}
Ret::Hdrs => {
message.message.flags |= MAIL_RET_HDRS;
}
Ret::Default => (),
}
// Queue message
let is_forward = message_id == 0;
let raw_message = if !is_forward {
messages.get(message_id - 1).map(|m| m.as_slice())
} else {
instance.message().raw_message().into()
};
if let Some(raw_message) = raw_message.filter(|m| !m.is_empty()) {
if let Some(metadata) = self.has_quota(&mut message).await {
let dkim_signers = if let Some(sign_domain) = &params.sign_domain {
match self.dkim_signers(sign_domain).await {
Ok(signers) => signers,
Err(err) => {
trc::error!(
err.details("Failed to obtain DKIM signers")
.caused_by(trc::location!())
);
None
}
}
} else {
None
};
message
.queue(
QueueParams::new(raw_message, session_id, self)
.with_dkim_signers(dkim_signers)
.with_raw_headers_opt(
params.headers.filter(|_| is_forward),
)
.with_original_raw_message(
instance.message().raw_message(),
)
.with_metadata(metadata),
)
.await;
} else {
trc::event!(
Sieve(SieveEvent::QuotaExceeded),
SpanId = session_id,
Id = script_id.clone(),
From = message.message.return_path,
To = message
.message
.recipients
.into_iter()
.map(|r| trc::Value::from(r.address().to_string()))
.collect::<Vec<_>>(),
);
}
}
input = true.into();
}
Event::CreatedMessage { message, .. } => {
messages.push(message);
input = true.into();
}
Event::SetEnvelope { envelope, value } => {
modifications.push(ScriptModification::SetEnvelope {
name: envelope,
value,
});
input = true.into();
}
unsupported => {
trc::event!(
Sieve(SieveEvent::NotSupported),
Id = script_id.clone(),
SpanId = session_id,
Reason = "Unsupported event",
Details = format!("{unsupported:?}"),
);
break;
}
},
Err(err) => {
trc::event!(
Sieve(SieveEvent::RuntimeError),
Id = script_id.clone(),
SpanId = session_id,
Reason = err.to_string(),
);
break;
}
}
}
// Keep id
// 0 = use original message
// MAX = implicit keep
// MAX - 1 = discard message
if keep_id == 0 {
trc::event!(
Sieve(SieveEvent::ActionAccept),
SpanId = session_id,
Id = script_id,
Elapsed = time.elapsed(),
);
ScriptResult::Accept { modifications }
} else if let Some(mut reject_reason) = reject_reason {
trc::event!(
Sieve(SieveEvent::ActionReject),
Id = script_id,
SpanId = session_id,
Details = reject_reason.clone(),
Elapsed = time.elapsed(),
);
if !reject_reason.ends_with('\n') {
reject_reason.push_str("\r\n");
}
let mut reject_bytes = reject_reason.as_bytes().iter();
if matches!(reject_bytes.next(), Some(ch) if ch.is_ascii_digit())
&& matches!(reject_bytes.next(), Some(ch) if ch.is_ascii_digit())
&& matches!(reject_bytes.next(), Some(ch) if ch.is_ascii_digit())
&& matches!(reject_bytes.next(), Some(ch) if ch == &b' ' )
{
ScriptResult::Reject(reject_reason)
} else {
ScriptResult::Reject(format!("503 5.5.3 {reject_reason}"))
}
} else if keep_id != usize::MAX - 1 {
if let Some(message) = messages.into_iter().nth(keep_id - 1) {
trc::event!(
Sieve(SieveEvent::ActionAccept),
SpanId = session_id,
Id = script_id,
Elapsed = time.elapsed(),
);
ScriptResult::Replace {
message,
modifications,
}
} else {
trc::event!(
Sieve(SieveEvent::ActionAcceptReplace),
SpanId = session_id,
Id = script_id,
Elapsed = time.elapsed(),
);
ScriptResult::Accept { modifications }
}
} else {
trc::event!(
Sieve(SieveEvent::ActionDiscard),
SpanId = session_id,
Id = script_id,
Elapsed = time.elapsed()
);
ScriptResult::Discard
}
}
}
+175
View File
@@ -0,0 +1,175 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::{sync::Arc, time::SystemTime};
use common::network::SessionStream;
use mail_auth::common::resolver::ToReverseName;
use sieve::{Envelope, Sieve, runtime::Variable};
use smtp_proto::*;
use crate::{core::Session, inbound::AuthResult};
use super::{ScriptParameters, ScriptResult, event_loop::RunScript};
impl<T: SessionStream> Session<T> {
pub fn build_script_parameters(&self, stage: &'static str) -> ScriptParameters<'_> {
let (tls_version, tls_cipher) = self.stream.tls_version_and_cipher();
let mut params = ScriptParameters::new()
.set_variable("remote_ip", self.data.remote_ip.to_string())
.set_variable("remote_ip.reverse", self.data.remote_ip.to_reverse_name())
.set_variable("helo_domain", self.data.helo_domain.as_str().to_lowercase())
.set_variable(
"authenticated_as",
self.authenticated_as().unwrap_or_default().to_string(),
)
.set_variable(
"now",
SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.map_or(0, |d| d.as_secs()),
)
.set_variable(
"asn",
self.data
.asn_geo_data
.asn
.as_ref()
.map(|r| r.id)
.unwrap_or_default(),
)
.set_variable(
"country",
self.data
.asn_geo_data
.country
.as_ref()
.map(|r| r.as_str())
.unwrap_or_default(),
)
.set_variable(
"spf.result",
self.data
.spf_mail_from
.as_ref()
.map(|r| r.result().as_str())
.unwrap_or_default(),
)
.set_variable(
"spf_ehlo.result",
self.data
.spf_ehlo
.as_ref()
.map(|r| r.result().as_str())
.unwrap_or_default(),
)
.set_variable("tls.version", tls_version)
.set_variable("tls.cipher", tls_cipher)
.set_variable("stage", stage);
if let Some(ip_rev) = &self.data.iprev {
params = params.set_variable("iprev.result", ip_rev.result().as_str());
if let Some(ptr) = ip_rev.ptr.as_ref().and_then(|addrs| addrs.first()) {
params = params.set_variable(
"iprev.ptr",
ptr.strip_suffix('.').unwrap_or(ptr).to_lowercase(),
);
}
}
if let Some(mail_from) = &self.data.mail_from {
params
.envelope
.push((Envelope::From, mail_from.address_lcase.to_string().into()));
if let Some(env_id) = &mail_from.dsn_info {
params
.envelope
.push((Envelope::Envid, env_id.as_str().to_lowercase().into()));
}
if stage != "data" {
if let Some(rcpt) = self.data.rcpt_to.last() {
params
.envelope
.push((Envelope::To, rcpt.address_lcase.to_string().into()));
if let Some(orcpt) = &rcpt.dsn_info {
params
.envelope
.push((Envelope::Orcpt, orcpt.as_str().to_lowercase().into()));
}
}
} else {
// Build recipients list
let mut recipients = Vec::with_capacity(self.data.rcpt_to.len());
let mut orcpts = Vec::with_capacity(self.data.rcpt_to.len());
let mut has_orcpts = false;
for rcpt in &self.data.rcpt_to {
recipients.push(Variable::from(rcpt.address_lcase.to_string()));
orcpts.push(match &rcpt.dsn_info {
Some(orcpt) => {
has_orcpts = true;
Variable::from(orcpt.as_str().to_lowercase())
}
None => Variable::default(),
});
}
params.envelope.push((Envelope::To, recipients.into()));
if has_orcpts {
params.envelope.push((Envelope::Orcpt, orcpts.into()));
}
}
if (mail_from.flags & MAIL_RET_FULL) != 0 {
params.envelope.push((Envelope::Ret, "FULL".into()));
} else if (mail_from.flags & MAIL_RET_HDRS) != 0 {
params.envelope.push((Envelope::Ret, "HDRS".into()));
}
if (mail_from.flags & MAIL_BY_NOTIFY) != 0 {
params.envelope.push((Envelope::ByMode, "N".into()));
} else if (mail_from.flags & MAIL_BY_RETURN) != 0 {
params.envelope.push((Envelope::ByMode, "R".into()));
}
if (mail_from.flags & MAIL_BODY_7BIT) != 0 {
params = params.set_variable("param.body", "7bit");
} else if (mail_from.flags & MAIL_BODY_8BITMIME) != 0 {
params = params.set_variable("param.body", "8bitmime");
} else if (mail_from.flags & MAIL_BODY_BINARYMIME) != 0 {
params = params.set_variable("param.body", "binarymime");
}
if (mail_from.flags & MAIL_SMTPUTF8) != 0 {
params = params.set_variable("param.smtputf8", Variable::Integer(1));
}
if (mail_from.flags & MAIL_REQUIRETLS) != 0 {
params = params.set_variable("param.requiretls", Variable::Integer(1));
}
}
params
}
pub async fn run_script(
&self,
script_id: String,
script: Arc<Sieve>,
params: ScriptParameters<'_>,
) -> ScriptResult {
Box::pin(
self.server.run_script(
script_id,
script,
params
.with_session_id(self.data.session_id)
.with_envelope(&self.server, self, self.data.session_id)
.await,
),
)
.await
}
}
+136
View File
@@ -0,0 +1,136 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::borrow::Cow;
use ahash::AHashMap;
use common::{
Server, auth::AccessToken, expr::functions::ResolveVariable, scripts::ScriptModification,
};
use mail_parser::Message;
use sieve::{Envelope, SpamStatus, runtime::Variable};
pub mod envelope;
pub mod event_loop;
pub mod exec;
#[derive(Debug, serde::Serialize)]
pub enum ScriptResult {
Accept {
modifications: Vec<ScriptModification>,
},
Replace {
message: Vec<u8>,
modifications: Vec<ScriptModification>,
},
Reject(String),
Discard,
}
pub struct ScriptParameters<'x> {
message: Option<Message<'x>>,
headers: Option<&'x [u8]>,
variables: AHashMap<Cow<'static, str>, Variable>,
envelope: Vec<(Envelope, Variable)>,
from_addr: String,
from_name: String,
return_path: String,
sign_domain: Option<String>,
access_token: Option<&'x AccessToken>,
spam_status: Option<SpamStatus>,
session_id: u64,
}
impl<'x> ScriptParameters<'x> {
pub fn new() -> Self {
ScriptParameters {
variables: AHashMap::with_capacity(10),
envelope: Vec::with_capacity(6),
message: None,
headers: None,
from_addr: Default::default(),
from_name: Default::default(),
return_path: Default::default(),
sign_domain: Default::default(),
access_token: None,
spam_status: None,
session_id: Default::default(),
}
}
pub async fn with_envelope(
mut self,
server: &Server,
vars: &impl ResolveVariable,
session_id: u64,
) -> Self {
for (variable, expr) in [
(&mut self.from_addr, &server.core.sieve.from_addr),
(&mut self.from_name, &server.core.sieve.from_name),
(&mut self.return_path, &server.core.sieve.return_path),
] {
if let Some(value) = server.eval_if(expr, vars, session_id).await {
*variable = value;
}
}
self.sign_domain = server
.eval_if(&server.core.sieve.sign, vars, session_id)
.await;
self
}
pub fn with_message(self, message: Message<'x>) -> Self {
Self {
message: message.into(),
..self
}
}
pub fn with_auth_headers(self, headers: &'x [u8]) -> Self {
Self {
headers: headers.into(),
..self
}
}
pub fn with_spam_status(self, status: SpamStatus) -> Self {
Self {
spam_status: status.into(),
..self
}
}
pub fn set_variable(
mut self,
name: impl Into<Cow<'static, str>>,
value: impl Into<Variable>,
) -> Self {
self.variables.insert(name.into(), value.into());
self
}
pub fn set_envelope(mut self, envelope: Envelope, value: impl Into<Variable>) -> Self {
self.envelope.push((envelope, value.into()));
self
}
pub fn with_access_token(mut self, access_token: &'x AccessToken) -> Self {
self.access_token = Some(access_token);
self
}
pub fn with_session_id(mut self, session_id: u64) -> Self {
self.session_id = session_id;
self
}
}
impl Default for ScriptParameters<'_> {
fn default() -> Self {
Self::new()
}
}