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:
+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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user