added backup and email client
This commit is contained in:
@@ -0,0 +1,304 @@
|
||||
// ==================== SMTP SERVICE ====================
|
||||
// Service für E-Mail-Versand via SMTP
|
||||
|
||||
import nodemailer from 'nodemailer';
|
||||
import type { Transporter } from 'nodemailer';
|
||||
import MailComposer from 'nodemailer/lib/mail-composer';
|
||||
|
||||
// Verschlüsselungstyp
|
||||
export type MailEncryption = 'SSL' | 'STARTTLS' | 'NONE';
|
||||
|
||||
export interface SmtpCredentials {
|
||||
host: string;
|
||||
port: number;
|
||||
user: string;
|
||||
password: string;
|
||||
encryption?: MailEncryption; // SSL, STARTTLS oder NONE (Standard: SSL)
|
||||
allowSelfSignedCerts?: boolean; // Selbstsignierte Zertifikate erlauben
|
||||
}
|
||||
|
||||
// Anhang-Interface
|
||||
export interface EmailAttachment {
|
||||
filename: string;
|
||||
content: string; // Base64-kodierter Inhalt
|
||||
contentType?: string; // MIME-Type (z.B. 'application/pdf')
|
||||
}
|
||||
|
||||
export interface SendEmailParams {
|
||||
to: string | string[];
|
||||
cc?: string | string[];
|
||||
subject: string;
|
||||
text?: string;
|
||||
html?: string;
|
||||
inReplyTo?: string; // Message-ID der E-Mail auf die geantwortet wird
|
||||
references?: string[]; // Thread-Referenzen
|
||||
attachments?: EmailAttachment[]; // Anhänge
|
||||
}
|
||||
|
||||
export interface SendEmailResult {
|
||||
success: boolean;
|
||||
messageId?: string;
|
||||
rawEmail?: Buffer; // RFC 5322 formatierte E-Mail für IMAP-Speicherung
|
||||
error?: string;
|
||||
}
|
||||
|
||||
// E-Mail senden
|
||||
export async function sendEmail(
|
||||
credentials: SmtpCredentials,
|
||||
fromAddress: string,
|
||||
params: SendEmailParams
|
||||
): Promise<SendEmailResult> {
|
||||
// Verschlüsselungs-Einstellungen basierend auf Modus
|
||||
const encryption = credentials.encryption ?? 'SSL';
|
||||
const rejectUnauthorized = !credentials.allowSelfSignedCerts;
|
||||
|
||||
// Transport-Optionen je nach Verschlüsselungstyp
|
||||
// SSL: secure=true (implicit TLS, Port 465)
|
||||
// STARTTLS: secure=false (upgrades to TLS, Port 587)
|
||||
// NONE: secure=false + ignoreTLS=true (no encryption, Port 25)
|
||||
const transportOptions: nodemailer.TransportOptions & {
|
||||
host: string;
|
||||
port: number;
|
||||
secure: boolean;
|
||||
auth: { user: string; pass: string };
|
||||
tls?: { rejectUnauthorized: boolean };
|
||||
ignoreTLS?: boolean;
|
||||
requireTLS?: boolean;
|
||||
connectionTimeout: number;
|
||||
greetingTimeout: number;
|
||||
socketTimeout: number;
|
||||
} = {
|
||||
host: credentials.host,
|
||||
port: credentials.port,
|
||||
secure: encryption === 'SSL',
|
||||
auth: {
|
||||
user: credentials.user,
|
||||
pass: credentials.password,
|
||||
},
|
||||
connectionTimeout: 10000,
|
||||
greetingTimeout: 10000,
|
||||
socketTimeout: 30000,
|
||||
};
|
||||
|
||||
// TLS-Optionen nur wenn nicht NONE
|
||||
if (encryption !== 'NONE') {
|
||||
transportOptions.tls = { rejectUnauthorized };
|
||||
} else {
|
||||
// Keine Verschlüsselung: STARTTLS ignorieren
|
||||
transportOptions.ignoreTLS = true;
|
||||
}
|
||||
|
||||
// Bei STARTTLS: requireTLS erzwingen
|
||||
if (encryption === 'STARTTLS') {
|
||||
transportOptions.requireTLS = true;
|
||||
}
|
||||
|
||||
// Debug-Logging für Entwicklung
|
||||
console.log(`[SMTP] Connecting to ${credentials.host}:${credentials.port} (${encryption}), user: ${credentials.user}`);
|
||||
|
||||
// Transporter erstellen
|
||||
const transporter: Transporter = nodemailer.createTransport(transportOptions);
|
||||
|
||||
try {
|
||||
// E-Mail-Optionen zusammenstellen
|
||||
const mailOptions: nodemailer.SendMailOptions = {
|
||||
from: fromAddress,
|
||||
to: Array.isArray(params.to) ? params.to.join(', ') : params.to,
|
||||
subject: params.subject,
|
||||
};
|
||||
|
||||
// CC hinzufügen falls vorhanden
|
||||
if (params.cc) {
|
||||
mailOptions.cc = Array.isArray(params.cc) ? params.cc.join(', ') : params.cc;
|
||||
}
|
||||
|
||||
// Body hinzufügen
|
||||
if (params.html) {
|
||||
mailOptions.html = params.html;
|
||||
// Auch Text-Version für Clients ohne HTML-Support
|
||||
mailOptions.text = params.text || stripHtml(params.html);
|
||||
} else if (params.text) {
|
||||
mailOptions.text = params.text;
|
||||
}
|
||||
|
||||
// Threading-Header für Antworten
|
||||
if (params.inReplyTo) {
|
||||
mailOptions.inReplyTo = params.inReplyTo;
|
||||
}
|
||||
if (params.references && params.references.length > 0) {
|
||||
mailOptions.references = params.references.join(' ');
|
||||
}
|
||||
|
||||
// Anhänge hinzufügen
|
||||
if (params.attachments && params.attachments.length > 0) {
|
||||
mailOptions.attachments = params.attachments.map((att) => ({
|
||||
filename: att.filename,
|
||||
content: Buffer.from(att.content, 'base64'),
|
||||
contentType: att.contentType,
|
||||
}));
|
||||
}
|
||||
|
||||
// E-Mail senden
|
||||
const result = await transporter.sendMail(mailOptions);
|
||||
|
||||
// Raw E-Mail für IMAP-Speicherung bauen (mit tatsächlicher Message-ID)
|
||||
let rawEmail: Buffer | undefined;
|
||||
try {
|
||||
const composerOptions = {
|
||||
...mailOptions,
|
||||
messageId: result.messageId, // Tatsächliche Message-ID vom Server
|
||||
};
|
||||
const composer = new MailComposer(composerOptions);
|
||||
rawEmail = await composer.compile().build();
|
||||
} catch (compileError) {
|
||||
console.error('Error compiling raw email:', compileError);
|
||||
// Nicht kritisch - E-Mail wurde trotzdem gesendet
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
messageId: result.messageId,
|
||||
rawEmail,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('SMTP sendEmail error:', error);
|
||||
|
||||
// Bessere Fehlermeldungen
|
||||
let errorMessage = 'Unbekannter Fehler beim E-Mail-Versand';
|
||||
|
||||
if (error instanceof Error) {
|
||||
const msg = error.message.toLowerCase();
|
||||
const errorCode = (error as NodeJS.ErrnoException).code?.toLowerCase() || '';
|
||||
|
||||
if (msg.includes('authentication') || msg.includes('auth') || msg.includes('login')) {
|
||||
errorMessage = 'SMTP-Authentifizierung fehlgeschlagen - Zugangsdaten prüfen';
|
||||
} else if (msg.includes('econnrefused') || errorCode === 'econnrefused') {
|
||||
errorMessage = `SMTP-Server nicht erreichbar: ${credentials.host}:${credentials.port} - Verbindung verweigert`;
|
||||
} else if (msg.includes('greeting never received') || msg.includes('etimedout') || errorCode === 'etimedout') {
|
||||
// Detaillierte Fehlermeldung je nach Port/Verschlüsselung
|
||||
const enc = credentials.encryption ?? 'SSL';
|
||||
if (enc === 'STARTTLS' && credentials.port === 587) {
|
||||
errorMessage = `SMTP-Verbindung zu Port 587 fehlgeschlagen - STARTTLS (Submission) ist möglicherweise nicht aktiviert auf ${credentials.host}`;
|
||||
} else if (enc === 'NONE' && credentials.port === 25) {
|
||||
errorMessage = `SMTP-Verbindung zu Port 25 fehlgeschlagen - Port möglicherweise blockiert oder nicht erreichbar auf ${credentials.host}`;
|
||||
} else {
|
||||
errorMessage = `SMTP-Verbindung zu ${credentials.host}:${credentials.port} fehlgeschlagen - Port nicht erreichbar oder Timeout`;
|
||||
}
|
||||
} else if (msg.includes('timeout')) {
|
||||
errorMessage = `SMTP-Verbindung: Zeitüberschreitung bei ${credentials.host}:${credentials.port}`;
|
||||
} else if (msg.includes('recipient') || msg.includes('rejected')) {
|
||||
errorMessage = 'Empfänger-Adresse wurde vom Server abgelehnt';
|
||||
} else if (msg.includes('certificate') || msg.includes('cert')) {
|
||||
errorMessage = 'SSL-Zertifikatfehler - Aktiviere "Selbstsignierte Zertifikate erlauben" in den Provider-Einstellungen';
|
||||
} else if (msg.includes('socket close') || msg.includes('socket hang up') || msg.includes('econnreset') || errorCode === 'econnreset') {
|
||||
// Server schließt Verbindung unerwartet - oft TLS-Problem bei STARTTLS
|
||||
const enc = credentials.encryption ?? 'SSL';
|
||||
if (enc === 'STARTTLS') {
|
||||
errorMessage = `SMTP-Verbindung abgebrochen bei STARTTLS - Aktiviere "Selbstsignierte Zertifikate erlauben" oder verwende SSL/TLS auf Port 465`;
|
||||
} else {
|
||||
errorMessage = `SMTP-Verbindung unerwartet geschlossen von ${credentials.host}:${credentials.port}`;
|
||||
}
|
||||
} else {
|
||||
errorMessage = error.message;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: errorMessage,
|
||||
};
|
||||
} finally {
|
||||
// Transporter schließen
|
||||
transporter.close();
|
||||
}
|
||||
}
|
||||
|
||||
// SMTP-Verbindung testen
|
||||
export async function testSmtpConnection(credentials: SmtpCredentials): Promise<void> {
|
||||
// Verschlüsselungs-Einstellungen basierend auf Modus
|
||||
const encryption = credentials.encryption ?? 'SSL';
|
||||
const rejectUnauthorized = !credentials.allowSelfSignedCerts;
|
||||
|
||||
const transportOptions: nodemailer.TransportOptions & {
|
||||
host: string;
|
||||
port: number;
|
||||
secure: boolean;
|
||||
auth: { user: string; pass: string };
|
||||
tls?: { rejectUnauthorized: boolean };
|
||||
ignoreTLS?: boolean;
|
||||
connectionTimeout: number;
|
||||
greetingTimeout: number;
|
||||
} = {
|
||||
host: credentials.host,
|
||||
port: credentials.port,
|
||||
secure: encryption === 'SSL',
|
||||
auth: {
|
||||
user: credentials.user,
|
||||
pass: credentials.password,
|
||||
},
|
||||
connectionTimeout: 10000,
|
||||
greetingTimeout: 10000,
|
||||
};
|
||||
|
||||
if (encryption !== 'NONE') {
|
||||
transportOptions.tls = { rejectUnauthorized };
|
||||
} else {
|
||||
transportOptions.ignoreTLS = true;
|
||||
}
|
||||
|
||||
const transporter: Transporter = nodemailer.createTransport(transportOptions);
|
||||
|
||||
try {
|
||||
// Verbindung verifizieren
|
||||
await transporter.verify();
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
const msg = error.message.toLowerCase();
|
||||
const errorCode = (error as NodeJS.ErrnoException).code?.toLowerCase() || '';
|
||||
|
||||
if (msg.includes('authentication') || msg.includes('auth') || msg.includes('login')) {
|
||||
throw new Error('SMTP-Authentifizierung fehlgeschlagen');
|
||||
}
|
||||
if (msg.includes('econnrefused') || errorCode === 'econnrefused') {
|
||||
throw new Error(`SMTP-Server nicht erreichbar: ${credentials.host}:${credentials.port} - Verbindung verweigert`);
|
||||
}
|
||||
if (msg.includes('greeting never received') || msg.includes('etimedout') || errorCode === 'etimedout') {
|
||||
if (encryption === 'STARTTLS' && credentials.port === 587) {
|
||||
throw new Error(`SMTP Port 587 (STARTTLS/Submission) ist nicht erreichbar - In Plesk unter Tools & Settings > Mail Server Settings aktivieren`);
|
||||
} else if (encryption === 'NONE' && credentials.port === 25) {
|
||||
throw new Error(`SMTP Port 25 ist nicht erreichbar auf ${credentials.host}`);
|
||||
} else {
|
||||
throw new Error(`SMTP-Verbindung zu ${credentials.host}:${credentials.port} fehlgeschlagen - Port nicht erreichbar`);
|
||||
}
|
||||
}
|
||||
if (msg.includes('certificate') || msg.includes('cert')) {
|
||||
throw new Error('SSL-Zertifikatfehler - Aktiviere "Selbstsignierte Zertifikate erlauben"');
|
||||
}
|
||||
}
|
||||
|
||||
throw error;
|
||||
} finally {
|
||||
transporter.close();
|
||||
}
|
||||
}
|
||||
|
||||
// Helper: HTML zu Text konvertieren (einfache Version)
|
||||
function stripHtml(html: string): string {
|
||||
return html
|
||||
// Zeilenumbrüche für Block-Elemente
|
||||
.replace(/<br\s*\/?>/gi, '\n')
|
||||
.replace(/<\/p>/gi, '\n\n')
|
||||
.replace(/<\/div>/gi, '\n')
|
||||
.replace(/<\/li>/gi, '\n')
|
||||
// Alle HTML-Tags entfernen
|
||||
.replace(/<[^>]+>/g, '')
|
||||
// HTML-Entities dekodieren
|
||||
.replace(/ /g, ' ')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
// Mehrfache Leerzeilen reduzieren
|
||||
.replace(/\n{3,}/g, '\n\n')
|
||||
.trim();
|
||||
}
|
||||
Reference in New Issue
Block a user