942 lines
28 KiB
TypeScript
942 lines
28 KiB
TypeScript
// ==================== 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',
|
||
};
|
||
}
|
||
}
|