Mailer#

Email sending with built-in support for Resend, SendGrid, and Mailgun (zero-deps HTTP). SMTP via optional nodemailer import. Supports attachments, CC/BCC, reply-to, inline images, template-based sending, provider-side templates, and Laravel-style Mailable classes.

Providers#

ProviderRequiresNotes
resendapiKeyZero-deps, HTTP-based
sendgridapiKeyZero-deps, HTTP-based
mailgunapiKey + domainZero-deps, HTTP-based
smtpsmtp configRequires nodemailer (bun add nodemailer)

Resend#

typescript
import { Mailer } from "@buntok/core/mailer";

const mailer = new Mailer({
  provider: "resend",
  apiKey: process.env.RESEND_API_KEY,
});

const result = await mailer.send({
  from: "noreply@example.com",
  to: "user@example.com",
  subject: "Welcome!",
  html: "<h1>Welcome to our platform</h1>",
});

console.log(result); // { success: true, id: "..." }

SendGrid#

typescript
const mailer = new Mailer({
  provider: "sendgrid",
  apiKey: process.env.SENDGRID_API_KEY,
});

await mailer.send({
  from: "noreply@example.com",
  to: ["user1@example.com", "user2@example.com"],
  subject: "Newsletter",
  text: "Plain text version",
  html: "<p>HTML version</p>",
});

Mailgun#

typescript
const mailer = new Mailer({
  provider: "mailgun",
  apiKey: process.env.MAILGUN_API_KEY,
  domain: "mg.example.com",
});

await mailer.send({
  from: "noreply@mg.example.com",
  to: "user@example.com",
  subject: "Hello",
  html: "<h1>Hello World</h1>",
});

SMTP (nodemailer)#

Warning
SMTP requires nodemailer: bun add nodemailer
typescript
const mailer = new Mailer({
  provider: "smtp",
  smtp: {
    host: "smtp.gmail.com",
    port: 587,
    secure: false,
    auth: {
      user: process.env.SMTP_USER,
      pass: process.env.SMTP_PASS,
    },
  },
});

await mailer.send({
  from: "user@gmail.com",
  to: "recipient@example.com",
  subject: "Hello via SMTP",
  html: "<p>Sent via SMTP</p>",
});

CC & BCC#

Send copies to additional recipients. CC recipients are visible to all; BCC recipients are hidden.

typescript
await mailer.send({
  from: "noreply@example.com",
  to: "user@example.com",
  cc: "manager@example.com",
  bcc: ["audit@example.com", "logs@example.com"],
  subject: "Invoice #123",
  html: "<p>See attached invoice</p>",
});

Reply-To#

Set a different reply-to address when the sender address is no-reply.

typescript
await mailer.send({
  from: "noreply@example.com",
  replyTo: "support@example.com",
  to: "user@example.com",
  subject: "Your account",
  html: "<p>Reply to our support team</p>",
});

Attachments#

Attach files using Buffer, base64 string, or remote URL (Resend only).

typescript
import { readFileSync } from "fs";

// From buffer
const pdfBuffer = readFileSync("./invoice.pdf");
await mailer.send({
  from: "noreply@example.com",
  to: "user@example.com",
  subject: "Your Invoice",
  html: "<p>See attached invoice</p>",
  attachments: [
    {
      filename: "invoice.pdf",
      content: pdfBuffer,
      contentType: "application/pdf",
    },
  ],
});

// From base64 string
await mailer.send({
  from: "noreply@example.com",
  to: "user@example.com",
  subject: "Report",
  html: "<p>See attached report</p>",
  attachments: [
    {
      filename: "report.xlsx",
      content: base64EncodedData,
      contentType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
    },
  ],
});

// From remote URL (Resend only)
await mailer.send({
  from: "noreply@example.com",
  to: "user@example.com",
  subject: "Download",
  html: "<p>Download attached</p>",
  attachments: [
    {
      filename: "document.pdf",
      path: "https://example.com/document.pdf",
    },
  ],
});

Inline Images (CID)#

Embed images directly in HTML emails using Content-ID (CID). Reference the image in HTML with cid:your-id.

typescript
const logoBase64 = readFileSync("./logo.png").toString("base64");

await mailer.send({
  from: "noreply@example.com",
  to: "user@example.com",
  subject: "Welcome!",
  html: `
    <h1>Welcome!</h1>
    <img src="cid:company-logo" alt="Company Logo" />
    <p>Thank you for joining us.</p>
  `,
  attachments: [
    {
      filename: "logo.png",
      content: logoBase64,
      contentType: "image/png",
      cid: "company-logo",  // Matches cid: in HTML
    },
  ],
});

Template Integration#

Use the built-in TemplateEngine to register, render, and send Handlebars-like email templates. Templates support variables, conditionals, loops, partials, and custom helpers.

typescript
import { Mailer } from "@buntok/core/mailer";

const mailer = new Mailer({
  provider: "resend",
  apiKey: process.env.RESEND_API_KEY,
});

// Register a template (Handlebars-like syntax)
mailer.registerTemplate("welcome", `
  <h1>Selamat datang, {{name}}!</h1>
  <p>Email: {{email}}</p>
  <p>Gunakan kode <strong>{{code}}</strong> untuk verifikasi.</p>
`);

// Register a partial (layout)
mailer.registerPartial("email-layout", `
  <div style="max-width:600px;margin:0 auto;">
    {{{body}}}
    <hr>
    <p style="font-size:12px;color:#999;">BunTok Framework</p>
  </div>
`);

// Register a custom helper
mailer.registerHelper("upper", (text) => text.toUpperCase());

// Send with template
await mailer.sendTemplate({
  from: "noreply@example.com",
  to: "user@example.com",
  subject: "Welcome!",
  template: "welcome",
  context: { name: "Tok", email: "tok@example.com", code: "ABC123" },
});

Template Syntax#

SyntaxDescription
{{var}}Variable (HTML-escaped)
{{{var}}}Variable (unescaped HTML)
{{#if condition}}...{{/if}}Conditional block
{{#if}}...{{else}}...{{/if}}Conditional with else
{{#unless condition}}...{{/unless}}Inverse conditional
{{#each items}}...{{/each}}Loop over array
{{> partialName}}Include a partial
{{! comment }}Comment (not rendered)

Provider-Side Templates#

Use Resend or SendGrid native template systems. Templates are managed in the provider's dashboard and referenced by ID.

typescript
// Resend - template UUID from Resend dashboard
await mailer.sendProviderTemplate({
  from: "noreply@example.com",
  to: "user@example.com",
  subject: "Welcome!",
  templateId: "template-uuid-123",
  templateData: {
    name: "Tok",
    action_url: "https://example.com/verify",
  },
});

// SendGrid - template_id from SendGrid dashboard
await mailer.sendProviderTemplate({
  from: "noreply@example.com",
  to: "user@example.com",
  subject: "Welcome!",
  templateId: "d-abc123",
  templateData: {
    name: "Tok",
    action_url: "https://example.com/verify",
  },
});
Info
Provider-side templates are only supported for Resend and SendGrid. Mailgun and SMTP use local template rendering.

Mailable Classes#

Laravel-style class-based email definitions. Extend Mailable, define template/subject, and build context in build().

typescript
import { Mailer, Mailable } from "@buntok/core/mailer";

class WelcomeEmail extends Mailable {
  template = "welcome";
  subject = "Welcome to BunTok!";

  constructor(private name: string, private email: string) {
    super();
  }

  build() {
    return { context: { name: this.name, email: this.email } };
  }
}

class PasswordResetEmail extends Mailable {
  template = "password-reset";
  subject = "Reset your password";
  from = "security@example.com";

  constructor(private token: string) {
    super();
  }

  build() {
    return { context: { token: this.token, expires: "30 minutes" } };
  }
}

// Usage
const mailer = new Mailer({ provider: "resend", apiKey: process.env.RESEND_API_KEY });

// Register templates first
mailer.registerTemplate("welcome", "<h1>Halo, {{name}}!</h1><p>{{email}}</p>");
mailer.registerTemplate("password-reset", "<p>Reset link: {{token}}</p>");

await mailer.sendMailable(new WelcomeEmail("Tok", "tok@example.com"));
await mailer.sendMailable(new PasswordResetEmail("abc123"));

Mailable Properties#

PropertyDescription
templateTemplate name (registered via registerTemplate)
subjectEmail subject line
fromOverride sender address
toOverride recipient
ccCC recipients
bccBCC recipients
replyToReply-To address
build()Returns { context?, from?, to? }

MailOptions#

FieldTypeRequiredDescription
fromstringYesSender email address
tostring | string[]YesRecipient(s)
ccstring | string[]NoCarbon copy recipients
bccstring | string[]NoBlind carbon copy recipients
replyTostring | string[]NoReply-to address
subjectstringYesEmail subject
textstringNoPlain text body
htmlstringNoHTML body
attachmentsMailAttachment[]NoFile attachments

MailAttachment#

FieldTypeRequiredDescription
filenamestringYesDisplay name for the file
contentBuffer | stringNo*File content (Buffer or base64)
pathstringNo*Remote URL (Resend only)
contentTypestringNoMIME type (auto-inferred if omitted)
cidstringNoContent-ID for inline images

* Either content or path is required.

Fire-and-Forget#

Don't await for non-critical emails - they'll send in the background:

typescript
// Don't await - runs in background
mailer.send({
  from: "noreply@example.com",
  to: user.email,
  subject: "Welcome!",
  html: welcomeTemplate,
});

// Response is instant
return ctx.json({ message: "Account created" });