# Production communications — Phase 5B

SMS production vendor: **NOT SELECTED**. Status: **provider boundary ready**.
Real SMS sending: **DISABLED**. No HTTP client, vendor endpoint, payload template,
authentication protocol or status lookup has been invented.

## Enable email

Use `config.settings.production`, then supply these deployment environment values
(or the existing untracked `.env`). Never place actual credentials in source,
database templates, shell command arguments or documentation:

```dotenv
COMMUNICATIONS_ALLOW_EXTERNAL=True
COMMUNICATIONS_EMAIL_BACKEND=apps.communications.providers.DjangoEmailProvider
COMMUNICATIONS_SMS_BACKEND=
EMAIL_BACKEND=django.core.mail.backends.smtp.EmailBackend
EMAIL_HOST=smtp.example.invalid
EMAIL_PORT=587
EMAIL_HOST_USER=
EMAIL_HOST_PASSWORD=
EMAIL_USE_TLS=True
EMAIL_USE_SSL=False
DEFAULT_FROM_EMAIL=service@example.invalid
```

Replace the illustrative host/from address with authorized deployment values.
Username and password must both be supplied, or both empty for an authenticated
network relay that does not use SMTP login. TLS or implicit SSL is required,
exclusively; use the port specified by your mail operator. The application supplies
a finite **10-second socket timeout**. This bounds individual socket operations,
not the total wall time of a multi-step SMTP conversation.

Run `python manage.py check --settings=config.settings.production` before enabling
dispatch. These checks inspect configuration only: they do not authenticate to a
server or send a test message. Missing/invalid selected adapters fail checks and
dispatch safely; an unconfigured channel never falls back to a fake in production.
Each channel is selected independently by its adapter path; both may remain
disabled even with global external enablement. An empty path disables that
channel, and an attempted production send records a configuration failure.

Production supports Django SMTP; development uses the in-memory backend. Other
backend classes require a later explicit review of their acceptance/error contract
before adding support. Django's backend configuration is described in its
[email documentation](https://docs.djangoproject.com/en/5.2/topics/email/).

## Outcomes, attempts and retries

The existing `DeliveryResult(outcome, reference)` and NotificationAttempt fields
remain authoritative. No new schema or parallel dispatch mechanism is introduced.

| Evidence | Normalized outcome | Retry |
| --- | --- | --- |
| Missing/unsafe configuration, invalid email destination/header | REJECTED | After correcting the cause |
| SMTP authentication, sender or all-recipient rejection | REJECTED | After correcting the cause |
| Backend reports exactly one sent message | ACCEPTED | No repeat dispatch |
| Timeout, disconnect, other connection error | UNKNOWN | No automatic or ordinary manual resend |
| Other backend exception, SMTP DATA error, unexpected/zero result | UNKNOWN | Investigate first |
| Malformed adapter result or unexpected outcome | UNKNOWN | Investigate first |
| Process dies after claiming dispatch | Existing SENDING/STARTED | Investigate first |

Ambiguous failures remain conservative because a backend can raise after the
server has accepted a message (including during connection cleanup). A known
pre-send connection failure could be refined by a future transport-specific
contract; this adapter does not infer that from an arbitrary exception.

SENT means **backend acceptance, not inbox delivery**. SMTP does not expose a
portable provider reference through Django's send count, so the reference stays
empty. It is not replaced with a fabricated receipt. Existing locks, immutable
snapshots, retained attempts and FAILED retry rules remain unchanged. UNKNOWN
and unfinished sends have no automatic retry or new reconciliation workflow.

Attempt rows retain the frozen generic sanitized codes (`NOT_ACCEPTED`,
`PROVIDER_CONFIGURATION`, `OUTCOME_UNKNOWN`, etc.). Structured logs add fixed
email failure categories and outcome/provider/attempt identifiers; no exception
text, credential, body, recipient or provider reference is logged. Logging failure
does not change recorded send outcomes. Existing authorized history already shows
masked destination, provider, reference, timestamps, status and error code; its
scoped permissions and responsive shell are reused without new disclosure.

## Future SMS adapter contract

Implement a trusted `SmsProvider` subclass with a safe `[a-zA-Z0-9_-]{1,64}`
identifier and `production_ready = True` only after the real contract is reviewed.
Set `COMMUNICATIONS_SMS_BACKEND` to its import path. Channel type checks prevent
selecting an email adapter for SMS. Constructors must be side-effect-free: system
checks instantiate adapters, but must never contact a vendor.

`send(*, destination, subject, body, idempotency_key)` receives the immutable
normalized destination and rendered snapshot; SMS subject is empty. The key is
the stable notification UUID, reused across legitimate attempts. Use it only
according to the chosen vendor's documented idempotency semantics. Never assume
a vendor deduplicates requests without that guarantee.

Return `DeliveryResult("ACCEPTED", safe_reference)` only on documented acceptance,
not merely HTTP success; `REJECTED` only when definitively not accepted; otherwise
`UNKNOWN`. Return only a non-secret opaque reference matching
`[a-zA-Z0-9_-]{1,128}`; the dispatcher discards other reference strings. Never return
raw payloads or sensitive response text as IDs. Validate destinations against the
actual vendor contract without silently retargeting stored snapshots. Apply bounded
connect/read timeouts and no hidden transport retries on ambiguous outcomes.

Credentials remain deployment secrets. Sanitize exceptions and fixed log categories
inside the adapter; dispatcher exceptions are already mapped to UNKNOWN without
persisting raw exception text. Definitive rejection allows the existing authorized
retry; uncertainty does not. Add deterministic contract and dispatch tests before
enablement. Delivery callbacks/status lookup require a future authoritative contract
and explicit reconciliation design; none exists in this phase.

## Development, tests and rotation

Default adapters are unconfigured and email uses memory. External delivery requires
production mode plus explicit enablement; fakes/base no-op adapters are rejected
in production. The project test runner replaces inherited adapter settings with an
empty mapping, disables external delivery and selects in-memory email before test
setup. Tests may explicitly select mocks/fakes; do not override this safety with a
live transport. Demo seeding still queues/sends no external message.

Rotate credentials through deployment secret configuration, restart application
and dispatch processes, and run configuration checks. Revoke old credentials using
the provider's operational procedure. Retry only definitively FAILED notifications
after investigating the cause; rotation is not authority to resend UNKNOWN/SENDING
messages. No real messages are sent by checks, audit or automated verification.

No webhook, delivery receipt, background sender, scheduler, bulk campaign engine
or automatic uncertain-send reconciliation is added. Communications failures do
not govern service, commercial, front-desk or SLA workflow success.

## Verification

Baseline: committed Phase 5A `96605a6` on master, initially clean.
The 48-test communications run passed (34 unchanged frozen tests and the initial
14 adapter tests) in 192.380 seconds. After the additional coverage/refinements,
the 19-test focused run passed in 17.055 seconds (18 new tests plus the existing
email-adapter test). The final 15 adapter/configuration tests also passed in
0.640 seconds after the independent-channel configuration refinement.

`manage.py check` passed and `makemigrations --check` found no changes. Exactly one
`audit_project --quick` passed: 19 smoke tests in 35.918 seconds, total audit
runtime **95.094 seconds**. All transport calls were mocked/in-memory/fake; no
real SMS/email was sent. No full audit, historical migration changes, existing
test edits, staging, commit or push were performed.
