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