stackforge_core/layer/pop3/
mod.rs1pub mod builder;
46pub use builder::Pop3Builder;
47
48use crate::layer::field::{FieldError, FieldValue};
49use crate::layer::{Layer, LayerIndex, LayerKind};
50
51pub const POP3_MIN_HEADER_LEN: usize = 4;
53
54pub const POP3_PORT: u16 = 110;
56
57pub const POP3S_PORT: u16 = 995;
59
60pub const CMD_USER: &str = "USER";
64pub const CMD_PASS: &str = "PASS";
65pub const CMD_QUIT: &str = "QUIT";
66pub const CMD_STAT: &str = "STAT";
67pub const CMD_LIST: &str = "LIST";
68pub const CMD_RETR: &str = "RETR";
69pub const CMD_DELE: &str = "DELE";
70pub const CMD_NOOP: &str = "NOOP";
71pub const CMD_RSET: &str = "RSET";
72pub const CMD_TOP: &str = "TOP";
73pub const CMD_UIDL: &str = "UIDL";
74pub const CMD_APOP: &str = "APOP";
75pub const CMD_AUTH: &str = "AUTH";
76pub const CMD_CAPA: &str = "CAPA";
77pub const CMD_STLS: &str = "STLS";
78
79pub static POP3_COMMANDS: &[&str] = &[
80 "USER", "PASS", "QUIT", "STAT", "LIST", "RETR", "DELE", "NOOP", "RSET", "TOP", "UIDL", "APOP",
81 "AUTH", "CAPA", "STLS",
82];
83
84pub static POP3_FIELD_NAMES: &[&str] = &[
86 "command",
87 "args",
88 "is_ok",
89 "is_err",
90 "response_text",
91 "is_response",
92 "raw",
93];
94
95#[must_use]
101pub fn is_pop3_payload(buf: &[u8]) -> bool {
102 if buf.is_empty() {
103 return false;
104 }
105 if buf.starts_with(b"+OK") || buf.starts_with(b"-ERR") {
107 return true;
108 }
109 if let Ok(text) = std::str::from_utf8(buf) {
111 let upper = text.to_ascii_uppercase();
112 let word = upper.split_ascii_whitespace().next().unwrap_or("");
113 return POP3_COMMANDS.contains(&word);
114 }
115 false
116}
117
118#[must_use]
124#[derive(Debug, Clone)]
125pub struct Pop3Layer {
126 pub index: LayerIndex,
127}
128
129impl Pop3Layer {
130 pub fn new(index: LayerIndex) -> Self {
131 Self { index }
132 }
133
134 pub fn at_start(len: usize) -> Self {
135 Self {
136 index: LayerIndex::new(LayerKind::Pop3, 0, len),
137 }
138 }
139
140 #[inline]
141 fn slice<'a>(&self, buf: &'a [u8]) -> &'a [u8] {
142 let end = self.index.end.min(buf.len());
143 &buf[self.index.start..end]
144 }
145
146 #[must_use]
148 pub fn is_response(&self, buf: &[u8]) -> bool {
149 let s = self.slice(buf);
150 s.starts_with(b"+OK") || s.starts_with(b"-ERR")
151 }
152
153 #[must_use]
155 pub fn is_ok(&self, buf: &[u8]) -> bool {
156 self.slice(buf).starts_with(b"+OK")
157 }
158
159 #[must_use]
161 pub fn is_err_response(&self, buf: &[u8]) -> bool {
162 self.slice(buf).starts_with(b"-ERR")
163 }
164
165 pub fn response_text(&self, buf: &[u8]) -> Result<String, FieldError> {
172 let s = self.slice(buf);
173 let text = std::str::from_utf8(s)
174 .map_err(|_| FieldError::InvalidValue("response_text: non-UTF8 payload".into()))?;
175 let first_line = text.lines().next().unwrap_or("").trim_end_matches('\r');
176 if let Some(rest) = first_line.strip_prefix("+OK") {
177 Ok(rest.trim_start_matches(' ').to_string())
178 } else if let Some(rest) = first_line.strip_prefix("-ERR") {
179 Ok(rest.trim_start_matches(' ').to_string())
180 } else {
181 Err(FieldError::InvalidValue(
182 "response_text: not a POP3 response".into(),
183 ))
184 }
185 }
186
187 pub fn command(&self, buf: &[u8]) -> Result<String, FieldError> {
193 let s = self.slice(buf);
194 let text = std::str::from_utf8(s)
195 .map_err(|_| FieldError::InvalidValue("command: non-UTF8 payload".into()))?;
196 let word = text.split_ascii_whitespace().next().unwrap_or("");
197 Ok(word.to_ascii_uppercase())
198 }
199
200 pub fn args(&self, buf: &[u8]) -> Result<String, FieldError> {
206 let s = self.slice(buf);
207 let text = std::str::from_utf8(s)
208 .map_err(|_| FieldError::InvalidValue("args: non-UTF8 payload".into()))?;
209 let first_line = text.lines().next().unwrap_or("");
210 let rest = first_line
211 .split_once(' ')
212 .map_or("", |(_, r)| r)
213 .trim_end_matches(['\r', '\n']);
214 Ok(rest.to_string())
215 }
216
217 #[must_use]
219 pub fn raw(&self, buf: &[u8]) -> String {
220 String::from_utf8_lossy(self.slice(buf)).to_string()
221 }
222
223 pub fn get_field(&self, buf: &[u8], name: &str) -> Option<Result<FieldValue, FieldError>> {
224 match name {
225 "command" => Some(self.command(buf).map(FieldValue::Str)),
226 "args" => Some(self.args(buf).map(FieldValue::Str)),
227 "is_ok" => Some(Ok(FieldValue::Bool(self.is_ok(buf)))),
228 "is_err" => Some(Ok(FieldValue::Bool(self.is_err_response(buf)))),
229 "response_text" => Some(self.response_text(buf).map(FieldValue::Str)),
230 "is_response" => Some(Ok(FieldValue::Bool(self.is_response(buf)))),
231 "raw" => Some(Ok(FieldValue::Str(self.raw(buf)))),
232 _ => None,
233 }
234 }
235}
236
237impl Layer for Pop3Layer {
238 fn kind(&self) -> LayerKind {
239 LayerKind::Pop3
240 }
241
242 fn summary(&self, buf: &[u8]) -> String {
243 let s = self.slice(buf);
244 let text = String::from_utf8_lossy(s);
245 let first_line = text.lines().next().unwrap_or("").trim_end_matches('\r');
246 format!("POP3 {first_line}")
247 }
248
249 fn header_len(&self, buf: &[u8]) -> usize {
250 self.slice(buf).len()
251 }
252
253 fn hashret(&self, buf: &[u8]) -> Vec<u8> {
254 if let Ok(cmd) = self.command(buf) {
255 cmd.into_bytes()
256 } else {
257 vec![]
258 }
259 }
260
261 fn field_names(&self) -> &'static [&'static str] {
262 POP3_FIELD_NAMES
263 }
264}
265
266#[must_use]
268pub fn pop3_show_fields(l: &Pop3Layer, buf: &[u8]) -> Vec<(&'static str, String)> {
269 let mut fields = Vec::new();
270 if l.is_response(buf) {
271 fields.push((
272 if l.is_ok(buf) { "is_ok" } else { "is_err" },
273 "true".to_string(),
274 ));
275 if let Ok(text) = l.response_text(buf) {
276 fields.push(("response_text", text));
277 }
278 } else if let Ok(cmd) = l.command(buf) {
279 fields.push(("command", cmd));
280 if let Ok(args) = l.args(buf)
281 && !args.is_empty()
282 {
283 fields.push(("args", args));
284 }
285 }
286 fields
287}
288
289#[cfg(test)]
290mod tests {
291 use super::*;
292 use crate::layer::LayerIndex;
293
294 fn make_layer(data: &[u8]) -> Pop3Layer {
295 Pop3Layer::new(LayerIndex::new(LayerKind::Pop3, 0, data.len()))
296 }
297
298 #[test]
299 fn test_pop3_detection_responses() {
300 assert!(is_pop3_payload(b"+OK POP3 server ready\r\n"));
301 assert!(is_pop3_payload(b"-ERR Permission denied\r\n"));
302 assert!(is_pop3_payload(b"+OK\r\n"));
303 }
304
305 #[test]
306 fn test_pop3_detection_commands() {
307 assert!(is_pop3_payload(b"USER alice\r\n"));
308 assert!(is_pop3_payload(b"PASS secret\r\n"));
309 assert!(is_pop3_payload(b"STAT\r\n"));
310 assert!(is_pop3_payload(b"LIST\r\n"));
311 assert!(is_pop3_payload(b"RETR 1\r\n"));
312 assert!(is_pop3_payload(b"DELE 1\r\n"));
313 assert!(is_pop3_payload(b"QUIT\r\n"));
314 }
315
316 #[test]
317 fn test_pop3_detection_negative() {
318 assert!(!is_pop3_payload(b""));
319 assert!(!is_pop3_payload(b"HTTP/1.1 200 OK\r\n"));
320 assert!(!is_pop3_payload(b"\x00\x01\x02\x03"));
321 }
322
323 #[test]
324 fn test_pop3_ok_response() {
325 let data = b"+OK POP3 server ready\r\n";
326 let layer = make_layer(data);
327 assert!(layer.is_response(data));
328 assert!(layer.is_ok(data));
329 assert!(!layer.is_err_response(data));
330 assert_eq!(layer.response_text(data).unwrap(), "POP3 server ready");
331 }
332
333 #[test]
334 fn test_pop3_err_response() {
335 let data = b"-ERR Permission denied\r\n";
336 let layer = make_layer(data);
337 assert!(layer.is_response(data));
338 assert!(!layer.is_ok(data));
339 assert!(layer.is_err_response(data));
340 assert_eq!(layer.response_text(data).unwrap(), "Permission denied");
341 }
342
343 #[test]
344 fn test_pop3_user_command() {
345 let data = b"USER alice\r\n";
346 let layer = make_layer(data);
347 assert!(!layer.is_response(data));
348 assert_eq!(layer.command(data).unwrap(), "USER");
349 assert_eq!(layer.args(data).unwrap(), "alice");
350 }
351
352 #[test]
353 fn test_pop3_retr_command() {
354 let data = b"RETR 5\r\n";
355 let layer = make_layer(data);
356 assert_eq!(layer.command(data).unwrap(), "RETR");
357 assert_eq!(layer.args(data).unwrap(), "5");
358 }
359
360 #[test]
361 fn test_pop3_stat_no_args() {
362 let data = b"STAT\r\n";
363 let layer = make_layer(data);
364 assert_eq!(layer.command(data).unwrap(), "STAT");
365 assert_eq!(layer.args(data).unwrap(), "");
366 }
367
368 #[test]
369 fn test_pop3_field_access() {
370 let data = b"+OK 5 messages\r\n";
371 let layer = make_layer(data);
372 assert!(matches!(
373 layer.get_field(data, "is_ok"),
374 Some(Ok(FieldValue::Bool(true)))
375 ));
376 assert!(matches!(
377 layer.get_field(data, "is_err"),
378 Some(Ok(FieldValue::Bool(false)))
379 ));
380 assert!(layer.get_field(data, "nonexistent").is_none());
381 }
382}