Skip to main content

imap_client/
search.rs

1//! Builders for IMAP `SEARCH` / `UID SEARCH` criteria.
2
3/// A single search criterion in an IMAP `SEARCH` command.
4#[derive(Debug, Clone)]
5pub enum SearchKey {
6    /// `FROM` — messages whose `From` header contains the substring.
7    From(String),
8    /// `TO` — messages whose `To` header contains the substring.
9    To(String),
10    /// `SUBJECT` — messages whose `Subject` header contains the substring.
11    Subject(String),
12    /// `BODY` — messages whose body contains the substring.
13    Body(String),
14    /// `TEXT` — messages whose header or body contains the substring.
15    Text(String),
16    /// `ALL` — every message in the mailbox.
17    All,
18    /// `ANSWERED` — messages with the `\Answered` flag.
19    Answered,
20    /// `DELETED` — messages with the `\Deleted` flag.
21    Deleted,
22    /// `DRAFT` — messages with the `\Draft` flag.
23    Draft,
24    /// `FLAGGED` — messages with the `\Flagged` flag.
25    Flagged,
26    /// `RECENT` — messages with the `\Recent` flag.
27    Recent,
28    /// `SEEN` — messages with the `\Seen` flag.
29    Seen,
30    /// `UNANSWERED` — messages without the `\Answered` flag.
31    Unanswered,
32    /// `UNDELETED` — messages without the `\Deleted` flag.
33    Undeleted,
34    /// `UNDRAFT` — messages without the `\Draft` flag.
35    Undraft,
36    /// `UNFLAGGED` — messages without the `\Flagged` flag.
37    Unflagged,
38    /// `UNSEEN` — messages without the `\Seen` flag.
39    Unseen,
40    /// Conjunction — all of the contained keys must match (space-joined).
41    And(Vec<SearchKey>),
42    /// `OR` — either of the two keys must match.
43    Or(Box<SearchKey>, Box<SearchKey>),
44    /// `NOT` — the contained key must not match.
45    Not(Box<SearchKey>),
46}
47
48impl SearchKey {
49    /// Render this criterion as an IMAP search-key string suitable for a
50    /// `SEARCH` command.
51    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
85/// A convenience builder that wraps a single [`SearchKey`] and renders it to
86/// the IMAP search-key string.
87pub struct SearchQuery {
88    key: SearchKey,
89}
90
91impl SearchQuery {
92    /// Build a query from an arbitrary [`SearchKey`] (including compound
93    /// `And` / `Or` / `Not` trees).
94    pub fn new(key: SearchKey) -> Self {
95        Self { key }
96    }
97
98    /// Shorthand for a `FROM` search.
99    pub fn from(addr: &str) -> Self {
100        Self::new(SearchKey::From(addr.to_string()))
101    }
102
103    /// Shorthand for a `TO` search.
104    pub fn to(addr: &str) -> Self {
105        Self::new(SearchKey::To(addr.to_string()))
106    }
107
108    /// Shorthand for a `SUBJECT` search.
109    pub fn subject(text: &str) -> Self {
110        Self::new(SearchKey::Subject(text.to_string()))
111    }
112
113    /// Consume the builder and render the IMAP search-key string.
114    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}