Skip to content

Conversation

i7an
Copy link
Contributor

@i7an i7an commented Sep 24, 2025

Re-created #52

Motivation

Support new functionality (Suppressions API)
https://help.mailtrap.io/article/95-suppressions-list

Changes

  • Added new Mailtrap::SuppressionsAPI entity for interactions with Suppressions API
    • delete
    • list
  • query params handling in Mailtrap::Client#get
  • Mailtrap::Suppression DTO
  • Added new tests
  • Added examples

How to test

rspec

or set yout api key and account id

suppressions = Mailtrap::SuppressionsAPI.new(ENV.fetch('MAILTRAP_ACCOUNT_ID', 1_111_111), client)

# List all suppressions
puts 'Listing all suppressions...'
list = suppressions.list
puts list

# delete suppression
suppressions.delete('123e4567-e89b-12d3-a456-426614174000')

Summary by CodeRabbit

  • New Features
    • Added Suppressions API: list account suppressions, filter by email, and delete entries.
    • GET requests now accept query parameters for filtered retrieval.
  • Documentation
    • Added a Ruby example demonstrating Suppressions API usage and environment variables (API key + account ID).
    • Updated examples to parameterize account ID via an environment variable instead of a hard-coded value.
  • Tests
    • Added tests and fixtures covering listing, filtering, deletion, and authorization error scenarios.

Copy link

coderabbitai bot commented Sep 24, 2025

Walkthrough

Adds a Suppressions API: new Suppression DTO, SuppressionsAPI with list/delete (supports email filter), example script, specs and VCR fixtures; refactors Client to accept query params and updates BaseAPI to forward them; loader now requires suppressions API.

Changes

Cohort / File(s) Summary of Changes
Suppressions feature (API, DTO, example)
lib/mailtrap/suppression.rb, lib/mailtrap/suppressions_api.rb, examples/suppressions_api.rb
Adds Mailtrap::Suppression struct and Mailtrap::SuppressionsAPI with list(email:) and delete(id); example demonstrates list, filtered list, and delete.
Client refactor & BaseAPI query support
lib/mailtrap/client.rb, lib/mailtrap/base_api.rb
Refactors request flow to keyword-arg perform_request (method:, host:, path:, query_params:, body:), builds URI internally, and adds query_params support; Client#get signature updated to accept query_params; BaseAPI#base_list now accepts and forwards query_params.
Library loader
lib/mailtrap.rb
Requires mailtrap/suppressions_api to expose the new API on load.
Tests and VCR fixtures
spec/mailtrap/suppression_spec.rb, spec/mailtrap/suppressions_api_spec.rb, spec/fixtures/vcr_cassettes/Mailtrap_SuppressionsAPI/...
Adds unit and integration specs for Suppression and SuppressionsAPI (success, filter, 401, 404) and VCR cassettes for list success and unauthorized response.
Examples parameterization
examples/contacts_api.rb, examples/email_templates_api.rb
Replace hard-coded account id with account_id variable and update example environment guidance to include MAILTRAP_ACCOUNT_ID.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Dev as Developer
  participant SA as SuppressionsAPI
  participant CL as Client
  participant API as Mailtrap API

  Dev->>SA: list(email: optional)
  SA->>CL: get(path: "/api/accounts/{account}/suppressions", query_params)
  CL->>API: HTTPS GET /api/accounts/{account}/suppressions?email=...
  API-->>CL: 200 JSON [suppressions]
  CL-->>SA: parsed response
  SA-->>Dev: [Suppression objects]

  rect rgba(230,255,230,0.25)
  note right of API: success path
  end

  alt Unauthorized
    API-->>CL: 401 {"error":"Incorrect API token"}
    CL-->>SA: raises AuthorizationError
    SA-->>Dev: AuthorizationError
  end

  Dev->>SA: delete(suppression_id)
  SA->>CL: delete(path: "/api/accounts/{account}/suppressions/{id}")
  CL->>API: HTTPS DELETE /api/.../suppressions/{id}
  API-->>CL: 204 / 404
  CL-->>SA: nil / raises Error
  SA-->>Dev: nil / Error
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

Possibly related PRs

  • Batch Sending API #53 — Modifies Mailtrap::Client; both PRs change request handling and query param support.
  • Contacts API #49 — Related to BaseAPI list behavior; this PR extends list to accept query parameters.

Suggested reviewers

  • IgorDobryn
  • mklocek

Poem

I nibble keys and hop with glee,
New suppressions sprout for all to see.
Queries trail like carrot string,
Lists return, deletes take wing.
Specs hum softly — carrot-crowd, hooray! 🥕🐇

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Title Check ✅ Passed The pull request title “Add Suppressions API” concisely and accurately summarizes the primary change introduced by this PR, namely the addition of support for the Suppressions API. It is clear, specific, and directly reflects the main feature being added without extraneous detail.
Description Check ✅ Passed The pull request description adheres to the repository’s template by including distinct Motivation, Changes, and How to test sections populated with relevant content and examples. It explains the rationale for supporting the new Suppressions API, lists the specific additions and modifications made, and provides clear instructions for running tests and using the new functionality.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch suppressions-api

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

Comment @coderabbitai help to get the list of available commands and usage tips.

@i7an i7an changed the title Suppressions Api Add Suppressions API Sep 24, 2025
@i7an i7an requested a review from IgorDobryn September 25, 2025 08:15
@i7an i7an marked this pull request as ready for review September 25, 2025 08:15
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: 1

🧹 Nitpick comments (9)
lib/mailtrap/base_api.rb (1)

67-69: Harden list mapping against non-array responses

Defensively coerce to an array to avoid errors if the client ever returns nil or a single object.

-    def base_list(query_params = {})
-      response = client.get(base_path, query_params)
-      response.map { |item| handle_response(item) }
-    end
+    def base_list(query_params = {})
+      response = client.get(base_path, query_params)
+      Array(response).map { |item| handle_response(item) }
+    end

Please confirm client.get(base_path, query_params) always returns an Array for list endpoints today.

examples/suppressions_api.rb (1)

1-39: Prefer ENV-based config and print outputs in examples

Use ENV for credentials by default and print results to make the example runnable as-is.

-require 'mailtrap'
-
-client = Mailtrap::Client.new(api_key: 'your-api-key')
-suppressions = Mailtrap::SuppressionsAPI.new 3229, client
+require 'mailtrap'
+
+# Set your API credentials as environment variables:
+# export MAILTRAP_API_KEY='your-api-key'
+# export MAILTRAP_ACCOUNT_ID='your-account-id'
+
+client = Mailtrap::Client.new(api_key: ENV.fetch('MAILTRAP_API_KEY'))
+suppressions = Mailtrap::SuppressionsAPI.new(ENV.fetch('MAILTRAP_ACCOUNT_ID'), client)
@@
-# Get all suppressions
-suppressions.list
+# Get all suppressions
+all = suppressions.list
+puts "Total suppressions: #{all.size}"
+puts all.map(&:to_h)
@@
-# Get suppressions for the email
-list = suppressions.list(email: '[email protected]')
+# Get suppressions for the email
+list = suppressions.list(email: '[email protected]')
+puts "Suppressions for [email protected]: #{list.size}"
@@
-# Delete a suppression
-suppressions.delete(list.first.id)
+# Delete a suppression (ensure there is at least one)
+if (first = list.first)
+  suppressions.delete(first.id)
+  puts "Deleted suppression #{first.id}"
+end
lib/mailtrap/suppressions_api.rb (1)

16-21: Optional: allow empty params with a leaner call

Tiny nit: you can avoid the intermediate hash.

-    def list(email: nil)
-      query_params = {}
-      query_params[:email] = email if email
-
-      base_list(query_params)
-    end
+    def list(email: nil)
+      base_list(email ? { email: } : {})
+    end
spec/mailtrap/suppressions_api_spec.rb (2)

70-78: Use WebMock query matcher to avoid percent-encoding mismatches.

Matching the full URL with a raw @ in the query can fail since the client percent-encodes it (%40). Prefer .with(query: ...) for robust matching.

Apply this diff:

-      stub_request(:get, "#{base_url}/suppressions?email=#{email}")
+      stub_request(:get, "#{base_url}/suppressions")
+        .with(query: { email: email })
         .to_return(
           status: 200,
           body: [expected_attributes].to_json,
           headers: { 'Content-Type' => 'application/json' }
         )

98-103: Use a realistic UUID in tests for clarity.

Minor readability improvement; it better reflects production IDs and prevents subtle type assumptions.

Apply this diff:

-    let(:suppression_id) { 1 }
+    let(:suppression_id) { '123e4567-e89b-12d3-a456-426614174000' }
lib/mailtrap/client.rb (4)

122-130: Avoid Ruby 3.1+ shorthand in gem code unless minimum Ruby >= 3.1.

base: and requests: shorthand requires Ruby 3.1. Use explicit values for broader compatibility.

Please confirm required_ruby_version in the gemspec is >= 3.1. If not, apply:

       perform_request(
         method: :post,
         host: api_host,
         path: batch_request_path,
         body: {
-          base:,
-          requests:
+          base: base,
+          requests: requests
         }
       )

271-275: Set Content-Type only when a body is present; add Accept header.

Avoid sending Content-Type on GET/DELETE and explicitly declare Accept.

Apply this diff:

-      request.body = body.to_json if body
-      request['Authorization'] = "Bearer #{api_key}"
-      request['Content-Type'] = 'application/json'
-      request['User-Agent'] = 'mailtrap-ruby (https://github.com/railsware/mailtrap-ruby)'
+      request.body = body.to_json if body
+      request['Authorization'] = "Bearer #{api_key}"
+      request['Accept'] = 'application/json'
+      request['Content-Type'] = 'application/json' if body
+      request['User-Agent'] = 'mailtrap-ruby (https://github.com/railsware/mailtrap-ruby)'

306-309: Harden error parsing to handle non-JSON error bodies and “message” fields.

Prevents JSON::ParserError on text responses and captures common message keys.

Apply this diff:

-    def response_errors(body)
-      parsed_body = json_response(body)
-      Array(parsed_body[:errors] || parsed_body[:error])
-    end
+    def response_errors(body)
+      parsed_body = json_response(body)
+      errors = parsed_body[:errors] || parsed_body[:error] || parsed_body[:message]
+      errors = errors.is_a?(Array) ? errors : [errors].compact
+      return errors unless errors.empty?
+
+      [body.to_s]
+    rescue JSON::ParserError
+      [body.to_s]
+    end

168-176: Doc return type for GET should include Array.

GET endpoints like suppressions return arrays.

Apply this diff:

-    # @return [Hash, nil] The JSON response
+    # @return [Hash, Array, nil] The parsed JSON response
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 440de0d and f5a944a.

📒 Files selected for processing (10)
  • examples/suppressions_api.rb (1 hunks)
  • lib/mailtrap.rb (1 hunks)
  • lib/mailtrap/base_api.rb (1 hunks)
  • lib/mailtrap/client.rb (5 hunks)
  • lib/mailtrap/suppression.rb (1 hunks)
  • lib/mailtrap/suppressions_api.rb (1 hunks)
  • spec/fixtures/vcr_cassettes/Mailtrap_SuppressionsAPI/vcr_list/maps_response_data_to_Suppression_objects.yml (1 hunks)
  • spec/fixtures/vcr_cassettes/Mailtrap_SuppressionsAPI/vcr_list/when_api_key_is_incorrect/raises_authorization_error.yml (1 hunks)
  • spec/mailtrap/suppression_spec.rb (1 hunks)
  • spec/mailtrap/suppressions_api_spec.rb (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (6)
lib/mailtrap/base_api.rb (1)
lib/mailtrap/client.rb (1)
  • get (173-180)
lib/mailtrap/suppressions_api.rb (2)
lib/mailtrap/base_api.rb (3)
  • response_class (31-33)
  • base_list (67-70)
  • base_path (80-82)
lib/mailtrap/client.rb (1)
  • delete (214-220)
lib/mailtrap/client.rb (2)
lib/mailtrap/suppressions_api.rb (1)
  • delete (27-29)
lib/mailtrap/base_api.rb (1)
  • handle_response (72-74)
examples/suppressions_api.rb (2)
lib/mailtrap/suppressions_api.rb (2)
  • list (16-21)
  • delete (27-29)
lib/mailtrap/client.rb (1)
  • delete (214-220)
spec/mailtrap/suppressions_api_spec.rb (2)
lib/mailtrap/suppressions_api.rb (3)
  • list (16-21)
  • delete (27-29)
  • include (7-36)
lib/mailtrap/client.rb (1)
  • delete (214-220)
spec/mailtrap/suppression_spec.rb (1)
lib/mailtrap/suppression.rb (1)
  • to_h (42-44)
🔇 Additional comments (10)
lib/mailtrap.rb (1)

12-12: Require wiring looks good

Requiring suppressions_api here cleanly exposes the new API surface.

spec/mailtrap/suppression_spec.rb (1)

1-43: DTO spec coverage looks solid

Initialization and to_h behavior are covered with realistic attributes.

spec/fixtures/vcr_cassettes/Mailtrap_SuppressionsAPI/vcr_list/maps_response_data_to_Suppression_objects.yml (1)

1-167: Fixture aligns with DTO mapping

Response shape matches Suppression fields; useful for deterministic tests.

lib/mailtrap/suppression.rb (1)

22-45: DTO implementation is clean; compact to_h is useful

Struct with keyword_init and compacted to_h fits the use case.

spec/fixtures/vcr_cassettes/Mailtrap_SuppressionsAPI/vcr_list/when_api_key_is_incorrect/raises_authorization_error.yml (1)

1-167: Global VCR token filtering verified spec_helper’s filter_sensitive_data is configured and all cassettes use the <BEARER_TOKEN> placeholder.

spec/mailtrap/suppressions_api_spec.rb (3)

100-106: LGTM: delete happy path is asserted correctly.

Asserting 204 → nil aligns with Client#handle_response.


108-117: LGTM: 404 maps to Mailtrap::Error.

Covers the generic 4xx branch.


149-159: LGTM: VCR auth failure scenario is well covered.

Validates both the error class and message contents.

lib/mailtrap/client.rb (2)

246-255: Nice: centralized URI building + query params support.

Clean separation of host/path and proper encoding via URI.encode_www_form.


173-179: All client.get call sites conform to the new (path, query_params = {}) signature. Single-argument calls use the default empty hash and two-argument calls pass parameters explicitly.

Comment on lines +27 to +29
def delete(suppression_id)
client.delete("#{base_path}/#{suppression_id}")
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 | 🟠 Major

🧩 Analysis chain

Return type mismatch in docs vs implementation; validate input

YARD says “return nil” but the method returns whatever client.delete returns. Either return nil explicitly or update the doc. Also guard against blank IDs.

-    # @return nil
+    # @return [nil] Always nil on success
     # @!macro api_errors
     def delete(suppression_id)
-      client.delete("#{base_path}/#{suppression_id}")
+      raise ArgumentError, 'suppression_id is required' if suppression_id.to_s.empty?
+      client.delete("#{base_path}/#{suppression_id}")
+      nil
     end

If you prefer surfacing the HTTP response (e.g., status/boolean), update the YARD return type instead and keep the current behavior.


🏁 Script executed:

#!/bin/bash
echo "SuppressionsApi delete method docs and implementation:"
rg -nP -C2 "(#.*@return|def delete)" lib/mailtrap/suppressions_api.rb

echo -e "\nOther API delete methods:"
rg -nP -C3 "def delete" -g "lib/mailtrap/*_api.rb"

echo -e "\nClient.delete method:"
rg -nP -C2 "def delete" -g "lib/mailtrap/*client*.rb"

Length of output: 2985


🏁 Script executed:

#!/bin/bash
# Locate base_delete implementation to verify its behavior
rg -nP "def base_delete" -g "lib/mailtrap/**/*.rb" -C5

Length of output: 649


🏁 Script executed:

#!/bin/bash
# Show class definition of SuppressionsApi
rg -nC3 "class SuppressionsApi" -g "lib/mailtrap/suppressions_api.rb"

Length of output: 0


Update YARD @return to match actual return value

  • Change # @return nil to # @return [Hash, nil] since client.delete returns parsed JSON or nil
  • (Optional) Guard against blank suppression_id with
    raise ArgumentError, 'suppression_id is required' if suppression_id.to_s.empty?
🤖 Prompt for AI Agents
In lib/mailtrap/suppressions_api.rb around lines 27 to 29, the YARD return tag
is incorrect and should reflect that client.delete returns parsed JSON or nil;
update the method documentation from `# @return nil` to `# @return [Hash, nil]`
(or a more specific type if known), and optionally add a guard at the top of the
method to raise ArgumentError when suppression_id is blank (e.g. raise
ArgumentError, 'suppression_id is required' if suppression_id.to_s.empty?)
before calling client.delete.

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

🧹 Nitpick comments (4)
examples/email_templates_api.rb (1)

3-5: Use ENV for credentials and account_id in examples

Avoid hard-coded IDs/keys; read from MAILTRAP_ACCOUNT_ID/MAILTRAP_API_KEY for copy‑pasteability.

Apply:

-account_id = 3229
-client = Mailtrap::Client.new(api_key: 'your-api-key')
-templates = Mailtrap::EmailTemplatesAPI.new(account_id, client)
+account_id = Integer(ENV.fetch('MAILTRAP_ACCOUNT_ID'))
+client = Mailtrap::Client.new(api_key: ENV.fetch('MAILTRAP_API_KEY'))
+templates = Mailtrap::EmailTemplatesAPI.new(account_id, client)

Also update the commented initializer below to include required args to prevent confusion.

examples/suppressions_api.rb (2)

3-5: Read account_id and API key from ENV

Make the example runnable without edits and avoid hard-coded secrets/IDs.

-account_id = 3229
-client = Mailtrap::Client.new(api_key: 'your-api-key')
-suppressions = Mailtrap::SuppressionsAPI.new(account_id, client)
+account_id = Integer(ENV.fetch('MAILTRAP_ACCOUNT_ID'))
+client = Mailtrap::Client.new(api_key: ENV.fetch('MAILTRAP_API_KEY'))
+suppressions = Mailtrap::SuppressionsAPI.new(account_id, client)

7-12: Fix commented initializer to reflect required constructor arguments

The no-arg example is misleading given the current signature.

-# suppressions = Mailtrap::SuppressionsAPI.new
+# suppressions = Mailtrap::SuppressionsAPI.new(account_id, client)
examples/contacts_api.rb (1)

3-8: Prefer ENV-driven initialization for examples

Use MAILTRAP_ACCOUNT_ID and MAILTRAP_API_KEY to make examples safer and ready to run.

-account_id = 3229
-client = Mailtrap::Client.new(api_key: 'your-api-key')
+account_id = Integer(ENV.fetch('MAILTRAP_ACCOUNT_ID'))
+client = Mailtrap::Client.new(api_key: ENV.fetch('MAILTRAP_API_KEY'))
 contact_lists = Mailtrap::ContactListsAPI.new(account_id, client)
 contacts = Mailtrap::ContactsAPI.new(account_id, client)
 contact_fields = Mailtrap::ContactFieldsAPI.new(account_id, client)
 contact_imports = Mailtrap::ContactImportsAPI.new(account_id, client)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f5a944a and d2e34e2.

📒 Files selected for processing (3)
  • examples/contacts_api.rb (1 hunks)
  • examples/email_templates_api.rb (1 hunks)
  • examples/suppressions_api.rb (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
examples/email_templates_api.rb (1)
lib/mailtrap/action_mailer/delivery_method.rb (1)
  • client (22-24)
examples/suppressions_api.rb (3)
lib/mailtrap/action_mailer/delivery_method.rb (1)
  • client (22-24)
lib/mailtrap/suppressions_api.rb (2)
  • list (16-21)
  • delete (27-29)
lib/mailtrap/client.rb (1)
  • delete (214-220)
examples/contacts_api.rb (1)
lib/mailtrap/action_mailer/delivery_method.rb (1)
  • client (22-24)

Comment on lines +37 to +40
list = suppressions.list(email: '[email protected]')

# Delete a suppression
suppressions.delete(list.first.id)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Guard against empty list before delete

list.first can be nil and raise NoMethodError on .id.

-# Delete a suppression
-suppressions.delete(list.first.id)
+# Delete a suppression (if any)
+if list.any?
+  suppressions.delete(list.first.id)
+else
+  warn 'No suppressions found for the given email'
+end
📝 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
list = suppressions.list(email: '[email protected]')
# Delete a suppression
suppressions.delete(list.first.id)
list = suppressions.list(email: '[email protected]')
# Delete a suppression (if any)
if list.any?
suppressions.delete(list.first.id)
else
warn 'No suppressions found for the given email'
end
🤖 Prompt for AI Agents
In examples/suppressions_api.rb around lines 37 to 40, calling list.first.id can
raise NoMethodError when the list is empty; update the code to check that the
returned list is non-empty (e.g., list.any? or !list.empty?) before attempting
to access first.id, and only call suppressions.delete when an item exists; if
the list is empty, either skip the delete or handle the case with a clear
message or error handling.

@i7an i7an merged commit a9f7b42 into main Sep 30, 2025
12 checks passed
@i7an i7an deleted the suppressions-api branch September 30, 2025 15:41
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.

4 participants