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> {
|
private async fetchAndStore(): Promise<void> {
|
||||||
const host = this.configService.get<string>('IMAP_HOST');
|
const host = this.configService.get<string>('IMAP_HOST');
|
||||||
const user = this.configService.get<string>('IMAP_USERNAME');
|
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 { Attachment } from '../database/entities/attachment.entity';
|
||||||
import { Content } from '../database/entities/content.entity';
|
import { Content } from '../database/entities/content.entity';
|
||||||
import { PaperlessService } from '../paperless/paperless.service';
|
import { PaperlessService } from '../paperless/paperless.service';
|
||||||
|
import { ImapFolderService } from './imap-folder.service';
|
||||||
import { RequirePermissions } from '../auth/permissions.decorator';
|
import { RequirePermissions } from '../auth/permissions.decorator';
|
||||||
import { Permission } from '../auth/permissions.enum';
|
import { Permission } from '../auth/permissions.enum';
|
||||||
|
|
||||||
@@ -31,6 +32,7 @@ export class EmailController {
|
|||||||
@InjectRepository(Content)
|
@InjectRepository(Content)
|
||||||
private readonly contentRepo: Repository<Content>,
|
private readonly contentRepo: Repository<Content>,
|
||||||
private readonly paperlessService: PaperlessService,
|
private readonly paperlessService: PaperlessService,
|
||||||
|
private readonly imapFolderService: ImapFolderService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
@@ -202,7 +204,21 @@ export class EmailController {
|
|||||||
this.logger.log(
|
this.logger.log(
|
||||||
`Prüfung abgeschlossen. ${updatedCount} E-Mails aktualisiert, ${idsUpdated} Paperless-IDs ergänzt, ${skippedCount} übersprungen.`,
|
`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) {
|
} catch (error: any) {
|
||||||
this.logger.error(
|
this.logger.error(
|
||||||
`Kritischer Fehler bei checkAttachments: ${error.message}`,
|
`Kritischer Fehler bei checkAttachments: ${error.message}`,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { Cron } from '@nestjs/schedule';
|
||||||
import { ImapFlow } from 'imapflow';
|
import { ImapFlow } from 'imapflow';
|
||||||
|
|
||||||
@Injectable()
|
@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> {
|
async moveToImportiert(messageId: string): Promise<void> {
|
||||||
if (!this.configService.get<string>('IMAP_HOST')) return;
|
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),
|
api.post<{ message: string }>('/api/emails/fetch').then((r) => r.data),
|
||||||
|
|
||||||
checkAttachments: (includeProcessed = false) =>
|
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) =>
|
updateStatus: (id: number, status: number) =>
|
||||||
api.patch<{ message?: string }>(`/api/emails/${id}/status`, { status }).then((r) => r.data),
|
api.patch<{ message?: string }>(`/api/emails/${id}/status`, { status }).then((r) => r.data),
|
||||||
|
|||||||
@@ -172,6 +172,7 @@ export default function MailpostfachPage() {
|
|||||||
const parts = [];
|
const parts = [];
|
||||||
if (result.updatedCount > 0) parts.push(`${result.updatedCount} E-Mail(s) aktualisiert`);
|
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.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.');
|
message.success(parts.length > 0 ? parts.join(', ') + '.' : 'Keine Änderungen.');
|
||||||
if (result.updatedCount > 0 || result.idsUpdated > 0) await loadData();
|
if (result.updatedCount > 0 || result.idsUpdated > 0) await loadData();
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
Reference in New Issue
Block a user