hanami-mailer 3.0.0.rc1 → 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 +10 -1
- data/README.md +9 -0
- data/lib/hanami/mailer/attachment.rb +6 -1
- data/lib/hanami/mailer/delivery/result.rb +27 -8
- data/lib/hanami/mailer/delivery/smtp.rb +1 -1
- data/lib/hanami/mailer/delivery/test.rb +4 -0
- data/lib/hanami/mailer/errors.rb +6 -0
- data/lib/hanami/mailer/version.rb +2 -1
- data/lib/hanami/mailer.rb +33 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 97965d80d9eb093c0661acf2ad396560dad736b311b4512726407f297922013c
|
|
4
|
+
data.tar.gz: d848a74e389fd841b2c3c326fef0caa919571ec1d1bfdf9c7cfedaa2363e7775
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e7f216b3aa1ca3375092734d8cd90f24f4260636da51e8b2ba81a9d4b5a66d5ff7f8f408d1911aa6fa04c38faa197777063e728ac49872d1b3ac3a865ecdd10a
|
|
7
|
+
data.tar.gz: fbf4df20d0e93cb4e3d524abbc49c2f3307c00077cd1ea9f20539ab9f8ea3b1c175eb3b4033328f43a2c32afb097937a8108be3281e9fe43d848e7059789b820
|
data/CHANGELOG.md
CHANGED
|
@@ -19,7 +19,16 @@ and this project adheres to [Break Versioning](https://www.taoensso.com/break-ve
|
|
|
19
19
|
|
|
20
20
|
### Security
|
|
21
21
|
|
|
22
|
-
[Unreleased]: https://github.com/hanami/hanami-mailer/compare/v3.0.0
|
|
22
|
+
[Unreleased]: https://github.com/hanami/hanami-mailer/compare/v3.0.0...main
|
|
23
|
+
|
|
24
|
+
## [3.0.0] - 2026-06-30
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
|
|
28
|
+
- Rewrite the gem. (@timriley)
|
|
29
|
+
- Require Ruby 3.3 or newer.
|
|
30
|
+
|
|
31
|
+
[3.0.0]: https://github.com/hanami/hanami-mailer/compare/v1.3.3...v3.0.0
|
|
23
32
|
|
|
24
33
|
## [3.0.0.rc1] - 2026-06-16
|
|
25
34
|
|
data/README.md
CHANGED
|
@@ -555,8 +555,17 @@ smtp.call(message)
|
|
|
555
555
|
|
|
556
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
557
|
|
|
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`:
|
|
559
|
+
|
|
558
560
|
```ruby
|
|
559
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:
|
|
567
|
+
|
|
568
|
+
```ruby
|
|
560
569
|
message = mailer.prepare(user: {name: "Alice", email: "alice@example.com"})
|
|
561
570
|
|
|
562
571
|
preview = mailer.delivery_method.preview(message)
|
|
@@ -5,6 +5,7 @@ module Hanami
|
|
|
5
5
|
# Represents an email attachment
|
|
6
6
|
#
|
|
7
7
|
# @api public
|
|
8
|
+
# @since 3.0.0
|
|
8
9
|
class Attachment
|
|
9
10
|
# Common MIME types for attachments
|
|
10
11
|
#
|
|
@@ -66,7 +67,9 @@ module Hanami
|
|
|
66
67
|
def from_file(filename, attachment_paths:, inline: false)
|
|
67
68
|
content = read_attachment_file(filename, attachment_paths)
|
|
68
69
|
|
|
69
|
-
|
|
70
|
+
# A nested name (e.g. "foo/bar.pdf") is resolved via the search paths, but only its
|
|
71
|
+
# basename should reach the recipient as the attachment's filename and content-id.
|
|
72
|
+
new(filename: File.basename(filename), content:, inline:)
|
|
70
73
|
end
|
|
71
74
|
|
|
72
75
|
private
|
|
@@ -99,6 +102,7 @@ module Hanami
|
|
|
99
102
|
# @raise [ArgumentError] if filename or content is missing
|
|
100
103
|
#
|
|
101
104
|
# @api public
|
|
105
|
+
# @since 3.0.0
|
|
102
106
|
def initialize(filename:, content:, content_type: nil, inline: false)
|
|
103
107
|
raise ArgumentError, "filename is required" if filename.nil? || (filename.is_a?(String) && filename.empty?)
|
|
104
108
|
raise ArgumentError, "content is required" if content.nil?
|
|
@@ -115,6 +119,7 @@ module Hanami
|
|
|
115
119
|
# @return [Boolean]
|
|
116
120
|
#
|
|
117
121
|
# @api public
|
|
122
|
+
# @since 3.0.0
|
|
118
123
|
def inline? = @inline
|
|
119
124
|
|
|
120
125
|
private
|
|
@@ -14,7 +14,7 @@ module Hanami
|
|
|
14
14
|
# if result.success?
|
|
15
15
|
# log.info "Delivered to #{result.message.to.join(', ')}"
|
|
16
16
|
# else
|
|
17
|
-
# log.error "Delivery failed: #{result.error
|
|
17
|
+
# log.error "Delivery failed: #{result.error}"
|
|
18
18
|
# end
|
|
19
19
|
#
|
|
20
20
|
# @example A third-party delivery method returning a richer result
|
|
@@ -29,12 +29,14 @@ module Hanami
|
|
|
29
29
|
# end
|
|
30
30
|
#
|
|
31
31
|
# @api public
|
|
32
|
+
# @since 3.0.0
|
|
32
33
|
class Result
|
|
33
34
|
# The prepared message that was (or was attempted to be) delivered.
|
|
34
35
|
#
|
|
35
36
|
# @return [Hanami::Mailer::Message]
|
|
36
37
|
#
|
|
37
38
|
# @api public
|
|
39
|
+
# @since 3.0.0
|
|
38
40
|
attr_reader :message
|
|
39
41
|
|
|
40
42
|
# The raw return value from the delivery method, if any.
|
|
@@ -46,25 +48,31 @@ module Hanami
|
|
|
46
48
|
# @return [Object, nil]
|
|
47
49
|
#
|
|
48
50
|
# @api public
|
|
51
|
+
# @since 3.0.0
|
|
49
52
|
attr_reader :response
|
|
50
53
|
|
|
51
|
-
# The
|
|
54
|
+
# The error that occurred during delivery, if delivery failed.
|
|
52
55
|
#
|
|
53
|
-
#
|
|
56
|
+
# This is `nil` for a successful delivery. For a failed delivery it is a truthy object
|
|
57
|
+
# describing the error. For {SMTP} deliveries, this will be an exception raised during
|
|
58
|
+
# delivery, but delivery methods are free to represent failures with any object that
|
|
59
|
+
# responds to `#to_s`, allowing for objects that carry richer error details.
|
|
60
|
+
#
|
|
61
|
+
# @return [#to_s, nil] the error if delivery failed, or nil if it succeeded
|
|
54
62
|
#
|
|
55
63
|
# @api public
|
|
64
|
+
# @since 3.0.0
|
|
56
65
|
attr_reader :error
|
|
57
66
|
|
|
58
67
|
# @param message [Hanami::Mailer::Message] the prepared message
|
|
59
68
|
# @param response [Object, nil] the raw response from the delivery method
|
|
60
|
-
# @param
|
|
61
|
-
#
|
|
69
|
+
# @param error [#to_s, nil] the error, if delivery failed. The result's success status is
|
|
70
|
+
# derived from its absence.
|
|
62
71
|
#
|
|
63
72
|
# @api private
|
|
64
|
-
def initialize(message:, response: nil,
|
|
73
|
+
def initialize(message:, response: nil, error: nil)
|
|
65
74
|
@message = message
|
|
66
75
|
@response = response
|
|
67
|
-
@success = success
|
|
68
76
|
@error = error
|
|
69
77
|
end
|
|
70
78
|
|
|
@@ -73,8 +81,19 @@ module Hanami
|
|
|
73
81
|
# @return [Boolean]
|
|
74
82
|
#
|
|
75
83
|
# @api public
|
|
84
|
+
# @since 3.0.0
|
|
76
85
|
def success?
|
|
77
|
-
|
|
86
|
+
error.nil?
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Returns true if delivery failed.
|
|
90
|
+
#
|
|
91
|
+
# @return [Boolean]
|
|
92
|
+
#
|
|
93
|
+
# @api public
|
|
94
|
+
# @since 3.0.0
|
|
95
|
+
def failure?
|
|
96
|
+
!success?
|
|
78
97
|
end
|
|
79
98
|
end
|
|
80
99
|
end
|
|
@@ -8,6 +8,7 @@ module Hanami
|
|
|
8
8
|
# SMTP delivery method
|
|
9
9
|
#
|
|
10
10
|
# @api public
|
|
11
|
+
# @since 3.0.0
|
|
11
12
|
class SMTP
|
|
12
13
|
# Initialize SMTP delivery with configuration
|
|
13
14
|
#
|
|
@@ -37,7 +38,6 @@ module Hanami
|
|
|
37
38
|
Result.new(
|
|
38
39
|
message: message,
|
|
39
40
|
response: mail,
|
|
40
|
-
success: delivery_exception.nil?,
|
|
41
41
|
error: delivery_exception
|
|
42
42
|
)
|
|
43
43
|
end
|
|
@@ -6,12 +6,14 @@ module Hanami
|
|
|
6
6
|
# Test delivery method that stores delivery results in memory
|
|
7
7
|
#
|
|
8
8
|
# @api public
|
|
9
|
+
# @since 3.0.0
|
|
9
10
|
class Test
|
|
10
11
|
# Returns all delivery results
|
|
11
12
|
#
|
|
12
13
|
# @return [Array<Delivery::Result>]
|
|
13
14
|
#
|
|
14
15
|
# @api public
|
|
16
|
+
# @since 3.0.0
|
|
15
17
|
def deliveries
|
|
16
18
|
@deliveries ||= []
|
|
17
19
|
end
|
|
@@ -19,6 +21,7 @@ module Hanami
|
|
|
19
21
|
# Clear all delivery results
|
|
20
22
|
#
|
|
21
23
|
# @api public
|
|
24
|
+
# @since 3.0.0
|
|
22
25
|
def clear
|
|
23
26
|
deliveries.clear
|
|
24
27
|
end
|
|
@@ -44,6 +47,7 @@ module Hanami
|
|
|
44
47
|
# @return [Message]
|
|
45
48
|
#
|
|
46
49
|
# @api public
|
|
50
|
+
# @since 3.0.0
|
|
47
51
|
def preview(message)
|
|
48
52
|
message
|
|
49
53
|
end
|
data/lib/hanami/mailer/errors.rb
CHANGED
|
@@ -5,12 +5,14 @@ module Hanami
|
|
|
5
5
|
# Base error class for all Hanami::Mailer errors
|
|
6
6
|
#
|
|
7
7
|
# @api public
|
|
8
|
+
# @since 3.0.0
|
|
8
9
|
class Error < StandardError
|
|
9
10
|
end
|
|
10
11
|
|
|
11
12
|
# Raised when a mailer is missing required delivery configuration
|
|
12
13
|
#
|
|
13
14
|
# @api public
|
|
15
|
+
# @since 3.0.0
|
|
14
16
|
class MissingDeliveryError < Error
|
|
15
17
|
def initialize(message = "Missing delivery method. Configure a delivery method using `config.delivery = ...`")
|
|
16
18
|
super
|
|
@@ -20,6 +22,7 @@ module Hanami
|
|
|
20
22
|
# Raised when a mailer message is missing a sender address
|
|
21
23
|
#
|
|
22
24
|
# @api public
|
|
25
|
+
# @since 3.0.0
|
|
23
26
|
class MissingSenderError < Error
|
|
24
27
|
def initialize(message = "Missing sender. Provide a `from` address")
|
|
25
28
|
super
|
|
@@ -29,6 +32,7 @@ module Hanami
|
|
|
29
32
|
# Raised when a mailer message is missing required recipient information
|
|
30
33
|
#
|
|
31
34
|
# @api public
|
|
35
|
+
# @since 3.0.0
|
|
32
36
|
class MissingRecipientError < Error
|
|
33
37
|
def initialize(message = "Missing recipient. Provide at least one of: to, cc, or bcc")
|
|
34
38
|
super
|
|
@@ -38,6 +42,7 @@ module Hanami
|
|
|
38
42
|
# Raised when a static attachment file cannot be found
|
|
39
43
|
#
|
|
40
44
|
# @api public
|
|
45
|
+
# @since 3.0.0
|
|
41
46
|
class MissingAttachmentError < Error
|
|
42
47
|
def initialize(filename, paths = [])
|
|
43
48
|
message =
|
|
@@ -55,6 +60,7 @@ module Hanami
|
|
|
55
60
|
# Raised when duplicate attachment filenames are detected
|
|
56
61
|
#
|
|
57
62
|
# @api public
|
|
63
|
+
# @since 3.0.0
|
|
58
64
|
class DuplicateAttachmentError < Error
|
|
59
65
|
def initialize(filename)
|
|
60
66
|
super(
|
data/lib/hanami/mailer.rb
CHANGED
|
@@ -9,6 +9,7 @@ module Hanami
|
|
|
9
9
|
# Base mailer class
|
|
10
10
|
#
|
|
11
11
|
# @api public
|
|
12
|
+
# @since 3.0.0
|
|
12
13
|
class Mailer
|
|
13
14
|
# @api private
|
|
14
15
|
def self.gem_loader
|
|
@@ -35,6 +36,7 @@ module Hanami
|
|
|
35
36
|
|
|
36
37
|
# Paths to search for static attachment files
|
|
37
38
|
# @api public
|
|
39
|
+
# @since 3.0.0
|
|
38
40
|
setting :attachment_paths, default: []
|
|
39
41
|
|
|
40
42
|
# Include Hanami::View integration if available.
|
|
@@ -70,6 +72,7 @@ module Hanami
|
|
|
70
72
|
# @return [Attachment] attachment object
|
|
71
73
|
#
|
|
72
74
|
# @api public
|
|
75
|
+
# @since 3.0.0
|
|
73
76
|
#
|
|
74
77
|
# @example
|
|
75
78
|
# mailer.deliver(
|
|
@@ -106,6 +109,7 @@ module Hanami
|
|
|
106
109
|
# @param block [Proc] optional block for computing the value
|
|
107
110
|
#
|
|
108
111
|
# @api public
|
|
112
|
+
# @since 3.0.0
|
|
109
113
|
def header(field_name, value = nil, &block)
|
|
110
114
|
headers.add(field_name, block, default: value)
|
|
111
115
|
end
|
|
@@ -120,6 +124,7 @@ module Hanami
|
|
|
120
124
|
# parameters receive matching keys from the `deliver` input.
|
|
121
125
|
#
|
|
122
126
|
# @api public
|
|
127
|
+
# @since 3.0.0
|
|
123
128
|
STANDARD_HEADERS.each do |field_name|
|
|
124
129
|
define_method(field_name) do |value = nil, &block|
|
|
125
130
|
header(field_name, value, &block)
|
|
@@ -173,6 +178,7 @@ module Hanami
|
|
|
173
178
|
# @param block [Proc] block computing the value (single name only)
|
|
174
179
|
#
|
|
175
180
|
# @api public
|
|
181
|
+
# @since 3.0.0
|
|
176
182
|
def expose(*names, **options, &block)
|
|
177
183
|
if names.length == 1
|
|
178
184
|
exposures.add(names.first, block, **options)
|
|
@@ -190,6 +196,7 @@ module Hanami
|
|
|
190
196
|
# @see #expose
|
|
191
197
|
#
|
|
192
198
|
# @api public
|
|
199
|
+
# @since 3.0.0
|
|
193
200
|
def private_expose(*names, **options, &block)
|
|
194
201
|
expose(*names, **options, private: true, &block)
|
|
195
202
|
end
|
|
@@ -209,6 +216,7 @@ module Hanami
|
|
|
209
216
|
# @param proc [Proc] optional block for computing attachment
|
|
210
217
|
#
|
|
211
218
|
# @api public
|
|
219
|
+
# @since 3.0.0
|
|
212
220
|
def attachment(name_or_filename = nil, **options, &block)
|
|
213
221
|
attachments.add(name_or_filename, block, **options)
|
|
214
222
|
end
|
|
@@ -232,6 +240,7 @@ module Hanami
|
|
|
232
240
|
# @param block [Proc] optional block for computing the value
|
|
233
241
|
#
|
|
234
242
|
# @api public
|
|
243
|
+
# @since 3.0.0
|
|
235
244
|
#
|
|
236
245
|
# @example Static value
|
|
237
246
|
# delivery_option :track_opens, true
|
|
@@ -274,6 +283,7 @@ module Hanami
|
|
|
274
283
|
# @param delivery_method [Object] delivery method (defaults to Test delivery)
|
|
275
284
|
#
|
|
276
285
|
# @api public
|
|
286
|
+
# @since 3.0.0
|
|
277
287
|
def initialize(view: nil, delivery_method: nil)
|
|
278
288
|
@view = view
|
|
279
289
|
@delivery_method = delivery_method || default_delivery_method
|
|
@@ -289,11 +299,32 @@ module Hanami
|
|
|
289
299
|
# @return [Delivery::Result]
|
|
290
300
|
#
|
|
291
301
|
# @api public
|
|
302
|
+
# @since 3.0.0
|
|
292
303
|
def deliver(headers: {}, attachments: nil, format: nil, **input)
|
|
293
304
|
message = prepare(headers:, attachments:, format:, **input)
|
|
294
305
|
delivery_method.call(message)
|
|
295
306
|
end
|
|
296
307
|
|
|
308
|
+
# Previews the email without delivering it
|
|
309
|
+
#
|
|
310
|
+
# Builds the message and passes it to the delivery method's `preview` hook, returning whatever
|
|
311
|
+
# that returns. The default (and test) delivery method returns the message unchanged; a
|
|
312
|
+
# third-party delivery method can override `preview` to apply service-specific logic.
|
|
313
|
+
#
|
|
314
|
+
# @param headers [Hash] optional header overrides (from, to, cc, bcc, reply_to, return_path, subject)
|
|
315
|
+
# @param attachments [Array<Hash, Attachment>, nil] optional runtime attachments
|
|
316
|
+
# @param format [Symbol, nil] optional format to render (:html or :text)
|
|
317
|
+
# @param input [Hash] input data for exposures and rendering
|
|
318
|
+
#
|
|
319
|
+
# @return [Message]
|
|
320
|
+
#
|
|
321
|
+
# @api public
|
|
322
|
+
# @since 3.0.0
|
|
323
|
+
def preview(headers: {}, attachments: nil, format: nil, **input)
|
|
324
|
+
message = prepare(headers:, attachments:, format:, **input)
|
|
325
|
+
delivery_method.preview(message)
|
|
326
|
+
end
|
|
327
|
+
|
|
297
328
|
# rubocop:disable Metrics/AbcSize
|
|
298
329
|
|
|
299
330
|
# Build the message without delivering it
|
|
@@ -306,6 +337,7 @@ module Hanami
|
|
|
306
337
|
# @return [Message]
|
|
307
338
|
#
|
|
308
339
|
# @api public
|
|
340
|
+
# @since 3.0.0
|
|
309
341
|
def prepare(headers: {}, attachments: nil, format: nil, **input)
|
|
310
342
|
# Evaluate exposures as our "locals". These will be provided as the _depdenencies_ (available
|
|
311
343
|
# via positional params) to all our other class-level exposure-like APIs: headers,
|
|
@@ -369,6 +401,7 @@ module Hanami
|
|
|
369
401
|
# @return [Attachment] attachment object
|
|
370
402
|
#
|
|
371
403
|
# @api public
|
|
404
|
+
# @since 3.0.0
|
|
372
405
|
#
|
|
373
406
|
# @example
|
|
374
407
|
# attachment :invoice do |invoice:|
|