1#![doc = include_str!("../README.md")]
8
9use ahash::{AHashMap, AHashSet};
10use compiler::grammar::{
11 Capability,
12 actions::action_redirect::{ByTime, Notify, Ret},
13 instruction::Instruction,
14};
15use mail_parser::{HeaderName, Message};
16use runtime::{Variable, context::ScriptStack};
17use std::{borrow::Cow, sync::Arc, vec::IntoIter};
18
19pub mod compiler;
20pub mod runtime;
21pub(crate) mod serialize;
22
23#[cfg(feature = "rkyv")]
24pub use serialize::ArchiveError;
25
26pub(crate) const MAX_MATCH_VARIABLES: u32 = 63;
27pub(crate) const MAX_LOCAL_VARIABLES: u32 = 256;
28
29#[derive(Debug, Clone, Eq, PartialEq, Default)]
30#[cfg_attr(
31 any(test, feature = "serde"),
32 derive(serde::Serialize, serde::Deserialize)
33)]
34#[cfg_attr(
35 feature = "rkyv",
36 derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
37)]
38pub struct Sieve {
39 instructions: Box<[Instruction]>,
40 #[cfg_attr(
41 any(test, feature = "serde"),
42 serde(with = "crate::serialize::as_string_vec_serde")
43 )]
44 #[cfg_attr(feature = "rkyv", rkyv(with = crate::serialize::AsStringVec))]
45 constants: Arc<[Arc<str>]>,
46 num_vars: u32,
47 num_match_vars: u32,
48}
49
50#[derive(Clone)]
51pub struct Compiler {
52 pub(crate) max_script_size: usize,
54 pub(crate) max_string_size: usize,
55 pub(crate) max_variable_name_size: usize,
56 pub(crate) max_nested_blocks: usize,
57 pub(crate) max_nested_tests: usize,
58 pub(crate) max_nested_foreverypart: usize,
59 pub(crate) max_match_variables: usize,
60 pub(crate) max_local_variables: usize,
61 pub(crate) max_header_size: usize,
62 pub(crate) max_includes: usize,
63 pub(crate) no_capability_check: bool,
64
65 pub(crate) functions: AHashMap<String, (u32, u32)>,
67}
68
69pub type Function = for<'x> fn(&'x Context<'x>, Vec<Variable>) -> Variable;
70
71#[derive(Default, Clone)]
72pub struct FunctionMap {
73 pub(crate) map: AHashMap<String, (u32, u32)>,
74 pub(crate) functions: Vec<Function>,
75}
76
77#[derive(Debug, Clone)]
78pub struct Runtime {
79 pub(crate) allowed_capabilities: AHashSet<Capability>,
80 pub(crate) valid_notification_uris: AHashSet<Cow<'static, str>>,
81 pub(crate) valid_ext_lists: AHashSet<Cow<'static, str>>,
82 pub(crate) protected_headers: Vec<HeaderName<'static>>,
83 pub(crate) environment: AHashMap<Cow<'static, str>, Variable>,
84 pub(crate) metadata: Vec<(Metadata<String>, Cow<'static, str>)>,
85 pub(crate) include_scripts: AHashMap<String, Arc<Sieve>>,
86 pub(crate) local_hostname: Cow<'static, str>,
87 pub(crate) functions: Vec<Function>,
88
89 pub(crate) max_nested_includes: usize,
90 pub(crate) cpu_limit: usize,
91 pub(crate) max_variable_size: usize,
92 pub(crate) max_redirects: usize,
93 pub(crate) max_received_headers: usize,
94 pub(crate) max_header_size: usize,
95 pub(crate) max_out_messages: usize,
96
97 pub(crate) default_vacation_expiry: u64,
98 pub(crate) default_duplicate_expiry: u64,
99
100 pub(crate) vacation_use_orig_rcpt: bool,
101 pub(crate) vacation_default_subject: Cow<'static, str>,
102 pub(crate) vacation_subject_prefix: Cow<'static, str>,
103}
104
105#[derive(Clone, Debug)]
106pub struct Context<'x> {
107 #[cfg(test)]
108 pub(crate) runtime: Runtime,
109 #[cfg(not(test))]
110 pub(crate) runtime: &'x Runtime,
111 pub(crate) user_address: Cow<'x, str>,
112 pub(crate) user_full_name: Cow<'x, str>,
113 pub(crate) current_time: i64,
114
115 pub(crate) message: Message<'x>,
116 pub(crate) message_size: usize,
117 pub(crate) envelope: Vec<(Envelope, Variable)>,
118 pub(crate) metadata: Vec<(Metadata<String>, Cow<'x, str>)>,
119
120 pub(crate) part: u32,
121 pub(crate) part_iter: IntoIter<u32>,
122 pub(crate) part_iter_stack: Vec<(u32, IntoIter<u32>)>,
123
124 pub(crate) spam_status: SpamStatus,
125 pub(crate) virus_status: VirusStatus,
126
127 pub(crate) pos: usize,
128 pub(crate) test_result: bool,
129 pub(crate) script_cache: AHashMap<Script, Arc<Sieve>>,
130 pub(crate) script_stack: Vec<ScriptStack>,
131 pub(crate) vars_global: AHashMap<Cow<'static, str>, Variable>,
132 pub(crate) vars_env: AHashMap<Cow<'static, str>, Variable>,
133 pub(crate) vars_local: Vec<Variable>,
134 pub(crate) vars_match: Vec<Variable>,
135 pub(crate) expr_stack: Vec<Variable>,
136 pub(crate) expr_pos: usize,
137
138 pub(crate) constants: Arc<[Arc<str>]>,
139 pub(crate) flags: Vec<Arc<str>>,
140
141 pub(crate) queued_events: IntoIter<Event>,
142 pub(crate) final_event: Option<Event>,
143 pub(crate) last_message_id: usize,
144 pub(crate) main_message_id: usize,
145
146 pub(crate) has_changes: bool,
147 pub(crate) num_redirects: usize,
148 pub(crate) num_instructions: usize,
149 pub(crate) num_out_messages: usize,
150}
151
152#[derive(Debug, Clone, Eq, PartialEq, Hash)]
153pub enum Script {
154 Personal(String),
155 Global(String),
156}
157
158#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
159#[cfg_attr(
160 any(test, feature = "serde"),
161 derive(serde::Serialize, serde::Deserialize)
162)]
163#[cfg_attr(
164 feature = "rkyv",
165 derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
166)]
167#[repr(u8)]
168pub enum Envelope {
169 From = 0,
170 To = 1,
171 ByTimeAbsolute = 2,
172 ByTimeRelative = 3,
173 ByMode = 4,
174 ByTrace = 5,
175 Notify = 6,
176 Orcpt = 7,
177 Ret = 8,
178 Envid = 9,
179}
180
181#[derive(Debug, Clone, Eq, PartialEq, Hash)]
182#[cfg_attr(
183 any(test, feature = "serde"),
184 derive(serde::Serialize, serde::Deserialize)
185)]
186#[cfg_attr(
187 feature = "rkyv",
188 derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
189)]
190#[repr(u8)]
191pub enum Metadata<T> {
192 Server { annotation: T } = 0,
193 Mailbox { name: T, annotation: T } = 1,
194}
195
196#[derive(Debug, Clone, Eq, PartialEq)]
197pub enum Event {
198 IncludeScript {
199 name: Script,
200 optional: bool,
201 },
202 MailboxExists {
203 mailboxes: Vec<Mailbox>,
204 special_use: Vec<String>,
205 },
206 ListContains {
207 lists: Vec<String>,
208 values: Vec<String>,
209 match_as: MatchAs,
210 },
211 DuplicateId {
212 id: String,
213 expiry: u64,
214 last: bool,
215 },
216 SetEnvelope {
217 envelope: Envelope,
218 value: String,
219 },
220 Function {
221 id: ExternalId,
222 arguments: Vec<Variable>,
223 },
224
225 Keep {
227 flags: Vec<String>,
228 message_id: usize,
229 },
230 Discard,
231 Reject {
232 extended: bool,
233 reason: String,
234 },
235 FileInto {
236 folder: String,
237 flags: Vec<String>,
238 mailbox_id: Option<String>,
239 special_use: Option<String>,
240 create: bool,
241 message_id: usize,
242 },
243 SendMessage {
244 recipient: Recipient,
245 notify: Notify,
246 return_of_content: Ret,
247 by_time: ByTime<i64>,
248 message_id: usize,
249 },
250 Notify {
251 from: Option<String>,
252 importance: Importance,
253 options: Vec<String>,
254 message: String,
255 method: String,
256 },
257 CreatedMessage {
258 message_id: usize,
259 message: Vec<u8>,
260 },
261}
262
263pub type ExternalId = u32;
264
265#[derive(Debug, Clone, PartialEq, Eq, Hash)]
266#[cfg_attr(
267 any(test, feature = "serde"),
268 derive(serde::Serialize, serde::Deserialize)
269)]
270#[cfg_attr(
271 feature = "rkyv",
272 derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
273)]
274pub(crate) struct FileCarbonCopy<T> {
275 pub mailbox: T,
276 pub mailbox_id: Option<T>,
277 pub create: bool,
278 pub flags: Box<[T]>,
279 pub special_use: Option<T>,
280}
281
282#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
283pub enum Importance {
284 High,
285 Normal,
286 Low,
287}
288
289#[derive(Debug, Clone, Copy, Eq, PartialEq)]
290pub enum MatchAs {
291 Octet,
292 Lowercase,
293 Number,
294}
295
296#[derive(Debug, Clone, Eq, PartialEq, Hash)]
297pub enum Recipient {
298 Address(String),
299 List(String),
300 Group(Vec<String>),
301}
302
303#[derive(Debug, Clone, Eq, PartialEq)]
304pub enum Input {
305 True,
306 False,
307 FncResult(Variable),
308 Script { name: Script, script: Arc<Sieve> },
309}
310
311#[derive(Debug, Clone, Eq, PartialEq, Hash)]
312pub enum Mailbox {
313 Name(String),
314 Id(String),
315}
316
317#[derive(Debug, Clone, Copy, PartialEq)]
318pub enum SpamStatus {
319 Unknown,
320 Ham,
321 MaybeSpam(f64),
322 Spam,
323}
324
325#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
326pub enum VirusStatus {
327 Unknown,
328 Clean,
329 Replaced,
330 Cured,
331 MaybeVirus,
332 Virus,
333}
334
335impl Sieve {
336 #[inline(always)]
337 pub(crate) fn instructions_from(&self, pos: usize) -> std::slice::Iter<'_, Instruction> {
338 self.instructions.get(pos..).unwrap_or_default().iter()
339 }
340
341 #[inline(always)]
342 pub(crate) fn constants(&self) -> Arc<[Arc<str>]> {
343 self.constants.clone()
344 }
345
346 pub fn constant_count(&self) -> usize {
347 self.constants.len()
348 }
349
350 pub fn instruction_count(&self) -> usize {
351 self.instructions.len()
352 }
353
354 pub fn instruction_footprint(&self) -> usize {
355 self.instructions.len() * std::mem::size_of::<Instruction>()
356 }
357
358 pub fn instruction_size() -> usize {
359 std::mem::size_of::<Instruction>()
360 }
361
362 pub fn expression_size() -> usize {
363 std::mem::size_of::<compiler::grammar::expr::Expression>()
364 }
365
366 pub fn value_size() -> usize {
367 std::mem::size_of::<compiler::Value>()
368 }
369
370 pub fn probe_sizes() -> Vec<(&'static str, usize)> {
371 vec![
372 ("Value", std::mem::size_of::<compiler::Value>()),
373 (
374 "VariableType",
375 std::mem::size_of::<compiler::VariableType>(),
376 ),
377 (
378 "HeaderVariable",
379 std::mem::size_of::<compiler::HeaderVariable<'static>>(),
380 ),
381 ("HeaderPart", std::mem::size_of::<compiler::HeaderPart>()),
382 ("Regex", std::mem::size_of::<compiler::Regex>()),
383 ("Glob", std::mem::size_of::<compiler::Glob>()),
384 ("Number", std::mem::size_of::<compiler::Number>()),
385 (
386 "HeaderName",
387 std::mem::size_of::<mail_parser::HeaderName<'static>>(),
388 ),
389 ]
390 }
391}
392
393#[cfg(test)]
394mod tests {
395 use std::{
396 fs,
397 path::{Path, PathBuf},
398 };
399
400 use ahash::{AHashMap, AHashSet};
401 use mail_parser::{
402 Encoding, HeaderValue, Message, MessageParser, MessagePart, PartType,
403 parsers::MessageStream,
404 };
405
406 use crate::{
407 Compiler, Context, Envelope, Event, FunctionMap, Input, Mailbox, Recipient, Runtime, Sieve,
408 SpamStatus, VirusStatus,
409 compiler::grammar::Capability,
410 runtime::{Variable, actions::action_mime::reset_test_boundary},
411 };
412
413 impl Variable {
414 pub fn unwrap_string(self) -> String {
415 self.to_string().into_owned()
416 }
417 }
418
419 #[test]
420 fn test_suite() {
421 let mut tests = Vec::new();
422 let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
423 path.push("tests");
424
425 read_dir(path, &mut tests);
426
427 for test in tests {
428 println!("===== {} =====", test.display());
438 run_test(&test);
439 }
440 }
441
442 fn read_dir(path: PathBuf, files: &mut Vec<PathBuf>) {
443 for entry in fs::read_dir(path).unwrap() {
444 let entry = entry.unwrap().path();
445 if entry.is_dir() {
446 read_dir(entry, files);
447 } else if entry
448 .extension()
449 .and_then(|e| e.to_str())
450 .unwrap_or("")
451 .eq("svtest")
452 {
453 files.push(entry);
454 }
455 }
456 }
457
458 fn run_test(script_path: &Path) {
459 let mut fnc_map = FunctionMap::new()
460 .with_function("trim", |_, v| match v.into_iter().next().unwrap() {
461 crate::runtime::Variable::String(s) => s.trim().to_string().into(),
462 v => v.to_string().into(),
463 })
464 .with_function("len", |_, v| v[0].to_string().len().into())
465 .with_function("count", |_, v| {
466 v[0].as_array().map_or(0, |arr| arr.len()).into()
467 })
468 .with_function("to_lowercase", |_, v| {
469 v[0].to_string().to_lowercase().to_string().into()
470 })
471 .with_function("to_uppercase", |_, v| {
472 v[0].to_string().to_uppercase().to_string().into()
473 })
474 .with_function("is_uppercase", |_, v| {
475 v[0].to_string()
476 .as_ref()
477 .chars()
478 .filter(|c| c.is_alphabetic())
479 .all(|c| c.is_uppercase())
480 .into()
481 })
482 .with_function("is_ascii", |_, v| {
483 v[0].to_string().as_ref().is_ascii().into()
484 })
485 .with_function("char_count", |_, v| {
486 v[0].to_string().as_ref().chars().count().into()
487 })
488 .with_function("lines", |_, v| {
489 v[0].to_string()
490 .lines()
491 .map(|line| Variable::from(line.to_string()))
492 .collect::<Vec<_>>()
493 .into()
494 })
495 .with_function_args(
496 "contains",
497 |_, v| v[0].to_string().contains(v[1].to_string().as_ref()).into(),
498 2,
499 )
500 .with_function_args(
501 "eq_lowercase",
502 |_, v| {
503 v[0].to_string()
504 .as_ref()
505 .eq_ignore_ascii_case(v[1].to_string().as_ref())
506 .into()
507 },
508 2,
509 )
510 .with_function_args(
511 "concat_three",
512 |_, v| format!("{}-{}-{}", v[0], v[1], v[2]).into(),
513 3,
514 )
515 .with_function_args(
516 "in_array",
517 |_, v| {
518 v[0].as_array()
519 .is_some_and(|arr| arr.contains(&v[1]))
520 .into()
521 },
522 2,
523 )
524 .with_external_function("ext_zero", 0, 0)
525 .with_external_function("ext_one", 1, 1)
526 .with_external_function("ext_two", 2, 2)
527 .with_external_function("ext_three", 3, 3)
528 .with_external_function("ext_true", 4, 0)
529 .with_external_function("ext_false", 5, 0);
530 let mut compiler = Compiler::new()
531 .with_max_string_size(10240)
532 .register_functions(&mut fnc_map);
533
534 let mut ancestors = script_path.ancestors();
535 ancestors.next();
536 let base_path = ancestors.next().unwrap();
537 let script = compiler
538 .compile(&add_crlf(&fs::read(script_path).unwrap()))
539 .unwrap();
540
541 let mut input = Input::script("", script);
542 let mut current_test = String::new();
543 let mut raw_message_: Option<Vec<u8>> = None;
544 let mut prev_state = None;
545 let mut mailboxes = Vec::new();
546 let mut lists: AHashMap<String, AHashSet<String>> = AHashMap::new();
547 let mut duplicated_ids = AHashSet::new();
548 let mut actions = Vec::new();
549
550 'outer: loop {
551 let runtime = Runtime::new()
552 .with_protected_header("Auto-Submitted")
553 .with_protected_header("Received")
554 .with_valid_notification_uri("mailto")
555 .with_max_out_messages(100)
556 .with_capability(Capability::While)
557 .with_capability(Capability::Expressions)
558 .with_functions(&mut fnc_map.clone());
559 let mut instance = Context::new(
560 &runtime,
561 Message {
562 parts: vec![MessagePart {
563 headers: vec![],
564 is_encoding_problem: false,
565 body: PartType::Text("".into()),
566 encoding: Encoding::None,
567 offset_header: 0,
568 offset_body: 0,
569 offset_end: 0,
570 }],
571 raw_message: b""[..].into(),
572 ..Default::default()
573 },
574 );
575 let raw_message = raw_message_.take().unwrap_or_default();
576 instance.message =
577 MessageParser::new()
578 .parse(&raw_message)
579 .unwrap_or_else(|| Message {
580 html_body: vec![],
581 text_body: vec![],
582 attachments: vec![],
583 parts: vec![MessagePart {
584 headers: vec![],
585 is_encoding_problem: false,
586 body: PartType::Text("".into()),
587 encoding: Encoding::None,
588 offset_header: 0,
589 offset_body: 0,
590 offset_end: 0,
591 }],
592 raw_message: b""[..].into(),
593 });
594 instance.message_size = raw_message.len();
595 if let Some((
596 pos,
597 script_cache,
598 script_stack,
599 constants,
600 flags,
601 vars_global,
602 vars_local,
603 vars_match,
604 )) = prev_state.take()
605 {
606 instance.pos = pos;
607 instance.script_cache = script_cache;
608 instance.script_stack = script_stack;
609 instance.constants = constants;
610 instance.flags = flags;
611 instance.vars_global = vars_global;
612 instance.vars_local = vars_local;
613 instance.vars_match = vars_match;
614 }
615 instance.set_env_variable("vnd.stalwart.default_mailbox", "INBOX");
616 instance.set_env_variable("vnd.stalwart.username", "john.doe");
617 instance.set_user_address("MAILER-DAEMON");
618 if let Some(addr) = instance
619 .message
620 .from()
621 .and_then(|a| a.first())
622 .and_then(|a| a.address.as_ref())
623 {
624 instance.set_envelope(Envelope::From, addr.to_string());
625 }
626 if let Some(addr) = instance
627 .message
628 .to()
629 .and_then(|a| a.first())
630 .and_then(|a| a.address.as_ref())
631 {
632 instance.set_envelope(Envelope::To, addr.to_string());
633 }
634
635 while let Some(event) = instance.run(input) {
636 match event.unwrap() {
637 Event::IncludeScript { name, optional } => {
638 let mut include_path = PathBuf::from(base_path);
639 include_path.push(if matches!(name, crate::Script::Personal(_)) {
640 "included"
641 } else {
642 "included-global"
643 });
644 include_path.push(format!("{name}.sieve"));
645
646 if let Ok(bytes) = fs::read(include_path.as_path()) {
647 let script = compiler.compile(&add_crlf(&bytes)).unwrap();
648 input = Input::script(name, script);
649 } else if optional {
650 input = Input::False;
651 } else {
652 panic!("Script {} not found.", include_path.display());
653 }
654 }
655 Event::MailboxExists {
656 mailboxes: mailboxes_,
657 special_use,
658 } => {
659 for action in &actions {
660 if let Event::FileInto { folder, create, .. } = action
661 && *create
662 && !mailboxes.contains(folder)
663 {
664 mailboxes.push(folder.to_string());
665 }
666 }
667 input = (special_use.is_empty()
668 && mailboxes_.iter().all(|n| {
669 if let Mailbox::Name(n) = n {
670 mailboxes.contains(n)
671 } else {
672 false
673 }
674 }))
675 .into();
676 }
677 Event::ListContains {
678 lists: lists_,
679 values,
680 ..
681 } => {
682 let mut result = false;
683 'list: for list in &lists_ {
684 if let Some(list) = lists.get(list) {
685 for value in &values {
686 if list.contains(value) {
687 result = true;
688 break 'list;
689 }
690 }
691 }
692 }
693
694 input = result.into();
695 }
696 Event::DuplicateId { id, .. } => {
697 input = duplicated_ids.contains(&id).into();
698 }
699 Event::Function { id, arguments } => {
700 if id == u32::MAX {
701 input = Input::True;
703 let mut arguments = arguments.into_iter();
704 let command = arguments.next().unwrap().unwrap_string();
705 let mut params =
706 arguments.map(|arg| arg.unwrap_string()).collect::<Vec<_>>();
707
708 match command.as_str() {
709 "test" => {
710 current_test = params.pop().unwrap();
711 println!("Running test '{current_test}'...");
712 }
713 "test_set" => {
714 let mut params = params.into_iter();
715 let target = params.next().expect("test_set parameter");
716 if target == "message" {
717 let value = params.next().unwrap();
718 raw_message_ = if value.eq_ignore_ascii_case(":smtp") {
719 let mut message = None;
720 for action in actions.iter().rev() {
721 if let Event::SendMessage { message_id, .. } =
722 action
723 {
724 let message_ = actions
725 .iter()
726 .find_map(|item| {
727 if let Event::CreatedMessage {
728 message_id: message_id_,
729 message,
730 } = item
731 && message_id == message_id_
732 {
733 return Some(message);
734 }
735 None
736 })
737 .unwrap();
738 message = message_.into();
743 break;
744 }
745 }
746 message.expect("No SMTP message found").to_vec().into()
747 } else {
748 value.into_bytes().into()
749 };
750 prev_state = (
751 instance.pos,
752 instance.script_cache,
753 instance.script_stack,
754 instance.constants,
755 instance.flags,
756 instance.vars_global,
757 instance.vars_local,
758 instance.vars_match,
759 )
760 .into();
761
762 continue 'outer;
763 } else if let Some(envelope) = target.strip_prefix("envelope.")
764 {
765 let envelope =
766 Envelope::try_from(envelope.to_string()).unwrap();
767 instance.envelope.retain(|(e, _)| e != &envelope);
768 instance.set_envelope(envelope, params.next().unwrap());
769 } else if target == "currentdate" {
770 let bytes = params.next().unwrap().into_bytes();
771 if let HeaderValue::DateTime(dt) =
772 MessageStream::new(&bytes).parse_date()
773 {
774 instance.current_time = dt.to_timestamp();
775 } else {
776 panic!("Invalid currentdate");
777 }
778 } else {
779 panic!("test_set {target} not implemented.");
780 }
781 }
782 "test_message" => {
783 let mut params = params.into_iter();
784 input = match params.next().unwrap().as_str() {
785 ":folder" => {
786 let folder_name = params.next().expect("test_message folder name");
787 matches!(&instance.final_event, Some(Event::Keep { .. })) ||
788 actions.iter().any(|a| if !folder_name.eq_ignore_ascii_case("INBOX") {
789 matches!(a, Event::FileInto { folder, .. } if folder == &folder_name )
790 } else {
791 matches!(a, Event::Keep { .. })
792 })
793 }
794 ":smtp" => {
795 actions.iter().any(|a| matches!(a, Event::SendMessage { .. } ))
796 }
797 param => panic!("Invalid test_message param '{param}'" ),
798 }.into();
799 }
800 "test_assert_message" => {
801 let expected_message =
802 params.first().expect("test_set parameter");
803 let built_message = instance.build_message();
804 if expected_message.as_bytes() != built_message {
805 print!("<[");
807 print!("{}", String::from_utf8(built_message).unwrap());
808 println!("]>");
809 panic!("Message built incorrectly at '{current_test}'");
810 }
811 }
812 "test_config_set" => {
813 let mut params = params.into_iter();
814 let name = params.next().unwrap();
815 let value = params.next().expect("test_config_set value");
816
817 match name.as_str() {
818 "sieve_editheader_protected"
819 | "sieve_editheader_forbid_add"
820 | "sieve_editheader_forbid_delete" => {
821 if !value.is_empty() {
822 for header_name in value.split(' ') {
823 instance.runtime.set_protected_header(
824 header_name.to_string(),
825 );
826 }
827 } else {
828 instance.runtime.protected_headers.clear();
829 }
830 }
831 "sieve_variables_max_variable_size" => {
832 instance
833 .runtime
834 .set_max_variable_size(value.parse().unwrap());
835 }
836 "sieve_valid_ext_list" => {
837 instance.runtime.set_valid_ext_list(value);
838 }
839 "sieve_ext_list_item" => {
840 lists
841 .entry(value)
842 .or_default()
843 .insert(params.next().expect("list item value"));
844 }
845 "sieve_duplicated_id" => {
846 duplicated_ids.insert(value);
847 }
848 "sieve_user_email" => {
849 instance.set_user_address(value);
850 }
851 "sieve_vacation_use_original_recipient" => {
852 instance.runtime.set_vacation_use_orig_rcpt(
853 value.eq_ignore_ascii_case("yes"),
854 );
855 }
856 "sieve_vacation_default_subject" => {
857 instance.runtime.set_vacation_default_subject(value);
858 }
859 "sieve_vacation_default_subject_template" => {
860 instance.runtime.set_vacation_subject_prefix(value);
861 }
862 "sieve_spam_status" => {
863 instance.set_spam_status(SpamStatus::from_number(
864 value.parse().unwrap(),
865 ));
866 }
867 "sieve_spam_status_plus" => {
868 instance.set_spam_status(
869 match value.parse::<u32>().unwrap() {
870 0 => SpamStatus::Unknown,
871 100.. => SpamStatus::Spam,
872 n => SpamStatus::MaybeSpam((n as f64) / 100.0),
873 },
874 );
875 }
876 "sieve_virus_status" => {
877 instance.set_virus_status(VirusStatus::from_number(
878 value.parse().unwrap(),
879 ));
880 }
881 "sieve_editheader_max_header_size" => {
882 let mhs = if !value.is_empty() {
883 value.parse::<usize>().unwrap()
884 } else {
885 1024
886 };
887 instance.runtime.set_max_header_size(mhs);
888 compiler.set_max_header_size(mhs);
889 }
890 "sieve_include_max_includes" => {
891 compiler.set_max_includes(if !value.is_empty() {
892 value.parse::<usize>().unwrap()
893 } else {
894 3
895 });
896 }
897 "sieve_include_max_nesting_depth" => {
898 compiler.set_max_nested_blocks(if !value.is_empty() {
899 value.parse::<usize>().unwrap()
900 } else {
901 3
902 });
903 }
904 param => panic!("Invalid test_config_set param '{param}'"),
905 }
906 }
907 "test_result_execute" => {
908 input =
909 (matches!(&instance.final_event, Some(Event::Keep { .. }))
910 || actions.iter().any(|a| {
911 matches!(
912 a,
913 Event::Keep { .. }
914 | Event::FileInto { .. }
915 | Event::SendMessage { .. }
916 )
917 }))
918 .into();
919 }
920 "test_result_action" => {
921 let param =
922 params.first().expect("test_result_action parameter");
923 input = if param == "reject" {
924 (actions.iter().any(|a| matches!(a, Event::Reject { .. })))
925 .into()
926 } else if param == "redirect" {
927 let param = params
928 .last()
929 .expect("test_result_action redirect address");
930 (actions
931 .iter()
932 .any(|a| matches!(a, Event::SendMessage { recipient: Recipient::Address(address), .. } if address == param)))
933 .into()
934 } else if param == "keep" {
935 (matches!(&instance.final_event, Some(Event::Keep { .. }))
936 || actions
937 .iter()
938 .any(|a| matches!(a, Event::Keep { .. })))
939 .into()
940 } else if param == "send_message" {
941 (actions
942 .iter()
943 .any(|a| matches!(a, Event::SendMessage { .. })))
944 .into()
945 } else {
946 panic!("test_result_action {param} not implemented");
947 };
948 }
949 "test_result_action_count" => {
950 input = (actions.len()
951 == params.first().unwrap().parse::<usize>().unwrap())
952 .into();
953 }
954 "test_imap_metadata_set" => {
955 let mut params = params.into_iter();
956 let first = params.next().expect("metadata parameter");
957 let (mailbox, annotation) = if first == ":mailbox" {
958 (
959 params.next().expect("metadata mailbox name").into(),
960 params.next().expect("metadata annotation name"),
961 )
962 } else {
963 (None, first)
964 };
965 let value = params.next().expect("metadata value");
966 if let Some(mailbox) = mailbox {
967 instance.set_medatata((mailbox, annotation), value);
968 } else {
969 instance.set_medatata(annotation, value);
970 }
971 }
972 "test_mailbox_create" => {
973 mailboxes.push(params.pop().expect("mailbox to create"));
974 }
975 "test_result_reset" => {
976 actions.clear();
977 instance.final_event = Event::Keep {
978 flags: vec![],
979 message_id: 0,
980 }
981 .into();
982 instance.metadata.clear();
983 instance.has_changes = false;
984 instance.num_redirects = 0;
985 instance.runtime.vacation_use_orig_rcpt = false;
986 mailboxes.clear();
987 lists.clear();
988 reset_test_boundary();
989 }
990 "test_script_compile" => {
991 let mut include_path = PathBuf::from(base_path);
992 include_path.push(params.first().unwrap());
993
994 if let Ok(bytes) = fs::read(include_path.as_path()) {
995 let result = compiler.compile(&add_crlf(&bytes));
996 input = result.is_ok().into();
1000 } else {
1001 panic!("Script {} not found.", include_path.display());
1002 }
1003 }
1004 "test_config_reload" => (),
1005 "test_fail" => {
1006 panic!(
1007 "Test '{}' failed: {}",
1008 current_test,
1009 params.pop().unwrap()
1010 );
1011 }
1012 _ => panic!("Test command {command} not implemented."),
1013 }
1014 } else {
1015 let result = match id {
1016 0 => Variable::from("my_value"),
1017 1 => Variable::from(arguments[0].to_string().to_uppercase()),
1018 2 => Variable::from(format!(
1019 "{}-{}",
1020 arguments[0].to_string(),
1021 arguments[1].to_string()
1022 )),
1023 3 => Variable::from(format!(
1024 "{}-{}-{}",
1025 arguments[0].to_string(),
1026 arguments[1].to_string(),
1027 arguments[2].to_string()
1028 )),
1029 4 => true.into(),
1030 5 => false.into(),
1031 _ => {
1032 panic!("Unknown external function {id}");
1033 }
1034 };
1035
1036 input = result.into();
1037 }
1038 }
1039
1040 action => {
1041 actions.push(action);
1042 input = true.into();
1043 }
1044 }
1045 }
1046
1047 return;
1048 }
1049 }
1050
1051 const ROUND_TRIP_SCRIPT: &str = concat!(
1052 "require [\"variables\", \"fileinto\"];\n",
1053 "if header :contains \"subject\" \"hello\" { set \"greeting\" \"hello\"; }\n",
1054 "if header :matches \"subject\" \"*world*\" { fileinto \"hello\"; }\n"
1055 );
1056
1057 #[test]
1058 fn constants_serialize_as_strings() {
1059 let script = Compiler::new()
1060 .compile(&add_crlf(ROUND_TRIP_SCRIPT.as_bytes()))
1061 .unwrap();
1062
1063 let json = serde_json::to_value(&script).unwrap();
1064 let constants = json
1065 .get("constants")
1066 .expect("constants field")
1067 .as_array()
1068 .expect("constants array");
1069
1070 assert!(
1071 constants.iter().all(|value| value.is_string()),
1072 "constants must serialize as plain strings: {constants:?}"
1073 );
1074 assert_eq!(
1075 constants
1076 .iter()
1077 .filter(|value| value.as_str() == Some("hello"))
1078 .count(),
1079 1,
1080 "repeated literals must be deduplicated: {constants:?}"
1081 );
1082
1083 let restored: Sieve = serde_json::from_value(json).unwrap();
1084 assert_eq!(script, restored);
1085 }
1086
1087 #[test]
1088 #[cfg(feature = "rkyv")]
1089 fn constants_rkyv_round_trip() {
1090 use crate::serialize::AsStringVec;
1091 use rkyv::{Archive, with::ArchiveWith};
1092 use std::marker::PhantomData;
1093 use std::sync::Arc;
1094
1095 fn assert_same_archived<T: ?Sized>(_: PhantomData<T>, _: PhantomData<T>) {}
1096
1097 assert_same_archived(
1098 PhantomData::<<Vec<String> as Archive>::Archived>,
1099 PhantomData::<<AsStringVec as ArchiveWith<Arc<[Arc<str>]>>>::Archived>,
1100 );
1101
1102 let script = Compiler::new()
1103 .compile(&add_crlf(ROUND_TRIP_SCRIPT.as_bytes()))
1104 .unwrap();
1105
1106 let bytes = rkyv::to_bytes::<rkyv::rancor::Error>(&script).unwrap();
1107 let restored = rkyv::from_bytes::<Sieve, rkyv::rancor::Error>(&bytes).unwrap();
1108
1109 assert_eq!(script, restored);
1110 }
1111
1112 #[test]
1113 #[cfg(feature = "rkyv")]
1114 fn versioned_rkyv_round_trip() {
1115 use crate::ArchiveError;
1116
1117 let script = Compiler::new()
1118 .compile(&add_crlf(ROUND_TRIP_SCRIPT.as_bytes()))
1119 .unwrap();
1120
1121 let bytes = script.to_bytes().unwrap();
1122
1123 assert_eq!(
1124 bytes.last().copied(),
1125 Some(Compiler::VERSION as u8),
1126 "the compiler version must be stamped on the serialized script"
1127 );
1128 assert_eq!(script, Sieve::from_bytes(&bytes).unwrap());
1129 assert_eq!(
1130 script,
1131 unsafe { Sieve::from_bytes_unchecked(&bytes) }.unwrap()
1132 );
1133
1134 let mut stale = bytes.clone();
1135 *stale.last_mut().unwrap() = Compiler::VERSION as u8 - 1;
1136
1137 assert!(matches!(
1138 Sieve::from_bytes(&stale),
1139 Err(ArchiveError::UnsupportedVersion(_))
1140 ));
1141 assert!(matches!(
1142 Sieve::from_bytes(&[]),
1143 Err(ArchiveError::Truncated)
1144 ));
1145 assert!(matches!(
1146 Sieve::from_bytes(&bytes[bytes.len() - 1..]),
1147 Err(ArchiveError::Truncated)
1148 ));
1149 }
1150
1151 #[test]
1152 #[cfg(feature = "rkyv")]
1153 fn archived_variant_budget() {
1154 use crate::compiler::grammar::{instruction::Instruction, test::Test};
1155 use rkyv::Archive;
1156 use std::mem::size_of;
1157
1158 assert_eq!(
1159 size_of::<<Test as Archive>::Archived>(),
1160 8,
1161 "a Test variant grew the archived enum, box its payload instead"
1162 );
1163 assert_eq!(
1164 size_of::<<Instruction as Archive>::Archived>(),
1165 12,
1166 "an Instruction variant grew the archived enum, box its payload instead"
1167 );
1168 }
1169
1170 fn add_crlf(bytes: &[u8]) -> Vec<u8> {
1171 let mut result = Vec::with_capacity(bytes.len());
1172 let mut last_ch = 0;
1173 for &ch in bytes {
1174 if ch == b'\n' && last_ch != b'\r' {
1175 result.push(b'\r');
1176 }
1177 result.push(ch);
1178 last_ch = ch;
1179 }
1180 result
1181 }
1182}