feat: send welcome emails through smtp

This commit is contained in:
Cauê Faleiros
2026-09-08 12:17:41 -03:00
parent 03a6491989
commit ee8d365321
6 changed files with 52 additions and 19 deletions

View File

@@ -19,8 +19,11 @@ const environmentSchema = z.object({
INVITATION_TTL_HOURS: z.coerce.number().int().min(1).max(24 * 90).default(24 * 7),
PASSWORD_RESET_TTL_HOURS: z.coerce.number().int().min(1).max(24 * 30).default(24),
AUDIT_LOG_PAGE_SIZE: z.coerce.number().int().min(10).max(500).default(50),
RESEND_API_KEY: optionalEnvironmentValue(z.string().min(20)),
EMAIL_FROM: optionalEnvironmentValue(z.string().trim().min(3).max(320)),
SMTP_HOST: optionalEnvironmentValue(z.string().trim().min(1).max(255)),
SMTP_PORT: optionalEnvironmentValue(z.coerce.number().int().min(1).max(65_535)),
SMTP_USER: optionalEnvironmentValue(z.string().trim().min(1).max(320)),
SMTP_PASS: optionalEnvironmentValue(z.string().min(1).max(1_000)),
MAIL_FROM: optionalEnvironmentValue(z.string().trim().min(3).max(320)),
BUNNY_STREAM_LIBRARY_ID: optionalEnvironmentValue(z.coerce.number().int().positive()),
BUNNY_STREAM_API_KEY: optionalEnvironmentValue(z.string().min(20)),
BUNNY_EMBED_TOKEN_KEY: optionalEnvironmentValue(z.string().min(20)),
@@ -61,6 +64,7 @@ if (bunnyStorageConfigurationValues.some(Boolean) && !bunnyStorageConfigurationV
throw new Error('BUNNY_STORAGE_ZONE, BUNNY_STORAGE_PASSWORD, BUNNY_STORAGE_ENDPOINT, and BUNNY_STORAGE_CDN_HOST must be configured together.');
}
if (Boolean(config.RESEND_API_KEY) !== Boolean(config.EMAIL_FROM)) {
throw new Error('RESEND_API_KEY and EMAIL_FROM must be configured together.');
const smtpConfigurationValues = [config.SMTP_HOST, config.SMTP_PORT, config.SMTP_USER, config.SMTP_PASS, config.MAIL_FROM];
if (smtpConfigurationValues.some(Boolean) && !smtpConfigurationValues.every(Boolean)) {
throw new Error('SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASS, and MAIL_FROM must be configured together.');
}

View File

@@ -133,7 +133,7 @@ export const authRoutes: FastifyPluginAsync = async (app) => {
const token = await reply.jwtSign(user, { expiresIn: config.JWT_SESSION_TTL });
try {
const sent = await sendWelcomeEmail({ name: user.name, email: user.email });
if (!sent && config.RESEND_API_KEY) request.log.warn({ email: user.email }, 'Welcome email was rejected by the provider');
if (!sent && config.SMTP_HOST) request.log.warn({ email: user.email }, 'Welcome email was rejected by the SMTP server');
} catch (error) {
request.log.error(error, 'Welcome email could not be sent');
}

View File

@@ -1,20 +1,23 @@
import { config } from '../config.js';
import nodemailer from 'nodemailer';
type WelcomeRecipient = { name: string; email: string };
export async function sendWelcomeEmail(recipient: WelcomeRecipient) {
if (!config.RESEND_API_KEY || !config.EMAIL_FROM) return false;
if (!config.SMTP_HOST || !config.SMTP_PORT || !config.SMTP_USER || !config.SMTP_PASS || !config.MAIL_FROM) return false;
const escapedName = recipient.name.replace(/[&<>"']/g, (character) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#039;' }[character] || character));
const response = await fetch('https://api.resend.com/emails', {
method: 'POST',
headers: { Authorization: `Bearer ${config.RESEND_API_KEY}`, 'Content-Type': 'application/json' },
body: JSON.stringify({
from: config.EMAIL_FROM,
to: [recipient.email],
subject: 'Bem-vindo(a) ao Compor HUB',
html: `<main style="font-family:Arial,sans-serif;color:#18181b;line-height:1.55"><h1>Bem-vindo(a), ${escapedName}!</h1><p>Sua conta no <strong>Compor HUB</strong> já está pronta.</p><p>Agora você pode acessar cursos, acompanhar suas aulas e baixar materiais complementares.</p><p><a href="${config.FRONTEND_ORIGIN}" style="display:inline-block;background:#f97316;color:#fff;padding:12px 18px;border-radius:8px;text-decoration:none;font-weight:700">Acessar Compor HUB</a></p></main>`,
}),
const transporter = nodemailer.createTransport({
host: config.SMTP_HOST,
port: config.SMTP_PORT,
secure: config.SMTP_PORT === 465,
auth: { user: config.SMTP_USER, pass: config.SMTP_PASS },
});
return response.ok;
await transporter.sendMail({
from: config.MAIL_FROM,
to: recipient.email,
subject: 'Bem-vindo(a) ao Compor HUB',
html: `<main style="font-family:Arial,sans-serif;color:#18181b;line-height:1.55"><h1>Bem-vindo(a), ${escapedName}!</h1><p>Sua conta no <strong>Compor HUB</strong> já está pronta.</p><p>Agora você pode acessar cursos, acompanhar suas aulas e baixar materiais complementares.</p><p><a href="${config.FRONTEND_ORIGIN}" style="display:inline-block;background:#f97316;color:#fff;padding:12px 18px;border-radius:8px;text-decoration:none;font-weight:700">Acessar Compor HUB</a></p></main>`,
});
return true;
}