/** * Email Service * Handles sending emails using nodemailer */ import { logger } from "./logger"; const SMTP_HOST = import.meta.env.SMTP_HOST; const SMTP_PORT = parseInt(import.meta.env.SMTP_PORT || "587"); const SMTP_USER = import.meta.env.SMTP_USER; const SMTP_PASSWORD = import.meta.env.SMTP_PASSWORD; const SMTP_FROM = import.meta.env.SMTP_FROM || "noreply@opencodehub.local"; const SITE_URL = import.meta.env.SITE_URL || "http://localhost:4321"; export interface EmailOptions { to: string; subject: string; text?: string; html?: string; } export function isSmtpConfigured(): boolean { return Boolean(SMTP_HOST); } let transporter: any = null; async function getTransporter() { if (transporter) return transporter; if (!SMTP_HOST) { logger.warn("SMTP not configured, emails will be logged only"); return null; } try { const nodemailer = await import("nodemailer"); transporter = nodemailer.createTransport({ host: SMTP_HOST, port: SMTP_PORT, secure: SMTP_PORT === 465, auth: SMTP_USER ? { user: SMTP_USER, pass: SMTP_PASSWORD, } : undefined, }); // Verify connection await transporter.verify(); logger.info("SMTP connection verified"); return transporter; } catch (error) { logger.error({ error }, "Failed to create SMTP transporter"); return null; } } export async function sendEmail(options: EmailOptions): Promise { try { const transport = await getTransporter(); if (!transport) { // Log email for development/testing logger.info({ to: options.to, subject: options.subject }, "Email would be sent (SMTP not configured)"); return true; } await transport.sendMail({ from: SMTP_FROM, to: options.to, subject: options.subject, text: options.text, html: options.html, }); logger.info({ to: options.to, subject: options.subject }, "Email sent"); return true; } catch (error) { logger.error({ error, to: options.to }, "Failed to send email"); return false; } } export async function sendTestEmail(options: { to: string; dryRun?: boolean; }): Promise { const dryRun = options.dryRun ?? true; if (dryRun) { logger.info({ to: options.to }, "Test email dry-run (not sent)"); return true; } return sendEmail({ to: options.to, subject: "OpenCodeHub email delivery test", text: "This is a test email from OpenCodeHub. If you received this, email delivery is working.", html: `

OpenCodeHub email delivery test

This is a test email from OpenCodeHub.

If you received this, your email delivery configuration is working.

`, }); } export async function sendPasswordResetEmail(email: string, token: string): Promise { const resetUrl = `${SITE_URL}/reset-password?token=${token}`; return sendEmail({ to: email, subject: "Reset your OpenCodeHub password", text: ` You requested a password reset for your OpenCodeHub account. Click the link below to reset your password: ${resetUrl} This link will expire in 1 hour. If you didn't request this, you can safely ignore this email. `.trim(), html: `

Reset Your Password

You requested a password reset for your OpenCodeHub account. Click the button below to set a new password.

Reset Password

This link will expire in 1 hour. If you didn't request this, you can safely ignore this email.

© ${new Date().getFullYear()} OpenCodeHub. All rights reserved.

`.trim(), }); } export async function sendVerificationEmail(email: string, token: string): Promise { const verifyUrl = `${SITE_URL}/verify-email?token=${token}`; return sendEmail({ to: email, subject: "Verify your OpenCodeHub email", text: ` Welcome to OpenCodeHub! Please verify your email by clicking the link below: ${verifyUrl} This link will expire in 24 hours. `.trim(), html: `

Welcome to OpenCodeHub! 🎉

Thanks for signing up! Please verify your email address to get started.

Verify Email

This link will expire in 24 hours.

© ${new Date().getFullYear()} OpenCodeHub. All rights reserved.

`.trim(), }); } export async function sendPullRequestEmail( to: string, type: "opened" | "merged" | "closed" | "review_requested", pr: { title: string; number: number; url: string; repository: { name: string; owner: { username: string } }; author: { username: string }; } ): Promise { const subjectPrefix = `[${pr.repository.owner.username}/${pr.repository.name}]`; let action = ""; switch (type) { case "opened": action = "opened"; break; case "merged": action = "merged"; break; case "closed": action = "closed"; break; case "review_requested": action = "requested your review on"; break; } return sendEmail({ to, subject: `${subjectPrefix} PR #${pr.number} ${action}: ${pr.title}`, html: `

Pull Request ${action}

${pr.author.username} ${action} pull request #${pr.number}: ${pr.title}

` }); } export async function sendIssueEmail( to: string, type: "opened" | "closed" | "assigned", issue: { title: string; number: number; url: string; repository: { name: string; owner: { username: string } }; author: { username: string }; } ): Promise { const subjectPrefix = `[${issue.repository.owner.username}/${issue.repository.name}]`; let action = type; return sendEmail({ to, subject: `${subjectPrefix} Issue #${issue.number} ${action}: ${issue.title}`, html: `

Issue ${action}

${issue.author.username} ${action} issue #${issue.number}: ${issue.title}

` }); }