Create a Private Email API for Free
Steeve
Posted on May 8, 2024
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 Email API server : 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:
- Make an HTTP request to the Email API from a website or server.
- 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. - 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 : Send maximum 300 emails/day
- Smtp2go : Send 1,000 emails/month
- Mailjet : Send 6000 emails/month
- Google SMTP: If you have a Google Workspace subscription, you can access a quota of emails per months.
- OVHCloud: When you buy a domain and an hosting plan, OVH gives you a free SMTP server for one or multiple emails addresses.
- Proton Mail: 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:
git clone https://github.com/steevepay/email-api.git
- Go to the
email-api
directory:
cd email-api
- Create an .env file with the following configuration, and add your SMTP credentials:
# 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:
# 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:
{
"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. Here is the /send
request:
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.
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:
// 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:
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 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: The service is mutualised, and that's cheap and easy to maintain.
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 🍻
Posted on May 8, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.