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>
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user