Guide To Stress Free Email Testing with Next.js

Developing an application that sends emails is straightforward but not without its risks. Ensuring deliverability but not actually having any of those emails land inside real inboxes is a top concern for any developer. Which leads to the question “How do you test your application’s outbound email capabilities?” 

Enter email capture services. While the term “email capture service” tends to focus on the marketing aspects (capturing information from your calls to action, ensuring emails don’t get caught in spam, etc) they also include SMTP deliverability. Mailsac offers an email capture service that addresses the deliverability aspect, specifically not delivering any email to its intended recipient. Effectively a “black hole” where no email should escape to the outside world.

In this post, we’ll walk through a sample application in Next.js that will generate emails and have those emails captured by Mailsac’s email sandboxing service.

Do I Really Need To Do Email Testing?

Some frameworks do come with email previewing capabilities like Rails’ ActionMailer. Said frameworks don’t actually attempt to send anything but instead preview the email on your machine. We recommend real testing during the development and quality assurance phase by using an external SMTP server to mimic the application’s behavior in production.

Testing that capability has to be done safely unless you want to land on Twitter’s trending page for accidentally sending customers an integration test email.

Test Email Sending With A Next.js Application

For the rest of this guide, we’ll focus on wiring up a simple application that will allow users to send an email when a button is pushed from a UI. We’ll then demonstrate the capture of those emails in our development environment.

The components we’ll use are:

Application Creation

While the focus of this guide isn’t a line-by-line walkthrough of the sample code, we’ll focus on the key aspects of the application that mainly involve emailing capabilities.

The application source can be found in our git repository.

1. Application setup

Let’s start by creating a quick next app with tailwind support:

mailsac % npx create-next-app
…
Success! Created nodejs-send-email at /Users/mailsac/code/nodejs-send-email

cd nodejs-send-email
npm install -D tailwindcss postcss autoprefixer @tailwindcss/forms
npm install @headlessui/[email protected] @heroicons/react
npx tailwindcss init -p

The above is the recommended way to install tailwind on Next.js according to their guide.

Configure tailwind.config.js by adding the highlighted lines:

module.exports = {
  content: [
	"./pages/**/*.{js,ts,jsx,tsx}",
	"./components/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
	extend: {},
  },
  plugins: [
    require('@tailwindcss/forms'),
  ],
}

and then add tailwind itself to the global CSS file inside styles/global.css and comment out some some default CSS created by npx:

/* @media (prefers-color-scheme: dark) {
  html {
    color-scheme: dark;
  }
  body {
    color: white;
    background: black;
  }
}
*/

@tailwind base;
@tailwind components;
@tailwind utilities;

Tailwind is strictly optional but recommended for easy styling of the frontend.

2. Add the front page UI

Feel free to add your custom frontend code or use the index.js and components/notifications.js react component samples inside our repo.

index.js

import { useEffect, useState } from "react";
import Notification from "../components/notifications";

export default function Index() {
  const [sentEmail, setSentEmail] = useState(false);
  const [emailTo, setEmailTo] = useState("");
  const [emailBody,setEmailBody] = useState("");
  const [resultMessage, setResultMessage] = useState("");

  const sendEmail = () => {
    setSentEmail(true);
  }

  useEffect( () => {
    fetch('/api/send-email',{
      method: 'POST',
      body: JSON.stringify({ to: emailTo, body: emailBody})
    })
    .then( res => res.json())
    .then(response => {
      setResultMessage(response.message)
    })
    .catch(error => console.log(error));    

  },[sentEmail]);

  const SentEmailBanner = sentEmail === true? <Notification message={resultMessage} /> : null;

The second line brings in a component that takes in a message and formats it as a pop-up notification. The useEffect() method sends your email recipient and body input to the backend, which will forward that data to Mailsac’s servers.

3. Sign up for Mailsac’s Email Capture

Mailsac has a free email capture service. All you need is to sign up for an account and generate a key:

Mailsac Dashboard

4. Add a backend mail handler route

Once you’ve generated and saved your keys, you can place them in a .env file:

.env

MAILSAC_USERNAME=lcanal
MAILSAC_API_KEY=Key generated from above

Following our email capture documentation, we’ll create a backend API route for Next to handle the request:

pages/api/send-email.js

const nodemailer = require("nodemailer");

export default async function handler(req, res) {
  let emailEnvelope = JSON.parse(req.body)
  if (
        req.method === 'POST' 
        && typeof(emailEnvelope.to) !== 'undefined' 
        && emailEnvelope.to !== ''
  ){
    const mailsaUserName = process.env.MAILSAC_USERNAME
    const mailsacAPIKey  = process.env.MAILSAC_API_KEY
  
    const transporter = nodemailer.createTransport({
      host: 'capture.mailsac.com',
      port: 5587,
      // will use TLS by upgrading later in the connection with STARTTLS
      secure: false,
      auth: {
        user: mailsaUserName,
        pass: mailsacAPIKey
      }
    })
  
    try {
      const results = await transporter.sendMail({
        from: '"Sample App" [email protected]',
        to: emailEnvelope.to,
        subject: 'Sample App Send',
        text: emailEnvelope.body
      })
      res.status(200).json(
        { 
          message: "You should now see an email in Mailsac's capture service", 
          response: results.data 
        }
      )
    } catch (error){
      console.log(`ERROR ${error}`)
      res.status(500).json({ message: `${error.response}`, response: error })
    }
  } else {
    return res.status(200).json({message: "No data"});
  }
}

In the highlighted line, we’re ensuring the useEffect() hook gets called with input data before we allow the rest of the function to continue. useEffect() gets called a variety of times in the component lifecycle, and this check is to ensure it was initiated by an end user and not as part of the component mounting.

5. Test driving the app

Fire up the application via

npm run dev

Navigate to http://localhost:3000 and type a text message:

Then navigate over to mailsac.com to view the message

6. Capturing other email domains

While that works well as a contrived example, the real value comes when using any arbitrary email in the recipient field:

Capturing emails outside the mailsac.com domain is extremely valuable when switching between different environments. For example, in the demo application example above, the .env environment file could instead look like

MAILSAC_USERNAME=$MAILSAC_NAME
MAILSAC_API_KEY=$MAILSAC_KEY
MAILSAC_HOST=capture.mailsac.com
MAILSAC_PORT=5587

With the updated send-email.js

const nodemailer = require("nodemailer");

export default async function handler(req, res) {
  let emailEnvelope = JSON.parse(req.body)
  if (
        req.method === 'POST' 
        && typeof(emailEnvelope.to) !== 'undefined' 
        && emailEnvelope.to !== ''
  ){
    const mailsaUserName = process.env.MAILSAC_USERNAME
    const mailsacAPIKey  = process.env.MAILSAC_API_KEY
  
    const transporter = nodemailer.createTransport({
      host: process.env.MAILSAC_HOST,
      port: process.env.MAILSAC_PORT,
      // will use TLS by upgrading later in the connection with STARTTLS
      secure: false,
      auth: {
        user: mailsaUserName,
        pass: mailsacAPIKey
      }
    })
  
    try {
      const results = await transporter.sendMail({
        from: '"Sample App" [email protected]',
        to: emailEnvelope.to,
        subject: 'Sample App Send',
        text: emailEnvelope.body
      })
      res.status(200).json(
        { 
          message: "You should now see an email in Mailsac's capture service", 
          response: results.data 
        }
      )
    } catch (error){
      res.status(500).json({ message: `${error.response}`, response: error })
    }
  } else {
    return res.status(200).json({message: "No data"});
  }
}

The above edits would allow you to deploy to a testing or production environment and the only changes required would be in the .env file. Specifically, the SMTP host and authentication settings.

Conclusion

The above guide just scratches the surface of what you can do with our email services. We provide a unified inbox that allows testers to view their bulk email testing in one unified view and custom domains for those who do not have their own domains with zero setup configurations.

Removal of Google Analytics Tracking from all Mailsac websites, effective immediately

Google Analytics (GA), the ubiquitous web analytics service provided by Google, has been removed from all Mailsac properties. GA is used to see which pages people visit, how frequently users return to a website, and where they were referred from.

Despite Google Analytics being the de facto tracking service on the internet – mostly because it is entirely free – we decided it was not a good fit for our users.

Frequently, disposable email services are used as a way to avoid spam and tracking. But the free GA product by Google is actually an information gathering honeypot, extremely lucrative for them. By some estimates, GA is on 65% of the top 1 million most popular sites on the internet.

It is worth noting that Mailsac does not use Facebook analytics, nor any other 3rd party trackers. We do leverage the privacy-focused – and paid – Cloudflare Analytics, which is included with our DNS and caching service. We also track minimal usage metrics on the server side of Mailsac.com, for billing purposes.

Mailsac was created over 10 years ago with the goal of providing a reliable disposable email platform for software testers. We serve no advertisements, sell no data to 3rd parties. We rely on users to pay us for providing a good service. From the time of Mailsac’s creation – until recently – we did leverage Google Analytics as a tool to understand website load. As a “free” product, GA helped keep costs down and worked well. We have come to understand that “free” meant we effectively shared our users browsing habits with Google, for the purpose of serving ads. So we no longer do that.

Screenshot of mailsac.com on 2022-04-28 in Safari showing zero trackers

Deprecation of Outbound SMTP Service

Outbound SMTP service will no longer be supported by Mailsac.

What Does this Mean for Me?

Mail will no longer be able to sent from Mailsac addresses or custom domains using the outbound Mailsac SMTP service.

If you are sending from the REST API, compose email form, or Unified Inbox we encourage you to seek out other SMTP sending services. Mailsac has always supported direct SMTP from anywhere, without a mail relay, as well.

Reasons for the Change

Email delivery is not an easy problem at scale.

Our customers, especially quality assurance teams, are using Mailsac as a receive-only service.

The likelihood that Mailsac’s outbound messages are delivered to the intended inbox has been trending lower over time, despite increased effort.

We made the decision to focus our efforts on improving our core product – disposable inbound email for testing.

Recommended Providers of Outbound SMTP Service


Maintenance Notice: Database Upgrade

Date and Time

Friday May 6th from 13:00 through 14:30 UTC

Expected Downtime

There may be intermittent service interruption while database failover happens (60-120 seconds)

Reason for the Change

Mailsac is upgrading to a larger database cluster to ensure service availability during peak load.

Customer Changes

No customer changes are required.

Self-Hosted Mailsac UI

This tutorial references code published at https://github.com/mailsac/mailsac-self-hosted-ui

The Mailsac Self-Hosted Temporary Email User Interface is available in a GitHub repository. This project provides a self-hosted user interface for viewing disposable email. It uses mailsac.com as the backend email service.

Mailsac.com Limitations

Mailsac already offers disposable email without a need to sign up for an account. What need does this application meet that Mailsac doesn’t already provide?

Mailsac has limitations on what can be viewed without signing up for an account. Only the latest email in a public mailbox can be viewed without signing in. Mail in a private domain cannot be viewed without signing in with an account that has permissions to the private domain.

Use Cases

There are two use cases that customer’s have brought to our attention that Mailsac’s service doesn’t satisfy. Both stem from a requirement to allow users read-only access to an inbox without the requirement of creating a Mailsac account.

Class Room Use Case

An instructor may want students, who are young in age and don’t have an email address, to sign up for an account with a web service used in the class. The Mailsac Self-Hosted Temporary Email User Interface application provides a simplified interface for students to view email sent to a private mailsac hosted domain without the need to sign up for a mailsac account or email address.

Acceptance Tester Use Case

As part of the sofware development lifecycle there is a need to have software tested by users. Temporary email has long been beneficial to testing. The Mailsac Self-Hosted Temporary Email User Interface makes this easier. Users can test applications using email addresses in a Mailsac hosted private domain without the need to sign up for a Mailsac account. Furthermore, because the application is self-hosted companies can use a reverse proxy to enforce IP allow lists or put the application behind basic authentication.

Running the Mailsac Self-Hosted Email User Interface

Local

With NodeJS installed this application can be run with the following commands.

npm install && npm run build
MAILSAC_KEY=YOUR_MAILSAC_API_KEY npm run start

You will need to generate a Mailsac API key. To generate or manage API Keys use the API Keys page.

The application is now running and can be accessed via a web browser at http://localhost:3000 .

Any public or private Mailsac hosted address the API key has access to can be viewed by entering the email address in the text box and selecting “view mail”.

Screenshot of Application with no domain defined

Domain Option

You can prepopulate the domain by using the NEXT_PUBLIC_MAILSAC_CUSTOM_DOMAIN environment variable.

NEXT_PUBLIC_MAILSAC_CUSTOM_DOMAIN=example.mailsac.com npm run build
MAILSAC_KEY=YOUR_MAILSAC_API_KEY npm run start
Screenshot of pre-populated domain

Vercel Hosted

Vercel is a platform as a service provider. Their service makes running your own Next.js application easy.

The Vercel Getting Started guide is easy to follow.

  1. Fork this repo.
  2. Sign up for a Vercel account
  3. Grant Vercel permissions to read all your repos or choose to grant permission on the forked repo
  4. Import forked repository into Vercel
Screenshot showing import of forked repo
  1. Configure MAILSAC_KEY environment variable
Screenshot of environment variables

  1. Deploy application
Screenshot showing deployment success

After a successful deployment you can click on the image of the application to be taken to the live application.

NOTE There is currently no authentication on this application. Anyone with the URL will be able to view emails and domains associated with the Mailsac API key that was used. Operations will be tracked in the Mailsac account in which the API key is associated with.

You are free to deploy this app however you like. Please keep the attribution to Mailsac.

Maintenance Notice: DNS Provider Change

Date and Time

Saturday April 2nd from 13:30 through 17:30 UTC

Expected Downtime

No downtime is expected.

Reasons for the Change

Mailsac is changing DNS providers to Cloudflare to provide a more resilient SaaS offering.

Customer Changes

No customer changes are required. If you implemented IP based ACLs at the VLAN or border firewall, it is possible these rules may need to be updated. Cloudflare publishes a list of their IP addresses.

Updates

Saturday April 2nd 14:33 UTC
DNS has been switched over to use Cloudflare. All validation tests have been completed. We will continue to monitor for issues.

Email Integration Tests Using Java

Mailsac provides a REST API to fetch and read email. The REST API also allows you to reserve an email address that can forward messages to another mailsac email address, Slack, WebSocket, or webhook

This article describes how to integrate with Mailsac using Java and the JUnit testing framework. The JavaMail API will be used to send email via SMTP.

What is JUnit?

JUnit is a unit testing framework for the Java programming language. The latest version of the framework, JUnit 5, requires Java 8 or above. It supports testing using a command-line interface, build automation tools, and IDEs.

JUnit can be used to test individual components of code to ensure that each unit is performing as intended.

Setting Up the Environment

Depending on the environment, there are multiple ways to run tests. Testing using the command and JUnit are included in this example.

Testing Using Command-Line

Running tests from the command-line requires the ConsoleLauncher application(junit-platform-console-standalone-1.7.2.jar). JUnit ConsoleLauncher is published in the Maven Central repository under the junit-platform-console-standalone directory.

  1. Navigate to the Maven Central directory
  2. Download junit-platform-console-standalone-1.7.2.jar.
  3. Create a directory for the project: mkdir mailsac-tests.
  4. Move the jar file you downloaded into the directory mailsac-tests.
  5. Create a directory inside mailsac-testsmkdir test.

    Note: mailsac-tests/test will contain your source code.

JUnit Testing Introduction

This code example shows basic usage of the JUnit testing framework.

Inside the directory mailsac-tests/test, create a java file: touch TestClass.java.

Add the following code snippet to ./mailsac-tests/test/TestClass.java

import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

@DisplayName("tests truth")
class TestClass {

    @Test
    @DisplayName("true equals true")
    void trueEqualsTrue() {
        // The assertTrue method asserts that the supplied condition is true.
        // static void assertTrue(condition)
        assertTrue(true);
    }

    @Test
    @DisplayName("false equals false")
    void falseEqualsFalse() {
        // The assertEquals method asserts that expected and actual are equal.
        // static void assertEquals(expected, actual)
        assertEquals(false, false);
    }

}

@Test Denotes that a method is a test.

@DisplayName Declares a custom display name for the test class or test method.

Refer to JUnit annotations and JUnit Assertions for further reading.

Running JUnit Tests From The Command-Line

  1. Inside the directory mailsac-tests, compile the test:javac -verbose -cp junit-platform-console-standalone-1.7.2.jar -d test test/TestClass.java.
  2. Then run:java -jar junit-platform-console-standalone-1.7.2.jar --class-path test --scan-class-path.

The output should appear similar to this:

╷
├─ JUnit Jupiter ✔
│  └─ tests truth ✔
│     ├─ false equals false ✔
│     └─ true equals true ✔
└─ JUnit Vintage ✔

Test run finished after 92 ms
[         3 containers found      ]
[         0 containers skipped    ]
[         3 containers started    ]
[         0 containers aborted    ]
[         3 containers successful ]
[         0 containers failed     ]
[         2 tests found           ]
[         0 tests skipped         ]
[         2 tests started         ]
[         0 tests aborted         ]
[         2 tests successful      ]
[         0 tests failed          ]

The first section of output shows the name of the unit test (tests truth) and the test names (true equals true and false equals false). The checkmark next to the test name indicates it was successful.

The second section of output shows a summary of the test results.

Testing Using Build Tools

Testing from build automation tools, like Maven, is another option. In many ways, using build tools is the best option. For instance, they provide a standard directory layout that encourages industry standard naming conventions.

Maven abstracts many underlying mechanisms allowing developers to run a single command for validating, compiling, testing, packaging, verifying, installing, and deploying code.

This section will describe how to set up Maven for building, managing, and testing a project.

  1. Navigate to the Apache Maven download page and follow the installation instructions. If you have Homebrew you can install Maven using the command: brew install maven.
  2. After installing Maven, run on the command-line to initialize the directory mailsac-integration-test-java as a maven managed project:
mvn archetype:generate \
    -DgroupId=com.mailsac.api \
    -DartifactId=mailsac-integration-test-java \
    -DarchetypeArtifactId=maven-archetype-quickstart \
    -DarchetypeVersion=1.4 \
    -DinteractiveMode=false
  1. Navigate into the directory: cd mailsac-integration-test-java
  2. Update the <dependencies> and <build> sections of pom.xml with the following xml.
<!-- ... -->
<dependencies>
  <dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-api</artifactId>
    <version>5.7.2</version>
    <scope>test</scope>
  </dependency>
  <dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-engine</artifactId>
    <version>5.7.2</version>
    <scope>test</scope>
  </dependency>
</dependencies>
<!-- ... -->
<build>
  <pluginManagement>
    <!-- ... -->
    <plugins>
      <plugin>
        <artifactId>maven-surefire-plugin</artifactId>
        <version>2.22.2</version>
      </plugin>
      <plugin>
        <artifactId>maven-failsafe-plugin</artifactId>
        <version>2.22.2</version>
      </plugin>
    </plugins>
    <!-- ... -->
  </pluginManagement>
</build>
<!-- ... -->
  1. Edit the AppTest.java file: $EDITOR src/test/java/com/mailsac/api/AppTest.java
package com.mailsac.api;

import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;

class TestClass {

    @Test
    void trueEqualsTrue() {
        // The assertTrue method asserts that the supplied condition is true.
        // static void assertTrue(condition)
        assertTrue(true);
    }

    @Test
    void falseEqualsFalse() {
        // The assertEquals method asserts that expected and actual are equal.
        // static void assertEquals(expected, actual)
        assertEquals(false, false);
    }

}
  1. In the directory mailsac-integration-test-java, run mvn clean package. This command deletes the folder target , packages the project into a new target folder, and runs a unit test.
  2. Tests can be manually run using the command mvn test in the mailsac-integration-test-java directory.The output should appear similar to:
[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
[INFO] Running com.mailsac.api.TestClass
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.029 s - in com.mailsac.api.TestClass
[INFO] 
[INFO] Results:
[INFO] 
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0

Mailsac Java Integration Test

This section describes how to leverage Mailsac and JUnit to test mail delivery. Emails will be sent to Mailsac using SMTP and email delivery will be validated with JUnit.

There are 3 additional libraries that will be used:

Integration Test Example

  1. With Maven, add the following dependencies to pom.xml
    If you are not using Maven include the JAR files in the classpath:

https://mvnrepository.com/artifact/com.mashape.unirest/unirest-java/1.4.9

https://mvnrepository.com/artifact/com.sun.mail/javax.mail/1.6.2

https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind/2.12.5

<!-- ... -->
<dependencies>
  <!-- ... -->
  <dependency>
    <groupId>com.mashape.unirest</groupId>
    <artifactId>unirest-java</artifactId>
    <version>1.4.9</version>
  </dependency>
  <dependency>
    <groupId>com.sun.mail</groupId>
    <artifactId>javax.mail</artifactId>
    <version>1.6.2</version>
  </dependency>
  <dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.12.5</version>
  </dependency>
</dependencies>
<!-- ... -->
  1. Edit the AppTest.java file: $EDITOR src/test/java/com/mailsac/api/AppTest.java

    Import the required modules
package com.mailsac.api;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.mashape.unirest.http.HttpResponse;
import com.mashape.unirest.http.Unirest;
import com.mashape.unirest.http.exceptions.UnirestException;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.Session;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.Date;
import java.util.Properties;

import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
  1. Acquire a Mailsac API key and configure SMTP sending. Export these parameters as environment variables:
export MAILSAC_API_KEY=your_mailsac_key;
export [email protected]
export [email protected]
export SMTP_USERNAME=your_smtp_username
export SMTP_PASSWORD=your_smtp_password
export SMTP_HOST=smtp.example.com
public class AppTest {
    // MAILSAC_API_KEY environment variable. Generated by mailsac. See
    // https://mailsac.com/api-keys
    static String mailsacAPIKey = "";
    // MAILSAC_TO_ADDRESS environment variable. Who you're sending an email to.
    static String mailsacToAddress = "";
    // SMTP_FROM_ADDRESS environment variable. Necessary if you are sending
    // through out.mailsac.com (unlikely - you most likely will replace
    // sendMail() below.
    static String fromAddress = "";
    // SMTP_USERNAME environment variable. Required for authenticated SMTP sending
    static String smtpUserName = "";
    // SMTP_PASSWORD environment variable. Required for authenticated SMTP sending
    static String smtpPassword = "";
    // SMTP_HOST environment variable. Hostname of your SMTP server
    static String smtpHost = "";
    // SMTP_PORT environment variable. Port used for SMTP sending
    static int smtpPort = 587;

    @BeforeAll
    static void setup() throws Exception {
        mailsacAPIKey = System.getenv().get("MAILSAC_API_KEY");
        mailsacToAddress = System.getenv().get("MAILSAC_TO_ADDRESS");
        fromAddress = System.getenv().get("SMTP_FROM_ADDRESS");
        smtpUserName = System.getenv().get("SMTP_USERNAME");
        smtpPassword = System.getenv().get("SMTP_PASSWORD");
        smtpHost = System.getenv().get("SMTP_HOST");
        if (System.getenv().get("SMTP_PORT") != null) {
            Integer.parseInt(System.getenv().get("SMTP_PORT"));
        }
        if (mailsacAPIKey == null || mailsacToAddress == null || fromAddress == null) {
            throw new Exception("Missing environment variable setup!");
        }
        if (smtpUserName == null || smtpPassword == null || smtpHost == null) {
            throw new Exception("Missing SMTP environment variables");
        }
        System.out.println(mailsacAPIKey);
        System.out.println(mailsacToAddress);
        System.out.println(fromAddress);
    }
}
  1. Add a purgeInbox() method which makes a DELETE request to api/addresses/{email}/messages/(messageId}.

    This section of code should be added to the existing AppTest class.
public class AppTest {
  //...
 @BeforeEach
 @AfterEach
 // purgeInbox cleans up all messages in the inbox before and after running each
 // test,
 // so there is a clean state.
 void purgeInbox() throws UnirestException, JsonProcessingException {
     HttpResponse<String> response = Unirest
             .get(String.format("https://mailsac.com/api/addresses/%s/messages", mailsacToAddress))
             .header("Mailsac-Key", mailsacAPIKey)
             .asString();

     // Parse JSON
     ObjectMapper objectMapper = new ObjectMapper();
     Object[] messagesArray = objectMapper.readValue(response.getBody(), Object[].class);

     for (int i = 0; i < messagesArray.length; i++) {
         JsonNode m = objectMapper.convertValue(messagesArray[i], JsonNode.class);
         String id = m.get("_id").asText();
         System.out.printf("Purging inbox message %s\n", id);
         Unirest.delete(String.format("https://mailsac.com/api/addresses/%s/messages/%s", mailsacToAddress, id))
                 .header("Mailsac-Key", mailsacAPIKey)
                 .asString();
     }
 }
  //...
}
  1. Implement a sendMail() method which sends an email. This section will likely likely be different depending on your use case. For example, you may be sending emails via your web application or via an email campaign.
public class AppTest {
    //...
    static void sendMail(String subject, String textMessage, String htmlMessage)
            throws UnsupportedEncodingException, MessagingException {
        Session session = Session.getDefaultInstance(new Properties());
        javax.mail.Transport transport = session.getTransport("smtp");
        MimeMessage msg = new MimeMessage(session);

        // set message headers
        msg.addHeader("Content-type", "text/HTML; charset=UTF-8");
        msg.addHeader("format", "flowed");
        msg.addHeader("Content-Transfer-Encoding", "8bit");

        msg.setFrom(fromAddress);
        msg.setReplyTo(InternetAddress.parse(fromAddress));
        msg.setSubject(subject, "UTF-8");
        msg.setText(textMessage, "UTF-8");
        msg.setContent(htmlMessage, "text/html");

        msg.setSentDate(new Date());

        msg.setRecipients(Message.RecipientType.TO, mailsacToAddress);
        msg.saveChanges();
        System.out.println("Email message is ready to send");
        transport.connect(smtpHost, smtpPort, smtpUserName, smtpPassword);
        transport.sendMessage(msg, msg.getAllRecipients());

        System.out.println("Email sent successfully");
    }
    // ...
}
  1. Add test. Use a for loop to check if the message was received by scanning the recipient inbox periodically. If the recipient inbox is not empty, and a message was found, the test verifies the message content:

    This test uses the Mailsac API endpoint /api/addresses/{email}/messages which lists all messages in an inbox.
public class AppTest {
    //...
    @Test
    void checkEmailWithLink() throws MessagingException, UnirestException, IOException, InterruptedException {
        sendMail("Hello!", "Check out https://example.com", "Check out <a href='https://example.com'>My website</a>");
        // Check inbox for the message up to 10x, waiting 5 seconds between checks.
        found: {
            for (int i = 0; i < 10; i++) {
                // Send request to fetch a JSON array of email message objects from mailsac
                HttpResponse<String> response = Unirest
                        .get(String.format("https://mailsac.com/api/addresses/%s/messages", mailsacToAddress))
                        .header("Mailsac-Key", mailsacAPIKey)
                        .asString();

                // Parse JSON
                ObjectMapper objectMapper = new ObjectMapper();
                Object[] messagesArray = objectMapper.readValue(response.getBody(), Object[].class);

                System.out.printf("Fetched %d messages from Mailsac for address %s\n", messagesArray.length,
                        mailsacToAddress);
                eachMessage: {
                    for (int m = 0; m < messagesArray.length; m++) {
                        // Convert object into JSON to fetch a field
                        JsonNode thisMessage = objectMapper.convertValue(messagesArray[m], JsonNode.class);

                        // After a message is found, the JSON object is checked to see if the link was
                        // sent correctly
                        assertTrue(thisMessage.get("links").toString().contains("https://example.com"),
                                "Missing / Incorrect link in email");

                        System.out.printf("Message id %s contained the correct link\n",
                                thisMessage.get("_id").asText());

                        return; // end the tests
                    }
                }

                System.out.println("Message not found yet, waiting 5 secs");
                Thread.sleep(5000);
            }

            // Fail the test if we haven't reached assertTrue above
            fail("Never received expected message!");
        }
    }
    // ..
}
  1. At this point, the code is complete. Package the project: mvn clean package. This will also run a test.

    Subsequent changes to the source file do not require you to run mvn clean package again. Instead, run mvn test.

    The output should appear similar to this:
[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
[INFO] Running com.mailsac.api.AppTest
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 9.148 s s - in com.mailsac.api.AppTest
[INFO] 
[INFO] Results:
[INFO] 
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0

GitHub Repository

If you encounter any difficulties, git clone https://github.com/mailsac/mailsac-integration-test-java. Make edits as necessary, and run mvn package.

Alternatively, if your tests fail because of error codes when making requests to the Mailsac API, please refer to the API Specification for further reading.

Next Steps

The Mailsac API Specification has generated code examples in Java + Unirest for making requests. It also has code examples in other languages.

This example can be adjusted to get all private email addresses for an account and purge their inboxes if necessary.

Please visit our forums if you have any questions!

Plus-addressing is supported by all Mailsac inboxes

When you send to any inbox @mailsac.com, if a + plus symbol is included, we remove that symbol and everything after it.

jeff+12345asdf@mailsac.com

will be delivered to

[email protected]

Many email services including Gmail, iCloud and Fastmail support stripping the + plus symbol and everything after it in the local-part of the address (everything before the @ symbol).

Plus-addressing has long been a useful feature to segment user accounts across services. At Mailsac we offer a variety of disposable email and forwarding utilities that are designed for software QA engineers and developers. Things like forwarding all messages in a domain to a single address, or automatically routing email to webhooks or slack, are really easy – may not even require DNS setup.

Mailsac is not affected by Log4J CVEs

Tech news has recently been full of CVEs related to a popular JVM logging library named Log4J.

Mailsac services do rely on JVM languages, including Java. This extends through the entire stack, custom apps, self-hosted open source software, internal and external, infrastructure, proxies, and scripts.

There is one exception – an instance of the CI server Jenkins which is isolated behind a VPN, and is was never vulnerable according to troubleshooting steps from the Jenkins developers.

Mailsac and Security

The Mailsac Team is small yet mighty, with decades of experience taking security seriously. We follow best practices for infrastructure-as-code, patching, testing, network isolation, backups, restoration, and principle of least access access. Large enterprises including banks and government agencies trust Mailsac for disposable email testing. We provide exceptionally fast and predictable REST and Web Socket APIs with an excellent uptime record.

Mailsac has support for multiple users under the same account, so you can keep disposable email testing private within your company.

It’s free to test email immediately – no payment details required. You can send email to any address @mailsac.com and confirm delivery in seconds without even logging in. Start now at mailsac.com.

A new open source counting and throttling server: say hello to dracula

dracula is a high performance, low latency, low resource counting server with auto-expiration.

The Mailsac engineering team recently open sourced our internal throttling service, dracula, under an open source license. Check it out on Github. In the repo we prebuild server and CLI binaries for mac and linux, and provide a client library for Go.

Dracula has performed extremely well in AWS on ARM64 in production for us. It handles thousands of requests per second without noticeable CPU spikes, while maintaining low memory.

In this blog post we’re going to give an overview of why it was necessary, explain how it works, and describe dracula’s limitations.

Why we made it

For the past few years Mailsac tracked throttling in a PostgreSQL unlogged table. By using an unlogged table we didn’t have to worry about lots of disk writes, nor the safety provided by having the write-ahead-log. Throttling records are only kept for a few minutes. We figured if Postgres was rebooting, losing throttling records from the past minutes would be the least of our worries.

In the months leading up to replacing this unlogged table with dracula we began having performance bottlenecks. Mailsac is experiencing fast growth in the past few years. Heavy sustained inbound mail was resulting in big CPU time while Postgres vacuumed the throttling tables. The throttling table started eating too many CPU credits in AWS RDS – credits the we needed for more important stuff like processing emails.

We needed a better throttling solution. One that could independently protect inbound mail processing and REST API services. Postgres was also the primary data store for parsed emails. The Postgres-based solution was a multi-tiered approach to throttling – especially against bad actors – and helped our website and REST API snappy, even when receiving a lot of mail from questionable sources. The throttling layer also caches customer data so we can filter out the paying users from unknown users. Separating this layer from the primary data store would help them scale independently.

Can Redis do it?

So it was time to add a dedicated throttle cache. We reached for Redis, the beloved data structure server.

We were surprised to find our use case – counting quickly-expiring entries – is not something Redis does very well.

Redis can count items in a hash or list. Redis can return keys matching a pattern. Redis can expire keys. But it can’t expire list or hash item entries. And Redis can’t count the number of keys matching a pattern – it can only return those keys which you count yourself.

What we needed Redis to do was count items matching a pattern while also automatically expiring old entries. Since Redis couldn’t do this combination of things, we looked elsewhere.

Other utility services seemed too heavy and full-of-features for our needs. We could have stood up a separate Postgres instance, used MongoDB, Elasticache, or Prometheus. The team has experience running all these services. But the team is also aware that the more features and knobs a service has, the more context is needed to debug it – the more expertise to understand its nuances, the more risk you’ll actually use additional features, and the more risk you’ll be slow responding to issues under heavy load.

All we wanted to do was put some values in a store, have them expired automatically, and frequently count them. We’d need application level logic to do at least some of this, so we made a service for it – dracula. Please check it out and give it a try!

How it works under the hood

Dracula is a server where you can put entries, count the entries, and have the entries automatically expire.

The dracula packet layout is as follows. See protocol.go for the implementation.

Section DescriptionCommand characterPut, Count, Errorspacexxhashpre shared key + id + namespace + dataspaceClient Message IDspaceNamespacespaceEntry data
Size1 byte1 byte8 bytes1 byte4 bytesunsigned 32 bit integer (Little Endian)1 byte64 bytes1 byteremaining 1419 bytes
Examplebyte(‘P’), ‘C’, ‘E’byte(‘ ‘)0x1c330fb2d66be179byte(‘ ‘)6 or []byte{6, 0, 0, 0}byte(‘ ‘)“Default” or “anything” up to 64 bytesbyte(‘ ‘)192.169.0.1, or any string up to end of packet
500 byte dracula packet byte order

Here’s roughly how the dracula client-server model works:

  1. The client constructs a 1500 byte packet containing a client-message ID, the namespace, and the value they want to store in the namespace (to be counted later).
  2. A hash of the pre-shared secret + message ID + namespace + entry data is set inside the front part of the message.
  3. A handler is registered under the client message ID.
  4. The bytes are sent over UDP to the dracula server.
  5. Client is listening on a response port.
  6. If no response is received before the message times out, a timeout error is returned and the handler is destroyed. If the response comes after the timeout, it’s ignored.
  7. Server receives packet, decodes it and checks the hash which contains a pre-shared secret.
  8. Server performs the action. There are only two commands – either Put a namespace + entry key, or Count a namespace + entry key.
  9. Server responds to the client using the same command (Put or Count). The entry data is replaced with a 32 bit unsigned integer in the case of a Count command. The hash is computed similarly to before.
  10. Client receives the packed, decodes it and confirms the response hash.

Data structures

Dracula uses a few data structures for storing data.

Namespaces are stored in a hashmap provided by github.com/emirpasic/gods, and we use a simple mutex to sync multithreaded access. Entries in each namespace are stored in wrapped AVL tree from the same repo, which we added garbage collection and thread safety. Each node of the AVL tree has an array of sorted dates.

Here’s another view:

  • dracula server
    • Namespaces (hashmap)
      • Entries (avltree)
        • sorted dates (go slice / dynamic array of int64)

Server configuration

When using dracula, the client has a different receiving port than the server. By default the dracula server uses port 3509. The server will write responses back to the same UDP port it received messages from on the client.

Messages are stored in a “namespace” which is pretty much just a container for stored values. The namespace is like a top-level key in Redis. The CLI has a default namespace if you don’t provide one. The go client requires choosing a namespace.

Namespaces and entries in namespaces are exact – dracula does not offer any matching on namespaces.

At Mailsac, we use uses the namespaces to separate messages on a per-customer basis, and to separate free traffic. Namespaces are intentionally generic. You could just use one namespace if you like, but performance under load improves if entries are bucketed into namespaces.

Production Performance

Dracula is fast and uses minimal resources by today’s standards.

While we develop it on Intel, and in production we run dracula on Arm64 architecture under Amazon Linux for a significant savings.

In its first months of use, dracula did not spike above 1% CPU usage and 19 MB of RAM, even when handling single-digit-thousands of requests simultaneously.

Tradeoffs

By focusing on a small subset of needs, we designed a service with sharp edges. Some of these may be unexpected features so we want to enumerate what we know.

It only counts

It’s called dracula in an allusion to Count Dracula. There’s no way to list namespaces, keys, nor return stored values. Entries in a namespace can be counted, and the number of namespaces can be counted. That’s it! If we provided features like listing keys or namespace, we would have needed to change the name to List Dracula.

No persistence

Dracula is designed for short-lived ephemeral data. If dracula restarts, nothing is currently saved. This may considered for the future, though. Storing metrics or session data in dracula is an interesting idea. On the other hand, we see no need to reinvent Redis or Prometheus.

Small messages

An entire dracula protocol message is 1500 bytes. If that sounds familiar, it’s because 1500 bytes is the normal maximum-transmission-unit for UDP. Namespaces are capped at 64 bytes and values can be up to 1419. After that they’re cut off.

Same expiry

All namespaces and entries in the entire server have the same expire time (in seconds). It shouldn’t be too difficult to run multiple draculas on other ports f you have different expiry needs.

HA

The approach to high-availability assumes short-lived expiry of entries. A pool of dracula servers can replicate to one another, and dracula clients track health of pool members, automatically handling failover. Any client can read from any server, but in the case of network partitioning, consistency won’t be perfect.

Retries

Messages that fail or timeout are not retried by the dracula client right now. There’s nothing stopping the application level from handling this. It may be added as an option later.

Garbage

While we have not yet experienced issues with dracula’s garbage collection, it’s worth noting that it exists. A subset of entries are crawled and expired on a schedule. On “count” commands, old entries are expired. The entire store is typically not locked, but it’s likely you would see a little slowdown when counting entires in very large namespaces, or when there are a lot of old entires to cleanup, while GC is running. In our testing it’s on the order of single digit miliseconds, but this can be expected to grow linearly with size.

Unknown scale

We’re working with low-tens-of-thousands entries per namespace, maximum. Above that, we’re unsure how it will perform.

Language support

Upon release, dracula has a reference client implementation in Golang. Node.js support is on our radar, but not finished. Please open an issue in the dracula repo to request support for a specific language. We’d be thrilled to receive links to community written drivers as well.

What’s next?

Hopefully you enjoyed learning a little bit about dracula and are ready to give it a try. Head over to Github https://github.com/mailsac/dracula where added examples of using the server, client library, and CLI.

Check out the roadmap of features, currently tracked in the Readme.

Finally, Mailsac would love your feedback. Open a Github issue or head to forum.mailsac.com. If you’d like to see additional library languages supported, let us know.

Happy counting!