use axum::extract::{Path, Query}; use axum::http::StatusCode; use axum::routing::get; use axum::{Extension, Json, Router}; use serde::Serialize; use serde_json::{Value, json}; use sqlx::PgPool; use crate::config::MailPollerConfig; use crate::model::{MailListQuery, MailMessage}; use crate::poller::mark_remote_message_seen; use crate::repository::MailRepository; type ApiError = (StatusCode, Json); #[derive(Clone)] pub struct MailRouteState { pub db: PgPool, } #[derive(Debug, Clone, Serialize)] struct MailRenderingSettings { trusted_sender: bool, remote_resources_allowed: bool, } impl MailRouteState { pub fn new(db: PgPool) -> Self { Self { db } } } pub fn routes(db: PgPool) -> Router where S: Clone + Send + Sync + 'static, { Router::new() .route("/v1/mail/messages", get(list_messages)) .route("/v1/mail/search", get(list_messages)) .route("/v1/mail/messages/{id}", get(get_message)) .layer(Extension(MailRouteState::new(db))) } // ── JSON API ───────────────────────────────────────────── async fn list_messages( Extension(state): Extension, Query(query): Query, ) -> Result, ApiError> { let (messages, total, page, per_page) = MailRepository::query_messages(&state.db, &query) .await .map_err(api_internal_error)?; Ok(Json(json!({ "success": true, "page": page, "per_page": per_page, "total": total, "messages": messages, }))) } async fn get_message( Extension(state): Extension, Path(id): Path, ) -> Result, ApiError> { let mut message = MailRepository::find(&state.db, id) .await .map_err(api_internal_error)? .ok_or_else(|| api_not_found("邮件记录不存在"))?; if !message.seen { match mark_message_seen(&state.db, &message).await { Ok(true) => message.seen = true, Ok(false) => {} Err(error) => eprintln!("同步邮件已读失败 id={id}: {error:#}"), } } let rendering = rendering_settings_for_message(&state.db, &message).await; Ok(Json(json!({ "success": true, "message": message, "rendering": rendering, }))) } // ── 辅助函数 ───────────────────────────────────────────── async fn mark_message_seen(pool: &PgPool, message: &MailMessage) -> anyhow::Result { let mut config = mail_config_for_message(pool, message) .await? .ok_or_else(|| anyhow::anyhow!("没有可用于同步已读状态的邮件账户配置"))?; config.mailbox = message.mailbox.clone(); mark_remote_message_seen(&config, &message.uid).await?; MailRepository::mark_as_seen(pool, message.id).await } async fn mail_config_for_message( pool: &PgPool, message: &MailMessage, ) -> anyhow::Result> { if let Some(account_id) = message.mail_account_id { let account = MailRepository::find_account(pool, account_id) .await? .ok_or_else(|| anyhow::anyhow!("邮件账户不存在: {account_id}"))?; return Ok(Some(MailPollerConfig::from_account(account))); } let Some(config) = MailPollerConfig::from_env()? else { return Ok(None); }; if config.source_key == message.source_key { return Ok(Some(config)); } Ok(None) } async fn rendering_settings_for_message( pool: &PgPool, message: &MailMessage, ) -> MailRenderingSettings { let config = match mail_config_for_message(pool, message).await { Ok(config) => config, Err(error) => { eprintln!("读取邮件渲染配置失败 id={}: {error:#}", message.id); None } }; let trusted_sender = config .as_ref() .map(|config| sender_matches_trusted_list(&message.from_label, &config.trusted_senders)) .unwrap_or(false); MailRenderingSettings { trusted_sender, remote_resources_allowed: trusted_sender, } } fn sender_matches_trusted_list(from_label: &str, trusted_senders: &[String]) -> bool { if trusted_senders.is_empty() { return false; } let candidates = sender_candidates(from_label); trusted_senders.iter().any(|trusted| { let trusted_candidates = sender_candidates(trusted); !trusted_candidates.is_empty() && trusted_candidates .iter() .any(|trusted| candidates.iter().any(|candidate| candidate == trusted)) }) } fn sender_candidates(from_label: &str) -> Vec { let mut values = Vec::new(); if let Some(start) = from_label.find('<') { if let Some(end) = from_label[start + 1..].find('>') { push_sender_candidate(&mut values, &from_label[start + 1..start + 1 + end]); } } else { // 没有尖括号时,整个 label 可能就是纯邮箱地址 push_sender_candidate(&mut values, from_label); } values } fn push_sender_candidate(values: &mut Vec, value: &str) { let value = value .trim() .trim_matches(['<', '>', '"', '\'', '(', ')', '[', ']']) .to_ascii_lowercase(); if value.contains('@') && !values.iter().any(|existing| existing == &value) { values.push(value); } } fn api_not_found(message: impl Into) -> ApiError { ( StatusCode::NOT_FOUND, Json(json!({ "success": false, "message": message.into(), })), ) } fn api_internal_error(error: anyhow::Error) -> ApiError { eprintln!("mail 接口失败: {error}"); ( StatusCode::INTERNAL_SERVER_ERROR, Json(json!({ "success": false, "message": "mail 接口失败", })), ) } #[cfg(test)] mod tests { use super::sender_matches_trusted_list; #[test] fn matches_trusted_sender_email_from_display_label() { assert!(sender_matches_trusted_list( "Facebook ", &["reminders@facebookmail.com".to_string()] )); } #[test] fn trusted_sender_matching_is_case_insensitive_and_exact() { assert!(sender_matches_trusted_list( "Alice ", &["alice@example.com".to_string()] )); assert!(sender_matches_trusted_list( "Alice ", &["Trusted ".to_string()] )); assert!(!sender_matches_trusted_list( "mallory@example.net", &["alice@example.com".to_string()] )); } }