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.
@@ -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
@@ -0,0 +1,4 @@
1
+ module DMARCator
2
+ # dmarcator version
3
+ VERSION = '0.1.0'
4
+ end
data/lib/dmarcator.rb ADDED
@@ -0,0 +1,5 @@
1
+ require 'dmarcator/exceptions'
2
+ require 'dmarcator/query'
3
+ require 'dmarcator/record'
4
+ require 'dmarcator/parser'
5
+ require 'dmarcator/version'