// ==================== 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 { // 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 { // 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(//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(); }