Skip to content

Latest commit

 

History

124 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

sieve

crates.io build docs.rs License: AGPL v3

sieve is a fast and secure Sieve filter interpreter for Rust that supports all registered Sieve extensions.

Usage Example

use sieve::{Arena, Compiler, Context, Handler, Reply, Runtime, SieveAction, Status};

struct Printer {
    messages: Vec<String>,
    raw_message: &'static str,
}

impl<'x> Handler<'x> for Printer {
    fn action(&mut self, _: &Context<'x>, action: SieveAction<'_>) -> Reply<()> {
        match action {
            SieveAction::Keep { flags, message_id } => {
                println!(
                    "Keep message '{}' with flags {:?}.",
                    self.message(message_id),
                    flags
                );
            }
            SieveAction::Discard => {
                println!("Discard message.");
            }
            SieveAction::Reject { reason, .. } => {
                println!("Reject message with reason {reason:?}.");
            }
            SieveAction::FileInto {
                folder,
                flags,
                message_id,
                ..
            } => {
                println!(
                    "File message '{}' in folder {:?} with flags {:?}.",
                    self.message(message_id),
                    folder,
                    flags
                );
            }
            SieveAction::SendMessage {
                recipient,
                message_id,
                ..
            } => {
                println!(
                    "Send message '{}' to {:?}.",
                    self.message(message_id),
                    recipient
                );
            }
            SieveAction::Notify {
                message, method, ..
            } => {
                println!("Notify URI {method:?} with message {message:?}");
            }
            SieveAction::SetEnvelope { envelope, value } => {
                println!("Set envelope {envelope:?} to {value:?}");
            }
            SieveAction::CreatedMessage { message, .. } => {
                self.messages
                    .push(String::from_utf8_lossy(&message).into_owned());
            }
        }
        Reply::Ready(())
    }
}

impl Printer {
    fn message(&self, message_id: usize) -> &str {
        if message_id > 0 {
            self.messages[message_id - 1].as_str()
        } else {
            self.raw_message
        }
    }
}

fn main() {
    let text_script = br#"
    require ["fileinto", "body", "imap4flags"];
    
    if body :contains "tps" {
        setflag "$tps_reports";
    }

    if header :matches "List-ID" "*<*@*" {
        fileinto "INBOX.lists.${2}"; stop;
    }
    "#;
    let raw_message = r#"From: Sales Mailing List <list-sales@example.org>
To: John Doe <jdoe@example.org>
List-ID: <sales@example.org>
Subject: TPS Reports

We're putting new coversheets on all the TPS reports before they go out now.
So if you could go ahead and try to remember to do that from now on, that'd be great. All right! 
"#;

    let compiler = Compiler::new();
    let script = compiler.compile(text_script).unwrap();

    let runtime = Runtime::new();

    let mut arena = Arena::new();
    let mut instance = runtime.filter(raw_message.as_bytes(), &script, &mut arena);
    let mut handler = Printer {
        messages: Vec::new(),
        raw_message,
    };

    loop {
        match instance.run(&mut handler) {
            Ok(Status::Finished) => break,
            Ok(Status::Pending) => instance.resume(true),
            Err(error) => {
                println!("Runtime error {error}");
                break;
            }
        }
    }
}

Runtime::filter parses a copy of the raw message. A message already parsed with mail_parser::MessageParser is passed by reference to Runtime::filter_parsed instead, and the context borrows it without copying. The parsed message is never modified: header edits, replace and convert are recorded by the context, enclose builds and parses a new message owned by the arena, and the edited message is delivered as SieveAction::CreatedMessage.

Scripts compile to a compact bytecode. Store script.to_bytes() and load it again with Sieve::from_bytes(&bytes), which borrows the buffer instead of copying it, so a script can be loaded on every delivery at almost no cost. Operations that need an asynchronous answer (list lookups, duplicate tracking, mailbox checks, external functions, scripts loaded on demand) return Reply::Pending from the handler; the interpreter then returns Status::Pending and continues after resume(input). Scripts loaded while a run is in progress (include of a user script) must outlive the Context; push them into a ScriptArena declared before the context and hand the interpreter the reference it returns.

Testing & Fuzzing

To run the testsuite:

 $ cargo test --all-features

To fuzz the library with cargo-fuzz:

 $ cargo +nightly fuzz run sieve

Conformed RFCs

License

Licensed under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

You can be released from the requirements of the AGPLv3 license by purchasing a commercial license. Please contact licensing@stalw.art for more details.

Copyright

Copyright (C) 2020, Stalwart Labs LLC

About

Sieve filter interpreter for Rust

Topics

Resources

Code of conduct

Security policy

Stars

31 stars

Watchers

2 watching

Forks

Releases

Sponsor this project

Used by

Contributors

Languages