hanami-mailer 1.3.2 → 3.0.0.rc1

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,421 +1,626 @@
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
- [![Build Status](https://ci.hanamirb.org/api/badges/hanami/mailer/status.svg)](https://ci.hanamirb.org/hanami/mailer)
9
- [![CircleCI](https://circleci.com/gh/hanami/mailer/tree/master.svg?style=svg)](https://circleci.com/gh/hanami/mailer/tree/master)
10
- [![Test Coverage](https://codecov.io/gh/hanami/mailer/branch/master/graph/badge.svg)](https://codecov.io/gh/hanami/mailer)
11
- [![Depfu](https://badges.depfu.com/badges/739c6e10eaf20d3ba4240d00828284db/overview.svg)](https://depfu.com/github/hanami/mailer?project=Bundler)
12
- [![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]
13
12
 
14
- ## Contact
13
+ Email delivery for Hanami apps and Ruby projects.
15
14
 
16
- * Home page: http://hanamirb.org
17
- * Mailing List: http://hanamirb.org/mailing-list
18
- * API Doc: http://rdoc.info/gems/hanami-mailer
19
- * Bugs/Issues: https://github.com/hanami/mailer/issues
20
- * Support: http://stackoverflow.com/questions/tagged/hanami
21
- * Chat: http://chat.hanamirb.org
15
+ ## Installation
22
16
 
23
- ## Rubies
17
+ Add the following to your app's Gemfile.
24
18
 
25
- __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
+ ```
26
23
 
27
- ## Installation
24
+ ## Usage
28
25
 
29
- 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.
30
31
 
31
32
  ```ruby
32
- 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
33
43
  ```
34
44
 
35
- And then execute:
45
+ The HTML and text bodies come from these templates.
36
46
 
37
- $ bundle
47
+ `app/templates/mailers/welcome_mailer.html.erb`:
38
48
 
39
- Or install it yourself as:
49
+ ```erb
50
+ <h1>Welcome to our app!</h1>
51
+ ```
40
52
 
41
- $ gem install hanami-mailer
53
+ `app/templates/mailers/welcome_mailer.text.erb`:
42
54
 
43
- ## 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.
70
+
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.
72
+
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.
74
+
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)).
76
+
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.
82
+
83
+ ```ruby
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]}!" }
90
+
91
+ expose :user
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>
104
+ ```
105
+
106
+ `app/templates/mailers/user_mailer.text.erb`:
107
+
108
+ ```erb
109
+ Hello, <%= user[:name] %>!
110
+ ```
44
111
 
45
- ### Conventions
112
+ `expose` comes in a few forms:
46
113
 
47
- * Templates are searched under `Hanami::Mailer.configuration.root`, set this value according to your app structure (eg. `"app/templates"`).
48
- * A mailer will look for a template with a file name that is composed by its full class name (eg. `"articles/index"`).
49
- * A template must have two concatenated extensions: one for the format and one for the engine (eg. `".html.erb"`).
50
- * The framework must be loaded before rendering the first time: `Hanami::Mailer.load!`.
114
+ ```ruby
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"
51
123
 
52
- ### Mailers
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}"
128
+ end
129
+ ```
53
130
 
54
- A simple mailer looks like this:
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:
134
+
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.
55
137
 
56
138
  ```ruby
57
- require 'hanami/mailer'
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
58
149
 
59
- class InvoiceMailer
60
- include Hanami::Mailer
150
+ # `greeting` receives the value from the `:greeting` exposure above
151
+ subject { |greeting| greeting }
61
152
  end
153
+
154
+ OrderMailer.new.deliver(customer: {name: "Alice", email: "alice@example.com"})
62
155
  ```
63
156
 
64
- A mailer with `.to` and `.from` addresses and mailer delivery:
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`.
65
160
 
66
161
  ```ruby
67
- require 'hanami/mailer'
68
-
69
- Hanami::Mailer.configure do
70
- delivery_method :smtp,
71
- address: "smtp.gmail.com",
72
- port: 587,
73
- domain: "example.com",
74
- user_name: ENV['SMTP_USERNAME'],
75
- password: ENV['SMTP_PASSWORD'],
76
- authentication: "plain",
77
- enable_starttls_auto: true
78
- end.load!
79
-
80
- class WelcomeMailer
81
- include Hanami::Mailer
82
-
83
- return_path 'bounce@sender.com'
84
- from 'noreply@sender.com'
85
- to 'noreply@recipient.com'
86
- cc 'cc@sender.com'
87
- bcc 'alice@example.com'
88
-
89
- subject 'Welcome'
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"
90
182
  end
183
+ ```
184
+
185
+ ### Overriding headers at delivery time
91
186
 
92
- WelcomeMailer.deliver
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
+ )
93
205
  ```
94
206
 
95
- ### 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
+ ```
96
226
 
97
- 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.
98
228
 
99
229
  ```ruby
100
- require 'hanami/mailer'
230
+ class ApplicationMailer < Hanami::Mailer
231
+ config.attachment_paths = [
232
+ "app/attachments",
233
+ "app/assets/pdfs"
234
+ ]
235
+ end
236
+ ```
237
+
238
+ If a file cannot be found in any of the configured paths, a `MissingAttachmentError` is raised.
101
239
 
102
- User = Struct.new(:name, :username, :email)
103
- luca = User.new('Luca', 'jodosha', 'luca@jodosha.com')
240
+ ### Dynamic attachments
104
241
 
105
- class WelcomeMailer
106
- include Hanami::Mailer
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.
107
243
 
108
- from 'noreply@sender.com'
109
- subject 'Welcome'
110
- to :recipient
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
111
265
 
112
266
  private
113
267
 
114
- def recipient
115
- user.email
268
+ def generate_report_pdf(user)
269
+ # ... generate PDF content
116
270
  end
117
271
  end
272
+ ```
273
+
274
+ You can also return multiple attachments from a single block.
118
275
 
119
- WelcomeMailer.deliver(user: luca)
276
+ ```ruby
277
+ attachment do |documents:|
278
+ documents.map do |doc|
279
+ file(doc[:name], doc[:content])
280
+ end
281
+ end
120
282
  ```
121
283
 
122
- The corresponding `erb` file:
284
+ Or use a named instance method instead of a block.
123
285
 
124
- ```erb
125
- 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
126
306
  ```
127
307
 
128
- ### Scope
308
+ ### Inline attachments
129
309
 
130
- 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`.
131
311
 
132
312
  ```ruby
133
- require 'hanami/mailer'
313
+ class NewsletterMailer < Hanami::Mailer
314
+ from "news@example.com"
315
+ to { |subscriber:| subscriber[:email] }
316
+ subject "Weekly Newsletter"
134
317
 
135
- class WelcomeMailer
136
- include Hanami::Mailer
318
+ expose :subscriber
137
319
 
138
- from 'noreply@sender.com'
139
- to 'noreply@recipient.com'
140
- subject 'Welcome'
320
+ attachment do
321
+ file("header-image.png", header_image_data, inline: true)
322
+ end
323
+
324
+ private
141
325
 
142
- def greeting
143
- 'Ahoy'
326
+ def header_image_data
327
+ File.read("app/assets/images/newsletter-header.png")
144
328
  end
145
329
  end
146
330
  ```
147
331
 
148
- ```erb
149
- <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">
150
336
  ```
151
337
 
152
- ### Template
338
+ Static attachments can also be made inline.
153
339
 
154
- 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.
155
347
 
156
348
  ```ruby
157
- # Given this root
158
- 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"
159
353
 
160
- # For InvoiceMailer, it looks for:
161
- # * app/templates/invoice_mailer.html.erb
162
- # * app/templates/invoice_mailer.txt.erb
354
+ # Class-level attachment always included
355
+ attachment "terms.pdf"
356
+ end
357
+
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
+ )
163
377
  ```
164
378
 
165
- 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.
166
384
 
167
385
  ```ruby
168
- class InvoiceMailer
169
- include Hanami::Mailer
386
+ class CampaignMailer < Hanami::Mailer
387
+ from "campaigns@example.com"
388
+ to { |recipient:| recipient[:email] }
389
+ subject "Special Offer"
390
+
391
+ # Static delivery option
392
+ delivery_option :track_opens, true
170
393
 
171
- template 'invoice'
394
+ # Dynamic delivery option
395
+ delivery_option(:send_at) { |scheduled_time:| scheduled_time }
396
+ delivery_option(:tags) { |campaign:| ["campaign-#{campaign[:id]}"] }
172
397
  end
173
398
 
174
- # It will look for:
175
- # * app/templates/invoice.html.erb
176
- # * 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
+ )
177
405
  ```
178
406
 
179
- ### Engines
180
-
181
- The builtin rendering engine is [ERb](http://en.wikipedia.org/wiki/ERuby).
182
-
183
- This is the list of the supported engines.
184
- They are listed in order of **higher precedence**, for a given extension.
185
- For instance, if [ERubis](http://www.kuwata-lab.com/erubis/) is loaded, it will be preferred over ERb to render `.erb` templates.
186
-
187
- <table>
188
- <tr>
189
- <th>Engine</th>
190
- <th>Extensions</th>
191
- </tr>
192
- <tr>
193
- <td>Erubis</td>
194
- <td>erb, rhtml, erubis</td>
195
- </tr>
196
- <tr>
197
- <td>ERb</td>
198
- <td>erb, rhtml</td>
199
- </tr>
200
- <tr>
201
- <td>Redcarpet</td>
202
- <td>markdown, mkd, md</td>
203
- </tr>
204
- <tr>
205
- <td>RDiscount</td>
206
- <td>markdown, mkd, md</td>
207
- </tr>
208
- <tr>
209
- <td>Kramdown</td>
210
- <td>markdown, mkd, md</td>
211
- </tr>
212
- <tr>
213
- <td>Maruku</td>
214
- <td>markdown, mkd, md</td>
215
- </tr>
216
- <tr>
217
- <td>BlueCloth</td>
218
- <td>markdown, mkd, md</td>
219
- </tr>
220
- <tr>
221
- <td>Asciidoctor</td>
222
- <td>ad, adoc, asciidoc</td>
223
- </tr>
224
- <tr>
225
- <td>Builder</td>
226
- <td>builder</td>
227
- </tr>
228
- <tr>
229
- <td>CSV</td>
230
- <td>rcsv</td>
231
- </tr>
232
- <tr>
233
- <td>CoffeeScript</td>
234
- <td>coffee</td>
235
- </tr>
236
- <tr>
237
- <td>WikiCloth</td>
238
- <td>wiki, mediawiki, mw</td>
239
- </tr>
240
- <tr>
241
- <td>Creole</td>
242
- <td>wiki, creole</td>
243
- </tr>
244
- <tr>
245
- <td>Etanni</td>
246
- <td>etn, etanni</td>
247
- </tr>
248
- <tr>
249
- <td>Haml</td>
250
- <td>haml</td>
251
- </tr>
252
- <tr>
253
- <td>Less</td>
254
- <td>less</td>
255
- </tr>
256
- <tr>
257
- <td>Liquid</td>
258
- <td>liquid</td>
259
- </tr>
260
- <tr>
261
- <td>Markaby</td>
262
- <td>mab</td>
263
- </tr>
264
- <tr>
265
- <td>Nokogiri</td>
266
- <td>nokogiri</td>
267
- </tr>
268
- <tr>
269
- <td>Plain</td>
270
- <td>html</td>
271
- </tr>
272
- <tr>
273
- <td>RDoc</td>
274
- <td>rdoc</td>
275
- </tr>
276
- <tr>
277
- <td>Radius</td>
278
- <td>radius</td>
279
- </tr>
280
- <tr>
281
- <td>RedCloth</td>
282
- <td>textile</td>
283
- </tr>
284
- <tr>
285
- <td>Sass</td>
286
- <td>sass</td>
287
- </tr>
288
- <tr>
289
- <td>Scss</td>
290
- <td>scss</td>
291
- </tr>
292
- <tr>
293
- <td>Slim</td>
294
- <td>slim</td>
295
- </tr>
296
- <tr>
297
- <td>String</td>
298
- <td>str</td>
299
- </tr>
300
- <tr>
301
- <td>Yajl</td>
302
- <td>yajl</td>
303
- </tr>
304
- </table>
305
-
306
-
307
- ### Configuration
308
-
309
- __Hanami::Mailer__ can be configured with a DSL that determines its behavior.
310
- 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.
311
412
 
312
413
  ```ruby
313
- require 'hanami/mailer'
314
-
315
- Hanami::Mailer.configure do
316
- # Set the root path where to search for templates
317
- # Argument: String, Pathname, #to_pathname, defaults to the current directory
318
- #
319
- root '/path/to/root'
320
-
321
- # Set the default charset for emails
322
- # Argument: String, defaults to "UTF-8"
323
- #
324
- default_charset 'iso-8859'
325
-
326
- # Set the delivery method
327
- # Argument: Symbol
328
- # Argument: Hash, optional configurations
329
- delivery_method :stmp
414
+ class ApplicationMailer < Hanami::Mailer
415
+ from "noreply@example.com"
416
+ config.attachment_paths = ["app/attachments"]
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"
330
433
  end
331
434
  ```
332
435
 
333
- ### Attachments
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.
334
441
 
335
- Attachments can be added with the following API:
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.
336
464
 
337
465
  ```ruby
338
- class InvoiceMailer
339
- include Hanami::Mailer
340
- # ...
341
-
342
- def prepare
343
- mail.attachments['invoice.pdf'] = '/path/to/invoice.pdf'
344
- # or
345
- mail.attachments['invoice.pdf'] = File.read('/path/to/invoice.pdf')
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
481
+ ```
482
+
483
+ #### Custom delivery methods
484
+
485
+ Implement your own delivery method by creating a class that responds to `#call(message)` and returns a `Delivery::Result`.
486
+
487
+ ```ruby
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
+ )
346
509
  end
347
510
  end
348
511
  ```
349
512
 
350
- ### Delivery Method
351
-
352
- 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.
353
514
 
354
515
  ```ruby
355
- Hanami::Mailer.configuration do
356
- 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
357
524
  end
358
525
  ```
359
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
+
360
531
  ```ruby
361
- Hanami::Mailer.configuration do
362
- 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
363
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)
364
552
  ```
365
553
 
366
- Builtin options are:
554
+ ### Previewing messages
367
555
 
368
- * Exim (`:exim`)
369
- * Sendmail (`:sendmail`)
370
- * SMTP (`:smtp`, for local installations)
371
- * SMTP Connection (`:smtp_connection`, via `Net::SMTP` - for remote installations)
372
- * 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.
557
+
558
+ ```ruby
559
+ mailer = WelcomeMailer.new
560
+ message = mailer.prepare(user: {name: "Alice", email: "alice@example.com"})
561
+
562
+ preview = mailer.delivery_method.preview(message)
563
+ ```
373
564
 
374
- ### Custom Delivery Method
565
+ ### Using a custom view class
375
566
 
376
- Developers can specify their own custom delivery policy:
567
+ 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.
377
568
 
378
569
  ```ruby
379
- require 'hanami/mailer'
570
+ class ReportMailer < Hanami::Mailer
571
+ config.view_class = MyApp::View
380
572
 
381
- class MandrillDeliveryMethod
382
- def initialize(options)
383
- @options = options
384
- end
573
+ from "reports@example.com"
574
+ to { |user:| user[:email] }
575
+ subject "Monthly Report"
385
576
 
386
- def deliver!(mail)
387
- # ...
388
- end
577
+ expose :user
389
578
  end
390
-
391
- Hanami::Mailer.configure do
392
- delivery_method MandrillDeliveryMethod,
393
- username: ENV['MANDRILL_USERNAME'],
394
- password: ENV['MANDRILL_API_KEY']
395
- end.load!
396
579
  ```
397
580
 
398
- The class passed to `.delivery_method` must accept an optional set of options
399
- with the constructor (`#initialize`) and respond to `#deliver!`.
581
+ Because template paths are inherited from the configured view class, you typically don't need to set `config.paths` or yourself in this case.
400
582
 
401
- ### Multipart Delivery
583
+ ### Custom rendering without Hanami View
402
584
 
403
- All the email are sent as multipart messages by default.
404
- For a given mailer, the framework looks up for associated text (`.txt`) and `HTML` (`.html`) templates and render them.
585
+ If you don't use Hanami View, override the internal rendering methods. These are also a hook for integrations with other rendering systems.
405
586
 
406
587
  ```ruby
407
- InvoiceMailer.deliver # delivers both text and html templates
408
- InvoiceMailer.deliver(format: :txt) # delivers only text template
588
+ class CustomMailer < Hanami::Mailer
589
+ from "custom@example.com"
590
+ to { |user:| user[:email] }
591
+ subject "Custom Email"
592
+
593
+ expose :user
594
+
595
+ private
596
+
597
+ def render_view(format, input)
598
+ user = input[:user]
599
+
600
+ case format
601
+ when :html
602
+ <<~HTML
603
+ <html>
604
+ <body>
605
+ <h1>Hello, #{user[:name]}!</h1>
606
+ </body>
607
+ </html>
608
+ HTML
609
+ when :text
610
+ "Hello, #{user[:name]}!"
611
+ end
612
+ end
613
+ end
409
614
  ```
410
615
 
411
- Please note that **they aren't both mandatory, but at least one of them MUST** be present.
616
+ 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.
412
617
 
413
- ## Versioning
618
+ ## Links
414
619
 
415
- __Hanami::Mailer__ uses [Semantic Versioning 2.0.0](http://semver.org)
620
+ - [User documentation](https://hanamirb.org)
621
+ - [API documentation](http://rubydoc.info/gems/hanami-mailer)
416
622
 
417
- ## Copyright
418
623
 
419
- Copyright © 2015-2017 Luca Guidi – Released under MIT License
624
+ ## License
420
625
 
421
- This project was formerly known as Lotus (`lotus-mailer`).
626
+ See `LICENSE` file.