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

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

Branch1Tags4
FilesLast commitLast update
25 days ago
25 days ago
29 days ago
29 days ago
25 days ago
29 days ago
29 days ago
25 days ago
29 days ago
25 days ago
29 days ago
29 days ago
29 days ago

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。

Introduction

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

https://softwarescience.top/projects/mailweaveCustomize your domain