Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 7 additions & 13 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Project Overview

mayl is an email-sending HTTP API backed by protonmail-bridge, running in a single Docker container. The container runs both protonmail-bridge (SMTP on localhost:1025) and the mayl Rust API server (HTTP on port 8080). noVNC on port 6080 provides browser access to the bridge GUI for Proton account login. All processes are supervised by runit (PID 1).
mayl is an email-sending HTTP API backed by protonmail-bridge, running in a single Docker container. The container runs both protonmail-bridge in headless/noninteractive mode (SMTP on localhost:1025) and the mayl Rust API server (HTTP on port 8080). All processes are supervised by runit (PID 1).

Emails can be sent synchronously or queued for background delivery. All sent emails are optionally archived in SQLite with automatic old-row culling. Domain-based token authentication controls who can send.

Expand Down Expand Up @@ -42,14 +42,8 @@ Rust edition 2024 requires a recent stable toolchain.
├── Dockerfile # Multi-stage: trixie runtime + rust builder + final
├── docker-compose.yml # Single service, 5 volumes
├── entrypoint.sh # One-time init (GPG, pass, dbus) then exec runsvdir
├── novnc.html # Minimal noVNC client page
├── sv/ # runit service directories
│ ├── xvfb/run # Virtual X display
│ ├── fluxbox/run # Window manager
│ ├── stalonetray/run # System tray for bridge icon
│ ├── x11vnc/run # VNC server
│ ├── websockify/run # noVNC WebSocket proxy
│ ├── bridge/run # protonmail-bridge (with lock cleanup)
│ ├── bridge/run # protonmail-bridge --noninteractive (with lock cleanup)
│ └── mayl/run # mayl API server
├── src/
│ └── main.rs # Entire application (~970 lines)
Expand Down Expand Up @@ -106,7 +100,7 @@ Access serialized via `tokio::sync::Mutex<Connection>`.

## Process Supervision

runit (`runsvdir`) runs as PID 1. The entrypoint does one-time setup (GPG key generation, pass init, D-Bus) then `exec runsvdir /etc/service`. Each service in `sv/` has a `run` script; runit auto-restarts any that exit. Services that depend on X wait for `/tmp/.X99-lock`. The bridge service cleans stale lock files before each start.
runit (`runsvdir`) runs as PID 1. The entrypoint does one-time setup (GPG key generation, pass init, D-Bus) then `exec runsvdir /etc/service`. Each service in `sv/` has a `run` script; runit auto-restarts any that exit. The bridge service cleans stale lock files before each start. Initial Proton account login is done via `docker exec -it mayl protonmail-bridge --cli`.

## Key Design Decisions

Expand All @@ -117,7 +111,7 @@ runit (`runsvdir`) runs as PID 1. The entrypoint does one-time setup (GPG key ge
- **`dangerous_accept_invalid_certs(true)`:** Bridge uses self-signed TLS. Must use `TlsParameters::builder().dangerous_accept_invalid_certs(true)` then `AsyncSmtpTransport::builder_dangerous()` with `Tls::Required(tls_params)`.
- **Domain token auth:** `POST /domains` creates a domain + UUID token. `POST /email` validates the Bearer token matches the `from` domain.
- **Background workers:** `queue_worker` and `archive_culler` run as `tokio::spawn` tasks.
- **Custom noVNC page:** Minimal `novnc.html` using noVNC core `RFB` module directly (the packaged `vnc.html` UI is broken in trixie).
- **Headless bridge:** Bridge runs with `--noninteractive`. Initial login via `docker exec -it mayl protonmail-bridge --cli`.

## Testing

Expand All @@ -142,6 +136,6 @@ All tests use in-memory SQLite. No SMTP or Docker required.
- Rust 2024 edition: `std::env::remove_var` is unsafe. Tests avoid it.
- maud 0.27 required for axum 0.8 compatibility (0.26 uses axum-core 0.4, needs 0.5).
- Bridge Dockerfile: use `apt-get install -y /tmp/bridge.deb` (NOT `dpkg -i || apt-get -yf` which removes the package).
- Bridge runs with GUI (no `--noninteractive`) so it's visible/usable in noVNC.
- Stale X lock (`/tmp/.X99-lock`) and bridge locks must be cleaned on service start to survive container stop/start cycles.
- Base image is `debian:trixie-slim` (not bookworm) because `stalonetray` and OpenGL libs needed by bridge-gui are only in trixie.
- Bridge runs with `--noninteractive` (headless). Initial account login via `docker exec -it mayl protonmail-bridge --cli`.
- Bridge lock files must be cleaned on service start to survive container stop/start cycles.
- Base image is `debian:trixie-slim` (not bookworm) because OpenGL/Qt libs needed by bridge are only in trixie.
17 changes: 1 addition & 16 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,7 @@ RUN apt-get update && apt-get install -y \
pass \
dbus \
dbus-x11 \
xvfb \
x11vnc \
fluxbox \
stalonetray \
novnc \
websockify \
gnome-keyring \
python3-gi \
gir1.2-secret-1 \
libegl1 \
libgl1 \
Expand Down Expand Up @@ -65,21 +58,13 @@ FROM runtime

COPY --from=builder /app/target/release/mayl /usr/local/bin/mayl
COPY entrypoint.sh /entrypoint.sh
COPY novnc.html /novnc.html
COPY sv/ /etc/sv/
RUN chmod +x /entrypoint.sh \
&& chmod +x /etc/sv/*/run \
&& ln -s /etc/sv/xvfb /etc/service/xvfb \
&& ln -s /etc/sv/fluxbox /etc/service/fluxbox \
&& ln -s /etc/sv/stalonetray /etc/service/stalonetray \
&& ln -s /etc/sv/x11vnc /etc/service/x11vnc \
&& ln -s /etc/sv/websockify /etc/service/websockify \
&& ln -s /etc/sv/bridge /etc/service/bridge \
&& ln -s /etc/sv/mayl /etc/service/mayl

ENV DISPLAY=:99

EXPOSE 6080 8080
EXPOSE 8080

VOLUME ["/root/.config/protonmail", "/root/.local/share/protonmail", "/root/.gnupg", "/root/.password-store", "/data"]

Expand Down
33 changes: 16 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ token authentication controls who can send.
│ ┌─────────────────────────────────┐ │
│ │ protonmail-bridge │ │
│ │ localhost:1025 (SMTP) │ │
│ │ Xvfb + Fluxbox + noVNC :6080 │ │
│ │ --noninteractive (headless) │ │
│ └──────────────┬──────────────────┘ │
│ │ SMTP │
│ ┌──────────────▼──────────────────┐ │
Expand All @@ -29,13 +29,12 @@ token authentication controls who can send.
│ │
│ runit (PID 1) supervises all services │
└──────────────────────────────────────┘
:8080 :6080
HTTP API VNC (browser)
:8080
HTTP API
```

A single container runs both **protonmail-bridge** and the **mayl** API.
Bridge provides SMTP on localhost:1025; mayl connects to it directly. noVNC
on port 6080 lets you log in to your Proton account through a browser.
A single container runs both **protonmail-bridge** (headless) and the **mayl** API.
Bridge provides SMTP on localhost:1025; mayl connects to it directly.
[runit](https://smarden.org/runit/) supervises all processes, handling signal
forwarding, automatic restarts, and clean shutdown.

Expand All @@ -47,11 +46,17 @@ forwarding, automatic restarts, and clean shutdown.
docker compose up -d --build
```

### 2. Log in to Protonmail Bridge via VNC
### 2. Log in to Protonmail Bridge

Open [http://localhost:6080](http://localhost:6080) in your browser. You will
see a desktop with the Protonmail Bridge GUI. Sign in with your Proton account
credentials and note the SMTP username and password that Bridge generates.
Run the bridge CLI to log in to your Proton account:

```bash
docker exec -it mayl protonmail-bridge --cli
```

Use the `login` command to authenticate with your Proton credentials. Note the
SMTP username and password that Bridge generates. This is a one-time step;
credentials persist across container restarts via Docker volumes.

### 3. Configure SMTP credentials

Expand Down Expand Up @@ -234,12 +239,7 @@ runit automatically restarts any service that exits.

| Service | Description |
|--------------|-------------|
| `xvfb` | Virtual X display (:99) |
| `fluxbox` | Window manager |
| `stalonetray`| System tray (for bridge icon) |
| `x11vnc` | VNC server on :5900 |
| `websockify` | WebSocket proxy (noVNC on :6080) |
| `bridge` | protonmail-bridge with GUI |
| `bridge` | protonmail-bridge (headless, `--noninteractive`) |
| `mayl` | mayl HTTP API |

The entrypoint script runs one-time init (GPG key, pass, D-Bus) then
Expand All @@ -250,7 +250,6 @@ The entrypoint script runs one-time init (GPG key, pass, D-Bus) then
| Port | Service |
|--------|------------------|
| `8080` | mayl HTTP API |
| `6080` | noVNC (browser) |

## Volumes

Expand Down
1 change: 0 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ services:
container_name: mayl
ports:
- "8080:8080"
- "6080:6080"
environment:
- MAYL_SMTP_HOST=localhost
- MAYL_SMTP_PORT=1025
Expand Down
31 changes: 0 additions & 31 deletions novnc.html

This file was deleted.

3 changes: 1 addition & 2 deletions sv/bridge/run
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
#!/bin/sh
while [ ! -e /tmp/.X99-lock ]; do sleep 0.2; done

# Clean up stale lock files from previous runs
rm -f /root/.cache/protonmail/bridge-v3/bridge-v3-gui.lock
Expand All @@ -8,4 +7,4 @@ rm -f /root/.cache/protonmail/bridge-v3/bridge-v3.lock
# Source dbus session address written by entrypoint
[ -f /run/dbus-session-env ] && . /run/dbus-session-env

exec protonmail-bridge
exec protonmail-bridge --noninteractive
4 changes: 0 additions & 4 deletions sv/fluxbox/run

This file was deleted.

3 changes: 0 additions & 3 deletions sv/stalonetray/run

This file was deleted.

8 changes: 0 additions & 8 deletions sv/websockify/run

This file was deleted.

3 changes: 0 additions & 3 deletions sv/x11vnc/run

This file was deleted.

3 changes: 0 additions & 3 deletions sv/xvfb/run

This file was deleted.

Loading