# `Mailglass.Outbound`
[🔗](https://github.com/szTheory/mailglass/blob/v0.1.0/lib/mailglass/outbound.ex#L1)

Public facade for the mailglass send pipeline (TRANS-04, SEND-01).

All four delivery shapes (sync, async, batch, bang variants) converge
on the same `%Mailglass.Outbound.Delivery{}` return object. Adopter code
pattern-matches by struct + status field — never by message strings.

## Public verbs

`deliver/2` is the canonical public name (matches Swoosh + ActionMailer
familiarity). `send/2` is the internal implementation verb; `deliver/2`
is a `defdelegate` alias (D-13).

## Preflight pipeline (SEND-01, D-18)

0. `Mailglass.Tenancy.assert_stamped!/0` — precondition (raises)
1. `Mailglass.Tracking.Guard.assert_safe!/1` — D-38 precondition (raises)
2. `Mailglass.Suppression.check_before_send/1`
3. `Mailglass.RateLimiter.check/3` (`:transactional` bypasses)
4. `Mailglass.Stream.policy_check/1` (no-op seam v0.1)
5. `Mailglass.Renderer.render/1`
6. Persist (two Multis separated by adapter call)

Preconditions (0 + 1) raise on violation. Stages 2-5 return
`{:error, struct}`; the `with` short-circuits.

## Two-Multi sync path (D-20)

Multi#1 (inside `Repo.multi/1`):
- `Ecto.Multi.insert(:delivery, Delivery.changeset(attrs))`
- `Mailglass.Events.append_multi(:event_queued, ...)`

Adapter call OUTSIDE any transaction.

Multi#2 (inside `Repo.multi/1`):
- `Ecto.Multi.update(:delivery, ...)` — applies
  `Projector.update_projections/2` with the dispatched event
- `Mailglass.Events.append_multi(:event_dispatched, ...)`

After Multi#2 commits → `Projector.broadcast_delivery_updated/3`.

**Adapter-call-in-transaction is a hard no** (D-20) — Postgres
connection-pool starvation under provider latency. Orphan `:queued`
Delivery rows between Multi#1 and adapter call are reconcilable via
`Mailglass.Events.Reconciler` (Phase 2 D-19) with age ≥5min.

## Return shapes

- `{:ok, %Delivery{status: :sent}}` — sync success
- `{:error, %Mailglass.Error{}}` — preflight short-circuit or Multi failure

## deliver_many/2 scope (v0.1)

Async-only. Every message produces an Oban job (or Task.Supervisor spawn
when Oban absent). Sync-batch fan-out deferred to v0.5.
`[ASSUMED — Plan 05 Task 4 decision]`

## Heterogeneous-tenant batches

`deliver_many/2` assumes all messages share the same tenant_id. Mixed-tenant
batches corrupt idempotency key derivation (the hash includes tenant_id).
Adopters must batch per-tenant. Future enhancement: raise ArgumentError on
mixed tenants.

# `deliver`
*since 0.1.0* 

Canonical public verb for synchronous delivery (D-13). Delegates to `send/2`.
Matches the naming convention from Swoosh and ActionMailer for adopter
familiarity.

# `deliver!`
*since 0.1.0* 

```elixir
@spec deliver!(
  Mailglass.Message.t(),
  keyword()
) :: Mailglass.Outbound.Delivery.t()
```

Bang variant — raises the error struct directly on failure.

# `deliver_later`
*since 0.1.0* 

```elixir
@spec deliver_later(
  Mailglass.Message.t(),
  keyword()
) :: {:ok, Mailglass.Outbound.Delivery.t()} | {:error, Mailglass.Error.t()}
```

Async delivery. Runs preflight pipeline, persists the Delivery, and enqueues
an Oban job (or spawns a Task.Supervisor task when Oban is absent).
Always returns `{:ok, %Delivery{status: :queued}}` on success — never an
`%Oban.Job{}` (D-14 return-shape lock).

# `deliver_many`
*since 0.1.0* 

```elixir
@spec deliver_many(
  [Mailglass.Message.t()],
  keyword()
) :: {:ok, [Mailglass.Outbound.Delivery.t()]} | {:error, Mailglass.Error.t()}
```

Async batch send (TRANS-04, D-15). v0.1 scope: **async-only** — every
message in the batch produces an Oban job (or Task.Supervisor spawn
when Oban absent). Sync-batch fan-out deferred to v0.5.
`[ASSUMED — Plan 05 Task 4 decision]`

## Return shape

`{:ok, [%Delivery{}]}` always (one row per input message). Each Delivery
carries its own `:status`:
- `:queued` — successfully enqueued
- `:failed` — preflight rejected; `:last_error` carries the specific error

Batch-level errors (DB unavailable) return `{:error, %Mailglass.Error{}}`.

## Replay safety

`idempotency_key` + partial UNIQUE index make re-running the same batch a
DB-level no-op. Existing rows are re-fetched via companion SELECT.

# `deliver_many!`
*since 0.1.0* 

```elixir
@spec deliver_many!(
  [Mailglass.Message.t()],
  keyword()
) :: [Mailglass.Outbound.Delivery.t()]
```

Bang variant of `deliver_many/2`. Raises `%Mailglass.Error.BatchFailed{}`
when any Delivery has `status: :failed`.

`deliver_later!/2` is deliberately NOT provided (enqueue isn't a
delivery — nothing delivery-shaped to raise about, per D-16).

# `dispatch_by_id`
*since 0.1.0* 

```elixir
@spec dispatch_by_id(binary()) ::
  {:ok, Mailglass.Outbound.Delivery.t()} | {:error, Mailglass.Error.t()}
```

Hydrates a Delivery by id, calls the adapter OUTSIDE any transaction, and
writes Multi#2. Called by `Mailglass.Outbound.Worker.perform/1` and by the
`Task.Supervisor` fallback in `enqueue_task_supervisor/2`.

Declared public so the Worker can call it from outside this module.

# `send`
*since 0.1.0* 

```elixir
@spec send(
  Mailglass.Message.t() | Swoosh.Email.t(),
  keyword()
) :: {:ok, Mailglass.Outbound.Delivery.t()} | {:error, Mailglass.Error.t()}
```

Synchronous hot path. Runs the full preflight pipeline, persists the Delivery
via two Multis (adapter call between them, OUTSIDE any transaction per D-20),
and returns `{:ok, %Delivery{status: :sent}}` on success.

`deliver/2` is the canonical public alias (see below). `send/2` is the
internal implementation verb.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
