Rename the identifiers that carried the upstream name
ci / fork-checks (pull_request) Successful in 16s
ci / build (pull_request) Successful in 7m53s

Everything clients, users and operators meet now carries the fork's name,
with no aliases (SPEC.md §2.4, changed here from "protocol identifiers
stay"):

- JMAP: upstream's registry capability is urn:inbuxa:jmap:registry, beside
  the fork's own urn:inbuxa:jmap.
- WebDAV lock and sync tokens are urn:inbuxa:dav*; clients resync once.
- Sieve: vnd.inbuxa.while and vnd.inbuxa.expressions. sieve-rs spells these
  into its compiler, so it's vendored (vendor/sieve-rs, 0.7.3) and patched in;
  a unit test fails if Cargo.lock ever moves past the vendored copy. The
  trusted runtime now names itself too, rather than answering sieve-rs's
  default.
- The web interface's OAuth client is inbuxa-webui. On every start the old
  stalwart-webui client is removed and any application naming it is moved
  over.
- The spam filter's blobs are INBUXA_SPAM_*; every start moves any left
  under the old keys, so a trained model survives.
- SQL stores and log files default to inbuxa, in the code and in the
  schema served to the admin (checksum regenerated).
- Settings are INBUXA_* only. A STALWART_* variable that's set where its
  INBUXA_* one isn't stops the server at startup, naming it.
- The version-upgrade messages link docs.inbuxa.org's migration page, and
  the OpenAPI description, smtp crate metadata and web-push test fixtures
  lose the name.

Kept on purpose, allowlisted with reasons: the OAuth key-derivation
contexts (renaming them would end every session and invalidate every
sealed client id) and the hashed application prefix.

Also fixes a latent start-up failure: ensure_client updated an existing
first-party client with a revision of 0, which the registry's assertion
never matches, so adding a redirect URI or changing the webmail secret
failed start-up. And the principal session test now expects
legacyProtocols (C-1, added 2026-09-21), which it had missed.

Tested: the server builds without warnings; common's 106 unit tests,
including the vendoring check; a new integration test for the two
start-up migrations; and the webdav, jmap, imap and SMTP Sieve suites.
This commit is contained in:
2026-09-22 19:33:02 -07:00
parent 4799d191a0
commit cc6f1eb298
129 changed files with 20504 additions and 168 deletions
+102
View File
@@ -0,0 +1,102 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use mail_parser::{
Encoding, Header, HeaderName, HeaderValue, MimeHeaders, PartType,
decoders::html::{html_to_text, text_to_html},
};
use crate::{
Context, compiler::grammar::actions::action_convert::Convert, runtime::tests::TestResult,
};
#[derive(Clone, Copy)]
enum Conversion {
TextToHtml,
TextPlainToHtml,
HtmlToText,
}
impl Convert {
pub(crate) fn exec(&self, ctx: &mut Context) -> TestResult {
let _from_media_type = ctx.eval_value(&self.from_media_type);
let _to_media_type = ctx.eval_value(&self.to_media_type);
let from_media_type = _from_media_type.to_string();
let to_media_type = _to_media_type.to_string();
if from_media_type.eq_ignore_ascii_case(to_media_type.as_ref()) {
return TestResult::Bool(false ^ self.is_not);
}
let conversion = if (from_media_type.eq_ignore_ascii_case("text")
|| from_media_type.starts_with("text/"))
&& to_media_type.eq_ignore_ascii_case("text/html")
{
if from_media_type.eq_ignore_ascii_case("text") {
Conversion::TextPlainToHtml
} else {
Conversion::TextToHtml
}
} else if from_media_type.eq_ignore_ascii_case("text/html")
&& to_media_type.eq_ignore_ascii_case("text/plain")
{
Conversion::HtmlToText
} else {
return TestResult::Bool(false ^ self.is_not);
};
let mut did_convert = false;
for part in ctx.message.parts.iter_mut() {
let (new_body, ct) = match (&part.body, conversion) {
(PartType::Html(html), Conversion::HtmlToText) => (
PartType::Text(html_to_text(html.as_ref()).into()),
"text/plain; charset=utf8",
),
(PartType::Text(text), Conversion::TextToHtml) => (
PartType::Html(text_to_html(text.as_ref()).into()),
"text/html; charset=utf8",
),
(PartType::Text(text), Conversion::TextPlainToHtml)
if part
.content_type()
.and_then(|ct| ct.c_subtype.as_ref())
.is_some_and(|st| st.eq_ignore_ascii_case("plain")) =>
{
(
PartType::Html(text_to_html(text.as_ref()).into()),
"text/html; charset=utf8",
)
}
_ => {
continue;
}
};
part.headers = vec![Header {
name: HeaderName::Other("Content-Type".into()),
value: HeaderValue::Text(ct.to_string().into()),
offset_start: 0,
offset_end: 0,
offset_field: 0,
}];
ctx.message_size = ctx.message_size + ct.len() + new_body.len() + 16
- (if part.offset_body != 0 {
(part.offset_end - part.offset_header) as usize
} else {
part.body.len()
});
part.offset_body = 0;
part.body = new_body;
part.encoding = Encoding::QuotedPrintable; //Used as non-mime flag
did_convert = true;
}
if did_convert {
ctx.has_changes = true;
}
TestResult::Bool(did_convert ^ self.is_not)
}
}
+184
View File
@@ -0,0 +1,184 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::borrow::Cow;
use mail_parser::{Header, HeaderName, HeaderValue};
use crate::{
Context,
compiler::grammar::{
MatchType,
actions::{
action_editheader::{AddHeader, DeleteHeader},
action_mime::MimeOpts,
},
},
};
impl AddHeader {
pub(crate) fn exec(&self, ctx: &mut Context) {
let header_name__ = ctx.eval_value(&self.field_name);
let header_name_ = header_name__.to_string();
let mut header_name = String::with_capacity(header_name_.len());
for ch in header_name_.chars() {
if ch.is_alphanumeric() || ch == '-' {
header_name.push(ch);
}
}
if !header_name.is_empty()
&& let Some(header_name) = HeaderName::parse(header_name)
&& !ctx.runtime.protected_headers.contains(&header_name)
{
ctx.has_changes = true;
ctx.insert_header(
ctx.part,
header_name,
ctx.eval_value(&self.value)
.to_string()
.as_ref()
.remove_crlf(ctx.runtime.max_header_size),
self.last,
)
}
}
}
impl DeleteHeader {
pub(crate) fn exec(&self, ctx: &mut Context) {
let header_name__ = ctx.eval_value(&self.field_name);
let header_name_ = header_name__.to_string();
let header_name = if let Some(header_name) = HeaderName::parse(header_name_.as_ref()) {
header_name
} else {
return;
};
let value_patterns = ctx.eval_values(&self.value_patterns);
let mut deleted_headers = Vec::new();
let mut deleted_bytes = 0;
if ctx.runtime.protected_headers.contains(&header_name) {
return;
}
ctx.find_headers(
&[header_name],
self.index,
self.mime_anychild,
|header, part_id, header_pos| {
if !value_patterns.is_empty() {
let did_match = ctx.find_header_values(header, &MimeOpts::None, |value| {
for (pattern_expr, pattern) in
value_patterns.iter().zip(self.value_patterns.iter())
{
if match &self.match_type {
MatchType::Is => self.comparator.is(&value, pattern_expr),
MatchType::Contains => self
.comparator
.contains(value, pattern_expr.to_string().as_ref()),
MatchType::Value(rel_match) => {
self.comparator.relational(rel_match, &value, pattern_expr)
}
MatchType::Matches(_) => self.comparator.matches(
value,
pattern_expr.to_string().as_ref(),
0,
&mut Vec::new(),
),
MatchType::Regex(_) => self.comparator.regex(
pattern,
pattern_expr,
value,
0,
&mut Vec::new(),
),
MatchType::Count(_) => false,
MatchType::List => false,
} {
return true;
}
}
false
});
if !did_match {
return false;
}
}
if header.offset_end != 0 {
deleted_bytes += (header.offset_end - header.offset_field) as usize;
} else {
deleted_bytes += header.name.as_str().len() + header.value.len() + 4;
}
deleted_headers.push((part_id, header_pos));
false
},
);
if !deleted_headers.is_empty() {
ctx.has_changes = true;
for (part_id, header_pos) in deleted_headers.iter().rev() {
ctx.message.parts[*part_id as usize]
.headers
.remove(*header_pos);
}
}
ctx.message_size -= deleted_bytes;
}
}
pub(crate) trait RemoveCrLf {
fn remove_crlf(&self, max_len: usize) -> String;
}
impl RemoveCrLf for &str {
fn remove_crlf(&self, max_len: usize) -> String {
let mut header_value = String::with_capacity(self.len());
for ch in self.chars() {
if !['\n', '\r'].contains(&ch) {
if header_value.len() + ch.len_utf8() <= max_len {
header_value.push(ch);
} else {
return header_value;
}
}
}
header_value
}
}
impl<'x> Context<'x> {
pub(crate) fn insert_header(
&mut self,
part_id: u32,
header_name: HeaderName<'x>,
header_value: impl Into<Cow<'static, str>>,
last: bool,
) {
let header_value = header_value.into();
self.message_size += header_name.len() + header_value.len() + 4;
let header = Header {
name: header_name,
value: HeaderValue::Text(header_value),
offset_start: 0,
offset_end: 0,
offset_field: 0,
};
if !last {
self.message.parts[part_id as usize]
.headers
.insert(0, header);
} else {
self.message.parts[part_id as usize].headers.push(header);
}
}
}
+40
View File
@@ -0,0 +1,40 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{Context, Event, compiler::grammar::actions::action_fileinto::FileInto};
impl FileInto {
pub(crate) fn exec(&self, ctx: &mut Context) {
let folder = ctx.eval_value(&self.folder).to_string().into_owned();
let mut events = Vec::with_capacity(2);
if let Some(event) = ctx.build_message_id() {
events.push(event);
}
if !self.copy
&& !matches!(&ctx.final_event, Some(Event::Keep { flags, .. }) if !flags.is_empty())
{
ctx.final_event = None;
}
events.push(Event::FileInto {
folder,
flags: ctx.get_local_or_global_flags(&self.flags),
mailbox_id: self
.mailbox_id
.as_ref()
.map(|mi| ctx.eval_value(mi).to_string().into_owned()),
special_use: self
.special_use
.as_ref()
.map(|su| ctx.eval_value(su).to_string().into_owned()),
create: self.create,
message_id: ctx.main_message_id,
});
ctx.queued_events = events.into_iter();
}
}
+140
View File
@@ -0,0 +1,140 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
Context,
compiler::{
Value, VariableType,
grammar::actions::action_flags::{Action, EditFlags},
},
};
impl EditFlags {
pub(crate) fn exec(&self, ctx: &mut Context) {
let mut var_name_ = None;
let var_name = self.name.as_ref().unwrap_or_else(|| {
var_name_.get_or_insert_with(|| VariableType::Global("__flags".to_string()))
});
match &self.action {
Action::Set => {
let mut flags_lc = Vec::new();
let mut flags = String::new();
ctx.tokenize_flags(&self.flags, |flag| {
let flag_lc = flag.to_lowercase();
if !flags_lc.contains(&flag_lc) {
if !flags.is_empty() {
flags.push(' ');
}
flags.push_str(flag);
flags_lc.push(flag_lc);
}
false
});
ctx.set_variable(var_name, flags.into());
}
Action::Add => {
let mut new_flags = ctx
.get_variable(var_name)
.map(|v| v.to_string())
.unwrap_or_default()
.into_owned();
let mut current_flags = new_flags
.split(' ')
.map(|f| f.to_lowercase())
.collect::<Vec<_>>();
ctx.tokenize_flags(&self.flags, |flag| {
let flag_lc = flag.to_lowercase();
if !current_flags.contains(&flag_lc) {
if !new_flags.is_empty() {
new_flags.push(' ');
}
new_flags.push_str(flag);
current_flags.push(flag_lc);
}
false
});
ctx.set_variable(var_name, new_flags.into());
}
Action::Remove => {
let mut current_flags = Vec::new();
let mut current_flags_lc = Vec::new();
let flags = ctx
.get_variable(var_name)
.map(|v| v.to_string().into_owned())
.unwrap_or_default();
for flag in flags.split(' ') {
current_flags.push(flag);
current_flags_lc.push(flag.to_lowercase());
}
ctx.tokenize_flags(&self.flags, |flag| {
let flag = flag.to_lowercase();
if let Some(pos) = current_flags_lc.iter().position(|lflag| lflag == &flag) {
current_flags.swap_remove(pos);
current_flags_lc.swap_remove(pos);
}
false
});
ctx.set_variable(var_name, current_flags.join(" ").into());
}
}
}
}
impl Context<'_> {
pub(crate) fn tokenize_flags(
&self,
strings: &[Value],
mut cb: impl FnMut(&str) -> bool,
) -> bool {
for (pos, string) in strings.iter().enumerate() {
let flag_ = self.eval_value(string);
let flag = flag_.to_string();
if !flag.is_empty() {
if pos == 0 && strings.len() == 1 {
for flag in flag.split_ascii_whitespace() {
if !flag.is_empty() && cb(flag) {
return true;
}
}
} else if cb(flag.trim()) {
return true;
}
}
}
false
}
pub(crate) fn get_local_flags(&self, strings: &[Value]) -> Vec<String> {
let mut flags = Vec::new();
self.tokenize_flags(strings, |flag| {
flags.push(flag.to_string());
false
});
flags
}
pub(crate) fn get_global_flags(&self) -> Vec<String> {
match self.vars_global.get("__flags") {
Some(flags) if !flags.is_empty() => flags
.to_string()
.split(' ')
.map(|s| s.to_string())
.collect::<Vec<String>>(),
_ => Vec::new(),
}
}
pub(crate) fn get_local_or_global_flags(&self, strings: &[Value]) -> Vec<String> {
if strings.is_empty() {
self.get_global_flags()
} else {
self.get_local_flags(strings)
}
}
}
+53
View File
@@ -0,0 +1,53 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::sync::Arc;
use crate::{
Context, Event, Script, Sieve,
compiler::grammar::actions::action_include::{Include, Location},
runtime::RuntimeError,
};
pub(crate) enum IncludeResult {
Cached(Arc<Sieve>),
Event(Event),
Error(RuntimeError),
None,
}
impl Include {
pub(crate) fn exec(&self, ctx: &Context) -> IncludeResult {
let script_name = ctx.eval_value(&self.value);
if !script_name.is_empty() {
let script_name = if self.location == Location::Global {
Script::Global(script_name.to_string().into_owned())
} else {
Script::Personal(script_name.to_string().into_owned())
};
let cached_script = ctx.script_cache.get(&script_name);
if !self.once || cached_script.is_none() {
if ctx.script_stack.len() < ctx.runtime.max_nested_includes {
if let Some(script) = cached_script
.or_else(|| ctx.runtime.include_scripts.get(script_name.as_str()))
{
return IncludeResult::Cached(script.clone());
} else {
return IncludeResult::Event(Event::IncludeScript {
name: script_name,
optional: self.optional,
});
}
} else {
return IncludeResult::Error(RuntimeError::TooManyIncludes);
}
}
}
IncludeResult::None
}
}
+582
View File
@@ -0,0 +1,582 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::cmp::Reverse;
use mail_parser::{
Encoding, HeaderName, Message, MessagePart, PartType, decoders::html::html_to_text,
};
use crate::{
Context, Event,
compiler::{
VariableType,
grammar::actions::action_mime::{Enclose, ExtractText, Replace},
},
};
use super::action_editheader::RemoveCrLf;
#[cfg(not(test))]
use mail_builder::headers::message_id::generate_message_id_header;
impl Replace {
pub(crate) fn exec(&self, ctx: &mut Context) {
// Delete children parts
let mut part_ids = ctx.find_nested_parts_ids(false);
part_ids.sort_unstable_by_key(|a| Reverse(*a));
for part_id in part_ids {
ctx.message.parts.remove(part_id as usize);
}
ctx.has_changes = true;
// Update part
let body = ctx.eval_value(&self.replacement).to_string().into_owned();
let body_len = body.len();
let part = &mut ctx.message.parts[ctx.part as usize];
ctx.message_size = ctx.message_size + body_len
- (if part.offset_body != 0 {
(part.offset_end - part.offset_header) as usize
} else {
part.body.len()
});
part.body = PartType::Text(body.into());
part.encoding = if !self.mime {
Encoding::QuotedPrintable
} else {
Encoding::None
};
part.offset_body = 0;
let prev_headers = std::mem::take(&mut part.headers);
let mut add_date = true;
let mut has_original_from = false;
if ctx.part == 0 {
for mut header in prev_headers {
let mut size = (header.offset_end - header.offset_field) as usize;
match &header.name {
HeaderName::Subject => {
if self.subject.is_some() {
header.name = HeaderName::Other("Original-Subject".into());
header.offset_field = header.offset_start;
size += "Original-".len();
}
}
HeaderName::From => {
if self.from.is_some() {
header.name = HeaderName::Other("Original-From".into());
header.offset_field = header.offset_start;
size += "Original-".len();
} else {
has_original_from = true;
}
}
HeaderName::To | HeaderName::Cc | HeaderName::Bcc | HeaderName::Received => (),
HeaderName::Date => {
add_date = false;
}
_ => continue,
}
ctx.message_size += size;
part.headers.push(header);
}
// Add From
let mut add_from = true;
if let Some(from) = self.from.as_ref().map(|f| ctx.eval_value(f))
&& !from.is_empty()
{
ctx.insert_header(
0,
HeaderName::Other("From".into()),
from.to_string()
.as_ref()
.remove_crlf(ctx.runtime.max_header_size),
true,
);
add_from = false;
}
if add_from && !has_original_from {
ctx.insert_header(
0,
HeaderName::Other("From".to_string().into()),
ctx.user_from_field(),
true,
);
}
// Add Subject
if let Some(subject) = self.subject.as_ref().map(|f| ctx.eval_value(f))
&& !subject.is_empty()
{
ctx.insert_header(
0,
HeaderName::Other("Subject".into()),
subject
.to_string()
.as_ref()
.remove_crlf(ctx.runtime.max_header_size),
true,
);
}
// Add Date
if add_date {
#[cfg(not(test))]
let header_value = mail_builder::headers::date::Date::now().to_rfc822();
#[cfg(test)]
let header_value = "Tue, 20 Nov 2022 05:14:20 -0300".to_string();
ctx.insert_header(
0,
HeaderName::Other("Date".to_string().into()),
header_value,
true,
);
}
// Add Message-ID
let mut header_value = Vec::with_capacity(20);
#[cfg(not(test))]
generate_message_id_header(&mut header_value, &ctx.runtime.local_hostname).unwrap();
#[cfg(test)]
header_value.extend_from_slice(b"<auto-generated@message-id>");
ctx.insert_header(
0,
HeaderName::Other("Message-ID".to_string().into()),
String::from_utf8(header_value).unwrap(),
true,
);
}
if !self.mime {
ctx.insert_header(
ctx.part,
HeaderName::Other("Content-Type".into()),
"text/plain; charset=utf-8".to_string(),
true,
);
}
}
}
impl Enclose {
pub(crate) fn exec(&self, ctx: &mut Context) {
let body = ctx.eval_value(&self.value).to_string().into_owned();
let subject = self
.subject
.as_ref()
.map(|s| {
ctx.eval_value(s)
.to_string()
.as_ref()
.remove_crlf(ctx.runtime.max_header_size)
})
.or_else(|| ctx.message.subject().map(|s| s.to_string()))
.unwrap_or_default();
let message = std::mem::take(&mut ctx.message);
#[cfg(test)]
let boundary = make_test_boundary();
#[cfg(not(test))]
let boundary = mail_builder::mime::make_boundary(".");
ctx.message_size += ((boundary.len() + 6) * 3) + body.len() + 2;
ctx.part = 0;
ctx.has_changes = true;
ctx.message = Message {
html_body: Vec::with_capacity(0),
text_body: Vec::with_capacity(0),
attachments: Vec::with_capacity(0),
parts: vec![
MessagePart {
headers: vec![],
is_encoding_problem: false,
body: PartType::Multipart(vec![1, 2]),
encoding: Encoding::None,
offset_header: 0,
offset_body: 0,
offset_end: 0,
},
MessagePart {
headers: vec![],
is_encoding_problem: false,
body: PartType::Text(body.into()),
encoding: Encoding::QuotedPrintable, // Flag non-mime part
offset_header: 0,
offset_body: 0,
offset_end: 0,
},
MessagePart {
headers: vec![],
is_encoding_problem: false,
body: PartType::Message(message),
encoding: Encoding::QuotedPrintable, // Flag non-mime part
offset_header: 0,
offset_body: 0,
offset_end: 0,
},
],
raw_message: b""[..].into(),
};
ctx.insert_header(
0,
HeaderName::Other("Content-Type".into()),
format!("multipart/mixed; boundary=\"{boundary}\""),
true,
);
ctx.insert_header(0, HeaderName::Other("Subject".into()), subject, true);
ctx.insert_header(
1,
HeaderName::Other("Content-Type".into()),
"text/plain; charset=utf-8",
true,
);
ctx.insert_header(
2,
HeaderName::Other("Content-Type".into()),
"message/rfc822",
true,
);
let mut add_date = true;
let mut add_message_id = true;
let mut add_from = true;
for header in &self.headers {
let header = ctx.eval_value(header);
if let Some((mut header_name, mut header_value)) =
header.to_string().as_ref().split_once(':')
{
header_name = header_name.trim();
header_value = header_value.trim();
if !header_value.is_empty()
&& let Some(name) = HeaderName::parse(header_name)
&& !ctx.runtime.protected_headers.contains(&name)
{
match &name {
HeaderName::Date => {
add_date = false;
}
HeaderName::From => {
add_from = false;
}
HeaderName::MessageId => {
add_message_id = false;
}
_ => (),
}
ctx.insert_header(
0,
HeaderName::Other(header_name.to_string().into()),
header_value.remove_crlf(ctx.runtime.max_header_size),
true,
);
}
}
}
if add_from {
ctx.insert_header(
0,
HeaderName::Other("From".to_string().into()),
ctx.user_from_field(),
true,
);
}
if add_date {
#[cfg(not(test))]
let header_value = mail_builder::headers::date::Date::now().to_rfc822();
#[cfg(test)]
let header_value = "Tue, 20 Nov 2022 05:14:20 -0300".to_string();
ctx.insert_header(
0,
HeaderName::Other("Date".to_string().into()),
header_value,
true,
);
}
if add_message_id {
let mut header_value = Vec::with_capacity(20);
#[cfg(not(test))]
generate_message_id_header(&mut header_value, &ctx.runtime.local_hostname).unwrap();
#[cfg(test)]
header_value.extend_from_slice(b"<auto-generated@message-id>");
ctx.insert_header(
0,
HeaderName::Other("Message-ID".to_string().into()),
String::from_utf8(header_value).unwrap(),
true,
);
}
}
}
impl ExtractText {
pub(crate) fn exec(&self, ctx: &mut Context) {
let mut value = String::new();
if !ctx.part_iter_stack.is_empty() {
match ctx.message.parts.get(ctx.part as usize).map(|p| &p.body) {
Some(PartType::Text(text)) => {
value = if let Some(first) = &self.first {
text.chars().take(*first).collect()
} else {
text.as_ref().to_string()
};
}
Some(PartType::Html(html)) => {
value = if let Some(first) = &self.first {
html_to_text(html.as_ref()).chars().take(*first).collect()
} else {
html_to_text(html.as_ref())
};
}
_ => (),
}
if !self.modifiers.is_empty() && !value.is_empty() {
for modifier in &self.modifiers {
value = modifier.apply(&value, ctx);
}
}
}
match &self.name {
VariableType::Local(var_id) => {
if let Some(var) = ctx.vars_local.get_mut(*var_id) {
*var = value.into();
} else {
debug_assert!(false, "Non-existent local variable {var_id}");
}
}
VariableType::Global(var_name) => {
ctx.vars_global
.insert(var_name.to_string().into(), value.into());
}
VariableType::Envelope(env) => {
ctx.add_set_envelope_event(*env, value);
}
_ => (),
}
}
}
enum StackItem<'x> {
Message(&'x Message<'x>),
Boundary(&'x str),
None,
}
impl Context<'_> {
pub(crate) fn build_message_id(&mut self) -> Option<Event> {
if self.has_changes {
self.last_message_id += 1;
self.main_message_id = self.last_message_id;
self.has_changes = false;
let message = self.build_message();
Some(Event::CreatedMessage {
message_id: self.main_message_id,
message,
})
} else {
None
}
}
pub(crate) fn build_message(&mut self) -> Vec<u8> {
let mut current_message = &self.message;
let mut current_boundary = "";
let mut message = Vec::with_capacity(self.message_size);
let mut iter = [0u32].iter();
let mut iter_stack = Vec::new();
let mut last_offset = 0;
'outer: loop {
while let Some(part) = iter
.next()
.and_then(|p| current_message.parts.get(*p as usize))
{
if last_offset > 0 {
message.extend_from_slice(
&current_message.raw_message
[last_offset as usize..part.offset_header as usize],
);
} else if !current_boundary.is_empty()
&& part.offset_end == 0
&& !matches!(iter_stack.last(), Some((StackItem::Message(_), _, _)))
{
message.extend_from_slice(b"\r\n--");
message.extend_from_slice(current_boundary.as_bytes());
message.extend_from_slice(b"\r\n");
}
let mut ct_pos = usize::MAX;
for (header_pos, header) in part.headers.iter().enumerate() {
if header.offset_end != 0 {
if header.offset_field != header.offset_start {
message.extend_from_slice(
&current_message.raw_message
[header.offset_field as usize..header.offset_end as usize],
);
} else {
// Renamed header
message.extend_from_slice(header.name.as_str().as_bytes());
message.extend_from_slice(b":");
message.extend_from_slice(
&current_message.raw_message
[header.offset_start as usize..header.offset_end as usize],
);
}
} else {
if header.name == HeaderName::Other("Content-Type".into()) {
ct_pos = header_pos;
}
message.extend_from_slice(header.name.as_str().as_bytes());
message.extend_from_slice(b": ");
message.extend_from_slice(header.value.as_text().unwrap_or("").as_bytes());
message.extend_from_slice(b"\r\n");
}
}
if part.offset_body != 0 || part.encoding != Encoding::None {
// Add CRLF unless this is a :mime replaced part
message.extend_from_slice(b"\r\n");
}
if part.offset_body != 0 {
// Original message part
if let PartType::Multipart(subparts) = &part.body {
// Multiparts contain offsets of the entire part, do not add.
iter_stack.push((
StackItem::None,
part,
std::mem::replace(&mut iter, subparts.iter()),
));
last_offset = part.offset_body;
continue 'outer;
} else {
message.extend_from_slice(
&current_message.raw_message
[part.offset_body as usize..part.offset_end as usize],
)
}
} else {
match &part.body {
PartType::Message(nested_message) => {
// Enclosed message
iter_stack.push((
StackItem::Message(current_message),
part,
std::mem::replace(&mut iter, [0].iter()),
));
current_message = nested_message;
continue 'outer;
}
PartType::Multipart(subparts) => {
// Multipart enclosing nested message, obtain MIME boundary
let prev_boundary = std::mem::replace(
&mut current_boundary,
if ct_pos != usize::MAX {
part.headers[ct_pos]
.value
.as_text()
.and_then(|h| h.split_once("boundary=\""))
.and_then(|(_, h)| h.split_once('\"'))
.map(|(h, _)| h)
} else {
None
}
.unwrap_or("invalid-boundary"),
);
// Enclose multipart
iter_stack.push((
StackItem::Boundary(prev_boundary),
part,
std::mem::replace(&mut iter, subparts.iter()),
));
continue 'outer;
}
_ => {
// Replaced part
message.extend_from_slice(part.contents());
}
}
}
last_offset = part.offset_end;
}
if let Some((prev_item, prev_part, prev_iter)) = iter_stack.pop() {
match prev_item {
StackItem::Message(prev_message) => {
if last_offset > 0 {
if let Some(bytes) =
current_message.raw_message.get(last_offset as usize..)
{
message.extend_from_slice(bytes);
}
last_offset = 0;
}
current_message = prev_message;
}
StackItem::Boundary(prev_boundary) => {
if !current_boundary.is_empty() {
message.extend_from_slice(b"\r\n--");
message.extend_from_slice(current_boundary.as_bytes());
message.extend_from_slice(b"--\r\n");
}
current_boundary = prev_boundary;
}
StackItem::None => {
message.extend_from_slice(
&current_message.raw_message
[last_offset as usize..prev_part.offset_end as usize],
);
last_offset = prev_part.offset_end;
}
}
iter = prev_iter;
} else {
break;
}
}
if last_offset > 0
&& let Some(bytes) = current_message.raw_message.get(last_offset as usize..)
{
message.extend_from_slice(bytes);
}
message
}
}
#[cfg(test)]
thread_local!(static COUNTER: std::cell::Cell<u64> = 0.into());
#[cfg(test)]
pub(crate) fn make_test_boundary() -> String {
format!("boundary_{}", COUNTER.with(|c| { c.replace(c.get() + 1) }))
}
#[cfg(test)]
pub(crate) fn reset_test_boundary() {
COUNTER.with(|c| c.replace(0));
}
+575
View File
@@ -0,0 +1,575 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use mail_builder::headers::{date::Date, message_id::generate_message_id_header};
use mail_parser::{HeaderName, decoders::quoted_printable::HEX_MAP};
use crate::{
Context, Event, Importance, Recipient,
compiler::grammar::actions::{
action_notify::Notify,
action_redirect::{ByTime, Ret},
},
};
use super::action_vacation::MAX_SUBJECT_LEN;
impl Notify {
pub(crate) fn exec(&self, ctx: &mut Context) {
// Do not notify on Auto-Submitted messages
for header in &ctx.message.parts[0].headers {
if matches!(&header.name, HeaderName::Other(name) if name.eq_ignore_ascii_case("Auto-Submitted"))
&& header
.value
.as_text()
.is_none_or(|v| !v.eq_ignore_ascii_case("no"))
{
return;
}
}
let uri = ctx.eval_value(&self.method).to_string().into_owned();
let (scheme, params) = if let Some(parts) = parse_uri(&uri) {
parts
} else {
return;
};
let has_fcc = self.fcc.is_some();
let is_mailto = scheme.eq_ignore_ascii_case("mailto")
&& ctx.num_out_messages < ctx.runtime.max_out_messages;
let mut events = Vec::with_capacity(3);
if is_mailto || has_fcc {
let params = if is_mailto {
if let Some(params) = parse_mailto(params) {
params
} else {
return;
}
} else {
MailtoMessage {
to: Vec::new(),
cc: Vec::new(),
bcc: Vec::new(),
body: None,
headers: Vec::new(),
}
};
let from = if let Some(from) = &self.from {
let from = ctx.eval_value(from).to_string().into_owned();
if from
.to_ascii_lowercase()
.contains(&ctx.user_address.to_ascii_lowercase())
{
from
} else {
ctx.user_from_field()
}
} else {
ctx.user_from_field()
};
let notify_message = self
.message
.as_ref()
.map(|m| ctx.eval_value(m).to_string().into_owned());
let message_len = params
.to
.iter()
.chain(params.cc.iter())
.map(|a| a.len() + 4)
.sum::<usize>()
+ params
.headers
.iter()
.map(|(h, v)| h.len() + v.len() + 4)
.sum::<usize>()
+ params.body.as_ref().map_or(0, |b| b.len())
+ notify_message.as_ref().map_or(0, |b| b.len())
+ from.len()
+ 200;
let mut message = Vec::with_capacity(message_len);
message.extend_from_slice(b"From: ");
message.extend_from_slice(from.as_bytes());
message.extend_from_slice(b"\r\n");
for (header, addresses) in [("To: ", &params.to), ("Cc: ", &params.cc)] {
if !addresses.is_empty() {
message.extend_from_slice(header.as_bytes());
for (pos, address) in addresses.iter().enumerate() {
if pos > 0 {
message.extend_from_slice(b", ");
}
if !address.contains('<') {
message.push(b'<');
}
message.extend_from_slice(address.as_bytes());
if !address.contains('<') {
message.push(b'>');
}
}
message.extend_from_slice(b"\r\n");
}
}
let mut has_subject = None;
let mut has_date = false;
let mut has_message_id = false;
for (header, value) in &params.headers {
match header {
HeaderName::Subject => {
has_subject = value.into();
continue;
}
HeaderName::Date => {
has_date = true;
}
HeaderName::MessageId => {
has_message_id = true;
}
HeaderName::From => {
continue;
}
_ => (),
}
message.extend_from_slice(header.as_str().as_bytes());
message.extend_from_slice(b": ");
message.extend_from_slice(value.as_bytes());
message.extend_from_slice(b"\r\n");
}
if !has_date {
message.extend_from_slice(b"Date: ");
message.extend_from_slice(Date::now().to_rfc822().as_bytes());
message.extend_from_slice(b"\r\n");
}
if !has_message_id {
message.extend_from_slice(b"Message-ID: ");
generate_message_id_header(&mut message, &ctx.runtime.local_hostname).unwrap();
message.extend_from_slice(b"\r\n");
}
let (importance, priority) =
self.importance
.as_ref()
.map_or(("Normal", "3 (Normal)"), |i| {
match ctx.eval_value(i).to_string().as_ref() {
"1" => ("High", "1 (High)"),
"3" => ("Low", "5 (Low)"),
_ => ("Normal", "3 (Normal)"),
}
});
message.extend_from_slice(b"Importance: ");
message.extend_from_slice(importance.as_bytes());
message.extend_from_slice(b"\r\n");
message.extend_from_slice(b"X-Priority: ");
message.extend_from_slice(priority.as_bytes());
message.extend_from_slice(b"\r\n");
message.extend_from_slice(b"Subject: ");
let subject = if let Some(subject) = has_subject {
subject.as_str()
} else if let Some(subject) = &notify_message {
subject.as_ref()
} else {
ctx.message.subject().unwrap_or_default()
};
let mut iter = subject.chars().enumerate();
let mut buf = [0; 4];
#[allow(clippy::while_let_on_iterator)]
while let Some((pos, char)) = iter.next() {
if pos < MAX_SUBJECT_LEN {
message.extend_from_slice(char.encode_utf8(&mut buf).as_bytes());
} else {
break;
}
}
if iter.next().is_some() {
message.extend_from_slice('…'.encode_utf8(&mut buf).as_bytes());
}
message.extend_from_slice(b"\r\n");
message.extend_from_slice(b"Auto-Submitted: auto-notified\r\n");
message.extend_from_slice(b"X-Sieve: yes\r\n");
message.extend_from_slice(b"Content-type: text/plain; charset=utf-8\r\n\r\n");
if let Some(body) = params.body {
message.extend_from_slice(body.as_bytes());
} else if let Some(subject) = &notify_message {
message.extend_from_slice(subject.as_bytes());
} else if let Some(subject) = ctx.message.subject() {
message.extend_from_slice(subject.as_bytes());
}
ctx.last_message_id += 1;
events.push(Event::CreatedMessage {
message_id: ctx.last_message_id,
message,
});
if is_mailto {
events.push(Event::SendMessage {
recipient: Recipient::Group(
params
.to
.into_iter()
.chain(params.cc)
.chain(params.bcc)
.map(|addr| {
if let Some((addr, _)) = addr
.rsplit_once('<')
.and_then(|(_, addr)| addr.rsplit_once('>'))
{
addr.to_string()
} else {
addr
}
})
.collect(),
),
notify: crate::compiler::grammar::actions::action_redirect::Notify::Never,
return_of_content: Ret::Default,
by_time: ByTime::None,
message_id: ctx.last_message_id,
});
}
}
if !is_mailto {
events.push(Event::Notify {
method: uri,
from: self
.from
.as_ref()
.map(|f| ctx.eval_value(f).to_string().into_owned()),
importance: self.importance.as_ref().map_or(Importance::Normal, |i| {
match ctx.eval_value(i).to_string().as_ref() {
"1" => Importance::High,
"3" => Importance::Low,
_ => Importance::Normal,
}
}),
options: ctx.eval_values_owned(&self.options),
message: self
.message
.as_ref()
.map(|m| ctx.eval_value(m).to_string().into_owned())
.or_else(|| ctx.message.subject().map(|s| s.to_string()))
.unwrap_or_default(),
});
ctx.num_out_messages += 1;
}
if let Some(fcc) = &self.fcc {
// File carbon copy
events.push(Event::FileInto {
folder: ctx.eval_value(&fcc.mailbox).to_string().into_owned(),
flags: ctx.get_local_flags(&fcc.flags),
mailbox_id: fcc
.mailbox_id
.as_ref()
.map(|m| ctx.eval_value(m).to_string().into_owned()),
special_use: fcc
.special_use
.as_ref()
.map(|s| ctx.eval_value(s).to_string().into_owned()),
create: fcc.create,
message_id: ctx.last_message_id,
});
}
ctx.queued_events = events.into_iter();
}
}
pub fn validate_from(addr: &str) -> bool {
let mut has_at = false;
let mut has_dot = false;
let mut in_quote = false;
let mut in_angle = false;
let mut last_ch = 0;
for &ch in addr.as_bytes().iter() {
match ch {
b'\"' if last_ch != b'\\' => {
in_quote = !in_quote;
}
b'<' if !in_quote => {
if !in_angle {
in_angle = true;
has_at = false;
has_dot = false;
} else {
return false;
}
}
b'>' if !in_quote => {
if in_angle {
in_angle = false;
} else {
return false;
}
}
b'@' if !in_quote => {
if !has_at && last_ch.is_ascii_alphanumeric() {
has_at = true;
} else {
return false;
}
}
b'.' if !in_quote && has_at => {
has_dot = true;
}
_ => (),
}
last_ch = ch;
}
has_dot && has_at && !in_angle
}
pub fn validate_uri(uri: &str) -> Option<&str> {
let (scheme, uri) = parse_uri(uri)?;
if scheme.eq_ignore_ascii_case("mailto") {
parse_mailto(uri)?;
scheme.into()
} else if ["xmpp", "tel", "http", "https"].contains(&scheme) {
scheme.into()
} else {
None
}
}
pub(crate) fn parse_uri(uri: &str) -> Option<(&str, &str)> {
let (scheme, uri) = uri.split_once(':')?;
if !uri.is_empty() {
Some((scheme, uri))
} else {
None
}
}
pub enum Mailto {
Header(HeaderName<'static>),
Body,
Other(String),
}
enum State {
Address((HeaderName<'static>, bool)),
ParamName,
ParamValue(Mailto),
}
#[derive(Default)]
struct MailtoMessage {
to: Vec<String>,
cc: Vec<String>,
bcc: Vec<String>,
body: Option<String>,
headers: Vec<(HeaderName<'static>, String)>,
}
fn parse_mailto(uri: &str) -> Option<MailtoMessage> {
let mut params = MailtoMessage::default();
let mut state = State::Address((HeaderName::To, false));
let mut buf = Vec::new();
let uri_ = uri.as_bytes();
let mut iter = uri_.iter();
let mut has_addresses = false;
while let Some(&ch) = iter.next() {
match ch {
b'%' => {
let hex1 = HEX_MAP[*iter.next()? as usize];
let hex2 = HEX_MAP[*iter.next()? as usize];
if hex1 != -1 && hex2 != -1 {
let ch = ((hex1 as u8) << 4) | hex2 as u8;
match &state {
State::Address((header, has_at)) => match ch {
b',' => {
if *has_at {
insert_address(
&mut params,
header.clone(),
String::from_utf8(std::mem::take(&mut buf)).ok()?,
);
has_addresses = true;
state = State::Address((header.clone(), false));
} else {
return None;
}
}
b'@' => {
if !*has_at {
state = State::Address((header.clone(), true));
buf.push(ch);
} else {
return None;
}
}
_ => {
buf.push(ch);
}
},
_ => buf.push(ch),
}
} else {
return None;
}
}
b',' => match &state {
State::Address((header, true)) => {
insert_address(
&mut params,
header.clone(),
String::from_utf8(std::mem::take(&mut buf)).ok()?,
);
state = State::Address((header.clone(), false));
has_addresses = true;
}
State::ParamValue(_) => buf.push(ch),
_ => return None,
},
b'?' => match &state {
State::Address((header, has_at)) if *has_at || buf.is_empty() => {
if !buf.is_empty() {
insert_address(
&mut params,
header.clone(),
String::from_utf8(std::mem::take(&mut buf)).ok()?,
);
has_addresses = true;
}
state = State::ParamName;
}
State::ParamValue(_) => buf.push(ch),
_ => return None,
},
b'@' => match &state {
State::Address((header, false)) if !buf.is_empty() => {
buf.push(ch);
state = State::Address((header.clone(), true));
}
State::ParamName | State::ParamValue(_) => buf.push(ch),
_ => return None,
},
b'=' => match &state {
State::ParamName if !buf.is_empty() => {
let param = String::from_utf8(std::mem::take(&mut buf)).ok()?;
state = HeaderName::parse(param)
.map(|hdr| match hdr {
HeaderName::To | HeaderName::Cc | HeaderName::Bcc => {
State::Address((hdr, false))
}
HeaderName::Other(param) => {
if param.eq_ignore_ascii_case("body") {
State::ParamValue(Mailto::Body)
} else {
State::ParamValue(Mailto::Other(param.into_owned()))
}
}
_ => State::ParamValue(Mailto::Header(hdr)),
})
.unwrap_or_else(|| State::ParamValue(Mailto::Other(String::new())));
}
State::ParamValue(_) => buf.push(ch),
_ => return None,
},
b'&' => match state {
State::Address((header, true)) => {
if !buf.is_empty() {
insert_address(
&mut params,
header,
String::from_utf8(std::mem::take(&mut buf)).ok()?,
);
}
state = State::ParamName;
}
State::ParamValue(param) => {
if !buf.is_empty() {
let value = String::from_utf8(std::mem::take(&mut buf)).ok()?;
match param {
Mailto::Header(header) => params.headers.push((header, value)),
Mailto::Body => params.body = value.into(),
Mailto::Other(header) => params.headers.push((header.into(), value)),
}
}
state = State::ParamName;
}
_ => return None,
},
_ => match &state {
State::ParamName => {
if ch.is_ascii_alphanumeric() || b"-_".contains(&ch) {
buf.push(ch);
} else {
return None;
}
}
_ => {
if !ch.is_ascii_whitespace() {
buf.push(ch);
}
}
},
}
}
if !buf.is_empty() {
let value = String::from_utf8(std::mem::take(&mut buf)).ok()?;
match state {
State::Address((header, true)) => {
insert_address(&mut params, header, value);
has_addresses = true;
}
State::ParamName => {
params
.headers
.push((HeaderName::Other(value.into()), String::new()));
}
State::ParamValue(param) => match param {
Mailto::Header(header) => params.headers.push((header, value)),
Mailto::Body => params.body = value.into(),
Mailto::Other(header) => params
.headers
.push((HeaderName::Other(header.into()), value)),
},
_ => return None,
}
}
if has_addresses { Some(params) } else { None }
}
#[inline(always)]
fn insert_address(params: &mut MailtoMessage, name: HeaderName, value: String) {
if !params
.to
.iter()
.chain(params.cc.iter())
.chain(params.bcc.iter())
.any(|v| v.eq_ignore_ascii_case(&value))
{
match name {
HeaderName::To => {
params.to.push(value);
}
HeaderName::Cc => {
params.cc.push(value);
}
HeaderName::Bcc => {
params.bcc.push(value);
}
_ => (),
}
}
}
+135
View File
@@ -0,0 +1,135 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use mail_parser::{DateTime, HeaderName};
use crate::{
Context, Event, Recipient,
compiler::grammar::actions::action_redirect::{ByTime, Redirect},
};
impl Redirect {
pub(crate) fn exec(&self, ctx: &mut Context) {
if let Some(address) = sanitize_address(ctx.eval_value(&self.address).to_string().as_ref())
&& ctx.num_redirects < ctx.runtime.max_redirects
&& ctx.num_out_messages < ctx.runtime.max_out_messages
&& ctx.message.parts[0]
.headers
.iter()
.filter(|h| matches!(&h.name, HeaderName::Received))
.count()
< ctx.runtime.max_received_headers
{
// Try to avoid forwarding loops
if !self.list && address.eq_ignore_ascii_case(ctx.user_address.as_ref()) {
return;
}
if !self.copy && matches!(&ctx.final_event, Some(Event::Keep { .. })) {
ctx.final_event = None;
}
let mut events = Vec::with_capacity(2);
if let Some(event) = ctx.build_message_id() {
events.push(event);
}
ctx.num_redirects += 1;
ctx.num_out_messages += 1;
events.push(Event::SendMessage {
recipient: if !self.list {
Recipient::Address(address)
} else {
Recipient::List(address)
},
notify: self.notify.clone(),
return_of_content: self.return_of_content.clone(),
by_time: match &self.by_time {
ByTime::Relative {
rlimit,
mode,
trace,
} => ByTime::Relative {
rlimit: *rlimit,
mode: mode.clone(),
trace: *trace,
},
ByTime::Absolute {
alimit,
mode,
trace,
} => ByTime::Absolute {
alimit: DateTime::parse_rfc3339(
ctx.eval_value(alimit).to_string().as_ref(),
)
.and_then(|d| {
if d.is_valid() {
d.to_timestamp().into()
} else {
None
}
})
.unwrap_or(0),
mode: mode.clone(),
trace: *trace,
},
ByTime::None => ByTime::None,
},
message_id: ctx.main_message_id,
});
ctx.queued_events = events.into_iter();
}
}
}
pub(crate) fn sanitize_address(addr: &str) -> Option<String> {
let mut result = String::with_capacity(addr.len());
let mut in_quote = false;
let mut last_ch = '\n';
let mut has_at = false;
let mut has_dot = false;
for ch in addr.chars() {
match ch {
'\"' => {
if !in_quote {
in_quote = true;
} else if last_ch != '\\' {
in_quote = false;
}
}
'@' if !in_quote => {
if !has_at && !result.is_empty() {
has_at = true;
result.push(ch);
} else {
return None;
}
}
'.' if !in_quote && has_at && !has_dot => {
has_dot = true;
result.push(ch);
}
'<' => {
result.clear();
has_at = false;
has_dot = false;
}
'>' => (),
_ => {
if !ch.is_ascii_whitespace() || in_quote {
result.push(ch);
}
}
}
last_ch = ch;
}
if !result.is_empty() && has_at && has_dot {
Some(result)
} else {
None
}
}
+199
View File
@@ -0,0 +1,199 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
Context, Envelope, Event,
compiler::{
VariableType,
grammar::actions::action_set::{Modifier, Set},
},
runtime::Variable,
};
use std::fmt::Write;
impl Set {
pub(crate) fn exec(&self, ctx: &mut Context) {
let mut value = ctx.eval_value(&self.value);
for modifier in &self.modifiers {
value = modifier.apply(value.to_string().as_ref(), ctx).into();
}
ctx.set_variable(&self.name, value);
}
}
impl Context<'_> {
pub(crate) fn set_variable(&mut self, var_name: &VariableType, mut variable: Variable) {
if variable.len() > self.runtime.max_variable_size {
let mut new_variable = String::with_capacity(self.runtime.max_variable_size);
for ch in variable.to_string().chars() {
if ch.len_utf8() + new_variable.len() <= self.runtime.max_variable_size {
new_variable.push(ch);
} else {
break;
}
}
variable = new_variable.into();
}
match var_name {
VariableType::Local(var_id) => {
if let Some(var) = self.vars_local.get_mut(*var_id) {
*var = variable.clone();
} else {
debug_assert!(false, "Non-existent local variable {var_id}");
}
}
VariableType::Global(var_name) => {
self.vars_global
.insert(var_name.to_string().into(), variable.clone());
}
VariableType::Envelope(env) => {
self.add_set_envelope_event(*env, variable.to_string().into_owned());
}
_ => (),
}
}
pub(crate) fn add_set_envelope_event(&mut self, envelope: Envelope, value: String) {
let mut did_find = false;
for (name, val) in self.envelope.iter_mut() {
if *name == envelope {
*val = Variable::String(value.clone().into());
did_find = true;
break;
}
}
if !did_find {
self.envelope
.push((envelope, Variable::String(value.clone().into())));
}
self.queued_events = vec![Event::SetEnvelope { envelope, value }].into_iter();
}
pub(crate) fn get_variable(&self, var_name: &VariableType) -> Option<&Variable> {
match var_name {
VariableType::Local(var_id) => self.vars_local.get(*var_id),
VariableType::Global(var_name) => self.vars_global.get(var_name.as_str()),
VariableType::Envelope(env) => self
.envelope
.iter()
.find_map(|(name, val)| if name == env { Some(val) } else { None }),
_ => unreachable!(),
}
}
}
impl Modifier {
pub(crate) fn apply(&self, input: &str, ctx: &Context) -> String {
let max_len = ctx.runtime.max_variable_size;
match self {
Modifier::Lower => input.to_lowercase(),
Modifier::Upper => input.to_uppercase(),
Modifier::LowerFirst => {
let mut result = String::with_capacity(input.len());
for (pos, char) in input.chars().enumerate() {
if result.len() + char.len_utf8() <= max_len {
if pos != 0 {
result.push(char);
} else {
for char in char.to_lowercase() {
result.push(char);
}
}
} else {
return result;
}
}
result
}
Modifier::UpperFirst => {
let mut result = String::with_capacity(input.len());
for (pos, char) in input.chars().enumerate() {
if result.len() + char.len_utf8() <= max_len {
if pos != 0 {
result.push(char);
} else {
for char in char.to_uppercase() {
result.push(char);
}
}
} else {
return result;
}
}
result
}
Modifier::QuoteWildcard => {
let mut result = String::with_capacity(input.len());
for char in input.chars() {
if ['*', '\\', '?'].contains(&char) {
if result.len() + char.len_utf8() < max_len {
result.push('\\');
result.push(char);
} else {
return result;
}
} else if result.len() + char.len_utf8() <= max_len {
result.push(char);
} else {
return result;
}
}
result
}
Modifier::QuoteRegex => {
let mut result = String::with_capacity(input.len());
for char in input.chars() {
if [
'*', '\\', '?', '.', '[', ']', '(', ')', '+', '{', '}', '|', '^', '=', ':',
'$',
]
.contains(&char)
{
if result.len() + char.len_utf8() < max_len {
result.push('\\');
result.push(char);
} else {
return result;
}
} else if result.len() + char.len_utf8() <= max_len {
result.push(char);
} else {
return result;
}
}
result
}
Modifier::Length => input.chars().count().to_string(),
Modifier::EncodeUrl => {
let mut buf = [0; 4];
let mut result = String::with_capacity(input.len());
for char in input.chars() {
if char.is_ascii_alphanumeric() || ['-', '.', '_', '~'].contains(&char) {
if result.len() < max_len {
result.push(char);
} else {
return result;
}
} else if result.len() + (char.len_utf8() * 3) <= max_len {
for byte in char.encode_utf8(&mut buf).as_bytes().iter() {
write!(result, "%{byte:02x}").ok();
}
} else {
return result;
}
}
result
}
Modifier::Replace { find, replace } => input.replace(
ctx.eval_value(find).to_string().as_ref(),
ctx.eval_value(replace).to_string().as_ref(),
),
}
}
}
+336
View File
@@ -0,0 +1,336 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::borrow::Cow;
use mail_builder::headers::{date::Date, message_id::generate_message_id_header};
use mail_parser::{HeaderName, HeaderValue};
use crate::{
Context, Envelope, Event, Recipient,
compiler::grammar::{
AddressPart,
actions::{
action_redirect::{ByTime, Notify, Ret},
action_vacation::{Period, TestVacation, Vacation},
},
},
runtime::tests::TestResult,
};
pub(crate) const MAX_SUBJECT_LEN: usize = 256;
impl TestVacation {
pub(crate) fn exec(&self, ctx: &mut Context) -> TestResult {
let mut from = String::new();
let mut user_addresses = Vec::new();
if ctx.num_out_messages >= ctx.runtime.max_out_messages {
return TestResult::Bool(false);
}
for (name, value) in &ctx.envelope {
if !value.is_empty() {
match name {
Envelope::From => {
from = value.to_string().to_ascii_lowercase();
}
Envelope::To if !ctx.runtime.vacation_use_orig_rcpt => {
user_addresses.push(value.to_string());
}
Envelope::Orcpt if ctx.runtime.vacation_use_orig_rcpt => {
user_addresses.push(value.to_string());
}
_ => (),
}
}
}
// Add user specified addresses
for address in &self.addresses {
let address = ctx.eval_value(address).to_string().into_owned();
if !address.is_empty() {
user_addresses.push(address.into());
}
}
if !ctx.user_address.is_empty() {
user_addresses.push(ctx.user_address.as_ref().into());
}
// Do not reply to own address
if from.is_empty()
|| user_addresses.is_empty()
|| from.starts_with("mailer-daemon")
|| from.starts_with("owner-")
|| from.contains("-request@")
|| user_addresses.iter().any(|a| a.eq_ignore_ascii_case(&from))
{
return TestResult::Bool(false);
}
// Check headers
let mut found_rcpt = false;
let mut received_count = 0;
for header in &ctx.message.parts[0].headers {
match &header.name {
HeaderName::To
| HeaderName::Cc
| HeaderName::Bcc
| HeaderName::ResentTo
| HeaderName::ResentBcc
| HeaderName::ResentCc
if !found_rcpt =>
{
found_rcpt = ctx.find_addresses(header, &AddressPart::All, |addr| {
user_addresses.iter().any(|a| a.eq_ignore_ascii_case(addr))
});
}
HeaderName::ListArchive
| HeaderName::ListHelp
| HeaderName::ListId
| HeaderName::ListOwner
| HeaderName::ListPost
| HeaderName::ListSubscribe
| HeaderName::ListUnsubscribe => {
// Do not send vacation responses to lists
return TestResult::Bool(false);
}
HeaderName::Received => {
received_count += 1;
}
HeaderName::Other(header_name) => {
if header_name.eq_ignore_ascii_case("Auto-Submitted") {
if header
.value
.as_text()
.is_none_or(|v| !v.eq_ignore_ascii_case("no"))
{
return TestResult::Bool(false);
}
} else if header_name.eq_ignore_ascii_case("X-Auto-Response-Suppress") {
if header.value.as_text().is_some_and(|v| {
v.to_ascii_lowercase()
.split(',')
.any(|v| ["all", "oof"].contains(&v.trim()))
}) {
return TestResult::Bool(false);
}
} else if header_name.eq_ignore_ascii_case("Precedence")
&& header
.value
.as_text()
.is_some_and(|v| v.eq_ignore_ascii_case("bulk"))
{
return TestResult::Bool(false);
}
}
_ => (),
}
}
// No user address found in header or possible loop
if found_rcpt && received_count <= ctx.runtime.max_received_headers {
TestResult::Event {
event: Event::DuplicateId {
id: if let Some(handle) = &self.handle {
format!("_v{}{}", from, ctx.eval_value(handle).to_string())
} else {
format!("_v{}{}", from, ctx.eval_value(&self.reason).to_string())
},
expiry: match &self.period {
Period::Days(days) => days * 86400,
Period::Seconds(seconds) => *seconds,
Period::Default => ctx.runtime.default_vacation_expiry,
},
last: false,
},
is_not: true,
}
} else {
TestResult::Bool(false)
}
}
}
impl Vacation {
pub(crate) fn exec(&self, ctx: &mut Context) {
let mut vacation_to = Cow::from("");
for (name, value) in &ctx.envelope {
if !value.is_empty() && name == &Envelope::From {
vacation_to = value.to_string();
break;
}
}
// Check headers
let mut vacation_subject = if let Some(subject) = &self.subject {
ctx.eval_value(subject)
} else {
"".into()
};
// Check headers
let mut message_id = None;
let mut vacation_to_full = None;
let mut references = None;
for header in &ctx.message.parts[0].headers {
match &header.name {
HeaderName::Subject if vacation_subject.is_empty() => {
if let Some(subject) = header.value.as_text() {
let mut vacation_subject_ = String::with_capacity(MAX_SUBJECT_LEN);
let mut iter = ctx
.runtime
.vacation_subject_prefix
.chars()
.chain(subject.chars())
.enumerate();
#[allow(clippy::while_let_on_iterator)]
while let Some((pos, char)) = iter.next() {
if pos < MAX_SUBJECT_LEN {
vacation_subject_.push(char);
} else {
break;
}
}
if iter.next().is_some() {
vacation_subject_.push('…');
}
vacation_subject = vacation_subject_.into();
}
}
HeaderName::MessageId => {
message_id = header.value.as_text();
}
HeaderName::References if header.offset_start > 0 => {
references = (&ctx.message.raw_message
[header.offset_start as usize..header.offset_end as usize])
.into();
}
HeaderName::From | HeaderName::Sender
if matches!(&header.value, HeaderValue::Address(address) if address.contains(vacation_to.as_ref()))
&& header.offset_start > 0 =>
{
vacation_to_full = (&ctx.message.raw_message
[header.offset_start as usize..header.offset_end as usize])
.into();
}
_ => (),
}
}
// Build message
let vacation_from = if let Some(from) = &self.from {
ctx.eval_value(from)
} else if !ctx.user_address.is_empty() {
ctx.user_from_field().into()
} else if let Some(addr) = ctx
.envelope
.iter()
.find_map(|(n, v)| if n == &Envelope::To { Some(v) } else { None })
{
addr.to_string().into()
} else {
"".into()
};
if vacation_subject.is_empty() {
vacation_subject = ctx.runtime.vacation_default_subject.as_ref().into();
}
let vacation_body = ctx.eval_value(&self.reason);
let message_len = vacation_body.len()
+ vacation_from.len()
+ vacation_to_full
.as_ref()
.map_or(vacation_to.len(), |t| t.len())
+ vacation_subject.len()
+ message_id.as_ref().map_or(0, |m| m.len() * 2)
+ references.as_ref().map_or(0, |m| m.len())
+ 160;
let mut message = Vec::with_capacity(message_len);
write_header(&mut message, "From: ", vacation_from.to_string().as_ref());
if let Some(vacation_to_full) = vacation_to_full {
message.extend_from_slice(b"To:");
message.extend_from_slice(vacation_to_full);
} else {
write_header(&mut message, "To: ", vacation_to.to_string().as_ref());
}
write_header(
&mut message,
"Subject: ",
vacation_subject.to_string().as_ref(),
);
if let Some(message_id) = message_id {
message.extend_from_slice(b"In-Reply-To: <");
message.extend_from_slice(message_id.as_bytes());
message.extend_from_slice(b">\r\n");
message.extend_from_slice(b"References: <");
message.extend_from_slice(message_id.as_bytes());
if let Some(references) = references {
message.extend_from_slice(b"> ");
message.extend_from_slice(references);
} else {
message.extend_from_slice(b">\r\n");
}
}
message.extend_from_slice(b"Date: ");
message.extend_from_slice(Date::now().to_rfc822().as_bytes());
message.extend_from_slice(b"\r\n");
message.extend_from_slice(b"Message-ID: ");
generate_message_id_header(&mut message, &ctx.runtime.local_hostname).unwrap();
message.extend_from_slice(b"\r\n");
write_header(&mut message, "Auto-Submitted: ", "auto-replied");
if !self.mime {
message.extend_from_slice(b"Content-type: text/plain; charset=utf-8\r\n\r\n");
}
message.extend_from_slice(vacation_body.to_string().as_bytes());
// Add action
let mut events = Vec::with_capacity(3);
ctx.last_message_id += 1;
ctx.num_out_messages += 1;
events.push(Event::CreatedMessage {
message_id: ctx.last_message_id,
message,
});
events.push(Event::SendMessage {
recipient: Recipient::Address(vacation_to.to_string()),
notify: Notify::Never,
return_of_content: Ret::Default,
by_time: ByTime::None,
message_id: ctx.last_message_id,
});
// File carbon copy
if let Some(fcc) = &self.fcc {
events.push(Event::FileInto {
folder: ctx.eval_value(&fcc.mailbox).to_string().into_owned(),
flags: ctx.get_local_flags(&fcc.flags),
mailbox_id: fcc
.mailbox_id
.as_ref()
.map(|m| ctx.eval_value(m).to_string().into_owned()),
special_use: fcc
.special_use
.as_ref()
.map(|s| ctx.eval_value(s).to_string().into_owned()),
create: fcc.create,
message_id: ctx.last_message_id,
});
}
ctx.queued_events = events.into_iter();
}
}
fn write_header(buf: &mut Vec<u8>, name: &str, value: &str) {
buf.extend_from_slice(name.as_bytes());
buf.extend_from_slice(value.as_bytes());
buf.extend_from_slice(b"\r\n");
}
+16
View File
@@ -0,0 +1,16 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
pub mod action_convert;
pub mod action_editheader;
pub mod action_fileinto;
pub mod action_flags;
pub mod action_include;
pub mod action_mime;
pub mod action_notify;
pub mod action_redirect;
pub mod action_set;
pub mod action_vacation;
+689
View File
@@ -0,0 +1,689 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::{borrow::Cow, sync::Arc, time::SystemTime};
use ahash::AHashMap;
use mail_parser::Message;
use crate::{
Context, Envelope, Event, Input, MAX_LOCAL_VARIABLES, MAX_MATCH_VARIABLES, Metadata, Runtime,
Sieve, SpamStatus, VirusStatus,
compiler::grammar::{Capability, instruction::Instruction},
};
use super::{
RuntimeError, Variable,
actions::action_include::IncludeResult,
tests::{TestResult, test_envelope::parse_envelope_address},
};
#[derive(Clone, Debug)]
pub(crate) struct ScriptStack {
pub(crate) script: Arc<Sieve>,
pub(crate) prev_pos: usize,
pub(crate) prev_vars_local: Vec<Variable>,
pub(crate) prev_vars_match: Vec<Variable>,
}
impl<'x> Context<'x> {
#[cfg(not(test))]
pub(crate) fn new(runtime: &'x Runtime, message: Message<'x>) -> Self {
Context {
#[cfg(test)]
runtime: runtime.clone(),
#[cfg(not(test))]
runtime,
message,
part: 0,
part_iter: Vec::new().into_iter(),
part_iter_stack: Vec::new(),
pos: usize::MAX,
test_result: false,
script_cache: AHashMap::new(),
script_stack: Vec::with_capacity(0),
vars_global: AHashMap::new(),
vars_env: AHashMap::new(),
vars_local: Vec::with_capacity(0),
vars_match: Vec::with_capacity(0),
expr_stack: Vec::with_capacity(16),
expr_pos: 0,
envelope: Vec::new(),
metadata: Vec::new(),
message_size: usize::MAX,
final_event: Event::Keep {
flags: Vec::with_capacity(0),
message_id: 0,
}
.into(),
queued_events: vec![].into_iter(),
has_changes: false,
user_address: "".into(),
user_full_name: "".into(),
current_time: SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0) as i64,
num_redirects: 0,
num_instructions: 0,
num_out_messages: 0,
last_message_id: 0,
main_message_id: 0,
virus_status: VirusStatus::Unknown,
spam_status: SpamStatus::Unknown,
}
}
#[allow(clippy::while_let_on_iterator)]
pub fn run(&mut self, input: Input) -> Option<Result<Event, RuntimeError>> {
match input {
Input::True => self.test_result ^= true,
Input::False => self.test_result ^= false,
Input::FncResult(result) => {
self.expr_stack.push(result);
}
Input::Script { name, script } => {
let num_vars = script.num_vars;
let num_match_vars = script.num_match_vars;
if num_match_vars <= MAX_MATCH_VARIABLES && num_vars <= MAX_LOCAL_VARIABLES {
if self.message_size == usize::MAX {
self.message_size = self.message.raw_message.len();
}
self.script_cache.insert(name, script.clone());
self.script_stack.push(ScriptStack {
script,
prev_pos: self.pos,
prev_vars_local: std::mem::replace(
&mut self.vars_local,
vec![Variable::default(); num_vars as usize],
),
prev_vars_match: std::mem::replace(
&mut self.vars_match,
vec![Variable::default(); num_match_vars as usize],
),
});
self.pos = 0;
self.test_result = false;
}
}
}
// Return any queued events
if let Some(event) = self.queued_events.next() {
return Some(Ok(event));
}
let mut current_script = self.script_stack.last()?.script.clone();
let mut iter = current_script.instructions.get(self.pos..)?.iter();
'outer: loop {
while let Some(instruction) = iter.next() {
self.num_instructions += 1;
if self.num_instructions > self.runtime.cpu_limit {
self.finish_loop();
return Some(Err(RuntimeError::CPULimitReached));
}
self.pos += 1;
match instruction {
Instruction::Jz(jmp_pos) => {
if !self.test_result {
debug_assert!(*jmp_pos > self.pos - 1);
self.pos = *jmp_pos;
iter = current_script.instructions.get(self.pos..)?.iter();
continue;
}
}
Instruction::Jnz(jmp_pos) => {
if self.test_result {
debug_assert!(*jmp_pos > self.pos - 1);
self.pos = *jmp_pos;
iter = current_script.instructions.get(self.pos..)?.iter();
continue;
}
}
Instruction::Jmp(jmp_pos) => {
debug_assert_ne!(*jmp_pos, self.pos - 1);
self.pos = *jmp_pos;
iter = current_script.instructions.get(self.pos..)?.iter();
continue;
}
Instruction::Test(test) => match test.exec(self) {
TestResult::Bool(result) => {
self.test_result = result;
}
TestResult::Event { event, is_not } => {
self.test_result = is_not;
return Some(Ok(event));
}
TestResult::Error(err) => {
self.finish_loop();
return Some(Err(err));
}
},
Instruction::Eval(expr) => match self.eval_expression(expr) {
Ok(result) => {
self.test_result = result.to_bool();
}
Err(event) => {
return Some(Ok(event));
}
},
Instruction::Clear(clear) => {
if clear.local_vars_num > 0 {
if let Some(local_vars) = self.vars_local.get_mut(
clear.local_vars_idx as usize
..(clear.local_vars_idx + clear.local_vars_num) as usize,
) {
for local_var in local_vars.iter_mut() {
if !local_var.is_empty() {
*local_var = Variable::default();
}
}
} else {
debug_assert!(false, "Failed to clear local variables: {clear:?}");
}
}
if clear.match_vars != 0 {
self.clear_match_variables(clear.match_vars);
}
}
Instruction::Keep(keep) => {
let next_event = self.build_message_id();
self.final_event = Event::Keep {
flags: self.get_local_or_global_flags(&keep.flags),
message_id: self.main_message_id,
}
.into();
if let Some(next_event) = next_event {
return Some(Ok(next_event));
}
}
Instruction::FileInto(fi) => {
fi.exec(self);
if let Some(event) = self.queued_events.next() {
return Some(Ok(event));
}
}
Instruction::Redirect(redirect) => {
redirect.exec(self);
if let Some(event) = self.queued_events.next() {
return Some(Ok(event));
}
}
Instruction::Discard => {
self.final_event = Event::Discard.into();
}
Instruction::Stop => {
self.script_stack.clear();
break 'outer;
}
Instruction::Reject(reject) => {
self.final_event = None;
return Some(Ok(Event::Reject {
extended: reject.ereject,
reason: self.eval_value(&reject.reason).to_string().into_owned(),
}));
}
Instruction::ForEveryPart(fep) => {
if let Some(next_part) = self.part_iter.next() {
self.part = next_part;
} else if let Some((prev_part, prev_part_iter)) = self.part_iter_stack.pop()
{
debug_assert!(fep.jz_pos > self.pos - 1);
self.part_iter = prev_part_iter;
self.part = prev_part;
self.pos = fep.jz_pos;
iter = current_script.instructions.get(self.pos..)?.iter();
continue;
} else {
self.part = 0;
#[cfg(test)]
panic!("ForEveryPart executed without items on stack.");
}
}
Instruction::ForEveryPartPush => {
let part_iter = self
.find_nested_parts_ids(self.part_iter_stack.is_empty())
.into_iter();
self.part_iter_stack
.push((self.part, std::mem::replace(&mut self.part_iter, part_iter)));
}
Instruction::ForEveryPartPop(num_pops) => {
debug_assert!(
*num_pops > 0 && *num_pops <= self.part_iter_stack.len(),
"Pop out of range: {} with {} items.",
num_pops,
self.part_iter_stack.len()
);
for _ in 0..*num_pops {
if let Some((prev_part, prev_part_iter)) = self.part_iter_stack.pop() {
self.part_iter = prev_part_iter;
self.part = prev_part;
} else {
break;
}
}
}
Instruction::While(while_) => match self.eval_expression(&while_.expr) {
Ok(result) => {
if !result.to_bool() {
debug_assert!(while_.jz_pos > self.pos - 1);
self.pos = while_.jz_pos;
iter = current_script.instructions.get(self.pos..)?.iter();
continue;
}
}
Err(event) => {
return Some(Ok(event));
}
},
Instruction::Let(let_) => match self.eval_expression(&let_.expr) {
Ok(result) => {
self.set_variable(&let_.name, result);
}
Err(event) => {
return Some(Ok(event));
}
},
Instruction::Replace(replace) => replace.exec(self),
Instruction::Enclose(enclose) => enclose.exec(self),
Instruction::ExtractText(extract) => {
extract.exec(self);
if let Some(event) = self.queued_events.next() {
return Some(Ok(event));
}
}
Instruction::AddHeader(add_header) => add_header.exec(self),
Instruction::DeleteHeader(delete_header) => delete_header.exec(self),
Instruction::Set(set) => {
set.exec(self);
if let Some(event) = self.queued_events.next() {
return Some(Ok(event));
}
}
Instruction::Notify(notify) => {
notify.exec(self);
if let Some(event) = self.queued_events.next() {
return Some(Ok(event));
}
}
Instruction::Vacation(vacation) => {
vacation.exec(self);
if let Some(event) = self.queued_events.next() {
return Some(Ok(event));
}
}
Instruction::EditFlags(flags) => flags.exec(self),
Instruction::Include(include) => match include.exec(self) {
IncludeResult::Cached(script) => {
self.script_stack.push(ScriptStack {
script: script.clone(),
prev_pos: self.pos,
prev_vars_local: std::mem::replace(
&mut self.vars_local,
vec![Variable::default(); script.num_vars as usize],
),
prev_vars_match: std::mem::replace(
&mut self.vars_match,
vec![Variable::default(); script.num_match_vars as usize],
),
});
self.pos = 0;
current_script = script;
iter = current_script.instructions.iter();
continue;
}
IncludeResult::Event(event) => {
return Some(Ok(event));
}
IncludeResult::Error(err) => {
self.finish_loop();
return Some(Err(err));
}
IncludeResult::None => (),
},
Instruction::Convert(convert) => {
convert.exec(self);
}
Instruction::Return => {
break;
}
Instruction::Require(capabilities) => {
for capability in capabilities {
if !self.runtime.allowed_capabilities.contains(capability) {
self.finish_loop();
return Some(Err(
if let Capability::Other(not_supported) = capability {
RuntimeError::CapabilityNotSupported(not_supported.clone())
} else {
RuntimeError::CapabilityNotAllowed(capability.clone())
},
));
}
}
}
Instruction::Error(err) => {
self.finish_loop();
return Some(Err(RuntimeError::ScriptErrorMessage(
self.eval_value(&err.message).to_string().into_owned(),
)));
}
Instruction::Invalid(invalid) => {
self.finish_loop();
return Some(Err(RuntimeError::InvalidInstruction(invalid.clone())));
}
#[cfg(test)]
Instruction::TestCmd(arguments) => {
return Some(Ok(Event::Function {
id: u32::MAX,
arguments: arguments
.iter()
.map(|s| self.eval_value(s).to_owned())
.collect(),
}));
}
}
}
if let Some(prev_script) = self.script_stack.pop() {
self.pos = prev_script.prev_pos;
self.vars_local = prev_script.prev_vars_local;
self.vars_match = prev_script.prev_vars_match;
}
if let Some(script_stack) = self.script_stack.last() {
current_script = script_stack.script.clone();
iter = current_script.instructions.get(self.pos..)?.iter();
} else {
break;
}
}
match self.final_event.take() {
Some(Event::Keep {
mut flags,
message_id,
}) => {
let create_event = if self.has_changes {
self.build_message_id()
} else {
None
};
let global_flags = self.get_global_flags();
if flags.is_empty() && !global_flags.is_empty() {
flags = global_flags;
}
if let Some(create_event) = create_event {
self.queued_events = vec![
create_event,
Event::Keep {
flags,
message_id: self.main_message_id,
},
]
.into_iter();
self.queued_events.next().map(Ok)
} else {
Some(Ok(Event::Keep { flags, message_id }))
}
}
Some(event) => Some(Ok(event)),
_ => None,
}
}
pub(crate) fn finish_loop(&mut self) {
self.script_stack.clear();
if let Some(event) = self.final_event.take() {
self.queued_events = if let Event::Keep {
mut flags,
message_id,
} = event
{
let global_flags = self.get_global_flags();
if flags.is_empty() && !global_flags.is_empty() {
flags = global_flags;
}
if self.has_changes {
if let Some(event) = self.build_message_id() {
vec![
event,
Event::Keep {
flags,
message_id: self.main_message_id,
},
]
} else {
vec![Event::Keep { flags, message_id }]
}
} else {
vec![Event::Keep { flags, message_id }]
}
} else {
vec![event]
}
.into_iter();
}
}
pub fn set_envelope(
&mut self,
envelope: impl TryInto<Envelope>,
value: impl Into<Cow<'x, str>>,
) {
if let Ok(envelope) = envelope.try_into() {
if matches!(&envelope, Envelope::From | Envelope::To) {
let value: Cow<str> = value.into();
if let Some(value) = parse_envelope_address(value.as_ref()) {
self.envelope.push((envelope, value.to_string().into()));
}
} else {
self.envelope.push((envelope, Variable::from(value.into())));
}
}
}
pub fn with_vars_env(mut self, vars_env: AHashMap<Cow<'static, str>, Variable>) -> Self {
self.vars_env = vars_env;
self
}
pub fn with_envelope_list(mut self, envelope: Vec<(Envelope, Variable)>) -> Self {
self.envelope = envelope;
self
}
pub fn with_envelope(
mut self,
envelope: impl TryInto<Envelope>,
value: impl Into<Cow<'x, str>>,
) -> Self {
self.set_envelope(envelope, value);
self
}
pub fn clear_envelope(&mut self) {
self.envelope.clear()
}
pub fn set_user_address(&mut self, from: impl Into<Cow<'x, str>>) {
self.user_address = from.into();
}
pub fn with_user_address(mut self, from: impl Into<Cow<'x, str>>) -> Self {
self.set_user_address(from);
self
}
pub fn set_user_full_name(&mut self, name: &str) {
let mut name_ = String::with_capacity(name.len());
for ch in name.chars() {
if ['\"', '\\'].contains(&ch) {
name_.push('\\');
}
name_.push(ch);
}
self.user_full_name = name_.into();
}
pub fn with_user_full_name(mut self, name: &str) -> Self {
self.set_user_full_name(name);
self
}
pub fn set_env_variable(
&mut self,
name: impl Into<Cow<'static, str>>,
value: impl Into<Variable>,
) {
self.vars_env.insert(name.into(), value.into());
}
pub fn with_env_variable(
mut self,
name: impl Into<Cow<'static, str>>,
value: impl Into<Variable>,
) -> Self {
self.set_env_variable(name, value);
self
}
pub fn set_global_variable(
&mut self,
name: impl Into<Cow<'static, str>>,
value: impl Into<Variable>,
) {
self.vars_global.insert(name.into(), value.into());
}
pub fn with_global_variable(
mut self,
name: impl Into<Cow<'static, str>>,
value: impl Into<Variable>,
) -> Self {
self.set_global_variable(name, value);
self
}
pub fn set_medatata(
&mut self,
name: impl Into<Metadata<String>>,
value: impl Into<Cow<'x, str>>,
) {
self.metadata.push((name.into(), value.into()));
}
pub fn with_metadata(
mut self,
name: impl Into<Metadata<String>>,
value: impl Into<Cow<'x, str>>,
) -> Self {
self.set_medatata(name, value);
self
}
pub fn set_spam_status(&mut self, status: impl Into<SpamStatus>) {
self.spam_status = status.into();
}
pub fn with_spam_status(mut self, status: impl Into<SpamStatus>) -> Self {
self.set_spam_status(status);
self
}
pub fn set_virus_status(&mut self, status: impl Into<VirusStatus>) {
self.virus_status = status.into();
}
pub fn with_virus_status(mut self, status: impl Into<VirusStatus>) -> Self {
self.set_virus_status(status);
self
}
pub fn take_message(&mut self) -> Message<'x> {
std::mem::take(&mut self.message)
}
pub fn has_message_changed(&self) -> bool {
self.main_message_id > 0
}
pub(crate) fn user_from_field(&self) -> String {
if !self.user_full_name.is_empty() {
format!("\"{}\" <{}>", self.user_full_name, self.user_address)
} else {
self.user_address.to_string()
}
}
pub fn global_variable_names(&self) -> impl Iterator<Item = &str> {
self.vars_global.keys().map(|k| k.as_ref())
}
pub fn global_variable(&self, name: &str) -> Option<&Variable> {
self.vars_global.get(name)
}
pub fn message(&self) -> &Message<'x> {
&self.message
}
pub fn part(&self) -> u32 {
self.part
}
}
#[cfg(test)]
impl<'x> Context<'x> {
pub(crate) fn new(runtime: &'x Runtime, message: Message<'x>) -> Self {
Context {
runtime: runtime.clone(),
message,
part: 0,
part_iter: Vec::new().into_iter(),
part_iter_stack: Vec::new(),
pos: usize::MAX,
test_result: false,
script_cache: AHashMap::new(),
script_stack: Vec::with_capacity(0),
vars_global: AHashMap::new(),
vars_env: AHashMap::new(),
vars_local: Vec::with_capacity(0),
vars_match: Vec::with_capacity(0),
expr_stack: Vec::with_capacity(16),
expr_pos: 0,
envelope: Vec::new(),
metadata: Vec::new(),
message_size: usize::MAX,
final_event: Event::Keep {
flags: Vec::with_capacity(0),
message_id: 0,
}
.into(),
queued_events: vec![].into_iter(),
has_changes: false,
user_address: "".into(),
user_full_name: "".into(),
current_time: SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0) as i64,
num_redirects: 0,
num_instructions: 0,
num_out_messages: 0,
last_message_id: 0,
main_message_id: 0,
virus_status: VirusStatus::Unknown,
spam_status: SpamStatus::Unknown,
}
}
}
+482
View File
@@ -0,0 +1,482 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::cmp::Ordering;
use mail_parser::{
Addr, Header, HeaderName, HeaderValue, Host, PartType, Received,
decoders::html::{html_to_text, text_to_html},
parsers::MessageStream,
};
use crate::{
Context,
compiler::{
ContentTypePart, HeaderPart, HeaderVariable, MessagePart, ReceivedHostname, ReceivedPart,
Value, VariableType,
},
};
use super::Variable;
impl<'x> Context<'x> {
pub(crate) fn variable<'y: 'x>(&'y self, var: &VariableType) -> Option<Variable> {
match var {
VariableType::Local(var_num) => self.vars_local.get(*var_num).cloned(),
VariableType::Match(var_num) => self.vars_match.get(*var_num).cloned(),
VariableType::Global(var_name) => self.vars_global.get(var_name.as_str()).cloned(),
VariableType::Environment(var_name) => self
.vars_env
.get(var_name.as_str())
.or_else(|| self.runtime.environment.get(var_name.as_str()))
.cloned(),
VariableType::Envelope(envelope) => self
.envelope
.iter()
.find_map(|(e, v)| if e == envelope { Some(v.clone()) } else { None }),
VariableType::Header(header) => self.eval_header(header),
VariableType::Part(part) => match part {
MessagePart::TextBody(convert) => {
let part = self
.message
.parts
.get(*self.message.text_body.first()? as usize)?;
match &part.body {
PartType::Text(text) => Some(text.as_ref().into()),
PartType::Html(html) if *convert => {
Some(html_to_text(html.as_ref()).into())
}
_ => None,
}
}
MessagePart::HtmlBody(convert) => {
let part = self
.message
.parts
.get(*self.message.html_body.first()? as usize)?;
match &part.body {
PartType::Html(html) => Some(html.as_ref().into()),
PartType::Text(text) if *convert => {
Some(text_to_html(text.as_ref()).into())
}
_ => None,
}
}
MessagePart::Contents => match &self.message.parts.get(self.part as usize)?.body {
PartType::Text(text) | PartType::Html(text) => {
Variable::from(text.as_ref()).into()
}
PartType::Binary(bin) | PartType::InlineBinary(bin) => {
Variable::from(String::from_utf8_lossy(bin.as_ref())).into()
}
_ => None,
},
MessagePart::Raw => {
let part = self.message.parts.get(self.part as usize)?;
self.message
.raw_message()
.get(part.raw_body_offset() as usize..part.raw_end_offset() as usize)
.map(|v| Variable::from(String::from_utf8_lossy(v)))
}
},
}
}
pub(crate) fn eval_value(&self, string: &Value) -> Variable {
match string {
Value::Text(text) => Variable::String(text.clone()),
Value::Variable(var) => self.variable(var).unwrap_or_default(),
Value::List(list) => {
let mut data = String::new();
for item in list {
match item {
Value::Text(string) => {
data.push_str(string);
}
Value::Variable(var) => {
if let Some(value) = self.variable(var) {
data.push_str(&value.to_string());
}
}
Value::List(_) => {
debug_assert!(false, "This should not have happened: {string:?}");
}
Value::Number(n) => {
data.push_str(&n.to_string());
}
Value::Regex(_) => (),
}
}
data.into()
}
Value::Number(n) => Variable::from(*n),
Value::Regex(r) => Variable::String(r.expr.clone().into()),
}
}
fn eval_header<'z: 'x>(&'z self, header: &HeaderVariable) -> Option<Variable> {
let mut result = Vec::new();
let part = self.message.part(self.part)?;
let raw = self.message.raw_message();
if !header.name.is_empty() {
let mut headers = part
.headers
.iter()
.filter(|h| header.name.contains(&h.name));
match header.index_hdr.cmp(&0) {
Ordering::Greater => {
if let Some(h) = headers.nth((header.index_hdr - 1) as usize) {
header.eval_part(h, raw, &mut result);
}
}
Ordering::Less => {
if let Some(h) = headers
.rev()
.nth((header.index_hdr.unsigned_abs() - 1) as usize)
{
header.eval_part(h, raw, &mut result);
}
}
Ordering::Equal => {
for h in headers {
header.eval_part(h, raw, &mut result);
}
}
}
} else {
for h in &part.headers {
match &header.part {
HeaderPart::Raw => {
if let Some(var) = raw
.get(h.offset_field as usize..h.offset_end as usize)
.map(sanitize_raw_header)
{
result.push(Variable::from(var));
}
}
HeaderPart::Text => {
if let HeaderValue::Text(text) = &h.value {
result.push(Variable::from(format!("{}: {}", h.name.as_str(), text)));
} else if let HeaderValue::Text(text) = MessageStream::new(
raw.get(h.offset_start as usize..h.offset_end as usize)
.unwrap_or(b""),
)
.parse_unstructured()
{
result.push(Variable::from(format!("{}: {}", h.name.as_str(), text)));
}
}
_ => {
header.eval_part(h, raw, &mut result);
}
}
}
}
match result.len() {
1 if header.index_hdr != 0 && header.index_part != 0 => result.pop(),
0 => None,
_ => Some(Variable::Array(result.into())),
}
}
#[inline(always)]
pub(crate) fn eval_values<'z: 'y, 'y>(&'z self, strings: &'y [Value]) -> Vec<Variable> {
strings.iter().map(|s| self.eval_value(s)).collect()
}
#[inline(always)]
pub(crate) fn eval_values_owned(&self, strings: &[Value]) -> Vec<String> {
strings
.iter()
.map(|s| self.eval_value(s).to_string().into_owned())
.collect()
}
}
impl<'x> HeaderVariable<'x> {
fn eval_part(&self, header: &'x Header<'x>, raw: &'x [u8], result: &mut Vec<Variable>) {
let var = match &self.part {
HeaderPart::Text => match &header.value {
HeaderValue::Text(v) if self.include_single_part() => {
Some(Variable::from(v.as_ref()))
}
HeaderValue::TextList(list) => match self.index_part.cmp(&0) {
Ordering::Greater => list
.get((self.index_part - 1) as usize)
.map(|v| Variable::from(v.as_ref())),
Ordering::Less => list
.iter()
.rev()
.nth((self.index_part.unsigned_abs() - 1) as usize)
.map(|v| Variable::from(v.as_ref())),
Ordering::Equal => {
for item in list {
result.push(Variable::from(item.as_ref()));
}
return;
}
},
HeaderValue::ContentType(ct) => if let Some(st) = &ct.c_subtype {
Variable::from(format!("{}/{}", ct.c_type, st))
} else {
Variable::from(ct.c_type.as_ref())
}
.into(),
HeaderValue::Address(list) => {
let mut list = list.iter();
match self.index_part.cmp(&0) {
Ordering::Greater => list
.nth((self.index_part - 1) as usize)
.map(|a| a.to_text()),
Ordering::Less => list
.rev()
.nth((self.index_part.unsigned_abs() - 1) as usize)
.map(|a| a.to_text()),
Ordering::Equal => {
for item in list {
result.push(item.to_text());
}
return;
}
}
}
HeaderValue::DateTime(_) => raw
.get(header.offset_start as usize..header.offset_end as usize)
.and_then(|bytes| std::str::from_utf8(bytes).ok())
.map(|s| s.trim())
.map(Variable::from),
_ => None,
},
HeaderPart::Address(part) => match &header.value {
HeaderValue::Address(addr) => {
let mut list = addr.iter();
match self.index_part.cmp(&0) {
Ordering::Greater => list
.nth((self.index_part - 1) as usize)
.and_then(|a| part.eval_strict(a))
.map(Variable::from),
Ordering::Less => list
.rev()
.nth((self.index_part.unsigned_abs() - 1) as usize)
.and_then(|a| part.eval_strict(a))
.map(Variable::from),
Ordering::Equal => {
for item in list {
result.push(
part.eval_strict(item)
.map(Variable::from)
.unwrap_or_default(),
);
}
return;
}
}
}
HeaderValue::Text(_) => {
let addr = raw
.get(header.offset_start as usize..header.offset_end as usize)
.and_then(|bytes| match MessageStream::new(bytes).parse_address() {
HeaderValue::Address(addr) => addr.into(),
_ => None,
});
if let Some(addr) = addr {
let mut list = addr.iter();
match self.index_part.cmp(&0) {
Ordering::Greater => list
.nth((self.index_part - 1) as usize)
.and_then(|a| part.eval_strict(a))
.map(|s| Variable::String(s.to_string().into())),
Ordering::Less => list
.rev()
.nth((self.index_part.unsigned_abs() - 1) as usize)
.and_then(|a| part.eval_strict(a))
.map(|s| Variable::String(s.to_string().into())),
Ordering::Equal => {
for item in list {
result.push(
part.eval_strict(item)
.map(|s| Variable::String(s.to_string().into()))
.unwrap_or_default(),
);
}
return;
}
}
} else {
None
}
}
_ => None,
},
HeaderPart::Date => {
if let HeaderValue::DateTime(dt) = &header.value {
Variable::from(dt.to_timestamp()).into()
} else {
raw.get(header.offset_start as usize..header.offset_end as usize)
.and_then(|bytes| match MessageStream::new(bytes).parse_date() {
HeaderValue::DateTime(dt) => Variable::from(dt.to_timestamp()).into(),
_ => None,
})
}
}
HeaderPart::Id => match &header.name {
HeaderName::MessageId | HeaderName::ResentMessageId => match &header.value {
HeaderValue::Text(id) => Variable::from(id.as_ref()).into(),
HeaderValue::TextList(ids) => {
for id in ids {
result.push(Variable::from(id.as_ref()));
}
return;
}
_ => None,
},
HeaderName::Other(_) => {
match MessageStream::new(
raw.get(header.offset_start as usize..header.offset_end as usize)
.unwrap_or(b""),
)
.parse_id()
{
HeaderValue::Text(id) => Variable::from(id).into(),
HeaderValue::TextList(ids) => {
for id in ids {
result.push(Variable::from(id));
}
return;
}
_ => None,
}
}
_ => None,
},
HeaderPart::Raw => raw
.get(header.offset_start as usize..header.offset_end as usize)
.map(sanitize_raw_header)
.map(Variable::from),
HeaderPart::RawName => raw
.get(header.offset_field as usize..header.offset_start as usize - 1)
.map(|bytes| std::str::from_utf8(bytes).unwrap_or_default())
.map(Variable::from),
HeaderPart::Exists => Variable::from(true).into(),
_ => match (&header.value, &self.part) {
(HeaderValue::ContentType(ct), HeaderPart::ContentType(part)) => match part {
ContentTypePart::Type => Variable::from(ct.c_type.as_ref()).into(),
ContentTypePart::Subtype => {
ct.c_subtype.as_ref().map(|s| Variable::from(s.as_ref()))
}
ContentTypePart::Attribute(attr) => ct.attributes.as_ref().and_then(|attrs| {
attrs.iter().find_map(|a| {
if a.name.eq_ignore_ascii_case(attr) {
Some(Variable::from(a.value.as_ref()))
} else {
None
}
})
}),
},
(HeaderValue::Received(rcvd), HeaderPart::Received(part)) => part.eval(rcvd),
_ => None,
},
};
result.push(var.unwrap_or_default());
}
#[inline(always)]
fn include_single_part(&self) -> bool {
[-1, 0, 1].contains(&self.index_part)
}
}
impl ReceivedPart {
pub fn eval<'x>(&self, rcvd: &'x Received<'x>) -> Option<Variable> {
match self {
ReceivedPart::From(from) => rcvd
.from()
.or_else(|| rcvd.helo())
.and_then(|v| from.to_variable(v)),
ReceivedPart::FromIp => rcvd.from_ip().map(|ip| Variable::from(ip.to_string())),
ReceivedPart::FromIpRev => rcvd.from_iprev().map(Variable::from),
ReceivedPart::By(by) => rcvd.by().and_then(|v: &Host<'_>| by.to_variable(v)),
ReceivedPart::For => rcvd.for_().map(Variable::from),
ReceivedPart::With => rcvd.with().map(|v| Variable::from(v.as_str())),
ReceivedPart::TlsVersion => rcvd.tls_version().map(|v| Variable::from(v.as_str())),
ReceivedPart::TlsCipher => rcvd.tls_cipher().map(Variable::from),
ReceivedPart::Id => rcvd.id().map(Variable::from),
ReceivedPart::Ident => rcvd.ident().map(Variable::from),
ReceivedPart::Via => rcvd.via().map(Variable::from),
ReceivedPart::Date => rcvd.date().map(|d| Variable::from(d.to_timestamp())),
ReceivedPart::DateRaw => rcvd.date().map(|d| Variable::from(d.to_rfc822())),
}
}
}
trait AddrToText<'x> {
fn to_text<'z: 'x>(&'z self) -> Variable;
}
impl<'x> AddrToText<'x> for Addr<'x> {
fn to_text<'z: 'x>(&'z self) -> Variable {
if let Some(name) = &self.name {
if let Some(address) = &self.address {
Variable::String(format!("{name} <{address}>").into())
} else {
Variable::String(name.to_string().into())
}
} else if let Some(address) = &self.address {
Variable::String(format!("<{address}>").into())
} else {
Variable::default()
}
}
}
impl ReceivedHostname {
fn to_variable<'x>(&self, host: &'x Host<'x>) -> Option<Variable> {
match (self, host) {
(ReceivedHostname::Name, Host::Name(name)) => Variable::from(name.as_ref()).into(),
(ReceivedHostname::Ip, Host::IpAddr(ip)) => Variable::from(ip.to_string()).into(),
(ReceivedHostname::Any, _) => Variable::from(host.to_string()).into(),
_ => None,
}
}
}
pub(crate) trait IntoString: Sized {
fn into_string(self) -> String;
}
pub(crate) trait ToString: Sized {
fn to_string(&self) -> String;
}
impl IntoString for Vec<u8> {
fn into_string(self) -> String {
String::from_utf8(self)
.unwrap_or_else(|err| String::from_utf8_lossy(err.as_bytes()).into_owned())
}
}
fn sanitize_raw_header(bytes: &[u8]) -> String {
let mut result = Vec::with_capacity(bytes.len());
let mut last_is_space = false;
for &ch in bytes {
if ch.is_ascii_whitespace() {
last_is_space = true;
} else {
if last_is_space {
result.push(b' ');
last_is_space = false;
}
result.push(ch);
}
}
result.into_string()
}
+641
View File
@@ -0,0 +1,641 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::{cmp::Ordering, fmt::Display};
use crate::Event;
use crate::compiler::grammar::expr::parser::ID_EXTERNAL;
use crate::{Context, compiler::Number, runtime::Variable};
use crate::compiler::grammar::expr::{BinaryOperator, Constant, Expression, UnaryOperator};
impl Context<'_> {
pub(crate) fn eval_expression(&mut self, expr: &[Expression]) -> Result<Variable, Event> {
let mut exprs = expr.iter().skip(self.expr_pos);
while let Some(expr) = exprs.next() {
self.expr_pos += 1;
match expr {
Expression::Variable(v) => {
self.expr_stack.push(self.variable(v).unwrap_or_default());
}
Expression::Constant(val) => {
self.expr_stack.push(Variable::from(val));
}
Expression::UnaryOperator(op) => {
let value = self.expr_stack.pop().unwrap_or_default();
self.expr_stack.push(match op {
UnaryOperator::Not => value.op_not(),
UnaryOperator::Minus => value.op_minus(),
});
}
Expression::BinaryOperator(op) => {
let right = self.expr_stack.pop().unwrap_or_default();
let left = self.expr_stack.pop().unwrap_or_default();
self.expr_stack.push(match op {
BinaryOperator::Add => left.op_add(right),
BinaryOperator::Subtract => left.op_subtract(right),
BinaryOperator::Multiply => left.op_multiply(right),
BinaryOperator::Divide => left.op_divide(right),
BinaryOperator::And => left.op_and(right),
BinaryOperator::Or => left.op_or(right),
BinaryOperator::Xor => left.op_xor(right),
BinaryOperator::Eq => left.op_eq(right),
BinaryOperator::Ne => left.op_ne(right),
BinaryOperator::Lt => left.op_lt(right),
BinaryOperator::Le => left.op_le(right),
BinaryOperator::Gt => left.op_gt(right),
BinaryOperator::Ge => left.op_ge(right),
});
}
Expression::Function { id, num_args } => {
let num_args = *num_args as usize;
if let Some(fnc) = self.runtime.functions.get(*id as usize) {
let mut arguments = vec![Variable::Integer(0); num_args];
for arg_num in 0..num_args {
arguments[num_args - arg_num - 1] =
self.expr_stack.pop().unwrap_or_default();
}
self.expr_stack.push((fnc)(self, arguments));
} else {
let mut arguments = vec![Variable::Integer(0); num_args];
for arg_num in 0..num_args {
arguments[num_args - arg_num - 1] =
self.expr_stack.pop().unwrap_or_default();
}
self.pos -= 1; // We need to re-evaluate the function call
return Err(Event::Function {
id: ID_EXTERNAL - *id,
arguments,
});
}
}
Expression::JmpIf { val, pos } => {
if self.expr_stack.last().is_some_and(|v| v.to_bool()) == *val {
self.expr_pos += *pos as usize;
for _ in 0..*pos {
exprs.next();
}
}
}
Expression::ArrayAccess => {
let index = self.expr_stack.pop().unwrap_or_default().to_usize();
let array = self.expr_stack.pop().unwrap_or_default().into_array();
self.expr_stack
.push(array.get(index).cloned().unwrap_or_default());
}
Expression::ArrayBuild(num_items) => {
let num_items = *num_items as usize;
let mut items = vec![Variable::Integer(0); num_items];
for arg_num in 0..num_items {
items[num_items - arg_num - 1] = self.expr_stack.pop().unwrap_or_default();
}
self.expr_stack.push(Variable::Array(items.into()));
}
}
}
let result = self.expr_stack.pop().unwrap_or_default();
self.expr_stack.clear();
self.expr_pos = 0;
Ok(result)
}
}
impl Variable {
pub fn op_add(self, other: Variable) -> Variable {
match (self, other) {
(Variable::Integer(a), Variable::Integer(b)) => Variable::Integer(a.saturating_add(b)),
(Variable::Float(a), Variable::Float(b)) => Variable::Float(a + b),
(Variable::Integer(i), Variable::Float(f))
| (Variable::Float(f), Variable::Integer(i)) => Variable::Float(i as f64 + f),
(Variable::Array(a), Variable::Array(b)) => {
Variable::Array(a.iter().chain(b.iter()).cloned().collect::<Vec<_>>().into())
}
(Variable::Array(a), b) => a.iter().cloned().chain([b]).collect::<Vec<_>>().into(),
(a, Variable::Array(b)) => [a]
.into_iter()
.chain(b.iter().cloned())
.collect::<Vec<_>>()
.into(),
(Variable::String(a), b) => {
if !a.is_empty() {
Variable::String(format!("{}{}", a, b).into())
} else {
b
}
}
(a, Variable::String(b)) => {
if !b.is_empty() {
Variable::String(format!("{}{}", a, b).into())
} else {
a
}
}
}
}
pub fn op_subtract(self, other: Variable) -> Variable {
match (self, other) {
(Variable::Integer(a), Variable::Integer(b)) => Variable::Integer(a.saturating_sub(b)),
(Variable::Float(a), Variable::Float(b)) => Variable::Float(a - b),
(Variable::Integer(a), Variable::Float(b)) => Variable::Float(a as f64 - b),
(Variable::Float(a), Variable::Integer(b)) => Variable::Float(a - b as f64),
(Variable::Array(a), b) | (b, Variable::Array(a)) => Variable::Array(
a.iter()
.filter(|v| *v != &b)
.cloned()
.collect::<Vec<_>>()
.into(),
),
(a, b) => a.parse_number().op_subtract(b.parse_number()),
}
}
pub fn op_multiply(self, other: Variable) -> Variable {
match (self, other) {
(Variable::Integer(a), Variable::Integer(b)) => Variable::Integer(a.saturating_mul(b)),
(Variable::Float(a), Variable::Float(b)) => Variable::Float(a * b),
(Variable::Integer(i), Variable::Float(f))
| (Variable::Float(f), Variable::Integer(i)) => Variable::Float(i as f64 * f),
(a, b) => a.parse_number().op_multiply(b.parse_number()),
}
}
pub fn op_divide(self, other: Variable) -> Variable {
match (self, other) {
(Variable::Integer(a), Variable::Integer(b)) => {
Variable::Float(if b != 0 { a as f64 / b as f64 } else { 0.0 })
}
(Variable::Float(a), Variable::Float(b)) => {
Variable::Float(if b != 0.0 { a / b } else { 0.0 })
}
(Variable::Integer(a), Variable::Float(b)) => {
Variable::Float(if b != 0.0 { a as f64 / b } else { 0.0 })
}
(Variable::Float(a), Variable::Integer(b)) => {
Variable::Float(if b != 0 { a / b as f64 } else { 0.0 })
}
(a, b) => a.parse_number().op_divide(b.parse_number()),
}
}
pub fn op_and(self, other: Variable) -> Variable {
Variable::Integer(i64::from(self.to_bool() & other.to_bool()))
}
pub fn op_or(self, other: Variable) -> Variable {
Variable::Integer(i64::from(self.to_bool() | other.to_bool()))
}
pub fn op_xor(self, other: Variable) -> Variable {
Variable::Integer(i64::from(self.to_bool() ^ other.to_bool()))
}
pub fn op_eq(self, other: Variable) -> Variable {
Variable::Integer(i64::from(self == other))
}
pub fn op_ne(self, other: Variable) -> Variable {
Variable::Integer(i64::from(self != other))
}
pub fn op_lt(self, other: Variable) -> Variable {
Variable::Integer(i64::from(self < other))
}
pub fn op_le(self, other: Variable) -> Variable {
Variable::Integer(i64::from(self <= other))
}
pub fn op_gt(self, other: Variable) -> Variable {
Variable::Integer(i64::from(self > other))
}
pub fn op_ge(self, other: Variable) -> Variable {
Variable::Integer(i64::from(self >= other))
}
pub fn op_not(self) -> Variable {
Variable::Integer(i64::from(!self.to_bool()))
}
pub fn op_minus(self) -> Variable {
match self {
Variable::Integer(n) => Variable::Integer(-n),
Variable::Float(n) => Variable::Float(-n),
_ => self.parse_number().op_minus(),
}
}
pub fn parse_number(&self) -> Variable {
match self {
Variable::String(s) if !s.is_empty() => {
if let Ok(n) = s.parse::<i64>() {
Variable::Integer(n)
} else if let Ok(n) = s.parse::<f64>() {
Variable::Float(n)
} else {
Variable::Integer(0)
}
}
Variable::Integer(n) => Variable::Integer(*n),
Variable::Float(n) => Variable::Float(*n),
Variable::Array(l) => Variable::Integer(l.is_empty() as i64),
_ => Variable::Integer(0),
}
}
pub fn to_bool(&self) -> bool {
match self {
Variable::Float(f) => *f != 0.0,
Variable::Integer(n) => *n != 0,
Variable::String(s) => !s.is_empty(),
Variable::Array(a) => !a.is_empty(),
}
}
}
impl PartialEq for Variable {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::Integer(a), Self::Integer(b)) => a == b,
(Self::Float(a), Self::Float(b)) => a == b,
(Self::Integer(a), Self::Float(b)) | (Self::Float(b), Self::Integer(a)) => {
*a as f64 == *b
}
(Self::String(a), Self::String(b)) => a == b,
(Self::String(_), Self::Integer(_) | Self::Float(_)) => &self.parse_number() == other,
(Self::Integer(_) | Self::Float(_), Self::String(_)) => self == &other.parse_number(),
(Self::Array(a), Self::Array(b)) => a == b,
_ => false,
}
}
}
impl Eq for Variable {}
#[allow(clippy::non_canonical_partial_ord_impl)]
impl PartialOrd for Variable {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
match (self, other) {
(Self::Integer(a), Self::Integer(b)) => a.partial_cmp(b),
(Self::Float(a), Self::Float(b)) => a.partial_cmp(b),
(Self::Integer(a), Self::Float(b)) => (*a as f64).partial_cmp(b),
(Self::Float(a), Self::Integer(b)) => a.partial_cmp(&(*b as f64)),
(Self::String(a), Self::String(b)) => a.partial_cmp(b),
(Self::String(_), Self::Integer(_) | Self::Float(_)) => {
self.parse_number().partial_cmp(other)
}
(Self::Integer(_) | Self::Float(_), Self::String(_)) => {
self.partial_cmp(&other.parse_number())
}
(Self::Array(a), Self::Array(b)) => a.partial_cmp(b),
(Self::Array(_) | Self::String(_), _) => Ordering::Greater.into(),
(_, Self::Array(_)) => Ordering::Less.into(),
}
}
}
impl Ord for Variable {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.partial_cmp(other).unwrap_or(Ordering::Greater)
}
}
impl Display for Variable {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Variable::String(v) => v.fmt(f),
Variable::Integer(v) => v.fmt(f),
Variable::Float(v) => v.fmt(f),
Variable::Array(v) => {
for (i, v) in v.iter().enumerate() {
if i > 0 {
f.write_str("\n")?;
}
v.fmt(f)?;
}
Ok(())
}
}
}
}
impl Number {
pub fn is_non_zero(&self) -> bool {
match self {
Number::Integer(n) => *n != 0,
Number::Float(n) => *n != 0.0,
}
}
}
impl Default for Number {
fn default() -> Self {
Number::Integer(0)
}
}
impl From<bool> for Number {
#[inline(always)]
fn from(b: bool) -> Self {
Number::Integer(i64::from(b))
}
}
impl From<i64> for Number {
#[inline(always)]
fn from(n: i64) -> Self {
Number::Integer(n)
}
}
impl From<f64> for Number {
#[inline(always)]
fn from(n: f64) -> Self {
Number::Float(n)
}
}
impl From<i32> for Number {
#[inline(always)]
fn from(n: i32) -> Self {
Number::Integer(n as i64)
}
}
impl<'x> From<&'x Constant> for Variable {
fn from(value: &'x Constant) -> Self {
match value {
Constant::Integer(i) => Variable::Integer(*i),
Constant::Float(f) => Variable::Float(*f),
Constant::String(s) => Variable::String(s.clone()),
}
}
}
#[cfg(test)]
mod test {
use ahash::{HashMap, HashMapExt};
use crate::{
compiler::{
VariableType,
grammar::expr::{
BinaryOperator, Expression, Token, UnaryOperator, parser::ExpressionParser,
tokenizer::Tokenizer,
},
},
runtime::Variable,
};
use evalexpr::*;
pub trait EvalExpression {
fn eval(&self, variables: &HashMap<String, Variable>) -> Option<Variable>;
}
impl EvalExpression for Vec<Expression> {
fn eval(&self, variables: &HashMap<String, Variable>) -> Option<Variable> {
let mut stack = Vec::with_capacity(self.len());
let mut exprs = self.iter();
while let Some(expr) = exprs.next() {
match expr {
Expression::Variable(VariableType::Global(v)) => {
stack.push(variables.get(v)?.clone());
}
Expression::Constant(val) => {
stack.push(Variable::from(val));
}
Expression::UnaryOperator(op) => {
let value = stack.pop()?;
stack.push(match op {
UnaryOperator::Not => value.op_not(),
UnaryOperator::Minus => value.op_minus(),
});
}
Expression::BinaryOperator(op) => {
let right = stack.pop()?;
let left = stack.pop()?;
stack.push(match op {
BinaryOperator::Add => left.op_add(right),
BinaryOperator::Subtract => left.op_subtract(right),
BinaryOperator::Multiply => left.op_multiply(right),
BinaryOperator::Divide => left.op_divide(right),
BinaryOperator::And => left.op_and(right),
BinaryOperator::Or => left.op_or(right),
BinaryOperator::Xor => left.op_xor(right),
BinaryOperator::Eq => left.op_eq(right),
BinaryOperator::Ne => left.op_ne(right),
BinaryOperator::Lt => left.op_lt(right),
BinaryOperator::Le => left.op_le(right),
BinaryOperator::Gt => left.op_gt(right),
BinaryOperator::Ge => left.op_ge(right),
});
}
Expression::JmpIf { val, pos } => {
if stack.last()?.to_bool() == *val {
for _ in 0..*pos {
exprs.next();
}
}
}
_ => unreachable!("Invalid expression"),
}
}
stack.pop()
}
}
#[test]
fn eval_expression() {
let mut variables = HashMap::from_iter([
("A".to_string(), Variable::Integer(0)),
("B".to_string(), Variable::Integer(0)),
("C".to_string(), Variable::Integer(0)),
("D".to_string(), Variable::Integer(0)),
("E".to_string(), Variable::Integer(0)),
("F".to_string(), Variable::Integer(0)),
("G".to_string(), Variable::Integer(0)),
("H".to_string(), Variable::Integer(0)),
("I".to_string(), Variable::Integer(0)),
("J".to_string(), Variable::Integer(0)),
]);
let num_vars = variables.len();
for expr in [
"A + B",
"A * B",
"A / B",
"A - B",
"-A",
"A == B",
"A != B",
"A > B",
"A < B",
"A >= B",
"A <= B",
"A + B * C - D / E",
"A + B + C - D - E",
"(A + B) * (C - D) / E",
"A - B + C * D / E * F - G",
"A + B * C - D / E",
"(A + B) * (C - D) / E",
"A - B + C / D * E",
"(A + B) / (C - D) + E",
"A * (B + C) - D / E",
"A / (B - C + D) * E",
"(A + B) * C - D / (E + F)",
"A * B - C + D / E",
"A + B - C * D / E",
"(A * B + C) / D - E",
"A - B / C + D * E",
"A + B * (C - D) / E",
"A * B / C + (D - E)",
"(A - B) * C / D + E",
"A * (B / C) - D + E",
"(A + B) / (C + D) * E",
"A - B * C / D + E",
"A + (B - C) * D / E",
"(A + B) * (C / D) - E",
"A - B / (C * D) + E",
"(A + B) > (C - D) && E <= F",
"A * B == C / D || E - F != G + H",
"A / B >= C * D && E + F < G - H",
"(A * B - C) != (D / E + F) && G > H",
"A - B < C && D + E >= F * G",
"(A * B) > C && (D / E) < F || G == H",
"(A + B) <= (C - D) || E > F && G != H",
"A * B != C + D || E - F == G / H",
"A >= B * C && D < E - F || G != H + I",
"(A / B + C) > D && E * F <= G - H",
"A * (B - C) == D && E / F > G + H",
"(A - B + C) != D || E * F >= G && H < I",
"A < B / C && D + E * F == G - H",
"(A + B * C) <= D && E > F / G",
"(A * B - C) > D || E <= F + G && H != I",
"A != B / C && D == E * F - G",
"A <= B + C - D && E / F > G * H",
"(A - B * C) < D || E >= F + G && H != I",
"(A + B) / C == D && E - F < G * H",
"A * B != C && D >= E + F / G || H < I",
"!(A * B != C) && !(D >= E + F / G) || !(H < I)",
"-A - B - (- C - D) - E - (-F)",
] {
println!("Testing {}", expr);
for (pos, v) in variables.values_mut().enumerate() {
*v = Variable::Integer(pos as i64 + 1);
}
assert_expr(expr, &variables);
for (pos, v) in variables.values_mut().enumerate() {
*v = Variable::Integer((num_vars - pos) as i64);
}
assert_expr(expr, &variables);
}
for expr in [
"true && false",
"!true || false",
"true && !false",
"!(true && false)",
"true || true && false",
"!false && (true || false)",
"!(true || !false) && true",
"!(!true && !false)",
"true || false && !true",
"!(true && true) || !false",
"!(!true || !false) && (!false) && !(!true)",
] {
let pexp = parse_expression(expr.replace("true", "1").replace("false", "0").as_str());
let result = pexp.eval(&HashMap::new()).unwrap();
//println!("{} => {:?}", expr, result);
match (eval(expr).expect(expr), result) {
(Value::Float(a), Variable::Float(b)) if a == b => (),
(Value::Float(a), Variable::Integer(b)) if a == b as f64 => (),
(Value::Boolean(a), Variable::Integer(b)) if a == (b != 0) => (),
(a, b) => {
panic!("{} => {:?} != {:?}", expr, a, b)
}
}
}
}
fn assert_expr(expr: &str, variables: &HashMap<String, Variable>) {
let e = parse_expression(expr);
let result = e.eval(variables).unwrap();
let mut str_expr = expr.to_string();
let mut str_expr_float = expr.to_string();
for (k, v) in variables {
let v = v.to_string();
if v.contains('.') {
str_expr_float = str_expr_float.replace(k, &v);
} else {
str_expr_float = str_expr_float.replace(k, &format!("{}.0", v));
}
str_expr = str_expr.replace(k, &v);
}
assert_eq!(
parse_expression(&str_expr)
.eval(&HashMap::new())
.unwrap()
.to_number()
.to_float(),
result.to_number().to_float()
);
assert_eq!(
parse_expression(&str_expr_float)
.eval(&HashMap::new())
.unwrap()
.to_number()
.to_float(),
result.to_number().to_float()
);
//println!("{str_expr} ({e:?}) => {result:?}");
match (
eval(&str_expr_float)
.map(|v| {
// Divisions by zero are converted to 0.0
if matches!(&v, Value::Float(f) if f64::is_infinite(*f)) {
Value::Float(0.0)
} else {
v
}
})
.expect(&str_expr),
result,
) {
(Value::Float(a), Variable::Float(b)) if a == b => (),
(Value::Float(a), Variable::Integer(b)) if a == b as f64 => (),
(Value::Boolean(a), Variable::Integer(b)) if a == (b != 0) => (),
(a, b) => {
panic!("{} => {:?} != {:?}", str_expr, a, b)
}
}
}
fn parse_expression(expr: &str) -> Vec<Expression> {
ExpressionParser::from_tokenizer(Tokenizer::new(expr, |var_name: &str, _: bool| {
Ok::<_, String>(Token::Variable(VariableType::Global(var_name.to_string())))
}))
.parse()
.unwrap()
.output
}
}
+803
View File
@@ -0,0 +1,803 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*
* Modified by Coffey Labs in 2026 for INBUXA.
*/
pub mod actions;
pub mod context;
pub mod eval;
pub mod expression;
pub mod tests;
pub mod variables;
use ahash::{AHashMap, AHashSet};
use mail_parser::HeaderName;
#[cfg(not(test))]
use mail_parser::{Encoding, Message, MessageParser, MessagePart, PartType};
use std::{borrow::Cow, fmt::Display, hash::Hash, ops::Deref, sync::Arc};
#[cfg(not(test))]
use crate::Context;
use crate::{
ExternalId, Function, FunctionMap, Input, Metadata, Runtime, Script, Sieve,
compiler::{
Number,
grammar::{Capability, Invalid, expr::parser::ID_EXTERNAL},
},
};
use self::eval::ToString;
#[derive(Debug, Clone)]
#[cfg_attr(
any(test, feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
pub enum Variable {
String(Arc<String>),
Integer(i64),
Float(f64),
Array(Arc<Vec<Variable>>),
}
#[derive(Debug)]
pub enum RuntimeError {
TooManyIncludes,
InvalidInstruction(Invalid),
ScriptErrorMessage(String),
CapabilityNotAllowed(Capability),
CapabilityNotSupported(String),
CPULimitReached,
}
impl Default for Variable {
fn default() -> Self {
Variable::String(Arc::new(String::new()))
}
}
impl Variable {
pub fn to_string(&self) -> Cow<'_, str> {
match self {
Variable::String(s) => Cow::Borrowed(s.as_str()),
Variable::Integer(n) => Cow::Owned(n.to_string()),
Variable::Float(n) => Cow::Owned(n.to_string()),
Variable::Array(l) => Cow::Owned(l.to_string()),
}
}
pub fn to_number(&self) -> Number {
self.to_number_checked()
.unwrap_or(Number::Float(f64::INFINITY))
}
pub fn to_number_checked(&self) -> Option<Number> {
let s = match self {
Variable::Integer(n) => return Number::Integer(*n).into(),
Variable::Float(n) => return Number::Float(*n).into(),
Variable::String(s) if !s.is_empty() => s.as_str(),
_ => return None,
};
if !s.contains('.') {
s.parse::<i64>().map(Number::Integer).ok()
} else {
s.parse::<f64>().map(Number::Float).ok()
}
}
pub fn to_integer(&self) -> i64 {
match self {
Variable::Integer(n) => *n,
Variable::Float(n) => *n as i64,
Variable::String(s) if !s.is_empty() => s.parse::<i64>().unwrap_or(0),
_ => 0,
}
}
pub fn to_usize(&self) -> usize {
match self {
Variable::Integer(n) => *n as usize,
Variable::Float(n) => *n as usize,
Variable::String(s) if !s.is_empty() => s.parse::<usize>().unwrap_or(0),
_ => 0,
}
}
pub fn len(&self) -> usize {
match self {
Variable::String(s) => s.len(),
Variable::Integer(_) | Variable::Float(_) => 2,
Variable::Array(l) => l.iter().map(|v| v.len() + 2).sum(),
}
}
pub fn is_empty(&self) -> bool {
match self {
Variable::String(s) => s.is_empty(),
_ => false,
}
}
pub fn as_array(&self) -> Option<&[Variable]> {
match self {
Variable::Array(l) => Some(l),
_ => None,
}
}
pub fn into_array(self) -> Arc<Vec<Variable>> {
match self {
Variable::Array(l) => l,
v if !v.is_empty() => vec![v].into(),
_ => vec![].into(),
}
}
pub fn to_array(&self) -> Arc<Vec<Variable>> {
match self {
Variable::Array(l) => l.clone(),
v if !v.is_empty() => vec![v.clone()].into(),
_ => vec![].into(),
}
}
pub fn into_string_array(self) -> Vec<String> {
match self {
Variable::Array(l) => l.iter().map(|i| i.to_string().into_owned()).collect(),
v if !v.is_empty() => vec![v.to_string().into_owned()],
_ => vec![],
}
}
pub fn to_string_array(&self) -> Vec<Cow<'_, str>> {
match self {
Variable::Array(l) => l.iter().map(|i| i.to_string()).collect(),
v if !v.is_empty() => vec![v.to_string()],
_ => vec![],
}
}
}
impl From<String> for Variable {
fn from(s: String) -> Self {
Variable::String(s.into())
}
}
impl<'x> From<&'x String> for Variable {
fn from(s: &'x String) -> Self {
Variable::String(s.as_str().to_string().into())
}
}
impl<'x> From<&'x str> for Variable {
fn from(s: &'x str) -> Self {
Variable::String(s.to_string().into())
}
}
impl<'x> From<Cow<'x, str>> for Variable {
fn from(s: Cow<'x, str>) -> Self {
match s {
Cow::Borrowed(s) => Variable::String(s.to_string().into()),
Cow::Owned(s) => Variable::String(s.into()),
}
}
}
impl From<Vec<Variable>> for Variable {
fn from(l: Vec<Variable>) -> Self {
Variable::Array(l.into())
}
}
impl From<Number> for Variable {
fn from(n: Number) -> Self {
match n {
Number::Integer(n) => Variable::Integer(n),
Number::Float(n) => Variable::Float(n),
}
}
}
impl From<usize> for Variable {
fn from(n: usize) -> Self {
Variable::Integer(n as i64)
}
}
impl From<i64> for Variable {
fn from(n: i64) -> Self {
Variable::Integer(n)
}
}
impl From<u64> for Variable {
fn from(n: u64) -> Self {
Variable::Integer(n as i64)
}
}
impl From<f64> for Variable {
fn from(n: f64) -> Self {
Variable::Float(n)
}
}
impl From<i32> for Variable {
fn from(n: i32) -> Self {
Variable::Integer(n as i64)
}
}
impl From<u32> for Variable {
fn from(n: u32) -> Self {
Variable::Integer(n as i64)
}
}
impl From<bool> for Variable {
fn from(b: bool) -> Self {
Variable::Integer(i64::from(b))
}
}
impl PartialEq for Number {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::Integer(a), Self::Integer(b)) => a == b,
(Self::Float(a), Self::Float(b)) => a == b,
(Self::Integer(a), Self::Float(b)) => (*a as f64) == *b,
(Self::Float(a), Self::Integer(b)) => *a == (*b as f64),
}
}
}
impl Eq for Number {}
impl PartialOrd for Number {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
let (a, b) = match (self, other) {
(Number::Integer(a), Number::Integer(b)) => return a.partial_cmp(b),
(Number::Float(a), Number::Float(b)) => (*a, *b),
(Number::Integer(a), Number::Float(b)) => (*a as f64, *b),
(Number::Float(a), Number::Integer(b)) => (*a, *b as f64),
};
a.partial_cmp(&b)
}
}
impl self::eval::ToString for Vec<Variable> {
fn to_string(&self) -> String {
let mut result = String::with_capacity(self.len() * 10);
for item in self {
if !result.is_empty() {
result.push_str("\r\n");
}
match item {
Variable::String(v) => result.push_str(v),
Variable::Integer(v) => result.push_str(&v.to_string()),
Variable::Float(v) => result.push_str(&v.to_string()),
Variable::Array(_) => {}
}
}
result
}
}
impl Hash for Variable {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
match self {
Variable::String(s) => s.hash(state),
Variable::Integer(n) => n.hash(state),
Variable::Float(n) => n.to_bits().hash(state),
Variable::Array(l) => l.hash(state),
}
}
}
#[cfg(not(test))]
impl Runtime {
pub fn filter<'z: 'x, 'x>(&'z self, raw_message: &'x [u8]) -> Context<'x> {
Context::new(
self,
MessageParser::new()
.parse(raw_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()
}),
)
}
pub fn filter_parsed<'z: 'x, 'x>(&'z self, message: Message<'x>) -> Context<'x> {
Context::new(self, message)
}
}
impl Default for Runtime {
fn default() -> Self {
Self::new()
}
}
impl Runtime {
pub fn new() -> Self {
#[allow(unused_mut)]
let mut allowed_capabilities = AHashSet::from_iter(Capability::all().iter().cloned());
#[cfg(test)]
allowed_capabilities.insert(Capability::Other("vnd.inbuxa.testsuite".to_string()));
Runtime {
allowed_capabilities,
environment: AHashMap::from_iter([
("name".into(), "inbuxa Sieve".into()),
("version".into(), env!("CARGO_PKG_VERSION").into()),
]),
metadata: Vec::new(),
include_scripts: AHashMap::new(),
max_nested_includes: 3,
cpu_limit: 5000,
max_variable_size: 4096,
max_redirects: 1,
max_received_headers: 10,
protected_headers: vec![
HeaderName::Other("Original-Subject".into()),
HeaderName::Other("Original-From".into()),
],
valid_notification_uris: AHashSet::new(),
valid_ext_lists: AHashSet::new(),
vacation_use_orig_rcpt: false,
vacation_default_subject: "Automated reply".into(),
vacation_subject_prefix: "Auto: ".into(),
max_header_size: 1024,
max_out_messages: 3,
default_vacation_expiry: 30 * 86400,
default_duplicate_expiry: 7 * 86400,
local_hostname: "localhost".into(),
functions: Vec::new(),
}
}
pub fn set_cpu_limit(&mut self, size: usize) {
self.cpu_limit = size;
}
pub fn with_cpu_limit(mut self, size: usize) -> Self {
self.cpu_limit = size;
self
}
pub fn set_max_nested_includes(&mut self, size: usize) {
self.max_nested_includes = size;
}
pub fn with_max_nested_includes(mut self, size: usize) -> Self {
self.max_nested_includes = size;
self
}
pub fn set_max_redirects(&mut self, size: usize) {
self.max_redirects = size;
}
pub fn with_max_redirects(mut self, size: usize) -> Self {
self.max_redirects = size;
self
}
pub fn set_max_out_messages(&mut self, size: usize) {
self.max_out_messages = size;
}
pub fn with_max_out_messages(mut self, size: usize) -> Self {
self.max_out_messages = size;
self
}
pub fn set_max_received_headers(&mut self, size: usize) {
self.max_received_headers = size;
}
pub fn with_max_received_headers(mut self, size: usize) -> Self {
self.max_received_headers = size;
self
}
pub fn set_max_variable_size(&mut self, size: usize) {
self.max_variable_size = size;
}
pub fn with_max_variable_size(mut self, size: usize) -> Self {
self.max_variable_size = size;
self
}
pub fn set_max_header_size(&mut self, size: usize) {
self.max_header_size = size;
}
pub fn with_max_header_size(mut self, size: usize) -> Self {
self.max_header_size = size;
self
}
pub fn set_default_vacation_expiry(&mut self, expiry: u64) {
self.default_vacation_expiry = expiry;
}
pub fn with_default_vacation_expiry(mut self, expiry: u64) -> Self {
self.default_vacation_expiry = expiry;
self
}
pub fn set_default_duplicate_expiry(&mut self, expiry: u64) {
self.default_duplicate_expiry = expiry;
}
pub fn with_default_duplicate_expiry(mut self, expiry: u64) -> Self {
self.default_duplicate_expiry = expiry;
self
}
pub fn set_capability(&mut self, capability: impl Into<Capability>) {
self.allowed_capabilities.insert(capability.into());
}
pub fn with_capability(mut self, capability: impl Into<Capability>) -> Self {
self.set_capability(capability);
self
}
pub fn unset_capability(&mut self, capability: impl Into<Capability>) {
self.allowed_capabilities.remove(&capability.into());
}
pub fn without_capability(mut self, capability: impl Into<Capability>) -> Self {
self.unset_capability(capability);
self
}
pub fn without_capabilities(
mut self,
capabilities: impl IntoIterator<Item = impl Into<Capability>>,
) -> Self {
for capability in capabilities {
self.allowed_capabilities.remove(&capability.into());
}
self
}
pub fn set_protected_header(&mut self, header_name: impl Into<Cow<'static, str>>) {
if let Some(header_name) = HeaderName::parse(header_name) {
self.protected_headers.push(header_name);
}
}
pub fn with_protected_header(mut self, header_name: impl Into<Cow<'static, str>>) -> Self {
self.set_protected_header(header_name);
self
}
pub fn with_protected_headers(
mut self,
header_names: impl IntoIterator<Item = impl Into<Cow<'static, str>>>,
) -> Self {
self.protected_headers = header_names
.into_iter()
.filter_map(HeaderName::parse)
.collect();
self
}
pub fn set_env_variable(
&mut self,
name: impl Into<Cow<'static, str>>,
value: impl Into<Variable>,
) {
self.environment.insert(name.into(), value.into());
}
pub fn with_env_variable(
mut self,
name: impl Into<Cow<'static, str>>,
value: impl Into<Cow<'static, str>>,
) -> Self {
self.set_env_variable(name.into(), value.into());
self
}
pub fn set_medatata(
&mut self,
name: impl Into<Metadata<String>>,
value: impl Into<Cow<'static, str>>,
) {
self.metadata.push((name.into(), value.into()));
}
pub fn with_metadata(
mut self,
name: impl Into<Metadata<String>>,
value: impl Into<Cow<'static, str>>,
) -> Self {
self.set_medatata(name, value);
self
}
pub fn set_valid_notification_uri(&mut self, uri: impl Into<Cow<'static, str>>) {
self.valid_notification_uris.insert(uri.into());
}
pub fn with_valid_notification_uri(mut self, uri: impl Into<Cow<'static, str>>) -> Self {
self.valid_notification_uris.insert(uri.into());
self
}
pub fn with_valid_notification_uris(
mut self,
uris: impl IntoIterator<Item = impl Into<Cow<'static, str>>>,
) -> Self {
self.valid_notification_uris = uris.into_iter().map(Into::into).collect();
self
}
pub fn set_valid_ext_list(&mut self, name: impl Into<Cow<'static, str>>) {
self.valid_ext_lists.insert(name.into());
}
pub fn with_valid_ext_list(mut self, name: impl Into<Cow<'static, str>>) -> Self {
self.set_valid_ext_list(name);
self
}
pub fn set_vacation_use_orig_rcpt(&mut self, value: bool) {
self.vacation_use_orig_rcpt = value;
}
pub fn with_valid_ext_lists(
mut self,
lists: impl IntoIterator<Item = impl Into<Cow<'static, str>>>,
) -> Self {
self.valid_ext_lists = lists.into_iter().map(Into::into).collect();
self
}
pub fn with_vacation_use_orig_rcpt(mut self, value: bool) -> Self {
self.set_vacation_use_orig_rcpt(value);
self
}
pub fn set_vacation_default_subject(&mut self, value: impl Into<Cow<'static, str>>) {
self.vacation_default_subject = value.into();
}
pub fn with_vacation_default_subject(mut self, value: impl Into<Cow<'static, str>>) -> Self {
self.set_vacation_default_subject(value);
self
}
pub fn set_vacation_subject_prefix(&mut self, value: impl Into<Cow<'static, str>>) {
self.vacation_subject_prefix = value.into();
}
pub fn with_vacation_subject_prefix(mut self, value: impl Into<Cow<'static, str>>) -> Self {
self.set_vacation_subject_prefix(value);
self
}
pub fn set_local_hostname(&mut self, value: impl Into<Cow<'static, str>>) {
self.local_hostname = value.into();
}
pub fn with_local_hostname(mut self, value: impl Into<Cow<'static, str>>) -> Self {
self.set_local_hostname(value);
self
}
pub fn with_functions(mut self, fnc_map: &mut FunctionMap) -> Self {
self.functions = std::mem::take(&mut fnc_map.functions);
self
}
pub fn set_functions(&mut self, fnc_map: &mut FunctionMap) {
self.functions = std::mem::take(&mut fnc_map.functions);
}
}
impl FunctionMap {
pub fn new() -> Self {
FunctionMap {
map: Default::default(),
functions: Default::default(),
}
}
pub fn with_function(self, name: impl Into<String>, fnc: Function) -> Self {
self.with_function_args(name, fnc, 1)
}
pub fn with_function_no_args(self, name: impl Into<String>, fnc: Function) -> Self {
self.with_function_args(name, fnc, 0)
}
pub fn with_function_args(
mut self,
name: impl Into<String>,
fnc: Function,
num_args: u32,
) -> Self {
self.map
.insert(name.into(), (self.functions.len() as u32, num_args));
self.functions.push(fnc);
self
}
pub fn with_external_function(
mut self,
name: impl Into<String>,
id: ExternalId,
num_args: u32,
) -> Self {
self.set_external_function(name, id, num_args);
self
}
pub fn set_external_function(
&mut self,
name: impl Into<String>,
id: ExternalId,
num_args: u32,
) {
self.map.insert(name.into(), (ID_EXTERNAL - id, num_args));
}
}
impl Input {
pub fn script(name: impl Into<Script>, script: impl Into<Arc<Sieve>>) -> Self {
Input::Script {
name: name.into(),
script: script.into(),
}
}
pub fn success() -> Self {
Input::True
}
pub fn fail() -> Self {
Input::False
}
pub fn result(result: Variable) -> Self {
Input::FncResult(result)
}
}
impl From<bool> for Input {
fn from(value: bool) -> Self {
if value { Input::True } else { Input::False }
}
}
impl From<Variable> for Input {
fn from(value: Variable) -> Self {
Input::FncResult(value)
}
}
impl Deref for Script {
type Target = String;
fn deref(&self) -> &Self::Target {
match self {
Script::Personal(name) | Script::Global(name) => name,
}
}
}
impl AsRef<str> for Script {
fn as_ref(&self) -> &str {
match self {
Script::Personal(name) | Script::Global(name) => name.as_str(),
}
}
}
impl AsRef<String> for Script {
fn as_ref(&self) -> &String {
match self {
Script::Personal(name) | Script::Global(name) => name,
}
}
}
impl Script {
pub fn into_string(self) -> String {
match self {
Script::Personal(name) | Script::Global(name) => name,
}
}
pub fn as_str(&self) -> &String {
match self {
Script::Personal(name) | Script::Global(name) => name,
}
}
}
impl Display for Script {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
impl From<String> for Script {
fn from(name: String) -> Self {
Script::Personal(name)
}
}
impl From<&str> for Script {
fn from(name: &str) -> Self {
Script::Personal(name.to_string())
}
}
impl<T> Metadata<T> {
pub fn server(annotation: impl Into<T>) -> Self {
Metadata::Server {
annotation: annotation.into(),
}
}
pub fn mailbox(name: impl Into<T>, annotation: impl Into<T>) -> Self {
Metadata::Mailbox {
name: name.into(),
annotation: annotation.into(),
}
}
}
impl From<String> for Metadata<String> {
fn from(annotation: String) -> Self {
Metadata::Server { annotation }
}
}
impl From<&'_ str> for Metadata<String> {
fn from(annotation: &'_ str) -> Self {
Metadata::Server {
annotation: annotation.to_string(),
}
}
}
impl From<(String, String)> for Metadata<String> {
fn from((name, annotation): (String, String)) -> Self {
Metadata::Mailbox { name, annotation }
}
}
impl From<(&'_ str, &'_ str)> for Metadata<String> {
fn from((name, annotation): (&'_ str, &'_ str)) -> Self {
Metadata::Mailbox {
name: name.to_string(),
annotation: annotation.to_string(),
}
}
}
+176
View File
@@ -0,0 +1,176 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::{borrow::Cow, sync::Arc};
use crate::{
MatchAs,
compiler::{
Number, Value,
grammar::{Comparator, RelationalMatch},
},
runtime::Variable,
};
use super::glob::GlobPattern;
pub(crate) trait Comparable {
fn to_str(&'_ self) -> Cow<'_, str>;
fn to_number(&self) -> Number;
}
impl Comparator {
pub(crate) fn is(&self, a: &impl Comparable, b: &impl Comparable) -> bool {
match self {
Comparator::Octet => a.to_str() == b.to_str(),
Comparator::AsciiNumeric => RelationalMatch::Eq.cmp(&a.to_number(), &b.to_number()),
_ => a.to_str().to_lowercase() == b.to_str().to_lowercase(),
}
}
pub(crate) fn contains(&self, haystack: &str, needle: &str) -> bool {
needle.is_empty()
|| match self {
Comparator::Octet => haystack.contains(needle),
_ => haystack.to_lowercase().contains(&needle.to_lowercase()),
}
}
pub(crate) fn relational(
&self,
relation: &RelationalMatch,
a: &impl Comparable,
b: &impl Comparable,
) -> bool {
match self {
Comparator::Octet => relation.cmp(a.to_str().as_ref(), b.to_str().as_ref()),
Comparator::AsciiNumeric => relation.cmp(&a.to_number(), &b.to_number()),
_ => relation.cmp(&a.to_str().to_lowercase(), &b.to_str().to_lowercase()),
}
}
pub(crate) fn matches(
&self,
value: &str,
pattern: &str,
capture_positions: u64,
captured_values: &mut Vec<(usize, String)>,
) -> bool {
let pattern = GlobPattern::compile(pattern, matches!(self, Comparator::AsciiCaseMap));
match self {
Comparator::AsciiCaseMap if capture_positions == 0 => pattern.matches(value),
Comparator::AsciiCaseMap => pattern.capture(value, capture_positions, captured_values),
_ if capture_positions == 0 => pattern.matches(value),
_ => pattern.capture(value, capture_positions, captured_values),
}
}
pub(crate) fn regex(
&self,
pattern: &Value,
pattern_expr: &Variable,
value: &str,
capture_positions: u64,
captured_values: &mut Vec<(usize, String)>,
) -> bool {
if let Value::Regex(regex) = pattern {
let lazy_regex = regex.regex.0.load();
if let Some(regex) = lazy_regex.as_ref() {
eval_regex(regex, value, capture_positions, captured_values)
} else {
match fancy_regex::Regex::new(&regex.expr) {
Ok(fancy_regex) => {
let result =
eval_regex(&fancy_regex, value, capture_positions, captured_values);
regex.regex.0.store(Arc::new(Some(fancy_regex)));
result
}
Err(err) => {
debug_assert!(false, "Failed to compile regex: {err:?}");
false
}
}
}
} else {
match fancy_regex::Regex::new(pattern_expr.to_string().as_ref()) {
Ok(regex) => eval_regex(&regex, value, capture_positions, captured_values),
Err(err) => {
debug_assert!(false, "Failed to compile regex: {err:?}");
false
}
}
}
}
pub(crate) fn as_match(&self) -> MatchAs {
match self {
Comparator::AsciiCaseMap => MatchAs::Lowercase,
Comparator::AsciiNumeric => MatchAs::Number,
_ => MatchAs::Octet,
}
}
}
fn eval_regex(
regex: &fancy_regex::Regex,
value: &str,
mut capture_positions: u64,
captured_values: &mut Vec<(usize, String)>,
) -> bool {
if capture_positions == 0 {
regex.is_match(value).unwrap_or_default()
} else if let Ok(Some(captures)) = regex.captures(value) {
captured_values.clear();
while capture_positions != 0 {
let index = 63 - capture_positions.leading_zeros();
capture_positions ^= 1 << index;
if let Some(match_var) = captures.get(index as usize) {
captured_values.push((index as usize, match_var.as_str().to_string()));
}
}
true
} else {
false
}
}
impl Comparable for Variable {
fn to_str(&'_ self) -> Cow<'_, str> {
self.to_string()
}
fn to_number(&self) -> Number {
self.to_number()
}
}
impl Comparable for &str {
fn to_str(&'_ self) -> Cow<'_, str> {
(*self).into()
}
fn to_number(&self) -> Number {
self.parse::<f64>()
.map(Number::Float)
.unwrap_or(Number::Float(0.0))
}
}
impl RelationalMatch {
pub fn cmp<T>(&self, a: &T, b: &T) -> bool
where
T: PartialOrd + ?Sized,
{
match self {
RelationalMatch::Gt => a.gt(b),
RelationalMatch::Ge => a.ge(b),
RelationalMatch::Lt => a.lt(b),
RelationalMatch::Le => a.le(b),
RelationalMatch::Eq => a.eq(b),
RelationalMatch::Ne => a.ne(b),
}
}
}
+300
View File
@@ -0,0 +1,300 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::char::REPLACEMENT_CHARACTER;
use crate::MAX_MATCH_VARIABLES;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GlobPattern {
pattern: Vec<PatternChar>,
to_lower: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PatternChar {
WildcardMany { num: usize, match_pos: usize },
WildcardSingle { match_pos: usize },
Char { char: char, match_pos: usize },
}
impl GlobPattern {
pub fn compile(pattern: &str, to_lower: bool) -> Self {
let mut chars = Vec::new();
let mut is_escaped = false;
let mut str = pattern.chars().peekable();
while let Some(char) = str.next() {
match char {
'*' if !is_escaped => {
let mut num = 1;
while let Some('*') = str.peek() {
num += 1;
str.next();
}
chars.push(PatternChar::WildcardMany { num, match_pos: 0 });
}
'?' if !is_escaped => {
chars.push(PatternChar::WildcardSingle { match_pos: 0 });
}
'\\' if !is_escaped => {
is_escaped = true;
continue;
}
_ => {
if is_escaped {
is_escaped = false;
}
if to_lower && char.is_uppercase() {
for char in char.to_lowercase() {
chars.push(PatternChar::Char { char, match_pos: 0 });
}
} else {
chars.push(PatternChar::Char { char, match_pos: 0 });
}
}
}
}
GlobPattern {
pattern: chars,
to_lower,
}
}
// Credits: Algorithm ported from https://research.swtch.com/glob
pub fn matches(&self, value: &str) -> bool {
let value = if self.to_lower {
value.to_lowercase().chars().collect::<Vec<_>>()
} else {
value.chars().collect::<Vec<_>>()
};
let mut px = 0;
let mut nx = 0;
let mut next_px = 0;
let mut next_nx = 0;
while px < self.pattern.len() || nx < value.len() {
match self.pattern.get(px) {
Some(PatternChar::Char { char, .. }) => {
if matches!(value.get(nx), Some(nc) if nc == char ) {
px += 1;
nx += 1;
continue;
}
}
Some(PatternChar::WildcardSingle { .. }) if nx < value.len() => {
px += 1;
nx += 1;
continue;
}
Some(PatternChar::WildcardMany { .. }) => {
next_px = px;
next_nx = nx + 1;
px += 1;
continue;
}
_ => (),
}
if 0 < next_nx && next_nx <= value.len() {
px = next_px;
nx = next_nx;
continue;
}
return false;
}
true
}
pub fn capture(
mut self,
value_: &str,
capture_positions: u64,
captured_values: &mut Vec<(usize, String)>,
) -> bool {
let value = if self.to_lower {
let mut value = Vec::with_capacity(value_.len());
for char in value_.chars() {
if char.is_uppercase() {
for (pos, lowerchar) in char.to_lowercase().enumerate() {
value.push((
lowerchar,
if pos == 0 {
char
} else {
REPLACEMENT_CHARACTER
},
));
}
} else {
value.push((char, char));
}
}
value
} else {
value_.chars().map(|char| (char, char)).collect::<Vec<_>>()
};
let mut px = 0;
let mut nx = 0;
let mut next_px = 0;
let mut next_nx = 0;
while px < self.pattern.len() || nx < value.len() {
match self.pattern.get_mut(px) {
Some(PatternChar::Char { char, match_pos }) => {
if matches!(value.get(nx), Some(nc) if &nc.0 == char ) {
*match_pos = nx;
px += 1;
nx += 1;
continue;
}
}
Some(PatternChar::WildcardSingle { match_pos }) if nx < value.len() => {
*match_pos = nx;
px += 1;
nx += 1;
continue;
}
Some(PatternChar::WildcardMany { match_pos, .. }) => {
*match_pos = nx;
next_px = px;
next_nx = nx + 1;
px += 1;
continue;
}
_ => (),
}
if 0 < next_nx && next_nx <= value.len() {
px = next_px;
nx = next_nx;
continue;
}
return false;
}
let mut last_pos = 0;
captured_values.clear();
if capture_positions & 1 != 0 {
captured_values.push((0usize, value_.to_string()));
}
let mut wildcard_pos: usize = 1;
for item in &mut self.pattern {
if wildcard_pos <= MAX_MATCH_VARIABLES as usize {
last_pos = match item {
PatternChar::WildcardMany { num, match_pos } => {
while *num > 1 {
if capture_positions & (1 << wildcard_pos) != 0 {
captured_values.push((wildcard_pos, String::with_capacity(0)));
}
wildcard_pos += 1;
*num -= 1;
}
if capture_positions & (1 << wildcard_pos) != 0 {
if let Some(range) = value.get(last_pos..*match_pos) {
captured_values.push((
wildcard_pos,
range
.iter()
.filter_map(|(_, char)| {
if char != &REPLACEMENT_CHARACTER {
Some(char)
} else {
None
}
})
.collect::<String>(),
));
} else {
debug_assert!(false, "Glob pattern failure.");
return false;
}
}
wildcard_pos += 1;
*match_pos
}
PatternChar::WildcardSingle { match_pos } => {
if capture_positions & (1 << wildcard_pos) != 0 {
if let Some((char, orig_char)) = value.get(*match_pos) {
captured_values.push((
wildcard_pos,
(if orig_char != &REPLACEMENT_CHARACTER {
orig_char
} else {
char
})
.to_string(),
));
} else {
debug_assert!(false, "Glob pattern failure.");
return false;
}
}
wildcard_pos += 1;
*match_pos
}
PatternChar::Char { match_pos, .. } => *match_pos,
} + 1;
} else {
break;
}
}
true
}
}
#[cfg(test)]
mod tests {
use crate::runtime::tests::glob::GlobPattern;
#[test]
fn glob_match() {
for (value, pattern, expected_result) in [
(
"frop.......frop.........frop....",
"?*frop*",
vec!["f", "rop.......", ".........frop...."],
),
("frop:frup:frop", "*:*:*", vec!["frop", "frup", "frop"]),
(
"a b c d e f g",
"? ? ? ? ? ? ?",
vec!["a", "b", "c", "d", "e", "f", "g"],
),
("puk pok puk pok", "pu*ok", vec!["k pok puk p"]),
("snot kip snot", "snot*snot", vec![" kip "]),
(
"klopfropstroptop",
"*fr??*top",
vec!["klop", "o", "p", "strop"],
),
("toptoptop", "*top", vec!["toptop"]),
(
"Fehlende Straße zur Karte hinzufügen",
"FEHLENDE * ZUR Karte HINZUFÜGEN",
vec!["Straße"],
),
] {
let p = GlobPattern::compile(pattern, true);
let mut match_values = Vec::new();
assert!(
p.clone().capture(value, u64::MAX ^ 1, &mut match_values),
"{value:?} {pattern:?}",
);
assert_eq!(
match_values.into_iter().map(|(_, v)| v).collect::<Vec<_>>(),
expected_result,
"{value:?} {pattern:?}",
);
assert!(p.matches(value), "{value:?} {pattern:?}",);
}
}
}
+203
View File
@@ -0,0 +1,203 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::slice::Iter;
use mail_parser::{Message, MessagePart, MimeHeaders, PartType};
use crate::Context;
#[derive(Debug)]
pub(crate) enum ContentTypeFilter {
Type(String),
TypeSubtype((String, String)),
}
pub(crate) struct SubpartIterator<'x> {
ctx: &'x Context<'x>,
iter: Iter<'x, u32>,
iter_stack: Vec<Iter<'x, u32>>,
anychild: bool,
}
impl<'x> SubpartIterator<'x> {
pub(crate) fn new(ctx: &'x Context<'x>, parts: &'x [u32], anychild: bool) -> Self {
SubpartIterator {
ctx,
iter: parts.iter(),
iter_stack: Vec::new(),
anychild,
}
}
#[allow(clippy::should_implement_trait)]
pub fn next(&mut self) -> Option<(u32, &MessagePart<'x>)> {
loop {
if let Some(&part_id) = self.iter.next() {
let subpart = self.ctx.message.parts.get(part_id as usize)?;
match &subpart.body {
PartType::Multipart(subparts) if self.anychild => {
self.iter_stack
.push(std::mem::replace(&mut self.iter, subparts.iter()));
}
_ => (),
}
return Some((part_id, subpart));
}
{
let prev_iter = self.iter_stack.pop()?;
self.iter = prev_iter;
}
}
}
}
impl<'x> Context<'x> {
pub(crate) fn find_nested_parts<'z: 'x>(
&'z self,
mut message: &'x Message<'x>,
ct_filter: &[ContentTypeFilter],
visitor_fnc: &mut impl FnMut(&MessagePart, &[u8]) -> bool,
) -> bool {
let mut iter_stack = Vec::new();
let mut iter = vec![self.part].into_iter();
loop {
while let Some(part_id) = iter.next() {
if let Some(subpart) = message.parts.get(part_id as usize) {
let process_part = if !ct_filter.is_empty() {
let mut process_part = false;
let (ct, cst) = if let Some(ct) = subpart.content_type() {
(ct.c_type.as_ref(), ct.c_subtype.as_deref().unwrap_or(""))
} else {
match &subpart.body {
PartType::Text(_) => ("text", "plain"),
PartType::Html(_) => ("text", "html"),
PartType::Message(_) => ("message", "rfc822"),
PartType::Multipart(_) => ("multipart", "mixed"),
_ => ("application", "octet-stream"),
}
};
for ctf in ct_filter {
match ctf {
ContentTypeFilter::Type(name) => {
if name.eq_ignore_ascii_case(ct) {
process_part = true;
break;
}
}
ContentTypeFilter::TypeSubtype((name, subname)) => {
if name.eq_ignore_ascii_case(ct)
&& subname.eq_ignore_ascii_case(cst)
{
process_part = true;
break;
}
}
}
}
process_part
} else {
true
};
if process_part && visitor_fnc(subpart, message.raw_message.as_ref()) {
return true;
}
match &subpart.body {
PartType::Multipart(subparts) => {
iter_stack.push((
std::mem::replace(&mut iter, subparts.clone().into_iter()),
None,
));
}
PartType::Message(next_message) => {
iter_stack.push((
std::mem::replace(&mut iter, vec![0].into_iter()),
Some(message),
));
message = next_message;
}
_ => (),
}
}
}
if let Some((prev_iter, prev_message)) = iter_stack.pop() {
iter = prev_iter;
if let Some(prev_message) = prev_message {
message = prev_message;
}
} else {
break;
}
}
false
}
pub(crate) fn find_nested_parts_ids(&self, include_current: bool) -> Vec<u32> {
if self.part == 0 {
if include_current {
(0u32..self.message.parts.len() as u32).collect()
} else if self.message.parts.len() > 1 {
(1u32..self.message.parts.len() as u32).collect()
} else {
Vec::new()
}
} else {
let mut part_ids = Vec::new();
let mut iter_stack = Vec::new();
if include_current {
part_ids.push(self.part);
}
if let Some(PartType::Multipart(subparts)) =
self.message.parts.get(self.part as usize).map(|p| &p.body)
{
let mut iter = subparts.iter();
loop {
while let Some(&part_id) = iter.next() {
part_ids.push(part_id);
if let Some(PartType::Multipart(subparts)) =
self.message.parts.get(part_id as usize).map(|p| &p.body)
{
iter_stack.push(std::mem::replace(&mut iter, subparts.iter()));
}
}
if let Some(prev_iter) = iter_stack.pop() {
iter = prev_iter;
} else {
break;
}
}
}
part_ids
}
}
}
impl ContentTypeFilter {
pub(crate) fn parse(ct: &str) -> Option<ContentTypeFilter> {
let mut iter = ct.split('/');
let name = iter.next()?;
if let Some(sub_name) = iter.next() {
if !name.is_empty() && !sub_name.is_empty() && iter.next().is_none() {
Some(ContentTypeFilter::TypeSubtype((
name.to_string(),
sub_name.to_string(),
)))
} else {
None
}
} else if !name.is_empty() {
Some(ContentTypeFilter::Type(name.to_string()))
} else {
None
}
}
}
+121
View File
@@ -0,0 +1,121 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
Context, Event, Mailbox,
compiler::grammar::{Capability, test::Test},
};
use super::RuntimeError;
pub mod comparator;
pub mod glob;
pub mod mime;
pub mod test_address;
pub mod test_body;
pub mod test_date;
pub mod test_duplicate;
pub mod test_envelope;
pub mod test_exists;
pub mod test_extlists;
pub mod test_hasflag;
pub mod test_header;
pub mod test_metadata;
pub mod test_notify;
pub mod test_size;
pub mod test_spamtest;
pub mod test_string;
pub(crate) enum TestResult {
Bool(bool),
Event { event: Event, is_not: bool },
Error(RuntimeError),
}
impl Test {
pub(crate) fn exec(&self, ctx: &mut Context) -> TestResult {
match &self {
Test::Header(test) => test.exec(ctx),
Test::Address(test) => test.exec(ctx),
Test::Envelope(test) => test.exec(ctx),
Test::Exists(test) => test.exec(ctx),
Test::Size(test) => test.exec(ctx),
Test::Body(test) => test.exec(ctx),
Test::String(test) => test.exec(ctx, false),
Test::HasFlag(test) => test.exec(ctx),
Test::Date(test) => test.exec(ctx),
Test::CurrentDate(test) => test.exec(ctx),
Test::Duplicate(test) => test.exec(ctx),
Test::NotifyMethodCapability(test) => test.exec(ctx),
Test::ValidNotifyMethod(test) => test.exec(ctx),
Test::Environment(test) => test.exec(ctx, true),
Test::ValidExtList(test) => test.exec(ctx),
Test::Ihave(test) => TestResult::Bool(
test.capabilities.iter().all(|c| {
![Capability::Variables, Capability::EncodedCharacter].contains(c)
&& ctx.runtime.allowed_capabilities.contains(c)
}) ^ test.is_not,
),
Test::MailboxExists(test) => TestResult::Event {
event: Event::MailboxExists {
mailboxes: test
.mailbox_names
.iter()
.map(|m| Mailbox::Name(ctx.eval_value(m).to_string().into_owned()))
.collect(),
special_use: Vec::new(),
},
is_not: test.is_not,
},
Test::Vacation(test) => test.exec(ctx),
Test::Metadata(test) => test.exec(ctx),
Test::MetadataExists(test) => test.exec(ctx),
Test::MailboxIdExists(test) => TestResult::Event {
event: Event::MailboxExists {
mailboxes: test
.mailbox_ids
.iter()
.map(|m| Mailbox::Id(ctx.eval_value(m).to_string().into_owned()))
.collect(),
special_use: Vec::new(),
},
is_not: test.is_not,
},
Test::SpamTest(test) => test.exec(ctx),
Test::VirusTest(test) => test.exec(ctx),
Test::SpecialUseExists(test) => TestResult::Event {
event: Event::MailboxExists {
mailboxes: if let Some(mailbox) = &test.mailbox {
vec![Mailbox::Name(
ctx.eval_value(mailbox).to_string().into_owned(),
)]
} else {
Vec::new()
},
special_use: ctx.eval_values_owned(&test.attributes),
},
is_not: test.is_not,
},
Test::Convert(test) => test.exec(ctx),
Test::True => TestResult::Bool(true),
Test::False => TestResult::Bool(false),
Test::Invalid(invalid) => {
TestResult::Error(RuntimeError::InvalidInstruction(invalid.clone()))
}
#[cfg(test)]
Test::TestCmd { arguments, is_not } => TestResult::Event {
event: Event::Function {
id: u32::MAX,
arguments: arguments
.iter()
.map(|s| ctx.eval_value(s).to_owned())
.collect(),
},
is_not: *is_not,
},
}
}
}
+296
View File
@@ -0,0 +1,296 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use mail_parser::{
Addr, Address, Header, HeaderValue,
parsers::{
MessageStream,
fields::address::{
parse_address_detail_part, parse_address_domain, parse_address_local_part,
parse_address_user_part,
},
},
};
use crate::{
Context, Event,
compiler::{
Number,
grammar::{AddressPart, MatchType, tests::test_address::TestAddress},
},
};
use super::TestResult;
impl TestAddress {
pub(crate) fn exec(&self, ctx: &mut Context) -> TestResult {
let key_list = ctx.eval_values(&self.key_list);
let header_list = ctx.parse_header_names(&self.header_list);
let result = match &self.match_type {
MatchType::Is | MatchType::Contains => {
let is_is = matches!(&self.match_type, MatchType::Is);
ctx.find_headers(
&header_list,
self.index,
self.mime_anychild,
|header, _, _| {
ctx.find_addresses(header, &self.address_part, |value| {
for key in &key_list {
if is_is {
if self.comparator.is(&value, key) {
return true;
}
} else if self.comparator.contains(value, key.to_string().as_ref())
{
return true;
}
}
false
})
},
)
}
MatchType::Value(rel_match) => ctx.find_headers(
&header_list,
self.index,
self.mime_anychild,
|header, _, _| {
ctx.find_addresses(header, &self.address_part, |value| {
for key in &key_list {
if self.comparator.relational(rel_match, &value, key) {
return true;
}
}
false
})
},
),
MatchType::Matches(capture_positions) | MatchType::Regex(capture_positions) => {
let mut captured_positions = Vec::new();
let is_matches = matches!(&self.match_type, MatchType::Matches(_));
let result = ctx.find_headers(
&header_list,
self.index,
self.mime_anychild,
|header, _, _| {
ctx.find_addresses(header, &self.address_part, |value| {
for (pattern_expr, pattern) in key_list.iter().zip(self.key_list.iter())
{
if is_matches {
if self.comparator.matches(
value,
pattern_expr.to_string().as_ref(),
*capture_positions,
&mut captured_positions,
) {
return true;
}
} else if self.comparator.regex(
pattern,
pattern_expr,
value,
*capture_positions,
&mut captured_positions,
) {
return true;
}
}
false
})
},
);
if !captured_positions.is_empty() {
ctx.set_match_variables(captured_positions);
}
result
}
MatchType::Count(rel_match) => {
let mut count: i64 = 0;
ctx.find_headers(
&header_list,
self.index,
self.mime_anychild,
|header, _, _| {
ctx.find_addresses(header, &self.address_part, |value| {
if !value.is_empty() {
count += 1;
}
false
})
},
);
let mut result = false;
for key in &key_list {
if rel_match.cmp(&Number::from(count), &key.to_number()) {
result = true;
break;
}
}
result
}
MatchType::List => {
let mut values: Vec<String> = Vec::new();
ctx.find_headers(
&header_list,
self.index,
self.mime_anychild,
|header, _, _| {
ctx.find_addresses(header, &self.address_part, |value| {
if !value.is_empty() && !values.iter().any(|v| v.eq(value)) {
values.push(value.to_string());
}
false
})
},
);
if !values.is_empty() {
return TestResult::Event {
event: Event::ListContains {
lists: ctx.eval_values_owned(&self.key_list),
values,
match_as: self.comparator.as_match(),
},
is_not: self.is_not,
};
}
false
}
};
TestResult::Bool(result ^ self.is_not)
}
}
impl Context<'_> {
#[allow(unused_assignments)]
pub(crate) fn find_addresses(
&self,
header: &Header,
part: &AddressPart,
mut visitor_fnc: impl FnMut(&str) -> bool,
) -> bool {
match &header.value {
HeaderValue::Address(Address::List(addr_list)) => {
for addr in addr_list {
if let Some(addr) = part.eval(addr)
&& visitor_fnc(addr)
{
return true;
}
}
false
}
HeaderValue::Address(Address::Group(group_list)) => {
for group in group_list {
for addr in &group.addresses {
if let Some(addr) = part.eval(addr)
&& visitor_fnc(addr)
{
return true;
}
}
}
false
}
_ => {
let mut raw_header = None;
let bytes = if header.offset_end > 0 {
self.message
.raw_message
.get(header.offset_start as usize..header.offset_end as usize)
.unwrap_or(b"")
} else if let HeaderValue::Text(text) = &header.value {
// Inserted header
raw_header = format!("{text}\n").into_bytes().into();
raw_header.as_deref().unwrap()
} else {
b""
};
match MessageStream::new(bytes).parse_address() {
HeaderValue::Address(Address::List(addr_list)) => {
for addr in &addr_list {
if let Some(addr) = part.eval(addr)
&& visitor_fnc(addr)
{
return true;
}
}
false
}
HeaderValue::Address(Address::Group(group_list)) => {
for group in group_list {
for addr in &group.addresses {
if let Some(addr) = part.eval(addr)
&& visitor_fnc(addr)
{
return true;
}
}
}
false
}
_ => visitor_fnc(""),
}
}
}
}
}
impl AddressPart {
pub(crate) fn eval<'x>(&self, addr: &'x Addr<'x>) -> Option<&'x str> {
let email = addr.address.as_deref().or(addr.name.as_deref());
match (self, email) {
(AddressPart::All, _) => email,
(AddressPart::LocalPart, Some(email)) if !email.is_empty() => {
parse_address_local_part(email)
}
(AddressPart::Domain, Some(email)) if !email.is_empty() => parse_address_domain(email),
(AddressPart::User, Some(email)) if !email.is_empty() => parse_address_user_part(email),
(AddressPart::Detail, Some(email)) if !email.is_empty() => {
parse_address_detail_part(email)
}
(AddressPart::Name, _) => addr.name.as_deref(),
_ => email,
}
}
pub(crate) fn eval_strict<'x>(&self, addr: &'x Addr<'x>) -> Option<&'x str> {
match (self, addr.address.as_deref()) {
(AddressPart::All, Some(email)) => Some(email),
(AddressPart::LocalPart, Some(email)) if !email.is_empty() => {
parse_address_local_part(email)
}
(AddressPart::Domain, Some(email)) if !email.is_empty() => parse_address_domain(email),
(AddressPart::User, Some(email)) if !email.is_empty() => parse_address_user_part(email),
(AddressPart::Detail, Some(email)) if !email.is_empty() => {
parse_address_detail_part(email)
}
(AddressPart::Name, _) => addr.name.as_deref(),
(_, email) => email,
}
}
pub(crate) fn eval_string<'x>(&self, addr: &'x str) -> Option<&'x str> {
if !addr.is_empty() {
match self {
AddressPart::All => addr.into(),
AddressPart::LocalPart => parse_address_local_part(addr),
AddressPart::Domain => parse_address_domain(addr),
AddressPart::User => parse_address_user_part(addr),
AddressPart::Detail => parse_address_detail_part(addr),
_ => addr.into(),
}
} else {
addr.into()
}
}
}
+224
View File
@@ -0,0 +1,224 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use mail_parser::{MimeHeaders, PartType, decoders::html::html_to_text};
use crate::{
Context,
compiler::{
Number,
grammar::{
MatchType,
tests::test_body::{BodyTransform, TestBody},
},
},
};
use super::{TestResult, mime::ContentTypeFilter};
impl TestBody {
pub(crate) fn exec(&self, ctx: &mut Context) -> TestResult {
// Check Subject (not a Sieve standard)
let key_list = ctx.eval_values(&self.key_list);
if self.include_subject {
let subject = if !matches!(&self.body_transform, BodyTransform::Raw) {
ctx.message.subject().unwrap_or_default()
} else {
ctx.message.header_raw("Subject").unwrap_or_default()
};
for (key, pattern) in key_list.iter().zip(self.key_list.iter()) {
let result = match &self.match_type {
MatchType::Is => self.comparator.is(&subject, key),
MatchType::Contains => {
self.comparator.contains(subject, key.to_string().as_ref())
}
MatchType::Value(rel_match) => {
self.comparator.relational(rel_match, &subject, key)
}
MatchType::Matches(_) => self.comparator.matches(
subject,
key.to_string().as_ref(),
0,
&mut Vec::new(),
),
MatchType::Regex(_) => {
self.comparator
.regex(pattern, key, subject, 0, &mut Vec::new())
}
_ => break,
};
if result {
return TestResult::Bool(result ^ self.is_not);
}
}
}
let ct_filter = match &self.body_transform {
BodyTransform::Text | BodyTransform::Raw => Vec::new(),
BodyTransform::Content(values) => {
let mut ct_filter = Vec::with_capacity(values.len());
for ct in values {
let ct = ctx.eval_value(ct);
if ct.is_empty() {
break;
} else if let Some(ctf) = ContentTypeFilter::parse(ct.to_string().as_ref()) {
ct_filter.push(ctf);
} else {
return TestResult::Bool(false ^ self.is_not);
}
}
ct_filter
}
};
let result = if let MatchType::Count(rel_match) = &self.match_type {
let mut count = 0;
let mut result = false;
ctx.find_nested_parts(&ctx.message, &ct_filter, &mut |_part, _raw_message| {
count += 1;
false
});
for key in &self.key_list {
if rel_match.cmp(&Number::from(count), &ctx.eval_value(key).to_number()) {
result = true;
break;
}
}
result
} else {
ctx.find_nested_parts(&ctx.message, &ct_filter, &mut |part, raw_message| {
let text = match (&self.body_transform, &part.body) {
(BodyTransform::Content(_), PartType::Message(message)) => {
if let Some(part) = message.parts.first() {
String::from_utf8_lossy(
raw_message
.get(
part.raw_header_offset() as usize
..part.raw_body_offset() as usize,
)
.unwrap_or(b""),
)
} else {
return false;
}
}
(BodyTransform::Content(_), PartType::Multipart(_)) => {
if let Some(boundary) =
part.content_type().and_then(|ct| ct.attribute("boundary"))
{
let mime_body = std::str::from_utf8(
raw_message
.get(
part.raw_body_offset() as usize
..part.raw_end_offset() as usize,
)
.unwrap_or(b""),
)
.unwrap_or("");
let mut mime_part = String::with_capacity(64);
if let Some((prologue, epilogue)) =
mime_body.split_once(&format!("\n--{boundary}"))
{
mime_part.push_str(prologue);
if let Some((_, epilogue)) =
epilogue.rsplit_once(&format!("\n--{boundary}--"))
{
mime_part.push_str(epilogue);
}
}
mime_part.into()
} else {
String::from_utf8_lossy(
raw_message
.get(
part.raw_body_offset() as usize
..part.raw_end_offset() as usize,
)
.unwrap_or(b""),
)
}
}
(BodyTransform::Raw, _) => {
match &part.body {
PartType::Text(text) if part.raw_body_offset() == 0 => {
// Inserted part
text.as_ref().into()
}
_ if part.raw_end_offset() > part.raw_body_offset() => {
String::from_utf8_lossy(
raw_message
.get(
part.raw_body_offset() as usize
..part.raw_end_offset() as usize,
)
.unwrap_or(b""),
)
}
_ => return false,
}
}
(_, PartType::Text(text))
| (BodyTransform::Content(_), PartType::Html(text)) => text.as_ref().into(),
(_, PartType::Html(html)) => html_to_text(html.as_ref()).into(),
(
BodyTransform::Text,
PartType::Binary(bytes) | PartType::InlineBinary(bytes),
) if part.content_type().is_some_and(|ct| {
ct.c_type.eq_ignore_ascii_case("application")
&& ct.c_subtype.as_ref().is_some_and(|st| st.contains("xml"))
}) =>
{
html_to_text(std::str::from_utf8(bytes.as_ref()).unwrap_or("")).into()
}
(
BodyTransform::Content(_),
PartType::Binary(bytes) | PartType::InlineBinary(bytes),
) => String::from_utf8_lossy(bytes.as_ref()),
_ => {
return false;
}
};
let mut result = false;
for (key, pattern) in key_list.iter().zip(self.key_list.iter()) {
result = match &self.match_type {
MatchType::Is => self.comparator.is(&text.as_ref(), key),
MatchType::Contains => self
.comparator
.contains(text.as_ref(), key.to_string().as_ref()),
MatchType::Value(rel_match) => {
self.comparator.relational(rel_match, &text.as_ref(), key)
}
MatchType::Matches(_) => self.comparator.matches(
text.as_ref(),
key.to_string().as_ref(),
0,
&mut Vec::new(),
),
MatchType::Regex(_) => {
self.comparator
.regex(pattern, key, text.as_ref(), 0, &mut Vec::new())
}
_ => false,
};
if result {
break;
}
}
result
})
};
TestResult::Bool(result ^ self.is_not)
}
}
+293
View File
@@ -0,0 +1,293 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::borrow::Cow;
use mail_parser::{DateTime, Header, HeaderValue, parsers::MessageStream};
use crate::{
Context, Event,
compiler::{
Number,
grammar::{
MatchType,
tests::test_date::{DatePart, TestCurrentDate, TestDate, Zone},
},
},
};
use super::TestResult;
impl TestDate {
pub(crate) fn exec(&self, ctx: &mut Context) -> TestResult {
let header_name = if let Some(header_name) = ctx.parse_header_name(&self.header_name) {
header_name
} else {
return TestResult::Bool(false ^ self.is_not);
};
let result = match &self.match_type {
MatchType::Count(rel_match) => {
let mut date_count = 0;
ctx.find_headers(
&[header_name],
self.index,
self.mime_anychild,
|header, _, _| {
if ctx.find_dates(header).is_some() {
date_count += 1;
}
false
},
);
let mut result = false;
for key in &self.key_list {
if rel_match.cmp(&Number::from(date_count), &ctx.eval_value(key).to_number()) {
result = true;
break;
}
}
result
}
MatchType::List => {
let mut values = Vec::new();
ctx.find_headers(
&[header_name],
self.index,
self.mime_anychild,
|header, _, _| {
if let Some(dt) = ctx.find_dates(header) {
let value = self.date_part.eval(self.zone.eval(dt.as_ref()).as_ref());
if !value.is_empty() && !values.iter().any(|v: &String| v.eq(&value)) {
values.push(value);
}
}
false
},
);
if !values.is_empty() {
return TestResult::Event {
event: Event::ListContains {
lists: ctx.eval_values_owned(&self.key_list),
values,
match_as: self.comparator.as_match(),
},
is_not: self.is_not,
};
}
false
}
_ => {
let key_list = ctx.eval_values(&self.key_list);
let mut captured_values = Vec::new();
let result = ctx.find_headers(
&[header_name],
self.index,
self.mime_anychild,
|header, _, _| {
if let Some(dt) = ctx.find_dates(header) {
let date_part =
self.date_part.eval(self.zone.eval(dt.as_ref()).as_ref());
for key in &key_list {
if match &self.match_type {
MatchType::Is => self.comparator.is(&date_part.as_str(), key),
MatchType::Contains => self
.comparator
.contains(&date_part, key.to_string().as_ref()),
MatchType::Value(rel_match) => self.comparator.relational(
rel_match,
&date_part.as_str(),
key,
),
MatchType::Matches(capture_positions) => {
self.comparator.matches(
&date_part,
key.to_string().as_ref(),
*capture_positions,
&mut captured_values,
)
}
MatchType::Regex(capture_positions) => self.comparator.matches(
&date_part,
key.to_string().as_ref(),
*capture_positions,
&mut captured_values,
),
MatchType::Count(_) | MatchType::List => false,
} {
return true;
}
}
}
false
},
);
if !captured_values.is_empty() {
ctx.set_match_variables(captured_values);
}
result
}
};
TestResult::Bool(result ^ self.is_not)
}
}
impl TestCurrentDate {
pub(crate) fn exec(&self, ctx: &mut Context) -> TestResult {
let mut result = false;
match &self.match_type {
MatchType::Count(rel_match) => {
for key in &self.key_list {
if rel_match.cmp(&Number::from(1.0), &ctx.eval_value(key).to_number()) {
result = true;
break;
}
}
}
MatchType::List => {
let value = self.date_part.eval(
&(if let Some(zone) = self.zone {
DateTime::from_timestamp(ctx.current_time).to_timezone(zone)
} else {
DateTime::from_timestamp(ctx.current_time)
}),
);
if !value.is_empty() {
return TestResult::Event {
event: Event::ListContains {
lists: ctx.eval_values_owned(&self.key_list),
values: vec![value],
match_as: self.comparator.as_match(),
},
is_not: self.is_not,
};
}
}
_ => {
let mut captured_values = Vec::new();
let date_part = self.date_part.eval(
&(if let Some(zone) = self.zone {
DateTime::from_timestamp(ctx.current_time).to_timezone(zone)
} else {
DateTime::from_timestamp(ctx.current_time)
}),
);
for key in &self.key_list {
let key = ctx.eval_value(key);
if match &self.match_type {
MatchType::Is => self.comparator.is(&date_part.as_str(), &key),
MatchType::Contains => self
.comparator
.contains(&date_part, key.to_string().as_ref()),
MatchType::Value(rel_match) => {
self.comparator
.relational(rel_match, &date_part.as_str(), &key)
}
MatchType::Matches(capture_positions) => self.comparator.matches(
&date_part,
key.to_string().as_ref(),
*capture_positions,
&mut captured_values,
),
MatchType::Regex(capture_positions) => self.comparator.matches(
&date_part,
key.to_string().as_ref(),
*capture_positions,
&mut captured_values,
),
MatchType::Count(_) | MatchType::List => false,
} {
result = true;
break;
}
}
if !captured_values.is_empty() {
ctx.set_match_variables(captured_values);
}
}
}
TestResult::Bool(result ^ self.is_not)
}
}
impl<'x> Context<'x> {
#[allow(unused_assignments)]
pub(crate) fn find_dates(&self, header: &'x Header) -> Option<Cow<'x, DateTime>> {
if let HeaderValue::DateTime(dt) = &header.value {
if dt.is_valid() {
return Some(Cow::Borrowed(dt));
}
} else if header.offset_end > 0 {
let bytes = self
.message
.raw_message
.get(header.offset_start as usize..header.offset_end as usize)?;
if let HeaderValue::DateTime(dt) = MessageStream::new(bytes).parse_date()
&& dt.is_valid()
{
return Some(Cow::Owned(dt));
}
} else if let HeaderValue::Text(text) = &header.value {
// Inserted header
let bytes = format!("{text}\n").into_bytes();
if let HeaderValue::DateTime(dt) = MessageStream::new(&bytes).parse_date()
&& dt.is_valid()
{
return Some(Cow::Owned(dt));
}
}
None
}
}
impl DatePart {
fn eval(&self, dt: &DateTime) -> String {
match self {
DatePart::Year => format!("{:04}", dt.year),
DatePart::Month => format!("{:02}", dt.month),
DatePart::Day => format!("{:02}", dt.day),
DatePart::Date => format!("{:04}-{:02}-{:02}", dt.year, dt.month, dt.day,),
DatePart::Julian => ((dt.julian_day() as f64 - 2400000.5) as i64).to_string(),
DatePart::Hour => format!("{:02}", dt.hour),
DatePart::Minute => format!("{:02}", dt.minute),
DatePart::Second => format!("{:02}", dt.second),
DatePart::Time => format!("{:02}:{:02}:{:02}", dt.hour, dt.minute, dt.second,),
DatePart::Iso8601 => dt.to_rfc3339(),
DatePart::Std11 => dt.to_rfc822(),
DatePart::Zone => format!(
"{}{:02}{:02}",
if dt.tz_before_gmt && (dt.tz_hour > 0 || dt.tz_minute > 0) {
"-"
} else {
"+"
},
dt.tz_hour,
dt.tz_minute
),
DatePart::Weekday => dt.day_of_week().to_string(),
}
}
}
impl Zone {
pub(crate) fn eval<'x>(&self, dt: &'x DateTime) -> Cow<'x, DateTime> {
match self {
Zone::Time(tz) => Cow::Owned(dt.to_timezone(*tz)),
Zone::Original => Cow::Borrowed(dt),
Zone::Local => Cow::Owned(DateTime::from_timestamp(dt.to_timestamp())),
}
}
}
+70
View File
@@ -0,0 +1,70 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::borrow::Cow;
use mail_parser::{HeaderValue, parsers::MessageStream};
use crate::{
Context, Event,
compiler::grammar::tests::test_duplicate::{DupMatch, TestDuplicate},
};
use super::TestResult;
impl TestDuplicate {
pub(crate) fn exec(&self, ctx: &mut Context) -> TestResult {
let id: Cow<str> = match &self.dup_match {
DupMatch::Header(header_name) => {
let mut value = String::new();
if let Some(header_name) = ctx.parse_header_name(header_name) {
ctx.find_headers(&[header_name], None, true, |header, _, _| {
if header.offset_end > 0 {
if let Some(bytes) = ctx
.message
.raw_message
.get(header.offset_start as usize..header.offset_end as usize)
&& let HeaderValue::Text(id) = MessageStream::new(bytes).parse_id()
&& !id.is_empty()
{
value = id.to_string();
return true;
}
} else if let HeaderValue::Text(text) = &header.value {
// Inserted header
let bytes = format!("{text}\n").into_bytes();
if let HeaderValue::Text(id) = MessageStream::new(&bytes).parse_id()
&& !id.is_empty()
{
value = id.to_string();
return true;
}
}
false
});
}
value.into()
}
DupMatch::UniqueId(s) => ctx.eval_value(s).to_string().into_owned().into(),
DupMatch::Default => ctx.message.message_id().unwrap_or("").into(),
};
TestResult::Event {
event: Event::DuplicateId {
id: if id.is_empty() {
return TestResult::Bool(false ^ self.is_not);
} else if let Some(handle) = &self.handle {
format!("{}{}", ctx.eval_value(handle).to_string(), id)
} else {
id.into_owned()
},
expiry: self.seconds.unwrap_or(ctx.runtime.default_duplicate_expiry),
last: self.last,
},
is_not: self.is_not,
}
}
}
+258
View File
@@ -0,0 +1,258 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use mail_parser::DateTime;
use crate::{
Context, Envelope, Event,
compiler::{
Number,
grammar::{MatchType, tests::test_envelope::TestEnvelope},
},
};
use super::TestResult;
impl TestEnvelope {
pub(crate) fn exec(&self, ctx: &mut Context) -> TestResult {
let key_list = ctx.eval_values(&self.key_list);
let result = match &self.match_type {
MatchType::Is | MatchType::Contains => {
let is_is = matches!(&self.match_type, MatchType::Is);
ctx.find_envelopes(self, |value| {
for key in &key_list {
if is_is {
if self.comparator.is(&value, key) {
return true;
}
} else if self.comparator.contains(value, key.to_string().as_ref()) {
return true;
}
}
false
})
}
MatchType::Value(rel_match) => ctx.find_envelopes(self, |value| {
for key in &key_list {
if self.comparator.relational(rel_match, &value, key) {
return true;
}
}
false
}),
MatchType::Matches(capture_positions) | MatchType::Regex(capture_positions) => {
let mut captured_positions = Vec::new();
let is_matches = matches!(&self.match_type, MatchType::Matches(_));
let result = ctx.find_envelopes(self, |value| {
for (pattern_expr, pattern) in key_list.iter().zip(self.key_list.iter()) {
if is_matches {
if self.comparator.matches(
value,
pattern_expr.to_string().as_ref(),
*capture_positions,
&mut captured_positions,
) {
return true;
}
} else if self.comparator.regex(
pattern,
pattern_expr,
value,
*capture_positions,
&mut captured_positions,
) {
return true;
}
}
false
});
if !captured_positions.is_empty() {
ctx.set_match_variables(captured_positions);
}
result
}
MatchType::Count(rel_match) => {
let mut count = 0;
ctx.find_envelopes(self, |value| {
if !value.is_empty() {
count += 1;
}
false
});
let mut result = false;
for key in &key_list {
if rel_match.cmp(&Number::from(count), &key.to_number()) {
result = true;
break;
}
}
result
}
MatchType::List => {
let mut values: Vec<String> = Vec::new();
ctx.find_envelopes(self, |value| {
if !value.is_empty() && !values.iter().any(|v| v.eq(value)) {
values.push(value.to_string());
}
false
});
if !values.is_empty() {
return TestResult::Event {
event: Event::ListContains {
lists: ctx.eval_values_owned(&self.key_list),
values,
match_as: self.comparator.as_match(),
},
is_not: self.is_not,
};
}
false
}
};
TestResult::Bool(result ^ self.is_not)
}
}
impl Context<'_> {
fn find_envelopes(
&self,
test_envelope: &TestEnvelope,
mut cb: impl FnMut(&str) -> bool,
) -> bool {
for (name, value) in &self.envelope {
if test_envelope.envelope_list.contains(name)
&& match name {
Envelope::From | Envelope::To | Envelope::Orcpt => {
if let Some(value) = test_envelope
.address_part
.eval_string(value.to_string().as_ref())
{
cb(value)
} else {
false
}
}
Envelope::ByTimeAbsolute if test_envelope.zone.is_some() => {
if let Some(dt) = DateTime::parse_rfc3339(value.to_string().as_ref()) {
cb(&dt.to_timezone(test_envelope.zone.unwrap()).to_rfc3339())
} else {
cb("")
}
}
_ => cb(value.to_string().as_ref()),
}
{
return true;
}
}
false
}
}
pub fn parse_envelope_address(addr: &str) -> Option<&str> {
let addr = addr.as_bytes();
let mut addr_start_pos = 0;
let mut addr_end_pos = addr.len();
let mut last_ch = 0;
let mut at_pos = 0;
let mut has_bracket = false;
let mut in_path = false;
if addr.is_empty() {
return "".into();
}
for (pos, &ch) in addr.iter().enumerate() {
match ch {
b'<' => {
if pos == 0 {
addr_start_pos = pos + 1;
has_bracket = true;
} else {
return None;
}
}
b'>' => {
if has_bracket && pos == addr.len() - 1 {
if addr.len() > 2 {
has_bracket = false;
addr_end_pos = pos;
} else {
// <>
return "".into();
}
} else {
return None;
}
}
b':' => {
if at_pos != 0 {
at_pos = 0;
addr_start_pos = pos + 1;
in_path = false;
} else {
return None;
}
}
b',' => {
if at_pos != 0 {
at_pos = 0;
in_path = true;
} else {
return None;
}
}
b'@' => {
if at_pos == 0 && pos != addr.len() - 1 {
at_pos = pos;
} else {
return None;
}
}
b'.' => {
if (at_pos != 0 && last_ch == b'.') || last_ch == b'@' {
return None;
}
}
_ => {
if ch.is_ascii_whitespace() || !ch.is_ascii() {
return None;
}
}
}
last_ch = ch;
}
if !has_bracket && !in_path && at_pos > addr_start_pos && addr_end_pos - 1 > at_pos {
std::str::from_utf8(&addr[addr_start_pos..addr_end_pos])
.unwrap()
.into()
} else {
match addr.get(addr_start_pos..addr_end_pos) {
Some(addr) if at_pos == 0 && addr.eq_ignore_ascii_case(b"mailer-daemon") => {
std::str::from_utf8(addr).unwrap().into()
}
_ => None,
}
}
}
+36
View File
@@ -0,0 +1,36 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{Context, compiler::grammar::tests::test_exists::TestExists};
use super::{TestResult, mime::SubpartIterator};
impl TestExists {
pub(crate) fn exec(&self, ctx: &mut Context) -> TestResult {
let header_names = ctx.parse_header_names(&self.header_names);
let mut header_exists = vec![false; header_names.len()];
let parts = [ctx.part];
let mut part_iter = SubpartIterator::new(ctx, &parts, self.mime_anychild);
let mut result = false;
while let Some((_, message_part)) = part_iter.next() {
for (pos, header_name) in header_names.iter().enumerate() {
if !header_exists[pos]
&& message_part.headers.iter().any(|h| &h.name == header_name)
{
header_exists[pos] = true;
}
}
if header_exists.iter().all(|v| *v) {
result = true;
break;
}
}
TestResult::Bool(result ^ self.is_not)
}
}
+27
View File
@@ -0,0 +1,27 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{Context, compiler::grammar::tests::test_extlists::TestValidExtList};
use super::TestResult;
impl TestValidExtList {
pub(crate) fn exec(&self, ctx: &mut Context) -> TestResult {
let mut num_valid = 0;
for list in &self.list_names {
if ctx
.runtime
.valid_ext_lists
.contains(&ctx.eval_value(list).to_string())
{
num_valid += 1;
}
}
TestResult::Bool((num_valid == self.list_names.len()) ^ self.is_not)
}
}
+96
View File
@@ -0,0 +1,96 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
Context,
compiler::{
Number, VariableType,
grammar::{MatchType, tests::test_hasflag::TestHasFlag},
},
};
use super::TestResult;
impl TestHasFlag {
pub(crate) fn exec(&self, ctx: &mut Context) -> TestResult {
let mut variable_list_ = None;
let variable_list = if !self.variable_list.is_empty() {
&self.variable_list
} else {
variable_list_.get_or_insert_with(|| vec![VariableType::Global("__flags".to_string())])
};
let result = if let MatchType::Count(rel_match) = &self.match_type {
let mut flag_count = 0;
for variable in variable_list {
match ctx.get_variable(variable) {
Some(flags) if !flags.is_empty() => {
flag_count += flags.to_string().split(' ').count();
}
_ => (),
}
}
let mut result = false;
for key in &self.flags {
if rel_match.cmp(
&Number::from(flag_count as i64),
&ctx.eval_value(key).to_number(),
) {
result = true;
break;
}
}
result
} else {
let mut captured_values = Vec::new();
let result = ctx.tokenize_flags(&self.flags, |check_flag| {
for variable in variable_list {
match ctx.get_variable(variable) {
Some(flags) if !flags.is_empty() => {
for flag in flags.to_string().split(' ') {
if match &self.match_type {
MatchType::Is => self.comparator.is(&flag, &check_flag),
MatchType::Contains => {
self.comparator.contains(flag, check_flag)
}
MatchType::Value(rel_match) => {
self.comparator.relational(rel_match, &flag, &check_flag)
}
MatchType::Matches(capture_positions) => {
self.comparator.matches(
flag,
check_flag,
*capture_positions,
&mut captured_values,
)
}
MatchType::Regex(capture_positions) => self.comparator.matches(
flag,
check_flag,
*capture_positions,
&mut captured_values,
),
MatchType::Count(_) | MatchType::List => false,
} {
return true;
}
}
}
_ => (),
}
}
false
});
if !captured_values.is_empty() {
ctx.set_match_variables(captured_values);
}
result
};
TestResult::Bool(result ^ self.is_not)
}
}
+364
View File
@@ -0,0 +1,364 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use mail_parser::{Header, HeaderName, HeaderValue, parsers::MessageStream};
use crate::{
Context, Event,
compiler::{
Number, Value,
grammar::{MatchType, actions::action_mime::MimeOpts, tests::test_header::TestHeader},
},
runtime::Variable,
};
use super::{TestResult, mime::SubpartIterator};
impl TestHeader {
pub(crate) fn exec(&self, ctx: &mut Context) -> TestResult {
let key_list = ctx.eval_values(&self.key_list);
let header_list = ctx.parse_header_names(&self.header_list);
let mime_opts = match &self.mime_opts {
MimeOpts::Type => MimeOpts::Type,
MimeOpts::Subtype => MimeOpts::Subtype,
MimeOpts::ContentType => MimeOpts::ContentType,
MimeOpts::Param(params) => MimeOpts::Param(ctx.eval_values(params)),
MimeOpts::None => MimeOpts::None,
};
let result = match &self.match_type {
MatchType::Is | MatchType::Contains => {
let is_is = matches!(&self.match_type, MatchType::Is);
ctx.find_headers(
&header_list,
self.index,
self.mime_anychild,
|header, _, _| {
ctx.find_header_values(header, &mime_opts, |value| {
for key in &key_list {
if is_is {
if self.comparator.is(&value, key) {
return true;
}
} else if self.comparator.contains(value, key.to_string().as_ref())
{
return true;
}
}
false
})
},
)
}
MatchType::Value(rel_match) => ctx.find_headers(
&header_list,
self.index,
self.mime_anychild,
|header, _, _| {
ctx.find_header_values(header, &mime_opts, |value| {
for key in &key_list {
if self.comparator.relational(rel_match, &value, key) {
return true;
}
}
false
})
},
),
MatchType::Matches(capture_positions) | MatchType::Regex(capture_positions) => {
let mut captured_values = Vec::new();
let is_matches = matches!(&self.match_type, MatchType::Matches(_));
let result = ctx.find_headers(
&header_list,
self.index,
self.mime_anychild,
|header, _, _| {
ctx.find_header_values(header, &mime_opts, |value| {
for (pattern_expr, pattern) in key_list.iter().zip(self.key_list.iter())
{
if is_matches {
if self.comparator.matches(
value,
pattern_expr.to_string().as_ref(),
*capture_positions,
&mut captured_values,
) {
return true;
}
} else if self.comparator.regex(
pattern,
pattern_expr,
value,
*capture_positions,
&mut captured_values,
) {
return true;
}
}
false
})
},
);
if !captured_values.is_empty() {
ctx.set_match_variables(captured_values);
}
result
}
MatchType::Count(rel_match) => {
let mut count = 0;
ctx.find_headers(
&header_list,
self.index,
self.mime_anychild,
|header, _, _| {
match &mime_opts {
MimeOpts::None => {
count += 1;
}
MimeOpts::Type | MimeOpts::Subtype | MimeOpts::ContentType => {
if let HeaderValue::ContentType(_) = &header.value {
count += 1;
}
}
MimeOpts::Param(params) => {
if let HeaderValue::ContentType(ct) = &header.value
&& let Some(attributes) = &ct.attributes
{
for attr in attributes {
if params
.iter()
.any(|p| p.to_string().eq_ignore_ascii_case(&attr.name))
{
count += 1;
}
}
}
}
}
false
},
);
let mut result = false;
for key in &key_list {
if rel_match.cmp(&Number::from(count), &key.to_number()) {
result = true;
break;
}
}
result
}
MatchType::List => {
let mut values: Vec<String> = Vec::new();
ctx.find_headers(
&header_list,
self.index,
self.mime_anychild,
|header, _, _| {
ctx.find_header_values(header, &mime_opts, |value| {
if !value.is_empty() && !values.iter().any(|v| v.eq(value)) {
values.push(value.to_string());
}
false
})
},
);
if !values.is_empty() {
return TestResult::Event {
event: Event::ListContains {
lists: ctx.eval_values_owned(&self.key_list),
values,
match_as: self.comparator.as_match(),
},
is_not: self.is_not,
};
}
false
}
};
TestResult::Bool(result ^ self.is_not)
}
}
impl Context<'_> {
pub(crate) fn parse_header_names<'z: 'y, 'y>(
&'z self,
header_names: &'y [Value],
) -> Vec<HeaderName<'y>> {
let mut result = Vec::with_capacity(header_names.len());
for header_name in header_names {
if let Some(header_name) = self.parse_header_name(header_name) {
result.push(header_name);
}
}
result
}
#[inline(always)]
pub(crate) fn parse_header_name(&self, header_name: &Value) -> Option<HeaderName<'static>> {
let h_ = self.eval_value(header_name);
let h = h_.to_string();
match HeaderName::parse(h.as_ref())? {
HeaderName::Other(_) => HeaderName::Other(h.into_owned().into()),
hn => hn.into_owned(),
}
.into()
}
pub(crate) fn find_headers(
&self,
header_names: &[HeaderName],
index: Option<i32>,
any_child: bool,
mut visitor_fnc: impl FnMut(&Header, u32, usize) -> bool,
) -> bool {
let parts = [self.part];
let mut part_iter = SubpartIterator::new(self, &parts, any_child);
while let Some((part_id, message_part)) = part_iter.next() {
'outer: for header_name in header_names {
match index {
None => {
for (pos, header) in message_part
.headers
.iter()
.enumerate()
.filter(|(_, h)| &h.name == header_name)
{
if visitor_fnc(header, part_id, pos) {
return true;
}
}
}
Some(index) if index >= 0 => {
let mut header_count = 0;
for (pos, header) in message_part.headers.iter().enumerate() {
if &header.name == header_name {
header_count += 1;
if header_count == index {
if visitor_fnc(header, part_id, pos) {
return true;
}
continue 'outer;
}
}
}
}
Some(index) => {
let index = -index;
let mut header_count = 0;
for (pos, header) in message_part.headers.iter().enumerate().rev() {
if &header.name == header_name {
header_count += 1;
if header_count == index {
if visitor_fnc(header, part_id, pos) {
return true;
}
break;
}
}
}
}
}
}
}
false
}
#[allow(unused_assignments)]
pub(crate) fn find_header_values(
&self,
header: &Header,
mime_opts: &MimeOpts<Variable>,
mut visitor_fnc: impl FnMut(&str) -> bool,
) -> bool {
let mut raw_header = None;
let mut header_value_ = None;
let header_value = if header.offset_end != 0 {
&header.value
} else {
let value = if let HeaderValue::Text(text) = &header.value {
text.as_ref()
} else {
#[cfg(test)]
panic!("Unexpected value.");
#[cfg(not(test))]
return false;
};
if mime_opts == &MimeOpts::None {
return visitor_fnc(value);
} else {
raw_header = format!("{value}\n").into_bytes().into();
header_value_ = MessageStream::new(raw_header.as_ref().unwrap())
.parse_content_type()
.into();
header_value_.as_ref().unwrap()
}
};
match (mime_opts, header_value) {
(MimeOpts::None, HeaderValue::Text(text))
if matches!(
&header.name,
HeaderName::Subject
| HeaderName::Comments
| HeaderName::ContentDescription
| HeaderName::ContentLocation
| HeaderName::ContentTransferEncoding,
) =>
{
visitor_fnc(text.as_ref())
}
(MimeOpts::None, _) => {
if let HeaderValue::Text(text) = MessageStream::new(
self.message
.raw_message
.get(header.offset_start as usize..header.offset_end as usize)
.unwrap_or(b""),
)
.parse_unstructured()
{
visitor_fnc(text.as_ref())
} else {
visitor_fnc("")
}
}
(MimeOpts::Type, HeaderValue::ContentType(ct)) => visitor_fnc(ct.c_type.as_ref()),
(MimeOpts::Subtype, HeaderValue::ContentType(ct)) => {
visitor_fnc(ct.c_subtype.as_deref().unwrap_or(""))
}
(MimeOpts::ContentType, HeaderValue::ContentType(ct)) => {
if let Some(sub_type) = &ct.c_subtype {
visitor_fnc(&format!("{}/{}", ct.c_type, sub_type))
} else {
visitor_fnc(ct.c_type.as_ref())
}
}
(MimeOpts::Param(params), HeaderValue::ContentType(ct)) => {
if let Some(attributes) = &ct.attributes {
for param in params {
for attr in attributes {
if param.to_string().eq_ignore_ascii_case(&attr.name)
&& visitor_fnc(attr.value.as_ref())
{
return true;
}
}
}
}
visitor_fnc("")
}
_ => visitor_fnc(""),
}
}
}
+132
View File
@@ -0,0 +1,132 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
Context, Metadata,
compiler::{
Number,
grammar::{
MatchType,
tests::test_mailbox::{TestMetadata, TestMetadataExists},
},
},
};
use super::TestResult;
impl TestMetadata {
pub(crate) fn exec(&self, ctx: &mut Context) -> TestResult {
let metadata = match &self.medatata {
Metadata::Server { annotation } => Metadata::Server {
annotation: ctx.eval_value(annotation).to_string().into_owned(),
},
Metadata::Mailbox { name, annotation } => Metadata::Mailbox {
name: ctx.eval_value(name).to_string().into_owned(),
annotation: ctx.eval_value(annotation).to_string().into_owned(),
},
};
let value = if let Some((_, value)) = [&ctx.metadata, &ctx.runtime.metadata]
.into_iter()
.flatten()
.find(|(m, _)| match (m, &metadata) {
(Metadata::Server { annotation: a }, Metadata::Server { annotation: b }) => {
a.eq_ignore_ascii_case(b)
}
(
Metadata::Mailbox {
name: a,
annotation: c,
},
Metadata::Mailbox {
name: b,
annotation: d,
},
) => a.eq(b) && c.eq_ignore_ascii_case(d),
_ => false,
}) {
value.as_ref()
} else {
return TestResult::Bool(false ^ self.is_not);
};
let mut result = false;
if let MatchType::Count(match_type) = &self.match_type {
for key in &self.key_list {
if match_type.cmp(&Number::Float(1.0), &ctx.eval_value(key).to_number()) {
result = true;
break;
}
}
} else {
let mut captured_values = Vec::new();
for pattern in &self.key_list {
let key = ctx.eval_value(pattern);
result = match &self.match_type {
MatchType::Is => self.comparator.is(&value, &key),
MatchType::Contains => {
self.comparator.contains(value, key.to_string().as_ref())
}
MatchType::Value(relation) => {
self.comparator.relational(relation, &value, &key)
}
MatchType::Matches(capture_positions) => self.comparator.matches(
value,
key.to_string().as_ref(),
*capture_positions,
&mut captured_values,
),
MatchType::Regex(capture_positions) => self.comparator.regex(
pattern,
&key,
value,
*capture_positions,
&mut captured_values,
),
_ => false,
};
if result {
break;
}
}
if !captured_values.is_empty() {
ctx.set_match_variables(captured_values);
}
}
TestResult::Bool(result ^ self.is_not)
}
}
impl TestMetadataExists {
pub(crate) fn exec(&self, ctx: &Context) -> TestResult {
let mailbox = self
.mailbox
.as_ref()
.map(|s| ctx.eval_value(s).to_string().into_owned());
let mut annotations = ctx.eval_values(&self.annotation_names);
for (metadata, _) in [&ctx.metadata, &ctx.runtime.metadata].into_iter().flatten() {
match (metadata, mailbox.as_ref()) {
(Metadata::Server { annotation }, None) => {
annotations.retain(|a| !a.to_string().eq_ignore_ascii_case(annotation))
}
(Metadata::Mailbox { name, annotation }, Some(mailbox)) if name.eq(mailbox) => {
annotations.retain(|a| !a.to_string().eq_ignore_ascii_case(annotation));
}
_ => (),
}
if annotations.is_empty() {
return TestResult::Bool(true ^ self.is_not);
}
}
TestResult::Bool(false ^ self.is_not)
}
}
+99
View File
@@ -0,0 +1,99 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::borrow::Cow;
use crate::{
Context,
compiler::{
Number,
grammar::{
MatchType,
tests::test_notify::{TestNotifyMethodCapability, TestValidNotifyMethod},
},
},
runtime::actions::action_notify::validate_uri,
};
use super::TestResult;
impl TestValidNotifyMethod {
pub(crate) fn exec(&self, ctx: &mut Context) -> TestResult {
let mut num_valid = 0;
for uri in &self.notification_uris {
let uri_ = ctx.eval_value(uri);
let uri = uri_.to_string();
if let Some(scheme) = validate_uri(uri.as_ref())
&& (ctx
.runtime
.valid_notification_uris
.contains(&Cow::from(scheme))
|| ctx.runtime.valid_notification_uris.contains(&uri))
{
num_valid += 1;
}
}
TestResult::Bool((num_valid == self.notification_uris.len()) ^ self.is_not)
}
}
impl TestNotifyMethodCapability {
pub(crate) fn exec(&self, ctx: &mut Context) -> TestResult {
let uri_ = ctx.eval_value(&self.notification_uri);
let uri = uri_.to_string();
if !ctx
.eval_value(&self.notification_capability)
.to_string()
.eq_ignore_ascii_case("online")
|| !validate_uri(uri.as_ref()).is_some_and(|scheme| {
ctx.runtime
.valid_notification_uris
.contains(&Cow::from(scheme))
|| ctx.runtime.valid_notification_uris.contains(&uri)
})
{
return TestResult::Bool(false ^ self.is_not);
}
if let MatchType::Count(rel_match) = &self.match_type {
for key in &self.key_list {
if rel_match.cmp(&Number::from(1.0), &ctx.eval_value(key).to_number()) {
return TestResult::Bool(true ^ self.is_not);
}
}
} else {
for pattern in &self.key_list {
let key = ctx.eval_value(pattern);
if match &self.match_type {
MatchType::Is => self.comparator.is(&"maybe", &key),
MatchType::Contains => {
self.comparator.contains("maybe", key.to_string().as_ref())
}
MatchType::Value(relation) => {
self.comparator.relational(relation, &"maybe", &key)
}
MatchType::Matches(_) => self.comparator.matches(
"maybe",
key.to_string().as_ref(),
0,
&mut Vec::new(),
),
MatchType::Regex(_) => {
self.comparator
.regex(pattern, &key, "maybe", 0, &mut Vec::new())
}
_ => false,
} {
return TestResult::Bool(true ^ self.is_not);
}
}
}
TestResult::Bool(false ^ self.is_not)
}
}
+21
View File
@@ -0,0 +1,21 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{Context, compiler::grammar::tests::test_size::TestSize};
use super::TestResult;
impl TestSize {
pub(crate) fn exec(&self, ctx: &Context) -> TestResult {
TestResult::Bool(
(if self.over {
ctx.message_size > self.limit
} else {
ctx.message_size < self.limit
}) ^ self.is_not,
)
}
}
+199
View File
@@ -0,0 +1,199 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
Context, SpamStatus, VirusStatus,
compiler::{
Number,
grammar::{
MatchType,
tests::test_spamtest::{TestSpamTest, TestVirusTest},
},
},
runtime::Variable,
};
use super::TestResult;
impl TestSpamTest {
pub(crate) fn exec(&self, ctx: &mut Context) -> TestResult {
let status = if self.percent {
ctx.spam_status.as_percentage()
} else {
ctx.spam_status.as_number()
};
let value = ctx.eval_value(&self.value);
let mut captured_values = Vec::new();
let result = match &self.match_type {
MatchType::Is => self.comparator.is(&status, &value),
MatchType::Contains => self
.comparator
.contains(status.to_string().as_ref(), value.to_string().as_ref()),
MatchType::Value(rel_match) => self.comparator.relational(rel_match, &status, &value),
MatchType::Matches(capture_positions) => self.comparator.matches(
status.to_string().as_ref(),
value.to_string().as_ref(),
*capture_positions,
&mut captured_values,
),
MatchType::Regex(capture_positions) => self.comparator.regex(
&self.value,
&value,
status.to_string().as_ref(),
*capture_positions,
&mut captured_values,
),
MatchType::Count(rel_match) => rel_match.cmp(
&Number::from(if matches!(&ctx.spam_status, SpamStatus::Unknown) {
0.0
} else {
1.1
}),
&value.to_number(),
),
MatchType::List => false,
};
if !captured_values.is_empty() {
ctx.set_match_variables(captured_values);
}
TestResult::Bool(result ^ self.is_not)
}
}
impl TestVirusTest {
pub(crate) fn exec(&self, ctx: &mut Context) -> TestResult {
let status = ctx.virus_status.as_number();
let value = ctx.eval_value(&self.value);
let mut captured_values = Vec::new();
let result = match &self.match_type {
MatchType::Is => self.comparator.is(&status, &value),
MatchType::Contains => self
.comparator
.contains(status.to_string().as_ref(), value.to_string().as_ref()),
MatchType::Value(rel_match) => self.comparator.relational(rel_match, &status, &value),
MatchType::Matches(capture_positions) => self.comparator.matches(
status.to_string().as_ref(),
value.to_string().as_ref(),
*capture_positions,
&mut captured_values,
),
MatchType::Regex(capture_positions) => self.comparator.regex(
&self.value,
&value,
status.to_string().as_ref(),
*capture_positions,
&mut captured_values,
),
MatchType::Count(rel_match) => rel_match.cmp(
&Number::from(if matches!(&ctx.virus_status, VirusStatus::Unknown) {
0.0
} else {
1.1
}),
&value.to_number(),
),
MatchType::List => false,
};
if !captured_values.is_empty() {
ctx.set_match_variables(captured_values);
}
TestResult::Bool(result ^ self.is_not)
}
}
impl SpamStatus {
pub fn from_number(number: u32) -> Self {
match number {
1 => SpamStatus::Ham,
2..=9 => SpamStatus::MaybeSpam(number as f64 / 10.0),
10 => SpamStatus::Spam,
_ => SpamStatus::Unknown,
}
}
pub(crate) fn as_number(&self) -> Variable {
Variable::Integer(match self {
SpamStatus::Unknown => 0,
SpamStatus::Ham => 1,
SpamStatus::MaybeSpam(pct) => ((pct * 10.0) as i64).clamp(2, 9),
SpamStatus::Spam => 10,
})
}
pub(crate) fn as_percentage(&self) -> Variable {
Variable::Integer(match self {
SpamStatus::Unknown | SpamStatus::Ham => 0,
SpamStatus::MaybeSpam(pct) => ((pct * 100.0).ceil() as i64).clamp(1, 100),
SpamStatus::Spam => 100,
})
}
}
impl VirusStatus {
pub fn from_number(number: u32) -> Self {
match number {
1 => VirusStatus::Clean,
2 => VirusStatus::Replaced,
3 => VirusStatus::Cured,
4 => VirusStatus::MaybeVirus,
5 => VirusStatus::Virus,
_ => VirusStatus::Unknown,
}
}
pub(crate) fn as_number(&self) -> Variable {
Variable::Integer(match self {
VirusStatus::Unknown => 0,
VirusStatus::Clean => 1,
VirusStatus::Replaced => 2,
VirusStatus::Cured => 3,
VirusStatus::MaybeVirus => 4,
VirusStatus::Virus => 5,
})
}
}
impl From<u32> for SpamStatus {
fn from(number: u32) -> Self {
SpamStatus::from_number(number)
}
}
impl From<i32> for SpamStatus {
fn from(number: i32) -> Self {
SpamStatus::from_number(number as u32)
}
}
impl From<usize> for SpamStatus {
fn from(number: usize) -> Self {
SpamStatus::from_number(number as u32)
}
}
impl From<u32> for VirusStatus {
fn from(number: u32) -> Self {
VirusStatus::from_number(number)
}
}
impl From<i32> for VirusStatus {
fn from(number: i32) -> Self {
VirusStatus::from_number(number as u32)
}
}
impl From<usize> for VirusStatus {
fn from(number: usize) -> Self {
VirusStatus::from_number(number as u32)
}
}
+106
View File
@@ -0,0 +1,106 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
Context, Event,
compiler::{
Number,
grammar::{MatchType, tests::test_string::TestString},
},
};
use super::TestResult;
impl TestString {
pub(crate) fn exec(&self, ctx: &mut Context, empty_is_null: bool) -> TestResult {
let mut result = false;
match &self.match_type {
MatchType::Count(match_type) => {
let num_items = self
.source
.iter()
.filter(|x| !ctx.eval_value(x).is_empty())
.count() as i64;
if !empty_is_null || num_items > 0 {
for key in &self.key_list {
if match_type
.cmp(&Number::from(num_items), &ctx.eval_value(key).to_number())
{
result = true;
break;
}
}
}
}
MatchType::List => {
let mut values = Vec::with_capacity(self.source.len());
for source in &self.source {
let value = ctx.eval_value(source).to_string().into_owned();
if !value.is_empty() && !values.iter().any(|v: &String| v.eq(&value)) {
values.push(value);
}
}
if !values.is_empty() {
return TestResult::Event {
event: Event::ListContains {
lists: ctx.eval_values_owned(&self.key_list),
values,
match_as: self.comparator.as_match(),
},
is_not: self.is_not,
};
}
}
_ => {
let mut captured_values = Vec::new();
let sources = ctx.eval_values(&self.source);
for pattern in &self.key_list {
let key = ctx.eval_value(pattern);
for source in &sources {
if !empty_is_null || !source.is_empty() {
result = match &self.match_type {
MatchType::Is => self.comparator.is(source, &key),
MatchType::Contains => self.comparator.contains(
source.to_string().as_ref(),
key.to_string().as_ref(),
),
MatchType::Value(relation) => {
self.comparator.relational(relation, source, &key)
}
MatchType::Matches(capture_positions) => self.comparator.matches(
source.to_string().as_ref(),
key.to_string().as_ref(),
*capture_positions,
&mut captured_values,
),
MatchType::Regex(capture_positions) => self.comparator.regex(
pattern,
&key,
source.to_string().as_ref(),
*capture_positions,
&mut captured_values,
),
_ => false,
};
if result {
break;
}
}
}
}
if !captured_values.is_empty() {
ctx.set_match_variables(captured_values);
}
}
}
TestResult::Bool(result ^ self.is_not)
}
}
+35
View File
@@ -0,0 +1,35 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::Context;
use super::Variable;
impl Context<'_> {
pub(crate) fn set_match_variables(&mut self, set_vars: Vec<(usize, String)>) {
for (var_num, value) in set_vars {
if let Some(var) = self.vars_match.get_mut(var_num) {
*var = value.into();
} else {
debug_assert!(false, "Invalid match variable {var_num}");
}
}
}
pub(crate) fn clear_match_variables(&mut self, mut positions: u64) {
while positions != 0 {
let index = 63 - positions.leading_zeros();
positions ^= 1 << index;
if let Some(match_var) = self.vars_match.get_mut(index as usize) {
if !match_var.is_empty() {
*match_var = Variable::default();
}
} else {
debug_assert!(false, "Failed to clear match variable at index {index}.");
}
}
}
}