7076eef57b
Build and Push Multi-Platform Images / build-and-push (push) Successful in 39s
- New useIsMobile hook and MobileCardList component - AppLayout: hamburger menu + Drawer navigation on mobile - All list pages (Inbox, Posteingang, Manuell, Mail, Freigabe, Zahlung, TaskLog) show card layout on mobile instead of tables - CSS: responsive modal height, horizontal table scroll, text-size-adjust - Backend: Agrarmonitor polling fixes, Zahlung service improvements, IMAP folder service extended, misc controller fixes Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
174 lines
5.6 KiB
TypeScript
174 lines
5.6 KiB
TypeScript
import { Injectable, Logger } from '@nestjs/common';
|
|
import { ConfigService } from '@nestjs/config';
|
|
import { Cron } from '@nestjs/schedule';
|
|
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,
|
|
});
|
|
}
|
|
|
|
@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;
|
|
|
|
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(() => {});
|
|
}
|
|
}
|
|
}
|