Skip to content

added ContactImportAPI #64

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 4 commits into
base: main
Choose a base branch
from
Open
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
97 changes: 94 additions & 3 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,6 +13,7 @@
# 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')
Expand Down Expand Up @@ -135,8 +137,97 @@
# 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
contact_import = contact_imports.create(
[
{
email: 'imported@example.com',
fields: {
first_name: 'Jane',
},
list_ids_included: [list.id],
list_ids_excluded: []
Copy link
Contributor

@i7an i7an Jul 17, 2025

Choose a reason for hiding this comment

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

I think this is not elegant and exposes the confusing underlying API. Instead I suggest this:

contacts = Mailtrap::ContactsAPI.new

contacts.import do |req|
  req.upsert(...)
  req.upsert(...)

  req.add_to_lists(...)
  req.add_to_lists(...)

  req.remove_from_lists(...)
  req.remove_from_lists(...)
end

req = ImportRequest.new

req.upsert(...)
req.upsert(...)

req.add_to_lists(...)
req.add_to_lists(...)

req.remove_from_lists(...)
req.remove_from_lists(...)

contacts.import req

constats.import([{...}, {...}, {...}])

@IgorDobryn thoughts?

}
]
)
# => ContactImport.new(
# id: 1,
# status: 'created',
# list_ids: [1],
# created_contacts_count: 1,
# updated_contacts_count: 0,
# contacts_over_limit_count: 0
# )

# Get a contact import by ID
contact_imports.get(contact_import.id)
# => ContactImport.new(
# id: 1,
# status: 'started',
# list_ids: [1],
# created_contacts_count: 1,
# updated_contacts_count: 0,
# contacts_over_limit_count: 0
# )

# Create a new contact import using ContactsImportRequest builder
import_request = Mailtrap::ContactsImportRequest.new

# Add contacts using the builder pattern
import_request
.upsert(
email: 'john.doe@example.com',
fields: { first_name: 'John' }
)
.add_to_lists(email: 'john.doe@example.com', list_ids: [list.id])
.upsert(
email: 'jane.smith@example.com',
fields: { first_name: 'Jane' }
)
.add_to_lists(email: 'jane.smith@example.com', list_ids: [list.id])
.remove_from_lists(email: 'jane.smith@example.com', list_ids: [])

# Execute the import
contact_imports.create(import_request)
# => ContactImport.new(
# id: 2,
# status: 'created',
# list_ids: [1],
# created_contacts_count: 2,
# updated_contacts_count: 0,
# contacts_over_limit_count: 0
# )

# Alternative: Step-by-step building
builder = Mailtrap::ContactsImportRequest.new
builder.upsert(email: 'jane.doe@example.com', fields: { first_name: 'Jane' })
builder.add_to_lists(email: 'jane.doe@example.com', list_ids: [list.id])

contact_import_2 = contact_imports.create(builder)
# => ContactImport.new(
# id: 3,
# status: 'created',
# list_ids: [1],
# created_contacts_count: 1,
# updated_contacts_count: 0,
# contacts_over_limit_count: 0
# )

sleep 3 # Wait for the import to complete (if needed)
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Remove hardcoded sleep from production example.

The sleep 3 statement is problematic in example code as it suggests users should hardcode delays in their applications. Consider replacing with a comment about polling or using a more robust waiting mechanism.

-sleep 3 # Wait for the import to complete (if needed)
+# In production, consider polling the import status instead of sleeping
+# loop do
+#   import_status = contact_imports.get(contact_import_2.id)
+#   break if %w[completed failed].include?(import_status.status)
+#   sleep 1
+# end
🤖 Prompt for AI Agents
In examples/contacts_api.rb at line 219, remove the hardcoded sleep 3 statement
as it encourages poor practice in production code. Replace it with a comment
suggesting users implement a polling mechanism or a more robust way to wait for
the import to complete instead of using fixed delays.


# Get the import status
contact_imports.get(contact_import_2.id)
# => ContactImport.new(
# id: 3,
# status: 'completed',
# list_ids: [1],
# created_contacts_count: 1,
# updated_contacts_count: 0,
# contacts_over_limit_count: 0
# )

# Delete contact list
contact_lists.delete(list.id)
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 [String] 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(
: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
49 changes: 49 additions & 0 deletions lib/mailtrap/contact_imports_api.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# 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 [String] The contact import identifier
# @return [ContactImport] Contact import object
# @!macro api_errors
def get(import_id)
base_get(import_id)
end

# Create contacts import
# @param contacts [Array<Hash>, ContactsImportRequest, #to_a] Any object that responds to #to_a and returns an array of contact hashes. # rubocop:disable Layout/LineLength
# Accepts Array<Hash>, ContactsImportRequest, or any other object implementing #to_a
# When using Array<Hash>, each contact object should have the following keys:
# - email [String] The contact's email address
# - fields [Hash] Object of fields in the format: field_merge_tag => String, Integer, Float, Boolean, or ISO-8601 date string (yyyy-mm-dd) # rubocop:disable Layout/LineLength
# - list_ids_included [Array<Integer>] List IDs to include the contact in
# - list_ids_excluded [Array<Integer>] List IDs to exclude the contact from
# @return [ContactImport] Created contact list object
# @!macro api_errors
# @raise [ArgumentError] If invalid options are provided
def create(contacts)
contact_data = contacts.to_a
contact_data.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
63 changes: 63 additions & 0 deletions lib/mailtrap/contacts_import_request.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# 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 = {}
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) # rubocop:disable Layout/LineLength
# @return [ContactsImportRequest] Returns self for method chaining
def upsert(email:, fields: {})
unless @data[email]
@data[email] = { email:, fields:, list_ids_included: [], list_ids_excluded: [] }
return self
end

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

self
end

# 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:)
unless @data[email]
@data[email] = { email:, fields: {}, list_ids_included: list_ids, list_ids_excluded: [] }
return self
end

@data[email][:list_ids_included] |= list_ids

self
end

# 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:)
unless @data[email]
@data[email] = { email:, fields: {}, list_ids_included: [], list_ids_excluded: list_ids }
return self
end

@data[email][:list_ids_excluded] |= list_ids

self
end

# Converts the import request to a JSON-serializable array
# @return [Array<Hash>] Array of contact objects ready for import
def as_json
@data.values
end
alias to_a as_json
end
end
Loading