2025-07-04 10:39:31 -03:00
use crate ::services ::{ config ::EmailConfig , state ::AppState } ;
use actix_web ::error ::ErrorInternalServerError ;
use actix_web ::http ::header ::ContentType ;
use actix_web ::{ web , HttpResponse , Result } ;
use lettre ::{ transport ::smtp ::authentication ::Credentials , Message , SmtpTransport , Transport } ;
2025-07-04 23:20:48 -03:00
use serde ::Serialize ;
2025-07-04 10:39:31 -03:00
use mailparse ::{ parse_mail , MailHeaderMap } ; // Added MailHeaderMap import
use imap ::types ::{ Seq } ;
2025-07-02 14:22:11 -03:00
#[ derive(Debug, Serialize) ]
pub struct EmailResponse {
pub id : String ,
pub name : String ,
pub email : String ,
pub subject : String ,
pub text : String ,
2025-07-04 10:39:31 -03:00
date : String ,
read : bool ,
labels : Vec < String > ,
2025-07-02 14:22:11 -03:00
}
2025-06-20 22:11:45 -03:00
2025-07-04 10:39:31 -03:00
async fn internal_send_email ( config : & EmailConfig , to : & str , subject : & str , body : & str ) {
let email = Message ::builder ( )
. from ( config . from . parse ( ) . unwrap ( ) )
. to ( to . parse ( ) . unwrap ( ) )
. subject ( subject )
. body ( body . to_string ( ) )
2025-07-02 14:22:11 -03:00
. unwrap ( ) ;
2025-07-04 10:39:31 -03:00
let creds = Credentials ::new ( config . username . clone ( ) , config . password . clone ( ) ) ;
2025-07-02 14:22:11 -03:00
2025-07-04 10:39:31 -03:00
SmtpTransport ::relay ( & config . server )
. unwrap ( )
. port ( config . port )
. credentials ( creds )
. build ( )
. send ( & email )
. unwrap ( ) ;
2025-07-02 14:22:11 -03:00
}
2025-07-04 10:39:31 -03:00
#[ actix_web::get( " /emails/list " ) ]
2025-07-02 14:22:11 -03:00
pub async fn list_emails (
state : web ::Data < AppState > ,
) -> Result < web ::Json < Vec < EmailResponse > > , actix_web ::Error > {
2025-07-04 10:39:31 -03:00
let _config = state
. config
. as_ref ( )
. ok_or_else ( | | ErrorInternalServerError ( " Configuration not available " ) ) ? ;
// Establish connection
let tls = native_tls ::TlsConnector ::builder ( ) . build ( ) . map_err ( | e | {
ErrorInternalServerError ( format! ( " Failed to create TLS connector: {:?} " , e ) )
} ) ? ;
let client = imap ::connect (
( _config . email . server . as_str ( ) , 993 ) ,
_config . email . server . as_str ( ) ,
& tls ,
)
. map_err ( | e | ErrorInternalServerError ( format! ( " Failed to connect to IMAP: {:?} " , e ) ) ) ? ;
// Login
let mut session = client
. login ( & _config . email . username , & _config . email . password )
. map_err ( | e | ErrorInternalServerError ( format! ( " Login failed: {:?} " , e ) ) ) ? ;
// Select INBOX
session
. select ( " INBOX " )
. map_err ( | e | ErrorInternalServerError ( format! ( " Failed to select INBOX: {:?} " , e ) ) ) ? ;
// Search for all messages
let messages = session
. search ( " ALL " )
. map_err ( | e | ErrorInternalServerError ( format! ( " Failed to search emails: {:?} " , e ) ) ) ? ;
2025-06-20 22:11:45 -03:00
let mut email_list = Vec ::new ( ) ;
2025-07-04 10:39:31 -03:00
// Get last 20 messages
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
for seq in recent_messages {
// Fetch the entire message (headers + body)
let fetch_result = session . fetch ( seq . to_string ( ) , " RFC822 " ) ;
let messages = fetch_result
. map_err ( | e | ErrorInternalServerError ( format! ( " Failed to fetch email: {:?} " , e ) ) ) ? ;
for msg in messages . iter ( ) {
let body = msg
. body ( )
. ok_or_else ( | | ErrorInternalServerError ( " No body found " ) ) ? ;
// Parse the complete email message
let parsed = parse_mail ( body )
. map_err ( | e | ErrorInternalServerError ( format! ( " Failed to parse email: {:?} " , e ) ) ) ? ;
// 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 ( ) ;
// 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 " ) {
body_part . get_body ( ) . unwrap_or_default ( )
} else {
parsed . get_body ( ) . unwrap_or_default ( )
} ;
// Create preview
let preview = body_text . lines ( ) . take ( 3 ) . collect ::< Vec < _ > > ( ) . join ( " " ) ;
let preview_truncated = if preview . len ( ) > 150 {
format! ( " {} ... " , & preview [ .. 150 ] )
} else {
preview
} ;
// Parse From field
let ( from_name , from_email ) = parse_from_field ( & from ) ;
email_list . push ( EmailResponse {
id : seq . to_string ( ) ,
name : from_name ,
email : from_email ,
subject : if subject . is_empty ( ) {
" (No Subject) " . to_string ( )
} else {
subject
} ,
text : preview_truncated ,
date : if date . is_empty ( ) {
chrono ::Utc ::now ( ) . format ( " %Y-%m-%d %H:%M:%S " ) . to_string ( )
} else {
date
} ,
read : false ,
labels : Vec ::new ( ) ,
} ) ;
}
2025-06-20 22:11:45 -03:00
}
2025-07-04 10:39:31 -03:00
session
. logout ( )
. map_err ( | e | ErrorInternalServerError ( format! ( " Failed to logout: {:?} " , e ) ) ) ? ;
2025-06-30 10:14:25 -03:00
2025-07-04 10:39:31 -03:00
Ok ( web ::Json ( email_list ) )
2025-07-02 14:22:11 -03:00
}
2025-07-04 10:39:31 -03:00
// Helper function to parse From field
fn parse_from_field ( from : & str ) -> ( String , String ) {
if let Some ( start ) = from . find ( '<' ) {
if let Some ( end ) = from . find ( '>' ) {
let email = from [ start + 1 .. end ] . trim ( ) . to_string ( ) ;
let name = from [ .. start ] . trim ( ) . trim_matches ( '"' ) . to_string ( ) ;
return ( name , email ) ;
2025-07-02 14:22:11 -03:00
}
2025-07-04 10:39:31 -03:00
}
( " Unknown " . to_string ( ) , from . to_string ( ) )
2025-07-02 14:22:11 -03:00
}
2025-07-04 10:39:31 -03:00
// #[actix_web::post("/emails/suggest-answer/{email_id}")]
// pub async fn suggest_answer(
// path: web::Path<String>,
// state: web::Data<AppState>,
// ) -> Result<HttpResponse, actix_web::Error> {
// let email_id = path.into_inner();
// let config = state
// .config
// .as_ref()
// .ok_or_else(|| ErrorInternalServerError("Configuration not available"))?;
// // let mut session = create_imap_session(&config.email).await?;
// // session
// // .select("INBOX")
// // .await
// // .map_err(|e| ErrorInternalServerError(format!("Failed to select INBOX: {:?}", e)))?;
// // let messages = session
// // .fetch(&email_id, "RFC822.HEADER BODY[TEXT]")
// // .await
// // .map_err(|e| ErrorInternalServerError(format!("Failed to fetch email: {:?}", e)))?;
// // let msg = messages
// // .iter()
// // .next()
// // .ok_or_else(|| actix_web::error::ErrorNotFound("Email not found"))?;
// // let header = msg
// // .header()
// // .ok_or_else(|| ErrorInternalServerError("No header found"))?;
// // let body = msg
// // .text()
// // .ok_or_else(|| ErrorInternalServerError("No body found"))?;
// // let header_str = String::from_utf8_lossy(header);
// // let mut subject = String::new();
// // let mut from_info = String::new();
// // for line in header_str.lines() {
// // if line.starts_with("Subject: ") {
// // subject = line.strip_prefix("Subject: ").unwrap_or("").to_string();
// // } else if line.starts_with("From: ") {
// // from_info = line.strip_prefix("From: ").unwrap_or("").to_string();
// // }
// // }
// // let body_text = String::from_utf8_lossy(body);
// // let response = serde_json::json!({
// // "suggested_response": "Thank you for your email. I will review this and get back to you shortly.",
// // "prompt": format!(
// // "Email from: {}\nSubject: {}\n\nBody:\n{}\n\n---\n\nPlease draft a professional response to this email.",
// // from_info, subject, body_text.lines().take(20).collect::<Vec<_>>().join("\n")
// // )
// // });
// // session.logout().await.ok();
// //Ok(HttpResponse::Ok().json(response))
// }
// #[actix_web::post("/emails/archive/{email_id}")]
// pub async fn archive_email(
// path: web::Path<String>,
// state: web::Data<AppState>,
// ) -> Result<HttpResponse, actix_web::Error> {
// let email_id = path.into_inner();
// let config = state
// .config
// .as_ref()
// .ok_or_else(|| ErrorInternalServerError("Configuration not available"))?;
// let mut session = create_imap_session(&config.email).await?;
// session
// .select("INBOX")
// .await
// .map_err(|e| ErrorInternalServerError(format!("Failed to select INBOX: {:?}", e)))?;
// // Create Archive folder if it doesn't exist
// session.create("Archive").await.ok(); // Ignore error if folder exists
// // Move email to Archive folder
// session.mv(&email_id, "Archive").await.map_err(|e| {
// ErrorInternalServerError(format!("Failed to move email to archive: {:?}", e))
// })?;
// session.logout().await.ok();
// Ok(HttpResponse::Ok().json(serde_json::json!({
// "message": "Email archived successfully",
// "email_id": email_id,
// "archive_folder": "Archive"
// })))
// }
2025-07-02 14:22:11 -03:00
#[ actix_web::post( " /emails/send " ) ]
pub async fn send_email (
payload : web ::Json < ( String , String , String ) > ,
state : web ::Data < AppState > ,
) -> Result < HttpResponse , actix_web ::Error > {
let ( to , subject , body ) = payload . into_inner ( ) ;
println! ( " To: {} " , to ) ;
println! ( " Subject: {} " , subject ) ;
println! ( " Body: {} " , body ) ;
2025-07-04 10:39:31 -03:00
// Send via SMTP
internal_send_email ( & state . config . clone ( ) . unwrap ( ) . email , & to , & subject , & body ) . await ;
2025-07-02 14:22:11 -03:00
Ok ( HttpResponse ::Ok ( ) . finish ( ) )
}
2025-06-30 16:15:36 -03:00
#[ actix_web::get( " /campaigns/{campaign_id}/click/{email} " ) ]
2025-06-30 10:14:25 -03:00
pub async fn save_click (
path : web ::Path < ( String , String ) > ,
state : web ::Data < AppState > ,
2025-06-30 13:06:46 -03:00
) -> HttpResponse {
2025-06-30 10:14:25 -03:00
let ( campaign_id , email ) = path . into_inner ( ) ;
let _ = sqlx ::query ( " INSERT INTO clicks (campaign_id, email, updated_at) VALUES ($1, $2, NOW()) ON CONFLICT (campaign_id, email) DO UPDATE SET updated_at = NOW() " )
. bind ( campaign_id )
. bind ( email )
. execute ( state . db . as_ref ( ) . unwrap ( ) )
. await ;
2025-06-30 13:06:46 -03:00
2025-06-30 15:06:05 -03:00
let pixel = [
0x89 , 0x50 , 0x4E , 0x47 , 0x0D , 0x0A , 0x1A , 0x0A , // PNG header
0x00 , 0x00 , 0x00 , 0x0D , 0x49 , 0x48 , 0x44 , 0x52 , // IHDR chunk
0x00 , 0x00 , 0x00 , 0x01 , 0x00 , 0x00 , 0x00 , 0x01 , // 1x1 dimension
0x08 , 0x06 , 0x00 , 0x00 , 0x00 , 0x1F , 0x15 , 0xC4 , 0x89 , // RGBA
0x00 , 0x00 , 0x00 , 0x0A , 0x49 , 0x44 , 0x41 , 0x54 , // IDAT chunk
0x78 , 0x9C , 0x63 , 0x00 , 0x01 , 0x00 , 0x00 , 0x05 , // data
0x00 , 0x01 , 0x0D , 0x0A , 0x2D , 0xB4 , // CRC
0x00 , 0x00 , 0x00 , 0x00 , 0x49 , 0x45 , 0x4E , 0x44 , // IEND chunk
0xAE , 0x42 , 0x60 , 0x82 ,
] ; // EOF
2025-06-30 13:06:46 -03:00
2025-06-30 15:06:05 -03:00
// At the end of your save_click function:
2025-06-30 13:06:46 -03:00
HttpResponse ::Ok ( )
. content_type ( ContentType ::png ( ) )
2025-06-30 15:06:05 -03:00
. body ( pixel . to_vec ( ) ) // Using slicing to pass a reference
2025-06-30 10:14:25 -03:00
}
#[ actix_web::get( " /campaigns/{campaign_id}/emails " ) ]
2025-06-30 15:06:05 -03:00
pub async fn get_emails ( path : web ::Path < String > , state : web ::Data < AppState > ) -> String {
2025-06-30 10:14:25 -03:00
let campaign_id = path . into_inner ( ) ;
let rows = sqlx ::query_scalar ::< _ , String > ( " SELECT email FROM clicks WHERE campaign_id = $1 " )
. bind ( campaign_id )
. fetch_all ( state . db . as_ref ( ) . unwrap ( ) )
. await
. unwrap_or_default ( ) ;
rows . join ( " , " )
}