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:
parent
109f774d62
commit
fd55f3129f
|
|
@ -508,9 +508,24 @@ export async function downloadAttachment(req: Request, res: Response): Promise<v
|
||||||
res.send(attachment.content);
|
res.send(attachment.content);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('downloadAttachment error:', error);
|
console.error('downloadAttachment error:', error);
|
||||||
|
const rawMsg = error instanceof Error ? error.message : 'Unbekannter Fehler';
|
||||||
|
const lower = rawMsg.toLowerCase();
|
||||||
|
|
||||||
|
let friendly = rawMsg;
|
||||||
|
if (lower.includes('socket disconnected') && lower.includes('tls')) {
|
||||||
|
friendly =
|
||||||
|
'IMAP-Server hat die TLS-Verbindung abgelehnt. Mögliche Ursache: selbstsigniertes Zertifikat. Bitte in den E-Mail-Provider-Einstellungen "Selbstsignierte Zertifikate erlauben" aktivieren.';
|
||||||
|
} else if (lower.includes('econnrefused')) {
|
||||||
|
friendly = 'IMAP-Server ist nicht erreichbar (Verbindung verweigert). Bitte Server/Port prüfen.';
|
||||||
|
} else if (lower.includes('etimedout')) {
|
||||||
|
friendly = 'Zeitüberschreitung beim Verbinden zum IMAP-Server. Bitte später erneut versuchen.';
|
||||||
|
} else if (lower.includes('authentication') || lower.includes('auth')) {
|
||||||
|
friendly = 'IMAP-Authentifizierung fehlgeschlagen. Bitte Zugangsdaten prüfen.';
|
||||||
|
}
|
||||||
|
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Fehler beim Herunterladen des Anhangs',
|
error: `Fehler beim Herunterladen des Anhangs: ${friendly}`,
|
||||||
} as ApiResponse);
|
} as ApiResponse);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,12 @@ import { Request, Response } from 'express';
|
||||||
import * as emailProviderService from '../services/emailProvider/emailProviderService.js';
|
import * as emailProviderService from '../services/emailProvider/emailProviderService.js';
|
||||||
import { logChange } from '../services/audit.service.js';
|
import { logChange } from '../services/audit.service.js';
|
||||||
import { ApiResponse } from '../types/index.js';
|
import { ApiResponse } from '../types/index.js';
|
||||||
|
import { testImapConnection, ImapCredentials } from '../services/imapService.js';
|
||||||
|
import { testSmtpConnection, SmtpCredentials } from '../services/smtpService.js';
|
||||||
|
import { decrypt } from '../utils/encryption.js';
|
||||||
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
// ==================== CONFIG CRUD ====================
|
// ==================== CONFIG CRUD ====================
|
||||||
|
|
||||||
|
|
@ -122,6 +128,156 @@ export async function testConnection(req: Request, res: Response): Promise<void>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Testet IMAP + SMTP-Zugang für die System-E-Mail eines Providers.
|
||||||
|
* - Option A: Provider-ID + optional überschreibendes Passwort aus Body (Modal)
|
||||||
|
* - Option B: Testdaten komplett aus Body (beim Anlegen, noch nicht gespeichert)
|
||||||
|
*/
|
||||||
|
export async function testMailAccess(req: Request, res: Response): Promise<void> {
|
||||||
|
try {
|
||||||
|
const id = req.body?.id ? parseInt(req.body.id) : undefined;
|
||||||
|
const bodyEmail = typeof req.body?.systemEmailAddress === 'string' ? req.body.systemEmailAddress : undefined;
|
||||||
|
const bodyPassword = typeof req.body?.systemEmailPassword === 'string' ? req.body.systemEmailPassword : undefined;
|
||||||
|
|
||||||
|
let emailAddress: string | undefined;
|
||||||
|
let password: string | undefined;
|
||||||
|
let smtpServer: string;
|
||||||
|
let smtpPort: number;
|
||||||
|
let imapServer: string;
|
||||||
|
let imapPort: number;
|
||||||
|
let smtpEncryption: 'SSL' | 'STARTTLS' | 'NONE';
|
||||||
|
let imapEncryption: 'SSL' | 'STARTTLS' | 'NONE';
|
||||||
|
let allowSelfSignedCerts: boolean;
|
||||||
|
|
||||||
|
if (id) {
|
||||||
|
// Gespeicherten Provider laden
|
||||||
|
const config = await prisma.emailProviderConfig.findUnique({ where: { id } });
|
||||||
|
if (!config) {
|
||||||
|
res.status(404).json({ success: false, error: 'Provider nicht gefunden' } as ApiResponse);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
emailAddress = bodyEmail || config.systemEmailAddress || undefined;
|
||||||
|
if (bodyPassword) {
|
||||||
|
password = bodyPassword;
|
||||||
|
} else if (config.systemEmailPasswordEncrypted) {
|
||||||
|
try {
|
||||||
|
password = decrypt(config.systemEmailPasswordEncrypted);
|
||||||
|
} catch {
|
||||||
|
password = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IMAP/SMTP-Settings vom Provider ableiten
|
||||||
|
const settings = await emailProviderService.getImapSmtpSettings();
|
||||||
|
if (!settings) {
|
||||||
|
res.status(400).json({ success: false, error: 'Keine IMAP/SMTP-Einstellungen verfügbar' } as ApiResponse);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
smtpServer = settings.smtpServer;
|
||||||
|
smtpPort = settings.smtpPort;
|
||||||
|
imapServer = settings.imapServer;
|
||||||
|
imapPort = settings.imapPort;
|
||||||
|
smtpEncryption = settings.smtpEncryption;
|
||||||
|
imapEncryption = settings.imapEncryption;
|
||||||
|
allowSelfSignedCerts = settings.allowSelfSignedCerts;
|
||||||
|
} else if (req.body?.apiUrl) {
|
||||||
|
// Formulardaten ohne gespeicherten Provider
|
||||||
|
emailAddress = bodyEmail;
|
||||||
|
password = bodyPassword;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const url = new URL(req.body.apiUrl);
|
||||||
|
smtpServer = url.hostname;
|
||||||
|
imapServer = url.hostname;
|
||||||
|
} catch {
|
||||||
|
smtpServer = `mail.${req.body.domain || ''}`;
|
||||||
|
imapServer = smtpServer;
|
||||||
|
}
|
||||||
|
|
||||||
|
imapEncryption = (req.body.imapEncryption || 'SSL') as 'SSL' | 'STARTTLS' | 'NONE';
|
||||||
|
smtpEncryption = (req.body.smtpEncryption || 'SSL') as 'SSL' | 'STARTTLS' | 'NONE';
|
||||||
|
allowSelfSignedCerts = !!req.body.allowSelfSignedCerts;
|
||||||
|
|
||||||
|
imapPort = imapEncryption === 'SSL' ? 993 : 143;
|
||||||
|
smtpPort = smtpEncryption === 'SSL' ? 465 : smtpEncryption === 'STARTTLS' ? 587 : 25;
|
||||||
|
} else {
|
||||||
|
res.status(400).json({ success: false, error: 'Provider-ID oder Testdaten erforderlich' } as ApiResponse);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!emailAddress || !password) {
|
||||||
|
res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: 'System-E-Mail-Adresse und Passwort sind erforderlich',
|
||||||
|
} as ApiResponse);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// IMAP testen
|
||||||
|
const imapCredentials: ImapCredentials = {
|
||||||
|
host: imapServer,
|
||||||
|
port: imapPort,
|
||||||
|
user: emailAddress,
|
||||||
|
password,
|
||||||
|
encryption: imapEncryption,
|
||||||
|
allowSelfSignedCerts,
|
||||||
|
};
|
||||||
|
|
||||||
|
// SMTP testen
|
||||||
|
const smtpCredentials: SmtpCredentials = {
|
||||||
|
host: smtpServer,
|
||||||
|
port: smtpPort,
|
||||||
|
user: emailAddress,
|
||||||
|
password,
|
||||||
|
encryption: smtpEncryption,
|
||||||
|
allowSelfSignedCerts,
|
||||||
|
};
|
||||||
|
|
||||||
|
let imapResult: { success: boolean; error?: string } = { success: false };
|
||||||
|
let smtpResult: { success: boolean; error?: string } = { success: false };
|
||||||
|
|
||||||
|
try {
|
||||||
|
await testImapConnection(imapCredentials);
|
||||||
|
imapResult = { success: true };
|
||||||
|
} catch (e) {
|
||||||
|
imapResult = { success: false, error: e instanceof Error ? e.message : 'Unbekannter Fehler' };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await testSmtpConnection(smtpCredentials);
|
||||||
|
smtpResult = { success: true };
|
||||||
|
} catch (e) {
|
||||||
|
smtpResult = { success: false, error: e instanceof Error ? e.message : 'Unbekannter Fehler' };
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: imapResult.success && smtpResult.success,
|
||||||
|
data: {
|
||||||
|
imap: {
|
||||||
|
...imapResult,
|
||||||
|
server: imapServer,
|
||||||
|
port: imapPort,
|
||||||
|
encryption: imapEncryption,
|
||||||
|
},
|
||||||
|
smtp: {
|
||||||
|
...smtpResult,
|
||||||
|
server: smtpServer,
|
||||||
|
port: smtpPort,
|
||||||
|
encryption: smtpEncryption,
|
||||||
|
},
|
||||||
|
user: emailAddress,
|
||||||
|
},
|
||||||
|
} as ApiResponse);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('testMailAccess error:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Fehler beim Test',
|
||||||
|
} as ApiResponse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function checkEmailExists(req: Request, res: Response): Promise<void> {
|
export async function checkEmailExists(req: Request, res: Response): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const { localPart } = req.params;
|
const { localPart } = req.params;
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ router.delete('/configs/:id', authenticate, requirePermission('settings:update')
|
||||||
|
|
||||||
// Email Operations
|
// Email Operations
|
||||||
router.post('/test-connection', authenticate, requirePermission('settings:update'), emailProviderController.testConnection);
|
router.post('/test-connection', authenticate, requirePermission('settings:update'), emailProviderController.testConnection);
|
||||||
|
router.post('/test-mail-access', authenticate, requirePermission('settings:update'), emailProviderController.testMailAccess);
|
||||||
router.get('/domain', authenticate, emailProviderController.getProviderDomain);
|
router.get('/domain', authenticate, emailProviderController.getProviderDomain);
|
||||||
router.get('/check/:localPart', authenticate, requirePermission('customers:read'), emailProviderController.checkEmailExists);
|
router.get('/check/:localPart', authenticate, requirePermission('customers:read'), emailProviderController.checkEmailExists);
|
||||||
router.post('/provision', authenticate, requirePermission('customers:update'), emailProviderController.provisionEmail);
|
router.post('/provision', authenticate, requirePermission('customers:update'), emailProviderController.provisionEmail);
|
||||||
|
|
|
||||||
|
|
@ -360,6 +360,41 @@ export async function fetchAttachment(
|
||||||
uid: number,
|
uid: number,
|
||||||
attachmentFilename: string,
|
attachmentFilename: string,
|
||||||
folder: string = 'INBOX'
|
folder: string = 'INBOX'
|
||||||
|
): Promise<EmailAttachmentData | null> {
|
||||||
|
// Bei transienten Netzwerkfehlern automatisch bis zu 2x retry
|
||||||
|
let lastError: unknown;
|
||||||
|
for (let attempt = 1; attempt <= 3; attempt++) {
|
||||||
|
try {
|
||||||
|
return await fetchAttachmentInner(credentials, uid, attachmentFilename, folder);
|
||||||
|
} catch (err) {
|
||||||
|
lastError = err;
|
||||||
|
const msg = err instanceof Error ? err.message.toLowerCase() : '';
|
||||||
|
const isTransient =
|
||||||
|
msg.includes('socket disconnected') ||
|
||||||
|
msg.includes('econnreset') ||
|
||||||
|
msg.includes('etimedout') ||
|
||||||
|
msg.includes('socket hang up') ||
|
||||||
|
msg.includes('network socket');
|
||||||
|
|
||||||
|
if (!isTransient || attempt === 3) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.warn(
|
||||||
|
`[fetchAttachment] Versuch ${attempt}/3 fehlgeschlagen (transient), retry in ${attempt * 500}ms:`,
|
||||||
|
msg,
|
||||||
|
);
|
||||||
|
await new Promise((r) => setTimeout(r, attempt * 500));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw lastError;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchAttachmentInner(
|
||||||
|
credentials: ImapCredentials,
|
||||||
|
uid: number,
|
||||||
|
attachmentFilename: string,
|
||||||
|
folder: string = 'INBOX'
|
||||||
): Promise<EmailAttachmentData | null> {
|
): Promise<EmailAttachmentData | null> {
|
||||||
// Verschlüsselungs-Einstellungen basierend auf Modus
|
// Verschlüsselungs-Einstellungen basierend auf Modus
|
||||||
const encryption = credentials.encryption ?? 'SSL';
|
const encryption = credentials.encryption ?? 'SSL';
|
||||||
|
|
@ -374,6 +409,8 @@ export async function fetchAttachment(
|
||||||
pass: credentials.password,
|
pass: credentials.password,
|
||||||
},
|
},
|
||||||
logger: false,
|
logger: false,
|
||||||
|
// Timeouts gegen hängende Verbindungen
|
||||||
|
socketTimeout: 30000,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (encryption !== 'NONE') {
|
if (encryption !== 'NONE') {
|
||||||
|
|
@ -382,37 +419,68 @@ export async function fetchAttachment(
|
||||||
|
|
||||||
const client = new ImapFlow(clientOptions);
|
const client = new ImapFlow(clientOptions);
|
||||||
|
|
||||||
|
console.log(`[fetchAttachment] Host: ${credentials.host}:${credentials.port} | User: ${credentials.user} | Folder: ${folder} | UID: ${uid} | File: ${attachmentFilename}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await client.connect();
|
await client.connect();
|
||||||
await client.mailboxOpen(folder);
|
|
||||||
|
// Ordner öffnen – bei Fehler alle verfügbaren Ordner listen für Debugging
|
||||||
|
try {
|
||||||
|
await client.mailboxOpen(folder);
|
||||||
|
} catch (folderErr) {
|
||||||
|
console.error(`[fetchAttachment] mailboxOpen('${folder}') failed:`, folderErr);
|
||||||
|
try {
|
||||||
|
const list = await client.list();
|
||||||
|
const available = list.map((m) => m.path).join(', ');
|
||||||
|
console.error(`[fetchAttachment] Verfügbare Ordner: ${available}`);
|
||||||
|
throw new Error(
|
||||||
|
`Ordner '${folder}' nicht gefunden. Verfügbar: ${available}`,
|
||||||
|
);
|
||||||
|
} catch (listErr) {
|
||||||
|
throw folderErr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// E-Mail per UID abrufen
|
// E-Mail per UID abrufen
|
||||||
let attachment: EmailAttachmentData | null = null;
|
let attachment: EmailAttachmentData | null = null;
|
||||||
|
let foundMessage = false;
|
||||||
|
|
||||||
// Drittes Argument { uid: true } sorgt dafür, dass UID FETCH statt FETCH verwendet wird
|
// Drittes Argument { uid: true } sorgt dafür, dass UID FETCH statt FETCH verwendet wird
|
||||||
for await (const message of client.fetch(uid.toString(), {
|
try {
|
||||||
source: true,
|
for await (const message of client.fetch(uid.toString(), {
|
||||||
}, { uid: true })) {
|
source: true,
|
||||||
if (!message.source) continue;
|
}, { uid: true })) {
|
||||||
|
foundMessage = true;
|
||||||
|
if (!message.source) continue;
|
||||||
|
|
||||||
// E-Mail parsen
|
// E-Mail parsen
|
||||||
const parsed = await simpleParser(message.source);
|
const parsed = await simpleParser(message.source);
|
||||||
|
|
||||||
// Anhang suchen
|
// Anhang suchen
|
||||||
if (parsed.attachments) {
|
if (parsed.attachments) {
|
||||||
for (const att of parsed.attachments) {
|
for (const att of parsed.attachments) {
|
||||||
const filename = att.filename || 'unnamed';
|
const filename = att.filename || 'unnamed';
|
||||||
if (filename === attachmentFilename) {
|
if (filename === attachmentFilename) {
|
||||||
attachment = {
|
attachment = {
|
||||||
filename,
|
filename,
|
||||||
content: att.content,
|
content: att.content,
|
||||||
contentType: att.contentType || 'application/octet-stream',
|
contentType: att.contentType || 'application/octet-stream',
|
||||||
size: att.size,
|
size: att.size,
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (fetchErr) {
|
||||||
|
console.error(`[fetchAttachment] fetch(UID ${uid}) failed:`, fetchErr);
|
||||||
|
throw new Error(
|
||||||
|
`Nachricht mit UID ${uid} konnte nicht geladen werden (${fetchErr instanceof Error ? fetchErr.message : 'unbekannter Fehler'}). Möglicherweise wurde sie im IMAP-Postfach verschoben oder gelöscht.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!foundMessage) {
|
||||||
|
console.warn(`[fetchAttachment] Keine Nachricht mit UID ${uid} in Ordner '${folder}' gefunden`);
|
||||||
}
|
}
|
||||||
|
|
||||||
await client.logout();
|
await client.logout();
|
||||||
|
|
|
||||||
|
|
@ -80,6 +80,13 @@ export default function EmailProviders() {
|
||||||
// Test-Status pro Provider in der Liste
|
// Test-Status pro Provider in der Liste
|
||||||
const [providerTestResults, setProviderTestResults] = useState<Record<number, TestResult | null>>({});
|
const [providerTestResults, setProviderTestResults] = useState<Record<number, TestResult | null>>({});
|
||||||
const [testingProviderId, setTestingProviderId] = useState<number | null>(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({
|
const { data: configsData, isLoading } = useQuery({
|
||||||
queryKey: ['email-provider-configs'],
|
queryKey: ['email-provider-configs'],
|
||||||
|
|
@ -151,6 +158,7 @@ export default function EmailProviders() {
|
||||||
setFormData(emptyForm);
|
setFormData(emptyForm);
|
||||||
setShowPassword(false);
|
setShowPassword(false);
|
||||||
setModalTestResult(null);
|
setModalTestResult(null);
|
||||||
|
setMailAccessResult(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Test für einen gespeicherten Provider in der Liste
|
// 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) => {
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
|
|
@ -659,6 +718,83 @@ export default function EmailProviders() {
|
||||||
)}
|
)}
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
<div className="flex justify-end gap-3 pt-4 border-t">
|
<div className="flex justify-end gap-3 pt-4 border-t">
|
||||||
|
|
|
||||||
|
|
@ -1254,6 +1254,23 @@ export const emailProviderApi = {
|
||||||
const res = await api.post<ApiResponse<EmailOperationResult>>('/email-providers/test-connection', body);
|
const res = await api.post<ApiResponse<EmailOperationResult>>('/email-providers/test-connection', body);
|
||||||
return res.data;
|
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 () => {
|
getDomain: async () => {
|
||||||
const res = await api.get<ApiResponse<{ domain: string | null }>>('/email-providers/domain');
|
const res = await api.get<ApiResponse<{ domain: string | null }>>('/email-providers/domain');
|
||||||
return res.data;
|
return res.data;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue