Hacking Sitecore and Life one pipeline at a time!

Automatically resuming paused email campaigns

When an email campaign is being dispatched, it moves through a number of states e.g. draft, queuing, sending and finally sent. However, if an error occurs during dispatch, the email campaign may not be moved to the “sent” state, but it is instead moved to the “paused” state.

The error handling differs depending on the version of EXM, and the type of the error.

In this post we will have a look at:

  1. Why and when is an email campaign paused, and
  2. How can we resume it

Non-critical errors

Non-critical errors are errors such as failing to process a specific contact e.g. because of no or an invalid email address.

EXM 3.0 -> 3.4.2

In these versions of EXM, the email campaign will never move into the “Sent” state if any non-critical errors occurred. All contacts will be processed, but it will remain in the “Paused” state. The reasoning was that this would allow the editor to fix any invalid contacts, and then resume the email campaign.

EXM 3.5+

In later versions of EXM, the email campaign will move to the “Sent” state, even if there are non-critical errors. For each of those contacts an interaction will be created with a “Dispatch failed” event describing the reason. You can see the list of failures on the campaign report page.

Critical errors

Critical errors are non-transient errors that prevent EXM from dispatching emails e.g.

  • No connection to the SMTP server
  • No connection to the EXM.Master database
  • No connection to xConnect/xDB

If the connection is lost, and cannot be established, the email campaign dispatch will be aborted and the email campaign will be paused.

There is no out-of-the-box functionality to automatically resume paused dispatches, but in the following, we will examine how we can create a custom Sitecore task that will resume any paused dispatches.

To be paused, or to be aborted

DISCLAIMER: The EXM UI allows the editor to pause an email campaign. Unfortunately for us, there is no way to distinguish an email campaign that is manually paused, from an email campaign that is aborted because of a critical error.

Automatically resuming

We will need to create three things:

  1. Implement a task that finds and resumes all paused email campaigns
  2. A command item
  3. A schedule item

The task

To figure out which email campaigns are paused, we will take advantage of some of the built-in EXM functionality.

The Sitecore.Modules.EmailCampaign.Services.IManagerRootService allow us to work with manager roots. We can get a specific manager root if we know the specific id of the manager root item, or we can load all manager roots. The latter is suitable for our purposes, as we want to resume any email campaign on any manager root, so we do the following:

List<ManagerRoot> managerRoots = _managerRootService.GetManagerRoots();

For each manager root, we can then use the Sitecore.Modules.EmailCampaign.Core.Data.EcmDataProvider to find a list of paused email campaigns. The EcmDataProvider is a data provider for the “Campaigns” table of the EXM.Master database. When an email campaign is created, the details about it are stored in the “Campaigns” table. From this table you are able to extract information such as the status as well as the number of included-, excluded and total recipients.

The “Campaigns” table is continously updated, by the “Message statitistics”-commands, but that is a story for another day.

For our purposes, we are going to get all paused email campaigns by querying the “Campaigns” table through the EcmDataProvider. Note that you are not limited to getting paused email campaigns, you can also get e.g. any draft email campaigns:

ar options = new CampaignSearchOptions
{
  ManagerRootId = managerRoot.Id,
  StatusInclude = new[]
  {
    EcmTexts.Paused,
    // EcmTexts.SendingState - for email campaigns in progress of being sent
    // EcmTexts.Queuing - for email campaigns in progress of being queued
    // EcmTexts.AbTesting, EcmTexts.SelectWinner - for email campaigns in awaiting an A/B test winner
    // EcmTexts.Draft, EcmTexts.Inactive - for draft email campaigns and inactive automated email campaigns
    // EcmTexts.Active - for activated automated email campaigns
    // EcmTexts.DispatchScheduled, EcmTexts.ActivationScheduled - for scheduled email campaigns
    // EcmTexts.Sent - for sent messages
  },
  Page = new DataPage
  {
    Index = 0, 
    Size = 10
  }
};

CampaignSearchResults campaigns = _dataProvider.SearchCampaigns(options);

This search will return a list of paused email campaigns. Using the email campaign id, we can resume the dispatch using the Resume() method on Sitecore.Modules.EmailCampaign.Application.EmailDispatch.IEmailDispatch. This interface also contains methods to send, pause, (de)activate and schedule email campaigns, and is also used by the EXM UI under the hood. NB: There’s one caveat that you need to be aware of, which we will look at below when we create the schedule item.

_application.EmailDispatch.Resume(campaign.MessageId);

Putting it all together, our task looks like this:

using System.Collections.Generic;
using Microsoft.Extensions.DependencyInjection;
using Sitecore.Data.Items;
using Sitecore.DependencyInjection;
using Sitecore.EmailCampaign.Model.Data;
using Sitecore.EmailCampaign.Model.Message;
using Sitecore.EmailCampaign.Model.Web;
using Sitecore.Framework.Conditions;
using Sitecore.Modules.EmailCampaign;
using Sitecore.Modules.EmailCampaign.Application;
using Sitecore.Modules.EmailCampaign.Core.Data;
using Sitecore.Modules.EmailCampaign.Services;
using Sitecore.Tasks;

namespace Sitecore.Hacker
{
  [UsedImplicitly]
  public class ResumePaused
  {
    private readonly IExmCampaignService _exmCampaignService;
    private readonly EcmDataProvider _dataProvider;
    private readonly IManagerRootService _managerRootService;
    private readonly IApplication _application;

    public ResumePaused()
      : this(ServiceLocator.ServiceProvider.GetService<IExmCampaignService>(), ServiceLocator.ServiceProvider.GetService<EcmDataProvider>(), ServiceLocator.ServiceProvider.GetService<IManagerRootService>(), Application.Instance)
    {
    }

    internal ResumePaused([NotNull] IExmCampaignService exmCampaignService, [NotNull] EcmDataProvider dataProvider, [NotNull] IManagerRootService managerRootService, [NotNull] IApplication application)
    {
      Condition.Requires(exmCampaignService, nameof(exmCampaignService)).IsNotNull();
      Condition.Requires(dataProvider, nameof(dataProvider)).IsNotNull();
      Condition.Requires(managerRootService, nameof(managerRootService)).IsNotNull();
      Condition.Requires(application, nameof(application)).IsNotNull();

      _exmCampaignService = exmCampaignService;
      _dataProvider = dataProvider;
      _managerRootService = managerRootService;
      _application = application;
    }

    /// <summary>
    /// Executes task.
    /// </summary>
    /// <param name="itemArray">The item array.</param>
    /// <param name="commandItem">The <see cref="CommandItem"/>.</param>
    /// <param name="scheduledItem">The <see cref="ScheduleItem"/>.</param>
    public void Execute(Item[] itemArray, CommandItem commandItem, ScheduleItem scheduledItem)
    {
      List<ManagerRoot> managerRoots = _managerRootService.GetManagerRoots();

      foreach (ManagerRoot managerRoot in managerRoots)
      {
        var options = new CampaignSearchOptions
        {
          ManagerRootId = managerRoot.Id,
          StatusInclude = new[]
          {
            EcmTexts.Paused
          },
          Page = new DataPage
          {
            Index = 0, // I'm leaving this hardcoded for simplicity, but you should iterate through all results
            Size = 10
          }
        };

        CampaignSearchResults campaigns = _dataProvider.SearchCampaigns(options);
        //int totalResults = campaigns.TotalResults;

        foreach (EmailCampaignsData campaign in campaigns)
        {
          // We don't actually need the IExmCampaignService for this example, but I'm leaving it here
          // in case it's useful for your implementation
          //MessageItem messageItem = _exmCampaignService.GetMessageItem(campaign.MessageId);

          _application.EmailDispatch.Resume(campaign.MessageId);
        }
      }
    }
  }
}

The command item

We need to create a command item in order to tell our scheduled task what to do.

We will create the item in “/sitecore/system/Tasks/Commands/” and name it
“Resume paused email campaigns “

The schedule item

The final step is to create a schedule item, but if your deployment contains one/more dedicated dispatch servers, we need to make sure that this task is only being run on the primary CM server. Why? Because only the primary CM server can start/resume a dispatch. In other words, the primary CM server acts as a director telling any dedicated dispatch servers what to do.

Luckily this is straightforward to achieve, as long as we create our schedule in “/sitecore/system/Settings/Email/Instance Tasks/Content Management Primary/”:

Here I have configured the schedule to run every minute. That is probably not necessary in a real-life scenario, so change it as you see fit.

Categorised in: EXM, Sitecore

1 Response »

  1. The real error not definitely in real case

    Like

Leave a Reply to vishwjeetgupta1413 Cancel reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

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

Google photo

You are commenting using your Google 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 )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Enter your email address to follow this blog and receive notifications of new posts by email.

Join 879 other followers

Blog Stats

  • 59,746 hits
Follow Sitecore Hacker on WordPress.com
%d bloggers like this: