mcpField
M
mailindex-mcp

mailindex-mcp

本地服务
github.com通信协作02 周前更新

关于 mailindex-mcp

mailindex-mcp 是供 AI Agent 使用的可自托管邮件服务,可在本地索引邮件以供搜索和操作。它也索引附件文本,并报告各账户的搜索覆盖情况。

主要功能

  1. 01PostgreSQL 全文邮件搜索
  2. 02索引附件中的文本
  3. 03移动、标记、删除和起草邮件
  4. 04按账户报告搜索覆盖率
  5. 05后台同步邮箱
  6. 06多租户且数据按所有者隔离

适用场景

  1. 01查找旧邮件附件中的短语
  2. 02批量删除匹配的新闻邮件
  3. 03按搜索结果移动或标记邮件
  4. 04查看已索引邮件的范围

MCP 相关

它通过可流式 HTTP 向 MCP 客户端提供邮箱管理、邮件搜索和邮件操作工具。

配置

本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。

mcp.json · 通用配置
{
"mcpServers": {
"alexanderkrauck-mailindex": {
"command": "docker",
"args": [
"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.1"
],
"env": {
"EMAILSERVER_DATABASE_URL": "<EMAILSERVER_DATABASE_URL>",
"EMAILSERVER_AUTH_MODE": "<EMAILSERVER_AUTH_MODE>",
"EMAILSERVER_API_TOKEN": "<EMAILSERVER_API_TOKEN>",
"CREDENTIAL_ENCRYPTION_KEY": "<CREDENTIAL_ENCRYPTION_KEY>"
}
}
}
}
启动命令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.1
需要配置的环境变量
EMAILSERVER_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+)。各客户端的配置文件位置不同,请参考客户端文档。

安装包
ocighcr.io/alexanderkrauck/mailindex-mcp:0.3.1

工具

还没有获取到工具列表

本地服务需要在你的电脑上运行后才能读取工具,维护者暂未提供工具列表。

详细介绍

mailindex-mcp

release image CI licence: MIT MCP

面向 AI Agent 的邮件客户端。你在 Thunderbird 中能做的一切——搜索、 移动、标记、删除、草拟——都可以通过你自己的索引,在你自己的机器上完成。

大多数邮件 MCP 服务都会把每个问题直接转发给 IMAP。这样可以回答“显示 最近 10 封邮件”,但其他操作就无能为力:不同服务提供商的 SEARCH 表现 不一致,无法查看附件内容,也从不告诉模型是否真的搜索了所有邮件,更无法 对搜索结果采取任何操作。

因此,当你让助手从收件箱中清理八千封新闻邮件时,它会告诉你这需要调用 八千次工具,并建议你到网页界面中手动操作。

这个服务会保留一份邮件副本,并据此执行操作:

delete_mail(account_id=3, participants=["[email protected]", ...])
→ matched: 4147, affected: 4147, to: "[Google Mail]/Bin"

一次调用。一个连接。六秒。这就是搜索框与邮件客户端的区别。

Claude 通过此服务回答有关邮箱的问题

针对包含约 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 GraphMail.Send发送邮件
Microsoft GraphUser.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/callback
    • https://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 — 各版本之间的变更。

评论

支持 Markdown

第一次接触 MCP?

从协议是什么,到在客户端里装上第一个服务,这几条回答覆盖了大多数入门问题。

一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。