Dev Life
.NET is one of the most popular, stable, and admired web frameworks you can use to build modern web applications. The tooling provided by .NET can help you get started quickly and maintain your applications well into the future. With it, you can build everything from small laser-focused microservices to full-fledged web applications.
Eventually, though, every real-life application needs to be able to send transactional emails (such as account verification, password resets, automated daily/monthly summary reports, and so on). While .NET offers tooling that helps you send emails, it doesn’t have the production-capable infrastructure you need to reliably and efficiently send them.
In this article, you’ll learn how to use Mailgun’s HTTP API to send transactional emails from your .NET applications and ensure that your integration is reliable, scalable, and secure.
Before you begin this tutorial, you’ll need the following prerequisites:
If you choose not to use Visual Studio and running dotnet –info from a terminal isn’t successful, then you’ll need to download and install the latest .NET SDK.
Start by creating a free Mailgun account. Once you’re logged in, create a new API key and store it for later use.
For production email sending, you need to register a domain that you own with Mailgun. In this tutorial, you’ll use the sandbox domain provided by Mailgun.
Once your Mailgun account is set up, you need to create a new .NET MVC web solution and configure some additional tools to help you send transactional emails with .NET.
Open a terminal and run dotnet new mvc -n MailDemo. This command creates a new .NET web project using an MVC architecture inside a new /MailDemo folder. Next, execute cd MailDemo to navigate into the new folder created for you.
After you’ve navigated into the folder, you need to add your configuration values to appsettings.json. Add a new JSON entry named “Mailgun”. The value for Mailgun:ApiKey is the API key that you created in the previous section (you’ll learn about securing your API keys later). Mailgun:Domain is the sandbox domain that has already been created for you.
Your entire appsettings.json file should look like this:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"Mailgun": {
"ApiKey": "<Your API Key>",
"Domain": "<Your sandbox domain>"
}
}
Next, you need to configure a named HttpClient for easy reuse of default parameters. To do so, open Program.cs. The top of the file should look like this:
using System.Net.Http.Headers;
using System.Text;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container
builder.Services.AddControllersWithViews();
// Set up your named HttpClient for easy reuse
builder.Services.AddHttpClient("Mailgun", client =>
{
// Grab values from the configuration
var apiKey = builder.Configuration.GetValue<string>("Mailgun:ApiKey");
var base64Auth = Convert.ToBase64String(Encoding.ASCII.GetBytes($"api:{apiKey}"));
var domain = builder.Configuration.GetValue<string>("Mailgun:Domain");
// Set default values on the HttpClient
client.BaseAddress = new Uri($"https://api.mailgun.net/v3/{domain}/messages");
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", base64Auth);
});
var app = builder.Build();
Since you’ll only be sending emails, your HttpClient is configured with the full URL from Mailgun’s HTTP API that’s used for sending mail.
At this point, everything you need to send your first email is ready.
The HttpClient is configured to send HTTP requests to the https://api.mailgun.net/v3/{domain}/messages endpoint. Mailgun also offers other endpoints to help you have a robust and holistic tool for managing and sending transactional emails:
Before you send your first transactional email, make sure that the recipient email address you use is one of the authorized recipients for your sandbox domain. In your Mailgun account, view your domains, open your sandbox domain, and on the right-hand side, add your personal email as an authorized recipient:
Next, create a new file at /Controllers/OrderController.cs and replace the contents of the file with the following:
using System.Diagnostics;
using Microsoft.AspNetCore.Mvc;
using MailDemo.Models;
using System.Text;
using System.Net.Mime;
namespace MailDemo.Controllers;
public class OrderController : Controller
{
private readonly HttpClient _httpClient;
private readonly IConfiguration _config;
public OrderController(IHttpClientFactory httpClientFactory, IConfiguration config)
{
// Get your named HttpClient that is preconfigured
this._httpClient = httpClientFactory.CreateClient("Mailgun");
this._config = config;
}
public async Task<IActionResult> Confirm()
{
using MultipartFormDataContent form = new();
// Local function keeps this code a bit clearer
void SetFormParam(string key, string value) =>
form.Add(new StringContent(value, Encoding.UTF8, MediaTypeNames.Text.Plain), key);
SetFormParam("from", $"Test User <postmaster@{this._config.GetValue<string>("Mailgun:Domain")}>");
SetFormParam("to", "your_authorized_recipient.com");
SetFormParam("subject", "Hello World!");
SetFormParam("text", "My first transactional email!");
SetFormParam("html", @"<html><body><p style=""color:blue;"">My first transactional email!</p></body></html>");
var result = await this._httpClient.PostAsync(string.Empty, form);
if (!result.IsSuccessStatusCode)
{
return new JsonResult(await result.Content.ReadAsStringAsync());
}
return new JsonResult(@"Your order was confirmed. You should get an email soon!");
}
}
Execute dotnet run from your terminal to start your web application. Then, open a web browser and navigate to /order/confirm. You should get an email in your inbox within a couple of seconds:
Unfortunately, sometimes things go wrong. For instance, your API key could have a typo, maybe you didn’t add all the required fields to the HTTP request, or you could hit Mailgun’s rate limits.
A failed HTTP request to Mailgun’s API will return one of four different HTTP error codes:
Let’s take a look at how you can handle some of these failure scenarios.
Whenever you get a 400 HTTP status code returned, it means that your payload or formatting was incorrect. For example, Mailgun will respond with a 400 HTTP status code if a required field is missing.
In these cases, there’s a general approach that you can take:
Here’s what the code for this could look like:
if (!result.IsSuccessStatusCode)
{
var status = (int) result.StatusCode;
if (status == 400)
{
var responseContent = await result.Content.ReadAsStringAsync();
var exception = new BadHttpRequestException("Mailgun 400 HTTP status code");
this._logger.LogError(exception, exception.Message, responseContent);
throw exception;
}
}
You may also want to take a similar approach with the 401 HTTP status code.
Whenever you receive a 500 HTTP status code, it may mean something has gone wrong on Mailgun’s side or a general network issue was experienced. In these cases, there are a few different approaches you could take:
Here’s what a basic retry approach might look like:
if (!result.IsSuccessStatusCode)
{
var status = (int) result.StatusCode;
if (status == 500)
{
// Retry the email after 1 second in hopes that the error was transient
await Task.Delay(1000);
result = await _httpClient.PostAsync(string.Empty, form);
if (!result.IsSuccessStatusCode)
{
// Log and throw an exception like you did in the previous example
}
}
}
In this scenario, if you receive a 500 status code, you’ll wait for one second and try the HTTP request again. If the retried request also has a failure, then you’ll log and throw an exception in the same way that you did for the 400 status code scenario.
To give you an idea of what the overall code might look like, here’s a skeleton of how your code could begin to handle various error scenarios:
if (!result.IsSuccessStatusCode)
{
var status = (int)result.StatusCode;
if (status == 400)
{
// Log the error and notify the development team that the
// request was not formatted properly.
}
else if (status == 401)
{
// Log the error and notify the development team that the
// request's authentication failed.
}
else if (status == 429)
{
// Use a .NET library like Polly to throttle or try this request
// again with an exponential back-off, etc.
}
else
{
// Something went wrong: log the error and apply a distributed
// error handling technique like a circuit breaker.
// See https://learn.microsoft.com/en-us/azure/architecture/patterns/circuit-breaker for more.
}
}
At this point, you’ve sent an email with .NET and C#, but to get production ready, there are a few important things to consider.
You cannot store your real production API key in source code. Additionally, it should not be accessible by anyone in plain text files.
There are many tools you can use to secure your API key. Every cloud provider has its own key management system that allows your application to retrieve your secrets securely. For example, Azure Key Vault or AWS Secrets Manager work great.
Mailgun also supports many production-grade features you may need, such as:
To demonstrate how easy it is to integrate with production-ready tooling from the .NET ecosystem, go ahead and use an open source library like Coravel to enhance the reuse and developer experience of your solution.
Execute dotnet add package coravel.mailer from your terminal while in the MailDemo folder.
Then, add the following JSON entry to your appsettings.json file:
"Coravel": {
"Mail": {
"From": {
"Name": "Test User",
"Email": "postmaster@<Your sandbox domain>"
}
}
}
Next, create a class file called MailgunMailer.cs that implements the ICanSendMail interface. You might notice that this is essentially the same logic you originally had in OrderController:
using System.Net.Mime;
using System.Text;
using Coravel.Mailer.Mail;
namespace MailDemo;
public class MailgunMailer : ICanSendMail
{
private readonly HttpClient _httpClient;
private readonly IConfiguration _config;
public MailgunMailer(IHttpClientFactory httpClientFactory, IConfiguration config)
{
this._httpClient = httpClientFactory.CreateClient("Mailgun");
this._config = config;
}
public async Task SendAsync(MessageBody message, string subject, IEnumerable<MailRecipient> to, MailRecipient from, MailRecipient replyTo, IEnumerable<MailRecipient> cc, IEnumerable<MailRecipient> bcc, IEnumerable<Attachment>? attachments = null, MailRecipient? sender = null)
{
using MultipartFormDataContent form = new();
void SetFormParam(string key, string value) =>
form.Add(new StringContent(value, Encoding.UTF8, MediaTypeNames.Text.Plain), key);
if (from is not null)
{
SetFormParam("from", $"{from.Name} <{from.Email}>");
}
else
{
// This gives you a default "from" field if not defined by the caller
SetFormParam("from", $"{this._config.GetValue<string>("Coravel:Mail:From:Name")} <{this._config.GetValue<string>("Coravel:Mail:From:Email")}>");
}
foreach (var recipient in to)
{
SetFormParam("to", recipient.Email);
}
SetFormParam("subject", subject);
if (message.HasHtmlMessage())
{
SetFormParam("html", message.Html);
}
if (message.HasPlainTextMessage())
{
SetFormParam("text", message.Text);
}
var result = await _httpClient.PostAsync(string.Empty, form);
// Error handling logic...
}
}
Next, you need to tell Coravel to use this mailer. In your Program.cs file, before var app = builder.Build() is called, add the following:
builder.Services.AddScoped<MailDemo.MailgunMailer>();
builder.AddCustomMailer<MailDemo.MailgunMailer>();
Finally, replace the code for OrderController with the following:
using Microsoft.AspNetCore.Mvc;
using Coravel.Mailer.Mail.Interfaces;
using Coravel.Mailer.Mail;
namespace MailDemo.Controllers;
public class OrderController : Controller
{
private readonly IMailer _mailer;
public OrderController(IMailer mailer)
{
this._mailer = mailer;
}
public async Task<IActionResult> Confirm()
{
var mailable = Mailable.AsInline()
.To("jamesmichaelhickey@gmail.com")
.Subject("Hello World!")
.Html(@"<html><body><p style=""color:blue;"">My first transactional email!</p></body></html>")
.Text("My first transactional email!");
await _mailer.SendAsync(mailable);
return new JsonResult(@"Your order was confirmed. You should get an email soon!");
}
}
After a little bit of a one-time configuration, you’ll notice the logic for sending emails from your application code is much easier to understand and less verbose.
You’ve also added support through Mailgun and Coravel to define both HTML and plain text email bodies. Mailgun recommends sending both HTML and plain text together. It’s best to send multipart emails using both text and HTML, or text only. Sending HTML-only email is not well received by ESPs.
Mailgun offers a straightforward HTTP API to send emails from your .NET solutions efficiently and securely. In this article, you learned how to integrate Mailgun’s HTTP API into a C# web application and begin adding production-grade tooling along with Mailgun’s production-ready capabilities.
By using Mailgun and .NET together, your transactional emails can scale efficiently with .NET async/await functionality, the framework’s overall high-performance web infrastructure, additional tooling from the .NET ecosystem, and Mailgun’s performant and straightforward HTTP API.
Was this helpful? Subscribe to our newsletter to get updates on tutorials, emails news, and insights from our resident email geeks.