Resend Robot
letter_opener for the Resend gem.
Intercepts Resend API calls in development, stores emails as JSON on disk, and provides a web UI to browse, preview, and simulate replies. Production code runs unchanged — same pipeline in dev and prod.
Installation
Add to your Gemfile:
gem "resend_robot", group: :developmentRun bundle install, then run the install generator:
bin/rails generate resend_robot:installThis creates:
-
config/initializers/resend_robot.rb— configuration (reply domain, mount path, etc.) -
.claude/skills/resend-robot-read/skill.md— Claude skill for reading dev emails -
.claude/skills/resend-robot-send/skill.md— Claude skill for simulating inbound emails
The generator is safe to re-run — it will ask before overwriting existing files.
Resend Robot automatically:
- Installs shims that intercept
Resend::Emails.send,Resend::Batch.send, etc. - Sets a dummy API key so
Resend::Mailerdoesn't raise - Injects routes at
/dev/emailfor the web UI - Provides rake tasks for CLI interaction
Web UI
Visit http://localhost:3000/dev/email to browse your dev mailbox:
- Outbox — all intercepted emails, newest first, with search/filter
- Preview — inline HTML rendering with sanitization (no iframes)
- Reply — simulate inbound email through your real webhook pipeline
Configuration
# config/initializers/resend_robot.rb
ResendRobot.configure do |config|
config.reply_domain = "reply.example.com" # required for inbound simulation
config.webhook_path = "/webhooks/resend" # default
config.dev_port = 3000 # auto-detected from action_mailer
config.open_in_browser = true # auto-open emails in browser
config.mount_path = "/dev/email" # change to mount UI elsewhere
endBypass the shim
To send real emails via the Resend API in development:
RESEND_IN_DEV=1 bin/devSuppress auto-open
OPEN_EMAILS=0 bin/devTest Helpers
Resend Robot includes Minitest assertions for verifying emails in your test suite:
class UserSignupTest < ActiveSupport::TestCase
include ResendRobot::TestHelper
test "sends welcome email on signup" do
clear_emails
User.create!(email: "alice@example.com")
assert_email_sent_to "alice@example.com"
assert_email_sent_to "alice@example.com", subject: /Welcome/
assert_equal 1, emails_sent.count
assert_equal "Welcome", last_email[:subject]
end
test "does not send email without signup" do
clear_emails
assert_no_emails_sent
end
endAvailable assertions
| Method | Description |
|---|---|
clear_emails |
Clear all stored emails (call in setup) |
emails_sent |
All outbound emails as symbol-keyed hashes |
last_email |
Most recent outbound email |
assert_email_sent_to(addr, subject: nil) |
Assert email was sent to address, optionally matching subject (String or Regexp) |
assert_no_emails_sent |
Assert no emails were sent |
JSON API
For E2E tests (Playwright, Capybara), the web UI also serves JSON:
GET /dev/email.json → array of all emails
GET /dev/email/:id.json → single email object
Claude Skills
The install generator adds two Claude Code skills:
| Skill | Description |
|---|---|
/resend-robot-read |
List, search, and inspect dev emails from the CLI |
/resend-robot-send |
Simulate inbound emails through your webhook pipeline |
These let Claude interact with your dev mailbox conversationally:
- "Show me the last email sent" → runs
/resend-robot-read - "Simulate a reply to the welcome email" → runs
/resend-robot-send
Skills are plain markdown files in .claude/skills/ — customize them for your app (e.g., add app-specific models or webhook details).
Rake Tasks
bin/rails resend_robot:outbox # List recent emails
bin/rails resend_robot:show[rl_xxx] # Show specific email
bin/rails resend_robot:receive[from,to,subject,body] # Simulate inbound
bin/rails resend_robot:reply[index,body] # Reply to Nth most recent
bin/rails resend_robot:clear # Clear all emailsHow It Works
Resend Robot monkey-patches 5 Resend gem methods in development:
| Method | Shimmed behavior |
|---|---|
Resend::Emails.send |
Stores email as JSON on disk |
Resend::Batch.send |
Stores each email individually |
Resend::Emails::Receiving.get |
Returns stored inbound email |
Resend::Emails::Receiving::Attachments.get |
Returns stub attachment |
Resend::Webhooks.verify |
Returns true (bypasses signature validation) |
Your production code (mailers, webhook controllers, inbound email handlers) runs unchanged. The shim operates at the Resend gem boundary — everything downstream is real.
vs letter_opener
| letter_opener | Resend Robot | |
|---|---|---|
| Works with | SMTP delivery | Resend API client |
| Storage | Rendered HTML files | JSON (preserves all metadata) |
| Inbound simulation | No | Yes — POSTs to your real webhook endpoint |
| Test helpers | No | Yes (assert_email_sent_to, etc.) |
| JSON API | No | Yes (for E2E tests) |
| Web UI | Basic | Search, filter, reply simulation |
Requirements
- Ruby >= 3.1
- Rails >= 7.0
- resend gem >= 1.0
Documentation
- ARCHITECTURE.md — System design, component overview, data flow diagrams
- CONTRIBUTING.md — Dev setup, running tests, project structure
- CHANGELOG.md — Release history
- TODOS.md — Planned future work
License
MIT