This entire application (backend, frontend, and this README) was generated by an AI coding agent (Claude) for personal use. It has not undergone any professional or human security review, and no specific security hardening has been done beyond basic good practices (encrypted credential storage, a password gate, sandboxed message rendering).
This is intended strictly for single-user, private/local-network use. Do not expose this container directly to the public internet. If you want remote access, put it behind your own trusted VPN or a reverse proxy with independent authentication. Use at your own risk.
A self-hosted webmail aggregator: one modern inbox view for all your existing mail accounts (generic IMAP/SMTP providers, plus Outlook.com/Microsoft 365 via OAuth2), running entirely in a single Docker container. Your mail never leaves the origin servers — inboxone only caches lightweight message headers/flags locally for a fast aggregated view; full bodies and attachments are always fetched live from IMAP when you open a message.
- Aggregate multiple IMAP/SMTP accounts (and Outlook.com/Microsoft 365 via OAuth2) into one chronological inbox.
- Color-coded account indicator on every message so you always know which mailbox it came from.
- Reply from any configured account, from a single aggregated view.
- Per-account folder selection — sync just
INBOX, or additional folders/labels too. - A single app-wide password gate protects the whole UI.
- Modern three-pane UI (filter rail / message list / reading pane), not an old-school webmail look.
- Backend: Python (FastAPI), SQLAlchemy async + SQLite, APScheduler for background
IMAP polling,
IMAPClientfor IMAP,aiosmtplibfor basic-auth SMTP sending,msalfor Microsoft OAuth2 device-code sign-in, Microsoft Graph for sending mail from Outlook accounts. - Frontend: React + TypeScript + Vite + TailwindCSS + TanStack Query + Zustand.
- Data model: only message headers/flags (subject, sender, date, read/flagged state, etc.) are cached in a local SQLite database. Bodies and attachments are never persisted — they're fetched live from the mail server every time you open a message.
- Runs as a single Docker container: the API and the background sync scheduler share one Python process (no separate worker process needed).
- Docker and Docker Compose.
- Copy the example environment file and fill in real values:
cp .env.example .env
- Generate the required secrets:
(If you don't have Python locally, run these one-liners inside the built container instead:
python3 -c "import secrets; print(secrets.token_urlsafe(32))" # SECRET_KEY python3 -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())" # ENCRYPTION_KEY
docker compose run --rm inboxone python3 -c "...".) - Set
APP_PASSWORDin.envto a password of your choosing (this only seeds the initial password - see "Forgot your password?" below for how to reset it later). - Pull and start (uses the published image from ghcr.io):
docker compose up -d
- Open
http://localhost:8000, log in withAPP_PASSWORD, and add your accounts under Settings.
Use your provider's IMAP/SMTP host, port, and your normal username/password (or an app-specific password, see below). Settings → Add account → "Generic IMAP/SMTP".
Gmail requires an App Password rather than your normal account password once
2-Step Verification is enabled (Google no longer allows plain account passwords for
IMAP/SMTP). Create one at
myaccount.google.com/apppasswords and use
it as the IMAP/SMTP password. Host: imap.gmail.com (port 993), smtp.gmail.com
(port 587).
Microsoft has deprecated basic auth for IMAP/SMTP, so Outlook accounts (personal
outlook.com/hotmail.com/live.com addresses included, not just work/school ones)
use OAuth2 ("modern auth") via a device-code sign-in flow. Settings → Add account →
"Outlook / Microsoft 365" → enter a name → Connect. You'll get a short code to enter at
microsoft.com/devicelogin, sign in with your normal Microsoft account, and you're
done — no Azure setup required. inboxone ships with Microsoft's own first-party
"Office" client ID pre-registered (the same well-known ID tools like Thunderbird reuse
for exactly this), so you're just doing a normal OAuth sign-in, not registering an app.
Advanced: use your own Azure app instead. If you'd rather isolate your account under an app you control (e.g. the shared client ID ever gets rate-limited or blocked), click "Advanced" in the connect flow and register your own, free:
- Go to Azure Portal → App registrations → New registration.
- Name it anything. Under Supported account types, choose "Accounts in any organizational directory and personal Microsoft accounts".
- Leave the redirect URI blank — the device-code flow doesn't need one.
- Go to Authentication → Advanced settings and set "Allow public client flows" to Yes.
- Go to API permissions → Add a permission → Microsoft Graph → Delegated
permissions, and add:
IMAP.AccessAsUser.All,Mail.Send,offline_access,User.Read. No admin consent is needed for a personal registration. - Copy the Application (client) ID from the app's Overview page and paste it into the "Advanced" Client ID field before connecting.
| Variable | Description |
|---|---|
APP_PASSWORD |
Password required to access the web UI. |
SECRET_KEY |
Random secret used to sign the session cookie. |
ENCRYPTION_KEY |
Fernet key used to encrypt stored IMAP/SMTP passwords and OAuth tokens at rest. Do not rotate after accounts exist without a migration - existing encrypted values would become unreadable. |
DATABASE_URL |
SQLite connection string (defaults to the /data bind mount). |
SYNC_DEFAULT_INTERVAL_SECONDS |
Default IMAP polling interval per account (minimum 30s). |
APP_PASSWORD in .env only sets the initial password the first time the container
starts - after that, the password lives in the database and editing .env no longer
has any effect. If you forget it, reset it directly from inside the running container:
docker exec -it inboxone python -m app.cli reset-passwordThis prompts for a new password and updates it immediately, without needing to know the
old one - no need to touch .env or restart anything.
- Mail never leaves your mail servers. The one exception is explicit deletion: hitting Delete on a message moves it to that account's Trash/Deleted Items folder on the server (the same "soft delete" any normal mail client does), it isn't just hidden locally. Nothing is ever permanently erased by this app.
- Only message metadata (subject, sender/recipients, date, read/flagged state, folder, size) is cached locally in SQLite, so the aggregated view loads instantly.
- Message bodies and attachments are fetched live over IMAP every time you open a message or download an attachment - they are never written to disk.
- IMAP/SMTP passwords and Microsoft OAuth tokens are encrypted at rest with a
symmetric key (
ENCRYPTION_KEY); they are only decrypted in memory for the duration of a connection.
The published image (ghcr.io/dermute/inboxone:latest) is what docker-compose.yml
uses by default. To build and run from your own checkout instead, uncomment the
build: block in docker-compose.yml (and comment out image:), then:
docker compose up -d --build# backend
cd backend
pip install -e ".[dev]"
alembic upgrade head
uvicorn app.main:app --reload
# frontend (separate terminal)
cd frontend
npm install
npm run devThe Vite dev server proxies /api requests to http://localhost:8000.
- No IMAP IDLE or push updates yet — the backend polls each account on an interval (default 60s) and the frontend polls the API every 15s. New mail typically appears within a minute.
- No Gmail OAuth - Gmail accounts are configured as generic IMAP/SMTP using an App Password.
- Single user, single SQLite database - not designed for multiple concurrent users.
- Message list "snippet" preview is not populated yet, since generating it would require fetching bodies during sync (which this app intentionally avoids to keep mail off local storage).
This project was developed with AI assistance (Anthropic Claude, Sonnet 5).
MIT — see LICENSE.