fix(filter): correct account-field evaluation and reject header conditions #223

Merged
KofTwentyTwo merged 1 commits from story/m4-filter-correctness into dev 2026-07-30 00:19:37 +00:00
2 changed files with 49 additions and 3 deletions

No files matched your search

+33 -2
View File
@@ -97,9 +97,12 @@ impl CompiledFilter {
.unwrap_or("");
Self::eval_string_op(body, &leaf.operator, &leaf.value, regexes)
}
FilterField::Folder | FilterField::Account => {
FilterField::Folder => {
Self::eval_string_op(&email.folder_id, &leaf.operator, &leaf.value, regexes)
}
FilterField::Account => {
Self::eval_string_op(&email.account_id, &leaf.operator, &leaf.value, regexes)
}
FilterField::HasAttachment => {
let has = !email.attachments.is_empty();
if let FilterValue::Boolean(b) = leaf.value {
@@ -148,7 +151,11 @@ impl CompiledFilter {
}
}
FilterField::Header(_name) => {
// Header evaluation fallback
// Unreachable defensive branch: `NsqlValidator::pass1_field_types`
// rejects any rule with a `header[...]` condition before it can
// reach a `CompiledFilter`, since `Email` has no parsed-headers
// field to match against. Kept only so this match stays
// exhaustive over `FilterField`.
false
}
}
@@ -475,6 +482,30 @@ mod tests {
assert_eq!(engine.evaluate(&email).len(), 1);
}
#[test]
fn test_account_condition_field_matches_account_id_not_folder_id() {
let nsql = "WHERE account = 'acct-a' ACTION MARK READ";
let rule = crate::parser::NsqlParser::parse_rule("Account Filter", 1, nsql).unwrap();
let engine = FilterEngine::new(vec![rule]).unwrap();
// Same folder_id, different account_id: a rule matching on the
// `account` condition field must key off email.account_id, never
// fall through to comparing folder_id.
let email_a = test_email("acct-a", "Anything", "shared-folder");
assert_eq!(
engine.evaluate(&email_a).len(),
1,
"rule on account = 'acct-a' must match a message with account_id 'acct-a'"
);
let email_b = test_email("acct-b", "Anything", "shared-folder");
assert!(
engine.evaluate(&email_b).is_empty(),
"rule on account = 'acct-a' must not match a message with account_id 'acct-b', \
even when it shares the same folder_id as an acct-a message"
);
}
#[test]
fn test_not_in_operator_is_complement_of_in() {
let nsql = "WHERE folder NOT IN ('spam', 'trash') ACTION MARK READ";
+16 -1
View File
@@ -83,11 +83,14 @@ impl NsqlValidator {
"field 'has_attachment' requires a boolean value".to_string(),
)),
},
FilterField::Header(_) => Err(ValidationError::FieldTypeMismatch(
"header field conditions are not yet supported: message headers are not indexed"
.to_string(),
)),
FilterField::Subject
| FilterField::From
| FilterField::To
| FilterField::Body
| FilterField::Header(_)
| FilterField::Folder
| FilterField::Account => match &leaf.value {
FilterValue::String(_) | FilterValue::List(_) => Ok(()),
@@ -322,6 +325,18 @@ mod tests {
assert!(matches!(err, ValidationError::FieldTypeMismatch(_)));
}
#[test]
fn test_pass1_rejects_unsupported_header_field_conditions() {
let rule = crate::parser::NsqlParser::parse_rule(
"Header Spam Score",
1,
"WHERE header['X-Spam-Score'] = '10' ACTION MARK READ",
)
.unwrap();
let err = NsqlValidator::validate(&rule, &ValidationOptions::default()).unwrap_err();
assert!(matches!(err, ValidationError::FieldTypeMismatch(_)));
}
#[test]
fn test_pass4_redos_validation() {
let rule = crate::parser::NsqlParser::parse_rule(