You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

206 lines
6.6 KiB
TypeScript

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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<string>('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<string>('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<string>('SMTP_HOST');
const port = Number(this.configService.get<string>('SMTP_PORT'));
if (!host || !port) {
this.logger.warn('SMTP configuration missing. Emails will not be sent.');
return null;
}
const secure =
this.configService.get<string>('SMTP_SECURE') === 'true' || port === 465;
const auth = {
user: this.configService.get<string>('SMTP_USER'),
pass: this.configService.get<string>('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 `
<div style="font-family: Arial, sans-serif; padding: 20px; color: #333;">
<h2 style="font-weight: 600; font-size: 20px;">You've been invited!</h2>
<p style="font-size: 15px;">
<strong>${inviterEmail}</strong> has invited you to collaborate on the project
<strong>${projectName}</strong>.
</p>
<a href="${inviteLink}"
style="display: inline-block; margin: 20px 0; padding: 12px 22px; background-color: #4f46e5;
color: #fff; text-decoration: none; border-radius: 6px; font-size: 15px;">
Accept Invitation
</a>
<p style="font-size: 14px; color: #666; margin-top: 20px;">
If the button above doesnt work, copy and paste this link into your browser:
</p>
<p style="font-size: 14px; color: #555;">
${inviteLink}
</p>
<hr style="margin: 30px 0; border: none; border-top: 1px solid #eee;" />
<p style="font-size: 12px; color: #999;">
This email was sent automatically. If you did not expect it, you can ignore it.
</p>
</div>
`;
}
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
? `<p style="font-size: 14px; color: #444;"><strong>Commit message:</strong> ${template.commitMessage}</p>`
: '';
return `
<div style="font-family: Arial, sans-serif; padding: 20px; color: #333;">
<h2 style="font-weight: 600; font-size: 20px; margin-bottom: 10px;">
${template.projectName} &mdash; ${template.environment} release ${action}
</h2>
<p style="font-size: 15px; color: #444;">
${template.actorName} (${template.actorEmail}) ${action} a release for this project.
</p>
<div style="font-size: 14px; line-height: 1.6; color: #333; background: #f5f5ff; padding: 16px; border-radius: 8px;">
<p style="margin: 0;"><strong>Version:</strong> ${template.version}</p>
<p style="margin: 0;"><strong>Branch:</strong> ${template.branch}</p>
<p style="margin: 0;"><strong>Build:</strong> ${template.build}</p>
<p style="margin: 0;"><strong>Date:</strong> ${template.date}</p>
</div>
${commitMessageSection}
<p style="font-size: 12px; color: #777; margin-top: 20px;">
You are receiving this because you collaborate on ${template.projectName}.
</p>
</div>
`;
}
}