ASP.NET Core 2.2 - SMTP EmailSender Implementation


Ken Haggerty
Created 02/06/2019 - Updated 03/08/2019 17:40
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.

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.


Article Tags:

Email Identity Scaffold

5 Following


Comment Count = 4

rhunt56
rhunt56
Member Since 06/26/2019
Posted 06/26/2019 16:42

I can't get this to work if I move the Index.cshtml to a different folder. In fact the demonstrated Index test sender page only worked when the Namespace was also Services. Can you point me in the right direction to allow the sending page to connect to the Service?

Ken Haggerty
Ken Haggerty *
Member Since 01/04/2019
Posted 06/26/2019 18:15

As a registered member, you may download the Bootstrap Native Project from Manage > Assets which implements the features from most of the articles including SMTP EmailSender Implementation. The project has a demo page which sends an admin email.

rhunt56
rhunt56
Member Since 06/26/2019
Posted 06/26/2019 19:56

Is Bootstrap required? you don't show it on this page but do show it in the Demo app.

Ken Haggerty
Ken Haggerty *
Member Since 01/04/2019
Posted 06/26/2019 20:11

Bootstrap is configured when you create a new ASP.NET Core 2.2 Razor Pages project.

Please log in to comment or follow.

Login Register