#![allow(clippy::unwrap_used, clippy::expect_used)]
use super::*;
#[test]
fn all() {
let c = SearchCriteria::new().all();
assert_eq!(c.as_str(), "ALL");
}
#[test]
fn answered() {
assert_eq!(SearchCriteria::new().answered().as_str(), "ANSWERED");
}
#[test]
fn deleted() {
assert_eq!(SearchCriteria::new().deleted().as_str(), "DELETED");
}
#[test]
fn draft() {
assert_eq!(SearchCriteria::new().draft().as_str(), "DRAFT");
}
#[test]
fn flagged() {
assert_eq!(SearchCriteria::new().flagged().as_str(), "FLAGGED");
}
#[test]
fn seen() {
assert_eq!(SearchCriteria::new().seen().as_str(), "SEEN");
}
#[test]
fn recent() {
assert_eq!(SearchCriteria::new().recent().as_str(), "RECENT");
}
#[test]
fn new_messages() {
assert_eq!(SearchCriteria::new().new_messages().as_str(), "NEW");
}
#[test]
fn old() {
assert_eq!(SearchCriteria::new().old().as_str(), "OLD");
}
#[test]
fn unanswered() {
assert_eq!(SearchCriteria::new().unanswered().as_str(), "UNANSWERED");
}
#[test]
fn undeleted() {
assert_eq!(SearchCriteria::new().undeleted().as_str(), "UNDELETED");
}
#[test]
fn undraft() {
assert_eq!(SearchCriteria::new().undraft().as_str(), "UNDRAFT");
}
#[test]
fn unflagged() {
assert_eq!(SearchCriteria::new().unflagged().as_str(), "UNFLAGGED");
}
#[test]
fn unseen() {
assert_eq!(SearchCriteria::new().unseen().as_str(), "UNSEEN");
}
#[test]
fn keyword() {
assert_eq!(
SearchCriteria::new()
.keyword("$Important")
.unwrap()
.as_str(),
"KEYWORD $Important"
);
}
#[test]
fn unkeyword() {
assert_eq!(
SearchCriteria::new().unkeyword("$Junk").unwrap().as_str(),
"UNKEYWORD $Junk"
);
}
#[test]
fn bcc() {
assert_eq!(
SearchCriteria::new()
.bcc("alice@example.com")
.unwrap()
.as_str(),
"BCC \"alice@example.com\""
);
}
#[test]
fn cc() {
assert_eq!(
SearchCriteria::new()
.cc("bob@example.com")
.unwrap()
.as_str(),
"CC \"bob@example.com\""
);
}
#[test]
fn from() {
assert_eq!(
SearchCriteria::new()
.from("alice@example.com")
.unwrap()
.as_str(),
"FROM \"alice@example.com\""
);
}
#[test]
fn to() {
assert_eq!(
SearchCriteria::new()
.to("bob@example.com")
.unwrap()
.as_str(),
"TO \"bob@example.com\""
);
}
#[test]
fn subject() {
assert_eq!(
SearchCriteria::new()
.subject("hello world")
.unwrap()
.as_str(),
"SUBJECT \"hello world\""
);
}
#[test]
fn header() {
assert_eq!(
SearchCriteria::new()
.header("X-Mailer", "thunderbird")
.unwrap()
.as_str(),
"HEADER X-Mailer \"thunderbird\""
);
}
#[test]
fn body() {
assert_eq!(
SearchCriteria::new()
.body("meeting notes")
.unwrap()
.as_str(),
"BODY \"meeting notes\""
);
}
#[test]
fn text() {
assert_eq!(
SearchCriteria::new().text("urgent").unwrap().as_str(),
"TEXT \"urgent\""
);
}
#[test]
fn before() {
assert_eq!(
SearchCriteria::new()
.before("13-Feb-2025")
.unwrap()
.as_str(),
"BEFORE 13-Feb-2025"
);
}
#[test]
fn on() {
assert_eq!(
SearchCriteria::new().on("13-Feb-2025").unwrap().as_str(),
"ON 13-Feb-2025"
);
}
#[test]
fn since() {
assert_eq!(
SearchCriteria::new().since("1-Jan-2024").unwrap().as_str(),
"SINCE 1-Jan-2024"
);
}
#[test]
fn sent_before() {
assert_eq!(
SearchCriteria::new()
.sent_before("1-Mar-2025")
.unwrap()
.as_str(),
"SENTBEFORE 1-Mar-2025"
);
}
#[test]
fn sent_on() {
assert_eq!(
SearchCriteria::new()
.sent_on("1-Mar-2025")
.unwrap()
.as_str(),
"SENTON 1-Mar-2025"
);
}
#[test]
fn sent_since() {
assert_eq!(
SearchCriteria::new()
.sent_since("1-Mar-2025")
.unwrap()
.as_str(),
"SENTSINCE 1-Mar-2025"
);
}
#[test]
fn larger() {
assert_eq!(SearchCriteria::new().larger(10240).as_str(), "LARGER 10240");
}
#[test]
fn smaller() {
assert_eq!(SearchCriteria::new().smaller(1024).as_str(), "SMALLER 1024");
}
#[test]
fn uid_set() {
assert_eq!(
SearchCriteria::new().uid("1:100").unwrap().as_str(),
"UID 1:100"
);
}
#[test]
fn uid_star() {
assert_eq!(
SearchCriteria::new().uid("1:*").unwrap().as_str(),
"UID 1:*"
);
}
#[test]
fn sequence_set() {
assert_eq!(
SearchCriteria::new().sequence("1:50").unwrap().as_str(),
"1:50"
);
}
#[test]
fn sequence_comma_separated() {
assert_eq!(
SearchCriteria::new().sequence("1,2,5:10").unwrap().as_str(),
"1,2,5:10"
);
}
#[test]
fn not_single_key() {
let inner = SearchCriteria::new().seen();
let c = SearchCriteria::new().not(&inner);
assert_eq!(c.as_str(), "NOT SEEN");
}
#[test]
fn not_compound() {
let inner = SearchCriteria::new()
.seen()
.from("alice@example.com")
.unwrap();
let c = SearchCriteria::new().not(&inner);
assert_eq!(c.as_str(), "NOT (SEEN FROM \"alice@example.com\")");
}
#[test]
fn or_simple() {
let a = SearchCriteria::new().seen();
let b = SearchCriteria::new().flagged();
let c = SearchCriteria::new().or(&a, &b);
assert_eq!(c.as_str(), "OR SEEN FLAGGED");
}
#[test]
fn or_compound_operands() {
let a = SearchCriteria::new().unseen().since("1-Jan-2025").unwrap();
let b = SearchCriteria::new()
.flagged()
.from("bob@example.com")
.unwrap();
let c = SearchCriteria::new().or(&a, &b);
assert_eq!(
c.as_str(),
"OR (UNSEEN SINCE 1-Jan-2025) (FLAGGED FROM \"bob@example.com\")"
);
}
#[test]
fn chain_multiple_criteria() {
let c = SearchCriteria::new()
.unseen()
.since("13-Feb-2025")
.unwrap()
.from("alice@example.com")
.unwrap();
assert_eq!(
c.as_str(),
"UNSEEN SINCE 13-Feb-2025 FROM \"alice@example.com\""
);
}
#[test]
fn chain_flags_and_size() {
let c = SearchCriteria::new()
.undeleted()
.unseen()
.larger(5000)
.smaller(1_000_000);
assert_eq!(c.as_str(), "UNDELETED UNSEEN LARGER 5000 SMALLER 1000000");
}
#[test]
fn chain_with_or_and_not() {
let deleted = SearchCriteria::new().deleted();
let draft = SearchCriteria::new().draft();
let c = SearchCriteria::new()
.unseen()
.or(&deleted, &draft)
.since("1-Jan-2025")
.unwrap();
assert_eq!(c.as_str(), "UNSEEN OR DELETED DRAFT SINCE 1-Jan-2025");
}
#[test]
fn quote_backslash_in_string() {
let c = SearchCriteria::new().from("a\\b").unwrap();
assert_eq!(c.as_str(), "FROM \"a\\\\b\"");
}
#[test]
fn quote_double_quote_in_string() {
let c = SearchCriteria::new().subject("say \"hello\"").unwrap();
assert_eq!(c.as_str(), "SUBJECT \"say \\\"hello\\\"\"");
}
#[test]
fn empty_string_value() {
let c = SearchCriteria::new().from("").unwrap();
assert_eq!(c.as_str(), "FROM \"\"");
}
#[test]
fn display_trait() {
let c = SearchCriteria::new().unseen().flagged();
assert_eq!(format!("{c}"), "UNSEEN FLAGGED");
}
#[test]
fn as_ref_str() {
let c = SearchCriteria::new().all();
let s: &str = c.as_ref();
assert_eq!(s, "ALL");
}
#[test]
fn as_str_method() {
let c = SearchCriteria::new().unseen();
assert_eq!(c.as_str(), "UNSEEN");
}
#[test]
fn default_is_empty() {
let c = SearchCriteria::default();
assert_eq!(c.as_str(), "");
}
#[test]
fn clone_and_eq() {
let c1 = SearchCriteria::new()
.unseen()
.from("alice@example.com")
.unwrap();
let c2 = c1.clone();
assert_eq!(c1, c2);
}
#[test]
fn ne_different_criteria() {
let c1 = SearchCriteria::new().unseen();
let c2 = SearchCriteria::new().seen();
assert_ne!(c1, c2);
}
#[test]
fn real_world_unread_from_sender_this_month() {
let c = SearchCriteria::new()
.unseen()
.since("1-Mar-2026")
.unwrap()
.from("notifications@github.com")
.unwrap();
assert_eq!(
c.as_str(),
"UNSEEN SINCE 1-Mar-2026 FROM \"notifications@github.com\""
);
}
#[test]
fn real_world_large_unseen_or_flagged() {
let unseen = SearchCriteria::new().unseen();
let flagged = SearchCriteria::new().flagged();
let c = SearchCriteria::new().larger(100_000).or(&unseen, &flagged);
assert_eq!(c.as_str(), "LARGER 100000 OR UNSEEN FLAGGED");
}
#[test]
fn real_world_not_deleted_not_draft() {
let deleted = SearchCriteria::new().deleted();
let draft = SearchCriteria::new().draft();
let c = SearchCriteria::new().not(&deleted).not(&draft);
assert_eq!(c.as_str(), "NOT DELETED NOT DRAFT");
}
#[test]
fn real_world_header_search() {
let c = SearchCriteria::new()
.header("X-Priority", "1")
.unwrap()
.unseen()
.since("1-Jan-2026")
.unwrap();
assert_eq!(
c.as_str(),
"HEADER X-Priority \"1\" UNSEEN SINCE 1-Jan-2026"
);
}
#[test]
fn mod_seq_simple() {
assert_eq!(
SearchCriteria::new().mod_seq(12345).as_str(),
"MODSEQ 12345"
);
}
#[test]
fn mod_seq_zero() {
assert_eq!(SearchCriteria::new().mod_seq(0).as_str(), "MODSEQ 0");
}
#[test]
fn mod_seq_combined_with_unseen() {
let c = SearchCriteria::new().unseen().mod_seq(100);
assert_eq!(c.as_str(), "UNSEEN MODSEQ 100");
}
#[test]
fn mod_seq_large_value() {
let c = SearchCriteria::new().mod_seq(9_999_999_999_999);
assert_eq!(c.as_str(), "MODSEQ 9999999999999");
}
#[test]
fn is_compound_modseq_single() {
assert!(!is_compound("MODSEQ 12345"));
}
#[test]
fn is_compound_single_keyword() {
assert!(!is_compound("UNSEEN"));
}
#[test]
fn is_compound_keyword_with_arg() {
assert!(!is_compound("FROM \"alice\""));
}
#[test]
fn is_compound_multiple_keywords() {
assert!(is_compound("UNSEEN FLAGGED"));
}
#[test]
fn is_compound_header_two_args() {
assert!(!is_compound("HEADER X-Mailer \"thunderbird\""));
}
#[test]
fn is_compound_or_two_args() {
assert!(!is_compound("OR SEEN FLAGGED"));
}
#[test]
fn is_compound_not_with_arg() {
assert!(!is_compound("NOT SEEN"));
}
#[test]
fn is_compound_not_with_parenthesized_group() {
assert!(!is_compound("NOT (SEEN FLAGGED)"));
}
#[test]
fn is_compound_or_with_parenthesized_groups() {
assert!(!is_compound("OR (UNSEEN) (FLAGGED)"));
}
#[test]
fn is_compound_bare_sequence_set() {
assert!(!is_compound("1:100"));
}
#[test]
fn count_tokens_empty() {
assert_eq!(count_top_level_tokens(""), 0);
}
#[test]
fn count_tokens_single_word() {
assert_eq!(count_top_level_tokens("UNSEEN"), 1);
}
#[test]
fn count_tokens_two_words() {
assert_eq!(count_top_level_tokens("UNSEEN FLAGGED"), 2);
}
#[test]
fn count_tokens_quoted_string_with_spaces() {
assert_eq!(count_top_level_tokens("FROM \"alice bob\""), 2);
}
#[test]
fn count_tokens_parenthesized_group() {
assert_eq!(count_top_level_tokens("NOT (SEEN FLAGGED)"), 2);
}
#[test]
fn count_tokens_escaped_quote_in_string() {
assert_eq!(count_top_level_tokens("SUBJECT \"say \\\"hi\\\"\""), 2);
}
#[test]
fn quote_imap_string_rejects_nul() {
let result = SearchCriteria::new().from("hello\0world");
assert!(
result.is_err(),
"NUL byte must be rejected in quoted strings"
);
}
#[test]
fn quote_imap_string_rejects_cr() {
let result = SearchCriteria::new().from("hello\rworld");
assert!(
result.is_err(),
"CR byte must be rejected in quoted strings"
);
}
#[test]
fn quote_imap_string_rejects_lf() {
let result = SearchCriteria::new().from("hello\nworld");
assert!(
result.is_err(),
"LF byte must be rejected in quoted strings"
);
}
#[test]
fn quote_imap_string_rejects_nul_in_header_value() {
let result = SearchCriteria::new().header("X-Test", "val\0ue");
assert!(result.is_err(), "NUL in header value must be rejected");
}
#[test]
fn keyword_rejects_space() {
let result = SearchCriteria::new().keyword("my flag");
assert!(result.is_err(), "space in keyword must be rejected");
}
#[test]
fn keyword_rejects_close_paren() {
let result = SearchCriteria::new().keyword("$foo)");
assert!(result.is_err(), "closing paren in keyword must be rejected");
}
#[test]
fn keyword_rejects_empty() {
let result = SearchCriteria::new().keyword("");
assert!(result.is_err(), "empty keyword must be rejected");
}
#[test]
fn unkeyword_rejects_space() {
let result = SearchCriteria::new().unkeyword("my flag");
assert!(result.is_err(), "space in unkeyword must be rejected");
}
#[test]
fn unkeyword_rejects_empty() {
let result = SearchCriteria::new().unkeyword("");
assert!(result.is_err(), "empty unkeyword must be rejected");
}
#[test]
fn uid_rejects_non_set() {
let result = SearchCriteria::new().uid("not a set");
assert!(
result.is_err(),
"invalid sequence set must be rejected by uid()"
);
}
#[test]
fn uid_rejects_empty() {
let result = SearchCriteria::new().uid("");
assert!(
result.is_err(),
"empty sequence set must be rejected by uid()"
);
}
#[test]
fn sequence_rejects_spaces() {
let result = SearchCriteria::new().sequence("1 2 3");
assert!(
result.is_err(),
"spaces in sequence set must be rejected by sequence()"
);
}
#[test]
fn sequence_rejects_empty() {
let result = SearchCriteria::new().sequence("");
assert!(
result.is_err(),
"empty sequence set must be rejected by sequence()"
);
}
#[test]
fn before_rejects_non_date() {
let result = SearchCriteria::new().before("not-a-date");
assert!(
result.is_err(),
"non-date string must be rejected by before()"
);
}
#[test]
fn before_rejects_empty() {
let result = SearchCriteria::new().before("");
assert!(result.is_err(), "empty date must be rejected by before()");
}
#[test]
fn before_rejects_invalid_month() {
let result = SearchCriteria::new().before("31-Foo-2025");
assert!(
result.is_err(),
"invalid month abbreviation must be rejected"
);
}
#[test]
fn since_rejects_wrong_format() {
let result = SearchCriteria::new().since("2025-01-13");
assert!(result.is_err(), "ISO date format must be rejected");
}
#[test]
fn sent_on_rejects_garbage() {
let result = SearchCriteria::new().sent_on("garbage");
assert!(
result.is_err(),
"garbage date must be rejected by sent_on()"
);
}
#[test]
fn validate_imap_date_valid_single_digit_day() {
assert!(validate_imap_date("1-Jan-2025").is_ok());
}
#[test]
fn validate_imap_date_valid_double_digit_day() {
assert!(validate_imap_date("13-Feb-2025").is_ok());
}
#[test]
fn validate_imap_date_rejects_three_digit_day() {
assert!(validate_imap_date("123-Jan-2025").is_err());
}
#[test]
fn validate_imap_date_rejects_two_digit_year() {
assert!(validate_imap_date("1-Jan-25").is_err());
}
#[test]
fn validate_imap_date_all_months_valid() {
for month in [
"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
] {
let date = format!("1-{month}-2025");
assert!(
validate_imap_date(&date).is_ok(),
"{month} should be accepted"
);
}
}