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
@@ -0,0 +1,51 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::compiler::{
CompileError, Value,
grammar::{
instruction::{CompilerState, Instruction},
test::Test,
},
};
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(
any(test, feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
pub(crate) struct Convert {
pub from_media_type: Value,
pub to_media_type: Value,
pub transcoding_params: Vec<Value>,
pub is_not: bool,
}
impl CompilerState<'_> {
pub(crate) fn parse_test_convert(&mut self) -> Result<Test, CompileError> {
Ok(Test::Convert(Convert {
from_media_type: self.parse_string()?,
to_media_type: self.parse_string()?,
transcoding_params: self.parse_strings(false)?,
is_not: false,
}))
}
pub(crate) fn parse_convert(&mut self) -> Result<(), CompileError> {
let cmd = Instruction::Convert(Convert {
from_media_type: self.parse_string()?,
to_media_type: self.parse_string()?,
transcoding_params: self.parse_strings(false)?,
is_not: false,
});
self.instructions.push(cmd);
Ok(())
}
}
@@ -0,0 +1,212 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use mail_parser::HeaderName;
use crate::compiler::{
CompileError, ErrorType, Value,
grammar::{
Capability, Comparator,
instruction::{CompilerState, Instruction},
},
lexer::{Token, word::Word},
};
use crate::compiler::grammar::MatchType;
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(
any(test, feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
pub(crate) struct AddHeader {
pub last: bool,
pub field_name: Value,
pub value: Value,
}
/*
Usage: "deleteheader" [":index" <fieldno: number> [":last"]]
[COMPARATOR] [MATCH-TYPE]
<field-name: string>
[<value-patterns: string-list>]
*/
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(
any(test, feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
pub(crate) struct DeleteHeader {
pub index: Option<i32>,
pub comparator: Comparator,
pub match_type: MatchType,
pub field_name: Value,
pub value_patterns: Vec<Value>,
pub mime_anychild: bool,
}
impl CompilerState<'_> {
pub(crate) fn parse_addheader(&mut self) -> Result<(), CompileError> {
let mut field_name = None;
let value;
let mut last = false;
loop {
let token_info = self.tokens.unwrap_next()?;
match token_info.token {
Token::Tag(Word::Last) => {
self.validate_argument(1, None, token_info.line_num, token_info.line_pos)?;
last = true;
}
_ => {
let string = self.parse_string_token(token_info)?;
if field_name.is_none() {
if let Value::Text(header_name) = &string
&& HeaderName::parse(header_name.as_ref()).is_none()
{
return Err(self
.tokens
.unwrap_next()?
.custom(ErrorType::InvalidHeaderName));
}
field_name = string.into();
} else {
if matches!(
&string,
Value::Text(value) if value.len() > self.compiler.max_header_size
) {
return Err(self
.tokens
.unwrap_next()?
.custom(ErrorType::HeaderTooLong));
}
value = string;
break;
}
}
}
}
self.instructions.push(Instruction::AddHeader(AddHeader {
last,
field_name: field_name.unwrap(),
value,
}));
Ok(())
}
pub(crate) fn parse_deleteheader(&mut self) -> Result<(), CompileError> {
let field_name: Value;
let mut match_type = MatchType::Is;
let mut comparator = Comparator::AsciiCaseMap;
let mut index = None;
let mut index_last = false;
let mut mime = false;
let mut mime_anychild = false;
loop {
let token_info = self.tokens.unwrap_next()?;
match token_info.token {
Token::Tag(
word @ (Word::Is
| Word::Contains
| Word::Matches
| Word::Value
| Word::Count
| Word::Regex),
) => {
self.validate_argument(
1,
match word {
Word::Value | Word::Count => Capability::Relational.into(),
Word::Regex => Capability::Regex.into(),
Word::List => Capability::ExtLists.into(),
_ => None,
},
token_info.line_num,
token_info.line_pos,
)?;
match_type = self.parse_match_type(word)?;
}
Token::Tag(Word::Comparator) => {
self.validate_argument(2, None, token_info.line_num, token_info.line_pos)?;
comparator = self.parse_comparator()?;
}
Token::Tag(Word::Index) => {
self.validate_argument(3, None, token_info.line_num, token_info.line_pos)?;
index = (self.tokens.expect_number(u16::MAX as usize)? as i32).into();
}
Token::Tag(Word::Last) => {
self.validate_argument(4, None, token_info.line_num, token_info.line_pos)?;
index_last = true;
}
Token::Tag(Word::Mime) => {
self.validate_argument(
5,
Capability::Mime.into(),
token_info.line_num,
token_info.line_pos,
)?;
mime = true;
}
Token::Tag(Word::AnyChild) => {
self.validate_argument(
6,
Capability::Mime.into(),
token_info.line_num,
token_info.line_pos,
)?;
mime_anychild = true;
}
_ => {
field_name = self.parse_string_token(token_info)?;
if let Value::Text(header_name) = &field_name
&& HeaderName::parse(header_name.as_ref()).is_none()
{
return Err(self
.tokens
.unwrap_next()?
.custom(ErrorType::InvalidHeaderName));
}
break;
}
}
}
if !mime && mime_anychild {
return Err(self.tokens.unwrap_next()?.missing_tag(":mime"));
}
let cmd = Instruction::DeleteHeader(DeleteHeader {
index: if index_last { index.map(|i| -i) } else { index },
comparator,
match_type,
field_name,
value_patterns: if let Some(Ok(
Token::StringConstant(_) | Token::StringVariable(_) | Token::BracketOpen,
)) = self.tokens.peek().map(|r| r.map(|t| &t.token))
{
let mut key_list = self.parse_strings(false)?;
self.validate_match(&match_type, &mut key_list)?;
key_list
} else {
Vec::new()
},
mime_anychild,
});
self.instructions.push(cmd);
Ok(())
}
}
@@ -0,0 +1,108 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::compiler::{
CompileError, Value,
grammar::{
Capability,
instruction::{CompilerState, Instruction},
},
lexer::{Token, word::Word},
};
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(
any(test, feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
pub(crate) struct FileInto {
pub copy: bool,
pub create: bool,
pub folder: Value,
pub flags: Vec<Value>,
pub mailbox_id: Option<Value>,
pub special_use: Option<Value>,
}
impl CompilerState<'_> {
pub(crate) fn parse_fileinto(&mut self) -> Result<(), CompileError> {
let folder;
let mut copy = false;
let mut create = false;
let mut flags = Vec::new();
let mut mailbox_id = None;
let mut special_use = None;
loop {
let token_info = self.tokens.unwrap_next()?;
match token_info.token {
Token::Tag(Word::Copy) => {
self.validate_argument(
1,
Capability::Copy.into(),
token_info.line_num,
token_info.line_pos,
)?;
copy = true;
}
Token::Tag(Word::Create) => {
self.validate_argument(
2,
Capability::Mailbox.into(),
token_info.line_num,
token_info.line_pos,
)?;
create = true;
}
Token::Tag(Word::Flags) => {
self.validate_argument(
3,
Capability::Imap4Flags.into(),
token_info.line_num,
token_info.line_pos,
)?;
flags = self.parse_strings(false)?;
}
Token::Tag(Word::MailboxId) => {
self.validate_argument(
4,
Capability::Mailbox.into(),
token_info.line_num,
token_info.line_pos,
)?;
mailbox_id = self.parse_string()?.into();
}
Token::Tag(Word::SpecialUse) => {
self.validate_argument(
5,
Capability::SpecialUse.into(),
token_info.line_num,
token_info.line_pos,
)?;
special_use = self.parse_string()?.into();
}
_ => {
folder = self.parse_string_token(token_info)?;
break;
}
}
}
self.instructions.push(Instruction::FileInto(FileInto {
folder,
copy,
create,
flags,
mailbox_id,
special_use,
}));
Ok(())
}
}
@@ -0,0 +1,87 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::compiler::{
CompileError, ErrorType, Value, VariableType,
grammar::instruction::{CompilerState, Instruction},
lexer::{Token, word::Word},
};
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(
any(test, feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
pub(crate) struct EditFlags {
pub action: Action,
pub name: Option<VariableType>,
pub flags: Vec<Value>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(
any(test, feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
pub(crate) enum Action {
Set,
Add,
Remove,
}
impl CompilerState<'_> {
pub(crate) fn parse_flag_action(&mut self, word: Word) -> Result<(), CompileError> {
let token_info = self.tokens.unwrap_next()?;
let action = match word {
Word::SetFlag => Action::Set,
Word::AddFlag => Action::Add,
Word::RemoveFlag => Action::Remove,
_ => unreachable!(),
};
let instruction = Instruction::EditFlags(
match (
&token_info.token,
self.tokens.peek().map(|r| r.map(|t| &t.token)),
) {
(
Token::StringConstant(_),
Some(Ok(
Token::StringConstant(_) | Token::StringVariable(_) | Token::BracketOpen,
)),
) => EditFlags {
name: self.parse_variable_name(token_info, false)?.into(),
flags: self.parse_strings(false)?,
action,
},
(Token::BracketOpen, _)
| (
Token::StringConstant(_) | Token::StringVariable(_),
Some(Ok(Token::Semicolon)),
) => EditFlags {
name: None,
flags: self.parse_strings_token(token_info)?,
action,
},
_ => {
return Err(token_info.custom(ErrorType::InvalidArguments));
}
},
);
self.instructions.push(instruction);
Ok(())
}
}
@@ -0,0 +1,91 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::compiler::{
CompileError, Value,
grammar::instruction::{CompilerState, Instruction},
lexer::{Token, word::Word},
};
/*
include [LOCATION] [":once"] [":optional"] <value: string>
LOCATION = ":personal" / ":global"
*/
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(
any(test, feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
pub(crate) struct Include {
pub location: Location,
pub once: bool,
pub optional: bool,
pub value: Value,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(
any(test, feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
pub(crate) enum Location {
Personal,
Global,
}
impl CompilerState<'_> {
pub(crate) fn parse_include(&mut self) -> Result<(), CompileError> {
let value;
let mut once = false;
let mut optional = false;
let mut location = Location::Personal;
loop {
let token_info = self.tokens.unwrap_next()?;
match token_info.token {
Token::Tag(Word::Once) => {
self.validate_argument(1, None, token_info.line_num, token_info.line_pos)?;
once = true;
}
Token::Tag(Word::Optional) => {
self.validate_argument(2, None, token_info.line_num, token_info.line_pos)?;
optional = true;
}
Token::Tag(Word::Personal) => {
self.validate_argument(3, None, token_info.line_num, token_info.line_pos)?;
location = Location::Personal;
}
Token::Tag(Word::Global) => {
self.validate_argument(3, None, token_info.line_num, token_info.line_pos)?;
location = Location::Global;
}
_ => {
value = self.parse_string_token(token_info)?;
break;
}
}
}
self.instructions.push(Instruction::Include(Include {
location,
once,
optional,
value,
}));
Ok(())
}
}
@@ -0,0 +1,49 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::compiler::{
CompileError, Value,
grammar::{
Capability,
instruction::{CompilerState, Instruction},
},
lexer::{Token, word::Word},
};
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(
any(test, feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
pub(crate) struct Keep {
pub flags: Vec<Value>,
}
impl CompilerState<'_> {
pub(crate) fn parse_keep(&mut self) -> Result<(), CompileError> {
let cmd = Instruction::Keep(Keep {
flags: match self.tokens.peek().map(|r| r.map(|t| &t.token)) {
Some(Ok(Token::Tag(Word::Flags))) => {
let token_info = self.tokens.next().unwrap().unwrap();
self.validate_argument(
0,
Capability::Imap4Flags.into(),
token_info.line_num,
token_info.line_pos,
)?;
self.parse_strings(false)?
}
_ => Vec::new(),
},
});
self.instructions.push(cmd);
Ok(())
}
}
@@ -0,0 +1,225 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::compiler::{
CompileError, Value, VariableType,
grammar::instruction::{CompilerState, Instruction},
lexer::{Token, word::Word},
};
use super::action_set::Modifier;
#[derive(Debug, Eq, PartialEq, Clone)]
#[cfg_attr(
any(test, feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
pub(crate) struct ForEveryPart {
pub jz_pos: usize,
}
#[derive(Debug, Eq, PartialEq, Clone)]
#[cfg_attr(
any(test, feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
pub(crate) struct Replace {
pub subject: Option<Value>,
pub from: Option<Value>,
pub replacement: Value,
pub mime: bool,
}
#[derive(Debug, Eq, PartialEq, Clone)]
#[cfg_attr(
any(test, feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
pub(crate) struct Enclose {
pub subject: Option<Value>,
pub headers: Vec<Value>,
pub value: Value,
}
#[derive(Debug, Eq, PartialEq, Clone)]
#[cfg_attr(
any(test, feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
pub(crate) struct ExtractText {
pub modifiers: Vec<Modifier>,
pub first: Option<usize>,
pub name: VariableType,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(
any(test, feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
pub(crate) enum MimeOpts<T> {
Type,
Subtype,
ContentType,
Param(Vec<T>),
None,
}
impl CompilerState<'_> {
pub(crate) fn parse_replace(&mut self) -> Result<(), CompileError> {
let mut subject = None;
let mut from = None;
let replacement;
let mut mime = false;
loop {
let token_info = self.tokens.unwrap_next()?;
match token_info.token {
Token::Tag(Word::Mime) => {
self.validate_argument(1, None, token_info.line_num, token_info.line_pos)?;
mime = true;
}
Token::Tag(Word::Subject) => {
self.validate_argument(2, None, token_info.line_num, token_info.line_pos)?;
subject = self.parse_string()?.into();
}
Token::Tag(Word::From) => {
self.validate_argument(3, None, token_info.line_num, token_info.line_pos)?;
from = self.parse_string()?.into();
}
_ => {
replacement = self.parse_string_token(token_info)?;
break;
}
}
}
self.instructions.push(Instruction::Replace(Replace {
subject,
from,
replacement,
mime,
}));
Ok(())
}
pub(crate) fn parse_enclose(&mut self) -> Result<(), CompileError> {
let mut subject = None;
let mut headers = Vec::new();
let value;
loop {
let token_info = self.tokens.unwrap_next()?;
match token_info.token {
Token::Tag(Word::Subject) => {
self.validate_argument(1, None, token_info.line_num, token_info.line_pos)?;
subject = self.parse_string()?.into();
}
Token::Tag(Word::Headers) => {
self.validate_argument(2, None, token_info.line_num, token_info.line_pos)?;
headers = self.parse_strings(false)?;
}
_ => {
value = self.parse_string_token(token_info)?;
break;
}
}
}
self.instructions.push(Instruction::Enclose(Enclose {
subject,
headers,
value,
}));
Ok(())
}
pub(crate) fn parse_extracttext(&mut self) -> Result<(), CompileError> {
let mut modifiers = Vec::new();
let mut first = None;
let name;
let mut is_local = false;
loop {
let token_info = self.tokens.unwrap_next()?;
match token_info.token {
Token::Tag(Word::First) => {
self.validate_argument(1, None, token_info.line_num, token_info.line_pos)?;
first = self.tokens.expect_number(usize::MAX)?.into();
}
Token::Tag(
word @ (Word::Lower
| Word::Upper
| Word::LowerFirst
| Word::UpperFirst
| Word::QuoteWildcard
| Word::QuoteRegex
| Word::Length),
) => {
let modifier = word.into();
if !modifiers.contains(&modifier) {
modifiers.push(modifier);
}
}
Token::Tag(Word::Replace) => {
let find = self.tokens.unwrap_next()?;
let replace = self.tokens.unwrap_next()?;
modifiers.push(Modifier::Replace {
find: self.parse_string_token(find)?,
replace: self.parse_string_token(replace)?,
});
}
Token::Tag(Word::Local) => {
is_local = true;
}
_ => {
name = self.parse_variable_name(token_info, is_local)?;
break;
}
}
}
modifiers.sort_unstable_by_key(|m| std::cmp::Reverse(m.order()));
self.instructions
.push(Instruction::ExtractText(ExtractText {
modifiers,
first,
name,
}));
Ok(())
}
pub(crate) fn parse_mimeopts(&mut self, opts: Word) -> Result<MimeOpts<Value>, CompileError> {
Ok(match opts {
Word::Type => MimeOpts::Type,
Word::Subtype => MimeOpts::Subtype,
Word::ContentType => MimeOpts::ContentType,
Word::Param => MimeOpts::Param(self.parse_strings(false)?),
_ => MimeOpts::None,
})
}
}
@@ -0,0 +1,180 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
FileCarbonCopy,
compiler::{
CompileError, ErrorType, Value,
grammar::{
Capability,
instruction::{CompilerState, Instruction, MapLocalVars},
},
lexer::{Token, word::Word},
},
runtime::actions::action_notify::{validate_from, validate_uri},
};
/*
notify [":from" string]
[":importance" <"1" / "2" / "3">]
[":options" string-list]
[":message" string]
<method: string>
*/
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(
any(test, feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
pub(crate) struct Notify {
pub from: Option<Value>,
pub importance: Option<Value>,
pub options: Vec<Value>,
pub message: Option<Value>,
pub fcc: Option<FileCarbonCopy<Value>>,
pub method: Value,
}
impl CompilerState<'_> {
pub(crate) fn parse_notify(&mut self) -> Result<(), CompileError> {
let method;
let mut from = None;
let mut importance = None;
let mut message = None;
let mut options = Vec::new();
let mut fcc = None;
let mut create = false;
let mut flags = Vec::new();
let mut special_use = None;
let mut mailbox_id = None;
loop {
let token_info = self.tokens.unwrap_next()?;
match token_info.token {
Token::Tag(Word::From) => {
self.validate_argument(1, None, token_info.line_num, token_info.line_pos)?;
let address = self.parse_string()?;
if let Value::Text(address) = &address
&& (address.is_empty() || !validate_from(address))
{
return Err(token_info.custom(ErrorType::InvalidAddress));
}
from = address.into();
}
Token::Tag(Word::Message) => {
self.validate_argument(2, None, token_info.line_num, token_info.line_pos)?;
message = self.parse_string()?.into();
}
Token::Tag(Word::Importance) => {
self.validate_argument(3, None, token_info.line_num, token_info.line_pos)?;
importance = self.parse_string()?.into();
}
Token::Tag(Word::Options) => {
self.validate_argument(4, None, token_info.line_num, token_info.line_pos)?;
options = self.parse_strings(false)?;
}
Token::Tag(Word::Create) => {
self.validate_argument(
5,
Capability::Mailbox.into(),
token_info.line_num,
token_info.line_pos,
)?;
create = true;
}
Token::Tag(Word::SpecialUse) => {
self.validate_argument(
6,
Capability::SpecialUse.into(),
token_info.line_num,
token_info.line_pos,
)?;
special_use = self.parse_string()?.into();
}
Token::Tag(Word::MailboxId) => {
self.validate_argument(
7,
Capability::MailboxId.into(),
token_info.line_num,
token_info.line_pos,
)?;
mailbox_id = self.parse_string()?.into();
}
Token::Tag(Word::Fcc) => {
self.validate_argument(
8,
Capability::Fcc.into(),
token_info.line_num,
token_info.line_pos,
)?;
fcc = self.parse_string()?.into();
}
Token::Tag(Word::Flags) => {
self.validate_argument(
9,
Capability::Imap4Flags.into(),
token_info.line_num,
token_info.line_pos,
)?;
flags = self.parse_strings(false)?;
}
_ => {
if let Token::StringConstant(uri) = &token_info.token
&& validate_uri(uri.to_string().as_ref()).is_none()
{
return Err(token_info.custom(ErrorType::InvalidURI));
}
method = self.parse_string_token(token_info)?;
break;
}
}
}
if fcc.is_none()
&& (create || !flags.is_empty() || special_use.is_some() || mailbox_id.is_some())
{
return Err(self.tokens.unwrap_next()?.missing_tag(":fcc"));
}
self.instructions.push(Instruction::Notify(Notify {
method,
from,
importance,
options,
message,
fcc: if let Some(fcc) = fcc {
FileCarbonCopy {
mailbox: fcc,
create,
flags,
special_use,
mailbox_id,
}
.into()
} else {
None
},
}));
Ok(())
}
}
impl MapLocalVars for FileCarbonCopy<Value> {
fn map_local_vars(&mut self, last_id: usize) {
self.mailbox.map_local_vars(last_id);
self.mailbox_id.map_local_vars(last_id);
self.flags.map_local_vars(last_id);
self.special_use.map_local_vars(last_id);
}
}
@@ -0,0 +1,288 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::compiler::{
CompileError, Value,
grammar::{
Capability,
instruction::{CompilerState, Instruction, MapLocalVars},
},
lexer::{Token, word::Word},
};
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(
any(test, feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
pub(crate) struct Redirect {
pub copy: bool,
pub address: Value,
pub notify: Notify,
pub return_of_content: Ret,
pub by_time: ByTime<Value>,
pub list: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(
any(test, feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
pub enum NotifyItem {
Success,
Failure,
Delay,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(
any(test, feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
pub enum Notify {
Never,
Items(Vec<NotifyItem>),
Default,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(
any(test, feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
pub enum Ret {
Full,
Hdrs,
Default,
}
/*
Usage: redirect [:bytimerelative <rlimit: number> /
:bytimeabsolute <alimit:string>
[:bymode "notify"|"return"] [:bytrace]]
<address: string>
*/
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(
any(test, feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
pub enum ByTime<T> {
Relative {
rlimit: u64,
mode: ByMode,
trace: bool,
},
Absolute {
alimit: T,
mode: ByMode,
trace: bool,
},
None,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(
any(test, feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
pub enum ByMode {
Notify,
Return,
Default,
}
impl CompilerState<'_> {
pub(crate) fn parse_redirect(&mut self) -> Result<(), CompileError> {
let address;
let mut copy = false;
let mut ret = Ret::Default;
let mut notify = Notify::Default;
let mut list = false;
let mut by_mode = ByMode::Default;
let mut by_trace = false;
let mut by_rlimit = None;
let mut by_alimit = None;
loop {
let token_info = self.tokens.unwrap_next()?;
match token_info.token {
Token::Tag(Word::Copy) => {
self.validate_argument(
1,
Capability::Copy.into(),
token_info.line_num,
token_info.line_pos,
)?;
copy = true;
}
Token::Tag(Word::List) => {
self.validate_argument(
2,
Capability::ExtLists.into(),
token_info.line_num,
token_info.line_pos,
)?;
list = true;
}
Token::Tag(Word::ByTrace) => {
self.validate_argument(
3,
Capability::RedirectDeliverBy.into(),
token_info.line_num,
token_info.line_pos,
)?;
by_trace = true;
}
Token::Tag(Word::ByMode) => {
self.validate_argument(
4,
Capability::RedirectDeliverBy.into(),
token_info.line_num,
token_info.line_pos,
)?;
let by_mode_ = self.tokens.expect_static_string()?;
if by_mode_.eq_ignore_ascii_case("notify") {
by_mode = ByMode::Notify;
} else if by_mode_.eq_ignore_ascii_case("return") {
by_mode = ByMode::Return;
} else {
return Err(token_info.expected("\"notify\" or \"return\""));
}
}
Token::Tag(Word::ByTimeRelative) => {
self.validate_argument(
5,
Capability::RedirectDeliverBy.into(),
token_info.line_num,
token_info.line_pos,
)?;
by_rlimit = (self.tokens.expect_number(u64::MAX as usize)? as u64).into();
}
Token::Tag(Word::ByTimeAbsolute) => {
self.validate_argument(
5,
Capability::RedirectDeliverBy.into(),
token_info.line_num,
token_info.line_pos,
)?;
by_alimit = self.parse_string()?.into();
}
Token::Tag(Word::Ret) => {
self.validate_argument(
6,
Capability::RedirectDsn.into(),
token_info.line_num,
token_info.line_pos,
)?;
let ret_ = self.tokens.expect_static_string()?;
if ret_.eq_ignore_ascii_case("full") {
ret = Ret::Full;
} else if ret_.eq_ignore_ascii_case("hdrs") {
ret = Ret::Hdrs;
} else {
return Err(token_info.expected("\"FULL\" or \"HDRS\""));
}
}
Token::Tag(Word::Notify) => {
self.validate_argument(
7,
Capability::RedirectDsn.into(),
token_info.line_num,
token_info.line_pos,
)?;
let notify_ = self.tokens.expect_static_string()?;
if notify_.eq_ignore_ascii_case("never") {
notify = Notify::Never;
} else {
let mut items = Vec::new();
for item in notify_.split(',') {
let item = item.trim();
if item.eq_ignore_ascii_case("success") {
items.push(NotifyItem::Success);
} else if item.eq_ignore_ascii_case("failure") {
items.push(NotifyItem::Failure);
} else if item.eq_ignore_ascii_case("delay") {
items.push(NotifyItem::Delay);
}
}
if !items.is_empty() {
notify = Notify::Items(items);
} else {
return Err(
token_info.expected("\"NEVER\" or \"SUCCESS, FAILURE, DELAY, ..\"")
);
}
}
}
_ => {
address = self.parse_string_token(token_info)?;
break;
}
}
}
self.instructions.push(Instruction::Redirect(Redirect {
address,
copy,
notify,
return_of_content: ret,
by_time: if let Some(alimit) = by_alimit {
ByTime::Absolute {
alimit,
mode: by_mode,
trace: by_trace,
}
} else if let Some(rlimit) = by_rlimit {
ByTime::Relative {
rlimit,
mode: by_mode,
trace: by_trace,
}
} else {
ByTime::None
},
list,
}));
Ok(())
}
}
impl MapLocalVars for ByTime<Value> {
fn map_local_vars(&mut self, last_id: usize) {
if let ByTime::Absolute { alimit, .. } = self {
alimit.map_local_vars(last_id)
}
}
}
@@ -0,0 +1,35 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::compiler::{
CompileError, Value,
grammar::instruction::{CompilerState, Instruction},
};
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(
any(test, feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
pub(crate) struct Reject {
pub ereject: bool,
pub reason: Value,
}
impl CompilerState<'_> {
pub(crate) fn parse_reject(&mut self, ereject: bool) -> Result<(), CompileError> {
let cmd = Instruction::Reject(Reject {
ereject,
reason: self.parse_string()?,
});
self.instructions.push(cmd);
Ok(())
}
}
@@ -0,0 +1,93 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::compiler::{
CompileError,
grammar::{
Capability,
instruction::{CompilerState, Instruction},
},
lexer::Token,
};
impl CompilerState<'_> {
fn add_capability(&mut self, capabilities: &mut Vec<Capability>, capability: Capability) {
if !self.has_capability(&capability) {
let parent_capability = if matches!(&capability, Capability::SpamTestPlus) {
Some(Capability::SpamTest)
} else {
None
};
capabilities.push(capability.clone());
self.block.capabilities.insert(capability);
if let Some(capability) = parent_capability
&& !self.has_capability(&capability)
{
capabilities.push(capability.clone());
self.block.capabilities.insert(capability);
}
}
}
pub(crate) fn parse_require(&mut self) -> Result<(), CompileError> {
let mut capabilities = Vec::new();
let token_info = self.tokens.unwrap_next()?;
match token_info.token {
Token::BracketOpen => loop {
let token_info = self.tokens.unwrap_next()?;
match token_info.token {
Token::StringConstant(value) => {
self.add_capability(
&mut capabilities,
Capability::parse(value.to_string().as_ref()),
);
let token_info = self.tokens.unwrap_next()?;
match token_info.token {
Token::Comma => (),
Token::BracketClose => break,
_ => {
return Err(token_info.expected("']' or ','"));
}
}
}
_ => {
return Err(token_info.expected("string"));
}
}
},
Token::StringConstant(value) => {
self.add_capability(
&mut capabilities,
Capability::parse(value.to_string().as_ref()),
);
}
_ => {
return Err(token_info.expected("'[' or string"));
}
}
if !capabilities.is_empty() {
if self.block.require_pos == usize::MAX {
self.block.require_pos = self.instructions.len();
self.instructions.push(Instruction::Require(capabilities));
} else if let Some(Instruction::Require(capabilties)) =
self.instructions.get_mut(self.block.require_pos)
{
capabilties.extend(capabilities)
} else {
#[cfg(test)]
panic!(
"Invalid require instruction position {}.",
self.block.require_pos
)
}
}
Ok(())
}
}
@@ -0,0 +1,207 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
Envelope,
compiler::{
CompileError, ErrorType, Value, VariableType,
grammar::{
expr::Expression,
instruction::{CompilerState, Instruction},
},
lexer::{Token, tokenizer::TokenInfo, word::Word},
},
};
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(
any(test, feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
#[allow(clippy::large_enum_variant)]
pub(crate) enum Modifier {
Lower,
Upper,
LowerFirst,
UpperFirst,
QuoteWildcard,
QuoteRegex,
EncodeUrl,
Length,
Replace { find: Value, replace: Value },
}
impl Modifier {
pub fn order(&self) -> usize {
match self {
Modifier::Lower => 41,
Modifier::Upper => 40,
Modifier::LowerFirst => 31,
Modifier::UpperFirst => 30,
Modifier::QuoteWildcard => 20,
Modifier::QuoteRegex => 21,
Modifier::EncodeUrl => 15,
Modifier::Length => 10,
Modifier::Replace { .. } => 40,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(
any(test, feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
pub(crate) struct Set {
pub modifiers: Vec<Modifier>,
pub name: VariableType,
pub value: Value,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(
any(test, feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
pub(crate) struct Let {
pub name: VariableType,
pub expr: Vec<Expression>,
}
impl CompilerState<'_> {
pub(crate) fn parse_set(&mut self) -> Result<(), CompileError> {
let mut modifiers = Vec::new();
let mut name = None;
let mut is_local = false;
let value;
loop {
let token_info = self.tokens.unwrap_next()?;
match token_info.token {
Token::Tag(
word @ (Word::Lower
| Word::Upper
| Word::LowerFirst
| Word::UpperFirst
| Word::QuoteWildcard
| Word::QuoteRegex
| Word::Length
| Word::EncodeUrl),
) => {
let modifier = word.into();
if !modifiers.contains(&modifier) {
modifiers.push(modifier);
}
}
Token::Tag(Word::Replace) => {
let find = self.tokens.unwrap_next()?;
let replace = self.tokens.unwrap_next()?;
modifiers.push(Modifier::Replace {
find: self.parse_string_token(find)?,
replace: self.parse_string_token(replace)?,
});
}
Token::Tag(Word::Local) => {
is_local = true;
}
_ => {
if name.is_none() {
name = self.parse_variable_name(token_info, is_local)?.into();
} else {
value = self.parse_string_token(token_info)?;
break;
}
}
}
}
modifiers.sort_unstable_by_key(|m| std::cmp::Reverse(m.order()));
self.instructions.push(Instruction::Set(Set {
modifiers,
name: name.unwrap(),
value,
}));
Ok(())
}
pub(crate) fn parse_let(&mut self) -> Result<(), CompileError> {
let name = self.tokens.unwrap_next()?;
let name = self.parse_variable_name(name, false)?;
let expr = self.parse_expr()?;
self.instructions.push(Instruction::Let(Let { name, expr }));
Ok(())
}
pub(crate) fn parse_variable_name(
&mut self,
token_info: TokenInfo,
register_as_local: bool,
) -> Result<VariableType, CompileError> {
match token_info.token {
Token::StringConstant(value) => self
.register_variable(value.into_string(), register_as_local)
.map_err(|error_type| CompileError {
line_num: token_info.line_num,
line_pos: token_info.line_pos,
error_type,
}),
_ => Err(token_info.custom(ErrorType::ExpectedConstantString)),
}
}
pub(crate) fn register_variable(
&mut self,
name: String,
register_as_local: bool,
) -> Result<VariableType, ErrorType> {
let name = name.to_lowercase();
if let Some((namespace, part)) = name.split_once('.') {
match namespace {
"global" | "t" => Ok(VariableType::Global(part.to_string())),
"envelope" => Envelope::try_from(part)
.map(VariableType::Envelope)
.map_err(|_| ErrorType::InvalidNamespace(namespace.to_string())),
_ => Err(ErrorType::InvalidNamespace(namespace.to_string())),
}
} else {
Ok(if !self.is_var_global(&name) {
VariableType::Local(self.register_local_var(name, register_as_local))
} else {
VariableType::Global(name)
})
}
}
}
impl From<Word> for Modifier {
fn from(word: Word) -> Self {
match word {
Word::Lower => Modifier::Lower,
Word::Upper => Modifier::Upper,
Word::LowerFirst => Modifier::LowerFirst,
Word::UpperFirst => Modifier::UpperFirst,
Word::QuoteWildcard => Modifier::QuoteWildcard,
Word::QuoteRegex => Modifier::QuoteRegex,
Word::Length => Modifier::Length,
Word::EncodeUrl => Modifier::EncodeUrl,
_ => unreachable!(),
}
}
}
@@ -0,0 +1,240 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
FileCarbonCopy,
compiler::{
CompileError, Value,
grammar::{
Capability,
instruction::{CompilerState, Instruction},
test::Test,
},
lexer::{Token, word::Word},
},
};
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(
any(test, feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
pub(crate) struct Vacation {
pub subject: Option<Value>,
pub from: Option<Value>,
pub mime: bool,
pub fcc: Option<FileCarbonCopy<Value>>,
pub reason: Value,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(
any(test, feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
pub(crate) struct TestVacation {
pub addresses: Vec<Value>,
pub period: Period,
pub handle: Option<Value>,
pub reason: Value,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(
any(test, feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
pub(crate) enum Period {
Days(u64),
Seconds(u64),
Default,
}
/*
vacation [":days" number] [":subject" string]
[":from" string] [":addresses" string-list]
[":mime"] [":handle" string] <reason: string>
vacation [FCC]
[":days" number | ":seconds" number]
[":subject" string]
[":from" string]
[":addresses" string-list]
[":mime"]
[":handle" string]
<reason: string>
":flags" <list-of-flags: string-list>
FCC = ":fcc" string *FCC-OPTS
; per Section 2.6.2 of RFC 5228,
; the tagged arguments in FCC may appear in any order
FCC-OPTS = CREATE / IMAP-FLAGS / SPECIAL-USE
; each option MUST NOT appear more than once
CREATE = ":create"
IMAP-FLAGS = ":flags" string-list
SPECIAL-USE = ":specialuse" string
*/
impl CompilerState<'_> {
pub(crate) fn parse_vacation(&mut self) -> Result<(), CompileError> {
let mut period = Period::Default;
let mut subject = None;
let mut from = None;
let mut handle = None;
let mut addresses = Vec::new();
let mut mime = false;
let reason;
let mut fcc = None;
let mut create = false;
let mut flags = Vec::new();
let mut special_use = None;
let mut mailbox_id = None;
loop {
let token_info = self.tokens.unwrap_next()?;
match token_info.token {
Token::Tag(Word::Mime) => {
self.validate_argument(1, None, token_info.line_num, token_info.line_pos)?;
mime = true;
}
Token::Tag(Word::Create) => {
self.validate_argument(
2,
Capability::Mailbox.into(),
token_info.line_num,
token_info.line_pos,
)?;
create = true;
}
Token::Tag(Word::Days) => {
self.validate_argument(3, None, token_info.line_num, token_info.line_pos)?;
period = Period::Days(self.tokens.expect_number(u64::MAX as usize)? as u64);
}
Token::Tag(Word::Seconds) => {
self.validate_argument(
3,
Capability::VacationSeconds.into(),
token_info.line_num,
token_info.line_pos,
)?;
period = Period::Seconds(self.tokens.expect_number(u64::MAX as usize)? as u64);
}
Token::Tag(Word::Subject) => {
self.validate_argument(4, None, token_info.line_num, token_info.line_pos)?;
subject = self.parse_string()?.into();
}
Token::Tag(Word::From) => {
self.validate_argument(5, None, token_info.line_num, token_info.line_pos)?;
from = self.parse_string()?.into();
}
Token::Tag(Word::Handle) => {
self.validate_argument(6, None, token_info.line_num, token_info.line_pos)?;
handle = self.parse_string()?.into();
}
Token::Tag(Word::SpecialUse) => {
self.validate_argument(
7,
Capability::SpecialUse.into(),
token_info.line_num,
token_info.line_pos,
)?;
special_use = self.parse_string()?.into();
}
Token::Tag(Word::MailboxId) => {
self.validate_argument(
8,
Capability::MailboxId.into(),
token_info.line_num,
token_info.line_pos,
)?;
mailbox_id = self.parse_string()?.into();
}
Token::Tag(Word::Fcc) => {
self.validate_argument(
9,
Capability::Fcc.into(),
token_info.line_num,
token_info.line_pos,
)?;
fcc = self.parse_string()?.into();
}
Token::Tag(Word::Flags) => {
self.validate_argument(
10,
Capability::Imap4Flags.into(),
token_info.line_num,
token_info.line_pos,
)?;
flags = self.parse_strings(false)?;
}
Token::Tag(Word::Addresses) => {
self.validate_argument(11, None, token_info.line_num, token_info.line_pos)?;
addresses = self.parse_strings(false)?;
}
_ => {
reason = self.parse_string_token(token_info)?;
break;
}
}
}
if fcc.is_none()
&& (create || !flags.is_empty() || special_use.is_some() || mailbox_id.is_some())
{
return Err(self.tokens.unwrap_next()?.missing_tag(":fcc"));
}
self.instructions
.push(Instruction::Test(Test::Vacation(TestVacation {
period,
handle,
reason: reason.clone(),
addresses,
})));
self.instructions
.push(Instruction::Jz(self.instructions.len() + 2));
self.instructions.push(Instruction::Vacation(Vacation {
reason,
subject,
from,
mime,
fcc: if let Some(fcc) = fcc {
FileCarbonCopy {
mailbox: fcc,
create,
flags,
special_use,
mailbox_id,
}
.into()
} else {
None
},
}));
Ok(())
}
}
+19
View File
@@ -0,0 +1,19 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
pub mod action_convert;
pub mod action_editheader;
pub mod action_fileinto;
pub mod action_flags;
pub mod action_include;
pub mod action_keep;
pub mod action_mime;
pub mod action_notify;
pub mod action_redirect;
pub mod action_reject;
pub mod action_require;
pub mod action_set;
pub mod action_vacation;
+124
View File
@@ -0,0 +1,124 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::sync::Arc;
use crate::compiler::{Number, VariableType};
pub mod parser;
pub mod tokenizer;
#[derive(Debug, PartialEq, Eq, Clone)]
#[cfg_attr(
any(test, feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
pub(crate) enum Expression {
Variable(VariableType),
Constant(Constant),
BinaryOperator(BinaryOperator),
UnaryOperator(UnaryOperator),
JmpIf { val: bool, pos: u32 },
Function { id: u32, num_args: u32 },
ArrayAccess,
ArrayBuild(u32),
}
#[derive(Debug, PartialEq, Clone)]
#[cfg_attr(
any(test, feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
pub(crate) enum Constant {
Integer(i64),
Float(f64),
String(Arc<String>),
}
impl Eq for Constant {}
impl From<Number> for Constant {
fn from(value: Number) -> Self {
match value {
Number::Integer(i) => Constant::Integer(i),
Number::Float(f) => Constant::Float(f),
}
}
}
impl From<String> for Constant {
fn from(value: String) -> Self {
Constant::String(value.into())
}
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
#[cfg_attr(
any(test, feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
pub(crate) enum BinaryOperator {
Add,
Subtract,
Multiply,
Divide,
And,
Or,
Xor,
Eq,
Ne,
Lt,
Le,
Gt,
Ge,
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
#[cfg_attr(
any(test, feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
pub(crate) enum UnaryOperator {
Not,
Minus,
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub(crate) enum Token {
Variable(VariableType),
Function {
name: String,
id: u32,
num_args: u32,
},
Number(Number),
String(String),
BinaryOperator(BinaryOperator),
UnaryOperator(UnaryOperator),
OpenParen,
CloseParen,
OpenBracket,
CloseBracket,
Comma,
}
+257
View File
@@ -0,0 +1,257 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::{BinaryOperator, Expression, Token, tokenizer::Tokenizer};
pub(crate) struct ExpressionParser<'x, F>
where
F: Fn(&str, bool) -> Result<Token, String>,
{
pub(crate) tokenizer: Tokenizer<'x, F>,
pub(crate) output: Vec<Expression>,
operator_stack: Vec<(Token, Option<usize>)>,
arg_count: Vec<i32>,
}
pub(crate) const ID_ARRAY_ACCESS: u32 = u32::MAX;
pub(crate) const ID_ARRAY_BUILD: u32 = u32::MAX - 1;
pub(crate) const ID_EXTERNAL: u32 = u32::MAX - 2;
impl<'x, F> ExpressionParser<'x, F>
where
F: Fn(&str, bool) -> Result<Token, String>,
{
pub fn from_tokenizer(tokenizer: Tokenizer<'x, F>) -> Self {
Self {
tokenizer,
output: Vec::new(),
operator_stack: Vec::new(),
arg_count: Vec::new(),
}
}
pub fn parse(mut self) -> Result<Self, String> {
let mut last_is_var_or_fnc = false;
while let Some(token) = self.tokenizer.next()? {
let mut is_var_or_fnc = false;
match token {
Token::Variable(v) => {
self.inc_arg_count();
is_var_or_fnc = true;
self.output.push(Expression::Variable(v))
}
Token::Number(n) => {
self.inc_arg_count();
self.output.push(Expression::Constant(n.into()))
}
Token::String(s) => {
self.inc_arg_count();
self.output.push(Expression::Constant(s.into()))
}
Token::UnaryOperator(uop) => {
self.operator_stack.push((Token::UnaryOperator(uop), None))
}
Token::OpenParen => self.operator_stack.push((token, None)),
Token::CloseParen | Token::CloseBracket => {
let expect_token = if matches!(token, Token::CloseParen) {
Token::OpenParen
} else {
Token::OpenBracket
};
loop {
match self.operator_stack.pop() {
Some((t, _)) if t == expect_token => {
break;
}
Some((Token::BinaryOperator(bop), jmp_pos)) => {
self.update_jmp_pos(jmp_pos);
self.output.push(Expression::BinaryOperator(bop))
}
Some((Token::UnaryOperator(uop), _)) => {
self.output.push(Expression::UnaryOperator(uop))
}
_ => return Err("Mismatched parentheses".to_string()),
}
}
if let Some((Token::Function { id, num_args, name }, _)) =
self.operator_stack.last()
{
let got_args = self.arg_count.pop().unwrap();
if got_args != *num_args as i32 {
return Err(if *id != u32::MAX {
format!(
"Expression function {:?} expected {} arguments, got {}",
name, num_args, got_args
)
} else {
"Missing array index".to_string()
});
}
let expr = match *id {
ID_ARRAY_ACCESS => Expression::ArrayAccess,
ID_ARRAY_BUILD => Expression::ArrayBuild(*num_args),
id => Expression::Function {
id,
num_args: *num_args,
},
};
self.operator_stack.pop();
self.output.push(expr);
}
is_var_or_fnc = true;
}
Token::BinaryOperator(bop) => {
self.dec_arg_count();
while let Some((top_token, prev_jmp_pos)) = self.operator_stack.last() {
match top_token {
Token::BinaryOperator(top_bop) => {
if bop.precedence() <= top_bop.precedence() {
let top_bop = *top_bop;
let jmp_pos = *prev_jmp_pos;
self.update_jmp_pos(jmp_pos);
self.operator_stack.pop();
self.output.push(Expression::BinaryOperator(top_bop));
} else {
break;
}
}
Token::UnaryOperator(top_uop) => {
let top_uop = *top_uop;
self.operator_stack.pop();
self.output.push(Expression::UnaryOperator(top_uop));
}
_ => break,
}
}
// Add jump instruction for short-circuiting
let jmp_pos = match bop {
BinaryOperator::And => {
self.output.push(Expression::JmpIf { val: false, pos: 0 });
Some(self.output.len() - 1)
}
BinaryOperator::Or => {
self.output.push(Expression::JmpIf { val: true, pos: 0 });
Some(self.output.len() - 1)
}
_ => None,
};
self.operator_stack
.push((Token::BinaryOperator(bop), jmp_pos));
}
Token::Function { id, name, num_args } => {
self.inc_arg_count();
self.arg_count.push(0);
self.operator_stack
.push((Token::Function { id, name, num_args }, None))
}
Token::OpenBracket => {
// Array functions
let (id, num_args, arg_count) = if last_is_var_or_fnc {
(ID_ARRAY_ACCESS, 2, 1)
} else {
self.inc_arg_count();
(ID_ARRAY_BUILD, 0, 0)
};
self.arg_count.push(arg_count);
self.operator_stack.push((
Token::Function {
id,
name: String::from("array"),
num_args,
},
None,
));
self.operator_stack.push((token, None));
}
Token::Comma => {
while let Some((token, jmp_pos)) = self.operator_stack.last() {
match token {
Token::OpenParen => break,
Token::BinaryOperator(bop) => {
let bop = *bop;
let jmp_pos = *jmp_pos;
self.update_jmp_pos(jmp_pos);
self.output.push(Expression::BinaryOperator(bop));
self.operator_stack.pop();
}
Token::UnaryOperator(uop) => {
self.output.push(Expression::UnaryOperator(*uop));
self.operator_stack.pop();
}
_ => break,
}
}
}
}
last_is_var_or_fnc = is_var_or_fnc;
}
while let Some((token, jmp_pos)) = self.operator_stack.pop() {
match token {
Token::BinaryOperator(bop) => {
self.update_jmp_pos(jmp_pos);
self.output.push(Expression::BinaryOperator(bop))
}
Token::UnaryOperator(uop) => self.output.push(Expression::UnaryOperator(uop)),
_ => return Err("Invalid token on the operator stack".to_string()),
}
}
Ok(self)
}
fn inc_arg_count(&mut self) {
if let Some(x) = self.arg_count.last_mut() {
*x = x.saturating_add(1);
let op_pos = self.operator_stack.len().saturating_sub(2);
match self.operator_stack.get_mut(op_pos) {
Some((Token::Function { num_args, id, .. }, _)) if *id == ID_ARRAY_BUILD => {
*num_args += 1;
}
_ => {}
}
}
}
fn dec_arg_count(&mut self) {
if let Some(x) = self.arg_count.last_mut() {
*x = x.saturating_sub(1);
}
}
fn update_jmp_pos(&mut self, jmp_pos: Option<usize>) {
if let Some(jmp_pos) = jmp_pos {
let cur_pos = self.output.len();
if let Expression::JmpIf { pos, .. } = &mut self.output[jmp_pos] {
*pos = (cur_pos - jmp_pos) as u32;
} else {
#[cfg(test)]
panic!("Invalid jump position");
}
}
}
}
impl BinaryOperator {
fn precedence(&self) -> i32 {
match self {
BinaryOperator::Multiply | BinaryOperator::Divide => 7,
BinaryOperator::Add | BinaryOperator::Subtract => 6,
BinaryOperator::Gt | BinaryOperator::Ge | BinaryOperator::Lt | BinaryOperator::Le => 5,
BinaryOperator::Eq | BinaryOperator::Ne => 4,
BinaryOperator::Xor => 3,
BinaryOperator::And => 2,
BinaryOperator::Or => 1,
}
}
}
+285
View File
@@ -0,0 +1,285 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::{
iter::{Enumerate, Peekable},
slice::Iter,
};
use crate::{compiler::Number, runtime::eval::IntoString};
use super::{BinaryOperator, Token, UnaryOperator};
pub(crate) struct Tokenizer<'x, F>
where
F: Fn(&str, bool) -> Result<Token, String>,
{
pub(crate) iter: Peekable<Enumerate<Iter<'x, u8>>>,
token_map: F,
buf: Vec<u8>,
depth: u32,
next_token: Vec<Token>,
has_number: bool,
has_dot: bool,
has_alpha: bool,
is_start: bool,
is_eof: bool,
}
impl<'x, F> Tokenizer<'x, F>
where
F: Fn(&str, bool) -> Result<Token, String>,
{
#[cfg(test)]
pub fn new(expr: &'x str, token_map: F) -> Self {
Self::from_iter(expr.as_bytes().iter().enumerate().peekable(), token_map)
}
#[allow(clippy::should_implement_trait)]
pub(crate) fn from_iter(iter: Peekable<Enumerate<Iter<'x, u8>>>, token_map: F) -> Self {
Self {
iter,
buf: Vec::new(),
depth: 0,
next_token: Vec::with_capacity(2),
has_number: false,
has_dot: false,
has_alpha: false,
is_start: true,
is_eof: false,
token_map,
}
}
#[allow(clippy::should_implement_trait)]
pub(crate) fn next(&mut self) -> Result<Option<Token>, String> {
if let Some(token) = self.next_token.pop() {
return Ok(Some(token));
} else if self.is_eof {
return Ok(None);
}
while let Some((_, &ch)) = self.iter.next() {
match ch {
b'A'..=b'Z' | b'a'..=b'z' | b'_' => {
self.buf.push(ch);
self.has_alpha = true;
}
b'0'..=b'9' => {
self.buf.push(ch);
self.has_number = true;
}
b'.' => {
self.buf.push(ch);
self.has_dot = true;
}
b'}' => {
self.is_eof = true;
break;
}
b'[' if matches!(self.buf.get(0..7), Some(b"header.")) => {
self.buf.push(ch);
}
b'-' if self.buf.last().is_some_and(|c| *c == b'[')
|| matches!(self.buf.get(0..7), Some(b"header.")) =>
{
self.buf.push(ch);
}
b':' if self.buf.contains(&b'.') => {
self.buf.push(ch);
}
b']' if self.buf.contains(&b'[') => {
self.buf.push(b']');
}
b'*' if self.buf.last().is_some_and(|&c| c == b'[' || c == b'.') => {
self.buf.push(ch);
}
_ => {
let prev_token = if !self.buf.is_empty() {
self.is_start = false;
self.parse_buf()?.into()
} else {
None
};
let token = match ch {
b'&' => {
if matches!(self.iter.peek(), Some((_, b'&'))) {
self.iter.next();
}
Token::BinaryOperator(BinaryOperator::And)
}
b'|' => {
if matches!(self.iter.peek(), Some((_, b'|'))) {
self.iter.next();
}
Token::BinaryOperator(BinaryOperator::Or)
}
b'!' => {
if matches!(self.iter.peek(), Some((_, b'='))) {
self.iter.next();
Token::BinaryOperator(BinaryOperator::Ne)
} else {
Token::UnaryOperator(UnaryOperator::Not)
}
}
b'^' => Token::BinaryOperator(BinaryOperator::Xor),
b'(' => {
self.depth += 1;
Token::OpenParen
}
b')' => {
if self.depth == 0 {
return Err("Unmatched close parenthesis".to_string());
}
self.depth -= 1;
Token::CloseParen
}
b'+' => Token::BinaryOperator(BinaryOperator::Add),
b'*' => Token::BinaryOperator(BinaryOperator::Multiply),
b'/' => Token::BinaryOperator(BinaryOperator::Divide),
b'-' => {
if self.is_start {
Token::UnaryOperator(UnaryOperator::Minus)
} else {
Token::BinaryOperator(BinaryOperator::Subtract)
}
}
b'=' => match self.iter.next() {
Some((_, b'=')) => Token::BinaryOperator(BinaryOperator::Eq),
Some((_, b'>')) => Token::BinaryOperator(BinaryOperator::Ge),
Some((_, b'<')) => Token::BinaryOperator(BinaryOperator::Le),
_ => Token::BinaryOperator(BinaryOperator::Eq),
},
b'>' => match self.iter.peek() {
Some((_, b'=')) => {
self.iter.next();
Token::BinaryOperator(BinaryOperator::Ge)
}
_ => Token::BinaryOperator(BinaryOperator::Gt),
},
b'<' => match self.iter.peek() {
Some((_, b'=')) => {
self.iter.next();
Token::BinaryOperator(BinaryOperator::Le)
}
_ => Token::BinaryOperator(BinaryOperator::Lt),
},
b',' => Token::Comma,
b'[' => Token::OpenBracket,
b']' => Token::CloseBracket,
b' ' | b'\r' | b'\n' => {
if prev_token.is_some() {
return Ok(prev_token);
} else {
continue;
}
}
b'\"' | b'\'' => {
let mut buf = Vec::with_capacity(16);
let stop_ch = ch;
let mut last_ch = 0;
let mut found_end = false;
for (_, &ch) in self.iter.by_ref() {
if last_ch != b'\\' {
if ch != stop_ch {
buf.push(ch);
} else {
found_end = true;
break;
}
} else {
match ch {
b'n' => {
buf.push(b'\n');
}
b'r' => {
buf.push(b'\r');
}
b't' => {
buf.push(b'\t');
}
_ => {
buf.push(ch);
}
}
}
last_ch = ch;
}
if found_end {
Token::String(
String::from_utf8(buf)
.map_err(|_| "Invalid UTF-8".to_string())?,
)
} else {
return Err("Unterminated string".to_string());
}
}
_ => {
return Err(format!("Invalid character {:?}", char::from(ch),));
}
};
self.is_start = matches!(
token,
Token::OpenParen | Token::Comma | Token::BinaryOperator(_)
);
return if prev_token.is_some() {
self.next_token.push(token);
Ok(prev_token)
} else {
Ok(Some(token))
};
}
}
}
if self.depth > 0 {
Err("Unmatched open parenthesis".to_string())
} else if !self.buf.is_empty() {
self.parse_buf().map(Some)
} else {
Ok(None)
}
}
fn parse_buf(&mut self) -> Result<Token, String> {
let buf = std::mem::take(&mut self.buf).into_string();
if self.has_number && !self.has_alpha {
self.has_number = false;
if self.has_dot {
self.has_dot = false;
buf.parse::<f64>()
.map(|f| Token::Number(Number::Float(f)))
.map_err(|_| format!("Invalid float value {}", buf,))
} else {
buf.parse::<i64>()
.map(|i| Token::Number(Number::Integer(i)))
.map_err(|_| format!("Invalid integer value {}", buf,))
}
} else {
let has_dot = self.has_dot;
let has_number = self.has_number;
self.has_alpha = false;
self.has_number = false;
self.has_dot = false;
if !has_number && !has_dot && [4, 5].contains(&buf.len()) {
if buf == "true" {
return Ok(Token::Number(Number::Integer(1)));
} else if buf == "false" {
return Ok(Token::Number(Number::Integer(0)));
}
}
(self.token_map)(&buf, has_dot)
}
}
}
File diff suppressed because it is too large Load Diff
+736
View File
@@ -0,0 +1,736 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*
* Modified by Coffey Labs in 2026 for INBUXA.
*/
use std::fmt::Display;
use self::{expr::Expression, instruction::CompilerState};
use super::{
CompileError, ErrorType, Regex, Value,
lexer::{Token, tokenizer::TokenInfo, word::Word},
};
pub mod actions;
pub mod expr;
pub mod instruction;
pub mod test;
pub mod tests;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(
any(test, feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
pub enum Capability {
Envelope,
EnvelopeDsn,
EnvelopeDeliverBy,
FileInto,
EncodedCharacter,
Comparator(Comparator),
Other(String),
Body,
Convert,
Copy,
Relational,
Date,
Index,
Duplicate,
Variables,
EditHeader,
ForEveryPart,
Mime,
Replace,
Enclose,
ExtractText,
Enotify,
RedirectDsn,
RedirectDeliverBy,
Environment,
Reject,
Ereject,
ExtLists,
SubAddress,
Vacation,
VacationSeconds,
Fcc,
Mailbox,
MailboxId,
MboxMetadata,
ServerMetadata,
SpecialUse,
Imap4Flags,
Ihave,
ImapSieve,
Include,
Regex,
SpamTest,
SpamTestPlus,
VirusTest,
// Extensions
Expressions,
While,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(
any(test, feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
pub enum AddressPart {
LocalPart,
Domain,
All,
User,
Detail,
Name,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(
any(test, feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
pub(crate) enum MatchType {
Is,
Contains,
Matches(u64),
Regex(u64),
Value(RelationalMatch),
Count(RelationalMatch),
List,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(
any(test, feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
pub(crate) enum RelationalMatch {
Gt,
Ge,
Lt,
Le,
Eq,
Ne,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(
any(test, feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
pub enum Comparator {
Elbonia,
Octet,
AsciiCaseMap,
AsciiNumeric,
Other(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(
any(test, feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
pub struct Clear {
pub(crate) local_vars_idx: u32,
pub(crate) local_vars_num: u32,
pub(crate) match_vars: u64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(
any(test, feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
pub struct Invalid {
pub(crate) name: String,
pub(crate) line_num: usize,
pub(crate) line_pos: usize,
}
#[derive(Debug, Eq, PartialEq, Clone)]
#[cfg_attr(
any(test, feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
pub(crate) struct While {
pub expr: Vec<Expression>,
pub jz_pos: usize,
}
impl CompilerState<'_> {
#[inline(always)]
pub fn expect_instruction_end(&mut self) -> Result<(), CompileError> {
self.tokens.expect_token(Token::Semicolon)
}
pub fn ignore_instruction(&mut self) -> Result<(), CompileError> {
// Skip entire instruction
let mut curly_count = 0;
loop {
let token_info = self.tokens.unwrap_next()?;
match token_info.token {
Token::Semicolon if curly_count == 0 => {
break;
}
Token::CurlyOpen => {
curly_count += 1;
}
Token::CurlyClose => match curly_count {
0 => {
return Err(token_info.expected("instruction"));
}
1 => {
break;
}
_ => curly_count -= 1,
},
_ => (),
}
}
Ok(())
}
pub fn ignore_test(&mut self) -> Result<(), CompileError> {
let mut d_count = 0;
while let Some(token_info) = self.tokens.peek() {
match token_info?.token {
Token::ParenthesisOpen => {
d_count += 1;
}
Token::ParenthesisClose => {
if d_count == 0 {
break;
} else {
d_count -= 1;
}
}
Token::Comma if d_count == 0 => {
break;
}
Token::CurlyOpen => {
break;
}
_ => (),
}
self.tokens.next();
}
Ok(())
}
pub fn parse_match_type(&mut self, word: Word) -> Result<MatchType, CompileError> {
match word {
Word::Is => Ok(MatchType::Is),
Word::Contains => Ok(MatchType::Contains),
Word::Matches => {
self.block.match_test_pos.push(self.instructions.len());
Ok(MatchType::Matches(0))
}
Word::Regex => {
self.block.match_test_pos.push(self.instructions.len());
Ok(MatchType::Regex(0))
}
Word::List => Ok(MatchType::List),
_ => {
let token_info = self.tokens.unwrap_next()?;
if let Token::StringConstant(text) = &token_info.token
&& let Some(relational) = lookup_relational(text.to_string().as_ref())
{
return Ok(if word == Word::Value {
MatchType::Value(relational)
} else {
MatchType::Count(relational)
});
}
Err(token_info.expected("relational match"))
}
}
}
pub(crate) fn parse_comparator(&mut self) -> Result<Comparator, CompileError> {
let comparator = self.tokens.expect_static_string()?;
Ok(if let Some(comparator) = lookup_comparator(&comparator) {
comparator
} else {
Comparator::Other(comparator)
})
}
pub(crate) fn parse_static_strings(&mut self) -> Result<Vec<String>, CompileError> {
let token_info = self.tokens.unwrap_next()?;
match token_info.token {
Token::BracketOpen => {
let mut strings = Vec::new();
loop {
let token_info = self.tokens.unwrap_next()?;
match token_info.token {
Token::StringConstant(string) => {
strings.push(string.into_string());
}
Token::Comma => (),
Token::BracketClose if !strings.is_empty() => break,
_ => return Err(token_info.expected("constant string")),
}
}
Ok(strings)
}
Token::StringConstant(string) => Ok(vec![string.into_string()]),
_ => Err(token_info.expected("'[' or constant string")),
}
}
pub fn parse_string(&mut self) -> Result<Value, CompileError> {
let next_token = self.tokens.unwrap_next()?;
match next_token.token {
Token::StringConstant(s) => Ok(Value::from(s)),
Token::StringVariable(s) => {
self.tokenize_string(&s, true)
.map_err(|error_type| CompileError {
line_num: next_token.line_num,
line_pos: next_token.line_pos,
error_type,
})
}
Token::BracketOpen => {
let mut items = self.parse_string_list(false)?;
match items.pop() {
Some(s) if items.is_empty() => Ok(s),
_ => Err(next_token.expected("string")),
}
}
_ => Err(next_token.expected("string")),
}
}
pub(crate) fn parse_strings(&mut self, allow_empty: bool) -> Result<Vec<Value>, CompileError> {
let token_info = self.tokens.unwrap_next()?;
match token_info.token {
Token::BracketOpen => self.parse_string_list(allow_empty),
Token::StringConstant(s) => Ok(vec![Value::from(s)]),
Token::StringVariable(s) => {
self.tokenize_string(&s, true)
.map(|s| vec![s])
.map_err(|error_type| CompileError {
line_num: token_info.line_num,
line_pos: token_info.line_pos,
error_type,
})
}
_ => Err(token_info.expected("'[' or string")),
}
}
pub(crate) fn parse_string_token(
&mut self,
token_info: TokenInfo,
) -> Result<Value, CompileError> {
match token_info.token {
Token::StringConstant(s) => Ok(Value::from(s)),
Token::StringVariable(s) => {
self.tokenize_string(&s, true)
.map_err(|error_type| CompileError {
line_num: token_info.line_num,
line_pos: token_info.line_pos,
error_type,
})
}
_ => Err(token_info.expected("string")),
}
}
pub(crate) fn parse_strings_token(
&mut self,
token_info: TokenInfo,
) -> Result<Vec<Value>, CompileError> {
match token_info.token {
Token::StringConstant(s) => Ok(vec![Value::from(s)]),
Token::StringVariable(s) => {
self.tokenize_string(&s, true)
.map(|s| vec![s])
.map_err(|error_type| CompileError {
line_num: token_info.line_num,
line_pos: token_info.line_pos,
error_type,
})
}
Token::BracketOpen => self.parse_string_list(false),
_ => Err(token_info.expected("string")),
}
}
pub(crate) fn parse_string_list(
&mut self,
allow_empty: bool,
) -> Result<Vec<Value>, CompileError> {
let mut strings = Vec::new();
loop {
let token_info = self.tokens.unwrap_next()?;
match token_info.token {
Token::StringConstant(s) => {
strings.push(Value::from(s));
}
Token::StringVariable(s) => {
strings.push(self.tokenize_string(&s, true).map_err(|error_type| {
CompileError {
line_num: token_info.line_num,
line_pos: token_info.line_pos,
error_type,
}
})?);
}
Token::Comma => (),
Token::BracketClose if !strings.is_empty() || allow_empty => break,
_ => return Err(token_info.expected("string or string list")),
}
}
Ok(strings)
}
#[inline(always)]
pub(crate) fn has_capability(&self, capability: &Capability) -> bool {
[&self.block]
.into_iter()
.chain(self.block_stack.iter())
.any(|b| b.capabilities.contains(capability))
|| (capability != &Capability::Ihave && self.compiler.no_capability_check)
}
#[inline(always)]
pub(crate) fn reset_param_check(&mut self) {
self.param_check.fill(false);
}
#[inline(always)]
pub(crate) fn validate_argument(
&mut self,
arg_num: usize,
capability: Option<Capability>,
line_num: usize,
line_pos: usize,
) -> Result<(), CompileError> {
if arg_num > 0 {
if let Some(param) = self.param_check.get_mut(arg_num - 1) {
if !*param {
*param = true;
} else {
return Err(CompileError {
line_num,
line_pos,
error_type: ErrorType::DuplicatedParameter,
});
}
} else {
#[cfg(test)]
panic!("Argument out of range {arg_num}");
}
}
if let Some(capability) = capability
&& !self.has_capability(&capability)
{
return Err(CompileError {
line_num,
line_pos,
error_type: ErrorType::UndeclaredCapability(capability),
});
}
Ok(())
}
pub(crate) fn validate_match(
&mut self,
match_type: &MatchType,
key_list: &mut [Value],
) -> Result<(), CompileError> {
if matches!(match_type, MatchType::Regex(_)) {
for key in key_list {
if let Value::Text(expr) = key {
match fancy_regex::Regex::new(expr) {
Ok(regex) => {
*key = Value::Regex(Regex::new(expr.to_string(), regex));
}
Err(err) => {
return Err(self
.tokens
.unwrap_next()?
.custom(ErrorType::InvalidRegex(format!("{expr}: {err}"))));
}
}
}
}
}
Ok(())
}
}
impl Capability {
pub fn parse(capability: &str) -> Capability {
if let Some(capability) = lookup_capabilities(capability) {
capability
} else if let Some(comparator) = capability.strip_prefix("comparator-") {
Capability::Comparator(Comparator::Other(comparator.to_string()))
} else {
Capability::Other(capability.to_string())
}
}
pub fn all() -> &'static [Capability] {
&[
Capability::Envelope,
Capability::EnvelopeDsn,
Capability::EnvelopeDeliverBy,
Capability::FileInto,
Capability::EncodedCharacter,
Capability::Comparator(Comparator::Elbonia),
Capability::Comparator(Comparator::AsciiCaseMap),
Capability::Comparator(Comparator::AsciiNumeric),
Capability::Comparator(Comparator::Octet),
Capability::Body,
Capability::Convert,
Capability::Copy,
Capability::Relational,
Capability::Date,
Capability::Index,
Capability::Duplicate,
Capability::Variables,
Capability::EditHeader,
Capability::ForEveryPart,
Capability::Mime,
Capability::Replace,
Capability::Enclose,
Capability::ExtractText,
Capability::Enotify,
Capability::RedirectDsn,
Capability::RedirectDeliverBy,
Capability::Environment,
Capability::Reject,
Capability::Ereject,
Capability::ExtLists,
Capability::SubAddress,
Capability::Vacation,
Capability::VacationSeconds,
Capability::Fcc,
Capability::Mailbox,
Capability::MailboxId,
Capability::MboxMetadata,
Capability::ServerMetadata,
Capability::SpecialUse,
Capability::Imap4Flags,
Capability::Ihave,
Capability::ImapSieve,
Capability::Include,
Capability::Regex,
Capability::SpamTest,
Capability::SpamTestPlus,
Capability::VirusTest,
]
}
}
fn lookup_relational(input: &str) -> Option<RelationalMatch> {
hashify::tiny_map!(
input.as_bytes(),
"gt" => RelationalMatch::Gt,
"ge" => RelationalMatch::Ge,
"lt" => RelationalMatch::Lt,
"le" => RelationalMatch::Le,
"eq" => RelationalMatch::Eq,
"ne" => RelationalMatch::Ne,
)
}
fn lookup_comparator(input: &str) -> Option<Comparator> {
hashify::tiny_map!(
input.as_bytes(),
"i;octet" => Comparator::Octet,
"i;ascii-casemap" => Comparator::AsciiCaseMap,
"i;ascii-numeric" => Comparator::AsciiNumeric,
)
}
impl Invalid {
pub fn name(&self) -> &str {
&self.name
}
pub fn line_num(&self) -> usize {
self.line_num
}
pub fn line_pos(&self) -> usize {
self.line_pos
}
}
impl From<&str> for Capability {
fn from(value: &str) -> Self {
Capability::parse(value)
}
}
impl From<String> for Capability {
fn from(value: String) -> Self {
Capability::parse(&value)
}
}
impl Display for Capability {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Capability::Envelope => f.write_str("envelope"),
Capability::EnvelopeDsn => f.write_str("envelope-dsn"),
Capability::EnvelopeDeliverBy => f.write_str("envelope-deliverby"),
Capability::FileInto => f.write_str("fileinto"),
Capability::EncodedCharacter => f.write_str("encoded-character"),
Capability::Comparator(Comparator::Elbonia) => f.write_str("comparator-elbonia"),
Capability::Comparator(Comparator::Octet) => f.write_str("comparator-i;octet"),
Capability::Comparator(Comparator::AsciiCaseMap) => {
f.write_str("comparator-i;ascii-casemap")
}
Capability::Comparator(Comparator::AsciiNumeric) => {
f.write_str("comparator-i;ascii-numeric")
}
Capability::Comparator(Comparator::Other(comparator)) => f.write_str(comparator),
Capability::Body => f.write_str("body"),
Capability::Convert => f.write_str("convert"),
Capability::Copy => f.write_str("copy"),
Capability::Relational => f.write_str("relational"),
Capability::Date => f.write_str("date"),
Capability::Index => f.write_str("index"),
Capability::Duplicate => f.write_str("duplicate"),
Capability::Variables => f.write_str("variables"),
Capability::EditHeader => f.write_str("editheader"),
Capability::ForEveryPart => f.write_str("foreverypart"),
Capability::Mime => f.write_str("mime"),
Capability::Replace => f.write_str("replace"),
Capability::Enclose => f.write_str("enclose"),
Capability::ExtractText => f.write_str("extracttext"),
Capability::Enotify => f.write_str("enotify"),
Capability::RedirectDsn => f.write_str("redirect-dsn"),
Capability::RedirectDeliverBy => f.write_str("redirect-deliverby"),
Capability::Environment => f.write_str("environment"),
Capability::Reject => f.write_str("reject"),
Capability::Ereject => f.write_str("ereject"),
Capability::ExtLists => f.write_str("extlists"),
Capability::SubAddress => f.write_str("subaddress"),
Capability::Vacation => f.write_str("vacation"),
Capability::VacationSeconds => f.write_str("vacation-seconds"),
Capability::Fcc => f.write_str("fcc"),
Capability::Mailbox => f.write_str("mailbox"),
Capability::MailboxId => f.write_str("mailboxid"),
Capability::MboxMetadata => f.write_str("mboxmetadata"),
Capability::ServerMetadata => f.write_str("servermetadata"),
Capability::SpecialUse => f.write_str("special-use"),
Capability::Imap4Flags => f.write_str("imap4flags"),
Capability::Ihave => f.write_str("ihave"),
Capability::ImapSieve => f.write_str("imapsieve"),
Capability::Include => f.write_str("include"),
Capability::Regex => f.write_str("regex"),
Capability::SpamTest => f.write_str("spamtest"),
Capability::SpamTestPlus => f.write_str("spamtestplus"),
Capability::VirusTest => f.write_str("virustest"),
Capability::While => f.write_str("vnd.inbuxa.while"),
Capability::Expressions => f.write_str("vnd.inbuxa.expressions"),
Capability::Other(capability) => f.write_str(capability),
}
}
}
fn lookup_capabilities(input: &str) -> Option<Capability> {
hashify::tiny_map!(
input.as_bytes(),
"envelope" => Capability::Envelope,
"envelope-dsn" => Capability::EnvelopeDsn,
"envelope-deliverby" => Capability::EnvelopeDeliverBy,
"fileinto" => Capability::FileInto,
"encoded-character" => Capability::EncodedCharacter,
"comparator-elbonia" => Capability::Comparator(Comparator::Elbonia),
"comparator-i;octet" => Capability::Comparator(Comparator::Octet),
"comparator-i;ascii-casemap" => Capability::Comparator(Comparator::AsciiCaseMap),
"comparator-i;ascii-numeric" => Capability::Comparator(Comparator::AsciiNumeric),
"body" => Capability::Body,
"convert" => Capability::Convert,
"copy" => Capability::Copy,
"relational" => Capability::Relational,
"date" => Capability::Date,
"index" => Capability::Index,
"duplicate" => Capability::Duplicate,
"variables" => Capability::Variables,
"editheader" => Capability::EditHeader,
"foreverypart" => Capability::ForEveryPart,
"mime" => Capability::Mime,
"replace" => Capability::Replace,
"enclose" => Capability::Enclose,
"extracttext" => Capability::ExtractText,
"enotify" => Capability::Enotify,
"redirect-dsn" => Capability::RedirectDsn,
"redirect-deliverby" => Capability::RedirectDeliverBy,
"environment" => Capability::Environment,
"reject" => Capability::Reject,
"ereject" => Capability::Ereject,
"extlists" => Capability::ExtLists,
"subaddress" => Capability::SubAddress,
"vacation" => Capability::Vacation,
"vacation-seconds" => Capability::VacationSeconds,
"fcc" => Capability::Fcc,
"mailbox" => Capability::Mailbox,
"mailboxid" => Capability::MailboxId,
"mboxmetadata" => Capability::MboxMetadata,
"servermetadata" => Capability::ServerMetadata,
"special-use" => Capability::SpecialUse,
"imap4flags" => Capability::Imap4Flags,
"ihave" => Capability::Ihave,
"imapsieve" => Capability::ImapSieve,
"include" => Capability::Include,
"regex" => Capability::Regex,
"spamtest" => Capability::SpamTest,
"spamtestplus" => Capability::SpamTestPlus,
"virustest" => Capability::VirusTest,
// Extensions
"vnd.inbuxa.while" => Capability::While,
"vnd.inbuxa.expressions" => Capability::Expressions,
)
}
+686
View File
@@ -0,0 +1,686 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::compiler::{
CompileError, ErrorType,
lexer::{Token, tokenizer::TokenInfo, word::Word},
};
use super::{
Capability, Invalid,
actions::{action_convert::Convert, action_vacation::TestVacation},
expr::{Expression, UnaryOperator, parser::ExpressionParser, tokenizer::Tokenizer},
instruction::{CompilerState, Instruction},
tests::{
test_address::TestAddress,
test_body::TestBody,
test_date::{TestCurrentDate, TestDate},
test_duplicate::TestDuplicate,
test_envelope::TestEnvelope,
test_exists::TestExists,
test_extlists::TestValidExtList,
test_hasflag::TestHasFlag,
test_header::TestHeader,
test_ihave::TestIhave,
test_mailbox::{TestMailboxExists, TestMetadata, TestMetadataExists},
test_mailboxid::TestMailboxIdExists,
test_notify::{TestNotifyMethodCapability, TestValidNotifyMethod},
test_size::TestSize,
test_spamtest::{TestSpamTest, TestVirusTest},
test_specialuse::TestSpecialUseExists,
test_string::TestString,
},
};
#[allow(clippy::enum_variant_names)]
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(
any(test, feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
pub(crate) enum Test {
True,
False,
Address(TestAddress),
Envelope(TestEnvelope),
Exists(TestExists),
Header(TestHeader),
Size(TestSize),
Invalid(Invalid),
// RFC 5173
Body(TestBody),
// RFC 6558
Convert(Convert),
// RFC 5260
Date(TestDate),
CurrentDate(TestCurrentDate),
// RFC 7352
Duplicate(TestDuplicate),
// RFC 5229 & RFC 5183
String(TestString),
Environment(TestString),
// RFC 5435
NotifyMethodCapability(TestNotifyMethodCapability),
ValidNotifyMethod(TestValidNotifyMethod),
// RFC 6134
ValidExtList(TestValidExtList),
// RFC 5463
Ihave(TestIhave),
// RFC 5232
HasFlag(TestHasFlag),
// RFC 5490
MailboxExists(TestMailboxExists),
Metadata(TestMetadata),
MetadataExists(TestMetadataExists),
// RFC 9042
MailboxIdExists(TestMailboxIdExists),
// RFC 5235
SpamTest(TestSpamTest),
VirusTest(TestVirusTest),
// RFC 8579
SpecialUseExists(TestSpecialUseExists),
// RFC 5230
Vacation(TestVacation),
// Only test
#[cfg(test)]
TestCmd {
arguments: Vec<crate::compiler::Value>,
is_not: bool,
},
}
#[derive(Debug)]
struct Block {
is_all: bool,
is_not: bool,
p_count: u32,
jmps: Vec<usize>,
}
impl CompilerState<'_> {
pub(crate) fn parse_test(&mut self) -> Result<(), CompileError> {
let mut block_stack: Vec<Block> = Vec::new();
let mut block = Block {
is_all: false,
is_not: false,
p_count: 0,
jmps: Vec::new(),
};
let mut is_not = false;
loop {
let token_info = self.tokens.unwrap_next()?;
self.reset_param_check();
let test: Instruction =
match token_info.token {
Token::Comma
if !block_stack.is_empty()
&& matches!(
self.instructions.last(),
Some(Instruction::Test(_) | Instruction::Eval(_))
)
&& matches!(
self.tokens.peek(),
Some(Ok(TokenInfo {
token: Token::Identifier(_) | Token::Unknown(_),
..
}))
) =>
{
is_not = block.is_not;
block.jmps.push(self.instructions.len());
self.instructions.push(if block.is_all {
Instruction::Jz(usize::MAX)
} else {
Instruction::Jnz(usize::MAX)
});
continue;
}
Token::ParenthesisOpen => {
block.p_count += 1;
continue;
}
Token::ParenthesisClose => {
if block.p_count > 0 {
block.p_count -= 1;
continue;
} else if let Some(prev_block) = block_stack.pop() {
let cur_pos = self.instructions.len();
for jmp_pos in block.jmps {
if let Instruction::Jnz(jmp_pos) | Instruction::Jz(jmp_pos) =
&mut self.instructions[jmp_pos]
{
*jmp_pos = cur_pos;
} else {
debug_assert!(false, "This should not have happened")
}
}
block = prev_block;
is_not = block.is_not;
if block_stack.is_empty() {
break;
} else {
continue;
}
} else {
return Err(token_info.expected("test name"));
}
}
Token::Identifier(Word::Not) => {
if !matches!(
self.tokens.peek(),
Some(Ok(TokenInfo {
token: Token::Identifier(_) | Token::Unknown(_),
..
}))
) {
return Err(token_info.expected("test name"));
}
is_not = !is_not;
continue;
}
Token::Identifier(word @ (Word::AnyOf | Word::AllOf)) => {
if block_stack.len() < self.tokens.compiler.max_nested_tests {
self.tokens.expect_token(Token::ParenthesisOpen)?;
block_stack.push(block);
let (is_all, block_is_not) = if word == Word::AllOf {
if !is_not {
(true, false)
} else {
(false, true)
}
} else if !is_not {
(false, false)
} else {
(true, true)
};
block = Block {
is_all,
is_not: block_is_not,
p_count: 0,
jmps: Vec::new(),
};
is_not = block_is_not;
continue;
} else {
return Err(CompileError {
line_num: token_info.line_num,
line_pos: token_info.line_pos,
error_type: ErrorType::TooManyNestedTests,
});
}
}
Token::Identifier(Word::True) => if !is_not {
Test::True
} else {
is_not = false;
Test::False
}
.into(),
Token::Identifier(Word::False) => if !is_not {
Test::False
} else {
is_not = false;
Test::True
}
.into(),
Token::Identifier(Word::Address) => self.parse_test_address()?.into(),
Token::Identifier(Word::Envelope) => {
self.validate_argument(
0,
Capability::Envelope.into(),
token_info.line_num,
token_info.line_pos,
)?;
self.parse_test_envelope()?.into()
}
Token::Identifier(Word::Header) => self.parse_test_header()?.into(),
Token::Identifier(Word::Size) => self.parse_test_size()?.into(),
Token::Identifier(Word::Exists) => self.parse_test_exists()?.into(),
// RFC 5173
Token::Identifier(Word::Body) => {
self.validate_argument(
0,
Capability::Body.into(),
token_info.line_num,
token_info.line_pos,
)?;
self.parse_test_body()?.into()
}
// RFC 6558
Token::Identifier(Word::Convert) => {
self.validate_argument(
0,
Capability::Convert.into(),
token_info.line_num,
token_info.line_pos,
)?;
self.parse_test_convert()?.into()
}
// RFC 5260
Token::Identifier(Word::Date) => {
self.validate_argument(
0,
Capability::Date.into(),
token_info.line_num,
token_info.line_pos,
)?;
self.parse_test_date()?.into()
}
Token::Identifier(Word::CurrentDate) => {
self.validate_argument(
0,
Capability::Date.into(),
token_info.line_num,
token_info.line_pos,
)?;
self.parse_test_currentdate()?.into()
}
// RFC 7352
Token::Identifier(Word::Duplicate) => {
self.validate_argument(
0,
Capability::Duplicate.into(),
token_info.line_num,
token_info.line_pos,
)?;
self.parse_test_duplicate()?.into()
}
// RFC 5229
Token::Identifier(Word::String) => {
self.validate_argument(
0,
Capability::Variables.into(),
token_info.line_num,
token_info.line_pos,
)?;
self.parse_test_string()?.into()
}
// RFC 5435
Token::Identifier(Word::NotifyMethodCapability) => {
self.validate_argument(
0,
Capability::Enotify.into(),
token_info.line_num,
token_info.line_pos,
)?;
self.parse_test_notify_method_capability()?.into()
}
Token::Identifier(Word::ValidNotifyMethod) => {
self.validate_argument(
0,
Capability::Enotify.into(),
token_info.line_num,
token_info.line_pos,
)?;
self.parse_test_valid_notify_method()?.into()
}
// RFC 5183
Token::Identifier(Word::Environment) => {
self.validate_argument(
0,
Capability::Environment.into(),
token_info.line_num,
token_info.line_pos,
)?;
self.parse_test_environment()?.into()
}
// RFC 6134
Token::Identifier(Word::ValidExtList) => {
self.validate_argument(
0,
Capability::ExtLists.into(),
token_info.line_num,
token_info.line_pos,
)?;
self.parse_test_valid_ext_list()?.into()
}
// RFC 5463
Token::Identifier(Word::Ihave) => {
self.validate_argument(
0,
Capability::Ihave.into(),
token_info.line_num,
token_info.line_pos,
)?;
self.parse_test_ihave()?.into()
}
// RFC 5232
Token::Identifier(Word::HasFlag) => {
self.validate_argument(
0,
Capability::Imap4Flags.into(),
token_info.line_num,
token_info.line_pos,
)?;
self.parse_test_hasflag()?.into()
}
// RFC 5490
Token::Identifier(Word::MailboxExists) => {
self.validate_argument(
0,
Capability::Mailbox.into(),
token_info.line_num,
token_info.line_pos,
)?;
self.parse_test_mailboxexists()?.into()
}
Token::Identifier(Word::Metadata) => {
self.validate_argument(
0,
Capability::MboxMetadata.into(),
token_info.line_num,
token_info.line_pos,
)?;
self.parse_test_metadata()?.into()
}
Token::Identifier(Word::MetadataExists) => {
self.validate_argument(
0,
Capability::MboxMetadata.into(),
token_info.line_num,
token_info.line_pos,
)?;
self.parse_test_metadataexists()?.into()
}
Token::Identifier(Word::ServerMetadata) => {
self.validate_argument(
0,
Capability::ServerMetadata.into(),
token_info.line_num,
token_info.line_pos,
)?;
self.parse_test_servermetadata()?.into()
}
Token::Identifier(Word::ServerMetadataExists) => {
self.validate_argument(
0,
Capability::ServerMetadata.into(),
token_info.line_num,
token_info.line_pos,
)?;
self.parse_test_servermetadataexists()?.into()
}
// RFC 9042
Token::Identifier(Word::MailboxIdExists) => {
self.validate_argument(
0,
Capability::MailboxId.into(),
token_info.line_num,
token_info.line_pos,
)?;
self.parse_test_mailboxidexists()?.into()
}
// RFC 5235
Token::Identifier(Word::SpamTest) => {
self.validate_argument(
0,
Capability::SpamTest.into(),
token_info.line_num,
token_info.line_pos,
)?;
self.parse_test_spamtest()?.into()
}
Token::Identifier(Word::VirusTest) => {
self.validate_argument(
0,
Capability::VirusTest.into(),
token_info.line_num,
token_info.line_pos,
)?;
self.parse_test_virustest()?.into()
}
// RFC 8579
Token::Identifier(Word::SpecialUseExists) => {
self.validate_argument(
0,
Capability::SpecialUse.into(),
token_info.line_num,
token_info.line_pos,
)?;
self.parse_test_specialuseexists()?.into()
}
// Expressions extension
Token::Identifier(Word::Eval) => {
self.validate_argument(
0,
Capability::Expressions.into(),
token_info.line_num,
token_info.line_pos,
)?;
Instruction::Eval(self.parse_expr()?)
}
Token::Identifier(word) => {
self.ignore_test()?;
Test::Invalid(Invalid {
name: word.to_string(),
line_num: token_info.line_num,
line_pos: token_info.line_pos,
})
.into()
}
#[cfg(test)]
Token::Unknown(name) if name.contains("test") => {
use crate::compiler::Value;
let mut arguments = Vec::new();
arguments.push(Value::Text(name.into()));
while !matches!(
self.tokens.peek().map(|r| r.map(|t| &t.token)),
Some(Ok(Token::Comma
| Token::ParenthesisClose
| Token::CurlyOpen))
) {
arguments.push(match self.tokens.unwrap_next()?.token {
Token::StringConstant(s) => Value::from(s),
Token::StringVariable(s) => self
.tokenize_string(&s, true)
.map_err(|error_type| CompileError {
line_num: 0,
line_pos: 0,
error_type,
})?,
Token::Number(n) => {
Value::Number(crate::compiler::Number::Integer(n as i64))
}
Token::Identifier(s) => Value::Text(s.to_string().into()),
Token::Tag(s) => Value::Text(format!(":{s}").into()),
Token::Unknown(s) => Value::Text(s.into()),
other => panic!("Invalid test param {other:?}"),
});
}
Test::TestCmd {
arguments,
is_not: false,
}
.into()
}
Token::Unknown(name) => {
self.ignore_test()?;
Test::Invalid(Invalid {
name,
line_num: token_info.line_num,
line_pos: token_info.line_pos,
})
.into()
}
_ => return Err(token_info.expected("test name")),
};
while block.p_count > 0 {
self.tokens.expect_token(Token::ParenthesisClose)?;
block.p_count -= 1;
}
self.instructions
.push(if !is_not { test } else { test.set_not() });
if block_stack.is_empty() {
break;
}
}
self.instructions.push(Instruction::Jz(usize::MAX));
Ok(())
}
pub(crate) fn parse_expr(&mut self) -> Result<Vec<Expression>, CompileError> {
let mut next_token = self.tokens.unwrap_next()?;
let expr = match next_token.token {
Token::StringConstant(s) => s.into_string().into_bytes(),
Token::StringVariable(s) => s,
_ => return Err(next_token.expected("string")),
};
match ExpressionParser::from_tokenizer(Tokenizer::from_iter(
expr.iter().enumerate().peekable(),
|var_name, maybe_namespace| self.parse_expr_fnc_or_var(var_name, maybe_namespace),
))
.parse()
{
Ok(parser) => Ok(parser.output),
Err(err) => {
let err = ErrorType::InvalidExpression(format!(
"{}: {}",
std::str::from_utf8(&expr).unwrap_or_default(),
err
));
next_token.token = Token::StringVariable(expr);
Err(next_token.custom(err))
}
}
}
}
impl From<Test> for Instruction {
fn from(test: Test) -> Self {
Instruction::Test(test)
}
}
impl Instruction {
pub fn set_not(mut self) -> Self {
match &mut self {
Instruction::Test(test) => match test {
Test::True => return Instruction::Test(Test::False),
Test::False => return Instruction::Test(Test::True),
Test::Address(op) => {
op.is_not = true;
}
Test::Envelope(op) => {
op.is_not = true;
}
Test::Exists(op) => {
op.is_not = true;
}
Test::Header(op) => {
op.is_not = true;
}
Test::Size(op) => {
op.is_not = true;
}
Test::Body(op) => {
op.is_not = true;
}
Test::Convert(op) => {
op.is_not = true;
}
Test::Date(op) => {
op.is_not = true;
}
Test::CurrentDate(op) => {
op.is_not = true;
}
Test::Duplicate(op) => {
op.is_not = true;
}
Test::String(op) | Test::Environment(op) => {
op.is_not = true;
}
Test::NotifyMethodCapability(op) => {
op.is_not = true;
}
Test::ValidNotifyMethod(op) => {
op.is_not = true;
}
Test::ValidExtList(op) => {
op.is_not = true;
}
Test::Ihave(op) => {
op.is_not = true;
}
Test::HasFlag(op) => {
op.is_not = true;
}
Test::MailboxExists(op) => {
op.is_not = true;
}
Test::Metadata(op) => {
op.is_not = true;
}
Test::MetadataExists(op) => {
op.is_not = true;
}
Test::MailboxIdExists(op) => {
op.is_not = true;
}
Test::SpamTest(op) => {
op.is_not = true;
}
Test::VirusTest(op) => {
op.is_not = true;
}
Test::SpecialUseExists(op) => {
op.is_not = true;
}
#[cfg(test)]
Test::TestCmd { is_not, .. } => {
*is_not = true;
}
Test::Vacation(_) | Test::Invalid(_) => {}
},
Instruction::Eval(expr) => expr.push(Expression::UnaryOperator(UnaryOperator::Not)),
_ => (),
}
self
}
}
+24
View File
@@ -0,0 +1,24 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
pub mod test_address;
pub mod test_body;
pub mod test_date;
pub mod test_duplicate;
pub mod test_envelope;
pub mod test_environment;
pub mod test_exists;
pub mod test_extlists;
pub mod test_hasflag;
pub mod test_header;
pub mod test_ihave;
pub mod test_mailbox;
pub mod test_mailboxid;
pub mod test_notify;
pub mod test_size;
pub mod test_spamtest;
pub mod test_specialuse;
pub mod test_string;
@@ -0,0 +1,175 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::compiler::{
CompileError, Value,
grammar::{Capability, Comparator, instruction::CompilerState, test::Test},
lexer::{Token, word::Word},
};
use crate::compiler::grammar::{AddressPart, MatchType};
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(
any(test, feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
pub(crate) struct TestAddress {
pub header_list: Vec<Value>,
pub key_list: Vec<Value>,
pub address_part: AddressPart,
pub match_type: MatchType,
pub comparator: Comparator,
pub index: Option<i32>,
pub mime_anychild: bool,
pub is_not: bool,
}
impl CompilerState<'_> {
pub(crate) fn parse_test_address(&mut self) -> Result<Test, CompileError> {
let mut address_part = AddressPart::All;
let mut match_type = MatchType::Is;
let mut comparator = Comparator::AsciiCaseMap;
let mut header_list = None;
let mut key_list;
let mut index = None;
let mut index_last = false;
let mut mime = false;
let mut mime_anychild = false;
loop {
let token_info = self.tokens.unwrap_next()?;
match token_info.token {
Token::Tag(
word @ (Word::LocalPart
| Word::Domain
| Word::All
| Word::User
| Word::Detail
| Word::Name),
) => {
self.validate_argument(
1,
if matches!(word, Word::User | Word::Detail) {
Capability::SubAddress.into()
} else {
None
},
token_info.line_num,
token_info.line_pos,
)?;
address_part = word.into();
}
Token::Tag(
word @ (Word::Is
| Word::Contains
| Word::Matches
| Word::Value
| Word::Count
| Word::Regex
| Word::List),
) => {
self.validate_argument(
2,
match word {
Word::Value | Word::Count => Capability::Relational.into(),
Word::Regex => Capability::Regex.into(),
Word::List => Capability::ExtLists.into(),
_ => None,
},
token_info.line_num,
token_info.line_pos,
)?;
match_type = self.parse_match_type(word)?;
}
Token::Tag(Word::Comparator) => {
self.validate_argument(3, None, token_info.line_num, token_info.line_pos)?;
comparator = self.parse_comparator()?;
}
Token::Tag(Word::Index) => {
self.validate_argument(
4,
Capability::Index.into(),
token_info.line_num,
token_info.line_pos,
)?;
index = (self.tokens.expect_number(u16::MAX as usize)? as i32).into();
}
Token::Tag(Word::Last) => {
self.validate_argument(
5,
Capability::Index.into(),
token_info.line_num,
token_info.line_pos,
)?;
index_last = true;
}
Token::Tag(Word::Mime) => {
self.validate_argument(
6,
Capability::Mime.into(),
token_info.line_num,
token_info.line_pos,
)?;
mime = true;
}
Token::Tag(Word::AnyChild) => {
self.validate_argument(
7,
Capability::Mime.into(),
token_info.line_num,
token_info.line_pos,
)?;
mime_anychild = true;
}
_ => {
if header_list.is_none() {
header_list = self.parse_strings_token(token_info)?.into();
} else {
key_list = self.parse_strings_token(token_info)?;
break;
}
}
}
}
if !mime && mime_anychild {
return Err(self.tokens.unwrap_next()?.missing_tag(":mime"));
}
self.validate_match(&match_type, &mut key_list)?;
Ok(Test::Address(TestAddress {
header_list: header_list.unwrap(),
key_list,
address_part,
match_type,
comparator,
index: if index_last { index.map(|i| -i) } else { index },
mime_anychild,
is_not: false,
}))
}
}
impl From<Word> for AddressPart {
fn from(word: Word) -> Self {
match word {
Word::LocalPart => AddressPart::LocalPart,
Word::Domain => AddressPart::Domain,
Word::All => AddressPart::All,
Word::User => AddressPart::User,
Word::Detail => AddressPart::Detail,
Word::Name => AddressPart::Name,
_ => unreachable!(),
}
}
}
+118
View File
@@ -0,0 +1,118 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::compiler::{
CompileError, Value,
grammar::{Capability, Comparator, instruction::CompilerState},
lexer::{Token, word::Word},
};
use crate::compiler::grammar::{MatchType, test::Test};
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(
any(test, feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
pub(crate) struct TestBody {
pub key_list: Vec<Value>,
pub body_transform: BodyTransform,
pub match_type: MatchType,
pub comparator: Comparator,
pub include_subject: bool,
pub is_not: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(
any(test, feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
pub(crate) enum BodyTransform {
Raw,
Content(Vec<Value>),
Text,
}
impl CompilerState<'_> {
pub(crate) fn parse_test_body(&mut self) -> Result<Test, CompileError> {
let mut body_transform = BodyTransform::Text;
let mut match_type = MatchType::Is;
let mut comparator = Comparator::AsciiCaseMap;
let mut key_list;
let mut include_subject = false;
loop {
let token_info = self.tokens.unwrap_next()?;
match token_info.token {
Token::Tag(Word::Raw) => {
self.validate_argument(1, None, token_info.line_num, token_info.line_pos)?;
body_transform = BodyTransform::Raw;
}
Token::Tag(Word::Text) => {
self.validate_argument(1, None, token_info.line_num, token_info.line_pos)?;
body_transform = BodyTransform::Text;
}
Token::Tag(Word::Content) => {
self.validate_argument(1, None, token_info.line_num, token_info.line_pos)?;
body_transform = BodyTransform::Content(self.parse_strings(false)?);
}
Token::Tag(Word::Subject) => {
self.validate_argument(4, None, token_info.line_num, token_info.line_pos)?;
include_subject = true;
}
Token::Tag(
word @ (Word::Is
| Word::Contains
| Word::Matches
| Word::Value
| Word::Count
| Word::Regex),
) => {
self.validate_argument(
2,
match word {
Word::Value | Word::Count => Capability::Relational.into(),
Word::Regex => Capability::Regex.into(),
Word::List => Capability::ExtLists.into(),
_ => None,
},
token_info.line_num,
token_info.line_pos,
)?;
match_type = self.parse_match_type(word)?;
}
Token::Tag(Word::Comparator) => {
self.validate_argument(3, None, token_info.line_num, token_info.line_pos)?;
comparator = self.parse_comparator()?;
}
_ => {
key_list = self.parse_strings_token(token_info)?;
break;
}
}
}
self.validate_match(&match_type, &mut key_list)?;
Ok(Test::Body(TestBody {
key_list,
body_transform,
match_type,
comparator,
include_subject,
is_not: false,
}))
}
}
+368
View File
@@ -0,0 +1,368 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use mail_parser::HeaderName;
use crate::compiler::{
CompileError, ErrorType, Number, Value,
grammar::{Capability, Comparator, instruction::CompilerState},
lexer::{StringConstant, Token, word::Word},
};
use crate::compiler::grammar::{MatchType, test::Test};
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(
any(test, feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
pub(crate) struct TestDate {
pub header_name: Value,
pub key_list: Vec<Value>,
pub match_type: MatchType,
pub comparator: Comparator,
pub index: Option<i32>,
pub zone: Zone,
pub date_part: DatePart,
pub mime_anychild: bool,
pub is_not: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(
any(test, feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
pub(crate) struct TestCurrentDate {
pub zone: Option<i64>,
pub match_type: MatchType,
pub comparator: Comparator,
pub date_part: DatePart,
pub key_list: Vec<Value>,
pub is_not: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(
any(test, feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
pub(crate) enum Zone {
Time(i64),
Original,
Local,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(
any(test, feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
pub(crate) enum DatePart {
Year,
Month,
Day,
Date,
Julian,
Hour,
Minute,
Second,
Time,
Iso8601,
Std11,
Zone,
Weekday,
}
impl CompilerState<'_> {
pub(crate) fn parse_test_date(&mut self) -> Result<Test, CompileError> {
let mut match_type = MatchType::Is;
let mut comparator = Comparator::AsciiCaseMap;
let mut header_name = None;
let mut key_list;
let mut index = None;
let mut index_last = false;
let mut zone = Zone::Local;
let mut date_part = None;
let mut mime = false;
let mut mime_anychild = false;
loop {
let token_info = self.tokens.unwrap_next()?;
match token_info.token {
Token::Tag(
word @ (Word::Is
| Word::Contains
| Word::Matches
| Word::Value
| Word::Count
| Word::Regex
| Word::List),
) => {
self.validate_argument(
1,
match word {
Word::Value | Word::Count => Capability::Relational.into(),
Word::Regex => Capability::Regex.into(),
Word::List => Capability::ExtLists.into(),
_ => None,
},
token_info.line_num,
token_info.line_pos,
)?;
match_type = self.parse_match_type(word)?;
}
Token::Tag(Word::Comparator) => {
self.validate_argument(2, None, token_info.line_num, token_info.line_pos)?;
comparator = self.parse_comparator()?;
}
Token::Tag(Word::Index) => {
self.validate_argument(
3,
Capability::Index.into(),
token_info.line_num,
token_info.line_pos,
)?;
index = (self.tokens.expect_number(u16::MAX as usize)? as i32).into();
}
Token::Tag(Word::Last) => {
self.validate_argument(
4,
Capability::Index.into(),
token_info.line_num,
token_info.line_pos,
)?;
index_last = true;
}
Token::Tag(Word::Mime) => {
self.validate_argument(
5,
Capability::Mime.into(),
token_info.line_num,
token_info.line_pos,
)?;
mime = true;
}
Token::Tag(Word::AnyChild) => {
self.validate_argument(
6,
Capability::Mime.into(),
token_info.line_num,
token_info.line_pos,
)?;
mime_anychild = true;
}
Token::Tag(Word::OriginalZone) => {
self.validate_argument(7, None, token_info.line_num, token_info.line_pos)?;
zone = Zone::Original;
}
Token::Tag(Word::Zone) => {
self.validate_argument(7, None, token_info.line_num, token_info.line_pos)?;
zone = Zone::Time(self.parse_timezone()?);
}
_ => {
if header_name.is_none() {
let header = self.parse_string_token(token_info)?;
if let Value::Text(header_name) = &header
&& HeaderName::parse(header_name.as_ref()).is_none()
{
return Err(self
.tokens
.unwrap_next()?
.custom(ErrorType::InvalidHeaderName));
}
header_name = header.into();
} else if date_part.is_none() {
if let Token::StringConstant(string) = &token_info.token
&& let Some(date_part_) =
lookup_date_part(&string.to_string().to_ascii_lowercase())
{
date_part = date_part_.into();
continue;
}
return Err(token_info.expected("valid date part"));
} else {
key_list = self.parse_strings_token(token_info)?;
break;
}
}
}
}
if !mime && mime_anychild {
return Err(self.tokens.unwrap_next()?.missing_tag(":mime"));
}
self.validate_match(&match_type, &mut key_list)?;
Ok(Test::Date(TestDate {
header_name: header_name.unwrap(),
key_list,
date_part: date_part.unwrap(),
match_type,
comparator,
index: if index_last { index.map(|i| -i) } else { index },
zone,
mime_anychild,
is_not: false,
}))
}
pub(crate) fn parse_test_currentdate(&mut self) -> Result<Test, CompileError> {
let mut match_type = MatchType::Is;
let mut comparator = Comparator::AsciiCaseMap;
let mut key_list;
let mut zone = None;
let mut date_part = None;
loop {
let token_info = self.tokens.unwrap_next()?;
match token_info.token {
Token::Tag(
word @ (Word::Is
| Word::Contains
| Word::Matches
| Word::Value
| Word::Count
| Word::Regex
| Word::List),
) => {
self.validate_argument(
1,
match word {
Word::Value | Word::Count => Capability::Relational.into(),
Word::Regex => Capability::Regex.into(),
Word::List => Capability::ExtLists.into(),
_ => None,
},
token_info.line_num,
token_info.line_pos,
)?;
match_type = self.parse_match_type(word)?;
}
Token::Tag(Word::Comparator) => {
self.validate_argument(2, None, token_info.line_num, token_info.line_pos)?;
comparator = self.parse_comparator()?;
}
Token::Tag(Word::Zone) => {
self.validate_argument(3, None, token_info.line_num, token_info.line_pos)?;
zone = self.parse_timezone()?.into();
}
_ => {
if date_part.is_none() {
if let Token::StringConstant(string) = &token_info.token
&& let Some(date_part_) =
lookup_date_part(&string.to_string().to_ascii_lowercase())
{
date_part = date_part_.into();
continue;
}
return Err(token_info.expected("valid date part"));
} else {
key_list = self.parse_strings_token(token_info)?;
break;
}
}
}
}
self.validate_match(&match_type, &mut key_list)?;
Ok(Test::CurrentDate(TestCurrentDate {
key_list,
date_part: date_part.unwrap(),
match_type,
comparator,
zone,
is_not: false,
}))
}
pub(crate) fn parse_timezone(&mut self) -> Result<i64, CompileError> {
let token_info = self.tokens.unwrap_next()?;
if let Token::StringConstant(value) = &token_info.token {
let timezone = match value {
StringConstant::String(value) => value.parse::<i64>().unwrap_or(i64::MAX),
StringConstant::Number(Number::Integer(n)) => *n,
StringConstant::Number(Number::Float(n)) => *n as i64,
};
return match timezone {
0..=1400 => Ok((timezone / 100 * 3600) + (timezone % 100 * 60)),
-1200..=-1 => Ok((timezone / 100 * 3600) - (-timezone % 100 * 60)),
_ => Err(token_info.expected("invalid timezone")),
};
}
Err(token_info.expected("string containing time zone"))
}
}
/*
"year" => the year, "0000" .. "9999".
"month" => the month, "01" .. "12".
"day" => the day, "01" .. "31".
"date" => the date in "yyyy-mm-dd" format.
"julian" => the Modified Julian Day, that is, the date
expressed as an integer number of days since
00:00 UTC on November 17, 1858 (using the Gregorian
calendar). This corresponds to the regular
Julian Day minus 2400000.5. Sample routines to
convert to and from modified Julian dates are
given in Appendix A.
"hour" => the hour, "00" .. "23".
"minute" => the minute, "00" .. "59".
"second" => the second, "00" .. "60".
"time" => the time in "hh:mm:ss" format.
"iso8601" => the date and time in restricted ISO 8601 format.
"std11" => the date and time in a format appropriate
for use in a Date: header field [RFC2822].
"zone" => the time zone in use. If the user specified a
time zone with ":zone", "zone" will
contain that value. If :originalzone is specified
this value will be the original zone specified
in the date-time value. If neither argument is
specified the value will be the server's default
time zone in offset format "+hhmm" or "-hhmm". An
offset of 0 (Zulu) always has a positive sign.
"weekday" => the day of the week expressed as an integer between
"0" and "6". "0" is Sunday, "1" is Monday, etc.
*/
fn lookup_date_part(input: &str) -> Option<DatePart> {
hashify::tiny_map!(
input.as_bytes(),
"year" => DatePart::Year,
"month" => DatePart::Month,
"day" => DatePart::Day,
"date" => DatePart::Date,
"julian" => DatePart::Julian,
"hour" => DatePart::Hour,
"minute" => DatePart::Minute,
"second" => DatePart::Second,
"time" => DatePart::Time,
"iso8601" => DatePart::Iso8601,
"std11" => DatePart::Std11,
"zone" => DatePart::Zone,
"weekday" => DatePart::Weekday,
)
}
@@ -0,0 +1,118 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use mail_parser::HeaderName;
use crate::compiler::{
CompileError, ErrorType, Value,
grammar::instruction::{CompilerState, MapLocalVars},
lexer::{Token, word::Word},
};
use crate::compiler::grammar::test::Test;
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(
any(test, feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
pub(crate) struct TestDuplicate {
pub handle: Option<Value>,
pub dup_match: DupMatch,
pub seconds: Option<u64>,
pub last: bool,
pub is_not: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(
any(test, feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
pub(crate) enum DupMatch {
Header(Value),
UniqueId(Value),
Default,
}
impl CompilerState<'_> {
pub(crate) fn parse_test_duplicate(&mut self) -> Result<Test, CompileError> {
let mut handle = None;
let mut dup_match = DupMatch::Default;
let mut seconds = None;
let mut last = false;
while let Some(token_info) = self.tokens.peek() {
let token_info = token_info?;
let line_num = token_info.line_num;
let line_pos = token_info.line_pos;
match token_info.token {
Token::Tag(Word::Handle) => {
self.validate_argument(1, None, line_num, line_pos)?;
self.tokens.next();
handle = self.parse_string()?.into();
}
Token::Tag(Word::Header) => {
self.validate_argument(2, None, line_num, line_pos)?;
self.tokens.next();
let header = self.parse_string()?;
if let Value::Text(header_name) = &header
&& HeaderName::parse(header_name.as_ref()).is_none()
{
return Err(self
.tokens
.unwrap_next()?
.custom(ErrorType::InvalidHeaderName));
}
dup_match = DupMatch::Header(header);
}
Token::Tag(Word::UniqueId) => {
self.validate_argument(2, None, line_num, line_pos)?;
self.tokens.next();
dup_match = DupMatch::UniqueId(self.parse_string()?);
}
Token::Tag(Word::Seconds) => {
self.validate_argument(3, None, line_num, line_pos)?;
self.tokens.next();
seconds = (self.tokens.expect_number(u64::MAX as usize)? as u64).into();
}
Token::Tag(Word::Last) => {
self.validate_argument(4, None, line_num, line_pos)?;
self.tokens.next();
last = true;
}
_ => break,
}
}
Ok(Test::Duplicate(TestDuplicate {
handle,
dup_match,
seconds,
last,
is_not: false,
}))
}
}
impl MapLocalVars for DupMatch {
fn map_local_vars(&mut self, last_id: usize) {
match self {
DupMatch::Header(header) => header.map_local_vars(last_id),
DupMatch::UniqueId(unique_id) => unique_id.map_local_vars(last_id),
DupMatch::Default => {}
}
}
}
@@ -0,0 +1,236 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
Envelope,
compiler::{
CompileError, ErrorType, Value,
grammar::{Capability, Comparator, instruction::CompilerState},
lexer::{Token, word::Word},
},
};
use crate::compiler::grammar::{AddressPart, MatchType, test::Test};
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(
any(test, feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
pub(crate) struct TestEnvelope {
pub envelope_list: Vec<Envelope>,
pub key_list: Vec<Value>,
pub address_part: AddressPart,
pub match_type: MatchType,
pub comparator: Comparator,
pub zone: Option<i64>,
pub is_not: bool,
}
impl CompilerState<'_> {
pub(crate) fn parse_test_envelope(&mut self) -> Result<Test, CompileError> {
let mut address_part = AddressPart::All;
let mut match_type = MatchType::Is;
let mut comparator = Comparator::AsciiCaseMap;
let mut envelope_list = None;
let mut key_list;
let mut zone = None;
loop {
let mut token_info = self.tokens.unwrap_next()?;
match token_info.token {
Token::Tag(
word @ (Word::LocalPart | Word::Domain | Word::All | Word::User | Word::Detail),
) => {
self.validate_argument(
1,
if matches!(word, Word::User | Word::Detail) {
Capability::SubAddress.into()
} else {
None
},
token_info.line_num,
token_info.line_pos,
)?;
address_part = word.into();
}
Token::Tag(
word @ (Word::Is
| Word::Contains
| Word::Matches
| Word::Value
| Word::Count
| Word::Regex
| Word::List),
) => {
self.validate_argument(
2,
match word {
Word::Value | Word::Count => Capability::Relational.into(),
Word::Regex => Capability::Regex.into(),
Word::List => Capability::ExtLists.into(),
_ => None,
},
token_info.line_num,
token_info.line_pos,
)?;
match_type = self.parse_match_type(word)?;
}
Token::Tag(Word::Comparator) => {
self.validate_argument(3, None, token_info.line_num, token_info.line_pos)?;
comparator = self.parse_comparator()?;
}
Token::Tag(Word::Zone) => {
self.validate_argument(
4,
Capability::EnvelopeDeliverBy.into(),
token_info.line_num,
token_info.line_pos,
)?;
zone = self.parse_timezone()?.into();
}
_ => {
if envelope_list.is_none() {
let mut envelopes = Vec::new();
let line_num = token_info.line_num;
let line_pos = token_info.line_pos;
match token_info.token {
Token::StringConstant(s) => {
match Envelope::try_from(s.into_string().to_ascii_lowercase()) {
Ok(envelope) => {
envelopes.push(envelope);
}
Err(invalid) => {
token_info.token = Token::Comma;
return Err(
token_info.custom(ErrorType::InvalidEnvelope(invalid))
);
}
}
}
Token::BracketOpen => loop {
let mut token_info = self.tokens.unwrap_next()?;
match token_info.token {
Token::StringConstant(s) => {
match Envelope::try_from(
s.into_string().to_ascii_lowercase(),
) {
Ok(envelope) => {
if !envelopes.contains(&envelope) {
envelopes.push(envelope);
}
}
Err(invalid) => {
token_info.token = Token::Comma;
return Err(token_info
.custom(ErrorType::InvalidEnvelope(invalid)));
}
}
}
Token::Comma => (),
Token::BracketClose if !envelopes.is_empty() => break,
_ => return Err(token_info.expected("constant string")),
}
},
_ => return Err(token_info.expected("constant string")),
}
for envelope in &envelopes {
match envelope {
Envelope::ByTimeAbsolute
| Envelope::ByTimeRelative
| Envelope::ByMode
| Envelope::ByTrace => {
self.validate_argument(
0,
Capability::EnvelopeDeliverBy.into(),
line_num,
line_pos,
)?;
}
Envelope::Notify
| Envelope::Orcpt
| Envelope::Ret
| Envelope::Envid => {
self.validate_argument(
0,
Capability::EnvelopeDsn.into(),
line_num,
line_pos,
)?;
}
_ => (),
}
}
envelope_list = envelopes.into();
} else {
key_list = self.parse_strings_token(token_info)?;
break;
}
}
}
}
self.validate_match(&match_type, &mut key_list)?;
Ok(Test::Envelope(TestEnvelope {
envelope_list: envelope_list.unwrap(),
key_list,
address_part,
match_type,
comparator,
zone,
is_not: false,
}))
}
}
impl TryFrom<String> for Envelope {
type Error = String;
fn try_from(value: String) -> Result<Self, Self::Error> {
if let Some(envelope) = lookup_envelope(&value) {
Ok(envelope)
} else {
Err(value)
}
}
}
impl<'x> TryFrom<&'x str> for Envelope {
type Error = &'x str;
fn try_from(value: &'x str) -> Result<Self, Self::Error> {
if let Some(envelope) = lookup_envelope(value) {
Ok(envelope)
} else {
Err(value)
}
}
}
fn lookup_envelope(input: &str) -> Option<Envelope> {
hashify::tiny_map!(
input.as_bytes(),
"from" => Envelope::From,
"to" => Envelope::To,
"bytimeabsolute" => Envelope::ByTimeAbsolute,
"bytimerelative" => Envelope::ByTimeRelative,
"bymode" => Envelope::ByMode,
"bytrace" => Envelope::ByTrace,
"notify" => Envelope::Notify,
"orcpt" => Envelope::Orcpt,
"ret" => Envelope::Ret,
"envid" => Envelope::Envid,
)
}
@@ -0,0 +1,80 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::compiler::{
CompileError, Value, VariableType,
grammar::{Capability, Comparator, instruction::CompilerState},
lexer::{Token, word::Word},
};
use crate::compiler::grammar::{MatchType, test::Test};
use super::test_string::TestString;
impl CompilerState<'_> {
pub(crate) fn parse_test_environment(&mut self) -> Result<Test, CompileError> {
let mut match_type = MatchType::Is;
let mut comparator = Comparator::AsciiCaseMap;
let mut name = None;
let mut key_list;
loop {
let token_info = self.tokens.unwrap_next()?;
match token_info.token {
Token::Tag(
word @ (Word::Is
| Word::Contains
| Word::Matches
| Word::Value
| Word::Count
| Word::Regex),
) => {
self.validate_argument(
1,
match word {
Word::Value | Word::Count => Capability::Relational.into(),
Word::Regex => Capability::Regex.into(),
Word::List => Capability::ExtLists.into(),
_ => None,
},
token_info.line_num,
token_info.line_pos,
)?;
match_type = self.parse_match_type(word)?;
}
Token::Tag(Word::Comparator) => {
self.validate_argument(2, None, token_info.line_num, token_info.line_pos)?;
comparator = self.parse_comparator()?;
}
_ => {
if name.is_none() {
if let Token::StringConstant(s) = token_info.token {
name = Value::Variable(VariableType::Environment(
s.into_string().to_lowercase(),
))
.into();
} else {
return Err(token_info.expected("environment variable"));
}
} else {
key_list = self.parse_strings_token(token_info)?;
break;
}
}
}
}
self.validate_match(&match_type, &mut key_list)?;
Ok(Test::Environment(TestString {
source: vec![name.unwrap()],
key_list,
match_type,
comparator,
is_not: false,
}))
}
}
@@ -0,0 +1,87 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use mail_parser::HeaderName;
use crate::compiler::{
CompileError, ErrorType, Value,
grammar::{Capability, instruction::CompilerState},
lexer::{Token, word::Word},
};
use crate::compiler::grammar::test::Test;
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(
any(test, feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
pub(crate) struct TestExists {
pub header_names: Vec<Value>,
pub mime_anychild: bool,
pub is_not: bool,
}
impl CompilerState<'_> {
pub(crate) fn parse_test_exists(&mut self) -> Result<Test, CompileError> {
let mut header_names = None;
let mut mime = false;
let mut mime_anychild = false;
while header_names.is_none() {
let token_info = self.tokens.unwrap_next()?;
match token_info.token {
Token::Tag(Word::Mime) => {
self.validate_argument(
1,
Capability::Mime.into(),
token_info.line_num,
token_info.line_pos,
)?;
mime = true;
}
Token::Tag(Word::AnyChild) => {
self.validate_argument(
2,
Capability::Mime.into(),
token_info.line_num,
token_info.line_pos,
)?;
mime_anychild = true;
}
_ => {
let headers = self.parse_strings_token(token_info)?;
for header in &headers {
if let Value::Text(header_name) = &header
&& HeaderName::parse(header_name.as_ref()).is_none()
{
return Err(self
.tokens
.unwrap_next()?
.custom(ErrorType::InvalidHeaderName));
}
}
header_names = headers.into();
}
}
}
if !mime && mime_anychild {
return Err(self.tokens.unwrap_next()?.missing_tag(":mime"));
}
Ok(Test::Exists(TestExists {
header_names: header_names.unwrap(),
mime_anychild,
is_not: false,
}))
}
}
@@ -0,0 +1,34 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::compiler::CompileError;
use crate::compiler::Value;
use crate::compiler::grammar::instruction::CompilerState;
use crate::compiler::grammar::test::Test;
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(
any(test, feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
pub(crate) struct TestValidExtList {
pub list_names: Vec<Value>,
pub is_not: bool,
}
impl CompilerState<'_> {
pub(crate) fn parse_test_valid_ext_list(&mut self) -> Result<Test, CompileError> {
Ok(Test::ValidExtList(TestValidExtList {
list_names: self.parse_strings(false)?,
is_not: false,
}))
}
}
@@ -0,0 +1,146 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::compiler::{
CompileError, ErrorType, Value, VariableType,
grammar::{Capability, Comparator, instruction::CompilerState},
lexer::{Token, tokenizer::TokenInfo, word::Word},
};
use crate::compiler::grammar::{MatchType, test::Test};
/*
Usage: hasflag [MATCH-TYPE] [COMPARATOR]
[<variable-list: string-list>]
<list-of-flags: string-list>
*/
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(
any(test, feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
pub(crate) struct TestHasFlag {
pub comparator: Comparator,
pub match_type: MatchType,
pub variable_list: Vec<VariableType>,
pub flags: Vec<Value>,
pub is_not: bool,
}
impl CompilerState<'_> {
pub(crate) fn parse_test_hasflag(&mut self) -> Result<Test, CompileError> {
let mut match_type = MatchType::Is;
let mut comparator = Comparator::AsciiCaseMap;
let mut is_local = false;
let mut maybe_variables;
loop {
let token_info = self.tokens.unwrap_next()?;
match token_info.token {
Token::Tag(
word @ (Word::Is
| Word::Contains
| Word::Matches
| Word::Value
| Word::Count
| Word::Regex),
) => {
self.validate_argument(
1,
match word {
Word::Value | Word::Count => Capability::Relational.into(),
Word::Regex => Capability::Regex.into(),
Word::List => Capability::ExtLists.into(),
_ => None,
},
token_info.line_num,
token_info.line_pos,
)?;
match_type = self.parse_match_type(word)?;
}
Token::Tag(Word::Comparator) => {
self.validate_argument(2, None, token_info.line_num, token_info.line_pos)?;
comparator = self.parse_comparator()?;
}
Token::Tag(Word::Local) => {
is_local = true;
}
_ => {
maybe_variables = self.parse_strings_token(token_info)?;
break;
}
}
}
match self.tokens.peek() {
Some(Ok(TokenInfo {
token: Token::StringConstant(_) | Token::StringVariable(_) | Token::BracketOpen,
line_num,
line_pos,
})) => {
if !maybe_variables.is_empty() {
let line_num = *line_num;
let line_pos = *line_pos;
let mut variable_list = Vec::with_capacity(maybe_variables.len());
for variable in maybe_variables {
match variable {
Value::Text(var_name) => {
variable_list.push(
self.register_variable(var_name.to_string(), is_local)
.map_err(|error_type| CompileError {
line_num,
line_pos,
error_type,
})?,
);
}
_ => {
return Err(self
.tokens
.unwrap_next()?
.custom(ErrorType::ExpectedConstantString));
}
}
}
let mut flags = self.parse_strings(false)?;
self.validate_match(&match_type, &mut flags)?;
Ok(Test::HasFlag(TestHasFlag {
comparator,
match_type,
variable_list,
flags,
is_not: false,
}))
} else {
Err(self
.tokens
.unwrap_next()?
.custom(ErrorType::ExpectedConstantString))
}
}
_ => {
self.validate_match(&match_type, &mut maybe_variables)?;
Ok(Test::HasFlag(TestHasFlag {
comparator,
match_type,
variable_list: Vec::new(),
flags: maybe_variables,
is_not: false,
}))
}
}
}
}
@@ -0,0 +1,181 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use mail_parser::HeaderName;
use crate::compiler::{
CompileError, ErrorType, Value,
grammar::{
Capability, Comparator,
actions::action_mime::MimeOpts,
instruction::{CompilerState, MapLocalVars},
},
lexer::{Token, word::Word},
};
use crate::compiler::grammar::{MatchType, test::Test};
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(
any(test, feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
pub(crate) struct TestHeader {
pub header_list: Vec<Value>,
pub key_list: Vec<Value>,
pub match_type: MatchType,
pub comparator: Comparator,
pub index: Option<i32>,
pub mime_opts: MimeOpts<Value>,
pub mime_anychild: bool,
pub is_not: bool,
}
impl CompilerState<'_> {
pub(crate) fn parse_test_header(&mut self) -> Result<Test, CompileError> {
let mut match_type = MatchType::Is;
let mut comparator = Comparator::AsciiCaseMap;
let mut header_list = None;
let mut key_list;
let mut index = None;
let mut index_last = false;
let mut mime = false;
let mut mime_opts = MimeOpts::None;
let mut mime_anychild = false;
loop {
let token_info = self.tokens.unwrap_next()?;
match token_info.token {
Token::Tag(
word @ (Word::Is
| Word::Contains
| Word::Matches
| Word::Value
| Word::Count
| Word::Regex
| Word::List),
) => {
self.validate_argument(
1,
match word {
Word::Value | Word::Count => Capability::Relational.into(),
Word::Regex => Capability::Regex.into(),
Word::List => Capability::ExtLists.into(),
_ => None,
},
token_info.line_num,
token_info.line_pos,
)?;
match_type = self.parse_match_type(word)?;
}
Token::Tag(Word::Comparator) => {
self.validate_argument(2, None, token_info.line_num, token_info.line_pos)?;
comparator = self.parse_comparator()?;
}
Token::Tag(Word::Index) => {
self.validate_argument(
3,
Capability::Index.into(),
token_info.line_num,
token_info.line_pos,
)?;
index = (self.tokens.expect_number(u16::MAX as usize)? as i32).into();
}
Token::Tag(Word::Last) => {
self.validate_argument(
4,
Capability::Index.into(),
token_info.line_num,
token_info.line_pos,
)?;
index_last = true;
}
Token::Tag(Word::Mime) => {
self.validate_argument(
5,
Capability::Mime.into(),
token_info.line_num,
token_info.line_pos,
)?;
mime = true;
}
Token::Tag(Word::AnyChild) => {
self.validate_argument(
6,
Capability::Mime.into(),
token_info.line_num,
token_info.line_pos,
)?;
mime_anychild = true;
}
Token::Tag(
word @ (Word::Type | Word::Subtype | Word::ContentType | Word::Param),
) => {
self.validate_argument(
7,
Capability::Mime.into(),
token_info.line_num,
token_info.line_pos,
)?;
mime_opts = self.parse_mimeopts(word)?;
}
_ => {
if header_list.is_none() {
let headers = self.parse_strings_token(token_info)?;
for header in &headers {
if let Value::Text(header_name) = &header
&& HeaderName::parse(header_name.as_ref()).is_none()
{
return Err(self
.tokens
.unwrap_next()?
.custom(ErrorType::InvalidHeaderName));
}
}
header_list = headers.into();
} else {
key_list = self.parse_strings_token(token_info)?;
break;
}
}
}
}
if !mime && (mime_anychild || mime_opts != MimeOpts::None) {
return Err(self.tokens.unwrap_next()?.missing_tag(":mime"));
}
self.validate_match(&match_type, &mut key_list)?;
Ok(Test::Header(TestHeader {
header_list: header_list.unwrap(),
key_list,
match_type,
comparator,
index: if index_last { index.map(|i| -i) } else { index },
mime_opts,
mime_anychild,
is_not: false,
}))
}
}
impl MapLocalVars for MimeOpts<Value> {
fn map_local_vars(&mut self, last_id: usize) {
if let MimeOpts::Param(value) = self {
value.map_local_vars(last_id)
}
}
}
@@ -0,0 +1,60 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::compiler::CompileError;
use crate::compiler::Value;
use crate::compiler::grammar::Capability;
use crate::compiler::grammar::instruction::{CompilerState, Instruction};
use crate::compiler::grammar::test::Test;
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(
any(test, feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
pub(crate) struct TestIhave {
pub capabilities: Vec<Capability>,
pub is_not: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(
any(test, feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
pub(crate) struct Error {
pub message: Value,
}
impl CompilerState<'_> {
pub(crate) fn parse_test_ihave(&mut self) -> Result<Test, CompileError> {
Ok(Test::Ihave(TestIhave {
capabilities: self
.parse_static_strings()?
.into_iter()
.map(|n| n.into())
.collect(),
is_not: false,
}))
}
pub(crate) fn parse_error(&mut self) -> Result<(), CompileError> {
let cmd = Instruction::Error(Error {
message: self.parse_string()?,
});
self.instructions.push(cmd);
Ok(())
}
}
@@ -0,0 +1,239 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
Metadata,
compiler::{
CompileError, Value,
grammar::{
Capability, Comparator,
instruction::{CompilerState, MapLocalVars},
},
lexer::{Token, word::Word},
},
};
use crate::compiler::grammar::{MatchType, test::Test};
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(
any(test, feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
pub(crate) struct TestMailboxExists {
pub mailbox_names: Vec<Value>,
pub is_not: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(
any(test, feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
pub(crate) struct TestMetadataExists {
pub mailbox: Option<Value>,
pub annotation_names: Vec<Value>,
pub is_not: bool,
}
/*
metadata [MATCH-TYPE] [COMPARATOR]
<mailbox: string>
<annotation-name: string> <key-list: string-list>
*/
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(
any(test, feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
pub(crate) struct TestMetadata {
pub match_type: MatchType,
pub comparator: Comparator,
pub medatata: Metadata<Value>,
pub key_list: Vec<Value>,
pub is_not: bool,
}
/*
servermetadata [MATCH-TYPE] [COMPARATOR]
<annotation-name: string> <key-list: string-list>
*/
impl CompilerState<'_> {
pub(crate) fn parse_test_mailboxexists(&mut self) -> Result<Test, CompileError> {
Ok(Test::MailboxExists(TestMailboxExists {
mailbox_names: self.parse_strings(false)?,
is_not: false,
}))
}
pub(crate) fn parse_test_metadataexists(&mut self) -> Result<Test, CompileError> {
Ok(Test::MetadataExists(TestMetadataExists {
mailbox: self.parse_string()?.into(),
annotation_names: self.parse_strings(false)?,
is_not: false,
}))
}
pub(crate) fn parse_test_servermetadataexists(&mut self) -> Result<Test, CompileError> {
Ok(Test::MetadataExists(TestMetadataExists {
mailbox: None,
annotation_names: self.parse_strings(false)?,
is_not: false,
}))
}
pub(crate) fn parse_test_metadata(&mut self) -> Result<Test, CompileError> {
let mut match_type = MatchType::Is;
let mut comparator = Comparator::AsciiCaseMap;
let mut mailbox = None;
let mut annotation_name = None;
let mut key_list: Vec<Value>;
loop {
let token_info = self.tokens.unwrap_next()?;
match token_info.token {
Token::Tag(
word @ (Word::Is
| Word::Contains
| Word::Matches
| Word::Value
| Word::Count
| Word::Regex),
) => {
self.validate_argument(
1,
match word {
Word::Value | Word::Count => Capability::Relational.into(),
Word::Regex => Capability::Regex.into(),
Word::List => Capability::ExtLists.into(),
_ => None,
},
token_info.line_num,
token_info.line_pos,
)?;
match_type = self.parse_match_type(word)?;
}
Token::Tag(Word::Comparator) => {
self.validate_argument(2, None, token_info.line_num, token_info.line_pos)?;
comparator = self.parse_comparator()?;
}
_ => {
if mailbox.is_none() {
mailbox = self.parse_string_token(token_info)?.into();
} else if annotation_name.is_none() {
annotation_name = self.parse_string_token(token_info)?.into();
} else {
key_list = self.parse_strings_token(token_info)?;
break;
}
}
}
}
self.validate_match(&match_type, &mut key_list)?;
Ok(Test::Metadata(TestMetadata {
match_type,
comparator,
medatata: Metadata::Mailbox {
name: mailbox.unwrap(),
annotation: annotation_name.unwrap(),
},
key_list,
is_not: false,
}))
}
pub(crate) fn parse_test_servermetadata(&mut self) -> Result<Test, CompileError> {
let mut match_type = MatchType::Is;
let mut comparator = Comparator::AsciiCaseMap;
let mut annotation_name = None;
let mut key_list: Vec<Value>;
loop {
let token_info = self.tokens.unwrap_next()?;
match token_info.token {
Token::Tag(
word @ (Word::Is
| Word::Contains
| Word::Matches
| Word::Value
| Word::Count
| Word::Regex),
) => {
self.validate_argument(
1,
match word {
Word::Value | Word::Count => Capability::Relational.into(),
Word::Regex => Capability::Regex.into(),
Word::List => Capability::ExtLists.into(),
_ => None,
},
token_info.line_num,
token_info.line_pos,
)?;
match_type = self.parse_match_type(word)?;
}
Token::Tag(Word::Comparator) => {
self.validate_argument(2, None, token_info.line_num, token_info.line_pos)?;
comparator = self.parse_comparator()?;
}
_ => {
if annotation_name.is_none() {
annotation_name = self.parse_string_token(token_info)?.into();
} else {
key_list = self.parse_strings_token(token_info)?;
break;
}
}
}
}
self.validate_match(&match_type, &mut key_list)?;
Ok(Test::Metadata(TestMetadata {
match_type,
comparator,
medatata: Metadata::Server {
annotation: annotation_name.unwrap(),
},
key_list,
is_not: false,
}))
}
}
impl MapLocalVars for Metadata<Value> {
fn map_local_vars(&mut self, last_id: usize) {
match self {
Metadata::Mailbox { name, annotation } => {
name.map_local_vars(last_id);
annotation.map_local_vars(last_id);
}
Metadata::Server { annotation } => {
annotation.map_local_vars(last_id);
}
}
}
}
@@ -0,0 +1,34 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::compiler::CompileError;
use crate::compiler::Value;
use crate::compiler::grammar::instruction::CompilerState;
use crate::compiler::grammar::test::Test;
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(
any(test, feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
pub(crate) struct TestMailboxIdExists {
pub mailbox_ids: Vec<Value>,
pub is_not: bool,
}
impl CompilerState<'_> {
pub(crate) fn parse_test_mailboxidexists(&mut self) -> Result<Test, CompileError> {
Ok(Test::MailboxIdExists(TestMailboxIdExists {
mailbox_ids: self.parse_strings(false)?,
is_not: false,
}))
}
}
@@ -0,0 +1,114 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::compiler::{
CompileError, Value,
grammar::{Capability, Comparator, instruction::CompilerState},
lexer::{Token, word::Word},
};
use crate::compiler::grammar::{MatchType, test::Test};
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(
any(test, feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
pub(crate) struct TestNotifyMethodCapability {
pub comparator: Comparator,
pub match_type: MatchType,
pub notification_uri: Value,
pub notification_capability: Value,
pub key_list: Vec<Value>,
pub is_not: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(
any(test, feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
pub(crate) struct TestValidNotifyMethod {
pub notification_uris: Vec<Value>,
pub is_not: bool,
}
impl CompilerState<'_> {
pub(crate) fn parse_test_valid_notify_method(&mut self) -> Result<Test, CompileError> {
Ok(Test::ValidNotifyMethod(TestValidNotifyMethod {
notification_uris: self.parse_strings(false)?,
is_not: false,
}))
}
pub(crate) fn parse_test_notify_method_capability(&mut self) -> Result<Test, CompileError> {
let mut match_type = MatchType::Is;
let mut comparator = Comparator::AsciiCaseMap;
let mut notification_uri = None;
let mut notification_capability = None;
let mut key_list;
loop {
let token_info = self.tokens.unwrap_next()?;
match token_info.token {
Token::Tag(
word @ (Word::Is
| Word::Contains
| Word::Matches
| Word::Value
| Word::Count
| Word::Regex),
) => {
self.validate_argument(
1,
match word {
Word::Value | Word::Count => Capability::Relational.into(),
Word::Regex => Capability::Regex.into(),
Word::List => Capability::ExtLists.into(),
_ => None,
},
token_info.line_num,
token_info.line_pos,
)?;
match_type = self.parse_match_type(word)?;
}
Token::Tag(Word::Comparator) => {
self.validate_argument(2, None, token_info.line_num, token_info.line_pos)?;
comparator = self.parse_comparator()?;
}
_ => {
if notification_uri.is_none() {
notification_uri = self.parse_string_token(token_info)?.into();
} else if notification_capability.is_none() {
notification_capability = self.parse_string_token(token_info)?.into();
} else {
key_list = self.parse_strings_token(token_info)?;
break;
}
}
}
}
self.validate_match(&match_type, &mut key_list)?;
Ok(Test::NotifyMethodCapability(TestNotifyMethodCapability {
key_list,
match_type,
comparator,
notification_uri: notification_uri.unwrap(),
notification_capability: notification_capability.unwrap(),
is_not: false,
}))
}
}
+51
View File
@@ -0,0 +1,51 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::compiler::{
CompileError,
grammar::instruction::CompilerState,
lexer::{Token, word::Word},
};
use crate::compiler::grammar::test::Test;
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(
any(test, feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
pub(crate) struct TestSize {
pub over: bool,
pub limit: usize,
pub is_not: bool,
}
impl CompilerState<'_> {
pub(crate) fn parse_test_size(&mut self) -> Result<Test, CompileError> {
let token_info = self.tokens.unwrap_next()?;
let over = match token_info.token {
Token::Tag(Word::Over) => true,
Token::Tag(Word::Under) => false,
_ => {
return Err(token_info.expected("':over' or ':under'"));
}
};
let token_info = self.tokens.unwrap_next()?;
if let Token::Number(limit) = token_info.token {
Ok(Test::Size(TestSize {
over,
limit,
is_not: false,
}))
} else {
Err(token_info.expected("number"))
}
}
}
@@ -0,0 +1,162 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::compiler::{
CompileError, Value,
grammar::{Capability, Comparator, instruction::CompilerState},
lexer::{Token, word::Word},
};
use crate::compiler::grammar::{MatchType, test::Test};
/*
Usage: spamtest [":percent"] [COMPARATOR] [MATCH-TYPE]
<value: string>
*/
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(
any(test, feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
pub(crate) struct TestSpamTest {
pub value: Value,
pub match_type: MatchType,
pub comparator: Comparator,
pub percent: bool,
pub is_not: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(
any(test, feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
pub(crate) struct TestVirusTest {
pub value: Value,
pub match_type: MatchType,
pub comparator: Comparator,
pub is_not: bool,
}
impl CompilerState<'_> {
pub(crate) fn parse_test_spamtest(&mut self) -> Result<Test, CompileError> {
let mut match_type = MatchType::Is;
let mut comparator = Comparator::AsciiCaseMap;
let mut percent = false;
let value;
loop {
let token_info = self.tokens.unwrap_next()?;
match token_info.token {
Token::Tag(
word @ (Word::Is
| Word::Contains
| Word::Matches
| Word::Value
| Word::Count
| Word::Regex),
) => {
self.validate_argument(
1,
match word {
Word::Value | Word::Count => Capability::Relational.into(),
Word::Regex => Capability::Regex.into(),
Word::List => Capability::ExtLists.into(),
_ => None,
},
token_info.line_num,
token_info.line_pos,
)?;
match_type = self.parse_match_type(word)?;
}
Token::Tag(Word::Comparator) => {
self.validate_argument(2, None, token_info.line_num, token_info.line_pos)?;
comparator = self.parse_comparator()?;
}
Token::Tag(Word::Percent) => {
self.validate_argument(
3,
Capability::SpamTestPlus.into(),
token_info.line_num,
token_info.line_pos,
)?;
percent = true;
}
_ => {
value = self.parse_string_token(token_info)?;
break;
}
}
}
Ok(Test::SpamTest(TestSpamTest {
value,
percent,
match_type,
comparator,
is_not: false,
}))
}
pub(crate) fn parse_test_virustest(&mut self) -> Result<Test, CompileError> {
let mut match_type = MatchType::Is;
let mut comparator = Comparator::AsciiCaseMap;
let value;
loop {
let token_info = self.tokens.unwrap_next()?;
match token_info.token {
Token::Tag(
word @ (Word::Is
| Word::Contains
| Word::Matches
| Word::Value
| Word::Count
| Word::Regex),
) => {
self.validate_argument(
1,
match word {
Word::Value | Word::Count => Capability::Relational.into(),
Word::Regex => Capability::Regex.into(),
Word::List => Capability::ExtLists.into(),
_ => None,
},
token_info.line_num,
token_info.line_pos,
)?;
match_type = self.parse_match_type(word)?;
}
Token::Tag(Word::Comparator) => {
self.validate_argument(2, None, token_info.line_num, token_info.line_pos)?;
comparator = self.parse_comparator()?;
}
_ => {
value = self.parse_string_token(token_info)?;
break;
}
}
}
Ok(Test::VirusTest(TestVirusTest {
value,
match_type,
comparator,
is_not: false,
}))
}
}
@@ -0,0 +1,54 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::compiler::{CompileError, Value, grammar::instruction::CompilerState, lexer::Token};
use crate::compiler::grammar::test::Test;
/*
Usage: specialuse_exists [<mailbox: string>]
<special-use-attrs: string-list>
*/
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(
any(test, feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
pub(crate) struct TestSpecialUseExists {
pub mailbox: Option<Value>,
pub attributes: Vec<Value>,
pub is_not: bool,
}
impl CompilerState<'_> {
pub(crate) fn parse_test_specialuseexists(&mut self) -> Result<Test, CompileError> {
let mut maybe_attributes = self.parse_strings(false)?;
match self.tokens.peek().map(|r| r.map(|t| &t.token)) {
Some(Ok(Token::StringConstant(_) | Token::StringVariable(_) | Token::BracketOpen)) => {
if maybe_attributes.len() == 1 {
Ok(Test::SpecialUseExists(TestSpecialUseExists {
mailbox: maybe_attributes.pop(),
attributes: self.parse_strings(false)?,
is_not: false,
}))
} else {
Err(self.tokens.unwrap_next()?.expected("string"))
}
}
_ => Ok(Test::SpecialUseExists(TestSpecialUseExists {
mailbox: None,
attributes: maybe_attributes,
is_not: false,
})),
}
}
}
@@ -0,0 +1,89 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::compiler::{
CompileError, Value,
grammar::{Capability, Comparator, instruction::CompilerState},
lexer::{Token, word::Word},
};
use crate::compiler::grammar::{MatchType, test::Test};
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(
any(test, feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
pub(crate) struct TestString {
pub match_type: MatchType,
pub comparator: Comparator,
pub source: Vec<Value>,
pub key_list: Vec<Value>,
pub is_not: bool,
}
impl CompilerState<'_> {
pub(crate) fn parse_test_string(&mut self) -> Result<Test, CompileError> {
let mut match_type = MatchType::Is;
let mut comparator = Comparator::AsciiCaseMap;
let mut source = None;
let mut key_list: Vec<Value>;
loop {
let token_info = self.tokens.unwrap_next()?;
match token_info.token {
Token::Tag(
word @ (Word::Is
| Word::Contains
| Word::Matches
| Word::Value
| Word::Count
| Word::Regex
| Word::List),
) => {
self.validate_argument(
1,
match word {
Word::Value | Word::Count => Capability::Relational.into(),
Word::Regex => Capability::Regex.into(),
Word::List => Capability::ExtLists.into(),
_ => None,
},
token_info.line_num,
token_info.line_pos,
)?;
match_type = self.parse_match_type(word)?;
}
Token::Tag(Word::Comparator) => {
self.validate_argument(2, None, token_info.line_num, token_info.line_pos)?;
comparator = self.parse_comparator()?;
}
_ => {
if source.is_none() {
source = self.parse_strings_token(token_info)?.into();
} else {
key_list = self.parse_strings_token(token_info)?;
break;
}
}
}
}
self.validate_match(&match_type, &mut key_list)?;
Ok(Test::String(TestString {
source: source.unwrap(),
key_list,
match_type,
comparator,
is_not: false,
}))
}
}
+90
View File
@@ -0,0 +1,90 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
pub mod string;
pub mod tokenizer;
pub mod word;
use std::{borrow::Cow, fmt::Display};
use self::word::Word;
use super::{Number, Value};
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum Token {
CurlyOpen,
CurlyClose,
BracketOpen,
BracketClose,
ParenthesisOpen,
ParenthesisClose,
Comma,
Semicolon,
StringConstant(StringConstant),
StringVariable(Vec<u8>),
Number(usize),
Identifier(Word),
Tag(Word),
Unknown(String),
Colon,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum StringConstant {
String(String),
Number(Number),
}
impl StringConstant {
pub fn to_string(&'_ self) -> Cow<'_, str> {
match self {
StringConstant::String(s) => s.as_str().into(),
StringConstant::Number(n) => n.to_string().into(),
}
}
pub fn into_string(self) -> String {
match self {
StringConstant::String(s) => s,
StringConstant::Number(n) => n.to_string(),
}
}
}
impl From<StringConstant> for Value {
fn from(value: StringConstant) -> Self {
match value {
StringConstant::String(s) => Value::Text(s.into()),
StringConstant::Number(n) => Value::Number(n),
}
}
}
impl Display for Token {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Token::CurlyOpen => f.write_str("{"),
Token::CurlyClose => f.write_str("}"),
Token::BracketOpen => f.write_str("["),
Token::BracketClose => f.write_str("]"),
Token::ParenthesisOpen => f.write_str("("),
Token::ParenthesisClose => f.write_str(")"),
Token::Comma => f.write_str(","),
Token::Semicolon => f.write_str(";"),
Token::Colon => f.write_str(":"),
Token::Number(n) => write!(f, "{n}"),
Token::Identifier(w) => w.fmt(f),
Token::Tag(t) => write!(f, ":{t}"),
Token::Unknown(s) => f.write_str(s),
Token::StringVariable(s) => f.write_str(&String::from_utf8_lossy(s)),
Token::StringConstant(c) => match c {
StringConstant::String(s) => f.write_str(s),
StringConstant::Number(n) => write!(f, "{n}"),
},
}
}
}
+922
View File
@@ -0,0 +1,922 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::fmt::Display;
use mail_parser::HeaderName;
use crate::{
Envelope, MAX_MATCH_VARIABLES,
compiler::{
ContentTypePart, ErrorType, HeaderPart, HeaderVariable, MessagePart, Number,
ReceivedHostname, ReceivedPart, Value, VariableType,
grammar::{
AddressPart,
expr::{self},
instruction::CompilerState,
},
},
runtime::eval::IntoString,
};
enum State {
None,
Variable,
Encoded {
is_unicode: bool,
initial_buf_size: usize,
},
}
impl CompilerState<'_> {
pub(crate) fn tokenize_string(
&mut self,
bytes: &[u8],
parse_decoded: bool,
) -> Result<Value, ErrorType> {
let mut state = State::None;
let mut items = Vec::with_capacity(3);
let mut last_ch = 0;
let mut var_start_pos = usize::MAX;
let mut var_is_number = true;
let mut var_has_namespace = false;
let mut text_has_digits = true;
let mut text_has_dots = false;
let mut hex_start = usize::MAX;
let mut decode_buf = Vec::with_capacity(bytes.len());
for (pos, &ch) in bytes.iter().enumerate() {
let mut is_var_error = false;
match state {
State::None => match ch {
b'{' if last_ch == b'$' => {
decode_buf.pop();
var_start_pos = pos + 1;
var_is_number = true;
var_has_namespace = false;
state = State::Variable;
}
b'.' => {
if text_has_dots {
text_has_digits = false;
} else {
text_has_dots = true;
}
decode_buf.push(ch);
}
b'0'..=b'9' => {
decode_buf.push(ch);
}
_ => {
text_has_digits = false;
decode_buf.push(ch);
}
},
State::Variable => match ch {
b'a'..=b'z' | b'A'..=b'Z' | b'_' | b'[' | b']' | b'*' | b'-' => {
var_is_number = false;
}
b'.' => {
var_is_number = false;
var_has_namespace = true;
}
b'0'..=b'9' => {}
b'}' if pos > var_start_pos => {
// Add any text before the variable
if !decode_buf.is_empty() {
self.add_value(
&mut items,
&decode_buf,
parse_decoded,
text_has_digits,
text_has_dots,
)?;
decode_buf.clear();
text_has_digits = true;
text_has_dots = false;
}
// Parse variable type
let var_name = std::str::from_utf8(&bytes[var_start_pos..pos]).unwrap();
let var_type = if !var_is_number {
self.parse_variable(var_name, var_has_namespace)
} else {
self.parse_match_variable(var_name)
};
match var_type {
Ok(Some(var)) => items.push(Value::Variable(var)),
Ok(None) => {}
Err(ErrorType::InvalidNamespace(_) | ErrorType::InvalidEnvelope(_)) => {
is_var_error = true;
}
Err(e) => return Err(e),
}
state = State::None;
}
b':' => {
if parse_decoded && !var_has_namespace {
match bytes.get(var_start_pos..pos) {
Some(enc) if enc.eq_ignore_ascii_case(b"hex") => {
state = State::Encoded {
is_unicode: false,
initial_buf_size: decode_buf.len(),
};
}
Some(enc) if enc.eq_ignore_ascii_case(b"unicode") => {
state = State::Encoded {
is_unicode: true,
initial_buf_size: decode_buf.len(),
};
}
_ => {
is_var_error = true;
}
}
} else if var_has_namespace {
var_is_number = false;
} else {
is_var_error = true;
}
}
_ => {
is_var_error = true;
}
},
State::Encoded {
is_unicode,
initial_buf_size,
} => match ch {
b'0'..=b'9' | b'a'..=b'f' | b'A'..=b'F' => {
if hex_start == usize::MAX {
hex_start = pos;
}
}
b' ' | b'\t' | b'\r' | b'\n' | b'}' => {
if hex_start != usize::MAX {
let code = std::str::from_utf8(&bytes[hex_start..pos]).unwrap();
hex_start = usize::MAX;
if !is_unicode {
if let Ok(ch) = u8::from_str_radix(code, 16) {
decode_buf.push(ch);
} else {
is_var_error = true;
}
} else if let Ok(ch) = u32::from_str_radix(code, 16) {
let mut buf = [0; 4];
decode_buf.extend_from_slice(
char::from_u32(ch)
.ok_or(ErrorType::InvalidUnicodeSequence(ch))?
.encode_utf8(&mut buf)
.as_bytes(),
);
} else {
is_var_error = true;
}
}
if ch == b'}' {
if decode_buf.len() != initial_buf_size {
state = State::None;
} else {
is_var_error = true;
}
}
}
_ => {
is_var_error = true;
}
},
}
if is_var_error {
if let State::Encoded {
initial_buf_size, ..
} = state
&& initial_buf_size != decode_buf.len()
{
decode_buf.truncate(initial_buf_size);
}
decode_buf.extend_from_slice(&bytes[var_start_pos - 2..pos + 1]);
hex_start = usize::MAX;
state = State::None;
}
last_ch = ch;
}
match state {
State::Variable => {
decode_buf.extend_from_slice(&bytes[var_start_pos - 2..bytes.len()]);
}
State::Encoded {
initial_buf_size, ..
} => {
if initial_buf_size != decode_buf.len() {
decode_buf.truncate(initial_buf_size);
}
decode_buf.extend_from_slice(&bytes[var_start_pos - 2..bytes.len()]);
}
State::None => (),
}
if !decode_buf.is_empty() {
self.add_value(
&mut items,
&decode_buf,
parse_decoded,
text_has_digits,
text_has_dots,
)?;
}
Ok(match items.len() {
1 => items.pop().unwrap(),
0 => Value::Text(String::new().into()),
_ => Value::List(items),
})
}
fn parse_match_variable(&mut self, var_name: &str) -> Result<Option<VariableType>, ErrorType> {
let num = var_name
.parse()
.map_err(|_| ErrorType::InvalidNumber(var_name.to_string()))?;
if num < MAX_MATCH_VARIABLES as usize {
if self.register_match_var(num) {
let total_vars = num + 1;
if total_vars > self.vars_match_max {
self.vars_match_max = total_vars;
}
Ok(Some(VariableType::Match(num)))
} else {
Ok(None)
}
} else {
Err(ErrorType::InvalidMatchVariable(num))
}
}
pub fn parse_variable(
&self,
var_name: &str,
maybe_namespace: bool,
) -> Result<Option<VariableType>, ErrorType> {
if !maybe_namespace {
if self.is_var_global(var_name) {
Ok(Some(VariableType::Global(var_name.to_string())))
} else if let Some(var_id) = self.get_local_var(var_name) {
Ok(Some(VariableType::Local(var_id)))
} else {
Ok(None)
}
} else {
let var = match var_name.to_lowercase().split_once('.') {
Some(("global" | "t", var_name)) if !var_name.is_empty() => {
VariableType::Global(var_name.to_string())
}
Some(("env", var_name)) if !var_name.is_empty() => {
VariableType::Environment(var_name.to_string())
}
Some(("envelope", var_name)) if !var_name.is_empty() => {
let envelope = match var_name {
"from" => Envelope::From,
"to" => Envelope::To,
"by_time_absolute" => Envelope::ByTimeAbsolute,
"by_time_relative" => Envelope::ByTimeRelative,
"by_mode" => Envelope::ByMode,
"by_trace" => Envelope::ByTrace,
"notify" => Envelope::Notify,
"orcpt" => Envelope::Orcpt,
"ret" => Envelope::Ret,
"envid" => Envelope::Envid,
_ => {
return Err(ErrorType::InvalidEnvelope(var_name.to_string()));
}
};
VariableType::Envelope(envelope)
}
Some(("header", var_name)) if !var_name.is_empty() => {
self.parse_header_variable(var_name)?
}
Some(("body", var_name)) if !var_name.is_empty() => match var_name {
"text" => VariableType::Part(MessagePart::TextBody(false)),
"html" => VariableType::Part(MessagePart::HtmlBody(false)),
"to_text" => VariableType::Part(MessagePart::TextBody(true)),
"to_html" => VariableType::Part(MessagePart::HtmlBody(true)),
_ => return Err(ErrorType::InvalidNamespace(var_name.to_string())),
},
Some(("part", var_name)) if !var_name.is_empty() => match var_name {
"text" => VariableType::Part(MessagePart::Contents),
"raw" => VariableType::Part(MessagePart::Raw),
_ => return Err(ErrorType::InvalidNamespace(var_name.to_string())),
},
None => {
if self.is_var_global(var_name) {
VariableType::Global(var_name.to_string())
} else if let Some(var_id) = self.get_local_var(var_name) {
VariableType::Local(var_id)
} else {
return Ok(None);
}
}
_ => return Err(ErrorType::InvalidNamespace(var_name.to_string())),
};
Ok(Some(var))
}
}
fn parse_header_variable(&self, var_name: &str) -> Result<VariableType, ErrorType> {
#[derive(Debug)]
enum State {
Name,
Index,
Part,
PartIndex,
}
let mut name = vec![];
let mut has_name = false;
let mut has_wildcard = false;
let mut hdr_name = String::new();
let mut hdr_index = String::new();
let mut part = String::new();
let mut part_index = String::new();
let mut state = State::Name;
for ch in var_name.chars() {
match state {
State::Name => match ch {
'[' => {
state = if hdr_index.is_empty() {
State::Index
} else if part.is_empty() {
State::PartIndex
} else {
return Err(ErrorType::InvalidExpression(var_name.to_string()));
};
has_name = true;
}
'.' => {
state = State::Part;
has_name = true;
}
' ' | '\t' | '\r' | '\n' => {}
'*' if !has_wildcard && hdr_name.is_empty() && name.is_empty() => {
has_wildcard = true;
}
':' if !hdr_name.is_empty() && !has_wildcard => {
name.push(
HeaderName::parse(std::mem::take(&mut hdr_name)).ok_or_else(|| {
ErrorType::InvalidExpression(var_name.to_string())
})?,
);
}
_ if !has_name && !has_wildcard => {
hdr_name.push(ch);
}
_ => {
return Err(ErrorType::InvalidExpression(var_name.to_string()));
}
},
State::Index => match ch {
']' => {
state = State::Name;
}
' ' | '\t' | '\r' | '\n' => {}
_ => {
hdr_index.push(ch);
}
},
State::Part => match ch {
'[' => {
state = State::PartIndex;
}
' ' | '\t' | '\r' | '\n' => {}
_ => {
part.push(ch);
}
},
State::PartIndex => match ch {
']' => {
state = State::Name;
}
' ' | '\t' | '\r' | '\n' => {}
_ => {
part_index.push(ch);
}
},
}
}
if !hdr_name.is_empty() {
name.push(
HeaderName::parse(hdr_name)
.ok_or_else(|| ErrorType::InvalidExpression(var_name.to_string()))?,
);
}
if !name.is_empty() || has_wildcard {
Ok(VariableType::Header(HeaderVariable {
name,
part: HeaderPart::try_from(part.as_str())
.map_err(|_| ErrorType::InvalidExpression(var_name.to_string()))?,
index_hdr: match hdr_index.as_str() {
"" => {
if !has_wildcard {
-1
} else {
0
}
}
"*" => 0,
_ => hdr_index
.parse()
.map(|v| if v == 0 { 1 } else { v })
.map_err(|_| ErrorType::InvalidExpression(var_name.to_string()))?,
},
index_part: match part_index.as_str() {
"" => {
if !has_wildcard {
-1
} else {
0
}
}
"*" => 0,
_ => part_index
.parse()
.map(|v| if v == 0 { 1 } else { v })
.map_err(|_| ErrorType::InvalidExpression(var_name.to_string()))?,
},
}))
} else {
Err(ErrorType::InvalidExpression(var_name.to_string()))
}
}
pub fn parse_expr_fnc_or_var(
&self,
var_name: &str,
maybe_namespace: bool,
) -> Result<expr::Token, String> {
match self.parse_variable(var_name, maybe_namespace) {
Ok(Some(var)) => Ok(expr::Token::Variable(var)),
_ => {
if let Some((id, num_args)) = self.compiler.functions.get(var_name) {
Ok(expr::Token::Function {
name: var_name.to_string(),
id: *id,
num_args: *num_args,
})
} else {
Err(format!("Invalid variable or function name {var_name:?}"))
}
}
}
}
#[inline(always)]
fn add_value(
&mut self,
items: &mut Vec<Value>,
buf: &[u8],
parse_decoded: bool,
has_digits: bool,
has_dots: bool,
) -> Result<(), ErrorType> {
if !parse_decoded {
items.push(if has_digits {
if has_dots {
match std::str::from_utf8(buf)
.ok()
.and_then(|v| (v, v.parse::<f64>().ok()?).into())
{
Some((v, n)) if n.to_string() == v => Value::Number(Number::Float(n)),
_ => Value::Text(buf.to_vec().into_string().into()),
}
} else {
match std::str::from_utf8(buf)
.ok()
.and_then(|v| (v, v.parse::<i64>().ok()?).into())
{
Some((v, n)) if n.to_string() == v => Value::Number(Number::Integer(n)),
_ => Value::Text(buf.to_vec().into_string().into()),
}
}
} else {
Value::Text(buf.to_vec().into_string().into())
});
} else {
match self.tokenize_string(buf, false)? {
Value::List(new_items) => items.extend(new_items),
item => items.push(item),
}
}
Ok(())
}
}
impl TryFrom<&str> for HeaderPart {
type Error = ();
fn try_from(value: &str) -> Result<Self, Self::Error> {
let (value, subvalue) = value.split_once('.').unwrap_or((value, ""));
Ok(match value {
"" | "text" => HeaderPart::Text,
// Addresses
"name" => HeaderPart::Address(AddressPart::Name),
"addr" => {
if !subvalue.is_empty() {
HeaderPart::Address(AddressPart::try_from(subvalue)?)
} else {
HeaderPart::Address(AddressPart::All)
}
}
// Content-type
"type" => HeaderPart::ContentType(ContentTypePart::Type),
"subtype" => HeaderPart::ContentType(ContentTypePart::Subtype),
"attr" if !subvalue.is_empty() => {
HeaderPart::ContentType(ContentTypePart::Attribute(subvalue.to_string()))
}
// Received
"rcvd" => {
if !subvalue.is_empty() {
HeaderPart::Received(ReceivedPart::try_from(subvalue)?)
} else {
HeaderPart::Text
}
}
// Id
"id" => HeaderPart::Id,
// Raw
"raw" => HeaderPart::Raw,
"raw_name" => HeaderPart::RawName,
// Date
"date" => HeaderPart::Date,
// Exists
"exists" => HeaderPart::Exists,
_ => {
return Err(());
}
})
}
}
impl TryFrom<&str> for ReceivedPart {
type Error = ();
fn try_from(value: &str) -> Result<Self, Self::Error> {
Ok(match value {
// Received
"from" => ReceivedPart::From(ReceivedHostname::Any),
"from.name" => ReceivedPart::From(ReceivedHostname::Name),
"from.ip" => ReceivedPart::From(ReceivedHostname::Ip),
"ip" => ReceivedPart::FromIp,
"iprev" => ReceivedPart::FromIpRev,
"by" => ReceivedPart::By(ReceivedHostname::Any),
"by.name" => ReceivedPart::By(ReceivedHostname::Name),
"by.ip" => ReceivedPart::By(ReceivedHostname::Ip),
"for" => ReceivedPart::For,
"with" => ReceivedPart::With,
"tls" => ReceivedPart::TlsVersion,
"cipher" => ReceivedPart::TlsCipher,
"id" => ReceivedPart::Id,
"ident" => ReceivedPart::Ident,
"date" => ReceivedPart::Date,
"date.raw" => ReceivedPart::DateRaw,
_ => return Err(()),
})
}
}
impl TryFrom<&str> for AddressPart {
type Error = ();
fn try_from(value: &str) -> Result<Self, Self::Error> {
Ok(match value {
"name" => AddressPart::Name,
"addr" | "all" => AddressPart::All,
"addr.domain" => AddressPart::Domain,
"addr.local" => AddressPart::LocalPart,
"addr.user" => AddressPart::User,
"addr.detail" => AddressPart::Detail,
_ => return Err(()),
})
}
}
impl Display for Value {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Value::Text(t) => f.write_str(t),
Value::List(l) => {
for i in l {
i.fmt(f)?;
}
Ok(())
}
Value::Number(n) => n.fmt(f),
Value::Variable(v) => v.fmt(f),
Value::Regex(r) => f.write_str(&r.expr),
}
}
}
impl Display for VariableType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
VariableType::Local(v) => write!(f, "${{{v}}}"),
VariableType::Match(v) => write!(f, "${{{v}}}"),
VariableType::Global(v) => write!(f, "${{global.{v}}}"),
VariableType::Environment(v) => write!(f, "${{env.{v}}}"),
VariableType::Envelope(env) => f.write_str(match env {
Envelope::From => "${{envelope.from}}",
Envelope::To => "${{envelope.to}}",
Envelope::ByTimeAbsolute => "${{envelope.by_time_absolute}}",
Envelope::ByTimeRelative => "${{envelope.by_time_relative}}",
Envelope::ByMode => "${{envelope.by_mode}}",
Envelope::ByTrace => "${{envelope.by_trace}}",
Envelope::Notify => "${{envelope.notify}}",
Envelope::Orcpt => "${{envelope.orcpt}}",
Envelope::Ret => "${{envelope.ret}}",
Envelope::Envid => "${{envelope.envit}}",
}),
VariableType::Header(hdr) => {
write!(
f,
"${{header.{}",
hdr.name.first().map(|h| h.as_str()).unwrap_or_default()
)?;
if hdr.index_hdr != 0 {
write!(f, "[{}]", hdr.index_hdr)?;
} else {
f.write_str("[*]")?;
}
/*if hdr.part != HeaderPart::Text {
f.write_str(".")?;
f.write_str(match &hdr.part {
HeaderPart::Name => "name",
HeaderPart::Address => "address",
HeaderPart::Type => "type",
HeaderPart::Subtype => "subtype",
HeaderPart::Raw => "raw",
HeaderPart::Date => "date",
HeaderPart::Attribute(attr) => attr.as_str(),
HeaderPart::Text => unreachable!(),
})?;
}*/
if hdr.index_part != 0 {
write!(f, "[{}]", hdr.index_part)?;
} else {
f.write_str("[*]")?;
}
f.write_str("}")
}
VariableType::Part(part) => {
write!(
f,
"${{{}",
match part {
MessagePart::TextBody(true) => "body.to_text",
MessagePart::TextBody(false) => "body.text",
MessagePart::HtmlBody(true) => "body.to_html",
MessagePart::HtmlBody(false) => "body.html",
MessagePart::Contents => "part.text",
MessagePart::Raw => "part.raw",
}
)?;
f.write_str("}")
}
}
}
}
#[cfg(test)]
mod tests {
use mail_parser::HeaderName;
use super::Value;
use crate::compiler::grammar::instruction::{Block, CompilerState, Instruction, MAX_PARAMS};
use crate::compiler::grammar::test::Test;
use crate::compiler::grammar::tests::test_string::TestString;
use crate::compiler::grammar::{Comparator, MatchType};
use crate::compiler::lexer::tokenizer::Tokenizer;
use crate::compiler::lexer::word::Word;
use crate::compiler::{AddressPart, HeaderPart, HeaderVariable, VariableType};
use crate::{AHashSet, Compiler};
#[test]
fn tokenize_string() {
let c = Compiler::new();
let mut block = Block::new(Word::Not);
block.match_test_pos.push(0);
let mut compiler = CompilerState {
compiler: &c,
instructions: vec![Instruction::Test(Test::String(TestString {
match_type: MatchType::Regex(u64::MAX),
comparator: Comparator::AsciiCaseMap,
source: vec![Value::Variable(VariableType::Local(0))],
key_list: vec![Value::Variable(VariableType::Local(0))],
is_not: false,
}))],
block_stack: Vec::new(),
block,
last_block_type: Word::Not,
vars_global: AHashSet::new(),
vars_num: 0,
vars_num_max: 0,
vars_local: 0,
tokens: Tokenizer::new(&c, b""),
vars_match_max: usize::MAX,
param_check: [false; MAX_PARAMS],
includes_num: 0,
};
for (input, expected_result) in [
("$${hex:24 24}", Value::Text("$$$".to_string().into())),
("$${hex:40}", Value::Text("$@".to_string().into())),
("${hex: 40 }", Value::Text("@".to_string().into())),
("${HEX: 40}", Value::Text("@".to_string().into())),
("${hex:40", Value::Text("${hex:40".to_string().into())),
("${hex:400}", Value::Text("${hex:400}".to_string().into())),
(
"${hex:4${hex:30}}",
Value::Text("${hex:40}".to_string().into()),
),
("${unicode:40}", Value::Text("@".to_string().into())),
(
"${ unicode:40}",
Value::Text("${ unicode:40}".to_string().into()),
),
("${UNICODE:40}", Value::Text("@".to_string().into())),
("${UnICoDE:0000040}", Value::Text("@".to_string().into())),
("${Unicode:40}", Value::Text("@".to_string().into())),
(
"${Unicode:40 40 ",
Value::Text("${Unicode:40 40 ".to_string().into()),
),
(
"${Unicode:Cool}",
Value::Text("${Unicode:Cool}".to_string().into()),
),
("", Value::Text("".to_string().into())),
(
"${global.full}",
Value::Variable(VariableType::Global("full".to_string())),
),
(
"${BAD${global.Company}",
Value::List(vec![
Value::Text("${BAD".to_string().into()),
Value::Variable(VariableType::Global("company".to_string())),
]),
),
(
"${President, ${global.Company} Inc.}",
Value::List(vec![
Value::Text("${President, ".to_string().into()),
Value::Variable(VariableType::Global("company".to_string())),
Value::Text(" Inc.}".to_string().into()),
]),
),
(
"dear${hex:20 24 7b}global.Name}",
Value::List(vec![
Value::Text("dear ".to_string().into()),
Value::Variable(VariableType::Global("name".to_string())),
]),
),
(
"INBOX.lists.${2}",
Value::List(vec![
Value::Text("INBOX.lists.".to_string().into()),
Value::Variable(VariableType::Match(2)),
]),
),
(
"Ein unerh${unicode:00F6}rt gro${unicode:00DF}er Test",
Value::Text("Ein unerhört großer Test".to_string().into()),
),
("&%${}!", Value::Text("&%${}!".to_string().into())),
("${doh!}", Value::Text("${doh!}".to_string().into())),
(
"${hex: 20 }${global.hi}${hex: 20 }",
Value::List(vec![
Value::Text(" ".to_string().into()),
Value::Variable(VariableType::Global("hi".to_string())),
Value::Text(" ".to_string().into()),
]),
),
(
"${hex:20 24 7b z}${global.hi}${unicode:}${unicode: }${hex:20}",
Value::List(vec![
Value::Text("${hex:20 24 7b z}".to_string().into()),
Value::Variable(VariableType::Global("hi".to_string())),
Value::Text("${unicode:}${unicode: } ".to_string().into()),
]),
),
(
"${header.from}",
Value::Variable(VariableType::Header(HeaderVariable {
name: vec![HeaderName::From],
part: HeaderPart::Text,
index_hdr: -1,
index_part: -1,
})),
),
(
"${header.from.addr}",
Value::Variable(VariableType::Header(HeaderVariable {
name: vec![HeaderName::From],
part: HeaderPart::Address(AddressPart::All),
index_hdr: -1,
index_part: -1,
})),
),
(
"${header.from[1]}",
Value::Variable(VariableType::Header(HeaderVariable {
name: vec![HeaderName::From],
part: HeaderPart::Text,
index_hdr: 1,
index_part: -1,
})),
),
(
"${header.from[*]}",
Value::Variable(VariableType::Header(HeaderVariable {
name: vec![HeaderName::From],
part: HeaderPart::Text,
index_hdr: 0,
index_part: -1,
})),
),
(
"${header.from[20].name}",
Value::Variable(VariableType::Header(HeaderVariable {
name: vec![HeaderName::From],
part: HeaderPart::Address(AddressPart::Name),
index_hdr: 20,
index_part: -1,
})),
),
(
"${header.from[*].addr}",
Value::Variable(VariableType::Header(HeaderVariable {
name: vec![HeaderName::From],
part: HeaderPart::Address(AddressPart::All),
index_hdr: 0,
index_part: -1,
})),
),
(
"${header.from[-5].name[2]}",
Value::Variable(VariableType::Header(HeaderVariable {
name: vec![HeaderName::From],
part: HeaderPart::Address(AddressPart::Name),
index_hdr: -5,
index_part: 2,
})),
),
(
"${header.from[*].raw[*]}",
Value::Variable(VariableType::Header(HeaderVariable {
name: vec![HeaderName::From],
part: HeaderPart::Raw,
index_hdr: 0,
index_part: 0,
})),
),
] {
assert_eq!(
compiler.tokenize_string(input.as_bytes(), true).unwrap(),
expected_result,
"Failed for {input}"
);
}
for input in ["${unicode:200000}", "${Unicode:DF01}"] {
assert!(compiler.tokenize_string(input.as_bytes(), true).is_err());
}
}
}
+577
View File
@@ -0,0 +1,577 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::{StringConstant, Token, word::lookup_words};
use crate::{
Compiler,
compiler::{CompileError, ErrorType, Number},
runtime::eval::IntoString,
};
use std::{iter::Peekable, slice::Iter};
pub(crate) struct Tokenizer<'x> {
pub compiler: &'x Compiler,
pub iter: Peekable<Iter<'x, u8>>,
pub buf: Vec<u8>,
pub next_token: Vec<TokenInfo>,
pub pos: usize,
pub line_num: usize,
pub line_start: usize,
pub text_line_num: usize,
pub text_line_pos: usize,
pub token_line_num: usize,
pub token_line_pos: usize,
pub token_is_tag: bool,
pub last_ch: u8,
pub state: State,
}
#[derive(Debug)]
pub(crate) struct TokenInfo {
pub(crate) token: Token,
pub(crate) line_num: usize,
pub(crate) line_pos: usize,
}
pub(crate) enum State {
None,
BracketComment,
HashComment,
QuotedString(StringType),
MultiLine(StringType),
}
#[derive(Clone, Copy, Default)]
pub(crate) struct StringType {
maybe_variable: bool,
has_other: bool,
has_digits: bool,
has_dots: bool,
}
impl<'x> Tokenizer<'x> {
pub fn new(compiler: &'x Compiler, bytes: &'x [u8]) -> Self {
Tokenizer {
compiler,
iter: bytes.iter().peekable(),
buf: Vec::with_capacity(bytes.len() / 2),
pos: usize::MAX,
line_num: 1,
line_start: 0,
text_line_num: 0,
text_line_pos: 0,
token_line_num: 0,
token_line_pos: 0,
token_is_tag: false,
next_token: Vec::with_capacity(2),
last_ch: 0,
state: State::None,
}
}
pub fn get_current_token(&mut self) -> Option<TokenInfo> {
if !self.buf.is_empty() {
let word = std::str::from_utf8(&self.buf).unwrap();
let token = if let Some(word) = lookup_words(word) {
if self.token_is_tag {
self.token_line_pos -= 1;
Token::Tag(word)
} else {
Token::Identifier(word)
}
} else if self.buf.first().unwrap().is_ascii_digit() {
let multiplier = match self.buf.last().unwrap() {
b'k' => 1024,
b'm' => 1048576,
b'g' => 1073741824,
_ => 1,
};
if let Ok(number) = (if multiplier > 1 && self.buf.len() > 1 {
std::str::from_utf8(&self.buf[..self.buf.len() - 1]).unwrap()
} else {
word
})
.parse::<usize>()
{
Token::Number(number.saturating_mul(multiplier))
} else if self.token_is_tag {
Token::Unknown(format!(":{word}"))
} else {
Token::Unknown(word.to_string())
}
} else if self.token_is_tag {
Token::Unknown(format!(":{word}"))
} else {
Token::Unknown(word.to_string())
};
self.reset_current_token();
Some(TokenInfo {
token,
line_num: self.token_line_num,
line_pos: self.token_line_pos,
})
} else {
None
}
}
#[inline(always)]
pub fn reset_current_token(&mut self) {
self.buf.clear();
self.token_is_tag = false;
}
#[inline(always)]
pub fn token_is_tag(&mut self) {
self.token_is_tag = true;
}
pub fn get_token(&mut self, token: Token) -> TokenInfo {
let next_token = TokenInfo {
token,
line_num: self.line_num,
line_pos: self.pos - self.line_start,
};
if let Some(token) = self.get_current_token() {
self.next_token.push(next_token);
token
} else {
next_token
}
}
pub fn get_string(&mut self, str_type: StringType) -> Result<TokenInfo, CompileError> {
if self.buf.len() < self.compiler.max_string_size {
let token = if str_type.maybe_variable {
Token::StringVariable(self.buf.to_vec())
} else {
let constant = self.buf.to_vec().into_string();
if !str_type.has_other && str_type.has_digits {
if !str_type.has_dots {
if let Some(number) = constant
.parse::<i64>()
.ok()
.filter(|&n| n.to_string() == constant)
{
Token::StringConstant(StringConstant::Number(Number::Integer(number)))
} else {
Token::StringConstant(StringConstant::String(constant))
}
} else if let Some(number) = constant
.parse::<f64>()
.ok()
.filter(|&n| n.to_string() == constant)
{
Token::StringConstant(StringConstant::Number(Number::Float(number)))
} else {
Token::StringConstant(StringConstant::String(constant))
}
} else {
Token::StringConstant(StringConstant::String(constant))
}
};
self.buf.clear();
Ok(TokenInfo {
token,
line_num: self.text_line_num,
line_pos: self.text_line_pos,
})
} else {
Err(CompileError {
line_num: self.text_line_num,
line_pos: self.text_line_pos,
error_type: ErrorType::StringTooLong,
})
}
}
#[inline(always)]
pub fn push_byte(&mut self, ch: u8) {
if self.buf.is_empty() {
self.token_line_num = self.line_num;
self.token_line_pos = self.pos - self.line_start;
}
self.buf.push(ch);
}
#[inline(always)]
pub fn new_line(&mut self) {
self.line_num += 1;
self.line_start = self.pos;
}
#[inline(always)]
pub fn text_start(&mut self) {
self.text_line_num = self.line_num;
self.text_line_pos = self.pos - self.line_start;
}
#[inline(always)]
pub fn is_token_start(&self) -> bool {
self.buf.is_empty()
}
#[inline(always)]
pub fn token_bytes(&self) -> &[u8] {
&self.buf
}
#[inline(always)]
pub fn next_byte(&mut self) -> Option<(u8, u8)> {
self.iter.next().map(|&ch| {
let last_ch = self.last_ch;
self.pos = self.pos.wrapping_add(1);
self.last_ch = ch;
(ch, last_ch)
})
}
#[inline(always)]
pub fn peek_byte(&mut self) -> Option<u8> {
self.iter.peek().map(|ch| **ch)
}
pub fn unwrap_next(&mut self) -> Result<TokenInfo, CompileError> {
if let Some(token) = self.next() {
token
} else {
Err(CompileError {
line_num: self.line_num,
line_pos: self.pos - self.line_start,
error_type: ErrorType::UnexpectedEOF,
})
}
}
pub fn expect_token(&mut self, token: Token) -> Result<(), CompileError> {
let next_token = self.unwrap_next()?;
if next_token.token == token {
Ok(())
} else {
Err(next_token.expected(format!("'{token}'")))
}
}
pub fn expect_static_string(&mut self) -> Result<String, CompileError> {
let next_token = self.unwrap_next()?;
match next_token.token {
Token::StringConstant(s) => Ok(s.into_string()),
Token::BracketOpen => {
let mut string = None;
loop {
let token_info = self.unwrap_next()?;
match token_info.token {
Token::StringConstant(string_) => {
string = string_.into();
}
Token::BracketClose if string.is_some() => break,
_ => return Err(token_info.expected("constant string")),
}
}
Ok(string.unwrap().into_string())
}
_ => Err(next_token.expected("constant string")),
}
}
pub fn expect_number(&mut self, max_value: usize) -> Result<usize, CompileError> {
let next_token = self.unwrap_next()?;
if let Token::Number(n) = next_token.token {
if n < max_value {
Ok(n)
} else {
Err(next_token.expected(format!("number lower than {max_value}")))
}
} else {
Err(next_token.expected("number"))
}
}
pub fn invalid_character(&self) -> CompileError {
CompileError {
line_num: self.line_num,
line_pos: self.pos - self.line_start,
error_type: ErrorType::InvalidCharacter(self.last_ch),
}
}
pub fn peek(&mut self) -> Option<Result<&TokenInfo, CompileError>> {
if self.next_token.is_empty() {
match self.next()? {
Ok(next_token) => self.next_token.push(next_token),
Err(err) => return Some(Err(err)),
}
}
self.next_token.last().map(Ok)
}
}
impl Iterator for Tokenizer<'_> {
type Item = Result<TokenInfo, CompileError>;
fn next(&mut self) -> Option<Self::Item> {
if let Some(prev_token) = self.next_token.pop() {
return Some(Ok(prev_token));
}
'outer: while let Some((ch, last_ch)) = self.next_byte() {
match self.state {
State::None => match ch {
b'a'..=b'z' | b'0'..=b'9' | b'_' | b'.' | b'$' => {
self.push_byte(ch);
}
b'A'..=b'Z' => {
self.push_byte(ch.to_ascii_lowercase());
}
b':' => {
if self.is_token_start()
&& matches!(self.peek_byte(), Some(b) if b.is_ascii_alphabetic())
{
self.token_is_tag();
} else if self.token_bytes().eq_ignore_ascii_case(b"text") {
self.state = State::MultiLine(StringType::default());
self.text_start();
while let Some((ch, _)) = self.next_byte() {
if ch == b'\n' {
self.new_line();
self.reset_current_token();
continue 'outer;
}
}
} else {
return Some(Ok(self.get_token(Token::Colon)));
//return Some(Err(self.invalid_character()));
}
}
b'"' => {
self.state = State::QuotedString(StringType::default());
self.text_start();
if let Some(token) = self.get_current_token() {
return Some(Ok(token));
}
}
b'{' => {
return Some(Ok(self.get_token(Token::CurlyOpen)));
}
b'}' => {
return Some(Ok(self.get_token(Token::CurlyClose)));
}
b';' => {
return Some(Ok(self.get_token(Token::Semicolon)));
}
b',' => {
return Some(Ok(self.get_token(Token::Comma)));
}
b'[' => {
return Some(Ok(self.get_token(Token::BracketOpen)));
}
b']' => {
return Some(Ok(self.get_token(Token::BracketClose)));
}
b'(' => {
return Some(Ok(self.get_token(Token::ParenthesisOpen)));
}
b')' => {
return Some(Ok(self.get_token(Token::ParenthesisClose)));
}
b'/' => {
if let Some((b'*', _)) = self.next_byte() {
self.last_ch = 0;
self.state = State::BracketComment;
self.text_start();
if let Some(token) = self.get_current_token() {
return Some(Ok(token));
}
} else {
return Some(Err(self.invalid_character()));
}
}
b'#' => {
self.state = State::HashComment;
if let Some(token) = self.get_current_token() {
return Some(Ok(token));
}
}
b'\n' => {
self.new_line();
if let Some(token) = self.get_current_token() {
return Some(Ok(token));
}
}
b' ' | b'\t' | b'\r' => {
if let Some(token) = self.get_current_token() {
return Some(Ok(token));
}
}
_ => {
return Some(Err(self.invalid_character()));
}
},
State::BracketComment => match ch {
b'/' if last_ch == b'*' => {
self.state = State::None;
}
b'\n' => {
self.new_line();
}
_ => (),
},
State::HashComment => {
if ch == b'\n' {
self.state = State::None;
self.new_line();
}
}
State::QuotedString(mut str_type) => match ch {
b'"' if last_ch != b'\\' => {
self.state = State::None;
return Some(self.get_string(str_type));
}
b'\n' => {
self.new_line();
self.push_byte(b'\n');
str_type.has_other = true;
self.state = State::QuotedString(str_type);
}
b'{' if (last_ch == b'$' || last_ch == b'%') => {
str_type.maybe_variable = true;
self.state = State::QuotedString(str_type);
self.push_byte(ch);
}
b'\\' => {
if last_ch == b'\\' {
self.push_byte(ch);
}
}
b'0'..=b'9' => {
if !str_type.has_digits {
str_type.has_digits = true;
self.state = State::QuotedString(str_type);
}
self.push_byte(ch);
}
b'.' => {
if !str_type.has_dots {
str_type.has_dots = true;
} else {
str_type.has_other = true;
}
self.state = State::QuotedString(str_type);
self.push_byte(ch);
}
_ => {
let ch = if last_ch == b'\\' {
match ch {
b'n' => b'\n',
b'r' => b'\r',
b't' => b'\t',
_ => ch,
}
} else {
ch
};
if !str_type.has_other && ch != b'-' {
str_type.has_other = true;
self.state = State::QuotedString(str_type);
}
self.push_byte(ch);
}
},
State::MultiLine(mut str_type) => match ch {
b'.' if last_ch == b'\n' => {
let is_eof = match (self.next_byte(), self.peek_byte()) {
(Some((b'\r', _)), Some(b'\n')) => {
self.next_byte();
true
}
(Some((b'\n', _)), _) => true,
(Some((b'.', _)), _) => {
self.push_byte(b'.');
false
}
(Some((ch, _)), _) => {
self.push_byte(b'.');
self.push_byte(ch);
false
}
_ => false,
};
if is_eof {
self.new_line();
self.state = State::None;
return Some(self.get_string(str_type));
}
}
b'\n' => {
self.new_line();
self.push_byte(b'\n');
}
b'{' if (last_ch == b'$' || last_ch == b'%') => {
str_type.maybe_variable = true;
self.state = State::MultiLine(str_type);
self.push_byte(ch);
}
b'0'..=b'9' => {
if !str_type.has_digits {
str_type.has_digits = true;
self.state = State::MultiLine(str_type);
}
self.push_byte(ch);
}
b'.' => {
if !str_type.has_dots {
str_type.has_dots = true;
} else {
str_type.has_other = true;
}
self.state = State::MultiLine(str_type);
self.push_byte(ch);
}
_ => {
if !str_type.has_other && ch != b'-' {
str_type.has_other = true;
self.state = State::MultiLine(str_type);
}
self.push_byte(ch);
}
},
}
}
match self.state {
State::BracketComment | State::QuotedString(_) | State::MultiLine(_) => {
Some(Err(CompileError {
line_num: self.text_line_num,
line_pos: self.text_line_pos,
error_type: (&self.state).into(),
}))
}
_ => None,
}
}
}
impl From<&State> for ErrorType {
fn from(state: &State) -> Self {
match state {
State::BracketComment => ErrorType::UnterminatedComment,
State::QuotedString(_) => ErrorType::UnterminatedString,
State::MultiLine(_) => ErrorType::UnterminatedMultiline,
_ => unreachable!(),
}
}
}
+410
View File
@@ -0,0 +1,410 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::fmt::Display;
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub(crate) enum Word {
AddFlag,
AddHeader,
Address,
Addresses,
All,
AllOf,
AnyChild,
AnyOf,
Body,
Break,
ByMode,
ByTimeAbsolute,
ByTimeRelative,
ByTrace,
Comparator,
Contains,
Content,
ContentType,
Convert,
Copy,
Count,
Create,
CurrentDate,
Date,
Days,
DeleteHeader,
Detail,
Discard,
Domain,
Duplicate,
Else,
ElsIf,
Enclose,
EncodeUrl,
Envelope,
Environment,
Ereject,
Error,
Exists,
ExtractText,
False,
Fcc,
FileInto,
First,
Flags,
ForEveryPart,
From,
Global,
Handle,
HasFlag,
Header,
Headers,
If,
Ihave,
Importance,
Include,
Index,
Is,
Keep,
Last,
Length,
List,
LocalPart,
Lower,
LowerFirst,
MailboxExists,
MailboxId,
MailboxIdExists,
Matches,
Message,
Metadata,
MetadataExists,
Mime,
Name,
Not,
Notify,
NotifyMethodCapability,
Once,
Optional,
Options,
OriginalZone,
Over,
Param,
Percent,
Personal,
QuoteRegex,
QuoteWildcard,
Raw,
Redirect,
Regex,
Reject,
RemoveFlag,
Replace,
Require,
Ret,
Return,
Seconds,
ServerMetadata,
ServerMetadataExists,
Set,
SetFlag,
Size,
SpamTest,
SpecialUse,
SpecialUseExists,
Stop,
String,
Subject,
Subtype,
Text,
True,
Type,
Under,
UniqueId,
Upper,
UpperFirst,
User,
Vacation,
ValidExtList,
ValidNotifyMethod,
Value,
VirusTest,
Zone,
// Extensions
Eval,
Local,
While,
Let,
Continue,
}
pub(crate) fn lookup_words(input: &str) -> Option<Word> {
hashify::tiny_map!(
input.as_bytes(),
"addflag" => Word::AddFlag,
"addheader" => Word::AddHeader,
"address" => Word::Address,
"addresses" => Word::Addresses,
"all" => Word::All,
"allof" => Word::AllOf,
"anychild" => Word::AnyChild,
"anyof" => Word::AnyOf,
"body" => Word::Body,
"break" => Word::Break,
"bymode" => Word::ByMode,
"bytimeabsolute" => Word::ByTimeAbsolute,
"bytimerelative" => Word::ByTimeRelative,
"bytrace" => Word::ByTrace,
"comparator" => Word::Comparator,
"contains" => Word::Contains,
"content" => Word::Content,
"contenttype" => Word::ContentType,
"convert" => Word::Convert,
"copy" => Word::Copy,
"count" => Word::Count,
"create" => Word::Create,
"currentdate" => Word::CurrentDate,
"date" => Word::Date,
"days" => Word::Days,
"deleteheader" => Word::DeleteHeader,
"detail" => Word::Detail,
"discard" => Word::Discard,
"domain" => Word::Domain,
"duplicate" => Word::Duplicate,
"else" => Word::Else,
"elsif" => Word::ElsIf,
"enclose" => Word::Enclose,
"encodeurl" => Word::EncodeUrl,
"envelope" => Word::Envelope,
"environment" => Word::Environment,
"ereject" => Word::Ereject,
"error" => Word::Error,
"exists" => Word::Exists,
"extracttext" => Word::ExtractText,
"false" => Word::False,
"fcc" => Word::Fcc,
"fileinto" => Word::FileInto,
"first" => Word::First,
"flags" => Word::Flags,
"foreverypart" => Word::ForEveryPart,
"from" => Word::From,
"global" => Word::Global,
"handle" => Word::Handle,
"hasflag" => Word::HasFlag,
"header" => Word::Header,
"headers" => Word::Headers,
"if" => Word::If,
"ihave" => Word::Ihave,
"importance" => Word::Importance,
"include" => Word::Include,
"index" => Word::Index,
"is" => Word::Is,
"keep" => Word::Keep,
"last" => Word::Last,
"length" => Word::Length,
"list" => Word::List,
"localpart" => Word::LocalPart,
"lower" => Word::Lower,
"lowerfirst" => Word::LowerFirst,
"mailboxexists" => Word::MailboxExists,
"mailboxid" => Word::MailboxId,
"mailboxidexists" => Word::MailboxIdExists,
"matches" => Word::Matches,
"message" => Word::Message,
"metadata" => Word::Metadata,
"metadataexists" => Word::MetadataExists,
"mime" => Word::Mime,
"name" => Word::Name,
"not" => Word::Not,
"notify" => Word::Notify,
"notify_method_capability" => Word::NotifyMethodCapability,
"once" => Word::Once,
"optional" => Word::Optional,
"options" => Word::Options,
"originalzone" => Word::OriginalZone,
"over" => Word::Over,
"param" => Word::Param,
"percent" => Word::Percent,
"personal" => Word::Personal,
"quoteregex" => Word::QuoteRegex,
"quotewildcard" => Word::QuoteWildcard,
"raw" => Word::Raw,
"redirect" => Word::Redirect,
"regex" => Word::Regex,
"reject" => Word::Reject,
"removeflag" => Word::RemoveFlag,
"replace" => Word::Replace,
"require" => Word::Require,
"ret" => Word::Ret,
"return" => Word::Return,
"seconds" => Word::Seconds,
"servermetadata" => Word::ServerMetadata,
"servermetadataexists" => Word::ServerMetadataExists,
"set" => Word::Set,
"setflag" => Word::SetFlag,
"size" => Word::Size,
"spamtest" => Word::SpamTest,
"specialuse" => Word::SpecialUse,
"specialuse_exists" => Word::SpecialUseExists,
"stop" => Word::Stop,
"string" => Word::String,
"subject" => Word::Subject,
"subtype" => Word::Subtype,
"text" => Word::Text,
"true" => Word::True,
"type" => Word::Type,
"under" => Word::Under,
"uniqueid" => Word::UniqueId,
"upper" => Word::Upper,
"upperfirst" => Word::UpperFirst,
"user" => Word::User,
"vacation" => Word::Vacation,
"valid_ext_list" => Word::ValidExtList,
"valid_notify_method" => Word::ValidNotifyMethod,
"value" => Word::Value,
"virustest" => Word::VirusTest,
"zone" => Word::Zone,
"eval" => Word::Eval,
"local" => Word::Local,
"while" => Word::While,
"let" => Word::Let,
"continue" => Word::Continue,
)
}
impl Display for Word {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Word::AddFlag => f.write_str("addflag"),
Word::AddHeader => f.write_str("addheader"),
Word::Address => f.write_str("address"),
Word::Addresses => f.write_str("addresses"),
Word::All => f.write_str("all"),
Word::AllOf => f.write_str("allof"),
Word::AnyChild => f.write_str("anychild"),
Word::AnyOf => f.write_str("anyof"),
Word::Body => f.write_str("body"),
Word::Break => f.write_str("break"),
Word::ByMode => f.write_str("bymode"),
Word::ByTimeAbsolute => f.write_str("bytimeabsolute"),
Word::ByTimeRelative => f.write_str("bytimerelative"),
Word::ByTrace => f.write_str("bytrace"),
Word::Comparator => f.write_str("comparator"),
Word::Contains => f.write_str("contains"),
Word::Content => f.write_str("content"),
Word::ContentType => f.write_str("contenttype"),
Word::Convert => f.write_str("convert"),
Word::Copy => f.write_str("copy"),
Word::Count => f.write_str("count"),
Word::Create => f.write_str("create"),
Word::CurrentDate => f.write_str("currentdate"),
Word::Date => f.write_str("date"),
Word::Days => f.write_str("days"),
Word::DeleteHeader => f.write_str("deleteheader"),
Word::Detail => f.write_str("detail"),
Word::Discard => f.write_str("discard"),
Word::Domain => f.write_str("domain"),
Word::Duplicate => f.write_str("duplicate"),
Word::Else => f.write_str("else"),
Word::ElsIf => f.write_str("elsif"),
Word::Enclose => f.write_str("enclose"),
Word::EncodeUrl => f.write_str("encodeurl"),
Word::Envelope => f.write_str("envelope"),
Word::Environment => f.write_str("environment"),
Word::Ereject => f.write_str("ereject"),
Word::Error => f.write_str("error"),
Word::Exists => f.write_str("exists"),
Word::ExtractText => f.write_str("extracttext"),
Word::False => f.write_str("false"),
Word::Fcc => f.write_str("fcc"),
Word::FileInto => f.write_str("fileinto"),
Word::First => f.write_str("first"),
Word::Flags => f.write_str("flags"),
Word::ForEveryPart => f.write_str("foreverypart"),
Word::From => f.write_str("from"),
Word::Global => f.write_str("global"),
Word::Handle => f.write_str("handle"),
Word::HasFlag => f.write_str("hasflag"),
Word::Header => f.write_str("header"),
Word::Headers => f.write_str("headers"),
Word::If => f.write_str("if"),
Word::Ihave => f.write_str("ihave"),
Word::Importance => f.write_str("importance"),
Word::Include => f.write_str("include"),
Word::Index => f.write_str("index"),
Word::Is => f.write_str("is"),
Word::Keep => f.write_str("keep"),
Word::Last => f.write_str("last"),
Word::Length => f.write_str("length"),
Word::List => f.write_str("list"),
Word::LocalPart => f.write_str("localpart"),
Word::Lower => f.write_str("lower"),
Word::LowerFirst => f.write_str("lowerfirst"),
Word::MailboxExists => f.write_str("mailboxexists"),
Word::MailboxId => f.write_str("mailboxid"),
Word::MailboxIdExists => f.write_str("mailboxidexists"),
Word::Matches => f.write_str("matches"),
Word::Message => f.write_str("message"),
Word::Metadata => f.write_str("metadata"),
Word::MetadataExists => f.write_str("metadataexists"),
Word::Mime => f.write_str("mime"),
Word::Name => f.write_str("name"),
Word::Not => f.write_str("not"),
Word::Notify => f.write_str("notify"),
Word::NotifyMethodCapability => f.write_str("notify_method_capability"),
Word::Once => f.write_str("once"),
Word::Optional => f.write_str("optional"),
Word::Options => f.write_str("options"),
Word::OriginalZone => f.write_str("originalzone"),
Word::Over => f.write_str("over"),
Word::Param => f.write_str("param"),
Word::Percent => f.write_str("percent"),
Word::Personal => f.write_str("personal"),
Word::QuoteRegex => f.write_str("quoteregex"),
Word::QuoteWildcard => f.write_str("quotewildcard"),
Word::Raw => f.write_str("raw"),
Word::Redirect => f.write_str("redirect"),
Word::Regex => f.write_str("regex"),
Word::Reject => f.write_str("reject"),
Word::RemoveFlag => f.write_str("removeflag"),
Word::Replace => f.write_str("replace"),
Word::Require => f.write_str("require"),
Word::Ret => f.write_str("ret"),
Word::Return => f.write_str("return"),
Word::Seconds => f.write_str("seconds"),
Word::ServerMetadata => f.write_str("servermetadata"),
Word::ServerMetadataExists => f.write_str("servermetadataexists"),
Word::Set => f.write_str("set"),
Word::SetFlag => f.write_str("setflag"),
Word::Size => f.write_str("size"),
Word::SpamTest => f.write_str("spamtest"),
Word::SpecialUse => f.write_str("specialuse"),
Word::SpecialUseExists => f.write_str("specialuse_exists"),
Word::Stop => f.write_str("stop"),
Word::String => f.write_str("string"),
Word::Subject => f.write_str("subject"),
Word::Subtype => f.write_str("subtype"),
Word::Text => f.write_str("text"),
Word::True => f.write_str("true"),
Word::Type => f.write_str("type"),
Word::Under => f.write_str("under"),
Word::UniqueId => f.write_str("uniqueid"),
Word::Upper => f.write_str("upper"),
Word::UpperFirst => f.write_str("upperfirst"),
Word::User => f.write_str("user"),
Word::Vacation => f.write_str("vacation"),
Word::ValidExtList => f.write_str("valid_ext_list"),
Word::ValidNotifyMethod => f.write_str("valid_notify_method"),
Word::Value => f.write_str("value"),
Word::VirusTest => f.write_str("virustest"),
Word::Zone => f.write_str("zone"),
Word::Eval => f.write_str("eval"),
Word::Local => f.write_str("local"),
Word::While => f.write_str("while"),
Word::Let => f.write_str("let"),
Word::Continue => f.write_str("continue"),
}
}
}
+662
View File
@@ -0,0 +1,662 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use self::{
grammar::{AddressPart, Capability},
lexer::tokenizer::TokenInfo,
};
use crate::{Compiler, Envelope, FunctionMap, runtime::RuntimeError};
use ahash::AHashMap;
use arc_swap::ArcSwap;
use mail_parser::HeaderName;
use std::{borrow::Cow, fmt::Display, sync::Arc};
pub mod grammar;
pub mod lexer;
#[derive(Debug)]
pub struct CompileError {
line_num: usize,
line_pos: usize,
error_type: ErrorType,
}
#[derive(Debug)]
pub enum ErrorType {
InvalidCharacter(u8),
InvalidNumber(String),
InvalidMatchVariable(usize),
InvalidUnicodeSequence(u32),
InvalidNamespace(String),
InvalidRegex(String),
InvalidExpression(String),
InvalidUtf8String,
InvalidHeaderName,
InvalidArguments,
InvalidAddress,
InvalidURI,
InvalidEnvelope(String),
UnterminatedString,
UnterminatedComment,
UnterminatedMultiline,
UnterminatedBlock,
ScriptTooLong,
StringTooLong,
VariableTooLong,
VariableIsLocal(String),
HeaderTooLong,
ExpectedConstantString,
UnexpectedToken {
expected: Cow<'static, str>,
found: String,
},
UnexpectedEOF,
TooManyNestedBlocks,
TooManyNestedTests,
TooManyNestedForEveryParts,
TooManyIncludes,
LabelAlreadyDefined(String),
LabelUndefined(String),
BreakOutsideLoop,
ContinueOutsideLoop,
UnsupportedComparator(String),
DuplicatedParameter,
UndeclaredCapability(Capability),
MissingTag(Cow<'static, str>),
}
impl Default for Compiler {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(
any(test, feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
#[cfg_attr(
feature = "rkyv",
rkyv(serialize_bounds(
__S: rkyv::ser::Writer + rkyv::ser::Allocator,
__S::Error: rkyv::rancor::Source,
))
)]
#[cfg_attr(
feature = "rkyv",
rkyv(deserialize_bounds(__D::Error: rkyv::rancor::Source))
)]
#[cfg_attr(
feature = "rkyv",
rkyv(bytecheck(
bounds(
__C: rkyv::validation::ArchiveContext,
)
))
)]
pub(crate) enum Value {
Text(Arc<String>),
Number(Number),
Variable(VariableType),
Regex(Regex),
List(#[cfg_attr(feature = "rkyv", rkyv(omit_bounds))] Vec<Value>),
}
#[derive(Debug, Clone)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
#[cfg_attr(
any(test, feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
pub struct Regex {
#[cfg_attr(feature = "rkyv", rkyv(with = rkyv::with::Skip))]
#[cfg_attr(any(test, feature = "serde"), serde(skip, default))]
pub regex: LazyRegex,
pub expr: String,
}
#[derive(Debug, Clone)]
pub struct LazyRegex(pub Arc<ArcSwap<Option<fancy_regex::Regex>>>);
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(
any(test, feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
pub enum VariableType {
Local(usize),
Match(usize),
Global(String),
Environment(String),
Envelope(Envelope),
Header(HeaderVariable<'static>),
Part(MessagePart),
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(
any(test, feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
pub struct Transform {
pub variable: Box<VariableType>,
pub functions: Vec<usize>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(
any(test, feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
pub struct HeaderVariable<'x> {
pub name: Vec<HeaderName<'x>>,
pub part: HeaderPart,
pub index_hdr: i32,
pub index_part: i32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(
any(test, feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
pub enum MessagePart {
TextBody(bool),
HtmlBody(bool),
Contents,
Raw,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(
any(test, feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
pub enum HeaderPart {
Text,
Date,
Id,
Address(AddressPart),
ContentType(ContentTypePart),
Received(ReceivedPart),
Raw,
RawName,
Exists,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(
any(test, feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
pub enum ContentTypePart {
Type,
Subtype,
Attribute(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(
any(test, feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
pub enum ReceivedPart {
From(ReceivedHostname),
FromIp,
FromIpRev,
By(ReceivedHostname),
For,
With,
TlsVersion,
TlsCipher,
Id,
Ident,
Via,
Date,
DateRaw,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(
any(test, feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
pub enum ReceivedHostname {
Name,
Ip,
Any,
}
#[derive(Debug, Clone, Copy)]
#[cfg_attr(
any(test, feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
pub enum Number {
Integer(i64),
Float(f64),
}
impl Number {
#[cfg(test)]
pub fn to_float(&self) -> f64 {
match self {
Number::Integer(i) => *i as f64,
Number::Float(fl) => *fl,
}
}
}
impl From<Number> for usize {
fn from(value: Number) -> Self {
match value {
Number::Integer(i) => i as usize,
Number::Float(fl) => fl as usize,
}
}
}
impl Display for Number {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Number::Integer(i) => i.fmt(f),
Number::Float(fl) => fl.fmt(f),
}
}
}
impl Compiler {
pub const VERSION: u32 = 2;
pub fn new() -> Self {
Compiler {
max_script_size: 1024 * 1024,
max_string_size: 4096,
max_variable_name_size: 32,
max_nested_blocks: 15,
max_nested_tests: 15,
max_nested_foreverypart: 3,
max_match_variables: 30,
max_local_variables: 128,
max_header_size: 1024,
max_includes: 6,
functions: AHashMap::new(),
no_capability_check: false,
}
}
pub fn set_max_header_size(&mut self, size: usize) {
self.max_header_size = size;
}
pub fn with_max_header_size(mut self, size: usize) -> Self {
self.max_header_size = size;
self
}
pub fn set_max_includes(&mut self, size: usize) {
self.max_includes = size;
}
pub fn with_max_includes(mut self, size: usize) -> Self {
self.max_includes = size;
self
}
pub fn set_max_nested_blocks(&mut self, size: usize) {
self.max_nested_blocks = size;
}
pub fn with_max_nested_blocks(mut self, size: usize) -> Self {
self.max_nested_blocks = size;
self
}
pub fn set_max_nested_tests(&mut self, size: usize) {
self.max_nested_tests = size;
}
pub fn with_max_nested_tests(mut self, size: usize) -> Self {
self.max_nested_tests = size;
self
}
pub fn set_max_nested_foreverypart(&mut self, size: usize) {
self.max_nested_foreverypart = size;
}
pub fn with_max_nested_foreverypart(mut self, size: usize) -> Self {
self.max_nested_foreverypart = size;
self
}
pub fn set_max_script_size(&mut self, size: usize) {
self.max_script_size = size;
}
pub fn with_max_script_size(mut self, size: usize) -> Self {
self.max_script_size = size;
self
}
pub fn set_max_string_size(&mut self, size: usize) {
self.max_string_size = size;
}
pub fn with_max_string_size(mut self, size: usize) -> Self {
self.max_string_size = size;
self
}
pub fn set_max_variable_name_size(&mut self, size: usize) {
self.max_variable_name_size = size;
}
pub fn with_max_variable_name_size(mut self, size: usize) -> Self {
self.max_variable_name_size = size;
self
}
pub fn set_max_match_variables(&mut self, size: usize) {
self.max_match_variables = size;
}
pub fn with_max_match_variables(mut self, size: usize) -> Self {
self.max_match_variables = size;
self
}
pub fn set_max_local_variables(&mut self, size: usize) {
self.max_local_variables = size;
}
pub fn with_max_local_variables(mut self, size: usize) -> Self {
self.max_local_variables = size;
self
}
pub fn register_functions(mut self, fnc_map: &mut FunctionMap) -> Self {
self.functions = std::mem::take(&mut fnc_map.map);
self
}
pub fn with_no_capability_check(mut self, value: bool) -> Self {
self.no_capability_check = value;
self
}
pub fn set_no_capability_check(&mut self, value: bool) {
self.no_capability_check = value;
}
}
impl CompileError {
pub fn line_num(&self) -> usize {
self.line_num
}
pub fn line_pos(&self) -> usize {
self.line_pos
}
pub fn error_type(&self) -> &ErrorType {
&self.error_type
}
}
impl PartialEq for Regex {
fn eq(&self, other: &Self) -> bool {
self.expr == other.expr
}
}
impl Eq for Regex {}
impl TokenInfo {
pub fn expected(self, expected: impl Into<Cow<'static, str>>) -> CompileError {
CompileError {
line_num: self.line_num,
line_pos: self.line_pos,
error_type: ErrorType::UnexpectedToken {
expected: expected.into(),
found: self.token.to_string(),
},
}
}
pub fn missing_tag(self, tag: impl Into<Cow<'static, str>>) -> CompileError {
CompileError {
line_num: self.line_num,
line_pos: self.line_pos,
error_type: ErrorType::MissingTag(tag.into()),
}
}
pub fn custom(self, error_type: ErrorType) -> CompileError {
CompileError {
line_num: self.line_num,
line_pos: self.line_pos,
error_type,
}
}
}
impl Default for LazyRegex {
fn default() -> Self {
Self(Arc::new(ArcSwap::new(Arc::new(None))))
}
}
impl Regex {
pub fn new(expr: String, regex: fancy_regex::Regex) -> Self {
Self {
expr,
regex: LazyRegex(Arc::new(ArcSwap::new(Arc::new(Some(regex))))),
}
}
}
impl Display for CompileError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match &self.error_type() {
ErrorType::InvalidCharacter(value) => {
write!(f, "Invalid character {:?}", char::from(*value))
}
ErrorType::InvalidNumber(value) => write!(f, "Invalid number {value:?}"),
ErrorType::InvalidMatchVariable(value) => {
write!(f, "Match variable {value} out of range")
}
ErrorType::InvalidUnicodeSequence(value) => {
write!(f, "Invalid Unicode sequence {value:04x}")
}
ErrorType::InvalidNamespace(value) => write!(f, "Invalid namespace {value:?}"),
ErrorType::InvalidRegex(value) => write!(f, "Invalid regular expression {value:?}"),
ErrorType::InvalidExpression(value) => write!(f, "Invalid expression {value}"),
ErrorType::InvalidUtf8String => write!(f, "Invalid UTF-8 string"),
ErrorType::InvalidHeaderName => write!(f, "Invalid header name"),
ErrorType::InvalidArguments => write!(f, "Invalid Arguments"),
ErrorType::InvalidAddress => write!(f, "Invalid Address"),
ErrorType::InvalidURI => write!(f, "Invalid URI"),
ErrorType::InvalidEnvelope(value) => write!(f, "Invalid envelope {value:?}"),
ErrorType::UnterminatedString => write!(f, "Unterminated string"),
ErrorType::UnterminatedComment => write!(f, "Unterminated comment"),
ErrorType::UnterminatedMultiline => write!(f, "Unterminated multi-line string"),
ErrorType::UnterminatedBlock => write!(f, "Unterminated block"),
ErrorType::ScriptTooLong => write!(f, "Sieve script is too large"),
ErrorType::StringTooLong => write!(f, "String is too long"),
ErrorType::VariableTooLong => write!(f, "Variable name is too long"),
ErrorType::VariableIsLocal(value) => {
write!(f, "Variable {value:?} was already defined as local")
}
ErrorType::HeaderTooLong => write!(f, "Header value is too long"),
ErrorType::ExpectedConstantString => write!(f, "Expected a constant string"),
ErrorType::UnexpectedToken { expected, found } => {
write!(f, "Expected token {expected:?} but found {found:?}")
}
ErrorType::UnexpectedEOF => write!(f, "Unexpected end of file"),
ErrorType::TooManyNestedBlocks => write!(f, "Too many nested blocks"),
ErrorType::TooManyNestedTests => write!(f, "Too many nested tests"),
ErrorType::TooManyNestedForEveryParts => {
write!(f, "Too many nested foreverypart blocks")
}
ErrorType::TooManyIncludes => write!(f, "Too many includes"),
ErrorType::LabelAlreadyDefined(value) => write!(f, "Label {value:?} already defined"),
ErrorType::LabelUndefined(value) => write!(f, "Label {value:?} does not exist"),
ErrorType::BreakOutsideLoop => write!(f, "Break used outside of foreverypart loop"),
ErrorType::ContinueOutsideLoop => write!(f, "Continue used outside of while loop"),
ErrorType::UnsupportedComparator(value) => {
write!(f, "Comparator {value:?} is not supported")
}
ErrorType::DuplicatedParameter => write!(f, "Duplicated argument"),
ErrorType::UndeclaredCapability(value) => {
write!(f, "Undeclared capability '{value}'")
}
ErrorType::MissingTag(value) => write!(f, "Missing tag {value:?}"),
}?;
write!(
f,
" at line {}, column {}.",
self.line_num(),
self.line_pos()
)
}
}
impl Display for RuntimeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
RuntimeError::TooManyIncludes => write!(f, ""),
RuntimeError::InvalidInstruction(value) => write!(
f,
"Script executed invalid instruction {:?} at line {}, column {}.",
value.name(),
value.line_pos(),
value.line_num()
),
RuntimeError::ScriptErrorMessage(value) => {
write!(f, "Script reported error {value:?}.")
}
RuntimeError::CapabilityNotAllowed(value) => {
write!(f, "Capability '{value}' has been disabled.")
}
RuntimeError::CapabilityNotSupported(value) => {
write!(f, "Capability '{value}' not supported.")
}
RuntimeError::CPULimitReached => write!(
f,
"Script exceeded the maximum number of instructions allowed to execute."
),
}
}
}
#[cfg(test)]
mod tests {
use std::{fs, path::PathBuf};
use crate::Compiler;
#[test]
fn parse_rfc() {
let mut test_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
test_dir.push("tests");
test_dir.push("rfcs");
let mut tests_run = 0;
let compiler = Compiler::new().with_max_nested_foreverypart(10);
for file_name in fs::read_dir(&test_dir).unwrap() {
let mut file_name = file_name.unwrap().path();
if file_name.extension().is_some_and(|e| e == "sieve") {
println!("Parsing {}", file_name.display());
/*if !file_name
.file_name()
.unwrap()
.to_str()
.unwrap()
.contains("plugins")
{
let test = "true";
continue;
}*/
let script = fs::read(&file_name).unwrap();
file_name.set_extension("json");
let expected_result = fs::read(&file_name).unwrap();
tests_run += 1;
let sieve = compiler.compile(&script).unwrap();
let json_sieve = serde_json::to_string_pretty(
&sieve
.instructions
.into_iter()
.enumerate()
.collect::<Vec<_>>(),
)
.unwrap();
if json_sieve.as_bytes() != expected_result {
file_name.set_extension("failed");
fs::write(&file_name, json_sieve.as_bytes()).unwrap();
panic!("Test failed, parsed sieve saved to {}", file_name.display());
}
}
}
assert!(
tests_run > 0,
"Did not find any tests to run in folder {}.",
test_dir.display()
);
}
}
+981
View File
@@ -0,0 +1,981 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*
* Modified by Coffey Labs in 2026 for INBUXA.
*/
#![doc = include_str!("../README.md")]
use std::{borrow::Cow, sync::Arc, vec::IntoIter};
use ahash::{AHashMap, AHashSet};
use compiler::grammar::{
Capability,
actions::action_redirect::{ByTime, Notify, Ret},
instruction::Instruction,
};
use mail_parser::{HeaderName, Message};
use runtime::{Variable, context::ScriptStack};
pub mod compiler;
pub mod runtime;
pub(crate) const MAX_MATCH_VARIABLES: u32 = 63;
pub(crate) const MAX_LOCAL_VARIABLES: u32 = 256;
#[derive(Debug, Clone, Eq, PartialEq)]
#[cfg_attr(
any(test, feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
pub struct Sieve {
instructions: Vec<Instruction>,
num_vars: u32,
num_match_vars: u32,
}
#[derive(Clone)]
pub struct Compiler {
// Settings
pub(crate) max_script_size: usize,
pub(crate) max_string_size: usize,
pub(crate) max_variable_name_size: usize,
pub(crate) max_nested_blocks: usize,
pub(crate) max_nested_tests: usize,
pub(crate) max_nested_foreverypart: usize,
pub(crate) max_match_variables: usize,
pub(crate) max_local_variables: usize,
pub(crate) max_header_size: usize,
pub(crate) max_includes: usize,
pub(crate) no_capability_check: bool,
// Functions
pub(crate) functions: AHashMap<String, (u32, u32)>,
}
pub type Function = for<'x> fn(&'x Context<'x>, Vec<Variable>) -> Variable;
#[derive(Default, Clone)]
pub struct FunctionMap {
pub(crate) map: AHashMap<String, (u32, u32)>,
pub(crate) functions: Vec<Function>,
}
#[derive(Debug, Clone)]
pub struct Runtime {
pub(crate) allowed_capabilities: AHashSet<Capability>,
pub(crate) valid_notification_uris: AHashSet<Cow<'static, str>>,
pub(crate) valid_ext_lists: AHashSet<Cow<'static, str>>,
pub(crate) protected_headers: Vec<HeaderName<'static>>,
pub(crate) environment: AHashMap<Cow<'static, str>, Variable>,
pub(crate) metadata: Vec<(Metadata<String>, Cow<'static, str>)>,
pub(crate) include_scripts: AHashMap<String, Arc<Sieve>>,
pub(crate) local_hostname: Cow<'static, str>,
pub(crate) functions: Vec<Function>,
pub(crate) max_nested_includes: usize,
pub(crate) cpu_limit: usize,
pub(crate) max_variable_size: usize,
pub(crate) max_redirects: usize,
pub(crate) max_received_headers: usize,
pub(crate) max_header_size: usize,
pub(crate) max_out_messages: usize,
pub(crate) default_vacation_expiry: u64,
pub(crate) default_duplicate_expiry: u64,
pub(crate) vacation_use_orig_rcpt: bool,
pub(crate) vacation_default_subject: Cow<'static, str>,
pub(crate) vacation_subject_prefix: Cow<'static, str>,
}
#[derive(Clone, Debug)]
pub struct Context<'x> {
#[cfg(test)]
pub(crate) runtime: Runtime,
#[cfg(not(test))]
pub(crate) runtime: &'x Runtime,
pub(crate) user_address: Cow<'x, str>,
pub(crate) user_full_name: Cow<'x, str>,
pub(crate) current_time: i64,
pub(crate) message: Message<'x>,
pub(crate) message_size: usize,
pub(crate) envelope: Vec<(Envelope, Variable)>,
pub(crate) metadata: Vec<(Metadata<String>, Cow<'x, str>)>,
pub(crate) part: u32,
pub(crate) part_iter: IntoIter<u32>,
pub(crate) part_iter_stack: Vec<(u32, IntoIter<u32>)>,
pub(crate) spam_status: SpamStatus,
pub(crate) virus_status: VirusStatus,
pub(crate) pos: usize,
pub(crate) test_result: bool,
pub(crate) script_cache: AHashMap<Script, Arc<Sieve>>,
pub(crate) script_stack: Vec<ScriptStack>,
pub(crate) vars_global: AHashMap<Cow<'static, str>, Variable>,
pub(crate) vars_env: AHashMap<Cow<'static, str>, Variable>,
pub(crate) vars_local: Vec<Variable>,
pub(crate) vars_match: Vec<Variable>,
pub(crate) expr_stack: Vec<Variable>,
pub(crate) expr_pos: usize,
pub(crate) queued_events: IntoIter<Event>,
pub(crate) final_event: Option<Event>,
pub(crate) last_message_id: usize,
pub(crate) main_message_id: usize,
pub(crate) has_changes: bool,
pub(crate) num_redirects: usize,
pub(crate) num_instructions: usize,
pub(crate) num_out_messages: usize,
}
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub enum Script {
Personal(String),
Global(String),
}
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
#[cfg_attr(
any(test, feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
pub enum Envelope {
From,
To,
ByTimeAbsolute,
ByTimeRelative,
ByMode,
ByTrace,
Notify,
Orcpt,
Ret,
Envid,
}
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
#[cfg_attr(
any(test, feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
pub enum Metadata<T> {
Server { annotation: T },
Mailbox { name: T, annotation: T },
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum Event {
IncludeScript {
name: Script,
optional: bool,
},
MailboxExists {
mailboxes: Vec<Mailbox>,
special_use: Vec<String>,
},
ListContains {
lists: Vec<String>,
values: Vec<String>,
match_as: MatchAs,
},
DuplicateId {
id: String,
expiry: u64,
last: bool,
},
SetEnvelope {
envelope: Envelope,
value: String,
},
Function {
id: ExternalId,
arguments: Vec<Variable>,
},
// Actions
Keep {
flags: Vec<String>,
message_id: usize,
},
Discard,
Reject {
extended: bool,
reason: String,
},
FileInto {
folder: String,
flags: Vec<String>,
mailbox_id: Option<String>,
special_use: Option<String>,
create: bool,
message_id: usize,
},
SendMessage {
recipient: Recipient,
notify: Notify,
return_of_content: Ret,
by_time: ByTime<i64>,
message_id: usize,
},
Notify {
from: Option<String>,
importance: Importance,
options: Vec<String>,
message: String,
method: String,
},
CreatedMessage {
message_id: usize,
message: Vec<u8>,
},
}
pub type ExternalId = u32;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(
any(test, feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
pub(crate) struct FileCarbonCopy<T> {
pub mailbox: T,
pub mailbox_id: Option<T>,
pub create: bool,
pub flags: Vec<T>,
pub special_use: Option<T>,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
pub enum Importance {
High,
Normal,
Low,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum MatchAs {
Octet,
Lowercase,
Number,
}
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub enum Recipient {
Address(String),
List(String),
Group(Vec<String>),
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum Input {
True,
False,
FncResult(Variable),
Script { name: Script, script: Arc<Sieve> },
}
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub enum Mailbox {
Name(String),
Id(String),
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum SpamStatus {
Unknown,
Ham,
MaybeSpam(f64),
Spam,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
pub enum VirusStatus {
Unknown,
Clean,
Replaced,
Cured,
MaybeVirus,
Virus,
}
#[cfg(test)]
mod tests {
use std::{
fs,
path::{Path, PathBuf},
};
use ahash::{AHashMap, AHashSet};
use mail_parser::{
Encoding, HeaderValue, Message, MessageParser, MessagePart, PartType,
parsers::MessageStream,
};
use crate::{
Compiler, Context, Envelope, Event, FunctionMap, Input, Mailbox, Recipient, Runtime,
SpamStatus, VirusStatus,
compiler::grammar::Capability,
runtime::{Variable, actions::action_mime::reset_test_boundary},
};
impl Variable {
pub fn unwrap_string(self) -> String {
self.to_string().into_owned()
}
}
#[test]
fn test_suite() {
let mut tests = Vec::new();
let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
path.push("tests");
read_dir(path, &mut tests);
for test in tests {
/*if !test
.file_name()
.unwrap()
.to_str()
.unwrap()
.contains("expressions")
{
continue;
}*/
println!("===== {} =====", test.display());
run_test(&test);
}
}
fn read_dir(path: PathBuf, files: &mut Vec<PathBuf>) {
for entry in fs::read_dir(path).unwrap() {
let entry = entry.unwrap().path();
if entry.is_dir() {
read_dir(entry, files);
} else if entry
.extension()
.and_then(|e| e.to_str())
.unwrap_or("")
.eq("svtest")
{
files.push(entry);
}
}
}
fn run_test(script_path: &Path) {
let mut fnc_map = FunctionMap::new()
.with_function("trim", |_, v| match v.into_iter().next().unwrap() {
crate::runtime::Variable::String(s) => s.trim().to_string().into(),
v => v.to_string().into(),
})
.with_function("len", |_, v| v[0].to_string().len().into())
.with_function("count", |_, v| {
v[0].as_array().map_or(0, |arr| arr.len()).into()
})
.with_function("to_lowercase", |_, v| {
v[0].to_string().to_lowercase().to_string().into()
})
.with_function("to_uppercase", |_, v| {
v[0].to_string().to_uppercase().to_string().into()
})
.with_function("is_uppercase", |_, v| {
v[0].to_string()
.as_ref()
.chars()
.filter(|c| c.is_alphabetic())
.all(|c| c.is_uppercase())
.into()
})
.with_function("is_ascii", |_, v| {
v[0].to_string().as_ref().is_ascii().into()
})
.with_function("char_count", |_, v| {
v[0].to_string().as_ref().chars().count().into()
})
.with_function("lines", |_, v| {
v[0].to_string()
.lines()
.map(|line| Variable::from(line.to_string()))
.collect::<Vec<_>>()
.into()
})
.with_function_args(
"contains",
|_, v| v[0].to_string().contains(v[1].to_string().as_ref()).into(),
2,
)
.with_function_args(
"eq_lowercase",
|_, v| {
v[0].to_string()
.as_ref()
.eq_ignore_ascii_case(v[1].to_string().as_ref())
.into()
},
2,
)
.with_function_args(
"concat_three",
|_, v| format!("{}-{}-{}", v[0], v[1], v[2]).into(),
3,
)
.with_function_args(
"in_array",
|_, v| {
v[0].as_array()
.is_some_and(|arr| arr.contains(&v[1]))
.into()
},
2,
)
.with_external_function("ext_zero", 0, 0)
.with_external_function("ext_one", 1, 1)
.with_external_function("ext_two", 2, 2)
.with_external_function("ext_three", 3, 3)
.with_external_function("ext_true", 4, 0)
.with_external_function("ext_false", 5, 0);
let mut compiler = Compiler::new()
.with_max_string_size(10240)
.register_functions(&mut fnc_map);
let mut ancestors = script_path.ancestors();
ancestors.next();
let base_path = ancestors.next().unwrap();
let script = compiler
.compile(&add_crlf(&fs::read(script_path).unwrap()))
.unwrap();
let mut input = Input::script("", script);
let mut current_test = String::new();
let mut raw_message_: Option<Vec<u8>> = None;
let mut prev_state = None;
let mut mailboxes = Vec::new();
let mut lists: AHashMap<String, AHashSet<String>> = AHashMap::new();
let mut duplicated_ids = AHashSet::new();
let mut actions = Vec::new();
'outer: loop {
let runtime = Runtime::new()
.with_protected_header("Auto-Submitted")
.with_protected_header("Received")
.with_valid_notification_uri("mailto")
.with_max_out_messages(100)
.with_capability(Capability::While)
.with_capability(Capability::Expressions)
.with_functions(&mut fnc_map.clone());
let mut instance = Context::new(
&runtime,
Message {
parts: vec![MessagePart {
headers: vec![],
is_encoding_problem: false,
body: PartType::Text("".into()),
encoding: Encoding::None,
offset_header: 0,
offset_body: 0,
offset_end: 0,
}],
raw_message: b""[..].into(),
..Default::default()
},
);
let raw_message = raw_message_.take().unwrap_or_default();
instance.message =
MessageParser::new()
.parse(&raw_message)
.unwrap_or_else(|| Message {
html_body: vec![],
text_body: vec![],
attachments: vec![],
parts: vec![MessagePart {
headers: vec![],
is_encoding_problem: false,
body: PartType::Text("".into()),
encoding: Encoding::None,
offset_header: 0,
offset_body: 0,
offset_end: 0,
}],
raw_message: b""[..].into(),
});
instance.message_size = raw_message.len();
if let Some((pos, script_cache, script_stack, vars_global, vars_local, vars_match)) =
prev_state.take()
{
instance.pos = pos;
instance.script_cache = script_cache;
instance.script_stack = script_stack;
instance.vars_global = vars_global;
instance.vars_local = vars_local;
instance.vars_match = vars_match;
}
instance.set_env_variable("vnd.inbuxa.default_mailbox", "INBOX");
instance.set_env_variable("vnd.inbuxa.username", "john.doe");
instance.set_user_address("MAILER-DAEMON");
if let Some(addr) = instance
.message
.from()
.and_then(|a| a.first())
.and_then(|a| a.address.as_ref())
{
instance.set_envelope(Envelope::From, addr.to_string());
}
if let Some(addr) = instance
.message
.to()
.and_then(|a| a.first())
.and_then(|a| a.address.as_ref())
{
instance.set_envelope(Envelope::To, addr.to_string());
}
while let Some(event) = instance.run(input) {
match event.unwrap() {
Event::IncludeScript { name, optional } => {
let mut include_path = PathBuf::from(base_path);
include_path.push(if matches!(name, crate::Script::Personal(_)) {
"included"
} else {
"included-global"
});
include_path.push(format!("{name}.sieve"));
if let Ok(bytes) = fs::read(include_path.as_path()) {
let script = compiler.compile(&add_crlf(&bytes)).unwrap();
input = Input::script(name, script);
} else if optional {
input = Input::False;
} else {
panic!("Script {} not found.", include_path.display());
}
}
Event::MailboxExists {
mailboxes: mailboxes_,
special_use,
} => {
for action in &actions {
if let Event::FileInto { folder, create, .. } = action
&& *create
&& !mailboxes.contains(folder)
{
mailboxes.push(folder.to_string());
}
}
input = (special_use.is_empty()
&& mailboxes_.iter().all(|n| {
if let Mailbox::Name(n) = n {
mailboxes.contains(n)
} else {
false
}
}))
.into();
}
Event::ListContains {
lists: lists_,
values,
..
} => {
let mut result = false;
'list: for list in &lists_ {
if let Some(list) = lists.get(list) {
for value in &values {
if list.contains(value) {
result = true;
break 'list;
}
}
}
}
input = result.into();
}
Event::DuplicateId { id, .. } => {
input = duplicated_ids.contains(&id).into();
}
Event::Function { id, arguments } => {
if id == u32::MAX {
// Test functions
input = Input::True;
let mut arguments = arguments.into_iter();
let command = arguments.next().unwrap().unwrap_string();
let mut params =
arguments.map(|arg| arg.unwrap_string()).collect::<Vec<_>>();
match command.as_str() {
"test" => {
current_test = params.pop().unwrap();
println!("Running test '{current_test}'...");
}
"test_set" => {
let mut params = params.into_iter();
let target = params.next().expect("test_set parameter");
if target == "message" {
let value = params.next().unwrap();
raw_message_ = if value.eq_ignore_ascii_case(":smtp") {
let mut message = None;
for action in actions.iter().rev() {
if let Event::SendMessage { message_id, .. } =
action
{
let message_ = actions
.iter()
.find_map(|item| {
if let Event::CreatedMessage {
message_id: message_id_,
message,
} = item
&& message_id == message_id_
{
return Some(message);
}
None
})
.unwrap();
/*println!(
"<[{}]>",
std::str::from_utf8(message_).unwrap()
);*/
message = message_.into();
break;
}
}
message.expect("No SMTP message found").to_vec().into()
} else {
value.into_bytes().into()
};
prev_state = (
instance.pos,
instance.script_cache,
instance.script_stack,
instance.vars_global,
instance.vars_local,
instance.vars_match,
)
.into();
continue 'outer;
} else if let Some(envelope) = target.strip_prefix("envelope.")
{
let envelope =
Envelope::try_from(envelope.to_string()).unwrap();
instance.envelope.retain(|(e, _)| e != &envelope);
instance.set_envelope(envelope, params.next().unwrap());
} else if target == "currentdate" {
let bytes = params.next().unwrap().into_bytes();
if let HeaderValue::DateTime(dt) =
MessageStream::new(&bytes).parse_date()
{
instance.current_time = dt.to_timestamp();
} else {
panic!("Invalid currentdate");
}
} else {
panic!("test_set {target} not implemented.");
}
}
"test_message" => {
let mut params = params.into_iter();
input = match params.next().unwrap().as_str() {
":folder" => {
let folder_name = params.next().expect("test_message folder name");
matches!(&instance.final_event, Some(Event::Keep { .. })) ||
actions.iter().any(|a| if !folder_name.eq_ignore_ascii_case("INBOX") {
matches!(a, Event::FileInto { folder, .. } if folder == &folder_name )
} else {
matches!(a, Event::Keep { .. })
})
}
":smtp" => {
actions.iter().any(|a| matches!(a, Event::SendMessage { .. } ))
}
param => panic!("Invalid test_message param '{param}'" ),
}.into();
}
"test_assert_message" => {
let expected_message =
params.first().expect("test_set parameter");
let built_message = instance.build_message();
if expected_message.as_bytes() != built_message {
//fs::write("_deleteme.json", serde_json::to_string_pretty(&Message::parse(&built_message).unwrap()).unwrap()).unwrap();
print!("<[");
print!("{}", String::from_utf8(built_message).unwrap());
println!("]>");
panic!("Message built incorrectly at '{current_test}'");
}
}
"test_config_set" => {
let mut params = params.into_iter();
let name = params.next().unwrap();
let value = params.next().expect("test_config_set value");
match name.as_str() {
"sieve_editheader_protected"
| "sieve_editheader_forbid_add"
| "sieve_editheader_forbid_delete" => {
if !value.is_empty() {
for header_name in value.split(' ') {
instance.runtime.set_protected_header(
header_name.to_string(),
);
}
} else {
instance.runtime.protected_headers.clear();
}
}
"sieve_variables_max_variable_size" => {
instance
.runtime
.set_max_variable_size(value.parse().unwrap());
}
"sieve_valid_ext_list" => {
instance.runtime.set_valid_ext_list(value);
}
"sieve_ext_list_item" => {
lists
.entry(value)
.or_default()
.insert(params.next().expect("list item value"));
}
"sieve_duplicated_id" => {
duplicated_ids.insert(value);
}
"sieve_user_email" => {
instance.set_user_address(value);
}
"sieve_vacation_use_original_recipient" => {
instance.runtime.set_vacation_use_orig_rcpt(
value.eq_ignore_ascii_case("yes"),
);
}
"sieve_vacation_default_subject" => {
instance.runtime.set_vacation_default_subject(value);
}
"sieve_vacation_default_subject_template" => {
instance.runtime.set_vacation_subject_prefix(value);
}
"sieve_spam_status" => {
instance.set_spam_status(SpamStatus::from_number(
value.parse().unwrap(),
));
}
"sieve_spam_status_plus" => {
instance.set_spam_status(
match value.parse::<u32>().unwrap() {
0 => SpamStatus::Unknown,
100.. => SpamStatus::Spam,
n => SpamStatus::MaybeSpam((n as f64) / 100.0),
},
);
}
"sieve_virus_status" => {
instance.set_virus_status(VirusStatus::from_number(
value.parse().unwrap(),
));
}
"sieve_editheader_max_header_size" => {
let mhs = if !value.is_empty() {
value.parse::<usize>().unwrap()
} else {
1024
};
instance.runtime.set_max_header_size(mhs);
compiler.set_max_header_size(mhs);
}
"sieve_include_max_includes" => {
compiler.set_max_includes(if !value.is_empty() {
value.parse::<usize>().unwrap()
} else {
3
});
}
"sieve_include_max_nesting_depth" => {
compiler.set_max_nested_blocks(if !value.is_empty() {
value.parse::<usize>().unwrap()
} else {
3
});
}
param => panic!("Invalid test_config_set param '{param}'"),
}
}
"test_result_execute" => {
input =
(matches!(&instance.final_event, Some(Event::Keep { .. }))
|| actions.iter().any(|a| {
matches!(
a,
Event::Keep { .. }
| Event::FileInto { .. }
| Event::SendMessage { .. }
)
}))
.into();
}
"test_result_action" => {
let param =
params.first().expect("test_result_action parameter");
input = if param == "reject" {
(actions.iter().any(|a| matches!(a, Event::Reject { .. })))
.into()
} else if param == "redirect" {
let param = params
.last()
.expect("test_result_action redirect address");
(actions
.iter()
.any(|a| matches!(a, Event::SendMessage { recipient: Recipient::Address(address), .. } if address == param)))
.into()
} else if param == "keep" {
(matches!(&instance.final_event, Some(Event::Keep { .. }))
|| actions
.iter()
.any(|a| matches!(a, Event::Keep { .. })))
.into()
} else if param == "send_message" {
(actions
.iter()
.any(|a| matches!(a, Event::SendMessage { .. })))
.into()
} else {
panic!("test_result_action {param} not implemented");
};
}
"test_result_action_count" => {
input = (actions.len()
== params.first().unwrap().parse::<usize>().unwrap())
.into();
}
"test_imap_metadata_set" => {
let mut params = params.into_iter();
let first = params.next().expect("metadata parameter");
let (mailbox, annotation) = if first == ":mailbox" {
(
params.next().expect("metadata mailbox name").into(),
params.next().expect("metadata annotation name"),
)
} else {
(None, first)
};
let value = params.next().expect("metadata value");
if let Some(mailbox) = mailbox {
instance.set_medatata((mailbox, annotation), value);
} else {
instance.set_medatata(annotation, value);
}
}
"test_mailbox_create" => {
mailboxes.push(params.pop().expect("mailbox to create"));
}
"test_result_reset" => {
actions.clear();
instance.final_event = Event::Keep {
flags: vec![],
message_id: 0,
}
.into();
instance.metadata.clear();
instance.has_changes = false;
instance.num_redirects = 0;
instance.runtime.vacation_use_orig_rcpt = false;
mailboxes.clear();
lists.clear();
reset_test_boundary();
}
"test_script_compile" => {
let mut include_path = PathBuf::from(base_path);
include_path.push(params.first().unwrap());
if let Ok(bytes) = fs::read(include_path.as_path()) {
let result = compiler.compile(&add_crlf(&bytes));
/*if let Err(err) = &result {
println!("Error: {:?}", err);
}*/
input = result.is_ok().into();
} else {
panic!("Script {} not found.", include_path.display());
}
}
"test_config_reload" => (),
"test_fail" => {
panic!(
"Test '{}' failed: {}",
current_test,
params.pop().unwrap()
);
}
_ => panic!("Test command {command} not implemented."),
}
} else {
let result = match id {
0 => Variable::from("my_value"),
1 => Variable::from(arguments[0].to_string().to_uppercase()),
2 => Variable::from(format!(
"{}-{}",
arguments[0].to_string(),
arguments[1].to_string()
)),
3 => Variable::from(format!(
"{}-{}-{}",
arguments[0].to_string(),
arguments[1].to_string(),
arguments[2].to_string()
)),
4 => true.into(),
5 => false.into(),
_ => {
panic!("Unknown external function {id}");
}
};
input = result.into();
}
}
action => {
actions.push(action);
input = true.into();
}
}
}
return;
}
}
fn add_crlf(bytes: &[u8]) -> Vec<u8> {
let mut result = Vec::with_capacity(bytes.len());
let mut last_ch = 0;
for &ch in bytes {
if ch == b'\n' && last_ch != b'\r' {
result.push(b'\r');
}
result.push(ch);
last_ch = ch;
}
result
}
}
+102
View File
@@ -0,0 +1,102 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use mail_parser::{
Encoding, Header, HeaderName, HeaderValue, MimeHeaders, PartType,
decoders::html::{html_to_text, text_to_html},
};
use crate::{
Context, compiler::grammar::actions::action_convert::Convert, runtime::tests::TestResult,
};
#[derive(Clone, Copy)]
enum Conversion {
TextToHtml,
TextPlainToHtml,
HtmlToText,
}
impl Convert {
pub(crate) fn exec(&self, ctx: &mut Context) -> TestResult {
let _from_media_type = ctx.eval_value(&self.from_media_type);
let _to_media_type = ctx.eval_value(&self.to_media_type);
let from_media_type = _from_media_type.to_string();
let to_media_type = _to_media_type.to_string();
if from_media_type.eq_ignore_ascii_case(to_media_type.as_ref()) {
return TestResult::Bool(false ^ self.is_not);
}
let conversion = if (from_media_type.eq_ignore_ascii_case("text")
|| from_media_type.starts_with("text/"))
&& to_media_type.eq_ignore_ascii_case("text/html")
{
if from_media_type.eq_ignore_ascii_case("text") {
Conversion::TextPlainToHtml
} else {
Conversion::TextToHtml
}
} else if from_media_type.eq_ignore_ascii_case("text/html")
&& to_media_type.eq_ignore_ascii_case("text/plain")
{
Conversion::HtmlToText
} else {
return TestResult::Bool(false ^ self.is_not);
};
let mut did_convert = false;
for part in ctx.message.parts.iter_mut() {
let (new_body, ct) = match (&part.body, conversion) {
(PartType::Html(html), Conversion::HtmlToText) => (
PartType::Text(html_to_text(html.as_ref()).into()),
"text/plain; charset=utf8",
),
(PartType::Text(text), Conversion::TextToHtml) => (
PartType::Html(text_to_html(text.as_ref()).into()),
"text/html; charset=utf8",
),
(PartType::Text(text), Conversion::TextPlainToHtml)
if part
.content_type()
.and_then(|ct| ct.c_subtype.as_ref())
.is_some_and(|st| st.eq_ignore_ascii_case("plain")) =>
{
(
PartType::Html(text_to_html(text.as_ref()).into()),
"text/html; charset=utf8",
)
}
_ => {
continue;
}
};
part.headers = vec![Header {
name: HeaderName::Other("Content-Type".into()),
value: HeaderValue::Text(ct.to_string().into()),
offset_start: 0,
offset_end: 0,
offset_field: 0,
}];
ctx.message_size = ctx.message_size + ct.len() + new_body.len() + 16
- (if part.offset_body != 0 {
(part.offset_end - part.offset_header) as usize
} else {
part.body.len()
});
part.offset_body = 0;
part.body = new_body;
part.encoding = Encoding::QuotedPrintable; //Used as non-mime flag
did_convert = true;
}
if did_convert {
ctx.has_changes = true;
}
TestResult::Bool(did_convert ^ self.is_not)
}
}
+184
View File
@@ -0,0 +1,184 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::borrow::Cow;
use mail_parser::{Header, HeaderName, HeaderValue};
use crate::{
Context,
compiler::grammar::{
MatchType,
actions::{
action_editheader::{AddHeader, DeleteHeader},
action_mime::MimeOpts,
},
},
};
impl AddHeader {
pub(crate) fn exec(&self, ctx: &mut Context) {
let header_name__ = ctx.eval_value(&self.field_name);
let header_name_ = header_name__.to_string();
let mut header_name = String::with_capacity(header_name_.len());
for ch in header_name_.chars() {
if ch.is_alphanumeric() || ch == '-' {
header_name.push(ch);
}
}
if !header_name.is_empty()
&& let Some(header_name) = HeaderName::parse(header_name)
&& !ctx.runtime.protected_headers.contains(&header_name)
{
ctx.has_changes = true;
ctx.insert_header(
ctx.part,
header_name,
ctx.eval_value(&self.value)
.to_string()
.as_ref()
.remove_crlf(ctx.runtime.max_header_size),
self.last,
)
}
}
}
impl DeleteHeader {
pub(crate) fn exec(&self, ctx: &mut Context) {
let header_name__ = ctx.eval_value(&self.field_name);
let header_name_ = header_name__.to_string();
let header_name = if let Some(header_name) = HeaderName::parse(header_name_.as_ref()) {
header_name
} else {
return;
};
let value_patterns = ctx.eval_values(&self.value_patterns);
let mut deleted_headers = Vec::new();
let mut deleted_bytes = 0;
if ctx.runtime.protected_headers.contains(&header_name) {
return;
}
ctx.find_headers(
&[header_name],
self.index,
self.mime_anychild,
|header, part_id, header_pos| {
if !value_patterns.is_empty() {
let did_match = ctx.find_header_values(header, &MimeOpts::None, |value| {
for (pattern_expr, pattern) in
value_patterns.iter().zip(self.value_patterns.iter())
{
if match &self.match_type {
MatchType::Is => self.comparator.is(&value, pattern_expr),
MatchType::Contains => self
.comparator
.contains(value, pattern_expr.to_string().as_ref()),
MatchType::Value(rel_match) => {
self.comparator.relational(rel_match, &value, pattern_expr)
}
MatchType::Matches(_) => self.comparator.matches(
value,
pattern_expr.to_string().as_ref(),
0,
&mut Vec::new(),
),
MatchType::Regex(_) => self.comparator.regex(
pattern,
pattern_expr,
value,
0,
&mut Vec::new(),
),
MatchType::Count(_) => false,
MatchType::List => false,
} {
return true;
}
}
false
});
if !did_match {
return false;
}
}
if header.offset_end != 0 {
deleted_bytes += (header.offset_end - header.offset_field) as usize;
} else {
deleted_bytes += header.name.as_str().len() + header.value.len() + 4;
}
deleted_headers.push((part_id, header_pos));
false
},
);
if !deleted_headers.is_empty() {
ctx.has_changes = true;
for (part_id, header_pos) in deleted_headers.iter().rev() {
ctx.message.parts[*part_id as usize]
.headers
.remove(*header_pos);
}
}
ctx.message_size -= deleted_bytes;
}
}
pub(crate) trait RemoveCrLf {
fn remove_crlf(&self, max_len: usize) -> String;
}
impl RemoveCrLf for &str {
fn remove_crlf(&self, max_len: usize) -> String {
let mut header_value = String::with_capacity(self.len());
for ch in self.chars() {
if !['\n', '\r'].contains(&ch) {
if header_value.len() + ch.len_utf8() <= max_len {
header_value.push(ch);
} else {
return header_value;
}
}
}
header_value
}
}
impl<'x> Context<'x> {
pub(crate) fn insert_header(
&mut self,
part_id: u32,
header_name: HeaderName<'x>,
header_value: impl Into<Cow<'static, str>>,
last: bool,
) {
let header_value = header_value.into();
self.message_size += header_name.len() + header_value.len() + 4;
let header = Header {
name: header_name,
value: HeaderValue::Text(header_value),
offset_start: 0,
offset_end: 0,
offset_field: 0,
};
if !last {
self.message.parts[part_id as usize]
.headers
.insert(0, header);
} else {
self.message.parts[part_id as usize].headers.push(header);
}
}
}
+40
View File
@@ -0,0 +1,40 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{Context, Event, compiler::grammar::actions::action_fileinto::FileInto};
impl FileInto {
pub(crate) fn exec(&self, ctx: &mut Context) {
let folder = ctx.eval_value(&self.folder).to_string().into_owned();
let mut events = Vec::with_capacity(2);
if let Some(event) = ctx.build_message_id() {
events.push(event);
}
if !self.copy
&& !matches!(&ctx.final_event, Some(Event::Keep { flags, .. }) if !flags.is_empty())
{
ctx.final_event = None;
}
events.push(Event::FileInto {
folder,
flags: ctx.get_local_or_global_flags(&self.flags),
mailbox_id: self
.mailbox_id
.as_ref()
.map(|mi| ctx.eval_value(mi).to_string().into_owned()),
special_use: self
.special_use
.as_ref()
.map(|su| ctx.eval_value(su).to_string().into_owned()),
create: self.create,
message_id: ctx.main_message_id,
});
ctx.queued_events = events.into_iter();
}
}
+140
View File
@@ -0,0 +1,140 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
Context,
compiler::{
Value, VariableType,
grammar::actions::action_flags::{Action, EditFlags},
},
};
impl EditFlags {
pub(crate) fn exec(&self, ctx: &mut Context) {
let mut var_name_ = None;
let var_name = self.name.as_ref().unwrap_or_else(|| {
var_name_.get_or_insert_with(|| VariableType::Global("__flags".to_string()))
});
match &self.action {
Action::Set => {
let mut flags_lc = Vec::new();
let mut flags = String::new();
ctx.tokenize_flags(&self.flags, |flag| {
let flag_lc = flag.to_lowercase();
if !flags_lc.contains(&flag_lc) {
if !flags.is_empty() {
flags.push(' ');
}
flags.push_str(flag);
flags_lc.push(flag_lc);
}
false
});
ctx.set_variable(var_name, flags.into());
}
Action::Add => {
let mut new_flags = ctx
.get_variable(var_name)
.map(|v| v.to_string())
.unwrap_or_default()
.into_owned();
let mut current_flags = new_flags
.split(' ')
.map(|f| f.to_lowercase())
.collect::<Vec<_>>();
ctx.tokenize_flags(&self.flags, |flag| {
let flag_lc = flag.to_lowercase();
if !current_flags.contains(&flag_lc) {
if !new_flags.is_empty() {
new_flags.push(' ');
}
new_flags.push_str(flag);
current_flags.push(flag_lc);
}
false
});
ctx.set_variable(var_name, new_flags.into());
}
Action::Remove => {
let mut current_flags = Vec::new();
let mut current_flags_lc = Vec::new();
let flags = ctx
.get_variable(var_name)
.map(|v| v.to_string().into_owned())
.unwrap_or_default();
for flag in flags.split(' ') {
current_flags.push(flag);
current_flags_lc.push(flag.to_lowercase());
}
ctx.tokenize_flags(&self.flags, |flag| {
let flag = flag.to_lowercase();
if let Some(pos) = current_flags_lc.iter().position(|lflag| lflag == &flag) {
current_flags.swap_remove(pos);
current_flags_lc.swap_remove(pos);
}
false
});
ctx.set_variable(var_name, current_flags.join(" ").into());
}
}
}
}
impl Context<'_> {
pub(crate) fn tokenize_flags(
&self,
strings: &[Value],
mut cb: impl FnMut(&str) -> bool,
) -> bool {
for (pos, string) in strings.iter().enumerate() {
let flag_ = self.eval_value(string);
let flag = flag_.to_string();
if !flag.is_empty() {
if pos == 0 && strings.len() == 1 {
for flag in flag.split_ascii_whitespace() {
if !flag.is_empty() && cb(flag) {
return true;
}
}
} else if cb(flag.trim()) {
return true;
}
}
}
false
}
pub(crate) fn get_local_flags(&self, strings: &[Value]) -> Vec<String> {
let mut flags = Vec::new();
self.tokenize_flags(strings, |flag| {
flags.push(flag.to_string());
false
});
flags
}
pub(crate) fn get_global_flags(&self) -> Vec<String> {
match self.vars_global.get("__flags") {
Some(flags) if !flags.is_empty() => flags
.to_string()
.split(' ')
.map(|s| s.to_string())
.collect::<Vec<String>>(),
_ => Vec::new(),
}
}
pub(crate) fn get_local_or_global_flags(&self, strings: &[Value]) -> Vec<String> {
if strings.is_empty() {
self.get_global_flags()
} else {
self.get_local_flags(strings)
}
}
}
+53
View File
@@ -0,0 +1,53 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::sync::Arc;
use crate::{
Context, Event, Script, Sieve,
compiler::grammar::actions::action_include::{Include, Location},
runtime::RuntimeError,
};
pub(crate) enum IncludeResult {
Cached(Arc<Sieve>),
Event(Event),
Error(RuntimeError),
None,
}
impl Include {
pub(crate) fn exec(&self, ctx: &Context) -> IncludeResult {
let script_name = ctx.eval_value(&self.value);
if !script_name.is_empty() {
let script_name = if self.location == Location::Global {
Script::Global(script_name.to_string().into_owned())
} else {
Script::Personal(script_name.to_string().into_owned())
};
let cached_script = ctx.script_cache.get(&script_name);
if !self.once || cached_script.is_none() {
if ctx.script_stack.len() < ctx.runtime.max_nested_includes {
if let Some(script) = cached_script
.or_else(|| ctx.runtime.include_scripts.get(script_name.as_str()))
{
return IncludeResult::Cached(script.clone());
} else {
return IncludeResult::Event(Event::IncludeScript {
name: script_name,
optional: self.optional,
});
}
} else {
return IncludeResult::Error(RuntimeError::TooManyIncludes);
}
}
}
IncludeResult::None
}
}
+582
View File
@@ -0,0 +1,582 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::cmp::Reverse;
use mail_parser::{
Encoding, HeaderName, Message, MessagePart, PartType, decoders::html::html_to_text,
};
use crate::{
Context, Event,
compiler::{
VariableType,
grammar::actions::action_mime::{Enclose, ExtractText, Replace},
},
};
use super::action_editheader::RemoveCrLf;
#[cfg(not(test))]
use mail_builder::headers::message_id::generate_message_id_header;
impl Replace {
pub(crate) fn exec(&self, ctx: &mut Context) {
// Delete children parts
let mut part_ids = ctx.find_nested_parts_ids(false);
part_ids.sort_unstable_by_key(|a| Reverse(*a));
for part_id in part_ids {
ctx.message.parts.remove(part_id as usize);
}
ctx.has_changes = true;
// Update part
let body = ctx.eval_value(&self.replacement).to_string().into_owned();
let body_len = body.len();
let part = &mut ctx.message.parts[ctx.part as usize];
ctx.message_size = ctx.message_size + body_len
- (if part.offset_body != 0 {
(part.offset_end - part.offset_header) as usize
} else {
part.body.len()
});
part.body = PartType::Text(body.into());
part.encoding = if !self.mime {
Encoding::QuotedPrintable
} else {
Encoding::None
};
part.offset_body = 0;
let prev_headers = std::mem::take(&mut part.headers);
let mut add_date = true;
let mut has_original_from = false;
if ctx.part == 0 {
for mut header in prev_headers {
let mut size = (header.offset_end - header.offset_field) as usize;
match &header.name {
HeaderName::Subject => {
if self.subject.is_some() {
header.name = HeaderName::Other("Original-Subject".into());
header.offset_field = header.offset_start;
size += "Original-".len();
}
}
HeaderName::From => {
if self.from.is_some() {
header.name = HeaderName::Other("Original-From".into());
header.offset_field = header.offset_start;
size += "Original-".len();
} else {
has_original_from = true;
}
}
HeaderName::To | HeaderName::Cc | HeaderName::Bcc | HeaderName::Received => (),
HeaderName::Date => {
add_date = false;
}
_ => continue,
}
ctx.message_size += size;
part.headers.push(header);
}
// Add From
let mut add_from = true;
if let Some(from) = self.from.as_ref().map(|f| ctx.eval_value(f))
&& !from.is_empty()
{
ctx.insert_header(
0,
HeaderName::Other("From".into()),
from.to_string()
.as_ref()
.remove_crlf(ctx.runtime.max_header_size),
true,
);
add_from = false;
}
if add_from && !has_original_from {
ctx.insert_header(
0,
HeaderName::Other("From".to_string().into()),
ctx.user_from_field(),
true,
);
}
// Add Subject
if let Some(subject) = self.subject.as_ref().map(|f| ctx.eval_value(f))
&& !subject.is_empty()
{
ctx.insert_header(
0,
HeaderName::Other("Subject".into()),
subject
.to_string()
.as_ref()
.remove_crlf(ctx.runtime.max_header_size),
true,
);
}
// Add Date
if add_date {
#[cfg(not(test))]
let header_value = mail_builder::headers::date::Date::now().to_rfc822();
#[cfg(test)]
let header_value = "Tue, 20 Nov 2022 05:14:20 -0300".to_string();
ctx.insert_header(
0,
HeaderName::Other("Date".to_string().into()),
header_value,
true,
);
}
// Add Message-ID
let mut header_value = Vec::with_capacity(20);
#[cfg(not(test))]
generate_message_id_header(&mut header_value, &ctx.runtime.local_hostname).unwrap();
#[cfg(test)]
header_value.extend_from_slice(b"<auto-generated@message-id>");
ctx.insert_header(
0,
HeaderName::Other("Message-ID".to_string().into()),
String::from_utf8(header_value).unwrap(),
true,
);
}
if !self.mime {
ctx.insert_header(
ctx.part,
HeaderName::Other("Content-Type".into()),
"text/plain; charset=utf-8".to_string(),
true,
);
}
}
}
impl Enclose {
pub(crate) fn exec(&self, ctx: &mut Context) {
let body = ctx.eval_value(&self.value).to_string().into_owned();
let subject = self
.subject
.as_ref()
.map(|s| {
ctx.eval_value(s)
.to_string()
.as_ref()
.remove_crlf(ctx.runtime.max_header_size)
})
.or_else(|| ctx.message.subject().map(|s| s.to_string()))
.unwrap_or_default();
let message = std::mem::take(&mut ctx.message);
#[cfg(test)]
let boundary = make_test_boundary();
#[cfg(not(test))]
let boundary = mail_builder::mime::make_boundary(".");
ctx.message_size += ((boundary.len() + 6) * 3) + body.len() + 2;
ctx.part = 0;
ctx.has_changes = true;
ctx.message = Message {
html_body: Vec::with_capacity(0),
text_body: Vec::with_capacity(0),
attachments: Vec::with_capacity(0),
parts: vec![
MessagePart {
headers: vec![],
is_encoding_problem: false,
body: PartType::Multipart(vec![1, 2]),
encoding: Encoding::None,
offset_header: 0,
offset_body: 0,
offset_end: 0,
},
MessagePart {
headers: vec![],
is_encoding_problem: false,
body: PartType::Text(body.into()),
encoding: Encoding::QuotedPrintable, // Flag non-mime part
offset_header: 0,
offset_body: 0,
offset_end: 0,
},
MessagePart {
headers: vec![],
is_encoding_problem: false,
body: PartType::Message(message),
encoding: Encoding::QuotedPrintable, // Flag non-mime part
offset_header: 0,
offset_body: 0,
offset_end: 0,
},
],
raw_message: b""[..].into(),
};
ctx.insert_header(
0,
HeaderName::Other("Content-Type".into()),
format!("multipart/mixed; boundary=\"{boundary}\""),
true,
);
ctx.insert_header(0, HeaderName::Other("Subject".into()), subject, true);
ctx.insert_header(
1,
HeaderName::Other("Content-Type".into()),
"text/plain; charset=utf-8",
true,
);
ctx.insert_header(
2,
HeaderName::Other("Content-Type".into()),
"message/rfc822",
true,
);
let mut add_date = true;
let mut add_message_id = true;
let mut add_from = true;
for header in &self.headers {
let header = ctx.eval_value(header);
if let Some((mut header_name, mut header_value)) =
header.to_string().as_ref().split_once(':')
{
header_name = header_name.trim();
header_value = header_value.trim();
if !header_value.is_empty()
&& let Some(name) = HeaderName::parse(header_name)
&& !ctx.runtime.protected_headers.contains(&name)
{
match &name {
HeaderName::Date => {
add_date = false;
}
HeaderName::From => {
add_from = false;
}
HeaderName::MessageId => {
add_message_id = false;
}
_ => (),
}
ctx.insert_header(
0,
HeaderName::Other(header_name.to_string().into()),
header_value.remove_crlf(ctx.runtime.max_header_size),
true,
);
}
}
}
if add_from {
ctx.insert_header(
0,
HeaderName::Other("From".to_string().into()),
ctx.user_from_field(),
true,
);
}
if add_date {
#[cfg(not(test))]
let header_value = mail_builder::headers::date::Date::now().to_rfc822();
#[cfg(test)]
let header_value = "Tue, 20 Nov 2022 05:14:20 -0300".to_string();
ctx.insert_header(
0,
HeaderName::Other("Date".to_string().into()),
header_value,
true,
);
}
if add_message_id {
let mut header_value = Vec::with_capacity(20);
#[cfg(not(test))]
generate_message_id_header(&mut header_value, &ctx.runtime.local_hostname).unwrap();
#[cfg(test)]
header_value.extend_from_slice(b"<auto-generated@message-id>");
ctx.insert_header(
0,
HeaderName::Other("Message-ID".to_string().into()),
String::from_utf8(header_value).unwrap(),
true,
);
}
}
}
impl ExtractText {
pub(crate) fn exec(&self, ctx: &mut Context) {
let mut value = String::new();
if !ctx.part_iter_stack.is_empty() {
match ctx.message.parts.get(ctx.part as usize).map(|p| &p.body) {
Some(PartType::Text(text)) => {
value = if let Some(first) = &self.first {
text.chars().take(*first).collect()
} else {
text.as_ref().to_string()
};
}
Some(PartType::Html(html)) => {
value = if let Some(first) = &self.first {
html_to_text(html.as_ref()).chars().take(*first).collect()
} else {
html_to_text(html.as_ref())
};
}
_ => (),
}
if !self.modifiers.is_empty() && !value.is_empty() {
for modifier in &self.modifiers {
value = modifier.apply(&value, ctx);
}
}
}
match &self.name {
VariableType::Local(var_id) => {
if let Some(var) = ctx.vars_local.get_mut(*var_id) {
*var = value.into();
} else {
debug_assert!(false, "Non-existent local variable {var_id}");
}
}
VariableType::Global(var_name) => {
ctx.vars_global
.insert(var_name.to_string().into(), value.into());
}
VariableType::Envelope(env) => {
ctx.add_set_envelope_event(*env, value);
}
_ => (),
}
}
}
enum StackItem<'x> {
Message(&'x Message<'x>),
Boundary(&'x str),
None,
}
impl Context<'_> {
pub(crate) fn build_message_id(&mut self) -> Option<Event> {
if self.has_changes {
self.last_message_id += 1;
self.main_message_id = self.last_message_id;
self.has_changes = false;
let message = self.build_message();
Some(Event::CreatedMessage {
message_id: self.main_message_id,
message,
})
} else {
None
}
}
pub(crate) fn build_message(&mut self) -> Vec<u8> {
let mut current_message = &self.message;
let mut current_boundary = "";
let mut message = Vec::with_capacity(self.message_size);
let mut iter = [0u32].iter();
let mut iter_stack = Vec::new();
let mut last_offset = 0;
'outer: loop {
while let Some(part) = iter
.next()
.and_then(|p| current_message.parts.get(*p as usize))
{
if last_offset > 0 {
message.extend_from_slice(
&current_message.raw_message
[last_offset as usize..part.offset_header as usize],
);
} else if !current_boundary.is_empty()
&& part.offset_end == 0
&& !matches!(iter_stack.last(), Some((StackItem::Message(_), _, _)))
{
message.extend_from_slice(b"\r\n--");
message.extend_from_slice(current_boundary.as_bytes());
message.extend_from_slice(b"\r\n");
}
let mut ct_pos = usize::MAX;
for (header_pos, header) in part.headers.iter().enumerate() {
if header.offset_end != 0 {
if header.offset_field != header.offset_start {
message.extend_from_slice(
&current_message.raw_message
[header.offset_field as usize..header.offset_end as usize],
);
} else {
// Renamed header
message.extend_from_slice(header.name.as_str().as_bytes());
message.extend_from_slice(b":");
message.extend_from_slice(
&current_message.raw_message
[header.offset_start as usize..header.offset_end as usize],
);
}
} else {
if header.name == HeaderName::Other("Content-Type".into()) {
ct_pos = header_pos;
}
message.extend_from_slice(header.name.as_str().as_bytes());
message.extend_from_slice(b": ");
message.extend_from_slice(header.value.as_text().unwrap_or("").as_bytes());
message.extend_from_slice(b"\r\n");
}
}
if part.offset_body != 0 || part.encoding != Encoding::None {
// Add CRLF unless this is a :mime replaced part
message.extend_from_slice(b"\r\n");
}
if part.offset_body != 0 {
// Original message part
if let PartType::Multipart(subparts) = &part.body {
// Multiparts contain offsets of the entire part, do not add.
iter_stack.push((
StackItem::None,
part,
std::mem::replace(&mut iter, subparts.iter()),
));
last_offset = part.offset_body;
continue 'outer;
} else {
message.extend_from_slice(
&current_message.raw_message
[part.offset_body as usize..part.offset_end as usize],
)
}
} else {
match &part.body {
PartType::Message(nested_message) => {
// Enclosed message
iter_stack.push((
StackItem::Message(current_message),
part,
std::mem::replace(&mut iter, [0].iter()),
));
current_message = nested_message;
continue 'outer;
}
PartType::Multipart(subparts) => {
// Multipart enclosing nested message, obtain MIME boundary
let prev_boundary = std::mem::replace(
&mut current_boundary,
if ct_pos != usize::MAX {
part.headers[ct_pos]
.value
.as_text()
.and_then(|h| h.split_once("boundary=\""))
.and_then(|(_, h)| h.split_once('\"'))
.map(|(h, _)| h)
} else {
None
}
.unwrap_or("invalid-boundary"),
);
// Enclose multipart
iter_stack.push((
StackItem::Boundary(prev_boundary),
part,
std::mem::replace(&mut iter, subparts.iter()),
));
continue 'outer;
}
_ => {
// Replaced part
message.extend_from_slice(part.contents());
}
}
}
last_offset = part.offset_end;
}
if let Some((prev_item, prev_part, prev_iter)) = iter_stack.pop() {
match prev_item {
StackItem::Message(prev_message) => {
if last_offset > 0 {
if let Some(bytes) =
current_message.raw_message.get(last_offset as usize..)
{
message.extend_from_slice(bytes);
}
last_offset = 0;
}
current_message = prev_message;
}
StackItem::Boundary(prev_boundary) => {
if !current_boundary.is_empty() {
message.extend_from_slice(b"\r\n--");
message.extend_from_slice(current_boundary.as_bytes());
message.extend_from_slice(b"--\r\n");
}
current_boundary = prev_boundary;
}
StackItem::None => {
message.extend_from_slice(
&current_message.raw_message
[last_offset as usize..prev_part.offset_end as usize],
);
last_offset = prev_part.offset_end;
}
}
iter = prev_iter;
} else {
break;
}
}
if last_offset > 0
&& let Some(bytes) = current_message.raw_message.get(last_offset as usize..)
{
message.extend_from_slice(bytes);
}
message
}
}
#[cfg(test)]
thread_local!(static COUNTER: std::cell::Cell<u64> = 0.into());
#[cfg(test)]
pub(crate) fn make_test_boundary() -> String {
format!("boundary_{}", COUNTER.with(|c| { c.replace(c.get() + 1) }))
}
#[cfg(test)]
pub(crate) fn reset_test_boundary() {
COUNTER.with(|c| c.replace(0));
}
+575
View File
@@ -0,0 +1,575 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use mail_builder::headers::{date::Date, message_id::generate_message_id_header};
use mail_parser::{HeaderName, decoders::quoted_printable::HEX_MAP};
use crate::{
Context, Event, Importance, Recipient,
compiler::grammar::actions::{
action_notify::Notify,
action_redirect::{ByTime, Ret},
},
};
use super::action_vacation::MAX_SUBJECT_LEN;
impl Notify {
pub(crate) fn exec(&self, ctx: &mut Context) {
// Do not notify on Auto-Submitted messages
for header in &ctx.message.parts[0].headers {
if matches!(&header.name, HeaderName::Other(name) if name.eq_ignore_ascii_case("Auto-Submitted"))
&& header
.value
.as_text()
.is_none_or(|v| !v.eq_ignore_ascii_case("no"))
{
return;
}
}
let uri = ctx.eval_value(&self.method).to_string().into_owned();
let (scheme, params) = if let Some(parts) = parse_uri(&uri) {
parts
} else {
return;
};
let has_fcc = self.fcc.is_some();
let is_mailto = scheme.eq_ignore_ascii_case("mailto")
&& ctx.num_out_messages < ctx.runtime.max_out_messages;
let mut events = Vec::with_capacity(3);
if is_mailto || has_fcc {
let params = if is_mailto {
if let Some(params) = parse_mailto(params) {
params
} else {
return;
}
} else {
MailtoMessage {
to: Vec::new(),
cc: Vec::new(),
bcc: Vec::new(),
body: None,
headers: Vec::new(),
}
};
let from = if let Some(from) = &self.from {
let from = ctx.eval_value(from).to_string().into_owned();
if from
.to_ascii_lowercase()
.contains(&ctx.user_address.to_ascii_lowercase())
{
from
} else {
ctx.user_from_field()
}
} else {
ctx.user_from_field()
};
let notify_message = self
.message
.as_ref()
.map(|m| ctx.eval_value(m).to_string().into_owned());
let message_len = params
.to
.iter()
.chain(params.cc.iter())
.map(|a| a.len() + 4)
.sum::<usize>()
+ params
.headers
.iter()
.map(|(h, v)| h.len() + v.len() + 4)
.sum::<usize>()
+ params.body.as_ref().map_or(0, |b| b.len())
+ notify_message.as_ref().map_or(0, |b| b.len())
+ from.len()
+ 200;
let mut message = Vec::with_capacity(message_len);
message.extend_from_slice(b"From: ");
message.extend_from_slice(from.as_bytes());
message.extend_from_slice(b"\r\n");
for (header, addresses) in [("To: ", &params.to), ("Cc: ", &params.cc)] {
if !addresses.is_empty() {
message.extend_from_slice(header.as_bytes());
for (pos, address) in addresses.iter().enumerate() {
if pos > 0 {
message.extend_from_slice(b", ");
}
if !address.contains('<') {
message.push(b'<');
}
message.extend_from_slice(address.as_bytes());
if !address.contains('<') {
message.push(b'>');
}
}
message.extend_from_slice(b"\r\n");
}
}
let mut has_subject = None;
let mut has_date = false;
let mut has_message_id = false;
for (header, value) in &params.headers {
match header {
HeaderName::Subject => {
has_subject = value.into();
continue;
}
HeaderName::Date => {
has_date = true;
}
HeaderName::MessageId => {
has_message_id = true;
}
HeaderName::From => {
continue;
}
_ => (),
}
message.extend_from_slice(header.as_str().as_bytes());
message.extend_from_slice(b": ");
message.extend_from_slice(value.as_bytes());
message.extend_from_slice(b"\r\n");
}
if !has_date {
message.extend_from_slice(b"Date: ");
message.extend_from_slice(Date::now().to_rfc822().as_bytes());
message.extend_from_slice(b"\r\n");
}
if !has_message_id {
message.extend_from_slice(b"Message-ID: ");
generate_message_id_header(&mut message, &ctx.runtime.local_hostname).unwrap();
message.extend_from_slice(b"\r\n");
}
let (importance, priority) =
self.importance
.as_ref()
.map_or(("Normal", "3 (Normal)"), |i| {
match ctx.eval_value(i).to_string().as_ref() {
"1" => ("High", "1 (High)"),
"3" => ("Low", "5 (Low)"),
_ => ("Normal", "3 (Normal)"),
}
});
message.extend_from_slice(b"Importance: ");
message.extend_from_slice(importance.as_bytes());
message.extend_from_slice(b"\r\n");
message.extend_from_slice(b"X-Priority: ");
message.extend_from_slice(priority.as_bytes());
message.extend_from_slice(b"\r\n");
message.extend_from_slice(b"Subject: ");
let subject = if let Some(subject) = has_subject {
subject.as_str()
} else if let Some(subject) = &notify_message {
subject.as_ref()
} else {
ctx.message.subject().unwrap_or_default()
};
let mut iter = subject.chars().enumerate();
let mut buf = [0; 4];
#[allow(clippy::while_let_on_iterator)]
while let Some((pos, char)) = iter.next() {
if pos < MAX_SUBJECT_LEN {
message.extend_from_slice(char.encode_utf8(&mut buf).as_bytes());
} else {
break;
}
}
if iter.next().is_some() {
message.extend_from_slice('…'.encode_utf8(&mut buf).as_bytes());
}
message.extend_from_slice(b"\r\n");
message.extend_from_slice(b"Auto-Submitted: auto-notified\r\n");
message.extend_from_slice(b"X-Sieve: yes\r\n");
message.extend_from_slice(b"Content-type: text/plain; charset=utf-8\r\n\r\n");
if let Some(body) = params.body {
message.extend_from_slice(body.as_bytes());
} else if let Some(subject) = &notify_message {
message.extend_from_slice(subject.as_bytes());
} else if let Some(subject) = ctx.message.subject() {
message.extend_from_slice(subject.as_bytes());
}
ctx.last_message_id += 1;
events.push(Event::CreatedMessage {
message_id: ctx.last_message_id,
message,
});
if is_mailto {
events.push(Event::SendMessage {
recipient: Recipient::Group(
params
.to
.into_iter()
.chain(params.cc)
.chain(params.bcc)
.map(|addr| {
if let Some((addr, _)) = addr
.rsplit_once('<')
.and_then(|(_, addr)| addr.rsplit_once('>'))
{
addr.to_string()
} else {
addr
}
})
.collect(),
),
notify: crate::compiler::grammar::actions::action_redirect::Notify::Never,
return_of_content: Ret::Default,
by_time: ByTime::None,
message_id: ctx.last_message_id,
});
}
}
if !is_mailto {
events.push(Event::Notify {
method: uri,
from: self
.from
.as_ref()
.map(|f| ctx.eval_value(f).to_string().into_owned()),
importance: self.importance.as_ref().map_or(Importance::Normal, |i| {
match ctx.eval_value(i).to_string().as_ref() {
"1" => Importance::High,
"3" => Importance::Low,
_ => Importance::Normal,
}
}),
options: ctx.eval_values_owned(&self.options),
message: self
.message
.as_ref()
.map(|m| ctx.eval_value(m).to_string().into_owned())
.or_else(|| ctx.message.subject().map(|s| s.to_string()))
.unwrap_or_default(),
});
ctx.num_out_messages += 1;
}
if let Some(fcc) = &self.fcc {
// File carbon copy
events.push(Event::FileInto {
folder: ctx.eval_value(&fcc.mailbox).to_string().into_owned(),
flags: ctx.get_local_flags(&fcc.flags),
mailbox_id: fcc
.mailbox_id
.as_ref()
.map(|m| ctx.eval_value(m).to_string().into_owned()),
special_use: fcc
.special_use
.as_ref()
.map(|s| ctx.eval_value(s).to_string().into_owned()),
create: fcc.create,
message_id: ctx.last_message_id,
});
}
ctx.queued_events = events.into_iter();
}
}
pub fn validate_from(addr: &str) -> bool {
let mut has_at = false;
let mut has_dot = false;
let mut in_quote = false;
let mut in_angle = false;
let mut last_ch = 0;
for &ch in addr.as_bytes().iter() {
match ch {
b'\"' if last_ch != b'\\' => {
in_quote = !in_quote;
}
b'<' if !in_quote => {
if !in_angle {
in_angle = true;
has_at = false;
has_dot = false;
} else {
return false;
}
}
b'>' if !in_quote => {
if in_angle {
in_angle = false;
} else {
return false;
}
}
b'@' if !in_quote => {
if !has_at && last_ch.is_ascii_alphanumeric() {
has_at = true;
} else {
return false;
}
}
b'.' if !in_quote && has_at => {
has_dot = true;
}
_ => (),
}
last_ch = ch;
}
has_dot && has_at && !in_angle
}
pub fn validate_uri(uri: &str) -> Option<&str> {
let (scheme, uri) = parse_uri(uri)?;
if scheme.eq_ignore_ascii_case("mailto") {
parse_mailto(uri)?;
scheme.into()
} else if ["xmpp", "tel", "http", "https"].contains(&scheme) {
scheme.into()
} else {
None
}
}
pub(crate) fn parse_uri(uri: &str) -> Option<(&str, &str)> {
let (scheme, uri) = uri.split_once(':')?;
if !uri.is_empty() {
Some((scheme, uri))
} else {
None
}
}
pub enum Mailto {
Header(HeaderName<'static>),
Body,
Other(String),
}
enum State {
Address((HeaderName<'static>, bool)),
ParamName,
ParamValue(Mailto),
}
#[derive(Default)]
struct MailtoMessage {
to: Vec<String>,
cc: Vec<String>,
bcc: Vec<String>,
body: Option<String>,
headers: Vec<(HeaderName<'static>, String)>,
}
fn parse_mailto(uri: &str) -> Option<MailtoMessage> {
let mut params = MailtoMessage::default();
let mut state = State::Address((HeaderName::To, false));
let mut buf = Vec::new();
let uri_ = uri.as_bytes();
let mut iter = uri_.iter();
let mut has_addresses = false;
while let Some(&ch) = iter.next() {
match ch {
b'%' => {
let hex1 = HEX_MAP[*iter.next()? as usize];
let hex2 = HEX_MAP[*iter.next()? as usize];
if hex1 != -1 && hex2 != -1 {
let ch = ((hex1 as u8) << 4) | hex2 as u8;
match &state {
State::Address((header, has_at)) => match ch {
b',' => {
if *has_at {
insert_address(
&mut params,
header.clone(),
String::from_utf8(std::mem::take(&mut buf)).ok()?,
);
has_addresses = true;
state = State::Address((header.clone(), false));
} else {
return None;
}
}
b'@' => {
if !*has_at {
state = State::Address((header.clone(), true));
buf.push(ch);
} else {
return None;
}
}
_ => {
buf.push(ch);
}
},
_ => buf.push(ch),
}
} else {
return None;
}
}
b',' => match &state {
State::Address((header, true)) => {
insert_address(
&mut params,
header.clone(),
String::from_utf8(std::mem::take(&mut buf)).ok()?,
);
state = State::Address((header.clone(), false));
has_addresses = true;
}
State::ParamValue(_) => buf.push(ch),
_ => return None,
},
b'?' => match &state {
State::Address((header, has_at)) if *has_at || buf.is_empty() => {
if !buf.is_empty() {
insert_address(
&mut params,
header.clone(),
String::from_utf8(std::mem::take(&mut buf)).ok()?,
);
has_addresses = true;
}
state = State::ParamName;
}
State::ParamValue(_) => buf.push(ch),
_ => return None,
},
b'@' => match &state {
State::Address((header, false)) if !buf.is_empty() => {
buf.push(ch);
state = State::Address((header.clone(), true));
}
State::ParamName | State::ParamValue(_) => buf.push(ch),
_ => return None,
},
b'=' => match &state {
State::ParamName if !buf.is_empty() => {
let param = String::from_utf8(std::mem::take(&mut buf)).ok()?;
state = HeaderName::parse(param)
.map(|hdr| match hdr {
HeaderName::To | HeaderName::Cc | HeaderName::Bcc => {
State::Address((hdr, false))
}
HeaderName::Other(param) => {
if param.eq_ignore_ascii_case("body") {
State::ParamValue(Mailto::Body)
} else {
State::ParamValue(Mailto::Other(param.into_owned()))
}
}
_ => State::ParamValue(Mailto::Header(hdr)),
})
.unwrap_or_else(|| State::ParamValue(Mailto::Other(String::new())));
}
State::ParamValue(_) => buf.push(ch),
_ => return None,
},
b'&' => match state {
State::Address((header, true)) => {
if !buf.is_empty() {
insert_address(
&mut params,
header,
String::from_utf8(std::mem::take(&mut buf)).ok()?,
);
}
state = State::ParamName;
}
State::ParamValue(param) => {
if !buf.is_empty() {
let value = String::from_utf8(std::mem::take(&mut buf)).ok()?;
match param {
Mailto::Header(header) => params.headers.push((header, value)),
Mailto::Body => params.body = value.into(),
Mailto::Other(header) => params.headers.push((header.into(), value)),
}
}
state = State::ParamName;
}
_ => return None,
},
_ => match &state {
State::ParamName => {
if ch.is_ascii_alphanumeric() || b"-_".contains(&ch) {
buf.push(ch);
} else {
return None;
}
}
_ => {
if !ch.is_ascii_whitespace() {
buf.push(ch);
}
}
},
}
}
if !buf.is_empty() {
let value = String::from_utf8(std::mem::take(&mut buf)).ok()?;
match state {
State::Address((header, true)) => {
insert_address(&mut params, header, value);
has_addresses = true;
}
State::ParamName => {
params
.headers
.push((HeaderName::Other(value.into()), String::new()));
}
State::ParamValue(param) => match param {
Mailto::Header(header) => params.headers.push((header, value)),
Mailto::Body => params.body = value.into(),
Mailto::Other(header) => params
.headers
.push((HeaderName::Other(header.into()), value)),
},
_ => return None,
}
}
if has_addresses { Some(params) } else { None }
}
#[inline(always)]
fn insert_address(params: &mut MailtoMessage, name: HeaderName, value: String) {
if !params
.to
.iter()
.chain(params.cc.iter())
.chain(params.bcc.iter())
.any(|v| v.eq_ignore_ascii_case(&value))
{
match name {
HeaderName::To => {
params.to.push(value);
}
HeaderName::Cc => {
params.cc.push(value);
}
HeaderName::Bcc => {
params.bcc.push(value);
}
_ => (),
}
}
}
+135
View File
@@ -0,0 +1,135 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use mail_parser::{DateTime, HeaderName};
use crate::{
Context, Event, Recipient,
compiler::grammar::actions::action_redirect::{ByTime, Redirect},
};
impl Redirect {
pub(crate) fn exec(&self, ctx: &mut Context) {
if let Some(address) = sanitize_address(ctx.eval_value(&self.address).to_string().as_ref())
&& ctx.num_redirects < ctx.runtime.max_redirects
&& ctx.num_out_messages < ctx.runtime.max_out_messages
&& ctx.message.parts[0]
.headers
.iter()
.filter(|h| matches!(&h.name, HeaderName::Received))
.count()
< ctx.runtime.max_received_headers
{
// Try to avoid forwarding loops
if !self.list && address.eq_ignore_ascii_case(ctx.user_address.as_ref()) {
return;
}
if !self.copy && matches!(&ctx.final_event, Some(Event::Keep { .. })) {
ctx.final_event = None;
}
let mut events = Vec::with_capacity(2);
if let Some(event) = ctx.build_message_id() {
events.push(event);
}
ctx.num_redirects += 1;
ctx.num_out_messages += 1;
events.push(Event::SendMessage {
recipient: if !self.list {
Recipient::Address(address)
} else {
Recipient::List(address)
},
notify: self.notify.clone(),
return_of_content: self.return_of_content.clone(),
by_time: match &self.by_time {
ByTime::Relative {
rlimit,
mode,
trace,
} => ByTime::Relative {
rlimit: *rlimit,
mode: mode.clone(),
trace: *trace,
},
ByTime::Absolute {
alimit,
mode,
trace,
} => ByTime::Absolute {
alimit: DateTime::parse_rfc3339(
ctx.eval_value(alimit).to_string().as_ref(),
)
.and_then(|d| {
if d.is_valid() {
d.to_timestamp().into()
} else {
None
}
})
.unwrap_or(0),
mode: mode.clone(),
trace: *trace,
},
ByTime::None => ByTime::None,
},
message_id: ctx.main_message_id,
});
ctx.queued_events = events.into_iter();
}
}
}
pub(crate) fn sanitize_address(addr: &str) -> Option<String> {
let mut result = String::with_capacity(addr.len());
let mut in_quote = false;
let mut last_ch = '\n';
let mut has_at = false;
let mut has_dot = false;
for ch in addr.chars() {
match ch {
'\"' => {
if !in_quote {
in_quote = true;
} else if last_ch != '\\' {
in_quote = false;
}
}
'@' if !in_quote => {
if !has_at && !result.is_empty() {
has_at = true;
result.push(ch);
} else {
return None;
}
}
'.' if !in_quote && has_at && !has_dot => {
has_dot = true;
result.push(ch);
}
'<' => {
result.clear();
has_at = false;
has_dot = false;
}
'>' => (),
_ => {
if !ch.is_ascii_whitespace() || in_quote {
result.push(ch);
}
}
}
last_ch = ch;
}
if !result.is_empty() && has_at && has_dot {
Some(result)
} else {
None
}
}
+199
View File
@@ -0,0 +1,199 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
Context, Envelope, Event,
compiler::{
VariableType,
grammar::actions::action_set::{Modifier, Set},
},
runtime::Variable,
};
use std::fmt::Write;
impl Set {
pub(crate) fn exec(&self, ctx: &mut Context) {
let mut value = ctx.eval_value(&self.value);
for modifier in &self.modifiers {
value = modifier.apply(value.to_string().as_ref(), ctx).into();
}
ctx.set_variable(&self.name, value);
}
}
impl Context<'_> {
pub(crate) fn set_variable(&mut self, var_name: &VariableType, mut variable: Variable) {
if variable.len() > self.runtime.max_variable_size {
let mut new_variable = String::with_capacity(self.runtime.max_variable_size);
for ch in variable.to_string().chars() {
if ch.len_utf8() + new_variable.len() <= self.runtime.max_variable_size {
new_variable.push(ch);
} else {
break;
}
}
variable = new_variable.into();
}
match var_name {
VariableType::Local(var_id) => {
if let Some(var) = self.vars_local.get_mut(*var_id) {
*var = variable.clone();
} else {
debug_assert!(false, "Non-existent local variable {var_id}");
}
}
VariableType::Global(var_name) => {
self.vars_global
.insert(var_name.to_string().into(), variable.clone());
}
VariableType::Envelope(env) => {
self.add_set_envelope_event(*env, variable.to_string().into_owned());
}
_ => (),
}
}
pub(crate) fn add_set_envelope_event(&mut self, envelope: Envelope, value: String) {
let mut did_find = false;
for (name, val) in self.envelope.iter_mut() {
if *name == envelope {
*val = Variable::String(value.clone().into());
did_find = true;
break;
}
}
if !did_find {
self.envelope
.push((envelope, Variable::String(value.clone().into())));
}
self.queued_events = vec![Event::SetEnvelope { envelope, value }].into_iter();
}
pub(crate) fn get_variable(&self, var_name: &VariableType) -> Option<&Variable> {
match var_name {
VariableType::Local(var_id) => self.vars_local.get(*var_id),
VariableType::Global(var_name) => self.vars_global.get(var_name.as_str()),
VariableType::Envelope(env) => self
.envelope
.iter()
.find_map(|(name, val)| if name == env { Some(val) } else { None }),
_ => unreachable!(),
}
}
}
impl Modifier {
pub(crate) fn apply(&self, input: &str, ctx: &Context) -> String {
let max_len = ctx.runtime.max_variable_size;
match self {
Modifier::Lower => input.to_lowercase(),
Modifier::Upper => input.to_uppercase(),
Modifier::LowerFirst => {
let mut result = String::with_capacity(input.len());
for (pos, char) in input.chars().enumerate() {
if result.len() + char.len_utf8() <= max_len {
if pos != 0 {
result.push(char);
} else {
for char in char.to_lowercase() {
result.push(char);
}
}
} else {
return result;
}
}
result
}
Modifier::UpperFirst => {
let mut result = String::with_capacity(input.len());
for (pos, char) in input.chars().enumerate() {
if result.len() + char.len_utf8() <= max_len {
if pos != 0 {
result.push(char);
} else {
for char in char.to_uppercase() {
result.push(char);
}
}
} else {
return result;
}
}
result
}
Modifier::QuoteWildcard => {
let mut result = String::with_capacity(input.len());
for char in input.chars() {
if ['*', '\\', '?'].contains(&char) {
if result.len() + char.len_utf8() < max_len {
result.push('\\');
result.push(char);
} else {
return result;
}
} else if result.len() + char.len_utf8() <= max_len {
result.push(char);
} else {
return result;
}
}
result
}
Modifier::QuoteRegex => {
let mut result = String::with_capacity(input.len());
for char in input.chars() {
if [
'*', '\\', '?', '.', '[', ']', '(', ')', '+', '{', '}', '|', '^', '=', ':',
'$',
]
.contains(&char)
{
if result.len() + char.len_utf8() < max_len {
result.push('\\');
result.push(char);
} else {
return result;
}
} else if result.len() + char.len_utf8() <= max_len {
result.push(char);
} else {
return result;
}
}
result
}
Modifier::Length => input.chars().count().to_string(),
Modifier::EncodeUrl => {
let mut buf = [0; 4];
let mut result = String::with_capacity(input.len());
for char in input.chars() {
if char.is_ascii_alphanumeric() || ['-', '.', '_', '~'].contains(&char) {
if result.len() < max_len {
result.push(char);
} else {
return result;
}
} else if result.len() + (char.len_utf8() * 3) <= max_len {
for byte in char.encode_utf8(&mut buf).as_bytes().iter() {
write!(result, "%{byte:02x}").ok();
}
} else {
return result;
}
}
result
}
Modifier::Replace { find, replace } => input.replace(
ctx.eval_value(find).to_string().as_ref(),
ctx.eval_value(replace).to_string().as_ref(),
),
}
}
}
+336
View File
@@ -0,0 +1,336 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::borrow::Cow;
use mail_builder::headers::{date::Date, message_id::generate_message_id_header};
use mail_parser::{HeaderName, HeaderValue};
use crate::{
Context, Envelope, Event, Recipient,
compiler::grammar::{
AddressPart,
actions::{
action_redirect::{ByTime, Notify, Ret},
action_vacation::{Period, TestVacation, Vacation},
},
},
runtime::tests::TestResult,
};
pub(crate) const MAX_SUBJECT_LEN: usize = 256;
impl TestVacation {
pub(crate) fn exec(&self, ctx: &mut Context) -> TestResult {
let mut from = String::new();
let mut user_addresses = Vec::new();
if ctx.num_out_messages >= ctx.runtime.max_out_messages {
return TestResult::Bool(false);
}
for (name, value) in &ctx.envelope {
if !value.is_empty() {
match name {
Envelope::From => {
from = value.to_string().to_ascii_lowercase();
}
Envelope::To if !ctx.runtime.vacation_use_orig_rcpt => {
user_addresses.push(value.to_string());
}
Envelope::Orcpt if ctx.runtime.vacation_use_orig_rcpt => {
user_addresses.push(value.to_string());
}
_ => (),
}
}
}
// Add user specified addresses
for address in &self.addresses {
let address = ctx.eval_value(address).to_string().into_owned();
if !address.is_empty() {
user_addresses.push(address.into());
}
}
if !ctx.user_address.is_empty() {
user_addresses.push(ctx.user_address.as_ref().into());
}
// Do not reply to own address
if from.is_empty()
|| user_addresses.is_empty()
|| from.starts_with("mailer-daemon")
|| from.starts_with("owner-")
|| from.contains("-request@")
|| user_addresses.iter().any(|a| a.eq_ignore_ascii_case(&from))
{
return TestResult::Bool(false);
}
// Check headers
let mut found_rcpt = false;
let mut received_count = 0;
for header in &ctx.message.parts[0].headers {
match &header.name {
HeaderName::To
| HeaderName::Cc
| HeaderName::Bcc
| HeaderName::ResentTo
| HeaderName::ResentBcc
| HeaderName::ResentCc
if !found_rcpt =>
{
found_rcpt = ctx.find_addresses(header, &AddressPart::All, |addr| {
user_addresses.iter().any(|a| a.eq_ignore_ascii_case(addr))
});
}
HeaderName::ListArchive
| HeaderName::ListHelp
| HeaderName::ListId
| HeaderName::ListOwner
| HeaderName::ListPost
| HeaderName::ListSubscribe
| HeaderName::ListUnsubscribe => {
// Do not send vacation responses to lists
return TestResult::Bool(false);
}
HeaderName::Received => {
received_count += 1;
}
HeaderName::Other(header_name) => {
if header_name.eq_ignore_ascii_case("Auto-Submitted") {
if header
.value
.as_text()
.is_none_or(|v| !v.eq_ignore_ascii_case("no"))
{
return TestResult::Bool(false);
}
} else if header_name.eq_ignore_ascii_case("X-Auto-Response-Suppress") {
if header.value.as_text().is_some_and(|v| {
v.to_ascii_lowercase()
.split(',')
.any(|v| ["all", "oof"].contains(&v.trim()))
}) {
return TestResult::Bool(false);
}
} else if header_name.eq_ignore_ascii_case("Precedence")
&& header
.value
.as_text()
.is_some_and(|v| v.eq_ignore_ascii_case("bulk"))
{
return TestResult::Bool(false);
}
}
_ => (),
}
}
// No user address found in header or possible loop
if found_rcpt && received_count <= ctx.runtime.max_received_headers {
TestResult::Event {
event: Event::DuplicateId {
id: if let Some(handle) = &self.handle {
format!("_v{}{}", from, ctx.eval_value(handle).to_string())
} else {
format!("_v{}{}", from, ctx.eval_value(&self.reason).to_string())
},
expiry: match &self.period {
Period::Days(days) => days * 86400,
Period::Seconds(seconds) => *seconds,
Period::Default => ctx.runtime.default_vacation_expiry,
},
last: false,
},
is_not: true,
}
} else {
TestResult::Bool(false)
}
}
}
impl Vacation {
pub(crate) fn exec(&self, ctx: &mut Context) {
let mut vacation_to = Cow::from("");
for (name, value) in &ctx.envelope {
if !value.is_empty() && name == &Envelope::From {
vacation_to = value.to_string();
break;
}
}
// Check headers
let mut vacation_subject = if let Some(subject) = &self.subject {
ctx.eval_value(subject)
} else {
"".into()
};
// Check headers
let mut message_id = None;
let mut vacation_to_full = None;
let mut references = None;
for header in &ctx.message.parts[0].headers {
match &header.name {
HeaderName::Subject if vacation_subject.is_empty() => {
if let Some(subject) = header.value.as_text() {
let mut vacation_subject_ = String::with_capacity(MAX_SUBJECT_LEN);
let mut iter = ctx
.runtime
.vacation_subject_prefix
.chars()
.chain(subject.chars())
.enumerate();
#[allow(clippy::while_let_on_iterator)]
while let Some((pos, char)) = iter.next() {
if pos < MAX_SUBJECT_LEN {
vacation_subject_.push(char);
} else {
break;
}
}
if iter.next().is_some() {
vacation_subject_.push('…');
}
vacation_subject = vacation_subject_.into();
}
}
HeaderName::MessageId => {
message_id = header.value.as_text();
}
HeaderName::References if header.offset_start > 0 => {
references = (&ctx.message.raw_message
[header.offset_start as usize..header.offset_end as usize])
.into();
}
HeaderName::From | HeaderName::Sender
if matches!(&header.value, HeaderValue::Address(address) if address.contains(vacation_to.as_ref()))
&& header.offset_start > 0 =>
{
vacation_to_full = (&ctx.message.raw_message
[header.offset_start as usize..header.offset_end as usize])
.into();
}
_ => (),
}
}
// Build message
let vacation_from = if let Some(from) = &self.from {
ctx.eval_value(from)
} else if !ctx.user_address.is_empty() {
ctx.user_from_field().into()
} else if let Some(addr) = ctx
.envelope
.iter()
.find_map(|(n, v)| if n == &Envelope::To { Some(v) } else { None })
{
addr.to_string().into()
} else {
"".into()
};
if vacation_subject.is_empty() {
vacation_subject = ctx.runtime.vacation_default_subject.as_ref().into();
}
let vacation_body = ctx.eval_value(&self.reason);
let message_len = vacation_body.len()
+ vacation_from.len()
+ vacation_to_full
.as_ref()
.map_or(vacation_to.len(), |t| t.len())
+ vacation_subject.len()
+ message_id.as_ref().map_or(0, |m| m.len() * 2)
+ references.as_ref().map_or(0, |m| m.len())
+ 160;
let mut message = Vec::with_capacity(message_len);
write_header(&mut message, "From: ", vacation_from.to_string().as_ref());
if let Some(vacation_to_full) = vacation_to_full {
message.extend_from_slice(b"To:");
message.extend_from_slice(vacation_to_full);
} else {
write_header(&mut message, "To: ", vacation_to.to_string().as_ref());
}
write_header(
&mut message,
"Subject: ",
vacation_subject.to_string().as_ref(),
);
if let Some(message_id) = message_id {
message.extend_from_slice(b"In-Reply-To: <");
message.extend_from_slice(message_id.as_bytes());
message.extend_from_slice(b">\r\n");
message.extend_from_slice(b"References: <");
message.extend_from_slice(message_id.as_bytes());
if let Some(references) = references {
message.extend_from_slice(b"> ");
message.extend_from_slice(references);
} else {
message.extend_from_slice(b">\r\n");
}
}
message.extend_from_slice(b"Date: ");
message.extend_from_slice(Date::now().to_rfc822().as_bytes());
message.extend_from_slice(b"\r\n");
message.extend_from_slice(b"Message-ID: ");
generate_message_id_header(&mut message, &ctx.runtime.local_hostname).unwrap();
message.extend_from_slice(b"\r\n");
write_header(&mut message, "Auto-Submitted: ", "auto-replied");
if !self.mime {
message.extend_from_slice(b"Content-type: text/plain; charset=utf-8\r\n\r\n");
}
message.extend_from_slice(vacation_body.to_string().as_bytes());
// Add action
let mut events = Vec::with_capacity(3);
ctx.last_message_id += 1;
ctx.num_out_messages += 1;
events.push(Event::CreatedMessage {
message_id: ctx.last_message_id,
message,
});
events.push(Event::SendMessage {
recipient: Recipient::Address(vacation_to.to_string()),
notify: Notify::Never,
return_of_content: Ret::Default,
by_time: ByTime::None,
message_id: ctx.last_message_id,
});
// File carbon copy
if let Some(fcc) = &self.fcc {
events.push(Event::FileInto {
folder: ctx.eval_value(&fcc.mailbox).to_string().into_owned(),
flags: ctx.get_local_flags(&fcc.flags),
mailbox_id: fcc
.mailbox_id
.as_ref()
.map(|m| ctx.eval_value(m).to_string().into_owned()),
special_use: fcc
.special_use
.as_ref()
.map(|s| ctx.eval_value(s).to_string().into_owned()),
create: fcc.create,
message_id: ctx.last_message_id,
});
}
ctx.queued_events = events.into_iter();
}
}
fn write_header(buf: &mut Vec<u8>, name: &str, value: &str) {
buf.extend_from_slice(name.as_bytes());
buf.extend_from_slice(value.as_bytes());
buf.extend_from_slice(b"\r\n");
}
+16
View File
@@ -0,0 +1,16 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
pub mod action_convert;
pub mod action_editheader;
pub mod action_fileinto;
pub mod action_flags;
pub mod action_include;
pub mod action_mime;
pub mod action_notify;
pub mod action_redirect;
pub mod action_set;
pub mod action_vacation;
+689
View File
@@ -0,0 +1,689 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::{borrow::Cow, sync::Arc, time::SystemTime};
use ahash::AHashMap;
use mail_parser::Message;
use crate::{
Context, Envelope, Event, Input, MAX_LOCAL_VARIABLES, MAX_MATCH_VARIABLES, Metadata, Runtime,
Sieve, SpamStatus, VirusStatus,
compiler::grammar::{Capability, instruction::Instruction},
};
use super::{
RuntimeError, Variable,
actions::action_include::IncludeResult,
tests::{TestResult, test_envelope::parse_envelope_address},
};
#[derive(Clone, Debug)]
pub(crate) struct ScriptStack {
pub(crate) script: Arc<Sieve>,
pub(crate) prev_pos: usize,
pub(crate) prev_vars_local: Vec<Variable>,
pub(crate) prev_vars_match: Vec<Variable>,
}
impl<'x> Context<'x> {
#[cfg(not(test))]
pub(crate) fn new(runtime: &'x Runtime, message: Message<'x>) -> Self {
Context {
#[cfg(test)]
runtime: runtime.clone(),
#[cfg(not(test))]
runtime,
message,
part: 0,
part_iter: Vec::new().into_iter(),
part_iter_stack: Vec::new(),
pos: usize::MAX,
test_result: false,
script_cache: AHashMap::new(),
script_stack: Vec::with_capacity(0),
vars_global: AHashMap::new(),
vars_env: AHashMap::new(),
vars_local: Vec::with_capacity(0),
vars_match: Vec::with_capacity(0),
expr_stack: Vec::with_capacity(16),
expr_pos: 0,
envelope: Vec::new(),
metadata: Vec::new(),
message_size: usize::MAX,
final_event: Event::Keep {
flags: Vec::with_capacity(0),
message_id: 0,
}
.into(),
queued_events: vec![].into_iter(),
has_changes: false,
user_address: "".into(),
user_full_name: "".into(),
current_time: SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0) as i64,
num_redirects: 0,
num_instructions: 0,
num_out_messages: 0,
last_message_id: 0,
main_message_id: 0,
virus_status: VirusStatus::Unknown,
spam_status: SpamStatus::Unknown,
}
}
#[allow(clippy::while_let_on_iterator)]
pub fn run(&mut self, input: Input) -> Option<Result<Event, RuntimeError>> {
match input {
Input::True => self.test_result ^= true,
Input::False => self.test_result ^= false,
Input::FncResult(result) => {
self.expr_stack.push(result);
}
Input::Script { name, script } => {
let num_vars = script.num_vars;
let num_match_vars = script.num_match_vars;
if num_match_vars <= MAX_MATCH_VARIABLES && num_vars <= MAX_LOCAL_VARIABLES {
if self.message_size == usize::MAX {
self.message_size = self.message.raw_message.len();
}
self.script_cache.insert(name, script.clone());
self.script_stack.push(ScriptStack {
script,
prev_pos: self.pos,
prev_vars_local: std::mem::replace(
&mut self.vars_local,
vec![Variable::default(); num_vars as usize],
),
prev_vars_match: std::mem::replace(
&mut self.vars_match,
vec![Variable::default(); num_match_vars as usize],
),
});
self.pos = 0;
self.test_result = false;
}
}
}
// Return any queued events
if let Some(event) = self.queued_events.next() {
return Some(Ok(event));
}
let mut current_script = self.script_stack.last()?.script.clone();
let mut iter = current_script.instructions.get(self.pos..)?.iter();
'outer: loop {
while let Some(instruction) = iter.next() {
self.num_instructions += 1;
if self.num_instructions > self.runtime.cpu_limit {
self.finish_loop();
return Some(Err(RuntimeError::CPULimitReached));
}
self.pos += 1;
match instruction {
Instruction::Jz(jmp_pos) => {
if !self.test_result {
debug_assert!(*jmp_pos > self.pos - 1);
self.pos = *jmp_pos;
iter = current_script.instructions.get(self.pos..)?.iter();
continue;
}
}
Instruction::Jnz(jmp_pos) => {
if self.test_result {
debug_assert!(*jmp_pos > self.pos - 1);
self.pos = *jmp_pos;
iter = current_script.instructions.get(self.pos..)?.iter();
continue;
}
}
Instruction::Jmp(jmp_pos) => {
debug_assert_ne!(*jmp_pos, self.pos - 1);
self.pos = *jmp_pos;
iter = current_script.instructions.get(self.pos..)?.iter();
continue;
}
Instruction::Test(test) => match test.exec(self) {
TestResult::Bool(result) => {
self.test_result = result;
}
TestResult::Event { event, is_not } => {
self.test_result = is_not;
return Some(Ok(event));
}
TestResult::Error(err) => {
self.finish_loop();
return Some(Err(err));
}
},
Instruction::Eval(expr) => match self.eval_expression(expr) {
Ok(result) => {
self.test_result = result.to_bool();
}
Err(event) => {
return Some(Ok(event));
}
},
Instruction::Clear(clear) => {
if clear.local_vars_num > 0 {
if let Some(local_vars) = self.vars_local.get_mut(
clear.local_vars_idx as usize
..(clear.local_vars_idx + clear.local_vars_num) as usize,
) {
for local_var in local_vars.iter_mut() {
if !local_var.is_empty() {
*local_var = Variable::default();
}
}
} else {
debug_assert!(false, "Failed to clear local variables: {clear:?}");
}
}
if clear.match_vars != 0 {
self.clear_match_variables(clear.match_vars);
}
}
Instruction::Keep(keep) => {
let next_event = self.build_message_id();
self.final_event = Event::Keep {
flags: self.get_local_or_global_flags(&keep.flags),
message_id: self.main_message_id,
}
.into();
if let Some(next_event) = next_event {
return Some(Ok(next_event));
}
}
Instruction::FileInto(fi) => {
fi.exec(self);
if let Some(event) = self.queued_events.next() {
return Some(Ok(event));
}
}
Instruction::Redirect(redirect) => {
redirect.exec(self);
if let Some(event) = self.queued_events.next() {
return Some(Ok(event));
}
}
Instruction::Discard => {
self.final_event = Event::Discard.into();
}
Instruction::Stop => {
self.script_stack.clear();
break 'outer;
}
Instruction::Reject(reject) => {
self.final_event = None;
return Some(Ok(Event::Reject {
extended: reject.ereject,
reason: self.eval_value(&reject.reason).to_string().into_owned(),
}));
}
Instruction::ForEveryPart(fep) => {
if let Some(next_part) = self.part_iter.next() {
self.part = next_part;
} else if let Some((prev_part, prev_part_iter)) = self.part_iter_stack.pop()
{
debug_assert!(fep.jz_pos > self.pos - 1);
self.part_iter = prev_part_iter;
self.part = prev_part;
self.pos = fep.jz_pos;
iter = current_script.instructions.get(self.pos..)?.iter();
continue;
} else {
self.part = 0;
#[cfg(test)]
panic!("ForEveryPart executed without items on stack.");
}
}
Instruction::ForEveryPartPush => {
let part_iter = self
.find_nested_parts_ids(self.part_iter_stack.is_empty())
.into_iter();
self.part_iter_stack
.push((self.part, std::mem::replace(&mut self.part_iter, part_iter)));
}
Instruction::ForEveryPartPop(num_pops) => {
debug_assert!(
*num_pops > 0 && *num_pops <= self.part_iter_stack.len(),
"Pop out of range: {} with {} items.",
num_pops,
self.part_iter_stack.len()
);
for _ in 0..*num_pops {
if let Some((prev_part, prev_part_iter)) = self.part_iter_stack.pop() {
self.part_iter = prev_part_iter;
self.part = prev_part;
} else {
break;
}
}
}
Instruction::While(while_) => match self.eval_expression(&while_.expr) {
Ok(result) => {
if !result.to_bool() {
debug_assert!(while_.jz_pos > self.pos - 1);
self.pos = while_.jz_pos;
iter = current_script.instructions.get(self.pos..)?.iter();
continue;
}
}
Err(event) => {
return Some(Ok(event));
}
},
Instruction::Let(let_) => match self.eval_expression(&let_.expr) {
Ok(result) => {
self.set_variable(&let_.name, result);
}
Err(event) => {
return Some(Ok(event));
}
},
Instruction::Replace(replace) => replace.exec(self),
Instruction::Enclose(enclose) => enclose.exec(self),
Instruction::ExtractText(extract) => {
extract.exec(self);
if let Some(event) = self.queued_events.next() {
return Some(Ok(event));
}
}
Instruction::AddHeader(add_header) => add_header.exec(self),
Instruction::DeleteHeader(delete_header) => delete_header.exec(self),
Instruction::Set(set) => {
set.exec(self);
if let Some(event) = self.queued_events.next() {
return Some(Ok(event));
}
}
Instruction::Notify(notify) => {
notify.exec(self);
if let Some(event) = self.queued_events.next() {
return Some(Ok(event));
}
}
Instruction::Vacation(vacation) => {
vacation.exec(self);
if let Some(event) = self.queued_events.next() {
return Some(Ok(event));
}
}
Instruction::EditFlags(flags) => flags.exec(self),
Instruction::Include(include) => match include.exec(self) {
IncludeResult::Cached(script) => {
self.script_stack.push(ScriptStack {
script: script.clone(),
prev_pos: self.pos,
prev_vars_local: std::mem::replace(
&mut self.vars_local,
vec![Variable::default(); script.num_vars as usize],
),
prev_vars_match: std::mem::replace(
&mut self.vars_match,
vec![Variable::default(); script.num_match_vars as usize],
),
});
self.pos = 0;
current_script = script;
iter = current_script.instructions.iter();
continue;
}
IncludeResult::Event(event) => {
return Some(Ok(event));
}
IncludeResult::Error(err) => {
self.finish_loop();
return Some(Err(err));
}
IncludeResult::None => (),
},
Instruction::Convert(convert) => {
convert.exec(self);
}
Instruction::Return => {
break;
}
Instruction::Require(capabilities) => {
for capability in capabilities {
if !self.runtime.allowed_capabilities.contains(capability) {
self.finish_loop();
return Some(Err(
if let Capability::Other(not_supported) = capability {
RuntimeError::CapabilityNotSupported(not_supported.clone())
} else {
RuntimeError::CapabilityNotAllowed(capability.clone())
},
));
}
}
}
Instruction::Error(err) => {
self.finish_loop();
return Some(Err(RuntimeError::ScriptErrorMessage(
self.eval_value(&err.message).to_string().into_owned(),
)));
}
Instruction::Invalid(invalid) => {
self.finish_loop();
return Some(Err(RuntimeError::InvalidInstruction(invalid.clone())));
}
#[cfg(test)]
Instruction::TestCmd(arguments) => {
return Some(Ok(Event::Function {
id: u32::MAX,
arguments: arguments
.iter()
.map(|s| self.eval_value(s).to_owned())
.collect(),
}));
}
}
}
if let Some(prev_script) = self.script_stack.pop() {
self.pos = prev_script.prev_pos;
self.vars_local = prev_script.prev_vars_local;
self.vars_match = prev_script.prev_vars_match;
}
if let Some(script_stack) = self.script_stack.last() {
current_script = script_stack.script.clone();
iter = current_script.instructions.get(self.pos..)?.iter();
} else {
break;
}
}
match self.final_event.take() {
Some(Event::Keep {
mut flags,
message_id,
}) => {
let create_event = if self.has_changes {
self.build_message_id()
} else {
None
};
let global_flags = self.get_global_flags();
if flags.is_empty() && !global_flags.is_empty() {
flags = global_flags;
}
if let Some(create_event) = create_event {
self.queued_events = vec![
create_event,
Event::Keep {
flags,
message_id: self.main_message_id,
},
]
.into_iter();
self.queued_events.next().map(Ok)
} else {
Some(Ok(Event::Keep { flags, message_id }))
}
}
Some(event) => Some(Ok(event)),
_ => None,
}
}
pub(crate) fn finish_loop(&mut self) {
self.script_stack.clear();
if let Some(event) = self.final_event.take() {
self.queued_events = if let Event::Keep {
mut flags,
message_id,
} = event
{
let global_flags = self.get_global_flags();
if flags.is_empty() && !global_flags.is_empty() {
flags = global_flags;
}
if self.has_changes {
if let Some(event) = self.build_message_id() {
vec![
event,
Event::Keep {
flags,
message_id: self.main_message_id,
},
]
} else {
vec![Event::Keep { flags, message_id }]
}
} else {
vec![Event::Keep { flags, message_id }]
}
} else {
vec![event]
}
.into_iter();
}
}
pub fn set_envelope(
&mut self,
envelope: impl TryInto<Envelope>,
value: impl Into<Cow<'x, str>>,
) {
if let Ok(envelope) = envelope.try_into() {
if matches!(&envelope, Envelope::From | Envelope::To) {
let value: Cow<str> = value.into();
if let Some(value) = parse_envelope_address(value.as_ref()) {
self.envelope.push((envelope, value.to_string().into()));
}
} else {
self.envelope.push((envelope, Variable::from(value.into())));
}
}
}
pub fn with_vars_env(mut self, vars_env: AHashMap<Cow<'static, str>, Variable>) -> Self {
self.vars_env = vars_env;
self
}
pub fn with_envelope_list(mut self, envelope: Vec<(Envelope, Variable)>) -> Self {
self.envelope = envelope;
self
}
pub fn with_envelope(
mut self,
envelope: impl TryInto<Envelope>,
value: impl Into<Cow<'x, str>>,
) -> Self {
self.set_envelope(envelope, value);
self
}
pub fn clear_envelope(&mut self) {
self.envelope.clear()
}
pub fn set_user_address(&mut self, from: impl Into<Cow<'x, str>>) {
self.user_address = from.into();
}
pub fn with_user_address(mut self, from: impl Into<Cow<'x, str>>) -> Self {
self.set_user_address(from);
self
}
pub fn set_user_full_name(&mut self, name: &str) {
let mut name_ = String::with_capacity(name.len());
for ch in name.chars() {
if ['\"', '\\'].contains(&ch) {
name_.push('\\');
}
name_.push(ch);
}
self.user_full_name = name_.into();
}
pub fn with_user_full_name(mut self, name: &str) -> Self {
self.set_user_full_name(name);
self
}
pub fn set_env_variable(
&mut self,
name: impl Into<Cow<'static, str>>,
value: impl Into<Variable>,
) {
self.vars_env.insert(name.into(), value.into());
}
pub fn with_env_variable(
mut self,
name: impl Into<Cow<'static, str>>,
value: impl Into<Variable>,
) -> Self {
self.set_env_variable(name, value);
self
}
pub fn set_global_variable(
&mut self,
name: impl Into<Cow<'static, str>>,
value: impl Into<Variable>,
) {
self.vars_global.insert(name.into(), value.into());
}
pub fn with_global_variable(
mut self,
name: impl Into<Cow<'static, str>>,
value: impl Into<Variable>,
) -> Self {
self.set_global_variable(name, value);
self
}
pub fn set_medatata(
&mut self,
name: impl Into<Metadata<String>>,
value: impl Into<Cow<'x, str>>,
) {
self.metadata.push((name.into(), value.into()));
}
pub fn with_metadata(
mut self,
name: impl Into<Metadata<String>>,
value: impl Into<Cow<'x, str>>,
) -> Self {
self.set_medatata(name, value);
self
}
pub fn set_spam_status(&mut self, status: impl Into<SpamStatus>) {
self.spam_status = status.into();
}
pub fn with_spam_status(mut self, status: impl Into<SpamStatus>) -> Self {
self.set_spam_status(status);
self
}
pub fn set_virus_status(&mut self, status: impl Into<VirusStatus>) {
self.virus_status = status.into();
}
pub fn with_virus_status(mut self, status: impl Into<VirusStatus>) -> Self {
self.set_virus_status(status);
self
}
pub fn take_message(&mut self) -> Message<'x> {
std::mem::take(&mut self.message)
}
pub fn has_message_changed(&self) -> bool {
self.main_message_id > 0
}
pub(crate) fn user_from_field(&self) -> String {
if !self.user_full_name.is_empty() {
format!("\"{}\" <{}>", self.user_full_name, self.user_address)
} else {
self.user_address.to_string()
}
}
pub fn global_variable_names(&self) -> impl Iterator<Item = &str> {
self.vars_global.keys().map(|k| k.as_ref())
}
pub fn global_variable(&self, name: &str) -> Option<&Variable> {
self.vars_global.get(name)
}
pub fn message(&self) -> &Message<'x> {
&self.message
}
pub fn part(&self) -> u32 {
self.part
}
}
#[cfg(test)]
impl<'x> Context<'x> {
pub(crate) fn new(runtime: &'x Runtime, message: Message<'x>) -> Self {
Context {
runtime: runtime.clone(),
message,
part: 0,
part_iter: Vec::new().into_iter(),
part_iter_stack: Vec::new(),
pos: usize::MAX,
test_result: false,
script_cache: AHashMap::new(),
script_stack: Vec::with_capacity(0),
vars_global: AHashMap::new(),
vars_env: AHashMap::new(),
vars_local: Vec::with_capacity(0),
vars_match: Vec::with_capacity(0),
expr_stack: Vec::with_capacity(16),
expr_pos: 0,
envelope: Vec::new(),
metadata: Vec::new(),
message_size: usize::MAX,
final_event: Event::Keep {
flags: Vec::with_capacity(0),
message_id: 0,
}
.into(),
queued_events: vec![].into_iter(),
has_changes: false,
user_address: "".into(),
user_full_name: "".into(),
current_time: SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0) as i64,
num_redirects: 0,
num_instructions: 0,
num_out_messages: 0,
last_message_id: 0,
main_message_id: 0,
virus_status: VirusStatus::Unknown,
spam_status: SpamStatus::Unknown,
}
}
}
+482
View File
@@ -0,0 +1,482 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::cmp::Ordering;
use mail_parser::{
Addr, Header, HeaderName, HeaderValue, Host, PartType, Received,
decoders::html::{html_to_text, text_to_html},
parsers::MessageStream,
};
use crate::{
Context,
compiler::{
ContentTypePart, HeaderPart, HeaderVariable, MessagePart, ReceivedHostname, ReceivedPart,
Value, VariableType,
},
};
use super::Variable;
impl<'x> Context<'x> {
pub(crate) fn variable<'y: 'x>(&'y self, var: &VariableType) -> Option<Variable> {
match var {
VariableType::Local(var_num) => self.vars_local.get(*var_num).cloned(),
VariableType::Match(var_num) => self.vars_match.get(*var_num).cloned(),
VariableType::Global(var_name) => self.vars_global.get(var_name.as_str()).cloned(),
VariableType::Environment(var_name) => self
.vars_env
.get(var_name.as_str())
.or_else(|| self.runtime.environment.get(var_name.as_str()))
.cloned(),
VariableType::Envelope(envelope) => self
.envelope
.iter()
.find_map(|(e, v)| if e == envelope { Some(v.clone()) } else { None }),
VariableType::Header(header) => self.eval_header(header),
VariableType::Part(part) => match part {
MessagePart::TextBody(convert) => {
let part = self
.message
.parts
.get(*self.message.text_body.first()? as usize)?;
match &part.body {
PartType::Text(text) => Some(text.as_ref().into()),
PartType::Html(html) if *convert => {
Some(html_to_text(html.as_ref()).into())
}
_ => None,
}
}
MessagePart::HtmlBody(convert) => {
let part = self
.message
.parts
.get(*self.message.html_body.first()? as usize)?;
match &part.body {
PartType::Html(html) => Some(html.as_ref().into()),
PartType::Text(text) if *convert => {
Some(text_to_html(text.as_ref()).into())
}
_ => None,
}
}
MessagePart::Contents => match &self.message.parts.get(self.part as usize)?.body {
PartType::Text(text) | PartType::Html(text) => {
Variable::from(text.as_ref()).into()
}
PartType::Binary(bin) | PartType::InlineBinary(bin) => {
Variable::from(String::from_utf8_lossy(bin.as_ref())).into()
}
_ => None,
},
MessagePart::Raw => {
let part = self.message.parts.get(self.part as usize)?;
self.message
.raw_message()
.get(part.raw_body_offset() as usize..part.raw_end_offset() as usize)
.map(|v| Variable::from(String::from_utf8_lossy(v)))
}
},
}
}
pub(crate) fn eval_value(&self, string: &Value) -> Variable {
match string {
Value::Text(text) => Variable::String(text.clone()),
Value::Variable(var) => self.variable(var).unwrap_or_default(),
Value::List(list) => {
let mut data = String::new();
for item in list {
match item {
Value::Text(string) => {
data.push_str(string);
}
Value::Variable(var) => {
if let Some(value) = self.variable(var) {
data.push_str(&value.to_string());
}
}
Value::List(_) => {
debug_assert!(false, "This should not have happened: {string:?}");
}
Value::Number(n) => {
data.push_str(&n.to_string());
}
Value::Regex(_) => (),
}
}
data.into()
}
Value::Number(n) => Variable::from(*n),
Value::Regex(r) => Variable::String(r.expr.clone().into()),
}
}
fn eval_header<'z: 'x>(&'z self, header: &HeaderVariable) -> Option<Variable> {
let mut result = Vec::new();
let part = self.message.part(self.part)?;
let raw = self.message.raw_message();
if !header.name.is_empty() {
let mut headers = part
.headers
.iter()
.filter(|h| header.name.contains(&h.name));
match header.index_hdr.cmp(&0) {
Ordering::Greater => {
if let Some(h) = headers.nth((header.index_hdr - 1) as usize) {
header.eval_part(h, raw, &mut result);
}
}
Ordering::Less => {
if let Some(h) = headers
.rev()
.nth((header.index_hdr.unsigned_abs() - 1) as usize)
{
header.eval_part(h, raw, &mut result);
}
}
Ordering::Equal => {
for h in headers {
header.eval_part(h, raw, &mut result);
}
}
}
} else {
for h in &part.headers {
match &header.part {
HeaderPart::Raw => {
if let Some(var) = raw
.get(h.offset_field as usize..h.offset_end as usize)
.map(sanitize_raw_header)
{
result.push(Variable::from(var));
}
}
HeaderPart::Text => {
if let HeaderValue::Text(text) = &h.value {
result.push(Variable::from(format!("{}: {}", h.name.as_str(), text)));
} else if let HeaderValue::Text(text) = MessageStream::new(
raw.get(h.offset_start as usize..h.offset_end as usize)
.unwrap_or(b""),
)
.parse_unstructured()
{
result.push(Variable::from(format!("{}: {}", h.name.as_str(), text)));
}
}
_ => {
header.eval_part(h, raw, &mut result);
}
}
}
}
match result.len() {
1 if header.index_hdr != 0 && header.index_part != 0 => result.pop(),
0 => None,
_ => Some(Variable::Array(result.into())),
}
}
#[inline(always)]
pub(crate) fn eval_values<'z: 'y, 'y>(&'z self, strings: &'y [Value]) -> Vec<Variable> {
strings.iter().map(|s| self.eval_value(s)).collect()
}
#[inline(always)]
pub(crate) fn eval_values_owned(&self, strings: &[Value]) -> Vec<String> {
strings
.iter()
.map(|s| self.eval_value(s).to_string().into_owned())
.collect()
}
}
impl<'x> HeaderVariable<'x> {
fn eval_part(&self, header: &'x Header<'x>, raw: &'x [u8], result: &mut Vec<Variable>) {
let var = match &self.part {
HeaderPart::Text => match &header.value {
HeaderValue::Text(v) if self.include_single_part() => {
Some(Variable::from(v.as_ref()))
}
HeaderValue::TextList(list) => match self.index_part.cmp(&0) {
Ordering::Greater => list
.get((self.index_part - 1) as usize)
.map(|v| Variable::from(v.as_ref())),
Ordering::Less => list
.iter()
.rev()
.nth((self.index_part.unsigned_abs() - 1) as usize)
.map(|v| Variable::from(v.as_ref())),
Ordering::Equal => {
for item in list {
result.push(Variable::from(item.as_ref()));
}
return;
}
},
HeaderValue::ContentType(ct) => if let Some(st) = &ct.c_subtype {
Variable::from(format!("{}/{}", ct.c_type, st))
} else {
Variable::from(ct.c_type.as_ref())
}
.into(),
HeaderValue::Address(list) => {
let mut list = list.iter();
match self.index_part.cmp(&0) {
Ordering::Greater => list
.nth((self.index_part - 1) as usize)
.map(|a| a.to_text()),
Ordering::Less => list
.rev()
.nth((self.index_part.unsigned_abs() - 1) as usize)
.map(|a| a.to_text()),
Ordering::Equal => {
for item in list {
result.push(item.to_text());
}
return;
}
}
}
HeaderValue::DateTime(_) => raw
.get(header.offset_start as usize..header.offset_end as usize)
.and_then(|bytes| std::str::from_utf8(bytes).ok())
.map(|s| s.trim())
.map(Variable::from),
_ => None,
},
HeaderPart::Address(part) => match &header.value {
HeaderValue::Address(addr) => {
let mut list = addr.iter();
match self.index_part.cmp(&0) {
Ordering::Greater => list
.nth((self.index_part - 1) as usize)
.and_then(|a| part.eval_strict(a))
.map(Variable::from),
Ordering::Less => list
.rev()
.nth((self.index_part.unsigned_abs() - 1) as usize)
.and_then(|a| part.eval_strict(a))
.map(Variable::from),
Ordering::Equal => {
for item in list {
result.push(
part.eval_strict(item)
.map(Variable::from)
.unwrap_or_default(),
);
}
return;
}
}
}
HeaderValue::Text(_) => {
let addr = raw
.get(header.offset_start as usize..header.offset_end as usize)
.and_then(|bytes| match MessageStream::new(bytes).parse_address() {
HeaderValue::Address(addr) => addr.into(),
_ => None,
});
if let Some(addr) = addr {
let mut list = addr.iter();
match self.index_part.cmp(&0) {
Ordering::Greater => list
.nth((self.index_part - 1) as usize)
.and_then(|a| part.eval_strict(a))
.map(|s| Variable::String(s.to_string().into())),
Ordering::Less => list
.rev()
.nth((self.index_part.unsigned_abs() - 1) as usize)
.and_then(|a| part.eval_strict(a))
.map(|s| Variable::String(s.to_string().into())),
Ordering::Equal => {
for item in list {
result.push(
part.eval_strict(item)
.map(|s| Variable::String(s.to_string().into()))
.unwrap_or_default(),
);
}
return;
}
}
} else {
None
}
}
_ => None,
},
HeaderPart::Date => {
if let HeaderValue::DateTime(dt) = &header.value {
Variable::from(dt.to_timestamp()).into()
} else {
raw.get(header.offset_start as usize..header.offset_end as usize)
.and_then(|bytes| match MessageStream::new(bytes).parse_date() {
HeaderValue::DateTime(dt) => Variable::from(dt.to_timestamp()).into(),
_ => None,
})
}
}
HeaderPart::Id => match &header.name {
HeaderName::MessageId | HeaderName::ResentMessageId => match &header.value {
HeaderValue::Text(id) => Variable::from(id.as_ref()).into(),
HeaderValue::TextList(ids) => {
for id in ids {
result.push(Variable::from(id.as_ref()));
}
return;
}
_ => None,
},
HeaderName::Other(_) => {
match MessageStream::new(
raw.get(header.offset_start as usize..header.offset_end as usize)
.unwrap_or(b""),
)
.parse_id()
{
HeaderValue::Text(id) => Variable::from(id).into(),
HeaderValue::TextList(ids) => {
for id in ids {
result.push(Variable::from(id));
}
return;
}
_ => None,
}
}
_ => None,
},
HeaderPart::Raw => raw
.get(header.offset_start as usize..header.offset_end as usize)
.map(sanitize_raw_header)
.map(Variable::from),
HeaderPart::RawName => raw
.get(header.offset_field as usize..header.offset_start as usize - 1)
.map(|bytes| std::str::from_utf8(bytes).unwrap_or_default())
.map(Variable::from),
HeaderPart::Exists => Variable::from(true).into(),
_ => match (&header.value, &self.part) {
(HeaderValue::ContentType(ct), HeaderPart::ContentType(part)) => match part {
ContentTypePart::Type => Variable::from(ct.c_type.as_ref()).into(),
ContentTypePart::Subtype => {
ct.c_subtype.as_ref().map(|s| Variable::from(s.as_ref()))
}
ContentTypePart::Attribute(attr) => ct.attributes.as_ref().and_then(|attrs| {
attrs.iter().find_map(|a| {
if a.name.eq_ignore_ascii_case(attr) {
Some(Variable::from(a.value.as_ref()))
} else {
None
}
})
}),
},
(HeaderValue::Received(rcvd), HeaderPart::Received(part)) => part.eval(rcvd),
_ => None,
},
};
result.push(var.unwrap_or_default());
}
#[inline(always)]
fn include_single_part(&self) -> bool {
[-1, 0, 1].contains(&self.index_part)
}
}
impl ReceivedPart {
pub fn eval<'x>(&self, rcvd: &'x Received<'x>) -> Option<Variable> {
match self {
ReceivedPart::From(from) => rcvd
.from()
.or_else(|| rcvd.helo())
.and_then(|v| from.to_variable(v)),
ReceivedPart::FromIp => rcvd.from_ip().map(|ip| Variable::from(ip.to_string())),
ReceivedPart::FromIpRev => rcvd.from_iprev().map(Variable::from),
ReceivedPart::By(by) => rcvd.by().and_then(|v: &Host<'_>| by.to_variable(v)),
ReceivedPart::For => rcvd.for_().map(Variable::from),
ReceivedPart::With => rcvd.with().map(|v| Variable::from(v.as_str())),
ReceivedPart::TlsVersion => rcvd.tls_version().map(|v| Variable::from(v.as_str())),
ReceivedPart::TlsCipher => rcvd.tls_cipher().map(Variable::from),
ReceivedPart::Id => rcvd.id().map(Variable::from),
ReceivedPart::Ident => rcvd.ident().map(Variable::from),
ReceivedPart::Via => rcvd.via().map(Variable::from),
ReceivedPart::Date => rcvd.date().map(|d| Variable::from(d.to_timestamp())),
ReceivedPart::DateRaw => rcvd.date().map(|d| Variable::from(d.to_rfc822())),
}
}
}
trait AddrToText<'x> {
fn to_text<'z: 'x>(&'z self) -> Variable;
}
impl<'x> AddrToText<'x> for Addr<'x> {
fn to_text<'z: 'x>(&'z self) -> Variable {
if let Some(name) = &self.name {
if let Some(address) = &self.address {
Variable::String(format!("{name} <{address}>").into())
} else {
Variable::String(name.to_string().into())
}
} else if let Some(address) = &self.address {
Variable::String(format!("<{address}>").into())
} else {
Variable::default()
}
}
}
impl ReceivedHostname {
fn to_variable<'x>(&self, host: &'x Host<'x>) -> Option<Variable> {
match (self, host) {
(ReceivedHostname::Name, Host::Name(name)) => Variable::from(name.as_ref()).into(),
(ReceivedHostname::Ip, Host::IpAddr(ip)) => Variable::from(ip.to_string()).into(),
(ReceivedHostname::Any, _) => Variable::from(host.to_string()).into(),
_ => None,
}
}
}
pub(crate) trait IntoString: Sized {
fn into_string(self) -> String;
}
pub(crate) trait ToString: Sized {
fn to_string(&self) -> String;
}
impl IntoString for Vec<u8> {
fn into_string(self) -> String {
String::from_utf8(self)
.unwrap_or_else(|err| String::from_utf8_lossy(err.as_bytes()).into_owned())
}
}
fn sanitize_raw_header(bytes: &[u8]) -> String {
let mut result = Vec::with_capacity(bytes.len());
let mut last_is_space = false;
for &ch in bytes {
if ch.is_ascii_whitespace() {
last_is_space = true;
} else {
if last_is_space {
result.push(b' ');
last_is_space = false;
}
result.push(ch);
}
}
result.into_string()
}
+641
View File
@@ -0,0 +1,641 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::{cmp::Ordering, fmt::Display};
use crate::Event;
use crate::compiler::grammar::expr::parser::ID_EXTERNAL;
use crate::{Context, compiler::Number, runtime::Variable};
use crate::compiler::grammar::expr::{BinaryOperator, Constant, Expression, UnaryOperator};
impl Context<'_> {
pub(crate) fn eval_expression(&mut self, expr: &[Expression]) -> Result<Variable, Event> {
let mut exprs = expr.iter().skip(self.expr_pos);
while let Some(expr) = exprs.next() {
self.expr_pos += 1;
match expr {
Expression::Variable(v) => {
self.expr_stack.push(self.variable(v).unwrap_or_default());
}
Expression::Constant(val) => {
self.expr_stack.push(Variable::from(val));
}
Expression::UnaryOperator(op) => {
let value = self.expr_stack.pop().unwrap_or_default();
self.expr_stack.push(match op {
UnaryOperator::Not => value.op_not(),
UnaryOperator::Minus => value.op_minus(),
});
}
Expression::BinaryOperator(op) => {
let right = self.expr_stack.pop().unwrap_or_default();
let left = self.expr_stack.pop().unwrap_or_default();
self.expr_stack.push(match op {
BinaryOperator::Add => left.op_add(right),
BinaryOperator::Subtract => left.op_subtract(right),
BinaryOperator::Multiply => left.op_multiply(right),
BinaryOperator::Divide => left.op_divide(right),
BinaryOperator::And => left.op_and(right),
BinaryOperator::Or => left.op_or(right),
BinaryOperator::Xor => left.op_xor(right),
BinaryOperator::Eq => left.op_eq(right),
BinaryOperator::Ne => left.op_ne(right),
BinaryOperator::Lt => left.op_lt(right),
BinaryOperator::Le => left.op_le(right),
BinaryOperator::Gt => left.op_gt(right),
BinaryOperator::Ge => left.op_ge(right),
});
}
Expression::Function { id, num_args } => {
let num_args = *num_args as usize;
if let Some(fnc) = self.runtime.functions.get(*id as usize) {
let mut arguments = vec![Variable::Integer(0); num_args];
for arg_num in 0..num_args {
arguments[num_args - arg_num - 1] =
self.expr_stack.pop().unwrap_or_default();
}
self.expr_stack.push((fnc)(self, arguments));
} else {
let mut arguments = vec![Variable::Integer(0); num_args];
for arg_num in 0..num_args {
arguments[num_args - arg_num - 1] =
self.expr_stack.pop().unwrap_or_default();
}
self.pos -= 1; // We need to re-evaluate the function call
return Err(Event::Function {
id: ID_EXTERNAL - *id,
arguments,
});
}
}
Expression::JmpIf { val, pos } => {
if self.expr_stack.last().is_some_and(|v| v.to_bool()) == *val {
self.expr_pos += *pos as usize;
for _ in 0..*pos {
exprs.next();
}
}
}
Expression::ArrayAccess => {
let index = self.expr_stack.pop().unwrap_or_default().to_usize();
let array = self.expr_stack.pop().unwrap_or_default().into_array();
self.expr_stack
.push(array.get(index).cloned().unwrap_or_default());
}
Expression::ArrayBuild(num_items) => {
let num_items = *num_items as usize;
let mut items = vec![Variable::Integer(0); num_items];
for arg_num in 0..num_items {
items[num_items - arg_num - 1] = self.expr_stack.pop().unwrap_or_default();
}
self.expr_stack.push(Variable::Array(items.into()));
}
}
}
let result = self.expr_stack.pop().unwrap_or_default();
self.expr_stack.clear();
self.expr_pos = 0;
Ok(result)
}
}
impl Variable {
pub fn op_add(self, other: Variable) -> Variable {
match (self, other) {
(Variable::Integer(a), Variable::Integer(b)) => Variable::Integer(a.saturating_add(b)),
(Variable::Float(a), Variable::Float(b)) => Variable::Float(a + b),
(Variable::Integer(i), Variable::Float(f))
| (Variable::Float(f), Variable::Integer(i)) => Variable::Float(i as f64 + f),
(Variable::Array(a), Variable::Array(b)) => {
Variable::Array(a.iter().chain(b.iter()).cloned().collect::<Vec<_>>().into())
}
(Variable::Array(a), b) => a.iter().cloned().chain([b]).collect::<Vec<_>>().into(),
(a, Variable::Array(b)) => [a]
.into_iter()
.chain(b.iter().cloned())
.collect::<Vec<_>>()
.into(),
(Variable::String(a), b) => {
if !a.is_empty() {
Variable::String(format!("{}{}", a, b).into())
} else {
b
}
}
(a, Variable::String(b)) => {
if !b.is_empty() {
Variable::String(format!("{}{}", a, b).into())
} else {
a
}
}
}
}
pub fn op_subtract(self, other: Variable) -> Variable {
match (self, other) {
(Variable::Integer(a), Variable::Integer(b)) => Variable::Integer(a.saturating_sub(b)),
(Variable::Float(a), Variable::Float(b)) => Variable::Float(a - b),
(Variable::Integer(a), Variable::Float(b)) => Variable::Float(a as f64 - b),
(Variable::Float(a), Variable::Integer(b)) => Variable::Float(a - b as f64),
(Variable::Array(a), b) | (b, Variable::Array(a)) => Variable::Array(
a.iter()
.filter(|v| *v != &b)
.cloned()
.collect::<Vec<_>>()
.into(),
),
(a, b) => a.parse_number().op_subtract(b.parse_number()),
}
}
pub fn op_multiply(self, other: Variable) -> Variable {
match (self, other) {
(Variable::Integer(a), Variable::Integer(b)) => Variable::Integer(a.saturating_mul(b)),
(Variable::Float(a), Variable::Float(b)) => Variable::Float(a * b),
(Variable::Integer(i), Variable::Float(f))
| (Variable::Float(f), Variable::Integer(i)) => Variable::Float(i as f64 * f),
(a, b) => a.parse_number().op_multiply(b.parse_number()),
}
}
pub fn op_divide(self, other: Variable) -> Variable {
match (self, other) {
(Variable::Integer(a), Variable::Integer(b)) => {
Variable::Float(if b != 0 { a as f64 / b as f64 } else { 0.0 })
}
(Variable::Float(a), Variable::Float(b)) => {
Variable::Float(if b != 0.0 { a / b } else { 0.0 })
}
(Variable::Integer(a), Variable::Float(b)) => {
Variable::Float(if b != 0.0 { a as f64 / b } else { 0.0 })
}
(Variable::Float(a), Variable::Integer(b)) => {
Variable::Float(if b != 0 { a / b as f64 } else { 0.0 })
}
(a, b) => a.parse_number().op_divide(b.parse_number()),
}
}
pub fn op_and(self, other: Variable) -> Variable {
Variable::Integer(i64::from(self.to_bool() & other.to_bool()))
}
pub fn op_or(self, other: Variable) -> Variable {
Variable::Integer(i64::from(self.to_bool() | other.to_bool()))
}
pub fn op_xor(self, other: Variable) -> Variable {
Variable::Integer(i64::from(self.to_bool() ^ other.to_bool()))
}
pub fn op_eq(self, other: Variable) -> Variable {
Variable::Integer(i64::from(self == other))
}
pub fn op_ne(self, other: Variable) -> Variable {
Variable::Integer(i64::from(self != other))
}
pub fn op_lt(self, other: Variable) -> Variable {
Variable::Integer(i64::from(self < other))
}
pub fn op_le(self, other: Variable) -> Variable {
Variable::Integer(i64::from(self <= other))
}
pub fn op_gt(self, other: Variable) -> Variable {
Variable::Integer(i64::from(self > other))
}
pub fn op_ge(self, other: Variable) -> Variable {
Variable::Integer(i64::from(self >= other))
}
pub fn op_not(self) -> Variable {
Variable::Integer(i64::from(!self.to_bool()))
}
pub fn op_minus(self) -> Variable {
match self {
Variable::Integer(n) => Variable::Integer(-n),
Variable::Float(n) => Variable::Float(-n),
_ => self.parse_number().op_minus(),
}
}
pub fn parse_number(&self) -> Variable {
match self {
Variable::String(s) if !s.is_empty() => {
if let Ok(n) = s.parse::<i64>() {
Variable::Integer(n)
} else if let Ok(n) = s.parse::<f64>() {
Variable::Float(n)
} else {
Variable::Integer(0)
}
}
Variable::Integer(n) => Variable::Integer(*n),
Variable::Float(n) => Variable::Float(*n),
Variable::Array(l) => Variable::Integer(l.is_empty() as i64),
_ => Variable::Integer(0),
}
}
pub fn to_bool(&self) -> bool {
match self {
Variable::Float(f) => *f != 0.0,
Variable::Integer(n) => *n != 0,
Variable::String(s) => !s.is_empty(),
Variable::Array(a) => !a.is_empty(),
}
}
}
impl PartialEq for Variable {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::Integer(a), Self::Integer(b)) => a == b,
(Self::Float(a), Self::Float(b)) => a == b,
(Self::Integer(a), Self::Float(b)) | (Self::Float(b), Self::Integer(a)) => {
*a as f64 == *b
}
(Self::String(a), Self::String(b)) => a == b,
(Self::String(_), Self::Integer(_) | Self::Float(_)) => &self.parse_number() == other,
(Self::Integer(_) | Self::Float(_), Self::String(_)) => self == &other.parse_number(),
(Self::Array(a), Self::Array(b)) => a == b,
_ => false,
}
}
}
impl Eq for Variable {}
#[allow(clippy::non_canonical_partial_ord_impl)]
impl PartialOrd for Variable {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
match (self, other) {
(Self::Integer(a), Self::Integer(b)) => a.partial_cmp(b),
(Self::Float(a), Self::Float(b)) => a.partial_cmp(b),
(Self::Integer(a), Self::Float(b)) => (*a as f64).partial_cmp(b),
(Self::Float(a), Self::Integer(b)) => a.partial_cmp(&(*b as f64)),
(Self::String(a), Self::String(b)) => a.partial_cmp(b),
(Self::String(_), Self::Integer(_) | Self::Float(_)) => {
self.parse_number().partial_cmp(other)
}
(Self::Integer(_) | Self::Float(_), Self::String(_)) => {
self.partial_cmp(&other.parse_number())
}
(Self::Array(a), Self::Array(b)) => a.partial_cmp(b),
(Self::Array(_) | Self::String(_), _) => Ordering::Greater.into(),
(_, Self::Array(_)) => Ordering::Less.into(),
}
}
}
impl Ord for Variable {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.partial_cmp(other).unwrap_or(Ordering::Greater)
}
}
impl Display for Variable {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Variable::String(v) => v.fmt(f),
Variable::Integer(v) => v.fmt(f),
Variable::Float(v) => v.fmt(f),
Variable::Array(v) => {
for (i, v) in v.iter().enumerate() {
if i > 0 {
f.write_str("\n")?;
}
v.fmt(f)?;
}
Ok(())
}
}
}
}
impl Number {
pub fn is_non_zero(&self) -> bool {
match self {
Number::Integer(n) => *n != 0,
Number::Float(n) => *n != 0.0,
}
}
}
impl Default for Number {
fn default() -> Self {
Number::Integer(0)
}
}
impl From<bool> for Number {
#[inline(always)]
fn from(b: bool) -> Self {
Number::Integer(i64::from(b))
}
}
impl From<i64> for Number {
#[inline(always)]
fn from(n: i64) -> Self {
Number::Integer(n)
}
}
impl From<f64> for Number {
#[inline(always)]
fn from(n: f64) -> Self {
Number::Float(n)
}
}
impl From<i32> for Number {
#[inline(always)]
fn from(n: i32) -> Self {
Number::Integer(n as i64)
}
}
impl<'x> From<&'x Constant> for Variable {
fn from(value: &'x Constant) -> Self {
match value {
Constant::Integer(i) => Variable::Integer(*i),
Constant::Float(f) => Variable::Float(*f),
Constant::String(s) => Variable::String(s.clone()),
}
}
}
#[cfg(test)]
mod test {
use ahash::{HashMap, HashMapExt};
use crate::{
compiler::{
VariableType,
grammar::expr::{
BinaryOperator, Expression, Token, UnaryOperator, parser::ExpressionParser,
tokenizer::Tokenizer,
},
},
runtime::Variable,
};
use evalexpr::*;
pub trait EvalExpression {
fn eval(&self, variables: &HashMap<String, Variable>) -> Option<Variable>;
}
impl EvalExpression for Vec<Expression> {
fn eval(&self, variables: &HashMap<String, Variable>) -> Option<Variable> {
let mut stack = Vec::with_capacity(self.len());
let mut exprs = self.iter();
while let Some(expr) = exprs.next() {
match expr {
Expression::Variable(VariableType::Global(v)) => {
stack.push(variables.get(v)?.clone());
}
Expression::Constant(val) => {
stack.push(Variable::from(val));
}
Expression::UnaryOperator(op) => {
let value = stack.pop()?;
stack.push(match op {
UnaryOperator::Not => value.op_not(),
UnaryOperator::Minus => value.op_minus(),
});
}
Expression::BinaryOperator(op) => {
let right = stack.pop()?;
let left = stack.pop()?;
stack.push(match op {
BinaryOperator::Add => left.op_add(right),
BinaryOperator::Subtract => left.op_subtract(right),
BinaryOperator::Multiply => left.op_multiply(right),
BinaryOperator::Divide => left.op_divide(right),
BinaryOperator::And => left.op_and(right),
BinaryOperator::Or => left.op_or(right),
BinaryOperator::Xor => left.op_xor(right),
BinaryOperator::Eq => left.op_eq(right),
BinaryOperator::Ne => left.op_ne(right),
BinaryOperator::Lt => left.op_lt(right),
BinaryOperator::Le => left.op_le(right),
BinaryOperator::Gt => left.op_gt(right),
BinaryOperator::Ge => left.op_ge(right),
});
}
Expression::JmpIf { val, pos } => {
if stack.last()?.to_bool() == *val {
for _ in 0..*pos {
exprs.next();
}
}
}
_ => unreachable!("Invalid expression"),
}
}
stack.pop()
}
}
#[test]
fn eval_expression() {
let mut variables = HashMap::from_iter([
("A".to_string(), Variable::Integer(0)),
("B".to_string(), Variable::Integer(0)),
("C".to_string(), Variable::Integer(0)),
("D".to_string(), Variable::Integer(0)),
("E".to_string(), Variable::Integer(0)),
("F".to_string(), Variable::Integer(0)),
("G".to_string(), Variable::Integer(0)),
("H".to_string(), Variable::Integer(0)),
("I".to_string(), Variable::Integer(0)),
("J".to_string(), Variable::Integer(0)),
]);
let num_vars = variables.len();
for expr in [
"A + B",
"A * B",
"A / B",
"A - B",
"-A",
"A == B",
"A != B",
"A > B",
"A < B",
"A >= B",
"A <= B",
"A + B * C - D / E",
"A + B + C - D - E",
"(A + B) * (C - D) / E",
"A - B + C * D / E * F - G",
"A + B * C - D / E",
"(A + B) * (C - D) / E",
"A - B + C / D * E",
"(A + B) / (C - D) + E",
"A * (B + C) - D / E",
"A / (B - C + D) * E",
"(A + B) * C - D / (E + F)",
"A * B - C + D / E",
"A + B - C * D / E",
"(A * B + C) / D - E",
"A - B / C + D * E",
"A + B * (C - D) / E",
"A * B / C + (D - E)",
"(A - B) * C / D + E",
"A * (B / C) - D + E",
"(A + B) / (C + D) * E",
"A - B * C / D + E",
"A + (B - C) * D / E",
"(A + B) * (C / D) - E",
"A - B / (C * D) + E",
"(A + B) > (C - D) && E <= F",
"A * B == C / D || E - F != G + H",
"A / B >= C * D && E + F < G - H",
"(A * B - C) != (D / E + F) && G > H",
"A - B < C && D + E >= F * G",
"(A * B) > C && (D / E) < F || G == H",
"(A + B) <= (C - D) || E > F && G != H",
"A * B != C + D || E - F == G / H",
"A >= B * C && D < E - F || G != H + I",
"(A / B + C) > D && E * F <= G - H",
"A * (B - C) == D && E / F > G + H",
"(A - B + C) != D || E * F >= G && H < I",
"A < B / C && D + E * F == G - H",
"(A + B * C) <= D && E > F / G",
"(A * B - C) > D || E <= F + G && H != I",
"A != B / C && D == E * F - G",
"A <= B + C - D && E / F > G * H",
"(A - B * C) < D || E >= F + G && H != I",
"(A + B) / C == D && E - F < G * H",
"A * B != C && D >= E + F / G || H < I",
"!(A * B != C) && !(D >= E + F / G) || !(H < I)",
"-A - B - (- C - D) - E - (-F)",
] {
println!("Testing {}", expr);
for (pos, v) in variables.values_mut().enumerate() {
*v = Variable::Integer(pos as i64 + 1);
}
assert_expr(expr, &variables);
for (pos, v) in variables.values_mut().enumerate() {
*v = Variable::Integer((num_vars - pos) as i64);
}
assert_expr(expr, &variables);
}
for expr in [
"true && false",
"!true || false",
"true && !false",
"!(true && false)",
"true || true && false",
"!false && (true || false)",
"!(true || !false) && true",
"!(!true && !false)",
"true || false && !true",
"!(true && true) || !false",
"!(!true || !false) && (!false) && !(!true)",
] {
let pexp = parse_expression(expr.replace("true", "1").replace("false", "0").as_str());
let result = pexp.eval(&HashMap::new()).unwrap();
//println!("{} => {:?}", expr, result);
match (eval(expr).expect(expr), result) {
(Value::Float(a), Variable::Float(b)) if a == b => (),
(Value::Float(a), Variable::Integer(b)) if a == b as f64 => (),
(Value::Boolean(a), Variable::Integer(b)) if a == (b != 0) => (),
(a, b) => {
panic!("{} => {:?} != {:?}", expr, a, b)
}
}
}
}
fn assert_expr(expr: &str, variables: &HashMap<String, Variable>) {
let e = parse_expression(expr);
let result = e.eval(variables).unwrap();
let mut str_expr = expr.to_string();
let mut str_expr_float = expr.to_string();
for (k, v) in variables {
let v = v.to_string();
if v.contains('.') {
str_expr_float = str_expr_float.replace(k, &v);
} else {
str_expr_float = str_expr_float.replace(k, &format!("{}.0", v));
}
str_expr = str_expr.replace(k, &v);
}
assert_eq!(
parse_expression(&str_expr)
.eval(&HashMap::new())
.unwrap()
.to_number()
.to_float(),
result.to_number().to_float()
);
assert_eq!(
parse_expression(&str_expr_float)
.eval(&HashMap::new())
.unwrap()
.to_number()
.to_float(),
result.to_number().to_float()
);
//println!("{str_expr} ({e:?}) => {result:?}");
match (
eval(&str_expr_float)
.map(|v| {
// Divisions by zero are converted to 0.0
if matches!(&v, Value::Float(f) if f64::is_infinite(*f)) {
Value::Float(0.0)
} else {
v
}
})
.expect(&str_expr),
result,
) {
(Value::Float(a), Variable::Float(b)) if a == b => (),
(Value::Float(a), Variable::Integer(b)) if a == b as f64 => (),
(Value::Boolean(a), Variable::Integer(b)) if a == (b != 0) => (),
(a, b) => {
panic!("{} => {:?} != {:?}", str_expr, a, b)
}
}
}
fn parse_expression(expr: &str) -> Vec<Expression> {
ExpressionParser::from_tokenizer(Tokenizer::new(expr, |var_name: &str, _: bool| {
Ok::<_, String>(Token::Variable(VariableType::Global(var_name.to_string())))
}))
.parse()
.unwrap()
.output
}
}
+803
View File
@@ -0,0 +1,803 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*
* Modified by Coffey Labs in 2026 for INBUXA.
*/
pub mod actions;
pub mod context;
pub mod eval;
pub mod expression;
pub mod tests;
pub mod variables;
use ahash::{AHashMap, AHashSet};
use mail_parser::HeaderName;
#[cfg(not(test))]
use mail_parser::{Encoding, Message, MessageParser, MessagePart, PartType};
use std::{borrow::Cow, fmt::Display, hash::Hash, ops::Deref, sync::Arc};
#[cfg(not(test))]
use crate::Context;
use crate::{
ExternalId, Function, FunctionMap, Input, Metadata, Runtime, Script, Sieve,
compiler::{
Number,
grammar::{Capability, Invalid, expr::parser::ID_EXTERNAL},
},
};
use self::eval::ToString;
#[derive(Debug, Clone)]
#[cfg_attr(
any(test, feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
pub enum Variable {
String(Arc<String>),
Integer(i64),
Float(f64),
Array(Arc<Vec<Variable>>),
}
#[derive(Debug)]
pub enum RuntimeError {
TooManyIncludes,
InvalidInstruction(Invalid),
ScriptErrorMessage(String),
CapabilityNotAllowed(Capability),
CapabilityNotSupported(String),
CPULimitReached,
}
impl Default for Variable {
fn default() -> Self {
Variable::String(Arc::new(String::new()))
}
}
impl Variable {
pub fn to_string(&self) -> Cow<'_, str> {
match self {
Variable::String(s) => Cow::Borrowed(s.as_str()),
Variable::Integer(n) => Cow::Owned(n.to_string()),
Variable::Float(n) => Cow::Owned(n.to_string()),
Variable::Array(l) => Cow::Owned(l.to_string()),
}
}
pub fn to_number(&self) -> Number {
self.to_number_checked()
.unwrap_or(Number::Float(f64::INFINITY))
}
pub fn to_number_checked(&self) -> Option<Number> {
let s = match self {
Variable::Integer(n) => return Number::Integer(*n).into(),
Variable::Float(n) => return Number::Float(*n).into(),
Variable::String(s) if !s.is_empty() => s.as_str(),
_ => return None,
};
if !s.contains('.') {
s.parse::<i64>().map(Number::Integer).ok()
} else {
s.parse::<f64>().map(Number::Float).ok()
}
}
pub fn to_integer(&self) -> i64 {
match self {
Variable::Integer(n) => *n,
Variable::Float(n) => *n as i64,
Variable::String(s) if !s.is_empty() => s.parse::<i64>().unwrap_or(0),
_ => 0,
}
}
pub fn to_usize(&self) -> usize {
match self {
Variable::Integer(n) => *n as usize,
Variable::Float(n) => *n as usize,
Variable::String(s) if !s.is_empty() => s.parse::<usize>().unwrap_or(0),
_ => 0,
}
}
pub fn len(&self) -> usize {
match self {
Variable::String(s) => s.len(),
Variable::Integer(_) | Variable::Float(_) => 2,
Variable::Array(l) => l.iter().map(|v| v.len() + 2).sum(),
}
}
pub fn is_empty(&self) -> bool {
match self {
Variable::String(s) => s.is_empty(),
_ => false,
}
}
pub fn as_array(&self) -> Option<&[Variable]> {
match self {
Variable::Array(l) => Some(l),
_ => None,
}
}
pub fn into_array(self) -> Arc<Vec<Variable>> {
match self {
Variable::Array(l) => l,
v if !v.is_empty() => vec![v].into(),
_ => vec![].into(),
}
}
pub fn to_array(&self) -> Arc<Vec<Variable>> {
match self {
Variable::Array(l) => l.clone(),
v if !v.is_empty() => vec![v.clone()].into(),
_ => vec![].into(),
}
}
pub fn into_string_array(self) -> Vec<String> {
match self {
Variable::Array(l) => l.iter().map(|i| i.to_string().into_owned()).collect(),
v if !v.is_empty() => vec![v.to_string().into_owned()],
_ => vec![],
}
}
pub fn to_string_array(&self) -> Vec<Cow<'_, str>> {
match self {
Variable::Array(l) => l.iter().map(|i| i.to_string()).collect(),
v if !v.is_empty() => vec![v.to_string()],
_ => vec![],
}
}
}
impl From<String> for Variable {
fn from(s: String) -> Self {
Variable::String(s.into())
}
}
impl<'x> From<&'x String> for Variable {
fn from(s: &'x String) -> Self {
Variable::String(s.as_str().to_string().into())
}
}
impl<'x> From<&'x str> for Variable {
fn from(s: &'x str) -> Self {
Variable::String(s.to_string().into())
}
}
impl<'x> From<Cow<'x, str>> for Variable {
fn from(s: Cow<'x, str>) -> Self {
match s {
Cow::Borrowed(s) => Variable::String(s.to_string().into()),
Cow::Owned(s) => Variable::String(s.into()),
}
}
}
impl From<Vec<Variable>> for Variable {
fn from(l: Vec<Variable>) -> Self {
Variable::Array(l.into())
}
}
impl From<Number> for Variable {
fn from(n: Number) -> Self {
match n {
Number::Integer(n) => Variable::Integer(n),
Number::Float(n) => Variable::Float(n),
}
}
}
impl From<usize> for Variable {
fn from(n: usize) -> Self {
Variable::Integer(n as i64)
}
}
impl From<i64> for Variable {
fn from(n: i64) -> Self {
Variable::Integer(n)
}
}
impl From<u64> for Variable {
fn from(n: u64) -> Self {
Variable::Integer(n as i64)
}
}
impl From<f64> for Variable {
fn from(n: f64) -> Self {
Variable::Float(n)
}
}
impl From<i32> for Variable {
fn from(n: i32) -> Self {
Variable::Integer(n as i64)
}
}
impl From<u32> for Variable {
fn from(n: u32) -> Self {
Variable::Integer(n as i64)
}
}
impl From<bool> for Variable {
fn from(b: bool) -> Self {
Variable::Integer(i64::from(b))
}
}
impl PartialEq for Number {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::Integer(a), Self::Integer(b)) => a == b,
(Self::Float(a), Self::Float(b)) => a == b,
(Self::Integer(a), Self::Float(b)) => (*a as f64) == *b,
(Self::Float(a), Self::Integer(b)) => *a == (*b as f64),
}
}
}
impl Eq for Number {}
impl PartialOrd for Number {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
let (a, b) = match (self, other) {
(Number::Integer(a), Number::Integer(b)) => return a.partial_cmp(b),
(Number::Float(a), Number::Float(b)) => (*a, *b),
(Number::Integer(a), Number::Float(b)) => (*a as f64, *b),
(Number::Float(a), Number::Integer(b)) => (*a, *b as f64),
};
a.partial_cmp(&b)
}
}
impl self::eval::ToString for Vec<Variable> {
fn to_string(&self) -> String {
let mut result = String::with_capacity(self.len() * 10);
for item in self {
if !result.is_empty() {
result.push_str("\r\n");
}
match item {
Variable::String(v) => result.push_str(v),
Variable::Integer(v) => result.push_str(&v.to_string()),
Variable::Float(v) => result.push_str(&v.to_string()),
Variable::Array(_) => {}
}
}
result
}
}
impl Hash for Variable {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
match self {
Variable::String(s) => s.hash(state),
Variable::Integer(n) => n.hash(state),
Variable::Float(n) => n.to_bits().hash(state),
Variable::Array(l) => l.hash(state),
}
}
}
#[cfg(not(test))]
impl Runtime {
pub fn filter<'z: 'x, 'x>(&'z self, raw_message: &'x [u8]) -> Context<'x> {
Context::new(
self,
MessageParser::new()
.parse(raw_message)
.unwrap_or_else(|| Message {
parts: vec![MessagePart {
headers: vec![],
is_encoding_problem: false,
body: PartType::Text("".into()),
encoding: Encoding::None,
offset_header: 0,
offset_body: 0,
offset_end: 0,
}],
raw_message: b""[..].into(),
..Default::default()
}),
)
}
pub fn filter_parsed<'z: 'x, 'x>(&'z self, message: Message<'x>) -> Context<'x> {
Context::new(self, message)
}
}
impl Default for Runtime {
fn default() -> Self {
Self::new()
}
}
impl Runtime {
pub fn new() -> Self {
#[allow(unused_mut)]
let mut allowed_capabilities = AHashSet::from_iter(Capability::all().iter().cloned());
#[cfg(test)]
allowed_capabilities.insert(Capability::Other("vnd.inbuxa.testsuite".to_string()));
Runtime {
allowed_capabilities,
environment: AHashMap::from_iter([
("name".into(), "inbuxa Sieve".into()),
("version".into(), env!("CARGO_PKG_VERSION").into()),
]),
metadata: Vec::new(),
include_scripts: AHashMap::new(),
max_nested_includes: 3,
cpu_limit: 5000,
max_variable_size: 4096,
max_redirects: 1,
max_received_headers: 10,
protected_headers: vec![
HeaderName::Other("Original-Subject".into()),
HeaderName::Other("Original-From".into()),
],
valid_notification_uris: AHashSet::new(),
valid_ext_lists: AHashSet::new(),
vacation_use_orig_rcpt: false,
vacation_default_subject: "Automated reply".into(),
vacation_subject_prefix: "Auto: ".into(),
max_header_size: 1024,
max_out_messages: 3,
default_vacation_expiry: 30 * 86400,
default_duplicate_expiry: 7 * 86400,
local_hostname: "localhost".into(),
functions: Vec::new(),
}
}
pub fn set_cpu_limit(&mut self, size: usize) {
self.cpu_limit = size;
}
pub fn with_cpu_limit(mut self, size: usize) -> Self {
self.cpu_limit = size;
self
}
pub fn set_max_nested_includes(&mut self, size: usize) {
self.max_nested_includes = size;
}
pub fn with_max_nested_includes(mut self, size: usize) -> Self {
self.max_nested_includes = size;
self
}
pub fn set_max_redirects(&mut self, size: usize) {
self.max_redirects = size;
}
pub fn with_max_redirects(mut self, size: usize) -> Self {
self.max_redirects = size;
self
}
pub fn set_max_out_messages(&mut self, size: usize) {
self.max_out_messages = size;
}
pub fn with_max_out_messages(mut self, size: usize) -> Self {
self.max_out_messages = size;
self
}
pub fn set_max_received_headers(&mut self, size: usize) {
self.max_received_headers = size;
}
pub fn with_max_received_headers(mut self, size: usize) -> Self {
self.max_received_headers = size;
self
}
pub fn set_max_variable_size(&mut self, size: usize) {
self.max_variable_size = size;
}
pub fn with_max_variable_size(mut self, size: usize) -> Self {
self.max_variable_size = size;
self
}
pub fn set_max_header_size(&mut self, size: usize) {
self.max_header_size = size;
}
pub fn with_max_header_size(mut self, size: usize) -> Self {
self.max_header_size = size;
self
}
pub fn set_default_vacation_expiry(&mut self, expiry: u64) {
self.default_vacation_expiry = expiry;
}
pub fn with_default_vacation_expiry(mut self, expiry: u64) -> Self {
self.default_vacation_expiry = expiry;
self
}
pub fn set_default_duplicate_expiry(&mut self, expiry: u64) {
self.default_duplicate_expiry = expiry;
}
pub fn with_default_duplicate_expiry(mut self, expiry: u64) -> Self {
self.default_duplicate_expiry = expiry;
self
}
pub fn set_capability(&mut self, capability: impl Into<Capability>) {
self.allowed_capabilities.insert(capability.into());
}
pub fn with_capability(mut self, capability: impl Into<Capability>) -> Self {
self.set_capability(capability);
self
}
pub fn unset_capability(&mut self, capability: impl Into<Capability>) {
self.allowed_capabilities.remove(&capability.into());
}
pub fn without_capability(mut self, capability: impl Into<Capability>) -> Self {
self.unset_capability(capability);
self
}
pub fn without_capabilities(
mut self,
capabilities: impl IntoIterator<Item = impl Into<Capability>>,
) -> Self {
for capability in capabilities {
self.allowed_capabilities.remove(&capability.into());
}
self
}
pub fn set_protected_header(&mut self, header_name: impl Into<Cow<'static, str>>) {
if let Some(header_name) = HeaderName::parse(header_name) {
self.protected_headers.push(header_name);
}
}
pub fn with_protected_header(mut self, header_name: impl Into<Cow<'static, str>>) -> Self {
self.set_protected_header(header_name);
self
}
pub fn with_protected_headers(
mut self,
header_names: impl IntoIterator<Item = impl Into<Cow<'static, str>>>,
) -> Self {
self.protected_headers = header_names
.into_iter()
.filter_map(HeaderName::parse)
.collect();
self
}
pub fn set_env_variable(
&mut self,
name: impl Into<Cow<'static, str>>,
value: impl Into<Variable>,
) {
self.environment.insert(name.into(), value.into());
}
pub fn with_env_variable(
mut self,
name: impl Into<Cow<'static, str>>,
value: impl Into<Cow<'static, str>>,
) -> Self {
self.set_env_variable(name.into(), value.into());
self
}
pub fn set_medatata(
&mut self,
name: impl Into<Metadata<String>>,
value: impl Into<Cow<'static, str>>,
) {
self.metadata.push((name.into(), value.into()));
}
pub fn with_metadata(
mut self,
name: impl Into<Metadata<String>>,
value: impl Into<Cow<'static, str>>,
) -> Self {
self.set_medatata(name, value);
self
}
pub fn set_valid_notification_uri(&mut self, uri: impl Into<Cow<'static, str>>) {
self.valid_notification_uris.insert(uri.into());
}
pub fn with_valid_notification_uri(mut self, uri: impl Into<Cow<'static, str>>) -> Self {
self.valid_notification_uris.insert(uri.into());
self
}
pub fn with_valid_notification_uris(
mut self,
uris: impl IntoIterator<Item = impl Into<Cow<'static, str>>>,
) -> Self {
self.valid_notification_uris = uris.into_iter().map(Into::into).collect();
self
}
pub fn set_valid_ext_list(&mut self, name: impl Into<Cow<'static, str>>) {
self.valid_ext_lists.insert(name.into());
}
pub fn with_valid_ext_list(mut self, name: impl Into<Cow<'static, str>>) -> Self {
self.set_valid_ext_list(name);
self
}
pub fn set_vacation_use_orig_rcpt(&mut self, value: bool) {
self.vacation_use_orig_rcpt = value;
}
pub fn with_valid_ext_lists(
mut self,
lists: impl IntoIterator<Item = impl Into<Cow<'static, str>>>,
) -> Self {
self.valid_ext_lists = lists.into_iter().map(Into::into).collect();
self
}
pub fn with_vacation_use_orig_rcpt(mut self, value: bool) -> Self {
self.set_vacation_use_orig_rcpt(value);
self
}
pub fn set_vacation_default_subject(&mut self, value: impl Into<Cow<'static, str>>) {
self.vacation_default_subject = value.into();
}
pub fn with_vacation_default_subject(mut self, value: impl Into<Cow<'static, str>>) -> Self {
self.set_vacation_default_subject(value);
self
}
pub fn set_vacation_subject_prefix(&mut self, value: impl Into<Cow<'static, str>>) {
self.vacation_subject_prefix = value.into();
}
pub fn with_vacation_subject_prefix(mut self, value: impl Into<Cow<'static, str>>) -> Self {
self.set_vacation_subject_prefix(value);
self
}
pub fn set_local_hostname(&mut self, value: impl Into<Cow<'static, str>>) {
self.local_hostname = value.into();
}
pub fn with_local_hostname(mut self, value: impl Into<Cow<'static, str>>) -> Self {
self.set_local_hostname(value);
self
}
pub fn with_functions(mut self, fnc_map: &mut FunctionMap) -> Self {
self.functions = std::mem::take(&mut fnc_map.functions);
self
}
pub fn set_functions(&mut self, fnc_map: &mut FunctionMap) {
self.functions = std::mem::take(&mut fnc_map.functions);
}
}
impl FunctionMap {
pub fn new() -> Self {
FunctionMap {
map: Default::default(),
functions: Default::default(),
}
}
pub fn with_function(self, name: impl Into<String>, fnc: Function) -> Self {
self.with_function_args(name, fnc, 1)
}
pub fn with_function_no_args(self, name: impl Into<String>, fnc: Function) -> Self {
self.with_function_args(name, fnc, 0)
}
pub fn with_function_args(
mut self,
name: impl Into<String>,
fnc: Function,
num_args: u32,
) -> Self {
self.map
.insert(name.into(), (self.functions.len() as u32, num_args));
self.functions.push(fnc);
self
}
pub fn with_external_function(
mut self,
name: impl Into<String>,
id: ExternalId,
num_args: u32,
) -> Self {
self.set_external_function(name, id, num_args);
self
}
pub fn set_external_function(
&mut self,
name: impl Into<String>,
id: ExternalId,
num_args: u32,
) {
self.map.insert(name.into(), (ID_EXTERNAL - id, num_args));
}
}
impl Input {
pub fn script(name: impl Into<Script>, script: impl Into<Arc<Sieve>>) -> Self {
Input::Script {
name: name.into(),
script: script.into(),
}
}
pub fn success() -> Self {
Input::True
}
pub fn fail() -> Self {
Input::False
}
pub fn result(result: Variable) -> Self {
Input::FncResult(result)
}
}
impl From<bool> for Input {
fn from(value: bool) -> Self {
if value { Input::True } else { Input::False }
}
}
impl From<Variable> for Input {
fn from(value: Variable) -> Self {
Input::FncResult(value)
}
}
impl Deref for Script {
type Target = String;
fn deref(&self) -> &Self::Target {
match self {
Script::Personal(name) | Script::Global(name) => name,
}
}
}
impl AsRef<str> for Script {
fn as_ref(&self) -> &str {
match self {
Script::Personal(name) | Script::Global(name) => name.as_str(),
}
}
}
impl AsRef<String> for Script {
fn as_ref(&self) -> &String {
match self {
Script::Personal(name) | Script::Global(name) => name,
}
}
}
impl Script {
pub fn into_string(self) -> String {
match self {
Script::Personal(name) | Script::Global(name) => name,
}
}
pub fn as_str(&self) -> &String {
match self {
Script::Personal(name) | Script::Global(name) => name,
}
}
}
impl Display for Script {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
impl From<String> for Script {
fn from(name: String) -> Self {
Script::Personal(name)
}
}
impl From<&str> for Script {
fn from(name: &str) -> Self {
Script::Personal(name.to_string())
}
}
impl<T> Metadata<T> {
pub fn server(annotation: impl Into<T>) -> Self {
Metadata::Server {
annotation: annotation.into(),
}
}
pub fn mailbox(name: impl Into<T>, annotation: impl Into<T>) -> Self {
Metadata::Mailbox {
name: name.into(),
annotation: annotation.into(),
}
}
}
impl From<String> for Metadata<String> {
fn from(annotation: String) -> Self {
Metadata::Server { annotation }
}
}
impl From<&'_ str> for Metadata<String> {
fn from(annotation: &'_ str) -> Self {
Metadata::Server {
annotation: annotation.to_string(),
}
}
}
impl From<(String, String)> for Metadata<String> {
fn from((name, annotation): (String, String)) -> Self {
Metadata::Mailbox { name, annotation }
}
}
impl From<(&'_ str, &'_ str)> for Metadata<String> {
fn from((name, annotation): (&'_ str, &'_ str)) -> Self {
Metadata::Mailbox {
name: name.to_string(),
annotation: annotation.to_string(),
}
}
}
+176
View File
@@ -0,0 +1,176 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::{borrow::Cow, sync::Arc};
use crate::{
MatchAs,
compiler::{
Number, Value,
grammar::{Comparator, RelationalMatch},
},
runtime::Variable,
};
use super::glob::GlobPattern;
pub(crate) trait Comparable {
fn to_str(&'_ self) -> Cow<'_, str>;
fn to_number(&self) -> Number;
}
impl Comparator {
pub(crate) fn is(&self, a: &impl Comparable, b: &impl Comparable) -> bool {
match self {
Comparator::Octet => a.to_str() == b.to_str(),
Comparator::AsciiNumeric => RelationalMatch::Eq.cmp(&a.to_number(), &b.to_number()),
_ => a.to_str().to_lowercase() == b.to_str().to_lowercase(),
}
}
pub(crate) fn contains(&self, haystack: &str, needle: &str) -> bool {
needle.is_empty()
|| match self {
Comparator::Octet => haystack.contains(needle),
_ => haystack.to_lowercase().contains(&needle.to_lowercase()),
}
}
pub(crate) fn relational(
&self,
relation: &RelationalMatch,
a: &impl Comparable,
b: &impl Comparable,
) -> bool {
match self {
Comparator::Octet => relation.cmp(a.to_str().as_ref(), b.to_str().as_ref()),
Comparator::AsciiNumeric => relation.cmp(&a.to_number(), &b.to_number()),
_ => relation.cmp(&a.to_str().to_lowercase(), &b.to_str().to_lowercase()),
}
}
pub(crate) fn matches(
&self,
value: &str,
pattern: &str,
capture_positions: u64,
captured_values: &mut Vec<(usize, String)>,
) -> bool {
let pattern = GlobPattern::compile(pattern, matches!(self, Comparator::AsciiCaseMap));
match self {
Comparator::AsciiCaseMap if capture_positions == 0 => pattern.matches(value),
Comparator::AsciiCaseMap => pattern.capture(value, capture_positions, captured_values),
_ if capture_positions == 0 => pattern.matches(value),
_ => pattern.capture(value, capture_positions, captured_values),
}
}
pub(crate) fn regex(
&self,
pattern: &Value,
pattern_expr: &Variable,
value: &str,
capture_positions: u64,
captured_values: &mut Vec<(usize, String)>,
) -> bool {
if let Value::Regex(regex) = pattern {
let lazy_regex = regex.regex.0.load();
if let Some(regex) = lazy_regex.as_ref() {
eval_regex(regex, value, capture_positions, captured_values)
} else {
match fancy_regex::Regex::new(&regex.expr) {
Ok(fancy_regex) => {
let result =
eval_regex(&fancy_regex, value, capture_positions, captured_values);
regex.regex.0.store(Arc::new(Some(fancy_regex)));
result
}
Err(err) => {
debug_assert!(false, "Failed to compile regex: {err:?}");
false
}
}
}
} else {
match fancy_regex::Regex::new(pattern_expr.to_string().as_ref()) {
Ok(regex) => eval_regex(&regex, value, capture_positions, captured_values),
Err(err) => {
debug_assert!(false, "Failed to compile regex: {err:?}");
false
}
}
}
}
pub(crate) fn as_match(&self) -> MatchAs {
match self {
Comparator::AsciiCaseMap => MatchAs::Lowercase,
Comparator::AsciiNumeric => MatchAs::Number,
_ => MatchAs::Octet,
}
}
}
fn eval_regex(
regex: &fancy_regex::Regex,
value: &str,
mut capture_positions: u64,
captured_values: &mut Vec<(usize, String)>,
) -> bool {
if capture_positions == 0 {
regex.is_match(value).unwrap_or_default()
} else if let Ok(Some(captures)) = regex.captures(value) {
captured_values.clear();
while capture_positions != 0 {
let index = 63 - capture_positions.leading_zeros();
capture_positions ^= 1 << index;
if let Some(match_var) = captures.get(index as usize) {
captured_values.push((index as usize, match_var.as_str().to_string()));
}
}
true
} else {
false
}
}
impl Comparable for Variable {
fn to_str(&'_ self) -> Cow<'_, str> {
self.to_string()
}
fn to_number(&self) -> Number {
self.to_number()
}
}
impl Comparable for &str {
fn to_str(&'_ self) -> Cow<'_, str> {
(*self).into()
}
fn to_number(&self) -> Number {
self.parse::<f64>()
.map(Number::Float)
.unwrap_or(Number::Float(0.0))
}
}
impl RelationalMatch {
pub fn cmp<T>(&self, a: &T, b: &T) -> bool
where
T: PartialOrd + ?Sized,
{
match self {
RelationalMatch::Gt => a.gt(b),
RelationalMatch::Ge => a.ge(b),
RelationalMatch::Lt => a.lt(b),
RelationalMatch::Le => a.le(b),
RelationalMatch::Eq => a.eq(b),
RelationalMatch::Ne => a.ne(b),
}
}
}
+300
View File
@@ -0,0 +1,300 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::char::REPLACEMENT_CHARACTER;
use crate::MAX_MATCH_VARIABLES;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GlobPattern {
pattern: Vec<PatternChar>,
to_lower: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PatternChar {
WildcardMany { num: usize, match_pos: usize },
WildcardSingle { match_pos: usize },
Char { char: char, match_pos: usize },
}
impl GlobPattern {
pub fn compile(pattern: &str, to_lower: bool) -> Self {
let mut chars = Vec::new();
let mut is_escaped = false;
let mut str = pattern.chars().peekable();
while let Some(char) = str.next() {
match char {
'*' if !is_escaped => {
let mut num = 1;
while let Some('*') = str.peek() {
num += 1;
str.next();
}
chars.push(PatternChar::WildcardMany { num, match_pos: 0 });
}
'?' if !is_escaped => {
chars.push(PatternChar::WildcardSingle { match_pos: 0 });
}
'\\' if !is_escaped => {
is_escaped = true;
continue;
}
_ => {
if is_escaped {
is_escaped = false;
}
if to_lower && char.is_uppercase() {
for char in char.to_lowercase() {
chars.push(PatternChar::Char { char, match_pos: 0 });
}
} else {
chars.push(PatternChar::Char { char, match_pos: 0 });
}
}
}
}
GlobPattern {
pattern: chars,
to_lower,
}
}
// Credits: Algorithm ported from https://research.swtch.com/glob
pub fn matches(&self, value: &str) -> bool {
let value = if self.to_lower {
value.to_lowercase().chars().collect::<Vec<_>>()
} else {
value.chars().collect::<Vec<_>>()
};
let mut px = 0;
let mut nx = 0;
let mut next_px = 0;
let mut next_nx = 0;
while px < self.pattern.len() || nx < value.len() {
match self.pattern.get(px) {
Some(PatternChar::Char { char, .. }) => {
if matches!(value.get(nx), Some(nc) if nc == char ) {
px += 1;
nx += 1;
continue;
}
}
Some(PatternChar::WildcardSingle { .. }) if nx < value.len() => {
px += 1;
nx += 1;
continue;
}
Some(PatternChar::WildcardMany { .. }) => {
next_px = px;
next_nx = nx + 1;
px += 1;
continue;
}
_ => (),
}
if 0 < next_nx && next_nx <= value.len() {
px = next_px;
nx = next_nx;
continue;
}
return false;
}
true
}
pub fn capture(
mut self,
value_: &str,
capture_positions: u64,
captured_values: &mut Vec<(usize, String)>,
) -> bool {
let value = if self.to_lower {
let mut value = Vec::with_capacity(value_.len());
for char in value_.chars() {
if char.is_uppercase() {
for (pos, lowerchar) in char.to_lowercase().enumerate() {
value.push((
lowerchar,
if pos == 0 {
char
} else {
REPLACEMENT_CHARACTER
},
));
}
} else {
value.push((char, char));
}
}
value
} else {
value_.chars().map(|char| (char, char)).collect::<Vec<_>>()
};
let mut px = 0;
let mut nx = 0;
let mut next_px = 0;
let mut next_nx = 0;
while px < self.pattern.len() || nx < value.len() {
match self.pattern.get_mut(px) {
Some(PatternChar::Char { char, match_pos }) => {
if matches!(value.get(nx), Some(nc) if &nc.0 == char ) {
*match_pos = nx;
px += 1;
nx += 1;
continue;
}
}
Some(PatternChar::WildcardSingle { match_pos }) if nx < value.len() => {
*match_pos = nx;
px += 1;
nx += 1;
continue;
}
Some(PatternChar::WildcardMany { match_pos, .. }) => {
*match_pos = nx;
next_px = px;
next_nx = nx + 1;
px += 1;
continue;
}
_ => (),
}
if 0 < next_nx && next_nx <= value.len() {
px = next_px;
nx = next_nx;
continue;
}
return false;
}
let mut last_pos = 0;
captured_values.clear();
if capture_positions & 1 != 0 {
captured_values.push((0usize, value_.to_string()));
}
let mut wildcard_pos: usize = 1;
for item in &mut self.pattern {
if wildcard_pos <= MAX_MATCH_VARIABLES as usize {
last_pos = match item {
PatternChar::WildcardMany { num, match_pos } => {
while *num > 1 {
if capture_positions & (1 << wildcard_pos) != 0 {
captured_values.push((wildcard_pos, String::with_capacity(0)));
}
wildcard_pos += 1;
*num -= 1;
}
if capture_positions & (1 << wildcard_pos) != 0 {
if let Some(range) = value.get(last_pos..*match_pos) {
captured_values.push((
wildcard_pos,
range
.iter()
.filter_map(|(_, char)| {
if char != &REPLACEMENT_CHARACTER {
Some(char)
} else {
None
}
})
.collect::<String>(),
));
} else {
debug_assert!(false, "Glob pattern failure.");
return false;
}
}
wildcard_pos += 1;
*match_pos
}
PatternChar::WildcardSingle { match_pos } => {
if capture_positions & (1 << wildcard_pos) != 0 {
if let Some((char, orig_char)) = value.get(*match_pos) {
captured_values.push((
wildcard_pos,
(if orig_char != &REPLACEMENT_CHARACTER {
orig_char
} else {
char
})
.to_string(),
));
} else {
debug_assert!(false, "Glob pattern failure.");
return false;
}
}
wildcard_pos += 1;
*match_pos
}
PatternChar::Char { match_pos, .. } => *match_pos,
} + 1;
} else {
break;
}
}
true
}
}
#[cfg(test)]
mod tests {
use crate::runtime::tests::glob::GlobPattern;
#[test]
fn glob_match() {
for (value, pattern, expected_result) in [
(
"frop.......frop.........frop....",
"?*frop*",
vec!["f", "rop.......", ".........frop...."],
),
("frop:frup:frop", "*:*:*", vec!["frop", "frup", "frop"]),
(
"a b c d e f g",
"? ? ? ? ? ? ?",
vec!["a", "b", "c", "d", "e", "f", "g"],
),
("puk pok puk pok", "pu*ok", vec!["k pok puk p"]),
("snot kip snot", "snot*snot", vec![" kip "]),
(
"klopfropstroptop",
"*fr??*top",
vec!["klop", "o", "p", "strop"],
),
("toptoptop", "*top", vec!["toptop"]),
(
"Fehlende Straße zur Karte hinzufügen",
"FEHLENDE * ZUR Karte HINZUFÜGEN",
vec!["Straße"],
),
] {
let p = GlobPattern::compile(pattern, true);
let mut match_values = Vec::new();
assert!(
p.clone().capture(value, u64::MAX ^ 1, &mut match_values),
"{value:?} {pattern:?}",
);
assert_eq!(
match_values.into_iter().map(|(_, v)| v).collect::<Vec<_>>(),
expected_result,
"{value:?} {pattern:?}",
);
assert!(p.matches(value), "{value:?} {pattern:?}",);
}
}
}
+203
View File
@@ -0,0 +1,203 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::slice::Iter;
use mail_parser::{Message, MessagePart, MimeHeaders, PartType};
use crate::Context;
#[derive(Debug)]
pub(crate) enum ContentTypeFilter {
Type(String),
TypeSubtype((String, String)),
}
pub(crate) struct SubpartIterator<'x> {
ctx: &'x Context<'x>,
iter: Iter<'x, u32>,
iter_stack: Vec<Iter<'x, u32>>,
anychild: bool,
}
impl<'x> SubpartIterator<'x> {
pub(crate) fn new(ctx: &'x Context<'x>, parts: &'x [u32], anychild: bool) -> Self {
SubpartIterator {
ctx,
iter: parts.iter(),
iter_stack: Vec::new(),
anychild,
}
}
#[allow(clippy::should_implement_trait)]
pub fn next(&mut self) -> Option<(u32, &MessagePart<'x>)> {
loop {
if let Some(&part_id) = self.iter.next() {
let subpart = self.ctx.message.parts.get(part_id as usize)?;
match &subpart.body {
PartType::Multipart(subparts) if self.anychild => {
self.iter_stack
.push(std::mem::replace(&mut self.iter, subparts.iter()));
}
_ => (),
}
return Some((part_id, subpart));
}
{
let prev_iter = self.iter_stack.pop()?;
self.iter = prev_iter;
}
}
}
}
impl<'x> Context<'x> {
pub(crate) fn find_nested_parts<'z: 'x>(
&'z self,
mut message: &'x Message<'x>,
ct_filter: &[ContentTypeFilter],
visitor_fnc: &mut impl FnMut(&MessagePart, &[u8]) -> bool,
) -> bool {
let mut iter_stack = Vec::new();
let mut iter = vec![self.part].into_iter();
loop {
while let Some(part_id) = iter.next() {
if let Some(subpart) = message.parts.get(part_id as usize) {
let process_part = if !ct_filter.is_empty() {
let mut process_part = false;
let (ct, cst) = if let Some(ct) = subpart.content_type() {
(ct.c_type.as_ref(), ct.c_subtype.as_deref().unwrap_or(""))
} else {
match &subpart.body {
PartType::Text(_) => ("text", "plain"),
PartType::Html(_) => ("text", "html"),
PartType::Message(_) => ("message", "rfc822"),
PartType::Multipart(_) => ("multipart", "mixed"),
_ => ("application", "octet-stream"),
}
};
for ctf in ct_filter {
match ctf {
ContentTypeFilter::Type(name) => {
if name.eq_ignore_ascii_case(ct) {
process_part = true;
break;
}
}
ContentTypeFilter::TypeSubtype((name, subname)) => {
if name.eq_ignore_ascii_case(ct)
&& subname.eq_ignore_ascii_case(cst)
{
process_part = true;
break;
}
}
}
}
process_part
} else {
true
};
if process_part && visitor_fnc(subpart, message.raw_message.as_ref()) {
return true;
}
match &subpart.body {
PartType::Multipart(subparts) => {
iter_stack.push((
std::mem::replace(&mut iter, subparts.clone().into_iter()),
None,
));
}
PartType::Message(next_message) => {
iter_stack.push((
std::mem::replace(&mut iter, vec![0].into_iter()),
Some(message),
));
message = next_message;
}
_ => (),
}
}
}
if let Some((prev_iter, prev_message)) = iter_stack.pop() {
iter = prev_iter;
if let Some(prev_message) = prev_message {
message = prev_message;
}
} else {
break;
}
}
false
}
pub(crate) fn find_nested_parts_ids(&self, include_current: bool) -> Vec<u32> {
if self.part == 0 {
if include_current {
(0u32..self.message.parts.len() as u32).collect()
} else if self.message.parts.len() > 1 {
(1u32..self.message.parts.len() as u32).collect()
} else {
Vec::new()
}
} else {
let mut part_ids = Vec::new();
let mut iter_stack = Vec::new();
if include_current {
part_ids.push(self.part);
}
if let Some(PartType::Multipart(subparts)) =
self.message.parts.get(self.part as usize).map(|p| &p.body)
{
let mut iter = subparts.iter();
loop {
while let Some(&part_id) = iter.next() {
part_ids.push(part_id);
if let Some(PartType::Multipart(subparts)) =
self.message.parts.get(part_id as usize).map(|p| &p.body)
{
iter_stack.push(std::mem::replace(&mut iter, subparts.iter()));
}
}
if let Some(prev_iter) = iter_stack.pop() {
iter = prev_iter;
} else {
break;
}
}
}
part_ids
}
}
}
impl ContentTypeFilter {
pub(crate) fn parse(ct: &str) -> Option<ContentTypeFilter> {
let mut iter = ct.split('/');
let name = iter.next()?;
if let Some(sub_name) = iter.next() {
if !name.is_empty() && !sub_name.is_empty() && iter.next().is_none() {
Some(ContentTypeFilter::TypeSubtype((
name.to_string(),
sub_name.to_string(),
)))
} else {
None
}
} else if !name.is_empty() {
Some(ContentTypeFilter::Type(name.to_string()))
} else {
None
}
}
}
+121
View File
@@ -0,0 +1,121 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
Context, Event, Mailbox,
compiler::grammar::{Capability, test::Test},
};
use super::RuntimeError;
pub mod comparator;
pub mod glob;
pub mod mime;
pub mod test_address;
pub mod test_body;
pub mod test_date;
pub mod test_duplicate;
pub mod test_envelope;
pub mod test_exists;
pub mod test_extlists;
pub mod test_hasflag;
pub mod test_header;
pub mod test_metadata;
pub mod test_notify;
pub mod test_size;
pub mod test_spamtest;
pub mod test_string;
pub(crate) enum TestResult {
Bool(bool),
Event { event: Event, is_not: bool },
Error(RuntimeError),
}
impl Test {
pub(crate) fn exec(&self, ctx: &mut Context) -> TestResult {
match &self {
Test::Header(test) => test.exec(ctx),
Test::Address(test) => test.exec(ctx),
Test::Envelope(test) => test.exec(ctx),
Test::Exists(test) => test.exec(ctx),
Test::Size(test) => test.exec(ctx),
Test::Body(test) => test.exec(ctx),
Test::String(test) => test.exec(ctx, false),
Test::HasFlag(test) => test.exec(ctx),
Test::Date(test) => test.exec(ctx),
Test::CurrentDate(test) => test.exec(ctx),
Test::Duplicate(test) => test.exec(ctx),
Test::NotifyMethodCapability(test) => test.exec(ctx),
Test::ValidNotifyMethod(test) => test.exec(ctx),
Test::Environment(test) => test.exec(ctx, true),
Test::ValidExtList(test) => test.exec(ctx),
Test::Ihave(test) => TestResult::Bool(
test.capabilities.iter().all(|c| {
![Capability::Variables, Capability::EncodedCharacter].contains(c)
&& ctx.runtime.allowed_capabilities.contains(c)
}) ^ test.is_not,
),
Test::MailboxExists(test) => TestResult::Event {
event: Event::MailboxExists {
mailboxes: test
.mailbox_names
.iter()
.map(|m| Mailbox::Name(ctx.eval_value(m).to_string().into_owned()))
.collect(),
special_use: Vec::new(),
},
is_not: test.is_not,
},
Test::Vacation(test) => test.exec(ctx),
Test::Metadata(test) => test.exec(ctx),
Test::MetadataExists(test) => test.exec(ctx),
Test::MailboxIdExists(test) => TestResult::Event {
event: Event::MailboxExists {
mailboxes: test
.mailbox_ids
.iter()
.map(|m| Mailbox::Id(ctx.eval_value(m).to_string().into_owned()))
.collect(),
special_use: Vec::new(),
},
is_not: test.is_not,
},
Test::SpamTest(test) => test.exec(ctx),
Test::VirusTest(test) => test.exec(ctx),
Test::SpecialUseExists(test) => TestResult::Event {
event: Event::MailboxExists {
mailboxes: if let Some(mailbox) = &test.mailbox {
vec![Mailbox::Name(
ctx.eval_value(mailbox).to_string().into_owned(),
)]
} else {
Vec::new()
},
special_use: ctx.eval_values_owned(&test.attributes),
},
is_not: test.is_not,
},
Test::Convert(test) => test.exec(ctx),
Test::True => TestResult::Bool(true),
Test::False => TestResult::Bool(false),
Test::Invalid(invalid) => {
TestResult::Error(RuntimeError::InvalidInstruction(invalid.clone()))
}
#[cfg(test)]
Test::TestCmd { arguments, is_not } => TestResult::Event {
event: Event::Function {
id: u32::MAX,
arguments: arguments
.iter()
.map(|s| ctx.eval_value(s).to_owned())
.collect(),
},
is_not: *is_not,
},
}
}
}
+296
View File
@@ -0,0 +1,296 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use mail_parser::{
Addr, Address, Header, HeaderValue,
parsers::{
MessageStream,
fields::address::{
parse_address_detail_part, parse_address_domain, parse_address_local_part,
parse_address_user_part,
},
},
};
use crate::{
Context, Event,
compiler::{
Number,
grammar::{AddressPart, MatchType, tests::test_address::TestAddress},
},
};
use super::TestResult;
impl TestAddress {
pub(crate) fn exec(&self, ctx: &mut Context) -> TestResult {
let key_list = ctx.eval_values(&self.key_list);
let header_list = ctx.parse_header_names(&self.header_list);
let result = match &self.match_type {
MatchType::Is | MatchType::Contains => {
let is_is = matches!(&self.match_type, MatchType::Is);
ctx.find_headers(
&header_list,
self.index,
self.mime_anychild,
|header, _, _| {
ctx.find_addresses(header, &self.address_part, |value| {
for key in &key_list {
if is_is {
if self.comparator.is(&value, key) {
return true;
}
} else if self.comparator.contains(value, key.to_string().as_ref())
{
return true;
}
}
false
})
},
)
}
MatchType::Value(rel_match) => ctx.find_headers(
&header_list,
self.index,
self.mime_anychild,
|header, _, _| {
ctx.find_addresses(header, &self.address_part, |value| {
for key in &key_list {
if self.comparator.relational(rel_match, &value, key) {
return true;
}
}
false
})
},
),
MatchType::Matches(capture_positions) | MatchType::Regex(capture_positions) => {
let mut captured_positions = Vec::new();
let is_matches = matches!(&self.match_type, MatchType::Matches(_));
let result = ctx.find_headers(
&header_list,
self.index,
self.mime_anychild,
|header, _, _| {
ctx.find_addresses(header, &self.address_part, |value| {
for (pattern_expr, pattern) in key_list.iter().zip(self.key_list.iter())
{
if is_matches {
if self.comparator.matches(
value,
pattern_expr.to_string().as_ref(),
*capture_positions,
&mut captured_positions,
) {
return true;
}
} else if self.comparator.regex(
pattern,
pattern_expr,
value,
*capture_positions,
&mut captured_positions,
) {
return true;
}
}
false
})
},
);
if !captured_positions.is_empty() {
ctx.set_match_variables(captured_positions);
}
result
}
MatchType::Count(rel_match) => {
let mut count: i64 = 0;
ctx.find_headers(
&header_list,
self.index,
self.mime_anychild,
|header, _, _| {
ctx.find_addresses(header, &self.address_part, |value| {
if !value.is_empty() {
count += 1;
}
false
})
},
);
let mut result = false;
for key in &key_list {
if rel_match.cmp(&Number::from(count), &key.to_number()) {
result = true;
break;
}
}
result
}
MatchType::List => {
let mut values: Vec<String> = Vec::new();
ctx.find_headers(
&header_list,
self.index,
self.mime_anychild,
|header, _, _| {
ctx.find_addresses(header, &self.address_part, |value| {
if !value.is_empty() && !values.iter().any(|v| v.eq(value)) {
values.push(value.to_string());
}
false
})
},
);
if !values.is_empty() {
return TestResult::Event {
event: Event::ListContains {
lists: ctx.eval_values_owned(&self.key_list),
values,
match_as: self.comparator.as_match(),
},
is_not: self.is_not,
};
}
false
}
};
TestResult::Bool(result ^ self.is_not)
}
}
impl Context<'_> {
#[allow(unused_assignments)]
pub(crate) fn find_addresses(
&self,
header: &Header,
part: &AddressPart,
mut visitor_fnc: impl FnMut(&str) -> bool,
) -> bool {
match &header.value {
HeaderValue::Address(Address::List(addr_list)) => {
for addr in addr_list {
if let Some(addr) = part.eval(addr)
&& visitor_fnc(addr)
{
return true;
}
}
false
}
HeaderValue::Address(Address::Group(group_list)) => {
for group in group_list {
for addr in &group.addresses {
if let Some(addr) = part.eval(addr)
&& visitor_fnc(addr)
{
return true;
}
}
}
false
}
_ => {
let mut raw_header = None;
let bytes = if header.offset_end > 0 {
self.message
.raw_message
.get(header.offset_start as usize..header.offset_end as usize)
.unwrap_or(b"")
} else if let HeaderValue::Text(text) = &header.value {
// Inserted header
raw_header = format!("{text}\n").into_bytes().into();
raw_header.as_deref().unwrap()
} else {
b""
};
match MessageStream::new(bytes).parse_address() {
HeaderValue::Address(Address::List(addr_list)) => {
for addr in &addr_list {
if let Some(addr) = part.eval(addr)
&& visitor_fnc(addr)
{
return true;
}
}
false
}
HeaderValue::Address(Address::Group(group_list)) => {
for group in group_list {
for addr in &group.addresses {
if let Some(addr) = part.eval(addr)
&& visitor_fnc(addr)
{
return true;
}
}
}
false
}
_ => visitor_fnc(""),
}
}
}
}
}
impl AddressPart {
pub(crate) fn eval<'x>(&self, addr: &'x Addr<'x>) -> Option<&'x str> {
let email = addr.address.as_deref().or(addr.name.as_deref());
match (self, email) {
(AddressPart::All, _) => email,
(AddressPart::LocalPart, Some(email)) if !email.is_empty() => {
parse_address_local_part(email)
}
(AddressPart::Domain, Some(email)) if !email.is_empty() => parse_address_domain(email),
(AddressPart::User, Some(email)) if !email.is_empty() => parse_address_user_part(email),
(AddressPart::Detail, Some(email)) if !email.is_empty() => {
parse_address_detail_part(email)
}
(AddressPart::Name, _) => addr.name.as_deref(),
_ => email,
}
}
pub(crate) fn eval_strict<'x>(&self, addr: &'x Addr<'x>) -> Option<&'x str> {
match (self, addr.address.as_deref()) {
(AddressPart::All, Some(email)) => Some(email),
(AddressPart::LocalPart, Some(email)) if !email.is_empty() => {
parse_address_local_part(email)
}
(AddressPart::Domain, Some(email)) if !email.is_empty() => parse_address_domain(email),
(AddressPart::User, Some(email)) if !email.is_empty() => parse_address_user_part(email),
(AddressPart::Detail, Some(email)) if !email.is_empty() => {
parse_address_detail_part(email)
}
(AddressPart::Name, _) => addr.name.as_deref(),
(_, email) => email,
}
}
pub(crate) fn eval_string<'x>(&self, addr: &'x str) -> Option<&'x str> {
if !addr.is_empty() {
match self {
AddressPart::All => addr.into(),
AddressPart::LocalPart => parse_address_local_part(addr),
AddressPart::Domain => parse_address_domain(addr),
AddressPart::User => parse_address_user_part(addr),
AddressPart::Detail => parse_address_detail_part(addr),
_ => addr.into(),
}
} else {
addr.into()
}
}
}
+224
View File
@@ -0,0 +1,224 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use mail_parser::{MimeHeaders, PartType, decoders::html::html_to_text};
use crate::{
Context,
compiler::{
Number,
grammar::{
MatchType,
tests::test_body::{BodyTransform, TestBody},
},
},
};
use super::{TestResult, mime::ContentTypeFilter};
impl TestBody {
pub(crate) fn exec(&self, ctx: &mut Context) -> TestResult {
// Check Subject (not a Sieve standard)
let key_list = ctx.eval_values(&self.key_list);
if self.include_subject {
let subject = if !matches!(&self.body_transform, BodyTransform::Raw) {
ctx.message.subject().unwrap_or_default()
} else {
ctx.message.header_raw("Subject").unwrap_or_default()
};
for (key, pattern) in key_list.iter().zip(self.key_list.iter()) {
let result = match &self.match_type {
MatchType::Is => self.comparator.is(&subject, key),
MatchType::Contains => {
self.comparator.contains(subject, key.to_string().as_ref())
}
MatchType::Value(rel_match) => {
self.comparator.relational(rel_match, &subject, key)
}
MatchType::Matches(_) => self.comparator.matches(
subject,
key.to_string().as_ref(),
0,
&mut Vec::new(),
),
MatchType::Regex(_) => {
self.comparator
.regex(pattern, key, subject, 0, &mut Vec::new())
}
_ => break,
};
if result {
return TestResult::Bool(result ^ self.is_not);
}
}
}
let ct_filter = match &self.body_transform {
BodyTransform::Text | BodyTransform::Raw => Vec::new(),
BodyTransform::Content(values) => {
let mut ct_filter = Vec::with_capacity(values.len());
for ct in values {
let ct = ctx.eval_value(ct);
if ct.is_empty() {
break;
} else if let Some(ctf) = ContentTypeFilter::parse(ct.to_string().as_ref()) {
ct_filter.push(ctf);
} else {
return TestResult::Bool(false ^ self.is_not);
}
}
ct_filter
}
};
let result = if let MatchType::Count(rel_match) = &self.match_type {
let mut count = 0;
let mut result = false;
ctx.find_nested_parts(&ctx.message, &ct_filter, &mut |_part, _raw_message| {
count += 1;
false
});
for key in &self.key_list {
if rel_match.cmp(&Number::from(count), &ctx.eval_value(key).to_number()) {
result = true;
break;
}
}
result
} else {
ctx.find_nested_parts(&ctx.message, &ct_filter, &mut |part, raw_message| {
let text = match (&self.body_transform, &part.body) {
(BodyTransform::Content(_), PartType::Message(message)) => {
if let Some(part) = message.parts.first() {
String::from_utf8_lossy(
raw_message
.get(
part.raw_header_offset() as usize
..part.raw_body_offset() as usize,
)
.unwrap_or(b""),
)
} else {
return false;
}
}
(BodyTransform::Content(_), PartType::Multipart(_)) => {
if let Some(boundary) =
part.content_type().and_then(|ct| ct.attribute("boundary"))
{
let mime_body = std::str::from_utf8(
raw_message
.get(
part.raw_body_offset() as usize
..part.raw_end_offset() as usize,
)
.unwrap_or(b""),
)
.unwrap_or("");
let mut mime_part = String::with_capacity(64);
if let Some((prologue, epilogue)) =
mime_body.split_once(&format!("\n--{boundary}"))
{
mime_part.push_str(prologue);
if let Some((_, epilogue)) =
epilogue.rsplit_once(&format!("\n--{boundary}--"))
{
mime_part.push_str(epilogue);
}
}
mime_part.into()
} else {
String::from_utf8_lossy(
raw_message
.get(
part.raw_body_offset() as usize
..part.raw_end_offset() as usize,
)
.unwrap_or(b""),
)
}
}
(BodyTransform::Raw, _) => {
match &part.body {
PartType::Text(text) if part.raw_body_offset() == 0 => {
// Inserted part
text.as_ref().into()
}
_ if part.raw_end_offset() > part.raw_body_offset() => {
String::from_utf8_lossy(
raw_message
.get(
part.raw_body_offset() as usize
..part.raw_end_offset() as usize,
)
.unwrap_or(b""),
)
}
_ => return false,
}
}
(_, PartType::Text(text))
| (BodyTransform::Content(_), PartType::Html(text)) => text.as_ref().into(),
(_, PartType::Html(html)) => html_to_text(html.as_ref()).into(),
(
BodyTransform::Text,
PartType::Binary(bytes) | PartType::InlineBinary(bytes),
) if part.content_type().is_some_and(|ct| {
ct.c_type.eq_ignore_ascii_case("application")
&& ct.c_subtype.as_ref().is_some_and(|st| st.contains("xml"))
}) =>
{
html_to_text(std::str::from_utf8(bytes.as_ref()).unwrap_or("")).into()
}
(
BodyTransform::Content(_),
PartType::Binary(bytes) | PartType::InlineBinary(bytes),
) => String::from_utf8_lossy(bytes.as_ref()),
_ => {
return false;
}
};
let mut result = false;
for (key, pattern) in key_list.iter().zip(self.key_list.iter()) {
result = match &self.match_type {
MatchType::Is => self.comparator.is(&text.as_ref(), key),
MatchType::Contains => self
.comparator
.contains(text.as_ref(), key.to_string().as_ref()),
MatchType::Value(rel_match) => {
self.comparator.relational(rel_match, &text.as_ref(), key)
}
MatchType::Matches(_) => self.comparator.matches(
text.as_ref(),
key.to_string().as_ref(),
0,
&mut Vec::new(),
),
MatchType::Regex(_) => {
self.comparator
.regex(pattern, key, text.as_ref(), 0, &mut Vec::new())
}
_ => false,
};
if result {
break;
}
}
result
})
};
TestResult::Bool(result ^ self.is_not)
}
}
+293
View File
@@ -0,0 +1,293 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::borrow::Cow;
use mail_parser::{DateTime, Header, HeaderValue, parsers::MessageStream};
use crate::{
Context, Event,
compiler::{
Number,
grammar::{
MatchType,
tests::test_date::{DatePart, TestCurrentDate, TestDate, Zone},
},
},
};
use super::TestResult;
impl TestDate {
pub(crate) fn exec(&self, ctx: &mut Context) -> TestResult {
let header_name = if let Some(header_name) = ctx.parse_header_name(&self.header_name) {
header_name
} else {
return TestResult::Bool(false ^ self.is_not);
};
let result = match &self.match_type {
MatchType::Count(rel_match) => {
let mut date_count = 0;
ctx.find_headers(
&[header_name],
self.index,
self.mime_anychild,
|header, _, _| {
if ctx.find_dates(header).is_some() {
date_count += 1;
}
false
},
);
let mut result = false;
for key in &self.key_list {
if rel_match.cmp(&Number::from(date_count), &ctx.eval_value(key).to_number()) {
result = true;
break;
}
}
result
}
MatchType::List => {
let mut values = Vec::new();
ctx.find_headers(
&[header_name],
self.index,
self.mime_anychild,
|header, _, _| {
if let Some(dt) = ctx.find_dates(header) {
let value = self.date_part.eval(self.zone.eval(dt.as_ref()).as_ref());
if !value.is_empty() && !values.iter().any(|v: &String| v.eq(&value)) {
values.push(value);
}
}
false
},
);
if !values.is_empty() {
return TestResult::Event {
event: Event::ListContains {
lists: ctx.eval_values_owned(&self.key_list),
values,
match_as: self.comparator.as_match(),
},
is_not: self.is_not,
};
}
false
}
_ => {
let key_list = ctx.eval_values(&self.key_list);
let mut captured_values = Vec::new();
let result = ctx.find_headers(
&[header_name],
self.index,
self.mime_anychild,
|header, _, _| {
if let Some(dt) = ctx.find_dates(header) {
let date_part =
self.date_part.eval(self.zone.eval(dt.as_ref()).as_ref());
for key in &key_list {
if match &self.match_type {
MatchType::Is => self.comparator.is(&date_part.as_str(), key),
MatchType::Contains => self
.comparator
.contains(&date_part, key.to_string().as_ref()),
MatchType::Value(rel_match) => self.comparator.relational(
rel_match,
&date_part.as_str(),
key,
),
MatchType::Matches(capture_positions) => {
self.comparator.matches(
&date_part,
key.to_string().as_ref(),
*capture_positions,
&mut captured_values,
)
}
MatchType::Regex(capture_positions) => self.comparator.matches(
&date_part,
key.to_string().as_ref(),
*capture_positions,
&mut captured_values,
),
MatchType::Count(_) | MatchType::List => false,
} {
return true;
}
}
}
false
},
);
if !captured_values.is_empty() {
ctx.set_match_variables(captured_values);
}
result
}
};
TestResult::Bool(result ^ self.is_not)
}
}
impl TestCurrentDate {
pub(crate) fn exec(&self, ctx: &mut Context) -> TestResult {
let mut result = false;
match &self.match_type {
MatchType::Count(rel_match) => {
for key in &self.key_list {
if rel_match.cmp(&Number::from(1.0), &ctx.eval_value(key).to_number()) {
result = true;
break;
}
}
}
MatchType::List => {
let value = self.date_part.eval(
&(if let Some(zone) = self.zone {
DateTime::from_timestamp(ctx.current_time).to_timezone(zone)
} else {
DateTime::from_timestamp(ctx.current_time)
}),
);
if !value.is_empty() {
return TestResult::Event {
event: Event::ListContains {
lists: ctx.eval_values_owned(&self.key_list),
values: vec![value],
match_as: self.comparator.as_match(),
},
is_not: self.is_not,
};
}
}
_ => {
let mut captured_values = Vec::new();
let date_part = self.date_part.eval(
&(if let Some(zone) = self.zone {
DateTime::from_timestamp(ctx.current_time).to_timezone(zone)
} else {
DateTime::from_timestamp(ctx.current_time)
}),
);
for key in &self.key_list {
let key = ctx.eval_value(key);
if match &self.match_type {
MatchType::Is => self.comparator.is(&date_part.as_str(), &key),
MatchType::Contains => self
.comparator
.contains(&date_part, key.to_string().as_ref()),
MatchType::Value(rel_match) => {
self.comparator
.relational(rel_match, &date_part.as_str(), &key)
}
MatchType::Matches(capture_positions) => self.comparator.matches(
&date_part,
key.to_string().as_ref(),
*capture_positions,
&mut captured_values,
),
MatchType::Regex(capture_positions) => self.comparator.matches(
&date_part,
key.to_string().as_ref(),
*capture_positions,
&mut captured_values,
),
MatchType::Count(_) | MatchType::List => false,
} {
result = true;
break;
}
}
if !captured_values.is_empty() {
ctx.set_match_variables(captured_values);
}
}
}
TestResult::Bool(result ^ self.is_not)
}
}
impl<'x> Context<'x> {
#[allow(unused_assignments)]
pub(crate) fn find_dates(&self, header: &'x Header) -> Option<Cow<'x, DateTime>> {
if let HeaderValue::DateTime(dt) = &header.value {
if dt.is_valid() {
return Some(Cow::Borrowed(dt));
}
} else if header.offset_end > 0 {
let bytes = self
.message
.raw_message
.get(header.offset_start as usize..header.offset_end as usize)?;
if let HeaderValue::DateTime(dt) = MessageStream::new(bytes).parse_date()
&& dt.is_valid()
{
return Some(Cow::Owned(dt));
}
} else if let HeaderValue::Text(text) = &header.value {
// Inserted header
let bytes = format!("{text}\n").into_bytes();
if let HeaderValue::DateTime(dt) = MessageStream::new(&bytes).parse_date()
&& dt.is_valid()
{
return Some(Cow::Owned(dt));
}
}
None
}
}
impl DatePart {
fn eval(&self, dt: &DateTime) -> String {
match self {
DatePart::Year => format!("{:04}", dt.year),
DatePart::Month => format!("{:02}", dt.month),
DatePart::Day => format!("{:02}", dt.day),
DatePart::Date => format!("{:04}-{:02}-{:02}", dt.year, dt.month, dt.day,),
DatePart::Julian => ((dt.julian_day() as f64 - 2400000.5) as i64).to_string(),
DatePart::Hour => format!("{:02}", dt.hour),
DatePart::Minute => format!("{:02}", dt.minute),
DatePart::Second => format!("{:02}", dt.second),
DatePart::Time => format!("{:02}:{:02}:{:02}", dt.hour, dt.minute, dt.second,),
DatePart::Iso8601 => dt.to_rfc3339(),
DatePart::Std11 => dt.to_rfc822(),
DatePart::Zone => format!(
"{}{:02}{:02}",
if dt.tz_before_gmt && (dt.tz_hour > 0 || dt.tz_minute > 0) {
"-"
} else {
"+"
},
dt.tz_hour,
dt.tz_minute
),
DatePart::Weekday => dt.day_of_week().to_string(),
}
}
}
impl Zone {
pub(crate) fn eval<'x>(&self, dt: &'x DateTime) -> Cow<'x, DateTime> {
match self {
Zone::Time(tz) => Cow::Owned(dt.to_timezone(*tz)),
Zone::Original => Cow::Borrowed(dt),
Zone::Local => Cow::Owned(DateTime::from_timestamp(dt.to_timestamp())),
}
}
}
+70
View File
@@ -0,0 +1,70 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::borrow::Cow;
use mail_parser::{HeaderValue, parsers::MessageStream};
use crate::{
Context, Event,
compiler::grammar::tests::test_duplicate::{DupMatch, TestDuplicate},
};
use super::TestResult;
impl TestDuplicate {
pub(crate) fn exec(&self, ctx: &mut Context) -> TestResult {
let id: Cow<str> = match &self.dup_match {
DupMatch::Header(header_name) => {
let mut value = String::new();
if let Some(header_name) = ctx.parse_header_name(header_name) {
ctx.find_headers(&[header_name], None, true, |header, _, _| {
if header.offset_end > 0 {
if let Some(bytes) = ctx
.message
.raw_message
.get(header.offset_start as usize..header.offset_end as usize)
&& let HeaderValue::Text(id) = MessageStream::new(bytes).parse_id()
&& !id.is_empty()
{
value = id.to_string();
return true;
}
} else if let HeaderValue::Text(text) = &header.value {
// Inserted header
let bytes = format!("{text}\n").into_bytes();
if let HeaderValue::Text(id) = MessageStream::new(&bytes).parse_id()
&& !id.is_empty()
{
value = id.to_string();
return true;
}
}
false
});
}
value.into()
}
DupMatch::UniqueId(s) => ctx.eval_value(s).to_string().into_owned().into(),
DupMatch::Default => ctx.message.message_id().unwrap_or("").into(),
};
TestResult::Event {
event: Event::DuplicateId {
id: if id.is_empty() {
return TestResult::Bool(false ^ self.is_not);
} else if let Some(handle) = &self.handle {
format!("{}{}", ctx.eval_value(handle).to_string(), id)
} else {
id.into_owned()
},
expiry: self.seconds.unwrap_or(ctx.runtime.default_duplicate_expiry),
last: self.last,
},
is_not: self.is_not,
}
}
}
+258
View File
@@ -0,0 +1,258 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use mail_parser::DateTime;
use crate::{
Context, Envelope, Event,
compiler::{
Number,
grammar::{MatchType, tests::test_envelope::TestEnvelope},
},
};
use super::TestResult;
impl TestEnvelope {
pub(crate) fn exec(&self, ctx: &mut Context) -> TestResult {
let key_list = ctx.eval_values(&self.key_list);
let result = match &self.match_type {
MatchType::Is | MatchType::Contains => {
let is_is = matches!(&self.match_type, MatchType::Is);
ctx.find_envelopes(self, |value| {
for key in &key_list {
if is_is {
if self.comparator.is(&value, key) {
return true;
}
} else if self.comparator.contains(value, key.to_string().as_ref()) {
return true;
}
}
false
})
}
MatchType::Value(rel_match) => ctx.find_envelopes(self, |value| {
for key in &key_list {
if self.comparator.relational(rel_match, &value, key) {
return true;
}
}
false
}),
MatchType::Matches(capture_positions) | MatchType::Regex(capture_positions) => {
let mut captured_positions = Vec::new();
let is_matches = matches!(&self.match_type, MatchType::Matches(_));
let result = ctx.find_envelopes(self, |value| {
for (pattern_expr, pattern) in key_list.iter().zip(self.key_list.iter()) {
if is_matches {
if self.comparator.matches(
value,
pattern_expr.to_string().as_ref(),
*capture_positions,
&mut captured_positions,
) {
return true;
}
} else if self.comparator.regex(
pattern,
pattern_expr,
value,
*capture_positions,
&mut captured_positions,
) {
return true;
}
}
false
});
if !captured_positions.is_empty() {
ctx.set_match_variables(captured_positions);
}
result
}
MatchType::Count(rel_match) => {
let mut count = 0;
ctx.find_envelopes(self, |value| {
if !value.is_empty() {
count += 1;
}
false
});
let mut result = false;
for key in &key_list {
if rel_match.cmp(&Number::from(count), &key.to_number()) {
result = true;
break;
}
}
result
}
MatchType::List => {
let mut values: Vec<String> = Vec::new();
ctx.find_envelopes(self, |value| {
if !value.is_empty() && !values.iter().any(|v| v.eq(value)) {
values.push(value.to_string());
}
false
});
if !values.is_empty() {
return TestResult::Event {
event: Event::ListContains {
lists: ctx.eval_values_owned(&self.key_list),
values,
match_as: self.comparator.as_match(),
},
is_not: self.is_not,
};
}
false
}
};
TestResult::Bool(result ^ self.is_not)
}
}
impl Context<'_> {
fn find_envelopes(
&self,
test_envelope: &TestEnvelope,
mut cb: impl FnMut(&str) -> bool,
) -> bool {
for (name, value) in &self.envelope {
if test_envelope.envelope_list.contains(name)
&& match name {
Envelope::From | Envelope::To | Envelope::Orcpt => {
if let Some(value) = test_envelope
.address_part
.eval_string(value.to_string().as_ref())
{
cb(value)
} else {
false
}
}
Envelope::ByTimeAbsolute if test_envelope.zone.is_some() => {
if let Some(dt) = DateTime::parse_rfc3339(value.to_string().as_ref()) {
cb(&dt.to_timezone(test_envelope.zone.unwrap()).to_rfc3339())
} else {
cb("")
}
}
_ => cb(value.to_string().as_ref()),
}
{
return true;
}
}
false
}
}
pub fn parse_envelope_address(addr: &str) -> Option<&str> {
let addr = addr.as_bytes();
let mut addr_start_pos = 0;
let mut addr_end_pos = addr.len();
let mut last_ch = 0;
let mut at_pos = 0;
let mut has_bracket = false;
let mut in_path = false;
if addr.is_empty() {
return "".into();
}
for (pos, &ch) in addr.iter().enumerate() {
match ch {
b'<' => {
if pos == 0 {
addr_start_pos = pos + 1;
has_bracket = true;
} else {
return None;
}
}
b'>' => {
if has_bracket && pos == addr.len() - 1 {
if addr.len() > 2 {
has_bracket = false;
addr_end_pos = pos;
} else {
// <>
return "".into();
}
} else {
return None;
}
}
b':' => {
if at_pos != 0 {
at_pos = 0;
addr_start_pos = pos + 1;
in_path = false;
} else {
return None;
}
}
b',' => {
if at_pos != 0 {
at_pos = 0;
in_path = true;
} else {
return None;
}
}
b'@' => {
if at_pos == 0 && pos != addr.len() - 1 {
at_pos = pos;
} else {
return None;
}
}
b'.' => {
if (at_pos != 0 && last_ch == b'.') || last_ch == b'@' {
return None;
}
}
_ => {
if ch.is_ascii_whitespace() || !ch.is_ascii() {
return None;
}
}
}
last_ch = ch;
}
if !has_bracket && !in_path && at_pos > addr_start_pos && addr_end_pos - 1 > at_pos {
std::str::from_utf8(&addr[addr_start_pos..addr_end_pos])
.unwrap()
.into()
} else {
match addr.get(addr_start_pos..addr_end_pos) {
Some(addr) if at_pos == 0 && addr.eq_ignore_ascii_case(b"mailer-daemon") => {
std::str::from_utf8(addr).unwrap().into()
}
_ => None,
}
}
}
+36
View File
@@ -0,0 +1,36 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{Context, compiler::grammar::tests::test_exists::TestExists};
use super::{TestResult, mime::SubpartIterator};
impl TestExists {
pub(crate) fn exec(&self, ctx: &mut Context) -> TestResult {
let header_names = ctx.parse_header_names(&self.header_names);
let mut header_exists = vec![false; header_names.len()];
let parts = [ctx.part];
let mut part_iter = SubpartIterator::new(ctx, &parts, self.mime_anychild);
let mut result = false;
while let Some((_, message_part)) = part_iter.next() {
for (pos, header_name) in header_names.iter().enumerate() {
if !header_exists[pos]
&& message_part.headers.iter().any(|h| &h.name == header_name)
{
header_exists[pos] = true;
}
}
if header_exists.iter().all(|v| *v) {
result = true;
break;
}
}
TestResult::Bool(result ^ self.is_not)
}
}
+27
View File
@@ -0,0 +1,27 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{Context, compiler::grammar::tests::test_extlists::TestValidExtList};
use super::TestResult;
impl TestValidExtList {
pub(crate) fn exec(&self, ctx: &mut Context) -> TestResult {
let mut num_valid = 0;
for list in &self.list_names {
if ctx
.runtime
.valid_ext_lists
.contains(&ctx.eval_value(list).to_string())
{
num_valid += 1;
}
}
TestResult::Bool((num_valid == self.list_names.len()) ^ self.is_not)
}
}
+96
View File
@@ -0,0 +1,96 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
Context,
compiler::{
Number, VariableType,
grammar::{MatchType, tests::test_hasflag::TestHasFlag},
},
};
use super::TestResult;
impl TestHasFlag {
pub(crate) fn exec(&self, ctx: &mut Context) -> TestResult {
let mut variable_list_ = None;
let variable_list = if !self.variable_list.is_empty() {
&self.variable_list
} else {
variable_list_.get_or_insert_with(|| vec![VariableType::Global("__flags".to_string())])
};
let result = if let MatchType::Count(rel_match) = &self.match_type {
let mut flag_count = 0;
for variable in variable_list {
match ctx.get_variable(variable) {
Some(flags) if !flags.is_empty() => {
flag_count += flags.to_string().split(' ').count();
}
_ => (),
}
}
let mut result = false;
for key in &self.flags {
if rel_match.cmp(
&Number::from(flag_count as i64),
&ctx.eval_value(key).to_number(),
) {
result = true;
break;
}
}
result
} else {
let mut captured_values = Vec::new();
let result = ctx.tokenize_flags(&self.flags, |check_flag| {
for variable in variable_list {
match ctx.get_variable(variable) {
Some(flags) if !flags.is_empty() => {
for flag in flags.to_string().split(' ') {
if match &self.match_type {
MatchType::Is => self.comparator.is(&flag, &check_flag),
MatchType::Contains => {
self.comparator.contains(flag, check_flag)
}
MatchType::Value(rel_match) => {
self.comparator.relational(rel_match, &flag, &check_flag)
}
MatchType::Matches(capture_positions) => {
self.comparator.matches(
flag,
check_flag,
*capture_positions,
&mut captured_values,
)
}
MatchType::Regex(capture_positions) => self.comparator.matches(
flag,
check_flag,
*capture_positions,
&mut captured_values,
),
MatchType::Count(_) | MatchType::List => false,
} {
return true;
}
}
}
_ => (),
}
}
false
});
if !captured_values.is_empty() {
ctx.set_match_variables(captured_values);
}
result
};
TestResult::Bool(result ^ self.is_not)
}
}
+364
View File
@@ -0,0 +1,364 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use mail_parser::{Header, HeaderName, HeaderValue, parsers::MessageStream};
use crate::{
Context, Event,
compiler::{
Number, Value,
grammar::{MatchType, actions::action_mime::MimeOpts, tests::test_header::TestHeader},
},
runtime::Variable,
};
use super::{TestResult, mime::SubpartIterator};
impl TestHeader {
pub(crate) fn exec(&self, ctx: &mut Context) -> TestResult {
let key_list = ctx.eval_values(&self.key_list);
let header_list = ctx.parse_header_names(&self.header_list);
let mime_opts = match &self.mime_opts {
MimeOpts::Type => MimeOpts::Type,
MimeOpts::Subtype => MimeOpts::Subtype,
MimeOpts::ContentType => MimeOpts::ContentType,
MimeOpts::Param(params) => MimeOpts::Param(ctx.eval_values(params)),
MimeOpts::None => MimeOpts::None,
};
let result = match &self.match_type {
MatchType::Is | MatchType::Contains => {
let is_is = matches!(&self.match_type, MatchType::Is);
ctx.find_headers(
&header_list,
self.index,
self.mime_anychild,
|header, _, _| {
ctx.find_header_values(header, &mime_opts, |value| {
for key in &key_list {
if is_is {
if self.comparator.is(&value, key) {
return true;
}
} else if self.comparator.contains(value, key.to_string().as_ref())
{
return true;
}
}
false
})
},
)
}
MatchType::Value(rel_match) => ctx.find_headers(
&header_list,
self.index,
self.mime_anychild,
|header, _, _| {
ctx.find_header_values(header, &mime_opts, |value| {
for key in &key_list {
if self.comparator.relational(rel_match, &value, key) {
return true;
}
}
false
})
},
),
MatchType::Matches(capture_positions) | MatchType::Regex(capture_positions) => {
let mut captured_values = Vec::new();
let is_matches = matches!(&self.match_type, MatchType::Matches(_));
let result = ctx.find_headers(
&header_list,
self.index,
self.mime_anychild,
|header, _, _| {
ctx.find_header_values(header, &mime_opts, |value| {
for (pattern_expr, pattern) in key_list.iter().zip(self.key_list.iter())
{
if is_matches {
if self.comparator.matches(
value,
pattern_expr.to_string().as_ref(),
*capture_positions,
&mut captured_values,
) {
return true;
}
} else if self.comparator.regex(
pattern,
pattern_expr,
value,
*capture_positions,
&mut captured_values,
) {
return true;
}
}
false
})
},
);
if !captured_values.is_empty() {
ctx.set_match_variables(captured_values);
}
result
}
MatchType::Count(rel_match) => {
let mut count = 0;
ctx.find_headers(
&header_list,
self.index,
self.mime_anychild,
|header, _, _| {
match &mime_opts {
MimeOpts::None => {
count += 1;
}
MimeOpts::Type | MimeOpts::Subtype | MimeOpts::ContentType => {
if let HeaderValue::ContentType(_) = &header.value {
count += 1;
}
}
MimeOpts::Param(params) => {
if let HeaderValue::ContentType(ct) = &header.value
&& let Some(attributes) = &ct.attributes
{
for attr in attributes {
if params
.iter()
.any(|p| p.to_string().eq_ignore_ascii_case(&attr.name))
{
count += 1;
}
}
}
}
}
false
},
);
let mut result = false;
for key in &key_list {
if rel_match.cmp(&Number::from(count), &key.to_number()) {
result = true;
break;
}
}
result
}
MatchType::List => {
let mut values: Vec<String> = Vec::new();
ctx.find_headers(
&header_list,
self.index,
self.mime_anychild,
|header, _, _| {
ctx.find_header_values(header, &mime_opts, |value| {
if !value.is_empty() && !values.iter().any(|v| v.eq(value)) {
values.push(value.to_string());
}
false
})
},
);
if !values.is_empty() {
return TestResult::Event {
event: Event::ListContains {
lists: ctx.eval_values_owned(&self.key_list),
values,
match_as: self.comparator.as_match(),
},
is_not: self.is_not,
};
}
false
}
};
TestResult::Bool(result ^ self.is_not)
}
}
impl Context<'_> {
pub(crate) fn parse_header_names<'z: 'y, 'y>(
&'z self,
header_names: &'y [Value],
) -> Vec<HeaderName<'y>> {
let mut result = Vec::with_capacity(header_names.len());
for header_name in header_names {
if let Some(header_name) = self.parse_header_name(header_name) {
result.push(header_name);
}
}
result
}
#[inline(always)]
pub(crate) fn parse_header_name(&self, header_name: &Value) -> Option<HeaderName<'static>> {
let h_ = self.eval_value(header_name);
let h = h_.to_string();
match HeaderName::parse(h.as_ref())? {
HeaderName::Other(_) => HeaderName::Other(h.into_owned().into()),
hn => hn.into_owned(),
}
.into()
}
pub(crate) fn find_headers(
&self,
header_names: &[HeaderName],
index: Option<i32>,
any_child: bool,
mut visitor_fnc: impl FnMut(&Header, u32, usize) -> bool,
) -> bool {
let parts = [self.part];
let mut part_iter = SubpartIterator::new(self, &parts, any_child);
while let Some((part_id, message_part)) = part_iter.next() {
'outer: for header_name in header_names {
match index {
None => {
for (pos, header) in message_part
.headers
.iter()
.enumerate()
.filter(|(_, h)| &h.name == header_name)
{
if visitor_fnc(header, part_id, pos) {
return true;
}
}
}
Some(index) if index >= 0 => {
let mut header_count = 0;
for (pos, header) in message_part.headers.iter().enumerate() {
if &header.name == header_name {
header_count += 1;
if header_count == index {
if visitor_fnc(header, part_id, pos) {
return true;
}
continue 'outer;
}
}
}
}
Some(index) => {
let index = -index;
let mut header_count = 0;
for (pos, header) in message_part.headers.iter().enumerate().rev() {
if &header.name == header_name {
header_count += 1;
if header_count == index {
if visitor_fnc(header, part_id, pos) {
return true;
}
break;
}
}
}
}
}
}
}
false
}
#[allow(unused_assignments)]
pub(crate) fn find_header_values(
&self,
header: &Header,
mime_opts: &MimeOpts<Variable>,
mut visitor_fnc: impl FnMut(&str) -> bool,
) -> bool {
let mut raw_header = None;
let mut header_value_ = None;
let header_value = if header.offset_end != 0 {
&header.value
} else {
let value = if let HeaderValue::Text(text) = &header.value {
text.as_ref()
} else {
#[cfg(test)]
panic!("Unexpected value.");
#[cfg(not(test))]
return false;
};
if mime_opts == &MimeOpts::None {
return visitor_fnc(value);
} else {
raw_header = format!("{value}\n").into_bytes().into();
header_value_ = MessageStream::new(raw_header.as_ref().unwrap())
.parse_content_type()
.into();
header_value_.as_ref().unwrap()
}
};
match (mime_opts, header_value) {
(MimeOpts::None, HeaderValue::Text(text))
if matches!(
&header.name,
HeaderName::Subject
| HeaderName::Comments
| HeaderName::ContentDescription
| HeaderName::ContentLocation
| HeaderName::ContentTransferEncoding,
) =>
{
visitor_fnc(text.as_ref())
}
(MimeOpts::None, _) => {
if let HeaderValue::Text(text) = MessageStream::new(
self.message
.raw_message
.get(header.offset_start as usize..header.offset_end as usize)
.unwrap_or(b""),
)
.parse_unstructured()
{
visitor_fnc(text.as_ref())
} else {
visitor_fnc("")
}
}
(MimeOpts::Type, HeaderValue::ContentType(ct)) => visitor_fnc(ct.c_type.as_ref()),
(MimeOpts::Subtype, HeaderValue::ContentType(ct)) => {
visitor_fnc(ct.c_subtype.as_deref().unwrap_or(""))
}
(MimeOpts::ContentType, HeaderValue::ContentType(ct)) => {
if let Some(sub_type) = &ct.c_subtype {
visitor_fnc(&format!("{}/{}", ct.c_type, sub_type))
} else {
visitor_fnc(ct.c_type.as_ref())
}
}
(MimeOpts::Param(params), HeaderValue::ContentType(ct)) => {
if let Some(attributes) = &ct.attributes {
for param in params {
for attr in attributes {
if param.to_string().eq_ignore_ascii_case(&attr.name)
&& visitor_fnc(attr.value.as_ref())
{
return true;
}
}
}
}
visitor_fnc("")
}
_ => visitor_fnc(""),
}
}
}
+132
View File
@@ -0,0 +1,132 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
Context, Metadata,
compiler::{
Number,
grammar::{
MatchType,
tests::test_mailbox::{TestMetadata, TestMetadataExists},
},
},
};
use super::TestResult;
impl TestMetadata {
pub(crate) fn exec(&self, ctx: &mut Context) -> TestResult {
let metadata = match &self.medatata {
Metadata::Server { annotation } => Metadata::Server {
annotation: ctx.eval_value(annotation).to_string().into_owned(),
},
Metadata::Mailbox { name, annotation } => Metadata::Mailbox {
name: ctx.eval_value(name).to_string().into_owned(),
annotation: ctx.eval_value(annotation).to_string().into_owned(),
},
};
let value = if let Some((_, value)) = [&ctx.metadata, &ctx.runtime.metadata]
.into_iter()
.flatten()
.find(|(m, _)| match (m, &metadata) {
(Metadata::Server { annotation: a }, Metadata::Server { annotation: b }) => {
a.eq_ignore_ascii_case(b)
}
(
Metadata::Mailbox {
name: a,
annotation: c,
},
Metadata::Mailbox {
name: b,
annotation: d,
},
) => a.eq(b) && c.eq_ignore_ascii_case(d),
_ => false,
}) {
value.as_ref()
} else {
return TestResult::Bool(false ^ self.is_not);
};
let mut result = false;
if let MatchType::Count(match_type) = &self.match_type {
for key in &self.key_list {
if match_type.cmp(&Number::Float(1.0), &ctx.eval_value(key).to_number()) {
result = true;
break;
}
}
} else {
let mut captured_values = Vec::new();
for pattern in &self.key_list {
let key = ctx.eval_value(pattern);
result = match &self.match_type {
MatchType::Is => self.comparator.is(&value, &key),
MatchType::Contains => {
self.comparator.contains(value, key.to_string().as_ref())
}
MatchType::Value(relation) => {
self.comparator.relational(relation, &value, &key)
}
MatchType::Matches(capture_positions) => self.comparator.matches(
value,
key.to_string().as_ref(),
*capture_positions,
&mut captured_values,
),
MatchType::Regex(capture_positions) => self.comparator.regex(
pattern,
&key,
value,
*capture_positions,
&mut captured_values,
),
_ => false,
};
if result {
break;
}
}
if !captured_values.is_empty() {
ctx.set_match_variables(captured_values);
}
}
TestResult::Bool(result ^ self.is_not)
}
}
impl TestMetadataExists {
pub(crate) fn exec(&self, ctx: &Context) -> TestResult {
let mailbox = self
.mailbox
.as_ref()
.map(|s| ctx.eval_value(s).to_string().into_owned());
let mut annotations = ctx.eval_values(&self.annotation_names);
for (metadata, _) in [&ctx.metadata, &ctx.runtime.metadata].into_iter().flatten() {
match (metadata, mailbox.as_ref()) {
(Metadata::Server { annotation }, None) => {
annotations.retain(|a| !a.to_string().eq_ignore_ascii_case(annotation))
}
(Metadata::Mailbox { name, annotation }, Some(mailbox)) if name.eq(mailbox) => {
annotations.retain(|a| !a.to_string().eq_ignore_ascii_case(annotation));
}
_ => (),
}
if annotations.is_empty() {
return TestResult::Bool(true ^ self.is_not);
}
}
TestResult::Bool(false ^ self.is_not)
}
}
+99
View File
@@ -0,0 +1,99 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::borrow::Cow;
use crate::{
Context,
compiler::{
Number,
grammar::{
MatchType,
tests::test_notify::{TestNotifyMethodCapability, TestValidNotifyMethod},
},
},
runtime::actions::action_notify::validate_uri,
};
use super::TestResult;
impl TestValidNotifyMethod {
pub(crate) fn exec(&self, ctx: &mut Context) -> TestResult {
let mut num_valid = 0;
for uri in &self.notification_uris {
let uri_ = ctx.eval_value(uri);
let uri = uri_.to_string();
if let Some(scheme) = validate_uri(uri.as_ref())
&& (ctx
.runtime
.valid_notification_uris
.contains(&Cow::from(scheme))
|| ctx.runtime.valid_notification_uris.contains(&uri))
{
num_valid += 1;
}
}
TestResult::Bool((num_valid == self.notification_uris.len()) ^ self.is_not)
}
}
impl TestNotifyMethodCapability {
pub(crate) fn exec(&self, ctx: &mut Context) -> TestResult {
let uri_ = ctx.eval_value(&self.notification_uri);
let uri = uri_.to_string();
if !ctx
.eval_value(&self.notification_capability)
.to_string()
.eq_ignore_ascii_case("online")
|| !validate_uri(uri.as_ref()).is_some_and(|scheme| {
ctx.runtime
.valid_notification_uris
.contains(&Cow::from(scheme))
|| ctx.runtime.valid_notification_uris.contains(&uri)
})
{
return TestResult::Bool(false ^ self.is_not);
}
if let MatchType::Count(rel_match) = &self.match_type {
for key in &self.key_list {
if rel_match.cmp(&Number::from(1.0), &ctx.eval_value(key).to_number()) {
return TestResult::Bool(true ^ self.is_not);
}
}
} else {
for pattern in &self.key_list {
let key = ctx.eval_value(pattern);
if match &self.match_type {
MatchType::Is => self.comparator.is(&"maybe", &key),
MatchType::Contains => {
self.comparator.contains("maybe", key.to_string().as_ref())
}
MatchType::Value(relation) => {
self.comparator.relational(relation, &"maybe", &key)
}
MatchType::Matches(_) => self.comparator.matches(
"maybe",
key.to_string().as_ref(),
0,
&mut Vec::new(),
),
MatchType::Regex(_) => {
self.comparator
.regex(pattern, &key, "maybe", 0, &mut Vec::new())
}
_ => false,
} {
return TestResult::Bool(true ^ self.is_not);
}
}
}
TestResult::Bool(false ^ self.is_not)
}
}
+21
View File
@@ -0,0 +1,21 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{Context, compiler::grammar::tests::test_size::TestSize};
use super::TestResult;
impl TestSize {
pub(crate) fn exec(&self, ctx: &Context) -> TestResult {
TestResult::Bool(
(if self.over {
ctx.message_size > self.limit
} else {
ctx.message_size < self.limit
}) ^ self.is_not,
)
}
}
+199
View File
@@ -0,0 +1,199 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
Context, SpamStatus, VirusStatus,
compiler::{
Number,
grammar::{
MatchType,
tests::test_spamtest::{TestSpamTest, TestVirusTest},
},
},
runtime::Variable,
};
use super::TestResult;
impl TestSpamTest {
pub(crate) fn exec(&self, ctx: &mut Context) -> TestResult {
let status = if self.percent {
ctx.spam_status.as_percentage()
} else {
ctx.spam_status.as_number()
};
let value = ctx.eval_value(&self.value);
let mut captured_values = Vec::new();
let result = match &self.match_type {
MatchType::Is => self.comparator.is(&status, &value),
MatchType::Contains => self
.comparator
.contains(status.to_string().as_ref(), value.to_string().as_ref()),
MatchType::Value(rel_match) => self.comparator.relational(rel_match, &status, &value),
MatchType::Matches(capture_positions) => self.comparator.matches(
status.to_string().as_ref(),
value.to_string().as_ref(),
*capture_positions,
&mut captured_values,
),
MatchType::Regex(capture_positions) => self.comparator.regex(
&self.value,
&value,
status.to_string().as_ref(),
*capture_positions,
&mut captured_values,
),
MatchType::Count(rel_match) => rel_match.cmp(
&Number::from(if matches!(&ctx.spam_status, SpamStatus::Unknown) {
0.0
} else {
1.1
}),
&value.to_number(),
),
MatchType::List => false,
};
if !captured_values.is_empty() {
ctx.set_match_variables(captured_values);
}
TestResult::Bool(result ^ self.is_not)
}
}
impl TestVirusTest {
pub(crate) fn exec(&self, ctx: &mut Context) -> TestResult {
let status = ctx.virus_status.as_number();
let value = ctx.eval_value(&self.value);
let mut captured_values = Vec::new();
let result = match &self.match_type {
MatchType::Is => self.comparator.is(&status, &value),
MatchType::Contains => self
.comparator
.contains(status.to_string().as_ref(), value.to_string().as_ref()),
MatchType::Value(rel_match) => self.comparator.relational(rel_match, &status, &value),
MatchType::Matches(capture_positions) => self.comparator.matches(
status.to_string().as_ref(),
value.to_string().as_ref(),
*capture_positions,
&mut captured_values,
),
MatchType::Regex(capture_positions) => self.comparator.regex(
&self.value,
&value,
status.to_string().as_ref(),
*capture_positions,
&mut captured_values,
),
MatchType::Count(rel_match) => rel_match.cmp(
&Number::from(if matches!(&ctx.virus_status, VirusStatus::Unknown) {
0.0
} else {
1.1
}),
&value.to_number(),
),
MatchType::List => false,
};
if !captured_values.is_empty() {
ctx.set_match_variables(captured_values);
}
TestResult::Bool(result ^ self.is_not)
}
}
impl SpamStatus {
pub fn from_number(number: u32) -> Self {
match number {
1 => SpamStatus::Ham,
2..=9 => SpamStatus::MaybeSpam(number as f64 / 10.0),
10 => SpamStatus::Spam,
_ => SpamStatus::Unknown,
}
}
pub(crate) fn as_number(&self) -> Variable {
Variable::Integer(match self {
SpamStatus::Unknown => 0,
SpamStatus::Ham => 1,
SpamStatus::MaybeSpam(pct) => ((pct * 10.0) as i64).clamp(2, 9),
SpamStatus::Spam => 10,
})
}
pub(crate) fn as_percentage(&self) -> Variable {
Variable::Integer(match self {
SpamStatus::Unknown | SpamStatus::Ham => 0,
SpamStatus::MaybeSpam(pct) => ((pct * 100.0).ceil() as i64).clamp(1, 100),
SpamStatus::Spam => 100,
})
}
}
impl VirusStatus {
pub fn from_number(number: u32) -> Self {
match number {
1 => VirusStatus::Clean,
2 => VirusStatus::Replaced,
3 => VirusStatus::Cured,
4 => VirusStatus::MaybeVirus,
5 => VirusStatus::Virus,
_ => VirusStatus::Unknown,
}
}
pub(crate) fn as_number(&self) -> Variable {
Variable::Integer(match self {
VirusStatus::Unknown => 0,
VirusStatus::Clean => 1,
VirusStatus::Replaced => 2,
VirusStatus::Cured => 3,
VirusStatus::MaybeVirus => 4,
VirusStatus::Virus => 5,
})
}
}
impl From<u32> for SpamStatus {
fn from(number: u32) -> Self {
SpamStatus::from_number(number)
}
}
impl From<i32> for SpamStatus {
fn from(number: i32) -> Self {
SpamStatus::from_number(number as u32)
}
}
impl From<usize> for SpamStatus {
fn from(number: usize) -> Self {
SpamStatus::from_number(number as u32)
}
}
impl From<u32> for VirusStatus {
fn from(number: u32) -> Self {
VirusStatus::from_number(number)
}
}
impl From<i32> for VirusStatus {
fn from(number: i32) -> Self {
VirusStatus::from_number(number as u32)
}
}
impl From<usize> for VirusStatus {
fn from(number: usize) -> Self {
VirusStatus::from_number(number as u32)
}
}
+106
View File
@@ -0,0 +1,106 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
Context, Event,
compiler::{
Number,
grammar::{MatchType, tests::test_string::TestString},
},
};
use super::TestResult;
impl TestString {
pub(crate) fn exec(&self, ctx: &mut Context, empty_is_null: bool) -> TestResult {
let mut result = false;
match &self.match_type {
MatchType::Count(match_type) => {
let num_items = self
.source
.iter()
.filter(|x| !ctx.eval_value(x).is_empty())
.count() as i64;
if !empty_is_null || num_items > 0 {
for key in &self.key_list {
if match_type
.cmp(&Number::from(num_items), &ctx.eval_value(key).to_number())
{
result = true;
break;
}
}
}
}
MatchType::List => {
let mut values = Vec::with_capacity(self.source.len());
for source in &self.source {
let value = ctx.eval_value(source).to_string().into_owned();
if !value.is_empty() && !values.iter().any(|v: &String| v.eq(&value)) {
values.push(value);
}
}
if !values.is_empty() {
return TestResult::Event {
event: Event::ListContains {
lists: ctx.eval_values_owned(&self.key_list),
values,
match_as: self.comparator.as_match(),
},
is_not: self.is_not,
};
}
}
_ => {
let mut captured_values = Vec::new();
let sources = ctx.eval_values(&self.source);
for pattern in &self.key_list {
let key = ctx.eval_value(pattern);
for source in &sources {
if !empty_is_null || !source.is_empty() {
result = match &self.match_type {
MatchType::Is => self.comparator.is(source, &key),
MatchType::Contains => self.comparator.contains(
source.to_string().as_ref(),
key.to_string().as_ref(),
),
MatchType::Value(relation) => {
self.comparator.relational(relation, source, &key)
}
MatchType::Matches(capture_positions) => self.comparator.matches(
source.to_string().as_ref(),
key.to_string().as_ref(),
*capture_positions,
&mut captured_values,
),
MatchType::Regex(capture_positions) => self.comparator.regex(
pattern,
&key,
source.to_string().as_ref(),
*capture_positions,
&mut captured_values,
),
_ => false,
};
if result {
break;
}
}
}
}
if !captured_values.is_empty() {
ctx.set_match_variables(captured_values);
}
}
}
TestResult::Bool(result ^ self.is_not)
}
}
+35
View File
@@ -0,0 +1,35 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::Context;
use super::Variable;
impl Context<'_> {
pub(crate) fn set_match_variables(&mut self, set_vars: Vec<(usize, String)>) {
for (var_num, value) in set_vars {
if let Some(var) = self.vars_match.get_mut(var_num) {
*var = value.into();
} else {
debug_assert!(false, "Invalid match variable {var_num}");
}
}
}
pub(crate) fn clear_match_variables(&mut self, mut positions: u64) {
while positions != 0 {
let index = 63 - positions.leading_zeros();
positions ^= 1 << index;
if let Some(match_var) = self.vars_match.get_mut(index as usize) {
if !match_var.is_empty() {
*match_var = Variable::default();
}
} else {
debug_assert!(false, "Failed to clear match variable at index {index}.");
}
}
}
}