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.signing and 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.

Encrypted credential storage

IMAP passwords never sit in the database in plaintext. They're stored as Fernet ciphertext using a FIELD_ENCRYPTION_KEY that is separate from Django's SECRET_KEY — rotating one doesn't invalidate the other. See the Security page for the full story.

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_URL in 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.

See it running on the live app or browse the source on GitHub.