hanami-mailer 0.0.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,23 +1,29 @@
1
- # coding: utf-8
2
1
  lib = File.expand_path('../lib', __FILE__)
3
2
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
3
  require 'hanami/mailer/version'
5
4
 
6
5
  Gem::Specification.new do |spec|
7
- spec.name = "hanami-mailer"
6
+ spec.name = 'hanami-mailer'
8
7
  spec.version = Hanami::Mailer::VERSION
9
- spec.authors = ["Luca Guidi"]
10
- spec.email = ["me@lucaguidi.com"]
8
+ spec.authors = ['Luca Guidi', 'Ines Coelho', 'Rosa Faria']
9
+ spec.email = ['me@lucaguidi.com', 'ines.opcoelho@gmail.com', 'rosa1853@live.com']
11
10
 
12
- spec.summary = %q{The web, with simplicity}
13
- spec.description = %q{Hanami is a web framework for Ruby}
14
- spec.homepage = "http://hanamirb.org"
11
+ spec.summary = %q{Mail for Ruby applications.}
12
+ spec.description = %q{Mail for Ruby applications and Hanami mailers}
13
+ spec.homepage = 'http://hanamirb.org'
14
+ spec.license = 'MIT'
15
15
 
16
- spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
17
- spec.bindir = "exe"
16
+ spec.files = `git ls-files -- lib/* CHANGELOG.md LICENSE.md README.md hanami-mailer.gemspec`.split($/)
17
+ spec.bindir = 'exe'
18
18
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
19
- spec.require_paths = ["lib"]
19
+ spec.require_paths = ['lib']
20
+ spec.required_ruby_version = '>= 2.0.0'
20
21
 
21
- spec.add_development_dependency "bundler", "~> 1.11"
22
- spec.add_development_dependency "rake", "~> 10.0"
22
+ spec.add_dependency 'hanami-utils', '~> 0.7'
23
+ spec.add_dependency 'tilt', '~> 2.0', '>= 2.0.1'
24
+ spec.add_dependency 'mail', '~> 2.5'
25
+
26
+ spec.add_development_dependency 'bundler', '~> 1.10'
27
+ spec.add_development_dependency 'rake', '~> 10.0'
28
+ spec.add_development_dependency 'minitest', '~> 5.7'
23
29
  end
@@ -0,0 +1 @@
1
+ require 'hanami/mailer'
@@ -1,7 +1,316 @@
1
- require "hanami/mailer/version"
1
+ require 'hanami/utils/class_attribute'
2
+ require 'hanami/mailer/version'
3
+ require 'hanami/mailer/configuration'
4
+ require 'hanami/mailer/dsl'
5
+ require 'mail'
2
6
 
3
7
  module Hanami
8
+ # Hanami::Mailer
9
+ #
10
+ # @since 0.1.0
4
11
  module Mailer
5
- # Your code goes here...
12
+ # Base error for Hanami::Mailer
13
+ #
14
+ # @since 0.1.0
15
+ class Error < ::StandardError
16
+ end
17
+
18
+ # Missing delivery data error
19
+ #
20
+ # It's raised when a mailer doesn't specify <tt>from</tt> or <tt>to</tt>.
21
+ #
22
+ # @since 0.1.0
23
+ class MissingDeliveryDataError < Error
24
+ def initialize
25
+ super("Missing delivery data, please check 'from', or 'to'")
26
+ end
27
+ end
28
+
29
+ # Content types mapping
30
+ #
31
+ # @since 0.1.0
32
+ # @api private
33
+ CONTENT_TYPES = {
34
+ html: 'text/html',
35
+ txt: 'text/plain'
36
+ }.freeze
37
+
38
+ include Utils::ClassAttribute
39
+
40
+ # @since 0.1.0
41
+ # @api private
42
+ class_attribute :configuration
43
+ self.configuration = Configuration.new
44
+
45
+ # Configure the framework.
46
+ # It yields the given block in the context of the configuration
47
+ #
48
+ # @param blk [Proc] the configuration block
49
+ #
50
+ # @since 0.1.0
51
+ #
52
+ # @see Hanami::Mailer::Configuration
53
+ #
54
+ # @example
55
+ # require 'hanami/mailer'
56
+ #
57
+ # Hanami::Mailer.configure do
58
+ # root '/path/to/root'
59
+ # end
60
+ def self.configure(&blk)
61
+ configuration.instance_eval(&blk)
62
+ self
63
+ end
64
+
65
+ # Override Ruby's hook for modules.
66
+ # It includes basic Hanami::Mailer modules to the given Class.
67
+ # It sets a copy of the framework configuration
68
+ #
69
+ # @param base [Class] the target mailer
70
+ #
71
+ # @since 0.1.0
72
+ # @api private
73
+ #
74
+ # @see http://www.ruby-doc.org/core/Module.html#method-i-included
75
+ def self.included(base)
76
+ conf = self.configuration
77
+ conf.add_mailer(base)
78
+
79
+ base.class_eval do
80
+ extend Dsl
81
+ extend ClassMethods
82
+
83
+ include Utils::ClassAttribute
84
+ class_attribute :configuration
85
+
86
+ self.configuration = conf.duplicate
87
+ end
88
+
89
+ conf.copy!(base)
90
+ end
91
+
92
+ # Test deliveries
93
+ #
94
+ # This is a collection of delivered messages, used when <tt>delivery_method</tt>
95
+ # is set on <tt>:test</tt>
96
+ #
97
+ # @return [Array] a collection of delivered messages
98
+ #
99
+ # @since 0.1.0
100
+ #
101
+ # @see Hanami::Mailer::Configuration#delivery_mode
102
+ #
103
+ # @example
104
+ # require 'hanami/mailer'
105
+ #
106
+ # Hanami::Mailer.configure do
107
+ # delivery_method :test
108
+ # end.load!
109
+ #
110
+ # # In testing code
111
+ # Signup::Welcome.deliver
112
+ # Hanami::Mailer.deliveries.count # => 1
113
+ def self.deliveries
114
+ Mail::TestMailer.deliveries
115
+ end
116
+
117
+ # Load the framework
118
+ #
119
+ # @since 0.1.0
120
+ # @api private
121
+ def self.load!
122
+ Mail.eager_autoload!
123
+ configuration.load!
124
+ end
125
+
126
+ # @since 0.1.0
127
+ module ClassMethods
128
+ # Delivers a multipart email message.
129
+ #
130
+ # When a mailer defines a <tt>html</tt> and <tt>txt</tt> template, they are
131
+ # both delivered.
132
+ #
133
+ # In order to selectively deliver only one of the two templates, use
134
+ # <tt>Signup::Welcome.deliver(format: :txt)</tt>
135
+ #
136
+ # All the given locals, excepted the reserved ones (<tt>:format</tt> and
137
+ # <tt>charset</tt>), are avaliable as rendering context for the templates.
138
+ #
139
+ # @param locals [Hash] a set of objects that acts as context for the rendering
140
+ # @option :format [Symbol] specify format to deliver
141
+ # @option :charset [String] charset
142
+ #
143
+ # @since 0.1.0
144
+ #
145
+ # @see Hanami::Mailer::Configuration#default_charset
146
+ #
147
+ # @example
148
+ # require 'hanami/mailer'
149
+ #
150
+ # Hanami::Mailer.configure do
151
+ # delivery_method :smtp
152
+ # end.load!
153
+ #
154
+ # module Billing
155
+ # class Invoice
156
+ # include Hanami::Mailer
157
+ #
158
+ # from 'noreply@example.com'
159
+ # to :recipient
160
+ # subject :subject_line
161
+ #
162
+ # def prepare
163
+ # mail.attachments['invoice.pdf'] = File.read('/path/to/invoice.pdf')
164
+ # end
165
+ #
166
+ # private
167
+ #
168
+ # def recipient
169
+ # user.email
170
+ # end
171
+ #
172
+ # def subject_line
173
+ # "Invoice - #{ invoice.number }"
174
+ # end
175
+ # end
176
+ # end
177
+ #
178
+ # invoice = Invoice.new
179
+ # user = User.new(name: 'L', email: 'user@example.com')
180
+ #
181
+ # Billing::Invoice.deliver(invoice: invoice, user: user) # Deliver both text, HTML parts and the attachment
182
+ # Billing::Invoice.deliver(invoice: invoice, user: user, format: :txt) # Deliver only the text part and the attachment
183
+ # Billing::Invoice.deliver(invoice: invoice, user: user, format: :html) # Deliver only the text part and the attachment
184
+ # Billing::Invoice.deliver(invoice: invoice, user: user, charset: 'iso-8859') # Deliver both the parts with "iso-8859"
185
+ def deliver(locals = {})
186
+ new(locals).deliver
187
+ end
188
+ end
189
+
190
+ # Initialize a mailer
191
+ #
192
+ # @param locals [Hash] a set of objects that acts as context for the rendering
193
+ # @option :format [Symbol] specify format to deliver
194
+ # @option :charset [String] charset
195
+ #
196
+ # @since 0.1.0
197
+ def initialize(locals = {})
198
+ @locals = locals
199
+ @format = locals.fetch(:format, nil)
200
+ @charset = charset = locals.fetch(:charset, self.class.configuration.default_charset)
201
+ @mail = Mail.new.tap do |m|
202
+ m.from = __dsl(:from)
203
+ m.to = __dsl(:to)
204
+ m.subject = __dsl(:subject)
205
+
206
+ m.charset = charset
207
+ m.html_part = __part(:html)
208
+ m.text_part = __part(:txt)
209
+
210
+ m.delivery_method(*Hanami::Mailer.configuration.delivery_method)
211
+ end
212
+
213
+ prepare
214
+ end
215
+
216
+ # Render a single template with the specified format.
217
+ #
218
+ # @param format [Symbol] format
219
+ #
220
+ # @return [String] the output of the rendering process.
221
+ #
222
+ # @since 0.1.0
223
+ # @api private
224
+ def render(format)
225
+ self.class.templates(format).render(self, @locals)
226
+ end
227
+
228
+ # Delivers a multipart email, by looking at all the associated templates and render them.
229
+ #
230
+ # @since 0.1.0
231
+ # @api private
232
+ def deliver
233
+ mail.deliver
234
+ rescue ArgumentError
235
+ raise MissingDeliveryDataError
236
+ end
237
+
238
+ protected
239
+
240
+ # Prepare the email message when a new mailer is initialized.
241
+ #
242
+ # This is a hook that can be overwritten by mailers.
243
+ #
244
+ # @since 0.1.0
245
+ #
246
+ # @example
247
+ # require 'hanami/mailer'
248
+ #
249
+ # module Billing
250
+ # class Invoice
251
+ # include Hanami::Mailer
252
+ #
253
+ # subject 'Invoice'
254
+ # from 'noreply@example.com'
255
+ # to ''
256
+ #
257
+ # def prepare
258
+ # mail.attachments['invoice.pdf'] = File.read('/path/to/invoice.pdf')
259
+ # end
260
+ #
261
+ # private
262
+ #
263
+ # def recipient
264
+ # user.email
265
+ # end
266
+ # end
267
+ # end
268
+ #
269
+ # invoice = Invoice.new
270
+ # user = User.new(name: 'L', email: 'user@example.com')
271
+ def prepare
272
+ end
273
+
274
+ # @private
275
+ # @since 0.1.0
276
+ def method_missing(m)
277
+ @locals.fetch(m) { super }
278
+ end
279
+
280
+ # @since 0.1.0
281
+ attr_reader :mail
282
+
283
+ # @private
284
+ # @since 0.1.0
285
+ attr_reader :charset
286
+
287
+ private
288
+
289
+ # @private
290
+ # @since 0.1.0
291
+ def __dsl(method_name)
292
+ case result = self.class.__send__(method_name)
293
+ when Symbol
294
+ __send__(result)
295
+ else
296
+ result
297
+ end
298
+ end
299
+
300
+ # @private
301
+ # @since 0.1.0
302
+ def __part(format)
303
+ Mail::Part.new.tap do |part|
304
+ part.content_type = "#{ CONTENT_TYPES.fetch(format) }; charset=#{ charset }"
305
+ part.body = render(format)
306
+ end if __part?(format)
307
+ end
308
+
309
+ # @private
310
+ # @since 0.1.0
311
+ def __part?(format)
312
+ @format == format ||
313
+ (!@format && !self.class.templates(format).nil?)
314
+ end
6
315
  end
7
316
  end
@@ -0,0 +1,307 @@
1
+ require 'set'
2
+ require 'hanami/utils/kernel'
3
+
4
+ module Hanami
5
+ module Mailer
6
+ # Framework configuration
7
+ #
8
+ # @since 0.1.0
9
+ class Configuration
10
+ # Default root
11
+ #
12
+ # @since 0.1.0
13
+ # @api private
14
+ DEFAULT_ROOT = '.'.freeze
15
+
16
+ # Default delivery method
17
+ #
18
+ # @since 0.1.0
19
+ # @api private
20
+ DEFAULT_DELIVERY_METHOD = :smtp
21
+
22
+ # Default charset
23
+ #
24
+ # @since 0.1.0
25
+ # @api private
26
+ DEFAULT_CHARSET = 'UTF-8'.freeze
27
+
28
+ # @since 0.1.0
29
+ # @api private
30
+ attr_reader :mailers
31
+
32
+ # @since 0.1.0
33
+ # @api private
34
+ attr_reader :modules
35
+
36
+ # Initialize a configuration instance
37
+ #
38
+ # @return [Hanami::Mailer::Configuration] a new configuration's instance
39
+ #
40
+ # @since 0.1.0
41
+ def initialize
42
+ @namespace = Object
43
+ reset!
44
+ end
45
+
46
+ # Set the Ruby namespace where to lookup for mailers.
47
+ #
48
+ # When multiple instances of the framework are used, we want to make sure
49
+ # that if a `MyApp` wants a `Mailers::Signup` mailer, we are loading the
50
+ # right one.
51
+ #
52
+ # If not set, this value defaults to `Object`.
53
+ #
54
+ # This is part of a DSL, for this reason when this method is called with
55
+ # an argument, it will set the corresponding instance variable. When
56
+ # called without, it will return the already set value, or the default.
57
+ #
58
+ # @overload namespace(value)
59
+ # Sets the given value
60
+ # @param value [Class, Module, String] a valid Ruby namespace identifier
61
+ #
62
+ # @overload namespace
63
+ # Gets the value
64
+ # @return [Class, Module, String]
65
+ #
66
+ # @api private
67
+ # @since 0.1.0
68
+ #
69
+ # @example Getting the value
70
+ # require 'hanami/mailer'
71
+ #
72
+ # Hanami::Mailer.configuration.namespace # => Object
73
+ #
74
+ # @example Setting the value
75
+ # require 'hanami/mailer'
76
+ #
77
+ # Hanami::Mailer.configure do
78
+ # namespace 'MyApp::Mailers'
79
+ # end
80
+ def namespace(value = nil)
81
+ if value
82
+ @namespace = value
83
+ else
84
+ @namespace
85
+ end
86
+ end
87
+
88
+ # Set the root path where to search for templates
89
+ #
90
+ # If not set, this value defaults to the current directory.
91
+ #
92
+ # When this method is called with an argument, it will set the corresponding instance variable.
93
+ # When called without, it will return the already set value, or the default.
94
+ #
95
+ # @overload root(value)
96
+ # Sets the given value
97
+ # @param value [String, Pathname, #to_pathname] an object that can be
98
+ # coerced to Pathname
99
+ #
100
+ # @overload root
101
+ # Gets the value
102
+ # @return [Pathname]
103
+ #
104
+ # @since 0.1.0
105
+ #
106
+ # @see http://www.ruby-doc.org/stdlib/libdoc/pathname/rdoc/Pathname.html
107
+ # @see http://rdoc.info/gems/hanami-utils/Hanami/Utils/Kernel#Pathname-class_method
108
+ #
109
+ # @example Getting the value
110
+ # require 'hanami/mailer'
111
+ #
112
+ # Hanami::Mailer.configuration.root # => #<Pathname:.>
113
+ #
114
+ # @example Setting the value
115
+ # require 'hanami/mailer'
116
+ #
117
+ # Hanami::Mailer.configure do
118
+ # root '/path/to/templates'
119
+ # end
120
+ #
121
+ # Hanami::Mailer.configuration.root # => #<Pathname:/path/to/templates>
122
+ def root(value = nil)
123
+ if value
124
+ @root = Utils::Kernel.Pathname(value).realpath
125
+ else
126
+ @root
127
+ end
128
+ end
129
+
130
+ # Prepare the mailers.
131
+ #
132
+ # The given block will be yielded when `Hanami::Mailer` will be included by
133
+ # a mailer.
134
+ #
135
+ # This method can be called multiple times.
136
+ #
137
+ # @param blk [Proc] the code block
138
+ #
139
+ # @return [void]
140
+ #
141
+ # @raise [ArgumentError] if called without passing a block
142
+ #
143
+ # @since 0.1.0
144
+ #
145
+ # @see Hanami::Mailer.configure
146
+ def prepare(&blk)
147
+ if block_given?
148
+ @modules.push(blk)
149
+ else
150
+ raise ArgumentError.new('Please provide a block')
151
+ end
152
+ end
153
+
154
+ # Add a mailer to the registry
155
+ #
156
+ # @since 0.1.0
157
+ # @api private
158
+ def add_mailer(mailer)
159
+ @mailers.add(mailer)
160
+ end
161
+
162
+ # Duplicate by copying the settings in a new instance.
163
+ #
164
+ # @return [Hanami::Mailer::Configuration] a copy of the configuration
165
+ #
166
+ # @since 0.1.0
167
+ # @api private
168
+ def duplicate
169
+ Configuration.new.tap do |c|
170
+ c.namespace = namespace
171
+ c.root = root.dup
172
+ c.modules = modules.dup
173
+ c.delivery_method = delivery_method
174
+ c.default_charset = default_charset
175
+ end
176
+ end
177
+
178
+ # Load the configuration
179
+ def load!
180
+ mailers.each { |m| m.__send__(:load!) }
181
+ freeze
182
+ end
183
+
184
+ # Reset the configuration
185
+ def reset!
186
+ root(DEFAULT_ROOT)
187
+ delivery_method(DEFAULT_DELIVERY_METHOD)
188
+ default_charset(DEFAULT_CHARSET)
189
+
190
+ @mailers = Set.new
191
+ @modules = []
192
+ end
193
+
194
+ alias_method :unload!, :reset!
195
+
196
+ # Copy the configuration for the given mailer
197
+ #
198
+ # @param base [Class] the target mailer
199
+ #
200
+ # @return void
201
+ #
202
+ # @since 0.1.0
203
+ # @api private
204
+ def copy!(base)
205
+ modules.each do |mod|
206
+ base.class_eval(&mod)
207
+ end
208
+ end
209
+
210
+ # Specify a global delivery method for the mail gateway.
211
+ #
212
+ # It supports the following delivery methods:
213
+ #
214
+ # * Exim (<tt>:exim</tt>)
215
+ # * Sendmail (<tt>:sendmail</tt>)
216
+ # * SMTP (<tt>:smtp</tt>, for local installations)
217
+ # * SMTP Connection (<tt>:smtp_connection</tt>,
218
+ # via <tt>Net::SMTP</tt> - for remote installations)
219
+ # * Test (<tt>:test</tt>, for testing purposes)
220
+ #
221
+ # The default delivery method is SMTP (<tt>:smtp</tt>).
222
+ #
223
+ # Custom delivery methods can be specified by passing the class policy and
224
+ # a set of optional configurations. This class MUST respond to:
225
+ #
226
+ # * <tt>initialize(options = {})</tt>
227
+ # * <tt>deliver!(mail)<tt>
228
+ #
229
+ # @param method [Symbol, #initialize, deliver!] delivery method
230
+ # @param options [Hash] optional settings
231
+ #
232
+ # @return [Array] an array containing the delivery method and the optional settings as an Hash
233
+ #
234
+ # @since 0.1.0
235
+ #
236
+ # @example Setup delivery method with supported symbol
237
+ # require 'hanami/mailer'
238
+ #
239
+ # Hanami::Mailer.configure do
240
+ # delivery_method :sendmail
241
+ # end
242
+ #
243
+ # @example Setup delivery method with supported symbol and options
244
+ # require 'hanami/mailer'
245
+ #
246
+ # Hanami::Mailer.configure do
247
+ # delivery_method :smtp, address: "localhost", port: 1025
248
+ # end
249
+ #
250
+ # @example Setup custom delivery method with options
251
+ # require 'hanami/mailer'
252
+ #
253
+ # class MandrillDeliveryMethod
254
+ # def initialize(options)
255
+ # @options = options
256
+ # end
257
+ #
258
+ # def deliver!(mail)
259
+ # # ...
260
+ # end
261
+ # end
262
+ #
263
+ # Hanami::Mailer.configure do
264
+ # delivery_method MandrillDeliveryMethod,
265
+ # username: ENV['MANDRILL_USERNAME'],
266
+ # password: ENV['MANDRILL_API_KEY']
267
+ # end
268
+ def delivery_method(method = nil, options = {})
269
+ if method.nil?
270
+ @delivery_method
271
+ else
272
+ @delivery_method = [method, options]
273
+ end
274
+ end
275
+
276
+ # @since 0.1.0
277
+ def default_charset(value = nil)
278
+ if value.nil?
279
+ @default_charset
280
+ else
281
+ @default_charset = value
282
+ end
283
+ end
284
+
285
+ protected
286
+ # @api private
287
+ # @since 0.1.0
288
+ attr_writer :root
289
+
290
+ # @api private
291
+ # @since 0.1.0
292
+ attr_writer :delivery_method
293
+
294
+ # @api private
295
+ # @since 0.1.0
296
+ attr_writer :default_charset
297
+
298
+ # @api private
299
+ # @since 0.1.0
300
+ attr_writer :namespace
301
+
302
+ # @api private
303
+ # @since 0.1.0
304
+ attr_writer :modules
305
+ end
306
+ end
307
+ end