Backend Development

Emails

Transactional email with MJML templates.

Nuda Kit uses AdonisJS Mail for sending emails and MJML for creating responsive HTML email templates.

Configuration

Mail configuration is in config/mail.ts:

import { defineConfig, transports } from '@adonisjs/mail'

const mailConfig = defineConfig({
  default: env.get('MAIL_DRIVER'),
  from: {
    name: env.get('MAIL_FROM_NAME'),
    address: env.get('MAIL_FROM_ADDRESS'),
  },

  mailers: {
    smtp: transports.smtp({
      host: env.get('SMTP_HOST'),
      port: env.get('SMTP_PORT'),
    }),

    mailgun: transports.mailgun({
      key: env.get('MAILGUN_API_KEY'),
      baseUrl: 'https://api.mailgun.net/v3',
      domain: env.get('MAILGUN_DOMAIN'),
    }),

    resend: transports.resend({
      key: env.get('RESEND_API_KEY'),
      baseUrl: 'https://api.resend.com',
    }),
  },
})

Environment Variables

# Mail driver (smtp, mailgun, resend)
MAIL_DRIVER=smtp
MAIL_FROM_NAME="Nuda Kit"
MAIL_FROM_ADDRESS=hello@example.com

# SMTP (MailHog for development)
SMTP_HOST=localhost
SMTP_PORT=1025

# Mailgun (production)
MAILGUN_API_KEY=
MAILGUN_DOMAIN=

# Resend (production)
RESEND_API_KEY=

Supported Providers

ProviderUse CaseConfiguration
SMTPDevelopment (MailHog)Host, port
MailgunProductionAPI key, domain
ResendProductionAPI key
For development, Docker Compose includes MailHog. View sent emails at http://localhost:8025.

Existing Email Notifications

Email notifications are located in app/mails/:

NotificationPurposeTemplate
VerifyEmailNotificationEmail verification codeverify_email_html.edge
ResetPasswordEmailNotificationPassword reset linkreset_password_email_html.edge
MagicLinkEmailNotificationPasswordless login linkmagic_link_email_html.edge
InvitationEmailNotificationTeam invitationinvitation_email_html.edge

Mail Class Structure

Mail classes extend BaseMail and define the email content:

import User from '#models/user'
import { BaseMail } from '@adonisjs/mail'

export default class VerifyEmailNotification extends BaseMail {
  subject = 'Verify your email address'

  constructor(private user: User) {
    super()
  }

  prepare() {
    const payload = {
      user: { firstName: this.user.firstName },
      verifyToken: this.user.emailVerificationToken.token,
    }

    this.message.to(this.user.email)
    this.message.htmlView('emails/verify_email_html', payload)
    this.message.textView('emails/verify_email_text', payload)
  }
}

Key Methods

MethodPurpose
subjectEmail subject line
prepare()Configure recipient, views, and data
message.to()Set recipient
message.htmlView()HTML template (MJML)
message.textView()Plain text fallback

Sending Emails

Direct Send

import mail from '@adonisjs/mail/services/main'
import VerifyEmailNotification from '#mails/verify_email_notification'

await mail.send(new VerifyEmailNotification(user))

For better performance, send emails through the queue:

import Queue from '@rlanz/bull-queue/services/main'
import SendVerificationEmailJob from '#jobs/send_verification_email_job'

await Queue.dispatch(
  SendVerificationEmailJob,
  { userId: user.id },
  { queueName: 'emails' }
)
All built-in emails are sent through the queue for optimal performance.

Email Templates

Templates are located in resources/views/emails/ and use MJML for responsive HTML.

Template Structure

resources/views/emails/
├── partials/
│   ├── header.edge      # Logo header
│   └── footer.edge      # Footer with copyright
├── verify_email_html.edge
├── verify_email_text.edge
├── reset_password_email_html.edge
├── reset_password_email_text.edge
├── magic_link_email_html.edge
├── magic_link_email_text.edge
├── invitation_email_html.edge
└── invitation_email_text.edge

MJML Syntax

MJML is wrapped with the @mjml() directive in Edge templates:

@mjml()
  <mjml version="3.3.3">
    <mj-body background-color="#ffffff">
      @include('emails/partials/header')
      
      <mj-wrapper background-color="#ffffff" padding="0px">
        <mj-section padding="20px 0px">
          <mj-column>
            <mj-text font-size="16px" color="#55575d">
              Hi {{ user.firstName }} 👋!
            </mj-text>
          </mj-column>
        </mj-section>

        <mj-section>
          <mj-column>
            <mj-button 
              href="{{ actionUrl }}" 
              background-color="#7c3aed"
            >
              Click Here
            </mj-button>
          </mj-column>
        </mj-section>
      </mj-wrapper>

      @include('emails/partials/footer')
    </mj-body>
  </mjml>
@end

Plain Text Version

Always include a plain text version for email clients that don't support HTML:

Hi {{ user.firstName }}!

To verify your email, use this code:

{{ verifyToken }}

Nuda Kit © 2025 - All rights reserved.

Creating New Emails

1. Create the Mail Class

// app/mails/welcome_notification.ts

import User from '#models/user'
import { BaseMail } from '@adonisjs/mail'

export default class WelcomeNotification extends BaseMail {
  subject = 'Welcome to Nuda Kit!'

  constructor(private user: User) {
    super()
  }

  prepare() {
    this.message.to(this.user.email)
    this.message.htmlView('emails/welcome_html', {
      user: { firstName: this.user.firstName },
    })
    this.message.textView('emails/welcome_text', {
      user: { firstName: this.user.firstName },
    })
  }
}

2. Create the HTML Template

<!-- resources/views/emails/welcome_html.edge -->

@mjml()
  <mjml version="3.3.3">
    <mj-body background-color="#ffffff">
      @include('emails/partials/header')
      
      <mj-wrapper background-color="#ffffff">
        <mj-section>
          <mj-column>
            <mj-text font-size="24px" font-weight="bold">
              Welcome, {{ user.firstName }}! 🎉
            </mj-text>
            <mj-text font-size="16px" color="#55575d">
              We're excited to have you on board.
            </mj-text>
          </mj-column>
        </mj-section>

        <mj-section>
          <mj-column>
            <mj-button 
              href="{{ frontendUrl }}/app" 
              background-color="#7c3aed"
              border-radius="8px"
            >
              Get Started
            </mj-button>
          </mj-column>
        </mj-section>
      </mj-wrapper>

      @include('emails/partials/footer')
    </mj-body>
  </mjml>
@end

3. Create the Text Template

<!-- resources/views/emails/welcome_text.edge -->

Welcome, {{ user.firstName }}! 🎉

We're excited to have you on board.

Get started: {{ frontendUrl }}/app

Nuda Kit © 2025 - All rights reserved.

4. Create a Job (Optional)

// app/jobs/send_welcome_email_job.ts

import { Job } from '@rlanz/bull-queue'
import mail from '@adonisjs/mail/services/main'
import User from '#models/user'
import WelcomeNotification from '#mails/welcome_notification'

export default class SendWelcomeEmailJob extends Job {
  static get $$filepath() {
    return import.meta.url
  }

  async handle(payload: { userId: number }) {
    const user = await User.findOrFail(payload.userId)
    await mail.send(new WelcomeNotification(user))
  }
}

5. Dispatch the Email

import Queue from '@rlanz/bull-queue/services/main'
import SendWelcomeEmailJob from '#jobs/send_welcome_email_job'

await Queue.dispatch(
  SendWelcomeEmailJob,
  { userId: user.id },
  { queueName: 'emails' }
)

Common MJML Components

ComponentPurpose
<mj-section>Row container
<mj-column>Column within section
<mj-text>Text content
<mj-button>Call-to-action button
<mj-image>Responsive image
<mj-divider>Horizontal line
<mj-spacer>Vertical spacing
<mj-wrapper>Full-width wrapper

Testing Emails

Preview with MailHog

  1. Ensure Docker is running
  2. Trigger an email (e.g., register a new user)
  3. Open http://localhost:8025
  4. View the email in MailHog's inbox

Unit Testing

import { test } from '@japa/runner'
import mail from '@adonisjs/mail/services/main'
import VerifyEmailNotification from '#mails/verify_email_notification'

test('sends verification email', async ({ assert }) => {
  const { mails } = mail.fake()

  await mail.send(new VerifyEmailNotification(user))

  assert.lengthOf(mails.sent, 1)
  assert.equal(mails.sent[0].subject, 'Verify your email address')
  assert.equal(mails.sent[0].to[0].address, user.email)

  mail.restore()
})

Production Setup

Mailgun

MAIL_DRIVER=mailgun
MAILGUN_API_KEY=key-xxxxxxxxxxxxx
MAILGUN_DOMAIN=mg.yourdomain.com

Resend

MAIL_DRIVER=resend
RESEND_API_KEY=re_xxxxxxxxxxxxx

SMTP with Authentication

Update config/mail.ts to enable auth:

smtp: transports.smtp({
  host: env.get('SMTP_HOST'),
  port: env.get('SMTP_PORT'),
  auth: {
    type: 'login',
    user: env.get('SMTP_USERNAME'),
    pass: env.get('SMTP_PASSWORD'),
  },
}),

Resources