1#[must_use]
21#[derive(Debug, Clone)]
22pub struct ImapBuilder {
23 tag: String,
25 command: String,
27 args: String,
29 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 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 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 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 pub fn server_greeting(self, text: impl Into<String>) -> Self {
87 self.untagged("OK", text)
88 }
89
90 pub fn bye(self, text: impl Into<String>) -> Self {
92 self.untagged("BYE", text)
93 }
94
95 pub fn capability(self, caps: impl Into<String>) -> Self {
97 self.untagged("CAPABILITY", caps)
98 }
99
100 pub fn exists(self, n: u32) -> Self {
102 self.untagged(format!("{n}"), "EXISTS")
103 }
104
105 pub fn recent(self, n: u32) -> Self {
107 self.untagged(format!("{n}"), "RECENT")
108 }
109
110 pub fn expunge_notify(self, n: u32) -> Self {
112 self.untagged(format!("{n}"), "EXPUNGE")
113 }
114
115 pub fn ok(self, tag: impl Into<String>, text: impl Into<String>) -> Self {
117 self.tagged_response(tag, "OK", text)
118 }
119
120 pub fn no(self, tag: impl Into<String>, text: impl Into<String>) -> Self {
122 self.tagged_response(tag, "NO", text)
123 }
124
125 pub fn bad(self, tag: impl Into<String>, text: impl Into<String>) -> Self {
127 self.tagged_response(tag, "BAD", text)
128 }
129
130 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 pub fn capability_cmd(self, tag: impl Into<String>) -> Self {
149 self.client_cmd(tag, "CAPABILITY", "")
150 }
151
152 pub fn noop(self, tag: impl Into<String>) -> Self {
154 self.client_cmd(tag, "NOOP", "")
155 }
156
157 pub fn logout(self, tag: impl Into<String>) -> Self {
159 self.client_cmd(tag, "LOGOUT", "")
160 }
161
162 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 pub fn authenticate(self, tag: impl Into<String>, mechanism: impl Into<String>) -> Self {
175 self.client_cmd(tag, "AUTHENTICATE", mechanism)
176 }
177
178 pub fn starttls(self, tag: impl Into<String>) -> Self {
180 self.client_cmd(tag, "STARTTLS", "")
181 }
182
183 pub fn select(self, tag: impl Into<String>, mailbox: impl Into<String>) -> Self {
185 self.client_cmd(tag, "SELECT", mailbox)
186 }
187
188 pub fn examine(self, tag: impl Into<String>, mailbox: impl Into<String>) -> Self {
190 self.client_cmd(tag, "EXAMINE", mailbox)
191 }
192
193 pub fn create(self, tag: impl Into<String>, mailbox: impl Into<String>) -> Self {
195 self.client_cmd(tag, "CREATE", mailbox)
196 }
197
198 pub fn delete(self, tag: impl Into<String>, mailbox: impl Into<String>) -> Self {
200 self.client_cmd(tag, "DELETE", mailbox)
201 }
202
203 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 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 pub fn subscribe(self, tag: impl Into<String>, mailbox: impl Into<String>) -> Self {
227 self.client_cmd(tag, "SUBSCRIBE", mailbox)
228 }
229
230 pub fn unsubscribe(self, tag: impl Into<String>, mailbox: impl Into<String>) -> Self {
232 self.client_cmd(tag, "UNSUBSCRIBE", mailbox)
233 }
234
235 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 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 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 pub fn search(self, tag: impl Into<String>, criteria: impl Into<String>) -> Self {
271 self.client_cmd(tag, "SEARCH", criteria)
272 }
273
274 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 pub fn expunge(self, tag: impl Into<String>) -> Self {
287 self.client_cmd(tag, "EXPUNGE", "")
288 }
289
290 pub fn close(self, tag: impl Into<String>) -> Self {
292 self.client_cmd(tag, "CLOSE", "")
293 }
294
295 pub fn check(self, tag: impl Into<String>) -> Self {
297 self.client_cmd(tag, "CHECK", "")
298 }
299
300 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 #[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 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 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 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 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}