belt-messaging 0.1.1 → 0.2.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: 66741e509a64ee8df126ea2f5ddefc6b84c6ad544dcb5528be839c6f2a31556f
4
- data.tar.gz: d7d4e424fed7aca6be6c7917fb8fa9de87e92fd6b6b0f69b839ba73b3056966d
3
+ metadata.gz: e3f46b8a5c65296c353f37726532835ef2c4b65a988861f8d6c26efbf433a7d8
4
+ data.tar.gz: 64d9a505cfd10b21c606f78e7cea5c208147c6857bad19629e2a0b307e24a659
5
5
  SHA512:
6
- metadata.gz: cdca528c94667c4fd4dfb4071c3aa5a132dc016d39c668c9d1daa328cbf1290cecada794620dab4940632d8522a699e9eebb8653089eb107f79b67c208630577
7
- data.tar.gz: 6d2f2ba6f8cf82c88c669508910e9b22cbe7f21c4753036c2df13ee11f658294be1f3170e3add197e0a0af4625e50113d287581b1a09f19828c00881bf52e022
6
+ metadata.gz: 217d18276f86e6efd68ae4149a105a01e1a4d541afea332f22fd1e132dd1d39dbc0c21f01ecc16dc45637c30d5d6f6b7d4a809d74e6144adfd07eba465a7f289
7
+ data.tar.gz: 5b5161710702b061b8739c8145a2c92f2023958c73742a20a9bca90c51f1c5ac50582380e98d23da7f09a72fbcf4a9e74aabb0ca590b8f60f761a502eaba8eac
data/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.0
4
+
5
+ - `Belt::Messaging.send_email` accepts `headers:` (Hash of extra message headers) and `reply_to:` (String or Array). Headers go through SESv2 simple content, so you can set `List-Unsubscribe` + `List-Unsubscribe-Post: List-Unsubscribe=One-Click` (RFC 8058) and get the Gmail/Yahoo one-click unsubscribe button. Invalid reply-to addresses reject the send, same as invalid recipients.
6
+ - Require `aws-sdk-sesv2 ~> 1.45` (first release with SendEmail custom headers).
7
+
3
8
  ## 0.1.1
4
9
 
5
10
  - Fix Terraform `email_send` IAM policy failing to apply on a fresh deploy: its `count` was gated on the SES identity ARN, a computed attribute unknown until apply, which raised `Invalid count argument`. Gate the policy document, policy resource, and `email_send_policy_arn` output on a plan-time-known local derived from input variables instead. Without this, the Lambda role never received `ses:SendEmail` and outbound transactional email silently never sent.
data/README.md CHANGED
@@ -44,6 +44,18 @@ Belt::Messaging.send_email(
44
44
  html: '<h1>Weekly Update</h1><p>HTML version...</p>'
45
45
  )
46
46
 
47
+ # Reply-To and custom headers (e.g. RFC 8058 one-click unsubscribe)
48
+ Belt::Messaging.send_email(
49
+ to: 'user@example.com',
50
+ subject: 'You were invited',
51
+ body: 'Join us... Stop these emails: https://example.com/unsub?t=abc',
52
+ reply_to: 'support@example.com',
53
+ headers: {
54
+ 'List-Unsubscribe' => '<https://example.com/unsub?t=abc>',
55
+ 'List-Unsubscribe-Post' => 'List-Unsubscribe=One-Click'
56
+ }
57
+ )
58
+
47
59
  # Phone number utilities
48
60
  Belt::Messaging::PhoneFormatter.format_e164('(555) 123-4567') #=> '+15551234567'
49
61
  Belt::Messaging::PhoneFormatter.valid?('555-123-4567') #=> true
@@ -20,8 +20,12 @@ module Belt
20
20
  # @param body [String] Email body (plain text)
21
21
  # @param html [String, nil] Optional HTML body
22
22
  # @param from [String, nil] Override the configured from address
23
+ # @param reply_to [String, Array<String>, nil] Reply-To address(es)
24
+ # @param headers [Hash{String => String}] Extra message headers, e.g.
25
+ # { 'List-Unsubscribe' => '<https://example.com/unsub?t=abc>',
26
+ # 'List-Unsubscribe-Post' => 'List-Unsubscribe=One-Click' }
23
27
  # @return [Boolean] true if sent successfully, false otherwise
24
- def self.send(to:, subject:, body:, html: nil, from: nil)
28
+ def self.send(to:, subject:, body:, html: nil, from: nil, reply_to: nil, headers: {})
25
29
  config = Belt::Messaging.configuration
26
30
 
27
31
  unless config.email_enabled?
@@ -48,6 +52,13 @@ module Belt
48
52
  return false
49
53
  end
50
54
 
55
+ reply_to_addresses = Array(reply_to)
56
+ invalid_reply_to = reply_to_addresses.reject { |email| valid_email?(email) }
57
+ if invalid_reply_to.any?
58
+ log(:error, 'Invalid reply-to address format', invalid: invalid_reply_to)
59
+ return false
60
+ end
61
+
51
62
  from_address = from || config.email_from_address
52
63
  if from_address.nil? || from_address.empty?
53
64
  log(:error, 'EMAIL_FROM_ADDRESS not configured')
@@ -57,13 +68,16 @@ module Belt
57
68
  begin
58
69
  client = Aws::SESV2::Client.new(region: config.aws_region)
59
70
 
60
- email_content = build_email_content(subject, body, html)
71
+ email_content = build_email_content(subject, body, html, headers)
61
72
 
62
- client.send_email({
73
+ params = {
63
74
  from_email_address: from_address,
64
75
  destination: { to_addresses: recipients },
65
76
  content: { simple: email_content }
66
- })
77
+ }
78
+ params[:reply_to_addresses] = reply_to_addresses if reply_to_addresses.any?
79
+
80
+ client.send_email(params)
67
81
 
68
82
  log(:info, 'Email sent successfully', to: recipients, subject: subject)
69
83
  true
@@ -91,7 +105,7 @@ module Belt
91
105
  email.match?(/\A[^@\s]+@[^@\s]+\.[^@\s]+\z/)
92
106
  end
93
107
 
94
- def self.build_email_content(subject, body, html)
108
+ def self.build_email_content(subject, body, html, headers)
95
109
  content = {
96
110
  subject: { data: subject, charset: 'UTF-8' },
97
111
  body: {
@@ -99,6 +113,9 @@ module Belt
99
113
  }
100
114
  }
101
115
  content[:body][:html] = { data: html, charset: 'UTF-8' } if html
116
+ if headers && !headers.empty?
117
+ content[:headers] = headers.map { |name, value| { name: name.to_s, value: value.to_s } }
118
+ end
102
119
  content
103
120
  end
104
121
  private_class_method :build_email_content
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Belt
4
4
  module Messaging
5
- VERSION = '0.1.1'
5
+ VERSION = '0.2.0'
6
6
  end
7
7
  end
@@ -49,9 +49,12 @@ module Belt
49
49
  # @param body [String] Email body (plain text)
50
50
  # @param html [String, nil] Optional HTML body
51
51
  # @param from [String, nil] Override configured from address
52
+ # @param reply_to [String, Array<String>, nil] Reply-To address(es)
53
+ # @param headers [Hash{String => String}] Extra message headers (e.g. List-Unsubscribe)
52
54
  # @return [Boolean] true if sent successfully
53
- def send_email(to:, subject:, body:, html: nil, from: nil)
54
- Email.send(to: to, subject: subject, body: body, html: html, from: from)
55
+ def send_email(to:, subject:, body:, html: nil, from: nil, reply_to: nil, headers: {})
56
+ Email.send(to: to, subject: subject, body: body, html: html, from: from,
57
+ reply_to: reply_to, headers: headers)
55
58
  end
56
59
  end
57
60
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: belt-messaging
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stowzilla
@@ -43,14 +43,14 @@ dependencies:
43
43
  requirements:
44
44
  - - "~>"
45
45
  - !ruby/object:Gem::Version
46
- version: '1.0'
46
+ version: '1.45'
47
47
  type: :runtime
48
48
  prerelease: false
49
49
  version_requirements: !ruby/object:Gem::Requirement
50
50
  requirements:
51
51
  - - "~>"
52
52
  - !ruby/object:Gem::Version
53
- version: '1.0'
53
+ version: '1.45'
54
54
  description: Belt plugin providing SMS and email messaging. SMS via AWS PinpointSMSVoiceV2,
55
55
  email via AWS SESv2. Includes phone verification, opt-in/opt-out handling, and Terraform
56
56
  infrastructure.