Problem Descriptions:

Many Dynamics CRM applications have automatically generated emails for business workflow notifications. When an entity’s email address column contains both internal users and external customers or when email recipient logic requires gathering email addresses from multiple entities that contain both internal users and external customers, there will be a risk that internal communications are accidentally sent to external recipients. The outcome could be either embarrassing or causing legal complications. 

 

Typical Solutions:

1. When retrieving email address by internal Alias column, making sure to verify the retrieved email addresses are with internal email domains.

2. For internal communication emails, adding logic in the PreEmailCreate and PreEmailSendFromTemplate plugins to remove any external email addresses from TO, CC, and BCC fields.

These solutions are very helpful; but the problem could still occur occasionally, due to the following reasons:

  • It requires all the logics that are scattered in several different places to work together.
  • It lacks contingency protection when some of the logic above failed.
  • It is more susceptible to future business rule change, because the logics of preventing illegitimate recipients are tightly mixed with other email generation business rules.

 

Extra Safety:

To add extra protection, we can use a custom send email provider for the Dynamics CRM Email Router to remove external recipients from internal emails. This approach is separated from email-generation logic and can provide logging for later investigation when events of illegitimate recipient removal occur. 

 

Development:

Dynamics CRM Email Router Extensibility is documented here. The Email Provider object model can be found here. Our goal is to create a custom class to inherit from “CrmPollingSendEmailProvider” that will interact with Email Router and another custom class to inherit from “SmtpPollingSendEmailProvider” to reuse all of its SMTP sender functionalities.

We first create a C# project named “MyEmailLegalPolicyEnforcer” using Class Library template to implement our illegitimate recipients removal policy.

 

public class MyEmailLegalPolicyEnforcer
{
    #region data
    StringCollection _systemSenders;
    string _legalPolicyLogFolder;
    #endregion data

    #region constructor
    public MyEmailLegalPolicyEnforcer(StringCollection systemSenders, string legalpolicyLogFolder)
    {
        _systemSenders = systemSenders;
        _legalPolicyLogFolder = legalpolicyLogFolder;
    }
    #endregion constructor

    #region public method
    public Entity Execute(Entity emailMessage)
    {
        if (IsSystemGenerated(emailMessage))
        {
            return RemoveExternalEmail(emailMessage);
        }
        else
        {
            return emailMessage;
        }
    }       
    #endregion public method

When an illegitimate recipient is removed, we log the event in a file, whose path is passed to the constructor. Dynamics CRM allows user to send emails manually. To apply our policy only to application-generated notifications, we need to know the system sender email addresses, which is passed to the other parameter - “systemSenders” – of the constructor. In Dynamics CRM, the the ActivityParty.AddressUsed attribute is where email address is set and the method IsSystemGenerated below shows its usage. 

 

#region private method
private bool IsSystemGenerated(Entity emailMessage)
{
    if (emailMessage.Contains("from"))
    {
        foreach (Entity a in ((EntityCollection)emailMessage.Attributes["from"]).Entities)
        {
            if (a.Contains("addressused"))
            {
                if (_systemSenders.Contains(a.Attributes["addressused"].ToString().ToLower()))
                         return true;
            }
        }
    }
    return false;
}

private Entity RemoveExternalEmail(Entity emailMessage)
{
    EntityCollection removedFromTo = null;
    EntityCollection removedFromCc = null;
    EntityCollection removedFromBcc = null;
    if (emailMessage.Contains("to"))
    {
        removedFromTo = RemovedExternalEmailAddresses((EntityCollection)emailMessage.Attributes["to"]);
    }
    if (emailMessage.Contains("cc"))
    {
        removedFromCc = RemovedExternalEmailAddresses((EntityCollection)emailMessage.Attributes["cc"]);
    }
    if (emailMessage.Contains("bcc"))
    {
        removedFromBcc = RemovedExternalEmailAddresses((EntityCollection)emailMessage.Attributes["bcc"]);
    }
    if ((removedFromTo != null && removedFromTo.Entities.Count > 0) ||
        (removedFromCc != null && removedFromCc.Entities.Count > 0) ||
        (removedFromBcc != null && removedFromBcc.Entities.Count > 0))
    {
        LogIllegalEmail(emailMessage, removedFromTo, removedFromCc, removedFromBcc);
    }
    return emailMessage;
}

private void LogIllegalEmail(Entity message, EntityCollection removedFromTo, EntityCollection removedFromCC, EntityCollection removedFromBcc)
{
    StringBuilder sb = new StringBuilder();
    sb.Append("ActivityId: ").Append(message.Attributes["activityid"].ToString()).Append(Environment.NewLine);;
    if (removedFromTo != null && removedFromTo.Entities.Count > 0)
    {
        sb.Append("Removed External Address in To: ");
        WriteAddressesOfEntities(removedFromTo.Entities, sb);
        sb.Append(Environment.NewLine);
    }
    if (removedFromCC != null && removedFromCC.Entities.Count > 0)
    {
        sb.Append("Removed External Address in Cc: ");
        WriteAddressesOfEntities(removedFromCC.Entities, sb);
        sb.Append(Environment.NewLine);
    }
    if (removedFromBcc != null && removedFromBcc.Entities.Count > 0)
    {
        sb.Append("Removed External Address in Bcc: ");
        WriteAddressesOfEntities(removedFromBcc.Entities, sb);
        sb.Append(Environment.NewLine);
    }
    sb.Append(Environment.NewLine);

    sb.Append("From: ");
    WriteAddressesOfEntities((EntityCollection)message.Attributes["from"], sb);
    sb.Append(Environment.NewLine);
    sb.Append("SentOn: ").Append(DateTime.Now).Append(Environment.NewLine);
    sb.Append("To: ");
    WriteAddressesOfEntities((EntityCollection)message.Attributes["to"], sb);
    sb.Append(Environment.NewLine);
    sb.Append("CC: ");
    WriteAddressesOfEntities((EntityCollection)message.Attributes["cc"], sb);
    sb.Append(Environment.NewLine);
    sb.Append("Bcc: ");
    WriteAddressesOfEntities((EntityCollection)message.Attributes["bcc"], sb);
    sb.Append(Environment.NewLine);
    sb.Append("Subject: ").Append(message.Attributes["subject"].ToString()).Append(Environment.NewLine);
    sb.Append(message.Attributes["description"].ToString()).Append(Environment.NewLine);
    WriteLog(sb.ToString());
}

Then, we create another C# project, named “MySmtpPollingSendEmailProvider”, using Class Library template and add references to “Microsoft.Crm.Tools.EmailProviders.dll”, “microsoft.xrm.sdk.dll”, and the “MyEmailLegalPolicyEnforcer” project above. This project will contain two classes. The first one is “MySmtpPollingSendEmailProvider”, where we execute MyEmailLegalPolicyEnforcer in the “ValidateMessageInternal” method and pass the resulting message to the corresponding method in the parent class SmtpPollingSendEmailProvider . 

 

public class MySmtpPollingSendEmailProvider : SmtpPollingSendEmailProvider
{
    #region data
    private MyEmailLegalPolicyEnforcer.MyEmailLegalPolicyEnforcer _legalPolicy;
    #endregion data

    #region constructor
    public MySmtpPollingSendEmailProvider(ProviderConfiguration providerConfiguration,
        ManualResetEvent shutdownEvent, StringCollection systemSenders, string legalPolicyLogFolder)
        : base(providerConfiguration, shutdownEvent)
    {
        _legalPolicy = new MyEmailLegalPolicyEnforcer.MyEmailLegalPolicyEnforcer(systemSenders, legalPolicyLogFolder);
    }
    #endregion constructor

    #region public method       
    public new void ValidateMessageInternal(Entity emailMessage)
    {
        base.ValidateMessageInternal(_legalPolicy.Execute(emailMessage));
    }
    public new void NotifySenderUndeliverableMessage(Entity emailMessage, Collection<string> failedRecipients)
    {
        base.NotifySenderUndeliverableMessage(emailMessage, failedRecipients);
    }
    public new void ProcessMessageInternal(Entity emailMessage)
    {
        base.ProcessMessageInternal(emailMessage);
    }
    public new Collection<string> SaveFailedRecipients(Exception exception)
    {
        return base.SaveFailedRecipients(exception);
    }
    #endregion public method
}

The second one is “MyCrmPollingSendEmailProvider”, which is a straightforward concrete implementation of the abstract class “CrmPollingSendEmailProvider”. Here we simply instantiate the MySmtpPollingSendEmailProvider class and call its corresponding methods.

 

using Microsoft.Crm.Tools.Email.Providers;
using Microsoft.Xrm.Sdk;
using System;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Threading;

namespace MySmtpPollingSendEmailProvider
{
    public class MyCrmPollingSendEmailProvider : CrmPollingSendEmailProvider
    {
        #region data
        private MySmtpPollingSendEmailProvider _smtpProvider;
        private StringCollection _systemSenders = new StringCollection();
        #endregion data

        #region constructor
        public MyCrmPollingSendEmailProvider(ProviderConfiguration providerConfiguration, ManualResetEvent shutdownEvent)
            : base(providerConfiguration, shutdownEvent, "PollingPeriod", "MaxMessageCount")
        {
            string systemSenders = providerConfiguration.ConfigNodeReader.GetCheckedStringValue("SystemEmailSenders", "serviceAccount@mycompany.com", true);
            string legalPolicyLogFolder = providerConfiguration.ConfigNodeReader.GetCheckedStringValue("LegalPolicyLogFolder", @"C:\", true);           
            string[] systemSendersArray = systemSenders.ToLower().Split(new Char[] { ';', ',' });
            _systemSenders.AddRange(TrimAllStrings(systemSendersArray));
            if (!legalPolicyLogFolder.EndsWith(@"\"))
            {
                legalPolicyLogFolder = legalPolicyLogFolder + @"\";
            }
            _smtpProvider = new MySmtpPollingSendEmailProvider(providerConfiguration, shutdownEvent, _systemSenders, legalPolicyLogFolder);
        }       
        #endregion constructor

        #region override abstract methods of base class
        protected override void NotifySenderUndeliverableMessage(Entity emailMessage, Collection<string> failedRecipients)
        {
            _smtpProvider.NotifySenderUndeliverableMessage(emailMessage, failedRecipients);
        }
        protected override void ProcessMessageInternal(Entity emailMessage)
        {
            _smtpProvider.ProcessMessageInternal(emailMessage);
        }
        protected override Collection<string> SaveFailedRecipients(Exception exception)
        {
            return _smtpProvider.SaveFailedRecipients(exception);
        }
        #endregion override abstract methods of base class

        #region override virtual methods of base class
        protected override void ValidateMessageInternal(Entity emailMessage)
        {
            _smtpProvider.ValidateMessageInternal(emailMessage);
        }
        #endregion override virtual methods of base class

        #region private methods
        private string[] TrimAllStrings(string[] stringArray)
        {
            string[] toReturn = new string[stringArray.Length];
            for (int i = 0; i < stringArray.Length; i++)
            {
                toReturn[i] = stringArray[i].Trim();
            }
            return toReturn;
        }
        private string[] LowerCaseAllStrings(string[] stringArray)
        {
            string[] toReturn = new string[stringArray.Length];
            for (int i = 0; i < stringArray.Length; i++)
            {
                toReturn[i] = stringArray[i].ToLower();
            }
            return toReturn;
        }
        #endregion private methods
    }
}

 

Deployment:

The steps of deployment is documented here. Our solution above generates two assemblies, MySmtpPollingSendEmailProvider.dll and MyEmailLegalPolicyEnforcer.dll. They need to be placed in in the %program files%\Microsoft CRM Email\Service folder on the server where the E-mail Router is installed. Then, we need to insert our custom provider configuration settings into the Microsoft.Crm.Tools.EmailAgent.xml file, located in the Service folder. The important elements are highlighted in colors in the example below.

 

<?xml version="1.0" encoding="utf-8"?>
<Configuration>
  <SystemConfiguration>
    <MaxThreads>50</MaxThreads>
    <MaxThreadExecution>600000</MaxThreadExecution>
    <SchedulingPeriod>1000</SchedulingPeriod>
    <ConfigRefreshPeriod>5000000</ConfigRefreshPeriod>
    <ConfigUpdatePeriod>0</ConfigUpdatePeriod>
    <LogLevel>1</LogLevel>
    <ProviderOverrides>
      <CacheCapacity>1024</CacheCapacity>
      <PendingStatusDelay>300000</PendingStatusDelay>
      <SendingStatusDelay>1800000</SendingStatusDelay>
      <MaximumDeliveryAttempts>10</MaximumDeliveryAttempts>
      <EWSRetrieveMessageCount>10</EWSRetrieveMessageCount>
      <BatchSize>5</BatchSize>
      <RequestBatchSize>5</RequestBatchSize>
    </ProviderOverrides>
  </SystemConfiguration>
  <ProviderConfiguration>
    <ProviderAssembly>MySmtpPollingSendEmailProvider.dll</ProviderAssembly>
    <ProviderClass>MySmtpPollingSendEmailProvider.MyCrmPollingSendEmailProvider</ProviderClass>
    <CrmServerUrl>
http://www.mycompany.com/myOrg</CrmServerUrl>
    <CrmAuthMode>WindowsAuthentication</CrmAuthMode>
    <CrmUser>mydomain\serviceaccount</CrmUser>
    <CrmPassword>{2A48C4DB-F2BF-48DF-A8EF-20F531EA9BAA}:WtvPFKnG763/RsjwMg5riw==@BHK1gIcQznhdo4ZeqCCBOQ==</CrmPassword>
    <EmailServer>smtphost.mycompany.com</EmailServer>
    <EmailAuthMode>WindowsAuthentication</EmailAuthMode>
    <EmailUser>mydomain\myuser</EmailUser>
    <EmailPassword>{2A48C4DB-F2BF-48DF-A8EF-20F531EA9BAA}:wkZRUfLEHe+kpgajzltn3A==@BI+CzJWzD602q1sIND464A==</EmailPassword>
    <UserId>4a5e65af-e8a9-e111-adce-00155dc94858</UserId>
    <UserId>84b8c25f-eca9-e111-adce-00155dc94858</UserId>
    <Target>smtphost.mycompany.com</Target>
    <Direction>Outbound</Direction>
    <CacheCapacity>1024</CacheCapacity>
    <ConnectionTimeout>300000</ConnectionTimeout>
    <PollingPeriod>60000</PollingPeriod>
    <MaxMessageCount>1000</MaxMessageCount>
    <EmailPort>25</EmailPort>
    <EmailUseSsl>false</EmailUseSsl>
    <UseAutoDiscover>false</UseAutoDiscover>
    <DeliveryMethod>Network</DeliveryMethod>
    <PendingStatusDelay>300000</PendingStatusDelay>
    <SendingStatusDelay>1800000</SendingStatusDelay>
    <CodePage>Utf-8</CodePage>
    <MaximumDeliveryAttempts>10</MaximumDeliveryAttempts>
    <BatchSize>5</BatchSize>
    <RequestBatchSize>5</RequestBatchSize>
    <AccessCredentials>Other</AccessCredentials>
    <SystemEmailSenders>app1@mycompany.com;app2@mycompany.com;app3@mycompany.com</SystemEmailSenders>
    <LegalPolicyLogFolder>
C:\EmailLegalPolicyEnforcerLog</LegalPolicyLogFolder>
  </ProviderConfiguration>
</Configuration>

The values of <ProviderAssembly> and <ProviderClass> are now replaced with corresponding values of our custom send email provider. The <ConfigUpdatePeriod> must be set to 0 so that the <ProviderAssembly> and <ProviderClass> values will not be reset back to default values when service restarts. The <SystemEmailSenders> and <LegalPolicyLogFolder> are new elements added for our solution. They are read in the constructor of the MyCrmPollingSendEmailProvider class.

 

The meaning of each elements in the SystemConfiguration section can be found here. The meaning of each elements in the ProviderConfiguration section can be found here.

 

Tracing:

By adding <LogFile> tag and setting <LogLevel> value to 3, we can get detailed log from CRM Email Router in the specified LogFile. The file can then be provided to others for help with troubleshooting.

 

<?xml version="1.0" encoding="utf-8"?>
<Configuration>
  <SystemConfiguration>
    <MaxThreads>50</MaxThreads>
    <MaxThreadExecution>600000</MaxThreadExecution>
    <SchedulingPeriod>1000</SchedulingPeriod>
    <ConfigRefreshPeriod>5000000</ConfigRefreshPeriod>
    <ConfigUpdatePeriod>0</ConfigUpdatePeriod>
    <LogLevel>3</LogLevel>
    <LogFile>c:\temp\trace.txt</LogFile>
    <ProviderOverrides>
      <CacheCapacity>1024</CacheCapacity>
      <PendingStatusDelay>300000</PendingStatusDelay>
      <SendingStatusDelay>1800000</SendingStatusDelay>
      <MaximumDeliveryAttempts>10</MaximumDeliveryAttempts>
      <EWSRetrieveMessageCount>10</EWSRetrieveMessageCount>
      <BatchSize>5</BatchSize>
      <RequestBatchSize>5</RequestBatchSize>
    </ProviderOverrides>
  </SystemConfiguration>