diff --git a/docker-compose.yml b/docker-compose.yml index 12a8d0b..11c008d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -36,6 +36,8 @@ services: - IMAP_USE_SSL=${IMAP_USE_SSL:-true} - IMAP_USERNAME=${IMAP_USERNAME:-} - IMAP_PASSWORD=${IMAP_PASSWORD:-} + - IMAP_IMPORTED_FOLDER=${IMAP_IMPORTED_FOLDER:-importiert} + - IMAP_TRASH_FOLDER=${IMAP_TRASH_FOLDER:-Trash} - BELEGNUMMER_GET_URL=${BELEGNUMMER_GET_URL:-} - BELEGNUMMER_SET_URL=${BELEGNUMMER_SET_URL:-} - AGRARMONITOR_BASE_URL=${AGRARMONITOR_BASE_URL:-https://admin7.agrarmonitor.de} diff --git a/paperless-backend/src/email-download/email-download.service.ts b/paperless-backend/src/email-download/email-download.service.ts index a21283d..83b40b7 100644 --- a/paperless-backend/src/email-download/email-download.service.ts +++ b/paperless-backend/src/email-download/email-download.service.ts @@ -55,11 +55,62 @@ export class EmailDownloadService { } } + private createImapClient(): ImapFlow { + return new ImapFlow({ + host: this.configService.get('IMAP_HOST', ''), + port: this.configService.get('IMAP_PORT', 993), + secure: this.configService.get('IMAP_USE_SSL', 'true') === 'true', + auth: { + user: this.configService.get('IMAP_USERNAME', ''), + pass: this.configService.get('IMAP_PASSWORD', ''), + }, + logger: false, + }); + } + + @Cron('0 3 * * *', { timeZone: 'Europe/Berlin' }) + async cleanupImportedEmails(): Promise { + if (!this.configService.get('IMAP_HOST')) return; + const importedFolder = this.configService.get('IMAP_IMPORTED_FOLDER', 'importiert'); + const trashFolder = this.configService.get('IMAP_TRASH_FOLDER', 'Trash'); + const client = this.createImapClient(); + 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(() => {}); + } + } + private async fetchAndStore(): Promise { const host = this.configService.get('IMAP_HOST'); - const port = this.configService.get('IMAP_PORT', 993); - const secure = - this.configService.get('IMAP_USE_SSL', 'true') === 'true'; const user = this.configService.get('IMAP_USERNAME'); const pass = this.configService.get('IMAP_PASSWORD'); @@ -72,16 +123,10 @@ export class EmailDownloadService { this.logger.log('E-Mail Fetch Job gestartet.'); - const client = new ImapFlow({ - host, - port, - secure, - auth: { user, pass }, - logger: false, - }); + const client = this.createImapClient(); await client.connect(); - this.logger.log(`Verbunden mit IMAP-Server ${host}:${port}`); + this.logger.log(`Verbunden mit IMAP-Server ${host}.`); const lock = await client.getMailboxLock('INBOX'); try { diff --git a/paperless-backend/src/email/email-import.service.ts b/paperless-backend/src/email/email-import.service.ts index 022b167..cff63e0 100644 --- a/paperless-backend/src/email/email-import.service.ts +++ b/paperless-backend/src/email/email-import.service.ts @@ -12,6 +12,7 @@ import { Task } from '../database/entities/task.entity'; import { PaperlessService } from '../paperless/paperless.service'; import * as QRCode from 'qrcode'; import { EmailPageCacheService } from './email-page-cache.service'; +import { ImapFolderService } from './imap-folder.service'; import { PdfService } from '../preprocessing/pdf.service'; import * as path from 'path'; import * as os from 'os'; @@ -52,6 +53,7 @@ export class EmailImportService { private readonly paperlessService: PaperlessService, private readonly pdfService: PdfService, private readonly pageCache: EmailPageCacheService, + private readonly imapFolderService: ImapFolderService, ) {} async ensurePreviews(emailId: number): Promise { @@ -646,6 +648,12 @@ export class EmailImportService { this.logger.log( `Email ${firstAtt.EmailMessageId} als verarbeitet markiert.`, ); + const emailEntity = await this.emailRepo.findOne({ where: { Id: firstAtt.EmailMessageId } }); + if (emailEntity) { + this.imapFolderService.moveToImportiert(emailEntity.MessageId).catch(err => + this.logger.error('IMAP-Verschieben fehlgeschlagen: ' + err.message), + ); + } } } diff --git a/paperless-backend/src/email/email.module.ts b/paperless-backend/src/email/email.module.ts index 6b52873..bd202dc 100644 --- a/paperless-backend/src/email/email.module.ts +++ b/paperless-backend/src/email/email.module.ts @@ -9,6 +9,7 @@ import { EmailPageCacheService } from './email-page-cache.service'; import { EmailImportController } from './email-import.controller'; import { EmailImportService } from './email-import.service'; +import { ImapFolderService } from './imap-folder.service'; import { CorrespondentEmailMapping } from '../database/entities/correspondent-email-mapping.entity'; import { Task } from '../database/entities/task.entity'; import { PreprocessingModule } from '../preprocessing/preprocessing.module'; @@ -26,7 +27,7 @@ import { PreprocessingModule } from '../preprocessing/preprocessing.module'; PreprocessingModule, ], controllers: [EmailController, EmailImportController], - providers: [EmailImportService, EmailPageCacheService], + providers: [EmailImportService, EmailPageCacheService, ImapFolderService], exports: [EmailPageCacheService], }) export class EmailModule {} diff --git a/paperless-backend/src/email/imap-folder.service.ts b/paperless-backend/src/email/imap-folder.service.ts new file mode 100644 index 0000000..c4ff0c5 --- /dev/null +++ b/paperless-backend/src/email/imap-folder.service.ts @@ -0,0 +1,52 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +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('IMAP_HOST', ''), + port: this.configService.get('IMAP_PORT', 993), + secure: this.configService.get('IMAP_USE_SSL', 'true') === 'true', + auth: { + user: this.configService.get('IMAP_USERNAME', ''), + pass: this.configService.get('IMAP_PASSWORD', ''), + }, + logger: false, + }); + } + + async moveToImportiert(messageId: string): Promise { + if (!this.configService.get('IMAP_HOST')) return; + + const importedFolder = this.configService.get('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(() => {}); + } + } +}