Skip to content

βœ‰οΈ Mailgun Setup

πŸ“ Introduction

Mailgun is not required, but you’ll need an email service to handle magic login links, abandoned cart emails, and more.


⚠️ Notes:

  • Mailgun silently removed the "pay-as-you-go" tier from the pricing page, but it still exists.
  • πŸ’‘ Tip: Start with the $35 trial and cancel it.
  • πŸ”„ Result: You’ll be downgraded to the free β€œpay-as-you-go” tier automatically.
  • πŸ’° Cost: If you send fewer than 1,000 emails per month, it’s only $1/month.

βš™οΈ Setup

1️⃣ Create a new Mailgun account.
2️⃣ Under [Sending], go to [Domains] β†’ [+ Add Domain]
- 🌐 Recommended: Use a subdomain like mail.yourdomain.com.

3️⃣ If using the EU region:
- Add the following in libs/mailgun.js:

url: "https://api.eu.mailgun.net/"
````

4️⃣ Complete all DNS verification steps

* 🌍 If using a subdomain, make sure your DNS records are updated.

5️⃣ Add an additional DMARC record to improve deliverability:
TXT | _dmarc.mail.yourdomain.com | v=DMARC1; p=none
6️⃣ SMTP Setup:

* Go to \[Domain Settings] β†’ \[SMTP Credentials] β†’ \[Reset Password]
* Choose \[Automatic] β†’ \[Create Password]
* Click \[Copy] in the bottom-right of the popup
* πŸ“‚ In `.env.local`, set:

```bash
smtp://postmaster@[mail.yourdomain.com]:[copied_password]@smtp.mailgun.org:587

7️⃣ API Key Setup:

  • Under [Sending API Keys], click [Create sending key]
  • Add to .env.local as MAILGUN_API_KEY

8️⃣ Update config.js:

mailgun: {
  subdomain: "mg",
  fromNoReply: `SaaSFast <noreply@mg.SaaSfa.st>`,
  fromAdmin: `Marc at SaaSFast <marc@mg.SaaSfa.st>`,
  supportEmail: "marc@mg.SaaSfa.st",
  forwardRepliesTo: "marc.louvion@gmail.com",
}

9️⃣ TypeScript Config (optional): In types/config.ts, add:

mailgun: {
  subdomain: string;
  fromNoReply: string;
  fromAdmin: string;
  supportEmail?: string;
  forwardRepliesTo?: string;
};

πŸ—ƒοΈ Mailgun Library Setup

πŸ”Ÿ Create libs/mailgun.js with:

import config from "@/config";
const formData = require("form-data");
const Mailgun = require("mailgun.js");
const mailgun = new Mailgun(formData);

const mg = mailgun.client({
  username: "api",
  key: process.env.MAILGUN_API_KEY || "dummy",
});

export const sendEmail = async ({ to, subject, text, html, replyTo }) => {
  const data = {
    from: config.mailgun.fromAdmin,
    to: [to],
    subject,
    text,
    html,
    ...(replyTo && { "h:Reply-To": replyTo }),
  };

  await mg.messages.create(
    (config.mailgun.subdomain ? `${config.mailgun.subdomain}.` : "") + config.domainName,
    data
  );
};

11️⃣ Update EmailProvider in libs/next-auth.js:

EmailProvider({
  server: process.env.EMAIL_SERVER,
  from: config.mailgun.fromNoReply,
}),

12️⃣ Replace all config.resend with config.mailgun, and libs/resend with libs/mailgun across your codebase.


πŸ“§ Receiving Emails

13️⃣ Go to [Receiving] β†’ [Create Route]:

  • Choose [Match Recipient]
  • Add the email address you'll send from (e.g. name@mail.yourdomain.com)
  • Forward to:
https://[your-domain].com/api/webhook/mailgun
  • Click [Create Route]

14️⃣ Webhook Setup:

  • Go to [Sending] β†’ [Webhooks]
  • Copy the HTTP Webhook Signing Key
  • Add to .env.local as MAILGUN_SIGNING_KEY

15️⃣ Add a forwarding email (e.g., your Gmail) to mailgun.forwardRepliesTo in config.js.


πŸ—‚οΈ Create Mailgun Webhook Route

16️⃣ Create app/api/webhook/mailgun/route.js:

import { NextResponse } from "next/server";
import { sendEmail } from "@/libs/mailgun";
import config from "@/config";
import crypto from "crypto";

export async function POST(req) {
  try {
    const formData = await req.formData();
    const signingKey = process.env.MAILGUN_SIGNING_KEY;
    const timestamp = formData.get("timestamp").toString();
    const token = formData.get("token").toString();
    const signature = formData.get("signature").toString();

    const hash = crypto.createHmac("sha256", signingKey)
      .update(timestamp + token)
      .digest("hex");

    if (hash !== signature) {
      return NextResponse.json({ error: "Invalid signature" }, { status: 401 });
    }

    const sender = formData.get("From");
    const subject = formData.get("Subject");
    const html = formData.get("body-html");

    if (config.mailgun.forwardRepliesTo && html && subject && sender) {
      await sendEmail({
        to: config.mailgun.forwardRepliesTo,
        subject: `${config?.appName} | ${subject}`,
        html: `<div><p><b>- Subject:</b> ${subject}</p><p><b>- From:</b> ${sender}</p><p><b>- Content:</b></p><div>${html}</div></div>`,
        replyTo: sender,
      });
    }

    return NextResponse.json({});
  } catch (e) {
    console.error(e?.message);
    return NextResponse.json({ error: e?.message }, { status: 500 });
  }
}

πŸ”„ DNS Records Check

Check DNS record validity using MxToolbox (enter the subdomain if applicable).


βœ‰οΈ Sending Emails

  1. SMTP

  2. Used for sending magic login links.

  3. Mailgun API

  4. Use sendEmail() in libs/mailgun.js for everything else.


πŸ“₯ Receiving Emails

  • 🚫 Mailgun does not store or forward emails by default.
  • That’s why we created a route to handle emails sent to mailgun.supportEmail from config.js and redirect them to your API.
  • πŸ“§ Replies will automatically include the original sender in the reply-to field, so you can reply directly from your inbox.

βœ… Spam Prevention Checklist

πŸ“· There’s an image here