diff --git a/CHANGELOG.md b/CHANGELOG.md index b6aa1a0..3025ffa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,95 @@ # 更新日志 +## 0.2.8 - 2026-07-07 + +- 新增邮件已读/未读状态持久化,保存 IMAP `\Seen` 标记到 `ias_mail_messages.seen`。 +- 邮件列表页面新增状态筛选,支持查看全部、未读或已读邮件。 +- 邮件列表卡片和详情页显示已读/未读状态,未读邮件在列表中使用更醒目的样式。 +- 邮件 API `GET /v1/mail/messages` 支持 `seen=true/false` 过滤,并在返回的邮件对象中包含 `seen` 字段。 +- 数据库新增 `ias_mail_messages.seen` 字段;已有历史邮件默认标记为未读,后续拉取最近邮件时会刷新为 IMAP 当前状态。 + +## 0.2.7 - 2026-07-07 + +- 修复 multipart/alternative 邮件详情页只展示纯文本 fallback 的问题;现在详情页优先保存和渲染 HTML 邮件体。 +- 邮件列表摘要、搜索预览和微信通知继续使用纯文本正文;没有纯文本正文时会从 HTML 中提取简要文本,避免标签污染摘要。 +- 邮件轮询或手动拉取遇到已缓存的重复邮件时,会刷新缓存正文和摘要,使最近抓取范围内的旧纯文本缓存可自动更新为 HTML 正文。 +- 邮件详情 iframe 继续禁止脚本和表单,但允许加载 `http/https` 图片,使 Facebook 等 HTML 邮件更接近邮箱客户端渲染效果。 +- 无数据库结构变化;不在最近抓取范围内的旧邮件仍需要扩大抓取范围或重新入库后才能显示 HTML 版本。 + +## 0.2.6 - 2026-07-07 + +- 新增工具列表 HTTP 页面 `/tools`,展示内置工具和 `tools/*.yaml` 工具定义。 +- 新增工具列表 API `GET /v1/tools`,返回内置工具、YAML 工具、参数、环境变量名和工具目录加载告警。 +- 新增用户配置列表 HTTP 页面 `/configs`,展示微信渠道账号和邮件账户配置。 +- 新增用户配置列表 API `GET /v1/user-configs`,返回脱敏后的微信与邮件配置,不暴露微信 token、更新游标和邮件密码明文。 +- 顶部导航新增“工具”和“配置”入口;无数据库结构变化,新增 `0.2.6` 升级日志记录。 + +## 0.2.5 - 2026-07-07 + +- 修复新增 SQL 迁移文件后后台服务可能继续使用旧二进制迁移列表的问题。 +- `ias-service` 新增 `build.rs`,显式监听 `migrations` 目录和 SQL 文件变化,确保 `sqlx::migrate!` 重新嵌入最新迁移。 +- 无数据库结构变化;新增 `0.2.5` 升级日志记录。 + +## 0.2.4 - 2026-07-07 + +- 新增 YAML 配置的邮件查询 HTTP 工具 `query_mail_messages`,通过 `/v1/mail/messages` 查询或搜索邮件列表。 +- 工具支持关键词、邮箱目录、来源、页码和每页数量参数,返回分页信息和当前页邮件 JSON 列表。 +- 无数据库结构变化;新增 `0.2.4` 升级日志记录。 + +## 0.2.3 - 2026-07-07 + +- 修复邮件自动通知漏发:后台轮询现在会对所有新入库邮件发送通知,不再因为 IMAP `\Seen` 已读标记跳过通知。 +- 邮件轮询现在尊重账户配置的 `fetch_limit`,不再固定只检查最近 20 封邮件。 +- 邮件轮询启动日志新增 `fetch_limit`,便于确认实际加载的抓取范围。 +- 新增单元测试覆盖已读新邮件仍会被自动轮询处理,以及不同抓取上限下的 IMAP 序列范围计算。 + +## 0.2.2 - 2026-07-06 + +- 修复邮件详情页预览链接的 HTML 属性转义,避免异常 URL 破坏页面结构或注入属性。 +- 修复长中文或 Emoji 邮件内容、Message-ID 截断时可能因 UTF-8 非字符边界切片导致的页面错误。 +- 收紧 HTML 邮件 iframe 沙箱:移除同源权限,并通过 CSP 默认禁止外部资源、表单提交和脚本加载。 +- 修复 Web 动态页面 404/500 错误页的响应类型,确保浏览器按 HTML 页面渲染。 +- 补齐 `0.2.0` 数据库升级日志记录,并新增本版本升级日志记录。 +- 新增针对邮件 HTML 安全渲染、Unicode 截断和 Web 错误页响应类型的单元测试。 + +## 0.2.1 - 2026-07-06 + +- **回滚为服务端渲染架构**:恢复 HTML 页面渲染,移除独立 React 前端项目 +- **全面采用 Tailwind CSS**:通过 CDN 引入,统一所有页面样式 + - 更新日志列表/详情:卡片布局,蓝色点缀 + - 邮件列表:支持筛选表单、分页、Hover 高亮 + - 邮件详情:详细信息折叠面板、键盘左右键导航 + - Web 页面/Mail 提醒页面:元数据网格展示 +- 提取公共 `shell` 模块:统一 HTML 布局、导航栏和 Tailwind CDN 引入 +- 保留 JSON API 端点(`/v1/*`),供内部调用 +- 首页 `/` 重定向到更新日志 + +## 0.2.0 - 2026-07-06 + +- **重大架构调整**:前后端分离,将 HTML 服务端渲染改为 RESTful JSON API + React 前端 +- 新增 `ias-web` 前端项目,基于 Vite + React + TypeScript 构建 +- 后端 API 路由调整: + - 更新日志接口改为 `/v1/updates`(JSON) + - 新增页面列表接口 `/v1/web/pages`(GET) + - Web 页面查看接口改为 `/v1/web/pages/{type}/{slug}`(JSON) + - 邮件页面查看接口改为 `/v1/web/mail/{slug}`(JSON) + - 移除所有 HTML 渲染端点(`/updates`、`/mail`、`/web/{type}/{slug}` 等) +- 后端添加 CORS 支持(tower-http) +- 后端集成前端静态文件托管,`cargo build` 自动构建前端并嵌入服务 +- 新增 `build.rs`:前端源文件变更自动触发 `npm build`,无变更时零开销跳过 +- 支持 `SKIP_FRONTEND_BUILD` 和 `IA_STATIC_DIR` 环境变量 +- React 前端支持页面:首页、更新日志列表/详情、邮件列表/详情、通知页面 +- 前端开发代理配置自动转发 `/v1` API 请求到后端 + +## 0.1.15 - 2026-07-06 + +- 邮件预览页面全新设计:现代化卡片布局,顶部展示发件人、时间和来源等关键信息。 +- 新增上一封/下一封快速导航,无需返回列表即可连续浏览邮件。 +- 详细信息折叠面板,UID 和 Message-ID 支持一键复制。 +- HTML 邮件自动检测并在沙箱 iframe 中渲染,纯文本邮件保持原有展示方式。 +- 新增 `find_adjacent_ids` 仓库方法,按 ID 查询相邻邮件记录。 +- 无数据库结构变化;新增 `0.1.15` 升级日志记录。 + ## 0.1.14 - 2026-07-06 - 删除旧邮件查询兼容路由 `GET /mail-api/messages`、`GET /mail-api/messages/{id}` 和 `GET /mail-api/search`。 diff --git a/Cargo.lock b/Cargo.lock index a06722a..f58370f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1036,7 +1036,7 @@ dependencies = [ [[package]] name = "ias-ai" -version = "0.1.1" +version = "0.1.2" dependencies = [ "anyhow", "chrono", @@ -1080,7 +1080,7 @@ dependencies = [ [[package]] name = "ias-http" -version = "0.1.1" +version = "0.1.4" dependencies = [ "anyhow", "axum", @@ -1089,12 +1089,13 @@ dependencies = [ "serde_json", "sqlx", "tokio", + "tower-http", "uuid", ] [[package]] name = "ias-mail" -version = "0.1.7" +version = "0.1.13" dependencies = [ "anyhow", "async-imap", @@ -1113,7 +1114,7 @@ dependencies = [ [[package]] name = "ias-main" -version = "0.1.14" +version = "0.2.8" dependencies = [ "anyhow", "clap", @@ -1128,7 +1129,7 @@ dependencies = [ [[package]] name = "ias-service" -version = "0.1.1" +version = "0.1.7" dependencies = [ "anyhow", "axum", diff --git a/VERSION b/VERSION index 71d6a66..a45be46 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.1.14 +0.2.8 diff --git a/ias-ai/Cargo.toml b/ias-ai/Cargo.toml index 20da269..6468f51 100644 --- a/ias-ai/Cargo.toml +++ b/ias-ai/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ias-ai" -version = "0.1.1" +version = "0.1.2" edition = "2024" [dependencies] diff --git a/ias-http/Cargo.toml b/ias-http/Cargo.toml index 12d0886..074fc9d 100644 --- a/ias-http/Cargo.toml +++ b/ias-http/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ias-http" -version = "0.1.1" +version = "0.1.4" edition = "2024" [dependencies] @@ -16,4 +16,5 @@ sqlx = { version = "0.9.0", features = [ "chrono", ] } tokio = { version = "1.0", features = ["net", "sync"] } +tower-http = { version = "0.6", features = ["cors"] } uuid = { version = "1", features = ["v4", "serde"] } diff --git a/ias-http/src/app.rs b/ias-http/src/app.rs index 1e2c5cc..911795e 100644 --- a/ias-http/src/app.rs +++ b/ias-http/src/app.rs @@ -1,7 +1,9 @@ +use axum::response::Redirect; use axum::routing::get; use axum::{Json, Router}; use sqlx::PgPool; use tokio::sync::watch; +use tower_http::cors::{Any, CorsLayer}; use crate::updates::routes as update_routes; use crate::web::routes as web_routes; @@ -12,11 +14,19 @@ pub async fn create_http( db: PgPool, extra_routes: Router, ) -> anyhow::Result<()> { + let cors = CorsLayer::new() + .allow_origin(Any) + .allow_methods(Any) + .allow_headers(Any); + let app = Router::new() + .route("/", get(home)) .route("/v1/health", get(health)) .merge(update_routes::<()>(db.clone())) .merge(web_routes::routes::<()>(db)) - .merge(extra_routes); + .merge(extra_routes) + .layer(cors); + let listener = tokio::net::TcpListener::bind(&host).await.map_err(|err| { if err.kind() == std::io::ErrorKind::AddrInUse { anyhow::anyhow!( @@ -40,6 +50,10 @@ pub async fn create_http( Ok(()) } +async fn home() -> Redirect { + Redirect::permanent("/updates") +} + async fn health() -> Json { Json(serde_json::json!({ "success": true, diff --git a/ias-http/src/lib.rs b/ias-http/src/lib.rs index e84298b..e9e12e9 100644 --- a/ias-http/src/lib.rs +++ b/ias-http/src/lib.rs @@ -1,4 +1,5 @@ pub mod app; +pub mod shell; pub mod updates; pub mod web; diff --git a/ias-http/src/shell.rs b/ias-http/src/shell.rs new file mode 100644 index 0000000..2cebd49 --- /dev/null +++ b/ias-http/src/shell.rs @@ -0,0 +1,50 @@ +/// 公共 HTML shell:Tailwind CSS CDN +pub fn html_shell(title: &str, body: &str) -> String { + format!( + r#" + + + + + {title} + + + + +
+
+ iAs + +
+
+
+ {body} +
+ +"#, + title = escape_html(title), + body = body + ) +} + +pub fn escape_html(value: &str) -> String { + let mut escaped = String::with_capacity(value.len()); + for ch in value.chars() { + match ch { + '&' => escaped.push_str("&"), + '<' => escaped.push_str("<"), + '>' => escaped.push_str(">"), + '"' => escaped.push_str("""), + '\'' => escaped.push_str("'"), + _ => escaped.push(ch), + } + } + escaped +} diff --git a/ias-http/src/updates.rs b/ias-http/src/updates.rs index fcb7c7e..f32f777 100644 --- a/ias-http/src/updates.rs +++ b/ias-http/src/updates.rs @@ -8,6 +8,8 @@ use serde_json::Value; use sqlx::PgPool; use sqlx::prelude::FromRow; +use crate::shell::{escape_html, html_shell}; + type HtmlError = (StatusCode, Html); #[derive(Clone)] @@ -81,221 +83,113 @@ async fn find_upgrade_log(pool: &PgPool, version: &str) -> anyhow::Result