Mailer
Send transactional email over SMTP. Define a mailer like a controller — one method per email — render a view, and deliver now or in the background.
Mailers live in app/mailers/ and subclass Mailer. Each action sets instance variables, renders a matching .slv view by convention, and returns a Message you send with deliver_now or deliver_later. SMTP supports STARTTLS (587) and implicit TLS (465).
Defining a mailer
Each public method is an action that builds a message with this.mail(...). Instance variables set in the action are available to its view.
class UserMailer < Mailer
def welcome(user)
@user = user
# Renders app/views/user_mailer/welcome.html.slv with @user in scope.
this.mail(to: user.email, subject: "Welcome!")
end
def reset_password(user)
@user = user
this.mail(to: user.email, subject: "Reset your password")
end
end
Scaffold one with the generator:
soli generate mailer User welcome reset_password
This writes the mailer class plus an HTML view per action under app/views/user_mailer/.
Views
A mailer view is a normal template; the action's instance variables are available as locals, exactly like a controller view. UserMailer#welcome renders user_mailer/welcome by convention.
<h1>Welcome, <%= h(user.name) %>!</h1>
<p>Thanks for joining. <a href="https://example.com/start">Get started</a>.</p>
Override the path with template:, or skip rendering by passing html: (and/or text:) straight to mail.
Plain-text part: add a welcome.text.slv next to welcome.html.slv and it's rendered automatically as the text/plain alternative.
Multiple arguments
An action can take more than one argument:
class OrderMailer < Mailer
def receipt(order, invoice)
@order = order
this.mail(to: order.email, subject: "Receipt #{invoice.number}")
end
end
OrderMailer.receipt(order, invoice).deliver_later
Omitted arguments arrive as nil (default parameter values are not applied), so pass every argument explicitly or fold them into a single hash.
Previewing (dev)
With soli serve --dev, browse every mailer view at /__soli/mailers — a gallery that renders each <mailer>/<action> HTML body in an iframe, so you can iterate on an email without sending one. Each entry links to /__soli/mailers/<mailer>/<action> for a full-page preview.
The preview renders the view directly (not the action), so give it example data with a leading <%# preview: {json} %> header — the same convention as the component catalog:
<%# preview: { "user": { "name": "Ada Lovelace" } } %>
<h1>Welcome, <%= h(user.name) %>!</h1>
Previews render the HTML part only (no layout) with built-in helpers plus the preview data; the action's real instance variables and request context aren't available. The gallery is dev-only — the routes don't exist in production.
Sending
Every action returns a Message. Send it synchronously, or enqueue it on the Job queue for background delivery.
# Send synchronously, within the current request
UserMailer.welcome(user).deliver_now
# Enqueue for background delivery; returns immediately
UserMailer.welcome(user).deliver_later
deliver_later enqueues a __MailDelivery job; if the queue is unavailable it logs and falls back to sending synchronously so a message is never dropped.
this.mail(...)
to : String | Array — one address or a listsubject : String — encoded automatically for non-ASCIIhtml : String? — skip the view render and use this bodytext : String? — plain-text alternative partcc, bcc : String | Array? — bcc stays out of the headersreply_to : String?sender : String? — overrides the configured default Fromattachments : Array? — { "filename", "content_type", "content" } hashestemplate : String? — override the convention view pathAddresses accept a bare addr@host or a "Name <addr@host>" display form.
Attachments
Pass an attachments array to mail, or chain attach / attach_base64 on the returned Message. Text content goes verbatim; binary content is supplied as a base64 string.
UserMailer.welcome(user)
.attach("notes.txt", "Thanks for signing up!")
.attach_base64("logo.png", Base64.encode(File.read("logo.png")), "image/png")
.deliver_later
Each attachment is { "filename", "content_type", "content" } (text) or { "filename", "content_type", "base64" } (binary).
Configuration
Configure delivery once in config/application.sl:
Mailer.configure({
"delivery_method": "smtp", # "smtp" | "test" | "logger"
"host": getenv("SMTP_HOST"),
"port": 587, # 465 = implicit TLS, 587 = STARTTLS
"user": getenv("SMTP_USER"),
"pass": getenv("SMTP_PASS"),
"tls": "auto", # "auto" | "starttls" | "tls" | "none"
"from": "Acme <noreply@example.com>"
})
tls: "auto" uses implicit TLS on port 465 and STARTTLS everywhere else. AUTH LOGIN is used when user/pass are set.
Delivery methods
smtp — send over SMTP (default)test — capture mail in memory for assertions; never sendslogger — print a one-line summary instead of sendingEnvironment variables
SOLI_MAIL_DELIVERY_METHOD — delivery methodSOLI_SMTP_HOST / SOLI_SMTP_PORTSOLI_SMTP_USER / SOLI_SMTP_PASSSOLI_SMTP_TLS — tls modeSOLI_SMTP_FROM — default FromSOLI_SMTP_DOMAIN — EHLO name / Message-ID hostTesting
Set delivery_method: "test" and assert on Mailer.deliveries() — the rendered mail hashes, newest last.
describe("UserMailer", fn() {
before_each(fn() {
Mailer.configure({ "delivery_method": "test" })
Mailer.clear_deliveries()
})
test("welcome email", fn() {
UserMailer.welcome(user).deliver_now()
let sent = Mailer.deliveries()
assert_eq(len(sent), 1)
assert_eq(sent[0]["to"], user.email)
})
})