Dev Life

Sending email using C# and the Mailgun API

Master transactional email sending with Mailgun's HTTP API in C#. This guide walks you through setting up a Mailgun account, integrating with .NET, and handling errors for production-ready email delivery.
Image for Sending email using C# and the Mailgun API
December 11, 2024

.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.

u003ca href=’/’u003eMailgunu003c/au003e is a transactional email service that can power your .NET applications to send production-ready emails. You can choose to use either Mailgunu0026#x27;s u003ca href=’/features/smtp-server/’u003eSMTP serviceu003c/au003e or its u003ca href=’/features/email-api/’u003eHTTP APIu003c/au003e for higher-performance transactional email delivery.

How to send email using the Mailgun API

Before you begin this tutorial, you’ll need the following prerequisites:

  • Basic knowledge of C# and .NET
  • Any modern IDE like Visual Studio, JetBrains Rider, or Visual Studio Code

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.

.NET is a cross-platform framework, so you can install it on Windows, Mac, and Linux.If you have trouble creating an API key, you can follow u003ca href=’https://help.mailgun.com/hc/en-us/articles/203380100-Where-can-I-find-my-API-keys-and-SMTP-credentials’u003ethese instructionsu003c/au003e.

Set up your Mailgun account

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.

Create a new C# project

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:

                            

                                {rn    u0022Loggingu0022: {rn        u0022LogLevelu0022: {rn            u0022Defaultu0022: u0022Informationu0022,rn            u0022Microsoft.AspNetCoreu0022: u0022Warningu0022rn        }rn    },rn    u0022AllowedHostsu0022: u0022*u0022,rn    u0022Mailgunu0022: {rn        u0022ApiKeyu0022: u0022u003cYour API Keyu003eu0022,rn        u0022Domainu0022: u0022u003cYour sandbox domainu003eu0022rn    }rn}
                            
                        

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;rnusing System.Text;rnrnvar builder = WebApplication.CreateBuilder(args);rnrn// Add services to the containerrnbuilder.Services.AddControllersWithViews();rnrn// Set up your named HttpClient for easy reusernbuilder.Services.AddHttpClient(u0022Mailgunu0022, client =u003ern{rn    // Grab values from the configurationrn    var apiKey = builder.Configuration.GetValueu003cstringu003e(u0022Mailgun:ApiKeyu0022);rn    var base64Auth = Convert.ToBase64String(Encoding.ASCII.GetBytes($u0022api:{apiKey}u0022));rn    var domain = builder.Configuration.GetValueu003cstringu003e(u0022Mailgun:Domainu0022);rnrn    // Set default values on the HttpClientrn    client.BaseAddress = new Uri($u0022https://api.mailgun.net/v3/{domain}/messagesu0022);rn    client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(u0022Basicu0022, base64Auth);rn});rnrnvar app = builder.Build();rn
                            
                        

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.

More about Mailgun API endpoints

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:

  • Send email: POST /v3/{domain_name}/messages
  • Send email in MIME format: POST /v3/{domain_name}/messages.mime
  • Retrieve a stored email: GET /v3/domains/{domain_name}/messages/{storage_key}
  • View your domains: GET /v4/domains
  • Create a new domain: POST /v4/domains
  • View a paginated list of all inbound and outbound events: GET /v3/{domain_name}/events
  • View a paginated list of all bounces: GET /v3/{domainID}/bounces
You can view the other available endpoints by visiting u003ca href=’https://documentation.mailgun.com/docs/mailgun/api-reference/openapi-final/tag/Messages/’u003eMailgunu0026#x27;s API reference documentationu003c/au003e.

Send your first email with the Mailgun API

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:

Overview Screen

Next, create a new file at /Controllers/OrderController.cs and replace the contents of the file with the following:

                            

                                using System.Diagnostics;rnusing Microsoft.AspNetCore.Mvc;rnusing MailDemo.Models;rnusing System.Text;rnusing System.Net.Mime;rnrnnamespace MailDemo.Controllers;rnrnpublic class OrderController : Controllerrn{rn    private readonly HttpClient _httpClient;rn    private readonly IConfiguration _config;rnrn    public OrderController(IHttpClientFactory httpClientFactory, IConfiguration config)rn    {rn        // Get your named HttpClient that is preconfiguredrn        this._httpClient = httpClientFactory.CreateClient(u0022Mailgunu0022);rn        this._config = config;rn    }rnrn    public async Tasku003cIActionResultu003e Confirm()rn    {rn        using MultipartFormDataContent form = new();rnrn        // Local function keeps this code a bit clearerrn        void SetFormParam(string key, string value) =u003ern        form.Add(new StringContent(value, Encoding.UTF8, MediaTypeNames.Text.Plain), key);rnrn        SetFormParam(u0022fromu0022, $u0022Test User u003cpostmaster@{this._config.GetValueu003cstringu003e(u0022Mailgun:Domainu0022)}u003eu0022);rn        SetFormParam(u0022tou0022, u0022your_authorized_recipient.comu0022);rn        SetFormParam(u0022subjectu0022, u0022Hello World!u0022);rn        SetFormParam(u0022textu0022, u0022My first transactional email!u0022);rn        SetFormParam(u0022htmlu0022, @u0022u003chtmlu003eu003cbodyu003eu003cp style=u0022u0022color:blue;u0022u0022u003eMy first transactional email!u003c/pu003eu003c/bodyu003eu003c/htmlu003eu0022);rnrn        var result = await this._httpClient.PostAsync(string.Empty, form);rnrn        if (!result.IsSuccessStatusCode)rn        {rn            return new JsonResult(await result.Content.ReadAsStringAsync());rn        }rnrn        return new JsonResult(@u0022Your order was confirmed. You should get an email soon!u0022);rn    }rn}rn
                            
                        

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:

Hello World Screen Shot
The email might get sent to your spam folder because you havenu0026#x27;t registered the domain yet. Thatu0026#x27;s okay for now.

Handle responses and errors

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:

  • 400: Something with the formatting of the request is wrong.
  • 401: Authentication failed.
  • 429: You’ve reached Mailgun’s rate limit.
  • 500: Something on the network or Mailgun’s end went wrong.

Let’s take a look at how you can handle some of these failure scenarios.

Handle 400 error codes

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:

  • Throw an exception, as this is a failure scenario that’s not recoverable.
  • Log the exception and the reasons for the exception.

Here’s what the code for this could look like:

                            

                                if (!result.IsSuccessStatusCode)rn{rn    var status = (int) result.StatusCode;rnrn    if (status == 400)rn    {rn        var responseContent = await result.Content.ReadAsStringAsync();rn        var exception = new BadHttpRequestException(u0022Mailgun 400 HTTP status codeu0022);rnrn        this._logger.LogError(exception, exception.Message, responseContent);rn        throw exception;rn    }rn}rn
                            
                        

You may also want to take a similar approach with the 401 HTTP status code.

Handle 500 error codes

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:

  • Queue the email to be attempted again later.
  • In the same code path, retry after a short period.
  • Employ an advanced technique like a circuit breaker

Here’s what a basic retry approach might look like:

                            

                                if (!result.IsSuccessStatusCode)rn{rn    var status = (int) result.StatusCode;rnrn    if (status == 500)rn    {rn        // Retry the email after 1 second in hopes that the error was transientrnrn        await Task.Delay(1000);rn        result = await _httpClient.PostAsync(string.Empty, form);rnrn        if (!result.IsSuccessStatusCode)rn        {rn            // Log and throw an exception like you did in the previous examplern        }rn    }rn}rn
                            
                        

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)rn{rn    var status = (int)result.StatusCode;rnrn    if (status == 400)rn    {rn        // Log the error and notify the development team that thern        // request was not formatted properly.rn    }rn    else if (status == 401)rn    {rn        // Log the error and notify the development team that thern        // request's authentication failed.rn    }rn    else if (status == 429)rn    {rn        // Use a .NET library like Polly to throttle or try this requestrn        // again with an exponential back-off, etc.rn    }rn    elsern    {rn        // Something went wrong: log the error and apply a distributed rn        // error handling technique like a circuit breaker.rn        // See https://learn.microsoft.com/en-us/azure/architecture/patterns/circuit-breaker for more.rn    }rn}rn
                            
                        

Production-ready email integration

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.

Secure your API key

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.

Other production-grade features

Mailgun also supports many production-grade features you may need, such as:

Integrate with the .NET ecosystem

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:

                            

                                u0022Coravelu0022: {rn    u0022Mailu0022: {rn        u0022Fromu0022: {rn            u0022Nameu0022: u0022Test Useru0022,rn            u0022Emailu0022: u0022postmaster@u003cYour sandbox domainu003eu0022rn        }rn    }rn}rnNext, 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:rnusing System.Net.Mime;rnusing System.Text;rnusing Coravel.Mailer.Mail;rnrnnamespace MailDemo;rnrnpublic class MailgunMailer : ICanSendMailrn{rn    private readonly HttpClient _httpClient;rn    private readonly IConfiguration _config;rnrn    public MailgunMailer(IHttpClientFactory httpClientFactory, IConfiguration config)rn    {rn        this._httpClient = httpClientFactory.CreateClient(u0022Mailgunu0022);rn        this._config = config;rn    }rnrn    public async Task SendAsync(MessageBody message, string subject, IEnumerableu003cMailRecipientu003e to, MailRecipient from, MailRecipient replyTo, IEnumerableu003cMailRecipientu003e cc, IEnumerableu003cMailRecipientu003e bcc, IEnumerableu003cAttachmentu003e? attachments = null, MailRecipient? sender = null)rn    {rn        using MultipartFormDataContent form = new();rnrn        void SetFormParam(string key, string value) =u003ern        form.Add(new StringContent(value, Encoding.UTF8, MediaTypeNames.Text.Plain), key);rnrn        if (from is not null)rn        {rn            SetFormParam(u0022fromu0022, $u0022{from.Name} u003c{from.Email}u003eu0022);rn        }rn        elsern        {rn            // This gives you a default u0022fromu0022 field if not defined by the callerrn            SetFormParam(u0022fromu0022, $u0022{this._config.GetValueu003cstringu003e(u0022Coravel:Mail:From:Nameu0022)} u003c{this._config.GetValueu003cstringu003e(u0022Coravel:Mail:From:Emailu0022)}u003eu0022);rn        }rnrn        foreach (var recipient in to)rn        {rn            SetFormParam(u0022tou0022, recipient.Email);rn        }rn        SetFormParam(u0022subjectu0022, subject);rnrn        if (message.HasHtmlMessage())rn        {rn            SetFormParam(u0022htmlu0022, message.Html);rn        }rnrn        if (message.HasPlainTextMessage())rn        {rn            SetFormParam(u0022textu0022, message.Text);rn        }rnrn        var result = await _httpClient.PostAsync(string.Empty, form);rnrn        // Error handling logic...rn    }rn}rn
                            
                        

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.AddScopedu003cMailDemo.MailgunMaileru003e();rnbuilder.AddCustomMaileru003cMailDemo.MailgunMaileru003e();rnFinally, replace the code for OrderController with the following:rnusing Microsoft.AspNetCore.Mvc;rnusing Coravel.Mailer.Mail.Interfaces;rnusing Coravel.Mailer.Mail;rnrnnamespace MailDemo.Controllers;rnrnpublic class OrderController : Controllerrn{rn    private readonly IMailer _mailer;rnrn    public OrderController(IMailer mailer)rn    {rn        this._mailer = mailer;rn    }rnrn    public async Tasku003cIActionResultu003e Confirm()rn    {rn        var mailable = Mailable.AsInline()rn            .To(u0022jamesmichaelhickey@gmail.comu0022)rn            .Subject(u0022Hello World!u0022)rn            .Html(@u0022u003chtmlu003eu003cbodyu003eu003cp style=u0022u0022color:blue;u0022u0022u003eMy first transactional email!u003c/pu003eu003c/bodyu003eu003c/htmlu003eu0022)rn            .Text(u0022My first transactional email!u0022);rnrn        await _mailer.SendAsync(mailable);rnrn        return new JsonResult(@u0022Your order was confirmed. You should get an email soon!u0022);rn    }rn}rn
                            
                        

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 and Coravel also support other features like CC, BCC, and attachments for more production-grade transactional emails.

Wrapping up

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.