You can send emails from your AdonisJS application using the @adonisjs/mail
package. The mail package is built on top of Nodemailer, bringing the following quality of life improvements over Nodemailer.
- Fluent API to configure mail messages.
- Ability to define emails as classes for better organization and easier testing.
- An extensive suite of officially maintained transports. It includes
smtp
,ses
,mailgun
,sparkpost
,resend
, andbrevo
. - Improved testing experience using the Fakes API.
- Mail messenger to queue emails.
- Functional APIs to generate calendar events.
Installation
Install and configure the package using the following command :
node ace add @adonisjs/mail
# Pre-define transports to use via CLI flag
node ace add @adonisjs/mail --transports=resend --transports=smtp
-
Installs the
@adonisjs/mail
package using the detected package manager. -
Registers the following service provider and command inside the
adonisrc.ts
file.{commands: [// ...other commands() => import('@adonisjs/mail/commands')],providers: [// ...other providers() => import('@adonisjs/mail/mail_provider')]} -
Create the
config/mail.ts
file. -
Defines the environment variables and their validations for the selected mail services
Configuration
The configuration for the mail package is stored inside the config/mail.ts
file. Inside this file, you may configure multiple email services as mailers
to use them within your application.
See also: Config stub
import env from '#start/env'
import { defineConfig, transports } from '@adonisjs/mail'
const mailConfig = defineConfig({
default: 'smtp',
/**
* A static address for the "from" property. It will be
* used unless an explicit from address is set on the
* Email
*/
from: {
address: '',
name: '',
},
/**
* A static address for the "reply-to" property. It will be
* used unless an explicit replyTo address is set on the
* Email
*/
replyTo: {
address: '',
name: '',
},
/**
* The mailers object can be used to configure multiple mailers
* each using a different transport or the same transport with a different
* options.
*/
mailers: {
smtp: transports.smtp({
host: env.get('SMTP_HOST'),
port: env.get('SMTP_PORT'),
}),
resend: transports.resend({
key: env.get('RESEND_API_KEY'),
baseUrl: 'https://api.resend.com',
}),
},
})
-
default
-
The name of the mailer to use by default for sending emails.
-
from
-
A static global address to use for the
from
property. The global address will be used unless an explicitfrom
address is defined on the email. -
replyTo
-
A static global address to use for the
reply-to
property. The global address will be used unless an explicitreplyTo
address is defined on the email. -
mailers
-
The
mailers
object is used to configure one or more mailers you want to use for sending emails. You can switch between the mailers at runtime using themail.use
method.
Transports config
Following is a complete reference of configuration options accepted by the officially supported transports.
See also: TypeScript types for config object
The following configuration options are sent to the Mailgun's /messages.mime
API endpoint.
{
mailers: {
mailgun: transports.mailgun({
baseUrl: 'https://api.mailgun.net/v3',
key: env.get('MAILGUN_API_KEY'),
domain: env.get('MAILGUN_DOMAIN'),
/**
* The following options can be overridden at
* runtime when calling the `mail.send` method.
*/
oDkim: true,
oTags: ['transactional', 'adonisjs_app'],
oDeliverytime: new Date(2024, 8, 18),
oTestMode: false,
oTracking: false,
oTrackingClick: false,
oTrackingOpens: false,
headers: {
// h:prefixed headers
},
variables: {
appId: '',
userId: '',
// v:prefixed variables
}
})
}
}
The following configuration options are forwarded to Nodemailer as it is. So please check the Nodemailer documentation as well.
{
mailers: {
smtp: transports.smtp({
host: env.get('SMTP_HOST'),
port: env.get('SMTP_PORT'),
secure: false,
auth: {
type: 'login',
user: env.get('SMTP_USERNAME'),
pass: env.get('SMTP_PASSWORD')
},
tls: {},
ignoreTLS: false,
requireTLS: false,
pool: false,
maxConnections: 5,
maxMessages: 100,
})
}
}
The following configuration options are forwarded to Nodemailer as it is. So please check the Nodemailer documentation as well.
Make sure to install the @aws-sdk/client-ses
package to use the SES transport.
{
mailers: {
ses: transports.ses({
/**
* Forwarded to aws sdk
*/
apiVersion: '2010-12-01',
region: 'us-east-1',
credentials: {
accessKeyId: env.get('AWS_ACCESS_KEY_ID'),
secretAccessKey: env.get('AWS_SECRET_ACCESS_KEY'),
},
/**
* Nodemailer specific
*/
sendingRate: 10,
maxConnections: 5,
})
}
}
The following configuration options are sent to the SparkPost's /transmissions
API endpoint.
{
mailers: {
sparkpost: transports.sparkpost({
baseUrl: 'https://api.sparkpost.com/api/v1',
key: env.get('SPARKPOST_API_KEY'),
/**
* The following options can be overridden at
* runtime when calling the `mail.send` method.
*/
startTime: new Date(),
openTracking: false,
clickTracking: false,
initialOpen: false,
transactional: true,
sandbox: false,
skipSuppression: false,
ipPool: '',
})
}
}
The following configuration options are sent to the Resend's /emails
API endpoint.
{
mailers: {
resend: transports.resend({
baseUrl: 'https://api.resend.com',
key: env.get('RESEND_API_KEY'),
/**
* The following options can be overridden at
* runtime when calling the `mail.send` method.
*/
tags: [
{
name: 'category',
value: 'confirm_email'
}
]
})
}
}
Basic example
Once the initial configuration is completed, you may send emails using the mail.send
method. The mail service is a singleton instance of the MailManager class created using the config file.
The mail.send
method passes an instance of the Message class to the callback and delivers the email using the default
mailer configured inside the config file.
In the following example, we trigger an email from the controller after creating a new user account.
import User from '#models/user'
import { HttpContext } from '@adonisjs/core/http'
import mail from '@adonisjs/mail/services/main'
export default class UsersController {
async store({ request }: HttpContext) {
/**
* For demonstration only. You should validate the data
* before storing it inside the database.
*/
const user = await User.create(request.all())
await mail.send((message) => {
message
.to(user.email)
.from('info@example.org')
.subject('Verify your email address')
.htmlView('emails/verify_email', { user })
})
}
}
Queueing emails
Since sending emails can be time-consuming, you might want to push them to a queue and send emails in the background. You can do the same using the mail.sendLater
method.
The sendLater
method accepts the same parameters as the send
method. However, instead of sending the email immediately, it will use the Mail messenger to queue it.
await mail.send((message) => {
await mail.sendLater((message) => {
message
.to(user.email)
.from('info@example.org')
.subject('Verify your email address')
.htmlView('emails/verify_email', { user })
})
By default, the mail messenger uses an in-memory queue, meaning the queue will drop the jobs if your process dies with pending jobs. This might not be a huge deal if your application UI allows re-sending emails with manual actions. However, you can always configure a custom messenger and use a database-backed queue.
Using bullmq for queueing emails
npm i bullmq
In the following example, we use the mail.setMessenger
method to configure a custom queue that uses bullmq
under the hood for storing jobs.
We store the compiled email, runtime configuration, and the mailer name inside the job. Later, we will use this data to send emails inside a worker process.
import { Queue } from 'bullmq'
import mail from '@adonisjs/mail/services/main'
const emailsQueue = new Queue('emails')
mail.setMessenger((mailer) => {
return {
async queue(mailMessage, config) {
await emailsQueue.add('send_email', {
mailMessage,
config,
mailerName: mailer.name,
})
}
}
})
Finally, let's write the code for the queue Worker. Depending on your application workflow, you may have to start another process for the workers to process the jobs.
In the following example:
- We process jobs named
send_email
from theemails
queue. - Access compiled mail message, runtime config, and the mailer name from the job data.
- And send the email using the
mailer.sendCompiled
method.
import { Worker } from 'bullmq'
import mail from '@adonisjs/mail/services/main'
new Worker('emails', async (job) => {
if (job.name === 'send_email') {
const {
mailMessage,
config,
mailerName
} = job.data
await mail
.use(mailerName)
.sendCompiled(mailMessage, config)
}
})
That's all! You may continue using the mail.sendLater
method. However, the emails will be queued inside a redis database this time.
Switching between mailers
You may switch between the configured mailers using the mail.use
method. The mail.use
method accepts the name of the mailer (as defined inside the config file) and returns an instance of the Mailer class.
import mail from '@adonisjs/mail/services/main'
mail.use() // Instance of default mailer
mail.use('mailgun') // Mailgun mailer instance
You may call the mailer.send
or mailer.sendLater
methods to send email using a mailer instance. For example:
await mail
.use('mailgun')
.send((message) => {
})
await mail
.use('mailgun')
.sendLater((message) => {
})
The mailer instances are cached for the lifecycle of the process. You may use the mail.close
method to destroy an existing instance and re-create a new instance from scratch.
import mail from '@adonisjs/mail/services/main'
/**
* Close transport and remove instance from
* cache
*/
await mail.close('mailgun')
/**
* Create a fresh instance
*/
mail.use('mailgun')
Configuring the template engine
By default, the mail package is configured to use the Edge template engine for defining the email HTML and Plain text contents.§
However, as shown in the following example, you may also register a custom template engine by overriding the Message.templateEngine
property.
See also: Defining email contents
import { Message } from '@adonisjs/mail'
Message.templateEngine = {
async render(templatePath, data) {
return someTemplateEngine.render(templatePath, data)
}
}
Events
Please check the events reference guide to view the list of events dispatched by the @adonisjs/mail
package.
Configuring message
The properties of an email are defined using the Message class. An instance of this class is provided to the callback function created using the mail.send
, or mail.sendLater
methods.
import { Message } from '@adonisjs/mail'
import mail from '@adonisjs/mail/services/main'
await mail.send((message) => {
console.log(message instanceof Message) // true
})
await mail.sendLater((message) => {
console.log(message instanceof Message) // true
})
Defining subject and sender
You may define the email subject using the message.subject
method and the email's sender using the message.from
method.
await mail.send((message) => {
message
.subject('Verify your email address')
.from('info@example.org')
})
The from
method accepts the email address as a string or an object with the sender name and the email address.
message
.from({
address: 'info@example.com',
name: 'AdonisJS'
})
The sender can also be defined globally within the config file. The global sender will be used if no explicit sender is defined for an individual message.
const mailConfig = defineConfig({
from: {
address: 'info@example.com',
name: 'AdonisJS'
}
})
Defining recipients
You may define the email recipients using the message.to
, message.cc
, and the message.bcc
methods. These methods accept the email address as a string or an object with the recipient name and the email address.
await mail.send((message) => {
message
.to(user.email)
.cc(user.team.email)
.bcc(user.team.admin.email)
})
await mail.send((message) => {
message
.to({
address: user.email,
name: user.fullName,
})
.cc({
address: user.team.email,
name: user.team.name,
})
.bcc({
address: user.team.admin.email,
name: user.team.admin.fullName,
})
})
You can define multiple cc
and bcc
recipients as an array of email addresses or an object with email addresses and the recipient name.
await mail.send((message) => {
message
.cc(['first@example.com', 'second@example.com'])
.bcc([
{
name: 'First recipient',
address: 'first@example.com'
},
{
name: 'Second recipient',
address: 'second@example.com'
}
])
})
You may also define the replyTo
email address using the message.replyTo
method.
await mail.send((message) => {
message
.from('info@example.org')
.replyTo('noreply@example.org')
})
Defining email contents
You may define the HTML and Plain text contents for an email using message.html
or message.text
methods.
await mail.send((message) => {
/**
* HTML contents
*/
message.html(`
<h1> Verify email address </h1>
<p> <a href="https://myapp.com">Click here</a> to verify your email address </a>
`)
/**
* Plain text contents
*/
message.text(`
Verify email address
Please visit https://myapp.com to verify your email address
`)
})
Using Edge templates
Since writing inline content could be cumbersome, you may use Edge templates instead. If you have already configured Edge, you may use the message.htmlView
and message.textView
methods to render templates.
node ace make:view emails/verify_email_html
node ace make:view emails/verify_email_text
await mail.send((message) => {
message.htmlView('emails/verify_email_html', stateToShare)
message.textView('emails/verify_email_text', stateToShare)
})
Using MJML for email markup
MJML is a markup language for creating emails without writing all the complex HTML to make your emails look good in every email client.
The first step is to install the mjml package from npm.
npm i mjml
Once done, you can write MJML markup inside your Edge templates by wrapping it inside the @mjml
tag.
Since the output of MJML contains the html
, head
, and body
tags, it is unnecessary to define them within your Edge templates.
@mjml()
<mjml>
<mj-body>
<mj-section>
<mj-column>
<mj-text>
Hello World!
</mj-text>
</mj-column>
</mj-section>
</mj-body>
</mjml>
@end
You may pass the MJML configuration options as props to the @mjml
tag.
@mjml({
keepComments: false,
fonts: {
Lato: 'https://fonts.googleapis.com/css?family=Lato:400,500,700'
}
})
Attaching files
You may use the message.attach
method to send attachments in an email. The attach
method accepts an absolute path or a file system URL of a file you want to send as an attachment.
import app from '@adonisjs/core/services/app'
await mail.send((message) => {
message.attach(app.makePath('uploads/invoice.pdf'))
})
You may define the filename for the attachment using the options.filename
property.
message.attach(app.makePath('uploads/invoice.pdf'), {
filename: 'invoice_october_2023.pdf'
})
The complete list of options accepted by the message.attach
method follows.
Option | Description |
---|---|
filename |
The display name for the attachment. Defaults to the basename of the attachment path. |
contentType |
The content type for the attachment. If not set, the contentType will be inferred from the file extension. |
contentDisposition |
Content disposition type for the attachment. Defaults to attachment |
headers |
Custom headers for the attachment node. The headers property is a key-value pair |
Attaching files from streams and buffers
You may create email attachments from streams and buffers using the message.attachData
method. The method accepts a readable stream or the buffer as the first argument and the options object as the second argument.
The message.attachData
method should not be used when queueing emails using the mail.sendLater
method. Since queued jobs are serialized and persisted inside a database, attaching raw data will increase the storage size.
Moreover, queueing an email will fail if you attach a stream using the message.attachData
method.
message.attachData(fs.createReadStream('./invoice.pdf'), {
filename: 'invoice_october_2023.pdf'
})
message.attachData(Buffer.from('aGVsbG8gd29ybGQh'), {
encoding: 'base64',
filename: 'greeting.txt',
})
Embedding images
You may embed images within the contents of your email using the embedImage
view helper. The embedImage
method under the hood uses CID to mark the image as an attachment and uses its content id as the source of the image.
<img src="{{
embedImage(app.makePath('assets/hero.jpg'))
}}" />
Following will be the output HTML
<img src="cid:a-random-content-id" />
The following attachment will be defined automatically on the email payload.
{
attachments: [{
path: '/root/app/assets/hero.jpg',
filename: 'hero.jpg',
cid: 'a-random-content-id'
}]
}
Embedding images from buffers
Like the embedImage
method, you may use the embedImageData
method to embed an image from raw data.
<img src="{{
embedImageData(rawBuffer, { filename: 'hero.jpg' })
}}" />
Attaching calendar events
You may attach calendar events to an email using the message.icalEvent
method. The icalEvent
method accepts the event contents as the first parameter and the options
object as the second parameter.
const contents = 'BEGIN:VCALENDAR\r\nPRODID:-//ACME/DesktopCalendar//EN\r\nMETHOD:REQUEST\r\n...'
await mail.send((message) => {
message.icalEvent(contents, {
method: 'PUBLISH',
filename: 'invite.ics',
})
})
Since defining the event file contents manually can be cumbersome, you may pass a callback function to the icalEvent
method and generate the invite contents using JavaScript API.
The calendar
object provided to the callback function is a reference of the ical-generator npm package, so make sure to go through the package's README file as well.
message.icalEvent((calendar) => {
calendar
.createEvent({
summary: 'Adding support for ALS',
start: DateTime.local().plus({ minutes: 30 }),
end: DateTime.local().plus({ minutes: 60 }),
})
}, {
method: 'PUBLISH',
filename: 'invite.ics',
})
Reading invite contents from a file or a URL
You may define the invite contents from a file or an HTTP URL using the icalEventFromFile
or icalEventFromUrl
methods.
message.icalEventFromFile(
app.resourcesPath('calendar-invites/invite.ics'),
{
filename: 'invite.ics',
method: 'PUBLISH'
}
)
message.icalEventFromFile(
'https://myapp.com/users/1/invite.ics',
{
filename: 'invite.ics',
method: 'PUBLISH'
}
)
Defining email headers
You may define additional email headers using the message.header
method. The method accepts the header key as the first parameter and the value as the second parameter.
message.header('x-my-key', 'header value')
/**
* Define an array of values
*/
message.header('x-my-key', ['header value', 'another value'])
By default, the email headers are encoded and folded to meet the requirement of having plain ASCII messages with lines no longer than 78 bytes. However, if you want to bypass the encoding rules, you may set a header using the message.preparedHeader
method.
message.preparedHeader(
'x-unprocessed',
'a really long header or value with non-ascii characters 👮',
)
Defining List
headers
The message class includes helper methods to define complex headers like List-Unsubscribe or List-Help with ease. You can learn about the encoding rules for List
headers on the nodemailer website.
message.listHelp('admin@example.com?subject=help')
// List-Help: <mailto:admin@example.com?subject=help>
message.listUnsubscribe({
url: 'http://example.com',
comment: 'Comment'
})
// List-Unsubscribe: <http://example.com> (Comment)
/**
* Repeating header multiple times
*/
message.listSubscribe('admin@example.com?subject=subscribe')
message.listSubscribe({
url: 'http://example.com',
comment: 'Subscribe'
})
// List-Subscribe: <mailto:admin@example.com?subject=subscribe>
// List-Subscribe: <http://example.com> (Subscribe)
For all other arbitrary List
headers, you may use the addListHeader
method.
message.addListHeader('post', 'http://example.com/post')
// List-Post: <http://example.com/post>
Class-based emails
Instead of writing emails inside the mail.send
method closure, you may move them to dedicated mail classes for better organization and easier testing.
The mail classes are stored inside the ./app/mails
directory, and each file represents a single email. You may create a mail class by running the make:mail
ace command.
See also: Make mail command
node ace make:mail verify_email
The mail class extends the BaseMail class and is scaffolded with following properties and methods. You may configure the mail message inside the prepare
method using the this.message
property.
import User from '#models/user'
import { BaseMail } from '@adonisjs/mail'
export default class VerifyEmailNotification extends BaseMail {
from = 'sender_email@example.org'
subject = 'Verify email'
prepare() {
this.message.to('user_email@example.org')
}
}
-
from
-
Configure the sender's email address. If you omit this property, you must call the
message.from
method to define the sender. -
subject
-
Configure the email subject. If you omit this property, you must use the
message.subject
method to define the email subject. -
replyTo
-
Configure the
replyTo
email address. -
prepare
-
The
prepare
method is called automatically by thebuild
method to prepare the mail message for sending.You must define the email contents, attachments, recipients, etc, within this method.
-
build Inherited
-
The
build
method is inherited from theBaseMail
class. The method is called automatically at the time of sending the email.Make sure to reference the original implementation if you decide to override this method.
Sending email using the mail class
You may call the mail.send
method and pass it an instance of the mail class to send the email. For example:
import mail from '@adonisjs/mail/services/main'
import VerifyEmailNotification from '#mails/verify_email'
await mail.send(new VerifyEmailNotification())
import mail from '@adonisjs/mail/services/main'
import VerifyEmailNotification from '#mails/verify_email'
await mail.sendLater(new VerifyEmailNotification())
You may share data with the mail class using constructor arguments. For example:
/**
* Creating a user
*/
const user = await User.create(payload)
await mail.send(
/**
* Passing user to the mail class
*/
new VerifyEmailNotification(user)
)
Testing mail classes
One of the primary benefits of using Mail classes is a better testing experience. You can build mail classes without sending them and write assertions for the message properties.
import { test } from '@japa/runner'
import VerifyEmailNotification from '#mails/verify_email'
test.group('Verify email notification', () => {
test('prepare email for sending', async () => {
const email = new VerifyEmailNotification()
/**
* Build email message and render templates to
* compute the email HTML and plain text
* contents
*/
await email.buildWithContents()
/**
* Write assertions to ensure the message is built
* as expected
*/
email.message.assertTo('user@example.org')
email.message.assertFrom('info@example.org')
email.message.assertSubject('Verify email address')
email.message.assertReplyTo('no-reply@example.org')
})
})
You may write assertions for the message contents as follows.
const email = new VerifyEmailNotification()
await email.buildWithContents()
email.message.assertHtmlIncludes(
`<a href="/emails/1/verify"> Verify email address </a>`
)
email.message.assertTextIncludes('Verify email address')
Also, you may write assertions for the attachments. The assertions only work with file-based attachments and not for streams or raw content.
const email = new VerifyEmailNotification()
await email.buildWithContents()
email.message.assertAttachment(
app.makePath('uploads/invoice.pdf')
)
Feel free to look at the Message class source code for all the available assertion methods.
Fake mailer
You may want to use the Fake mailer during testing to prevent your application from sending emails. The Fake mailer collects all outgoing emails within memory and offers an easy-to-use API for writing assertions against them.
In the following example:
- We start by creating an instance of the FakeMailer using the
mail.fake
method. - Next, we call the
/register
endpoint API. - Finally, we use the
mails
property from the fake mailer to assert theVerifyEmailNotification
was sent.
import { test } from '@japa/runner'
import mail from '@adonisjs/mail/services/main'
import VerifyEmailNotification from '#mails/verify_email'
test.group('Users | register', () => {
test('create a new user account', async ({ client, route }) => {
/**
* Turn on the fake mode
*/
const { mails } = mail.fake()
/**
* Make an API call
*/
await client
.post(route('users.store'))
.send(userData)
/**
* Assert the controller indeed sent the
* VerifyEmailNotification mail
*/
mails.assertSent(VerifyEmailNotification, ({ message }) => {
return message
.hasTo(userData.email)
.hasSubject('Verify email address')
})
})
})
Once you are done writing the test, you must restore the fake using the mail.restore
method.
test('create a new user account', async ({ client, route, cleanup }) => {
const { mails } = mail.fake()
/**
* The cleanup hooks are executed after the test
* finishes successfully or with an error.
*/
cleanup(() => {
mail.restore()
})
})
Writing assertions
The mails.assertSent
method accepts the mail class constructor as the first argument and throws an exception when unable to find any emails for the expected class.
const { mails } = mail.fake()
/**
* Asser the email was sent
*/
mails.assertSent(VerifyEmailNotification)
You may pass a callback function to the assertSent
method to further check if the email was sent to the expected recipient or has correct subject.
The callback function receives an instance of the mail class and you can use the .message
property to get access to the message object.
mails.assertSent(VerifyEmailNotification, (email) => {
return email.message.hasTo(userData.email)
})
You may run assertions on the message
object within the callback. For example:
mails.assertSent(VerifyEmailNotification, (email) => {
email.message.assertTo(userData.email)
email.message.assertFrom('info@example.org')
email.message.assertSubject('Verify your email address')
/**
* All assertions passed, so return true to consider the
* email as sent.
*/
return true
})
Assert email was not sent
You may use the mails.assertNotSent
method to assert an email was not sent during the test. This method is the opposite of the assertSent
method and accepts the same arguments.
const { mails } = mail.fake()
mails.assertNotSent(PasswordResetNotification)
Assert emails count
Finally, you can assert the count of sent emails using the assertSentCount
and assertNoneSent
methods.
const { mails } = mail.fake()
// Assert 2 emails were sent in total
mails.assertSentCount(2)
// Assert only one VerifyEmailNotification was sent
mails.assertSentCount(VerifyEmailNotification, 1)
const { mails } = mail.fake()
// Assert zero emails were sent
mails.assertNoneSent()
Writing assertions for queued emails
If you have queued emails using the mail.sendLater
method, you may use the following methods to write assertions for them.
const { mails } = mail.fake()
/**
* Assert "VerifyEmailNotification" email was queued
* Optionally, you may pass the finder function to
* narrow down the email
*/
mails.assertQueued(VerifyEmailNotification)
/**
* Assert "VerifyEmailNotification" email was not queued
* Optionally, you may pass the finder function to
* narrow down the email
*/
mails.assertNotQueued(PasswordResetNotification)
/**
* Assert two emails were queued in total.
*/
mails.assertQueuedCount(2)
/**
* Assert "VerifyEmailNotification" email was queued
* only once
*/
mails.assertQueuedCount(VerifyEmailNotification , 1)
/**
* Assert nothing was queued
*/
mails.assertNoneQueued()
Getting a list of sent or queued emails
You may use the mails.sent
or mails.queued
methods to get an array of emails sent/queued during tests.
const { mails } = mail.fake()
const sentEmails = mails.sent()
const queuedEmails = mails.queued()
const email = sentEmails.find((email) => {
return email instanceof VerifyEmailNotification
})
if (email) {
email.message.assertTo(userData.email)
email.message.assertFrom(userData.email)
email.message.assertHtmlIncludes('<a href="/verify/email"> Verify your email address</a>')
}
Creating custom transports
AdonisJS Mail transports are built on top of Nodemailer transports; therefore, you must create/use a nodemailer transport before you can register it with the Mail package.
In this guide, we will wrap the nodemailer-postmark-transport to an AdonisJS Mail transport.
npm i nodemailer nodemailer-postmark-transport
As you can see in the following example, the heavy lifting of sending an email is done by the nodemailer
. The AdonisJS transport acts as an adapter forwarding the message to nodemailer and normalizing its response to an instance of MailResponse.
import nodemailer from 'nodemailer'
import nodemailerTransport from 'nodemailer-postmark-transport'
import { MailResponse } from '@adonisjs/mail'
import type {
NodeMailerMessage,
MailTransportContract
} from '@adonisjs/mail/types'
/**
* Configuration accepted by the transport
*/
export type PostMarkConfig = {
auth: {
apiKey: string
}
}
/**
* Transport implementation
*/
export class PostMarkTransport implements MailTransportContract {
#config: PostMarkConfig
constructor(config: PostMarkConfig) {
this.#config = config
}
#createNodemailerTransport(config: PostMarkConfig) {
return nodemailer.createTransport(nodemailerTransport(config))
}
async send(
message: NodeMailerMessage,
config?: PostMarkConfig
): Promise<MailResponse> {
/**
* Create nodemailer transport
*/
const transporter = this.#createNodemailerTransport({
...this.#config,
...config,
})
/**
* Send email
*/
const response = await transporter.sendMail(message)
/**
* Normalize response to an instance of the "MailResponse" class
*/
return new MailResponse(response.messageId, response.envelope, response)
}
}
Creating the config factory function
To reference the above transport inside the config/mail.ts
file, you must create a factory function that returns an instance of the transport.
You may write the following code within the same file as your transport's implementation.
import type {
NodeMailerMessage,
MailTransportContract,
MailManagerTransportFactory
} from '@adonisjs/mail/types'
export function postMarkTransport(
config: PostMarkConfig
): MailManagerTransportFactory {
return () => {
return new PostMarkTransport(config)
}
}
Using the transport
Finally, you can reference the transport inside your config file using the postMarkTransport
helper.
import env from '#start/env'
import { defineConfig } from '@adonisjs/mail'
import { postMarkTransport } from 'my-custom-package'
const mailConfig = defineConfig({
mailers: {
postmark: postMarkTransport({
auth: {
apiKey: env.get('POSTMARK_API_KEY'),
},
}),
},
})