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:
@@ -1,4 +1,16 @@
|
||||
import { Controller, Get, Post, Body, Param, Query, Res, HttpException, HttpStatus, Logger, Delete } from '@nestjs/common';
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Body,
|
||||
Param,
|
||||
Query,
|
||||
Res,
|
||||
HttpException,
|
||||
HttpStatus,
|
||||
Logger,
|
||||
Delete,
|
||||
} from '@nestjs/common';
|
||||
import type { Response } from 'express';
|
||||
import { EmailImportService } from './email-import.service';
|
||||
import { EmailPageCacheService } from './email-page-cache.service';
|
||||
@@ -23,11 +35,19 @@ export class EmailImportController {
|
||||
|
||||
@Post('mappings')
|
||||
@RequirePermissions(Permission.MANAGE_SETTINGS)
|
||||
async addMapping(@Body() body: { emailAddress: string; paperlessCorrespondentId: number }) {
|
||||
async addMapping(
|
||||
@Body() body: { emailAddress: string; paperlessCorrespondentId: number },
|
||||
) {
|
||||
if (!body.emailAddress || !body.paperlessCorrespondentId) {
|
||||
throw new HttpException('Missing emailAddress or paperlessCorrespondentId', HttpStatus.BAD_REQUEST);
|
||||
throw new HttpException(
|
||||
'Missing emailAddress or paperlessCorrespondentId',
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
return this.importService.addMapping(body.emailAddress, body.paperlessCorrespondentId);
|
||||
return this.importService.addMapping(
|
||||
body.emailAddress,
|
||||
body.paperlessCorrespondentId,
|
||||
);
|
||||
}
|
||||
|
||||
@Delete('mappings/:id')
|
||||
@@ -54,7 +74,11 @@ export class EmailImportController {
|
||||
@Get('belegnummer')
|
||||
@RequirePermissions(Permission.VIEW_MAIL)
|
||||
async getBelegnummer(@Query('date') date: string) {
|
||||
if (!date) throw new HttpException('Date query parameter required', HttpStatus.BAD_REQUEST);
|
||||
if (!date)
|
||||
throw new HttpException(
|
||||
'Date query parameter required',
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
const nummer = await this.importService.getBelegnummer(date);
|
||||
return { nummer };
|
||||
}
|
||||
@@ -62,7 +86,11 @@ export class EmailImportController {
|
||||
@Post('belegnummer/release')
|
||||
@RequirePermissions(Permission.VIEW_MAIL)
|
||||
async releaseBelegnummer(@Body() body: { date: string; number: string }) {
|
||||
if (!body.date || !body.number) throw new HttpException('Date and number required', HttpStatus.BAD_REQUEST);
|
||||
if (!body.date || !body.number)
|
||||
throw new HttpException(
|
||||
'Date and number required',
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
await this.importService.releaseBelegnummer(body.date, body.number);
|
||||
return { success: true };
|
||||
}
|
||||
@@ -74,7 +102,10 @@ export class EmailImportController {
|
||||
@Param('attachmentId') attachmentId: number,
|
||||
@Body() body: { pages: { start: number; end: number } },
|
||||
) {
|
||||
const isDuplicate = await this.importService.checkSplitChecksum(attachmentId, body.pages);
|
||||
const isDuplicate = await this.importService.checkSplitChecksum(
|
||||
attachmentId,
|
||||
body.pages,
|
||||
);
|
||||
return { isDuplicate };
|
||||
}
|
||||
|
||||
@@ -87,10 +118,18 @@ export class EmailImportController {
|
||||
@Res() res: Response,
|
||||
) {
|
||||
try {
|
||||
const pdfBuffer = await this.importService.generatePrintPdf(attachmentId, barcodeData);
|
||||
this.logger.log(`Print preview generated for attachment ${attachmentId}: ${pdfBuffer.length} bytes`);
|
||||
const pdfBuffer = await this.importService.generatePrintPdf(
|
||||
attachmentId,
|
||||
barcodeData,
|
||||
);
|
||||
this.logger.log(
|
||||
`Print preview generated for attachment ${attachmentId}: ${pdfBuffer.length} bytes`,
|
||||
);
|
||||
res.setHeader('Content-Type', 'application/pdf');
|
||||
res.setHeader('Content-Disposition', `inline; filename="preview-${attachmentId}.pdf"`);
|
||||
res.setHeader(
|
||||
'Content-Disposition',
|
||||
`inline; filename="preview-${attachmentId}.pdf"`,
|
||||
);
|
||||
res.send(pdfBuffer);
|
||||
} catch (err: any) {
|
||||
this.logger.error(`Error generating print preview: ${err.message}`);
|
||||
@@ -134,7 +173,9 @@ export class EmailImportController {
|
||||
@Get('jobs/:jobId/status')
|
||||
@RequirePermissions(Permission.VIEW_MAIL)
|
||||
getJobStatus(@Param('jobId') jobId: string) {
|
||||
return this.importService.getJobStatus(jobId) ?? { message: '', done: false };
|
||||
return (
|
||||
this.importService.getJobStatus(jobId) ?? { message: '', done: false }
|
||||
);
|
||||
}
|
||||
|
||||
// --- Final Import ---
|
||||
|
||||
@@ -21,9 +21,16 @@ import * as crypto from 'crypto';
|
||||
@Injectable()
|
||||
export class EmailImportService {
|
||||
private readonly logger = new Logger(EmailImportService.name);
|
||||
private readonly importJobs = new Map<string, { message: string; done: boolean }>();
|
||||
private readonly importJobs = new Map<
|
||||
string,
|
||||
{ message: string; done: boolean }
|
||||
>();
|
||||
|
||||
private setJobStatus(jobId: string | undefined, message: string, done = false): void {
|
||||
private setJobStatus(
|
||||
jobId: string | undefined,
|
||||
message: string,
|
||||
done = false,
|
||||
): void {
|
||||
if (!jobId) return;
|
||||
this.importJobs.set(jobId, { message, done });
|
||||
}
|
||||
@@ -35,9 +42,12 @@ export class EmailImportService {
|
||||
constructor(
|
||||
private readonly configService: ConfigService,
|
||||
@InjectRepository(Email) private readonly emailRepo: Repository<Email>,
|
||||
@InjectRepository(Attachment) private readonly attachmentRepo: Repository<Attachment>,
|
||||
@InjectRepository(Content) private readonly contentRepo: Repository<Content>,
|
||||
@InjectRepository(CorrespondentEmailMapping) private readonly mappingRepo: Repository<CorrespondentEmailMapping>,
|
||||
@InjectRepository(Attachment)
|
||||
private readonly attachmentRepo: Repository<Attachment>,
|
||||
@InjectRepository(Content)
|
||||
private readonly contentRepo: Repository<Content>,
|
||||
@InjectRepository(CorrespondentEmailMapping)
|
||||
private readonly mappingRepo: Repository<CorrespondentEmailMapping>,
|
||||
@InjectRepository(Task) private readonly taskRepo: Repository<Task>,
|
||||
private readonly paperlessService: PaperlessService,
|
||||
private readonly pdfService: PdfService,
|
||||
@@ -53,8 +63,13 @@ export class EmailImportService {
|
||||
for (const attachment of attachments) {
|
||||
const hasPreview = await this.pageCache.hasPreview(attachment.Id, 1);
|
||||
if (!hasPreview && attachment.Content?.Content1) {
|
||||
this.logger.log(`Generiere fehlende Vorschaubilder für Anhang ${attachment.Id} (Email ${emailId})`);
|
||||
const tempPdfPath = path.join(os.tmpdir(), `email-att-gen-${attachment.Id}.pdf`);
|
||||
this.logger.log(
|
||||
`Generiere fehlende Vorschaubilder für Anhang ${attachment.Id} (Email ${emailId})`,
|
||||
);
|
||||
const tempPdfPath = path.join(
|
||||
os.tmpdir(),
|
||||
`email-att-gen-${attachment.Id}.pdf`,
|
||||
);
|
||||
try {
|
||||
await fs.writeFile(tempPdfPath, attachment.Content.Content1);
|
||||
|
||||
@@ -66,7 +81,9 @@ export class EmailImportService {
|
||||
|
||||
await this.pdfService.cleanup(images);
|
||||
} catch (err: any) {
|
||||
this.logger.warn(`Fehler bei on-demand Vorschau-Generierung für Anhang ${attachment.Id}: ${err.message}`);
|
||||
this.logger.warn(
|
||||
`Fehler bei on-demand Vorschau-Generierung für Anhang ${attachment.Id}: ${err.message}`,
|
||||
);
|
||||
} finally {
|
||||
await fs.unlink(tempPdfPath).catch(() => {});
|
||||
}
|
||||
@@ -80,9 +97,14 @@ export class EmailImportService {
|
||||
}
|
||||
|
||||
async addMapping(emailAddress: string, paperlessCorrespondentId: number) {
|
||||
let mapping = await this.mappingRepo.findOne({ where: { EmailAddress: emailAddress } });
|
||||
let mapping = await this.mappingRepo.findOne({
|
||||
where: { EmailAddress: emailAddress },
|
||||
});
|
||||
if (!mapping) {
|
||||
mapping = this.mappingRepo.create({ EmailAddress: emailAddress, PaperlessCorrespondentId: paperlessCorrespondentId });
|
||||
mapping = this.mappingRepo.create({
|
||||
EmailAddress: emailAddress,
|
||||
PaperlessCorrespondentId: paperlessCorrespondentId,
|
||||
});
|
||||
} else {
|
||||
mapping.PaperlessCorrespondentId = paperlessCorrespondentId;
|
||||
}
|
||||
@@ -94,114 +116,177 @@ export class EmailImportService {
|
||||
}
|
||||
|
||||
async getCorrespondentByEmail(emailAddress: string): Promise<number | null> {
|
||||
const mapping = await this.mappingRepo.findOne({ where: { EmailAddress: emailAddress } });
|
||||
const mapping = await this.mappingRepo.findOne({
|
||||
where: { EmailAddress: emailAddress },
|
||||
});
|
||||
return mapping ? mapping.PaperlessCorrespondentId : null;
|
||||
}
|
||||
|
||||
// --- Belegnummern API ---
|
||||
private buildUrl(urlTemplate: string, dateStr: string): string {
|
||||
const dateObj = new Date(dateStr);
|
||||
const year = (isNaN(dateObj.getTime()) ? new Date() : dateObj).getFullYear().toString();
|
||||
const year = (isNaN(dateObj.getTime()) ? new Date() : dateObj)
|
||||
.getFullYear()
|
||||
.toString();
|
||||
return urlTemplate.replace('{Jahr}', year);
|
||||
}
|
||||
|
||||
async getBelegnummer(emailDate: string): Promise<string> {
|
||||
const urlTemplate = this.configService.get<string>('BELEGNUMMER_GET_URL');
|
||||
if (!urlTemplate) throw new HttpException('BELEGNUMMER_GET_URL not configured', HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
if (!urlTemplate)
|
||||
throw new HttpException(
|
||||
'BELEGNUMMER_GET_URL not configured',
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
|
||||
const url = this.buildUrl(urlTemplate, emailDate);
|
||||
try {
|
||||
this.logger.debug(`Fetching Belegnummer from ${url}`);
|
||||
const response = await axios.get(url);
|
||||
|
||||
|
||||
// If the response is an object, try to extract 'nummer' or 'number'
|
||||
let result = response.data;
|
||||
if (result && typeof result === 'object') {
|
||||
result = result.nummer || result.number || result.data?.nummer || JSON.stringify(result);
|
||||
result =
|
||||
result.nummer ||
|
||||
result.number ||
|
||||
result.data?.nummer ||
|
||||
JSON.stringify(result);
|
||||
}
|
||||
|
||||
|
||||
this.logger.debug(`Received Belegnummer: ${result}`);
|
||||
return String(result);
|
||||
} catch (error: any) {
|
||||
const status = error.response?.status || 'UNKNOWN';
|
||||
const detail = error.response?.data ? JSON.stringify(error.response.data) : error.message;
|
||||
this.logger.error(`Failed to fetch Belegnummer from ${url}. Status: ${status}, Detail: ${detail}`);
|
||||
throw new HttpException(`Fehler beim Abrufen der Belegnummer: ${detail}`, HttpStatus.BAD_GATEWAY);
|
||||
const detail = error.response?.data
|
||||
? JSON.stringify(error.response.data)
|
||||
: error.message;
|
||||
this.logger.error(
|
||||
`Failed to fetch Belegnummer from ${url}. Status: ${status}, Detail: ${detail}`,
|
||||
);
|
||||
throw new HttpException(
|
||||
`Fehler beim Abrufen der Belegnummer: ${detail}`,
|
||||
HttpStatus.BAD_GATEWAY,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async releaseBelegnummer(emailDate: string, number: string): Promise<void> {
|
||||
const urlTemplate = this.configService.get<string>('BELEGNUMMER_RELEASE_URL');
|
||||
const urlTemplate = this.configService.get<string>(
|
||||
'BELEGNUMMER_RELEASE_URL',
|
||||
);
|
||||
if (!urlTemplate) {
|
||||
this.logger.warn('BELEGNUMMER_RELEASE_URL not configured, skipping release.');
|
||||
this.logger.warn(
|
||||
'BELEGNUMMER_RELEASE_URL not configured, skipping release.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const cleanNumber = number.replace(/^0+/, '') || '0';
|
||||
let url = this.buildUrl(urlTemplate, emailDate);
|
||||
url = url.replace('{Nummer}', cleanNumber);
|
||||
|
||||
|
||||
try {
|
||||
this.logger.log(`Releasing Belegnummer: ${cleanNumber} (original: ${number}) via ${url}`);
|
||||
this.logger.log(
|
||||
`Releasing Belegnummer: ${cleanNumber} (original: ${number}) via ${url}`,
|
||||
);
|
||||
await axios.get(url);
|
||||
} catch (error: any) {
|
||||
this.logger.error(`Failed to release Belegnummer at ${url}: ${error.message}`);
|
||||
this.logger.error(
|
||||
`Failed to release Belegnummer at ${url}: ${error.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async setBelegnummer(emailDate: string, number: string): Promise<void> {
|
||||
const urlTemplate = this.configService.get<string>('BELEGNUMMER_SET_URL');
|
||||
if (!urlTemplate) throw new HttpException('BELEGNUMMER_SET_URL not configured', HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
if (!urlTemplate)
|
||||
throw new HttpException(
|
||||
'BELEGNUMMER_SET_URL not configured',
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
|
||||
const cleanNumber = number.replace(/^0+/, '') || '0';
|
||||
let url = this.buildUrl(urlTemplate, emailDate);
|
||||
url = url.replace('{Nummer}', cleanNumber);
|
||||
|
||||
|
||||
try {
|
||||
this.logger.log(`Setting Belegnummer: ${cleanNumber} (original: ${number}) via ${url}`);
|
||||
this.logger.log(
|
||||
`Setting Belegnummer: ${cleanNumber} (original: ${number}) via ${url}`,
|
||||
);
|
||||
await axios.get(url);
|
||||
} catch (error: any) {
|
||||
this.logger.error(`Failed to set Belegnummer at ${url}: ${error.message}`);
|
||||
throw new HttpException('Fehler beim Setzen der Belegnummer', HttpStatus.BAD_GATEWAY);
|
||||
this.logger.error(
|
||||
`Failed to set Belegnummer at ${url}: ${error.message}`,
|
||||
);
|
||||
throw new HttpException(
|
||||
'Fehler beim Setzen der Belegnummer',
|
||||
HttpStatus.BAD_GATEWAY,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Checksum Check for Split Documents ---
|
||||
async checkSplitChecksum(attachmentId: number, pages: { start: number; end: number }): Promise<boolean> {
|
||||
const content = await this.contentRepo.findOne({ where: { AttachmentEntityId: attachmentId } });
|
||||
async checkSplitChecksum(
|
||||
attachmentId: number,
|
||||
pages: { start: number; end: number },
|
||||
): Promise<boolean> {
|
||||
const content = await this.contentRepo.findOne({
|
||||
where: { AttachmentEntityId: attachmentId },
|
||||
});
|
||||
if (!content) return false;
|
||||
|
||||
const pdfDoc = await PDFDocument.load(content.Content1, { ignoreEncryption: true });
|
||||
const pdfDoc = await PDFDocument.load(content.Content1, {
|
||||
ignoreEncryption: true,
|
||||
});
|
||||
const total = pdfDoc.getPageCount();
|
||||
const startIdx = Math.max(1, pages.start) - 1;
|
||||
const endIdx = Math.min(pages.end === 999 ? total : pages.end, total) - 1;
|
||||
|
||||
const sliced = await PDFDocument.create();
|
||||
const indices = Array.from({ length: endIdx - startIdx + 1 }, (_, i) => startIdx + i);
|
||||
const indices = Array.from(
|
||||
{ length: endIdx - startIdx + 1 },
|
||||
(_, i) => startIdx + i,
|
||||
);
|
||||
const copied = await sliced.copyPages(pdfDoc, indices);
|
||||
copied.forEach(p => sliced.addPage(p));
|
||||
copied.forEach((p) => sliced.addPage(p));
|
||||
|
||||
const checksum = crypto.createHash('md5').update(Buffer.from(await sliced.save())).digest('hex');
|
||||
const checksum = crypto
|
||||
.createHash('md5')
|
||||
.update(Buffer.from(await sliced.save()))
|
||||
.digest('hex');
|
||||
return this.paperlessService.checksumExists(checksum);
|
||||
}
|
||||
|
||||
// --- Print Preview ---
|
||||
async generatePrintPdf(attachmentId: number, barcodeData: any): Promise<Buffer> {
|
||||
const content = await this.contentRepo.findOne({ where: { AttachmentEntityId: attachmentId } });
|
||||
if (!content) throw new HttpException('Inhalt nicht gefunden', HttpStatus.NOT_FOUND);
|
||||
async generatePrintPdf(
|
||||
attachmentId: number,
|
||||
barcodeData: any,
|
||||
): Promise<Buffer> {
|
||||
const content = await this.contentRepo.findOne({
|
||||
where: { AttachmentEntityId: attachmentId },
|
||||
});
|
||||
if (!content)
|
||||
throw new HttpException('Inhalt nicht gefunden', HttpStatus.NOT_FOUND);
|
||||
|
||||
let pdfBytes: Buffer = content.Content1;
|
||||
|
||||
const pages: { start: number; end: number } | undefined = barcodeData._pages;
|
||||
const pages: { start: number; end: number } | undefined =
|
||||
barcodeData._pages;
|
||||
if (pages) {
|
||||
const pdfDoc = await PDFDocument.load(pdfBytes, { ignoreEncryption: true });
|
||||
const pdfDoc = await PDFDocument.load(pdfBytes, {
|
||||
ignoreEncryption: true,
|
||||
});
|
||||
const total = pdfDoc.getPageCount();
|
||||
const startIdx = Math.max(1, pages.start) - 1;
|
||||
const endIdx = Math.min(pages.end === 999 ? total : pages.end, total) - 1;
|
||||
const sliced = await PDFDocument.create();
|
||||
const indices = Array.from({ length: endIdx - startIdx + 1 }, (_, i) => startIdx + i);
|
||||
const indices = Array.from(
|
||||
{ length: endIdx - startIdx + 1 },
|
||||
(_, i) => startIdx + i,
|
||||
);
|
||||
const copied = await sliced.copyPages(pdfDoc, indices);
|
||||
copied.forEach(p => sliced.addPage(p));
|
||||
copied.forEach((p) => sliced.addPage(p));
|
||||
pdfBytes = Buffer.from(await sliced.save());
|
||||
}
|
||||
|
||||
@@ -210,18 +295,24 @@ export class EmailImportService {
|
||||
}
|
||||
|
||||
async applyBarcodeToPdf(pdfBytes: Buffer, barcodeData: any): Promise<Buffer> {
|
||||
this.logger.debug(`applyBarcodeToPdf: Input size = ${pdfBytes.length} bytes`);
|
||||
|
||||
this.logger.debug(
|
||||
`applyBarcodeToPdf: Input size = ${pdfBytes.length} bytes`,
|
||||
);
|
||||
|
||||
let currentPdfBytes = pdfBytes;
|
||||
const tempInputPath = path.join(os.tmpdir(), `input-${Date.now()}.pdf`);
|
||||
await fs.writeFile(tempInputPath, pdfBytes);
|
||||
|
||||
try {
|
||||
// First try to load to check encryption
|
||||
let pdfDoc = await PDFDocument.load(currentPdfBytes, { ignoreEncryption: true });
|
||||
|
||||
let pdfDoc = await PDFDocument.load(currentPdfBytes, {
|
||||
ignoreEncryption: true,
|
||||
});
|
||||
|
||||
if (pdfDoc.isEncrypted) {
|
||||
this.logger.log('PDF ist verschlüsselt, versuche Bereinigung via Ghostscript...');
|
||||
this.logger.log(
|
||||
'PDF ist verschlüsselt, versuche Bereinigung via Ghostscript...',
|
||||
);
|
||||
const sanitizedPath = await this.pdfService.sanitizePdf(tempInputPath);
|
||||
currentPdfBytes = await fs.readFile(sanitizedPath);
|
||||
await fs.unlink(sanitizedPath).catch(() => {});
|
||||
@@ -230,107 +321,128 @@ export class EmailImportService {
|
||||
}
|
||||
|
||||
const pages = pdfDoc.getPages();
|
||||
this.logger.debug(`applyBarcodeToPdf: Pages = ${pages.length}, Encrypted = ${pdfDoc.isEncrypted}`);
|
||||
|
||||
this.logger.debug(
|
||||
`applyBarcodeToPdf: Pages = ${pages.length}, Encrypted = ${pdfDoc.isEncrypted}`,
|
||||
);
|
||||
|
||||
if (pages.length === 0) {
|
||||
this.logger.warn('applyBarcodeToPdf: Keine Seiten gefunden');
|
||||
return Buffer.from(await pdfDoc.save());
|
||||
}
|
||||
|
||||
|
||||
const firstPage = pages[0];
|
||||
|
||||
const { x, y, nummer, datum, jahr } = barcodeData;
|
||||
|
||||
// Parse date
|
||||
const d = new Date(datum);
|
||||
const yyyy = (isNaN(d.getTime()) ? new Date() : d).getFullYear().toString();
|
||||
const mm = String((isNaN(d.getTime()) ? new Date() : d).getMonth() + 1).padStart(2, '0');
|
||||
const dd = String((isNaN(d.getTime()) ? new Date() : d).getDate()).padStart(2, '0');
|
||||
|
||||
const qrDateStr = `${yyyy}${mm}${dd}`; // yyyyMMdd
|
||||
const qrContent = `${String(jahr).padStart(4, '0')}-${String(nummer).padStart(6, '0')}-${qrDateStr}`;
|
||||
const printDateStr = `${dd}.${mm}.${yyyy}`;
|
||||
const { x, y, nummer, datum, jahr } = barcodeData;
|
||||
|
||||
// Dimensions: 57x32 mm
|
||||
const PT_PER_MM = 2.83465;
|
||||
const boxW = 57 * PT_PER_MM;
|
||||
const boxH = 32 * PT_PER_MM;
|
||||
|
||||
// A4 dimensions: 210x297 mm
|
||||
const PAGE_H_PT = 297 * PT_PER_MM;
|
||||
|
||||
// Convert mm to points (Y is from bottom in pdf-lib)
|
||||
const startX = Number(x) * PT_PER_MM;
|
||||
const startY = PAGE_H_PT - (Number(y) * PT_PER_MM) - boxH;
|
||||
// Parse date
|
||||
const d = new Date(datum);
|
||||
const yyyy = (isNaN(d.getTime()) ? new Date() : d)
|
||||
.getFullYear()
|
||||
.toString();
|
||||
const mm = String(
|
||||
(isNaN(d.getTime()) ? new Date() : d).getMonth() + 1,
|
||||
).padStart(2, '0');
|
||||
const dd = String(
|
||||
(isNaN(d.getTime()) ? new Date() : d).getDate(),
|
||||
).padStart(2, '0');
|
||||
|
||||
// 1. Draw Background Box (White with Black border)
|
||||
firstPage.drawRectangle({
|
||||
x: startX,
|
||||
y: startY,
|
||||
width: boxW,
|
||||
height: boxH,
|
||||
color: rgb(1, 1, 1),
|
||||
borderColor: rgb(0, 0, 0),
|
||||
borderWidth: 1,
|
||||
});
|
||||
const qrDateStr = `${yyyy}${mm}${dd}`; // yyyyMMdd
|
||||
const qrContent = `${String(jahr).padStart(4, '0')}-${String(nummer).padStart(6, '0')}-${qrDateStr}`;
|
||||
const printDateStr = `${dd}.${mm}.${yyyy}`;
|
||||
|
||||
// 2. Draw QR Code
|
||||
const qrBuffer = await QRCode.toBuffer(qrContent, {
|
||||
errorCorrectionLevel: 'H',
|
||||
margin: 0,
|
||||
width: 300,
|
||||
color: { dark: '#000000', light: '#FFFFFF' }
|
||||
});
|
||||
const qrImage = await pdfDoc.embedPng(qrBuffer);
|
||||
|
||||
// QR Code size: 27x27 mm (10% smaller than 30x30)
|
||||
const qrSize = 27 * PT_PER_MM;
|
||||
const padding = (32 - 27) / 2; // Center vertically in 32mm box
|
||||
const qrX = startX + (padding * PT_PER_MM);
|
||||
const qrY = startY + (padding * PT_PER_MM);
|
||||
|
||||
firstPage.drawImage(qrImage, {
|
||||
x: qrX,
|
||||
y: qrY,
|
||||
width: qrSize,
|
||||
height: qrSize,
|
||||
});
|
||||
// Dimensions: 57x32 mm
|
||||
const PT_PER_MM = 2.83465;
|
||||
const boxW = 57 * PT_PER_MM;
|
||||
const boxH = 32 * PT_PER_MM;
|
||||
|
||||
// 3. Draw Texts
|
||||
const helveticaBold = await pdfDoc.embedFont(StandardFonts.HelveticaBold);
|
||||
|
||||
// Helper to draw centered text in a specific area
|
||||
const drawCenteredInArea = (text: string, relX: number, relY: number, areaW: number, areaH: number, fontSize: number) => {
|
||||
const textWidth = helveticaBold.widthOfTextAtSize(text, fontSize);
|
||||
const absX = startX + (relX * PT_PER_MM) + (areaW * PT_PER_MM / 2) - (textWidth / 2);
|
||||
const absY = (startY + boxH) - (relY * PT_PER_MM) - (areaH * PT_PER_MM / 2) - (fontSize / 2.5);
|
||||
|
||||
firstPage.drawText(text, {
|
||||
x: absX,
|
||||
y: absY,
|
||||
size: fontSize,
|
||||
font: helveticaBold,
|
||||
color: rgb(0, 0, 0),
|
||||
// A4 dimensions: 210x297 mm
|
||||
const PAGE_H_PT = 297 * PT_PER_MM;
|
||||
|
||||
// Convert mm to points (Y is from bottom in pdf-lib)
|
||||
const startX = Number(x) * PT_PER_MM;
|
||||
const startY = PAGE_H_PT - Number(y) * PT_PER_MM - boxH;
|
||||
|
||||
// 1. Draw Background Box (White with Black border)
|
||||
firstPage.drawRectangle({
|
||||
x: startX,
|
||||
y: startY,
|
||||
width: boxW,
|
||||
height: boxH,
|
||||
color: rgb(1, 1, 1),
|
||||
borderColor: rgb(0, 0, 0),
|
||||
borderWidth: 1,
|
||||
});
|
||||
};
|
||||
|
||||
const isNeu = barcodeData.isNeu === true;
|
||||
// 2. Draw QR Code
|
||||
const qrBuffer = await QRCode.toBuffer(qrContent, {
|
||||
errorCorrectionLevel: 'H',
|
||||
margin: 0,
|
||||
width: 300,
|
||||
color: { dark: '#000000', light: '#FFFFFF' },
|
||||
});
|
||||
const qrImage = await pdfDoc.embedPng(qrBuffer);
|
||||
|
||||
// Text Area X: +33.3mm, Width: 21mm
|
||||
// Year: Y + 3mm, Height: 7.5mm
|
||||
drawCenteredInArea(String(jahr).padStart(4, '0'), 33.3, 3, 21, 7.5, 12);
|
||||
// QR Code size: 27x27 mm (10% smaller than 30x30)
|
||||
const qrSize = 27 * PT_PER_MM;
|
||||
const padding = (32 - 27) / 2; // Center vertically in 32mm box
|
||||
const qrX = startX + padding * PT_PER_MM;
|
||||
const qrY = startY + padding * PT_PER_MM;
|
||||
|
||||
// Number: Y + 10.5mm, Height: 7.5mm
|
||||
const numberText = isNeu ? '<- neu ->' : String(nummer).padStart(6, '0');
|
||||
drawCenteredInArea(numberText, 33.3, 10.5, 21, 7.5, 12);
|
||||
firstPage.drawImage(qrImage, {
|
||||
x: qrX,
|
||||
y: qrY,
|
||||
width: qrSize,
|
||||
height: qrSize,
|
||||
});
|
||||
|
||||
// "Eingegangen": Y + 19mm, Height: 4mm, Size 8
|
||||
drawCenteredInArea('Eingegangen', 33.3, 19, 21, 4, 8);
|
||||
// 3. Draw Texts
|
||||
const helveticaBold = await pdfDoc.embedFont(StandardFonts.HelveticaBold);
|
||||
|
||||
// Date: Y + 19 + 3.5 = 22.5mm, Height: 4mm, Size 8
|
||||
drawCenteredInArea(printDateStr, 33.3, 22.5, 21, 4, 8);
|
||||
// Helper to draw centered text in a specific area
|
||||
const drawCenteredInArea = (
|
||||
text: string,
|
||||
relX: number,
|
||||
relY: number,
|
||||
areaW: number,
|
||||
areaH: number,
|
||||
fontSize: number,
|
||||
) => {
|
||||
const textWidth = helveticaBold.widthOfTextAtSize(text, fontSize);
|
||||
const absX =
|
||||
startX + relX * PT_PER_MM + (areaW * PT_PER_MM) / 2 - textWidth / 2;
|
||||
const absY =
|
||||
startY +
|
||||
boxH -
|
||||
relY * PT_PER_MM -
|
||||
(areaH * PT_PER_MM) / 2 -
|
||||
fontSize / 2.5;
|
||||
|
||||
return Buffer.from(await pdfDoc.save());
|
||||
firstPage.drawText(text, {
|
||||
x: absX,
|
||||
y: absY,
|
||||
size: fontSize,
|
||||
font: helveticaBold,
|
||||
color: rgb(0, 0, 0),
|
||||
});
|
||||
};
|
||||
|
||||
const isNeu = barcodeData.isNeu === true;
|
||||
|
||||
// Text Area X: +33.3mm, Width: 21mm
|
||||
// Year: Y + 3mm, Height: 7.5mm
|
||||
drawCenteredInArea(String(jahr).padStart(4, '0'), 33.3, 3, 21, 7.5, 12);
|
||||
|
||||
// Number: Y + 10.5mm, Height: 7.5mm
|
||||
const numberText = isNeu ? '<- neu ->' : String(nummer).padStart(6, '0');
|
||||
drawCenteredInArea(numberText, 33.3, 10.5, 21, 7.5, 12);
|
||||
|
||||
// "Eingegangen": Y + 19mm, Height: 4mm, Size 8
|
||||
drawCenteredInArea('Eingegangen', 33.3, 19, 21, 4, 8);
|
||||
|
||||
// Date: Y + 19 + 3.5 = 22.5mm, Height: 4mm, Size 8
|
||||
drawCenteredInArea(printDateStr, 33.3, 22.5, 21, 4, 8);
|
||||
|
||||
return Buffer.from(await pdfDoc.save());
|
||||
} finally {
|
||||
await fs.unlink(tempInputPath).catch(() => {});
|
||||
}
|
||||
@@ -345,12 +457,20 @@ export class EmailImportService {
|
||||
paperlessCorrespondentId?: number | null;
|
||||
parentDocumentId?: number | null;
|
||||
splitRanges?: { start: number; end: number }[];
|
||||
barcode?: { x: number; y: number; nummer: string; datum: string; jahr: string };
|
||||
barcode?: {
|
||||
x: number;
|
||||
y: number;
|
||||
nummer: string;
|
||||
datum: string;
|
||||
jahr: string;
|
||||
};
|
||||
belegnummer?: string;
|
||||
}[];
|
||||
emailDate: string;
|
||||
}): Promise<{ success: boolean; results: any[] }> {
|
||||
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'paperless-mail-import-'));
|
||||
const tempDir = await fs.mkdtemp(
|
||||
path.join(os.tmpdir(), 'paperless-mail-import-'),
|
||||
);
|
||||
const results = [];
|
||||
this.setJobStatus(data.jobId, 'Dokumente werden vorbereitet...');
|
||||
|
||||
@@ -358,10 +478,14 @@ export class EmailImportService {
|
||||
for (const att of data.attachments) {
|
||||
if (att.type === 'IGNORE') continue;
|
||||
|
||||
const attachmentEntity = await this.attachmentRepo.findOne({ where: { Id: att.attachmentId } });
|
||||
const attachmentEntity = await this.attachmentRepo.findOne({
|
||||
where: { Id: att.attachmentId },
|
||||
});
|
||||
if (!attachmentEntity) continue;
|
||||
|
||||
const content = await this.contentRepo.findOne({ where: { AttachmentEntityId: att.attachmentId } });
|
||||
const content = await this.contentRepo.findOne({
|
||||
where: { AttachmentEntityId: att.attachmentId },
|
||||
});
|
||||
if (!content) continue;
|
||||
|
||||
const originalPdfBytes = content.Content1;
|
||||
@@ -375,26 +499,36 @@ export class EmailImportService {
|
||||
|
||||
if (att.splitRanges && att.splitRanges.length > 0) {
|
||||
// SPLIT PDF
|
||||
const pdfDoc = await PDFDocument.load(originalPdfBytes, { ignoreEncryption: true });
|
||||
const pdfDoc = await PDFDocument.load(originalPdfBytes, {
|
||||
ignoreEncryption: true,
|
||||
});
|
||||
const totalPages = pdfDoc.getPageCount();
|
||||
|
||||
for (const range of att.splitRanges) {
|
||||
const start = Math.max(1, range.start);
|
||||
const end = Math.min(range.end, totalPages);
|
||||
|
||||
|
||||
if (start > end) {
|
||||
this.logger.warn(`Ungültiger Bereich für Splitting: ${start}-${end} (Seiten gesamt: ${totalPages})`);
|
||||
this.logger.warn(
|
||||
`Ungültiger Bereich für Splitting: ${start}-${end} (Seiten gesamt: ${totalPages})`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const newPdf = await PDFDocument.create();
|
||||
// Pages are 0-indexed in pdf-lib
|
||||
const pageIndices = Array.from({ length: end - start + 1 }, (_, i) => start - 1 + i);
|
||||
const pageIndices = Array.from(
|
||||
{ length: end - start + 1 },
|
||||
(_, i) => start - 1 + i,
|
||||
);
|
||||
const copiedPages = await newPdf.copyPages(pdfDoc, pageIndices);
|
||||
copiedPages.forEach((p) => newPdf.addPage(p));
|
||||
|
||||
|
||||
const splitPdfBytes = await newPdf.save();
|
||||
const tempFilePath = path.join(tempDir, `${baseFilename}_${start}-${end}.pdf`);
|
||||
const tempFilePath = path.join(
|
||||
tempDir,
|
||||
`${baseFilename}_${start}-${end}.pdf`,
|
||||
);
|
||||
await fs.writeFile(tempFilePath, Buffer.from(splitPdfBytes));
|
||||
|
||||
uploadPromises.push({
|
||||
@@ -423,20 +557,28 @@ export class EmailImportService {
|
||||
for (const uploadItem of uploadPromises) {
|
||||
const options: any = {
|
||||
filename: uploadItem.filename,
|
||||
title: att.belegnummer ? `Beleg ${att.belegnummer}` : uploadItem.filename,
|
||||
title: att.belegnummer
|
||||
? `Beleg ${att.belegnummer}`
|
||||
: uploadItem.filename,
|
||||
created: createdDate,
|
||||
owner: null,
|
||||
};
|
||||
if (att.paperlessCorrespondentId) options.correspondent = att.paperlessCorrespondentId;
|
||||
if (att.paperlessCorrespondentId)
|
||||
options.correspondent = att.paperlessCorrespondentId;
|
||||
|
||||
this.setJobStatus(data.jobId, `Lade ${uploadItem.filename} hoch...`);
|
||||
const paperlessTaskId = await this.paperlessService.uploadDocument(uploadItem.path, options);
|
||||
const paperlessTaskId = await this.paperlessService.uploadDocument(
|
||||
uploadItem.path,
|
||||
options,
|
||||
);
|
||||
|
||||
// Create background task for enrichment (same logic as Inbox)
|
||||
const backgroundTask = this.taskRepo.create({
|
||||
TaskId: paperlessTaskId,
|
||||
InterneBelegnummer: att.belegnummer || '',
|
||||
Eingangsdatum: att.barcode?.datum ? new Date(att.barcode.datum) : createdDate,
|
||||
Eingangsdatum: att.barcode?.datum
|
||||
? new Date(att.barcode.datum)
|
||||
: createdDate,
|
||||
Belegdatum: createdDate,
|
||||
BarcodeJson: att.barcode ? JSON.stringify(att.barcode) : null,
|
||||
BetriebID: null, // Owner
|
||||
@@ -450,17 +592,25 @@ export class EmailImportService {
|
||||
// Still poll for Doc ID so we can return it to the frontend for immediate preview
|
||||
let docId = null;
|
||||
for (let i = 0; i < 30; i++) {
|
||||
this.setJobStatus(data.jobId, `Warte auf Paperless-Verarbeitung... (${i + 1}/30)`);
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
try {
|
||||
const taskStatus = await this.paperlessService.getTask(paperlessTaskId);
|
||||
// Paperless returns { results: [ ... ] } for filtered tasks
|
||||
const statusObj = taskStatus.results ? taskStatus.results[0] : (Array.isArray(taskStatus) ? taskStatus[0] : taskStatus);
|
||||
if (statusObj && statusObj.related_document) {
|
||||
docId = statusObj.related_document;
|
||||
break;
|
||||
}
|
||||
} catch(e) {}
|
||||
this.setJobStatus(
|
||||
data.jobId,
|
||||
`Warte auf Paperless-Verarbeitung... (${i + 1}/30)`,
|
||||
);
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
try {
|
||||
const taskStatus =
|
||||
await this.paperlessService.getTask(paperlessTaskId);
|
||||
// Paperless returns { results: [ ... ] } for filtered tasks
|
||||
const statusObj = taskStatus.results
|
||||
? taskStatus.results[0]
|
||||
: Array.isArray(taskStatus)
|
||||
? taskStatus[0]
|
||||
: taskStatus;
|
||||
if (statusObj && statusObj.related_document) {
|
||||
docId = statusObj.related_document;
|
||||
break;
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
if (docId) {
|
||||
@@ -478,7 +628,9 @@ export class EmailImportService {
|
||||
|
||||
// Confirm Belegnummer if used
|
||||
if (att.belegnummer && att.barcode?.nummer) {
|
||||
await this.setBelegnummer(data.emailDate, att.barcode.nummer).catch(e => this.logger.warn(`Failed to set Belegnummer: ${e.message}`));
|
||||
await this.setBelegnummer(data.emailDate, att.barcode.nummer).catch(
|
||||
(e) => this.logger.warn(`Failed to set Belegnummer: ${e.message}`),
|
||||
);
|
||||
}
|
||||
|
||||
results.push({ attachmentId: att.attachmentId, paperlessIds });
|
||||
@@ -486,12 +638,14 @@ export class EmailImportService {
|
||||
|
||||
// Mark Email as processed (Status = 1)
|
||||
if (data.attachments.length > 0) {
|
||||
const firstAtt = await this.attachmentRepo.findOne({
|
||||
where: { Id: data.attachments[0].attachmentId }
|
||||
const firstAtt = await this.attachmentRepo.findOne({
|
||||
where: { Id: data.attachments[0].attachmentId },
|
||||
});
|
||||
if (firstAtt) {
|
||||
await this.emailRepo.update(firstAtt.EmailMessageId, { Status: 1 });
|
||||
this.logger.log(`Email ${firstAtt.EmailMessageId} als verarbeitet markiert.`);
|
||||
this.logger.log(
|
||||
`Email ${firstAtt.EmailMessageId} als verarbeitet markiert.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -500,7 +654,8 @@ export class EmailImportService {
|
||||
} finally {
|
||||
// Clean up temp dir and job status
|
||||
await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {});
|
||||
if (data.jobId) setTimeout(() => this.importJobs.delete(data.jobId!), 5000);
|
||||
if (data.jobId)
|
||||
setTimeout(() => this.importJobs.delete(data.jobId!), 5000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,10 @@ export class EmailPageCacheService {
|
||||
private readonly mailsRoot: string;
|
||||
|
||||
constructor(configService: ConfigService) {
|
||||
this.mailsRoot = configService.get<string>('MAILS_DATA_DIR', '/mnt/data/mails');
|
||||
this.mailsRoot = configService.get<string>(
|
||||
'MAILS_DATA_DIR',
|
||||
'/mnt/data/mails',
|
||||
);
|
||||
}
|
||||
|
||||
attachmentDir(attachmentId: number | string): string {
|
||||
@@ -20,14 +23,23 @@ export class EmailPageCacheService {
|
||||
}
|
||||
|
||||
previewPath(attachmentId: number | string, page: number): string {
|
||||
return path.join(this.attachmentDir(attachmentId), `page-${page}.preview.png`);
|
||||
return path.join(
|
||||
this.attachmentDir(attachmentId),
|
||||
`page-${page}.preview.png`,
|
||||
);
|
||||
}
|
||||
|
||||
thumbnailPath(attachmentId: number | string, page: number): string {
|
||||
return path.join(this.attachmentDir(attachmentId), `page-${page}.thumb.png`);
|
||||
return path.join(
|
||||
this.attachmentDir(attachmentId),
|
||||
`page-${page}.thumb.png`,
|
||||
);
|
||||
}
|
||||
|
||||
async generate(attachmentId: number | string, renderedImages: string[]): Promise<void> {
|
||||
async generate(
|
||||
attachmentId: number | string,
|
||||
renderedImages: string[],
|
||||
): Promise<void> {
|
||||
const dir = this.attachmentDir(attachmentId);
|
||||
await fs.mkdir(dir, { recursive: true });
|
||||
|
||||
@@ -39,14 +51,22 @@ export class EmailPageCacheService {
|
||||
|
||||
try {
|
||||
await fs.copyFile(src, previewDest);
|
||||
await sharp(src).resize({ width: THUMBNAIL_WIDTH }).png().toFile(thumbDest);
|
||||
await sharp(src)
|
||||
.resize({ width: THUMBNAIL_WIDTH })
|
||||
.png()
|
||||
.toFile(thumbDest);
|
||||
} catch (err: any) {
|
||||
this.logger.warn(`E-Mail Page Cache fehlgeschlagen (Attachment ${attachmentId} Seite ${page}): ${err.message}`);
|
||||
this.logger.warn(
|
||||
`E-Mail Page Cache fehlgeschlagen (Attachment ${attachmentId} Seite ${page}): ${err.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async hasPreview(attachmentId: number | string, page: number): Promise<boolean> {
|
||||
async hasPreview(
|
||||
attachmentId: number | string,
|
||||
page: number,
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
await fs.access(this.previewPath(attachmentId, page));
|
||||
return true;
|
||||
|
||||
@@ -4,8 +4,26 @@ import { EmailController } from './email.controller';
|
||||
import { Email } from '../database/entities/email.entity';
|
||||
|
||||
const mockEmails: Partial<Email>[] = [
|
||||
{ Id: 1, MessageId: 'msg-1', SenderAddress: 'a@test.de', RecipientAddress: 'b@test.de', Subject: 'Test 1', Date: new Date(), Body: 'body', Status: 0 },
|
||||
{ Id: 2, MessageId: 'msg-2', SenderAddress: 'c@test.de', RecipientAddress: 'd@test.de', Subject: 'Test 2', Date: new Date(), Body: 'body2', Status: 1 },
|
||||
{
|
||||
Id: 1,
|
||||
MessageId: 'msg-1',
|
||||
SenderAddress: 'a@test.de',
|
||||
RecipientAddress: 'b@test.de',
|
||||
Subject: 'Test 1',
|
||||
Date: new Date(),
|
||||
Body: 'body',
|
||||
Status: 0,
|
||||
},
|
||||
{
|
||||
Id: 2,
|
||||
MessageId: 'msg-2',
|
||||
SenderAddress: 'c@test.de',
|
||||
RecipientAddress: 'd@test.de',
|
||||
Subject: 'Test 2',
|
||||
Date: new Date(),
|
||||
Body: 'body2',
|
||||
Status: 1,
|
||||
},
|
||||
];
|
||||
|
||||
const mockQueryBuilder = {
|
||||
@@ -44,7 +62,9 @@ describe('EmailController', () => {
|
||||
|
||||
it('getEmails filters by status', async () => {
|
||||
await controller.getEmails('1');
|
||||
expect(mockQueryBuilder.where).toHaveBeenCalledWith('e.Status = :status', { status: 1 });
|
||||
expect(mockQueryBuilder.where).toHaveBeenCalledWith('e.Status = :status', {
|
||||
status: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it('getEmail returns single item', async () => {
|
||||
|
||||
@@ -1,4 +1,15 @@
|
||||
import { Controller, Get, Post, Param, Query, Res, Logger, NotFoundException, Patch, Body } from '@nestjs/common';
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Param,
|
||||
Query,
|
||||
Res,
|
||||
Logger,
|
||||
NotFoundException,
|
||||
Patch,
|
||||
Body,
|
||||
} from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import type { Response } from 'express';
|
||||
@@ -15,8 +26,10 @@ export class EmailController {
|
||||
|
||||
constructor(
|
||||
@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>,
|
||||
private readonly paperlessService: PaperlessService,
|
||||
) {}
|
||||
|
||||
@@ -26,7 +39,8 @@ export class EmailController {
|
||||
@Query('status') status?: string,
|
||||
@Query('limit') limit?: string,
|
||||
) {
|
||||
const qb = this.emailRepo.createQueryBuilder('e')
|
||||
const qb = this.emailRepo
|
||||
.createQueryBuilder('e')
|
||||
.leftJoinAndSelect('e.Attachments', 'a')
|
||||
.orderBy('e.Date', 'DESC')
|
||||
.take(parseInt(limit ?? '50', 10));
|
||||
@@ -66,21 +80,28 @@ export class EmailController {
|
||||
const attachment = await this.attachmentRepo.findOne({ where: { Id: id } });
|
||||
if (!attachment) throw new NotFoundException('Anhang nicht gefunden');
|
||||
|
||||
const content = await this.contentRepo.findOne({ where: { AttachmentEntityId: id } });
|
||||
const content = await this.contentRepo.findOne({
|
||||
where: { AttachmentEntityId: id },
|
||||
});
|
||||
if (!content) throw new NotFoundException('Inhalt nicht gefunden');
|
||||
|
||||
res.setHeader('Content-Type', attachment.ContentType || 'application/octet-stream');
|
||||
res.setHeader('Content-Disposition', `inline; filename="${encodeURIComponent(attachment.FileName)}"`);
|
||||
res.setHeader(
|
||||
'Content-Type',
|
||||
attachment.ContentType || 'application/octet-stream',
|
||||
);
|
||||
res.setHeader(
|
||||
'Content-Disposition',
|
||||
`inline; filename="${encodeURIComponent(attachment.FileName)}"`,
|
||||
);
|
||||
res.send(content.Content1);
|
||||
}
|
||||
|
||||
@Patch(':id/status')
|
||||
@RequirePermissions(Permission.MANAGE_ALL)
|
||||
async updateStatus(
|
||||
@Param('id') id: string,
|
||||
@Body('status') status: number,
|
||||
) {
|
||||
const email = await this.emailRepo.findOneOrFail({ where: { Id: parseInt(id, 10) } });
|
||||
async updateStatus(@Param('id') id: string, @Body('status') status: number) {
|
||||
const email = await this.emailRepo.findOneOrFail({
|
||||
where: { Id: parseInt(id, 10) },
|
||||
});
|
||||
email.Status = status;
|
||||
await this.emailRepo.save(email);
|
||||
this.logger.log(`E-Mail ${id} auf Status ${status} gesetzt.`);
|
||||
@@ -91,10 +112,14 @@ export class EmailController {
|
||||
@RequirePermissions(Permission.MANAGE_ALL)
|
||||
async checkAttachments(@Body() body: { includeProcessed?: boolean } = {}) {
|
||||
const { includeProcessed = false } = body;
|
||||
this.logger.log(`Starte manuelle Prüfung der E-Mail-Anhänge in Paperless... (includeProcessed=${includeProcessed})`);
|
||||
this.logger.log(
|
||||
`Starte manuelle Prüfung der E-Mail-Anhänge in Paperless... (includeProcessed=${includeProcessed})`,
|
||||
);
|
||||
|
||||
try {
|
||||
const whereCondition = includeProcessed ? [{ Status: 0 }, { Status: 1 }] : { Status: 0 };
|
||||
const whereCondition = includeProcessed
|
||||
? [{ Status: 0 }, { Status: 1 }]
|
||||
: { Status: 0 };
|
||||
const emails = await this.emailRepo.find({
|
||||
where: whereCondition,
|
||||
relations: ['Attachments'],
|
||||
@@ -116,25 +141,43 @@ export class EmailController {
|
||||
|
||||
for (const attachment of email.Attachments) {
|
||||
// Prüfe nur PDFs mit Checksumme
|
||||
if (attachment.ContentType === 'application/pdf' && attachment.Checksum) {
|
||||
this.logger.debug(`Prüfe Checksumme für E-Mail ${email.Id}, Anhang ${attachment.Id} (${attachment.FileName})`);
|
||||
if (
|
||||
attachment.ContentType === 'application/pdf' &&
|
||||
attachment.Checksum
|
||||
) {
|
||||
this.logger.debug(
|
||||
`Prüfe Checksumme für E-Mail ${email.Id}, Anhang ${attachment.Id} (${attachment.FileName})`,
|
||||
);
|
||||
try {
|
||||
const docId = await this.paperlessService.getDocumentIdByChecksum(attachment.Checksum);
|
||||
const docId = await this.paperlessService.getDocumentIdByChecksum(
|
||||
attachment.Checksum,
|
||||
);
|
||||
if (docId !== null) {
|
||||
this.logger.log(`Treffer! Anhang ${attachment.Id} (E-Mail ${email.Id}) → Paperless-Dokument ${docId}.`);
|
||||
this.logger.log(
|
||||
`Treffer! Anhang ${attachment.Id} (E-Mail ${email.Id}) → Paperless-Dokument ${docId}.`,
|
||||
);
|
||||
hasMatch = true;
|
||||
|
||||
// PaperlessDocumentId hinterlegen, falls noch nicht vorhanden
|
||||
const existingIds: Record<string, number> = attachment.PaperlessDocumentIds ?? {};
|
||||
const existingIds: Record<string, number> =
|
||||
attachment.PaperlessDocumentIds ?? {};
|
||||
if (!existingIds['full']) {
|
||||
attachment.PaperlessDocumentIds = { ...existingIds, full: docId };
|
||||
attachment.PaperlessDocumentIds = {
|
||||
...existingIds,
|
||||
full: docId,
|
||||
};
|
||||
await this.attachmentRepo.save(attachment);
|
||||
idsUpdated++;
|
||||
this.logger.log(`Anhang ${attachment.Id}: PaperlessDocumentIds aktualisiert (full=${docId}).`);
|
||||
this.logger.log(
|
||||
`Anhang ${attachment.Id}: PaperlessDocumentIds aktualisiert (full=${docId}).`,
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (err: any) {
|
||||
this.logger.error(`Fehler bei Checksummen-Prüfung für Attachment ${attachment.Id}: ${err.message}`, err.stack);
|
||||
this.logger.error(
|
||||
`Fehler bei Checksummen-Prüfung für Attachment ${attachment.Id}: ${err.message}`,
|
||||
err.stack,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -144,18 +187,27 @@ export class EmailController {
|
||||
email.Status = 1;
|
||||
await this.emailRepo.save(email);
|
||||
updatedCount++;
|
||||
this.logger.log(`E-Mail ${email.Id} auf Status 1 (Verarbeitet) gesetzt.`);
|
||||
this.logger.log(
|
||||
`E-Mail ${email.Id} auf Status 1 (Verarbeitet) gesetzt.`,
|
||||
);
|
||||
}
|
||||
|
||||
if ((index + 1) % 10 === 0) {
|
||||
this.logger.log(`Zwischenstand: ${index + 1} von ${emails.length} E-Mails geprüft.`);
|
||||
this.logger.log(
|
||||
`Zwischenstand: ${index + 1} von ${emails.length} E-Mails geprüft.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.log(`Prüfung abgeschlossen. ${updatedCount} E-Mails aktualisiert, ${idsUpdated} Paperless-IDs ergänzt, ${skippedCount} übersprungen.`);
|
||||
this.logger.log(
|
||||
`Prüfung abgeschlossen. ${updatedCount} E-Mails aktualisiert, ${idsUpdated} Paperless-IDs ergänzt, ${skippedCount} übersprungen.`,
|
||||
);
|
||||
return { updatedCount, idsUpdated };
|
||||
} catch (error: any) {
|
||||
this.logger.error(`Kritischer Fehler bei checkAttachments: ${error.message}`, error.stack);
|
||||
this.logger.error(
|
||||
`Kritischer Fehler bei checkAttachments: ${error.message}`,
|
||||
error.stack,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,13 @@ import { PreprocessingModule } from '../preprocessing/preprocessing.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([Email, Attachment, Content, CorrespondentEmailMapping, Task]),
|
||||
TypeOrmModule.forFeature([
|
||||
Email,
|
||||
Attachment,
|
||||
Content,
|
||||
CorrespondentEmailMapping,
|
||||
Task,
|
||||
]),
|
||||
PaperlessModule,
|
||||
PreprocessingModule,
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user