Skip to content
socrtwoPublic

About

Android email client: downloads IMAP/POP3 mail, composes new messages with a standard formatting toolbar, and routes incoming mail into folders (e.g. Spam) via user-defined rules.

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

quillbox

Email client for Android, desktop, web and iOS: downloads IMAP/POP3 mail, composes with a formatting toolbar, and keeps junk out of the Inbox with open DNS blocklists, an offline brand-impersonation detector, a learning classifier and user rules. The web client is laid out like Microsoft 365 Outlook.


Quillbox (Android app)

Quillbox for Android is the Outlook-style client packaged as an app. The APK contains the same mail engine and the same interface as the web and desktop versions: at start-up the app launches the backend on 127.0.0.1 inside its own process and shows the UI full-screen in a WebView. Nothing leaves the phone except the connections to your own mail provider, the DNS blocklist lookups and the provider autodiscovery, exactly as on the desktop.

  • Application ID: info.socrtwo.quillbox
  • minSdk: 26 (Android 8.0) · compile/target SDK: 35 (Android 15)
  • Also runs on ChromeOS (install the APK) and on Android tablets, where the layout becomes folders + list with the reading pane sliding in, like Outlook on an iPad.

What the Android shell adds on top of the shared UI:

  • the back button closes dialogs, the folder drawer and the open message before leaving the app;
  • attachments are saved to Downloads/Quillbox and offered to a viewer app; compose attachments use the system file picker;
  • links in messages open in the browser, mailto: links from other apps open the composer;
  • Print uses Android's print service; the theme follows the system light/dark setting;
  • the account (with its password, when Keep me signed in is ticked) is stored in the app's private WebView storage; rules, settings and the learned classifier live under the app's private files directory.

Tech stack

Concern Choice
Language / build Kotlin, Gradle Kotlin DSL, Gradle wrapper 8.13
UI The web client (web/src/main/resources/web) in a WebView
Backend Ktor 3 (CIO engine) compiled from web/src/main/kotlin
Mail protocols Jakarta Mail — org.eclipse.angus:angus-mail (IMAP/POP3/SMTP)
Junk engine Shared with web/desktop (web/src/main/kotlin/.../spam)

Project layout

app/src/main/java/info/socrtwo/quillbox/
├── MainActivity.kt     // WebView shell: back button, downloads, file picker, links, printing
└── LocalServer.kt      // starts the shared Ktor backend on 127.0.0.1 (stable port, app data dir)
web/src/main/kotlin/    // the backend and junk engine, compiled into the APK as-is
web/src/main/resources/web/   // index.html, styles.css, app.js — the UI, packaged into the APK

Build the debug APK

Requires JDK 17+ and the Android SDK (platform 35, build-tools). The Gradle wrapper is committed, so:

./gradlew assembleDebug

The sideloadable debug APK is written to:

app/build/outputs/apk/debug/app-debug.apk

Install it on a device with adb install app/build/outputs/apk/debug/app-debug.apk.

CI: .github/workflows/android.yml builds assembleDebug on every push and uploads app-debug.apk as a build artifact (the quillbox-debug-apk artifact).

Web client (Outlook-style, with built-in junk protection)

web/ is a self-hosted web mail client: a Ktor backend that talks IMAP/POP3/SMTP on your behalf, plus a browser front end laid out like Microsoft 365 Outlook (folder pane, message list, reading pane, command bar, keyboard shortcuts, dark mode, responsive down to phone width).

Run it

cd web
./gradlew run            # http://localhost:8080

Environment variables: PORT (default 8080), HOST (default 0.0.0.0), QUILLBOX_DATA_DIR (default ~/.quillbox; holds per-account rules, settings, the learned classifier and the analysis cache — never passwords).

To try it without a real mailbox:

cd web
./gradlew demo           # in-memory IMAP/SMTP seeded with genuine, spoofed and blacklisted mail

then sign in as demo@quillbox.test / demo and, under Server settings, use IMAP 127.0.0.1:3143 (security None) and SMTP 127.0.0.1:3025 (None).

Setting up an account

A three-step wizard, as in Outlook:

  1. Choose your provider. Tiles for the hundred most used mailbox domains worldwide plus Proton Mail — Gmail, Yahoo, Outlook.com/Hotmail/Live/MSN, AOL, iCloud, Comcast/Xfinity, Verizon, AT&T/SBCGlobal/BellSouth, Cox, Spectrum, EarthLink, Erols/Astound, Orange/Wanadoo, Free, SFR, La Poste, GMX, WEB.DE, T-Online, Libero, Virgilio, TIM/Alice, Ziggo, KPN, Telenet, Proximus, Bluewin, BT, Sky, Virgin Media, TalkTalk, QQ, NetEase 163/126, Sina, Sohu, Alibaba, Yahoo! JAPAN, docomo, au, @nifty, BIGLOBE, OCN, Naver, Daum/Kakao, Nate, Rediffmail, Yandex, Mail.ru, Rambler, UOL, BOL, iG, Terra, Telstra/BigPond, Optus and more (web/src/main/kotlin/.../Providers.kt, 100+ providers, 350+ domains) — grouped by region with a search box, plus Other for any mailbox. Historic brands that were merged (Verizon → AOL, Erols → Astound, Voilà → Orange, Chello → Ziggo…) point at the successor's servers and say so.
  2. Name, address and password. The provider's servers, caveats and Create an app password link are shown right away (Gmail, Yahoo, iCloud, AOL, Fastmail, Yandex, Mail.ru and QQ need an app password or authorisation code). With Other the settings are looked up from the address (built-in table → Mozilla ISPDB → the domain's autoconfig → MX record → port probe).
  3. Review the server settings and Test connection before signing in. Tick Keep me signed in to store the account on the device.

A branded splash screen covers start-up on every platform (system launch splash on Android and iOS, then the page's own splash until the mailbox is ready).

Junk protection (on by default)

Every new message is analysed on the server and, when it is junk, moved to the Junk folder automatically (POP3 accounts get a local Junk view instead, since POP3 has no folders). Signals, all free and needing no API key:

Signal What it does
DNS blocklists The sending IPs from the Received chain are checked against Spamhaus ZEN, SpamCop, Barracuda and PSBL; the sender, Reply-To and link domains against Spamhaus DBL, SURBL and URIBL. Lists can be toggled, re-weighted and extended with your own zones in Settings → Junk email. Spamhaus refuses queries via public resolvers (8.8.8.8, 1.1.1.1); the UI shows refused when that happens.
Authentication results SPF / DKIM / DMARC verdicts recorded by your own mail provider.
Impersonation detector An offline dictionary of ~2,500 organisations (Fortune/Global 500 companies, banks, insurers, retailers, carriers, government agencies, universities) and the domains they really send from; every domain was checked to resolve in DNS when the table was generated (web/src/main/resources/brands/*.tsv, regenerated with scripts/gen-brands.py). The sender's display name, address, subject and body signature are scanned for a claimed organisation and compared with the real sending domain; look-alike domains (paypa1.com, arnazon.com, secure-paypal-login.net, paypal.com.verify.ru) are caught with homoglyph normalisation and edit distance. A mismatch is shown as "Claims to be PayPal — domain is not PayPal's".
Learned classifier A naive-Bayes model that trains on your Junk / Not junk clicks (seeded with a small built-in corpus, and deliberately weak until you have taught it).
Content heuristics Urgency and prize language, link-text/href mismatches, bare-IP links, shorteners, hidden text, dangerous attachment types, Reply-To on another domain, and so on.
Rules, safe and blocked senders Deterministic and always win.

Every verdict is explained: the reading pane shows the sender's address in large bold type with the domain underlined and coloured by verdict, badges for the claimed organisation and the authentication results, and a Why? list with the weight of each signal.

Per-message buttons (toolbar, hover actions, context menu and keyboard):

  • Junk (J) moves the message to Junk and trains the classifier; Not junk (Shift+J) restores it, trains the classifier and guarantees it is never auto-filed again. Both have Undo.
  • Analyse & make rule runs the full analysis and proposes a rule that would catch this message and others like it (sender domain or exact address, spoofed display name, distinctive subject phrase, scam phrase). Each condition explains why it was chosen. Edit it, Preview matches against the current folder, then Save rule or Save & apply to current mail to file the matching messages that are already in the folder.

Optionally, a locally running open-source language model served by Ollama (Settings → AI models) adds a second opinion and extra rule conditions. Nothing is ever sent to a cloud service; the built-in engine needs no model at all.

Other features

Reply / reply all / forward with quoting, rich-text compose with attachments (drag-and-drop or paste), drafts, flags, read/unread, move, archive, delete with undo, server-side search, remote image blocking with per-sender allow list, sanitised HTML rendering in a sandboxed frame, attachment download, message headers view, print, unsubscribe, three reading-pane layouts, compact/comfortable density, and Outlook-style shortcuts (N, R, A, F, Del, E, J, U, S, /, ?).

API

The backend exposes a JSON API (/api/session, /api/folders, /api/messages, /api/message, /api/junk, /api/analyze, /api/rules/preview, /api/rules/apply, /api/settings, …) with a bearer token from /api/session. The original stateless endpoints /api/inbox and /api/send are unchanged, so the iOS client keeps working.

Other platforms

Quillbox started as an Android app; sibling clients live in their own folders. Each is built by its own GitHub Actions workflow.

Folder Target Stack Build artifact
app/ Android (and ChromeOS) WebView shell around the embedded web client (Ktor CIO backend in-process, Jakarta Mail) .apk
desktop/ Windows / macOS / Linux / Raspberry Pi OS Native launcher (Compose Desktop) around the embedded web client .msi/.dmg/.deb (x64 + arm64) + portable archives
web/ Browser (self-hosted) Ktor backend (JVM, Jakarta Mail) + Outlook-style web UI, junk engine server .zip
ios/ iPhone / iPad SwiftUI + WebKit shell showing the web client served by your own Quillbox server .app (simulator)

Notes on the architecture choices:

  • One interface everywhere. The Outlook-style UI and the backend live in web/ and are compiled into every other target, so Android, iOS, Windows, macOS, Linux, ChromeOS and the browser all show the same three-pane client (stacked panes with a folder drawer on phones, folders + list with a sliding reading pane on tablets), the same junk verdicts, the same Analyse & make rule dialog and the same settings.
  • Android runs the backend inside the app process and shows the UI in a WebView (app/src/main/java/.../LocalServer.kt, MainActivity.kt). No server needed.
  • ChromeOS runs the Android APK directly — no separate build. The Linux .deb or the web zip also work in the ChromeOS Linux container.
  • Desktop is the web client packaged as a native app: it embeds the same backend and Outlook-style UI (compiled straight from web/), starts it on 127.0.0.1 and opens it in your browser, with a tray icon to reopen or quit. Run locally with cd desktop && ./gradlew run; package installers with ./gradlew packageDistributionForCurrentOS (.msi / .dmg / .deb, the arm64 .deb being the Raspberry Pi OS package).
  • Web: browsers cannot open IMAP/SMTP sockets, so web/ is a small backend that does the mail work and serves a browser UI. See the section above.
  • iOS: Jakarta Mail is JVM-only and Apple does not allow a JVM on iOS, so the iPhone/iPad app cannot run the backend itself. It shows the same web client full-screen in WebKit, served by a Quillbox server on your own network — the quillbox-web zip on a PC, Mac, Raspberry Pi or NAS (it binds to all interfaces by default), or the desktop app started with HOST=0.0.0.0. Enter the address (for example http://192.168.1.20:8080) on the first screen; Settings → Account → Change server changes it later. Printing, attachment saving (share sheet), external links and mailto: are handled natively. The Xcode project is generated from ios/project.yml via XcodeGen (cd ios && xcodegen generate && open Quillbox.xcodeproj).

Releases

Pushing a tag such as v1.2.0 makes GitHub Actions build every target and attach the files to one GitHub Release (Releases → Quillbox v1.2.0):

Target Release file
Android, ChromeOS quillbox-v1.2.0.apk (signed when the signing secrets exist, else -unsigned)
Windows quillbox-desktop-v1.2.0-windows-x64.msi, or the portable .zip
macOS quillbox-desktop-v1.2.0-macos-arm64.dmg, or the portable .tar.gz
Linux x64 quillbox-desktop-v1.2.0-linux-x64.deb, or the portable .tar.gz
Raspberry Pi OS (64-bit) / Linux arm64 quillbox-desktop-v1.2.0-linux-arm64.deb, or the portable .tar.gz
Web / any OS with Java 17+ (servers, Termux, ChromeOS Linux) quillbox-web-v1.2.0-any-jvm.zip → unzip, run bin/quillbox-web
iOS Quillbox-iOS-simulator.app.zip (unsigned simulator build; a device build needs an Apple Developer account)

Making a release from Termux (Android)

Windows, macOS and iOS packages can only be built on those systems, so from a phone the job is: build and test what the phone can (the web/JVM server), then push a tag and let GitHub Actions build all eight targets. Everything below is typed into Termux.

One-time setup:

pkg update && pkg upgrade -y
pkg install -y git openjdk-17 gh unzip
termux-setup-storage                                  # optional: reach ~/storage/downloads
git config --global user.name  "Your Name"
git config --global user.email "you@example.com"
gh auth login                                         # GitHub CLI login (HTTPS + browser/token)
git clone https://github.com/socrtwo/quillbox.git
cd quillbox
mkdir -p ~/.gradle && printf 'org.gradle.jvmargs=-Xmx1536m\norg.gradle.daemon=false\n' >> ~/.gradle/gradle.properties

Build and test the web/JVM server on the phone (this is the same zip the release ships):

cd ~/quillbox/web
chmod +x gradlew
./gradlew build distZip --no-daemon                   # runs the junk-engine tests, writes build/distributions/quillbox-web-1.2.0.zip
cd build/distributions && unzip -o quillbox-web-1.2.0.zip && cd quillbox-web-1.2.0
PORT=8080 ./bin/quillbox-web                          # open http://localhost:8080 in the phone browser

Cut the release (all targets are built by GitHub Actions and attached to one GitHub Release):

cd ~/quillbox
git checkout main && git pull
# bump the version first if needed: app/build.gradle.kts (versionCode/versionName),
#   web/build.gradle.kts (version), desktop/build.gradle.kts (version, packageVersion)
git commit -am "Release v1.2.0"        # only if you changed something
git push
git tag -a v1.2.0 -m "Quillbox 1.2.0"
git push origin v1.2.0                 # <- this triggers the Android, Desktop, Web and iOS release builds

gh run list --limit 8                  # watch the four workflows
gh run watch                           # follow one interactively
gh release view v1.2.0                 # list the attached files when they are done
gh release download v1.2.0 -D ~/storage/downloads/quillbox-v1.2.0

If the repository has no signing secrets, the APK arrives unsigned; sign it on the phone:

pkg install -y apksigner
keytool -genkeypair -v -keystore ~/quillbox-release.jks -alias quillbox -keyalg RSA -keysize 2048 -validity 10000
apksigner sign --ks ~/quillbox-release.jks --ks-key-alias quillbox \
  --out quillbox-v1.2.0.apk ~/storage/downloads/quillbox-v1.2.0/quillbox-v1.2.0-unsigned.apk

To have CI sign it instead, add the secrets KEYSTORE_BASE64, KEYSTORE_PASSWORD, KEY_ALIAS and KEY_PASSWORD (see Automated signed builds in CI above):

base64 -w 0 ~/quillbox-release.jks | gh secret set KEYSTORE_BASE64
gh secret set KEYSTORE_PASSWORD
gh secret set KEY_ALIAS --body quillbox
gh secret set KEY_PASSWORD

docs/RELEASING.md has the longer version, including the unofficial way to build the APK inside Termux and what a phone cannot build (Windows/macOS installers, a signed iOS app).

Security note

Credentials and downloaded mail are stored locally only and are transmitted solely to the user's own mail servers (on the web/iOS clients, via your own Quillbox backend). Use real credentials only on your own device; the repository ships with placeholder values only.

The web backend keeps passwords in memory for the life of a session and writes only rules, settings and analysis results to disk. Put it behind HTTPS (a reverse proxy such as Caddy or nginx) whenever it is reachable from outside the machine it runs on. The junk engine's only outbound traffic is DNS: blocklist lookups for sending IPs and domains.

About

Android email client: downloads IMAP/POP3 mail, composes new messages with a standard formatting toolbar, and routes incoming mail into folders (e.g. Spam) via user-defined rules.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages