added backup and email client

This commit is contained in:
2026-02-01 00:02:35 +01:00
parent ff857be01a
commit e4fdfbc95f
210 changed files with 24211 additions and 742 deletions
+825
View File
@@ -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',
};
}
}