Sitecore Custom Workflow Email Action

Workflows are one of the most powerful tools in the Sitecore toolkit. An interesting request we recently had was to build a custom workflow action that will send email to a different mailing group/groups based on the location and the language of the item moved to next step of the workflow. At first it seemed like a complex task but as in many cases РSitecore is pretty extensible. In this blog post I will cover the simplest possible implementation where we will just send the email to a different mailing group or email. The build should be pretty extensible, so feel free to extend based on it. There are three main steps required for the component implementation:

  1. Create set of templates that will allow the users to control the rules.
  2. Create a new save action template that will build on top of the existing email action.
  3. Code it ūüôā

Rule Engine (Kind of :))

For the rule engine we will need 2 templates Рone folder template will just store our rules and one rule template. The rule item should give the administrators a chance to pick a start path(from where the items will count as a members of the section) and a possibility to pick languages in which the approvers should be able to move the the item to the next step of the workflow, and finally a list of emails. To be consistent, we are going to add the templates to /sitecore/templates/System/Workflow

So here is an example of how the rule template should look like:

Rule Template

The next decision we have to make is where to place the rules in the content tree. I usually don`t like adding items to the /sitecore/system node if it is not absolutely necessary, but it seems like a good location to place them. Another good option is to place them under the settings node of the website (if there is one). In this example I will place them under the /sitecore/system node like this:

Workflow Rules

Save Action Template

The EmailActionEx should be similar to the existing sitecore/templates/System/Workflow/Email action. The only addition is adding our custom parameter, so the user will be able to pick rules for the action. The save action should look like this:

Email Action Ex

Keep in mind that the Rules DataSource might vary based on where you decided to place your rules folder.

Code Time ! ūüôā

When coding the custom action we need to make sure that we keep the existing token functionality of the default functionality of the existing save action (like tokens etc). Snippet of the source code can be found below:


using System;
using System.Collections.Generic;
using System.Linq;
using Sitecore.Data.Fields;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using System.Net.Mail;
using Sitecore.Globalization;
using Sitecore.Workflows.Simple;
 
namespace Sitecore.Workflows.Web.Workflows
{
    public class SendEmailExAction
    {
        public void Process(WorkflowPipelineArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
 
            ProcessorItem processorItem = args.ProcessorItem;
 
            if (processorItem == null)
            {
                return;
            }
 
            Item innerItem = processorItem.InnerItem;
 
            string fullPath = innerItem.Paths.FullPath;
 
            List<string> recipents =
                GetItems(innerItem, "rules")
                    .Where(x => ShouldSendEmail(args.DataItem, x))
                    .SelectMany(x => x.Fields["Emails"].Value.Split(';'))
                    .ToList();
 
            if (recipents.Any())
            {
 
                string from = GetText(innerItem, "from", args);
                string mailServer = GetText(innerItem, "mail server", args);
                string subject = GetText(innerItem, "subject", args);
                string message = GetText(innerItem, "message", args);
 
                Error.Assert(from.Length > 0, "The 'From' field is not specified in the mail action item: " + fullPath);
                Error.Assert(subject.Length > 0,
                    "The 'Subject' field is not specified in the mail action item: " + fullPath);
                Error.Assert(mailServer.Length > 0,
                    "The 'Mail server' field is not specified in the mail action item: " + fullPath);
 
                MailMessage mailMessage = new MailMessage();
 
                mailMessage.From = new MailAddress(from);
                mailMessage.Subject = subject;
                mailMessage.Body = message;
 
                foreach (string recipent in recipents)
                {
                    mailMessage.To.Add(new MailAddress(recipent));
                }
 
                SmtpClient client = new SmtpClient(mailServer);
 
                try
                {
                    client.Send(mailMessage);
                }
                catch (Exception ex)
                {
                    Log.Error("EmailExAction Threw An Exception", ex, this);
                }
            }
        }
 
        ///

<summary>
        /// Gets the text.
        /// 
        /// </summary>


        /// <param name="commandItem">The command item.</param><param name="field">The field.</param><param name="args">The arguments.</param>
        /// <returns/>
        private string GetText(Item commandItem, string field, WorkflowPipelineArgs args)
        {
            string text = commandItem[field];
            return text.Length > 0 ? ReplaceVariables(text, args) : string.Empty;
        }
 
        ///

<summary>
        /// Replaces the variables.
        /// 
        /// </summary>


        /// <param name="text">The text.</param><param name="args">The arguments.</param>
        /// <returns/>
        private string ReplaceVariables(string text, WorkflowPipelineArgs args)
        {
            text = text.Replace("$itemPath$", args.DataItem.Paths.FullPath);
            text = text.Replace("$itemLanguage$", args.DataItem.Language.ToString());
            text = text.Replace("$itemVersion$", args.DataItem.Version.ToString());
            return text;
        }
 
        private bool ShouldSendEmail(Item dataItem, Item ruleItem)
        {
            return IsItemMatch(dataItem, ruleItem) && IsLanguageMatch(dataItem, ruleItem);
        }
 
        private bool IsItemMatch(Item dataItem, Item ruleItem)
        {
            Item ruleStartItem = GetStartItem(ruleItem);
 
            return ruleStartItem == null || IsAncestor(dataItem, ruleStartItem);
        }
 
        private bool IsLanguageMatch(Item dataItem, Item ruleItem)
        {
            List<Language> languages = GetLanguages(ruleItem).ToList();
 
            return !languages.Any() || languages.Contains(dataItem.Language);
        }
 
        private IEnumerable<Language> GetLanguages(Item ruleItem)
        {
            MultilistField selectedLanguages = ruleItem.Fields["Languages"];
 
            if (selectedLanguages != null && selectedLanguages.TargetIDs.Any())
            {
                return selectedLanguages.GetItems().Select(x => Language.Parse(x.Name));
            }
 
            return Enumerable.Empty<Language>();
        }
 
        ///

<summary>
        /// Gets the items
        /// </summary>


        /// <param name="commandItem"></param>
        /// <param name="field"></param>
        /// <returns></returns>
        private IEnumerable<Item> GetItems(Item commandItem, string field)
        {
            MultilistField rules = commandItem.Fields[field];
 
            if (rules != null && rules.TargetIDs.Any())
            {
                return rules.GetItems();
            }
 
            return Enumerable.Empty<Item>();
        }
 
        private Item GetStartItem(Item ruleItem)
        {
            ReferenceField startPathField = ruleItem.Fields["StartPath"];
 
            if (startPathField != null && startPathField.TargetItem != null)
            {
                return startPathField.TargetItem;
            }
 
            return null;
 
        }
 
        private bool IsAncestor(Item currentItem, Item ancestor)
        {
            if (currentItem.ID == ancestor.ID)
            {
                return true;
            }
 
            if (currentItem.Parent != null)
            {
                return IsAncestor(currentItem.Parent, ancestor);
            }
 
            return false;
        }
    }
}

In the code we need to check if the item matches both rules for language and location. If one or more of the rules are matched – we send the email to the corresponding emails. Otherwise we skip them. If there is no match – no email is sent.

And that is it ! Now we have our custom email action that can send items based on the item location and the item language !

The Sample Project can be found on BitBucket.

For people without TDS the package can be downloaded separately from here: SitecoreWorkflows-1.0.zip

Happy Workflowling !

2 thoughts on “Sitecore Custom Workflow Email Action”

  1. For SMTP mail server settings, I would maintain this information in the Sitecore config section in the web.config instead of from a Sitecore item. Sitecore items can get published and that may cause problems for different Sitecore environments needing different SMTP settings.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s