ded652382d
Each sendExportEmail call created a new nodemailer transport which was never closed, leaving the internal SMTP connection pool alive. With 3-retry backoff, up to 3 leaked transports per invoice accumulate over time and can exhaust OS socket limits. Add transport.close() in a finally block.
114 lines
4.2 KiB
TypeScript
114 lines
4.2 KiB
TypeScript
import { Injectable, Logger } from '@nestjs/common';
|
|
import * as nodemailer from 'nodemailer';
|
|
import { SettingsService } from '../settings/settings.service';
|
|
|
|
/**
|
|
* DkvMailService — sends DKV export files via SMTP with a runtime transport.
|
|
*
|
|
* Key design decision (Research Pitfall 3): @nestjs-modules/mailer cannot change its
|
|
* SMTP transport after startup. DkvMailService solves this by calling
|
|
* nodemailer.createTransport() fresh on every send — reflecting any SMTP config change
|
|
* made in the UI immediately without a service restart.
|
|
*
|
|
* This service uses nodemailer directly — NOT the @nestjs-modules/mailer abstraction.
|
|
*
|
|
* Security: T-07-10 — decrypted SMTP credentials are used only inside this method
|
|
* scope and never logged. Generic error messages are emitted on failure.
|
|
*/
|
|
@Injectable()
|
|
export class DkvMailService {
|
|
private readonly logger = new Logger(DkvMailService.name);
|
|
|
|
constructor(private readonly settingsService: SettingsService) {}
|
|
|
|
/**
|
|
* Send a DKV export xlsx file as an email attachment to the configured recipient.
|
|
*
|
|
* Transport is created fresh per send using the decrypted SMTP config from DB.
|
|
* This ensures that any admin SMTP config change takes effect on the next send
|
|
* without restarting the API (Pitfall 3 mitigation).
|
|
*
|
|
* On failure: logs a generic error message (never credentials — T-07-10) and
|
|
* rethrows so the orchestrator (Plan 04) can execute 3-retry exponential backoff (D-16).
|
|
* Error is NOT swallowed here — unlike MailService (which swallows for T-02-12 reasons).
|
|
*
|
|
* @param tenantId - Tenant whose SmtpConfig to use
|
|
* @param recipient - Export recipient email address (from DkvModuleConfig.exportRecipient)
|
|
* @param attachmentBuffer - xlsx Buffer from DkvExportService.buildExcelBuffer()
|
|
* @param filename - Attachment filename (e.g. "DKV_2026-04_26-651566449-001.xlsx")
|
|
*/
|
|
async sendExportEmail(
|
|
tenantId: string,
|
|
recipient: string,
|
|
attachmentBuffer: Buffer,
|
|
filename: string,
|
|
): Promise<void> {
|
|
// Load decrypted SMTP config — used only within this method scope (T-07-10)
|
|
const smtpConfig = await this.settingsService.getDecryptedSmtpConfig(tenantId);
|
|
|
|
if (!smtpConfig) {
|
|
throw new Error(
|
|
`No SMTP configuration found for tenant ${tenantId}. Configure SMTP in Settings first.`,
|
|
);
|
|
}
|
|
|
|
// Build transport at send time (NOT at module startup — Pitfall 3)
|
|
const transport = nodemailer.createTransport({
|
|
host: smtpConfig.host,
|
|
port: smtpConfig.port,
|
|
secure: smtpConfig.encryption === 'ssl-tls',
|
|
requireTLS: smtpConfig.encryption === 'starttls',
|
|
auth: smtpConfig.username
|
|
? {
|
|
user: smtpConfig.username,
|
|
// T-07-10: decryptedPassword used only here, never logged
|
|
pass: smtpConfig.decryptedPassword ?? '',
|
|
}
|
|
: undefined,
|
|
});
|
|
|
|
const subject = `DKV Flottenabrechnung: ${filename}`;
|
|
const text = [
|
|
'Sehr geehrte Damen und Herren,',
|
|
'',
|
|
'anbei erhalten Sie die aktuelle DKV-Flottenabrechnung als Excel-Datei.',
|
|
'',
|
|
`Datei: ${filename}`,
|
|
'',
|
|
'Mit freundlichen Grüßen,',
|
|
'Ihr Tessera-System',
|
|
].join('\n');
|
|
|
|
try {
|
|
await transport.sendMail({
|
|
from: smtpConfig.fromAddress,
|
|
to: recipient,
|
|
subject,
|
|
text,
|
|
attachments: [
|
|
{
|
|
filename,
|
|
content: attachmentBuffer,
|
|
contentType:
|
|
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
},
|
|
],
|
|
});
|
|
|
|
this.logger.log(`DKV export email sent to ${recipient}: ${filename}`);
|
|
} catch (error) {
|
|
// T-07-10: Generic log message — no SMTP credentials, host, or transport details
|
|
this.logger.error(
|
|
`Failed to send DKV export to ${recipient} (file: ${filename}): ${(error as Error).message}`,
|
|
);
|
|
// RETHROW — caller (DkvService) handles exponential backoff retries (D-16)
|
|
throw error;
|
|
} finally {
|
|
// Always close the transport to release the SMTP connection pool (WR-01).
|
|
// Without this, repeated sends (especially with retry backoff) accumulate
|
|
// open connections and can exhaust OS socket limits.
|
|
transport.close();
|
|
}
|
|
}
|
|
}
|