JustEmails
Start free trial
Tutorials··12 min read

AdonisJS Mailer Setup: Sending Email through JustEmails

AdonisJS mailer meets JustEmails SMTP. TypeScript config that actually works.

By JustEmails Platform Team
Contents
  1. What You'll Build
  2. Prerequisites
  3. Step 1: Install the Mail Package
  4. Step 2: Set Up Environment Variables
  5. Step 3: Configure the SMTP Transport
  6. Step 4: Create an Edge Email Template
  7. Step 5: Send Email from a Controller
  8. Step 6: Add a Plain-Text Fallback
  9. Common Errors and How to Fix Them
  10. Testing Locally
  11. Next Steps
  12. Frequently Asked Questions
  13. Can I use the JustEmails REST API instead of SMTP with AdonisJS?
  14. How many emails can I send through AdonisJS with JustEmails?
  15. Does JustEmails work with AdonisJS 6?
  16. Why is my AdonisJS email landing in spam when using JustEmails?
  17. Try JustEmails

I was three hours into migrating an Express app to AdonisJS when I hit the email part. The old setup used nodemailer with raw SMTP strings hardcoded in four different places. Classic. Four different places! I counted.

AdonisJS has a proper mailer package — @adonisjs/mail — with typed config, Edge templates, and multiple transport support. Every tutorial I found assumed Mailgun or SendGrid though. Nothing for simpler SMTP setups, nothing for flat-fee providers where you're not paying per thousand sends.

So I wrote this. (Partly because I was procrastinating on the actual migration.)

We're the JustEmails team — JustEmails is built by Velocity Digital Labs, same folks behind ClickzProtect for ad fraud protection and JustAnalytics for privacy-first analytics. We've done integration guides for Express, Bun/Hono, Cloudflare Workers, and others, but AdonisJS keeps coming up in support threads. Fair enough — the framework has a devoted community, and the "batteries included" philosophy means people expect email to just work.

(Honestly, I underestimated AdonisJS for years. The TypeScript-first approach and convention-over-configuration design actually make sense once you're building something larger than a weekend project.)

What You'll Build

A working AdonisJS mail configuration that sends transactional email through JustEmails SMTP. You'll have:

  • Proper environment variable setup (not hardcoded credentials — I've been burned by that one)
  • A typed mail config with JustEmails as the default transport
  • An Edge template for verification emails
  • A controller method that sends mail with error handling
  • Patterns you can copy to password resets, receipts, whatever

By the end, you can call await mail.send(...) anywhere in your AdonisJS app and emails go out through JustEmails. Your sends authenticate with SPF/DKIM because you're sending from a verified domain — no spam folder guessing games.

Prerequisites

Before starting:

  • AdonisJS 6 project (fresh or existing) — run node ace --version to confirm you're on v6
  • Node.js 20+ (AdonisJS 6 requires it)
  • A JustEmails account with at least one verified domain — start the 7-day trial if you don't have one
  • Your JustEmails SMTP credentials (Dashboard → Domains → Select domain → SMTP Settings)
  • 20 minutes

Quick note on sending limits: the base $49/year plan includes 1,000 transactional API emails per month. SMTP sends count against this same quota. For verification emails, password resets, receipts — that covers most early-stage apps. Need more? Add 10,000 emails/month for $25/year, stackable indefinitely.

If you're deciding between SMTP vs API for transactional email, this tutorial is on the SMTP side. SMTP integrates cleaner with AdonisJS's built-in mailer. The REST API makes sense if you need per-message webhooks or aren't using the mailer abstraction.

Step 1: Install the Mail Package

AdonisJS doesn't include mail out of the box. Add it:

node ace add @adonisjs/mail

This installs the package and creates config/mail.ts. The CLI also updates your adonisrc.ts to load the mail provider. You'll see output like:

DONE:    Installed @adonisjs/mail
CREATE:  config/mail.ts
UPDATE:  adonisrc.ts

If you're coming from Express, this is one of those "batteries included" moments. No hunting for compatible packages. No version conflicts at 11pm. The ace CLI handles it.

(I still occasionally forget that AdonisJS has its own CLI. Old habits from the npm-everything days. My muscle memory types npm run before my brain catches up.)

Step 2: Set Up Environment Variables

Never hardcode SMTP credentials. I've watched too many people commit secrets to public repos. AdonisJS has first-class env validation — use it.

Add these to your .env:

# JustEmails SMTP
SMTP_HOST=smtp.justemails.app
SMTP_PORT=587
SMTP_USERNAME=your-smtp-username
SMTP_PASSWORD=your-smtp-password
MAIL_FROM_ADDRESS=noreply@yourdomain.com
MAIL_FROM_NAME="Your App Name"

Get your SMTP username and password from the JustEmails dashboard: Dashboard → Domains → Select your domain → SMTP Settings. The username usually looks like your full email address.

Now register these in start/env.ts (AdonisJS validates env vars at boot):

// start/env.ts
import { Env } from '@adonisjs/core/env'

export default await Env.create(new URL('../', import.meta.url), {
  // ... existing vars ...

  SMTP_HOST: Env.schema.string(),
  SMTP_PORT: Env.schema.number(),
  SMTP_USERNAME: Env.schema.string(),
  SMTP_PASSWORD: Env.schema.string(),
  MAIL_FROM_ADDRESS: Env.schema.string(),
  MAIL_FROM_NAME: Env.schema.string.optional(),
})

If any required var is missing, AdonisJS fails at startup with a clear error. Much better than getting a cryptic SMTP auth failure at runtime.

Step 3: Configure the SMTP Transport

Open config/mail.ts and set up JustEmails as your SMTP transport:

// config/mail.ts
import env from '#start/env'
import { defineConfig, transports } from '@adonisjs/mail'

const mailConfig = defineConfig({
  default: 'justemails',

  mailers: {
    justemails: transports.smtp({
      host: env.get('SMTP_HOST'),
      port: env.get('SMTP_PORT'),
      secure: false, // STARTTLS on port 587
      auth: {
        type: 'login',
        user: env.get('SMTP_USERNAME'),
        pass: env.get('SMTP_PASSWORD'),
      },
    }),
  },

  from: {
    address: env.get('MAIL_FROM_ADDRESS'),
    name: env.get('MAIL_FROM_NAME', 'Your App'),
  },
})

export default mailConfig

declare module '@adonisjs/mail/types' {
  export interface MailersList extends InferMailers<typeof mailConfig> {}
}

A few things happening here:

  • secure: false with port 587 means STARTTLS — the connection starts unencrypted then upgrades. Standard for submission ports. Port 465 uses implicit TLS and would need secure: true.
  • The auth.type: 'login' is explicit. Some SMTP servers are picky about this. I've wasted hours debugging auth issues that turned out to be the wrong type.
  • The from object sets defaults. Override per-email if you want.
  • That declare module block at the bottom gives you TypeScript autocompletion for mail.use('justemails'). Worth the boilerplate.

Multiple sending domains? Add more transports — one per domain — and switch between them. Not my favorite pattern honestly, but it works.

Step 4: Create an Edge Email Template

AdonisJS uses Edge for templating. Create a folder for email templates:

mkdir -p resources/views/emails

Now create a verification email template:

{{-- resources/views/emails/verification.edge --}}
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px;">
  <h2 style="color: #1a1a1a; margin-bottom: 24px;">Verify your email</h2>

  <p style="color: #4a4a4a; line-height: 1.6;">
    Hi{{ userName ? ` ${userName}` : '' }},
  </p>

  <p style="color: #4a4a4a; line-height: 1.6;">
    Click the button below to verify your email address:
  </p>

  <a href="{{ verificationUrl }}"
     style="display: inline-block; background: #0070f3; color: white;
            padding: 12px 24px; text-decoration: none; border-radius: 6px;
            margin: 24px 0; font-weight: 500;">
    Verify Email
  </a>

  <p style="color: #888; font-size: 14px; margin-top: 32px;">
    If you didn't create an account, you can ignore this email.
  </p>

  <p style="color: #888; font-size: 14px;">
    Or copy this link: {{ verificationUrl }}
  </p>
</body>
</html>

Edge templates are basically HTML with {{ variable }} interpolation. The {{ userName ? ... }} syntax handles the optional name — Edge supports JavaScript expressions inside the double braces. Nice.

(I know inline styles are ugly. But email clients still don't handle CSS reliably in 2026 — Gmail strips <style> blocks in many cases. Inline is the least-bad option. I hate it too.)

Step 5: Send Email from a Controller

Create a service or put this in your auth controller. Here's a clean pattern:

// app/services/email_service.ts
import mail from '@adonisjs/mail/services/main'

interface SendVerificationOptions {
  to: string
  userName?: string
  verificationUrl: string
}

export class EmailService {
  async sendVerification({ to, userName, verificationUrl }: SendVerificationOptions) {
    try {
      const response = await mail.send((message) => {
        message
          .to(to)
          .subject('Verify your email address')
          .htmlView('emails/verification', {
            userName,
            verificationUrl,
          })
      })

      return { success: true, messageId: response.messageId }
    } catch (error) {
      console.error('Email send failed:', error)

      // Parse common SMTP errors
      const errorMessage = error instanceof Error ? error.message : 'Unknown error'

      if (errorMessage.includes('535')) {
        return { success: false, error: 'SMTP authentication failed' }
      }
      if (errorMessage.includes('550')) {
        return { success: false, error: 'Recipient rejected' }
      }

      return { success: false, error: 'Failed to send email' }
    }
  }
}

Then in your controller:

// app/controllers/auth_controller.ts
import { EmailService } from '#services/email_service'
import type { HttpContext } from '@adonisjs/core/http'

export default class AuthController {
  async register({ request, response }: HttpContext) {
    // ... user creation logic ...

    const emailService = new EmailService()
    const token = generateVerificationToken() // your implementation
    const verificationUrl = `https://yourapp.com/verify?token=${token}`

    const result = await emailService.sendVerification({
      to: user.email,
      userName: user.name,
      verificationUrl,
    })

    if (!result.success) {
      // User created but email failed — decide your policy
      // I usually proceed but flag for retry
      console.warn(`Verification email failed for ${user.email}: ${result.error}`)
    }

    return response.created({ user, emailSent: result.success })
  }
}

The mail.send() callback gets a message builder. Chain .to(), .subject(), .htmlView(), and optionally .textView() for a plain-text fallback. The builder pattern keeps things readable. I actually like this API — not something I say often about email libraries.

Step 6: Add a Plain-Text Fallback

Some corporate email clients prefer plain text. Create a text template:

{{-- resources/views/emails/verification_text.edge --}}
Verify your email

Hi{{ userName ? ` ${userName}` : '' }},

Click the link below to verify your email address:

{{ verificationUrl }}

If you didn't create an account, you can ignore this email.

Update the send call:

await mail.send((message) => {
  message
    .to(to)
    .subject('Verify your email address')
    .htmlView('emails/verification', { userName, verificationUrl })
    .textView('emails/verification_text', { userName, verificationUrl })
})

Both versions go out in the same email as MIME parts. The recipient's client picks which to display. Most people skip the text version. Don't be most people.

Common Errors and How to Fix Them

Real talk — you'll probably hit at least one of these.

"535 Authentication failed"

Your SMTP credentials are wrong. Check:

  • Did you copy the full username (usually an email address)?
  • Is the password correct? Regenerate it in the JustEmails dashboard if unsure.
  • Did you restart the AdonisJS server after updating .env?

AdonisJS caches env vars at boot. You need to restart. This one gets me every single time.

"550 Sender address rejected"

You're trying to send from a domain that isn't verified in JustEmails. The MAIL_FROM_ADDRESS must match a domain you've added and verified in Dashboard → Domains. Add the DNS records and wait for verification if you haven't.

Emails send but land in spam

Check your email authentication. JustEmails auto-configures SPF/DKIM/DMARC when you verify a domain, but if you added records manually, something might be off. Run a test at mail-tester.com. Our custom domain setup guide covers DNS configuration if things look wrong.

"ECONNREFUSED" or connection timeouts

Either your server can't reach the internet, or there's a firewall blocking outbound SMTP. Port 587 is sometimes blocked on shared hosting. Try:

telnet smtp.justemails.app 587

If that hangs or refuses, it's a network issue on your end.

"Missing environment variable" at startup

You added vars to .env but didn't register them in start/env.ts. AdonisJS validates the schema on boot. Add the missing schema entries.

Testing Locally

Use AdonisJS's mail fake for tests:

// tests/functional/auth.spec.ts
import { test } from '@japa/runner'
import mail from '@adonisjs/mail/services/main'

test.group('Auth registration', () => {
  test('sends verification email on signup', async ({ client, assert }) => {
    const { mails } = mail.fake()

    const response = await client.post('/register').json({
      email: 'test@example.com',
      password: 'securepassword',
    })

    response.assertStatus(201)

    assert.isTrue(
      mails.exists((mail) => {
        return mail.to?.includes('test@example.com') &&
               mail.subject === 'Verify your email address'
      })
    )

    mail.restore()
  })
})

The fake traps all sends and lets you assert against them without hitting real SMTP. Essential for CI pipelines. I've seen people skip this and test against real SMTP in CI — don't.

Next Steps

You've got AdonisJS sending email through JustEmails. What now?

Queue your emails. Right now, mail.send() blocks the request while waiting for SMTP. For production, push emails to a queue (AdonisJS has a jobs package) and process them async. Users don't wait, and you can retry failures.

Add more templates. Password reset, welcome email, receipts. Same pattern — Edge template, service method, controller call. Keep templates in resources/views/emails/ and they're easy to find.

Monitor your quota. JustEmails Dashboard → API → Usage shows your monthly sends. The base $49/year plan includes 1,000/month. If you're approaching that, add a 10,000/month tier for $25/year — stackable. Still way cheaper than per-send pricing at Postmark or SendGrid, which adds up fast once you're past the free tier.

If you need analytics alongside your app, JustAnalytics works well with AdonisJS — same privacy-first approach, simple integration. And if you're running paid campaigns to drive signups, ClickzProtect catches invalid clicks before they burn your ad budget.

AdonisJS doesn't get enough credit. The TypeScript-first design, the CLI tooling, the convention-over-configuration approach — less time fighting the framework, more time building features. Email setup is typical: install the package, add config, send. Compare that to wiring up nodemailer manually with Express middleware. Night and day.

Frequently Asked Questions

Can I use the JustEmails REST API instead of SMTP with AdonisJS?

Yes. AdonisJS mailer supports custom transports, so you can write a driver that calls the JustEmails HTTP API directly. But for most use cases, SMTP is simpler to configure and works out of the box with AdonisJS's built-in smtp transport. The REST API makes more sense if you need webhook confirmations per-message or you're building something that doesn't fit the mailer abstraction.

How many emails can I send through AdonisJS with JustEmails?

The base $49/year plan includes 1,000 transactional API emails per month. SMTP sends count against the same quota. If you need more, add 10,000 emails/month for $25/year — stackable as many times as needed. Most early-stage apps doing verification emails and password resets stay well under 1,000 monthly.

Does JustEmails work with AdonisJS 6?

Yes. The @adonisjs/mail package works the same in AdonisJS 6. The config lives in config/mail.ts, you define transports there, and the Mail.send() API is unchanged. The Edge templating also works identically. This tutorial covers AdonisJS 6 syntax specifically.

Why is my AdonisJS email landing in spam when using JustEmails?

Usually it's missing email authentication on your domain. JustEmails auto-configures SPF, DKIM, and DMARC when you verify a domain in the dashboard, but if you added records manually, something might be misconfigured. Run your domain through mail-tester.com to see what's failing. Also check that your FROM_EMAIL env var matches a verified domain in your JustEmails account.


Try JustEmails

Unlimited custom domain email hosting for $49/year flat — unlimited domains, unlimited mailboxes, 10 GB storage, full IMAP/SMTP. Built for agencies, freelancers, and anyone managing email across more than one domain.

Start your 7-day free trial → · How it compares

adonisjstransactional-emailsmtp-integrationtypescriptnode-frameworkbuildinpublicsaasstudioaiworkforcebuildwithclaude

Related posts

Guides
How to Set Up Role-Based Team Inboxes (support@, sales@, billing@)
12 min read
Tutorials
Uptime Kuma Email Alerts: JustEmails SMTP Setup (2026)
11 min read
Industry
Email Hosting Cost for 25-Person Remote Teams (2026)
11 min read