Tuesday, August 7, 2012

Creating a Custom E-mail Notification Solution for SharePoint 2010

In this post I outline a possible solution for a custom e-mail notification system built on SharePoint Server 2010.

Requirements

  • The notification system must allow for fully branded and customized e-mails.
  • Administrators and Designers must be allowed to easily modify branding elements and content within each e-mail notification.
  • Each e-mail notification must allow for personalized dynamic content (for example, recipient name, address, etc...).
  • Some e-mail notifications will be time delayed or scheduled.
Our task is to create a scalable n-tier code solution that will allow other developers to easily generate custom e-mails using predefined templates.

High Level Process Flow

The high level process flow will look something like this:


Some type of web based activity on SharePoint 2010 will trigger the need for a notification. We don't really care what the activity is for our task. We just need to provide other developers some way of plugging into the e-mail notification framework that we are building. Once an activity is triggered, we need to know two things (these are our inputs):
  1. Which e-mail template to use
  2. The values of any personalized properties for the notification (for example, the e-mail address to which the e-mail will be sent).

Data Layer

You could probably set this up a hundred different ways. For my purposes and for this example, I created two tables and a SharePoint document library.

SharePoint Document Library

The SharePoint document library stores an html file for each e-mail template. The example file I will use is a temporary password e-mail. The file, called TemporaryPasswordEmailTemplate.html looks like something this:

[ a bunch of branding stuff]
Your temporary password is: #!#TEMPORARYPASSWORD#!#
[further instructions, disclaimers, and more branding]

#!#TEMPORARYPASSWORD#!# is the replacement key. The code will look for and replace that key with the actual temporary password at the time the e-mail is generated.

Also in the SharePoint document library is a column for the template name. In this example, the template name is TempPassword. The template name in the document library tells the code which replacement keys to look for.

EmailTemplateConfiguration Table

The EmailTemplateConfigurationTable has two relevant columns:
  • TemplateId: the template id
  • Key: the name of a replacement key contained in the template
In this example we have one entry (but there could be as many as needed):
  • TemplateId: 2
  • Key: TemporaryPassword

Email Table

This table stores all outgoing e-mails. A timer job picks up any unsent e-mail and sends it. It has columns for To, From, Subject, Body, ScheduledSendTime, ActualSendTime, etc...

Base Layer

The base layer contains an enum for EmailTemplateType and an abstract class that the business layer will inherit. The abstract class does the heavy lifting.

EmailTemplateType Enum

First, I create an enum for the e-mail template types. Right now, we just have two (an undefined default type and an e-mail type for sending out a temporary password).

    public enum EmailTemplateType : int
    {
        Undefined = 0,
        TempPassword = 1
    }

CustomEmailMessage Abstract Class

Next I create an abstract class that will do most of the work of constructing the e-mails. The idea is that a new class will be created for each e-mail type (the implementation classes). Each new class will inherit the core functionality from the abstract class. The implementations will set the template type in the constructor. Properties on each of the implementations will set the values of dynamic content (if any). The implementations will then call the SendMail method (added later in this post) to do the work of constructing the e-mail and sending it out.

    public abstract class CustomEmailMessage : MailMessage
    {
         //Placeholder
    }

Note that the CustomEmailMessage abstract class inherits from System.Net.Mail.MailMessage. We will use the functionality in that class to set the To, From, Subject, and Body properties rather than create them ourselves.
Next I add the appropriate properties to the abstract class:
   
    public abstract class CustomEmailMessage : MailMessage
    {     
        private EmailTemplateType _type = EmailTemplateType.Undefined;
        private StringDictionary _replacementTags = new StringDictionary();
        private DateTime _sendDateTime = DateTime.Now;
        
        public EmailTemplateType TemplateType
        {
            get { return _type; }
            set { _type = value; }
        }
    }

The _type will be used to determine the e-mail template that we are going to use. The constructor of the implementation class will set the TemplateType property. Notice that it is of type EmailTemplateType (the enum created above).

The _replacementTags StringDictionary will be used to place personalized content in the e-mail templates. Each e-mail template with personalized content will contain placeholder keys. The placeholder keys will correspond to properties in the implementation class. At run time, the implementation class will set those properties and, when the SendEmail method is called, the base class will match up the properties with the appropriate keys in the template.

The _sendDateTime will be used to queue up e-mails that need to be scheduled for a certain time in the future.

Next I stub out the methods:
   
    public abstract class CustomEmailMessage : MailMessage
    {     
        private EmailTemplateType _type = EmailTemplateType.Undefined;
        private StringDictionary _replacementTags = new StringDictionary();
        private DateTime _sendDateTime = DateTime.Now;
        
        public EmailTemplateType TemplateType
        {
            get { return _type; }
            set { _type = value; }
        }
        
        public virtual void GetReplacementKeys()
        {
            //Placeholder
        }

        public virtual void GetReplacementValues(object sender)
        {
            //Placeholder
        }

        public virtual void GetTemplateHtml()
        {
            //Placeholder
        }

        public virtual void ReplaceTags()
        {
            //Placeholder
        }

        public virtual void WriteEmailToDB()
        {
            //Placeholder
        }

        public virtual void SendEmail()
        {
            //Placeholder
        }
    }

GetReplacementKeys
GetReplacementKeys does just as the name suggests. It populates _replacementTags with the appropriate keys. It's a StringDictionary so it is stored in key/value pairs. At this point we are just populating the keys. I won't include the code here, because I have a data layer with another level of abstraction that doesn't suit this walk through. The simplest way to do this, though is to create a SharePoint list with a column for TemplateType (_type) and a column for Key. Use the _type to pull the appropriate keys and add them to the StringDictionary _replacementTags.

GetReplacementValues
This method is a little trickier. We are going to use reflection to get the values of the implementing method's properties and match them up with the keys in _replacementTags. The code looks like this:
    public virtual void GetReplacementValues (object sender)
    {
        PropertyInfo[] propertyInfos = this.GetType().GetProperties();
        foreach (PropertyInfo propertyInfo in propertyInfos)
        {
             object _internal = propertyInfo.GetValue(sender, null);
             if (_internal != null)
             {
                  if(_internal.GetType() == typeof(string))
                  {
                       _replacementTags[Globals.Delimiter + 
                           propertyInfo.Name.ToLowerInvariant() +
                           Globals.Delimiter] =
                           (string)_internal;
                  }
             }
        }           
    }

The caller of this method is actually the SendEmail method (which we will get to shortly). It passes in "this" as the sender object used in the GetReplacementValues method above.

Next, I get all the properties of the implementation object and put them into a PropertyInfo array. I enumerate through the array and extract the values from the sender object. As long as the values are not null and are of type string, I add them to the _replacementTags StringDictionary using the property name (plus a delimiter) as the key.

So this is important...in this setup, the property names on the implementation object must match the keys stored for the EmailTemplateType and they must match the replacement values in the html markup.

Note Globals.Delimiter. I have a class in my code for global variables. I have to use some kind of delimiter in the html markup to denote a replacement key. In this example, our delimiter is "#!#". The markup includes the delimiter but the stored keys don't. I add it here.

GetTemplateHtml
This method is pretty simple...go get the template html file and put it in the Body property (the Body property is part of the inherited MailMessage class. I store the template html files in a document library with a custom column to denote which EmailTemplateType the file corresponds to. Sparing you all the code to get the appropriate document library item, here is the most important line:
    Body = web.GetFileAsString(items[0].File.ToString());
ReplaceTags
Once I have all the appropriate template in the Body property and I have the key/value pairs in the _replacementTags StringDictionary, it's time to actually replace the tags:

    public virtual void ReplaceTags()
    {
        foreach (string key in _replacementTags.Keys)
        {
             Body = Body.Replace(key.ToUpperInvariant(), _replacementTags[key]);
        }
    }

WriteEmailToDB
Now I write the e-mail to a SQL database and a timer job picks it up and sends it.
    public virtual void WriteEmailToDB()
    {
        emailHelper.WriteToDB(To.ToString(), From.ToString(), Subject, Body,
            _sendDateTime);
    }

Note that I use a helper class. I don't include the code that actually writes the data to the table in this post.

SendEmail
Finally, the SendEmail method pulls it all together:

    public virtual void SendEmail(DateTime sendDateTime)
    {
        _sendDateTime = sendDateTime;

        GetReplacementKeys();
        GetReplacementValues(this);
        GetTemplateHtml()l
        ReplaceTags();
        WriteEmailToDB();
    }

Business Layer

Every time I create a new e-mail type, I will add an entry to the EmailTemplateType enum, create appropriate rows on the EmailTemplateConfiguration table, add the template html to the SharePoint document library, and create a class in the business layer that inherits from the CustomEmailMessage abstract class. For this example, I create a Temporary Password e-mail. My business layer implementation looks like this:
    public class TempPasswordEmail : CustomEmailMessage
    {
        public string TemporaryPassword { get; set; }
        
        public TempPassword()
        {
             TemplateType = EmailTemplateType.TempPassword;
             base.Subject = "Your Temporary Password";
             base.From = new MailAddress("SharePointDoug@doughemminger.com");
        }
    }

In this example, I know that the From and the Subject are going to be the same for every instance of the TempPassword class. So, I set them in the constructor.

UI Layer

When the user clicks a button to request a temporary password, the process triggers a notification to the user. Assuming the temporary password is appropriately generated and stored in the variable "x" and the user's email address is stored in the variable "userEmail" we would trigger a notification like this:

    TempPasswordEmail tempPasswordEmail = new TempPasswordEmail();
    tempPasswordEmail.To = userEmail;
    tempPasswordEmail.TemporaryPassword = x;
    tempPasswordEmail.SendEmail(DateTime.Now);

Wrap-Up

There are a few disadvantages and things to note about this approach:
  • A new e-mail type (for example, if we were to add an e-mail type for workflow notifications), requires a code change. An enum value has to be added at Base layer and implementations have to be added at the business layer and possibly at the UI layer.
  • I intentionally left out all of the try/catches and RunWithElevatedPrivileges. Add where appropriate if you use this code.
  • The properties in the business layer implementations must match the Keys stored in the data layer. In this example TemporaryPassword matches exactly at the business layer and the data layer
  • The replacement keys in the html markup must be all uppercase. In this example, #!#TEMPORARYPASSWORD#!# is all uppercase.
There are many advantages to this approach:
  • New e-mail types are added relatively easily (even though they require a code change). And once a new e-mail type is created, it can be easily implemented wherever you need it.
  • Storing the e-mail templates as html in a document library allows designers to mock-up and make changes to the templates as needed. In my real-world implementation we use header and footer html files to store CSS, logo, and disclaimer information for all e-mails.
  • It is fairly straightforward to implement dynamic content by using replacement keys in the html markup and the e-mail implementations.
  • Any of the methods in the abstract class can be overridden. If for, example, there were a use case to bypass the timer job and send the e-mail immediately, you could override the SendMail method, call all of the appropriate methods that are already there except the WriteEmailToDB method.
 
 

4 comments:

  1. I'll pass this on to some folks that had this need recently. Good post!

    ReplyDelete
  2. Looks like a good item for a codeplex project. Have you ever enhanced this or created any other plugin systems for sharepoint?

    ReplyDelete
  3. Anonymous, I have created tons of add ons and plugins for SharePoint. Unfortunately they are either proprietary or I have difficulty finding the time to blog/publish them. I plan to do more though, so keep an eye on the blog. :)

    ReplyDelete