6

We are running Email Experience Manager in Sitecore 9.1 with custom processors in the UnsubscribeFromList & UnsubscribeFromAllEvent pipelines in order to sync email subscriptions to a 3rd party system.

The OOTB newsletter contains links for the client to manage their subscriptions... Unsubscribe, Unsubscribe from all & Update subscription preferences.

My question relates to these email links and submitting the update on the subscription preferences page. I'm seeing different behavior depending on where I submit from.

Selecting Unsubscribe from the email will fire the UnsubscribeFromList pipeline. Great!

But selecting the Unsubscribe from all link in the email does not fire the UnsubscribeFromAllEvent as I expected.

The Update subscription page, by default, contains the checkbox "I do not want to receive any emails from Sitecore EXM. Unsubscribe me from all.".

Updating the preferences, with this global opt-out selected, will run the UnsubscribeFromAllEvent pipeline. Great again!

On this same page are the checkboxes relating to the Email types in our Marketing categories, the lists to unsubscribe from.

Selecting any of these and submitting the update does not run the UnsubscribeFromList pipeline as I expected and generates this in the Exm.log - ERROR [UpdateListSubscriptionHandler] Failed to update the list subscription for 'Alias PII removed'. Operation: 'RemoveFromList'

Are there other pipelines or processes for each of these different cases?

Update

From within email:

  • Selecting Unsubscribe from the email, CHECK
  • Selecting Unsubscribe From All from the email does not file UnsubscribeFromAllEvent pipeline.

From Update subscription page:

  • Unsubscribe me from all CHECK
  • Checking any box and clicking the update button causes error.
  • I've updated my answer below with more detail. – Pete Navarra Mar 29 at 19:08
5

DISCLAIMER: This post ONLY APPLIES to Sitecore 9.1 and beyond.

Marketing Preferences

Marketing Preferences is a new part of Sitecore that allows users to self-select marketing segments. This is an important concept because, at least as of now, the selection of Marketing Preferences, out of the box, has no bearing or effect on the subscription to EXM. In fact, all Marketing Preferences really do is add additional metadata to the Contact’s Facets.

Updating marketing preferences does not fire any of the unsubscribe pipelines. All it does is set data on the Marketing Preferences facet.

enter image description here

As for the error you are seeing when you check a box, is probably an install or message root issue, however, more information is needed to assist.

Unsubscribe From Email/All

Understanding the error message you are getting, requires a deep dive into how this Process works.

Content Management Role in Unsubscribes

The UnsubscribeFromAllEvent pipeline

This pipeline is executed via the SubscriptionManager that is only available on the Content Manager role server.

public virtual bool UnsubscribeFromAll(Contact contact, ManagerRoot managerRoot)
{
  Assert.ArgumentNotNull((object) contact, "contact");
  Assert.ArgumentNotNull((object) managerRoot, "managerRoot");
  try
  {
    if (managerRoot.GlobalSubscription.IsInDefaultExcludeCollection(contact))
      return true;
    if (!managerRoot.GlobalSubscription.AddToDefaultExcludeCollection(contact))
      return false;
    this._pipelineHelper.RunPipeline("UnsubscribeFromAllEvent", (PipelineArgs) new UnsubscribeFromAllEventArgs(contact));
    return true;
  }
  catch (Exception ex)
  {
    this._logger.LogError(ex.Message, ex);
  }
  return false;
}

This method is called from the UpdateListSubscriptionHandler

UpdateListSubscriptionHandler

This is a Rebus handler responsible for taking subscription event messages from the Content Delivery servers (which can't process subscription changes) and performing the changes needed. This handler has an options enum object that can handle a number of other actions aside from UnsubscribeFromAll.

namespace Sitecore.EmailCampaign.Model.Messaging
{
  [Serializable]
  public enum ListSubscribeOperation
  {
    Subscribe,
    Unsubscribe,
    UnsubscribeFromAll,
    AddToList,
    RemoveFromList,
  }
}

protected bool ProcessListOperation(UpdateListSubscriptionMessage message)
{
  if (message.ListSubscribeOperationSucceeded)
    return true;
  try
  {
    switch (message.ListSubscribeOperation)
    {
      case ListSubscribeOperation.Subscribe:
        this._logger.LogDebug(FormattableString.Invariant(FormattableStringFactory.Create("[{0}] Subscribing '{1}' from '{2}'. Require subscription confirmation: '{3}'", (object) "UpdateListSubscriptionHandler", (object) message.ContactIdentifier.ToLogFile(), (object) message.MessageId, (object) message.RequireSubscriptionConfirmation)));
        return this._subscriptionManager.Subscribe(message.ContactIdentifier, message.MessageId, message.RequireSubscriptionConfirmation);
      case ListSubscribeOperation.Unsubscribe:
        this._logger.LogDebug(FormattableString.Invariant(FormattableStringFactory.Create("[{0}] Unsubscribing '{1}' from message '{2}'", (object) "UpdateListSubscriptionHandler", (object) message.ContactIdentifier.ToLogFile(), (object) message.MessageId)));
        return this._subscriptionManager.Unsubscribe(message.ContactIdentifier, message.MessageId);
      case ListSubscribeOperation.UnsubscribeFromAll:
        this._logger.LogDebug(FormattableString.Invariant(FormattableStringFactory.Create("[{0}] Unsubscribing '{1}' from manager root '{2}'", (object) "UpdateListSubscriptionHandler", (object) message.ContactIdentifier.ToLogFile(), (object) this.GetManagerRootId(message))));
        return this._subscriptionManager.UnsubscribeFromAll(message.ContactIdentifier, this.GetManagerRootId(message));
      case ListSubscribeOperation.AddToList:
        this._logger.LogDebug(FormattableString.Invariant(FormattableStringFactory.Create("[{0}] Removing '{1}' from the list with id '{2}'", (object) "UpdateListSubscriptionHandler", (object) message.ContactIdentifier.ToLogFile(), (object) message.ListId)));
        return this._subscriptionManager.AddContactToList(message.ContactIdentifier, message.ListId);
      case ListSubscribeOperation.RemoveFromList:
        this._logger.LogDebug(FormattableString.Invariant(FormattableStringFactory.Create("[{0}] Adding '{1}' to the list with id '{2}'", (object) "UpdateListSubscriptionHandler", (object) message.ContactIdentifier.ToLogFile(), (object) message.ListId)));
        return this._subscriptionManager.RemoveContactFromList(message.ContactIdentifier, message.ListId);
    }
  }
  catch (Exception ex)
  {
    this._logger.LogError(FormattableString.Invariant(FormattableStringFactory.Create("[{0}] Failed to process list operation for '{1}'", (object) "UpdateListSubscriptionHandler", (object) message.ContactIdentifier.ToLogFile())), ex);
  }
  return false;
}

This is the end of the responsibility that the Content Manager server has, and it appears that all of this is functioning, including the execution of the UnsubscribeFromAllEvent pipeline. This takes our discovery to the Content Delivery side.

Content Delivery role in Unsubscribes.

We went backwards on the Content Management side of the process. Now we're going to start with the UnsubscribeFromAll.aspx page that is linked in the email.

The UnsubscribeFromAll.aspx Page

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="UnsubscribeFromAll.aspx.cs" Inherits="Sitecore.EmailCampaign.Cd.sitecore_modules.Web.EXM.UnsubscribeFromAll" %>

It's clear that this ASPX page calls the UnsubscribeFromAll class.

UnsubscribeFromAll WebForm

The web form inherits the UnsubscribeMessageEventPage class. enter image description here

Taking a look at that base class, there's a method called HandleMessageEvent() that does a lot, but in particular the following. This method is actually called FIRST on both the Unsubscribe and UnsubscribeFromAll links/actions.

  else if (this.IsTrackerEnabled)
  {
    if (!ExmContext.Message.ExcludeFromReports)
    {
      UnsubscribedFromEmailEvent unsubscribedFromEmailEvent = new UnsubscribedFromEmailEvent();
      Guid messageId1 = ExmContext.Message.MessageId;
      unsubscribedFromEmailEvent.MessageId = messageId1;
      Guid messageId2 = ExmContext.Message.MessageId;
      unsubscribedFromEmailEvent.InstanceId = messageId2;
      Guid managerRootId = ExmContext.Message.ManagerRootId;
      unsubscribedFromEmailEvent.ManagerRootId = managerRootId;
      string name = ExmContext.Message.TargetLanguage.Name;
      unsubscribedFromEmailEvent.MessageLanguage = name;
      byte? testValueIndex = ExmContext.Message.TestValueIndex;
      unsubscribedFromEmailEvent.TestValueIndex = testValueIndex;
      int addressHistoryEntryId = ExmContext.Message.EmailAddressHistoryEntryId;
      unsubscribedFromEmailEvent.EmailAddressHistoryEntryId = addressHistoryEntryId;
      EmailEventDto dto = this._eventDataService.EmailEventToDto((EmailEvent) unsubscribedFromEmailEvent);
      this._analyticsGateway.RegisterNextPageEvent(new PageEventData("Unsubscribe from email", Sitecore.EmailCampaign.Model.Constants.UnsubscribeFromEmailPageEventId)
      {
        CustomValues = {
          {
            "ExmCustomValues",
            (object) dto
          }
        }
      });
      this.CancelTracker();
    }
    using (new LanguageSwitcher(this.LanguageName))
      url = this.VerifyContactSubscriptions(ExmContext.ContactIdentifier, ExmContext.Message.MessageId) ?? this.UnsubscribeContact(ExmContext.ContactIdentifier, ExmContext.Message.MessageId);
  }

Of particular note is this line:

    using (new LanguageSwitcher(this.LanguageName))
      url = this.VerifyContactSubscriptions(ExmContext.ContactIdentifier, ExmContext.Message.MessageId) ?? this.UnsubscribeContact(ExmContext.ContactIdentifier, ExmContext.Message.MessageId);

UnsubscribeContact() is a method that is defined on the ASPX WebForm and is only getting called if the VerifyContactSubscription is returning a null value, which appears to always be null except if the contact is already in the global opt-out list.

Note: VerifyContactSubscriptions has to be null, in order for the Unsubscribe page to run the UnsubscribeContact method, which in turn will run the UnsubscribeFromAllEvent pipeline.

On the UnsubscribeFromAll.aspx WebForm, this method is defined as:

protected override string UnsubscribeContact(ContactIdentifier contactIdentifier, Guid messageID)
{
  Guid guid = ExmContext.Message != null ? ExmContext.Message.ManagerRootId : Guid.Empty;
  this.ClientApiService.UpdateListSubscription(new UpdateListSubscriptionMessage()
  {
    ListSubscribeOperation = ListSubscribeOperation.UnsubscribeFromAll,
    ContactIdentifier = contactIdentifier,
    MessageId = messageID,
    ManagerRootId = guid
  });
  MessageItem message = ExmContext.Message;
  return (message != null ? message.ManagerRoot.GetConfirmativePageUrl() : (string) null) ?? "/";
}

}

What's important to note here is that the enum being used ListSubscribeOperation.UnsubscribeFromAll.

Going back to your stated error message:

Selecting any of these and submitting the update does not run the UnsubscribeFromList pipeline as I expected and generates this in the Exm.log -

ERROR [UpdateListSubscriptionHandler] Failed to update the list subscription for 'Alias PII removed'. Operation: 'RemoveFromList'

RemoveFromList is NOT the UnsubscribeFromAll enum. Which means that this error message is coming from another process.

Summary

I believe that your system is working as expected, but that maybe you're links are wrong in the email template. If the links are RIGHT, then I would inspect the VerifyContactSubscription method, below.

protected virtual string VerifyContactSubscriptions(ContactIdentifier contactIdentifier, Guid messageID)
{
  GetContactPipelineArgs contactPipelineArgs = new GetContactPipelineArgs(contactIdentifier, (ID) null, new string[1]
  {
    "ListSubscriptions"
  });
  this._pipelineHelper.RunPipeline("getContact", (PipelineArgs) contactPipelineArgs, "exm");
  Sitecore.XConnect.Contact contact = contactPipelineArgs.Contact;
  string unsubscribedPage = ExmContext.Message.ManagerRoot.GetAlreadyUnsubscribedPage();
  if (ExmContext.Message.ManagerRoot.GlobalSubscription.IsInDefaultExcludeCollection(contact))
  {
    this._logger.LogInfo(string.Format("Will not unsubscribe {0} from message {1}: already in the Global opt-out list", (object) contactIdentifier.ToLogFile(), (object) messageID));
    return unsubscribedPage;
  }
  ID[] array = ID.ParseArray(ExmContext.Message.InnerItem["{F8D592CC-F3E5-4A8E-A600-8AEBE8CC36E4}"], false);
  if (array == null)
  {
    this._logger.LogWarn(string.Format("Must add {0} to Global opt-out list: No include-lists found in message {1}", (object) contactIdentifier.ToLogFile(), (object) messageID));
    return (string) null;
  }
  if (this.ListManagerWrapper.HasSegmentedList(((IEnumerable<ID>) array).Select<ID, Guid>((Func<ID, Guid>) (x => x.Guid))))
  {
    this._logger.LogWarn(string.Format("Must add {0} to Global opt-out list: One of the Include lists is Segmented in message {1}", (object) contactIdentifier.ToLogFile(), (object) messageID));
    return (string) null;
  }
  if (((IEnumerable<ID>) array).Any<ID>((Func<ID, bool>) (x => this.ListManagerWrapper.IsSubscribed(x.Guid, contact))))
    return (string) null;
  MessageItem message = ExmContext.Message;
  if (message != null && message.Source.MessageType == MessageType.Automated)
  {
    this._logger.LogInfo(string.Format("Must add {0} to Global opt-out list: Type of message {1} is Automated.", (object) contactIdentifier.ToLogFile(), (object) messageID));
    return (string) null;
  }
  this._logger.LogInfo(string.Format("Will not unsubscribe {0} from message {1}: already unsubscribed", (object) contactIdentifier.ToLogFile(), (object) messageID));
  return unsubscribedPage;
}

There's a pipeline called getContact that you can hook into, and see if it's running when the Unsubscribe From All link is clicked.

  • That is one heck of a write up Pete. Well done. – Chris Auer Mar 30 at 0:07
  • Great explanation. Thanks for the detailed response, it's a huge help. – LCM Apr 2 at 20:27

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.