0.0
No release in over 3 years
Intercepts Resend gem API calls in development, stores emails as JSON, and provides a web UI to browse, preview, and simulate replies. Production code runs unchanged — same pipeline in dev and prod.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Runtime

>= 7.0
>= 1.0
 Project Readme

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: :development

Run bundle install, then run the install generator:

bin/rails generate resend_robot:install

This 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::Mailer doesn't raise
  • Injects routes at /dev/email for 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
end

Bypass the shim

To send real emails via the Resend API in development:

RESEND_IN_DEV=1 bin/dev

Suppress auto-open

OPEN_EMAILS=0 bin/dev

Test 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
end

Available 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 emails

How 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

License

MIT