自建邮件推送 + 收发系统:服务器盯着多个 IMAP 邮箱,新邮件经 FCM 推送到自写的安卓客户端; 客户端只跟服务器的私有 API 通信,读正文 / 回信 / 发信等 IMAP/SMTP 操作全在服务器完成。 手机不存任何邮箱凭据、不维持持久 IMAP 连接(只靠系统级 FCM 连接,省电)。
┌──────────── 服务器(Docker,常驻插电)────────────┐
Gmail / 其他 IMAP ◄┤ goimapnotify(IMAP IDLE 监听)─新邮件─► push-fcm ──FCM──► 安卓 app(弹通知)
(邮件留在原 provider)│ imaplib 连接池(读列表/正文/附件/回复模板) ◄ FastAPI ─┐ │
│ smtplib(发信/回信) │◄── 私有 JSON/multipart API ──► 安卓 app(读/写/回)
└──────────────────────────────────────────────────┘
- 监听:goimapnotify(纯 RFC2177 IDLE,兼容 Gmail)
- 引擎:Python
imaplib持久连接池(读列表/正文/附件 + 生成回复转发模板) —— 0.2.0 起取代原 himalaya CLI 后端(见下「变更日志」) - 发信:Python smtplib(直连 SMTP,不碰 IMAP)
- API:FastAPI(bearer 鉴权;须置于 HTTPS 之后)
- 管理后台:独立的内网 web 界面(密码登录,图形化管理邮箱账户/app token/OAuth + 看状态),独立端口、仅限局域网;无桌面的 VPS 可用
server/admin-cli.sh命令行做同样操作 - 推送:FCM HTTP v1 + Firebase Admin SDK(data message,多设备)
- 客户端:Kotlin + Jetpack Compose + Material 3
| 路径 | 内容 |
|---|---|
server/ |
服务端(Docker 镜像:goimapnotify + FastAPI + Python imaplib/smtplib)。部署见 server/README.md |
android/ |
安卓客户端(Compose)。构建见 android/README.md |
mail-push-relay-design.md |
整体设计文档 |
docker-design.md |
Docker / ZimaOS 部署设计 |
- 多账号收件箱 + 统一收件箱(跨账号合并,各账户并发拉取)
- 多种邮箱:Gmail / Yahoo / 网易 163·126 等用应用专用密码;Outlook / Microsoft 365 / Hotmail 走 OAuth2(微软已停用密码登录,见快速上手)
- 未读数:统一收件箱顶部显示总未读;账户列表每个账户显示各自未读(服务端 IMAP
SEARCH UNSEEN全量统计) - 查看已发邮件(自动探测各账号的「已发」文件夹,收件箱顶部一键切换)
- 置顶(pin)邮件:右滑置顶,固定在收件箱与统一收件箱顶部;置顶内容单独缓存,不被自动清理;邮件在服务器端被删或取消置顶时自动移除
- 下拉刷新 + 上拉无限加载;未读/已读区分(打开即标已读)
- HTML 正文用 WebView 渲染(默认禁远程图防跟踪,可一键显示);跟随系统暗色主题
- 查看 / 下载附件(保存到设备)
- 写信 / 回复 / 回复全部 / 转发,均支持附件(转发自动带原附件);收件人可从手机通讯录选
- 删除邮件(列表左滑 / 详情页,带确认,同步服务器移回收站)
- 详情页可展开看完整发件人/收件人地址
- 离线缓存:无网时可看已加载的收件箱与读过的正文;正文缓存有容量上限自动回收(置顶豁免)
- FCM 推送,多设备自动注册(token 自动上报);点通知直达对应邮件
- 内网管理后台(独立端口):图形化管理邮箱账户(增删改收发+推送账户,选 provider 预设自动填 host/port,直接在网页里设密码;每个账户可单独决定是否开推送)、管理多个 app token(每台设备一个;删某 token 连带切断其推送设备,丢设备一键同时断「读信 + 推送」)、OAuth 授权(Outlook 等);查看版本/运行状态、管理推送设备;app 端 token 输入框掩码显示
服务端用官方预构建镜像免构建直接拉;只有安卓 app 需要你本地编译(因为要放你自己的 Firebase 配置)。
前置:Docker + docker compose;编译 app 需 Android Studio 或 Android SDK(./gradlew);一个免费 Firebase 项目,从中拿两个文件——服务端发 FCM 用的 service-account.json、安卓 app 用的 google-services.json(两者必须同一个 Firebase 项目)。
# 1) 克隆
git clone https://github.com/bidabrain/mailpush && cd mailpush
# 2) 编译安卓 app(放入你自己的 google-services.json 覆盖占位文件)
cp /path/to/google-services.json android/app/google-services.json
( cd android && ./gradlew assembleDebug ) # 产物:android/app/build/outputs/apk/debug/app-debug.apk → 装到手机
# 3) 服务端:准备密钥(都放 server/config,已被 .gitignore 忽略)
cd server
mkdir -p config/secrets data
cp .env.sample .env
# 编辑 .env:取消注释 CONFIG_DIR=./config 与 DATA_DIR=./data(让配置落在 server/config、server/data)
printf '管理后台密码' > config/secrets/admin.pass # 内网管理后台登录用(必需)
cp /path/to/service-account.json config/service-account.json # Firebase 服务端密钥(发推送必需)
# 邮箱账户:本步【先不用配】—— 起服务后在管理后台网页里加(方式 B,见下);
# 想用文件配置的看「两种配置账户的方式 · 方式 A」。
# 4) 拉官方镜像并运行(docker-compose.dist.yml 默认就指向 bidabrain/mailpush:latest)
docker compose -f docker-compose.dist.yml pull
docker compose -f docker-compose.dist.yml up -d
# 5) 内网浏览器开 http://<服务器内网IP>:8098,用管理密码登录:
# - 「邮箱账户」加你的邮箱(填地址+应用专用密码,选 provider 预设)→ 立刻能收发,几秒内起推送
# - 「App Token」给每台手机「新建 Token」→ app 设置里填:服务器地址 + 该 Token
# (服务器地址生产环境务必走 HTTPS,见「安全」)完成后:发封测试邮件,手机应收到 FCM 推送;点开能读正文、回信、发信。
- 没有图形界面?(VPS / 纯 SSH):管理后台只绑本机、打不开浏览器时,用
server/admin-cli.sh在命令行做与网页完全一样的操作(登录、增删邮箱账户 / app token、OAuth 授权等)。见server/README.md的「命令行管理后台」。 - 架构匹配:官方镜像是
linux/amd64。arm64 机器(树莓派等)需自行buildx构建,详见server/README.md。 - 自己构建服务端:
docker compose up -d --build,或buildx --push发到自己的 Docker Hub,见server/README.md。 ⚠️ 上线前务必读「安全」:API(8099)要放 HTTPS 之后,管理后台(8098)只能内网。
收发邮箱账户有两种配置途径,互相兼容、可混用:同名时以 webui 为准,异名各自生效。
| 方式 A · 手写配置文件 | 方式 B · webui 管理后台(:8098) | |
|---|---|---|
| 存放 | config/config.toml + config/imapnotify.yaml + config/secrets/*.pass(/config,只读挂载) |
/data/accounts.json + /data/secrets/<name>.pass(可写,网页里增删改) |
| 加账户 | 复制 *.sample 改账号,改完重启 mail-api/mail-watch |
网页「邮箱账户」填表保存,收发即时生效,推送自动重启监听 |
| 设密码 | 写进 secrets/<name>.pass |
网页密码框直接填(编辑时留空=不改) |
| 只收发不推送 | config 写、imapnotify 不写 | 取消勾选「启用推送监听」 |
| 适合 | 批量/版本化/CI、想把配置纳入 git | 日常增删、不想 SSH、图形化操作 |
- 方式 B 的底层:mail-api 读
config.toml后把 webui 账户叠加进来(accounts.py的overlay_config);mail-watch 用render_imapnotify.py把 webui 账户与手写 base 合并成/data/imapnotify.generated.yaml再跑 goimapnotify,改动经/data/watch.reload触发自动重启。/config始终只读,安全边界不变。 - 可以完全不写 config 文件:只放
admin.pass+service-account.json就能起服务,账户全在 webui 加(零账户时 mail-watch 空转等待,不报错)。 - 详见
server/README.md。
微软已对 Outlook.com 个人账户与多数 M365 停用密码登录(Basic Auth) —— 没有"应用专用密码"这条路,只能走 OAuth2。Gmail / Yahoo / 网易等不受影响,仍用上面的 .pass;只有微软账户要多做下面这套(一次性,约 10 分钟):
- Azure 免费注册一个应用:用那个 Outlook 邮箱登录 entra.microsoft.com → App registrations → New registration:
- 账户类型选**含「个人 Microsoft 账户」**的那项(outlook.com 个人号必需);
- Authentication → Allow public client flows = Yes(设备码流需要);
- API permissions 加**委托(Delegated)**权限:
IMAP.AccessAsUser.All、SMTP.Send、offline_access; - 记下 Application (client) ID(不要建 client secret)。
- 在内网管理后台授权(最省事):浏览器开
http://<内网IP>:8098→ 「OAuth」区填账号名(如outlook)+ 上面的 client_id → 保存 → 点「授权」→ 按提示在浏览器输设备码、用 Outlook 账户登录同意。凭据会自动存到/data/oauth/(refresh token 后续自动静默刷新)。 - 加账号(任选其一,见上「两种配置账户的方式」):
- 方式 B(推荐):授权后,在管理后台「邮箱账户」用相同账号名添加一条(provider 选 Outlook、鉴权选 OAuth2、密码留空),保存即生效;
- 方式 A:在
config.toml与imapnotify.yaml各加一个 Outlook 账号块(照*.sample的 Outlook 段:auth.type = "oauth2"/xoauth2: true,auth.cmd与passwordCMD都调oauth_token.py),重启mail-api与mail-watch。 ⚠️ 邮箱地址必须填真实域名:个人号可能是@live.com/@hotmail.com/@outlook.com任一;域名写错会报User is authenticated but not connected(OAuth 授权能成功、token 也有效,但 IMAP 按错地址找不到邮箱)。host统一是outlook.office365.com,只地址要对。
安卓 app 无需任何改动 —— Outlook 只是多一个账号。详细 Azure 步骤与排错见 server/README.md。
详细说明见 server/README.md 与 android/README.md。
仓库根的 VERSION 文件是唯一版本号来源,server 和 app 构建时都从它读取,二者版本号保持一致。
- 安卓 app:
android/app/build.gradle.kts在构建时读VERSION→ 自动设为versionName,并由x.y.z推导出versionCode(x*10000 + y*100 + z)。正常./gradlew assembleDebug/installDebug即可,无需手改。 - 服务端:版本经 Docker build-arg 注入镜像(环境变量
MAILPUSH_VERSION),并由GET /version、GET /healthz暴露。构建时带上 build-arg:(不传 build-arg 时默认# buildx 发布镜像 docker buildx build --platform linux/amd64 \ --build-arg APP_VERSION=$(cat ../VERSION) \ -t <user>/mailpush:latest --push . # 本地 compose 构建 export APP_VERSION=$(cat ../VERSION) && docker compose up -d --build
dev,不影响运行。) - app 设置页会显示当前 app 版本与所连服务器的版本,方便核对两端是否一致。
升版本:改 VERSION 一个文件 → 重新构建 app 与镜像即可。
- 管理后台可视化管理邮箱账户(收发 + 推送):不再必须手写
config.toml/imapnotify.yaml。- 新增
app/accounts.py:webui 账户存可写的/data/accounts.json+/data/secrets/<name>.pass,overlay_config()把它们叠加进 himalaya schema 供 mail-api 读(现读现生效);提供 provider 预设(Gmail/Outlook/Yahoo/网易/Fastmail),网页里直接设密码; - 新增
app/render_imapnotify.py:把 webui 账户与/config的手写 base 合并渲染成/data/imapnotify.generated.yaml;watch-entrypoint.sh监听/data/watch.reload,改账户后自动重启 goimapnotify,零账户时空转等待不报错; - 每个账户可单独开关推送(只用 API 收发、不进推送监听);
- 两种配置方式可并存:
/config始终只读、手写账户作为 base 兼容保留,同名时 webui 覆盖;可完全不写 config 文件、纯靠 webui 起服务; imap_pool/mailsend/api.py读配置处统一走 overlay 并容忍config.toml缺失;新增依赖pyyaml。
- 新增
- 接入 Outlook / Microsoft 365 / Hotmail(OAuth2 / XOAUTH2):微软停用密码登录后唯一可行路径。
- 新增
app/oauth.py(MSAL 设备码授权 + 静默刷新,flock防两容器并发)、oauth_token.py(输出裸 access token,供 config 的auth.cmd/ imapnotify 的passwordCMD)、oauth_enroll.py(CLI 一次性授权); imap_pool(收/读)与mailsend(发信)增加auth.type=oauth2分支走 SASLXOAUTH2;监听端 goimapnotify 用xoauth2: true原生支持;- 内网管理后台新增「OAuth」区:填 client_id、点「授权」走设备码流(无需 SSH 进容器);凭据存可写的
/data/oauth/; - 现有密码账户零改动(Gmail/Yahoo/网易仍
auth.type=password);安卓端零改动。
- 新增
- 收信引擎从 himalaya(Rust CLI)切换为 Python
imaplib持久连接池(server/app/imap_pool.py+imap_client.py)。动机与收益:- 提速:连接复用,免去每次新建连接的握手开销(对会做反向 DNS、新连接要等数十秒的 IMAP 服务器尤其明显)。
- 解锁网易 163/126 读信:在代码层登录后发 IMAP ID(RFC 2971),解决 himalaya v1.2.0 不支持 ID 导致的读信失败。
- 构建更快更小:去掉 Rust 编译阶段,Docker 镜像构建从约 20 分钟降到约 1 分钟。
- 零迁移:
config.toml等用户配置与 API 形状不变,无需改配置、安卓端无需改动。
- 安卓端体验:
- 统一收件箱并发拉取各账户(整轮耗时≈最慢账户,而非各账户之和);
- 未读数:统一收件箱顶部总未读 + 账户列表各账户未读(新增
/unread、/unread-all端点); - 统一/单账户共享新鲜度:统一刷过的账户点进去不再重复拉;
loadUnified防并发 + 新鲜度跳过; - 双击底部「统一」图标回到列表顶部;根页连按两次返回彻底退出;
- 「已发」文件夹探测失败可自愈(不缓存 null,下次重试)。
- 内网管理后台(mail-admin)+ 多 app token:
- 新增独立容器
mail-admin(默认端口 8098,仅限内网),密码登录后可新建/删除多个 app token(每台设备一个,丢设备直接吊销该 token,无需改配置/全量轮换),并显示版本与运行状态; - 推送设备与 app token 绑定:FCM 推送 token 记录其注册时所用的 app token;后台删 app token 会连带删除其名下推送设备 → 丢设备一键同时切断「读信」和「推送」(否则旧 FCM token 仍会把发件人/主题推到丢失设备);后台也可单删/清空推送设备;
require_auth接受后台管理的多 token,兼容旧的单/config/api-token;默认不再预设固定 token(走后台管理);- 关闭 FastAPI 自动文档(
/docs、/redoc、/openapi.json),减少接口结构泄露; - app 端「API Token」输入框改为掩码显示。
- 新增独立容器
- himalaya 已从代码与镜像中移除。
- 首个 beta:goimapnotify 监听 + himalaya 引擎 + smtplib 发信 + FCM 推送 + 安卓客户端。
凭据(邮箱密码、token、Firebase 私钥、管理密码)只存服务器/本机,不进 Git(见 .gitignore)。
两个端口,安全要求完全不同,务必分清:
- 🔒 API(mail-api,默认 8099)必须放在 HTTPS 之后。它能读全部邮件、以你名义发信,且 uvicorn 跑的是明文 HTTP——直接裸暴露公网会让 bearer token 和邮件内容被明文嗅探。用 Cloudflare Tunnel(提供 HTTPS + 出站连接)对外暴露,严禁把 8099 经路由器端口转发直连公网。token 用强随机值(后台生成的即是),走 HTTPS 传输。
- 🚫 管理后台(mail-admin,默认 8098)只能内网访问,绝不可暴露公网。它能增删 token、看状态,是你的"钥匙串管理处"。不要给它配 Cloudflare Tunnel、不要在路由器上转发 8098。只想本机访问可在 compose 把端口写成
127.0.0.1:8098:8098。
多 token + 吊销:给每台设备建独立 token;设备丢失时内网登录后台删除该 token 即可(即时生效,不影响其他设备),无需全量轮换。 注意:后台仅内网 → 人在外面丢手机需回到局域网(或 VPN 回家)才能吊销。
服务端 + 安卓主体功能均已实现并跑通(本地编译 / 真机验证)。
本项目站在这两个优秀开源项目的肩膀上,在此致谢:
- goimapnotify —— 纯 RFC 2177 IMAP IDLE 监听,新邮件实时触发;兼容 Gmail,无 QRESYNC 依赖。本项目用它做推送监听端。
- himalaya(Pimalaya)—— 命令行 IMAP/SMTP 客户端。本项目 0.1.x 的邮件引擎(读列表 / 读正文 / 列附件 / 生成回复转发模板);0.2.0 起改用 Python
imaplib自实现连接池,不再依赖 himalaya。感谢它在早期阶段让项目快速跑通。
感谢以上项目的作者与维护者。