import { readFileSync } from 'node:fs' import nodemailer, { type Transporter } from 'nodemailer' type VerificationEmailData = { email: string; url: string; name?: string } type PasswordResetEmailData = { email: string; url: string; name?: string } type InvitationEmailData = { email: string; url: string; organizationName: string } // Colores de marca (mismo sistema que la web: index.css de apps/web). const BRAND_600 = '#2544ea' const TEXT_STRONG = '#0f172a' const TEXT_MUTED = '#475569' const ON_ACCENT = '#ffffff' const LINK_BG = BRAND_600 // Logotipo embebido como data URI para que los mails no dependan de hosting. function loadLogoTypoDataUri(): string { try { const base64 = readFileSync( new URL('../assets/logotipo-y-organica.png', import.meta.url), ).toString('base64') return `data:image/png;base64,${base64}` } catch { return '' } } const LOGO_DATA_URI = loadLogoTypoDataUri() interface EmailProvider { sendVerificationEmail(data: VerificationEmailData): Promise sendPasswordResetEmail(data: PasswordResetEmailData): Promise sendOrganizationInvitation(data: InvitationEmailData): Promise } class SMTPEmailProvider implements EmailProvider { private transporter: Transporter constructor() { this.transporter = nodemailer.createTransport({ host: process.env.EMAIL_SERVER_HOST, port: Number(process.env.EMAIL_SERVER_PORT ?? 587), secure: (process.env.EMAIL_SERVER_PORT ?? '587') === '465', auth: { user: process.env.EMAIL_SERVER_USER, pass: process.env.EMAIL_SERVER_PASSWORD, }, }) } private from() { return process.env.EMAIL_FROM ?? 'Gruperly ' } private layout(title: string, body: string) { const header = LOGO_DATA_URI ? `Gruperly` : '

Gruperly

' return `
${header}

${title}

${body}

` } private async send(to: string, subject: string, html: string) { // Sin SMTP configurado (dev) no rompe el flujo: solo registra un aviso if (!process.env.EMAIL_SERVER_HOST) { console.warn(`[email] SMTP no configurado. Email no enviado a ${to}: "${subject}"`) return } await this.transporter.sendMail({ from: this.from(), to, subject, html }) } async sendVerificationEmail({ email, url, name }: VerificationEmailData) { const link = `Verificar email` await this.send( email, 'Verifica tu email en Gruperly', this.layout( 'Confirma tu dirección de email', `${name ? `Hola ${name}, ` : 'Hola, '}gracias por crear tu cuenta en Gruperly. Para empezar, verifica tu email con el botón de abajo (el enlace vence en 1 hora).

${link}`, ), ) } async sendPasswordResetEmail({ email, url, name }: PasswordResetEmailData) { const link = `Restablecer contraseña` await this.send( email, 'Restablece tu contraseña en Gruperly', this.layout( 'Solicitaste restablecer tu contraseña', `${name ? `Hola ${name}, ` : 'Hola, '}haz clic en el botón para elegir una nueva contraseña (el enlace vence en 1 hora). Si no fuiste tú, ignora este email.

${link}`, ), ) } async sendOrganizationInvitation({ email, url, organizationName }: InvitationEmailData) { const link = `Unirme a ${organizationName}` await this.send( email, `Te invitaron a ${organizationName} en Gruperly`, this.layout( `Te invitaron a ${organizationName}`, `Acepta la invitación con el botón de abajo para empezar a colaborar en Gruperly.

${link}`, ), ) } } // Futuro: proveedores transaccionales (Resend, SendGrid, Postmark). // Añadir implementaciones y alternar con EMAIL_PROVIDER. export function createEmailProvider(): EmailProvider { return new SMTPEmailProvider() } export const emailProvider = createEmailProvider()