# LeoCRM — Extracted Architecture Details **Zweck:** Implementierungs-Details, die aus der bereinigten `requirements.md` entfernt wurden. Für den Solution Architect als Referenz. **Quelle:** Original `requirements.md` (git HEAD, 1956 Zeilen) vs. bereinigte Version (2105 Zeilen) **Datum:** 2026-06-28 --- ## 1. Tech-Stack (vollständige Spezifikation) ### Original Tech-Stack-Tabelle (entfernt) | Komponente | Entscheidung | Bemerkung | |------------|-------------|----------| | Backend | FastAPI + SQLAlchemy 2.0 | Python 3.12 | | Datenbank | PostgreSQL (empfohlen) | Siehe DB-Empfehlung unten | | Frontend | React SPA | Client-side rendering, i18n (bestätigt durch Prototyp) | | Deployment | Coolify (Docker) | Bestätigt | | Linting | ruff + black | Python-Standard | | Testing | pytest + Vitest/Jest | Backend + Frontend | ### DB-Empfehlung: PostgreSQL statt SQLite Ursprünglich war SQLite spezifiziert. Bei 200.000 Kontakten + mehreren Usern + Concurrent Writes ist SQLite limitiert: - **SQLite:** File-Level Locking, nur 1 Writer gleichzeitig, ~15 Updates/Sekunde bei Concurrent Writes (StackOverflow Benchmark) - **PostgreSQL:** True Concurrent Writers, ~1.500 Updates/Sekunde, MVCC-Architektur (tableone.dev Benchmark) - **Fazit:** Bei 200k Datensätzen + Multi-User + gleichzeitige Schreibzugriffe → PostgreSQL ### v0.1 Historischer Tech-Stack (archiviert) - ~~Backend: FastAPI (Python 3.11+)~~ → Python 3.12 - ~~DB: SQLite (Datei `leocrm.db`)~~ → PostgreSQL 16 - ~~Templates: Jinja2 (HTML)~~ → React 18 SPA - ~~Server: Uvicorn / Gunicorn~~ → Uvicorn (async) - ~~Deployment: 1 Container Docker Compose~~ → Multi-Container (Backend + Frontend + PostgreSQL + OnlyOffice + Worker) --- ## 2. Domain Knowledge (entfernte Referenzen) **Domain:** Customer Relationship Management (CRM) Standard-Entitäten in CRMs (Referenz: Zoho CRM, HubSpot, Salesforce): - **Accounts (Firmen):** Name, Adresse, Industrie, Employees, Revenue, Owner, Phone, Website, Billing/Shipping Address - **Contacts (Kontaktpersonen):** Name, Email, Phone, Mobile, Title, Department, Mailing Address, Reports To, Description - **Beziehungen:** N:M (ein Kontakt kann mehreren Firmen zugeordnet sein) Quellen: - Zoho CRM Standard Fields Accounts: https://help.zoho.com/portal/en/kb/crm/sales-force-automation/accounts/articles/standard-fields-accounts - Zoho CRM Standard Fields Contacts: https://help.zoho.com/portal/en/kb/crm/sales-force-automation/contacts/articles/standard-fields-contacts - Encore Business Solutions — 30 CRM Custom Account Fields - Bitrix24 Standard fields in CRM --- ## 3. HTTP-Endpunkt-Spezifikationen ### F-AUTH-01: Login - **Endpoint:** POST `/api/auth/login` - **Akzeptiert:** E-Mail + Passwort - **Response:** Session-Cookie (HttpOnly+Secure+SameSite=Strict) oder 401 ### F-AUTH-02: Logout - **Clientseitig:** Token wird entfernt - **Server:** Token-Blacklist optional für v1 ### F-AUTH-03: User-Verwaltung - **Endpoint:** POST `/api/users` - **Auth:** Admin-Token erforderlich - **Response:** 201 (erstellt) oder 403 (ohne Admin-Token) ### F-AUTH-04: RBAC - **Jeder API-Endpoint prüft Rolle:** falsche Rolle → 403; korrekte Rolle → 200/201 - **Beispiele:** POST `/api/users` → 403 (editor); POST `/api/companies` → 403 (viewer), GET `/api/companies` → 200 (viewer) ### F-AUTH-05: Passwort-Reset - **Request:** POST `/api/auth/password-reset/request` → sendet E-Mail - **Confirm:** POST `/api/auth/password-reset/confirm` mit Token + neuem Passwort → aktualisiert Passwort ### F-COMP-01: Firma anlegen - **Endpoint:** POST `/api/companies` - **Response:** 201 Created (valide Daten); 422 Validation Error (ohne Name) ### F-COMP-02: Firma anzeigen - **Endpoint:** GET `/api/companies/{id}` - **Response:** 200 mit Firmendaten + Kontakte-Array; 404 (nicht-existent) ### F-COMP-03: Firma bearbeiten - **Endpoint:** PUT/PATCH `/api/companies/{id}` - **Response:** 200 mit aktualisierten Daten; 422 (Validierungsfehler) ### F-COMP-04: Firma löschen (Soft-Delete) - **Endpoint:** DELETE `/api/companies/{id}?cascade=true|false` - **Response:** 200; Firma wird als `deleted_at = NOW()` markiert - **Cascade:** bei cascade=true auch Kontakte soft-deleted ### F-COMP-05: Firmen-Liste mit Pagination - **Endpoint:** GET `/api/companies?page=2&page_size=25&sort_by=name&sort_order=desc` - **Response:** 200 mit `{items, total, page, page_size}` ### F-COMP-06: Firmen-Suche & Filter - **Endpoint:** GET `/api/companies?search=Tech&industry=IT&country=Germany` - **Response:** 200 mit gefilterten Ergebnissen ### F-COMP-07: Audit-Log - **Tabelle:** `audit_log` - **Mechanismus:** Middleware/Decorator loggt schreibende Aktionen - **Endpoint:** GET `/api/audit-log` (Admin, paginiert) ### F-COMP-08: DSGVO / Right to be Forgotten - **Endpoint:** DELETE `/api/contacts/{id}?gdpr=true` → harte Löschung - **Tabelle:** separate `deletion_log` Tabelle (unveränderlich) ### F-CONT-01: Kontaktperson anlegen - **Endpoint:** POST `/api/contacts` - **Response:** 201 (valide Daten); 422 (ohne last_name) - **N:M:** mit company_ids → N:M-Verknüpfungen erstellt ### F-CONT-02: Kontaktperson anzeigen - **Endpoint:** GET `/api/contacts/{id}` - **Response:** 200 mit Kontaktdaten + Firmen-Array; 404 ### F-CONT-03: Kontaktperson bearbeiten - **Endpoint:** PUT/PATCH `/api/contacts/{id}` - **Response:** 200; Änderung an company_ids → N:M-Tabelle aktualisiert ### F-CONT-04: Kontaktperson löschen (Soft-Delete) - **Endpoint:** DELETE `/api/contacts/{id}` - **Response:** 200; `deleted_at` gesetzt; N:M-Einträge gelöscht; Firmen unberührt ### F-CONT-05: Kontakt-Liste mit Pagination - **Endpoint:** GET `/api/contacts?page=1&page_size=25&sort_by=last_name&sort_order=asc` - **Response:** 200 mit paginierten Ergebnissen - **Performance:** Response-Zeit <500ms bei 200k Datensätzen (mit DB-Index) ### F-CONT-06: Kontakt-Suche & Filter - **Endpoint:** GET `/api/contacts?search=Müller&company_id=5` - **Response:** 200 mit gefilterten Ergebnissen ### F-CONT-07: N:M Firmen-Kontakt-Zuordnung - **Tabelle:** `company_contacts` (N:M-Verknüpfungstabelle) - **Zuordnung:** POST `/api/companies/{id}/contacts/{contact_id}` - **Entfernung:** DELETE `/api/companies/{id}/contacts/{contact_id}` ### F-DATA-01: CSV-Export - **Endpoint:** GET `/api/companies/export?format=csv&filters=...` - **Response:** 200 mit `Content-Type: text/csv`, Datei-Download ### F-DATA-02: Excel-Export - **Endpoint:** GET `/api/companies/export?format=xlsx&filters=...` - **Response:** 200 mit `Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet` ### F-DATA-03: Daten-Validierung - **Technologie:** Pydantic-Schemas für alle Entities - **Response:** ungültige Eingaben → 422 mit detailiertem Fehler-Objekt ### F-DATA-06: ARIA-Rollen auf DataTable - **ARIA-Rollen:** `role="table"`, `role="row"`, `role="columnheader"`, `role="cell"` - **Sortierung:** `aria-sort="ascending|descending|none"` - **Label:** beschreibendes `aria-label` - **Testing:** NVDA und axe DevTools ### F-UI-01: Responsive Design - **Breakpoints:** Mobile-First; Touch-Targets min 44px - **Testing:** Chrome DevTools (375px, 768px, 1920px) ### F-UI-02: Internationalisierung - **Library:** react-i18next - **Locale-Files:** JSON-Locale-Files für DE/EN - **Persistenz:** Sprachwahl persistiert in User-Settings ### F-UI-03: Error-Handling & Toast-Notifications - **Komponente:** Toast-Komponente (success/error/warning/info) - **Routen:** dedizierte 404/500-Routen im Frontend ### F-UI-04: Loading-States - **Komponenten:** Loading-State-Komponenten; Skeleton-Loader - **State:** API-Loading-State im Frontend-Store; Button-Disabled-State während Pending ### F-UI-05: Empty-States - **Komponente:** Empty-State-Komponenten mit Icon + Text + CTA ### F-UI-06: Confirmation-Dialogs - **Komponente:** Modal-Dialog-Komponente - **Regel:** alle DELETE-Operationen erfordern Bestätigung ### F-SEC-01: CSRF-Schutz - **Implementierung:** SameSite=Strict Cookies + Origin-Header-Validierung - **Kein Double-Submit-Token** - **Middleware:** Origin-Header-Validierung-Middleware - **Exempt:** nur GET/HEAD/OPTIONS ### F-SEC-02: XSS-Schutz & Input-Sanitization - **Frontend:** Output-Encoding - **Backend:** Pydantic-Validierung strippt gefährliche Eingaben - **Header:** CSP-Header gesetzt ### F-SEC-03: Session-Timeout - **Timeout:** 8h - **Interceptor:** 401-Interceptor im Frontend → Redirect zur Login-Seite - **Refresh:** Refresh-Token optional für v1 ### F-INFRA-01: Health-Check Endpoint - **Endpoint:** GET `/api/health` - **Response:** 200 `{status: "healthy", db: "connected"}` oder 503 `{status: "unhealthy", db: "disconnected"}` - **Auth:** kein Auth erforderlich - **Coolify:** nutzt Endpoint für Health-Check ### F-INFRA-02: Backup & Restore - **Backup:** Docker-Volume-Backup konfiguriert - **Doku:** Restore-Dokumentation in README ### F-INFRA-03: Logging - **Format:** Python `logging` mit JSON-Formatter - **Konfiguration:** Log-Level konfigurierbar via Env-Var - **Inhalt:** Method, Path, Status, Duration; Error-Logs mit Stacktrace ### F-INFRA-04: Monitoring & Alerting - **Monitoring:** Coolify-Built-in-Monitoring nutzt `/api/health` - **Optional:** externes Monitoring-Tool ### F-MIG-01: CSV-Import - **Endpoint:** POST `/api/import` mit CSV-Datei + Entity-Type - **Response:** Import-Job mit success/skipped/failed counts ### F-INT-01: E-Mail-Integration (Passwort-Reset) - **Config:** SMTP via Env-Vars (SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASS) - **Templates:** E-Mail-Templates für Reset ### F-INT-02: API-Keys / Token-Auth - **Auth:** Session-Auth-Middleware auf allen Endpoints außer `/api/health` und `/api/auth/*` - **Optional:** API-Key für externe Integrationen (post-MVP) ### F-TEST-01: Testing-Strategie - **Backend:** pytest + httpx für API-Tests; Coverage >80% - **Frontend:** Vitest für Component-Tests - **E2E:** Playwright für Critical Paths - **CI:** Coverage-Report in CI; bei Fehlschlag wird Build blockiert ### F-ENV-01: Environments & Secrets - **Env-Vars:** `LEOCRM_SECRET_KEY` (Session-Secret, min 32 Zeichen) - **Datei:** `.env.example` dokumentiert alle Vars - **Secrets:** keine Secrets im Git-Repo - **Coolify:** Env-Vars konfiguriert ### F-DOC-01: Dokumentation - **README:** `README.md` mit Setup-Anleitung - **API-Doku:** `/docs` (Swagger/OpenAPI) auto-gen via FastAPI - **Admin-Doku:** `docs/admin-guide.md` für Deploy/Backup/Restore ### F-PERF-01: Performance - **Response-Zeit:** <500ms für List-Endpunkte bei 200k Datensätzen - **DB-Indizes:** last_name, first_name, email, company.name, company.industry, company.country - **Pagination:** verhindert Full-Table-Scan - **Export:** >50k Datensätze als Background-Job oder Streaming-Response ### F-A11Y-01: Accessibility (WCAG 2.1 AA) - **CSS-Utility:** `.sr-only` CSS-Klasse (visual-hidden pattern) - **Kontrast:** >4.5:1 - **Testing:** axe DevTools ### F-A11Y-02: prefers-reduced-motion - **CSS:** `@media (prefers-reduced-motion: reduce)` Block in globalen Styles - **Properties:** alle `transition` und `animation` auf `none` oder `0.01ms` - **Testing:** Chrome DevTools (Rendering → Emulate CSS prefers-reduced-motion: reduce) ### F-A11Y-03: 44px Touch-Targets - **CSS-Regel:** `min-height: 44px; min-width: 44px;` für alle interaktiven Elemente - **Alternative:** `::after` Pseudo-Element mit 44px Touch-Bereich - **Testing:** Chrome DevTools (375px Breite, Touch-Emulation) ### F-SCHED-01: Background-Jobs - **Queue:** Background-Task-Queue (Celery, ARQ, oder FastAPI BackgroundTasks für v1) - **Status:** Job-Status-Endpoint - **Export-Limit:** >50k Datensätze als Background-Job, <50k als direkter Download --- ## 4. DB-Schema-Definitionen (Feld-Tabellen) ### F-COMP-01: Company-Felder | Feld | Typ | Pflicht | Bemerkung | |------|-----|---------|-----------| | name | String(100) | ✅ | Firmenname | | account_number | String(40) | ❌ | Interne Referenznummer | | industry | Picklist | ❌ | IT, Finance, Manufacturing, etc. | | account_type | Picklist | ❌ | Customer, Partner, Prospect, etc. | | ownership | Picklist | ❌ | Public, Private, Government, etc. | | employees | Integer | ❌ | Anzahl Mitarbeiter | | annual_revenue | Decimal | ❌ | Jahresumsatz | | phone | String(30) | ❌ | Haupttelefon | | fax | String(30) | ❌ | Fax | | email | Email | ❌ | Allgemeine E-Mail | | website | URL | ❌ | Website | | rating | Picklist | ❌ | Hot, Warm, Cold | | parent_account_id | FK→Company | ❌ | Muttergesellschaft | | billing_street | String(250) | ❌ | Rechnungsadresse Strasse | | billing_city | String(100) | ❌ | Rechnungsadresse Stadt | | billing_state | String(100) | ❌ | Rechnungsadresse Bundesland | | billing_postal_code | String(20) | ❌ | Rechnungsadresse PLZ | | billing_country | String(100) | ❌ | Rechnungsadresse Land | | shipping_street | String(250) | ❌ | Besuchsadresse Strasse | | shipping_city | String(100) | ❌ | Besuchsadresse Stadt | | shipping_state | String(100) | ❌ | Besuchsadresse Bundesland | | shipping_postal_code | String(20) | ❌ | Besuchsadresse PLZ | | shipping_country | String(100) | ❌ | Besuchsadresse Land | | description | Text(32000) | ❌ | Notizfeld | | sic_code | String(10) | ❌ | Standard Industrial Classification | | ticker_symbol | String(30) | ❌ | Börsenkürzel | | account_site | String(80) | ❌ | Standort-Name (z.B. Headquarters) | ### F-CONT-01: Contact-Felder | Feld | Typ | Pflicht | Bemerkung | |------|-----|---------|-----------| | first_name | String(50) | ❌ | Vorname | | last_name | String(50) | ✅ | Nachname | | salutation | Picklist | ❌ | Herr, Frau, Dr., etc. | | email | Email | ❌ | Haupt-E-Mail | | secondary_email | Email | ❌ | Zweit-E-Mail | | phone | String(30) | ❌ | Bürotelefon | | mobile | String(30) | ❌ | Mobiltelefon | | home_phone | String(30) | ❌ | Privattelefon | | fax | String(30) | ❌ | Fax | | title | String(100) | ❌ | Jobtitel (CEO, Manager, etc.) | | department | String(100) | ❌ | Abteilung | | reports_to | FK→Contact | ❌ | Vorgesetzter | | date_of_birth | Date | ❌ | Geburtsdatum | | assistant | String(50) | ❌ | Assistent-Name | | assistant_phone | String(30) | ❌ | Assistent-Telefon | | mailing_street | String(250) | ❌ | Postadresse Strasse | | mailing_city | String(100) | ❌ | Postadresse Stadt | | mailing_state | String(100) | ❌ | Postadresse Bundesland | | mailing_postal_code | String(20) | ❌ | Postadresse PLZ | | mailing_country | String(100) | ❌ | Postadresse Land | | other_street | String(250) | ❌ | Andere Adresse Strasse | | other_city | String(100) | ❌ | Andere Adresse Stadt | | other_state | String(100) | ❌ | Andere Adresse Bundesland | | other_postal_code | String(20) | ❌ | Andere Adresse PLZ | | other_country | String(100) | ❌ | Andere Adresse Land | | skype_id | String(50) | ❌ | Skype | | linkedin | URL | ❌ | LinkedIn-Profil | | twitter | String(50) | ❌ | Twitter-Handle | | description | Text(32000) | ❌ | Notizfeld | | company_ids | [FK→Company] | ❌ | N:M-Zuordnung (0..n Firmen) | ### Implizite DB-Tabellen (aus Akzeptanzkriterien extrahiert) - `company_contacts` — N:M-Verknüpfungstabelle (company_id ↔ contact_id) - `audit_log` — Tabelle für Audit-Log-Einträge (user, action, entity, entity_id, timestamp) - `deletion_log` — Unveränderliche Tabelle für DSGVO-Löschungen - `users` — User-Tabelle (email, name, role, password_hash) - `companies` — Firmen-Tabelle (siehe Feld-Tabelle oben, + `deleted_at` TIMESTAMP für Soft-Delete) - `contacts` — Kontakt-Tabelle (siehe Feld-Tabelle oben, + `deleted_at` TIMESTAMP für Soft-Delete) --- ## 5. Nicht-funktionale Anforderungen (vollständige Tabelle) | ID | Kategorie | Anforderung | Metrik | |----|-----------|-------------|--------| | NF-01 | Performance | API-Response <500ms bei 200k Datensätzen | 95th Percentile <500ms | | NF-02 | Skalierung | DB-Connection-Pooling (SQLAlchemy Pool size=10) | Keine Connection-Erschöpfung bei 10 concurrent Users | | NF-03 | Sicherheit | Alle Passwörter bcrypt-gehashed (cost=12) | Keine Plain-Text-Passwörter in DB | | NF-04 | Sicherheit | Session-Secret via Env-Var, min 32 Zeichen | App startet nicht ohne Secret | | NF-05 | Verfügbarkeit | Health-Check für Coolify Auto-Restart | Auto-Restart bei 503 | | NF-06 | Wartbarkeit | Code-Struktur: `api/`, `models/`, `schemas/`, `services/`, `tests/` | Klare Trennung | | NF-07 | Wartbarkeit | Linting: ruff + black, pre-commit-hook | CI blockt bei Lint-Fehlern | | NF-08 | Testbarkeit | pytest Coverage >80% Backend | Coverage-Report in CI | | NF-09 | i18n | DE + EN, Sprachwahl persistiert | Alle UI-Texte übersetzt | | NF-10 | Accessibility | WCAG 2.1 AA | axe-Check: 0 Violations | | NF-11 | Logging | Strukturierte JSON-Logs | `docker logs` zeigt JSON-Einträge | | NF-12 | Deployment | Docker-Container auf Coolify | `docker-compose.yml` + `Dockerfile` | --- ## 6. Annahmen (entfernte Details) 1. **Multi-Tenant (Multi-Company):** v1 ist Multi-Tenant (Multi-Company) — mehrere Organisationen, mehrere User. 2. **PostgreSQL statt SQLite:** Bei 200k Kontakten + Multi-User + Concurrent Writes wird PostgreSQL empfohlen. SQLite limitiert auf ~15 concurrent updates/sec vs PostgreSQL ~1500/sec. 3. **SPA-Frontend:** Client-side rendering mit React SPA (bestätigt durch genehmigten Prototyp leocrm-prototype-x7k2p9, v7). 4. **Max 10 concurrent Users:** v1 ist für kleine Teams, nicht für Enterprise. 5. **E-Mail-Versand:** SMTP via externem Provider (Mailtrap für Dev, Production-SMTP für Prod). 6. **Soft-Delete als Default:** Firmen und Kontakte werden soft-deleted (recoverable), außer DSGVO-Löschung (hard delete). 7. **Rollensystem v1:** 3 Rollen: admin, editor, viewer. 8. **Export-Limit:** Export von >50k Datensätzen als Background-Job, <50k als direkter Download. 9. **i18n-Default:** Deutsch ist Default-Sprache, Englisch ist zweites Locale. 10. **Python 3.12:** Aktuelle stabile Python-Version. --- ## 7. Technologie-Wahlen (Alle Module) ### Core-Stack | Technologie | Verwendung | |-------------|-----------| | FastAPI | Backend-Framework | | SQLAlchemy 2.0 | ORM | | PostgreSQL 16 | Datenbank | | React 18 SPA | Frontend | | Uvicorn (async) | ASGI-Server | | ruff + black | Linting | | pytest + httpx | Backend-Tests | | Vitest | Frontend-Tests | | Playwright | E2E-Tests | | Pydantic | Input-Validierung | | bcrypt (cost=12) | Passwort-Hashing | | Celery / ARQ / FastAPI BackgroundTasks | Background-Jobs | | react-i18next | i18n-Frontend | | Docker Compose | Multi-Container-Deployment | | Coolify | Deployment-Plattform | ### DMS-Modul | Technologie | Verwendung | |-------------|-----------| | PDF.js | PDF-Preview im Browser | | OnlyOffice Document Server | Online-Bearbeitung (separater Container) | ### Mail-Modul | Technologie | Verwendung | |-------------|-----------| | IMAP4rev1 (RFC 3501) | Mail-Empfang | | IMAP IDLE (RFC 2177) | Real-time Push für neue Mails | | SMTP Submission (RFC 6409) | Mail-Versand | | DOMPurify | HTML-Sanitization (XSS-Schutz) | | python-gnupg | PGP-Verschlüsselung (OpenPGP, RFC 4880) | | PostgreSQL Full-Text Search (tsvector) | Volltext-Suche über Mails | | AES-256 | Passwort-Verschlüsselung für IMAP/SMTP-Credentials | --- ## 8. Farbcodes (Hex-Werte) ### Kalender-Modul (F-CAL-06) | Eintrags-Typ | Subtyp | Farbe (Hex) | |-------------|-------|-------------| | appointment | normal | `#3B82F6` (blau) | | task | normal | `#F59E0B` (gelb) | | * | follow_up | `#F97316` (orange) | | * | private | `#9CA3AF` (grau) | ### Tag-System (F-TAG-02) - Tags können mit Farbe versehen werden, z.B. `#FF0000` für „VIP" --- ## 9. Protokoll-Details (Mail-Modul) ### IMAP - **Protokoll:** IMAP4rev1 (RFC 3501) - **IDLE:** RFC 2177 für Push, Fallback: Polling alle 5 Min - **Port:** 993 (SSL/TLS) - **Auth:** PLAIN/LOGIN - **MOVE:** RFC 6851 (Fallback: COPY + STORE \\Deleted + EXPUNGE) - **Flags:** `\\Seen`, `\\Flagged`, `\\Answered`, `\\Draft`, `$Spam` - **Kompatibilität:** Getestet mit Dovecot, Courier IMAP ### SMTP - **Protokoll:** SMTP Submission (RFC 6409) - **Port 587:** mit STARTTLS - **Port 465:** mit SSL - **Auth:** PLAIN/LOGIN mit Benutzername+Passwort ### PGP - **Standard:** OpenPGP (RFC 4880) - **Library:** python-gnupg - **Private Key:** verschlüsselt gespeichert (AES-256 + Passphrase) - **Passphrase:** nicht gespeichert, nur im Session-Cache - **Public Key:** pro Kontakt speicherbar - **S/MIME:** post-MVP ### Attachment-Storage - Anhänge werden lokal gespeichert (Dateisystem oder S3) - Nicht im IMAP-Server belassen (Caching) - Mail-Body wird in PostgreSQL gespeichert - Anhänge bei Bedarf vom IMAP-Server nachgeladen - Max: 25 MB pro Anhang, 50 MB pro Mail ### Passwort-Speicherung - IMAP/SMTP-Passwörter AES-256 verschlüsselt - Key via Env-Var `MAIL_ENCRYPTION_KEY` ### Ordner-Mapping - INBOX → Posteingang - Sent → Postausgang - Drafts → Entwürfe - Spam/Junk → Spam ### Threading - Basierend auf `References`- und `In-Reply-To`-Headern (RFC 5322) ### HTML-Rendering - Sanitization mit DOMPurify (XSS-Schutz) - Inline-Styles erlaubt, Scripts/IFrames entfernt - Plain-Text-Fallback --- ## 10. RFC-Referenzen (alle) | RFC | Titel | Verwendung | |-----|-------|-----------| | RFC 3501 | IMAP4rev1 | Mail-Empfang | | RFC 2177 | IMAP IDLE | Real-time Push | | RFC 6851 | IMAP MOVE | Mail verschieben | | RFC 6409 | SMTP Submission | Mail-Versand | | RFC 4880 | OpenPGP | PGP-Verschlüsselung | | RFC 5322 | Internet Message Format | Threading (References/In-Reply-To) | | RFC 5545 | iCalendar | Recurrence Rules (RRULE) für wiederkehrende Termine | | RFC 6047 | iMIP | (Non-Goal: nicht in v2-mail) | | RFC 8620 | JMAP | (Non-Goal: nicht unterstützt) | --- ## 11. DMS-Modul: HTTP-Endpunkte & Architektur-Details ### F-DMS-01: Ordner-Struktur - **Create:** POST `/api/dms/folders` → 201 - **Rename/Move:** PATCH `/api/dms/folders/{id}` → 200 - **Delete:** DELETE `/api/dms/folders/{id}` → 200 (Soft-Delete) - **Zirkuläre Verschiebung:** serverseitig verhindert → 422 ### F-DMS-02: Datei-Upload - **Endpoint:** POST `/api/dms/files/upload` (multipart) → 201 mit Datei-Metadaten - **Max-Size:** 413 bei Überschreitung (Default 100 MB) - **Status:** WebSocket oder Polling ### F-DMS-03: Datei-Operationen - **Rename:** PATCH `/api/dms/files/{id}` → 200 - **Move:** PATCH `/api/dms/files/{id}` mit `folder_id` → 200 - **Delete:** DELETE `/api/dms/files/{id}` → 200 (Soft-Delete) - **Restore:** POST `/api/dms/files/{id}/restore` → 200 ### F-DMS-04: PDF-Preview - **Endpoint:** GET `/api/dms/files/{id}/preview` → 200 mit `Content-Type: application/pdf` - **Frontend:** PDF.js - **Non-PDF:** 415 Unsupported Media Type ### F-DMS-05: OnlyOffice Integration - **Server:** OnlyOffice Document Server als separater Container (Coolify) - **Endpoint:** POST `/api/dms/files/{id}/edit-session` → 200 mit OnlyOffice-URL - **Formate:** DOCX, XLSX, PPTX ### F-DMS-06: Datei-Metadaten - **Endpoint:** GET `/api/dms/files/{id}` → 200 mit `{name, size, mime_type, uploaded_at, modified_at, uploaded_by}` - **Größe:** serverseitig in Bytes, clientseitig formatiert ### F-DMS-07: Datei-Suche - **Endpoint:** GET `/api/dms/search?q=vertrag&folder_id={id}` → 200 mit `{items: [{type, name, path, ...}]}` - **Global:** ohne `folder_id` → gesamte DMS ### F-LINK-01: Dateien mit Firmen verknüpfen - **Endpoint:** POST `/api/dms/files/{id}/link` mit `{entity_type: "company", entity_id: N}` → 201 - **Delete:** DELETE `/api/dms/files/{id}/link?entity_type=company&entity_id=N` → 204 - **Unique Constraint:** Duplikate serverseitig verhindert ### F-LINK-02: Dateien mit Kontakten verknüpfen - **Endpoint:** POST `/api/dms/files/{id}/link` mit `{entity_type: "contact", entity_id: N}` → 201 ### F-LINK-03: Verknüpfte Dateien in Firmen-Detail - **Response:** GET `/api/companies/{id}` enthält `linked_files: [{id, name, size, mime_type, modified_at}]` ### F-LINK-04: Verknüpfte Dateien in Kontakt-Detail - **Response:** GET `/api/contacts/{id}` enthält `linked_files: [{id, name, size, mime_type, modified_at}]` ### F-LINK-05: Reverse-Verknüpfung - **Response:** GET `/api/dms/files/{id}` enthält `linked_entities: [{entity_type, entity_id, entity_name}]` - **Delete:** DELETE `/api/dms/files/{id}/link?entity_type=company&entity_id=N` → 204 ### F-LINK-06: Mehrfach-Verknüpfung - **Bulk:** POST `/api/dms/files/bulk-link` mit `{file_ids: [...], entity_type: "company", entity_id: N}` → 201 - **Ordner:** POST `/api/dms/folders/{id}/link` ### F-TAG-01: Tags anwenden - **Assign:** POST `/api/tags/assign` mit `{entity_type, entity_id, tag_id}` → 201 - **Remove:** DELETE `/api/tags/assign?entity_type=...&entity_id=...&tag_id=...` → 204 - **Unique Constraint:** (entity_type, entity_id, tag_id) ### F-TAG-02: Tag-Verwaltung - **Create:** POST `/api/tags` (Admin) → 201 - **Update:** PATCH `/api/tags/{id}` → 200 - **Delete:** DELETE `/api/tags/{id}` → 204 mit Cascade Delete auf `tag_assignments` - **Non-Admin:** → 403 ### F-TAG-03: Tag-Filterung - **Endpoint:** GET `/api/companies?tag_ids=1,2&tag_mode=and` → 200 - **Modi:** `tag_mode=or` → OR-Verknüpfung - **Gleiche Parameter** für `/api/contacts`, `/api/dms/files` ### F-TAG-04: Tag-Cloud - **Endpoint:** GET `/api/tags?with_counts=true` → 200 mit `[{id, name, color, count}]` ### F-PERM-01: Persönlicher Root-Ordner - **User-Creation:** automatisch `personal_folder_id` gesetzt - **Ownership-Check:** GET `/api/dms/folders/{id}` prüft Ownership → 403 bei fremdem Ordner (außer Admin) ### F-PERM-02: Gemeinsame Root-Ordner - **Create:** POST `/api/dms/folders` mit `shared_with_group_id` → 201 - **Permissions:** `folder_permissions` Tabelle (folder_id, group_id, permission: read|write) ### F-PERM-03: Datei/Ordner mit Usern teilen - **Share:** POST `/api/dms/files/{id}/share` mit `{user_id, permission: "read|write"}` → 201 - **Remove:** DELETE `/api/dms/files/{id}/share?user_id=N` → 204 - **Shared-with-me:** GET `/api/dms/shared-with-me` → 200 ### F-PERM-04: Datei/Ordner mit Gruppen teilen - **Share:** POST `/api/dms/files/{id}/share` mit `{group_id, permission: "read|write"}` → 201 - **Resolution:** Individual > Group > Default (Deny vor Allow) - **Remove:** DELETE `/api/dms/files/{id}/share?group_id=N` → 204 ### F-PERM-05: Share-Links - **Create:** POST `/api/dms/files/{id}/share-link` mit `{password?, expires_at?, download_only?}` → 201 mit `{url}` - **Public:** GET `/api/public/share/{token}` → 200 (oder 401 bei Passwort, 410 bei abgelaufen) - **Public-Endpoint:** braucht keine Auth ### F-PERM-06: Berechtigungs-Anzeige - **Endpoint:** GET `/api/dms/files/{id}/permissions` → 200 mit `{owner: {id, name}, shares: [{user_id?, group_id?, name, permission}], share_links: [{id, url, has_password, expires_at, is_active}]}` ### F-FILEUI-01: Datei-Browser - **Komponenten:** `FileBrowser` mit `SidebarTree` + `MainView` (Grid/List Toggle) - **State-Management:** für aktiven Ordner ### F-FILEUI-02: Breadcrumb-Navigation - **Komponente:** `Breadcrumb` mit klickbaren Segmenten - **Pfad:** aus `folder.path` (Materialized Path oder rekursive Abfrage) generiert ### F-FILEUI-03: Kontext-Menü - **Komponente:** `ContextMenu` mit dynamischen Items basierend auf `entity_type` (file/folder) und `permission` (read/write) - **Mobile:** Long-Press statt Rechtsklick ### F-FILEUI-04: Bulk-Aktionen - **Move:** POST `/api/dms/files/bulk-move` mit `{file_ids: [...], target_folder_id}` → 200 - **Delete:** POST `/api/dms/files/bulk-delete` → 200 - **Tag:** POST `/api/tags/bulk-assign` → 201 ### F-FILEUI-05: Upload-Progress - **Komponente:** `UploadProgress` mit pro-Datei Progress - **Upload:** via `XMLHttpRequest` (für Progress-Events) oder WebSocket ### F-FILEUI-06: Drag & Drop - **Frontend:** HTML5 Drag & Drop API - **Drop-Target:** validiert Permission clientseitig (grün = erlaubt, rot = verboten) - **Server:** PATCH `/api/dms/files/{id}` mit `folder_id` → Permission-Check → 200 oder 403 --- ## 12. Kalender-Modul: HTTP-Endpunkte & Architektur-Details ### Architektur-Hinweis Ein Kalender-Eintrag (`CalendarEntry`) hat einen `entry_type`: - **`appointment`** — Termin mit Start/Ende (Datum+Zeit), optional Ganztägig, Ort, Teilnehmer - **`task`** — Aufgabe mit Fälligkeitsdatum (due_date), Priorität, Status, Sub-Tasks, Zuständiger — keine feste Uhrzeit Beide Typen teilen sich die gleichen Verknüpfungs-, Erinnerungs-, Wiederholungs- und Anzeige-Mechanismen. ### F-CAL-01: Kalender-Ansichten - **Frontend:** `CalendarView` Komponente mit `mode: month|week|day` - **Endpoint:** GET `/api/calendar/entries?from=2026-06-01&to=2026-06-30` → 200 mit Entry-Array ### F-CAL-02: Kanban-Zeitraum-Ansicht - **Frontend:** `KanbanCalendar` Komponente mit `period: this_week|next_2_weeks|this_month|custom` - **Endpoint:** GET `/api/calendar/kanban?period=this_week` → 200 mit `{columns: [{date, appointments: [...], tasks: [...]}]}` ### F-CAL-03: Kalender-Eintrag erstellen - **Endpoint:** POST `/api/calendar/entries` → 201 - **Fields appointment:** `entry_type`, `title`, `description?`, `start_at`, `end_at`, `all_day?`, `location?`, `attendees?` - **Fields task:** `entry_type`, `title`, `description?`, `due_date?`, `priority: "high|medium|low"`, `status?: "open"` - **Validierung:** end_at > start_at (außer all_day=true); title nicht leer ### F-CAL-04: Einträge mit Entitäten verknüpfen - **Link:** POST `/api/calendar/entries/{id}/link` mit `{entity_type: "company|contact|deal", entity_id: N}` → 201 - **Response companies:** GET `/api/companies/{id}` enthält `upcoming_events: [...]` und `open_tasks: [...]` ### F-CAL-05: Drag & Drop im Kalender - **Frontend:** HTML5 Drag & Drop auf Kalender-Zellen (appointments) und Kanban-Spalten (tasks) - **Update:** PATCH `/api/calendar/entries/{id}` mit neuem `start_at`/`end_at` (appointment) oder `status` (task) → 200 - **Optimistic Update** mit Rollback bei Fehler ### F-CAL-06: Farbcodierung - **Fields:** `entry_type: appointment|task` und `subtype: normal|follow_up|private` - **Color Map:** `{appointment+normal: "#3B82F6", task+normal: "#F59E0B", *+follow_up: "#F97316", *+private: "#9CA3AF"}` - **Update:** PATCH `/api/calendar/entries/{id}` mit `subtype` → 200 ### F-CAL-07: Erinnerungen/Alerts - **Field:** `reminder: {value: N, unit: "minutes|hours|days", channel: "in_app|email"}` - **Background-Job:** prüft fällige Erinnerungen (Cron, minütlich) - **Notification:** POST `/api/notifications` bei Fälligkeit - **Überfällig-Check:** täglicher Cron-Job ### F-CAL-08: Wiederkehrende Einträge - **Field:** `recurrence: {pattern: "daily|weekly|monthly|yearly|custom", custom_rule?, end_date?, exceptions?: [dates]}` - **Appointments:** Server generiert Instanzen via RRULE (RFC 5545) - **Tasks:** bei Status-Wechsel auf `done` → POST `/api/calendar/entries` mit neuem `due_date` - **Status `cancelled`:** keine Generierung ### F-CAL-09: Kalender-Feeds (ICS) - **Export:** GET `/api/calendar/{calendar_id}/ics-feed?token={user_token}` → 200 mit `Content-Type: text/calendar` (RFC 5545 konform) - **Import:** POST `/api/calendar/import` (multipart ICS, optional calendar_id) → 201 mit `{imported: N, skipped: M}` - **Auth:** Token-basierte Auth für Feed-URL ### F-CAL-10: Ressourcen-Booking - **Create Resource:** POST `/api/resources` (Admin) → 201 - **Book:** POST `/api/calendar/entries/{id}/book-resource` mit `{resource_id}` → 201 oder 409 bei Konflikt - **Bookings:** GET `/api/resources/{id}/bookings?from=...&to=...` → 200 ### F-CAL-11: Mehrere Kalender - **Create:** POST `/api/calendars` mit `{name, color, type: "personal|team|project|company"}` → 201 - **Auto-Create:** bei User-Anlage automatisch POST `/api/calendars` mit `{name: "Mein Kalender", type: "personal"}` - **List:** GET `/api/calendars` → 200 mit sichtbaren Kalendern - **Delete:** DELETE `/api/calendars/{id}` → 204 (Cascade auf Einträge) ### F-CAL-12: Kalender abonnieren - **Frontend:** `CalendarSidebar` mit Toggle-Switches pro Kalender - **Endpoint:** GET `/api/calendar/entries?calendar_ids=1,3,5` → 200 - **State:** persistiert pro User (`user_calendar_visibility` Tabelle) ### F-CAL-13: Kalender teilen - **Share:** POST `/api/calendars/{id}/share` mit `{user_id?, group_id?, permission: "read|write"}` → 201 - **Remove:** DELETE `/api/calendars/{id}/share?user_id=N` → 204 - **Permissions:** GET `/api/calendars/{id}/permissions` → 200 mit `{owner, shares}` ### F-CAL-14: Default-Kalender - **Setting:** `default_calendar_id` (User-Setting) - **Update:** PATCH `/api/users/me/settings` mit `{default_calendar_id: N}` → 200 - **Fallback:** persönlicher Kalender bei keinem Setting ### F-CAL-15: Aufgaben-Zuweisung - **Field:** `assigned_to: user_id` (nur für entry_type=task) - **Update:** PATCH `/api/calendar/entries/{id}` mit `assigned_to` → 200, sendet Notification - **Query:** GET `/api/calendar/entries?entry_type=task&assigned_to={user_id}` → 200 ### F-CAL-16: Sub-Tasks - **Create:** POST `/api/calendar/entries/{id}/subtasks` mit `{title}` → 201 (nur für entry_type=task) - **Update:** PATCH `/api/calendar/entries/{id}/subtasks/{sub_id}` mit `{completed: true}` → 200 - **Response:** GET `/api/calendar/entries/{id}` enthält `subtasks: [{id, title, completed}]` und `progress: {completed, total}` ### F-CAL-17: Aufgaben-Filter & Liste - **Query:** GET `/api/calendar/entries?entry_type=task&assigned_to={id}&priority=high&status=open&due_filter=overdue&page=1&page_size=25&sort_by=due_date&sort_order=asc` → 200 mit `{items, total, page, page_size}` - **Export:** GET `/api/calendar/entries/export?format=csv&entry_type=task&status=open` → 200 mit CSV-Download - **Frontend:** `TaskKanban` (4 Spalten) und `TaskList` (DataTable) Komponenten ### F-CAL-18: Bulk-Aktionen - **Endpoint:** POST `/api/calendar/entries/bulk` mit `{entry_ids: [...], action: "status|assign|due_date|delete", value: ...}` → 200 mit `{updated: N}` --- ## 13. Mail-Modul: HTTP-Endpunkte & Architektur-Details ### F-MAIL-01: Standard-Ordner - **Endpoint:** GET `/api/mail/folders` → 200 mit `{folders: [{id, name, type: "inbox|sent|drafts|spam|custom", unread_count, total_count}]}` - **IMAP IDLE-Listener:** läuft als Background-Task, neue Mails innerhalb von 5 Sekunden - **Bidirektionale Flag-Sync:** \\Seen, \\Flagged, \\Answered, \\Draft, $Spam ### F-MAIL-02: E-Mail schreiben - **Send:** POST `/api/mail/send` mit `{to: [...], cc: [...], bcc: [...], subject, body_html, in_reply_to?, attachments: [...]}` → 201 - **Draft:** POST `/api/mail/drafts` → speichert Entwurf - **Reply:** POST `/api/mail/{id}/reply` → erstellt Reply mit vorausgefüllten Feldern - **Forward:** POST `/api/mail/{id}/forward` → erstellt Forward - **HTML-Sanitization:** DOMPurify ### F-MAIL-03: Volltext-Suche - **Endpoint:** GET `/api/mail/search?q=angebot&folder=inbox&date_from=...&date_to=...` → 200 mit `{items: [{id, subject, from, date, snippet, folder}], total}` - **Index:** PostgreSQL tsvector-Index über `mail_body_tsv` und `mail_subject_tsv` - **Performance:** <500ms bei 10.000 Mails ### F-MAIL-04: Anhänge - **Send:** POST `/api/mail/send` mit multipart-Form-Data für Anhänge - **Download:** GET `/api/mail/{id}/attachments/{att_id}` → 200 - **Content-Disposition:** `attachment` für Downloads, `inline` für inline-Bilder - **Max:** 25 MB pro Anhang, 50 MB pro Mail ### F-MAIL-05: Threading - **Endpoint:** GET `/api/mail/threads?folder=inbox` → 200 mit `{items: [{thread_id, subject, participants, message_count, last_date, messages: [{id, from, date, snippet}]}]}` - **Basis:** `In-Reply-To`/`References`-Header (RFC 5322) - **Frontend:** `ThreadView` Komponente ### F-MAIL-06: Vorlagen/Templates - **Create:** POST `/api/mail/templates` mit `{name, subject, body_html, shared: false}` → 201 - **List:** GET `/api/mail/templates?scope=me|shared` → 200 - **Compose:** POST `/api/mail/compose?template_id=N&contact_id=M&company_id=K` → 200 mit aufgelösten Platzhaltern - **Syntax:** `{{entity.field}}` ### F-MAIL-07: Filter/Regeln - **Create:** POST `/api/mail/rules` mit `{name, conditions: [{field, operator, value}], actions: [{type, value}]}` → 201 - **Evaluation:** Background-Worker nach IMAP-IDLE-Trigger - **List:** GET `/api/mail/rules` → 200 - **Delete:** DELETE `/api/mail/rules/{id}` → 204 ### F-MAIL-08: Abwesenheitsnotiz - **Endpoint:** POST `/api/mail/vacation` mit `{active: true, subject, body, start_date, end_date}` → 201 - **Background-Worker:** prüft eingehende Mails, `vacation_sent_log` Tabelle (einmal pro Absender) - **No-Reply-Erkennung:** Absender enthält „noreply", „no-reply", „donotreply" ### F-MAIL-09: Labels/Flags - **Flag:** PATCH `/api/mail/{id}/flags` mit `{starred: true}` → 200, setzt IMAP \\Flagged - **Label:** POST `/api/mail/{id}/labels` mit `{label_id: N}` → 201 - **Filter:** GET `/api/mail?label=vertrieb` → 200 - **Label-Verwaltung:** POST `/api/mail/labels` mit `{name, color}` ### F-MAIL-10: Kontakt-Verknüpfung - **Contact-Mails:** GET `/api/contacts/{id}/emails` → 200 mit `{items: [{id, subject, from, date, direction: "in|out", thread_id}]}` - **Company-Mails:** GET `/api/companies/{id}/emails` → 200 - **Auto-Verknüpfung:** Background-Job matcht Absender/Empfänger gegen `contacts.email`-Feld - **Manual:** POST `/api/mail/{id}/link` mit `{contact_id, company_id}` ### F-MAIL-11: Kalender-Integration - **Endpoint:** POST `/api/mail/{id}/create-event` mit `{date, time, duration, calendar_id?, title?}` → 201 - **Defaults:** Titel = Mail-Betreff, Beschreibung = Mail-Body (plain text), Teilnehmer = Mail-Absender - **Link:** `calendar_entry.source_mail_id = mail.id` ### F-MAIL-12: PGP-Verschlüsselung - **Import Private Key:** POST `/api/mail/pgp/keys` mit `{private_key, passphrase}` → 201 (verschlüsselt gespeichert) - **Import Public Key:** POST `/api/contacts/{id}/pgp-key` mit `{public_key}` → 201 - **Encrypt Send:** POST `/api/mail/send` mit `{encrypt: true}` → python-gnupg Verschlüsselung - **Decrypt:** GET `/api/mail/{id}` → erkennt PGP-Block, entschlüsselt bei vorhandener Passphrase (Session-Cache) ### F-MAIL-13: Signaturen - **Create:** POST `/api/mail/signatures` mit `{name, body_html}` → 201 - **Assign Default:** PATCH `/api/mail/accounts/{id}` mit `{default_signature_id: N}` → 200 - **Compose:** POST `/api/mail/compose?account_id=N` → Response enthält `signature`-Feld ### F-MAIL-14: Mehrere Postfächer - **Create:** POST `/api/mail/accounts` mit `{name, imap_host, imap_port, imap_ssl, smtp_host, smtp_port, smtp_ssl, username, password}` → 201 (Passwort verschlüsselt) - **List:** GET `/api/mail/accounts` → 200 - **Default:** PATCH `/api/users/me/settings` mit `{default_mail_account_id: N}` ### F-MAIL-15: Geteilte Postfächer - **Create:** POST `/api/mail/accounts` mit `{type: "shared", name: "info@firma.de", ...}` → 201 - **Assign Users:** POST `/api/mail/accounts/{id}/users` mit `{user_ids: [1, 2, 3]}` → 200 - **List:** GET `/api/mail/accounts/shared` → 200 - **Seen-By:** `mail_seen_by` Tabelle (mail_id, user_id, seen_at) ### F-MAIL-16: Stellvertretung - **Delegate:** POST `/api/mail/accounts/{id}/delegates` mit `{delegate_user_id, permission: "read|full"}` → 201 - **Remove:** DELETE `/api/mail/accounts/{id}/delegates?user_id=N` → 204 - **List:** GET `/api/mail/accounts?include_delegated=true` → 200 mit `delegated: true` Markierung ### F-MAIL-17: Sende-Berechtigungen - **Grant:** POST `/api/mail/accounts/{id}/send-permissions` mit `{user_ids: [...]}` → 200 - **List:** GET `/api/mail/accounts/{id}/send-permissions` → 200 - **Send-Check:** POST `/api/mail/send` mit `{from_account_id: N}` → prüft, 403 wenn nicht ### F-MAIL-18: Postfach-Konfiguration - **Create:** POST `/api/mail/accounts` mit `{imap_host, imap_port, imap_ssl: true, smtp_host, smtp_port, smtp_starttls: true, username, password}` - **Validierung:** IMAP-Login + SMTP-Login, 422 bei Fehler - **Verschlüsselung:** AES-256, Key via Env-Var `MAIL_ENCRYPTION_KEY` - **Maskierung:** GET `/api/mail/accounts/{id}` → Passwort-Feld ist `***` ### F-MAIL-19: Mail-Ordner verwalten - **Create:** POST `/api/mail/folders` mit `{name, parent_id?}` → 201, führt IMAP CREATE aus - **Rename:** PATCH `/api/mail/folders/{id}` mit `{name}` → 200, führt IMAP RENAME aus - **Delete:** DELETE `/api/mail/folders/{id}` → 204, führt IMAP DELETE aus --- ## 14. Mail-Modul: Technische Constraints (vollständige Tabelle) | Constraint | Beschreibung | |-----------|-------------| | IMAP-Protokoll | IMAP4rev1 (RFC 3501) — Server muss IMAP IDLE (RFC 2177) für Push unterstützen | | SMTP-Protokoll | SMTP Submission (RFC 6409) — Port 587 mit STARTTLS oder Port 465 mit SSL | | Auth | SMTP: PLAIN/LOGIN mit Benutzername+Passwort; IMAP: PLAIN/LOGIN | | Attachment-Storage | Anhänge lokal gespeichert (DMS-Integration), nicht im IMAP-Server belassen (Caching) | | IMAP IDLE | Real-time Push; Fallback: Polling alle 5 Min | | Volltext-Suche | PostgreSQL Full-Text Search (tsvector) über Mail-Body + Anhang-Namen (OCR optional post-MVP) | | HTML-Rendering | Sanitization mit DOMPurify (XSS-Schutz); Plain-Text-Fallback | | Verschlüsselung | PGP (OpenPGP, RFC 4880) via python-gnupg; S/MIME post-MVP | | Mail-Sync | Bidirektionale Sync: IMAP-Flags (Seen/Flagged) synchronisiert | | Ordner-Mapping | INBOX→Posteingang, Sent→Postausgang, Drafts→Entwürfe, Spam/Junk→Spam | | Passwort-Speicherung | IMAP/SMTP-Passwörter AES-256 verschlüsselt, Key via Env-Var MAIL_ENCRYPTION_KEY | | Max Anhang-Größe | 25 MB pro Anhang, 50 MB pro Mail | --- ## 15. Mail-Modul: Annahmen 1. **IMAP-Server-Kompatibilität:** IMAP4rev1 (RFC 3501), getestet mit Dovecot, Courier IMAP. 2. **SMTP-Auth:** SMTP-Server muss Authentifizierung unterstützen (PLAIN/LOGIN). Kein Open-Relay. 3. **IMAP IDLE:** Server muss RFC 2177 unterstützen. Fallback: Polling alle 5 Minuten. 4. **IMAP MOVE:** Server muss RFC 6851 unterstützen. Fallback: COPY + STORE \\Deleted + EXPUNGE. 5. **Attachment-Storage:** Lokal (Dateisystem oder S3), Caching in PostgreSQL. 6. **Maximale Anhang-Größe:** 25 MB pro Anhang, 50 MB pro Mail. 7. **Passwort-Speicherung:** AES-256 verschlüsselt, Key via `MAIL_ENCRYPTION_KEY`. 8. **HTML-Sanitization:** DOMPurify, Inline-Styles erlaubt, Scripts/IFrames entfernt. 9. **Multi-Tenant (Multi-Company):** Mail-Modul ist Multi-Tenant (Multi-Company). 10. **PGP-Key-Verwaltung:** Private Keys verschlüsselt (AES-256 + Passphrase). Passphrase nicht gespeichert, nur Session-Cache. --- ## 16. Implizite DB-Tabellen (aus allen Akzeptanzkriterien extrahiert) | Tabelle | Modul | Zweck | |---------|-------|-------| | `users` | Core | User (email, name, role, password_hash, default_calendar_id, default_mail_account_id) | | `companies` | Core | Firmen (siehe Feld-Tabelle + deleted_at, created_at) | | `contacts` | Core | Kontakte (siehe Feld-Tabelle + deleted_at, created_at) | | `company_contacts` | Core | N:M-Verknüpfung (company_id, contact_id) | | `audit_log` | Core | Audit (user, action, entity, entity_id, timestamp) | | `deletion_log` | Core | Unveränderliche DSGVO-Lösch-Logs | | `dms_folders` | DMS | Ordner (id, name, parent_id, owner_id, deleted_at) | | `dms_files` | DMS | Dateien (id, name, folder_id, size, mime_type, uploaded_at, modified_at, uploaded_by, deleted_at) | | `file_links` | DMS | Verknüpfung (file_id, entity_type, entity_id) — Unique Constraint | | `tags` | DMS | Tags (id, name, color) | | `tag_assignments` | DMS | Zuordnung (entity_type, entity_id, tag_id) — Unique Constraint | | `folder_permissions` | DMS | (folder_id, group_id, permission: read|write) | | `file_shares` | DMS | (file_id, user_id?, group_id?, permission) | | `share_links` | DMS | (file_id, token, password?, expires_at?, download_only?) | | `calendar_entries` | Kalender | (id, entry_type, title, description, start_at?, end_at?, due_date?, all_day?, location?, priority?, status?, assigned_to?, calendar_id, subtype, reminder, recurrence) | | `calendars` | Kalender | (id, name, color, type, owner_id) | | `calendar_entry_links` | Kalender | (entry_id, entity_type, entity_id) | | `calendar_shares` | Kalender | (calendar_id, user_id?, group_id?, permission) | | `user_calendar_visibility` | Kalender | (user_id, calendar_id, visible) | | `subtasks` | Kalender | (entry_id, title, completed) | | `resources` | Kalender | (id, name, type) — später | | `resource_bookings` | Kalender | (resource_id, entry_id, from, to) — später | | `mail_accounts` | Mail | (id, user_id, name, imap_host, imap_port, imap_ssl, smtp_host, smtp_port, smtp_ssl/starttls, username, password_encrypted, type) | | `mails` | Mail | (id, account_id, folder_id, subject, body_html, body_text, from, to, cc, bcc, date, in_reply_to, references, thread_id, seen, flagged, has_attachments) | | `mail_attachments` | Mail | (mail_id, filename, mime_type, size, dms_file_id) | | `mail_folders` | Mail | (id, account_id, name, type, parent_id, unread_count, total_count) | | `mail_labels` | Mail | (id, name, color) | | `mail_label_assignments` | Mail | (mail_id, label_id) | | `mail_rules` | Mail | (id, account_id, name, conditions, actions, priority) | | `mail_templates` | Mail | (id, user_id, name, subject, body_html, shared) | | `mail_signatures` | Mail | (id, user_id, name, body_html) | | `vacation_sent_log` | Mail | (account_id, sender_address, sent_at) | | `mail_seen_by` | Mail | (mail_id, user_id, seen_at) | | `mail_account_delegates` | Mail | (account_id, delegate_user_id, permission) | | `mail_account_send_permissions` | Mail | (account_id, user_id) | | `pgp_keys` | Mail | (user_id, private_key_encrypted, public_key) | | `contact_pgp_keys` | Mail | (contact_id, public_key) | | `notifications` | Core | (id, user_id, type, title, body, created_at, read_at) | --- ## 17. Historische v0.1 Endpoints (archiviert) | Endpoint | Historisch | Aktuell | |----------|-----------|--------| | POST `/login` | F-1 | POST `/api/auth/login` | | POST `/logout` | F-2 | POST `/api/auth/logout` | | GET `/companies` | F-5 | GET `/api/companies` | | POST `/companies` | F-5 | POST `/api/companies` | | DELETE `/companies/{id}` | F-8 | DELETE `/api/companies/{id}` | | POST `/contacts` | F-10 | POST `/api/contacts` | | GET `/api/health` | F-15 | GET `/api/health` | ### v0.1 Demo-Seed - Beim ersten Start: 1 Admin (`admin`/`admin`), 2 Firmen, 3 Kontakte - Frische DB → Seed-Daten vorhanden - DB bereits befüllt → Seed überspringt --- ## 18. Non-Goals (alle Module, vollständig) ### v1 Non-Goals 1. Self-Registration (nur Admin legt User an) 2. ~~Multi-Tenant (Single-Tenant in v1)~~ — Multi-Tenant (Multi-Company) ist v1-Feature 3. Sales Pipeline / Deals 4. Kampagnen-Management 5. Mobile App (Native) — nur Responsive Web-UI 6. Offline-Support 7. AI-Features (Lead-Scoring, Auto-Enrichment) 8. Real-time Collaboration (Google Docs-style) 9. Webhooks für externe Systeme 10. Multi-Currency 11. Advanced Analytics/Dashboards (BI) 12. SSO/OAuth (nur E-Mail/Passwort) 13. Custom Fields (Standard-Felder fest definiert) 14. Two-Factor Auth (post-MVP) 15. PWA ### v2 DMS Non-Goals 1. Versionierung (History, Diff) 2. Volltext-Suche in Dokumenten (kein OCR, kein Volltext-Index) 3. Eigene Office-Suite (nur OnlyOffice) 4. E-Mail-Attachment aus DMS 5. Externes Sync (WebDAV/Nextcloud) 6. Watermarking ### v2 Kalender Non-Goals 1. Ressourcen-Booking (nur optional markiert) 2. Time-Tracking 3. Task-Templates 4. Automatisierte Task-Erstellung aus Triggern 5. Gantt-Diagramm 6. Öffentliche Kalender-Sync (außer ICS) ### v2 Mail Non-Goals 1. Google/Microsoft API (nur direkte IMAP/SMTP) 2. S/MIME (nur PGP in v2-mail) 3. Mail-Server-Hosting (nur Client) 4. Mailinglisten-Management 5. Newsletter-Tool 6. Mail-to-Ticket 7. OCR für Anhänge 8. Kalender-Einladungen per Mail (iMIP, RFC 6047) 9. JMAP (RFC 8620) 10. Push-Benachrichtigungen (Web Push) — nur In-App --- **Ende der extrahierten Architektur-Details.**