Remove global state. Immutable mailer. (#69)
This commit is contained in:
parent
dded5d2268
commit
d2b8a32ade
|
@ -2,3 +2,7 @@
|
|||
# alphabetically
|
||||
inherit_from:
|
||||
- https://raw.githubusercontent.com/hanami/devtools/master/.rubocop.yml
|
||||
Style/Documentation:
|
||||
Exclude:
|
||||
- "examples/*"
|
||||
- "spec/**/*"
|
||||
|
|
5
Gemfile
5
Gemfile
|
@ -2,8 +2,9 @@ source 'https://rubygems.org'
|
|||
gemspec
|
||||
|
||||
unless ENV['TRAVIS']
|
||||
gem 'byebug', require: false, platforms: :mri
|
||||
gem 'yard', require: false
|
||||
gem 'byebug', require: false, platforms: :mri
|
||||
gem 'allocation_stats', require: false
|
||||
gem 'benchmark-ips', require: false
|
||||
end
|
||||
|
||||
gem 'hanami-utils', '2.0.0.alpha1', require: false, git: 'https://github.com/hanami/utils.git', branch: 'unstable'
|
||||
|
|
155
README.md
155
README.md
|
@ -44,10 +44,10 @@ Or install it yourself as:
|
|||
|
||||
### Conventions
|
||||
|
||||
* Templates are searched under `Hanami::Mailer.configuration.root`, set this value according to your app structure (eg. `"app/templates"`).
|
||||
* Templates are searched under `Hanami::Mailer::Configuration#root`, set this value according to your app structure (eg. `"app/templates"`).
|
||||
* A mailer will look for a template with a file name that is composed by its full class name (eg. `"articles/index"`).
|
||||
* A template must have two concatenated extensions: one for the format and one for the engine (eg. `".html.erb"`).
|
||||
* The framework must be loaded before rendering the first time: `Hanami::Mailer.load!`.
|
||||
* The framework must be loaded before rendering the first time: `Hanami::Mailer.finalize(configuration)`.
|
||||
|
||||
### Mailers
|
||||
|
||||
|
@ -55,10 +55,60 @@ A simple mailer looks like this:
|
|||
|
||||
```ruby
|
||||
require 'hanami/mailer'
|
||||
require 'ostruct'
|
||||
|
||||
class InvoiceMailer
|
||||
include Hanami::Mailer
|
||||
# Create two files: `invoice.html.erb` and `invoice.txt.erb`
|
||||
|
||||
configuration = Hanami::Mailer::Configuration.new do |config|
|
||||
config.delivery_method = :test
|
||||
end
|
||||
|
||||
class Invoice < Hanami::Mailer
|
||||
from "noreply@example.com"
|
||||
to ->(locals) { locals.fetch(:user).email }
|
||||
end
|
||||
|
||||
configuration = Hanami::Mailer.finalize(configuration)
|
||||
|
||||
invoice = OpenStruct.new(number: 23)
|
||||
mailer = InvoiceMailer.new(configuration: configuration)
|
||||
mail = mailer.deliver(invoice: invoice)
|
||||
|
||||
mail
|
||||
# => #<Mail::Message:70303354246540, Multipart: true, Headers: <Date: Wed, 22 Mar 2017 11:48:57 +0100>, <From: noreply@example.com>, <To: user@example.com>, <Cc: >, <Bcc: >, <Message-ID: <58d25699e47f9_b4e13ff0c503e4f4632e6@escher.mail>>, <Subject: >, <Mime-Version: 1.0>, <Content-Type: multipart/alternative; boundary=--==_mimepart_58d25699e42d2_b4e13ff0c503e4f463186>, <Content-Transfer-Encoding: 7bit>>
|
||||
|
||||
mail.to_s
|
||||
# =>
|
||||
# From: noreply@example.com
|
||||
# To: user@example.com
|
||||
# Message-ID: <58d25699e47f9_b4e13ff0c503e4f4632e6@escher.mail>
|
||||
# Subject:
|
||||
# Mime-Version: 1.0
|
||||
# Content-Type: multipart/alternative;
|
||||
# boundary="--==_mimepart_58d25699e42d2_b4e13ff0c503e4f463186";
|
||||
# charset=UTF-8
|
||||
# Content-Transfer-Encoding: 7bit
|
||||
#
|
||||
#
|
||||
# ----==_mimepart_58d25699e42d2_b4e13ff0c503e4f463186
|
||||
# Content-Type: text/plain;
|
||||
# charset=UTF-8
|
||||
# Content-Transfer-Encoding: 7bit
|
||||
#
|
||||
# Invoice #23
|
||||
#
|
||||
# ----==_mimepart_58d25699e42d2_b4e13ff0c503e4f463186
|
||||
# Content-Type: text/html;
|
||||
# charset=UTF-8
|
||||
# Content-Transfer-Encoding: 7bit
|
||||
#
|
||||
# <html>
|
||||
# <body>
|
||||
# <h1>Invoice template</h1>
|
||||
# </body>
|
||||
# </html>
|
||||
#
|
||||
# ----==_mimepart_58d25699e42d2_b4e13ff0c503e4f463186--
|
||||
```
|
||||
|
||||
A mailer with `.to` and `.from` addresses and mailer delivery:
|
||||
|
@ -66,20 +116,18 @@ A mailer with `.to` and `.from` addresses and mailer delivery:
|
|||
```ruby
|
||||
require 'hanami/mailer'
|
||||
|
||||
Hanami::Mailer.configure do
|
||||
delivery_method :smtp,
|
||||
address: "smtp.gmail.com",
|
||||
port: 587,
|
||||
domain: "example.com",
|
||||
user_name: ENV['SMTP_USERNAME'],
|
||||
password: ENV['SMTP_PASSWORD'],
|
||||
authentication: "plain",
|
||||
enable_starttls_auto: true
|
||||
end.load!
|
||||
|
||||
class WelcomeMailer
|
||||
include Hanami::Mailer
|
||||
configuration = Hanami::Mailer::Configuration.new do |config|
|
||||
config.delivery_method = :smtp,
|
||||
address: "smtp.gmail.com",
|
||||
port: 587,
|
||||
domain: "example.com",
|
||||
user_name: ENV['SMTP_USERNAME'],
|
||||
password: ENV['SMTP_PASSWORD'],
|
||||
authentication: "plain",
|
||||
enable_starttls_auto: true
|
||||
end
|
||||
|
||||
class WelcomeMailer < Hanami::Mailer
|
||||
from 'noreply@sender.com'
|
||||
to 'noreply@recipient.com'
|
||||
cc 'cc@sender.com'
|
||||
|
@ -88,7 +136,7 @@ class WelcomeMailer
|
|||
subject 'Welcome'
|
||||
end
|
||||
|
||||
WelcomeMailer.deliver
|
||||
WelcomeMailer.new(configuration: configuration).call(locals)
|
||||
```
|
||||
|
||||
### Locals
|
||||
|
@ -97,25 +145,17 @@ The set of objects passed in the `deliver` call are called `locals` and are avai
|
|||
|
||||
```ruby
|
||||
require 'hanami/mailer'
|
||||
require 'ostruct'
|
||||
|
||||
User = Struct.new(:name, :username, :email)
|
||||
luca = User.new('Luca', 'jodosha', 'luca@jodosha.com')
|
||||
|
||||
class WelcomeMailer
|
||||
include Hanami::Mailer
|
||||
user = OpenStruct.new(name: Luca', email: 'user@hanamirb.org')
|
||||
|
||||
class WelcomeMailer < Hanami::Mailer
|
||||
from 'noreply@sender.com'
|
||||
subject 'Welcome'
|
||||
to :recipient
|
||||
|
||||
private
|
||||
|
||||
def recipient
|
||||
user.email
|
||||
end
|
||||
to ->(locals) { locals.fetch(:user).email }
|
||||
end
|
||||
|
||||
WelcomeMailer.deliver(user: luca)
|
||||
WelcomeMailer.new(configuration: configuration).deliver(user: luca)
|
||||
```
|
||||
|
||||
The corresponding `erb` file:
|
||||
|
@ -131,9 +171,7 @@ All public methods defined in the mailer are accessible from the template:
|
|||
```ruby
|
||||
require 'hanami/mailer'
|
||||
|
||||
class WelcomeMailer
|
||||
include Hanami::Mailer
|
||||
|
||||
class WelcomeMailer < Hanami::Mailer
|
||||
from 'noreply@sender.com'
|
||||
to 'noreply@recipient.com'
|
||||
subject 'Welcome'
|
||||
|
@ -154,7 +192,7 @@ The template file must be located under the relevant `root` and must match the i
|
|||
|
||||
```ruby
|
||||
# Given this root
|
||||
Hanami::Mailer.configuration.root # => #<Pathname:app/templates>
|
||||
configuration.root # => #<Pathname:app/templates>
|
||||
|
||||
# For InvoiceMailer, it looks for:
|
||||
# * app/templates/invoice_mailer.html.erb
|
||||
|
@ -164,9 +202,7 @@ Hanami::Mailer.configuration.root # => #<Pathname:app/templates>
|
|||
If we want to specify a different template, we can do:
|
||||
|
||||
```ruby
|
||||
class InvoiceMailer
|
||||
include Hanami::Mailer
|
||||
|
||||
class InvoiceMailer < Hanami::Mailer
|
||||
template 'invoice'
|
||||
end
|
||||
|
||||
|
@ -311,21 +347,22 @@ It supports a few options:
|
|||
```ruby
|
||||
require 'hanami/mailer'
|
||||
|
||||
Hanami::Maler.configure do
|
||||
configuration = Hanami::Mailer::Configuration.new do |config|
|
||||
# Set the root path where to search for templates
|
||||
# Argument: String, Pathname, #to_pathname, defaults to the current directory
|
||||
#
|
||||
root '/path/to/root'
|
||||
config.root = 'path/to/root'
|
||||
|
||||
# Set the default charset for emails
|
||||
# Argument: String, defaults to "UTF-8"
|
||||
#
|
||||
default_charset 'iso-8859'
|
||||
config.default_charset = 'iso-8859'
|
||||
|
||||
# Set the delivery method
|
||||
# Argument: Symbol
|
||||
# Argument: Hash, optional configurations
|
||||
delivery_method :stmp
|
||||
config.delivery_method = :stmp
|
||||
end
|
||||
```
|
||||
|
||||
### Attachments
|
||||
|
@ -333,14 +370,10 @@ Hanami::Maler.configure do
|
|||
Attachments can be added with the following API:
|
||||
|
||||
```ruby
|
||||
class InvoiceMailer
|
||||
include Hanami::Mailer
|
||||
class InvoiceMailer < Hanami::Mailer
|
||||
# ...
|
||||
|
||||
def prepare
|
||||
mail.attachments['invoice.pdf'] = '/path/to/invoice.pdf'
|
||||
# or
|
||||
mail.attachments['invoice.pdf'] = File.read('/path/to/invoice.pdf')
|
||||
before do |mail, locals|
|
||||
mail.attachments["invoice-#{locals.fetch(:invoice).number}.pdf"] = 'path/to/invoice.pdf'
|
||||
end
|
||||
end
|
||||
```
|
||||
|
@ -350,14 +383,14 @@ end
|
|||
The global delivery method is defined through the __Hanami::Mailer__ configuration, as:
|
||||
|
||||
```ruby
|
||||
Hanami::Mailer.configuration do
|
||||
delivery_method :smtp
|
||||
configuration = Hanami::Mailer::Configuration.new do |config|
|
||||
config.delivery_method = :smtp
|
||||
end
|
||||
```
|
||||
|
||||
```ruby
|
||||
Hanami::Mailer.configuration do
|
||||
delivery_method :smtp, address: "localhost", port: 1025
|
||||
configuration = Hanami::Mailer::Configuration.new do |config|
|
||||
config.delivery_method = :smtp, { address: "localhost", port: 1025 }
|
||||
end
|
||||
```
|
||||
|
||||
|
@ -386,14 +419,14 @@ class MandrillDeliveryMethod
|
|||
end
|
||||
end
|
||||
|
||||
Hanami::Mailer.configure do
|
||||
delivery_method MandrillDeliveryMethod,
|
||||
username: ENV['MANDRILL_USERNAME'],
|
||||
password: ENV['MANDRILL_API_KEY']
|
||||
end.load!
|
||||
configuration = Hanami::Mailer::Configuration.new do |config|
|
||||
config.delivery_method = MandrillDeliveryMethod,
|
||||
username: ENV['MANDRILL_USERNAME'],
|
||||
password: ENV['MANDRILL_API_KEY']
|
||||
end
|
||||
```
|
||||
|
||||
The class passed to `.delivery_method` must accept an optional set of options
|
||||
The class passed to `.delivery_method=` must accept an optional set of options
|
||||
with the constructor (`#initialize`) and respond to `#deliver!`.
|
||||
|
||||
### Multipart Delivery
|
||||
|
@ -402,8 +435,8 @@ All the email are sent as multipart messages by default.
|
|||
For a given mailer, the framework looks up for associated text (`.txt`) and `HTML` (`.html`) templates and render them.
|
||||
|
||||
```ruby
|
||||
InvoiceMailer.deliver # delivers both text and html templates
|
||||
InvoiceMailer.deliver(format: :txt) # delivers only text template
|
||||
InvoiceMailer.new(configuration: configuration).deliver({}) # delivers both text and html templates
|
||||
InvoiceMailer.new(configuration: configuration).deliver(format: :txt) # delivers only text template
|
||||
```
|
||||
|
||||
Please note that **they aren't both mandatory, but at least one of them MUST** be present.
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'bundler/setup'
|
||||
require 'hanami/mailer'
|
||||
require 'benchmark/ips'
|
||||
require 'allocation_stats'
|
||||
require_relative './examples/base'
|
||||
|
||||
configuration = Hanami::Mailer::Configuration.new do |config|
|
||||
config.root = "examples/base"
|
||||
config.delivery_method = :test
|
||||
end
|
||||
|
||||
configuration = Hanami::Mailer.finalize(configuration)
|
||||
|
||||
invoice = Invoice.new(1, 23)
|
||||
user = User.new("Luca", "luca@domain.test")
|
||||
|
||||
mailer = InvoiceMailer.new(configuration: configuration)
|
||||
|
||||
Benchmark.ips do |x|
|
||||
# # Configure the number of seconds used during
|
||||
# # the warmup phase (default 2) and calculation phase (default 5)
|
||||
# x.config(time: 5, warmup: 2)
|
||||
x.report "deliver" do
|
||||
mailer.deliver(invoice: invoice, user: user)
|
||||
end
|
||||
end
|
||||
|
||||
stats = AllocationStats.new(burn: 5).trace do
|
||||
1_000.times do
|
||||
mailer.deliver(invoice: invoice, user: user)
|
||||
end
|
||||
end
|
||||
|
||||
total_allocations = stats.allocations.all.size
|
||||
puts "total allocations: #{total_allocations}"
|
||||
|
||||
total_memsize = stats.allocations.bytes.to_a.inject(&:+)
|
||||
puts "total memsize: #{total_memsize}"
|
||||
|
||||
detailed_allocations = stats.allocations(alias_paths: true)
|
||||
.group_by(:sourcefile, :class_plus)
|
||||
.sort_by_count
|
||||
.to_text
|
||||
|
||||
puts 'allocations by source file and class:'
|
||||
puts detailed_allocations
|
|
@ -1,3 +0,0 @@
|
|||
---
|
||||
threshold: 10
|
||||
total_score: 77
|
|
@ -1,2 +1,2 @@
|
|||
---
|
||||
threshold: 20.2
|
||||
threshold: 18.4
|
||||
|
|
|
@ -25,12 +25,12 @@ DuplicateMethodCall:
|
|||
FeatureEnvy:
|
||||
enabled: true
|
||||
exclude:
|
||||
- Hanami::Mailer#build
|
||||
- Hanami::Mailer#__part?
|
||||
LongParameterList:
|
||||
enabled: true
|
||||
exclude:
|
||||
- Devtools::Config#self.attribute
|
||||
max_params: 2
|
||||
max_params: 4
|
||||
overrides: {}
|
||||
LongYieldList:
|
||||
enabled: true
|
||||
|
@ -44,28 +44,23 @@ NestedIterators:
|
|||
NilCheck:
|
||||
enabled: true
|
||||
exclude:
|
||||
- Hanami::Mailer::Configuration#default_charset
|
||||
- Hanami::Mailer::Configuration#delivery_method
|
||||
- Hanami::Mailer::Dsl#bcc
|
||||
- Hanami::Mailer::Dsl#cc
|
||||
- Hanami::Mailer::Dsl#from
|
||||
- Hanami::Mailer::Dsl#subject
|
||||
- Hanami::Mailer::Dsl#template
|
||||
- Hanami::Mailer::Dsl#templates
|
||||
- Hanami::Mailer::Dsl#to
|
||||
- Hanami::Mailer#__part?
|
||||
RepeatedConditional:
|
||||
enabled: true
|
||||
max_ifs: 1
|
||||
exclude:
|
||||
- Hanami::Mailer::Configuration
|
||||
exclude: []
|
||||
TooManyConstants:
|
||||
enabled: true
|
||||
exclude:
|
||||
- Devtools
|
||||
TooManyInstanceVariables:
|
||||
enabled: true
|
||||
max_instance_variables: 2
|
||||
max_instance_variables: 3
|
||||
exclude:
|
||||
- Hanami::Mailer::Configuration
|
||||
TooManyMethods:
|
||||
|
@ -76,12 +71,10 @@ TooManyStatements:
|
|||
enabled: true
|
||||
max_statements: 5
|
||||
exclude:
|
||||
- initialize
|
||||
- Hanami::Mailer::Configuration#duplicate
|
||||
- Hanami::Mailer::Dsl#self.extended
|
||||
- Hanami::Mailer::Rendering::TemplatesFinder#find
|
||||
- Hanami::Mailer#build
|
||||
- Hanami::Mailer#self.included
|
||||
- Hanami::Mailer#bind
|
||||
- Hanami::Mailer::Configuration#initialize
|
||||
- Hanami::Mailer::Dsl#self.extended
|
||||
- Hanami::Mailer::TemplatesFinder#find
|
||||
UncommunicativeMethodName:
|
||||
enabled: true
|
||||
reject:
|
||||
|
@ -104,9 +97,7 @@ UncommunicativeParameterName:
|
|||
- !ruby/regexp /[0-9]$/
|
||||
- !ruby/regexp /[A-Z]/
|
||||
accept: []
|
||||
exclude:
|
||||
- Hanami::Mailer#method_missing
|
||||
- Hanami::Mailer#respond_to_missing?
|
||||
exclude: []
|
||||
UncommunicativeVariableName:
|
||||
enabled: true
|
||||
reject:
|
||||
|
@ -114,10 +105,7 @@ UncommunicativeVariableName:
|
|||
- !ruby/regexp /[0-9]$/
|
||||
- !ruby/regexp /[A-Z]/
|
||||
accept: []
|
||||
exclude:
|
||||
- Hanami::Mailer::Configuration#duplicate
|
||||
- Hanami::Mailer::Configuration#load!
|
||||
- Hanami::Mailer#build
|
||||
exclude: []
|
||||
UnusedParameters:
|
||||
enabled: true
|
||||
exclude: []
|
||||
|
@ -125,15 +113,10 @@ UtilityFunction:
|
|||
enabled: true
|
||||
exclude:
|
||||
- Devtools::Project::Initializer::Rspec#require_files # intentional for deduplication
|
||||
- Hanami::Mailer::Rendering::TemplateName#tokens
|
||||
max_helper_calls: 0
|
||||
PrimaDonnaMethod:
|
||||
exclude:
|
||||
- Hanami::Mailer::Configuration
|
||||
- Hanami::Mailer::Rendering::TemplateName
|
||||
exclude: []
|
||||
ModuleInitialize:
|
||||
exclude:
|
||||
- Hanami::Mailer
|
||||
exclude: []
|
||||
InstanceVariableAssumption:
|
||||
exclude:
|
||||
- Hanami::Mailer::Configuration
|
||||
exclude: []
|
||||
|
|
|
@ -1,97 +0,0 @@
|
|||
inherit_from:
|
||||
- https://raw.githubusercontent.com/hanami/hanami/master/.rubocop.yml
|
||||
|
||||
Metrics/BlockLength:
|
||||
Exclude:
|
||||
# Ignore RSpec DSL
|
||||
- spec/**/*
|
||||
|
||||
# Avoid parameter lists longer than four parameters.
|
||||
ParameterLists:
|
||||
Max: 4
|
||||
CountKeywordArgs: true
|
||||
|
||||
# Avoid more than `Max` levels of nesting.
|
||||
BlockNesting:
|
||||
Max: 3
|
||||
|
||||
# Align with the style guide.
|
||||
CollectionMethods:
|
||||
PreferredMethods:
|
||||
collect: 'map'
|
||||
inject: 'reduce'
|
||||
find: 'detect'
|
||||
find_all: 'select'
|
||||
|
||||
# Disable documentation checking until a class needs to be documented once
|
||||
Documentation:
|
||||
Enabled: false
|
||||
|
||||
# Do not favor modifier if/unless usage when you have a single-line body
|
||||
IfUnlessModifier:
|
||||
Enabled: false
|
||||
|
||||
# Allow case equality operator (in limited use within the specs)
|
||||
CaseEquality:
|
||||
Enabled: false
|
||||
|
||||
# Constants do not always have to use SCREAMING_SNAKE_CASE
|
||||
ConstantName:
|
||||
Enabled: false
|
||||
|
||||
# Not all trivial readers/writers can be defined with attr_* methods
|
||||
TrivialAccessors:
|
||||
Enabled: false
|
||||
|
||||
# Allow empty lines around class body
|
||||
EmptyLinesAroundClassBody:
|
||||
Enabled: false
|
||||
|
||||
# Allow empty lines around module body
|
||||
EmptyLinesAroundModuleBody:
|
||||
Enabled: false
|
||||
|
||||
# Allow empty lines around block body
|
||||
EmptyLinesAroundBlockBody:
|
||||
Enabled: false
|
||||
|
||||
# Allow multiple line operations to not require indentation
|
||||
MultilineOperationIndentation:
|
||||
Enabled: false
|
||||
|
||||
# Prefer String#% over Kernel#sprintf
|
||||
FormatString:
|
||||
Enabled: false
|
||||
|
||||
# Align if/else blocks with the variable assignment
|
||||
EndAlignment:
|
||||
EnforcedStyleAlignWith: variable
|
||||
|
||||
# Do not always align parameters when it is easier to read
|
||||
AlignParameters:
|
||||
Exclude:
|
||||
- spec/**/*_spec.rb
|
||||
|
||||
# Prefer #kind_of? over #is_a?
|
||||
ClassCheck:
|
||||
EnforcedStyle: kind_of?
|
||||
|
||||
# Do not prefer double quotes to be used when %q or %Q is more appropriate
|
||||
UnneededPercentQ:
|
||||
Enabled: false
|
||||
|
||||
# Do not prefer lambda.call(...) over lambda.(...)
|
||||
LambdaCall:
|
||||
Enabled: false
|
||||
|
||||
# Allow additional spaces
|
||||
ExtraSpacing:
|
||||
Enabled: false
|
||||
|
||||
# All objects can still be mutated if their eigenclass is patched
|
||||
RedundantFreeze:
|
||||
Enabled: false
|
||||
|
||||
# Prefer using `fail` when raising and `raise` when reraising
|
||||
SignalException:
|
||||
Enabled: false
|
|
@ -0,0 +1,46 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'bundler/setup'
|
||||
require 'hanami/mailer'
|
||||
|
||||
configuration = Hanami::Mailer::Configuration.new do |config|
|
||||
config.root = File.expand_path(__dir__, "base")
|
||||
config.delivery_method = :test
|
||||
end
|
||||
|
||||
class Invoice
|
||||
attr_reader :id, :number
|
||||
|
||||
def initialize(id, number)
|
||||
@id = id
|
||||
@number = number
|
||||
freeze
|
||||
end
|
||||
end
|
||||
|
||||
class User
|
||||
attr_reader :name, :email
|
||||
|
||||
def initialize(name, email)
|
||||
@name = name
|
||||
@email = email
|
||||
freeze
|
||||
end
|
||||
end
|
||||
|
||||
class InvoiceMailer < Hanami::Mailer
|
||||
template "invoice"
|
||||
|
||||
from "invoices@domain.test"
|
||||
to ->(locals) { locals.fetch(:user).email }
|
||||
|
||||
subject ->(locals) { "Invoice ##{locals.fetch(:invoice).number}" }
|
||||
end
|
||||
|
||||
configuration = Hanami::Mailer.finalize(configuration)
|
||||
|
||||
invoice = Invoice.new(1, 23)
|
||||
user = User.new("Luca", "luca@domain.test")
|
||||
|
||||
mailer = InvoiceMailer.new(configuration: configuration)
|
||||
puts mailer.deliver(invoice: invoice, user: user)
|
|
@ -0,0 +1,8 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>Invoice</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Invoice #<%= invoice.number %></h1>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1 @@
|
|||
Invoice #<%= invoice.number %>
|
|
@ -25,5 +25,5 @@ Gem::Specification.new do |spec|
|
|||
|
||||
spec.add_development_dependency 'bundler', '~> 1.15'
|
||||
spec.add_development_dependency 'rake', '~> 12'
|
||||
spec.add_development_dependency 'rspec', '~> 3.5'
|
||||
spec.add_development_dependency 'rspec', '~> 3.7'
|
||||
end
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
require 'hanami/mailer' # rubocop:disable Naming/FileName
|
|
@ -1,8 +1,7 @@
|
|||
require 'hanami/utils/class_attribute'
|
||||
require 'hanami/mailer/version'
|
||||
require 'hanami/mailer/configuration'
|
||||
require 'hanami/mailer/dsl'
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'mail'
|
||||
require 'concurrent'
|
||||
|
||||
# Hanami
|
||||
#
|
||||
|
@ -11,23 +10,12 @@ 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
|
||||
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
|
||||
#
|
||||
|
@ -38,173 +26,156 @@ module Hanami
|
|||
txt: 'text/plain'
|
||||
}.freeze
|
||||
|
||||
include Utils::ClassAttribute
|
||||
private_constant(:CONTENT_TYPES)
|
||||
|
||||
# @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
|
||||
# Base error for Hanami::Mailer
|
||||
#
|
||||
# @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
|
||||
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 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)
|
||||
# @since next
|
||||
# @api unstable
|
||||
def self.inherited(base)
|
||||
@_subclasses.push(base)
|
||||
base.extend Dsl
|
||||
end
|
||||
|
||||
# Test deliveries
|
||||
private_class_method :inherited
|
||||
|
||||
# Finalize the configuration
|
||||
#
|
||||
# This is a collection of delivered messages, used when <tt>delivery_method</tt>
|
||||
# is set on <tt>:test</tt>
|
||||
# This should be used before to start to use the mailers
|
||||
#
|
||||
# @return [Array] a collection of delivered messages
|
||||
# @param configuration [Hanami::Mailer::Configuration] the configuration to
|
||||
# finalize
|
||||
#
|
||||
# @since 0.1.0
|
||||
# @return [Hanami::Mailer::Configuration] the finalized configuration
|
||||
#
|
||||
# @see Hanami::Mailer::Configuration#delivery_mode
|
||||
# @since next
|
||||
# @api unstable
|
||||
#
|
||||
# @example
|
||||
# require 'hanami/mailer'
|
||||
#
|
||||
# Hanami::Mailer.configure do
|
||||
# delivery_method :test
|
||||
# end.load!
|
||||
# configuration = Hanami::Mailer::Configuration.new do |config|
|
||||
# # ...
|
||||
# end
|
||||
#
|
||||
# # 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')
|
||||
#
|
||||
# Billing::Invoice.deliver(invoice: invoice, user: user) # Deliver both text, HTML parts 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 only the text part and the attachment
|
||||
# Billing::Invoice.deliver(invoice: invoice, user: user, charset: 'iso-8859') # Deliver both the parts with "iso-8859"
|
||||
def deliver(locals = {})
|
||||
new(locals).deliver
|
||||
end
|
||||
# configuration = Hanami::Mailer.finalize(configuration)
|
||||
# MyMailer.new(configuration: configuration)
|
||||
def self.finalize(configuration)
|
||||
Finalizer.finalize(@_subclasses, configuration)
|
||||
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
|
||||
# @param configuration [Hanami::Mailer::Configuration] the configuration
|
||||
# @return [Hanami::Mailer]
|
||||
#
|
||||
# @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
|
||||
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
|
||||
raise MissingDeliveryDataError
|
||||
end
|
||||
|
||||
# @since next
|
||||
# @api unstable
|
||||
alias call deliver
|
||||
|
||||
# Render a single template with the specified format.
|
||||
#
|
||||
# @param format [Symbol] format
|
||||
|
@ -212,127 +183,78 @@ module Hanami
|
|||
# @return [String] the output of the rendering process.
|
||||
#
|
||||
# @since 0.1.0
|
||||
# @api private
|
||||
def render(format)
|
||||
self.class.templates(format).render(self, @locals)
|
||||
# @api unstable
|
||||
def render(format, locals)
|
||||
template(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(m)
|
||||
@locals.fetch(m) { super }
|
||||
end
|
||||
|
||||
# @since 0.1.0
|
||||
attr_reader :mail
|
||||
|
||||
# @api private
|
||||
# @since 0.1.0
|
||||
attr_reader :charset
|
||||
|
||||
private
|
||||
|
||||
# rubocop:disable Metrics/MethodLength
|
||||
# rubocop:disable Metrics/AbcSize
|
||||
# @api private
|
||||
def build
|
||||
Mail.new.tap do |m|
|
||||
m.from = __dsl(:from)
|
||||
m.to = __dsl(:to)
|
||||
m.cc = __dsl(:cc)
|
||||
m.bcc = __dsl(:bcc)
|
||||
m.subject = __dsl(:subject)
|
||||
# @api unstable
|
||||
# @since next
|
||||
attr_reader :configuration
|
||||
|
||||
m.charset = charset
|
||||
m.html_part = __part(:html)
|
||||
m.text_part = __part(:txt)
|
||||
|
||||
m.delivery_method(*Hanami::Mailer.configuration.delivery_method)
|
||||
# @api unstable
|
||||
# @since next
|
||||
def mail(locals)
|
||||
Mail.new.tap do |mail|
|
||||
instance_exec(mail, locals, &self.class.before)
|
||||
bind(mail, locals)
|
||||
end
|
||||
end
|
||||
# rubocop:enable Metrics/MethodLength
|
||||
# rubocop:enable Metrics/AbcSize
|
||||
|
||||
# @api private
|
||||
# @api unstable
|
||||
# @since next
|
||||
def bind(mail, locals) # rubocop:disable Metrics/AbcSize
|
||||
charset = locals.fetch(:charset, configuration.default_charset)
|
||||
|
||||
mail.from = __dsl(:from, locals)
|
||||
mail.to = __dsl(:to, locals)
|
||||
mail.cc = __dsl(:cc, locals)
|
||||
mail.bcc = __dsl(:bcc, 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
|
||||
def __dsl(method_name)
|
||||
# @api unstable
|
||||
def __dsl(method_name, locals)
|
||||
case result = self.class.__send__(method_name)
|
||||
when Symbol
|
||||
__send__(result)
|
||||
when Proc
|
||||
result.call(locals)
|
||||
else
|
||||
result
|
||||
end
|
||||
end
|
||||
|
||||
# @api private
|
||||
# @since 0.1.0
|
||||
def __part(format)
|
||||
return unless __part?(format)
|
||||
# @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)
|
||||
part.body = render(format, locals)
|
||||
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?(m, _include_all)
|
||||
@locals.key?(m)
|
||||
# @api unstable
|
||||
def __part?(format, locals)
|
||||
wanted = locals.fetch(:format, nil)
|
||||
wanted == format ||
|
||||
(!wanted && !template(format).nil?)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
require 'set'
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'hanami/utils/kernel'
|
||||
require 'hanami/mailer/template_name'
|
||||
require 'hanami/mailer/templates_finder'
|
||||
|
||||
module Hanami
|
||||
module Mailer
|
||||
class Mailer
|
||||
# Framework configuration
|
||||
#
|
||||
# @since 0.1.0
|
||||
|
@ -11,7 +14,7 @@ module Hanami
|
|||
#
|
||||
# @since 0.1.0
|
||||
# @api private
|
||||
DEFAULT_ROOT = '.'.freeze
|
||||
DEFAULT_ROOT = '.'
|
||||
|
||||
# Default delivery method
|
||||
#
|
||||
|
@ -23,24 +26,33 @@ module Hanami
|
|||
#
|
||||
# @since 0.1.0
|
||||
# @api private
|
||||
DEFAULT_CHARSET = 'UTF-8'.freeze
|
||||
DEFAULT_CHARSET = 'UTF-8'
|
||||
|
||||
# @since 0.1.0
|
||||
# @api private
|
||||
attr_reader :mailers
|
||||
|
||||
# @since 0.1.0
|
||||
# @api private
|
||||
attr_reader :modules
|
||||
private_constant(*constants(false))
|
||||
|
||||
# Initialize a configuration instance
|
||||
#
|
||||
# @yield [config] the new initialized configuration instance
|
||||
# @return [Hanami::Mailer::Configuration] a new configuration's instance
|
||||
#
|
||||
# @since 0.1.0
|
||||
#
|
||||
# @example Basic Usage
|
||||
# require 'hanami/mailer'
|
||||
#
|
||||
# configuration = Hanami::Mailer::Configuration.new do |config|
|
||||
# config.delivery_method :smtp, ...
|
||||
# end
|
||||
def initialize
|
||||
@namespace = Object
|
||||
reset!
|
||||
@mailers = {}
|
||||
|
||||
self.namespace = Object
|
||||
self.root = DEFAULT_ROOT
|
||||
self.delivery_method = DEFAULT_DELIVERY_METHOD
|
||||
self.default_charset = DEFAULT_CHARSET
|
||||
|
||||
yield(self) if block_given?
|
||||
@finder = TemplatesFinder.new(root)
|
||||
end
|
||||
|
||||
# Set the Ruby namespace where to lookup for mailers.
|
||||
|
@ -49,91 +61,52 @@ module Hanami
|
|||
# that if a `MyApp` wants a `Mailers::Signup` mailer, we are loading the
|
||||
# right one.
|
||||
#
|
||||
# If not set, this value defaults to `Object`.
|
||||
# @!attribute namespace
|
||||
# @return [Class,Module,String] the Ruby namespace where the mailers
|
||||
# are located
|
||||
#
|
||||
# This is part of a DSL, for this reason when this method is called with
|
||||
# an argument, it will set the corresponding instance variable. When
|
||||
# called without, it will return the already set value, or the default.
|
||||
# @since next
|
||||
# @api unstable
|
||||
#
|
||||
# @overload namespace(value)
|
||||
# Sets the given value
|
||||
# @param value [Class, Module, String] a valid Ruby namespace identifier
|
||||
#
|
||||
# @overload namespace
|
||||
# Gets the value
|
||||
# @return [Class, Module, String]
|
||||
#
|
||||
# @api private
|
||||
# @since 0.1.0
|
||||
#
|
||||
# @example Getting the value
|
||||
# @example
|
||||
# require 'hanami/mailer'
|
||||
#
|
||||
# Hanami::Mailer.configuration.namespace # => Object
|
||||
#
|
||||
# @example Setting the value
|
||||
# require 'hanami/mailer'
|
||||
#
|
||||
# Hanami::Mailer.configure do
|
||||
# namespace 'MyApp::Mailers'
|
||||
# Hanami::Mailer::Configuration.new do |config|
|
||||
# config.namespace = MyApp::Mailers
|
||||
# end
|
||||
def namespace(value = nil)
|
||||
if value
|
||||
@namespace = value
|
||||
else
|
||||
@namespace
|
||||
end
|
||||
end
|
||||
attr_accessor :namespace
|
||||
|
||||
# Set the root path where to search for templates
|
||||
#
|
||||
# If not set, this value defaults to the current directory.
|
||||
#
|
||||
# When this method is called with an argument, it will set the corresponding instance variable.
|
||||
# When called without, it will return the already set value, or the default.
|
||||
# @param value [String, Pathname] the root path for mailer templates
|
||||
#
|
||||
# @overload root(value)
|
||||
# Sets the given value
|
||||
# @param value [String, Pathname, #to_pathname] an object that can be
|
||||
# coerced to Pathname
|
||||
# @raise [Errno::ENOENT] if the path doesn't exist
|
||||
#
|
||||
# @overload root
|
||||
# Gets the value
|
||||
# @return [Pathname]
|
||||
#
|
||||
# @since 0.1.0
|
||||
# @since next
|
||||
# @api unstable
|
||||
#
|
||||
# @see http://www.ruby-doc.org/stdlib/libdoc/pathname/rdoc/Pathname.html
|
||||
# @see http://rdoc.info/gems/hanami-utils/Hanami/Utils/Kernel#Pathname-class_method
|
||||
#
|
||||
# @example Getting the value
|
||||
# @example
|
||||
# require 'hanami/mailer'
|
||||
#
|
||||
# Hanami::Mailer.configuration.root # => #<Pathname:.>
|
||||
#
|
||||
# @example Setting the value
|
||||
# require 'hanami/mailer'
|
||||
#
|
||||
# Hanami::Mailer.configure do
|
||||
# root '/path/to/templates'
|
||||
# Hanami::Mailer::Configuration.new do |config|
|
||||
# config.root = 'path/to/templates'
|
||||
# end
|
||||
#
|
||||
# Hanami::Mailer.configuration.root # => #<Pathname:/path/to/templates>
|
||||
def root(value = nil)
|
||||
if value
|
||||
@root = Utils::Kernel.Pathname(value).realpath
|
||||
else
|
||||
@root
|
||||
end
|
||||
def root=(value)
|
||||
@root = Utils::Kernel.Pathname(value).realpath
|
||||
end
|
||||
|
||||
# Prepare the mailers.
|
||||
#
|
||||
# The given block will be yielded when `Hanami::Mailer` will be included by
|
||||
# a mailer.
|
||||
#
|
||||
# This method can be called multiple times.
|
||||
# @!attribute [r] root
|
||||
# @return [Pathname] the root path for mailer templates
|
||||
#
|
||||
# @since next
|
||||
# @api unstable
|
||||
attr_reader :root
|
||||
|
||||
# @param blk [Proc] the code block
|
||||
#
|
||||
# @return [void]
|
||||
|
@ -144,19 +117,8 @@ module Hanami
|
|||
#
|
||||
# @see Hanami::Mailer.configure
|
||||
def prepare(&blk)
|
||||
if block_given? # rubocop:disable Style/GuardClause
|
||||
@modules.push(blk)
|
||||
else
|
||||
raise ArgumentError.new('Please provide a block')
|
||||
end
|
||||
end
|
||||
|
||||
# Add a mailer to the registry
|
||||
#
|
||||
# @since 0.1.0
|
||||
# @api private
|
||||
def add_mailer(mailer)
|
||||
@mailers.add(mailer)
|
||||
raise ArgumentError.new('Please provide a block') unless block_given?
|
||||
@modules.push(blk)
|
||||
end
|
||||
|
||||
# Duplicate by copying the settings in a new instance.
|
||||
|
@ -211,40 +173,41 @@ module Hanami
|
|||
#
|
||||
# It supports the following delivery methods:
|
||||
#
|
||||
# * Exim (<tt>:exim</tt>)
|
||||
# * Sendmail (<tt>:sendmail</tt>)
|
||||
# * SMTP (<tt>:smtp</tt>, for local installations)
|
||||
# * SMTP Connection (<tt>:smtp_connection</tt>,
|
||||
# via <tt>Net::SMTP</tt> - for remote installations)
|
||||
# * Test (<tt>:test</tt>, for testing purposes)
|
||||
# * Exim (`:exim`)
|
||||
# * Sendmail (`:sendmail`)
|
||||
# * SMTP (`:smtp`, for local installations)
|
||||
# * SMTP Connection (`:smtp_connection`,
|
||||
# via `Net::SMTP` - for remote installations)
|
||||
# * Test (`:test`, for testing purposes)
|
||||
#
|
||||
# The default delivery method is SMTP (<tt>:smtp</tt>).
|
||||
# The default delivery method is SMTP (`:smtp`).
|
||||
#
|
||||
# Custom delivery methods can be specified by passing the class policy and
|
||||
# a set of optional configurations. This class MUST respond to:
|
||||
#
|
||||
# * <tt>initialize(options = {})</tt>
|
||||
# * <tt>deliver!(mail)<tt>
|
||||
# * `initialize(options = {})`
|
||||
# * `deliver!(mail)`
|
||||
#
|
||||
# @param method [Symbol, #initialize, deliver!] delivery method
|
||||
# @param options [Hash] optional settings
|
||||
#
|
||||
# @return [Array] an array containing the delivery method and the optional settings as an Hash
|
||||
#
|
||||
# @since 0.1.0
|
||||
# @since next
|
||||
# @api unstable
|
||||
#
|
||||
# @example Setup delivery method with supported symbol
|
||||
# require 'hanami/mailer'
|
||||
#
|
||||
# Hanami::Mailer.configure do
|
||||
# delivery_method :sendmail
|
||||
# Hanami::Mailer::Configuration.new do |config|
|
||||
# config.delivery_method = :sendmail
|
||||
# end
|
||||
#
|
||||
# @example Setup delivery method with supported symbol and options
|
||||
# require 'hanami/mailer'
|
||||
#
|
||||
# Hanami::Mailer.configure do
|
||||
# delivery_method :smtp, address: "localhost", port: 1025
|
||||
# Hanami::Mailer::Configuration.new do |config|
|
||||
# config.delivery_method = :smtp, address: "localhost", port: 1025
|
||||
# end
|
||||
#
|
||||
# @example Setup custom delivery method with options
|
||||
|
@ -260,49 +223,77 @@ module Hanami
|
|||
# end
|
||||
# end
|
||||
#
|
||||
# Hanami::Mailer.configure do
|
||||
# delivery_method MandrillDeliveryMethod,
|
||||
# username: ENV['MANDRILL_USERNAME'],
|
||||
# password: ENV['MANDRILL_API_KEY']
|
||||
# Hanami::Mailer.Configuration.new do |config|
|
||||
# config.delivery_method = MandrillDeliveryMethod,
|
||||
# username: ENV['MANDRILL_USERNAME'],
|
||||
# password: ENV['MANDRILL_API_KEY']
|
||||
# end
|
||||
def delivery_method(method = nil, options = {})
|
||||
if method.nil?
|
||||
@delivery_method
|
||||
else
|
||||
@delivery_method = [method, options]
|
||||
end
|
||||
attr_accessor :delivery_method
|
||||
|
||||
# Specify a default charset for all the delivered emails
|
||||
#
|
||||
# If not set, it defaults to `UTF-8`
|
||||
#
|
||||
# @!attribute default_charset
|
||||
# @return [String] the charset
|
||||
#
|
||||
# @since next
|
||||
# @api unstable
|
||||
#
|
||||
# @example
|
||||
# require 'hanami/mailer'
|
||||
#
|
||||
# Hanami::Mailer::Configuration.new do |config|
|
||||
# config.default_charset = "iso-8859-1"
|
||||
# end
|
||||
attr_accessor :default_charset
|
||||
|
||||
# Add a mailer to the registry
|
||||
#
|
||||
# @param mailer [Hanami::Mailer] a mailer
|
||||
#
|
||||
# @since 0.1.0
|
||||
# @api unstable
|
||||
def add_mailer(mailer)
|
||||
template_name = TemplateName[mailer.template_name, namespace]
|
||||
templates = finder.find(template_name)
|
||||
|
||||
mailers[mailer] = templates
|
||||
end
|
||||
|
||||
# @since 0.1.0
|
||||
def default_charset(value = nil)
|
||||
if value.nil?
|
||||
@default_charset
|
||||
else
|
||||
@default_charset = value
|
||||
end
|
||||
# @param mailer [Hanami::Mailer] a mailer
|
||||
# @param format [Symbol] the wanted format (eg. `:html`, `:txt`)
|
||||
#
|
||||
# @raise [Hanami::Mailer::UnknownMailerError] if the given mailer is not
|
||||
# present in the configuration. This happens when the configuration is
|
||||
# used before to being finalized.
|
||||
#
|
||||
# @since next
|
||||
# @api unstable
|
||||
def template(mailer, format)
|
||||
mailers.fetch(mailer) { raise UnknownMailerError.new(mailer) }[format]
|
||||
end
|
||||
|
||||
protected
|
||||
# Deep freeze the important instance variables
|
||||
#
|
||||
# @since next
|
||||
# @api unstable
|
||||
def freeze
|
||||
delivery_method.freeze
|
||||
default_charset.freeze
|
||||
mailers.freeze
|
||||
super
|
||||
end
|
||||
|
||||
# @api private
|
||||
# @since 0.1.0
|
||||
attr_writer :root
|
||||
private
|
||||
|
||||
# @api private
|
||||
# @since 0.1.0
|
||||
attr_writer :delivery_method
|
||||
# @api private
|
||||
attr_reader :mailers
|
||||
|
||||
# @api private
|
||||
# @since 0.1.0
|
||||
attr_writer :default_charset
|
||||
|
||||
# @api private
|
||||
# @since 0.1.0
|
||||
attr_writer :namespace
|
||||
|
||||
# @api private
|
||||
# @since 0.1.0
|
||||
attr_writer :modules
|
||||
# @since next
|
||||
# @api unstable
|
||||
attr_reader :finder
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,89 +1,31 @@
|
|||
require 'hanami/mailer/rendering/template_name'
|
||||
require 'hanami/mailer/rendering/templates_finder'
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Hanami
|
||||
module Mailer
|
||||
# Hanami::Mailer
|
||||
#
|
||||
# @since 0.1.0
|
||||
class Mailer
|
||||
require 'hanami/mailer/template_name'
|
||||
|
||||
# Class level DSL
|
||||
#
|
||||
# @since 0.1.0
|
||||
module Dsl
|
||||
# @since 0.3.0
|
||||
# @api private
|
||||
# @api unstable
|
||||
def self.extended(base)
|
||||
base.class_eval do
|
||||
@from = nil
|
||||
@to = nil
|
||||
@cc = nil
|
||||
@bcc = nil
|
||||
@subject = nil
|
||||
@from = nil
|
||||
@to = nil
|
||||
@cc = nil
|
||||
@bcc = nil
|
||||
@subject = nil
|
||||
@template = nil
|
||||
@before = ->(*) {}
|
||||
end
|
||||
end
|
||||
|
||||
# Set the template name IF it differs from the convention.
|
||||
#
|
||||
# For a given mailer named <tt>Signup::Welcome</tt> it will look for
|
||||
# <tt>signup/welcome.*.*</tt> templates under the root directory.
|
||||
#
|
||||
# If for some reason, we need to specify a different template name, we can
|
||||
# use this method.
|
||||
#
|
||||
# This is part of a DSL, for this reason when this method is called with
|
||||
# an argument, it will set the corresponding class variable. When
|
||||
# called without, it will return the already set value, or the default.
|
||||
#
|
||||
# @overload template(value)
|
||||
# Sets the given value
|
||||
# @param value [String, #to_s] relative template path, under root
|
||||
# @return [NilClass]
|
||||
#
|
||||
# @overload template
|
||||
# Gets the template name
|
||||
# @return [String]
|
||||
#
|
||||
# @since 0.1.0
|
||||
#
|
||||
# @see Hanami::Mailers::Configuration.root
|
||||
#
|
||||
# @example Custom template name
|
||||
# require 'hanami/mailer'
|
||||
#
|
||||
# class MyMailer
|
||||
# include Hanami::Mailer
|
||||
# template 'mailer'
|
||||
# end
|
||||
def template(value = nil)
|
||||
if value.nil?
|
||||
@template ||= ::Hanami::Mailer::Rendering::TemplateName.new(name, configuration.namespace).to_s
|
||||
else
|
||||
@template = value
|
||||
end
|
||||
end
|
||||
|
||||
# Returns a set of associated templates or only one for the given format
|
||||
#
|
||||
# This is part of a DSL, for this reason when this method is called with
|
||||
# an argument, it will set the corresponding class variable. When
|
||||
# called without, it will return the already set value, or the default.
|
||||
#
|
||||
# @overload templates(format)
|
||||
# Returns the template associated with the given format
|
||||
# @param value [Symbol] the format
|
||||
# @return [Hash]
|
||||
#
|
||||
# @overload templates
|
||||
# Returns all the associated templates
|
||||
# Gets the template name
|
||||
# @return [Hash] a set of templates
|
||||
#
|
||||
# @since 0.1.0
|
||||
# @api private
|
||||
def templates(format = nil)
|
||||
if format.nil?
|
||||
@templates = ::Hanami::Mailer::Rendering::TemplatesFinder.new(self).find
|
||||
else
|
||||
@templates.fetch(format, nil)
|
||||
end
|
||||
end
|
||||
private_class_method :extended
|
||||
|
||||
# Sets the sender for mail messages
|
||||
#
|
||||
|
@ -114,24 +56,15 @@ module Hanami
|
|||
# @example Hardcoded value (String)
|
||||
# require 'hanami/mailer'
|
||||
#
|
||||
# class WelcomeMailer
|
||||
# include Hanami::Mailer
|
||||
#
|
||||
# class WelcomeMailer < Hanami::Mailer
|
||||
# from "noreply@example.com"
|
||||
# end
|
||||
#
|
||||
# @example Method (Symbol)
|
||||
# @example Lazy (Proc)
|
||||
# require 'hanami/mailer'
|
||||
#
|
||||
# class WelcomeMailer
|
||||
# include Hanami::Mailer
|
||||
# from :sender
|
||||
#
|
||||
# private
|
||||
#
|
||||
# def sender
|
||||
# "noreply@example.com"
|
||||
# end
|
||||
# class WelcomeMailer < Hanami::Mailer
|
||||
# from ->(locals) { locals.fetch(:sender).email }
|
||||
# end
|
||||
def from(value = nil)
|
||||
if value.nil?
|
||||
|
@ -141,6 +74,74 @@ module Hanami
|
|||
end
|
||||
end
|
||||
|
||||
# Sets the recipient for mail messages
|
||||
#
|
||||
# It accepts a hardcoded value as a string or array of strings.
|
||||
# For dynamic values, you can specify a symbol that represents an instance
|
||||
# method.
|
||||
#
|
||||
# This value MUST be set, otherwise an exception is raised at the delivery
|
||||
# time.
|
||||
#
|
||||
# When a value is given, specify the recipient of the email
|
||||
# Otherwise, it returns the recipient of the email
|
||||
#
|
||||
# This is part of a DSL, for this reason when this method is called with
|
||||
# an argument, it will set the corresponding class variable. When
|
||||
# called without, it will return the already set value, or the default.
|
||||
#
|
||||
# @overload to(value)
|
||||
# Sets the recipient
|
||||
# @param value [String, Array, Symbol] the hardcoded value or method name
|
||||
# @return [NilClass]
|
||||
#
|
||||
# @overload to
|
||||
# Returns the recipient
|
||||
# @return [String, Array, Symbol] the recipient
|
||||
#
|
||||
# @since 0.1.0
|
||||
#
|
||||
# @example Hardcoded value (String)
|
||||
# require 'hanami/mailer'
|
||||
#
|
||||
# class WelcomeMailer < Hanami::Mailer
|
||||
# to "user@example.com"
|
||||
# end
|
||||
#
|
||||
# @example Hardcoded value (Array)
|
||||
# require 'hanami/mailer'
|
||||
#
|
||||
# class WelcomeMailer < Hanami::Mailer
|
||||
# to ["user-1@example.com", "user-2@example.com"]
|
||||
# end
|
||||
#
|
||||
# @example Lazy value (Proc)
|
||||
# require 'hanami/mailer'
|
||||
#
|
||||
# class WelcomeMailer < Hanami::Mailer
|
||||
# to ->(locals) { locals.fetch(:user).email }
|
||||
# end
|
||||
#
|
||||
# user = User.new(name: 'L')
|
||||
# WelcomeMailer.new(configuration: configuration).deliver(user: user)
|
||||
#
|
||||
# @example Lazy values (Proc)
|
||||
# require 'hanami/mailer'
|
||||
#
|
||||
# class WelcomeMailer < Hanami::Mailer
|
||||
# to ->(locals) { locals.fetch(:users).map(&:email) }
|
||||
# end
|
||||
#
|
||||
# users = [User.new(name: 'L'), User.new(name: 'MG')]
|
||||
# WelcomeMailer.new(configuration: configuration).deliver(users: users)
|
||||
def to(value = nil)
|
||||
if value.nil?
|
||||
@to
|
||||
else
|
||||
@to = value
|
||||
end
|
||||
end
|
||||
|
||||
# Sets the cc (carbon copy) for mail messages
|
||||
#
|
||||
# It accepts a hardcoded value as a string or array of strings.
|
||||
|
@ -170,58 +171,36 @@ module Hanami
|
|||
# @example Hardcoded value (String)
|
||||
# require 'hanami/mailer'
|
||||
#
|
||||
# class WelcomeMailer
|
||||
# include Hanami::Mailer
|
||||
#
|
||||
# to "user@example.com"
|
||||
# class WelcomeMailer < Hanami::Mailer
|
||||
# cc "other.user@example.com"
|
||||
# end
|
||||
#
|
||||
# @example Hardcoded value (Array)
|
||||
# require 'hanami/mailer'
|
||||
#
|
||||
# class WelcomeMailer
|
||||
# include Hanami::Mailer
|
||||
#
|
||||
# to ["user-1@example.com", "user-2@example.com"]
|
||||
# class WelcomeMailer < Hanami::Mailer
|
||||
# cc ["other.user-1@example.com", "other.user-2@example.com"]
|
||||
# end
|
||||
#
|
||||
# @example Method (Symbol)
|
||||
# @example Lazy value (Proc)
|
||||
# require 'hanami/mailer'
|
||||
#
|
||||
# class WelcomeMailer
|
||||
# include Hanami::Mailer
|
||||
# to "user@example.com"
|
||||
# cc :email_address
|
||||
#
|
||||
# private
|
||||
#
|
||||
# def email_address
|
||||
# user.email
|
||||
# end
|
||||
# class WelcomeMailer < Hanami::Mailer
|
||||
# cc ->(locals) { locals.fetch(:user).email }
|
||||
# end
|
||||
#
|
||||
# other_user = User.new(name: 'L')
|
||||
# WelcomeMailer.deliver(user: other_user)
|
||||
# user = User.new(name: 'L')
|
||||
# WelcomeMailer.new(configuration: configuration).deliver(user: user)
|
||||
#
|
||||
# @example Method that returns a collection of recipients
|
||||
# @example Lazy values (Proc)
|
||||
# require 'hanami/mailer'
|
||||
#
|
||||
# class WelcomeMailer
|
||||
# include Hanami::Mailer
|
||||
# to "user@example.com"
|
||||
# cc :recipients
|
||||
#
|
||||
# private
|
||||
#
|
||||
# def recipients
|
||||
# users.map(&:email)
|
||||
# end
|
||||
# class WelcomeMailer < Hanami::Mailer
|
||||
# cc ->(locals) { locals.fetch(:users).map(&:email) }
|
||||
# end
|
||||
#
|
||||
# other_users = [User.new(name: 'L'), User.new(name: 'MG')]
|
||||
# WelcomeMailer.deliver(users: other_users)
|
||||
# users = [User.new(name: 'L'), User.new(name: 'MG')]
|
||||
# WelcomeMailer.new(configuration: configuration).deliver(users: users)
|
||||
def cc(value = nil)
|
||||
if value.nil?
|
||||
@cc
|
||||
|
@ -259,58 +238,36 @@ module Hanami
|
|||
# @example Hardcoded value (String)
|
||||
# require 'hanami/mailer'
|
||||
#
|
||||
# class WelcomeMailer
|
||||
# include Hanami::Mailer
|
||||
#
|
||||
# to "user@example.com"
|
||||
# class WelcomeMailer < Hanami::Mailer
|
||||
# bcc "other.user@example.com"
|
||||
# end
|
||||
#
|
||||
# @example Hardcoded value (Array)
|
||||
# require 'hanami/mailer'
|
||||
#
|
||||
# class WelcomeMailer
|
||||
# include Hanami::Mailer
|
||||
#
|
||||
# to ["user-1@example.com", "user-2@example.com"]
|
||||
# class WelcomeMailer < Hanami::Mailer
|
||||
# bcc ["other.user-1@example.com", "other.user-2@example.com"]
|
||||
# end
|
||||
#
|
||||
# @example Method (Symbol)
|
||||
# @example Lazy value (Proc)
|
||||
# require 'hanami/mailer'
|
||||
#
|
||||
# class WelcomeMailer
|
||||
# include Hanami::Mailer
|
||||
# to "user@example.com"
|
||||
# bcc :email_address
|
||||
#
|
||||
# private
|
||||
#
|
||||
# def email_address
|
||||
# user.email
|
||||
# end
|
||||
# class WelcomeMailer < Hanami::Mailer
|
||||
# bcc ->(locals) { locals.fetch(:user).email }
|
||||
# end
|
||||
#
|
||||
# other_user = User.new(name: 'L')
|
||||
# WelcomeMailer.deliver(user: other_user)
|
||||
# user = User.new(name: 'L')
|
||||
# WelcomeMailer.new(configuration: configuration).deliver(user: user)
|
||||
#
|
||||
# @example Method that returns a collection of recipients
|
||||
# @example Lazy values (Proc)
|
||||
# require 'hanami/mailer'
|
||||
#
|
||||
# class WelcomeMailer
|
||||
# include Hanami::Mailer
|
||||
# to "user@example.com"
|
||||
# bcc :recipients
|
||||
#
|
||||
# private
|
||||
#
|
||||
# def recipients
|
||||
# users.map(&:email)
|
||||
# end
|
||||
# class WelcomeMailer < Hanami::Mailer
|
||||
# bcc ->(locals) { locals.fetch(:users).map(&:email) }
|
||||
# end
|
||||
#
|
||||
# other_users = [User.new(name: 'L'), User.new(name: 'MG')]
|
||||
# WelcomeMailer.deliver(users: other_users)
|
||||
# users = [User.new(name: 'L'), User.new(name: 'MG')]
|
||||
# WelcomeMailer.new(configuration: configuration).deliver(users: users)
|
||||
def bcc(value = nil)
|
||||
if value.nil?
|
||||
@bcc
|
||||
|
@ -319,92 +276,6 @@ module Hanami
|
|||
end
|
||||
end
|
||||
|
||||
# Sets the recipient for mail messages
|
||||
#
|
||||
# It accepts a hardcoded value as a string or array of strings.
|
||||
# For dynamic values, you can specify a symbol that represents an instance
|
||||
# method.
|
||||
#
|
||||
# This value MUST be set, otherwise an exception is raised at the delivery
|
||||
# time.
|
||||
#
|
||||
# When a value is given, specify the recipient of the email
|
||||
# Otherwise, it returns the recipient of the email
|
||||
#
|
||||
# This is part of a DSL, for this reason when this method is called with
|
||||
# an argument, it will set the corresponding class variable. When
|
||||
# called without, it will return the already set value, or the default.
|
||||
#
|
||||
# @overload to(value)
|
||||
# Sets the recipient
|
||||
# @param value [String, Array, Symbol] the hardcoded value or method name
|
||||
# @return [NilClass]
|
||||
#
|
||||
# @overload to
|
||||
# Returns the recipient
|
||||
# @return [String, Array, Symbol] the recipient
|
||||
#
|
||||
# @since 0.1.0
|
||||
#
|
||||
# @example Hardcoded value (String)
|
||||
# require 'hanami/mailer'
|
||||
#
|
||||
# class WelcomeMailer
|
||||
# include Hanami::Mailer
|
||||
#
|
||||
# to "user@example.com"
|
||||
# end
|
||||
#
|
||||
# @example Hardcoded value (Array)
|
||||
# require 'hanami/mailer'
|
||||
#
|
||||
# class WelcomeMailer
|
||||
# include Hanami::Mailer
|
||||
#
|
||||
# to ["user-1@example.com", "user-2@example.com"]
|
||||
# end
|
||||
#
|
||||
# @example Method (Symbol)
|
||||
# require 'hanami/mailer'
|
||||
#
|
||||
# class WelcomeMailer
|
||||
# include Hanami::Mailer
|
||||
# to :email_address
|
||||
#
|
||||
# private
|
||||
#
|
||||
# def email_address
|
||||
# user.email
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# user = User.new(name: 'L')
|
||||
# WelcomeMailer.deliver(user: user)
|
||||
#
|
||||
# @example Method that returns a collection of recipients
|
||||
# require 'hanami/mailer'
|
||||
#
|
||||
# class WelcomeMailer
|
||||
# include Hanami::Mailer
|
||||
# to :recipients
|
||||
#
|
||||
# private
|
||||
#
|
||||
# def recipients
|
||||
# users.map(&:email)
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# users = [User.new(name: 'L'), User.new(name: 'MG')]
|
||||
# WelcomeMailer.deliver(users: users)
|
||||
def to(value = nil)
|
||||
if value.nil?
|
||||
@to
|
||||
else
|
||||
@to = value
|
||||
end
|
||||
end
|
||||
|
||||
# Sets the subject for mail messages
|
||||
#
|
||||
# It accepts a hardcoded value as a string, or a symbol that represents
|
||||
|
@ -431,28 +302,19 @@ module Hanami
|
|||
# @example Hardcoded value (String)
|
||||
# require 'hanami/mailer'
|
||||
#
|
||||
# class WelcomeMailer
|
||||
# include Hanami::Mailer
|
||||
#
|
||||
# class WelcomeMailer < Hanami::Mailer
|
||||
# subject "Welcome"
|
||||
# end
|
||||
#
|
||||
# @example Method (Symbol)
|
||||
# @example Lazy value (Proc)
|
||||
# require 'hanami/mailer'
|
||||
#
|
||||
# class WelcomeMailer
|
||||
# include Hanami::Mailer
|
||||
# subject :greeting
|
||||
#
|
||||
# private
|
||||
#
|
||||
# def greeting
|
||||
# "Hello, #{ user.name }"
|
||||
# end
|
||||
# class WelcomeMailer < Hanami::Mailer
|
||||
# subject ->(locals) { "Hello #{locals.fetch(:user).name}" }
|
||||
# end
|
||||
#
|
||||
# user = User.new(name: 'L')
|
||||
# WelcomeMailer.deliver(user: user)
|
||||
# WelcomeMailer.new(configuration: configuration).deliver(user: user)
|
||||
def subject(value = nil)
|
||||
if value.nil?
|
||||
@subject
|
||||
|
@ -461,17 +323,70 @@ module Hanami
|
|||
end
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
# Loading mechanism hook.
|
||||
# Set the template name **IF** it differs from the naming convention.
|
||||
#
|
||||
# For a given mailer named `Signup::Welcome` it will look for
|
||||
# `signup/welcome.*.*` templates under the root directory.
|
||||
#
|
||||
# If for some reason, we need to specify a different template name, we can
|
||||
# use this method.
|
||||
#
|
||||
# @param value [String] the template name
|
||||
#
|
||||
# @api private
|
||||
# @since 0.1.0
|
||||
# @api unstable
|
||||
#
|
||||
# @see Hanami::Mailer.load!
|
||||
def load!
|
||||
templates.freeze
|
||||
configuration.freeze
|
||||
# @example Custom template name
|
||||
# require 'hanami/mailer'
|
||||
#
|
||||
# class MyMailer < Hanami::Mailer
|
||||
# template 'mailer'
|
||||
# end
|
||||
def template(value)
|
||||
@template = value
|
||||
end
|
||||
|
||||
# @since next
|
||||
# @api unstable
|
||||
def template_name
|
||||
@template || name
|
||||
end
|
||||
|
||||
# Before callback for email delivery
|
||||
#
|
||||
# @since next
|
||||
# @api unstable
|
||||
#
|
||||
# @example
|
||||
# require 'hanami/mailer'
|
||||
#
|
||||
# module Billing
|
||||
# class InvoiceMailer < Hanami::Mailer
|
||||
# subject 'Invoice'
|
||||
# from 'noreply@example.com'
|
||||
# to ->(locals) { locals.fetch(:user).email }
|
||||
#
|
||||
# before do |mail, locals|
|
||||
# user = locals.fetch(:user)
|
||||
# mail.attachments["invoice-#{invoice_code}-#{user.id}.pdf"] = File.read('/path/to/invoice.pdf')
|
||||
# end
|
||||
#
|
||||
# def invoice_code
|
||||
# "123"
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# invoice = Invoice.new
|
||||
# user = User.new(name: 'L', email: 'user@example.com')
|
||||
#
|
||||
# InvoiceMailer.new(configuration: configuration).deliver(invoice: invoice, user: user)
|
||||
def before(&blk)
|
||||
if block_given?
|
||||
@before = blk
|
||||
else
|
||||
@before
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'mail'
|
||||
# require 'ice_nine'
|
||||
|
||||
module Hanami
|
||||
class Mailer
|
||||
# @since next
|
||||
# @api unstable
|
||||
class Finalizer
|
||||
# Finalize the given configuration before to start to use the mailers
|
||||
#
|
||||
# @param mailers [Array<Hanami::Mailer>] all the subclasses of
|
||||
# `Hanami::Mailer`
|
||||
# @param configuration [Hanami::Mailer::Configuration] the configuration
|
||||
# to finalize
|
||||
#
|
||||
# @return configuration [Hanami::Mailer::Configuration] the finalized
|
||||
# configuration
|
||||
#
|
||||
# @since next
|
||||
# @api unstable
|
||||
def self.finalize(mailers, configuration)
|
||||
Mail.eager_autoload!
|
||||
mailers.each { |mailer| configuration.add_mailer(mailer) }
|
||||
|
||||
configuration.freeze
|
||||
configuration
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,53 +0,0 @@
|
|||
require 'hanami/utils/string'
|
||||
|
||||
module Hanami
|
||||
module Mailer
|
||||
module Rendering
|
||||
# @since 0.1.0
|
||||
# @api private
|
||||
#
|
||||
# TODO this is identical to Hanami::View, consider to move into Hanami::Utils
|
||||
class TemplateName
|
||||
# @since 0.1.0
|
||||
# @api private
|
||||
NAMESPACE_SEPARATOR = '::'.freeze
|
||||
|
||||
# @since 0.1.0
|
||||
# @api private
|
||||
def initialize(name, namespace)
|
||||
@name = name
|
||||
compile!(namespace)
|
||||
end
|
||||
|
||||
# @since 0.1.0
|
||||
# @api private
|
||||
def to_s
|
||||
@name
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# @since 0.1.0
|
||||
# @api private
|
||||
def compile!(namespace)
|
||||
tokens(namespace) { |token| replace!(token) }
|
||||
@name = Utils::String.underscore(@name)
|
||||
end
|
||||
|
||||
# @since 0.1.0
|
||||
# @api private
|
||||
def tokens(namespace)
|
||||
namespace.to_s.split(NAMESPACE_SEPARATOR).each do |token|
|
||||
yield token
|
||||
end
|
||||
end
|
||||
|
||||
# @since 0.1.0
|
||||
# @api private
|
||||
def replace!(token)
|
||||
@name.gsub!(/\A#{token}#{NAMESPACE_SEPARATOR}/, '')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,133 +0,0 @@
|
|||
require 'hanami/mailer/template'
|
||||
|
||||
module Hanami
|
||||
module Mailer
|
||||
module Rendering
|
||||
# Find templates for a mailer
|
||||
#
|
||||
# @api private
|
||||
# @since 0.1.0
|
||||
#
|
||||
# @see Mailer::Template
|
||||
class TemplatesFinder
|
||||
# Default format
|
||||
#
|
||||
# @api private
|
||||
# @since 0.1.0
|
||||
FORMAT = '*'.freeze
|
||||
|
||||
# Default template engines
|
||||
#
|
||||
# @api private
|
||||
# @since 0.1.0
|
||||
ENGINES = '*'.freeze
|
||||
|
||||
# Recursive pattern
|
||||
#
|
||||
# @api private
|
||||
# @since 0.1.0
|
||||
RECURSIVE = '**'.freeze
|
||||
|
||||
# Initialize a finder
|
||||
#
|
||||
# @param mailer [Class] the mailer class
|
||||
#
|
||||
# @api private
|
||||
# @since 0.1.0
|
||||
def initialize(mailer)
|
||||
@mailer = mailer
|
||||
end
|
||||
|
||||
# Find all the associated templates to the mailer.
|
||||
# It recursively looks for templates under the root path of the mailer,
|
||||
# that are matching the template name
|
||||
#
|
||||
# @return [Hash] the templates
|
||||
#
|
||||
# @api private
|
||||
# @since 0.1.0
|
||||
#
|
||||
# @see Hanami::Mailer::Dsl#root
|
||||
# @see Hanami::Mailer::Dsl#templates
|
||||
#
|
||||
# @example
|
||||
# require 'hanami/mailer'
|
||||
#
|
||||
# module Mailers
|
||||
# class Welcome
|
||||
# include Hanami::Mailer
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# Mailers::Welcome.root # => "/path/to/templates"
|
||||
# Mailers::Welcome.templates # => {[:html] => "welcome"}
|
||||
#
|
||||
# # This mailer has a template:
|
||||
# #
|
||||
# # "/path/to/templates/welcome.html.erb"
|
||||
#
|
||||
# Hanami::Mailer::Rendering::TemplatesFinder.new(Mailers::Welcome).find
|
||||
# # => [#<Hanami::Mailer::Template:0x007f8a0a86a970 ... @file="/path/to/templates/welcome.html.erb">]
|
||||
def find
|
||||
templates = Hash[]
|
||||
_find.map do |template|
|
||||
name = File.basename(template)
|
||||
format = (name.split('.')[-2]).to_sym
|
||||
templates[format] = Mailer::Template.new(template)
|
||||
end
|
||||
templates
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
# @api private
|
||||
# @since 0.1.0
|
||||
def _find(lookup = search_path)
|
||||
Dir.glob("#{[root, lookup, template_name].join(separator)}.#{format}.#{engines}")
|
||||
end
|
||||
|
||||
# @api private
|
||||
# @since 0.1.0
|
||||
def template_name
|
||||
Rendering::TemplateName.new(@mailer.template, @mailer.configuration.namespace).to_s
|
||||
end
|
||||
|
||||
# @api private
|
||||
# @since 0.1.0
|
||||
def root
|
||||
@mailer.configuration.root
|
||||
end
|
||||
|
||||
# @api private
|
||||
# @since 0.1.0
|
||||
def search_path
|
||||
recursive
|
||||
end
|
||||
|
||||
# @api private
|
||||
# @since 0.1.0
|
||||
def recursive
|
||||
RECURSIVE
|
||||
end
|
||||
|
||||
# @api private
|
||||
# @since 0.1.0
|
||||
def separator
|
||||
::File::SEPARATOR
|
||||
end
|
||||
|
||||
# @api private
|
||||
# @since 0.1.0
|
||||
def format
|
||||
FORMAT
|
||||
end
|
||||
|
||||
# @api private
|
||||
# @since 0.1.0
|
||||
def engines
|
||||
ENGINES
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,7 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'tilt'
|
||||
|
||||
module Hanami
|
||||
module Mailer
|
||||
class Mailer
|
||||
# A logic-less template.
|
||||
#
|
||||
# @api private
|
||||
|
@ -11,29 +13,20 @@ module Hanami
|
|||
class Template
|
||||
def initialize(template)
|
||||
@_template = Tilt.new(template)
|
||||
freeze
|
||||
end
|
||||
|
||||
# Render the template within the context of the given scope.
|
||||
#
|
||||
# @param scope [Class] the rendering scope
|
||||
# @param scope [Object] the rendering scope
|
||||
# @param locals [Hash] set of objects passed to the constructor
|
||||
#
|
||||
# @return [String] the output of the rendering process
|
||||
#
|
||||
# @api private
|
||||
# @since 0.1.0
|
||||
def render(scope = Object.new, locals = {})
|
||||
@_template.render(scope, locals)
|
||||
end
|
||||
|
||||
# Get the path to the template
|
||||
#
|
||||
# @return [String] the pathname
|
||||
#
|
||||
# @api private
|
||||
# @since 0.1.0
|
||||
def file
|
||||
@_template.file
|
||||
def render(scope, locals = {})
|
||||
@_template.render(scope.dup, locals)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'hanami/utils/string'
|
||||
|
||||
module Hanami
|
||||
class Mailer
|
||||
# @since 0.1.0
|
||||
# @api private
|
||||
#
|
||||
# TODO this is identical to Hanami::View, consider to move into Hanami::Utils
|
||||
class TemplateName
|
||||
# @since next
|
||||
# @api unstable
|
||||
def self.call(name, namespace)
|
||||
Utils::String.new(name.gsub(/\A#{namespace}(::)*/, '')).underscore.to_s
|
||||
end
|
||||
|
||||
class << self
|
||||
# @since next
|
||||
# @api unstable
|
||||
alias [] call
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,152 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'hanami/mailer/template'
|
||||
require 'hanami/utils/file_list'
|
||||
require 'pathname'
|
||||
|
||||
module Hanami
|
||||
class Mailer
|
||||
# Find templates for a mailer
|
||||
#
|
||||
# @api private
|
||||
# @since 0.1.0
|
||||
#
|
||||
# @see Mailer::Template
|
||||
class TemplatesFinder
|
||||
# Default format
|
||||
#
|
||||
# @api private
|
||||
# @since 0.1.0
|
||||
FORMAT = '*'
|
||||
|
||||
# Default template engines
|
||||
#
|
||||
# @api private
|
||||
# @since 0.1.0
|
||||
ENGINES = '*'
|
||||
|
||||
# Recursive pattern
|
||||
#
|
||||
# @api private
|
||||
# @since 0.1.0
|
||||
RECURSIVE = '**'
|
||||
|
||||
# Format separator
|
||||
#
|
||||
# @api unstable
|
||||
# @since next
|
||||
#
|
||||
# @example
|
||||
# welcome.html.erb
|
||||
FORMAT_SEPARATOR = '.'
|
||||
|
||||
private_constant(*constants(true))
|
||||
|
||||
# Initialize a finder
|
||||
#
|
||||
# @param root [String,Pathname] the root directory where to recursively
|
||||
# look for templates
|
||||
#
|
||||
# @raise [Errno::ENOENT] if the directory doesn't exist
|
||||
#
|
||||
# @api unstable
|
||||
# @since 0.1.0
|
||||
def initialize(root)
|
||||
@root = Pathname.new(root).realpath
|
||||
freeze
|
||||
end
|
||||
|
||||
# Find all the associated templates to the mailer.
|
||||
#
|
||||
# It starts under the root path and it **recursively** looks for templates
|
||||
# that are matching the given template name.
|
||||
#
|
||||
# @param template_name [String] the template name
|
||||
#
|
||||
# @return [Hash] the templates
|
||||
#
|
||||
# @api unstable
|
||||
# @since 0.1.0
|
||||
#
|
||||
# @example
|
||||
# require 'hanami/mailer'
|
||||
#
|
||||
# module Mailers
|
||||
# class Welcome < Hanami::Mailer
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# configuration = Hanami::Mailer::Configuration.new do |config|
|
||||
# config.root = "path/to/templates"
|
||||
# end
|
||||
#
|
||||
# # This mailer has a template:
|
||||
# #
|
||||
# # "path/to/templates/welcome.html.erb"
|
||||
#
|
||||
# Hanami::Mailer::Rendering::TemplatesFinder.new(root).find("welcome")
|
||||
# # => [#<Hanami::Mailer::Template:0x007f8a0a86a970 ... @file="path/to/templates/welcome.html.erb">]
|
||||
def find(template_name)
|
||||
templates(template_name).each_with_object({}) do |template, result|
|
||||
format = extract_format(template)
|
||||
result[format] = Mailer::Template.new(template)
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
# @api unstable
|
||||
# @since 0.1.0
|
||||
def templates(template_name, lookup = search_path)
|
||||
Utils::FileList["#{[root, lookup, template_name].join(separator)}#{format_separator}#{format}#{format_separator}#{engines}"]
|
||||
end
|
||||
|
||||
# @api unstable
|
||||
# @since 0.1.0
|
||||
attr_reader :root
|
||||
|
||||
# @api private
|
||||
# @since 0.1.0
|
||||
def search_path
|
||||
recursive
|
||||
end
|
||||
|
||||
# @api private
|
||||
# @since 0.1.0
|
||||
def recursive
|
||||
RECURSIVE
|
||||
end
|
||||
|
||||
# @api private
|
||||
# @since 0.1.0
|
||||
def separator
|
||||
::File::SEPARATOR
|
||||
end
|
||||
|
||||
# @api private
|
||||
# @since 0.1.0
|
||||
def format
|
||||
FORMAT
|
||||
end
|
||||
|
||||
# @api private
|
||||
# @since 0.1.0
|
||||
def engines
|
||||
ENGINES
|
||||
end
|
||||
|
||||
# @api unstable
|
||||
# @since next
|
||||
def format_separator
|
||||
FORMAT_SEPARATOR
|
||||
end
|
||||
|
||||
# @api unstable
|
||||
# @since next
|
||||
def extract_format(template)
|
||||
filename = File.basename(template)
|
||||
filename.split(format_separator)[-2].to_sym
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,6 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Hanami
|
||||
module Mailer
|
||||
class Mailer
|
||||
# @since 0.1.0
|
||||
VERSION = '1.1.0'.freeze
|
||||
VERSION = '1.1.0'
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,154 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe Hanami::Mailer do
|
||||
describe '.deliver' do
|
||||
it 'can deliver with specified charset' do
|
||||
mail = CharsetMailer.new(configuration: configuration).deliver(charset: charset = 'iso-2022-jp')
|
||||
|
||||
expect(mail.charset).to eq(charset)
|
||||
expect(mail.parts.first.charset).to eq(charset)
|
||||
end
|
||||
|
||||
it "raises error when 'from' isn't specified" do
|
||||
expect { MissingFromMailer.new(configuration: configuration).deliver({}) }.to raise_error(Hanami::Mailer::MissingDeliveryDataError)
|
||||
end
|
||||
|
||||
it "raises error when 'to' isn't specified" do
|
||||
expect { MissingToMailer.new(configuration: configuration).deliver({}) }.to raise_error(Hanami::Mailer::MissingDeliveryDataError)
|
||||
end
|
||||
|
||||
describe 'test delivery with hardcoded values' do
|
||||
subject { WelcomeMailer.new(configuration: configuration).deliver({}) }
|
||||
|
||||
it 'sends the correct information' do
|
||||
expect(subject.from).to eq(['noreply@sender.com'])
|
||||
expect(subject.to).to eq(['noreply@recipient.com', 'owner@recipient.com'])
|
||||
expect(subject.cc).to eq(['cc@recipient.com'])
|
||||
expect(subject.bcc).to eq(['bcc@recipient.com'])
|
||||
expect(subject.subject).to eq('Welcome')
|
||||
end
|
||||
|
||||
it 'has the correct templates' do
|
||||
expect(subject.html_part.to_s).to include(%(template))
|
||||
expect(subject.text_part.to_s).to include(%(template))
|
||||
end
|
||||
|
||||
it 'interprets the prepare statement' do
|
||||
attachment = subject.attachments['invoice.pdf']
|
||||
|
||||
expect(attachment).to be_kind_of(Mail::Part)
|
||||
|
||||
expect(attachment).to be_attachment
|
||||
expect(attachment).to_not be_inline
|
||||
expect(attachment).to_not be_multipart
|
||||
|
||||
expect(attachment.filename).to eq('invoice.pdf')
|
||||
|
||||
expect(attachment.content_type).to match('application/pdf')
|
||||
expect(attachment.content_type).to match('filename=invoice.pdf')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'test delivery with procs' do
|
||||
subject { ProcMailer.new(configuration: configuration).deliver(user: user) }
|
||||
let(:user) { User.new('Name', 'student@deigirls.com') }
|
||||
|
||||
it 'sends the correct information' do
|
||||
expect(subject.from).to eq(["hello-#{user.name.downcase}@example.com"])
|
||||
expect(subject.to).to eq([user.email])
|
||||
expect(subject.subject).to eq("[Hanami] Hello, #{user.name}")
|
||||
end
|
||||
end
|
||||
|
||||
describe 'test delivery with locals' do
|
||||
subject { EventMailer.new(configuration: configuration) }
|
||||
let(:count) { 100 }
|
||||
|
||||
it 'delivers the message' do
|
||||
threads = []
|
||||
mails = {}
|
||||
|
||||
count.times do |i|
|
||||
threads << Thread.new do
|
||||
user = OpenStruct.new(name: "Luca #{i}", email: "luca-#{i}@domain.test")
|
||||
event = OpenStruct.new(id: i, title: "Event ##{i}")
|
||||
|
||||
mails[i] = subject.deliver(user: user, event: event)
|
||||
end
|
||||
end
|
||||
threads.map(&:join)
|
||||
|
||||
expect(mails.count).to eq(count)
|
||||
mails.each do |i, mail|
|
||||
expect(mail.to).to eq(["luca-#{i}@domain.test"])
|
||||
expect(mail.subject).to eq("Invitation: Event ##{i}")
|
||||
expect(mail.attachments[0].filename).to eq("invitation-#{i}.ics")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'multipart' do
|
||||
it 'delivers all the parts by default' do
|
||||
mail = WelcomeMailer.new(configuration: configuration).deliver({})
|
||||
body = mail.body.encoded
|
||||
|
||||
expect(body).to include(%(<h1>Hello World!</h1>))
|
||||
expect(body).to include(%(This is a txt template))
|
||||
end
|
||||
|
||||
it 'can deliver only the text part' do
|
||||
mail = WelcomeMailer.new(configuration: configuration).deliver(format: :txt)
|
||||
body = mail.body.encoded
|
||||
|
||||
expect(body).to_not include(%(<h1>Hello World!</h1>))
|
||||
expect(body).to include(%(This is a txt template))
|
||||
end
|
||||
|
||||
it 'can deliver only the html part' do
|
||||
mail = WelcomeMailer.new(configuration: configuration).deliver(format: :html)
|
||||
body = mail.body.encoded
|
||||
|
||||
expect(body).to include(%(<h1>Hello World!</h1>))
|
||||
expect(body).to_not include(%(This is a txt template))
|
||||
end
|
||||
end
|
||||
|
||||
describe 'custom delivery' do
|
||||
before do
|
||||
mailer.deliver({})
|
||||
end
|
||||
|
||||
subject { options.fetch(:deliveries).first }
|
||||
let(:mailer) { WelcomeMailer.new(configuration: configuration) }
|
||||
let(:options) { { deliveries: [] } }
|
||||
|
||||
let(:configuration) do
|
||||
configuration = Hanami::Mailer::Configuration.new do |config|
|
||||
config.root = 'spec/support/fixtures'
|
||||
config.delivery_method = MandrillDeliveryMethod, options
|
||||
end
|
||||
|
||||
Hanami::Mailer.finalize(configuration)
|
||||
end
|
||||
|
||||
it 'delivers the mail' do
|
||||
expect(options.fetch(:deliveries).size).to be(1)
|
||||
end
|
||||
|
||||
it 'sends the correct information' do
|
||||
expect(subject.from).to eq(['noreply@sender.com'])
|
||||
expect(subject.to).to eq(['noreply@recipient.com', 'owner@recipient.com'])
|
||||
expect(subject.subject).to eq("Welcome")
|
||||
end
|
||||
|
||||
it 'has the correct templates' do
|
||||
expect(subject.html_part.to_s).to include(%(template))
|
||||
expect(subject.text_part.to_s).to include(%(template))
|
||||
end
|
||||
|
||||
it 'runs the before callback' do
|
||||
expect(subject.attachments['invoice.pdf']).to_not be(nil)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,3 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.configure do |config|
|
||||
config.expect_with :rspec do |expectations|
|
||||
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
|
||||
|
@ -22,35 +24,9 @@ RSpec.configure do |config|
|
|||
Kernel.srand config.seed
|
||||
end
|
||||
|
||||
require 'ostruct'
|
||||
require 'hanami/utils'
|
||||
|
||||
$LOAD_PATH.unshift 'lib'
|
||||
require 'hanami/mailer'
|
||||
|
||||
Hanami::Mailer.configure do
|
||||
root Pathname.new __dir__ + '/support/fixtures/templates'
|
||||
end
|
||||
|
||||
Hanami::Mailer.class_eval do
|
||||
def self.reset!
|
||||
self.configuration = configuration.duplicate
|
||||
configuration.reset!
|
||||
end
|
||||
end
|
||||
|
||||
Hanami::Mailer::Dsl.class_eval do
|
||||
def reset!
|
||||
@templates = {}
|
||||
end
|
||||
end
|
||||
|
||||
Hanami::Utils.require!("spec/support")
|
||||
|
||||
Hanami::Mailer.configure do
|
||||
root __dir__ + '/support/fixtures'
|
||||
delivery_method :test
|
||||
|
||||
prepare do
|
||||
include DefaultSubject
|
||||
end
|
||||
end.load!
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
module RSpec
|
||||
module Support
|
||||
module Context
|
||||
def self.included(base)
|
||||
base.class_eval do
|
||||
let(:configuration) do
|
||||
configuration = Hanami::Mailer::Configuration.new do |config|
|
||||
config.root = 'spec/support/fixtures'
|
||||
config.delivery_method = :test
|
||||
end
|
||||
|
||||
Hanami::Mailer.finalize(configuration)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.configure do |config|
|
||||
config.include(RSpec::Support::Context)
|
||||
end
|
|
@ -1,42 +1,36 @@
|
|||
class InvoiceMailer
|
||||
include Hanami::Mailer
|
||||
# frozen_string_literal: true
|
||||
|
||||
class InvoiceMailer < Hanami::Mailer
|
||||
template 'invoice'
|
||||
end
|
||||
|
||||
class RenderMailer
|
||||
include Hanami::Mailer
|
||||
class RenderMailer < Hanami::Mailer
|
||||
end
|
||||
|
||||
class TemplateEngineMailer
|
||||
include Hanami::Mailer
|
||||
class TemplateEngineMailer < Hanami::Mailer
|
||||
end
|
||||
|
||||
class CharsetMailer
|
||||
include Hanami::Mailer
|
||||
|
||||
class CharsetMailer < Hanami::Mailer
|
||||
from 'noreply@example.com'
|
||||
to 'user@example.com'
|
||||
subject 'こんにちは'
|
||||
end
|
||||
|
||||
class MissingFromMailer
|
||||
include Hanami::Mailer
|
||||
class MissingFromMailer < Hanami::Mailer
|
||||
template 'missing'
|
||||
|
||||
to 'recipient@example.com'
|
||||
subject 'Hello'
|
||||
end
|
||||
|
||||
class MissingToMailer
|
||||
include Hanami::Mailer
|
||||
class MissingToMailer < Hanami::Mailer
|
||||
template 'missing'
|
||||
|
||||
from 'sender@example.com'
|
||||
subject 'Hello'
|
||||
end
|
||||
|
||||
class CcOnlyMailer
|
||||
include Hanami::Mailer
|
||||
class CcOnlyMailer < Hanami::Mailer
|
||||
template 'missing'
|
||||
|
||||
cc 'recipient@example.com'
|
||||
|
@ -44,8 +38,7 @@ class CcOnlyMailer
|
|||
subject 'Hello'
|
||||
end
|
||||
|
||||
class BccOnlyMailer
|
||||
include Hanami::Mailer
|
||||
class BccOnlyMailer < Hanami::Mailer
|
||||
template 'missing'
|
||||
|
||||
bcc 'recipient@example.com'
|
||||
|
@ -55,35 +48,20 @@ end
|
|||
|
||||
User = Struct.new(:name, :email)
|
||||
|
||||
class LazyMailer
|
||||
include Hanami::Mailer
|
||||
class LazyMailer < Hanami::Mailer
|
||||
end
|
||||
|
||||
class MethodMailer
|
||||
include Hanami::Mailer
|
||||
class ProcMailer < Hanami::Mailer
|
||||
from ->(locals) { "hello-#{locals.fetch(:user).name.downcase}@example.com" }
|
||||
to ->(locals) { locals.fetch(:user).email }
|
||||
subject ->(locals) { "[Hanami] #{locals.fetch(:greeting)}" }
|
||||
|
||||
from :sender
|
||||
to :recipient
|
||||
subject :greeting
|
||||
|
||||
def greeting
|
||||
"Hello, #{user.name}"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def sender
|
||||
"hello-#{user.name.downcase}@example.com"
|
||||
end
|
||||
|
||||
def recipient
|
||||
user.email
|
||||
before do |_, locals|
|
||||
locals[:greeting] = "Hello, #{locals.fetch(:user).name}"
|
||||
end
|
||||
end
|
||||
|
||||
class WelcomeMailer
|
||||
include Hanami::Mailer
|
||||
|
||||
class WelcomeMailer < Hanami::Mailer
|
||||
from 'noreply@sender.com'
|
||||
to ['noreply@recipient.com', 'owner@recipient.com']
|
||||
cc 'cc@recipient.com'
|
||||
|
@ -91,12 +69,34 @@ class WelcomeMailer
|
|||
|
||||
subject 'Welcome'
|
||||
|
||||
before do |mail|
|
||||
mail.attachments['invoice.pdf'] = "/path/to/invoice-#{invoice_code}.pdf"
|
||||
end
|
||||
|
||||
def greeting
|
||||
'Ahoy'
|
||||
end
|
||||
|
||||
def prepare
|
||||
mail.attachments['invoice.pdf'] = '/path/to/invoice.pdf'
|
||||
def invoice_code
|
||||
"123"
|
||||
end
|
||||
end
|
||||
|
||||
class EventMailer < Hanami::Mailer
|
||||
from 'events@domain.test'
|
||||
to ->(locals) { locals.fetch(:user).email }
|
||||
subject ->(locals) { "Invitation: #{locals.fetch(:event).title}" }
|
||||
|
||||
before do |mail, locals|
|
||||
mail.attachments["invitation-#{locals.fetch(:event).id}.ics"] = generate_invitation_attachment(locals)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Simulate on-the-fly creation of an attachment file.
|
||||
# For speed purposes we're not gonna create the file, but only return a path.
|
||||
def generate_invitation_attachment(locals)
|
||||
"invitation-#{locals.fetch(:event).id}.ics"
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -111,8 +111,14 @@ class MandrillDeliveryMethod
|
|||
end
|
||||
|
||||
module Users
|
||||
class Welcome
|
||||
include Hanami::Mailer
|
||||
class Welcome < Hanami::Mailer
|
||||
end
|
||||
end
|
||||
|
||||
module Web
|
||||
module Mailers
|
||||
class SignupMailer < Hanami::Mailer
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
Fixture used by
|
||||
spec/unit/hanami/mailer/templates_finder_spec.rb
|
|
@ -0,0 +1,2 @@
|
|||
Fixture used by
|
||||
spec/unit/hanami/mailer/templates_finder_spec.rb
|
|
@ -0,0 +1,2 @@
|
|||
Fixture used by
|
||||
spec/unit/hanami/mailer/templates_finder_spec.rb
|
|
@ -1,21 +1,21 @@
|
|||
RSpec.describe Hanami::Mailer::Configuration do
|
||||
before do
|
||||
@configuration = Hanami::Mailer::Configuration.new
|
||||
end
|
||||
# frozen_string_literal: true
|
||||
|
||||
describe '#root' do
|
||||
RSpec.describe Hanami::Mailer::Configuration do
|
||||
subject { described_class.new }
|
||||
|
||||
describe '#root=' do
|
||||
describe 'when a value is given' do
|
||||
describe 'and it is a string' do
|
||||
it 'sets it as a Pathname' do
|
||||
@configuration.root 'spec'
|
||||
expect(@configuration.root).to eq(Pathname.new('spec').realpath)
|
||||
subject.root = 'spec'
|
||||
expect(subject.root).to eq(Pathname.new('spec').realpath)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'and it is a pathname' do
|
||||
it 'sets it' do
|
||||
@configuration.root Pathname.new('spec')
|
||||
expect(@configuration.root).to eq(Pathname.new('spec').realpath)
|
||||
subject.root = Pathname.new('spec')
|
||||
expect(subject.root).to eq(Pathname.new('spec').realpath)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -33,15 +33,15 @@ RSpec.describe Hanami::Mailer::Configuration do
|
|||
end
|
||||
|
||||
it 'sets the converted value' do
|
||||
@configuration.root RootPath.new('spec')
|
||||
expect(@configuration.root).to eq(Pathname.new('spec').realpath)
|
||||
subject.root = RootPath.new('spec')
|
||||
expect(subject.root).to eq(Pathname.new('spec').realpath)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'and it is an unexisting path' do
|
||||
it 'raises an error' do
|
||||
expect do
|
||||
@configuration.root '/path/to/unknown'
|
||||
subject.root = '/path/to/unknown'
|
||||
end.to raise_error(Errno::ENOENT)
|
||||
end
|
||||
end
|
||||
|
@ -49,146 +49,69 @@ RSpec.describe Hanami::Mailer::Configuration do
|
|||
|
||||
describe 'when a value is not given' do
|
||||
it 'defaults to the current path' do
|
||||
expect(@configuration.root).to eq(Pathname.new('.').realpath)
|
||||
expect(subject.root).to eq(Pathname.new('.').realpath)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#mailers' do
|
||||
it 'defaults to an empty set' do
|
||||
expect(@configuration.mailers).to be_empty
|
||||
end
|
||||
|
||||
it 'allows to add mailers' do
|
||||
@configuration.add_mailer(InvoiceMailer)
|
||||
expect(@configuration.mailers).to include(InvoiceMailer)
|
||||
end
|
||||
|
||||
it 'eliminates duplications' do
|
||||
@configuration.add_mailer(RenderMailer)
|
||||
@configuration.add_mailer(RenderMailer)
|
||||
|
||||
expect(@configuration.mailers.size).to eq(1)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#prepare' do
|
||||
before do
|
||||
module FooRendering
|
||||
def render
|
||||
'foo'
|
||||
end
|
||||
end
|
||||
|
||||
class PrepareMailer
|
||||
end
|
||||
end
|
||||
|
||||
after do
|
||||
Object.__send__(:remove_const, :FooRendering)
|
||||
Object.__send__(:remove_const, :PrepareMailer)
|
||||
end
|
||||
|
||||
it 'raises error in case of missing block' do
|
||||
expect { @configuration.prepare }.to raise_error(ArgumentError, 'Please provide a block')
|
||||
end
|
||||
end
|
||||
|
||||
# describe '#reset!' do
|
||||
# before do
|
||||
# @configuration.root 'test'
|
||||
# @configuration.add_mailer(InvoiceMailer)
|
||||
|
||||
# @configuration.reset!
|
||||
# end
|
||||
|
||||
# it 'resets root' do
|
||||
# root = Pathname.new('.').realpath
|
||||
|
||||
# @configuration.root.must_equal root
|
||||
# @configuration.mailers.must_be_empty
|
||||
# end
|
||||
|
||||
# it "doesn't reset namespace" do
|
||||
# @configuration.namespace(InvoiceMailer)
|
||||
# @configuration.reset!
|
||||
|
||||
# @configuration.namespace.must_equal(InvoiceMailer)
|
||||
# end
|
||||
|
||||
# end
|
||||
|
||||
describe '#load!' do
|
||||
before do
|
||||
@configuration.root 'spec'
|
||||
@configuration.load!
|
||||
end
|
||||
|
||||
it 'loads root' do
|
||||
root = Pathname.new('spec').realpath
|
||||
expect(@configuration.root).to eq(root)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#delivery_method' do
|
||||
describe 'when not previously set' do
|
||||
before do
|
||||
@configuration.reset!
|
||||
end
|
||||
|
||||
it 'defaults to SMTP' do
|
||||
expect(@configuration.delivery_method).to eq([:smtp, {}])
|
||||
expect(subject.delivery_method).to eq(:smtp)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'set with a symbol' do
|
||||
before do
|
||||
@configuration.delivery_method :exim, location: '/path/to/exim'
|
||||
subject.delivery_method = :exim, { location: '/path/to/exim' }
|
||||
end
|
||||
|
||||
it 'saves the delivery method in the configuration' do
|
||||
expect(@configuration.delivery_method).to eq([:exim, { location: '/path/to/exim' }])
|
||||
expect(subject.delivery_method).to eq([:exim, { location: '/path/to/exim' }])
|
||||
end
|
||||
end
|
||||
|
||||
describe 'set with a class' do
|
||||
before do
|
||||
@configuration.delivery_method MandrillDeliveryMethod,
|
||||
username: 'mandrill-username', password: 'mandrill-api-key'
|
||||
subject.delivery_method = MandrillDeliveryMethod,
|
||||
{ username: 'mandrill-username', password: 'mandrill-api-key' }
|
||||
end
|
||||
|
||||
it 'saves the delivery method in the configuration' do
|
||||
expect(@configuration.delivery_method).to eq([MandrillDeliveryMethod, username: 'mandrill-username', password: 'mandrill-api-key'])
|
||||
expect(subject.delivery_method).to eq([MandrillDeliveryMethod, username: 'mandrill-username', password: 'mandrill-api-key'])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#default_charset' do
|
||||
describe 'when not previously set' do
|
||||
before do
|
||||
@configuration.reset!
|
||||
end
|
||||
|
||||
it 'defaults to UTF-8' do
|
||||
expect(@configuration.default_charset).to eq('UTF-8')
|
||||
expect(subject.default_charset).to eq('UTF-8')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'when set' do
|
||||
before do
|
||||
@configuration.default_charset 'iso-8859-1'
|
||||
subject.default_charset = 'iso-8859-1'
|
||||
end
|
||||
|
||||
it 'saves the delivery method in the configuration' do
|
||||
expect(@configuration.default_charset).to eq('iso-8859-1')
|
||||
expect(subject.default_charset).to eq('iso-8859-1')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#prepare' do
|
||||
it 'injects code in each mailer'
|
||||
# it 'injects code in each mailer' do
|
||||
# InvoiceMailer.subject.must_equal 'default subject'
|
||||
# end
|
||||
describe "#freeze" do
|
||||
before do
|
||||
subject.freeze
|
||||
end
|
||||
|
||||
it "is frozen" do
|
||||
expect(subject).to be_frozen
|
||||
end
|
||||
|
||||
it "raises error if trying to add a mailer" do
|
||||
expect { subject.add_mailer(WelcomeMailer) }.to raise_error(RuntimeError)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,184 +0,0 @@
|
|||
RSpec.describe Hanami::Mailer do
|
||||
describe '.deliver' do
|
||||
before do
|
||||
Hanami::Mailer.deliveries.clear
|
||||
end
|
||||
|
||||
it 'can deliver with specified charset' do
|
||||
CharsetMailer.deliver(charset: charset = 'iso-2022-jp')
|
||||
|
||||
mail = Hanami::Mailer.deliveries.first
|
||||
expect(mail.charset).to eq(charset)
|
||||
expect(mail.parts.first.charset).to eq(charset)
|
||||
end
|
||||
|
||||
it "raises error when 'from' isn't specified" do
|
||||
expect { MissingFromMailer.deliver }.to raise_error(Hanami::Mailer::MissingDeliveryDataError)
|
||||
end
|
||||
|
||||
it "raises error when 'to' isn't specified" do
|
||||
expect { MissingToMailer.deliver }.to raise_error(Hanami::Mailer::MissingDeliveryDataError)
|
||||
end
|
||||
|
||||
it "doesn't raise error when 'cc' is specified but 'to' isn't" do
|
||||
CcOnlyMailer.deliver
|
||||
end
|
||||
|
||||
it "doesn't raise error when 'bcc' is specified but 'to' isn't" do
|
||||
BccOnlyMailer.deliver
|
||||
end
|
||||
|
||||
it "lets other errors to bubble up" do
|
||||
mailer = CharsetMailer.new({})
|
||||
mail = Class.new do
|
||||
def deliver
|
||||
raise ArgumentError, "ouch"
|
||||
end
|
||||
end.new
|
||||
|
||||
expect(mailer).to receive(:mail).and_return(mail)
|
||||
expect { mailer.deliver }.to raise_error(ArgumentError, "ouch")
|
||||
end
|
||||
|
||||
describe 'test delivery with hardcoded values' do
|
||||
before do
|
||||
WelcomeMailer.deliver
|
||||
@mail = Hanami::Mailer.deliveries.first
|
||||
end
|
||||
|
||||
after do
|
||||
Hanami::Mailer.deliveries.clear
|
||||
end
|
||||
|
||||
it 'delivers the mail' do
|
||||
expect(Hanami::Mailer.deliveries.length).to eq(1)
|
||||
end
|
||||
|
||||
it 'sends the correct information' do
|
||||
expect(@mail.from).to eq(['noreply@sender.com'])
|
||||
expect(@mail.to).to eq(['noreply@recipient.com', 'owner@recipient.com'])
|
||||
expect(@mail.cc).to eq(['cc@recipient.com'])
|
||||
expect(@mail.bcc).to eq(['bcc@recipient.com'])
|
||||
expect(@mail.subject).to eq('Welcome')
|
||||
end
|
||||
|
||||
it 'has the correct templates' do
|
||||
expect(@mail.html_part.to_s).to include(%(template))
|
||||
expect(@mail.text_part.to_s).to include(%(template))
|
||||
end
|
||||
|
||||
it 'interprets the prepare statement' do
|
||||
attachment = @mail.attachments['invoice.pdf']
|
||||
|
||||
expect(attachment).to be_kind_of(Mail::Part)
|
||||
|
||||
expect(attachment).to be_attachment
|
||||
expect(attachment).to_not be_inline
|
||||
expect(attachment).to_not be_multipart
|
||||
|
||||
expect(attachment.filename).to eq('invoice.pdf')
|
||||
|
||||
expect(attachment.content_type).to match('application/pdf')
|
||||
expect(attachment.content_type).to match('filename=invoice.pdf')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'test delivery with methods' do
|
||||
before do
|
||||
@user = User.new('Name', 'student@deigirls.com')
|
||||
MethodMailer.deliver(user: @user)
|
||||
|
||||
@mail = Hanami::Mailer.deliveries.first
|
||||
end
|
||||
|
||||
after do
|
||||
Hanami::Mailer.deliveries.clear
|
||||
end
|
||||
|
||||
it 'delivers the mail' do
|
||||
expect(Hanami::Mailer.deliveries.length).to eq(1)
|
||||
end
|
||||
|
||||
it 'sends the correct information' do
|
||||
expect(@mail.from).to eq(["hello-#{@user.name.downcase}@example.com"])
|
||||
expect(@mail.to).to eq([@user.email])
|
||||
expect(@mail.subject).to eq("Hello, #{@user.name}")
|
||||
end
|
||||
end
|
||||
|
||||
describe 'multipart' do
|
||||
after do
|
||||
Hanami::Mailer.deliveries.clear
|
||||
end
|
||||
|
||||
it 'delivers all the parts by default' do
|
||||
WelcomeMailer.deliver
|
||||
|
||||
mail = Hanami::Mailer.deliveries.first
|
||||
body = mail.body.encoded
|
||||
|
||||
expect(body).to include(%(<h1>Hello World!</h1>))
|
||||
expect(body).to include(%(This is a txt template))
|
||||
end
|
||||
|
||||
it 'can deliver only the text part' do
|
||||
WelcomeMailer.deliver(format: :txt)
|
||||
|
||||
mail = Hanami::Mailer.deliveries.first
|
||||
body = mail.body.encoded
|
||||
|
||||
expect(body).to_not include(%(<h1>Hello World!</h1>))
|
||||
expect(body).to include(%(This is a txt template))
|
||||
end
|
||||
|
||||
it 'can deliver only the html part' do
|
||||
WelcomeMailer.deliver(format: :html)
|
||||
|
||||
mail = Hanami::Mailer.deliveries.first
|
||||
body = mail.body.encoded
|
||||
|
||||
expect(body).to include(%(<h1>Hello World!</h1>))
|
||||
expect(body).to_not include(%(This is a txt template))
|
||||
end
|
||||
end
|
||||
|
||||
describe 'custom delivery' do
|
||||
before do
|
||||
@options = options = { deliveries: [] }
|
||||
|
||||
# Hanami::Mailer.reset!
|
||||
# Hanami::Mailer.configure do
|
||||
# delivery_method MandrillDeliveryMethod, options
|
||||
# end.load!
|
||||
|
||||
WelcomeMailer.deliver
|
||||
|
||||
@mail = options.fetch(:deliveries).first
|
||||
end
|
||||
|
||||
it 'delivers the mail'
|
||||
# it 'delivers the mail' do
|
||||
# @options.fetch(:deliveries).length.must_equal 1
|
||||
# end
|
||||
|
||||
# it 'sends the correct information' do
|
||||
# @mail.from.must_equal ['noreply@sender.com']
|
||||
# @mail.to.must_equal ['noreply@recipient.com']
|
||||
# @mail.subject.must_equal "Welcome"
|
||||
# end
|
||||
|
||||
# it 'has the correct templates' do
|
||||
# @mail.html_part.to_s.must_include %(template)
|
||||
# @mail.text_part.to_s.must_include %(template)
|
||||
# end
|
||||
|
||||
# it 'interprets the prepare statement' do
|
||||
# @mail.attachments['invoice.pdf'].wont_be_nil
|
||||
# end
|
||||
|
||||
# it 'adds the attachment to the mail object' do
|
||||
# @mail.attachments['render_mailer.html.erb'].wont_be_nil
|
||||
# end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,47 +1,123 @@
|
|||
RSpec.describe Hanami::Mailer do
|
||||
describe '.template' do
|
||||
describe 'when no value is set' do
|
||||
it 'returns the convention name' do
|
||||
expect(RenderMailer.template).to eq('render_mailer')
|
||||
end
|
||||
# frozen_string_literal: true
|
||||
|
||||
it 'returns correct namespaced value' do
|
||||
expect(Users::Welcome.template).to eq('users/welcome')
|
||||
end
|
||||
RSpec.describe Hanami::Mailer::Dsl do
|
||||
let(:mailer) { Class.new { extend Hanami::Mailer::Dsl } }
|
||||
|
||||
describe '.from' do
|
||||
it 'returns the default value' do
|
||||
expect(mailer.from).to be(nil)
|
||||
end
|
||||
|
||||
describe 'when a value is set' do
|
||||
it 'returns that name' do
|
||||
expect(InvoiceMailer.template).to eq('invoice')
|
||||
end
|
||||
it 'sets the value' do
|
||||
sender = 'sender@hanami.test'
|
||||
mailer.from sender
|
||||
|
||||
expect(mailer.from).to eq(sender)
|
||||
end
|
||||
end
|
||||
|
||||
describe '.templates' do
|
||||
describe 'when no value is set' do
|
||||
it 'returns a set of templates' do
|
||||
template_formats = LazyMailer.templates.keys
|
||||
expect(template_formats).to eq(%i[html txt])
|
||||
end
|
||||
|
||||
it 'returns only the template for the given format' do
|
||||
template = LazyMailer.templates(:txt)
|
||||
expect(template).to be_kind_of(Hanami::Mailer::Template)
|
||||
expect(template.file).to match(%r{spec/support/fixtures/templates/lazy_mailer.txt.erb\z})
|
||||
end
|
||||
describe '.to' do
|
||||
it 'returns the default value' do
|
||||
expect(mailer.to).to be(nil)
|
||||
end
|
||||
|
||||
describe 'when a value is set' do
|
||||
it 'returns a set of templates' do
|
||||
template_formats = InvoiceMailer.templates.keys
|
||||
expect(template_formats).to eq([:html])
|
||||
end
|
||||
it 'sets a single value' do
|
||||
recipient = 'recipient@hanami.test'
|
||||
mailer.to recipient
|
||||
|
||||
it 'returns only the template for the given format' do
|
||||
template = InvoiceMailer.templates(:html)
|
||||
expect(template).to be_kind_of(Hanami::Mailer::Template)
|
||||
expect(template.file).to match(%r{spec/support/fixtures/templates/invoice.html.erb\z})
|
||||
end
|
||||
expect(mailer.to).to eq(recipient)
|
||||
end
|
||||
|
||||
it 'sets an array of values' do
|
||||
recipients = ['recipient@hanami.test']
|
||||
mailer.to recipients
|
||||
|
||||
expect(mailer.to).to eq(recipients)
|
||||
end
|
||||
end
|
||||
|
||||
describe '.cc' do
|
||||
it 'returns the default value' do
|
||||
expect(mailer.cc).to be(nil)
|
||||
end
|
||||
|
||||
it 'sets a single value' do
|
||||
recipient = 'cc@hanami.test'
|
||||
mailer.cc recipient
|
||||
|
||||
expect(mailer.cc).to eq(recipient)
|
||||
end
|
||||
|
||||
it 'sets an array of values' do
|
||||
recipients = ['cc@hanami.test']
|
||||
mailer.cc recipients
|
||||
|
||||
expect(mailer.cc).to eq(recipients)
|
||||
end
|
||||
end
|
||||
|
||||
describe '.bcc' do
|
||||
it 'returns the default value' do
|
||||
expect(mailer.bcc).to be(nil)
|
||||
end
|
||||
|
||||
it 'sets a single value' do
|
||||
recipient = 'bcc@hanami.test'
|
||||
mailer.bcc recipient
|
||||
|
||||
expect(mailer.bcc).to eq(recipient)
|
||||
end
|
||||
|
||||
it 'sets an array of values' do
|
||||
recipients = ['bcc@hanami.test']
|
||||
mailer.bcc recipients
|
||||
|
||||
expect(mailer.bcc).to eq(recipients)
|
||||
end
|
||||
end
|
||||
|
||||
describe '.subject' do
|
||||
it 'returns the default value' do
|
||||
expect(mailer.subject).to be(nil)
|
||||
end
|
||||
|
||||
it 'sets a value' do
|
||||
mail_subject = 'Hello'
|
||||
mailer.subject mail_subject
|
||||
|
||||
expect(mailer.subject).to eq(mail_subject)
|
||||
end
|
||||
end
|
||||
|
||||
describe '.template' do
|
||||
it 'sets a value' do
|
||||
mailer.template 'file'
|
||||
end
|
||||
end
|
||||
|
||||
describe '.template_name' do
|
||||
it 'returns the default value' do
|
||||
expect(mailer.template_name).to be(nil)
|
||||
end
|
||||
|
||||
it 'returns value, if set' do
|
||||
template = 'file'
|
||||
mailer.template template
|
||||
|
||||
expect(mailer.template_name).to eq(template)
|
||||
end
|
||||
end
|
||||
|
||||
describe '.before' do
|
||||
it 'returns the default value' do
|
||||
expect(mailer.before).to be_kind_of(Proc)
|
||||
end
|
||||
|
||||
it 'sets a value' do
|
||||
blk = ->(*) {}
|
||||
mailer.before(&blk)
|
||||
|
||||
expect(mailer.before).to eq(blk)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe Hanami::Mailer::Error do
|
||||
it "inherits from StandardError" do
|
||||
expect(described_class.ancestors).to include(StandardError)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,28 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe Hanami::Mailer::Finalizer do
|
||||
let(:configuration) do
|
||||
Hanami::Mailer::Configuration.new do |config|
|
||||
config.root = 'spec/support/fixtures'
|
||||
end
|
||||
end
|
||||
|
||||
let(:mailers) { [double('mailer', template_name: "invoice")] }
|
||||
|
||||
describe '.finalize' do
|
||||
it 'eager autoloads modules from mail gem' do
|
||||
expect(Mail).to receive(:eager_autoload!)
|
||||
described_class.finalize(mailers, configuration)
|
||||
end
|
||||
|
||||
it "adds the mailer to the configuration" do
|
||||
expect(configuration).to receive(:add_mailer).with(mailers.first)
|
||||
described_class.finalize(mailers, configuration)
|
||||
end
|
||||
|
||||
it "returns frozen configuration" do
|
||||
actual = described_class.finalize(mailers, configuration)
|
||||
expect(actual).to be_frozen
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe Hanami::Mailer::MissingDeliveryDataError do
|
||||
it "inherits from Hanami::Error" do
|
||||
expect(described_class.ancestors).to include(Hanami::Mailer::Error)
|
||||
end
|
||||
|
||||
it "has a custom error message" do
|
||||
expect { raise described_class }.to raise_error(described_class, "Missing delivery data, please check 'from', or 'to'")
|
||||
end
|
||||
end
|
|
@ -1,44 +0,0 @@
|
|||
RSpec.describe Hanami::Mailer do
|
||||
describe '#render' do
|
||||
describe 'when template is explicitly declared' do
|
||||
let(:mailer) { InvoiceMailer.new }
|
||||
|
||||
it 'renders the given template' do
|
||||
expect(mailer.render(:html)).to include(%(<h1>Invoice template</h1>))
|
||||
end
|
||||
end
|
||||
|
||||
describe 'when template is implicitly declared' do
|
||||
let(:mailer) { LazyMailer.new }
|
||||
|
||||
it 'looks for template with same name with inflected classname and render it' do
|
||||
expect(mailer.render(:html)).to include(%(Hello World))
|
||||
expect(mailer.render(:txt)).to include(%(This is a txt template))
|
||||
end
|
||||
end
|
||||
|
||||
describe 'when mailer defines context' do
|
||||
let(:mailer) { WelcomeMailer.new }
|
||||
|
||||
it 'renders template with defined context' do
|
||||
expect(mailer.render(:txt)).to include(%(Ahoy))
|
||||
end
|
||||
end
|
||||
|
||||
describe 'when locals are parsed in' do
|
||||
let(:mailer) { RenderMailer.new(user: User.new('Luca')) }
|
||||
|
||||
it 'renders template with parsed locals' do
|
||||
expect(mailer.render(:html)).to include(%(Luca))
|
||||
end
|
||||
end
|
||||
|
||||
describe 'with HAML template engine' do
|
||||
let(:mailer) { TemplateEngineMailer.new(user: User.new('Luca')) }
|
||||
|
||||
it 'renders template with parsed locals' do
|
||||
expect(mailer.render(:html)).to include(%(<h1>\nLuca\n</h1>\n))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,67 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe Hanami::Mailer::TemplateName do
|
||||
describe ".call" do
|
||||
context "with top level namespace" do
|
||||
let(:namespace) { Object }
|
||||
|
||||
it "returns an instance of ::String" do
|
||||
template_name = described_class.call("template", namespace)
|
||||
expect(template_name).to be_kind_of(String)
|
||||
end
|
||||
|
||||
it "returns name from plain name" do
|
||||
template_name = described_class.call("template", namespace)
|
||||
expect(template_name).to eq("template")
|
||||
end
|
||||
|
||||
it "returns name from camel case name" do
|
||||
template_name = described_class.call("ATemplate", namespace)
|
||||
expect(template_name).to eq("a_template")
|
||||
end
|
||||
|
||||
it "returns name from snake case name" do
|
||||
template_name = described_class.call("a_template", namespace)
|
||||
expect(template_name).to eq("a_template")
|
||||
end
|
||||
|
||||
it "returns name from modulized name" do
|
||||
template_name = described_class.call("Mailers::WelcomeMailer", namespace)
|
||||
expect(template_name).to eq("mailers/welcome_mailer")
|
||||
end
|
||||
|
||||
it "returns name from class" do
|
||||
template_name = described_class.call(InvoiceMailer.name, namespace)
|
||||
expect(template_name).to eq("invoice_mailer")
|
||||
end
|
||||
|
||||
it "returns name from modulized class" do
|
||||
template_name = described_class.call(Users::Welcome.name, namespace)
|
||||
expect(template_name).to eq("users/welcome")
|
||||
end
|
||||
|
||||
it "returns blank string from blank name" do
|
||||
template_name = described_class.call("", namespace)
|
||||
expect(template_name).to eq("")
|
||||
end
|
||||
|
||||
it "raises error with nil name" do
|
||||
expect { described_class.call(nil, namespace) }.to raise_error(NoMethodError)
|
||||
end
|
||||
end
|
||||
|
||||
context "with application namespace" do
|
||||
let(:namespace) { Web::Mailers }
|
||||
|
||||
it "returns name from class name" do
|
||||
template_name = described_class.call("SignupMailer", namespace)
|
||||
expect(template_name).to eq("signup_mailer")
|
||||
end
|
||||
|
||||
it "returns name from modulized class name" do
|
||||
template_name = described_class.call(Web::Mailers::SignupMailer.name, namespace)
|
||||
expect(template_name).to eq("signup_mailer")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,46 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe Hanami::Mailer::Template do
|
||||
subject { described_class.new(file) }
|
||||
let(:file) { "spec/support/fixtures/templates/welcome_mailer.txt.erb" }
|
||||
|
||||
describe "#initialize" do
|
||||
context "with existing file" do
|
||||
it "instantiates template" do
|
||||
expect(subject).to be_kind_of(described_class)
|
||||
end
|
||||
|
||||
it "initialize frozen instance" do
|
||||
expect(subject).to be_frozen
|
||||
end
|
||||
end
|
||||
|
||||
context "with missing template engine" do
|
||||
it "returns error" do
|
||||
expect { described_class.new("Gemfile") }.to raise_error(RuntimeError, "No template engine registered for Gemfile")
|
||||
end
|
||||
end
|
||||
|
||||
context "with unexisting file" do
|
||||
it "returns error" do
|
||||
expect { described_class.new("foo.erb") }.to raise_error(Errno::ENOENT)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#render" do
|
||||
it "renders template" do
|
||||
scope = Object.new
|
||||
actual = subject.render(scope, greeting: "Hello")
|
||||
|
||||
expect(actual).to eq("This is a txt template\nHello")
|
||||
end
|
||||
|
||||
it "renders with unfrozen object" do
|
||||
scope = Object.new
|
||||
expect(scope).to receive(:dup)
|
||||
|
||||
subject.render(scope, greeting: "Hello")
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,75 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe Hanami::Mailer::TemplatesFinder do
|
||||
subject { described_class.new(root) }
|
||||
# NOTE: please do not change this name, because `#find` specs are relying on
|
||||
# template fixtures. See all the fixtures under: spec/support/fixtures/templates
|
||||
let(:template_name) { "welcome_mailer" }
|
||||
let(:root) { configuration.root }
|
||||
|
||||
describe "#initialize" do
|
||||
context "with valid root" do
|
||||
it "instantiates a new finder instance" do
|
||||
expect(subject).to be_kind_of(described_class)
|
||||
end
|
||||
|
||||
it "returns a frozen object" do
|
||||
expect(subject).to be_frozen
|
||||
end
|
||||
end
|
||||
|
||||
context "with unexisting root" do
|
||||
let(:root) { "path/to/unexisting" }
|
||||
|
||||
it "raises error" do
|
||||
expect { subject }.to raise_error(Errno::ENOENT)
|
||||
end
|
||||
end
|
||||
|
||||
context "with nil root" do
|
||||
let(:root) { nil }
|
||||
|
||||
it "raises error" do
|
||||
expect { subject }.to raise_error(TypeError)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#find" do
|
||||
context "with valid template name" do
|
||||
it "returns templates" do
|
||||
actual = subject.find(template_name)
|
||||
|
||||
# It excludes all the files that aren't matching the convention:
|
||||
#
|
||||
# `<template_name>.<format>.<template_engine>`
|
||||
#
|
||||
# Under `spec/support/fixtures/templates` we have the following files:
|
||||
#
|
||||
# * welcome_mailer
|
||||
# * welcome_mailer.erb
|
||||
# * welcome_mailer.html
|
||||
# * welcome_mailer.html.erb
|
||||
# * welcome_mailer.txt.erb
|
||||
#
|
||||
# Only the last two are matching the pattern, here's why we have only
|
||||
# two templates loaded.
|
||||
expect(actual.keys).to eq(%i[html txt])
|
||||
actual.each_value do |template|
|
||||
expect(template).to be_kind_of(Hanami::Mailer::Template)
|
||||
expect(template.instance_variable_get(:@_template).__send__(:file)).to match(%r{spec/support/fixtures/templates/welcome_mailer.(html|txt).erb})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "with missing template" do
|
||||
let(:template_name) { "missing_template" }
|
||||
|
||||
it "doesn't return templates" do
|
||||
actual = subject.find(template_name)
|
||||
|
||||
expect(actual).to be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe Hanami::Mailer::UnknownMailerError do
|
||||
it "inherits from Hanami::Error" do
|
||||
expect(described_class.ancestors).to include(Hanami::Mailer::Error)
|
||||
end
|
||||
|
||||
it "has a custom error message" do
|
||||
mailer = InvoiceMailer
|
||||
expect { raise described_class.new(mailer) }.to raise_error(described_class, "Unknown mailer: #{mailer}. Please finalize the configuration before to use it.")
|
||||
end
|
||||
|
||||
it "has explicit handling for nil" do
|
||||
mailer = nil
|
||||
expect { raise described_class.new(mailer) }.to raise_error(described_class, "Unknown mailer: #{mailer.inspect}. Please finalize the configuration before to use it.")
|
||||
end
|
||||
end
|
|
@ -1,3 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe "Hanami::Mailer::VERSION" do
|
||||
it "returns current version" do
|
||||
expect(Hanami::Mailer::VERSION).to eq("1.1.0")
|
||||
|
|
|
@ -0,0 +1,168 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe Hanami::Mailer do
|
||||
context "constants" do
|
||||
it "marks them as private" do
|
||||
expect { described_class::CONTENT_TYPES }.to raise_error(NameError)
|
||||
end
|
||||
end
|
||||
|
||||
context ".finalize" do
|
||||
let(:configuration) { Hanami::Mailer::Configuration.new }
|
||||
|
||||
it "finalizes the given configuration" do
|
||||
actual = described_class.finalize(configuration)
|
||||
expect(actual).to be_frozen
|
||||
end
|
||||
end
|
||||
|
||||
context "#initialize" do
|
||||
let(:configuration) { Hanami::Mailer::Configuration.new }
|
||||
|
||||
it "builds an frozen instance" do
|
||||
mailer = described_class.new(configuration: configuration)
|
||||
expect(mailer).to be_frozen
|
||||
end
|
||||
|
||||
it "prevents memoization mutations" do
|
||||
mailer = Class.new(described_class) do
|
||||
def self.name
|
||||
"memoization_attempt"
|
||||
end
|
||||
|
||||
def foo
|
||||
@foo ||= "foo"
|
||||
end
|
||||
end.new(configuration: configuration)
|
||||
|
||||
expect { mailer.foo }.to raise_error(RuntimeError)
|
||||
end
|
||||
|
||||
it "prevents accidental configuration removal" do
|
||||
mailer = Class.new(described_class) do
|
||||
def self.name
|
||||
"configuration_removal"
|
||||
end
|
||||
|
||||
def foo
|
||||
@configuration = nil
|
||||
end
|
||||
end.new(configuration: configuration)
|
||||
|
||||
expect { mailer.foo }.to raise_error(RuntimeError)
|
||||
end
|
||||
end
|
||||
|
||||
context "#deliver" do
|
||||
context "when mailer has from/to defined with DSL" do
|
||||
let(:mailer) { CharsetMailer.new(configuration: configuration) }
|
||||
|
||||
it "delivers email with valid locals" do
|
||||
mail = mailer.deliver({})
|
||||
expect(mail).to be_kind_of(Mail::Message)
|
||||
end
|
||||
|
||||
it "is aliased as #call" do
|
||||
mail = mailer.call({})
|
||||
expect(mail).to be_kind_of(Mail::Message)
|
||||
end
|
||||
|
||||
it "raises error when locals are nil" do
|
||||
expect { mailer.deliver(nil) }.to raise_error(NoMethodError)
|
||||
end
|
||||
end
|
||||
|
||||
context "when from/to are missing" do
|
||||
let(:mailer) { InvoiceMailer.new(configuration: configuration) }
|
||||
|
||||
it "raises error" do
|
||||
expect { mailer.deliver({}) }.to raise_error(Hanami::Mailer::MissingDeliveryDataError)
|
||||
end
|
||||
end
|
||||
|
||||
context "when using #{described_class} directly" do
|
||||
let(:mailer) do
|
||||
described_class.new(configuration: configuration)
|
||||
end
|
||||
|
||||
it "raises error" do
|
||||
expect { mailer.deliver({}) }.to raise_error(NoMethodError)
|
||||
end
|
||||
end
|
||||
|
||||
context "with non-finalized configuration" do
|
||||
let(:configuration) { Hanami::Mailer::Configuration.new }
|
||||
|
||||
let(:mailer) do
|
||||
Class.new(described_class) do
|
||||
def self.name
|
||||
"anonymous_mailer"
|
||||
end
|
||||
end.new(configuration: configuration)
|
||||
end
|
||||
|
||||
it "raises error" do
|
||||
expect { mailer.deliver({}) }.to raise_error(Hanami::Mailer::UnknownMailerError)
|
||||
end
|
||||
end
|
||||
|
||||
context "locals" do
|
||||
let(:mailer) { EventMailer.new(configuration: configuration) }
|
||||
let(:user) { OpenStruct.new(name: "Luca", email: "luca@domain.test") }
|
||||
let(:event) { OpenStruct.new(id: 23, title: "Event #23") }
|
||||
|
||||
it "uses locals during the delivery process" do
|
||||
mail = mailer.deliver(user: user, event: event)
|
||||
|
||||
expect(mail.to).to eq(["luca@domain.test"])
|
||||
expect(mail.subject).to eq("Invitation: Event #23")
|
||||
expect(mail.attachments[0].filename).to eq("invitation-23.ics")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#render' do
|
||||
describe 'when template is explicitly declared' do
|
||||
let(:mailer) { InvoiceMailer.new(configuration: configuration) }
|
||||
|
||||
it 'renders the given template' do
|
||||
expect(mailer.render(:html, {})).to include(%(<h1>Invoice template</h1>))
|
||||
end
|
||||
end
|
||||
|
||||
describe 'when template is implicitly declared' do
|
||||
let(:mailer) { LazyMailer.new(configuration: configuration) }
|
||||
|
||||
it 'looks for template with same name with inflected classname and render it' do
|
||||
expect(mailer.render(:html, {})).to include(%(Hello World))
|
||||
expect(mailer.render(:txt, {})).to include(%(This is a txt template))
|
||||
end
|
||||
end
|
||||
|
||||
describe 'when mailer defines context' do
|
||||
let(:mailer) { WelcomeMailer.new(configuration: configuration) }
|
||||
|
||||
it 'renders template with defined context' do
|
||||
expect(mailer.render(:txt, {})).to include(%(Ahoy))
|
||||
end
|
||||
end
|
||||
|
||||
describe 'when locals are parsed in' do
|
||||
let(:mailer) { RenderMailer.new(configuration: configuration) }
|
||||
let(:locals) { { user: User.new('Luca') } }
|
||||
|
||||
it 'renders template with parsed locals' do
|
||||
expect(mailer.render(:html, locals)).to include(locals.fetch(:user).name)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'with HAML template engine' do
|
||||
let(:mailer) { TemplateEngineMailer.new(configuration: configuration) }
|
||||
let(:locals) { { user: User.new('MG') } }
|
||||
|
||||
it 'renders template with parsed locals' do
|
||||
expect(mailer.render(:html, locals)).to include(%(<h1>\n#{locals.fetch(:user).name}\n</h1>\n))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue