# MailGo

Go 语言编写的轻量级邮件系统，集成 SMTP / IMAP / POP3 协议服务和 Web 管理界面。
Web 前端采用 QQ 邮箱风格的布局：顶部导航 + 左侧文件夹栏 + 邮件列表三栏设计。

## 功能特性

- **邮件协议**：SMTP（发送）、IMAP（同步）、POP3（收取），均支持 TLS 加密
- **外部投递**：认证用户可向外部邮箱（QQ/Gmail/Outlook 等）发送邮件，内置外发队列、**并发 worker 池投递**（默认 4 线程 + 每收件域并发上限 2）、MX 直投、STARTTLS、指数退避重试、退信通知与 DKIM 签名
- **Web 邮箱**：QQ 邮箱风格界面，支持收件箱 / 已发送 / 草稿箱、未读角标与搜索过滤、全选 / 批量删除、发件人头像、富文本编辑（Quill.js）、附件上传/下载
- **管理后台**：域名管理、用户管理、DKIM 密钥自动生成、DNS 配置提示、全量邮件查看、外发队列管理、IP 封禁管理、仪表盘统计
- **协议调用日志**：SMTP / IMAP / POP3 每次连接自动记录来源 IP、用户名、成功/失败、失败原因与操作摘要，可按协议/状态/IP/用户名/时间筛选，用于分析密码爆破、中继滥用等攻击行为（默认保留 30 天，自动清理）
- **IMAP 新邮件推送**：本地投递（SMTP/Web 写信）成功后实时推送，挂起 IDLE 的客户端即时收到新邮件通知（无需轮询）；其他客户端造成的已读/星标/删除变化也实时同步（IMAP STORE/EXPUNGE、POP3 删除、Web 标已读/删除）
- **当前连接监控**：管理后台实时查看 SMTP/IMAP/POP3 活动连接（来源 IP、用户名、TLS、时长），每 5 秒自动刷新；支持「断开并封禁」一键封禁该 IP 全部在线连接（封禁 180 天，可随时解封）
- **外部认证**：OAuth2（Google / GitHub）、LDAP（可选，默认关闭）
- **安全机制**：BCrypt 密码哈希、登录失败自动封禁 IP（**阶段性封禁**：前 3 次达到失败阈值只计数，第 4 次起封禁并按档位递增 30分钟 → 3小时 → 3个月 → 半年，成功登录清零）、外发频率限制（防滥用）、非认证禁止中继（防开放中继）、管理员可解封
- **多数据库**：默认 SQLite，可切换 MySQL
- **跨平台**：Linux 生产部署 + Windows 本地调试

## 界面预览

| 收件箱 | 邮件阅读 |
|--------|----------|
| ![收件箱](docs/screenshots/inbox.png) | ![邮件阅读](docs/screenshots/view.png) |

| 写信 | 设置 | 登录 |
|------|------|------|
| ![写信](docs/screenshots/compose.png) | ![设置](docs/screenshots/settings.png) | ![登录](docs/screenshots/login.png) |

> 截图使用演示数据渲染，实际界面以部署为准。

## 快速开始

### 编译

```bash
go build -o mailgo .
```

### 启动

```bash
./mailgo
```

首次启动会自动创建配置文件和数据库，默认管理员账户：

| 项目 | 值 |
|------|-----|
| 邮箱 | `admin@example.com` |
| 密码 | `admin` |

> ⚠️ 请在生产环境中立即修改默认密码。

### 访问

| 页面 | 地址 |
|------|------|
| 用户邮箱 | `http://localhost:8080/` |
| 管理后台 | `http://localhost:8080/admin` |

---

## 配置文件

配置文件路径（TOML 格式）：

| 系统 | 路径 |
|------|------|
| Linux | `/etc/mail_go/mail_go.toml` |
| Windows | `./win/etc/mail_go/mail_go.toml` |

首次启动自动生成，缺失字段自动补全。修改后需重启服务生效。

### 完整配置参考

```toml
[database]
driver = "sqlite"                          # sqlite | mysql
dsn = "/srv/mail_go/mail.db"               # SQLite: 文件路径; MySQL: DSN 连接串

[storage]
base_dir = "/srv/mail_go"                  # 数据根目录
attach_dir = "/srv/mail_go/attachments"     # 附件存储目录

[web]
addr = ":8080"                             # 监听地址，支持 TCP 端口或 Unix socket
secret_key = ""                             # Web 会话签名密钥；留空时首次启动自动生成
                                           # 随机密钥并写入本文件（请妥善备份，泄露/丢失
                                           # 分别意味着会话可被伪造/所有登录态失效）
cookie_secure = true                        # 会话 cookie 仅通过 HTTPS 传输（Secure 标志）；
                                           # 仅本地 HTTP 调试时才改为 false
protocol_log_keep_days = 30                # SMTP/IMAP/POP3 协议调用日志保留天数，
                                           # 超出后由后台任务自动清理；0 表示不清理

[smtp]
addr = ":25"                               # SMTP 明文端口
tls_addr = ":465"                          # SMTPS 端口（需配置 TLS 证书）
domain = "example.com"                     # 邮件域名
tls_cert = ""                             # TLS 证书路径（留空则不启用 TLS）
tls_key = ""                              # TLS 私钥路径
max_message_bytes = 67108864              # 单封邮件最大 64MB

[imap]
addr = ":143"                              # IMAP 明文端口
tls_addr = ":993"                          # IMAPS 端口
tls_cert = ""
tls_key = ""

[pop3]
addr = ":110"                              # POP3 明文端口
tls_addr = ":995"                          # POP3S 端口
tls_cert = ""
tls_key = ""

[auth]
oauth2_enabled = false                     # 是否启用 OAuth2 登录
oauth2_provider = ""                       # google | github
oauth2_client_id = ""
oauth2_client_secret = ""
oauth2_redirect_url = ""
ldap_enabled = false                      # 是否启用 LDAP 登录
ldap_server = ""                          # 例: ldap://localhost:389
ldap_bind_dn = ""                         # 例: cn=admin,dc=example,dc=com
ldap_bind_password = ""
ldap_search_base = ""                     # 例: ou=users,dc=example,dc=com
ldap_search_filter = ""                   # 例: (uid=%s)
ldap_use_tls = false

[ban]
max_fail_attempts = 5                    # 登录失败次数阈值
ban_duration_min = 30                     # 第 1 次封禁时长（分钟）；之后按档位递增：
                                           # 第 2 次 3 小时 → 第 3 次 3 个月 → 第 4 次起半年（上限）
                                           # 前 3 次达到阈值只计数不封禁，成功登录后清零

[caddy]
data_dir = ""                             # Caddy 数据目录（含 certificates/ 的那个），
                                          # 供后台一键导入证书；留空自动探测
                                          # /var/lib/caddy/.local/share/caddy 等常见位置

[outbound]
hostname = ""                            # EHLO 主机名，留空使用 [smtp] domain
poll_interval = 15                       # 外发队列扫描间隔（秒）
workers = 4                              # 并发投递 worker 数（多线程并行发送，
                                         # 大量邮件时吞吐提升；0/1 为串行）
batch_size = 50                          # 每次扫描最多取出的待投递邮件数
max_concurrent_per_domain = 2            # 同一收件域（或中继）的最大并发连接数，
                                         # 防被判定为滥发；0 表示不限制
max_attempts = 12                        # 单封邮件最大投递尝试次数
retry_base_min = 5                       # 重试退避基数（分钟），指数增长：5/10/20/40...
max_recipients = 50                      # 单封邮件最大外部收件人数
max_per_min = 30                         # 每用户每分钟最大外发数
max_per_day = 500                        # 每用户每日最大外发数，0 表示禁用外部投递
connect_timeout = 30                     # 连接远程 MX 超时（秒）
relay_host = ""                          # 智能主机（smarthost），留空则直投 MX
relay_port = 587                         # 465 = 隐式 TLS，其他端口按需 STARTTLS
relay_user = ""                          # 中继认证用户名（AUTH PLAIN）
relay_password = ""                      # 中继认证密码
relay_starttls = true                    # 非 465 端口是否使用 STARTTLS
relay_tls_insecure = false                # 是否跳过中继服务器 TLS 证书验证；
                                          # 默认验证证书（保护中继凭据），仅自签证书
                                          # 内网中继且明确知晓风险时才改为 true
ip_family = "ipv4"                       # 出站地址族：ipv4（默认）| ipv6 | auto
source_ip = ""                           # 出站源地址绑定（如静态 IPv6 地址），留空由内核选择
```

---

## 常见配置场景

### 1. 切换到 MySQL

```toml
[database]
driver = "mysql"
dsn = "mailgo:YourPassword@tcp(127.0.0.1:3306)/mailgo?charset=utf8mb4&parseTime=True&loc=Local"
```

MySQL DSN 格式：`用户名:密码@tcp(主机:端口)/数据库名?参数`

> 数据库和用户需提前在 MySQL 中创建：
> ```sql
> CREATE DATABASE mailgo CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
> CREATE USER 'mailgo'@'localhost' IDENTIFIED BY 'YourPassword';
> GRANT ALL PRIVILEGES ON mailgo.* TO 'mailgo'@'localhost';
> FLUSH PRIVILEGES;
> ```

### 2. Web 使用 Unix Socket

```toml
[web]
addr = "/run/mail_go/web.sock"
```

当 `addr` 以 `/` 开头时，Gin 自动以 Unix socket 方式监听。

### 3. 指定会话密钥（容器/多实例部署）

会话签名密钥可通过环境变量 `MAILGO_SECRET_KEY` 覆盖（优先于配置文件，且不会写入磁盘）：

```bash
MAILGO_SECRET_KEY="$(openssl rand -hex 32)" mail_go
```

要求：长度至少 16 字节；留空时由配置文件提供（首次启动自动生成）。更换密钥后所有已登录会话立即失效，用户需重新登录。

Nginx 反向代理配置：

```nginx
server {
    listen 80;
    server_name mail.example.com;

    location / {
        proxy_pass http://unix:/run/mail_go/web.sock;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}
```

> 确保 socket 目录存在并有写权限：
> ```bash
> mkdir -p /run/mail_go
> chown mailgo:mailgo /run/mail_go
> ```

### 3. 启用 TLS 加密

为 SMTP/IMAP/POP3 启用 TLS（三个协议共享同一证书，或分别配置）：

```toml
[smtp]
tls_cert = "/etc/mail_go/certs/server.crt"
tls_key = "/etc/mail_go/certs/server.key"

[imap]
tls_cert = "/etc/mail_go/certs/server.crt"
tls_key = "/etc/mail_go/certs/server.key"

[pop3]
tls_cert = "/etc/mail_go/certs/server.crt"
tls_key = "/etc/mail_go/certs/server.key"
```

配置 TLS 后，系统会自动启动对应的加密端口（465/993/995）。管理后台添加域名时勾选 TLS 会自动切换端口号。

> 可用 Let's Encrypt 获取免费证书：
> ```bash
> certbot certonly --standalone -d mail.example.com
> # 证书路径: /etc/letsencrypt/live/mail.example.com/fullchain.pem
> # 私钥路径: /etc/letsencrypt/live/mail.example.com/privkey.pem
> ```

#### 从 Caddy 一键导入证书

如果本机已用 [Caddy](https://caddyserver.com/) 托管该域名 HTTPS（Caddy 会自动签发并续期证书），
可在管理后台 **域名管理 → 编辑域名** 页面点击 **“从 Caddy 获取证书”** 按钮，
一键把 Caddy 存储中的证书与私钥导入邮件服务（自动启用该域名的 TLS），无需手动复制 PEM 文件。
支持通配符证书（如 `*.example.com` 可匹配 `mail.example.com`）。
证书支持**热加载**：导入（或手动上传）后立即生效，无需重启服务——SMTP/IMAP/POP3
每次 TLS 握手会自动检查并重载变化的证书文件。

由于 Caddy 的证书目录仅 `caddy` 用户可读，install.sh 会安装一个 root 权限的证书同步任务
（`mailgo-caddy-sync.{path,timer}`），把 Caddy 证书树镜像到 `/srv/mail_go/tls/caddy`，
证书续期后自动同步，mail_go 始终可读；另外还会授予 ACL 权限作为直接读取的兜底。
安装时自动配置，也可手动执行：

```bash
sudo ./install.sh setup-caddy-cert          # 自动探测 Caddy 数据目录并配置同步 + ACL
sudo ./install.sh setup-caddy-cert /path/to/caddy/data   # 或手动指定数据目录
```

若 Caddy 数据目录不在常见位置，可在配置文件中显式指定：

```toml
[caddy]
data_dir = "/var/lib/caddy/.local/share/caddy"
```

### 4. 启用 OAuth2 登录（Google 示例）

```toml
[auth]
oauth2_enabled = true
oauth2_provider = "google"
oauth2_client_id = "your-client-id.apps.googleusercontent.com"
oauth2_client_secret = "your-client-secret"
oauth2_redirect_url = "https://mail.example.com/auth/oauth2/callback"
```

> 需要在 [Google Cloud Console](https://console.cloud.google.com/) 创建 OAuth 2.0 客户端，授权回调地址填 `https://你的域名/auth/oauth2/callback`。

### 5. 启用 LDAP 认证

```toml
[auth]
ldap_enabled = true
ldap_server = "ldap://ldap.example.com:389"
ldap_bind_dn = "cn=admin,dc=example,dc=com"
ldap_bind_password = "ldap_admin_password"
ldap_search_base = "ou=users,dc=example,dc=com"
ldap_search_filter = "(uid=%s)"
ldap_use_tls = true
```

### 6. 对外发送邮件（外部投递）

认证用户（Web 邮箱 / SMTP 提交）可以向外部邮箱地址发送邮件。系统通过
收件人域名的 MX 记录直投，自动处理 STARTTLS、指数退避重试，并为外发
邮件添加 DKIM 签名（使用后台域名管理中生成的密钥）。

```toml
[smtp]
domain = "example.com"        # 必须设置为真实的邮件域名（EHLO/退信地址）

[outbound]
hostname = "mail.example.com" # 建议与邮件主机名一致
poll_interval = 15
max_attempts = 12
max_per_day = 500             # 设为 0 可完全禁用外部投递
```

需要满足的 DNS / 服务器条件（详见 `todo.md` 与后台 DNS 提示页）：

| 项目 | 说明 |
|------|------|
| MX | `example.com MX 10 mail.example.com` |
| SPF | `example.com TXT "v=spf1 mx -all"` |
| DKIM | `default._domainkey.example.com TXT "v=DKIM1; k=rsa; p=<公钥>"`（后台自动生成） |
| DMARC | `_dmarc.example.com TXT "v=DMARC1; p=none; rua=mailto:postmaster@example.com"` |
| PTR | 服务器 IP 反向解析指向邮件主机名（需向机房申请） |
| 网络 | 服务器 25 端口出站未被云厂商封锁 |

安全策略：外部收件人仅接受已认证用户；`MAIL FROM` 必须与登录用户一致；
每用户每分钟/每日外发数受限；失败邮件会退信到发件人收件箱；
管理员可在后台「外发队列」查看投递状态、手动重试或取消。

> **IPv4/IPv6**：默认仅使用 IPv4 出站（`ip_family = "ipv4"`），因为很多收件方
> （如 Gmail）会拒收没有 PTR 的 IPv6 地址，而 IPv4 通常具备正反向一致的 PTR。
> 如需走 IPv6：请运营商为静态地址配置 PTR（指向 `mail.example.com`），
> 然后设置 `ip_family = "ipv6"` 并把 `source_ip` 绑定到该静态地址
> （避免内核使用轮换的临时隐私地址）。

### 7. 通过智能主机（smarthost）中继外发

服务器 IP 属于家庭宽带/动态 IP 段时，常被 Spamhaus PBL 等策略列表收录，
Microsoft（Outlook/Hotmail）等收件方会直接拒收。此时建议把外发邮件交给
第三方 SMTP 中继（Mailgun / SendGrid / Amazon SES / 阿里云邮件推送等），
在 `[outbound]` 中配置即可，所有外部投递自动改走中继：

```toml
[outbound]
relay_host = "smtp.example-relay.com"
relay_port = 587          # 465 为隐式 TLS
relay_user = "your-api-user"
relay_password = "your-api-key"
relay_starttls = true
```

中继使用 AUTH PLAIN 认证；本地收件人仍走本地投递，不受影响。

---

## 端口速查

| 协议 | 明文端口 | TLS 端口 | 说明 |
|------|---------|---------|------|
| SMTP | 25 | 465 | 邮件发送 |
| IMAP | 143 | 993 | 邮箱同步 |
| POP3 | 110 | 995 | 邮件收取 |
| Web | 8080 | — | Web 界面（可配 Unix socket） |

---

## 目录结构

```
mailgo/
├── main.go                          # 程序入口
├── config/
│   ├── config.go                    # 配置加载与合并
│   └── defaults.go                  # 默认常量
├── internal/
│   ├── db/
│   │   ├── db.go                    # 数据库初始化（SQLite/MySQL）
│   │   └── models.go               # GORM 模型定义
│   ├── store/
│   │   ├── stores.go                # Store 聚合器
│   │   ├── user_store.go            # 用户数据操作
│   │   ├── mail_store.go            # 邮件数据操作
│   │   ├── domain_store.go          # 域名数据操作
│   │   ├── attachment_store.go      # 附件数据操作
│   │   ├── outbound_store.go        # 外发队列数据操作
│   │   ├── ban_store.go             # 封禁数据操作
│   │   └── protocol_log_store.go    # 协议调用日志数据操作
│   ├── smtp_server/server.go        # SMTP 服务
│   ├── outbound/
│   │   ├── mailer.go               # MX 查询与 SMTP 出站客户端
│   │   ├── manager.go              # 外发队列、重试、限速、退信
│   │   └── sign.go                 # DKIM 签名
│   ├── imap_server/
│   │   ├── server.go                # IMAP 服务
│   │   └── backend.go               # IMAP 后端
│   ├── pop3_server/server.go         # POP3 服务
│   ├── connhub/hub.go                # 协议连接注册中心（当前连接监控）
│   ├── storage/attachment.go         # 附件文件存储
│   ├── dkim/keys.go                  # DKIM 密钥生成
│   ├── auth/
│   │   ├── provider.go              # 认证接口
│   │   ├── oauth2.go                # OAuth2 认证
│   │   └── ldap.go                  # LDAP 认证
│   └── web/
│       ├── server.go                 # Web 路由与模板加载
│       ├── handlers/
│       │   ├── auth.go              # 登录/登出/OAuth2/LDAP
│       │   ├── mail.go              # 邮箱操作
│       │   └── admin.go             # 管理后台
│       ├── middleware/
│       │   ├── auth.go              # 会话认证
│       │   ├── admin.go             # 管理员权限
│       │   └── ban.go               # IP 封禁检查
│       └── templates/               # HTML 模板
│           ├── *.html               # 用户页面
│           └── admin/*.html          # 管理页面
└── .gitignore
```

### 运行时数据目录（Linux）

| 路径 | 说明 |
|------|------|
| `/etc/mail_go/` | 配置文件 |
| `/srv/mail_go/` | 数据库 + 附件存储 |

### 运行时数据目录（Windows 调试）

| 路径 | 说明 |
|------|------|
| `./win/etc/mail_go/` | 配置文件 |
| `./win/srv/mail_go/` | 数据库 + 附件存储 |

---

## Linux 服务部署

### systemd 服务文件

创建 `/etc/systemd/system/mailgo.service`：

```ini
[Unit]
Description=MailGo Mail Server
After=network.target mysql.service

[Service]
Type=simple
User=mailgo
Group=mailgo
WorkingDirectory=/opt/mailgo
ExecStart=/opt/mailgo/mailgo
Restart=on-failure
RestartSec=5
LimitNOFILE=65536

# 安全加固
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/srv/mail_go /run/mail_go /etc/mail_go

[Install]
WantedBy=multi-user.target
```

### 启动服务

```bash
# 创建系统用户
sudo useradd -r -s /sbin/nologin -d /srv/mail_go mailgo

# 设置目录权限
sudo chown -R mailgo:mailgo /srv/mail_go /etc/mail_go

# 启用并启动
sudo systemctl daemon-reload
sudo systemctl enable mailgo
sudo systemctl start mailgo

# 查看日志
sudo journalctl -u mailgo -f
```

---

## DNS 配置

邮件服务需要以下 DNS 记录（管理后台 → DNS 提示 页面可查看完整配置）：

| 记录类型 | 名称 | 值 | 说明 |
|---------|------|-----|------|
| A | mail | 服务器 IP | 邮件服务器地址 |
| MX | @ | mail.example.com | 邮件路由 |
| TXT | @ | `v=spf1 mx ~all` | SPF 反垃圾 |
| TXT | default._domainkey | DKIM 公钥（后台自动生成） | DKIM 签名验证 |
| TXT | _dmarc | `v=DMARC1; p=none; rua=mailto:admin@example.com` | DMARC 策略 |

---

## 技术栈

| 组件 | 技术 |
|------|------|
| 语言 | Go 1.25+ |
| Web 框架 | Gin |
| 模板引擎 | html/template |
| ORM | GORM |
| 数据库 | SQLite（默认）/ MySQL |
| 配置格式 | TOML |
| SMTP | github.com/emersion/go-smtp |
| IMAP | github.com/emersion/go-imap v1 |
| POP3 | 手工实现 TCP 协议 |
| 密码哈希 | golang.org/x/crypto/bcrypt |
| 富文本 | Quill.js (CDN) |
| OAuth2 | golang.org/x/oauth2 |
| LDAP | github.com/go-ldap/ldap/v3 |
| DKIM | RSA 2048 自动生成 |

---

## 默认账户

| 角色 | 邮箱 | 密码 | 默认配额 |
|------|------|------|---------|
| 管理员 | admin@example.com | admin | 5 GB |

> ⚠️ 部署到生产环境后请立即修改管理员密码。

## License

MIT
