resend_robot 0.1.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.
@@ -0,0 +1,392 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title><%= @email[:subject] %> — Resend Robot</title>
5
+ <meta charset="utf-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1">
7
+ <style>
8
+ @import url('https://fonts.googleapis.com/css2?family=Source+Serif+4:opsz,wght@8..60,400;8..60,600&family=DM+Sans:wght@400;500;600&display=swap');
9
+
10
+ * { margin: 0; padding: 0; box-sizing: border-box; }
11
+
12
+ body {
13
+ font-family: 'DM Sans', -apple-system, BlinkMacSystemFont, sans-serif;
14
+ background: #f8f6f3;
15
+ color: #334155;
16
+ min-height: 100vh;
17
+ display: flex;
18
+ flex-direction: column;
19
+ }
20
+
21
+ a { color: #334155; text-decoration: none; }
22
+ a:hover { text-decoration: underline; }
23
+
24
+ /* ── Top identity bar ── */
25
+ .identity-bar {
26
+ background: #1e293b;
27
+ padding: 10px 24px;
28
+ display: flex;
29
+ align-items: center;
30
+ gap: 10px;
31
+ border-bottom: 2px solid #be123c;
32
+ }
33
+ .identity-bar .logo {
34
+ font-family: 'Source Serif 4', Georgia, serif;
35
+ font-weight: 600;
36
+ font-size: 15px;
37
+ color: #fbbf24;
38
+ letter-spacing: -0.01em;
39
+ }
40
+ .identity-bar .logo:hover { text-decoration: none; color: #fde68a; }
41
+ .identity-bar .env-badge {
42
+ font-size: 10px;
43
+ font-weight: 600;
44
+ text-transform: uppercase;
45
+ letter-spacing: 0.08em;
46
+ color: #1e293b;
47
+ background: #fbbf24;
48
+ padding: 2px 7px;
49
+ border-radius: 3px;
50
+ }
51
+ .identity-bar .all-link {
52
+ font-size: 12px;
53
+ font-weight: 500;
54
+ color: #94a3b8;
55
+ background: rgba(255,255,255,0.08);
56
+ padding: 4px 10px;
57
+ border-radius: 4px;
58
+ border: 1px solid rgba(255,255,255,0.1);
59
+ }
60
+ .identity-bar .all-link:hover { color: #fff; background: rgba(255,255,255,0.15); text-decoration: none; }
61
+ .identity-bar .id-text {
62
+ margin-left: auto;
63
+ font-size: 12px;
64
+ color: #64748b;
65
+ font-family: ui-monospace, 'SF Mono', Monaco, monospace;
66
+ }
67
+
68
+ /* ── Flash messages ── */
69
+ .flash {
70
+ padding: 10px 24px;
71
+ font-size: 13px;
72
+ font-weight: 500;
73
+ }
74
+ .flash-notice { background: #ecfdf5; color: #065f46; border-bottom: 1px solid #a7f3d0; }
75
+ .flash-alert { background: #fef2f2; color: #991b1b; border-bottom: 1px solid #fecaca; }
76
+
77
+ /* ── Metadata panel ── */
78
+ .metadata {
79
+ background: #fff;
80
+ border-bottom: 1px solid #e2e0db;
81
+ padding: 20px 24px;
82
+ box-shadow: 0 1px 2px rgba(0,0,0,0.04);
83
+ }
84
+ .subject-line {
85
+ font-family: 'Source Serif 4', Georgia, serif;
86
+ font-size: 22px;
87
+ font-weight: 600;
88
+ color: #1e293b;
89
+ margin-bottom: 16px;
90
+ line-height: 1.3;
91
+ }
92
+ .fields {
93
+ display: grid;
94
+ grid-template-columns: auto 1fr;
95
+ gap: 6px 16px;
96
+ align-items: baseline;
97
+ font-size: 13px;
98
+ }
99
+ .field-label {
100
+ font-weight: 600;
101
+ color: #94a3b8;
102
+ text-transform: uppercase;
103
+ font-size: 10px;
104
+ letter-spacing: 0.06em;
105
+ padding-top: 2px;
106
+ text-align: left;
107
+ white-space: nowrap;
108
+ }
109
+ .field-value {
110
+ color: #334155;
111
+ font-size: 13px;
112
+ line-height: 1.5;
113
+ word-break: break-all;
114
+ }
115
+ .field-value .addr {
116
+ display: inline-block;
117
+ background: #f1f5f9;
118
+ padding: 1px 8px;
119
+ border-radius: 4px;
120
+ margin: 1px 2px;
121
+ font-family: ui-monospace, 'SF Mono', Monaco, monospace;
122
+ font-size: 12px;
123
+ }
124
+ .field-value .tag {
125
+ display: inline-block;
126
+ background: #fff1f2;
127
+ color: #9f1239;
128
+ padding: 1px 8px;
129
+ border-radius: 4px;
130
+ margin: 1px 2px;
131
+ font-size: 12px;
132
+ font-weight: 500;
133
+ }
134
+ .field-value .timestamp { color: #64748b; }
135
+
136
+ .divider {
137
+ grid-column: 1 / -1;
138
+ height: 1px;
139
+ background: #f1f0ed;
140
+ margin: 4px 0;
141
+ }
142
+
143
+ /* ── Two-column layout: body + sidebar ── */
144
+ .content-layout {
145
+ display: flex;
146
+ gap: 24px;
147
+ flex: 1;
148
+ }
149
+
150
+ /* ── Email body — inline rendered ── */
151
+ .body-section {
152
+ flex: 1;
153
+ min-width: 0;
154
+ border-top: 1px solid #e2e0db;
155
+ overflow: hidden;
156
+ }
157
+ .email-body-content {
158
+ background: #fff;
159
+ padding: 24px;
160
+ }
161
+ .body-section pre {
162
+ padding: 24px;
163
+ white-space: pre-wrap;
164
+ word-wrap: break-word;
165
+ font-family: ui-monospace, 'SF Mono', Monaco, monospace;
166
+ font-size: 13px;
167
+ line-height: 1.7;
168
+ color: #334155;
169
+ }
170
+ .body-section .empty {
171
+ padding: 40px 24px;
172
+ text-align: center;
173
+ color: #94a3b8;
174
+ font-style: italic;
175
+ }
176
+
177
+ /* ── Sidebar (reply) ── */
178
+ .sidebar {
179
+ width: 280px;
180
+ flex-shrink: 0;
181
+ display: flex;
182
+ flex-direction: column;
183
+ gap: 12px;
184
+ padding-top: 20px;
185
+ }
186
+ .reply-card {
187
+ background: #fff;
188
+ border: 1px solid #e2e0db;
189
+ border-radius: 6px;
190
+ padding: 16px;
191
+ box-shadow: 0 1px 3px rgba(0,0,0,0.06);
192
+ }
193
+ .reply-card h3 {
194
+ font-size: 13px;
195
+ font-weight: 600;
196
+ color: #1e293b;
197
+ margin-bottom: 4px;
198
+ }
199
+ .reply-card .reply-hint {
200
+ font-size: 12px;
201
+ color: #94a3b8;
202
+ margin-bottom: 12px;
203
+ }
204
+ .reply-card .reply-hint strong {
205
+ color: #64748b;
206
+ font-family: ui-monospace, 'SF Mono', Monaco, monospace;
207
+ font-size: 11px;
208
+ }
209
+ .reply-card textarea {
210
+ width: 100%;
211
+ padding: 10px 12px;
212
+ border: 1px solid #d1d5db;
213
+ border-radius: 5px;
214
+ font-family: inherit;
215
+ font-size: 13px;
216
+ line-height: 1.5;
217
+ resize: vertical;
218
+ background: #fff;
219
+ color: #334155;
220
+ }
221
+ .reply-card textarea:focus {
222
+ outline: none;
223
+ border-color: #be123c;
224
+ box-shadow: 0 0 0 2px rgba(190,18,60,0.1);
225
+ }
226
+ .reply-actions {
227
+ display: flex;
228
+ gap: 8px;
229
+ margin-top: 10px;
230
+ }
231
+ .btn-reply {
232
+ background: #be123c;
233
+ color: #fff;
234
+ border: none;
235
+ padding: 7px 16px;
236
+ border-radius: 5px;
237
+ font-size: 13px;
238
+ font-weight: 500;
239
+ cursor: pointer;
240
+ font-family: inherit;
241
+ }
242
+ .btn-reply:hover { background: #9f1239; }
243
+ .btn-reply:disabled { opacity: 0.6; cursor: not-allowed; }
244
+ .no-reply-hint {
245
+ font-size: 12px;
246
+ color: #94a3b8;
247
+ font-style: italic;
248
+ padding: 8px 0;
249
+ }
250
+ /* ── Responsive ── */
251
+ @media (max-width: 768px) {
252
+ .metadata { padding: 16px; }
253
+ .subject-line { font-size: 18px; }
254
+ .fields { grid-template-columns: 1fr; gap: 2px 0; }
255
+ .content-layout { flex-direction: column; gap: 0; }
256
+ .sidebar { width: 100%; padding: 12px; }
257
+ }
258
+ </style>
259
+ </head>
260
+ <body>
261
+ <%# ── Identity bar ── %>
262
+ <div class="identity-bar">
263
+ <a href="<%= main_app.dev_email_index_path %>" class="logo">Resend Robot</a>
264
+ <span class="env-badge">Dev</span>
265
+ <a href="<%= main_app.dev_email_index_path %>" class="all-link">&larr; All Emails</a>
266
+ <span class="id-text"><%= @email[:id] %></span>
267
+ </div>
268
+
269
+ <%# ── Flash messages ── %>
270
+ <% if flash[:notice] %>
271
+ <div class="flash flash-notice"><%= flash[:notice] %></div>
272
+ <% end %>
273
+ <% if flash[:alert] %>
274
+ <div class="flash flash-alert"><%= flash[:alert] %></div>
275
+ <% end %>
276
+
277
+ <%# ── Metadata panel ── %>
278
+ <div class="metadata">
279
+ <div class="subject-line"><%= @email[:subject] %></div>
280
+ <div class="fields">
281
+ <div class="field-label">From</div>
282
+ <div class="field-value"><span class="addr"><%= @email[:from] %></span></div>
283
+
284
+ <div class="field-label">To</div>
285
+ <div class="field-value">
286
+ <% Array(@email[:to]).each do |addr| %>
287
+ <span class="addr"><%= addr %></span>
288
+ <% end %>
289
+ </div>
290
+
291
+ <% if Array(@email[:cc]).any? %>
292
+ <div class="field-label">CC</div>
293
+ <div class="field-value">
294
+ <% Array(@email[:cc]).each do |addr| %>
295
+ <span class="addr"><%= addr %></span>
296
+ <% end %>
297
+ </div>
298
+ <% end %>
299
+
300
+ <% if Array(@email[:bcc]).any? %>
301
+ <div class="field-label">BCC</div>
302
+ <div class="field-value">
303
+ <% Array(@email[:bcc]).each do |addr| %>
304
+ <span class="addr"><%= addr %></span>
305
+ <% end %>
306
+ </div>
307
+ <% end %>
308
+
309
+ <% if Array(@email[:reply_to]).any? %>
310
+ <div class="field-label">Reply-To</div>
311
+ <div class="field-value">
312
+ <% Array(@email[:reply_to]).each do |addr| %>
313
+ <span class="addr"><%= addr %></span>
314
+ <% end %>
315
+ </div>
316
+ <% end %>
317
+
318
+ <div class="divider"></div>
319
+
320
+ <div class="field-label">Date</div>
321
+ <div class="field-value">
322
+ <span class="timestamp"><%= Time.parse(@email[:sent_at]).strftime("%B %-d, %Y at %-I:%M %p") rescue @email[:sent_at] %></span>
323
+ </div>
324
+
325
+ <% if Array(@email[:tags]).any? %>
326
+ <div class="field-label">Tags</div>
327
+ <div class="field-value">
328
+ <% Array(@email[:tags]).each do |t| %>
329
+ <span class="tag"><%= t[:name] %>: <%= t[:value] %></span>
330
+ <% end %>
331
+ </div>
332
+ <% end %>
333
+
334
+ <% if @email[:headers].present? %>
335
+ <div class="field-label">Headers</div>
336
+ <div class="field-value">
337
+ <% @email[:headers].each do |key, value| %>
338
+ <span class="tag"><%= key %>: <%= value %></span>
339
+ <% end %>
340
+ </div>
341
+ <% end %>
342
+ </div>
343
+ </div>
344
+
345
+ <%# ── Two-column layout: body + sidebar ── %>
346
+ <div class="content-layout">
347
+ <div class="body-section">
348
+ <% if @email_html.present? %>
349
+ <div class="email-body-content"><%= @email_html.html_safe %></div>
350
+ <% elsif @email[:text].present? %>
351
+ <pre><%= @email[:text] %></pre>
352
+ <% else %>
353
+ <div class="empty">No email body</div>
354
+ <% end %>
355
+ </div>
356
+
357
+ <div class="sidebar">
358
+ <div class="reply-card">
359
+ <h3>Simulate Reply</h3>
360
+ <% reply_to = Array(@email[:reply_to]).first %>
361
+ <% if reply_to.present? %>
362
+ <div class="reply-hint">
363
+ Sends through the real webhook pipeline.
364
+ Reply-to: <strong><%= reply_to %></strong>
365
+ </div>
366
+ <form action="<%= main_app.dev_email_reply_path(@email[:id]) %>" method="post" id="reply-form">
367
+ <input type="hidden" name="authenticity_token" value="<%= form_authenticity_token %>">
368
+ <textarea name="body" rows="4" placeholder="Type a reply..."></textarea>
369
+ <div class="reply-actions">
370
+ <button type="submit" class="btn-reply" id="reply-btn">Send Reply</button>
371
+ </div>
372
+ </form>
373
+ <% else %>
374
+ <div class="no-reply-hint">No reply-to address — this email cannot receive simulated replies.</div>
375
+ <% end %>
376
+ </div>
377
+ </div>
378
+ </div>
379
+
380
+ <script>
381
+ document.addEventListener('DOMContentLoaded', function() {
382
+ var form = document.getElementById('reply-form');
383
+ if (!form) return;
384
+ form.addEventListener('submit', function() {
385
+ var btn = document.getElementById('reply-btn');
386
+ btn.disabled = true;
387
+ btn.textContent = 'Sending...';
388
+ });
389
+ });
390
+ </script>
391
+ </body>
392
+ </html>
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ResendRobot
4
+ module Generators
5
+ class InstallGenerator < Rails::Generators::Base
6
+ source_root File.expand_path("templates", __dir__)
7
+
8
+ desc "Install Resend Robot initializer and Claude skills"
9
+
10
+ def copy_initializer
11
+ template "initializer.rb", "config/initializers/resend_robot.rb"
12
+ end
13
+
14
+ def copy_skills
15
+ directory "skills/resend-robot-read", ".claude/skills/resend-robot-read"
16
+ directory "skills/resend-robot-send", ".claude/skills/resend-robot-send"
17
+ end
18
+
19
+ def show_post_install
20
+ say ""
21
+ say "Resend Robot installed!", :green
22
+ say ""
23
+ say " Initializer: config/initializers/resend_robot.rb"
24
+ say " Claude skills: .claude/skills/resend-robot-read/"
25
+ say " .claude/skills/resend-robot-send/"
26
+ say ""
27
+ say "Next steps:", :yellow
28
+ say " 1. Edit config/initializers/resend_robot.rb to set your reply_domain"
29
+ say " 2. Start your dev server (bin/dev) and visit /dev/email"
30
+ say " 3. Use /resend-robot-read and /resend-robot-send in Claude"
31
+ say ""
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Configure Resend Robot (dev-mode Resend API shim).
4
+ # The gem auto-installs shims and routes — only app-specific config goes here.
5
+ #
6
+ # Run `bin/rails generate resend_robot:install` to regenerate this file.
7
+ if defined?(ResendRobot)
8
+ ResendRobot.configure do |config|
9
+ # Domain for inbound email simulation (e.g., "reply.example.com").
10
+ # Required for `resend_robot:reply` and `resend_robot:receive` tasks.
11
+ # config.reply_domain = "reply.example.com"
12
+
13
+ # Path where inbound webhook POSTs are sent (default: "/webhooks/resend")
14
+ # config.webhook_path = "/webhooks/resend"
15
+
16
+ # Port for localhost webhook POST and browser open (auto-detected from
17
+ # action_mailer.default_url_options[:port], fallback: 3000)
18
+ # config.dev_port = 3000
19
+
20
+ # Auto-open emails in the browser when sent (default: true)
21
+ # config.open_in_browser = true
22
+
23
+ # URL path for the web UI (default: "/dev/email")
24
+ # config.mount_path = "/dev/email"
25
+
26
+ # Storage path for JSON email files (default: tmp/resend_robot)
27
+ # config.storage_path = Rails.root.join("tmp/resend_robot")
28
+ end
29
+ end
@@ -0,0 +1,64 @@
1
+ ---
2
+ name: resend-robot-read
3
+ version: 1.0.0
4
+ description: |
5
+ Read dev mailbox. Lists sent emails, shows email details, searches by
6
+ recipient or subject. Uses Resend Robot's local Resend shim.
7
+ allowed-tools:
8
+ - Bash
9
+ - Read
10
+ - Grep
11
+ - Glob
12
+ ---
13
+
14
+ # Read Dev Email
15
+
16
+ You are running the `/resend-robot-read` workflow. Use Resend Robot to inspect emails sent in development.
17
+
18
+ ## Commands
19
+
20
+ ### List recent emails
21
+
22
+ ```bash
23
+ bin/rails resend_robot:outbox
24
+ ```
25
+
26
+ Shows a table of recent outbound emails: ID, time, recipient, subject.
27
+
28
+ ### Show specific email
29
+
30
+ Read the JSON file directly for full details:
31
+
32
+ ```bash
33
+ # Find the file by ID
34
+ ls tmp/resend_robot/outbound/*_rl_XXXX*.json
35
+ ```
36
+
37
+ Then use the Read tool on that file to see full HTML/text body, headers, tags, reply-to address.
38
+
39
+ ### Search emails
40
+
41
+ Use Grep to search `tmp/resend_robot/outbound/` for a recipient email or subject:
42
+
43
+ ```bash
44
+ # Search by recipient
45
+ grep -l "alice@example.com" tmp/resend_robot/outbound/*.json
46
+
47
+ # Search by subject
48
+ grep -l "Welcome" tmp/resend_robot/outbound/*.json
49
+ ```
50
+
51
+ ## Storage
52
+
53
+ - Emails are stored as JSON in `tmp/resend_robot/outbound/`
54
+ - Each file has: id, from, to, cc, reply_to, subject, html, text, tags, headers
55
+ - Files are named `{timestamp}_{id}.json` — newest files sort last alphabetically
56
+ - The dev UI (default: `/dev/email`) shows the same data with HTML preview and reply form
57
+
58
+ ## Rake Tasks Reference
59
+
60
+ | Task | Description |
61
+ |------|-------------|
62
+ | `resend_robot:outbox` | List recent outbound emails (table format) |
63
+ | `resend_robot:show[ID]` | Show full JSON for a specific email |
64
+ | `resend_robot:clear` | Delete all stored emails |
@@ -0,0 +1,77 @@
1
+ ---
2
+ name: resend-robot-send
3
+ version: 1.0.0
4
+ description: |
5
+ Simulate receiving an inbound email in dev. Sends through the app's
6
+ webhook pipeline via Resend Robot's local shim.
7
+ allowed-tools:
8
+ - Bash
9
+ - Read
10
+ - Grep
11
+ - Glob
12
+ ---
13
+
14
+ # Send Dev Email (Simulate Inbound)
15
+
16
+ You are running the `/resend-robot-send` workflow. Use Resend Robot to simulate receiving an inbound email through the app's webhook pipeline.
17
+
18
+ ## Commands
19
+
20
+ ### Reply to most recent outbound email
21
+
22
+ ```bash
23
+ bin/rails "resend_robot:reply[0,Thanks for the email!]"
24
+ ```
25
+
26
+ This finds the most recent outbound email, uses its recipient as `from` and its reply-to address as `to`, then:
27
+ 1. Stores the inbound email JSON
28
+ 2. POSTs to the webhook endpoint (default: `/webhooks/resend`)
29
+ 3. The app's webhook controller processes it through the normal pipeline
30
+
31
+ The first argument is the index (0 = most recent, 1 = second most recent, etc.).
32
+
33
+ ### Reply to a specific outbound email
34
+
35
+ First find the email index:
36
+
37
+ ```bash
38
+ bin/rails resend_robot:outbox
39
+ ```
40
+
41
+ Then reply to it:
42
+
43
+ ```bash
44
+ bin/rails "resend_robot:reply[2,I have a question about this]"
45
+ ```
46
+
47
+ ### Send from a specific address
48
+
49
+ ```bash
50
+ bin/rails "resend_robot:receive[stranger@gmail.com,,Question about something,Is this available?]"
51
+ ```
52
+
53
+ Arguments: `[from, to, subject, body]`
54
+
55
+ - If `to` is empty and `reply_domain` is configured, Resend Robot auto-generates a `cold-{hex}@{reply_domain}` address
56
+ - The `to` address should match your app's inbound email domain for the webhook controller to accept it
57
+
58
+ ## Common Patterns
59
+
60
+ - **Reply to last email:** `resend_robot:reply[0,Thanks! Got it.]`
61
+ - **Cold inbound:** `resend_robot:receive[stranger@gmail.com,,Question,Is this available?]`
62
+ - **Test full cycle:** Send email via app -> `/resend-robot-read` to verify -> `/resend-robot-send` to reply -> check app state
63
+
64
+ ## Important
65
+
66
+ - The dev server must be running (`bin/dev`) for inbound simulation to work
67
+ - All inbound emails go through the REAL app webhook pipeline (same code path as prod)
68
+ - If your app processes inbound emails asynchronously, poll for completion after sending
69
+
70
+ ## Rake Tasks Reference
71
+
72
+ | Task | Description |
73
+ |------|-------------|
74
+ | `resend_robot:reply[INDEX,BODY]` | Reply to the Nth most recent outbound email |
75
+ | `resend_robot:receive[FROM,TO,SUBJECT,BODY]` | Simulate receiving an email from a specific address |
76
+ | `resend_robot:outbox` | List recent outbound emails |
77
+ | `resend_robot:clear` | Delete all stored emails |
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ResendRobot
4
+ class Configuration
5
+ attr_accessor :storage_path, # Pathname — where to store JSON files
6
+ :reply_domain, # String — domain for inbound email simulation (nil disables ensure_reply_domain)
7
+ :webhook_path, # String — path to POST inbound webhooks
8
+ :dev_port, # Integer — port for localhost webhook POST + browser open
9
+ :open_in_browser, # Boolean — auto-open emails in browser
10
+ :mount_path, # String — URL path for the web UI
11
+ :logger # Logger instance
12
+
13
+ def initialize
14
+ @storage_path = nil # resolved lazily via resolved_storage_path
15
+ @reply_domain = nil
16
+ @webhook_path = "/webhooks/resend"
17
+ @dev_port = 3000
18
+ @open_in_browser = true
19
+ @mount_path = "/dev/email"
20
+ @logger = nil # resolved lazily via resolved_logger
21
+ end
22
+
23
+ def resolved_storage_path
24
+ @storage_path || default_storage_path
25
+ end
26
+
27
+ def resolved_logger
28
+ @logger || default_logger
29
+ end
30
+
31
+ private
32
+
33
+ def default_storage_path
34
+ if defined?(Rails) && Rails.root
35
+ Rails.root.join("tmp/resend_robot")
36
+ else
37
+ Pathname.new("tmp/resend_robot")
38
+ end
39
+ end
40
+
41
+ def default_logger
42
+ if defined?(Rails) && Rails.logger
43
+ Rails.logger
44
+ else
45
+ require "logger"
46
+ Logger.new($stdout)
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ResendRobot
4
+ class Engine < ::Rails::Engine
5
+ isolate_namespace ResendRobot
6
+
7
+ # Auto-install shims in development (replaces the old initializer)
8
+ initializer "resend_robot.install_shim" do
9
+ next unless Rails.env.local? && ENV["RESEND_IN_DEV"] != "1"
10
+
11
+ Rails.application.config.after_initialize do
12
+ require "resend_robot/shim"
13
+ Resend.api_key ||= "rl_dev_dummy_key" if defined?(Resend)
14
+ ResendRobot::Shim.install!
15
+ end
16
+ end
17
+
18
+ # Auto-detect dev_port from action_mailer config
19
+ initializer "resend_robot.detect_port" do
20
+ Rails.application.config.after_initialize do
21
+ detected_port = Rails.application.config.action_mailer.default_url_options&.dig(:port)
22
+ ResendRobot.configuration.dev_port = detected_port if detected_port
23
+ end
24
+ end
25
+
26
+ # Auto-inject routes so users don't need a mount line
27
+ initializer "resend_robot.add_routes", after: :add_routing_paths do |app|
28
+ mount_path = ResendRobot.configuration.mount_path
29
+
30
+ app.routes.prepend do
31
+ constraints ->(_) { Rails.env.local? } do
32
+ scope module: :resend_robot do
33
+ get "#{mount_path}", to: "mailbox#index", as: :dev_email_index
34
+ get "#{mount_path}/:id", to: "mailbox#show", as: :dev_email_preview
35
+ post "#{mount_path}/:id/reply", to: "mailbox#reply", as: :dev_email_reply
36
+ delete "#{mount_path}/clear", to: "mailbox#clear", as: :dev_email_clear
37
+ end
38
+ end
39
+ end
40
+ end
41
+
42
+ # Load rake tasks
43
+ rake_tasks do
44
+ load "resend_robot/tasks/resend_robot.rake"
45
+ end
46
+ end
47
+ end