mailcheckr 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 6961901669d98e1acf873bf68523f2a6701600ccac4cca46c69e4bc7870f5da3
4
+ data.tar.gz: 207b9219579311d9c782194964b63334845e49e6b5276a58c51fd1c41fac7a87
5
+ SHA512:
6
+ metadata.gz: 47d996133599bad00402124c4935bbc02eb5bef7f741418e8cccaefeda62f14576c4960b22700470595190c74799ed1024d3f5aadcab37e36b40062f4f688c89
7
+ data.tar.gz: 1eda2d93b87fee25cc456d4212918084eeab23e49ed62a172235342383e31c760a5660ecd3a37c97438f1e7340573b8c9869cc6c3afa66560011f150dcf8f3e2
data/Gemfile ADDED
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gemspec
6
+
7
+ gem "minitest", "~> 5.0"
8
+ gem "rake"
9
+ gem "rubocop", "~> 1.0"
data/README.md ADDED
@@ -0,0 +1,134 @@
1
+ # MailCheckr Ruby SDK
2
+
3
+ [![Tests](https://github.com/Eazybright/mailcheckr/actions/workflows/test.yml/badge.svg)](https://github.com/Eazybright/mailcheckr/actions/workflows/test.yml)
4
+ [![Gem Version](https://img.shields.io/gem/v/mailcheckr.svg)](https://rubygems.org/gems/mailcheckr)
5
+ [![Gem](https://img.shields.io/gem/dt/mailcheckr.svg)](https://rubygems.org/gems/mailcheckr/)
6
+
7
+ A minimal Ruby client for the [MailCheckr API](https://mailcheckr.app/docs). It uses Ruby's
8
+ standard HTTP and JSON libraries.
9
+
10
+ ## Requirements
11
+
12
+ Ruby 3.2 or newer.
13
+
14
+ ## Installation
15
+
16
+ You can install the client library via RubyGems:
17
+
18
+ ```ruby
19
+ gem install mailcheckr
20
+ ```
21
+
22
+ or add it to your Gemfile:
23
+
24
+ ```ruby
25
+ gem "mailcheckr"
26
+ ```
27
+
28
+ Then run `bundle install`.
29
+
30
+ ## Usage
31
+
32
+ Create a client with an API key from your MailCheckr dashboard:
33
+
34
+ ```ruby
35
+ require "mailcheckr"
36
+
37
+ client = Mailcheckr::Client.new(api_key: ENV.fetch("MAILCHECKR_API_KEY"))
38
+ ```
39
+
40
+ Create a verification. MailCheckr requires an `Idempotency-Key`; use a stable
41
+ unique value when retrying the same operation. The result remains hash-like,
42
+ and `http_status` distinguishes an immediately completed response from one to
43
+ poll:
44
+
45
+ ```ruby
46
+ result = client.create_verification(
47
+ email: "person@example.com",
48
+ idempotency_key: "signup-123"
49
+ )
50
+
51
+ case result.http_status
52
+ when 200
53
+ puts result["status"]
54
+ when 202
55
+ result = client.get_verification(id: result.fetch("id"))
56
+ end
57
+ ```
58
+
59
+ Creation returns the parsed `data` object as a hash-like
60
+ `Mailcheckr::VerificationResponse`. Both `200 OK` and `202 Accepted` responses
61
+ include `http_status`; poll the returned ID after `202` until the state is
62
+ `completed` or `failed`.
63
+
64
+ The client also accepts a custom base URL and HTTP timeouts:
65
+
66
+ ```ruby
67
+ client = Mailcheckr::Client.new(
68
+ api_key: ENV.fetch("MAILCHECKR_API_KEY"),
69
+ base_url: "https://mailcheckr.app/api/v1",
70
+ open_timeout: 5,
71
+ read_timeout: 30
72
+ )
73
+ ```
74
+
75
+ ## Webhook signatures
76
+
77
+ Verify webhook deliveries using the exact raw request body, the
78
+ `X-MailCheckr-Timestamp` header, the `X-MailCheckr-Signature` header, and the
79
+ signing secret shown when configuring the webhook:
80
+
81
+ ```ruby
82
+ valid = Mailcheckr::Webhook.verify_signature(
83
+ raw_body: request.raw_post,
84
+ timestamp: request.headers["X-MailCheckr-Timestamp"],
85
+ signature: request.headers["X-MailCheckr-Signature"],
86
+ secret: ENV.fetch("MAILCHECKR_WEBHOOK_SECRET")
87
+ )
88
+
89
+ halt 401 unless valid
90
+ ```
91
+
92
+ MailCheckr signs `timestamp + "." + raw_body` with HMAC-SHA256 and sends the
93
+ hex digest as `v1=<digest>`. Verification uses a constant-time comparison and
94
+ rejects timestamps more than five minutes away from the current time. Set the
95
+ `tolerance:` option in seconds to change that window.
96
+
97
+ ## Errors
98
+
99
+ Non-2xx responses raise `Mailcheckr::APIError` (or a more specific subclass).
100
+ Errors expose `status`/`status_code`, parsed `response`, raw `response_body`,
101
+ API `code`, and `request_id` when the server supplies an `X-Request-Id` header.
102
+ Their message includes the HTTP status and API message/code when present.
103
+
104
+ ```ruby
105
+ begin
106
+ client.create_verification(email: email, idempotency_key: key)
107
+ rescue Mailcheckr::InsufficientCreditsError => error
108
+ logger.error("MailCheckr #{error.status} #{error.code}: #{error.message}")
109
+ rescue Mailcheckr::APIError => error
110
+ logger.error("MailCheckr request failed: #{error.status} #{error.response_body}")
111
+ end
112
+ ```
113
+
114
+ Documented error statuses map to `AuthenticationError` (401),
115
+ `InsufficientCreditsError` (402), `EmailVerificationRequiredError` (403),
116
+ `IdempotencyConflictError` (409), and `ValidationError` (422). All inherit
117
+ from `APIError`. Invalid JSON or an unexpected response envelope on a
118
+ successful status raises `Mailcheckr::ResponseError`. Network and timeout
119
+ errors from `Net::HTTP` are propagated to the caller.
120
+
121
+ ## Development
122
+
123
+ Run the test suite with:
124
+
125
+ ```sh
126
+ bin/setup
127
+ bundle exec rake test
128
+ ```
129
+
130
+ Start an interactive Ruby console with:
131
+
132
+ ```sh
133
+ bin/console
134
+ ```
data/Rakefile ADDED
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rake"
4
+ require "rake/testtask"
5
+
6
+ Rake::TestTask.new do |task|
7
+ task.libs << "lib"
8
+ task.pattern = "spec/**/*_spec.rb"
9
+ end
10
+
11
+ task default: :test
data/bin/console ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "mailcheckr"
6
+
7
+ # Add fixtures or initialization code here to make experimenting with the gem easier.
8
+
9
+ require "irb"
10
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Add any other automated development setup here.
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "net/http"
5
+ require "uri"
6
+ require_relative "errors"
7
+
8
+ module Mailcheckr
9
+ class VerificationResponse < Hash
10
+ attr_reader :http_status
11
+
12
+ def initialize(http_status:, data:)
13
+ super()
14
+ update(data)
15
+ @http_status = http_status
16
+ end
17
+ end
18
+
19
+ class Client
20
+ DEFAULT_BASE_URL = "https://mailcheckr.app/api/v1"
21
+
22
+ def initialize(api_key:, base_url: DEFAULT_BASE_URL, open_timeout: 5, read_timeout: 30)
23
+ raise ArgumentError, "api_key must not be empty" if api_key.nil? || api_key.empty?
24
+
25
+ @api_key = api_key
26
+ @base_url = normalize_base_url(base_url)
27
+ @open_timeout = open_timeout
28
+ @read_timeout = read_timeout
29
+ end
30
+
31
+ def create_verification(email:, idempotency_key:)
32
+ raise ArgumentError, "idempotency_key must not be empty" if idempotency_key.nil? || idempotency_key.empty?
33
+
34
+ request(
35
+ Net::HTTP::Post,
36
+ "verifications",
37
+ body: { email: email },
38
+ idempotency_key: idempotency_key
39
+ )
40
+ end
41
+
42
+ def get_verification(id:)
43
+ raise ArgumentError, "id must not be empty" if id.nil? || id.to_s.empty?
44
+
45
+ encoded_id = URI.encode_www_form_component(id.to_s).gsub("+", "%20")
46
+ request(Net::HTTP::Get, "verifications/#{encoded_id}")
47
+ end
48
+
49
+ private
50
+
51
+ def normalize_base_url(base_url)
52
+ uri = URI.parse(base_url)
53
+ unless %w[http https].include?(uri.scheme) && uri.host && !uri.userinfo && !uri.query && !uri.fragment
54
+ raise ArgumentError, "base_url must be an HTTP(S) URL without credentials, query, or fragment"
55
+ end
56
+
57
+ uri.to_s.sub(%r{/+\z}, "")
58
+ rescue URI::InvalidURIError
59
+ raise ArgumentError, "base_url must be a valid HTTP(S) URL"
60
+ end
61
+
62
+ def request(http_method, path, body: nil, idempotency_key: nil)
63
+ uri = URI.join("#{@base_url}/", path)
64
+ request = http_method.new(uri)
65
+ request["Authorization"] = "Bearer #{@api_key}"
66
+ request["Accept"] = "application/json"
67
+
68
+ unless body.nil?
69
+ request["Content-Type"] = "application/json"
70
+ request["Idempotency-Key"] = idempotency_key if idempotency_key
71
+ request.body = JSON.generate(body)
72
+ end
73
+
74
+ response = Net::HTTP.start(
75
+ uri.host,
76
+ uri.port,
77
+ use_ssl: uri.scheme == "https",
78
+ open_timeout: @open_timeout,
79
+ read_timeout: @read_timeout
80
+ ) { |http| http.request(request) }
81
+
82
+ status = response.code.to_i
83
+ response_body = response.body.to_s
84
+ request_id = response["X-Request-Id"]
85
+ unless (200..299).cover?(status)
86
+ raise APIError.for_response(status: status, response_body: response_body, request_id: request_id)
87
+ end
88
+
89
+ parsed_body = JSON.parse(response_body)
90
+ data = parsed_body.is_a?(Hash) ? parsed_body["data"] : nil
91
+ raise ResponseError.new(status: status, response_body: response_body) unless data.is_a?(Hash)
92
+
93
+ VerificationResponse.new(http_status: status, data: data)
94
+ rescue JSON::ParserError
95
+ raise ResponseError.new(status: status, response_body: response_body)
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Mailcheckr
6
+ class Error < StandardError; end
7
+
8
+ class APIError < Error
9
+ attr_reader :status, :response_body, :response, :code, :request_id
10
+ alias status_code status
11
+
12
+ def self.for_response(status:, response_body:, request_id: nil)
13
+ error_class = {
14
+ 401 => AuthenticationError,
15
+ 402 => InsufficientCreditsError,
16
+ 403 => EmailVerificationRequiredError,
17
+ 409 => IdempotencyConflictError,
18
+ 422 => ValidationError
19
+ }[status] || self
20
+ error_class.new(status: status, response_body: response_body, request_id: request_id)
21
+ end
22
+
23
+ def initialize(status:, response_body:, request_id: nil)
24
+ @status = status
25
+ @response_body = response_body
26
+ @request_id = request_id
27
+ @response = parse_response(response_body)
28
+ error_details = response.is_a?(Hash) ? response["error"] : nil
29
+ @code = response.is_a?(Hash) ? response["code"] : nil
30
+ @code ||= error_details["code"] if error_details.is_a?(Hash)
31
+ @code ||= error_details if error_details.is_a?(String)
32
+ message_detail = response.is_a?(Hash) ? response["message"] : nil
33
+ message_detail ||= error_details["message"] if error_details.is_a?(Hash)
34
+
35
+ message = "MailCheckr API request failed with HTTP #{status}"
36
+ detail = [message_detail, code].find { |value| value.is_a?(String) && !value.empty? }
37
+ message = "#{message}: #{detail}" if detail
38
+ message = "#{message} (request ID: #{request_id})" if request_id && !request_id.empty?
39
+ super(message)
40
+ end
41
+
42
+ private
43
+
44
+ def parse_response(body)
45
+ JSON.parse(body)
46
+ rescue JSON::ParserError
47
+ nil
48
+ end
49
+ end
50
+
51
+ class AuthenticationError < APIError; end
52
+ class InsufficientCreditsError < APIError; end
53
+ class EmailVerificationRequiredError < APIError; end
54
+ class IdempotencyConflictError < APIError; end
55
+ class ValidationError < APIError; end
56
+
57
+ class ResponseError < Error
58
+ attr_reader :status, :response_body
59
+
60
+ def initialize(status:, response_body:)
61
+ @status = status
62
+ @response_body = response_body
63
+ super("MailCheckr API returned invalid JSON with HTTP #{status}")
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mailcheckr
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "openssl"
4
+
5
+ module Mailcheckr
6
+ module Webhook
7
+ DEFAULT_TOLERANCE = 300
8
+
9
+ def self.verify_signature(raw_body:, timestamp:, signature:, secret:, tolerance: DEFAULT_TOLERANCE, now: Time.now)
10
+ tolerance = Integer(tolerance)
11
+ raise ArgumentError, "tolerance must not be negative" if tolerance.negative?
12
+
13
+ return false unless raw_body.is_a?(String) && secret.is_a?(String) && !secret.empty?
14
+ return false unless timestamp.is_a?(String) && timestamp.match?(/\A\d+\z/)
15
+ return false unless signature.is_a?(String) && signature.match?(/\Av1=[a-fA-F0-9]{64}\z/)
16
+
17
+ timestamp_value = Integer(timestamp, 10)
18
+ return false if (now.to_i - timestamp_value).abs > tolerance
19
+
20
+ expected = OpenSSL::HMAC.hexdigest("SHA256", secret, "#{timestamp}.#{raw_body}")
21
+ OpenSSL.fixed_length_secure_compare(expected, signature.delete_prefix("v1=").downcase)
22
+ end
23
+ end
24
+ end
data/lib/mailcheckr.rb ADDED
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "mailcheckr/version"
4
+ require_relative "mailcheckr/errors"
5
+ require_relative "mailcheckr/client"
6
+ require_relative "mailcheckr/webhook"
@@ -0,0 +1,159 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "minitest/autorun"
4
+ require "minitest/mock"
5
+ require "mailcheckr"
6
+
7
+ class MailcheckrClientTest < Minitest::Test
8
+ Response = Struct.new(:code, :body, :headers) do
9
+ def [](name)
10
+ headers[name]
11
+ end
12
+ end
13
+
14
+ class FakeHTTP
15
+ attr_reader :last_request
16
+
17
+ def initialize(response)
18
+ @response = response
19
+ end
20
+
21
+ def request(request)
22
+ @last_request = request
23
+ @response
24
+ end
25
+ end
26
+
27
+ def with_response(code: "200", body: '{"data":{"id":"verification-123","state":"completed"}}', request_id: nil)
28
+ headers = request_id ? { "X-Request-Id" => request_id } : {}
29
+ http = FakeHTTP.new(Response.new(code, body, headers))
30
+ start = lambda do |_host, _port, **_options, &block|
31
+ block.call(http)
32
+ end
33
+
34
+ Net::HTTP.stub(:start, start) { yield http }
35
+ end
36
+
37
+ def test_create_verification_sends_bearer_auth_idempotency_key_and_email
38
+ with_response do |http|
39
+ result = Mailcheckr::Client.new(api_key: "secret").create_verification(
40
+ email: "person@example.com",
41
+ idempotency_key: "verify-person-123"
42
+ )
43
+
44
+ assert_equal "verification-123", result["id"]
45
+ assert_equal 200, result.http_status
46
+ assert_equal "Bearer secret", http.last_request["Authorization"]
47
+ assert_equal "verify-person-123", http.last_request["Idempotency-Key"]
48
+ assert_equal "application/json", http.last_request["Content-Type"]
49
+ assert_equal({ "email" => "person@example.com" }, JSON.parse(http.last_request.body))
50
+ assert_equal "/api/v1/verifications", http.last_request.path
51
+ end
52
+ end
53
+
54
+ def test_get_verification_quotes_id_as_a_path_segment
55
+ with_response do |http|
56
+ Mailcheckr::Client.new(api_key: "secret").get_verification(id: "verify/123")
57
+
58
+ assert_equal "/api/v1/verifications/verify%2F123", http.last_request.path
59
+ assert_nil http.last_request["Idempotency-Key"]
60
+ assert_equal "Bearer secret", http.last_request["Authorization"]
61
+ end
62
+ end
63
+
64
+ def test_accepts_a_202_response_for_async_verification
65
+ with_response(code: "202", body: '{"data":{"id":"verification-123","state":"processing"}}') do
66
+ result = Mailcheckr::Client.new(api_key: "secret").create_verification(
67
+ email: "person@example.com",
68
+ idempotency_key: "verify-person-123"
69
+ )
70
+
71
+ assert_equal "processing", result["state"]
72
+ assert_equal 202, result.http_status
73
+ end
74
+ end
75
+
76
+ def test_raises_api_error_for_non_success_responses
77
+ with_response(
78
+ code: "409",
79
+ body: '{"error":"idempotency_conflict"}',
80
+ request_id: "req_123"
81
+ ) do
82
+ error = assert_raises(Mailcheckr::APIError) do
83
+ Mailcheckr::Client.new(api_key: "secret").create_verification(
84
+ email: "person@example.com",
85
+ idempotency_key: "verify-person-123"
86
+ )
87
+ end
88
+
89
+ assert_equal 409, error.status
90
+ assert_equal 409, error.status_code
91
+ assert_equal "idempotency_conflict", error.code
92
+ assert_equal "req_123", error.request_id
93
+ assert_equal '{"error":"idempotency_conflict"}', error.response_body
94
+ end
95
+ end
96
+
97
+ def test_maps_documented_error_statuses_to_specific_error_classes
98
+ cases = {
99
+ "401" => Mailcheckr::AuthenticationError,
100
+ "402" => Mailcheckr::InsufficientCreditsError,
101
+ "403" => Mailcheckr::EmailVerificationRequiredError,
102
+ "409" => Mailcheckr::IdempotencyConflictError,
103
+ "422" => Mailcheckr::ValidationError
104
+ }
105
+
106
+ cases.each do |status, error_class|
107
+ with_response(code: status, body: '{"code":"api_error","message":"Request rejected"}') do
108
+ error = assert_raises(error_class) do
109
+ Mailcheckr::Client.new(api_key: "secret").get_verification(id: "verification-123")
110
+ end
111
+
112
+ assert_equal status.to_i, error.status
113
+ assert_equal "api_error", error.code
114
+ assert_equal "Request rejected", error.response["message"]
115
+ assert_includes error.message, "Request rejected"
116
+ end
117
+ end
118
+ end
119
+
120
+ def test_extracts_error_details_from_nested_error_object
121
+ with_response(code: "402", body: '{"error":{"code":"insufficient_credits","message":"No credits left"}}') do
122
+ error = assert_raises(Mailcheckr::InsufficientCreditsError) do
123
+ Mailcheckr::Client.new(api_key: "secret").get_verification(id: "verification-123")
124
+ end
125
+
126
+ assert_equal "insufficient_credits", error.code
127
+ assert_includes error.message, "No credits left"
128
+ end
129
+ end
130
+
131
+ def test_raises_response_error_for_invalid_json
132
+ with_response(body: "not json") do
133
+ error = assert_raises(Mailcheckr::ResponseError) do
134
+ Mailcheckr::Client.new(api_key: "secret").get_verification(id: "verification-123")
135
+ end
136
+
137
+ assert_equal 200, error.status
138
+ assert_equal "not json", error.response_body
139
+ end
140
+ end
141
+
142
+ def test_raises_response_error_when_data_envelope_is_missing
143
+ with_response(body: '{"id":"verification-123","state":"completed"}') do
144
+ error = assert_raises(Mailcheckr::ResponseError) do
145
+ Mailcheckr::Client.new(api_key: "secret").get_verification(id: "verification-123")
146
+ end
147
+
148
+ assert_equal 200, error.status
149
+ end
150
+ end
151
+
152
+ def test_requires_an_api_key_and_idempotency_key
153
+ assert_raises(ArgumentError) { Mailcheckr::Client.new(api_key: "") }
154
+ client = Mailcheckr::Client.new(api_key: "secret")
155
+ assert_raises(ArgumentError) do
156
+ client.create_verification(email: "person@example.com", idempotency_key: "")
157
+ end
158
+ end
159
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "minitest/autorun"
4
+ require "openssl"
5
+ require "mailcheckr/webhook"
6
+
7
+ class MailcheckrWebhookSpec < Minitest::Test
8
+ SECRET = "webhook-secret"
9
+ RAW_BODY = '{"id":"event_123","type":"verification.completed"}'
10
+ NOW = Time.at(1_800_000_000)
11
+
12
+ def signed_headers(timestamp: NOW.to_i.to_s, raw_body: RAW_BODY)
13
+ signed_payload = "#{timestamp}.#{raw_body}"
14
+ signature = OpenSSL::HMAC.hexdigest("SHA256", SECRET, signed_payload)
15
+ { timestamp: timestamp, signature: "v1=#{signature}" }
16
+ end
17
+
18
+ def verify(raw_body: RAW_BODY, timestamp: NOW.to_i.to_s, signature: signed_headers[:signature], secret: SECRET,
19
+ **)
20
+ Mailcheckr::Webhook.verify_signature(
21
+ raw_body: raw_body,
22
+ timestamp: timestamp,
23
+ signature: signature,
24
+ secret: secret,
25
+ now: NOW,
26
+ **
27
+ )
28
+ end
29
+
30
+ def test_accepts_a_valid_signature
31
+ headers = signed_headers
32
+
33
+ assert Mailcheckr::Webhook.verify_signature(
34
+ raw_body: RAW_BODY,
35
+ timestamp: headers[:timestamp],
36
+ signature: headers[:signature],
37
+ secret: SECRET,
38
+ now: NOW
39
+ )
40
+ end
41
+
42
+ def test_rejects_a_modified_body_or_signature
43
+ refute verify(raw_body: "#{RAW_BODY} ")
44
+ refute verify(signature: "v1=#{"0" * 64}")
45
+ end
46
+
47
+ def test_rejects_timestamps_outside_tolerance
48
+ stale = (NOW.to_i - Mailcheckr::Webhook::DEFAULT_TOLERANCE - 1).to_s
49
+ future = (NOW.to_i + Mailcheckr::Webhook::DEFAULT_TOLERANCE + 1).to_s
50
+
51
+ refute verify(timestamp: stale, signature: signed_headers(timestamp: stale)[:signature])
52
+ refute verify(timestamp: future, signature: signed_headers(timestamp: future)[:signature])
53
+ end
54
+
55
+ def test_accepts_timestamp_inside_custom_tolerance
56
+ timestamp = (NOW.to_i - 600).to_s
57
+ signature = signed_headers(timestamp: timestamp)[:signature]
58
+
59
+ assert verify(timestamp: timestamp, signature: signature, tolerance: 600)
60
+ end
61
+
62
+ def test_rejects_malformed_headers_and_empty_secret
63
+ refute verify(timestamp: "not-a-timestamp")
64
+ refute verify(signature: "sha256=invalid")
65
+ refute verify(secret: "")
66
+ end
67
+
68
+ def test_rejects_negative_tolerance
69
+ assert_raises(ArgumentError) { verify(tolerance: -1) }
70
+ end
71
+ end
metadata ADDED
@@ -0,0 +1,60 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mailcheckr
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Kolawole Ezekiel
8
+ - Ridwan Kasim
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2026-10-31 00:00:00.000000000 Z
13
+ dependencies: []
14
+ description: A Ruby SDK for creating and retrieving MailCheckr verifications.
15
+ email:
16
+ - doneazy911@gmail.com
17
+ - kasimsheyi20@gmail.com
18
+ executables: []
19
+ extensions: []
20
+ extra_rdoc_files: []
21
+ files:
22
+ - Gemfile
23
+ - README.md
24
+ - Rakefile
25
+ - bin/console
26
+ - bin/setup
27
+ - lib/mailcheckr.rb
28
+ - lib/mailcheckr/client.rb
29
+ - lib/mailcheckr/errors.rb
30
+ - lib/mailcheckr/version.rb
31
+ - lib/mailcheckr/webhook.rb
32
+ - spec/client_spec.rb
33
+ - spec/webhook_spec.rb
34
+ homepage: https://mailcheckr.app/docs
35
+ licenses:
36
+ - MIT
37
+ metadata:
38
+ homepage_uri: https://mailcheckr.app/docs
39
+ source_code_uri: https://github.com/Eazybright/mailcheckr
40
+ rubygems_mfa_required: 'true'
41
+ post_install_message:
42
+ rdoc_options: []
43
+ require_paths:
44
+ - lib
45
+ required_ruby_version: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: '3.2'
50
+ required_rubygems_version: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ requirements: []
56
+ rubygems_version: 3.4.1
57
+ signing_key:
58
+ specification_version: 4
59
+ summary: Ruby client for the MailCheckr API
60
+ test_files: []