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 ( — compile-time tier)
:stream—:transactional | :operational | :bulk(default:transactional). Compile-time known; LINT-checks read via AST.:tracking—[opens: boolean, clicks: boolean](default all false). Off by default (TRACK-01 / project-level).TRACK-02 NoTrackingOnAuthStreamenforces at compile time;Mailglass.Tracking.Guard.assert_safe!/1enforces at runtime ().:from_default—{name, address}tuple for thefromheader. Applied atnew/0time; per-callSwoosh.Email.from/2overrides.:reply_to_default— same shape as:from_defaultfor Reply-To.
Adopter convention ()
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 runtime Guard).
Runtime tier ()
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.
admin preview calls Mailglass.Renderer.render/1 directly on the
already-built %Message{}; no template resolution happens at render time.
Injection budget (LINT-05, )
The __using__/1 macro injects ≤20 top-level AST forms (target: 15).
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 admin discovery define it themselves. - Module attributes like
@subjector@from— compile-time interpolation does not work the way adopters expect; the builder-function tier is the only correct place ( 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).
Match on %Mailglass.Error{} structs, never message strings.
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 ).
Types
Callbacks
@callback deliver( Mailglass.Message.t(), keyword() ) :: {:ok, term()} | {:error, Mailglass.Error.t()}
@callback deliver_later( Mailglass.Message.t(), keyword() ) :: {:ok, term()} | {:error, Mailglass.Error.t()}
@callback new() :: Mailglass.Message.t()
@callback render(Mailglass.Message.t(), atom(), map()) :: {:ok, Mailglass.Message.t()} | {:error, Mailglass.TemplateError.t()}