1#[derive(Debug, Clone)]
5pub enum SearchKey {
6 From(String),
8 To(String),
10 Subject(String),
12 Body(String),
14 Text(String),
16 All,
18 Answered,
20 Deleted,
22 Draft,
24 Flagged,
26 Recent,
28 Seen,
30 Unanswered,
32 Undeleted,
34 Undraft,
36 Unflagged,
38 Unseen,
40 And(Vec<SearchKey>),
42 Or(Box<SearchKey>, Box<SearchKey>),
44 Not(Box<SearchKey>),
46}
47
48impl SearchKey {
49 pub fn to_imap_string(&self) -> String {
52 match self {
53 SearchKey::From(s) => format!("FROM \"{}\"", s),
54 SearchKey::To(s) => format!("TO \"{}\"", s),
55 SearchKey::Subject(s) => format!("SUBJECT \"{}\"", s),
56 SearchKey::Body(s) => format!("BODY \"{}\"", s),
57 SearchKey::Text(s) => format!("TEXT \"{}\"", s),
58 SearchKey::All => "ALL".to_string(),
59 SearchKey::Answered => "ANSWERED".to_string(),
60 SearchKey::Deleted => "DELETED".to_string(),
61 SearchKey::Draft => "DRAFT".to_string(),
62 SearchKey::Flagged => "FLAGGED".to_string(),
63 SearchKey::Recent => "RECENT".to_string(),
64 SearchKey::Seen => "SEEN".to_string(),
65 SearchKey::Unanswered => "UNANSWERED".to_string(),
66 SearchKey::Undeleted => "UNDELETED".to_string(),
67 SearchKey::Undraft => "UNDRAFT".to_string(),
68 SearchKey::Unflagged => "UNFLAGGED".to_string(),
69 SearchKey::Unseen => "UNSEEN".to_string(),
70 SearchKey::And(keys) => keys
71 .iter()
72 .map(|k| k.to_imap_string())
73 .collect::<Vec<_>>()
74 .join(" "),
75 SearchKey::Or(left, right) => format!(
76 "OR ({}) ({})",
77 left.to_imap_string(),
78 right.to_imap_string()
79 ),
80 SearchKey::Not(key) => format!("NOT ({})", key.to_imap_string()),
81 }
82 }
83}
84
85pub struct SearchQuery {
88 key: SearchKey,
89}
90
91impl SearchQuery {
92 pub fn new(key: SearchKey) -> Self {
95 Self { key }
96 }
97
98 pub fn from(addr: &str) -> Self {
100 Self::new(SearchKey::From(addr.to_string()))
101 }
102
103 pub fn to(addr: &str) -> Self {
105 Self::new(SearchKey::To(addr.to_string()))
106 }
107
108 pub fn subject(text: &str) -> Self {
110 Self::new(SearchKey::Subject(text.to_string()))
111 }
112
113 pub fn build(self) -> String {
115 self.key.to_imap_string()
116 }
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122
123 #[test]
124 fn test_search_key_formatting() {
125 assert_eq!(SearchKey::All.to_imap_string(), "ALL");
126 assert_eq!(
127 SearchKey::From("alice".into()).to_imap_string(),
128 "FROM \"alice\""
129 );
130 assert_eq!(SearchKey::To("bob".into()).to_imap_string(), "TO \"bob\"");
131 assert_eq!(
132 SearchKey::Subject("hello".into()).to_imap_string(),
133 "SUBJECT \"hello\""
134 );
135 assert_eq!(
136 SearchKey::Body("world".into()).to_imap_string(),
137 "BODY \"world\""
138 );
139 assert_eq!(
140 SearchKey::Text("foo".into()).to_imap_string(),
141 "TEXT \"foo\""
142 );
143 assert_eq!(SearchKey::Answered.to_imap_string(), "ANSWERED");
144 assert_eq!(SearchKey::Deleted.to_imap_string(), "DELETED");
145 assert_eq!(SearchKey::Draft.to_imap_string(), "DRAFT");
146 assert_eq!(SearchKey::Flagged.to_imap_string(), "FLAGGED");
147 assert_eq!(SearchKey::Recent.to_imap_string(), "RECENT");
148 assert_eq!(SearchKey::Seen.to_imap_string(), "SEEN");
149 assert_eq!(SearchKey::Unanswered.to_imap_string(), "UNANSWERED");
150 assert_eq!(SearchKey::Undeleted.to_imap_string(), "UNDELETED");
151 assert_eq!(SearchKey::Undraft.to_imap_string(), "UNDRAFT");
152 assert_eq!(SearchKey::Unflagged.to_imap_string(), "UNFLAGGED");
153 assert_eq!(SearchKey::Unseen.to_imap_string(), "UNSEEN");
154 }
155
156 #[test]
157 fn test_logical_operators() {
158 let and = SearchKey::And(vec![
159 SearchKey::From("alice".into()),
160 SearchKey::Subject("hello".into()),
161 ]);
162 assert_eq!(and.to_imap_string(), "FROM \"alice\" SUBJECT \"hello\"");
163
164 let or = SearchKey::Or(Box::new(SearchKey::Seen), Box::new(SearchKey::Recent));
165 assert_eq!(or.to_imap_string(), "OR (SEEN) (RECENT)");
166
167 let not = SearchKey::Not(Box::new(SearchKey::Deleted));
168 assert_eq!(not.to_imap_string(), "NOT (DELETED)");
169 }
170
171 #[test]
172 fn test_search_query_builder() {
173 let q = SearchQuery::subject("Security Alert");
174 assert_eq!(q.build(), "SUBJECT \"Security Alert\"");
175
176 let q = SearchQuery::from("alerts@arlo.com");
177 assert_eq!(q.build(), "FROM \"alerts@arlo.com\"");
178
179 let q = SearchQuery::to("user@example.com");
180 assert_eq!(q.build(), "TO \"user@example.com\"");
181 }
182}