Mail Guide
Guren provides a fluent API for sending emails with support for multiple transport backends. The mail system integrates with the queue system for async sending and supports HTML templates, attachments, and more.
Mail Guide
Guren provides a fluent API for sending emails with support for multiple transport backends. The mail system integrates with the queue system for async sending and supports HTML templates, attachments, and more.
The standard vNext path is: import mail APIs from @guren/core, configure the mail manager in a provider, and keep controllers focused on composing and dispatching mail work.
Core Concepts
- MailManager – Central registry for configuring and accessing mail transports.
- Mail – Fluent builder for composing and sending emails.
- Transport – Email delivery backend. Guren ships with SMTP, Resend, and Memory (testing) transports.
Basic Usage
Quick Start (Container-bound Facade)
The simplest way to send mail without passing a manager around is to create facades from an application container:
import { createFacades } from '@guren/core'
const { Mail } = createFacades(app.container)
// Send a simple email — no need to pass a manager instance
await Mail.to('user@example.com')
.subject('Hello!')
.text('Hello World!')
.send()
Direct Instantiation
You can also create a MailManager directly:
import { MailManager, mail } from '@guren/core'
const mailManager = new MailManager({
default: 'smtp',
from: { email: 'noreply@example.com', name: 'MyApp' },
transports: {
smtp: {
driver: 'smtp',
host: 'smtp.example.com',
port: 587,
auth: { user: 'user', pass: 'password' },
},
},
})
// Send a simple email
await mail(mailManager)
.to('user@example.com')
.subject('Hello!')
.text('Hello World!')
.send()
Fluent API
const builder = mail(mailManager)
// Recipients
builder.to('user@example.com') // Add recipient
builder.to({ email: 'user@example.com', name: 'John' }) // With name
builder.toMany(['a@example.com', 'b@example.com']) // Multiple recipients
builder.cc('copy@example.com') // CC recipient
builder.bcc('blind@example.com') // BCC recipient
// Sender and reply
builder.from('sender@example.com') // Override default from
builder.replyTo('support@example.com') // Reply-to address
// Content
builder.subject('Welcome!') // Email subject
builder.text('Plain text body') // Plain text
builder.html('<h1>HTML body</h1>') // HTML content
// Attachments
builder.attach({
filename: 'report.pdf',
path: './storage/report.pdf',
})
builder.attach({
filename: 'data.json',
content: JSON.stringify(data),
contentType: 'application/json',
})
// Headers
builder.header('X-Custom-Header', 'value')
// Send
await builder.send()
Configuration
Multiple Transports
Configure multiple mail backends for different use cases:
import { MailManager, mail } from '@guren/core'
const mailManager = new MailManager({
default: 'smtp',
from: { email: 'noreply@example.com', name: 'MyApp' },
transports: {
smtp: {
driver: 'smtp',
host: process.env.SMTP_HOST,
port: Number(process.env.SMTP_PORT) || 587,
secure: process.env.SMTP_SECURE === 'true',
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS,
},
},
resend: {
driver: 'resend',
apiKey: process.env.RESEND_API_KEY,
},
memory: {
driver: 'memory',
},
},
})
// Use default transport (smtp)
await mail(mailManager).to('user@example.com').subject('Test').text('Hello').send()
// Use specific transport
await mail(mailManager)
.via('resend')
.to('user@example.com')
.subject('Via Resend')
.text('Hello')
.send()
Transport Options
SMTP Transport:
| Option | Default | Description |
|---|---|---|
host |
required | SMTP server hostname |
port |
587 |
SMTP server port |
secure |
false |
Use TLS (usually for port 465) |
auth.user |
- | SMTP username |
auth.pass |
- | SMTP password |
pool |
true |
Use connection pooling |
maxConnections |
5 |
Max pool connections |
Resend Transport:
| Option | Default | Description |
|---|---|---|
apiKey |
required | Resend API key |
Memory Transport (Testing):
| Option | Default | Description |
|---|---|---|
simulateFailure |
false |
Simulate send failures |
failureMessage |
- | Error message for failures |
HTML Templates
Using React Email
Guren integrates with React Email for type-safe email templates:
bun add @react-email/render react
// app/Mail/WelcomeEmail.tsx
import * as React from 'react'
import { Html, Head, Body, Container, Text, Button } from '@react-email/components'
interface WelcomeEmailProps {
name: string
loginUrl: string
}
export function WelcomeEmail({ name, loginUrl }: WelcomeEmailProps) {
return (
<Html>
<Head />
<Body style={{ fontFamily: 'sans-serif' }}>
<Container>
<Text>Hello {name}!</Text>
<Text>Welcome to our application.</Text>
<Button href={loginUrl}>Get Started</Button>
</Container>
</Body>
</Html>
)
}
import { mail } from '@guren/core'
import { WelcomeEmail } from '@/app/Mail/WelcomeEmail'
await mail(mailManager)
.to('user@example.com')
.subject('Welcome!')
.template(WelcomeEmail, { name: 'John', loginUrl: 'https://example.com/login' })
.send()
Using Plain HTML
await mail(mailManager)
.to('user@example.com')
.subject('Welcome!')
.html(`
<h1>Welcome, ${user.name}!</h1>
<p>Thank you for signing up.</p>
<a href="${loginUrl}">Get Started</a>
`)
.send()
Attachments
// File attachment
await mail(mailManager)
.to('user@example.com')
.subject('Your Report')
.text('Please find your report attached.')
.attach({
filename: 'report.pdf',
path: './storage/reports/monthly.pdf',
})
.send()
// Inline content
await mail(mailManager)
.to('user@example.com')
.subject('Data Export')
.text('Your data export is ready.')
.attach({
filename: 'data.json',
content: JSON.stringify(exportData, null, 2),
contentType: 'application/json',
})
.send()
// Inline images (CID)
await mail(mailManager)
.to('user@example.com')
.subject('Newsletter')
.html('<img src="cid:logo" alt="Logo" /><p>Welcome!</p>')
.attach({
filename: 'logo.png',
path: './public/logo.png',
cid: 'logo',
})
.send()
Queued Emails
Send emails asynchronously using the queue system. In a real app, configure the mail manager in a provider and expose it through the container. setMailManager() is only the queue-worker bridge that lets queued mail jobs resolve the same manager instance:
import { mail, setMailManager, createQueueManager, MemoryDriver } from '@guren/core'
// Configure queue manager
const queue = createQueueManager({
default: 'memory',
drivers: {
memory: () => new MemoryDriver(),
},
})
queue.driver()
// Bridge the provider-managed mail manager into queued jobs
setMailManager(mailManager)
// Queue the email instead of sending immediately
await mail(mailManager)
.to('user@example.com')
.subject('Weekly Report')
.html(reportHtml)
.queue('emails') // Queue name
// The email will be processed by a worker
// bunx guren queue:work --queue=emails
Mailable Classes
Generate mailable classes for reusable email templates:
bunx guren make:mail WelcomeMail
// app/Mail/WelcomeMail.ts
import { Mail, MailManager } from '@guren/core'
interface WelcomeMailData {
user: { name: string; email: string }
loginUrl: string
}
export class WelcomeMail {
constructor(
private readonly manager: MailManager,
private readonly data: WelcomeMailData
) {}
async send(): Promise<void> {
const { user, loginUrl } = this.data
await new Mail(this.manager)
.to(user.email)
.subject(`Welcome, ${user.name}!`)
.html(`
<h1>Welcome to MyApp!</h1>
<p>Hi ${user.name},</p>
<p>Thank you for joining us.</p>
<a href="${loginUrl}">Get Started</a>
`)
.send()
}
async queue(queueName: string = 'emails'): Promise<string> {
const { user, loginUrl } = this.data
return new Mail(this.manager)
.to(user.email)
.subject(`Welcome, ${user.name}!`)
.html(`
<h1>Welcome to MyApp!</h1>
<p>Hi ${user.name},</p>
<p>Thank you for joining us.</p>
<a href="${loginUrl}">Get Started</a>
`)
.queue(queueName)
}
}
// Usage
const welcomeMail = new WelcomeMail(mailManager, {
user: { name: 'John', email: 'john@example.com' },
loginUrl: 'https://example.com/login',
})
await welcomeMail.send()
// or
await welcomeMail.queue('emails')
Container Integration
The mail subsystem is registered as a singleton via a ServiceProvider. You can resolve it from the container:
// Access via app.container or this.container in providers
const mailManager = container.make('mail') // MailManager
Testing with container.fake()
Swap the mail manager in tests to capture sent messages without sending real emails:
// Access via app.container or this.container in providers
import { MailManager, MemoryTransport } from '@guren/core'
test('sends welcome email on registration', async () => {
const memoryTransport = new MemoryTransport()
const fakeMail = new MailManager({
default: 'memory',
from: { email: 'test@example.com' },
})
fakeMail.registerTransport('memory', () => memoryTransport)
using _ = container.fake('mail', fakeMail)
// Run the code under test — all mail sent via the facade or container
// is captured by memoryTransport
await registerUser({ email: 'new@example.com' })
const sent = memoryTransport.getSentMessages()
expect(sent).toHaveLength(1)
expect(sent[0].to[0].email).toBe('new@example.com')
})
Testing
Use the Memory transport for testing:
import { describe, test, expect, beforeEach } from 'bun:test'
import { MailManager, mail, MemoryTransport } from '@guren/core'
describe('Email', () => {
let mailManager: MailManager
let memoryTransport: MemoryTransport
beforeEach(() => {
memoryTransport = new MemoryTransport()
mailManager = new MailManager({
default: 'memory',
from: { email: 'test@example.com' },
})
mailManager.registerTransport('memory', () => memoryTransport)
})
test('sends welcome email', async () => {
await mail(mailManager)
.to('user@example.com')
.subject('Welcome!')
.text('Hello World!')
.send()
const sent = memoryTransport.getSentMessages()
expect(sent).toHaveLength(1)
expect(sent[0].to[0].email).toBe('user@example.com')
expect(sent[0].subject).toBe('Welcome!')
})
test('sends email with attachment', async () => {
await mail(mailManager)
.to('user@example.com')
.subject('Report')
.text('See attached.')
.attach({ filename: 'data.txt', content: 'test data' })
.send()
const sent = memoryTransport.getSentMessages()
expect(sent[0].attachments).toHaveLength(1)
expect(sent[0].attachments![0].filename).toBe('data.txt')
})
})
Best Practices
Use environment variables: Never hardcode SMTP credentials or API keys.
Set a default from address: Configure a default sender to avoid repetition.
Use queued sending for bulk emails: Avoid blocking requests with synchronous sends.
Use React Email for complex templates: Type-safe templates are easier to maintain.
Test with Memory transport: Never send real emails in tests.
Handle send failures: Check the
SendResultand implement retry logic for critical emails.Use meaningful subjects: Clear subjects improve email deliverability and user experience.