Integration testing email functionality

28th Apr 2023

Share this article:

Sending emails in software is often the result of an important interaction of the user and the system, and the delivery of the email can be critical to both the user and the business sending the email. Common examples include sending magic links as part of a login flow, or sending vouchers that the user has paid for, or sending invoices after some transaction.

Testing that the emails send is a necessity during the build of the software but critical parts of software are often the parts that get updated the most. Integration, or end-to-end, testing of those flows is a common requirement when building software, so that you can be sure that regressions haven't been made when those functions are changed.

Integration testing of software is a well-known topic and, in the case of web-based software, multiple solutions exist for testing the system as if a user was interacting with it. For examples, see: Playwright and Selenium

When testing email based flows from these types of tools, though, you need to be able to fake the email sending part of the system. For example, in a user registration flow, you might want to test that when a user signs up to your software they are sent a verification email using a code. You want to check that the email is delivered, that it contains the correct code and that you can complete the sign up with that code.

How does Imitate Email help with integration testing?

In order to integration test email flows, Imitate Email provides Web Socket support that lets you connect to an Imitate Email mailbox (either personal or team) and be notified when email is delivered to that mailbox.

We use Web Sockets for several reasons:

  1. As a widely accepted standard for inter process communication, libraries are available in most languages.
  2. If you're in to JavaScript, they're a native part of the web.
  3. They implement a simple API and don't open up unnecessary security holes.

But, the main reason we use them, is that it allows you (and us) to keep your integration tests as quick as possible. Once we have the email we let you know immediately over the web socket.

When we do let you know we give you the main parts of the email (recipients, subject, text/html) so that you can immediately check out the content of the email and progress your test.

Where this is not enough information, you can also make use of our IMAP server to download the full content of the email (attachments, headers etc) and do any extra testing you would like there.

How to open the Web Socket connection

To connect to our websocket open a request to https://imitate.email/notify-ws

In order to start listening for emails the first thing you must do is authenticate with the credentials for the mailbox that you want to be notified about.

To do that, once connected, send a string containing a Basic authentication string. For your personal mailbox, find your credentials at Settings → My Mailbox.

For example, if your username is 123 and password is 456 you must concatenate them with a :, so 123:456, base 64 encode that string and then send the following on the web socket:

Basic <base64encodedstring>

Once that authenticates (the connection will be closed if you do not - check the close reason for help), keep listening for messages.

When an email is delivered to your mailbox we'll send a JSON encoded string containing the email details to you.

Here's an example in C#

var websocket = new ClientWebSocket();
await websocket.ConnectAsync(new Uri("wss://imitate.email/notify-ws"), CancellationToken.None);
await websocket.SendAsync(
    Encoding.UTF8.GetBytes($"Basic {Convert.ToBase64String(Encoding.UTF8.GetBytes($"{user}:{pass}"))}"), 
    WebSocketMessageType.Text, 
    true, 
    CancellationToken.None);
var buffer = new byte[1024];
while (websocket.State == WebSocketState.Open) {
	var message = await websocket.ReceiveAsync(buffer, CancellationToken.None);

	if (message.MessageType == WebSocketMessageType.Text) {
        var emailJson = Encoding.UTF8.GetString(await ReadUntilEndAsync(buffer, message.Count, message.EndOfMessage));
    } else if (message.MessageType == WebSocketMessageType.Close) {
        await websocket.CloseAsync(WebSocketCloseStatus.NormalClosure, null, CancellationToken.None);
        Console.WriteLine(Encoding.UTF8.GetString(buffer, 0, message.Count));
    }
}


async Task<byte[]> ReadUntilEndAsync(byte[] bytes, int readSoFar, bool messageEndOfMessage) {
    if (messageEndOfMessage) {
        return bytes.Take(readSoFar).ToArray();
    }

    await using var stream = new MemoryStream(bytes, 0, readSoFar);
    while (websocket.State == WebSocketState.Open) {
        var message = await websocket.ReceiveAsync(bytes, CancellationToken.None);
        stream.Write(bytes, 0, message.Count);
        if (message.EndOfMessage) {
            return stream.ToArray();
        }
    }

    throw new InvalidOperationException();
}

Accessing the whole email using IMAP

Once you receive an email over your Web Socket, you can use IMAP to very quickly download the entire email.

In the json response that we send you we include a property called imapId. The value of this corresponds to the uid of the email for the IMAP server.

Here's an example, of fetching the email, using the ImapClient from the .Net MailKit library:

var imapClient = new ImapClient();
await imapClient.ConnectAsync("imap.imitate.email", 993, true);
await imapClient.AuthenticateAsync("<username>", "<password>");
await imapClient.Inbox.OpenAsync(FolderAccess.ReadWrite);
var email = await imapClient.Inbox.GetMessageAsync(new UniqueId((uint)email.ImapId));

Next steps

Hopefully this makes integration testing email flows very easy. If you have any problems, please do get in touch with us - we'd love to help.