Skip to main content

Email Delivery

The Email Delivery tab of the System page (the dispatch outbox) is a per-message delivery audit log for all outbound notifications, currently email. Every message Tagnet sends (user invitations, password reset links, admin password-reset notifications) creates a row in the outbox before any provider call is attempted. The row tracks the full delivery lifecycle: queued, in flight, sent, retried, or dead-lettered.

Accessing the page​

Open the Email Delivery tab of the System page (/system/email-delivery). The page requires the Manage Policies permission, granted to the Admin role by default. Until Tagnet has sent its first email, the grid shows a No email deliveries yet message.

Row lifecycle​

Each outbox row moves through the following states:

StatusColorWhat it means
PendingBlueThe row is waiting to be picked up by the consumer, or is waiting for its next retry window after a transient failure.
SendingBlueA consumer has claimed the row and the provider call is in flight.
SentGreenThe provider accepted the message. Terminal.
FailedRedThe provider returned a permanent, non-retryable failure (for example, a rejected recipient address). Terminal.
DeadLetteredRedThe retry budget was exhausted without a successful delivery. Terminal. The row stays in the outbox until retention removes it, and can be manually re-queued by an administrator.

The normal path is Pending → Sending → Sent. Transient provider failures (network timeout, rate limit, temporary server error) bounce the row back to Pending with the next retry time scheduled according to the dispatch policy. After the retry budget is exhausted, the row moves to DeadLettered.

Pending and Sending rows are never deleted by the retention policy; only terminal rows (Sent, Failed, DeadLettered) age out.

Filtering the grid​

Use the filter bar to narrow the list:

  • Search recipient: free-text search against the recipient address.
  • Status: filter to a specific lifecycle state.
  • Message Type: exact match on the message type (e.g., Invitation, PasswordReset, TestEmail). The message type identifies what triggered the dispatch.
  • Created Date: date range filter on the row creation timestamp.

Active filters appear as chips above the grid. Close a chip to remove that filter, or click Clear All to reset everything.

Row details​

Click any row to open the row's own detail page. Shows:

  • Status, provider, and attempt count: how many times delivery was attempted and which provider handled it.
  • Recipient and message type: who the message was for and what kind of message it was.
  • Last error: the sanitized error message from the last failed attempt (when applicable). Full provider error detail is in the application logs, correlated by the row's correlation ID.
  • Payload preview: the template name and variables used to render the message. The rendered body is not stored; it is re-rendered from the template at inspection time, so you can see exactly what the recipient would have received without storing PII-heavy email bodies in the database.
  • Timestamps: created, last attempted, and sent.

Row actions​

Retry​

Available for rows in the Failed or DeadLettered state. Clicking the retry icon on the list (or the Retry header action on the row's detail page) re-queues the row immediately. It returns to Pending with AttemptCount reset and picks up on the next consumer cycle.

Use retry after you have resolved the underlying cause (for example, corrected the SMTP credentials or verified the From address). Retrying without fixing the root cause will exhaust the budget again and dead-letter the row a second time.

Dead-letter​

Available for rows in the Pending or Sending state. Clicking the block icon on the list (or the Dead-letter header action on the row's detail page) immediately moves the row to DeadLettered and stops the retry loop. This is useful when you know a message cannot be delivered and you want to stop the consumer from continuing to attempt it (for example, an invitation to an address that no longer exists).

The action requires confirmation before it takes effect.

Dispatch policy​

Retry behavior is controlled by the Dispatch / Email policy on the Policies tab of the System page. The policy is "always on": it cannot be disabled, only tuned. Settings include:

SettingDescription
Max attemptsTotal attempts (including the first) before the row is dead-lettered. Setting this to 1 disables retries.
BackoffHow the wait time between retries grows: Exponential (doubles each attempt) or Linear (constant wait).
Initial delayWait time before the first retry. Anchors the backoff curve.
Max delayCap on the computed delay so exponential backoff doesn't extend into next-day territory.
Send timeoutHard upper bound on a single provider call. Timeouts count as retryable failures.

For full details on editing the dispatch policy, see Policies.

Retention​

Outbox rows are cleaned up by the Retention / DispatchOutbox policy, which runs on the same nightly schedule as other retention jobs. The policy has two independent retention windows:

  • Sent retention: how long to keep successfully delivered rows (default 30 days). Sent rows are routine audit.
  • Failed / dead-lettered retention: how long to keep failed and dead-lettered rows (default 90 days). These rows are what you reach for during incident triage, so they get a longer window.

Pending and Sending rows are live state and are never deleted by retention.

For storage planning details, see Policies.

Recovery worker​

A background recovery worker periodically scans for rows the in-memory consumer is no longer driving:

  • Pending rows whose NextAttemptAt retry window has passed, and Pending rows that have been sitting unclaimed too long, typically because a process restart dropped them from the in-memory queue.
  • Sending rows that have been in flight longer than the send timeout allows, which happens when the process is killed or crashes mid-send. The worker returns these to Pending so delivery is retried rather than left stranded.

Under normal operation the recovery worker finds nothing to do; it is a safety net, not the primary delivery path.

SOC 2 context​

The outbox table is the delivery counterpart to the Security Events log: Security Events records decisions (who did what), Email Delivery records deliveries (what was sent to whom and when). Auditors can use the outbox to verify that password reset and invitation emails were actually dispatched to the expected recipient.

Troubleshooting​

Many rows stuck in Pending​

The in-memory consumer may not be running, or the provider is rejecting every attempt and the retry delay has not elapsed. Check:

  1. The application is running and healthy (check the Background Jobs page for job-runner status).
  2. The Email Settings page shows a provider configured and no recent failures.
  3. If there are failures, read the Last error on each row's detail page to identify the root cause.

Rows flip from Pending to DeadLettered quickly​

Max attempts in the Dispatch / Email policy is set low, or Initial delay is very short. Increase either to give the system more time to recover from transient provider outages before giving up.

Test send succeeds but invitation emails dead-letter​

The test send bypasses the outbox and hits the provider directly. If test sends succeed but outbox rows are dead-lettering, compare the From address used in the test (shown in the test-send panel on the Email Settings page) against the From address the outbox is using. A mismatch (for example, an unverified From address) can cause the provider to accept the test but reject the templated message.