Kentico 12 Hotfix 12.0.101 Email Sending Issues: Quick Fix for Non-TLS/SSL Connections

The introduction of support for OAuth 2.0 in Kentico 12.0.101's email provider introduced a breaking change for unsecure SMTP servers. Here's a quick fix that overrides the provider only to use the original SMTP client.

In Kentico version 12.0.101, there was a change to the underlying implementation of the email provider. Namely, support for OAuth 2.0 was added. On the surface, this change appears to be beneficial; however, this introduces a breaking change. In environments that use unsecure SMTP servers, emails were failing. They were being routed through the new MailKit implementation. By default, MailKit will require the connection made to the SMTP server to use TLS/SSL.

Kentico logo

Currently, Kentico version 12.0.102 has been released and provides ISmtpClientFactory. This interface allows us to create a MailKit client. If you have a more advanced use case, the following solution will likely not be beneficial for you. Instead, you can upgrade to version 12.0.102 and create the fix that fits your needs.

The Fix

As a quick fix, what we’ve done is override the email provider to only use the original SMTP client from the System.Net.Mail namespace. The methods SendEmailInternal and SendEmailAsyncInternal are where we override the logic. Since the internal email provider has limited accessibility levels, we needed to copy various methods from the original provider into our custom provider.

				
					/// <summary>
/// Forcefully sends emails through the .NET SMTP Client.
/// </summary>
[assembly: RegisterCustomProvider(typeof(SmtpEmailProvider))]
namespace YourSolution.Providers
{
   public class SmtpEmailProvider : EmailProvider
   {
       private readonly SemaphoreSlim semaphore = new SemaphoreSlim(1, 1);


       private readonly ConcurrentDictionary<int, (SmtpClient client, System.Timers.Timer timer)> mailKitClients = new ConcurrentDictionary<int, (SmtpClient, System.Timers.Timer)>();


       protected override void SendEmailInternal(string siteName, MailMessage message, SMTPServerInfo smtpServer)
       {
           SendWithSmtpClient(message, smtpServer);
       }


       protected async override void SendEmailAsyncInternal(string siteName, MailMessage message, SMTPServerInfo smtpServer, EmailToken emailToken)
       {
           await SendWithSmtpClientAsync(message, smtpServer, emailToken).ConfigureAwait(continueOnCapturedContext: false);
       }


       private static void SendWithSmtpClient(MailMessage message, SMTPServerInfo smtpServer)
       {
           using (SmtpClient smtpClient = GetSMTPClient(smtpServer))
           {
               smtpClient.Send(message);
           }
       }


       private async Task SendWithSmtpClientAsync(MailMessage message, SMTPServerInfo smtpServer, EmailToken emailToken)
       {
           SmtpClient sMTPClient = GetSMTPClient(smtpServer);
           CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
           cancellationTokenSource.CancelAfter(60000);
           TaskCompletionSource<object> taskCompletionSource = new TaskCompletionSource<object>();
           CancellationTokenRegistration registration = default(CancellationTokenRegistration);
           sMTPClient.SendCompleted += delegate (object sender, AsyncCompletedEventArgs e)
           {
               registration.Dispose();
               ThreadPool.QueueUserWorkItem(delegate
               {
                   CMSThread.Wrap<AsyncCompletedEventArgs>(OnSendCompleted)(new AsyncCompletedEventArgs(e.Error, e.Cancelled, e.UserState));
               });
               taskCompletionSource.TrySetResult(null);
           };
           try
           {
               sMTPClient.SendAsync(message, emailToken);
               registration = cancellationTokenSource.Token.Register(sMTPClient.SendAsyncCancel);
               await taskCompletionSource.Task.ConfigureAwait(continueOnCapturedContext: false);
           }
           catch (Exception ex2)
           {
               Exception ex = ex2;
               registration.Dispose();
               Service.Resolve<IEventLogService>().LogException("EmailEngine", "EmailProvider", ex);
               ThreadPool.QueueUserWorkItem(delegate
               {
                   OnSendCompleted(new AsyncCompletedEventArgs(ex, cancelled: false, emailToken));
               });
           }
           finally
           {
               cancellationTokenSource.Dispose();
           }
       }


       private static SmtpClient GetSMTPClient(SMTPServerInfo smtpServer)
       {
           string serverName = smtpServer.ServerName;
           int num = 0;
           int num2 = serverName.LastIndexOf(":", StringComparison.Ordinal);
           if (num2 >= 0)
           {
               num = ValidationHelper.GetInteger(serverName.Substring(num2 + 1), 0);
           }


           SmtpClient smtpClient;
           if (num > 0)
           {
               serverName = serverName.Substring(0, num2);
               smtpClient = new SmtpClient(serverName, num);
           }
           else
           {
               smtpClient = new SmtpClient(serverName);
           }


           smtpClient.EnableSsl = smtpServer.ServerUseSSL;
           if (!string.IsNullOrEmpty(smtpServer.ServerUserName))
           {
               NetworkCredential networkCredential = (NetworkCredential)(smtpClient.Credentials = new NetworkCredential(smtpServer.ServerUserName, EncryptionHelper.DecryptData(smtpServer.ServerPassword)));
           }
           else
           {
               smtpClient.UseDefaultCredentials = ValidationHelper.GetBoolean(Service.Resolve<IAppSettingsService>()["CMSEmailUseDefaultCredentials"], defaultValue: false);
           }


           smtpClient.DeliveryMethod = GetDeliveryMethod(smtpServer.ServerDeliveryMethod);
           smtpClient.PickupDirectoryLocation = StorageHelper.GetFullFilePhysicalPath(smtpServer.ServerPickupDirectory, (string)null);
           return smtpClient;
       }


       private static SmtpDeliveryMethod GetDeliveryMethod(SMTPServerDeliveryEnum deliveryEnum)
       {
           switch (deliveryEnum)
           {
               case SMTPServerDeliveryEnum.SpecifiedPickupDirectory:
                   return SmtpDeliveryMethod.SpecifiedPickupDirectory;
               case SMTPServerDeliveryEnum.PickupDirectoryFromIis:
                   return SmtpDeliveryMethod.PickupDirectoryFromIis;
               default:
                   return SmtpDeliveryMethod.Network;
           }
       }
   }
}

				
			

Final Thoughts

In conclusion, the introduction of support for OAuth 2.0 in Kentico version 12.0.101 was a welcome change, but it also brought about an unintended consequence for environments that use unsecure SMTP servers. This led to email failures when routing through the new MailKit implementation, which required TLS/SSL connections to the SMTP server. A quick fix for the issue is to override the email provider to only use the original SMTP client, which requires copying various methods into a custom provider due to limited accessibility levels. Overall, this experience highlights the importance of careful consideration and testing of changes to underlying implementations in software updates.

About the Author

Nick Kooman

Nick began as an intern at BizStream, not even a year after graduating from high school, and is now a full-time developer! Nick thinks that growth stagnates when you are comfortable, and the best moments in life happen when you are uncomfortable. He believes he would not have had the chance to make a connection with BizStream if he had not stepped out of his comfort zone during school. When he’s not working, Nick spends time with friends, plays video games, and watches YouTube.

Subscribe to Our Blog

Stay up to date on what BizStream is doing and keep in the loop on the latest in marketing & technology.