use std::str::FromStr; use actix_web::{error::ErrorInternalServerError, http::header::ContentType, web, HttpResponse}; use jmap_client::{ client::Client, core::query::Filter, email, identity::Property, mailbox::{self, Role}, email::Property as EmailProperty }; use serde::Serialize; #[derive(Debug, Serialize)] pub struct EmailResponse { pub id: String, pub name: String, pub email: String, pub subject: String, pub text: String, } use crate::services::state::AppState; async fn create_jmap_client( state: &web::Data, ) -> Result<(Client, String, String, String), actix_web::Error> { let config = state .config .as_ref() .ok_or_else(|| actix_web::error::ErrorInternalServerError("Configuration not available"))?; let client = Client::new() .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])) .await .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, ) -> Result>, actix_web::Error> { let (client, account_id, email, identity_id) = create_jmap_client(&state).await?; // Get inbox mailbox let inbox_id = client .mailbox_query( mailbox::query::Filter::role(Role::Inbox).into(), None::>, ) .await .map_err(|e| { actix_web::error::ErrorInternalServerError(format!("Failed to query inbox: {}", e)) })? .take_ids() .first() .ok_or_else(|| actix_web::error::ErrorInternalServerError("Inbox not found"))? .clone(); // Query emails in inbox let email_ids = client .email_query( Filter::and([email::query::Filter::in_mailbox(&inbox_id)]).into(), None::>, ) .await .map_err(|e| { actix_web::error::ErrorInternalServerError(format!("Failed to query emails: {}", e)) })? .take_ids(); let mut email_list = Vec::new(); for email_id in email_ids { // Fetch email details 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 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, }); } Ok(web::Json(email_list)) } #[actix_web::post("/emails/suggest-answer/{email_id}")] pub async fn suggest_answer( path: web::Path, state: web::Data, ) -> Result { 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: {}\nSubject: {}\n\nBody:\n{}\n\n---\n\nPlease 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, state: web::Data, ) -> Result { 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::>, ) .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::, 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, ) -> Result { // 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()) } #[actix_web::get("/campaigns/{campaign_id}/click/{email}")] pub async fn save_click( path: web::Path<(String, String)>, state: web::Data, ) -> HttpResponse { 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; 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 // At the end of your save_click function: HttpResponse::Ok() .content_type(ContentType::png()) .body(pixel.to_vec()) // Using slicing to pass a reference } #[actix_web::get("/campaigns/{campaign_id}/emails")] pub async fn get_emails(path: web::Path, state: web::Data) -> String { 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(",") }