Features
What's in the box, with enough implementation detail to be honest about depth.
Email-verified signup
New accounts can't log in until the email address is confirmed. The signup form sends a signed, time-limited token via Resend; the user clicks the link to flip the account from pending to active.
- Tokens are signed with Django's
django.core.signingand validated on the callback. - Verification delivery failures surface as inline form errors instead of a silent fail.
- The confirmation screen includes a "check your Spam folder" hint — new domains often land there first.
Login by email OTP (2FA)
Two-factor authentication is on by default for every new account. After the password, you enter a 6-digit code delivered to your verified email.
- Codes have a TTL — they auto-expire after a few minutes.
- Five wrong attempts invalidate the code; the user must request a new one.
- Resend is rate-limited to keep an attacker from triggering email flooding.
- Users can toggle 2FA from the profile page if they really need to — but the portal stores IMAP credentials, so the recommendation is on.
Password reset over email
Standard Django reset token flow. A signed link with a one-hour TTL is sent to the verified email; clicking it lets the user pick a new password. Reset emails are rate-limited per-user and per-IP.
Multi-account IMAP linking
Connect mailboxes one at a time, or paste a CSV to bulk-add several at once.
- Per-account fields: email, password, IMAP host, IMAP port, optional group label.
- The bulk-add form takes four columns:
email,password,host,port. Empty lines are ignored; duplicates and over-cap rows are reported in the result summary. - Each account has a Test button that opens an IMAP connection live and reports back without leaving the page.
Per-account actions
- Test — open an IMAP session and report success or the server's error.
- Enable / disable — keep an account in the list but stop polling it.
- Rotate password — re-encrypt and store a new password without touching anything else.
- Edit — change host, port, group, or display name.
- Delete — drop the row; the ciphertext is removed with it.
Inbox view
The inbox reads the live IMAP server on every request — no server-side message cache.
- Filters: window (1 day / 7 days / 30 days), folder, account, group.
- Per-message actions: open, mark unread, delete. All hit the live server — there's no portal-only "hide".
- Pulls via imap-tools.
Production-shaped settings
- HTTPS-only in production:
SECURE_SSL_REDIRECT+ preloaded HSTS. - Secure, HttpOnly, SameSite-Lax cookies for session and CSRF.
- Hardening headers explicit:
X-Frame-Options: DENY,X-Content-Type-Options: nosniff,Referrer-Policy: same-origin,Cross-Origin-Opener-Policy: same-origin. - Custom no-cache middleware on every HTML response so signed-in pages don't sit in proxy or browser caches after logout.
- Static files served by WhiteNoise with finders-mode enabled — a tuning the deploy notes explain.
- Postgres via
DATABASE_URLin production, SQLite fallback locally.
Staff admin panel
Staff users get a Users page listing signup date, last-login, and the number of connected accounts per user. Account-limit overrides are editable here, so admins can raise the cap for specific accounts without touching code.
Mobile-first UI
Bottom navigation bar on small screens, dropdown user menu on desktop.
Dark / light theme toggle backed by localStorage and the
user's OS preference. Bootstrap 5.3 throughout.