Tutorial
Send email with .NET
Send email from ASP.NET Core and .NET with the Emailit NuGet package, MailKit or System.Net.Mail over SMTP, and verify Emailit webhooks.
This guide shows how to send email from a .NET app with the official Emailit NuGet package, how to use MailKit or System.Net.Mail over SMTP instead, and how to verify webhooks in an ASP.NET Core minimal API.
Prerequisites
- .NET 8 or later.
- A verified sending domain, for example
acme.com. - An API key. A Sending Only key restricted to your domain is enough.
- Until your workspace has production access, you can only send to the account emails of workspace members.
Install the SDK
dotnet add package EmailitConfigure your API key
ASP.NET Core reads configuration from environment variables, where __ separates sections. Set Emailit__ApiKey in production:
export Emailit__ApiKey=secret_••••••••••••••••••••••••••••••••During development, keep the key in user secrets instead of appsettings.json:
dotnet user-secrets init
dotnet user-secrets set "Emailit:ApiKey" "secret_••••••••••••••••••••••••••••••••"Send an email
Register one client and inject it into your endpoints:
using Emailit;
using Emailit.Options;
using Emailit.Resources;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton(new EmailitClient(builder.Configuration["Emailit:ApiKey"]!));
var app = builder.Build();
app.MapPost("/orders/{id}/confirm", (string id, EmailitClient emailit) =>
{
Email email = emailit.Emails.Send(new EmailSendOptions
{
From = "Acme <orders@acme.com>",
To = new[] { "ada@example.com" },
Subject = $"Order {id} confirmed",
Html = $"<p>We've received order {id}.</p>",
});
return Results.Ok(new { emailId = email.Id });
});
app.Run();Send is synchronous; for bulk sends, run it from a background service so request threads stay free. The options also accept Cc, Bcc, ReplyTo, Attachments, Template with Variables, ScheduledAt and Tracking; see Send an email.
If your code already builds System.Net.Mail.MailMessage objects, the SDK can send them through the API:
using System.Net.Mail;
var message = new MailMessage("orders@acme.com", "ada@example.com", "Order confirmed", "<p>Thanks!</p>")
{
IsBodyHtml = true,
};
Email email = emailit.Emails.Send(message);Handle errors
using Emailit.Exceptions;
try
{
emailit.Emails.Send(options);
}
catch (RateLimitException)
{
// 429: back off and retry
}
catch (AuthenticationException)
{
// 401: the API key is missing or invalid
}
catch (UnprocessableEntityException ex)
{
// 422: for example, the from domain isn't verified
logger.LogWarning("Emailit rejected the email: {Message}", ex.Message);
}
catch (ApiErrorException ex)
{
logger.LogError("Emailit error {Status}: {Body}", ex.HttpStatus, ex.HttpBody);
}Send with SMTP instead
MailKit
Microsoft recommends MailKit for new SMTP code. Install it with dotnet add package MailKit:
using MailKit.Net.Smtp;
using MailKit.Security;
using MimeKit;
var message = new MimeMessage();
message.From.Add(new MailboxAddress("Acme", "orders@acme.com"));
message.To.Add(MailboxAddress.Parse("ada@example.com"));
message.Subject = "Order confirmed";
message.Body = new TextPart("html") { Text = "<p>We've received your order.</p>" };
using var smtp = new SmtpClient();
await smtp.ConnectAsync("smtp.emailit.com", 587, SecureSocketOptions.StartTls);
await smtp.AuthenticateAsync("emailit", builder.Configuration["Emailit:ApiKey"]);
await smtp.SendAsync(message);
await smtp.DisconnectAsync(true);For implicit TLS, connect to port 465 with SecureSocketOptions.SslOnConnect.
System.Net.Mail
The built-in client works for simple cases. It only supports STARTTLS, so use port 587 (or 2525 or 2587):
using System.Net;
using System.Net.Mail;
using var client = new SmtpClient("smtp.emailit.com", 587)
{
EnableSsl = true,
Credentials = new NetworkCredential("emailit", apiKey),
};
client.Send(new MailMessage("orders@acme.com", "ada@example.com", "Order confirmed", "We've received your order."));See SMTP settings for every port.
Receive webhooks
Create a webhook and store its signing secret as Emailit:WebhookSecret. Read the raw body, verify it, then parse the array of events:
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
app.MapPost("/webhooks/emailit", async (HttpRequest request, IConfiguration config) =>
{
using var reader = new StreamReader(request.Body, Encoding.UTF8);
var body = await reader.ReadToEndAsync();
var signature = request.Headers["X-Emailit-Signature"].ToString();
var timestamp = request.Headers["X-Emailit-Timestamp"].ToString();
if (!long.TryParse(timestamp, out var ts) ||
Math.Abs(DateTimeOffset.UtcNow.ToUnixTimeSeconds() - ts) > 300)
{
return Results.Unauthorized();
}
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(config["Emailit:WebhookSecret"]!));
var expected = Convert.ToHexString(hmac.ComputeHash(Encoding.UTF8.GetBytes($"{timestamp}.{body}"))).ToLowerInvariant();
if (!CryptographicOperations.FixedTimeEquals(Encoding.UTF8.GetBytes(expected), Encoding.UTF8.GetBytes(signature)))
{
return Results.Unauthorized();
}
// The body is a JSON array of up to 100 events.
using var events = JsonDocument.Parse(body);
foreach (var evt in events.RootElement.EnumerateArray())
{
if (evt.GetProperty("type").GetString() == "email.bounced")
{
var address = evt.GetProperty("data").GetProperty("object").GetProperty("to").GetString();
// Stop emailing this address.
}
}
return Results.Ok();
});Return a 2xx within 30 seconds; other responses are retried. See Request signature.
Production tips
- Register the client as a singleton. One
EmailitClientper app is enough. - Queue bulk mail. Send from a
BackgroundServiceor a job library such as Hangfire, and throttle it to stay under your sending limits (2 emails per second by default). - Make retries safe. If you retry after a timeout, send an
Idempotency-Keyheader withHttpClient; see Idempotency. - Deduplicate webhooks by storing each
event_idyou process.