ReconArt.Email.Sender
4.2.1
Prefix Reserved
dotnet add package ReconArt.Email.Sender --version 4.2.1
NuGet\Install-Package ReconArt.Email.Sender -Version 4.2.1
<PackageReference Include="ReconArt.Email.Sender" Version="4.2.1" />
<PackageVersion Include="ReconArt.Email.Sender" Version="4.2.1" />
<PackageReference Include="ReconArt.Email.Sender" />
paket add ReconArt.Email.Sender --version 4.2.1
#r "nuget: ReconArt.Email.Sender, 4.2.1"
#:package ReconArt.Email.Sender@4.2.1
#addin nuget:?package=ReconArt.Email.Sender&version=4.2.1
#tool nuget:?package=ReconArt.Email.Sender&version=4.2.1
ReconArt.Email.Sender
Overview
ReconArt.Email.Sender is a robust .NET library designed for sending emails using the SMTP protocol. It provides a comprehensive suite of features that make it suitable for a wide range of email sending scenarios, from simple email dispatch to basic queuing and health monitoring.
Features
- Targets .NET 8, .NET 9, and .NET 10: Leverages the latest .NET frameworks for optimal performance and compatibility.
- Thread-safe Design: Utilizes a connection pool, ensuring thread safety and efficient resource management by prioritizing "hot" connections.
- Email Sending and Queuing: Capable of sending emails immediately or queuing them for asynchronous dispatch.
- Health Monitoring: Includes a separate service for monitoring the health and liveness of the email sender, ensuring reliability.
- Customizable Options: Offers a comprehensive suite of configuration options to tailor the email sending process to your specific needs.
Installation
To install the ReconArt.Email.Sender package, use the NuGet Package Manager or the Package Manager Console with the following command:
Install-Package ReconArt.Email.Sender
Usage
Standalone Usage
To use the EmailSenderService in a standalone application, you can directly instantiate it with the necessary options and logger configuration. Here's how you can set it up:
using Microsoft.Extensions.Logging;
using ReconArt.Email;
// Configure email sender options
var emailSenderOptions = EmailSenderOptions.CreateBasic(
host: "smtp.example.com",
port: 587,
requiresAuthentication: true,
username: "your-username",
password: "your-password",
// FromAddress is only necessary in the event Username is not an actual email address,
// or no authentication is involved.
fromAddress: "no-reply@example.com");
// Create the email sender service
var emailSenderService = new EmailSenderService(emailSenderOptions, new EmailSenderStartupOptions(), configureLogger: builder =>
{
builder.AddConsole();
});
// Use the email sender service to send an email
var emailMessage = new EmailMessage("recipient@example.com", "Subject", "Body");
await emailSenderService.TrySendAsync(emailMessage);
Standalone Usage with OAuth2
To use OAuth2, create the options in code and provide a callback that returns refreshed token values.
using Microsoft.Extensions.Logging;
using ReconArt.Email;
var emailSenderOptions = EmailSenderOptions.CreateOAuth2(
host: "smtp.example.com",
port: 587,
username: "mailer@example.com",
accessToken: initialToken.AccessToken,
accessTokenExpiresAtUtc: initialToken.ExpiresAtUtc,
refreshAccessTokenAsync: async cancellationToken =>
{
var refreshedToken = await myTokenProvider.RefreshAsync(cancellationToken);
return new EmailSenderOAuthRefreshResult
{
AccessToken = refreshedToken.AccessToken,
RefreshToken = refreshedToken.RefreshToken,
AccessTokenExpiresAtUtc = refreshedToken.ExpiresAtUtc
};
},
onOAuth2CredentialsRefreshed: async (refreshedToken, cancellationToken) =>
{
await myTokenStore.SaveAsync(refreshedToken, cancellationToken);
});
var emailSenderService = new EmailSenderService(emailSenderOptions, new EmailSenderStartupOptions(), configureLogger: builder =>
{
builder.AddConsole();
});
await emailSenderService.TrySendAsync(new EmailMessage("recipient@example.com", "Subject", "Body"));
Integration with ASP.NET Core
To integrate the EmailSenderService with an ASP.NET Core application, you can use the provided extension methods to register it within the dependency injection container. Here's how you can set it up:
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using ReconArt.Email;
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
// Register the email sender service using the extension method
services.AddEmailSenderService(configuration);
// Other service registrations...
}
}
In this setup, the AddEmailSenderService extension method is used to register the EmailSenderService with the ASP.NET Core dependency injection system. This method allows you to optionally load options from a configuration source, such as appsettings.json, and optionally override them with a delegate if needed.
The method can be called without providing any arguments. In such case, an instance of EmailSenderOptions with the default values will be used.
Startup options (EmailSenderStartupOptions - pool size, queue size, and the certificate-validation callback) bind from the EmailSender:Startup configuration section (or <sectionName>:Startup when a custom section name is supplied) and can be overridden with the optional configureStartupOptions delegate on AddEmailSenderService.
For OAuth2 scenarios, prefer creating EmailSenderOptions in code via CreateOAuth2(...), because access tokens and refresh callbacks are typically not a good fit for appsettings-driven configuration.
Dynamic Runtime Configuration
When SMTP settings are supplied at runtime from a database, cache, secret store, or another external source, register an IEmailSenderOptionsProvider. Configuration-bound settings already flow through the framework's IOptionsMonitor<EmailSenderOptions>.
Use IEmailSenderOptionsProvider when fetching options requires custom business logic:
using Microsoft.Extensions.DependencyInjection;
using ReconArt.Email;
// Mail options come from the provider; startup options (pool size, queue size,
// certificate callback) bind from the EmailSender:Startup configuration section.
services.AddEmailSenderService<DatabaseEmailSenderOptionsProvider>(configuration);
Omit the configuration argument when startup options should use their defaults, or pass a configureStartupOptions delegate to set them in code.
The provider returns null while the sender should be treated as unavailable. Sends will fail gracefully until the provider returns a valid EmailSenderOptions.CreateBasic(...) or EmailSenderOptions.CreateOAuth2(...) instance.
public sealed class DatabaseEmailSenderOptionsProvider : IEmailSenderOptionsProvider
{
public async ValueTask<EmailSenderOptions?> GetOptionsAsync(CancellationToken cancellationToken)
{
var settings = await settingsStore.GetCurrentAsync(cancellationToken);
if (settings is null)
{
return null;
}
return EmailSenderOptions.CreateOAuth2(
host: settings.Host,
port: settings.Port,
username: settings.Username,
accessToken: settings.AccessToken,
accessTokenExpiresAtUtc: settings.AccessTokenExpiresAtUtc,
refreshAccessTokenAsync: async token =>
{
var refreshed = await tokenProvider.RefreshAsync(settings.RefreshToken, token);
return new EmailSenderOAuthRefreshResult
{
AccessToken = refreshed.AccessToken,
RefreshToken = refreshed.RefreshToken,
AccessTokenExpiresAtUtc = refreshed.ExpiresAtUtc
};
},
onOAuth2CredentialsRefreshed: async (refreshed, token) =>
{
await settingsStore.SaveTokensAsync(refreshed.AccessToken, refreshed.RefreshToken, refreshed.AccessTokenExpiresAtUtc, token);
});
}
}
IOptionsMonitor<EmailSenderOptions> is the framework's own reloadable-options shape: when options bind from configuration (services.AddEmailSenderService(configuration)), ASP.NET Core builds the monitor and reloads CurrentValue on configuration changes - there is nothing to implement. Do not hand-roll an IOptionsMonitor for a custom source; that is what IEmailSenderOptionsProvider is for. If another component already supplies a monitor implementation, register it directly via services.AddEmailSenderService<TOptionsSource>().
For OAuth2, the sender applies refreshed AccessToken, AccessTokenExpiresAtUtc, and RefreshToken values onto the current options instance. Use OnOAuth2CredentialsRefreshed when the application needs to persist or observe those refreshed credentials. Exceptions thrown by user-provided delegates are logged and do not fail the send operation.
Testing Candidate Settings
Before publishing candidate settings from a setup screen or external provider, call TestConnectionAsync(candidateOptions) with a dedicated candidate EmailSenderOptions instance. The overload validates the supplied options and performs a one-off SMTP connect/authentication probe without fetching runtime options or reusing pooled sender connections. For OAuth2 candidates, the access token and expiration can be omitted or expired - the probe refreshes through the candidate's own callbacks before connecting when needed, and refreshes/retries once if the SMTP server rejects the candidate token.
Refreshed values are applied onto the candidate instance - read and persist them from it. There is one timing rule, and it only matters for providers that rotate one-time-use refresh tokens: persist the token values unconditionally after the call, whether or not the test succeeded. A refresh can succeed - consuming the refresh token in your store - even though the SMTP probe afterwards fails, and gating token persistence on a successful test would strand that rotation, breaking every subsequent refresh with invalid_grant. Only the transport settings (host, port, username) are what a successful test vouches for.
As everywhere else, the refresh delegate should read the current refresh token from its source of truth - the store or token library that owns it, as in the examples above. A candidate test is the one exception: the credentials under test come from user input and are not persisted anywhere yet, so a local variable carries them for the duration of the test.
// The candidate's refresh token has no store row yet - this local is its temporary home,
// kept current across a mid-test rotation by the callback below.
string? currentRefreshToken = token.RefreshToken;
EmailSenderOptions candidateOptions = EmailSenderOptions.CreateOAuth2(
host: settings.Host,
port: settings.Port,
username: settings.Username,
accessToken: token.AccessToken,
accessTokenExpiresAtUtc: token.ExpiresAtUtc,
refreshToken: currentRefreshToken,
refreshAccessTokenAsync: async cancellationToken =>
{
var refreshed = await tokenProvider.RefreshAsync(currentRefreshToken, cancellationToken);
return new EmailSenderOAuthRefreshResult
{
AccessToken = refreshed.AccessToken,
RefreshToken = refreshed.RefreshToken,
AccessTokenExpiresAtUtc = refreshed.ExpiresAtUtc
};
},
onOAuth2CredentialsRefreshed: (refreshed, _) =>
{
currentRefreshToken = refreshed.RefreshToken ?? currentRefreshToken;
return ValueTask.CompletedTask;
});
Exception? connectionError = await emailSenderService.TestConnectionAsync(candidateOptions, cancellationToken);
// Unconditional, and from the candidate instance: a refresh may have rotated the tokens
// even when the test failed, and the store's old refresh token is already consumed.
await settingsStore.SaveTokensAsync(
candidateOptions.AccessToken,
candidateOptions.RefreshToken,
candidateOptions.AccessTokenExpiresAtUtc,
cancellationToken);
if (connectionError is null)
{
// The candidate works: publish its transport settings.
await settingsStore.SaveAsync(
candidateOptions.Host,
candidateOptions.Port,
candidateOptions.Username,
cancellationToken);
}
If the application already persists rotations from OnOAuth2CredentialsRefreshed (as the runtime examples above do), that hook fires during the candidate test too and covers even a crash mid-probe - the unconditional save then becomes a harmless overwrite of the same values.
Health Monitoring
The EmailSenderLivenessService is designed to monitor the health of the email sending process by periodically checking the connection to the SMTP server. It implements Microsoft's BackgroundService, allowing it to run in the background and perform health checks.
Standalone Usage
In a standalone application, you need to start the EmailSenderLivenessService and periodically check the healthiness report using GetSnapshotAsync. Here's how you can set it up:
using Microsoft.Extensions.Logging;
using ReconArt.Email;
using System;
using System.Threading;
using System.Threading.Tasks;
// Configure email sender options
var emailSenderOptions = EmailSenderOptions.CreateBasic(
host: "smtp.example.com",
port: 587,
requiresAuthentication: true,
username: "your-username",
password: "your-password",
fromAddress: "no-reply@example.com");
// Create the email sender service
var emailSenderService = new EmailSenderService(emailSenderOptions, new EmailSenderStartupOptions(), configureLogger: builder =>
{
builder.AddConsole();
});
// Configure email sender liveness options
var livenessOptions = new EmailSenderLivenessOptions
{
LivenessReportResetsMessageCount = true
};
// Create the email sender liveness service
var emailSenderLivenessService = new EmailSenderLivenessService(emailSenderService, livenessOptions, configureLogger: builder =>
{
builder.AddConsole();
});
// Start the liveness service
await emailSenderLivenessService.StartAsync(CancellationToken.None);
// Periodically check the healthiness report by receiving a snapshot of the last
// health monitoring check
while (true)
{
var livenessSnapshot = await emailSenderLivenessService.GetSnapshotAsync();
Console.WriteLine($"Service is alive: {livenessSnapshot.Success}");
await Task.Delay(TimeSpan.FromMinutes(2)); // Check every 2 minutes
}
Internally, the EmailSenderLivenessService tests the connection of the provided IEmailSenderService by invoking its TestConnectionAsync(CancellationToken cancellationToken) method. When you call GetSnapshotAsync(), you receive a snapshot of the most recent health check operation.
To determine if a health check has never been performed, examine the properties of the EmailSenderLivenessSnapshot, particularly Success and TimeInSecondsToNextLivenessCheck. If the snapshot is outdated and due for a refresh, TimeInSecondsToNextLivenessCheck will report 0. Once the background operation completes, a new snapshot with updated properties will be available.
If the connection to the SMTP server fails, the background service will retry the operation after 2 minutes. If successful, it will perform the next check in 10 minutes.
Configuration
Below are the configuration options available for EmailSenderService and EmailSenderLivenessService.
For more detailed insights into what each option does, refer to their XML documentation.
EmailSenderService Configuration Options
| Option | Type | Description | Default Value |
|---|---|---|---|
| Host | string | Host of the mail server. | (Required) |
| Port | int | Port of the mail server. | (Required) |
| AuthenticationType | EmailSenderAuthenticationType | Selects the SMTP authentication flow. Use Basic for traditional SMTP and OAuth2 for token-based auth. |
Basic |
| RequiresAuthentication | bool | Applies only to AuthenticationType = Basic. When true, uses Username and Password for SMTP basic auth; when false, only connects. |
true |
| Username | string? | Username to authenticate as for the mail server. Required for basic-authenticated SMTP and OAuth2. | null |
| FromAddress | string? | Email address to send emails from. If null, Username will be used when it is a valid email address. |
null |
| Password | string? | Password to authenticate as for the mail server. Used only for AuthenticationType = Basic when RequiresAuthentication = true. |
null |
| AccessToken | string? | Optional initial OAuth2 access token. When omitted, one is obtained via RefreshAccessTokenAsync before the first send. |
null |
| RefreshToken | string? | OAuth2 refresh token used by upstream refresh callbacks, when applicable. Not used directly for SMTP authentication. | null |
| AccessTokenExpiresAtUtc | DateTime | Optional UTC expiration of the OAuth2 access token. The default means the expiry is unknown - the token is used until the server rejects it; supplying it enables proactive refresh. | 0001-01-01 |
| RefreshAccessTokenAsync | Func<CancellationToken, ValueTask<EmailSenderOAuthRefreshResult>>? | Callback that returns refreshed OAuth2 token values. Required when AuthenticationType = OAuth2. |
null |
| OnOAuth2CredentialsRefreshed | Func<EmailSenderOAuthRefreshResult, CancellationToken, ValueTask>? | Optional callback invoked after refreshed OAuth2 credentials are applied to the current options instance. Exceptions are logged and ignored. | null |
| RetryCount | int | Number of times to retry sending an email before giving up. | 3 |
| RetryDelayInMilliseconds | int | Approximate wait time before retrying to send an email. Uses a jitter formula for delay calculation. | 2000 |
| TreatEmptyRecipientsAsSuccess | bool | Set to true to treat emails with no recipients as successfully sent. |
false |
| EnableTempMailRouting | bool | Allows using some_email+N@somedomain.com for routing to some_email@somedomain.com. Useful for testing. |
false |
| Whitelist | string[] | Collection of email addresses allowed to receive emails. If empty, no filtering is applied. | [] |
| AllowUnquotedCommasInAddresses | bool | Set to true to allow unquoted commas in email addresses. |
true |
| AllowAddressesWithoutDomain | bool | Set to true to allow parsing addresses without a domain. |
true |
| UseStrictAddressParser | bool | Set to true to use a stricter RFC-822 address parser. |
false |
| SignalFailureOnInvalidParameters | bool | Set to true to signal a failure when invalid parameters are detected. |
false |
| VerifyInlineAttachments | bool | Set to true to verify inline attachments exist in the email body. |
true |
| OnEmailSendingFailure | Func<IEmailMessage, EmailFailureReason, ValueTask>? | Called when there's a failure sending an email to the SMTP server. | null |
OnEmailSendingFailure will not be invoked if cancellation is requested. Additionally, unless SignalFailureOnInvalidParameters is set to true, it will not be called for failures during the construction of the MIME message. These failures can be inspected through the return values of IEmailSenderService.TrySendAsync and IEmailSenderService.TryScheduleAsync.
EmailSenderStartupOptions Configuration Options
These options are fixed at construction. In ASP.NET Core they bind from the EmailSender:Startup configuration section (or <sectionName>:Startup when a custom section name is used) and can be overridden with the configureStartupOptions delegate on AddEmailSenderService.
| Option | Type | Description | Default Value |
|---|---|---|---|
| MaxConcurrentConnections | int | Maximum number of concurrent SMTP connections maintained in the pool, and the maximum number of messages processed in parallel. Higher values improve throughput under load but consume more resources and may be limited by the mail server. | 3 |
| MessageQueueSize | int | Number of messages that can be queued before back-pressure applies. Set to -1 for unlimited. When capacity is reached, calls to TryScheduleAsync await asynchronously until capacity is available. |
10,000 |
| ServerCertificateValidationCallback | RemoteCertificateValidationCallback? | Callback to validate the server certificate. If no value is specified, the default validation will be used. | null |
EmailSenderLivenessService Configuration Options
| Option | Type | Description | Default Value |
|---|---|---|---|
| LivenessReportResetsMessageCount | bool | Set to true to reset the count of unsuccessfully sent email messages when a liveness check is performed. |
true |
ASP.NET Identity Support
There's a separate package ReconArt.Email.Sender.Identity which allows integrating the IEmailSenderService with ASP.NET Identity's infrastructure.
You can read more about it here.
Contributing
If you'd like to contribute to the project, please reach out to the ReconArt/email-sdk team.
Support
If you encounter any issues or require assistance, please file an issue in the GitHub Issues section of the repository.
Authors and Acknowledgments
Developed by ReconArt, Inc..
Special thanks to the contributors of the MailKit and MimeKit libraries for providing the underlying implementations for communicating and interacting with an SMTP server.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- MailKit (>= 4.17.0)
- Microsoft.Extensions.Hosting (>= 10.0.10)
- Microsoft.Extensions.Options.ConfigurationExtensions (>= 10.0.10)
- Microsoft.Extensions.Options.DataAnnotations (>= 10.0.10)
- MimeKit (>= 4.17.0)
- Polly.Contrib.WaitAndRetry (>= 1.1.1)
- ReconArt.Email.Sender.Abstractions (>= 4.0.0)
-
net8.0
- MailKit (>= 4.17.0)
- Microsoft.Extensions.Hosting (>= 10.0.10)
- Microsoft.Extensions.Options.ConfigurationExtensions (>= 10.0.10)
- Microsoft.Extensions.Options.DataAnnotations (>= 10.0.10)
- MimeKit (>= 4.17.0)
- Polly.Contrib.WaitAndRetry (>= 1.1.1)
- ReconArt.Email.Sender.Abstractions (>= 4.0.0)
- System.Text.Json (>= 10.0.10)
-
net9.0
- MailKit (>= 4.17.0)
- Microsoft.Extensions.Hosting (>= 10.0.10)
- Microsoft.Extensions.Options.ConfigurationExtensions (>= 10.0.10)
- Microsoft.Extensions.Options.DataAnnotations (>= 10.0.10)
- MimeKit (>= 4.17.0)
- Polly.Contrib.WaitAndRetry (>= 1.1.1)
- ReconArt.Email.Sender.Abstractions (>= 4.0.0)
- System.Text.Json (>= 10.0.10)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on ReconArt.Email.Sender:
| Package | Downloads |
|---|---|
|
ReconArt.Email.Sender.Identity
Extends the default implementation with support for the ASP.NET Core Identity infrastructure. |
GitHub repositories
This package is not used by any popular GitHub repositories.