Skip to content

add BatchSender with base mail support and request validation #46

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

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

Conversation

Viktorkosandyak
Copy link

@Viktorkosandyak Viktorkosandyak commented Jun 12, 2025

Motivation

This PR introduces support for Mailtrap's Batch Send API, allowing the delivery of multiple personalized emails within a single request.

Instead of extending the existing Mailtrap::Client#send method, we designed a dedicated BatchSender service for better separation of concerns and future flexibility. Batch sending has different structural and behavioral expectations compared to single sends, and keeping these paths isolated avoids conditional complexity and ensures correct API usage (e.g. enforcing bulk.api.mailtrap.io).


Changes

**_Added Mailtrap::BatchSender with full support for base + requests payload structure.

Introduced Mailtrap::Batch::Base to allow reuse of common email attributes across batch requests.

Added EmailValidator to ensure recipient email format validity inside to, cc, and bcc.

Added custom InvalidApiResponseError for strict format validation of batch responses.

Extended Mailtrap::Client with batch_send and a general-purpose request layer (get, post, patch, delete) for future REST API usage.

Improved internal structure of the Client to support both send.api and bulk.api hosts._**


How to test

Run bundle exec rspec spec/mailtrap/batch_sender_spec.rb — full test coverage for happy/invalid batch flows

Manually test with valid API key in examples/batch_sender.rb to verify real delivery via bulk API


Why separate BatchSender instead of reusing send?

We intentionally introduced Mailtrap::BatchSender as a dedicated class instead of extending the existing Client#send method, for the following architectural and maintainability reasons:

1. Single Responsibility Principle (SRP)
Mailtrap::Client is responsible for low-level HTTP transport (get, post, etc.).

BatchSender encapsulates batch-specific responsibilities: payload preparation, request validation, and error handling.

This separation keeps both components focused, isolated, and easier to test or evolve independently.

2. Structural divergence from send
The Batch Send API requires a nested { base, requests } payload, which differs entirely from the single-message JSON used in Client#send.

It also enforces a separate endpoint (/api/batch) and a different host (bulk.api.mailtrap.io).

Merging both into one method would have introduced conditional logic, reduced clarity, and increased complexity.

3. Explicit and predictable SDK behavior
Keeping client.send(mail) for single sends and BatchSender.new(client).send_emails(...) for batch operations provides clear, intentional API usage.

This avoids ambiguity in argument formats (e.g. Hash, Array, or Mail object).

4. Validation flexibility
BatchSender supports strict validation (e.g. email formats, base structure, key filtering), which would be inappropriate to bundle into the generic HTTP client.

5. Easier testing and mocking
The sender class can be fully unit-tested with a mocked Client.

There's no need to involve real HTTP requests or fixture payloads when validating logic.

6. Scalable for future use cases
The same pattern can support future senders (e.g. TemplateSender, DelayedSender, PriorityBatchSender) without rewriting or refactoring Client.

Each sender can build upon Mailtrap::Client while enforcing their own rules and expectations.

7. Backward compatibility
This change doesn't affect existing users of Client#send.
It's a non-breaking enhancement — users can opt into batch functionality only if needed.

🔚 In summary: introducing BatchSender allows us to maintain separation of concerns, strict validation, and predictable SDK behavior — while keeping Client clean, HTTP-focused, and reusable for all other API domains


Summary by CodeRabbit

  • New Features

    • Introduced batch email sending support, allowing users to send multiple emails in a single request.
    • Added a new example script and updated documentation to demonstrate batch sending with attachments, custom headers, and variable substitution.
  • Bug Fixes

    • Improved validation for email addresses and batch request payloads to ensure correct formatting and error handling.
  • Documentation

    • Updated README with a detailed example for batch sending emails using the Ruby client.
  • Tests

    • Added comprehensive tests for batch sending functionality and client error handling.

Summary by CodeRabbit

  • New Features

    • Added support for batch email sending, allowing multiple emails to be sent in a single request.
    • Introduced new attributes for emails: template UUID and template variables, enabling template-based and personalized messages.
  • Documentation

    • Updated the README with a usage example demonstrating batch email sending.
  • Bug Fixes

    • Improved error handling for invalid API responses.
  • Tests

    • Added comprehensive tests for batch sending functionality and related error scenarios.

Copy link

coderabbitai bot commented Jun 12, 2025

Walkthrough

The update introduces batch email sending functionality to the Mailtrap Ruby client. It adds a send_batch method, updates the mail base class to support templates and variables, modularizes HTTP request handling, and documents the new feature in the README. Corresponding tests and error handling for batch operations are also included.

Changes

Files / Grouped Paths Change Summary
README.md Added usage example for batch sending with send_batch and template variables.
lib/mailtrap/client.rb Added send_batch, generic HTTP request helpers, modularized request building, header attachment.
lib/mailtrap/errors.rb Added InvalidApiResponseError for handling unexpected API responses.
lib/mailtrap/mail/base.rb Added template_uuid and template_variables attributes, updated serialization.
spec/mailtrap/client_spec.rb Added tests for send_batch, including success and error scenarios; enabled WebMock for stubbing.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Client
    participant MailtrapAPI

    User->>Client: initialize(bulk: true)
    User->>Client: send_batch(base, requests)
    Client->>Client: Validate inputs
    Client->>MailtrapAPI: POST /api/batch (base, requests)
    MailtrapAPI-->>Client: Batch response
    Client-->>User: Return responses
Loading

Possibly related issues

Poem

In fields of code, a rabbit hops,
Sending emails in clever batches and stops.
With templates and variables, the messages fly—
Through modular code, they reach the sky.
Now with each test and header attached,
Our bulk mail dreams are finally matched!
🐇📧✨

✨ 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: 3

🧹 Nitpick comments (16)
lib/mailtrap/errors.rb (1)

19-21: Clarify doc-string wording & usage contract

The comment says “invalid or unexpected structure” but doesn’t mention response (the subject).
Also, InvalidApiResponseError inherits from Error, which expects messages (an array) during initialization. Make sure every place raising this new error passes an array of messages; otherwise the superclass will NoMethodError on #join.

-  # Raised when the API returns invalid or unexpected structure
+  # Raised when the API returns an invalid or unexpected response structure
lib/mailtrap/validators/email_validator.rb (3)

8-10: Email check is overly permissive

/@/ allows foo@, @bar, or even @. Consider switching to Ruby’s built-in, RFC-5322-aware regex for a low-cost accuracy boost:

-require 'uri'
-
-EMAIL_REGEX = URI::MailTo::EMAIL_REGEXP.freeze
-
-...
-  def valid?(email)
-    email.to_s.match?(EMAIL_REGEX)
-  end
+require 'uri'
+
+EMAIL_REGEX = URI::MailTo::EMAIL_REGEXP.freeze
+
+def valid?(email)
+  email.to_s.match?(EMAIL_REGEX)
+end

This keeps the validator lightweight without pulling extra deps.


15-16: Provide actionable feedback in validation error

Returning just “Invalid email” doesn’t help users pinpoint the problem. Including the offending value greatly improves debuggability:

-raise ArgumentError, "Invalid #{field_name}"
+raise ArgumentError, "Invalid #{field_name}: '#{email}'"

19-19: Missing final newline

Rubocop flagged the absence of a trailing newline. Add one to stay compliant with POSIX editors & linters.

🧰 Tools
🪛 RuboCop (1.75.5)

[convention] 19-19: Final newline missing.

(Layout/TrailingEmptyLines)

README.md (1)

70-100: Example may not match the public API

  1. The sample calls batch.send_emails, but the service is named BatchSender.
    In code, does the method actually exist, or is it #send / #call? Double-check to avoid broken docs.

  2. The client is configured only with api_host: 'bulk.api.mailtrap.io'.
    Everywhere else (tests, specs) you rely on bulk: true to toggle host selection.
    Consider aligning the docs with the canonical approach to avoid user confusion.

spec/mailtrap/client_spec.rb (2)

255-263: Trim example size or use helper

RuboCop flags the spec block as too long (9 lines). Extract the stub & expectations into a helper to silence the cop and keep specs focused:

-    it 'sends payload to /api/batch and returns parsed response' do
-      stub = stub_request(:post, 'https://bulk.api.mailtrap.io/api/batch')
-             .with(body: JSON.dump(payload))
-             .to_return(status: 200, body: JSON.dump({ responses: [{ status: 202 }] }))
-
-      result = client.batch_send(payload)
-      expect(result[:responses]).to eq([{ status: 202 }])
-      expect(stub).to have_been_requested
-    end
+    it 'posts payload and parses response' do
+      responses_body = { responses: [{ status: 202 }] }
+      stub_batch_send(status: 200, body: responses_body)
+
+      expect(client.batch_send(payload)[:responses]).to eq responses_body[:responses]
+    end

(Where stub_batch_send is a small helper defined in the spec.)

🧰 Tools
🪛 RuboCop (1.75.5)

[convention] 255-263: Example has too many lines. [6/5]

(RSpec/ExampleLength)


266-272: Assertion could be tighter

The regex /bad request/ will also match “super bad request”. If you want an exact match:

expect { client.batch_send(payload) }
  .to raise_error(Mailtrap::Error, 'bad request')
examples/batch_sender.rb (1)

46-47: Add a trailing newline

POSIX tools expect files to end with a newline; add one for consistency.

🧰 Tools
🪛 RuboCop (1.75.5)

[convention] 47-47: Final newline missing.

(Layout/TrailingEmptyLines)

lib/mailtrap/batch_sender.rb (2)

54-60: Redundant exception wrapping

Rescuing ArgumentError only to re-raise the same message adds no value and obscures back-traces. You can drop the rescue block entirely.


63-64: Missing final newline

Add a newline at EOF.

🧰 Tools
🪛 RuboCop (1.75.5)

[convention] 64-64: Final newline missing.

(Layout/TrailingEmptyLines)

lib/mailtrap/batch/base.rb (2)

20-22: Potential log noise

warn on every unknown key will flood STDERR in production. Consider using a logger with configurable level or raising for truly unexpected keys.


51-53: Missing final newline

Add a newline at EOF.

🧰 Tools
🪛 RuboCop (1.75.5)

[convention] 53-53: Final newline missing.

(Layout/TrailingEmptyLines)

spec/mailtrap/batch_sender_spec.rb (1)

103-104: Prefer verifying doubles

Using instance_double or class_double ensures the test double matches the public API and prevents silent API drift.

🧰 Tools
🪛 RuboCop (1.75.5)

[convention] 103-103: Prefer using verifying doubles over normal doubles.

(RSpec/VerifiedDoubles)

lib/mailtrap/client.rb (3)

101-105: Set Net::HTTP timeouts

Net::HTTP defaults to unlimited timeouts, risking hung threads on network issues. Configure at least open_timeout and read_timeout (and possibly write_timeout in Ruby 3.0+).

- Net::HTTP.new(api_host, api_port).tap { |client| client.use_ssl = true }
+ Net::HTTP.new(api_host, api_port).tap do |client|
+   client.use_ssl      = true
+   client.open_timeout = 5
+   client.read_timeout = 10
+ end

120-124: Unused uri parameter

perform_request(_uri, request) ignores uri; remove the param or use it (e.g., for logging) to avoid dead code.


152-153: Missing final newline

Add newline at EOF.

🧰 Tools
🪛 RuboCop (1.75.5)

[convention] 153-153: Final newline missing.

(Layout/TrailingEmptyLines)

📜 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 75ba645.

📒 Files selected for processing (10)
  • README.md (1 hunks)
  • examples/batch_sender.rb (1 hunks)
  • lib/mailtrap.rb (1 hunks)
  • lib/mailtrap/batch/base.rb (1 hunks)
  • lib/mailtrap/batch_sender.rb (1 hunks)
  • lib/mailtrap/client.rb (3 hunks)
  • lib/mailtrap/errors.rb (1 hunks)
  • lib/mailtrap/validators/email_validator.rb (1 hunks)
  • spec/mailtrap/batch_sender_spec.rb (1 hunks)
  • spec/mailtrap/client_spec.rb (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
spec/mailtrap/client_spec.rb (1)
lib/mailtrap/client.rb (1)
  • batch_send (55-57)
🪛 RuboCop (1.75.5)
lib/mailtrap/validators/email_validator.rb

[convention] 19-19: Final newline missing.

(Layout/TrailingEmptyLines)

examples/batch_sender.rb

[convention] 47-47: Final newline missing.

(Layout/TrailingEmptyLines)

spec/mailtrap/client_spec.rb

[convention] 255-263: Example has too many lines. [6/5]

(RSpec/ExampleLength)

spec/mailtrap/batch_sender_spec.rb

[convention] 45-54: Example has too many lines. [8/5]

(RSpec/ExampleLength)


[convention] 56-66: Example has too many lines. [7/5]

(RSpec/ExampleLength)


[convention] 76-85: Example has too many lines. [6/5]

(RSpec/ExampleLength)


[convention] 103-103: Prefer using verifying doubles over normal doubles.

(RSpec/VerifiedDoubles)


[convention] 145-145: Final newline missing.

(Layout/TrailingEmptyLines)

lib/mailtrap/batch/base.rb

[convention] 53-53: Final newline missing.

(Layout/TrailingEmptyLines)

lib/mailtrap/batch_sender.rb

[convention] 49-62: Method has too many lines. [11/10]

(Metrics/MethodLength)


[convention] 64-64: Final newline missing.

(Layout/TrailingEmptyLines)

lib/mailtrap/client.rb

[convention] 27-28: Avoid parameter lists longer than 5 parameters. [6/5]

(Metrics/ParameterLists)


[convention] 153-153: Final newline missing.

(Layout/TrailingEmptyLines)

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

4-8: 👍 New requires look correct

All newly-introduced components are now eagerly loaded, preventing NameErrors at runtime. No issues spotted here.

Comment on lines 6 to 11
API_KEY = 'your-real-mailtrap-api-key-here'

client = Mailtrap::Client.new(
api_key: API_KEY,
bulk: true
)
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Do not commit real API keys

Hard-coding API_KEY is dangerous. Load it from an environment variable instead to avoid accidental leakage.

-API_KEY = 'your-real-mailtrap-api-key-here'
+API_KEY = ENV.fetch('MAILTRAP_API_KEY')

[security]

📝 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
API_KEY = 'your-real-mailtrap-api-key-here'
client = Mailtrap::Client.new(
api_key: API_KEY,
bulk: true
)
API_KEY = ENV.fetch('MAILTRAP_API_KEY')
client = Mailtrap::Client.new(
api_key: API_KEY,
bulk: true
)
🤖 Prompt for AI Agents
In examples/batch_sender.rb around lines 6 to 11, the API key is hard-coded
which poses a security risk. Replace the hard-coded API_KEY assignment with code
that loads the API key from an environment variable, such as using
ENV['MAILTRAP_API_KEY']. Ensure the rest of the code uses this variable so the
real key is never stored in the source code.

Comment on lines 11 to 12
base_payload = ensure_hash(base).transform_keys(&:to_sym).except(:to, :cc, :bcc)
validate_requests!(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

Implicit ActiveSupport dependency & key handling

transform_keys / except are ActiveSupport extensions. If the gem is used in a plain Ruby app these calls will raise NoMethodError.
Either:

  1. require 'active_support/core_ext/hash' and declare activesupport as a runtime dependency, or
  2. replace with pure-Ruby equivalents:
-base_payload = ensure_hash(base).transform_keys(&:to_sym).except(:to, :cc, :bcc)
+hash        = ensure_hash(base).each_with_object({}) { |(k,v),h| h[k.to_sym]=v }
+base_payload = hash.reject { |k,_| %i[to cc bcc].include?(k) }

Also consider normalising each request’s keys the same way to accept both string and symbol keys.

📝 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
base_payload = ensure_hash(base).transform_keys(&:to_sym).except(:to, :cc, :bcc)
validate_requests!(requests)
hash = ensure_hash(base).each_with_object({}) { |(k, v), h| h[k.to_sym] = v }
base_payload = hash.reject { |k, _| %i[to cc bcc].include?(k) }
validate_requests!(requests)
🤖 Prompt for AI Agents
In lib/mailtrap/batch_sender.rb around lines 11 to 12, the use of transform_keys
and except relies on ActiveSupport extensions which may cause NoMethodError in
plain Ruby environments. To fix this, either add require
'active_support/core_ext/hash' at the top and declare activesupport as a runtime
dependency, or replace transform_keys(&:to_sym) and except(:to, :cc, :bcc) with
pure Ruby code that converts keys to symbols and excludes those keys manually.
Additionally, normalize each request's keys similarly to handle both string and
symbol keys consistently.

# frozen_string_literal: true

module Mailtrap
class BatchSender
Copy link
Contributor

@i7an i7an Jun 12, 2025

Choose a reason for hiding this comment

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

@mklocek as you are the one who implemented the batch endpoint, I'd appreciate you opinion on the approach with BatchSender?

I think that there is no need for such class and the implementation could live in Mailtrap::Client along with send. Raw but working implementation to demonstrate what I mean:

# @param base [Mailtrap::Base]
# @param requests [Array<Mailtrap::Base>]
# @return [Hash]
def send_batch(base, requests)
  body = {
    base: base.as_json.except('to', 'cc', 'bcc'),
    requests: requests.map(&:as_json)
  }.to_json
  request = post_request(request_url(batch: true), body)
  response = http_client.request(request)

  handle_response(response)
end

To be aligned with the existing implementation, I think its ok to rely on our API to validate the input as we already instead of writing validations in ruby. That would make it easier later to change the limits without the need to update all the libraries.

# 501 message
client.send_batch(base, requests)
# => lib/mailtrap/client.rb:109:in `handle_response': a maximum of 500 requests per batch is allowed (Mailtrap::Error)

# large attachment
client.send_batch(base, requests)
# => lib/mailtrap/client.rb:115:in `handle_response': message too large (Mailtrap::MailSizeError)

Copy link
Author

Choose a reason for hiding this comment

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

@i7an Refactored everything according to your feedback — moved the batch logic into the main client. Please check if everything looks good now

@mklocek mklocek self-requested a review June 12, 2025 19:09
Comment on lines 46 to 56
request = post_request(request_url, mail.to_json)
response = http_client.request(request)

response = http_client(cache: true).request(request)
handle_response(response)
end

def post(path, body: {})
request(:post, path, body:)
end

def batch_send(payload)
post('/api/batch', body: payload)
Copy link
Contributor

Choose a reason for hiding this comment

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

We don't need 2 different ways to send a basic HTTP POST request. def post probably shouldn't be a public method as well.

Copy link
Author

Choose a reason for hiding this comment

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

@mklocek Hello,

Знімок екрана 2025-06-14 о 4 51 53 пп

That’s a fair point — but I’ve introduced post as part of a general-purpose HTTP layer on the client.

We’ll be adding support for other REST APIs like Contacts and Templates:

So having get / post / patch / delete methods on Client helps unify and scale all of that going forward.

Happy to revisit this if you still feel it’s unnecessary — let me know what you think!

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

🧹 Nitpick comments (3)
lib/mailtrap/client.rb (2)

27-33: Initializer is already at 6 kwargs – extract config object.

The constructor keeps growing (api_key, api_host, api_port, bulk, sandbox, inbox_id).
Before it turns into the next Net::HTTP.start, consider a small config struct / options hash that can be validated in one place:

Config = Struct.new(:api_key, :api_host, :api_port, :bulk, :sandbox, :inbox_id, keyword_init: true)

This keeps the public surface stable while allowing future flags (contacts, templates, etc.) without blowing up the signature.

🧰 Tools
🪛 RuboCop (1.75.5)

[convention] 27-28: Avoid parameter lists longer than 5 parameters. [6/5]

(Metrics/ParameterLists)


111-115: Cached HTTP client is great – but perform_request bypasses it.

perform_request instantiates a new client (cache: false) every call, whereas send caches and reuses the connection. Decide on one strategy (preferably cached with automatic connection-reuse) to avoid TLS setup overhead.

spec/mailtrap/client_spec.rb (1)

235-303: Spec example is verbose & contains trailing whitespace.

The happy-path batch spec is ~40 lines long; RuboCop already flags this.
Extract common setup into let blocks / helper methods and tighten expectations to the essentials to keep the suite readable. Also strip trailing spaces to keep the diff clean.

🧰 Tools
🪛 RuboCop (1.75.5)

[convention] 237-237: Trailing whitespace detected.

(Layout/TrailingWhitespace)


[convention] 246-246: Trailing whitespace detected.

(Layout/TrailingWhitespace)


[convention] 259-259: Trailing whitespace detected.

(Layout/TrailingWhitespace)


[convention] 260-279: Example has too many lines. [16/5]

(RSpec/ExampleLength)


[convention] 272-272: Trailing whitespace detected.

(Layout/TrailingWhitespace)


[convention] 274-274: Trailing whitespace detected.

(Layout/TrailingWhitespace)


[convention] 286-286: Trailing whitespace detected.

(Layout/TrailingWhitespace)


[convention] 292-292: Trailing whitespace detected.

(Layout/TrailingWhitespace)


[convention] 296-296: Line is too long. [128/120]

(Layout/LineLength)


[convention] 297-297: Trailing whitespace detected.

(Layout/TrailingWhitespace)


[convention] 301-301: Trailing whitespace detected.

(Layout/TrailingWhitespace)


[convention] 303-303: Trailing whitespace detected.

(Layout/TrailingWhitespace)

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 75ba645 and 39a2a68.

📒 Files selected for processing (4)
  • README.md (1 hunks)
  • lib/mailtrap/client.rb (3 hunks)
  • lib/mailtrap/mail/base.rb (4 hunks)
  • spec/mailtrap/client_spec.rb (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • README.md
🧰 Additional context used
🧬 Code Graph Analysis (1)
spec/mailtrap/client_spec.rb (2)
lib/mailtrap/mail/base.rb (1)
  • to_json (67-69)
lib/mailtrap/client.rb (1)
  • send_batch (51-63)
🪛 RuboCop (1.75.5)
lib/mailtrap/client.rb

[convention] 27-28: Avoid parameter lists longer than 5 parameters. [6/5]

(Metrics/ParameterLists)


[convention] 53-53: Add empty line after guard clause.

(Layout/EmptyLineAfterGuardClause)


[convention] 58-58: Trailing whitespace detected.

(Layout/TrailingWhitespace)


[convention] 61-61: Trailing whitespace detected.

(Layout/TrailingWhitespace)


[convention] 163-163: Final newline missing.

(Layout/TrailingEmptyLines)

spec/mailtrap/client_spec.rb

[convention] 2-2: Add an empty line after magic comments.

(Layout/EmptyLineAfterMagicComment)


[convention] 237-237: Trailing whitespace detected.

(Layout/TrailingWhitespace)


[convention] 246-246: Trailing whitespace detected.

(Layout/TrailingWhitespace)


[convention] 259-259: Trailing whitespace detected.

(Layout/TrailingWhitespace)


[convention] 260-279: Example has too many lines. [16/5]

(RSpec/ExampleLength)


[convention] 272-272: Trailing whitespace detected.

(Layout/TrailingWhitespace)


[convention] 274-274: Trailing whitespace detected.

(Layout/TrailingWhitespace)


[convention] 286-286: Trailing whitespace detected.

(Layout/TrailingWhitespace)


[convention] 292-292: Trailing whitespace detected.

(Layout/TrailingWhitespace)


[convention] 296-296: Line is too long. [128/120]

(Layout/LineLength)


[convention] 297-297: Trailing whitespace detected.

(Layout/TrailingWhitespace)


[convention] 301-301: Trailing whitespace detected.

(Layout/TrailingWhitespace)


[convention] 303-303: Trailing whitespace detected.

(Layout/TrailingWhitespace)

🔇 Additional comments (1)
lib/mailtrap/mail/base.rb (1)

61-64: as_json now leaks nil values when serialised.

JSON.generate will serialise nil to null, so a missing template field becomes "template_uuid":null.
If the API treats “null” differently from “absent”, switch to:

-'template_uuid' => template_uuid,
-'template_variables' => template_variables
+'template_uuid'     => template_uuid,
+'template_variables' => template_variables

… and keep the existing .compact – but add:

.compact # removes nils so keys disappear entirely

to guarantee the keys are omitted rather than set to null.

Comment on lines +8 to +12
attr_accessor :from, :to, :reply_to, :cc, :bcc,
:headers, :custom_variables, :subject,
:text, :html, :category,
:template_uuid, :template_variables

Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider validating template_uuid & template_variables.

The new attributes are simply accepted and forwarded. A couple of edge-cases worth guarding against:

  1. template_uuid – enforce a UUID format to fail fast on obviously bad data.
  2. template_variables – ensure it’s a Hash (or at least JSON-serialisable) to avoid runtime errors when the body is serialised.

A light touch validation in the constructor (or a dedicated validator similar to EmailValidator) would make the API more predictable.

🤖 Prompt for AI Agents
In lib/mailtrap/mail/base.rb around lines 8 to 12, the new attributes
template_uuid and template_variables lack validation, which can lead to runtime
errors or bad data usage. Add validation in the constructor or a dedicated
validator method to ensure template_uuid matches a UUID format and
template_variables is a Hash or JSON-serializable object. This will enforce
correct data types early and prevent issues during serialization or API calls.

Comment on lines +65 to +78
def post(path, body: {})
request(:post, path, body: body)
end

def request(method, path, body: nil, params: nil)
uri = URI::HTTPS.build(
host: api_host,
path: path,
query: params ? URI.encode_www_form(params) : nil
)

request = build_request(method, uri, body)
perform_request(uri, request)
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

#request duplicates logic already present in send/send_batch.

Both high-level methods manually build a post_request and call http_client.request.
Now that a general-purpose request layer exists, thin those methods down to:

request(:post, '/api/batch', body: body)

This reduces code paths and ensures all requests go through the same header / error-handling pipeline.

🤖 Prompt for AI Agents
In lib/mailtrap/client.rb around lines 65 to 78, the request method duplicates
logic already implemented in send and send_batch methods, which manually build
post_request and call http_client.request. Refactor send and send_batch to use
the general-purpose request method by replacing their internal logic with calls
to request(:post, path, body: body). This will consolidate request handling,
reduce code duplication, and ensure consistent header and error handling across
all requests.

Comment on lines +51 to 63
def send_batch(base, requests)
raise ArgumentError, 'hould be Mailtrap::Mail::Base object' unless base.is_a?(Mailtrap::Mail::Base)
raise ArgumentError, 'requests must be present' if !requests.is_a?(Array) || requests.empty?
body = {
base: base.as_json.except('to', 'cc', 'bcc'),
requests: requests
}.to_json

request = post_request('/api/batch', body)
response = http_client.request(request)

handle_response(response)
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

Guard-clause typo & missing bulk-mode enforcement.

  1. Typo in the error message:
-raise ArgumentError, 'hould be Mailtrap::Mail::Base object'
+raise ArgumentError, 'should be Mailtrap::Mail::Base object'
  1. send_batch can be called on a client initialised without bulk: true; in that case the host defaults to send.api.mailtrap.io, and the request will 404. Fail fast:
+unless bulk || api_host == BULK_SENDING_API_HOST
+  raise ArgumentError, 'send_batch requires the client to be initialised with bulk: true'
+end
  1. RuboCop flagged the missing blank line after the guard clause – worth fixing while you’re here.
📝 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, 'hould be Mailtrap::Mail::Base object' unless base.is_a?(Mailtrap::Mail::Base)
raise ArgumentError, 'requests must be present' if !requests.is_a?(Array) || requests.empty?
body = {
base: base.as_json.except('to', 'cc', 'bcc'),
requests: requests
}.to_json
request = post_request('/api/batch', body)
response = http_client.request(request)
handle_response(response)
end
def send_batch(base, requests)
raise ArgumentError, 'should be Mailtrap::Mail::Base object' unless base.is_a?(Mailtrap::Mail::Base)
unless bulk || api_host == BULK_SENDING_API_HOST
raise ArgumentError, 'send_batch requires the client to be initialised with bulk: true'
end
raise ArgumentError, 'requests must be present' if !requests.is_a?(Array) || requests.empty?
body = {
base: base.as_json.except('to', 'cc', 'bcc'),
requests: requests
}.to_json
request = post_request('/api/batch', body)
response = http_client.request(request)
handle_response(response)
end
🧰 Tools
🪛 RuboCop (1.75.5)

[convention] 53-53: Add empty line after guard clause.

(Layout/EmptyLineAfterGuardClause)


[convention] 58-58: Trailing whitespace detected.

(Layout/TrailingWhitespace)


[convention] 61-61: Trailing whitespace detected.

(Layout/TrailingWhitespace)

🤖 Prompt for AI Agents
In lib/mailtrap/client.rb around lines 51 to 63, fix the typo in the first
ArgumentError message by correcting 'hould' to 'should'. Add a guard clause at
the start of send_batch to check if the client was initialized with bulk mode
enabled, and raise an error if not, to prevent 404 errors from the wrong host.
Also, insert a blank line after the guard clauses to satisfy RuboCop style
guidelines.

Comment on lines +90 to +93
request.body = JSON.dump(body) if %i[post patch].include?(method.to_sym) && body

attach_headers(request)
request
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Double JSON encoding risk in build_request.

post('/foo', body: json_string) will still run JSON.dump, producing a doubly-quoted string.
Safeguard:

-if %i[post patch].include?(method.to_sym) && body
-  request.body = JSON.dump(body)
+if %i[post patch].include?(method.to_sym) && body
+  request.body = body.is_a?(String) ? body : JSON.generate(body)
 end

This keeps behaviour consistent whether the caller passes a Hash or a pre-encoded String.

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

🤖 Prompt for AI Agents
In lib/mailtrap/client.rb around lines 90 to 93, the method build_request risks
double JSON encoding when the body is already a JSON string. To fix this, check
if the body is a String before calling JSON.dump; only encode if the body is not
a String (e.g., a Hash). This ensures consistent behavior whether the caller
passes a Hash or a pre-encoded JSON string.

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.

3 participants