// // meli - sieve module // // Copyright 2022 Emmanouil 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 . // // SPDX-License-Identifier: EUPL-1.2 OR GPL-3.0-or-later use super::*; macro_rules! parse_action { ($parser_name:ident, $lit:literal, $t:ty, $action:expr) => { pub fn $parser_name<'a>() -> impl Parser<'a, $t> { move |input| { map( ws(right(match_literal_anycase($lit), ws(match_literal(";")))), |_| $action, ) .parse(input) } } }; ($parser_name:ident, $lit:literal, $t:ty, $action:expr, $argument:ident) => { pub fn $parser_name<'a>() -> impl Parser<'a, $t> { move |input| { map( ws(right( parse_token($lit), left(ws(string()), ws(parse_token(";"))), )), |$argument| $action, ) .parse(input) } } }; } parse_action! { parse_sieve_keep, "keep", ActionCommand, ActionCommand::Keep } parse_action! { parse_sieve_discard, "discard", ActionCommand, ActionCommand::Discard } parse_action! { parse_sieve_stop, "stop", ControlCommand, ControlCommand::Stop } parse_action! { parse_sieve_fileinto, "fileinto", ActionCommand, ActionCommand::Fileinto { mailbox }, mailbox } parse_action! { parse_sieve_redirect, "redirect", ActionCommand, ActionCommand::Redirect { address }, address } #[inline(always)] pub fn parse_token<'a>(literal: &'static str) -> impl Parser<'a, ()> { move |input| map(ws(match_literal_anycase(literal)), |_| ()).parse(input) } #[inline(always)] fn ws_inner<'a>() -> impl Parser<'a, ()> { move |input: &'a str| { let mut offset = 0; let input_b = input.as_bytes(); while offset < input_b.len() { while offset < input_b.len() && [b' ', b'\t', b'\n', b'\r'].contains(&input_b[offset]) { offset += 1; } if offset >= input_b.len() { break; } if input_b[offset] == b'#' { while offset < input_b.len() && !input[offset..].starts_with("\r\n") && !input[offset..].starts_with('\n') { offset += 1; } if offset >= input_b.len() { break; } if input[offset..].starts_with("\r\n") { offset += 2; } else if input[offset..].starts_with('\n') { offset += 1; } } else if input[offset..].starts_with("/*") { while offset < input_b.len() && !input[offset..].starts_with("*/") { offset += 1; } if offset >= input_b.len() { break; } if input[offset..].starts_with("*/") { offset += 2; } } else { break; } } Ok((&input[offset..], ())) } } pub fn ws<'a, P, A>(parser: P) -> impl Parser<'a, A> where P: Parser<'a, A>, { move |input1| { let (input2, ()) = ws_inner().parse(input1)?; let (input3, res) = parser.parse(input2)?; let (input4, ()) = ws_inner().parse(input3)?; Ok((input4, res)) } } // string = quoted-string / multi-line // // quoted-other = "\" octet-not-qspecial // ; represents just the octet-no-qspecial // ; character. SHOULD NOT be used // quoted-safe = CRLF / octet-not-qspecial // ; either a CRLF pair, OR a single octet other // ; than NUL, CR, LF, double-quote, or backslash // quoted-special = "\" (DQUOTE / "\") // ; represents just a double-quote or backslash // quoted-string = DQUOTE quoted-text DQUOTE // quoted-text = *(quoted-safe / quoted-special / quoted-other) pub fn string<'a>() -> impl Parser<'a, String> { #[inline(always)] fn quoted_text<'a>() -> impl Parser<'a, String> { move |input: &'a str| { let mut offset = 0; let mut unescape_dquote: bool = false; let mut unescape_slash: bool = false; while offset < input.len() { if input.len() >= offset + 2 { if input.starts_with("\r\n") { offset += 2; } else if input.starts_with("\\\"") { unescape_dquote = true; offset += 2; } else if input.starts_with("\\\\") { unescape_slash = true; offset += 2; } } // a single octet other ; than NUL, CR, LF, double-quote, or backslash if [b'\x00', b'\r', b'\n', b'"', b'\\'].contains(&input.as_bytes()[offset]) { break; } offset += 1; } match (unescape_dquote, unescape_slash) { (false, false) => Ok((&input[offset..], input[..offset].to_string())), (true, false) => Ok((&input[offset..], input[..offset].replace("\\\"", "\""))), (false, true) => Ok((&input[offset..], input[..offset].replace("\\\\", "\\"))), (true, true) => Ok(( &input[offset..], input[..offset].replace("\\\"", "\"").replace("\\\\", "\\"), )), } } } #[inline(always)] fn quoted_string<'a>() -> impl Parser<'a, String> { delimited(parse_token("\""), quoted_text(), parse_token("\"")) } //fn multiline() -> impl Parser<'a, String> {} //either(quoted_string(), multiline()) quoted_string() } // number = 1*DIGIT [ QUANTIFIER ] // QUANTIFIER = "K" / "M" / "G" pub fn number<'a>() -> impl Parser<'a, u64> { map_res( pair( is_a(b"0123456789"), pred(any_char, |c| { ['k', 'm', 'g'].contains(&c.to_ascii_lowercase()) }), ), |(num_s, quant)| { Ok(match (num_s.parse::(), quant.to_ascii_lowercase()) { (Ok(num), 'k') => num * 1_000, (Ok(num), 'm') => num * 1_000_000, (Ok(num), 'g') => num * 1_000_000_000, _ => return Err(num_s), }) }, ) } pub fn parse_sieve_integer_operator<'a>() -> impl Parser<'a, (IntegerOperator, u64)> { move |input| { ws(pair( either( map(parse_token(":over"), |_| IntegerOperator::Over), map(parse_token(":under"), |_| IntegerOperator::Under), ), ws(number()), )) .parse(input) } } // ":comparator" pub fn parse_sieve_comparator<'a>() -> impl Parser<'a, CharacterOperator> { move |input| { ws(right( parse_token(":comparator"), ws(map_res(string(), |s| { if s == "i;octet" { Ok(CharacterOperator::Octet) } else if s == "i;ascii-casemap" { Ok(CharacterOperator::AsciiCasemap) } else { Err("invalid comparator") } })), )) .parse(input) } } // MATCH-TYPE = ":is" / ":contains" / ":matches" pub fn parse_sieve_match_type<'a>() -> impl Parser<'a, MatchOperator> { move |input| { either( map(parse_token(":is"), |_| MatchOperator::Is), either( map(parse_token(":contains"), |_| MatchOperator::Contains), map(parse_token(":matches"), |_| MatchOperator::Matches), ), ) .parse(input) } } /* string-list = "[" string *("," string) "]" / string ; if there is only a single string, the brackets ; are optional */ pub fn parse_string_list<'a>() -> impl Parser<'a, Vec> { move |input| { either( delimited( ws(parse_token("[")), separated_list0(string(), ws(parse_token(",")), false), ws(parse_token("]")), ), map(string(), |s| vec![s]), ) .parse(input) } } /* Usage: header [COMPARATOR] [MATCH-TYPE] * */ pub fn parse_sieve_header<'a>() -> impl Parser<'a, ConditionRule> { move |input| { map( ws(pair( right(parse_token("header"), move |input| { crate::permutation! { input, comparator, Option, opt(parse_sieve_comparator()), match_type, Option, opt(parse_sieve_match_type()) } }), pair(ws(parse_string_list()), ws(parse_string_list())), )), |((comparator, match_operator), (header_names, key_list))| ConditionRule::Header { comparator, match_operator, header_names, key_list, }, ) .parse(input) } } // ADDRESS-PART = ":localpart" / ":domain" / ":all" pub fn parse_sieve_address_type<'a>() -> impl Parser<'a, AddressOperator> { move |input| { either( map(parse_token(":localpart"), |_| AddressOperator::Localpart), either( map(parse_token(":domain"), |_| AddressOperator::Domain), map(parse_token(":all"), |_| AddressOperator::All), ), ) .parse(input) } } // address [COMPARATOR] [ADDRESS-PART] [MATCH-TYPE] // pub fn parse_sieve_address<'a>() -> impl Parser<'a, ConditionRule> { move |input| { map( ws(pair( right(parse_token("address"), move |input| { crate::permutation! { input, match_type, Option, opt(parse_sieve_match_type()), comparator, Option, opt(parse_sieve_comparator()), address_type, Option, opt(parse_sieve_address_type()) } }), pair(ws(parse_string_list()), ws(parse_string_list())), )), |((match_type, comparator, address_part), (header_list, key_list))| { ConditionRule::Address { comparator, address_part, match_type, header_list, key_list, } }, ) .parse(input) } } pub fn parse_sieve_test<'a>() -> impl Parser<'a, ConditionRule> { move |input| { either( either( map(parse_token("true"), |_| ConditionRule::Literal(true)), map(parse_token("false"), |_| ConditionRule::Literal(false)), ), either( either( map( right(ws(parse_token("exists")), ws(parse_string_list())), ConditionRule::Exists, ), map( right(ws(parse_token("size")), ws(parse_sieve_integer_operator())), |(operator, limit)| ConditionRule::Size { operator, limit }, ), ), either( either( map(right(ws(parse_token("not")), parse_sieve_test()), |cond| { ConditionRule::Not(Box::new(cond)) }), either(parse_sieve_header(), parse_sieve_address()), ), either( map( right(ws(parse_token("allof")), parse_test_list()), ConditionRule::AllOf, ), map( right(ws(parse_token("anyof")), parse_test_list()), ConditionRule::AnyOf, ), ), ), ), ) .parse(input) } } /* test-list = "(" test *("," test) ")" */ pub fn parse_test_list<'a>() -> impl Parser<'a, Vec> { move |input| { delimited( ws(parse_token("(")), separated_list0(ws(parse_sieve_test()), ws(parse_token(",")), false), ws(parse_token(")")), ) .parse(input) } } pub fn parse_sieve_rule<'a>() -> impl Parser<'a, Rule> { either( map( either( either(parse_sieve_stop(), parse_sieve_require()), parse_sieve_if(), ), Rule::Control, ), map( either( either(parse_sieve_keep(), parse_sieve_fileinto()), either(parse_sieve_redirect(), parse_sieve_discard()), ), Rule::Action, ), ) } pub fn parse_sieve_block<'a>() -> impl Parser<'a, RuleBlock> { move |input| { map( ws(delimited( parse_token("{"), ws(zero_or_more(parse_sieve_rule())), parse_token("}"), )), RuleBlock, ) .parse(input) } } pub fn parse_sieve_if<'a>() -> impl Parser<'a, ControlCommand> { either( map( pair( parse_sieve_if_bare(), ws(right(parse_token("else"), ws(parse_sieve_block()))), ), |(ifbare, else_)| match ifbare { ControlCommand::If { condition, elsif, else_: _, } => ControlCommand::If { condition, elsif, else_: Some(else_), }, _ => unreachable!(), }, ), parse_sieve_if_bare(), ) } pub fn parse_sieve_if_bare<'a>() -> impl Parser<'a, ControlCommand> { either( map( pair( ws(pair( ws(right(parse_token("if"), ws(parse_sieve_test()))), ws(parse_sieve_block()), )), ws(pair( ws(right(parse_token("elsif"), ws(parse_sieve_test()))), ws(parse_sieve_block()), )), ), |(condition, elsif)| ControlCommand::If { condition, elsif: Some(elsif), else_: None, }, ), map( pair( ws(right(parse_token("if"), ws(parse_sieve_test()))), ws(parse_sieve_block()), ), |(cond, block)| ControlCommand::If { condition: (cond, block), elsif: None, else_: None, }, ), ) } pub fn parse_sieve_require<'a>() -> impl Parser<'a, ControlCommand> { move |input| { right( ws(parse_token("require")), ws(left( map(parse_string_list(), |string_list| { ControlCommand::Require(string_list) }), ws(parse_token(";")), )), ) .parse(input) } } pub fn parse_sieve<'a>() -> impl Parser<'a, Vec> { ws(zero_or_more(ws(parse_sieve_rule()))) }