stackforge_core/layer/pop3/
builder.rs1#[must_use]
21#[derive(Debug, Clone)]
22pub struct Pop3Builder {
23 is_reply: Option<bool>,
25 command: Option<String>,
26 text: String,
27 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 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 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 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 pub fn server_ready(self) -> Self {
82 self.ok("POP3 server ready")
83 }
84
85 pub fn user_accepted(self) -> Self {
87 self.ok("Password required")
88 }
89
90 pub fn logged_in(self) -> Self {
92 self.ok("logged in")
93 }
94
95 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 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 pub fn unknown_command(self) -> Self {
113 self.err("Unknown command")
114 }
115
116 pub fn permission_denied(self) -> Self {
118 self.err("Permission denied")
119 }
120
121 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 pub fn user(self, username: impl Into<String>) -> Self {
135 self.command("USER", username)
136 }
137
138 pub fn pass(self, password: impl Into<String>) -> Self {
140 self.command("PASS", password)
141 }
142
143 pub fn quit(self) -> Self {
145 self.command("QUIT", "")
146 }
147
148 pub fn stat(self) -> Self {
150 self.command("STAT", "")
151 }
152
153 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 pub fn retr(self, msg: u32) -> Self {
161 self.command("RETR", msg.to_string())
162 }
163
164 pub fn dele(self, msg: u32) -> Self {
166 self.command("DELE", msg.to_string())
167 }
168
169 pub fn noop(self) -> Self {
171 self.command("NOOP", "")
172 }
173
174 pub fn rset(self) -> Self {
176 self.command("RSET", "")
177 }
178
179 pub fn top(self, msg: u32, lines: u32) -> Self {
181 let args = format!("{msg} {lines}");
182 self.command("TOP", args)
183 }
184
185 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 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 pub fn capa(self) -> Self {
199 self.command("CAPA", "")
200 }
201
202 pub fn auth(self, mechanism: impl Into<String>) -> Self {
204 self.command("AUTH", mechanism)
205 }
206
207 pub fn stls(self) -> Self {
209 self.command("STLS", "")
210 }
211
212 #[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 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"); }
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}