Showing posts with label Alerts. Show all posts
Showing posts with label Alerts. Show all posts

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.

Tuesday, May 7, 2019

Maximum alerts limit per user in Sharepoint

When you try to subscribe for alerts to specific user account in Sharepoint and get the following error:

You have created the maximum number of alerts allowed for this site

it may be so that you have reached alerts limit for this user in current site. This per-web application setting and it is possible to extend this limit in Central administration > Manage web applications > web app > General settings > Alerts:

By default it is set to 500 alerts per user. After you will increase this limit error should disappear.

Wednesday, October 17, 2018

UnauthorizedAccessException when try to delete alert from Sharepoint site

If you try to delete alerts from the Sharepoint site programmatically:

SPWeb web = ...;
web.Alerts.Delete(alertId);

You may face with UnauthorizedAccessException:

<nativehr>0x80070005</nativehr><nativestack></nativestack>
    at Microsoft.SharePoint.SPGlobal.HandleUnauthorizedAccessException(UnauthorizedAccessException ex)
    at Microsoft.SharePoint.Library.SPRequest.DeleteSubscription(String bstrUrl, String bstrListName, String bstrSubId, Boolean bListItem, UInt32 ulItemId, Boolean bSiteAdmin, Int32 lUserId)
    at Microsoft.SharePoint.SPAlertCollection.Delete(Guid idAlert)

First thing to check is of course that account under which the code above is executed has all necessary permissions on the site. If this is the case but the problem is still there check that your site collection is not in readonly mode. You may do it using the following PowerShell command:

Get-SPSite -Id http://example.com | select ReadOnly,Readlocked,WriteLocked,LockIssue | ft -autosize

If site is in readonly mode result will look like this:

image

and if site is not readonly it will look like this:

image

You may unlock site collection using the following PowerShell command:

Set-SPSite -Id http://example.com -LockState Unlock

And set it to readonly mode like this:

Set-SPSite -Id http://example.com -LockState ReadOnly

Hope that this information will be helpful.

Sunday, June 24, 2012

Sharepoint bug: incorrect itemId in summary alerts with custom alert handler

Some time ago I faced with strange behavior. Custom alert template with custom alert handler was used for alerts for particular list. Immediate alerts worked successfully and custom alert handler was called properly. However summary daily alerts didn’t work.

Custom alert handler is inheritor of IAlertNotifyHandler interface which allows you to override default behavior of alerts processing. E.g. you may log all alerts with custom handler. Here are several examples which shows how to implement custom alert handler step by step: SharePoint – Customizing Alert emails using IAlertNotifyHandler, How To: Customizing alert emails using IAlertNotifyHandler. Shortly you need to override IAlertNotifyHandler.OnNotification method. It has single parameter of SPAlertHandlerParams type. In turn it has SPAlertEventData[] eventData property.

When custom alert handler processes immediate alert (which is caused e.g. by adding new item) eventData has single element and its itemId property contains integer identifier of the list item which caused alert. Using this identifier you may open list item in your handler and read its metadata, e.g. determine user who modified this item.

When summary alert is processed, eventData contains several items – separate item for each event. The problem is that if you will iterate through collection and get eventData[i].itemId you will get any except correct integer identifier (it can be 0, 186, 1932917208, etc., while real identifiers will be 10, 11, 12). For me such behavior looks like a bug.

This issue was mentioned also in this forum thread: SPAlertHandlerParams - not behaving correctly for daily alerts. And author Johnny Dogbert provided workaround:

   1: string url = ahp.eventData[i].itemFullUrl;
   2: int itemId = int.Parse(url.Substring(url.LastIndexOf('/') + 1).Replace("_.000", ""));

It works because in itemFullUrl for list items Sharepoint passes the following URL: testsite/Lists/Test list/10_.000. I.e. we just get id from URL. Note that for documents it won’t most probably work – you will need to open SPFile from url and get its id using SPFile.Item.ID property.

This problem is reproducible on Sharepoint 2007. I didn’t test it on Sharepoint 2010, if you will test it please share the results in comments.

How to trigger and test daily alerts in Sharepoint

In Sharepoint it is possible to create alerts with different frequency:

  • immediate – sent immediately when next time immediate alerts job will run
  • daily – sent daily also by immediate alerts job
  • weekly – sent weekly

If you create new daily alert and want to see whether it will work or not it is not very convenient to wait 24 day until Sharepoint will sent them next time. In this post I will show several ways to trigger summary alerts and send them when you need.

Method 1. When you add a new daily alert, new row is added to the SchedSubscriptions table into Sharepoint content database. This is the key element of this method. We are interesting in the following 2 columns in this table:

  1. NotifyTime
  2. NotifyTimeUNC (NotifyTime minus 3 hours)

In these columns Sharepoint stores time when next time daily alert for particular list will be sent. So first of all determine row which corresponds to your list:

   1: SELECT * FROM SchedSubscriptions

Table contains SiteUrl, WebUrl, ListUrl columns. Using them you will be able to find needed row. Copy Id (uniqueidentifier) and execute the following SQL query:

   1: declare @s datetime
   2: declare @u datetime
   3: set @s = CAST('2012-06-24 12:00:00.000' as datetime)
   4: set @u = CAST('2012-06-24 09:00:00.000' as datetime)
   5:  
   6: update dbo.SchedSubscriptions
   7: set NotifyTime = @s, NotifyTimeUTC = @u
   8: where Id = '...'

In this example in Id you should specify value which you copied from previous query’s result, @s corresponds to NotifyTime, @u to NotifyTimeUNC (NotifyTime minus 3 hours). Time should be in past (comparing with current datetime) – only in this case Sharepoint will send daily alerts.

After that wait some time. Exact time of waiting depends on the job-immediate-alerts property which can be determined by the following command:

   1: stsadm -o getproperty -pn job-immediate-alerts -url http://example.com

for testing you can set it to 1 minute:

   1: stsadm -o setproperty -pn job-immediate-alerts -url http://example.com -pv "every 1 minutes between 0 and 59"

but after testing it is better to revert it e.g. to 5 minutes:

   1: stsadm -o setproperty -pn job-immediate-alerts -url http://example.com -pv "every 5 minutes"

So after this time if you will check SchedSubscriptions table you will see that time which you updated is increased by 1 day: in out example it will be “2012-06-25 12:00:00.000” for NotifyTime and “2012-06-25 09:00:00.000” for NotifyTimeUNC. It means that Sharepoint processed daily alert and it was sent. If everything is Ok, you or alert’s recipient should get email alert with daily summary.

Method 2. I found it in the following forum thread: SPAlertHandlerParams - not behaving correctly for daily alerts, but didn’t test it by myself. May be it will be useful for you as well:

   1: SPSite site = new SPSite("http://example.com");
   2: SPWeb web = site.OpenWeb();
   3: SPAlert alert = web.Alerts[new Guid("...")];
   4: alert.AlertFrequency = SPAlertFrequency.Daily; 
   5: alert.AlertTime = DateTime.Now.AddMinutes(1);
   6: alert.Update();

For general alerts troubleshooting I recommend the following articles: The Truth About How Daily SharePoint Alerts Actually Work, Troubleshooting Alerts. They will economy some time for you. Possibility to trigger daily alerts is very important for troubleshooting. It helped me in my work, hope it will be helpful for you as well.

Friday, March 2, 2012

One problem with updating alert template for Sharepoint list

In this post I would like to describe one problem with Sharepoint alerts. Alerting is OTB Sharepoint feature which can be used for subscribing on the events from list or document library. Supported event types are:

  • All changes
  • New items are added
  • Existing items are modified
  • Items are deleted

Each list has single associated alert template. Programmatically you can read and set this alert template using SPList.AlertTemplate property. Each list created using OTB list template (e.g. Custom list) has associated OTB alert template. Alert templates are located in 14/Template/Xml/AlertTemplates.xml file. E.g. for Custom list there is “SPAlertTemplateType.GenericList” alert template. Alert templates contain subject and body which will be sent to end user.

You can modify existing alert templates or create new one. Most of the articles which I found say that you should not make changes in original AlertTemplates.xml (this is quite reasonable because it may be overriden by next Sharepoint update). Instead create a copy of this file, modify it (add new alert template with unique name or update existing template) and call updatealerttemplates stsadm command:

   1: stsadm -o updatealerttemplates -url http://example.com -f MyAlerttemplates.xml

However it is not the only way to create alerts templates. You can also create alert templates in configuration database. I will write separate post about it.

But let’s return to the original topic: suppose that you created new custom list. After creation it will have SPAlertTemplateType.GenericList alert template associated. Then users subscribe to events from this list or you subscribe them by yourself (it is possible to specify different users in the OTB alerts subscribe page). After that you change alert template, e.g. programmatically:

   1: list.AlertTemplate = alertTemplate;
   2: list.Update();

or using 3rd party component via UI. You may expect that users who subscribed to the list previously will receive emails created with updated alert template. But this is not the case. Previously subscribed users will receive emails created with old alert template, but those users who will subscribe after modification of alert template – will receive new emails.

It may cause many problems and a lot of debugging efforts. E.g. if you created custom alert handler (inheritor of IAlertNotifyHandler interface), attach it to some alert template (here is the good example of how to do it: How To: Customizing alert emails using IAlertNotifyHandler. It also uses example with file, not with config database), then associate alert template with list – but it is not fired anyway. One of the reason is that users were subscribed before alert template was changed for this list.

One way to avoid it is to set alert template for all subscribers when list alert template was changed. Here is code for this (didn’t test it by myself, but it should work):

   1: list.AlertTemplate = at;
   2: list.Update();
   3:  
   4: foreach (SPAlert alert in list.ParentWeb.Alerts)
   5: {
   6:     if (alert.ListID == list.ID)
   7:     {
   8:         alert.AlertTemplate = list.AlertTemplate;
   9:         alert.Update();
  10:     }
  11: }

or re-subscribe users manually. Hope this information will be useful for you if you will face with similar problem.