1use crate::llm::actions::{
2 client_trait::{Client, ClientActionResult},
3 protocol_trait::Protocol,
4 ActionDefinition, Parameter, ParameterDefinition,
5};
6use crate::protocol::EventType;
7use crate::state::app_state::AppState;
8use anyhow::{Context, Result};
9use serde_json::json;
10use std::sync::LazyLock;
11
12pub static POP3_CLIENT_CONNECTED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
14 EventType::new(
15 "pop3_connected",
16 "POP3 client connected to server",
17 json!({"type": "placeholder", "event_id": "pop3_connected"}),
18 )
19 .with_parameters(vec![Parameter {
20 name: "pop3_server".to_string(),
21 type_hint: "string".to_string(),
22 description: "POP3 server hostname".to_string(),
23 required: true,
24 }])
25});
26
27pub static POP3_CLIENT_RESPONSE_RECEIVED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
29 EventType::new(
30 "pop3_response_received",
31 "POP3 response received from server",
32 json!({
33 "type": "send_pop3_command",
34 "command": "USER alice"
35 }),
36 )
37 .with_parameters(vec![
38 Parameter {
39 name: "response".to_string(),
40 type_hint: "string".to_string(),
41 description: "POP3 server response (e.g., '+OK' or '-ERR')".to_string(),
42 required: true,
43 },
44 Parameter {
45 name: "is_ok".to_string(),
46 type_hint: "boolean".to_string(),
47 description: "Whether response is +OK (true) or -ERR (false)".to_string(),
48 required: true,
49 },
50 ])
51});
52
53pub struct Pop3ClientProtocol;
54
55impl Default for Pop3ClientProtocol {
56 fn default() -> Self {
57 Self::new()
58 }
59}
60
61impl Pop3ClientProtocol {
62 pub fn new() -> Self {
63 Self
64 }
65}
66
67impl Protocol for Pop3ClientProtocol {
69 fn get_startup_parameters(&self) -> Vec<ParameterDefinition> {
70 vec![ParameterDefinition {
71 name: "use_tls".to_string(),
72 description: "Must be false. This client speaks POP3 over a plain TCP socket and implements no TLS, so `use_tls: true` is REFUSED at connect with an error rather than silently producing a cleartext session carrying the USER/PASS exchange. Terminate TLS in front of the server instead."
73 .to_string(),
74 type_hint: "boolean".to_string(),
75 required: false,
76 example: json!(false),
77 default: None,
78 }]
79 }
80
81 fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
82 vec![ActionDefinition {
87 name: "disconnect".to_string(),
88 description: "Disconnect from POP3 server".to_string(),
89 parameters: vec![],
90 example: json!({
91 "type": "disconnect"
92 }),
93 log_template: None,
94 }]
95 }
96
97 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
98 vec![
99 ActionDefinition {
100 name: "send_pop3_command".to_string(),
101 description: "Send a POP3 command to the server".to_string(),
102 parameters: vec![Parameter {
103 name: "command".to_string(),
104 type_hint: "string".to_string(),
105 description:
106 "POP3 command to send (e.g., 'USER alice', 'PASS secret', 'STAT', 'LIST', 'RETR 1', 'QUIT')"
107 .to_string(),
108 required: true,
109 }],
110 example: json!({
111 "type": "send_pop3_command",
112 "command": "USER alice"
113 }),
114 log_template: None,
115 },
116 ActionDefinition {
117 name: "disconnect".to_string(),
118 description: "Disconnect from POP3 server".to_string(),
119 parameters: vec![],
120 example: json!({
121 "type": "disconnect"
122 }),
123 log_template: None,
124 },
125 ActionDefinition {
126 name: "wait_for_more".to_string(),
127 description: "Wait for more data from server".to_string(),
128 parameters: vec![],
129 example: json!({
130 "type": "wait_for_more"
131 }),
132 log_template: None,
133 },
134 ]
135 }
136
137 fn protocol_name(&self) -> &'static str {
138 "POP3"
139 }
140
141 fn get_event_types(&self) -> Vec<EventType> {
142 vec![
143 EventType::new(
144 "pop3_connected",
145 "Triggered when POP3 client connects to server",
146 json!({"type": "placeholder", "event_id": "pop3_connected"}),
147 ),
148 EventType::new(
149 "pop3_response_received",
150 "Triggered when POP3 client receives a response from server",
151 json!({"type": "placeholder", "event_id": "pop3_response_received"}),
152 ),
153 ]
154 }
155
156 fn stack_name(&self) -> &'static str {
157 "ETH>IP>TCP>POP3"
158 }
159
160 fn keywords(&self) -> Vec<&'static str> {
161 vec!["pop3", "pop3 client", "connect to pop3", "pop3s"]
162 }
163
164 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
165 use crate::protocol::metadata::{DevelopmentState, ProtocolMetadataV2};
166
167 ProtocolMetadataV2::builder()
168 .state(DevelopmentState::Experimental)
169 .implementation(
170 "Custom TCP/TLS client using tokio and rustls for POP3/POP3S email retrieval",
171 )
172 .llm_control("Full control over POP3 commands (USER, PASS, STAT, LIST, RETR, DELE)")
173 .e2e_testing("NetGet POP3 server or local Dovecot server")
174 .build()
175 }
176
177 fn description(&self) -> &'static str {
178 "POP3/POP3S client for retrieving email from mailboxes"
179 }
180
181 fn example_prompt(&self) -> &'static str {
182 "Connect to pop.gmail.com:995 with TLS and authenticate as user@example.com"
183 }
184
185 fn group_name(&self) -> &'static str {
186 "Application"
187 }
188
189 fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
190 use crate::llm::actions::StartupExamples;
191 use serde_json::json;
192
193 StartupExamples::new(
194 json!({
196 "type": "open_client",
197 "remote_addr": "pop.example.com:995",
198 "base_stack": "pop3",
199 "instruction": "Authenticate with USER alice and PASS secret, then retrieve all messages"
200 }),
201 json!({
203 "type": "open_client",
204 "remote_addr": "pop.example.com:995",
205 "base_stack": "pop3",
206 "event_handlers": [{
207 "event_pattern": "pop3_response_received",
208 "handler": {
209 "type": "script",
210 "language": "python",
211 "code": "<pop3_client_handler>"
212 }
213 }]
214 }),
215 json!({
217 "type": "open_client",
218 "remote_addr": "pop.example.com:995",
219 "base_stack": "pop3",
220 "event_handlers": [
221 {
222 "event_pattern": "pop3_connected",
223 "handler": {
224 "type": "static",
225 "actions": [{
226 "type": "send_pop3_command",
227 "command": "USER alice"
228 }]
229 }
230 },
231 {
232 "event_pattern": "pop3_response_received",
233 "handler": {
234 "type": "static",
235 "actions": [{
236 "type": "wait_for_more"
237 }]
238 }
239 }
240 ]
241 }),
242 )
243 }
244}
245
246impl Client for Pop3ClientProtocol {
248 fn connect(
249 &self,
250 ctx: crate::protocol::ConnectContext,
251 ) -> std::pin::Pin<
252 Box<dyn std::future::Future<Output = anyhow::Result<std::net::SocketAddr>> + Send>,
253 > {
254 Box::pin(async move {
255 crate::client::pop3::Pop3Client::connect_with_llm_actions(
256 ctx.remote_addr,
257 ctx.llm_client,
258 ctx.state,
259 ctx.status_tx,
260 ctx.client_id,
261 ctx.startup_params,
262 )
263 .await
264 })
265 }
266
267 fn execute_action(&self, action: serde_json::Value) -> Result<ClientActionResult> {
268 let action_type = action
269 .get("type")
270 .and_then(|v| v.as_str())
271 .context("Missing 'type' field in action")?;
272
273 match action_type {
274 "send_pop3_command" => {
275 let command = action
276 .get("command")
277 .and_then(|v| v.as_str())
278 .context("Missing 'command' parameter")?
279 .to_string();
280
281 Ok(ClientActionResult::Custom {
282 name: "pop3_command".to_string(),
283 data: json!({ "command": command }),
284 })
285 }
286 "disconnect" => Ok(ClientActionResult::Disconnect),
287 "wait_for_more" => Ok(ClientActionResult::WaitForMore),
288 _ => Err(anyhow::anyhow!(
289 "Unknown POP3 client action: {}",
290 action_type
291 )),
292 }
293 }
294}