feat: verarbeitete/ignorierte E-Mails beim Prüfen in IMAP-Ordner verschieben
Build and Push Multi-Platform Images / build-and-push (push) Successful in 38s
Build and Push Multi-Platform Images / build-and-push (push) Successful in 38s
- Cleanup-Cron von EmailDownloadService in ImapFolderService verschoben, damit er auch aus EmailController aufrufbar ist (zirkuläre Abhängigkeit vermieden) - Beim Klick auf „Anhänge prüfen" wird der IMAP-Cleanup fire-and-forget gestartet - Beim Klick auf „Bereits verarbeitete Anhänge prüfen" werden zusätzlich alle E-Mails im IMAP-Posteingang, die in der DB als verarbeitet (Status 1) oder ignoriert (Status 3) markiert sind, in den Ordner „importiert" verschoben - Erfolgsmeldung zeigt Anzahl verschobener E-Mails an Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -68,47 +68,6 @@ export class EmailDownloadService {
|
||||
});
|
||||
}
|
||||
|
||||
@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> {
|
||||
const host = this.configService.get<string>('IMAP_HOST');
|
||||
const user = this.configService.get<string>('IMAP_USERNAME');
|
||||
|
||||
@@ -17,6 +17,7 @@ import { Email } from '../database/entities/email.entity';
|
||||
import { Attachment } from '../database/entities/attachment.entity';
|
||||
import { Content } from '../database/entities/content.entity';
|
||||
import { PaperlessService } from '../paperless/paperless.service';
|
||||
import { ImapFolderService } from './imap-folder.service';
|
||||
import { RequirePermissions } from '../auth/permissions.decorator';
|
||||
import { Permission } from '../auth/permissions.enum';
|
||||
|
||||
@@ -31,6 +32,7 @@ export class EmailController {
|
||||
@InjectRepository(Content)
|
||||
private readonly contentRepo: Repository<Content>,
|
||||
private readonly paperlessService: PaperlessService,
|
||||
private readonly imapFolderService: ImapFolderService,
|
||||
) {}
|
||||
|
||||
@Get()
|
||||
@@ -202,7 +204,21 @@ export class EmailController {
|
||||
this.logger.log(
|
||||
`Prüfung abgeschlossen. ${updatedCount} E-Mails aktualisiert, ${idsUpdated} Paperless-IDs ergänzt, ${skippedCount} übersprungen.`,
|
||||
);
|
||||
return { updatedCount, idsUpdated };
|
||||
this.imapFolderService.cleanupImportedEmails().catch(err =>
|
||||
this.logger.error('IMAP-Cleanup fehlgeschlagen: ' + err.message),
|
||||
);
|
||||
|
||||
let movedToImportiert = 0;
|
||||
if (body.includeProcessed) {
|
||||
const processedEmails = await this.emailRepo.find({
|
||||
where: [{ Status: 1 }, { Status: 3 }],
|
||||
select: ['MessageId'],
|
||||
});
|
||||
const messageIds = processedEmails.map((e) => e.MessageId).filter(Boolean);
|
||||
movedToImportiert = await this.imapFolderService.moveProcessedInboxToImportiert(messageIds);
|
||||
}
|
||||
|
||||
return { updatedCount, idsUpdated, movedToImportiert };
|
||||
} catch (error: any) {
|
||||
this.logger.error(
|
||||
`Kritischer Fehler bei checkAttachments: ${error.message}`,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { Cron } from '@nestjs/schedule';
|
||||
import { ImapFlow } from 'imapflow';
|
||||
|
||||
@Injectable()
|
||||
@@ -21,6 +22,91 @@ export class ImapFolderService {
|
||||
});
|
||||
}
|
||||
|
||||
@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;
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ export const emailsApi = {
|
||||
api.post<{ message: string }>('/api/emails/fetch').then((r) => r.data),
|
||||
|
||||
checkAttachments: (includeProcessed = false) =>
|
||||
api.post<{ updatedCount: number; idsUpdated: number }>('/api/emails/check-attachments', { includeProcessed }).then((r) => r.data),
|
||||
api.post<{ updatedCount: number; idsUpdated: number; movedToImportiert: number }>('/api/emails/check-attachments', { includeProcessed }).then((r) => r.data),
|
||||
|
||||
updateStatus: (id: number, status: number) =>
|
||||
api.patch<{ message?: string }>(`/api/emails/${id}/status`, { status }).then((r) => r.data),
|
||||
|
||||
@@ -172,6 +172,7 @@ export default function MailpostfachPage() {
|
||||
const parts = [];
|
||||
if (result.updatedCount > 0) parts.push(`${result.updatedCount} E-Mail(s) aktualisiert`);
|
||||
if (result.idsUpdated > 0) parts.push(`${result.idsUpdated} Paperless-ID(s) ergänzt`);
|
||||
if (result.movedToImportiert > 0) parts.push(`${result.movedToImportiert} E-Mail(s) in „importiert" verschoben`);
|
||||
message.success(parts.length > 0 ? parts.join(', ') + '.' : 'Keine Änderungen.');
|
||||
if (result.updatedCount > 0 || result.idsUpdated > 0) await loadData();
|
||||
} catch {
|
||||
|
||||
Reference in New Issue
Block a user