关于 mailindex-mcp
mailindex-mcp 是供 AI Agent 使用的可自托管邮件服务,可在本地索引邮件以供搜索和操作。它也索引附件文本,并报告各账户的搜索覆盖情况。
主要功能
- 01PostgreSQL 全文邮件搜索
- 02索引附件中的文本
- 03移动、标记、删除和起草邮件
- 04按账户报告搜索覆盖率
- 05后台同步邮箱
- 06多租户且数据按所有者隔离
适用场景
- 01查找旧邮件附件中的短语
- 02批量删除匹配的新闻邮件
- 03按搜索结果移动或标记邮件
- 04查看已索引邮件的范围
MCP 相关
它通过可流式 HTTP 向 MCP 客户端提供邮箱管理、邮件搜索和邮件操作工具。
配置
本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。
docker run -i --rm -e EMAILSERVER_DATABASE_URL -e EMAILSERVER_AUTH_MODE -e EMAILSERVER_API_TOKEN -e CREDENTIAL_ENCRYPTION_KEY ghcr.io/alexanderkrauck/mailindex-mcp:0.3.1EMAILSERVER_DATABASE_URLPostgreSQL connection URL. This server keeps its own index and does not run without a database; the repository ships docker-compose files that start one for you.EMAILSERVER_AUTH_MODE密钥How callers are authenticated: development (no auth, loopback only), single_user (static bearer token), or google (multi-user OAuth).EMAILSERVER_API_TOKEN密钥Bearer token required in single_user mode. At least 32 characters, or the server refuses to start.CREDENTIAL_ENCRYPTION_KEY密钥Key used to encrypt stored mailbox credentials at rest. Generate with: openssl rand -base64 32本地服务需要在你的电脑上安装运行环境(如 Node.js 18+)。各客户端的配置文件位置不同,请参考客户端文档。
ghcr.io/alexanderkrauck/mailindex-mcp:0.3.1Streamable HTTP工具
本地服务需要在你的电脑上运行后才能读取工具,维护者暂未提供工具列表。
详细介绍
mailindex-mcp
面向 AI Agent 的邮件客户端。你在 Thunderbird 中能做的一切——搜索、 移动、标记、删除、草拟——都可以通过你自己的索引,在你自己的机器上完成。
大多数邮件 MCP 服务都会把每个问题直接转发给 IMAP。这样可以回答“显示
最近 10 封邮件”,但其他操作就无能为力:不同服务提供商的 SEARCH 表现
不一致,无法查看附件内容,也从不告诉模型是否真的搜索了所有邮件,更无法
对搜索结果采取任何操作。
因此,当你让助手从收件箱中清理八千封新闻邮件时,它会告诉你这需要调用 八千次工具,并建议你到网页界面中手动操作。
这个服务会保留一份邮件副本,并据此执行操作:
delete_mail(account_id=3, participants=["[email protected]", ...])
→ matched: 4147, affected: 4147, to: "[Google Mail]/Bin"
一次调用。一个连接。六秒。这就是搜索框与邮件客户端的区别。

针对包含约 59,000 封邮件的实时索引提出的真实问题,邮件来自六个 账户。其中一段字符串已模糊处理:它属于真实诉讼案件的案号。
为什么选择它,而不是其他邮件 MCP 服务
| 本项目 | 典型邮件 MCP | |
|---|---|---|
| 能否修改内容 | 可批量移动、标记、删除、草拟邮件以及管理文件夹,并支持搜索 | 只读,或每次调用只能处理一封邮件 |
| 搜索 | 自有 PostgreSQL 索引,使用 GIN 全文搜索,支持跨语言词干提取 | 每次调用都实时执行 IMAP SEARCH |
| 结果完整性 | 精确的 total_count、签名游标、逐账户覆盖情况 | 文件夹返回的内容 |
| 附件 | 同步时提取并索引文本(PDF、DOCX、XLSX、PPTX、OCR) | 将 Base64 内容放入模型上下文,或完全不处理 |
| 附件二进制文件 | 从不存储;通过有效期为 5 分钟的签名 URL 重新获取 | 存储在磁盘上或直接内嵌 |
| 传输方式 | 远程 HTTP 端点 | 运行在你机器上的本地 stdio 进程 |
| 客户端上的设置 | 粘贴 URL | 安装运行时、编辑配置 JSON、本地存储凭据 |
| 用户 | 多租户,每一行都按所有者隔离 | 单用户 |
| 工具数量 | 22 个工具,全部带注释 | 通常超过 40 个 |
**先说明代价。**你需要运行 PostgreSQL 和一个容器。索引大小约为每 1,000 封邮件 28 MB——一个包含 50,000 封邮件的存档约为 1.5 GB——镜像 大小为 1.25 GB,因为其中包含 OCR 语言数据。首次同步会下载每封邮件一次; 收到的邮件可以立即用于搜索,其余邮件则在后台继续同步。每个账户都会报告 自身的覆盖情况,让模型知道还有哪些内容尚未查看。
作为回报,你的助手可以回答多年前邮件中的问题,包括附件中的文本,并据此 采取行动。所有内容都不会离开你的机器。
快速入门
五分钟,仅限本地,无需在任何地方创建账户。
git clone https://github.com/alexanderkrauck/mailindex-mcp.git
cd mailindex-mcp
cp .env.example .env
docker compose up -d
这里的每个 compose 文件都会拉取
ghcr.io/alexanderkrauck/mailindex-mcp
——支持 amd64 和 arm64,因此 Raspberry Pi 和 Apple Silicon 设备的用法相同。
在任意文件后追加 --build,即可自行编译;预计需要几分钟,因为镜像包含 OCR 语言数据。
固定版本,用于任何重要场景。这是 1.0 之前的版本:不同版本之间的架构会发生变化,迁移会在启动时自动运行,因此未固定版本的 latest 可能会在你重启时迁移数据库。
MAILINDEX_IMAGE=ghcr.io/alexanderkrauck/mailindex-mcp:0.1.0 docker compose up -d
MAILINDEX_IMAGE 适用于全部三个 compose 文件,应放在 .env 中,而不是命令行中。注意,镜像标签没有 v——git 标签是 v0.1.0,镜像标签是 0.1.0,而 0.1 会跟随该次要版本的最新补丁版本。
检查服务是否已启动:
curl http://localhost:8002/api/v1/health
默认的 development 模式未经身份验证,且 Docker 仅绑定到 127.0.0.1。它正是为这种情况设计的:在自己的机器上试用。
连接 AI 客户端
连接邮箱前先完成这一步——这是连接邮箱的方式。
claude mcp add --transport http mail http://localhost:8002/mcp
对于其他客户端,请通过可流式传输的 HTTP 将其指向 http://localhost:8002/mcp。然后提出一个收件箱搜索难以应对的问题——例如搜索三年前别人发给你的某个 PDF 中的一句话。
连接邮箱
直接向客户端提出请求即可——add_mail_account 是其中一个工具:
连接我的邮箱 [email protected],IMAP imap.example.com,SMTP smtp.example.com
不要把密码告诉它。 不提供密码时,它会返回一个短时有效的网址,指向一个只要求输入密码的表单,并从你的浏览器直接将密码发送到服务端。密码不会经过模型,也不会出现在 AI 提供商保存的对话记录中;对于拒绝传输机密信息的 ChatGPT 等客户端,这是唯一可行的方式。
请使用提供商安全设置中的应用专用密码,绝不要使用账户登录密码。对于 Gmail,请让它改为启动 Gmail OAuth 流程:该流程使用 Gmail API,在标签发生变化后也更可靠。Microsoft 365 和 Exchange Online 完全不接受密码,因此请让它连接 Microsoft 邮箱:你将在 Microsoft 登录并批准服务端可以执行的操作,方式与 Gmail 完全相同(设置)。
或者,如果你更喜欢使用 shell,也可以通过 HTTP 操作
curl -X POST http://localhost:8002/api/v1/accounts \
-H 'Content-Type: application/json' \
-d '{
"name": "personal",
"account_name": "[email protected]",
"username": "[email protected]",
"password": "your-app-password",
"host": "imap.example.com",
"port": 993,
"smtp_host": "smtp.example.com",
"smtp_port": 465
}'
同步会自动启动并在后台运行。搜索会基于已收到的邮件执行——询问**“到目前为止,我有多少邮件已编入索引?”**,它会按账户准确告知,因为每次搜索都会报告自身的覆盖范围,而不会假装已完成全部索引。
我需要哪种设置?
人们会通过四种方式来到这里,以下是每种情况最直接且诚实的做法。
“我想看看这是否靠谱”——5 分钟,用你的笔记本电脑
所有内容都在本地运行,无需注册任何服务。
git clone https://github.com/alexanderkrauck/mailindex-mcp.git
cd mailindex-mcp && cp .env.example .env
docker compose up -d # pulls the published image
claude mcp add --transport http mail http://localhost:8002/mcp
使用应用专用密码添加邮箱(见下文),等待同步完成,然后让客户端搜索一个收件箱搜索难以找到的内容。你需要: Docker,以及提供商提供的应用专用密码。**预期情况:**首次构建会编译 psycopg2 并拉取约 130 MB 的 OCR 语言数据,因此需要几分钟,而非几秒。身份验证处于关闭状态,且 Docker 仅绑定到 127.0.0.1——在这里这样没问题,但绝不要将其暴露出去。
“我想每天在手机和笔记本电脑上使用它”——一个人,一个服务
你需要一台小型 VPS 和一个域名。Caddy 会为你获取 TLS 证书。
cat > .env <<'ENV'
EMAILSERVER_DOMAIN=mail.example.com
POSTGRES_PASSWORD=...
EMAILSERVER_API_TOKEN=...
CREDENTIAL_ENCRYPTION_KEY=...
SESSION_SECRET=...
ENV
docker compose -f docker-compose.single-user.yml up -d
每个值都来自 openssl rand -base64 32;API token 必须至少有 32 个
字符,否则服务将拒绝启动。**你需要:**一台约有 2 GB
RAM、磁盘空间可按每 1,000 条消息约 25 MB 计算的 VPS,一个域名,以及每个邮箱对应的
应用密码。**预期效果:**可与任何发送
Authorization 标头的工具配合使用——Claude Code、Cursor、mcp-remote。它无法与 claude.ai 或 ChatGPT 网页连接器配合使用,因为这些连接器会协商 OAuth,无法
发送静态 token。如果你需要这些连接器,请使用下一个方案。
“我希望家人/团队成员各自拥有自己的账户”——几个人
使用 Google OAuth,这样每个人都以自己的身份登录,只能看到自己的邮箱。
docker compose -f docker-compose.production.yml up -d
**你需要:**以上所有内容,外加一个 Google Cloud OAuth Web application,其中包含部署模式下列出的两个重定向 URI。**预期效果:**在 提交同意屏幕之前会出现未验证应用警告,且未验证期间用户上限为 100 人——对于这种规模,两者都不成问题。
**将
REGISTRATION_MODE=allowlist。**如果设为open,地球上任何 Google 账户都可以在 你的服务上注册并关联邮箱,而且系统不会读取ALLOWED_GOOGLE_EMAILS。租户隔离仍会阻止陌生人访问你的邮件,但他们可以在你的服务器上创建账户。这是在公网上最容易出错的地方。
“能否将它作为内部基础设施?”——一家有 500 人的公司
坦白说:还不行。以下是确切的缺失项,而不是模棱两可的说法。
**目前可用的功能。**使用上述 Google 配置,让一个团队先进行试点。多租户是真正实现并在查询层强制执行的——每一行数据都限定了所有者,所有权来自经过身份验证的 token,而不是调用方提供的任何内容。邮箱凭据在静态存储时会加密,且绝不会返回。附件二进制文件不会被存储。
阻碍全公司推广的问题。
| 缺口 | 重要性 |
|---|---|
| 登录仅支持 Google OIDC | 不支持 Entra ID、Okta、通用 OIDC 或 SAML。如果你的目录服务不是 Google,就没人能登录。 |
| 没有配置或取消配置机制 | 用户自行注册;没有 SCIM、没有组映射,也无法在有人离职时撤销其索引。 |
| 没有审计导出 | 发送操作会被审计;读取、搜索和邮箱写入操作不会被审计,而大多数合规审查都会要求审计这些操作。 |
| 单个 Postgres,单个容器 | 没有高可用、没有只读副本、没有横向扩展的同步工作进程。备份和恢复都靠你的 pg_dump。 |
| 附件文本存放在数据库中 | 保留期限和法律保全机制都由你自行实现。 |
如果你正是需要它的人:这个试点确实值得开展,而诚实的内部说法应该是“一个构建在自有硬件上的自有邮件搜索索引,助手可以使用”,而不是“一个获批的平台”。
无论选择哪种配置,邮箱凭据都需要单独设置
登录此服务本身不会授予任何访问权限。每个邮箱都需要单独连接:
- Gmail — 可以使用通过 IMAP 的应用密码,也可以通过
begin_gmail_connection使用 Gmail OAuth 流程;后者改用 Gmail API,对标签变更的适应能力更强。 - Microsoft 365 / Exchange Online — 仅支持 OAuth,通过
begin_microsoft_connection完成。 Microsoft 不接受这些邮箱的密码或应用密码。 请参阅连接 Microsoft 365。 - 其他所有邮箱 — 使用邮箱服务提供商安全设置中的应用密码。 切勿使用你的登录密码。
连接 Microsoft 365 / Exchange Online
连接 Microsoft 邮箱的方式与 Gmail 相同:你在 Microsoft 登录并批准此服务可以执行的操作,服务只会保留由此生成的刷新令牌,并对其加密。每个服务只需注册一个应用,注册一次即可。
1. 注册应用:在 Microsoft Entra 管理中心中,依次选择 应用注册 → 新注册:
- 支持的帐户类型:选择 任何组织目录中的帐户和个人 Microsoft 帐户,这样任何 Microsoft 365 邮箱都可以连接。如果只允许你自己的组织连接,请选择 仅此组织目录,并将
MICROSOFT_TENANT设置为下方的租户 ID;此类应用不接受common。 - 重定向 URI,平台选择 Web:
https://mail.example.com/api/v1/accounts/microsoft/callback(本地试用时,可添加http://localhost:8002/api/v1/accounts/microsoft/callback。)
2. 添加以下委派权限:依次选择 API 权限 → 添加权限:
| API | 权限 | 用途 |
|---|---|---|
| Office 365 Exchange Online(位于 我的组织使用的 API 下) | IMAP.AccessAsUser.All | 通过 IMAP 读取、搜索、移动和标记邮件 |
| Microsoft Graph | Mail.Send | 发送邮件 |
| Microsoft Graph | User.Read、openid、profile、email、offline_access | 确认登录的邮箱,并保持登录状态 |
用户能否自行批准这些权限,取决于租户的 用户许可 设置。如果此设置有限制,连接页面会提示需要管理员先批准此应用;在此页面选择 授予管理员同意,即可为所有人批准。
3. 创建客户端密钥:在 证书和机密 下创建。立即复制其 值;Microsoft 只会显示一次。密钥会过期(最长 24 个月):请将到期日期记入日历。更新环境变量中的密钥并重启即可续期,因为存储的凭据不包含该密钥,也无需重新连接任何邮箱。
4. 将应用注册信息提供给服务,然后重启服务:
MICROSOFT_CLIENT_ID=<Application (client) ID>
MICROSOFT_CLIENT_SECRET=<the secret's Value>
MICROSOFT_TENANT=common # or your tenant ID
(docker-compose.yml 会读取带有 EMAILSERVER_ 前缀的相同三个变量。)
5. 连接邮箱。 请你的助手连接 Microsoft 邮箱,打开它返回的链接,选择帐户并批准。后续页面会说明邮箱是否可用;如果不可用,也会说明原因。
须知:
- 邮件通过 Microsoft Graph 发送,而不是 SMTP。 Exchange Online 默认会为整个组织关闭 SMTP AUTH,是否开启是该组织管理员的安全决策。Graph 只需获得用户本人的同意。Exchange 会自行将邮件存入 已发送邮件,并保留其
Message-ID,因此已发送邮件绝不会被重复编入索引。回复会按会话归并,其他人看不到Bcc收件人。 - Exchange 为每封邮件只保留一个正文。 同时发送文本和 HTML 时,收到的会是 HTML。搜索不受影响:索引会从中提取文本。
- Exchange 会将日历、联系人、任务、笔记和日志列为邮件文件夹。对于使用英语、德语、法语、西班牙语、意大利语、荷兰语和葡萄牙语的邮箱,这些文件夹不会同步,也不会出现在
list_mail_folders中。对于其他语言,服务会在日志中说明,并为所有文件夹编制索引;请在EMAILSERVER_EXCLUDED_SYNC_FOLDERS='["Kalendarz","Kontakty"]'中列出要跳过的文件夹。 - 每个邮箱都可以单独关闭 IMAP。 此时连接页面会说明这一点;管理员可通过
Set-CASMailbox -Identity [email protected] -ImapEnabled $true将其开启。 - 撤销。 在此处删除邮箱会移除存储的令牌,但不会在 Microsoft 撤销该令牌。若要撤销,请在 Entra 的 企业应用程序 下移除该应用的许可。
- 个人 Outlook.com 帐户使用相同的流程,但目前只在 Exchange Online 工作和学校邮箱上验证过。
部署模式
EMAILSERVER_AUTH_MODE 用于选择调用方的身份验证方式。邮箱凭据始终与此分开。
| 模式 | 谁可以调用 | 所需条件 | 适用于 |
|---|---|---|---|
development | 所有本机回环地址上的用户 | 无 | 仅限本地客户端 |
single_user | 持有静态 bearer 令牌的单个所有者 | 一个令牌、一个域名 | Claude Code、Cursor、mcp-remote,以及任何会发送标头的客户端 |
google | 通过 Google OAuth + 动态客户端注册的多个用户 | 一个 Google Cloud OAuth 客户端、一个域名 | Claude.ai 和 ChatGPT 网页连接器,以及上述所有客户端 |
生产环境,单用户,无需 Google 项目
部署真实服务的最短路径。Caddy 会获取并续期 TLS 证书。
cat > .env <<'ENV'
EMAILSERVER_DOMAIN=mail.example.com
POSTGRES_PASSWORD=...
EMAILSERVER_API_TOKEN=...
CREDENTIAL_ENCRYPTION_KEY=...
SESSION_SECRET=...
ENV
docker compose -f docker-compose.single-user.yml up -d
使用 openssl rand -base64 32 生成每个密钥。EMAILSERVER_API_TOKEN 必须至少包含 32 个字符,否则服务器会拒绝启动。
claude mcp add --transport http mail https://mail.example.com/mcp \
--header "Authorization: Bearer $EMAILSERVER_API_TOKEN"
生产环境,多用户,Google OAuth
如果要使用 Claude.ai 或 ChatGPT 网页连接器,则需要此模式。这些连接器会协商 OAuth,无法发送静态标头。
创建一个 Google OAuth Web 应用并配置:
- 授权的 JavaScript 来源:
https://mail.example.com - 授权的重定向 URI:
https://mail.example.com/auth/callbackhttps://mail.example.com/api/v1/accounts/gmail/callback
不要将 AI 供应商的回调地址添加到 Google 客户端。MCP 服务自身会允许 https://claude.ai/api/mcp/auth_callback 和 https://chatgpt.com/connector/oauth/*,然后将完成的授权重定向回客户端。
cat > .env <<'ENV'
EMAILSERVER_DOMAIN=mail.example.com
POSTGRES_PASSWORD=...
GOOGLE_CLIENT_ID=123.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=...
JWT_SIGNING_KEY=...
CREDENTIAL_ENCRYPTION_KEY=...
SESSION_SECRET=...
REGISTRATION_MODE=allowlist
ALLOWED_GOOGLE_EMAILS=["[email protected]"]
ENV
docker compose -f docker-compose.production.yml up -d
在客户端中添加 https://mail.example.com/mcp。客户端会收到 OAuth 挑战,发现授权元数据,注册自己的回调地址,并引导用户通过 Google 完成授权。
Google 有两点需要注意:提交同意屏幕以供审核之前,会显示未验证应用警告;应用未验证期间,用户上限为 100 人。个人或家庭部署不受这两点影响。
升级现有的单所有者安装:将 CLAIM_LEGACY_ACCOUNTS_ON_FIRST_LOGIN=true 设为 true,仅将预期所有者加入允许名单,让该用户登录一次以认领现有账户,然后将其设回 false。
安全模型
- Google OpenID Connect 通过稳定的
sub声明识别应用用户。 - 每位用户拥有多个 Gmail、Microsoft 365、Zoho 或通用 IMAP/SMTP 账户。
- 邮箱凭据与登录身份分开,并以加密形式静态存储。
- MCP 根据经过身份验证的令牌确定所有权;调用方绝不会提供所有者 ID。
- 邮箱密码只能作为工具和 API 的写入输入,绝不会返回。
- 不存储原始附件文件。需要时会通过签名 URL 重新获取附件。
- 开发模式无需身份验证,且必须绑定到本机回环地址。
登录并不会授予对任何邮箱的访问权限。Gmail 和 Microsoft 365 通过单独的 OAuth 授权流程连接;Zoho 和通用 IMAP 使用提供商的应用密码。Microsoft 邮箱存储的凭据只有其刷新令牌:应用注册的密钥保留在服务器的环境中。
MCP 工具
共 22 个工具,每个工具都标注了只读、破坏性和开放世界提示。这是一个邮件客户端,而不是搜索框:你在 Thunderbird 中能做的一切,都可以在这里通过索引(而不是文件夹列表)完成。
| 工具 | |
|---|---|
list_mail_accounts | 账户、非机密设置、存储的准确邮件数量 |
add_mail_account | 添加 IMAP/SMTP 邮箱 |
update_mail_account | 更改一个邮箱的设置 |
delete_mail_account | 删除账户及从中索引的所有内容;不会删除邮件服务提供商上的任何内容 |
begin_mail_account_password_setup | 短时有效、仅用于输入密码的浏览器表单 |
begin_gmail_connection | 用于 Google 授权的五分钟有效签名 URL |
begin_microsoft_connection | 用于 Microsoft 授权的五分钟有效签名 URL |
search_mail | 全面词法搜索 |
search_mail_regex | 有界正则表达式搜索 |
get_mail | 获取一封邮件及有长度限制的正文 |
get_thread | 获取重建的邮件线程及置信度 |
get_attachment | 获取元数据、提取的文本和会过期的下载 URL |
begin_attachment_upload | 为新文件内容创建签名上传位置,可作为附件或内嵌内容附加 |
send_mail | 发送邮件或回复邮件,并附上自有附件 |
list_mail_folders | 列出一个邮箱的文件夹,以及声明的角色和已索引数量 |
create_mail_folder | 创建并订阅文件夹 |
rename_mail_folder | 重命名文件夹,并保留其中的邮件和子文件夹 |
delete_mail_folder | 删除文件夹,先将其内容清空到“已删除邮件” |
mark_mail | 批量设置或清除已读和已标记状态 |
move_mail | 批量将邮件移至其他文件夹 |
delete_mail | 批量移至“已删除邮件”;仅当邮件已在“已删除邮件”中时,permanent 才生效 |
save_draft | 将草稿写入邮箱的“草稿”文件夹 |
独立的连接测试和手动同步特意不纳入 MCP 接口。
批量操作。 mark_mail、move_mail 和 delete_mail 采用与
search_mail 相同的方式选取邮件——传入 email_ids,或传入相同的筛选条件,
以对所有匹配项执行操作。一次调用会打开一个连接,并对每个文件夹发送一条命令,因此
清理 8,000 封新闻邮件只需一次调用,而不是 8,000 次。每个响应都会报告
matched 和 affected,并在达到限制导致操作提前停止时设置 truncated,
因此不会把部分批次误认为已完成。既未传入 ID 也未传入筛选条件的调用会被拒绝,
而不会被视为针对“整个邮箱”的操作。
Gmail。 Gmail 使用标签而非文件夹,因此会根据优先级将标签映射为单一位置——
TRASH > SPAM > DRAFT > INBOX > SENT;如果邮件不带这些标签中的任何一个,
则映射为 ARCHIVE。这一映射使文件夹范围搜索、排除“已删除邮件”和写入操作在不同服务提供商之间
保持一致:Gmail 邮件通过服务提供商 ID(而非 UID)定位,移动邮件意味着添加一个标签,
并移除原有标签。SENT 和 DRAFT 由 Gmail 指定,永远不会被移除。在 Gmail 中,
创建、重命名和删除文件夹的操作会被拒绝,并说明原因。
写入操作。 每项写入操作都会先写入邮箱,只有服务器确认后才会记录到本地,
因此索引不会声称发生了实际上并未发生的更改。写入操作会持有与同步器相同的租约,
租约依据 (host, port, username) 而非账户行进行标识,因为两个账户可能指向同一个
实际邮箱;此外,在文件夹清点过程中到达的未标记 EXPUNGE 会重新编号该清点正在读取的序列号。
写入操作针对邮件的当前副本执行,而不是针对位于“已删除邮件”中的副本。
delete_mail 会将邮件移至“已删除邮件”;要彻底删除任何内容,必须再次显式调用。
密码。 add_mail_account 和 update_mail_account 可选接收一个仅用于写入的密码。
如果客户端不会传输机密信息,可省略密码并打开返回的设置 URL,或调用
begin_mail_account_password_setup;关联表单只会询问密码。连接测试失败时,
配置和加密后的凭据都会保留,因此可以更正设置而无需重新输入密码。
移除账户。 delete_mail_account 需要调用两次。第一次只传入
account_id,会报告将要删除的内容;第二次执行删除,且只有当
confirm_name 与账户名称完全一致时才会执行——必须是名称,而不是邮箱
地址,因为旧密码账户和较新的 OAuth 连接可能共用一个邮箱。删除后会清除:
已存储的凭据、设置、所有已索引邮件及其提取出的附件文本,以及同步状态。
服务商端不会发生任何变化,因此之后可以重新连接邮箱,并从头开始索引;
撤销在服务商处授予的访问权限(Google 账户权限、Microsoft My Apps)仍需
由所有者操作。通过该账户发送的邮件记录会保留,并与账户解除关联。若有
同步步骤正在向账户写入,则会等待该步骤完成(最多 30 秒),并阻止该次
同步的其余操作继续进行。如果某个步骤仍在写入,或已记录发送声明的账户
发信操作尚未完成,响应将为 CONFLICT 并带有 retry_after,且不会删除
任何内容。若删除操作先于某次发送完成,则该次发送会失败,不会发送任何
内容。DELETE /api/v1/accounts/{id} 则有所不同:它会停用账户并保留其数据。
搜索。 search_mail 和 search_mail_regex 会返回 total_count、
raw_count、returned_count、has_more 和经过签名的 next_cursor。要
获取完整结果,请使用相同的筛选条件重复查询并传入 next_cursor,直到
has_more 为 false。默认去重模式为 exact,它会将 RFC Message-ID
值相同的邮件归为一组,同时保留每个来源账户和邮件 ID;mirror 还会将
规范化后的正文副本归为一组;none 则返回每一条已存储记录。响应还会
报告匹配字段、参与者域名分面以及各账户的同步覆盖情况。
词干提取。 match 默认值为 stemmed,可匹配单词的词形变化:在一个
真实的、含有 52,000 封邮件的邮箱中,invoices 的命中数从 104 增至
1,016,Verträge 从 133 增至 1,168。代价是精确度降低——meeting 也会
匹配 meet——因此,搜索订单号、标识符和姓氏时,请传入 match="exact",
避免词干提取扩大匹配范围。两种模式都使用索引;每个响应都会报告实际使用
的模式。
已读状态。 is_unread、is_flagged 和 is_answered 会根据从服务商
同步的标志进行筛选。这些标志有三种状态:如果服务商从未报告某封邮件的
标志,该邮件既不匹配 true,也不匹配 false;搜索会返回
FLAG_STATE_UNKNOWN 警告,说明因此排除了多少封邮件,而不是悄然将它们
报告为已读。Gmail 不提供已回答标签,因此,对于 Gmail OAuth 账户,
is_answered 始终为未知。标志由定期协调器刷新,因此最多会比邮箱状态
落后 EMAILSERVER_DELETION_RECONCILE_INTERVAL。
get_mail 和 get_thread 默认返回有长度限制的纯文本;如需 HTML,必须
明确请求。send_mail 接受归该账户所有的 attachment_ids,并会在发送前
从服务商重新获取每个附件的原始二进制文件。
HTTP API
GET /api/v1/docs 提供 OpenAPI 浏览器。所有账户、邮件、附件、同步和发送
查询都限定在所有者范围内。
GET /api/v1/me
GET /api/v1/accounts
POST /api/v1/accounts
GET /api/v1/accounts/{id}
PATCH /api/v1/accounts/{id}
DELETE /api/v1/accounts/{id}
POST /api/v1/accounts/{id}/test
POST /api/v1/accounts/{id}/sync
GET /api/v1/accounts/gmail/connect
GET /api/v1/accounts/microsoft/connect
GET /api/v1/emails/search
GET /api/v1/emails/search/regex
GET /api/v1/emails/{id}
GET /api/v1/attachments/{id}
POST /api/v1/send
同步的工作原理
让搜索值得信赖,而非尽力而为的关键部分:
-
Alembic 在启动时应用带版本控制且保留数据的迁移。
-
邮件身份由规范化后的 RFC
Message-ID确定,该 ID 会随邮件一起传递。邮件的位置单独保存在message_placements中,每个文件夹一行,因此在上游移动邮件时,只会改变其位置,不会删除后重新创建。Gmail API 邮件保留服务商自己的 ID,该 ID 在标签变更后仍然有效。 -
IMAP 游标会持久化 UID、UIDVALIDITY 和文件夹。回填按从旧到新的顺序运行,并且每个周期都有数量上限。只有在批次提交后,游标才会前进;UIDVALIDITY 发生变化时,只会重置对应文件夹。
-
Gmail OAuth 账户使用
messages.list/get进行可续传回填,并使用history.list跟踪增量变更。历史 ID 过期时,会先执行带有代次标记的完整同步,再协调处理上游删除。 -
Microsoft 365 账户像其他 IMAP 账户一样,使用 OAuth 令牌通过 IMAP 同步,并通过 Microsoft Graph 发送邮件。Sent Items 中的邮件副本与投递到 INBOX 的邮件具有相同的
Message-ID,但 Exchange 对它们的 MIME 排列方式不同;因此,附件会根据校验和而不是位置重新查找。 -
定期进行仅针对元数据的协调,以同步标记和上游删除,并使用持久化检查点,确保重启后无需完整重新扫描。
-
账户任务受全局并发限制约束,并由具有过期时间的数据库租约保护,因此两个工作进程不会同时同步同一个邮箱,崩溃的工作进程也不会永久持有锁。
-
PostgreSQL GIN 索引支持邮件正文和附件的词法搜索。相同文本会按配置的每种语言各索引一次,并存储为一个组合向量,因此同一邮箱中的德语发票和英语新闻简报都能正确进行词干提取;相同词素会合并,因此它的开销大约与旁边的未提取词干索引相当。
EMAILSERVER_SEARCH_TEXT_CONFIGS用于选择语言,默认值为["simple", "english", "german"]。更改此项需要匹配的索引,而迁移只会构建一次:CREATE INDEX CONCURRENTLY ix_email_logs_search_fts_simple_english_french ON email_logs USING gin (( to_tsvector('simple', coalesce(sender,'')||' '||coalesce(recipient,'')||' '||coalesce(subject,'')||' '||coalesce(body_plain,'')) || to_tsvector('english', coalesce(sender,'')||' '||coalesce(recipient,'')||' '||coalesce(subject,'')||' '||coalesce(body_plain,'')) || to_tsvector('french', coalesce(sender,'')||' '||coalesce(recipient,'')||' '||coalesce(subject,'')||' '||coalesce(body_plain,'')) ));如果没有该索引,搜索仍会返回相同结果,但需要扫描所有已存储的正文。
-
同步时会将服务商标记规范化为
is_unread、is_flagged和is_answered。IMAP 报告邮件已读,而 Gmail API 报告邮件未读,且两者的编码方式不同;这两种形式都无法直接用于筛选。如果服务商从未报告邮件标记,其值会保持为 null,而不会默认为已读。 -
同步时会提取附件文本。图像附件会使用图像中安装的所有语言进行 OCR,因为 tesseract 未指定语言时会默认使用英语,并错误处理带重音符号的文字:德语变音符号会被识别为
dirfen Fuboden,而不是dürfen Fußboden,因此文本虽然会被索引,但搜索页面上的词语时永远无法找到。镜像包含eng deu fra ita spa nld por;可通过docker build --build-arg TESSERACT_LANGS="eng deu jpn"添加或删减语言,也可在运行时通过EMAILSERVER_OCR_LANGUAGES=deu+eng固定语言子集,以覆盖范围换取速度。 -
Date标头即使能解析,若其值不合理(例如年份为 2611),也会被丢弃而非存储,因为搜索会按该列排序和分页。Gmail 会回退到服务商时间戳;IMAP 则将其留空。scripts/repair_email_dates.py会清除此检查实施前写入的值。 -
正则表达式搜索与其他搜索分开,并受范围、模式、结果数量和语句执行时间限制。
-
发送支持幂等键,并会追加一条限定所有者范围的审计记录。
开发
测试套件独立运行。它使用内存中的 SQLite 和临时数据目录,因此无需 PostgreSQL、Docker 或网络连接。
python -m venv .venv && . .venv/bin/activate
pip install -r requirements.txt
pytest -q
ruff check .
它涵盖租户隔离、工具公开与注解、静态令牌和 OAuth 身份验证、加密、令牌篡改、附件限制、IMAP 游标行为、Gmail 历史记录行为以及 OAuth 挑战元数据。
scripts/verify_postgres_search.py 则是另一回事:它是一个只读冒烟测试,会在已填充数据的部署上运行,以确认全面搜索、去重、分面和正则表达式在真实数据上的表现。
docker compose exec email-server python -m scripts.verify_postgres_search
许可证
MIT。请参阅 LICENSE。
贡献和安全
- CONTRIBUTING.md — 如何运行本项目,以及本项目有哪些在代码审查中值得与你争论的关注点。
- SECURITY.md — 哪些内容受到保护、哪些没有,以及在哪里私下报告漏洞。
- CHANGELOG.md — 各版本之间的变更。
评论
一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。