Files
paperlessmanager/paperless-backend/src/email/imap-folder.service.ts
T
bjoernpoettker 7076eef57b
Build and Push Multi-Platform Images / build-and-push (push) Successful in 39s
feat: add mobile-responsive layout and card views across all pages
- New useIsMobile hook and MobileCardList component
- AppLayout: hamburger menu + Drawer navigation on mobile
- All list pages (Inbox, Posteingang, Manuell, Mail, Freigabe, Zahlung, TaskLog)
  show card layout on mobile instead of tables
- CSS: responsive modal height, horizontal table scroll, text-size-adjust
- Backend: Agrarmonitor polling fixes, Zahlung service improvements,
  IMAP folder service extended, misc controller fixes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-14 09:22:58 +02:00

174 lines
5.6 KiB
TypeScript

import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Cron } from '@nestjs/schedule';
import { ImapFlow } from 'imapflow';
@Injectable()
export class ImapFolderService {
private readonly logger = new Logger(ImapFolderService.name);
constructor(private readonly configService: ConfigService) {}
private createClient(): ImapFlow {
return new ImapFlow({
host: this.configService.get<string>('IMAP_HOST', ''),
port: this.configService.get<number>('IMAP_PORT', 993),
secure: this.configService.get<string>('IMAP_USE_SSL', 'true') === 'true',
auth: {
user: this.configService.get<string>('IMAP_USERNAME', ''),
pass: this.configService.get<string>('IMAP_PASSWORD', ''),
},
logger: false,
});
}
@Cron('0 3 * * *', { timeZone: 'Europe/Berlin' })
async cleanupImportedEmails(): Promise<void> {
if (!this.configService.get<string>('IMAP_HOST')) return;
const importedFolder = this.configService.get<string>(
'IMAP_IMPORTED_FOLDER',
'importiert',
);
const trashFolder = this.configService.get<string>(
'IMAP_TRASH_FOLDER',
'Trash',
);
const client = this.createClient();
try {
await client.connect();
// E-Mails älter als 90 Tage in Papierkorb verschieben
try {
await client.mailboxOpen(importedFolder);
const cutoff = new Date();
cutoff.setDate(cutoff.getDate() - 90);
const oldUids = await client.search({ before: cutoff }, { uid: true });
if (Array.isArray(oldUids) && oldUids.length > 0) {
await client.messageMove(oldUids, trashFolder, { uid: true });
this.logger.log(
`${oldUids.length} alte E-Mail(s) aus "${importedFolder}" in "${trashFolder}" verschoben.`,
);
}
} catch (err: any) {
this.logger.warn(
`Bereinigung "${importedFolder}" nicht möglich: ${err.message}`,
);
}
// Papierkorb leeren
try {
await client.mailboxOpen(trashFolder);
const trashUids = await client.search({ all: true }, { uid: true });
if (Array.isArray(trashUids) && trashUids.length > 0) {
await client.messageDelete(trashUids, { uid: true });
this.logger.log(
`${trashUids.length} E-Mail(s) aus "${trashFolder}" gelöscht.`,
);
}
} catch (err: any) {
this.logger.warn(
`Papierkorb "${trashFolder}" konnte nicht geleert werden: ${err.message}`,
);
}
} catch (err: any) {
this.logger.error(`IMAP-Cleanup fehlgeschlagen: ${err.message}`);
} finally {
await client.logout().catch(() => {});
}
}
async moveProcessedInboxToImportiert(messageIds: string[]): Promise<number> {
if (!this.configService.get<string>('IMAP_HOST') || messageIds.length === 0)
return 0;
const importedFolder = this.configService.get<string>(
'IMAP_IMPORTED_FOLDER',
'importiert',
);
const client = this.createClient();
let movedCount = 0;
try {
await client.connect();
const mailboxes = await client.list();
if (!mailboxes.some((m) => m.path === importedFolder)) {
await client.mailboxCreate(importedFolder);
this.logger.log(`IMAP-Ordner "${importedFolder}" erstellt.`);
}
const status = await client.mailboxOpen('INBOX');
if (status.exists === 0) return 0;
const normalize = (id: string) => id.replace(/^<|>$/g, '').toLowerCase();
const idSet = new Set(messageIds.map(normalize));
const uidsToMove: number[] = [];
for await (const msg of client.fetch('1:*', {
uid: true,
envelope: true,
})) {
const msgId = msg.envelope?.messageId;
if (msgId && idSet.has(normalize(msgId))) {
uidsToMove.push(msg.uid);
}
}
if (uidsToMove.length > 0) {
await client.messageMove(uidsToMove, importedFolder, { uid: true });
movedCount = uidsToMove.length;
this.logger.log(
`${movedCount} E-Mail(s) aus INBOX → "${importedFolder}" verschoben.`,
);
}
} catch (err: any) {
this.logger.error(
`moveProcessedInboxToImportiert fehlgeschlagen: ${err.message}`,
);
} finally {
await client.logout().catch(() => {});
}
return movedCount;
}
async moveToImportiert(messageId: string): Promise<void> {
if (!this.configService.get<string>('IMAP_HOST')) return;
const importedFolder = this.configService.get<string>(
'IMAP_IMPORTED_FOLDER',
'importiert',
);
const client = this.createClient();
try {
await client.connect();
const mailboxes = await client.list();
if (!mailboxes.some((m) => m.path === importedFolder)) {
await client.mailboxCreate(importedFolder);
this.logger.log(`IMAP-Ordner "${importedFolder}" erstellt.`);
}
await client.mailboxOpen('INBOX');
const uids = await client.search(
{ header: { 'message-id': messageId } },
{ uid: true },
);
if (Array.isArray(uids) && uids.length > 0) {
await client.messageMove(uids, importedFolder, { uid: true });
this.logger.log(
`E-Mail ${messageId} → "${importedFolder}" verschoben.`,
);
} else {
this.logger.warn(
`E-Mail ${messageId} nicht in INBOX gefunden (bereits verschoben?).`,
);
}
} catch (err: any) {
this.logger.error(`IMAP moveToImportiert fehlgeschlagen: ${err.message}`);
} finally {
await client.logout().catch(() => {});
}
}
}