mirror of https://git.meli.delivery/meli/meli
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
704 lines
25 KiB
Rust
704 lines
25 KiB
Rust
/*
|
|
* Copyright (c) 2020-2023, Stalwart Labs Ltd.
|
|
*
|
|
* This file is part of the Stalwart Sieve Interpreter.
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Affero General Public License as
|
|
* published by the Free Software Foundation, either version 3 of
|
|
* the License, or (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Affero General Public License for more details.
|
|
* in the LICENSE file at the top-level directory of this distribution.
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*
|
|
* You can be released from the requirements of the AGPLv3 license by
|
|
* purchasing a commercial license. Please contact licensing@stalw.art
|
|
* for more details.
|
|
*/
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
use crate::sieve::compiler::{
|
|
lexer::{tokenizer::TokenInfo, word::Word, Token},
|
|
CompileError, ErrorType,
|
|
};
|
|
|
|
use super::{
|
|
actions::{action_convert::Convert, action_vacation::TestVacation},
|
|
expr::{parser::ExpressionParser, tokenizer::Tokenizer, Expression},
|
|
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_plugin::Plugin,
|
|
test_size::TestSize,
|
|
test_spamtest::{TestSpamTest, TestVirusTest},
|
|
test_specialuse::TestSpecialUseExists,
|
|
test_string::TestString,
|
|
},
|
|
Capability, Invalid,
|
|
};
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
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),
|
|
|
|
// Stalwart proprietary
|
|
EvalExpression(EvalExpression),
|
|
Plugin(Plugin),
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub(crate) struct EvalExpression {
|
|
pub expr: Vec<Expression>,
|
|
pub is_not: bool,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct Block {
|
|
is_all: bool,
|
|
is_not: bool,
|
|
p_count: u32,
|
|
jmps: Vec<usize>,
|
|
}
|
|
|
|
impl<'x> CompilerState<'x> {
|
|
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 = match token_info.token {
|
|
Token::Comma
|
|
if !block_stack.is_empty()
|
|
&& matches!(self.instructions.last(), Some(Instruction::Test(_)))
|
|
&& 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
|
|
}
|
|
}
|
|
Token::Identifier(Word::False) => {
|
|
if !is_not {
|
|
Test::False
|
|
} else {
|
|
is_not = false;
|
|
Test::True
|
|
}
|
|
}
|
|
Token::Identifier(Word::Address) => self.parse_test_address()?,
|
|
Token::Identifier(Word::Envelope) => {
|
|
self.validate_argument(
|
|
0,
|
|
Capability::Envelope.into(),
|
|
token_info.line_num,
|
|
token_info.line_pos,
|
|
)?;
|
|
self.parse_test_envelope()?
|
|
}
|
|
Token::Identifier(Word::Header) => self.parse_test_header()?,
|
|
Token::Identifier(Word::Size) => self.parse_test_size()?,
|
|
Token::Identifier(Word::Exists) => self.parse_test_exists()?,
|
|
|
|
// 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()?
|
|
}
|
|
|
|
// 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()?
|
|
}
|
|
|
|
// 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()?
|
|
}
|
|
Token::Identifier(Word::CurrentDate) => {
|
|
self.validate_argument(
|
|
0,
|
|
Capability::Date.into(),
|
|
token_info.line_num,
|
|
token_info.line_pos,
|
|
)?;
|
|
self.parse_test_currentdate()?
|
|
}
|
|
|
|
// 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()?
|
|
}
|
|
|
|
// 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()?
|
|
}
|
|
|
|
// 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()?
|
|
}
|
|
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()?
|
|
}
|
|
|
|
// 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()?
|
|
}
|
|
|
|
// 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()?
|
|
}
|
|
|
|
// 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()?
|
|
}
|
|
|
|
// 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()?
|
|
}
|
|
|
|
// 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()?
|
|
}
|
|
Token::Identifier(Word::Metadata) => {
|
|
self.validate_argument(
|
|
0,
|
|
Capability::MboxMetadata.into(),
|
|
token_info.line_num,
|
|
token_info.line_pos,
|
|
)?;
|
|
self.parse_test_metadata()?
|
|
}
|
|
Token::Identifier(Word::MetadataExists) => {
|
|
self.validate_argument(
|
|
0,
|
|
Capability::MboxMetadata.into(),
|
|
token_info.line_num,
|
|
token_info.line_pos,
|
|
)?;
|
|
self.parse_test_metadataexists()?
|
|
}
|
|
Token::Identifier(Word::ServerMetadata) => {
|
|
self.validate_argument(
|
|
0,
|
|
Capability::ServerMetadata.into(),
|
|
token_info.line_num,
|
|
token_info.line_pos,
|
|
)?;
|
|
self.parse_test_servermetadata()?
|
|
}
|
|
Token::Identifier(Word::ServerMetadataExists) => {
|
|
self.validate_argument(
|
|
0,
|
|
Capability::ServerMetadata.into(),
|
|
token_info.line_num,
|
|
token_info.line_pos,
|
|
)?;
|
|
self.parse_test_servermetadataexists()?
|
|
}
|
|
|
|
// 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()?
|
|
}
|
|
|
|
// 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()?
|
|
}
|
|
Token::Identifier(Word::VirusTest) => {
|
|
self.validate_argument(
|
|
0,
|
|
Capability::VirusTest.into(),
|
|
token_info.line_num,
|
|
token_info.line_pos,
|
|
)?;
|
|
self.parse_test_virustest()?
|
|
}
|
|
|
|
// 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()?
|
|
}
|
|
Token::Identifier(Word::Eval) => {
|
|
self.validate_argument(
|
|
0,
|
|
Capability::Eval.into(),
|
|
token_info.line_num,
|
|
token_info.line_pos,
|
|
)?;
|
|
|
|
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) => Test::EvalExpression(EvalExpression {
|
|
expr: parser.output,
|
|
is_not: false,
|
|
}),
|
|
Err(err) => {
|
|
let err = ErrorType::InvalidExpression(format!(
|
|
"{}: {}",
|
|
std::str::from_utf8(&expr).unwrap_or_default(),
|
|
err
|
|
));
|
|
next_token.token = Token::StringVariable(expr);
|
|
return Err(next_token.custom(err));
|
|
}
|
|
}
|
|
}
|
|
|
|
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,
|
|
})
|
|
}
|
|
#[cfg(test)]
|
|
Token::Unknown(name) if name.contains("test") => {
|
|
use crate::sieve::compiler::Value;
|
|
|
|
let mut arguments = Vec::new();
|
|
arguments.push(crate::sieve::PluginArgument::Text(Value::Text(name)));
|
|
while !matches!(
|
|
self.tokens.peek().map(|r| r.map(|t| &t.token)),
|
|
Some(Ok(Token::Comma
|
|
| Token::ParenthesisClose
|
|
| Token::CurlyOpen))
|
|
) {
|
|
arguments.push(crate::sieve::PluginArgument::Text(
|
|
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::sieve::compiler::Number::Integer(n as i64))
|
|
}
|
|
Token::Identifier(s) => Value::Text(s.to_string()),
|
|
Token::Tag(s) => Value::Text(format!(":{s}")),
|
|
Token::Unknown(s) => Value::Text(s),
|
|
other => panic!("Invalid test param {other:?}"),
|
|
},
|
|
));
|
|
}
|
|
Test::Plugin(Plugin {
|
|
id: u32::MAX,
|
|
arguments,
|
|
is_not: false,
|
|
})
|
|
}
|
|
Token::Unknown(name) => {
|
|
if let Some(schema) = self.compiler.plugins.get(&name) {
|
|
self.validate_argument(
|
|
0,
|
|
Capability::Plugins.into(),
|
|
token_info.line_num,
|
|
token_info.line_pos,
|
|
)?;
|
|
self.parse_test_plugin(schema)?
|
|
} else {
|
|
self.ignore_test()?;
|
|
Test::Invalid(Invalid {
|
|
name,
|
|
line_num: token_info.line_num,
|
|
line_pos: token_info.line_pos,
|
|
})
|
|
}
|
|
}
|
|
_ => 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(Instruction::Test(if !is_not {
|
|
test
|
|
} else {
|
|
test.set_not()
|
|
}));
|
|
|
|
if block_stack.is_empty() {
|
|
break;
|
|
}
|
|
}
|
|
|
|
self.instructions.push(Instruction::Jz(usize::MAX));
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl Test {
|
|
pub fn set_not(mut self) -> Self {
|
|
match &mut self {
|
|
Test::True => return Test::False,
|
|
Test::False => return 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;
|
|
}
|
|
Test::Plugin(op) => {
|
|
op.is_not = true;
|
|
}
|
|
Test::EvalExpression(op) => {
|
|
op.is_not = true;
|
|
}
|
|
Test::Vacation(_) | Test::Invalid(_) => {}
|
|
}
|
|
self
|
|
}
|
|
}
|