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.
This commit is contained in:
Vendored
+981
@@ -0,0 +1,981 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*
|
||||
* Modified by Coffey Labs in 2026 for INBUXA.
|
||||
*/
|
||||
|
||||
#![doc = include_str!("../README.md")]
|
||||
|
||||
use std::{borrow::Cow, sync::Arc, vec::IntoIter};
|
||||
|
||||
use ahash::{AHashMap, AHashSet};
|
||||
use compiler::grammar::{
|
||||
Capability,
|
||||
actions::action_redirect::{ByTime, Notify, Ret},
|
||||
instruction::Instruction,
|
||||
};
|
||||
use mail_parser::{HeaderName, Message};
|
||||
use runtime::{Variable, context::ScriptStack};
|
||||
|
||||
pub mod compiler;
|
||||
pub mod runtime;
|
||||
|
||||
pub(crate) const MAX_MATCH_VARIABLES: u32 = 63;
|
||||
pub(crate) const MAX_LOCAL_VARIABLES: u32 = 256;
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
#[cfg_attr(
|
||||
any(test, feature = "serde"),
|
||||
derive(serde::Serialize, serde::Deserialize)
|
||||
)]
|
||||
#[cfg_attr(
|
||||
feature = "rkyv",
|
||||
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
|
||||
)]
|
||||
pub struct Sieve {
|
||||
instructions: Vec<Instruction>,
|
||||
num_vars: u32,
|
||||
num_match_vars: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Compiler {
|
||||
// Settings
|
||||
pub(crate) max_script_size: usize,
|
||||
pub(crate) max_string_size: usize,
|
||||
pub(crate) max_variable_name_size: usize,
|
||||
pub(crate) max_nested_blocks: usize,
|
||||
pub(crate) max_nested_tests: usize,
|
||||
pub(crate) max_nested_foreverypart: usize,
|
||||
pub(crate) max_match_variables: usize,
|
||||
pub(crate) max_local_variables: usize,
|
||||
pub(crate) max_header_size: usize,
|
||||
pub(crate) max_includes: usize,
|
||||
pub(crate) no_capability_check: bool,
|
||||
|
||||
// Functions
|
||||
pub(crate) functions: AHashMap<String, (u32, u32)>,
|
||||
}
|
||||
|
||||
pub type Function = for<'x> fn(&'x Context<'x>, Vec<Variable>) -> Variable;
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
pub struct FunctionMap {
|
||||
pub(crate) map: AHashMap<String, (u32, u32)>,
|
||||
pub(crate) functions: Vec<Function>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Runtime {
|
||||
pub(crate) allowed_capabilities: AHashSet<Capability>,
|
||||
pub(crate) valid_notification_uris: AHashSet<Cow<'static, str>>,
|
||||
pub(crate) valid_ext_lists: AHashSet<Cow<'static, str>>,
|
||||
pub(crate) protected_headers: Vec<HeaderName<'static>>,
|
||||
pub(crate) environment: AHashMap<Cow<'static, str>, Variable>,
|
||||
pub(crate) metadata: Vec<(Metadata<String>, Cow<'static, str>)>,
|
||||
pub(crate) include_scripts: AHashMap<String, Arc<Sieve>>,
|
||||
pub(crate) local_hostname: Cow<'static, str>,
|
||||
pub(crate) functions: Vec<Function>,
|
||||
|
||||
pub(crate) max_nested_includes: usize,
|
||||
pub(crate) cpu_limit: usize,
|
||||
pub(crate) max_variable_size: usize,
|
||||
pub(crate) max_redirects: usize,
|
||||
pub(crate) max_received_headers: usize,
|
||||
pub(crate) max_header_size: usize,
|
||||
pub(crate) max_out_messages: usize,
|
||||
|
||||
pub(crate) default_vacation_expiry: u64,
|
||||
pub(crate) default_duplicate_expiry: u64,
|
||||
|
||||
pub(crate) vacation_use_orig_rcpt: bool,
|
||||
pub(crate) vacation_default_subject: Cow<'static, str>,
|
||||
pub(crate) vacation_subject_prefix: Cow<'static, str>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Context<'x> {
|
||||
#[cfg(test)]
|
||||
pub(crate) runtime: Runtime,
|
||||
#[cfg(not(test))]
|
||||
pub(crate) runtime: &'x Runtime,
|
||||
pub(crate) user_address: Cow<'x, str>,
|
||||
pub(crate) user_full_name: Cow<'x, str>,
|
||||
pub(crate) current_time: i64,
|
||||
|
||||
pub(crate) message: Message<'x>,
|
||||
pub(crate) message_size: usize,
|
||||
pub(crate) envelope: Vec<(Envelope, Variable)>,
|
||||
pub(crate) metadata: Vec<(Metadata<String>, Cow<'x, str>)>,
|
||||
|
||||
pub(crate) part: u32,
|
||||
pub(crate) part_iter: IntoIter<u32>,
|
||||
pub(crate) part_iter_stack: Vec<(u32, IntoIter<u32>)>,
|
||||
|
||||
pub(crate) spam_status: SpamStatus,
|
||||
pub(crate) virus_status: VirusStatus,
|
||||
|
||||
pub(crate) pos: usize,
|
||||
pub(crate) test_result: bool,
|
||||
pub(crate) script_cache: AHashMap<Script, Arc<Sieve>>,
|
||||
pub(crate) script_stack: Vec<ScriptStack>,
|
||||
pub(crate) vars_global: AHashMap<Cow<'static, str>, Variable>,
|
||||
pub(crate) vars_env: AHashMap<Cow<'static, str>, Variable>,
|
||||
pub(crate) vars_local: Vec<Variable>,
|
||||
pub(crate) vars_match: Vec<Variable>,
|
||||
pub(crate) expr_stack: Vec<Variable>,
|
||||
pub(crate) expr_pos: usize,
|
||||
|
||||
pub(crate) queued_events: IntoIter<Event>,
|
||||
pub(crate) final_event: Option<Event>,
|
||||
pub(crate) last_message_id: usize,
|
||||
pub(crate) main_message_id: usize,
|
||||
|
||||
pub(crate) has_changes: bool,
|
||||
pub(crate) num_redirects: usize,
|
||||
pub(crate) num_instructions: usize,
|
||||
pub(crate) num_out_messages: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
pub enum Script {
|
||||
Personal(String),
|
||||
Global(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
#[cfg_attr(
|
||||
any(test, feature = "serde"),
|
||||
derive(serde::Serialize, serde::Deserialize)
|
||||
)]
|
||||
#[cfg_attr(
|
||||
feature = "rkyv",
|
||||
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
|
||||
)]
|
||||
pub enum Envelope {
|
||||
From,
|
||||
To,
|
||||
ByTimeAbsolute,
|
||||
ByTimeRelative,
|
||||
ByMode,
|
||||
ByTrace,
|
||||
Notify,
|
||||
Orcpt,
|
||||
Ret,
|
||||
Envid,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
#[cfg_attr(
|
||||
any(test, feature = "serde"),
|
||||
derive(serde::Serialize, serde::Deserialize)
|
||||
)]
|
||||
#[cfg_attr(
|
||||
feature = "rkyv",
|
||||
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
|
||||
)]
|
||||
pub enum Metadata<T> {
|
||||
Server { annotation: T },
|
||||
Mailbox { name: T, annotation: T },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub enum Event {
|
||||
IncludeScript {
|
||||
name: Script,
|
||||
optional: bool,
|
||||
},
|
||||
MailboxExists {
|
||||
mailboxes: Vec<Mailbox>,
|
||||
special_use: Vec<String>,
|
||||
},
|
||||
ListContains {
|
||||
lists: Vec<String>,
|
||||
values: Vec<String>,
|
||||
match_as: MatchAs,
|
||||
},
|
||||
DuplicateId {
|
||||
id: String,
|
||||
expiry: u64,
|
||||
last: bool,
|
||||
},
|
||||
SetEnvelope {
|
||||
envelope: Envelope,
|
||||
value: String,
|
||||
},
|
||||
Function {
|
||||
id: ExternalId,
|
||||
arguments: Vec<Variable>,
|
||||
},
|
||||
|
||||
// Actions
|
||||
Keep {
|
||||
flags: Vec<String>,
|
||||
message_id: usize,
|
||||
},
|
||||
Discard,
|
||||
Reject {
|
||||
extended: bool,
|
||||
reason: String,
|
||||
},
|
||||
FileInto {
|
||||
folder: String,
|
||||
flags: Vec<String>,
|
||||
mailbox_id: Option<String>,
|
||||
special_use: Option<String>,
|
||||
create: bool,
|
||||
message_id: usize,
|
||||
},
|
||||
SendMessage {
|
||||
recipient: Recipient,
|
||||
notify: Notify,
|
||||
return_of_content: Ret,
|
||||
by_time: ByTime<i64>,
|
||||
message_id: usize,
|
||||
},
|
||||
Notify {
|
||||
from: Option<String>,
|
||||
importance: Importance,
|
||||
options: Vec<String>,
|
||||
message: String,
|
||||
method: String,
|
||||
},
|
||||
CreatedMessage {
|
||||
message_id: usize,
|
||||
message: Vec<u8>,
|
||||
},
|
||||
}
|
||||
|
||||
pub type ExternalId = u32;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
#[cfg_attr(
|
||||
any(test, feature = "serde"),
|
||||
derive(serde::Serialize, serde::Deserialize)
|
||||
)]
|
||||
#[cfg_attr(
|
||||
feature = "rkyv",
|
||||
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
|
||||
)]
|
||||
pub(crate) struct FileCarbonCopy<T> {
|
||||
pub mailbox: T,
|
||||
pub mailbox_id: Option<T>,
|
||||
pub create: bool,
|
||||
pub flags: Vec<T>,
|
||||
pub special_use: Option<T>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
pub enum Importance {
|
||||
High,
|
||||
Normal,
|
||||
Low,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||
pub enum MatchAs {
|
||||
Octet,
|
||||
Lowercase,
|
||||
Number,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
pub enum Recipient {
|
||||
Address(String),
|
||||
List(String),
|
||||
Group(Vec<String>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub enum Input {
|
||||
True,
|
||||
False,
|
||||
FncResult(Variable),
|
||||
Script { name: Script, script: Arc<Sieve> },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
pub enum Mailbox {
|
||||
Name(String),
|
||||
Id(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum SpamStatus {
|
||||
Unknown,
|
||||
Ham,
|
||||
MaybeSpam(f64),
|
||||
Spam,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
pub enum VirusStatus {
|
||||
Unknown,
|
||||
Clean,
|
||||
Replaced,
|
||||
Cured,
|
||||
MaybeVirus,
|
||||
Virus,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use ahash::{AHashMap, AHashSet};
|
||||
use mail_parser::{
|
||||
Encoding, HeaderValue, Message, MessageParser, MessagePart, PartType,
|
||||
parsers::MessageStream,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
Compiler, Context, Envelope, Event, FunctionMap, Input, Mailbox, Recipient, Runtime,
|
||||
SpamStatus, VirusStatus,
|
||||
compiler::grammar::Capability,
|
||||
runtime::{Variable, actions::action_mime::reset_test_boundary},
|
||||
};
|
||||
|
||||
impl Variable {
|
||||
pub fn unwrap_string(self) -> String {
|
||||
self.to_string().into_owned()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_suite() {
|
||||
let mut tests = Vec::new();
|
||||
let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
path.push("tests");
|
||||
|
||||
read_dir(path, &mut tests);
|
||||
|
||||
for test in tests {
|
||||
/*if !test
|
||||
.file_name()
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.contains("expressions")
|
||||
{
|
||||
continue;
|
||||
}*/
|
||||
println!("===== {} =====", test.display());
|
||||
run_test(&test);
|
||||
}
|
||||
}
|
||||
|
||||
fn read_dir(path: PathBuf, files: &mut Vec<PathBuf>) {
|
||||
for entry in fs::read_dir(path).unwrap() {
|
||||
let entry = entry.unwrap().path();
|
||||
if entry.is_dir() {
|
||||
read_dir(entry, files);
|
||||
} else if entry
|
||||
.extension()
|
||||
.and_then(|e| e.to_str())
|
||||
.unwrap_or("")
|
||||
.eq("svtest")
|
||||
{
|
||||
files.push(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn run_test(script_path: &Path) {
|
||||
let mut fnc_map = FunctionMap::new()
|
||||
.with_function("trim", |_, v| match v.into_iter().next().unwrap() {
|
||||
crate::runtime::Variable::String(s) => s.trim().to_string().into(),
|
||||
v => v.to_string().into(),
|
||||
})
|
||||
.with_function("len", |_, v| v[0].to_string().len().into())
|
||||
.with_function("count", |_, v| {
|
||||
v[0].as_array().map_or(0, |arr| arr.len()).into()
|
||||
})
|
||||
.with_function("to_lowercase", |_, v| {
|
||||
v[0].to_string().to_lowercase().to_string().into()
|
||||
})
|
||||
.with_function("to_uppercase", |_, v| {
|
||||
v[0].to_string().to_uppercase().to_string().into()
|
||||
})
|
||||
.with_function("is_uppercase", |_, v| {
|
||||
v[0].to_string()
|
||||
.as_ref()
|
||||
.chars()
|
||||
.filter(|c| c.is_alphabetic())
|
||||
.all(|c| c.is_uppercase())
|
||||
.into()
|
||||
})
|
||||
.with_function("is_ascii", |_, v| {
|
||||
v[0].to_string().as_ref().is_ascii().into()
|
||||
})
|
||||
.with_function("char_count", |_, v| {
|
||||
v[0].to_string().as_ref().chars().count().into()
|
||||
})
|
||||
.with_function("lines", |_, v| {
|
||||
v[0].to_string()
|
||||
.lines()
|
||||
.map(|line| Variable::from(line.to_string()))
|
||||
.collect::<Vec<_>>()
|
||||
.into()
|
||||
})
|
||||
.with_function_args(
|
||||
"contains",
|
||||
|_, v| v[0].to_string().contains(v[1].to_string().as_ref()).into(),
|
||||
2,
|
||||
)
|
||||
.with_function_args(
|
||||
"eq_lowercase",
|
||||
|_, v| {
|
||||
v[0].to_string()
|
||||
.as_ref()
|
||||
.eq_ignore_ascii_case(v[1].to_string().as_ref())
|
||||
.into()
|
||||
},
|
||||
2,
|
||||
)
|
||||
.with_function_args(
|
||||
"concat_three",
|
||||
|_, v| format!("{}-{}-{}", v[0], v[1], v[2]).into(),
|
||||
3,
|
||||
)
|
||||
.with_function_args(
|
||||
"in_array",
|
||||
|_, v| {
|
||||
v[0].as_array()
|
||||
.is_some_and(|arr| arr.contains(&v[1]))
|
||||
.into()
|
||||
},
|
||||
2,
|
||||
)
|
||||
.with_external_function("ext_zero", 0, 0)
|
||||
.with_external_function("ext_one", 1, 1)
|
||||
.with_external_function("ext_two", 2, 2)
|
||||
.with_external_function("ext_three", 3, 3)
|
||||
.with_external_function("ext_true", 4, 0)
|
||||
.with_external_function("ext_false", 5, 0);
|
||||
let mut compiler = Compiler::new()
|
||||
.with_max_string_size(10240)
|
||||
.register_functions(&mut fnc_map);
|
||||
|
||||
let mut ancestors = script_path.ancestors();
|
||||
ancestors.next();
|
||||
let base_path = ancestors.next().unwrap();
|
||||
let script = compiler
|
||||
.compile(&add_crlf(&fs::read(script_path).unwrap()))
|
||||
.unwrap();
|
||||
|
||||
let mut input = Input::script("", script);
|
||||
let mut current_test = String::new();
|
||||
let mut raw_message_: Option<Vec<u8>> = None;
|
||||
let mut prev_state = None;
|
||||
let mut mailboxes = Vec::new();
|
||||
let mut lists: AHashMap<String, AHashSet<String>> = AHashMap::new();
|
||||
let mut duplicated_ids = AHashSet::new();
|
||||
let mut actions = Vec::new();
|
||||
|
||||
'outer: loop {
|
||||
let runtime = Runtime::new()
|
||||
.with_protected_header("Auto-Submitted")
|
||||
.with_protected_header("Received")
|
||||
.with_valid_notification_uri("mailto")
|
||||
.with_max_out_messages(100)
|
||||
.with_capability(Capability::While)
|
||||
.with_capability(Capability::Expressions)
|
||||
.with_functions(&mut fnc_map.clone());
|
||||
let mut instance = Context::new(
|
||||
&runtime,
|
||||
Message {
|
||||
parts: vec![MessagePart {
|
||||
headers: vec![],
|
||||
is_encoding_problem: false,
|
||||
body: PartType::Text("".into()),
|
||||
encoding: Encoding::None,
|
||||
offset_header: 0,
|
||||
offset_body: 0,
|
||||
offset_end: 0,
|
||||
}],
|
||||
raw_message: b""[..].into(),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
let raw_message = raw_message_.take().unwrap_or_default();
|
||||
instance.message =
|
||||
MessageParser::new()
|
||||
.parse(&raw_message)
|
||||
.unwrap_or_else(|| Message {
|
||||
html_body: vec![],
|
||||
text_body: vec![],
|
||||
attachments: vec![],
|
||||
parts: vec![MessagePart {
|
||||
headers: vec![],
|
||||
is_encoding_problem: false,
|
||||
body: PartType::Text("".into()),
|
||||
encoding: Encoding::None,
|
||||
offset_header: 0,
|
||||
offset_body: 0,
|
||||
offset_end: 0,
|
||||
}],
|
||||
raw_message: b""[..].into(),
|
||||
});
|
||||
instance.message_size = raw_message.len();
|
||||
if let Some((pos, script_cache, script_stack, vars_global, vars_local, vars_match)) =
|
||||
prev_state.take()
|
||||
{
|
||||
instance.pos = pos;
|
||||
instance.script_cache = script_cache;
|
||||
instance.script_stack = script_stack;
|
||||
instance.vars_global = vars_global;
|
||||
instance.vars_local = vars_local;
|
||||
instance.vars_match = vars_match;
|
||||
}
|
||||
instance.set_env_variable("vnd.inbuxa.default_mailbox", "INBOX");
|
||||
instance.set_env_variable("vnd.inbuxa.username", "john.doe");
|
||||
instance.set_user_address("MAILER-DAEMON");
|
||||
if let Some(addr) = instance
|
||||
.message
|
||||
.from()
|
||||
.and_then(|a| a.first())
|
||||
.and_then(|a| a.address.as_ref())
|
||||
{
|
||||
instance.set_envelope(Envelope::From, addr.to_string());
|
||||
}
|
||||
if let Some(addr) = instance
|
||||
.message
|
||||
.to()
|
||||
.and_then(|a| a.first())
|
||||
.and_then(|a| a.address.as_ref())
|
||||
{
|
||||
instance.set_envelope(Envelope::To, addr.to_string());
|
||||
}
|
||||
|
||||
while let Some(event) = instance.run(input) {
|
||||
match event.unwrap() {
|
||||
Event::IncludeScript { name, optional } => {
|
||||
let mut include_path = PathBuf::from(base_path);
|
||||
include_path.push(if matches!(name, crate::Script::Personal(_)) {
|
||||
"included"
|
||||
} else {
|
||||
"included-global"
|
||||
});
|
||||
include_path.push(format!("{name}.sieve"));
|
||||
|
||||
if let Ok(bytes) = fs::read(include_path.as_path()) {
|
||||
let script = compiler.compile(&add_crlf(&bytes)).unwrap();
|
||||
input = Input::script(name, script);
|
||||
} else if optional {
|
||||
input = Input::False;
|
||||
} else {
|
||||
panic!("Script {} not found.", include_path.display());
|
||||
}
|
||||
}
|
||||
Event::MailboxExists {
|
||||
mailboxes: mailboxes_,
|
||||
special_use,
|
||||
} => {
|
||||
for action in &actions {
|
||||
if let Event::FileInto { folder, create, .. } = action
|
||||
&& *create
|
||||
&& !mailboxes.contains(folder)
|
||||
{
|
||||
mailboxes.push(folder.to_string());
|
||||
}
|
||||
}
|
||||
input = (special_use.is_empty()
|
||||
&& mailboxes_.iter().all(|n| {
|
||||
if let Mailbox::Name(n) = n {
|
||||
mailboxes.contains(n)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}))
|
||||
.into();
|
||||
}
|
||||
Event::ListContains {
|
||||
lists: lists_,
|
||||
values,
|
||||
..
|
||||
} => {
|
||||
let mut result = false;
|
||||
'list: for list in &lists_ {
|
||||
if let Some(list) = lists.get(list) {
|
||||
for value in &values {
|
||||
if list.contains(value) {
|
||||
result = true;
|
||||
break 'list;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
input = result.into();
|
||||
}
|
||||
Event::DuplicateId { id, .. } => {
|
||||
input = duplicated_ids.contains(&id).into();
|
||||
}
|
||||
Event::Function { id, arguments } => {
|
||||
if id == u32::MAX {
|
||||
// Test functions
|
||||
input = Input::True;
|
||||
let mut arguments = arguments.into_iter();
|
||||
let command = arguments.next().unwrap().unwrap_string();
|
||||
let mut params =
|
||||
arguments.map(|arg| arg.unwrap_string()).collect::<Vec<_>>();
|
||||
|
||||
match command.as_str() {
|
||||
"test" => {
|
||||
current_test = params.pop().unwrap();
|
||||
println!("Running test '{current_test}'...");
|
||||
}
|
||||
"test_set" => {
|
||||
let mut params = params.into_iter();
|
||||
let target = params.next().expect("test_set parameter");
|
||||
if target == "message" {
|
||||
let value = params.next().unwrap();
|
||||
raw_message_ = if value.eq_ignore_ascii_case(":smtp") {
|
||||
let mut message = None;
|
||||
for action in actions.iter().rev() {
|
||||
if let Event::SendMessage { message_id, .. } =
|
||||
action
|
||||
{
|
||||
let message_ = actions
|
||||
.iter()
|
||||
.find_map(|item| {
|
||||
if let Event::CreatedMessage {
|
||||
message_id: message_id_,
|
||||
message,
|
||||
} = item
|
||||
&& message_id == message_id_
|
||||
{
|
||||
return Some(message);
|
||||
}
|
||||
None
|
||||
})
|
||||
.unwrap();
|
||||
/*println!(
|
||||
"<[{}]>",
|
||||
std::str::from_utf8(message_).unwrap()
|
||||
);*/
|
||||
message = message_.into();
|
||||
break;
|
||||
}
|
||||
}
|
||||
message.expect("No SMTP message found").to_vec().into()
|
||||
} else {
|
||||
value.into_bytes().into()
|
||||
};
|
||||
prev_state = (
|
||||
instance.pos,
|
||||
instance.script_cache,
|
||||
instance.script_stack,
|
||||
instance.vars_global,
|
||||
instance.vars_local,
|
||||
instance.vars_match,
|
||||
)
|
||||
.into();
|
||||
|
||||
continue 'outer;
|
||||
} else if let Some(envelope) = target.strip_prefix("envelope.")
|
||||
{
|
||||
let envelope =
|
||||
Envelope::try_from(envelope.to_string()).unwrap();
|
||||
instance.envelope.retain(|(e, _)| e != &envelope);
|
||||
instance.set_envelope(envelope, params.next().unwrap());
|
||||
} else if target == "currentdate" {
|
||||
let bytes = params.next().unwrap().into_bytes();
|
||||
if let HeaderValue::DateTime(dt) =
|
||||
MessageStream::new(&bytes).parse_date()
|
||||
{
|
||||
instance.current_time = dt.to_timestamp();
|
||||
} else {
|
||||
panic!("Invalid currentdate");
|
||||
}
|
||||
} else {
|
||||
panic!("test_set {target} not implemented.");
|
||||
}
|
||||
}
|
||||
"test_message" => {
|
||||
let mut params = params.into_iter();
|
||||
input = match params.next().unwrap().as_str() {
|
||||
":folder" => {
|
||||
let folder_name = params.next().expect("test_message folder name");
|
||||
matches!(&instance.final_event, Some(Event::Keep { .. })) ||
|
||||
actions.iter().any(|a| if !folder_name.eq_ignore_ascii_case("INBOX") {
|
||||
matches!(a, Event::FileInto { folder, .. } if folder == &folder_name )
|
||||
} else {
|
||||
matches!(a, Event::Keep { .. })
|
||||
})
|
||||
}
|
||||
":smtp" => {
|
||||
actions.iter().any(|a| matches!(a, Event::SendMessage { .. } ))
|
||||
}
|
||||
param => panic!("Invalid test_message param '{param}'" ),
|
||||
}.into();
|
||||
}
|
||||
"test_assert_message" => {
|
||||
let expected_message =
|
||||
params.first().expect("test_set parameter");
|
||||
let built_message = instance.build_message();
|
||||
if expected_message.as_bytes() != built_message {
|
||||
//fs::write("_deleteme.json", serde_json::to_string_pretty(&Message::parse(&built_message).unwrap()).unwrap()).unwrap();
|
||||
print!("<[");
|
||||
print!("{}", String::from_utf8(built_message).unwrap());
|
||||
println!("]>");
|
||||
panic!("Message built incorrectly at '{current_test}'");
|
||||
}
|
||||
}
|
||||
"test_config_set" => {
|
||||
let mut params = params.into_iter();
|
||||
let name = params.next().unwrap();
|
||||
let value = params.next().expect("test_config_set value");
|
||||
|
||||
match name.as_str() {
|
||||
"sieve_editheader_protected"
|
||||
| "sieve_editheader_forbid_add"
|
||||
| "sieve_editheader_forbid_delete" => {
|
||||
if !value.is_empty() {
|
||||
for header_name in value.split(' ') {
|
||||
instance.runtime.set_protected_header(
|
||||
header_name.to_string(),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
instance.runtime.protected_headers.clear();
|
||||
}
|
||||
}
|
||||
"sieve_variables_max_variable_size" => {
|
||||
instance
|
||||
.runtime
|
||||
.set_max_variable_size(value.parse().unwrap());
|
||||
}
|
||||
"sieve_valid_ext_list" => {
|
||||
instance.runtime.set_valid_ext_list(value);
|
||||
}
|
||||
"sieve_ext_list_item" => {
|
||||
lists
|
||||
.entry(value)
|
||||
.or_default()
|
||||
.insert(params.next().expect("list item value"));
|
||||
}
|
||||
"sieve_duplicated_id" => {
|
||||
duplicated_ids.insert(value);
|
||||
}
|
||||
"sieve_user_email" => {
|
||||
instance.set_user_address(value);
|
||||
}
|
||||
"sieve_vacation_use_original_recipient" => {
|
||||
instance.runtime.set_vacation_use_orig_rcpt(
|
||||
value.eq_ignore_ascii_case("yes"),
|
||||
);
|
||||
}
|
||||
"sieve_vacation_default_subject" => {
|
||||
instance.runtime.set_vacation_default_subject(value);
|
||||
}
|
||||
"sieve_vacation_default_subject_template" => {
|
||||
instance.runtime.set_vacation_subject_prefix(value);
|
||||
}
|
||||
"sieve_spam_status" => {
|
||||
instance.set_spam_status(SpamStatus::from_number(
|
||||
value.parse().unwrap(),
|
||||
));
|
||||
}
|
||||
"sieve_spam_status_plus" => {
|
||||
instance.set_spam_status(
|
||||
match value.parse::<u32>().unwrap() {
|
||||
0 => SpamStatus::Unknown,
|
||||
100.. => SpamStatus::Spam,
|
||||
n => SpamStatus::MaybeSpam((n as f64) / 100.0),
|
||||
},
|
||||
);
|
||||
}
|
||||
"sieve_virus_status" => {
|
||||
instance.set_virus_status(VirusStatus::from_number(
|
||||
value.parse().unwrap(),
|
||||
));
|
||||
}
|
||||
"sieve_editheader_max_header_size" => {
|
||||
let mhs = if !value.is_empty() {
|
||||
value.parse::<usize>().unwrap()
|
||||
} else {
|
||||
1024
|
||||
};
|
||||
instance.runtime.set_max_header_size(mhs);
|
||||
compiler.set_max_header_size(mhs);
|
||||
}
|
||||
"sieve_include_max_includes" => {
|
||||
compiler.set_max_includes(if !value.is_empty() {
|
||||
value.parse::<usize>().unwrap()
|
||||
} else {
|
||||
3
|
||||
});
|
||||
}
|
||||
"sieve_include_max_nesting_depth" => {
|
||||
compiler.set_max_nested_blocks(if !value.is_empty() {
|
||||
value.parse::<usize>().unwrap()
|
||||
} else {
|
||||
3
|
||||
});
|
||||
}
|
||||
param => panic!("Invalid test_config_set param '{param}'"),
|
||||
}
|
||||
}
|
||||
"test_result_execute" => {
|
||||
input =
|
||||
(matches!(&instance.final_event, Some(Event::Keep { .. }))
|
||||
|| actions.iter().any(|a| {
|
||||
matches!(
|
||||
a,
|
||||
Event::Keep { .. }
|
||||
| Event::FileInto { .. }
|
||||
| Event::SendMessage { .. }
|
||||
)
|
||||
}))
|
||||
.into();
|
||||
}
|
||||
"test_result_action" => {
|
||||
let param =
|
||||
params.first().expect("test_result_action parameter");
|
||||
input = if param == "reject" {
|
||||
(actions.iter().any(|a| matches!(a, Event::Reject { .. })))
|
||||
.into()
|
||||
} else if param == "redirect" {
|
||||
let param = params
|
||||
.last()
|
||||
.expect("test_result_action redirect address");
|
||||
(actions
|
||||
.iter()
|
||||
.any(|a| matches!(a, Event::SendMessage { recipient: Recipient::Address(address), .. } if address == param)))
|
||||
.into()
|
||||
} else if param == "keep" {
|
||||
(matches!(&instance.final_event, Some(Event::Keep { .. }))
|
||||
|| actions
|
||||
.iter()
|
||||
.any(|a| matches!(a, Event::Keep { .. })))
|
||||
.into()
|
||||
} else if param == "send_message" {
|
||||
(actions
|
||||
.iter()
|
||||
.any(|a| matches!(a, Event::SendMessage { .. })))
|
||||
.into()
|
||||
} else {
|
||||
panic!("test_result_action {param} not implemented");
|
||||
};
|
||||
}
|
||||
"test_result_action_count" => {
|
||||
input = (actions.len()
|
||||
== params.first().unwrap().parse::<usize>().unwrap())
|
||||
.into();
|
||||
}
|
||||
"test_imap_metadata_set" => {
|
||||
let mut params = params.into_iter();
|
||||
let first = params.next().expect("metadata parameter");
|
||||
let (mailbox, annotation) = if first == ":mailbox" {
|
||||
(
|
||||
params.next().expect("metadata mailbox name").into(),
|
||||
params.next().expect("metadata annotation name"),
|
||||
)
|
||||
} else {
|
||||
(None, first)
|
||||
};
|
||||
let value = params.next().expect("metadata value");
|
||||
if let Some(mailbox) = mailbox {
|
||||
instance.set_medatata((mailbox, annotation), value);
|
||||
} else {
|
||||
instance.set_medatata(annotation, value);
|
||||
}
|
||||
}
|
||||
"test_mailbox_create" => {
|
||||
mailboxes.push(params.pop().expect("mailbox to create"));
|
||||
}
|
||||
"test_result_reset" => {
|
||||
actions.clear();
|
||||
instance.final_event = Event::Keep {
|
||||
flags: vec![],
|
||||
message_id: 0,
|
||||
}
|
||||
.into();
|
||||
instance.metadata.clear();
|
||||
instance.has_changes = false;
|
||||
instance.num_redirects = 0;
|
||||
instance.runtime.vacation_use_orig_rcpt = false;
|
||||
mailboxes.clear();
|
||||
lists.clear();
|
||||
reset_test_boundary();
|
||||
}
|
||||
"test_script_compile" => {
|
||||
let mut include_path = PathBuf::from(base_path);
|
||||
include_path.push(params.first().unwrap());
|
||||
|
||||
if let Ok(bytes) = fs::read(include_path.as_path()) {
|
||||
let result = compiler.compile(&add_crlf(&bytes));
|
||||
/*if let Err(err) = &result {
|
||||
println!("Error: {:?}", err);
|
||||
}*/
|
||||
input = result.is_ok().into();
|
||||
} else {
|
||||
panic!("Script {} not found.", include_path.display());
|
||||
}
|
||||
}
|
||||
"test_config_reload" => (),
|
||||
"test_fail" => {
|
||||
panic!(
|
||||
"Test '{}' failed: {}",
|
||||
current_test,
|
||||
params.pop().unwrap()
|
||||
);
|
||||
}
|
||||
_ => panic!("Test command {command} not implemented."),
|
||||
}
|
||||
} else {
|
||||
let result = match id {
|
||||
0 => Variable::from("my_value"),
|
||||
1 => Variable::from(arguments[0].to_string().to_uppercase()),
|
||||
2 => Variable::from(format!(
|
||||
"{}-{}",
|
||||
arguments[0].to_string(),
|
||||
arguments[1].to_string()
|
||||
)),
|
||||
3 => Variable::from(format!(
|
||||
"{}-{}-{}",
|
||||
arguments[0].to_string(),
|
||||
arguments[1].to_string(),
|
||||
arguments[2].to_string()
|
||||
)),
|
||||
4 => true.into(),
|
||||
5 => false.into(),
|
||||
_ => {
|
||||
panic!("Unknown external function {id}");
|
||||
}
|
||||
};
|
||||
|
||||
input = result.into();
|
||||
}
|
||||
}
|
||||
|
||||
action => {
|
||||
actions.push(action);
|
||||
input = true.into();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
fn add_crlf(bytes: &[u8]) -> Vec<u8> {
|
||||
let mut result = Vec::with_capacity(bytes.len());
|
||||
let mut last_ch = 0;
|
||||
for &ch in bytes {
|
||||
if ch == b'\n' && last_ch != b'\r' {
|
||||
result.push(b'\r');
|
||||
}
|
||||
result.push(ch);
|
||||
last_ch = ch;
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user