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

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

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

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

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

Tested: the server builds without warnings; common's 106 unit tests,
including the vendoring check; a new integration test for the two
start-up migrations; and the webdav, jmap, imap and SMTP Sieve suites.
This commit is contained in:
2026-09-22 19:33:02 -07:00
parent 4799d191a0
commit cc6f1eb298
129 changed files with 20504 additions and 168 deletions
+176
View File
@@ -0,0 +1,176 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::{borrow::Cow, sync::Arc};
use crate::{
MatchAs,
compiler::{
Number, Value,
grammar::{Comparator, RelationalMatch},
},
runtime::Variable,
};
use super::glob::GlobPattern;
pub(crate) trait Comparable {
fn to_str(&'_ self) -> Cow<'_, str>;
fn to_number(&self) -> Number;
}
impl Comparator {
pub(crate) fn is(&self, a: &impl Comparable, b: &impl Comparable) -> bool {
match self {
Comparator::Octet => a.to_str() == b.to_str(),
Comparator::AsciiNumeric => RelationalMatch::Eq.cmp(&a.to_number(), &b.to_number()),
_ => a.to_str().to_lowercase() == b.to_str().to_lowercase(),
}
}
pub(crate) fn contains(&self, haystack: &str, needle: &str) -> bool {
needle.is_empty()
|| match self {
Comparator::Octet => haystack.contains(needle),
_ => haystack.to_lowercase().contains(&needle.to_lowercase()),
}
}
pub(crate) fn relational(
&self,
relation: &RelationalMatch,
a: &impl Comparable,
b: &impl Comparable,
) -> bool {
match self {
Comparator::Octet => relation.cmp(a.to_str().as_ref(), b.to_str().as_ref()),
Comparator::AsciiNumeric => relation.cmp(&a.to_number(), &b.to_number()),
_ => relation.cmp(&a.to_str().to_lowercase(), &b.to_str().to_lowercase()),
}
}
pub(crate) fn matches(
&self,
value: &str,
pattern: &str,
capture_positions: u64,
captured_values: &mut Vec<(usize, String)>,
) -> bool {
let pattern = GlobPattern::compile(pattern, matches!(self, Comparator::AsciiCaseMap));
match self {
Comparator::AsciiCaseMap if capture_positions == 0 => pattern.matches(value),
Comparator::AsciiCaseMap => pattern.capture(value, capture_positions, captured_values),
_ if capture_positions == 0 => pattern.matches(value),
_ => pattern.capture(value, capture_positions, captured_values),
}
}
pub(crate) fn regex(
&self,
pattern: &Value,
pattern_expr: &Variable,
value: &str,
capture_positions: u64,
captured_values: &mut Vec<(usize, String)>,
) -> bool {
if let Value::Regex(regex) = pattern {
let lazy_regex = regex.regex.0.load();
if let Some(regex) = lazy_regex.as_ref() {
eval_regex(regex, value, capture_positions, captured_values)
} else {
match fancy_regex::Regex::new(&regex.expr) {
Ok(fancy_regex) => {
let result =
eval_regex(&fancy_regex, value, capture_positions, captured_values);
regex.regex.0.store(Arc::new(Some(fancy_regex)));
result
}
Err(err) => {
debug_assert!(false, "Failed to compile regex: {err:?}");
false
}
}
}
} else {
match fancy_regex::Regex::new(pattern_expr.to_string().as_ref()) {
Ok(regex) => eval_regex(&regex, value, capture_positions, captured_values),
Err(err) => {
debug_assert!(false, "Failed to compile regex: {err:?}");
false
}
}
}
}
pub(crate) fn as_match(&self) -> MatchAs {
match self {
Comparator::AsciiCaseMap => MatchAs::Lowercase,
Comparator::AsciiNumeric => MatchAs::Number,
_ => MatchAs::Octet,
}
}
}
fn eval_regex(
regex: &fancy_regex::Regex,
value: &str,
mut capture_positions: u64,
captured_values: &mut Vec<(usize, String)>,
) -> bool {
if capture_positions == 0 {
regex.is_match(value).unwrap_or_default()
} else if let Ok(Some(captures)) = regex.captures(value) {
captured_values.clear();
while capture_positions != 0 {
let index = 63 - capture_positions.leading_zeros();
capture_positions ^= 1 << index;
if let Some(match_var) = captures.get(index as usize) {
captured_values.push((index as usize, match_var.as_str().to_string()));
}
}
true
} else {
false
}
}
impl Comparable for Variable {
fn to_str(&'_ self) -> Cow<'_, str> {
self.to_string()
}
fn to_number(&self) -> Number {
self.to_number()
}
}
impl Comparable for &str {
fn to_str(&'_ self) -> Cow<'_, str> {
(*self).into()
}
fn to_number(&self) -> Number {
self.parse::<f64>()
.map(Number::Float)
.unwrap_or(Number::Float(0.0))
}
}
impl RelationalMatch {
pub fn cmp<T>(&self, a: &T, b: &T) -> bool
where
T: PartialOrd + ?Sized,
{
match self {
RelationalMatch::Gt => a.gt(b),
RelationalMatch::Ge => a.ge(b),
RelationalMatch::Lt => a.lt(b),
RelationalMatch::Le => a.le(b),
RelationalMatch::Eq => a.eq(b),
RelationalMatch::Ne => a.ne(b),
}
}
}
+300
View File
@@ -0,0 +1,300 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::char::REPLACEMENT_CHARACTER;
use crate::MAX_MATCH_VARIABLES;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GlobPattern {
pattern: Vec<PatternChar>,
to_lower: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PatternChar {
WildcardMany { num: usize, match_pos: usize },
WildcardSingle { match_pos: usize },
Char { char: char, match_pos: usize },
}
impl GlobPattern {
pub fn compile(pattern: &str, to_lower: bool) -> Self {
let mut chars = Vec::new();
let mut is_escaped = false;
let mut str = pattern.chars().peekable();
while let Some(char) = str.next() {
match char {
'*' if !is_escaped => {
let mut num = 1;
while let Some('*') = str.peek() {
num += 1;
str.next();
}
chars.push(PatternChar::WildcardMany { num, match_pos: 0 });
}
'?' if !is_escaped => {
chars.push(PatternChar::WildcardSingle { match_pos: 0 });
}
'\\' if !is_escaped => {
is_escaped = true;
continue;
}
_ => {
if is_escaped {
is_escaped = false;
}
if to_lower && char.is_uppercase() {
for char in char.to_lowercase() {
chars.push(PatternChar::Char { char, match_pos: 0 });
}
} else {
chars.push(PatternChar::Char { char, match_pos: 0 });
}
}
}
}
GlobPattern {
pattern: chars,
to_lower,
}
}
// Credits: Algorithm ported from https://research.swtch.com/glob
pub fn matches(&self, value: &str) -> bool {
let value = if self.to_lower {
value.to_lowercase().chars().collect::<Vec<_>>()
} else {
value.chars().collect::<Vec<_>>()
};
let mut px = 0;
let mut nx = 0;
let mut next_px = 0;
let mut next_nx = 0;
while px < self.pattern.len() || nx < value.len() {
match self.pattern.get(px) {
Some(PatternChar::Char { char, .. }) => {
if matches!(value.get(nx), Some(nc) if nc == char ) {
px += 1;
nx += 1;
continue;
}
}
Some(PatternChar::WildcardSingle { .. }) if nx < value.len() => {
px += 1;
nx += 1;
continue;
}
Some(PatternChar::WildcardMany { .. }) => {
next_px = px;
next_nx = nx + 1;
px += 1;
continue;
}
_ => (),
}
if 0 < next_nx && next_nx <= value.len() {
px = next_px;
nx = next_nx;
continue;
}
return false;
}
true
}
pub fn capture(
mut self,
value_: &str,
capture_positions: u64,
captured_values: &mut Vec<(usize, String)>,
) -> bool {
let value = if self.to_lower {
let mut value = Vec::with_capacity(value_.len());
for char in value_.chars() {
if char.is_uppercase() {
for (pos, lowerchar) in char.to_lowercase().enumerate() {
value.push((
lowerchar,
if pos == 0 {
char
} else {
REPLACEMENT_CHARACTER
},
));
}
} else {
value.push((char, char));
}
}
value
} else {
value_.chars().map(|char| (char, char)).collect::<Vec<_>>()
};
let mut px = 0;
let mut nx = 0;
let mut next_px = 0;
let mut next_nx = 0;
while px < self.pattern.len() || nx < value.len() {
match self.pattern.get_mut(px) {
Some(PatternChar::Char { char, match_pos }) => {
if matches!(value.get(nx), Some(nc) if &nc.0 == char ) {
*match_pos = nx;
px += 1;
nx += 1;
continue;
}
}
Some(PatternChar::WildcardSingle { match_pos }) if nx < value.len() => {
*match_pos = nx;
px += 1;
nx += 1;
continue;
}
Some(PatternChar::WildcardMany { match_pos, .. }) => {
*match_pos = nx;
next_px = px;
next_nx = nx + 1;
px += 1;
continue;
}
_ => (),
}
if 0 < next_nx && next_nx <= value.len() {
px = next_px;
nx = next_nx;
continue;
}
return false;
}
let mut last_pos = 0;
captured_values.clear();
if capture_positions & 1 != 0 {
captured_values.push((0usize, value_.to_string()));
}
let mut wildcard_pos: usize = 1;
for item in &mut self.pattern {
if wildcard_pos <= MAX_MATCH_VARIABLES as usize {
last_pos = match item {
PatternChar::WildcardMany { num, match_pos } => {
while *num > 1 {
if capture_positions & (1 << wildcard_pos) != 0 {
captured_values.push((wildcard_pos, String::with_capacity(0)));
}
wildcard_pos += 1;
*num -= 1;
}
if capture_positions & (1 << wildcard_pos) != 0 {
if let Some(range) = value.get(last_pos..*match_pos) {
captured_values.push((
wildcard_pos,
range
.iter()
.filter_map(|(_, char)| {
if char != &REPLACEMENT_CHARACTER {
Some(char)
} else {
None
}
})
.collect::<String>(),
));
} else {
debug_assert!(false, "Glob pattern failure.");
return false;
}
}
wildcard_pos += 1;
*match_pos
}
PatternChar::WildcardSingle { match_pos } => {
if capture_positions & (1 << wildcard_pos) != 0 {
if let Some((char, orig_char)) = value.get(*match_pos) {
captured_values.push((
wildcard_pos,
(if orig_char != &REPLACEMENT_CHARACTER {
orig_char
} else {
char
})
.to_string(),
));
} else {
debug_assert!(false, "Glob pattern failure.");
return false;
}
}
wildcard_pos += 1;
*match_pos
}
PatternChar::Char { match_pos, .. } => *match_pos,
} + 1;
} else {
break;
}
}
true
}
}
#[cfg(test)]
mod tests {
use crate::runtime::tests::glob::GlobPattern;
#[test]
fn glob_match() {
for (value, pattern, expected_result) in [
(
"frop.......frop.........frop....",
"?*frop*",
vec!["f", "rop.......", ".........frop...."],
),
("frop:frup:frop", "*:*:*", vec!["frop", "frup", "frop"]),
(
"a b c d e f g",
"? ? ? ? ? ? ?",
vec!["a", "b", "c", "d", "e", "f", "g"],
),
("puk pok puk pok", "pu*ok", vec!["k pok puk p"]),
("snot kip snot", "snot*snot", vec![" kip "]),
(
"klopfropstroptop",
"*fr??*top",
vec!["klop", "o", "p", "strop"],
),
("toptoptop", "*top", vec!["toptop"]),
(
"Fehlende Straße zur Karte hinzufügen",
"FEHLENDE * ZUR Karte HINZUFÜGEN",
vec!["Straße"],
),
] {
let p = GlobPattern::compile(pattern, true);
let mut match_values = Vec::new();
assert!(
p.clone().capture(value, u64::MAX ^ 1, &mut match_values),
"{value:?} {pattern:?}",
);
assert_eq!(
match_values.into_iter().map(|(_, v)| v).collect::<Vec<_>>(),
expected_result,
"{value:?} {pattern:?}",
);
assert!(p.matches(value), "{value:?} {pattern:?}",);
}
}
}
+203
View File
@@ -0,0 +1,203 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::slice::Iter;
use mail_parser::{Message, MessagePart, MimeHeaders, PartType};
use crate::Context;
#[derive(Debug)]
pub(crate) enum ContentTypeFilter {
Type(String),
TypeSubtype((String, String)),
}
pub(crate) struct SubpartIterator<'x> {
ctx: &'x Context<'x>,
iter: Iter<'x, u32>,
iter_stack: Vec<Iter<'x, u32>>,
anychild: bool,
}
impl<'x> SubpartIterator<'x> {
pub(crate) fn new(ctx: &'x Context<'x>, parts: &'x [u32], anychild: bool) -> Self {
SubpartIterator {
ctx,
iter: parts.iter(),
iter_stack: Vec::new(),
anychild,
}
}
#[allow(clippy::should_implement_trait)]
pub fn next(&mut self) -> Option<(u32, &MessagePart<'x>)> {
loop {
if let Some(&part_id) = self.iter.next() {
let subpart = self.ctx.message.parts.get(part_id as usize)?;
match &subpart.body {
PartType::Multipart(subparts) if self.anychild => {
self.iter_stack
.push(std::mem::replace(&mut self.iter, subparts.iter()));
}
_ => (),
}
return Some((part_id, subpart));
}
{
let prev_iter = self.iter_stack.pop()?;
self.iter = prev_iter;
}
}
}
}
impl<'x> Context<'x> {
pub(crate) fn find_nested_parts<'z: 'x>(
&'z self,
mut message: &'x Message<'x>,
ct_filter: &[ContentTypeFilter],
visitor_fnc: &mut impl FnMut(&MessagePart, &[u8]) -> bool,
) -> bool {
let mut iter_stack = Vec::new();
let mut iter = vec![self.part].into_iter();
loop {
while let Some(part_id) = iter.next() {
if let Some(subpart) = message.parts.get(part_id as usize) {
let process_part = if !ct_filter.is_empty() {
let mut process_part = false;
let (ct, cst) = if let Some(ct) = subpart.content_type() {
(ct.c_type.as_ref(), ct.c_subtype.as_deref().unwrap_or(""))
} else {
match &subpart.body {
PartType::Text(_) => ("text", "plain"),
PartType::Html(_) => ("text", "html"),
PartType::Message(_) => ("message", "rfc822"),
PartType::Multipart(_) => ("multipart", "mixed"),
_ => ("application", "octet-stream"),
}
};
for ctf in ct_filter {
match ctf {
ContentTypeFilter::Type(name) => {
if name.eq_ignore_ascii_case(ct) {
process_part = true;
break;
}
}
ContentTypeFilter::TypeSubtype((name, subname)) => {
if name.eq_ignore_ascii_case(ct)
&& subname.eq_ignore_ascii_case(cst)
{
process_part = true;
break;
}
}
}
}
process_part
} else {
true
};
if process_part && visitor_fnc(subpart, message.raw_message.as_ref()) {
return true;
}
match &subpart.body {
PartType::Multipart(subparts) => {
iter_stack.push((
std::mem::replace(&mut iter, subparts.clone().into_iter()),
None,
));
}
PartType::Message(next_message) => {
iter_stack.push((
std::mem::replace(&mut iter, vec![0].into_iter()),
Some(message),
));
message = next_message;
}
_ => (),
}
}
}
if let Some((prev_iter, prev_message)) = iter_stack.pop() {
iter = prev_iter;
if let Some(prev_message) = prev_message {
message = prev_message;
}
} else {
break;
}
}
false
}
pub(crate) fn find_nested_parts_ids(&self, include_current: bool) -> Vec<u32> {
if self.part == 0 {
if include_current {
(0u32..self.message.parts.len() as u32).collect()
} else if self.message.parts.len() > 1 {
(1u32..self.message.parts.len() as u32).collect()
} else {
Vec::new()
}
} else {
let mut part_ids = Vec::new();
let mut iter_stack = Vec::new();
if include_current {
part_ids.push(self.part);
}
if let Some(PartType::Multipart(subparts)) =
self.message.parts.get(self.part as usize).map(|p| &p.body)
{
let mut iter = subparts.iter();
loop {
while let Some(&part_id) = iter.next() {
part_ids.push(part_id);
if let Some(PartType::Multipart(subparts)) =
self.message.parts.get(part_id as usize).map(|p| &p.body)
{
iter_stack.push(std::mem::replace(&mut iter, subparts.iter()));
}
}
if let Some(prev_iter) = iter_stack.pop() {
iter = prev_iter;
} else {
break;
}
}
}
part_ids
}
}
}
impl ContentTypeFilter {
pub(crate) fn parse(ct: &str) -> Option<ContentTypeFilter> {
let mut iter = ct.split('/');
let name = iter.next()?;
if let Some(sub_name) = iter.next() {
if !name.is_empty() && !sub_name.is_empty() && iter.next().is_none() {
Some(ContentTypeFilter::TypeSubtype((
name.to_string(),
sub_name.to_string(),
)))
} else {
None
}
} else if !name.is_empty() {
Some(ContentTypeFilter::Type(name.to_string()))
} else {
None
}
}
}
+121
View File
@@ -0,0 +1,121 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
Context, Event, Mailbox,
compiler::grammar::{Capability, test::Test},
};
use super::RuntimeError;
pub mod comparator;
pub mod glob;
pub mod mime;
pub mod test_address;
pub mod test_body;
pub mod test_date;
pub mod test_duplicate;
pub mod test_envelope;
pub mod test_exists;
pub mod test_extlists;
pub mod test_hasflag;
pub mod test_header;
pub mod test_metadata;
pub mod test_notify;
pub mod test_size;
pub mod test_spamtest;
pub mod test_string;
pub(crate) enum TestResult {
Bool(bool),
Event { event: Event, is_not: bool },
Error(RuntimeError),
}
impl Test {
pub(crate) fn exec(&self, ctx: &mut Context) -> TestResult {
match &self {
Test::Header(test) => test.exec(ctx),
Test::Address(test) => test.exec(ctx),
Test::Envelope(test) => test.exec(ctx),
Test::Exists(test) => test.exec(ctx),
Test::Size(test) => test.exec(ctx),
Test::Body(test) => test.exec(ctx),
Test::String(test) => test.exec(ctx, false),
Test::HasFlag(test) => test.exec(ctx),
Test::Date(test) => test.exec(ctx),
Test::CurrentDate(test) => test.exec(ctx),
Test::Duplicate(test) => test.exec(ctx),
Test::NotifyMethodCapability(test) => test.exec(ctx),
Test::ValidNotifyMethod(test) => test.exec(ctx),
Test::Environment(test) => test.exec(ctx, true),
Test::ValidExtList(test) => test.exec(ctx),
Test::Ihave(test) => TestResult::Bool(
test.capabilities.iter().all(|c| {
![Capability::Variables, Capability::EncodedCharacter].contains(c)
&& ctx.runtime.allowed_capabilities.contains(c)
}) ^ test.is_not,
),
Test::MailboxExists(test) => TestResult::Event {
event: Event::MailboxExists {
mailboxes: test
.mailbox_names
.iter()
.map(|m| Mailbox::Name(ctx.eval_value(m).to_string().into_owned()))
.collect(),
special_use: Vec::new(),
},
is_not: test.is_not,
},
Test::Vacation(test) => test.exec(ctx),
Test::Metadata(test) => test.exec(ctx),
Test::MetadataExists(test) => test.exec(ctx),
Test::MailboxIdExists(test) => TestResult::Event {
event: Event::MailboxExists {
mailboxes: test
.mailbox_ids
.iter()
.map(|m| Mailbox::Id(ctx.eval_value(m).to_string().into_owned()))
.collect(),
special_use: Vec::new(),
},
is_not: test.is_not,
},
Test::SpamTest(test) => test.exec(ctx),
Test::VirusTest(test) => test.exec(ctx),
Test::SpecialUseExists(test) => TestResult::Event {
event: Event::MailboxExists {
mailboxes: if let Some(mailbox) = &test.mailbox {
vec![Mailbox::Name(
ctx.eval_value(mailbox).to_string().into_owned(),
)]
} else {
Vec::new()
},
special_use: ctx.eval_values_owned(&test.attributes),
},
is_not: test.is_not,
},
Test::Convert(test) => test.exec(ctx),
Test::True => TestResult::Bool(true),
Test::False => TestResult::Bool(false),
Test::Invalid(invalid) => {
TestResult::Error(RuntimeError::InvalidInstruction(invalid.clone()))
}
#[cfg(test)]
Test::TestCmd { arguments, is_not } => TestResult::Event {
event: Event::Function {
id: u32::MAX,
arguments: arguments
.iter()
.map(|s| ctx.eval_value(s).to_owned())
.collect(),
},
is_not: *is_not,
},
}
}
}
+296
View File
@@ -0,0 +1,296 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use mail_parser::{
Addr, Address, Header, HeaderValue,
parsers::{
MessageStream,
fields::address::{
parse_address_detail_part, parse_address_domain, parse_address_local_part,
parse_address_user_part,
},
},
};
use crate::{
Context, Event,
compiler::{
Number,
grammar::{AddressPart, MatchType, tests::test_address::TestAddress},
},
};
use super::TestResult;
impl TestAddress {
pub(crate) fn exec(&self, ctx: &mut Context) -> TestResult {
let key_list = ctx.eval_values(&self.key_list);
let header_list = ctx.parse_header_names(&self.header_list);
let result = match &self.match_type {
MatchType::Is | MatchType::Contains => {
let is_is = matches!(&self.match_type, MatchType::Is);
ctx.find_headers(
&header_list,
self.index,
self.mime_anychild,
|header, _, _| {
ctx.find_addresses(header, &self.address_part, |value| {
for key in &key_list {
if is_is {
if self.comparator.is(&value, key) {
return true;
}
} else if self.comparator.contains(value, key.to_string().as_ref())
{
return true;
}
}
false
})
},
)
}
MatchType::Value(rel_match) => ctx.find_headers(
&header_list,
self.index,
self.mime_anychild,
|header, _, _| {
ctx.find_addresses(header, &self.address_part, |value| {
for key in &key_list {
if self.comparator.relational(rel_match, &value, key) {
return true;
}
}
false
})
},
),
MatchType::Matches(capture_positions) | MatchType::Regex(capture_positions) => {
let mut captured_positions = Vec::new();
let is_matches = matches!(&self.match_type, MatchType::Matches(_));
let result = ctx.find_headers(
&header_list,
self.index,
self.mime_anychild,
|header, _, _| {
ctx.find_addresses(header, &self.address_part, |value| {
for (pattern_expr, pattern) in key_list.iter().zip(self.key_list.iter())
{
if is_matches {
if self.comparator.matches(
value,
pattern_expr.to_string().as_ref(),
*capture_positions,
&mut captured_positions,
) {
return true;
}
} else if self.comparator.regex(
pattern,
pattern_expr,
value,
*capture_positions,
&mut captured_positions,
) {
return true;
}
}
false
})
},
);
if !captured_positions.is_empty() {
ctx.set_match_variables(captured_positions);
}
result
}
MatchType::Count(rel_match) => {
let mut count: i64 = 0;
ctx.find_headers(
&header_list,
self.index,
self.mime_anychild,
|header, _, _| {
ctx.find_addresses(header, &self.address_part, |value| {
if !value.is_empty() {
count += 1;
}
false
})
},
);
let mut result = false;
for key in &key_list {
if rel_match.cmp(&Number::from(count), &key.to_number()) {
result = true;
break;
}
}
result
}
MatchType::List => {
let mut values: Vec<String> = Vec::new();
ctx.find_headers(
&header_list,
self.index,
self.mime_anychild,
|header, _, _| {
ctx.find_addresses(header, &self.address_part, |value| {
if !value.is_empty() && !values.iter().any(|v| v.eq(value)) {
values.push(value.to_string());
}
false
})
},
);
if !values.is_empty() {
return TestResult::Event {
event: Event::ListContains {
lists: ctx.eval_values_owned(&self.key_list),
values,
match_as: self.comparator.as_match(),
},
is_not: self.is_not,
};
}
false
}
};
TestResult::Bool(result ^ self.is_not)
}
}
impl Context<'_> {
#[allow(unused_assignments)]
pub(crate) fn find_addresses(
&self,
header: &Header,
part: &AddressPart,
mut visitor_fnc: impl FnMut(&str) -> bool,
) -> bool {
match &header.value {
HeaderValue::Address(Address::List(addr_list)) => {
for addr in addr_list {
if let Some(addr) = part.eval(addr)
&& visitor_fnc(addr)
{
return true;
}
}
false
}
HeaderValue::Address(Address::Group(group_list)) => {
for group in group_list {
for addr in &group.addresses {
if let Some(addr) = part.eval(addr)
&& visitor_fnc(addr)
{
return true;
}
}
}
false
}
_ => {
let mut raw_header = None;
let bytes = if header.offset_end > 0 {
self.message
.raw_message
.get(header.offset_start as usize..header.offset_end as usize)
.unwrap_or(b"")
} else if let HeaderValue::Text(text) = &header.value {
// Inserted header
raw_header = format!("{text}\n").into_bytes().into();
raw_header.as_deref().unwrap()
} else {
b""
};
match MessageStream::new(bytes).parse_address() {
HeaderValue::Address(Address::List(addr_list)) => {
for addr in &addr_list {
if let Some(addr) = part.eval(addr)
&& visitor_fnc(addr)
{
return true;
}
}
false
}
HeaderValue::Address(Address::Group(group_list)) => {
for group in group_list {
for addr in &group.addresses {
if let Some(addr) = part.eval(addr)
&& visitor_fnc(addr)
{
return true;
}
}
}
false
}
_ => visitor_fnc(""),
}
}
}
}
}
impl AddressPart {
pub(crate) fn eval<'x>(&self, addr: &'x Addr<'x>) -> Option<&'x str> {
let email = addr.address.as_deref().or(addr.name.as_deref());
match (self, email) {
(AddressPart::All, _) => email,
(AddressPart::LocalPart, Some(email)) if !email.is_empty() => {
parse_address_local_part(email)
}
(AddressPart::Domain, Some(email)) if !email.is_empty() => parse_address_domain(email),
(AddressPart::User, Some(email)) if !email.is_empty() => parse_address_user_part(email),
(AddressPart::Detail, Some(email)) if !email.is_empty() => {
parse_address_detail_part(email)
}
(AddressPart::Name, _) => addr.name.as_deref(),
_ => email,
}
}
pub(crate) fn eval_strict<'x>(&self, addr: &'x Addr<'x>) -> Option<&'x str> {
match (self, addr.address.as_deref()) {
(AddressPart::All, Some(email)) => Some(email),
(AddressPart::LocalPart, Some(email)) if !email.is_empty() => {
parse_address_local_part(email)
}
(AddressPart::Domain, Some(email)) if !email.is_empty() => parse_address_domain(email),
(AddressPart::User, Some(email)) if !email.is_empty() => parse_address_user_part(email),
(AddressPart::Detail, Some(email)) if !email.is_empty() => {
parse_address_detail_part(email)
}
(AddressPart::Name, _) => addr.name.as_deref(),
(_, email) => email,
}
}
pub(crate) fn eval_string<'x>(&self, addr: &'x str) -> Option<&'x str> {
if !addr.is_empty() {
match self {
AddressPart::All => addr.into(),
AddressPart::LocalPart => parse_address_local_part(addr),
AddressPart::Domain => parse_address_domain(addr),
AddressPart::User => parse_address_user_part(addr),
AddressPart::Detail => parse_address_detail_part(addr),
_ => addr.into(),
}
} else {
addr.into()
}
}
}
+224
View File
@@ -0,0 +1,224 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use mail_parser::{MimeHeaders, PartType, decoders::html::html_to_text};
use crate::{
Context,
compiler::{
Number,
grammar::{
MatchType,
tests::test_body::{BodyTransform, TestBody},
},
},
};
use super::{TestResult, mime::ContentTypeFilter};
impl TestBody {
pub(crate) fn exec(&self, ctx: &mut Context) -> TestResult {
// Check Subject (not a Sieve standard)
let key_list = ctx.eval_values(&self.key_list);
if self.include_subject {
let subject = if !matches!(&self.body_transform, BodyTransform::Raw) {
ctx.message.subject().unwrap_or_default()
} else {
ctx.message.header_raw("Subject").unwrap_or_default()
};
for (key, pattern) in key_list.iter().zip(self.key_list.iter()) {
let result = match &self.match_type {
MatchType::Is => self.comparator.is(&subject, key),
MatchType::Contains => {
self.comparator.contains(subject, key.to_string().as_ref())
}
MatchType::Value(rel_match) => {
self.comparator.relational(rel_match, &subject, key)
}
MatchType::Matches(_) => self.comparator.matches(
subject,
key.to_string().as_ref(),
0,
&mut Vec::new(),
),
MatchType::Regex(_) => {
self.comparator
.regex(pattern, key, subject, 0, &mut Vec::new())
}
_ => break,
};
if result {
return TestResult::Bool(result ^ self.is_not);
}
}
}
let ct_filter = match &self.body_transform {
BodyTransform::Text | BodyTransform::Raw => Vec::new(),
BodyTransform::Content(values) => {
let mut ct_filter = Vec::with_capacity(values.len());
for ct in values {
let ct = ctx.eval_value(ct);
if ct.is_empty() {
break;
} else if let Some(ctf) = ContentTypeFilter::parse(ct.to_string().as_ref()) {
ct_filter.push(ctf);
} else {
return TestResult::Bool(false ^ self.is_not);
}
}
ct_filter
}
};
let result = if let MatchType::Count(rel_match) = &self.match_type {
let mut count = 0;
let mut result = false;
ctx.find_nested_parts(&ctx.message, &ct_filter, &mut |_part, _raw_message| {
count += 1;
false
});
for key in &self.key_list {
if rel_match.cmp(&Number::from(count), &ctx.eval_value(key).to_number()) {
result = true;
break;
}
}
result
} else {
ctx.find_nested_parts(&ctx.message, &ct_filter, &mut |part, raw_message| {
let text = match (&self.body_transform, &part.body) {
(BodyTransform::Content(_), PartType::Message(message)) => {
if let Some(part) = message.parts.first() {
String::from_utf8_lossy(
raw_message
.get(
part.raw_header_offset() as usize
..part.raw_body_offset() as usize,
)
.unwrap_or(b""),
)
} else {
return false;
}
}
(BodyTransform::Content(_), PartType::Multipart(_)) => {
if let Some(boundary) =
part.content_type().and_then(|ct| ct.attribute("boundary"))
{
let mime_body = std::str::from_utf8(
raw_message
.get(
part.raw_body_offset() as usize
..part.raw_end_offset() as usize,
)
.unwrap_or(b""),
)
.unwrap_or("");
let mut mime_part = String::with_capacity(64);
if let Some((prologue, epilogue)) =
mime_body.split_once(&format!("\n--{boundary}"))
{
mime_part.push_str(prologue);
if let Some((_, epilogue)) =
epilogue.rsplit_once(&format!("\n--{boundary}--"))
{
mime_part.push_str(epilogue);
}
}
mime_part.into()
} else {
String::from_utf8_lossy(
raw_message
.get(
part.raw_body_offset() as usize
..part.raw_end_offset() as usize,
)
.unwrap_or(b""),
)
}
}
(BodyTransform::Raw, _) => {
match &part.body {
PartType::Text(text) if part.raw_body_offset() == 0 => {
// Inserted part
text.as_ref().into()
}
_ if part.raw_end_offset() > part.raw_body_offset() => {
String::from_utf8_lossy(
raw_message
.get(
part.raw_body_offset() as usize
..part.raw_end_offset() as usize,
)
.unwrap_or(b""),
)
}
_ => return false,
}
}
(_, PartType::Text(text))
| (BodyTransform::Content(_), PartType::Html(text)) => text.as_ref().into(),
(_, PartType::Html(html)) => html_to_text(html.as_ref()).into(),
(
BodyTransform::Text,
PartType::Binary(bytes) | PartType::InlineBinary(bytes),
) if part.content_type().is_some_and(|ct| {
ct.c_type.eq_ignore_ascii_case("application")
&& ct.c_subtype.as_ref().is_some_and(|st| st.contains("xml"))
}) =>
{
html_to_text(std::str::from_utf8(bytes.as_ref()).unwrap_or("")).into()
}
(
BodyTransform::Content(_),
PartType::Binary(bytes) | PartType::InlineBinary(bytes),
) => String::from_utf8_lossy(bytes.as_ref()),
_ => {
return false;
}
};
let mut result = false;
for (key, pattern) in key_list.iter().zip(self.key_list.iter()) {
result = match &self.match_type {
MatchType::Is => self.comparator.is(&text.as_ref(), key),
MatchType::Contains => self
.comparator
.contains(text.as_ref(), key.to_string().as_ref()),
MatchType::Value(rel_match) => {
self.comparator.relational(rel_match, &text.as_ref(), key)
}
MatchType::Matches(_) => self.comparator.matches(
text.as_ref(),
key.to_string().as_ref(),
0,
&mut Vec::new(),
),
MatchType::Regex(_) => {
self.comparator
.regex(pattern, key, text.as_ref(), 0, &mut Vec::new())
}
_ => false,
};
if result {
break;
}
}
result
})
};
TestResult::Bool(result ^ self.is_not)
}
}
+293
View File
@@ -0,0 +1,293 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::borrow::Cow;
use mail_parser::{DateTime, Header, HeaderValue, parsers::MessageStream};
use crate::{
Context, Event,
compiler::{
Number,
grammar::{
MatchType,
tests::test_date::{DatePart, TestCurrentDate, TestDate, Zone},
},
},
};
use super::TestResult;
impl TestDate {
pub(crate) fn exec(&self, ctx: &mut Context) -> TestResult {
let header_name = if let Some(header_name) = ctx.parse_header_name(&self.header_name) {
header_name
} else {
return TestResult::Bool(false ^ self.is_not);
};
let result = match &self.match_type {
MatchType::Count(rel_match) => {
let mut date_count = 0;
ctx.find_headers(
&[header_name],
self.index,
self.mime_anychild,
|header, _, _| {
if ctx.find_dates(header).is_some() {
date_count += 1;
}
false
},
);
let mut result = false;
for key in &self.key_list {
if rel_match.cmp(&Number::from(date_count), &ctx.eval_value(key).to_number()) {
result = true;
break;
}
}
result
}
MatchType::List => {
let mut values = Vec::new();
ctx.find_headers(
&[header_name],
self.index,
self.mime_anychild,
|header, _, _| {
if let Some(dt) = ctx.find_dates(header) {
let value = self.date_part.eval(self.zone.eval(dt.as_ref()).as_ref());
if !value.is_empty() && !values.iter().any(|v: &String| v.eq(&value)) {
values.push(value);
}
}
false
},
);
if !values.is_empty() {
return TestResult::Event {
event: Event::ListContains {
lists: ctx.eval_values_owned(&self.key_list),
values,
match_as: self.comparator.as_match(),
},
is_not: self.is_not,
};
}
false
}
_ => {
let key_list = ctx.eval_values(&self.key_list);
let mut captured_values = Vec::new();
let result = ctx.find_headers(
&[header_name],
self.index,
self.mime_anychild,
|header, _, _| {
if let Some(dt) = ctx.find_dates(header) {
let date_part =
self.date_part.eval(self.zone.eval(dt.as_ref()).as_ref());
for key in &key_list {
if match &self.match_type {
MatchType::Is => self.comparator.is(&date_part.as_str(), key),
MatchType::Contains => self
.comparator
.contains(&date_part, key.to_string().as_ref()),
MatchType::Value(rel_match) => self.comparator.relational(
rel_match,
&date_part.as_str(),
key,
),
MatchType::Matches(capture_positions) => {
self.comparator.matches(
&date_part,
key.to_string().as_ref(),
*capture_positions,
&mut captured_values,
)
}
MatchType::Regex(capture_positions) => self.comparator.matches(
&date_part,
key.to_string().as_ref(),
*capture_positions,
&mut captured_values,
),
MatchType::Count(_) | MatchType::List => false,
} {
return true;
}
}
}
false
},
);
if !captured_values.is_empty() {
ctx.set_match_variables(captured_values);
}
result
}
};
TestResult::Bool(result ^ self.is_not)
}
}
impl TestCurrentDate {
pub(crate) fn exec(&self, ctx: &mut Context) -> TestResult {
let mut result = false;
match &self.match_type {
MatchType::Count(rel_match) => {
for key in &self.key_list {
if rel_match.cmp(&Number::from(1.0), &ctx.eval_value(key).to_number()) {
result = true;
break;
}
}
}
MatchType::List => {
let value = self.date_part.eval(
&(if let Some(zone) = self.zone {
DateTime::from_timestamp(ctx.current_time).to_timezone(zone)
} else {
DateTime::from_timestamp(ctx.current_time)
}),
);
if !value.is_empty() {
return TestResult::Event {
event: Event::ListContains {
lists: ctx.eval_values_owned(&self.key_list),
values: vec![value],
match_as: self.comparator.as_match(),
},
is_not: self.is_not,
};
}
}
_ => {
let mut captured_values = Vec::new();
let date_part = self.date_part.eval(
&(if let Some(zone) = self.zone {
DateTime::from_timestamp(ctx.current_time).to_timezone(zone)
} else {
DateTime::from_timestamp(ctx.current_time)
}),
);
for key in &self.key_list {
let key = ctx.eval_value(key);
if match &self.match_type {
MatchType::Is => self.comparator.is(&date_part.as_str(), &key),
MatchType::Contains => self
.comparator
.contains(&date_part, key.to_string().as_ref()),
MatchType::Value(rel_match) => {
self.comparator
.relational(rel_match, &date_part.as_str(), &key)
}
MatchType::Matches(capture_positions) => self.comparator.matches(
&date_part,
key.to_string().as_ref(),
*capture_positions,
&mut captured_values,
),
MatchType::Regex(capture_positions) => self.comparator.matches(
&date_part,
key.to_string().as_ref(),
*capture_positions,
&mut captured_values,
),
MatchType::Count(_) | MatchType::List => false,
} {
result = true;
break;
}
}
if !captured_values.is_empty() {
ctx.set_match_variables(captured_values);
}
}
}
TestResult::Bool(result ^ self.is_not)
}
}
impl<'x> Context<'x> {
#[allow(unused_assignments)]
pub(crate) fn find_dates(&self, header: &'x Header) -> Option<Cow<'x, DateTime>> {
if let HeaderValue::DateTime(dt) = &header.value {
if dt.is_valid() {
return Some(Cow::Borrowed(dt));
}
} else if header.offset_end > 0 {
let bytes = self
.message
.raw_message
.get(header.offset_start as usize..header.offset_end as usize)?;
if let HeaderValue::DateTime(dt) = MessageStream::new(bytes).parse_date()
&& dt.is_valid()
{
return Some(Cow::Owned(dt));
}
} else if let HeaderValue::Text(text) = &header.value {
// Inserted header
let bytes = format!("{text}\n").into_bytes();
if let HeaderValue::DateTime(dt) = MessageStream::new(&bytes).parse_date()
&& dt.is_valid()
{
return Some(Cow::Owned(dt));
}
}
None
}
}
impl DatePart {
fn eval(&self, dt: &DateTime) -> String {
match self {
DatePart::Year => format!("{:04}", dt.year),
DatePart::Month => format!("{:02}", dt.month),
DatePart::Day => format!("{:02}", dt.day),
DatePart::Date => format!("{:04}-{:02}-{:02}", dt.year, dt.month, dt.day,),
DatePart::Julian => ((dt.julian_day() as f64 - 2400000.5) as i64).to_string(),
DatePart::Hour => format!("{:02}", dt.hour),
DatePart::Minute => format!("{:02}", dt.minute),
DatePart::Second => format!("{:02}", dt.second),
DatePart::Time => format!("{:02}:{:02}:{:02}", dt.hour, dt.minute, dt.second,),
DatePart::Iso8601 => dt.to_rfc3339(),
DatePart::Std11 => dt.to_rfc822(),
DatePart::Zone => format!(
"{}{:02}{:02}",
if dt.tz_before_gmt && (dt.tz_hour > 0 || dt.tz_minute > 0) {
"-"
} else {
"+"
},
dt.tz_hour,
dt.tz_minute
),
DatePart::Weekday => dt.day_of_week().to_string(),
}
}
}
impl Zone {
pub(crate) fn eval<'x>(&self, dt: &'x DateTime) -> Cow<'x, DateTime> {
match self {
Zone::Time(tz) => Cow::Owned(dt.to_timezone(*tz)),
Zone::Original => Cow::Borrowed(dt),
Zone::Local => Cow::Owned(DateTime::from_timestamp(dt.to_timestamp())),
}
}
}
+70
View File
@@ -0,0 +1,70 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::borrow::Cow;
use mail_parser::{HeaderValue, parsers::MessageStream};
use crate::{
Context, Event,
compiler::grammar::tests::test_duplicate::{DupMatch, TestDuplicate},
};
use super::TestResult;
impl TestDuplicate {
pub(crate) fn exec(&self, ctx: &mut Context) -> TestResult {
let id: Cow<str> = match &self.dup_match {
DupMatch::Header(header_name) => {
let mut value = String::new();
if let Some(header_name) = ctx.parse_header_name(header_name) {
ctx.find_headers(&[header_name], None, true, |header, _, _| {
if header.offset_end > 0 {
if let Some(bytes) = ctx
.message
.raw_message
.get(header.offset_start as usize..header.offset_end as usize)
&& let HeaderValue::Text(id) = MessageStream::new(bytes).parse_id()
&& !id.is_empty()
{
value = id.to_string();
return true;
}
} else if let HeaderValue::Text(text) = &header.value {
// Inserted header
let bytes = format!("{text}\n").into_bytes();
if let HeaderValue::Text(id) = MessageStream::new(&bytes).parse_id()
&& !id.is_empty()
{
value = id.to_string();
return true;
}
}
false
});
}
value.into()
}
DupMatch::UniqueId(s) => ctx.eval_value(s).to_string().into_owned().into(),
DupMatch::Default => ctx.message.message_id().unwrap_or("").into(),
};
TestResult::Event {
event: Event::DuplicateId {
id: if id.is_empty() {
return TestResult::Bool(false ^ self.is_not);
} else if let Some(handle) = &self.handle {
format!("{}{}", ctx.eval_value(handle).to_string(), id)
} else {
id.into_owned()
},
expiry: self.seconds.unwrap_or(ctx.runtime.default_duplicate_expiry),
last: self.last,
},
is_not: self.is_not,
}
}
}
+258
View File
@@ -0,0 +1,258 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use mail_parser::DateTime;
use crate::{
Context, Envelope, Event,
compiler::{
Number,
grammar::{MatchType, tests::test_envelope::TestEnvelope},
},
};
use super::TestResult;
impl TestEnvelope {
pub(crate) fn exec(&self, ctx: &mut Context) -> TestResult {
let key_list = ctx.eval_values(&self.key_list);
let result = match &self.match_type {
MatchType::Is | MatchType::Contains => {
let is_is = matches!(&self.match_type, MatchType::Is);
ctx.find_envelopes(self, |value| {
for key in &key_list {
if is_is {
if self.comparator.is(&value, key) {
return true;
}
} else if self.comparator.contains(value, key.to_string().as_ref()) {
return true;
}
}
false
})
}
MatchType::Value(rel_match) => ctx.find_envelopes(self, |value| {
for key in &key_list {
if self.comparator.relational(rel_match, &value, key) {
return true;
}
}
false
}),
MatchType::Matches(capture_positions) | MatchType::Regex(capture_positions) => {
let mut captured_positions = Vec::new();
let is_matches = matches!(&self.match_type, MatchType::Matches(_));
let result = ctx.find_envelopes(self, |value| {
for (pattern_expr, pattern) in key_list.iter().zip(self.key_list.iter()) {
if is_matches {
if self.comparator.matches(
value,
pattern_expr.to_string().as_ref(),
*capture_positions,
&mut captured_positions,
) {
return true;
}
} else if self.comparator.regex(
pattern,
pattern_expr,
value,
*capture_positions,
&mut captured_positions,
) {
return true;
}
}
false
});
if !captured_positions.is_empty() {
ctx.set_match_variables(captured_positions);
}
result
}
MatchType::Count(rel_match) => {
let mut count = 0;
ctx.find_envelopes(self, |value| {
if !value.is_empty() {
count += 1;
}
false
});
let mut result = false;
for key in &key_list {
if rel_match.cmp(&Number::from(count), &key.to_number()) {
result = true;
break;
}
}
result
}
MatchType::List => {
let mut values: Vec<String> = Vec::new();
ctx.find_envelopes(self, |value| {
if !value.is_empty() && !values.iter().any(|v| v.eq(value)) {
values.push(value.to_string());
}
false
});
if !values.is_empty() {
return TestResult::Event {
event: Event::ListContains {
lists: ctx.eval_values_owned(&self.key_list),
values,
match_as: self.comparator.as_match(),
},
is_not: self.is_not,
};
}
false
}
};
TestResult::Bool(result ^ self.is_not)
}
}
impl Context<'_> {
fn find_envelopes(
&self,
test_envelope: &TestEnvelope,
mut cb: impl FnMut(&str) -> bool,
) -> bool {
for (name, value) in &self.envelope {
if test_envelope.envelope_list.contains(name)
&& match name {
Envelope::From | Envelope::To | Envelope::Orcpt => {
if let Some(value) = test_envelope
.address_part
.eval_string(value.to_string().as_ref())
{
cb(value)
} else {
false
}
}
Envelope::ByTimeAbsolute if test_envelope.zone.is_some() => {
if let Some(dt) = DateTime::parse_rfc3339(value.to_string().as_ref()) {
cb(&dt.to_timezone(test_envelope.zone.unwrap()).to_rfc3339())
} else {
cb("")
}
}
_ => cb(value.to_string().as_ref()),
}
{
return true;
}
}
false
}
}
pub fn parse_envelope_address(addr: &str) -> Option<&str> {
let addr = addr.as_bytes();
let mut addr_start_pos = 0;
let mut addr_end_pos = addr.len();
let mut last_ch = 0;
let mut at_pos = 0;
let mut has_bracket = false;
let mut in_path = false;
if addr.is_empty() {
return "".into();
}
for (pos, &ch) in addr.iter().enumerate() {
match ch {
b'<' => {
if pos == 0 {
addr_start_pos = pos + 1;
has_bracket = true;
} else {
return None;
}
}
b'>' => {
if has_bracket && pos == addr.len() - 1 {
if addr.len() > 2 {
has_bracket = false;
addr_end_pos = pos;
} else {
// <>
return "".into();
}
} else {
return None;
}
}
b':' => {
if at_pos != 0 {
at_pos = 0;
addr_start_pos = pos + 1;
in_path = false;
} else {
return None;
}
}
b',' => {
if at_pos != 0 {
at_pos = 0;
in_path = true;
} else {
return None;
}
}
b'@' => {
if at_pos == 0 && pos != addr.len() - 1 {
at_pos = pos;
} else {
return None;
}
}
b'.' => {
if (at_pos != 0 && last_ch == b'.') || last_ch == b'@' {
return None;
}
}
_ => {
if ch.is_ascii_whitespace() || !ch.is_ascii() {
return None;
}
}
}
last_ch = ch;
}
if !has_bracket && !in_path && at_pos > addr_start_pos && addr_end_pos - 1 > at_pos {
std::str::from_utf8(&addr[addr_start_pos..addr_end_pos])
.unwrap()
.into()
} else {
match addr.get(addr_start_pos..addr_end_pos) {
Some(addr) if at_pos == 0 && addr.eq_ignore_ascii_case(b"mailer-daemon") => {
std::str::from_utf8(addr).unwrap().into()
}
_ => None,
}
}
}
+36
View File
@@ -0,0 +1,36 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{Context, compiler::grammar::tests::test_exists::TestExists};
use super::{TestResult, mime::SubpartIterator};
impl TestExists {
pub(crate) fn exec(&self, ctx: &mut Context) -> TestResult {
let header_names = ctx.parse_header_names(&self.header_names);
let mut header_exists = vec![false; header_names.len()];
let parts = [ctx.part];
let mut part_iter = SubpartIterator::new(ctx, &parts, self.mime_anychild);
let mut result = false;
while let Some((_, message_part)) = part_iter.next() {
for (pos, header_name) in header_names.iter().enumerate() {
if !header_exists[pos]
&& message_part.headers.iter().any(|h| &h.name == header_name)
{
header_exists[pos] = true;
}
}
if header_exists.iter().all(|v| *v) {
result = true;
break;
}
}
TestResult::Bool(result ^ self.is_not)
}
}
+27
View File
@@ -0,0 +1,27 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{Context, compiler::grammar::tests::test_extlists::TestValidExtList};
use super::TestResult;
impl TestValidExtList {
pub(crate) fn exec(&self, ctx: &mut Context) -> TestResult {
let mut num_valid = 0;
for list in &self.list_names {
if ctx
.runtime
.valid_ext_lists
.contains(&ctx.eval_value(list).to_string())
{
num_valid += 1;
}
}
TestResult::Bool((num_valid == self.list_names.len()) ^ self.is_not)
}
}
+96
View File
@@ -0,0 +1,96 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
Context,
compiler::{
Number, VariableType,
grammar::{MatchType, tests::test_hasflag::TestHasFlag},
},
};
use super::TestResult;
impl TestHasFlag {
pub(crate) fn exec(&self, ctx: &mut Context) -> TestResult {
let mut variable_list_ = None;
let variable_list = if !self.variable_list.is_empty() {
&self.variable_list
} else {
variable_list_.get_or_insert_with(|| vec![VariableType::Global("__flags".to_string())])
};
let result = if let MatchType::Count(rel_match) = &self.match_type {
let mut flag_count = 0;
for variable in variable_list {
match ctx.get_variable(variable) {
Some(flags) if !flags.is_empty() => {
flag_count += flags.to_string().split(' ').count();
}
_ => (),
}
}
let mut result = false;
for key in &self.flags {
if rel_match.cmp(
&Number::from(flag_count as i64),
&ctx.eval_value(key).to_number(),
) {
result = true;
break;
}
}
result
} else {
let mut captured_values = Vec::new();
let result = ctx.tokenize_flags(&self.flags, |check_flag| {
for variable in variable_list {
match ctx.get_variable(variable) {
Some(flags) if !flags.is_empty() => {
for flag in flags.to_string().split(' ') {
if match &self.match_type {
MatchType::Is => self.comparator.is(&flag, &check_flag),
MatchType::Contains => {
self.comparator.contains(flag, check_flag)
}
MatchType::Value(rel_match) => {
self.comparator.relational(rel_match, &flag, &check_flag)
}
MatchType::Matches(capture_positions) => {
self.comparator.matches(
flag,
check_flag,
*capture_positions,
&mut captured_values,
)
}
MatchType::Regex(capture_positions) => self.comparator.matches(
flag,
check_flag,
*capture_positions,
&mut captured_values,
),
MatchType::Count(_) | MatchType::List => false,
} {
return true;
}
}
}
_ => (),
}
}
false
});
if !captured_values.is_empty() {
ctx.set_match_variables(captured_values);
}
result
};
TestResult::Bool(result ^ self.is_not)
}
}
+364
View File
@@ -0,0 +1,364 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use mail_parser::{Header, HeaderName, HeaderValue, parsers::MessageStream};
use crate::{
Context, Event,
compiler::{
Number, Value,
grammar::{MatchType, actions::action_mime::MimeOpts, tests::test_header::TestHeader},
},
runtime::Variable,
};
use super::{TestResult, mime::SubpartIterator};
impl TestHeader {
pub(crate) fn exec(&self, ctx: &mut Context) -> TestResult {
let key_list = ctx.eval_values(&self.key_list);
let header_list = ctx.parse_header_names(&self.header_list);
let mime_opts = match &self.mime_opts {
MimeOpts::Type => MimeOpts::Type,
MimeOpts::Subtype => MimeOpts::Subtype,
MimeOpts::ContentType => MimeOpts::ContentType,
MimeOpts::Param(params) => MimeOpts::Param(ctx.eval_values(params)),
MimeOpts::None => MimeOpts::None,
};
let result = match &self.match_type {
MatchType::Is | MatchType::Contains => {
let is_is = matches!(&self.match_type, MatchType::Is);
ctx.find_headers(
&header_list,
self.index,
self.mime_anychild,
|header, _, _| {
ctx.find_header_values(header, &mime_opts, |value| {
for key in &key_list {
if is_is {
if self.comparator.is(&value, key) {
return true;
}
} else if self.comparator.contains(value, key.to_string().as_ref())
{
return true;
}
}
false
})
},
)
}
MatchType::Value(rel_match) => ctx.find_headers(
&header_list,
self.index,
self.mime_anychild,
|header, _, _| {
ctx.find_header_values(header, &mime_opts, |value| {
for key in &key_list {
if self.comparator.relational(rel_match, &value, key) {
return true;
}
}
false
})
},
),
MatchType::Matches(capture_positions) | MatchType::Regex(capture_positions) => {
let mut captured_values = Vec::new();
let is_matches = matches!(&self.match_type, MatchType::Matches(_));
let result = ctx.find_headers(
&header_list,
self.index,
self.mime_anychild,
|header, _, _| {
ctx.find_header_values(header, &mime_opts, |value| {
for (pattern_expr, pattern) in key_list.iter().zip(self.key_list.iter())
{
if is_matches {
if self.comparator.matches(
value,
pattern_expr.to_string().as_ref(),
*capture_positions,
&mut captured_values,
) {
return true;
}
} else if self.comparator.regex(
pattern,
pattern_expr,
value,
*capture_positions,
&mut captured_values,
) {
return true;
}
}
false
})
},
);
if !captured_values.is_empty() {
ctx.set_match_variables(captured_values);
}
result
}
MatchType::Count(rel_match) => {
let mut count = 0;
ctx.find_headers(
&header_list,
self.index,
self.mime_anychild,
|header, _, _| {
match &mime_opts {
MimeOpts::None => {
count += 1;
}
MimeOpts::Type | MimeOpts::Subtype | MimeOpts::ContentType => {
if let HeaderValue::ContentType(_) = &header.value {
count += 1;
}
}
MimeOpts::Param(params) => {
if let HeaderValue::ContentType(ct) = &header.value
&& let Some(attributes) = &ct.attributes
{
for attr in attributes {
if params
.iter()
.any(|p| p.to_string().eq_ignore_ascii_case(&attr.name))
{
count += 1;
}
}
}
}
}
false
},
);
let mut result = false;
for key in &key_list {
if rel_match.cmp(&Number::from(count), &key.to_number()) {
result = true;
break;
}
}
result
}
MatchType::List => {
let mut values: Vec<String> = Vec::new();
ctx.find_headers(
&header_list,
self.index,
self.mime_anychild,
|header, _, _| {
ctx.find_header_values(header, &mime_opts, |value| {
if !value.is_empty() && !values.iter().any(|v| v.eq(value)) {
values.push(value.to_string());
}
false
})
},
);
if !values.is_empty() {
return TestResult::Event {
event: Event::ListContains {
lists: ctx.eval_values_owned(&self.key_list),
values,
match_as: self.comparator.as_match(),
},
is_not: self.is_not,
};
}
false
}
};
TestResult::Bool(result ^ self.is_not)
}
}
impl Context<'_> {
pub(crate) fn parse_header_names<'z: 'y, 'y>(
&'z self,
header_names: &'y [Value],
) -> Vec<HeaderName<'y>> {
let mut result = Vec::with_capacity(header_names.len());
for header_name in header_names {
if let Some(header_name) = self.parse_header_name(header_name) {
result.push(header_name);
}
}
result
}
#[inline(always)]
pub(crate) fn parse_header_name(&self, header_name: &Value) -> Option<HeaderName<'static>> {
let h_ = self.eval_value(header_name);
let h = h_.to_string();
match HeaderName::parse(h.as_ref())? {
HeaderName::Other(_) => HeaderName::Other(h.into_owned().into()),
hn => hn.into_owned(),
}
.into()
}
pub(crate) fn find_headers(
&self,
header_names: &[HeaderName],
index: Option<i32>,
any_child: bool,
mut visitor_fnc: impl FnMut(&Header, u32, usize) -> bool,
) -> bool {
let parts = [self.part];
let mut part_iter = SubpartIterator::new(self, &parts, any_child);
while let Some((part_id, message_part)) = part_iter.next() {
'outer: for header_name in header_names {
match index {
None => {
for (pos, header) in message_part
.headers
.iter()
.enumerate()
.filter(|(_, h)| &h.name == header_name)
{
if visitor_fnc(header, part_id, pos) {
return true;
}
}
}
Some(index) if index >= 0 => {
let mut header_count = 0;
for (pos, header) in message_part.headers.iter().enumerate() {
if &header.name == header_name {
header_count += 1;
if header_count == index {
if visitor_fnc(header, part_id, pos) {
return true;
}
continue 'outer;
}
}
}
}
Some(index) => {
let index = -index;
let mut header_count = 0;
for (pos, header) in message_part.headers.iter().enumerate().rev() {
if &header.name == header_name {
header_count += 1;
if header_count == index {
if visitor_fnc(header, part_id, pos) {
return true;
}
break;
}
}
}
}
}
}
}
false
}
#[allow(unused_assignments)]
pub(crate) fn find_header_values(
&self,
header: &Header,
mime_opts: &MimeOpts<Variable>,
mut visitor_fnc: impl FnMut(&str) -> bool,
) -> bool {
let mut raw_header = None;
let mut header_value_ = None;
let header_value = if header.offset_end != 0 {
&header.value
} else {
let value = if let HeaderValue::Text(text) = &header.value {
text.as_ref()
} else {
#[cfg(test)]
panic!("Unexpected value.");
#[cfg(not(test))]
return false;
};
if mime_opts == &MimeOpts::None {
return visitor_fnc(value);
} else {
raw_header = format!("{value}\n").into_bytes().into();
header_value_ = MessageStream::new(raw_header.as_ref().unwrap())
.parse_content_type()
.into();
header_value_.as_ref().unwrap()
}
};
match (mime_opts, header_value) {
(MimeOpts::None, HeaderValue::Text(text))
if matches!(
&header.name,
HeaderName::Subject
| HeaderName::Comments
| HeaderName::ContentDescription
| HeaderName::ContentLocation
| HeaderName::ContentTransferEncoding,
) =>
{
visitor_fnc(text.as_ref())
}
(MimeOpts::None, _) => {
if let HeaderValue::Text(text) = MessageStream::new(
self.message
.raw_message
.get(header.offset_start as usize..header.offset_end as usize)
.unwrap_or(b""),
)
.parse_unstructured()
{
visitor_fnc(text.as_ref())
} else {
visitor_fnc("")
}
}
(MimeOpts::Type, HeaderValue::ContentType(ct)) => visitor_fnc(ct.c_type.as_ref()),
(MimeOpts::Subtype, HeaderValue::ContentType(ct)) => {
visitor_fnc(ct.c_subtype.as_deref().unwrap_or(""))
}
(MimeOpts::ContentType, HeaderValue::ContentType(ct)) => {
if let Some(sub_type) = &ct.c_subtype {
visitor_fnc(&format!("{}/{}", ct.c_type, sub_type))
} else {
visitor_fnc(ct.c_type.as_ref())
}
}
(MimeOpts::Param(params), HeaderValue::ContentType(ct)) => {
if let Some(attributes) = &ct.attributes {
for param in params {
for attr in attributes {
if param.to_string().eq_ignore_ascii_case(&attr.name)
&& visitor_fnc(attr.value.as_ref())
{
return true;
}
}
}
}
visitor_fnc("")
}
_ => visitor_fnc(""),
}
}
}
+132
View File
@@ -0,0 +1,132 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
Context, Metadata,
compiler::{
Number,
grammar::{
MatchType,
tests::test_mailbox::{TestMetadata, TestMetadataExists},
},
},
};
use super::TestResult;
impl TestMetadata {
pub(crate) fn exec(&self, ctx: &mut Context) -> TestResult {
let metadata = match &self.medatata {
Metadata::Server { annotation } => Metadata::Server {
annotation: ctx.eval_value(annotation).to_string().into_owned(),
},
Metadata::Mailbox { name, annotation } => Metadata::Mailbox {
name: ctx.eval_value(name).to_string().into_owned(),
annotation: ctx.eval_value(annotation).to_string().into_owned(),
},
};
let value = if let Some((_, value)) = [&ctx.metadata, &ctx.runtime.metadata]
.into_iter()
.flatten()
.find(|(m, _)| match (m, &metadata) {
(Metadata::Server { annotation: a }, Metadata::Server { annotation: b }) => {
a.eq_ignore_ascii_case(b)
}
(
Metadata::Mailbox {
name: a,
annotation: c,
},
Metadata::Mailbox {
name: b,
annotation: d,
},
) => a.eq(b) && c.eq_ignore_ascii_case(d),
_ => false,
}) {
value.as_ref()
} else {
return TestResult::Bool(false ^ self.is_not);
};
let mut result = false;
if let MatchType::Count(match_type) = &self.match_type {
for key in &self.key_list {
if match_type.cmp(&Number::Float(1.0), &ctx.eval_value(key).to_number()) {
result = true;
break;
}
}
} else {
let mut captured_values = Vec::new();
for pattern in &self.key_list {
let key = ctx.eval_value(pattern);
result = match &self.match_type {
MatchType::Is => self.comparator.is(&value, &key),
MatchType::Contains => {
self.comparator.contains(value, key.to_string().as_ref())
}
MatchType::Value(relation) => {
self.comparator.relational(relation, &value, &key)
}
MatchType::Matches(capture_positions) => self.comparator.matches(
value,
key.to_string().as_ref(),
*capture_positions,
&mut captured_values,
),
MatchType::Regex(capture_positions) => self.comparator.regex(
pattern,
&key,
value,
*capture_positions,
&mut captured_values,
),
_ => false,
};
if result {
break;
}
}
if !captured_values.is_empty() {
ctx.set_match_variables(captured_values);
}
}
TestResult::Bool(result ^ self.is_not)
}
}
impl TestMetadataExists {
pub(crate) fn exec(&self, ctx: &Context) -> TestResult {
let mailbox = self
.mailbox
.as_ref()
.map(|s| ctx.eval_value(s).to_string().into_owned());
let mut annotations = ctx.eval_values(&self.annotation_names);
for (metadata, _) in [&ctx.metadata, &ctx.runtime.metadata].into_iter().flatten() {
match (metadata, mailbox.as_ref()) {
(Metadata::Server { annotation }, None) => {
annotations.retain(|a| !a.to_string().eq_ignore_ascii_case(annotation))
}
(Metadata::Mailbox { name, annotation }, Some(mailbox)) if name.eq(mailbox) => {
annotations.retain(|a| !a.to_string().eq_ignore_ascii_case(annotation));
}
_ => (),
}
if annotations.is_empty() {
return TestResult::Bool(true ^ self.is_not);
}
}
TestResult::Bool(false ^ self.is_not)
}
}
+99
View File
@@ -0,0 +1,99 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::borrow::Cow;
use crate::{
Context,
compiler::{
Number,
grammar::{
MatchType,
tests::test_notify::{TestNotifyMethodCapability, TestValidNotifyMethod},
},
},
runtime::actions::action_notify::validate_uri,
};
use super::TestResult;
impl TestValidNotifyMethod {
pub(crate) fn exec(&self, ctx: &mut Context) -> TestResult {
let mut num_valid = 0;
for uri in &self.notification_uris {
let uri_ = ctx.eval_value(uri);
let uri = uri_.to_string();
if let Some(scheme) = validate_uri(uri.as_ref())
&& (ctx
.runtime
.valid_notification_uris
.contains(&Cow::from(scheme))
|| ctx.runtime.valid_notification_uris.contains(&uri))
{
num_valid += 1;
}
}
TestResult::Bool((num_valid == self.notification_uris.len()) ^ self.is_not)
}
}
impl TestNotifyMethodCapability {
pub(crate) fn exec(&self, ctx: &mut Context) -> TestResult {
let uri_ = ctx.eval_value(&self.notification_uri);
let uri = uri_.to_string();
if !ctx
.eval_value(&self.notification_capability)
.to_string()
.eq_ignore_ascii_case("online")
|| !validate_uri(uri.as_ref()).is_some_and(|scheme| {
ctx.runtime
.valid_notification_uris
.contains(&Cow::from(scheme))
|| ctx.runtime.valid_notification_uris.contains(&uri)
})
{
return TestResult::Bool(false ^ self.is_not);
}
if let MatchType::Count(rel_match) = &self.match_type {
for key in &self.key_list {
if rel_match.cmp(&Number::from(1.0), &ctx.eval_value(key).to_number()) {
return TestResult::Bool(true ^ self.is_not);
}
}
} else {
for pattern in &self.key_list {
let key = ctx.eval_value(pattern);
if match &self.match_type {
MatchType::Is => self.comparator.is(&"maybe", &key),
MatchType::Contains => {
self.comparator.contains("maybe", key.to_string().as_ref())
}
MatchType::Value(relation) => {
self.comparator.relational(relation, &"maybe", &key)
}
MatchType::Matches(_) => self.comparator.matches(
"maybe",
key.to_string().as_ref(),
0,
&mut Vec::new(),
),
MatchType::Regex(_) => {
self.comparator
.regex(pattern, &key, "maybe", 0, &mut Vec::new())
}
_ => false,
} {
return TestResult::Bool(true ^ self.is_not);
}
}
}
TestResult::Bool(false ^ self.is_not)
}
}
+21
View File
@@ -0,0 +1,21 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{Context, compiler::grammar::tests::test_size::TestSize};
use super::TestResult;
impl TestSize {
pub(crate) fn exec(&self, ctx: &Context) -> TestResult {
TestResult::Bool(
(if self.over {
ctx.message_size > self.limit
} else {
ctx.message_size < self.limit
}) ^ self.is_not,
)
}
}
+199
View File
@@ -0,0 +1,199 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
Context, SpamStatus, VirusStatus,
compiler::{
Number,
grammar::{
MatchType,
tests::test_spamtest::{TestSpamTest, TestVirusTest},
},
},
runtime::Variable,
};
use super::TestResult;
impl TestSpamTest {
pub(crate) fn exec(&self, ctx: &mut Context) -> TestResult {
let status = if self.percent {
ctx.spam_status.as_percentage()
} else {
ctx.spam_status.as_number()
};
let value = ctx.eval_value(&self.value);
let mut captured_values = Vec::new();
let result = match &self.match_type {
MatchType::Is => self.comparator.is(&status, &value),
MatchType::Contains => self
.comparator
.contains(status.to_string().as_ref(), value.to_string().as_ref()),
MatchType::Value(rel_match) => self.comparator.relational(rel_match, &status, &value),
MatchType::Matches(capture_positions) => self.comparator.matches(
status.to_string().as_ref(),
value.to_string().as_ref(),
*capture_positions,
&mut captured_values,
),
MatchType::Regex(capture_positions) => self.comparator.regex(
&self.value,
&value,
status.to_string().as_ref(),
*capture_positions,
&mut captured_values,
),
MatchType::Count(rel_match) => rel_match.cmp(
&Number::from(if matches!(&ctx.spam_status, SpamStatus::Unknown) {
0.0
} else {
1.1
}),
&value.to_number(),
),
MatchType::List => false,
};
if !captured_values.is_empty() {
ctx.set_match_variables(captured_values);
}
TestResult::Bool(result ^ self.is_not)
}
}
impl TestVirusTest {
pub(crate) fn exec(&self, ctx: &mut Context) -> TestResult {
let status = ctx.virus_status.as_number();
let value = ctx.eval_value(&self.value);
let mut captured_values = Vec::new();
let result = match &self.match_type {
MatchType::Is => self.comparator.is(&status, &value),
MatchType::Contains => self
.comparator
.contains(status.to_string().as_ref(), value.to_string().as_ref()),
MatchType::Value(rel_match) => self.comparator.relational(rel_match, &status, &value),
MatchType::Matches(capture_positions) => self.comparator.matches(
status.to_string().as_ref(),
value.to_string().as_ref(),
*capture_positions,
&mut captured_values,
),
MatchType::Regex(capture_positions) => self.comparator.regex(
&self.value,
&value,
status.to_string().as_ref(),
*capture_positions,
&mut captured_values,
),
MatchType::Count(rel_match) => rel_match.cmp(
&Number::from(if matches!(&ctx.virus_status, VirusStatus::Unknown) {
0.0
} else {
1.1
}),
&value.to_number(),
),
MatchType::List => false,
};
if !captured_values.is_empty() {
ctx.set_match_variables(captured_values);
}
TestResult::Bool(result ^ self.is_not)
}
}
impl SpamStatus {
pub fn from_number(number: u32) -> Self {
match number {
1 => SpamStatus::Ham,
2..=9 => SpamStatus::MaybeSpam(number as f64 / 10.0),
10 => SpamStatus::Spam,
_ => SpamStatus::Unknown,
}
}
pub(crate) fn as_number(&self) -> Variable {
Variable::Integer(match self {
SpamStatus::Unknown => 0,
SpamStatus::Ham => 1,
SpamStatus::MaybeSpam(pct) => ((pct * 10.0) as i64).clamp(2, 9),
SpamStatus::Spam => 10,
})
}
pub(crate) fn as_percentage(&self) -> Variable {
Variable::Integer(match self {
SpamStatus::Unknown | SpamStatus::Ham => 0,
SpamStatus::MaybeSpam(pct) => ((pct * 100.0).ceil() as i64).clamp(1, 100),
SpamStatus::Spam => 100,
})
}
}
impl VirusStatus {
pub fn from_number(number: u32) -> Self {
match number {
1 => VirusStatus::Clean,
2 => VirusStatus::Replaced,
3 => VirusStatus::Cured,
4 => VirusStatus::MaybeVirus,
5 => VirusStatus::Virus,
_ => VirusStatus::Unknown,
}
}
pub(crate) fn as_number(&self) -> Variable {
Variable::Integer(match self {
VirusStatus::Unknown => 0,
VirusStatus::Clean => 1,
VirusStatus::Replaced => 2,
VirusStatus::Cured => 3,
VirusStatus::MaybeVirus => 4,
VirusStatus::Virus => 5,
})
}
}
impl From<u32> for SpamStatus {
fn from(number: u32) -> Self {
SpamStatus::from_number(number)
}
}
impl From<i32> for SpamStatus {
fn from(number: i32) -> Self {
SpamStatus::from_number(number as u32)
}
}
impl From<usize> for SpamStatus {
fn from(number: usize) -> Self {
SpamStatus::from_number(number as u32)
}
}
impl From<u32> for VirusStatus {
fn from(number: u32) -> Self {
VirusStatus::from_number(number)
}
}
impl From<i32> for VirusStatus {
fn from(number: i32) -> Self {
VirusStatus::from_number(number as u32)
}
}
impl From<usize> for VirusStatus {
fn from(number: usize) -> Self {
VirusStatus::from_number(number as u32)
}
}
+106
View File
@@ -0,0 +1,106 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
Context, Event,
compiler::{
Number,
grammar::{MatchType, tests::test_string::TestString},
},
};
use super::TestResult;
impl TestString {
pub(crate) fn exec(&self, ctx: &mut Context, empty_is_null: bool) -> TestResult {
let mut result = false;
match &self.match_type {
MatchType::Count(match_type) => {
let num_items = self
.source
.iter()
.filter(|x| !ctx.eval_value(x).is_empty())
.count() as i64;
if !empty_is_null || num_items > 0 {
for key in &self.key_list {
if match_type
.cmp(&Number::from(num_items), &ctx.eval_value(key).to_number())
{
result = true;
break;
}
}
}
}
MatchType::List => {
let mut values = Vec::with_capacity(self.source.len());
for source in &self.source {
let value = ctx.eval_value(source).to_string().into_owned();
if !value.is_empty() && !values.iter().any(|v: &String| v.eq(&value)) {
values.push(value);
}
}
if !values.is_empty() {
return TestResult::Event {
event: Event::ListContains {
lists: ctx.eval_values_owned(&self.key_list),
values,
match_as: self.comparator.as_match(),
},
is_not: self.is_not,
};
}
}
_ => {
let mut captured_values = Vec::new();
let sources = ctx.eval_values(&self.source);
for pattern in &self.key_list {
let key = ctx.eval_value(pattern);
for source in &sources {
if !empty_is_null || !source.is_empty() {
result = match &self.match_type {
MatchType::Is => self.comparator.is(source, &key),
MatchType::Contains => self.comparator.contains(
source.to_string().as_ref(),
key.to_string().as_ref(),
),
MatchType::Value(relation) => {
self.comparator.relational(relation, source, &key)
}
MatchType::Matches(capture_positions) => self.comparator.matches(
source.to_string().as_ref(),
key.to_string().as_ref(),
*capture_positions,
&mut captured_values,
),
MatchType::Regex(capture_positions) => self.comparator.regex(
pattern,
&key,
source.to_string().as_ref(),
*capture_positions,
&mut captured_values,
),
_ => false,
};
if result {
break;
}
}
}
}
if !captured_values.is_empty() {
ctx.set_match_variables(captured_values);
}
}
}
TestResult::Bool(result ^ self.is_not)
}
}