Skip to main content

stackforge_core/layer/imap/
builder.rs

1//! IMAP packet builder.
2//!
3//! Provides a fluent API for constructing IMAP commands and responses.
4//!
5//! # Examples
6//!
7//! ```rust
8//! use stackforge_core::layer::imap::builder::ImapBuilder;
9//!
10//! // Build a LOGIN command
11//! let pkt = ImapBuilder::new().login("A001", "alice", "password").build();
12//! assert_eq!(pkt, b"A001 LOGIN alice password\r\n");
13//!
14//! // Build an untagged OK server greeting
15//! let pkt = ImapBuilder::new().server_greeting("IMAP4rev1 Service Ready").build();
16//! assert_eq!(pkt, b"* OK IMAP4rev1 Service Ready\r\n");
17//! ```
18
19/// Builder for IMAP messages (client commands and server responses).
20#[must_use]
21#[derive(Debug, Clone)]
22pub struct ImapBuilder {
23    /// Message tag: "*" for untagged, "+" for continuation, or client tag.
24    tag: String,
25    /// Command verb or response status.
26    command: String,
27    /// Arguments or response text.
28    args: String,
29    /// Additional response lines (for multi-line responses like FETCH data).
30    extra_lines: Vec<String>,
31}
32
33impl Default for ImapBuilder {
34    fn default() -> Self {
35        Self {
36            tag: "A001".to_string(),
37            command: "NOOP".to_string(),
38            args: String::new(),
39            extra_lines: Vec::new(),
40        }
41    }
42}
43
44impl ImapBuilder {
45    pub fn new() -> Self {
46        Self::default()
47    }
48
49    // ========================================================================
50    // Server Response Builders
51    // ========================================================================
52
53    /// Untagged response: `* STATUS [text]`
54    pub fn untagged(mut self, status: impl Into<String>, text: impl Into<String>) -> Self {
55        self.tag = "*".to_string();
56        self.command = status.into().to_ascii_uppercase();
57        self.args = text.into();
58        self.extra_lines.clear();
59        self
60    }
61
62    /// Continuation request: `+ [text]`
63    pub fn continuation(mut self, text: impl Into<String>) -> Self {
64        self.tag = "+".to_string();
65        self.command = String::new();
66        self.args = text.into();
67        self.extra_lines.clear();
68        self
69    }
70
71    /// Tagged response: `tag STATUS [text]`
72    pub fn tagged_response(
73        mut self,
74        tag: impl Into<String>,
75        status: impl Into<String>,
76        text: impl Into<String>,
77    ) -> Self {
78        self.tag = tag.into();
79        self.command = status.into().to_ascii_uppercase();
80        self.args = text.into();
81        self.extra_lines.clear();
82        self
83    }
84
85    /// `* OK IMAP4rev1 Service Ready` (server greeting).
86    pub fn server_greeting(self, text: impl Into<String>) -> Self {
87        self.untagged("OK", text)
88    }
89
90    /// `* BYE [text]` (server closing connection).
91    pub fn bye(self, text: impl Into<String>) -> Self {
92        self.untagged("BYE", text)
93    }
94
95    /// `* CAPABILITY [capabilities]`
96    pub fn capability(self, caps: impl Into<String>) -> Self {
97        self.untagged("CAPABILITY", caps)
98    }
99
100    /// `* N EXISTS` (mailbox contains N messages).
101    pub fn exists(self, n: u32) -> Self {
102        self.untagged(format!("{n}"), "EXISTS")
103    }
104
105    /// `* N RECENT` (N messages are recent).
106    pub fn recent(self, n: u32) -> Self {
107        self.untagged(format!("{n}"), "RECENT")
108    }
109
110    /// `* N EXPUNGE` (message N was expunged).
111    pub fn expunge_notify(self, n: u32) -> Self {
112        self.untagged(format!("{n}"), "EXPUNGE")
113    }
114
115    /// `tag OK [text]`
116    pub fn ok(self, tag: impl Into<String>, text: impl Into<String>) -> Self {
117        self.tagged_response(tag, "OK", text)
118    }
119
120    /// `tag NO [text]`
121    pub fn no(self, tag: impl Into<String>, text: impl Into<String>) -> Self {
122        self.tagged_response(tag, "NO", text)
123    }
124
125    /// `tag BAD [text]`
126    pub fn bad(self, tag: impl Into<String>, text: impl Into<String>) -> Self {
127        self.tagged_response(tag, "BAD", text)
128    }
129
130    // ========================================================================
131    // Client Command Builders
132    // ========================================================================
133
134    fn client_cmd(
135        mut self,
136        tag: impl Into<String>,
137        command: impl Into<String>,
138        args: impl Into<String>,
139    ) -> Self {
140        self.tag = tag.into();
141        self.command = command.into().to_ascii_uppercase();
142        self.args = args.into();
143        self.extra_lines.clear();
144        self
145    }
146
147    /// `tag CAPABILITY`
148    pub fn capability_cmd(self, tag: impl Into<String>) -> Self {
149        self.client_cmd(tag, "CAPABILITY", "")
150    }
151
152    /// `tag NOOP`
153    pub fn noop(self, tag: impl Into<String>) -> Self {
154        self.client_cmd(tag, "NOOP", "")
155    }
156
157    /// `tag LOGOUT`
158    pub fn logout(self, tag: impl Into<String>) -> Self {
159        self.client_cmd(tag, "LOGOUT", "")
160    }
161
162    /// `tag LOGIN <user> <pass>`
163    pub fn login(
164        self,
165        tag: impl Into<String>,
166        user: impl Into<String>,
167        pass: impl Into<String>,
168    ) -> Self {
169        let args = format!("{} {}", user.into(), pass.into());
170        self.client_cmd(tag, "LOGIN", args)
171    }
172
173    /// `tag AUTHENTICATE <mechanism>`
174    pub fn authenticate(self, tag: impl Into<String>, mechanism: impl Into<String>) -> Self {
175        self.client_cmd(tag, "AUTHENTICATE", mechanism)
176    }
177
178    /// `tag STARTTLS`
179    pub fn starttls(self, tag: impl Into<String>) -> Self {
180        self.client_cmd(tag, "STARTTLS", "")
181    }
182
183    /// `tag SELECT <mailbox>`
184    pub fn select(self, tag: impl Into<String>, mailbox: impl Into<String>) -> Self {
185        self.client_cmd(tag, "SELECT", mailbox)
186    }
187
188    /// `tag EXAMINE <mailbox>`
189    pub fn examine(self, tag: impl Into<String>, mailbox: impl Into<String>) -> Self {
190        self.client_cmd(tag, "EXAMINE", mailbox)
191    }
192
193    /// `tag CREATE <mailbox>`
194    pub fn create(self, tag: impl Into<String>, mailbox: impl Into<String>) -> Self {
195        self.client_cmd(tag, "CREATE", mailbox)
196    }
197
198    /// `tag DELETE <mailbox>`
199    pub fn delete(self, tag: impl Into<String>, mailbox: impl Into<String>) -> Self {
200        self.client_cmd(tag, "DELETE", mailbox)
201    }
202
203    /// `tag RENAME <old> <new>`
204    pub fn rename(
205        self,
206        tag: impl Into<String>,
207        old: impl Into<String>,
208        new_name: impl Into<String>,
209    ) -> Self {
210        let args = format!("{} {}", old.into(), new_name.into());
211        self.client_cmd(tag, "RENAME", args)
212    }
213
214    /// `tag LIST <ref> <pattern>`
215    pub fn list(
216        self,
217        tag: impl Into<String>,
218        reference: impl Into<String>,
219        pattern: impl Into<String>,
220    ) -> Self {
221        let args = format!("\"{}\" \"{}\"", reference.into(), pattern.into());
222        self.client_cmd(tag, "LIST", args)
223    }
224
225    /// `tag SUBSCRIBE <mailbox>`
226    pub fn subscribe(self, tag: impl Into<String>, mailbox: impl Into<String>) -> Self {
227        self.client_cmd(tag, "SUBSCRIBE", mailbox)
228    }
229
230    /// `tag UNSUBSCRIBE <mailbox>`
231    pub fn unsubscribe(self, tag: impl Into<String>, mailbox: impl Into<String>) -> Self {
232        self.client_cmd(tag, "UNSUBSCRIBE", mailbox)
233    }
234
235    /// `tag STATUS <mailbox> (<items>)`
236    pub fn status_cmd(
237        self,
238        tag: impl Into<String>,
239        mailbox: impl Into<String>,
240        items: impl Into<String>,
241    ) -> Self {
242        let args = format!("{} ({})", mailbox.into(), items.into());
243        self.client_cmd(tag, "STATUS", args)
244    }
245
246    /// `tag FETCH <sequence> <items>`
247    pub fn fetch(
248        self,
249        tag: impl Into<String>,
250        sequence: impl Into<String>,
251        items: impl Into<String>,
252    ) -> Self {
253        let args = format!("{} {}", sequence.into(), items.into());
254        self.client_cmd(tag, "FETCH", args)
255    }
256
257    /// `tag STORE <sequence> <flags>`
258    pub fn store(
259        self,
260        tag: impl Into<String>,
261        sequence: impl Into<String>,
262        mode: impl Into<String>,
263        flags: impl Into<String>,
264    ) -> Self {
265        let args = format!("{} {} ({})", sequence.into(), mode.into(), flags.into());
266        self.client_cmd(tag, "STORE", args)
267    }
268
269    /// `tag SEARCH <criteria>`
270    pub fn search(self, tag: impl Into<String>, criteria: impl Into<String>) -> Self {
271        self.client_cmd(tag, "SEARCH", criteria)
272    }
273
274    /// `tag COPY <sequence> <mailbox>`
275    pub fn copy(
276        self,
277        tag: impl Into<String>,
278        sequence: impl Into<String>,
279        mailbox: impl Into<String>,
280    ) -> Self {
281        let args = format!("{} {}", sequence.into(), mailbox.into());
282        self.client_cmd(tag, "COPY", args)
283    }
284
285    /// `tag EXPUNGE`
286    pub fn expunge(self, tag: impl Into<String>) -> Self {
287        self.client_cmd(tag, "EXPUNGE", "")
288    }
289
290    /// `tag CLOSE`
291    pub fn close(self, tag: impl Into<String>) -> Self {
292        self.client_cmd(tag, "CLOSE", "")
293    }
294
295    /// `tag CHECK`
296    pub fn check(self, tag: impl Into<String>) -> Self {
297        self.client_cmd(tag, "CHECK", "")
298    }
299
300    /// `tag UID <command> <args>`
301    pub fn uid(
302        self,
303        tag: impl Into<String>,
304        command: impl Into<String>,
305        args: impl Into<String>,
306    ) -> Self {
307        let uid_args = format!("{} {}", command.into().to_ascii_uppercase(), args.into());
308        self.client_cmd(tag, "UID", uid_args)
309    }
310
311    // ========================================================================
312    // Build
313    // ========================================================================
314
315    #[must_use]
316    pub fn build(&self) -> Vec<u8> {
317        let mut out = Vec::new();
318        let args = &self.args;
319        let tag = &self.tag;
320        let command = &self.command;
321        if self.tag == "+" {
322            // Continuation
323            if args.is_empty() {
324                out.extend_from_slice(b"+ \r\n");
325            } else {
326                out.extend_from_slice(format!("+ {args}\r\n").as_bytes());
327            }
328        } else if self.tag == "*" {
329            // Untagged
330            if args.is_empty() {
331                out.extend_from_slice(format!("* {command}\r\n").as_bytes());
332            } else {
333                out.extend_from_slice(format!("* {command} {args}\r\n").as_bytes());
334            }
335        } else {
336            // Tagged (command or response)
337            if command.is_empty() {
338                out.extend_from_slice(format!("{tag}\r\n").as_bytes());
339            } else if args.is_empty() {
340                out.extend_from_slice(format!("{tag} {command}\r\n").as_bytes());
341            } else {
342                out.extend_from_slice(format!("{tag} {command} {args}\r\n").as_bytes());
343            }
344        }
345        // Append extra lines
346        for line in &self.extra_lines {
347            out.extend_from_slice(format!("{line}\r\n").as_bytes());
348        }
349        out
350    }
351}
352
353#[cfg(test)]
354mod tests {
355    use super::*;
356
357    #[test]
358    fn test_build_server_greeting() {
359        let pkt = ImapBuilder::new()
360            .server_greeting("IMAP4rev1 Service Ready")
361            .build();
362        assert_eq!(pkt, b"* OK IMAP4rev1 Service Ready\r\n");
363    }
364
365    #[test]
366    fn test_build_bye() {
367        let pkt = ImapBuilder::new().bye("Server logging out").build();
368        assert_eq!(pkt, b"* BYE Server logging out\r\n");
369    }
370
371    #[test]
372    fn test_build_ok_response() {
373        let pkt = ImapBuilder::new().ok("A001", "LOGIN completed").build();
374        assert_eq!(pkt, b"A001 OK LOGIN completed\r\n");
375    }
376
377    #[test]
378    fn test_build_no_response() {
379        let pkt = ImapBuilder::new().no("A002", "login failed").build();
380        assert_eq!(pkt, b"A002 NO login failed\r\n");
381    }
382
383    #[test]
384    fn test_build_bad_response() {
385        let pkt = ImapBuilder::new().bad("A003", "unknown command").build();
386        assert_eq!(pkt, b"A003 BAD unknown command\r\n");
387    }
388
389    #[test]
390    fn test_build_login_command() {
391        let pkt = ImapBuilder::new()
392            .login("A001", "alice", "password123")
393            .build();
394        assert_eq!(pkt, b"A001 LOGIN alice password123\r\n");
395    }
396
397    #[test]
398    fn test_build_select_command() {
399        let pkt = ImapBuilder::new().select("A002", "INBOX").build();
400        assert_eq!(pkt, b"A002 SELECT INBOX\r\n");
401    }
402
403    #[test]
404    fn test_build_fetch_command() {
405        let pkt = ImapBuilder::new().fetch("A003", "1:*", "FLAGS").build();
406        assert_eq!(pkt, b"A003 FETCH 1:* FLAGS\r\n");
407    }
408
409    #[test]
410    fn test_build_store_command() {
411        let pkt = ImapBuilder::new()
412            .store("A004", "1", "+FLAGS", "\\Seen")
413            .build();
414        assert_eq!(pkt, b"A004 STORE 1 +FLAGS (\\Seen)\r\n");
415    }
416
417    #[test]
418    fn test_build_search_command() {
419        let pkt = ImapBuilder::new().search("A005", "UNSEEN").build();
420        assert_eq!(pkt, b"A005 SEARCH UNSEEN\r\n");
421    }
422
423    #[test]
424    fn test_build_noop() {
425        let pkt = ImapBuilder::new().noop("A006").build();
426        assert_eq!(pkt, b"A006 NOOP\r\n");
427    }
428
429    #[test]
430    fn test_build_logout() {
431        let pkt = ImapBuilder::new().logout("A007").build();
432        assert_eq!(pkt, b"A007 LOGOUT\r\n");
433    }
434
435    #[test]
436    fn test_build_exists_untagged() {
437        let pkt = ImapBuilder::new().exists(5).build();
438        assert_eq!(pkt, b"* 5 EXISTS\r\n");
439    }
440
441    #[test]
442    fn test_build_recent_untagged() {
443        let pkt = ImapBuilder::new().recent(2).build();
444        assert_eq!(pkt, b"* 2 RECENT\r\n");
445    }
446
447    #[test]
448    fn test_build_continuation() {
449        let pkt = ImapBuilder::new().continuation("go ahead").build();
450        assert_eq!(pkt, b"+ go ahead\r\n");
451    }
452
453    #[test]
454    fn test_build_uid_fetch() {
455        let pkt = ImapBuilder::new().uid("A008", "FETCH", "1:* FLAGS").build();
456        assert_eq!(pkt, b"A008 UID FETCH 1:* FLAGS\r\n");
457    }
458
459    #[test]
460    fn test_build_capability() {
461        let pkt = ImapBuilder::new()
462            .capability("IMAP4rev1 AUTH=PLAIN STARTTLS")
463            .build();
464        assert_eq!(pkt, b"* CAPABILITY IMAP4rev1 AUTH=PLAIN STARTTLS\r\n");
465    }
466}