AdonisJS ships with an official package for sending emails. It internally uses nodemailer to send emails but removes the boilerplate for manually constructing transports and allows trapping emails during tests, so that they are not sent to the real recipients.
By the end of this guide, you will know:
- How to install and configure the mail package
- How to create and use multiple mailers
- Trapping emails during tests
- Previewing emails on a dummy SMTP server
Setup
Install the @adonisjs/mail
package from npm registry using the following command.
The @adonisjs/mail
package relies on @adonisjs/view
package. Make sure the view package is installed and configured correctly.
npm i @adonisjs/[email protected]
Invoke Generator
AdonisJS packages can configure themselves by running the post install instructions. Run the following command to setup @adonisjs/mail
package.
node ace invoke @adonisjs/mail
# create config/mail.ts
# create contracts/mail.ts
# update .env
# update tsconfig.json { types += @adonisjs/mail }
# update .adonisrc.json { providers += @adonisjs/mail }
# ✔ create ace-manifest.json
Supported drivers
The mail module out of the box supports the following drivers.
- smtp ( Uses the SMTP protocol )
- ses ( Uses AWS SES )
- mailgun ( Uses mailgun service )
- sparkpost ( Uses sparkpost service )
Configuration
The configuration for the mail package is stored inside the config/mail.ts
file. Inside the config file, you can define one or more mailers, along with the default mailer to use.
import Env from '@ioc:Adonis/Core/Env'
import { MailConfig } from '@ioc:Adonis/Addons/Mail'
const mailConfig: MailConfig = {
/*
|--------------------------------------------------------------------------
| DEFAULT MAILER
|--------------------------------------------------------------------------
*/
mailer: 'smtp',
/*
|--------------------------------------------------------------------------
| MAILERS LIST
|--------------------------------------------------------------------------
*/
mailers: {
smtp: {
driver: 'smtp',
host: Env.get('SMTP_HOST') as string,
port: Env.get('SMTP_PORT') as string,
},
ses: {
driver: 'ses',
apiVersion: '2010-12-01',
key: Env.get('SES_ACCESS_KEY') as string,
secret: Env.get('SES_ACCESS_SECRET') as string,
region: Env.get('SES_REGION') as string,
sslEnabled: true,
sendingRate: 10,
maxConnections: 5,
},
mailgun: {
driver: 'mailgun',
baseUrl: 'https://api.mailgun.net/v3',
key: Env.get('MAILGUN_API_KEY') as string,
},
sparkpost: {
driver: 'sparkpost',
baseUrl: 'https://api.sparkpost.com/api/v1',
key: Env.get('SPARKPOST_API_KEY') as string,
},
},
}
export default mailConfig
Points to note
You can create multiple mailers using the same underlying driver. For example: A mailer to send promotional emails and another one to send transactional.
When defining a new mailer inside the config file, it needs to be first registered inside
contracts/mail.ts
file. Otherwise, the typescript compiler will complain.declare module '@ioc:Adonis/Addons/Mail' { import { MailDrivers } from '@ioc:Adonis/Addons/Mail' interface MailersList { smtp: MailDrivers['smtp'], ses: MailDrivers['ses'], mailgun: MailDrivers['mailgun'], sparkpost: MailDrivers['sparkpost'], } }
To switch between mailers, you can use
Mail.use('<MAILER NAME>')
method.Finally, feel free to remove the configuration for the mailers you are not planning to use.
Mailgun config
The mailgun configuration block optionally accepts the following options.
Config option | Mailgun variant |
---|---|
oTag | o:tag |
oDeliverytime | o:deliverytime |
oTestMode | o:testmode |
oTracking | o:tracking |
oTrackingClick | o:tracking-clicks |
oTrackingOpens | o:tracking-opens |
oDkim | o:dkim |
headers | h: |
All of the options except oDkim
can be passed during the Mail.send
call as well.
await Mail.use('mailgun').send((message) => {
message.subject('Welcome Onboard!')
}, {
oTag: ['signup'],
})
Sparkpost config
The sparkpost configuration block optionally accepts the following options.
Config option | Sparkpost variant |
---|---|
startTime | start_time |
openTracking | open_tracking |
clickTracking | click_tracking |
transactional | transactional |
sandbox | sandbox |
skipSuppression | skip_suppression |
ipPool | ip_pool |
All of the configuration options can also be defined at runtime during the Mail.send
call.
await Mail.use('sparkpost').send((message) => {
message.subject('Welcome Onboard!')
}, {
transaction: true,
openTracking: false,
})
Usage
Once done with the setup, you can import the Mail
module and send emails using the Mail.send
method.
import Mail from '@ioc:Adonis/Addons/Mail'
Mail.send((message) => {
message
.from('[email protected]')
.to('[email protected]')
.subject('Welcome Onboard!')
.htmlView('emails/welcome', { name: 'Virk' })
})
- The
Mail.send
method accepts a callback function. - Inside the callback, you can configure the mail message by calling the appropriate methods.
- At bare minimum, you must define
from
,to
,subject
and the content of the email using thehtmlView
method.
Defer email sending
Most of the times, you will be sending emails in respond to some action. For example: Send email after user registration or during the password reset flow.
import User from 'App/Models/User'
import Mail from '@ioc:Adonis/Addons/Mail'
export default class UsersController {
public async store () {
const user = await User.create({})
await Mail.send((message) => { // ...configure message })
return user
}
}
In the above example, the HTTP request to create the user will have to wait until the email is sent and hence makes your application appear slower.
To change this behavior, you need to make sure that the email is sent in the background and not during the HTTP request and the same can be achieved using the Mail.sendLater
method.
The Mail.sendLater
method uses an in-memory queue to send emails in the background. For critical emails, you may want to use a real queue server, since an in-memory queue will loose all the jobs when server goes down.
export default class UsersController {
public async store () {
const user = await User.create({})
// Pushed to in-memory queue await Mail.sendLater((message) => { // ...configure message
})
return user
}
}
Creating email templates
You can make use of standard edge templates for defining email content. The templates lives inside the same resources/views
directory. For better organization, you can move them inside a sub-directory called emails
. For example:
node ace make:view emails/welcome
# ✔ create resources/views/emails/welcome.edge
Open the newly created template file and paste following contents inside it.
<h1> Welcome {{ user.fullName }} </h1>
<p>
<a href="{{ url }}">Click here</a> to verify your email address.
</p>
Finally, you can use this view as the content for the email by calling the following method.
await Mail.sendLater((message) => {
message.htmlView('emails/welcome', {
user: { fullName: 'Some Name' },
url: 'https://your-app.com/verification-url',
})
})
Similarly, you can also define the plain text content, along with the content for the Apple watch.
message.textView('emails/welcome.plain', {})
message.watchView('emails/welcome.watch', {})
Sending attachments
You can send attachments in email using the message.attach
or message.embed
methods.
import Application from '@ioc:Adonis/Core/Application'
await Mail.sendLater((message) => {
message.attach(Application.publicPath('receipt.png'))
})
- The
message.attach
method needs an absolute path to the file. Application.publicPath
returns an absolute path for thepublic
directory inside your project root.- The
public
directory is used just as an example. You can attach files from any directory.
CID attachments
There are multiple ways to render images inside the email body. One of them is sending the image as an attachment and then adding it to the HTML using it's Content-Id (CID).
await Mail.sendLater((message) => {
message.embed(
Application.publicPath('receipt.png'),
'a-unique-id-for-the-attachment',
)
})
<img src="cid:a-unique-id-for-the-attachment" />
Points to note
- You have to use
message.embed
method and notmessage.attach
. - Each embedded image must have a unique id, so that you can later reference it inside the template.
- The
img[src]
needs the unique id, along with thecid:
prefix. - Learn more about CID attachments.
Mailer classes
Mailer classes are dedicated ES6 classes to configure an email. A mailer should always be responsible for sending email of a given type. For example: You should create different mailers for email change, password reset or for welcoming a new user.
To begin, run the following command to create a new mailer
node ace make:mailer VerifyEmail
# CREATE: app/Mailers/VerifyEmail.ts
Open the newly created file to inspect its source code. For the most part, you will be working inside the prepare
method to configure the mail message.
import { BaseMailer, MessageContract } from '@ioc:Adonis/Addons/Mail'
export default class VerifyEmail extends BaseMailer {
public prepare(message: MessageContract) {
message
.subject('The email subject')
.from('[email protected]')
.to('[email protected]')
}
}
Finally, you can use the mailer class as follows
import VerifyEmail from 'App/Mailers/VerifyEmail'
await new VerifyEmail().sendLater()
Passing data to the mailer class
Mailer can accept data using the constructor arguments. For example:
export default class VerifyEmail extends BaseMailer {
constructor (private user: User) {}
public prepare(message: MessageContract) {
message
.subject('The email subject')
.from('[email protected]')
.to(this.user.email) // 👈
}
}
const user = await User.find(1)
await new VerifyEmail(user).sendLater()
Use a different mailer
The mailer classes uses the default mailer configured inside the config/mail
file. However, you can use a different one by defining the following property on the class instance.
export default class VerifyEmail extends BaseMailer {
// Definining options is optional public mailer = this.mail.use('mailgun').options({ oTags: ['transactional'], })
public prepare(message: MessageContract) {
message
.subject('The email subject')
.from('[email protected]')
.to('[email protected]')
}
}
Switching mailers at runtime
You can make use of the Mail.use()
method to switch between the mailers.
The use
method accepts the mailer name and not the driver name. Remember, you can create multiple mailers using the same underlying driver.
Mail.use('mailgun').send(() => {})
Mail.use('smtp').send(() => {})
SMTP server previews
Emails are tricky and hence there are dozens of tools/products to help you find issues with your emails. AdonisJS also provides a handy way to test your emails by sending them to a dummy SMTP server.
Consider the following example:
const { url } = await Mail.preview((message) => {
message
.to(user.email)
.from('[email protected]')
.subject('Welcome Onboard!')
.htmlView('emails/welcome')
})
console.log(`Preview url: ${url}`)
- You just need to replace the
Mail.send
method with theMail.preview
method. - The response contains the URL to view the email on https://ethereal.email.
Debugging email calls
The mail module emits adonis:mail:sent
event that you can listen to observe email calls. Let's begin by creating start/events.ts
file by running the following ace command.
node ace make:prldfile events
# ✔ create start/events.ts
Open the newly created file and paste the following contents inside it.
import Event from '@ioc:Adonis/Core/Event'
import Mail from '@ioc:Adonis/Addons/Mail'
Event.on('mail:sent', Mail.prettyPrint)
Now, if you send an email, it will be pretty printed on the terminal.
The Mail.prettyPrint
method is just one way to handle the event. You are free to define your own event listener and handle the event. The event listener receives only a single argument of MailEventData type.