Class: Hanami::Mailer

Inherits:
Object
  • Object
show all
Extended by:
Dry::Configurable
Includes:
ViewIntegration
Defined in:
lib/hanami/mailer.rb,
lib/hanami/mailer/errors.rb,
lib/hanami/mailer/message.rb,
lib/hanami/mailer/version.rb,
lib/hanami/mailer/attachment.rb,
lib/hanami/mailer/dsl/exposure.rb,
lib/hanami/mailer/delivery/smtp.rb,
lib/hanami/mailer/delivery/test.rb,
lib/hanami/mailer/dsl/exposures.rb,
lib/hanami/mailer/attachment_set.rb,
lib/hanami/mailer/delivery/result.rb,
lib/hanami/mailer/dsl/attachments.rb,
lib/hanami/mailer/dsl/plucky_proc.rb,
lib/hanami/mailer/view_integration.rb

Overview

Base mailer class

Since:

  • 3.0.0

Defined Under Namespace

Modules: DSL, Delivery, ViewIntegration Classes: Attachment, AttachmentSet, DuplicateAttachmentError, Error, Message, MissingAttachmentError, MissingDeliveryError, MissingRecipientError, MissingSenderError

Constant Summary collapse

STANDARD_HEADERS =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

Standard email headers that have dedicated convenience methods

Since:

  • 3.0.0

%i[from to cc bcc reply_to return_path subject].freeze
VERSION =

Since:

  • 3.0.0

"3.0.0"

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from ViewIntegration

included

Constructor Details

#initialize(view: nil, delivery_method: nil) ⇒ Mailer

Initialize a new mailer instance

Parameters:

  • view (Object, nil) (defaults to: nil)

    optional view object for rendering

  • delivery_method (Object) (defaults to: nil)

    delivery method (defaults to Test delivery)

Since:

  • 3.0.0



287
288
289
290
# File 'lib/hanami/mailer.rb', line 287

def initialize(view: nil, delivery_method: nil)
  @view = view
  @delivery_method = delivery_method || default_delivery_method
end

Instance Attribute Details

#delivery_methodObject (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Since:

  • 3.0.0



278
279
280
# File 'lib/hanami/mailer.rb', line 278

def delivery_method
  @delivery_method
end

#viewObject (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Since:

  • 3.0.0



278
279
280
# File 'lib/hanami/mailer.rb', line 278

def view
  @view
end

Class Method Details

.attachment(name_or_filename = nil, **options, &block) ⇒ Object

Define an attachment

An attachment block returns one or more attachment objects (use the #file helper). As with #header, its positional parameters receive exposure values and its keyword parameters receive matching keys from the deliver input.

Parameters:

  • name_or_filename (Symbol, String) (defaults to: nil)

    method name or static filename

  • proc (Proc)

    optional block for computing attachment

Since:

  • 3.0.0



220
221
222
# File 'lib/hanami/mailer.rb', line 220

def attachment(name_or_filename = nil, **options, &block)
  attachments.add(name_or_filename, block, **options)
end

.attachmentsObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Since:

  • 3.0.0



225
226
227
# File 'lib/hanami/mailer.rb', line 225

def attachments
  @attachments ||= DSL::Attachments.new
end

.delivery_option(name, value = nil, &block) ⇒ Object

Define a delivery option

Delivery options are delivery-method-specific parameters that can be used to customize how a message is sent. For example, a third-party email service might support scheduled sending, priority levels, or tracking options.

As with #header, a block's positional parameters receive exposure values and its keyword parameters receive matching keys from the deliver input.

Examples:

Static value

delivery_option :track_opens, true

Value computed from the input (keyword parameter)

delivery_option :send_at do |scheduled_time:|
  scheduled_time
end

Value computed from an exposure (positional parameter)

delivery_option :priority do |user_type|
  user_type == "premium" ? "high" : "normal"
end

Parameters:

  • name (Symbol)

    the option name

  • value (Object, nil) (defaults to: nil)

    optional static value

  • block (Proc)

    optional block for computing the value

Since:

  • 3.0.0



257
258
259
# File 'lib/hanami/mailer.rb', line 257

def delivery_option(name, value = nil, &block)
  delivery_options.add(name, block, default: value)
end

.delivery_optionsObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Since:

  • 3.0.0



262
263
264
# File 'lib/hanami/mailer.rb', line 262

def delivery_options
  @delivery_options ||= DSL::Exposures.new
end

.expose(*names, **options, &block) ⇒ Object

Defines one or more values to expose to the template.

An exposure's value comes from the first of these that applies:

  1. The given block (single name only).
  2. An instance method matching the name.
  3. The matching key in the input given to #call, or the :default option if the input has no such key.

When a block or method provides the value, its parameters determine what it receives:

  • Positional parameters receive other exposures' values, matched by name.
  • Keyword parameters receive matching keys from the input. Give them defaults to make those input keys optional.
  • A keyword splat (**input) receives the entire input.

Pass several names to expose multiple values at once; the options then apply to every named exposure. A block may only be given for a single name.

Examples:

A value computed by a block

expose :greeting do |user:|
  "Hello, #{user.name}"
end

A value from a matching instance method, or passed through from the input

expose :user

Multiple values passed through from the input

expose :user, :order

Parameters:

  • names (Array<Symbol>)

    the exposure names

  • options (Hash)

    options applied to the exposure(s)

  • block (Proc)

    block computing the value (single name only)

Options Hash (**options):

  • :default (Object)

    value to use when the input has no matching key (pass-through exposures only)

  • :private (Boolean)

    withhold from the view, while keeping the value available as a dependency to other exposures, headers, attachments, and delivery options (defaults to false)

Since:

  • 3.0.0



182
183
184
185
186
187
188
# File 'lib/hanami/mailer.rb', line 182

def expose(*names, **options, &block)
  if names.length == 1
    exposures.add(names.first, block, **options)
  else
    names.each { |name| exposures.add(name, nil, **options) }
  end
end

.exposuresObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Since:

  • 3.0.0



205
206
207
# File 'lib/hanami/mailer.rb', line 205

def exposures
  @exposures ||= DSL::Exposures.new
end

.file(filename, content, content_type: nil, inline: false) ⇒ Attachment

Helper method for creating Attachment objects

This is a convenience method for creating Attachment objects that can be passed to the attachments: parameter.

Examples:

mailer.deliver(
  user: user,
  attachments: [
    Hanami::Mailer.file("invoice.pdf", pdf_bytes, content_type: "application/pdf")
  ]
)

Parameters:

  • filename (String)

    name of the file

  • content (String)

    file content

  • options (Hash)

    additional options (content_type, inline, etc.)

Returns:

Since:

  • 3.0.0



84
85
86
# File 'lib/hanami/mailer.rb', line 84

def file(filename, content, content_type: nil, inline: false)
  Attachment.new(filename:, content:, content_type:, inline:)
end

.gem_loaderObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Since:

  • 3.0.0



15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# File 'lib/hanami/mailer.rb', line 15

def self.gem_loader
  @gem_loader ||= Zeitwerk::Loader.new.tap do |loader|
    root = File.expand_path("..", __dir__)
    loader.tag = "hanami-mailer"
    loader.push_dir(root)
    loader.ignore(
      "#{root}/hanami-mailer.rb",
      "#{root}/hanami/mailer/version.rb",
      "#{root}/hanami/mailer/errors.rb"
    )
    loader.inflector = Zeitwerk::GemInflector.new("#{root}/hanami-mailer.rb")
    loader.inflector.inflect(
      "dsl" => "DSL",
      "smtp" => "SMTP"
    )
  end
end

.header(field_name, value = nil, &block) ⇒ Object

Define a header field

Can be called with:

  • A static value: header :from, "noreply@example.com"
  • A static value with proper casing: header "X-Priority", "1"
  • A proc/block: header(:to) { |recipient| recipient[:email] }

A block's parameters follow the same convention as everywhere in the mailer:

  • Positional parameters receive exposure values, matched by name.
  • Keyword parameters receive matching keys from the deliver input.

Header names:

  • Symbols with underscores (e.g., :x_priority) are converted to Title-Case (X-Priority)
  • Strings are passed through as-is, preserving casing
  • Use strings for full control over casing

Parameters:

  • field_name (Symbol, String)

    the header field name

  • value (Object, nil) (defaults to: nil)

    optional static value

  • block (Proc)

    optional block for computing the value

Since:

  • 3.0.0



113
114
115
# File 'lib/hanami/mailer.rb', line 113

def header(field_name, value = nil, &block)
  headers.add(field_name, block, default: value)
end

.headersObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Since:

  • 3.0.0



135
136
137
# File 'lib/hanami/mailer.rb', line 135

def headers
  @headers ||= DSL::Exposures.new
end

.inherited(subclass) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Since:

  • 3.0.0



267
268
269
270
271
272
273
274
# File 'lib/hanami/mailer.rb', line 267

def inherited(subclass)
  super

  subclass.instance_variable_set(:@headers, headers.dup)
  subclass.instance_variable_set(:@exposures, exposures.dup)
  subclass.instance_variable_set(:@attachments, attachments.dup)
  subclass.instance_variable_set(:@delivery_options, delivery_options.dup)
end

.private_expose(*names, **options, &block) ⇒ Object

Defines one or more private exposures.

A private exposure is computed and stays available as a dependency to other exposures, and to the mailer's headers, attachments, and delivery options, but is never passed to the view for rendering. This is a shorthand for expose(..., private: true).

See Also:

  • #expose

Since:

  • 3.0.0



200
201
202
# File 'lib/hanami/mailer.rb', line 200

def private_expose(*names, **options, &block)
  expose(*names, **options, private: true, &block)
end

Instance Method Details

#deliver(headers: {}, attachments: nil, format: nil, **input) ⇒ Delivery::Result

Deliver the email

Parameters:

  • headers (Hash) (defaults to: {})

    optional header overrides (from, to, cc, bcc, reply_to, return_path, subject)

  • attachments (Array<Hash, Attachment>, nil) (defaults to: nil)

    optional runtime attachments

  • format (Symbol, nil) (defaults to: nil)

    optional format to render (:html or :text)

  • input (Hash)

    input data for exposures and rendering

Returns:

Since:

  • 3.0.0



303
304
305
306
# File 'lib/hanami/mailer.rb', line 303

def deliver(headers: {}, attachments: nil, format: nil, **input)
  message = prepare(headers:, attachments:, format:, **input)
  delivery_method.call(message)
end

#fileAttachment

Helper method for creating attachments in attachment blocks

Returns an Attachment object that provides a structured, validated way to define attachment data instead of using raw hashes.

Examples:

attachment :invoice do |invoice:|
  file("invoice-#{invoice.number}.pdf", invoice.to_pdf, content_type: "application/pdf")
end

Parameters:

  • filename (String)

    name of the file

  • content (String)

    file content

  • options (Hash)

    additional options (content_type, inline, etc.)

Returns:

Since:

  • 3.0.0



410
411
412
# File 'lib/hanami/mailer.rb', line 410

def file(...)
  self.class.file(...)
end

#prepare(headers: {}, attachments: nil, format: nil, **input) ⇒ Message

Build the message without delivering it

Parameters:

  • headers (Hash) (defaults to: {})

    optional header overrides (from, to, cc, bcc, reply_to, return_path, subject)

  • attachments (Array<Hash, Attachment>, nil) (defaults to: nil)

    optional runtime attachments

  • format (Symbol, nil) (defaults to: nil)

    optional format to render (:html or :text)

  • input (Hash)

    input data for exposures and rendering

Returns:

Since:

  • 3.0.0



341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
# File 'lib/hanami/mailer.rb', line 341

def prepare(headers: {}, attachments: nil, format: nil, **input)
  # Evaluate exposures as our "locals". These will be provided as the _depdenencies_ (available
  # via positional params) to all our other class-level exposure-like APIs: headers,
  # attachments, and delivery options.
  locals = self.class.exposures.bind(self).call(input)

  # Evaluate class-level headers, giving precdence to headers given as explicit arguments.
  header_overrides = headers.compact
  headers = self.class.headers
    .bind(self)
    .call(input, dependencies: locals)
    .merge(header_overrides)

  # Extract custom headers and normalize their header names to proper casing.
  custom_headers = headers
    .reject { |key, _| STANDARD_HEADERS.include?(key) }
    .transform_keys { |key| normalize_header_name(key) }

  # Render bodies. Private exposures are available to the methods above as dependencies, but are
  # withheld from the view.
  html_body, text_body = render(self.class.exposures.reject_private(locals), format:)

  # Evaluate class-level attachments and merge with runtime attachments.
  runtime_attachments = attachments
  attachments = self.class.attachments
    .bind(self)
    .call(input, dependencies: locals)
    .concat(runtime_attachments)
    .to_a

  # Evaluate delivery options.
  delivery_options = self.class.delivery_options.bind(self).call(input, dependencies: locals)

  # Build message
  Message.new(
    from: headers[:from],
    to: headers[:to],
    cc: headers[:cc],
    bcc: headers[:bcc],
    reply_to: headers[:reply_to],
    return_path: headers[:return_path],
    subject: headers[:subject],
    html_body:,
    text_body:,
    attachments: attachments,
    headers: custom_headers,
    delivery_options:
  )
end

#preview(headers: {}, attachments: nil, format: nil, **input) ⇒ Message

Previews the email without delivering it

Builds the message and passes it to the delivery method's preview hook, returning whatever that returns. The default (and test) delivery method returns the message unchanged; a third-party delivery method can override preview to apply service-specific logic.

Parameters:

  • headers (Hash) (defaults to: {})

    optional header overrides (from, to, cc, bcc, reply_to, return_path, subject)

  • attachments (Array<Hash, Attachment>, nil) (defaults to: nil)

    optional runtime attachments

  • format (Symbol, nil) (defaults to: nil)

    optional format to render (:html or :text)

  • input (Hash)

    input data for exposures and rendering

Returns:

Since:

  • 3.0.0



323
324
325
326
# File 'lib/hanami/mailer.rb', line 323

def preview(headers: {}, attachments: nil, format: nil, **input)
  message = prepare(headers:, attachments:, format:, **input)
  delivery_method.preview(message)
end