1#[derive(Debug, Clone)]
3pub enum SearchKey {
4 From(String),
5 To(String),
6 Subject(String),
7 Body(String),
8 Text(String),
9 All,
10 Answered,
11 Deleted,
12 Draft,
13 Flagged,
14 Recent,
15 Seen,
16 Unanswered,
17 Undeleted,
18 Undraft,
19 Unflagged,
20 Unseen,
21 And(Vec<SearchKey>),
22 Or(Box<SearchKey>, Box<SearchKey>),
23 Not(Box<SearchKey>),
24}
25
26impl SearchKey {
27 pub fn to_imap_string(&self) -> String {
28 match self {
29 SearchKey::From(s) => format!("FROM \"{}\"", s),
30 SearchKey::To(s) => format!("TO \"{}\"", s),
31 SearchKey::Subject(s) => format!("SUBJECT \"{}\"", s),
32 SearchKey::Body(s) => format!("BODY \"{}\"", s),
33 SearchKey::Text(s) => format!("TEXT \"{}\"", s),
34 SearchKey::All => "ALL".to_string(),
35 SearchKey::Answered => "ANSWERED".to_string(),
36 SearchKey::Deleted => "DELETED".to_string(),
37 SearchKey::Draft => "DRAFT".to_string(),
38 SearchKey::Flagged => "FLAGGED".to_string(),
39 SearchKey::Recent => "RECENT".to_string(),
40 SearchKey::Seen => "SEEN".to_string(),
41 SearchKey::Unanswered => "UNANSWERED".to_string(),
42 SearchKey::Undeleted => "UNDELETED".to_string(),
43 SearchKey::Undraft => "UNDRAFT".to_string(),
44 SearchKey::Unflagged => "UNFLAGGED".to_string(),
45 SearchKey::Unseen => "UNSEEN".to_string(),
46 SearchKey::And(keys) => keys
47 .iter()
48 .map(|k| k.to_imap_string())
49 .collect::<Vec<_>>()
50 .join(" "),
51 SearchKey::Or(left, right) => format!(
52 "OR ({}) ({})",
53 left.to_imap_string(),
54 right.to_imap_string()
55 ),
56 SearchKey::Not(key) => format!("NOT ({})", key.to_imap_string()),
57 }
58 }
59}
60
61pub struct SearchQuery {
62 key: SearchKey,
63}
64
65impl SearchQuery {
66 pub fn new(key: SearchKey) -> Self {
67 Self { key }
68 }
69
70 pub fn from(addr: &str) -> Self {
71 Self::new(SearchKey::From(addr.to_string()))
72 }
73
74 pub fn to(addr: &str) -> Self {
75 Self::new(SearchKey::To(addr.to_string()))
76 }
77
78 pub fn subject(text: &str) -> Self {
79 Self::new(SearchKey::Subject(text.to_string()))
80 }
81
82 pub fn build(self) -> String {
83 self.key.to_imap_string()
84 }
85}
86
87#[cfg(test)]
88mod tests {
89 use super::*;
90
91 #[test]
92 fn test_search_key_formatting() {
93 assert_eq!(SearchKey::All.to_imap_string(), "ALL");
94 assert_eq!(
95 SearchKey::From("alice".into()).to_imap_string(),
96 "FROM \"alice\""
97 );
98 assert_eq!(SearchKey::To("bob".into()).to_imap_string(), "TO \"bob\"");
99 assert_eq!(
100 SearchKey::Subject("hello".into()).to_imap_string(),
101 "SUBJECT \"hello\""
102 );
103 assert_eq!(
104 SearchKey::Body("world".into()).to_imap_string(),
105 "BODY \"world\""
106 );
107 assert_eq!(
108 SearchKey::Text("foo".into()).to_imap_string(),
109 "TEXT \"foo\""
110 );
111 assert_eq!(SearchKey::Answered.to_imap_string(), "ANSWERED");
112 assert_eq!(SearchKey::Deleted.to_imap_string(), "DELETED");
113 assert_eq!(SearchKey::Draft.to_imap_string(), "DRAFT");
114 assert_eq!(SearchKey::Flagged.to_imap_string(), "FLAGGED");
115 assert_eq!(SearchKey::Recent.to_imap_string(), "RECENT");
116 assert_eq!(SearchKey::Seen.to_imap_string(), "SEEN");
117 assert_eq!(SearchKey::Unanswered.to_imap_string(), "UNANSWERED");
118 assert_eq!(SearchKey::Undeleted.to_imap_string(), "UNDELETED");
119 assert_eq!(SearchKey::Undraft.to_imap_string(), "UNDRAFT");
120 assert_eq!(SearchKey::Unflagged.to_imap_string(), "UNFLAGGED");
121 assert_eq!(SearchKey::Unseen.to_imap_string(), "UNSEEN");
122 }
123
124 #[test]
125 fn test_logical_operators() {
126 let and = SearchKey::And(vec![
127 SearchKey::From("alice".into()),
128 SearchKey::Subject("hello".into()),
129 ]);
130 assert_eq!(and.to_imap_string(), "FROM \"alice\" SUBJECT \"hello\"");
131
132 let or = SearchKey::Or(Box::new(SearchKey::Seen), Box::new(SearchKey::Recent));
133 assert_eq!(or.to_imap_string(), "OR (SEEN) (RECENT)");
134
135 let not = SearchKey::Not(Box::new(SearchKey::Deleted));
136 assert_eq!(not.to_imap_string(), "NOT (DELETED)");
137 }
138
139 #[test]
140 fn test_search_query_builder() {
141 let q = SearchQuery::subject("Security Alert");
142 assert_eq!(q.build(), "SUBJECT \"Security Alert\"");
143
144 let q = SearchQuery::from("alerts@arlo.com");
145 assert_eq!(q.build(), "FROM \"alerts@arlo.com\"");
146
147 let q = SearchQuery::to("user@example.com");
148 assert_eq!(q.build(), "TO \"user@example.com\"");
149 }
150}