1290cdad10
Alle hardcoded Referenzen auf 'stressfrei-wechseln.de' und 'Stressfrei-Wechseln'
durch dynamische Werte aus der EmailProviderConfig ersetzt. Notwendig für
Multi-Mandanten-Betrieb, wenn das CRM an Dritte vermietet wird.
Schema:
- Neues Feld EmailProviderConfig.customerEmailLabel (String?)
- Wenn leer, wird Label aus Domain abgeleitet ('stressfrei-wechseln.de' → 'Stressfrei-Wechseln')
Backend:
- Neuer Endpoint GET /api/email-providers/public-settings liefert { domain, customerEmailLabel }
- Neue Service-Funktionen: getProviderPublicSettings(), deriveLabelFromDomain()
- create/updateProviderConfig erweitert um customerEmailLabel
Frontend:
- Neuer Hook useProviderSettings() mit Auto-Caching
- Neues Eingabefeld 'Bezeichnung für Kunden-E-Mails' im Provider-Modal
- Dynamische Domain-Suffix im Adress-Hinzufügen-Dialog (@<domain>)
- Tab-Label 'Stressfrei-Wechseln' im Kunden-Detail → dynamisch
- 'Stressfrei-Wechseln Adresse' in ContractForm → dynamisch
- '(Stressfrei-Wechseln)' Badge in ContractDetail → dynamisch
- 'Stressfrei-Wechseln E-Mail' im Generate-Modal → dynamisch
- Leere-Zustand-Meldungen in Tab und E-Mail-Client → dynamisch
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
357 lines
12 KiB
TypeScript
357 lines
12 KiB
TypeScript
// ==================== EMAIL PROVIDER CONTROLLER ====================
|
|
|
|
import { Request, Response } from 'express';
|
|
import * as emailProviderService from '../services/emailProvider/emailProviderService.js';
|
|
import { logChange } from '../services/audit.service.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 ====================
|
|
|
|
export async function getProviderConfigs(req: Request, res: Response): Promise<void> {
|
|
try {
|
|
const configs = await emailProviderService.getAllProviderConfigs();
|
|
res.json({ success: true, data: configs } as ApiResponse);
|
|
} catch (error) {
|
|
res.status(500).json({
|
|
success: false,
|
|
error: 'Fehler beim Laden der Email-Provider',
|
|
} as ApiResponse);
|
|
}
|
|
}
|
|
|
|
export async function getProviderConfig(req: Request, res: Response): Promise<void> {
|
|
try {
|
|
const id = parseInt(req.params.id);
|
|
const config = await emailProviderService.getProviderConfigById(id);
|
|
|
|
if (!config) {
|
|
res.status(404).json({
|
|
success: false,
|
|
error: 'Email-Provider nicht gefunden',
|
|
} as ApiResponse);
|
|
return;
|
|
}
|
|
|
|
res.json({ success: true, data: config } as ApiResponse);
|
|
} catch (error) {
|
|
res.status(500).json({
|
|
success: false,
|
|
error: 'Fehler beim Laden des Email-Providers',
|
|
} as ApiResponse);
|
|
}
|
|
}
|
|
|
|
export async function createProviderConfig(req: Request, res: Response): Promise<void> {
|
|
try {
|
|
const config = await emailProviderService.createProviderConfig(req.body);
|
|
await logChange({
|
|
req, action: 'CREATE', resourceType: 'EmailProviderConfig',
|
|
resourceId: config.id.toString(),
|
|
label: `E-Mail-Provider ${config.name} angelegt`,
|
|
});
|
|
res.status(201).json({ success: true, data: config } as ApiResponse);
|
|
} catch (error) {
|
|
res.status(400).json({
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Fehler beim Erstellen des Email-Providers',
|
|
} as ApiResponse);
|
|
}
|
|
}
|
|
|
|
export async function updateProviderConfig(req: Request, res: Response): Promise<void> {
|
|
try {
|
|
const id = parseInt(req.params.id);
|
|
const config = await emailProviderService.updateProviderConfig(id, req.body);
|
|
await logChange({
|
|
req, action: 'UPDATE', resourceType: 'EmailProviderConfig',
|
|
resourceId: id.toString(),
|
|
label: `E-Mail-Provider ${config.name} aktualisiert`,
|
|
});
|
|
res.json({ success: true, data: config } as ApiResponse);
|
|
} catch (error) {
|
|
res.status(400).json({
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Fehler beim Aktualisieren des Email-Providers',
|
|
} as ApiResponse);
|
|
}
|
|
}
|
|
|
|
export async function deleteProviderConfig(req: Request, res: Response): Promise<void> {
|
|
try {
|
|
const id = parseInt(req.params.id);
|
|
const config = await emailProviderService.getProviderConfigById(id);
|
|
await emailProviderService.deleteProviderConfig(id);
|
|
await logChange({
|
|
req, action: 'DELETE', resourceType: 'EmailProviderConfig',
|
|
resourceId: id.toString(),
|
|
label: `E-Mail-Provider ${config?.name || id} gelöscht`,
|
|
});
|
|
res.json({ success: true, message: 'Email-Provider gelöscht' } as ApiResponse);
|
|
} catch (error) {
|
|
res.status(400).json({
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Fehler beim Löschen des Email-Providers',
|
|
} as ApiResponse);
|
|
}
|
|
}
|
|
|
|
// ==================== EMAIL OPERATIONS ====================
|
|
|
|
export async function testConnection(req: Request, res: Response): Promise<void> {
|
|
try {
|
|
// Option 1: Provider-ID für gespeicherten Provider
|
|
const id = req.body?.id ? parseInt(req.body.id) : undefined;
|
|
|
|
// Option 2: Testdaten aus Body (für Test im Modal mit ungespeicherten Daten)
|
|
const testData = req.body && req.body.type ? {
|
|
type: req.body.type as 'PLESK' | 'CPANEL' | 'DIRECTADMIN',
|
|
apiUrl: req.body.apiUrl,
|
|
apiKey: req.body.apiKey || undefined,
|
|
username: req.body.username || undefined,
|
|
password: req.body.password || undefined,
|
|
domain: req.body.domain,
|
|
} : undefined;
|
|
|
|
const result = await emailProviderService.testProviderConnection({ id, testData });
|
|
res.json({ success: result.success, data: result } as ApiResponse);
|
|
} catch (error) {
|
|
res.status(500).json({
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Verbindungstest fehlgeschlagen',
|
|
} as ApiResponse);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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> {
|
|
try {
|
|
const { localPart } = req.params;
|
|
const result = await emailProviderService.checkEmailExists(localPart);
|
|
res.json({ success: true, data: result } as ApiResponse);
|
|
} catch (error) {
|
|
res.status(500).json({
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Fehler bei der E-Mail-Prüfung',
|
|
} as ApiResponse);
|
|
}
|
|
}
|
|
|
|
export async function provisionEmail(req: Request, res: Response): Promise<void> {
|
|
try {
|
|
const { localPart, customerEmail } = req.body;
|
|
|
|
if (!localPart || !customerEmail) {
|
|
res.status(400).json({
|
|
success: false,
|
|
error: 'localPart und customerEmail sind erforderlich',
|
|
} as ApiResponse);
|
|
return;
|
|
}
|
|
|
|
const result = await emailProviderService.provisionEmail(localPart, customerEmail);
|
|
res.json({ success: result.success, data: result } as ApiResponse);
|
|
} catch (error) {
|
|
res.status(500).json({
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Fehler bei der E-Mail-Provisionierung',
|
|
} as ApiResponse);
|
|
}
|
|
}
|
|
|
|
export async function deprovisionEmail(req: Request, res: Response): Promise<void> {
|
|
try {
|
|
const { localPart } = req.params;
|
|
const result = await emailProviderService.deprovisionEmail(localPart);
|
|
res.json({ success: result.success, data: result } as ApiResponse);
|
|
} catch (error) {
|
|
res.status(500).json({
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Fehler beim Löschen der E-Mail',
|
|
} as ApiResponse);
|
|
}
|
|
}
|
|
|
|
export async function getProviderDomain(req: Request, res: Response): Promise<void> {
|
|
try {
|
|
const domain = await emailProviderService.getProviderDomain();
|
|
res.json({ success: true, data: { domain } } as ApiResponse);
|
|
} catch (error) {
|
|
res.status(500).json({
|
|
success: false,
|
|
error: 'Fehler beim Laden der Domain',
|
|
} as ApiResponse);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Öffentliche Provider-Einstellungen für die Frontend-UI:
|
|
* Domain + Label für Kunden-E-Mail-Adressen.
|
|
* Auch für Nicht-Admin-Mitarbeiter verfügbar, da nur UI-Labels.
|
|
*/
|
|
export async function getPublicSettings(req: Request, res: Response): Promise<void> {
|
|
try {
|
|
const settings = await emailProviderService.getProviderPublicSettings();
|
|
res.json({ success: true, data: settings } as ApiResponse);
|
|
} catch (error) {
|
|
res.status(500).json({
|
|
success: false,
|
|
error: 'Fehler beim Laden der Einstellungen',
|
|
} as ApiResponse);
|
|
}
|
|
}
|