E-Mail-Client: Spam-Ordner als eigener Tab

Neuer Tab "Spam" (zwischen Gesendet und Papierkorb), zeigt den Junk-/
Spam-Ordner des gewählten Postfachs – damit fälschlich als Spam
einsortierte Mails auffindbar sind.

Backend:
- EmailFolder-Enum um SPAM erweitert (Migration, Wert angehängt →
  kein Rewrite bestehender Zeilen).
- imapService.findJunkFolderPath: ermittelt den Junk-Ordner per
  Special-Use-Flag \Junk + üblicher Namensliste (Junk/Spam/…).
- syncAllFoldersForAccount synct den Junk-Ordner zusätzlich als
  dbFolder=SPAM (syncEmailsForAccount bekommt dbFolder-Option).
- getCachedEmails + getFolderCountsForAccount um SPAM erweitert.
- Papierkorb-Move/Restore für Spam-Mails nutzt den echten Junk-Pfad
  als Quell-/Zielordner.

Frontend: Tab + Badge (ungelesen/gesamt), gleicher List-/Detail-Pfad
wie INBOX; Zuordnen-zu-Vertrag auch aus Spam möglich.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-07-27 15:52:24 +02:00
co-authored by Claude Opus 4.7
parent 8eb3790e7b
commit dfb4dadae1
8 changed files with 194 additions and 23 deletions
@@ -1,5 +1,5 @@
import { useState, useEffect, useMemo } from 'react';
import { RefreshCw, Plus, Mail, Inbox, Send, Trash2, Search, SlidersHorizontal, X } from 'lucide-react';
import { RefreshCw, Plus, Mail, Inbox, Send, Trash2, Search, SlidersHorizontal, X, ShieldAlert } from 'lucide-react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import toast from 'react-hot-toast';
import { cachedEmailApi, stressfreiEmailApi, CachedEmail, EmailFilterParams } from '../../services/api';
@@ -13,7 +13,7 @@ import ComposeEmailModal from './ComposeEmailModal';
import AssignToContractModal from './AssignToContractModal';
import TrashEmailList from './TrashEmailList';
type EmailFolder = 'INBOX' | 'SENT' | 'TRASH';
type EmailFolder = 'INBOX' | 'SENT' | 'SPAM' | 'TRASH';
interface EmailClientTabProps {
customerId: number;
@@ -117,7 +117,7 @@ export default function EmailClientTab({ customerId }: EmailClientTabProps) {
queryFn: () =>
cachedEmailApi.getForCustomer(customerId, {
accountId: selectedAccountId || undefined,
folder: selectedFolder as 'INBOX' | 'SENT',
folder: selectedFolder as 'INBOX' | 'SENT' | 'SPAM',
...filterParams,
}),
enabled: !!selectedAccountId && selectedFolder !== 'TRASH',
@@ -152,6 +152,8 @@ export default function EmailClientTab({ customerId }: EmailClientTabProps) {
inboxUnread: 0,
sent: 0,
sentUnread: 0,
spam: 0,
spamUnread: 0,
trash: 0,
trashUnread: 0,
};
@@ -396,6 +398,31 @@ export default function EmailClientTab({ customerId }: EmailClientTabProps) {
</span>
)}
</button>
<button
onClick={() => handleFolderChange('SPAM')}
className={`flex items-center gap-1.5 px-3 py-1.5 text-sm rounded-md transition-colors ${
selectedFolder === 'SPAM'
? 'bg-white text-amber-600 shadow-sm font-medium'
: 'text-gray-600 hover:text-gray-900'
}`}
>
<ShieldAlert className="w-4 h-4" />
Spam
{folderCounts.spam > 0 && (
<span
className={`ml-1 px-1.5 py-0.5 text-xs rounded-full cursor-help ${
folderCounts.spamUnread > 0
? 'bg-amber-100 text-amber-600 font-medium'
: 'bg-gray-100 text-gray-500'
}`}
title={`${folderCounts.spamUnread} ungelesen / ${folderCounts.spam} gesamt`}
>
{folderCounts.spamUnread > 0
? `${folderCounts.spamUnread}/${folderCounts.spam}`
: folderCounts.spam}
</span>
)}
</button>
{canAccessTrash && (
<button
onClick={() => handleFolderChange('TRASH')}
@@ -626,7 +653,7 @@ export default function EmailClientTab({ customerId }: EmailClientTabProps) {
queryClient.invalidateQueries({ queryKey: ['folder-counts', selectedAccountId] });
}}
isLoading={emailsLoading}
folder={selectedFolder as 'INBOX' | 'SENT'}
folder={selectedFolder as 'INBOX' | 'SENT' | 'SPAM'}
accountId={selectedAccountId}
/>
)}
+3 -3
View File
@@ -12,7 +12,7 @@ interface EmailListProps {
onSelectEmail: (email: CachedEmail) => void;
onEmailDeleted?: (emailId: number) => void;
isLoading?: boolean;
folder?: 'INBOX' | 'SENT';
folder?: 'INBOX' | 'SENT' | 'SPAM';
accountId?: number | null; // Für Folder-Count-Aktualisierung bei Lesen/Ungelesen
}
@@ -250,8 +250,8 @@ export default function EmailList({
<span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800">
{email.contract.contractNumber}
</span>
{/* X-Button nur für INBOX oder manuell zugeordnete gesendete E-Mails */}
{(folder === 'INBOX' || (folder === 'SENT' && !email.isAutoAssigned)) && (
{/* X-Button nur für empfangene (INBOX/SPAM) oder manuell zugeordnete gesendete E-Mails */}
{(folder === 'INBOX' || folder === 'SPAM' || (folder === 'SENT' && !email.isAutoAssigned)) && (
<button
onClick={(e) => handleUnassign(e, email.id)}
className="p-0.5 text-gray-400 hover:text-red-600 hover:bg-red-50 rounded"
+3 -1
View File
@@ -585,6 +585,8 @@ export const stressfreiEmailApi = {
inboxUnread: number;
sent: number;
sentUnread: number;
spam: number;
spamUnread: number;
trash: number;
trashUnread: number;
}>>(`/stressfrei-emails/${id}/folder-counts`);
@@ -595,7 +597,7 @@ export const stressfreiEmailApi = {
// Cached Email API (E-Mail-Client)
export interface EmailFilterParams {
accountId?: number;
folder?: 'INBOX' | 'SENT';
folder?: 'INBOX' | 'SENT' | 'SPAM';
limit?: number;
offset?: number;
// Suche / Filter (alle AND-verknüpft)