Skip to content

Repository files navigation

emagg

A local-first aggregator that pulls all your email accounts into one searchable place, so you stop juggling 15 browser tabs to read your mail.

emagg fetches mail from many accounts (multiple Gmail, multiple Zoho Mail, and a plain IMAP college account), stores everything locally, and shows it in a mail-client layout: a sidebar of inboxes (per account, an "All" view, and saved filter views) with strong full-text filtering across every inbox at once.

Read-only by design: you read and triage here, then jump to the right account in its own browser profile to act.

Status

Implemented and local-first. See docs/superpowers/specs/2026-06-20-email-aggregator-design.md for the full architecture.

Quick start

Requirements: Node 24.17.0 (see .nvmrc) and pnpm.

# 1. Install dependencies.
pnpm install

# 2. Create your local env file and set the one required value.
cp .env.example .env
# Generate a 32-byte at-rest encryption key and paste it as EMAGG_ENCRYPTION_KEY:
openssl rand -hex 32

# 3. (Optional) bootstrap the IITB IMAP account from env, so the walking
#    skeleton has mail on first boot. In .env set:
#      IITB_IMAP_USER=<your-iitb-username>
#      IITB_IMAP_PASSWORD=<your-iitb-password>
#    On first boot the password is encrypted at rest and never written in clear.

# 4. Run the app (Fastify API on 127.0.0.1:3001, Vite UI on 127.0.0.1:5173).
pnpm dev

Open http://127.0.0.1:5173. If you set the IITB env vars, the background sync pulls the last 14 days on boot and mail appears in the list newest-first.

You can also add accounts from the UI instead of (or in addition to) the env bootstrap: click Add account, then either connect Gmail/Zoho via Composio (requires COMPOSIO_API_KEY and the per-provider auth-config ids) or fill in the IMAP form (host imap.iitb.ac.in, port 993, your username + password). IMAP credentials are verified with a real test login before anything is saved, and the password is stored encrypted at rest.

If your IMAP server runs old or outdated TLS and presents a self-signed or otherwise unverifiable certificate (IITB's mail servers do), the secure test login and sync will fail. Set IMAP_ALLOW_INSECURE_SSL=true to accept such certificates. The connection stays TLS-encrypted; this disables certificate verification only (server identity is no longer checked), so use it only for servers you trust. It applies to both the sync connector and the setup test login, never to Gmail/Zoho, and logs a clear warning on startup when enabled.

Useful scripts: pnpm test, pnpm lint, pnpm typecheck, pnpm build.

Configuration

All configuration is via environment variables (see .env.example). .env is gitignored; never commit real secrets.

Variable Required Default Purpose
EMAGG_ENCRYPTION_KEY yes AES-256-GCM key for the at-rest IMAP password. 64-char hex (openssl rand -hex 32). The app refuses to start if missing or not 32 bytes.
COMPOSIO_API_KEY no Composio key for Gmail/Zoho sync. Leave blank to run IMAP-only.
COMPOSIO_GMAIL_AUTH_CONFIG_ID no Composio auth-config id for the Gmail connect flow.
COMPOSIO_ZOHO_AUTH_CONFIG_ID no Composio auth-config id for the Zoho connect flow.
IITB_IMAP_USER no IITB IMAP username for the boot bootstrap.
IITB_IMAP_PASSWORD no IITB IMAP password (read once at boot, stored encrypted).
IITB_IMAP_HOST no imap.iitb.ac.in IITB IMAP host.
IITB_IMAP_PORT no 993 IITB IMAP port (TLS).
IMAP_ALLOW_INSECURE_SSL no false Accept self-signed / unverifiable IMAP TLS certs (e.g. IITB's old servers). Stays encrypted; disables cert verification only. Logs a warning when on. Affects IMAP only.
EMAGG_DB_PATH no ./data/emagg.sqlite SQLite file path (gitignored, created on first run).
HOST no 127.0.0.1 Server bind address. Keep loopback (see security below).
PORT no 3001 API server port.

Security posture (local-only)

This is a single-user, local-first app with no application auth. The protections below assume it stays on your machine.

  • Loopback bind only. The server binds 127.0.0.1 by default. Because there is no auth, binding to a public interface (0.0.0.0, a LAN IP) would expose every connected mailbox to the network; boot warns loudly if HOST is not a loopback address.
  • Hostile email HTML is isolated. Email bodies are untrusted. Each is run through DOMPurify on every render and shown inside an <iframe sandbox> with no allow-scripts and a default-src 'none' Content-Security-Policy, so email JavaScript can never run and the render context cannot reach the network even if a sanitizer rule regresses. Raw HTML is stored unmodified so the archive stays re-cleanable if the rules are tightened later.
  • Remote content blocked by default. Tracking pixels / remote images leak your IP and read-status, so they are stripped at render until you flip the per-email "Load images" toggle. Each newly opened email starts blocked again.
  • Security headers. The API sets a strict CSP plus anti-sniff / anti-frame headers on every response; the dev UI server sets an origin-scoped CSP for the app shell. For a production static deploy, serve the same app-shell CSP without the dev-only 'unsafe-eval' and ws: allowances (see vite.config.ts).
  • Secrets at rest and in logs. The only secret stored locally is the IMAP password, encrypted with AES-256-GCM (EMAGG_ENCRYPTION_KEY). Gmail/Zoho OAuth tokens live in Composio, not here. Secrets are stripped from every API response and redacted in logs, and CI runs gitleaks on every PR.

What it does

  • One unified app with a left sidebar of inboxes. Each account is an inbox; there is an "All" combined view; saved filters become their own views.
  • Connect accounts from the UI: Gmail and Zoho via Composio's managed OAuth (click, authorize, done); the IMAP account via a credentials form.
  • Background sync per account on its own interval (15 or 60 minutes), into a local SQLite store that keeps everything and never auto-deletes.
  • Strong filtering: by sender, date range, subject keywords, and body keywords, with boolean operators, powered by SQLite FTS5. Save any filter as a view.
  • Per-account body-fetch policy (eager or lazy) with a per-sender override, so a high-volume notification firehose does not blow your API budget.
  • Privacy-first reading: email HTML rendered in a sandboxed iframe with remote content (tracking pixels) blocked by default.

Architecture (short)

Frontend (sidebar + list + sandboxed reading pane)
Web server (Fastify): emails, sync, filters, account connect/manage
SQLite (better-sqlite3): accounts | emails | emails_fts (FTS5) | saved_filters
Sync engine: single ticker, per-account mutex, provider cursors, overlap window
Connectors: Gmail (Composio), Zoho (Composio), IMAP (direct) behind one interface

All three providers sit behind one Connector interface, so the sync engine never knows whether an email came from Composio or raw IMAP.

Stack

Node + TypeScript, Fastify, better-sqlite3 (SQLite + FTS5), Composio for Gmail/Zoho OAuth and fetching, imapflow for the direct IMAP account.

Roadmap

Local-first now; a hosted multi-user version is an explicit later goal, which is why accounts are connected at runtime and the design avoids local-only assumptions.

About

emagg: a local-first aggregator that pulls all your email accounts (multiple Gmail, Zoho Mail, IMAP) into one searchable, mail-client-style inbox.

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages