import { Injectable, Logger } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import nodemailer, { Transporter } from 'nodemailer'; interface ProjectInviteTemplate { inviteLink: string; projectName: string; inviterEmail: string; } interface ReleaseUpdateTemplate { projectName: string; projectSlug: string; environment: string; version: string; branch: string; build: number; date: string; commitMessage: string | null; actorName: string; actorEmail: string; isNewRelease: boolean; } @Injectable() export class EmailService { private readonly logger = new Logger(EmailService.name); private readonly transporter: Transporter | null; constructor(private readonly configService: ConfigService) { this.transporter = this.createTransporter(); } async sendProjectInvite(recipient: string, template: ProjectInviteTemplate) { if (!this.transporter) { this.logger.warn( `Email transport not configured. Invite for ${recipient}: ${template.inviteLink}`, ); return; } try { await this.transporter.sendMail({ from: this.configService.get('EMAIL_FROM'), to: recipient, subject: `Invitation to collaborate on ${template.projectName}`, text: this.buildPlainText(template), html: this.buildHtml(template), }); } catch (error) { this.logger.error( 'Failed to send invite email', error instanceof Error ? error.stack : undefined, ); throw error; } } async sendReleaseUpdateNotification(recipient: string, template: ReleaseUpdateTemplate) { if (!this.transporter) { this.logger.warn( `Email transport not configured. Release update for ${recipient} in ${template.projectSlug}`, ); return; } try { await this.transporter.sendMail({ from: this.configService.get('EMAIL_FROM'), to: recipient, subject: this.buildReleaseSubject(template), text: this.buildReleasePlainText(template), html: this.buildReleaseHtml(template), }); } catch (error) { this.logger.error( 'Failed to send release update email', error instanceof Error ? error.stack : undefined, ); throw error; } } private createTransporter(): Transporter | null { const host = this.configService.get('SMTP_HOST'); const port = Number(this.configService.get('SMTP_PORT')); if (!host || !port) { this.logger.warn('SMTP configuration missing. Emails will not be sent.'); return null; } const secure = this.configService.get('SMTP_SECURE') === 'true' || port === 465; const auth = { user: this.configService.get('SMTP_USER'), pass: this.configService.get('SMTP_PASSWORD'), }; return nodemailer.createTransport({ host, port, secure, auth, }); } private buildPlainText({ projectName, inviteLink, inviterEmail }: ProjectInviteTemplate) { return ` ${inviterEmail} has invited you to collaborate on the project "${projectName}". To accept the invitation, open this link: ${inviteLink} If you did not expect this email, you can safely ignore it. `; } private buildHtml({ projectName, inviteLink, inviterEmail }: ProjectInviteTemplate) { return `

You've been invited!

${inviterEmail} has invited you to collaborate on the project ${projectName}.

Accept Invitation

If the button above doesn’t work, copy and paste this link into your browser:

${inviteLink}


This email was sent automatically. If you did not expect it, you can ignore it.

`; } private buildReleaseSubject(template: ReleaseUpdateTemplate) { const action = template.isNewRelease ? 'created' : 'updated'; return `${template.projectName}: ${template.environment} release ${action}`; } private buildReleasePlainText(template: ReleaseUpdateTemplate) { const action = template.isNewRelease ? 'created' : 'updated'; const lines = [ `${template.actorName} (${template.actorEmail}) ${action} a release for ${template.projectName}.`, '', `Environment: ${template.environment}`, `Version: ${template.version}`, `Branch: ${template.branch}`, `Build: ${template.build}`, `Date: ${template.date}`, ]; if (template.commitMessage) { lines.push(`Commit message: ${template.commitMessage}`); } lines.push('', 'You are receiving this because you collaborate on this project.'); return lines.join('\n'); } private buildReleaseHtml(template: ReleaseUpdateTemplate) { const action = template.isNewRelease ? 'created' : 'updated'; const commitMessageSection = template.commitMessage ? `

Commit message: ${template.commitMessage}

` : ''; return `

${template.projectName} — ${template.environment} release ${action}

${template.actorName} (${template.actorEmail}) ${action} a release for this project.

Version: ${template.version}

Branch: ${template.branch}

Build: ${template.build}

Date: ${template.date}

${commitMessageSection}

You are receiving this because you collaborate on ${template.projectName}.

`; } }