Nuda Kit uses AdonisJS Mail for sending emails and MJML for creating responsive HTML email templates.
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',
}),
},
})
# 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=
| Provider | Use Case | Configuration |
|---|---|---|
| SMTP | Development (MailHog) | Host, port |
| Mailgun | Production | API key, domain |
| Resend | Production | API key |
Email notifications are located in app/mails/:
| Notification | Purpose | Template |
|---|---|---|
VerifyEmailNotification | Email verification code | verify_email_html.edge |
ResetPasswordEmailNotification | Password reset link | reset_password_email_html.edge |
MagicLinkEmailNotification | Passwordless login link | magic_link_email_html.edge |
InvitationEmailNotification | Team invitation | invitation_email_html.edge |
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)
}
}
| Method | Purpose |
|---|---|
subject | Email subject line |
prepare() | Configure recipient, views, and data |
message.to() | Set recipient |
message.htmlView() | HTML template (MJML) |
message.textView() | Plain text fallback |
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' }
)
Templates are located in resources/views/emails/ and use MJML for responsive HTML.
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 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
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.
// 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 },
})
}
}
<!-- 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
<!-- 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.
// 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))
}
}
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' }
)
| Component | Purpose |
|---|---|
<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 |
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()
})
MAIL_DRIVER=mailgun
MAILGUN_API_KEY=key-xxxxxxxxxxxxx
MAILGUN_DOMAIN=mg.yourdomain.com
MAIL_DRIVER=resend
RESEND_API_KEY=re_xxxxxxxxxxxxx
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'),
},
}),