laneful-ruby 1.1.1 → 1.3.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 +4 -4
- data/lib/laneful/client.rb +138 -17
- data/lib/laneful/models.rb +107 -15
- data/lib/laneful/org_models.rb +452 -0
- data/lib/laneful/version.rb +1 -1
- data/lib/laneful/webhooks.rb +5 -11
- data/lib/laneful.rb +2 -1
- metadata +8 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ae0fefd1de5f15988a54f6d158b70bb9037a7879d344ce217066347bec84dbd0
|
|
4
|
+
data.tar.gz: 5c4f9702e0fa83de52b7bbe040700817bbcabeabed1d753951cdaed0a6ea831f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bb8596d2eccaac4b25963ef3bfa5dc0eff89bd3884d5a44fa61b10892aad841d342d06f303918e68e3a49cf17509519fa3bcf3a577c2672cb2cf4c82dbb8182c
|
|
7
|
+
data.tar.gz: 7012812bed4f9c9c217d1aa6299c76eeb43c29cc8c4effe3cf0b78aaae6340992483b05ed58e1e975d95e98f11029f76f813e0c0775345150062a3dab15dcc8c
|
data/lib/laneful/client.rb
CHANGED
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'cgi'
|
|
4
|
+
require 'json'
|
|
5
|
+
|
|
3
6
|
module Laneful
|
|
4
|
-
# Main client for communicating with the Laneful
|
|
7
|
+
# Main client for communicating with the Laneful API.
|
|
8
|
+
# Email sending uses a send host (https://your-endpoint.send.laneful.net).
|
|
9
|
+
# Domain, unsubscribe-group, and analytics endpoints use the organization
|
|
10
|
+
# API host (https://api.laneful.net).
|
|
5
11
|
class Client
|
|
6
12
|
include HTTParty
|
|
7
13
|
|
|
8
|
-
# Default timeout in seconds
|
|
9
14
|
DEFAULT_TIMEOUT = 30
|
|
10
15
|
|
|
16
|
+
# Repeat array keys as workspace_ids=1&workspace_ids=2 (not workspace_ids[]=).
|
|
17
|
+
query_string_normalizer HTTParty::Request::NON_RAILS_QUERY_STRING_NORMALIZER
|
|
18
|
+
|
|
11
19
|
attr_reader :base_url, :auth_token, :timeout
|
|
12
20
|
|
|
13
21
|
def initialize(base_url, auth_token, timeout: DEFAULT_TIMEOUT)
|
|
@@ -16,26 +24,112 @@ module Laneful
|
|
|
16
24
|
@timeout = timeout
|
|
17
25
|
|
|
18
26
|
validate_configuration!
|
|
19
|
-
|
|
20
|
-
# Configure HTTParty
|
|
21
|
-
self.class.base_uri @base_url
|
|
22
|
-
self.class.default_timeout @timeout
|
|
23
|
-
self.class.headers default_headers
|
|
24
27
|
end
|
|
25
28
|
|
|
26
29
|
# Sends a single email
|
|
27
|
-
def send_email(email)
|
|
28
|
-
send_emails([email])
|
|
30
|
+
def send_email(email, mail_settings = nil)
|
|
31
|
+
send_emails([email], mail_settings)
|
|
29
32
|
end
|
|
30
33
|
|
|
31
34
|
# Sends multiple emails
|
|
32
|
-
def send_emails(emails)
|
|
35
|
+
def send_emails(emails, mail_settings = nil)
|
|
33
36
|
validate_emails!(emails)
|
|
34
37
|
|
|
35
38
|
request_data = { 'emails' => emails.map(&:to_hash) }
|
|
36
|
-
|
|
39
|
+
request_data['mail_settings'] = mail_settings.to_hash if mail_settings
|
|
37
40
|
|
|
38
|
-
|
|
41
|
+
request('POST', '/email/send', request_data)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# List unsubscribe groups for a workspace.
|
|
45
|
+
# Uses the organization API host (https://api.laneful.net).
|
|
46
|
+
def list_unsubscribe_groups(workspace_id, params = nil)
|
|
47
|
+
ListUnsubscribeGroupsResponse.from_hash(
|
|
48
|
+
request('GET', "/workspaces/#{workspace_id}/unsubscribe-groups", nil, params&.to_query)
|
|
49
|
+
)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Create an unsubscribe group in a workspace.
|
|
53
|
+
def create_unsubscribe_group(workspace_id, name)
|
|
54
|
+
data = request('POST', "/workspaces/#{workspace_id}/unsubscribe-groups", { 'name' => name })
|
|
55
|
+
UnsubscribeGroup.from_hash(data['unsubscribe_group'] || data)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Update an unsubscribe group.
|
|
59
|
+
def update_unsubscribe_group(workspace_id, unsubscribe_group_id, name)
|
|
60
|
+
data = request(
|
|
61
|
+
'PATCH',
|
|
62
|
+
"/workspaces/#{workspace_id}/unsubscribe-groups/#{unsubscribe_group_id}",
|
|
63
|
+
{ 'name' => name }
|
|
64
|
+
)
|
|
65
|
+
UnsubscribeGroup.from_hash(data['unsubscribe_group'] || data)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# List sending domains for a workspace.
|
|
69
|
+
def list_domains(workspace_id, params = nil)
|
|
70
|
+
ListDomainsResponse.from_hash(
|
|
71
|
+
request('GET', "/workspaces/#{workspace_id}/domains", nil, params&.to_query)
|
|
72
|
+
)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Get a single sending domain by name.
|
|
76
|
+
def get_domain(workspace_id, domain)
|
|
77
|
+
Domain.from_hash(
|
|
78
|
+
request('GET', "/workspaces/#{workspace_id}/domains/#{encode_path(domain)}")
|
|
79
|
+
)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Create a sending domain in a workspace.
|
|
83
|
+
def create_domain(workspace_id, create_request)
|
|
84
|
+
Domain.from_hash(
|
|
85
|
+
request('POST', "/workspaces/#{workspace_id}/domains", create_request.to_hash)
|
|
86
|
+
)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Update a domain's mutable settings (currently the email track).
|
|
90
|
+
def update_domain(workspace_id, domain, update_request)
|
|
91
|
+
Domain.from_hash(
|
|
92
|
+
request(
|
|
93
|
+
'PATCH',
|
|
94
|
+
"/workspaces/#{workspace_id}/domains/#{encode_path(domain)}",
|
|
95
|
+
update_request.to_hash
|
|
96
|
+
)
|
|
97
|
+
)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Trigger DNS verification for a domain.
|
|
101
|
+
def verify_domain(workspace_id, domain)
|
|
102
|
+
Domain.from_hash(
|
|
103
|
+
request('POST', "/workspaces/#{workspace_id}/domains/#{encode_path(domain)}/verify")
|
|
104
|
+
)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Delete a sending domain from a workspace.
|
|
108
|
+
def delete_domain(workspace_id, domain)
|
|
109
|
+
SuccessResponse.from_hash(
|
|
110
|
+
request('DELETE', "/workspaces/#{workspace_id}/domains/#{encode_path(domain)}")
|
|
111
|
+
)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# List domains whose spam complaint ratio reached a critical level.
|
|
115
|
+
def list_domain_spam_ratio_radar(params = nil)
|
|
116
|
+
ListDomainSpamRatioRadarResponse.from_hash(
|
|
117
|
+
request('GET', '/analytics/radar/domain-spam-ratio', nil, params&.to_query)
|
|
118
|
+
)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# List daily Google Postmaster Tools spam-rate reports.
|
|
122
|
+
def list_google_postmaster_spam_reports(params = nil)
|
|
123
|
+
ListGooglePostmasterSpamReportsResponse.from_hash(
|
|
124
|
+
request('GET', '/analytics/google-postmaster/spam-reports', nil, params&.to_query)
|
|
125
|
+
)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# List daily Microsoft SNDS reports for the organization's sending IPs.
|
|
129
|
+
def list_snds_reports(params = nil)
|
|
130
|
+
ListSndsReportsResponse.from_hash(
|
|
131
|
+
request('GET', '/analytics/microsoft-snds/reports', nil, params&.to_query)
|
|
132
|
+
)
|
|
39
133
|
end
|
|
40
134
|
|
|
41
135
|
private
|
|
@@ -45,7 +139,6 @@ module Laneful
|
|
|
45
139
|
|
|
46
140
|
raise ValidationException, 'Auth token cannot be empty' if auth_token.nil? || auth_token.empty?
|
|
47
141
|
|
|
48
|
-
# Basic URL validation
|
|
49
142
|
return if base_url.match?(%r{^https?://})
|
|
50
143
|
|
|
51
144
|
raise ValidationException, 'Base URL must be a valid HTTP/HTTPS URL'
|
|
@@ -59,18 +152,46 @@ module Laneful
|
|
|
59
152
|
end
|
|
60
153
|
end
|
|
61
154
|
|
|
62
|
-
def default_headers
|
|
63
|
-
{
|
|
155
|
+
def default_headers(with_json: true)
|
|
156
|
+
headers = {
|
|
64
157
|
'Authorization' => "Bearer #{auth_token}",
|
|
65
|
-
'Content-Type' => 'application/json',
|
|
66
158
|
'Accept' => 'application/json',
|
|
67
159
|
'User-Agent' => USER_AGENT
|
|
68
160
|
}
|
|
161
|
+
headers['Content-Type'] = 'application/json' if with_json
|
|
162
|
+
headers
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def build_url(path)
|
|
166
|
+
"#{base_url.chomp('/')}/#{API_VERSION}/#{path.sub(%r{\A/}, '')}"
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def encode_path(value)
|
|
170
|
+
CGI.escape(value.to_s).gsub('+', '%20')
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def request(method, path, json = nil, query = nil)
|
|
174
|
+
url = build_url(path)
|
|
175
|
+
|
|
176
|
+
options = {
|
|
177
|
+
headers: default_headers(with_json: !json.nil?),
|
|
178
|
+
timeout: timeout
|
|
179
|
+
}
|
|
180
|
+
options[:query] = query unless query.nil? || query.empty?
|
|
181
|
+
options[:body] = json.to_json unless json.nil?
|
|
182
|
+
|
|
183
|
+
begin
|
|
184
|
+
response = self.class.send(method.downcase, url, options)
|
|
185
|
+
rescue StandardError => e
|
|
186
|
+
raise HttpException.new("HTTP request failed: #{e.message}", 0, e)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
handle_response(response)
|
|
69
190
|
end
|
|
70
191
|
|
|
71
192
|
def handle_response(response)
|
|
72
193
|
case response.code
|
|
73
|
-
when 200, 201, 202
|
|
194
|
+
when 200, 201, 202, 204
|
|
74
195
|
parse_success_response(response)
|
|
75
196
|
when 404
|
|
76
197
|
raise HttpException.new(
|
data/lib/laneful/models.rb
CHANGED
|
@@ -59,12 +59,15 @@ module Laneful
|
|
|
59
59
|
|
|
60
60
|
# Configuration for email tracking settings
|
|
61
61
|
class TrackingSettings
|
|
62
|
-
attr_reader :opens, :clicks, :unsubscribes
|
|
62
|
+
attr_reader :opens, :clicks, :unsubscribes, :unsubscribe_group_id, :unsubscribe_group_name
|
|
63
63
|
|
|
64
|
-
def initialize(opens: false, clicks: false, unsubscribes: false
|
|
64
|
+
def initialize(opens: false, clicks: false, unsubscribes: false,
|
|
65
|
+
unsubscribe_group_id: nil, unsubscribe_group_name: nil)
|
|
65
66
|
@opens = opens
|
|
66
67
|
@clicks = clicks
|
|
67
68
|
@unsubscribes = unsubscribes
|
|
69
|
+
@unsubscribe_group_id = unsubscribe_group_id
|
|
70
|
+
@unsubscribe_group_name = unsubscribe_group_name
|
|
68
71
|
end
|
|
69
72
|
|
|
70
73
|
# Creates tracking settings from a hash representation
|
|
@@ -72,22 +75,31 @@ module Laneful
|
|
|
72
75
|
new(
|
|
73
76
|
opens: data['opens'] || false,
|
|
74
77
|
clicks: data['clicks'] || false,
|
|
75
|
-
unsubscribes: data['unsubscribes'] || false
|
|
78
|
+
unsubscribes: data['unsubscribes'] || false,
|
|
79
|
+
unsubscribe_group_id: data['unsubscribe_group_id'],
|
|
80
|
+
unsubscribe_group_name: data['unsubscribe_group_name']
|
|
76
81
|
)
|
|
77
82
|
end
|
|
78
83
|
|
|
79
84
|
def to_hash
|
|
80
|
-
{
|
|
85
|
+
hash = {
|
|
81
86
|
'opens' => opens,
|
|
82
87
|
'clicks' => clicks,
|
|
83
88
|
'unsubscribes' => unsubscribes
|
|
84
89
|
}
|
|
90
|
+
hash['unsubscribe_group_id'] = unsubscribe_group_id unless unsubscribe_group_id.nil?
|
|
91
|
+
hash['unsubscribe_group_name'] = unsubscribe_group_name unless unsubscribe_group_name.nil?
|
|
92
|
+
hash
|
|
85
93
|
end
|
|
86
94
|
|
|
87
95
|
def ==(other)
|
|
88
96
|
return false unless other.is_a?(TrackingSettings)
|
|
89
97
|
|
|
90
|
-
opens == other.opens &&
|
|
98
|
+
opens == other.opens &&
|
|
99
|
+
clicks == other.clicks &&
|
|
100
|
+
unsubscribes == other.unsubscribes &&
|
|
101
|
+
unsubscribe_group_id == other.unsubscribe_group_id &&
|
|
102
|
+
unsubscribe_group_name == other.unsubscribe_group_name
|
|
91
103
|
end
|
|
92
104
|
|
|
93
105
|
def eql?(other)
|
|
@@ -95,7 +107,7 @@ module Laneful
|
|
|
95
107
|
end
|
|
96
108
|
|
|
97
109
|
def hash
|
|
98
|
-
[opens, clicks, unsubscribes].hash
|
|
110
|
+
[opens, clicks, unsubscribes, unsubscribe_group_id, unsubscribe_group_name].hash
|
|
99
111
|
end
|
|
100
112
|
|
|
101
113
|
def to_s
|
|
@@ -103,14 +115,71 @@ module Laneful
|
|
|
103
115
|
end
|
|
104
116
|
end
|
|
105
117
|
|
|
118
|
+
# Request-level mail settings (sandbox mode, return message IDs, idempotency key).
|
|
119
|
+
#
|
|
120
|
+
# idempotency_key protects against duplicate sends: Laneful accepts a key once per
|
|
121
|
+
# organization for one hour. A repeated request with the same key is answered with
|
|
122
|
+
# status "accepted" (and no message IDs) and sends nothing. Keys are 1 to 36 bytes.
|
|
123
|
+
class MailSettings
|
|
124
|
+
attr_reader :sandbox_mode, :return_message_ids, :idempotency_key
|
|
125
|
+
|
|
126
|
+
def initialize(sandbox_mode: nil, return_message_ids: nil, idempotency_key: nil)
|
|
127
|
+
@sandbox_mode = sandbox_mode
|
|
128
|
+
@return_message_ids = return_message_ids
|
|
129
|
+
@idempotency_key = idempotency_key
|
|
130
|
+
validate_idempotency_key!
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def self.from_hash(data)
|
|
134
|
+
new(
|
|
135
|
+
sandbox_mode: data['sandbox_mode'],
|
|
136
|
+
return_message_ids: data['return_message_ids'],
|
|
137
|
+
idempotency_key: data['idempotency_key']
|
|
138
|
+
)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def to_hash
|
|
142
|
+
hash = {}
|
|
143
|
+
hash['sandbox_mode'] = sandbox_mode unless sandbox_mode.nil?
|
|
144
|
+
hash['return_message_ids'] = return_message_ids unless return_message_ids.nil?
|
|
145
|
+
hash['idempotency_key'] = idempotency_key unless idempotency_key.nil?
|
|
146
|
+
hash
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def ==(other)
|
|
150
|
+
return false unless other.is_a?(MailSettings)
|
|
151
|
+
|
|
152
|
+
sandbox_mode == other.sandbox_mode && return_message_ids == other.return_message_ids &&
|
|
153
|
+
idempotency_key == other.idempotency_key
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def eql?(other)
|
|
157
|
+
self == other
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def hash
|
|
161
|
+
[sandbox_mode, return_message_ids, idempotency_key].hash
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
private
|
|
165
|
+
|
|
166
|
+
def validate_idempotency_key!
|
|
167
|
+
return if idempotency_key.nil?
|
|
168
|
+
return if idempotency_key.bytesize.between?(1, 36)
|
|
169
|
+
|
|
170
|
+
raise ValidationException, 'idempotency_key must contain between 1 and 36 bytes'
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
106
174
|
# Represents a file attachment for an email
|
|
107
175
|
class Attachment
|
|
108
|
-
attr_reader :filename, :content_type, :content
|
|
176
|
+
attr_reader :filename, :content_type, :content, :inline_id
|
|
109
177
|
|
|
110
|
-
def initialize(filename, content_type, content)
|
|
178
|
+
def initialize(filename, content_type, content, inline_id = nil)
|
|
111
179
|
@filename = filename
|
|
112
180
|
@content_type = content_type
|
|
113
181
|
@content = content
|
|
182
|
+
@inline_id = inline_id
|
|
114
183
|
validate!
|
|
115
184
|
end
|
|
116
185
|
|
|
@@ -124,22 +193,27 @@ module Laneful
|
|
|
124
193
|
|
|
125
194
|
# Creates an attachment from a hash representation
|
|
126
195
|
def self.from_hash(data)
|
|
127
|
-
filename = data['file_name'] || data['filename']
|
|
128
|
-
new(filename, data['content_type'], data['content'])
|
|
196
|
+
filename = data['file_name'] || data['filename']
|
|
197
|
+
new(filename, data['content_type'], data['content'], data['inline_id'])
|
|
129
198
|
end
|
|
130
199
|
|
|
131
200
|
def to_hash
|
|
132
|
-
{
|
|
201
|
+
hash = {
|
|
133
202
|
'file_name' => filename,
|
|
134
203
|
'content_type' => content_type,
|
|
135
204
|
'content' => content
|
|
136
205
|
}
|
|
206
|
+
hash['inline_id'] = inline_id if inline_id && !inline_id.empty?
|
|
207
|
+
hash
|
|
137
208
|
end
|
|
138
209
|
|
|
139
210
|
def ==(other)
|
|
140
211
|
return false unless other.is_a?(Attachment)
|
|
141
212
|
|
|
142
|
-
filename == other.filename &&
|
|
213
|
+
filename == other.filename &&
|
|
214
|
+
content_type == other.content_type &&
|
|
215
|
+
content == other.content &&
|
|
216
|
+
inline_id == other.inline_id
|
|
143
217
|
end
|
|
144
218
|
|
|
145
219
|
def eql?(other)
|
|
@@ -147,7 +221,7 @@ module Laneful
|
|
|
147
221
|
end
|
|
148
222
|
|
|
149
223
|
def hash
|
|
150
|
-
[filename, content_type, content].hash
|
|
224
|
+
[filename, content_type, content, inline_id].hash
|
|
151
225
|
end
|
|
152
226
|
|
|
153
227
|
def to_s
|
|
@@ -193,12 +267,13 @@ module Laneful
|
|
|
193
267
|
|
|
194
268
|
# Represents a single email to be sent
|
|
195
269
|
class Email
|
|
196
|
-
attr_reader :from, :to, :cc, :bcc, :subject, :text_content, :html_content,
|
|
270
|
+
attr_reader :from, :from_header, :to, :cc, :bcc, :subject, :text_content, :html_content,
|
|
197
271
|
:template_id, :template_data, :attachments, :headers, :reply_to,
|
|
198
272
|
:send_time, :webhook_data, :tag, :tracking
|
|
199
273
|
|
|
200
274
|
def initialize(builder)
|
|
201
275
|
@from = builder.instance_variable_get(:@from)
|
|
276
|
+
@from_header = builder.instance_variable_get(:@from_header)
|
|
202
277
|
@to = builder.instance_variable_get(:@to).dup.freeze
|
|
203
278
|
@cc = builder.instance_variable_get(:@cc).dup.freeze
|
|
204
279
|
@bcc = builder.instance_variable_get(:@bcc).dup.freeze
|
|
@@ -222,6 +297,7 @@ module Laneful
|
|
|
222
297
|
builder = Builder.new
|
|
223
298
|
|
|
224
299
|
builder.from(Address.from_hash(data['from'])) if data && data['from']
|
|
300
|
+
builder.from_header(Address.from_hash(data['from_header'])) if data && data['from_header']
|
|
225
301
|
|
|
226
302
|
data['to'].each { |to_data| builder.to(Address.from_hash(to_data)) } if data && data['to']
|
|
227
303
|
|
|
@@ -253,6 +329,7 @@ module Laneful
|
|
|
253
329
|
|
|
254
330
|
# Required fields
|
|
255
331
|
hash['from'] = from.to_hash
|
|
332
|
+
hash['from_header'] = from_header.to_hash if from_header
|
|
256
333
|
hash['to'] = to.map(&:to_hash)
|
|
257
334
|
hash['subject'] = subject if subject
|
|
258
335
|
|
|
@@ -278,6 +355,7 @@ module Laneful
|
|
|
278
355
|
return false unless other.is_a?(Email)
|
|
279
356
|
|
|
280
357
|
from == other.from &&
|
|
358
|
+
from_header == other.from_header &&
|
|
281
359
|
to == other.to &&
|
|
282
360
|
cc == other.cc &&
|
|
283
361
|
bcc == other.bcc &&
|
|
@@ -300,7 +378,7 @@ module Laneful
|
|
|
300
378
|
end
|
|
301
379
|
|
|
302
380
|
def hash
|
|
303
|
-
[from, to, cc, bcc, subject, text_content, html_content, template_id,
|
|
381
|
+
[from, from_header, to, cc, bcc, subject, text_content, html_content, template_id,
|
|
304
382
|
template_data, attachments, headers, reply_to, send_time, webhook_data, tag, tracking].hash
|
|
305
383
|
end
|
|
306
384
|
|
|
@@ -340,6 +418,15 @@ module Laneful
|
|
|
340
418
|
raise ValidationException, 'Email must have either content (text/HTML) or a template ID'
|
|
341
419
|
end
|
|
342
420
|
|
|
421
|
+
if webhook_data
|
|
422
|
+
raise ValidationException, 'Webhook data cannot have more than 20 keys' if webhook_data.size > 20
|
|
423
|
+
|
|
424
|
+
webhook_data.each do |key, value|
|
|
425
|
+
raise ValidationException, 'Webhook data keys cannot exceed 50 characters' if key.to_s.length > 50
|
|
426
|
+
raise ValidationException, 'Webhook data values cannot exceed 100 characters' if value.to_s.length > 100
|
|
427
|
+
end
|
|
428
|
+
end
|
|
429
|
+
|
|
343
430
|
# Validate send time
|
|
344
431
|
return unless send_time && send_time <= Time.now.to_i
|
|
345
432
|
|
|
@@ -360,6 +447,11 @@ module Laneful
|
|
|
360
447
|
self
|
|
361
448
|
end
|
|
362
449
|
|
|
450
|
+
def from_header(address)
|
|
451
|
+
@from_header = address
|
|
452
|
+
self
|
|
453
|
+
end
|
|
454
|
+
|
|
363
455
|
def to(address)
|
|
364
456
|
@to << address
|
|
365
457
|
self
|
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Laneful
|
|
4
|
+
# An unsubscribe group in a workspace
|
|
5
|
+
class UnsubscribeGroup
|
|
6
|
+
attr_reader :unsubscribe_group_id, :name, :created_at
|
|
7
|
+
|
|
8
|
+
def initialize(unsubscribe_group_id:, name:, created_at: 0)
|
|
9
|
+
@unsubscribe_group_id = unsubscribe_group_id
|
|
10
|
+
@name = name
|
|
11
|
+
@created_at = created_at
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.from_hash(data)
|
|
15
|
+
new(
|
|
16
|
+
unsubscribe_group_id: (data['unsubscribe_group_id'] || 0).to_i,
|
|
17
|
+
name: data['name'] || '',
|
|
18
|
+
created_at: (data['created_at'] || 0).to_i
|
|
19
|
+
)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def to_hash
|
|
23
|
+
{
|
|
24
|
+
'unsubscribe_group_id' => unsubscribe_group_id,
|
|
25
|
+
'name' => name,
|
|
26
|
+
'created_at' => created_at
|
|
27
|
+
}
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Paginated list of unsubscribe groups
|
|
32
|
+
class ListUnsubscribeGroupsResponse
|
|
33
|
+
attr_reader :unsubscribe_groups, :next_cursor
|
|
34
|
+
|
|
35
|
+
def initialize(unsubscribe_groups:, next_cursor: nil)
|
|
36
|
+
@unsubscribe_groups = unsubscribe_groups || []
|
|
37
|
+
@next_cursor = next_cursor
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def self.from_hash(data)
|
|
41
|
+
groups = (data['unsubscribe_groups'] || []).map { |group| UnsubscribeGroup.from_hash(group) }
|
|
42
|
+
new(unsubscribe_groups: groups, next_cursor: data['next_cursor'])
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Query parameters for listing unsubscribe groups
|
|
47
|
+
class ListUnsubscribeGroupsParams
|
|
48
|
+
attr_reader :cursor, :limit, :search
|
|
49
|
+
|
|
50
|
+
def initialize(cursor: nil, limit: nil, search: nil)
|
|
51
|
+
@cursor = cursor
|
|
52
|
+
@limit = limit
|
|
53
|
+
@search = search
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def to_query
|
|
57
|
+
query = {}
|
|
58
|
+
query['cursor'] = cursor if present?(cursor)
|
|
59
|
+
query['limit'] = limit if limit&.positive?
|
|
60
|
+
query['search'] = search if present?(search)
|
|
61
|
+
query
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def present?(value)
|
|
67
|
+
!value.nil? && !value.to_s.empty?
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# A sending domain and its verification state
|
|
72
|
+
class Domain
|
|
73
|
+
attr_reader :domain, :tracking, :return_path, :verified, :tracking_verified,
|
|
74
|
+
:return_path_verified, :dkim1_verified, :dkim2_verified,
|
|
75
|
+
:dmarc_verified, :require_tls, :email_track_id
|
|
76
|
+
|
|
77
|
+
def initialize(domain:, tracking: '', return_path: '', verified: false, tracking_verified: false,
|
|
78
|
+
return_path_verified: false, dkim1_verified: false, dkim2_verified: false,
|
|
79
|
+
dmarc_verified: false, require_tls: false, email_track_id: '')
|
|
80
|
+
@domain = domain
|
|
81
|
+
@tracking = tracking
|
|
82
|
+
@return_path = return_path
|
|
83
|
+
@verified = verified
|
|
84
|
+
@tracking_verified = tracking_verified
|
|
85
|
+
@return_path_verified = return_path_verified
|
|
86
|
+
@dkim1_verified = dkim1_verified
|
|
87
|
+
@dkim2_verified = dkim2_verified
|
|
88
|
+
@dmarc_verified = dmarc_verified
|
|
89
|
+
@require_tls = require_tls
|
|
90
|
+
@email_track_id = email_track_id
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def self.from_hash(data)
|
|
94
|
+
new(
|
|
95
|
+
domain: data['domain'] || '',
|
|
96
|
+
tracking: data['tracking'] || '',
|
|
97
|
+
return_path: data['return_path'] || '',
|
|
98
|
+
verified: truthy?(data['verified']),
|
|
99
|
+
tracking_verified: truthy?(data['tracking_verified']),
|
|
100
|
+
return_path_verified: truthy?(data['return_path_verified']),
|
|
101
|
+
dkim1_verified: truthy?(data['dkim1_verified']),
|
|
102
|
+
dkim2_verified: truthy?(data['dkim2_verified']),
|
|
103
|
+
dmarc_verified: truthy?(data['dmarc_verified']),
|
|
104
|
+
require_tls: truthy?(data['require_tls']),
|
|
105
|
+
email_track_id: data['email_track_id'] || ''
|
|
106
|
+
)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def to_hash
|
|
110
|
+
{
|
|
111
|
+
'domain' => domain,
|
|
112
|
+
'tracking' => tracking,
|
|
113
|
+
'return_path' => return_path,
|
|
114
|
+
'verified' => verified,
|
|
115
|
+
'tracking_verified' => tracking_verified,
|
|
116
|
+
'return_path_verified' => return_path_verified,
|
|
117
|
+
'dkim1_verified' => dkim1_verified,
|
|
118
|
+
'dkim2_verified' => dkim2_verified,
|
|
119
|
+
'dmarc_verified' => dmarc_verified,
|
|
120
|
+
'require_tls' => require_tls,
|
|
121
|
+
'email_track_id' => email_track_id
|
|
122
|
+
}
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def self.truthy?(value)
|
|
126
|
+
value == true
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Paginated list of sending domains
|
|
131
|
+
class ListDomainsResponse
|
|
132
|
+
attr_reader :domains, :next_cursor
|
|
133
|
+
|
|
134
|
+
def initialize(domains:, next_cursor: nil)
|
|
135
|
+
@domains = domains || []
|
|
136
|
+
@next_cursor = next_cursor
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def self.from_hash(data)
|
|
140
|
+
pagination = data['pagination'] || {}
|
|
141
|
+
new(
|
|
142
|
+
domains: (data['domains'] || []).map { |domain| Domain.from_hash(domain) },
|
|
143
|
+
next_cursor: pagination['next_cursor']
|
|
144
|
+
)
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Query parameters for listing domains
|
|
149
|
+
class ListDomainsParams
|
|
150
|
+
attr_reader :cursor, :limit, :filter_domain
|
|
151
|
+
|
|
152
|
+
def initialize(cursor: nil, limit: nil, filter_domain: nil)
|
|
153
|
+
@cursor = cursor
|
|
154
|
+
@limit = limit
|
|
155
|
+
@filter_domain = filter_domain
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def to_query
|
|
159
|
+
query = {}
|
|
160
|
+
query['cursor'] = cursor if present?(cursor)
|
|
161
|
+
query['limit'] = limit if limit&.positive?
|
|
162
|
+
query['filter[domain]'] = filter_domain if present?(filter_domain)
|
|
163
|
+
query
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
private
|
|
167
|
+
|
|
168
|
+
def present?(value)
|
|
169
|
+
!value.nil? && !value.to_s.empty?
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Request body for creating a sending domain
|
|
174
|
+
class CreateDomainRequest
|
|
175
|
+
attr_reader :domain, :tracking, :return_path, :require_tls, :email_track_id
|
|
176
|
+
|
|
177
|
+
def initialize(domain:, tracking:, return_path:, require_tls: nil, email_track_id: nil)
|
|
178
|
+
@domain = domain
|
|
179
|
+
@tracking = tracking
|
|
180
|
+
@return_path = return_path
|
|
181
|
+
@require_tls = require_tls
|
|
182
|
+
@email_track_id = email_track_id
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def to_hash
|
|
186
|
+
hash = {
|
|
187
|
+
'domain' => domain,
|
|
188
|
+
'tracking' => tracking,
|
|
189
|
+
'return_path' => return_path
|
|
190
|
+
}
|
|
191
|
+
hash['require_tls'] = require_tls unless require_tls.nil?
|
|
192
|
+
hash['email_track_id'] = email_track_id if email_track_id && !email_track_id.empty?
|
|
193
|
+
hash
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Request body for updating a domain's mutable settings.
|
|
198
|
+
# Pass a track ID to set the email track, an empty string to clear it,
|
|
199
|
+
# or nil to leave it unchanged.
|
|
200
|
+
class UpdateDomainRequest
|
|
201
|
+
attr_reader :email_track_id
|
|
202
|
+
|
|
203
|
+
def initialize(email_track_id = nil)
|
|
204
|
+
@email_track_id = email_track_id
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def to_hash
|
|
208
|
+
return {} if email_track_id.nil?
|
|
209
|
+
|
|
210
|
+
{ 'email_track_id' => email_track_id }
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Generic success message from mutating endpoints
|
|
215
|
+
class SuccessResponse
|
|
216
|
+
attr_reader :message
|
|
217
|
+
|
|
218
|
+
def initialize(message:)
|
|
219
|
+
@message = message
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def self.from_hash(data)
|
|
223
|
+
new(message: data['message'] || '')
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def to_hash
|
|
227
|
+
{ 'message' => message }
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# A sending domain whose spam complaint ratio reached a critical level
|
|
232
|
+
class DomainSpamRatioRadar
|
|
233
|
+
attr_reader :workspace_id, :domain, :esp, :spam_ratio, :date
|
|
234
|
+
|
|
235
|
+
def initialize(workspace_id:, domain:, esp:, spam_ratio:, date:)
|
|
236
|
+
@workspace_id = workspace_id
|
|
237
|
+
@domain = domain
|
|
238
|
+
@esp = esp
|
|
239
|
+
@spam_ratio = spam_ratio
|
|
240
|
+
@date = date
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def self.from_hash(data)
|
|
244
|
+
new(
|
|
245
|
+
workspace_id: (data['workspace_id'] || 0).to_i,
|
|
246
|
+
domain: data['domain'] || '',
|
|
247
|
+
esp: data['esp'] || '',
|
|
248
|
+
spam_ratio: (data['spam_ratio'] || 0).to_f,
|
|
249
|
+
date: (data['date'] || '').to_s
|
|
250
|
+
)
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# Paginated list of domain spam-ratio radar entries
|
|
255
|
+
class ListDomainSpamRatioRadarResponse
|
|
256
|
+
attr_reader :radar, :next_cursor
|
|
257
|
+
|
|
258
|
+
def initialize(radar:, next_cursor: nil)
|
|
259
|
+
@radar = radar || []
|
|
260
|
+
@next_cursor = next_cursor
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def self.from_hash(data)
|
|
264
|
+
new(
|
|
265
|
+
radar: (data['radar'] || []).map { |entry| DomainSpamRatioRadar.from_hash(entry) },
|
|
266
|
+
next_cursor: data['next_cursor']
|
|
267
|
+
)
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# Query parameters for listing domain spam-ratio radar entries
|
|
272
|
+
class ListDomainSpamRatioRadarParams
|
|
273
|
+
attr_reader :workspace_ids, :domain, :start_date, :end_date, :cursor, :limit
|
|
274
|
+
|
|
275
|
+
def initialize(workspace_ids: [], domain: nil, start_date: nil, end_date: nil, cursor: nil, limit: nil)
|
|
276
|
+
@workspace_ids = workspace_ids || []
|
|
277
|
+
@domain = domain
|
|
278
|
+
@start_date = start_date
|
|
279
|
+
@end_date = end_date
|
|
280
|
+
@cursor = cursor
|
|
281
|
+
@limit = limit
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def to_query
|
|
285
|
+
query = {}
|
|
286
|
+
query['workspace_ids'] = workspace_ids unless workspace_ids.empty?
|
|
287
|
+
query['domain'] = domain if present?(domain)
|
|
288
|
+
query['start_date'] = start_date if present?(start_date)
|
|
289
|
+
query['end_date'] = end_date if present?(end_date)
|
|
290
|
+
query['cursor'] = cursor if present?(cursor)
|
|
291
|
+
query['limit'] = limit if limit&.positive?
|
|
292
|
+
query
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
private
|
|
296
|
+
|
|
297
|
+
def present?(value)
|
|
298
|
+
!value.nil? && !value.to_s.empty?
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
# A daily Gmail spam-rate report from Google Postmaster Tools
|
|
303
|
+
class GooglePostmasterSpamReport
|
|
304
|
+
attr_reader :workspace_id, :domain, :date, :spam_ratio
|
|
305
|
+
|
|
306
|
+
def initialize(workspace_id:, domain:, date:, spam_ratio:)
|
|
307
|
+
@workspace_id = workspace_id
|
|
308
|
+
@domain = domain
|
|
309
|
+
@date = date
|
|
310
|
+
@spam_ratio = spam_ratio
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def self.from_hash(data)
|
|
314
|
+
new(
|
|
315
|
+
workspace_id: (data['workspace_id'] || 0).to_i,
|
|
316
|
+
domain: data['domain'] || '',
|
|
317
|
+
date: (data['date'] || '').to_s,
|
|
318
|
+
spam_ratio: (data['spam_ratio'] || 0).to_f
|
|
319
|
+
)
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
# Paginated list of Google Postmaster spam reports
|
|
324
|
+
class ListGooglePostmasterSpamReportsResponse
|
|
325
|
+
attr_reader :spam_reports, :next_cursor
|
|
326
|
+
|
|
327
|
+
def initialize(spam_reports:, next_cursor: nil)
|
|
328
|
+
@spam_reports = spam_reports || []
|
|
329
|
+
@next_cursor = next_cursor
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
def self.from_hash(data)
|
|
333
|
+
new(
|
|
334
|
+
spam_reports: (data['spam_reports'] || []).map { |report| GooglePostmasterSpamReport.from_hash(report) },
|
|
335
|
+
next_cursor: data['next_cursor']
|
|
336
|
+
)
|
|
337
|
+
end
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
# Query parameters for listing Google Postmaster spam reports
|
|
341
|
+
class ListGooglePostmasterSpamReportsParams
|
|
342
|
+
attr_reader :workspace_ids, :domain, :start_date, :end_date, :cursor, :limit
|
|
343
|
+
|
|
344
|
+
def initialize(workspace_ids: [], domain: nil, start_date: nil, end_date: nil, cursor: nil, limit: nil)
|
|
345
|
+
@workspace_ids = workspace_ids || []
|
|
346
|
+
@domain = domain
|
|
347
|
+
@start_date = start_date
|
|
348
|
+
@end_date = end_date
|
|
349
|
+
@cursor = cursor
|
|
350
|
+
@limit = limit
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
def to_query
|
|
354
|
+
query = {}
|
|
355
|
+
query['workspace_ids'] = workspace_ids unless workspace_ids.empty?
|
|
356
|
+
query['domain'] = domain if present?(domain)
|
|
357
|
+
query['start_date'] = start_date if present?(start_date)
|
|
358
|
+
query['end_date'] = end_date if present?(end_date)
|
|
359
|
+
query['cursor'] = cursor if present?(cursor)
|
|
360
|
+
query['limit'] = limit if limit&.positive?
|
|
361
|
+
query
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
private
|
|
365
|
+
|
|
366
|
+
def present?(value)
|
|
367
|
+
!value.nil? && !value.to_s.empty?
|
|
368
|
+
end
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
# A daily Microsoft SNDS report for a sending IP
|
|
372
|
+
class SndsReport
|
|
373
|
+
FILTER_UNKNOWN = ''
|
|
374
|
+
FILTER_GREEN = 'GREEN'
|
|
375
|
+
FILTER_YELLOW = 'YELLOW'
|
|
376
|
+
FILTER_RED = 'RED'
|
|
377
|
+
|
|
378
|
+
attr_reader :ip, :date, :rcpt_commands, :data_commands, :message_recipients,
|
|
379
|
+
:filter_result, :complaint_rate, :trap_hits
|
|
380
|
+
|
|
381
|
+
def initialize(ip:, date:, rcpt_commands:, data_commands:, message_recipients:,
|
|
382
|
+
filter_result:, complaint_rate:, trap_hits:)
|
|
383
|
+
@ip = ip
|
|
384
|
+
@date = date
|
|
385
|
+
@rcpt_commands = rcpt_commands
|
|
386
|
+
@data_commands = data_commands
|
|
387
|
+
@message_recipients = message_recipients
|
|
388
|
+
@filter_result = filter_result
|
|
389
|
+
@complaint_rate = complaint_rate
|
|
390
|
+
@trap_hits = trap_hits
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
def self.from_hash(data)
|
|
394
|
+
new(
|
|
395
|
+
ip: data['ip'] || '',
|
|
396
|
+
date: (data['date'] || '').to_s,
|
|
397
|
+
rcpt_commands: (data['rcpt_commands'] || 0).to_i,
|
|
398
|
+
data_commands: (data['data_commands'] || 0).to_i,
|
|
399
|
+
message_recipients: (data['message_recipients'] || 0).to_i,
|
|
400
|
+
filter_result: data['filter_result'] || FILTER_UNKNOWN,
|
|
401
|
+
complaint_rate: (data['complaint_rate'] || 0).to_f,
|
|
402
|
+
trap_hits: (data['trap_hits'] || 0).to_i
|
|
403
|
+
)
|
|
404
|
+
end
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
# Paginated list of Microsoft SNDS reports
|
|
408
|
+
class ListSndsReportsResponse
|
|
409
|
+
attr_reader :snds_reports, :next_cursor
|
|
410
|
+
|
|
411
|
+
def initialize(snds_reports:, next_cursor: nil)
|
|
412
|
+
@snds_reports = snds_reports || []
|
|
413
|
+
@next_cursor = next_cursor
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
def self.from_hash(data)
|
|
417
|
+
new(
|
|
418
|
+
snds_reports: (data['snds_reports'] || []).map { |report| SndsReport.from_hash(report) },
|
|
419
|
+
next_cursor: data['next_cursor']
|
|
420
|
+
)
|
|
421
|
+
end
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
# Query parameters for listing Microsoft SNDS reports
|
|
425
|
+
class ListSndsReportsParams
|
|
426
|
+
attr_reader :ip, :start_date, :end_date, :cursor, :limit
|
|
427
|
+
|
|
428
|
+
def initialize(ip: nil, start_date: nil, end_date: nil, cursor: nil, limit: nil)
|
|
429
|
+
@ip = ip
|
|
430
|
+
@start_date = start_date
|
|
431
|
+
@end_date = end_date
|
|
432
|
+
@cursor = cursor
|
|
433
|
+
@limit = limit
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
def to_query
|
|
437
|
+
query = {}
|
|
438
|
+
query['ip'] = ip if present?(ip)
|
|
439
|
+
query['start_date'] = start_date if present?(start_date)
|
|
440
|
+
query['end_date'] = end_date if present?(end_date)
|
|
441
|
+
query['cursor'] = cursor if present?(cursor)
|
|
442
|
+
query['limit'] = limit if limit&.positive?
|
|
443
|
+
query
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
private
|
|
447
|
+
|
|
448
|
+
def present?(value)
|
|
449
|
+
!value.nil? && !value.to_s.empty?
|
|
450
|
+
end
|
|
451
|
+
end
|
|
452
|
+
end
|
data/lib/laneful/version.rb
CHANGED
data/lib/laneful/webhooks.rb
CHANGED
|
@@ -10,14 +10,12 @@ module Laneful
|
|
|
10
10
|
SIGNATURE_PREFIX = 'sha256='
|
|
11
11
|
SIGNATURE_HEADER_NAME = 'x-webhook-signature'
|
|
12
12
|
|
|
13
|
-
#
|
|
13
|
+
# Documented event types. Informational: parse_webhook_payload keeps events of other
|
|
14
|
+
# types, so new event types don't make the whole payload fail.
|
|
14
15
|
VALID_EVENT_TYPES = %w[
|
|
15
|
-
delivery open click drop spam_complaint unsubscribe bounce
|
|
16
|
+
request delivery open click drop spam_complaint unsubscribe bounce
|
|
16
17
|
].freeze
|
|
17
18
|
|
|
18
|
-
# UUID pattern for lane_id validation
|
|
19
|
-
UUID_PATTERN = /\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/i
|
|
20
|
-
|
|
21
19
|
# Verifies a webhook signature with support for sha256= prefix
|
|
22
20
|
# @param secret [String] The webhook secret
|
|
23
21
|
# @param payload [String] The webhook payload
|
|
@@ -133,9 +131,7 @@ module Laneful
|
|
|
133
131
|
raise ArgumentError, "Missing required field: #{field}" unless event.key?(field)
|
|
134
132
|
end
|
|
135
133
|
|
|
136
|
-
|
|
137
|
-
event_type = event['event']
|
|
138
|
-
raise ArgumentError, "Invalid event type: #{event_type}" unless VALID_EVENT_TYPES.include?(event_type)
|
|
134
|
+
raise ArgumentError, 'Missing required field: event' if event['event'].to_s.empty?
|
|
139
135
|
|
|
140
136
|
# Validate email format
|
|
141
137
|
email = event['email']
|
|
@@ -149,9 +145,7 @@ module Laneful
|
|
|
149
145
|
raise ArgumentError, 'Invalid timestamp format'
|
|
150
146
|
end
|
|
151
147
|
|
|
152
|
-
#
|
|
153
|
-
lane_id = event['lane_id']
|
|
154
|
-
raise ArgumentError, "Invalid lane_id format: #{lane_id}" unless UUID_PATTERN.match?(lane_id)
|
|
148
|
+
# lane_id isn't checked: request events have an empty lane_id.
|
|
155
149
|
|
|
156
150
|
# Return event with all fields (required + optional)
|
|
157
151
|
event
|
data/lib/laneful.rb
CHANGED
|
@@ -8,6 +8,7 @@ require 'base64'
|
|
|
8
8
|
require_relative 'laneful/version'
|
|
9
9
|
require_relative 'laneful/exceptions'
|
|
10
10
|
require_relative 'laneful/models'
|
|
11
|
+
require_relative 'laneful/org_models'
|
|
11
12
|
require_relative 'laneful/client'
|
|
12
13
|
require_relative 'laneful/webhooks'
|
|
13
14
|
|
|
@@ -23,5 +24,5 @@ module Laneful
|
|
|
23
24
|
DEFAULT_TIMEOUT = 30
|
|
24
25
|
|
|
25
26
|
# User agent string
|
|
26
|
-
USER_AGENT =
|
|
27
|
+
USER_AGENT = "laneful-ruby/#{VERSION}".freeze
|
|
27
28
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: laneful-ruby
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Laneful Team
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2026-10-14 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: httparty
|
|
@@ -50,16 +50,17 @@ files:
|
|
|
50
50
|
- lib/laneful/client.rb
|
|
51
51
|
- lib/laneful/exceptions.rb
|
|
52
52
|
- lib/laneful/models.rb
|
|
53
|
+
- lib/laneful/org_models.rb
|
|
53
54
|
- lib/laneful/version.rb
|
|
54
55
|
- lib/laneful/webhooks.rb
|
|
55
|
-
homepage: https://github.com/
|
|
56
|
+
homepage: https://github.com/lanefulhq/laneful-ruby
|
|
56
57
|
licenses:
|
|
57
58
|
- MIT
|
|
58
59
|
metadata:
|
|
59
|
-
homepage_uri: https://github.com/
|
|
60
|
-
source_code_uri: https://github.com/
|
|
61
|
-
changelog_uri: https://github.com/
|
|
62
|
-
documentation_uri: https://
|
|
60
|
+
homepage_uri: https://github.com/lanefulhq/laneful-ruby
|
|
61
|
+
source_code_uri: https://github.com/lanefulhq/laneful-ruby
|
|
62
|
+
changelog_uri: https://github.com/lanefulhq/laneful-ruby/blob/main/CHANGELOG.md
|
|
63
|
+
documentation_uri: https://laneful.com/docs/ruby-sdk/
|
|
63
64
|
rubygems_mfa_required: 'true'
|
|
64
65
|
post_install_message:
|
|
65
66
|
rdoc_options: []
|