This commit is contained in:
parent
17ed0bde36
commit
10b4d85629
3 changed files with 135 additions and 104 deletions
|
@ -5,9 +5,8 @@ use actix_web::{web, HttpResponse, Result};
|
||||||
use lettre::{transport::smtp::authentication::Credentials, Message, SmtpTransport, Transport};
|
use lettre::{transport::smtp::authentication::Credentials, Message, SmtpTransport, Transport};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
use mailparse::{parse_mail, MailHeaderMap}; // Added MailHeaderMap import
|
use imap::types::Seq;
|
||||||
use imap::types::{Seq};
|
use mailparse::{parse_mail, MailHeaderMap}; // Added MailHeaderMap import
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
pub struct EmailResponse {
|
pub struct EmailResponse {
|
||||||
|
@ -79,8 +78,8 @@ pub async fn list_emails(
|
||||||
let mut email_list = Vec::new();
|
let mut email_list = Vec::new();
|
||||||
|
|
||||||
// Get last 20 messages
|
// Get last 20 messages
|
||||||
let recent_messages: Vec<_> = messages.iter().cloned().collect(); // Collect items into a Vec
|
let recent_messages: Vec<_> = messages.iter().cloned().collect(); // Collect items into a Vec
|
||||||
let recent_messages: Vec<Seq> = recent_messages.into_iter().rev().take(20).collect(); // Now you can reverse and take the last 20
|
let recent_messages: Vec<Seq> = recent_messages.into_iter().rev().take(20).collect(); // Now you can reverse and take the last 20
|
||||||
for seq in recent_messages {
|
for seq in recent_messages {
|
||||||
// Fetch the entire message (headers + body)
|
// Fetch the entire message (headers + body)
|
||||||
let fetch_result = session.fetch(seq.to_string(), "RFC822");
|
let fetch_result = session.fetch(seq.to_string(), "RFC822");
|
||||||
|
@ -103,7 +102,11 @@ pub async fn list_emails(
|
||||||
let date = headers.get_first_value("Date").unwrap_or_default();
|
let date = headers.get_first_value("Date").unwrap_or_default();
|
||||||
|
|
||||||
// Extract body text (handles both simple and multipart emails)
|
// Extract body text (handles both simple and multipart emails)
|
||||||
let body_text = if let Some(body_part) = parsed.subparts.iter().find(|p| p.ctype.mimetype == "text/plain") {
|
let body_text = if let Some(body_part) = parsed
|
||||||
|
.subparts
|
||||||
|
.iter()
|
||||||
|
.find(|p| p.ctype.mimetype == "text/plain")
|
||||||
|
{
|
||||||
body_part.get_body().unwrap_or_default()
|
body_part.get_body().unwrap_or_default()
|
||||||
} else {
|
} else {
|
||||||
parsed.get_body().unwrap_or_default()
|
parsed.get_body().unwrap_or_default()
|
||||||
|
@ -152,7 +155,7 @@ pub async fn list_emails(
|
||||||
fn parse_from_field(from: &str) -> (String, String) {
|
fn parse_from_field(from: &str) -> (String, String) {
|
||||||
if let Some(start) = from.find('<') {
|
if let Some(start) = from.find('<') {
|
||||||
if let Some(end) = from.find('>') {
|
if let Some(end) = from.find('>') {
|
||||||
let email = from[start+1..end].trim().to_string();
|
let email = from[start + 1..end].trim().to_string();
|
||||||
let name = from[..start].trim().trim_matches('"').to_string();
|
let name = from[..start].trim().trim_matches('"').to_string();
|
||||||
return (name, email);
|
return (name, email);
|
||||||
}
|
}
|
||||||
|
@ -160,7 +163,6 @@ fn parse_from_field(from: &str) -> (String, String) {
|
||||||
("Unknown".to_string(), from.to_string())
|
("Unknown".to_string(), from.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[derive(serde::Deserialize)]
|
#[derive(serde::Deserialize)]
|
||||||
pub struct SaveDraftRequest {
|
pub struct SaveDraftRequest {
|
||||||
pub to: String,
|
pub to: String,
|
||||||
|
@ -208,10 +210,9 @@ pub async fn save_draft(
|
||||||
success: false,
|
success: false,
|
||||||
message: format!("Failed to save draft: {}", e),
|
message: format!("Failed to save draft: {}", e),
|
||||||
draft_id: None,
|
draft_id: None,
|
||||||
}))
|
})),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
pub async fn save_email_draft(
|
pub async fn save_email_draft(
|
||||||
email_config: &EmailConfig,
|
email_config: &EmailConfig,
|
||||||
|
@ -226,7 +227,8 @@ pub async fn save_email_draft(
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
// Login
|
// Login
|
||||||
let mut session = client.login(&email_config.username, &email_config.password)
|
let mut session = client
|
||||||
|
.login(&email_config.username, &email_config.password)
|
||||||
.map_err(|e| format!("Login failed: {:?}", e))?;
|
.map_err(|e| format!("Login failed: {:?}", e))?;
|
||||||
|
|
||||||
// Select or create Drafts folder
|
// Select or create Drafts folder
|
||||||
|
@ -237,7 +239,9 @@ pub async fn save_email_draft(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create email message
|
// Create email message
|
||||||
let cc_header = draft_data.cc.as_deref()
|
let cc_header = draft_data
|
||||||
|
.cc
|
||||||
|
.as_deref()
|
||||||
.filter(|cc| !cc.is_empty())
|
.filter(|cc| !cc.is_empty())
|
||||||
.map(|cc| format!("Cc: {}\r\n", cc))
|
.map(|cc| format!("Cc: {}\r\n", cc))
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
@ -254,12 +258,93 @@ pub async fn save_email_draft(
|
||||||
|
|
||||||
// Append to Drafts folder
|
// Append to Drafts folder
|
||||||
session.append("Drafts", &email_message)?;
|
session.append("Drafts", &email_message)?;
|
||||||
|
|
||||||
session.logout()?;
|
session.logout()?;
|
||||||
|
|
||||||
Ok(chrono::Utc::now().timestamp().to_string())
|
Ok(chrono::Utc::now().timestamp().to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn fetch_latest_email_from_sender(
|
||||||
|
email_config: &EmailConfig,
|
||||||
|
from_email: &str,
|
||||||
|
) -> Result<String, Box<dyn std::error::Error>> {
|
||||||
|
// Establish connection
|
||||||
|
let tls = native_tls::TlsConnector::builder().build()?;
|
||||||
|
let client = imap::connect(
|
||||||
|
(email_config.server.as_str(), 993),
|
||||||
|
email_config.server.as_str(),
|
||||||
|
&tls,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// Login
|
||||||
|
let mut session = client
|
||||||
|
.login(&email_config.username, &email_config.password)
|
||||||
|
.map_err(|e| format!("Login failed: {:?}", e))?;
|
||||||
|
|
||||||
|
// Try to select Archive folder first, then fall back to INBOX
|
||||||
|
if session.select("Archive").is_err() {
|
||||||
|
session.select("INBOX")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search for emails from the specified sender
|
||||||
|
let search_query = format!("FROM \"{}\"", from_email);
|
||||||
|
let messages = session.search(&search_query)?;
|
||||||
|
|
||||||
|
if messages.is_empty() {
|
||||||
|
session.logout()?;
|
||||||
|
return Err(format!("No emails found from {}", from_email).into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the latest message (highest sequence number)
|
||||||
|
let latest_seq = messages.iter().max().unwrap();
|
||||||
|
|
||||||
|
// Fetch the entire message
|
||||||
|
let messages = session.fetch(latest_seq.to_string(), "RFC822")?;
|
||||||
|
|
||||||
|
let mut email_text = String::new();
|
||||||
|
|
||||||
|
for msg in messages.iter() {
|
||||||
|
let body = msg.body().ok_or("No body found in email")?;
|
||||||
|
|
||||||
|
// Parse the complete email message
|
||||||
|
let parsed = parse_mail(body)?;
|
||||||
|
|
||||||
|
// Extract headers
|
||||||
|
let headers = parsed.get_headers();
|
||||||
|
let subject = headers.get_first_value("Subject").unwrap_or_default();
|
||||||
|
let from = headers.get_first_value("From").unwrap_or_default();
|
||||||
|
let date = headers.get_first_value("Date").unwrap_or_default();
|
||||||
|
let to = headers.get_first_value("To").unwrap_or_default();
|
||||||
|
|
||||||
|
// Extract body text
|
||||||
|
let body_text = if let Some(body_part) = parsed
|
||||||
|
.subparts
|
||||||
|
.iter()
|
||||||
|
.find(|p| p.ctype.mimetype == "text/plain")
|
||||||
|
{
|
||||||
|
body_part.get_body().unwrap_or_default()
|
||||||
|
} else {
|
||||||
|
parsed.get_body().unwrap_or_default()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Format the email text ready for reply with headers
|
||||||
|
email_text = format!(
|
||||||
|
"--- Original Message ---\nFrom: {}\nTo: {}\nDate: {}\nSubject: {}\n\n{}\n\n--- Reply Above This Line ---\n\n",
|
||||||
|
from, to, date, subject, body_text
|
||||||
|
);
|
||||||
|
|
||||||
|
break; // We only want the first (and should be only) message
|
||||||
|
}
|
||||||
|
|
||||||
|
session.logout()?;
|
||||||
|
|
||||||
|
if email_text.is_empty() {
|
||||||
|
Err("Failed to extract email content".into())
|
||||||
|
} else {
|
||||||
|
Ok(email_text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[actix_web::post("/emails/get_latest_from")]
|
#[actix_web::post("/emails/get_latest_from")]
|
||||||
pub async fn get_latest_email_from(
|
pub async fn get_latest_email_from(
|
||||||
state: web::Data<AppState>,
|
state: web::Data<AppState>,
|
||||||
|
@ -303,7 +388,8 @@ pub async fn fetch_latest_sent_to(
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
// Login
|
// Login
|
||||||
let mut session = client.login(&email_config.username, &email_config.password)
|
let mut session = client
|
||||||
|
.login(&email_config.username, &email_config.password)
|
||||||
.map_err(|e| format!("Login failed: {:?}", e))?;
|
.map_err(|e| format!("Login failed: {:?}", e))?;
|
||||||
|
|
||||||
// Try to select Archive folder first, then fall back to INBOX
|
// Try to select Archive folder first, then fall back to INBOX
|
||||||
|
@ -341,24 +427,45 @@ pub async fn fetch_latest_sent_to(
|
||||||
let date = headers.get_first_value("Date").unwrap_or_default();
|
let date = headers.get_first_value("Date").unwrap_or_default();
|
||||||
let to = headers.get_first_value("To").unwrap_or_default();
|
let to = headers.get_first_value("To").unwrap_or_default();
|
||||||
|
|
||||||
// Extract body text
|
if !to
|
||||||
let body_text = if let Some(body_part) = parsed.subparts.iter().find(|p| p.ctype.mimetype == "text/plain") {
|
.trim()
|
||||||
|
.to_lowercase()
|
||||||
|
.contains(&to_email.trim().to_lowercase())
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Extract body text (handles both simple and multipart emails) - SAME AS LIST_EMAILS
|
||||||
|
let body_text = if let Some(body_part) = parsed
|
||||||
|
.subparts
|
||||||
|
.iter()
|
||||||
|
.find(|p| p.ctype.mimetype == "text/plain")
|
||||||
|
{
|
||||||
body_part.get_body().unwrap_or_default()
|
body_part.get_body().unwrap_or_default()
|
||||||
} else {
|
} else {
|
||||||
parsed.get_body().unwrap_or_default()
|
parsed.get_body().unwrap_or_default()
|
||||||
};
|
};
|
||||||
|
|
||||||
// Format the email text ready for reply with headers
|
// Only format if we have actual content
|
||||||
email_text = format!(
|
if !body_text.trim().is_empty() && body_text != "No readable content found" {
|
||||||
"--- Original Message ---\nFrom: {}\nTo: {}\nDate: {}\nSubject: {}\n\n{}\n\n--- Reply Above This Line ---\n\n",
|
// Format the email text ready for reply with headers
|
||||||
from, to, date, subject, body_text
|
email_text = format!(
|
||||||
);
|
"--- Original Message ---\nFrom: {}\nTo: {}\nDate: {}\nSubject: {}\n\n{}\n\n--- Reply Above This Line ---\n\n",
|
||||||
|
from, to, date, subject, body_text.trim()
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Still provide headers even if body is empty
|
||||||
|
email_text = format!(
|
||||||
|
"--- Original Message ---\nFrom: {}\nTo: {}\nDate: {}\nSubject: {}\n\n[No readable content]\n\n--- Reply Above This Line ---\n\n",
|
||||||
|
from, to, date, subject
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
break; // We only want the first (and should be only) message
|
break; // We only want the first (and should be only) message
|
||||||
}
|
}
|
||||||
|
|
||||||
session.logout()?;
|
session.logout()?;
|
||||||
|
|
||||||
|
// Always return something, even if it's just headers
|
||||||
if email_text.is_empty() {
|
if email_text.is_empty() {
|
||||||
Err("Failed to extract email content".into())
|
Err("Failed to extract email content".into())
|
||||||
} else {
|
} else {
|
||||||
|
@ -366,84 +473,6 @@ pub async fn fetch_latest_sent_to(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn fetch_latest_email_from_sender(
|
|
||||||
email_config: &EmailConfig,
|
|
||||||
from_email: &str,
|
|
||||||
) -> Result<String, Box<dyn std::error::Error>> {
|
|
||||||
// Establish connection
|
|
||||||
let tls = native_tls::TlsConnector::builder().build()?;
|
|
||||||
let client = imap::connect(
|
|
||||||
(email_config.server.as_str(), 993),
|
|
||||||
email_config.server.as_str(),
|
|
||||||
&tls,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
// Login
|
|
||||||
let mut session = client.login(&email_config.username, &email_config.password)
|
|
||||||
.map_err(|e| format!("Login failed: {:?}", e))?;
|
|
||||||
|
|
||||||
// Try to select Archive folder first, then fall back to INBOX
|
|
||||||
if session.select("Archive").is_err() {
|
|
||||||
session.select("INBOX")?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Search for emails from the specified sender
|
|
||||||
let search_query = format!("FROM \"{}\"", from_email);
|
|
||||||
let messages = session.search(&search_query)?;
|
|
||||||
|
|
||||||
if messages.is_empty() {
|
|
||||||
session.logout()?;
|
|
||||||
return Err(format!("No emails found from {}", from_email).into());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the latest message (highest sequence number)
|
|
||||||
let latest_seq = messages.iter().max().unwrap();
|
|
||||||
|
|
||||||
// Fetch the entire message
|
|
||||||
let messages = session.fetch(latest_seq.to_string(), "RFC822")?;
|
|
||||||
|
|
||||||
let mut email_text = String::new();
|
|
||||||
|
|
||||||
for msg in messages.iter() {
|
|
||||||
let body = msg.body().ok_or("No body found in email")?;
|
|
||||||
|
|
||||||
// Parse the complete email message
|
|
||||||
let parsed = parse_mail(body)?;
|
|
||||||
|
|
||||||
// Extract headers
|
|
||||||
let headers = parsed.get_headers();
|
|
||||||
let subject = headers.get_first_value("Subject").unwrap_or_default();
|
|
||||||
let from = headers.get_first_value("From").unwrap_or_default();
|
|
||||||
let date = headers.get_first_value("Date").unwrap_or_default();
|
|
||||||
let to = headers.get_first_value("To").unwrap_or_default();
|
|
||||||
|
|
||||||
// Extract body text
|
|
||||||
let body_text = if let Some(body_part) = parsed.subparts.iter().find(|p| p.ctype.mimetype == "text/plain") {
|
|
||||||
body_part.get_body().unwrap_or_default()
|
|
||||||
} else {
|
|
||||||
parsed.get_body().unwrap_or_default()
|
|
||||||
};
|
|
||||||
|
|
||||||
// Format the email text ready for reply with headers
|
|
||||||
email_text = format!(
|
|
||||||
"--- Original Message ---\nFrom: {}\nTo: {}\nDate: {}\nSubject: {}\n\n{}\n\n--- Reply Above This Line ---\n\n",
|
|
||||||
from, to, date, subject, body_text
|
|
||||||
);
|
|
||||||
|
|
||||||
break; // We only want the first (and should be only) message
|
|
||||||
}
|
|
||||||
|
|
||||||
session.logout()?;
|
|
||||||
|
|
||||||
if email_text.is_empty() {
|
|
||||||
Err("Failed to extract email content".into())
|
|
||||||
} else {
|
|
||||||
Ok(email_text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#[actix_web::post("/emails/send")]
|
#[actix_web::post("/emails/send")]
|
||||||
pub async fn send_email(
|
pub async fn send_email(
|
||||||
payload: web::Json<(String, String, String)>,
|
payload: web::Json<(String, String, String)>,
|
||||||
|
|
|
@ -38,9 +38,11 @@ async fn execute_create_draft(
|
||||||
let get_result = fetch_latest_sent_to(&state.config.clone().unwrap().email, to).await;
|
let get_result = fetch_latest_sent_to(&state.config.clone().unwrap().email, to).await;
|
||||||
let email_body = if let Ok(get_result_str) = get_result {
|
let email_body = if let Ok(get_result_str) = get_result {
|
||||||
if !get_result_str.is_empty() {
|
if !get_result_str.is_empty() {
|
||||||
reply_text.to_string() + get_result_str.as_str()
|
let email_separator = "\n\n-------------------------------------------------\n\n"; // Horizontal rule style separator
|
||||||
|
reply_text.to_string() + email_separator + get_result_str.as_str()
|
||||||
} else {
|
} else {
|
||||||
"".to_string()
|
// Fixed: Use reply_text when get_result_str is empty, not empty string
|
||||||
|
reply_text.to_string()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
reply_text.to_string()
|
reply_text.to_string()
|
||||||
|
@ -59,4 +61,4 @@ async fn execute_create_draft(
|
||||||
Err(e) => Err(e.to_string()),
|
Err(e) => Err(e.to_string()),
|
||||||
};
|
};
|
||||||
save_result
|
save_result
|
||||||
}
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
use rhai::Dynamic;
|
use rhai::Dynamic;
|
||||||
use rhai::Engine;
|
use rhai::Engine;
|
||||||
use serde_json::{json, Value};
|
use serde_json::{json, Value};
|
||||||
use sqlx::{PgPool, Row};
|
use sqlx::{PgPool};
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
|
|
||||||
use crate::services::state::AppState;
|
use crate::services::state::AppState;
|
||||||
|
|
Loading…
Add table
Reference in a new issue