//! Email preview: render an email to a self-contained HTML file, open it in //! the system browser, and reap stale preview temp files. Split out of the //! email command module. use tracing::instrument; use super::*; /// Opens an email in the system's default web browser. #[tauri::command] #[instrument(skip_all)] pub async fn open_email_in_browser(state: State<'_, Arc>, id: EmailId) -> Result<(), ApiError> { let email = state.emails.get_by_id(id, DESKTOP_USER_ID).await? .or_not_found("email", id)?; let html_content = if let Some(ref html) = email.html_body { let sanitized_body = docengine::sanitize_html(html); format!( r#" {}

{}

{}

{}

{}

{}
"#, html_escape(&email.subject), html_escape(&email.from), html_escape(&email.to), html_escape(&email.subject), email.received_at.format("%Y-%m-%d %H:%M:%S UTC"), sanitized_body ) } else { let body_html = html_escape(&email.body).replace('\n', "
\n"); format!( r#" {}

{}

{}

{}

{}

{}
"#, html_escape(&email.subject), html_escape(&email.from), html_escape(&email.to), html_escape(&email.subject), email.received_at.format("%Y-%m-%d %H:%M:%S UTC"), body_html ) }; let temp_dir = std::env::temp_dir(); let file_name = format!("goingson_email_{}_{}.html", id, uuid::Uuid::new_v4().simple()); let file_path = temp_dir.join(file_name); // Written owner-only (0600): the email body/subject/sender must not be // readable by other local users via the world-readable system temp dir. let write_path = file_path.clone(); tokio::task::spawn_blocking(move || crate::commands::write_private_temp(&write_path, html_content.as_bytes())) .await .map_api_err("Task join error", ApiError::internal)? .map_api_err("Failed to write temp file", ApiError::internal)?; let path = file_path.clone(); tokio::task::spawn_blocking(move || open::that(&path)).await .map_api_err("Task join error", ApiError::internal)? .map_api_err("Failed to open browser", ApiError::internal)?; // Clean up temp file after a delay to give the browser time to load it tokio::spawn(async move { tokio::time::sleep(std::time::Duration::from_secs(30)).await; let _ = tokio::fs::remove_file(&file_path).await; }); Ok(()) } /// Remove stale `goingson_email_*.html` temp files from previous sessions. pub async fn cleanup_stale_temp_files() { let temp_dir = std::env::temp_dir(); let mut entries = match tokio::fs::read_dir(&temp_dir).await { Ok(e) => e, Err(_) => return, }; while let Ok(Some(entry)) = entries.next_entry().await { if let Some(name) = entry.file_name().to_str() && name.starts_with("goingson_email_") && name.ends_with(".html") { let _ = tokio::fs::remove_file(entry.path()).await; } } } /// Escape HTML special characters to prevent XSS when injecting user /// content (email bodies, subjects) into the browser preview template. fn html_escape(s: &str) -> String { s.replace('&', "&") .replace('<', "<") .replace('>', ">") .replace('"', """) .replace('\'', "'") }