Skip to main content

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 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    // Settings
53    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    // Functions
66    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    // Actions
226    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            /*if !test
429                .file_name()
430                .unwrap()
431                .to_str()
432                .unwrap()
433                .contains("expressions")
434            {
435                continue;
436            }*/
437            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                            // Test functions
702                            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                                                    /*println!(
739                                                        "<[{}]>",
740                                                        std::str::from_utf8(message_).unwrap()
741                                                    );*/
742                                                    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                                        //fs::write("_deleteme.json", serde_json::to_string_pretty(&Message::parse(&built_message).unwrap()).unwrap()).unwrap();
806                                        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                                        /*if let Err(err) = &result {
997                                            println!("Error: {:?}", err);
998                                        }*/
999                                        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}