Send email from C# with MailKit, SendGrid, Amazon SES, or a notification API. Current NuGet packages and code that compiles, plus how to choose between them.
Updated Aug 7, 2026
Last updated: August 2026. All package versions verified against NuGet on 2026-08-06.
.NET has SMTP in the base class library and nothing above it, so sending email from C# means picking between an SMTP library and an HTTPS API. There are four realistic options, and they differ mostly in what you get back after the send.
One thing before you start: do not use System.Net.Mail.SmtpClient for new code. Microsoft's own documentation for the class says its use is not recommended and points at MailKit. It is worth being precise about why, because the class is not marked [Obsolete] and compiles without a warning: it predates modern TLS expectations, has awkward async support, and does not support implicit TLS on port 465.
MailKit is the library Microsoft's documentation recommends in place of SmtpClient. It is a .NET Foundation project and handles modern TLS, authentication, and MIME properly.
What you need first
Install
dotnet add package MailKit
Verified against MailKit 4.17.0, which brings in MimeKit 4.17.0.
Send the message
using MailKit.Net.Smtp;using MailKit.Security;using MimeKit;var message = new MimeMessage();message.From.Add(new MailboxAddress("Acme", "orders@example.com"));message.To.Add(new MailboxAddress("Erika", "erika@example.com"));message.Subject = "Your order shipped";message.Body = new TextPart("html"){Text = "<h1>Your order shipped</h1><p>Track it in the app.</p>",};using var client = new SmtpClient();// StartTls on 587. For implicit TLS use SecureSocketOptions.SslOnConnect on 465.await client.ConnectAsync("smtp.example.com", 587, SecureSocketOptions.StartTls);await client.AuthenticateAsync(Environment.GetEnvironmentVariable("SMTP_USERNAME"),Environment.GetEnvironmentVariable("SMTP_PASSWORD"));await client.SendAsync(message);await client.DisconnectAsync(true);
Note this is MailKit.Net.Smtp.SmtpClient, not System.Net.Mail.SmtpClient. If you have both namespaces imported you will need to disambiguate.
SecureSocketOptions is the part that makes MailKit worth the swap. StartTls on 587 and SslOnConnect on 465 are both supported and explicit, where System.Net.Mail.SmtpClient's EnableSsl only ever meant STARTTLS. Setting that class's port to 465 and enabling SSL, which older tutorials do, produces a hang rather than an error.
Use TextPart("plain") for plain text, or a BodyBuilder when you want both an HTML and a plain-text alternative plus attachments.
Where it gets thin: an SMTP accept or reject is all you get. No delivery status, no bounce handling, no per-message ID. Many hosting platforms block outbound 25 and some block 587.
SendGrid is a transactional email API with a maintained .NET library and per-message analytics, and it has a free tier for getting started.
What you need first
Install
dotnet add package SendGrid
Verified against SendGrid 9.29.3.
Send the message
using SendGrid;using SendGrid.Helpers.Mail;var client = new SendGridClient(Environment.GetEnvironmentVariable("SENDGRID_API_KEY"));var from = new EmailAddress("orders@example.com", "Acme");var to = new EmailAddress("erika@example.com", "Erika");const string plainTextContent = "Your order shipped. Track it in the app.";const string htmlContent = "<h1>Your order shipped</h1><p>Track it in the app.</p>";var msg = MailHelper.CreateSingleEmail(from, to, "Your order shipped", plainTextContent, htmlContent);var response = await client.SendEmailAsync(msg);if (!response.IsSuccessStatusCode){var body = await response.Body.ReadAsStringAsync();Console.Error.WriteLine($"SendGrid rejected the send: {response.StatusCode} {body}");}
The status check matters. SendEmailAsync does not throw on a 401 or a 403, so without it a rejected send is indistinguishable from a successful one. A successful send returns 202 Accepted, not 200.
MailHelper.CreateSingleEmail takes both a plain-text and an HTML body and builds the multipart message for you. Passing both is worth doing, since some clients and most spam filters prefer a text alternative to be present.
Where it gets thin: one provider, copy in your source, and everything above delivery is yours to build.
SES fits when you are already on AWS, mostly because credentials come from the standard IAM chain rather than being another secret to inject.
Two corrections to older guides. The free tier of 62,000 emails per month that older guides quote applied only to EC2-hosted senders and has been discontinued; check SES pricing for the current allowance rather than trusting a number in a tutorial. And for analytics, use SES event publishing to CloudWatch, Firehose, or SNS. Do not add AWS Pinpoint, which older guides recommend: AWS ends support for Pinpoint on 30 October 2026.
What you need first
Install
dotnet add package AWSSDK.SimpleEmailV2
Verified against AWSSDK.SimpleEmailV2 4.0.102.1. Note the V2: that is the SES v2 API, which is the current one. The older AWSSDK.SimpleEmail package targets the v1 API.
Send the message
using Amazon.SimpleEmailV2;using Amazon.SimpleEmailV2.Model;using var client = new AmazonSimpleEmailServiceV2Client();var request = new SendEmailRequest{FromEmailAddress = "orders@example.com",Destination = new Destination{ToAddresses = new List<string> { "erika@example.com" },},Content = new EmailContent{Simple = new Message{Subject = new Content { Data = "Your order shipped", Charset = "UTF-8" },Body = new Body{Html = new Content{Data = "<h1>Your order shipped</h1>",Charset = "UTF-8",},Text = new Content{Data = "Your order shipped. Track it in the app.",Charset = "UTF-8",},},},},};var response = await client.SendEmailAsync(request);Console.WriteLine(response.MessageId);
The nesting is the part to get right, and it is where the old version of this page went wrong: Destination holds only recipients, and Content is a sibling of it on the request, not a child. Content wraps a Simple Message, which holds a Subject and a Body, and the Body holds Html and Text parts.
Constructing the client with no arguments picks up region and credentials from the environment, a profile, or an IAM role.
Where it gets thin: SES is a sending service, not a notification system. Its templating is basic, and preferences, digesting, and other channels are yours. You are also committing to AWS.
The three options above solve delivery for one channel. Here is what tends to arrive afterward:
A notification API such as Courier sits above your email provider and handles those. You keep SendGrid or SES delivering, and your C# stops knowing which one it is.
What the layer gives you:
Set it up
Create a free account, then connect an email provider under Channels using the credentials it already gave you. Same SendGrid or SES account as above; you are not changing who delivers.
Install
dotnet add package TryCourier
Verified against TryCourier 5.21.0, targeting .NET Standard 2.0 or later. Note the package name: an older Courier.Client package exists on NuGet but stalled at 0.2.0 in November 2024. TryCourier is the maintained one.
Send the message
using TryCourier;using TryCourier.Models;using TryCourier.Models.Send;// Reads COURIER_API_KEY from the environment.CourierClient client = new();SendMessageParams parameters = new(){Message = new(){To = new UserRecipient { Email = "erika@example.com" },Content = new ElementalContentSugar{Title = "Your order shipped",Body = "Track it anytime in the app.",},},};var response = await client.Send.Message(parameters);Console.WriteLine(response);
The version you will run in production references a template you published in Courier and addresses the user rather than a raw address, so their channel preferences apply:
using System.Text.Json;SendMessageParams parameters = new(){Message = new(){To = new UserRecipient { UserID = "user-123" },Template = "ORDER_SHIPPED",Data = new Dictionary<string, JsonElement>{{ "name", JsonSerializer.SerializeToElement("Erika") },{ "tracking_url", JsonSerializer.SerializeToElement("https://example.com/t/abc") },},},};
Nothing in that call names email. Routing and the user's stored preferences decide the channel, so moving a notification to push, or sending both, is configuration rather than a rebuild.
Data takes Dictionary<string, JsonElement>, so template variables go through JsonSerializer.SerializeToElement. That is more ceremony than a dynamically typed SDK needs, which is why the second example imports System.Text.Json.
If you prefer configuration to environment variables, new CourierClient { ApiKey = "..." } works, but read that key from configuration rather than writing it into source.
Where it gets thin: one more service in the path, and a concept to learn before your first send. If you only ever send one email from one provider, the SDK for that provider is less machinery.
| MailKit | SendGrid | Amazon SES | Notification API | |
|---|---|---|---|---|
| NuGet package | MailKit 4.17.0 | SendGrid 9.29.3 | AWSSDK.SimpleEmailV2 4.0.102.1 | TryCourier 5.21.0 |
| Transport | SMTP | HTTPS | HTTPS | HTTPS |
| Credentials | SMTP user and password | API key | AWS IAM chain | API key |
| Blocked-port risk | real, 25 and 587 | no | no | no |
| Free tier | your relay's terms | yes, check current terms | yes, check current terms | yes, check current terms |
| Delivery status | accept or reject | per-message analytics | event publishing to CloudWatch | one log, per-message status |
| Attachments | yes, BodyBuilder | yes | yes | yes |
| Templates outside your code | no | yes, SendGrid-hosted | basic | yes |
| Preferences and unsubscribes | you build it | unsubscribe groups | you build it | built in |
| Provider failover | n/a | n/a | n/a | yes, across providers |
| Other channels | none | none | none | SMS, push, in-app, chat |
| Best for | someone else's mail server | transactional email, fast start | teams already on AWS | email plus other channels |
If you have SMTP credentials, MailKit is the whole answer. If you are starting fresh on transactional email, SendGrid is the shortest path and SES is the cheaper one at volume. If email is becoming one of several channels, the notification layer is what stops copy changes from being deploys.
FAQ
Keep exploring
One API, every channel
Courier gives you one API for email, SMS, push, and chat, with templates, routing, retries, and delivery logs built in.
Last updated Aug 7, 2026. Code samples are illustrative; provider APIs and pricing change over time, so check each provider’s docs before relying on them.
© 2026 Courier. All rights reserved.