ASP.NET Core 3.1 - SMTP EmailSender


Ken Haggerty
Created 04/23/2020 - Updated 05/23/2020 15:13

This article will demonstrate the implementation of a dependency injected SMTP EmailSender. I will assume you have created a new ASP.NET Core 3.1 Razor Pages project and have reviewed the previous article of the series. I won't use Identity or Individual User Accounts. See Tutorial: Get started with Razor Pages in ASP.NET Core.

FIDO Utilties Project and Article Series

This project helps mitigate some of the issues implementing the challenge only. The Users Without Passwords Project implements the challenge with users and authenticators.


Access to the research project source code is free to registered users on KenHaggerty.Com at Manage > Assets.

I will publish the FIDO Utilities Project at fido.kenhaggerty.com until I publish the Users Without Passwords Project.

I enjoy writing these articles. It often enhances and clarifies my coding. The research project is a result of a lot of refactoring and hopefully provides logical segues for the articles. Thank you for supporting my efforts.

The FIDO processes involve a lot of things that can go wrong and I would like to know when it does. A notice of something going right is also nice. By far, my most popular article is ASP.NET Core 2.2 - SMTP EmailSender Implementation. I have added a SendAdminEmail function by adding an AdminEmail setting and injecting IHttpContextAccessor. This allows access to the current HttpContext properties like UserAgent, Anonymized IP, Path and QueryString inside the service.

Install the MailKit NuGet package. Right click the project then click Manage NuGet Packages.

Installed MailKit NuGet Package

The EmailSettings get populated from appsettings.json. The environment and User Secrets features of appsettings.json is beyond the scope of this article. Care should be taken to ensure email credentials and settings are not compromised. The EmailSettings are injected to the EmailSender.

Add an Entities folder to the project root and add an EmailSettings class to Entities.

Entities > EmailSettings.cs
public class EmailSettings
{
    public bool IsDevelopment { get; set; }
    public bool UseSsl { get; set; }
    public string MailServer { get; set; }
    public int MailPort { get; set; }
    public string SenderEmail { get; set; }
    public string SenderName { get; set; }
    public string Password { get; set; }
    public string AdminEmail { get; set; }
}

Edit appsettings.json, add an EmailSettings section with your email server settings.

EmailSettings section
"EmailSettings": {
  "IsDevelopment": true,
  "UseSsl": true,
  "MailServer": "smtp.your_server.com",
  "MailPort": 587,
  "SenderName": "your name",
  "SenderEmail": "your_email@your_server.com",
  "Password": "your_password",
  "AdminEmail": "admin@your_server.com"
}

I inject the IHttpContextAccessor to the EmailSender to allow the SendAdminEmail function access to the current HttpContext properties like UserAgent, Anonymized IP, Path and QueryString inside the service. I parse the user's IP address but I anonymize the IP before storing or emailing. The FIDO Utilities Project implements an Email Settings Verifier and the SendAdminEmail function on the Error page.

Create a new folder in the project root named Services. Add a new class named AnonymizeIpAddressExtention.cs 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;
    }
}

Add a new class named EmailSender.cs to Services.

EmailSender.cs
// Copyright © 2020 Ken Haggerty (https://kenhaggerty.com)
// Licensed under the MIT License.

/// <summary>
/// Interface for the EmailSender service using <paramref name="emailSettings" /> 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>
    Task SendEmailAsync(string email, string subject, string textMessage, string htmlMessage);

    /// <summary>
    /// Sends an email to the address set with the readonly <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>    
    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)
    {
        var mimeMessage = new MimeMessage();
        mimeMessage.From.Add(new MailboxAddress(_emailSettings.SenderName, _emailSettings.SenderEmail));
        mimeMessage.To.Add(new MailboxAddress(email));

        mimeMessage.Subject = subject;
        var builder = new BodyBuilder { TextBody = textMessage, HtmlBody = htmlMessage };
        mimeMessage.Body = builder.ToMessageBody();

        try
        {
            using var client = new SmtpClient();
            client.ServerCertificateValidationCallback = (s, c, h, e) => true;
            if (_emailSettings.IsDevelopment)
                await client.ConnectAsync(_emailSettings.MailServer, _emailSettings.MailPort, _emailSettings.UseSsl)
                    .ConfigureAwait(false);
            else
                await client.ConnectAsync(_emailSettings.MailServer).ConfigureAwait(false);

            // Note: only needed if the SMTP server requires authentication
            await client.AuthenticateAsync(_emailSettings.SenderEmail, _emailSettings.Password).ConfigureAwait(false);
            await client.SendAsync(mimeMessage).ConfigureAwait(false);
            await client.DisconnectAsync(true).ConfigureAwait(false);
        }
        catch (Exception ex)
        {
            throw new InvalidOperationException(ex.Message);
        }
    }

    public async Task SendAdminEmailAsync(string subject, string textMessage)
    {
        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);

        var mimeMessage = new MimeMessage();
        mimeMessage.From.Add(new MailboxAddress(_emailSettings.SenderName, _emailSettings.SenderEmail));
        mimeMessage.To.Add(new MailboxAddress(_emailSettings.AdminEmail));

        mimeMessage.Subject = subject;
        mimeMessage.Body = new TextPart("plain") { Text = sb.ToString() };

        try
        {
            using var client = new SmtpClient();
            // For demo-purposes, accept all SSL certificates (in case the server supports STARTTLS)
            client.ServerCertificateValidationCallback = (s, c, h, e) => true;
            if (_emailSettings.IsDevelopment)
                await client.ConnectAsync(_emailSettings.MailServer, _emailSettings.MailPort, _emailSettings.UseSsl)
                    .ConfigureAwait(false);
            else
                await client.ConnectAsync(_emailSettings.MailServer).ConfigureAwait(false);

            // Note: only needed if the SMTP server requires authentication
            await client.AuthenticateAsync(_emailSettings.SenderEmail, _emailSettings.Password).ConfigureAwait(false);
            await client.SendAsync(mimeMessage).ConfigureAwait(false);
            await client.DisconnectAsync(true).ConfigureAwait(false);
        }
        catch (Exception ex)
        {
            throw new InvalidOperationException(ex.Message);
        }
    }
}

Resolve using namespaces. Be sure to reference MailKit. Net. Smtp. SmtpClient not System. Net. Mail. SmtpClient.

Add the IHttpContextAccessor, EmailSettings and EmailSender to the service container.

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 free FIDO Utilities Project implements the EmailSender and an Email Settings Verifier which allows you to test email settings and displays the current settings from appsettings.json and appsettings.development.json.

Email Settings Verifier
Update 05/8/2020

I updated the article links and added a screenshot of the Email Settings Verifier.

Update 05/10/2020

I updated the article links.

Update 05/23/2020

I added the AES Cipher article link and updated the screenshot.


Article Tags:

Email FIDO Model

Comment Count = 0

Please log in to comment or follow.

Login Register
Follow to get web notifications when new comments are posted to this article.
Logged in users receive web notifications for new articles, topics and assets.
Web Notifications