Skip to content
dermutePublic

About

Self-hosted, single-user mail aggregator (IMAP/SMTP + Outlook OAuth2) in one Docker container. AI-generated, personal use only - see README.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

inboxone

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.

Features

  • 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 INBOX folders 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.

Architecture (brief)

  • Backend: Python (FastAPI), SQLAlchemy async + SQLite, supervised background IMAP IDLE and reconciliation, IMAPClient for IMAP, aiosmtplib for basic-auth SMTP sending, msal for 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).

Requirements

  • 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.

Quick start

  1. Copy the example environment file and fill in real values:
    cp .env.example .env
    
  2. Generate the required secrets:
    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
    
    (If you don't have Python locally, run these one-liners inside the built container instead: docker compose run --rm inboxone python3 -c "...".)
  3. Set APP_PASSWORD in .env to a password of your choosing (this only seeds the initial password - see "Forgot your password?" below for how to reset it later).
  4. Pull and start (uses the published image from ghcr.io):
    docker compose up -d
    
  5. Open http://localhost:8000, log in with APP_PASSWORD, and add your accounts under Settings.

Install as a Windows app

  1. Open inboxone in Microsoft Edge or Google Chrome using HTTPS (or http://localhost:8000 on the same PC that runs inboxone). Plain HTTP on a LAN IP address or hostname does not support PWA installation.
  2. 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.
  3. 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).

Adding accounts

Generic IMAP/SMTP (most providers)

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

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

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.

Environment variables

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.

Forgot your password?

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.

Data & privacy model

  • 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_KEY and 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.

Folders and synchronization

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.

Building from source

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

Development (without Docker)

# 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.

Verification

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.

Known limitations / roadmap

  • 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).

AI attribution

This project was developed with AI assistance.

License

MIT — see LICENSE.

About

Self-hosted, single-user mail aggregator (IMAP/SMTP + Outlook OAuth2) in one Docker container. AI-generated, personal use only - see README.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Packages

Contributors

Languages