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.
data/README.md CHANGED
@@ -1,420 +1,635 @@
1
- # Hanami::Mailer
1
+ <!--- This file is synced from hanakai-rb/repo-sync -->
2
2
 
3
- Mail for Ruby applications.
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
- ## Status
8
+ # hanami-mailer [![Gem Version](https://badge.fury.io/rb/hanami-mailer.svg)][rubygem] [![CI Status](https://github.com/hanami/hanami-mailer/workflows/CI/badge.svg)][actions]
6
9
 
7
- [![Gem Version](https://badge.fury.io/rb/hanami-mailer.svg)](https://badge.fury.io/rb/hanami-mailer)
8
- [![CI](https://github.com/hanami/mailer/workflows/ci/badge.svg?branch=master)](https://github.com/hanami/mailer/actions?query=workflow%3Aci+branch%3Amaster)
9
- [![Test Coverage](https://codecov.io/gh/hanami/mailer/branch/master/graph/badge.svg)](https://codecov.io/gh/hanami/mailer)
10
- [![Depfu](https://badges.depfu.com/badges/739c6e10eaf20d3ba4240d00828284db/overview.svg)](https://depfu.com/github/hanami/mailer?project=Bundler)
11
- [![Inline Docs](http://inch-ci.org/github/hanami/mailer.svg)](http://inch-ci.org/github/hanami/mailer)
10
+ [![Forum](https://img.shields.io/badge/Forum-dc360f?logo=discourse&logoColor=white)][forum]
11
+ [![Chat](https://img.shields.io/badge/Chat-717cf8?logo=discord&logoColor=white)][chat]
12
12
 
13
- ## Contact
13
+ Email delivery for Hanami apps and Ruby projects.
14
14
 
15
- * Home page: http://hanamirb.org
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
- ## Rubies
17
+ Add the following to your app's Gemfile.
23
18
 
24
- __Hanami::Mailer__ supports Ruby (MRI) 2.3+ and JRuby 9.1.5.0+.
19
+ ```ruby
20
+ gem "hanami-mailer"
21
+ gem "hanami-view" # For standard mailer view rendering
22
+ ```
25
23
 
26
- ## Installation
24
+ ## Usage
27
25
 
28
- Add this line to your application's Gemfile:
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
- gem 'hanami-mailer'
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
- And then execute:
45
+ The HTML and text bodies come from these templates.
35
46
 
36
- $ bundle
47
+ `app/templates/mailers/welcome_mailer.html.erb`:
37
48
 
38
- Or install it yourself as:
49
+ ```erb
50
+ <h1>Welcome to our app!</h1>
51
+ ```
39
52
 
40
- $ gem install hanami-mailer
53
+ `app/templates/mailers/welcome_mailer.text.erb`:
41
54
 
42
- ## Usage
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
- ### Conventions
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
- * Templates are searched under `Hanami::Mailer.configuration.root`, set this value according to your app structure (eg. `"app/templates"`).
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
- ### Mailers
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
- A simple mailer looks like this:
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
- require 'hanami/mailer'
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
- class InvoiceMailer
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
- A mailer with `.to` and `.from` addresses and mailer delivery:
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
- require 'hanami/mailer'
67
-
68
- Hanami::Mailer.configure do
69
- delivery_method :smtp,
70
- address: "smtp.gmail.com",
71
- port: 587,
72
- domain: "example.com",
73
- user_name: ENV['SMTP_USERNAME'],
74
- password: ENV['SMTP_PASSWORD'],
75
- authentication: "plain",
76
- enable_starttls_auto: true
77
- end.load!
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
- WelcomeMailer.deliver
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
- ### Locals
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
- The set of objects passed in the `deliver` call are called `locals` and are available inside the mailer and the template.
227
+ You can configure multiple attachment paths in a base mailer class.
97
228
 
98
229
  ```ruby
99
- require 'hanami/mailer'
230
+ class ApplicationMailer < Hanami::Mailer
231
+ config.attachment_paths = [
232
+ "app/attachments",
233
+ "app/assets/pdfs"
234
+ ]
235
+ end
236
+ ```
100
237
 
101
- User = Struct.new(:name, :username, :email)
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
- class WelcomeMailer
105
- include Hanami::Mailer
240
+ ### Dynamic attachments
106
241
 
107
- from 'noreply@sender.com'
108
- subject 'Welcome'
109
- to :recipient
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 recipient
114
- user.email
268
+ def generate_report_pdf(user)
269
+ # ... generate PDF content
115
270
  end
116
271
  end
272
+ ```
117
273
 
118
- WelcomeMailer.deliver(user: luca)
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
- The corresponding `erb` file:
284
+ Or use a named instance method instead of a block.
122
285
 
123
- ```erb
124
- Hello <%= user.name %>!
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
- ### Scope
308
+ ### Inline attachments
128
309
 
129
- All public methods defined in the mailer are accessible from the template:
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
- require 'hanami/mailer'
313
+ class NewsletterMailer < Hanami::Mailer
314
+ from "news@example.com"
315
+ to { |subscriber:| subscriber[:email] }
316
+ subject "Weekly Newsletter"
133
317
 
134
- class WelcomeMailer
135
- include Hanami::Mailer
318
+ expose :subscriber
136
319
 
137
- from 'noreply@sender.com'
138
- to 'noreply@recipient.com'
139
- subject 'Welcome'
320
+ attachment do
321
+ file("header-image.png", header_image_data, inline: true)
322
+ end
140
323
 
141
- def greeting
142
- 'Ahoy'
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
- ```erb
148
- <h2><%= greeting %></h2>
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
- ### Template
338
+ Static attachments can also be made inline.
152
339
 
153
- The template file must be located under the relevant `root` and must match the inflected snake case of the mailer class name.
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
- # Given this root
157
- Hanami::Mailer.configuration.root # => #<Pathname:app/templates>
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
- # For InvoiceMailer, it looks for:
160
- # * app/templates/invoice_mailer.html.erb
161
- # * app/templates/invoice_mailer.txt.erb
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
- If we want to specify a different template, we can do:
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 InvoiceMailer
168
- include Hanami::Mailer
386
+ class CampaignMailer < Hanami::Mailer
387
+ from "campaigns@example.com"
388
+ to { |recipient:| recipient[:email] }
389
+ subject "Special Offer"
169
390
 
170
- template 'invoice'
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
- # It will look for:
174
- # * app/templates/invoice.html.erb
175
- # * app/templates/invoice.txt.erb
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
- ### Engines
179
-
180
- The builtin rendering engine is [ERb](http://en.wikipedia.org/wiki/ERuby).
181
-
182
- This is the list of the supported engines.
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
- require 'hanami/mailer'
313
-
314
- Hanami::Mailer.configure do
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
- ### Attachments
483
+ #### Custom delivery methods
333
484
 
334
- Attachments can be added with the following API:
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 InvoiceMailer
338
- include Hanami::Mailer
339
- # ...
340
-
341
- def prepare
342
- mail.attachments['invoice.pdf'] = '/path/to/invoice.pdf'
343
- # or
344
- mail.attachments['invoice.pdf'] = File.read('/path/to/invoice.pdf')
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
- ### Delivery Method
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.configuration do
355
- delivery_method :smtp
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.configuration do
361
- delivery_method :smtp, address: "localhost", port: 1025
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
- Builtin options are:
554
+ ### Previewing messages
366
555
 
367
- * Exim (`:exim`)
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
- ### Custom Delivery Method
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
- Developers can specify their own custom delivery policy:
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
- require 'hanami/mailer'
569
+ message = mailer.prepare(user: {name: "Alice", email: "alice@example.com"})
379
570
 
380
- class MandrillDeliveryMethod
381
- def initialize(options)
382
- @options = options
383
- end
571
+ preview = mailer.delivery_method.preview(message)
572
+ ```
384
573
 
385
- def deliver!(mail)
386
- # ...
387
- end
388
- end
574
+ ### Using a custom view class
389
575
 
390
- Hanami::Mailer.configure do
391
- delivery_method MandrillDeliveryMethod,
392
- username: ENV['MANDRILL_USERNAME'],
393
- password: ENV['MANDRILL_API_KEY']
394
- end.load!
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
- The class passed to `.delivery_method` must accept an optional set of options
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
- ### Multipart Delivery
592
+ ### Custom rendering without Hanami View
401
593
 
402
- All the email are sent as multipart messages by default.
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
- InvoiceMailer.deliver # delivers both text and html templates
407
- InvoiceMailer.deliver(format: :txt) # delivers only text template
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
- Please note that **they aren't both mandatory, but at least one of them MUST** be present.
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
- ## Versioning
627
+ ## Links
413
628
 
414
- __Hanami::Mailer__ uses [Semantic Versioning 2.0.0](http://semver.org)
629
+ - [User documentation](https://hanamirb.org)
630
+ - [API documentation](http://rubydoc.info/gems/hanami-mailer)
415
631
 
416
- ## Copyright
417
632
 
418
- Copyright © 2015-2021 Luca Guidi – Released under MIT License
633
+ ## License
419
634
 
420
- This project was formerly known as Lotus (`lotus-mailer`).
635
+ See `LICENSE` file.