loading...
Cover image for Kentico CMS Quick Tip: Creating an Email Whitelist For Testing Kentico EMS
WiredViews

Kentico CMS Quick Tip: Creating an Email Whitelist For Testing Kentico EMS

seangwright profile image Sean G. Wright ・9 min read

Kentico Quick Tips (9 Part Series)

1) Kentico CMS Quick Tip: Ensuring Successful Backups in Azure App Service 2) Kentico CMS Quick Tip: Azure Search with Faceted Document Categories 3 ... 7 3) Kentico CMS Quick Tip: Integration Testing Roles 4) Kentico CMS Quick Tip: FluentCacheKeys - Consistent Cache Dependency Key Generation 5) Kentico CMS Quick Tip: Automatic Static File Fingerprinting 6) Kentico CMS Quick Tip: Minimal JSON Web APIs with IHttpHandler and .ashx Files 7) Kentico CMS Quick Tip: Faking A Live Site For Integration Testing 8) Kentico CMS Quick Tip: Understanding E-Commerce XML Structures 9) Kentico CMS Quick Tip: Creating an Email Whitelist For Testing Kentico EMS

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:

... 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 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:

/// <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:

// βœ… 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 to define a place where site administrators can update the email whitelist settings πŸ™‚.

Take a look at Kentico's documentation on 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

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

New settings group named Email Whitelist

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


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 πŸ€“:

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:

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:

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?

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 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, MailSlurper, or Papercut but a code level check is double-safe!

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

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:

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:

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):

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:

/// <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 πŸ™!


Photo by Joanna Kosinska on Unsplash

We've put together a list over on Kentico's GitHub account of developer resources. Go check it out!

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

Or my Kentico blog series:

Kentico Quick Tips (9 Part Series)

1) Kentico CMS Quick Tip: Ensuring Successful Backups in Azure App Service 2) Kentico CMS Quick Tip: Azure Search with Faceted Document Categories 3 ... 7 3) Kentico CMS Quick Tip: Integration Testing Roles 4) Kentico CMS Quick Tip: FluentCacheKeys - Consistent Cache Dependency Key Generation 5) Kentico CMS Quick Tip: Automatic Static File Fingerprinting 6) Kentico CMS Quick Tip: Minimal JSON Web APIs with IHttpHandler and .ashx Files 7) Kentico CMS Quick Tip: Faking A Live Site For Integration Testing 8) Kentico CMS Quick Tip: Understanding E-Commerce XML Structures 9) Kentico CMS Quick Tip: Creating an Email Whitelist For Testing Kentico EMS

Posted on Jun 1 '19 by:

seangwright profile

Sean G. Wright

@seangwright

dev lead @WiredViews, founding partner @craftbrewingbiz. @Kentico MVP. love to learn / teach web dev & software engineering, collecting vinyl records, mowing my lawn, craft 🍺

WiredViews

Web Dev and Marketing Agency based in Cuyahoga Falls, OH.

Discussion

markdown guide