Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/mailtrap.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
require_relative 'mailtrap/contact_imports_api'
require_relative 'mailtrap/suppressions_api'
require_relative 'mailtrap/projects_api'
require_relative 'mailtrap/sandbox_messages_api'
require_relative 'mailtrap/inboxes_api'
require_relative 'mailtrap/sending_domains_api'

Expand Down
14 changes: 8 additions & 6 deletions lib/mailtrap/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -168,14 +168,16 @@ def send(mail)
# Performs a GET request to the specified path
# @param path [String] The request path
# @param query_params [Hash] Query parameters to append to the URL (optional)
# @param custom_parser [Proc] custom parsing function for the response body (optional)
# @return [Hash, nil] The JSON response
# @!macro api_errors
def get(path, query_params = {})
def get(path, query_params = {}, custom_parser: nil)
perform_request(
method: :get,
host: general_api_host,
path:,
query_params:
query_params:,
custom_parser:
)
end

Expand Down Expand Up @@ -243,15 +245,15 @@ def batch_request_path
"/api/batch#{"/#{inbox_id}" if sandbox}"
end

def perform_request(method:, host:, path:, query_params: {}, body: nil)
def perform_request(method:, host:, path:, query_params: {}, body: nil, custom_parser: nil)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
http_client = http_client_for(host)

uri = URI::HTTPS.build(host:, path:)
uri.query = URI.encode_www_form(query_params) if query_params.any?

request = setup_request(method, uri, body)
response = http_client.request(request)
handle_response(response)
handle_response(response, custom_parser:)
end

def setup_request(method, uri_or_path, body = nil)
Expand All @@ -276,10 +278,10 @@ def setup_request(method, uri_or_path, body = nil)
request
end

def handle_response(response) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength
def handle_response(response, custom_parser: nil) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength
case response
when Net::HTTPOK, Net::HTTPCreated
json_response(response.body)
custom_parser.nil? ? json_response(response.body) : custom_parser.call(response.body)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
when Net::HTTPNoContent
nil
when Net::HTTPBadRequest
Expand Down
59 changes: 59 additions & 0 deletions lib/mailtrap/sandbox_message.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# frozen_string_literal: true

module Mailtrap
# Data Transfer Object for Sandbox Message
# @see https://docs.mailtrap.io/developers/email-sandbox/email-sandbox-api/messages
# @attr_reader id [Integer] The message ID
# @attr_reader inbox_id [Integer] The inbox ID
# @attr_reader subject [String] The message subject
# @attr_reader sent_at [String] The timestamp when the message was sent
# @attr_reader from_email [String] The sender's email address
# @attr_reader from_name [String] The sender's name
# @attr_reader to_email [String] The recipient's email address
# @attr_reader to_name [String] The recipient's name
# @attr_reader email_size [Integer] The size of the email in bytes
# @attr_reader is_read [Boolean] Whether the message has been read
# @attr_reader created_at [String] The timestamp when the message was created
# @attr_reader updated_at [String] The timestamp when the message was last updated
# @attr_reader html_body_size [Integer] The size of the HTML body in bytes
# @attr_reader text_body_size [Integer] The size of the text body in bytes
# @attr_reader human_size [String] The human-readable size of the email
# @attr_reader html_path [String] The path to the HTML version of the email
# @attr_reader txt_path [String] The path to the text version of the email
# @attr_reader raw_path [String] The path to the raw version of the email
# @attr_reader download_path [String] The path to download the email
# @attr_reader html_source_path [String] The path to the HTML source of the email
# @attr_reader blacklists_report_info [Boolean] Information about blacklists report
# @attr_reader smtp_information [Hash] Information about SMTP
#
SandboxMessage = Struct.new(
:id,
:inbox_id,
:subject,
:sent_at,
:from_email,
:from_name,
:to_email,
:to_name,
:email_size,
:is_read,
:created_at,
:updated_at,
:html_body_size,
:text_body_size,
:human_size,
:html_path,
:txt_path,
:raw_path,
:download_path,
:html_source_path,
:blacklists_report_info,
:smtp_information,
keyword_init: true
) do
# @return [Hash] The SendingDomain attributes as a hash
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
def to_h
super.compact
end
end
end
154 changes: 154 additions & 0 deletions lib/mailtrap/sandbox_messages_api.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
# frozen_string_literal: true

require_relative 'base_api'
require_relative 'sandbox_message'

module Mailtrap
class SandboxMessagesAPI

@i7an i7an Feb 10, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Inboxes API but Sandbox Messages API. Let's discuss today.

include BaseAPI

attr_reader :account_id, :inbox_id, :client

self.supported_options = %i[is_read]

self.response_class = SandboxMessage

# @param inbox_id [Integer] The inbox ID
# @param account_id [Integer] The account ID
# @param client [Mailtrap::Client] The client instance
# @raise [ArgumentError] If account_id is nil
def initialize(inbox_id, account_id = ENV.fetch('MAILTRAP_ACCOUNT_ID'), client = Mailtrap::Client.new)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I reckon the order should be account_id, inbox_id, client. or maybe its better to use named arguments. though its inconsistent with other APIs. Lets discuss.

@i7an i7an Feb 10, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

And there is inbox_id in the client :)
Probably will address in the new major release.

raise ArgumentError, 'account_id is required' if account_id.nil?
raise ArgumentError, 'inbox_id is required' if inbox_id.nil?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat -n lib/mailtrap/sandbox_messages_api.rb | head -30

Repository: mailtrap/mailtrap-ruby

Length of output: 1217


🏁 Script executed:

# Let's also check if there are any other references to account_id handling
rg "account_id" lib/mailtrap/sandbox_messages_api.rb -A 2 -B 2

Repository: mailtrap/mailtrap-ruby

Length of output: 806


🏁 Script executed:

# Verify the behavior of ENV.fetch vs ENV[]
# ENV.fetch raises KeyError if key doesn't exist
# ENV[] returns nil if key doesn't exist
python3 << 'EOF'
# Simulate Ruby's ENV.fetch behavior
# ENV.fetch('KEY') raises KeyError if not found
# ENV['KEY'] returns nil if not found
print("ENV.fetch behavior: raises KeyError when env var not found")
print("ENV['KEY'] behavior: returns nil when env var not found")
print("\nThe review claim:")
print("- Using ENV.fetch means KeyError is raised (not ArgumentError)")
print("- The `@raise` documentation claims ArgumentError will be raised")
print("- This is a mismatch between documented behavior and actual behavior")
EOF

Repository: mailtrap/mailtrap-ruby

Length of output: 394


🏁 Script executed:

# Check YARD documentation in the file
rg "@raise" lib/mailtrap/sandbox_messages_api.rb -B 5 -A 1

Repository: mailtrap/mailtrap-ruby

Length of output: 756


Align error behavior with the YARD contract.

Line 20 uses ENV.fetch, which raises KeyError when the environment variable is unset, but the documentation declares @raise [ArgumentError]. To match the documented contract, use ENV['MAILTRAP_ACCOUNT_ID'] so the explicit ArgumentError check on line 21 actually executes when the account ID is missing or nil.

💡 Proposed fix
-    def initialize(inbox_id, account_id = ENV.fetch('MAILTRAP_ACCOUNT_ID'), client = Mailtrap::Client.new)
+    def initialize(inbox_id, account_id = ENV['MAILTRAP_ACCOUNT_ID'], client = Mailtrap::Client.new)
       raise ArgumentError, 'account_id is required' if account_id.nil?
       raise ArgumentError, 'inbox_id is required' if inbox_id.nil?
📝 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
# @param inbox_id [Integer] The inbox ID
# @param account_id [Integer] The account ID
# @param client [Mailtrap::Client] The client instance
# @raise [ArgumentError] If account_id is nil
def initialize(inbox_id, account_id = ENV.fetch('MAILTRAP_ACCOUNT_ID'), client = Mailtrap::Client.new)
raise ArgumentError, 'account_id is required' if account_id.nil?
raise ArgumentError, 'inbox_id is required' if inbox_id.nil?
# `@param` inbox_id [Integer] The inbox ID
# `@param` account_id [Integer] The account ID
# `@param` client [Mailtrap::Client] The client instance
# `@raise` [ArgumentError] If account_id is nil
def initialize(inbox_id, account_id = ENV['MAILTRAP_ACCOUNT_ID'], client = Mailtrap::Client.new)
raise ArgumentError, 'account_id is required' if account_id.nil?
raise ArgumentError, 'inbox_id is required' if inbox_id.nil?
🤖 Prompt for AI Agents
In `@lib/mailtrap/sandbox_messages_api.rb` around lines 16 - 22, The initialize
method currently uses ENV.fetch('MAILTRAP_ACCOUNT_ID') which raises KeyError and
breaks the documented `@raise` [ArgumentError] contract; change the default to
ENV['MAILTRAP_ACCOUNT_ID'] so the explicit guard (raise ArgumentError,
'account_id is required') for account_id in initialize(inbox_id, account_id =
ENV['MAILTRAP_ACCOUNT_ID'], client = Mailtrap::Client.new) will run when the env
var is missing or nil, leaving the inbox_id guard (raise ArgumentError,
'inbox_id is required') and the rest of the initializer unchanged.


@account_id = account_id

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

can you use super(account_id, client)?

@inbox_id = inbox_id
@client = client
end

# Retrieves a specific sandbox message from inbox
# @param message_id [Integer] The sandbox message ID
# @return [SandboxMessage] Sandbox message object
# @!macro api_errors
def get(message_id)
base_get(message_id)
end

# Deletes a sandbox message
# @param message_id [Integer] The sandbox message ID
# @return [SandboxMessage] Deleted Sandbox message object
# @!macro api_errors
def delete(message_id)
base_delete(message_id)
end

# Updates an existing sandbox message
# @param message_id [Integer] The sandbox message ID
# @param [Hash] options The parameters to update
# @return [SandboxMessage] Updated Sandbox message object
# @!macro api_errors
# @raise [ArgumentError] If invalid options are provided
def update(message_id, options)
base_update(message_id, options)
end

# Lists all sandbox messages for the account, limited up to 30 at once
# @param search [String] Search query string. Matches subject, to_email, and to_name.
# @param last_id [Integer] If specified, a page of records before last_id is returned.
# Overrides page if both are given.
# @param page [Integer] Page number for paginated results.
# @return [Array<SandboxMessage>] Array of sandbox message objects
# @!macro api_errors
def list(search: nil, last_id: nil, page: nil)
query_params = {}
query_params[:search] = search unless search.nil?
query_params[:last_id] = last_id unless last_id.nil?
query_params[:page] = page unless page.nil?
Comment thread
coderabbitai[bot] marked this conversation as resolved.

base_list(query_params)
end

# Forward message to an email address.
# @param message_id [Integer] The Sandbox message ID
# @param email [String] The email to forward sandbox message to
# @return [String] Forwarded message confirmation
# @!macro api_errors
def forward_message(message_id, email)
client.post("#{base_path}/#{message_id}/forward", { email: email })
end

# Get message spam score
# @param message_id [Integer] The Sandbox message ID
# @return [Hash] Spam report
# @!macro api_errors
def get_spam_score(message_id)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

get_ prefix should be avoided.

client.get("#{base_path}/#{message_id}/spam_report")
end

# Get message HTML analysis
# @param message_id [Integer] The Sandbox message ID
# @return [Hash] brief HTML report
# @!macro api_errors
def get_html_analysis(message_id)
client.get("#{base_path}/#{message_id}/analyze")
end

# Get text message
# @param message_id [Integer] The Sandbox message ID
# @return [String] text email body
# @!macro api_errors
def get_text_message(message_id)
client.get("#{base_path}/#{message_id}/body.txt", custom_parser: ->(response) { response })
end

# Get raw message
# @param message_id [Integer] The Sandbox message ID
# @return [String] raw email body
# @!macro api_errors
def get_raw_message(message_id)
client.get("#{base_path}/#{message_id}/body.raw", custom_parser: ->(response) { response })
end

# Get message source
# @param message_id [Integer] The Sandbox message ID
# @return [String] HTML source of a message.
# @!macro api_errors
def get_html_source(message_id)
client.get("#{base_path}/#{message_id}/body.htmlsource", custom_parser: ->(response) { response })
end

# Get formatted HTML email body. Not applicable for plain text emails.
# @param message_id [Integer] The Sandbox message ID
# @return [String] message body in html format.
# @!macro api_errors
def get_html_message(message_id)
client.get("#{base_path}/#{message_id}/body.html", custom_parser: ->(response) { response })
end

# Get mail headers
# @param message_id [Integer] The Sandbox message ID
# @return [Hash] mail headers of the message.
# @!macro api_errors
def get_message_as_eml(message_id)
client.get("#{base_path}/#{message_id}/body.eml", custom_parser: ->(response) { response })
end

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Incorrect documentation for get_message_as_eml.

The YARD comment says "Get mail headers" but this method retrieves the message as EML format. This appears to be a copy-paste error.

📝 Proposed fix
-    # Get mail headers
+    # Get message as EML
     # `@param` message_id [Integer] The Sandbox message ID
-    # `@return` [Hash] mail headers of the message.
+    # `@return` [String] message in EML format.
     # @!macro api_errors
     def get_message_as_eml(message_id)
       client.get("#{base_path}/#{message_id}/body.eml", custom_parser: ->(response) { response })
     end
🤖 Prompt for AI Agents
In `@lib/mailtrap/sandbox_messages_api.rb` around lines 128 - 134, Update the YARD
docstring for get_message_as_eml: replace the incorrect "Get mail headers"
description and the `@return` type with a correct description indicating it
retrieves the full message in EML format (e.g., "Get message as EML") and adjust
the `@return` to reflect the raw EML response (e.g., String or raw response) while
keeping the `@param` and @!macro api_errors intact; locate the doc above def
get_message_as_eml(message_id) which calls
client.get("#{base_path}/#{message_id}/body.eml", custom_parser: ->(response) {
response }).


# Get mail headers
# @param message_id [Integer] The Sandbox message ID
# @return [Hash] mail headers of the message.
# @!macro api_errors
def get_mail_headers(message_id)
client.get("#{base_path}/#{message_id}/mail_headers")
end

private

def base_path
"/api/accounts/#{account_id}/inboxes/#{inbox_id}/messages"
end

def wrap_request(options)
{ message: options }
end
end
end

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading