sieve/
lib.rs

1/*
2 * Copyright (c) 2020-2023, Stalwart Labs Ltd.
3 *
4 * This file is part of the Stalwart Sieve Interpreter.
5 *
6 * This program is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU Affero General Public License as
8 * published by the Free Software Foundation, either version 3 of
9 * the License, or (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU Affero General Public License for more details.
15 * in the LICENSE file at the top-level directory of this distribution.
16 * You should have received a copy of the GNU Affero General Public License
17 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
18 *
19 * You can be released from the requirements of the AGPLv3 license by
20 * purchasing a commercial license. Please contact licensing@stalw.art
21 * for more details.
22*/
23
24//! # sieve
25//!
26//! [![crates.io](https://img.shields.io/crates/v/sieve-rs)](https://crates.io/crates/sieve-rs)
27//! [![build](https://github.com/stalwartlabs/sieve/actions/workflows/rust.yml/badge.svg)](https://github.com/stalwartlabs/sieve/actions/workflows/rust.yml)
28//! [![docs.rs](https://img.shields.io/docsrs/sieve-rs)](https://docs.rs/sieve-rs)
29//! [![License: AGPL v3](https://img.shields.io/badge/License-AGPL_v3-blue.svg)](https://www.gnu.org/licenses/agpl-3.0)
30//!
31//! _sieve_ is a fast and secure Sieve filter interpreter for Rust that supports all [registered Sieve extensions](https://www.iana.org/assignments/sieve-extensions/sieve-extensions.xhtml).
32//!
33//! ## Usage Example
34//!
35//! ```rust
36//!     use sieve::{runtime::RuntimeError, Compiler, Event, Input, Runtime};
37//!
38//!     let text_script = br#"
39//!     require ["fileinto", "body", "imap4flags"];
40//!     
41//!     if body :contains "tps" {
42//!         setflag "$tps_reports";
43//!     }
44//!
45//!     if header :matches "List-ID" "*<*@*" {
46//!         fileinto "INBOX.lists.${2}"; stop;
47//!     }
48//!     "#;
49//!     let raw_message = r#"From: Sales Mailing List <list-sales@example.org>
50//! To: John Doe <jdoe@example.org>
51//! List-ID: <sales@example.org>
52//! Subject: TPS Reports
53//!
54//! We're putting new coversheets on all the TPS reports before they go out now.
55//! So if you could go ahead and try to remember to do that from now on, that'd be great. All right!
56//! "#;
57//!
58//!     // Compile
59//!     let compiler = Compiler::new();
60//!     let script = compiler.compile(text_script).unwrap();
61//!
62//!     // Build runtime
63//!     let runtime = Runtime::new();
64//!
65//!     // Create filter instance
66//!     let mut instance = runtime.filter(raw_message.as_bytes());
67//!     let mut input = Input::script("my-script", script);
68//!     let mut messages: Vec<String> = Vec::new();
69//!
70//!     // Start event loop
71//!     while let Some(result) = instance.run(input) {
72//!         match result {
73//!             Ok(event) => match event {
74//!                 Event::IncludeScript { name, optional } => {
75//!                     // NOTE: Just for demonstration purposes, script name needs to be validated first.
76//!                     if let Ok(bytes) = std::fs::read(name.as_str()) {
77//!                         let script = compiler.compile(&bytes).unwrap();
78//!                         input = Input::script(name, script);
79//!                     } else if optional {
80//!                         input = Input::False;
81//!                     } else {
82//!                         panic!("Script {} not found.", name);
83//!                     }
84//!                 }
85//!                 Event::MailboxExists { .. } => {
86//!                     // Set to true if the mailbox exists
87//!                     input = false.into();
88//!                 }
89//!                 Event::ListContains { .. } => {
90//!                     // Set to true if the list(s) contains an entry
91//!                     input = false.into();
92//!                 }
93//!                 Event::DuplicateId { .. } => {
94//!                     // Set to true if the ID is duplicate
95//!                     input = false.into();
96//!                 }
97//!                 Event::SetEnvelope { envelope, value } => {
98//!                     println!("Set envelope {envelope:?} to {value:?}");
99//!                     input = true.into();
100//!                 }
101//!
102//!                 Event::Keep { flags, message_id } => {
103//!                     println!(
104//!                         "Keep message '{}' with flags {:?}.",
105//!                         if message_id > 0 {
106//!                             messages[message_id - 1].as_str()
107//!                         } else {
108//!                             raw_message
109//!                         },
110//!                         flags
111//!                     );
112//!                     input = true.into();
113//!                 }
114//!                 Event::Discard => {
115//!                     println!("Discard message.");
116//!                     input = true.into();
117//!                 }
118//!                 Event::Reject { reason, .. } => {
119//!                     println!("Reject message with reason {:?}.", reason);
120//!                     input = true.into();
121//!                 }
122//!                 Event::FileInto {
123//!                     folder,
124//!                     flags,
125//!                     message_id,
126//!                     ..
127//!                 } => {
128//!                     println!(
129//!                         "File message '{}' in folder {:?} with flags {:?}.",
130//!                         if message_id > 0 {
131//!                             messages[message_id - 1].as_str()
132//!                         } else {
133//!                             raw_message
134//!                         },
135//!                         folder,
136//!                         flags
137//!                     );
138//!                     input = true.into();
139//!                 }
140//!                 Event::SendMessage {
141//!                     recipient,
142//!                     message_id,
143//!                     ..
144//!                 } => {
145//!                     println!(
146//!                         "Send message '{}' to {:?}.",
147//!                         if message_id > 0 {
148//!                             messages[message_id - 1].as_str()
149//!                         } else {
150//!                             raw_message
151//!                         },
152//!                         recipient
153//!                     );
154//!                     input = true.into();
155//!                 }
156//!                 Event::Notify {
157//!                     message, method, ..
158//!                 } => {
159//!                     println!("Notify URI {:?} with message {:?}", method, message);
160//!                     input = true.into();
161//!                 }
162//!                 Event::CreatedMessage { message, .. } => {
163//!                     messages.push(String::from_utf8(message).unwrap());
164//!                     input = true.into();
165//!                 }
166//!                 Event::Function { id, arguments } => {
167//!                    println!(
168//!                        "Script executed external function {id} with parameters {arguments:?}"
169//!                    );
170//!                    // Return variable result back to interpreter
171//!                    input = Input::result("hello world".into());
172//!                }
173//!             },
174//!             Err(error) => {
175//!                 match error {
176//!                     RuntimeError::TooManyIncludes => {
177//!                         eprintln!("Too many included scripts.");
178//!                     }
179//!                     RuntimeError::InvalidInstruction(instruction) => {
180//!                         eprintln!(
181//!                             "Invalid instruction {:?} found at {}:{}.",
182//!                             instruction.name(),
183//!                             instruction.line_num(),
184//!                             instruction.line_pos()
185//!                         );
186//!                     }
187//!                     RuntimeError::ScriptErrorMessage(message) => {
188//!                         eprintln!("Script called the 'error' function with {:?}", message);
189//!                     }
190//!                     RuntimeError::CapabilityNotAllowed(capability) => {
191//!                         eprintln!(
192//!                             "Capability {:?} has been disabled by the administrator.",
193//!                             capability
194//!                         );
195//!                     }
196//!                     RuntimeError::CapabilityNotSupported(capability) => {
197//!                         eprintln!("Capability {:?} not supported.", capability);
198//!                     }
199//!                     RuntimeError::CPULimitReached => {
200//!                         eprintln!("Script exceeded the configured CPU limit.");
201//!                     }
202//!                 }
203//!                 input = true.into();
204//!             }
205//!         }
206//!     }
207//! ```
208//!
209//! ## Testing and Fuzzing
210//!
211//! To run the testsuite:
212//!
213//! ```bash
214//!  $ cargo test --all-features
215//! ```
216//!
217//! To fuzz the library with `cargo-fuzz`:
218//!
219//! ```bash
220//!  $ cargo +nightly fuzz run mail_parser
221//! ```
222//!
223//! ## Conformed RFCs
224//!
225//! - [RFC 5228 - Sieve: An Email Filtering Language](https://datatracker.ietf.org/doc/html/rfc5228)
226//! - [RFC 3894 - Copying Without Side Effects](https://datatracker.ietf.org/doc/html/rfc3894)
227//! - [RFC 5173 - Body Extension](https://datatracker.ietf.org/doc/html/rfc5173)
228//! - [RFC 5183 - Environment Extension](https://datatracker.ietf.org/doc/html/rfc5183)
229//! - [RFC 5229 - Variables Extension](https://datatracker.ietf.org/doc/html/rfc5229)
230//! - [RFC 5230 - Vacation Extension](https://datatracker.ietf.org/doc/html/rfc5230)
231//! - [RFC 5231 - Relational Extension](https://datatracker.ietf.org/doc/html/rfc5231)
232//! - [RFC 5232 - Imap4flags Extension](https://datatracker.ietf.org/doc/html/rfc5232)
233//! - [RFC 5233 - Subaddress Extension](https://datatracker.ietf.org/doc/html/rfc5233)
234//! - [RFC 5235 - Spamtest and Virustest Extensions](https://datatracker.ietf.org/doc/html/rfc5235)
235//! - [RFC 5260 - Date and Index Extensions](https://datatracker.ietf.org/doc/html/rfc5260)
236//! - [RFC 5293 - Editheader Extension](https://datatracker.ietf.org/doc/html/rfc5293)
237//! - [RFC 5429 - Reject and Extended Reject Extensions](https://datatracker.ietf.org/doc/html/rfc5429)
238//! - [RFC 5435 - Extension for Notifications](https://datatracker.ietf.org/doc/html/rfc5435)
239//! - [RFC 5463 - Ihave Extension](https://datatracker.ietf.org/doc/html/rfc5463)
240//! - [RFC 5490 - Extensions for Checking Mailbox Status and Accessing Mailbox Metadata](https://datatracker.ietf.org/doc/html/rfc5490)
241//! - [RFC 5703 - MIME Part Tests, Iteration, Extraction, Replacement, and Enclosure](https://datatracker.ietf.org/doc/html/rfc5703)
242//! - [RFC 6009 - Delivery Status Notifications and Deliver-By Extensions](https://datatracker.ietf.org/doc/html/rfc6009)
243//! - [RFC 6131 - Sieve Vacation Extension: "Seconds" Parameter](https://datatracker.ietf.org/doc/html/rfc6131)
244//! - [RFC 6134 - Externally Stored Lists](https://datatracker.ietf.org/doc/html/rfc6134)
245//! - [RFC 6558 - Converting Messages before Delivery](https://datatracker.ietf.org/doc/html/rfc6558)
246//! - [RFC 6609 - Include Extension](https://datatracker.ietf.org/doc/html/rfc6609)
247//! - [RFC 7352 - Detecting Duplicate Deliveries](https://datatracker.ietf.org/doc/html/rfc7352)
248//! - [RFC 8579 - Delivering to Special-Use Mailboxes](https://datatracker.ietf.org/doc/html/rfc8579)
249//! - [RFC 8580 - File Carbon Copy (FCC)](https://datatracker.ietf.org/doc/html/rfc8580)
250//! - [RFC 9042 - Delivery by MAILBOXID](https://datatracker.ietf.org/doc/html/rfc9042)
251//! - [REGEX-01 - Regular Expression Extension (draft-ietf-sieve-regex-01)](https://www.ietf.org/archive/id/draft-ietf-sieve-regex-01.html)
252//!
253//! ## License
254//!
255//! Licensed under the terms of the [GNU Affero General Public License](https://www.gnu.org/licenses/agpl-3.0.en.html) as published by
256//! the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
257//! See [LICENSE](LICENSE) for more details.
258//!
259//! You can be released from the requirements of the AGPLv3 license by purchasing
260//! a commercial license. Please contact licensing@stalw.art for more details.
261//!   
262//! ## Copyright
263//!
264//! Copyright (C) 2020-2023, Stalwart Labs Ltd.
265//!
266
267use std::{borrow::Cow, sync::Arc, vec::IntoIter};
268
269use ahash::{AHashMap, AHashSet};
270use compiler::grammar::{
271    actions::action_redirect::{ByTime, Notify, Ret},
272    instruction::Instruction,
273    Capability,
274};
275use mail_parser::{HeaderName, Message};
276use runtime::{context::ScriptStack, Variable};
277use serde::{Deserialize, Serialize};
278
279pub mod compiler;
280pub mod runtime;
281
282pub(crate) const MAX_MATCH_VARIABLES: usize = 63;
283pub(crate) const MAX_LOCAL_VARIABLES: usize = 256;
284
285#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
286pub struct Sieve {
287    pub instructions: Vec<Instruction>,
288    num_vars: usize,
289    num_match_vars: usize,
290}
291
292#[derive(Clone)]
293pub struct Compiler {
294    // Settings
295    pub(crate) max_script_size: usize,
296    pub(crate) max_string_size: usize,
297    pub(crate) max_variable_name_size: usize,
298    pub(crate) max_nested_blocks: usize,
299    pub(crate) max_nested_tests: usize,
300    pub(crate) max_nested_foreverypart: usize,
301    pub(crate) max_match_variables: usize,
302    pub(crate) max_local_variables: usize,
303    pub(crate) max_header_size: usize,
304    pub(crate) max_includes: usize,
305    pub(crate) no_capability_check: bool,
306
307    // Functions
308    pub(crate) functions: AHashMap<String, (u32, u32)>,
309}
310
311pub type Function = for<'x> fn(&'x Context<'x>, Vec<Variable>) -> Variable;
312
313#[derive(Default, Clone)]
314pub struct FunctionMap {
315    pub(crate) map: AHashMap<String, (u32, u32)>,
316    pub(crate) functions: Vec<Function>,
317}
318
319#[derive(Debug, Clone)]
320pub struct Runtime {
321    pub(crate) allowed_capabilities: AHashSet<Capability>,
322    pub(crate) valid_notification_uris: AHashSet<Cow<'static, str>>,
323    pub(crate) valid_ext_lists: AHashSet<Cow<'static, str>>,
324    pub(crate) protected_headers: Vec<HeaderName<'static>>,
325    pub(crate) environment: AHashMap<Cow<'static, str>, Variable>,
326    pub(crate) metadata: Vec<(Metadata<String>, Cow<'static, str>)>,
327    pub(crate) include_scripts: AHashMap<String, Arc<Sieve>>,
328    pub(crate) local_hostname: Cow<'static, str>,
329    pub(crate) functions: Vec<Function>,
330
331    pub(crate) max_nested_includes: usize,
332    pub(crate) cpu_limit: usize,
333    pub(crate) max_variable_size: usize,
334    pub(crate) max_redirects: usize,
335    pub(crate) max_received_headers: usize,
336    pub(crate) max_header_size: usize,
337    pub(crate) max_out_messages: usize,
338
339    pub(crate) default_vacation_expiry: u64,
340    pub(crate) default_duplicate_expiry: u64,
341
342    pub(crate) vacation_use_orig_rcpt: bool,
343    pub(crate) vacation_default_subject: Cow<'static, str>,
344    pub(crate) vacation_subject_prefix: Cow<'static, str>,
345}
346
347#[derive(Clone, Debug)]
348pub struct Context<'x> {
349    #[cfg(test)]
350    pub(crate) runtime: Runtime,
351    #[cfg(not(test))]
352    pub(crate) runtime: &'x Runtime,
353    pub(crate) user_address: Cow<'x, str>,
354    pub(crate) user_full_name: Cow<'x, str>,
355    pub(crate) current_time: i64,
356
357    pub(crate) message: Message<'x>,
358    pub(crate) message_size: usize,
359    pub(crate) envelope: Vec<(Envelope, Variable)>,
360    pub(crate) metadata: Vec<(Metadata<String>, Cow<'x, str>)>,
361
362    pub(crate) part: usize,
363    pub(crate) part_iter: IntoIter<usize>,
364    pub(crate) part_iter_stack: Vec<(usize, IntoIter<usize>)>,
365
366    pub(crate) spam_status: SpamStatus,
367    pub(crate) virus_status: VirusStatus,
368
369    pub(crate) pos: usize,
370    pub(crate) test_result: bool,
371    pub(crate) script_cache: AHashMap<Script, Arc<Sieve>>,
372    pub(crate) script_stack: Vec<ScriptStack>,
373    pub(crate) vars_global: AHashMap<Cow<'static, str>, Variable>,
374    pub(crate) vars_env: AHashMap<Cow<'static, str>, Variable>,
375    pub(crate) vars_local: Vec<Variable>,
376    pub(crate) vars_match: Vec<Variable>,
377    pub(crate) expr_stack: Vec<Variable>,
378    pub(crate) expr_pos: usize,
379
380    pub(crate) queued_events: IntoIter<Event>,
381    pub(crate) final_event: Option<Event>,
382    pub(crate) last_message_id: usize,
383    pub(crate) main_message_id: usize,
384
385    pub(crate) has_changes: bool,
386    pub(crate) num_redirects: usize,
387    pub(crate) num_instructions: usize,
388    pub(crate) num_out_messages: usize,
389}
390
391#[derive(Debug, Clone, Eq, PartialEq, Hash)]
392pub enum Script {
393    Personal(String),
394    Global(String),
395}
396
397#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Serialize, Deserialize)]
398pub enum Envelope {
399    From,
400    To,
401    ByTimeAbsolute,
402    ByTimeRelative,
403    ByMode,
404    ByTrace,
405    Notify,
406    Orcpt,
407    Ret,
408    Envid,
409}
410
411#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
412pub enum Metadata<T> {
413    Server { annotation: T },
414    Mailbox { name: T, annotation: T },
415}
416
417#[derive(Debug, Clone, Eq, PartialEq)]
418pub enum Event {
419    IncludeScript {
420        name: Script,
421        optional: bool,
422    },
423    MailboxExists {
424        mailboxes: Vec<Mailbox>,
425        special_use: Vec<String>,
426    },
427    ListContains {
428        lists: Vec<String>,
429        values: Vec<String>,
430        match_as: MatchAs,
431    },
432    DuplicateId {
433        id: String,
434        expiry: u64,
435        last: bool,
436    },
437    SetEnvelope {
438        envelope: Envelope,
439        value: String,
440    },
441    Function {
442        id: ExternalId,
443        arguments: Vec<Variable>,
444    },
445
446    // Actions
447    Keep {
448        flags: Vec<String>,
449        message_id: usize,
450    },
451    Discard,
452    Reject {
453        extended: bool,
454        reason: String,
455    },
456    FileInto {
457        folder: String,
458        flags: Vec<String>,
459        mailbox_id: Option<String>,
460        special_use: Option<String>,
461        create: bool,
462        message_id: usize,
463    },
464    SendMessage {
465        recipient: Recipient,
466        notify: Notify,
467        return_of_content: Ret,
468        by_time: ByTime<i64>,
469        message_id: usize,
470    },
471    Notify {
472        from: Option<String>,
473        importance: Importance,
474        options: Vec<String>,
475        message: String,
476        method: String,
477    },
478    CreatedMessage {
479        message_id: usize,
480        message: Vec<u8>,
481    },
482}
483
484pub type ExternalId = u32;
485
486#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
487pub(crate) struct FileCarbonCopy<T> {
488    pub mailbox: T,
489    pub mailbox_id: Option<T>,
490    pub create: bool,
491    pub flags: Vec<T>,
492    pub special_use: Option<T>,
493}
494
495#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
496pub enum Importance {
497    High,
498    Normal,
499    Low,
500}
501
502#[derive(Debug, Clone, Copy, Eq, PartialEq)]
503pub enum MatchAs {
504    Octet,
505    Lowercase,
506    Number,
507}
508
509#[derive(Debug, Clone, Eq, PartialEq, Hash)]
510pub enum Recipient {
511    Address(String),
512    List(String),
513    Group(Vec<String>),
514}
515
516#[derive(Debug, Clone, Eq, PartialEq)]
517pub enum Input {
518    True,
519    False,
520    FncResult(Variable),
521    Script { name: Script, script: Arc<Sieve> },
522}
523
524#[derive(Debug, Clone, Eq, PartialEq, Hash)]
525pub enum Mailbox {
526    Name(String),
527    Id(String),
528}
529
530#[derive(Debug, Clone, Copy, PartialEq)]
531pub enum SpamStatus {
532    Unknown,
533    Ham,
534    MaybeSpam(f64),
535    Spam,
536}
537
538#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
539pub enum VirusStatus {
540    Unknown,
541    Clean,
542    Replaced,
543    Cured,
544    MaybeVirus,
545    Virus,
546}
547
548#[cfg(test)]
549mod tests {
550    use std::{
551        fs,
552        path::{Path, PathBuf},
553    };
554
555    use ahash::{AHashMap, AHashSet};
556    use mail_parser::{
557        parsers::MessageStream, Encoding, HeaderValue, Message, MessageParser, MessagePart,
558        PartType,
559    };
560
561    use crate::{
562        compiler::grammar::Capability,
563        runtime::{actions::action_mime::reset_test_boundary, Variable},
564        Compiler, Context, Envelope, Event, FunctionMap, Input, Mailbox, Recipient, Runtime,
565        SpamStatus, VirusStatus,
566    };
567
568    impl Variable {
569        pub fn unwrap_string(self) -> String {
570            self.to_string().into_owned()
571        }
572    }
573
574    #[test]
575    fn test_suite() {
576        let mut tests = Vec::new();
577        let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
578        path.push("tests");
579
580        read_dir(path, &mut tests);
581
582        for test in tests {
583            /*if !test
584                .file_name()
585                .unwrap()
586                .to_str()
587                .unwrap()
588                .contains("expressions")
589            {
590                continue;
591            }*/
592            println!("===== {} =====", test.display());
593            run_test(&test);
594        }
595    }
596
597    fn read_dir(path: PathBuf, files: &mut Vec<PathBuf>) {
598        for entry in fs::read_dir(path).unwrap() {
599            let entry = entry.unwrap().path();
600            if entry.is_dir() {
601                read_dir(entry, files);
602            } else if entry
603                .extension()
604                .and_then(|e| e.to_str())
605                .unwrap_or("")
606                .eq("svtest")
607            {
608                files.push(entry);
609            }
610        }
611    }
612
613    fn run_test(script_path: &Path) {
614        let mut fnc_map = FunctionMap::new()
615            .with_function("trim", |_, v| match v.into_iter().next().unwrap() {
616                crate::runtime::Variable::String(s) => s.trim().to_string().into(),
617                v => v.to_string().into(),
618            })
619            .with_function("len", |_, v| v[0].to_string().len().into())
620            .with_function("count", |_, v| {
621                v[0].as_array().map_or(0, |arr| arr.len()).into()
622            })
623            .with_function("to_lowercase", |_, v| {
624                v[0].to_string().to_lowercase().to_string().into()
625            })
626            .with_function("to_uppercase", |_, v| {
627                v[0].to_string().to_uppercase().to_string().into()
628            })
629            .with_function("is_uppercase", |_, v| {
630                v[0].to_string()
631                    .as_ref()
632                    .chars()
633                    .filter(|c| c.is_alphabetic())
634                    .all(|c| c.is_uppercase())
635                    .into()
636            })
637            .with_function("is_ascii", |_, v| {
638                v[0].to_string()
639                    .as_ref()
640                    .chars()
641                    .any(|c| !c.is_ascii())
642                    .into()
643            })
644            .with_function("char_count", |_, v| {
645                v[0].to_string().as_ref().chars().count().into()
646            })
647            .with_function("lines", |_, v| {
648                v[0].to_string()
649                    .lines()
650                    .map(|line| Variable::from(line.to_string()))
651                    .collect::<Vec<_>>()
652                    .into()
653            })
654            .with_function_args(
655                "contains",
656                |_, v| v[0].to_string().contains(v[1].to_string().as_ref()).into(),
657                2,
658            )
659            .with_function_args(
660                "eq_lowercase",
661                |_, v| {
662                    v[0].to_string()
663                        .as_ref()
664                        .eq_ignore_ascii_case(v[1].to_string().as_ref())
665                        .into()
666                },
667                2,
668            )
669            .with_function_args(
670                "concat_three",
671                |_, v| format!("{}-{}-{}", v[0], v[1], v[2]).into(),
672                3,
673            )
674            .with_function_args(
675                "in_array",
676                |_, v| {
677                    v[0].as_array()
678                        .map_or(false, |arr| arr.contains(&v[1]))
679                        .into()
680                },
681                2,
682            )
683            .with_external_function("ext_zero", 0, 0)
684            .with_external_function("ext_one", 1, 1)
685            .with_external_function("ext_two", 2, 2)
686            .with_external_function("ext_three", 3, 3)
687            .with_external_function("ext_true", 4, 0)
688            .with_external_function("ext_false", 5, 0);
689        let mut compiler = Compiler::new()
690            .with_max_string_size(10240)
691            .register_functions(&mut fnc_map);
692
693        let mut ancestors = script_path.ancestors();
694        ancestors.next();
695        let base_path = ancestors.next().unwrap();
696        let script = compiler
697            .compile(&add_crlf(&fs::read(script_path).unwrap()))
698            .unwrap();
699
700        let mut input = Input::script("", script);
701        let mut current_test = String::new();
702        let mut raw_message_: Option<Vec<u8>> = None;
703        let mut prev_state = None;
704        let mut mailboxes = Vec::new();
705        let mut lists: AHashMap<String, AHashSet<String>> = AHashMap::new();
706        let mut duplicated_ids = AHashSet::new();
707        let mut actions = Vec::new();
708
709        'outer: loop {
710            let runtime = Runtime::new()
711                .with_protected_header("Auto-Submitted")
712                .with_protected_header("Received")
713                .with_valid_notification_uri("mailto")
714                .with_max_out_messages(100)
715                .with_capability(Capability::While)
716                .with_capability(Capability::Expressions)
717                .with_functions(&mut fnc_map.clone());
718            let mut instance = Context::new(
719                &runtime,
720                Message {
721                    parts: vec![MessagePart {
722                        headers: vec![],
723                        is_encoding_problem: false,
724                        body: PartType::Text("".into()),
725                        encoding: Encoding::None,
726                        offset_header: 0,
727                        offset_body: 0,
728                        offset_end: 0,
729                    }],
730                    raw_message: b""[..].into(),
731                    ..Default::default()
732                },
733            );
734            let raw_message = raw_message_.take().unwrap_or_default();
735            instance.message =
736                MessageParser::new()
737                    .parse(&raw_message)
738                    .unwrap_or_else(|| Message {
739                        html_body: vec![],
740                        text_body: vec![],
741                        attachments: vec![],
742                        parts: vec![MessagePart {
743                            headers: vec![],
744                            is_encoding_problem: false,
745                            body: PartType::Text("".into()),
746                            encoding: Encoding::None,
747                            offset_header: 0,
748                            offset_body: 0,
749                            offset_end: 0,
750                        }],
751                        raw_message: b""[..].into(),
752                    });
753            instance.message_size = raw_message.len();
754            if let Some((pos, script_cache, script_stack, vars_global, vars_local, vars_match)) =
755                prev_state.take()
756            {
757                instance.pos = pos;
758                instance.script_cache = script_cache;
759                instance.script_stack = script_stack;
760                instance.vars_global = vars_global;
761                instance.vars_local = vars_local;
762                instance.vars_match = vars_match;
763            }
764            instance.set_env_variable("vnd.stalwart.default_mailbox", "INBOX");
765            instance.set_env_variable("vnd.stalwart.username", "john.doe");
766            instance.set_user_address("MAILER-DAEMON");
767            if let Some(addr) = instance
768                .message
769                .from()
770                .and_then(|a| a.first())
771                .and_then(|a| a.address.as_ref())
772            {
773                instance.set_envelope(Envelope::From, addr.to_string());
774            }
775            if let Some(addr) = instance
776                .message
777                .to()
778                .and_then(|a| a.first())
779                .and_then(|a| a.address.as_ref())
780            {
781                instance.set_envelope(Envelope::To, addr.to_string());
782            }
783
784            while let Some(event) = instance.run(input) {
785                match event.unwrap() {
786                    Event::IncludeScript { name, optional } => {
787                        let mut include_path = PathBuf::from(base_path);
788                        include_path.push(if matches!(name, crate::Script::Personal(_)) {
789                            "included"
790                        } else {
791                            "included-global"
792                        });
793                        include_path.push(format!("{name}.sieve"));
794
795                        if let Ok(bytes) = fs::read(include_path.as_path()) {
796                            let script = compiler.compile(&add_crlf(&bytes)).unwrap();
797                            input = Input::script(name, script);
798                        } else if optional {
799                            input = Input::False;
800                        } else {
801                            panic!("Script {} not found.", include_path.display());
802                        }
803                    }
804                    Event::MailboxExists {
805                        mailboxes: mailboxes_,
806                        special_use,
807                    } => {
808                        for action in &actions {
809                            if let Event::FileInto { folder, create, .. } = action {
810                                if *create && !mailboxes.contains(folder) {
811                                    mailboxes.push(folder.to_string());
812                                }
813                            }
814                        }
815                        input = (special_use.is_empty()
816                            && mailboxes_.iter().all(|n| {
817                                if let Mailbox::Name(n) = n {
818                                    mailboxes.contains(n)
819                                } else {
820                                    false
821                                }
822                            }))
823                        .into();
824                    }
825                    Event::ListContains {
826                        lists: lists_,
827                        values,
828                        ..
829                    } => {
830                        let mut result = false;
831                        'list: for list in &lists_ {
832                            if let Some(list) = lists.get(list) {
833                                for value in &values {
834                                    if list.contains(value) {
835                                        result = true;
836                                        break 'list;
837                                    }
838                                }
839                            }
840                        }
841
842                        input = result.into();
843                    }
844                    Event::DuplicateId { id, .. } => {
845                        input = duplicated_ids.contains(&id).into();
846                    }
847                    Event::Function { id, arguments } => {
848                        if id == u32::MAX {
849                            // Test functions
850                            input = Input::True;
851                            let mut arguments = arguments.into_iter();
852                            let command = arguments.next().unwrap().unwrap_string();
853                            let mut params =
854                                arguments.map(|arg| arg.unwrap_string()).collect::<Vec<_>>();
855
856                            match command.as_str() {
857                                "test" => {
858                                    current_test = params.pop().unwrap();
859                                    println!("Running test '{current_test}'...");
860                                }
861                                "test_set" => {
862                                    let mut params = params.into_iter();
863                                    let target = params.next().expect("test_set parameter");
864                                    if target == "message" {
865                                        let value = params.next().unwrap();
866                                        raw_message_ = if value.eq_ignore_ascii_case(":smtp") {
867                                            let mut message = None;
868                                            for action in actions.iter().rev() {
869                                                if let Event::SendMessage { message_id, .. } =
870                                                    action
871                                                {
872                                                    let message_ = actions
873                                                        .iter()
874                                                        .find_map(|item| {
875                                                            if let Event::CreatedMessage {
876                                                                message_id: message_id_,
877                                                                message,
878                                                            } = item
879                                                            {
880                                                                if message_id == message_id_ {
881                                                                    return Some(message);
882                                                                }
883                                                            }
884                                                            None
885                                                        })
886                                                        .unwrap();
887                                                    /*println!(
888                                                        "<[{}]>",
889                                                        std::str::from_utf8(message_).unwrap()
890                                                    );*/
891                                                    message = message_.into();
892                                                    break;
893                                                }
894                                            }
895                                            message.expect("No SMTP message found").to_vec().into()
896                                        } else {
897                                            value.into_bytes().into()
898                                        };
899                                        prev_state = (
900                                            instance.pos,
901                                            instance.script_cache,
902                                            instance.script_stack,
903                                            instance.vars_global,
904                                            instance.vars_local,
905                                            instance.vars_match,
906                                        )
907                                            .into();
908
909                                        continue 'outer;
910                                    } else if let Some(envelope) = target.strip_prefix("envelope.")
911                                    {
912                                        let envelope =
913                                            Envelope::try_from(envelope.to_string()).unwrap();
914                                        instance.envelope.retain(|(e, _)| e != &envelope);
915                                        instance.set_envelope(envelope, params.next().unwrap());
916                                    } else if target == "currentdate" {
917                                        let bytes = params.next().unwrap().into_bytes();
918                                        if let HeaderValue::DateTime(dt) =
919                                            MessageStream::new(&bytes).parse_date()
920                                        {
921                                            instance.current_time = dt.to_timestamp();
922                                        } else {
923                                            panic!("Invalid currentdate");
924                                        }
925                                    } else {
926                                        panic!("test_set {target} not implemented.");
927                                    }
928                                }
929                                "test_message" => {
930                                    let mut params = params.into_iter();
931                                    input = match params.next().unwrap().as_str() {
932                                    ":folder" => {
933                                        let folder_name = params.next().expect("test_message folder name");
934                                        matches!(&instance.final_event, Some(Event::Keep { .. })) ||
935                                            actions.iter().any(|a| if !folder_name.eq_ignore_ascii_case("INBOX") {
936                                                matches!(a, Event::FileInto { folder, .. } if folder == &folder_name )
937                                            } else {
938                                                matches!(a, Event::Keep { .. })
939                                            })
940                                    }
941                                    ":smtp" => {
942                                        actions.iter().any(|a| matches!(a, Event::SendMessage { .. } ))
943                                    }
944                                    param => panic!("Invalid test_message param '{param}'" ),
945                                }.into();
946                                }
947                                "test_assert_message" => {
948                                    let expected_message =
949                                        params.first().expect("test_set parameter");
950                                    let built_message = instance.build_message();
951                                    if expected_message.as_bytes() != built_message {
952                                        //fs::write("_deleteme.json", serde_json::to_string_pretty(&Message::parse(&built_message).unwrap()).unwrap()).unwrap();
953                                        print!("<[");
954                                        print!("{}", String::from_utf8(built_message).unwrap());
955                                        println!("]>");
956                                        panic!("Message built incorrectly at '{current_test}'");
957                                    }
958                                }
959                                "test_config_set" => {
960                                    let mut params = params.into_iter();
961                                    let name = params.next().unwrap();
962                                    let value = params.next().expect("test_config_set value");
963
964                                    match name.as_str() {
965                                        "sieve_editheader_protected"
966                                        | "sieve_editheader_forbid_add"
967                                        | "sieve_editheader_forbid_delete" => {
968                                            if !value.is_empty() {
969                                                for header_name in value.split(' ') {
970                                                    instance.runtime.set_protected_header(
971                                                        header_name.to_string(),
972                                                    );
973                                                }
974                                            } else {
975                                                instance.runtime.protected_headers.clear();
976                                            }
977                                        }
978                                        "sieve_variables_max_variable_size" => {
979                                            instance
980                                                .runtime
981                                                .set_max_variable_size(value.parse().unwrap());
982                                        }
983                                        "sieve_valid_ext_list" => {
984                                            instance.runtime.set_valid_ext_list(value);
985                                        }
986                                        "sieve_ext_list_item" => {
987                                            lists
988                                                .entry(value)
989                                                .or_default()
990                                                .insert(params.next().expect("list item value"));
991                                        }
992                                        "sieve_duplicated_id" => {
993                                            duplicated_ids.insert(value);
994                                        }
995                                        "sieve_user_email" => {
996                                            instance.set_user_address(value);
997                                        }
998                                        "sieve_vacation_use_original_recipient" => {
999                                            instance.runtime.set_vacation_use_orig_rcpt(
1000                                                value.eq_ignore_ascii_case("yes"),
1001                                            );
1002                                        }
1003                                        "sieve_vacation_default_subject" => {
1004                                            instance.runtime.set_vacation_default_subject(value);
1005                                        }
1006                                        "sieve_vacation_default_subject_template" => {
1007                                            instance.runtime.set_vacation_subject_prefix(value);
1008                                        }
1009                                        "sieve_spam_status" => {
1010                                            instance.set_spam_status(SpamStatus::from_number(
1011                                                value.parse().unwrap(),
1012                                            ));
1013                                        }
1014                                        "sieve_spam_status_plus" => {
1015                                            instance.set_spam_status(
1016                                                match value.parse::<u32>().unwrap() {
1017                                                    0 => SpamStatus::Unknown,
1018                                                    100.. => SpamStatus::Spam,
1019                                                    n => SpamStatus::MaybeSpam((n as f64) / 100.0),
1020                                                },
1021                                            );
1022                                        }
1023                                        "sieve_virus_status" => {
1024                                            instance.set_virus_status(VirusStatus::from_number(
1025                                                value.parse().unwrap(),
1026                                            ));
1027                                        }
1028                                        "sieve_editheader_max_header_size" => {
1029                                            let mhs = if !value.is_empty() {
1030                                                value.parse::<usize>().unwrap()
1031                                            } else {
1032                                                1024
1033                                            };
1034                                            instance.runtime.set_max_header_size(mhs);
1035                                            compiler.set_max_header_size(mhs);
1036                                        }
1037                                        "sieve_include_max_includes" => {
1038                                            compiler.set_max_includes(if !value.is_empty() {
1039                                                value.parse::<usize>().unwrap()
1040                                            } else {
1041                                                3
1042                                            });
1043                                        }
1044                                        "sieve_include_max_nesting_depth" => {
1045                                            compiler.set_max_nested_blocks(if !value.is_empty() {
1046                                                value.parse::<usize>().unwrap()
1047                                            } else {
1048                                                3
1049                                            });
1050                                        }
1051                                        param => panic!("Invalid test_config_set param '{param}'"),
1052                                    }
1053                                }
1054                                "test_result_execute" => {
1055                                    input =
1056                                        (matches!(&instance.final_event, Some(Event::Keep { .. }))
1057                                            || actions.iter().any(|a| {
1058                                                matches!(
1059                                                    a,
1060                                                    Event::Keep { .. }
1061                                                        | Event::FileInto { .. }
1062                                                        | Event::SendMessage { .. }
1063                                                )
1064                                            }))
1065                                        .into();
1066                                }
1067                                "test_result_action" => {
1068                                    let param =
1069                                        params.first().expect("test_result_action parameter");
1070                                    input = if param == "reject" {
1071                                        (actions.iter().any(|a| matches!(a, Event::Reject { .. })))
1072                                            .into()
1073                                    } else if param == "redirect" {
1074                                        let param = params
1075                                            .last()
1076                                            .expect("test_result_action redirect address");
1077                                        (actions
1078                                        .iter()
1079                                        .any(|a| matches!(a, Event::SendMessage { recipient: Recipient::Address(address), .. } if address == param)))
1080                                    .into()
1081                                    } else if param == "keep" {
1082                                        (matches!(&instance.final_event, Some(Event::Keep { .. }))
1083                                            || actions
1084                                                .iter()
1085                                                .any(|a| matches!(a, Event::Keep { .. })))
1086                                        .into()
1087                                    } else if param == "send_message" {
1088                                        (actions
1089                                            .iter()
1090                                            .any(|a| matches!(a, Event::SendMessage { .. })))
1091                                        .into()
1092                                    } else {
1093                                        panic!("test_result_action {param} not implemented");
1094                                    };
1095                                }
1096                                "test_result_action_count" => {
1097                                    input = (actions.len()
1098                                        == params.first().unwrap().parse::<usize>().unwrap())
1099                                    .into();
1100                                }
1101                                "test_imap_metadata_set" => {
1102                                    let mut params = params.into_iter();
1103                                    let first = params.next().expect("metadata parameter");
1104                                    let (mailbox, annotation) = if first == ":mailbox" {
1105                                        (
1106                                            params.next().expect("metadata mailbox name").into(),
1107                                            params.next().expect("metadata annotation name"),
1108                                        )
1109                                    } else {
1110                                        (None, first)
1111                                    };
1112                                    let value = params.next().expect("metadata value");
1113                                    if let Some(mailbox) = mailbox {
1114                                        instance.set_medatata((mailbox, annotation), value);
1115                                    } else {
1116                                        instance.set_medatata(annotation, value);
1117                                    }
1118                                }
1119                                "test_mailbox_create" => {
1120                                    mailboxes.push(params.pop().expect("mailbox to create"));
1121                                }
1122                                "test_result_reset" => {
1123                                    actions.clear();
1124                                    instance.final_event = Event::Keep {
1125                                        flags: vec![],
1126                                        message_id: 0,
1127                                    }
1128                                    .into();
1129                                    instance.metadata.clear();
1130                                    instance.has_changes = false;
1131                                    instance.num_redirects = 0;
1132                                    instance.runtime.vacation_use_orig_rcpt = false;
1133                                    mailboxes.clear();
1134                                    lists.clear();
1135                                    reset_test_boundary();
1136                                }
1137                                "test_script_compile" => {
1138                                    let mut include_path = PathBuf::from(base_path);
1139                                    include_path.push(params.first().unwrap());
1140
1141                                    if let Ok(bytes) = fs::read(include_path.as_path()) {
1142                                        let result = compiler.compile(&add_crlf(&bytes));
1143                                        /*if let Err(err) = &result {
1144                                            println!("Error: {:?}", err);
1145                                        }*/
1146                                        input = result.is_ok().into();
1147                                    } else {
1148                                        panic!("Script {} not found.", include_path.display());
1149                                    }
1150                                }
1151                                "test_config_reload" => (),
1152                                "test_fail" => {
1153                                    panic!(
1154                                        "Test '{}' failed: {}",
1155                                        current_test,
1156                                        params.pop().unwrap()
1157                                    );
1158                                }
1159                                _ => panic!("Test command {command} not implemented."),
1160                            }
1161                        } else {
1162                            let result = match id {
1163                                0 => Variable::from("my_value"),
1164                                1 => Variable::from(arguments[0].to_string().to_uppercase()),
1165                                2 => Variable::from(format!(
1166                                    "{}-{}",
1167                                    arguments[0].to_string(),
1168                                    arguments[1].to_string()
1169                                )),
1170                                3 => Variable::from(format!(
1171                                    "{}-{}-{}",
1172                                    arguments[0].to_string(),
1173                                    arguments[1].to_string(),
1174                                    arguments[2].to_string()
1175                                )),
1176                                4 => true.into(),
1177                                5 => false.into(),
1178                                _ => {
1179                                    panic!("Unknown external function {id}");
1180                                }
1181                            };
1182
1183                            input = result.into();
1184                        }
1185                    }
1186
1187                    action => {
1188                        actions.push(action);
1189                        input = true.into();
1190                    }
1191                }
1192            }
1193
1194            return;
1195        }
1196    }
1197
1198    fn add_crlf(bytes: &[u8]) -> Vec<u8> {
1199        let mut result = Vec::with_capacity(bytes.len());
1200        let mut last_ch = 0;
1201        for &ch in bytes {
1202            if ch == b'\n' && last_ch != b'\r' {
1203                result.push(b'\r');
1204            }
1205            result.push(ch);
1206            last_ch = ch;
1207        }
1208        result
1209    }
1210}