Files
iAs/ias-mail/src/routes.rs
T
wunianxiao af6cfdaa83 feat: 拆分 worker 架构并增强 assistant 安全保护
- 新增 daemon/channel/assistant/function/scheduler/logging worker 架构和 JetStream 消息协议

- 修复邮件通知误入 assistant、迁移 checksum、日志 subject 和工具调用无限循环风险

- 新增 assistant 工具轮次熔断、工具结果截断和提示词软收敛规则

- 补充邮件推送关键字、可信发件人配置、升级日志和验证测试
2026-07-10 00:41:36 +08:00

229 lines
6.7 KiB
Rust

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<Value>);
#[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<S>(db: PgPool) -> Router<S>
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<MailRouteState>,
Query(query): Query<MailListQuery>,
) -> Result<Json<Value>, 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<MailRouteState>,
Path(id): Path<i64>,
) -> Result<Json<Value>, 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<bool> {
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<Option<MailPollerConfig>> {
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<String> {
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<String>, 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<String>) -> 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>",
&["reminders@facebookmail.com".to_string()]
));
}
#[test]
fn trusted_sender_matching_is_case_insensitive_and_exact() {
assert!(sender_matches_trusted_list(
"Alice <ALICE@example.com>",
&["alice@example.com".to_string()]
));
assert!(sender_matches_trusted_list(
"Alice <alice@example.com>",
&["Trusted <alice@example.com>".to_string()]
));
assert!(!sender_matches_trusted_list(
"mallory@example.net",
&["alice@example.com".to_string()]
));
}
}