hanami-mailer/lib/hanami/mailer.rb

267 lines
6.8 KiB
Ruby

# frozen_string_literal: true
require "mail"
require "concurrent"
# Hanami
#
# @since 0.1.0
module Hanami
# Hanami::Mailer
#
# @since 0.1.0
class Mailer
require "hanami/mailer/version"
require "hanami/mailer/template"
require "hanami/mailer/finalizer"
require "hanami/mailer/configuration"
require "hanami/mailer/dsl"
# Content types mapping
#
# @since 0.1.0
# @api private
CONTENT_TYPES = {
html: "text/html",
txt: "text/plain"
}.freeze
private_constant(:CONTENT_TYPES)
# Base error for Hanami::Mailer
#
# @since 0.1.0
class Error < ::StandardError
end
# Unknown mailer
#
# This error is raised at the runtime when trying to deliver a mail message,
# by using a configuration that it wasn't finalized yet.
#
# @since next
# @api unstable
#
# @see Hanami::Mailer.finalize
class UnknownMailerError < Error
# @param mailer [Hanami::Mailer] a mailer
#
# @since next
# @api unstable
def initialize(mailer)
super("Unknown mailer: #{mailer.inspect}. Please finalize the configuration before to use it.")
end
end
# Missing delivery data error
#
# It's raised when a mailer doesn't specify `from` or `to`.
#
# @since 0.1.0
class MissingDeliveryDataError < Error
def initialize
super("Missing delivery data, please check 'from', or 'to'")
end
end
# @since next
# @api unstable
@_subclasses = Concurrent::Array.new
# Override Ruby's hook for modules.
# It includes basic `Hanami::Mailer` modules to the given Class.
# It sets a copy of the framework configuration
#
# @param base [Class] the target mailer
#
# @since next
# @api unstable
def self.inherited(base)
super
@_subclasses.push(base)
base.extend Dsl
end
private_class_method :inherited
# Finalize the configuration
#
# This should be used before to start to use the mailers
#
# @param configuration [Hanami::Mailer::Configuration] the configuration to
# finalize
#
# @return [Hanami::Mailer::Configuration] the finalized configuration
#
# @since next
# @api unstable
#
# @example
# require 'hanami/mailer'
#
# configuration = Hanami::Mailer::Configuration.new do |config|
# # ...
# end
#
# configuration = Hanami::Mailer.finalize(configuration)
# MyMailer.new(configuration: configuration)
def self.finalize(configuration)
Finalizer.finalize(@_subclasses, configuration)
end
# Initialize a mailer
#
# @param configuration [Hanami::Mailer::Configuration] the configuration
# @return [Hanami::Mailer]
#
# @since 0.1.0
def initialize(configuration:)
@configuration = configuration
freeze
end
# Prepare the email message when a new mailer is initialized.
#
# @return [Mail::Message] the delivered email
#
# @since 0.1.0
# @api unstable
#
# @see Hanami::Mailer::Configuration#default_charset
#
# @example
# require 'hanami/mailer'
#
# configuration = Hanami::Mailer::Configuration.new do |config|
# config.delivery_method = :smtp
# end
#
# configuration = Hanami::Mailer.finalize(configuration)
#
# module Billing
# class InvoiceMailer < Hanami::Mailer
# from 'noreply@example.com'
# to ->(locals) { locals.fetch(:user).email }
# subject ->(locals) { "Invoice number #{locals.fetch(:invoice).number}" }
#
# before do |mail, locals|
# mail.attachments["invoice-#{locals.fetch(:invoice).number}.pdf"] =
# File.read('/path/to/invoice.pdf')
# end
# end
# end
#
# invoice = Invoice.new(number: 23)
# user = User.new(name: 'L', email: 'user@example.com')
#
# mailer = Billing::InvoiceMailer.new(configuration: configuration)
#
# # Deliver both text, HTML parts and the attachment
# mailer.deliver(invoice: invoice, user: user)
#
# # Deliver only the text part and the attachment
# mailer.deliver(invoice: invoice, user: user, format: :txt)
#
# # Deliver only the text part and the attachment
# mailer.deliver(invoice: invoice, user: user, format: :html)
#
# # Deliver both the parts with "iso-8859"
# mailer.deliver(invoice: invoice, user: user, charset: 'iso-8859')
def deliver(locals)
mail(locals).deliver
rescue ArgumentError => exception
raise MissingDeliveryDataError if exception.message =~ /SMTP (From|To) address/
raise
end
# @since next
# @api unstable
alias_method :call, :deliver
# Render a single template with the specified format.
#
# @param format [Symbol] format
#
# @return [String] the output of the rendering process.
#
# @since 0.1.0
# @api unstable
def render(format, locals)
template(format).render(self, locals)
end
private
# @api unstable
# @since next
attr_reader :configuration
# @api unstable
# @since next
def mail(locals)
Mail.new.tap do |mail|
instance_exec(mail, locals, &self.class.before)
bind(mail, locals)
end
end
# @api unstable
# @since next
#
def bind(mail, locals) # rubocop:disable Metrics/AbcSize
charset = locals.fetch(:charset, configuration.default_charset)
mail.return_path = __dsl(:return_path, locals)
mail.from = __dsl(:from, locals)
mail.to = __dsl(:to, locals)
mail.cc = __dsl(:cc, locals)
mail.bcc = __dsl(:bcc, locals)
mail.reply_to = __dsl(:reply_to, locals)
mail.subject = __dsl(:subject, locals)
mail.html_part = __part(:html, charset, locals)
mail.text_part = __part(:txt, charset, locals)
mail.charset = charset
mail.delivery_method(*configuration.delivery_method)
end
# @since next
# @api unstable
def template(format)
configuration.template(self.class, format)
end
# @since 0.1.0
# @api unstable
def __dsl(method_name, locals)
case result = self.class.__send__(method_name)
when Proc
result.call(locals)
else
result
end
end
# @since 0.1.0
# @api unstable
def __part(format, charset, locals)
return unless __part?(format, locals)
Mail::Part.new.tap do |part|
part.content_type = "#{CONTENT_TYPES.fetch(format)}; charset=#{charset}"
part.body = render(format, locals)
end
end
# @since 0.1.0
# @api unstable
def __part?(format, locals)
wanted = locals.fetch(:format, nil)
wanted == format ||
(!wanted && !template(format).nil?)
end
end
end