Author's Picture
Author: Joel Gray Published: 19 May 2023 Read Time: ~16 minutes

Setting Up an Email Server for React Apps with Express, Nodemailer, and Postfix

Welcome to Graycode’s step-by-step guide on building a React contact form with email functionality using an Express server, Nodemailer, and Postfix!

Are you trying to make a React contact form work with Postfix on your own VPS? Don’t want to pay for MailChimp? Look no further, as we’ve got you covered. Here are the essentials of what we’re going to achieve:

  • Secure Email Transmission: We’ll use Nodemailer within an Express server to protect your email credentials from exposure. Your React app will communicate with this server, ensuring security and privacy.
  • React Contact Form: Our guide will help you build a fully functional contact form within your React application. This form will handle user inputs and requests, further communicating with the Express server.
  • Postfix Integration: We’ll make use of Postfix, a powerful open-source mail transfer agent, to route our emails. We’ll configure this to work on your VPS, ensuring you have full control over your email functionality.

By the end of this guide, you’ll have a React contact form that securely sends emails using your own VPS, Express, Nodemailer, and Postfix. Let’s get started!

TLDR

Skip to Express Set Up

Skip to Nodemailer Set Up

Skip to React Contact Form Set Up

Skip to Security Set Up

High level overview of what we’re going to achieve

  1. React App & Contact Form: The process begins when a user interacts with the contact form on your React application. They’ll fill out the fields and hit ‘submit’. This triggers the handleSubmit function in your ContactPage React component. This function collects the form data and sends a POST request to the Express server with this data.
  2. Express Server: The server, running an Express.js application, listens for POST requests on the ‘/api/sendmail’ endpoint. When it receives a request, it calls the sendMail function with the necessary data from the request body. The server then responds to the POST request, indicating whether the email was sent successfully or not.
  3. Nodemailer & Postfix: The sendMail function is where Nodemailer and Postfix come into play. Nodemailer uses a transporter object, configured to use Postfix, to send the email. The function logs the process and any errors to a file, and sends a message back to the Express server, indicating whether the email was sent successfully.

The benefits of this setup compared to a paid solution like Mailchimp include:

  • Control: You have total control over your setup, from the server to the mailing agent. You can customise it to meet your specific needs.
  • Cost-effectiveness: You avoid the recurring costs of a paid service. This can make a significant difference, especially for a small business or a start-up.
  • Privacy: Your email data doesn’t pass through a third-party service, which can be a consideration if privacy is a key concern for your project.
  • Learning opportunity: Implementing this setup provides an excellent learning opportunity. You’ll get to work with several different technologies and gain a deeper understanding of how email works.

Remember, however, that this setup might not be right for everyone. For larger businesses or projects where email deliverability is critical, a paid service like Mailchimp may still be a better choice. They take care of a lot of the technical aspects and ensure high deliverability rates, which can be challenging to achieve with a self-hosted solution.

Why do I need an Express server to send emails from React?

Now, you might be wondering: Why do we need an Express server, and why can’t Nodemailer work independently? Here’s why:

  • Sensitive Data Protection: Nodemailer requires SMTP credentials to send emails. Embedding these credentials directly in your client-side code (React app) exposes them to potential misuse. An Express server allows you to securely hide these credentials on the server side.
  • Backend Interaction: Nodemailer is a Node.js module, which works on the server-side. Therefore, it can’t be run directly within the client-side React code. The Express server acts as the intermediary to facilitate this interaction.
  • Controlling Request Flow: With an Express server, you can control the flow of requests and responses, manage error handling, and maintain the overall robustness of your application’s communication flow.

Therefore, while Nodemailer is an excellent tool for sending emails in Node.js, it’s the combination of React, Express, and Nodemailer that delivers a secure, functional, and efficient solution for handling emails in your React app.

Setting up the Express Server

First, let’s explore the code needed to set up your Express server. You should put this in a server.js file located at the root of your project directory:

require('dotenv').config({ path: __dirname+'/.env', override: true });
const express = require('express');
const bodyParser = require('body-parser');
const sendMail = require('./src/sendMail.js');
const app = express();
const port = process.env.PORT || 3001;

app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));

app.post('/api/sendmail', async (req, res) => {
  const { name, from, subject, message } = req.body;
  try {
    await sendMail(name, "[email protected]", from, subject, message);
    res.status(200).send({ message: 'Email sent successfully' });
  } catch (error) {
    console.error('Error sending email:', error);
    res.status(500).send({ message: 'Error sending email' });
  }
});

app.listen(port, () => {
  console.log(`Server is running on port ${port}`);
});

In this file, the code first sets up an Express server and configures it to parse JSON and URL-encoded bodies. It uses the dotenv package to securely load environment variables, such as the port number for the server to listen on. Checkout how to set up your .env file here. Then, it sets up a POST route at /api/sendmail which is expected to receive a JSON object with fields name, from, subject, and message. When a request is made to this route, it attempts to send an email using a function sendMail imported from a local module. If the email is sent successfully, the server responds with a status of 200 and a success message. If there’s an error, it logs the error and sends a response with status 500 and an error message. Finally, the server starts listening on the specified port, logging a message to console indicating that it is running.

NOTE: You will have to expose port 3001 in your firewall for this to work. We show you how to proxy this url later so you don’t have to expose your ports to the world. Check that out here.

Setting up Nodemailer with Postfix

Now, let’s tackle sendMail.js, the function responsible for actually sending the emails using Nodemailer and Postfix. When the user submits a contact form it performs a post request that goes to the Express server we previously set up, after that the server.js called the sendMail module we imported, this sendMail.js is that module. It is the part that actually interacts with Postfix to send the email.

Imagine, Contact Form sends to Express Server – Client Side to Server Side, and then Express Server send to Nodemailer – Server Side to Email Client.

const nodemailer = require('nodemailer');
const fs = require('fs');

const logToFile = (message) => {
  const logMessage = `[${new Date().toISOString()}] ${message}\n`;
  fs.appendFile('sendMail.log', logMessage, (err) => {
    if (err) {
      console.error('Error writing to log file:', err);
    }
  });
};

const sendMail = async (name, to, from, subject, message) => {
  logToFile('Sending email..., in the sendMail.js file');
  try {
    let transporter = nodemailer.createTransport({
      service: 'postfix',
      host: 'localhost',
      secure: false,
      port: 25,
      auth: { user: process.env.EMAIL_USER, pass: process.env.EMAIL_PASS },
      tls: { rejectUnauthorized: false },
    });

    const mailOptions = {
      from: from,
      to: to,
      subject: subject,
      text: `${name} has sent a message: ${message}`,
    };

    await transporter.sendMail(mailOptions);
    logToFile('Email sent!');
    return true;
  } catch (error) {
    logToFile(`Error sending email: ${error}`);
    return false;
  }
};

module.exports = sendMail;

This code primarily relies on the nodemailer package to handle email sending, using Postfix as the mail transfer agent.

Let’s delve into the details.

The logToFile function is a utility that logs messages with timestamps into a file named ‘sendMail.log’. It’s used throughout the sendMail function to keep track of the email sending process and log any potential errors.

The sendMail function is the crux of this script. It begins by logging that the email sending process has started. It then creates a transporter object using nodemailer.createTransport(). This transporter is configured to use Postfix, a free and open-source mail transfer agent that routes and delivers electronic mail. It’s known for its flexibility, security, and efficiency, making it a popular choice for sending emails from applications.

The transporter configuration includes Postfix service details like the host (which is set to ‘localhost’ as Postfix is typically running on the same machine as this script), the port number (commonly 25 for mail servers), and the authentication details. It’s important to note that we’ve set secure to false and tls.rejectUnauthorized to false because we’re using a local Postfix server. However, in a production scenario with a remote server, these settings might need to be adjusted to maintain secure connections.

Next, an email message is composed with mailOptions, specifying the sender’s email (from), the recipient’s email (to), the email subject (subject), and the email body (text). The email body includes the sender’s name and the message.

The transporter then attempts to send the email using transporter.sendMail(mailOptions). If the email is sent successfully, a message is logged and the function returns true. If an error occurs at any point in the process, the error message is logged and the function returns false.

Finally, the sendMail function is exported as a module to be imported and used in the Express server code.

This integration between your React application, Express server, Nodemailer, and Postfix ensures your app can securely send emails from the client side to any email address, while also preserving the integrity and security of your application.

Sending an Email via a React Contact Form

In your react app you will have a contact form that the user is going to fill in and submit, when that submission happens it will get sent to our Express server and the email sending begins as described previously.

Let’s see how do set up a contact form to do this!

import React, { useState } from "react";

const ContactPage = () => {
  const [state, setState] = useState({
    name: "",
    from: "",
    subject: `Contact Form Submission`,
    message: "",
  });

  const handleChange = (event) => {
    setState((prevState) => ({
      ...prevState,
      [event.target.name]: event.target.value,
    }));
  };

  const handleSubmit = async (event) => {
    event.preventDefault();
    const { name, from, subject, message } = state;

    try {
      const response = await fetch("https://example.com:3001/api/sendmail", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ name, from, subject, message }),
      });
      if (response.status === 200) {
        console.log("Email sent!. \nResponse:", response);
        setState((prevState) => ({ ...prevState, formSubmitted: true }));
      } else {
        console.log("Email not sent. \nResponse:", response);
      }
    } catch (error) {
      console.error("Error sending email:", error);
    }
  };

  const { name, from, message } = state;

  return (
    <div className="contact-page">
      <form className="contact-form" onSubmit={handleSubmit}>
        <input
          type="text"
          name="name"
          value={name}
          onChange={handleChange}
          required
        />
        <input
          type="email"
          name="from"
          value={from}
          onChange={handleChange}
          required
        />
        <textarea
          name="message"
          value={message}
          onChange={handleChange}
          required
        />
        <button type="submit">Submit</button>
      </form>
    </div>
  );
};
export default ContactPage;

Replace https://example.com:3001/api/sendmail with your actual server’s address.

This is a React functional component named ContactPage that manages a contact form. Using React’s useState hook, it manages the state of the form fields: ‘name’, ‘from’, ‘subject’, and ‘message’. The handleChange function updates the state as the user types in the fields, and the handleSubmit function is triggered when the form is submitted. This function sends a POST request to the ‘/api/sendmail’ endpoint of the server with the form data. It also handles the response from the server, logging the result and setting the formSubmitted state to true if the email is successfully sent. The return statement renders the form in the UI, using controlled components for the input fields.

CONGRATULATIONS!

You’ve just set up your react app to send the contents of a contact form to your email address using the power of React, Express, Nodemailer and Postfix. This might have seemed like a lot of work but at least it was free right? 😀

Adding security to your server

Set up a proxy to reroute port 3001 to root domain

We want to reroute the express server api endpoint from domain:3001/api/sendMail to domain/api/sendMail, this means we don’t have to expose port 3001 to the world. I’m using HTTPS as my main setting for my domain and I use certbot to achieve this so I’m going to be editing my website-le-ssl.conf file in /etc/apache2/sites-available, if you’re using http it will just be the usual website.conf file, but SSL is one of our layers of security here so I advise against using HTTP.

Apache HTTP Server’s mod_proxy module provides basic proxying capabilities that can be used to forward incoming requests from your main server to other servers running on your system. This is useful when you have multiple applications running on different ports, but you want to access all of them through a single domain and port.

In your case, you have an Express server running on localhost:3001. But you don’t want users to have to specify the port number when they make requests to your application. Instead, you want them to be able to use a standard HTTPS request on port 443.

This is where the ProxyPass and ProxyPassReverse directives come in, add the following to your respective website*.conf file.

ProxyPreserveHost On
ProxyPass /api/ http://localhost:3001/api/
ProxyPassReverse /api/ http://localhost:3001/api/
  • ProxyPass tells Apache to forward incoming requests to the specified address. So /api/ http://localhost:3001/api/ means that when Apache receives a request at https://yourdomain.com/api, it should forward it to http://localhost:3001/api.
  • ProxyPassReverse is used to modify the Location, Content-Location and URI headers in the HTTP responses coming from your Express server. This ensures that the client’s browser doesn’t get confused by the proxying process and works as expected.

The ProxyPreserveHost On directive tells Apache to preserve the original Host header in the incoming HTTP request when it forwards that request to your Express server. This can be useful if your Express server needs to know the original domain of the request for logging or other purposes.

Your configuration ensures that all the /api routes are proxied to the Express server running on localhost:3001, providing a seamless experience for the client.

It’s worth noting that the above configuration is for HTTPS (SSL/TLS). If you’re using HTTP, you would add the same lines to the non-SSL configuration file (typically site.conf or 000-default.conf).

However, remember that using HTTP is not recommended for production environments due to security concerns. Unencrypted HTTP traffic can be intercepted and read, so it’s best practice to always use HTTPS, especially when dealing with sensitive data like email.

Once you’ve completed these steps, you can restart the Apache2 service using the following command:

sudo service apache2 restart
Add a Google reCAPTCHA

To use Google’s reCAPTCHA, follow these steps:

  1. Visit the reCAPTCHA website.
  2. Click on the ‘Admin Console’ button in the upper-right corner.
  3. Sign in with your Google account.
  4. Click on the ‘+’ button to register a new site.
  5. Fill out the form:
    • Label: Give it a name that will help you identify it.
    • reCAPTCHA type: Choose the type you want (likely “reCAPTCHA v3”).
    • Domains: Enter the domain of your website.
  6. Accept the reCAPTCHA Terms of Service.
  7. Click on ‘Submit’.

After submitting, you will be given a Site Key and a Secret Key. The Site Key is used in your React app, and the Secret Key is used on your server to verify the user’s response. These should be stored in your .env file.

The code for sendMail.js will stay the same but there are changes to the contact form and the server.js. See them below!

New Contact Form containing captcha:

import React, { useState, useRef, useEffect } from "react";
import "./ContactPage.css";
import { Navbar, Footer } from "../../components";
import ReCAPTCHA from "react-google-recaptcha";

const ContactPage = () => {
  const [state, setState] = useState({
    name: "",
    from: "",
    subject: `Contact Form Submission`,
    message: "",
    formSubmitted: false,
    errorMessage: "",
    captchaValue: null,
    website: null,
  });

  const recaptchaRef = useRef();

  const handleChange = (event) => {
    setState((prevState) => ({
      ...prevState,
      [event.target.name]: event.target.value,
    }));
  };

  const handleSubmit = async (event) => {
    event.preventDefault();

    recaptchaRef.current.execute();
    
    if (!state.captchaValue) {
      setState((prevState) => ({
        ...prevState,
        errorMessage: "CAPTCHA was not completed",
      }));
      console.log("CAPTCHA error");
      return;
    }

    const { name, from, subject, message, captchaValue, website } = state;
    try {
      const response = await fetch("https://example.com/api/sendmail", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          name,
          from,
          subject,
          message,
          captcha: captchaValue,
          website: website,
        }),
      });
      if (response.status === 200) {
        console.log("Email sent!. \nResponse:", response);
        setState((prevState) => ({ ...prevState, formSubmitted: true }));
      } else {
        setState((prevState) => ({
          ...prevState,
          errorMessage:
            "There was an error sending your message. Please try again later.",
        }));
        console.log("Email not sent. \nResponse:", response);
      }

    } catch (error) {
      console.error("Error sending email:", error);
      setState((prevState) => ({
        ...prevState,
        errorMessage:
          "There was an error sending your message. Please try again later.",
      }));
      console.log("Email not sent. \nError:", error);
    }

    recaptchaRef.current.reset();
  };

  const handleCaptchaChange = (value) => {
    setState((prevState) => ({ ...prevState, captchaValue: value }));
  };

  useEffect(() => {
    recaptchaRef.current.execute();
  }, []);

  const { name, from, message, formSubmitted, errorMessage } = state;

  return (
    <div className="contact-page">
      <form className="contact-form" onSubmit={handleSubmit}>
        <input
          type="text"
          name="name"
          value={name}
          onChange={handleChange}
          required
        />
        <input
          type="email"
          name="from"
          value={from}
          onChange={handleChange}
          required
        />
        <textarea
          name="message"
          value={message}
          onChange={handleChange}
          required
        />
        <button type="submit">Submit</button>
      </form>
    </div>
  );
};

export default ContactPage;

New Server.js containing captcha:

require('dotenv').config({ path: __dirname+'/.env', override: true });
const express = require('express');
const bodyParser = require('body-parser');
const sendMail = require('./src/sendMail.js');
const axios = require('axios');
const app = express();
const cors = require('cors');
const port = process.env.PORT || 3001;

app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(cors());

app.post('/api/sendmail', async (req, res) => {
  const { name, from, subject, message, captcha, website } = req.body;
  
  if (website) {
    console.log("Spam detected, website field was filled in.", req.body);
    return res.status(400).send("Spam detected.");
  }

  try {
    const verifyURL = `https://www.google.com/recaptcha/api/siteverify?secret=${process.env.RECAPTCHA_SECRET_KEY}&response=${captcha}`;
    const verifyResponse = await axios.post(verifyURL);
    const verifyBody = verifyResponse.data;

    if (verifyBody.success !== true) {
      console.log("Failed CAPTCHA verification");
      return res.status(401).send({ message: 'Failed CAPTCHA verification' });
    }

    await sendMail(name, "[email protected]", from, subject, message);
    res.status(200).send({ message: 'Email sent successfully' });
  } catch (error) {
    console.error('Error sending email:', error);
    res.status(500).send({ message: 'Error sending email' });
  }
});

app.listen(port, () => {
  console.log(`Server is running on port ${port}`);
});
.env File and Environment Variables

A .env file is a simple text file where you can declare environment variables. To use them, you will need to install the dotenv package using npm, then include require('dotenv').config() at the top of your Node.js file.

An example of a .env file might look like this:

PORT=3001
REACT_APP_RECAPTCHA_SITE_KEY=your-site-key
RECAPTCHA_SECRET_KEY=your-secret-key
EMAIL_USER=your-email-user
EMAIL_PASS=your-email-password

These environment variables can then be accessed in your Node.js application using process.env, like process.env.PORT.

Please replace your-site-key, your-secret-key, your-email-user, and your-email-password with your actual key, user, and password.

Written by Joel Gray

19/05/2023

Checkout some of our other popular blog posts