Create a Private Email API for Free
Tags: api, programming, node, opensource
Published: 2024-05-08 16:00:35

> This article covers how to create your own secured Email API for free, with all the code explained.

The origin of this project came because I wanted to make websites really fast to load worldwide, almost instantaneously (< 30ms). The problem is that: 
* Each of my website has a server for sending emails when the contact form is filled. 
* Servers are slow and located in one (or two) place on the planet earth. 
* Replicating Server is a pain to manage and expensive.


My strategies to solve that issues was to:
* **Keep only the front-end and upload it to CDN:** the website is pre-built in pure HTML/CSS/JS, and it is now available worldwide. No more servers. This article does not cover this subject.
* **Make an independent <u>Email API server</u> :** Anytime a contact form is filled from one or multiple website, it will request the Email API, and emails will be sent 🎉 This is what we are going to talk about :D

## 👋 Meet Email API 

The Email API is open-source on Github, made with NodeJS: https://github.com/steevepay/email-api. The code is super simple with one API endpoint `POST /send`. Here is the detailed flow to send an Email:
![Flow chart to send a mail with the Mailer API through HTTP requests](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ohcvjcom6t26aq4rplnb.png)

1. Make an HTTP request to the Email API from a website or server.
2. Each HTTP request is verified: if the domain origin is part of the CORS whitelist, or an Access key is provided on the `Authorization` header, the request is accepted, otherwise it returns the 401 status code.
3. The Email API connects to the SMTP server (of your choice), then it send the email.

## Get your SMTP credentials

The Email API connects to an SMTP server to send mails, that's why you need SMTP credentials. Many SMTP servers are available for free, choose the service your prefer:
* [Brevo](https://www.brevo.com/free-smtp-server/) : Send maximum 300 emails/day
* [Smtp2go](https://www.smtp2go.com/pricing/) : Send 1,000 emails/month
* [Mailjet](https://www.mailjet.com/products/email-api/smtp-relay/) : Send 6000 emails/month
* [Google SMTP](https://support.google.com/a/answer/2956491): If you have a Google Workspace subscription, you can access a quota of emails per months.
* [OVHCloud](https://help.ovhcloud.com/csm/en-mx-plan-getting-started?id=kb_article_view&sysparm_article=KB0051928): When you buy a domain and an hosting plan, OVH gives you a free SMTP server for one or multiple emails addresses.
* [Proton Mail](https://proton.me/support/smtp-submission): If you have a Proton subscription, you can access SMTP credentials
* Non-exhaustive list, do your own research...

## Email API Setup

> Before going further, make sure you have installed: Git, NodeJS, and optionally Docker.

- First clone the following repository:
```sh
git clone https://github.com/steevepay/email-api.git
```
- Go  to the `email-api` directory:
```sh
cd email-api
```
- Create an .env file with the following configuration, and add your SMTP credentials:
```dotenv
# SERVER
DOMAIN="mailer.domain.com"
PORT=3000
# SECURITY WHITELIST (OPTIONAL): multiple domains are accepted if it is seperated with a coma, such as `website1.org,website2.org`
WHITELIST="domain.org"
# SECURITY ACCESS KEY (OPTIONAL)
AUTHORIZATION_KEY=""
# SMTP CONFIG
SMTP_HOST=""
SMTP_PORT=""
SMTP_FROM=""
SMTP_PASS=""
```
- Install NPM packages with `npm install`
- To start the server:
```sh
# With NPM Command Line:
npm run start
# With Docker Compose:
docker-compose up
```

**To send an email**, make an HTTP request to endpoint `POST /send`, you must provide the following body as JSON:
```json
{
  "from": "",
  "to": "",
  "subject": "",
  "text": "",
  "html": "",
}
```
The `from` is the email sender, and `to` is the email receiver. The content of the email must be provided as html and raw text.

If everything goes right, the response will return **OK** with the status code **200**. 

If something goes wrong, it will return the following responses:
* **401**: Unauthorized, the origin domain is probably not part of the CORS whitelist, or you did not provide the Access Key
* **404**: The page you are requesting is not correct
* **405**: The body of the HTTP request is not a JSON, or malformed, or it is missing an attribute.
* **500**: The server has an issue, check logs, probably your SMTP credentials are not correct, or the email failed to be delivered.

## Stack and Code Break-down

Only three files are required to start the API:
```
index.js
mailer.js
.env
```
On the **index.js**, environment variables are loaded, then we can find the API routing powered with [Express](https://www.npmjs.com/package/express). Here is the `/send` request:
```js
app.use(express.json());
app.options('/send', cors(corsOptionsDelegate)) // enable pre-flight request for POST request
app.post(
  "/send",
  cors(corsOptionsDelegate),
  body("from").optional().isEmail().normalizeEmail(),
  body("to").optional().isEmail().normalizeEmail(),
  body("cc").optional().isEmail().normalizeEmail(),
  body("subject")
    .optional()
    .not()
    .isEmpty()
    .trim()
    .escape()
    .isLength({ min: 2 }),
  body("html").optional().not().isEmpty().trim().escape().isLength({ min: 2 }),
  body("text").optional().not().isEmpty().trim().escape().isLength({ min: 2 }),
  (req, res, next) => {
    // Finds the validation errors in this request and wraps them in an object with handy functions
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(400).json({ errors: errors.array() });
    }
    return next();
  },
  mailer.sendEmail
);
```
Notice each body parameter is verified with the Node module [express-validator](https://www.npmjs.com/package/express-validator).

The CORS npm module is loaded as middleware, and checks before each HTTP request if the Origin domain of the request is part of the whitelist:
```js
// Configure CORS
var whitelist = process.env?.WHITELIST && process.env?.WHITELIST !== '' ? process.env.WHITELIST?.split(',') : [];
if (whitelist.length > 0) {
    console.log("CORS Whitelist: ", whitelist)
}
var corsOptionsDelegate = function (req, callback) {
  if (whitelist.indexOf(req.header('Origin')) === -1 && req.header('authorization') !== process.env.AUTHORIZATION_KEY) {
    return callback(new Error("Not allowed by CORS"));
  } 
  return callback(null, true);
}
```
The middleware also accept an Access Key, and must be provided through the header `Authorization`.

If the request configuration is good, the last middleware ` mailer.sendEmail` is executed to send the email. The function `mailer.sendEmail` is located in the file **mailer.js**:

```js
const nodemailer = require('nodemailer')

sendEmail = function (req, res) {
  const _cc =  [];

  if (req.body.cc) {
    _cc.push(req.body.cc);
  }
  if (process.env.SMTP_CC) {
    _cc.push(process.env.SMTP_CC);
  }
  if (req.body.from && req.body.to && req.body.subject && req.body.text && req.body.html) {
    if (req.body?.cc) {
      _cc.push(req.body.cc);
    }
    if (process.env.SMTP_CC) {
      _cc.push(process.env.SMTP_CC);
    }
    return deliver({
      from: req.body.from,
      to: req.body.to,
      cc: _cc,
      subject: `${req.body.subject}`,
      text: req.body.text, 
      html: req.body.html
    }, res);
  } else {
    return res.sendStatus(405); // Method Not Allowed
  }
}


function deliver(message, res) {
  const transporter = nodemailer.createTransport({
    host: process.env.SMTP_HOST,
    port: process.env.SMTP_PORT,
    secure: true, // true for 465, false for other ports
    auth: {
      user: process.env.SMTP_FROM,
      pass: process.env.SMTP_PASS
    },
    logger: true
  })

  transporter.sendMail(message, (error, info) => {
    if (error) {
      // eslint-disable-next-line no-console
      console.error("🔴 Send Email Error:", error.toString());
      res.sendStatus(500)
    } else {
      res.sendStatus(200)
    }
  })
}

module.exports = {
  sendEmail
}
```

The [Nodemailer](https://www.npmjs.com/package/nodemailer) module is used to connect to the SMTP server through the `nodemailer.createTransport` function, then the email is sent with `transporter.sendMail`. At the beginning of the function, each parameter of the mail is verified. If something is missing, the status code **405** is returned.

## Conclusion

Now multiple websites can request the same API: <b>The service is mutualised, and that's cheap and easy to maintain</b>.

Big downsides to consider:
* You can send emails only from one address, the one linked to your SMTP credentials.
* To send emails from a new website, you must add the website domain to the CORS whitelist, that requires a reboot to load new environment variables.
* The API supports only one single Access Key, if you want to update it, you must edit the `.env` file and reboot the server.

Thanks for reading! cheers 🍻