Skip to content

Add extended API support (Batch, Contacts) #45

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 2 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
3 changes: 3 additions & 0 deletions lib/mailtrap.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,8 @@
require_relative 'mailtrap/mail'
require_relative 'mailtrap/errors'
require_relative 'mailtrap/version'
require_relative 'mailtrap/resources/batch_sender'
require_relative 'mailtrap/resources/contacts'
require_relative 'mailtrap/resources/templates'

module Mailtrap; end
81 changes: 81 additions & 0 deletions lib/mailtrap/api/client.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# frozen_string_literal: true
require 'json'
require 'net/http'
require 'uri'

module Mailtrap
module Api
class Client
BASE_HOST = 'sandbox.api.mailtrap.io'
BASE_PORT = 443

def initialize(token:, host: BASE_HOST, port: BASE_PORT)
@token = token
@host = host
@port = port
end

def get(path, params: {})
uri = URI::HTTPS.build(host: @host, path: path, query: URI.encode_www_form(params))
request = Net::HTTP::Get.new(uri)
attach_headers(request)
perform_request(uri, request)
end

def post(path, body: {})
uri = URI::HTTPS.build(host: @host, path: path)
request = Net::HTTP::Post.new(uri)
request.body = JSON.dump(body)
attach_headers(request)
perform_request(uri, request)
end

def patch(path, body: {})
uri = URI::HTTPS.build(host: @host, path: path)
request = Net::HTTP::Patch.new(uri)
request.body = JSON.dump(body)
attach_headers(request)
perform_request(uri, request)
end

def delete(path)
uri = URI::HTTPS.build(host: @host, path: path)
request = Net::HTTP::Delete.new(uri)
attach_headers(request)
perform_request(uri, request)
end

def batch_send(payload)
post('/api/send/batch', body: payload)
end

private

def attach_headers(request)
request['Authorization'] = "Bearer #{@token}"
request['Content-Type'] = 'application/json'
request['User-Agent'] = 'mailtrap-ruby (https://github.com/railsware/mailtrap-ruby)'
end

def perform_request(uri, request)
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
response = http.request(request)

unless response.is_a?(Net::HTTPSuccess)
Rails.logger.warn("[Mailtrap] Request failed: #{response.code} #{response.body}")
raise "Mailtrap API Error (#{response.code}): #{response.body}"
end

body = JSON.parse(response.body, symbolize_names: true)

if body.is_a?(Hash) && body[:errors]
Rails.logger.warn("[Mailtrap] API errors in response: #{body[:errors]}")
end

body
end

end
end
end
121 changes: 121 additions & 0 deletions lib/mailtrap/resources/batch_sender.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# frozen_string_literal: true

module Mailtrap
module Resources
class BatchSender
REQUIRED_BASE_KEYS = %i[from subject].freeze
OPTIONAL_BASE_KEYS = %i[
reply_to text category headers attachments html template_uuid
track_opens track_clicks
].freeze
ALLOWED_BASE_KEYS = (REQUIRED_BASE_KEYS + OPTIONAL_BASE_KEYS).freeze

ALLOWED_REQUEST_KEYS = %i[
to cc bcc custom_variables template_variables template_uuid
].freeze

REQUIRED_FROM_KEYS = %i[email].freeze
OPTIONAL_FROM_KEYS = %i[name].freeze
ALLOWED_FROM_KEYS = (REQUIRED_FROM_KEYS + OPTIONAL_FROM_KEYS).freeze

REQUIRED_TO_KEYS = %i[email].freeze
OPTIONAL_TO_KEYS = %i[name].freeze
ALLOWED_TO_KEYS = (REQUIRED_TO_KEYS + OPTIONAL_TO_KEYS).freeze

def initialize(api_client, strict_mode: true)
@client = api_client
@strict = strict_mode
end

def send_emails(base:, requests:)
validate_base!(base)
validate_requests!(requests)
payload = { base: base, requests: requests }
@client.batch_send(payload)
end

private

def validate_base!(base)
raise ArgumentError, "Base must be a Hash" unless base.is_a?(Hash)

if base[:attachments]
total_size = 0

base[:attachments].each_with_index do |attachment, i|
raise ArgumentError, "Attachment ##{i + 1} must be a Hash" unless attachment.is_a?(Hash)
raise ArgumentError, "Attachment ##{i + 1} missing 'filename'" unless attachment[:filename]
raise ArgumentError, "Attachment ##{i + 1} missing 'content'" unless attachment[:content]

total_size += attachment[:content].bytesize if attachment[:content].is_a?(String)
end

if total_size > 50 * 1024 * 1024
raise ArgumentError, "Attachments exceed maximum allowed size (50MB)"
end
end

REQUIRED_BASE_KEYS.each do |key|
raise ArgumentError, "Missing required base field: #{key}" unless base.key?(key)
end

if @strict
base.each_key do |key|
raise ArgumentError, "Unexpected key in base: #{key}" unless ALLOWED_BASE_KEYS.include?(key)
end
end

from = base[:from]
raise ArgumentError, "Base 'from' must be a Hash" unless from.is_a?(Hash)

REQUIRED_FROM_KEYS.each do |key|
raise ArgumentError, "Missing 'from' field: #{key}" unless from.key?(key)
end

if @strict
from.each_key do |key|
raise ArgumentError, "Unexpected key in from: #{key}" unless ALLOWED_FROM_KEYS.include?(key)
end
end
end

def validate_requests!(requests)
raise ArgumentError, "Requests must be an Array" unless requests.is_a?(Array)
raise ArgumentError, "Requests array must not be empty" if requests.empty?

if requests.size > 500
raise ArgumentError, "Too many messages in batch: max 500 allowed"
end

requests.each_with_index do |request, index|
%i[to cc bcc].each do |field|
next unless request[field]

recipients = request[field]
unless recipients.is_a?(Array) && recipients.all? { |r| r[:email].to_s.match?(/@/) }
raise ArgumentError, "Invalid #{field} in request ##{index + 1}"
end

recipients.each do |recipient|
REQUIRED_TO_KEYS.each do |key|
raise ArgumentError, "Missing #{field}[:#{key}] in request ##{index + 1}" unless recipient.key?(key)
end

if @strict
recipient.each_key do |key|
raise ArgumentError, "Unexpected key in #{field} recipient: #{key}" unless ALLOWED_TO_KEYS.include?(key)
end
end
end
end

if @strict
request.each_key do |key|
raise ArgumentError, "Unexpected key in request ##{index + 1}: #{key}" unless ALLOWED_REQUEST_KEYS.include?(key)
end
end
end
end
end
end
end
78 changes: 78 additions & 0 deletions lib/mailtrap/resources/contacts.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# frozen_string_literal: true

module Mailtrap
module Resources
class Contacts
ALLOWED_CREATE_KEYS = %i[email fields list_ids].freeze
ALLOWED_UPDATE_KEYS = %i[
email fields list_ids_included list_ids_excluded unsubscribed
].freeze

EXPECTED_KEYS = %i[
id status email fields list_ids created_at updated_at
].freeze

def initialize(api_client, strict_mode: false)
@client = api_client
@strict_mode = strict_mode
end

def list(account_id:, page: 1, per_page: 50)
response = @client.get("/api/accounts/#{account_id}/contacts", params: {
page: page,
per_page: per_page
})
validate_response_keys!(response[:data]) if @strict_mode
response
end

def find(account_id:, contact_id:)
response = @client.get("/api/accounts/#{account_id}/contacts/#{contact_id}")
validate_response_keys!([response]) if @strict_mode
response
end

def create(account_id:, **attrs)
validate_keys!(attrs, ALLOWED_CREATE_KEYS)
@client.post("/api/accounts/#{account_id}/contacts", body: {
contact: attrs
})
end

def update(account_id:, contact_id:, **attrs)
validate_keys!(attrs, ALLOWED_UPDATE_KEYS)
@client.patch("/api/accounts/#{account_id}/contacts/#{contact_id}", body: {
contact: attrs.compact
})
end

def delete(account_id:, contact_id:)
@client.delete("/api/accounts/#{account_id}/contacts/#{contact_id}")
end

private

def validate_keys!(input, allowed_keys)
return unless @strict_mode

input.each_key do |key|
unless allowed_keys.include?(key)
raise ArgumentError, "Unexpected key in payload: #{key}"
end
end
end

def validate_response_keys!(records)
records.each do |record|
record.each_key do |key|
raise ArgumentError, "Unexpected key in response: #{key}" unless EXPECTED_KEYS.include?(key)
end

EXPECTED_KEYS.each do |key|
raise ArgumentError, "Missing key in contact object: #{key}" unless record.key?(key)
end
end
end
end
end
end
84 changes: 84 additions & 0 deletions lib/mailtrap/resources/templates.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# frozen_string_literal: true

module Mailtrap
module Resources
class Templates
ALLOWED_CREATE_KEYS = %i[name subject category body_html body_text].freeze
ALLOWED_UPDATE_KEYS = ALLOWED_CREATE_KEYS
ALLOWED_RESPONSE_KEYS = %i[
id uuid name subject category body_html body_text created_at updated_at
].freeze

def initialize(api_client, strict_mode: false)
@client = api_client
@strict_mode = strict_mode
end

def list(account_id:, page: 1, per_page: 50)
response = @client.get("/api/accounts/#{account_id}/email_templates", params: {
page: page,
per_page: per_page
})

validate_response_keys!(response[:data]) if @strict_mode
response
end

def find(account_id:, template_id:)
response = @client.get("/api/accounts/#{account_id}/email_templates/#{template_id}")
validate_response_keys!([response]) if @strict_mode
response
end

def create(account_id:, **attrs)
validate_keys!(attrs, ALLOWED_CREATE_KEYS)

@client.post("/api/accounts/#{account_id}/email_templates", body: {
email_template: attrs
})
end

def patch(account_id:, template_id:, **attrs)
validate_keys!(attrs, ALLOWED_UPDATE_KEYS)

@client.patch("/api/accounts/#{account_id}/email_templates/#{template_id}", body: {
email_template: attrs.compact
})
end

def delete(account_id:, template_id:)
@client.delete("/api/accounts/#{account_id}/email_templates/#{template_id}")
end

private

EXPECTED_KEYS = %i[id uuid name subject category body_html body_text created_at updated_at].freeze

def validate_keys!(input, allowed_keys)
return unless @strict_mode

input.each_key do |key|
unless allowed_keys.include?(key)
raise ArgumentError, "Unexpected key in payload: #{key}"
end
end
end

def validate_response_keys!(records)
records.each do |record|
record.each_key do |key|
unless EXPECTED_KEYS.include?(key)
raise ArgumentError, "Unexpected key in response: #{key}"
end
end

EXPECTED_KEYS.each do |key|
unless record.key?(key)
raise ArgumentError, "Missing key in template object: #{key}"
end
end
end
end
end
end
end
Loading