Carrigan.Emailer 0.1.3

dotnet add package Carrigan.Emailer --version 0.1.3
                    
NuGet\Install-Package Carrigan.Emailer -Version 0.1.3
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Carrigan.Emailer" Version="0.1.3" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Carrigan.Emailer" Version="0.1.3" />
                    
Directory.Packages.props
<PackageReference Include="Carrigan.Emailer" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Carrigan.Emailer --version 0.1.3
                    
#r "nuget: Carrigan.Emailer, 0.1.3"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Carrigan.Emailer@0.1.3
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Carrigan.Emailer&version=0.1.3
                    
Install as a Cake Addin
#tool nuget:?package=Carrigan.Emailer&version=0.1.3
                    
Install as a Cake Tool

Carrigan.Emailer

Carrigan.Emailer is a configuration-driven email sender for .NET that supports SMTP delivery through MailKit and an optional pickup-directory mode that writes .eml files to disk.

  • Current package targets: net9.0 and net10.0
  • License: Apache-2.0

Important notice

Carrigan.Emailer is a delivery library. It helps applications send email, but it does not by itself make an application compliant with anti-spam laws, marketing-email rules, privacy laws, retention rules, consent requirements, unsubscribe requirements, or provider policies.

Application developers remain responsible for ensuring that their use of this library complies with all applicable legal, regulatory, contractual, provider, and organizational requirements.

Examples include, without limitation:

  • obtaining any required recipient consent
  • honoring unsubscribe or opt-out requests where required
  • providing any required sender identification, notices, or disclosures
  • complying with retention, deletion, privacy, and security obligations
  • following the terms and technical requirements of the selected email provider

What it does

  • SMTP network delivery using MailKit.Net.Smtp.SmtpClient
  • Pickup-directory delivery that writes .eml files to disk
  • Domain-based SMTP endpoint mapping through EmailDomainSmtpEndpoint
  • Default sender support through IEmailConfiguration.DefaultAccount
  • Optional Cc, Bcc, and Reply-To
  • Optional List-Unsubscribe header support
  • Microsoft.Extensions.Logging integration through ILogger<Emailer>
  • Structured send results through EmailerResults

Install

dotnet add package Carrigan.Emailer

Configuration

Carrigan.Emailer uses a static configuration store internally. Call:

Emailer.ValidateConfiguration(configuration);

once during startup before sending email.

Implement IEmailConfiguration

using Carrigan.Emailer;

public sealed class MyEmailConfiguration : IEmailConfiguration
{
    public EmailAddress? DefaultAccount { get; set; } = new("no-reply@example.com", "My App");

    public IEnumerable<EmailDomainSmtpEndpoint> EmailDomainSmtpEndpoints { get; set; } =
    [
        new EmailDomainSmtpEndpoint
        {
            EmailDomain = "example.com",
            SmtpServer = "smtp.example.com",
            SmtpPort = 587
        }
    ];

    public SecurityEnum? SecurityOption { get; set; } = SecurityEnum.Tls;

    public string? PickupDirectory { get; set; }

    public bool UsePickupDirectory { get; set; }

    public bool? UseNetworkDelivery { get; set; } = true;

    public string? GetPasswordForAccount(string accountAddress)
    {
        return accountAddress switch
        {
            "no-reply@example.com" => Environment.GetEnvironmentVariable("EMAIL_NO_REPLY_PASSWORD"),
            _ => null
        };
    }
}

Example startup configuration

using Carrigan.Emailer;

MyEmailConfiguration config = new()
{
    DefaultAccount = new EmailAddress("no-reply@example.com", "My App"),

    EmailDomainSmtpEndpoints =
    [
        new EmailDomainSmtpEndpoint
        {
            EmailDomain = "example.com",
            SmtpServer = "smtp.example.com",
            SmtpPort = 587
        }
    ],

    SecurityOption = SecurityEnum.Tls,
    UseNetworkDelivery = true,
    UsePickupDirectory = false,
    PickupDirectory = "C:\\maildrop"
};

Emailer.ValidateConfiguration(config);

What validation enforces

Emailer.ValidateConfiguration(...) will throw if, among other checks:

  • DefaultAccount is missing or invalid
  • UseNetworkDelivery is null
  • neither network nor pickup delivery is enabled
  • SecurityOption is null or invalid when network delivery is enabled
  • GetPasswordForAccount(DefaultAccount.Address) returns null or empty when network delivery is enabled
  • no matching EmailDomainSmtpEndpoint exists for DefaultAccount.Host when network delivery is enabled
  • pickup-directory mode is enabled but PickupDirectory is invalid or unusable

Sending email

using Carrigan.Emailer;
using Microsoft.Extensions.Logging;

ILoggerFactory loggerFactory = LoggerFactory.Create(builder => builder.AddConsole());
ILogger<Emailer> logger = loggerFactory.CreateLogger<Emailer>();

Emailer emailer = new(logger);

EmailMessage message = new(
    id: Guid.NewGuid(),
    toAddress: "recipient@example.com",
    subject: "Hello from Carrigan.Emailer",
    htmlBody: "<p>Hello!</p>",
    textBody: "Hello!"
);

EmailerResults results = await emailer.SendEmailsAsync(message);

if (results.Errors.Any())
{
    throw new AggregateException("One or more emails failed to send.", results.Errors);
}

About From

  • If EmailMessage.From is null, the library uses configuration.DefaultAccount.

About message bodies

  • EmailMessage requires at least one body.
  • You may provide HTML, plain text, or both.

Pickup directory mode

Enable pickup-directory output in configuration:

config.UsePickupDirectory = true;
config.PickupDirectory = "C:\\maildrop";

Then call the same send method:

EmailerResults results = await emailer.SendEmailsAsync(message);

Pickup output structure:

  • {PickupDirectory}/{recipientFolderName}/inbox/{timestamp}_{guid}.eml
  • {PickupDirectory}/{senderFolderName}/sent/{timestamp}_{guid}.eml

Folder names are derived from email addresses and sanitized for use as paths.

Note: pickup-directory mode is primarily intended for development, testing, and local inspection scenarios. The library writes message files, but it does not manage retention, cleanup, archival, or directory security for you.


List-Unsubscribe header

If EmailMessage.UnsubscribeOptions contains one or more values, the library adds a List-Unsubscribe header.

Example:

EmailMessage newsletter = new(
    Guid.NewGuid(),
    new EmailAddress("recipient@example.com"),
    "Newsletter",
    "<p>...</p>",
    null,
    unsubscribeOptions: new[]
    {
        "mailto:unsubscribe@example.com?subject=unsubscribe",
        "https://example.com/unsubscribe?id=123"
    }
);

await emailer.SendEmailsAsync(newsletter);

Convenience helper

For applications that use a simple HTML-email flow, Emailer also provides:

await emailer.SendEmailAsync(
    toEmailAddress: "recipient@example.com",
    subject: "Confirm your account",
    htmlMessage: "<p>Click the link...</p>"
);

If sending fails, it throws an AggregateException containing the underlying errors.


Results and error handling

SendEmailsAsync(...) returns EmailerResults, which exposes:

  • SuccessfulEmailIds
  • PermanentlyFailedEmailIds
  • PermanentlyFailedRecipients
  • OtherErrors
  • HostErrors
  • AccountErrors
  • EmailErrors
  • Errors
  • EmailIdsToDelete

Current repository status

The repository includes a test project, but it is currently a scaffold for future unit tests rather than a mature coverage suite.

Support Development

If this package helps you, consider supporting development:

https://github.com/sponsors/CarriganSoftwareSolutions

Product Compatible and additional computed target framework versions.
.NET 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.1.3 41 7/25/2026
0.1.2 190 6/11/2026
0.1.2-alpha.202606110104 61 6/11/2026
0.1.2-alpha.202606030851 52 6/3/2026
0.1.1 108 6/3/2026
0.1.0 62 6/3/2026
0.1.0-alpha.202605270630 55 5/27/2026
0.1.0-alpha.202605270622 63 5/27/2026
0.1.0-alpha.202605030715 65 5/3/2026
0.1.0-alpha.202604232245 81 4/23/2026
0.1.0-alpha.202604232037 74 4/23/2026
0.1.0-alpha.202604231630 65 4/23/2026
0.1.0-alpha.202604231535 76 4/23/2026
0.1.0-alpha.202604191211 91 4/19/2026
0.1.0-alpha.202604140326 74 4/14/2026
0.1.0-alpha.202604042201 88 4/4/2026
0.1.0-alpha.202604042200 64 4/4/2026
0.1.0-alpha.202603221003 78 3/22/2026
0.1.0-alpha.202603180610 71 3/18/2026
0.1.0-alpha.202603130123 69 3/13/2026
Loading failed