Serilog.Sinks.PostmarkEmail 1.1.0

dotnet add package Serilog.Sinks.PostmarkEmail --version 1.1.0
                    
NuGet\Install-Package Serilog.Sinks.PostmarkEmail -Version 1.1.0
                    
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="Serilog.Sinks.PostmarkEmail" Version="1.1.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Serilog.Sinks.PostmarkEmail" Version="1.1.0" />
                    
Directory.Packages.props
<PackageReference Include="Serilog.Sinks.PostmarkEmail" />
                    
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 Serilog.Sinks.PostmarkEmail --version 1.1.0
                    
#r "nuget: Serilog.Sinks.PostmarkEmail, 1.1.0"
                    
#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 Serilog.Sinks.PostmarkEmail@1.1.0
                    
#: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=Serilog.Sinks.PostmarkEmail&version=1.1.0
                    
Install as a Cake Addin
#tool nuget:?package=Serilog.Sinks.PostmarkEmail&version=1.1.0
                    
Install as a Cake Tool

Serilog.Sinks.PostmarkEmail

A Serilog sink that batches log events and delivers them as email through the Postmark HTTP API.

No dependencies beyond Serilog itself. Targets net10.0, net8.0 and netstandard2.0.

dotnet add package Serilog.Sinks.PostmarkEmail

Quick start

using Serilog;
using Serilog.Events;
using Serilog.Sinks.PostmarkEmail;

Log.Logger = new LoggerConfiguration()
    .MinimumLevel.Debug()
    .WriteTo.PostmarkEmail(
        serverToken: "your-postmark-server-token",
        from: "Application Logs <logs@yourdomain.com>",
        to: "ops@yourdomain.com",
        subject: "[{Level}] {Message}",
        restrictedToMinimumLevel: LogEventLevel.Error)
    .CreateLogger();

Call Log.CloseAndFlush() (or dispose the logger) on shutdown so buffered events are sent rather than dropped.

Configuring from appsettings.json

Every parameter on the flat overload is a string or primitive, so Serilog.Settings.Configuration can bind it directly:

{
  "Serilog": {
    "Using": [ "Serilog.Sinks.PostmarkEmail" ],
    "WriteTo": [
      {
        "Name": "PostmarkEmail",
        "Args": {
          "serverToken": "your-postmark-server-token",
          "from": "Application Logs <logs@yourdomain.com>",
          "to": "ops@yourdomain.com; oncall@yourdomain.com",
          "subject": "[{Level}] {Message}",
          "tag": "app-errors",
          "batchSizeLimit": 50,
          "bufferingTimeLimit": "00:01:00",
          "retryTimeLimit": "00:02:00",
          "httpTimeout": "00:00:30",
          "restrictedToMinimumLevel": "Error"
        }
      }
    ]
  }
}

Keep the token out of source control — supply it through user secrets, an environment variable, or your key vault of choice and let configuration substitution fill it in.

Configuring in code

For anything the flat overload doesn't cover — a shared HttpClient, a custom IFormatProvider — use the options overload:

using var httpClient = httpClientFactory.CreateClient("postmark");

Log.Logger = new LoggerConfiguration()
    .WriteTo.PostmarkEmail(new PostmarkEmailSinkOptions
    {
        ServerToken = configuration["Postmark:ServerToken"],
        From = "logs@yourdomain.com",
        To = "ops@yourdomain.com",
        Cc = "audit@yourdomain.com",
        Subject = "[{Level}] {Message}",
        Tag = "app-errors",
        MessageStream = "outbound",
        BatchSizeLimit = 50,
        BufferingTimeLimit = TimeSpan.FromMinutes(1),
        HttpClient = httpClient
    }, restrictedToMinimumLevel: LogEventLevel.Error)
    .CreateLogger();

Options

Option Default Notes
ServerToken required Postmark server token. See the gotcha below.
From required Must be a verified sender signature or an address on a confirmed domain. Display Name <addr> is accepted.
To required Comma- or semicolon-separated. Postmark accepts at most 50.
Cc, Bcc, ReplyTo null Omitted from the request when unset.
Subject "Log Email" An output template, rendered against the most significant event in the batch.
OutputTemplate "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level}] {Message}{NewLine}{Exception}" Applied to every event to build the body.
IsBodyHtml false Sends the body as HtmlBody instead of TextBody. Nothing is escaped or wrapped, so pair it with an HTML-emitting template.
Tag null Groups these messages in Postmark's statistics.
MessageStream null Postmark defaults to outbound.
TrackOpens null Inherits the server setting when unset.
BatchSizeLimit 100 Events per email.
BufferingTimeLimit 30s How long to wait for a batch to fill.
QueueLimit 10000 Events buffered before new ones are dropped. null for unbounded.
EagerlyEmitFirstEvent false Serilog's own default is true; sending one email for the very first event defeats the point of batching log mail.
RetryTimeLimit null Serilog's default of ten minutes.
HttpClient / MessageHandler null Mutually exclusive, code-only. See below.
ServerUrl https://api.postmarkapp.com/ Override for proxies and tests. Must be an absolute URI.
HttpTimeout 30s Applies only to a sink-owned client.

Everything above except HttpClient and MessageHandler is reachable from appsettings.json.

Behavior worth knowing

The subject is a template. It is rendered against the highest-level event in the batch (earliest one, on a tie), so "[{Level}] {Message}" produces [Error] Order 4021 failed to settle. Because a mail header cannot span lines, runs of whitespace in the rendered subject collapse to single spaces and the result is truncated at 250 characters.

Errors are split by whether a retry could help. Serilog's batching infrastructure retries a batch whose send threw, for up to RetryTimeLimit. That is right for a network blip or a 500, so those throw. A rejected token or a malformed sender address would fail identically for the full ten minutes, so those are written once to SelfLog and the batch is dropped. Postmark also returns a non-zero ErrorCode inside some 200-level responses; those are reported to SelfLog too.

Turn SelfLog on while setting the sink up — it is where every delivery problem surfaces:

Serilog.Debugging.SelfLog.Enable(Console.Error);

HttpClient ownership. Supply MessageHandler and the sink builds a client over it, owns that client, and leaves the handler for you to dispose. Supply HttpClient and the sink borrows it: it never disposes it and never mutates its BaseAddress, Timeout, or default headers, so it is safe to hand over a client from IHttpClientFactory. Requests always use an absolute URI derived from ServerUrl, so a borrowed client's own BaseAddress is ignored. Supply neither and the sink creates and disposes its own.

Postmark gotchas

  • Server token, not account token. The sink sends X-Postmark-Server-Token. An account token manages servers and domains and cannot send email; using one yields a 401.
  • The sender must be verified. Postmark rejects a From that isn't a confirmed sender signature or on a verified domain, with ErrorCode 400. This is a configuration error, so the sink reports it to SelfLog and drops the batch rather than retrying.
  • Message streams. Log email is transactional and belongs on the default outbound stream. Sending it on a broadcast stream adds unsubscribe handling you don't want on an alert.
  • Recipient cap. Fifty addresses per field. Beyond that the sink drops the excess and warns through SelfLog, because Postmark would otherwise reject the whole message.

Building

dotnet build
dotnet test
dotnet pack -c Release

dotnet test runs on Microsoft.Testing.Platform, which global.json opts into. Note that MTP mode does not accept --nologo.

Releasing

The git tag is the source of truth for the published version; it overrides VersionPrefix in the csproj. Publishing is therefore:

git tag v1.0.0
git push origin v1.0.0

That runs .github/workflows/release.yml, which builds, runs the tests, packs with ContinuousIntegrationBuild=true, pushes the .nupkg and .snupkg to nuget.org, and opens a GitHub release with both attached.

Authentication uses trusted publishing: no long-lived API key is stored anywhere. The job mints a GitHub OIDC token, NuGet/login@v1 trades it with nuget.org for a temporary key valid for one hour and redeemable once, and that key is used for the push. Setup is a one-time trusted publishing policy on nuget.org:

Policy field Value
Repository Owner jfgreco
Repository Serilog.Sinks.PostmarkEmail
Workflow File release.yml (filename only, no path)
Environment nuget

The only repository secret is NUGET_USER, the nuget.org account name. That is not a credential — it is a secret purely to keep the account name out of public build logs.

Publishing is not reversible — a version number on nuget.org is permanent and can only be unlisted, never replaced. The workflow runs in a nuget GitHub environment, so adding required reviewers to that environment in repository settings puts a manual approval in front of the push.

Release checklist

  1. master is green and dotnet test passes locally.
  2. Decide the version. Breaking changes to PostmarkEmailSinkOptions or the WriteTo.PostmarkEmail signatures need a major bump; a published version cannot be re-cut.
  3. Tag and push. The tag drives the version — there is nothing to edit in the csproj.
  4. Watch the run. Everything before Push to NuGet is a gate: build, the full test suite, pack, and a check that the packed filename matches the tag. A failure in any of them publishes nothing, so the tag can safely be moved and re-pushed.
  5. Once Push to NuGet has succeeded the version is spent. From that point a mistake means publishing a new version, not re-tagging.
  6. nuget.org takes roughly five minutes to index a new version. A 404 immediately after a successful push is normal.

One-time setup

Already done for this repository; recorded here in case it needs rebuilding.

  • A trusted publishing policy on nuget.org with the fields in the table above, and a glob pattern of Serilog.Sinks.PostmarkEmail under package scoping. A package that does not exist yet cannot be picked from the list, so the glob field is the only way to scope the first release.
  • A NUGET_USER repository secret holding the nuget.org profile name, not the email address.
  • The nuget GitHub environment. The name is load-bearing twice over: it gates the push behind optional required reviewers, and it travels as a claim in the OIDC token that nuget.org validates against the policy. Renaming it means updating the policy too.

License

MIT

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  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 was computed.  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. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos 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
1.1.0 231 8/20/2026
1.0.0 83 8/20/2026