1use crate::llm::actions::{
2 protocol_trait::{ActionResult, Protocol, Server},
3 ActionDefinition, Parameter, ParameterDefinition,
4};
5use crate::protocol::log_template::LogTemplate;
6use crate::protocol::EventType;
7use crate::state::app_state::AppState;
8use anyhow::{Context, Result};
9use serde_json::json;
10use std::sync::LazyLock;
11use tracing::debug;
12
13pub static POP3_COMMAND_EVENT: LazyLock<EventType> = LazyLock::new(|| {
20 EventType::new(
21 "pop3_command",
22 "POP3 command received from client (USER, PASS, STAT, LIST, RETR, DELE, QUIT, etc.). \
23 The literal command 'CONNECTION_ESTABLISHED' is sent once when a client connects and \
24 must be answered with send_pop3_greeting. CAPA is answered by NetGet and never reaches \
25 you. Each command gets exactly one reply, and answer_with names the action for this \
26 one.",
27 json!({"type": "send_pop3_ok", "message": "command processed"}),
28 )
29 .with_parameters(vec![
30 Parameter {
31 name: "command".to_string(),
32 type_hint: "string".to_string(),
33 description:
34 "The POP3 command line (e.g., 'USER alice', 'STAT', 'RETR 1'), or the synthetic \
35 'CONNECTION_ESTABLISHED' on a new connection"
36 .to_string(),
37 required: true,
38 },
39 Parameter {
40 name: "connection_id".to_string(),
41 type_hint: "string".to_string(),
42 description: "Unique connection identifier".to_string(),
43 required: true,
44 },
45 Parameter {
46 name: "answer_with".to_string(),
47 type_hint: "string".to_string(),
48 description: "Which action answers this command".to_string(),
49 required: false,
50 },
51 Parameter {
52 name: "message_number".to_string(),
53 type_hint: "number".to_string(),
54 description: "RETR, TOP, LIST, UIDL, DELE: the message the command names".to_string(),
55 required: false,
56 },
57 Parameter {
58 name: "lines".to_string(),
59 type_hint: "number".to_string(),
60 description: "TOP only: how many body lines to send".to_string(),
61 required: false,
62 },
63 ])
64 .with_actions(Pop3Protocol::new().get_sync_actions())
65 .with_log_template(
66 LogTemplate::new()
67 .with_info("POP3 command: {command}")
68 .with_debug("POP3 {command} on {connection_id}")
69 .with_trace("POP3: {json_pretty(.)}"),
70 )
71});
72
73fn pop3_message_text(action: &serde_json::Value) -> Result<String> {
81 if let Some(content) = action.get("content").and_then(|v| v.as_str()) {
82 return Ok(content.to_string());
83 }
84 let field = |name: &str| action.get(name).and_then(|v| v.as_str());
85 let mut text = String::new();
86 for (header, name) in [
87 ("From", "from"),
88 ("To", "to"),
89 ("Subject", "subject"),
90 ("Date", "date"),
91 ] {
92 if let Some(value) = field(name) {
93 let value = value.replace(['\r', '\n'], " ");
95 text.push_str(&format!("{header}: {value}\n"));
96 }
97 }
98 let body = field("body");
99 if text.is_empty() && body.is_none() {
100 anyhow::bail!(
101 "send_pop3_retr needs the message: either 'content' (headers, a blank line, the \
102 body) or the fields from, to, subject and body"
103 );
104 }
105 text.push('\n');
106 text.push_str(body.unwrap_or(""));
107 Ok(text)
108}
109
110pub fn format_pop3_multiline_body(content: &str) -> String {
123 if content.is_empty() {
124 return String::new();
125 }
126
127 let mut out = String::with_capacity(content.len() + 16);
128 for line in content.replace("\r\n", "\n").split('\n') {
129 if line.starts_with('.') {
130 out.push('.');
131 }
132 out.push_str(line);
133 out.push_str("\r\n");
134 }
135
136 if content.ends_with('\n') {
139 out.truncate(out.len() - 2);
140 }
141
142 out
143}
144
145pub struct Pop3Protocol;
146
147impl Pop3Protocol {
148 pub fn new() -> Self {
149 Self
150 }
151
152 fn execute_send_pop3_greeting(&self, action: serde_json::Value) -> Result<ActionResult> {
153 let message = action
154 .get("message")
155 .and_then(|v| v.as_str())
156 .unwrap_or("POP3 server ready");
157
158 let response = format!("+OK {}\r\n", message);
159
160 debug!("POP3 sending greeting: {}", response.trim());
161 Ok(ActionResult::Output(response.as_bytes().to_vec()))
162 }
163
164 fn execute_send_pop3_ok(&self, action: serde_json::Value) -> Result<ActionResult> {
165 let message = action.get("message").and_then(|v| v.as_str()).unwrap_or("");
166
167 let response = if message.is_empty() {
168 "+OK\r\n".to_string()
169 } else {
170 format!("+OK {}\r\n", message)
171 };
172
173 debug!("POP3 sending +OK: {}", message);
174 Ok(ActionResult::Output(response.as_bytes().to_vec()))
175 }
176
177 fn execute_send_pop3_err(&self, action: serde_json::Value) -> Result<ActionResult> {
178 let message = action
179 .get("message")
180 .and_then(|v| v.as_str())
181 .context("Missing 'message' parameter")?;
182
183 let response = format!("-ERR {}\r\n", message);
184
185 debug!("POP3 sending -ERR: {}", message);
186 Ok(ActionResult::Output(response.as_bytes().to_vec()))
187 }
188
189 fn execute_send_pop3_stat(&self, action: serde_json::Value) -> Result<ActionResult> {
190 let message_count = action
191 .get("message_count")
192 .and_then(|v| v.as_u64())
193 .context("Missing 'message_count' parameter")?;
194
195 let total_size = action
196 .get("total_size")
197 .and_then(|v| v.as_u64())
198 .context("Missing 'total_size' parameter")?;
199
200 let response = format!("+OK {} {}\r\n", message_count, total_size);
201
202 debug!(
203 "POP3 sending STAT: {} messages, {} bytes",
204 message_count, total_size
205 );
206 Ok(ActionResult::Output(response.as_bytes().to_vec()))
207 }
208
209 fn execute_send_pop3_list(&self, action: serde_json::Value) -> Result<ActionResult> {
210 let messages = action
211 .get("messages")
212 .and_then(|v| v.as_array())
213 .context("Missing 'messages' parameter")?;
214
215 let mut response = format!("+OK {} messages\r\n", messages.len());
216 for msg in messages {
217 let id = msg.get("id").and_then(|v| v.as_u64()).unwrap_or(0);
218 let size = msg.get("size").and_then(|v| v.as_u64()).unwrap_or(0);
219 response.push_str(&format!("{} {}\r\n", id, size));
220 }
221 response.push_str(".\r\n");
222
223 debug!("POP3 sending LIST with {} messages", messages.len());
224 Ok(ActionResult::Output(response.as_bytes().to_vec()))
225 }
226
227 fn execute_send_pop3_uidl(&self, action: serde_json::Value) -> Result<ActionResult> {
228 let messages = action
229 .get("messages")
230 .and_then(|v| v.as_array())
231 .context("Missing 'messages' parameter")?;
232
233 let mut response = format!("+OK {} messages\r\n", messages.len());
234 for msg in messages {
235 let id = msg.get("id").and_then(|v| v.as_u64()).unwrap_or(0);
236 let uidl = msg.get("uidl").and_then(|v| v.as_str()).unwrap_or("");
237 response.push_str(&format!("{} {}\r\n", id, uidl));
238 }
239 response.push_str(".\r\n");
240
241 debug!("POP3 sending UIDL with {} messages", messages.len());
242 Ok(ActionResult::Output(response.as_bytes().to_vec()))
243 }
244
245 fn execute_send_pop3_retr(&self, action: serde_json::Value) -> Result<ActionResult> {
246 let content = pop3_message_text(&action)?;
247
248 let body = format_pop3_multiline_body(&content);
249
250 let size = action
254 .get("size")
255 .and_then(|v| v.as_u64())
256 .unwrap_or_else(|| body.len() as u64);
257
258 let response = format!("+OK {} octets\r\n{}.\r\n", size, body);
259
260 debug!("POP3 sending RETR with {} octets", size);
261 Ok(ActionResult::Output(response.into_bytes()))
262 }
263
264 fn execute_send_pop3_top(&self, action: serde_json::Value) -> Result<ActionResult> {
265 let content = action
266 .get("content")
267 .and_then(|v| v.as_str())
268 .context("Missing 'content' parameter")?;
269
270 let response = format!("+OK\r\n{}.\r\n", format_pop3_multiline_body(content));
271
272 debug!("POP3 sending TOP");
273 Ok(ActionResult::Output(response.into_bytes()))
274 }
275
276 fn execute_send_pop3_message(&self, action: serde_json::Value) -> Result<ActionResult> {
277 let message = action
278 .get("message")
279 .and_then(|v| v.as_str())
280 .context("Missing 'message' parameter")?;
281
282 let formatted = if message.ends_with("\r\n") {
284 message.to_string()
285 } else if message.ends_with('\n') {
286 format!("{}\r", message.trim_end_matches('\n'))
287 } else {
288 format!("{}\r\n", message)
289 };
290
291 debug!("POP3 sending custom message: {}", formatted.trim());
292 Ok(ActionResult::Output(formatted.as_bytes().to_vec()))
293 }
294}
295
296impl Protocol for Pop3Protocol {
298 fn get_startup_parameters(&self) -> Vec<ParameterDefinition> {
299 vec![
308 ParameterDefinition {
309 name: "first_byte_timeout_secs".to_string(),
310 type_hint: "number".to_string(),
311 description: "Seconds a connected peer may send no command at all - not even \
312 a CAPA or USER after the +OK greeting - before the server closes \
313 it. Default 300, matching the window a `manual` rule gives a \
314 human to answer one event. The peer is often NetGet's own POP3 \
315 client, which reads the greeting in its read loop and writes \
316 nothing until an action or [ send message ] says to. Lower it to \
317 60, Dovecot's `login_timeout`, for a listener exposed to \
318 strangers."
319 .to_string(),
320 required: false,
321 example: json!(300),
322 default: Some(serde_json::json!(
323 super::FIRST_COMMAND_READ_TIMEOUT.as_secs()
324 )),
325 },
326 ParameterDefinition {
327 name: "idle_timeout_secs".to_string(),
328 type_hint: "number".to_string(),
329 description: "Seconds an established session may go without a further command \
330 before the server closes it. Default 600. RFC 1939 section 3 \
331 says this timer MUST be at least 10 minutes, so a value below \
332 600 puts the server outside the specification - it is accepted, \
333 but that is the trade you are making."
334 .to_string(),
335 required: false,
336 example: json!(600),
337 default: Some(serde_json::json!(
338 super::IDLE_BETWEEN_COMMANDS_TIMEOUT.as_secs()
339 )),
340 },
341 ]
342 }
343
344 fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
345 Vec::new()
350 }
351
352 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
353 vec![
354 ActionDefinition {
355 name: "send_pop3_ok".to_string(),
356 description: "A single +OK line: accepts USER, PASS, DELE, NOOP, RSET, QUIT, or \
357 answers LIST/UIDL for one message. Never the answer to RETR, TOP, \
358 STAT or a whole-mailbox LIST - those have their own actions, and \
359 a bare +OK leaves the client waiting for data that never comes."
360 .to_string(),
361 parameters: vec![Parameter {
362 name: "message".to_string(),
363 type_hint: "string".to_string(),
364 description: "Optional message after +OK".to_string(),
365 required: false,
366 }],
367 example: json!({
368 "type": "send_pop3_ok",
369 "message": "user accepted"
370 }),
371 log_template: Some(
372 LogTemplate::new()
373 .with_info("-> POP3 +OK {message}")
374 .with_debug("POP3 send_pop3_ok: message={message}"),
375 ),
376 },
377 ActionDefinition {
378 name: "send_pop3_err".to_string(),
379 description: "Send POP3 -ERR response".to_string(),
380 parameters: vec![Parameter {
381 name: "message".to_string(),
382 type_hint: "string".to_string(),
383 description: "Error message".to_string(),
384 required: true,
385 }],
386 example: json!({
387 "type": "send_pop3_err",
388 "message": "Invalid credentials"
389 }),
390 log_template: Some(
391 LogTemplate::new()
392 .with_info("-> POP3 -ERR {message}")
393 .with_debug("POP3 send_pop3_err: message={message}"),
394 ),
395 },
396 ActionDefinition {
397 name: "send_pop3_greeting".to_string(),
398 description: "Send POP3 greeting banner (sent automatically on connect)"
399 .to_string(),
400 parameters: vec![Parameter {
401 name: "message".to_string(),
402 type_hint: "string".to_string(),
403 description: "Greeting message (e.g., server name)".to_string(),
404 required: false,
405 }],
406 example: json!({
407 "type": "send_pop3_greeting",
408 "message": "POP3 server ready"
409 }),
410 log_template: Some(
411 LogTemplate::new()
412 .with_info("-> POP3 greeting: {message}")
413 .with_debug("POP3 send_pop3_greeting: message={message}"),
414 ),
415 },
416 ActionDefinition {
417 name: "send_pop3_stat".to_string(),
418 description: "Send POP3 STAT response with message count and total size"
419 .to_string(),
420 parameters: vec![
421 Parameter {
422 name: "message_count".to_string(),
423 type_hint: "number".to_string(),
424 description: "Number of messages in mailbox".to_string(),
425 required: true,
426 },
427 Parameter {
428 name: "total_size".to_string(),
429 type_hint: "number".to_string(),
430 description: "Total size of all messages in octets".to_string(),
431 required: true,
432 },
433 ],
434 example: json!({
435 "type": "send_pop3_stat",
436 "message_count": 3,
437 "total_size": 1024
438 }),
439 log_template: Some(
440 LogTemplate::new()
441 .with_info("-> POP3 STAT {message_count} {total_size}")
442 .with_debug(
443 "POP3 send_pop3_stat: {message_count} messages, {total_size} bytes",
444 ),
445 ),
446 },
447 ActionDefinition {
448 name: "send_pop3_list".to_string(),
449 description: "Send POP3 LIST response with message sizes".to_string(),
450 parameters: vec![Parameter {
451 name: "messages".to_string(),
452 type_hint: "array".to_string(),
453 description: "Array of message objects with 'id' and 'size' fields".to_string(),
454 required: true,
455 }],
456 example: json!({
457 "type": "send_pop3_list",
458 "messages": [{"id": 1, "size": 512}, {"id": 2, "size": 256}]
459 }),
460 log_template: Some(
461 LogTemplate::new()
462 .with_info("-> POP3 LIST {messages_len} messages")
463 .with_debug("POP3 send_pop3_list: {messages_len} messages"),
464 ),
465 },
466 ActionDefinition {
467 name: "send_pop3_uidl".to_string(),
468 description: "Send POP3 UIDL response with unique message identifiers".to_string(),
469 parameters: vec![Parameter {
470 name: "messages".to_string(),
471 type_hint: "array".to_string(),
472 description: "Array of message objects with 'id' and 'uidl' fields".to_string(),
473 required: true,
474 }],
475 example: json!({
476 "type": "send_pop3_uidl",
477 "messages": [{"id": 1, "uidl": "msg-abc123"}, {"id": 2, "uidl": "msg-def456"}]
478 }),
479 log_template: Some(
480 LogTemplate::new()
481 .with_info("-> POP3 UIDL {messages_len} messages")
482 .with_debug("POP3 send_pop3_uidl: {messages_len} messages"),
483 ),
484 },
485 ActionDefinition {
486 name: "send_pop3_retr".to_string(),
487 description: "The answer to RETR: the whole message. Give it as from, to, \
488 subject and body and NetGet writes the header lines, or as \
489 content (headers, a blank line, the body). NetGet adds the octet \
490 count and the terminating '.' line, converts newlines to CRLF and \
491 byte-stuffs any line starting with '.'. The example's values are \
492 placeholders; send the message your instruction describes."
493 .to_string(),
494 parameters: vec![
495 Parameter {
496 name: "from".to_string(),
497 type_hint: "string".to_string(),
498 description: "Sender address (the From header)".to_string(),
499 required: false,
500 },
501 Parameter {
502 name: "to".to_string(),
503 type_hint: "string".to_string(),
504 description: "Recipient address (the To header)".to_string(),
505 required: false,
506 },
507 Parameter {
508 name: "subject".to_string(),
509 type_hint: "string".to_string(),
510 description: "The Subject header, verbatim".to_string(),
511 required: false,
512 },
513 Parameter {
514 name: "date".to_string(),
515 type_hint: "string".to_string(),
516 description: "The Date header (optional)".to_string(),
517 required: false,
518 },
519 Parameter {
520 name: "body".to_string(),
521 type_hint: "string".to_string(),
522 description: "Message body text".to_string(),
523 required: false,
524 },
525 Parameter {
526 name: "content".to_string(),
527 type_hint: "string".to_string(),
528 description: "Instead of the fields above: the whole message as RFC \
529 5322 text - headers, a blank line, then the body"
530 .to_string(),
531 required: false,
532 },
533 Parameter {
534 name: "size".to_string(),
535 type_hint: "number".to_string(),
536 description: "Octet count to advertise. Omit it and NetGet reports the \
537 real byte length of what it sends."
538 .to_string(),
539 required: false,
540 },
541 ],
542 example: json!({
543 "type": "send_pop3_retr",
544 "from": "sender@example.invalid",
545 "to": "mailbox@example.invalid",
546 "subject": "Example subject",
547 "body": "Example body."
548 }),
549 log_template: Some(
550 LogTemplate::new()
551 .with_info("-> POP3 RETR {size}B")
552 .with_debug("POP3 send_pop3_retr: size={size} bytes"),
553 ),
554 },
555 ActionDefinition {
556 name: "send_pop3_top".to_string(),
557 description: "Send POP3 TOP response: full headers plus the first N body lines \
558 the client asked for. NetGet adds the terminating '.' line and \
559 handles CRLF and byte-stuffing."
560 .to_string(),
561 parameters: vec![Parameter {
562 name: "content".to_string(),
563 type_hint: "string".to_string(),
564 description: "Email headers, a blank line, then the requested body lines"
565 .to_string(),
566 required: true,
567 }],
568 example: json!({
569 "type": "send_pop3_top",
570 "content": "From: sender@example.com\nTo: recipient@example.com\nSubject: Test\n\nFirst line"
571 }),
572 log_template: Some(
573 LogTemplate::new()
574 .with_info("-> POP3 TOP")
575 .with_debug("POP3 send_pop3_top"),
576 ),
577 },
578 ActionDefinition {
579 name: "send_pop3_message".to_string(),
580 description: "Send custom POP3 response".to_string(),
581 parameters: vec![Parameter {
582 name: "message".to_string(),
583 type_hint: "string".to_string(),
584 description: "Full POP3 response line (including +OK or -ERR)".to_string(),
585 required: true,
586 }],
587 example: json!({
588 "type": "send_pop3_message",
589 "message": "+OK Custom response"
590 }),
591 log_template: Some(
592 LogTemplate::new()
593 .with_info("-> POP3: {preview(message,60)}")
594 .with_debug("POP3 send_pop3_message: {message}"),
595 ),
596 },
597 ActionDefinition {
598 name: "wait_for_more".to_string(),
599 description: "Do not send any response, wait for more commands from client"
600 .to_string(),
601 parameters: vec![],
602 example: json!({
603 "type": "wait_for_more"
604 }),
605 log_template: Some(
606 LogTemplate::new()
607 .with_info("POP3 waiting for more data")
608 .with_debug("POP3 wait_for_more"),
609 ),
610 },
611 ActionDefinition {
612 name: "close_connection".to_string(),
613 description: "Close the POP3 connection".to_string(),
614 parameters: vec![],
615 example: json!({
616 "type": "close_connection"
617 }),
618 log_template: Some(
619 LogTemplate::new()
620 .with_info("POP3 connection closed")
621 .with_debug("POP3 close_connection"),
622 ),
623 },
624 ]
625 }
626
627 fn protocol_name(&self) -> &'static str {
628 "POP3"
629 }
630
631 fn get_event_types(&self) -> Vec<EventType> {
632 vec![POP3_COMMAND_EVENT.clone()]
636 }
637
638 fn stack_name(&self) -> &'static str {
639 "ETH>IP>TCP>POP3"
640 }
641
642 fn keywords(&self) -> Vec<&'static str> {
643 vec!["pop3", "pop3 server", "via pop3", "post office protocol"]
644 }
645
646 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
647 use crate::protocol::metadata::{
648 DevelopmentState, PrivilegeRequirement, ProtocolMetadataV2,
649 };
650
651 ProtocolMetadataV2::builder()
652 .state(DevelopmentState::Experimental)
653 .implementation(
654 "Manual line-based TCP implementation with full LLM control over protocol responses",
655 )
656 .llm_control("Full control over POP3 responses (+OK, -ERR, STAT, LIST, RETR, etc.)")
657 .e2e_testing("Manual TCP client with line-based protocol testing")
658 .privilege_requirement(PrivilegeRequirement::PrivilegedPort(110))
659 .well_known_port(110)
660 .max_inbound_bytes(crate::server::pop3::MAX_COMMAND_BYTES)
661 .notes(
662 "No mailbox storage: the model answers STAT/LIST/RETR itself. Plain TCP only - \
663 neither POP3S nor STLS is available.",
664 )
665 .build()
666 }
667
668 fn description(&self) -> &'static str {
669 "POP3 email retrieval server (RFC 1939)"
670 }
671
672 fn example_prompt(&self) -> &'static str {
673 "Listen on port 110 via POP3. Accept all authentication and return 3 test messages."
674 }
675
676 fn group_name(&self) -> &'static str {
677 "Application"
678 }
679 fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
680 use crate::llm::actions::StartupExamples;
681 StartupExamples::new(
682 json!({
684 "type": "open_server",
685 "port": 110,
686 "base_stack": "pop3",
687 "instruction": "POP3 server with 3 test messages, accept any credentials"
688 }),
689 json!({
691 "type": "open_server",
692 "port": 110,
693 "base_stack": "pop3",
694 "event_handlers": [{
695 "event_pattern": "pop3_command",
696 "handler": {
697 "type": "script",
698 "language": "python",
699 "code": "# Handle POP3 commands\ncmd = event.get('command', '').upper()\nif cmd.startswith('USER'):\n respond([{'type': 'send_pop3_ok', 'message': 'user accepted'}])\nelif cmd.startswith('PASS'):\n respond([{'type': 'send_pop3_ok', 'message': 'logged in, 3 messages'}])\nelif cmd == 'STAT':\n respond([{'type': 'send_pop3_stat', 'message_count': 3, 'total_size': 1024}])\nelif cmd == 'LIST':\n respond([{'type': 'send_pop3_list', 'messages': [{'id': 1, 'size': 512}, {'id': 2, 'size': 256}, {'id': 3, 'size': 256}]}])\nelif cmd == 'QUIT':\n respond([{'type': 'send_pop3_ok', 'message': 'bye'}])\nelse:\n respond([{'type': 'send_pop3_ok'}])"
700 }
701 }]
702 }),
703 json!({
705 "type": "open_server",
706 "port": 110,
707 "base_stack": "pop3",
708 "event_handlers": [{
709 "event_pattern": "pop3_command",
710 "handler": {
711 "type": "static",
712 "actions": [{
713 "type": "send_pop3_ok",
714 "message": "OK"
715 }]
716 }
717 }]
718 }),
719 )
720 }
721}
722
723impl Server for Pop3Protocol {
725 fn spawn(
726 &self,
727 ctx: crate::protocol::SpawnContext,
728 ) -> std::pin::Pin<
729 Box<dyn std::future::Future<Output = anyhow::Result<std::net::SocketAddr>> + Send>,
730 > {
731 Box::pin(async move {
732 use crate::server::pop3::Pop3Server;
733
734 let tls_config = None;
741
742 let secs = |name: &str| -> anyhow::Result<Option<u64>> {
745 Ok(ctx
746 .startup_params
747 .as_ref()
748 .map(|p| p.get_optional_u64(name))
749 .transpose()?
750 .flatten())
751 };
752 let first_byte_timeout_secs = secs("first_byte_timeout_secs")?;
753 let idle_timeout_secs = secs("idle_timeout_secs")?;
754
755 Pop3Server::spawn_with_llm_actions(
756 ctx.legacy_listen_addr(),
757 ctx.llm_client,
758 ctx.state,
759 ctx.status_tx,
760 ctx.server_id,
761 tls_config,
762 first_byte_timeout_secs,
763 idle_timeout_secs,
764 )
765 .await
766 })
767 }
768
769 fn execute_action(&self, action: serde_json::Value) -> Result<ActionResult> {
770 let action_type = action
771 .get("type")
772 .and_then(|v| v.as_str())
773 .context("Missing 'type' field in action")?;
774
775 match action_type {
776 "send_pop3_greeting" => self.execute_send_pop3_greeting(action),
777 "send_pop3_ok" => self.execute_send_pop3_ok(action),
778 "send_pop3_err" => self.execute_send_pop3_err(action),
779 "send_pop3_stat" => self.execute_send_pop3_stat(action),
780 "send_pop3_list" => self.execute_send_pop3_list(action),
781 "send_pop3_uidl" => self.execute_send_pop3_uidl(action),
782 "send_pop3_retr" => self.execute_send_pop3_retr(action),
783 "send_pop3_top" => self.execute_send_pop3_top(action),
784 "send_pop3_message" => self.execute_send_pop3_message(action),
785 "wait_for_more" => Ok(ActionResult::WaitForMore),
786 "close_connection" => Ok(ActionResult::CloseConnection),
787 _ => Err(anyhow::anyhow!("Unknown POP3 action: {}", action_type)),
788 }
789 }
790}