- TypeScript 98.9%
- CSS 0.9%
- JavaScript 0.1%
- Attachments from providers that stamp a Content-ID on every part (e.g. Gmail) now show in the attachment panel; previously they were silently filtered out as "inline" even when the HTML body never cited their cid. The viewer now parses the body for actual cid: references and treats only those attachments as inline (#58). - Email-to-self no longer gets discarded as a duplicate by the sender's own MTA. The send flow keeps the outgoing message in Drafts during submission and uses EmailSubmission.onSuccessUpdate\ Email to move it to Sent only after SMTP has accepted it, so the inbound delivery for self-send doesn't match a pre-existing Message-ID in the account (#60). Thanks @melges-morgen and @tamisoft for the reports. |
||
|---|---|---|
| .github/workflows | ||
| .husky | ||
| app | ||
| components | ||
| contexts | ||
| e2e | ||
| hooks | ||
| i18n | ||
| lib | ||
| locales | ||
| public | ||
| screenshots | ||
| stores | ||
| .dockerignore | ||
| .env.example | ||
| .gitignore | ||
| CHANGELOG.md | ||
| CONTRIBUTING.md | ||
| docker-compose.yml | ||
| Dockerfile | ||
| eslint.config.mjs | ||
| instrumentation.node.ts | ||
| instrumentation.ts | ||
| LICENSE | ||
| next.config.ts | ||
| package-lock.json | ||
| package.json | ||
| playwright.config.ts | ||
| postcss.config.mjs | ||
| proxy.ts | ||
| README.md | ||
| ROADMAP.md | ||
| tailwind.config.ts | ||
| tsconfig.json | ||
| VERSION | ||
| vitest.config.ts | ||
| vitest.setup.ts | ||
JMAP Webmail
A webmail client for Stalwart Mail Server, built with Next.js and the JMAP protocol.
Why Stalwart?
Stalwart is a mail server written in Rust with native JMAP support, not IMAP/SMTP with JMAP added as an afterthought. It handles JMAP, IMAP, SMTP, and ManageSieve. Self-hosted, no third-party dependencies.
Stalwart on GitHub | Documentation
Features
- Read, compose, reply, reply-all, and forward
- HTML rendering with DOMPurify sanitization and sandboxed iframe for complex emails
- Attachment upload and download
- Draft auto-save with discard confirmation
- Threading with inline expansion
- Mark as read/unread, star/unstar
- Archive and delete with configurable behavior
- Color tags/labels with sidebar counts
- Search with JMAP filter panel, search chips, cross-mailbox queries
- Virtual scrolling for large lists
- Empty folder (one-click empty Junk/Trash with batch progress)
- Sender info panel (click sender name to view contact, add to contacts, search)
- API retry with exponential backoff for transient failures
Interface
- Three-pane layout with dark and light themes
- Responsive (desktop sidebar + mobile bottom tab bar + mobile action bar)
- Keyboard shortcuts
- Drag-and-drop email organization
- Multi-select emails (checkboxes, shift-click) with batch move/delete toolbar
- Folder management (create, rename, move, delete) from sidebar context menu
- Drag-and-drop folder reparenting
- Right-click context menus (long-press on touch devices)
- Extra-compact, compact, regular, and comfortable density options
- Resizable sidebar (drag, touch, keyboard)
- Animations that respect
prefers-reduced-motion - Infinite scroll pagination
- Toast notifications with undo support
- Form validation with shake feedback
- Safe area insets for notched devices
- Screen reader live regions
Real-time
- Push notifications via JMAP EventSource
- Live unread counts
- Email arrival notifications
- Connection status indicator
Identity management
- Multiple sender identities with per-identity signatures
- Sub-addressing (user+tag@domain.com) with tag suggestions
- Identity badges in viewer and list
Address book
- Contact management with search and filtering
- JMAP server sync (RFC 9553/9610) with local fallback
- Email autocomplete in composer
- Contact groups with group expansion
- vCard import/export (RFC 6350) with duplicate detection
- Bulk operations (multi-select, delete, group add, export)
Calendar
- JMAP Calendar (RFC 8984) with capability detection
- Month, week, day, and agenda views
- Event create, edit, delete with recurrence and reminders
- Participant scheduling with iTIP invitations and RSVP
- Inline calendar invitation banner in email viewer (.ics detection, RSVP, import)
- Multi-day events, column-based overlap layout
- Mini-calendar sidebar with calendar visibility toggles
- Locale-aware date formatting
- Settings for first day of week, time format (12h/24h), default view
- Drag-and-drop rescheduling with time snap
- Click-drag on empty slots to create events
- Resize events by dragging (15-minute snap)
- Double-click quick create
- Event duplication (+1 day offset)
- Recurring event edit/delete scope (this, this and following, all)
- iCalendar (.ics) file import with preview
- Real-time updates via JMAP push
- Event notifications with configurable sound
Email templates
- Reusable templates organized by category
- Placeholder variables (
{{recipientName}},{{date}}, etc.) with auto-fill - Template picker in compose toolbar with search and filter
- Template manager in settings
Email filters
- Server-side filtering with JMAP Sieve Scripts (RFC 9661)
- Visual rule builder: conditions (From, To, Subject, Size, Body...) and actions (Move, Forward, Mark read, Star, Discard, Reject...)
- Raw Sieve editor with syntax validation
- Auto-save with rollback on failure
- Drag-and-drop rule reordering
- Only shown when the server supports Sieve
Vacation responder
- JMAP VacationResponse with date range scheduling
- Settings tab for message configuration
- Sidebar indicator when active
Security and privacy
- External content blocked by default
- Trusted senders list for automatic image loading
- HTML sanitization (DOMPurify)
- SPF/DKIM/DMARC status indicators with plain-language tooltips
- Session-based auth, no password storage by default
- TOTP two-factor authentication
- "Remember me" with AES-256-GCM encrypted httpOnly cookie (opt-in)
- OAuth2/OIDC with PKCE for SSO (opt-in, RP-initiated logout)
- External IdP support (Keycloak, Authentik) via configurable issuer URL
- CORS misconfiguration detection with detailed error messages
- Shared folder support
- Newsletter unsubscribe (RFC 2369)
- CSP, X-Content-Type-Options, X-Frame-Options, Referrer-Policy headers
Internationalization
- 8 languages: English, French, Japanese, Spanish, Italian, German, Dutch, Portuguese
- Automatic browser language detection
- Persistent language preference
Deployment
- Pre-built Docker images on Docker Hub and GHCR (amd64/arm64)
- Multi-stage build with standalone output
- Runtime environment variables (no rebuild needed)
- Health check endpoint
- Structured logging (text/JSON)
- Update check on startup (server logs only, no client exposure)
Tech stack
- Next.js 16 with App Router
- TypeScript
- Tailwind CSS v4
- Zustand for state management
- Custom JMAP client (RFC 8620)
- next-intl for i18n
- Lucide React icons
Getting started
Prerequisites
- Node.js 18+
- A JMAP-compatible mail server (Stalwart recommended)
Installation
git clone https://github.com/root-fr/jmap-webmail.git
cd jmap-webmail
npm install
cp .env.example .env.local
Configuration
Edit .env.local:
# App name displayed in the UI
APP_NAME=My Webmail
# Your JMAP server URL (required)
JMAP_SERVER_URL=https://mail.example.com
These are runtime environment variables, read at request time. Docker deployments can be configured without rebuilding. Legacy NEXT_PUBLIC_* variables still work as fallbacks.
OAuth2/OIDC (optional)
To enable SSO login alongside Basic Auth:
OAUTH_ENABLED=true
OAUTH_CLIENT_ID=webmail
OAUTH_CLIENT_SECRET= # optional, for confidential clients
OAUTH_ISSUER_URL= # optional, for external IdPs (Keycloak, Authentik)
Endpoints are auto-discovered via .well-known/oauth-authorization-server or .well-known/openid-configuration. If your JMAP server delegates auth to an external IdP, set OAUTH_ISSUER_URL to the IdP's base URL (e.g., https://keycloak.example.com/realms/mail).
To disable Basic Auth:
OAUTH_ONLY=true
Remember me (optional)
To enable "Remember me" for Basic Auth login:
SESSION_SECRET=your-secret-key # Generate with: openssl rand -base64 32
When set, a "Remember me" checkbox appears on the login form. Credentials are encrypted with AES-256-GCM and stored in an httpOnly cookie (30-day expiry).
Development
npm run dev # Start dev server
npm run typecheck # Type checking
npm run lint # Linting
Production
npm run build
npm start
Docker
# Pre-built image
docker run -p 3000:3000 -e JMAP_SERVER_URL=https://mail.example.com rootfr/jmap-webmail:latest
# From GHCR
docker run -p 3000:3000 -e JMAP_SERVER_URL=https://mail.example.com ghcr.io/root-fr/jmap-webmail:latest
# With docker compose
cp .env.example .env.local
# Edit .env.local with your JMAP_SERVER_URL
docker compose up -d
# Build from source
docker build -t jmap-webmail .
docker run -p 3000:3000 -e JMAP_SERVER_URL=https://mail.example.com jmap-webmail
Keyboard shortcuts
| Key | Action |
|---|---|
j / k |
Navigate between emails |
Enter / o |
Open selected email |
Esc |
Close viewer / deselect |
c |
Compose new email |
r |
Reply |
R / a |
Reply all |
f |
Forward |
s |
Toggle star |
e |
Archive |
# / Delete |
Delete |
u |
Mark as unread |
/ |
Focus search |
x |
Expand/collapse thread |
Ctrl+Shift+T |
Insert template |
? |
Show shortcuts help |
Screenshots
|
Login
|
Inbox
|
|
Email Viewer
|
Compose
|
|
Dark Mode
|
Settings
|
Contributing
See CONTRIBUTING.md for guidelines.
Roadmap
See ROADMAP.md for planned features.
Acknowledgments
- Stalwart Labs for the mail server
- The JMAP working group for the protocol spec
License
MIT. See LICENSE.





