Kentico CMS Quick Tip: Creating an Email Whitelist For Testing Kentico EMS
Tags: kentico, cms, csharp, dotnet
Published: 2020-04-13 13:47:54

---
title: Kentico CMS Quick Tip: Creating an Email Whitelist For Testing Kentico EMS
published: true
description: Learn how to intercept and inspect every email sent from Kentico EMS.
tags: kentico, cms, csharp, dotnet
series: Kentico Quick Tips
cover_image: https://dev-to-uploads.s3.amazonaws.com/i/v693ipe8b1923lcsegsu.jpg
---

## Our Requirements

Imagine we need to configure Kentico for testing and we have real customer or user data in the database.

We need to allow for testers and stakeholders to fully test the system - this means when they perform operations that would send emails, those emails should be sent 👍🏾!

However, we can't allow emails to be sent out to those real users from our testing environment 😨.

Telling everyone "Don't click this button because that could send out an email to a non-test user" isn't a great solution 🙁.

We need a way to whitelist a set of addresses or domains to receive emails, and intercept attempts to send emails to all others 🤔.

First, we need to identify all features in Kentico EMS that could result in emails being sent...

---

## What Features in Kentico EMS Send Emails?

Kentico uses its email system to support a lot of functionality within the application. This includes:

- [Email marketing](https://docs.kentico.com/k12sp/on-line-marketing-features/managing-your-on-line-marketing-features/email-marketing)
- [Page translation requests](https://docs.kentico.com/k12sp/multilingual-websites/configuring-translation-services/human-translation-services-email-translation)
- [E-Commerce notifications](https://docs.kentico.com/k12sp/e-commerce-features/configuring-on-line-stores/configuring-e-commerce-email-notifications)
- [Authentication](https://docs.kentico.com/k12sp/securing-websites/designing-secure-websites/configuring-email-confirmations)
- [Publishing workflows](https://docs.kentico.com/k12sp/configuring-kentico/configuring-the-environment-for-content-editors/configuring-workflows/configuring-workflow-email-notifications)
- [Form submissions](https://docs.kentico.com/k12sp/managing-website-content/forms/form-notification-emails)

... and more ...

That's a lot of different email configuration to manage!

So, it's unlikely that we'll be able to customize all these parts of the application to whitelist emails 😒.

Maybe there's a single, central service that Kentico uses to send emails where we could make our customizations?

---

## CMS.EmailEngine.EmailProvider

Fortunately for us, Kentico exposes a Provider class that does exactly what we need!

Looking through the [Kentico EMS documentation](https://docs.kentico.com/k12sp/custom-development/customizing-providers/custom-email-provider-example) we can find information about the `CMS.EmailEngine.EmailProvider` class 💪🏾.

The documentation states:

> Customizing the email provider allows you to:

>   - Execute custom actions when sending emails (for example logging the sent emails for auditing purposes)
>   - Use third‑party components for sending emails

> Once you create and register your custom email provider, it is used to process all emails sent out by Kentico.

Perfect!

The `EmailProvider` class exposes (3) protected methods we can override, however we only need to focus on (2) of them:

```csharp
/// <summary>
/// Synchronously sends an email through the SMTP server.
/// </summary>
protected override void SendEmailInternal(
    string siteName, MailMessage message, SMTPServerInfo smtpServer)

/// <summary>
/// Asynchronously sends an email through the SMTP server.
/// </summary>
protected override void SendEmailAsyncInternal(
    string siteName, MailMessage message, 
    SMTPServerInfo smtpServer, EmailToken emailToken)
```

---

## Creating Our `WhitelistEmailProvider`

To override and intercept Kentico's email processing, we need to create our sub-class and [register it as a custom provider](https://docs.kentico.com/k12sp/custom-development/customizing-providers/custom-info-provider-example):

```csharp
// ✅ Don't forget this attribute
[assembly: RegisterCustomProvider(typeof(WhitelistEmailProvider))]

namespace Sandbox.Infrastructure.Emails
{
    public class WhitelistEmailProvider: EmailProvider
    {
        protected override void SendEmailAsyncInternal(
            string siteName, MailMessage message, 
            SMTPServerInfo smtpServer, EmailToken emailToken)
        {
            // ... insert custom functionality

            base.SendEmailAsyncInternal(
                siteName, message, smtpServer, emailToken);
        }

        protected override void SendEmailInternal(
            string siteName, MailMessage message, SMTPServerInfo smtpServer)
        {
            ... insert custom functionality

            base.SendEmailInternal(siteName, message, smtpServer);
        }
    }
}
```

We still call the parent class methods (`base.SendEmailInternal()`) because we don't want to write _all_ the email processing logic - just a small part.

---

## Configuring an Email Whitelist

Now that we have a spot in our code base to whitelist and intercept outgoing emails, we need to decide how we are actually going to do that whitelisting.

Let's use Kentico's support for [Custom Settings](https://docs.kentico.com/k12sp/custom-development/creating-custom-modules/adding-custom-website-settings) to define a place where site administrators can update the email whitelist settings 🙂.

> Take a look at Kentico's documentation on [Creating custom modules](https://docs.kentico.com/k12sp/custom-development/creating-custom-modules) for more information on what Custom Modules are and how they work.

In the screenshot below you can see I've created a Custom Module named `Sandbox` and I'm about to modify the `Settings -> System -> Emails` node from within this module:

![Kentico Settings tree within the Sandbox Custom Module](https://dev-to-uploads.s3.amazonaws.com/i/m52rbl3l274ugv51zi1d.jpg)

I enter some values for a "New settings group":

![New settings group named Email Whitelist](https://dev-to-uploads.s3.amazonaws.com/i/wnf84ktzzbiw6kwxcmlc.jpg)

And then I create several "New settings key" entries:

Setting 1:

- Display Name: Is Whitelisting Enabled
- Code name: `SANDBOX_EMAIL_WHITELIST_IS_WHITELIST_ENABLED`
- Description: Check to enable email whitelisting
- Type: Boolean
- Editing control: Default

Setting 2:

- Display Name: Whitelisted Emails
- Code name: `SANDBOX_EMAIL_WHITELIST_WHITELISTED_EMAILS`
- Description: Domain suffixes or specific email addresses, semi-colon delimited that will be whitelisted from email interception
- Type: Text
- Editing control: Form Control -> Text area

Setting 3:

- Display Name: Intercepting Email Address
- Code name: `SANDBOX_EMAIL_WHITELIST_INTERCEPTING_ADDRESS`
- Description: When email whitelisting is enabled, any non-whitelisted emails will be send to this address instead of the intended recipients
- Type: Text
- Editing control: Default

Our resulting UI for these custom settings should look like the following screenshot:

![Email whitelist settings UI](https://dev-to-uploads.s3.amazonaws.com/i/e83nssudgttzgp24s0zr.jpg)

---

## Creating `EmailWhitelistSettings`

Now that we have a place to store our settings for email whitelisting and a custom class to override email sending functionality, we can add our custom code.

First, let's create a `EmailWhitelistSettings` class to abstract out our custom settings.

We create a nested class to hold our settings keys. By using the `nameof()` operator, I get the string value of the key to match the key name automatically 🤓:

```csharp
public static class EmailWhitelistSettings
{
    public static class SettingKeys
    {
        public const string SANDBOX_EMAIL_WHITELIST_IS_WHITELIST_ENABLED = 
            nameof(SANDBOX_EMAIL_WHITELIST_IS_WHITELIST_ENABLED);
        public const string SANDBOX_EMAIL_WHITELIST_WHITELISTED_EMAILS = 
            nameof(SANDBOX_EMAIL_WHITELIST_WHITELISTED_EMAILS);
        public const string SANDBOX_EMAIL_WHITELIST_INTERCEPTING_ADDRESS = 
            nameof(SANDBOX_EMAIL_WHITELIST_INTERCEPTING_ADDRESS);
    }

```

We make (2) private fields to hold some default values:

```csharp
private static readonly string fallbackWhitelist = 
    "<YOUR FALLBACK WHITELIST>";
private static readonly string fallbackInterceptingAddress = 
    "<YOUR FALLBACK INTERCEPTING ADDRESS>";
```

Now we make several `static` methods to retrieve the values from settings:

```csharp
public static IEnumerable<string> WhitelistedRecipients()
{
    string whitelistedRecipients = SettingsKeyInfoProvider.GetValue(
        SettingKeys.SANDBOX_EMAIL_WHITELIST_WHITELISTED_EMAILS);

    if (IsDebug())
    {
        whitelistedRecipients = string.IsNullOrWhiteSpace(whitelistedRecipients)
            ? fallbackWhitelist
            : whitelistedRecipients;
    }

    return (whitelistedRecipients ?? "")
        .Split(';')
        .Select(r => r.Trim());
}

public static string InterceptingAddress()
{
    string interceptingAddress = SettingsKeyInfoProvider.GetValue(
        SettingKeys.ZEL01_EMAIL_WHITELIST_INTERCEPTING_ADDRESS);

    if (IsDebug())
    {
        interceptingAddress = string.IsNullOrWhiteSpace(interceptingAddress)
            ? fallbackInterceptingAddress
            : interceptingAddress;
    }

    return interceptingAddress;
}

public static bool IsWhitelistEnabled =>
    IsDebug()
        ? true
        : SettingsKeyInfoProvider.GetBoolValue(
              SettingKeys.ZEL01_IS_EMAIL_WHITELIST_ENABLED);
```

What about that `IsDebug()` method?

```csharp
private static bool IsDebug()
{
    bool isDebug = false;
#if DEBUG
    isDebug = true;
#endif
    return isDebug;
}
```

We can see here, that I use a [pre-processor directive](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/preprocessor-directives/preprocessor-if) to ensure that if I'm doing a `DEBUG` build (running the site locally), I'm guaranteed to have whitelisting (and fallbacks) enabled.

This prevents me from accidentally running some code locally that sends out emails to real users 😏!

> We could also use a local email server for this (like [hMailServer](https://www.hmailserver.com/), [MailSlurper](http://mailslurper.com/), or [Papercut](https://github.com/ChangemakerStudios/Papercut-SMTP) but a code level check is double-safe!

Finally, we add a utility method to determine if a given email address has been whitelisted:

```csharp
public bool IsWhitelistedRecipient(string recipient) =>
    whitelistedEmails.Any(e => e.StartsWith("@")
        ? recipient.EndsWith(e, StringComparison.OrdinalIgnoreCase)
        : string.Equals(recipient, e, StringComparison.OrdinalIgnoreCase));
```

This method will match on full address or a domain suffix. For example, if the following are whitelisted, "test@test.com;@wiredviews.com", all of my co-workers could receive emails normally, along with "test@test.com". All other addresses are going to be captured and re-routed 🧐.

---

## Customizing the `WhitelistEmailProvider`

Ok! We're finally ready to custom the `WhitelistEmailProvider`.

> I'm only going to show the `SendEmailInternal` method, since the other will be implemented the same way.

The override is pretty simple because all the functionality is going to be in a `ProcessEmail` method:

```csharp
private override void SendEmailInternal(
    string siteName, MailMessage message, SMTPServerInfo smtpServer)
{
    base.SendEmailInternal(siteName, ProcessEmail(message), smtpServer);
}
```

We can see in `ProcessEmail` that determines if the email should be handled or directly returned:

```csharp
protected MailMessage ProcessEmail(MailMessage message) =>
    EmailWhitelistSettings.IsWhitelistEnabled && !IsWhitelisted(message)
        ? ModifyMessage(message)
        : message;
```

We use the `IsWhitelisted(MailMessage message)` method to determine if the given message has any non-whitelisted email addresses in all of its sending fields (To, CC, Bcc):

```csharp
private bool IsWhitelisted(MailMessage message)
{
    var whitelisted = EmailWhitelistSettings.WhitelistedRecipients();

    return message.To.All(a => EmailWhitelistSettings
            .IsWhitelistedRecipient(a.Address, whitelisted))
        && message.CC.All(a => EmailWhitelistSettings
            .IsWhitelistedRecipient(a.Address, whitelisted))
        && message.Bcc.All(a => EmailWhitelistSettings
            .IsWhitelistedRecipient(a.Address, whitelisted));
}
```

Finally we have a larger method, `ModifyMessage(MailMessage message)`, which is called when the recipients of the email are not whitelisted.

In this method we want to change the recipients to use the intercepting address, and record the original recipients at the bottom of the email's body:

```csharp
/// <summary>
/// Replaces the recipients fields with intercepting email address and appends
/// the original recipients to the end of the email
/// </summary>
/// <param name="message"></param>
/// <returns></returns>
private MailMessage ModifyMessage(MailMessage message)
{
    // ✅ Grab all the original recipients
    var originalTo = message.To.Select(e => e.Address).ToList();
    var originalCc = message.CC.Select(e => e.Address).ToList();
    var originalBcc = message.Bcc.Select(e => e.Address).ToList();

    // ✅ Now clear them out
    message.To.Clear();
    message.CC.Clear();
    message.Bcc.Clear();

    // ✅ Add our intercepting address as the only recipient
    message.To.Add(
        new MailAddress(EmailWhitelistSettings.InterceptingAddress()));

    // ✅ Indicate in the subject this email has been overriden
    message.Subject = $"{message.Subject}: override";

    // ✅ Find the HTML email (ignore plain text for now)
    var nullableView = message
        .AlternateViews
        .FirstOrDefault(m => 
            m.ContentType.MediaType == MediaTypeNames.Text.Html);

    if (!(nullableView is AlternateView view))
    {
        return message;
    }

    // ✅ Remove the existing HTML content from the email
    message.AlternateViews.Remove(view);

    var builder = new StringBuilder();

    string template = "<p>{0}</p>";

    // ✅ Read out the HTML content as a string
    using (var reader = new StreamReader(view.ContentStream))
    {
        string originalContents = reader.ReadToEnd();

        builder.Append(originalContents);
    }

    // ✅ Create all the override information
    builder.AppendFormat(template, $"--- Email Override ---");
    builder.AppendFormat(template, 
        $"original to: {string.Join(", ", originalTo.ToList())}");

    if (originalCc.Any())
    {
        builder.AppendFormat(template, 
            $"original cc: {string.Join(", ", originalCc.ToList())}");
    }

    if (originalBcc.Any())
    {
        builder.AppendFormat(template, 
            $"original bcc: {string.Join(", ", originalBcc.ToList())}");
    }

    string updatedBody = builder.ToString();

    var originalContentType = new ContentType(MediaTypeNames.Text.Html));

    // ✅ Create new HTML content and attach it to the email
    string newView = AlternateView
        .CreateAlternateViewFromString(updatedBody, originalContentType);

    message.AlternateViews.Add(newView);

    return message;
}
```

The `AlternateView` that makes up the content of the `MailMessage` is not editable 😕, so we instead have to remove it from the `MailMessage`, read its content as a string and append our override messages to it.

We then create a new `AlternateView` that has our updated content as the email body, and add it to the `MailMessage`.

There's definitely some extra processing going on here that you don't want to run during production while sending out bulk emails. However, that's definitely not a scenario for this email whitelisting 😋.

Assuming our settings are correctly configured in the CMS and the custom provider is registered with Kentico (by using the assembly attribute), when our application attempts to send emails, they will be checked by the whitelisting process and either re-routed or sent out correctly 😄!

---

## Conclusion

As we develop our Kentico applications we might find the need to test parts of the system that will send out emails using Kentico's underlying email processing.

If we have any real email addresses in the database, we run the risk of emails being sent to users from non-production sites 😦.

We could use SMTP servers that capture emails, but that might not allow testers and stakeholders to verify the functionality of the site as it would be in a production scenario. It also adds another tool that testers would need to have access to while testing common workflows 😔.

Instead, we can leverage Kentico's robust overriding/intercepting patterns of its internal providers 😅.

By designing an email "whitelist" feature, we can enable email whitelisting for testers and stakeholders while still intercepting and re-routing emails destined for real, non-test addresses.

Kentico's custom settings functionality allows site administrators easy access to these settings within the CMS, which is great for enabling new domains or email addresses as the testing phase of a site proceeds 👏🏾.

Finally, once the site is ready to go live, we can either disable whitelisting in the CMS settings, remove the custom provider attribute to un-register it in Kentico or delete the class entirely, at which point email processing by the CMS will return to normal.

Pretty cool 😎.

As always, thanks for reading 🙏!

---

<figcaption>Photo by <a href="https://unsplash.com/@joannakosinska">Joanna Kosinska</a> on <a href="https://unsplash.com">Unsplash</a></figcaption>

---

We've put together a list over on [Kentico's GitHub account](https://github.com/Kentico/Home/blob/master/RESOURCES.md) of developer resources. Go check it out!

If you are looking for additional Kentico content, checkout the Kentico tag here on DEV:

{% tag kentico %}

Or my Kentico blog series:

- [Kentico 12: Design Patterns](https://dev.to/search?q=Kentico%2012%3A%20Design%20Patterns)
- [Kentico CMS Quick Tips](https://dev.to/search?q=Kentico%20CMS%20Quick%20Tip)