Pearl's mail system is built around Mailable classes. Instead of calling a mailer function directly with a pile of arguments, you create a class that represents one specific email — its subject, recipient, and body — and send it with a Mailer.
This makes emails easy to test and easy to reuse across your app. Requires nodemailer v7+ — types are bundled, so @types/nodemailer is not needed.
Create a Mailable
pearl make:mail WelcomeMail
# → src/mail/WelcomeMail.tsImplement the build() method and chain the fluent helpers:
import { Mailable } from '@pearl-framework/pearl'
export class WelcomeMail extends Mailable {
constructor(private readonly user: User) {
super()
}
build() {
return this
.sendTo(this.user.email)
.from({ name: 'My App', address: 'hi@myapp.com' })
.subject(`Welcome to My App, ${this.user.name}!`)
.html(`
<h1>Hi ${this.user.name}, welcome aboard! 🎉</h1>
<p>Thanks for signing up. You're all set to get started.</p>
<a href="https://myapp.com/dashboard">Go to your dashboard →</a>
`)
.text(`Hi ${this.user.name}, welcome! Visit https://myapp.com/dashboard`)
}
}Sending mail
import { Mailer, LogTransport } from '@pearl-framework/pearl'
import { WelcomeMail } from '../mail/WelcomeMail.js'
const mailer = new Mailer(new LogTransport()) // logs to console — great for dev
await mailer.send(new WelcomeMail(user))Transports
| Transport | When to use it |
|---|---|
LogTransport | Development — prints the full email to the console, nothing is sent |
SmtpTransport | Production — sends via any SMTP provider (Postmark, SendGrid, Resend, SES, etc.) |
SesTransport | AWS SES — requires @aws-sdk/client-ses |
ArrayTransport | Testing — captures sent mail in memory so you can assert against it |
SMTP transport (production)
import { Mailer, SmtpTransport } from '@pearl-framework/pearl'
const mailer = new Mailer(
new SmtpTransport({
host: process.env.MAIL_HOST!,
port: Number(process.env.MAIL_PORT ?? 587),
secure: false,
auth: {
user: process.env.MAIL_USER!,
pass: process.env.MAIL_PASS!,
},
})
)MAIL_HOST=smtp.postmarkapp.com
MAIL_PORT=587
MAIL_USER=your-api-token
MAIL_PASS=your-api-tokenAttachments
build() {
return this
.sendTo(this.user.email)
.subject('Your invoice')
.html('<p>Please find your invoice attached.</p>')
.attach({
filename: 'invoice.pdf',
path: `/tmp/invoices/${this.invoiceId}.pdf`,
contentType: 'application/pdf',
})
}Testing with ArrayTransport
Swap in ArrayTransport in tests to capture sent mail in memory and assert against it — no SMTP server needed:
import { ArrayTransport, Mailer } from '@pearl-framework/pearl'
const transport = new ArrayTransport()
const mailer = new Mailer(transport)
await mailer.send(new WelcomeMail(user))
const sent = transport.last()
assert.equal(sent?.subject, `Welcome to My App, ${user.name}!`)
assert.equal(transport.sent.length, 1)
transport.clear() // reset between testsTip: always send mail via a queue
SMTP calls can be slow and fail. Instead of sending in your route handler, dispatch a queue job to send in the background:
// ❌ Blocks the response — slow and fragile
await mailer.send(new WelcomeMail(user.email))
// ✅ Returns immediately — job runs in the background
const job = new SendWelcomeEmailJob()
job.userId = user.id
await queue.dispatch(job)