Files
KC-APP/backend/src/mail/smtp-mail.provider.ts
T
linusandClaude Sonnet 5 320a41142b feat(backend): mail module + send personal Gemeinde-Teamer invites
New global mail/ module mirroring the files/storage/ provider pattern:
- MailProvider abstraction; default LogMailProvider only logs (no delivery),
  MAIL_PROVIDER=smtp switches to a nodemailer SMTP transport (SMTP_*,
  MAIL_FROM).
- MailService.sendTeamerInvite() composes the invite email with a link
  built from APP_BASE_URL.

TeamerService.createInvite() now mails personal invites (those with an
email) best-effort and returns `emailSent`; group links are unchanged.
Delivery failures are logged and swallowed, never blocking invite creation.

New env: APP_BASE_URL, MAIL_PROVIDER, MAIL_FROM, SMTP_HOST/PORT/SECURE/
USER/PASS. Tests: teamer spec covers mail-on-personal-invite,
no-mail-on-group-link, and transport-drop; npm test green at 56. Docs
updated.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-10 08:17:53 +02:00

46 lines
1.5 KiB
TypeScript

import { Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import * as nodemailer from 'nodemailer';
import { MailMessage, MailProvider } from './mail-provider';
/// SMTP transport (MAIL_PROVIDER=smtp). Delivery failures are logged and
/// swallowed — callers treat email as best-effort.
export class SmtpMailProvider implements MailProvider {
private readonly logger = new Logger('MailProvider');
private readonly from: string;
private readonly transport: nodemailer.Transporter;
constructor(config: ConfigService) {
this.from = config.getOrThrow<string>('MAIL_FROM');
this.transport = nodemailer.createTransport({
host: config.getOrThrow<string>('SMTP_HOST'),
port: Number(config.get<string>('SMTP_PORT') ?? 587),
secure: config.get<string>('SMTP_SECURE') === 'true',
auth: config.get<string>('SMTP_USER')
? {
user: config.getOrThrow<string>('SMTP_USER'),
pass: config.getOrThrow<string>('SMTP_PASS'),
}
: undefined,
});
}
async send(message: MailMessage): Promise<boolean> {
try {
await this.transport.sendMail({
from: this.from,
to: message.to,
subject: message.subject,
text: message.text,
html: message.html,
});
return true;
} catch (err) {
this.logger.error(
`Failed to send "${message.subject}" to ${message.to}: ${(err as Error).message}`,
);
return false;
}
}
}