Skip to content

Darling: alert email goes through notification routes when the default recipient list is blank (#4751) - #4777

Merged
erikdarlingdata merged 2 commits into
devfrom
fix/4751-route-email-recipients
Sep 29, 2026
Merged

erikdarlingdata merged 2 commits into
devfrom
fix/4751-route-email-recipients

Conversation

@erikdarlingdata

Copy link
Copy Markdown
Owner

Fixes #4751.

Why

In Darling, a notification route can name email recipients as its only destination. That route sent nothing when the SMTP settings had a server and a from address but a blank default recipient list. Two checks required the default list: the send core's "is SMTP configured" gate, and DarlingAlertSettings.SmtpEnabled. So the branch that reads the route's recipients never ran. The route editor and the README say only the server, the from address and the credentials come from the main settings, and the viewer accepts such a route, so the behavior contradicted what the product tells the operator. Lite has no routes and is not affected.

What changes

  • DarlingAlertSettings.SmtpEnabled is now server + from. The default recipient list is no longer part of it. The only readers of the IAlertSettings.SmtpEnabled member outside tests are the send core's gate and the router's default arm (both covered below).
  • NotificationRoute.HasEmailDestination and NotificationRouter.AnyRouteConfiguresEmail(routes) are new, next to their webhook twins.
  • EmailSendCore: the gate is SmtpEnabled + server + from + (default recipients not blank OR an enabled route names recipients). The line after it that folds in the webhook state is unchanged.
  • EmailSendCore: when the recipients resolved for a firing are blank (no covering route, blank default), the send core does not call the SMTP send, which throws on an empty list. It releases the repeat budget, logs at debug, and reports email as NotAttempted. It counts no failure, stamps no cooldown and returns no error, and it keeps the routing record. The existing try/catch is unchanged and now sits in the else branch. SendTestEmailAsync is untouched.
  • The router needed no code change. ResolveChannel's last arm already returns a null destination for a blank default. I added a comment at the call and pinned it with a test.
  • New row shape: an alert that no route covers, on a deployment whose email is set up only through routes (blank default list, no webhooks), now has a configured channel that nothing consulted. AlertDelivery.FromFanout records that as undelivered, the same shape an alert no webhook route covers already produces. EmailFanoutResult.AnyChannelConfigured is true and both channel outcomes are NotAttempted. Lite cannot produce this shape: it has no routes, so its gate reduces to the old expression.
  • Words that were no longer true are corrected: the AlertDelivery.FromFanout remarks and the matching pin's doc comment (the pin's method is renamed Undelivered_IsReachedOnlyByAConfiguredChannelNothingConsulted), the doc comment on Lite's Unreachable predicate, "host + from + to are all set" in Darling/README.md, darling.sample.json and DarlingConfig.cs, the README's route paragraph, and DarlingAlertingTests, which now expects SmtpEnabled once server and from are set.
  • The Darling viewer's Validate message for a blank default list now reads "Default recipients are needed for alerts that no notification route covers." Lite's identical message stays.

Test plan

  • Darling.Tests and Lite.Tests build with 0 warnings and 0 errors.
  • New EmailRouteRecipientsTests (8 tests, 0 skipped): a covering route sends one email whose To header is the route's recipients; an uncovered firing is NotAttempted with no message, no failure count and an undelivered row, and the next firing sends after either a covering route or a default list is added (so no cooldown was stamped); the same with Summary delivery and a last email 20 minutes ago against a 15 minute window is Delivered and not Folded (so the budget was released); no recipients anywhere, or only a disabled route, is still not a configured channel; SmtpEnabled is true with server + from + blank default, and Resolve(...).Email.Destination is null for a blank or whitespace default on the real settings; AnyRouteConfiguresEmail cases.
  • With EmailSendCore.cs and DarlingAlertSettings.cs put back to their dev versions, the new tests fail: 6 failures (5 in the new class, 1 in DarlingAlertingTests). The tests that pin unchanged behavior (no recipients anywhere, AnyRouteConfiguresEmail) pass on both.
  • Mutation checks on the new branch: removing the budget release fails 1 test (the repeat-window one); adding a cooldown stamp fails 3 (both cases of the uncovered-firing test and the repeat-window one).
  • Targeted: EmailRouteRecipientsTests, NotificationRoutingTests, DarlingAlertingTests, AlertDeliveryChannelTests in Darling.Tests: 77 total, 0 failed, 4 skipped (none in the new class). AlertDeliveryChannelTests in Lite.Tests: 14 passed.
  • Full Darling.Tests, run once with no database configured: 17167 total, 0 failed, 1121 skipped, 1 not run.
  • Full Lite.Tests, run once: 5597 total, 0 failed, 0 skipped.
  • Not run: the database-backed live tests (no database configured).
  • Not run: the Darling viewer window itself (only the Validate message text changed).

Worth a second look: a deployment whose email is set up only through routes will now record an undelivered row for every firing no route covers, where the send core previously reported no channel as set up.

CHANGELOG

SECTION: Fixed
ENTRY:

…t recipient list is blank (#4751)

A route that names email recipients as its only destination sent nothing when the SMTP
settings had a host and a from address but no default recipients: the send core's gate and
DarlingAlertSettings.SmtpEnabled both required the default list, so the branch that reads
the route's recipients never ran.

SmtpEnabled is now host + from. The gate asks for a default list OR an enabled route with
recipients. A firing that resolves to no recipients at all (no covering route, blank default)
is not attempted: the repeat budget is released, no cooldown is stamped, no failure is
counted, and SendEmailAsync is not called (it throws on an empty list). The router needed no
change: a blank default already resolves to a null destination.

Such a firing stores an undelivered row, the same shape an alert no webhook route covers
already stores. Words that said host + from + to are all required are corrected in the README,
sample config, DarlingConfig and the delivery-channel doc comments, and the viewer's
Validate message now says what the default list is for.
@erikdarlingdata
erikdarlingdata marked this pull request as ready for review September 29, 2026 09:37
@erikdarlingdata
erikdarlingdata merged commit 17a48cd into dev Sep 29, 2026
15 of 16 checks passed
@erikdarlingdata
erikdarlingdata deleted the fix/4751-route-email-recipients branch September 29, 2026 10:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant