A self-hosted webmail aggregator: one modern inbox view for all your existing mail accounts (generic IMAP/SMTP providers plus Outlook.com via OAuth2), running entirely in a single Docker container. inboxone does not upload your mail to a hosted third-party service. It stores account configuration, encrypted credentials and tokens, sync state, and message metadata in its local SQLite database; full bodies and attachments are fetched on demand and are not persisted.
- Aggregate multiple IMAP/SMTP accounts and Outlook.com into one chronological inbox.
- Color-coded account indicator on every message so you always know which mailbox it came from.
- Search subject, sender, and recipients on the mail server, including uncached old messages. Search the current view or explicitly include all folders; filter unread mail.
- Reply to or forward messages from any configured account, from a single aggregated view.
- Automatically discover every selectable folder, including Sent, Archive, Junk, and Trash. Cache recent inbox mail and load older messages in pages when needed.
- Choose “Include in inbox” folders per account. Combined and account inbox views,
total unread badges, and new-mail notifications include only those folders.
Standard
INBOXfolders are included by default. - 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.
- Installable as a desktop app in Microsoft Edge and Chrome, with its own window and app icon.
- Backend: Python (FastAPI), SQLAlchemy async + SQLite, supervised background
IMAP IDLE and reconciliation,
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: account configuration, encrypted credentials and tokens, sync state, and message metadata (subject, sender, recipients, date, flags, folder, size, etc.) are stored in a local SQLite database. Bodies and attachments are never persisted — they're fetched live from the mail server when requested.
- 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.
Security note: inboxone is a single-user application intended for private or local-network use. Do not expose it directly to the public internet. For remote access, use a trusted VPN or a reverse proxy with independent authentication.
- 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_KEYdocker 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.
- Open inboxone in Microsoft Edge or Google Chrome using HTTPS (or
http://localhost:8000on the same PC that runs inboxone). Plain HTTP on a LAN IP address or hostname does not support PWA installation. - Log in and click Install InboxOne above Settings when your browser offers installation, or use the browser's install icon in the address bar. The sidebar button only appears when the browser makes installation available. In Edge, you can also use More tools → Apps → Install this site as an app.
- Launch the installed app, then right-click its Windows taskbar icon and choose Pin to taskbar.
The installed app uses the same server and loads the current website when opened; updates do not require reinstalling. It requires a network connection and does not cache mail for offline use.
To enable new-mail notifications, click the bell button in inboxone and allow notifications in the browser's permission prompt. Windows notifications must also be enabled for the browser/app. Keep inboxone open (minimized is fine) to receive notifications; closing all its windows and tabs stops the current notification watcher. Installation does not add background push notifications. Delivery timing depends on IMAP IDLE availability and browser background activity limits. Hidden tabs keep their live event connection, but browser suspension may delay notifications. Backend inbox watching continues even when all browser windows are closed.
The PNG app icons are generated from the envelope artwork in
frontend/public/favicon.svg. After changing its geometry, regenerate them with
python3 frontend/scripts/generate-icons.py (no additional dependencies).
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).
Outlook.com accounts use OAuth2 via a device-code sign-in flow. Go to Settings → Add
account, choose Outlook, enter a name, and select Connect. Enter the displayed code at
microsoft.com/devicelogin, then sign in with
your Microsoft account. inboxone uses built-in OAuth client configuration, so you do
not need to register an Azure application.
| 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 |
Legacy compatibility setting; the global refresh setting below supersedes it. |
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-password
This prompts for a new password and updates it immediately, without needing to know the
old one - no need to touch .env or restart anything.
- inboxone does not upload your mail to a hosted third-party service. Its database and application data remain on the server where you run the container.
- The local SQLite database stores the app password hash, account configuration, encrypted credentials and OAuth tokens, sync state, and message metadata (subject, sender/recipients, date, flags, folder, and size).
- Message bodies and attachments are fetched from the provider when you open or download them and are not persisted to disk by inboxone.
- IMAP/SMTP passwords and Microsoft OAuth tokens are encrypted at rest with
ENCRYPTION_KEYand decrypted in memory when needed for a connection. - Deleting a message moves it to the account's Trash or Deleted Items folder on the provider; inboxone does not permanently erase it.
- Sent replies and forwards are saved in the sending account's Sent folder at the provider. SMTP accounts use IMAP to save the copy; Gmail and Microsoft accounts use their provider's automatic Sent storage. If a message is sent but saving its copy fails, inboxone displays a warning so you do not accidentally send it again.
All selectable folders are discovered on startup and scheduled account refreshes, including newly created folders. In Settings → Accounts → Inbox folders, check the folders to include in inbox views. These choices do not change provider folders. Other folders are available on demand. Expand an account in the sidebar to browse any folder; selecting the account itself shows only its marked inbox folders. Search and “Mark all as read” apply within the selected inbox or folder view. Search additionally offers Search all folders for the selected account, or all active accounts from the combined inbox.
Each inbox-marked folder of an active account has its own persistent IMAP IDLE connection. Changes trigger reconciliation and committed updates reach open pages through an authenticated server-sent event stream. IDLE renews every 10 minutes; connections retry with jittered exponential backoff from 5 seconds to 5 minutes, using fresh OAuth credentials and catching up after reconnecting. Unsupported IDLE or provider connection limits visibly fall back to scheduled inbox polling.
Automatic refreshes default to every 15 minutes for all accounts, including inbox
safety checks, polling fallback, previously opened folders, folder discovery, and
lightweight unread-count checks for unopened folders. Change
Automatic refresh in Settings to any whole-minute interval from 1 to 1,440 minutes.
The setting persists across restarts and applies immediately without reconnecting
accounts. The schedule is the same for visible and hidden pages. Live mail events
still sync immediately; renewing an idle connection alone does not trigger a sync.
Visibility leases renew every 20 seconds and expire after 60 seconds, so multiple
tabs and unexpectedly closed browsers are handled together. Opening or reloading
loads the selected view without starting full account refreshes. Returning to the
browser tab does not start a sync or reload cached messages. Use Refresh all in
the bottom status bar to manually refresh every active account. Navigation shows
cached mail immediately and requests a scoped refresh only when the folder is at
least 30 seconds stale. Requests across tabs merge with running work, with at most
one merged follow-up pass per account. Work yields account ownership between
bounded folder passes, and navigation can prioritize the requested folder.
The legacy per-account sync_interval_seconds API/database field and interval
environment settings remain accepted for compatibility; they no longer control
refresh timing.
The connection button in the bottom status bar (also accessible in Settings) opens connection details, last successful syncs, sanitized errors, retry times, per-account Retry, and Refresh all. Green means all active inbox connections and reconciliations are healthy. Yellow means connecting, retrying, polling fallback, or a disconnected browser event stream; browser cache polling then runs every 60 seconds. Red means an authentication/configuration failure or reconciliation failure awaiting successful recovery. The worst active state determines the overall color; disabled accounts are excluded, and no active accounts produces a neutral status.
The bottom status bar remains visible while browsing folders or reading messages, including on mobile. A separate spinner and action label show queued or running work immediately: opening or refreshing messages, syncing folders, sending, downloading attachments, deleting, and updating read status. Cached content stays visible during refreshes. Actions in the current tab take priority over background sync; “+N more” indicates additional work, listed in the connection dialog. Connection health keeps its own color while work runs. Waiting in IMAP IDLE, presence renewals, and event heartbeats do not make the app appear busy. Activity travels over the same event stream; if it disconnects, visible pages check activity every two seconds until reconnection. Mail cache fallback remains every 60 seconds. Failed and cancelled operations clear their busy state; errors retain their existing alerts and retry controls.
Persistent watchers use one server connection and one dedicated waiting thread per
marked inbox, plus transient connections for normal mail operations. Account edits,
OAuth reconnects, inbox choices, deactivation, and deletion update watchers automatically.
Keep a single backend process: presence, event subscriptions, and connection state are
held in memory. Reverse proxies must allow long-lived /api/sync/events responses;
this endpoint disables buffering and caching and sends a heartbeat every 15 seconds.
The first inbox sync caches the latest 200 headers, then tracks new arrivals using a separate live UID cursor. Browsing and searching fetch headers in pages of 50; older history does not have to finish importing before the app can be used. Already cached history is retained. Message bodies and attachments require an online connection; bodies are reused in browser memory after opening and are never stored in the database. Reopening a cached body still marks its message read.
Servers advertising QRESYNC receive a resynchronizing EXAMINE with the saved UIDVALIDITY, modification sequence, and a compact set of known UIDs. Changed flags and VANISHED responses update the cache. Rejected QRESYNC falls back to CONDSTORE CHANGEDSINCE, then baseline IMAP when necessary, including per-mailbox NOMODSEQ. An unchanged folder with a modification sequence needs only a status check. LIST-STATUS batches folder counters when available. Baseline IMAP reconciles cached flags in rotating batches of 200 rather than walking the entire history per refresh. Folders removed on the provider are removed locally after successful discovery.
Search shows cached matches while a cancellable server query checks the requested folders. Results include old uncached mail, fetch only matching headers, and page without importing unrelated history. Incomplete results identify folders that could not be checked. SORT orders results by date where supported; otherwise servers are queried in descending UID order and fetched page buffers are merged by message date. Server queries expire after five minutes and are recreated when needed. Changing a pending search cancels its work. Unread counts and Mark all as read include uncached messages. Historical page loads never generate new-mail notifications.
Folder events update affected message lists and counters and are coalesced for 250 milliseconds. Unchanged passes publish connection state without invalidating mail; ordinary folder events never redownload an open message body.
Existing installations migrate automatically on container startup. Existing message
IDs and cached headers are retained; the message table gains indexes and prevents
ID reuse after a mailbox reset. This one-time migration can take longer for a large
existing cache. Only standard INBOX folders
start marked for inbox inclusion; explicitly mark any additional folders after the
upgrade. All selectable folders remain available. Copies in multiple
marked folders appear as separate messages.
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 (from the repository root)
python3 -m venv .venv
source .venv/bin/activate
python -m pip install -e "./backend[dev]"
cd backend
alembic upgrade head
uvicorn app.main:app --reload
# frontend (separate terminal)
cd frontend
npm install
npm run dev
The Vite dev server proxies /api requests to http://localhost:8000.
Run pytest -q from backend after installing the development dependencies, and
npm run build from frontend. The live-sync backend tests use fake IMAP sockets
and an isolated SQLite database, including reconnects, IDLE fallback, UIDVALIDITY,
visibility leases, event ordering, request merging, and account lifecycle changes.
The optional browser smoke test uses the production build and a local fake API; it
never contacts real mail accounts. With playwright-core and Chromium available, run
node scripts/verify-live-sync.cjs from frontend after building. Set CHROMIUM_PATH
if using a system Chromium, and PLAYWRIGHT_MODULE if the package is installed
outside the frontend. This checks live messages/counts, notification history suppression,
reconnection, activity lifetimes and foreground priority, status colors, retry controls,
Settings, and the mobile status bar and dialog.
- Browser notifications require an open browser/app instance. Closed-app push notifications are not supported; suspended browsers may delay delivery.
- 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.
MIT — see LICENSE.