Skip to main content

stackforge_core/layer/pop3/
builder.rs

1//! POP3 packet builder.
2//!
3//! Provides a fluent API for constructing POP3 command and reply payloads.
4//!
5//! # Examples
6//!
7//! ```rust
8//! use stackforge_core::layer::pop3::builder::Pop3Builder;
9//!
10//! // Build a USER command
11//! let pkt = Pop3Builder::new().user("alice").build();
12//! assert_eq!(pkt, b"USER alice\r\n");
13//!
14//! // Build a +OK response
15//! let pkt = Pop3Builder::new().ok("POP3 server ready").build();
16//! assert_eq!(pkt, b"+OK POP3 server ready\r\n");
17//! ```
18
19/// Builder for POP3 messages (commands and replies).
20#[must_use]
21#[derive(Debug, Clone)]
22pub struct Pop3Builder {
23    /// If true, build a +OK reply; if false, -ERR; if None, build a command.
24    is_reply: Option<bool>,
25    command: Option<String>,
26    text: String,
27    /// Additional lines for multi-line responses (terminated by "." line).
28    body_lines: Vec<String>,
29}
30
31impl Default for Pop3Builder {
32    fn default() -> Self {
33        Self {
34            is_reply: None,
35            command: Some("NOOP".to_string()),
36            text: String::new(),
37            body_lines: Vec::new(),
38        }
39    }
40}
41
42impl Pop3Builder {
43    pub fn new() -> Self {
44        Self::default()
45    }
46
47    // ========================================================================
48    // Reply builders (server-side)
49    // ========================================================================
50
51    /// Build a +OK response.
52    pub fn ok(mut self, text: impl Into<String>) -> Self {
53        self.is_reply = Some(true);
54        self.command = None;
55        self.text = text.into();
56        self.body_lines.clear();
57        self
58    }
59
60    /// Build a -ERR response.
61    pub fn err(mut self, text: impl Into<String>) -> Self {
62        self.is_reply = Some(false);
63        self.command = None;
64        self.text = text.into();
65        self.body_lines.clear();
66        self
67    }
68
69    /// Build a +OK response with a multi-line body.
70    ///
71    /// The body is terminated by a `.\r\n` line automatically.
72    pub fn ok_multiline(mut self, header: impl Into<String>, lines: Vec<String>) -> Self {
73        self.is_reply = Some(true);
74        self.command = None;
75        self.text = header.into();
76        self.body_lines = lines;
77        self
78    }
79
80    /// +OK POP3 server ready (server greeting).
81    pub fn server_ready(self) -> Self {
82        self.ok("POP3 server ready")
83    }
84
85    /// +OK after successful USER.
86    pub fn user_accepted(self) -> Self {
87        self.ok("Password required")
88    }
89
90    /// +OK after successful PASS.
91    pub fn logged_in(self) -> Self {
92        self.ok("logged in")
93    }
94
95    /// +OK nmsgs octets (STAT response).
96    pub fn stat_reply(self, num_msgs: u32, total_size: u64) -> Self {
97        let text = format!("{num_msgs} {total_size}");
98        self.ok(text)
99    }
100
101    /// +OK with message list (LIST response, multi-line).
102    pub fn list_reply(self, messages: Vec<(u32, u64)>) -> Self {
103        let header = format!("{} messages", messages.len());
104        let lines = messages
105            .into_iter()
106            .map(|(num, size)| format!("{num} {size}"))
107            .collect();
108        self.ok_multiline(header, lines)
109    }
110
111    /// -ERR Unknown command.
112    pub fn unknown_command(self) -> Self {
113        self.err("Unknown command")
114    }
115
116    /// -ERR Permission denied.
117    pub fn permission_denied(self) -> Self {
118        self.err("Permission denied")
119    }
120
121    // ========================================================================
122    // Command builders (client-side)
123    // ========================================================================
124
125    fn command(mut self, verb: impl Into<String>, args: impl Into<String>) -> Self {
126        self.command = Some(verb.into().to_ascii_uppercase());
127        self.is_reply = None;
128        self.text = args.into();
129        self.body_lines.clear();
130        self
131    }
132
133    /// USER <username>
134    pub fn user(self, username: impl Into<String>) -> Self {
135        self.command("USER", username)
136    }
137
138    /// PASS <password>
139    pub fn pass(self, password: impl Into<String>) -> Self {
140        self.command("PASS", password)
141    }
142
143    /// QUIT
144    pub fn quit(self) -> Self {
145        self.command("QUIT", "")
146    }
147
148    /// STAT
149    pub fn stat(self) -> Self {
150        self.command("STAT", "")
151    }
152
153    /// LIST [msg]
154    pub fn list(self, msg: Option<u32>) -> Self {
155        let args = msg.map(|n| n.to_string()).unwrap_or_default();
156        self.command("LIST", args)
157    }
158
159    /// RETR <msg>
160    pub fn retr(self, msg: u32) -> Self {
161        self.command("RETR", msg.to_string())
162    }
163
164    /// DELE <msg>
165    pub fn dele(self, msg: u32) -> Self {
166        self.command("DELE", msg.to_string())
167    }
168
169    /// NOOP
170    pub fn noop(self) -> Self {
171        self.command("NOOP", "")
172    }
173
174    /// RSET
175    pub fn rset(self) -> Self {
176        self.command("RSET", "")
177    }
178
179    /// TOP <msg> <n> (retrieve header + first n body lines)
180    pub fn top(self, msg: u32, lines: u32) -> Self {
181        let args = format!("{msg} {lines}");
182        self.command("TOP", args)
183    }
184
185    /// UIDL [msg]
186    pub fn uidl(self, msg: Option<u32>) -> Self {
187        let args = msg.map(|n| n.to_string()).unwrap_or_default();
188        self.command("UIDL", args)
189    }
190
191    /// APOP <name> <digest>
192    pub fn apop(self, name: impl Into<String>, digest: impl Into<String>) -> Self {
193        let args = format!("{} {}", name.into(), digest.into());
194        self.command("APOP", args)
195    }
196
197    /// CAPA (list capabilities, RFC 2449)
198    pub fn capa(self) -> Self {
199        self.command("CAPA", "")
200    }
201
202    /// AUTH [mechanism]
203    pub fn auth(self, mechanism: impl Into<String>) -> Self {
204        self.command("AUTH", mechanism)
205    }
206
207    /// STLS (start TLS, RFC 2595)
208    pub fn stls(self) -> Self {
209        self.command("STLS", "")
210    }
211
212    // ========================================================================
213    // Build
214    // ========================================================================
215
216    #[must_use]
217    pub fn build(&self) -> Vec<u8> {
218        match self.is_reply {
219            Some(true) => self.build_ok(),
220            Some(false) => self.build_err(),
221            None => self.build_command(),
222        }
223    }
224
225    fn build_ok(&self) -> Vec<u8> {
226        let mut out = Vec::new();
227        if self.text.is_empty() {
228            out.extend_from_slice(b"+OK\r\n");
229        } else {
230            out.extend_from_slice(format!("+OK {}\r\n", self.text).as_bytes());
231        }
232        if !self.body_lines.is_empty() {
233            for line in &self.body_lines {
234                // Byte-stuff lines starting with "."
235                if line.starts_with('.') {
236                    out.extend_from_slice(format!(".{line}\r\n").as_bytes());
237                } else {
238                    out.extend_from_slice(format!("{line}\r\n").as_bytes());
239                }
240            }
241            out.extend_from_slice(b".\r\n"); // terminator
242        }
243        out
244    }
245
246    fn build_err(&self) -> Vec<u8> {
247        if self.text.is_empty() {
248            b"-ERR\r\n".to_vec()
249        } else {
250            format!("-ERR {}\r\n", self.text).into_bytes()
251        }
252    }
253
254    fn build_command(&self) -> Vec<u8> {
255        let verb = self.command.as_deref().unwrap_or("NOOP");
256        let args = &self.text;
257        let line = if args.is_empty() {
258            format!("{verb}\r\n")
259        } else {
260            format!("{verb} {args}\r\n")
261        };
262        line.into_bytes()
263    }
264}
265
266#[cfg(test)]
267mod tests {
268    use super::*;
269
270    #[test]
271    fn test_build_noop() {
272        let pkt = Pop3Builder::new().noop().build();
273        assert_eq!(pkt, b"NOOP\r\n");
274    }
275
276    #[test]
277    fn test_build_user() {
278        let pkt = Pop3Builder::new().user("alice").build();
279        assert_eq!(pkt, b"USER alice\r\n");
280    }
281
282    #[test]
283    fn test_build_pass() {
284        let pkt = Pop3Builder::new().pass("secret").build();
285        assert_eq!(pkt, b"PASS secret\r\n");
286    }
287
288    #[test]
289    fn test_build_ok() {
290        let pkt = Pop3Builder::new().ok("POP3 server ready").build();
291        assert_eq!(pkt, b"+OK POP3 server ready\r\n");
292    }
293
294    #[test]
295    fn test_build_err() {
296        let pkt = Pop3Builder::new().err("Permission denied").build();
297        assert_eq!(pkt, b"-ERR Permission denied\r\n");
298    }
299
300    #[test]
301    fn test_build_stat_reply() {
302        let pkt = Pop3Builder::new().stat_reply(3, 1024).build();
303        assert_eq!(pkt, b"+OK 3 1024\r\n");
304    }
305
306    #[test]
307    fn test_build_list_reply_multiline() {
308        let pkt = Pop3Builder::new()
309            .list_reply(vec![(1, 512), (2, 1024)])
310            .build();
311        let s = String::from_utf8(pkt).unwrap();
312        assert!(s.starts_with("+OK 2 messages\r\n"));
313        assert!(s.contains("1 512\r\n"));
314        assert!(s.contains("2 1024\r\n"));
315        assert!(s.ends_with(".\r\n"));
316    }
317
318    #[test]
319    fn test_build_retr() {
320        let pkt = Pop3Builder::new().retr(1).build();
321        assert_eq!(pkt, b"RETR 1\r\n");
322    }
323
324    #[test]
325    fn test_build_dele() {
326        let pkt = Pop3Builder::new().dele(3).build();
327        assert_eq!(pkt, b"DELE 3\r\n");
328    }
329
330    #[test]
331    fn test_build_top() {
332        let pkt = Pop3Builder::new().top(1, 5).build();
333        assert_eq!(pkt, b"TOP 1 5\r\n");
334    }
335
336    #[test]
337    fn test_build_quit() {
338        let pkt = Pop3Builder::new().quit().build();
339        assert_eq!(pkt, b"QUIT\r\n");
340    }
341
342    #[test]
343    fn test_build_list_no_arg() {
344        let pkt = Pop3Builder::new().list(None).build();
345        assert_eq!(pkt, b"LIST\r\n");
346    }
347
348    #[test]
349    fn test_build_list_with_arg() {
350        let pkt = Pop3Builder::new().list(Some(5)).build();
351        assert_eq!(pkt, b"LIST 5\r\n");
352    }
353}