dmarcator 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 +7 -0
- data/ChangeLog.md +55 -0
- data/LICENSE.txt +21 -0
- data/README.md +137 -0
- data/lib/dmarcator/exceptions.rb +6 -0
- data/lib/dmarcator/parser.rb +221 -0
- data/lib/dmarcator/query.rb +63 -0
- data/lib/dmarcator/record.rb +393 -0
- data/lib/dmarcator/uri.rb +114 -0
- data/lib/dmarcator/version.rb +4 -0
- data/lib/dmarcator.rb +5 -0
- data/spec/data/alexa.csv +500 -0
- data/spec/dmarcator_spec.rb +62 -0
- data/spec/parser_spec.rb +413 -0
- data/spec/record_spec.rb +229 -0
- data/spec/spec_helper.rb +11 -0
- data/spec/uri_spec.rb +141 -0
- metadata +88 -0
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
require 'dmarcator/query'
|
|
2
|
+
require 'dmarcator/parser'
|
|
3
|
+
require 'dmarcator/exceptions'
|
|
4
|
+
|
|
5
|
+
require 'resolv'
|
|
6
|
+
require 'uri'
|
|
7
|
+
|
|
8
|
+
module DMARCator
|
|
9
|
+
class Record
|
|
10
|
+
|
|
11
|
+
# `p` field.
|
|
12
|
+
#
|
|
13
|
+
# @return [:none, :quarantine, :reject]
|
|
14
|
+
attr_reader :p
|
|
15
|
+
|
|
16
|
+
# `rua` field.
|
|
17
|
+
#
|
|
18
|
+
# @return [Array<Uri>]
|
|
19
|
+
attr_reader :rua
|
|
20
|
+
|
|
21
|
+
# `rua` field.
|
|
22
|
+
#
|
|
23
|
+
# @return [Array<Uri>]
|
|
24
|
+
attr_reader :ruf
|
|
25
|
+
|
|
26
|
+
# `sp` field.
|
|
27
|
+
#
|
|
28
|
+
# @return [:none, :quarantine, :reject]
|
|
29
|
+
attr_reader :sp
|
|
30
|
+
|
|
31
|
+
# `v` field.
|
|
32
|
+
#
|
|
33
|
+
# @return [:DMARC1]
|
|
34
|
+
attr_reader :v
|
|
35
|
+
|
|
36
|
+
#
|
|
37
|
+
# Initializes the record.
|
|
38
|
+
#
|
|
39
|
+
# @param [Hash{Symbol => Object}] attributes
|
|
40
|
+
# Attributes for the record.
|
|
41
|
+
#
|
|
42
|
+
# @option attributes [:r, :s] :adkim (:r)
|
|
43
|
+
#
|
|
44
|
+
# @option attributes [:r, :s] :aspf (:r)
|
|
45
|
+
#
|
|
46
|
+
# @option attributes [Array<'0', '1', 'd', 's'>] :fo ('0')
|
|
47
|
+
#
|
|
48
|
+
# @option attributes [:none, :quarantine, :reject] :p
|
|
49
|
+
#
|
|
50
|
+
# @option attributes [Integer] :pct (100)
|
|
51
|
+
#
|
|
52
|
+
# @option attributes [:afrf, :iodef] :rf (:afrf)
|
|
53
|
+
#
|
|
54
|
+
# @option attributes [Integer] :ri (86400)
|
|
55
|
+
#
|
|
56
|
+
# @option attributes [Array<Uri>] :rua
|
|
57
|
+
#
|
|
58
|
+
# @option attributes [Array<Uri>] :ruf
|
|
59
|
+
#
|
|
60
|
+
# @option attributes [:none, :quarantine, :reject] :sp
|
|
61
|
+
#
|
|
62
|
+
# @option attributes [:DMARC1] :v
|
|
63
|
+
#
|
|
64
|
+
def initialize(attributes={})
|
|
65
|
+
@v = attributes.fetch(:v)
|
|
66
|
+
@adkim = attributes[:adkim]
|
|
67
|
+
@aspf = attributes[:aspf]
|
|
68
|
+
@fo = attributes[:fo]
|
|
69
|
+
@p = attributes[:p]
|
|
70
|
+
@pct = attributes[:pct]
|
|
71
|
+
@rf = attributes[:rf]
|
|
72
|
+
@ri = attributes[:ri]
|
|
73
|
+
@rua = attributes[:rua]
|
|
74
|
+
@ruf = attributes[:ruf]
|
|
75
|
+
@sp = attributes[:sp]
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
#
|
|
79
|
+
# Determines if the `sp=` field was specified?
|
|
80
|
+
#
|
|
81
|
+
# @return [Boolean]
|
|
82
|
+
#
|
|
83
|
+
# @since 0.4.0
|
|
84
|
+
#
|
|
85
|
+
def sp?
|
|
86
|
+
!@sp.nil?
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
#
|
|
90
|
+
# The `sp=` field.
|
|
91
|
+
#
|
|
92
|
+
# @return [:none, :quarantine, :reject]
|
|
93
|
+
# The value of the `sp=` field, or that of {#p} if the field was omitted.
|
|
94
|
+
#
|
|
95
|
+
def sp
|
|
96
|
+
@sp || @p
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
#
|
|
100
|
+
# Determines whether the `adkim=` field was specified.
|
|
101
|
+
#
|
|
102
|
+
# @return [Boolean]
|
|
103
|
+
#
|
|
104
|
+
# @since 0.4.0
|
|
105
|
+
#
|
|
106
|
+
def adkim?
|
|
107
|
+
!@adkim.nil?
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
#
|
|
111
|
+
# `adkim=` field.
|
|
112
|
+
#
|
|
113
|
+
# @return [:r, :s]
|
|
114
|
+
# The value of the `adkim=` field, or `:r` if the field was omitted.
|
|
115
|
+
#
|
|
116
|
+
def adkim
|
|
117
|
+
@adkim || :r
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
#
|
|
121
|
+
# Determines whether the `aspf=` field was specified.
|
|
122
|
+
#
|
|
123
|
+
# @return [Boolean]
|
|
124
|
+
#
|
|
125
|
+
# @since 0.4.0
|
|
126
|
+
#
|
|
127
|
+
def aspf?
|
|
128
|
+
!@aspf.nil?
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
#
|
|
132
|
+
# `aspf` field.
|
|
133
|
+
#
|
|
134
|
+
# @return [:r, :s]
|
|
135
|
+
# The value of the `aspf=` field, or `:r` if the field was omitted.
|
|
136
|
+
#
|
|
137
|
+
def aspf
|
|
138
|
+
@aspf || :r
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
#
|
|
142
|
+
# Determines whether the `fo=` field was specified.
|
|
143
|
+
#
|
|
144
|
+
# @return [Boolean]
|
|
145
|
+
#
|
|
146
|
+
# @since 0.4.0
|
|
147
|
+
#
|
|
148
|
+
def fo?
|
|
149
|
+
!@fo.nil?
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
#
|
|
153
|
+
# `fo` field.
|
|
154
|
+
#
|
|
155
|
+
# @return [Array<'0', '1', 'd', 's'>]
|
|
156
|
+
# The value of the `fo=` field, or `["0"]` if the field was omitted.
|
|
157
|
+
#
|
|
158
|
+
def fo
|
|
159
|
+
@fo || %w[0]
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
#
|
|
163
|
+
# Determines if the `p=` field was specified?
|
|
164
|
+
#
|
|
165
|
+
# @return [Boolean]
|
|
166
|
+
#
|
|
167
|
+
# @since 0.4.0
|
|
168
|
+
#
|
|
169
|
+
def p?
|
|
170
|
+
!@p.nil?
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
#
|
|
174
|
+
# Determines whether the `pct=` field was specified.
|
|
175
|
+
#
|
|
176
|
+
# @return [Boolean]
|
|
177
|
+
#
|
|
178
|
+
# @since 0.4.0
|
|
179
|
+
#
|
|
180
|
+
def pct?
|
|
181
|
+
!@pct.nil?
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
#
|
|
185
|
+
# `pct` field.
|
|
186
|
+
#
|
|
187
|
+
# @return [Integer]
|
|
188
|
+
# The value of the `pct=` field, or `100` if the field was omitted.
|
|
189
|
+
#
|
|
190
|
+
def pct
|
|
191
|
+
@pct || 100
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
#
|
|
195
|
+
# Determines whether the `rf=` field was specified.
|
|
196
|
+
#
|
|
197
|
+
# @return [Boolean]
|
|
198
|
+
#
|
|
199
|
+
# @since 0.4.0
|
|
200
|
+
#
|
|
201
|
+
def rf?
|
|
202
|
+
!@rf.nil?
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
#
|
|
206
|
+
# `rf` field.
|
|
207
|
+
#
|
|
208
|
+
# @return [:afrf, :iodef]
|
|
209
|
+
# The value of the `rf=` field, or `:afrf` if the field was omitted.
|
|
210
|
+
#
|
|
211
|
+
def rf
|
|
212
|
+
@rf || :afrf
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
#
|
|
216
|
+
# Determines whether the `ri=` field was specified.
|
|
217
|
+
#
|
|
218
|
+
# @return [Boolean]
|
|
219
|
+
#
|
|
220
|
+
# @since 0.4.0
|
|
221
|
+
#
|
|
222
|
+
def ri?
|
|
223
|
+
!@ri.nil?
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
#
|
|
227
|
+
# `ri` field.
|
|
228
|
+
#
|
|
229
|
+
# @return [Integer]
|
|
230
|
+
# The value of the `ri=` field, or `86400` if the field was omitted.
|
|
231
|
+
#
|
|
232
|
+
def ri
|
|
233
|
+
@ri || 86400
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
#
|
|
237
|
+
# Determines if the `rua=` field was specified?
|
|
238
|
+
#
|
|
239
|
+
# @return [Boolean]
|
|
240
|
+
#
|
|
241
|
+
# @since 0.4.0
|
|
242
|
+
#
|
|
243
|
+
def rua?
|
|
244
|
+
!@rua.nil?
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
#
|
|
248
|
+
# Determines if the `ruf=` field was specified?
|
|
249
|
+
#
|
|
250
|
+
# @return [Boolean]
|
|
251
|
+
#
|
|
252
|
+
# @since 0.4.0
|
|
253
|
+
#
|
|
254
|
+
def ruf?
|
|
255
|
+
!@ruf.nil?
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
#
|
|
259
|
+
# Parses a DMARC record.
|
|
260
|
+
#
|
|
261
|
+
# @param [String] record
|
|
262
|
+
# The raw DMARC record.
|
|
263
|
+
#
|
|
264
|
+
# @return [Record]
|
|
265
|
+
# The parsed DMARC record.
|
|
266
|
+
#
|
|
267
|
+
# @raise [InvalidRecord]
|
|
268
|
+
# The DMARC record could not be parsed.
|
|
269
|
+
#
|
|
270
|
+
# @since 0.3.0
|
|
271
|
+
#
|
|
272
|
+
# @api public
|
|
273
|
+
#
|
|
274
|
+
def self.parse(record)
|
|
275
|
+
attributes = Parser.parse(record)
|
|
276
|
+
|
|
277
|
+
unless attributes[:p]
|
|
278
|
+
raise InvalidRecord, "DMARC record is missing the required 'p' tag"
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
new(attributes)
|
|
282
|
+
rescue Parslet::ParseFailed => error
|
|
283
|
+
raise InvalidRecord.new(truncate_error_message(error.message), error.cause)
|
|
284
|
+
rescue URI::Error => error
|
|
285
|
+
raise InvalidRecord.new(truncate_error_message(error.message), error)
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
#
|
|
289
|
+
# @deprecated use {parse} instead.
|
|
290
|
+
#
|
|
291
|
+
def self.from_txt(rec)
|
|
292
|
+
parse(rec)
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
# Maximum length of error messages echoed from parsing untrusted input.
|
|
296
|
+
MAX_ERROR_MESSAGE_LENGTH = 500
|
|
297
|
+
|
|
298
|
+
#
|
|
299
|
+
# Truncates error messages to avoid echoing large amounts of untrusted
|
|
300
|
+
# input in exceptions.
|
|
301
|
+
#
|
|
302
|
+
# @param [String] message
|
|
303
|
+
# The error message to truncate.
|
|
304
|
+
#
|
|
305
|
+
# @return [String]
|
|
306
|
+
# The truncated error message.
|
|
307
|
+
#
|
|
308
|
+
def self.truncate_error_message(message)
|
|
309
|
+
message = message.to_s
|
|
310
|
+
|
|
311
|
+
if message.length > MAX_ERROR_MESSAGE_LENGTH
|
|
312
|
+
message[0, MAX_ERROR_MESSAGE_LENGTH] + "..."
|
|
313
|
+
else
|
|
314
|
+
message
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
#
|
|
319
|
+
# Queries and parses the DMARC record for a domain.
|
|
320
|
+
#
|
|
321
|
+
# @param [String] domain
|
|
322
|
+
# The domain to query DMARC for.
|
|
323
|
+
#
|
|
324
|
+
# @param [Resolv::DNS] resolver
|
|
325
|
+
# The resolver to use.
|
|
326
|
+
#
|
|
327
|
+
# @return [Record, nil]
|
|
328
|
+
# The parsed DMARC record. If no DMARC record was found, `nil` will be
|
|
329
|
+
# returned.
|
|
330
|
+
#
|
|
331
|
+
# @raise [InvalidRecord]
|
|
332
|
+
# The DMARC record could not be parsed.
|
|
333
|
+
#
|
|
334
|
+
# @since 0.3.0
|
|
335
|
+
#
|
|
336
|
+
# @api public
|
|
337
|
+
#
|
|
338
|
+
def self.query(domain,resolver=Resolv::DNS.new)
|
|
339
|
+
if (dmarc = DMARCator.query(domain,resolver))
|
|
340
|
+
parse(dmarc)
|
|
341
|
+
end
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
#
|
|
345
|
+
# Converts the record to a Hash.
|
|
346
|
+
#
|
|
347
|
+
# @return [Hash{Symbol => Object}]
|
|
348
|
+
#
|
|
349
|
+
# @since 0.4.0
|
|
350
|
+
#
|
|
351
|
+
def to_h
|
|
352
|
+
hash = {}
|
|
353
|
+
|
|
354
|
+
hash[:v] = @v if @v
|
|
355
|
+
hash[:p] = @p if @p
|
|
356
|
+
hash[:sp] = @sp if @sp
|
|
357
|
+
hash[:rua] = @rua if @rua
|
|
358
|
+
hash[:ruf] = @ruf if @ruf
|
|
359
|
+
hash[:adkim] = @adkim if @adkim
|
|
360
|
+
hash[:aspf] = @aspf if @aspf
|
|
361
|
+
hash[:ri] = @ri if @ri
|
|
362
|
+
hash[:fo] = @fo if @fo
|
|
363
|
+
hash[:rf] = @rf if @rf
|
|
364
|
+
hash[:pct] = @pct if @pct
|
|
365
|
+
|
|
366
|
+
return hash
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
#
|
|
370
|
+
# Converts the record back to a DMARC String.
|
|
371
|
+
#
|
|
372
|
+
# @return [String]
|
|
373
|
+
#
|
|
374
|
+
def to_s
|
|
375
|
+
tags = []
|
|
376
|
+
|
|
377
|
+
tags << "v=#{@v}" if @v
|
|
378
|
+
tags << "p=#{@p}" if @p
|
|
379
|
+
tags << "sp=#{@sp}" if @sp
|
|
380
|
+
tags << "rua=#{@rua.join(',')}" if @rua
|
|
381
|
+
tags << "ruf=#{@ruf.join(',')}" if @ruf
|
|
382
|
+
tags << "adkim=#{@adkim}" if @adkim
|
|
383
|
+
tags << "aspf=#{@aspf}" if @aspf
|
|
384
|
+
tags << "ri=#{@ri}" if @ri
|
|
385
|
+
tags << "fo=#{@fo.join(':')}" if @fo
|
|
386
|
+
tags << "rf=#{@rf}" if @rf
|
|
387
|
+
tags << "pct=#{@pct}" if @pct
|
|
388
|
+
|
|
389
|
+
return tags.join('; ')
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
end
|
|
393
|
+
end
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
module DMARCator
|
|
2
|
+
#
|
|
3
|
+
# Represents a DMARC URI.
|
|
4
|
+
#
|
|
5
|
+
# @see https://tools.ietf.org/html/rfc7489#section-6.2
|
|
6
|
+
#
|
|
7
|
+
# @since 0.5.0
|
|
8
|
+
#
|
|
9
|
+
class Uri
|
|
10
|
+
|
|
11
|
+
# The `mailto:` URI.
|
|
12
|
+
#
|
|
13
|
+
# @return [URI::MailTo]
|
|
14
|
+
attr_reader :uri
|
|
15
|
+
|
|
16
|
+
# The optional maximum-size.
|
|
17
|
+
#
|
|
18
|
+
# @return [Integer, nil]
|
|
19
|
+
attr_reader :size
|
|
20
|
+
|
|
21
|
+
# The optional unit.
|
|
22
|
+
#
|
|
23
|
+
# @return [:k, :m, :g, :t, nil]
|
|
24
|
+
attr_reader :unit
|
|
25
|
+
|
|
26
|
+
#
|
|
27
|
+
# Initializes the DMARC URI.
|
|
28
|
+
#
|
|
29
|
+
# @param [URI::MailTo] uri
|
|
30
|
+
# The `mailto:` URI.
|
|
31
|
+
#
|
|
32
|
+
# @param [Integer] size
|
|
33
|
+
# The optional maximum-size.
|
|
34
|
+
#
|
|
35
|
+
# @param [:k, :m, :g, :t] unit
|
|
36
|
+
# The optional size unit.
|
|
37
|
+
#
|
|
38
|
+
def initialize(uri,size=nil,unit=nil)
|
|
39
|
+
@uri = uri
|
|
40
|
+
|
|
41
|
+
@size = size
|
|
42
|
+
@unit = unit
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
#
|
|
46
|
+
# Determines if a maximum-size was set.
|
|
47
|
+
#
|
|
48
|
+
# @return [Boolean]
|
|
49
|
+
#
|
|
50
|
+
def size?
|
|
51
|
+
!@size.nil?
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
#
|
|
55
|
+
# Determines if a size unit was set.
|
|
56
|
+
#
|
|
57
|
+
# @return [Boolean]
|
|
58
|
+
#
|
|
59
|
+
def unit?
|
|
60
|
+
!@unit.nil?
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
#
|
|
64
|
+
# Determines if the DMARC URI matches the other.
|
|
65
|
+
#
|
|
66
|
+
# @param [Object] other
|
|
67
|
+
# the other DMARC URI to compare against.
|
|
68
|
+
#
|
|
69
|
+
# @return [Boolean]
|
|
70
|
+
#
|
|
71
|
+
def ==(other)
|
|
72
|
+
(self.class == other.class) &&
|
|
73
|
+
(@uri == other.uri) &&
|
|
74
|
+
(@size == other.size) &&
|
|
75
|
+
(@unit == other.unit)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
#
|
|
79
|
+
# Converts the DMARC URI back into a String.
|
|
80
|
+
#
|
|
81
|
+
# @return [String]
|
|
82
|
+
#
|
|
83
|
+
def to_s
|
|
84
|
+
str = @uri.to_s
|
|
85
|
+
|
|
86
|
+
if (@size || @unit)
|
|
87
|
+
str << "!"
|
|
88
|
+
str << "#{@size}" if @size
|
|
89
|
+
str << "#{@unit}" if @unit
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
return str
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
protected
|
|
96
|
+
|
|
97
|
+
#
|
|
98
|
+
# Pass all missing methods to {#uri}.
|
|
99
|
+
#
|
|
100
|
+
def method_missing(name,*arguments,&block)
|
|
101
|
+
@uri.send(name,*arguments,&block)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
#
|
|
105
|
+
# Indicates whether a missing method can be handled by {#uri}.
|
|
106
|
+
#
|
|
107
|
+
# @return [Boolean]
|
|
108
|
+
#
|
|
109
|
+
def respond_to_missing?(name, include_private = false)
|
|
110
|
+
@uri.respond_to?(name, include_private) || super
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
end
|
|
114
|
+
end
|