Restricting Outbound Email by Domain Whitelist - ServiceNow Community
G_Ede
Tera Guru

I recently came across a requirement to limit outbound emails sent by domain.  Of course there is an out of the box solution already available to restrict inbound email, but I struggled to find something that would restrict out-going email in the same way.  This requirement could help comply with Data Loss Prevention (DLP) policies.

You could do this (and many other things) using your own email infrastructure, but I wanted to see if we could do this without resorting to that.

I found a way to use a business rule, a custom system property, and (optionally) an event to build something that met the requirements and I thought I would post it here in case anyone else found it useful!

Requirements

1. Prevent any email from any part of the platform from being sent to a domain not on a pre-defined list of safe domain names; the White-List.

2. Flag any attempt to send to a domain not on the White-List for investigation.

Approach

Intercept the email record on the sys_email table using a before update business rule when the record type changes to send-ready.  Referring to a White-List of safe domains in a system property, inspect and strip any email addresses that don't match.  Trigger an event containing the original list of recipients and the user name who triggered the email.

Implementation

1. First, create a new System Property with an appropriate name, to store the list of safe domain names.  We'll refer to this later in our Business Rule.

find_real_file.png

2. Create a new Event type, to flag when an email is modified.  We'll also refer to this in our Business Rule.

 find_real_file.png

3. Now we need to create our Business Rule on the sys_email table.  This rule should run before insert or update when the Type field changes to send-ready.  This should allow our logic to run before ServiceNow processes and sends the email.

find_real_file.png

We'll add our script to the Advanced tab.  I'll try to break down the logic below, but for those who want to skip ahead, the full script looks like this:

(function executeRule(current, previous /*null when async*/) {
	
	// This inspects each of the recipient fields (To, CC, BCC) and strips out any
	// email addresses that aren't for domains listed in the system property
	// "custom.email.outbound.whitelist".
	// If "bad" addresses are found, it triggers the "outbound.email.bad.domain"
	// event so that the record can be investigated.
	
	// Get the safe domains we're allowed to send to:
	var whiteListProperty = gs.getProperty('custom.email.outbound.whitelist');
	
	// Take a copy of the original recipients:
	var originalRecipients = 'TO:' + current.getValue('direct')
							+ ', CC:' + current.getValue('copied')
							+ ', BCC:' + current.getValue('blind_copied');
	
	// Check each recipient field, and strip out "bad" addesses:
	var toResults;
	var ccResults;
	var bccResults;
	var recipientResults;
	
	if (current.direct) {
		toResults = stripRecipients(current.getValue('direct'),whiteListProperty);
		current.setValue('direct',toResults.safeList);
	}
	if (current.copied) {
		ccResults = stripRecipients(current.getValue('copied'),whiteListProperty);
		current.setValue('copied',ccResults.safeList);
	}
	if (current.blind_copied) {
		bccResults = stripRecipients(current.getValue('blind_copied'),whiteListProperty);
		current.setValue('blind_copied',bccResults.safeList);
	}
	if (current.recipients) {
		recipientResults = stripRecipients(current.getValue('recipients'),whiteListProperty);
		current.setValue('recipients',recipientResults.safeList);
	}
	
	// Trigger an event if we had to remove bad addresses:
	if (toResults.badAddress || ccResults.badAddress || bccResults.badAddress) {
		gs.eventQueue('outbound.email.bad.domain',current,originalRecipients,gs.getUserName());
	}
	
	// Returns an object with two key/value pairs:
	// safeList - List of "safe" addresses, checked against the whiteList.
	// badAddress - True/false, true if any "bad" addresses were removed.
	function stripRecipients(targetField, whiteList) {
		
		// Original List of recipients:
		var originalRecipients = targetField;
		// Convert to an array:
		var arrRecipients = [];
		arrRecipients = originalRecipients.split(',');
		
		// Build a list of safe recipients:
		var revisedRecipients = '';
		
		// Flag if we have "bad" recipients:
		var foundBad = false;
		
		// Loop through the original list of recipients:
		var arrLength = arrRecipients.length;
		for (var i = 0; i < arrLength; i++) {
			// Split the email address on the @ symbol:
			var arrDomain = [];
			arrDomain = arrRecipients[i].split('@');
			// Compare the domain to the WhiteList...
			if (whiteList.includes(arrDomain[1])) {
				// ...if ok, add to our revised list of recipients:
				revisedRecipients = revisedRecipients + arrRecipients[i] + ',';
			}
			else {
				// Potentially bad recipient:
				foundBad = true;
			}
		}
		
		// Remove the trailing comma:
		if (revisedRecipients.length > 1) {
			revisedRecipients = revisedRecipients.substring(0, revisedRecipients.length-1);
		}
		
		// Return the results as an object:
		var objResults = {
			"safeList" : revisedRecipients,
			"badAddress" : foundBad
		};
		
		return objResults;
		
	}
	
})(current, previous);

We can break down this logic into 3 parts;

First we create some variables to store the data we need, grab the white list from the System Property, store the original recipients.

	// Take a copy of the original recipients:
	var originalRecipients = 'TO:' + current.getValue('direct')
							+ ', CC:' + current.getValue('copied')
							+ ', BCC:' + current.getValue('blind_copied');

There are 4 different fields that store email addresses, so we need to run the same logic on each to preserve where the address will sit on the email (e.g. To, CC, or BCC).  To achieve this we'll offload the work to a function we'll define in the next section.  After we have our results we will also trigger the event, assuming we found at least one field that contained a "bad" address.

	// Check each recipient field, and strip out "bad" addesses:
	var toResults;
	var ccResults;
	var bccResults;
	var recipientResults;
	
	if (current.direct) {
		toResults = stripRecipients(current.getValue('direct'),whiteListProperty);
		current.setValue('direct',toResults.safeList);
	}
	if (current.copied) {
		ccResults = stripRecipients(current.getValue('copied'),whiteListProperty);
		current.setValue('copied',ccResults.safeList);
	}
	if (current.blind_copied) {
		bccResults = stripRecipients(current.getValue('blind_copied'),whiteListProperty);
		current.setValue('blind_copied',bccResults.safeList);
	}
	if (current.recipients) {
		recipientResults = stripRecipients(current.getValue('recipients'),whiteListProperty);
		current.setValue('recipients',recipientResults.safeList);
	}
	
	// Trigger an event if we had to remove bad addresses:
	if (toResults.badAddress || ccResults.badAddress || bccResults.badAddress) {
		gs.eventQueue('outbound.email.bad.domain',current,originalRecipients,gs.getUserName());
	}

Finally we need to define a function that does the comparison with the white list and returns a list of safe addresses to be updated into the corresponding field.

This function takes a comma separated list of email addresses, and a comma separated list of safe domains.  It takes the email address list and breaks it into an array of separate addresses, then loops through the array checking if anything after the @ matches something on the white list.

In the end, the function returns an object with two key/value pairs: the first is a comma separated list of all the address(es) that matched the white list, and the second is a true/false flag that is true if we encountered any addresses that didn't match.

	// Returns an object with two key/value pairs:
	// safeList - List of "safe" addresses, checked against the whiteList.
	// badAddress - True/false, true if any "bad" addresses were removed.
	function stripRecipients(targetField, whiteList) {
		
		// Original List of recipients:
		var originalRecipients = targetField;
		// Convert to an array:
		var arrRecipients = [];
		arrRecipients = originalRecipients.split(',');
		
		// Build a list of safe recipients:
		var revisedRecipients = '';
		
		// Flag if we have "bad" recipients:
		var foundBad = false;
		
		// Loop through the original list of recipients:
		var arrLength = arrRecipients.length;
		for (var i = 0; i < arrLength; i++) {
			// Split the email address on the @ symbol:
			var arrDomain = [];
			arrDomain = arrRecipients[i].split('@');
			// Compare the domain to the WhiteList...
			if (whiteList.includes(arrDomain[1])) {
				// ...if ok, add to our revised list of recipients:
				revisedRecipients = revisedRecipients + arrRecipients[i] + ',';
			}
			else {
				// Potentially bad recipient:
				foundBad = true;
			}
		}
		
		// Remove the trailing comma:
		if (revisedRecipients.length > 1) {
			revisedRecipients = revisedRecipients.substring(0, revisedRecipients.length-1);
		}
		
		// Return the results as an object:
		var objResults = {
			"safeList" : revisedRecipients,
			"badAddress" : foundBad
		};
		
		return objResults;
		
	}

 

Time to put it all together and test!

Enhancements

I'm sure there are ways to expand on this idea, for example, you could add a new field on the sys_email table to store the original list of recipients, so you wouldn't need to dig through the event records to capture that.  Obviously you can trigger any number of things based on the event - an email notification, a security incident, etc.

Another thing to think about is that this solution still allows the email to be sent to any recipients that do match the white list, you may wish to modify it so that the email is redirected or doesn't get sent at all.

Disclaimer

As always, please test and evaluate anything you find on the Community before adopting it for your own use.  Hope someone finds this useful, comments and suggestions welcome!

 

Updated: 2018-10-02, tidied up the script and removed any chance of 'pass by reference' problems (thanks Tim of SN Pro Tips for your informative articles!).

Comments
kevinzidek
Tera Expert

I just had this requirement come in. Thank you for the great solution!

Paul Curwen
Mega Sage

Nice one Graeme. Don't mind if I do 🙂

tsutherland
Kilo Sage

I love this--I am trying to modify to to use "blacklist" instead of "whitelist" in the system property. In other words, I want SN to never send an email to a specific domain. I edited the comments to be what I am using but I am not sure how to edit the strip recipients part to accommodate what I am trying to do. Do you mind helping?

(function executeRule(current, previous /*null when async*/) {
	
	// This inspects each of the recipient fields (To, CC, BCC) and strips out any
	// email addresses that ARE for domains listed in the system property
	// "custom.email.blacklist".
	// If "bad" addresses are found, it triggers the "outbound.email.blacklist"
	// event so that the record can be investigated.
	
	// Get the bad domains we're not allowed to send to:
	var blackListProperty = gs.getProperty('custom.email.blacklist');
	
	// Take a copy of the original recipients:
	var originalRecipients = 'TO:' + current.getValue('direct')
							+ ', CC:' + current.getValue('copied')
							+ ', BCC:' + current.getValue('blind_copied');
	
	// Check each recipient field, and strip out "bad" addesses:
	var toResults;
	var ccResults;
	var bccResults;
	var recipientResults;
	
	if (current.direct) {
		toResults = stripRecipients(current.getValue('direct'),blackListProperty);
		current.setValue('direct',toResults.blackList);
	}
	if (current.copied) {
		ccResults = stripRecipients(current.getValue('copied'),blackListProperty);
		current.setValue('copied',ccResults.blackList);
	}
	if (current.blind_copied) {
		bccResults = stripRecipients(current.getValue('blind_copied'),blackListProperty);
		current.setValue('blind_copied',bccResults.blackList);
	}
	if (current.recipients) {
		recipientResults = stripRecipients(current.getValue('recipients'),blackListProperty);
		current.setValue('recipients',recipientResults.blackList);
	}
	
	// Trigger an event if we had to remove bad addresses:
	if (toResults.badAddress || ccResults.badAddress || bccResults.badAddress) {
		gs.eventQueue('outbound.email.blacklist',current,originalRecipients,gs.getUserName());
	}
	
	// Returns an object with two key/value pairs:
	// blackList - List of "blacklisted" addresses, checked against the blackList.
	// badAddress - True/false, true if any "bad" addresses were removed.
	function stripRecipients(targetField, blackList) {
		
		// Original List of recipients:
		var originalRecipients = targetField;
		// Convert to an array:
		var arrRecipients = [];
		arrRecipients = originalRecipients.split(',');
		
		// Build a list of bad recipients:
		var revisedRecipients = '';
		
		// Flag if we have "bad" recipients:
		var foundBad = true;
		
		// Loop through the original list of recipients:
		var arrLength = arrRecipients.length;
		for (var i = 0; i < arrLength; i++) {
			// Split the email address on the @ symbol:
			var arrDomain = [];
			arrDomain = arrRecipients[i].split('@');
			// Compare the domain to the blackList...
			if (blackList.includes(arrDomain[1])) {
				// ...if ok, add to our revised list of recipients:
				revisedRecipients = revisedRecipients - arrRecipients[i] - ',';
			}
			else {
				// Potentially bad recipient:
				foundBad = false;
			}
		}
		
		// Remove the trailing comma:
		if (revisedRecipients.length > 1) {
			revisedRecipients = revisedRecipients.substring(0, revisedRecipients.length-1);
		}
		
		// Return the results as an object:
		var objResults = {
			"blackList" : revisedRecipients,
			"badAddress" : foundBad
		};
		
		return objResults;
		
G_Ede
Tera Guru

Apologies!  Somehow I missed this comment in my feed and only just came across it.

I hope you managed to solve this, but just in case it helps anyone else who's thinking along the same lines...

To tweak this to manage a blacklist instead, we can just probably flip the if statement within the stripRecipients function.  Something like:

	// Returns an object with two key/value pairs:
	// safeList - List of "safe" addresses (any addresses matching the blacklist will be removed).
	// badAddress - True/false, true if any "bad" addresses were removed.
	function stripRecipients(targetField, blackList) {
		
		// Original List of recipients:
		var originalRecipients = targetField;
		// Convert to an array:
		var arrRecipients = [];
		arrRecipients = originalRecipients.split(',');
		
		// Build a list of safe recipients:
		var revisedRecipients = '';
		
		// Flag if we have "bad" recipients:
		var foundBad = false;
		
		// Loop through the original list of recipients:
		var arrLength = arrRecipients.length;
		for (var i = 0; i < arrLength; i++) {
			// Split the email address on the @ symbol:
			var arrDomain = [];
			arrDomain = arrRecipients[i].split('@');
			// Compare the domain to the BlackList...
			if (blackList.includes(arrDomain[1])) {
				// Potentially bad recipient:
				foundBad = true;
			}
			else {
				// ...if ok, add to our revised list of recipients:
				revisedRecipients = revisedRecipients + arrRecipients[i] + ',';
			}
		}
		
		// Remove the trailing comma:
		if (revisedRecipients.length > 1) {
			revisedRecipients = revisedRecipients.substring(0, revisedRecipients.length-1);
		}
		
		// Return the results as an object:
		var objResults = {
			"safeList" : revisedRecipients,
			"badAddress" : foundBad
		};
		
		return objResults;
		
	}

As usual, please validate this before trying it out in your environment, I haven't tested this.

Dub Myers
ServiceNow Employee
ServiceNow Employee

 

The System Address Filter feature in Paris is designed specifically around this type of requirement for filtering both inbound (on the senders address), or for outbound (to remove individual recipients which do not pass the system address filter(s) defined).

Both types of filtering can be done:

  • Allow (or whitelist) with exceptions
  • Deny (or blacklist) with exceptions

See System address filters.

Mariano Lemus
Tera Expert

Very helpful, thank you.

You can even take it up one notch by creating a script include:

var verifyEmailDomain = Class.create();
verifyEmailDomain.prototype = {
    initialize: function() {
    },
    //clean up the valid email domain addresses and return them in JSON, also flag when there is an invalid address....
    stripRecipients: function(targetField, whiteList) {

        // Original List of recipients:
        var originalRecipients = targetField;
        // Convert to an array:
        var arrRecipients = [];
        arrRecipients = originalRecipients.split(',');

        // Build a list of safe recipients:
        var revisedRecipients = '';

        // Flag if we have "bad" recipients:
        var foundBad = false;

        // Loop through the original list of recipients:
        var arrLength = arrRecipients.length;
        for (var i = 0; i < arrLength; i++) {
            // Split the email address on the @ symbol:
            var arrDomain = [];
            arrDomain = arrRecipients[i].split('@');
            // Compare the domain to the WhiteList...
            if (whiteList.includes(arrDomain[1])) {
                // ...if ok, add to our revised list of recipients:
                revisedRecipients = revisedRecipients + arrRecipients[i] + ',';
            } else {
                // Potentially bad recipient:
                foundBad = true;
            }
        }

        // Remove the trailing comma:
        if (revisedRecipients.length > 1) {
            revisedRecipients = revisedRecipients.substring(0, revisedRecipients.length - 1);
        }

        // Return the results as an object:
        var objResults = {
            "safeList": revisedRecipients,
            "badAddress": foundBad
        };

        return objResults;

    },
   

    type: 'verifyEmailDomain'
};

 

And calling it from the business rule to evaluate any string containing email addresses:

    var si = new verifyEmailDomain();
    
    var emailField = si.stripRecipients(targetField, whiteListProperty);

You will get a reusable code in which the returned object has two properties:

emailField.safeList

emailField.badAddress

 

 

 

Cheers,

Mariano

Venkatesh A K1
Mega Expert

After Paris Release "System Address Filter" has been introduced which will help in making the domains whitelist OR block based on outbound OR inbound .

Steps as follows ::

1. Create a Email address filter ( System mailbox --> Email address filters)
2. you can mention the domains which can be allow OR deny .

Then 

3. Create a system address filter ( System mailbox --> system address filter )

4. Select Type Inbound OR Outbound 

5. add the Email address filter which you have created in above steps and submit.

Both types of filtering can be enabled which you have configured via email address:

  • Allow (or whitelist) 
  • Deny (or blacklist) 

SN Doc is FYI :: 

https://docs.servicenow.com/bundle/paris-platform administration/page/administer/notification/concept/system-address-filters.html

Mark as helpful/correct if it is correct.

Version history
Last update:
‎09-11-2018 04:57 AM
Updated by: