.net core c# smtp

DE0005: SmtpClient shouldn't be used

Matt 2020/05/18 13:49:40
5073

Basic Knowledge

Few days ago, when quick look at some documents, I spot something, SmtpClient Class: This API is now obsolete. I'm shocked about missing the news.

It says that SmtpClient and its network of types are poorly designed, we strongly recommend you use MailKit and MimeKit instead.

Therefor, it's time now, to learn a new package to send mail via smtp. More detail, please find out here.

Send Mails

Let's see a simple demo, we send a test mail by using two of them.

    // send by obsolete SmtpClient
    private static void SendByObsoleteSmtp()
    {
        var msg = new System.Net.Mail.MailMessage()
        {
            From = new System.Net.Mail.MailAddress("someone@friends.com", "someone's display name"),
            Subject = "A test mail sent by obsolete smtp",
            BodyEncoding = System.Text.Encoding.UTF8,
            Body = $"<strong>Html format body, {DateTime.Now.Ticks}</strong>" +
            "<p>some text</p>" +
            ......
        };
        msg.To.Add(new System.Net.Mail.MailAddress("johndoe@domain.com", "a name"));

        using var client = new System.Net.Mail.SmtpClient("your_smtp_server", port)
        {
            Credentials = new System.Net.NetworkCredential("your_account", "password"),
            EnableSsl = true // if needed
        };
        client.Send(msg);
    }

    // send by MailKit
    private static async Task SendByMailKit()
    {
        var msg = new MimeMessage();
        msg.From.Add(new MailboxAddress("someone's display name", "someone@friends.com"));
        msg.To.Add(new MailboxAddress("a friend", "johndoe@domain.com"));
        msg.Subject = "A mail sent by MailKit";

        msg.Body = new TextPart("html")
        {
            Text = $"<strong>Html format body, {DateTime.Now.Ticks}</strong>" +
            "<p>some text</p>" +
            ......
        };

        using var client = new SmtpClient
        {
            ServerCertificateValidationCallback = (s, c, h, e) => true
        };
        await client.ConnectAsync("your_smtp_server", port, true).ConfigureAwait(true);
        // await client.ConnectAsync("your_smtp_server", post, MailKit.Security.SecureSocketOptions.StartTls).ConfigureAwait(true);
        client.Authenticate("your_account", "password");

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

Let's run the program, we might get two test mails.

img "test mails"

SSL/TLS

I'd love to use smtp.gmail.com to send test mails. While I did this, I observed an interesting phenomenon.

In obsoleted SmtpClient,

        using var client = new System.Net.Mail.SmtpClient("smtp.gmail.com", 587);
        client.Credentials = new System.Net.NetworkCredential("your_account", "password");
        client.EnableSsl = true;

the code shown above will send mail via smtp.gmil.com, and secured by SSL.

In MailKit SmtpClient,

        using var client = new MailKit.Net.Mail.SmtpClient();
        await client.ConnectAsync("smtp.gmail.com", 587, true).ConfigureAwait(true);
        client.Authenticate("your_account", "password");

ConnectAsync() has 5 overloads. We pick second signature to do our smtp work, just like the old SmtpClient.

img "ConnectAsync/no.2 signature"

If you do this, you'll get an error,

MailKit.Security.SslHandshakeException: 'An error occurred while attempting to establish an SSL or TLS connection.
One possibility is that you are trying to connect to a port which does not support SSL/TLS.

I'm curious for the error. But why ? That might be an interesting things to investigate it. Base on the document, you will see the difference between Port for SSL: 465Port for TLS/STARTTLS: 587.

Outgoing Mail (SMTP) Server smtp.gmail.com
  Requires SSL: Yes
  Requires TLS: Yes (if available)
  Requires Authentication: Yes
  Port for SSL: 465
  Port for TLS/STARTTLS: 587

At this time, I realize why to use MailKit instead of the old SmtpClient. The old one might NOT support TLS (as we know, TLS is NOT SSL) correctly.

For that, we did some changes.

        await client.ConnectAsync("smtp.gmail.com", 465, true).ConfigureAwait(true);

img "ConnectAsync/no.2 signature"

or,

        await client.ConnectAsync("smtp.gmail.com", 587, MailKit.Security.SecureSocketOptions.StartTls).ConfigureAwait(true);

img "ConnectAsync/no.3 signature"

Retrieving Messages

Since we can send mail by MailKit, what about Retrieving Messages?

-Retrieving Messages via POP3

Hmm..., most popular mail providers, I usually use gmail, and hotmail, they're all disabled the POP3 server. Thus, I am here just demonstrated it's code, not yet try them in real world.

Please do remember using MailKit.Net.Pop3; first, before you test the code.

        using var pop3 = new Pop3Client()
        {
            ServerCertificateValidationCallback = (s, c, h, e) => true
        };
        await pop3.ConnectAsync("your_pop3_server", port_number, use_ssl_or_not).ConfigureAwait(true);
        await pop3.AuthenticateAsync("your_account", "password").ConfigureAwait(true);

        for (var i = pop3.Count -1 ; i > 0; i--)
        {
            var message = pop3.GetMessageAsync(i).ConfigureAwait(true);
            Console.WriteLine($"Subject: {message.Subject}\nBody: {message.Body}");
        }
        await pop3.DisconnectAsync(true).ConfigureAwait(true);

see, the code snippet is easy to understand.

-Retrieving Messages via IMAP

Still, please don't forget doing using MailKit.Net.Imap; first, before you try the code.

        using var client = new ImapClient()
        {
            ServerCertificateValidationCallback = (s, c, h, e) => true
        };
        await client.ConnectAsync("your_imap_server", port_number, user_ssl_or_no).ConfigureAwait(true);
        await client.AuthenticateAsync("your_account", "password").ConfigureAwait(true);

        // open inbox to read messages
        var inbox = client.Inbox;
        await inbox.OpenAsync(ReadOnly).ConfigureAwait(true);

        Console.WriteLine("Total messages: {0}", inbox.Count);
        Console.WriteLine("Recent messages: {0}\n", inbox.Recent);

        for (var i = inbox.Count -1; i > 0; i--)
        {
            var msg = await inbox.GetMessageAsync(i).ConfigureAwait(true);
            Console.WriteLine($"Date: {msg.Date}\nId: {msg.MessageId}\nSubject: {msg.Subject}\n");
            Console.ReadLine();
        }
        await client.DisconnectAsync(true).ConfigureAwait(true);

As you're smart, you will find something different from POP3 call. Yes, we shall open inbox, before we read messages.

It's also easy to understand.

If you are interesting in how IMAP and POP worked, try to read this, it's useful.

-Fetch command

Fetch command allows us retrieving message parts without downloading all messages first. This is very useful for our retrieving messages.

Let's take a look an easy example, after IMAP inbox (of course, other folders are okay to open it) opened,

        foreach (var summary in inbox.Fetch(0, 10, MessageSummaryItems.Envelope | MessageSummaryItems.UniqueId))
        {
            Console.WriteLine($"UniqueIq: {summary.UniqueId}\nSubject: {summary.Envelope.Subject}\n\n");
        }
		

when fetched, the code shows us some mail messages' UniqueId, and Subject.

And, we also can use Fetch command to help us downloading attachments just we needed.

        if (summary.Body is BodyPartMultipart multipart)
        {
            var attachment = multipart.BodyParts.OfType<BodyPartBasic>()?.FirstOrDefault();
            if (attachment != null)
            {
                // this will download just the attachment
                var mimeEntity = await inbox.GetBodyPartAsync(summary.UniqueId, attachment).ConfigureAwait(true);
            }
        }

If we just need plain text part, just try this below,

        if (summary.TextBody != null)
        {
            // this will download *just* the text/plain part
            var text = await inbox.GetBodyPartAsync(summary.UniqueId, summary.TextBody).ConfigureAwait(true);
            Console.WriteLine($"TextBody: {text}");
        }

What if, we need html part, try the code shown below,

        if (summary.HtmlBody != null)
        {
            // this will download *just* the text/html part
            var html = await inbox.GetBodyPartAsync(summary.UniqueId, summary.HtmlBody).ConfigureAwait(true);
            Console.WriteLine($"HtmlBody: {html}");
        }

-Search command

We can also search from inbox (or other folders), that's try it.

        var q = SearchQuery.Seen.And(SearchQuery.SubjectContains("you"));
        foreach (var uniqueId in inbox.Search(q)) // when searched, you will get IList<UniqueId>
        {
            var msg = await inbox.GetMessageAsync(uniqueId).ConfigureAwait(true);
            Console.WriteLine($"UniqueId: {uniqueId}\nSubject: {msg.Subject}\n\n");
        }
		

It will show us some mails that set to read, and Subject contains "you" (the keyword is case insensitive).

Another example,

        var q = SearchQuery.DeliveredAfter(new DateTime(2019, 12, 1));
        foreach (var uniqueId in inbox.Search(q)) // when searched, you will get IList<UniqueId>
        {
            var msg = await inbox.GetMessageAsync(uniqueId).ConfigureAwait(true);
            Console.WriteLine($"UniqueId: {uniqueId}\nSubject: {msg.Subject}\n\n");
        }

It shows us mails delivered after a date point,

there're some DateSearchQuery we can considerate,

  • DeliveredAfter, Matches messages that were delivered after the specified date.
  • DeliveredBefor, Matches messages that were delivered before the specified date.
  • DeliveredOn, Matches messages that were delivered on exact date (search query does not include the time).

-Sort command

Of course, we can also sort our queries from inbox (or other folders).

        var q = SearchQuery.DeliveredAfter(new DateTime(2019, 12, 1));
        var orderBy = new[] { OrderBy.ReverseArrival };
        var data = await inbox.SortAsync(q, orderBy).ConfigureAwait(true);
        foreach (var uniqueId in data)
        {
            var msg = await inbox.GetMessageAsync(uniqueId).ConfigureAwait(true);
            Console.WriteLine($"UniqueId: {uniqueId}\nSentDate: {msg.Date}\nSubject: {msg.Subject}\n\n");
        }
		

However, when we run the code, we might face an error says that: Unhandled exception. System.NotSupportedException: The IMAP server does not support the SORT extension.

In my testing, neither Gmail, nor Hotmail doesn't support the SORT extension.

-Folders

Another important point, Folder, we can also retrieve all available folders in the mailbox.

        foreach (var ns in client.PersonalNamespaces)
        {
            var folders = client.GetFolder(ns);
            foreach (var sub in folders.GetSubfolders(false))
            {
                Console.WriteLine($"Folder Name: {sub.Name}\n");
            }
        }

then, we can do more, to list mails in Trash can,

        var junk = client.GetFolder(SpecialFolder.Junk);
        await junk.OpenAsync(ReadOnly).ConfigureAwait(true);

        var q = SearchQuery.DeliveredAfter(new DateTime(2019, 12, 1));
        foreach (var uid in junk.Search(q))
        {
            var msg = await junk.GetMessageAsync(uid).ConfigureAwait(true);
            Console.WriteLine($"UniqueId: {msg.MessageId}\nSubject: {msg.Subject}");
        }
		

just like that, easy understanding.

If need more api references, try read API documentation

Enjoy it.


References:

SmtpClient Class

SmtpClient shouldn't be used

MimeKit

TLS

POP vs IMAP | What is it, and what’s the difference?

Matt