feat: add contacts phase 2, advanced search, vacation responder, Docker & TOTP 2FA

- Contact groups/lists, vCard import/export (RFC 6350), bulk operations
- Advanced search with JMAP filter panel, search chips, cross-mailbox queries
- Vacation responder with JMAP VacationResponse, settings tab, sidebar indicator
- TOTP two-factor authentication support
- Docker multi-stage build with standalone output and docker-compose
- CSP Report-Only headers and security headers via proxy middleware
- Virtual scrolling for large email lists
- Structured server-side logger (text/JSON, configurable level)
- 450+ tests (contacts, vCard, threads, headers, identity, components)
- Playwright E2E framework setup
- Updated README and ROADMAP with all new features
This commit is contained in:
Matthieu MALVACHE
2026-02-16 18:51:30 +01:00
committed by Matthieu MALVACHE
parent 8a5bc9b88d
commit fb414bf2c9
70 changed files with 8160 additions and 547 deletions
+132 -4
View File
@@ -11,14 +11,19 @@
"error": {
"invalid_credentials": "Ungültige E-Mail-Adresse oder Passwort",
"connection_failed": "Verbindung zum Server fehlgeschlagen",
"generic": "Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut."
"generic": "Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.",
"totp_invalid": "Ungültiger Authentifizierungscode. Überprüfen Sie Ihre Authenticator-App."
},
"config_error": {
"title": "Konfigurationsfehler",
"fetch_failed": "Die Anwendungskonfiguration kann nicht geladen werden. Bitte versuchen Sie es später erneut.",
"server_not_configured": "Der E-Mail-Server wurde nicht konfiguriert. Bitte kontaktieren Sie Ihren Administrator."
},
"remove_from_history": "Aus Verlauf entfernen"
"remove_from_history": "Aus Verlauf entfernen",
"totp_toggle": "Ich habe Zwei-Faktor-Authentifizierung",
"totp_label": "Authentifizierungscode",
"totp_placeholder": "000000",
"totp_hide": "Zwei-Faktor-Authentifizierung ausblenden"
},
"sidebar": {
"close": "Schließen",
@@ -59,7 +64,8 @@
"compose": "Verfassen",
"go_back": "Zurück"
},
"clear_search": "Suche löschen"
"clear_search": "Suche löschen",
"vacation_active": "Abwesenheitsnotiz ist aktiv"
},
"email_list": {
"no_emails": "Keine E-Mails",
@@ -313,7 +319,9 @@
"identity_update_failed": "Identität aktualisieren fehlgeschlagen: {{error}}",
"identity_delete_failed": "Identität löschen fehlgeschlagen: {{error}}",
"identity_unauthorized": "Sie sind nicht autorisiert, von dieser E-Mail-Adresse zu senden",
"identity_not_found": "Identität nicht gefunden"
"identity_not_found": "Identität nicht gefunden",
"vacation_saved": "Abwesenheitsnotiz-Einstellungen gespeichert",
"vacation_save_failed": "Fehler beim Speichern der Abwesenheitsnotiz-Einstellungen"
},
"date": {
"today": "Heute",
@@ -366,6 +374,7 @@
"privacy": "Datenschutz & Sicherheit",
"account": "Konto",
"identities": "Identitäten",
"vacation": "Abwesenheitsnotiz",
"advanced": "Erweitert"
},
"appearance": {
@@ -568,6 +577,49 @@
"learn_more": "Mehr erfahren"
}
},
"vacation": {
"title": "Abwesenheitsnotiz",
"description": "Automatisch auf eingehende E-Mails antworten, während Sie abwesend sind",
"loading": "Abwesenheitseinstellungen werden geladen...",
"not_supported": "Ihr Mailserver unterstützt keine Abwesenheitsantworten.",
"fetch_error": "Fehler beim Laden der Abwesenheitseinstellungen. Bitte versuchen Sie es erneut.",
"status": {
"label": "Abwesenheitsnotiz",
"description": "Senden Sie eine automatische Antwort an Personen, die Ihnen eine E-Mail senden",
"active": "Aktiv",
"inactive": "Inaktiv"
},
"date_range": {
"title": "Zeitraum",
"description": "Optional die automatische Antwort auf einen bestimmten Zeitraum beschränken",
"start": "Startdatum",
"start_description": "Leer lassen für kein Startlimit",
"end": "Enddatum",
"end_description": "Leer lassen für kein Endlimit"
},
"message": {
"title": "Automatische Antwortnachricht",
"description": "Die Nachricht, die als Antwort gesendet wird",
"subject_label": "Betreff",
"subject_description": "Betreffzeile der automatischen Antwort",
"subject_placeholder": "Abwesenheitsnotiz",
"body_label": "Nachrichtentext",
"body_description": "Nur-Text-Nachrichteninhalt",
"body_placeholder": "Vielen Dank für Ihre E-Mail. Ich bin derzeit nicht im Büro und werde nach meiner Rückkehr antworten."
},
"preview": {
"title": "Vorschau",
"show": "Vorschau anzeigen",
"hide": "Vorschau ausblenden"
},
"save": "Änderungen speichern",
"saving": "Speichern...",
"warnings": {
"end_before_start": "Das Enddatum muss nach dem Startdatum liegen",
"start_in_past": "Das Startdatum liegt in der Vergangenheit",
"empty_body": "Der Nachrichtentext ist leer — Empfänger erhalten eine leere Antwort"
}
},
"advanced": {
"title": "Erweitert",
"description": "Erweiterte Optionen und Entwicklereinstellungen",
@@ -753,6 +805,10 @@
"delete_confirm": "Möchten Sie diesen Kontakt wirklich löschen?",
"local_mode": "Kontakte werden lokal gespeichert (Server unterstützt kein JMAP Contacts)",
"back_to_mail": "Zurück zur E-Mail",
"tabs": {
"all": "Alle",
"groups": "Gruppen"
},
"detail": {
"emails": "E-Mail-Adressen",
"phones": "Telefonnummern",
@@ -788,6 +844,55 @@
"email_invalid": "Bitte geben Sie eine gültige E-Mail-Adresse ein",
"save_failed": "Kontakt konnte nicht gespeichert werden"
},
"groups": {
"create": "Neue Gruppe",
"edit": "Gruppe bearbeiten",
"empty": "Keine Gruppen",
"delete_confirm": "Möchten Sie diese Gruppe wirklich löschen?",
"name_label": "Gruppenname",
"name_placeholder": "z.B. Team, Familie",
"name_required": "Gruppenname ist erforderlich",
"save_failed": "Gruppe konnte nicht gespeichert werden",
"members_label": "Mitglieder",
"search_members": "Kontakte zum Hinzufügen suchen...",
"no_members": "Keine Mitglieder in dieser Gruppe",
"member_count": "{count, plural, =0 {Keine Mitglieder} one {1 Mitglied} other {# Mitglieder}}"
},
"import": {
"title": "Kontakte importieren",
"drop_hint": "Klicken Sie, um eine vCard-Datei auszuwählen",
"file_types": ".vcf- oder .vcard-Dateien",
"no_contacts": "Keine Kontakte in der Datei gefunden",
"parse_error": "vCard-Datei konnte nicht gelesen werden",
"found": "{count, plural, one {1 Kontakt gefunden} other {# Kontakte gefunden}}",
"duplicate": "Duplikat",
"select_all": "Alle auswählen",
"deselect_all": "Alle abwählen",
"selected": "{count, plural, one {1 ausgewählt} other {# ausgewählt}}",
"import_button": "Importieren",
"importing": "Wird importiert...",
"success": "{count, plural, one {1 Kontakt importiert} other {# Kontakte importiert}}",
"failed": "Import fehlgeschlagen",
"close": "Schließen",
"file_too_large": "Datei ist zu groß (max. 5 MB)"
},
"export": {
"title": "Kontakte exportieren",
"success": "{count, plural, one {1 Kontakt exportiert} other {# Kontakte exportiert}}"
},
"bulk": {
"selected": "{count, plural, one {1 ausgewählt} other {# ausgewählt}}",
"select_all": "Alle auswählen",
"delete": "Löschen",
"delete_confirm": "{count, plural, one {1 Kontakt} other {# Kontakte}} löschen?",
"deleted": "{count, plural, one {1 Kontakt gelöscht} other {# Kontakte gelöscht}}",
"add_to_group": "Zur Gruppe hinzufügen",
"choose_group": "Gruppe wählen",
"adding_contacts": "{count, plural, one {1 Kontakt} other {# Kontakte}} hinzufügen",
"added_to_group": "Kontakte zur Gruppe hinzugefügt",
"export": "Exportieren",
"clear": "Auswahl aufheben"
},
"toast": {
"created": "Kontakt erstellt",
"updated": "Kontakt aktualisiert",
@@ -796,5 +901,28 @@
"error_update": "Kontakt konnte nicht aktualisiert werden",
"error_delete": "Kontakt konnte nicht gelöscht werden"
}
},
"advanced_search": {
"title": "Erweiterte Suche",
"from": "Von",
"from_placeholder": "E-Mail oder Name des Absenders",
"to": "An",
"to_placeholder": "E-Mail oder Name des Empfängers",
"subject": "Betreff",
"subject_placeholder": "Betreff enthält...",
"body": "Nachricht",
"has_attachment": "Anhänge",
"date_after": "Nach dem",
"date_before": "Vor dem",
"starred": "Markiert",
"unread": "Ungelesen",
"read": "Gelesen",
"yes": "Ja",
"no": "Nein",
"clear": "Löschen",
"clear_all": "Alle löschen",
"filters_active": "{count} Filter",
"filters_active_plural": "{count} Filter",
"toggle_filters": "Filter"
}
}