A multi-tenant email management platform built with Next.js 16. Connect IMAP/SMTP, Gmail OAuth, or Microsoft/Outlook OAuth accounts, sync emails via background workers, and manage everything from a unified dashboard with full-text search, drag-and-drop labels, activity logs, spam risk detection, granular member access control, Progressive Web App (PWA) support, and Web Push notifications.
Captured from the local development app using fictional demo data. No live mail accounts are connected.
| Dashboard Overview | Multi-Account Email Stream | Mobile Responsive View |
|---|---|---|
![]() |
![]() |
![]() |
Managing multiple email accounts across local clients is a disjointed, sluggish experience. MailHub redesigns this workflow from the ground up:
- 📬 Aggregated Stream (Unified Context): No more clicking through 10 different mailboxes. MailHub unifies all your IMAP and OAuth accounts into a single, unified email feed.
- ⚙️ Server-Side Decoupling: Heavy syncing tasks are fully handled by backend BullMQ workers on your server. Your frontend Next.js 16 dashboard remains ultra-lightweight and buttery smooth, no matter how many accounts you add.
- ⚡ Instant Global Search: Powered by Meilisearch. Search through hundreds of thousands of emails across all connected accounts simultaneously with millisecond-level results.
- 🌐 Setup Once, Access Anywhere: Being a Self-hosted PWA, you only configure your email accounts once on your server. Any desktop or mobile device can then instantly access the fully synced dashboard without tedious re-configurations.
| Layer | Technology |
|---|---|
| Framework | Next.js 16 (App Router) |
| Language | TypeScript 5 |
| PWA / Client | Service Worker, Web App Manifest, usePWAInstall hook, @dnd-kit (Drag & Drop) |
| Database | PostgreSQL 16 + Prisma ORM 7 (@prisma/adapter-pg) |
| Search | Meilisearch |
| Queue / Cache | Redis 7 + BullMQ |
| Auth | JWT (jose) + bcryptjs |
| Email Protocols | IMAP (imapflow) · SMTP (nodemailer) · Gmail OAuth 2.0 API · Microsoft Outlook OAuth 2.0 API |
| UI | React 19, Tailwind CSS 4, Lucide Icons, TipTap editor, Local geist fonts |
| State | Zustand + TanStack React Query |
| Notifications | Web Push (web-push + VAPID) |
| Testing | Jest + ts-jest |
| Containerization | Docker multi-stage build + Docker Compose |
- 📧 Multi-Account & Multi-Tenant: Connect multiple IMAP/SMTP, Gmail OAuth, or Microsoft Outlook OAuth mailboxes across isolated organization workspaces.
- 👥 Granular Access Control: Organization administrators can manage and grant per-member access permissions to specific email accounts.
- 🤖 AI Email Assistance: Summarize labels, explain messages, draft or rewrite replies, extract action items, analyze tone, translate content, and deliver summaries through Telegram, WeCom, or generic webhooks.
- ⚙️ Email Automation Rules: Apply ordered rules globally or by account/account label, with conditional or match-all execution for labels, read/star/risk state, webhooks, and forwarding.
- ⚡ Realtime Email Sync: High-throughput background sync powered by BullMQ workers with automatic SPAM detection and Meilisearch indexation.
- 📤 Multi-Provider Email Sender: Send emails using native SMTP, Google Gmail API, or Microsoft Graph/Outlook API with automatic token refreshing.
- 📬 Aggregated All Emails View: View and filter emails across all connected accounts simultaneously in a unified stream, complete with customizable reading pane layouts (Right, Bottom, or Off).
- 🗑️ Trash & Restore Flow: Full support for moving emails to Trash, restoring messages back to inbox/folders, and emptying trash per account.
- 🔍 Instant Full-Text Search: Search headers, body content, and senders seamlessly across all connected accounts powered by Meilisearch.
- 🏷️ Drag & Drop Label Management: Organize emails using customizable organization labels and interactive drag-and-drop actions.
- 📱 Progressive Web App (PWA): Installable desktop/mobile experience with offline Service Worker caching, automated background update polling, system installation detection, and dynamic UI install buttons.
- 📝 Rich Text Composer & Fullscreen Mode: Full-featured TipTap rich text editor with image resizing, attachments, signature auto-insertion, and seamless fullscreen composition.
- 🔔 Real-Time SSE & Push Notifications: Instant inbox updates via Server-Sent Events (SSE) and native Web Push notifications.
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Next.js App │────▶│ PostgreSQL │◀────│ Workers │
│ (PWA + API) │ │ (Prisma) │ │ (BullMQ) │
└──────┬───────┘ └──────────────┘ └──────┬───────┘
│ │
│ ┌──────────────┐ │
├────────────▶│ Meilisearch │◀───────────┤
│ │ (Search) │ │
│ └──────────────┘ │
│ ┌──────────────┐ │
└────────────▶│ Redis │◀───────────┘
│ (Queue/SSE) │
└──────────────┘
- Next.js App — serves the dashboard UI, handles PWA service worker lifecycle, and exposes REST API routes.
- Workers — background processes that sync email via IMAP/Gmail/Outlook, managed by BullMQ. Horizontally scalable with partition keys (
WORKER_PARTITION). - Redis — backs the BullMQ job queue and server-sent events for realtime updates.
- Meilisearch — powers instant full-text search across all synced emails.
- PostgreSQL — primary data store for users, organizations, email accounts, permissions, emails, labels, favorites, and activity logs.
Core entities managed via Prisma:
- User — authentication identity (email + password hash)
- Organization — multi-tenant workspace (slug-based)
- OrganizationMember — user ↔ org membership with role permissions
- EmailAccount — connected mailbox (IMAP/SMTP creds or OAuth tokens, encrypted at rest)
- MemberEmailAccountAccess — granular per-member access permissions for specific email accounts
- Email / EmailBody / Attachment — synced messages, bodies, and file metadata
- Label / AccountLabel / EmailLabel — workspace and account-level labels applied to messages
- EmailActivityLog — detailed audit log of email operations
- PushSubscription — Web Push endpoints per organization user
- FavoriteEmail — starred/favorited emails tracking per user account
- Node.js ≥ 20
- Docker & Docker Compose (for PostgreSQL, Redis, Meilisearch)
git clone https://github.com/kawing11a/mailhub.git
cd mailhub
npm installcp .env.example .envEdit .env with your values. Required variables:
For a local synthetic demo, copy .env.example to .env.local instead and point its service URLs at your own loopback services. Leave the root .env absent; when .env exists, the app and Prisma use that real configuration and the demo seed refuses to run.
| Variable | Description |
|---|---|
DATABASE_URL |
PostgreSQL connection string |
REDIS_URL |
Redis connection string |
MEILI_HOST |
Meilisearch URL |
MEILI_MASTER_KEY |
Meilisearch admin key |
JWT_SECRET |
Secret for signing JWT tokens |
ENCRYPTION_KEY |
64-char hex key for encrypting stored credentials |
NEXT_PUBLIC_APP_URL |
Base application URL (e.g. https://localhost:3000) |
GOOGLE_CLIENT_ID |
OAuth 2.0 Client ID for Google Workspace / Gmail |
GOOGLE_CLIENT_SECRET |
OAuth 2.0 Client Secret for Google Workspace / Gmail |
AZURE_CLIENT_ID |
OAuth 2.0 Application (Client) ID for Microsoft Outlook |
AZURE_CLIENT_SECRET |
OAuth 2.0 Client Secret for Microsoft Outlook |
AZURE_TENANT_ID |
Azure Tenant ID (e.g., common or specific Directory ID) |
Optional worker and database controls use conservative defaults from .env.example:
| Variable | Default | Description |
|---|---|---|
DB_POOL_MAX |
5 |
Maximum PostgreSQL connections per Node.js process |
DB_CONNECTION_TIMEOUT_MS |
10000 |
Maximum time to wait for a PostgreSQL connection |
SYNC_WORKER_CONCURRENCY |
1 |
Concurrent sync jobs per worker process |
EMAIL_PERSIST_CONCURRENCY |
3 |
Concurrent email persistence operations per account |
IMAP_STARTUP_CONCURRENCY |
10 |
IMAP listener connections opened in parallel at worker startup |
GMAIL_POLL_INTERVAL_MS |
60000 |
Gmail polling interval |
GMAIL_REQUEST_INTERVAL_MS |
1000 |
Minimum local spacing between Gmail API requests for one identity |
IMAP_REQUEST_INTERVAL_MS |
250 |
Minimum local spacing between Outlook/IMAP requests for one account |
IMAP_RECONCILE_INTERVAL_MS |
300000 |
IMAP recovery reconciliation interval |
OUTBOUND_HOST_ALLOWLIST |
empty | Exact host=cidr allowlist for reserved/non-public IMAP/SMTP destinations |
System Preferences affects every organization. Access requires both an exact user ID in the server-only INSTANCE_ADMIN_USER_IDS deployment allowlist and current administrator membership in the active organization. Organization administration alone does not grant instance access.
- Create or identify the intended user and explicitly assign organization administrator membership through the existing organization administration process.
- Obtain the user's immutable
User.idfrom the database (users.id) or the authenticated/api/auth/meresponse (user.id). Use IDs, not email addresses, organization IDs, or wildcard patterns. - Set the web application's runtime environment, for example
INSTANCE_ADMIN_USER_IDS=user-id-one,user-id-two. Entries are comma-separated, trimmed, and matched exactly. Keep this variable server-only. - Restart/recreate every web process with the same configuration. For Docker, supply it through the web container's environment or
--env-file.
An absent, empty, or whitespace-only allowlist denies all instance settings reads and writes. To revoke access, remove the ID and restart the web processes; removing or downgrading the user's active organization membership also blocks access, even with an existing session.
On upgrade, existing organization administrators lose System Preferences access until explicitly provisioned. Existing global values, organization AI preferences, and personal semantic-search preferences continue to apply under their existing permissions. The instance-administrator policy itself grants no roles automatically. Version 0.4.0 removes the personal AI toggle. The initial create-admin command creates an organization administrator only; it does not provision instance access.
# Start PostgreSQL, Redis, and Meilisearch
npm run services:upnpx prisma generate
npx prisma migrate deployTo create an initial admin account for a clean deployment:
npm run create-admin
# Or pass parameters inline:
npm run create-admin admin@example.com "Admin User" "Password123!" "My Organization"To seed synthetic demo data into a local development database (optional):
npm run db:seedThe demo seed is repeatable and only targets a loopback database from .env.local. It refuses to run in production, against a non-local database, or while a root .env file exists. For demo development, start only the local dependencies with docker compose up -d postgres redis meilisearch; do not start the bundled web and worker containers.
Demo login: admin@demo.mailhub.test / DemoPass123!. The seeded mail accounts are inactive and have no server credentials, so they cannot connect to external mail servers.
# Development server (with experimental HTTPS for PWA & OAuth)
npm run devOpen https://localhost:3000.
Workers sync email accounts in the background:
npm run workerAn email account keeps the workerPartition stored with the account. Its initial-sync job is routed to that partition's queue, so existing accounts assigned to worker-1 remain handled by a worker deployed with WORKER_PARTITION=worker-1. Adding workers does not automatically rebalance existing accounts; keep every partition that owns accounts deployed until you deliberately reassign those accounts.
For a ten-worker deployment, start conservatively with SYNC_WORKER_CONCURRENCY=1 and EMAIL_PERSIST_CONCURRENCY=3. DB_POOL_MAX is per Node.js process, so budget it across every worker and the web process rather than per email account. The defaults in .env.example limit the database pool to five connections per process and use a ten-second pool checkout timeout.
By default, MailHub rejects IMAP/SMTP hosts that resolve to non-public addresses (SSRF protection). For self-hosted deployments that intentionally connect to a LAN mail server, or through a transparent proxy that returns fake-IP DNS answers, one opt-in setting is available:
# Exact hostname -> reserved CIDR. Repeat the host for multiple address ranges.
OUTBOUND_HOST_ALLOWLIST=mail.example.com=192.168.1.10/32,imap.example.com=198.18.0.0/15Rules:
- Exact hostname match only; wildcards and IP literals are not supported.
- Every resolved address must be inside one of the configured CIDRs.
- CIDRs must be inside a reserved/non-public range;
0.0.0.0/0and other public ranges are ignored. - Malformed entries are ignored; the host remains blocked.
- Only set the ranges your deployment actually uses; avoid broad link-local or metadata ranges.
- This exception applies to IMAP/SMTP host resolution only, not OAuth/HTTP requests.
To execute unit and API endpoint tests:
npm test| Method | Route | Description |
|---|---|---|
| POST | /api/auth/register |
Register new user + organization |
| POST | /api/auth/login |
Authenticate and receive JWT |
| POST | /api/auth/logout |
Clear authentication session |
| GET | /api/auth/me |
Fetch active user profile |
| POST | /api/auth/password |
Update user account password |
| GET | /api/accounts |
List connected email accounts |
| POST | /api/accounts |
Add an email account (IMAP/SMTP or OAuth) |
| DELETE | /api/accounts/[id] |
Remove connected email account |
| POST | /api/accounts/[id]/test |
Test IMAP/SMTP connection settings |
| POST | /api/accounts/[id]/read-all |
Mark all emails in account as read |
| POST | /api/accounts/[id]/reindex |
Trigger Meilisearch reindexing for account |
| GET | /api/accounts/[id]/stats |
Fetch synchronization and message stats |
| GET | /api/accounts/[id]/emails |
Fetch emails for account folder |
| POST | /api/accounts/[id]/emails/send |
Send email via SMTP, Gmail, or Microsoft OAuth API |
| GET/PATCH/DEL | /api/accounts/[id]/emails/[emailId] |
Fetch details, update status/starred, or trash/delete email |
| POST | /api/accounts/[id]/emails/[emailId]/restore |
Restore email from trash to original folder |
| GET | /api/accounts/[id]/emails/[emailId]/attachments/[attachmentId] |
Download email attachment |
| POST | /api/accounts/[id]/emails/empty-trash |
Purge all emails in trash for account |
| GET/POST | /api/accounts/[id]/drafts |
Manage email drafts |
| POST | /api/accounts/[id]/drafts/sync |
Sync local email draft to server |
| GET | /api/accounts/oauth/google/init |
Initiate Google OAuth 2.0 authorization |
| GET | /api/accounts/oauth/google/callback |
Google OAuth 2.0 callback redirect |
| GET | /api/accounts/oauth/microsoft/init |
Initiate Microsoft Outlook OAuth 2.0 authorization |
| GET | /api/accounts/oauth/microsoft/callback |
Microsoft Outlook OAuth 2.0 callback redirect |
| GET | /api/emails/new |
Aggregated stream of recent emails across all accounts |
| GET | /api/emails/search |
Full-text search across emails (Meilisearch) |
| GET | /api/emails/thread/[threadId] |
Fetch email thread by thread ID |
| POST | /api/emails/labels |
Apply or remove labels from emails |
| GET/POST | /api/labels |
List or create organization labels |
| PATCH/DELETE | /api/labels/[id] |
Update or delete organization label |
| GET/POST | /api/labels/[id]/emails |
Manage emails assigned to label |
| GET/POST | /api/labels/[id]/accounts |
Manage accounts assigned to label |
| GET/POST/DEL | /api/favourites |
Manage user favorite emails |
| POST | /api/favourites/reorder |
Reorder user favorite items |
| GET | /api/org |
Organization details |
| GET | /api/org/members |
Organization members list |
| PATCH/DELETE | /api/org/members/[userId] |
Update member role or remove member |
| GET/POST | /api/org/members/[userId]/accounts |
Manage member access control for accounts |
| GET | /api/activity |
Fetch activity audit log |
| GET | /api/dashboard |
Dashboard summary stats |
| GET | /api/realtime/stream |
SSE stream for live updates |
| GET | /api/version |
Service Worker app version check |
| GET | /api/pwa |
Check PWA system status |
| POST | /api/notifications/subscribe |
Register Web Push subscription |
| POST | /api/notifications/test |
Send test Web Push notification |
| Route | Page |
|---|---|
/login |
Sign in page |
/register |
Sign up page |
/inbox |
Account-specific email inbox with search, context menus & thread viewer |
/all-emails |
Aggregated multi-account email feed with reading pane customization (Right, Bottom, Off) |
/new-emails |
Live incoming email feed (redirects to /all-emails) |
/overview |
Analytics dashboard & overview statistics |
/labels |
Label & category management |
/settings |
Account, team access, and organization settings |
/activity |
System audit and activity logs |
/testing |
Integration test sandbox |
- System Installation Check: Utilizes
navigator.getInstalledRelatedApps()and display mode media queries (standalone,window-controls-overlay) to detect if MailHub is installed on the user's OS. - Smart Install UI: Displays an Install MailHub App button in the sidebar for browser tabs; automatically hides when accessed inside the installed PWA app or system.
- Icon Generation: Generated app icon suite available via
node scripts/generate-pwa-icons.mjs.
Pull and run the official pre-built MailHub Docker image directly from Docker Hub:
# Pull this release from Docker Hub (`latest` points to the same image)
docker pull kawing11a/mailhub:0.4.0
# Run MailHub web container (using your .env file)
docker run -d \
--name mailhub-web \
-p 3000:3000 \
--env-file .env \
kawing11a/mailhub:0.4.0
# Run background worker container
docker run -d \
--name mailhub-worker \
--env-file .env \
kawing11a/mailhub:0.4.0 \
npm run workerBuild and run the complete stack (MailHub web, workers, PostgreSQL, Redis, Meilisearch):
docker compose up -d --buildThe Dockerfile uses a multi-stage build (deps → build → runner) producing a lean production image. Workers run as separate containers using the same image with npm run worker as the entrypoint.
When upgrading an existing deployment, run npx prisma migrate deploy before starting the 0.4.0 web and worker containers.
src/
├── app/
│ ├── (auth)/ # Login & register pages
│ ├── (dashboard)/ # Authenticated dashboard pages
│ │ ├── activity/ # Activity log
│ │ ├── all-emails/ # Aggregated multi-account email feed
│ │ ├── inbox/ # Account email inbox & thread viewer
│ │ ├── labels/ # Label management
│ │ ├── new-emails/ # Redirect to all-emails feed
│ │ ├── overview/ # Dashboard stats
│ │ ├── settings/ # Settings & team account access control
│ │ └── testing/ # Testing sandbox
│ ├── api/ # REST API routes
│ │ ├── accounts/ # Email account CRUD, OAuth, testing, stats, drafts, and member access
│ │ ├── activity/ # Activity audit log
│ │ ├── auth/ # Auth endpoints (login, logout, me, password, register)
│ │ ├── dashboard/ # Summary statistics
│ │ ├── emails/ # Email search, threads, new email stream, and labels
│ │ ├── favourites/ # Favorites management & reordering
│ │ ├── labels/ # Organization and account label management
│ │ ├── notifications/# Push subscriptions & test notifications
│ │ ├── org/ # Organization & team member access management
│ │ ├── pwa/ # PWA status check
│ │ ├── realtime/ # Server-Sent Events stream
│ │ └── version/ # PWA / App version check
│ ├── layout.tsx # Root layout with local Geist font loading
│ └── manifest.ts # Web App Manifest for PWA
├── components/
│ ├── email/ # Email list, thread viewer, compose modal, context menu
│ ├── labels/ # Label assignment picker & list components
│ ├── providers/ # OAuth provider integration components
│ ├── pwa/ # PWA Install button & instruction modal
│ ├── search/ # Meilisearch query bar
│ ├── settings/ # Settings, account access modal, accounts list, edit account modal
│ └── sidebar/ # Dynamic navigation sidebar with PWA install integration
├── hooks/ # Custom React hooks (usePWAInstall, PWA updates, SSE, queries)
├── lib/
│ ├── accounts/ # Account management logic & OAuth tokens
│ ├── auth/ # JWT + session utilities
│ ├── db/ # Prisma client
│ ├── gmail/ # Gmail OAuth & API integration
│ ├── imap/ # IMAP connection manager & sync logic
│ ├── queue/ # BullMQ job definitions
│ ├── search/ # Meilisearch client
│ ├── smtp/ # Multi-provider email sender client (SMTP, Gmail API, Microsoft API)
│ ├── crypto.ts # AES credential encryption
│ ├── redis.ts # Redis client singleton
│ └── validation/ # Zod validation schemas
├── stores/ # Zustand state stores (UI, sync, accounts)
└── worker.ts # BullMQ worker entry point
scripts/ # Database seed & PWA icon generation scripts
prisma/
├── schema.prisma # Database schema
└── migrations/ # Migration history
If MailHub helps you or your organization manage email workflows effectively, please consider supporting ongoing development and maintenance!
Contributions are always welcome! Whether you are reporting bugs, improving documentation, or submitting pull requests, please read our Contributing Guide and review our Code of Conduct before getting started.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
For security disclosures, supported versions, and vulnerability reporting procedures, please read our Security Policy.
This project is licensed under the MIT License - see the LICENSE file for details.
Model endpoint operators: see model outbound origin policy for approval configuration, local-service compatibility, credential isolation and rollout checks.


