4 ways to send email with C#

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.

Which option should you pick?

  • MailKit over SMTP if you have been handed SMTP credentials, or must send through a mail server you do not control.
  • SendGrid for transactional email when you want a simple API, per-message analytics, and a free tier to start on.
  • Amazon SES if you are already on AWS, because credentials come from the same IAM chain as everything else.
  • A notification API once email is one channel among several, or when templates, per-user preferences, and one delivery log matter more than the send call.

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.

1. Send over SMTP with MailKit

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

  • .NET 6 or newer.
  • SMTP credentials: host, port, username, and password.
  • For Gmail, an app password rather than your account password, which requires two-factor authentication on the account. Google removed "less secure app access" in 2022, so the older approach cannot work.

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.

2. Send with SendGrid

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

  • A SendGrid account, and a verified sender identity. Sending from an unverified address is rejected, and it is the usual first-attempt failure.
  • An API key with Mail Send permission.

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.

3. Send with Amazon SES

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

  • An AWS account, and a verified sending domain or address in SES.
  • To know whether your account is still in the SES sandbox, where you can only send to verified addresses. This is the most common reason a correct-looking send never arrives.
  • Credentials available through the normal AWS chain.

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.

4. Send with a notification API

The three options above solve delivery for one channel. Here is what tends to arrive afterward:

  • Copy is a string literal in compiled code, so a wording change is a deploy.
  • Someone unsubscribes and you now own a suppression list.
  • The same event should reach one user by email and another by push.
  • Your provider has a bad hour and there is no second path.
  • Twenty events fire in a minute and one user gets twenty emails.
  • Someone asks how many notifications failed last week and you cannot answer.

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:

  • Automatic failover between providers when one starts erroring.
  • Journeys for multi-step sequences, with digest and throttling nodes.
  • One send endpoint for email, SMS, push, in-app, Slack, and Microsoft Teams.
  • Per-user, per-channel preferences enforced before a send.
  • Templates edited outside your codebase, with localization per user. For a compiled language this takes copy changes off your release cycle.
  • One log covering every channel.

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.

Comparing the four options

MailKitSendGridAmazon SESNotification API
NuGet packageMailKit 4.17.0SendGrid 9.29.3AWSSDK.SimpleEmailV2 4.0.102.1TryCourier 5.21.0
TransportSMTPHTTPSHTTPSHTTPS
CredentialsSMTP user and passwordAPI keyAWS IAM chainAPI key
Blocked-port riskreal, 25 and 587nonono
Free tieryour relay's termsyes, check current termsyes, check current termsyes, check current terms
Delivery statusaccept or rejectper-message analyticsevent publishing to CloudWatchone log, per-message status
Attachmentsyes, BodyBuilderyesyesyes
Templates outside your codenoyes, SendGrid-hostedbasicyes
Preferences and unsubscribesyou build itunsubscribe groupsyou build itbuilt in
Provider failovern/an/an/ayes, across providers
Other channelsnonenonenoneSMS, push, in-app, chat
Best forsomeone else's mail servertransactional email, fast startteams already on AWSemail 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.

Next steps

FAQ

Frequently asked questions

Add MailKit, build a `MimeMessage` with sender, recipient, subject, and body, then connect an `SmtpClient`, authenticate, and call `SendAsync`. For transactional email an HTTPS API such as SendGrid or Amazon SES is usually a better fit, because you get delivery events and no outbound-port problems. .NET has no email support beyond SMTP in the base class library.

One API, every channel

Ship notifications without the boilerplate

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.