feat: auto-move imported emails to IMAP folder and add 90-day cleanup
Build and Push Multi-Platform Images / build-and-push (push) Successful in 41s

- New ImapFolderService moves emails to configurable "importiert" folder
  after successful import, creating the folder if it doesn't exist
- Daily cron at 03:00 moves emails older than 90 days to trash and empties it
- Extract createImapClient() helper in EmailDownloadService
- Add ensurePageCache() with in-flight deduplication to BarcodeScannerService
- InboxService regenerates page cache on-demand when image file is missing
- IMAP_IMPORTED_FOLDER and IMAP_TRASH_FOLDER added to .env.example and docker-compose

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-16 13:53:56 +02:00
parent 07dfd7e840
commit b1b30fe1dd
8 changed files with 172 additions and 14 deletions
@@ -56,11 +56,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();
// Alte E-Mails (> 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 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 pass = this.configService.get<string>('IMAP_PASSWORD');
@@ -73,16 +124,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 {