/** * Mail settings page — signatures, rules, labels, PGP, vacation responder. */ import React, { useState, useCallback, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { useSearchParams } from 'react-router-dom'; import { Card } from '@/components/ui/Card'; import { Button } from '@/components/ui/Button'; import { Input } from '@/components/ui/Input'; import { useToast } from '@/components/ui/Toast'; import { Tabs } from '@/components/shared/Tabs'; import { SignatureManager } from '@/components/mail/SignatureManager'; import { RuleEditor } from '@/components/mail/RuleEditor'; import { LabelManager } from '@/components/mail/LabelManager'; import { VacationResponder } from '@/components/mail/VacationResponder'; import { PgpSettings } from '@/components/mail/PgpSettings'; import { fetchAccounts, createAccount, deleteAccount, testConnection, triggerSync, updateAccount, fetchFolders, type MailAccount, type MailFolder, type CreateAccountPayload, } from '@/api/mail'; export function MailSettingsPage() { const { t } = useTranslation(); const toast = useToast(); const [searchParams, setSearchParams] = useSearchParams(); const [accounts, setAccounts] = useState([]); const [loading, setLoading] = useState(true); const [selectedAccountId, setSelectedAccountId] = useState(''); const [showAddAccount, setShowAddAccount] = useState(false); const [newAccount, setNewAccount] = useState({ email: '', display_name: '', imap_host: '', imap_port: 993, smtp_host: '', smtp_port: 587, username: '', password: '', is_shared: false, }); const [saving, setSaving] = useState(false); const [testing, setTesting] = useState(null); const [syncing, setSyncing] = useState(null); const [editingDisplayName, setEditingDisplayName] = useState(null); const [displayNameValue, setDisplayNameValue] = useState(''); const [accountFolders, setAccountFolders] = useState>({}); const [folderMappingEdit, setFolderMappingEdit] = useState(null); const [folderMappingValues, setFolderMappingValues] = useState>({}); const [savingMapping, setSavingMapping] = useState(false); const loadAccounts = useCallback(async () => { setLoading(true); try { const accs = await fetchAccounts(); setAccounts(accs); if (accs.length > 0) { setSelectedAccountId((prev) => prev || accs[0].id); } } catch (err: any) { toast.error(err?.message || err?.detail || 'Failed to load accounts'); } setLoading(false); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); useEffect(() => { loadAccounts(); }, [loadAccounts]); const handleCreateAccount = useCallback(async () => { if (!newAccount.email.trim() || !newAccount.password.trim()) return; setSaving(true); try { const acc = await createAccount(newAccount); setAccounts((prev) => [...prev, acc]); toast.success(t('mail.accountCreated')); setShowAddAccount(false); setNewAccount({ email: '', display_name: '', imap_host: '', imap_port: 993, smtp_host: '', smtp_port: 587, username: '', password: '', is_shared: false, }); } catch (err: any) { toast.error(err?.message || err?.detail || 'Save failed'); } finally { setSaving(false); } }, [newAccount, toast, t]); const handleTestConnection = useCallback(async (accountId: string) => { setTesting(accountId); try { const result = await testConnection(accountId); if (result.success) { toast.success(t('mail.connectionSuccess')); } else { toast.error(result.message); } } catch (err: any) { toast.error(err?.message || err?.detail || 'Connection test failed'); } finally { setTesting(null); } }, [toast, t]); const handleSync = useCallback(async (accountId: string) => { setSyncing(accountId); try { const result = await triggerSync(accountId); if ((result as any)?.error) { toast.error((result as any).error); } else { toast.success(t('mail.syncSuccess')); } } catch (err: any) { toast.error(err?.message || err?.detail || 'Sync failed'); } finally { setSyncing(null); } }, [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: any) { toast.error(err?.message || err?.detail || 'Delete failed'); } }, [toast, t]); const handleSaveDisplayName = useCallback(async (accountId: string) => { try { const updated = await updateAccount(accountId, { display_name: displayNameValue }); setAccounts((prev) => prev.map((a) => (a.id === accountId ? updated : a))); setEditingDisplayName(null); toast.success(t('common.saved')); } catch (err: any) { toast.error(err?.message || err?.detail || 'Save failed'); } }, [displayNameValue, toast, t]); const handleStartEditDisplayName = useCallback((acc: MailAccount) => { setEditingDisplayName(acc.id); setDisplayNameValue(acc.display_name || ''); }, []); const loadAccountFolders = useCallback(async (accountId: string) => { try { const folders = await fetchFolders(accountId); setAccountFolders((prev) => ({ ...prev, [accountId]: folders })); } catch { // non-critical } }, []); const handleStartFolderMapping = useCallback((acc: MailAccount) => { setFolderMappingEdit(acc.id); setFolderMappingValues({ sent: acc.sent_folder_imap_name || '', drafts: acc.drafts_folder_imap_name || '', spam: acc.spam_folder_imap_name || '', trash: acc.trash_folder_imap_name || '', }); if (!accountFolders[acc.id]) { loadAccountFolders(acc.id); } }, [accountFolders, loadAccountFolders]); const handleSaveFolderMapping = useCallback(async (accountId: string) => { setSavingMapping(true); try { const payload: Record = {}; payload.sent_folder_imap_name = folderMappingValues.sent || null; payload.drafts_folder_imap_name = folderMappingValues.drafts || null; payload.spam_folder_imap_name = folderMappingValues.spam || null; payload.trash_folder_imap_name = folderMappingValues.trash || null; const updated = await updateAccount(accountId, payload); setAccounts((prev) => prev.map((a) => (a.id === accountId ? updated : a))); setFolderMappingEdit(null); toast.success(t('common.saved')); } catch (err: any) { toast.error(err?.message || err?.detail || 'Save failed'); } finally { setSavingMapping(false); } }, [folderMappingValues, toast, t]); const activeTab = searchParams.get('tab') || 'accounts'; const tabs = [ { key: 'accounts', label: t('mail.tabAccounts'), content: (

{t('mail.accounts')}

{showAddAccount && (
setNewAccount((prev) => ({ ...prev, email: e.target.value }))} placeholder="user@example.com" required /> setNewAccount((prev) => ({ ...prev, display_name: e.target.value }))} placeholder="John Doe" /> setNewAccount((prev) => ({ ...prev, username: e.target.value }))} placeholder="Leer lassen für E-Mail-Adresse" />
setNewAccount((prev) => ({ ...prev, imap_host: e.target.value }))} placeholder="imap.example.com" /> setNewAccount((prev) => ({ ...prev, imap_port: Number(e.target.value) }))} />
setNewAccount((prev) => ({ ...prev, smtp_host: e.target.value }))} placeholder="smtp.example.com" /> setNewAccount((prev) => ({ ...prev, smtp_port: Number(e.target.value) }))} />
setNewAccount((prev) => ({ ...prev, password: e.target.value }))} required />
)} {loading ? (
) : accounts.length === 0 ? (

{t('mail.noAccounts')}

) : (
{accounts.map((acc) => (
{editingDisplayName === acc.id ? (
setDisplayNameValue(e.target.value)} placeholder={acc.email} className="flex-1" />
) : (

{acc.display_name || acc.email}

)}

{acc.email}

{t('mail.serverInfo')}: {acc.imap_host}:{acc.imap_port} / {acc.smtp_host}:{acc.smtp_port}

{acc.is_shared ? t('mail.shared') : t('mail.personal')} · {acc.is_active ? t('common.active') : t('common.inactive')}

{/* Folder mapping section */} {folderMappingEdit === acc.id ? (

Ordner-Zuordnung

{(['sent', 'drafts', 'spam', 'trash'] as const).map((type) => (
))}
) : (
)}
))}
)}
), }, { key: 'signatures', label: t('mail.tabSignatures'), content: , }, { key: 'rules', label: t('mail.tabRules'), content: selectedAccountId ? : (

{t('mail.selectAccountFirst')}

), }, { key: 'labels', label: t('mail.tabLabels'), content: , }, { key: 'vacation', label: t('mail.tabVacation'), content: , }, { key: 'pgp', label: t('mail.tabPgp'), content: , }, ]; return (

{t('mail.settings')}

); }