static async Task SendNotificationAsync(this RequestInfo requestInfo, IPortalObject @object, string @event, Settings.Notifications notificationSettings, ApprovalStatus previousStatus, ApprovalStatus status, CancellationToken cancellationToken)
		{
			// check
			if (@object == null)
				return;

			// prepare settings
			var events = notificationSettings?.Events;
			var methods = notificationSettings?.Methods;
			var emails = notificationSettings?.Emails;
			var emailsByApprovalStatus = notificationSettings?.EmailsByApprovalStatus;
			var emailsWhenPublish = notificationSettings?.EmailsWhenPublish;
			var webHooks = notificationSettings?.WebHooks;
			var category = @object is Content ? (@object as Content).Category : null;
			var emailSettings = category != null ? category.EmailSettings : @object is Organization ? (@object as Organization).EmailSettings : null;

			var parent = @object.Parent;
			while (parent != null)
			{
				Settings.Notifications parentNotificationSettings = null;
				if (parent is Category parentAsCategory)
				{
					parentNotificationSettings = parentAsCategory.Notifications;
					emailSettings = emailSettings ?? parentAsCategory.EmailSettings;
				}
				else if (parent is ContentType parentAsContentType)
				{
					parentNotificationSettings = parentAsContentType.Notifications;
					emailSettings = emailSettings ?? parentAsContentType.EmailSettings;
				}
				else if (parent is Module parentAsModule)
				{
					parentNotificationSettings = parentAsModule.Notifications;
					emailSettings = emailSettings ?? parentAsModule.EmailSettings;
				}
				else if (parent is Organization parentAsOrganization)
				{
					parentNotificationSettings = parentAsOrganization.Notifications;
					emailSettings = emailSettings ?? parentAsOrganization.EmailSettings;
				}

				events = events != null && events.Count > 0 ? events : parentNotificationSettings?.Events;
				methods = methods != null && methods.Count > 0 ? methods : parentNotificationSettings?.Methods;
				emails = emails ?? parentNotificationSettings?.Emails;
				emailsByApprovalStatus = emailsByApprovalStatus ?? parentNotificationSettings?.EmailsByApprovalStatus;
				emailsWhenPublish = emailsWhenPublish ?? parentNotificationSettings?.EmailsWhenPublish;
				webHooks = webHooks ?? parentNotificationSettings?.WebHooks;
				parent = parent.Parent;
			}

			// stop if has no event
			if (events == null || events.Count < 1 || events.FirstOrDefault(e => e.IsEquals(@event)) == null)
				return;

			// prepare parameters
			var sender = (await requestInfo.GetUserProfilesAsync(new[] { requestInfo.Session.User.ID }, cancellationToken).ConfigureAwait(false) as JArray)?.FirstOrDefault();
			var businessObject = @object is IBusinessObject ? @object as IBusinessObject : null;
			var serviceName = ServiceBase.ServiceComponent.ServiceName;
			var objectName = (businessObject as RepositoryBase)?.GetObjectName() ?? @object.GetTypeName(true);
			var contentType = businessObject?.ContentType as ContentType;
			var organization = await (@object.OrganizationID ?? "").GetOrganizationByIDAsync(cancellationToken).ConfigureAwait(false);
			var alwaysUseHtmlSuffix = organization == null || organization.AlwaysUseHtmlSuffix;
			if (organization != null && organization._siteIDs == null)
				await organization.FindSitesAsync(cancellationToken).ConfigureAwait(false);
			var site = organization?.Sites.FirstOrDefault();
			var siteDomain = $"{site?.SubDomain}.{site?.PrimaryDomain}".Replace("*.", "www.").Replace("www.www.", "www.");
			var siteURL = $"http{(site != null && site.AlwaysUseHTTPs ? "s" : "")}://{siteDomain}/";

			// prepare the recipients and notification settings
			var recipientIDs = new List<string>();
			var sendWebHookNotifications = methods?.FirstOrDefault(method => method.IsEquals("WebHook")) != null && webHooks != null && webHooks.EndpointURLs != null && webHooks.EndpointURLs.Count > 0;
			var sendEmailNotifications = methods?.FirstOrDefault(method => method.IsEquals("Email")) != null;
			var emailNotifications = new Settings.EmailNotifications();

			switch (status)
			{
				case ApprovalStatus.Draft:
					recipientIDs = new[] { @object.CreatedID }.ToList();
					emailNotifications = emailsByApprovalStatus != null && emailsByApprovalStatus.ContainsKey($"{status}")
						? emailsByApprovalStatus[$"{status}"]
						: emails;
					break;

				case ApprovalStatus.Pending:
					recipientIDs = await @object.WorkingPrivileges.GetUserIDsAsync(PrivilegeRole.Editor, cancellationToken).ConfigureAwait(false);
					if (recipientIDs.Count < 1)
						recipientIDs = await @object.WorkingPrivileges.GetUserIDsAsync(PrivilegeRole.Moderator, cancellationToken).ConfigureAwait(false);
					if (recipientIDs.Count < 1)
						recipientIDs = await @object.WorkingPrivileges.GetUserIDsAsync(PrivilegeRole.Administrator, cancellationToken).ConfigureAwait(false);
					if (recipientIDs.Count < 1)
						recipientIDs = new[] { organization.OwnerID }.ToList();
					emailNotifications = emailsByApprovalStatus != null && emailsByApprovalStatus.ContainsKey($"{status}")
						? emailsByApprovalStatus[$"{status}"]
						: emails;
					break;

				case ApprovalStatus.Rejected:
					recipientIDs = new[] { @object.CreatedID }.ToList();
					emailNotifications = emailsByApprovalStatus != null && emailsByApprovalStatus.ContainsKey($"{status}")
						? emailsByApprovalStatus[$"{status}"]
						: emails;
					break;

				case ApprovalStatus.Approved:
					recipientIDs = await @object.WorkingPrivileges.GetUserIDsAsync(PrivilegeRole.Moderator, cancellationToken).ConfigureAwait(false);
					if (recipientIDs.Count < 1)
						recipientIDs = await @object.WorkingPrivileges.GetUserIDsAsync(PrivilegeRole.Administrator, cancellationToken).ConfigureAwait(false);
					if (recipientIDs.Count < 1)
						recipientIDs = new[] { organization.OwnerID }.ToList();
					emailNotifications = emailsByApprovalStatus != null && emailsByApprovalStatus.ContainsKey($"{status}")
						? emailsByApprovalStatus[$"{status}"]
						: emails;
					break;

				case ApprovalStatus.Published:
					recipientIDs = (await @object.WorkingPrivileges.GetUserIDsAsync(PrivilegeRole.Moderator, cancellationToken).ConfigureAwait(false))
						.Concat(await @object.WorkingPrivileges.GetUserIDsAsync(PrivilegeRole.Administrator, cancellationToken).ConfigureAwait(false))
						.Concat(new[] { @object.CreatedID }).ToList();
					if (recipientIDs.Count < 1)
						recipientIDs = new[] { organization.OwnerID }.ToList();
					emailNotifications = emailsByApprovalStatus != null && emailsByApprovalStatus.ContainsKey($"{status}")
						? emailsByApprovalStatus[$"{status}"]
						: emails;
					break;

				case ApprovalStatus.Archieved:
					recipientIDs = (await @object.WorkingPrivileges.GetUserIDsAsync(PrivilegeRole.Moderator, cancellationToken).ConfigureAwait(false))
						.Concat(await @object.WorkingPrivileges.GetUserIDsAsync(PrivilegeRole.Administrator, cancellationToken).ConfigureAwait(false))
						.Concat(new[] { @object.CreatedID, @object.LastModifiedID }).ToList();
					if (recipientIDs.Count < 1)
						recipientIDs = new[] { organization.OwnerID }.ToList();
					emailNotifications = emailsByApprovalStatus != null && emailsByApprovalStatus.ContainsKey($"{status}")
						? emailsByApprovalStatus[$"{status}"]
						: emails;
					break;
			}

			// prepare recipients
			recipientIDs = recipientIDs.Except(new[] { requestInfo.Session.User.ID }).Where(id => !string.IsNullOrWhiteSpace(id)).Distinct(StringComparer.OrdinalIgnoreCase).ToList();
			var recipients = await requestInfo.GetUserProfilesAsync(recipientIDs, cancellationToken).ConfigureAwait(false) as JArray;

			//  send app notifications
			try
			{
				var baseMessage = new BaseMessage
				{
					Type = $"Portals#Notification#{@event}",
					Data = new JObject
					{
						{ "Sender", new JObject
							{
								{ "ID", requestInfo.Session.User.ID },
								{ "Name", sender?.Get<string>("Name") ?? "Unknown" }
							}
						},
						{ "Action", @event },
						{ "Info", new JObject
							{
								{ "ServiceName", serviceName },
								{ "ObjectName", objectName },
								{ "SystemID", @object.OrganizationID },
								{ "ObjectID", @object.ID },
								{ "ObjectTitle", @object.Title },
								{ "Status", $"{status}" },
								{ "PreviousStatus", $"{previousStatus}" },
								{ "Time", DateTime.Now },
							}
						}
					}
				};
				await recipients.Select(recipient => recipient.Get<JArray>("Sessions"))
					.Where(sessions => sessions != null)
					.SelectMany(sessions => sessions.Select(session => session.Get<string>("DeviceID")))
					.ForEachAsync(deviceID => Utility.RTUService.SendUpdateMessageAsync(new UpdateMessage(baseMessage) { DeviceID = deviceID }, cancellationToken)).ConfigureAwait(false);
				if (Utility.Logger.IsEnabled(LogLevel.Debug))
					await requestInfo.WriteLogAsync($"Send app notifications successful\r\n{baseMessage.ToJson()}", cancellationToken).ConfigureAwait(false);
			}
			catch (Exception exception)
			{
				await requestInfo.WriteErrorAsync(exception, cancellationToken, "Error occurred while sending app notifications").ConfigureAwait(false);
			}

			// send email notifications
			if (sendEmailNotifications || emailsWhenPublish != null)
			{
				var appURL = $"/portals/initializer?x-request={("{" + $"\"SystemID\":\"{organization?.ID}\",\"ObjectName\":\"{objectName}\",\"ObjectID\":\"{@object.ID}\"" + "}").Url64Encode()}".GetAppURL();
				var normalizedHTMLs = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
				var definition = RepositoryMediator.GetEntityDefinition(@object.GetType());
				if (definition != null)
				{
					definition.Attributes.Where(attribute => attribute.IsCLOB != null && attribute.IsCLOB.Value).ForEach(attribute =>
					{
						var value = @object.GetAttributeValue<string>(attribute);
						normalizedHTMLs[attribute.Name] = value?.NormalizeHTML().NormalizeURLs(siteURL);
					});
					if (businessObject?.ExtendedProperties != null && definition.BusinessRepositoryEntities.TryGetValue(businessObject.RepositoryEntityID, out var repositiryEntity))
						repositiryEntity?.ExtendedPropertyDefinitions?.Where(propertyDefinition => propertyDefinition.Mode.Equals(ExtendedPropertyMode.LargeText)).ForEach(propertyDefinition =>
						{
							if (businessObject.ExtendedProperties.TryGetValue(propertyDefinition.Name, out var value))
								normalizedHTMLs[propertyDefinition.Name] = (value as string)?.NormalizeHTML().NormalizeURLs(siteURL);
						});
				}

				var @params = new Dictionary<string, ExpandoObject>(StringComparer.OrdinalIgnoreCase)
				{
					["Organization"] = organization?.ToJson(false, false, json =>
					{
						OrganizationProcessor.ExtraProperties.ForEach(name => json.Remove(name));
						json.Remove("Privileges");
						json["AlwaysUseHtmlSuffix"] = alwaysUseHtmlSuffix;
					}).ToExpandoObject(),
					["Site"] = site?.ToJson(json =>
					{
						SiteProcessor.ExtraProperties.ForEach(name => json.Remove(name));
						json.Remove("Privileges");
						json["Domain"] = siteDomain;
						json["URL"] = siteURL;
					}).ToExpandoObject(),
					["ContentTypeDefinition"] = contentType?.ContentTypeDefinition?.ToJson().ToExpandoObject(),
					["ModuleDefinition"] = contentType?.ContentTypeDefinition?.ModuleDefinition?.ToJson(json =>
					{
						(json as JObject).Remove("ContentTypeDefinitions");
						(json as JObject).Remove("ObjectDefinitions");
					}).ToExpandoObject(),
					["Module"] = contentType?.Module?.ToJson(false, false, json =>
					{
						ModuleProcessor.ExtraProperties.ForEach(name => json.Remove(name));
						json.Remove("Privileges");
					}).ToExpandoObject(),
					["ContentType"] = contentType?.ToJson(false, json =>
					{
						ModuleProcessor.ExtraProperties.ForEach(name => json.Remove(name));
						json.Remove("Privileges");
					}).ToExpandoObject(),
					["ParentContentType"] = contentType?.GetParent()?.ToJson(false, json =>
					{
						ModuleProcessor.ExtraProperties.ForEach(name => json.Remove(name));
						json.Remove("Privileges");
					}).ToExpandoObject(),
					["URLs"] = new JObject
					{
						{ "Public", $"{businessObject?.GetURL() ?? $"~/index"}{(alwaysUseHtmlSuffix ? ".html" : "")}".GetWebURL(siteURL) },
						{ "Portal", $"{businessObject?.GetURL() ?? $"~/index"}{(alwaysUseHtmlSuffix ? ".html" : "")}".GetWebURL($"{Utility.PortalsHttpURI}/~{organization?.Alias}/") },
						{ "Private", appURL },
						{ "Review", appURL }
					}.ToExpandoObject(),
					["HTMLs"] = normalizedHTMLs.ToExpandoObject(),
					["Sender"] = new JObject
					{
						{ "ID", requestInfo.Session.User.ID },
						{ "Name", sender?.Get<string>("Name") ?? "Unknown" },
						{ "Email", sender?.Get<string>("Email") },
						{ "URL", $"/users/profiles/{(sender?.Get<string>("Name") ?? "Unknown").GetANSIUri()}?x-request={("{\"ID\":\"" + requestInfo.Session.User.ID + "\"}").Url64Encode()}".GetAppURL() },
						{ "Location", await requestInfo.GetLocationAsync(cancellationToken).ConfigureAwait(false) },
						{ "IP", requestInfo.Session.IP },
						{ "AppName", requestInfo.Session.AppName },
						{ "AppPlatform", requestInfo.Session.AppPlatform }
					}.ToExpandoObject()
				};

				// add information of the CMS Category
				if (category != null)
					@params["Category"] = category.ToJson(false, false, json =>
					{
						CategoryProcessor.ExtraProperties.ForEach(name => json.Remove(name));
						json.Remove("Privileges");
						json["URL"] = $"{category.GetURL()}{(alwaysUseHtmlSuffix ? ".html" : "")}".GetWebURL(siteURL);
					}).ToExpandoObject();

				// normalize parameters for evaluating
				var language = requestInfo.CultureCode ?? "vi-VN";
				var languages = Utility.Languages.ContainsKey(language) ? Utility.Languages[language] : null;
				var requestExpando = requestInfo.ToExpandoObject(requestInfoAsExpandoObject =>
				{
					requestInfoAsExpandoObject.Set("Body", requestInfo.BodyAsExpandoObject);
					requestInfoAsExpandoObject.Get<ExpandoObject>("Header")?.Remove("x-app-token");
				});
				var objectExpando = @object.ToExpandoObject();
				var paramsExpando = new Dictionary<string, object>(@params.ToDictionary(kvp => kvp.Key, kvp => kvp.Value as object), StringComparer.OrdinalIgnoreCase)
				{
					{ "Event", @event },
					{ "Event-i18n", languages?.Get<string>($"events.{@event}") ?? @event },
					{ "ObjectName", objectName },
					{ "ObjectType", @object.GetTypeName() },
					{ "Status", $"{status}" },
					{ "Status-i18n", languages?.Get<string>($"status.approval.{status}") ?? $"{status}" },
					{ "PreviousStatus", $"{previousStatus}" },
					{ "PreviousStatus-i18n", languages?.Get<string>($"status.approval.{previousStatus}") ?? $"{previousStatus}" },
					{ "Signature", emailSettings?.Signature?.NormalizeHTMLBreaks() },
					{ "EmailSignature", emailSettings?.Signature?.NormalizeHTMLBreaks() }
				}.ToExpandoObject();

				JObject instructions = null;
				if (sendEmailNotifications || ([email protected]("Delete") && businessObject != null && status.Equals(ApprovalStatus.Published) && !status.Equals(previousStatus) && emailsWhenPublish != null))
					try
					{
						instructions = JObject.Parse(await UtilityService.FetchWebResourceAsync($"{Utility.APIsHttpURI}/statics/instructions/portals/{language}.json", cancellationToken).ConfigureAwait(false))?.Get<JObject>("notifications");
					}
					catch (Exception exception)
					{
						await requestInfo.WriteErrorAsync(exception, cancellationToken, "Error occurred while fetching instructions").ConfigureAwait(false);
					}

				// send email message
				if (sendEmailNotifications)
					try
					{
						var subject = emailNotifications?.Subject ?? instructions?.Get<JObject>("emailByApprovalStatus")?.Get<JObject>($"{status}")?.Get<string>("subject") ?? instructions?.Get<JObject>("email")?.Get<string>("subject");
						if (string.IsNullOrWhiteSpace(subject))
							subject = "[{{@params(Organization.Alias)}}] - \"{{@current(Title)}}\" was {{@toLower(@params(Event))}}d";
						var body = emailNotifications?.Body ?? instructions?.Get<JObject>("emailByApprovalStatus")?.Get<JObject>($"{status}")?.Get<string>("body") ?? instructions?.Get<JObject>("email")?.Get<string>("body");
						if (string.IsNullOrWhiteSpace(body))
							body = @"Hi,
							The content that titled as ""<b>{{@current(Title)}}</b>"" ({{@params(ObjectName)}} on <a href=""{{@params(Site.URL)}}"">{{@params(Site.Title)}}</a>) was {{@toLower(@params(Event))}}d by {{@params(Sender.Name)}}.
							You can reach that content by one of these URLs below:
							<ul>
								<li>Public website: <a href=""{{@params(URLs.Public)}}"">{{@params(URLs.Public)}}</a></li>
								<li>CMS portals: <a href=""{{@params(URLs.Portal)}}"">{{@params(URLs.Portal)}}</a></li>
								<li>CMS apps: <a href=""{{@params(URLs.Private)}}"">{{@params(URLs.Private)}}</a></li>
							</ul>
							{{@params(EmailSignature)}}";
						var parameters = $"{subject}\r\n{body}"
							.GetDoubleBracesTokens()
							.Select(token => token.Item2)
							.Distinct(StringComparer.OrdinalIgnoreCase)
							.ToDictionary(token => token, token =>
							{
								return token.StartsWith("@[") && token.EndsWith("]")
									? Extensions.JsEvaluate(token.GetJsExpression(requestExpando, objectExpando, paramsExpando))
									: token.StartsWith("@")
										? token.Evaluate(new Tuple<ExpandoObject, ExpandoObject, ExpandoObject>(requestExpando, objectExpando, paramsExpando))
										: token;
							});
						var message = new EmailMessage
						{
							From = emailSettings?.Sender,
							To = recipients.Select(recipient => recipient.Get<string>("Email")).Where(email => !string.IsNullOrWhiteSpace(email)).Join(";") + (string.IsNullOrWhiteSpace(emailNotifications.ToAddresses) ? "" : $";{emailNotifications.ToAddresses}"),
							Cc = emailNotifications?.CcAddresses,
							Bcc = emailNotifications?.BccAddresses,
							Subject = subject.NormalizeHTMLBreaks().Format(parameters),
							Body = body.NormalizeHTMLBreaks().Format(parameters),
							SmtpServer = emailSettings?.Smtp?.Host,
							SmtpServerPort = emailSettings?.Smtp != null ? emailSettings.Smtp.Port : 0,
							SmtpServerEnableSsl = emailSettings?.Smtp != null && emailSettings.Smtp.EnableSsl,
							SmtpUsername = emailSettings?.Smtp?.User,
							SmtpPassword = emailSettings?.Smtp?.UserPassword,
							CorrelationID = requestInfo.CorrelationID
						};
						await Utility.MessagingService.SendEmailAsync(message, cancellationToken).ConfigureAwait(false);
						var log = "Add an email notification into queue successful" + "\r\n" +
							$"- ID: {message.ID}" + "\r\n" +
							$"- Object: {@object.Title} [{@object.GetType()}#{@object.ID}]" + "\r\n" +
							$"- Event: {@event}" + "\r\n" +
							$"- Status: {status} (previous: {previousStatus})" + "\r\n" +
							$"- Sender: {sender?.Get<string>("Name")} ({sender?.Get<string>("Email")})" + "\r\n" +
							$"- To: {message.To}" + (!string.IsNullOrWhiteSpace(message.Cc) ? $" / {message.Cc}" : "") + (!string.IsNullOrWhiteSpace(message.Bcc) ? $" / {message.Bcc}" : "") + "\r\n" +
							$"- Subject: {message.Subject}";
						await requestInfo.WriteLogAsync(log, cancellationToken).ConfigureAwait(false);
						if (Utility.Logger.IsEnabled(LogLevel.Debug))
							Utility.Logger.LogDebug($"Add an email notification into queue successful\r\n{message.ToJson()}");
					}
					catch (Exception exception)
					{
						await requestInfo.WriteErrorAsync(exception, cancellationToken, "Error occurred while adding an email notification into queue").ConfigureAwait(false);
					}

				// send special email message (when publish)
				if ([email protected]("Delete") && businessObject != null && status.Equals(ApprovalStatus.Published) && !status.Equals(previousStatus) && emailsWhenPublish != null)
					try
					{
						var subject = emailsWhenPublish?.Subject ?? instructions?.Get<JObject>("emailsWhenPublish")?.Get<string>("subject");
						if (string.IsNullOrWhiteSpace(subject))
							subject = "[{{@params(Organization.Alias)}}] - \"{{@current(Title)}}\" was published";
						var body = emailsWhenPublish?.Body ?? instructions?.Get<JObject>("emailsWhenPublish")?.Get<string>("body");
						if (string.IsNullOrWhiteSpace(body))
							body = @"Hi,
							The content that titled as ""<b>{{@current(Title)}}</b>"" ({{@params(ObjectName)}} on <a href=""{{@params(Site.URL)}}"">{{@params(Site.Title)}}</a>) was published by {{@params(Sender.Name)}}.
							You can reach that content by one of these URLs below:
							<ul>
								<li>Public website: <a href=""{{@params(URLs.Public)}}"">{{@params(URLs.Public)}}</a></li>
								<li>CMS portals: <a href=""{{@params(URLs.Portal)}}"">{{@params(URLs.Portal)}}</a></li>
								<li>CMS apps: <a href=""{{@params(URLs.Private)}}"">{{@params(URLs.Private)}}</a></li>
							</ul>
							{{@params(EmailSignature)}}";
						var parameters = $"{subject}\r\n{body}"
							.GetDoubleBracesTokens()
							.Select(token => token.Item2)
							.Distinct(StringComparer.OrdinalIgnoreCase)
							.ToDictionary(token => token, token =>
							{
								return token.StartsWith("@[") && token.EndsWith("]")
									? Extensions.JsEvaluate(token.GetJsExpression(requestExpando, objectExpando, paramsExpando))
									: token.StartsWith("@")
										? token.Evaluate(new Tuple<ExpandoObject, ExpandoObject, ExpandoObject>(requestExpando, objectExpando, paramsExpando))
										: token;
							});
						var message = new EmailMessage
						{
							From = emailSettings?.Sender,
							To = emailsWhenPublish?.ToAddresses,
							Cc = emailsWhenPublish?.CcAddresses,
							Bcc = emailsWhenPublish?.BccAddresses,
							Subject = subject.NormalizeHTMLBreaks().Format(parameters),
							Body = body.NormalizeHTMLBreaks().Format(parameters),
							SmtpServer = emailSettings?.Smtp?.Host,
							SmtpServerPort = emailSettings?.Smtp != null ? emailSettings.Smtp.Port : 0,
							SmtpServerEnableSsl = emailSettings?.Smtp != null && emailSettings.Smtp.EnableSsl,
							SmtpUsername = emailSettings?.Smtp?.User,
							SmtpPassword = emailSettings?.Smtp?.UserPassword,
							CorrelationID = requestInfo.CorrelationID
						};
						await Utility.MessagingService.SendEmailAsync(message, cancellationToken).ConfigureAwait(false);
						var log = "Add an email notification (notify when publish) into queue successful" + "\r\n" +
							$"- ID: {message.ID}" + "\r\n" +
							$"- Object: {@object.Title} [{@object.GetType()}#{@object.ID}]" + "\r\n" +
							$"- Event: {@event}" + "\r\n" +
							$"- Status: {status} (previous: {previousStatus})" + "\r\n" +
							$"- Sender: {sender?.Get<string>("Name")} ({sender?.Get<string>("Email")})" + "\r\n" +
							$"- To: {message.To}" + (!string.IsNullOrWhiteSpace(message.Cc) ? $" / {message.Cc}" : "") + (!string.IsNullOrWhiteSpace(message.Bcc) ? $" / {message.Bcc}" : "") + "\r\n" +
							$"- Subject: {message.Subject}";
						await requestInfo.WriteLogAsync(log, cancellationToken).ConfigureAwait(false);
						if (Utility.Logger.IsEnabled(LogLevel.Debug))
							Utility.Logger.LogDebug($"Add an email notification (notify when publish) into queue successful\r\n{message.ToJson()}");
					}
					catch (Exception exception)
					{
						await requestInfo.WriteErrorAsync(exception, cancellationToken, "Error occurred while adding an email notification (notify when publish) into queue").ConfigureAwait(false);
					}
			}

			// send web-hook notifications
			if (sendWebHookNotifications)
				try
				{
					var signAlgorithm = webHooks.SignAlgorithm ?? "SHA256";
					var signKey = webHooks.SignKey ?? requestInfo.Session.AppID;
					var query = webHooks.AdditionalQuery?.ToExpandoObject().ToDictionary(kvp => kvp.Key, kvp => kvp.Value as string) ?? new Dictionary<string, string>();
					var header = webHooks.AdditionalHeader?.ToExpandoObject().ToDictionary(kvp => kvp.Key, kvp => kvp.Value as string) ?? new Dictionary<string, string>();
					header = new Dictionary<string, string>(header, StringComparer.OrdinalIgnoreCase)
					{
						{ "Event", @event },
						{ "ObjectName", objectName },
						{ "ObjectType", @object.GetTypeName() },
						{ "Status", $"{status}" },
						{ "PreviousStatus", $"{previousStatus}" },
						{ "DeveloperID", requestInfo.Session.DeveloperID },
						{ "AppID", requestInfo.Session.AppID },
						{ "SiteID", site?.ID },
						{ "SiteTitle", site?.Title },
						{ "SiteDomain", siteDomain },
						{ "SiteURL", siteURL },
						{ "OrganizationID", organization?.ID },
						{ "OrganizationTitle", organization?.Title },
						{ "ModuleID", contentType?.Module?.ID },
						{ "ModuleTitle", contentType?.Module?.Title },
						{ "ContentTypeID", contentType?.ID },
						{ "ContentTypeTitle", contentType?.Title }
					};

					var message = new WebHookMessage
					{
						EndpointURL = webHooks.EndpointURLs[0],
						Body = @object.ToJson(json =>
						{
							if (webHooks.GenerateIdentity)
							{
								var id = json.Get<string>("ID");
								if (!string.IsNullOrWhiteSpace(id))
									json["ID"] = id.GenerateUUID();
							}
						}).ToString(Newtonsoft.Json.Formatting.None),
						Query = query,
						Header = header,
						CorrelationID = requestInfo.CorrelationID
					}.Normalize(signAlgorithm, signKey, webHooks.SignatureName, webHooks.SignatureAsHex, webHooks.SignatureInQuery);

					await webHooks.EndpointURLs.ForEachAsync(async (endpointURL, _) =>
					{
						message.ID = UtilityService.NewUUID;
						message.EndpointURL = endpointURL;
						await Utility.MessagingService.SendWebHookAsync(message, cancellationToken).ConfigureAwait(false);
						var log = "Add a web-hook notification into queue successful" + "\r\n" +
							$"- ID: {message.ID}" + "\r\n" +
							$"- Object: {@object.Title} [{@object.GetType()}#{@object.ID}]" + "\r\n" +
							$"- Event: {@event}" + "\r\n" +
							$"- Status: {status} (previous: {previousStatus})" + "\r\n" +
							$"- Endpoint URL: {message.EndpointURL}";
						await requestInfo.WriteLogAsync(log, cancellationToken).ConfigureAwait(false);
						if (Utility.Logger.IsEnabled(LogLevel.Debug))
							Utility.Logger.LogDebug($"Add a web-hook notification into queue successful\r\n{message.ToJson(json => { json["Query"] = message.Query.ToJObject(); json["Header"] = message.Header.ToJObject(); })}");
					}, cancellationToken).ConfigureAwait(false);
				}
				catch (Exception exception)
				{
					await requestInfo.WriteErrorAsync(exception, cancellationToken, "Error occurred while adding a web-hook notification into queue").ConfigureAwait(false);
				}
		}
		/// <summary>
		/// Sends a notification when changed
		/// </summary>
		/// <param name="object"></param>
		/// <param name="event"></param>
		/// <param name="notificationSettings"></param>
		/// <param name="previousStatus"></param>
		/// <param name="status"></param>
		/// <param name="requestInfo"></param>
		/// <param name="cancellationToken"></param>
		/// <returns></returns>
		public static async Task SendNotificationAsync(this IPortalObject @object, string @event, Settings.Notifications notificationSettings, ApprovalStatus previousStatus, ApprovalStatus status, RequestInfo requestInfo, CancellationToken cancellationToken = default)
		{
			requestInfo = requestInfo ?? new RequestInfo();
			try
			{
				await requestInfo.SendNotificationAsync(@object, @event, notificationSettings, previousStatus, status, cancellationToken).ConfigureAwait(false);
			}
			catch (Exception exception)
			{
				await requestInfo.WriteErrorAsync(exception, cancellationToken).ConfigureAwait(false);
			}
		}