gbserver/src/services/email.rs

326 lines
10 KiB
Rust
Raw Normal View History

2025-07-02 14:22:11 -03:00
use std::str::FromStr;
use actix_web::{error::ErrorInternalServerError, http::header::ContentType, web, HttpResponse};
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-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,
}
use crate::services::state::AppState;
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"))?;
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]))
.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-07-02 14:22:11 -03:00
// Get inbox mailbox
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))
})?
.take_ids()
2025-07-02 14:22:11 -03:00
.first()
.ok_or_else(|| actix_web::error::ErrorInternalServerError("Inbox not found"))?
.clone();
2025-07-02 14:22:11 -03:00
// Query emails in inbox
let email_ids = client
.email_query(
2025-07-02 14:22:11 -03:00
Filter::and([email::query::Filter::in_mailbox(&inbox_id)]).into(),
None::<Vec<_>>,
)
.await
2025-07-02 14:22:11 -03:00
.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 {
2025-07-02 14:22:11 -03:00
// Fetch email details
let email = client
.email_get(
&email_id,
2025-07-02 14:22:11 -03:00
[
EmailProperty::Id,
EmailProperty::Subject,
EmailProperty::From,
EmailProperty::TextBody,
EmailProperty::Preview,
]
.into(),
)
.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,
});
}
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: {}\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<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())
}
#[actix_web::get("/campaigns/{campaign_id}/click/{email}")]
pub async fn save_click(
path: web::Path<(String, String)>,
state: web::Data<AppState>,
) -> 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<String>, state: web::Data<AppState>) -> 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(",")
}