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,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 => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(¶ms.from_addr)
|
||||
.with_user_full_name(¶ms.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) = ¶ms.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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user