Skip to content

Background Tasks Bulk Email Sending

Victor Tomaili edited this page May 3, 2021 · 1 revision

Background

  • We need to create a background service to run on separate thread to send email from QueuedEmail table
  • We will configure different Email Account to send email for Application in EmailAccounts table, e.g
  • We will store all outgoing email entries to QueuedEmail Tables for sending via our background task

Implementation

Migration

public class DefaultDB_20170130_154500_Emails : AutoReversingMigration
{
	public override void Up()
	{
		this.CreateTableWithId32("EmailAccounts", "Id", s => s
			.WithColumn("Email").AsString(255).NotNullable()
			.WithColumn("DisplayName").AsString(255).NotNullable()
			.WithColumn("Host").AsString(255).NotNullable()
			.WithColumn("Port").AsInt32().NotNullable()
			.WithColumn("Username").AsString(255).NotNullable()
			.WithColumn("Password").AsString(255).NotNullable()
			.WithColumn("EnableSsl").AsBoolean().NotNullable()
			.WithColumn("UseDefaultCredentials").AsBoolean().NotNullable()
			.WithColumn("InsertDate").AsDateTime().NotNullable()
			.WithColumn("InsertUserId").AsInt32().NotNullable()
			.WithColumn("UpdateDate").AsDateTime().Nullable()
			.WithColumn("UpdateUserId").AsInt32().Nullable()
			.WithColumn("IsActive").AsInt16().NotNullable().WithDefaultValue(1));

		this.CreateTableWithId32("QueuedEmail", "Id", s => s
			.WithColumn("PriorityId").AsInt32().NotNullable()
			.WithColumn("From").AsString(500).NotNullable()
			.WithColumn("FromName").AsString(500).Nullable()
			.WithColumn("To").AsString(500).NotNullable()
			.WithColumn("ToName").AsString(500).Nullable()
			.WithColumn("ReplyTo").AsString(500).Nullable()
			.WithColumn("ReplyToName").AsString(500).Nullable()
			.WithColumn("CC").AsString(500).Nullable()
			.WithColumn("Bcc").AsString(500).Nullable()
			.WithColumn("Subject").AsString(1000).Nullable()
			.WithColumn("Body").AsString(int.MaxValue).Nullable()
			.WithColumn("AttachmentFilePath").AsString(1000).Nullable()
			.WithColumn("AttachmentFileName").AsString(1000).Nullable()
			.WithColumn("AttachedDownloadId").AsInt32().Nullable()
			.WithColumn("CreatedOnUtc").AsDateTime().NotNullable()
			.WithColumn("SentTries").AsInt32().Nullable()
			.WithColumn("SentOnUtc").AsDateTime().Nullable()
			.WithColumn("EmailAccountId").AsInt32().NotNullable()
			 .ForeignKey("FK_QueuedEmail_EmailAccountId", "EmailAccounts", "Id")
			.WithColumn("DontSendBeforeDateUtc").AsDateTime().Nullable()
			.WithColumn("HasError").AsBoolean().Nullable()
			.WithColumn("Result").AsString(1000).Nullable());
	}
}

EmailThread - A Background Thread to Check and Send Queued Emails

public static class EmailThread
{
	private static Thread _SendEmailThread = null;
	public static void StartEmailThread()
	{
		if (_SendEmailThread == null)
		{
			_SendEmailThread = new Thread(new ThreadStart(SendEmails));
			_SendEmailThread.Priority = ThreadPriority.Lowest;
			_SendEmailThread.Start();
		}
	}
	public static void EndEmailThread()
	{
		if (_SendEmailThread != null)
		{
			_SendEmailThread.Abort();
		}
	}

	private static void SendEmails()
	{
		bool enableEmailService = Convert.ToBoolean(ConfigurationManager.AppSettings["EnableEmailService"]);
		if (enableEmailService)
		{

			while (true)
			{
				Thread.Sleep(60000); // 10 sec
				var connection = SqlConnections.NewFor<MyRow>();
				var request = new ListRequest();
				ListResponse<MyRow> rows =
				new MyRepository().List(connection, request);
				if (rows.TotalCount == 0)
					break;

				foreach (MyRow row in rows.Entities)
				{
					try
					{
						MailMessage mailMessage = new MailMessage();
						mailMessage.From = new MailAddress(row.From, row.FromName);
						foreach (var to in row.To.Split(','))
							mailMessage.To.Add(new MailAddress(to));
						if (!string.IsNullOrEmpty(row.Cc))
							foreach (var cc in row.Cc.Split(','))
								mailMessage.CC.Add(new MailAddress(cc));
						if (!string.IsNullOrEmpty(row.Bcc))
							foreach (var bcc in row.Bcc.Split(','))
								mailMessage.Bcc.Add(new MailAddress(bcc));
						mailMessage.Subject = row.Subject;
						mailMessage.Body = row.Body;
						mailMessage.IsBodyHtml = true;
						EmailAccountsRow emailAccount = connection.TryFirst<EmailAccountsRow>(EmailAccountsRow.Fields.Id == (int)row.EmailAccountId.Value);
						SmtpClient smtp = new SmtpClient
						{
							Host = emailAccount.Host,
							Port = (int)emailAccount.Port,
							EnableSsl = (bool)emailAccount.EnableSsl,
							DeliveryMethod = SmtpDeliveryMethod.Network,
							Credentials = new System.Net.NetworkCredential(emailAccount.Username, emailAccount.Password),
							Timeout = 30000,
						};
						smtp.Send(mailMessage);
						row.HasError = false;
						row.SentTries = 1;
						row.SentOnUtc = DateTime.UtcNow;
					}
					catch (Exception ex)
					{
						row.HasError = true;
						row.Result = ex.Message;
						row.SentTries = 1;
					}
					connection.UpdateById<MyRow>(row);
				}
			}
		}
	}
}

Challenges

  • Since In a separate thread, as there is no http request, user is NULL
  • We need to impersonate user which has rights to services (in our case EmailAccountsRow/Repository and QueuedEmailRow/Repository)
  • We need register Serenity's ImpersonatingAuthenticationService to make it work

SiteInitialization.cs

var registrar = Dependency.Resolve<IDependencyRegistrar>();
//.....
registrar.RegisterInstance<IAuthorizationService>(new ImpersonatingAuthorizationService(new Administration.AuthorizationService()));

using Impersonate and UndoImpersonate methods

(Dependency.Resolve<IAuthorizationService>() as ImpersonatingAuthorizationService).Impersonate("admin");
//... code to run under impersonated user
(Dependency.Resolve<IAuthorizationService>() as ImpersonatingAuthorizationService).UndoImpersonate();

##Working Code for EmailThread.cs

namespace Serene
{
	using Administration.Entities;
	using Serenity;
	using Serenity.Abstractions;
	using Serenity.Data;
	using Serenity.Services;
	using Serenity.Web;
	using System;
	using System.Collections.Generic;
	using System.Configuration;
	using System.Net.Mail;
	using System.Threading;
	using MyRepository = Administration.Repositories.QueuedEmailRepository;
	using MyRow = Administration.Entities.QueuedEmailRow;
	public static class EmailThread
	{
		private static Thread _SendEmailThread = null;

		public static void StartEmailThread()
		{
			if (_SendEmailThread == null)
			{
				_SendEmailThread = new Thread(new ThreadStart(SendEmails));
				_SendEmailThread.Priority = ThreadPriority.Lowest;
				_SendEmailThread.Start();
			}
		}
		public static void EndEmailThread()
		{
			if (_SendEmailThread != null)
			{
				_SendEmailThread.Abort();
			}
		}

		private static void SendEmails()
		{
			bool enableEmailService = Convert.ToBoolean(ConfigurationManager.AppSettings["EnableEmailService"]);
			if (enableEmailService)
			{
				(Dependency.Resolve<IAuthorizationService>() as ImpersonatingAuthorizationService).Impersonate("admin");
				while (true)
				{
					Thread.Sleep(60000); // 10 sec
					var connection = SqlConnections.NewFor<MyRow>();
					var request = new ListRequest();
					ListResponse<MyRow> rows =
					new MyRepository().List(connection, request);
					if (rows.TotalCount == 0)
						break;

					foreach (MyRow row in rows.Entities)
					{
						try
						{
							MailMessage mailMessage = new MailMessage();
							mailMessage.From = new MailAddress(row.From, row.FromName);
							foreach (var to in row.To.Split(','))
								mailMessage.To.Add(new MailAddress(to));
							if (!string.IsNullOrEmpty(row.Cc))
								foreach (var cc in row.Cc.Split(','))
									mailMessage.CC.Add(new MailAddress(cc));
							if (!string.IsNullOrEmpty(row.Bcc))
								foreach (var bcc in row.Bcc.Split(','))
									mailMessage.Bcc.Add(new MailAddress(bcc));
							mailMessage.Subject = row.Subject;
							mailMessage.Body = row.Body;
							mailMessage.IsBodyHtml = true;
							EmailAccountsRow emailAccount = connection.TryFirst<EmailAccountsRow>(EmailAccountsRow.Fields.Id == (int)row.EmailAccountId.Value);
							SmtpClient smtp = new SmtpClient
							{
								Host = emailAccount.Host,
								Port = (int)emailAccount.Port,
								EnableSsl = (bool)emailAccount.EnableSsl,
								DeliveryMethod = SmtpDeliveryMethod.Network,
								Credentials = new System.Net.NetworkCredential(emailAccount.Username, emailAccount.Password),
								Timeout = 30000,
							};
							smtp.Send(mailMessage);
							row.HasError = false;
							row.SentTries = 1;
							row.SentOnUtc = DateTime.UtcNow;
						}
						catch (Exception ex)
						{
							row.HasError = true;
							row.Result = ex.Message;
							row.SentTries = 1;
						}
						connection.UpdateById<MyRow>(row);
					}
				}
				(Dependency.Resolve<IAuthorizationService>() as ImpersonatingAuthorizationService).UndoImpersonate();
			}
		}
	}
}
Clone this wiki locally