Mailglass Integration

Copy Markdown View Source

This guide is the canonical adoption path for composing Chimeway with Mailglass. Follow it when you want one credible vertical slice: add both libraries, configure the Mailglass adapter, trigger an email delivery, inspect the trace, and optionally wire inbound feedback.

Responsibility split

Chimeway orchestrates the when and why: durable notification lifecycle, suppression and preference gates, idempotency, scheduling, and operator traces you can search at /admin/chimeway.

Mailglass handles templating and delivery: MJML templates, Swoosh email assembly, and provider send. Chimeway passes notifier rendering/2 assigns through to your host mailable; Mailglass builds the final message.

Chimeway.Adapter.Mailglass is the public adapter name; configure its built-in implementation module, Chimeway.Adapters.Mailglass.

For copy-paste notifier and adapter sections, see the Mailglass integration blueprint. This guide owns the end-to-end path from dependency to verification.

1. Dependencies

Add Chimeway and Mailglass to your host mix.exs:

def deps do
  [
    {:chimeway, "~> 1.0"},
    {:mailglass, "~> 1.3"}
  ]
end

Then fetch dependencies:

mix deps.get

2. Database / migrations

Chimeway stores the durable lifecycle spine (event → notification → delivery → attempt) in your database. Generate and run Chimeway migrations:

mix chimeway.gen.migrations
mix ecto.migrate

For Chimeway install depth — repo config, supervisor setup, and migration idempotency — see Installation.

Mailglass maintains its own schema but uses a host-configured Ecto repo. Follow the Mailglass installation docs for repo setup, migrations, and Oban queues in your host application.

Clean-consumer repository topology

The generated clean consumer proves the single-host-repo shape with ArtifactConsumer.Repo; it does not use the published Chimeway.Repo as a second persistence process. Its concrete configuration is:

config :artifact_consumer, ecto_repos: [ArtifactConsumer.Repo]
config :artifact_consumer, ArtifactConsumer.Repo, repo_config
config :chimeway, repo: ArtifactConsumer.Repo
config :mailglass, repo: ArtifactConsumer.Repo

That makes the host module the Ecto migration repo for one mix ecto.migrate invocation: generated Chimeway migrations and the public Mailglass.Migration.up/0 wrapper both run through ArtifactConsumer.Repo. The generated host loads Chimeway with included_applications: [:chimeway], so Chimeway modules are available without separately starting Chimeway.Repo; ArtifactConsumer.Application supervises ArtifactConsumer.Repo once.

For the synchronous artifact proof only, the process binds Chimeway's Ecto facade to the host repo before triggering and explaining the delivery, then restores the former dynamic repo afterwards:

previous_repo = Chimeway.Repo.get_dynamic_repo()
Chimeway.Repo.put_dynamic_repo(ArtifactConsumer.Repo)

try do
  # Chimeway.trigger/3 and Chimeway.Traces.explain_delivery/1 use ArtifactConsumer.Repo.
after
  Chimeway.Repo.put_dynamic_repo(previous_repo)
end

Your host chooses its own persistence topology; the clean-consumer proof intentionally configures, migrates, supervises, and routes both Chimeway and Mailglass through one consumer-owned ArtifactConsumer.Repo.

3. Runtime config

Register the Mailglass adapter for the email channel and map render_key values to host mailable functions:

config :chimeway,
  channel_adapters: %{"email" => Chimeway.Adapters.Mailglass},
  channel_adapter_configs: %{
    "email" => [
      mailables: %{
        "teampulse.invite_sent.email" => {DemoHost.Mailers.InviteEmail, :invite_email}
      }
    ]
  }

Replace DemoHost.Mailers.InviteEmail with your host mailable module in production apps. The render_key string in notifier rendering/2 must match the key in the mailables map.

Configure Mailglass per its docs — repo, Swoosh adapter, and provider credentials. Chimeway does not manage Mailglass application config; it only invokes your mailable through the adapter at delivery time.

For the full Chimeway runtime setup (installer repo, Chimeway.Repo, supervisor), see Installation §3–§4.

4. Host mailable

Your host owns the Mailglass.Mailable module. Chimeway's Mailglass adapter resolves render_key → {Module, :function} and passes delivery render_data (notifier assigns plus recipient "to") into the mailable function.

Example host mailable for the teampulse.invite_sent.email render key:

defmodule DemoHost.Mailers.InviteEmail do
  use Mailglass.Mailable, stream: :transactional

  def invite_email(assigns) when is_map(assigns) do
    to = Map.get(assigns, "to") || Map.get(assigns, :to)
    subject = Map.get(assigns, "subject") || "You're invited"
    html_body = Map.get(assigns, "html_body") || ""
    text_body = Map.get(assigns, "text_body") || ""

    new()
    |> Mailglass.Message.update_swoosh(fn email ->
      email
      |> Swoosh.Email.to(to)
      |> Swoosh.Email.from({"TeamPulse", "invites@teampulse.test"})
      |> Swoosh.Email.subject(subject)
      |> Swoosh.Email.html_body(html_body)
      |> Swoosh.Email.text_body(text_body)
    end)
    |> Mailglass.Message.put_function(:invite_email)
  end
end

Pair this mailable with a notifier that declares stable keys and a matching render_key:

defmodule DemoHost.Notifiers.InviteSent do
  use Chimeway.Notifier

  @impl true
  def notification_key, do: "teampulse.invite_sent"

  @impl true
  def version, do: 1

  @impl true
  def recipients(%{email: email}) do
    {:ok, [%{recipient_identity: "user:#{email}", recipient_type: "user"}]}
  end

  @impl true
  def build(%{team_name: team_name}, _recipient) do
    {:ok, %{"headline" => "You're invited to #{team_name}", "body" => "Join your team."}}
  end

  @impl true
  def channels(_params, _recipient), do: {:ok, [:email, :in_app]}

  @impl true
  def rendering(%{team_name: team_name}, _recipient) do
    {:ok,
     %{
       assigns: %{
         "subject" => "You're invited to #{team_name}",
         "html_body" => "<p>Join your team.</p>",
         "text_body" => "Join your team."
       },
       channels: %{
         email: %{render_key: "teampulse.invite_sent.email", render_version: 1}
       }
     }}
  end
end

Runnable reference: DemoHost.Notifiers.InviteSent and DemoHost.Mailers.InviteEmail in the demo host.

5. Trigger and verification

Trigger the notifier with required idempotency and tenancy:

Chimeway.trigger(
  DemoHost.Notifiers.InviteSent,
  %{email: "alex@teampulse.test", team_name: "Engineering"},
  idempotency_key: "teampulse-invite-alex",
  tenant_id: "teampulse"
)

Both :idempotency_key and :tenant_id are required. Omitting tenant_id returns {:error, :missing_tenant_id}.

After delivery, verify explainability:

  • Search /admin/chimeway by recipient identity — the delivery detail shows the stable notification key (teampulse.invite_sent) and the Mailglass adapter module on the attempt timeline.
  • In IEx, use Chimeway.Traces.explain_delivery/1 on a delivery ID from the trigger result.

Runnable demo: DemoHost.Seeds.seed_invite/0 triggers the same notifier with deterministic idempotency keys for local proof.

Clean-consumer proof boundary

What happened: In the unpacked-artifact clean-consumer proof, Fake recorded exactly one host-composed message and Chimeway recorded a successful Chimeway.Adapters.Mailglass attempt.

Why it matters: The one consumer-owned repo, stable notifier and render_key mapping, host mailable selection, adapter routing, and attempt persistence all executed together. This is local composition evidence, not a claim that an email reached a live provider or inbox.

Next step: Follow the focused Mailglass integration blueprint for your host application's wiring.

The proof does not cover real provider acceptance, sender/domain verification, inbox placement/display, production credentials, provider callbacks, or live webhook feedback.

mix verify.mailglass is this repository's repository-maintainer regression suite. It exercises the Mailglass adapter contract, executor routing, webhook pipeline, and demo-host proof; it is not a command supplied to Hex consumers.

6. Optional inbound feedback

When provider webhooks should drive workflow progression, mount inbound feedback through Chimeway.Webhooks.process/4 in your host controller. Do not bypass Chimeway's ingress layer with a standalone Mailglass webhook plug — Chimeway owns ingress durability, attempt recording, and signal emission; the Mailglass adapter supplies webhook callbacks (verify_webhook, resolve_delivery, normalize_feedback) behind the adapter behaviour.

Example host route (optional demo path /webhooks/chimeway/mailglass):

def create(conn, _params) do
  # Flatten cached iolist chunks to binary before HMAC verification.
  raw_body =
    conn.assigns
    |> Map.get(:raw_body, [])
    |> Enum.reverse()
    |> IO.iodata_to_binary()

  headers = conn.req_headers
  adapter_module = Chimeway.Adapters.Mailglass
  config = Application.get_env(:my_app, :chimeway_webhook_config, [])

  case Chimeway.Webhooks.process(adapter_module, raw_body, headers, config) do
    {:ok, _ingress} ->
      send_resp(conn, 200, "OK")

    {:error, :unauthorized} ->
      send_resp(conn, 401, "Unauthorized")

    {:error, _other} ->
      send_resp(conn, 500, "Internal Server Error")
  end
end

Hosts using a custom :body_reader must cache raw bytes in conn.assigns[:raw_body] before parsers consume the body — see DemoHost.Plugs.CacheBodyReader and the runnable reference at examples/chimeway_demo_host/lib/demo_host_web/controllers/webhooks_controller.ex.

Log error reasons server-side only; never return internal error tuples to the webhook provider. Hosts MAY use 400 or 422 for observability, but MUST return non-2xx for library errors so providers retry.

The adapter's verify_webhook/3 validates the provider signature, resolve_delivery/2 maps the payload to a Chimeway delivery row, and normalize_feedback/1 converts provider events into canonical delivery outcomes.

For workflow progression context — how chimeway.delivery.succeeded and chimeway.delivery.bounced signals resume or stop runs — see Feedback escalation workflow.