Add Inbound Email v2 API support#117
Conversation
EmailLogMessage gains rfc_message_id, in_reply_to, references, and thread_id. SendingDomain gains inbound_enabled and inbound_verified. Specs and VCR cassettes for both endpoints updated to cover the new fields. Cassette bodies were hand-edited rather than re-recorded, to avoid churning live resources and to keep the curated email-log fixture stable.
Struct DTOs for folders, inboxes, messages (with typed attachments), threads (with typed thread messages), the paginated list responses, and the reply/forward send result. Matches the OpenAPI spec.
InboundFoldersAPI (CRUD), InboundInboxesAPI (folder-scoped CRUD), InboundMessagesAPI (list/get/delete + reply/reply_all/forward) and InboundThreadsAPI (list/get/delete), wired into lib/mailtrap.rb. Inbound is token-scoped, so these override initialize to take just a client (no account_id). Nested attachments and thread messages are built into typed structs, mirroring EmailLogsAPI's event handling.
Offline DTO specs for all inbound structs, and VCR-based API specs for folders, inboxes, messages and threads (happy paths plus ArgumentError and Mailtrap::Error cases). Cassettes to be recorded against the live API.
|
Warning Review limit reached
Next review available in: 48 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (37)
📝 WalkthroughWalkthroughAdded inbound email support to the Ruby client through folder, inbox, message, and thread APIs. The change includes typed DTOs, CRUD and messaging operations, cursor pagination, attachment mapping, examples, compatibility fields, VCR fixtures, and RSpec coverage. ChangesInbound email functionality
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant Client
participant InboundThreadsAPI
participant MailtrapAPI
Client->>InboundThreadsAPI: list or get inbox threads
InboundThreadsAPI->>MailtrapAPI: GET threads endpoint with optional last_id
MailtrapAPI-->>InboundThreadsAPI: thread payload with nested messages
InboundThreadsAPI-->>Client: InboundThreadsListResponse or InboundThread
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (1)
spec/mailtrap/inbound_thread_message_spec.rb (1)
16-25: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winExpand DTO tests to cover the complete public contracts.
spec/mailtrap/inbound_thread_message_spec.rb#L16-L25: assert RFC/threading headers, addresses, bodies, attachments, andbounced_at.spec/mailtrap/inbound_thread_spec.rb#L20-L32: assertlast_received_at,last_sent_at, andlast_message_id.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@spec/mailtrap/inbound_thread_message_spec.rb` around lines 16 - 25, Expand the DTO expectations in spec/mailtrap/inbound_thread_message_spec.rb lines 16-25 to cover RFC/threading headers, addresses, bodies, attachments, and bounced_at in addition to the existing attributes. Also update spec/mailtrap/inbound_thread_spec.rb lines 20-32 to assert last_received_at, last_sent_at, and last_message_id, using the fixture values established by each DTO.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@examples/inbound_messages_api.rb`:
- Around line 14-15: Update the message retrieval flow around messages.get to
check whether page.data contains a first message before accessing its id; handle
an empty inbox gracefully and only perform the remaining message operations when
a message is available.
In `@examples/inbound_threads_api.rb`:
- Around line 14-19: Guard the example calls to threads.get and threads.delete
by checking that page.data contains a first thread before dereferencing
page.data.first.id. Preserve the existing get/delete behavior when the page is
non-empty, and skip both operations for an empty page.
In
`@spec/fixtures/vcr_cassettes/Mailtrap_InboundInboxesAPI/_list/maps_response_data_to_InboundInbox_objects.yml`:
- Line 71: Replace the recorded `@inbound-mailtrap.io` addresses with reserved
synthetic/local addresses in both fixture entries:
spec/fixtures/vcr_cassettes/Mailtrap_InboundInboxesAPI/_list/maps_response_data_to_InboundInbox_objects.yml:71
and
spec/fixtures/vcr_cassettes/Mailtrap_InboundInboxesAPI/_update/maps_response_data_to_an_InboundInbox_object.yml:71.
Keep the fixture response structure and inbox fields unchanged.
In
`@spec/fixtures/vcr_cassettes/Mailtrap_InboundMessagesAPI/_get/maps_response_data_to_an_InboundMessage_with_attachments.yml`:
- Around line 73-80: Sanitize the cassette response data by replacing the S3
presigned raw_message_url and raw_message_expires_at with deterministic redacted
values, and replace opaque unsubscribe and open-tracking URLs in html_body and
text_body with fixed placeholders. Remove the exposed credentials, signatures,
tokens, and PII from repository history, and invalidate the existing capture if
it remains usable.
In
`@spec/fixtures/vcr_cassettes/Mailtrap_InboundThreadsAPI/_get/maps_response_data_to_an_InboundThread_with_messages.yml`:
- Around line 72-79: Across the three listed fixture files and their indicated
ranges, address the repeated tickets-1a2b3c4d@inbound-mailtrap.io identity:
either document and test that it is intentional shared test setup, or replace it
consistently in every affected VCR JSON body with a reserved synthetic mailbox
address, preserving the fixture data structure and references.
---
Nitpick comments:
In `@spec/mailtrap/inbound_thread_message_spec.rb`:
- Around line 16-25: Expand the DTO expectations in
spec/mailtrap/inbound_thread_message_spec.rb lines 16-25 to cover RFC/threading
headers, addresses, bodies, attachments, and bounced_at in addition to the
existing attributes. Also update spec/mailtrap/inbound_thread_spec.rb lines
20-32 to assert last_received_at, last_sent_at, and last_message_id, using the
fixture values established by each DTO.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 4ab09e27-0db0-4c1a-bd3e-24e1faa00513
📒 Files selected for processing (68)
README.mdexamples/inbound_folders_api.rbexamples/inbound_inboxes_api.rbexamples/inbound_messages_api.rbexamples/inbound_threads_api.rblib/mailtrap.rblib/mailtrap/email_log_message.rblib/mailtrap/inbound_attachment.rblib/mailtrap/inbound_folder.rblib/mailtrap/inbound_folders_api.rblib/mailtrap/inbound_inbox.rblib/mailtrap/inbound_inboxes_api.rblib/mailtrap/inbound_message.rblib/mailtrap/inbound_messages_api.rblib/mailtrap/inbound_messages_list_response.rblib/mailtrap/inbound_send_result.rblib/mailtrap/inbound_thread.rblib/mailtrap/inbound_thread_message.rblib/mailtrap/inbound_threads_api.rblib/mailtrap/inbound_threads_list_response.rblib/mailtrap/sending_domain.rbspec/fixtures/vcr_cassettes/Mailtrap_EmailLogsAPI/_get/maps_response_data_to_EmailLogMessage_with_events_and_raw_message_url.ymlspec/fixtures/vcr_cassettes/Mailtrap_EmailLogsAPI/_list/maps_response_data_to_EmailLogsListResponse_with_messages.ymlspec/fixtures/vcr_cassettes/Mailtrap_InboundFoldersAPI/_create/maps_response_data_to_an_InboundFolder_object.ymlspec/fixtures/vcr_cassettes/Mailtrap_InboundFoldersAPI/_delete/returns_nil.ymlspec/fixtures/vcr_cassettes/Mailtrap_InboundFoldersAPI/_delete/when_the_folder_does_not_exist/raises_an_error.ymlspec/fixtures/vcr_cassettes/Mailtrap_InboundFoldersAPI/_get/maps_response_data_to_an_InboundFolder_object.ymlspec/fixtures/vcr_cassettes/Mailtrap_InboundFoldersAPI/_get/when_the_folder_does_not_exist/raises_an_error.ymlspec/fixtures/vcr_cassettes/Mailtrap_InboundFoldersAPI/_list/maps_response_data_to_InboundFolder_objects.ymlspec/fixtures/vcr_cassettes/Mailtrap_InboundFoldersAPI/_update/maps_response_data_to_an_InboundFolder_object.ymlspec/fixtures/vcr_cassettes/Mailtrap_InboundInboxesAPI/_create/maps_response_data_to_an_InboundInbox_object.ymlspec/fixtures/vcr_cassettes/Mailtrap_InboundInboxesAPI/_delete/returns_nil.ymlspec/fixtures/vcr_cassettes/Mailtrap_InboundInboxesAPI/_delete/when_the_inbox_does_not_exist/raises_an_error.ymlspec/fixtures/vcr_cassettes/Mailtrap_InboundInboxesAPI/_get/maps_response_data_to_an_InboundInbox_object.ymlspec/fixtures/vcr_cassettes/Mailtrap_InboundInboxesAPI/_get/when_the_inbox_does_not_exist/raises_an_error.ymlspec/fixtures/vcr_cassettes/Mailtrap_InboundInboxesAPI/_list/maps_response_data_to_InboundInbox_objects.ymlspec/fixtures/vcr_cassettes/Mailtrap_InboundInboxesAPI/_update/maps_response_data_to_an_InboundInbox_object.ymlspec/fixtures/vcr_cassettes/Mailtrap_InboundMessagesAPI/_delete/returns_nil.ymlspec/fixtures/vcr_cassettes/Mailtrap_InboundMessagesAPI/_forward/maps_response_data_to_an_InboundSendResult.ymlspec/fixtures/vcr_cassettes/Mailtrap_InboundMessagesAPI/_get/maps_response_data_to_an_InboundMessage_with_attachments.ymlspec/fixtures/vcr_cassettes/Mailtrap_InboundMessagesAPI/_get/when_the_message_does_not_exist/raises_an_error.ymlspec/fixtures/vcr_cassettes/Mailtrap_InboundMessagesAPI/_list/maps_response_data_to_an_InboundMessagesListResponse.ymlspec/fixtures/vcr_cassettes/Mailtrap_InboundMessagesAPI/_list/with_pagination_cursor/passes_last_id_and_returns_a_page.ymlspec/fixtures/vcr_cassettes/Mailtrap_InboundMessagesAPI/_reply/maps_response_data_to_an_InboundSendResult.ymlspec/fixtures/vcr_cassettes/Mailtrap_InboundMessagesAPI/_reply_all/maps_response_data_to_an_InboundSendResult.ymlspec/fixtures/vcr_cassettes/Mailtrap_InboundThreadsAPI/_delete/returns_nil.ymlspec/fixtures/vcr_cassettes/Mailtrap_InboundThreadsAPI/_delete/when_the_thread_does_not_exist/raises_an_error.ymlspec/fixtures/vcr_cassettes/Mailtrap_InboundThreadsAPI/_get/maps_response_data_to_an_InboundThread_with_messages.ymlspec/fixtures/vcr_cassettes/Mailtrap_InboundThreadsAPI/_get/when_the_thread_does_not_exist/raises_an_error.ymlspec/fixtures/vcr_cassettes/Mailtrap_InboundThreadsAPI/_list/maps_response_data_to_an_InboundThreadsListResponse.ymlspec/fixtures/vcr_cassettes/Mailtrap_InboundThreadsAPI/_list/with_pagination_cursor/passes_last_id_and_returns_a_page.ymlspec/fixtures/vcr_cassettes/Mailtrap_SendingDomainsAPI/_create/maps_response_data_to_sending_domain_object.ymlspec/fixtures/vcr_cassettes/Mailtrap_SendingDomainsAPI/_get/maps_response_data_to_SendingDomain_object.ymlspec/mailtrap/email_logs_api_spec.rbspec/mailtrap/inbound_attachment_spec.rbspec/mailtrap/inbound_folder_spec.rbspec/mailtrap/inbound_folders_api_spec.rbspec/mailtrap/inbound_inbox_spec.rbspec/mailtrap/inbound_inboxes_api_spec.rbspec/mailtrap/inbound_message_spec.rbspec/mailtrap/inbound_messages_api_spec.rbspec/mailtrap/inbound_messages_list_response_spec.rbspec/mailtrap/inbound_send_result_spec.rbspec/mailtrap/inbound_thread_message_spec.rbspec/mailtrap/inbound_thread_spec.rbspec/mailtrap/inbound_threads_api_spec.rbspec/mailtrap/inbound_threads_list_response_spec.rbspec/mailtrap/sending_domains_api_spec.rb
| # @attr_reader download_url [String, nil] Signed URL to download the attachment (only when fetched by ID) | ||
| # @attr_reader download_url_expires_at [String, nil] When the download URL expires (only when fetched by ID) | ||
| # rubocop:disable Lint/StructNewOverride -- +size+ is an API field that shadows Struct#size | ||
| InboundAttachment = Struct.new( |
There was a problem hiding this comment.
BTW i wonder if it would make more sense to use Data
... Data doesn't even have size :)
There was a problem hiding this comment.
Good thought — Data would be a nice fit semantically and would drop the Lint/StructNewOverride disable. The snag is construction: we build these from partial API responses via .new(**hash.slice(*members)), and several fields (download_url, download_url_expires_at, …) are only present when a message is fetched by ID. Struct with keyword_init: true nil-defaults the missing keys for free, whereas Data requires every member and would need a custom initialize defaulting all eight — a bit more boilerplate than the one-line disable. And the rest of the SDK is uniformly Struct, so I lean toward keeping it consistent here. Happy to revisit a repo-wide Struct → Data migration separately if we want it.
Cassettes recorded against the live API for folders, inboxes, messages and threads, with the spec let IDs pointed at real recorded resources. Response bodies anonymised: the sending domain is replaced with example.com and the live inbox addresses with non-real slugs; the public inbound-mailtrap.io domain and resource IDs are kept, consistent with the existing cassettes.
Per-resource example scripts for inbound folders, inboxes, messages (incl. reply/reply_all/forward) and threads, linked from a new Inbound Email section in the README.
Motivation
Add SDK support for the Inbound Email v2 API, and surface the inbound-related additions on existing endpoints. Mirrors the OpenAPI spec.
Changes
account_idrequired):InboundFoldersAPI— list, get, create, update, deleteInboundInboxesAPI(folder-scoped) — list, get, create (hosted & custom-domain), update, deleteInboundMessagesAPI(inbox-scoped) — list (cursor-paginated), get, delete,reply,reply_all,forwardInboundThreadsAPI(inbox-scoped) — list (cursor-paginated), get, deleteEmailLogMessagegainsrfc_message_id,in_reply_to,references,thread_id.SendingDomaingainsinbound_enabled,inbound_verified.Split into reviewable commits: existing-endpoint fields, DTOs, API classes, specs, cassettes, examples + README.