feat: importierte E-Mails automatisch in IMAP-Ordner verschieben und nach 90 Tagen löschen
Build and Push Multi-Platform Images / build-and-push (push) Successful in 31s
Build and Push Multi-Platform Images / build-and-push (push) Successful in 31s
- Neuer ImapFolderService verschiebt E-Mails nach erfolgreichem Import in den konfigurierbaren Ordner "importiert" (wird bei Bedarf automatisch erstellt) - Täglicher Cron um 03:00 Uhr verschiebt E-Mails älter als 90 Tage in den Papierkorb und leert ihn anschließend - createImapClient()-Hilfsmethode im EmailDownloadService ausgelagert - IMAP_IMPORTED_FOLDER und IMAP_TRASH_FOLDER in docker-compose ergänzt Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -36,6 +36,8 @@ services:
|
|||||||
- IMAP_USE_SSL=${IMAP_USE_SSL:-true}
|
- IMAP_USE_SSL=${IMAP_USE_SSL:-true}
|
||||||
- IMAP_USERNAME=${IMAP_USERNAME:-}
|
- IMAP_USERNAME=${IMAP_USERNAME:-}
|
||||||
- IMAP_PASSWORD=${IMAP_PASSWORD:-}
|
- 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_GET_URL=${BELEGNUMMER_GET_URL:-}
|
||||||
- BELEGNUMMER_SET_URL=${BELEGNUMMER_SET_URL:-}
|
- BELEGNUMMER_SET_URL=${BELEGNUMMER_SET_URL:-}
|
||||||
- AGRARMONITOR_BASE_URL=${AGRARMONITOR_BASE_URL:-https://admin7.agrarmonitor.de}
|
- AGRARMONITOR_BASE_URL=${AGRARMONITOR_BASE_URL:-https://admin7.agrarmonitor.de}
|
||||||
|
|||||||
@@ -55,11 +55,62 @@ export class EmailDownloadService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private createImapClient(): 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.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<void> {
|
private async fetchAndStore(): Promise<void> {
|
||||||
const host = this.configService.get<string>('IMAP_HOST');
|
const host = this.configService.get<string>('IMAP_HOST');
|
||||||
const port = this.configService.get<number>('IMAP_PORT', 993);
|
|
||||||
const secure =
|
|
||||||
this.configService.get<string>('IMAP_USE_SSL', 'true') === 'true';
|
|
||||||
const user = this.configService.get<string>('IMAP_USERNAME');
|
const user = this.configService.get<string>('IMAP_USERNAME');
|
||||||
const pass = this.configService.get<string>('IMAP_PASSWORD');
|
const pass = this.configService.get<string>('IMAP_PASSWORD');
|
||||||
|
|
||||||
@@ -72,16 +123,10 @@ export class EmailDownloadService {
|
|||||||
|
|
||||||
this.logger.log('E-Mail Fetch Job gestartet.');
|
this.logger.log('E-Mail Fetch Job gestartet.');
|
||||||
|
|
||||||
const client = new ImapFlow({
|
const client = this.createImapClient();
|
||||||
host,
|
|
||||||
port,
|
|
||||||
secure,
|
|
||||||
auth: { user, pass },
|
|
||||||
logger: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
await client.connect();
|
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');
|
const lock = await client.getMailboxLock('INBOX');
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { Task } from '../database/entities/task.entity';
|
|||||||
import { PaperlessService } from '../paperless/paperless.service';
|
import { PaperlessService } from '../paperless/paperless.service';
|
||||||
import * as QRCode from 'qrcode';
|
import * as QRCode from 'qrcode';
|
||||||
import { EmailPageCacheService } from './email-page-cache.service';
|
import { EmailPageCacheService } from './email-page-cache.service';
|
||||||
|
import { ImapFolderService } from './imap-folder.service';
|
||||||
import { PdfService } from '../preprocessing/pdf.service';
|
import { PdfService } from '../preprocessing/pdf.service';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as os from 'os';
|
import * as os from 'os';
|
||||||
@@ -52,6 +53,7 @@ export class EmailImportService {
|
|||||||
private readonly paperlessService: PaperlessService,
|
private readonly paperlessService: PaperlessService,
|
||||||
private readonly pdfService: PdfService,
|
private readonly pdfService: PdfService,
|
||||||
private readonly pageCache: EmailPageCacheService,
|
private readonly pageCache: EmailPageCacheService,
|
||||||
|
private readonly imapFolderService: ImapFolderService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async ensurePreviews(emailId: number): Promise<void> {
|
async ensurePreviews(emailId: number): Promise<void> {
|
||||||
@@ -646,6 +648,12 @@ export class EmailImportService {
|
|||||||
this.logger.log(
|
this.logger.log(
|
||||||
`Email ${firstAtt.EmailMessageId} als verarbeitet markiert.`,
|
`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),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { EmailPageCacheService } from './email-page-cache.service';
|
|||||||
|
|
||||||
import { EmailImportController } from './email-import.controller';
|
import { EmailImportController } from './email-import.controller';
|
||||||
import { EmailImportService } from './email-import.service';
|
import { EmailImportService } from './email-import.service';
|
||||||
|
import { ImapFolderService } from './imap-folder.service';
|
||||||
import { CorrespondentEmailMapping } from '../database/entities/correspondent-email-mapping.entity';
|
import { CorrespondentEmailMapping } from '../database/entities/correspondent-email-mapping.entity';
|
||||||
import { Task } from '../database/entities/task.entity';
|
import { Task } from '../database/entities/task.entity';
|
||||||
import { PreprocessingModule } from '../preprocessing/preprocessing.module';
|
import { PreprocessingModule } from '../preprocessing/preprocessing.module';
|
||||||
@@ -26,7 +27,7 @@ import { PreprocessingModule } from '../preprocessing/preprocessing.module';
|
|||||||
PreprocessingModule,
|
PreprocessingModule,
|
||||||
],
|
],
|
||||||
controllers: [EmailController, EmailImportController],
|
controllers: [EmailController, EmailImportController],
|
||||||
providers: [EmailImportService, EmailPageCacheService],
|
providers: [EmailImportService, EmailPageCacheService, ImapFolderService],
|
||||||
exports: [EmailPageCacheService],
|
exports: [EmailPageCacheService],
|
||||||
})
|
})
|
||||||
export class EmailModule {}
|
export class EmailModule {}
|
||||||
|
|||||||
@@ -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<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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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(() => {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user