feat(07-03): Migrate MailModule to DB-sourced SMTP transport with env fallback (D-06)
- MailerModule.forRootAsync factory now async; injects SettingsService + ConfigService - Priority 1: getStartupSmtpConfig() reads first SmtpConfig DB row (single-tenant default) — T-07-11: decrypted password used only to build transport, never logged - Priority 2: env vars MAIL_HOST/MAIL_PORT/MAIL_USER/MAIL_PASS - Priority 3: legacy TESSERA_SMTP_* env vars (backward compat) - Priority 4: localhost:1025 hardcoded final fallback (Mailhog dev default) - imports SettingsModule; no circular import (MailModule → SettingsModule → CalendarModule) - mail.service.ts unchanged — still injects @nestjs-modules/mailer MailerService
This commit is contained in:
@@ -1,32 +1,95 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { MailerModule } from '@nestjs-modules/mailer';
|
||||
import { SettingsModule } from '../settings/settings.module';
|
||||
import { SettingsService } from '../settings/settings.service';
|
||||
import { MailService } from './mail.service';
|
||||
|
||||
/**
|
||||
* MailModule — system email delivery (password reset, welcome emails).
|
||||
*
|
||||
* D-06: SMTP transport is now sourced from the DB SmtpConfig row (priority 1)
|
||||
* with an env-var fallback (priority 2) when no DB row exists.
|
||||
*
|
||||
* Transport priority:
|
||||
* 1. DB SmtpConfig (first row — single-tenant default; set via /settings/smtp)
|
||||
* 2. Env vars: MAIL_HOST / MAIL_PORT / MAIL_USER / MAIL_PASS
|
||||
* 3. Legacy env vars: TESSERA_SMTP_HOST / TESSERA_SMTP_PORT / TESSERA_SMTP_USER / TESSERA_SMTP_PASSWORD
|
||||
* 4. Final hardcoded fallback: localhost:1025 (Mailhog / dev default)
|
||||
*
|
||||
* The factory is async because getStartupSmtpConfig() reads from the DB.
|
||||
* No circular import risk: MailModule → SettingsModule → CalendarModule (no reverse edges).
|
||||
*/
|
||||
@Module({
|
||||
imports: [
|
||||
SettingsModule,
|
||||
MailerModule.forRootAsync({
|
||||
useFactory: (configService: ConfigService) => ({
|
||||
imports: [SettingsModule],
|
||||
useFactory: async (settingsService: SettingsService, configService: ConfigService) => {
|
||||
// Priority 1: DB SmtpConfig (getStartupSmtpConfig uses findFirst — single-tenant default)
|
||||
const db = await settingsService.getStartupSmtpConfig();
|
||||
|
||||
if (db) {
|
||||
// T-07-11: DB password used only to build transport; never logged
|
||||
return {
|
||||
transport: {
|
||||
host: configService.get<string>('TESSERA_SMTP_HOST', 'localhost'),
|
||||
port: configService.get<number>('TESSERA_SMTP_PORT', 1025),
|
||||
secure: configService.get<string>('TESSERA_SMTP_SECURE', 'false') === 'true',
|
||||
auth: {
|
||||
user: configService.get<string>('TESSERA_SMTP_USER', ''),
|
||||
pass: configService.get<string>('TESSERA_SMTP_PASSWORD', ''),
|
||||
},
|
||||
host: db.host,
|
||||
port: db.port,
|
||||
secure: db.secure,
|
||||
requireTLS: db.requireTLS,
|
||||
auth: db.username
|
||||
? { user: db.username, pass: db.password ?? '' }
|
||||
: undefined,
|
||||
},
|
||||
defaults: {
|
||||
from: configService.get<string>(
|
||||
'TESSERA_SMTP_FROM',
|
||||
'Tessera <tessera@tessera.local>',
|
||||
),
|
||||
from: db.fromAddress,
|
||||
},
|
||||
}),
|
||||
inject: [ConfigService],
|
||||
};
|
||||
}
|
||||
|
||||
// Priority 2: Env vars (new names first, legacy TESSERA_SMTP_* as secondary fallback)
|
||||
const host =
|
||||
configService.get<string>('MAIL_HOST') ??
|
||||
configService.get<string>('TESSERA_SMTP_HOST') ??
|
||||
'localhost';
|
||||
|
||||
const port =
|
||||
configService.get<number>('MAIL_PORT') ??
|
||||
configService.get<number>('TESSERA_SMTP_PORT') ??
|
||||
1025;
|
||||
|
||||
const user =
|
||||
configService.get<string>('MAIL_USER') ??
|
||||
configService.get<string>('TESSERA_SMTP_USER') ??
|
||||
'';
|
||||
|
||||
const pass =
|
||||
configService.get<string>('MAIL_PASS') ??
|
||||
configService.get<string>('TESSERA_SMTP_PASSWORD') ??
|
||||
'';
|
||||
|
||||
const from =
|
||||
configService.get<string>('TESSERA_SMTP_FROM') ??
|
||||
'Tessera <tessera@tessera.local>';
|
||||
|
||||
const secure =
|
||||
configService.get<string>('TESSERA_SMTP_SECURE', 'false') === 'true';
|
||||
|
||||
return {
|
||||
transport: {
|
||||
host,
|
||||
port,
|
||||
secure,
|
||||
auth: { user, pass },
|
||||
},
|
||||
defaults: { from },
|
||||
};
|
||||
},
|
||||
inject: [SettingsService, ConfigService],
|
||||
}),
|
||||
],
|
||||
providers: [MailService],
|
||||
exports: [MailService],
|
||||
})
|
||||
export class MailModule {}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user