Skip to content

Commit ec7b23b

Browse files
Add extended API support (Batch, Contacts)
1 parent fee2a44 commit ec7b23b

File tree

6 files changed

+587
-0
lines changed

6 files changed

+587
-0
lines changed

lib/mailtrap.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,7 @@
44
require_relative 'mailtrap/mail'
55
require_relative 'mailtrap/errors'
66
require_relative 'mailtrap/version'
7+
require_relative 'mailtrap/resources/batch_sender'
8+
require_relative 'mailtrap/resources/contacts'
79

810
module Mailtrap; end

lib/mailtrap/api/client.rb

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# frozen_string_literal: true
2+
require 'json'
3+
require 'net/http'
4+
require 'uri'
5+
6+
module Mailtrap
7+
module Api
8+
class Client
9+
BASE_HOST = 'sandbox.api.mailtrap.io'
10+
BASE_PORT = 443
11+
12+
def initialize(token:, host: BASE_HOST, port: BASE_PORT)
13+
@token = token
14+
@host = host
15+
@port = port
16+
end
17+
18+
def get(path, params: {})
19+
uri = URI::HTTPS.build(host: @host, path: path, query: URI.encode_www_form(params))
20+
request = Net::HTTP::Get.new(uri)
21+
attach_headers(request)
22+
perform_request(uri, request)
23+
end
24+
25+
def post(path, body: {})
26+
uri = URI::HTTPS.build(host: @host, path: path)
27+
request = Net::HTTP::Post.new(uri)
28+
request.body = JSON.dump(body)
29+
attach_headers(request)
30+
perform_request(uri, request)
31+
end
32+
33+
def patch(path, body: {})
34+
uri = URI::HTTPS.build(host: @host, path: path)
35+
request = Net::HTTP::Patch.new(uri)
36+
request.body = JSON.dump(body)
37+
attach_headers(request)
38+
perform_request(uri, request)
39+
end
40+
41+
def delete(path)
42+
uri = URI::HTTPS.build(host: @host, path: path)
43+
request = Net::HTTP::Delete.new(uri)
44+
attach_headers(request)
45+
perform_request(uri, request)
46+
end
47+
48+
def batch_send(payload)
49+
post('/api/send/batch', body: payload)
50+
end
51+
52+
private
53+
54+
def attach_headers(request)
55+
request['Authorization'] = "Bearer #{@token}"
56+
request['Content-Type'] = 'application/json'
57+
request['User-Agent'] = 'mailtrap-ruby (https://github.com/railsware/mailtrap-ruby)'
58+
end
59+
60+
def perform_request(uri, request)
61+
http = Net::HTTP.new(uri.host, uri.port)
62+
http.use_ssl = true
63+
response = http.request(request)
64+
65+
unless response.is_a?(Net::HTTPSuccess)
66+
Rails.logger.warn("[Mailtrap] Request failed: #{response.code} #{response.body}")
67+
raise "Mailtrap API Error (#{response.code}): #{response.body}"
68+
end
69+
70+
body = JSON.parse(response.body, symbolize_names: true)
71+
72+
if body.is_a?(Hash) && body[:errors]
73+
Rails.logger.warn("[Mailtrap] API errors in response: #{body[:errors]}")
74+
end
75+
76+
body
77+
end
78+
79+
end
80+
end
81+
end
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
# frozen_string_literal: true
2+
3+
module Mailtrap
4+
module Resources
5+
class BatchSender
6+
REQUIRED_BASE_KEYS = %i[from subject].freeze
7+
OPTIONAL_BASE_KEYS = %i[
8+
reply_to text category headers attachments html template_uuid
9+
track_opens track_clicks
10+
].freeze
11+
ALLOWED_BASE_KEYS = (REQUIRED_BASE_KEYS + OPTIONAL_BASE_KEYS).freeze
12+
13+
ALLOWED_REQUEST_KEYS = %i[
14+
to cc bcc custom_variables template_variables template_uuid
15+
].freeze
16+
17+
REQUIRED_FROM_KEYS = %i[email].freeze
18+
OPTIONAL_FROM_KEYS = %i[name].freeze
19+
ALLOWED_FROM_KEYS = (REQUIRED_FROM_KEYS + OPTIONAL_FROM_KEYS).freeze
20+
21+
REQUIRED_TO_KEYS = %i[email].freeze
22+
OPTIONAL_TO_KEYS = %i[name].freeze
23+
ALLOWED_TO_KEYS = (REQUIRED_TO_KEYS + OPTIONAL_TO_KEYS).freeze
24+
25+
def initialize(api_client, strict_mode: true)
26+
@client = api_client
27+
@strict = strict_mode
28+
end
29+
30+
def send_emails(base:, requests:)
31+
validate_base!(base)
32+
validate_requests!(requests)
33+
payload = { base: base, requests: requests }
34+
@client.batch_send(payload)
35+
end
36+
37+
private
38+
39+
def validate_base!(base)
40+
raise ArgumentError, "Base must be a Hash" unless base.is_a?(Hash)
41+
42+
if base[:attachments]
43+
total_size = 0
44+
45+
base[:attachments].each_with_index do |attachment, i|
46+
raise ArgumentError, "Attachment ##{i + 1} must be a Hash" unless attachment.is_a?(Hash)
47+
raise ArgumentError, "Attachment ##{i + 1} missing 'filename'" unless attachment[:filename]
48+
raise ArgumentError, "Attachment ##{i + 1} missing 'content'" unless attachment[:content]
49+
50+
total_size += attachment[:content].bytesize if attachment[:content].is_a?(String)
51+
end
52+
53+
if total_size > 50 * 1024 * 1024
54+
raise ArgumentError, "Attachments exceed maximum allowed size (50MB)"
55+
end
56+
end
57+
58+
REQUIRED_BASE_KEYS.each do |key|
59+
raise ArgumentError, "Missing required base field: #{key}" unless base.key?(key)
60+
end
61+
62+
if @strict
63+
base.each_key do |key|
64+
raise ArgumentError, "Unexpected key in base: #{key}" unless ALLOWED_BASE_KEYS.include?(key)
65+
end
66+
end
67+
68+
from = base[:from]
69+
raise ArgumentError, "Base 'from' must be a Hash" unless from.is_a?(Hash)
70+
71+
REQUIRED_FROM_KEYS.each do |key|
72+
raise ArgumentError, "Missing 'from' field: #{key}" unless from.key?(key)
73+
end
74+
75+
if @strict
76+
from.each_key do |key|
77+
raise ArgumentError, "Unexpected key in from: #{key}" unless ALLOWED_FROM_KEYS.include?(key)
78+
end
79+
end
80+
end
81+
82+
def validate_requests!(requests)
83+
raise ArgumentError, "Requests must be an Array" unless requests.is_a?(Array)
84+
raise ArgumentError, "Requests array must not be empty" if requests.empty?
85+
86+
if requests.size > 500
87+
raise ArgumentError, "Too many messages in batch: max 500 allowed"
88+
end
89+
90+
requests.each_with_index do |request, index|
91+
%i[to cc bcc].each do |field|
92+
next unless request[field]
93+
94+
recipients = request[field]
95+
unless recipients.is_a?(Array) && recipients.all? { |r| r[:email].to_s.match?(/@/) }
96+
raise ArgumentError, "Invalid #{field} in request ##{index + 1}"
97+
end
98+
99+
recipients.each do |recipient|
100+
REQUIRED_TO_KEYS.each do |key|
101+
raise ArgumentError, "Missing #{field}[:#{key}] in request ##{index + 1}" unless recipient.key?(key)
102+
end
103+
104+
if @strict
105+
recipient.each_key do |key|
106+
raise ArgumentError, "Unexpected key in #{field} recipient: #{key}" unless ALLOWED_TO_KEYS.include?(key)
107+
end
108+
end
109+
end
110+
end
111+
112+
if @strict
113+
request.each_key do |key|
114+
raise ArgumentError, "Unexpected key in request ##{index + 1}: #{key}" unless ALLOWED_REQUEST_KEYS.include?(key)
115+
end
116+
end
117+
end
118+
end
119+
end
120+
end
121+
end

lib/mailtrap/resources/contacts.rb

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# frozen_string_literal: true
2+
3+
module Mailtrap
4+
module Resources
5+
class Contacts
6+
ALLOWED_CREATE_KEYS = %i[email fields list_ids].freeze
7+
ALLOWED_UPDATE_KEYS = %i[
8+
email fields list_ids_included list_ids_excluded unsubscribed
9+
].freeze
10+
11+
EXPECTED_KEYS = %i[
12+
id status email fields list_ids created_at updated_at
13+
].freeze
14+
15+
def initialize(api_client, strict_mode: false)
16+
@client = api_client
17+
@strict_mode = strict_mode
18+
end
19+
20+
def list(account_id:, page: 1, per_page: 50)
21+
response = @client.get("/api/accounts/#{account_id}/contacts", params: {
22+
page: page,
23+
per_page: per_page
24+
})
25+
validate_response_keys!(response[:data]) if @strict_mode
26+
response
27+
end
28+
29+
def find(account_id:, contact_id:)
30+
response = @client.get("/api/accounts/#{account_id}/contacts/#{contact_id}")
31+
validate_response_keys!([response]) if @strict_mode
32+
response
33+
end
34+
35+
def create(account_id:, **attrs)
36+
validate_keys!(attrs, ALLOWED_CREATE_KEYS)
37+
@client.post("/api/accounts/#{account_id}/contacts", body: {
38+
contact: attrs
39+
})
40+
end
41+
42+
def update(account_id:, contact_id:, **attrs)
43+
validate_keys!(attrs, ALLOWED_UPDATE_KEYS)
44+
@client.patch("/api/accounts/#{account_id}/contacts/#{contact_id}", body: {
45+
contact: attrs.compact
46+
})
47+
end
48+
49+
def delete(account_id:, contact_id:)
50+
@client.delete("/api/accounts/#{account_id}/contacts/#{contact_id}")
51+
end
52+
53+
private
54+
55+
def validate_keys!(input, allowed_keys)
56+
return unless @strict_mode
57+
58+
input.each_key do |key|
59+
unless allowed_keys.include?(key)
60+
raise ArgumentError, "Unexpected key in payload: #{key}"
61+
end
62+
end
63+
end
64+
65+
def validate_response_keys!(records)
66+
records.each do |record|
67+
record.each_key do |key|
68+
raise ArgumentError, "Unexpected key in response: #{key}" unless EXPECTED_KEYS.include?(key)
69+
end
70+
71+
EXPECTED_KEYS.each do |key|
72+
raise ArgumentError, "Missing key in contact object: #{key}" unless record.key?(key)
73+
end
74+
end
75+
end
76+
end
77+
end
78+
end

0 commit comments

Comments
 (0)