email-connect/docs/EMAIL-WP-0004-T04-failure-evidence.md
tegwick 04897a149f
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Complete EMAIL-WP-0004 transactional invitation mail delivery.
Ship the railiance01 deploy package (OpenBao/ESO custody, NetworkPolicy,
probes), provider failure classification and suppression, T04 unit and live
proof, and non-secret NK-WP-0024 hand-back evidence. Workplan finished.
2026-08-12 13:32:11 +02:00

4.8 KiB

EMAIL-WP-0004-T04 — invitation delivery failure evidence

Date: 2026-08-12
Cluster: railiance01
Service: http://email-connect.email-connect.svc.cluster.local:8080
Image: forgejo.coulomb.social/coulomb/email-connect@sha256:a9de1994eea88502ec30ba7c2fc029ec901d873140d51e11d2c50e363a0578b1
Unit tests: PYTHONPATH=src python3 -m pytest tests/test_transactional.py9 passed

This note is the non-secret hand-back for NK-WP-0024. No SMTP passwords, ingest tokens, or raw provider exception text are included.

Contract for user-engine

HTTP error / status retryable Meaning for outbox
401 unauthorized Wrong or missing bearer; fail closed
400 template_not_allowed Event type/source not on allow-list
400 invalid_recipient Address failed local validation
400 idempotency_key_mismatch Idempotency-Key ≠ event id
400 recipient_suppressed Address on suppression list; do not retry as send
400 invitation_id_required Missing invitation id
200 status=duplicate + reference Same event id already accepted; safe replay
202 status=accepted + reference + event_id Provider accepted message; not inbox delivery
503 provider_timeout / temporary_deferral / provider_unavailable true Bounded retry
422 permanent_rejection false Do not retry; surface failure evidence

Evidence ceilings (must not become authorization)

  • Invitation accept: evidence_ceiling=provider_accepted — transport only.
  • Registration consume/cancel: authorization=false, ceilings mailbox_challenge_consumed / mailbox_challenge_canceled.
  • assurance.mailbox_control=true is channel evidence, not identity proof and not a flex-auth decision.

Mailbox ownership challenges and provider acceptance must never alter user-engine authorization decisions.

Unit matrix (local)

Case Result
Template allow-list denial 400 template_not_allowed
Invalid recipient 400 invalid_recipient
Idempotency mismatch 400 idempotency_key_mismatch
Auth denial 401 unauthorized
Duplicate event id 200 duplicate + same reference; provider called once
Resend (family_invitation.resent) New event id → second provider send
Suppression 400 recipient_suppressed; no provider call
Provider timeout / temporary / unavailable 503 + retryable=true, redacted codes
Permanent rejection 422 + retryable=false
SMTPRecipientsRefused → permanent ProviderError mapping unit-tested
Redaction Response JSON has no password/SMTP host/exception text
Mailbox evidence ≠ authz authorization=false on consume/cancel

Live matrix (railiance01, from user-engine pod)

Case HTTP Body (redacted)
Unauth 401 {"error":"unauthorized"}
Template deny 400 {"error":"template_not_allowed"}
Invalid recipient 400 {"error":"invalid_recipient"}
Idempotency mismatch 400 {"error":"idempotency_key_mismatch"}
Accept (to forgejo@coulomb.social) 202 status=accepted, evidence_ceiling=provider_accepted, event_id set
Duplicate same event id 200 status=duplicate, same reference
Resend new event id 202 status=accepted
Bad domain nobody@invalid.invalid 422 {"error":"permanent_rejection","retryable":false}
Suppressed suppressed@example.test 400 {"error":"recipient_suppressed"} (seeded in SQLite for smoke)
Redaction No password / smtp.ionos in any body

Negative path (unchanged from T03): pods outside user-engine cannot reach TCP 8080 (Connection refused).

Non-secret references for NK-WP-0024

  • Cluster-local base URL: http://email-connect.email-connect.svc.cluster.local:8080
  • Paths: POST /v1/send, POST /v1/registration-verifications, …/consume, …/cancel
  • Auth: shared ingest bearer (OpenBao platform/workloads/email-connect/transactional field EMAIL_CONNECT_INGEST_TOKEN only — never SMTP fields in user-engine)
  • Idempotency: header Idempotency-Key = event id
  • Allowed send types: family_member.invited, family_invitation.resent
  • Catalog route: email-connect-transactional (ops-warden)

Temporary deferral / timeout note

Live temporary deferral depends on provider behavior and was covered by unit classification (temporary_deferral, provider_timeout → 503 retryable). Live permanent path was observed via invalid domain → 422 permanent_rejection.

Follow-ups (out of T04)

  • Mount ingest token into user-engine runtime for production outbox (NK-WP-0024).
  • Operator API to manage suppressions (currently SQLite; smoke seeded manually).
  • Daily send-quota tiers deferred (DECISIONS.md).