Skip to main content

netget/client/pop3/
actions.rs

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
12/// Event: POP3 client connected to server
13pub 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
27/// Event: POP3 response received from server
28pub 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
67// Implement Protocol trait (common functionality)
68impl 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        // `modify_pop3_instruction` used to be advertised here and was unrunnable: this
83        // protocol's `execute_action` rejects the name ("Unknown POP3 client action"), and no
84        // generic client-side instruction plumbing exists — a client reads `instruction` once
85        // at connect and never re-reads it. Advertising it only cost the model a retry.
86        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            // LLM mode: LLM controls POP3 operations
195            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            // Script mode: Code-based deterministic responses
202            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            // Static mode: Fixed POP3 authentication on connect
216            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
246// Implement Client trait (client-specific functionality)
247impl 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}