@pearl-framework/mail
v1.3.0
Published
Pearl.js mail — Nodemailer-powered mailable classes, transports, and queue support
Maintainers
Readme
@pearl-framework/mail
Mailable classes and multi-transport email sending for Pearl.js.
Installation
npm install @pearl-framework/mail @pearl-framework/core nodemailerRequires nodemailer v7+. Types are bundled —
@types/nodemaileris not needed.
Define a Mailable
A Mailable is a class that encapsulates a single email. Build it with a fluent API:
import { Mailable } from '@pearl-framework/mail'
export class WelcomeEmail extends Mailable {
constructor(private readonly user: User) { super() }
build(): this {
return this
.sendTo(this.user.email)
.from({ name: 'Pearl App', address: '[email protected]' })
.subject(`Welcome to Pearl.js, ${this.user.name}!`)
.html(`
<h1>Hi ${this.user.name}, welcome aboard!</h1>
<p>We're thrilled to have you.</p>
<a href="https://yourapp.com/dashboard">Get started →</a>
`)
.text(`Hi ${this.user.name}, welcome aboard! Visit https://yourapp.com/dashboard`)
}
}Send Mail
const mailer = app.container.make(Mailer)
await mailer.send(new WelcomeEmail(user))Bulk sending with bounded concurrency
sendBulk runs many sends in parallel with a concurrency cap so a 1,000-mail blast doesn't open 1,000 SMTP connections at once. Default cap is 10. Override per call or set a defaultBulkConcurrency on the Mailer.
// Default: cap at 10 in-flight, fail fast on first error
await mailer.sendBulk(mailables)
// Override concurrency for this call
await mailer.sendBulk(mailables, { concurrency: 25 })
// Don't abort on first error — collect failures and return them
const result = await mailer.sendBulk(mailables, {
concurrency: 25,
continueOnError: true,
})
console.log(`Sent ${result.sent}, failed ${result.errors.length}`)
result.errors.forEach(({ index, error }) =>
log.error({ index, err: error }, 'bulk send failure')
)The default cap is set on the Mailer:
const mailer = new Mailer({
transport,
defaultBulkConcurrency: 25,
})Advanced Mailables
Multiple recipients
build(): this {
return this
.sendTo('[email protected]', '[email protected]')
.cc({ name: 'Manager', address: '[email protected]' })
.bcc('[email protected]')
.replyTo('[email protected]')
.subject('Team announcement')
.html('<p>Hello everyone!</p>')
}Attachments
build(): this {
return this
.sendTo(this.user.email)
.subject('Your invoice')
.html('<p>Please find your invoice attached.</p>')
.attach({
filename: 'invoice-2024-01.pdf',
content: await fs.readFile(`/tmp/invoices/${this.invoiceId}.pdf`),
contentType: 'application/pdf',
})
}Security — attachment path/URL access is disabled by default.
SmtpTransportconfigures nodemailer withdisableFileAccess: trueanddisableUrlAccess: true, so passingpath: '/some/file'orhref: 'https://...'on an attachment will fail. This prevents an attacker who controls part of an attachment from reading arbitrary local files or triggering SSRF against internal endpoints. Read the file yourself and pass it ascontent(as shown above). If you genuinely need path/URL fetching, build your own transport that wraps nodemailer without these flags.
Async build
build() can also be async — useful for fetching data inside the mailable:
async build(): Promise<this> {
const stats = await fetchUserStats(this.user.id)
return this
.sendTo(this.user.email)
.subject('Your weekly report')
.html(renderStatsTemplate(stats))
}MailServiceProvider
import { MailServiceProvider } from '@pearl-framework/mail'
import type { MailDriver } from '@pearl-framework/mail'
export class AppMailServiceProvider extends MailServiceProvider {
protected config = {
driver: (process.env.MAIL_DRIVER ?? 'log') as MailDriver,
from: {
name: 'Pearl App',
address: process.env.MAIL_FROM!,
},
smtp: {
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!,
},
},
}
}
app.register(AppMailServiceProvider)Transports
| Transport | Driver value | Description |
|---|---|---|
| SmtpTransport | 'smtp' | Send via any SMTP provider (Mailgun, Postmark, Resend, etc.) |
| SesTransport | 'ses' | Send via AWS SES (@aws-sdk/client-ses required) |
| LogTransport | 'log' | Print email details to the console instead of sending |
| ArrayTransport | 'array' | Capture sent mail in memory — for testing |
Switching by environment
// .env
MAIL_DRIVER=log # development
MAIL_DRIVER=smtp # productionThe LogTransport prints full email details to the console so you can verify emails look correct during development without needing an SMTP server.
Testing with ArrayTransport
import { ArrayTransport, Mailer } from '@pearl-framework/mail'
const transport = new ArrayTransport()
const mailer = new Mailer(transport)
await mailer.send(new WelcomeEmail(user))
const sent = transport.last()
assert.equal(sent?.subject, `Welcome to Pearl, ${user.name}!`)
assert.equal(transport.sent.length, 1)
transport.clear() // reset between testsAPI Reference
Mailable
| Method | Description |
|---|---|
| sendTo(...addresses) | Set recipient(s) — accepts strings or { name, address } objects |
| from(address) | Set the sender |
| cc(...addresses) | Add CC recipients |
| bcc(...addresses) | Add BCC recipients |
| replyTo(address) | Set the reply-to address |
| subject(value) | Set the subject line |
| html(content) | Set the HTML body |
| text(content) | Set the plain-text body |
| attach(attachment) | Add an attachment |
| build() | Required. Called by mailer.send() to build the mail — sync or async |
Mailer
| Method | Description |
|---|---|
| send(mailable) | Build and send a single mailable |
| sendBulk(mailables[]) | Build and send multiple mailables |
ArrayTransport
| Property / Method | Description |
|---|---|
| sent | Array of all BuiltMail objects that have been sent |
| last() | Returns the most recently sent mail, or undefined |
| clear() | Resets the sent array |
