forked from meli/meli
1
Fork 0

Compare commits

...

1 Commits

Author SHA1 Message Date
Manos Pitsidianakis ac04f1677c melib: add basic Sieve parser and interpreter 2023-01-02 20:40:37 +02:00
5 changed files with 1835 additions and 958 deletions

View File

@ -54,7 +54,7 @@ mailin-embedded = { version = "0.7", features = ["rtls"] }
stderrlog = "^0.5"
[features]
default = ["unicode_algorithms", "imap_backend", "maildir_backend", "mbox_backend", "vcard", "sqlite3", "smtp", "deflate_compression"]
default = ["unicode_algorithms", "imap_backend", "maildir_backend", "mbox_backend", "vcard", "sqlite3", "smtp", "deflate_compression", "sieve"]
debug-tracing = []
deflate_compression = ["flate2", ]
@ -71,3 +71,4 @@ sqlite3 = ["rusqlite", ]
tls = ["native-tls"]
unicode_algorithms = ["unicode-segmentation"]
vcard = []
sieve = []

View File

@ -110,6 +110,7 @@ pub use addressbook::*;
pub mod backends;
pub use backends::*;
mod collection;
#[cfg(feature = "sieve")]
pub mod sieve;
pub use collection::*;
pub mod conf;

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,531 @@
/*
* 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/>.
*/
//! Types representing the Sieve's language abstract syntax tree.
use super::Capability;
use std::collections::HashSet;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
/// A list of [rules](Rule).
pub struct RuleBlock(pub Vec<Rule>);
/*
MATCH-TYPE =/ COUNT / VALUE
COUNT = ":count" relational-match
VALUE = ":value" relational-match
relational-match = DQUOTE
("gt" / "ge" / "lt" / "le" / "eq" / "ne") DQUOTE
; "gt" means "greater than", the C operator ">".
; "ge" means "greater than or equal", the C operator ">=".
; "lt" means "less than", the C operator "<".
; "le" means "less than or equal", the C operator "<=".
; "eq" means "equal to", the C operator "==".
; "ne" means "not equal to", the C operator "!=".
*/
/// Sieve action commands.
#[derive(Debug, PartialEq, Eq, Clone, Hash)]
pub enum ActionCommand {
/// `keep`
Keep,
/// `fileinto`
FileInto {
///
mailbox: String,
},
/// `redirect`
Redirect {
///
address: String,
},
/// `discard`
Discard,
}
#[derive(Debug, PartialEq, Eq, Clone, Hash)]
/// Sieve control commands.
pub enum ControlCommand {
/// `stop`
///
/// > The "stop" action ends all processing. If the implicit keep has not
/// > been cancelled, then it is taken.
Stop,
/// `require`
Require(Vec<String>),
/// an `if`-`elsif`-`else` condition.
If {
///
condition: (ConditionRule, RuleBlock),
///
elsif: Option<(ConditionRule, RuleBlock)>,
///
else_: Option<RuleBlock>,
},
}
#[derive(Debug, PartialEq, Eq, Clone, Hash)]
/// Sieve rule commands.
pub enum Rule {
/// A list of rules enclosed by braces.
Block(RuleBlock),
/// An action command.
Action(ActionCommand),
/// A control command.
Control(ControlCommand),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
/// Specifies which part of an e-mail address to examine in conditionals..
pub enum AddressOperator {
/// The entire address.
All,
/// The localpart (the part before the `@` character).
Localpart,
/// The domain (the part after the `@` character).
Domain,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
/// Defines what integer operation to perform.
pub enum IntegerOperator {
/// Over
Over,
/// Under
Under,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
/// RFC 5231 Sieve Email Filtering: Relational Extension
pub enum RelationalMatch {
/// "gt" means "greater than", the C operator ">".
Gt,
/// "ge" means "greater than or equal", the C operator ">=".
Ge,
/// "lt" means "less than", the C operator "<".
Lt,
/// "le" means "less than or equal", the C operator "<=".
Le,
/// "eq" means "equal to", the C operator "==".
Eq,
/// "ne" means "not equal to", the C operator "!=".
Ne,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
/// Defines what match operation to perform.
pub enum MatchOperator {
/// Exact equality.
Is,
/// Pattern match.
Matches,
/// Content query.
Contains,
/// Count query.
Count(RelationalMatch),
/// Numerical value query.
Value(RelationalMatch),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
/// Defines how to compare strings/characters.
pub enum CharacterOperator {
/// `i;octet,` compares as raw bytes.
Octet,
/// `i;ascii-casemap` compares case-insensitive.
AsciiCasemap,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
/// Part of datetime to examine.
pub enum ZoneRule {
/// "year" => the year, "0000" .. "9999".
Year,
/// "month" => the month, "01" .. "12".
Month,
/// "day" => the day, "01" .. "31".
Day,
/// "date" => the date in "yyyy-mm-dd" format.
Date,
/// "julian" => the Modified Julian Day, that is, the date
/// expressed as an integer number of days since
/// 00:00 UTC on November 17, 1858 (using the Gregorian
/// calendar). This corresponds to the regular
/// Julian Day minus 2400000.5. Sample routines to
/// convert to and from modified Julian dates are
/// given in Appendix A.
Julian,
/// "hour" => the hour, "00" .. "23".
Hour,
/// "minute" => the minute, "00" .. "59".
Minute,
/// "second" => the second, "00" .. "60".
Second,
/// "time" => the time in "hh:mm:ss" format.
Time,
/// "iso8601" => the date and time in restricted ISO 8601 format.
Iso8601,
/// "std11" => the date and time in a format appropriate
/// for use in a Date: header field [RFC2822].
Std11,
/// "zone" => the time zone in use. If the user specified a
///time zone with ":zone", "zone" will
///contain that value. If :originalzone is specified
///this value will be the original zone specified
///in the date-time value. If neither argument is
///specified the value will be the server's default
///time zone in offset format "+hhmm" or "-hhmm". An
///offset of 0 (Zulu) always has a positive sign.
Zone,
/// "weekday" => the day of the week expressed as an integer between "0" and "6". "0" is Sunday, "1" is Monday, etc.
Weekday,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
/// Condition rules.
pub enum ConditionRule {
/// Logical OR operation.
AnyOf(Vec<ConditionRule>),
/// Logical AND operation.
AllOf(Vec<ConditionRule>),
/// Header values exist.
Exists(Vec<String>),
/// Header value check.
Header {
///
comparator: Option<CharacterOperator>,
///
match_type: Option<MatchOperator>,
///
header_names: Vec<String>,
///
key_list: Vec<String>,
},
/// Date value check.
Date {
///
comparator: Option<CharacterOperator>,
///
match_type: Option<MatchOperator>,
///
zone: ZoneRule,
///
header_name: String,
///
date_part: String,
///
key_list: Vec<String>,
},
/// Address value check.
Address {
///
comparator: Option<CharacterOperator>,
///
address_part: Option<AddressOperator>,
///
match_type: Option<MatchOperator>,
///
header_list: Vec<String>,
///
key_list: Vec<String>,
},
/// Test envelope ("envelope" capability).
Envelope {
///
comparator: Option<CharacterOperator>,
///
address_part: Option<AddressOperator>,
///
match_type: Option<MatchOperator>,
///
envelope_part: Vec<String>,
///
key_list: Vec<String>,
},
/// Invert a conditional.
Not(Box<ConditionRule>),
/// Check the size of an e-mail.
Size {
///
operator: IntegerOperator,
///
limit: u64,
},
/// Literal `true` or `false`.
Literal(bool),
}
/// Returns what capabilities an AST item requires, if any.
pub trait RequiredCapabilities {
fn requires(&self) -> Option<HashSet<Capability>> {
None
}
}
impl RequiredCapabilities for ActionCommand {
fn requires(&self) -> Option<HashSet<Capability>> {
if matches!(self, ActionCommand::FileInto { .. }) {
Some(HashSet::from([Capability::FileInto]))
} else {
None
}
}
}
impl RequiredCapabilities for ConditionRule {
fn requires(&self) -> Option<HashSet<Capability>> {
macro_rules! opt_map {
($id:ident) => {
$id.as_ref().and_then(RequiredCapabilities::requires)
};
}
match self {
ConditionRule::Address {
comparator,
match_type,
address_part: _,
header_list: _,
key_list: _,
}
| ConditionRule::Header {
comparator,
match_type,
header_names: _,
key_list: _,
} => {
let ret = IntoIterator::into_iter([opt_map!(comparator), opt_map!(match_type)])
.filter_map(std::convert::identity)
.flatten()
.collect::<HashSet<Capability>>();
if ret.is_empty() {
None
} else {
Some(ret)
}
}
ConditionRule::Date {
comparator,
match_type,
zone: _,
header_name: _,
date_part: _,
key_list: _,
} => {
let ret = IntoIterator::into_iter([opt_map!(comparator), opt_map!(match_type)])
.filter_map(std::convert::identity)
.flatten()
.chain(Some(Capability::Date).into_iter())
.collect::<HashSet<Capability>>();
Some(ret)
}
ConditionRule::Envelope {
comparator,
match_type,
address_part: _,
envelope_part: _,
key_list: _,
} => {
let ret = IntoIterator::into_iter([opt_map!(comparator), opt_map!(match_type)])
.filter_map(std::convert::identity)
.flatten()
.chain(Some(Capability::Envelope).into_iter())
.collect::<HashSet<Capability>>();
Some(ret)
}
ConditionRule::Not(ref inner) => inner.requires(),
ConditionRule::AnyOf(ref vec) | ConditionRule::AllOf(ref vec) => {
let ret = vec
.iter()
.filter_map(RequiredCapabilities::requires)
.flatten()
.collect::<HashSet<Capability>>();
if ret.is_empty() {
None
} else {
Some(ret)
}
}
ConditionRule::Literal(_) | ConditionRule::Size { .. } | ConditionRule::Exists(_) => {
None
}
}
}
}
impl RequiredCapabilities for MatchOperator {
fn requires(&self) -> Option<HashSet<Capability>> {
if matches!(self, MatchOperator::Count(_) | MatchOperator::Value(_)) {
Some(HashSet::from([Capability::Relational]))
} else {
None
}
}
}
impl RequiredCapabilities for CharacterOperator {}
impl RequiredCapabilities for Rule {
fn requires(&self) -> Option<HashSet<Capability>> {
match self {
Rule::Block(bl) => bl.requires(),
Rule::Action(cmd) => cmd.requires(),
Rule::Control(cmd) => cmd.requires(),
}
}
}
impl RequiredCapabilities for RuleBlock {
fn requires(&self) -> Option<HashSet<Capability>> {
let ret = self
.0
.iter()
.filter_map(RequiredCapabilities::requires)
.flatten()
.collect::<HashSet<Capability>>();
if ret.is_empty() {
None
} else {
Some(ret)
}
}
}
impl RequiredCapabilities for ControlCommand {
fn requires(&self) -> Option<HashSet<Capability>> {
match self {
ControlCommand::Stop | ControlCommand::Require(_) => None,
ControlCommand::If {
condition: (cond, ruleblock),
elsif,
else_,
} => {
let ret = else_
.as_ref()
.into_iter()
.filter_map(RequiredCapabilities::requires)
.chain(elsif.as_ref().into_iter().flat_map(|(cond, ruleblock)| {
cond.requires()
.into_iter()
.chain(ruleblock.requires().into_iter())
}))
.chain(cond.requires().into_iter())
.chain(ruleblock.requires().into_iter())
.flatten()
.collect::<HashSet<Capability>>();
if ret.is_empty() {
None
} else {
Some(ret)
}
}
}
}
}
#[cfg(test)]
mod test {
use std::collections::HashSet;
use std::iter::FromIterator;
use super::*;
use super::ActionCommand::*;
use super::AddressOperator::*;
// use super::CharacterOperator::*;
use super::ConditionRule::*;
use super::ControlCommand::*;
// use super::IntegerOperator::*;
use super::MatchOperator::*;
use super::Rule::*;
use super::RuleBlock;
#[test]
fn test_sieve_capabilities_detect() {
let cond = Envelope {
comparator: None,
address_part: Some(All),
match_type: Some(Is),
envelope_part: ["from".to_string()].to_vec(),
key_list: ["tim@example.com".to_string()].to_vec(),
};
assert_eq!(
Header {
comparator: None,
match_type: Some(Contains),
header_names: ["from".to_string()].to_vec(),
key_list: ["coyote".to_string()].to_vec()
}
.requires(),
None
);
assert_eq!(
Action(FileInto {
mailbox: "INBOX".to_string()
})
.requires()
.unwrap(),
HashSet::from([Capability::FileInto])
);
assert_eq!(
cond.requires().unwrap(),
HashSet::from([Capability::Envelope])
);
assert_eq!(
HashSet::from_iter(
Control(If {
condition: (
Envelope {
comparator: None,
address_part: Some(All),
match_type: Some(Is),
envelope_part: ["from".to_string()].to_vec(),
key_list: ["tim@example.com".to_string()].to_vec()
},
RuleBlock([Action(Discard)].to_vec())
),
elsif: Some((
Header {
comparator: None,
match_type: Some(Contains),
header_names: ["subject".to_string()].to_vec(),
key_list: ["$$$".to_string()].to_vec()
},
RuleBlock([Action(Discard)].to_vec())
)),
else_: Some(RuleBlock(
[Action(FileInto {
mailbox: "INBOX".to_string()
})]
.to_vec()
))
})
.requires()
.unwrap()
.into_iter()
),
HashSet::from([Capability::FileInto, Capability::Envelope])
);
}
}

File diff suppressed because it is too large Load Diff