Aktueller Stand

This commit is contained in:
2026-01-22 22:22:48 +01:00
parent 33e2bc61e2
commit fa5f3808bb
169 changed files with 58567 additions and 25460 deletions

161
README.md
View File

@@ -18,7 +18,7 @@ docker compose up --build
- Web UI: `http://localhost:${WEB_PORT}` (see root `.env`)
- API: `http://localhost:${API_PORT}`
- API Docs: `http://localhost:${API_PORT}/docs`
- API Docs: `http://localhost:${API_PORT}/docs` (only if `ENABLE_SWAGGER=true`)
## API (initial)
- `POST /auth/register` `{ tenantName, email, password }`
@@ -29,7 +29,8 @@ docker compose up --build
- `POST /mail/cleanup` (auth) `{ mailboxAccountId, dryRun, unsubscribeEnabled, routingEnabled }`
- `GET /jobs` (auth)
- `GET /jobs/:id/events` (auth)
- `GET /jobs/:id/stream?token=...` (auth via query token, SSE)
- `GET /jobs/:id/stream-token` (auth) -> short-lived SSE token
- `GET /jobs/:id/stream?token=...` (SSE using short-lived token)
- `GET /rules` (auth)
- `POST /rules` (auth)
- `PUT /rules/:id` (auth)
@@ -54,7 +55,8 @@ docker compose up --build
- `GET /admin/exports/:id/download` (admin)
- `POST /admin/exports/purge` (admin)
- `DELETE /admin/exports/:id` (admin)
- `GET /jobs/exports/:id/stream` (auth, SSE)
- `GET /jobs/exports/:id/stream-token` (admin) -> short-lived SSE token
- `GET /jobs/exports/:id/stream?token=...` (SSE using short-lived token)
Export queue:
- ZIP exports are queued via Redis/BullMQ and processed by the worker container.
@@ -74,6 +76,30 @@ UI:
- Weblink unsubscribe uses HTTP first, mailto fallback (SMTP required).
- Worker scans headers and applies routing rules (MOVE/DELETE) when not in dry run.
## Cleanup job behavior (what the button does)
When you click **“Bereinigung starten / Start cleanup”** a cleanup job is created and queued. The worker connects to the selected mailbox and:
1. Opens the INBOX (or first mailbox matching “inbox”).
2. Fetches recent message headers (subject/from/headers).
3. Detects newsletter candidates (ListUnsubscribe, ListId, heuristics).
4. Applies your routing rules (MOVE/ARCHIVE/LABEL/DELETE) if enabled.
5. Attempts to unsubscribe using `ListUnsubscribe` (HTTP oneclick or mailto).
6. Logs all actions and progress as job events (visible in the UI).
### The three checkboxes explained
**Dry run (keine Änderungen)**
Runs the full scan and logs what *would* happen, but **does not move/delete/unsubscribe** any mail. Useful for testing rules safely.
**Unsubscribe aktiv**
Enables `ListUnsubscribe` handling.
- HTTP links are called (oneclick POST when supported).
- Mailto links are sent via SMTP (requires SMTP host + app password).
**Routing aktiv**
Applies your configured rules (conditions → actions).
- MOVE/ARCHIVE/LABEL/DELETE will be executed when not in dry run.
- If disabled, no rule actions are executed (only detection + optional unsubscribe).
## Seed data
```bash
cd backend
@@ -84,7 +110,56 @@ SEED_TENANT=Default Tenant \\
SEED_TENANT_ID=seed-tenant \\
npm run prisma:seed
```
- DSGVO: data storage is designed for tenant isolation; encryption at rest will be added.
- DSGVO: tenant isolation supported; sensitive secrets are encrypted at rest when `ENCRYPTION_KEY` is set.
## Admin password reset (CLI)
Reset an admin password via CLI:
```
docker compose exec api npm run admin:reset -- admin@simplemailcleaner.local NEW_PASSWORD
```
Generate a temporary password (forces change on next login):
```
docker compose exec api npm run admin:reset -- admin@simplemailcleaner.local
```
## Security hardening (public hosting)
The app includes a security hardening pass for public deployments. Highlights:
- **No public DB/Redis ports** by default (only API/Web are bound, DB/Redis are internal to Docker).
- **CORS locked down** via `CORS_ORIGINS`.
- **Rate limiting** globally and stricter on auth endpoints.
- **Shortlived SSE tokens** instead of using the user JWT in URLs.
- **OAuth state signed** to prevent token injection.
- **SSRF protections** for ListUnsubscribe HTTP and custom mail hosts.
- **Secrets encrypted at rest** (OAuth tokens, app passwords, Google client secret).
- **Swagger disabled** by default in production.
- **Production env validation** rejects default secrets and missing encryption key.
### Findings and fixes (audit log)
- **Open DB/Redis ports** → removed public port bindings in `docker-compose.yml`.
- **Default secrets in production** → config validation blocks default JWT/seed secrets in `NODE_ENV=production`.
- **Tokens/app passwords stored in plain text** → encrypted at rest with `ENCRYPTION_KEY`.
- **SSRF via unsubscribe URLs / custom hosts** → private network block + scheme validation + timeouts.
- **OAuth state not verifiable** → state is now a signed, expiring JWT.
- **JWT in SSE URL** → replaced with shortlived stream tokens.
- **CORS allowall** → restricted by `CORS_ORIGINS`.
- **Swagger exposed** → disabled by default in production.
- **No rate limiting** → global and authspecific rate limits added.
### Required production settings
Set these in `.env` before going public:
- `NODE_ENV=production`
- `JWT_SECRET=<strong secret>`
- `ENCRYPTION_KEY=<min 32 chars>`
- `CORS_ORIGINS=https://your-domain.tld`
- `TRUST_PROXY=true` (when behind nginx)
- `ENABLE_SWAGGER=false`
- `SEED_ENABLED=false` (after initial setup)
### Optional hardening
- `ALLOW_CUSTOM_MAIL_HOSTS=false` (default) to force provider defaults
- `BLOCK_PRIVATE_NETWORKS=true` (default) to block private IPs in unsubscribe URLs
## Environment
All config lives in the repo root `.env` (see `.env.example`).
@@ -97,7 +172,85 @@ Proxy settings (Nginx Proxy Manager):
- `TRUST_PROXY=true`
- `VITE_API_URL=https://your-domain.tld`
- `GOOGLE_REDIRECT_URI=https://your-domain.tld/oauth/gmail/callback`
- `CORS_ORIGINS=https://your-domain.tld`
Local ports (override via `.env` in repo root):
- `BIND_IP` (default `127.0.0.1`)
- `API_PORT` (default `8000`, now set to `8201` in `.env`)
- `WEB_PORT` (default `3000`, now set to `3201` in `.env`)
## Reverse proxy notes (Nginx)
- Terminate TLS at nginx.
- Only expose nginx (80/443) publicly.
- Keep API/Web bound to `127.0.0.1` (or internal Docker network).
- Set `TRUST_PROXY=true` so the app honors `X-Forwarded-*` headers.
## Nginx Proxy Manager (NPM) setup
Minimal steps to run behind Nginx Proxy Manager with limited nginx customization.
### 1) Bind services locally
In `.env`:
```
BIND_IP=127.0.0.1
API_PORT=8201
WEB_PORT=3201
```
### 2) Put NPM and Mailcleaner in the same Docker network
If NPM runs in Docker, attach both stacks to a shared network (example: `proxy`).
Create network once:
```
docker network create proxy
```
Add to `docker-compose.yml`:
```
networks:
proxy:
external: true
```
Then attach services:
```
services:
api:
networks: [proxy]
web:
networks: [proxy]
```
### 3) Create proxy hosts in NPM
Create **two** Proxy Hosts:
**Frontend**
- Domain: `app.your-domain.tld`
- Scheme: `http`
- Forward Hostname/IP: `mailcleaner-web`
- Forward Port: `3000`
- Websockets: ON
- Block Common Exploits: ON
- SSL: Lets Encrypt, Force SSL
**API**
- Domain: `api.your-domain.tld`
- Scheme: `http`
- Forward Hostname/IP: `mailcleaner-api`
- Forward Port: `8201`
- Websockets: ON
- Block Common Exploits: ON
- SSL: Lets Encrypt, Force SSL
### 4) Environment for public hosting
Set in `.env`:
```
NODE_ENV=production
TRUST_PROXY=true
CORS_ORIGINS=https://app.your-domain.tld
VITE_API_URL=https://api.your-domain.tld
GOOGLE_REDIRECT_URI=https://api.your-domain.tld/oauth/gmail/callback
ENABLE_SWAGGER=false
JWT_SECRET=<strong secret>
ENCRYPTION_KEY=<min 32 chars>
SEED_ENABLED=false
```