Skip to main content

imap_client/
search.rs

1/// Represents a search criterion in an IMAP SEARCH command.
2#[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}