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:
| Status | Color | What it means |
|---|---|---|
| Pending | Blue | The row is waiting to be picked up by the consumer, or is waiting for its next retry window after a transient failure. |
| Sending | Blue | A consumer has claimed the row and the provider call is in flight. |
| Sent | Green | The provider accepted the message. Terminal. |
| Failed | Red | The provider returned a permanent, non-retryable failure (for example, a rejected recipient address). Terminal. |
| DeadLettered | Red | The 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:
| Setting | Description |
|---|---|
| Max attempts | Total attempts (including the first) before the row is dead-lettered. Setting this to 1 disables retries. |
| Backoff | How the wait time between retries grows: Exponential (doubles each attempt) or Linear (constant wait). |
| Initial delay | Wait time before the first retry. Anchors the backoff curve. |
| Max delay | Cap on the computed delay so exponential backoff doesn't extend into next-day territory. |
| Send timeout | Hard 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:
Pendingrows whoseNextAttemptAtretry window has passed, andPendingrows that have been sitting unclaimed too long, typically because a process restart dropped them from the in-memory queue.Sendingrows 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 toPendingso 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:
- The application is running and healthy (check the Background Jobs page for job-runner status).
- The Email Settings page shows a provider configured and no recent failures.
- 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.