use crate::shared::state::AppState; use crate::core::config::EmailConfig; use super::types::*; use axum::{ extract::{Path, Query, State}, response::IntoResponse, Json, }; use diesel::prelude::*; use log::{error, info, warn}; use mailparse::{parse_mail, MailHeaderMap}; use std::sync::Arc; use uuid::Uuid; fn extract_user_from_session(_state: &Arc) -> Result { Ok(Uuid::new_v4()) } fn fetch_emails_from_folder( config: &EmailConfig, folder: &str, ) -> Result, String> { #[cfg(feature = "mail")] { let client = imap::ClientBuilder::new(&config.server, config.port) .connect() .map_err(|e| format!("Connection error: {}", e))?; let mut session = client .login(&config.username, &config.password) .map_err(|e| format!("Login failed: {:?}", e))?; let folder_name = match folder { "sent" => "Sent", "drafts" => "Drafts", "trash" => "Trash", _ => "INBOX", }; session .select(folder_name) .map_err(|e| format!("Select folder failed: {}", e))?; let messages = session .fetch("1:20", "(FLAGS RFC822.HEADER)") .map_err(|e| format!("Fetch failed: {}", e))?; let mut emails = Vec::new(); for message in messages.iter() { if let Some(header) = message.header() { let parsed = parse_mail(header).ok(); if let Some(mail) = parsed { let subject = mail.headers.get_first_value("Subject").unwrap_or_default(); let from = mail.headers.get_first_value("From").unwrap_or_default(); let date = mail.headers.get_first_value("Date").unwrap_or_default(); let flags = message.flags(); let read = flags.iter().any(|f| matches!(f, imap::types::Flag::Seen)); let preview = subject.chars().take(100).collect(); emails.push(EmailSummary { id: message.message.to_string(), from_name: from.clone(), from_email: from, subject, preview, date, read, }); } } } session.logout().ok(); Ok(emails) } #[cfg(not(feature = "mail"))] { Ok(Vec::new()) } } fn get_folder_counts( config: &EmailConfig, ) -> Result, String> { use std::collections::HashMap; #[cfg(feature = "mail")] { let client = imap::ClientBuilder::new(&config.server, config.port) .connect() .map_err(|e| format!("Connection error: {}", e))?; let mut session = client .login(&config.username, &config.password) .map_err(|e| format!("Login failed: {:?}", e))?; let mut counts = HashMap::new(); for folder in ["INBOX", "Sent", "Drafts", "Trash"] { if let Ok(mailbox) = session.examine(folder) { counts.insert((*folder).to_string(), mailbox.exists as usize); } } session.logout().ok(); Ok(counts) } #[cfg(not(feature = "mail"))] { Ok(HashMap::new()) } } fn fetch_email_by_id(config: &EmailConfig, id: &str) -> Result { #[cfg(feature = "mail")] { let client = imap::ClientBuilder::new(&config.server, config.port) .connect() .map_err(|e| format!("Connection error: {}", e))?; let mut session = client .login(&config.username, &config.password) .map_err(|e| format!("Login failed: {:?}", e))?; session .select("INBOX") .map_err(|e| format!("Select failed: {}", e))?; let messages = session .fetch(id, "RFC822") .map_err(|e| format!("Fetch failed: {}", e))?; if let Some(message) = messages.iter().next() { if let Some(body) = message.body() { let parsed = parse_mail(body).map_err(|e| format!("Parse failed: {}", e))?; let subject = parsed .headers .get_first_value("Subject") .unwrap_or_default(); let from = parsed.headers.get_first_value("From").unwrap_or_default(); let to = parsed.headers.get_first_value("To").unwrap_or_default(); let date = parsed.headers.get_first_value("Date").unwrap_or_default(); let body_text = parsed .subparts .iter() .find_map(|p| p.get_body().ok()) .or_else(|| parsed.get_body().ok()) .unwrap_or_default(); session.logout().ok(); return Ok(EmailContent { id: id.to_string(), from_name: from.clone(), from_email: from, to, subject, body: body_text, date, read: false, }); } } session.logout().ok(); Err("Email not found".to_string()) } #[cfg(not(feature = "mail"))] { Err("Mail feature not enabled".to_string()) } } fn move_email_to_trash(config: &EmailConfig, id: &str) -> Result<(), String> { #[cfg(feature = "mail")] { let client = imap::ClientBuilder::new(&config.server, config.port) .connect() .map_err(|e| format!("Connection error: {}", e))?; let mut session = client .login(&config.username, &config.password) .map_err(|e| format!("Login failed: {:?}", e))?; session .select("INBOX") .map_err(|e| format!("Select failed: {}", e))?; session .store(id, "+FLAGS (\\Deleted)") .map_err(|e| format!("Store failed: {}", e))?; session .expunge() .map_err(|e| format!("Expunge failed: {}", e))?; session.logout().ok(); Ok(()) } #[cfg(not(feature = "mail"))] { Err("Mail feature not enabled".to_string()) } } pub async fn list_emails_htmx( State(state): State>, Query(params): Query>, ) -> impl IntoResponse { let folder = params .get("folder") .cloned() .unwrap_or_else(|| "inbox".to_string()); let user_id = match extract_user_from_session(&state) { Ok(id) => id, Err(_) => { return axum::response::Html( r#"

Authentication required

Please sign in to view your emails

"# .to_string(), ); } }; let conn = state.conn.clone(); let account_result = tokio::task::spawn_blocking(move || { let db_conn_result = conn.get(); let mut db_conn = match db_conn_result { Ok(c) => c, Err(e) => return Err(format!("DB connection error: {}", e)), }; diesel::sql_query("SELECT * FROM user_email_accounts WHERE user_id = $1 LIMIT 1") .bind::(user_id) .get_result::(&mut db_conn) .optional() .map_err(|e| format!("Failed to get email account: {}", e)) }) .await; let account = match account_result { Ok(Ok(Some(acc))) => acc, Ok(Ok(None)) => { return axum::response::Html( r##"

No email account configured

Please add an email account in settings to get started

Add Email Account
"## .to_string(), ); } Ok(Err(e)) => { error!("Email account query error: {}", e); return axum::response::Html( r#"

Unable to load emails

There was an error connecting to the database. Please try again later.

"# .to_string(), ); } Err(e) => { error!("Task join error: {}", e); return axum::response::Html( r#"

Unable to load emails

An internal error occurred. Please try again later.

"# .to_string(), ); } }; let config = EmailConfig { username: account.username.clone(), password: account.password_encrypted.clone(), server: account.imap_server.clone(), port: account.imap_port as u16, from: account.email.clone(), smtp_server: account.smtp_server.clone(), smtp_port: account.smtp_port as u16, }; let emails = fetch_emails_from_folder(&config, &folder).unwrap_or_default(); let mut html = String::new(); use std::fmt::Write; for email in &emails { let unread_class = if !email.read { "unread" } else { "" }; let _ = write!( html, r##"
{} {}
{}
{}
"##, unread_class, email.id, email.from_name, email.date, email.subject, email.preview ); } if html.is_empty() { html = format!( r#"

No emails in {}

This folder is empty

"#, folder ); } axum::response::Html(html) } pub async fn list_folders_htmx( State(state): State>, ) -> impl IntoResponse { let user_id = match extract_user_from_session(&state) { Ok(id) => id, Err(_) => { return axum::response::Html( r#""#.to_string(), ); } }; let conn = state.conn.clone(); let account_result = tokio::task::spawn_blocking(move || { let db_conn_result = conn.get(); let mut db_conn = match db_conn_result { Ok(c) => c, Err(e) => return Err(format!("DB connection error: {}", e)), }; diesel::sql_query("SELECT * FROM user_email_accounts WHERE user_id = $1 LIMIT 1") .bind::(user_id) .get_result::(&mut db_conn) .optional() .map_err(|e| format!("Failed to get email account: {}", e)) }) .await; let account = match account_result { Ok(Ok(Some(acc))) => acc, Ok(Ok(None)) => { return axum::response::Html( r#""#.to_string(), ); } Ok(Err(e)) => { error!("Email folder query error: {}", e); return axum::response::Html( r#""#.to_string(), ); } Err(e) => { error!("Task join error: {}", e); return axum::response::Html( r#""#.to_string(), ); } }; let config = EmailConfig { username: account.username.clone(), password: account.password_encrypted.clone(), server: account.imap_server.clone(), port: account.imap_port as u16, from: account.email.clone(), smtp_server: account.smtp_server.clone(), smtp_port: account.smtp_port as u16, }; let folder_counts = get_folder_counts(&config).unwrap_or_default(); let mut html = String::new(); for (folder_name, icon, count) in &[ ("inbox", "", folder_counts.get("INBOX").unwrap_or(&0)), ("sent", "", folder_counts.get("Sent").unwrap_or(&0)), ("drafts", "", folder_counts.get("Drafts").unwrap_or(&0)), ("trash", "", folder_counts.get("Trash").unwrap_or(&0)), ] { let active = if *folder_name == "inbox" { "active" } else { "" }; let count_badge = if **count > 0 { format!( r#"{}"#, count ) } else { String::new() }; use std::fmt::Write; let _ = write!( html, r##""##, active, folder_name, icon, folder_name .chars() .next() .unwrap_or_default() .to_uppercase() .collect::() + &folder_name[1..], count_badge ); } axum::response::Html(html) } pub async fn compose_email_htmx( State(_state): State>, ) -> Result { let html = r##"

Compose New Email

"##; Ok(axum::response::Html(html)) } pub async fn get_email_content_htmx( State(state): State>, Path(id): Path, ) -> Result { let user_id = extract_user_from_session(&state) .map_err(|_| EmailError("Authentication required".to_string()))?; let conn = state.conn.clone(); let account = tokio::task::spawn_blocking(move || { let mut db_conn = conn .get() .map_err(|e| format!("DB connection error: {}", e))?; diesel::sql_query("SELECT * FROM user_email_accounts WHERE user_id = $1 LIMIT 1") .bind::(user_id) .get_result::(&mut db_conn) .optional() .map_err(|e| format!("Failed to get email account: {}", e)) }) .await .map_err(|e| EmailError(format!("Task join error: {e}")))? .map_err(EmailError)?; let Some(account) = account else { return Ok(axum::response::Html( r#"

No email account configured

"# .to_string(), )); }; let config = EmailConfig { username: account.username.clone(), password: account.password_encrypted.clone(), server: account.imap_server.clone(), port: account.imap_port as u16, from: account.email.clone(), smtp_server: account.smtp_server.clone(), smtp_port: account.smtp_port as u16, }; let email_content = fetch_email_by_id(&config, &id) .map_err(|e| EmailError(format!("Failed to fetch email: {}", e)))?; let html = format!( r##"

{}

{}
to: {}
{}
{}
"##, id, id, id, email_content.subject, email_content.from_name, email_content.to, email_content.date, email_content.body ); Ok(axum::response::Html(html)) } pub async fn delete_email_htmx( State(state): State>, Path(id): Path, ) -> impl IntoResponse { let user_id = match extract_user_from_session(&state) { Ok(id) => id, Err(_) => { return axum::response::Html( r#"

Authentication required

Please sign in to delete emails

"# .to_string(), ); } }; let conn = state.conn.clone(); let account_result = tokio::task::spawn_blocking(move || { let db_conn_result = conn.get(); let mut db_conn = match db_conn_result { Ok(c) => c, Err(e) => return Err(format!("DB connection error: {}", e)), }; diesel::sql_query("SELECT * FROM user_email_accounts WHERE user_id = $1 LIMIT 1") .bind::(user_id) .get_result::(&mut db_conn) .optional() .map_err(|e| format!("Failed to get email account: {}", e)) }) .await; let account = match account_result { Ok(Ok(Some(acc))) => acc, Ok(Ok(None)) => { return axum::response::Html( r#"

No email account configured

Please add an email account first

"# .to_string(), ); } Ok(Err(e)) => { error!("Email account query error: {}", e); return axum::response::Html( r#"

Error deleting email

Database error occurred

"# .to_string(), ); } Err(e) => { error!("Task join error: {}", e); return axum::response::Html( r#"

Error deleting email

An internal error occurred

"# .to_string(), ); } }; let config = EmailConfig { username: account.username.clone(), password: account.password_encrypted.clone(), server: account.imap_server.clone(), port: account.imap_port as u16, from: account.email.clone(), smtp_server: account.smtp_server.clone(), smtp_port: account.smtp_port as u16, }; if let Err(e) = move_email_to_trash(&config, &id) { error!("Failed to delete email: {}", e); return axum::response::Html( r#"

Error deleting email

Failed to move email to trash

"# .to_string(), ); } info!("Email {} moved to trash", id); axum::response::Html( r#"

Email moved to trash

"# .to_string(), ) } pub async fn list_labels_htmx(State(_state): State>) -> impl IntoResponse { axum::response::Html( r#"
Important
Work
Personal
Finance
"# .to_string(), ) } pub async fn list_templates_htmx(State(_state): State>) -> impl IntoResponse { axum::response::Html( r#"

Welcome Email

Standard welcome message for new contacts

Follow Up

General follow-up template

Meeting Request

Request a meeting with scheduling options

Click a template to use it

"# .to_string(), ) } pub async fn list_signatures_htmx(State(_state): State>) -> impl IntoResponse { axum::response::Html( r#"

Default Signature

Best regards,
Your Name

Formal Signature

Sincerely,
Your Name
Title | Company

Click a signature to insert it

"# .to_string(), ) } pub async fn list_rules_htmx(State(_state): State>) -> impl IntoResponse { axum::response::Html( r#"
Auto-archive newsletters

From: *@newsletter.* → Archive

Label work emails

From: *@company.com → Label: Work

"# .to_string(), ) } pub async fn search_emails_htmx( State(state): State>, Query(params): Query>, ) -> impl IntoResponse { let query = params.get("q").map(|s| s.as_str()).unwrap_or(""); if query.is_empty() { return axum::response::Html( r#"

Enter a search term to find emails

"# .to_string(), ); } let search_term = format!("%{query_lower}%", query_lower = query.to_lowercase()); let Ok(mut conn) = state.conn.get() else { return axum::response::Html( r#"

Database connection error

"# .to_string(), ); }; let search_query = "SELECT id, subject, from_address, to_addresses, body_text, received_at FROM emails WHERE LOWER(subject) LIKE $1 OR LOWER(from_address) LIKE $1 OR LOWER(body_text) LIKE $1 ORDER BY received_at DESC LIMIT 50"; let results: Vec = match diesel::sql_query(search_query) .bind::(&search_term) .load::(&mut conn) { Ok(r) => r, Err(e) => { warn!("Email search query failed: {}", e); Vec::new() } }; if results.is_empty() { return axum::response::Html(format!( r#"

No results for "{}"

Try different keywords or check your spelling.

"#, query )); } let mut html = String::from(r#"
"#); use std::fmt::Write; let _ = write!( html, r#"
Found {} results for "{}"
"#, results.len(), query ); for row in results { let preview = row .body_text .as_deref() .unwrap_or("") .chars() .take(100) .collect::(); let formatted_date = row.received_at.format("%b %d, %Y").to_string(); let _ = write!( html, r##" "##, row.id, row.from_address, row.subject, preview, formatted_date ); } html.push_str("
"); axum::response::Html(html) } pub async fn save_auto_responder( State(_state): State>, axum::Form(form): axum::Form>, ) -> impl IntoResponse { info!("Saving auto-responder settings: {:?}", form); axum::response::Html( r#"
Auto-responder settings saved successfully!
"# .to_string(), ) }