belt-messaging 0.1.0 → 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 +4 -4
- data/CHANGELOG.md +9 -0
- data/README.md +12 -0
- data/lib/belt/messaging/email.rb +22 -5
- data/lib/belt/messaging/templates/terraform/main.tf.erb +10 -3
- data/lib/belt/messaging/templates/terraform/outputs.tf.erb +1 -1
- data/lib/belt/messaging/version.rb +1 -1
- data/lib/belt/messaging.rb +5 -2
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e3f46b8a5c65296c353f37726532835ef2c4b65a988861f8d6c26efbf433a7d8
|
|
4
|
+
data.tar.gz: 64d9a505cfd10b21c606f78e7cea5c208147c6857bad19629e2a0b307e24a659
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 217d18276f86e6efd68ae4149a105a01e1a4d541afea332f22fd1e132dd1d39dbc0c21f01ecc16dc45637c30d5d6f6b7d4a809d74e6144adfd07eba465a7f289
|
|
7
|
+
data.tar.gz: 5b5161710702b061b8739c8145a2c92f2023958c73742a20a9bca90c51f1c5ac50582380e98d23da7f09a72fbcf4a9e74aabb0ca590b8f60f761a502eaba8eac
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
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
|
+
|
|
8
|
+
## 0.1.1
|
|
9
|
+
|
|
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.
|
|
11
|
+
|
|
3
12
|
## 0.1.0
|
|
4
13
|
|
|
5
14
|
- Add `Belt::Messaging.send_email(to:, subject:, body:, html:)` — send email via AWS SESv2
|
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
|
data/lib/belt/messaging/email.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
|
@@ -25,9 +25,16 @@ locals {
|
|
|
25
25
|
ManagedBy = "Terraform"
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
# Whether an email identity will be created — known entirely from input vars at
|
|
29
|
+
# plan time. Gate the email_send policy's count on THIS, not on the resulting ARN:
|
|
30
|
+
# the ARN is a computed attribute of the SES identity and isn't known until apply,
|
|
31
|
+
# so using it in a count throws "Invalid count argument".
|
|
32
|
+
email_identity_enabled = var.enable_email && (var.email_domain != "" || var.email_from_address != "")
|
|
33
|
+
|
|
28
34
|
# Safe three-way resolution: domain takes precedence, then address, then empty.
|
|
29
35
|
# If enable_email is true but neither identity var is set, this resolves to ""
|
|
30
|
-
#
|
|
36
|
+
# (email_identity_enabled is false, so nothing references it). Used only inside
|
|
37
|
+
# resource bodies, where an unknown-until-apply value is fine.
|
|
31
38
|
email_identity_arn = var.enable_email ? (
|
|
32
39
|
var.email_domain != "" ? aws_sesv2_email_identity.domain[0].arn : (
|
|
33
40
|
var.email_from_address != "" ? aws_sesv2_email_identity.address[0].arn : ""
|
|
@@ -243,7 +250,7 @@ check "email_identity_configured" {
|
|
|
243
250
|
}
|
|
244
251
|
|
|
245
252
|
data "aws_iam_policy_document" "email_send" {
|
|
246
|
-
count =
|
|
253
|
+
count = local.email_identity_enabled ? 1 : 0
|
|
247
254
|
|
|
248
255
|
statement {
|
|
249
256
|
effect = "Allow"
|
|
@@ -256,7 +263,7 @@ data "aws_iam_policy_document" "email_send" {
|
|
|
256
263
|
}
|
|
257
264
|
|
|
258
265
|
resource "aws_iam_policy" "email_send" {
|
|
259
|
-
count =
|
|
266
|
+
count = local.email_identity_enabled ? 1 : 0
|
|
260
267
|
|
|
261
268
|
name = "${var.app_name}-${var.environment}-email-send"
|
|
262
269
|
description = "Allow Lambda to send email via SESv2"
|
|
@@ -41,7 +41,7 @@ output "email_identity_verification_status" {
|
|
|
41
41
|
|
|
42
42
|
output "email_send_policy_arn" {
|
|
43
43
|
description = "ARN of IAM policy granting ses:SendEmail"
|
|
44
|
-
value =
|
|
44
|
+
value = local.email_identity_enabled ? aws_iam_policy.email_send[0].arn : ""
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
output "email_domain_dkim_tokens" {
|
data/lib/belt/messaging.rb
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|