327 lines
9.9 KiB
Rust
327 lines
9.9 KiB
Rust
/*
|
|
* melib - sieve module
|
|
*
|
|
* Copyright 2022 Manos Pitsidianakis
|
|
*
|
|
* This file is part of meli.
|
|
*
|
|
* meli is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* meli is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
//! Parsing and interpreting the [RFC 5228 - Sieve: An Email Filtering Language]
|
|
//!
|
|
//! [RFC 5228 - Sieve: An Email Filtering Language]: https://www.rfc-editor.org/rfc/rfc5228.html
|
|
|
|
use crate::error::{Error, ErrorKind, Result};
|
|
use crate::parsec::Parser;
|
|
|
|
pub mod ast;
|
|
pub mod parser;
|
|
|
|
use ast::Rule;
|
|
|
|
use std::collections::{HashSet, VecDeque};
|
|
use std::convert::TryFrom;
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
pub enum Capability {
|
|
/// "body"
|
|
Body,
|
|
/// "fileinto"
|
|
FileInto,
|
|
/// "envelope"
|
|
Envelope,
|
|
/// "relational" <https://www.rfc-editor.org/rfc/rfc5231>
|
|
Relational,
|
|
/// "date" <https://www.rfc-editor.org/rfc/rfc5260.html>
|
|
Date,
|
|
}
|
|
|
|
impl TryFrom<&str> for Capability {
|
|
type Error = Error;
|
|
|
|
fn try_from(value: &str) -> Result<Self> {
|
|
use Capability::*;
|
|
for (literal, ext) in [
|
|
("body", Body),
|
|
("fileinto", FileInto),
|
|
("envelope", Envelope),
|
|
("relational", Relational),
|
|
("date", Date),
|
|
] {
|
|
if value.eq_ignore_ascii_case(literal) {
|
|
return Ok(ext);
|
|
}
|
|
}
|
|
Err(
|
|
Error::new(format!("Unrecognized Sieve capability: `{}`.", value))
|
|
.set_kind(ErrorKind::NotSupported),
|
|
)
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct SieveFilter {
|
|
rules: Vec<Rule>,
|
|
capabilities: HashSet<Capability>,
|
|
}
|
|
|
|
impl SieveFilter {
|
|
/// Parse and create a new Sieve script from string.
|
|
pub fn from_str(input: &str) -> Result<Self> {
|
|
match parser::parse_sieve().parse(input) {
|
|
Ok(("", rules)) => Self::new(rules),
|
|
Err(unparsed) | Ok((unparsed, _)) => Err(Error::new(format!(
|
|
"Could not parse part of Sieve filter input: {:?}.",
|
|
unparsed
|
|
))),
|
|
}
|
|
}
|
|
|
|
/// Create a new Sieve script from a vector of rules.
|
|
pub fn new(rules: Vec<Rule>) -> Result<Self> {
|
|
Ok(Self {
|
|
capabilities: Self::validate_rules(&rules)?,
|
|
rules,
|
|
})
|
|
}
|
|
|
|
/// Validate a slice of rules.
|
|
///
|
|
/// ```rust
|
|
/// use melib::parsec::Parser;
|
|
/// use melib::sieve::{parser::parse_sieve, Capability, SieveFilter};
|
|
/// use std::collections::HashSet;
|
|
///
|
|
/// assert_eq!(
|
|
/// SieveFilter::validate_rules(
|
|
/// &parse_sieve()
|
|
/// .parse(
|
|
/// r#"require "fileinto";
|
|
/// if header :contains "from" "coyote" {
|
|
/// discard;
|
|
/// } elsif header :contains ["subject"] ["$$$"] {
|
|
/// discard;
|
|
/// } else {
|
|
/// fileinto "INBOX";
|
|
/// }"#
|
|
/// )
|
|
/// .unwrap()
|
|
/// .1
|
|
/// )
|
|
/// .unwrap(),
|
|
/// HashSet::from([Capability::FileInto])
|
|
/// );
|
|
///
|
|
/// // These should err:
|
|
/// for s in [
|
|
/// "require \"date\";\nif envelope :all :is \"from\" \"tim@example.com\" {\ndiscard;\n}",
|
|
/// "if header :contains \"from\" \"coyote\" {\ndiscard;\n} elsif header :contains [\"subject\"] [\"$$$\"] {\ndiscard;\n} else {\nfileinto \"INBOX\";\n}"
|
|
/// ] {
|
|
/// assert!(
|
|
/// SieveFilter::validate_rules(
|
|
/// &parse_sieve()
|
|
/// .parse(s)
|
|
/// .unwrap()
|
|
/// .1
|
|
/// )
|
|
/// .is_err()
|
|
/// );
|
|
/// }
|
|
/// ```
|
|
pub fn validate_rules(rules: &[Rule]) -> Result<HashSet<Capability>> {
|
|
use ast::{ControlCommand::*, RequiredCapabilities, Rule::*};
|
|
|
|
let mut capabilities = HashSet::default();
|
|
let mut rule_queue = rules.iter().collect::<VecDeque<&Rule>>();
|
|
|
|
while let Some(rule) = rule_queue.pop_front() {
|
|
match rule {
|
|
Control(Require(ref required)) => {
|
|
for ext in required {
|
|
capabilities.insert(Capability::try_from(ext.as_str())?);
|
|
}
|
|
}
|
|
other_rule => {
|
|
if let Some(required_caps) = other_rule.requires() {
|
|
if required_caps.difference(&capabilities).count() > 0 {
|
|
return Err(Error::new(format!(
|
|
"Rules require capabilities {:?} but
|
|
they are not declared with `required`.",
|
|
required_caps
|
|
.difference(&capabilities)
|
|
.collect::<Vec<&Capability>>()
|
|
)));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Ok(capabilities)
|
|
}
|
|
}
|
|
|
|
/// Possible errors when parsing, validating and/or executing Sieve scripts.
|
|
#[derive(Debug, Clone)]
|
|
pub enum SieveError {
|
|
/// Script validity error.
|
|
ValidScriptError {
|
|
/// Encapsulated error value.
|
|
inner: Error,
|
|
},
|
|
/// Script runtime error.
|
|
RuntimeScriptError {
|
|
/// Encapsulated error value.
|
|
inner: Error,
|
|
},
|
|
/// Logic bug error.
|
|
Bug {
|
|
/// Encapsulated error value.
|
|
inner: Error,
|
|
},
|
|
}
|
|
|
|
/// Succesful outcome of a sieve script execution for an [`Envelope`].
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub enum Outcome {
|
|
/// Keep.
|
|
Keep,
|
|
/// Discard.
|
|
Discard,
|
|
/// File into.
|
|
FileInto {
|
|
/// Destination
|
|
destination_mailbox: String,
|
|
},
|
|
/// Redirect to address.
|
|
Redirect {
|
|
/// Destination
|
|
destination_address: String,
|
|
},
|
|
}
|
|
|
|
/// Optional action of a sieve script execution for an [`Envelope`].
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub enum Action {
|
|
/// Copy.
|
|
Copy {
|
|
/// Destination
|
|
destination_mailbox: String,
|
|
},
|
|
/// Forward.
|
|
Forward {
|
|
/// Destination
|
|
destination_address: String,
|
|
},
|
|
/// Modify
|
|
Modify,
|
|
}
|
|
|
|
pub trait Sieve {
|
|
fn passthrough(
|
|
&self,
|
|
script: &SieveFilter,
|
|
) -> std::result::Result<(Outcome, Vec<Action>), SieveError>;
|
|
}
|
|
|
|
impl Sieve for crate::Envelope {
|
|
fn passthrough(
|
|
&self,
|
|
script: &SieveFilter,
|
|
) -> std::result::Result<(Outcome, Vec<Action>), SieveError> {
|
|
use ast::{ActionCommand, ControlCommand::*, Rule::*};
|
|
|
|
// Implicit keep.
|
|
let mut outcome: Outcome = Outcome::Keep;
|
|
let actions: Vec<self::Action> = Vec::with_capacity(0);
|
|
let mut rule_queue = script.rules.iter().collect::<VecDeque<&Rule>>();
|
|
|
|
while let Some(rule) = rule_queue.pop_front() {
|
|
match rule {
|
|
Action(ActionCommand::Discard) => {
|
|
outcome = Outcome::Discard;
|
|
}
|
|
Action(ActionCommand::Keep) => {
|
|
outcome = Outcome::Keep;
|
|
}
|
|
Action(ActionCommand::Redirect { ref address }) => {
|
|
outcome = Outcome::Redirect {
|
|
destination_address: address.clone(),
|
|
};
|
|
}
|
|
Action(ActionCommand::FileInto { ref mailbox }) => {
|
|
outcome = Outcome::FileInto {
|
|
destination_mailbox: mailbox.clone(),
|
|
};
|
|
}
|
|
Control(Stop) => {
|
|
break;
|
|
}
|
|
Control(Require(_)) => {}
|
|
Control(If {
|
|
condition: (ifrule, ifthen),
|
|
elsif,
|
|
else_,
|
|
}) => {
|
|
for (cond, block) in Some((Some(ifrule), ifthen))
|
|
.into_iter()
|
|
.chain(elsif.as_ref().map(|(c, b)| (Some(c), b)).into_iter())
|
|
.chain(else_.as_ref().map(|b| (None, b)).into_iter())
|
|
{
|
|
if let Some(_cond) = cond {
|
|
todo!()
|
|
} else {
|
|
rule_queue.extend(block.0.iter());
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
Block(ref ruleblock) => {
|
|
rule_queue.extend(ruleblock.0.iter());
|
|
}
|
|
}
|
|
}
|
|
Ok((outcome, actions))
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
use super::*;
|
|
use crate::Envelope;
|
|
|
|
const MESSAGE_A: &str = r#"Date: Tue, 1 Apr 1997 09:06:31 -0800 (PST)
|
|
From: coyote@desert.example.org
|
|
To: roadrunner@acme.example.com
|
|
Subject: I have a present for you
|
|
|
|
Look, I'm sorry about the whole anvil thing, and I really
|
|
didn't mean to try and drop it on you from the top of the
|
|
cliff. I want to try to make it up to you. I've got some
|
|
great birdseed over here at my place--top of the line
|
|
stuff--and if you come by, I'll have it all wrapped up
|
|
for you. I'm really sorry for all the problems I've caused
|
|
for you over the years, but I know we can work this out.
|
|
--
|
|
Wile E. Coyote "Super Genius" coyote@desert.example.org"#;
|
|
|
|
#[test]
|
|
fn test_sieve_discard_keep() {
|
|
let f = SieveFilter::from_str(r#"keep;"#).unwrap();
|
|
let envelope =
|
|
Envelope::from_bytes(MESSAGE_A.as_bytes(), None).expect("Could not parse mail");
|
|
assert_eq!((Outcome::Keep, vec![]), envelope.passthrough(&f).unwrap());
|
|
}
|
|
}
|