SPB Git forge

spb/polyllm

Public
15commits 1branches 0releases
2.2 MBsize
maindefault branch
14 days agolast push
TypeScript 97.4% SQL 1% JavaScript 0.9% CSS 0.6%
2.7 KB · 76 lines typescript
Raw Blame History
1import "server-only";2import { Resend } from "resend";3import { APP_URL } from "@/lib/env";4import { log } from "@/lib/log";5import * as T from "./templates";67export interface SendResult {8  id: string | null;9  skipped?: boolean;10}1112class EmailService {13  private resend: Resend | null;14  private from: string;15  private dryRun: boolean;16  private siteUrl = APP_URL;1718  constructor() {19    const apiKey = process.env.RESEND_API_KEY;20    this.from = process.env.RESEND_FROM_EMAIL ?? "PolyLLM <polyllm@mail.spboucher.ai>";21    this.dryRun = !apiKey || process.env.EMAIL_DRY_RUN === "1";22    this.resend = apiKey ? new Resend(apiKey) : null;23  }2425  private async send(to: string, content: T.EmailContent, kind: string): Promise<SendResult> {26    if (this.dryRun || !this.resend) {27      log.info("email dry-run", { kind, to: to.replace(/(.).+(@.*)/, "$1***$2") });28      if (process.env.EMAIL_DRY_RUN_PRINT === "1") console.log(content.text);29      return { id: null, skipped: true };30    }31    const { data, error } = await this.resend.emails.send({32      from: this.from,33      to: [to],34      subject: content.subject,35      html: content.html,36      text: content.text,37    });38    if (error) {39      log.error("email send failed", { kind, error: error.message, name: error.name });40      throw new Error(`Email delivery failed: ${error.message}`);41    }42    log.info("email sent", { kind, id: data?.id });43    return { id: data?.id ?? null };44  }4546  sendVerification(to: string, url: string) {47    return this.send(to, T.verificationEmail(this.siteUrl, url), "verification");48  }49  sendWelcome(to: string, name: string) {50    return this.send(to, T.welcomeEmail(this.siteUrl, name), "welcome");51  }52  sendPasswordReset(to: string, url: string) {53    return this.send(to, T.passwordResetEmail(this.siteUrl, url), "password-reset");54  }55  sendPasswordChanged(to: string, ip?: string | null) {56    return this.send(to, T.passwordChangedEmail(this.siteUrl, new Date().toUTCString(), ip), "password-changed");57  }58  sendEmailChangeApproval(to: string, url: string, newEmail: string) {59    return this.send(to, T.emailChangeApprovalEmail(this.siteUrl, url, newEmail), "email-change-approval");60  }61  sendEmailChanged(to: string, newEmail: string) {62    return this.send(to, T.emailChangedEmail(this.siteUrl, newEmail), "email-changed");63  }64  sendAccountDeletion(to: string, url: string) {65    return this.send(to, T.accountDeletionEmail(this.siteUrl, url), "account-deletion");66  }67  sendNewLogin(to: string, ip?: string | null, userAgent?: string | null) {68    return this.send(to, T.newLoginEmail(this.siteUrl, new Date().toUTCString(), ip, userAgent), "new-login");69  }70}7172let service: EmailService | null = null;73export function getEmailService(): EmailService {74  return (service ??= new EmailService());75}76