2025-07-02 14:22:11 -03:00
use std ::str ::FromStr ;
use actix_web ::{ error ::ErrorInternalServerError , http ::header ::ContentType , web , HttpResponse } ;
2025-06-20 22:11:45 -03:00
use jmap_client ::{
2025-07-02 14:22:11 -03:00
client ::Client , core ::query ::Filter , email ,
identity ::Property , mailbox ::{ self , Role } ,
email ::Property as EmailProperty
2025-06-20 22:11:45 -03:00
} ;
2025-07-02 14:22:11 -03:00
use serde ::Serialize ;
#[ derive(Debug, Serialize) ]
pub struct EmailResponse {
pub id : String ,
pub name : String ,
pub email : String ,
pub subject : String ,
pub text : String ,
}
2025-06-20 22:11:45 -03:00
2025-06-30 10:14:25 -03:00
use crate ::services ::state ::AppState ;
2025-06-20 22:11:45 -03:00
2025-07-02 14:22:11 -03:00
async fn create_jmap_client (
state : & web ::Data < AppState > ,
) -> Result < ( Client , String , String , String ) , actix_web ::Error > {
let config = state
. config
. as_ref ( )
. ok_or_else ( | | actix_web ::error ::ErrorInternalServerError ( " Configuration not available " ) ) ? ;
2025-06-20 22:11:45 -03:00
let client = Client ::new ( )
2025-07-02 14:22:11 -03:00
. credentials ( (
config . email . username . as_ref ( ) ,
config . email . password . as_ref ( ) ,
) )
. connect ( & config . email . server )
. await
. map_err ( | e | {
actix_web ::error ::ErrorInternalServerError ( format! ( " JMAP connection error: {} " , e ) )
} ) ? ;
// 2. Get account ID and email
let session = client . session ( ) ;
let ( account_id , email ) = session
. accounts ( )
. find_map ( | account_id | {
let account = session . account ( account_id ) . unwrap ( ) ;
Some ( ( account_id . to_string ( ) , account . name ( ) . to_string ( ) ) )
} )
. unwrap ( ) ;
let identity = client
. identity_get ( " default " , Some ( vec! [ Property ::Id , Property ::Email ] ) )
2025-06-20 22:11:45 -03:00
. await
2025-07-02 14:22:11 -03:00
. map_err ( | e | {
actix_web ::error ::ErrorInternalServerError ( format! ( " JMAP connection error: {} " , e ) )
} ) ? . unwrap ( ) ;
let identity_id = identity . id ( ) . unwrap ( ) ;
println! ( " Account ID: {} " , account_id ) ;
println! ( " Email address: {} " , email ) ;
println! ( " IdentityID: {} " , identity_id ) ;
Ok ( ( client , account_id , email , String ::from_str ( identity_id ) ? ) )
}
#[ actix_web::post( " /emails/list " ) ]
pub async fn list_emails (
state : web ::Data < AppState > ,
) -> Result < web ::Json < Vec < EmailResponse > > , actix_web ::Error > {
let ( client , account_id , email , identity_id ) = create_jmap_client ( & state ) . await ? ;
2025-06-20 22:11:45 -03:00
2025-07-02 14:22:11 -03:00
// Get inbox mailbox
2025-06-20 22:11:45 -03:00
let inbox_id = client
. mailbox_query (
mailbox ::query ::Filter ::role ( Role ::Inbox ) . into ( ) ,
None ::< Vec < _ > > ,
)
. await
2025-07-02 14:22:11 -03:00
. map_err ( | e | {
actix_web ::error ::ErrorInternalServerError ( format! ( " Failed to query inbox: {} " , e ) )
} ) ?
2025-06-20 22:11:45 -03:00
. take_ids ( )
2025-07-02 14:22:11 -03:00
. first ( )
. ok_or_else ( | | actix_web ::error ::ErrorInternalServerError ( " Inbox not found " ) ) ?
. clone ( ) ;
2025-06-20 22:11:45 -03:00
2025-07-02 14:22:11 -03:00
// Query emails in inbox
let email_ids = client
2025-06-20 22:11:45 -03:00
. email_query (
2025-07-02 14:22:11 -03:00
Filter ::and ( [ email ::query ::Filter ::in_mailbox ( & inbox_id ) ] ) . into ( ) ,
None ::< Vec < _ > > ,
2025-06-20 22:11:45 -03:00
)
2025-06-30 10:14:25 -03:00
. await
2025-07-02 14:22:11 -03:00
. map_err ( | e | {
actix_web ::error ::ErrorInternalServerError ( format! ( " Failed to query emails: {} " , e ) )
} ) ?
. take_ids ( ) ;
2025-06-20 22:11:45 -03:00
let mut email_list = Vec ::new ( ) ;
for email_id in email_ids {
2025-07-02 14:22:11 -03:00
// Fetch email details
let email = client
2025-06-20 22:11:45 -03:00
. email_get (
& email_id ,
2025-07-02 14:22:11 -03:00
[
EmailProperty ::Id ,
EmailProperty ::Subject ,
EmailProperty ::From ,
EmailProperty ::TextBody ,
EmailProperty ::Preview ,
]
. into ( ) ,
2025-06-20 22:11:45 -03:00
)
2025-06-30 10:14:25 -03:00
. await
2025-07-02 14:22:11 -03:00
. map_err ( | e | {
actix_web ::error ::ErrorInternalServerError ( format! ( " Failed to get emails: {} " , e ) )
} ) ?
. unwrap ( ) ;
let from = email . from ( ) . unwrap ( ) . first ( ) ;
let ( name , email_addr ) = if let Some ( addr ) = from {
(
addr . name ( ) . unwrap_or ( " Unknown " ) . to_string ( ) ,
addr . email ( ) . to_string ( ) ,
)
} else {
( " Unknown " . to_string ( ) , " unknown@example.com " . to_string ( ) )
} ;
let text = email . preview ( ) . unwrap_or_default ( ) . to_string ( ) ;
email_list . push ( EmailResponse {
id : email . id ( ) . unwrap ( ) . to_string ( ) ,
name ,
email : email_addr ,
subject : email . subject ( ) . unwrap_or_default ( ) . to_string ( ) ,
text ,
} ) ;
2025-06-20 22:11:45 -03:00
}
2025-06-30 10:14:25 -03:00
Ok ( web ::Json ( email_list ) )
}
2025-07-02 14:22:11 -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 ( client , account_id , email , identity_id ) = create_jmap_client ( & state ) . await ? ;
// Fetch the specific email
let email = client
. email_get (
& email_id ,
[
EmailProperty ::Id ,
EmailProperty ::Subject ,
EmailProperty ::From ,
EmailProperty ::TextBody ,
EmailProperty ::Preview ,
]
. into ( ) ,
)
. await
. map_err ( | e | {
actix_web ::error ::ErrorInternalServerError ( format! ( " Failed to get email: {} " , e ) )
} ) ?
. into_iter ( )
. next ( )
. ok_or_else ( | | actix_web ::error ::ErrorNotFound ( " Email not found " ) ) ? ;
let from = email . from ( ) . unwrap ( ) . first ( ) ;
let sender_info = if let Some ( addr ) = from {
format! ( " {} < {} > " , addr . name ( ) . unwrap_or ( " Unknown " ) , addr . email ( ) )
} else {
" Unknown sender " . to_string ( )
} ;
let subject = email . subject ( ) . unwrap_or_default ( ) ;
let body_text = email . preview ( ) . unwrap_or_default ( ) ;
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: {} \n Subject: {} \n \n Body: \n {} \n \n --- \n \n Please draft a professional response to this email. " ,
sender_info , subject , body_text
)
} ) ;
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 ( client , account_id , email , identity_id ) = create_jmap_client ( & state ) . await ? ;
// Get Archive mailbox (or create if it doesn't exist)
let archive_id = match client
. mailbox_query (
mailbox ::query ::Filter ::name ( " Archive " ) . into ( ) ,
None ::< Vec < _ > > ,
)
. await
{
Ok ( mut result ) = > {
let ids = result . take_ids ( ) ;
if let Some ( id ) = ids . first ( ) {
id . clone ( )
} else {
// Create Archive mailbox if it doesn't exist
client
. mailbox_create ( " Archive " , None ::< String > , Role ::Archive )
. await
. map_err ( | e | {
actix_web ::error ::ErrorInternalServerError ( format! (
" Failed to create archive mailbox: {} " ,
e
) )
} ) ?
. take_id ( )
}
}
Err ( e ) = > {
return Err ( actix_web ::error ::ErrorInternalServerError ( format! (
" Failed to query mailboxes: {} " ,
e
) ) ) ;
}
} ;
// Move email to Archive mailbox
client
. email_set_mailboxes ( & email_id , [ & archive_id ] )
. await
. map_err ( | e | {
actix_web ::error ::ErrorInternalServerError ( format! ( " Failed to archive email: {} " , e ) )
} ) ? ;
Ok ( HttpResponse ::Ok ( ) . json ( serde_json ::json! ( {
" message " : " Email archived successfully " ,
" email_id " : email_id ,
" archive_mailbox_id " : archive_id
} ) ) )
}
#[ 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 > {
// Destructure the tuple into individual components
let ( to , subject , body ) = payload . into_inner ( ) ;
println! ( " To: {} " , to ) ;
println! ( " Subject: {} " , subject ) ;
println! ( " Body: {} " , body ) ;
let ( client , account_id , email , identity_id ) = create_jmap_client ( & state ) . await ? ;
let email_submission = client
. email_submission_create ( " 111 " , account_id )
. await
. map_err ( | e | {
actix_web ::error ::ErrorInternalServerError ( format! ( " Failed to create email: {} " , e ) )
} ) ? ;
let email_id = email_submission . email_id ( ) . unwrap ( ) ;
println! ( " Email-ID: {} " , email_id ) ;
client
. email_submission_create ( email_id , identity_id )
. await
. map_err ( | e | {
actix_web ::error ::ErrorInternalServerError ( format! ( " Failed to send email: {} " , e ) )
} ) ? ;
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 ( " , " )
}