hanami-mailer 1.3.3 → 3.0.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +154 -45
- data/LICENSE +20 -0
- data/README.md +518 -303
- data/hanami-mailer.gemspec +23 -18
- data/lib/hanami/mailer/attachment.rb +133 -0
- data/lib/hanami/mailer/attachment_set.rb +38 -0
- data/lib/hanami/mailer/delivery/result.rb +101 -0
- data/lib/hanami/mailer/delivery/smtp.rb +168 -0
- data/lib/hanami/mailer/delivery/test.rb +57 -0
- data/lib/hanami/mailer/dsl/attachments.rb +108 -0
- data/lib/hanami/mailer/dsl/exposure.rb +69 -0
- data/lib/hanami/mailer/dsl/exposures.rb +111 -0
- data/lib/hanami/mailer/dsl/plucky_proc.rb +135 -0
- data/lib/hanami/mailer/errors.rb +73 -0
- data/lib/hanami/mailer/message.rb +101 -0
- data/lib/hanami/mailer/version.rb +4 -3
- data/lib/hanami/mailer/view_integration.rb +205 -0
- data/lib/hanami/mailer.rb +372 -270
- data/lib/hanami-mailer.rb +1 -1
- metadata +40 -97
- data/LICENSE.md +0 -22
- data/lib/hanami/mailer/configuration.rb +0 -310
- data/lib/hanami/mailer/dsl.rb +0 -628
- data/lib/hanami/mailer/rendering/template_name.rb +0 -55
- data/lib/hanami/mailer/rendering/templates_finder.rb +0 -135
- data/lib/hanami/mailer/template.rb +0 -42
data/README.md
CHANGED
|
@@ -1,420 +1,635 @@
|
|
|
1
|
-
|
|
1
|
+
<!--- This file is synced from hanakai-rb/repo-sync -->
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[actions]: https://github.com/hanami/hanami-mailer/actions
|
|
4
|
+
[chat]: https://discord.gg/naQApPAsZB
|
|
5
|
+
[forum]: https://discourse.hanamirb.org
|
|
6
|
+
[rubygem]: https://rubygems.org/gems/hanami-mailer
|
|
4
7
|
|
|
5
|
-
|
|
8
|
+
# hanami-mailer [][rubygem] [][actions]
|
|
6
9
|
|
|
7
|
-
[](https://codecov.io/gh/hanami/mailer)
|
|
10
|
-
[](https://depfu.com/github/hanami/mailer?project=Bundler)
|
|
11
|
-
[](http://inch-ci.org/github/hanami/mailer)
|
|
10
|
+
[][forum]
|
|
11
|
+
[][chat]
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
Email delivery for Hanami apps and Ruby projects.
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
* Mailing List: http://hanamirb.org/mailing-list
|
|
17
|
-
* API Doc: http://rdoc.info/gems/hanami-mailer
|
|
18
|
-
* Bugs/Issues: https://github.com/hanami/mailer/issues
|
|
19
|
-
* Support: http://stackoverflow.com/questions/tagged/hanami
|
|
20
|
-
* Chat: http://chat.hanamirb.org
|
|
15
|
+
## Installation
|
|
21
16
|
|
|
22
|
-
|
|
17
|
+
Add the following to your app's Gemfile.
|
|
23
18
|
|
|
24
|
-
|
|
19
|
+
```ruby
|
|
20
|
+
gem "hanami-mailer"
|
|
21
|
+
gem "hanami-view" # For standard mailer view rendering
|
|
22
|
+
```
|
|
25
23
|
|
|
26
|
-
##
|
|
24
|
+
## Usage
|
|
27
25
|
|
|
28
|
-
|
|
26
|
+
Mailers can be used standalone in any Ruby project, or integrated into a Hanami app. The details below focus on standalone use; for mailers in a Hanami app, see the [Hanami mailers guide](https://hanakai.org/learn/hanami/mailers).
|
|
27
|
+
|
|
28
|
+
### Basic mailer
|
|
29
|
+
|
|
30
|
+
The simplest mailer uses static headers.
|
|
29
31
|
|
|
30
32
|
```ruby
|
|
31
|
-
|
|
33
|
+
class WelcomeMailer < Hanami::Mailer
|
|
34
|
+
config.paths = ["app/templates/mailers"]
|
|
35
|
+
|
|
36
|
+
from "noreply@example.com"
|
|
37
|
+
to "user@example.com"
|
|
38
|
+
subject "Welcome to our app!"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
mailer = WelcomeMailer.new
|
|
42
|
+
mailer.deliver
|
|
32
43
|
```
|
|
33
44
|
|
|
34
|
-
|
|
45
|
+
The HTML and text bodies come from these templates.
|
|
35
46
|
|
|
36
|
-
|
|
47
|
+
`app/templates/mailers/welcome_mailer.html.erb`:
|
|
37
48
|
|
|
38
|
-
|
|
49
|
+
```erb
|
|
50
|
+
<h1>Welcome to our app!</h1>
|
|
51
|
+
```
|
|
39
52
|
|
|
40
|
-
|
|
53
|
+
`app/templates/mailers/welcome_mailer.text.erb`:
|
|
41
54
|
|
|
42
|
-
|
|
55
|
+
```erb
|
|
56
|
+
Welcome to our app!
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
By default, a mailer's template is inferred from its mailer class name, e.g. `WelcomeMailer` renders
|
|
60
|
+
a `welcome_mailer` template. Set `config.template` to configure a name explicitly, e.g.
|
|
61
|
+
`config.template = "welcome"` would look for `welcome.html.erb` and `welcome.text.erb`.
|
|
62
|
+
|
|
63
|
+
### Rendering with Hanami View
|
|
64
|
+
|
|
65
|
+
Mailers render their HTML and text bodies using [Hanami View]. This is an optional dependency; add `hanami-view` to your bundle to enable it.
|
|
66
|
+
|
|
67
|
+
[Hanami View]: https://github.com/hanami/hanami-view
|
|
68
|
+
|
|
69
|
+
Hanami View settings are available directly on your mailer class, just like `config.paths` and `config.template` as used above. Each mailer builds its own view class behind the scenes to render your templates.
|
|
43
70
|
|
|
44
|
-
|
|
71
|
+
A mailer has two body formats, `:html` and `:text`, each rendered from its own template. The format is the first extension in the template file name: `welcome_mailer.html.erb` provides the `:html` body and `welcome_mailer.text.erb` the `:text` body.
|
|
45
72
|
|
|
46
|
-
|
|
47
|
-
* A mailer will look for a template with a file name that is composed by its full class name (eg. `"articles/index"`).
|
|
48
|
-
* A template must have two concatenated extensions: one for the format and one for the engine (eg. `".html.erb"`).
|
|
49
|
-
* The framework must be loaded before rendering the first time: `Hanami::Mailer.load!`.
|
|
73
|
+
Both HTML and text formats are rendered by default, producing a multipart email. Pass `format: :html` or `format: :text` to `#deliver` or `#prepare` to render a single format only.
|
|
50
74
|
|
|
51
|
-
|
|
75
|
+
Without Hanami View, mailers still send mail — you just supply the bodies yourself (see [Custom rendering without Hanami View](#custom-rendering-without-hanami-view)).
|
|
52
76
|
|
|
53
|
-
|
|
77
|
+
### Dynamic headers and exposures
|
|
78
|
+
|
|
79
|
+
Use header methods with blocks to compute headers dynamically based on input data.
|
|
80
|
+
|
|
81
|
+
Use `expose` to prepare values and make them available to your headers, attachments, and delivery options, and when Hanami View is available, your view templates for rendering.
|
|
54
82
|
|
|
55
83
|
```ruby
|
|
56
|
-
|
|
84
|
+
class UserMailer < Hanami::Mailer
|
|
85
|
+
config.paths = ["app/templates/mailers"]
|
|
86
|
+
|
|
87
|
+
from "notifications@example.com"
|
|
88
|
+
to { |user:| user[:email] }
|
|
89
|
+
subject { |user:| "Hello, #{user[:name]}!" }
|
|
57
90
|
|
|
58
|
-
|
|
59
|
-
include Hanami::Mailer
|
|
91
|
+
expose :user
|
|
60
92
|
end
|
|
93
|
+
|
|
94
|
+
mailer = UserMailer.new
|
|
95
|
+
mailer.deliver(user: {name: "Alice", email: "alice@example.com"})
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
The HTML and text bodies come from these templates.
|
|
99
|
+
|
|
100
|
+
`app/templates/mailers/user_mailer.html.erb`:
|
|
101
|
+
|
|
102
|
+
```erb
|
|
103
|
+
<h1>Hello, <%= user[:name] %>!</h1>
|
|
61
104
|
```
|
|
62
105
|
|
|
63
|
-
|
|
106
|
+
`app/templates/mailers/user_mailer.text.erb`:
|
|
107
|
+
|
|
108
|
+
```erb
|
|
109
|
+
Hello, <%= user[:name] %>!
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
`expose` comes in a few forms:
|
|
64
113
|
|
|
65
114
|
```ruby
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
class WelcomeMailer
|
|
80
|
-
include Hanami::Mailer
|
|
81
|
-
|
|
82
|
-
return_path 'bounce@sender.com'
|
|
83
|
-
from 'noreply@sender.com'
|
|
84
|
-
to 'noreply@recipient.com'
|
|
85
|
-
cc 'cc@sender.com'
|
|
86
|
-
bcc 'alice@example.com'
|
|
87
|
-
|
|
88
|
-
subject 'Welcome'
|
|
115
|
+
# A value passed straight through from the input.
|
|
116
|
+
expose :user
|
|
117
|
+
|
|
118
|
+
# A value computed by a block.
|
|
119
|
+
expose(:greeting) { |customer:| "Hello, #{customer[:name]}!" }
|
|
120
|
+
|
|
121
|
+
# A default for optional input.
|
|
122
|
+
expose :greeting, default: "Hello"
|
|
123
|
+
|
|
124
|
+
# A private value: available to other exposures, headers, attachments, and delivery options, but
|
|
125
|
+
# never passed to the view for rendering.
|
|
126
|
+
private_expose :full_name do |first_name:, last_name:|
|
|
127
|
+
"#{first_name} #{last_name}"
|
|
89
128
|
end
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Accessing input and exposures in blocks
|
|
132
|
+
|
|
133
|
+
Mailer class methods receiving blocks (`expose`, as well as the header methods, `attachment`, and `delivery_option`) follow one rule for their parameters:
|
|
90
134
|
|
|
91
|
-
|
|
135
|
+
- **Keyword parameters** receive matching keys from the `deliver` input. Give them defaults to make those keys optional.
|
|
136
|
+
- **Positional parameters** receive exposure values, matched by name.
|
|
137
|
+
|
|
138
|
+
```ruby
|
|
139
|
+
class OrderMailer < Hanami::Mailer
|
|
140
|
+
from "orders@example.com"
|
|
141
|
+
|
|
142
|
+
# `customer:` comes from the same keyword arg given to `#deliver`
|
|
143
|
+
to { |customer:| customer[:email] }
|
|
144
|
+
|
|
145
|
+
# `customer:` comes from the input; `greeting` becomes an exposure
|
|
146
|
+
expose :greeting do |customer:|
|
|
147
|
+
"Hello, #{customer[:name]}!"
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# `greeting` receives the value from the `:greeting` exposure above
|
|
151
|
+
subject { |greeting| greeting }
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
OrderMailer.new.deliver(customer: {name: "Alice", email: "alice@example.com"})
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Standard and custom email headers
|
|
158
|
+
|
|
159
|
+
Aside from the standard headers (which have their own dedicated convenience methods), you can add additional custom headers using `header`.
|
|
160
|
+
|
|
161
|
+
```ruby
|
|
162
|
+
class CampaignMailer < Hanami::Mailer
|
|
163
|
+
# Standard headers, with dedicated class methods
|
|
164
|
+
from "sender@example.com"
|
|
165
|
+
to { |recipient:| recipient[:email] }
|
|
166
|
+
cc { |cc_list:| cc_list }
|
|
167
|
+
bcc "archive@example.com"
|
|
168
|
+
reply_to "support@example.com"
|
|
169
|
+
return_path "bounces@example.com"
|
|
170
|
+
subject { |subject_line:| subject_line }
|
|
171
|
+
|
|
172
|
+
# Custom headers for bulk emails
|
|
173
|
+
header :precedence, "bulk"
|
|
174
|
+
header(:list_unsubscribe) { |unsubscribe_url:| "<#{unsubscribe_url}>" }
|
|
175
|
+
|
|
176
|
+
# Custom headers for tracking (symbol names auto-convert to Title-Case)
|
|
177
|
+
header(:x_campaign_id) { |campaign:| campaign[:id] } # => "X-Campaign-Id"
|
|
178
|
+
header(:x_user_segment) { |user:| user[:segment] } # => "X-User-Segment"
|
|
179
|
+
|
|
180
|
+
# Use strings for exact casing control
|
|
181
|
+
header "X-Mailer-Version", "2.0"
|
|
182
|
+
end
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Overriding headers at delivery time
|
|
186
|
+
|
|
187
|
+
Override any header when calling `deliver`.
|
|
188
|
+
|
|
189
|
+
```ruby
|
|
190
|
+
class NotificationMailer < Hanami::Mailer
|
|
191
|
+
from "notifications@example.com"
|
|
192
|
+
to "default@example.com"
|
|
193
|
+
subject "Default Subject"
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
mailer = NotificationMailer.new
|
|
197
|
+
mailer.deliver(
|
|
198
|
+
headers: {
|
|
199
|
+
to: "priority-user@example.com",
|
|
200
|
+
subject: "URGENT: Important Update",
|
|
201
|
+
cc: "manager@example.com",
|
|
202
|
+
x_priority: "1"
|
|
203
|
+
}
|
|
204
|
+
)
|
|
92
205
|
```
|
|
93
206
|
|
|
94
|
-
###
|
|
207
|
+
### Static attachments
|
|
208
|
+
|
|
209
|
+
Load attachment files from configured paths.
|
|
210
|
+
|
|
211
|
+
```ruby
|
|
212
|
+
class WelcomePackMailer < Hanami::Mailer
|
|
213
|
+
from "welcome@example.com"
|
|
214
|
+
to { |user:| user[:email] }
|
|
215
|
+
subject "Welcome Pack"
|
|
216
|
+
|
|
217
|
+
# Configure paths to search for attachment files
|
|
218
|
+
config.attachment_paths = ["public/attachments"]
|
|
219
|
+
|
|
220
|
+
# These files will be loaded from the configured paths
|
|
221
|
+
attachment "terms.pdf"
|
|
222
|
+
attachment "getting-started-guide.pdf"
|
|
223
|
+
attachment "company-logo.png"
|
|
224
|
+
end
|
|
225
|
+
```
|
|
95
226
|
|
|
96
|
-
|
|
227
|
+
You can configure multiple attachment paths in a base mailer class.
|
|
97
228
|
|
|
98
229
|
```ruby
|
|
99
|
-
|
|
230
|
+
class ApplicationMailer < Hanami::Mailer
|
|
231
|
+
config.attachment_paths = [
|
|
232
|
+
"app/attachments",
|
|
233
|
+
"app/assets/pdfs"
|
|
234
|
+
]
|
|
235
|
+
end
|
|
236
|
+
```
|
|
100
237
|
|
|
101
|
-
|
|
102
|
-
luca = User.new('Luca', 'jodosha', 'luca@jodosha.com')
|
|
238
|
+
If a file cannot be found in any of the configured paths, a `MissingAttachmentError` is raised.
|
|
103
239
|
|
|
104
|
-
|
|
105
|
-
include Hanami::Mailer
|
|
240
|
+
### Dynamic attachments
|
|
106
241
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
242
|
+
Return one or more attachments from an `attachment` block, whose parameters work [like every other block](#accessing-input-and-exposures-in-blocks). Use the `file` helper to create attachment objects.
|
|
243
|
+
|
|
244
|
+
```ruby
|
|
245
|
+
class ReportMailer < Hanami::Mailer
|
|
246
|
+
from "reports@example.com"
|
|
247
|
+
to { |user:| user[:email] }
|
|
248
|
+
subject "Monthly Report"
|
|
249
|
+
|
|
250
|
+
expose :user
|
|
251
|
+
|
|
252
|
+
# A single attachment from a block
|
|
253
|
+
attachment do |user:|
|
|
254
|
+
file(
|
|
255
|
+
"report-#{user[:id]}.pdf",
|
|
256
|
+
generate_report_pdf(user),
|
|
257
|
+
content_type: "application/pdf"
|
|
258
|
+
)
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# You can have multiple attachment blocks
|
|
262
|
+
attachment do
|
|
263
|
+
file("summary.txt", "Here is your summary.")
|
|
264
|
+
end
|
|
110
265
|
|
|
111
266
|
private
|
|
112
267
|
|
|
113
|
-
def
|
|
114
|
-
|
|
268
|
+
def generate_report_pdf(user)
|
|
269
|
+
# ... generate PDF content
|
|
115
270
|
end
|
|
116
271
|
end
|
|
272
|
+
```
|
|
117
273
|
|
|
118
|
-
|
|
274
|
+
You can also return multiple attachments from a single block.
|
|
275
|
+
|
|
276
|
+
```ruby
|
|
277
|
+
attachment do |documents:|
|
|
278
|
+
documents.map do |doc|
|
|
279
|
+
file(doc[:name], doc[:content])
|
|
280
|
+
end
|
|
281
|
+
end
|
|
119
282
|
```
|
|
120
283
|
|
|
121
|
-
|
|
284
|
+
Or use a named instance method instead of a block.
|
|
122
285
|
|
|
123
|
-
```
|
|
124
|
-
|
|
286
|
+
```ruby
|
|
287
|
+
class InvoiceMailer < Hanami::Mailer
|
|
288
|
+
from "billing@example.com"
|
|
289
|
+
to { |customer:| customer[:email] }
|
|
290
|
+
subject "Invoice"
|
|
291
|
+
|
|
292
|
+
expose :invoice
|
|
293
|
+
|
|
294
|
+
attachment :invoice_pdf
|
|
295
|
+
|
|
296
|
+
private
|
|
297
|
+
|
|
298
|
+
def invoice_pdf(invoice:)
|
|
299
|
+
file(
|
|
300
|
+
"invoice-#{invoice[:number]}.pdf",
|
|
301
|
+
generate_pdf(invoice),
|
|
302
|
+
content_type: "application/pdf"
|
|
303
|
+
)
|
|
304
|
+
end
|
|
305
|
+
end
|
|
125
306
|
```
|
|
126
307
|
|
|
127
|
-
###
|
|
308
|
+
### Inline attachments
|
|
128
309
|
|
|
129
|
-
|
|
310
|
+
Use inline attachments to embed images in your email HTML. The Content-ID is based on the filename, so you can reference it using `cid:filename`.
|
|
130
311
|
|
|
131
312
|
```ruby
|
|
132
|
-
|
|
313
|
+
class NewsletterMailer < Hanami::Mailer
|
|
314
|
+
from "news@example.com"
|
|
315
|
+
to { |subscriber:| subscriber[:email] }
|
|
316
|
+
subject "Weekly Newsletter"
|
|
133
317
|
|
|
134
|
-
|
|
135
|
-
include Hanami::Mailer
|
|
318
|
+
expose :subscriber
|
|
136
319
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
320
|
+
attachment do
|
|
321
|
+
file("header-image.png", header_image_data, inline: true)
|
|
322
|
+
end
|
|
140
323
|
|
|
141
|
-
|
|
142
|
-
|
|
324
|
+
private
|
|
325
|
+
|
|
326
|
+
def header_image_data
|
|
327
|
+
File.read("app/assets/images/newsletter-header.png")
|
|
143
328
|
end
|
|
144
329
|
end
|
|
145
330
|
```
|
|
146
331
|
|
|
147
|
-
|
|
148
|
-
|
|
332
|
+
In your HTML template, reference inline attachments using `cid:`.
|
|
333
|
+
|
|
334
|
+
```html
|
|
335
|
+
<img src="cid:header-image.png" alt="Newsletter Header">
|
|
149
336
|
```
|
|
150
337
|
|
|
151
|
-
|
|
338
|
+
Static attachments can also be made inline.
|
|
152
339
|
|
|
153
|
-
|
|
340
|
+
```ruby
|
|
341
|
+
attachment "logo.png", inline: true
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
### Runtime attachments
|
|
345
|
+
|
|
346
|
+
Add attachments at delivery time without defining them at the class level. This is useful for one-off or conditional attachments, or pre-generated files passed from calling code.
|
|
154
347
|
|
|
155
348
|
```ruby
|
|
156
|
-
|
|
157
|
-
|
|
349
|
+
class OrderMailer < Hanami::Mailer
|
|
350
|
+
from "orders@example.com"
|
|
351
|
+
to { |customer:| customer[:email] }
|
|
352
|
+
subject "Order Confirmation"
|
|
353
|
+
|
|
354
|
+
# Class-level attachment always included
|
|
355
|
+
attachment "terms.pdf"
|
|
356
|
+
end
|
|
158
357
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
#
|
|
358
|
+
mailer = OrderMailer.new
|
|
359
|
+
|
|
360
|
+
# Add runtime attachments using hashes
|
|
361
|
+
mailer.deliver(
|
|
362
|
+
customer: {email: "customer@example.com"},
|
|
363
|
+
attachments: [
|
|
364
|
+
{filename: "invoice-123.pdf", content: pdf_bytes},
|
|
365
|
+
{filename: "receipt.txt", content: "Thank you!"}
|
|
366
|
+
]
|
|
367
|
+
)
|
|
368
|
+
# All three attachments are included: terms.pdf, invoice-123.pdf, and receipt.txt
|
|
369
|
+
|
|
370
|
+
# You can also use the Hanami::Mailer.file helper
|
|
371
|
+
mailer.deliver(
|
|
372
|
+
customer: {email: "customer@example.com"},
|
|
373
|
+
attachments: [
|
|
374
|
+
Hanami::Mailer.file("invoice-123.pdf", pdf_bytes, content_type: "application/pdf")
|
|
375
|
+
]
|
|
376
|
+
)
|
|
162
377
|
```
|
|
163
378
|
|
|
164
|
-
|
|
379
|
+
### Delivery options
|
|
380
|
+
|
|
381
|
+
Delivery options are delivery-method-specific parameters that customize how a message is sent. Their blocks receive arguments [like every other block](#accessing-input-and-exposures-in-blocks), and the resulting options are passed through to the delivery method on the `Message` object.
|
|
382
|
+
|
|
383
|
+
A third-party email service might use these for scheduled sending, priority levels, or tracking.
|
|
165
384
|
|
|
166
385
|
```ruby
|
|
167
|
-
class
|
|
168
|
-
|
|
386
|
+
class CampaignMailer < Hanami::Mailer
|
|
387
|
+
from "campaigns@example.com"
|
|
388
|
+
to { |recipient:| recipient[:email] }
|
|
389
|
+
subject "Special Offer"
|
|
169
390
|
|
|
170
|
-
|
|
391
|
+
# Static delivery option
|
|
392
|
+
delivery_option :track_opens, true
|
|
393
|
+
|
|
394
|
+
# Dynamic delivery option
|
|
395
|
+
delivery_option(:send_at) { |scheduled_time:| scheduled_time }
|
|
396
|
+
delivery_option(:tags) { |campaign:| ["campaign-#{campaign[:id]}"] }
|
|
171
397
|
end
|
|
172
398
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
399
|
+
mailer = CampaignMailer.new(delivery_method: postmark_delivery)
|
|
400
|
+
mailer.deliver(
|
|
401
|
+
recipient: {email: "user@example.com"},
|
|
402
|
+
campaign: {id: 42},
|
|
403
|
+
scheduled_time: Time.now + 3600
|
|
404
|
+
)
|
|
176
405
|
```
|
|
177
406
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
They are listed in order of **higher precedence**, for a given extension.
|
|
184
|
-
For instance, if [ERubis](http://www.kuwata-lab.com/erubis/) is loaded, it will be preferred over ERb to render `.erb` templates.
|
|
185
|
-
|
|
186
|
-
<table>
|
|
187
|
-
<tr>
|
|
188
|
-
<th>Engine</th>
|
|
189
|
-
<th>Extensions</th>
|
|
190
|
-
</tr>
|
|
191
|
-
<tr>
|
|
192
|
-
<td>Erubis</td>
|
|
193
|
-
<td>erb, rhtml, erubis</td>
|
|
194
|
-
</tr>
|
|
195
|
-
<tr>
|
|
196
|
-
<td>ERb</td>
|
|
197
|
-
<td>erb, rhtml</td>
|
|
198
|
-
</tr>
|
|
199
|
-
<tr>
|
|
200
|
-
<td>Redcarpet</td>
|
|
201
|
-
<td>markdown, mkd, md</td>
|
|
202
|
-
</tr>
|
|
203
|
-
<tr>
|
|
204
|
-
<td>RDiscount</td>
|
|
205
|
-
<td>markdown, mkd, md</td>
|
|
206
|
-
</tr>
|
|
207
|
-
<tr>
|
|
208
|
-
<td>Kramdown</td>
|
|
209
|
-
<td>markdown, mkd, md</td>
|
|
210
|
-
</tr>
|
|
211
|
-
<tr>
|
|
212
|
-
<td>Maruku</td>
|
|
213
|
-
<td>markdown, mkd, md</td>
|
|
214
|
-
</tr>
|
|
215
|
-
<tr>
|
|
216
|
-
<td>BlueCloth</td>
|
|
217
|
-
<td>markdown, mkd, md</td>
|
|
218
|
-
</tr>
|
|
219
|
-
<tr>
|
|
220
|
-
<td>Asciidoctor</td>
|
|
221
|
-
<td>ad, adoc, asciidoc</td>
|
|
222
|
-
</tr>
|
|
223
|
-
<tr>
|
|
224
|
-
<td>Builder</td>
|
|
225
|
-
<td>builder</td>
|
|
226
|
-
</tr>
|
|
227
|
-
<tr>
|
|
228
|
-
<td>CSV</td>
|
|
229
|
-
<td>rcsv</td>
|
|
230
|
-
</tr>
|
|
231
|
-
<tr>
|
|
232
|
-
<td>CoffeeScript</td>
|
|
233
|
-
<td>coffee</td>
|
|
234
|
-
</tr>
|
|
235
|
-
<tr>
|
|
236
|
-
<td>WikiCloth</td>
|
|
237
|
-
<td>wiki, mediawiki, mw</td>
|
|
238
|
-
</tr>
|
|
239
|
-
<tr>
|
|
240
|
-
<td>Creole</td>
|
|
241
|
-
<td>wiki, creole</td>
|
|
242
|
-
</tr>
|
|
243
|
-
<tr>
|
|
244
|
-
<td>Etanni</td>
|
|
245
|
-
<td>etn, etanni</td>
|
|
246
|
-
</tr>
|
|
247
|
-
<tr>
|
|
248
|
-
<td>Haml</td>
|
|
249
|
-
<td>haml</td>
|
|
250
|
-
</tr>
|
|
251
|
-
<tr>
|
|
252
|
-
<td>Less</td>
|
|
253
|
-
<td>less</td>
|
|
254
|
-
</tr>
|
|
255
|
-
<tr>
|
|
256
|
-
<td>Liquid</td>
|
|
257
|
-
<td>liquid</td>
|
|
258
|
-
</tr>
|
|
259
|
-
<tr>
|
|
260
|
-
<td>Markaby</td>
|
|
261
|
-
<td>mab</td>
|
|
262
|
-
</tr>
|
|
263
|
-
<tr>
|
|
264
|
-
<td>Nokogiri</td>
|
|
265
|
-
<td>nokogiri</td>
|
|
266
|
-
</tr>
|
|
267
|
-
<tr>
|
|
268
|
-
<td>Plain</td>
|
|
269
|
-
<td>html</td>
|
|
270
|
-
</tr>
|
|
271
|
-
<tr>
|
|
272
|
-
<td>RDoc</td>
|
|
273
|
-
<td>rdoc</td>
|
|
274
|
-
</tr>
|
|
275
|
-
<tr>
|
|
276
|
-
<td>Radius</td>
|
|
277
|
-
<td>radius</td>
|
|
278
|
-
</tr>
|
|
279
|
-
<tr>
|
|
280
|
-
<td>RedCloth</td>
|
|
281
|
-
<td>textile</td>
|
|
282
|
-
</tr>
|
|
283
|
-
<tr>
|
|
284
|
-
<td>Sass</td>
|
|
285
|
-
<td>sass</td>
|
|
286
|
-
</tr>
|
|
287
|
-
<tr>
|
|
288
|
-
<td>Scss</td>
|
|
289
|
-
<td>scss</td>
|
|
290
|
-
</tr>
|
|
291
|
-
<tr>
|
|
292
|
-
<td>Slim</td>
|
|
293
|
-
<td>slim</td>
|
|
294
|
-
</tr>
|
|
295
|
-
<tr>
|
|
296
|
-
<td>String</td>
|
|
297
|
-
<td>str</td>
|
|
298
|
-
</tr>
|
|
299
|
-
<tr>
|
|
300
|
-
<td>Yajl</td>
|
|
301
|
-
<td>yajl</td>
|
|
302
|
-
</tr>
|
|
303
|
-
</table>
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
### Configuration
|
|
307
|
-
|
|
308
|
-
__Hanami::Mailer__ can be configured with a DSL that determines its behavior.
|
|
309
|
-
It supports a few options:
|
|
407
|
+
The delivery method receives these options via `message.delivery_options` and can act on them however it sees fit.
|
|
408
|
+
|
|
409
|
+
### Inheritance
|
|
410
|
+
|
|
411
|
+
Mailers support inheritance, which is useful for sharing common configuration.
|
|
310
412
|
|
|
311
413
|
```ruby
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
# Set the root path where to search for templates
|
|
316
|
-
# Argument: String, Pathname, #to_pathname, defaults to the current directory
|
|
317
|
-
#
|
|
318
|
-
root '/path/to/root'
|
|
319
|
-
|
|
320
|
-
# Set the default charset for emails
|
|
321
|
-
# Argument: String, defaults to "UTF-8"
|
|
322
|
-
#
|
|
323
|
-
default_charset 'iso-8859'
|
|
324
|
-
|
|
325
|
-
# Set the delivery method
|
|
326
|
-
# Argument: Symbol
|
|
327
|
-
# Argument: Hash, optional configurations
|
|
328
|
-
delivery_method :stmp
|
|
414
|
+
class ApplicationMailer < Hanami::Mailer
|
|
415
|
+
from "noreply@example.com"
|
|
416
|
+
config.attachment_paths = ["app/attachments"]
|
|
329
417
|
end
|
|
418
|
+
|
|
419
|
+
class WelcomeMailer < ApplicationMailer
|
|
420
|
+
to { |user:| user[:email] }
|
|
421
|
+
subject "Welcome!"
|
|
422
|
+
|
|
423
|
+
expose :user
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
class NewsletterMailer < ApplicationMailer
|
|
427
|
+
to { |subscriber:| subscriber[:email] }
|
|
428
|
+
subject "Weekly Newsletter"
|
|
429
|
+
|
|
430
|
+
expose :subscriber
|
|
431
|
+
|
|
432
|
+
attachment "terms.pdf"
|
|
433
|
+
end
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
Headers, exposures, attachments, and delivery options are all inherited and can be extended in subclasses.
|
|
437
|
+
|
|
438
|
+
### Delivery methods
|
|
439
|
+
|
|
440
|
+
`Hanami::Mailer` expects the delivery method to be provided as a `delivery_method:` dependency at initialization.
|
|
441
|
+
|
|
442
|
+
Every delivery method must respond to `#call(message)` and return a `Delivery::Result`.
|
|
443
|
+
|
|
444
|
+
#### Test delivery (default)
|
|
445
|
+
|
|
446
|
+
The test delivery method stores results in memory. It's the default when no delivery method is specified.
|
|
447
|
+
|
|
448
|
+
```ruby
|
|
449
|
+
mailer = WelcomeMailer.new
|
|
450
|
+
result = mailer.deliver(user: user)
|
|
451
|
+
|
|
452
|
+
result.success? # => true
|
|
453
|
+
result.message # => the Hanami::Mailer::Message that was delivered
|
|
454
|
+
|
|
455
|
+
# Inspect all deliveries via the mailer's delivery method instance
|
|
456
|
+
mailer.delivery_method.deliveries # => [result, ...]
|
|
457
|
+
mailer.delivery_method.deliveries.size # => 1
|
|
458
|
+
mailer.delivery_method.clear # reset between tests
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
#### SMTP delivery
|
|
462
|
+
|
|
463
|
+
For production use, provide an SMTP delivery method.
|
|
464
|
+
|
|
465
|
+
```ruby
|
|
466
|
+
smtp = Hanami::Mailer::Delivery::SMTP.new(
|
|
467
|
+
address: "smtp.example.com",
|
|
468
|
+
port: 587,
|
|
469
|
+
user_name: ENV["SMTP_USERNAME"],
|
|
470
|
+
password: ENV["SMTP_PASSWORD"],
|
|
471
|
+
authentication: :plain,
|
|
472
|
+
enable_starttls_auto: true
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
mailer = WelcomeMailer.new(delivery_method: smtp)
|
|
476
|
+
result = mailer.deliver(user: user)
|
|
477
|
+
|
|
478
|
+
result.success? # => true if SMTP accepted the message
|
|
479
|
+
result.response # => the Mail::Message object
|
|
480
|
+
result.error # => nil on success, the exception on failure
|
|
330
481
|
```
|
|
331
482
|
|
|
332
|
-
|
|
483
|
+
#### Custom delivery methods
|
|
333
484
|
|
|
334
|
-
|
|
485
|
+
Implement your own delivery method by creating a class that responds to `#call(message)` and returns a `Delivery::Result`.
|
|
335
486
|
|
|
336
487
|
```ruby
|
|
337
|
-
class
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
488
|
+
class MyApiDelivery
|
|
489
|
+
def call(message)
|
|
490
|
+
response = SomeEmailApi.send(
|
|
491
|
+
from: message.from,
|
|
492
|
+
to: message.to,
|
|
493
|
+
subject: message.subject,
|
|
494
|
+
html: message.html_body,
|
|
495
|
+
options: message.delivery_options
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
Hanami::Mailer::Delivery::Result.new(
|
|
499
|
+
message: message,
|
|
500
|
+
response: response,
|
|
501
|
+
success: response.ok?
|
|
502
|
+
)
|
|
503
|
+
rescue => error
|
|
504
|
+
Hanami::Mailer::Delivery::Result.new(
|
|
505
|
+
message: message,
|
|
506
|
+
success: false,
|
|
507
|
+
error: error
|
|
508
|
+
)
|
|
345
509
|
end
|
|
346
510
|
end
|
|
347
511
|
```
|
|
348
512
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
The global delivery method is defined through the __Hanami::Mailer__ configuration, as:
|
|
513
|
+
Third-party delivery methods can subclass `Delivery::Result` to expose service-specific attributes.
|
|
352
514
|
|
|
353
515
|
```ruby
|
|
354
|
-
Hanami::Mailer
|
|
355
|
-
|
|
516
|
+
class Postmark::Result < Hanami::Mailer::Delivery::Result
|
|
517
|
+
attr_reader :message_id, :submitted_at
|
|
518
|
+
|
|
519
|
+
def initialize(message_id:, submitted_at: nil, **)
|
|
520
|
+
super(**)
|
|
521
|
+
@message_id = message_id
|
|
522
|
+
@submitted_at = submitted_at
|
|
523
|
+
end
|
|
356
524
|
end
|
|
357
525
|
```
|
|
358
526
|
|
|
527
|
+
### Preparing messages without delivering
|
|
528
|
+
|
|
529
|
+
Use `prepare` to build a `Message` without sending it. This is useful for inspection, queuing, or delivering later through a different method.
|
|
530
|
+
|
|
359
531
|
```ruby
|
|
360
|
-
Hanami::Mailer
|
|
361
|
-
|
|
532
|
+
class WelcomeMailer < Hanami::Mailer
|
|
533
|
+
from "welcome@example.com"
|
|
534
|
+
to { |user:| user[:email] }
|
|
535
|
+
subject { |user:| "Welcome, #{user[:name]}!" }
|
|
536
|
+
|
|
537
|
+
expose :user
|
|
362
538
|
end
|
|
539
|
+
|
|
540
|
+
mailer = WelcomeMailer.new
|
|
541
|
+
message = mailer.prepare(user: {name: "Alice", email: "alice@example.com"})
|
|
542
|
+
|
|
543
|
+
message.from # => ["welcome@example.com"]
|
|
544
|
+
message.to # => ["alice@example.com"]
|
|
545
|
+
message.subject # => "Welcome, Alice!"
|
|
546
|
+
message.html_body # => rendered HTML (if templates exist)
|
|
547
|
+
message.text_body # => rendered text (if templates exist)
|
|
548
|
+
|
|
549
|
+
# Deliver the prepared message directly through a delivery method
|
|
550
|
+
smtp = Hanami::Mailer::Delivery::SMTP.new(address: "smtp.example.com")
|
|
551
|
+
smtp.call(message)
|
|
363
552
|
```
|
|
364
553
|
|
|
365
|
-
|
|
554
|
+
### Previewing messages
|
|
366
555
|
|
|
367
|
-
|
|
368
|
-
* Sendmail (`:sendmail`)
|
|
369
|
-
* SMTP (`:smtp`, for local installations)
|
|
370
|
-
* SMTP Connection (`:smtp_connection`, via `Net::SMTP` - for remote installations)
|
|
371
|
-
* Test (`:test`, for testing purposes)
|
|
556
|
+
Delivery methods expose a `preview` hook that returns a prepared message without sending it. The default (and test) delivery method returns the message unchanged; a third-party delivery method can override `preview` to apply service-specific logic, such as resolving a template through a remote API.
|
|
372
557
|
|
|
373
|
-
|
|
558
|
+
Use `#preview` to prepare the message and run it through the delivery method's hook in one step. It takes the same arguments as `#deliver` and `#prepare`:
|
|
374
559
|
|
|
375
|
-
|
|
560
|
+
```ruby
|
|
561
|
+
mailer = WelcomeMailer.new
|
|
562
|
+
|
|
563
|
+
preview = mailer.preview(user: {name: "Alice", email: "alice@example.com"})
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
When you already hold a prepared message, you can call the delivery method's hook directly instead:
|
|
376
567
|
|
|
377
568
|
```ruby
|
|
378
|
-
|
|
569
|
+
message = mailer.prepare(user: {name: "Alice", email: "alice@example.com"})
|
|
379
570
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
@options = options
|
|
383
|
-
end
|
|
571
|
+
preview = mailer.delivery_method.preview(message)
|
|
572
|
+
```
|
|
384
573
|
|
|
385
|
-
|
|
386
|
-
# ...
|
|
387
|
-
end
|
|
388
|
-
end
|
|
574
|
+
### Using a custom view class
|
|
389
575
|
|
|
390
|
-
Hanami::
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
576
|
+
By default mailers render using a subclass of `Hanami::View`. To inherit from another view class, configure it via `config.view_class`. The mailer's view will inherit from this and use its configuration — context, parts, scopes, paths, helpers, and so on. This is how mailers in a Hanami app pick up the app's standard view behaviour.
|
|
577
|
+
|
|
578
|
+
```ruby
|
|
579
|
+
class ReportMailer < Hanami::Mailer
|
|
580
|
+
config.view_class = MyApp::View
|
|
581
|
+
|
|
582
|
+
from "reports@example.com"
|
|
583
|
+
to { |user:| user[:email] }
|
|
584
|
+
subject "Monthly Report"
|
|
585
|
+
|
|
586
|
+
expose :user
|
|
587
|
+
end
|
|
395
588
|
```
|
|
396
589
|
|
|
397
|
-
|
|
398
|
-
with the constructor (`#initialize`) and respond to `#deliver!`.
|
|
590
|
+
Because template paths are inherited from the configured view class, you typically don't need to set `config.paths` or yourself in this case.
|
|
399
591
|
|
|
400
|
-
###
|
|
592
|
+
### Custom rendering without Hanami View
|
|
401
593
|
|
|
402
|
-
|
|
403
|
-
For a given mailer, the framework looks up for associated text (`.txt`) and `HTML` (`.html`) templates and render them.
|
|
594
|
+
If you don't use Hanami View, override the internal rendering methods. These are also a hook for integrations with other rendering systems.
|
|
404
595
|
|
|
405
596
|
```ruby
|
|
406
|
-
|
|
407
|
-
|
|
597
|
+
class CustomMailer < Hanami::Mailer
|
|
598
|
+
from "custom@example.com"
|
|
599
|
+
to { |user:| user[:email] }
|
|
600
|
+
subject "Custom Email"
|
|
601
|
+
|
|
602
|
+
expose :user
|
|
603
|
+
|
|
604
|
+
private
|
|
605
|
+
|
|
606
|
+
def render_view(format, input)
|
|
607
|
+
user = input[:user]
|
|
608
|
+
|
|
609
|
+
case format
|
|
610
|
+
when :html
|
|
611
|
+
<<~HTML
|
|
612
|
+
<html>
|
|
613
|
+
<body>
|
|
614
|
+
<h1>Hello, #{user[:name]}!</h1>
|
|
615
|
+
</body>
|
|
616
|
+
</html>
|
|
617
|
+
HTML
|
|
618
|
+
when :text
|
|
619
|
+
"Hello, #{user[:name]}!"
|
|
620
|
+
end
|
|
621
|
+
end
|
|
622
|
+
end
|
|
408
623
|
```
|
|
409
624
|
|
|
410
|
-
|
|
625
|
+
If Hanami View is installed but you don't want mailers building a view from it automatically, turn off auto view-building with `config.integrate_view = false`. Your overridden `render_view` then takes full responsibility for rendering.
|
|
411
626
|
|
|
412
|
-
##
|
|
627
|
+
## Links
|
|
413
628
|
|
|
414
|
-
|
|
629
|
+
- [User documentation](https://hanamirb.org)
|
|
630
|
+
- [API documentation](http://rubydoc.info/gems/hanami-mailer)
|
|
415
631
|
|
|
416
|
-
## Copyright
|
|
417
632
|
|
|
418
|
-
|
|
633
|
+
## License
|
|
419
634
|
|
|
420
|
-
|
|
635
|
+
See `LICENSE` file.
|