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#
| Provider | Requires | Notes |
|---|---|---|
| resend | apiKey | Zero-deps, HTTP-based |
| sendgrid | apiKey | Zero-deps, HTTP-based |
| mailgun | apiKey + domain | Zero-deps, HTTP-based |
| smtp | smtp config | Requires nodemailer (bun add nodemailer) |
Resend#
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#
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#
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)#
nodemailer: bun add nodemailerconst 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.
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.
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).
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.
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.
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#
| Syntax | Description |
|---|---|
| {{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.
// 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",
},
});Mailable Classes#
Laravel-style class-based email definitions. Extend Mailable, define template/subject, and build context in build().
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#
| Property | Description |
|---|---|
| template | Template name (registered via registerTemplate) |
| subject | Email subject line |
| from | Override sender address |
| to | Override recipient |
| cc | CC recipients |
| bcc | BCC recipients |
| replyTo | Reply-To address |
| build() | Returns { context?, from?, to? } |
MailOptions#
| Field | Type | Required | Description |
|---|---|---|---|
| from | string | Yes | Sender email address |
| to | string | string[] | Yes | Recipient(s) |
| cc | string | string[] | No | Carbon copy recipients |
| bcc | string | string[] | No | Blind carbon copy recipients |
| replyTo | string | string[] | No | Reply-to address |
| subject | string | Yes | Email subject |
| text | string | No | Plain text body |
| html | string | No | HTML body |
| attachments | MailAttachment[] | No | File attachments |
MailAttachment#
| Field | Type | Required | Description |
|---|---|---|---|
| filename | string | Yes | Display name for the file |
| content | Buffer | string | No* | File content (Buffer or base64) |
| path | string | No* | Remote URL (Resend only) |
| contentType | string | No | MIME type (auto-inferred if omitted) |
| cid | string | No | Content-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:
// 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" });