E-Mail-Zugang Test (IMAP + SMTP) in Provider-Einstellungen

Das bestehende „Verbindung testen" prüft nur den API-Zugang (Plesk/cPanel),
nicht den eigentlichen IMAP/SMTP-Zugang der System-E-Mail. Das führte dazu,
dass Anhang-Downloads scheiterten obwohl der API-Test grün war.

Neuer Button im EmailProviders-Modal: „E-Mail-Zugang testen (IMAP + SMTP)"
- Testet IMAP-Empfang und SMTP-Versand separat
- Zeigt pro Protokoll Erfolg oder Fehlermeldung mit Server/Port/Verschlüsselung
- Nutzt die hinterlegte System-E-Mail-Adresse + Passwort
- Funktioniert auch vor dem ersten Speichern (mit Formulardaten)

Außerdem im Anhang-Download:
- Retry-Mechanismus bei transienten TLS/Netzwerk-Fehlern (3 Versuche)
- Socket-Timeout 30s gegen hängende Verbindungen
- Sprechende Fehlermeldungen (z.B. Hinweis auf selbstsigniertes Zertifikat)
- Debug-Logging mit Host/Port/User/Folder/UID

Backend:
- Neuer Endpoint POST /api/email-providers/test-mail-access
- fetchAttachment in imapService: Retry-Wrapper + fetchAttachmentInner
- Besseres Error-Handling in downloadAttachment (Cert-Hinweis, Auth, Timeout)

Frontend:
- emailProviderApi.testMailAccess()
- EmailProviders-Modal: neuer Button + zweispaltige Ergebnis-Anzeige für IMAP+SMTP

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-23 14:59:06 +02:00
parent 109f774d62
commit fd55f3129f
6 changed files with 413 additions and 20 deletions
@@ -80,6 +80,13 @@ export default function EmailProviders() {
// Test-Status pro Provider in der Liste
const [providerTestResults, setProviderTestResults] = useState<Record<number, TestResult | null>>({});
const [testingProviderId, setTestingProviderId] = useState<number | null>(null);
// E-Mail-Zugang-Test
const [isTestingMailAccess, setIsTestingMailAccess] = useState(false);
const [mailAccessResult, setMailAccessResult] = useState<{
imap: { success: boolean; error?: string; server: string; port: number; encryption: string };
smtp: { success: boolean; error?: string; server: string; port: number; encryption: string };
user: string;
} | null>(null);
const { data: configsData, isLoading } = useQuery({
queryKey: ['email-provider-configs'],
@@ -151,6 +158,7 @@ export default function EmailProviders() {
setFormData(emptyForm);
setShowPassword(false);
setModalTestResult(null);
setMailAccessResult(null);
};
// Test für einen gespeicherten Provider in der Liste
@@ -216,6 +224,57 @@ export default function EmailProviders() {
}
};
// IMAP + SMTP-Zugang der System-E-Mail testen
const handleTestMailAccess = async () => {
if (!formData.systemEmailAddress) {
setMailAccessResult({
imap: { success: false, error: 'System-E-Mail-Adresse fehlt', server: '', port: 0, encryption: '' },
smtp: { success: false, error: 'System-E-Mail-Adresse fehlt', server: '', port: 0, encryption: '' },
user: '',
});
return;
}
setIsTestingMailAccess(true);
setMailAccessResult(null);
try {
const body: Parameters<typeof emailProviderApi.testMailAccess>[0] = editingId
? {
id: editingId,
systemEmailAddress: formData.systemEmailAddress,
systemEmailPassword: formData.systemEmailPassword || undefined,
}
: {
apiUrl: formData.apiUrl,
domain: formData.domain,
systemEmailAddress: formData.systemEmailAddress,
systemEmailPassword: formData.systemEmailPassword,
imapEncryption: formData.imapEncryption,
smtpEncryption: formData.smtpEncryption,
allowSelfSignedCerts: formData.allowSelfSignedCerts,
};
const result = await emailProviderApi.testMailAccess(body);
if (result.data) {
setMailAccessResult(result.data);
}
} catch (error) {
setMailAccessResult({
imap: {
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Test',
server: '',
port: 0,
encryption: '',
},
smtp: { success: false, server: '', port: 0, encryption: '' },
user: '',
});
} finally {
setIsTestingMailAccess(false);
}
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
@@ -659,6 +718,83 @@ export default function EmailProviders() {
)}
</div>
)}
{/* E-Mail-Zugang testen (IMAP + SMTP) */}
<div className="mt-4">
<p className="text-xs text-gray-500 mb-2">
Testet den tatsächlichen E-Mail-Zugang (IMAP-Empfang und SMTP-Versand) der System-E-Mail.
</p>
<Button
type="button"
variant="secondary"
onClick={handleTestMailAccess}
disabled={isTestingMailAccess || !formData.systemEmailAddress}
className="w-full"
>
{isTestingMailAccess ? (
'Teste IMAP + SMTP...'
) : (
<>
<Mail className="w-4 h-4 mr-2" />
E-Mail-Zugang testen (IMAP + SMTP)
</>
)}
</Button>
{mailAccessResult && (
<div className="mt-2 space-y-2">
{/* IMAP-Ergebnis */}
<div className={`p-3 rounded-lg text-sm ${mailAccessResult.imap.success ? 'bg-green-50 text-green-800' : 'bg-red-50 text-red-800'}`}>
{mailAccessResult.imap.success ? (
<div className="flex items-center gap-2">
<Check className="w-4 h-4 flex-shrink-0" />
<span>
<strong>IMAP</strong> erfolgreich ({mailAccessResult.imap.server}:{mailAccessResult.imap.port}, {mailAccessResult.imap.encryption})
</span>
</div>
) : (
<div className="flex items-start gap-2">
<WifiOff className="w-4 h-4 flex-shrink-0 mt-0.5" />
<div>
<strong>IMAP</strong> fehlgeschlagen
{mailAccessResult.imap.server && (
<span className="text-xs opacity-75"> ({mailAccessResult.imap.server}:{mailAccessResult.imap.port})</span>
)}
{mailAccessResult.imap.error && (
<div className="mt-1 text-xs">{mailAccessResult.imap.error}</div>
)}
</div>
</div>
)}
</div>
{/* SMTP-Ergebnis */}
<div className={`p-3 rounded-lg text-sm ${mailAccessResult.smtp.success ? 'bg-green-50 text-green-800' : 'bg-red-50 text-red-800'}`}>
{mailAccessResult.smtp.success ? (
<div className="flex items-center gap-2">
<Check className="w-4 h-4 flex-shrink-0" />
<span>
<strong>SMTP</strong> erfolgreich ({mailAccessResult.smtp.server}:{mailAccessResult.smtp.port}, {mailAccessResult.smtp.encryption})
</span>
</div>
) : (
<div className="flex items-start gap-2">
<WifiOff className="w-4 h-4 flex-shrink-0 mt-0.5" />
<div>
<strong>SMTP</strong> fehlgeschlagen
{mailAccessResult.smtp.server && (
<span className="text-xs opacity-75"> ({mailAccessResult.smtp.server}:{mailAccessResult.smtp.port})</span>
)}
{mailAccessResult.smtp.error && (
<div className="mt-1 text-xs">{mailAccessResult.smtp.error}</div>
)}
</div>
</div>
)}
</div>
</div>
)}
</div>
</div>
<div className="flex justify-end gap-3 pt-4 border-t">
+17
View File
@@ -1254,6 +1254,23 @@ export const emailProviderApi = {
const res = await api.post<ApiResponse<EmailOperationResult>>('/email-providers/test-connection', body);
return res.data;
},
testMailAccess: async (body: {
id?: number;
apiUrl?: string;
domain?: string;
systemEmailAddress?: string;
systemEmailPassword?: string;
imapEncryption?: 'SSL' | 'STARTTLS' | 'NONE';
smtpEncryption?: 'SSL' | 'STARTTLS' | 'NONE';
allowSelfSignedCerts?: boolean;
}) => {
const res = await api.post<ApiResponse<{
imap: { success: boolean; error?: string; server: string; port: number; encryption: string };
smtp: { success: boolean; error?: string; server: string; port: number; encryption: string };
user: string;
}>>('/email-providers/test-mail-access', body);
return res.data;
},
getDomain: async () => {
const res = await api.get<ApiResponse<{ domain: string | null }>>('/email-providers/domain');
return res.data;