Skip to content

Contacts API #49

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 6 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
25 changes: 25 additions & 0 deletions examples/contact_fields.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
require 'mailtrap'

client = Mailtrap::Client.new(api_key: 'your-api-key')
contact_fields = Mailtrap::ContactFieldsAPI.new 3229, client

# Set your API credentials as environment variables
# export MAILTRAP_API_KEY='your-api-key'
# export MAILTRAP_ACCOUNT_ID=your-account-id
#
# contact_fields = Mailtrap::ContactFieldsAPI.new

# Create new contact field
field = contact_fields.create(name: 'Updated name', data_type: 'text', merge_tag: 'updated_name')

# Get all contact fields
contact_fields.list

# Update contact field
contact_fields.update(field.id, name: 'Updated name 2', merge_tag: 'updated_name_2')

# Get contact field
field = contact_fields.get(field.id)

# Delete contact field
contact_fields.delete(field.id)
48 changes: 48 additions & 0 deletions examples/contacts_api.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
require 'mailtrap'

client = Mailtrap::Client.new(api_key: 'your-api-key')
contact_list = Mailtrap::ContactListsAPI.new 3229, client
contacts = Mailtrap::ContactsAPI.new 3229, client

# Set your API credentials as environment variables
# export MAILTRAP_API_KEY='your-api-key'
# export MAILTRAP_ACCOUNT_ID=your-account-id
#
# contact_list = Mailtrap::ContactListsAPI.new
# contacts = Mailtrap::ContactsAPI.new

# Create new contact list
list = contact_list.create(name: 'Test List')

# Get all contact lists
contact_list.list

# Update contact list
contact_list.update(list.id, name: 'Test List Updated')

# Get contact list
list = contact_list.get(list.id)

# Create new contact
contact = contacts.create(email: '[email protected]', fields: { first_name: 'John Doe' }, list_ids: [list.id])

# Get contact
contact = contacts.get(contact.id)

# Update contact using id
updated_contact = contacts.update(contact.id, email: '[email protected]', fields: { first_name: 'Jane Doe' })

# Update contact using email
contacts.update(updated_contact.data.email, email: '[email protected]', fields: { first_name: 'Jane Doe' })

# Remove contact from lists
contacts.remove_from_lists(contact.id, [list.id])

# Add contact to lists
contacts.add_to_lists(contact.id, [list.id])

# Delete contact
contacts.delete(contact.id)

# Delete contact list
contact_list.delete(list.id)
4 changes: 4 additions & 0 deletions lib/mailtrap.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,12 @@
require_relative 'mailtrap/action_mailer' if defined? ActionMailer
require_relative 'mailtrap/mail'
require_relative 'mailtrap/errors'
require_relative 'mailtrap/base_api'
require_relative 'mailtrap/version'
require_relative 'mailtrap/email_templates_api'
require_relative 'mailtrap/contacts_api'
require_relative 'mailtrap/contact_lists_api'
require_relative 'mailtrap/contact_fields_api'

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

module Mailtrap
module BaseAPI
attr_reader :account_id, :client

def self.included(base)
base.extend(ClassMethods)
end

module ClassMethods
def supported_options(options)
@supported_options = options
end

def get_supported_options # rubocop:disable Naming/AccessorMethodName
@supported_options
end
Comment on lines +16 to +18
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Remove unnecessary RuboCop disables

The RuboCop disables for Naming/AccessorMethodName are unnecessary and should be removed.

-def get_supported_options # rubocop:disable Naming/AccessorMethodName
+def get_supported_options
   @supported_options
 end

-def get_response_class # rubocop:disable Naming/AccessorMethodName
+def get_response_class
   @response_class
 end

Also applies to: 24-26

🧰 Tools
🪛 RuboCop (1.75.5)

[warning] 16-16: Unnecessary disabling of Naming/AccessorMethodName.

(Lint/RedundantCopDisableDirective)

🤖 Prompt for AI Agents
In lib/mailtrap/base_api.rb at lines 16-18 and 24-26, remove the unnecessary
RuboCop disable comments for Naming/AccessorMethodName from the
get_supported_options method and the other similar method. These disables are
not needed and should be deleted to clean up the code.


def response_class(response_class)
@response_class = response_class
end

def get_response_class # rubocop:disable Naming/AccessorMethodName
@response_class
end
end

# @param account_id [Integer] The account ID
# @param client [Mailtrap::Client] The client instance
# @raise [ArgumentError] If account_id is nil
def initialize(account_id = ENV.fetch('MAILTRAP_ACCOUNT_ID'), client = Mailtrap::Client.new)
raise ArgumentError, 'account_id is required' if account_id.nil?
Comment on lines +29 to +33
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Verify account_id parameter type consistency

The comment indicates account_id should be an Integer, but it's being fetched from ENV which returns a String. This could lead to type mismatches in API calls.


🏁 Script executed:

#!/bin/bash
# Description: Check how account_id is used across the codebase

# Check if account_id is used in string interpolation (which would work with both types)
rg -A 2 'account_id' --type ruby

# Check if there are any type conversions for account_id
ast-grep --pattern 'account_id.to_$_'

Length of output: 1980


Ensure consistent account_id typing

Currently, account_id is pulled directly from ENV.fetch, which always returns a String, even though the YARD comment says it’s an Integer—and the API client only uses it in URL interpolation ("/api/accounts/#{account_id}/…") so it never blows up at runtime. You should either:

  • Convert account_id to an Integer up front, e.g.

    def initialize(account_id = ENV.fetch('MAILTRAP_ACCOUNT_ID'), client = Mailtrap::Client.new)
      raise ArgumentError, 'account_id is required' if account_id.nil?
      @account_id = Integer(account_id)
      @client     = client
    end

    and keep the @param account_id [Integer] doc.

  • Or relax the doc to reflect that you accept both Strings and Integers, e.g.

     # @param account_id [Integer] The account ID
    -# @param account_id [Integer] The account ID
    +# @param account_id [String, Integer] The account ID (numeric value)

Pick one approach and update lib/mailtrap/base_api.rb accordingly.

🤖 Prompt for AI Agents
In lib/mailtrap/base_api.rb around lines 29 to 33, the account_id parameter is
documented as an Integer but is assigned from ENV.fetch which returns a String,
causing a type inconsistency. To fix this, convert account_id to an Integer
immediately after fetching it by wrapping it with Integer(account_id) in the
initialize method, and keep the YARD doc param as Integer to maintain
consistency.


@account_id = account_id
@client = client
end

private

def supported_options
self.class.get_supported_options
end

def response_class
self.class.get_response_class
end

def validate_options!(options, supported_options)
invalid_options = options.keys - supported_options
return if invalid_options.empty?

raise ArgumentError, "invalid options are given: #{invalid_options}, supported_options: #{supported_options}"
end

def build_entity(options, response_class)
response_class.new(options.slice(*response_class.members))
end

def base_get(id)
response = client.get("#{base_path}/#{id}")
handle_response(response)
end

def base_create(options, supported_options_override = supported_options)
validate_options!(options, supported_options_override)
response = client.post(base_path, wrap_request(options))
handle_response(response)
end

def base_update(id, options, supported_options_override = supported_options)
validate_options!(options, supported_options_override)
response = client.patch("#{base_path}/#{id}", wrap_request(options))
handle_response(response)
end

def base_delete(id)
client.delete("#{base_path}/#{id}")
end

def base_list
response = client.get(base_path)
response.map { |item| handle_response(item) }
end

def handle_response(response)
build_entity(response, response_class)
end

def wrap_request(options)
options
end

def base_path
raise NotImplementedError, 'base_path must be implemented in the including class'
end
end
end
44 changes: 44 additions & 0 deletions lib/mailtrap/contact.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# frozen_string_literal: true

module Mailtrap
# Data Transfer Object for Contact
# @see https://api-docs.mailtrap.io/docs/mailtrap-api-docs/220a54e31e5ca-contact
# @attr_reader id [String] The contact ID
# @attr_reader email [String] The contact's email address
# @attr_reader fields [Hash] Object of fields with merge tags
# @attr_reader list_ids [Array<Integer>] Array of list IDs
# @attr_reader status [String] The contact status (subscribed/unsubscribed)
# @attr_reader created_at [Integer] The creation timestamp
# @attr_reader updated_at [Integer] The last update timestamp
Contact = Struct.new(
Copy link
Contributor

@i7an i7an Jun 30, 2025

Choose a reason for hiding this comment

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

@IgorDobryn if the only motivation for resource classes it to achieve strictness, then we can do this:

contact_hash = {id: 1}
contact_hash.default_proc = proc { |_, k| raise "unknown key '#{k}'" }

Thoughts?

Copy link
Contributor

Choose a reason for hiding this comment

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

It can work, but rather unexpected. So, I'd prefer to keep separate class

:id,
:email,
:fields,
:list_ids,
:status,
:created_at,
:updated_at,
keyword_init: true
) do
# @return [Hash] The contact attributes as a hash
def to_h
super.compact
end
end

# Data Transfer Object for Contact Update Response
# @see https://api-docs.mailtrap.io/docs/mailtrap-api-docs/16eab4fff9740-contact-update-response
# @attr_reader action [String] The performed action (created/updated)
# @attr_reader data [Contact, Hash] The contact data
ContactUpdateResponse = Struct.new(:action, :data, keyword_init: true) do
def initialize(*)
super
self.data = Contact.new(data) if data.is_a?(Hash)
end

# @return [Hash] The response attributes as a hash
def to_h
super.compact
end
end
end
18 changes: 18 additions & 0 deletions lib/mailtrap/contact_field.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# frozen_string_literal: true

module Mailtrap
# Data Transfer Object for Contact Field
# @see https://api-docs.mailtrap.io/docs/mailtrap-api-docs/33efe96c91dcc-get-all-contact-fields
# @attr_reader id [Integer] The contact field ID
# @attr_reader name [String] The name of the contact field (max 80 characters)
# @attr_reader data_type [String] The data type of the field
# Allowed values: text, integer, float, boolean, date
# @attr_reader merge_tag [String] Personalize your campaigns by adding a merge tag.
# This field will be replaced with unique contact details for each recipient (max 80 characters)
ContactField = Struct.new(:id, :name, :data_type, :merge_tag, keyword_init: true) do
# @return [Hash] The contact field attributes as a hash
def to_h
super.compact
end
end
end
66 changes: 66 additions & 0 deletions lib/mailtrap/contact_fields_api.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# frozen_string_literal: true

require_relative 'contact_field'

module Mailtrap
class ContactFieldsAPI
include BaseAPI

supported_options %i[name data_type merge_tag]

response_class ContactField

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

# Creates a new contact field
# @param [Hash] options The parameters to create
# @option options [String] :name The contact field name
# @option options [String] :data_type The data type of the field
# @option options [String] :merge_tag The merge tag of the field
# @return [ContactField] Created contact field object
# @!macro api_errors
# @raise [ArgumentError] If invalid options are provided
def create(options)
base_create(options)
end

# Updates an existing contact field
# @param field_id [Integer] The contact field ID
# @param [Hash] options The parameters to update
# @option options [String] :name The contact field name
# @option options [String] :merge_tag The merge tag of the field
# @return [ContactField] Updated contact field object
# @!macro api_errors
# @raise [ArgumentError] If invalid options are provided
def update(field_id, options)
base_update(field_id, options, %i[name merge_tag])
end

# Deletes a contact field
# @param field_id [Integer] The contact field ID
# @return nil
# @!macro api_errors
def delete(field_id)
base_delete(field_id)
end

# Lists all contact fields for the account
# @return [Array<ContactField>] Array of contact field objects
# @!macro api_errors
def list
base_list
end

private

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

module Mailtrap
# Data Transfer Object for Contact List
# @see https://api-docs.mailtrap.io/docs/mailtrap-api-docs/6ec7a37234af2-contact-list
# @attr_reader id [Integer] The contact list ID
# @attr_reader name [String] The name of the contact list
ContactList = Struct.new(:id, :name, keyword_init: true) do
# @return [Hash] The contact list attributes as a hash
def to_h
super.compact
end
end
end
63 changes: 63 additions & 0 deletions lib/mailtrap/contact_lists_api.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# frozen_string_literal: true

require_relative 'contact_list'

module Mailtrap
class ContactListsAPI
include BaseAPI

supported_options %i[name]

response_class ContactList

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

# Creates a new contact list
# @param [Hash] options The parameters to create
# @option options [String] :name The contact list name
# @return [ContactList] Created contact list object
# @!macro api_errors
# @raise [ArgumentError] If invalid options are provided
def create(options)
base_create(options)
end

# Updates an existing contact list
# @param list_id [Integer] The contact list ID
# @param [Hash] options The parameters to update
# @option options [String] :name The contact list name
# @return [ContactList] Updated contact list object
# @!macro api_errors
# @raise [ArgumentError] If invalid options are provided
def update(list_id, options)
base_update(list_id, options)
end

# Deletes a contact list
# @param list_id [Integer] The contact list ID
# @return nil
# @!macro api_errors
def delete(list_id)
base_delete(list_id)
end

# Lists all contact lists for the account
# @return [Array<ContactList>] Array of contact list objects
# @!macro api_errors
def list
base_list
end

private

def base_path
"/api/accounts/#{account_id}/contacts/lists"
end
end
end
Loading