SelfMailer

SelfMailer

Docker app from S3lfcod3r's Repository

Overview

Self-hosted, multi-user e-mail client (not a mail server). Connects to existing IMAP/POP3/SMTP accounts, with calendar, contacts and notes. Admin can create users and pre-configure their accounts; users can also add accounts themselves. Single Docker container with SQLite - no external database.
SelfMailer logo

Self-hosted, multi-user e-mail client — with calendar, contacts, notes, a native Android app and real-time sync.
Selbstgehosteter Mehrbenutzer-Mailclient — mit Kalender, Kontakten, Notizen, nativer Android-App und Echtzeit-Sync.

Build Version License Backend Web App Ko-fi

🇬🇧 English · đŸ‡©đŸ‡Ș Deutsch

📰 What's new / Neuigkeiten → CHANGELOG.md


🇬🇧 English

A single Docker container that is not a mail server — it's a client for the mailboxes you already have. It connects to any IMAP account to read and to SMTP to send, and adds a calendar, contacts, notes and tasks on top. A polished web UI and a native Android app share one JSON API — and stay in sync live across devices.

Part of the Self family (SelfAuthenticator, SelfArchiver, SelfDashboard 
) — same design system, same deploy style (GHCR → Unraid).

TL;DR — IMAP/SMTP mail client · calendar/contacts/notes · web + native Android · live cross-device sync · 3 notification options (FCM / ntfy / background) · 2FA · single container · no external DB.

📑 Table of contents

Features · Blocking senders · Architecture · Notifications · Live sync · Quick start · Unraid · Configuration · Backups · Android app · API · Security · Tech stack · Development · Roadmap

✹ Features

📬 Mail

  • Multiple IMAP / SMTP accounts per user
  • Thunderbird-style stacked accounts with a per-account folder tree and unread counters
  • Read HTML mail (hardened), download/open attachments, SPF/DKIM/DMARC spoofing check
  • Compose / reply / forward with a per-account From picker and signatures; drafts, read/delivery receipts
  • Star, mark read/unread, delete, move, cross-account transfer
  • Multi-select (long-press → select → bulk move / read / unread / star / delete)
  • Filter rules (move/star/mark/delete — applied automatically on every background sync) and server-side mailbox migration
  • Mailbox cleanup: find similar mail by sender, domain, subject, keyword or label, preview, then move in bulk; saved search views
  • Block sender in one click → mail goes to trash (existing + future); a blocked-senders list lets you undo any block. Rules also match the reply-to address and the newsletter ID (List-Id) and show a hit counter
  • Auto-empty spam & trash per account (off / immediately / after 7 / 30 days)
  • Search with quick filters (unread · has attachment · starred), pagination, snippets
  • A local SQLite cache kept warm by a background sync → the UI never waits on a slow provider
  • New mail right away via IMAP IDLE: the server reports new mail itself (2-minute polling stays as a safety net); the text of new mail is prefetched, so it opens instantly

đŸ›Ąïž Spam & virus protection (Settings → Security, everything off until you switch it on)

  • Blocks for all accounts — a sender blocked in one account is removed from all your accounts
  • One-click unsubscribe for newsletters (RFC 8058) — with a warning when the sender is not verified
  • Virus scan of attachments with your own ClamAV — new mail is checked before it shows up or pushes; infected mail goes to spam, infected downloads are blocked
  • Spam filter with rspamd — rates new mail, moves spam away and learns from your “Spam” button

📅 Organizer

  • Calendar — local events, month/agenda views, birthdays from contacts
  • Contacts — rich address book (phones, address, organisation, website, birthday); import from / export to the phone, optional two-way sync
  • Notes & Tasks
  • CalDAV / CardDAV pull from external servers (Nextcloud, Synology 
) + subscribable ICS / vCard export feeds

🔔 Notifications — you choose

  • FCM (Google Push) — instant, no persistent notification (like Gmail/Synology)
  • ntfy — self-hosted push, no Google
  • Background check — ~1 min, zero setup
  • Per account, per folder — pick exactly which mailboxes notify you
  • Configurable in both the web UI and the app — one shared setting

🔄 Live sync (SSE)

  • Read/delete/move on your phone → an open web tab updates instantly (and vice versa)

🔐 Security & platform

  • Multi-user with admin/user roles; admin can pre-configure accounts for users
  • 2FA / TOTP login with backup codes; biometric app-lock on Android
  • Account credentials and the mail cache Fernet-encrypted at rest
  • i18n DE/EN, light/dark themes + custom accent colours
  • Single container — FastAPI + SQLite, no external database

đŸš« Blocking senders

Block a sender — open the mail → click ⋯ (top right) → đŸš« Block sender → confirm. All mail from that sender (existing + future, inbox + spam) is moved to trash and marked read. The trash auto-empties after 7 days (a safe undo window).

Undo a block — click your đŸ‘€ user menu (top right) → 🔀 Rules → the đŸš« Blocked senders box at the top lists every block; hit ↩ Unblock to let future mail through again. To recover a specific message, open it in the Trash folder and move it back to the inbox (within 7 days).

Still getting mail from a blocked sender? Spammers keep changing their address. Under ⚙ Settings → Security → Spam & virus protection you can:

  • let every block apply to all your accounts,
  • and under 🔀 Rules block by reply-to address or newsletter ID (List-Id) — features spammers change less often than the sender.

Each rule shows how often it matched — so you can see whether a block still works. For real newsletters, the Unsubscribe button (same settings page) is the cleaner way out; for unknown senders, blocking is safer, because unsubscribing confirms that your address exists.

💡 Tip: auto-empty timings (spam & trash: off / immediately / after 7 / 30 days) live under đŸ‘€ → ✎ Edit account.

🧭 Architecture

        ┌──────────────┐          ┌───────────────┐
        │   Web UI     │          │  Android app  │
        │ React + Vite │          │ Kotlin/Compose│
        └──────┬───────┘          └──────┬────────┘
               └───── JSON API + SSE ─────┘
                   /api/v1 · JWT (Bearer)
                          │
                 ┌────────▌─────────┐
                 │   FastAPI core   │  SQLite (cache + config)
                 │  background sync │  Fernet-encrypted secrets
                 │  + event bus     │  in-memory pub/sub → live sync
                 └────────┬─────────┘
        ┌──────────┬──────┮───────┬──────────────┬───────────┐
       IMAP       SMTP     CalDAV / CardDAV      FCM         ntfy
   (read mail) (send mail) (cal & contacts) (Google push) (self-hosted)

🔔 Notifications

Pick a method in Settings → E-mail notifications (web user menu → 🔔 / app Settings). The server can drive all three in parallel — e.g. FCM on your main phone, ntfy on a de-Googled one.

Method Latency Persistent notification Needs
FCM (Google Push) instant none Firebase project + Google Play Services
ntfy instant none (the ntfy app holds one connection for all apps) ntfy container + ntfy app
Background check ~1 min yes (Android requirement for a foreground service) nothing

Per folder: under Choose folders you pick, per account, which folders trigger a push.

Setup — FCM (Google Push)
  1. Create a free Firebase project at https://console.firebase.google.com.
  2. Add Android app with package name com.selfmailer.viewer → download google-services.json (goes into the app build).
  3. Project settings → Service accounts → Generate new private key → download the JSON.
  4. Put that JSON on the server as 
/selfmailer/data/fcm-service-account.json and set SELFMAILER_FCM_CREDENTIALS=/data/fcm-service-account.json → restart the container.
  5. In the app: notifications on → Google Push (FCM) → pick folders → use Send test push to verify.

The push payload is intentionally minimal ("2 new e-mails"); the app fetches the actual content from your server, so Google never sees mail bodies.

Setup — ntfy (self-hosted)
  1. Run an ntfy container (e.g. binwiederhier/ntfy serve, host port 8095 → container 80, NTFY_BASE_URL=http://<host>:8095).
  2. Install the ntfy app, add your server, subscribe to the topic SelfMailer shows you.
  3. In SelfMailer: notifications on → ntfy → enter the URL → Save & enable → pick folders.

🔄 Live sync

Every open client (web tab, app in the foreground) holds a thin SSE connection (/api/v1/events/stream). Whenever anyone performs an action — or new mail arrives — the server emits a tiny event and the other clients refresh the affected folder. No polling storm, no Google, near-instant.

🚀 Quick start (Docker)

cp .env.example .env
# generate a secret and put it in .env (SELFMAILER_SECRET):
python -c "import secrets; print(secrets.token_hex(32))"

docker compose up -d

Open http://<host>:8090 → the first-run setup creates the admin account → then add your mail accounts.

For access from outside your LAN, put a reverse proxy in front: HTTPS guide.

📩 Unraid

Add the template from https://raw.githubusercontent.com/s3lfcod3r/selfmailer/main/unraid/selfmailer.xml or import it under Docker → Add Container → Template. Set the Master Secret, leave the rest on defaults.

⚠ Master Secret: never change SELFMAILER_SECRET after first run — it encrypts the stored mailbox passwords. Changing it makes them unreadable.

⚠ Permissions: the container runs non-root (uid 99 / gid 100 = nobody:users). If a data file is owned by root, fix it once: chown -R 99:100 /mnt/user/appdata/selfmailer (or Tools → Docker Safe New Permissions).

⚙ Configuration

Environment variables, all with the prefix SELFMAILER_.

Variable Required Default Purpose
SELFMAILER_SECRET ✅ – JWT signing and at-rest encryption of mailbox passwords (≄ 32 chars). Do not change after setup.
SELFMAILER_DB_PATH – /data/selfmailer.db SQLite path
SELFMAILER_FCM_CREDENTIALS – /data/fcm-service-account.json Firebase service-account JSON for Google push. Empty/missing = FCM off.
SELFMAILER_TRANSLATE_URL – – Self-hosted LibreTranslate URL for in-app translation (e.g. http://192.168.1.10:5000). Empty = off (button hidden).
SELFMAILER_TRANSLATE_API_KEY – – Optional API key, only if your LibreTranslate instance requires one
SELFMAILER_ADMIN_TOKEN – – If set, first-run admin setup requires this token
SELFMAILER_BASE_URL – – Public base URL (e.g. for feed links)
SELFMAILER_SYNC_INTERVAL – 120 Seconds between background mail syncs (lower = faster push & sync)
SELFMAILER_IDLE – 1 IMAP IDLE on the inbox: new mail is reported by the server right away (one extra connection per account); 0 = off
SELFMAILER_PREFETCH_NEW – 20 Prefetch the full text of up to N new inbox mails per run (≀ 1 MB each, not marked as read); 0 = off
SELFMAILER_CLAMAV – – ClamAV (clamd) host:port for scanning attachments, e.g. 192.168.1.10:3310. LAN only – clamd has no password. Users switch it on under Settings → Security.
SELFMAILER_RSPAMD_URL – – rspamd scanner (port 11333) for the spam filter. Users switch it on under Settings → Security.
SELFMAILER_RSPAMD_CONTROLLER_URL / SELFMAILER_RSPAMD_PASSWORD – – Optional: rspamd controller (port 11334) + password so the "Spam" button teaches rspamd.
SELFMAILER_DAV_SYNC_INTERVAL – 120 Seconds between calendar/contact (DAV) syncs — separate from mail, min 30
SELFMAILER_SYNC_DISABLE – 0 Turn off the background sync
SELFMAILER_IMAP_TIMEOUT – 15 IMAP socket timeout (seconds)
SELFMAILER_DAV_BLOCK_PRIVATE – false Block private/LAN targets for DAV pull (SSRF strict mode)
SELFMAILER_JWT_ALGORITHM – HS256 HS256 / HS384 / HS512
SELFMAILER_BACKUP_ENABLED – true Nightly automatic DB backup (~03:00)
SELFMAILER_BACKUP_KEEP – 7 How many backups to keep (0 = keep all)

đŸ’Ÿ Backups

The scheduler takes a consistent nightly snapshot of the SQLite database (sqlite3 online-backup API, safe during writes) into data/backups/:

data/backups/selfmailer-YYYYMMDD-HHMMSS.db

Only the last SELFMAILER_BACKUP_KEEP snapshots are kept; the folder is git-ignored.

Restore — stop the container, replace the live DB with a snapshot, start again:

docker stop selfmailer
cp data/backups/selfmailer-20260101-030000.db data/selfmailer.db
# remove stale WAL/SHM sidecars so the restored file is authoritative
rm -f data/selfmailer.db-wal data/selfmailer.db-shm
docker start selfmailer

đŸ“± Android app

A real native client (android/SelfMailer.apk) — not a web-view wrapper. Built like Synology MailPlus:

  • Hamburger drawer → account switcher + special folders + sub-folders, with unread badges
  • Bottom navigation: Mail · Calendar · Notes
  • HTML mail, attachments, multi-select with a bulk action bar, search filters, Synology-style compose with From picker
  • Drafts are kept (encrypted locally) even after app-lock or restart
  • Contacts: import from / export to the phone, optional two-way sync
  • Biometric app-lock (face / fingerprint / device PIN), unread accent markers
  • Notifications: FCM / ntfy / background — your choice, per folder
  • Talks to the same /api/v1 as the web UI; server URL entered on first launch

🔌 API overview

Base path /api/v1, authentication via JWT Bearer. Interactive docs: http://<host>:8090/docs.

Group Endpoints (excerpt)
Auth auth/status, auth/setup, auth/login, auth/login/totp, auth/me, auth/totp/*
Accounts accounts (CRUD), accounts/{id}/test
Mail mail/{id}/folders[/counts], mail/{id}/messages, 
/{uid}, 
/flags, 
/move, 
/send, 
/transfer, 
/batch-*
Organizer calendar/events, contacts, notes, tasks
DAV / Feeds dav/accounts, feeds/token, calendar/export.ics, contacts/export.vcf
Push push (ntfy), push/folders (per-account folders), push/device (FCM tokens), push/test
Live events/stream (Server-Sent Events)
Dashboard dashboard/summary (bundled unseen counts)

đŸ›Ąïž Security

  • Login passwords Argon2-hashed; mailbox & DAV passwords Fernet-encrypted at rest (key from SELFMAILER_SECRET)
  • Data at rest: the mail cache (subject, sender, preview, full text) and scheduled mails are encrypted; database and backups readable by the container user only
  • IMAP certificates verified, optionally pinned per account (fingerprint) for self-signed LAN servers
  • Revocable sessions: logout, password/2FA changes and admin locks revoke tokens, SSE access and push registrations; CSRF protection for cookie sessions
  • JWT pinned to HS256/384/512; the 2FA intermediate token grants no access
  • Rate limiting on login/2FA/setup; defensive response headers; API responses Cache-Control: no-store; anti-enumeration dummy hash
  • SSRF guard with IP pinning on DAV pull, ntfy, translation and unsubscribe (loopback/link-local/cloud-metadata always blocked; unsubscribe only to public addresses); DAV credentials stay bound to their origin
  • Container: non-root (uid 99), app code read-only, no Linux capabilities (no-new-privileges, cap-drop=ALL), base images pinned by digest, Trivy image scan and dependency audits in CI
  • Virus scan never reports “clean” without a real scan — errors and oversized files are marked “not checked”
  • Android: allowBackup=false, token in the Android Keystore, hardened mail-HTML WebView (no JavaScript, no form submission), FileProvider with path-traversal check
  • Deliberate LAN trade-offs (documented): cleartext HTTP allowed (HTTPS optional in the container or via a reverse proxy), SELFMAILER_SECRET doubles as the encryption seed

Security update history → CHANGELOG.md (entries marked 🔒).

đŸ§± Tech stack

Part Tech
Backend FastAPI · SQLModel · SQLite (WAL) · httpx · aiosmtplib · imap-tools · PyJWT
Web React · Vite · TypeScript · EventSource (SSE)
App Kotlin · Jetpack Compose · OkHttp · WorkManager · Firebase Messaging · BiometricPrompt
Push FCM (Google) · ntfy (self-hosted)
Deploy Docker (multi-stage, non-root) · GHCR · Unraid

đŸ› ïž Development

# Backend
cd backend && pip install -r requirements.txt
export SELFMAILER_SECRET=$(python -c "import secrets; print(secrets.token_hex(32))")
uvicorn app.main:app --reload --port 8090

# Web (second terminal)
cd frontend && npm install && npm run dev   # http://localhost:5173, proxies /api → :8090

Tests:

cd backend && pip install -r requirements.txt -r requirements-dev.txt && pytest --cov=app

đŸ—ș Roadmap

  • Mail (IMAP/SMTP) · stacked accounts · folder tree + unread counts
  • Calendar · Contacts · Notes · Tasks · CalDAV/CardDAV + ICS/vCard feeds
  • 2FA / TOTP · filter rules · cross-account transfer · multi-select · search filters
  • Native Android app (Synology-style, biometric lock)
  • Notifications: FCM + ntfy + background, per account & folder
  • Live sync (SSE) between web clients and the Android app (foreground)
  • Calendar month grid in the app
  • Mailbox cleanup · saved views · phone contact import/export/sync
  • HTTPS reverse-proxy guide
  • OAuth for Gmail / Outlook
  • Tests toward 80 % coverage

⬆ back to top


đŸ‡©đŸ‡Ș Deutsch

Ein einzelner Docker-Container, der kein Mailserver ist — sondern ein Client fĂŒr die PostfĂ€cher, die du schon hast. Er verbindet sich mit jedem IMAP-Konto zum Lesen und mit SMTP zum Senden und bringt Kalender, Kontakte, Notizen und Aufgaben mit. Eine ausgefeilte Web-OberflĂ€che und eine native Android-App teilen sich eine JSON-API — und bleiben live ĂŒber GerĂ€te hinweg synchron.

Teil der Self-Familie (SelfAuthenticator, SelfArchiver, SelfDashboard 
) — gleiches Design-System, gleicher Deploy-Stil (GHCR → Unraid).

Kurz — IMAP/SMTP-Mailclient · Kalender/Kontakte/Notizen · Web + native Android · Live-Sync zwischen GerĂ€ten · 3 Benachrichtigungswege (FCM / ntfy / Hintergrund) · 2FA · ein Container · keine externe DB.

📑 Inhaltsverzeichnis

Funktionen · Absender blockieren · Architektur · Benachrichtigungen · Live-Sync · Schnellstart · Unraid · Konfiguration · Backups · Android-App · API · Sicherheit · Tech-Stack · Entwicklung · Roadmap

✹ Funktionen

📬 Mail

  • Mehrere IMAP-/SMTP-Konten pro Nutzer
  • Thunderbird-Ansicht: gestapelte Konten mit Ordnerbaum und Ungelesen-ZĂ€hlern
  • HTML-Mails lesen (gehĂ€rtet), AnhĂ€nge öffnen, SPF/DKIM/DMARC-Spoofing-Check
  • Schreiben / Antworten / Weiterleiten mit Absender-Auswahl und Signaturen; EntwĂŒrfe, Lese-/EmpfangsbestĂ€tigung
  • Stern, gelesen/ungelesen, löschen, verschieben, kontoĂŒbergreifend ĂŒbertragen
  • Mehrfachauswahl (lange drĂŒcken → auswĂ€hlen → Sammelaktionen Verschieben/Gelesen/Ungelesen/Stern/Löschen)
  • Filterregeln (Verschieben/Stern/Gelesen/Löschen — bei jedem Hintergrund-Sync automatisch angewandt) und serverseitige Postfach-Migration
  • Postfach aufrĂ€umen: Ă€hnliche Mails nach Absender, Domain, Betreff, Stichwort oder Label finden, Vorschau prĂŒfen, dann gesammelt verschieben; gespeicherte Suchansichten
  • Absender blockieren per Klick → Mails wandern in den Papierkorb (vorhandene + kĂŒnftige); eine Liste blockierter Absender macht jede Blockierung rĂŒckgĂ€ngig. Regeln greifen auch auf die Antwort-Adresse und die Newsletter-Kennung (List-Id) und zeigen einen TrefferzĂ€hler
  • Spam & Papierkorb automatisch leeren je Konto (aus / sofort / nach 7 / 30 Tagen)
  • Suche mit Schnellfiltern (ungelesen · Anhang · Stern), Seitenweise Anzeige, Vorschauen
  • Lokaler SQLite-Cache, vom Hintergrund-Sync warmgehalten → die OberflĂ€che wartet nie auf einen langsamen Anbieter
  • Neue Mails sofort per IMAP IDLE: der Server meldet neue Mails selbst (die Abfrage alle 2 Minuten bleibt als Sicherheitsnetz); der Text neuer Mails wird vorgeladen und öffnet sich ohne Wartezeit

đŸ›Ąïž Spam- & Virenschutz (Einstellungen → Sicherheit, alles aus, bis du es einschaltest)

  • Sperren fĂŒr alle Konten — ein in einem Konto gesperrter Absender verschwindet aus allen deinen Konten
  • Abmelden mit einem Klick fĂŒr Newsletter (RFC 8058) — mit Warnung, wenn der Absender nicht geprĂŒft ist
  • Virenscan der AnhĂ€nge mit deinem eigenen ClamAV — neue Mails werden geprĂŒft, bevor sie erscheinen oder pushen; infizierte Mails wandern in den Spam-Ordner, infizierte Downloads werden gesperrt
  • Spamfilter mit rspamd — bewertet neue Mails, verschiebt Spam und lernt mit deinem „Spam"-Knopf

📅 Organizer

  • Kalender — lokale Termine, Monats-/Agenda-Ansicht, Geburtstage aus Kontakten
  • Kontakte — umfangreiches Adressbuch (Telefon, Adresse, Firma, Website, Geburtstag); Import vom / Export aufs Handy, optional Sync in beide Richtungen
  • Notizen & Aufgaben
  • CalDAV-/CardDAV-Abgleich mit externen Servern (Nextcloud, Synology 
) + abonnierbare ICS-/vCard-Feeds

🔔 Benachrichtigungen — du wĂ€hlst

  • FCM (Google-Push) — sofort, keine Dauer-Benachrichtigung (wie Gmail/Synology)
  • ntfy — selbstgehosteter Push, kein Google
  • Hintergrund-PrĂŒfung — ~1 Min, keine Einrichtung nötig
  • Pro Konto, pro Ordner — du wĂ€hlst genau, welche PostfĂ€cher benachrichtigen
  • Einstellbar in Web-OberflĂ€che und App — eine gemeinsame Einstellung

🔄 Live-Sync (SSE)

  • Lesen/Löschen/Verschieben am Handy → ein offener Web-Tab aktualisiert sich sofort (und umgekehrt)

🔐 Sicherheit & Plattform

  • Mehrbenutzer mit Admin-/Nutzer-Rollen; der Admin kann Konten vorkonfigurieren
  • 2FA / TOTP mit Backup-Codes; biometrische App-Sperre unter Android
  • Konto-Zugangsdaten und Mail-Cache Fernet-verschlĂŒsselt gespeichert
  • Deutsch/Englisch, helles/dunkles Theme + eigene Akzentfarben
  • Ein Container — FastAPI + SQLite, keine externe Datenbank

đŸš« Absender blockieren

Blockieren — Mail öffnen → oben rechts auf ⋯ → đŸš« Absender blockieren → bestĂ€tigen. Alle Mails dieses Absenders (vorhandene + kĂŒnftige, Posteingang + Spam) wandern in den Papierkorb und werden als gelesen markiert. Der Papierkorb leert sich nach 7 Tagen automatisch (RĂŒckgĂ€ngig-Fenster).

Aufheben — oben rechts aufs đŸ‘€ BenutzermenĂŒ → 🔀 Regeln → der Kasten đŸš« Blockierte Absender ganz oben zeigt alle Blockierungen; mit ↩ Aufheben kommen kĂŒnftige Mails wieder an. Eine einzelne Mail rettest du, indem du sie im Ordner Papierkorb öffnest und zurĂŒck in den Posteingang verschiebst (innerhalb der 7 Tage).

Kommen trotzdem Mails vom gesperrten Absender? Spammer wechseln stĂ€ndig ihre Adresse. Unter ⚙ Einstellungen → Sicherheit → Spam- & Virenschutz kannst du:

  • jede Sperre fĂŒr alle deine Konten gelten lassen,
  • und unter 🔀 Regeln nach Antwort-Adresse oder Newsletter-Kennung (List-Id) sperren — Merkmale, die Spammer seltener wechseln als den Absender.

Jede Regel zeigt, wie oft sie zugeschlagen hat — so siehst du, ob eine Sperre noch wirkt. Bei echten Newslettern ist der Abmelden-Knopf (gleiche Einstellungsseite) der sauberere Weg; bei Unbekannten ist Sperren sicherer, denn Abmelden bestĂ€tigt, dass deine Adresse existiert.

💡 Tipp: die Zeiten fĂŒrs automatische Leeren (Spam & Papierkorb: aus / sofort / nach 7 / 30 Tagen) findest du unter đŸ‘€ → ✎ Konto bearbeiten.

🧭 Architektur

        ┌──────────────┐          ┌───────────────┐
        │  Web-UI      │          │  Android-App  │
        │ React + Vite │          │ Kotlin/Compose│
        └──────┬───────┘          └──────┬────────┘
               └───── JSON-API + SSE ─────┘
                   /api/v1 · JWT (Bearer)
                          │
                 ┌────────▌─────────┐
                 │  FastAPI-Kern    │  SQLite (Cache + Konfiguration)
                 │ Hintergrund-Sync │  Fernet-verschlĂŒsselte Geheimnisse
                 │  + Event-Bus     │  In-Memory-Pub/Sub → Live-Sync
                 └────────┬─────────┘
        ┌──────────┬──────┮───────┬──────────────┬───────────┐
       IMAP       SMTP     CalDAV / CardDAV      FCM         ntfy
  (Mail lesen) (Mail senden) (Kal. & Kontakte) (Google-Push) (selbstgehostet)

🔔 Benachrichtigungen

Methode unter Einstellungen → E-Mail-Benachrichtigungen wĂ€hlen (Web BenutzermenĂŒ → 🔔 / App Einstellungen). Der Server bedient alle drei parallel — z. B. FCM aufs Haupt-Handy, ntfy auf ein Handy ohne Google.

Methode Latenz Dauer-Benachrichtigung Voraussetzung
FCM (Google-Push) sofort keine Firebase-Projekt + Google-Play-Dienste
ntfy sofort keine (die ntfy-App hĂ€lt eine Verbindung fĂŒr alle Apps) ntfy-Container + ntfy-App
Hintergrund-PrĂŒfung ~1 Min ja (Android-Pflicht fĂŒr einen Vordergrund-Dienst) nichts

Pro Ordner: unter Ordner auswÀhlen legst du je Konto fest, welche Ordner einen Push auslösen.

Einrichtung — FCM (Google-Push)
  1. Kostenloses Firebase-Projekt anlegen: https://console.firebase.google.com.
  2. Android-App hinzufĂŒgen mit Paketname com.selfmailer.viewer → google-services.json herunterladen (kommt in den App-Build).
  3. Projekteinstellungen → Dienstkonten → Neuen privaten SchlĂŒssel generieren → JSON herunterladen.
  4. Diese JSON auf dem Server als 
/selfmailer/data/fcm-service-account.json ablegen und SELFMAILER_FCM_CREDENTIALS=/data/fcm-service-account.json setzen → Container neu starten.
  5. In der App: Benachrichtigungen an → Google-Push (FCM) → Ordner wĂ€hlen → mit Test-Push senden prĂŒfen.

Der Push-Inhalt ist bewusst minimal („2 neue E-Mails“); die App holt die Details direkt von deinem Server — Google sieht keine Mail-Inhalte.

Einrichtung — ntfy (selbstgehostet)
  1. ntfy-Container starten (z. B. binwiederhier/ntfy serve, Host-Port 8095 → Container 80, NTFY_BASE_URL=http://<host>:8095).
  2. ntfy-App installieren, Server eintragen, das in SelfMailer angezeigte Thema abonnieren.
  3. In SelfMailer: Benachrichtigungen an → ntfy → URL eintragen → Speichern & aktivieren → Ordner wĂ€hlen.

🔄 Live-Sync

Jeder offene Client (Web-Tab, App im Vordergrund) hĂ€lt eine schlanke SSE-Verbindung (/api/v1/events/stream). FĂŒhrt jemand eine Aktion aus — oder kommt neue Mail an — schickt der Server ein kleines Event, und die anderen Clients aktualisieren den betroffenen Ordner. Kein Dauer-Polling, kein Google, quasi sofort.

🚀 Schnellstart (Docker)

cp .env.example .env
# Secret erzeugen und in .env eintragen (SELFMAILER_SECRET):
python -c "import secrets; print(secrets.token_hex(32))"

docker compose up -d

http://<host>:8090 öffnen → die Ersteinrichtung legt das Admin-Konto an → danach deine Mailkonten hinzufĂŒgen.

FĂŒr Zugriff von außerhalb des LANs einen Reverse Proxy davorschalten: HTTPS-Anleitung.

📩 Unraid

Vorlage von https://raw.githubusercontent.com/s3lfcod3r/selfmailer/main/unraid/selfmailer.xml hinzufĂŒgen oder unter Docker → Add Container → Template importieren. Master Secret setzen, den Rest auf Standard lassen.

⚠ Master Secret: SELFMAILER_SECRET nach dem ersten Start nie Ă€ndern — damit werden die gespeicherten Postfach-Passwörter verschlĂŒsselt. Eine Änderung macht sie unlesbar.

⚠ Rechte: der Container lĂ€uft ohne Root (uid 99 / gid 100 = nobody:users). Gehört eine Datendatei root, einmalig korrigieren: chown -R 99:100 /mnt/user/appdata/selfmailer (oder Tools → Docker Safe New Permissions).

⚙ Konfiguration

Umgebungsvariablen, alle mit dem PrÀfix SELFMAILER_.

Variable Pflicht Standard Zweck
SELFMAILER_SECRET ✅ – JWT-Signatur und VerschlĂŒsselung der gespeicherten Postfach-Passwörter (≄ 32 Zeichen). Nach der Einrichtung nicht Ă€ndern.
SELFMAILER_DB_PATH – /data/selfmailer.db SQLite-Pfad
SELFMAILER_FCM_CREDENTIALS – /data/fcm-service-account.json Firebase-Service-Account-JSON fĂŒr Google-Push. Leer/fehlend = FCM aus.
SELFMAILER_TRANSLATE_URL – – URL einer selbstgehosteten LibreTranslate-Instanz fĂŒr die Übersetzung in der App (z. B. http://192.168.1.10:5000). Leer = aus (Button ausgeblendet).
SELFMAILER_TRANSLATE_API_KEY – – Optionaler API-Key, nur falls deine LibreTranslate-Instanz einen verlangt
SELFMAILER_ADMIN_TOKEN – – Wenn gesetzt, verlangt die Ersteinrichtung diesen Token
SELFMAILER_BASE_URL – – Öffentliche Basis-URL (z. B. fĂŒr Feed-Links)
SELFMAILER_SYNC_INTERVAL – 120 Sekunden zwischen Mail-Syncs im Hintergrund (kleiner = schnellerer Push & Sync)
SELFMAILER_IDLE – 1 IMAP IDLE auf dem Posteingang: der Server meldet neue Mails sofort (eine zusĂ€tzliche Verbindung je Konto); 0 = aus
SELFMAILER_PREFETCH_NEW – 20 Volltext von bis zu N neuen Posteingangs-Mails je Lauf vorladen (je ≀ 1 MB, ohne Gelesen-Markierung); 0 = aus
SELFMAILER_CLAMAV – – ClamAV (clamd) host:port fĂŒr den Virenscan der AnhĂ€nge, z. B. 192.168.1.10:3310. Nur im Heimnetz – clamd hat kein Passwort. Einschalten unter Einstellungen → Sicherheit.
SELFMAILER_RSPAMD_URL – – rspamd-Scanner (Port 11333) fĂŒr den Spamfilter. Einschalten unter Einstellungen → Sicherheit.
SELFMAILER_RSPAMD_CONTROLLER_URL / SELFMAILER_RSPAMD_PASSWORD – – Optional: rspamd-Controller (Port 11334) + Passwort, damit der „Spam"-Knopf rspamd anlernt.
SELFMAILER_DAV_SYNC_INTERVAL – 120 Sekunden zwischen Kalender-/Kontakt-Syncs (DAV) — getrennt vom Mail-Sync, min. 30
SELFMAILER_SYNC_DISABLE – 0 Hintergrund-Sync abschalten
SELFMAILER_IMAP_TIMEOUT – 15 IMAP-Socket-Timeout (Sekunden)
SELFMAILER_DAV_BLOCK_PRIVATE – false Private/LAN-Ziele beim DAV-Abgleich blockieren (strenger SSRF-Modus)
SELFMAILER_JWT_ALGORITHM – HS256 HS256 / HS384 / HS512
SELFMAILER_BACKUP_ENABLED – true NĂ€chtliches automatisches DB-Backup (~03:00)
SELFMAILER_BACKUP_KEEP – 7 Anzahl behaltener Backups (0 = alle behalten)

đŸ’Ÿ Backups

Der Scheduler erstellt nachts ein konsistentes Backup der SQLite-Datenbank (sqlite3-Online-Backup-API, sicher auch wÀhrend SchreibvorgÀngen) in data/backups/:

data/backups/selfmailer-YYYYMMDD-HHMMSS.db

Es werden nur die letzten SELFMAILER_BACKUP_KEEP Backups behalten; der Ordner ist git-ignoriert.

Wiederherstellen — Container stoppen, die Live-DB durch ein Backup ersetzen, neu starten:

docker stop selfmailer
cp data/backups/selfmailer-20260101-030000.db data/selfmailer.db
# veraltete WAL/SHM-Dateien entfernen, damit die wiederhergestellte Datei gilt
rm -f data/selfmailer.db-wal data/selfmailer.db-shm
docker start selfmailer

đŸ“± Android-App

Ein echter nativer Client (android/SelfMailer.apk) — kein WebView-Wrapper. Aufgebaut wie Synology MailPlus:

  • SeitenmenĂŒ → Kontowechsel + Sonderordner + Unterordner, mit Ungelesen-Badges
  • Untere Navigation: Mail · Kalender · Notizen
  • HTML-Mail, AnhĂ€nge, Mehrfachauswahl mit Aktionsleiste, Suchfilter, Verfassen im Synology-Stil mit Absender-Auswahl
  • EntwĂŒrfe bleiben erhalten (lokal verschlĂŒsselt), auch nach App-Sperre oder Neustart
  • Kontakte: Import vom / Export aufs Handy, optional Sync in beide Richtungen
  • Biometrische App-Sperre (Gesicht / Fingerabdruck / GerĂ€te-PIN), farbige Markierung fĂŒr Ungelesenes
  • Benachrichtigungen: FCM / ntfy / Hintergrund — deine Wahl, pro Ordner
  • Nutzt dieselbe /api/v1 wie die Web-OberflĂ€che; Server-URL beim ersten Start eingeben

🔌 API-Übersicht

Basispfad /api/v1, Anmeldung per JWT Bearer. Interaktive Doku: http://<host>:8090/docs.

Bereich Endpunkte (Auszug)
Auth auth/status, auth/setup, auth/login, auth/login/totp, auth/me, auth/totp/*
Konten accounts (CRUD), accounts/{id}/test
Mail mail/{id}/folders[/counts], mail/{id}/messages, 
/{uid}, 
/flags, 
/move, 
/send, 
/transfer, 
/batch-*
Organizer calendar/events, contacts, notes, tasks
DAV / Feeds dav/accounts, feeds/token, calendar/export.ics, contacts/export.vcf
Push push (ntfy), push/folders (Ordner je Konto), push/device (FCM-Tokens), push/test
Live events/stream (Server-Sent Events)
Dashboard dashboard/summary (gebĂŒndelte Ungelesen-ZĂ€hler)

đŸ›Ąïž Sicherheit

  • Login-Passwörter mit Argon2 gehasht; Postfach- & DAV-Passwörter Fernet-verschlĂŒsselt gespeichert (SchlĂŒssel aus SELFMAILER_SECRET)
  • Daten im Ruhezustand: Mail-Cache (Betreff, Absender, Vorschau, Volltext) und geplante Mails verschlĂŒsselt; Datenbank und Backups nur fĂŒr den Container-Nutzer lesbar
  • IMAP-Zertifikate werden geprĂŒft, fĂŒr selbstsignierte LAN-Server optional pro Konto per Fingerprint festgelegt
  • Widerrufbare Sitzungen: Logout, Passwort-/2FA-Änderungen und Admin-Sperren widerrufen Tokens, SSE-Zugriff und Push-Registrierungen; CSRF-Schutz fĂŒr Cookie-Sitzungen
  • JWT fest auf HS256/384/512; der 2FA-Zwischentoken gewĂ€hrt keinen Zugriff
  • Rate-Limiting bei Login/2FA/Einrichtung; defensive Response-Header; API-Antworten mit Cache-Control: no-store; Schutz gegen Nutzer-AusspĂ€hung (Dummy-Hash)
  • SSRF-Schutz mit IP-Pinning bei DAV-Abgleich, ntfy, Übersetzung und Abmelden (Loopback/Link-Local/Cloud-Metadaten immer blockiert; Abmelden nur an öffentliche Adressen); DAV-Zugangsdaten bleiben am Origin
  • Container: ohne Root (uid 99), App-Code schreibgeschĂŒtzt, keine Linux-Sonderrechte (no-new-privileges, cap-drop=ALL), Basis-Images per Digest festgelegt, Trivy-Image-Scan und AbhĂ€ngigkeits-Audits in der CI
  • Virenscan meldet nie „sauber" ohne echten Scan — Fehler und zu große Dateien gelten als „nicht geprĂŒft"
  • Android: allowBackup=false, Token im Android-Keystore, gehĂ€rtete Mail-HTML-WebView (kein JavaScript, keine Formulare), FileProvider mit Path-Traversal-PrĂŒfung
  • Bewusste LAN-Kompromisse (dokumentiert): unverschlĂŒsseltes HTTP erlaubt (HTTPS optional im Container oder ĂŒber einen Reverse-Proxy), SELFMAILER_SECRET ist zugleich der VerschlĂŒsselungs-Seed

Verlauf der Sicherheitsupdates → CHANGELOG.md (EintrĂ€ge mit 🔒).

đŸ§± Tech-Stack

Teil Technik
Backend FastAPI · SQLModel · SQLite (WAL) · httpx · aiosmtplib · imap-tools · PyJWT
Web React · Vite · TypeScript · EventSource (SSE)
App Kotlin · Jetpack Compose · OkHttp · WorkManager · Firebase Messaging · BiometricPrompt
Push FCM (Google) · ntfy (selbstgehostet)
Deploy Docker (Multi-Stage, ohne Root) · GHCR · Unraid

đŸ› ïž Entwicklung

# Backend
cd backend && pip install -r requirements.txt
export SELFMAILER_SECRET=$(python -c "import secrets; print(secrets.token_hex(32))")
uvicorn app.main:app --reload --port 8090

# Web (zweites Terminal)
cd frontend && npm install && npm run dev   # http://localhost:5173, leitet /api → :8090 weiter

Tests:

cd backend && pip install -r requirements.txt -r requirements-dev.txt && pytest --cov=app

đŸ—ș Roadmap

  • Mail (IMAP/SMTP) · gestapelte Konten · Ordnerbaum + Ungelesen-ZĂ€hler
  • Kalender · Kontakte · Notizen · Aufgaben · CalDAV/CardDAV + ICS/vCard-Feeds
  • 2FA / TOTP · Filterregeln · kontoĂŒbergreifend ĂŒbertragen · Mehrfachauswahl · Suchfilter
  • Native Android-App (Synology-Stil, biometrische Sperre)
  • Benachrichtigungen: FCM + ntfy + Hintergrund, pro Konto & Ordner
  • Live-Sync (SSE) zwischen Web-Clients und der Android-App (Vordergrund)
  • Kalender-Monatsansicht in der App
  • Postfach aufrĂ€umen · gespeicherte Ansichten · Handykontakte importieren/exportieren/abgleichen
  • HTTPS-Reverse-Proxy-Anleitung
  • OAuth fĂŒr Gmail / Outlook
  • Tests Richtung 80 % Abdeckung

⬆ nach oben


☕ Support / UnterstĂŒtzen

🇬🇧 This project is free for private use and built in my spare time. If it saves you time, a coffee keeps the next update coming. No pressure, a ⭐ on the repo helps too!

đŸ‡©đŸ‡Ș Dieses Projekt ist fĂŒr private Nutzung kostenlos und entsteht in meiner Freizeit. Wenn es dir Zeit spart, hĂ€lt ein Kaffee das nĂ€chste Update am Laufen. Kein Muss, ein ⭐ auf dem Repo hilft auch!

Support me on Ko-fi


⚖ Lizenz / License

đŸ‡©đŸ‡Ș Alle Rechte vorbehalten. © 2026 s3lfcod3r / SelfCoder

Du darfst die Software privat und nicht-kommerziell installieren und benutzen (z. B. im Homelab, ĂŒber Docker oder Unraid).

Ohne schriftliche Erlaubnis verboten: kommerzielle Nutzung · Kopieren · VerÀndern · Weitergeben · Einbauen in eigene Projekte (auch Closed Source) · Verkaufen.

Details: LICENSE

🇬🇧 All rights reserved. © 2026 s3lfcod3r / SelfCoder

You may install and use the software privately and non-commercially (e.g. in your home lab, via Docker or Unraid).

Not permitted without written permission: commercial use · copying · modifying · redistributing · incorporating into other projects (including closed source) · selling.

Details: LICENSE


SelfMailer · part of the Self family · Teil der Self-Familie
made with 📬 for self-hosting

Requirements

Existing e-mail accounts (IMAP/POP3 + SMTP). Set a long random Master Secret before the first start and never change it afterwards.

Related apps

Details

Repository
ghcr.io/s3lfcod3r/selfmailer:latest
Last Updated2026-11-09
First Seen2026-11-09

Runtime arguments

Web UI
http://[IP]:[PORT:8090]/
Network
bridge
Shell
bash
Privileged
false
Extra Params
--restart=unless-stopped --memory=1g --security-opt=no-new-privileges --cap-drop=ALL

Template configuration

Daten-PfadPathrw

Path where the SQLite database is stored.

Target
/data
Default
/mnt/user/appdata/selfmailer/data
Value
/mnt/user/appdata/selfmailer/data
WebUI PortPorttcp

Port for the SelfMailer web interface and API.

Target
8090
Default
8090
Value
8090
HTTPS Port (optional)Porttcp

Optional. Only used when TLS cert and key are set below. HTTP on 8090 keeps working.

Target
8443
TLS certificate (optional)Variable

Optional built-in HTTPS: path INSIDE the container to the PEM certificate (incl. chain), e.g. /data/certs/fullchain.pem. Set together with the key. Leave empty for plain HTTP or when using a reverse proxy.

Target
SELFMAILER_TLS_CERT
TLS key (optional)Variable

Optional built-in HTTPS: path INSIDE the container to the PEM private key, e.g. /data/certs/privkey.pem.

Target
SELFMAILER_TLS_KEY
Cookie secureVariable

auto (default) = secure cookie only over HTTPS, so HTTP in the LAN and HTTPS work side by side. true = always (HTTPS only). false = never.

Target
SELFMAILER_COOKIE_SECURE
Default
auto
Value
auto
Trusted proxies (optional)Variable

Optional. IP(s) of your reverse proxy (e.g. Nginx Proxy Manager), comma-separated. Needed behind a proxy for the real client IP (login rate limit) and HTTPS detection. Leave empty without a proxy.

Target
SELFMAILER_TRUSTED_PROXIES
HSTS max-age (optional)Variable

Optional. Seconds, 0 = off. Only sent on HTTPS responses.

Target
SELFMAILER_HSTS_MAX_AGE
Default
0
Value
0
Master SecretVariable

REQUIRED. Long random secret. Used for JWT signing AND at-rest encryption of stored mailbox passwords. Changing it makes saved account passwords unreadable.

Target
SELFMAILER_SECRET
Admin Token (optional)Variable

Optional. If set, the first-run admin setup requires this token.

Target
SELFMAILER_ADMIN_TOKEN
Base URL (optional)Variable

Optional public base URL, e.g. http://192.168.1.69:8090

Target
SELFMAILER_BASE_URL
Backup folder (optional)Variable

Optional. Folder INSIDE the container for the nightly DB backups. Empty = /data/backups. Recommended: add an extra Path mapping (e.g. container path /backups -> a folder on another disk) and enter /backups here. Do not share the data or backup folder via SMB.

Target
SELFMAILER_BACKUP_DIR
DB PathVariable

Path to SQLite database. Usually no need to change.

Target
SELFMAILER_DB_PATH
Default
/data/selfmailer.db
Value
/data/selfmailer.db
FCM credentials (optional)Variable

Optional. Path to a Firebase service-account JSON for Google push (FCM). Drop the file into the data folder. Leave/remove to disable FCM.

Target
SELFMAILER_FCM_CREDENTIALS
Default
/data/fcm-service-account.json
Value
/data/fcm-service-account.json
Translate URL (optional)Variable

Optional. URL of a self-hosted LibreTranslate instance for in-app e-mail translation, e.g. http://192.168.1.10:5000 (use the Unraid host IP + the LibreTranslate port, default 5000). Empty = translation feature disabled (the translate button stays hidden).

Target
SELFMAILER_TRANSLATE_URL
ClamAV (optional)Variable

Optional. ClamAV (clamd) host:port for scanning attachments, e.g. 192.168.1.10:3310 (ClamAV container on Unraid). LAN only - clamd has no password. Each user switches the virus scan on under Settings - Security.

Target
SELFMAILER_CLAMAV
rspamd URL (optional)Variable

Optional. rspamd scanner for the spam filter, e.g. http://192.168.1.10:11333. Each user switches the spam filter on under Settings - Security.

Target
SELFMAILER_RSPAMD_URL
rspamd controller URL (optional)Variable

Optional. rspamd controller (port 11334) so the Spam button teaches rspamd, e.g. http://192.168.1.10:11334.

Target
SELFMAILER_RSPAMD_CONTROLLER_URL
rspamd password (optional)Variable

Optional. Password of the rspamd controller (for learning).

Target
SELFMAILER_RSPAMD_PASSWORD
Translate API Key (optional)Variable

Optional. Only needed if your LibreTranslate instance requires an API key.

Target
SELFMAILER_TRANSLATE_API_KEY