This commit is contained in:
Mauro D
2023-08-18 19:48:57 +02:00
parent c8c32f3198
commit 016a5bde18
13 changed files with 701 additions and 342 deletions

View File

@@ -1,6 +1,12 @@
use std::{collections::HashMap, fmt::Display};
use std::{collections::HashMap, fmt::Display, iter::Peekable, str::Chars};
use super::Token;
use super::{Comparator, Logical, Operation, Token};
// Parse a meta expression into a list of tokens that can be easily
// converted into a Sieve test.
// The parser is not very robust but works on all SpamAssassin meta expressions.
// It might be a good idea in the future to instead build a parse tree and
// then convert that into a Sieve expression.
#[derive(Debug, Clone, Default)]
pub struct MetaExpression {
@@ -11,7 +17,7 @@ pub struct MetaExpression {
#[derive(Debug, Clone)]
pub struct TokenDepth {
token: Token,
pub token: Token,
depth: u32,
prefix: Vec<Token>,
}
@@ -40,15 +46,9 @@ impl MetaExpression {
if !buf.is_empty() {
let token = Token::from(buf);
buf = String::new();
if !seen_comp
&& matches!(
iter.clone()
.find(|t| { ['&', '|', '>', '<', '='].contains(t) }),
None | Some('&' | '|')
)
{
if !seen_comp && !meta.has_comparator(iter.clone()) {
meta.push(token);
meta.push(Token::Gt);
meta.push(Token::Comparator(Comparator::Gt));
meta.push(Token::Number(0));
seen_comp = true;
} else {
@@ -60,7 +60,7 @@ impl MetaExpression {
'&' => {
seen_comp = false;
if matches!(iter.next(), Some('&')) {
meta.push(Token::And);
meta.push(Token::Logical(Logical::And));
} else {
eprintln!("Warning: Single & in meta expression {expr}",);
}
@@ -68,24 +68,24 @@ impl MetaExpression {
'|' => {
seen_comp = false;
if matches!(iter.next(), Some('|')) {
meta.push(Token::Or);
meta.push(Token::Logical(Logical::Or));
} else {
eprintln!("Warning: Single | in meta expression {expr}",);
}
}
'!' => {
seen_comp = false;
meta.push(Token::Not)
meta.push(Token::Logical(Logical::Not))
}
'=' => {
seen_comp = true;
meta.push(match iter.next() {
Some('=') => Token::Eq,
Some('>') => Token::Ge,
Some('<') => Token::Le,
Some('=') => Token::Comparator(Comparator::Eq),
Some('>') => Token::Comparator(Comparator::Ge),
Some('<') => Token::Comparator(Comparator::Le),
_ => {
eprintln!("Warning: Single = in meta expression {expr}",);
Token::Eq
Token::Comparator(Comparator::Eq)
}
});
}
@@ -94,9 +94,9 @@ impl MetaExpression {
meta.push(match iter.peek() {
Some('=') => {
iter.next();
Token::Ge
Token::Comparator(Comparator::Ge)
}
_ => Token::Gt,
_ => Token::Comparator(Comparator::Gt),
})
}
'<' => {
@@ -104,9 +104,9 @@ impl MetaExpression {
meta.push(match iter.peek() {
Some('=') => {
iter.next();
Token::Le
Token::Comparator(Comparator::Le)
}
_ => Token::Lt,
_ => Token::Comparator(Comparator::Lt),
})
}
'(' => meta.push(Token::OpenParen),
@@ -119,9 +119,9 @@ impl MetaExpression {
meta.push(Token::CloseParen)
}
'+' => meta.push(Token::Add),
'*' => meta.push(Token::Multiply),
'/' => meta.push(Token::Divide),
'+' => meta.push(Token::Operation(Operation::Add)),
'*' => meta.push(Token::Operation(Operation::Multiply)),
'/' => meta.push(Token::Operation(Operation::Divide)),
' ' => {}
_ => {
eprintln!("Warning: Invalid character {ch} in meta expression {expr}");
@@ -139,7 +139,7 @@ impl MetaExpression {
if !buf.is_empty() {
meta.push(Token::from(buf));
if !seen_comp {
meta.push(Token::Gt);
meta.push(Token::Comparator(Comparator::Gt));
meta.push(Token::Number(0));
}
}
@@ -148,7 +148,7 @@ impl MetaExpression {
meta
}
fn push(&mut self, token: Token) {
fn push(&mut self, mut token: Token) {
let pos = self.tokens.len();
let depth_range = self
.depth_range
@@ -182,35 +182,60 @@ impl MetaExpression {
self.depth = self.depth.saturating_sub(1);
depth = self.depth;
}
Token::Or | Token::And => {
let start_prefix = &mut self.tokens[depth_range.start].prefix;
if !start_prefix.contains(&Token::And) && !start_prefix.contains(&Token::Or) {
start_prefix.insert(0, token.clone());
}
depth_range.logic_end = true;
if let Some((pos, is_static)) = depth_range.expr_end.take() {
self.tokens[pos + 2]
.prefix
.push(Token::BeginExpression(is_static));
prefix.push(Token::EndExpression(is_static));
Token::Logical(op) => {
if self
.tokens
.iter()
.any(|t| matches!(t.token, Token::Comparator(_)) && t.depth < depth)
{
token = Token::Operation(match op {
Logical::And => Operation::And,
Logical::Or => Operation::Or,
Logical::Not => Operation::Not,
});
if let Some((pos, true)) = depth_range.expr_end {
depth_range.expr_end = Some((pos, false));
}
} else if matches!(op, Logical::Or | Logical::And) {
let start_prefix = &mut self.tokens[depth_range.start].prefix;
if !start_prefix.contains(&Token::Logical(Logical::And))
&& !start_prefix.contains(&Token::Logical(Logical::Or))
{
start_prefix.insert(0, token.clone());
}
depth_range.logic_end = true;
if let Some((pos, is_static)) = depth_range.expr_end.take() {
self.tokens[pos + 2]
.prefix
.push(Token::BeginExpression(is_static));
prefix.push(Token::EndExpression(is_static));
}
}
}
Token::Lt | Token::Gt | Token::Eq | Token::Le | Token::Ge => {
Token::Comparator(_) => {
let mut is_static = true;
let mut start_pos = usize::MAX;
for (pos, token) in self.tokens.iter().enumerate().rev() {
for (pos, token) in self.tokens.iter_mut().enumerate().rev() {
if token.depth >= depth {
start_pos = pos;
match &token.token {
Token::And | Token::Or | Token::Not => {
start_pos += 1;
break;
Token::Logical(op) => {
if token.depth == depth {
start_pos += 1;
break;
} else {
is_static = false;
token.token = Token::Operation(match op {
Logical::And => Operation::And,
Logical::Or => Operation::Or,
Logical::Not => Operation::Not,
});
token.prefix.clear();
}
}
Token::OpenParen
| Token::CloseParen
| Token::Add
| Token::Multiply
| Token::Divide
| Token::Operation(_)
| Token::Tag(_) => {
is_static = false;
}
@@ -231,7 +256,7 @@ impl MetaExpression {
depth_range.expr_end = Some((pos, true));
}
}
Token::Tag(_) | Token::Add | Token::Multiply | Token::Divide => {
Token::Tag(_) | Token::Operation(_) => {
if let Some((pos, true)) = depth_range.expr_end {
depth_range.expr_end = Some((pos, false));
}
@@ -266,6 +291,47 @@ impl MetaExpression {
}
}
}
fn has_comparator(&self, iter: Peekable<Chars<'_>>) -> bool {
let mut d = self.depth;
let mut comp_depth = None;
let mut logic_depth = None;
for (pos, ch) in iter.enumerate() {
match ch {
'(' => {
d += 1;
}
')' => {
d = d.saturating_sub(1);
}
'>' | '<' | '=' => {
comp_depth = Some((pos, d));
break;
}
'&' | '|' => {
if d <= self.depth {
logic_depth = Some((pos, d));
}
}
_ => (),
}
}
println!("comp_depth: {comp_depth:?} {logic_depth:?}");
match (comp_depth, logic_depth) {
(Some((comp_pos, comp_depth)), Some((logic_pos, logic_depth))) => {
match comp_depth.cmp(&logic_depth) {
std::cmp::Ordering::Less => true,
std::cmp::Ordering::Equal => comp_pos < logic_pos,
_ => false,
}
}
(Some(_), None) => true,
_ => false,
}
}
}
impl From<String> for Token {
@@ -288,8 +354,12 @@ impl Display for MetaExpression {
}
match &token.token {
Token::And | Token::Or => f.write_str(", "),
Token::Gt | Token::Lt | Token::Eq | Token::Ge | Token::Le => f.write_str(" "),
Token::Logical(Logical::And) | Token::Logical(Logical::Or) => f.write_str(", "),
Token::Comparator(Comparator::Gt)
| Token::Comparator(Comparator::Lt)
| Token::Comparator(Comparator::Eq)
| Token::Comparator(Comparator::Ge)
| Token::Comparator(Comparator::Le) => f.write_str(" "),
_ => token.token.fmt(f),
}?;
}
@@ -303,27 +373,30 @@ impl Display for Token {
match self {
Token::Tag(t) => t.fmt(f),
Token::Number(n) => n.fmt(f),
Token::And => f.write_str("allof("),
Token::Or => f.write_str("anyof("),
Token::Not => f.write_str("not "),
Token::Lt | Token::Eq | Token::Ge | Token::Le | Token::Gt => {
f.write_str("string :")?;
match self {
Token::Eq => f.write_str("eq")?,
Token::Gt => f.write_str("gt")?,
Token::Lt => f.write_str("lt")?,
Token::Ge => f.write_str("ge")?,
Token::Le => f.write_str("gt")?,
Token::Logical(Logical::And) => f.write_str("allof("),
Token::Logical(Logical::Or) => f.write_str("anyof("),
Token::Logical(Logical::Not) => f.write_str("not "),
Token::Comparator(comp) => {
f.write_str("string :value \"")?;
match comp {
Comparator::Eq => f.write_str("eq")?,
Comparator::Gt => f.write_str("gt")?,
Comparator::Lt => f.write_str("lt")?,
Comparator::Ge => f.write_str("ge")?,
Comparator::Le => f.write_str("gt")?,
_ => unreachable!(),
}
f.write_str(" ")
f.write_str("\" :comparator \"i;ascii-numeric\" ")
}
Token::OpenParen => f.write_str("("),
Token::CloseParen => f.write_str(")"),
Token::Add => f.write_str(" + "),
Token::Multiply => f.write_str(" * "),
Token::Divide => f.write_str(" / "),
Token::Operation(Operation::Add) => f.write_str(" + "),
Token::Operation(Operation::Multiply) => f.write_str(" * "),
Token::Operation(Operation::Divide) => f.write_str(" / "),
Token::Operation(Operation::And) => f.write_str(" & "),
Token::Operation(Operation::Or) => f.write_str(" | "),
Token::Operation(Operation::Not) => f.write_str("!"),
Token::BeginExpression(is_static) => {
if *is_static {
f.write_str("\"")
@@ -363,17 +436,19 @@ mod test {
("__ML2 || __ML4", ""),
("(__AT_HOTMAIL_MSGID && (!__FROM_HOTMAIL_COM && !__FROM_MSN_COM && !__FROM_YAHOO_COM))", ""),
("(0)", ""),
("RAZOR2_CHECK + DCC_CHECK + PYZOR_CHECK > 1", ""),*/
("RAZOR2_CHECK + DCC_CHECK + PYZOR_CHECK > 1", ""),
("(SUBJECT_IN_BLOCKLIST)", ""),
("__HAS_MSGID && !(__SANE_MSGID || __MSGID_COMMENT)", ""),
("!__CTYPE_HTML && __X_MAILER_APPLEMAIL && (__MSGID_APPLEMAIL || __MIME_VERSION_APPLEMAIL)", ""),
("((__AUTO_GEN_MS||__AUTO_GEN_3||__AUTO_GEN_4) && !__XM_VBULLETIN && !__X_CRON_ENV)", ""),
("((__AUTO_GEN_MS||__AUTO_GEN_3||__AUTO_GEN_4) && !__XM_VBULLETIN && !__X_CRON_ENV)", ""),*/
("(__WEBMAIL_ACCT + __MAILBOX_FULL + (__TVD_PH_SUBJ_META || __TVD_PH_BODY_META) > 3)", ""),
] {
let meta = MetaExpression::from_meta(expr);
//println!("{:#?}", meta.tokens);
let result = meta.to_string();
//println!("{}", expected);
println!("{expr}");
println!("{}", result);
/*assert_eq!(

View File

@@ -1,10 +1,12 @@
use std::collections::HashMap;
use self::meta::MetaExpression;
pub mod meta;
pub mod spamassassin;
pub mod utils;
#[derive(Debug, Default)]
#[derive(Debug, Default, Clone)]
struct Rule {
name: String,
t: RuleType,
@@ -12,13 +14,16 @@ struct Rule {
description: HashMap<String, String>,
priority: i32,
flags: Vec<TestFlag>,
forward_score_pos: f64,
forward_score_neg: f64,
}
#[derive(Debug, Default)]
#[derive(Debug, Default, Clone)]
enum RuleType {
Header {
matches: HeaderMatches,
header: Header,
part: Vec<HeaderPart>,
if_unset: Option<String>,
pattern: String,
},
@@ -37,7 +42,7 @@ enum RuleType {
params: Vec<String>,
},
Meta {
tokens: Vec<Token>,
expr: MetaExpression,
},
#[default]
@@ -56,7 +61,7 @@ impl RuleType {
}
}
#[derive(Debug)]
#[derive(Debug, PartialEq, Eq, Clone)]
enum TestFlag {
Net,
Nice,
@@ -74,7 +79,7 @@ enum TestFlag {
DnsBlockRule(String),
}
#[derive(Debug, Default)]
#[derive(Debug, Default, PartialEq, Eq, Clone)]
enum Header {
#[default]
All,
@@ -82,13 +87,10 @@ enum Header {
AllExternal,
EnvelopeFrom,
ToCc,
Name {
name: String,
part: Vec<HeaderPart>,
},
Name(String),
}
#[derive(Debug, Default)]
#[derive(Debug, Default, Clone)]
enum HeaderMatches {
#[default]
Matches,
@@ -96,7 +98,7 @@ enum HeaderMatches {
Exists,
}
#[derive(Debug, Default)]
#[derive(Debug, Default, PartialEq, Eq, Clone)]
enum HeaderPart {
Name,
Addr,
@@ -108,23 +110,42 @@ enum HeaderPart {
pub enum Token {
Tag(String),
Number(u32),
Logical(Logical),
Comparator(Comparator),
Operation(Operation),
OpenParen,
CloseParen,
// Sieve specific
BeginExpression(bool),
EndExpression(bool),
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum Logical {
And,
Or,
Not,
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum Comparator {
Gt,
Lt,
Eq,
Ge,
Le,
OpenParen,
CloseParen,
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum Operation {
Add,
Multiply,
Divide,
// Sieve specific
BeginExpression(bool),
EndExpression(bool),
And,
Or,
Not,
}
impl Rule {
@@ -143,12 +164,39 @@ impl Rule {
impl Ord for Rule {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
match self.priority.cmp(&other.priority) {
std::cmp::Ordering::Equal => match self.score().partial_cmp(&other.score()).unwrap() {
std::cmp::Ordering::Equal => other.name.cmp(&self.name),
let this_score = self.score();
let other_score = other.score();
let this_is_negative = this_score < 0.0;
let other_is_negative = other_score < 0.0;
if this_is_negative != other_is_negative {
if this_is_negative {
std::cmp::Ordering::Less
} else {
std::cmp::Ordering::Greater
}
} else {
let this_priority = if this_score != 0.0 {
self.priority
} else {
9000
};
let other_priority = if other_score != 0.0 {
other.priority
} else {
9000
};
match this_priority.cmp(&other_priority) {
std::cmp::Ordering::Equal => {
match other_score.abs().partial_cmp(&this_score.abs()).unwrap() {
std::cmp::Ordering::Equal => other.name.cmp(&self.name),
x => x,
}
}
x => x,
},
x => x,
}
}
}
}

View File

@@ -1,8 +1,7 @@
use std::{
collections::{BTreeMap, BTreeSet, HashMap, HashSet},
default,
fmt::format,
fs,
collections::{BTreeMap, HashMap, HashSet},
fmt::{Display, Write},
fs::{self},
path::PathBuf,
};
@@ -12,7 +11,9 @@ use super::{
Header, HeaderMatches, HeaderPart, Rule, RuleType, TestFlag, Token, UnwrapResult,
};
static SUPPORTED_PLUGINS: [&str; 37] = [
const VERSION: f64 = 4.000000;
static IF_TRUE: [&str; 57] = [
"Mail::SpamAssassin::Plugin::DKIM",
"Mail::SpamAssassin::Plugin::SPF",
"Mail::SpamAssassin::Plugin::ASN",
@@ -49,9 +50,31 @@ static SUPPORTED_PLUGINS: [&str; 37] = [
"Mail::SpamAssassin::Plugin::VBounce",
"Mail::SpamAssassin::Plugin::WLBLEval",
"Mail::SpamAssassin::Plugin::WelcomeListSubject",
"Mail::SpamAssassin::Plugin::WhiteListSubject",
"Mail::SpamAssassin::Conf::feature_bayes_stopwords",
"Mail::SpamAssassin::Conf::feature_bug6558_free",
"Mail::SpamAssassin::Conf::feature_capture_rules",
"Mail::SpamAssassin::Conf::feature_dns_local_ports_permit_avoid",
"Mail::SpamAssassin::Conf::feature_originating_ip_headers",
"Mail::SpamAssassin::Conf::feature_registryboundaries",
"Mail::SpamAssassin::Conf::feature_welcomelist_blocklist",
"Mail::SpamAssassin::Conf::feature_yesno_takes_args",
"Mail::SpamAssassin::Conf::perl_min_version_5010000",
"Mail::SpamAssassin::Plugin::BodyEval::has_check_body_length",
"Mail::SpamAssassin::Plugin::DKIM::has_arc",
"Mail::SpamAssassin::Plugin::DecodeShortURLs::has_get",
"Mail::SpamAssassin::Plugin::DecodeShortURLs::has_short_url_redir",
"Mail::SpamAssassin::Plugin::MIMEEval::has_check_abundant_unicode_ratio",
"Mail::SpamAssassin::Plugin::MIMEEval::has_check_for_ascii_text_illegal",
"Mail::SpamAssassin::Plugin::SPF::has_check_for_spf_errors",
"Mail::SpamAssassin::Plugin::URIDNSBL::has_tflags_domains_only",
"Mail::SpamAssassin::Plugin::URIDNSBL::has_uridnsbl_for_a",
"Mail::SpamAssassin::Plugin::ASN::has_check_asn",
"Mail::SpamAssassin::Conf::compat_welcomelist_blocklist",
"Mail::SpamAssassin::Conf::feature_dns_block_rule",
];
static IF_FALSE: [&str; 1] = ["Mail::SpamAssassin::Plugin::WhiteListSubject"];
static SUPPORTED_FUNCTIONS: [&str; 162] = [
"check_abundant_unicode_ratio",
"check_access_database",
@@ -217,59 +240,6 @@ static SUPPORTED_FUNCTIONS: [&str; 162] = [
"tvd_vertical_words",
];
static IF_TRUE: [&str; 25] = [
"!(!plugin(Mail::SpamAssassin::Plugin::DKIM))",
"(version >= 3.003000)",
"(version >= 3.004000)",
"(version >= 3.004001)",
"(version >= 3.004002)",
"(version >= 3.004003)",
"(version >= 4.000000)",
"can(Mail::SpamAssassin::Conf::feature_bayes_stopwords)",
"can(Mail::SpamAssassin::Conf::feature_bug6558_free)",
"can(Mail::SpamAssassin::Conf::feature_capture_rules)",
"can(Mail::SpamAssassin::Conf::feature_dns_local_ports_permit_avoid)",
"can(Mail::SpamAssassin::Conf::feature_originating_ip_headers)",
"can(Mail::SpamAssassin::Conf::feature_registryboundaries)",
"can(Mail::SpamAssassin::Conf::feature_welcomelist_blocklist)",
"can(Mail::SpamAssassin::Conf::feature_yesno_takes_args)",
"can(Mail::SpamAssassin::Conf::perl_min_version_5010000)",
"can(Mail::SpamAssassin::Plugin::BodyEval::has_check_body_length)",
"can(Mail::SpamAssassin::Plugin::DKIM::has_arc)",
"can(Mail::SpamAssassin::Plugin::DecodeShortURLs::has_get)",
"can(Mail::SpamAssassin::Plugin::DecodeShortURLs::has_short_url_redir)",
"can(Mail::SpamAssassin::Plugin::MIMEEval::has_check_abundant_unicode_ratio)",
"can(Mail::SpamAssassin::Plugin::MIMEEval::has_check_for_ascii_text_illegal)",
"can(Mail::SpamAssassin::Plugin::SPF::has_check_for_spf_errors)",
"can(Mail::SpamAssassin::Plugin::URIDNSBL::has_tflags_domains_only)",
"can(Mail::SpamAssassin::Plugin::URIDNSBL::has_uridnsbl_for_a)",
];
static IF_FALSE: [&str; 22] = [
"(version < 4.000000)",
"!((version >= 3.003000))",
"!((version >= 3.004000))",
"can(Mail::SpamAssassin::Conf::feature_dns_block_rule)",
"!plugin(Mail::SpamAssassin::Plugin::BodyEval)",
"!plugin(Mail::SpamAssassin::Plugin::DKIM)",
"!plugin(Mail::SpamAssassin::Plugin::FreeMail)",
"!plugin(Mail::SpamAssassin::Plugin::HTMLEval)",
"!plugin(Mail::SpamAssassin::Plugin::HeaderEval)",
"!plugin(Mail::SpamAssassin::Plugin::ImageInfo)",
"!plugin(Mail::SpamAssassin::Plugin::MIMEEval)",
"!plugin(Mail::SpamAssassin::Plugin::MIMEHeader)",
"!plugin(Mail::SpamAssassin::Plugin::ReplaceTags)",
"!plugin(Mail::SpamAssassin::Plugin::SPF)",
"!plugin(Mail::SpamAssassin::Plugin::WLBLEval)",
"!plugin(Mail::SpamAssassin::Plugin::WelcomeListSubject)",
"!(can(Mail::SpamAssassin::Conf::feature_bug6558_free))",
"!(can(Mail::SpamAssassin::Plugin::ASN::has_check_asn))",
"!(can(Mail::SpamAssassin::Plugin::BodyEval::has_check_body_length))",
"!can(Mail::SpamAssassin::Conf::compat_welcomelist_blocklist)",
"!can(Mail::SpamAssassin::Conf::feature_welcomelist_blocklist)",
"!can(Mail::SpamAssassin::Plugin::DecodeShortURLs::has_short_url_redir)",
];
pub fn import_spamassassin(path: PathBuf, extension: String, do_warn: bool, validate_regex: bool) {
let mut paths: Vec<_> = fs::read_dir(&path)
.unwrap_result("read directory")
@@ -285,7 +255,7 @@ pub fn import_spamassassin(path: PathBuf, extension: String, do_warn: bool, vali
let mut replace_rules: HashSet<String> = HashSet::new();
let mut tags: HashMap<String, String> = HashMap::new();
let mut unsupported_plugins: BTreeMap<String, HashMap<PathBuf, Vec<String>>> = BTreeMap::new();
let mut unsupported_ifs: BTreeMap<String, HashMap<PathBuf, Vec<String>>> = BTreeMap::new();
let mut unsupported_commands: BTreeMap<String, HashMap<PathBuf, Vec<String>>> = BTreeMap::new();
for path in paths {
@@ -345,7 +315,7 @@ pub fn import_spamassassin(path: PathBuf, extension: String, do_warn: bool, vali
last_ch = ch;
}
let (cmd, params) = line
let (cmd, mut params) = line
.split_once(' ')
.map(|(k, v)| (k.trim(), v.trim()))
.unwrap_or((line.as_str().trim(), ""));
@@ -358,10 +328,10 @@ pub fn import_spamassassin(path: PathBuf, extension: String, do_warn: bool, vali
match cmd {
"ifplugin" => {
is_supported_stack.push(is_supported_block);
is_supported_block = SUPPORTED_PLUGINS.contains(&params);
is_supported_block = IF_TRUE.contains(&params);
if !is_supported_block {
unsupported_plugins
if !is_supported_block && !IF_FALSE.contains(&params) {
unsupported_ifs
.entry(params.to_string())
.or_default()
.entry(path.clone())
@@ -370,14 +340,79 @@ pub fn import_spamassassin(path: PathBuf, extension: String, do_warn: bool, vali
}
}
"if" => {
is_supported_stack.push(is_supported_block);
is_supported_block = IF_TRUE.contains(&params);
if !is_supported_block && !IF_FALSE.contains(&params) {
eprintln!(
"Warning: Unknown if condition on {}, line {}",
path.display(),
line_num
);
let _params = params;
let mut is_not = false;
loop {
let mut has_changes = false;
if let Some(expr) = params.strip_prefix('!') {
is_not = !is_not;
params = expr.trim();
has_changes = true;
}
if let Some(expr) =
params.strip_prefix('(').and_then(|v| v.strip_suffix(')'))
{
params = expr.trim();
has_changes = true;
}
if let Some(expr) = params
.strip_prefix("can(")
.or_else(|| params.strip_prefix("plugin("))
.and_then(|v| v.strip_suffix(')'))
{
params = expr.trim();
has_changes = true;
}
if !has_changes {
break;
}
}
if let Some(version) = params.strip_prefix("version ") {
is_supported_stack.push(is_supported_block);
let (op, version) = version.trim().split_once(' ').unwrap_or(("", version));
let version = version
.parse::<f64>()
.unwrap_result("Failed to parse version");
match op {
"<" => {
is_supported_block = (VERSION < version) ^ is_not;
}
"<=" => {
is_supported_block = (VERSION <= version) ^ is_not;
}
">" => {
is_supported_block = (VERSION > version) ^ is_not;
}
">=" => {
is_supported_block = (VERSION >= version) ^ is_not;
}
"==" => {
is_supported_block = (VERSION == version) ^ is_not;
}
"!=" => {
is_supported_block = (VERSION != version) ^ is_not;
}
_ => {
eprintln!(
"Warning: Invalid version operator on {}, line {}",
path.display(),
line_num
);
}
}
} else {
is_supported_stack.push(is_supported_block);
is_supported_block = IF_TRUE.contains(&params);
if !is_supported_block && !IF_FALSE.contains(&params) {
unsupported_ifs
.entry(params.to_string())
.or_default()
.entry(path.clone())
.or_default()
.push(line_num.to_string());
}
is_supported_block ^= is_not;
}
}
"endif" => {
@@ -410,7 +445,7 @@ pub fn import_spamassassin(path: PathBuf, extension: String, do_warn: bool, vali
if let Some((name, value)) =
params.split_once(' ').map(|(k, v)| (k.trim(), v.trim()))
{
let mut rule = rules.entry(name.to_string()).or_default();
let rule = rules.entry(name.to_string()).or_default();
if let Some(function) = value.strip_prefix("eval:") {
if let Some((fnc_name, params_)) = function
@@ -478,17 +513,33 @@ pub fn import_spamassassin(path: PathBuf, extension: String, do_warn: bool, vali
if let Some(exists) = value.strip_prefix("exists:") {
rule.t = RuleType::Header {
matches: HeaderMatches::Exists,
header: Header::Name {
name: exists.to_string(),
part: vec![],
},
header: Header::Name(exists.to_string()),
if_unset: None,
pattern: String::new(),
part: vec![],
};
} else if let Some((header, (op, mut pattern))) = value
.split_once(' ')
.and_then(|(k, v)| (k.trim(), v.trim().split_once(' ')?).into())
{
let (header, part) = header.split_once(':').unwrap_or((header, ""));
let part = part.split(':').filter_map(|part| {
match part.trim() {
"name" => {Some(HeaderPart::Name)}
"addr" => {Some(HeaderPart::Addr)}
"raw" => {Some(HeaderPart::Raw)}
"" => None,
_ => {
eprintln!(
"Warning: Invalid header part {part:?} on {}, line {}",
path.display(),
line_num
);
None
}
}
}).collect::<Vec<_>>();
rule.t = RuleType::Header {
matches: match op {
"=~" => HeaderMatches::Matches,
@@ -502,38 +553,13 @@ pub fn import_spamassassin(path: PathBuf, extension: String, do_warn: bool, vali
continue;
}
},
header: if let Some((header, part)) = header.split_once(':') {
Header::Name {
name: header.to_string(),
part: part.split(':').filter_map(|part| {
match part {
"name" => {Some(HeaderPart::Name)}
"addr" => {Some(HeaderPart::Addr)}
"raw" => {Some(HeaderPart::Raw)}
_ => {
eprintln!(
"Warning: Invalid header part {part:?} on {}, line {}",
path.display(),
line_num
);
None
}
}
}).collect::<Vec<_>>()
}
} else {
match header {
"ALL" => Header::All,
"MESSAGEID" => Header::MessageId,
"ALL-EXTERNAL" => Header::AllExternal,
"EnvelopeFrom" => Header::EnvelopeFrom,
"ToCc" => Header::ToCc,
_ => Header::Name {
name: header.to_string(),
part: vec![],
},
}
header: match header {
"ALL" => Header::All,
"MESSAGEID" => Header::MessageId,
"ALL-EXTERNAL" => Header::AllExternal,
"EnvelopeFrom" => Header::EnvelopeFrom,
"ToCc" => Header::ToCc,
_ => Header::Name(header.to_string()),
},
if_unset: pattern.rsplit_once("[if-unset:").and_then(
|(new_pattern, if_unset)| {
@@ -553,6 +579,7 @@ pub fn import_spamassassin(path: PathBuf, extension: String, do_warn: bool, vali
},
),
pattern: fix_broken_regex(pattern).to_string(),
part,
};
} else {
eprintln!(
@@ -620,7 +647,7 @@ pub fn import_spamassassin(path: PathBuf, extension: String, do_warn: bool, vali
}
"meta" => {
if let Some((test_name, expression)) = params.split_once(' ') {
let tokens = MetaExpression::from_meta(expression);
let expr = MetaExpression::from_meta(expression);
/*if tokens.tokens.contains(&Token::Divide) {
println!(
"->: {expression}\n{:?}\n<-: {}",
@@ -632,10 +659,8 @@ pub fn import_spamassassin(path: PathBuf, extension: String, do_warn: bool, vali
String::from(tokens.clone())
);
std::process::exit(1);
}
rules.entry(test_name.to_string()).or_default().t = RuleType::Meta {
tokens: tokens.tokens,
};*/
}*/
rules.entry(test_name.to_string()).or_default().t = RuleType::Meta { expr };
} else {
eprintln!(
"Warning: Invalid meta command on {}, line {}",
@@ -993,53 +1018,51 @@ pub fn import_spamassassin(path: PathBuf, extension: String, do_warn: bool, vali
}
})
.collect::<Vec<_>>();
rules.sort_unstable_by(|a, b| b.cmp(a));
rules.sort_unstable();
let no_meta: Vec<Token> = vec![];
let no_meta = MetaExpression::default();
let mut meta = &no_meta;
let mut tests_done = HashSet::new();
let mut tests_linked = HashSet::new();
let mut rules_iter = rules.iter();
let mut rules_stack = Vec::new();
let mut rules_sorted = Vec::with_capacity(rules.len());
// Sort rules by meta
loop {
while let Some(rule) = rules_iter.next() {
let in_meta = !meta.tokens.is_empty();
if tests_done.contains(&rule.name)
|| (!meta.is_empty()
|| (in_meta
&& !meta
.tokens
.iter()
.any(|t| matches!(t, Token::Tag(n) if n == &rule.name)))
.any(|t| matches!(&t.token, Token::Tag(n) if n == &rule.name)))
{
continue;
}
tests_done.insert(&rule.name);
if in_meta {
tests_linked.insert(&rule.name);
}
match &rule.t {
RuleType::Meta { tokens } => {
meta = tokens;
rules_stack.push((meta, rules_iter));
RuleType::Meta { expr } if rule.score() != 0.0 => {
rules_stack.push((meta, rule, rules_iter));
rules_iter = rules.iter();
meta = expr;
}
_ => {
rules_sorted.push(rule);
//write!(&mut script, "{rule}").unwrap();
}
RuleType::Header {
matches,
header,
if_unset,
pattern,
} => todo!(),
RuleType::Body { pattern, raw } => todo!(),
RuleType::Full { pattern } => todo!(),
RuleType::Uri { pattern } => todo!(),
RuleType::Eval { function, params } => todo!(),
RuleType::None => (),
}
tests_done.insert(&rule.name);
}
if let Some((prev_meta, prev_rules_iter)) = rules_stack.pop() {
for token in meta {
//TODO
}
if let Some((prev_meta, prev_rule, prev_rules_iter)) = rules_stack.pop() {
rules_sorted.push(prev_rule);
//write!(&mut script, "{prev_rule}").unwrap();
rules_iter = prev_rules_iter;
meta = prev_meta;
} else {
@@ -1047,9 +1070,48 @@ pub fn import_spamassassin(path: PathBuf, extension: String, do_warn: bool, vali
}
}
// Generate script
let mut script = String::new();
let mut rules_iter = rules_sorted.iter();
while let Some(&rule) = rules_iter.next() {
if rule.score() == 0.0 && !tests_linked.contains(&rule.name) {
if do_warn {
eprintln!("Warning: Test {} is never linked to.", rule.name);
}
continue;
}
// Calculate forward scores
let (score_pos, score_neg) =
rules_iter
.clone()
.fold((0.0, 0.0), |(acc_pos, acc_neg), rule| {
let score = rule.score();
if score > 0.0 {
(acc_pos + score, acc_neg)
} else if score < 0.0 {
(acc_pos, acc_neg + score)
} else {
(acc_pos, acc_neg)
}
});
let mut rule = rule.clone();
rule.forward_score_neg = score_neg;
rule.forward_score_pos = score_pos;
write!(&mut script, "{rule}").unwrap();
}
fs::write(
"/Users/me/code/mail-server/_ignore/script.sieve",
script.as_bytes(),
)
.unwrap();
for (message, unsupported) in [
("commands", unsupported_commands),
("plugins", unsupported_plugins),
("plugins", unsupported_ifs),
] {
if !unsupported.is_empty() {
eprintln!("Unsupported {}:", message);
@@ -1071,3 +1133,158 @@ pub fn import_spamassassin(path: PathBuf, extension: String, do_warn: bool, vali
}
}
}
impl Display for Rule {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
// Add comment
self.description
.get("en")
.map(|v| {
writeln!(f, "# {v} (rank {})", self.priority).unwrap();
})
.unwrap_or_else(|| writeln!(f, "# {} (rank {})", self.name, self.priority).unwrap());
match &self.t {
RuleType::Header {
matches,
header: header @ (Header::All | Header::AllExternal),
if_unset,
pattern,
part,
} => {
write!(
f,
"if vnd.stalwart.eval(\"match_all_headers\", \"{}\", {:?})",
if header == &Header::All {
"all"
} else {
"all-external"
},
pattern
)?;
}
RuleType::Header {
matches,
header,
if_unset,
pattern,
part,
} => {
f.write_str("if ")?;
let cmd = if matches!(header, Header::EnvelopeFrom) {
"envelope"
} else if part.contains(&HeaderPart::Addr) || part.contains(&HeaderPart::Name) {
"address"
} else {
"header"
};
match matches {
HeaderMatches::Matches => write!(f, "{cmd} :regex ")?,
HeaderMatches::NotMatches => write!(f, "not {cmd} :regex ")?,
HeaderMatches::Exists => write!(f, "{cmd} :contains ")?,
}
for part in part {
match part {
HeaderPart::Name => f.write_str(":name ")?,
HeaderPart::Addr => f.write_str(":all ")?,
HeaderPart::Raw => f.write_str(":raw ")?,
}
}
match header {
Header::MessageId => f.write_str("[\"Message-Id\",\"Resent-Message-Id\",\"X-Message-Id\",\"X-Original-Message-ID\"]")?,
Header::ToCc => f.write_str("[\"To\",\"Cc\"]")?,
Header::Name (name) => write!(f, "{:?}", name)?,
Header::EnvelopeFrom => f.write_str("\"from\"")?,
Header::All |
Header::AllExternal => unreachable!(),
}
write!(f, " {:?}", pattern)?;
}
RuleType::Body { pattern, raw } => {
if *raw {
write!(f, "if body :raw :regex {pattern:?}")?;
} else if !self.flags.contains(&TestFlag::NoSubject) {
write!(f, "if body :subject :regex {pattern:?}")?;
} else {
write!(f, "if body :regex {pattern:?}")?;
}
}
RuleType::Full { pattern } => {
write!(f, "if vnd.stalwart.eval(\"match_full\", {:?})", pattern)?;
}
RuleType::Uri { pattern } => {
write!(f, "if vnd.stalwart.eval(\"match_uri\", {:?})", pattern)?;
}
RuleType::Eval { function, params } => {
write!(f, "if vnd.stalwart.eval({function:?}")?;
for param in params {
write!(f, ", {param:?}")?;
}
f.write_str(")")?;
}
RuleType::Meta { expr } => {
expr.fmt(f)?;
}
RuleType::None => {
f.write_str("if false")?;
}
}
f.write_str(" {\n\tset \"")?;
f.write_str(&self.name)?;
f.write_str("\" \"1\";\n")?;
let score = self.score();
if score != 0.0 {
f.write_str("\tset \"score\" \"${score")?;
if score > 0.0 {
f.write_str(" + ")?;
score.fmt(f)?;
} else {
f.write_str(" - ")?;
(-score).fmt(f)?;
}
f.write_str("}\";\n\t")?;
if score > 0.0 {
if self.forward_score_neg != 0.0 {
write!(
f,
concat!(
"if allof(string :value \"ge\" :comparator ",
"\"i;ascii-numeric\" \"${{score}}\" \"${{spam_score}}\", ",
"string :value \"ge\" :comparator ",
"\"i;ascii-numeric\" \"${{score - {:.4}}}\" \"${{spam_score}}\")"
),
-self.forward_score_neg
)?;
} else {
f.write_str(concat!(
"if string :value \"ge\" :comparator ",
"\"i;ascii-numeric\" \"${score}\" \"${spam_score}\""
))?;
}
} else if self.forward_score_pos != 0.0 {
write!(
f,
concat!(
"if allof(string :value \"lt\" :comparator ",
"\"i;ascii-numeric\" \"${{score}}\" \"${{spam_score}}\", ",
"string :value \"lt\" :comparator ",
"\"i;ascii-numeric\" \"${{score + {:.4}}}\" \"${{spam_score}}\")"
),
self.forward_score_pos
)?;
} else {
f.write_str(concat!(
"if string :value \"lt\" :comparator ",
"\"i;ascii-numeric\" \"${score}\" \"${spam_score}\""
))?;
}
f.write_str(" {\n\t\treturn;\n\t}\n")?;
}
f.write_str("}\n\n")
}
}

View File

@@ -5,7 +5,7 @@ authors = ["Stalwart Labs Ltd. <hello@stalw.art>"]
license = "AGPL-3.0-only"
repository = "https://github.com/stalwartlabs/cli"
homepage = "https://github.com/stalwartlabs/cli"
version = "0.3.4"
version = "0.3.5"
edition = "2021"
readme = "README.md"
resolver = "2"

View File

@@ -1,6 +1,6 @@
[package]
name = "imap"
version = "0.3.4"
version = "0.3.5"
edition = "2021"
resolver = "2"

View File

@@ -5,7 +5,7 @@ authors = ["Stalwart Labs Ltd. <hello@stalw.art>"]
license = "AGPL-3.0-only"
repository = "https://github.com/stalwartlabs/mail-server"
homepage = "https://github.com/stalwartlabs/mail-server"
version = "0.3.4"
version = "0.3.5"
edition = "2021"
readme = "README.md"
resolver = "2"

View File

@@ -1,6 +1,6 @@
[package]
name = "jmap"
version = "0.3.4"
version = "0.3.5"
edition = "2021"
resolver = "2"

View File

@@ -7,7 +7,7 @@ homepage = "https://stalw.art"
keywords = ["imap", "jmap", "smtp", "email", "mail", "server"]
categories = ["email"]
license = "AGPL-3.0-only"
version = "0.3.4"
version = "0.3.5"
edition = "2021"
resolver = "2"

View File

@@ -7,7 +7,7 @@ homepage = "https://stalw.art/smtp"
keywords = ["smtp", "email", "mail", "server"]
categories = ["email"]
license = "AGPL-3.0-only"
version = "0.3.4"
version = "0.3.5"
edition = "2021"
resolver = "2"

View File

@@ -1,6 +1,6 @@
[package]
name = "utils"
version = "0.3.4"
version = "0.3.5"
edition = "2021"
resolver = "2"