Here’s the short of it:

var request = require('@root/request');

request({
    url: `https://api.mailgun.net/v3/${domain}/messages`,
    auth: `api:${apiKey}`,
    form: {
        from: 'mailer@example.com',
        'h:Reply-To': 'support@example.com',
        to: 'john.doe@gmail.com',
        subject: 'Hello',
        html: `${preHeader}<p>What's up?</p>`,
        text: "What's up?",
        attachment: [
            {
                value: "Place a string or stream.Readable or buffer here.",
                options: {
                    filename: "example.txt", // if not an fs stream.Readable
                    //contentType: "plain/text", // if cannot be determined by extension
                    //knownLength: "hello.txt".length // if cannot be determined by stream meta data
                }
            }
        ]
    }
}).then(function (resp) {
    if (resp.statusCode >= 200 && resp.statusCode < 300) {
        return resp;
    }

    var err = new Error("failed to send message");
    err.response = resp;
    throw err;
});

And, bonus, if you want to de-thread emails so that Gmail doesn’t nest them - which you might want for login emails, for example - you can use these headers:

request({
    // ...
    form: {
        // ...
        "h:Message-ID": `${rnd()}@msgs.example.com`,
        "h:X-Entity-Ref-ID": `${rnd()}@msgs.example.com`,
        "h:References": `${rnd()}@msgs.example.com`,
    }
})

Here are all the reasonable (and unreasonable) ways you could send an email with mailgun in node.js:

  • cURL
  • Sidenote: Don’t forget the Preview Text
  • @root/request (lightweight, simplest)
  • request.js (de facto)
  • axios (the new hotness)
  • mailgun-js (old, bloated)
  • http (built-in)

cURL

Actually, curl is NOT a way to send a message in Node.js, but it is the gold standard for translating HTTP requests between languages & libraries - so we’ll start with that:

MAILGUN_DOMAIN="mailer.example.com"
MAILGUN_API_KEY="xxxxxxxxxxxxxxxx"

curl -fsSL --user "api:${MAILGUN_API_KEY}" \
     "https://api.mailgun.net/v3/${MAILGUN_DOMAIN}/messages" \
     -F from='mailer@example.com' \
     -F to='john.doe@gmail.com' \
     -F subject='Hello' \
     -F html="<p>What's up?</p>" \
     -F text="What's up?"

Preview text

Just putting this here because I often forget, and it’s relevant.

If you want gmail et al to show the little preview text to the right of the subject, you want to include one of these little ditties:

var previewText = "👀 See your special greeting inside!";

var preHeader = `<span class='preheader' style='color: transparent; display: none; height: 0; max-height: 0; max-width: 0; opacity: 0; overflow: hidden; mso-hide: all; visibility: hidden; width: 0;'>${previewText}</span>`;

All you really have to do is create a span of text that is not visible as the first thing in your email. The example given is just a CYA / Kitchen Sink approach to doing so, as recommended by SendGrid.

See also:

request.js && @root/request

Just about as simple as it gets:

var request = require('@root/request');
var domain = process.env.MAILGUN_DOMAIN; // ex: mailer.example.com;
var apiKey = process.env.MAILGUN_API_KEY;

async function sendEmail() {
  return await request({
    url: `https://api.mailgun.net/v3/${domain}/messages`,
    auth: `api:${apiKey}`,
    form: {
        from: 'mailer@example.com',
        'h:Reply-To': 'support@example.com',
        to: 'john.doe@gmail.com',
        subject: 'Hello',
        html: `${preHeader}<p>What's up?</p>`,
        text: "What's up?",
    },
  });
}

Note: the original request.js doesn’t support promises, but @root/request does.

Axios

var querystring = require('querystring');

var resp = await axios({
    method: 'POST',
    url: `https://api.mailgun.net/v3/${domain}/messages`,
    auth: { username: "api", password: apiKey },
    data: querystring.stringify({
        from: 'mailer@example.com',
        'h:Reply-To': 'support@example.com',
        to: 'john.doe@gmail.com',
        subject: 'Hello',
        html: `${preHeader}<p>What's up?</p>`,
        text: "What's up?",
    }),
    headers: {
        'content-type': 'application/x-www-form-urlencoded;charset=utf-8'
    }
}).catch(function (err) {
    return err;
});

if (err instanceof Error) {
    // handle error and return...
    throw err;
}

// do more stuff...

mailgun-js

Outdated and bloat-y - probably not worth it if you just need to send messages and you’re not deeply intergrated with all of the other features:

var mailgun = require('mailgun-js')({
    apiKey: 'xxxxxxxxxxxxxxxx',
    domain: 'mailer.example.com'
});

var resp = await mailgun.messages.send({
    from: 'mailer@example.com',
    to: 'john.doe@gmail.com',
    subject: 'Hello',
    html: `${preHeader}<p>What's up?</p>`,
    text: "What's up?",
}).catch(function (err) {
  return err;
});

if (err instanceof Error) {
    // handle error and return...
    throw err;
}

// do more stuff...

http / https

The most lightweight (albeit somewhat less ergonomic), solution.

var https = require('https');

new Promise(function (resolve, reject) {
    var form = {
        from: 'mailer@example.com',
        'h:Reply-To': 'support@example.com',
        to: 'john.doe@gmail.com',
        subject: 'Hello',
        html: `${preHeader}<p>What's up?</p>`,
        text: "What's up?",
    };
    
    var buf = Buffer.from(querystring.stringify(form));

    var options = {
        hostname: 'api.mailgun.net',
        port: 443,
        path: `/v3/${domain}/messages`,
        method: 'POST',
        auth: `api:${apiKey}`,
        headers: {
            'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8',
            'Content-Length': buf.byteLength
        }
    }

    const req = https.request(options, function (resp) {
        console.log("statusCode:", resp.statusCode);

        var body = '';
        resp.on('data', function(chunk) {
            body += chunk.toString('utf8');
        })
        resp.on('end', function () {
            resp.body = body;
            try {
                resp.body = JSON.parse(resp.body);
                resolve(resp.body);
            } catch(e) {
                e.response = resp;
                reject(e);
            }
        });
    })

    req.on('error', reject);
    req.write(buf);
    req.end();
});