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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 999421ee15650e57104dd05b4f976a81889303efdde24727cd2e28a3ff34fa8a
4
- data.tar.gz: 85f58035c8e3be5befc9294a45f64c205dc7f47cf29a9a58436186b2bb9ab193
3
+ metadata.gz: 97965d80d9eb093c0661acf2ad396560dad736b311b4512726407f297922013c
4
+ data.tar.gz: d848a74e389fd841b2c3c326fef0caa919571ec1d1bfdf9c7cfedaa2363e7775
5
5
  SHA512:
6
- metadata.gz: 891a902c8da33f28c47afc01db9e9eff203c6a0837fde3f8c4c024974c4aae892d3feb1c563d42a91b1f612af267ba550d2b534226078c362cfc3f05967f5e6f
7
- data.tar.gz: 4b152b6e91fbfc20b591f3f9b3a30e7485137eb261e80610b35d2280c06c5344159a160b3e34b8c86422e9c647ba52f2183fe1850373a6f76d292b01130e87f3
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.rc1...main
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
- new(filename:, content:, inline:)
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.message}"
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 exception raised during delivery, if delivery failed.
54
+ # The error that occurred during delivery, if delivery failed.
52
55
  #
53
- # @return [Exception, nil]
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 success [Boolean] whether delivery succeeded (default: true)
61
- # @param error [Exception, nil] the exception raised, if delivery failed
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, success: true, error: 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
- @success
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
@@ -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(
@@ -3,6 +3,7 @@
3
3
  module Hanami
4
4
  class Mailer
5
5
  # @api public
6
- VERSION = "3.0.0.rc1"
6
+ # @since 3.0.0
7
+ VERSION = "3.0.0"
7
8
  end
8
9
  end
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:|
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hanami-mailer
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.0.rc1
4
+ version: 3.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Hanakai team