ASP.NET Core 2.2 - SMTP EmailSender Implementation

Update 06/09/2020

I have published a new article, ASP.NET Core 3.1 - SMTP EmailSender. 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 free FIDO Utilities Project implements an Email Settings Verifier and the SendAdminEmail function on the Error page.

The article I wrote for Code Project Require Confirmed Email in ASP.NET Core 2.2 - Part 1 detailing how to require a confirmed email in ASP.NET 2.2 included some of this information. In this article I will cover implementing an async SMTP EmailSender using MailKit. I will assume you have created a new ASP.NET Core 2.2 Razor Pages project with Individual User Accounts, scaffolded the Default Identity UI and updated the database with the CreateIdentitySchema migration.

Most of my research suggested I use a service like SendGrid for scalability. I hope to have a scalabilty problem but until then I will use my hosting provider's email service.

Section references:
From stackoverflow. ASP Core 2.1 Email Confirmation

If you followed my previous article ASP.NET Core 2.2 - Scaffold Identity UI you have a stubbed out EmailSender with an IEmailSender interface in a Services folder.

Add a Services folder to the project and add an EmailSender class to Services.
public interface IEmailSender
{
    Task SendEmailAsync(string email, string subject, string message);
}

public class EmailSender : IEmailSender
{
    public Task SendEmailAsync(string email, string subject, string message)
    {
        return Task.CompletedTask;
    }
} 

I use dependency injection to get the email server settings from appsettings.json or appsettings.Development.json to the EmailSender. You can use the User Secrets feature but that is beyond the scope of this article.

Edit appsettings.json and appsettings.Development.json, add EmailSettings with your email server settings.
"EmailSettings": {
    "MailServer": "smtp.some_server.com",
    "MailPort": 587,
    "SenderName": "some name",
    "Sender": "some_email@some_server.com",
    "Password": "some_password"
}
Add an Entities folder to the project and add an EmailSettings class to Entities.
public class EmailSettings
{
    public string MailServer { get; set; }
    public int MailPort { get; set; }
    public string SenderName { get; set; }
    public string Sender { get; set; }
    public string Password { get; set; }
}
Edit Startup.cs > ConfigureServices, add EmailSettings option.
services.Configure<EmailSettings>(Configuration.GetSection("EmailSettings"));
Add to the bottom of Startup.cs > ConfigureServices.
services.AddSingleton<IEmailSender, EmailSender>();

In the Code Project article, I used System.Net.Mail which contains the SmptClient class. MS docs have declared the SmtpClient Class obsolete and recommend using MailKit and MimeKit.

SmtpClient Class.

You can install MailKit with NuGet. Right click on your project then click Manage NuGet packages. Go to the Browse tab and search MailKit.

NuGet MailKit.

Getting the EmailSender configured and working properly usually takes a couple of attempts. The settings are probably different for development and production. MimeKit provides an example for creating a test email here and here. I implement the SendEmailAsync() with EmailSettings and IHostingEnvironment to use the email settings from appsettings.json or appsettings.Development.json.

Edit EmailSender.cs to use EmailSettings and IHostingEnvironment. Implement the SendEmailAsync() method.
public interface IEmailSender
{
    Task SendEmailAsync(string email, string subject, string message);
}

public class EmailSender : IEmailSender
{
    private readonly EmailSettings _emailSettings;
    private readonly IHostingEnvironment _env;

    public EmailSender(
        IOptions<EmailSettings> emailSettings,
        IHostingEnvironment env)
    {
        _emailSettings = emailSettings.Value;
        _env = env;
    }

    public async Task SendEmailAsync(string email, string subject, string message)
    {
        try
        {
            var mimeMessage = new MimeMessage();

            mimeMessage.From.Add(new MailboxAddress(_emailSettings.SenderName, _emailSettings.Sender));

            mimeMessage.To.Add(new MailboxAddress(email));

            mimeMessage.Subject = subject;

            mimeMessage.Body = new TextPart("html")
            {
                Text = message
            };

            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 (_env.IsDevelopment())
                {
                    // The third parameter is useSSL (true if the client should make an SSL-wrapped
                    // connection to the server; otherwise, false).
                    await client.ConnectAsync(_emailSettings.MailServer, _emailSettings.MailPort, true);
                }
                else
                {
                    await client.ConnectAsync(_emailSettings.MailServer);
                }

                // Note: only needed if the SMTP server requires authentication
                await client.AuthenticateAsync(_emailSettings.Sender, _emailSettings.Password);

                await client.SendAsync(mimeMessage);

                await client.DisconnectAsync(true);
            }

        }
        catch (Exception ex)
        {
            // TODO: handle exception
            throw new InvalidOperationException(ex.Message);
        }
    }

}

Update using namespaces. Be careful to use MailKit.Net.Smtp instead of System.Mail.Net. For development I am using a gmail account which requires ssl. For production I was getting certificate errors. My hosting provider has an internal email server which relays to a secure email server. I verified my emails are using TLS after the first relay.

Email Message Source.
Section references:

You can create an admin page or just use Pages\Index to test injecting IEmailSender and the SendEmailAsync() method.

Add this label and form to Index.cshtml.
<div class="row">
    <div class="col-6">
        <label class="alert alert-success">@Model.EmailStatusMessage</label>
    </div>
</div>
<div class="row">
    <div class="col-6">
        <form method="post">
            <div class="form-group">
                <label asp-for="Email"></label>
                <input asp-for="Email" class="form-control" />
                <span asp-validation-for="Email" class="text-danger"></span>
            </div>
            <button type="submit" class="btn btn-primary">Email Test</button>
        </form>
    </div>
</div>
Edit Index.cshtml.cs.
public class IndexModel : PageModel
{
    private readonly IEmailSender _emailSender;

    public IndexModel(IEmailSender emailSender)
    {
        _emailSender = emailSender;
    }

    public string EmailStatusMessage { get; set; }

    [Required]
    [BindProperty]
    public string Email { get; set; }

    public void OnGet()
    {
    }

    public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        var email = Email;

        var subject = "Email Test";

        var message = "This is a test message.";

        await _emailSender.SendEmailAsync(email, subject, message);

        EmailStatusMessage = "Send test email was successful.";

        return Page();
    }
}

You should determine which settings work for your environments sooner rather than later. You may want to use a provider like SendGrid for production dependent on the requirements. This implementation will work for Email Confirmations and Forgot Password Emails.

Look for my next article where I will demonstrate how to add a text view, a html template and an admin notification email.

Update 03/08/2019

I initialized a new project for my next article and referenced this article for the EmailSender. The cut and paste results were confusing. I found and corrected the EmailSettings type for IOptions when injecting it into the EmailSender and the IActionResult in the Index.cshtml.cs > OnPostAsync were not properly escaped.

Ken Haggerty
Created 02/06/19
Updated 06/09/20 21:36 GMT

Log In or Reset Quota to read more.

Article Tags:

Email Identity Scaffold
Successfully completed. Thank you for contributing.
Processing...
Something went wrong. Please try again.
Contribute to enjoy content without advertisments.
You can contribute without registering.

Comments(0)

Loading...
Loading...

Not accepting new comments.

Submit your comment. Comments are moderated.

User Image.
DisplayedName - Member Since ?