fix: mail UI German labels, multi-folder sync, account management, resizable panel fix

This commit is contained in:
Agent Zero
2026-07-15 14:36:45 +02:00
parent 0409a08002
commit e0975f4044
8 changed files with 690 additions and 151 deletions
+1
View File
@@ -27,6 +27,7 @@ export interface MailFolder {
id: string;
account_id: string;
name: string;
imap_name?: string;
parent_id: string | null;
unread_count: number;
total_count: number;
@@ -11,6 +11,36 @@ import type { MailAccount, MailFolder } from '@/api/mail';
import { Badge } from '@/components/ui/Badge';
import { EmptyState } from '@/components/ui/EmptyState';
/**
* Map IMAP folder names to German display names.
*/
const FOLDER_NAME_MAP: Record<string, string> = {
INBOX: 'Posteingang',
Sent: 'Gesendet',
Drafts: 'Entwürfe',
Spam: 'Spam',
Junk: 'Spam',
Trash: 'Papierkorb',
'Sent Items': 'Gesendet',
'Sent Mail': 'Gesendet',
'Draft': 'Entwürfe',
'Deleted': 'Papierkorb',
'Deleted Items': 'Papierkorb',
'Junk Email': 'Spam',
'Junk E-mail': 'Spam',
};
/**
* Get the display name for a folder — uses the German name from the map
* if the imap_name matches a known folder, otherwise uses the stored name.
*/
function getFolderDisplayName(folder: MailFolder): string {
if (folder.imap_name && FOLDER_NAME_MAP[folder.imap_name]) {
return FOLDER_NAME_MAP[folder.imap_name];
}
return folder.name;
}
export interface MailFolderTreeProps {
accounts: MailAccount[];
folders: MailFolder[];
@@ -90,7 +120,7 @@ function FolderNode({
const hasChildren = folder.children && folder.children.length > 0;
return (
<div role="treeitem" aria-selected={isSelected} aria-label={folder.name}>
<div role="treeitem" aria-selected={isSelected} aria-label={getFolderDisplayName(folder)}>
<div className="flex items-center" style={{ paddingLeft: `${depth * 12}px` }}>
{hasChildren ? (
<button
@@ -119,7 +149,7 @@ function FolderNode({
data-testid={`folder-${folder.id}`}
>
<FolderIcon />
<span className="flex-1 truncate text-left">{folder.name}</span>
<span className="flex-1 truncate text-left">{getFolderDisplayName(folder)}</span>
{folder.unread_count > 0 && (
<Badge variant="primary" className="text-xs">{folder.unread_count}</Badge>
)}
@@ -84,7 +84,8 @@ export function ResizablePanel({
{children}
{resizable && (
<div
className="absolute top-0 right-0 h-full w-1 cursor-col-resize bg-transparent hover:bg-primary-300 active:bg-primary-400 transition-colors z-10"
className="absolute top-0 right-0 h-full w-1.5 cursor-col-resize bg-transparent hover:bg-primary-300 active:bg-primary-400 transition-colors z-50 pointer-events-auto"
style={{ marginRight: '-3px' }}
onMouseDown={handleMouseDown}
data-testid="resize-handle"
role="separator"
+154
View File
@@ -380,5 +380,159 @@
"paymentTermsDays": "Zahlungsziel (Tage)",
"saved": "Systemeinstellungen gespeichert.",
"saveFailed": "Speichern fehlgeschlagen."
},
"mail": {
"title": "E-Mail",
"settings": "E-Mail-Einstellungen",
"accounts": "Mail-Accounts",
"addAccount": "Account hinzufügen",
"deleteAccount": "Löschen",
"confirmDeleteAccount": "Möchten Sie diesen Mail-Account wirklich löschen? Alle zugehörigen E-Mails und Ordner werden entfernt.",
"accountCreated": "Account erfolgreich erstellt.",
"accountDeleted": "Account erfolgreich gelöscht.",
"noAccounts": "Keine Mail-Accounts konfiguriert.",
"noAccountsDesc": "Fügen Sie einen Mail-Account hinzu, um E-Mails zu empfangen und zu senden.",
"configureAccount": "Account konfigurieren",
"personal": "Persönlich",
"shared": "Geteilt",
"email": "E-Mail-Adresse",
"displayName": "Anzeigename",
"password": "Passwort",
"imapHost": "IMAP-Server",
"imapPort": "IMAP-Port",
"smtpHost": "SMTP-Server",
"smtpPort": "SMTP-Port",
"serverInfo": "Server-Info",
"testConnection": "Verbindung testen",
"connectionSuccess": "Verbindung erfolgreich.",
"syncNow": "Jetzt synchronisieren",
"syncSuccess": "{{count}} E-Mails synchronisiert.",
"selectAccount": "Account auswählen",
"selectAccountFirst": "Bitte wählen Sie zuerst einen Account aus.",
"folders": "Ordner",
"noFolders": "Keine Ordner vorhanden",
"noFoldersDesc": "Es sind noch keine Ordner konfiguriert.",
"noMails": "Keine E-Mails",
"noMailsDesc": "In diesem Ordner sind keine E-Mails vorhanden.",
"noSubject": "Kein Betreff",
"selectMail": "E-Mail auswählen",
"selectMailToRead": "Keine E-Mail ausgewählt",
"selectMailToReadDesc": "Wählen Sie eine E-Mail aus der Liste, um sie zu lesen.",
"from": "Von",
"to": "An",
"cc": "CC",
"bcc": "BCC",
"date": "Datum",
"subject": "Betreff",
"subjectPlaceholder": "Betrereff eingeben...",
"body": "Text",
"attachments": "Anhänge",
"download": "Herunterladen",
"compose": "Verfassen",
"send": "Senden",
"sent": "E-Mail gesendet.",
"reply": "Antworten",
"forward": "Weiterleiten",
"forwarding": "Weiterleitung",
"flag": "Markieren",
"unflag": "Markierung entfernen",
"flagged": "Markiert",
"toggleFlag": "Flagge umschalten",
"unread": "Ungelesen",
"createEvent": "Termin erstellen",
"eventCreated": "Termin aus E-Mail erstellt.",
"searchPlaceholder": "E-Mails durchsuchen...",
"showCcBcc": "CC/BCC anzeigen",
"htmlUnsafe": "HTML-Inhalt wurde aus Sicherheitsgründen bereinigt.",
"bold": "Fett",
"italic": "Kursiv",
"insertLink": "Link einfügen",
"linkUrl": "URL",
"insertTemplate": "Vorlage einfügen",
"template": "Vorlage",
"selectTemplate": "Vorlage auswählen",
"noTemplates": "Keine Vorlagen vorhanden",
"noTemplatesDesc": "Es sind noch keine E-Mail-Vorlagen erstellt.",
"signature": "Signatur",
"signatures": "Signaturen",
"signatureName": "Name der Signatur",
"signatureBody": "Signaturtext",
"newSignature": "Neue Signatur",
"noSignature": "Keine Signatur",
"noSignatures": "Keine Signaturen vorhanden",
"noSignaturesDesc": "Erstellen Sie eine Signatur für Ihre E-Mails.",
"defaultSignature": "Standard-Signatur",
"signatureCreated": "Signatur erstellt.",
"signatureUpdated": "Signatur aktualisiert.",
"signatureDeleted": "Signatur gelöscht.",
"confirmDeleteSignature": "Möchten Sie diese Signatur wirklich löschen?",
"deleteSignature": "Signatur löschen",
"rules": "Regeln",
"newRule": "Neue Regel",
"ruleName": "Regelname",
"rulePriority": "Priorität",
"ruleConditions": "Bedingungen",
"ruleActions": "Aktionen",
"addCondition": "Bedingung hinzufügen",
"addAction": "Aktion hinzufügen",
"ruleCondFromContains": "Von enthält",
"ruleCondToContains": "An enthält",
"ruleCondSubjectContains": "Betreff enthält",
"ruleCondBodyContains": "Text enthält",
"ruleCondHasAttachment": "Hat Anhang",
"ruleCondValue": "Wert",
"ruleActionMoveToFolder": "In Ordner verschieben",
"ruleActionMarkAsRead": "Als gelesen markieren",
"ruleActionMarkAsFlagged": "Als markiert markieren",
"ruleActionForwardTo": "Weiterleiten an",
"ruleActionDelete": "Löschen",
"ruleActionValue": "Wert",
"ruleCreated": "Regel erstellt.",
"ruleDeleted": "Regel gelöscht.",
"confirmDeleteRule": "Möchten Sie diese Regel wirklich löschen?",
"deleteRule": "Regel löschen",
"noRules": "Keine Regeln vorhanden",
"noRulesDesc": "Erstellen Sie Regeln für die automatische E-Mail-Verarbeitung.",
"labels": "Labels",
"newLabel": "Neues Label",
"labelName": "Label-Name",
"labelColor": "Farbe",
"labelCreated": "Label erstellt.",
"labelDeleted": "Label gelöscht.",
"confirmDeleteLabel": "Möchten Sie dieses Label wirklich löschen?",
"deleteLabel": "Label löschen",
"noLabels": "Keine Labels vorhanden",
"noLabelsDesc": "Erstellen Sie Labels zur Organisation Ihrer E-Mails.",
"vacationResponder": "Abwesenheitsnotiz",
"enableVacation": "Abwesenheitsnotiz aktivieren",
"vacationSubject": "Betreff",
"vacationSubjectPlaceholder": "Abwesenheitsbetreff eingeben...",
"vacationBody": "Nachricht",
"vacationBodyPlaceholder": "Ihre Abwesenheitsnachricht...",
"vacationStart": "Startdatum",
"vacationEnd": "Enddatum",
"vacationSaved": "Abwesenheitsnotiz gespeichert.",
"pgpKeys": "PGP-Schlüssel",
"pgpImportKey": "PGP-Schlüssel importieren",
"importKey": "Schlüssel importieren",
"privateKey": "Privater Schlüssel",
"privateKeyLabel": "Privater PGP-Schlüssel",
"publicKey": "Öffentlicher Schlüssel",
"passphrase": "Passphrase",
"pgpKeyImported": "PGP-Schlüssel erfolgreich importiert.",
"contactPgpKeys": "PGP-Schlüssel der Kontakte",
"contactPgpStored": "PGP-Schlüssel für Kontakt gespeichert.",
"storeContactKey": "Öffentlichen Schlüssel speichern",
"contactId": "Kontakt-ID",
"noPgpKeys": "Keine PGP-Schlüssel vorhanden",
"noPgpKeysDesc": "Importieren Sie einen PGP-Schlüssel für die Verschlüsselung.",
"noContactKeys": "Keine Kontakt-Schlüssel vorhanden",
"noContactKeysDesc": "Speichern Sie öffentliche Schlüssel Ihrer Kontakte.",
"tabAccounts": "Accounts",
"tabSignatures": "Signaturen",
"tabRules": "Regeln",
"tabLabels": "Labels",
"tabVacation": "Abwesenheit",
"tabPgp": "PGP-Verschlüsselung"
}
}
+154
View File
@@ -380,5 +380,159 @@
"paymentTermsDays": "Payment terms (days)",
"saved": "System settings saved.",
"saveFailed": "Failed to save settings."
},
"mail": {
"title": "Email",
"settings": "Email Settings",
"accounts": "Mail Accounts",
"addAccount": "Add Account",
"deleteAccount": "Delete",
"confirmDeleteAccount": "Are you sure you want to delete this mail account? All associated emails and folders will be removed.",
"accountCreated": "Account created successfully.",
"accountDeleted": "Account deleted successfully.",
"noAccounts": "No mail accounts configured.",
"noAccountsDesc": "Add a mail account to send and receive emails.",
"configureAccount": "Configure Account",
"personal": "Personal",
"shared": "Shared",
"email": "Email Address",
"displayName": "Display Name",
"password": "Password",
"imapHost": "IMAP Server",
"imapPort": "IMAP Port",
"smtpHost": "SMTP Server",
"smtpPort": "SMTP Port",
"serverInfo": "Server Info",
"testConnection": "Test Connection",
"connectionSuccess": "Connection successful.",
"syncNow": "Sync Now",
"syncSuccess": "{{count}} emails synced.",
"selectAccount": "Select Account",
"selectAccountFirst": "Please select an account first.",
"folders": "Folders",
"noFolders": "No folders available",
"noFoldersDesc": "No folders have been configured yet.",
"noMails": "No emails",
"noMailsDesc": "There are no emails in this folder.",
"noSubject": "No Subject",
"selectMail": "Select Email",
"selectMailToRead": "No email selected",
"selectMailToReadDesc": "Select an email from the list to read it.",
"from": "From",
"to": "To",
"cc": "CC",
"bcc": "BCC",
"date": "Date",
"subject": "Subject",
"subjectPlaceholder": "Enter subject...",
"body": "Body",
"attachments": "Attachments",
"download": "Download",
"compose": "Compose",
"send": "Send",
"sent": "Email sent.",
"reply": "Reply",
"forward": "Forward",
"forwarding": "Forwarding",
"flag": "Flag",
"unflag": "Unflag",
"flagged": "Flagged",
"toggleFlag": "Toggle Flag",
"unread": "Unread",
"createEvent": "Create Event",
"eventCreated": "Event created from email.",
"searchPlaceholder": "Search emails...",
"showCcBcc": "Show CC/BCC",
"htmlUnsafe": "HTML content was sanitized for security.",
"bold": "Bold",
"italic": "Italic",
"insertLink": "Insert Link",
"linkUrl": "URL",
"insertTemplate": "Insert Template",
"template": "Template",
"selectTemplate": "Select Template",
"noTemplates": "No templates available",
"noTemplatesDesc": "No email templates have been created yet.",
"signature": "Signature",
"signatures": "Signatures",
"signatureName": "Signature Name",
"signatureBody": "Signature Body",
"newSignature": "New Signature",
"noSignature": "No Signature",
"noSignatures": "No signatures available",
"noSignaturesDesc": "Create a signature for your emails.",
"defaultSignature": "Default Signature",
"signatureCreated": "Signature created.",
"signatureUpdated": "Signature updated.",
"signatureDeleted": "Signature deleted.",
"confirmDeleteSignature": "Are you sure you want to delete this signature?",
"deleteSignature": "Delete Signature",
"rules": "Rules",
"newRule": "New Rule",
"ruleName": "Rule Name",
"rulePriority": "Priority",
"ruleConditions": "Conditions",
"ruleActions": "Actions",
"addCondition": "Add Condition",
"addAction": "Add Action",
"ruleCondFromContains": "From contains",
"ruleCondToContains": "To contains",
"ruleCondSubjectContains": "Subject contains",
"ruleCondBodyContains": "Body contains",
"ruleCondHasAttachment": "Has attachment",
"ruleCondValue": "Value",
"ruleActionMoveToFolder": "Move to folder",
"ruleActionMarkAsRead": "Mark as read",
"ruleActionMarkAsFlagged": "Mark as flagged",
"ruleActionForwardTo": "Forward to",
"ruleActionDelete": "Delete",
"ruleActionValue": "Value",
"ruleCreated": "Rule created.",
"ruleDeleted": "Rule deleted.",
"confirmDeleteRule": "Are you sure you want to delete this rule?",
"deleteRule": "Delete Rule",
"noRules": "No rules available",
"noRulesDesc": "Create rules for automatic email processing.",
"labels": "Labels",
"newLabel": "New Label",
"labelName": "Label Name",
"labelColor": "Color",
"labelCreated": "Label created.",
"labelDeleted": "Label deleted.",
"confirmDeleteLabel": "Are you sure you want to delete this label?",
"deleteLabel": "Delete Label",
"noLabels": "No labels available",
"noLabelsDesc": "Create labels to organize your emails.",
"vacationResponder": "Vacation Responder",
"enableVacation": "Enable vacation responder",
"vacationSubject": "Subject",
"vacationSubjectPlaceholder": "Enter vacation subject...",
"vacationBody": "Message",
"vacationBodyPlaceholder": "Your vacation message...",
"vacationStart": "Start Date",
"vacationEnd": "End Date",
"vacationSaved": "Vacation responder saved.",
"pgpKeys": "PGP Keys",
"pgpImportKey": "Import PGP Key",
"importKey": "Import Key",
"privateKey": "Private Key",
"privateKeyLabel": "Private PGP Key",
"publicKey": "Public Key",
"passphrase": "Passphrase",
"pgpKeyImported": "PGP key imported successfully.",
"contactPgpKeys": "Contact PGP Keys",
"contactPgpStored": "PGP key stored for contact.",
"storeContactKey": "Store Public Key",
"contactId": "Contact ID",
"noPgpKeys": "No PGP keys available",
"noPgpKeysDesc": "Import a PGP key for encryption.",
"noContactKeys": "No contact keys available",
"noContactKeysDesc": "Store public keys for your contacts.",
"tabAccounts": "Accounts",
"tabSignatures": "Signatures",
"tabRules": "Rules",
"tabLabels": "Labels",
"tabVacation": "Vacation",
"tabPgp": "PGP Encryption"
}
}
-16
View File
@@ -399,21 +399,6 @@ export function MailPage() {
),
onClick: () => { /* sync trigger */ },
},
{
id: 'settings',
plugin: 'mail',
label: 'Einstellungen',
group: 'tools',
icon: (
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
),
onClick: () => {
window.location.href = '/settings/mail';
},
},
]);
return () => unregisterPlugin('mail');
}, [selectedMailId, selectedMailFlagged, handleSearch, handleCompose, handleReply, handleForward, handleToggleFlag, handleCreateEvent]);
@@ -461,7 +446,6 @@ export function MailPage() {
data-testid="mail-folder-pane"
>
<div className="p-3">
<h3 className="text-xs font-semibold text-secondary-500 uppercase mb-2">{t('mail.folders')}</h3>
<MailFolderTree
accounts={accounts}
folders={folders}
+28 -5
View File
@@ -18,6 +18,7 @@ import { PgpSettings } from '@/components/mail/PgpSettings';
import {
fetchAccounts,
createAccount,
deleteAccount,
testConnection,
triggerSync,
type MailAccount,
@@ -113,6 +114,17 @@ export function MailSettingsPage() {
}
}, [toast, t]);
const handleDeleteAccount = useCallback(async (accountId: string) => {
if (!window.confirm(t('mail.confirmDeleteAccount'))) return;
try {
await deleteAccount(accountId);
setAccounts((prev) => prev.filter((a) => a.id !== accountId));
toast.success(t('mail.accountDeleted'));
} catch (err) {
toast.error(err instanceof Error ? err.message : String(err));
}
}, [toast, t]);
const activeTab = searchParams.get('tab') || 'accounts';
const tabs = [
@@ -201,15 +213,18 @@ export function MailSettingsPage() {
{accounts.map((acc) => (
<Card key={acc.id}>
<div className="flex items-center justify-between">
<div>
<p className="font-medium text-secondary-900">{acc.display_name}</p>
<p className="text-sm text-secondary-500">{acc.email}</p>
<div className="min-w-0 flex-1">
<p className="font-medium text-secondary-900">{acc.display_name || acc.email}</p>
<p className="text-sm text-secondary-500 truncate">{acc.email}</p>
<p className="text-xs text-secondary-400 mt-1">
{t('mail.serverInfo')}: {acc.imap_host}:{acc.imap_port} / {acc.smtp_host}:{acc.smtp_port}
</p>
<p className="text-xs text-secondary-400">
{acc.is_shared ? t('mail.shared') : t('mail.personal')} ·
{acc.is_shared ? t('mail.shared') : t('mail.personal')} ·
{acc.is_active ? t('common.active') : t('common.inactive')}
</p>
</div>
<div className="flex gap-2">
<div className="flex gap-2 flex-shrink-0">
<Button
variant="secondary"
size="sm"
@@ -228,6 +243,14 @@ export function MailSettingsPage() {
>
{t('mail.syncNow')}
</Button>
<Button
variant="danger"
size="sm"
onClick={() => handleDeleteAccount(acc.id)}
data-testid={`delete-account-${acc.id}`}
>
{t('mail.deleteAccount')}
</Button>
</div>
</div>
</Card>