helo-email-sdk 1.0.0.pre.beta.6 → 1.0.0.pre.beta.7

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 68419322a6f7e1bb6857bbb9e38f76ca48ea993bd31b1ac11756502b6ee54df6
4
- data.tar.gz: c2e11364e91c861ea02c07afa73f5a2a6ee9b930c5e905c01ad6be8637a8b117
3
+ metadata.gz: cbf62a1f7adb7ad672a9ddf17441cc9cda10e7c199775a4bf3a94785e2f3ba3d
4
+ data.tar.gz: 434a84d00c0104fb81211d0c105293f2dc1271b59a8872003b761d151247249d
5
5
  SHA512:
6
- metadata.gz: 249cbacbb3dbba114acc15bca15ebbbb0c530c401f67e332ca8043f6520b6bbbddee848293bf0189c226084ec365a1309e054bc7a90aadb77d0dcf9d943a23c2
7
- data.tar.gz: ed1b2ddb57aebe804fe6d75223cd4f8f002c4d0fbdda2cb9cbf0ca9e1b86e180d01d6de6cb8851606fd978afa36fc6c2934b9b102870e097e96c9210615924bc
6
+ metadata.gz: 5bd2ba8cd121447ead38cbd6916ccdbbd5c755afd4035d0c550fab2c82220ec2f56d80f2ebe5bc23a9352acd21bd7f2f68867a0c3feddfeca322071c51c8bb4a
7
+ data.tar.gz: 1f3bb34bbcfafc14d5d7d2ec10e9c74a9bc924db20b240e639d30f42423cd70e48f9e9fda9b0736ebc683061cbd9082faa681952131449e01645330e89b82b96
data/README.md ADDED
@@ -0,0 +1,125 @@
1
+ # helo-email-sdk
2
+
3
+ Helo API
4
+
5
+ ## Installation
6
+
7
+ Add the gem to your Gemfile:
8
+
9
+ ```ruby
10
+ gem "helo-email-sdk"
11
+ ```
12
+
13
+ Then run:
14
+
15
+ ```bash
16
+ bundle install
17
+ ```
18
+
19
+ ## Configuration
20
+
21
+ Configure the SDK once, at boot:
22
+
23
+ ```ruby
24
+ require "helo-email-sdk"
25
+
26
+ Helo.configure do |config|
27
+ config.api_key = ENV.fetch("HELO_API_KEY")
28
+ config.base_url = "https://api.helohq.com" # optional, this is the default
29
+ end
30
+ ```
31
+
32
+ ## Usage
33
+
34
+ Each API is a class with class-level methods. Responses come back as model objects with
35
+ typed attribute readers.
36
+
37
+ ```ruby
38
+ result = Helo::Channels.list
39
+ ```
40
+
41
+ Methods that take parameters accept a hash:
42
+
43
+ ```ruby
44
+ result = Helo::Channels.list(limit: 10)
45
+ ```
46
+
47
+ See the [API docs](#apis) for every method, with a runnable example each.
48
+
49
+ ### Errors
50
+
51
+ Failed requests raise `Helo::APIError`:
52
+
53
+ ```ruby
54
+ begin
55
+ Helo::Channels.list
56
+ rescue Helo::APIError => e
57
+ e.code # HTTP status
58
+ e.detail # human-readable detail from the API
59
+ e.errors # field-level validation errors, when present
60
+ end
61
+ ```
62
+
63
+ ## Webhook signature verification
64
+
65
+ Webhook deliveries are signed with the endpoint's signing key. Verify every delivery before
66
+ acting on it, against the **raw** request body — parsing and re-serializing the JSON changes
67
+ the bytes and the signature will not match.
68
+
69
+ ```ruby
70
+ class WebhooksController < ApplicationController
71
+ skip_before_action :verify_authenticity_token
72
+
73
+ def create
74
+ Helo::WebhookSignatures.verify!(
75
+ request.headers["X-Helo-Webhook-Signature"],
76
+ request.raw_post, # raw body, exactly as received
77
+ ENV.fetch("HELO_WEBHOOK_SIGNING_KEY")
78
+ )
79
+
80
+ event = JSON.parse(request.raw_post)
81
+ # ... handle the event, then acknowledge quickly
82
+ head :no_content
83
+ rescue Helo::WebhookSignatures::Error
84
+ head :bad_request
85
+ end
86
+ end
87
+ ```
88
+
89
+ `verify!` returns `true` when the signature is valid and raises otherwise. Each rejection has
90
+ its own class, so a stale delivery can be treated differently from a genuinely bad one:
91
+
92
+ | Exception | Meaning |
93
+ | --- | --- |
94
+ | `MalformedHeaderError` | The header was not in the expected format |
95
+ | `UnsupportedVersionError` | The delivery used a signing scheme this SDK version cannot verify — upgrade the gem |
96
+ | `TimestampSkewError` | Correctly signed, but too old to accept — possible replay, or clock drift |
97
+ | `SignatureMismatchError` | Wrong signing key, or the body was modified in transit |
98
+
99
+ All four inherit from `Helo::WebhookSignatures::Error`, so `rescue` that one class
100
+ to catch any rejection. If you only want a boolean, use `valid?` instead:
101
+
102
+ ```ruby
103
+ if Helo::WebhookSignatures.valid?(signature_header, raw_body, signing_key)
104
+ # ...
105
+ end
106
+ ```
107
+
108
+ The signature header may carry several versions at once (`t=...,v1=...,v2=...`) while a new
109
+ signing scheme is being rolled out. This SDK verifies against the newest version it supports
110
+ (`SUPPORTED_VERSIONS`) and ignores elements it does not recognize, so a rollout will not break
111
+ this integration.
112
+
113
+ To compute a signature yourself — signing a fixture in tests, for example — use
114
+ `Helo::WebhookSignatures.generate(payload, signing_key, timestamp)`.
115
+
116
+ ## APIs
117
+
118
+ - [Helo::Channels](docs/Channels.md)
119
+ - [Helo::Activity](docs/Activity.md)
120
+ - [Helo::Domains](docs/Domains.md)
121
+ - [Helo::Sending](docs/Sending.md)
122
+ - [Helo::Broadcasts](docs/Broadcasts.md)
123
+ - [Helo::Statistics](docs/Statistics.md)
124
+ - [Helo::Suppressions](docs/Suppressions.md)
125
+ - [Helo::Webhooks](docs/Webhooks.md)
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Helo
4
+ class BouncedDetails
5
+ include Helo::Core::Model
6
+
7
+ api_attribute :type, :string, key: "type"
8
+ api_attribute :sub_type, :string, key: "subType"
9
+ api_attribute :code, :string, key: "code"
10
+ end
11
+ end
@@ -5,8 +5,8 @@ module Helo
5
5
  include Helo::Core::Model
6
6
 
7
7
  api_attribute :event_type, :string, key: "eventType"
8
+ api_attribute :details, Helo::Core::ModelType.new("Helo::BouncedDetails"), key: "details"
8
9
  api_attribute :recipient, :string, key: "recipient"
9
- api_attribute :details, :string, key: "details"
10
10
  api_attribute :message_id, :string, key: "messageId"
11
11
  api_attribute :channel_id, :string, key: "channelId"
12
12
  api_attribute :mail_type, :string, key: "mailType"
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Helo
4
+ class ClickedDetails
5
+ include Helo::Core::Model
6
+
7
+ api_attribute :link, :string, key: "link"
8
+ api_attribute :ip, :string, key: "ip"
9
+ api_attribute :country, :string, key: "country"
10
+ api_attribute :country_code, :string, key: "countryCode"
11
+ api_attribute :client, Helo::Core::ModelType.new("Helo::ClientDetails"), key: "client"
12
+ api_attribute :device, Helo::Core::ModelType.new("Helo::DeviceDetails"), key: "device"
13
+ end
14
+ end
@@ -5,8 +5,8 @@ module Helo
5
5
  include Helo::Core::Model
6
6
 
7
7
  api_attribute :event_type, :string, key: "eventType"
8
+ api_attribute :details, Helo::Core::ModelType.new("Helo::ClickedDetails"), key: "details"
8
9
  api_attribute :recipient, :string, key: "recipient"
9
- api_attribute :details, :string, key: "details"
10
10
  api_attribute :message_id, :string, key: "messageId"
11
11
  api_attribute :channel_id, :string, key: "channelId"
12
12
  api_attribute :mail_type, :string, key: "mailType"
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Helo
4
+ class ClientDetails
5
+ include Helo::Core::Model
6
+
7
+ api_attribute :family, :string, key: "family"
8
+ api_attribute :version, :string, key: "version"
9
+ end
10
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Helo
4
+ class ComplainedDetails
5
+ include Helo::Core::Model
6
+
7
+ api_attribute :type, :string, key: "type"
8
+ end
9
+ end
@@ -5,8 +5,8 @@ module Helo
5
5
  include Helo::Core::Model
6
6
 
7
7
  api_attribute :event_type, :string, key: "eventType"
8
+ api_attribute :details, Helo::Core::ModelType.new("Helo::ComplainedDetails"), key: "details"
8
9
  api_attribute :recipient, :string, key: "recipient"
9
- api_attribute :details, :string, key: "details"
10
10
  api_attribute :message_id, :string, key: "messageId"
11
11
  api_attribute :channel_id, :string, key: "channelId"
12
12
  api_attribute :mail_type, :string, key: "mailType"
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Helo
4
+ class DeliveredDetails
5
+ include Helo::Core::Model
6
+
7
+ api_attribute :response, :string, key: "response"
8
+ end
9
+ end
@@ -5,8 +5,8 @@ module Helo
5
5
  include Helo::Core::Model
6
6
 
7
7
  api_attribute :event_type, :string, key: "eventType"
8
+ api_attribute :details, Helo::Core::ModelType.new("Helo::DeliveredDetails"), key: "details"
8
9
  api_attribute :recipient, :string, key: "recipient"
9
- api_attribute :details, :string, key: "details"
10
10
  api_attribute :message_id, :string, key: "messageId"
11
11
  api_attribute :channel_id, :string, key: "channelId"
12
12
  api_attribute :mail_type, :string, key: "mailType"
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Helo
4
+ class DeviceDetails
5
+ include Helo::Core::Model
6
+
7
+ api_attribute :brand, :string, key: "brand"
8
+ api_attribute :family, :string, key: "family"
9
+ api_attribute :model, :string, key: "model"
10
+ end
11
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Helo
4
+ class EngagementDetails
5
+ include Helo::Core::Model
6
+
7
+ api_attribute :ip, :string, key: "ip"
8
+ api_attribute :country, :string, key: "country"
9
+ api_attribute :country_code, :string, key: "countryCode"
10
+ api_attribute :client, Helo::Core::ModelType.new("Helo::ClientDetails"), key: "client"
11
+ api_attribute :device, Helo::Core::ModelType.new("Helo::DeviceDetails"), key: "device"
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Helo
4
+ class OpenedDetails
5
+ include Helo::Core::Model
6
+
7
+ api_attribute :ip, :string, key: "ip"
8
+ api_attribute :country, :string, key: "country"
9
+ api_attribute :country_code, :string, key: "countryCode"
10
+ api_attribute :client, Helo::Core::ModelType.new("Helo::ClientDetails"), key: "client"
11
+ api_attribute :device, Helo::Core::ModelType.new("Helo::DeviceDetails"), key: "device"
12
+ end
13
+ end
@@ -5,8 +5,8 @@ module Helo
5
5
  include Helo::Core::Model
6
6
 
7
7
  api_attribute :event_type, :string, key: "eventType"
8
+ api_attribute :details, Helo::Core::ModelType.new("Helo::OpenedDetails"), key: "details"
8
9
  api_attribute :recipient, :string, key: "recipient"
9
- api_attribute :details, :string, key: "details"
10
10
  api_attribute :message_id, :string, key: "messageId"
11
11
  api_attribute :channel_id, :string, key: "channelId"
12
12
  api_attribute :mail_type, :string, key: "mailType"
@@ -5,6 +5,5 @@ module Helo
5
5
  include Helo::Core::Model
6
6
 
7
7
  api_attribute :recipient, :string, key: "recipient"
8
- api_attribute :details, :string, key: "details"
9
8
  end
10
9
  end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Helo
4
+ class ResubscribedDetails
5
+ include Helo::Core::Model
6
+
7
+ api_attribute :ip, :string, key: "ip"
8
+ end
9
+ end
@@ -5,8 +5,8 @@ module Helo
5
5
  include Helo::Core::Model
6
6
 
7
7
  api_attribute :event_type, :string, key: "eventType"
8
+ api_attribute :details, Helo::Core::ModelType.new("Helo::ResubscribedDetails"), key: "details"
8
9
  api_attribute :recipient, :string, key: "recipient"
9
- api_attribute :details, :string, key: "details"
10
10
  api_attribute :message_id, :string, key: "messageId"
11
11
  api_attribute :channel_id, :string, key: "channelId"
12
12
  api_attribute :mail_type, :string, key: "mailType"
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Helo
4
+ class UnsubscribedDetails
5
+ include Helo::Core::Model
6
+
7
+ api_attribute :ip, :string, key: "ip"
8
+ end
9
+ end
@@ -5,8 +5,8 @@ module Helo
5
5
  include Helo::Core::Model
6
6
 
7
7
  api_attribute :event_type, :string, key: "eventType"
8
+ api_attribute :details, Helo::Core::ModelType.new("Helo::UnsubscribedDetails"), key: "details"
8
9
  api_attribute :recipient, :string, key: "recipient"
9
- api_attribute :details, :string, key: "details"
10
10
  api_attribute :message_id, :string, key: "messageId"
11
11
  api_attribute :channel_id, :string, key: "channelId"
12
12
  api_attribute :mail_type, :string, key: "mailType"
data/lib/helo/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Helo
4
- VERSION = "1.0.0-beta.6"
4
+ VERSION = "1.0.0-beta.7"
5
5
  end
@@ -0,0 +1,155 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "openssl"
4
+
5
+ module Helo
6
+ # Helpers for verifying the signature on an incoming webhook request.
7
+ module WebhookSignatures
8
+ # Base class for every rejection reason, so callers that do not care why a
9
+ # webhook was rejected can rescue this one class.
10
+ class Error < StandardError; end
11
+
12
+ # The header was not in the documented
13
+ # t={timestamp},v{version}={signature} form.
14
+ class MalformedHeaderError < Error; end
15
+
16
+ # The header carried only signing schemes this SDK does not know how to
17
+ # verify. Upgrading the SDK is the fix; see SUPPORTED_VERSIONS.
18
+ class UnsupportedVersionError < Error; end
19
+
20
+ # The signature was correctly formed but its timestamp is too far from the
21
+ # current time, so it may be a replay.
22
+ class TimestampSkewError < Error; end
23
+
24
+ # The signature did not match the body, either because the body was tampered
25
+ # with or the signing key is wrong.
26
+ class SignatureMismatchError < Error; end
27
+
28
+ # Signing schemes this SDK can verify. The signature header may carry several
29
+ # versions at once (t=...,v1=...,v2=...) so that a new scheme can be rolled
30
+ # out while receivers upgrade; verification uses the newest version present
31
+ # that appears in this list, and ignores the rest.
32
+ SUPPORTED_VERSIONS = [1].freeze
33
+
34
+ TIMESTAMP_VALUE_REGEX = /\A\d+\z/
35
+ SIGNATURE_KEY_REGEX = /\Av(\d+)\z/
36
+ HEX_SIGNATURE_REGEX = /\A[a-f0-9]+\z/
37
+ MAX_TIMESTAMP_SKEW_SECONDS = 300 # 5 minutes
38
+
39
+ class << self
40
+ # Verify a webhook signature header against the raw request body.
41
+ #
42
+ # @param signature_header [String] value of the signature header sent with the webhook
43
+ # @param request_body [String] raw (unparsed) request body
44
+ # @param signing_key [String] signing key for the webhook endpoint
45
+ # @return [true] when the signature is valid
46
+ # @raise [Error] a subclass describing why the signature was rejected
47
+ def verify!(signature_header, request_body, signing_key)
48
+ timestamp, signatures = parse_header(signature_header)
49
+
50
+ version = newest_supported_version(signatures)
51
+ unless version
52
+ raise UnsupportedVersionError,
53
+ "Unsupported webhook signature version: header carries only " \
54
+ "#{signatures.keys.sort.map { |v| "v#{v}" }.join(', ')}"
55
+ end
56
+
57
+ skew = (Time.now.to_i - timestamp.to_i).abs
58
+ if skew > MAX_TIMESTAMP_SKEW_SECONDS
59
+ raise TimestampSkewError,
60
+ "Webhook signature timestamp outside tolerance: off by #{skew}s, " \
61
+ "tolerance is #{MAX_TIMESTAMP_SKEW_SECONDS}s"
62
+ end
63
+
64
+ computed = signature_for_version(version, request_body, signing_key, timestamp)
65
+ unless signatures.fetch(version).any? { |signature| secure_compare(computed, signature) }
66
+ raise SignatureMismatchError, "Webhook signature mismatch"
67
+ end
68
+
69
+ true
70
+ end
71
+
72
+ # Verify a webhook signature header, returning false instead of raising.
73
+ #
74
+ # @see #verify!
75
+ # @return [Boolean]
76
+ def valid?(signature_header, request_body, signing_key)
77
+ verify!(signature_header, request_body, signing_key)
78
+ rescue Error
79
+ false
80
+ end
81
+
82
+ # Compute the hex-encoded HMAC-SHA256 signature for a webhook payload,
83
+ # using the v1 signing scheme.
84
+ #
85
+ # @param payload [String] raw (unparsed) request body
86
+ # @param key [String] signing key for the webhook endpoint
87
+ # @param timestamp [String] unix timestamp in seconds, as sent in the signature header
88
+ # @return [String]
89
+ def generate(payload, key, timestamp)
90
+ OpenSSL::HMAC.hexdigest("SHA256", key, "#{timestamp}.#{payload}")
91
+ end
92
+
93
+ private
94
+
95
+ # Compute the signature for one signing scheme. This is the single place a
96
+ # new scheme needs to be added.
97
+ def signature_for_version(version, payload, key, timestamp)
98
+ case version
99
+ when 1 then generate(payload, key, timestamp)
100
+ end
101
+ end
102
+
103
+ # Split the header into its timestamp and its signatures keyed by version.
104
+ # Elements that are not recognized are ignored, so that a sender adding new
105
+ # elements does not break verification here.
106
+ def parse_header(signature_header)
107
+ timestamp = nil
108
+ signatures = Hash.new { |hash, key| hash[key] = [] }
109
+
110
+ signature_header.to_s.split(",").each do |element|
111
+ key, value = element.strip.split("=", 2)
112
+ next if value.nil?
113
+
114
+ if key == "t"
115
+ raise MalformedHeaderError, "Malformed webhook signature header" unless TIMESTAMP_VALUE_REGEX.match?(value)
116
+
117
+ timestamp = value
118
+ next
119
+ end
120
+
121
+ match = SIGNATURE_KEY_REGEX.match(key)
122
+ next unless match
123
+
124
+ version = match[1].to_i
125
+
126
+ # Only versions this SDK verifies have a signature format it can insist
127
+ # on; anything else is recorded but left unchecked.
128
+ if SUPPORTED_VERSIONS.include?(version) && !HEX_SIGNATURE_REGEX.match?(value)
129
+ raise MalformedHeaderError, "Malformed webhook signature header"
130
+ end
131
+
132
+ signatures[version] << value
133
+ end
134
+
135
+ if timestamp.nil? || signatures.empty?
136
+ raise MalformedHeaderError, "Malformed webhook signature header"
137
+ end
138
+
139
+ [timestamp, signatures]
140
+ end
141
+
142
+ # Pick the highest version present that this SDK can verify, so that once a
143
+ # sender emits a newer scheme the older one stops being honored here.
144
+ def newest_supported_version(signatures)
145
+ (signatures.keys & SUPPORTED_VERSIONS).max
146
+ end
147
+
148
+ def secure_compare(computed, given)
149
+ return false unless computed.bytesize == given.bytesize
150
+
151
+ OpenSSL.fixed_length_secure_compare(computed, given)
152
+ end
153
+ end
154
+ end
155
+ end
data/lib/helo.rb CHANGED
@@ -17,6 +17,7 @@ require_relative "helo/version"
17
17
  require_relative "helo/api_error"
18
18
  require_relative "helo/configuration"
19
19
  require_relative "helo/client"
20
+ require_relative "helo/webhook_signatures"
20
21
 
21
22
 
22
23
  module Helo
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: helo-email-sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0.pre.beta.6
4
+ version: 1.0.0.pre.beta.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Helo Email
@@ -86,6 +86,7 @@ executables: []
86
86
  extensions: []
87
87
  extra_rdoc_files: []
88
88
  files:
89
+ - README.md
89
90
  - lib/helo-email-sdk.rb
90
91
  - lib/helo.rb
91
92
  - lib/helo/api_error.rb
@@ -121,6 +122,7 @@ files:
121
122
  - lib/helo/models/activity_list_messages_request.rb
122
123
  - lib/helo/models/activity_mail_address.rb
123
124
  - lib/helo/models/attachment.rb
125
+ - lib/helo/models/bounced_details.rb
124
126
  - lib/helo/models/bounced_webhook_payload.rb
125
127
  - lib/helo/models/broadcast_content.rb
126
128
  - lib/helo/models/broadcast_content_attachment.rb
@@ -137,7 +139,10 @@ files:
137
139
  - lib/helo/models/channel_details_response.rb
138
140
  - lib/helo/models/channel_tracking.rb
139
141
  - lib/helo/models/channels_list_request.rb
142
+ - lib/helo/models/clicked_details.rb
140
143
  - lib/helo/models/clicked_webhook_payload.rb
144
+ - lib/helo/models/client_details.rb
145
+ - lib/helo/models/complained_details.rb
141
146
  - lib/helo/models/complained_webhook_payload.rb
142
147
  - lib/helo/models/create_channel_request.rb
143
148
  - lib/helo/models/create_channel_tracking.rb
@@ -145,9 +150,11 @@ files:
145
150
  - lib/helo/models/create_suppressions_request.rb
146
151
  - lib/helo/models/create_suppressions_response.rb
147
152
  - lib/helo/models/create_webhook_request.rb
153
+ - lib/helo/models/delivered_details.rb
148
154
  - lib/helo/models/delivered_webhook_payload.rb
149
155
  - lib/helo/models/delivery_stats.rb
150
156
  - lib/helo/models/delivery_webhook_payload_common.rb
157
+ - lib/helo/models/device_details.rb
151
158
  - lib/helo/models/dns_record_response.rb
152
159
  - lib/helo/models/dns_records_response.rb
153
160
  - lib/helo/models/domain_channel_response.rb
@@ -157,6 +164,7 @@ files:
157
164
  - lib/helo/models/domain_response.rb
158
165
  - lib/helo/models/domain_with_dns_response.rb
159
166
  - lib/helo/models/domains_list_request.rb
167
+ - lib/helo/models/engagement_details.rb
160
168
  - lib/helo/models/error_response.rb
161
169
  - lib/helo/models/mail_address.rb
162
170
  - lib/helo/models/message.rb
@@ -165,6 +173,7 @@ files:
165
173
  - lib/helo/models/message_details_response_event.rb
166
174
  - lib/helo/models/message_details_response_tracking.rb
167
175
  - lib/helo/models/message_statistics.rb
176
+ - lib/helo/models/opened_details.rb
168
177
  - lib/helo/models/opened_webhook_payload.rb
169
178
  - lib/helo/models/paginated_events_response.rb
170
179
  - lib/helo/models/paginated_messages_response.rb
@@ -181,6 +190,7 @@ files:
181
190
  - lib/helo/models/remove_suppression_result.rb
182
191
  - lib/helo/models/remove_suppressions_request.rb
183
192
  - lib/helo/models/remove_suppressions_response.rb
193
+ - lib/helo/models/resubscribed_details.rb
184
194
  - lib/helo/models/resubscribed_webhook_payload.rb
185
195
  - lib/helo/models/return_path_domain_verification_failed_payload.rb
186
196
  - lib/helo/models/return_path_domain_verified_payload.rb
@@ -208,6 +218,7 @@ files:
208
218
  - lib/helo/models/suppression_response.rb
209
219
  - lib/helo/models/suppression_result.rb
210
220
  - lib/helo/models/suppressions_list_request.rb
221
+ - lib/helo/models/unsubscribed_details.rb
211
222
  - lib/helo/models/unsubscribed_webhook_payload.rb
212
223
  - lib/helo/models/update_channel_request.rb
213
224
  - lib/helo/models/update_channel_tracking.rb
@@ -220,6 +231,7 @@ files:
220
231
  - lib/helo/models/webhook_response.rb
221
232
  - lib/helo/models/webhooks_list_request.rb
222
233
  - lib/helo/version.rb
234
+ - lib/helo/webhook_signatures.rb
223
235
  homepage: https://rubygems.org/gems/helo-email-sdk
224
236
  licenses:
225
237
  - MIT