Rename the identifiers that carried the upstream name
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:
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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(
|
||||
¤t_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(
|
||||
¤t_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(
|
||||
¤t_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(
|
||||
¤t_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(
|
||||
¤t_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));
|
||||
}
|
||||
@@ -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: ", ¶ms.to), ("Cc: ", ¶ms.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 ¶ms.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) = ¬ify_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) = ¬ify_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);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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(),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+482
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
Vendored
+803
@@ -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
@@ -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(®ex.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(®ex, 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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())),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user