opencrm/backend/src/services/emailProvider/emailProviderService.ts

567 lines
17 KiB
TypeScript

// ==================== EMAIL PROVIDER SERVICE ====================
import { PrismaClient } from '@prisma/client';
import { decrypt } from '../../utils/encryption.js';
import {
IEmailProvider,
EmailProviderConfig,
EmailExistsResult,
EmailOperationResult,
CreateEmailParams,
MailEncryption,
} from './types.js';
import { PleskEmailProvider } from './pleskProvider.js';
const prisma = new PrismaClient();
// Factory-Funktion um den richtigen Provider zu erstellen
function createProvider(config: EmailProviderConfig): IEmailProvider {
switch (config.type) {
case 'PLESK':
return new PleskEmailProvider(config);
case 'CPANEL':
// TODO: cPanel Provider implementieren
throw new Error('cPanel Provider noch nicht implementiert');
case 'DIRECTADMIN':
// TODO: DirectAdmin Provider implementieren
throw new Error('DirectAdmin Provider noch nicht implementiert');
default:
throw new Error(`Unbekannter Provider-Typ: ${config.type}`);
}
}
// ==================== CONFIG CRUD ====================
export async function getAllProviderConfigs() {
return prisma.emailProviderConfig.findMany({
orderBy: [{ isDefault: 'desc' }, { name: 'asc' }],
});
}
export async function getProviderConfigById(id: number) {
return prisma.emailProviderConfig.findUnique({
where: { id },
});
}
export async function getDefaultProviderConfig() {
return prisma.emailProviderConfig.findFirst({
where: { isActive: true, isDefault: true },
});
}
export async function getActiveProviderConfig() {
// Erst Default-Provider versuchen, dann irgendeinen aktiven
const defaultProvider = await getDefaultProviderConfig();
if (defaultProvider) return defaultProvider;
return prisma.emailProviderConfig.findFirst({
where: { isActive: true },
});
}
export interface CreateProviderConfigData {
name: string;
type: 'PLESK' | 'CPANEL' | 'DIRECTADMIN';
apiUrl: string;
apiKey?: string;
username?: string;
password?: string;
domain: string;
defaultForwardEmail?: string;
// Verschlüsselungs-Einstellungen
imapEncryption?: MailEncryption;
smtpEncryption?: MailEncryption;
allowSelfSignedCerts?: boolean;
isActive?: boolean;
isDefault?: boolean;
}
export async function createProviderConfig(data: CreateProviderConfigData) {
// Falls isDefault=true, alle anderen auf false setzen
if (data.isDefault) {
await prisma.emailProviderConfig.updateMany({
where: { isDefault: true },
data: { isDefault: false },
});
}
// Passwort verschlüsseln falls vorhanden
const { encrypt } = await import('../../utils/encryption.js');
const passwordEncrypted = data.password ? encrypt(data.password) : null;
return prisma.emailProviderConfig.create({
data: {
name: data.name,
type: data.type,
apiUrl: data.apiUrl,
apiKey: data.apiKey || null,
username: data.username || null,
passwordEncrypted,
domain: data.domain,
defaultForwardEmail: data.defaultForwardEmail || null,
imapEncryption: data.imapEncryption ?? 'SSL',
smtpEncryption: data.smtpEncryption ?? 'SSL',
allowSelfSignedCerts: data.allowSelfSignedCerts ?? false,
isActive: data.isActive ?? true,
isDefault: data.isDefault ?? false,
},
});
}
export async function updateProviderConfig(
id: number,
data: Partial<CreateProviderConfigData>
) {
// Falls isDefault=true, alle anderen auf false setzen
if (data.isDefault) {
await prisma.emailProviderConfig.updateMany({
where: { isDefault: true, id: { not: id } },
data: { isDefault: false },
});
}
const updateData: Record<string, unknown> = {};
if (data.name !== undefined) updateData.name = data.name;
if (data.type !== undefined) updateData.type = data.type;
if (data.apiUrl !== undefined) updateData.apiUrl = data.apiUrl;
if (data.apiKey !== undefined) updateData.apiKey = data.apiKey || null;
if (data.username !== undefined) updateData.username = data.username || null;
if (data.domain !== undefined) updateData.domain = data.domain;
if (data.defaultForwardEmail !== undefined)
updateData.defaultForwardEmail = data.defaultForwardEmail || null;
if (data.imapEncryption !== undefined) updateData.imapEncryption = data.imapEncryption;
if (data.smtpEncryption !== undefined) updateData.smtpEncryption = data.smtpEncryption;
if (data.allowSelfSignedCerts !== undefined) updateData.allowSelfSignedCerts = data.allowSelfSignedCerts;
if (data.isActive !== undefined) updateData.isActive = data.isActive;
if (data.isDefault !== undefined) updateData.isDefault = data.isDefault;
// Passwort-Logik:
// - Wenn neues Passwort übergeben → verschlüsseln und speichern
// - Wenn Benutzername gelöscht wird → Passwort auch löschen (gehören zusammen)
if (data.password) {
const { encrypt } = await import('../../utils/encryption.js');
updateData.passwordEncrypted = encrypt(data.password);
} else if (data.username !== undefined && !data.username) {
// Benutzername wird gelöscht → Passwort auch löschen
updateData.passwordEncrypted = null;
}
return prisma.emailProviderConfig.update({
where: { id },
data: updateData,
});
}
export async function deleteProviderConfig(id: number) {
return prisma.emailProviderConfig.delete({
where: { id },
});
}
// ==================== EMAIL OPERATIONS ====================
// Provider-Instanz aus DB-Config erstellen
async function getProviderInstance(): Promise<IEmailProvider> {
const dbConfig = await getActiveProviderConfig();
if (!dbConfig) {
throw new Error('Kein aktiver Email-Provider konfiguriert');
}
// Passwort entschlüsseln
let password: string | undefined;
if (dbConfig.passwordEncrypted) {
try {
password = decrypt(dbConfig.passwordEncrypted);
} catch {
console.error('Konnte Passwort nicht entschlüsseln');
}
}
const config: EmailProviderConfig = {
id: dbConfig.id,
name: dbConfig.name,
type: dbConfig.type as 'PLESK' | 'CPANEL' | 'DIRECTADMIN',
apiUrl: dbConfig.apiUrl,
apiKey: dbConfig.apiKey || undefined,
username: dbConfig.username || undefined,
password,
domain: dbConfig.domain,
defaultForwardEmail: dbConfig.defaultForwardEmail || undefined,
imapServer: dbConfig.imapServer || undefined,
imapPort: dbConfig.imapPort || undefined,
smtpServer: dbConfig.smtpServer || undefined,
smtpPort: dbConfig.smtpPort || undefined,
imapEncryption: dbConfig.imapEncryption as MailEncryption,
smtpEncryption: dbConfig.smtpEncryption as MailEncryption,
allowSelfSignedCerts: dbConfig.allowSelfSignedCerts,
isActive: dbConfig.isActive,
isDefault: dbConfig.isDefault,
};
return createProvider(config);
}
// Prüfen ob eine E-Mail existiert
export async function checkEmailExists(localPart: string): Promise<EmailExistsResult> {
try {
const provider = await getProviderInstance();
return provider.emailExists(localPart);
} catch (error) {
console.error('checkEmailExists error:', error);
return { exists: false };
}
}
// E-Mail erstellen mit Weiterleitungen
export async function provisionEmail(
localPart: string,
customerEmail: string
): Promise<EmailOperationResult> {
try {
const provider = await getProviderInstance();
const config = await getActiveProviderConfig();
// Weiterleitungsziele zusammenstellen
const forwardTargets: string[] = [customerEmail];
// Unsere eigene Weiterleitungsadresse hinzufügen falls konfiguriert
if (config?.defaultForwardEmail) {
forwardTargets.push(config.defaultForwardEmail);
}
// Prüfen ob existiert
const exists = await provider.emailExists(localPart);
if (exists.exists) {
return {
success: true,
message: `E-Mail ${exists.email} existiert bereits`,
};
}
// Erstellen
const result = await provider.createEmail({
localPart,
forwardTargets,
});
return result;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unbekannter Fehler';
return {
success: false,
error: errorMessage,
};
}
}
// E-Mail mit echter Mailbox erstellen (IMAP/SMTP-Zugang)
export async function provisionEmailWithMailbox(
localPart: string,
customerEmail: string,
password: string
): Promise<EmailOperationResult & { email?: string }> {
try {
const provider = await getProviderInstance();
const config = await getActiveProviderConfig();
// Weiterleitungsziele zusammenstellen
const forwardTargets: string[] = [customerEmail];
// Unsere eigene Weiterleitungsadresse hinzufügen falls konfiguriert
if (config?.defaultForwardEmail) {
forwardTargets.push(config.defaultForwardEmail);
}
// Prüfen ob existiert
const exists = await provider.emailExists(localPart);
if (exists.exists) {
return {
success: true,
message: `E-Mail ${exists.email} existiert bereits`,
email: exists.email,
};
}
// Mit Mailbox erstellen
const result = await provider.createEmailWithMailbox({
localPart,
forwardTargets,
password,
});
return result;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unbekannter Fehler';
return {
success: false,
error: errorMessage,
};
}
}
// Mailbox für existierende E-Mail-Weiterleitung aktivieren
export async function enableMailboxForExistingEmail(
localPart: string,
password: string
): Promise<EmailOperationResult> {
try {
const provider = await getProviderInstance();
const result = await provider.enableMailboxForExisting({
localPart,
password,
});
return result;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unbekannter Fehler';
return {
success: false,
error: errorMessage,
};
}
}
// Mailbox-Passwort beim Provider aktualisieren
export async function updateMailboxPassword(
localPart: string,
password: string
): Promise<EmailOperationResult> {
try {
const provider = await getProviderInstance();
const result = await provider.updateMailboxPassword({
localPart,
password,
});
return result;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unbekannter Fehler';
return {
success: false,
error: errorMessage,
};
}
}
// IMAP/SMTP-Einstellungen vom aktiven Provider holen
export interface ImapSmtpSettings {
imapServer: string;
imapPort: number;
imapEncryption: MailEncryption; // SSL, STARTTLS oder NONE
smtpServer: string;
smtpPort: number;
smtpEncryption: MailEncryption; // SSL, STARTTLS oder NONE
allowSelfSignedCerts: boolean; // Selbstsignierte Zertifikate erlauben
domain: string;
}
export async function getImapSmtpSettings(): Promise<ImapSmtpSettings | null> {
const config = await getActiveProviderConfig();
if (!config) return null;
// Default-Server: Hostname aus der apiUrl extrahieren (z.B. rs001871.fastrootserver.de aus https://rs001871.fastrootserver.de:8443)
// Der Plesk-Server ist gleichzeitig der Mail-Server
let defaultServer: string;
try {
const url = new URL(config.apiUrl);
defaultServer = url.hostname;
} catch {
// Fallback falls apiUrl ungültig
defaultServer = `mail.${config.domain}`;
}
// Verschlüsselungs-Einstellungen
const imapEncryption = (config.imapEncryption ?? 'SSL') as MailEncryption;
const smtpEncryption = (config.smtpEncryption ?? 'SSL') as MailEncryption;
// Ports basierend auf Verschlüsselung berechnen:
// SSL: IMAP 993, SMTP 465
// STARTTLS: IMAP 143, SMTP 587
// NONE: IMAP 143, SMTP 25
//
// Standard-Ports werden IMMER basierend auf Verschlüsselung berechnet.
// Nur benutzerdefinierte Ports (nicht 993/143/465/587/25) werden aus der DB übernommen.
const getImapPort = (enc: MailEncryption, storedPort: number | null) => {
const standardPorts = [993, 143];
// Wenn ein nicht-standard Port gespeichert ist, diesen verwenden
if (storedPort && !standardPorts.includes(storedPort)) {
return storedPort;
}
// Sonst basierend auf Verschlüsselung
return enc === 'SSL' ? 993 : 143;
};
const getSmtpPort = (enc: MailEncryption, storedPort: number | null) => {
const standardPorts = [465, 587, 25];
// Wenn ein nicht-standard Port gespeichert ist, diesen verwenden
if (storedPort && !standardPorts.includes(storedPort)) {
return storedPort;
}
// Sonst basierend auf Verschlüsselung
if (enc === 'SSL') return 465;
if (enc === 'STARTTLS') return 587;
return 25; // NONE
};
return {
imapServer: config.imapServer || defaultServer,
imapPort: getImapPort(imapEncryption, config.imapPort),
imapEncryption,
smtpServer: config.smtpServer || defaultServer,
smtpPort: getSmtpPort(smtpEncryption, config.smtpPort),
smtpEncryption,
allowSelfSignedCerts: config.allowSelfSignedCerts ?? false,
domain: config.domain,
};
}
// E-Mail löschen
export async function deprovisionEmail(localPart: string): Promise<EmailOperationResult> {
try {
const provider = await getProviderInstance();
return provider.deleteEmail(localPart);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unbekannter Fehler';
return {
success: false,
error: errorMessage,
};
}
}
// E-Mail umbenennen
export async function renameProvisionedEmail(
oldLocalPart: string,
newLocalPart: string
): Promise<EmailOperationResult> {
try {
const provider = await getProviderInstance();
return provider.renameEmail({ oldLocalPart, newLocalPart });
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unbekannter Fehler';
return {
success: false,
error: errorMessage,
};
}
}
// Domain aus aktivem Provider holen
export async function getProviderDomain(): Promise<string | null> {
const config = await getActiveProviderConfig();
return config?.domain || null;
}
// Provider-Instanz aus übergebener Config erstellen (für Tests mit ungespeicherten Daten)
function createProviderFromFormData(data: {
type: 'PLESK' | 'CPANEL' | 'DIRECTADMIN';
apiUrl: string;
apiKey?: string;
username?: string;
password?: string;
domain: string;
}): IEmailProvider {
const config: EmailProviderConfig = {
id: 0,
name: 'Test',
type: data.type,
apiUrl: data.apiUrl,
apiKey: data.apiKey,
username: data.username,
password: data.password,
domain: data.domain,
isActive: true,
isDefault: false,
};
return createProvider(config);
}
// Provider-Instanz aus DB-Config per ID erstellen
async function getProviderInstanceById(id: number): Promise<IEmailProvider> {
const dbConfig = await getProviderConfigById(id);
if (!dbConfig) {
throw new Error('Email-Provider nicht gefunden');
}
// Passwort entschlüsseln
let password: string | undefined;
if (dbConfig.passwordEncrypted) {
try {
password = decrypt(dbConfig.passwordEncrypted);
} catch {
console.error('Konnte Passwort nicht entschlüsseln');
}
}
const config: EmailProviderConfig = {
id: dbConfig.id,
name: dbConfig.name,
type: dbConfig.type as 'PLESK' | 'CPANEL' | 'DIRECTADMIN',
apiUrl: dbConfig.apiUrl,
apiKey: dbConfig.apiKey || undefined,
username: dbConfig.username || undefined,
password,
domain: dbConfig.domain,
defaultForwardEmail: dbConfig.defaultForwardEmail || undefined,
imapServer: dbConfig.imapServer || undefined,
imapPort: dbConfig.imapPort || undefined,
smtpServer: dbConfig.smtpServer || undefined,
smtpPort: dbConfig.smtpPort || undefined,
imapEncryption: dbConfig.imapEncryption as MailEncryption,
smtpEncryption: dbConfig.smtpEncryption as MailEncryption,
allowSelfSignedCerts: dbConfig.allowSelfSignedCerts,
isActive: dbConfig.isActive,
isDefault: dbConfig.isDefault,
};
return createProvider(config);
}
// Provider-Verbindung testen (mit ID, Formulardaten oder Default-Provider)
export async function testProviderConnection(options?: {
id?: number;
testData?: {
type: 'PLESK' | 'CPANEL' | 'DIRECTADMIN';
apiUrl: string;
apiKey?: string;
username?: string;
password?: string;
domain: string;
};
}): Promise<EmailOperationResult> {
try {
let provider: IEmailProvider;
if (options?.testData) {
// Mit übergebenen Daten testen (z.B. aus Modal beim Neuanlegen)
provider = createProviderFromFormData(options.testData);
} else if (options?.id) {
// Gespeicherten Provider per ID testen
provider = await getProviderInstanceById(options.id);
} else {
// Default-Provider testen
provider = await getProviderInstance();
}
// Expliziter Verbindungstest (wirft Fehler bei Auth-Problemen)
await provider.testConnection();
return {
success: true,
message: 'Verbindung zum Email-Provider erfolgreich',
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unbekannter Fehler';
return {
success: false,
error: errorMessage,
};
}
}