1pub mod actions;
3
4pub use actions::SmtpClientProtocol;
5
6use anyhow::{Context, Result};
7use lettre::transport::smtp::authentication::Credentials;
8use lettre::transport::smtp::client::{Tls, TlsParameters};
9use lettre::{SmtpTransport, Transport};
10use std::net::SocketAddr;
11use std::sync::Arc;
12use std::time::Duration;
13use tokio::sync::mpsc;
14use tracing::{error, info, trace};
15
16use crate::client::llm_budget::call_llm_for_client;
17use crate::client::smtp::actions::SMTP_CLIENT_CONNECTED_EVENT;
18use crate::llm::actions::client_trait::{Client, ClientActionResult};
19use crate::llm::ollama_client::OllamaClient;
20use crate::llm::ClientLlmResult;
21use crate::protocol::{Event, StartupParams};
22use crate::state::app_state::AppState;
23use crate::state::{ClientId, ClientStatus};
24
25const LIVENESS_INTERVAL: Duration = Duration::from_secs(5);
28
29#[derive(Clone, Debug, Default)]
45struct SmtpSettings {
46 username: Option<String>,
47 password: Option<String>,
48 use_tls: Option<bool>,
50}
51
52impl SmtpSettings {
53 fn from_startup_params(params: Option<&StartupParams>) -> Result<Self> {
56 let Some(params) = params else {
57 return Ok(Self::default());
58 };
59 Ok(Self {
60 username: params.get_optional_string("username")?,
61 password: params.get_optional_string("password")?,
62 use_tls: params.get_optional_bool("use_tls")?,
63 })
64 }
65}
66
67enum SmtpApplied {
72 Delivered {
74 to: Vec<String>,
75 subject: String,
76 endpoint: String,
77 },
78 Disconnect,
80 Nothing(String),
82}
83
84pub struct SmtpClient;
86
87impl SmtpClient {
88 pub async fn connect_with_llm_actions(
90 remote_addr: String,
91 llm_client: OllamaClient,
92 app_state: Arc<AppState>,
93 status_tx: mpsc::UnboundedSender<String>,
94 client_id: ClientId,
95 startup_params: Option<StartupParams>,
96 ) -> Result<SocketAddr> {
97 info!(
98 "SMTP client {} initializing connection to {}",
99 client_id, remote_addr
100 );
101
102 let settings = Arc::new(SmtpSettings::from_startup_params(startup_params.as_ref())?);
105
106 let (smtp_server, smtp_port) = match remote_addr.rsplit_once(':') {
110 Some((host, port)) => (host.to_string(), port.parse::<u16>().ok()),
111 None => (remote_addr.clone(), None),
112 };
113
114 app_state
116 .with_client_mut(client_id, |client| {
117 client
118 .set_protocol_field("smtp_server".to_string(), serde_json::json!(smtp_server));
119 client.set_protocol_field("smtp_port".to_string(), serde_json::json!(smtp_port));
120 client
121 .set_protocol_field("remote_addr".to_string(), serde_json::json!(remote_addr));
122 })
123 .await;
124
125 app_state
127 .update_client_status(client_id, ClientStatus::Connected)
128 .await;
129 let _ = status_tx.send(format!(
130 "[CLIENT] SMTP client {} ready for {}",
131 client_id, remote_addr
132 ));
133 let _ = status_tx.send("__UPDATE_UI__".to_string());
134
135 let command_rx =
141 crate::client::command_support::register_command_channel(&app_state, client_id).await;
142 let command_task = tokio::spawn(Self::idle_and_command_loop(
143 command_rx,
144 client_id,
145 llm_client.clone(),
146 app_state.clone(),
147 status_tx.clone(),
148 settings.clone(),
149 ));
150 app_state
151 .register_client_task(client_id, command_task)
152 .await;
153
154 if let Some(instruction) = app_state.get_instruction_for_client(client_id).await {
156 let protocol = SmtpClientProtocol::new();
157 let event = Event::new(
158 &SMTP_CLIENT_CONNECTED_EVENT,
159 serde_json::json!({
160 "smtp_server": smtp_server,
161 }),
162 );
163
164 let memory = app_state
165 .get_memory_for_client(client_id)
166 .await
167 .unwrap_or_default();
168
169 match call_llm_for_client(
170 &llm_client,
171 &app_state,
172 client_id.to_string(),
173 &instruction,
174 &memory,
175 Some(&event),
176 &protocol,
177 &status_tx,
178 )
179 .await
180 {
181 Ok(ClientLlmResult {
182 actions,
183 memory_updates,
184 }) => {
185 if let Some(mem) = memory_updates {
187 app_state.set_memory_for_client(client_id, mem).await;
188 }
189
190 for action in actions {
195 match Self::apply_action(client_id, &app_state, action, &settings).await {
196 Ok(applied) => {
197 Self::follow_up(
198 applied,
199 client_id,
200 &llm_client,
201 &app_state,
202 &status_tx,
203 &settings,
204 )
205 .await
206 }
207 Err(e) => {
208 error!(
209 "SMTP client {} could not execute action after connect: {}",
210 client_id, e
211 );
212 }
213 }
214 }
215 }
216 Err(e) => {
217 error!("LLM error for SMTP client {}: {}", client_id, e);
218 }
219 }
220 }
221
222 Ok("0.0.0.0:0".parse().unwrap())
224 }
225
226 async fn idle_and_command_loop(
237 mut command_rx: mpsc::Receiver<crate::state::client_handles::ClientCommand>,
238 client_id: ClientId,
239 llm_client: OllamaClient,
240 app_state: Arc<AppState>,
241 status_tx: mpsc::UnboundedSender<String>,
242 settings: Arc<SmtpSettings>,
243 ) {
244 use crate::llm::actions::protocol_trait::Protocol;
245 use crate::state::client_handles::ClientSendOutcome;
246 use crate::state::AccessLogOwner;
247
248 let protocol = SmtpClientProtocol::new();
249 let mut liveness = tokio::time::interval(LIVENESS_INTERVAL);
250 liveness.tick().await; loop {
253 let command = tokio::select! {
254 _ = liveness.tick() => {
255 if app_state.get_client(client_id).await.is_none() {
256 info!("SMTP client {} stopped", client_id);
257 break;
258 }
259 continue;
260 }
261 command = command_rx.recv() => match command {
262 Some(command) => command,
263 None => break,
264 },
265 };
266
267 let action = command.action.clone();
268 let mut applied_for_follow_up = None;
269 let mut disconnect = false;
270
271 let outcome: Result<ClientSendOutcome> =
272 match Self::apply_action(client_id, &app_state, action.clone(), &settings).await {
273 Err(e) => Ok(ClientSendOutcome::Rejected {
274 error: e.to_string(),
275 }),
276 Ok(SmtpApplied::Disconnect) => {
277 disconnect = true;
278 Ok(ClientSendOutcome::Disconnected)
279 }
280 Ok(SmtpApplied::Nothing(detail)) => Ok(ClientSendOutcome::Executed { detail }),
281 Ok(SmtpApplied::Delivered {
282 to,
283 subject,
284 endpoint,
285 }) => {
286 let detail = format!(
287 "send_email '{}': accepted by {} for {} recipient(s)",
288 subject,
289 endpoint,
290 to.len()
291 );
292 applied_for_follow_up = Some(SmtpApplied::Delivered {
293 to,
294 subject,
295 endpoint,
296 });
297 Ok(ClientSendOutcome::Executed { detail })
298 }
299 };
300
301 let outcome_json = match &outcome {
302 Ok(outcome) => serde_json::to_value(outcome).unwrap_or(serde_json::Value::Null),
303 Err(e) => serde_json::json!({"error": e.to_string()}),
304 };
305 app_state
306 .record_access_log(
307 AccessLogOwner::Client(client_id.as_u32()),
308 protocol.protocol_name(),
309 None,
310 "injected_action",
311 action,
312 vec![outcome_json],
313 )
314 .await;
315
316 if let Err(e) = &outcome {
317 error!("SMTP client {} injected action failed: {}", client_id, e);
318 let _ = status_tx.send(format!(
319 "[WARN] Client {} injected action failed: {}",
320 client_id, e
321 ));
322 }
323 let _ = status_tx.send("__UPDATE_UI__".to_string());
324 crate::client::command_support::reply(command, outcome);
325
326 if disconnect {
327 app_state
328 .update_client_status(client_id, ClientStatus::Disconnected)
329 .await;
330 break;
331 }
332
333 if let Some(applied) = applied_for_follow_up {
336 let llm_client = llm_client.clone();
337 let state = app_state.clone();
338 let tx = status_tx.clone();
339 let settings = settings.clone();
340 let handle = tokio::spawn(async move {
341 Self::follow_up(applied, client_id, &llm_client, &state, &tx, &settings).await;
342 });
343 app_state.register_client_task(client_id, handle).await;
344 }
345 }
346
347 app_state.remove_client_handle(client_id).await;
350 let _ = status_tx.send("__UPDATE_UI__".to_string());
351 }
352
353 async fn apply_action(
359 client_id: ClientId,
360 app_state: &Arc<AppState>,
361 action: serde_json::Value,
362 settings: &SmtpSettings,
363 ) -> Result<SmtpApplied> {
364 let protocol = SmtpClientProtocol::new();
365 match protocol.execute_action(action)? {
366 ClientActionResult::Custom { name, data } if name == "smtp_send_email" => {
367 let to: Vec<String> = data
368 .get("to")
369 .and_then(|v| v.as_array())
370 .map(|a| {
371 a.iter()
372 .filter_map(|v| v.as_str().map(|s| s.to_string()))
373 .collect()
374 })
375 .unwrap_or_default();
376 let subject = data
377 .get("subject")
378 .and_then(|v| v.as_str())
379 .unwrap_or_default()
380 .to_string();
381
382 let endpoint = Self::deliver(client_id, app_state, &data, settings).await?;
383 Ok(SmtpApplied::Delivered {
384 to,
385 subject,
386 endpoint,
387 })
388 }
389 ClientActionResult::Disconnect => Ok(SmtpApplied::Disconnect),
390 ClientActionResult::WaitForMore => {
391 Ok(SmtpApplied::Nothing("wait_for_more".to_string()))
392 }
393 other => Ok(SmtpApplied::Nothing(format!(
394 "unsupported action result {other:?}"
395 ))),
396 }
397 }
398
399 async fn deliver(
401 client_id: ClientId,
402 app_state: &Arc<AppState>,
403 data: &serde_json::Value,
404 settings: &SmtpSettings,
405 ) -> Result<String> {
406 use lettre::message::Message;
407
408 let (smtp_server, smtp_port) = app_state
409 .with_client_mut(client_id, |client| {
410 (
411 client
412 .get_protocol_field("smtp_server")
413 .and_then(|v| v.as_str())
414 .map(|s| s.to_string()),
415 client
416 .get_protocol_field("smtp_port")
417 .and_then(|v| v.as_u64())
418 .and_then(|p| u16::try_from(p).ok()),
419 )
420 })
421 .await
422 .context("SMTP client is no longer registered")?;
423 let smtp_server = smtp_server.context("No SMTP server found")?;
424
425 let from = data
426 .get("from")
427 .and_then(|v| v.as_str())
428 .context("Missing 'from' field")?;
429 let to: Vec<String> = data
430 .get("to")
431 .and_then(|v| v.as_array())
432 .context("Missing 'to' field")?
433 .iter()
434 .filter_map(|v| v.as_str().map(|s| s.to_string()))
435 .collect();
436 let subject = data
437 .get("subject")
438 .and_then(|v| v.as_str())
439 .unwrap_or_default();
440 let body = data
441 .get("body")
442 .and_then(|v| v.as_str())
443 .unwrap_or_default()
444 .to_string();
445 let username = data
450 .get("username")
451 .and_then(|v| v.as_str())
452 .map(|s| s.to_string())
453 .or_else(|| settings.username.clone());
454 let password = data
455 .get("password")
456 .and_then(|v| v.as_str())
457 .map(|s| s.to_string())
458 .or_else(|| settings.password.clone());
459 let use_tls = data
460 .get("use_tls")
461 .and_then(|v| v.as_bool())
462 .or(settings.use_tls)
463 .unwrap_or(true);
464
465 info!("SMTP client {} sending email to {:?}", client_id, to);
466
467 let mut message_builder = Message::builder()
468 .from(from.parse().context("Invalid 'from' address")?)
469 .subject(subject);
470 for recipient in &to {
471 message_builder =
472 message_builder.to(recipient.parse().context("Invalid 'to' address")?);
473 }
474 let email = message_builder
475 .body(body)
476 .context("Failed to build email message")?;
477
478 let mut transport_builder =
479 SmtpTransport::relay(&smtp_server).context("Failed to create SMTP transport")?;
480 if let Some(port) = smtp_port {
481 transport_builder = transport_builder.port(port);
482 }
483
484 if let (Some(user), Some(pass)) = (username, password) {
485 transport_builder = transport_builder.credentials(Credentials::new(user, pass));
486 }
487
488 if use_tls {
489 let tls_parameters = TlsParameters::builder(smtp_server.clone())
490 .dangerous_accept_invalid_certs(false)
491 .build()
492 .context("Failed to build TLS parameters")?;
493 transport_builder = transport_builder.tls(Tls::Required(tls_parameters));
494 } else {
495 transport_builder = transport_builder.tls(Tls::None);
496 }
497
498 let mailer = transport_builder.build();
499
500 let response = tokio::task::spawn_blocking(move || mailer.send(&email))
502 .await
503 .context("Task join error")?
504 .context("SMTP server refused the message")?;
505
506 info!(
507 "SMTP client {} sent email successfully: {:?}",
508 client_id, response
509 );
510
511 Ok(match smtp_port {
512 Some(port) => format!("{smtp_server}:{port}"),
513 None => smtp_server,
514 })
515 }
516
517 async fn follow_up(
520 applied: SmtpApplied,
521 client_id: ClientId,
522 llm_client: &OllamaClient,
523 app_state: &Arc<AppState>,
524 status_tx: &mpsc::UnboundedSender<String>,
525 settings: &SmtpSettings,
526 ) {
527 let SmtpApplied::Delivered { to, subject, .. } = applied else {
528 match applied {
529 SmtpApplied::Disconnect => {
530 info!("SMTP client {} disconnecting", client_id);
531 app_state.remove_client_handle(client_id).await;
532 app_state
533 .update_client_status(client_id, ClientStatus::Disconnected)
534 .await;
535 let _ = status_tx.send("__UPDATE_UI__".to_string());
536 }
537 SmtpApplied::Nothing(detail) => {
538 trace!("SMTP client {} action sent nothing: {}", client_id, detail);
539 }
540 SmtpApplied::Delivered { .. } => unreachable!(),
541 }
542 return;
543 };
544
545 let _ = status_tx.send("[CLIENT] SMTP email sent successfully".to_string());
546
547 let Some(instruction) = app_state.get_instruction_for_client(client_id).await else {
548 return;
549 };
550 let protocol = SmtpClientProtocol::new();
551 let event = Event::new(
552 &crate::client::smtp::actions::SMTP_CLIENT_EMAIL_SENT_EVENT,
553 serde_json::json!({
554 "to": to,
555 "subject": subject,
556 "success": true,
557 }),
558 );
559
560 let memory = app_state
561 .get_memory_for_client(client_id)
562 .await
563 .unwrap_or_default();
564
565 match call_llm_for_client(
566 llm_client,
567 app_state,
568 client_id.to_string(),
569 &instruction,
570 &memory,
571 Some(&event),
572 &protocol,
573 status_tx,
574 )
575 .await
576 {
577 Ok(ClientLlmResult {
578 actions,
579 memory_updates,
580 }) => {
581 if let Some(mem) = memory_updates {
582 app_state.set_memory_for_client(client_id, mem).await;
583 }
584 for action in actions {
585 match Self::apply_action(client_id, app_state, action, settings).await {
586 Ok(applied) => {
587 Box::pin(Self::follow_up(
588 applied, client_id, llm_client, app_state, status_tx, settings,
589 ))
590 .await
591 }
592 Err(e) => {
593 error!("SMTP client {} follow-up action failed: {}", client_id, e)
594 }
595 }
596 }
597 }
598 Err(e) => {
599 error!("LLM error for SMTP client {}: {}", client_id, e);
600 }
601 }
602 }
603}