studio-engine 0.5.8 → 0.5.9
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 +1 -1
- data/lib/studio/email_smoke.rb +176 -0
- data/lib/studio/engine.rb +1 -0
- data/lib/studio/version.rb +1 -1
- data/lib/studio.rb +1 -0
- data/lib/tasks/studio_email.rake +20 -0
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cba132c4901014fde9dfb1404661065d526eebf7c3db40f29198413bf6aee994
|
|
4
|
+
data.tar.gz: 676040a89d09f62b86f3bf25ffa8a07418d9976900377e8dbdab0c8088194687
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c33acfe1016923235e9de220b719922b9885b4da918205004b18337722d07c17f2077a67d5fe7074c48202cd01cb193fc49fe0669bcf9ea02dd0325362a6a1a2
|
|
7
|
+
data.tar.gz: cbe15ef16f573cba78e7bf494da84b68e144f0cd0790695f8393f5d37a022ec37e6b1562a6be5babeab53eed61f35c8fd7f4fa544b60e540d2ab337f1a0dc3cd
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,15 @@ The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This pro
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## v0.5.9 (2026-06-14)
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- **`bin/rails "email:smoke[to@example.com]"`** — shared provider smoke-test
|
|
11
|
+
task that sends one direct ActionMailer message through the current transport
|
|
12
|
+
and prints the app, sender, transport, delivery method, `perform_deliveries`,
|
|
13
|
+
external-send status, and message id. It refuses capture/test/file modes by
|
|
14
|
+
default so agents do not mistake swallowed mail for a provider proof.
|
|
15
|
+
|
|
7
16
|
## v0.5.8 (2026-06-14)
|
|
8
17
|
|
|
9
18
|
### Added
|
data/README.md
CHANGED
|
@@ -11,7 +11,7 @@ Shared Rails engine for McRitchie apps. Provides authentication, error handling,
|
|
|
11
11
|
gem "studio-engine", "~> 0.5"
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
Then `bundle install`. The current release is **v0.5.
|
|
14
|
+
Then `bundle install`. The current release is **v0.5.9**; see [`CHANGELOG.md`](./CHANGELOG.md) for the history.
|
|
15
15
|
|
|
16
16
|
> Published to RubyGems as of v0.4.0 (2026-05-17). New installs should use the RubyGems form, which the consumer Rails apps (`mcritchie-studio`, `turf-monster`) already use.
|
|
17
17
|
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
|
|
5
|
+
module Studio
|
|
6
|
+
class EmailSmoke
|
|
7
|
+
NON_EXTERNAL_METHODS = %w[test file].freeze
|
|
8
|
+
|
|
9
|
+
Result = Struct.new(
|
|
10
|
+
:app_name,
|
|
11
|
+
:to,
|
|
12
|
+
:from,
|
|
13
|
+
:subject,
|
|
14
|
+
:transport,
|
|
15
|
+
:delivery_method,
|
|
16
|
+
:perform_deliveries,
|
|
17
|
+
:external_delivery,
|
|
18
|
+
:message_id,
|
|
19
|
+
keyword_init: true
|
|
20
|
+
) do
|
|
21
|
+
def report_lines
|
|
22
|
+
[
|
|
23
|
+
"Email smoke -> #{external_delivery ? 'sent' : 'not externally sent'}",
|
|
24
|
+
" app=#{app_name}",
|
|
25
|
+
" to=#{to}",
|
|
26
|
+
" from=#{from}",
|
|
27
|
+
" subject=#{subject}",
|
|
28
|
+
" transport=#{transport}",
|
|
29
|
+
" delivery_method=#{delivery_method}",
|
|
30
|
+
" perform_deliveries=#{perform_deliveries}",
|
|
31
|
+
" external_delivery=#{external_delivery}",
|
|
32
|
+
" message_id=#{message_id || '(none)'}"
|
|
33
|
+
]
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
class NonExternalDeliveryError < StandardError
|
|
38
|
+
attr_reader :result
|
|
39
|
+
|
|
40
|
+
def initialize(result)
|
|
41
|
+
@result = result
|
|
42
|
+
super("email smoke would not send externally")
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
class << self
|
|
47
|
+
def deliver(to:,
|
|
48
|
+
action_mailer: defined?(ActionMailer) ? ActionMailer::Base : nil,
|
|
49
|
+
env: ENV,
|
|
50
|
+
app_name: defined?(Studio) && Studio.respond_to?(:app_name) ? Studio.app_name : "Studio",
|
|
51
|
+
require_external: true,
|
|
52
|
+
clock: Time)
|
|
53
|
+
raise ArgumentError, "action_mailer is required" unless action_mailer
|
|
54
|
+
|
|
55
|
+
recipient = to.to_s.strip
|
|
56
|
+
raise ArgumentError, "recipient email is required" if recipient.empty?
|
|
57
|
+
|
|
58
|
+
from = sender(env)
|
|
59
|
+
subject = "#{app_name} email smoke test"
|
|
60
|
+
body = body_for(
|
|
61
|
+
app_name: app_name,
|
|
62
|
+
to: recipient,
|
|
63
|
+
from: from,
|
|
64
|
+
transport: transport_label(action_mailer: action_mailer, env: env),
|
|
65
|
+
delivery_method: action_mailer.delivery_method,
|
|
66
|
+
perform_deliveries: action_mailer.perform_deliveries,
|
|
67
|
+
sent_at: clock.now.utc
|
|
68
|
+
)
|
|
69
|
+
result = result_for(
|
|
70
|
+
app_name: app_name,
|
|
71
|
+
to: recipient,
|
|
72
|
+
from: from,
|
|
73
|
+
subject: subject,
|
|
74
|
+
action_mailer: action_mailer,
|
|
75
|
+
env: env
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
if require_external && !result.external_delivery
|
|
79
|
+
raise NonExternalDeliveryError, result
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
message = action_mailer.mail(
|
|
83
|
+
to: recipient,
|
|
84
|
+
from: from,
|
|
85
|
+
subject: subject,
|
|
86
|
+
body: body
|
|
87
|
+
)
|
|
88
|
+
delivered = message.deliver_now
|
|
89
|
+
result.message_id = delivered.message_id if delivered.respond_to?(:message_id)
|
|
90
|
+
result
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def result_for(app_name:, to:, from:, subject:, action_mailer:, env: ENV)
|
|
94
|
+
delivery_method = action_mailer.delivery_method.to_s
|
|
95
|
+
Result.new(
|
|
96
|
+
app_name: app_name,
|
|
97
|
+
to: to,
|
|
98
|
+
from: from,
|
|
99
|
+
subject: subject,
|
|
100
|
+
transport: transport_label(action_mailer: action_mailer, env: env),
|
|
101
|
+
delivery_method: delivery_method,
|
|
102
|
+
perform_deliveries: !!action_mailer.perform_deliveries,
|
|
103
|
+
external_delivery: external_delivery?(action_mailer: action_mailer, env: env)
|
|
104
|
+
)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def sender(env = ENV)
|
|
108
|
+
studio_value(:mailer_from) ||
|
|
109
|
+
env_value(env, "MAILER_FROM") ||
|
|
110
|
+
"McRitchie Studio <team@mcritchie.studio>"
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def transport_label(action_mailer:, env: ENV)
|
|
114
|
+
return "capture" if studio_local_email_capture?
|
|
115
|
+
return "ses" if studio_ses_transport_ready?(env)
|
|
116
|
+
|
|
117
|
+
delivery_method = action_mailer.delivery_method.to_s
|
|
118
|
+
return "resend" if delivery_method == "resend"
|
|
119
|
+
|
|
120
|
+
delivery_method.empty? ? "unknown" : delivery_method
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def external_delivery?(action_mailer:, env: ENV)
|
|
124
|
+
return false if studio_local_email_capture?
|
|
125
|
+
|
|
126
|
+
delivery_method = action_mailer.delivery_method.to_s
|
|
127
|
+
!!action_mailer.perform_deliveries && !NON_EXTERNAL_METHODS.include?(delivery_method)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
private
|
|
131
|
+
|
|
132
|
+
def body_for(app_name:, to:, from:, transport:, delivery_method:, perform_deliveries:, sent_at:)
|
|
133
|
+
<<~TEXT
|
|
134
|
+
Email smoke test for #{app_name}
|
|
135
|
+
|
|
136
|
+
This message was sent by studio-engine's shared email smoke task.
|
|
137
|
+
|
|
138
|
+
To: #{to}
|
|
139
|
+
From: #{from}
|
|
140
|
+
Transport: #{transport}
|
|
141
|
+
Delivery method: #{delivery_method}
|
|
142
|
+
perform_deliveries: #{perform_deliveries}
|
|
143
|
+
Sent at: #{sent_at.iso8601}
|
|
144
|
+
|
|
145
|
+
If you did not expect this message, no account action was performed.
|
|
146
|
+
TEXT
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def env_value(env, key)
|
|
150
|
+
value = env[key]
|
|
151
|
+
value if value && !value.to_s.strip.empty?
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def studio_value(method)
|
|
155
|
+
return unless defined?(Studio) && Studio.respond_to?(method)
|
|
156
|
+
|
|
157
|
+
value = Studio.public_send(method)
|
|
158
|
+
value if value && !value.to_s.strip.empty?
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def studio_ses_transport_ready?(env)
|
|
162
|
+
if defined?(Studio) && Studio.respond_to?(:ses_transport_ready?)
|
|
163
|
+
Studio.ses_transport_ready?(env)
|
|
164
|
+
else
|
|
165
|
+
env["MAIL_TRANSPORT"].to_s.downcase == "ses" &&
|
|
166
|
+
env_value(env, "SES_SMTP_USERNAME") &&
|
|
167
|
+
env_value(env, "SES_SMTP_PASSWORD")
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def studio_local_email_capture?
|
|
172
|
+
defined?(Studio) && Studio.respond_to?(:local_email_capture?) && Studio.local_email_capture?
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
data/lib/studio/engine.rb
CHANGED
data/lib/studio/version.rb
CHANGED
data/lib/studio.rb
CHANGED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
unless Rake::Task.task_defined?("email:smoke")
|
|
4
|
+
namespace :email do
|
|
5
|
+
desc "Send one provider smoke-test email: bin/rails \"email:smoke[to@example.com]\""
|
|
6
|
+
task :smoke, [:to] => :environment do |_task, args|
|
|
7
|
+
require "studio/email_smoke"
|
|
8
|
+
|
|
9
|
+
recipient = args[:to] || ENV["EMAIL_SMOKE_TO"] || ENV["TO"]
|
|
10
|
+
abort "Usage: bin/rails \"email:smoke[to@example.com]\" or EMAIL_SMOKE_TO=to@example.com bin/rails email:smoke" if recipient.to_s.strip.empty?
|
|
11
|
+
|
|
12
|
+
require_external = !%w[1 true yes on].include?(ENV["EMAIL_SMOKE_ALLOW_NON_EXTERNAL"].to_s.strip.downcase)
|
|
13
|
+
result = Studio::EmailSmoke.deliver(to: recipient, require_external: require_external)
|
|
14
|
+
puts result.report_lines
|
|
15
|
+
rescue Studio::EmailSmoke::NonExternalDeliveryError => e
|
|
16
|
+
puts e.result.report_lines
|
|
17
|
+
abort "Refusing to call this a provider smoke test because mail would not leave the process. Set EMAIL_SMOKE_ALLOW_NON_EXTERNAL=1 only when intentionally proving capture/test mode."
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: studio-engine
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.5.
|
|
4
|
+
version: 0.5.9
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Alex McRitchie
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-06-
|
|
11
|
+
date: 2026-06-15 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rails
|
|
@@ -210,6 +210,7 @@ files:
|
|
|
210
210
|
- lib/studio.rb
|
|
211
211
|
- lib/studio/color_scale.rb
|
|
212
212
|
- lib/studio/email.rb
|
|
213
|
+
- lib/studio/email_smoke.rb
|
|
213
214
|
- lib/studio/engine.rb
|
|
214
215
|
- lib/studio/image_cache.rb
|
|
215
216
|
- lib/studio/mail_transport.rb
|
|
@@ -217,6 +218,7 @@ files:
|
|
|
217
218
|
- lib/studio/theme_resolver.rb
|
|
218
219
|
- lib/studio/username_generator.rb
|
|
219
220
|
- lib/studio/version.rb
|
|
221
|
+
- lib/tasks/studio_email.rake
|
|
220
222
|
- lib/tasks/studio_ses.rake
|
|
221
223
|
- studio-engine.gemspec
|
|
222
224
|
- tailwind/studio.tailwind.config.js
|