sieve/
lib.rs

1/*
2 * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art>
3 *
4 * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
5 */
6
7#![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    // Settings
44    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    // Functions
57    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    // Actions
212    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            /*if !test
357                .file_name()
358                .unwrap()
359                .to_str()
360                .unwrap()
361                .contains("expressions")
362            {
363                continue;
364            }*/
365            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                            // Test functions
619                            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                                                    /*println!(
657                                                        "<[{}]>",
658                                                        std::str::from_utf8(message_).unwrap()
659                                                    );*/
660                                                    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                                        //fs::write("_deleteme.json", serde_json::to_string_pretty(&Message::parse(&built_message).unwrap()).unwrap()).unwrap();
722                                        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                                        /*if let Err(err) = &result {
913                                            println!("Error: {:?}", err);
914                                        }*/
915                                        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}