wlvalidate 0.1.6 → 0.3.2
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.
- data/lib/wlvalidate.rb +163 -48
- metadata +20 -4
data/lib/wlvalidate.rb
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
Results of the below calls will be returned in the following format
|
4
4
|
(TYPE, RESULT, DATA and EXT_DATA are return values in each hash line)
|
5
5
|
+=======+===========+===================+===========++========+=================+======================+
|
6
|
-
[
|
6
|
+
[ CALL | RESULT | DATA | EXT_DATA ][ Status | Return Type | Return on all paths? ]
|
7
7
|
+=======+===========+===================+===========++========+=================+======================+
|
8
8
|
| A | PASS/FAIL | (none) | (none) || Done | Array of hashes | Yes |
|
9
9
|
| SPF | PASS/FAIL | SPF record | Fail type || Done | Hash | Yes |
|
@@ -15,10 +15,6 @@
|
|
15
15
|
| ALL | (multi) | (multi) | (multi) || Done | Array of hashes | (n/a) |
|
16
16
|
+-------+-----------+-------------------+-----------++--------+-----------------+----------------------+
|
17
17
|
|
18
|
-
TO DO:
|
19
|
-
- Add master/slave DNS option
|
20
|
-
- Validate hashes for Ruby
|
21
|
-
|
22
18
|
=end
|
23
19
|
|
24
20
|
# Must have dnsruby gem installed for this to work
|
@@ -27,15 +23,84 @@ include Dnsruby
|
|
27
23
|
|
28
24
|
module WLValidate
|
29
25
|
class Validate
|
30
|
-
|
31
|
-
|
32
|
-
|
26
|
+
@@dns_updated = 0
|
27
|
+
@@dns_server = "8.8.8.8"
|
28
|
+
@@default_dns_server = "8.8.4.4"
|
29
|
+
@@dns_type = 1
|
30
|
+
|
31
|
+
def initialize( dns_server="8.8.4.4", dns_type=1 )
|
32
|
+
# PURPOSE: Executes code before anything else runs (hence Initialize)
|
33
|
+
# RETURNS: Not a darn thing...
|
34
|
+
# NOTES: We want to soak in parameters for DNS servers (array), DNS type (integer) and DNS timeouts (integer):
|
35
|
+
# Resulting global variables: @dns_servers, @dns_type, @dns_timeout
|
36
|
+
# If these values are not set during instancing, we want to put in some default values
|
37
|
+
#
|
38
|
+
# VARS: @dns_server: single string, non-array of IP address
|
39
|
+
# @dns_type: integer where 1 = local config, 2 = authorative server, 3 = Specific server
|
40
|
+
|
41
|
+
@dns_updated = 0
|
42
|
+
@dns_server = dns_server.to_s
|
43
|
+
@default_dns_server = "8.8.4.4"
|
44
|
+
@dns_type = dns_type.to_i
|
45
|
+
|
46
|
+
if (dns_type.to_i == 3)
|
47
|
+
if (dns_server)
|
48
|
+
@dns_server = dns_server
|
49
|
+
else
|
50
|
+
@dns_server = @default_dns_server
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def get_info()
|
56
|
+
hashReturn = Hash.new
|
57
|
+
hashReturn = { "dns_server" => @dns_server, "dns_type" => @dns_type, "dns_timeout" => @dns_timeout, "dns_updated" => @dns_updated }
|
58
|
+
return hashReturn
|
33
59
|
end
|
34
60
|
|
35
|
-
def
|
61
|
+
def set_auth_ns(strDomain)
|
62
|
+
# Since we can't get strDomain during __init__, we will check to see if @dns_type = 2 (auth) and if
|
63
|
+
# @dns_server is empty. If so, then we want to get the authorative NS and set it globally.
|
64
|
+
if ((@dns_updated == 0) && (@dns_type == 2))
|
65
|
+
iplist = []
|
66
|
+
begin
|
67
|
+
res = Dnsruby::Resolver.new
|
68
|
+
|
69
|
+
res.retry_times=(1)
|
70
|
+
ns_req = nil
|
71
|
+
ns_req = res.query(strDomain, 'NS')
|
72
|
+
|
73
|
+
res.recurse=(0)
|
74
|
+
|
75
|
+
(ns_req.answer.select {|r| r.type == 'NS'}).each do |nsrr|
|
76
|
+
ns = nsrr.domainname
|
77
|
+
|
78
|
+
local_res = Dnsruby::Resolver.new
|
79
|
+
a_req=nil
|
80
|
+
a_req = local_res.query(ns, 'A')
|
81
|
+
|
82
|
+
(a_req.answer.select {|r| r.type == 'A'}).each do |r|
|
83
|
+
ip = r.address
|
84
|
+
res.nameserver=(ip.to_s)
|
85
|
+
soa_req=nil
|
86
|
+
soa_req = res.query(strDomain, 'SOA', 'IN')
|
87
|
+
iplist.push(ip.to_s)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
iplist.shuffle!
|
91
|
+
@dns_server = iplist.first
|
92
|
+
@dns_updated = 1
|
93
|
+
rescue
|
94
|
+
@dns_server = @default_dns_server
|
95
|
+
@dns_updated = 1
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def ALL(strWL, strDomain)
|
36
101
|
# PURPOSE: Execute all of the below methods as a single call
|
37
102
|
# RETURNS: Returns the combined array of hashes as returned by each function
|
38
|
-
# NOTES:
|
103
|
+
# NOTES: All functions have values on all return paths, except for MX (see TODO above)
|
39
104
|
|
40
105
|
arrayReturns = Array.new
|
41
106
|
|
@@ -45,6 +110,7 @@ module WLValidate
|
|
45
110
|
hashDKIM1 = self.DKIM1(strWL, strDomain)
|
46
111
|
hashDKIM2 = self.DKIM2(strWL, strDomain)
|
47
112
|
arrayMX = self.MX(strWL, strDomain)
|
113
|
+
hashInfo = self.get_info()
|
48
114
|
|
49
115
|
# array.push hashes, and array.concat arrays
|
50
116
|
arrayReturns.push(hashSPF)
|
@@ -53,11 +119,12 @@ module WLValidate
|
|
53
119
|
arrayReturns.push(hashDKIM1)
|
54
120
|
arrayReturns.push(hashDKIM2)
|
55
121
|
arrayReturns.concat(arrayMX)
|
122
|
+
arrayReturns.push(hashInfo)
|
56
123
|
|
57
124
|
return arrayReturns
|
58
125
|
end
|
59
126
|
|
60
|
-
def
|
127
|
+
def MX(strWL, strDomain)
|
61
128
|
# PURPOSE: Loop through all <domain> MX records
|
62
129
|
# RETURNS: Array of hashes
|
63
130
|
hashTemp = Hash.new
|
@@ -66,9 +133,16 @@ module WLValidate
|
|
66
133
|
|
67
134
|
if ((strWL == "") || (strDomain == ""))
|
68
135
|
hashReturn = { "type" => "MX", "result" => "ERROR", "data" => "Missing parameters", "ext_data" => "One or more parameters are missing!" }
|
136
|
+
arrayReturn.push(hashReturn)
|
137
|
+
end
|
138
|
+
|
139
|
+
set_auth_ns(strDomain)
|
140
|
+
|
141
|
+
if ((@dns_type == 2) || (@dns_type == 3))
|
142
|
+
dns_params = {:nameserver=>@dns_server}
|
69
143
|
end
|
70
144
|
|
71
|
-
Dnsruby::DNS.open {|dns|
|
145
|
+
Dnsruby::DNS.open(dns_params) {|dns|
|
72
146
|
begin
|
73
147
|
arrayMX = dns.getresources(strDomain, Dnsruby::Types.MX)
|
74
148
|
if (arrayMX.count > 0)
|
@@ -76,7 +150,11 @@ module WLValidate
|
|
76
150
|
arrayMX.each do |r|
|
77
151
|
myExchange = r.exchange.to_s
|
78
152
|
myPreference = r.preference.to_s
|
79
|
-
|
153
|
+
if myExchange.match %r{sendgrid.}
|
154
|
+
hashTemp = { "type" => "MX", "result" => "ERROR", "data" => myExchange, "ext_data" => "SendGrid is listed as handling incoming email on the root domain - this is generally a bad idea unless you're using ParseAPI..." }
|
155
|
+
else
|
156
|
+
hashTemp = { "type" => "MX", "result" => "INFO", "data" => myExchange, "ext_data" => myPreference }
|
157
|
+
end
|
80
158
|
arrayReturn.push(hashTemp)
|
81
159
|
hashTemp = {}
|
82
160
|
end
|
@@ -99,7 +177,7 @@ module WLValidate
|
|
99
177
|
return arrayReturn
|
100
178
|
end
|
101
179
|
|
102
|
-
def
|
180
|
+
def A(strWL, strDomain)
|
103
181
|
# PURPOSE: Loop through all oN.<wl>.<domain>.<tlds> to get IPs, then also do reverse DNS checks
|
104
182
|
# RETURNS: Hash - see above for definition
|
105
183
|
|
@@ -108,8 +186,11 @@ module WLValidate
|
|
108
186
|
|
109
187
|
if ((strWL == "") || (strDomain == ""))
|
110
188
|
hashReturn = { "type" => "A", "result" => "ERROR", "data" => "Missing parameters", "ext_data" => "One or more expected paramters are missing!" }
|
189
|
+
arrayReturn.push(hashReturn)
|
111
190
|
end
|
112
191
|
|
192
|
+
set_auth_ns(strDomain)
|
193
|
+
|
113
194
|
i = 1
|
114
195
|
intDNSIPLoops = 11
|
115
196
|
while i < intDNSIPLoops do
|
@@ -142,7 +223,7 @@ module WLValidate
|
|
142
223
|
return arrayReturn
|
143
224
|
end
|
144
225
|
|
145
|
-
def
|
226
|
+
def CNAME(strWL, strDomain)
|
146
227
|
# PURPOSE: Get CNAME record and make sure it's mapped to 'sendgrid.net'
|
147
228
|
# RETURNS: Hash - See above for definition
|
148
229
|
|
@@ -153,7 +234,13 @@ module WLValidate
|
|
153
234
|
hashReturn = { "type" => "CNAME", "result" => "ERROR", "data" => "Missing parameters", "ext_data" => "One or more expected paramters are missing!" }
|
154
235
|
end
|
155
236
|
|
156
|
-
|
237
|
+
set_auth_ns(strDomain)
|
238
|
+
|
239
|
+
if ((@dns_type == 2) || (@dns_type == 3))
|
240
|
+
dns_params = {:nameserver=>@dns_server}
|
241
|
+
end
|
242
|
+
|
243
|
+
Dnsruby::DNS.open(dns_params) {|dns|
|
157
244
|
begin
|
158
245
|
strHost = strWL + "." + strDomain
|
159
246
|
arrayCNAME = dns.getresources(strHost, Dnsruby::Types.CNAME)
|
@@ -166,9 +253,12 @@ module WLValidate
|
|
166
253
|
end
|
167
254
|
end
|
168
255
|
if hashReturn.empty?
|
169
|
-
hashReturn = { "type" => "CNAME", "result" => "FAIL", "data" =>
|
256
|
+
hashReturn = { "type" => "CNAME", "result" => "FAIL", "data" => strHost, "ext_data" => "No matching CNAME records found" }
|
170
257
|
end
|
171
258
|
end
|
259
|
+
if boolFoundMatch == false
|
260
|
+
hashReturn = { "type" => "CNAME", "result" => "FAIL", "data" => strHost, "ext_data" => "No matching CNAME records found" }
|
261
|
+
end
|
172
262
|
rescue Dnsruby::ResolvTimeout
|
173
263
|
hashReturn = { "type" => "CNAME", "result" => "ERROR", "data" => strHost, "ext_data" => "Timed out while attempting rDNS lookup" }
|
174
264
|
rescue
|
@@ -178,7 +268,7 @@ module WLValidate
|
|
178
268
|
return hashReturn
|
179
269
|
end
|
180
270
|
|
181
|
-
def
|
271
|
+
def DKIM1(strWL, strDomain)
|
182
272
|
# PURPOSE: Get TXT/CNAME record for smtpapi._domainkey.<domain>.<tlds>
|
183
273
|
# RETURNS: Hash - See above for definition
|
184
274
|
|
@@ -186,32 +276,41 @@ module WLValidate
|
|
186
276
|
boolFoundMatch = false
|
187
277
|
strHost = "smtpapi._domainkey." + strDomain
|
188
278
|
strDKIM = "k=rsa; t=s; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDPtW5iwpXVPiH5FzJ7Nrl8USzuY9zqqzjE0D1r04xDN6qwziDnmgcFNNfMewVKN2D1O+2J9N14hRprzByFwfQW76yojh54Xu3uSbQ3JP0A7k8o8GutRF8zbFUA8n0ZH2y0cIEjMliXY4W4LwPA7m4q0ObmvSjhd63O9d8z1XkUBwIDAQAB"
|
189
|
-
strDKIM_k = "rsa"
|
190
|
-
strDKIM_t = "s"
|
191
|
-
strDKIM_p = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDPtW5iwpXVPiH5FzJ7Nrl8USzuY9zqqzjE0D1r04xDN6qwziDnmgcFNNfMewVKN2D1O+2J9N14hRprzByFwfQW76yojh54Xu3uSbQ3JP0A7k8o8GutRF8zbFUA8n0ZH2y0cIEjMliXY4W4LwPA7m4q0ObmvSjhd63O9d8z1XkUBwIDAQAB"
|
192
279
|
|
193
280
|
if ((strWL == "") || (strDomain == ""))
|
194
281
|
hashReturn = { "type" => "DKIM1", "result" => "ERROR", "data" => "Missing parameters", "ext_data" => "One or more expected paramters are missing!" }
|
195
282
|
end
|
196
283
|
|
197
|
-
|
284
|
+
set_auth_ns(strDomain)
|
285
|
+
|
286
|
+
if ((@dns_type == 2) || (@dns_type == 3))
|
287
|
+
dns_params = {:nameserver=>@dns_server}
|
288
|
+
end
|
289
|
+
|
290
|
+
Dnsruby::DNS.open(dns_params) {|dns|
|
198
291
|
begin
|
199
292
|
arrayDKIM1 = dns.getresources(strHost, Dnsruby::Types.TXT)
|
200
293
|
arrayDKIM1.each do |r|
|
201
|
-
if
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
294
|
+
if r.name.to_s.match %r{dkim.sendgrid.net}
|
295
|
+
hashReturn = { "type" => "DKIM1", "result" => "PASS", "data" => r.name.to_s, "ext_data" => "CNAME" }
|
296
|
+
boolFoundMatch = true
|
297
|
+
end
|
298
|
+
if ((r.name.to_s == strHost) && (r.data == strDKIM))
|
299
|
+
hashReturn = { "type" => "DKIM1", "result" => "PASS", "data" => r.name.to_s, "ext_data" => "TXT" }
|
300
|
+
boolFoundMatch = true
|
301
|
+
end
|
302
|
+
end
|
303
|
+
if ((boolFoundMatch == false) || (hashReturn.empty?))
|
304
|
+
arrayDKIM1CNAME = dns.getresources(strHost, Dnsruby::Types.CNAME)
|
305
|
+
arrayDKIM1CNAME.each do |c|
|
306
|
+
if c.rdata_to_string.match %r{dkim.sendgrid.net}
|
307
|
+
hashReturn = { "type" => "DKIM1", "result" => "PASS", "data" => c.name.to_s, "ext_data" => "CNAME" }
|
207
308
|
boolFoundMatch = true
|
208
|
-
else
|
209
|
-
hashReturn = { "type" => "DKIM1", "result" => "FAIL", "data" => strHost, "ext_data" => "No DKIM record found as either CNAME or TXT" }
|
210
309
|
end
|
211
310
|
end
|
212
311
|
end
|
213
|
-
if hashReturn.empty?
|
214
|
-
hashReturn = { "type" => "
|
312
|
+
if ((boolFoundMatch == false) || (hashReturn.empty?))
|
313
|
+
hashReturn = { "type" => "DKIM1", "result" => "FAIL", "data" => strHost, "ext_data" => "No matching DKIM/TXT records found" }
|
215
314
|
end
|
216
315
|
rescue
|
217
316
|
hashReturn = { "type" => "DKIM1", "result" => "ERROR", "data" => strHost, "ext_data" => "Unable to resolve host, or query timed out" }
|
@@ -220,7 +319,7 @@ module WLValidate
|
|
220
319
|
return hashReturn
|
221
320
|
end
|
222
321
|
|
223
|
-
def
|
322
|
+
def DKIM2(strWL, strDomain)
|
224
323
|
# PURPOSE: Get TXT/CNAME record for smtpapi._domainkey.<wl>.<domain>.<tlds>
|
225
324
|
# RETURNS: Hash - See above for definition
|
226
325
|
|
@@ -228,31 +327,41 @@ module WLValidate
|
|
228
327
|
boolFoundMatch = false
|
229
328
|
strHost = "smtpapi._domainkey." + strWL + "." + strDomain
|
230
329
|
strDKIM = "k=rsa; t=s; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDPtW5iwpXVPiH5FzJ7Nrl8USzuY9zqqzjE0D1r04xDN6qwziDnmgcFNNfMewVKN2D1O+2J9N14hRprzByFwfQW76yojh54Xu3uSbQ3JP0A7k8o8GutRF8zbFUA8n0ZH2y0cIEjMliXY4W4LwPA7m4q0ObmvSjhd63O9d8z1XkUBwIDAQAB"
|
231
|
-
strDKIM_k = "rsa"
|
232
|
-
strDKIM_t = "s"
|
233
|
-
strDKIM_p = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDPtW5iwpXVPiH5FzJ7Nrl8USzuY9zqqzjE0D1r04xDN6qwziDnmgcFNNfMewVKN2D1O+2J9N14hRprzByFwfQW76yojh54Xu3uSbQ3JP0A7k8o8GutRF8zbFUA8n0ZH2y0cIEjMliXY4W4LwPA7m4q0ObmvSjhd63O9d8z1XkUBwIDAQAB"
|
234
330
|
|
235
331
|
if ((strWL == "") || (strDomain == ""))
|
236
332
|
hashReturn = { "type" => "DKIM2", "result" => "ERROR", "data" => "Missing parameters", "ext_data" => "One or more expected paramters are missing!" }
|
237
333
|
end
|
238
334
|
|
239
|
-
|
335
|
+
set_auth_ns(strDomain)
|
336
|
+
|
337
|
+
if ((@dns_type == 2) || (@dns_type == 3))
|
338
|
+
dns_params = {:nameserver=>@dns_server}
|
339
|
+
end
|
340
|
+
|
341
|
+
Dnsruby::DNS.open(dns_params) {|dns|
|
240
342
|
begin
|
241
343
|
arrayDKIM2 = dns.getresources(strHost, Dnsruby::Types.TXT)
|
242
344
|
arrayDKIM2.each do |r|
|
243
|
-
if
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
345
|
+
if r.name.to_s.match %r{dkim.sendgrid.net}
|
346
|
+
hashReturn = { "type" => "DKIM2", "result" => "PASS", "data" => r.name.to_s, "ext_data" => "CNAME" }
|
347
|
+
boolFoundMatch = true
|
348
|
+
end
|
349
|
+
if ((r.name.to_s == strHost) && (r.data == strDKIM))
|
350
|
+
hashReturn = { "type" => "DKIM2", "result" => "PASS", "data" => r.name.to_s, "ext_data" => "TXT" }
|
351
|
+
boolFoundMatch = true
|
352
|
+
end
|
353
|
+
end
|
354
|
+
|
355
|
+
if ((boolFoundMatch == false) || (hashReturn.empty?))
|
356
|
+
arrayDKIM2CNAME = dns.getresources(strHost, Dnsruby::Types.CNAME)
|
357
|
+
arrayDKIM2CNAME.each do |c|
|
358
|
+
if c.rdata_to_string.match %r{dkim.sendgrid.net}
|
359
|
+
hashReturn = { "type" => "DKIM2", "result" => "PASS", "data" => c.name.to_s, "ext_data" => "CNAME" }
|
249
360
|
boolFoundMatch = true
|
250
|
-
else
|
251
|
-
hashReturn = { "type" => "DKIM2", "result" => "FAIL", "data" => strHost, "ext_data" => "No DKIM record found as either CNAME or TXT" }
|
252
361
|
end
|
253
362
|
end
|
254
363
|
end
|
255
|
-
if hashReturn.empty?
|
364
|
+
if ((boolFoundMatch == false) || (hashReturn.empty?))
|
256
365
|
hashReturn = { "type" => "DKIM2", "result" => "FAIL", "data" => strHost, "ext_data" => "No matching DKIM/TXT records found" }
|
257
366
|
end
|
258
367
|
rescue
|
@@ -262,7 +371,7 @@ module WLValidate
|
|
262
371
|
return hashReturn
|
263
372
|
end
|
264
373
|
|
265
|
-
def
|
374
|
+
def SPF(strWL, strDomain)
|
266
375
|
# PURPOSE: Get TXT record and make sure it has include:sendgrid.net
|
267
376
|
# RETURNS: Hash - See above for definition
|
268
377
|
|
@@ -273,7 +382,13 @@ module WLValidate
|
|
273
382
|
hashReturn = { "type" => "SPF", "result" => "ERROR", "data" => "Missing parameters", "ext_data" => "One or more expected paramters are missing!" }
|
274
383
|
end
|
275
384
|
|
276
|
-
|
385
|
+
set_auth_ns(strDomain)
|
386
|
+
|
387
|
+
if ((@dns_type == 2) || (@dns_type == 3))
|
388
|
+
dns_params = {:nameserver=>@dns_server}
|
389
|
+
end
|
390
|
+
|
391
|
+
Dnsruby::DNS.open(dns_params) {|dns|
|
277
392
|
begin
|
278
393
|
arrayTXT = dns.getresources(strDomain, Dnsruby::Types.TXT)
|
279
394
|
if arrayTXT.to_s != "[]"
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: wlvalidate
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.2
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,8 +9,24 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2013-
|
13
|
-
dependencies:
|
12
|
+
date: 2013-02-15 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: dnsruby
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
14
30
|
description: SendGrid Rails Gem to validate White Label settings
|
15
31
|
email: jayson.sperling@sendgrid.com
|
16
32
|
executables: []
|
@@ -38,7 +54,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
38
54
|
version: '0'
|
39
55
|
requirements: []
|
40
56
|
rubyforge_project:
|
41
|
-
rubygems_version: 1.8.
|
57
|
+
rubygems_version: 1.8.24
|
42
58
|
signing_key:
|
43
59
|
specification_version: 3
|
44
60
|
summary: SendGrid White Label Validator
|