Monday 4 April 2016

Sitecore PXM and ODG project (part 4 – customising email)

  1. Introduction
  2. Non-editable fields
  3. Architecture
  4. Customising ODG email
  5. PXM InDesign custom task

In the next couple of posts I'd like to focus on some of the customisations that can be made to improve or build upon the functionality provided by PXM and ODG out of the box.

Let's start with something simple: renaming the PDF file that's output from ODG.  Fortunately for us, Sitecore has made life a little easier by checking whether the name has already been set before setting its own, which means all we have to do is insert our "naming" processor before the others. If we look at Sitecore.PrintStudio.config, we can see in the <printPreview>, <printToDashboard> and <printToInDesign> pipelines it's the Sitecore.PrintStudio.PublishingEngine.Pipelines.RenderProjectXmlargs.PrintOptions.ResultFileName, so let's insert our processor before this one. In this case I only care about the <printToDashboard> pipeline, but the others will be the same.
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <printToDashboard>
        <processor
          patch:before="processor[@type='Sitecore.PrintStudio.PublishingEngine.Pipelines.RenderProjectXml, Sitecore.PrintStudio.PublishingEngine']"
          type="Custom.Pipelines.NameOutputFiles, Custom"/>
      </printToDashboard>
    </pipelines>
  </sitecore>
</configuration>

And the processor to set the name
public class NameOutputFiles : Sitecore.PrintStudio.PublishingEngine.Pipelines.IPrintProcessor
{
  public void Process(PrintPipelineArgs args)
  {
    args.PrintOptions.ResultFileName = Utils.GenerateFilename(Context.User, args.ProcessorItem.InnerItem.ID.Guid) + args.PrintOptions.ResultExtension;
  }

  public static string GenerateFilename(User user, Guid itemId)
  {
    return string.Concat(user.LocalName, "_", itemId, "_", DateTime.UtcNow.ToString("yyyy-MM-dd-HH-mm-ss-fff"));
  }
}

In this case I've separated the code generating the name into a separate method in case we need to re-use the same naming method elsewhere in the code (for example to generate a link to the document).

Now let's look at how we can add additional recipients to the email that ODG sends out once the document has been exported.  By default it sends an email to notify the user (who generated the document) that their document is ready, with a link to the document.  What if we want this to be CCd or BCCd to for example their boss, or a support email?

For our example, let's add a support email as a BCC to all emails sent.

The code which handles sending the email is in the Dashboard Service, however the code which tells the Dashboard Service which email(s) to use is in the printToDashboard pipeline, in the SendToDashboard class, so let's extend that and switch it out in a patch:
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <printToDashboard>
        <processor 
          patch:instead="processor[@type='Sitecore.PrintStudio.PublishingEngine.Pipelines.Dashboard.SendToDashboard, Sitecore.PrintStudio.PublishingEngine']"
          type="Custom.Pipelines.SendToDashboard, Custom"/>
      </printToDashboard>
    </pipelines>
  </sitecore>
</configuration>

So taking a look at SendToDashboard in the Send method, we can see the message is being built by SoapHelper.SendSoapMessage() call, and taking a look in SoapHelper we find that the CreateGroupNode method takes a BCC field, to which SendSoapMessage is just passing a blank string.

Unfortunately here's where things get a bit messy. We need to get BCC into SendSoapMessage, so ideally we would extend SoapHelper and add another override of SendSoapMessage which takes BCC as a parameter (with pretty much exactly the same code), however a lot of the methods that are used in SoapHelper are marked private (not protected) so we can't re-use the existing code!  At the end of the day I found it easiest to just copy the SendSoapMessage code directly into the new SendToDashboard class and use reflection to call CreateGroupNode() and CreateJobNode().  It would be great if Sitecore made these protected in the future, so we can extend the SoapHelper class (or expose the CC and BCC fields).

So now in our SendToDashboard class we have our new Send() method which calls the new SendSoapMessage() method with our BCC field.  Let's take things a little further and say we want to pass new info into this email that's being sent.  What if we want to pass the name of the user's document into the email so that they know which one it was (sounds pretty standard, right)?

Unfortunately this is just as messy.  In this case it's the CreateSoapMessageDetails() method we need to override to take the document name and pass it to SoapMessageDetails(), which should pass it into the string.Format() which builds the "Success" or "Failed" nodes.  Again none of these are protected, so we need to pretty  duplicate the code all the way through with our new parameter.  Don't forget to update the email template to make use of this new parameter!
public class SendToDashboard : Sitecore.PrintStudio.PublishingEngine.Pipelines.IPrintProcessor
{
  public void Process(PrintPipelineArgs args)
  {
    if (!string.IsNullOrEmpty(args.XmlResultFile))
    {
      Item processingJobItem = args.ProcessorItem.Database.GetItem(args.PrintJobId);
      this.Send(args.XmlResultFile, Path.Combine(args.PrintOptions.ResultFolder, args.PrintOptions.ResultFileName), args.PrintOptions.ResultFolder, processingJobItem, args.ProcessorItem.InnerItem.Name);
    }
    else
    {
      Logger.Info("Empty args.XmlResultFile");
      args.AddMessage("Empty args.XmlResultFile", PipelineMessageType.Error);
      args.AbortPipeline();
    }
  }
  private void Send(string resultXmlFileName, string pdfFileName, string absoluteFilePath, Item processingJobItem, string docName)
  {
    if (string.IsNullOrEmpty(resultXmlFileName))
    {
      return;
    }
    string dbServerIpAddress = WebConfigHandler.PrintStudioEngineSettings.DbServerIpAddress;
    string dbServerPort = WebConfigHandler.PrintStudioEngineSettings.DbServerPort;
    Assert.IsNotNullOrEmpty(dbServerIpAddress, "Missing PrintStudio.DBServer.IPAddress configuration");
    Assert.IsNotNullOrEmpty(dbServerPort, "Missing PrintStudio.DBServer.Port configuration");
    Language userLanguage = SitecoreHelper.GetUserLanguage(Context.User, processingJobItem.Database);
    XmlDocument resultDocument = new XmlDocument();
    Logger.Info("Sending to Dashboard: " + Context.User.Name + " (" + Context.User.Profile.Email + ") file " + resultXmlFileName);
    SendSoapMessage(Context.User.Name,
      Context.User.Name,
      Context.User.Profile.Email,
      resultXmlFileName,
      WebConfigHandler.PrintStudioEngineSettings.ResponseType,
      WebConfigHandler.PrintStudioEngineSettings.DashboardQueueName,
      WebConfigHandler.PrintStudioEngineSettings.DashboardServiceMethod,
      SitecoreHelper.GetMessage(processingJobItem, userLanguage, "Mail Subject") + " " + TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, TimeZoneInfo.FindSystemTimeZoneById(Constants.General.Timezone)).ToString("g"),
 string.Empty,
     CreateSoapMessageDetails(processingJobItem, resultDocument, userLanguage, docName).ToString(),
     SitecoreHelper.GetPrintFilePath(processingJobItem, "relative filename path"),
     absoluteFilePath,
     pdfFileName,
     $"{dbServerIpAddress}:{dbServerPort}",
     processingJobItem.Database.GetItem(new ID(Constants.Items.SiteSettings))?.Fields[Constants.Fields.SupportEmail]?.Value);
  }
  private static string SendSoapMessage(string userName, string loginName, string email, string jobXml, string responseType, string serviceType, string serviceMethod, string subject, string body, string messageBrands, string relativePath, string absolutePath, string fileName, string serviceUrl, string bcc)
  {
     string startTime = DateTime.UtcNow.ToString("s", DateTimeFormatInfo.InvariantInfo);
     XmlDataDocument xmlDataDocument = new XmlDataDocument();
     XmlNode groupNode = (XmlNode)typeof(SoapHelper).GetMethod("CreateGroupNode", BindingFlags.Static | BindingFlags.NonPublic).Invoke(null, new object[] { xmlDataDocument, userName, userName, loginName, startTime, responseType, email, body, subject, string.Empty, bcc, string.Empty, "HTML" });
     XmlNode jobNode = (XmlNode)typeof(SoapHelper).GetMethod("CreateJobNode", BindingFlags.Static | BindingFlags.NonPublic).Invoke(null, new object[] { xmlDataDocument, userName, "1", serviceType, serviceMethod, string.Empty, fileName, relativePath, absolutePath, string.Empty, string.Empty, jobXml, string.Empty, serviceUrl });
     typeof(SoapHelper).GetMethod("AddJobNode", BindingFlags.Static | BindingFlags.NonPublic).Invoke(null, new object[] { xmlDataDocument, groupNode, jobNode });
     if (!string.IsNullOrEmpty(messageBrands))
     {
       XmlNode xmlNode = groupNode.SelectSingleNode("//Jobs/Job");
       if (xmlNode != null && groupNode.OwnerDocument != null)
       {
         XmlElement element = groupNode.OwnerDocument.CreateElement("ResultMail");
         element.InnerXml = messageBrands;
         xmlNode.AppendChild(element);
       }
    }
    return SoapHelper.DashBoardWebService.Send("<?xml version=\"1.0\" encoding=\"utf-16\" ?>" + groupNode.OuterXml); // returns "Success"
  }
  private static StringBuilder CreateSoapMessageDetails(Item processingJobItem, XmlDocument resultDocument, Language language, string jobName)
  {
    string message1 = SitecoreHelper.GetMessage(processingJobItem, language, "Mail Header");
    string message2 = SitecoreHelper.GetMessage(processingJobItem, language, "Mail Footer");
    string message3 = SitecoreHelper.GetMessage(processingJobItem, language, "Ready Message");
    string message4 = SitecoreHelper.GetMessage(processingJobItem, language, "Rejected Message");
    return SoapMessageDetails(resultDocument, message3, message4, message1, message2, jobName);
  }
  private static StringBuilder SoapMessageDetails(XmlDocument resultDocument, string jobSuccess, string jobFailure, string mailHeader, string mailFooter, string jobName)
  {
    StringBuilder stringBuilder = new StringBuilder();
    XmlNode xmlNode1 = resultDocument.CreateElement("Header");
    xmlNode1.AppendChild(resultDocument.CreateCDataSection(mailHeader));
    stringBuilder.Append(xmlNode1.OuterXml);
    XmlNode xmlNode2 = resultDocument.CreateElement("Footer");
    xmlNode2.AppendChild(resultDocument.CreateCDataSection(mailFooter));
    stringBuilder.Append(xmlNode2.OuterXml);
    XmlNode xmlNode3 = resultDocument.CreateElement("Success");
    xmlNode3.AppendChild(resultDocument.CreateCDataSection(string.Format(jobSuccess, Context.User.Profile.FullName, Context.User.LocalName, jobName)));
    stringBuilder.Append(xmlNode3.OuterXml);
    XmlNode xmlNode4 = resultDocument.CreateElement("Failed");
    xmlNode4.AppendChild(resultDocument.CreateCDataSection(string.Format(jobFailure, Context.User.Profile.FullName, Context.User.LocalName, jobName)));
    stringBuilder.Append(xmlNode4.OuterXml);
    return stringBuilder;
  }
}

This was a bit of a messy one. Hopefully with newer releases of PXM and ODG Sitecore makes things a little more extensible!