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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 401a5db38e438ea4335d910bc67c88559739cb227a7377503c152cb2cea409ba
4
- data.tar.gz: 12703eff81b8af83d9d73e26ebfbd7ec11b6a53444372c8330f32f7b09b89113
3
+ metadata.gz: cba132c4901014fde9dfb1404661065d526eebf7c3db40f29198413bf6aee994
4
+ data.tar.gz: 676040a89d09f62b86f3bf25ffa8a07418d9976900377e8dbdab0c8088194687
5
5
  SHA512:
6
- metadata.gz: 8eff2ade70b7a335ac0f50a15e2085fa05a0ba4d5ae7ff36ff7a03c8ec811022fb4d6ece26fed7639c9faf3431d66a1548cc86ed0f261cf255d2dcd4c84ad51e
7
- data.tar.gz: 72df2ccff7e151d391edd34c62d1a1e1ac1782317a14548f0fcfd541e58646dba89fc13ea254a1a141d1347d9016ee42e04b7c475a426eb9c8eb959725c6a588
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.8**; see [`CHANGELOG.md`](./CHANGELOG.md) for the history.
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
@@ -1,6 +1,7 @@
1
1
  module Studio
2
2
  class Engine < ::Rails::Engine
3
3
  rake_tasks do
4
+ load File.expand_path("../tasks/studio_email.rake", __dir__)
4
5
  load File.expand_path("../tasks/studio_ses.rake", __dir__)
5
6
  end
6
7
 
@@ -1,3 +1,3 @@
1
1
  module Studio
2
- VERSION = "0.5.8"
2
+ VERSION = "0.5.9"
3
3
  end
data/lib/studio.rb CHANGED
@@ -6,6 +6,7 @@ require "studio/username_generator"
6
6
  require "studio/s3"
7
7
  require "studio/image_cache"
8
8
  require "studio/email"
9
+ require "studio/email_smoke"
9
10
  require "studio/mail_transport"
10
11
 
11
12
  module Studio
@@ -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.8
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-14 00:00:00.000000000 Z
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