Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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
24 changes: 17 additions & 7 deletions lib/mailtrap/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -279,17 +279,17 @@ def setup_request(method, uri_or_path, body = nil)
def handle_response(response) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength
case response
when Net::HTTPOK, Net::HTTPCreated
json_response(response.body)
parse_response(response)
when Net::HTTPNoContent
nil
when Net::HTTPBadRequest
raise Mailtrap::Error, ['bad request'] if response.body.empty?

raise Mailtrap::Error, response_errors(response.body)
raise Mailtrap::Error, response_errors(response)
when Net::HTTPUnauthorized
raise Mailtrap::AuthorizationError, response_errors(response.body)
raise Mailtrap::AuthorizationError, response_errors(response)
when Net::HTTPForbidden
raise Mailtrap::RejectionError, response_errors(response.body)
raise Mailtrap::RejectionError, response_errors(response)
when Net::HTTPPayloadTooLarge
raise Mailtrap::MailSizeError, ['message too large']
when Net::HTTPTooManyRequests
Expand All @@ -303,9 +303,19 @@ def handle_response(response) # rubocop:disable Metrics/CyclomaticComplexity, Me
end
end

def response_errors(body)
parsed_body = json_response(body)
Array(parsed_body[:errors] || parsed_body[:error])
def response_errors(response)
if response['Content-Type']&.include?('application/json')
parsed_body = json_response(response.body)
Array(parsed_body[:errors] || parsed_body[:error])
else
[response.body]
end
end

def parse_response(response)
return json_response(response.body) if response['Content-Type']&.include?('application/json')

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.

please see all the places where json_response is used and update those as well.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

the only other place is handling the errors, I think it should be kept.

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.

Please explain why. I can find places in our code where plain error response is returned e.g. mailsend_messages#index.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

You are right - done.


response.body
end

def json_response(body)
Expand Down
54 changes: 54 additions & 0 deletions lib/mailtrap/sandbox_message.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# 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
)
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")
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")
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")
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")
end

# Get message as EML
# @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")
end

# 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