Groundmail

Blog

How to send email in C# (SMTP and API)

Send email from C# and .NET with MailKit over SMTP, the built-in SmtpClient, or an HTTP email API with HttpClient. Includes HTML, attachments, common errors and production tips.

By Groundmail4 min read

On this page

To send email in C#, build the message with MimeKit's MimeMessage and send it with MailKit's SmtpClient: connect to your provider's SMTP server on port 587 with STARTTLS, authenticate, and call SendAsync. The built-in System.Net.Mail.SmtpClient still works, but Microsoft doesn't recommend it for new code. For application email at volume, an HTTP email API called with HttpClient is often simpler: one authenticated request per message, with a message ID and structured errors back.

All three are below, with HTML, attachments, the errors you'll most likely hit, and what to change before production.

What you need

  • .NET 8 or later (the examples use C# 12)
  • An SMTP server or email API to send through, with credentials
  • A sending domain you control, with SPF, DKIM and DMARC set up, so your mail isn't filtered as spam

Keep credentials out of source code. In development, use user secrets; in production, environment variables or a secret store such as Azure Key Vault:

dotnet user-secrets init
dotnet user-secrets set "Smtp:Password" "your-password"

Option 1: MailKit over SMTP

MailKit is the de facto SMTP library for .NET. Install it:

dotnet add package MailKit

This sends an email with an HTML body, a plain-text fallback and an attachment:

using MailKit.Net.Smtp;
using MailKit.Security;
using MimeKit;

var message = new MimeMessage();
message.From.Add(new MailboxAddress("Acme", "billing@yourdomain.com"));
message.To.Add(MailboxAddress.Parse("user@example.com"));
message.Subject = "Your receipt";

var body = new BodyBuilder
{
    TextBody = "Thanks for your order. Your invoice is attached.",
    HtmlBody = "<p>Thanks for your order. Your invoice is attached.</p>",
};
body.Attachments.Add("invoice-1042.pdf");
message.Body = body.ToMessageBody();

using var smtp = new SmtpClient();
await smtp.ConnectAsync(Environment.GetEnvironmentVariable("SMTP_HOST")!, 587, SecureSocketOptions.StartTls);
await smtp.AuthenticateAsync(
    Environment.GetEnvironmentVariable("SMTP_USER")!,
    Environment.GetEnvironmentVariable("SMTP_PASSWORD")!);
await smtp.SendAsync(message);
await smtp.DisconnectAsync(true);

What each part does:

  • MimeMessage and BodyBuilder build a correctly encoded MIME message. BodyBuilder creates the multipart/alternative structure for the text and HTML versions and wraps it with the attachments. It reads the file and guesses its content type from the extension.
  • SecureSocketOptions.StartTls connects in plain text and upgrades to TLS before anything sensitive is sent, and fails if the server can't do STARTTLS. For implicit TLS on port 465, use SecureSocketOptions.SslOnConnect.
  • MimeKit adds Date and Message-ID headers for you. Messages missing them score worse with spam filters.
  • SendAsync returns the server's final reply, which often includes the provider's queue or message ID. Log it.

To send many messages, connect once and call SendAsync for each. Reconnecting and re-authenticating per message is the most common reason SMTP sending is slow. SmtpClient isn't thread-safe, so give each worker its own.

Option 2: System.Net.Mail.SmtpClient

The SmtpClient that ships with .NET still works for simple cases. EnableSsl = true means STARTTLS on port 587:

using System.Net;
using System.Net.Mail;

using var client = new SmtpClient(Environment.GetEnvironmentVariable("SMTP_HOST"), 587)
{
    EnableSsl = true,
    Credentials = new NetworkCredential(
        Environment.GetEnvironmentVariable("SMTP_USER"),
        Environment.GetEnvironmentVariable("SMTP_PASSWORD")),
};

using var message = new MailMessage("billing@yourdomain.com", "user@example.com", "Your receipt", "<p>Thanks for your order.</p>")
{
    IsBodyHtml = true,
};
message.Attachments.Add(new Attachment("invoice-1042.pdf"));

await client.SendMailAsync(message);

Microsoft's documentation says SmtpClient "doesn't support many modern protocols" and is not recommended for new development. In practice the gaps that bite are no implicit TLS on port 465, limited authentication mechanisms and no further improvements. Keep it in existing code if it works, and pick MailKit or an API for anything new.

Option 3: an HTTP email API with HttpClient

With an API you skip the SMTP conversation and post JSON. Here's the same receipt with the Groundmail API, using nothing but HttpClient:

using System.Net.Http.Headers;
using System.Net.Http.Json;

using var http = new HttpClient { BaseAddress = new Uri("https://api.groundmail.dev/") };
http.DefaultRequestHeaders.Authorization =
    new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("GROUNDMAIL_API_KEY"));

using var request = new HttpRequestMessage(HttpMethod.Post, "emails")
{
    Content = JsonContent.Create(new
    {
        from = "Acme <billing@yourdomain.com>",
        to = new[] { "user@example.com" },
        subject = "Your receipt",
        html = "<p>Thanks for your order.</p>",
        text = "Thanks for your order.",
    }),
};
request.Headers.Add("Idempotency-Key", "receipt-order-1042");

using var response = await http.SendAsync(request);
Console.WriteLine(await response.Content.ReadAsStringAsync()); // {"id":"msg_…","status":"sent"}

You get a message ID back that you can store and look up later, and errors come back as JSON with a code you can branch on instead of an SMTP reply string. The Idempotency-Key header makes retries safe: a repeated request with the same key returns the original response instead of sending a second email.

In ASP.NET Core, wrap this in a typed HttpClient registered with AddHttpClient and bind the key from configuration. The C# and .NET guide has a complete typed client with dependency injection, error handling and retries.

Common errors and what they mean

MailKit.Security.AuthenticationException: 535: Authentication failed. The server rejected your username or password. Check for whitespace in your configuration values, and that you're using the right credential type: many providers want an SMTP-specific credential or API key, not your account password. Gmail returns 535 5.7.8 Username and Password not accepted unless you use an app password.

MailKit.Security.SslHandshakeException when connecting. Usually the port and TLS mode don't match: SslOnConnect on port 587, or StartTls on port 465. Use StartTls with 587 and SslOnConnect with 465. If the message mentions "an incomplete certificate revocation check", the OS couldn't reach the certificate authority's revocation list (common on macOS and locked-down networks); setting smtp.CheckCertificateRevocation = false skips only that check.

System.Net.Mail.SmtpException: The SMTP server requires a secure connection or the client was not authenticated. The server response was: 5.7.0 Must issue a STARTTLS command first. You left EnableSsl at its default of false. Set it to true.

SocketException or a timeout on connect. The port is blocked. Most cloud providers, Azure included, block outbound port 25. Use 587 or 465 for submission.

MailKit.Net.Smtp.SmtpCommandException: 550 ... or 554 .... The server refused the sender, a recipient or the message. Read the message: a refusal about the sender usually means your From domain isn't verified with the provider. SmtpCommandException.ErrorCode tells you which step failed (SenderNotAccepted, RecipientNotAccepted or MessageNotAccepted).

Production tips

  • Keep credentials in configuration, never in source, and use a credential that can only send.
  • Send from a background job, such as a hosted service reading from a Channel<T> or a queue consumer, not inside the web request.
  • Retry temporary failures with backoff. Retry 4xx SMTP replies, HTTP 429 and 5xx responses and network errors. Don't retry permanent failures like 5xx SMTP replies or HTTP 400 and 422: they'll fail the same way.
  • Make retries safe, with an idempotency key derived from what the email is for (an order ID, a reset token ID), so a request that timed out after the provider accepted it doesn't send twice.
  • Send a plain-text part with your HTML. Some clients show only text, and spam filters penalise HTML-only messages.

How Groundmail helps

Groundmail gives you both options on the same account: an SMTP relay at smtp.groundmail.dev on port 587 for MailKit or SmtpClient, and a REST API you call with HttpClient, including keyless auth with an Azure managed identity so there's no API key to store. Test keys and simulator addresses let you test bounces and complaints without sending real mail. The C# and .NET guide covers ASP.NET Core setup end to end. To check that what your code sends lands in the inbox, send one message to the free email spam test.

Frequently asked questions

What is the best way to send email in C#?

For SMTP, use MailKit, the library Microsoft's own SmtpClient documentation points to for new code. Build the message with MimeKit's MimeMessage, connect on port 587 with SecureSocketOptions.StartTls, authenticate and call SendAsync. For application email at volume, an HTTP email API called with HttpClient gives you message IDs, structured errors and safe retries.

Is System.Net.Mail.SmtpClient deprecated?

It isn't removed, and it still works for basic SMTP with STARTTLS on port 587 (EnableSsl = true). Microsoft marks it as not recommended for new development because it doesn't support many modern protocols. Use MailKit or an email API for new code.

Should I use port 587 or 465 with MailKit?

Use port 587 with SecureSocketOptions.StartTls, or port 465 with SecureSocketOptions.SslOnConnect if your provider offers implicit TLS. Mixing them up fails with a handshake error or a timeout. Don't use port 25 from cloud hosts; it's usually blocked.

How do I send email from ASP.NET Core?

Register a typed HttpClient for your email API (or a MailKit-based service) in Program.cs, bind its settings from configuration, and inject it where you send. Send from a background job rather than inside the request so a slow mail server doesn't slow your page.

Send from infrastructure we own.

Groundmail runs its own mail servers on its own IP space. Start free on the shared transactional pool, then move to dedicated IPs or a dedicated server without changing your code.