1use crate::llm::actions::{
4 client_trait::{Client, ClientActionResult},
5 protocol_trait::Protocol,
6 ActionDefinition, Parameter, ParameterDefinition,
7};
8use crate::protocol::EventType;
9use crate::state::app_state::AppState;
10use anyhow::{Context, Result};
11use serde_json::json;
12use std::sync::LazyLock;
13
14pub static SMTP_CLIENT_CONNECTED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
16 EventType::new(
17 "smtp_connected",
18 "SMTP client connected to server and ready to send emails",
19 json!({
20 "type": "send_email",
21 "from": "sender@example.com",
22 "to": ["recipient@example.com"],
23 "subject": "Follow-up Email",
24 "body": "This is a follow-up email."
25 }),
26 )
27 .with_parameters(vec![Parameter {
28 name: "smtp_server".to_string(),
29 type_hint: "string".to_string(),
30 description: "SMTP server hostname".to_string(),
31 required: true,
32 }])
33});
34
35pub static SMTP_CLIENT_EMAIL_SENT_EVENT: LazyLock<EventType> = LazyLock::new(|| {
37 EventType::new(
38 "smtp_email_sent",
39 "Email successfully sent via SMTP",
40 json!({"type": "placeholder", "event_id": "smtp_email_sent"}),
41 )
42 .with_parameters(vec![
43 Parameter {
44 name: "to".to_string(),
45 type_hint: "array".to_string(),
46 description: "Recipient email addresses".to_string(),
47 required: true,
48 },
49 Parameter {
50 name: "subject".to_string(),
51 type_hint: "string".to_string(),
52 description: "Email subject".to_string(),
53 required: true,
54 },
55 Parameter {
56 name: "success".to_string(),
57 type_hint: "boolean".to_string(),
58 description: "Whether email was sent successfully".to_string(),
59 required: true,
60 },
61 ])
62});
63
64#[derive(Default)]
66pub struct SmtpClientProtocol;
67
68impl SmtpClientProtocol {
69 pub fn new() -> Self {
70 Self
71 }
72}
73
74impl Protocol for SmtpClientProtocol {
76 fn get_startup_parameters(&self) -> Vec<ParameterDefinition> {
77 vec![
78 ParameterDefinition {
79 name: "username".to_string(),
80 description: "SMTP authentication username (optional)".to_string(),
81 type_hint: "string".to_string(),
82 required: false,
83 example: json!("user@example.com"),
84 default: None,
85 },
86 ParameterDefinition {
87 name: "password".to_string(),
88 description: "SMTP authentication password (optional)".to_string(),
89 type_hint: "string".to_string(),
90 required: false,
91 example: json!("secret123"),
92 default: None,
93 },
94 ParameterDefinition {
95 name: "use_tls".to_string(),
96 description: "Use STARTTLS for secure connection (default: true)".to_string(),
97 type_hint: "boolean".to_string(),
98 required: false,
99 example: json!(true),
100 default: None,
101 },
102 ]
103 }
104 fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
105 vec![
106 ActionDefinition {
107 name: "send_email".to_string(),
108 description: "Send an email via SMTP".to_string(),
109 parameters: vec![
110 Parameter {
111 name: "from".to_string(),
112 type_hint: "string".to_string(),
113 description: "Sender email address".to_string(),
114 required: true,
115 },
116 Parameter {
117 name: "to".to_string(),
118 type_hint: "array".to_string(),
119 description: "Recipient email addresses".to_string(),
120 required: true,
121 },
122 Parameter {
123 name: "subject".to_string(),
124 type_hint: "string".to_string(),
125 description: "Email subject".to_string(),
126 required: true,
127 },
128 Parameter {
129 name: "body".to_string(),
130 type_hint: "string".to_string(),
131 description: "Email body (plain text)".to_string(),
132 required: true,
133 },
134 Parameter {
135 name: "username".to_string(),
136 type_hint: "string".to_string(),
137 description: "SMTP authentication username (optional)".to_string(),
138 required: false,
139 },
140 Parameter {
141 name: "password".to_string(),
142 type_hint: "string".to_string(),
143 description: "SMTP authentication password (optional)".to_string(),
144 required: false,
145 },
146 Parameter {
147 name: "use_tls".to_string(),
148 type_hint: "boolean".to_string(),
149 description: "Use STARTTLS (default: true)".to_string(),
150 required: false,
151 },
152 ],
153 example: json!({
154 "type": "send_email",
155 "from": "sender@example.com",
156 "to": ["recipient@example.com"],
157 "subject": "Test Email",
158 "body": "Hello, this is a test email from NetGet SMTP client.",
159 "use_tls": true
160 }),
161 log_template: None,
162 },
163 ActionDefinition {
164 name: "disconnect".to_string(),
165 description: "Disconnect from the SMTP server".to_string(),
166 parameters: vec![],
167 example: json!({
168 "type": "disconnect"
169 }),
170 log_template: None,
171 },
172 ]
173 }
174 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
175 vec![ActionDefinition {
176 name: "send_email".to_string(),
177 description: "Send another email in response to previous operation".to_string(),
178 parameters: vec![
179 Parameter {
180 name: "from".to_string(),
181 type_hint: "string".to_string(),
182 description: "Sender email address".to_string(),
183 required: true,
184 },
185 Parameter {
186 name: "to".to_string(),
187 type_hint: "array".to_string(),
188 description: "Recipient email addresses".to_string(),
189 required: true,
190 },
191 Parameter {
192 name: "subject".to_string(),
193 type_hint: "string".to_string(),
194 description: "Email subject".to_string(),
195 required: true,
196 },
197 Parameter {
198 name: "body".to_string(),
199 type_hint: "string".to_string(),
200 description: "Email body".to_string(),
201 required: true,
202 },
203 ],
204 example: json!({
205 "type": "send_email",
206 "from": "sender@example.com",
207 "to": ["recipient@example.com"],
208 "subject": "Follow-up Email",
209 "body": "This is a follow-up email."
210 }),
211 log_template: None,
212 }]
213 }
214 fn protocol_name(&self) -> &'static str {
215 "SMTP"
216 }
217 fn get_event_types(&self) -> Vec<EventType> {
218 vec![
219 EventType::new(
220 "smtp_connected",
221 "Triggered when SMTP client connects to server",
222 json!({"type": "placeholder", "event_id": "smtp_connected"}),
223 ),
224 EventType::new(
225 "smtp_email_sent",
226 "Triggered when email is successfully sent",
227 json!({"type": "placeholder", "event_id": "smtp_email_sent"}),
228 ),
229 ]
230 }
231 fn stack_name(&self) -> &'static str {
232 "ETH>IP>TCP>SMTP"
233 }
234 fn keywords(&self) -> Vec<&'static str> {
235 vec!["smtp", "smtp client", "email", "send email", "mail"]
236 }
237 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
238 use crate::protocol::metadata::{DevelopmentState, ProtocolMetadataV2};
239
240 ProtocolMetadataV2::builder()
241 .state(DevelopmentState::Experimental)
242 .implementation("lettre library with STARTTLS support")
243 .llm_control("Full control over email composition (from, to, subject, body, auth)")
244 .e2e_testing("Local SMTP server or test mail service")
245 .build()
246 }
247 fn description(&self) -> &'static str {
248 "SMTP client for sending emails"
249 }
250 fn example_prompt(&self) -> &'static str {
251 "Connect to SMTP server at smtp.example.com:587 and send an email to user@example.com"
252 }
253 fn group_name(&self) -> &'static str {
254 "Email"
255 }
256
257 fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
258 use crate::llm::actions::StartupExamples;
259 use serde_json::json;
260
261 StartupExamples::new(
262 json!({
264 "type": "open_client",
265 "remote_addr": "smtp.example.com:587",
266 "base_stack": "smtp",
267 "instruction": "Send a test email to user@example.com with subject 'Hello' and body 'Test message'"
268 }),
269 json!({
271 "type": "open_client",
272 "remote_addr": "smtp.example.com:587",
273 "base_stack": "smtp",
274 "event_handlers": [{
275 "event_pattern": "smtp_email_sent",
276 "handler": {
277 "type": "script",
278 "language": "python",
279 "code": "<smtp_client_handler>"
280 }
281 }]
282 }),
283 json!({
285 "type": "open_client",
286 "remote_addr": "smtp.example.com:587",
287 "base_stack": "smtp",
288 "event_handlers": [
289 {
290 "event_pattern": "smtp_connected",
291 "handler": {
292 "type": "static",
293 "actions": [{
294 "type": "send_email",
295 "from": "sender@example.com",
296 "to": ["recipient@example.com"],
297 "subject": "Test Email",
298 "body": "Hello from NetGet SMTP client."
299 }]
300 }
301 },
302 {
303 "event_pattern": "smtp_email_sent",
304 "handler": {
305 "type": "static",
306 "actions": [{
307 "type": "disconnect"
308 }]
309 }
310 }
311 ]
312 }),
313 )
314 }
315}
316
317impl Client for SmtpClientProtocol {
319 fn connect(
320 &self,
321 ctx: crate::protocol::ConnectContext,
322 ) -> std::pin::Pin<
323 Box<dyn std::future::Future<Output = anyhow::Result<std::net::SocketAddr>> + Send>,
324 > {
325 Box::pin(async move {
326 use crate::client::smtp::SmtpClient;
327 SmtpClient::connect_with_llm_actions(
328 ctx.remote_addr,
329 ctx.llm_client,
330 ctx.state,
331 ctx.status_tx,
332 ctx.client_id,
333 ctx.startup_params,
334 )
335 .await
336 })
337 }
338 fn execute_action(&self, action: serde_json::Value) -> Result<ClientActionResult> {
339 let action_type = action
340 .get("type")
341 .and_then(|v| v.as_str())
342 .context("Missing 'type' field in action")?;
343
344 match action_type {
345 "send_email" => {
346 let from = action
347 .get("from")
348 .and_then(|v| v.as_str())
349 .context("Missing 'from' field")?
350 .to_string();
351
352 let to = action
353 .get("to")
354 .and_then(|v| v.as_array())
355 .context("Missing 'to' field or not an array")?
356 .iter()
357 .filter_map(|v| v.as_str().map(|s| s.to_string()))
358 .collect::<Vec<_>>();
359
360 if to.is_empty() {
361 return Err(anyhow::anyhow!(
362 "'to' field must contain at least one email address"
363 ));
364 }
365
366 let subject = action
367 .get("subject")
368 .and_then(|v| v.as_str())
369 .context("Missing 'subject' field")?
370 .to_string();
371
372 let body = action
373 .get("body")
374 .and_then(|v| v.as_str())
375 .context("Missing 'body' field")?
376 .to_string();
377
378 let username = action
379 .get("username")
380 .and_then(|v| v.as_str())
381 .map(|s| s.to_string());
382
383 let password = action
384 .get("password")
385 .and_then(|v| v.as_str())
386 .map(|s| s.to_string());
387
388 let use_tls = action.get("use_tls").and_then(|v| v.as_bool());
394
395 Ok(ClientActionResult::Custom {
397 name: "smtp_send_email".to_string(),
398 data: json!({
399 "from": from,
400 "to": to,
401 "subject": subject,
402 "body": body,
403 "username": username,
404 "password": password,
405 "use_tls": use_tls,
406 }),
407 })
408 }
409 "disconnect" => Ok(ClientActionResult::Disconnect),
410 _ => Err(anyhow::anyhow!(
411 "Unknown SMTP client action: {}",
412 action_type
413 )),
414 }
415 }
416}