mirror of
https://github.com/karatektus/pl_mail.git
synced 2026-10-25 01:54:10 +00:00
Offer a push service, so Android does not have to mean Google
Web Push assumes a push service: something that owns the endpoint URL, holds the connection to the device and receives our encrypted POST. Browsers ship one. A native Android app does not, and Android's own is FCM, which speaks its own protocol and cannot be reached by WebPushSender at all. That left a self-hoster with a gap this repository was well placed to close and did not: to get push without Firebase they had to find and run a push service themselves, having already been asked to self-host their mail. So ntfy is available here, behind a `push` profile and off by default: NTFY_BASE_URL=https://push.example.com docker compose --profile push up -d ntfy It adds no application code. ntfy is simply a push service speaking the protocol WebPushSender already emits -- RFC 8030 transport, RFC 8291 payload encryption, RFC 8292 VAPID -- so a UnifiedPush distributor on the phone can receive what this server already sends today. Nobody who does not want push pays for a container they never start. Two settings are worth reading before exposing it. NTFY_BASE_URL is the URL *phones* use, so it must be reachable from outside this machine, and it is baked into every endpoint issued -- changing it later forces every device to re-register. And access is open by default, which is a considered choice rather than an oversight: UnifiedPush topics are random and unguessable, and payloads are encrypted to the device's own key before they arrive, so this service cannot read mail and someone who guessed a topic could only send noise the app discards. The comment says how to lock it down for an internet-facing deployment. Verified end to end against the isolated Android test stack: the app registers with the distributor, ntfy issues an endpoint, the app registers it through PushSubscription/set, this server POSTs its encrypted PushVerification there, the connector decrypts it and echoes the code back. PushSubscription/get then returns the subscription with no verificationCode, which is what live means. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6edf33fed9
commit
37cf7ab254
2 files changed
+86
No files matched your search
@@ -200,6 +200,60 @@ services:
|
||||
- mercure_config:/config
|
||||
- app_secrets:/run/plmail:ro
|
||||
|
||||
# Push for Android, without Google.
|
||||
#
|
||||
# Optional and off by default: `docker compose --profile push up -d`. Nobody
|
||||
# who does not want push pays for a container they never use.
|
||||
#
|
||||
# This needs no plMail code at all. The app already speaks Web Push — RFC 8030
|
||||
# transport, RFC 8291 payload encryption, RFC 8292 VAPID signing — and ntfy is
|
||||
# a push service that speaks exactly that. What it adds is the missing half on
|
||||
# Android: RFC 8030 requires a *push service* that owns the endpoint URL and
|
||||
# holds the connection to the device, browsers ship one and native apps do
|
||||
# not. A UnifiedPush distributor on the phone talks to this.
|
||||
#
|
||||
# The alternative for an Android user is Firebase, which means Google sees
|
||||
# that a message arrived and when. Running this instead keeps that on your own
|
||||
# hardware, which is the reason to self-host mail in the first place.
|
||||
ntfy:
|
||||
image: binwiederhier/ntfy
|
||||
profiles: [push]
|
||||
restart: unless-stopped
|
||||
command: serve
|
||||
environment:
|
||||
# The URL *phones* will use, so it has to be reachable from outside this
|
||||
# machine — a LAN address, a Tailscale name, or a public hostname. It is
|
||||
# baked into every endpoint handed out, so changing it later invalidates
|
||||
# every existing subscription and every device has to re-register.
|
||||
NTFY_BASE_URL: ${NTFY_BASE_URL:-http://localhost:8090}
|
||||
NTFY_LISTEN_HTTP: ':80'
|
||||
NTFY_CACHE_FILE: /var/lib/ntfy/cache.db
|
||||
NTFY_AUTH_FILE: /var/lib/ntfy/auth.db
|
||||
# Open by default, and that is a considered choice rather than laziness.
|
||||
# UnifiedPush topics are random and unguessable, and the payload is
|
||||
# encrypted to the device's own key before it ever reaches here — so this
|
||||
# service cannot read your mail, and someone who guessed a topic could
|
||||
# only send noise the app fails to decrypt and discards.
|
||||
#
|
||||
# To lock it down anyway, set this to deny-all and create a user:
|
||||
# docker compose exec ntfy ntfy user add --role=admin plmail
|
||||
# then give the phone that login in the ntfy app. It is the right move if
|
||||
# this is exposed to the internet rather than reached over a VPN.
|
||||
NTFY_AUTH_DEFAULT_ACCESS: ${NTFY_AUTH_DEFAULT_ACCESS:-read-write}
|
||||
# A personal server has a handful of devices; the defaults are sized for a
|
||||
# public instance and would let one client exhaust it.
|
||||
NTFY_VISITOR_REQUEST_LIMIT_BURST: '60'
|
||||
NTFY_VISITOR_REQUEST_LIMIT_REPLENISH: '5s'
|
||||
ports:
|
||||
- "${NTFY_PORT:-8090}:80"
|
||||
volumes:
|
||||
- ntfy_data:/var/lib/ntfy
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "-qO-", "http://localhost/v1/health"]
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 20s
|
||||
|
||||
volumes:
|
||||
app_secrets:
|
||||
caddy_data:
|
||||
@@ -207,3 +261,4 @@ volumes:
|
||||
database_data:
|
||||
mercure_data:
|
||||
mercure_config:
|
||||
ntfy_data:
|
||||
@@ -449,6 +449,37 @@ you can create a Web Push subscription; RFC 8620 defines no standard place for i
|
||||
Note `capabilities` advertises the push URN but the *supported* `using` list is Core, Mail and
|
||||
Submission only. Do not put the push URN in `using`.
|
||||
|
||||
#### Push on Android, without Google
|
||||
|
||||
Web Push assumes a **push service**: something that owns the endpoint URL, holds the connection to
|
||||
the device and receives the server's encrypted POST. Browsers ship one. A native Android app does
|
||||
not, and Android's own service is FCM, which speaks its own protocol — `WebPushSender` cannot POST
|
||||
to it.
|
||||
|
||||
So an Android client has three options, and only the first needs nothing from this server:
|
||||
|
||||
1. **UnifiedPush.** The user installs a *distributor* app; it supplies an RFC 8030 endpoint and
|
||||
decrypts the RFC 8291 `aes128gcm` payload this server already sends. No server change at all.
|
||||
2. **Firebase.** Nothing for the user to install, and what most Android users expect — but it needs
|
||||
a Firebase project and an FCM sender here, and Google then learns that a message arrived and
|
||||
when.
|
||||
3. **An embedded distributor**, where the app holds the socket itself. Costs a foreground service
|
||||
and a permanent notification, per app.
|
||||
|
||||
For (1) this repository can supply the push service too, so a self-hoster does not have to find one:
|
||||
|
||||
```bash
|
||||
NTFY_BASE_URL=https://push.example.com docker compose --profile push up -d ntfy
|
||||
```
|
||||
|
||||
It is off by default and adds no plMail code — ntfy is simply a push service that speaks the
|
||||
protocol `WebPushSender` already emits. `NTFY_BASE_URL` must be reachable **from the phone**, and is
|
||||
baked into every endpoint issued, so changing it later forces every device to re-register.
|
||||
|
||||
Payloads are encrypted to the device's own key before they reach it, so the push service cannot read
|
||||
mail whichever one you use. It does learn *when* mail arrives, which is the argument for running
|
||||
your own rather than a public one.
|
||||
|
||||
### The API endpoint
|
||||
|
||||
```http
|
||||
|
||||
Reference in new issue
Block a user