Hacking Sitecore and Life one pipeline at a time!

Developer Guide

The following guide and topics are current as of Sitecore Experience Platform 9.1. As updates are released, this guide will be updated as quickly as possible.

The EXM Developer Guide is a living guide intended to provide additional information that the Sitecore documentation site might not make available. However, it is not intended to be cut and paste of the Sitecore Documentation Site. Instead, this is a companion guide. Please provide comments below if any information is incomplete, incorrect, or not present.

Sitecore Documentation Site Links


Table of Contents


Roles of Email Experience Manager

The server roles of Email Experience Manager do not differ from the roles of Sitecore, with the exception of the Dedicated Dispatch Server. Otherwise, all of the roles do the same things, only EXM makes each role do additional work.

When using EXM, it’s important t note, that on ALL role servers EXM should be enabled via the Web.config as shown here.

    <!-- EXM Enabled
				 Set this to anything other than 'yes' to disable EXM and its configuration.
			-->
    <add key="exmEnabled:define" value="yes"/>

Additionally, ALLservers should have the exact same keys defined in the ConnectionStrings.config as shown here:

<?xml version="1.0" encoding="utf-8"?>
<connectionStrings>
  <add name="EXM.CryptographicKey" connectionString="0x3068480677720075338228396754938017850570187873738392546365112406" />
  <add name="EXM.AuthenticationKey" connectionString="0x4110187119230947340592315047210744477554857314512310509679367459" />
</connectionStrings>

The keys listed above are for example only. You should generate and use 
different keys.

Go To Top

Primary Content Management

This is the primary authoring environment. All content management and authoring should occur on this server. To define a Primary Content Management server, use the following role:define in the Web.config.

    <!-- SUPPORTED SERVER ROLES     
         Specify the roles that you want this server to perform. A server can perform one or more roles. Enter the roles in a comma separated list. The supported roles are:

         ContentDelivery
         ContentManagement
         Processing
         Reporting
         Standalone
         DedicatedDispatch
          
    Default value: Standalone
    -->
    <add key="role:define" value="ContentManagement"/>

Go To Top

Dedicated Dispatch Server

In practice, the Dedicated Dispatch Server is just a secondary content management server, with an additional responsibility: Dispatching email through EXM. To define a Sitecore instance as a Dedicated Dispatch Server:

    <!-- SUPPORTED SERVER ROLES     
         Specify the roles that you want this server to perform. A server can perform one or more roles. Enter the roles in a comma separated list. The supported roles are:

         ContentDelivery
         ContentManagement
         Processing
         Reporting
         Standalone
         DedicatedDispatch
          
    Default value: Standalone
    -->
    <add key="role:define" value="ContentManagement,DedicatedDispatch"/>

Go To Top

Content Delivery

The content delivery server is the primary web server that the public internet is served. To Email Experience Manager, the CD server be mapped to the public domain of your website, as configured in the Email Manager Root in EXM.

The Content Delivery role serves one purpose for EXM: It is the front door for open and click tracking requests, as well as subscription client access, redirects, and for displaying subscription and marketing preferences.

    <!-- SUPPORTED SERVER ROLES     
         Specify the roles that you want this server to perform. A server can perform one or more roles. Enter the roles in a comma separated list. The supported roles are:

         ContentDelivery
         ContentManagement
         Processing
         Reporting
         Standalone
         DedicatedDispatch
          
    Default value: Standalone
    -->
    <add key="role:define" value="ContentDelivery"/>

It’s also very important the every Content Delivery server has the same MachineKey information in the Web.config and that the EXM keys in the ConnectionString.config that are the same on all CM and Dispatch Servers.

Go To Top

Processing

This is the standard Sitecore Processing role. Additional configs are enabled for Email Experience Manager to assist in additional processing aggregation.

    <!-- SUPPORTED SERVER ROLES     
         Specify the roles that you want this server to perform. A server can perform one or more roles. Enter the roles in a comma separated list. The supported roles are:

         ContentDelivery
         ContentManagement
         Processing
         Reporting
         Standalone
         DedicatedDispatch
          
    Default value: Standalone
    -->
    <add key="role:define" value="Processing"/>

Go To Top

Reporting

This is the standard Sitecore Reporting role. Additional configs are enabled for Email Experience Manager to assist in additional reporting endpoints.

    <!-- SUPPORTED SERVER ROLES     
         Specify the roles that you want this server to perform. A server can perform one or more roles. Enter the roles in a comma separated list. The supported roles are:

         ContentDelivery
         ContentManagement
         Processing
         Reporting
         Standalone
         DedicatedDispatch
          
    Default value: Standalone
    -->
    <add key="role:define" value="Reporting"/>

Go To Top


EXM Pipelines

Email Experience Manager, as expected, follows all of the same design patterns that the rest of Sitecore uses when it comes to how EXM is connected to Sitecore. This means that for almost every process, there is a pipeline or a provider.

The following sections of this guide are intended for Certified Sitecore developers that already have an understanding of how Sitecore pipelines work. Aside from highlighting key pipelines, there will not be a full description of every processor, unless intentionally called out. Use of dotPeek, or your favorite decompiler, is advised.

Go To Top

Dispatch Pipelines

The most important pipeline is the <DispatchNewsletter> pipeline. When the Send button is clicked in EXM, or when an Automated Message is triggered, Sitecore EXM runs this pipeline on the Primary CM server. There is an abbreviated version of the <DispatchNewsletter> pipeline on the Dedicated Dispatch Servers too.

Sitecore provides a good page for detailing out these pipelines.

Primary CM

Sitecore.EmailExperience.ContentManagementPrimary.config

      <DispatchNewsletter>
        <processor type="Sitecore.EmailCampaign.Cm.Pipelines.DispatchNewsletter.CheckPreconditions, Sitecore.EmailCampaign.Cm" resolve="true" />
        <processor type="Sitecore.EmailCampaign.Cm.Pipelines.DispatchNewsletter.MoveToQueuing, Sitecore.EmailCampaign.Cm" resolve="true" />
        <processor type="Sitecore.EmailCampaign.Cm.Pipelines.DispatchNewsletter.DeployAnalytics, Sitecore.EmailCampaign.Cm" resolve="true" />
        <processor type="Sitecore.EmailCampaign.Cm.Pipelines.DispatchNewsletter.PublishDispatchItems, Sitecore.EmailCampaign.Cm" resolve="true" />
        <processor type="Sitecore.EmailCampaign.Cm.Pipelines.DispatchNewsletter.QueueMessage, Sitecore.EmailCampaign.Cm" resolve="true"/>
        <processor type="Sitecore.EmailCampaign.Cm.Pipelines.DispatchNewsletter.MoveToProcessing, Sitecore.EmailCampaign.Cm" resolve="true" />
        <processor type="Sitecore.EmailCampaign.Cm.Pipelines.DispatchNewsletter.LaunchDedicatedServers, Sitecore.EmailCampaign.Cm" />
        <processor type="Sitecore.EmailCampaign.Cm.Pipelines.DispatchNewsletter.SendTestMessage, Sitecore.EmailCampaign.Cm" resolve="true" />
        <processor type="Sitecore.EmailCampaign.Cm.Pipelines.DispatchNewsletter.SendMessage, Sitecore.EmailCampaign.Cm" resolve="true">
            <!-- The number of milliseconds to wait after dispatch is completed/aborted/paused in order to ensure logging statistics are updated across all dispatch servers. Can be set to 0 if there are no dedicated dispatch servers configured. -->
            <Sleep>2000</Sleep>
        </processor>
        <!-- The WaitForDispatchToFinish pipeline processor should only be enabled if you have at least one dedicated dispatch server enabled.If you enable this processor you should disable the SendMessage processor. -->
        <!--<processor type="Sitecore.EmailCampaign.Cm.Pipelines.DispatchNewsletter.WaitForDispatchToFinish, Sitecore.EmailCampaign.Cm" resolve="true">
          <TimeToWaitBetweenChecks>1000</TimeToWaitBetweenChecks>
        </processor>-->
        <processor type="Sitecore.EmailCampaign.Cm.Pipelines.DispatchNewsletter.MoveToSent, Sitecore.EmailCampaign.Cm" resolve="true"/>
        <processor type="Sitecore.EmailCampaign.Cm.Pipelines.DispatchNewsletter.NotifyDispatchFinished, Sitecore.EmailCampaign.Cm" resolve="true"/>
        <processor type="Sitecore.EmailCampaign.Cm.Pipelines.DispatchNewsletter.FinalizeDispatch, Sitecore.EmailCampaign.Cm" resolve="true" />
      </DispatchNewsletter>

Dedicated Dispatch Server

Sitecore.EmailExperience.EmailProcessing.config

      <DispatchNewsletter>
        <processor type="Sitecore.EmailCampaign.Cm.Pipelines.DispatchNewsletter.CheckPreconditions, Sitecore.EmailCampaign.Cm" resolve="true"/>
        <processor type="Sitecore.EmailCampaign.Cm.Pipelines.DispatchNewsletter.SendMessage, Sitecore.EmailCampaign.Cm" resolve="true" >
            <!-- The number of milliseconds to wait after dispatch is completed/aborted/paused in order to ensure logging statistics are updated across all dispatch servers. Can be set to 0 if there are no dedicated dispatch servers configured. -->
            <Sleep>2000</Sleep>
        </processor>
        <processor type="Sitecore.EmailCampaign.Cm.Pipelines.DispatchNewsletter.FinalizeDispatch, Sitecore.EmailCampaign.Cm" resolve="true" />
      </DispatchNewsletter>

Be sure to adjust the <Sleep> value in a production environment.

Go To Top

Message Creation Pipelines

There are a couple of pipelines that deal with the individual creation/sending of an email to a single contact. For every contact being dispatched to, the following pipeline is executed on all DDS servers, as well as Primary CM assuming that dispatch hasn’t been disabled.

      <!-- SEND EMAIL PIPELINE
           This pipeline dispatches a single email through the SMTP server.
      -->
      <SendEmail>
        <processor type="Sitecore.EmailCampaign.Cm.Pipelines.SendEmail.FillEmail, Sitecore.EmailCampaign.Cm" resolve="true" />
        <processor type="Sitecore.EmailCampaign.Cm.Pipelines.SendEmail.SendEmail, Sitecore.EmailCampaign.Cm" resolve="true" />
        <processor type="Sitecore.EmailCampaign.Cm.Pipelines.SendEmail.Sleep, Sitecore.EmailCampaign.Cm">
              <!-- Number of milliseconds to put the thread to sleep for after an email has been sent. -->
              <param desc="sleep">50</param>
          </processor>
      </SendEmail>

Adjust Sleep value as needed. For fast dispatching, change to 0.

If your SMTP service has a rate limit, adjust this to a higher value.

Deep in the FillEmail processor above, the <modifyHyperlink> pipeline is executed for every link in an email. This augments the link URL in the email to a RedirectURL link that provides the appropriate ciick tracking and analytics tracking needed. The links are also encrypted coming out of this pipeline.

      <modifyHyperlink>
        <processor type="Sitecore.Modules.EmailCampaign.Core.Pipelines.GenerateLink.Hyperlink.SkipAnchorLinks, Sitecore.EmailCampaign" />
        <processor type="Sitecore.Modules.EmailCampaign.Core.Pipelines.GenerateLink.SetServerUrl, Sitecore.EmailCampaign" resolve="true" />
        <processor type="Sitecore.Modules.EmailCampaign.Core.Pipelines.GenerateLink.Hyperlink.SkipAlreadyProcessedHyperlink, Sitecore.EmailCampaign">
          <RedirectPagePath>/sitecore%20modules/Web/EXM/RedirectUrlPage.aspx</RedirectPagePath>
        </processor>
        <processor type="Sitecore.Modules.EmailCampaign.Core.Pipelines.GenerateLink.MapHostname, Sitecore.EmailCampaign" resolve="true" />
        <processor type="Sitecore.Modules.EmailCampaign.Core.Pipelines.GenerateLink.Hyperlink.SetAnalyticsQueryStringParameters, Sitecore.EmailCampaign" />
        <processor type="Sitecore.Modules.EmailCampaign.Core.Pipelines.GenerateLink.Hyperlink.HandleInternalLink, Sitecore.EmailCampaign" />
        <processor type="Sitecore.Modules.EmailCampaign.Core.Pipelines.GenerateLink.GeneratePreviewLink, Sitecore.EmailCampaign" />
        <processor type="Sitecore.Modules.EmailCampaign.Core.Pipelines.GenerateLink.Hyperlink.GenerateHyperlink, Sitecore.EmailCampaign">
          <RedirectPagePath>/sitecore%20modules/Web/EXM/RedirectUrlPage.aspx</RedirectPagePath>
          <UrlQueryKey ref="settings/setting[@name='QueryStringKey.RedirectUrl']/@value" />
        </processor>
        <processor type="Sitecore.Modules.EmailCampaign.Core.Pipelines.GenerateLink.Hyperlink.EncryptQueryString, Sitecore.EmailCampaign">
          <param desc="queryStringEncryption" ref="queryStringEncryption" />
        </processor>
      </modifyHyperlink>

Go To Top

Click Tracking Pipeline

On the Content Delivery servers, when a link is clicked on from an EXM message, the CD server processes the <redirectUrl> pipeline to process all of the appropriate trackings. This pipeline has a lot of useful processors. Once the processing is complete, the click action is redirected to the original URL intended.

            <group groupName="exm.messageEvents">
                <pipelines>
                    <!-- REDIRECT URL PIPELINE
                         This pipeline is executed when Email Experience Manager receives a request to redirect
                         a page request from an email link to the correct destination page.
                      -->
                    <redirectUrl>
                        <!-- Retrieves the message item associated with the redirect event. -->
                        <processor  type="Sitecore.EmailCampaign.Cd.Pipelines.RedirectUrl.GetMessage, Sitecore.EmailCampaign.Cd" resolve="true"/>
                        <!-- Determines whether the link provided in the request is a reference to a page on the local web site. -->
                        <processor type="Sitecore.EmailCampaign.Cd.Pipelines.RedirectUrl.CheckInternalLink, Sitecore.EmailCampaign.Cd" resolve="true" />
                        <!-- Constructs the URL to redirect the request to. -->
                        <processor type="Sitecore.EmailCampaign.Cd.Pipelines.RedirectUrl.SetRedirectToUrl, Sitecore.EmailCampaign.Cd" resolve="true">
                            <internalCarryoverFields hint="list:AddInternalCarryoverField">
                                <carryoverField type="Sitecore.EmailCampaign.Cd.Pipelines.RedirectUrl.CarryoverField, Sitecore.EmailCampaign.Cd">
                                    <param desc="fieldKey" ref="settings/setting[@name='QueryStringKey.MessageId']/@value" />
                                    <param desc="urlPattern">SubscriptionPreferences.ashx|.*ConfirmSubscription.aspx|.*Unsubscribe.aspx|UnsubscribeFromAll.aspx.*|.*sc_pd_view=1.*</param>
                                </carryoverField>
                                <carryoverField type="Sitecore.EmailCampaign.Cd.Pipelines.RedirectUrl.CarryoverField, Sitecore.EmailCampaign.Cd">
                                    <param desc="fieldKey" ref="settings/setting[@name='QueryStringKey.AnalyticsContactId']/@value" />
                                    <param desc="urlPattern">SubscriptionPreferences.ashx|.*ConfirmSubscription.aspx|.*Unsubscribe.aspx|UnsubscribeFromAll.aspx.*|.*sc_pd_view=1.*</param>
                                </carryoverField>
                                <carryoverField type="Sitecore.EmailCampaign.Cd.Pipelines.RedirectUrl.CarryoverField, Sitecore.EmailCampaign.Cd">
                                    <param desc="fieldKey" ref="settings/setting[@name='QueryStringKey.ContactIdentifierSource']/@value" />
                                    <param desc="urlPattern">SubscriptionPreferences.ashx|.*ConfirmSubscription.aspx|.*Unsubscribe.aspx|UnsubscribeFromAll.aspx.*|.*sc_pd_view=1.*</param>
                                </carryoverField>
                                <carryoverField type="Sitecore.EmailCampaign.Cd.Pipelines.RedirectUrl.CarryoverField, Sitecore.EmailCampaign.Cd">
                                    <param desc="fieldKey" ref="settings/setting[@name='QueryStringKey.ContactIdentifierIdentifier']/@value" />
                                    <param desc="urlPattern">SubscriptionPreferences.ashx|.*ConfirmSubscription.aspx|.*Unsubscribe.aspx|UnsubscribeFromAll.aspx.*|.*sc_pd_view=1.*</param>
                                </carryoverField>
                                <carryoverField type="Sitecore.EmailCampaign.Cd.Pipelines.RedirectUrl.CarryoverField, Sitecore.EmailCampaign.Cd">
                                    <param desc="fieldKey" ref="settings/setting[@name='QueryStringKey.Campaign']/@value" />
                                    <param desc="urlPattern">SubscriptionPreferences.ashx|.*ConfirmSubscription.aspx|.*Unsubscribe.aspx|UnsubscribeFromAll.aspx.*|.*sc_pd_view=1.*</param>
                                </carryoverField>
                                <carryoverField type="Sitecore.EmailCampaign.Cd.Pipelines.RedirectUrl.CarryoverField, Sitecore.EmailCampaign.Cd">
                                    <param desc="fieldKey" ref="settings/setting[@name='QueryStringKey.TargetLanguage']/@value" />
                                    <param desc="urlPattern">SubscriptionPreferences.ashx|.*ConfirmSubscription.aspx|.*Unsubscribe.aspx|UnsubscribeFromAll.aspx.*|.*sc_pd_view=1.*</param>
                                </carryoverField>
                                <carryoverField type="Sitecore.EmailCampaign.Cd.Pipelines.RedirectUrl.CarryoverField, Sitecore.EmailCampaign.Cd">
                                    <param desc="fieldKey" ref="settings/setting[@name='QueryStringKey.TestValueIndex']/@value" />
                                    <param desc="urlPattern">SubscriptionPreferences.ashx|.*ConfirmSubscription.aspx|.*Unsubscribe.aspx|UnsubscribeFromAll.aspx.*|.*sc_pd_view=1.*</param>
                                </carryoverField>
                                <carryoverField type="Sitecore.EmailCampaign.Cd.Pipelines.RedirectUrl.CarryoverField, Sitecore.EmailCampaign.Cd">
                                    <param desc="fieldKey" ref="settings/setting[@name='QueryStringKey.EmailHistoryEntryId']/@value" />
                                    <param desc="urlPattern">SubscriptionPreferences.ashx|.*ConfirmSubscription.aspx|.*Unsubscribe.aspx|UnsubscribeFromAll.aspx.*|.*sc_pd_view=1.*</param>
                                </carryoverField>
                            </internalCarryoverFields>
                        </processor>
                        <!-- Registers the link click event in emailEventStorage and attaches the result to the pipeline argument. -->
                        <processor type="Sitecore.EmailCampaign.Cd.Pipelines.RedirectUrl.RegisterMessageEvent, Sitecore.EmailCampaign.Cd">
                            <param desc="eventStorage" ref="exm/emailEventStorage" />
                            <param desc="duplicateProtectionIntervalSecs"
                                   ref="settings/setting[@name='EXM.DuplicateProtectionInterval']/@value" />
                            <param desc="logger" ref="exmLogger" />
                        </processor>
                        <!-- Registers custom page events. Internal page references matching the IgnoredUrlPattern will not add the event. -->
                        <processor type="Sitecore.EmailCampaign.Cd.Pipelines.RedirectUrl.RegisterPageEvents, Sitecore.EmailCampaign.Cd" resolve="true">
                            <IgnoredUrlPattern>SubscriptionPreferences.ashx|.*ConfirmSubscription.aspx|.*Unsubscribe.aspx|UnsubscribeFromAll.aspx.*|.*sc_pd_view=1.*</IgnoredUrlPattern>
                        </processor>
                        <!-- Triggers the campaign associated with the email message. -->
                        <processor type="Sitecore.EmailCampaign.Cd.Pipelines.RedirectUrl.TriggerCampaign, Sitecore.EmailCampaign.Cd" resolve="true">
                            <IgnoredUrlPattern>SubscriptionPreferences.ashx|.*ConfirmSubscription.aspx|.*Unsubscribe.aspx|UnsubscribeFromAll.aspx.*|.*sc_pd_view=1.*</IgnoredUrlPattern>
                        </processor>
                        <!-- Marks the current session as an email click session. -->
                        <processor type="Sitecore.EmailCampaign.Cd.Pipelines.RedirectUrl.MarkAsEmailClickSession, Sitecore.EmailCampaign.Cd" resolve="true"/>
                        <!-- Identifies the xDB contact related to the event in the xDB tracker. -->
                        <processor type="Sitecore.EmailCampaign.Cd.Pipelines.RedirectUrl.IdentifyContact, Sitecore.EmailCampaign.Cd" resolve="true">
                            <param desc="logger" ref="exmLogger" />
                        </processor>
                        <!-- Updates the classification of the identified contact if it is currently greater than a given threshold. -->
                        <processor type="Sitecore.EmailCampaign.Cd.Pipelines.RedirectUrl.UpdateContactClassification, Sitecore.EmailCampaign.Cd" resolve="true">
                            <LowerClassificationThreshold>900</LowerClassificationThreshold>
                            <NewClassification>0</NewClassification>
                        </processor>
                        <!-- Resets the email bounce counter of the identified contact to zero. -->
                        <processor type="Sitecore.EmailCampaign.Cd.Pipelines.RedirectUrl.ResetContactEmailBounceCount, Sitecore.EmailCampaign.Cd" resolve="true">
                            <EmailAddressesFacetName ref="model/entities/contact/facets/facet[@name='Emails']/@name" />
                        </processor>
                        <!-- Sets the channel id of the current visit according to the campaign activity associated with the email message. -->
                        <processor type="Sitecore.EmailCampaign.Cd.Pipelines.RedirectUrl.SetVisitChannelId, Sitecore.EmailCampaign.Cd" resolve="true"/>
                    </redirectUrl>
                </pipelines>
            </group>

Go To Top

EXM API’s

There are a number of API’s and .NET Service endpoints that can be referenced in solutions. The list below highlights some of the more common services.

EXM Client API Service

This service was covered in detail during the 25 Days of Sitecore EXM series. Day 21 describes this service in detail.

Go To Top

Subscription Manager Service

Available through Dependency Injection. This service can only be called and referenced on Content Management servers. This service does NOT work on CD’s.

using Sitecore.Data;
using Sitecore.Modules.EmailCampaign;
using Sitecore.Modules.EmailCampaign.Messages;
using Sitecore.XConnect;
using System;

namespace Sitecore.EmailCampaign.Cm
{
  public interface ISubscriptionManager
  {
    bool Subscribe(ContactIdentifier contactIdentifier, Guid messageId, bool subscriptionConfirmation);

    bool Unsubscribe(ContactIdentifier contactIdentifier, Guid messageId);

    bool UnsubscribeFromAll(ContactIdentifier contactIdentifier, Guid managerRootId);

    bool UnsubscribeFromAll(Contact contact, ManagerRoot managerRoot);

    bool ConfirmSubscription(string cid);

    string GetConfirmationKey(Guid recipientListId, ContactIdentifier contactIdentifier, ManagerRoot managerRoot);

    bool ConfirmSubscription(ShortID id);

    bool AddToGlobalOptOutList(ContactIdentifier contactIdentifier, ManagerRoot managerRoot);

    bool RemoveContactFromList(ContactIdentifier contactIdentifier, Guid listId);

    bool RemoveContactFromList(Contact contact, Guid listId);

    bool AddContactToList(ContactIdentifier contactIdentifier, Guid listId);

    Guid GetSubscriptionListFromMessage(MessageItem messageItem);
  }
}

Go To Top

Manager Root Service

Available through Dependency Injection. This can also be run on all Sitecore role servers.

using Sitecore.Data.Items;
using System;
using System.Collections.Generic;

namespace Sitecore.Modules.EmailCampaign.Services
{
  public interface IManagerRootService
  {
    List<ManagerRoot> GetManagerRoots();

    ManagerRoot GetManagerRootFromId(Guid id);

    ManagerRoot GetManagerRootFromItem(Item rootItem);

    ManagerRoot GetManagerRootFromChildItem(Item childItem);

    ManagerRoot GetManagerRoot(Guid managerRootId);

    Guid CreateRoot();
  }
}

Go To Top

Leave a 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 )

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 1,238 other subscribers

Blog Stats

  • 132,847 hits
Follow Sitecore Hacker on WordPress.com
Sitecore® and Own the Experience® are registered trademarks of Sitecore Corporation A/S in the U.S. and other countries.  This website is independent of Sitecore Corporation, and is not affiliated with or sponsored by Sitecore Corporation.
%d bloggers like this: