Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
165 changes: 100 additions & 65 deletions examples/contacts_api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
contact_lists = Mailtrap::ContactListsAPI.new 3229, client
contacts = Mailtrap::ContactsAPI.new 3229, client
contact_fields = Mailtrap::ContactFieldsAPI.new 3229, client
contact_imports = Mailtrap::ContactImportsAPI.new 3229, client

# Set your API credentials as environment variables
# export MAILTRAP_API_KEY='your-api-key'
Expand All @@ -12,83 +13,84 @@
# contact_lists = Mailtrap::ContactListsAPI.new
# contacts = Mailtrap::ContactsAPI.new
# contact_fields = Mailtrap::ContactFieldsAPI.new
# contact_imports = Mailtrap::ContactImportsAPI.new

# Create new contact list
list = contact_lists.create(name: 'Test List')
# => ContactList.new(id: 1, name: 'Test List')
# => #<struct Mailtrap::ContactList id=1, name="Test List">

# Get all contact lists
contact_lists.list
# => [ContactList.new(id: 1, name: 'Test List')]
# => [#<struct Mailtrap::ContactList id=1, name="Test List">]

# Update contact list
contact_lists.update(list.id, name: 'Test List Updated')
# => ContactList.new(id: 1, name: 'Test List Updated')
# => #<struct Mailtrap::ContactList id=1, name="Test List Updated">

# Get contact list
list = contact_lists.get(list.id)
# => ContactList.new(id: 1, name: 'Test List Updated')
# => #<struct Mailtrap::ContactList id=1, name="Test List Updated">

# Delete contact list
contact_lists.delete(list.id)

# Create new contact field
field = contact_fields.create(name: 'Nickname', data_type: 'text', merge_tag: 'nickname')
# => ContactField.new(id: 1, name: 'Nickname', data_type: 'text', merge_tag: 'nickname')
# => #<struct Mailtrap::ContactField id=1, name="Nickname", data_type="text", merge_tag="nickname">

# Get all contact fields
contact_fields.list
# => [ContactField.new(id: 1, name: 'Nickname', data_type: 'text', merge_tag: 'nickname')]
# => [#<struct Mailtrap::ContactField id=1, name="Nickname", data_type="text", merge_tag="nickname">]

# Update contact field
contact_fields.update(field.id, name: 'Nickname 2', merge_tag: 'nickname')
# => ContactField.new(id: 1, name: 'Nickname 2', data_type: 'text', merge_tag: 'nickname')
# => #<struct Mailtrap::ContactField id=1, name="Nickname 2", data_type="text", merge_tag="nickname">

# Get contact field
field = contact_fields.get(field.id)
# => ContactField.new(id: 1, name: 'Nickname 2', data_type: 'text', merge_tag: 'nickname')
# => #<struct Mailtrap::ContactField id=1, name="Nickname 2", data_type="text", merge_tag="nickname">

# Create new contact with all possible fields
contact = contacts.create(
email: 'test@example.com',
fields: { field.merge_tag => 'John Doe' },
list_ids: [list.id]
)
# => Contact.new(
# id: 1,
# email: 'test@example.com',
# fields: { 'nickname' => 'John Doe' },
# list_ids: [1],
# status: 'subscribed',
# created_at: 1721212345,
# updated_at: 1721212345
# )
# => #<struct Mailtrap::Contact
# id=1,
# email="test@example.com",
# fields={ "nickname" => "John Doe" },
# list_ids=[1],
# status="subscribed",
# created_at=1721212345,
# updated_at=1721212345>
contact.newly_created? # => true

# Get contact
contact = contacts.get(contact.id)
# => Contact.new(
# id: 1,
# email: 'test@example.com',
# fields: { 'nickname' => 'John Doe' },
# list_ids: [1],
# status: 'subscribed',
# created_at: 1721212345,
# updated_at: 1721212345
# )
# => #<struct Mailtrap::Contact
# id=1,
# email="test@example.com",
# fields={ "nickname" => "John Doe" },
# list_ids=[1],
# status="subscribed",
# created_at=1721212345,
# updated_at=1721212345>

# Update contact using id
updated_contact = contacts.upsert(
contact.id,
email: 'test2@example.com',
fields: { field.merge_tag => 'Jane Doe' }
)
# => Contact.new(
# id: 1,
# email: 'test2@example.com',
# fields: { 'nickname' => 'Jane Doe' },
# list_ids: [1],
# status: 'subscribed',
# created_at: 1721212345,
# updated_at: 1721212350
# )
# => #<struct Mailtrap::Contact
# id=1,
# email="test2@example.com",
# fields={ "nickname" => "Jane Doe" },
# list_ids=[1],
# status="subscribed",
# created_at=1721212345,
# updated_at=1721212350>
updated_contact.newly_created? # => false

# Update contact using email
Expand All @@ -97,46 +99,79 @@
email: 'test3@example.com',
fields: { field.merge_tag => 'Jane Doe' }
)
# => Contact.new(
# id: 1,
# email: 'test3@example.com',
# fields: { 'nickname' => 'Jane Doe' },
# list_ids: [1],
# status: 'subscribed',
# created_at: 1721212345,
# updated_at: 1721212355
# )
# => #<struct Mailtrap::Contact
# id=1,
# email="test3@example.com",
# fields={ "nickname" => "Jane Doe" },
# list_ids=[1],
# status="subscribed",
# created_at=1721212345,
# updated_at=1721212355>
updated_contact.newly_created? # => false

# Remove contact from lists
contacts.remove_from_lists(contact.id, [list.id])
# => Contact.new(
# id: 1,
# email: 'test3@example.com',
# fields: { 'nickname' => 'Jane Doe' },
# list_ids: [],
# status: 'subscribed',
# created_at: 1721212345,
# updated_at: 1721212360
# )
# => #<struct Mailtrap::Contact
# id=1,
# email="test3@example.com",
# fields={ "nickname" => "Jane Doe" },
# list_ids=[],
# status="subscribed",
# created_at=1721212345,
# updated_at=1721212360>

# Add contact to lists
contacts.add_to_lists(contact.id, [list.id])
# => Contact.new(
# id: 1,
# email: 'test3@example.com',
# fields: { 'nickname' => 'Jane Doe' },
# list_ids: [1],
# status: 'subscribed',
# created_at: 1721212345,
# updated_at: 1721212365
# )
# => #<struct Mailtrap::Contact
# id=1,
# email="test3@example.com",
# fields={ "nickname" => "Jane Doe" },
# list_ids=[1],
# status="subscribed",
# created_at=1721212345,
# updated_at=1721212365>

# Delete contact
contacts.delete(contact.id)

# Delete contact list
contact_lists.delete(list.id)

# Delete contact field
contact_fields.delete(field.id)

# Create a new contact import using ContactsImportRequest builder
import_request = Mailtrap::ContactsImportRequest.new.tap do |req|
req.upsert(email: 'john.doe@example.com', fields: { first_name: 'John' })
.add_to_lists(email: 'john.doe@example.com', list_ids: [1])
.remove_from_lists(email: 'jane.smith@example.com', list_ids: [2])

req.upsert(email: 'jane.smith@example.com', fields: { first_name: 'Jane' })
.add_to_lists(email: 'jane.smith@example.com', list_ids: [1])
.remove_from_lists(email: 'jane.smith@example.com', list_ids: [2])
end

# Execute the import
contact_import = contact_imports.create(import_request)
# => #<struct Mailtrap::ContactImport
# id=1,
# status="created",
# created_contacts_count=nil,
# updated_contacts_count=nil,
# contacts_over_limit_count=nil>

# Wait for the import to complete (if needed)

# Get the import status
contact_imports.get(contact_import.id)
# => #<struct Mailtrap::ContactImport
# id=1,
# status="finished",
# created_contacts_count=2,
# updated_contacts_count=0,
# contacts_over_limit_count=0>

# Import using plain hash
contact_imports.create(
[
{ email: 'john@example.com', fields: { first_name: 'John' }, list_ids_included: [1], list_ids_excluded: [2] },
{ email: 'jane@example.com', fields: { first_name: 'Jane' }, list_ids_included: [1], list_ids_excluded: [2] }
]
)
1 change: 1 addition & 0 deletions lib/mailtrap.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
require_relative 'mailtrap/contacts_api'
require_relative 'mailtrap/contact_lists_api'
require_relative 'mailtrap/contact_fields_api'
require_relative 'mailtrap/contact_imports_api'

module Mailtrap
# @!macro api_errors
Expand Down
23 changes: 23 additions & 0 deletions lib/mailtrap/contact_import.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# frozen_string_literal: true

module Mailtrap
# Data Transfer Object for Contact Import
# @attr_reader id [Integer] The contact import ID
# @attr_reader status [String] The status of the import (created, started, finished, failed)
# @attr_reader created_contacts_count [Integer, nil] Number of contacts created in this import
# @attr_reader updated_contacts_count [Integer, nil] Number of contacts updated in this import
# @attr_reader contacts_over_limit_count [Integer, nil] Number of contacts over the allowed limit
ContactImport = Struct.new(
Copy link
Contributor

Choose a reason for hiding this comment

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

Or it could be Data.define

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We chose Struct for flexibility. Struct does not require all fields to be present. For example here contact import API does not return created_contacts_count when import is started. Another reason Data class raises an error when more arguments are passed to it. So we could not add more fields to the api without breaking the ruby sdk.

:id,
:status,
:created_contacts_count,
:updated_contacts_count,
:contacts_over_limit_count,
keyword_init: true
) do
# @return [Hash] The contact attributes as a hash
def to_h
super.compact
end
end
end
61 changes: 61 additions & 0 deletions lib/mailtrap/contact_imports_api.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# frozen_string_literal: true

require_relative 'contact_import'
require_relative 'contacts_import_request'

module Mailtrap
class ContactImportsAPI
include BaseAPI

self.supported_options = %i[email fields list_ids_included list_ids_excluded]

self.response_class = ContactImport

# Retrieves a specific contact import
# @param import_id [Integer] The contact import identifier
# @return [ContactImport] Contact import object
# @!macro api_errors
def get(import_id)
base_get(import_id)
end

# Create contacts import
#
# @example Using Mailtrap::ContactsImportRequest
# import_request = Mailtrap::ContactsImportRequest.new.tap do |req|
# req.upsert(email: 'jane@example.com', fields: { first_name: 'Jane' })
# .add_to_lists(email: 'jane@example.com', list_ids: [1])
# .remove_from_lists(email: 'jane@example.com', list_ids: [2])
# req.upsert(email: 'john@example.com', fields: { first_name: 'John' })
# .add_to_lists(email: 'john@example.com', list_ids: [1])
# .remove_from_lists(email: 'john@example.com', list_ids: [2])
# end
# contact_imports.create(import_request)
#
# @example Using plain hash
# contact_imports.create([
# {email: 'john@example.com', fields: { first_name: 'John' }, list_ids_included: [1], list_ids_excluded: [2]},
# {email: 'jane@example.com', fields: { first_name: 'Jane' }, list_ids_included: [1], list_ids_excluded: [2]}
# ])
#
# @param contacts [Mailtrap::ContactsImportRequest, Array<Hash>] The contacts import request
#
# @return [ContactImport] Created contact import object
# @!macro api_errors
# @raise [ArgumentError] If invalid options are provided
def create(contacts)
contact_data = contacts.to_a.each do |contact|
validate_options!(contact, supported_options)
end
response = client.post(base_path, contacts: contact_data)
handle_response(response)
end
alias start create

private

def base_path
"/api/accounts/#{account_id}/contacts/imports"
end
end
end
67 changes: 67 additions & 0 deletions lib/mailtrap/contacts_import_request.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# frozen_string_literal: true

module Mailtrap
# A builder class for creating contact import requests
# Allows you to build a collection of contacts with their associated fields and list memberships
class ContactsImportRequest
def initialize
@data = Hash.new do |h, k|
h[k] = { email: k, fields: {}, list_ids_included: [], list_ids_excluded: [] }
end
end

# Creates or updates a contact with the provided email and fields
# @param email [String] The contact's email address
# @param fields [Hash] Contact fields in the format: field_merge_tag => String, Integer, Float, Boolean, or
# ISO-8601 date string (yyyy-mm-dd)
# @return [ContactsImportRequest] Returns self for method chaining
def upsert(email:, fields: {})
validate_email!(email)

@data[email][:fields].merge!(fields)

self
end
Comment on lines +18 to +24
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Nil or non-Hash fields will raise; normalize email before indexing.

merge! will NoMethodError if fields: nil. Also normalize the email once (trim/downcase) before using it as a Hash key to avoid duplicate entries for the same logical address.

Apply:

-    def upsert(email:, fields: {})
-      validate_email!(email)
-
-      @data[email][:fields].merge!(fields)
+    def upsert(email:, fields: {})
+      email = normalize_email(email)
+      validate_email!(email)
+      fields = {} if fields.nil?
+      raise ArgumentError, 'fields must be a Hash' unless fields.is_a?(Hash)
+      @data[email][:fields].merge!(fields.transform_keys(&:to_s))

Add normalize_email in the private section (see helper snippet below).

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

🤖 Prompt for AI Agents
In lib/mailtrap/contacts_import_request.rb around lines 18 to 24, the upsert
method uses the raw email as a hash key and calls merge! on fields which will
raise if fields is nil or not a Hash; normalize the email once (trim and
downcase) before indexing and ensure fields is turned into a Hash (e.g. coerce
nil to {} or call to_h) before merging. Change upsert to compute
normalized_email = normalize_email(email), validate that normalized_email,
ensure safe_fields = (fields || {}).to_h, then use
@data[normalized_email][:fields].merge!(safe_fields) and return self; add a
private normalize_email method (trim and downcase) in the private section as
requested.


# Adds a contact to the specified lists
# @param email [String] The contact's email address
# @param list_ids [Array<Integer>] Array of list IDs to add the contact to
# @return [ContactsImportRequest] Returns self for method chaining
def add_to_lists(email:, list_ids:)
validate_email!(email)

append_list_ids email:, list_ids:, key: :list_ids_included

self
end
Comment on lines +30 to +36
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Handle nil/invalid list_ids and normalize email before appending.

list_ids.empty? will blow up on nil. Also prefer validating content (positive Integers).

Apply:

-    def add_to_lists(email:, list_ids:)
-      validate_email!(email)
-
-      append_list_ids email:, list_ids:, key: :list_ids_included
+    def add_to_lists(email:, list_ids:)
+      email = normalize_email(email)
+      validate_email!(email)
+      list_ids = normalize_list_ids(list_ids)
+      append_list_ids email:, list_ids:, key: :list_ids_included
📝 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 add_to_lists(email:, list_ids:)
validate_email!(email)
append_list_ids email:, list_ids:, key: :list_ids_included
self
end
def add_to_lists(email:, list_ids:)
email = normalize_email(email)
validate_email!(email)
list_ids = normalize_list_ids(list_ids)
append_list_ids email:, list_ids:, key: :list_ids_included
self
end
🤖 Prompt for AI Agents
In lib/mailtrap/contacts_import_request.rb around lines 30 to 36, the method
add_to_lists currently assumes list_ids is a non-nil array and uses the raw
email; change it to first normalize the email (strip and downcase) before
calling validate_email!, coerce list_ids into an array if a single Integer was
passed, return early if list_ids is nil/empty, and validate each id is a
positive Integer (raise ArgumentError for invalid entries) before calling
append_list_ids with the normalized email and validated list IDs.


# Removes a contact from the specified lists
# @param email [String] The contact's email address
# @param list_ids [Array<Integer>] Array of list IDs to remove the contact from
# @return [ContactsImportRequest] Returns self for method chaining
def remove_from_lists(email:, list_ids:)
validate_email!(email)

append_list_ids email:, list_ids:, key: :list_ids_excluded

self
end
Comment on lines +42 to +48
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Mirror the same validation/normalization in remove_from_lists.

Apply:

-    def remove_from_lists(email:, list_ids:)
-      validate_email!(email)
-
-      append_list_ids email:, list_ids:, key: :list_ids_excluded
+    def remove_from_lists(email:, list_ids:)
+      email = normalize_email(email)
+      validate_email!(email)
+      list_ids = normalize_list_ids(list_ids)
+      append_list_ids email:, list_ids:, key: :list_ids_excluded
📝 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 remove_from_lists(email:, list_ids:)
validate_email!(email)
append_list_ids email:, list_ids:, key: :list_ids_excluded
self
end
def remove_from_lists(email:, list_ids:)
email = normalize_email(email)
validate_email!(email)
list_ids = normalize_list_ids(list_ids)
append_list_ids email:, list_ids:, key: :list_ids_excluded
self
end
🤖 Prompt for AI Agents
In lib/mailtrap/contacts_import_request.rb around lines 42 to 48,
remove_from_lists only calls validate_email! and append_list_ids but doesn't
apply the same list_ids validation/normalization used elsewhere; update
remove_from_lists to mirror add_to_lists by validating and normalizing list_ids
(e.g., call validate_list_ids! or normalize_list_ids! as used by add_to_lists),
ensure the email is normalized the same way if add_to_lists normalizes it, then
call append_list_ids with the normalized values and key: :list_ids_excluded, and
return self.


# @return [Array<Hash>] Array of contact objects ready for import
def to_a
@data.values
end
Comment on lines +51 to +53
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

to_a exposes live internal structures; deep copy to avoid accidental mutation.

Returning references allows callers/tests to mutate builder state unexpectedly.

Apply:

-    def to_a
-      @data.values
-    end
+    def to_a
+      @data.values.map do |c|
+        {
+          email: c[:email],
+          fields: c[:fields].dup,
+          list_ids_included: c[:list_ids_included].dup,
+          list_ids_excluded: c[:list_ids_excluded].dup
+        }
+      end
+    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
def to_a
@data.values
end
def to_a
@data.values.map do |c|
{
email: c[:email],
fields: c[:fields].dup,
list_ids_included: c[:list_ids_included].dup,
list_ids_excluded: c[:list_ids_excluded].dup
}
end
end
🤖 Prompt for AI Agents
In lib/mailtrap/contacts_import_request.rb around lines 51 to 53, the to_a
method currently returns @data.values which exposes internal objects to callers;
change it to return a deep copy of the values array so callers cannot mutate
internal state. Specifically, produce and return a new array where each element
is deep-duplicated (recursively duplicate hashes/arrays/strings as needed)
rather than returning the raw references; ensure the deep-dup approach preserves
value types and structure but isolates mutations from @data.


private

def validate_email!(email)
raise ArgumentError, 'email must be present' if email.nil? || email.empty?
end

def append_list_ids(email:, list_ids:, key:)
raise ArgumentError, 'list_ids must be present' if list_ids.empty?

@data[email][key] |= list_ids
end
end
end
Loading