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//! [](https://crates.io/crates/sieve-rs)
27//! [](https://github.com/stalwartlabs/sieve/actions/workflows/rust.yml)
28//! [](https://docs.rs/sieve-rs)
29//! [](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}