Sending Smtp Email on ASP.NET Core with MailKit

If you are porting web applications to ASP.NET Core or building new web applications, you may notice that the System.Net.Mail namespace is not implemented in .NET Core. My understanding is that there are intentions to implement that namespace later, but it may be a little stumbling block for early adopters, since sending email is a very fundamental thing that most web applications need to do for common tasks such as verifying an email address for a new account or facilitating password reset.

There are some examples out there for sending email with various 3rd party services such as SendGrid, MailGun, Elastic Email, and the like, by using REST APIs to send the mail instead of using SMTP, and that is certainly a good option to consider. But for those who already have an SMTP server that they want to use, a better solution is needed. The good news is a better solution already exists and it works currently with RC1 of ASP.NET Core and will most surely also be available for RC2 and later releases.

There are actually 2 related projects that you should know about, MailKit and MimeKit. The goal of the MailKit project is “to provide the .NET world with robust, fully featured and RFC-compliant SMTP, POP3, and IMAP client implementations”, and indeed it meets that goal and in many ways is actually more powerful and flexible than the traditional System.Net.Mail components.

Here I will show some working example code that comes from my cloudscribe Core project. More specifically this example code is from my EmailSender class which is a work in progress, but functional enough to be a good example.

Lets start with a simple class to represent and encapsulate the settings we need to connect and authenticate with an SMTP Server:

public class SmtpOptions
{
    public string Server { get; set; } = string.Empty;
    public int Port { get; set; } = 25;
    public string User { get; set; } = string.Empty;
    public string Password { get; set; } = string.Empty;
    public bool UseSsl { get; set; } = false;
    public bool RequiresAuthentication { get; set; } = false;
    public string PreferredEncoding { get; set; } = string.Empty;
}

You would new up one of these and set the properties according to your SMTP server configuration, then you would pass that in as one of the parameters to an EmailSender class that looks something like this:

using MailKit.Net.Smtp;
using MimeKit;
using System;
using System.Threading.Tasks;

namespace cloudscribe.Messaging.Email
{

    public class EmailSender
    {
        public EmailSender()
        {
        }

        public async Task SendEmailAsync(
            SmtpOptions smtpOptions,
            string to,
            string from,
            string subject,
            string plainTextMessage,
            string htmlMessage,
            string replyTo = null)
        {
            if (string.IsNullOrWhiteSpace(to))
            {
                throw new ArgumentException("no to address provided");
            }

            if (string.IsNullOrWhiteSpace(from))
            {
                throw new ArgumentException("no from address provided");
            }

            if (string.IsNullOrWhiteSpace(subject))
            {
                throw new ArgumentException("no subject provided");
            }

            var hasPlainText = !string.IsNullOrWhiteSpace(plainTextMessage);
            var hasHtml = !string.IsNullOrWhiteSpace(htmlMessage);
            if (!hasPlainText && !hasHtml)
            {
                throw new ArgumentException("no message provided");
            }

            var m = new MimeMessage();
          
            m.From.Add(new MailboxAddress("", from));
            if(!string.IsNullOrWhiteSpace(replyTo))
            {
                m.ReplyTo.Add(new MailboxAddress("", replyTo));
            }
            m.To.Add(new MailboxAddress("", to));
            m.Subject = subject;

            //m.Importance = MessageImportance.Normal;
            //Header h = new Header(HeaderId.Precedence, "Bulk");
            //m.Headers.Add()

            BodyBuilder bodyBuilder = new BodyBuilder();
            if(hasPlainText)
            {
                bodyBuilder.TextBody = plainTextMessage;
            }

            if (hasHtml)
            {
                bodyBuilder.HtmlBody = htmlMessage;
            }

            m.Body = bodyBuilder.ToMessageBody();
           
            using (var client = new SmtpClient())
            {
                await client.ConnectAsync(
                    smtpOptions.Server,
                    smtpOptions.Port,
                    smtpOptions.UseSsl)
                    .ConfigureAwait(false);
               
                // Note: since we don't have an OAuth2 token, disable
                // the XOAUTH2 authentication mechanism.
                client.AuthenticationMechanisms.Remove("XOAUTH2");

                // Note: only needed if the SMTP server requires authentication
                if(smtpOptions.RequiresAuthentication)
                {
                    await client.AuthenticateAsync(smtpOptions.User, smtpOptions.Password)
                        .ConfigureAwait(false);
                }
               
                await client.SendAsync(m).ConfigureAwait(false);
                await client.DisconnectAsync(true).ConfigureAwait(false);
            }

        }

        public async Task SendMultipleEmailAsync(
            SmtpOptions smtpOptions,
            string toCsv,
            string from,
            string subject,
            string plainTextMessage,
            string htmlMessage)
        {
            if (string.IsNullOrWhiteSpace(toCsv))
            {
                throw new ArgumentException("no to addresses provided");
            }

            if (string.IsNullOrWhiteSpace(from))
            {
                throw new ArgumentException("no from address provided");
            }

            if (string.IsNullOrWhiteSpace(subject))
            {
                throw new ArgumentException("no subject provided");
            }

            var hasPlainText = !string.IsNullOrWhiteSpace(plainTextMessage);
            var hasHtml = !string.IsNullOrWhiteSpace(htmlMessage);
            if (!hasPlainText && !hasHtml)
            {
                throw new ArgumentException("no message provided");
            }

            var m = new MimeMessage();
            m.From.Add(new MailboxAddress("", from));
            string[] adrs = toCsv.Split(',');

            foreach (string item in adrs)
            {
                if (!string.IsNullOrEmpty(item)) { m.To.Add(new MailboxAddress("", item)); ; }
            }

            m.Subject = subject;
            m.Importance = MessageImportance.High;
          
            BodyBuilder bodyBuilder = new BodyBuilder();
            if (hasPlainText)
            {
                bodyBuilder.TextBody = plainTextMessage;
            }

            if (hasHtml)
            {
                bodyBuilder.HtmlBody = htmlMessage;
            }

            m.Body = bodyBuilder.ToMessageBody();

            using (var client = new SmtpClient())
            {
                await client.ConnectAsync(
                    smtpOptions.Server,
                    smtpOptions.Port,
                    smtpOptions.UseSsl).ConfigureAwait(false);
               
                // Note: since we don't have an OAuth2 token, disable
                // the XOAUTH2 authentication mechanism.
                client.AuthenticationMechanisms.Remove("XOAUTH2");

                // Note: only needed if the SMTP server requires authentication
                if (smtpOptions.RequiresAuthentication)
                {
                    await client.AuthenticateAsync(
                        smtpOptions.User,
                        smtpOptions.Password).ConfigureAwait(false);
                }

                await client.SendAsync(m).ConfigureAwait(false);
                await client.DisconnectAsync(true).ConfigureAwait(false);
            }

        }

    }
}

Note that I’ve implemented 2 methods here, one that sends email to a single recipient and one that takes a comma separated list of recipients. This code could probably be refactored a bit to reduce duplication, I actually plan to implement more overloads for handling things like attachments. This is just an initial working stub that I plan to evolve as I encounter more varied needs in my project. Note that you can pass in either or both an html formatted message or plain text, but you must of course at least pass in one of them. I’ve left a few comments in the code to show how things like message importance can be set, but really I’ve only scratched the surface of what MailKit/MimeKit can do for you, so I encourage you to explore the available api surface of those projects.

Feel free to borrow this code and use it in your own projects, and I hope you will also take a look at my various open source projects on github that may be of use or value to you on your projects.

Happy Coding!!!

Comments