Menu

Intro and how notifications work in Umbraco.

Umbraco comes with the built-in functionality to send email notifications for users that subscribed to receive them when specific actions happen.

This is base functionality on top of which Umbraco's "Translation workflow" and "Approval workflow" can be built.

For example, after subscribing to the Publish action, the subscriber should receive an email whenever a given page or any descendants (!) gets published.

Sample email after publishing "Contact" node from "Fanoe Starter Kit" :

 

This probably isn't the most pretty email that you've seen … thankfully, umbraco allows us to override the HTML template!
HTML and plain-text email copy can be found in the (slightly hidden) Umbraco language files:
\Umbraco\Config\Lang

Each file here represents a different culture which the Umbraco backend supports. The backend can be displayed in each of these cultures (this setting is user specific).
Depending on your user's setting please find en.xml / en_us.xml files and modify settings for following keys: mailBody, mailBodyHtml, mailSubject

 

Note: it's easy to test email sending by modifying your development web.config for all SmtpClient email to be sent to file system directory:

 

That's great - we have a way to customise the HTML template, and make umbraco send nicely branded emails.
Umbraco uses SmtpClient internally so emails will always be sent using mailSettings configuraiton specified in the web.config.
That means email can be sent using services like SendGrid, dotMailer, other email sender service or even your own SMTP.

This functionality may already cover your requirements and you can stop reading! :) If you need more complicated customisation you will quite likely run into several… :

Problems

The functionality described above comes with following issues:

  1. NotificationService's code for finding users
    Up until version 7.5.4 the notification service was getting all the users in the system, getting all of their notifications and getting ALL existing versions of node that was modified.
    As you can imagine, that approach could slow down publishing on installations with large number of users and updates.
    Currently Umbraco only retrieves last 2 versions but maybe you don't need to display property changes in HTML email?
  2. Html in config
    Maybe you want to have a better way of managing your email content generation.
    One of the common ways is to use *.cshtml file and RazorEngine to generate HTML email. That option is suitable when editors don't need the control over email template generation.
    But what if it's required to manage the email template via 3rd party service like dotMailer? https://www.dotmailer.com/platform/email-marketing/
  3. Construction of HTML for updated properties.
    I personally don't think this code should be there at all, 1st screenshot in this post shows how the email looks when JSON-based property values exist in the template.
    But for people who do want to display some sort of list of changes then there should be a way of creating a provider; an injectable piece of code where you can decide which properties are displayed, in what format and how the HTML looks for this.
    Currently umbraco does this: 
  4. Additional data / processing
    Maybe you want to do some additional processing before sending the email.
    Maybe something like … recording each notification sent to the user and updating email with copy like "Send for Approval has been performed on [Page], You have [X] outstanding approvals".
  5. Not using SMTP.
    Most of the 3rd party email providers allows sending transactional emails via SMTP but … not all of them.
    You might want to send emails by sending message using provider's web-service API.
    Maybe some editors prefer to be notified via text-message… or a message on Slack/Yammer?
    Currently umbraco makes an assumption that it will be SMTP client.

Further Problems

If we decide that we do want to change how notifications are constructed and sent you may think "ok, that's easy, Umbraco has a collection of various services that implement interfaces! I'll just write my own INotificationService implementation".
Yes…and no….
Umbraco does indeed have these services but there is (currently!) no easy, non-hacky way to change registration of them.
Umbraco's ServiceContext has 2 constructors:

  • 1st takes 26 parameters, where each is an object that implements one of umbraco's services.

This one is only used for Umbraco Unit Tests but looks like a good candidate!

Unfortunately …there are 27 services that need to be assigned (!) - IServerRegistrationService is missing from the constructor parameters.
Until this problem is resolved, using this constructor will make umbraco throw an exception ( http://issues.umbraco.org/issue/U4-9097 )

  • 2nd constructor takes several parameters like cache helper, logger, repository factory etc.
    When called it will assign all services with Umbraco's implementation of interfaces - this constructor doesn't allow any service customisation.

ServiceContext is created during Umbraco startup process.
CreateServiceContext is a virtual method on BootManager class and a splendid candidate for what we need.

Implementation options

  1. Custom Boot Manager class and creation of service context.
    
    	    public class MyBootManager : WebBootManager
    	    {
    	        public MyBootManager(UmbracoApplicationBase umbracoApplication) : base(umbracoApplication)
    	        {
    	        }
    	
    	        protected override ServiceContext CreateServiceContext(DatabaseContext dbContext, IDatabaseFactory dbFactory)
    	        {
    	            var serviceCtx = base.CreateServiceContext(dbContext, dbFactory);
    	
    	            var newServiceCtx = new ServiceContext(serviceCtx.ContentService, serviceCtx.MediaService,
    	                serviceCtx.ContentTypeService, serviceCtx.DataTypeService,
    	                serviceCtx.FileService, serviceCtx.LocalizationService, serviceCtx.PackagingService,
    	                serviceCtx.EntityService, serviceCtx.RelationService,
    	                serviceCtx.MemberGroupService, serviceCtx.MemberTypeService, serviceCtx.MemberService,
    	                serviceCtx.UserService, serviceCtx.SectionService,
    	                serviceCtx.ApplicationTreeService, serviceCtx.TagService,
    	                new MyNotificationService(serviceCtx.NotificationService), 
    	                serviceCtx.TextService,
    	                serviceCtx.AuditService,
    	                serviceCtx.DomainService, serviceCtx.TaskService, serviceCtx.MacroService,
    	                serviceCtx.PublicAccessService, serviceCtx.ExternalLoginService,
    	                serviceCtx.MigrationEntryService, serviceCtx.RedirectUrlService);
    	
    	            return newServiceCtx;
    	        }
    	    }
    
    

    And then return this boot manager as the one used to initialise umbraco.

    
        public class Global : UmbracoApplication
    	{
        	protected override IBootManager GetBootManager()
        	{
            	return new MyBootManager(this);
        	}
    	}
    
    

    This is the preferred solution and should be used, unfortunately currently (7.5.4) constructor is missing 1 parameter as mentioned above.

  2. Custom EventHandlers for various umbraco actions (Publish, Save, Send to publish etc) and custom code to send notifications.
    It doesn't feel like this is the correct solution because it would require trying to disable / not use umbraco’s send notification functionality while at the same time writing code that will find the user's notification subscription and handler various events
  3. Reflection until problem from point 1) is resolved.

	public class MyBootManager : WebBootManager
	{
    	public MyBootManager(UmbracoApplicationBase umbracoApplication) : base(umbracoApplication)
    	{
    	}
 
    	protected override ServiceContext CreateServiceContext(DatabaseContext dbContext, IDatabaseFactory dbFactory)
    	{
        	var serviceCtx = base.CreateServiceContext(dbContext, dbFactory);
 
        	INotificationService umbNotificationService = serviceCtx.NotificationService;
 
        	var notificationService = new Lazy(() => new MyNotificationService(umbNotificationService));
 
        	var prop = serviceCtx.GetType().GetField("_notificationService", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
        	prop.SetValue(serviceCtx, notificationService);
 
        	return serviceCtx;
    	}
	}

Sample Code - Wrapper

Unless you want to totally overwrite umbraco functionality, it's worth creating a wrapper for Umbraco notification service and overwrite only methods we need to change.
SendNotifications method can be a bit confusing so I'll try to explain what each of the parameters does:

  • operatingUser - user that performed action that triggered notification
  • entities - entities that are involved in change that triggered notification.
  • action - letter code representing action that was performed
  • actionName - readable name of the action that was performed
  • http - current http context..

Last 2 parameters are slightly strange delegates. I suggest to ignore these parameters and construct email subject and body on your own.
I'm going to explain them briefly in case you wanted to know what they do.

  • createSubject - delegate that takes 2 parameters (user to send notification to and array of "subject vars" strings). Should return string - email subject.

"subject vars" is an array with 3 items:
[0] - URL of format: serverName:port/umbraco
[1] - readable name of the action
[2] - name of the content / node

  • createBody - delegate that takes 2 parameters (user to send notification to and array of "body vars" strings). Should return string - email body.

"body vars" is an array with 8 items:
[0] - name of the notification recipient.
[1] - readable name of the action
[2] - name of the content / node
[3] - name of the user that performed action that triggered notification
[4] - URL of format: serverName:port/umbraco
[5] - content / node ID
[6] - summary of changes as HTML table rows, see point 3) from Problems section above.
[7] - URL of format: scheme://serverName:port/nodeId.aspx
Array of strings is used by umbraco to replace elements of email subject and body from language file. e.g.
<key alias="mailSubject">[%0%] Notification about %1% performed on %2%</key>
Here you can see that %0%, %1% and %2% will be replaced with first, second and third element in "subject vars"

Sample skeleton of custom notification service: 


public class MyNotificationService : INotificationService
	{
    	private readonly INotificationService _umbracoNotificationService;
 
    	public MyNotificationService(INotificationService umbracoNotificationService)
    	{
        	_umbracoNotificationService = umbracoNotificationService;
    	}
 
    	public void SendNotifications(IUser operatingUser, IEnumerable entities, string action, string actionName, HttpContextBase http,
        	Func<IUser, string[], string> createSubject, Func<IUser, string[], string> createBody)
    	{
        	//TODO: check umbraco notification service implementation and get users that subscribed for given action and send your own notifications
        	throw new NotImplementedException();
    	}
 
    	public void SendNotifications(IUser operatingUser, IUmbracoEntity entity, string action, string actionName,
        	HttpContextBase http, Func<IUser, string[], string> createSubject, Func<IUser, string[], string> createBody)
    	{
     	   SendNotifications(operatingUser, new[] { entity }, action, actionName, http, createSubject, createBody);
    	}
 
    	#region call existing umbraco functionality
 
    	public IEnumerable GetUserNotifications(IUser user)
    	{
        	return _umbracoNotificationService.GetUserNotifications(user);
    	}
 
    	public IEnumerable GetUserNotifications(IUser user, string path)
    	{
        	return _umbracoNotificationService.GetUserNotifications(user, path);
    	}
 
    	public IEnumerable GetEntityNotifications(IEntity entity)
    	{
        	return _umbracoNotificationService.GetEntityNotifications(entity);
    	}
 
    	public void DeleteNotifications(IEntity entity)
    	{
        	_umbracoNotificationService.DeleteNotifications(entity);
    	}
 
    	public void DeleteNotifications(IUser user)
    	{
        	_umbracoNotificationService.DeleteNotifications(user);
    	}
 
    	public void DeleteNotifications(IUser user, IEntity entity)
    	{
        	_umbracoNotificationService.DeleteNotifications(user, entity);
    	}
 
    	public Notification CreateNotification(IUser user, IEntity entity, string action)
    	{
   	     return _umbracoNotificationService.CreateNotification(user, entity, action);
    	}
        
    	#endregion call existing umbraco functionality
 
	}


 Conclusion

We have learned how to replace umbraco services which can be very useful when customising umbraco behaviour.
Now we can change how umbraco sends notifications and we also have a good starting point for changing other services like content service, if, of course, we ever need to :)