Sunday, August 25, 2019

Internal mechanism of reply emails in Sharepoint discussion board

In Sharepoint you may create discussion board lists where users may create new discussion threads. When somebody writes reply into particular discussion author of this discussion receives email notification. In this post I will write about internal mechanism of these reply emails i.e. how they are implemented internally.

We may expect that reply notification emails in discussion boards are implemented via standard Sharepoint email alerts. However this is not the case. If you will check alerts list of parent web of discussion board list you will see that it will be empty (or it may contain alerts created in different list. Also it may contain alerts for discussion board but this is different story – I will write more about it below):

$web = Get-SPWeb http://example.com
$web.Alerts

I.e. authors of discussion board will still get email notifications on replies even when there are no alerts in the web. It mean that these reply emails are implemented via some different mechanism. Also it means that it is not possible to edit template of these emails by modifying Sharepoint alert templates. Users may still use OTB Sharepoint alerts and subscribe themselves to events in discussion board list: by clicking three dots near discussion thread subject and selecting Alert me link:

In this case real alert will be created and web.Alerts collection will contain it. This alert will be customizable i.e. it will be possible to modify it’s template by editing discussion board alert template. However still it will be different alert from reply notification email: if author of discussion will subscribe him or herself on discussion board event this way then author will get 2 alerts – one as reply notification and another as OTB alert.

So how reply email notifications are implemented then? If it is not OTB alert it may be implemented rather via workflow or via event receiver. If we will check list of workflows for discussion board we will see that it is empty. So only event receivers remain. Let’s try to execute the following PowerShell script which lists all event receivers for specified list:

param( 
    [string]$url,
    [string]$listName
)

$web = Get-SPWeb $url
$list = $web.Lists[$listName]

foreach($eventReceiverDef in $list.EventReceivers)
{
    $eventInfo = $eventReceiverDef.Class + ", " + $eventReceiverDef.Assembly + " – " + $eventReceiverDef.Type
    Write-Host $eventInfo -ForegroundColor green
}

For discussion board it will show the following event receivers:

Microsoft.SharePoint.Portal.CommunityEventReceiver, Microsoft.SharePoint.Portal, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c - ItemAdding
Microsoft.SharePoint.Portal.CommunityEventReceiver, Microsoft.SharePoint.Portal, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c - ItemUpdating
Microsoft.SharePoint.Portal.CommunityEventReceiver, Microsoft.SharePoint.Portal, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c - ItemDeleting
Microsoft.SharePoint.DiscussionListEventReceiver, Microsoft.SharePoint,Version=15.0.0.0,Culture=neutral,PublicKeyToken=71e9bce111e9429c - ItemAdded
Microsoft.SharePoint.Portal.CommunityEventReceiver, Microsoft.SharePoint.Portal, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c - ItemAdded
Microsoft.SharePoint.DiscussionListEventReceiver, Microsoft.SharePoint,Version=15.0.0.0,Culture=neutral,PublicKeyToken=71e9bce111e9429c - ItemUpdated

DiscussionListEventReceiver basically updates LastReplyBy field of the parent discussion board and doesn’t do other actions. So let’s check CommunityEventReceiver. If we will check it’s code via decompiler we will see that in all post event handler methods (ItemAdded, ItemUpdated, ItemDeleted) it calls internal method HandleEvent which in turn calls EventCache.Instance.HandleChange() method:

public sealed class CommunityEventReceiver : SPItemEventReceiver
{
 public CommunityEventReceiver()
 {
 }

 private void HandleEvent(SPItemEventProperties properties)
 {
  bool eventFiringEnabled = base.EventFiringEnabled;
  try
  {
   base.EventFiringEnabled = false;
   using (SPMonitoredScope sPMonitoredScope = new SPMonitoredScope("CommunityEventReceiver::HandleEvent"))
   {
    EventChangeRecord eventChangeRecord = null;
    SPSecurity.RunWithElevatedPrivileges(() => {
     using (SPSite sPSite = new SPSite(properties.Web.Site.ID))
     {
      using (SPWeb sPWeb = sPSite.OpenWeb(properties.Web.ID))
      {
       eventChangeRecord = EventCache.Instance.HandleChange(sPWeb, properties);
      }
     }
    });
    if (eventChangeRecord != null && eventChangeRecord.SocialPostCreationData != null)
    {
     FeedNotificationUtils.AddSocialPostNotification(properties.Web, eventChangeRecord.SocialPostCreationData);
    }
   }
  }
  finally
  {
   base.EventFiringEnabled = eventFiringEnabled;
  }
 }

 public override void ItemAdded(SPItemEventProperties properties)
 {
  this.HandleEvent(properties);
 }

 public override void ItemDeleted(SPItemEventProperties properties)
 {
  this.HandleEvent(properties);
 }

 public override void ItemUpdated(SPItemEventProperties properties)
 {
  this.HandleEvent(properties);
 }

 ...
}

(There are also pre events ItemAdding, ItemUpdating, ItemDeleting but they are not relevant to this post). Let’s now see what happens inside EventCache.Instance.HandleChange. Among with other actions it iterates through internal handlers collection and calls HandleEvent method for each handler in this collection:

public EventChangeRecord HandleChange(SPWeb web, SPItemEventProperties properties)
{
 ...
  BaseCommunityEventHandler[] baseCommunityEventHandlerArray = this.handlers;
  for (int i = 0; i < (int)baseCommunityEventHandlerArray.Length; i++)
  {
   BaseCommunityEventHandler baseCommunityEventHandler = baseCommunityEventHandlerArray[i];
   if (baseCommunityEventHandler.HandledTemplateType == (int)list.BaseTemplate || baseCommunityEventHandler.HandledTemplateType == BaseCommunityEventHandler.HandleAllTemplateTypes)
   {
    baseCommunityEventHandler.HandleEvent(properties, eventChangeRecord);
   }
  }
  ...
 }
 ...
}

Now let’s see what exact handlers are added to this collection:

private void InitializeHandlers()
{
 BaseCommunityEventHandler[] discussionListCommunityEventHandler = new BaseCommunityEventHandler[] { new DiscussionListCommunityEventHandler(), new CategoriesListCommunityEventHandler(), new ReputationCommunityEventHandler(), new MembersListCommunityEventHandler(), new BadgesListCommunityEventHandler(), new CommunityNotificationsEventHandler() };
 this.handlers = discussionListCommunityEventHandler;
}

So there are quite many handlers which implement different features of community sites (reputations, membership, bages, etc). One of them is CommunityNotificationsEventHandler – this is exact handler which sends email notification on reply from discussion board:

internal class CommunityNotificationsEventHandler : BaseCommunityEventHandler
{
 internal override void HandleEvent(SPItemEventProperties properties, EventChangeRecord record)
 {
  ...
  SPList list = record.GetList("properties");
  if (record.EventType == SPEventReceiverType.ItemAdded)
  {
    FeedNotificationUtils.SendEmailNotificationOnReply(record.Web, sPListItem, listItem);
  }
  ...
 }
}

It calls internal FeedNotificationUtils.SendEmailNotificationOnReply() method which sends actual email:

internal static class FeedNotificationUtils
{
 ...
 public static bool SendEmailNotificationOnReply(SPWeb communityWeb, SPListItem topic, SPListItem reply)
 {
  bool flag = false;
  try
  {
   SPUser author = FeedNotificationUtils.GetAuthor(communityWeb, topic);
   if (FeedNotificationUtils.ShouldSendReplyNotification(communityWeb, reply, author))
   {
    UserProfile userProfile = CommonFeedNotificationUtils.GetUserProfile(communityWeb, author);
    if (userProfile != null && (userProfile.get_EmailOptin() & 64) == 0)
    {
     UserProfileApplicationProxy proxy = UserProfileApplicationProxy.GetProxy(CommonFeedNotificationUtils.GetServiceContext(communityWeb));
     string mySitePortalUrl = proxy.GetMySitePortalUrl(ServerApplication.get_CurrentUrlZone(), userProfile.get_PartitionID());
     using (SPSite sPSite = new SPSite(mySitePortalUrl))
     {
      MailMessage mailMessage = null;
      try
      {
       string mySiteEmailSenderName = proxy.GetMySiteEmailSenderName(userProfile.get_PartitionID());
       mailMessage = FeedNotificationUtils.CreateReplyNotificationMailMessage(sPSite.RootWeb, mySiteEmailSenderName, communityWeb, author, topic, reply, mySitePortalUrl);
       using (SPSmtpClient sPSmtpClient = new SPSmtpClient(communityWeb.Site))
       {
        flag = SPMailMessageHelper.TrySendMailMessage(sPSmtpClient, mailMessage);
        if (!flag)
        {
         ...
        }
       }
      }
      finally
      {
       if (mailMessage != null)
       {
        SPMailMessageHelper.DisposeAttachmentStreams(mailMessage);
        mailMessage.Dispose();
       }
      }
     }
    }
   }
  }
  catch (Exception exception1)
  {
   ...
  }
  return flag;
 }
}

So as you can see reply emails in discussion boards are implemented via event handlers. At the end let’s also mention that it is possible to disable reply emails in discussion board – but it will also disable other community features like ratings (i.e. users won’t be able to like replies). In order to do that go to discussion board list settings > Rating settings and set “Allow items in this list to be rated” to No:

It will call internal method ReputationHelper.DisableReputation() which will remove CommunityEventReceiver from discussion board:

internal static class ReputationHelper
{
 ...
 internal static void DisableReputation(SPList list)
 {
  ReputationHelper.HideAllReputationFields(list);
  ReputationHelper.SetExperience(list, string.Empty, false);
  if (list.BaseTemplate == SPListTemplateType.DiscussionBoard)
  {
   List sPViews = new List();
   foreach (SPView view in list.Views)
   {
    sPViews.Add(view);
   }
   Guid[] contentReputationPopularityFieldId = new Guid[] { CommunitiesConstants.ContentReputation_Popularity_FieldId, CommunitiesConstants.ContentReputation_DescendantLikesCount_FieldId, CommunitiesConstants.ContentReputation_DescendantRatingsCount_FieldId, CommunitiesConstants.ContentReputation_LastRatedOrLikedBy_FieldId };
   FunctionalityEnablers.RemoveFieldsFromViews(contentReputationPopularityFieldId, list, sPViews);
   CommunityUtils.RemoveEventReceiver(list, typeof(CommunityEventReceiver).FullName);
   foreach (SPView sPView in sPViews)
   {
    if (sPView.JSLink != null)
    {
     sPView.JSLink = sPView.JSLink.Replace("|sp.ui.communities.js", "");
    }
    sPView.Update();
   }
  }
 }
}

This is how email notifications work in Sharepoint discussion boards. Hope that this information will help you in your work.

No comments:

Post a Comment