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)
Mailglass.Tenancy.assert_stamped!/0— precondition (raises)Mailglass.Tracking.Guard.assert_safe!/1— D-38 precondition (raises)Mailglass.Suppression.check_before_send/1Mailglass.RateLimiter.check/3(:transactionalbypasses)Mailglass.Stream.policy_check/1(no-op seam v0.1)Mailglass.Renderer.render/1- 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, ...)— appliesProjector.update_projections/2with the dispatched eventMailglass.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.
Summary
Functions
Canonical public verb for synchronous delivery (D-13). Delegates to send/2.
Matches the naming convention from Swoosh and ActionMailer for adopter
familiarity.
Bang variant — raises the error struct directly on failure.
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).
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]
Bang variant of deliver_many/2. Raises %Mailglass.Error.BatchFailed{}
when any Delivery has status: :failed.
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.
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.
Functions
Canonical public verb for synchronous delivery (D-13). Delegates to send/2.
Matches the naming convention from Swoosh and ActionMailer for adopter
familiarity.
@spec deliver!( Mailglass.Message.t(), keyword() ) :: Mailglass.Outbound.Delivery.t()
Bang variant — raises the error struct directly on failure.
@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).
@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_errorcarries 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.
@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).
@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.
@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.