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

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

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

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)



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

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.



269
270
271
# File 'lib/hanami/mailer.rb', line 269

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.



269
270
271
# File 'lib/hanami/mailer.rb', line 269

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



212
213
214
# File 'lib/hanami/mailer.rb', line 212

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.



217
218
219
# File 'lib/hanami/mailer.rb', line 217

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



248
249
250
# File 'lib/hanami/mailer.rb', line 248

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.



253
254
255
# File 'lib/hanami/mailer.rb', line 253

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)



176
177
178
179
180
181
182
# File 'lib/hanami/mailer.rb', line 176

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.



198
199
200
# File 'lib/hanami/mailer.rb', line 198

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:



81
82
83
# File 'lib/hanami/mailer.rb', line 81

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.



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

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

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



109
110
111
# File 'lib/hanami/mailer.rb', line 109

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.



130
131
132
# File 'lib/hanami/mailer.rb', line 130

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.



258
259
260
261
262
263
264
265
# File 'lib/hanami/mailer.rb', line 258

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


193
194
195
# File 'lib/hanami/mailer.rb', line 193

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:



292
293
294
295
# File 'lib/hanami/mailer.rb', line 292

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:



377
378
379
# File 'lib/hanami/mailer.rb', line 377

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:



309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
# File 'lib/hanami/mailer.rb', line 309

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