No description
  • TypeScript 95.4%
  • Java 3.6%
  • JavaScript 0.3%
  • HTML 0.3%
  • CSS 0.2%
  • Other 0.2%
Find a file
Gjessing1 2d7d439fca
All checks were successful
docker / build-and-deploy (push) Successful in 2m24s
chore(deploy): move host deploy dir to ~/docker/<Category>/<svc>
2026-10-28 21:32:46 +00:00
.forgejo/workflows chore(deploy): move host deploy dir to ~/docker/<Category>/<svc> 2026-10-28 21:32:46 +00:00
.github/workflows chore(deploy): move host deploy dir to ~/docker/<Category>/<svc> 2026-10-28 21:32:46 +00:00
android feat(attachments): open, share and download image attachments 2026-09-24 21:01:54 +00:00
assets docs(readme): add app screenshots 2026-10-28 18:29:28 +00:00
backend fix(sweep): drop the staged .eml when a sweep insert throws 2026-10-04 17:54:20 +00:00
frontend chore(deps): patch security advisories via major bumps 2026-10-04 08:21:49 +00:00
mobile feat(android): add signed app distribution 2026-08-14 18:15:02 +00:00
scripts fix(android): publish the APK where the container actually reads it 2026-08-18 21:32:09 +00:00
shared feat(rules): protect rules shield whole senders from cleanup 2026-10-04 06:48:36 +00:00
.dockerignore chore: scaffold monorepo, tooling, Docker, and DB schema 2026-06-01 19:13:54 +00:00
.env.example chore(deploy): move host deploy dir to ~/docker/<Category>/<svc> 2026-10-28 21:32:46 +00:00
.gitignore feat(android): replace FCM with a self-hosted push stream 2026-08-19 18:15:27 +00:00
.nvmrc chore: migrate to Node 24 2026-06-03 08:42:20 +00:00
.prettierignore fix: align mail rendering with Gmail behavior 2026-08-14 19:24:19 +00:00
.prettierrc.json chore: scaffold monorepo, tooling, Docker, and DB schema 2026-06-01 19:13:54 +00:00
capacitor.config.json feat(android): add signed app distribution 2026-08-14 18:15:02 +00:00
docker-compose.yml chore(deploy): move host deploy dir to ~/docker/<Category>/<svc> 2026-10-28 21:32:46 +00:00
eslint.config.js fix: align mail rendering with Gmail behavior 2026-08-14 19:24:19 +00:00
package-lock.json chore(deps): patch security advisories via major bumps 2026-10-04 08:21:49 +00:00
package.json feat(android): add signed app distribution 2026-08-14 18:15:02 +00:00
README.md docs(readme): note app version for screenshots 2026-10-28 19:06:53 +00:00
tsconfig.base.json chore: scaffold monorepo, tooling, Docker, and DB schema 2026-06-01 19:13:54 +00:00

Maily

Maily is a self-hosted, mobile-first mail client. It runs as a browser PWA and as a private Capacitor Android app that connects to a selected Maily server.

Unified inbox across two accounts, unread mail grouped on top Reader with an extracted parcel-tracking card above the message Folder drawer in dark mode, grouped per account Cleanup dashboard with suggestion thresholds and protected mail
Unified inbox Parcel tracking, extracted Folders (dark mode) Cleanup

Screenshots: Maily 0.5.0 (commit 75ec4d9), taken 28 October 2026 with fictional demo data.

Initial Android download

Open this address on the Android device to download the current APK:

https://mail.gjessing.io/api/app/download

Android may ask you to allow the browser to install apps from this source. After installation, open Maily and confirm the server address https://mail.gjessing.io. The address is stored on the device and can later be changed under Settings → Android app.

The download returns 404 Not Found until the first signed APK has been published. The web app remains available at mail.gjessing.io.

TinyAuth, Pocket ID, and passkeys

The Android WebView keeps TinyAuth and its chained Pocket ID redirects inside the app. Passkey requests run in WebView's WebAuthn browser mode (APK 0.4.3 and later): the WebView builds the request for the Pocket ID page it is showing, so the passkey is asserted for https://pocketid.gjessing.io exactly as it would be in a browser. Bitwarden passkeys require Android 14 or later; in Bitwarden, open Settings → Autofill → Passkey management and select Bitwarden as the preferred passkey provider. Also keep Android System WebView current.

The first passkey login from the app stops with Bitwarden reporting that the browser (Maily) is not recognized. Tap Trust, then choose the passkey: Bitwarden adds the package and its signing certificate to its locally trusted privileged apps, and later logins go straight through. Trust belongs to the signing certificate, so an APK signed with a different key (a debug build, say) has to be trusted separately.

No /.well-known/assetlinks.json is involved. WebAuthn's app mode, which Digital Asset Links would authorise, asserts the passkey for the app's android:apk-key-hash: origin instead of the website's, and Pocket ID only accepts its own HTTPS origin — Bitwarden refuses app mode with "Passkeys not supported for this app" when the asset links are missing, and Pocket ID rejects the login when they are present.

Android releases

The APK hosts the current UI from the selected HTTPS Maily server, so ordinary UI/server deployments arrive without an APK update. Rebuild the APK when native code, permissions, signing, or icons change. The app checks /api/app/version and offers a newer published APK under Settings → Android app.

Create ~/.config/maily/keystore.env outside the repository:

export MAILY_KEYSTORE_FILE=/absolute/path/to/maily-release.jks
export MAILY_KEYSTORE_PASSWORD='...'
export MAILY_KEY_ALIAS='maily'
export MAILY_KEY_PASSWORD='...'

For each native release, increment versionCode and update versionName in android/app/build.gradle, then build and publish:

npm ci
./android/build-apk.sh
./scripts/publish-android.sh android/app/build/outputs/apk/release/app-release.apk 1 0.1.0

The publish script writes to /home/gjessing/data/maily/app by default — the host directory this deployment bind-mounts into the container as /data. The standard Docker volume exposes this as /data/app, which the backend serves at /api/app/version and /api/app/download. Set MAILY_ANDROID_PUBLISH_DIR to change the host publication path; set backend MAILY_ANDROID_APP_DIR if the APK directory is elsewhere inside its container.

For local development, npm run android:build:debug writes android/app/build/outputs/apk/debug/app-debug.apk. Never publish a debug-signed build: Android cannot install it over a release-signed app.

The frontend uses Capacitor's Android safe-area values, with browser env() fallbacks, so headers and bottom controls remain clear of the status and navigation bars.

Development

npm install
npm run build
npm run typecheck
npm test