# MailHub

MailHub 是一个 Docker 化的多用户邮件控制面板、SMTP Submission 服务和收发 API。它面向需要自托管邮件能力的团队，提供域名验证、DKIM 签名、DNS 配置提示、邮箱账号、SMTP 凭据和 API Token 管理。

## 功能

- 多用户账号体系，隔离域名、SMTP 凭据、DNS API 凭据、API Token 和发送记录。
- 为发信域名生成验证 TXT、DKIM、SPF、DMARC、发信主机 A 记录和 PTR 检查提示。
- 支持 Cloudflare、阿里云 DNS、腾讯云 DNSPod API 的基础 DNS 写入。
- 检查公网 DNS 中的 SPF、DKIM、DMARC、PTR 和发信主机 A 记录状态。
- 通过内部 Postfix 出站队列发送邮件，并按发件域名添加 DKIM 签名。
- 提供 SMTP Submission 和 HTTP 发送 API。
- 通过 Dovecot 提供完整 IMAP/POP3 客户端兼容，Maildir 作为邮件真源。
- 内置 React + Ant Design 管理界面。

## 技术栈

- Node.js ESM，要求 Node.js `>=24.0.0`
- SQLite 持久化
- React、Vite、Ant Design
- Docker Compose + Postfix + Dovecot
- Node 内置 `node:test`

## 快速开始

```bash
cp .env.example .env
npm install
npm test
npm run build
npm run prepare:dovecot
docker compose up -d --build
docker compose logs -f app postfix dovecot
```

默认管理面板通过 `APP_PORT` 暴露到宿主机 `127.0.0.1:3025`。生产环境建议使用 Nginx、Caddy 或其他反向代理提供 HTTPS。

首次启动前务必修改 `.env` 中的默认管理员密码、`SESSION_SECRET`、SMTP 凭据和域名/IP 配置。

## 配置

以 `.env.example` 为模板创建 `.env`。常用配置包括：

- `APP_BASE_URL`：管理面板和发送 API 的公网访问地址。
- `MAIL_HOSTNAME`：出站邮件 HELO、Postfix `myhostname` 和 DNS 指南中的发信主机名。
- `SENDING_IP`：发信服务器公网 IP。
- `SESSION_SECRET`：会话和服务端加密使用的随机密钥，生产环境必须使用强随机值。
- `SUBMISSION_HOST`、`SUBMISSION_PORTS`：SMTP Submission 对外连接信息。
- `SUBMISSION_TLS_CERT`、`SUBMISSION_TLS_KEY`：TLS 证书路径。证书文件应放在本地 `certs/`，不要提交到 Git。
- `MAIL_ACCESS_BACKEND`：Docker 部署使用 `dovecot`；仅本地协议回归时可使用 `legacy`。
- `MAILDIR_ROOT`、`MAILDIR_SYNC_INTERVAL_MS`：Maildir 持久化目录及管理界面索引同步周期；默认 `300000` 毫秒（5 分钟），可按邮件量调整。
- `DEFAULT_SPF_MECHANISMS`：需要保留的第三方 SPF include，例如事务邮件服务商。
- `SEND_REQUIRES_VERIFIED`：是否要求域名 DNS 验证通过后才能发信。
- `LIST_UNSUBSCRIBE_MAILTO`、`LIST_UNSUBSCRIBE_URL`：可选退订头配置，支持 `{eventId}`、`{recipient}`、`{sender}`、`{domain}`、`{userId}` 占位符。
- `LIST_UNSUBSCRIBE_POST_ENABLED`：是否添加 one-click 退订头；仅在退订 URL 真正支持合规 POST 时开启。
- `FEEDBACK_ID_ENABLED`：是否为 API 发信添加不含明文邮箱的 `Feedback-Id`。
- `REPORT_ABUSE_TO`、`CSA_COMPLAINTS_TO`：可选投诉处理邮箱头。
- `BOUNCE_ADDRESS`、`BOUNCE_ENVELOPE_ENABLED`：可选退信 envelope sender；确认 bounce 邮箱/域名可收信和处理前不要开启。

## SMTP 发信

MailHub 提供公网 SMTP Submission。所有发信端口都需要 SMTP AUTH，未认证请求不会转发邮件，避免开放中继。

```txt
Host: 由 SUBMISSION_HOST 配置，例如 smtp.mailhub.example.com
Port 25:   SMTP + STARTTLS + AUTH
Port 587:  SMTP Submission + STARTTLS + AUTH
Port 465:  SMTPS implicit TLS + AUTH
Port 2525: SMTP + STARTTLS + AUTH
Username: 用户在网页“SMTP 凭据”中配置
Password: 用户在网页“SMTP 凭据”中配置
```

SMTP 密码会同时保存哈希和服务端加密密文：哈希用于认证，加密密文用于用户本人在网页复制。旧数据如果只有哈希，无法反解，需要用户重新设置一次密码后才能复制。

## IMAP / POP3 收信

Docker Compose 中由 Dovecot 独占 IMAP/POP3 端口，MailHub Node 服务不再直接实现客户端协议。邮箱地址和原密码继续由 MailHub 认证，Vesta 导入的 MD5-CRYPT 密码首次登录成功后会自动升级为 scrypt，不需要用户改密码。Compose 默认固定开放 IMAP 与 POP3；旧的 `IMAP_ENABLED`、`POP3_ENABLED`、`IMAP_PORTS`、`POP3_PORTS` 只用于本地 `legacy` 协议回归。若生产环境不提供某个协议，应通过 `IMAP_BIND` / `POP3_BIND`、Compose override 或防火墙限制对应端口。

```txt
IMAP: 143 STARTTLS / 993 implicit TLS
POP3: 110 STLS / 995 implicit TLS
Username: 完整邮箱地址
Password: 邮箱原密码或在 MailHub 设置的新密码
```

邮件原始字节、文件夹、已读和 flags 以 `data/maildir/` 为准；SQLite 保留管理界面/API 的检索索引。Dovecot 内的移动、已读、APPEND 和 EXPUNGE 会由后台同步到该索引。首次切换应短暂停止 app 与 Dovecot，再通过 app 镜像运行 `node scripts/migrate-sqlite-maildir.js`；迁移可重复执行，且会跳过已经切换到 Maildir 的邮件。

## API Token 与收发 API

每个用户可以在面板生成自己的 API Token。新 Token 会以哈希用于认证，并以服务端加密密文供所属账号在面板完整查看和复制；升级前创建、仅保存哈希的历史 Token 无法反解，可在面板确认后重新生成。

Token 支持以下权限：

- `send`：发送邮件。
- `mailboxes:read`：查询可访问邮箱的元数据。
- `mailboxes:write`：创建长期或临时邮箱。
- `messages:read`：查询收到的邮件列表、文件夹和邮件正文。

收信读取还受 Token 邮箱范围约束：默认仅 Token 所有者名下邮箱，也可限定到指定邮箱；管理员可显式授予全系统邮箱读取权限。该范围同样约束带 `mailboxes:read` 权限的邮箱元数据列表。

```bash
curl -X POST https://mailhub.example.com/api/send \
  -H "Authorization: Bearer <USER_API_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "noreply@example.com",
    "to": "user@example.net",
    "subject": "Hello from MailHub",
    "text": "Signed with DKIM and queued by MailHub."
  }'
```

发送时会校验 `From` 所属域名必须归当前 Token 所属用户所有。`.env` 中的 `API_TOKEN` 仅作为旧版 admin 兼容发送 token，新用户应使用网页生成的用户级 Token。

查询收信列表和邮件详情：

```bash
curl -H "Authorization: Bearer <INBOUND_API_TOKEN>" \
  "https://mailhub.example.com/api/inbound-messages?page=1&pageSize=20&folder=INBOX&read=false"

curl -H "Authorization: Bearer <INBOUND_API_TOKEN>" \
  "https://mailhub.example.com/api/inbound-messages/123"

curl -H "Authorization: Bearer <INBOUND_API_TOKEN>" \
  "https://mailhub.example.com/api/inbound-mailboxes/42/folders"
```

列表接口支持 `page`、`pageSize`、`mailboxId`、`folder`、`read` 和 `q`；详情接口返回 `rawMessage`、`textBody` 与 `htmlBody`；文件夹接口返回标准用途、邮件数和未读数。未授权邮箱不会出现在列表中，直接读取详情或文件夹时返回 `404`。

## DNS API 一键配置

用户可在网页“DNS API”中保存以下服务商凭据：

- Cloudflare：API Token，建议只授予目标 Zone 的 DNS Edit 权限；可选 Zone ID。
- 阿里云 DNS：AccessKeyId 和 AccessKeySecret。
- 腾讯云 DNSPod：SecretId 和 SecretKey。

域名绑定 DNS 凭据后，点击“一键配置 DNS”会写入或更新验证 TXT、DKIM TXT、SPF TXT、DMARC TXT 和发信主机 A 记录。PTR 反向解析只检测和提示，通常必须在云服务器或 IP 服务商控制台配置。

## 部署

生产环境建议：

1. 将仓库部署到服务器目录，例如 `/opt/mailhub`。
2. 基于 `.env.example` 创建 `.env`，填写真实域名、IP、证书路径和强随机密钥。
3. 将 TLS 证书放在本地 `certs/` 目录，确保私钥不会进入 Git。如证书由宝塔等主机端工具续期，可在 `.env` 中设置 `MAILHUB_CERT_SOURCE_DIR`，该目录需包含 `fullchain.pem` 和 `privkey.pem`。
4. 以 root（或 Linux 宿主 UID 1000）运行 `npm run prepare:dovecot`，生成仅 app 与 Dovecot 可读的内部认证 secret；脚本会在不兼容的 Linux UID 下直接失败，避免启动后才发现 Maildir/secret 无权限。
5. 运行 `docker compose build app postfix`，然后用 `docker compose stop app dovecot` 开始短维护窗口。
6. 运行 `docker compose run --rm --no-deps -T app node scripts/migrate-sqlite-maildir.js`，把现有 SQLite 邮件可重入地写入 Maildir；迁移完成前不要让旧 IMAP 或 SMTP 入站继续写邮件。
7. 运行 `docker compose up -d` 恢复服务。
8. 使用反向代理把 HTTPS 流量转发到 `127.0.0.1:${APP_PORT}`。
9. 在云防火墙和系统防火墙中放行需要的 SMTP、IMAP 和 POP3 端口。

可选的远程部署脚本需要显式提供目标服务器和目录：

```bash
MAILHUB_DEPLOY_REMOTE=deploy@example.com \
MAILHUB_DEPLOY_DIR=/opt/mailhub \
MAILHUB_DEPLOY_BRANCH=main \
npm run deploy:remote
```

脚本会要求本地 HEAD 已推送到对应远端分支，然后在目标目录执行 `git pull --ff-only`、准备 Dovecot secret、离线同步证书、预拉取 Dovecot 镜像、迁移现有 Maildir、重建服务，并等待 `app`、`postfix`、`dovecot` 都进入健康状态。如果迁移写入或最终 SQLite 切换事务失败，脚本会清理未提交的 Maildir 文件并恢复切换前容器；切换事务一旦成功则不会自动启动 legacy 协议栈，以免已读、移动或删除状态在两个真源间分叉。此时若新服务启动失败，应保持 Maildir 数据不动并修复后向前恢复。目标工作区存在未提交变更时脚本会停止；确认可暂存远端工作区时，可显式设置 `MAILHUB_DEPLOY_STASH_REMOTE=1`。

配置 `MAILHUB_CERT_SOURCE_DIR` 后，发布脚本会先把主机证书复制为受控快照，再校验有效期、主机名和公私钥匹配。同步过程带并发锁和失败回滚，目标证书固定为 `0644`，私钥固定为 `0640` 并授权给容器运行组。证书变化后会同时重启 app 与 Dovecot，并通过 `465`、`993` 的 SNI、证书链、主机名和 SHA-256 指纹确认两个服务均已加载新证书。

同一脚本可由宝塔计划任务定期执行；宝塔仍负责申请和续期证书，MailHub 只读取续期结果：

```bash
cd "/opt/mailhub" && MAILHUB_CERT_RESTART=1 ./scripts/sync-tls-certificate.sh
```

默认验证公网语义端口 `465 993`，其中 `993` 会自动映射到 Dovecot 的 rootless 容器端口 `31993`。若部署明确关闭了其中一个 TLS 服务，可通过 `MAILHUB_CERT_VERIFY_ENDPOINTS` 调整；离线同步且无法检测 app 容器组时，必须显式设置 `MAILHUB_CERT_READER_GID`。

## 测试

```bash
npm test
npm run build
```

测试覆盖数据库迁移、多用户隔离、SMTP 凭据、API Token、DNS 服务商逻辑、DKIM、投递日志解析和前端纯逻辑模型。

## 安全清单

- 不要提交 `.env`、SQLite 数据库、API Token、SMTP 密码、DNS API 密钥、证书私钥或真实生产证书。
- 替换默认管理员凭据，设置足够长的 `SESSION_SECRET`。
- 生产发信前确认 SPF、DKIM、DMARC、PTR 和发信主机 A 记录。
- 确认服务器出站 25 端口没有被云厂商拦截。
- 确认入站 `25/465/587/2525` 以及所需的 `110/143/993/995` 已在云防火墙和系统防火墙放行。
- 新 IP 先小流量预热，避免突然大批量发送。
- 遵守适用法律、服务商政策和收件人同意要求；不要使用 MailHub 发送垃圾邮件。

## 贡献

欢迎提交 issue 和 pull request。开始前请阅读 [CONTRIBUTING.md](CONTRIBUTING.md)、[CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) 和 [SECURITY.md](SECURITY.md)。

## 许可证

MailHub 使用 MIT License。详见 [LICENSE](LICENSE)。
