botserver/src/basic/keywords/on_email.rs
Rodrigo Rodriguez 5ea171d126
Some checks failed
BotServer CI / build (push) Failing after 1m34s
Refactor: Split large files into modular subdirectories
Split 20+ files over 1000 lines into focused subdirectories for better
maintainability and code organization. All changes maintain backward
compatibility through re-export wrappers.

Major splits:
- attendance/llm_assist.rs (2074→7 modules)
- basic/keywords/face_api.rs → face_api/ (7 modules)
- basic/keywords/file_operations.rs → file_ops/ (8 modules)
- basic/keywords/hear_talk.rs → hearing/ (6 modules)
- channels/wechat.rs → wechat/ (10 modules)
- channels/youtube.rs → youtube/ (5 modules)
- contacts/mod.rs → contacts_api/ (6 modules)
- core/bootstrap/mod.rs → bootstrap/ (5 modules)
- core/shared/admin.rs → admin_*.rs (5 modules)
- designer/canvas.rs → canvas_api/ (6 modules)
- designer/mod.rs → designer_api/ (6 modules)
- docs/handlers.rs → handlers_api/ (11 modules)
- drive/mod.rs → drive_handlers.rs, drive_types.rs
- learn/mod.rs → types.rs
- main.rs → main_module/ (7 modules)
- meet/webinar.rs → webinar_api/ (8 modules)
- paper/mod.rs → (10 modules)
- security/auth.rs → auth_api/ (7 modules)
- security/passkey.rs → (4 modules)
- sources/mod.rs → sources_api/ (5 modules)
- tasks/mod.rs → task_api/ (5 modules)

Stats: 38,040 deletions, 1,315 additions across 318 files

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-12 21:09:30 +00:00

456 lines
15 KiB
Rust

use diesel::prelude::*;
use log::{error, info, trace};
use rhai::{Dynamic, Engine};
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use uuid::Uuid;
use crate::core::shared::models::TriggerKind;
use crate::core::shared::models::UserSession;
use crate::core::shared::state::AppState;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EmailMonitor {
pub id: Uuid,
pub bot_id: Uuid,
pub email_address: String,
pub script_path: String,
pub is_active: bool,
pub filter_from: Option<String>,
pub filter_subject: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EmailReceivedEvent {
pub id: Uuid,
pub monitor_id: Uuid,
pub message_uid: i64,
pub message_id: Option<String>,
pub from_address: String,
pub to_addresses: Vec<String>,
pub subject: Option<String>,
pub has_attachments: bool,
pub attachments: Vec<EmailAttachment>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EmailAttachment {
pub filename: String,
pub mime_type: String,
pub size: i64,
}
pub fn on_email_keyword(state: &AppState, user: UserSession, engine: &mut Engine) {
register_on_email(state, user.clone(), engine);
register_on_email_from(state, user.clone(), engine);
register_on_email_subject(state, user, engine);
}
fn register_on_email(state: &AppState, user: UserSession, engine: &mut Engine) {
let state_clone = state.clone();
let bot_id = user.bot_id;
engine
.register_custom_syntax(["ON", "EMAIL", "$string$"], true, move |context, inputs| {
let email_address = context
.eval_expression_tree(&inputs[0])?
.to_string()
.trim_matches('"')
.to_string();
trace!("ON EMAIL '{}' for bot: {}", email_address, bot_id);
let script_name = format!(
"on_email_{}.rhai",
email_address.replace('@', "_at_").replace('.', "_")
);
let mut conn = state_clone
.conn
.get()
.map_err(|e| format!("DB error: {}", e))?;
let result =
execute_on_email(&mut conn, bot_id, &email_address, &script_name, None, None)
.map_err(|e| format!("DB error: {}", e))?;
if let Some(rows_affected) = result.get("rows_affected") {
info!(
"Email monitor registered for '{}' on bot {}",
email_address, bot_id
);
Ok(Dynamic::from(rows_affected.as_i64().unwrap_or(0)))
} else {
Err("Failed to register email monitor".into())
}
})
.expect("valid syntax registration");
}
fn register_on_email_from(state: &AppState, user: UserSession, engine: &mut Engine) {
let state_clone = state.clone();
let bot_id = user.bot_id;
engine
.register_custom_syntax(
["ON", "EMAIL", "$string$", "FROM", "$string$"],
true,
move |context, inputs| {
let email_address = context
.eval_expression_tree(&inputs[0])?
.to_string()
.trim_matches('"')
.to_string();
let filter_from = context
.eval_expression_tree(&inputs[1])?
.to_string()
.trim_matches('"')
.to_string();
trace!(
"ON EMAIL '{}' FROM '{}' for bot: {}",
email_address,
filter_from,
bot_id
);
let script_name = format!(
"on_email_{}_from_{}.rhai",
email_address.replace('@', "_at_").replace('.', "_"),
filter_from.replace('@', "_at_").replace('.', "_")
);
let mut conn = state_clone
.conn
.get()
.map_err(|e| format!("DB error: {}", e))?;
let result = execute_on_email(
&mut conn,
bot_id,
&email_address,
&script_name,
Some(&filter_from),
None,
)
.map_err(|e| format!("DB error: {}", e))?;
if let Some(rows_affected) = result.get("rows_affected") {
info!(
"Email monitor registered for '{}' from '{}' on bot {}",
email_address, filter_from, bot_id
);
Ok(Dynamic::from(rows_affected.as_i64().unwrap_or(0)))
} else {
Err("Failed to register email monitor".into())
}
},
)
.expect("valid syntax registration");
}
fn register_on_email_subject(state: &AppState, user: UserSession, engine: &mut Engine) {
let state_clone = state.clone();
let bot_id = user.bot_id;
engine
.register_custom_syntax(
["ON", "EMAIL", "$string$", "SUBJECT", "$string$"],
true,
move |context, inputs| {
let email_address = context
.eval_expression_tree(&inputs[0])?
.to_string()
.trim_matches('"')
.to_string();
let filter_subject = context
.eval_expression_tree(&inputs[1])?
.to_string()
.trim_matches('"')
.to_string();
trace!(
"ON EMAIL '{}' SUBJECT '{}' for bot: {}",
email_address,
filter_subject,
bot_id
);
let script_name = format!(
"on_email_{}_subject.rhai",
email_address.replace('@', "_at_").replace('.', "_")
);
let mut conn = state_clone
.conn
.get()
.map_err(|e| format!("DB error: {}", e))?;
let result = execute_on_email(
&mut conn,
bot_id,
&email_address,
&script_name,
None,
Some(&filter_subject),
)
.map_err(|e| format!("DB error: {}", e))?;
if let Some(rows_affected) = result.get("rows_affected") {
info!(
"Email monitor registered for '{}' with subject filter '{}' on bot {}",
email_address, filter_subject, bot_id
);
Ok(Dynamic::from(rows_affected.as_i64().unwrap_or(0)))
} else {
Err("Failed to register email monitor".into())
}
},
)
.expect("valid syntax registration");
}
pub fn execute_on_email(
conn: &mut diesel::PgConnection,
bot_id: Uuid,
email_address: &str,
script_path: &str,
filter_from: Option<&str>,
filter_subject: Option<&str>,
) -> Result<Value, String> {
use crate::core::shared::models::system_automations;
let new_automation = (
system_automations::kind.eq(TriggerKind::EmailReceived as i32),
system_automations::target.eq(email_address),
system_automations::param.eq(script_path),
system_automations::bot_id.eq(bot_id),
);
let result = diesel::insert_into(system_automations::table)
.values(&new_automation)
.execute(conn)
.map_err(|e| {
error!("SQL execution error: {}", e);
e.to_string()
})?;
let monitor_id = Uuid::new_v4();
let insert_sql = format!(
"INSERT INTO email_monitors (id, bot_id, email_address, script_path, filter_from, filter_subject, is_active) \
VALUES ('{}', '{}', '{}', '{}', {}, {}, true) \
ON CONFLICT (bot_id, email_address) DO UPDATE SET \
script_path = EXCLUDED.script_path, \
filter_from = EXCLUDED.filter_from, \
filter_subject = EXCLUDED.filter_subject, \
is_active = true, \
updated_at = NOW()",
monitor_id,
bot_id,
email_address.replace('\'', "''"),
script_path.replace('\'', "''"),
filter_from.map(|f| format!("'{}'", f.replace('\'', "''"))).unwrap_or_else(|| "NULL".to_string()),
filter_subject.map(|s| format!("'{}'", s.replace('\'', "''"))).unwrap_or_else(|| "NULL".to_string())
);
diesel::sql_query(&insert_sql).execute(conn).map_err(|e| {
error!("Failed to insert email monitor: {}", e);
e.to_string()
})?;
Ok(json!({
"command": "on_email",
"email_address": email_address,
"script_path": script_path,
"filter_from": filter_from,
"filter_subject": filter_subject,
"rows_affected": result
}))
}
pub fn check_email_monitors(
state: &AppState,
bot_id: Uuid,
) -> Result<Vec<(EmailReceivedEvent, String)>, String> {
let mut conn = state.conn.get().map_err(|e| e.to_string())?;
let monitors_sql = format!(
"SELECT id, bot_id, email_address, script_path, filter_from, filter_subject, last_uid \
FROM email_monitors WHERE bot_id = '{}' AND is_active = true",
bot_id
);
#[derive(QueryableByName)]
struct MonitorRow {
#[diesel(sql_type = diesel::sql_types::Uuid)]
id: Uuid,
#[diesel(sql_type = diesel::sql_types::Uuid)]
bot_id: Uuid,
#[diesel(sql_type = diesel::sql_types::Text)]
email_address: String,
#[diesel(sql_type = diesel::sql_types::Text)]
script_path: String,
#[diesel(sql_type = diesel::sql_types::Nullable<diesel::sql_types::Text>)]
filter_from: Option<String>,
#[diesel(sql_type = diesel::sql_types::Nullable<diesel::sql_types::Text>)]
filter_subject: Option<String>,
#[diesel(sql_type = diesel::sql_types::Nullable<diesel::sql_types::BigInt>)]
last_uid: Option<i64>,
}
let monitors: Vec<MonitorRow> = diesel::sql_query(&monitors_sql)
.load(&mut *conn)
.map_err(|e| e.to_string())?;
let mut events = Vec::new();
for monitor in monitors {
trace!(
"Checking email monitor for {} on bot {} (last_uid: {:?})",
monitor.email_address,
monitor.bot_id,
monitor.last_uid
);
let new_events = fetch_new_emails(
state,
monitor.id,
&monitor.email_address,
monitor.last_uid.unwrap_or(0),
monitor.filter_from.as_deref(),
monitor.filter_subject.as_deref(),
)?;
for event in new_events {
events.push((event, monitor.script_path.clone()));
}
}
Ok(events)
}
fn fetch_new_emails(
_state: &AppState,
monitor_id: Uuid,
email_address: &str,
_last_uid: i64,
filter_from: Option<&str>,
filter_subject: Option<&str>,
) -> Result<Vec<EmailReceivedEvent>, String> {
// Simulation: IMAP requires proper server setup
// Returns simulated email events for testing
info!(
"Email monitoring for {}: returning simulated events (requires IMAP setup for real emails)",
email_address
);
let _now = chrono::Utc::now();
let mut events = Vec::new();
// Simulated email 1
let from1 = "sender@example.com";
let subject1 = "Weekly Report";
let matches_from = filter_from.map(|f| from1.to_lowercase().contains(&f.to_lowercase())).unwrap_or(true);
let matches_subject = filter_subject.map(|s| subject1.to_lowercase().contains(&s.to_lowercase())).unwrap_or(true);
if matches_from && matches_subject {
events.push(EmailReceivedEvent {
id: Uuid::new_v4(),
monitor_id,
message_uid: 1001,
message_id: Some(format!("<{}@example.com>", Uuid::new_v4())),
from_address: from1.to_string(),
to_addresses: vec![email_address.to_string()],
subject: Some(subject1.to_string()),
has_attachments: true,
attachments: vec![EmailAttachment {
filename: "report.pdf".to_string(),
mime_type: "application/pdf".to_string(),
size: 1024,
}],
});
}
// Simulated email 2
let from2 = "notifications@service.com";
let subject2 = "New notification";
let matches_from2 = filter_from.map(|f| from2.to_lowercase().contains(&f.to_lowercase())).unwrap_or(true);
let matches_subject2 = filter_subject.map(|s| subject2.to_lowercase().contains(&s.to_lowercase())).unwrap_or(true);
if matches_from2 && matches_subject2 {
events.push(EmailReceivedEvent {
id: Uuid::new_v4(),
monitor_id,
message_uid: 1002,
message_id: Some(format!("<{}@service.com>", Uuid::new_v4())),
from_address: from2.to_string(),
to_addresses: vec![email_address.to_string()],
subject: Some(subject2.to_string()),
has_attachments: false,
attachments: vec![],
});
}
Ok(events)
}
fn _decrypt_password(_encrypted: &str) -> Option<String> {
// Simulation: password decryption placeholder
None
}
pub fn process_email_event(
state: &AppState,
event: &EmailReceivedEvent,
script_path: &str,
) -> Result<(), String> {
info!(
"Processing email event {} from {} with script {}",
event.id, event.from_address, script_path
);
let mut conn = state.conn.get().map_err(|e| e.to_string())?;
let update_sql = format!(
"UPDATE email_received_events SET processed = true, processed_at = NOW() WHERE id = '{}'",
event.id
);
diesel::sql_query(&update_sql)
.execute(&mut *conn)
.map_err(|e| e.to_string())?;
Ok(())
}
pub fn parse_email_path(path: &str) -> Option<(String, Option<String>)> {
if let Some(rest) = path.strip_prefix("email://") {
if let Some(slash_pos) = rest.find('/') {
let email = &rest[..slash_pos];
let folder = &rest[slash_pos + 1..];
return Some((email.to_string(), Some(folder.to_string())));
}
return Some((rest.to_string(), None));
}
None
}
pub fn is_email_path(path: &str) -> bool {
path.starts_with("email://")
}
pub fn sanitize_email_for_filename(email: &str) -> String {
email
.replace('@', "_at_")
.replace('.', "_")
.chars()
.filter(|c| c.is_alphanumeric() || *c == '_' || *c == '-')
.collect::<String>()
.to_lowercase()
}