Local Email Testing: Mailcatcher, Nodemailer & Docker
Email integration is crucial for modern applications, whether it's for user authentication, notifications, or marketing purposes. During development and testing, relying on external email providers can be cumbersome and unreliable. Setting up a local email testing environment allows developers to simulate email sending and receiving without the risks associated with real email services. This article guides you through creating a local email testing service using Mailcatcher, Nodemailer, and Docker.
Understanding the Need for Local Email Testing
When building applications, especially those involving user accounts and interactions, email functionality is often a core component. Think about user registration, password resets, and sending notifications. Traditionally, developers might use real email services during testing. However, this approach has several drawbacks. First, it can lead to accidental spamming of real users if testing is not carefully managed. Second, relying on external services introduces dependencies that can be unreliable due to network issues or service outages. Third, it can be challenging to automate email verification processes in automated tests. Local email testing provides a controlled environment where you can send and inspect emails without affecting real users or relying on external services. By using tools like Mailcatcher, developers can capture emails sent by their application and view them through a web interface. This allows for thorough testing of email content, headers, and formatting, ensuring that emails are delivered correctly when the application is deployed to a live environment. Furthermore, integrating a local email service into automated testing frameworks allows for programmatic verification of email content, making the testing process more robust and efficient. Setting up a local email testing environment is, therefore, a best practice for modern software development, enabling developers to build reliable and user-friendly applications with confidence.
Setting Up Mailcatcher with Docker
To start, Mailcatcher is a gem that intercepts outgoing emails and displays them in a web interface. Using Docker simplifies the setup, ensuring a consistent environment across different machines. Let's dive into how you can get Mailcatcher running inside a Docker container. First, ensure you have Docker installed on your system. If not, head over to the official Docker website and follow the installation instructions for your operating system. Once Docker is up and running, create a docker-compose.yml file in your project directory. This file will define the configuration for your Mailcatcher container. Add the following content to your docker-compose.yml file:
version: '3.1'
services:
  mailcatcher:
    image: schickling/mailcatcher
    ports:
      - "1080:1080" # Web interface
      - "1025:1025" # SMTP port
This configuration tells Docker to use the schickling/mailcatcher image, which is a popular and well-maintained Mailcatcher image. It also maps port 1080 on your host machine to port 1080 in the container, which is where Mailcatcher's web interface is accessible. Additionally, it maps port 1025, the standard SMTP port, allowing your application to send emails to Mailcatcher. With the docker-compose.yml file in place, open your terminal, navigate to the directory containing the file, and run the following command:
docker-compose up -d
This command tells Docker Compose to start the Mailcatcher container in detached mode, meaning it will run in the background. After the container starts, you can access Mailcatcher's web interface by opening your web browser and navigating to http://localhost:1080. You should see Mailcatcher's inbox, which will be empty initially. Now that Mailcatcher is running, you need to configure your application to send emails to it. This typically involves setting the SMTP host and port in your application's email configuration. For example, if you're using a Node.js application with Nodemailer, you would set the SMTP host to localhost and the port to 1025. We'll cover Nodemailer integration in the next section.
Implementing Email Sending with Nodemailer
Nodemailer is a popular Node.js library for sending emails. Integrating it with Mailcatcher is straightforward. First, install Nodemailer in your project:
npm install nodemailer
Next, configure Nodemailer to use Mailcatcher's SMTP server. Here's an example:
const nodemailer = require('nodemailer');
// Create a transporter object using the default SMTP transport
let transporter = nodemailer.createTransport({
  host: 'localhost',
  port: 1025,
  secure: false, // Use `true` for port 465, `false` for all other ports
  ignoreTLS: true, // Ignore TLS requirement for localhost
});
// Function to send email
async function sendEmail(to, subject, text) {
  try {
    // Send mail with defined transport object
    let info = await transporter.sendMail({
      from: 'sender@example.com', // Sender address
      to: to, // List of receivers
      subject: subject, // Subject line
      text: text, // Plain text body
    });
    console.log('Message sent: %s', info.messageId);
    // Message sent: <b658f8ca-6296-ccf4-8306-87d57a0b4321@example.com>
  } catch (error) {
    console.error('Error sending email:', error);
  }
}
// Example usage
sendEmail('recipient@example.com', 'Hello from Nodemailer', 'This is a test email sent via Nodemailer and Mailcatcher.');
In this example, we create a Nodemailer transporter configured to connect to localhost on port 1025, which is where Mailcatcher is listening for SMTP connections. The secure: false and ignoreTLS: true options are important because Mailcatcher doesn't use TLS encryption by default. Now, when you run this code, Nodemailer will send the email to Mailcatcher instead of a real email server. You can then open Mailcatcher's web interface at http://localhost:1080 to view the email. This setup allows you to test email sending functionality without the risk of sending emails to real users. You can verify that the email is sent correctly, check the content, headers, and attachments, and ensure that everything is working as expected. Integrating Nodemailer with Mailcatcher provides a convenient and reliable way to test email functionality in your Node.js applications.
Integrating the Email Service with the Backend
Integrating the local email service into your backend involves configuring your application to use the Mailcatcher SMTP server. This usually means setting the appropriate environment variables or configuration options. The specific steps depend on your backend framework or language. For instance, in a Node.js application using a framework like Express, you would configure Nodemailer as shown in the previous section, ensuring that the SMTP host and port are set to Mailcatcher's address. In a Python application using Flask or Django, you would configure the email settings in your application's configuration file. Here's an example of how you might configure email settings in a Django project:
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = 'localhost'
EMAIL_PORT = 1025
EMAIL_HOST_USER = ''
EMAIL_HOST_PASSWORD = ''
EMAIL_USE_TLS = False
In this configuration, EMAIL_BACKEND is set to use Django's SMTP email backend, EMAIL_HOST is set to localhost, and EMAIL_PORT is set to 1025, which is where Mailcatcher is listening. EMAIL_USE_TLS is set to False because Mailcatcher doesn't use TLS encryption by default. Once you've configured your backend to use Mailcatcher, you can test the email sending functionality by triggering events that send emails, such as user registration or password reset requests. You can then open Mailcatcher's web interface to verify that the emails are being sent correctly. This integration allows you to test email functionality as part of your backend development process, ensuring that emails are sent correctly and that your application behaves as expected.
Creating a Function to Clean and Capture the Last Email
To facilitate automated testing, you need a way to programmatically access and clear emails from Mailcatcher. While Mailcatcher doesn't provide a built-in API for this, you can use its web interface to retrieve and delete emails. Here's an example of how you can create a function to capture the last email sent to Mailcatcher using Node.js and the node-fetch library:
const fetch = require('node-fetch');
async function getLastEmail() {
  try {
    // Fetch the list of emails from Mailcatcher API
    const emailsResponse = await fetch('http://localhost:1080/messages');
    const emails = await emailsResponse.json();
    // Check if there are any emails
    if (emails.length === 0) {
      return null;
    }
    // Get the ID of the last email
    const lastEmailId = emails[emails.length - 1].id;
    // Fetch the content of the last email
    const emailResponse = await fetch(`http://localhost:1080/messages/${lastEmailId}.json`);
    const email = await emailResponse.json();
    return email;
  } catch (error) {
    console.error('Error fetching last email:', error);
    return null;
  }
}
async function clearEmails() {
  try {
    // Delete all emails from Mailcatcher
    await fetch('http://localhost:1080/messages', {
      method: 'DELETE',
    });
    console.log('Emails cleared from Mailcatcher');
  } catch (error) {
    console.error('Error clearing emails:', error);
  }
}
// Example usage
async function test() {
  const lastEmail = await getLastEmail();
  if (lastEmail) {
    console.log('Last email subject:', lastEmail.subject);
    console.log('Last email body:', lastEmail.text);
  } else {
    console.log('No emails found in Mailcatcher');
  }
  await clearEmails();
}
test();
This code defines two functions: getLastEmail and clearEmails. The getLastEmail function fetches the list of emails from Mailcatcher's API, retrieves the ID of the last email, and fetches the content of that email. The clearEmails function deletes all emails from Mailcatcher. These functions can be used in automated tests to verify that emails are being sent correctly and to clear the inbox after each test run. Integrating these functions into your testing framework allows you to automate the email verification process, making your tests more robust and efficient.
Ensuring Orchestrator Can Capture the Last Email and Clear the Inbox
To ensure that your orchestrator can capture the last email and clear the inbox after each test run, you need to integrate the functions we created in the previous section into your testing framework. The specific steps depend on your testing framework and orchestrator. For example, if you're using Jest as your testing framework and a CI/CD tool like Jenkins as your orchestrator, you would integrate the getLastEmail and clearEmails functions into your Jest test cases. Here's an example of how you might do this:
const { getLastEmail, clearEmails } = require('./mailcatcher-utils');
describe('Email tests', () => {
  beforeEach(async () => {
    await clearEmails(); // Clear emails before each test
  });
  it('should send a welcome email on user registration', async () => {
    // Perform user registration action
    await registerUser('test@example.com', 'password');
    // Get the last email sent to Mailcatcher
    const lastEmail = await getLastEmail();
    // Assert that the email was sent and contains the expected content
    expect(lastEmail).not.toBeNull();
    expect(lastEmail.subject).toBe('Welcome to our platform!');
    expect(lastEmail.text).toContain('Thank you for registering');
  });
  afterAll(async () => {
    await clearEmails(); // Clear emails after all tests
  });
});
In this example, we use the beforeEach hook to clear emails from Mailcatcher before each test case. We then perform the action that sends an email, such as user registration, and use the getLastEmail function to retrieve the last email sent to Mailcatcher. Finally, we use Jest's expect function to assert that the email was sent and contains the expected content. We also use the afterAll hook to clear emails from Mailcatcher after all tests have completed. By integrating these functions into your testing framework, you can ensure that your orchestrator can capture the last email and clear the inbox after each test run, making your email testing process more reliable and efficient. This approach allows you to automate the verification of email content, headers, and attachments, ensuring that your application's email functionality is working as expected.
By following these steps, you can create a robust local email testing environment that simplifies development and testing, ensuring your application's email functionality works flawlessly.