Files
inbuxa-server/vendor/sieve-rs/src/compiler/grammar/test.rs
T
jcoffey-dev cc6f1eb298
ci / fork-checks (pull_request) Successful in 16s
ci / build (pull_request) Successful in 7m53s
Rename the identifiers that carried the upstream name
Everything clients, users and operators meet now carries the fork's name,
with no aliases (SPEC.md §2.4, changed here from "protocol identifiers
stay"):

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

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

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

Tested: the server builds without warnings; common's 106 unit tests,
including the vendoring check; a new integration test for the two
start-up migrations; and the webdav, jmap, imap and SMTP Sieve suites.
2026-09-22 19:33:02 -07:00

687 lines
25 KiB
Rust

/*
* 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
}
}