Extension methods and helpers for System.Net.Mail: a fluent message builder, an EML parser and serializer, message validation and cloning, attachment factories with content-type inference, inline-HTML image embedding, and collection conveniences.
Full API documentation: https://chris-wolfgang.github.io/System.Mail-Extensions/
dotnet add package Wolfgang.Extensions.Mailusing Wolfgang.Extensions.Mail;
using var message = new MailMessageBuilder()
.From("sender@example.com", "Sender Name")
.To("alice@example.com", "bob@example.com")
.Cc("archive@example.com")
.Subject("Monthly Report")
.PlainTextBody("The report is attached.")
.HtmlBody("<h1>Report</h1><p>The report is attached.</p>")
.Attach("report.pdf")
.Build();Build() throws InvalidOperationException if the message has no From address or no recipient, reporting every missing part in one message. Also available: Bcc, ReplyTo, SenderAddress, Priority, DeliveryNotification, BodyEncoding, SubjectEncoding, Header, and Attach overloads for byte arrays and streams.
Parse RFC 2822 / MIME content — including multipart bodies, base64 and quoted-printable transfer encodings, attachments, and RFC 2047 encoded-word headers — into a MailMessage:
using var fromString = EmlParser.Parse(emlContent);
using var fromFile = EmlParser.ParseFile("message.eml");
using var fromFileAsync = await EmlParser.ParseFileAsync("message.eml", cancellationToken);Parsing is deliberately lenient, because real-world EML files routinely contain malformed headers: an address that cannot be parsed is skipped, and a malformed From header leaves message.From null.
To reject malformed input instead of skipping it, parse in strict mode — the parser throws an EmlParseException (a FormatException) on the first malformed construct:
using var message = EmlParser.Parse(emlContent, new EmlParserOptions { Strict = true });
// throws EmlParseException on a malformed address, undecodable body part, or malformed encoded wordTo see what a lenient parse dropped without throwing, use ParseWithDiagnostics, which returns the message alongside a list of skipped constructs:
ParseResult result = EmlParser.ParseWithDiagnostics(emlContent);
if (result.HasIssues)
{
foreach (ValidationIssue issue in result.Issues)
{
Console.WriteLine($"{issue.PropertyName}: {issue.Message}");
}
}
// result.Message is still populated best-effort (well-formed parts are kept)Both ParseFile and ParseFileAsync have matching EmlParserOptions overloads.
string eml = message.ToMimeString();
await File.WriteAllTextAsync("message.eml", eml);Produces a complete RFC 2822 MIME document, round-trippable through EmlParser.Parse.
Trimming / Native AOT:
ToMimeStringis the one API that is not trim- or AOT-safe — it uses reflection to reach the framework's internal MIME writer, so it's annotated[RequiresUnreferencedCode]/[RequiresDynamicCode]and will warn at compile time in a trimmed orPublishAotapp. Everything else in the library is trim/AOT-safe (verified by a Native AOT smoke in CI).
var result = message.Validate(new ValidationOptions
{
RequireSubject = true,
RequireBody = true,
MaxAttachmentSizeBytes = 10 * 1024 * 1024,
MaxTotalAttachmentSizeBytes = 25 * 1024 * 1024
});
if (!result.IsValid)
{
foreach (var issue in result.Errors)
{
Console.WriteLine($"{issue.PropertyName}: {issue.Message}");
}
}ValidationResult exposes IsValid, Errors, Warnings, and AllIssues. Calling Validate() without options reports a missing From address or missing recipients as errors, and an empty subject or body as warnings (options promote those to errors via RequireSubject/RequireBody).
using var clone = original.Clone();
clone.To.Add("extra@example.com"); // original is unchangedCopies addresses, headers, bodies, alternate views, linked resources, and attachments (with independent streams).
var fromBytes = AttachmentFactory.FromBytes(pdfBytes, "report.pdf");
var fromBase64 = AttachmentFactory.FromBase64(base64Content, "photo.jpg");
var fromStream = AttachmentFactory.FromStream(stream, "data.csv");Content types are inferred from the file extension. The registry is extensible:
AttachmentFactory.RegisterContentType(".heic", "image/heic");
string contentType = AttachmentFactory.InferContentType("photo.heic"); // "image/heic"
bool known = AttachmentFactory.TryGetRegisteredContentType(".heic", out var registered);Build an HTML AlternateView with embedded images wired up via Content-IDs — {0}, {1}, … placeholders are filled in the order images are embedded:
var view = new InlineHtmlBuilder()
.Html("<h1>Report</h1><img src='cid:{0}' />")
.EmbedImage("chart.png")
.Build();
message.AlternateViews.Add(view);EmbedImage overloads accept a file path, a byte array, or a stream.
Extension members that backfill MailAddress.TryCreate on frameworks that lack it (on .NET 5+ they delegate to the built-in):
if (MailAddress.TryParse("user@example.com", out var address))
{
Console.WriteLine(address.Address);
}
MailAddress.TryParse("user@example.com", "Display Name", out var withName);message.Attachments.AddRange(attachment1, attachment2); // params, enumerable,
message.Attachments.AddRange("report.pdf", "data.csv"); // file paths, or
message.Attachments.AddRange(filePathList); // enumerable of paths
long totalBytes = message.Attachments.TotalSize();
bool tooBig = message.Attachments.ExceedsLimit(25 * 1024 * 1024);message.To.AddRange("alice@example.com", "bob@example.com");
message.CC.AddRange(mailAddressList);
string formatted = message.To.ToFormattedString();
// "Alice Smith" <alice@example.com>; bob@example.com
string one = new MailAddress("alice@example.com", "Alice Smith").FormatMailAddress();This library targets:
- .NET Framework: 4.6.2
- .NET Standard: 2.0, 2.1
- .NET: 8.0, 9.0, 10.0
See the NuGet package page for the authoritative per-TFM compatibility matrix.
This project is licensed under the MIT License.