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()
);
}
}