Mailglass.Mailable behaviour (Mailglass v0.1.0)

Copy Markdown View Source

Behaviour + use macro for adopter-defined mailable modules (AUTHOR-01).

Usage

defmodule MyApp.UserMailer do
  use Mailglass.Mailable, stream: :transactional

  def welcome(user) do
    new()
    |> Mailglass.Message.update_swoosh(fn e ->
         e
         |> Swoosh.Email.from({"MyApp", "hello@example.com"})
         |> Swoosh.Email.to(user.email)
         |> Swoosh.Email.subject("Welcome, #{user.name}!")
         |> Swoosh.Email.text_body("Welcome!")
       end)
    |> Mailglass.Message.put_function(:welcome)
  end
end

# Adopter sends:
user |> MyApp.UserMailer.welcome() |> MyApp.UserMailer.deliver()

use opts (D-11 — compile-time tier)

  • :stream:transactional | :operational | :bulk (default :transactional). Compile-time known; Phase 6 LINT-checks read via AST.

  • :tracking[opens: boolean, clicks: boolean] (default all false). Off by default (TRACK-01 / D-08 project-level). Phase 6 TRACK-02 NoTrackingOnAuthStream enforces at compile time; Phase 3 Mailglass.Tracking.Guard.assert_safe!/1 enforces at runtime (D-38).
  • :from_default{name, address} tuple for the from header. Applied at new/0 time; per-call Swoosh.Email.from/2 overrides.
  • :reply_to_default — same shape as :from_default for Reply-To.

Adopter convention (D-10)

new/0 returns a %Mailglass.Message{}. Use Mailglass.Message.update_swoosh/2 to pipe into Swoosh builder functions and Mailglass.Message.put_function/2 to stamp the :mailable_function field (required by D-38 runtime Guard).

Runtime tier (D-11)

The injected new/0 returns a %Mailglass.Message{}; adopters pipe through Mailglass.Message.update_swoosh/2 and Swoosh builder functions. Compile-time opts seed initial values; per-call calls override.

Default render/3

The injected render/3 is a thin pass-through to Mailglass.Renderer.render/1. It ignores the template and assigns arguments — template resolution is an adopter-owned concern. Adopters who need template resolution override via defoverridable render: 3.

Phase 5 admin preview calls Mailglass.Renderer.render/1 directly on the already-built %Message{}; no template resolution happens at render time.

Injection budget (LINT-05, D-09)

The __using__/1 macro injects ≤20 top-level AST forms (target: 15). Phase 6 NoOversizedUseInjection enforces; a runtime AST-counting test in this phase asserts the budget.

Does NOT inject

  • Phoenix.Component — adopters opt in per-mailable by importing it themselves. Avoids HEEx collision risk with adopter-defined components.
  • Default preview_props/0 — optional callback; adopters who want Phase 5 admin discovery define it themselves.
  • Module attributes like @subject or @from — compile-time interpolation does not work the way adopters expect; the builder-function tier is the only correct place (D-11 rationale).

defoverridable surface

new/0, render/3, deliver/2, deliver_later/2 — all four injected functions are overridable. Adopters who bypass Mailglass.Outbound via deliver/2 override lose telemetry + projection writes (T-3-04-04 accepted).

See docs/api_stability.md §Mailable for the locked contract.

Summary

Functions

Injects the mailable boilerplate. ≤20 top-level AST forms (LINT-05 enforces at Phase 6).

Types

opts()

@type opts() :: [
  stream: :transactional | :operational | :bulk,
  tracking: [opens: boolean(), clicks: boolean()],
  from_default: {String.t(), String.t()} | nil,
  reply_to_default: {String.t(), String.t()} | nil
]

Callbacks

deliver(t, keyword)

@callback deliver(
  Mailglass.Message.t(),
  keyword()
) :: {:ok, term()} | {:error, Mailglass.Error.t()}

deliver_later(t, keyword)

@callback deliver_later(
  Mailglass.Message.t(),
  keyword()
) :: {:ok, term()} | {:error, Mailglass.Error.t()}

new()

@callback new() :: Mailglass.Message.t()

preview_props()

(optional)
@callback preview_props() :: [{atom(), map()}]

render(t, atom, map)

@callback render(Mailglass.Message.t(), atom(), map()) ::
  {:ok, Mailglass.Message.t()} | {:error, Mailglass.TemplateError.t()}

Functions

__using__(opts)

(macro)

Injects the mailable boilerplate. ≤20 top-level AST forms (LINT-05 enforces at Phase 6).