hanami-mailer/lib/hanami/mailer.rb

346 lines
8.3 KiB
Ruby

# frozen_string_literal: true
require "hanami/utils/class_attribute"
require "hanami/mailer/version"
require "hanami/mailer/configuration"
require "hanami/mailer/dsl"
require "mail"
# Hanami
#
# @since 0.1.0
module Hanami
# Hanami::Mailer
#
# @since 0.1.0
module Mailer
# Base error for Hanami::Mailer
#
# @since 0.1.0
class Error < ::StandardError
end
# Missing delivery data error
#
# It's raised when a mailer doesn't specify <tt>from</tt> or <tt>to</tt>.
#
# @since 0.1.0
class MissingDeliveryDataError < Error
def initialize
super("Missing delivery data, please check 'from', or 'to'")
end
end
# Content types mapping
#
# @since 0.1.0
# @api private
CONTENT_TYPES = {
html: "text/html",
txt: "text/plain"
}.freeze
include Utils::ClassAttribute
# @since 0.1.0
# @api private
class_attribute :configuration
self.configuration = Configuration.new
# Configure the framework.
# It yields the given block in the context of the configuration
#
# @param blk [Proc] the configuration block
#
# @since 0.1.0
#
# @see Hanami::Mailer::Configuration
#
# @example
# require 'hanami/mailer'
#
# Hanami::Mailer.configure do
# root '/path/to/root'
# end
def self.configure(&blk)
configuration.instance_eval(&blk)
self
end
# 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 0.1.0
# @api private
#
# @see http://www.ruby-doc.org/core/Module.html#method-i-included
def self.included(base)
conf = configuration
conf.add_mailer(base)
base.class_eval do
extend Dsl
extend ClassMethods
include Utils::ClassAttribute
class_attribute :configuration
self.configuration = conf.duplicate
end
conf.copy!(base)
end
# Test deliveries
#
# This is a collection of delivered messages, used when <tt>delivery_method</tt>
# is set on <tt>:test</tt>
#
# @return [Array] a collection of delivered messages
#
# @since 0.1.0
#
# @see Hanami::Mailer::Configuration#delivery_mode
#
# @example
# require 'hanami/mailer'
#
# Hanami::Mailer.configure do
# delivery_method :test
# end.load!
#
# # In testing code
# Signup::Welcome.deliver
# Hanami::Mailer.deliveries.count # => 1
def self.deliveries
Mail::TestMailer.deliveries
end
# Load the framework
#
# @since 0.1.0
# @api private
def self.load!
Mail.eager_autoload!
configuration.load!
end
# @since 0.1.0
module ClassMethods
# Delivers a multipart email message.
#
# When a mailer defines a <tt>html</tt> and <tt>txt</tt> template, they are
# both delivered.
#
# In order to selectively deliver only one of the two templates, use
# <tt>Signup::Welcome.deliver(format: :txt)</tt>
#
# All the given locals, excepted the reserved ones (<tt>:format</tt> and
# <tt>charset</tt>), are available as rendering context for the templates.
#
# @param locals [Hash] a set of objects that acts as context for the rendering
# @option :format [Symbol] specify format to deliver
# @option :charset [String] charset
#
# @since 0.1.0
#
# @see Hanami::Mailer::Configuration#default_charset
#
# @example
# require 'hanami/mailer'
#
# Hanami::Mailer.configure do
# delivery_method :smtp
# end.load!
#
# module Billing
# class Invoice
# include Hanami::Mailer
#
# from 'noreply@example.com'
# to :recipient
# subject :subject_line
#
# def prepare
# mail.attachments['invoice.pdf'] = File.read('/path/to/invoice.pdf')
# end
#
# private
#
# def recipient
# user.email
# end
#
# def subject_line
# "Invoice - #{ invoice.number }"
# end
# end
# end
#
# invoice = Invoice.new
# user = User.new(name: 'L', email: 'user@example.com')
#
# # Deliver both text, HTML parts and the attachment
# Billing::Invoice.deliver(invoice: invoice, user: user)
#
# # Deliver only the text part and the attachment
# Billing::Invoice.deliver(invoice: invoice, user: user, format: :txt)
#
# # Deliver only the text part and the attachment
# Billing::Invoice.deliver(invoice: invoice, user: user, format: :html)
#
# # Deliver both the parts with "iso-8859"
# Billing::Invoice.deliver(invoice: invoice, user: user, charset: "iso-8859")
def deliver(locals = {})
new(locals).deliver
end
end
# Initialize a mailer
#
# @param locals [Hash] a set of objects that acts as context for the rendering
# @option :format [Symbol] specify format to deliver
# @option :charset [String] charset
#
# @since 0.1.0
def initialize(locals = {})
@locals = locals
@format = locals.fetch(:format, nil)
@charset = locals.fetch(:charset, self.class.configuration.default_charset)
@mail = build
prepare
end
# 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 private
def render(format)
self.class.templates(format).render(self, @locals)
end
# Delivers a multipart email, by looking at all the associated templates and render them.
#
# @since 0.1.0
# @api private
def deliver
mail.deliver
rescue ArgumentError => exception
raise MissingDeliveryDataError if exception.message =~ /SMTP (From|To) address/
raise
end
protected
# Prepare the email message when a new mailer is initialized.
#
# This is a hook that can be overwritten by mailers.
#
# @since 0.1.0
#
# @example
# require 'hanami/mailer'
#
# module Billing
# class Invoice
# include Hanami::Mailer
#
# subject 'Invoice'
# from 'noreply@example.com'
# to ''
#
# def prepare
# mail.attachments['invoice.pdf'] = File.read('/path/to/invoice.pdf')
# end
#
# private
#
# def recipient
# user.email
# end
# end
# end
#
# invoice = Invoice.new
# user = User.new(name: 'L', email: 'user@example.com')
def prepare
end
# @api private
# @since 0.1.0
def method_missing(method_name)
@locals.fetch(method_name) { super }
end
# @since 0.1.0
attr_reader :mail
# @api private
# @since 0.1.0
attr_reader :charset
private
def build
Mail.new.tap do |m|
m.return_path = __dsl(:return_path)
m.from = __dsl(:from)
m.to = __dsl(:to)
m.cc = __dsl(:cc)
m.bcc = __dsl(:bcc)
m.reply_to = __dsl(:reply_to)
m.subject = __dsl(:subject)
m.charset = charset
m.html_part = __part(:html)
m.text_part = __part(:txt)
m.delivery_method(*Hanami::Mailer.configuration.delivery_method)
end
end
# @api private
# @since 0.1.0
def __dsl(method_name)
case result = self.class.__send__(method_name)
when Symbol
__send__(result)
else
result
end
end
# @api private
# @since 0.1.0
def __part(format)
return unless __part?(format)
Mail::Part.new.tap do |part|
part.content_type = "#{CONTENT_TYPES.fetch(format)}; charset=#{charset}"
part.body = render(format)
end
end
# @api private
# @since 0.1.0
def __part?(format)
@format == format ||
(!@format && !self.class.templates(format).nil?)
end
# @api private
# @since 0.4.0
def respond_to_missing?(method_name, _include_all)
@locals.key?(method_name)
end
end
end