ESC
Type to search...
S
Soli Docs

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 list
subject : String — encoded automatically for non-ASCII
html : String? — skip the view render and use this body
text : String? — plain-text alternative part
cc, bcc : String | Array?bcc stays out of the headers
reply_to : String?
sender : String? — overrides the configured default From
attachments : Array?{ "filename", "content_type", "content" } hashes
template : String? — override the convention view path

Addresses 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 sends
logger — print a one-line summary instead of sending

Environment variables

SOLI_MAIL_DELIVERY_METHOD — delivery method
SOLI_SMTP_HOST / SOLI_SMTP_PORT
SOLI_SMTP_USER / SOLI_SMTP_PASS
SOLI_SMTP_TLS — tls mode
SOLI_SMTP_FROM — default From
SOLI_SMTP_DOMAIN — EHLO name / Message-ID host

Testing

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)
  })
})