added backup and email client
This commit is contained in:
@@ -0,0 +1,825 @@
|
||||
// ==================== 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
|
||||
}
|
||||
|
||||
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') {
|
||||
clientOptions.tls = { rejectUnauthorized };
|
||||
}
|
||||
|
||||
// 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
|
||||
for await (const message of client.fetch(limitedUids, {
|
||||
uid: true,
|
||||
envelope: true,
|
||||
source: true, // Vollständige E-Mail für Parsing
|
||||
})) {
|
||||
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') {
|
||||
clientOptions.tls = { rejectUnauthorized };
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
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') {
|
||||
clientOptions.tls = { rejectUnauthorized };
|
||||
}
|
||||
|
||||
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> {
|
||||
// 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') {
|
||||
clientOptions.tls = { rejectUnauthorized };
|
||||
}
|
||||
|
||||
const client = new ImapFlow(clientOptions);
|
||||
|
||||
try {
|
||||
await client.connect();
|
||||
await client.mailboxOpen(folder);
|
||||
|
||||
// E-Mail per UID abrufen
|
||||
let attachment: EmailAttachmentData | null = null;
|
||||
|
||||
for await (const message of client.fetch([uid], {
|
||||
uid: true,
|
||||
source: 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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') {
|
||||
clientOptions.tls = { rejectUnauthorized };
|
||||
}
|
||||
|
||||
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') {
|
||||
clientOptions.tls = { rejectUnauthorized };
|
||||
}
|
||||
|
||||
const client = new ImapFlow(clientOptions);
|
||||
const attachments: Array<{ filename: string; contentType: string; size: number }> = [];
|
||||
|
||||
try {
|
||||
await client.connect();
|
||||
await client.mailboxOpen(folder);
|
||||
|
||||
for await (const message of client.fetch([uid], {
|
||||
uid: true,
|
||||
source: 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') {
|
||||
clientOptions.tls = { rejectUnauthorized };
|
||||
}
|
||||
|
||||
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') {
|
||||
clientOptions.tls = { rejectUnauthorized };
|
||||
}
|
||||
|
||||
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') {
|
||||
clientOptions.tls = { rejectUnauthorized };
|
||||
}
|
||||
|
||||
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',
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user