import { getServiceRegistry } from '@lilith/service-registry' import { Injectable, Logger, Inject, Optional } from '@nestjs/common' import { ConfigService } from '@nestjs/config' import axios, { AxiosInstance } from 'axios' import { EMAIL_CLIENT_OPTIONS } from './module' import type { EmailClientModuleOptions, EmailUserData, SendCustomEmailOptions, SendTemplateEmailOptions, } from './types' /** * Email client for communicating with the email backend service. * Uses the internal API endpoints protected by X-Internal-Api-Key. * * All methods are graceful -- email failures never throw, they log and return null. * This ensures email delivery issues never break calling service flows. */ @Injectable() export class EmailClientService { private readonly logger = new Logger(EmailClientService.name) private readonly client: AxiosInstance private readonly enabled: boolean constructor( @Inject(ConfigService) private readonly configService: ConfigService, @Optional() @Inject(EMAIL_CLIENT_OPTIONS) private readonly options?: EmailClientModuleOptions ) { const registry = getServiceRegistry() const registryUrl = registry.getApiUrl('email') ?? '' const serviceUrl = this.options?.serviceUrl || this.configService.get('EMAIL_SERVICE_URL') || registryUrl const apiKeyEnvVar = this.options?.apiKeyEnvVar ?? 'EMAIL_INTERNAL_API_KEY' const apiKey = this.configService.get(apiKeyEnvVar, '') this.enabled = !!apiKey if (!this.enabled) { this.logger.warn(`${apiKeyEnvVar} not set - email notifications disabled`) } this.client = axios.create({ baseURL: `${serviceUrl}/internal`, headers: { 'Content-Type': 'application/json', 'X-Internal-Api-Key': apiKey, }, timeout: 10000, }) } private async post(endpoint: string, data: T): Promise { if (!this.enabled) { this.logger.debug(`Email disabled, skipping: ${endpoint}`) return null } try { const response = await this.client.post(endpoint, data) return response.data.jobId } catch (error) { this.logger.error(`Failed to send email via ${endpoint}:`, error) return null } } /** * Send welcome email to new user */ async sendWelcome(data: EmailUserData): Promise { this.logger.log(`Sending welcome email to ${data.email}`) return this.post('/send/welcome', data) } /** * Send email verification link */ async sendVerification( data: EmailUserData & { verificationToken: string } ): Promise { this.logger.log(`Sending verification email to ${data.email}`) return this.post('/send/verification', data) } /** * Send password reset link. * Accepts resetToken (NOT resetUrl) to match the email service internal API contract. */ async sendPasswordReset( data: EmailUserData & { resetToken: string; expiresInMinutes?: number } ): Promise { this.logger.log(`Sending password reset email to ${data.email}`) return this.post('/send/password-reset', data) } /** * Send password changed confirmation */ async sendPasswordChanged(data: EmailUserData): Promise { this.logger.log(`Sending password changed email to ${data.email}`) return this.post('/send/password-changed', data) } /** * Send account locked notification */ async sendAccountLocked( data: EmailUserData & { reason?: string; unlockUrl?: string } ): Promise { this.logger.log(`Sending account locked email to ${data.email}`) return this.post('/send/account-locked', data) } /** * Send new login alert */ async sendLoginAlert( data: EmailUserData & { device?: string location?: string ipAddress?: string } ): Promise { this.logger.log(`Sending login alert to ${data.email}`) return this.post('/send/login-alert', data) } /** * Send OTP code for MFA */ async sendOtp( data: EmailUserData & { code: string; expiresInMinutes?: number } ): Promise { this.logger.log(`Sending OTP to ${data.email}`) return this.post('/send/otp', data) } /** * Send email using a named template with variable substitution. * This is the generic method for sending any template-based email. */ async sendTemplate(options: SendTemplateEmailOptions): Promise { this.logger.log(`Sending template email "${options.templateName}" to ${options.to}`) return this.post('/send/template', options) } /** * Send a custom email with raw HTML content. * Use this when no template exists for the email type. */ async sendCustom(options: SendCustomEmailOptions): Promise { this.logger.log(`Sending custom email "${options.subject}" to ${options.to}`) return this.post('/send/custom', options) } }