chore: apply ESLint auto-fix across entire backend
Build and Push Multi-Platform Images / build-and-push (push) Successful in 41s
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:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user