Skip to content

Batch sending api #48

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft

Conversation

sarco3t
Copy link

@sarco3t sarco3t commented Jun 17, 2025

Motivation

Integrate Batch Sending API to support sending multiple emails in a single request, improving efficiency and reducing API calls.

Changes

  • Added send_batch to Mailtrap::Client
  • Implemented batch sending support for all modes (transactional, bulk, sandbox)
  • Added comprehensive test coverage for batch sending functionality
  • Added example file with batch sending scenario

How to test

rspec

or set yout api key

mail = Mailtrap::Mail::Base.new(
  from: { email: 'mailtrap@example.com', name: 'Mailtrap Test' }
)

client = Mailtrap::Client.new(api_key: 'your-api-key')
client.send_batch(mail, [
                    Mailtrap::Mail::Base.new(
                      to: [
                        { email: 'your@email.com', name: 'Your name' }
                      ]
                    ),
                    Mailtrap::Mail::Base.new(
                      to: [
                        { email: 'your2@email.com', name: 'Your name' }
                      ]
                    )
                  ])

# other examples can be found in examples/batch.rb

Summary by CodeRabbit

  • New Features

    • Introduced support for managing email templates, including creating, listing, retrieving, updating, and deleting templates via the API.
    • Added batch email sending functionality, allowing multiple emails to be sent in a single request, with support for attachments and custom metadata.
  • Bug Fixes

    • Improved error handling for API responses, including clearer messages for authorization and validation errors.
  • Documentation

    • Added a changelog entry for version 2.3.1 documenting the new EmailTemplates API.
    • Provided new and updated example scripts demonstrating batch sending and email template management.
  • Tests

    • Added comprehensive tests for email template management and batch sending features, including error cases and API interaction recordings.
    • Enhanced test fixtures to anonymize sensitive data and ensure reliable, repeatable test runs.

Copy link

coderabbitai bot commented Jun 17, 2025

Walkthrough

This update introduces batch email sending support and a new EmailTemplates API to the Mailtrap Ruby client. It adds a send_batch method, unifies HTTP request handling, enhances error management, and implements a new Mailtrap::Template class for managing email templates. Comprehensive example scripts and RSpec tests are included, with supporting VCR fixtures for robust test coverage.

Changes

File(s) Change Summary
CHANGELOG.md Added entry for v2.3.1, documenting EmailTemplates API addition.
examples/batch.rb New example demonstrating batch email sending with attachments and custom metadata.
examples/email_template.rb Expanded to show full lifecycle management of email templates via the API.
lib/mailtrap.rb Added require for 'mailtrap/template' to load template functionality.
lib/mailtrap/client.rb Introduced batch sending (send_batch), generic HTTP methods, unified request logic, improved error handling, and mode checks.
lib/mailtrap/template.rb New module with DTOs and service class for managing email templates (CRUD operations).
spec/mailtrap/client_spec.rb Added and updated tests for batch sending, mode validation, and error handling.
spec/mailtrap/template_spec.rb New comprehensive tests for email template CRUD operations and related classes.
spec/spec_helper.rb Enhanced VCR config to anonymize account IDs in URIs and responses.
spec/fixtures/vcr_cassettes/Mailtrap_Client/_send_batch/... New VCR fixtures for testing batch sending in various modes, including error and template cases.
spec/fixtures/vcr_cassettes/Mailtrap_Template/_create/... New VCR fixtures for testing email template creation, including error and hash-based requests.
spec/fixtures/vcr_cassettes/Mailtrap_Template/_delete/... New VCR fixtures for testing email template deletion, including not-found scenarios.
spec/fixtures/vcr_cassettes/Mailtrap_Template/_get/... New VCR fixtures for testing retrieval of email templates, including not-found scenarios.
spec/fixtures/vcr_cassettes/Mailtrap_Template/_list/... New VCR fixtures for testing listing of email templates, including authorization errors.
spec/fixtures/vcr_cassettes/Mailtrap_Template/_update/... New VCR fixtures for testing template updates, including not-found and hash-based requests.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Client
    participant API

    User->>Client: send_batch(base_email, requests)
    Client->>API: POST /api/batch (base_email + requests)
    API-->>Client: Batch send response (success/failure per email)
    Client-->>User: Return batch send result
Loading
sequenceDiagram
    participant User
    participant TemplateService
    participant API

    User->>TemplateService: create(request)
    TemplateService->>API: POST /email_templates (request)
    API-->>TemplateService: Created template
    TemplateService-->>User: Return EmailTemplate

    User->>TemplateService: list()
    TemplateService->>API: GET /email_templates
    API-->>TemplateService: List of templates
    TemplateService-->>User: Return array of EmailTemplate

    User->>TemplateService: get(template_id)
    TemplateService->>API: GET /email_templates/:id
    API-->>TemplateService: EmailTemplate data
    TemplateService-->>User: Return EmailTemplate

    User->>TemplateService: update(template_id, request)
    TemplateService->>API: PATCH /email_templates/:id (request)
    API-->>TemplateService: Updated template
    TemplateService-->>User: Return EmailTemplate

    User->>TemplateService: delete(template_id)
    TemplateService->>API: DELETE /email_templates/:id
    API-->>TemplateService: 204 No Content
    TemplateService-->>User: Return true
Loading

Possibly related issues

Poem

In burrows deep, I code with glee,
Batch emails sent, as quick as can be!
Templates now dance, CRUD in a row,
With tests and fixtures, watch them grow.
The client’s improved, errors handled with care—
A rabbit’s delight, new features to share!
🐇💌

✨ Finishing Touches
  • 📝 Generate Docstrings

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.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🔭 Outside diff range comments (1)
lib/mailtrap/mail/base.rb (1)

64-66: attachments= silently fails for already-built Attachment objects.

attachments.map { |attachment| Mailtrap::Attachment.new(**attachment) } assumes every element is a Hash.
If callers pass Mailtrap::Attachment instances (quite likely inside send_batch), this raises TypeError.

def attachments=(attachments)
-  @attachments = attachments.map { |attachment| Mailtrap::Attachment.new(**attachment) }
+  @attachments = attachments.map do |attachment|
+    case attachment
+    when Mailtrap::Attachment
+      attachment
+    when Hash
+      Mailtrap::Attachment.new(**attachment)
+    else
+      raise ArgumentError, 'attachments must be Hashes or Mailtrap::Attachment objects'
+    end
+  end
end
♻️ Duplicate comments (7)
spec/fixtures/vcr_cassettes/Mailtrap_Template/_list/maps_response_data_to_EmailTemplate_objects.yml (1)

24-70: Same fixture-size concern as previous cassette

spec/fixtures/vcr_cassettes/Mailtrap_Template/_create/maps_response_data_to_EmailTemplate_object.yml (1)

24-70: Same fixture-size concern as previous cassette

spec/fixtures/vcr_cassettes/Mailtrap_Template/_update/with_hash_request/maps_response_data_to_EmailTemplate_object.yml (1)

24-70: Same fixture-size concern as previous cassette

spec/fixtures/vcr_cassettes/Mailtrap_Template/_get/returns_an_EmailTemplate_object.yml (1)

1-331: Filter out dynamic headers to slim fixtures
As above, this cassette includes many transient headers. Apply the same VCR configuration to remove them for consistency and maintainability.

spec/fixtures/vcr_cassettes/Mailtrap_Template/_create/with_hash_request/returns_an_EmailTemplate_object.yml (1)

1-168: Filter out dynamic headers to slim fixtures
Multiple volatile headers make the fixture overly large. Consolidate header filtering across all cassettes to keep them concise.

spec/fixtures/vcr_cassettes/Mailtrap_Template/_update/with_hash_request/returns_an_EmailTemplate_object.yml (1)

1-333: Filter out dynamic headers to slim fixtures
This cassette also records the same dynamic headers—centralize header filtering in your VCR config instead of duplicating cleanup logic.

spec/fixtures/vcr_cassettes/Mailtrap_Template/_create/with_hash_request/maps_response_data_to_EmailTemplate_object.yml (1)

1-168: Filter out dynamic headers to slim fixtures
Consistent with other fixtures, strip Date, Etag, Cf-Ray, X-Ratelimit-* headers to focus on request/response payloads.

🧹 Nitpick comments (21)
lib/mailtrap/client.rb (1)

60-63: Minor: tighten type guard for requests

Array#all?(Mail::Base) works but is obscure. An explicit block is clearer:

unless requests.all? { |r| r.is_a?(Mail::Base) }
lib/mailtrap/mail/base.rb (2)

40-55: Drop the redundant .compact call after compact_with_empty_arrays.

compact_with_empty_arrays already rejects nil values and empty arrays, so the additional .compact provides no benefit and costs an extra hash traversal.

-                                  }).compact
+                                  })

50-53: Finish the TODO for nested objects.

headers and custom_variables may contain nested structures that themselves need cleaning. A recursive compact_with_empty_arrays (or calling transform_values) would keep the payload minimal and symmetric with attachments serialization.

examples/batch.rb (2)

12-14: Use Base64.strict_encode64 to avoid embedded newlines in attachment content.

Some APIs reject base-64 strings containing line feeds produced by Base64.encode64.

-      content: Base64.encode64('Attachment content'), # base64 encoded content or IO string
+      content: Base64.strict_encode64('Attachment content'),

24-34: Pull sensitive credentials from environment variables in examples.

Hard-coding placeholders encourages copy-paste of real keys into source. Reading from ENV['MAILTRAP_API_KEY'] is safer and showcases best practice.

-client = Mailtrap::Client.new(api_key: 'your-api-key')
+client = Mailtrap::Client.new(api_key: ENV.fetch('MAILTRAP_API_KEY'))
examples/email_template.rb (2)

18-19: Prefer env-driven credentials & avoid hard-coding the API key

Hard-coding any credential (even a placeholder) in example code encourages copy/paste of insecure patterns. Recommend reading it from an env var, e.g. ENV.fetch("MAILTRAP_API_KEY"), so the snippet promotes production-ready practices.

-client = Mailtrap::Client.new(api_key: 'your-api-key')
+client = Mailtrap::Client.new(api_key: ENV.fetch('MAILTRAP_API_KEY'))

22-23: Account id should be configurable

As with the API key, lifting the hard-coded 1_111_111 into an env var (or CLI arg) makes the example immediately usable without edits.

spec/fixtures/vcr_cassettes/Mailtrap_Template/_list/returns_an_array_of_EmailTemplate_objects.yml (1)

24-70: Fixture size can be drastically reduced

The cassette stores every response header (e.g. ~50 lines of CSP) and the full list of 70-plus template objects. Tests usually only need a couple of representative objects and minimal headers. Trimming via VCR before_record filters (or record: :once + selective allow_headers) will:

• shrink repo size
• speed up diff reviews
• avoid brittle tests when the header ordering changes.

Example filter:

VCR.configure do |c|
  c.before_record do |interaction|
    interaction.response.headers.slice!('Content-Type')
    interaction.response.body = interaction.response.body[0, 1_000] # keep first 1 kB
  end
end
spec/mailtrap/client_spec.rb (3)

248-266: Recipient objects omit required fields – test may mask real failures

Each element in recipients only sets to, leaving from, subject, etc. blank.
send_batch presumably merges the base-level data, but the spec never asserts this.
If the implementation changes, these silent assumptions will break production before tests.

Recommendation:
• Assert that every request built for a recipient inherits from, subject, and text from base_mail.
• Alternatively, pass plain recipient hashes and keep one canonical Mail::Base.


268-282: Context wording violates project RuboCop rules

context 'when bulk and sandbox modes are used together' passes, but the next two are 'in bulk mode' and 'in sandbox mode', triggering RSpec/ContextWording offences.

Consider renaming to keep the when / with / without convention, e.g.:

-context 'in bulk mode' do
+context 'when bulk mode is enabled' do

Keeps lint green and documentation consistent.


285-371: Spec examples exceed agreed length – extract shared matcher

Several examples (lines 287-298, 304-315, 333-344, 355-369) breach RSpec/ExampleLength [10/5].
They all duplicate the same “successful batch send” assertions.

Refactor into a shared expectation:

shared_examples 'successful batch response' do
  it 'returns success with message_ids' do
    expect(response).to include(
      success: true,
      responses: array_including(
        hash_including(success: true, message_ids: array_including(kind_of(String)))
      )
    )
  end
end

Then it_behaves_like in each context → shorter, clearer tests and no RuboCop noise.

spec/fixtures/vcr_cassettes/Mailtrap_Template/_update/maps_response_data_to_EmailTemplate_object.yml (1)

1-333: Filter out dynamic headers to slim fixtures
This cassette captures many dynamic headers (Date, Etag, Cf-Ray, X-Ratelimit-*) that bloat the file and rarely add value to tests. Use VCR’s before_record hook to strip or ignore irrelevant headers.

lib/mailtrap/template.rb (2)

51-55: Implicit Mailtrap::Client instantiation may explode in unsuspecting environments.

Calling Template.new(account_id) will instantiate a fresh Mailtrap::Client with ENV['MAILTRAP_API_KEY'].
If the env var is missing the constructor raises, producing a confusing error far from the call-site.
Prefer making client a required argument or raising here with a clearer message.


90-96: Duplicated normalisation logic between create and update.

Extract into a small private helper to keep responsibilities single and remove repetition.

- normalised = request.is_a?(EmailTemplateRequest) ? request : EmailTemplateRequest.new(**request)
+ normalised = normalize_request(request)

…and add

def normalize_request(request)
  request.is_a?(EmailTemplateRequest) ? request : EmailTemplateRequest.new(**request)
end
private :normalize_request
spec/mailtrap/template_spec.rb (3)

3-8: Multiple top-level describe blocks violate RSpec/MultipleDescribes.

Nest the secondary groups (Mailtrap::EmailTemplateRequest, Mailtrap::EmailTemplate) inside a primary describe Mailtrap or split them into separate spec files for clearer organisation and to silence RuboCop.


24-34: Long example can be compacted.

The custom matcher chain inside the authorization-error expectation spans 8 lines.
Consider asserting with expect { list }.to raise_error(Mailtrap::AuthorizationError, /Incorrect API token/) to keep examples concise and comply with RSpec/ExampleLength.


146-163: Repeated remote setup inflates cassette size and spec runtime.

Each context recreates a template via the API. Extract shared setup into a before(:all) (or a fixture cassette) so the create call is made once per cassette instead of once per example group.

spec/fixtures/vcr_cassettes/Mailtrap_Template/_delete/when_template_does_not_exist/raises_not_found_error.yml (2)

1-3: Keep fixtures focused and clear.
This cassette records both a template creation and a failed deletion, but the scenario name implies only delete. Consider splitting the POST creation into its own cassette and isolating the delete-error case for clarity and maintainability.


8-10: Preserve JSON integrity with block scalars.
The request JSON is split across lines, which may introduce unintended whitespace. Switch to a YAML block scalar (|) for the string field to maintain the exact payload.

spec/fixtures/vcr_cassettes/Mailtrap_Template/_update/returns_an_EmailTemplate_object.yml (2)

1-1: Enforce lowercase for cassette filenames.
The filename returns_an_EmailTemplate_object.yml mixes CamelCase. Rename to returns_an_email_template_object.yml to adhere to standard snake_case conventions.


8-10: Avoid inline JSON line wrapping.
The string payload for the POST is wrapped across multiple lines. Use a YAML block scalar to preserve the exact JSON and prevent accidental whitespace or newline injection:

body:
  encoding: UTF-8
  string: |
    {"email_template":{"name":"Original Template",...}}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fee2a44 and 4a2f808.

📒 Files selected for processing (28)
  • CHANGELOG.md (1 hunks)
  • examples/batch.rb (1 hunks)
  • examples/email_template.rb (2 hunks)
  • lib/mailtrap.rb (1 hunks)
  • lib/mailtrap/client.rb (5 hunks)
  • lib/mailtrap/mail/base.rb (2 hunks)
  • lib/mailtrap/template.rb (1 hunks)
  • spec/fixtures/vcr_cassettes/Mailtrap_Template/_create/maps_response_data_to_EmailTemplate_object.yml (1 hunks)
  • spec/fixtures/vcr_cassettes/Mailtrap_Template/_create/returns_an_EmailTemplate_object.yml (1 hunks)
  • spec/fixtures/vcr_cassettes/Mailtrap_Template/_create/when_API_returns_an_error/raises_a_Mailtrap_Error.yml (1 hunks)
  • spec/fixtures/vcr_cassettes/Mailtrap_Template/_create/with_hash_request/maps_response_data_to_EmailTemplate_object.yml (1 hunks)
  • spec/fixtures/vcr_cassettes/Mailtrap_Template/_create/with_hash_request/returns_an_EmailTemplate_object.yml (1 hunks)
  • spec/fixtures/vcr_cassettes/Mailtrap_Template/_delete/returns_true.yml (1 hunks)
  • spec/fixtures/vcr_cassettes/Mailtrap_Template/_delete/when_template_does_not_exist/raises_not_found_error.yml (1 hunks)
  • spec/fixtures/vcr_cassettes/Mailtrap_Template/_get/maps_response_data_to_EmailTemplate_object.yml (1 hunks)
  • spec/fixtures/vcr_cassettes/Mailtrap_Template/_get/returns_an_EmailTemplate_object.yml (1 hunks)
  • spec/fixtures/vcr_cassettes/Mailtrap_Template/_get/when_template_does_not_exist/raises_not_found_error.yml (1 hunks)
  • spec/fixtures/vcr_cassettes/Mailtrap_Template/_list/maps_response_data_to_EmailTemplate_objects.yml (1 hunks)
  • spec/fixtures/vcr_cassettes/Mailtrap_Template/_list/returns_an_array_of_EmailTemplate_objects.yml (1 hunks)
  • spec/fixtures/vcr_cassettes/Mailtrap_Template/_list/when_api_key_is_incorrect/raises_authorization_error.yml (1 hunks)
  • spec/fixtures/vcr_cassettes/Mailtrap_Template/_update/maps_response_data_to_EmailTemplate_object.yml (1 hunks)
  • spec/fixtures/vcr_cassettes/Mailtrap_Template/_update/returns_an_EmailTemplate_object.yml (1 hunks)
  • spec/fixtures/vcr_cassettes/Mailtrap_Template/_update/when_template_does_not_exist/raises_not_found_error.yml (1 hunks)
  • spec/fixtures/vcr_cassettes/Mailtrap_Template/_update/with_hash_request/maps_response_data_to_EmailTemplate_object.yml (1 hunks)
  • spec/fixtures/vcr_cassettes/Mailtrap_Template/_update/with_hash_request/returns_an_EmailTemplate_object.yml (1 hunks)
  • spec/mailtrap/client_spec.rb (2 hunks)
  • spec/mailtrap/template_spec.rb (1 hunks)
  • spec/spec_helper.rb (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (3)
examples/batch.rb (2)
lib/mailtrap/action_mailer/delivery_method.rb (1)
  • client (22-24)
lib/mailtrap/client.rb (1)
  • send_batch (57-75)
lib/mailtrap/template.rb (2)
lib/mailtrap/client.rb (5)
  • initialize (28-51)
  • get (87-90)
  • post (96-99)
  • patch (105-108)
  • delete (113-116)
lib/mailtrap/action_mailer/delivery_method.rb (1)
  • client (22-24)
lib/mailtrap/client.rb (2)
lib/mailtrap/template.rb (2)
  • get (66-69)
  • delete (101-103)
lib/mailtrap/mail/base.rb (1)
  • to_json (57-62)
🪛 RuboCop (1.75.5)
spec/mailtrap/client_spec.rb

[convention] 284-284: Context description should match /^when\b/, /^with\b/, or /^without\b/.

(RSpec/ContextWording)


[convention] 287-298: Example has too many lines. [10/5]

(RSpec/ExampleLength)


[convention] 301-301: Context description should match /^when\b/, /^with\b/, or /^without\b/.

(RSpec/ContextWording)


[convention] 304-315: Example has too many lines. [10/5]

(RSpec/ExampleLength)


[convention] 333-344: Example has too many lines. [10/5]

(RSpec/ExampleLength)


[convention] 355-369: Example has too many lines. [13/5]

(RSpec/ExampleLength)

spec/mailtrap/template_spec.rb

[convention] 3-3: Do not use multiple top-level example groups - try to nest them.

(RSpec/MultipleDescribes)


[convention] 55-62: Example has too many lines. [6/5]

(RSpec/ExampleLength)


[convention] 258-266: Example has too many lines. [7/5]

(RSpec/ExampleLength)


[convention] 283-291: Example has too many lines. [7/5]

(RSpec/ExampleLength)


[convention] 331-343: Example has too many lines. [11/5]

(RSpec/ExampleLength)


[convention] 363-375: Example has too many lines. [11/5]

(RSpec/ExampleLength)

lib/mailtrap/client.rb

[convention] 57-75: Method has too many lines. [14/10]

(Metrics/MethodLength)

🔇 Additional comments (14)
lib/mailtrap/client.rb (1)

65-69: Use HTTPS helper to avoid mismatched scheme/SSL

You build the URI with URI::HTTP.build but later force use_ssl = true.
Simpler & less error-prone:

-      uri = URI::HTTP.build(
+      uri = URI::HTTPS.build(

Same applies to send, get, post, patch, delete.
[ suggest_optional_refactor ]

CHANGELOG.md (1)

1-3: Changelog entry LGTM

lib/mailtrap.rb (1)

7-7: No issues – new require integrates template API

spec/fixtures/vcr_cassettes/Mailtrap_Template/_list/when_api_key_is_incorrect/raises_authorization_error.yml (1)

1-165: Skipping – fixture content is fine.

examples/email_template.rb (1)

19-19: ```shell
#!/bin/bash

Search for all .send( call-sites in Ruby files without specifying type

rg -n '.send(' -g '*.rb' | head


</details>
<details>
<summary>spec/mailtrap/client_spec.rb (1)</summary>

`216-219`: ```shell
#!/bin/bash
set -e
echo "== Showing lines 160-200 from lib/mailtrap/client.rb =="
sed -n '160,200p' lib/mailtrap/client.rb
spec/fixtures/vcr_cassettes/Mailtrap_Template/_update/when_template_does_not_exist/raises_not_found_error.yml (1)

17-21: Sensitive headers look redacted – good job.

Authorization header is masked with <BEARER_TOKEN>. ✅

spec/fixtures/vcr_cassettes/Mailtrap_Template/_create/when_API_returns_an_error/raises_a_Mailtrap_Error.yml (1)

1-164: Skip auto-generated VCR cassette fixture
This YAML file is generated by VCR to record HTTP interactions for error scenarios and does not require manual review.

spec/fixtures/vcr_cassettes/Mailtrap_Template/_create/returns_an_EmailTemplate_object.yml (1)

1-168: Skip auto-generated VCR cassette fixture
This YAML file is generated by VCR to record the successful creation of an EmailTemplate and does not require manual review.

spec/fixtures/vcr_cassettes/Mailtrap_Template/_delete/returns_true.yml (1)

1-321: Skip auto-generated VCR cassette fixture
This YAML file captures delete interactions via VCR and is auto-generated; no manual review is needed.

spec/fixtures/vcr_cassettes/Mailtrap_Template/_get/when_template_does_not_exist/raises_not_found_error.yml (1)

1-327: Skip auto-generated VCR cassette fixture
This YAML file records a not-found scenario and is auto-generated by VCR; no manual review is necessary.

spec/fixtures/vcr_cassettes/Mailtrap_Template/_get/maps_response_data_to_EmailTemplate_object.yml (1)

1-331: Skip auto-generated VCR cassette fixture
This YAML file captures the mapping of response data to an EmailTemplate object and is auto-generated; it does not require manual review.

spec/fixtures/vcr_cassettes/Mailtrap_Template/_delete/when_template_does_not_exist/raises_not_found_error.yml (1)

5-6: Use dynamic placeholders for account IDs.
Hard-coding 1111111 in the URI can leak sensitive data. Ensure spec/spec_helper.rb filters this via config.filter_sensitive_data and replace it with a placeholder like <ACCOUNT_ID> in the fixture.

spec/fixtures/vcr_cassettes/Mailtrap_Template/_update/returns_an_EmailTemplate_object.yml (1)

5-6: Mask sensitive data in fixtures.
Confirm that the account ID (1111111) and token (<BEARER_TOKEN>) are filtered via config.filter_sensitive_data in spec/spec_helper.rb, and consider using <ACCOUNT_ID> placeholders in all URIs.

Comment on lines +17 to +19
interaction.request.uri.gsub!(%r{/accounts/\d+/}, '/accounts/1111111/')
interaction.response.body.gsub!(/"account_id":\d+/, '"account_id": 1111111')

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Avoid mutating a potentially frozen response body

interaction.response.body may be frozen by VCR; gsub! would then raise FrozenError.
Prefer a non-destructive variant or operate on dup:

interaction.response.body = interaction.response.body.gsub(/"account_id":\d+/, '"account_id": 1111111')
🤖 Prompt for AI Agents
In spec/spec_helper.rb around lines 17 to 19, avoid mutating the potentially
frozen interaction.response.body with gsub! as it may raise a FrozenError.
Instead, use the non-destructive gsub method and assign the result back to
interaction.response.body to prevent mutation errors.

Comment on lines 57 to 75
def send_batch(base, requests)
raise ArgumentError, 'base should be Mailtrap::Mail::Base object' unless base.is_a?(Mail::Base)

unless requests.all?(Mail::Base)
raise ArgumentError,
'requests should be an array of Mailtrap::Mail::Base objects'
end

uri = URI::HTTP.build(
host: api_host,
port: api_port,
path: batch_request_url
)

perform_request(:post, uri, {
base:,
requests:
})
end
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix double-encoding of payload sent to /api/batch

body.to_json is called on a hash whose values are Mailtrap::Mail::Base objects.
Because Mail::Base#to_json already returns a JSON string, the resulting payload nests those strings and the API receives invalid JSON.

-      perform_request(:post, uri, {
-                         base:,
-                         requests:
-                       })
+      payload = {
+        base:     base.as_json,
+        requests: requests.map(&:as_json)
+      }
+
+      perform_request(:post, uri, payload)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def send_batch(base, requests)
raise ArgumentError, 'base should be Mailtrap::Mail::Base object' unless base.is_a?(Mail::Base)
unless requests.all?(Mail::Base)
raise ArgumentError,
'requests should be an array of Mailtrap::Mail::Base objects'
end
uri = URI::HTTP.build(
host: api_host,
port: api_port,
path: batch_request_url
)
perform_request(:post, uri, {
base:,
requests:
})
end
def send_batch(base, requests)
raise ArgumentError, 'base should be Mailtrap::Mail::Base object' unless base.is_a?(Mail::Base)
unless requests.all?(Mail::Base)
raise ArgumentError,
'requests should be an array of Mailtrap::Mail::Base objects'
end
uri = URI::HTTP.build(
host: api_host,
port: api_port,
path: batch_request_url
)
payload = {
base: base.as_json,
requests: requests.map(&:as_json)
}
perform_request(:post, uri, payload)
end
🧰 Tools
🪛 RuboCop (1.75.5)

[convention] 57-75: Method has too many lines. [14/10]

(Metrics/MethodLength)

🤖 Prompt for AI Agents
In lib/mailtrap/client.rb around lines 57 to 75, the send_batch method is
double-encoding JSON because it calls to_json on a hash containing
Mailtrap::Mail::Base objects that already return JSON strings. To fix this,
convert each Mail::Base object to a hash or raw data structure before calling
to_json on the entire payload, ensuring the final JSON is correctly structured
without nested JSON strings.

Comment on lines 235 to 237
describe '#send_batch' do
let(:api_key) { '793a32549bc04f67fcd4f800009781f2' }
let(:base_mail) do
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Do not commit real-looking API keys – treat them as secrets

let(:api_key) { '793a32549bc04f67fcd4f800009781f2' } looks like a live key.
Even if it’s only a test token, publishing it normalises unsafe behaviour and forces re-rolls if it is real.

-let(:api_key) { '793a32549bc04f67fcd4f800009781f2' }
+let(:api_key) { ENV.fetch('MAILTRAP_TEST_API_KEY') }

Configure VCR to filter the env value so nothing leaks to the cassette.

[security/PII]

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
describe '#send_batch' do
let(:api_key) { '793a32549bc04f67fcd4f800009781f2' }
let(:base_mail) do
describe '#send_batch' do
- let(:api_key) { '793a32549bc04f67fcd4f800009781f2' }
+ let(:api_key) { ENV.fetch('MAILTRAP_TEST_API_KEY') }
let(:base_mail) do
# ...
end
# ...
end
🤖 Prompt for AI Agents
In spec/mailtrap/client_spec.rb around lines 235 to 237, the API key is
hardcoded as a real-looking string, which risks exposing sensitive information.
Replace the hardcoded API key with a reference to an environment variable or a
test-specific placeholder. Additionally, configure VCR to filter out the API key
from recorded cassettes by masking or replacing it with a placeholder to prevent
leaking secrets in test artifacts.

Comment on lines +17 to +21
- mailtrap-ruby (https://github.com/railsware/mailtrap-ruby)
Authorization:
- Bearer <BEARER_TOKEN>
Content-Type:
- application/json
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Mask sensitive authorization tokens
The Authorization: Bearer <BEARER_TOKEN> header should be filtered via VCR’s filter_sensitive_data to avoid exposing real credentials in fixtures.

🤖 Prompt for AI Agents
In
spec/fixtures/vcr_cassettes/Mailtrap_Template/_update/maps_response_data_to_EmailTemplate_object.yml
around lines 17 to 21, the Authorization header contains a real bearer token
that should be masked. Use VCR's filter_sensitive_data configuration to replace
the actual token with a placeholder like <BEARER_TOKEN> in the recorded fixture
to prevent exposing sensitive credentials.

Comment on lines +45 to +48
class Template
MAX_LENGTH = 255
MAX_BODY_LENGTH = 10_000_000

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

MAX_LENGTH / MAX_BODY_LENGTH are currently dead code.

The constants are declared but never referenced. Either enforce the limits (e.g. in EmailTemplateRequest#initialize) or remove the constants to avoid misleading future maintainers.

🤖 Prompt for AI Agents
In lib/mailtrap/template.rb around lines 45 to 48, the constants MAX_LENGTH and
MAX_BODY_LENGTH are declared but never used, making them dead code. To fix this,
either remove these constants if they are unnecessary or enforce these limits by
adding validation logic in the EmailTemplateRequest#initialize method to check
that input lengths do not exceed these constants, raising errors or handling
violations appropriately.

Comment on lines +75 to +82
def create(request)
normalised = request.is_a?(EmailTemplateRequest) ? request : EmailTemplateRequest.new(**request)
response = @client.post(base_path,
{
email_template: normalised.to_h
})
EmailTemplate.new(response)
end
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

create performs zero validation – required attributes or length limits can slip through.

You already define MAX_LENGTH / MAX_BODY_LENGTH; validate normalised before issuing the HTTP request so that we fail fast locally instead of receiving a 422/400 from the API.

🤖 Prompt for AI Agents
In lib/mailtrap/template.rb around lines 75 to 82, the create method lacks
validation for required attributes and length limits before making the HTTP
request. Add validation logic to check that the normalized EmailTemplateRequest
meets required attribute presence and does not exceed MAX_LENGTH or
MAX_BODY_LENGTH constraints. Raise an error or return early if validation fails
to ensure the method fails fast locally instead of relying on the API to reject
invalid data.

@sarco3t sarco3t marked this pull request as draft June 17, 2025 13:28
@sarco3t sarco3t force-pushed the batch-sending-api branch from 4a2f808 to a90b21c Compare June 17, 2025 13:36
@sarco3t sarco3t force-pushed the batch-sending-api branch from a90b21c to 3c95335 Compare June 17, 2025 13:41
@sarco3t sarco3t marked this pull request as ready for review June 17, 2025 13:41
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (1)
spec/mailtrap/client_spec.rb (1)

235-237: Avoid hard-coding “real-looking” API keys in specs

Previous review already asked to rely on an env var – the new #send_batch
examples re-introduce a literal token.

-let(:api_key) { 'correct-api-key' }
+let(:api_key) { ENV.fetch('MAILTRAP_TEST_API_KEY') }

Remember to mask it in VCR cassettes (config.filter_sensitive_data).

🧹 Nitpick comments (1)
lib/mailtrap/client.rb (1)

60-63: Type check is correct but brittle

requests.all?(Mail::Base) works because Module === obj is kind-of?
But the intent is clearer (and Ruby-version-agnostic) with an explicit block:

-unless requests.all?(Mail::Base)
+unless requests.all? { |r| r.is_a?(Mail::Base) }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4a2f808 and 3c95335.

📒 Files selected for processing (7)
  • examples/batch.rb (1 hunks)
  • lib/mailtrap/client.rb (6 hunks)
  • spec/fixtures/vcr_cassettes/Mailtrap_Client/_send_batch/in_bulk_mode/successfully_sends_a_batch_of_emails.yml (1 hunks)
  • spec/fixtures/vcr_cassettes/Mailtrap_Client/_send_batch/in_sandbox_mode/successfully_sends_a_batch_of_emails.yml (1 hunks)
  • spec/fixtures/vcr_cassettes/Mailtrap_Client/_send_batch/with_API_errors/handles_API_errors.yml (1 hunks)
  • spec/fixtures/vcr_cassettes/Mailtrap_Client/_send_batch/with_template/successfully_sends_a_batch_of_emails_with_template.yml (1 hunks)
  • spec/mailtrap/client_spec.rb (2 hunks)
✅ Files skipped from review due to trivial changes (4)
  • spec/fixtures/vcr_cassettes/Mailtrap_Client/_send_batch/in_sandbox_mode/successfully_sends_a_batch_of_emails.yml
  • spec/fixtures/vcr_cassettes/Mailtrap_Client/_send_batch/with_API_errors/handles_API_errors.yml
  • spec/fixtures/vcr_cassettes/Mailtrap_Client/_send_batch/in_bulk_mode/successfully_sends_a_batch_of_emails.yml
  • spec/fixtures/vcr_cassettes/Mailtrap_Client/_send_batch/with_template/successfully_sends_a_batch_of_emails_with_template.yml
🚧 Files skipped from review as they are similar to previous changes (1)
  • examples/batch.rb
🧰 Additional context used
🧬 Code Graph Analysis (1)
lib/mailtrap/client.rb (4)
lib/mailtrap/mail/base.rb (2)
  • as_json (39-55)
  • to_json (57-62)
lib/mailtrap/mail/from_template.rb (1)
  • as_json (34-41)
lib/mailtrap/attachment.rb (1)
  • as_json (19-27)
lib/mailtrap/template.rb (2)
  • get (66-69)
  • delete (101-103)
🪛 RuboCop (1.75.5)
lib/mailtrap/client.rb

[convention] 57-75: Method has too many lines. [14/10]

(Metrics/MethodLength)

spec/mailtrap/client_spec.rb

[convention] 284-284: Context description should match /^when\b/, /^with\b/, or /^without\b/.

(RSpec/ContextWording)


[convention] 287-298: Example has too many lines. [10/5]

(RSpec/ExampleLength)


[convention] 301-301: Context description should match /^when\b/, /^with\b/, or /^without\b/.

(RSpec/ContextWording)


[convention] 304-315: Example has too many lines. [10/5]

(RSpec/ExampleLength)


[convention] 333-344: Example has too many lines. [10/5]

(RSpec/ExampleLength)


[convention] 355-369: Example has too many lines. [13/5]

(RSpec/ExampleLength)

Comment on lines 216 to 220
it 'handles generic client errors' do
stub_api_send 418, '🫖' do
expect { send_mail }.to raise_error(Mailtrap::Error, 'client error')
expect { send_mail }.to raise_error(Mailtrap::Error, 'client error:, 🫖')
end
end
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Expectation will never match the raised message

handle_response now raises the client-error branch with
raise Mailtrap::Error, ['client error:', response.body] – Ruby turns that
array into the string ["client error:", "🫖"].
The spec expects 'client error:, 🫖', so the example will fail once it is
executed.

-        expect { send_mail }.to raise_error(Mailtrap::Error, 'client error:, 🫖')
+        expect { send_mail }.to raise_error(Mailtrap::Error, '["client error:", "🫖"]')

(Alternatively – and preferably – fix the implementation to raise a clean
string; see client.rb comment.)

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In spec/mailtrap/client_spec.rb around lines 216 to 220, the test expects the
error message to be a single string 'client error:, 🫖', but the implementation
raises an error with an array converted to a string, resulting in '["client
error:", "🫖"]'. To fix this, update the test expectation to match the actual
raised message format or preferably modify the implementation in client.rb to
raise a clean concatenated string error message instead of an array, ensuring
the raised error message matches the test expectation.

Comment on lines +188 to 189
raise Mailtrap::Error, ['client error:', response.body]
when Net::HTTPServerError
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Error message formatting – use a string, not an Array

Raising with an Array produces a cryptic ["client error:", "…"] message and
breaks the spec. Emit a plain string instead.

-      when Net::HTTPClientError
-        raise Mailtrap::Error, ['client error:', response.body]
+      when Net::HTTPClientError
+        raise Mailtrap::Error, "client error: #{response.body}"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
raise Mailtrap::Error, ['client error:', response.body]
when Net::HTTPServerError
when Net::HTTPClientError
raise Mailtrap::Error, "client error: #{response.body}"
when Net::HTTPServerError
🤖 Prompt for AI Agents
In lib/mailtrap/client.rb around lines 188 to 189, the raise statement uses an
Array to format the error message, which results in a cryptic output and breaks
the spec. Change the Array to a single string by concatenating or interpolating
the message and response body so that the error message is a plain, readable
string.

Comment on lines +71 to +74
perform_request(:post, uri, {
base: compact_with_empty_arrays(base.as_json),
requests:
})
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Syntax error & double-encoding in payload construction

requests: is missing a value, which makes the file uncompilable.
Even once a value is supplied, passing raw Mail::Base objects will JSON-encode
twice (object → JSON string, then JSON string re-encoded).

-      perform_request(:post, uri, {
-                        base: compact_with_empty_arrays(base.as_json),
-                        requests:
-                      })
+      payload = {
+        base:     compact_with_empty_arrays(base.as_json),
+        requests: requests.map { |r| compact_with_empty_arrays(r.as_json) }
+      }
+      perform_request(:post, uri, payload)

This produces valid JSON and eliminates the previously reported double-encoding
issue.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
perform_request(:post, uri, {
base: compact_with_empty_arrays(base.as_json),
requests:
})
payload = {
base: compact_with_empty_arrays(base.as_json),
requests: requests.map { |r| compact_with_empty_arrays(r.as_json) }
}
perform_request(:post, uri, payload)
🤖 Prompt for AI Agents
In lib/mailtrap/client.rb around lines 71 to 74, the payload construction has a
syntax error because the `requests:` key is missing a value, causing the code to
be uncompilable. To fix this, assign the correct value to `requests:` by
converting the `Mail::Base` objects to hashes or JSON-compatible structures
before passing them, avoiding double JSON encoding. Ensure that the objects are
serialized only once by using a method like `as_json` or equivalent to produce a
JSON-ready hash instead of raw objects or JSON strings.

@sarco3t sarco3t marked this pull request as draft June 19, 2025 06:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant