Standardized interfaces for common email-sending functionality. The library with this name contains all of the interfaces and types used by the implementations.
This set of libraries facilitates the sending of emails through a standard set of interfaces each implementing the IMailClient interface. Several different implementations are provided:
Before you can start using the email libraries functionality you need to first set up the configuration.
You can add the emailing to your service collection by using the appsettings.json
found in your api project.
By using the IConfiguration
interface you can get the SmtpOptions from the appsettings.json
as seen in the example below.
public static IServiceCollection AddEmailServices(this IServiceCollection services, IConfiguration configuration)
{
if (configuration == null)
{
throw new ArgumentNullException(nameof(configuration));
}
var smtpOptions = configuration.GetSection("SmtpOptions").Get<SmtpOptions>();
var senderAddress = smtpOptions.FromEmailAddress != null
? new EmailSenderDetails(smtpOptions.FromEmailAddress)
: new EmailSenderDetails();
return services
.AddSingleton(smtpOptions)
.AddSingleton(senderAddress)
.AddMailClient(smtpOptions);
}
As you can see below you can pass through empty/null
strings for the UserName
and Password
when using SmtpSettings
if the SMTP server is unauthenticated.
"SmtpOptions": {
"EmailClientType": "Local",
"FromEmailAddress": null,
"UserName": null,
"Password": null,
"Host": null,
"Port": 25,
"EnableSsl": false
}
The above example uses the SmtpOptions
class to store the settings found in the appsettings.json
to public variables.
SmtpSettings
is a class within Audacia.Mail
used to store the core STMP-related properties. SmtpOptions
is a class that extends SmtpSettings
with some more specific properties.
public class SmtpOptions : SmtpSettings
{
public EmailClientType EmailClientType { get; set; }
public string? FromEmailAddress { get; set; }
public bool EnableSsl { get; set; }
}
Whilst the EmailSenderDetails
class keeps hold of the fromAddress from the appsettings.json
.
public class EmailSenderDetails
{
public EmailSenderDetails() =>
Address = new MailAddress();
public EmailSenderDetails(string fromAddress) =>
Address = new MailAddress(fromAddress);
public MailAddress Address { get; }
}
To start using the Audacia mail library you need to set up the email client in your project. As seen in the above example AddEmailServices
the configuration references AddEmailServices
. This method creates a client factory, returning a different IMailClient
based on whichever enum is found in the appsettings.json
file.
private static IServiceCollection AddMailClient(this IServiceCollection services, SmtpOptions smtpOptions)
{
Func<IServiceProvider, IMailClient> mailClientFactory = smtpOptions.EmailClientType switch
{
EmailClientType.None => _ => new NoopMailClient(),
EmailClientType.Local => _ => new DevMailClient(ServerType.Papercut, smtpOptions.Port),
EmailClientType.MailTrap => _ => new MailtrapClient(smtpOptions.UserName, smtpOptions.Password),
_ => _ => new MailKitClient(smtpOptions)
};
return services.AddTransient(mailClientFactory);
}
As mentioned above the below example enum can be used to switch the EmailClientType
. This is an enum that can optionally be added to an application using Audacia.Mail
to make it easier to configure different types. The exact values will depend on what email clients your project needs.
// The type of email client being used, e.g. basic SMTP, local SMTP, MailTrap, etc.
public enum EmailClientType
{
// No SMTP client so no emails will be sent. Can be used for testing.
None,
// Use a general SMTP client.
Smtp,
// Use a local SMTP client.
Local,
// Use the MailTrap SMTP client.
MailTrap,
// Use SendGrid to send emails.
SendGrid
}
Using the standard IMailClient
interface you can create a service that will take in the chosen mail client, as seen above, as well as any information you need.
Below is an example of a password email service which takes in a IMailClient
, IHostingEnvironment
and as seen in the configuration a EmailSenderDetails
. The IMailClient
can be any of the above that inherits the IMailClient
interface.
public class PasswordEmailService : IPasswordEmailService
{
private readonly IMailClient _mailClient;
private readonly EmailSenderDetails _senderDetails;
private readonly IHostingEnvironment _hostingEnvironment;
/// <summary>
/// Initializes a new instance of the <see cref="PasswordEmailService"/> class.
/// </summary>
/// <param name="mailClient">The <see cref="IMailClient"/> that has been setup in the pipeline for actually sending emails.</param>
/// <param name="senderDetails">The details of the person that is sending the email.</param>
/// <param name="hostingEnvironment">The environment that we are currently in.</param>
public PasswordEmailService(
IMailClient mailClient,
EmailSenderDetails senderDetails,
IHostingEnvironment hostingEnvironment)
{
_mailClient = mailClient;
_senderDetails = senderDetails;
_hostingEnvironment = hostingEnvironment;
}
/// <inheritdoc />
public Task SendAsync(
ApplicationUser user,
Uri url,
PasswordMode mode)
{
user = user ?? throw new ArgumentNullException(nameof(user));
url = url ?? throw new ArgumentNullException(nameof(url));
var email = new MailMessage(user.Email)
{
Sender = _senderDetails.Address,
Subject = $"{mode.ToEnumDescriptionString()} your password",
Body = $"{mode.ToEnumDescriptionString()} your password by <a href='{url}'>clicking here</a>.",
Format = MailFormat.Html
};
if (!_hostingEnvironment.IsProduction())
{
email.Subject += $" ({_hostingEnvironment.EnvironmentName})";
}
return _mailClient.SendAsync(email);
}
}
Use case: Ability to test email sending functionality without actually sending an external email out to provider based on http custom header
NoopMailClient
helps us to mimic the email sending functionality without actually sending the email to the external provider. The following example code can help reduce the amount of external calls from this nuget package just by providing a custom header in the API request payload.
First create a MailClientFactory
namespace Audacia.Mail.Test.API;
public class MailClientFactory : IMailClientFactory
{
private readonly SmtpOptions _smtpOptions;
public MailClientFactory(
SmtpOptions smtpOptions
)
{
_smtpOptions = smtpOptions ?? throw new ArgumentNullException(nameof(smtpOptions));
}
public IMailClient CreateMailClient(HttpRequest request)
{
request.TryParseCustomHeaderValueIntoBoolean(
_smtpOptions.DontSendEmailHeaderName ?? string.Empty,
out bool dontSendEmail
);
return dontSendEmail
? new NoopMailClient()
: _smtpOptions.EmailClientType switch
{
EmailClientType.None => new NoopMailClient(),
_ => new MailKitClient(_smtpOptions)
};
}
}
Extract IMailClientFactory
from this class. Using DI add this interface as singleton.
An extension method can be created against HttpRequest
which tryes to extract value from a given custom header name
namespace Audacia.Mail.Test.API.Extensions;
public static class HttpRequestExtension
{
public static bool TryParseCustomHeaderValueIntoBoolean(this HttpRequest request, string headerName, out bool headerValue)
{
if (request.Headers.TryGetValue(headerName, out var headerStringValue))
{
return bool.TryParse(headerStringValue, out headerValue);
}
return headerValue = false;
}
}
Now we can use IMailClientFactory
at controller level
namespace Audacia.Mail.Test.API.Controllers;
[ApiController]
[Route("[controller]")]
public class MailController : Controller
{
private readonly IMailService _mailService;
private readonly IMailClientFactory _mailClientFactory;
public MailController(
IMailService mailService,
IMailClientFactory mailClientFactory
)
{
_mailService = mailService ?? throw new ArgumentNullException(nameof(mailService));
_mailClientFactory = mailClientFactory ?? throw new ArgumentNullException(nameof(mailClientFactory));
}
[HttpPost(Name = "SendMail")]
public async Task<IActionResult> SendMailAsync([FromBody]SendMailRequest sendMailRequest)
{
var mailClient = _mailClientFactory.CreateMailClient(HttpContext.Request);
await _mailService.SendMailAsync(sendMailRequest, mailClient);
return Ok();
}
}
This enables us to interact with multiple IMailClient
implementations at runtime based on custom logic. Full working example can be found under Audacia.Mail.Test.API
project.
Sends email to the local machine to be captured by a locally hosted SMTP server such as Papercut or smtp4dev. This library can automatically install (if needed) and start the specified SMTP server when debugging. Papercut is recommended as it doesn't require any configuration whereas smtp4dev needs authentication disabled in order to work.
This can be easily achieved by using the DevMailClient
found in the Audacia.Mail.Local
. This takes in a ServerType
, a port
(defaulted to 25) and a defaultSender
(defaulted to null). This class overrides the SendAsync
method from the IMailClient
interface. In the example under Setting up an email client you can configure this DevMailClient
by using the libraries enum ServerType
and a port from the smtpOptions
from the configuration.
public DevMailClient(ServerType serverType, int port = 25, string defaultSender = null)
: base(GetSettings(port))
{
ServerType = serverType;
DefaultSender = defaultSender;
if (serverType == ServerType.Smtp4dev)
{
UserName = Guid.NewGuid().ToString();
Password = Guid.NewGuid().ToString();
}
}
The DevMailClient
inherits the MailKitClient
and the GetSettings
method creates a new SmtpSettings
object to send back to the MailKitClient
.
private static SmtpSettings GetSettings(int port)
{
return new SmtpSettings
{
Host = "localhost",
Port = port
};
}
This library uses standard SMTP protocol to send mails, implemented with MailKit. This should be used if you want to use a standard SMTP client.
MailKit uses the Audacia.Mail
SmtpSettings
class to set up the various properties for sending an email. This SmtpSettings
can be configured below and using the appsetting.json
like in the section Configuring The Email Library.
public class SmtpSettings
{
public SmtpSettings();
public string UserName { get; set; }
public string Password { get; set; }
public string Host { get; set; }
public int Port { get; set; }
public string DefaultSender { get; set; }
}
You can set up a MailKitClient
with the above SmtpSettings
.
public MailKitClient(SmtpSettings settings)
{
if (settings == null) throw new ArgumentNullException(nameof(settings));
Host = settings.Host;
Port = settings.Port;
UserName = settings.UserName;
Password = settings.Password;
DefaultSender = settings.DefaultSender;
}
This class has an extra method called Connect
which will connect to a specified Host and Port and ensure it is Authenticated.
public void Connect()
{
if (!_client.IsConnected)
{
_client.Connect(Host, Port, SecureSocketOptions.None);
}
if (!_client.IsAuthenticated && _client.AuthenticationMechanisms.Any())
{
_client.Authenticate(UserName, Password);
}
}
Send mail to the Mailtrap server for testing purposes. Uses the MailKit SMTP implementation.
public MailtrapClient(string username, string password)
: base(GetSettings(username, password, HostType.Test))
{
}
You can also use Mailtrap sever for sending production emails.
public MailtrapClient(string username, string password, HostType hostType)
: base(GetSettings(username, password, hostType))
{
}
usage for production
var mailTrapClient = new MailtrapClient("userName", "password", HostType.Production);
MailtrapClient
also inherits MailKitClient
and similar to the DevMailClient
it creates a new SmtpSettings
object with the private method GetSettings
.
Send mail using the SendGrid API.
When creating a SendGridClient
you need to pass down an apiKey which will be sent to the SendGrid
api to create a sendGridClient.
public SendGridClient(string apiKey)
{
_client = new global::SendGrid.SendGridClient(apiKey);
}
Writes an email to a delegate, to help with testing.
Audacia.Mail
has an example of how to use this to log an email to the console.
/// <summary>
/// Logs an email to the console.
/// </summary>
public class ConsoleLogMailClient : LogMailClient
{
/// <summary>
/// Initializes a new <see cref="ConsoleLogMailClient"/>.
/// </summary>
public ConsoleLogMailClient() : base(System.Console.WriteLine)
{
}
}
This client doesn't do anything with the email that is sent.
Send emails using the Mandrill (Mailchimp) API.
Mandrill is different to the other IMailClient
implementations above as it uses a HttpClient
to send api calls to the api address.
Due to this it has more configuration needed to be able to use it, if you are using Mandrill just for SMTP it is best to use MailKit instead as it needs less configuration to get working.
There is an extension within the Mandrill library called AddMandrillClient
which will add the MandrillClient
and the MandrillService
to your IServiceCollection
to allow for dependency injection.
as seen below.
public static IServiceCollection AddMandrillClient(this IServiceCollection services, MandrillOptions options)
{
return services
.AddSingleton(options)
.AddTransient<IMailClient, MandrillClient>()
.AddHttpClient<IMandrillService, MandrillService>(client =>
{
client.BaseAddress = new Uri("https://mandrillapp.com/api/1.0/");
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
}).Services;
}
Mandrill uses MandrillOptions
instead of the SmtpSettings
to set up various properties for sending a Mandrill API Post
request. Similarly to the SmtpSettings
these can be configured using the
appsetting.json
like in the section Configuring The Email Library.
public class MandrillOptions
{
public string FromEmail { get; set; }
public string FromName { get; set; }
public string ApiKey { get; set; }
public bool Async { get; set; }
}
The Mandrill library also allows you to create template messages using the MandrillTemplate
class as seen below.
public class MandrillTemplate
{
public string Name { get; set; }
public string Content { get; set; }
}
public async Task<bool?> SendTemplateMessageAsync(MailMessage message, string templateName, List<MandrillTemplate> templates = null)
{
var mandrillMessage = new MandrillMailMessage(message);
var messageRequest = templates != null ?
new SendTemplateMessageRequest(_options.ApiKey, mandrillMessage, templates, templateName, _options.Async) :
new SendTemplateMessageRequest(_options.ApiKey, mandrillMessage, templateName, _options.Async);
using (var result = await SendPostRequestAsync($"messages/send-template{_outputFormat}", messageRequest))
{
return result.IsSuccessStatusCode;
}
}
Mandrill also has the ability to use webhooks to receive information about email events as they occur. This uses the MandrillWebhookProvider
class.
public MandrillWebhookProvider(IMandrillService mandrillService, MandrillOptions options)
{
_mandrillService = mandrillService;
_options = options;
}
We welcome contributions! Please feel free to check our Contribution Guidlines for feature requests, issue reporting and guidelines.