ASP.NET Core 5.0 - EmailSender Service
This article will demonstrate the implementation of a dependency injected SMTP EmailSender Service. I will assume you have downloaded the ASP.NET Core 5.0 - SMTP Settings Tester Project. Registered users can download the source code for free at Manage > Assets.
SMTP Settings Tester Project and Article Series
This project implements Bootstrap v5, Bootstrap Native, and the KenHaggerty. Com. SingleUser NuGet package which provides log in and log out pages for a single user to access the Admin pages. jQuery has been removed. I created a topic, ASP.NET Core 5.0 - SMTP Settings Tester Project for discussions. More details and screenshots at ASP.NET Core 5.0 - SMTP Settings Tester Project. The details page includes the version change log.
This article will describe the new EmailSender which implements a html template with a customizable banner, a ServerCertificateValidationCallback delegate, new MailKit. Net. Smtp client asynchronous methods, and a MailKit. Security. SecureSocketOptions. Auto option which allows the MailKit. IMailService decide which SSL or TLS options to use (default). If the server does not support SSL or TLS, then the connection will continue without any encryption.
Right click the project then click Manage NuGet Packages. Browse and install the MailKit NuGet package.
The EmailSender injects the runtime EmailSettings and sends an email to the provided address using a html template or a text only message email with HttpContext details like User-Agent, anonymized IP address, Path, and QueryString to the AdminEmail property. Add an Services folder to the project root and add the AnonymizeIpAddressExtention and EmailSender classes to Services.
AnonymizeIpAddressExtention.cs
// Copyright © 2020 Ken Haggerty (https://kenhaggerty.com) // Licensed under the MIT License. public static class AnonymizeIpAddressExtention { //const string IPV4_NETMASK = "255.255.255.0"; //const string IPV6_NETMASK = "ffff:ffff:ffff:0000:0000:0000:0000:0000"; /// <summary> /// Removes the unique part of an <see cref="IPAddress" />. /// </summary> /// <param name="ipAddress"></param> /// <returns><see cref="string" /></returns> public static string AnonymizeIP(this IPAddress ipAddress) { string ipAnonymizedString; if (ipAddress != null) { if (ipAddress.AddressFamily == AddressFamily.InterNetwork) { var ipString = ipAddress.ToString(); string[] octets = ipString.Split('.'); octets[3] = "0"; ipAnonymizedString = string.Join(".", octets); } else if (ipAddress.AddressFamily == AddressFamily.InterNetworkV6) { var ipString = ipAddress.ToString(); string[] hextets = ipString.Split(':'); var hl = hextets.Length; if (hl > 3) { for (var i = 3; i < hl; i++) { if (hextets[i].Length > 0) { hextets[i] = "0"; } } } ipAnonymizedString = string.Join(":", hextets); } else { ipAnonymizedString = $"Not Valid - {ipAddress.ToString()}"; } } else { ipAnonymizedString = "Is Null"; } return ipAnonymizedString; } }
IMPORANT: Your smtp login email MUST be same as your FROM address.
EmailSender.cs
// Copyright © Ken Haggerty (https://kenhaggerty.com) // Licensed under the MIT License. /// <summary> /// Interface for the EmailSender service using <paramref name="mailSettings" /> and /// <paramref name="httpContextAccessor" /> to send emails. /// </summary> /// <param name="httpContextAccessor">Injected IHttpContextAccessor.</param> /// <param name="emailSettings">Injected EmailSettings.</param> /// <remarks> /// Uses <see cref="HttpContextAccessor" /> to extract current context properties to include /// when an email is sent to the <see cref="EmailSettings.AdminEmail" /> address. /// </remarks> public interface IEmailSender { /// <summary> /// Sends an email to the <paramref name="email" /> address with <paramref name="subject" />, /// <paramref name="textMessage" /> and <paramref name="htmlMessage" />. /// </summary> /// <param name="email">The send to email address.</param> /// <param name="subject">The email's subject.</param> /// <param name="textMessage">The email's MimeKit.BodyBuilder.TextBody.</param> /// <param name="htmlMessage">The email's MimeKit.BodyBuilder.HtmlBody.</param> /// <exception cref="ArgumentException"></exception> /// <exception cref="ArgumentNullException"></exception> /// <exception cref="ArgumentOutOfRangeException"></exception> /// <exception cref="FormatException"></exception> /// <exception cref="InvalidOperationException"></exception> /// <exception cref="IOException"></exception> /// <exception cref="OverflowException"></exception> /// <exception cref="OutOfMemoryException"></exception> /// <exception cref="ParseException"></exception> Task SendEmailAsync(string email, string subject, string textMessage, string htmlMessage); /// <summary> /// Sends an email to the address set with the <see cref="EmailSettings.AdminEmail" /> property /// with <paramref name="subject" /> and <paramref name="textMessage" /> including HttpContext properties. /// </summary> /// <param name="subject">The email's subject.</param> /// <param name="textMessage">The email's MimeMessage.Body.TextPart.</param> /// <remarks> /// Uses <see cref="HttpContextAccessor" /> to extract current context properties to include when an email sent /// to the <see cref="EmailSettings.AdminEmail" /> address. /// </remarks> /// <exception cref="ArgumentException"></exception> /// <exception cref="ArgumentNullException"></exception> /// <exception cref="ArgumentOutOfRangeException"></exception> /// <exception cref="FormatException"></exception> /// <exception cref="IOException"></exception> /// <exception cref="InvalidOperationException"></exception> /// <exception cref="OverflowException"></exception> /// <exception cref="OutOfMemoryException"></exception> /// <exception cref="ParseException"></exception> Task SendAdminEmailAsync(string subject, string textMessage); } public class EmailSender : IEmailSender { private readonly EmailSettings _emailSettings; private readonly IHttpContextAccessor _httpContextAccessor; public EmailSender(IOptions<EmailSettings> emailSettings, IHttpContextAccessor httpContextAccessor) { _emailSettings = emailSettings?.Value; _httpContextAccessor = httpContextAccessor; } public async Task SendEmailAsync(string email, string subject, string textMessage, string htmlMessage) { if (!_emailSettings.Configured) throw new InvalidOperationException("The EmailSettings' Configured property is not true."); MimeMessage mimeMessage = new(); mimeMessage.From.Add(new MailboxAddress(_emailSettings.SenderName, _emailSettings.SenderEmail)); mimeMessage.To.Add(MailboxAddress.Parse(email)); mimeMessage.Subject = subject; BodyBuilder bodyBuilder = new() { TextBody = textMessage, HtmlBody = GetEmailTemplate(subject, htmlMessage) }; mimeMessage.Body = bodyBuilder.ToMessageBody(); var password = _emailSettings.Password; if (_emailSettings.PasswordEncrypted) password = AesCrypto.DecryptString(AesCrypto.CipherKey, AesCrypto.CipherIV, password); using var smtpClient = new SmtpClient(); smtpClient.ServerCertificateValidationCallback = SslCertificateValidationCallback; smtpClient.Timeout = _emailSettings.Timeout; await smtpClient.ConnectAsync(_emailSettings.MailServer, _emailSettings.MailPort, SecureSocketOptions.Auto).ConfigureAwait(false); await smtpClient.AuthenticateAsync(_emailSettings.SenderEmail, password).ConfigureAwait(false); await smtpClient.SendAsync(mimeMessage).ConfigureAwait(false); await smtpClient.DisconnectAsync(true).ConfigureAwait(false); } public async Task SendAdminEmailAsync(string subject, string textMessage) { if (!_emailSettings.Configured) throw new InvalidOperationException("The EmailSettings' Configured property is not true."); var _context = _httpContextAccessor.HttpContext; var _host = _context.Request.Host.ToString(); var _uaString = _context.Request.Headers["User-Agent"].ToString(); var _ipAnonymizedString = _context.Connection.RemoteIpAddress.AnonymizeIP(); var _uid = _context.User.Identity.IsAuthenticated ? _context.User.FindFirst(ClaimTypes.NameIdentifier).Value : "Unknown"; var _path = _context.Request.Path; var _queryString = _context.Request.QueryString; StringBuilder sb = new StringBuilder($"Host = {_host} \r\n"); sb.Append($"User Agent = {_uaString} \r\n"); sb.Append($"Anonymized IP = {_ipAnonymizedString} \r\n"); sb.Append($"UserId = {_uid} \r\n"); sb.Append($"Path = {_path} \r\n"); sb.Append($"QueryString = {_queryString} \r\n \r\n"); sb.Append(textMessage); MimeMessage mimeMessage = new(); mimeMessage.From.Add(new MailboxAddress(_emailSettings.SenderName, _emailSettings.SenderEmail)); mimeMessage.To.Add(MailboxAddress.Parse(_emailSettings.AdminEmail)); mimeMessage.Subject = subject; mimeMessage.Body = new TextPart("plain") { Text = sb.ToString() }; var password = _emailSettings.Password; if (_emailSettings.PasswordEncrypted) password = AesCrypto.DecryptString(AesCrypto.CipherKey, AesCrypto.CipherIV, password); using var smtpClient = new SmtpClient(); smtpClient.ServerCertificateValidationCallback = SslCertificateValidationCallback; smtpClient.Timeout = _emailSettings.Timeout; await smtpClient.ConnectAsync(_emailSettings.MailServer, _emailSettings.MailPort, SecureSocketOptions.Auto).ConfigureAwait(false); await smtpClient.AuthenticateAsync(_emailSettings.SenderEmail, password).ConfigureAwait(false); await smtpClient.SendAsync(mimeMessage).ConfigureAwait(false); await smtpClient.DisconnectAsync(true).ConfigureAwait(false); } static bool SslCertificateValidationCallback(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) { // If there are no errors, then everything went smoothly. if (sslPolicyErrors == SslPolicyErrors.None) return true; // Note: MailKit will always pass the host name string as the `sender` argument. var host = (string)sender; if ((sslPolicyErrors & SslPolicyErrors.RemoteCertificateNotAvailable) != 0) { // This means that the remote certificate is unavailable. Notify the user and return false. Console.WriteLine("The SSL certificate was not available for {0}", host); return false; } if ((sslPolicyErrors & SslPolicyErrors.RemoteCertificateNameMismatch) != 0) { // This means that the server's SSL certificate did not match the host name that we are trying to connect to. var certificate2 = certificate as X509Certificate2; var cn = certificate2 != null ? certificate2.GetNameInfo(X509NameType.SimpleName, false) : certificate.Subject; Console.WriteLine("The Common Name for the SSL certificate did not match {0}. Instead, it was {1}.", host, cn); return false; } // The only other errors left are chain errors. Console.WriteLine("The SSL certificate for the server could not be validated for the following reasons:"); // The first element's certificate will be the server's SSL certificate (and will match the `certificate` argument) // while the last element in the chain will typically either be the Root Certificate Authority's certificate -or- it // will be a non-authoritative self-signed certificate that the server admin created. foreach (var element in chain.ChainElements) { // Each element in the chain will have its own status list. If the status list is empty, it means that the // certificate itself did not contain any errors. if (element.ChainElementStatus.Length == 0) continue; Console.WriteLine("\u2022 {0}", element.Certificate.Subject); foreach (var error in element.ChainElementStatus) { // `error.StatusInformation` contains a human-readable error string while `error.Status` is the corresponding enum value. Console.WriteLine("\t\u2022 {0}", error.StatusInformation); } } return false; } private string GetEmailTemplate(string subject, string message) { StringBuilder sb = new(@$"<!DOCTYPE html> <html> <head> <meta charset=""utf-8"" /> <title>{subject}</title> </head> <body style=""font-family: Arial, Helvetica, sans-serif;""> <table border=""0"" cellspacing=""0"" width=""100%""> <tr> <td></td> <td width=""700""> <div style=""border: 2px solid {_emailSettings.BannerColor}; background-color: {_emailSettings.BannerBackcolor}; color: {_emailSettings.BannerColor}; width: 90%; margin: 0 auto;""> <div style=""padding: 5px 0; width: 100%; margin: 0 auto; text-align:center; font-size: 28px;""> {_emailSettings.BannerText} </div> </div> <div style=""padding: 10px 10px; width: 90%; margin: 0 auto; font-size: 18px;""> <h3>{subject}</h3> <p> {message} </p> <p> Thank you,<br /> {_emailSettings.EmailSignature} </p> <p> This is an automated message. </p> </div> <div style=""background-color: {_emailSettings.BannerBackcolor}; color: {_emailSettings.BannerColor}; padding: 10px 10px; width: 90%; margin: 0 auto; bottom: 10px;""> © {DateTime.Now.Year} </div> </td> <td></td> </tr> </table> </body> </html>"); return sb.ToString(); } }
Add the IHttpContextAccessor service, the EmailSettings configuration, and the IEmailSender service to the ServiceCollection in Startup.
Startup.cs > ConfigureServices
services.AddHttpContextAccessor(); services.Configure<EmailSettings>(Configuration.GetSection("EmailSettings")); services.AddSingleton<IEmailSender, EmailSender>();
To send an email to the admin email address from code, inject the EmailSender to the PageModel then invoke SendAdminEmailAsync.
Index.cshtml.cs
public class IndexModel : PageModel { private readonly IEmailSender _emailSender; public IndexModel(IEmailSender emailSender) { _emailSender = emailSender; } public async Task<IActionResult> OnGetAsync() { await _emailSender .SendAdminEmailAsync("New Visitor", "New visitor notice OnGetAsync.") .ConfigureAwait(false); return Page(); } }
The Admin page in the SMTP Settings Tester Project implements the SendAdminEmailAsync and SendEmailAsync functions.
Comments(0)