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>
46 lines
1.5 KiB
TypeScript
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;
|
|
}
|
|
}
|
|
}
|