opencrm/backend/src/services/imapService.ts

942 lines
28 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ==================== IMAP SERVICE ====================
// Service für IMAP-Zugriff auf Mailboxen
import { ImapFlow, FetchMessageObject } from 'imapflow';
import { simpleParser, ParsedMail, AddressObject } from 'mailparser';
// Verschlüsselungstyp
export type MailEncryption = 'SSL' | 'STARTTLS' | 'NONE';
export interface ImapCredentials {
host: string;
port: number;
user: string;
password: string;
encryption?: MailEncryption; // SSL, STARTTLS oder NONE (Standard: SSL)
allowSelfSignedCerts?: boolean; // Selbstsignierte Zertifikate erlauben
}
/**
* TLS-Optionen für IMAP-Verbindungen zusammenbauen.
* Wenn `allowSelfSignedCerts` aktiv ist, werden zusätzlich ältere TLS-Versionen
* (TLS 1.0+) und legacy Cipher-Suites erlaubt hilfreich bei älteren Mailservern,
* die sonst den Socket sofort nach Connect schließen.
*/
function buildTlsOptions(credentials: ImapCredentials): Record<string, unknown> | undefined {
const encryption = credentials.encryption ?? 'SSL';
if (encryption === 'NONE') return undefined;
const rejectUnauthorized = !credentials.allowSelfSignedCerts;
const options: Record<string, unknown> = { rejectUnauthorized };
if (credentials.allowSelfSignedCerts) {
options.minVersion = 'TLSv1';
options.ciphers = 'DEFAULT:@SECLEVEL=0';
}
return options;
}
export interface FetchedEmail {
uid: number;
messageId: string;
subject: string | null;
fromAddress: string;
fromName: string | null;
toAddresses: string[];
ccAddresses: string[];
date: Date;
textBody: string | null;
htmlBody: string | null;
hasAttachments: boolean;
attachmentNames: string[];
}
export interface FetchOptions {
folder?: string; // Default: 'INBOX'
since?: Date; // Nur E-Mails nach diesem Datum
limit?: number; // Max. Anzahl E-Mails
sinceUid?: number; // Nur E-Mails ab dieser UID (für inkrementellen Sync)
}
// Helper: Adressen aus mailparser-Format extrahieren
function extractAddresses(addressObj: AddressObject | AddressObject[] | undefined): string[] {
if (!addressObj) return [];
const addresses = Array.isArray(addressObj) ? addressObj : [addressObj];
const result: string[] = [];
for (const obj of addresses) {
if (obj.value) {
for (const addr of obj.value) {
if (addr.address) {
result.push(addr.address);
}
}
}
}
return result;
}
// Helper: Ersten Absender-Namen extrahieren
function extractFromName(addressObj: AddressObject | AddressObject[] | undefined): string | null {
if (!addressObj) return null;
const addresses = Array.isArray(addressObj) ? addressObj : [addressObj];
for (const obj of addresses) {
if (obj.value && obj.value[0]) {
return obj.value[0].name || null;
}
}
return null;
}
// E-Mails aus einer Mailbox abrufen
export async function fetchEmails(
credentials: ImapCredentials,
options: FetchOptions = {}
): Promise<FetchedEmail[]> {
const {
folder = 'INBOX',
since,
limit = 50,
sinceUid,
} = options;
// Verschlüsselungs-Einstellungen basierend auf Modus
const encryption = credentials.encryption ?? 'SSL';
const rejectUnauthorized = !credentials.allowSelfSignedCerts;
// ImapFlow-Optionen je nach Verschlüsselungstyp
// SSL: secure=true (implicit TLS, Port 993)
// STARTTLS: secure=false (upgrades to TLS, Port 143)
// NONE: secure=false + disableAutoIdle (no encryption, Port 143)
const clientOptions: ConstructorParameters<typeof ImapFlow>[0] = {
host: credentials.host,
port: credentials.port,
secure: encryption === 'SSL',
auth: {
user: credentials.user,
pass: credentials.password,
},
logger: false,
};
// TLS-Optionen nur wenn nicht NONE
if (encryption !== 'NONE') {
const __tls = buildTlsOptions(credentials); if (__tls) clientOptions.tls = __tls as any;
}
// Debug-Logging
console.log(`[IMAP] Connecting to ${credentials.host}:${credentials.port} (${encryption}), user: ${credentials.user}`);
const client = new ImapFlow(clientOptions);
const emails: FetchedEmail[] = [];
try {
await client.connect();
console.log(`[IMAP] Connected successfully`);
// Mailbox öffnen
await client.mailboxOpen(folder);
// Suchkriterien zusammenstellen
let searchCriteria: { since?: Date; uid?: string } = {};
if (since) {
searchCriteria.since = since;
}
// IMAP SEARCH ausführen
let uids: number[];
if (sinceUid) {
// Inkrementeller Sync: Nur E-Mails ab einer bestimmten UID
// IMAP UID-Range: "sinceUid:*" bedeutet alle E-Mails >= sinceUid
const messages = await client.search({ uid: `${sinceUid}:*` }, { uid: true });
const messageArray = Array.isArray(messages) ? messages : [];
uids = messageArray.filter((uid: number) => uid > sinceUid); // Exkludiere die sinceUid selbst
} else if (since) {
// Nach Datum suchen
const messages = await client.search({ since }, { uid: true });
uids = Array.isArray(messages) ? messages : [];
} else {
// Alle E-Mails (mit Limit)
const messages = await client.search({ all: true }, { uid: true });
uids = Array.isArray(messages) ? messages : [];
}
// Neueste zuerst (absteigend sortieren)
uids.sort((a, b) => b - a);
// Limit anwenden
const limitedUids = uids.slice(0, limit);
console.log(`[IMAP] Found ${uids.length} emails, fetching ${limitedUids.length}`);
if (limitedUids.length === 0) {
console.log(`[IMAP] No emails to fetch`);
await client.logout();
return [];
}
// E-Mails abrufen - drittes Argument { uid: true } für UID FETCH
for await (const message of client.fetch(limitedUids.join(','), {
uid: true, // UID im Response inkludieren
envelope: true,
source: true, // Vollständige E-Mail für Parsing
}, { uid: true })) {
try {
// Source muss vorhanden sein
if (!message.source) {
console.error(`E-Mail UID ${message.uid} hat keine Source`);
continue;
}
// E-Mail mit mailparser parsen
const parsed = await simpleParser(message.source) as ParsedMail;
const email: FetchedEmail = {
uid: message.uid,
messageId: parsed.messageId || `${message.uid}@unknown`,
subject: parsed.subject || null,
fromAddress: extractAddresses(parsed.from)[0] || 'unknown@unknown',
fromName: extractFromName(parsed.from),
toAddresses: extractAddresses(parsed.to),
ccAddresses: extractAddresses(parsed.cc),
date: parsed.date || new Date(),
textBody: parsed.text || null,
htmlBody: parsed.html ? String(parsed.html) : null,
hasAttachments: (parsed.attachments?.length || 0) > 0,
attachmentNames: parsed.attachments?.map((a) => a.filename || 'unnamed') || [],
};
emails.push(email);
} catch (parseError) {
console.error(`Fehler beim Parsen von E-Mail UID ${message.uid}:`, parseError);
// E-Mail überspringen bei Parse-Fehlern
}
}
await client.logout();
} catch (error) {
// Verbindung sauber schließen bei Fehlern
try {
await client.logout();
} catch {
// Ignorieren
}
// Bessere Fehlermeldungen
if (error instanceof Error) {
const msg = error.message.toLowerCase();
const errorCode = (error as NodeJS.ErrnoException).code?.toLowerCase() || '';
if (msg.includes('authentication') || msg.includes('login')) {
throw new Error('IMAP-Authentifizierung fehlgeschlagen - Zugangsdaten prüfen');
}
if (msg.includes('econnrefused') || errorCode === 'econnrefused') {
throw new Error(`IMAP-Server nicht erreichbar: ${credentials.host}:${credentials.port} - Verbindung verweigert`);
}
if (msg.includes('timeout') || msg.includes('etimedout') || errorCode === 'etimedout') {
const enc = credentials.encryption ?? 'SSL';
if (enc === 'STARTTLS' && credentials.port === 143) {
throw new Error(`IMAP Port 143 (STARTTLS) nicht erreichbar auf ${credentials.host}`);
} else if (enc === 'SSL' && credentials.port === 993) {
throw new Error(`IMAP Port 993 (SSL) nicht erreichbar auf ${credentials.host}`);
} else {
throw new Error(`IMAP-Verbindung zu ${credentials.host}:${credentials.port} fehlgeschlagen - Timeout`);
}
}
if (msg.includes('certificate') || msg.includes('cert')) {
throw new Error('IMAP SSL-Zertifikatfehler - Aktiviere "Selbstsignierte Zertifikate erlauben"');
}
}
throw error;
}
return emails;
}
// Verbindung testen
export async function testImapConnection(credentials: ImapCredentials): Promise<void> {
// Verschlüsselungs-Einstellungen basierend auf Modus
const encryption = credentials.encryption ?? 'SSL';
const rejectUnauthorized = !credentials.allowSelfSignedCerts;
const clientOptions: ConstructorParameters<typeof ImapFlow>[0] = {
host: credentials.host,
port: credentials.port,
secure: encryption === 'SSL',
auth: {
user: credentials.user,
pass: credentials.password,
},
logger: false,
};
if (encryption !== 'NONE') {
const __tls = buildTlsOptions(credentials); if (__tls) clientOptions.tls = __tls as any;
}
const client = new ImapFlow(clientOptions);
try {
await client.connect();
await client.logout();
} catch (error) {
// Verbindung sauber schließen bei Fehlern
try {
await client.logout();
} catch {
// Ignorieren
}
// Rohes Error-Objekt loggen, damit wir ImapFlow-spezifische Felder sehen
console.error('[testImapConnection] Raw error:', error);
if (error && typeof error === 'object') {
const e = error as any;
console.error('[testImapConnection] Details:', {
code: e.code,
response: e.response,
responseStatus: e.responseStatus,
responseText: e.responseText,
authenticationFailed: e.authenticationFailed,
serverResponseCode: e.serverResponseCode,
});
}
if (error instanceof Error) {
const msg = error.message.toLowerCase();
const errorCode = (error as NodeJS.ErrnoException).code?.toLowerCase() || '';
const e = error as any;
// ImapFlow-spezifische Details durchreichen wenn vorhanden
if (e.authenticationFailed) {
throw new Error(
`IMAP-Authentifizierung fehlgeschlagen${e.response ? `: ${e.response}` : ''}`,
);
}
if (e.response || e.responseText) {
throw new Error(
`IMAP ${e.responseStatus || 'Fehler'}: ${e.response || e.responseText}`,
);
}
if (msg.includes('authentication') || msg.includes('login')) {
throw new Error('IMAP-Authentifizierung fehlgeschlagen');
}
if (msg.includes('econnrefused') || errorCode === 'econnrefused') {
throw new Error(`IMAP-Server nicht erreichbar: ${credentials.host}:${credentials.port} - Verbindung verweigert`);
}
if (msg.includes('timeout') || msg.includes('etimedout') || errorCode === 'etimedout') {
if (encryption === 'STARTTLS' && credentials.port === 143) {
throw new Error(`IMAP Port 143 (STARTTLS) nicht erreichbar auf ${credentials.host}`);
} else if (encryption === 'SSL' && credentials.port === 993) {
throw new Error(`IMAP Port 993 (SSL) nicht erreichbar auf ${credentials.host}`);
} else {
throw new Error(`IMAP-Verbindung zu ${credentials.host}:${credentials.port} fehlgeschlagen - Timeout`);
}
}
if (msg.includes('certificate') || msg.includes('cert')) {
throw new Error('IMAP SSL-Zertifikatfehler - Aktiviere "Selbstsignierte Zertifikate erlauben"');
}
}
throw error;
}
}
// Höchste UID in einer Mailbox ermitteln (für inkrementellen Sync)
export async function getHighestUid(
credentials: ImapCredentials,
folder: string = 'INBOX'
): Promise<number> {
// Verschlüsselungs-Einstellungen basierend auf Modus
const encryption = credentials.encryption ?? 'SSL';
const rejectUnauthorized = !credentials.allowSelfSignedCerts;
const clientOptions: ConstructorParameters<typeof ImapFlow>[0] = {
host: credentials.host,
port: credentials.port,
secure: encryption === 'SSL',
auth: {
user: credentials.user,
pass: credentials.password,
},
logger: false,
};
if (encryption !== 'NONE') {
const __tls = buildTlsOptions(credentials); if (__tls) clientOptions.tls = __tls as any;
}
const client = new ImapFlow(clientOptions);
try {
await client.connect();
const mailbox = await client.mailboxOpen(folder);
const highestUid = mailbox.uidNext ? mailbox.uidNext - 1 : 0;
await client.logout();
return highestUid;
} catch (error) {
try {
await client.logout();
} catch {
// Ignorieren
}
throw error;
}
}
// Anhang-Interface
export interface EmailAttachmentData {
filename: string;
content: Buffer;
contentType: string;
size: number;
}
// Anhang einer E-Mail per UID abrufen
export async function fetchAttachment(
credentials: ImapCredentials,
uid: number,
attachmentFilename: string,
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> {
// Verschlüsselungs-Einstellungen basierend auf Modus
const encryption = credentials.encryption ?? 'SSL';
const rejectUnauthorized = !credentials.allowSelfSignedCerts;
const clientOptions: ConstructorParameters<typeof ImapFlow>[0] = {
host: credentials.host,
port: credentials.port,
secure: encryption === 'SSL',
auth: {
user: credentials.user,
pass: credentials.password,
},
logger: false,
// Timeouts gegen hängende Verbindungen
socketTimeout: 30000,
};
if (encryption !== 'NONE') {
const __tls = buildTlsOptions(credentials); if (__tls) clientOptions.tls = __tls as any;
}
const client = new ImapFlow(clientOptions);
console.log(`[fetchAttachment] Host: ${credentials.host}:${credentials.port} | User: ${credentials.user} | Folder: ${folder} | UID: ${uid} | File: ${attachmentFilename} | AllowSelfSigned: ${credentials.allowSelfSignedCerts}`);
try {
await client.connect();
// 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
let attachment: EmailAttachmentData | null = null;
let foundMessage = false;
// Drittes Argument { uid: true } sorgt dafür, dass UID FETCH statt FETCH verwendet wird
try {
for await (const message of client.fetch(uid.toString(), {
source: true,
}, { uid: true })) {
foundMessage = true;
if (!message.source) continue;
// E-Mail parsen
const parsed = await simpleParser(message.source);
// Anhang suchen
if (parsed.attachments) {
for (const att of parsed.attachments) {
const filename = att.filename || 'unnamed';
if (filename === attachmentFilename) {
attachment = {
filename,
content: att.content,
contentType: att.contentType || 'application/octet-stream',
size: att.size,
};
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();
return attachment;
} catch (error) {
try {
await client.logout();
} catch {
// Ignorieren
}
throw error;
}
}
// Gesendete E-Mail im IMAP Sent-Ordner speichern (für Attachment-Download)
export interface AppendToSentParams {
rawEmail: Buffer | string; // RFC 5322 formatierte E-Mail
sentFolder?: string; // Standard: 'Sent'
}
export interface AppendToSentResult {
success: boolean;
uid?: number; // UID der gespeicherten Nachricht
error?: string;
}
export async function appendToSent(
credentials: ImapCredentials,
params: AppendToSentParams
): Promise<AppendToSentResult> {
const { rawEmail, sentFolder = 'Sent' } = params;
const encryption = credentials.encryption ?? 'SSL';
const rejectUnauthorized = !credentials.allowSelfSignedCerts;
const clientOptions: ConstructorParameters<typeof ImapFlow>[0] = {
host: credentials.host,
port: credentials.port,
secure: encryption === 'SSL',
auth: {
user: credentials.user,
pass: credentials.password,
},
logger: false,
};
if (encryption !== 'NONE') {
const __tls = buildTlsOptions(credentials); if (__tls) clientOptions.tls = __tls as any;
}
console.log(`[IMAP] Appending email to ${sentFolder} folder...`);
const client = new ImapFlow(clientOptions);
try {
await client.connect();
// E-Mail im Sent-Ordner speichern
const result = await client.append(sentFolder, rawEmail, ['\\Seen'], new Date());
await client.logout();
// append kann false zurückgeben bei Fehler
if (!result) {
return { success: false, error: 'IMAP append fehlgeschlagen' };
}
console.log(`[IMAP] Email appended successfully, UID: ${result.uid}`);
return {
success: true,
uid: result.uid,
};
} catch (error) {
try {
await client.logout();
} catch {
// Ignorieren
}
console.error('[IMAP] Error appending to Sent folder:', error);
// Versuche mit alternativen Ordnernamen falls 'Sent' nicht existiert
if (sentFolder === 'Sent' && error instanceof Error) {
// Typische alternative Namen für Sent-Ordner
const alternativeNames = ['INBOX.Sent', 'Sent Messages', 'Sent Items'];
for (const altFolder of alternativeNames) {
try {
const altClient = new ImapFlow(clientOptions);
await altClient.connect();
const altResult = await altClient.append(altFolder, rawEmail, ['\\Seen'], new Date());
await altClient.logout();
if (altResult) {
console.log(`[IMAP] Email appended to ${altFolder}, UID: ${altResult.uid}`);
return { success: true, uid: altResult.uid };
}
} catch {
// Nächsten Namen versuchen
}
}
}
return {
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Speichern im Sent-Ordner',
};
}
}
// Alle Anhänge einer E-Mail per UID abrufen (Metadaten)
export async function fetchAttachmentList(
credentials: ImapCredentials,
uid: number,
folder: string = 'INBOX'
): Promise<Array<{ filename: string; contentType: string; size: number }>> {
// Verschlüsselungs-Einstellungen basierend auf Modus
const encryption = credentials.encryption ?? 'SSL';
const rejectUnauthorized = !credentials.allowSelfSignedCerts;
const clientOptions: ConstructorParameters<typeof ImapFlow>[0] = {
host: credentials.host,
port: credentials.port,
secure: encryption === 'SSL',
auth: {
user: credentials.user,
pass: credentials.password,
},
logger: false,
};
if (encryption !== 'NONE') {
const __tls = buildTlsOptions(credentials); if (__tls) clientOptions.tls = __tls as any;
}
const client = new ImapFlow(clientOptions);
const attachments: Array<{ filename: string; contentType: string; size: number }> = [];
try {
await client.connect();
await client.mailboxOpen(folder);
// Drittes Argument { uid: true } für UID FETCH
for await (const message of client.fetch(uid.toString(), {
source: true,
}, { uid: true })) {
if (!message.source) continue;
const parsed = await simpleParser(message.source);
if (parsed.attachments) {
for (const att of parsed.attachments) {
attachments.push({
filename: att.filename || 'unnamed',
contentType: att.contentType || 'application/octet-stream',
size: att.size,
});
}
}
}
await client.logout();
return attachments;
} catch (error) {
try {
await client.logout();
} catch {
// Ignorieren
}
throw error;
}
}
// ==================== TRASH OPERATIONS ====================
// Typische Namen für Trash-Ordner (verschiedene Server/Sprachen)
const TRASH_FOLDER_NAMES = ['Trash', 'INBOX.Trash', 'Deleted', 'Deleted Items', 'Deleted Messages', 'Papierkorb'];
// Helper: Trash-Ordner finden
async function findTrashFolder(client: ImapFlow): Promise<string | null> {
const mailboxes = await client.list();
for (const name of TRASH_FOLDER_NAMES) {
const found = mailboxes.find(m =>
m.path.toLowerCase() === name.toLowerCase() ||
m.name.toLowerCase() === name.toLowerCase()
);
if (found) return found.path;
}
// Suche nach Ordner mit \Trash Flag
const trashByFlag = mailboxes.find(m => m.specialUse === '\\Trash');
if (trashByFlag) return trashByFlag.path;
return null;
}
export interface MoveToTrashResult {
success: boolean;
newUid?: number; // UID im Trash-Ordner
error?: string;
}
// E-Mail in Papierkorb verschieben
export async function moveToTrash(
credentials: ImapCredentials,
uid: number,
sourceFolder: string = 'INBOX'
): Promise<MoveToTrashResult> {
const encryption = credentials.encryption ?? 'SSL';
const rejectUnauthorized = !credentials.allowSelfSignedCerts;
const clientOptions: ConstructorParameters<typeof ImapFlow>[0] = {
host: credentials.host,
port: credentials.port,
secure: encryption === 'SSL',
auth: {
user: credentials.user,
pass: credentials.password,
},
logger: false,
};
if (encryption !== 'NONE') {
const __tls = buildTlsOptions(credentials); if (__tls) clientOptions.tls = __tls as any;
}
const client = new ImapFlow(clientOptions);
try {
await client.connect();
// Trash-Ordner finden
const trashFolder = await findTrashFolder(client);
if (!trashFolder) {
await client.logout();
return { success: false, error: 'Trash-Ordner nicht gefunden' };
}
// Source-Ordner öffnen
await client.mailboxOpen(sourceFolder);
// E-Mail verschieben (kopieren + löschen)
const moveResult = await client.messageMove([uid], trashFolder, { uid: true });
await client.logout();
if (moveResult && moveResult.uidMap) {
// uidMap ist Map<number, number> - alte UID -> neue UID
const newUid = moveResult.uidMap.get(uid);
console.log(`[IMAP] Email moved to ${trashFolder}, new UID: ${newUid}`);
return { success: true, newUid };
}
return { success: true };
} catch (error) {
try {
await client.logout();
} catch {
// Ignorieren
}
console.error('[IMAP] Error moving to trash:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Verschieben in Papierkorb',
};
}
}
export interface RestoreFromTrashResult {
success: boolean;
newUid?: number; // UID im wiederhergestellten Ordner
error?: string;
}
// E-Mail aus Papierkorb wiederherstellen
export async function restoreFromTrash(
credentials: ImapCredentials,
uid: number,
targetFolder: string = 'INBOX'
): Promise<RestoreFromTrashResult> {
const encryption = credentials.encryption ?? 'SSL';
const rejectUnauthorized = !credentials.allowSelfSignedCerts;
const clientOptions: ConstructorParameters<typeof ImapFlow>[0] = {
host: credentials.host,
port: credentials.port,
secure: encryption === 'SSL',
auth: {
user: credentials.user,
pass: credentials.password,
},
logger: false,
};
if (encryption !== 'NONE') {
const __tls = buildTlsOptions(credentials); if (__tls) clientOptions.tls = __tls as any;
}
const client = new ImapFlow(clientOptions);
try {
await client.connect();
// Trash-Ordner finden
const trashFolder = await findTrashFolder(client);
if (!trashFolder) {
await client.logout();
return { success: false, error: 'Trash-Ordner nicht gefunden' };
}
// Trash-Ordner öffnen
await client.mailboxOpen(trashFolder);
// E-Mail zurück verschieben
const moveResult = await client.messageMove([uid], targetFolder, { uid: true });
await client.logout();
if (moveResult && moveResult.uidMap) {
const newUid = moveResult.uidMap.get(uid);
console.log(`[IMAP] Email restored to ${targetFolder}, new UID: ${newUid}`);
return { success: true, newUid };
}
return { success: true };
} catch (error) {
try {
await client.logout();
} catch {
// Ignorieren
}
console.error('[IMAP] Error restoring from trash:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Wiederherstellen',
};
}
}
export interface PermanentDeleteResult {
success: boolean;
error?: string;
}
// E-Mail endgültig löschen (aus Trash-Ordner)
export async function permanentDelete(
credentials: ImapCredentials,
uid: number,
folder?: string // Optional: Ordner angeben, sonst wird Trash verwendet
): Promise<PermanentDeleteResult> {
const encryption = credentials.encryption ?? 'SSL';
const rejectUnauthorized = !credentials.allowSelfSignedCerts;
const clientOptions: ConstructorParameters<typeof ImapFlow>[0] = {
host: credentials.host,
port: credentials.port,
secure: encryption === 'SSL',
auth: {
user: credentials.user,
pass: credentials.password,
},
logger: false,
};
if (encryption !== 'NONE') {
const __tls = buildTlsOptions(credentials); if (__tls) clientOptions.tls = __tls as any;
}
const client = new ImapFlow(clientOptions);
try {
await client.connect();
// Ordner bestimmen
let targetFolder: string | null | undefined = folder;
if (!targetFolder) {
targetFolder = await findTrashFolder(client);
if (!targetFolder) {
await client.logout();
return { success: false, error: 'Trash-Ordner nicht gefunden' };
}
}
// Ordner öffnen
await client.mailboxOpen(targetFolder);
// E-Mail als gelöscht markieren und expunge
await client.messageDelete([uid], { uid: true });
await client.logout();
console.log(`[IMAP] Email permanently deleted from ${targetFolder}, UID: ${uid}`);
return { success: true };
} catch (error) {
try {
await client.logout();
} catch {
// Ignorieren
}
console.error('[IMAP] Error permanently deleting:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Fehler beim endgültigen Löschen',
};
}
}