MailWeave:基于 Kotlin 多平台的 AI 邮件跨端项目

“Weave”寓意将邮件、AI 智能体、即时通讯平台、自定义钩子编织成一张统一的信息网络。名称简洁、国际化,且与项目“连接一切通讯渠道”的核心理念高度契合。|作者个人站:https://softwarescience.top/

分支1Tags4
文件最后提交记录最后更新时间
11 天前
11 天前
15 天前
15 天前
11 天前
15 天前
15 天前
11 天前
15 天前
11 天前
15 天前
15 天前
15 天前

MailWeave

作者个人站:https://softwarescience.top/ · 本项目主页:https://softwarescience.top/projects/mailweave(版本、截图与下载都在那儿)

“Weave”寓意将邮件、AI 智能体、即时通讯平台、自定义钩子编织成一张统一的信息网络。名称简洁、国际化,且与项目“连接一切通讯渠道”的核心理念高度契合。

备选名称:

  • PostPilot —— 强调 AI 作为邮件副驾驶的角色
  • MailNexus —— 强调多平台连接枢纽的定位
  • Kotlin MailHub —— 直白体现技术栈与枢纽属性

二、整体技术架构

2.1 技术栈总览

层次 技术选型 说明
跨端 UI Compose Multiplatform 一套 Compose UI 代码运行于 Android、iOS、Desktop、Web
鸿蒙适配 MYKMP / ovCompose 补充 HarmonyOS 的 Kotlin/Native target 支持
业务逻辑 Kotlin Multiplatform 共享 ViewModel、Repository、UseCase 层
AI 智能体 Koog + LangChain4j Koog 负责 Agent 编排与工具调用,LangChain4j 负责 LLM 接入与 RAG
邮件协议 Jakarta Mail / Kourrier IMAP/SMTP/POP3 协议栈
本地存储 SQLDelight 跨平台 SQLite 访问
网络层 Ktor Client HTTP/WebSocket 通信
序列化 kotlinx.serialization JSON 处理

2.2 分层架构

┌─────────────────────────────────────────────────────┐
│                   UI Layer (Compose MP)              │
│   MailList │ MailDetail │ Compose │ Settings │ AI Chat │
├─────────────────────────────────────────────────────┤
│                Presentation Layer (KMP)              │
│   ViewModels │ UI State │ Navigation                │
├─────────────────────────────────────────────────────┤
│                 Domain Layer (KMP)                   │
│   UseCases │ Models │ Repository Interfaces         │
├─────────────────────────────────────────────────────┤
│                Data Layer (KMP + Platform)           │
│   MailRepository │ AI Agent Service │ IM Bridge     │
│   SQLDelight DAO │ Ktor Client │ Token Store        │
├─────────────────────────────────────────────────────┤
│           Platform Layer (expect/actual)             │
│   Android │ iOS │ Desktop │ Web │ HarmonyOS         │
└─────────────────────────────────────────────────────┘

架构要点:UI 层使用 Compose Multiplatform 实现最大程度的代码复用,业务逻辑层完全共享。平台差异通过 expect/actual 机制处理,例如文件系统访问、通知推送、后台同步策略等。鸿蒙端需要补充 ohos_arm64 编译目标,并将共享 Kotlin 代码编译为动态库接入现有工程,Compose UI 经 Skia 绘制到 XComponent。

三、AI 智能体编排设计

3.1 Koog Agent 架构

Koog 是基于 Kotlin 的 AI Agent 框架,目标平台覆盖 JVM、Android、iOS、WebAssembly 和浏览器应用,擅长图策略编排,支持循环、分支、回退和并行分支执行。在本项目中,Koog 承担以下角色:

  • 邮件分诊 Agent:自动分类邮件(重要/普通/垃圾/待跟进),提取关键信息
  • 智能回复 Agent:基于上下文生成草稿回复,支持多轮对话
  • 日程解析 Agent:从邮件中识别会议邀请、截止日期,自动创建日历事件
  • 跨平台通知 Agent:判断哪些邮件需要推送到飞书/钉钉/微信

Koog 的 Agent 本质上是结构化的类型安全状态机,核心概念包括 Prompt Executor、工具节点、记忆存储等。Agent 策略以图的形式定义:

val mailTriageStrategy = strategy("mail-triage") {
    val analyze = node<String, MailAnalysis>("analyze") { input ->
        llmClient.execute(input.prompt)
    }
    val classify = node<MailAnalysis, MailCategory>("classify") { analysis ->
        when {
            analysis.hasActionItem -> MailCategory.ACTION_REQUIRED
            analysis.isUrgent -> MailCategory.URGENT
            else -> MailCategory.NORMAL
        }
    }
    val notify = node<MailCategory, Unit>("notify") { category ->
        if (category == MailCategory.URGENT) {
            imBridge.broadcast(NotificationPayload(...))
        }
    }

    analyze then classify then notify
}

3.2 工具注册与 MCP 集成

Koog 支持将任意 Kotlin 函数转化为 AI Agent 可调用的工具,支持串行和并行执行。同时,Koog 原生支持 MCP 集成,可通过 Kotlin MCP SDK 连接任何 MCP 服务器。

本项目中,将邮件操作为 MCP Tool 暴露给 Agent:

工具名 功能 对应实现
searchEmails 全文搜索邮件 SQLDelight FTS
sendEmail 发送邮件 SMTP Client
draftReply 生成回复草稿 LangChain4j LLM
addLabel 添加标签 本地数据库
forwardToIM 转发到 IM 平台 IM Bridge
createCalendarEvent 创建日历事件 Calendar API

3.3 A2A 跨 Agent 通信

Koog 从 0.5.0 版本开始集成 A2A 协议,使多个 Agent 能够通过标准化协议直接通信。在本项目中,邮件 Agent 可以将任务委托给日程 Agent、通知 Agent 等,实现插拔式的 Agent 协作。

3.4 LangChain4j 集成

LangChain4j 提供统一的 LLM 抽象层,原生支持 RAG、Agent、工具调用等高级功能,并且对 Kotlin 有专门的扩展支持,包括协程和挂起函数,以及类型安全构建器。

LangChain4j 在本项目中承担:

  • LLM 统一接入:支持 OpenAI、Claude、通义千问等模型的无缝切换
  • RAG 管道:将历史邮件作为知识库,为 AI 提供上下文增强
  • 记忆管理:维护多轮对话的 ChatMemory

四、跨平台方案

4.1 平台覆盖策略

Kotlin Multiplatform 已处于稳定阶段,Google 正式支持其在 Android 和 iOS 之间共享业务逻辑。Compose Multiplatform 允许跨平台共享 UI 代码,包括 iOS、Android、桌面和 Web。

平台 UI 方案 业务逻辑 备注
Android Compose MP KMP 共享 原生 target
iOS Compose MP KMP 共享 原生 target,Kotlin/Native
Desktop (Win/Mac/Linux) Compose MP KMP 共享 JVM target
Web Compose MP (Wasm) KMP 共享 Kotlin/Wasm
HarmonyOS Compose MP + MYKMP KMP 共享 需 ohos_arm64 target

4.2 鸿蒙适配方案

目前 JetBrains 或华为官方尚未提供 Compose Multiplatform 到鸿蒙的直接迁移文档,需要基于双方技术特性手动适配。可选方案包括:

方案 A:MYKMP(支付宝开源) 支付宝已正式开源基于 KMP + Compose Multiplatform 的三端方案 MYKMP,业务逻辑与 Compose UI 一次编写即可在 Android、iOS、HarmonyOS 三端复用。HarmonyOS 作为 Kotlin/Native target 参与多平台构建,共享 Kotlin 代码编译为动态库接入现有工程。

方案 B:ovCompose(腾讯开源) 腾讯视频团队基于 Compose Multiplatform 生态推出的跨平台框架,已在腾讯视频鸿蒙平台全面应用,是首个实现全跨端覆盖的 App。

建议:优先采用 MYKMP 方案,因其直接开源了 ohos_arm64 编译目标及完整的平台互操作定义,社区活跃度较高。

五、多平台 IM 集成设计

5.1 统一消息桥接层(IM Bridge)

设计一个统一的 IMBridge 抽象层,通过适配器模式对接各 IM 平台。核心接口定义:

interface IMChannel {
    val channelId: String
    suspend fun sendMessage(target: String, message: IMMessage): Result<Unit>
    suspend fun onMessageReceived(handler: suspend (IncomingMessage) -> Unit)
    suspend fun validateCredentials(): Boolean
}

// 适配器实现
class FeishuChannel : IMChannel { ... }
class DingTalkChannel : IMChannel { ... }
class WeComChannel : IMChannel { ... }
class CustomWebhookChannel : IMChannel { ... }

5.2 各平台集成要点

飞书:支持自定义机器人 Webhook 推送(https://open.feishu.cn/open-apis/bot/v2/hook/xxx),也支持机器人应用通过事件订阅接收消息。自定义机器人无需管理员审核,适合快速接入;机器人应用支持群管理、用户信息获取等完整能力。频率限制为单租户单机器人 100 次/分钟,消息体不超过 20KB。

钉钉:支持通过接口发送消息和通过 Webhook 发送消息两种方式。钉钉 Stream Mode 提供持久 WebSocket 连接用于接收机器人消息,并通过 Webhook/OAPI 发送回复,推荐使用 Stream Mode 以避免回调 URL 的配置复杂性。

企业微信:通过加密回调机制连接 Agent,需配置企业 ID、Token 和 EncodingAESKey,接收回调消息后通过 /cgi-bin/message/send 接口发送回复。需要域名主体与企业微信认证主体一致。

自定义通讯 App / 钩子函数:设计一个通用的 WebhookChannel,允许用户配置任意 HTTP 端点作为消息接收和发送通道。同时提供 Kotlin DSL 让开发者编写自定义钩子:

class CustomHookChannel : IMChannel {
    override val channelId = "custom-hook"
    override suspend fun sendMessage(target: String, message: IMMessage): Result<Unit> {
        return httpClient.post(target) {
            setBody(message.toJson())
        }.toResult()
    }
}

5.3 消息路由规则

AI Agent 根据邮件内容和用户预设规则,决定将哪些邮件通知路由到哪些 IM 平台:

触发条件 路由目标 消息格式
标记为“紧急” 飞书 + 钉钉 交互式卡片
含会议邀请 企业微信 文本 + 日历按钮
来自 VIP 发件人 所有渠道 富文本摘要
用户自定义规则 自定义 Webhook JSON 原始数据

六、邮件协议与数据层设计

6.1 邮件协议栈

协议 用途 推荐库
IMAP 接收邮件、文件夹同步 Jakarta Mail / Kourrier
SMTP 发送邮件 Jakarta Mail
POP3 旧邮箱兼容 Jakarta Mail
JMAP 现代邮件协议(可选) 自建

Kotlin 生态中,Kourrier 是 JavaMail API 的 Kotlin 封装库,支持 IMAP、SMTP 等协议。也可使用 SimpleKotlinMail,这是基于协程的 Kotlin 邮件 API,支持客户端和服务端场景。

6.2 本地存储设计

选用 SQLDelight 作为跨平台数据库方案,相比 Room KMP 在 KMP 场景下更成熟,性能表现更好。

核心数据表设计:

-- 账户表
CREATE TABLE account (
    id TEXT PRIMARY KEY,
    email TEXT NOT NULL,
    display_name TEXT,
    imap_host TEXT, imap_port INTEGER,
    smtp_host TEXT, smtp_port INTEGER,
    auth_type TEXT, -- PASSWORD / OAUTH2
    created_at INTEGER
);

-- 邮件表
CREATE TABLE email (
    id TEXT PRIMARY KEY,
    account_id TEXT NOT NULL,
    folder TEXT NOT NULL,
    uid INTEGER NOT NULL,
    subject TEXT,
    from_address TEXT,
    to_addresses TEXT,
    date INTEGER,
    body_preview TEXT,
    body_html TEXT,
    has_attachments INTEGER DEFAULT 0,
    is_read INTEGER DEFAULT 0,
    is_flagged INTEGER DEFAULT 0,
    ai_category TEXT, -- URGENT / ACTION / NORMAL / SPAM
    ai_summary TEXT
);

-- AI 分析结果
CREATE TABLE ai_analysis (
    email_id TEXT PRIMARY KEY,
    category TEXT,
    summary TEXT,
    action_items TEXT, -- JSON array
    sentiment REAL,
    analyzed_at INTEGER
);

-- IM 通知记录
CREATE TABLE im_notification (
    id TEXT PRIMARY KEY,
    email_id TEXT,
    channel TEXT, -- FEISHU / DINGTALK / WECOM / CUSTOM
    status TEXT, -- PENDING / SENT / FAILED
    sent_at INTEGER
);

6.3 OAuth2 认证

现代邮件服务(Gmail、Outlook 等)已逐步弃用密码认证,需支持 OAuth2。可选用 KMPAuth 等跨平台认证库,支持 Sign In with Google、Microsoft 等 OAuth Provider。

七、核心模块划分

mailweave/
├── shared/                          # KMP 共享模块
│   ├── core/
│   │   ├── model/                   # 数据模型
│   │   ├── repository/              # 仓库接口
│   │   └── util/                    # 工具类
│   ├── mail/                        # 邮件协议模块
│   │   ├── imap/                    # IMAP 客户端
│   │   ├── smtp/                    # SMTP 客户端
│   │   └── parser/                  # MIME 解析
│   ├── ai/                          # AI 智能体模块
│   │   ├── agent/                   # Koog Agent 定义
│   │   ├── llm/                     # LangChain4j 集成
│   │   ├── rag/                     # RAG 管道
│   │   └── tools/                   # MCP Tool 注册
│   ├── im/                          # IM 桥接模块
│   │   ├── bridge/                  # 统一接口
│   │   ├── feishu/                  # 飞书适配器
│   │   ├── dingtalk/                # 钉钉适配器
│   │   ├── wecom/                   # 企业微信适配器
│   │   └── custom/                  # 自定义 Webhook
│   ├── storage/                     # 数据存储
│   │   └── sqldelight/              # SQLDelight DAO
│   └── sync/                        # 同步引擎
│       ├── engine/                  # 同步调度
│       └── conflict/                # 冲突解决
├── ui/                              # Compose Multiplatform UI
│   ├── mail-list/                   # 邮件列表
│   ├── mail-detail/                 # 邮件详情
│   ├── compose/                     # 撰写邮件
│   ├── ai-chat/                     # AI 对话界面
│   ├── settings/                    # 设置
│   └── theme/                       # 主题
├── platform/                        # 平台特定实现
│   ├── android/
│   ├── ios/
│   ├── desktop/
│   ├── web/
│   └── harmony/                     # 鸿蒙适配
└── server/                          # 可选:后端服务
    ├── sync-gateway/                # 多设备同步网关
    └── push-service/                # 推送服务

八、建议开发路线

Phase 1:核心邮件功能(4-6 周)

  • KMP 项目脚手架搭建
  • IMAP/SMTP 协议栈接入
  • SQLDelight 数据层
  • Compose Multiplatform UI 基础框架
  • 邮件列表、详情、撰写三个核心页面

Phase 2:AI 能力集成(4-6 周)

  • LangChain4j 接入,配置 LLM Provider
  • Koog Agent 基础框架搭建
  • 邮件分诊 Agent + 智能回复 Agent
  • MCP Tool 注册
  • AI 对话界面

Phase 3:多平台 IM 集成(3-4 周)

  • IMBridge 统一抽象层
  • 飞书/钉钉/企业微信适配器
  • 自定义 Webhook 通道
  • 消息路由规则引擎

Phase 4:全平台适配(4-6 周)

  • iOS 平台适配与测试
  • Desktop 平台适配
  • Web (Wasm) 平台适配
  • 鸿蒙适配(基于 MYKMP 或 ovCompose)
  • 各平台通知推送集成

Phase 5:高级功能与优化(持续)

  • 多设备同步引擎
  • 离线优先架构优化
  • RAG 知识库构建
  • A2A 多 Agent 协作
  • 性能优化与内存管理

九、关键技术风险与应对

风险 影响 应对策略
Compose MP 鸿蒙适配不成熟 鸿蒙端开发受阻 优先采用 MYKMP,或先以 Android 端验证,鸿蒙端通过 WebView 桥接过渡
Koog 框架较新,API 可能变化 开发成本增加 封装一层抽象接口,隔离框架变更影响
多 IM 平台 API 差异大 适配工作量大 统一抽象层 + 适配器模式,分阶段接入
邮件协议兼容性 部分邮箱无法连接 支持 OAuth2 + 应用密码双模式,充分测试主流邮箱
AI 推理成本 运营成本高 支持本地模型(Ollama) + 云端模型混合模式

十、项目核心创新点

  1. 全 Kotlin 技术栈统一:从 UI 到 AI Agent 到邮件协议,全部使用 Kotlin 编写,通过 KMP 实现真正的全平台覆盖
  2. AI 原生邮件管理:不是简单地在邮件客户端上叠加 AI,而是将 Koog Agent 作为核心编排引擎
  3. 可编程的 IM 桥接:用户可以通过钩子函数和自定义 Webhook 连接任意通讯平台
  4. MCP 生态兼容:通过 MCP 协议,第三方开发者可以为 MailWeave 编写扩展工具
  5. 离线优先 + 多设备同步:本地数据优先,支持多设备通过自建同步网关保持一致

十一、实现状态(Desktop 端已可用)

本节描述仓库里已经跑起来的东西,与上文的目标架构对照阅读。上文是设计蓝图,本节是当前代码的真实边界。

11.1 技术栈落地版本

层次 设计选型 实际使用
跨端 UI Compose Multiplatform Compose Multiplatform 1.12.1(当前只启用 Desktop/JVM target)
业务逻辑 Kotlin Multiplatform Kotlin 2.4.21,共享代码放在 shared/src/commonMain
邮件协议 Jakarta Mail Eclipse Angus Mail 2.0.5(IMAP/SMTP,实现位于 jvmMain)
本地存储 SQLDelight SQLDelight 2.4.1 + SQLite JDBC
网络层 Ktor Client Ktor 3.6.0(Client: IM/LLM 出站;Server: 本机入站钩子)
序列化 kotlinx.serialization 1.11.0
AI 接入 LangChain4j LangChain4j 1.22.0(OpenAI 兼容 + Ollama 两种 ChatModel)
Agent 编排 Koog 暂未引入依赖(镜像站解析不到 Koog 1.3.0 产物)。MailAgent 抽象已就位,当前实现为 LangChain4j 直连 + 自研工具循环,契约见 11.5
协程 — kotlinx.coroutines 1.11.0

11.2 目录结构

shared/                                   KMP 共享模块(commonMain + jvmMain)
  commonMain/kotlin/com/mailweave/
    core/model/          MailAccount / EmailMessage / MailFolder / AI 与 IM 模型 / AppSettings
    core/repository/     AccountRepository / MailRepository / ImRepository / SettingsRepository
    core/credential/     CredentialVault(凭据库,独立于数据库)
    mail/                MailGateway 契约、MailSender、HtmlTextExtractor、CalendarExporter(expect)
    storage/             SQLDelight 仓库实现 + 行映射 + 布尔↔整数适配
    ai/                  LlmClient 抽象、MailPrompts、MailAnalysisParser、MailAgent、
                         ReplyAgent / ScheduleAgent / ChatAgent、MailTool 注册表(6 个工具)
    im/                  IMChannel 抽象、飞书/钉钉/企微/自定义 Webhook 适配器、
                         RoutingRules、NotificationRouter、ImCommandHandler
    sync/                MailSyncEngine(增量同步 + 按需拉正文 + 回写已读)
  commonMain/sqldelight/ 8 张表:account / folder / email / ai_analysis /
                         im_notification / im_channel / routing_rule / app_setting
  jvmMain/kotlin/com/mailweave/
    mail/                JakartaMailGateway(IMAP 抓取、MIME 解析、SMTP 发送、\Seen 回写)、IcsBuilder
    ai/                  LangChain4jLlmClient(含工具调用循环)
    im/                  InboundWebhookServer(本机 HTTP 钩子)
    platform/            文件系统、HMAC 签名、时间格式化、文件选择框
    app/AppContainer     组合根(数据库 + 凭据库 + 仓库 + 网关 + AI + IM 的装配)
composeApp/                               Compose UI
  commonMain/kotlin/com/mailweave/ui/
    AppViewModel.kt      单一状态容器(StateFlow + Flow 订阅)
    MailWeaveApp.kt      顶栏 / 导航 / 状态栏 / 写信面板挂载
    screens/             MailScreen(三栏)、ChatScreen、SettingsScreen、ComposeMailPanel
    components/Widgets.kt 统一组件(卡片、字段、枚举选择、分类徽标、邮件行)
  jvmMain/kotlin/com/mailweave/Main.kt   桌面入口(含 `-Dmailweave.smoke` / `-Dmailweave.screenshot` 开关)

11.3 构建、运行与打包

# 本机 JAVA_HOME 默认指向 JDK 25,Kotlin/Compose 未验证;先切到 JDK 21
export JAVA_HOME="D:/Develop/runtimes/Javas/jdk-21.0.11-full"

./gradlew :composeApp:run                    # 启动桌面端
./gradlew test                               # 全部测试(含 GreenMail 端到端)
./gradlew :composeApp:packageDistributionForCurrentOS   # 生成 Windows msi 安装包
./gradlew :composeApp:run -Pmailweave.smoke=true         # 冒烟:启动后自动退出并打印 MAILWEAVE_SMOKE_OK

依赖仓库固定走国内镜像(settings.gradle.kts 中的 Google/Central 镜像),首次构建需要下载较久。

11.4 已实现能力(可直接在桌面端使用)

能力 说明
多账号管理 IMAP/SMTP 主机、端口、SSL/STARTTLS、密码或 XOAUTH2 令牌;内置 QQ/163/阿里云/Gmail/Outlook/飞书预设,可按域名自动猜测
连接自检 「测试连接」同时验证 IMAP 与 SMTP,分别报告失败原因
增量同步 按 IMAP UID 游标拉取最新邮件(默认每文件夹 50 封),文件夹未读数/总数落库;单文件夹同步与全量同步并存
邮件阅读 三栏布局;列表显示发件人/主题/摘要/分类/标签/附件标记;打开时按需下载正文:HTML 邮件按 HTML 排版渲染(标题/列表/表格/加粗/斜体/引用/链接可点),只有纯文本时才按纯文本显示,两者都有时可一键切换;正文里声明的 charset 与实际字节不符(国内邮件常见)也能纠正解码
附件预览 正文下方列出全部附件;左键或右键点开菜单,用系统默认程序打开(先下载到 <tmp>/mailweave-attachments,随看随下,不占本地库)
检索 本地缓存全文检索(主题/发件人/预览/正文/AI 摘要/标签)
写信与发送 收件人/抄送/密送、附件(系统文件选择框)、签名;SMTP 发送;依赖服务器 Sent 文件夹保留副本
标记与标签 已读/未读(回写 IMAP \Seen)、星标、本地标签
AI 分诊 分类(紧急/待处理/普通/垃圾)+ 摘要 + 待办 + 情感 + 是否需要回复 + 日程线索,结果落库并在列表/详情展示;支持批量补跑未分诊邮件
AI 回复草稿 结合来信上下文生成草稿并填入写信面板,发送前人工确认
AI 日程抽取 从邮件抽取会议/截止时间,可导出 .ics(createCalendarEvent 工具)
AI 对话 + 工具调用 Agent 循环可调用 6 个工具:searchEmails / sendEmail / draftReply / addLabel / forwardToIM / createCalendarEvent
IM 通知桥接 飞书(文本 + 交互式卡片 + 加签)、钉钉(Markdown/文本 + 加签)、企业微信(access_token + 应用消息)、自定义 Webhook(模板 + 自定义请求头);一键测试发送
路由规则 条件类型:AI 分类 / VIP 发件人 / 会议邀请 / 主题关键字 / 发件人关键字 / 始终;按优先级匹配,同一邮件同一渠道幂等只推一次;失败原因落库可查
入站钩子 本机 POST /hooks/{通道ID} 执行命令:/search、/recent、/summary、/notify、/status,兼容飞书/钉钉事件文本字段
本地模型支持 Ollama 直连(默认 http://localhost:11434),无云端 API Key 也能跑全流程

11.5 已知边界与未实现项(明确声明,避免误解)

  • 平台:只启用 Desktop(JVM)。Android/iOS/Web/鸿蒙 target 未添加(需要各自 SDK 与主机环境);共享代码已按 expect/actual 组织,可增量启用。
  • POP3:可用,但只有单一收件箱、无服务器端标志位(已读/星标写不回去),UID 由 UIDL 派生;由 Jakarta 引擎承载(自研协议栈暂无 POP3,切到内置栈时 POP3 账号会自动回落到 Jakarta)。
  • OAuth2 的边界:授权流程本身已打通(细节见 §16),但应用不带任何内置客户端凭据——需要你自己注册应用、把客户端 ID(Google 还要客户端密钥)填进设置;另外 Google 的授权端点(accounts.google.com / oauth2.googleapis.com)在境内网络不可直连,需要自备代理。
  • Koog:MailAgent(分诊)与 ChatAgent(工具循环)抽象、提示词、6 个工具清单都已就位,但 Koog 依赖尚未引入——本次构建环境中华为云/阿里云镜像均无法解析 ai.koog:koog-agents:1.3.0 的实现 jar(只有 POM/metadata)。因此当前运行的是 LangChain4j + 自研工具循环实现,Koog 图策略可作为 MailAgent 的另一种实现增量接入,业务代码无需改动。
  • 日历:只做抽取与 .ics 导出,不写入系统日历、不调用 Google/Outlook Calendar API。
  • IM 入站:仅自定义通道提供本机 HTTP 入站;飞书/钉钉/企微的收消息(事件订阅 / Stream Mode)未实现,三者当前为出站通知。
  • 发送副本:不在本地伪造「已发送」,依赖服务器自动归档;IMAP APPEND 到 Sent 未实现(避免与服务器重复归档冲突)。
  • 凭据存储:明文 JSON 存于 <用户目录>/.mailweave/vault.json(不进仓库、不进日志);系统钥匙串(Windows Credential Manager 等)属于后续工作。
  • 同步调度:syncIntervalMinutes 已落库并在设置页可配,但后台定时器尚未接入,目前由「启动时同步」与手动同步触发。

11.6 验证方式

  • shared/src/jvmTest:153 个测试(3 个 LiveMailTest 需要真实账号环境变量,未提供时跳过),其中 JakartaMailGatewayTest / NativeAttachmentTest / Pop3AndPresetTest 用 GreenMail 起真实 IMAP/POP3/SMTP 端口,覆盖文件夹列举、增量抓取、正文/附件解析与按需下载、SMTP 发送、\Seen 回写、连接自检失败路径、charset 错标纠正;其余覆盖数据层(含重复同步不丢 AI 结果与标签)、路由匹配、通知幂等与失败记录、IM 协议报文(飞书/钉钉/企微/webhook,MockEngine)、入站钩子协议、同步引擎(假网关)、HTML 转文本、OAuth2 两条授权流程、ICS 生成。
  • composeApp/src/jvmTest:15 个测试,AppRenderTest 用 ImageComposeScene 离屏渲染真实 UI(邮件/设置/对话/写信/账号/提醒/数据/OAuth 配置),断言渲染尺寸与画面内容复杂度,并把 PNG 输出到 composeApp/build/screenshots/;HtmlMailViewTest 覆盖 HTML 解析与渲染;AppEndToEndTest 跑通收信→落库→分诊→推送→提醒→读正文→发信全链路。
  • 桌面冒烟::composeApp:run -Pmailweave.smoke=true 真实启动窗口与数据库,打印 MAILWEAVE_SMOKE_OK。

十二、第二次迭代:协议补齐 / 微软邮箱 / 提醒 / 数据迁移 / UI 重做

12.1 收信协议:IMAP + POP3 + SMTP

协议 能力 说明
IMAP 多文件夹、UID 增量、服务器已读回写(\Seen) 默认协议,推荐
POP3 单收件箱、UIDL 稳定标识、按需拉正文 老邮箱兜底;协议本身无标志位,已读只存在本地
SMTP SSL / STARTTLS、附件、XOAUTH2 发送副本由服务器自动归档

网易系(163/126/yeah.net/网易企业邮)实测坑:SMTP/IMAP 登录能过,但一开文件夹就被拒: C4 NO EXAMINE Unsafe Login. Please contact kefu@188.com for help。 原因是没有先发 IMAP ID 命令。现在账号带 imapIdRequired 开关,网易系预设默认打开,连接后会先 IMAPStore.id(name/version/vendor/support-email) 再操作文件夹。

12.2 微软邮箱与 OAuth2(回答“OAuth2 是什么问题”)

微软已停用 IMAP/SMTP 的密码认证,必须用 OAuth2。之前的实现只能“手工粘贴一个 access token”, 问题在于:token 一小时就过期,没有刷新能力,也不能帮用户走授权流程——等于只有半个 OAuth2。 本次补齐为完整的 RFC 8628 设备码流程:

  1. POST /devicecode 拿 user_code + verification_uri,界面显示“在浏览器输入这段验证码”;
  2. 按 interval 轮询 /token,正确处理 authorization_pending / slow_down / access_denied / expired_token;
  3. 成功后把 refresh_token 存进本机凭据库,access token 到期前 5 分钟自动刷新;
  4. 连接 IMAP/SMTP 时用 XOAUTH2(mail.<protocol>.auth.mechanisms=XOAUTH2,access token 当密码用)。

需要你自己注册一个公共客户端 ID(免费、两分钟):Azure 门户 → 应用注册 → 移动和桌面应用 → 允许公共客户端流 → 委托权限 IMAP.AccessAsUser.All、SMTP.Send,把客户端 ID 填进「设置 → 账号 → OAuth2」。 Google 同理(https://mail.google.com/)。刷新令牌与密码一样只存本机 vault.json。

12.3 内置邮箱预设(个人 + 企业)与自定义预设

内置预设已覆盖:网易(163 / 126 / yeah.net / 网易企业邮 qiye.163.com)、腾讯(QQ 邮箱 / Foxmail / 腾讯企业邮 exmail.qq.com)、华为云企业邮箱、阿里云邮箱 / 阿里企业邮、飞书邮箱、 Microsoft 365 / Outlook.com、Gmail,每项都带 IMAP + POP3 + SMTP 三套参数与加密方式。 「设置 → 账号」里选中预设即可一键填好;预设置信度由代码注释标明来源,核实不到的会显式标记为需要手填。

任何自建/公司邮箱也能用:手填服务器后点「保存预设」,会作为自定义预设持久化并出现在预设行里, 下次直接复用(AppSettings.customPresets)。输入邮箱地址时也会按域名自动套用能猜到的预设。

12.4 提醒:不写系统日历,改为到点提醒

AI 分诊抽到会议/截止时间后,会按「提前 N 分钟」落成本地提醒;邮件详情里也有「提醒我」快捷项 (1 小时后 / 3 小时后 / 明天 9:00 / 下周一 9:00)。到点由后台调度弹系统通知(Windows 托盘气泡), 可选同时推 IM。提醒页集中展示“已到期 / 即将到来 / 已结束”,可一键完成、忽略、删除、跳回原邮件。

12.5 后台同步(回答“能不能用 webdev 之类的同步”)

当前实现是应用内定时器(SyncScheduler,每 30 秒醒一次,按 syncIntervalMinutes 给每个账号的收件箱做增量同步), 关掉应用就停。可选路线与取舍:

方案 实时性 代价
应用内定时器(已实现) 分钟级 需要开着应用
IMAP IDLE 长连接(建议下一步) 秒级 每账号一条常驻连接,需处理断线重连,服务端有并发限制
headless 守护进程(同 jar 加 --daemon) 分钟级 不需要界面,但要做成服务/开机项
外部 web 服务 + 本机钩子 取决于对方 桌面端已提供 POST /hooks/{通道ID} 入站口,外部系统可主动触发 search/recent/summary/notify/status

结论:不需要另做一个 web 端同步服务也能满足“后台同步”;真要秒级实时,正确做法是 IMAP IDLE 而不是轮询 web。

12.6 数据存放位置与迁移

  • 数据目录默认 ~/.mailweave(mailweave.db + vault.json + calendar/)。
  • 「设置 → 数据存储」可改到任意位置,提供三种动作:迁移(搬走,清源)/ 复制(保留源,便于备份)/ 切换到已有目录。
  • 迁移前会先关闭数据库连接(避免拷到半写状态),全部复制成功后才写指针与清理源目录;失败则保持原状。
  • 引导目录里只放一个 location.json 指针,所以换到别处后仍能在原处找到新位置,重启后生效(界面会给出重启提示与退出按钮)。

12.7 UI/UX 重做

视觉:统一设计令牌(配色 / 间距刻度 / 圆角 / 字阶),左侧导航栏 + 顶部操作区 + 底部状态条的三段式外壳; 邮件三栏(文件夹 / 列表 / 阅读),列表带头像色块、未读圆点、分类胶囊、标签与星标,悬停/选中都有反馈; 设置改成分页签(账号 / AI 模型 / IM 渠道 / 路由规则 / 同步与提醒 / 数据存储 / 关于)。

UX(这次特别补的部分):

  • 键盘快捷键:Ctrl+N 写信、Ctrl+R/F5 同步、Ctrl+F/Ctrl+K 聚焦搜索、Ctrl+Enter 发送、 Esc 关闭写信/弹窗、Ctrl+1..4 切换页面;列表里 J/K 上下移动(自动滚动到可见)、Enter 打开、 U 切换已读、S 切换星标。
  • 危险操作二次确认:删除账号/渠道/规则/提醒都会弹确认,并说明影响范围(“服务器上的邮件不受影响”)。
  • 记住上次位置:账号与文件夹选择持久化,重开后回到原处;但不会因此触发自动同步,避免突然跑流量。
  • 输入即引导:输入邮箱地址自动套用能猜到的预设;缺少凭据、模型未配置等情况都给可点击的补救入口。
  • 就近反馈:连接测试结果显示在表单内,操作结果与错误集中在底部状态条且可一键关闭;发送失败保留草稿不丢内容。
  • 空态可行动:无账号时直接给“去添加账号”,有账号无邮件时给“同步”按钮。

12.8 包路径

源码包根改为 top.softwarescience.mailweave(Gradle group 同为 top.softwarescience), 即 shared/src/commonMain/kotlin/top/softwarescience/mailweave/...。

12.9 真机验证(用真实 163 账号)

shared/src/jvmTest/kotlin/.../live/LiveMailTest.kt 会走真实的 IMAP/POP3/SMTP(含发一封自检邮件), 凭据只从环境变量读取,未设置时显式 skip:

MAILWEAVE_LIVE_EMAIL=xxx@163.com MAILWEAVE_LIVE_PASSWORD=授权码 \
  gradle :shared:jvmTest --tests "top.softwarescience.mailweave.live.LiveMailTest"

十三、平台路线图(Roadmap)

13.1 现状与总览

平台 当前状态 可复用的部分 主要缺口
Windows / macOS / Linux(Desktop) ✅ 已可用:Windows 已验证运行与打包;macOS/Linux 同一套 JVM 代码与 AWT 实现,配置已就位 全部 需在对应系统上出包并做一轮人工验证
Android ⛔ 未启用 commonMain 的模型/仓库/AI/IM/同步引擎、整套 Compose UI、SQLDelight schema 邮件协议栈(Jakarta Mail 是 JVM 专属)、Android 端 actual 实现、通知与文件访问
iOS ⛔ 未启用 同上 需要 macOS 主机 + Xcode;邮件协议栈;Kotlin/Native target 与 actual 实现
HarmonyOS ⛔ 未启用 同上 需要 MYKMP / ovCompose 提供 ohos_arm64 target;邮件协议栈

13.2 关键决策:移动端的邮件协议栈怎么选(必须先定)

Jakarta Mail / Angus Mail、LangChain4j 都是 JVM 专属,这是移动端最大的阻塞点。三条路线:

方案 做法 优点 代价
A. 自研 KMP 原生协议栈 用 Ktor Socket 在 commonMain 写 IMAP/SMTP(含 IDLE、SASL XOAUTH2),全平台共用一套 一次投入长期受益;GreenMail 可端到端验证;不再受 JVM 限制 工作量最大(IMAP 子集 + MIME 解析约 2000 行),首版功能需裁剪
B. 各端独立实现 Android 用 android-mail(javax.mail 分支)或自研;iOS 走 Swift 侧 IMAP 库;鸿蒙用 Java/Kotlin 侧实现 每端都能快速跑通 同一套逻辑维护 3 遍,行为不一致风险高
C. 瘦客户端 + 同步网关 手机不直连 IMAP,走 HTTP/WebSocket 连自建网关(server/sync-gateway),网关持有 IMAP 连接、跑 AI 与提醒,手机只做展示与推送 移动端最快落地;省电、推送友好;顺带把“后台同步”升级成常驻服务 需要一台常驻机器/服务器;离线能力受限

建议顺序:先 A 的“最小可用子集”(GET/SEARCH/FETCH 头 + 正文 + SMTP 发送 + IDLE),它同时解决 Android/iOS/鸿蒙;C 作为 A 未完成期间的过渡(桌面已具备入站钩子,扩展成网关成本不高)。

13.3 分阶段计划

0.2 —— 桌面做扎实

  • IMAP IDLE 长连接(秒级收信,替代轮询);后台同步可选“IDLE 优先、轮询兜底”。
  • Google OAuth2 联调(设备码 + 刷新);微软邮箱用真实账号再验一轮。
  • 华为云企业邮箱服务器参数核实(当前标为需手填)。
  • 多账号并发同步 + 失败退避重试;同步日志面板。
  • 备份/恢复(导出 mailweave.db + vault.json 为单文件包);自动更新(jpackage + 更新源)。

0.3 —— Android

  • 前置:androidTarget() + SQLDelight android-driver + Compose MP Android 壳工程。
  • 需要新写的 actual:应用数据目录、通知(NotificationManager)、附件选择(SAF)、HTTP 钩子。
  • 邮件栈按 13.2 选定的方案接入;先只做“收信 + 阅读 + 回复 + 提醒”。
  • 验收:真机安装、真实邮箱收发、后台通知、断网重连。

0.4 —— macOS / Linux 正式出包

  • 在对应系统上 packageDistributionForCurrentOS:macOS 出 dmg(需要 Apple 开发者签名与公证才能免“未知开发者”提示)、Linux 出 deb/rpm。
  • 平台适配:macOS 使用钥匙串替代 vault.json、菜单栏与 Dock 行为;Linux 桌面通知走 libnotify(当前托盘通知在部分桌面环境不可用)。

0.5 —— iOS

  • 前置:macOS + Xcode;Compose MP iOS target;Framework 集成方式(CocoaPods/SPM)。
  • 复用 commonMain 全部业务代码与 UI;写 iOS actual(文件、通知、后台刷新)。
  • 验收:TestFlight 内测、真机收发、分享扩展(“用 MailWeave 转发这封邮件”)。

0.6 —— HarmonyOS

  • 前置:MYKMP(或 ovCompose)提供的 ohos_arm64 target 与 DevEco 工程接入;共享代码编译为动态库。
  • Compose UI 经 Skia 绘到 XComponent(MYKMP 方案)。

1.0 —— 多设备与生态

  • 自建同步网关(server/sync-gateway):桌面/手机共享同一份账号与提醒,配合 C 方案还能给移动端省电。
  • MCP 工具生态对外开放;RAG 知识库(历史邮件检索增强);A2A 多 Agent 协作。

13.4 每个阶段的验收口径(避免“看起来做完了”)

  • 协议类改动:必须跑通 LiveMailTest(真实服务器)+ GreenMail 端到端,并留下输出。
  • UI 类改动:必须离屏渲染或真实运行截图复核,不能只看编译通过。
  • 平台类改动:必须在该平台上跑起来一次(打包产物启动 + 收发一封真实邮件),否则只能标“未验证”。

十四、打包与发布

14.1 版本与产物

  • 版本号单一来源:gradle.properties 的 mailweave.version(当前 0.4.0),Gradle 会据此生成 MailWeaveInfo.kt, 应用内显示、打包版本、后续发布都读它,不再有两处硬编码。
  • 目标格式(composeApp/build.gradle.kts):Windows msi/exe、macOS dmg、Linux deb/rpm。 jpackage 不支持交叉构建:在 Windows 上只能出 Windows 产物,dmg 必须在 macOS 上构建、deb/rpm 必须在 Linux 上构建。
  • 常用命令:
./gradlew :composeApp:createDistributable            # 开发版免安装运行时
./gradlew :composeApp:createReleaseDistributable     # 发布版(ProGuard 压缩 + 合并 jar)
./gradlew :composeApp:packageDistributionForCurrentOS # 当前系统的安装包(Windows 需安装 WiX)

14.2 包体优化做了什么

  • 内置运行时按模块裁剪:nativeDistributions.modules(...) 只带必需的 JDK 模块(java.sql/java.net.http/ java.naming/java.security.sasl/jdk.unsupported 等)。新增依赖后要重跑 :composeApp:suggestRuntimeModules 对齐。 注意:如果本机装了 JDK 17,jlink 会把整个模块图带进来,包体会明显变大——这是官方已知行为,不是本项目的问题。

  • ProGuard 发布版实测效果(createReleaseDistributable):

    产物 总大小 app 目录 jar 数
    开发版 createDistributable 167 MB 81 MB 97 个
    发布版(压缩 + 合并 jar) 151 MB 64 MB 1 个

    jar 合并不只是省体积,也少掉 96 次类路径扫描,冷启动更稳。规则见 composeApp/proguard-rules.pro。 Windows 安装包同样受益:MailWeave-0.2.0.msi 开发版 95 MB → 发布版 79 MB(-17%)。 (Compose 插件会自动下载 WiX,所以 Windows 上不需要手工装 WiX 也能出 msi。)

  • ProGuard 踩过的三个坑(都已验证并固化):

    1. -dontwarn 的参数是「产生告警的引用方类」,不是缺失的类;写错会一直 Please correct the above warnings first。
    2. optimize=true 会让 Compose 启动即抛 configureSwingGlobalsForCompose 相关异常 —— 所以发布版只做压缩与 jar 合并,不动字节码优化。
    3. kotlinx.coroutines 必须整包 keep:否则 JobSupport.cancel() 被改写成 invokespecial 非法字节码,启动直接 VerifyError: Bad invokespecial instruction ... indirect superinterface。
  • 发布版自检:注入 -Dmailweave.smoke=true 跑一次打包产物(本次实测 exit 0 并打印 MAILWEAVE_SMOKE_OK),只编译通过不算数。

  • 图标资产可复现:packaging/generate-icons.java 一条命令生成 png/ico/icns,不手工维护二进制。 ICO 必须是小端的多尺寸 DIB(16/24/32/48/64/128/256),只塞一张 PNG 或写成大端都会被 Win32 判为“数据无效”, jpackage 报 无法更新 ... 的图标;ICNS 则相反是大端。

  • 打包后的自检:createDistributable 产物用 java-options=-Dmailweave.smoke=true 注入到 app/MailWeave.cfg 里跑一次,确认能独立启动(这是发现“漏带 JDK 模块”的唯一有效手段)。

关于

这个项目由 CodeLife Leon 独立开发与维护。更多项目、各项目发版信息、赞赏与联系方式都在个人站:

问题反馈与需求建议:优先在仓库提 Issue,也可以邮件 virgil_lgp@163.com。


十五、自研 KMP 原生协议栈(路线 A 已落地第一版)

15.1 为什么自己做

Jakarta Mail / Angus Mail 与 LangChain4j 都是 JVM 专属,这是 Android/iOS/鸿蒙复用的最大阻塞点。 按路线图 13.2 选了方案 A:自研 KMP 原生协议栈(Ktor Socket + TLS),第一版已完成并在真实 163 账号上验证。

15.2 代码位置与能力

shared/src/commonMain/kotlin/top/softwarescience/mailweave/mail/
  wire/MailWireTypes.kt     MailConnectionConfig / RemoteFolder / MailboxStatus / RemoteHeader / MailWireException
  wire/ImapClient.kt        自写 IMAP:CAPABILITY / LIST / EXAMINE|SELECT / UID FETCH / UID STORE / UID SEARCH /
                            LOGIN / AUTHENTICATE XOAUTH2 / ID(网易系必需)/ STARTTLS / IDLE / LOGOUT,
                            自己实现 CRLF 行读取与 `{n}` 字面量按长度读取
  wire/SmtpClient.kt        自写 SMTP:EHLO/HELO 回退、能力解析、STARTTLS、AUTH(XOAUTH2 / PLAIN / LOGIN)、
                            MAIL FROM / RCPT TO / DATA(dot-stuffing)
  wire/MimeCodec.kt         Base64 / quoted-printable / RFC 2047 编码字 / RFC 2231 参数 + `expect decodeText`
  wire/MimeHeaderParser.kt  头部折叠展开、重复头、大小写不敏感
  wire/MimeMessageParser.kt multipart 递归(深度上限)、CTE 先解字节再按 charset 解码、附件与预览
  wire/MimeComposer.kt      发信报文的 commonMain 组包(UTF-8 + base64、RFC 2047 主题与文件名)
  wire/ModifiedUtf7.kt      IMAP modified UTF-7(中文文件夹名)
  wire/AddressParser.kt     地址头解析
  wire/TlsSupport.kt        TLS 收口:`mailTlsConfig`(expect) / `Socket.connectTls` / `Connection.upgradeTls`
  NativeMailGateway.kt      用上面这些实现既有的 MailGateway 契约(UI/同步/AI 全都不用改)
  SwitchableMailGateway.kt  按设置路由到 Jakarta 或原生引擎,切换不需要重启

设置入口:设置 → 同步与提醒 → 邮件协议引擎(MailEngine.JAKARTA 默认 / MailEngine.NATIVE)。 两个引擎共用同一套账户、缓存、AI 分诊与 IM 路由。

15.3 真机验证证据(163 真实账号)

环节 结果
IMAP over TLS(imap.163.com:993) ✅ 问候→CAPABILITY→LOGIN→ID 命令→EXAMINE
列文件夹 ✅ 6 个,且中文名解码正确:&V4NXPpCuTvY-→垃圾邮件(SPAM)、&XfJT0ZAB-→已发送(SENT)、&g0l6Pux-→草稿箱(DRAFTS)
增量抓头 / 取正文 ✅ 4 封,uid 升序,正文按 charset 正确解码
SMTP over TLS(smtp.163.com:465) ✅ 发送成功,163 侧确实收到(抓取结果里能看到自己发的自检邮件)
GreenMail 线协议用例 ✅ IMAP/SMTP/MIME 共 20 个用例(含 CDC、字面量、dot-stuffing、附件、GBK 解码)

15.4 真机验证挖出的两个坑(已修)

  1. 隐式 SSL 必须用 Socket.tls(...),不能用 Connection.tls(...):后者在 465 上会卡死握手(30s 超时), 而前者在 993/465 上都正常。现在 TLS 收口在 TlsSupport.kt,两个客户端共用同一实现。
  2. 中文文件夹名是 modified UTF-7:不解码不仅显示乱码,还会让文件夹类型识别失效(全部落到 CUSTOM)。
  3. 登录后必须重新取 CAPABILITY(RFC 3501):很多服务器只在认证后才公告 IDLE 等能力;163 实测前后都不公告, 但这条刷新是通用正确性修复。
  4. 默认引擎已切到自研协议栈:这样 IMAP 账号开箱即得实时收信;POP3 账号由 SwitchableMailGateway 自动回落到 Jakarta(自研栈暂无 POP3),不会因为换默认引擎而失效。

15.5 当前边界(如实声明)

  • POP3 未实现:原生引擎遇到 POP3 账号会返回明确提示,请切回 Jakarta 引擎(桌面端两者都可用)。
  • IMAP IDLE 只到协议层:switchToIdle / awaitIdleEvent 已实现并有能力一致性用例,但还没接进同步引擎做常驻推送(当前仍是定时增量拉取)。
  • STARTTLS 分支未在真实服务器上验证:163 的 993/465 都是隐式 SSL,STARTTLS 只有代码路径与 GreenMail 侧的静态核对。
  • iOS / 鸿蒙 actual 待补:decodeText、mailTlsConfig 目前只有 JVM(Android 可直接复用 JVM 实现)。 补两个同签名 actual 即可让整套协议栈在 Kotlin/Native 上编译通过。
  • XOAUTH2 未对真实 OAuth2 服务器验证(协议报文有单测)。

15.6 实时收信(IMAP IDLE)

  • 实现:sync/RealtimeSyncService 给每个 IMAP 账号挂一条常驻 IDLE 监听;收到服务器推送后去抖 1.5 秒 再做增量同步,并弹系统通知(可在设置里关)。断线按 5s→10s→…最多 5 分钟指数退避重连。 状态栏显示「● 实时收信」与上次收信时间。
  • 哪个引擎提供:目前只有自研协议栈(NativeMailGateway.watch,自己发 IDLE/DONE,每轮 ≤29 分钟)。 Jakarta 引擎刻意不实现:Angus 的 IMAPFolder.idle() 是阻塞调用,取消协程不会中断它,而从另一个线程 close() 打断又会与 idle 争同一把锁 —— 实测直接把测试任务挂死、greenMail.stop() 也等不到连接关闭。 与其留一个退不掉的连接,不如让它只做定时同步兜底。
  • 163/126 为什么没有秒级推送:实测 163(Coremail)登录前后都不在 CAPABILITY 里公告 IDLE (只有 XAPPLEPUSHSERVICE/X-CM-EXT-1 这类私有扩展)。所以网易邮箱会走到"明确拒绝 → 回落定时同步"这条 正常路径;《服务器支持 IDLE 时》才是秒级推送(GreenMail 用例已证明推送到回调的完整链路)。
  • 验收:IdleWatcherTest(GreenMail:建立 IDLE → 投递 → 回调触发 → 取消后 15 秒内退出); LiveMailTest.idlePushRoundTrip(真机:服务器支持就做"自发自收 + 等待推送",不支持就断言"干脆拒绝且不挂死")。

15.7 下一步(把路线 A 走完)

  1. 补 POP3 到原生栈(UIDL 去重模型已有),这样默认引擎不再需要 Jakarta 兜底。
  2. 补 iOS / 鸿蒙 actual,然后按 13.3 的 0.3 / 0.5 / 0.6 阶段推移动端 —— 移动端按你的意思先只留在路线图里, 近期精力放在桌面端可用与好用。

十六、第三次迭代(v0.4.0):正文排版 / 附件预览 / OAuth2 全流程

这一版只解决三个"明明有功能却用不了"的问题,改动都落在用户能看见的地方。

16.1 正文不再被压成纯文本(并顺手治乱码)

问题:阅读区取的是"纯文本形态的正文"(EmailMessage.displayBody),HTML 邮件在抓取阶段就被 HtmlTextExtractor 压平了 —— 表格散成一行、加粗/链接/列表全丢;而部分中文邮件还会整段乱码。

做法:

  • 新增 ui/components/HtmlMailView.kt:一个绝不抛异常的 HTML 解析器 + Compose 渲染器(commonMain,无新依赖、 不用 jsoup —— jsoup 是 JVM 专属,这个组件将来要被移动端复用)。块级结构映射成 HtmlBlock (标题/段落/有序无序列表/引用/pre/分割线/表格/图片占位),行内样式映射成 HtmlSpan(粗体/斜体/下划线/删除线/等宽/链接)。 链接可点(LocalUriHandler),表格按 Row + weight(1f) 画成网格(邮件 HTML 大量用表格排版), cid: 内联图降级为 [图片] alt 占位而不是破图。
  • 阅读区按邮件真实内容展示:有 HTML 就渲染 HTML,只有纯文本才显示纯文本,两者都有时给一个"看纯文本 / 看 HTML 排版" 的切换按钮;正文没拉下来时退回列表预览并说明原因。
  • 字符集纠偏(两个引擎共用一条解码链,MimeCodec.jvm.decodeText):声明缺失/不认识时 UTF-8 → GB18030 → ISO-8859-1; 声明是单字节字符集或国标/Big5 这类双字节遗产字符集、而整段字节恰好是合法 UTF-8 时按 UTF-8 解;其它声明解出替换字符 再回退。实测覆盖"声明 us-ascii / latin1 / gb2312 却塞 GBK 或 UTF-8"这几种最常见的错标。 Jakarta 侧正文也改成"读字节 → 同一条解码链"(MimeParsing.readText),不再依赖容器按声明解好的字符串。

16.2 附件:右键预览(按需下载 + 系统默认程序)

问题:附件在列表里只是一行文字,点了没反应;附件字节根本没被保存,想预览也无从下手。

做法:

  • MailGateway 新增 fetchAttachment(account, credentials, folder, uid, attachmentIndex):按需取回单个附件的原始字节。 同步阶段依旧只存元数据(ParsedAttachment.data 只在 keepAttachmentData = true 时填充),不会把整封邮件的附件读进内存。 两个引擎都实现:Jakarta 走 UID 定位 + 同一个 MIME 遍历器;自研栈复用 IMAP fetchMessage + 解析器; POP3(Jakarta)线性扫描定位邮件后同样可取。
  • 平台层新增 expect fun openAttachmentWithSystem(fileName, bytes): Result<String>:落到 <tmp>/mailweave-attachments/<安全文件名>(路径分隔符与 Windows 保留字符会被替换,每次都按当前字节覆写—— 同名附件长度可能一样而内容不同,靠"文件已存在就复用"会把旧内容当成新附件打开),再用系统默认程序打开 (Windows 走 Desktop/cmd start,macOS open,Linux xdg-open)。
  • 界面:附件全部列出(FlowRow 换行),左键或右键弹出菜单 → "预览(用系统默认程序打开)";菜单同时显示大小。 Compose 这版的 detectTapGestures 不带右键回调,右键是自己读 PointerEventType.Press + buttons.isSecondaryPressed 实现的。

16.3 OAuth2:从"只有半个"到两条流程都能用

问题:设置页里 OAuth2 只有一个全局客户端 ID + "开始设备登录",而且: ① 列表页"测试"按钮拿空密码去连(OAuth2 账号必然失败); ② POP3 账号因为"必须填 IMAP 服务器"的校验根本存不下来; ③ Microsoft 365 / Outlook.com 预设不会自动切到 OAuth2,用户白试密码; ④ 最关键的是 Google 根本不可能成功 —— Google 的设备码端点不接受 Gmail 的 IMAP scope(只对白名单 scope 开放), 必须走授权码流程。

做法:

  • 客户端配置(client ID / client secret / 租户)按服务商存进凭据库(VaultData.oauthClients), 不再放在应用设置里当全局字段;一次授权的配置和之后每次刷新用的是同一份,避免"改了设置就去刷新旧 client_id 的令牌" 导致的 invalid_grant。
  • 两条流程:
    • Microsoft:RFC 8628 设备码(浏览器输验证码),public client 不需要密钥;
    • Google:授权码 + PKCE(S256) + 本机回环重定向(127.0.0.1 随机端口,只接 /callback), state 恒定时间比较防伪造回调,code_verifier 只在进程内存里。
  • 令牌按账号存(含 refresh token),取凭据时自动带上、过期前 5 分钟自动刷新(OAuthRefreshingAccountRepository); 服务商优先用"该账号已授权过的那个"(令牌里记着),再回落到域名推断 —— 用户换了服务器地址也不会拿另一家的端点去刷新。
  • 设置页重做:服务商可切换(按域名推断出默认值),客户端 ID / 密钥 / 租户按服务商分别填写并即时落盘; 设备码流程显示验证码并给"取消登录";已授权可"清除授权"。
  • 账号设置的三个缺陷一并修掉:列表"测试"改用仓库里的凭据(含 OAuth2 令牌);POP3 账号按协议校验 POP3 主机; Microsoft 预设直接切到 OAuth2(MailProviderPreset.requiresOAuth2),密码框留空时不再抹掉已存的密码。

16.4 验证

  • gradle :shared:jvmTest :composeApp:jvmTest(本机 JDK 21):168 个用例全绿(shared 153 + composeApp 15;其中 3 个 LiveMailTest 需要真实账号环境变量,按设计跳过)。新增的关键用例:
    • NativeAttachmentTest / JakartaMailGatewayTest / Pop3AndPresetTest:GreenMail 真协议端口上按下标取附件字节、越界返回 null;
    • CharsetFallbackTest / MimeParsingCharsetTest:错标 charset 的中文正文(us-ascii+GBK、latin1+UTF-8、gb2312+UTF-8、UTF-8+GBK)都能正确解码;
    • HtmlMailViewTest:解析器 13 个用例(标题/行内样式/链接/列表嵌套/表格/实体/脚本剔除/cid 占位/畸形 HTML)+ 离屏渲染冒烟;
    • OAuth 侧 48 个用例:设备码全状态、回环 PKCE(RFC 7636 向量)、state 校验、错误分支、超时、vault 往返与刷新。
  • 界面证据(离屏渲染,非截屏):composeApp/build/screenshots/01-mail.png(HTML 正文 + 附件菜单提示)、 06-settings-account.png、08-settings-oauth.png(OAuth2 区块)。
  • 桌面冒烟:gradle :composeApp:run -Pmailweave.smoke=true → MAILWEAVE_SMOKE_OK。

16.5 仍未验证 / 边界

  • 没有真机 OAuth 授权:本机没有可用的 Microsoft/Google 客户端凭据,且 oauth2.googleapis.com 在境内不可达 (实测 curl 超时),所以两条流程都只做到"MockEngine + 真实回环端口"级别的验证;拿到凭据后按 §16.3 走一遍即可。
  • 内联 cid: 图片不显示(降级为占位);HTML 邮件不执行 CSS/JS(安全边界,刻意不做)。
  • 正文里的图片链接不会自动下载(避免读一封邮件就向发件人服务器回连)。

版本历史

  • 0.4.0 —— 三个"用不了"的修复:正文按邮件真实内容展示(HTML 邮件不再被压成纯文本,且声明 charset 与实际字节 不符的中文邮件不再乱码);附件左键/右键菜单用系统默认程序预览(按需下载到临时目录,两个引擎 + POP3 都实现); OAuth2 授权全流程打通(Microsoft 设备码 / Google 回环 + PKCE,令牌自动刷新),并修掉账号设置里"测试连接被 空密码挡住""POP3 账号存不下来""Microsoft 预设不会自动切 OAuth2"等问题。
  • 0.3.0 —— 桌面端实时收信:IMAP IDLE 常驻监听(Jakarta 与自研栈双引擎)+ 新邮件系统通知 + 定时同步兜底;桌面易用性补齐(J/K 移动、Enter 打开、U 已读、S 星标,状态栏实时指示与上次收信时间)。
  • 0.2.0 —— 自研 KMP 原生邮件协议栈第一版(route A):IMAP/SMTP/MIME/modified UTF-7 在 commonMain 自实现, 与 Jakarta 引擎可热切换;真机验证 163(IMAP+SMTP over TLS、ID 命令、中文文件夹名)。
  • 0.1.0 —— 首个可运行版本:桌面端邮件收发(IMAP/POP3/SMTP)、SQLDelight 缓存、AI 分诊/回复/日程/对话、 IM 通知桥接与路由规则、本地提醒、数据目录迁移、桌面 UI。

项目介绍

“Weave”寓意将邮件、AI 智能体、即时通讯平台、自定义钩子编织成一张统一的信息网络。名称简洁、国际化,且与项目“连接一切通讯渠道”的核心理念高度契合。|作者个人站:https://softwarescience.top/

https://softwarescience.top/projects/mailweave定制我的领域