自建Web邮箱:Roundcube+Docker+QQ收信+Resend发信
折腾完这套自建 Web 邮箱,确实省了不少事。直接在浏览器里收 QQ 邮箱的信,发信走 Resend 的 SMTP,打开页面就能自动登录,不用每次输授权码。整套东西全部跑在 Docker 里,配置和升级都很干净。这里把从零到一的完整过程撸一遍,重点说清楚每个环节为什么这么配、踩过哪些坑,给想在 Docker 里搭个人邮箱或者私有 Web 客户端的同学一个可直接抄的作业。
先梳理一下整个组合:Roundcube 是目前开源圈子里比较老牌的 Web 邮件客户端,基于 PHP,功能完整、插件体系成熟;Docker 负责把它一键跑起来,省去手动装 PHP/Nginx 环境的麻烦;QQ 邮箱负责当“收件后端”,通过 IMAP 协议把收件箱里的信同步到 Roundcube;发信则交给 Resend,用它提供的标准 SMTP 服务发出去;最后再用一个小插件实现自动登录,让浏览器打开 Roundcube 地址时直接进入邮件界面,无需手动输入账号密码。适合的场景很明确:个人自用、单账号、想在浏览器统一处理 QQ 来信且发信用自己的域名身份,同时对 Docker 部署和邮件协议有一定折腾兴趣的人。
1. 项目整体设计与思路拆解
1.1 先搞清楚这个组合在做什么
很多人看到“Roundcube”“IMAP”“SMTP”这几个词会有点懵,我用大白话拆一下。你的 QQ 邮箱账号里有一堆收到的邮件,正常情况下你要么用网页版 QQ 邮箱看,要么用桌面客户端登录。Roundcube 做的事情,就是把这些邮件“搬”到一个你自己的网页界面上展示。它本身不存邮件,只是通过 IMAP 协议跟 QQ 邮箱的服务器实时同步,你看到的收件箱、已发送、草稿都是远程服务器上的真实数据。
发信是另一条线。Roundcube 写完一封新邮件后,要通过 SMTP 协议把信投递出去。标题里的 Resend 就是干这个的,它提供一个标准的 SMTP 发信服务,你只需要在 Roundcube 里把发件服务器指向 Resend,再配上密钥,邮件就能以你绑定的域名身份发出去。这里有个关键认知:收信服务器和发信服务器可以是完全不同的两家。收信用 QQ,发信用 Resend,互不影响,Roundcube 在其中只是做聚合和桥接。
1.2 为什么从一堆方案里选了 Roundcube
自建 Web 邮箱客户端不是只有 Roundcube 一个选项,有更轻量的 RainLoop、SnappyMail,也有偏协作的 SOGo、Mailu 全家桶。我最后选 Roundcube,主要看中三点。
第一是稳定和兼容性。Roundcube 活了十几年,IMAP/SMTP 各种怪异的服务器行为基本都见过,尤其是 QQ 邮箱这种有些“个性”的 IMAP 实现,Roundcube 处理起来比较成熟。第二是插件生态。日历、通讯录、自动登录、密码找回这些功能都有现成插件,就算没有,它的 hook 机制也允许你写个几十行 PHP 的小插件,扩展成本很低。第三是界面接近传统邮件客户端,收件箱、文件夹树、邮件列表三栏布局,用惯了 Outlook 或 Foxmail 的人几乎零学习成本。
相比起来,RainLoop 确实更轻更现代,但插件生态较弱,社区维护节奏也慢;SnappyMail 性能好,但配置逻辑跟主流 Roundcube 差别较大,网上的中文经验贴太少。个人自用的话,选用户基数大、踩坑资料多的方案永远更稳妥。
1.3 收信、发信、免登录三件事的分工
这三件事虽然在一个系统里,但对应的协议和配置完全不同,理解这个分工很重要。
收信走 IMAP,端口 993,全程 SSL 加密。QQ 邮箱这边需要你先开启“IMAP/SMTP 服务”,拿到一个授权码,这个授权码替代 QQ 密码用在 IMAP 连接上。Roundcube 登录时拿到的“用户名 + 授权码”,本质就是拿去连 imap.qq.com 做认证。
发信走 SMTP,端口 587 或 465。Resend 的验证方式比较简单:用户名固定是 resend,密码是一串以 re_ 开头的 API Key。Roundcube 发信时就用这对凭据跟 Resend 建立 SMTP 连接,投递完成后 Resend 会在后台统计发送状态。
自动登录则完全发生在 Roundcube 自己的应用层。它跟 IMAP/SMTP 没有关系,而是利用 Roundcube 的插件 hook,在用户访问页面时预先注入默认账号和密码,让登录流程自动完成。说白了就是帮你“代填表单 + 自动提交”,只是用更优雅的插件方式实现,而不是前端脚本硬刷。
2. 前置准备:Docker 环境与两个账号
2.1 Docker 环境准备
开始之前先确认本机 Docker 能用。我用的是 Linux 服务器,Docker Engine + Docker Compose Plugin 的组合。如果你在 Windows 或 macOS 上,就装 Docker Desktop,它自带 docker compose 命令,使用体验一致。
装好之后跑一下版本验证:
两个命令都有输出就说明环境没问题。然后确认守护进程是启动状态,尤其是 Linux 上刚装完经常忘了启动服务:
Docker 环境这步没什么技术含量,但值得多说一句:容器跑起来之后,日志、挂载目录、端口占用这三个维度决定了你后面排障的效率。所以我从一开始就规划好目录结构,比如在 /opt/mail/ 下建 config/、db/、plugins/ 三个目录,后面所有配置文件都有固定位置,升级和迁移都方便。
2.2 QQ 邮箱的 IMAP 授权码
这是整个工程里最容易卡住的一步,因为很多人还想着用 QQ 密码去连第三方客户端,根本不认。
登录 QQ 邮箱网页版,进入“设置 → 账号”,找到“POP3/IMAP/SMTP/Exchange/CardDAV/CalDAV 服务”。把“IMAP/SMTP 服务”那一项开启,网页会提示你用手机发一条短信验证,验证通过后会生成一串 16 位左右的授权码。把这串授权码单独存好,它就是你之后所有第三方客户端登录 QQ 邮箱的凭证。
有一点必须强调:IMAP 服务开启后,授权码是唯一的密码凭证,QQ 密码本身不能用于 IMAP 连接。Roundcube 自动登录插件里填的“密码”也是这串授权码,不是 QQ 密码。另外,QQ 邮箱的 IMAP 服务对第三方客户端的连接频率和登录地点有一定风控,如果频繁断连、频繁在异地 IP 登录,有可能会临时限制服务,建议之后在 Roundcube 里不要设置太短的自动刷新时间。
2.3 Resend 的发信域名与 SMTP 凭据
Resend 是一个邮件发送 API 平台,除了 API 方式,它也提供标准 SMTP 接口。去 Resend 官网注册一个账号,然后在 Dashboard 里进入 Domains,添加一个你拥有管理权的域名,比如 mail.example.com 或直接用主域名。添加后会让你配三条 DNS 记录,一般是 SPF、DKIM、DMARC 或者其中某几条。按页面提示把记录在你的域名解析商那里加上,等验证通过后,这个域名就可以作为发件人域名使用了。
验证通过后,在 API Keys 页面生成一把新密钥,格式是 re_xxxxxxxxxxx。这把密钥就是 SMTP 密码。至于 SMTP 用户名,Resend 规定固定是 resend,约定俗成,不用改。
这里有一个很容易被忽略的点:如果没有验证域名,Resend SMTP 只允许给你自己在 Resend 上验证过的邮箱发信,不能往任意收件人发。所以想拿它做正式发信渠道,DNS 验证那步必须老老实实配好。
3. Docker Compose 部署 Roundcube
3.1 服务编排与数据库选型
Roundcube 需要数据库来存用户偏好、联系人、会话信息。官方镜像支持 SQLite、MySQL、MariaDB、PostgreSQL,个人最省事的是 SQLite,一个单文件搞定,不用额外起容器。但为了长期稳定、方便备份,我选择 MariaDB 容器跟 Roundcube 一起编排。数据库独立成容器的好处是:Roundcube 容器随便升级重建,用户数据不丢。
Compose 文件我放在 /opt/mail/docker-compose.yml,内容如下:
两个容器放到同一个 bridge 网络里,Roundcube 可以直接用服务名 db 连接数据库。Roundcube 通过端口 8080 暴露给宿主机,如果你本机没有其他服务占用 8080,直接访问 <服务器IP>:8080 就行。
3.2 挂载式配置文件的编写
官方镜像支持一堆 ROUNDCURL_* 环境变量,但不同版本变量名经常变,最保险的做法是直接挂载一个自定义的 config.inc.php,所有配置白纸黑字写在文件里,不依赖镜像内部逻辑。
配置文件放在 /opt/mail/config/config.inc.php:
几个地方展开说一下。
db_dsnw 里的密码、用户名要和 compose 里 MariaDB 环境变量保持一致;主机名写 db 而不是 localhost,这是容器网络里访问数据库服务的正确方式。
smtp_server 写成 tls://smtp.resend.com:587 语法其实也支持,我这里把 host 和 port 分开写更清晰。tls:// 前缀表示先建立普通连接再升级 TLS,对应 Resend 的 587 端口;如果想用 465 端口,就写成 ssl://smtp.resend.com 配 smtp_port = 465,我实测两边都通,587 更通用一些。
des_key 是 Roundcube 用来加密会话数据的密钥,升级容器版本前保持不变就不会把已登录会话全部踢掉。
3.3 首次启动与数据库初始化
配置文件就绪后,第一次启动要先把数据库表结构建好。Roundcube 官方镜像启动后会自动尝试初始化数据库,但为了确保干净,我习惯手动跑初始化脚本。
如果容器日志里提示数据库没有表,进入容器执行初始化:
initdb.sh 会读取 db_dsnw 里配置的数据库连接信息,自动创建 Roundcube 所需的全部数据表。执行成功后重启一下 Roundcube 容器:
这时候访问 http://<服务器IP>:8080,应该能看到 Roundcube 的登录页面。到这一步,部署的骨架已经搭好,接下来把收发信的细节调通。
4. QQ 收信 + Resend 发信配置实战
4.1 QQ 收信的 IMAP 参数
Roundcube 登录页打开后,正常情况下需要填用户名、密码,服务器那一栏可以默认。因为我们已经在 config.inc.php 里写死了 default_host = ssl://imap.qq.com:993,登录页的服务器下拉框会被预置成这个地址。
QQ 邮箱的 IMAP 有两个细节要特别留意。
第一个是端口协议。ssl://imap.qq.com:993 表示用 SSL 直连 993 端口,这是 QQ 邮箱唯一稳的 IMAP 方式。不要写 imap.qq.com 不带前缀然后端口填 143,QQ 邮箱的 143 端口基本不开放,会连不上。
第二个是用户名格式。Roundcube 里登录 QQ 邮箱时,用户名要填完整的邮箱地址,也就是 xxxxxx@qq.com,授权码作为密码。如果你只填 QQ 号不带 @qq.com,QQ 的 IMAP 服务器会直接拒绝认证。
如果出现认证失败,先回 QQ 邮箱网页端确认 IMAP 服务是开启状态,再到 Roundcube 里重新核对授权码有没有输错。这里没有太多技巧,就是细心。
4.2 Resend 发信的 SMTP 参数
SMTP 参数在 config.inc.php 里已经配好了,就是那三行 smtp_server、smtp_port、smtp_user/smtp_pass。但发信能不能成功,还取决于一个前提:Roundcube 当前登录用户必须有一个跟 Resend 验证域名匹配的发件人身份。
配置好了以后,进入 Roundcube 的“设置 → 身份”,默认身份的名称可以写成你自己,邮箱地址要写成你在 Resend 验证过的域名邮箱,比如 你喜欢的名字@example.com,回复地址也填写同一个。这样写出去的邮件,发件人显示的是你的域名邮箱,而不是 xxxxxx@qq.com。
如果收信和发信地址不一样,Roundcube 会在邮件列表里显示两个身份,回复时默认使用写信时选择的身份。这个问题在个人自用场景无伤大雅,但如果你希望回复时发件人也跟着收件地址走,可以多建几个身份,Roundcube 支持根据当前收件箱文件夹自动切换身份,不过那是另一个话题了。
4.3 文件夹映射与身份设置
QQ 邮箱的 IMAP 文件夹命名比较反直觉。登录成功后会看到一批文件夹,但中文收件箱下面还藏着 已发送、垃圾邮件 这些目录,有些客户端不主动拉的话,已发送文件夹是空的。
我的做法是在 config.inc.php 里把几个特殊文件夹的映射写死。
这几个值是实测下来相对通用的。不同时期注册的 QQ 邮箱,IMAP 文件夹结构可能略有差异,如果你在 Roundcube 左侧的文件夹树里发现“已发送”还是空的,或者草稿存不进去,就登进 QQ 邮箱网页版或者用任意 IMAP 客户端看下实际的文件夹路径,把上面的值改成对应路径。
4.4 收发信联调测试
配置完成后,先做一轮完整的收发测试。
第一步,用 Roundcube 登录后,看收件箱能否拉取到 QQ 邮箱里已有的邮件,能拉到就说明 IMAP 通了。第二步,新建一封邮件,收件人写一个你能收到信的地址,比如你的另一个邮箱,主题写 test from rcm,点击发送。如果发送成功,Roundcube 会提示“消息已发送”,同时可以在 Resend 后台的 Activity 页面看到一条发送记录,里面有状态和时间。
第三步,等几分钟,把你发到另一个邮箱的那封邮件再回复到你的 QQ 邮箱,回到 Roundcube 收件箱看看能否收到。三轮都通过,这条“QQ 收 + Resend 发”的链路就完全打通了。
5. 自动登录免授权实现
5.1 自动登录的思路与适用边界
很多国内用户第一次用 Roundcube 的时候,最烦的一件事就是登录时要么输入 QQ 密码,要么输入那串授权码。授权码这玩意很长,记不住,每次都得翻聊天记录或者记事本。所以“自动登录”就成了刚需。
自动登录的本质,是在 Roundcube 还没展示登录页之前,就把默认账号密码注入到认证流程里。Roundcube 有一整套插件 hook 机制,其中 authenticate hook 就是专门用来在认证阶段改用户名、密码、认证服务器地址的。利用好这个 hook,就能做到访问首页时静默认证、免去登录表单。
先说边界:这套方案只适合单账号个人自用。如果有人访问你的 Roundcube 地址,会直接进到你的邮箱里,没有任何二次确认。如果你打算部署给公司或陌生人使用,自动登录是一个必须警惕的安全风险,到时候应该反过来使用 SSO 或外部认证插件。
5.2 写一个最小的 auto_login 插件
插件目录我放在 /opt/mail/plugins/auto_login/,两个文件。第一件是插件入口 auto_login.php:
第二件是插件的元信息文件 composer.json,Roundcube 的插件机制会读取它识别插件 ID 和类型:
插件代码的思路分成两个 hook。
startup hook 在 Roundcube 每次页面加载时执行,负责“拦截进场”:如果当前用户还没登录,就往 $_POST 里预填用户名、密码,然后把请求伪装成登录提交。这样 Roundcube 不会渲染登录表单,而是直接走到登录认证流程。
authenticate hook 只在认证阶段触发,负责“兜底”:把认证用的用户名、密码、主机地址全部替换成配置里预设的值。这样即使 Roundcube 内部某个环节没拿到 $_POST 数据,认证也能顺利完成。
手动切账号的场景也考虑到了:如果你在 URL 后面拼一个登录参数或者直接访问登录页并输入了其他用户名,插件会检测到 $_POST['_user'] 已存在且和预设账号不一致,就放行正常登录,不会把你的手动输入覆盖掉。
5.3 插件安装与效果验证
插件代码写好后,需要挂载到容器里。因为 Compose 文件里已经写了 ./plugins/auto_login 挂载到 /var/www/html/plugins/auto_login,所以宿主机目录 plugins/auto_login/ 下放好两个文件就行。
重启后验证:打开浏览器访问 http://<服务器IP>:8080,观察是否直接进入收件箱,而不是停在登录页面。如果直接进入,自动登录就生效了。
到这里,整个“Docker 部署 Roundcube + Resend 发信 + QQ 收信 + 自动登录”的链路已经完整跑通。下面把操练过程中最容易踩的坑集中列一下,给后续动手的人省点时间。
6. 常见问题与排查技巧实录
6.1 高频报错速查表
| 现象 | 直接原因 | 解决办法 |
|---|---|---|
| 登录时提示“登录失败” | IMAP 认证失败 | 确认 IMAP 服务已开启,确认用户名是完整的 xxx@qq.com,密码是授权码而非 QQ 密码 |
| 连接服务器超时 | 网络不通或端口不对 | 宿主机上先用 nc -vz imap.qq.com 993 测试 TCP 连通性;确认配置里是 ssl://imap.qq.com:993 |
| 发信失败,提示 SMTP 认证错误 | Resend 密钥错误 | 确认 smtp_user 固定为 resend,smtp_pass 是 re_ 开头的 API Key,注意别把 API Key 和验证域名搞混 |
| 发信失败,提示“发件人被拒绝” | 发件人邮箱域名未在 Resend 验证 | 去 Resend Domains 页面确认 DNS 验证状态是 Verified,然后在 Roundcube 身份里使用该域名的邮箱地址 |
| 一直停在登录页但自动登录不生效 | 插件没加载或配置没写入 | 检查 config.inc.php 里 plugins 数组是否包含 auto_login,检查 plugin 目录是否挂载,容器里执行 ls /var/www/html/plugins/auto_login |
| 界面显示英文 | 语言配置没生效 | 确认 language 是 zh_CN,且相关语言包存在;重启 Roundcube 容器 |
| 打开页面报数据库连接错误 | db_dsnw 配置不对 | 检查密码、用户名、主机名是否和数据库容器 env 一致,必要时 docker exec -it rcm-db mysql -u roundcube -p roundcubemail 手动验证 |
6.2 日志排查的正确姿势
遇到问题先看日志,别瞎改配置。Roundcube 容器的日志是最直接的线索:
IMAP 和 SMTP 的握手细节要看 Roundcube 自己的日志。在 config.inc.php 中可以开启更详细的日志:
开启后,IMAP 和 SMTP 的交互过程会被写到日志里。如果配置了 syslog 驱动,日志在容器内 /var/log 下;也可以改成 file 驱动,指定一个挂载目录方便外部查看。注意排障结束后要把 debug 关掉,否则日志文件会膨胀得很快。
另外,数据库容器日志也能帮忙判断问题。比如 Roundcube 连接不上数据库,docker logs rcm-db 里通常会有访问拒绝或连接数达到上限的记录。
6.3 几条实操避坑心得
第一,QQ 邮箱授权码不是一次设置永久安宁。如果你换了设备、在异地登录,或者 QQ 安全中心里主动重置了授权码,旧授权码立刻失效,Roundcube 会自动登录失败。这种时候别怀疑插件有 bug,先回 QQ 邮箱重新生成一个授权码,同步更新 config.inc.php 和插件配置。
第二,Resend 的 DNS 验证记录加完之后不会立刻生效,一般要等几分钟到几小时。在 Resend 后台确认 DOMAIN 状态变为 Verified 之后再发正式邮件,否则容易出现“看起来配置都对但发不出去”的情况。
第三,如果容器挂在 Nginx 反代后面,记得在 Nginx 里配置 X-Forwarded-Proto,并且在 Roundcube 里设置信任代理,否则 Roundcube 会认为当前请求是 HTTP,生成一堆 http 开头的绝对链接,邮件里的链接点开会跳去错误协议。
第四,自动登录这种插件一定要守住一个前提:只放到个人自用的项目里。如果你后面打算开放注册或多人共用,务必把插件停用,改成正常的用户密码登录。
整套系统跑稳之后,日常使用体验就是:浏览器打开一个书签,直接看到 QQ 邮箱收件箱的邮件列表,写新邮件时发件人是自己的域名邮箱,信发出去后还能在 Resend 后台看到详细的投递状态。这套组合的维护成本很低,Docker 容器升级只需要拉新镜像再 docker compose up -d,数据库和配置文件都留在宿主机目录里,重来一遍也不心疼。我个人折腾下来最满意的地方其实是自动登录那一下——省掉了每次翻授权码的烦躁感,这也算是自建邮箱最实打实的“体验升级”了。