studio-engine 0.5.7 → 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 +17 -0
- data/README.md +1 -1
- data/app/helpers/studio_email_delivery_helper.rb +35 -0
- 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 +9 -3
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,23 @@ 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
|
+
|
|
16
|
+
## v0.5.8 (2026-06-14)
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
- **`StudioEmailDeliveryHelper#email_delivery_banner_status`** — shared
|
|
20
|
+
non-production banner status text for whether the current process sends
|
|
21
|
+
external email and which transport is active (`resend`, `ses`, `capture`, or
|
|
22
|
+
the ActionMailer delivery method).
|
|
23
|
+
|
|
7
24
|
## v0.5.7 (2026-06-14)
|
|
8
25
|
|
|
9
26
|
### 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,35 @@
|
|
|
1
|
+
module StudioEmailDeliveryHelper
|
|
2
|
+
NON_DELIVERING_EMAIL_METHODS = %w[test file].freeze
|
|
3
|
+
|
|
4
|
+
def email_delivery_banner_status
|
|
5
|
+
delivery_method = studio_email_delivery_method
|
|
6
|
+
capture_enabled = Studio.local_email_capture?
|
|
7
|
+
sends_email = studio_email_perform_deliveries? && !capture_enabled &&
|
|
8
|
+
!NON_DELIVERING_EMAIL_METHODS.include?(delivery_method)
|
|
9
|
+
|
|
10
|
+
"EMAIL SEND #{sends_email} · #{email_delivery_transport_label(delivery_method, capture_enabled)}"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def email_delivery_transport_label(delivery_method = studio_email_delivery_method,
|
|
14
|
+
capture_enabled = Studio.local_email_capture?)
|
|
15
|
+
return "capture" if capture_enabled
|
|
16
|
+
return "ses" if Studio.ses_transport_ready?
|
|
17
|
+
return "resend" if delivery_method == "resend"
|
|
18
|
+
|
|
19
|
+
delivery_method.to_s.strip.empty? ? "unknown" : delivery_method
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def studio_email_delivery_method
|
|
25
|
+
return "unknown" unless defined?(ActionMailer)
|
|
26
|
+
|
|
27
|
+
ActionMailer::Base.delivery_method.to_s
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def studio_email_perform_deliveries?
|
|
31
|
+
return false unless defined?(ActionMailer)
|
|
32
|
+
|
|
33
|
+
ActionMailer::Base.perform_deliveries
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -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,13 +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
|
+
autorequire:
|
|
8
9
|
bindir: bin
|
|
9
10
|
cert_chain: []
|
|
10
|
-
date:
|
|
11
|
+
date: 2026-06-15 00:00:00.000000000 Z
|
|
11
12
|
dependencies:
|
|
12
13
|
- !ruby/object:Gem::Dependency
|
|
13
14
|
name: rails
|
|
@@ -151,6 +152,7 @@ files:
|
|
|
151
152
|
- app/controllers/solana_sessions_controller.rb
|
|
152
153
|
- app/controllers/studio/local_emails_controller.rb
|
|
153
154
|
- app/controllers/theme_settings_controller.rb
|
|
155
|
+
- app/helpers/studio_email_delivery_helper.rb
|
|
154
156
|
- app/helpers/studio_theme_helper.rb
|
|
155
157
|
- app/jobs/error_log_cleanup_job.rb
|
|
156
158
|
- app/jobs/studio/email_delivery_job.rb
|
|
@@ -208,6 +210,7 @@ files:
|
|
|
208
210
|
- lib/studio.rb
|
|
209
211
|
- lib/studio/color_scale.rb
|
|
210
212
|
- lib/studio/email.rb
|
|
213
|
+
- lib/studio/email_smoke.rb
|
|
211
214
|
- lib/studio/engine.rb
|
|
212
215
|
- lib/studio/image_cache.rb
|
|
213
216
|
- lib/studio/mail_transport.rb
|
|
@@ -215,6 +218,7 @@ files:
|
|
|
215
218
|
- lib/studio/theme_resolver.rb
|
|
216
219
|
- lib/studio/username_generator.rb
|
|
217
220
|
- lib/studio/version.rb
|
|
221
|
+
- lib/tasks/studio_email.rake
|
|
218
222
|
- lib/tasks/studio_ses.rake
|
|
219
223
|
- studio-engine.gemspec
|
|
220
224
|
- tailwind/studio.tailwind.config.js
|
|
@@ -226,6 +230,7 @@ metadata:
|
|
|
226
230
|
source_code_uri: https://github.com/amcritchie/studio-engine/tree/main
|
|
227
231
|
bug_tracker_uri: https://github.com/amcritchie/studio-engine/issues
|
|
228
232
|
changelog_uri: https://github.com/amcritchie/studio-engine/blob/main/CHANGELOG.md
|
|
233
|
+
post_install_message:
|
|
229
234
|
rdoc_options: []
|
|
230
235
|
require_paths:
|
|
231
236
|
- lib
|
|
@@ -240,7 +245,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
240
245
|
- !ruby/object:Gem::Version
|
|
241
246
|
version: '0'
|
|
242
247
|
requirements: []
|
|
243
|
-
rubygems_version:
|
|
248
|
+
rubygems_version: 3.5.11
|
|
249
|
+
signing_key:
|
|
244
250
|
specification_version: 4
|
|
245
251
|
summary: Shared Rails engine providing auth, SSO, error logging, theming, and S3-backed
|
|
246
252
|
image caching
|