# Mailgun Transactional Email

## Objective

Send transactional email through Mailgun without losing bounce and complaint state.

A Mailgun delivery path bound to the correct region and sending domain, with durable suppression and event handling.

## Before You Begin

This feature is being added to an application that already exists and already
works. Do not scaffold a new project, and do not assume a blank slate.

Inspect the codebase first and establish:

- The existing application structure and where code of this kind already lives.
- The framework and version in use.
- The existing design system — colours, spacing, typography, and component conventions.
- Existing UI components you can reuse instead of writing new ones.
- The existing database structure, if this feature needs to persist anything.
- The existing authentication and authorization system, if this feature is user-scoped.
- Dependencies already installed, so you don't add a library that duplicates one.
- The existing test setup and conventions.

Only start writing code once you understand the above. If the application
already implements part of this feature, extend it rather than replacing it.

## Implementation Instructions

1. Put Mailgun behind the app's existing mail layer as one adapter. It is an alternative to the other transactional email integrations, not an addition; whichever adapter is in place, the template contract and suppression handling above it are unchanged.
2. Bind the region and sending domain in server-side configuration and validate at startup that they are consistent. A credential issued in one region will simply not work against another, and the failure looks like an authentication problem rather than a configuration one.
3. Check the suppression state for an address before a message is queued, not after the provider rejects it, so bouncing and complaining addresses stop consuming send attempts.
4. Send from the app's background-job system keyed on a stable per-message identifier, and store the provider's identifier on the record for support diagnostics.
5. Do not retain full message bodies for diagnostics. Keep the recipient, the message type, the timestamps, the delivery state, and the provider identifier, which are enough to answer a support question without holding a copy of every reset link the app has ever sent.

## UI and UX Requirements

Match the application's existing design system exactly. Reuse its components,
spacing, and typography. This feature should look like it was always there.

## Responsive Requirements

Works on mobile, tablet, and desktop. Touch targets are large enough to hit on a
phone, and nothing overflows horizontally at 320px.

## Accessibility Requirements

- Fully keyboard navigable.
- Correct semantic elements and ARIA roles.
- Visible focus states.
- Meets WCAG AA contrast.
- Dynamic changes are announced to screen readers.
- Respects prefers-reduced-motion.

## Edge Cases

- Region and sending domain must match the credential in use. Validate the combination at startup and fail loudly, rather than letting every send fail at runtime with a misleading authentication error.
- Event webhooks arrive on a public endpoint, are retried by the provider, and can arrive out of order. Verify their authenticity, apply them idempotently, and never let a replayed older event overwrite a newer delivery state.
- An address on the suppression list must be filtered before the message is queued. Queueing mail that will be rejected wastes attempts, muddies delivery metrics, and hides the real problem from the user whose address is broken.
- A background job retried after a timeout must reconcile against the existing message rather than sending a second copy. Key it on a stable identifier.
- Retaining full message bodies for diagnostics turns the log store into a source of live credentials and personal data. Keep metadata and delivery state only, with a defined retention period.
- Transactional mail must not be blocked by marketing unsubscribe state, and marketing mail must not be routed through this path to bypass it.
- Rate limits and transient provider errors must be distinguished from permanent rejections, backing off on the former and stopping immediately on the latter.
- Non-production environments must suppress delivery or redirect all recipients to a fixed internal address by default, so a seeded data set cannot mail real people.

## Testing

Exercise the feature end to end in the running application. Cover every edge case
above, then run the existing test suite and confirm nothing regressed.

## Acceptance Criteria

- [ ] Region and sending domain are validated at startup against the credential in use.
- [ ] Suppressed addresses are filtered before a message is queued.
- [ ] Event webhooks are authenticated, idempotent, and cannot regress delivery state.
- [ ] Every message record carries the provider identifier and its delivery state.
- [ ] Message bodies are not retained; metadata is kept under a defined retention period.
- [ ] Retried jobs never produce a duplicate email.
- [ ] Non-production environments cannot deliver to real recipients.
- [ ] The feature matches the existing design system.
- [ ] No existing functionality is broken.

## Adaptation Rules

- Match the existing design system. Do not introduce a new colour palette,
  spacing scale, or component library.
- Reuse existing components and utilities wherever they fit.
- Follow the naming, file layout, and code style already present.
- Do not upgrade, replace, or remove existing dependencies to make this
  feature fit. Adapt the feature to the app, not the app to the feature.
- Do not break existing functionality. If a change is genuinely required in
  existing code, make the smallest one that works and say so.
- If something in these instructions conflicts with how the application is
  built, follow the application and explain the deviation.

## Final Verification

Before you report the work as done:

1. Re-read the acceptance criteria above and check each one against what you
   actually built.
2. Run the application and exercise the feature end to end.
3. Run the existing test suite and confirm you have broken nothing.
4. Check the feature on mobile, tablet, and desktop widths.
5. Check keyboard navigation and focus handling.
6. Summarize what changed: files added, files modified, and anything you
   deliberately did differently because of how this application is built.

If any acceptance criterion is unmet, fix it before reporting completion.
