chore: apply ESLint auto-fix across entire backend
Build and Push Multi-Platform Images / build-and-push (push) Successful in 41s

Reformats code style (line breaks, indentation, type annotations)
without changing logic. Also includes minor feature additions bundled
in the same lint run (stats service, user-settings groups, agrarmonitor
polling improvements).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-08 09:02:02 +02:00
parent 4c75a1ded2
commit dad0136365
74 changed files with 4022 additions and 1052 deletions
@@ -4,7 +4,11 @@ import { Cron, CronExpression } from '@nestjs/schedule';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { ImapFlow, type FetchMessageObject } from 'imapflow';
import { simpleParser, type AddressObject, type Attachment as MailAttachment } from 'mailparser';
import {
simpleParser,
type AddressObject,
type Attachment as MailAttachment,
} from 'mailparser';
import * as crypto from 'crypto';
import * as os from 'os';
import * as path from 'path';
@@ -26,8 +30,10 @@ export class EmailDownloadService {
private readonly pdfService: PdfService,
private readonly pageCache: EmailPageCacheService,
@InjectRepository(Email) private readonly emailRepo: Repository<Email>,
@InjectRepository(Attachment) private readonly attachmentRepo: Repository<Attachment>,
@InjectRepository(Content) private readonly contentRepo: Repository<Content>,
@InjectRepository(Attachment)
private readonly attachmentRepo: Repository<Attachment>,
@InjectRepository(Content)
private readonly contentRepo: Repository<Content>,
) {}
@Cron(CronExpression.EVERY_5_MINUTES)
@@ -40,7 +46,10 @@ export class EmailDownloadService {
try {
await this.fetchAndStore();
} catch (err: any) {
this.logger.error(`Fehler im E-Mail-Download-Job: ${err.message}`, err.stack);
this.logger.error(
`Fehler im E-Mail-Download-Job: ${err.message}`,
err.stack,
);
} finally {
this.running = false;
}
@@ -49,12 +58,15 @@ export class EmailDownloadService {
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 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');
if (!host || !user || !pass) {
this.logger.warn('IMAP-Konfiguration unvollständig Job wird übersprungen.');
this.logger.warn(
'IMAP-Konfiguration unvollständig Job wird übersprungen.',
);
return;
}
@@ -74,35 +86,47 @@ export class EmailDownloadService {
const lock = await client.getMailboxLock('INBOX');
try {
const status = await client.status('INBOX', { messages: true });
this.logger.log(`Posteingang geöffnet. Anzahl der Nachrichten: ${status.messages ?? 0}`);
this.logger.log(
`Posteingang geöffnet. Anzahl der Nachrichten: ${status.messages ?? 0}`,
);
if (!status.messages || status.messages === 0) {
return;
}
const iter = client.fetch(
'1:*',
{ envelope: true, uid: true, source: true },
);
const iter = client.fetch('1:*', {
envelope: true,
uid: true,
source: true,
});
for await (const msg of iter) {
const messageId = msg.envelope?.messageId;
if (!messageId) continue;
try {
const existing = await this.emailRepo.findOne({ where: { MessageId: messageId } });
const existing = await this.emailRepo.findOne({
where: { MessageId: messageId },
});
if (existing) {
this.logger.debug(`E-Mail mit MessageId ${messageId} bereits vorhanden.`);
this.logger.debug(
`E-Mail mit MessageId ${messageId} bereits vorhanden.`,
);
continue;
}
await this.processMessage(msg);
} catch (err: any) {
this.logger.error(`Fehler beim Abrufen der Nachricht ${messageId}: ${err.message}`, err.stack);
this.logger.error(
`Fehler beim Abrufen der Nachricht ${messageId}: ${err.message}`,
err.stack,
);
}
}
this.logger.log('Alle neuen E-Mails und deren Anhänge wurden in der Datenbank gespeichert.');
this.logger.log(
'Alle neuen E-Mails und deren Anhänge wurden in der Datenbank gespeichert.',
);
} finally {
lock.release();
await client.logout().catch(() => undefined);
@@ -119,12 +143,18 @@ export class EmailDownloadService {
email.MessageId = messageId;
email.SenderAddress = formatAddress(parsed.from);
email.RecipientAddress = formatAddress(parsed.to);
email.Subject = (msg.envelope?.subject ?? parsed.subject ?? '').slice(0, 500);
email.Subject = (msg.envelope?.subject ?? parsed.subject ?? '').slice(
0,
500,
);
email.Date = msg.envelope?.date ?? parsed.date ?? new Date();
email.Body = parsed.html || parsed.text || '';
email.Status = 0;
const attachmentsToPersist: Array<{ attachment: Attachment; buffer: Buffer }> = [];
const attachmentsToPersist: Array<{
attachment: Attachment;
buffer: Buffer;
}> = [];
for (const att of parsed.attachments) {
const entry = await this.buildAttachment(att);
@@ -132,9 +162,13 @@ export class EmailDownloadService {
}
// Double-Check: nochmal gegen DB prüfen (Race-Condition-Schutz wie in C#)
const existing2 = await this.emailRepo.findOne({ where: { MessageId: messageId } });
const existing2 = await this.emailRepo.findOne({
where: { MessageId: messageId },
});
if (existing2) {
this.logger.debug(`E-Mail mit MessageId ${messageId} nach dem Download bereits vorhanden.`);
this.logger.debug(
`E-Mail mit MessageId ${messageId} nach dem Download bereits vorhanden.`,
);
return;
}
@@ -152,55 +186,79 @@ export class EmailDownloadService {
// Generate PDF thumbnails if it's a PDF
if (savedAttachment.ContentType === 'application/pdf') {
await this.generateThumbnailsForAttachment(savedAttachment, buffer);
await this.generateThumbnailsForAttachment(savedAttachment, buffer);
}
}
this.logger.debug(`Neue E-Mail mit MessageId ${messageId} hinzugefügt (${attachmentsToPersist.length} Anhänge).`);
this.logger.debug(
`Neue E-Mail mit MessageId ${messageId} hinzugefügt (${attachmentsToPersist.length} Anhänge).`,
);
}
public async generateThumbnailsForAttachment(attachment: Attachment, buffer: Buffer): Promise<void> {
try {
const tempPdfPath = path.join(os.tmpdir(), `email-att-${attachment.Id}.pdf`);
await fs.writeFile(tempPdfPath, buffer);
const images = await this.pdfService.pdfToImages(tempPdfPath, 400);
await this.pageCache.generate(attachment.Id, images);
attachment.PageCount = images.length;
await this.attachmentRepo.save(attachment);
await this.pdfService.cleanup(images);
await fs.unlink(tempPdfPath).catch(() => {});
} catch (err: any) {
this.logger.warn(`Konnte Vorschaubilder für Anhang ${attachment.Id} nicht generieren: ${err.message}`);
}
public async generateThumbnailsForAttachment(
attachment: Attachment,
buffer: Buffer,
): Promise<void> {
try {
const tempPdfPath = path.join(
os.tmpdir(),
`email-att-${attachment.Id}.pdf`,
);
await fs.writeFile(tempPdfPath, buffer);
const images = await this.pdfService.pdfToImages(tempPdfPath, 400);
await this.pageCache.generate(attachment.Id, images);
attachment.PageCount = images.length;
await this.attachmentRepo.save(attachment);
await this.pdfService.cleanup(images);
await fs.unlink(tempPdfPath).catch(() => {});
} catch (err: any) {
this.logger.warn(
`Konnte Vorschaubilder für Anhang ${attachment.Id} nicht generieren: ${err.message}`,
);
}
}
public async backfillThumbnailsForNewEmails(): Promise<{ processed: number; failed: number }> {
public async backfillThumbnailsForNewEmails(): Promise<{
processed: number;
failed: number;
}> {
const emails = await this.emailRepo.find({
where: { Status: 0 },
relations: ['Attachments', 'Attachments.Content']
where: { Status: 0 },
relations: ['Attachments', 'Attachments.Content'],
});
let processed = 0;
let failed = 0;
for (const email of emails) {
for (const attachment of email.Attachments) {
if (attachment.ContentType === 'application/pdf' && attachment.PageCount === 0 && attachment.Content?.Content1) {
this.logger.log(`Backfill: Generiere Thumbnails für Attachment ${attachment.Id} (Email ${email.Id})`);
try {
await this.generateThumbnailsForAttachment(attachment, attachment.Content.Content1);
processed++;
} catch (err) {
failed++;
}
for (const attachment of email.Attachments) {
if (
attachment.ContentType === 'application/pdf' &&
attachment.PageCount === 0 &&
attachment.Content?.Content1
) {
this.logger.log(
`Backfill: Generiere Thumbnails für Attachment ${attachment.Id} (Email ${email.Id})`,
);
try {
await this.generateThumbnailsForAttachment(
attachment,
attachment.Content.Content1,
);
processed++;
} catch (err) {
failed++;
}
}
}
}
}
this.logger.log(`Backfill abgeschlossen: ${processed} erfolgreich, ${failed} fehlgeschlagen.`);
this.logger.log(
`Backfill abgeschlossen: ${processed} erfolgreich, ${failed} fehlgeschlagen.`,
);
return { processed, failed };
}
@@ -234,14 +292,19 @@ export class EmailDownloadService {
attachment.IsEmbedded = isEmbedded;
attachment.ContentId = att.cid ? att.cid.slice(0, 255) : null;
attachment.Checksum = crypto.createHash('md5').update(buffer).digest('hex');
attachment.Erechnung = contentType.toLowerCase() === 'application/pdf' ? isERechnung(buffer) : false;
attachment.Erechnung =
contentType.toLowerCase() === 'application/pdf'
? isERechnung(buffer)
: false;
attachment.ParentId = null;
return { attachment, buffer };
}
}
function formatAddress(addr: AddressObject | AddressObject[] | undefined): string {
function formatAddress(
addr: AddressObject | AddressObject[] | undefined,
): string {
if (!addr) return '';
const first = Array.isArray(addr) ? addr[0] : addr;
return (first?.text ?? '').slice(0, 255);