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,413 @@
1
+ require 'spec_helper'
2
+ require 'dmarcator/parser'
3
+
4
+ describe Parser do
5
+ describe "rules" do
6
+ describe 'dmarc_uri' do
7
+ subject { described_class.new.dmarc_uri }
8
+
9
+ let(:uri) { 'mailto:user@example.org' }
10
+
11
+ it 'parses mailto URIs' do
12
+ expect(subject.parse(uri)).to eq(uri: uri)
13
+ end
14
+
15
+ it 'parses mailto URIs with size' do
16
+ expect(subject.parse(uri + '!20')).to eq(uri: uri, size: '20')
17
+ end
18
+
19
+ it 'parses mailto URIs with size and unit' do
20
+ expect(subject.parse(uri + '!20k')).to eq(uri: uri, size: '20', unit: 'k')
21
+ end
22
+ end
23
+
24
+ describe 'dmarc_record' do
25
+ subject { described_class.new.dmarc_record }
26
+
27
+ it 'parses version and policy' do
28
+ record = 'v=DMARC1;p=none'
29
+ expect(subject.parse record).to eq([
30
+ {v: 'DMARC1'},
31
+ {p: 'none'}
32
+ ])
33
+ end
34
+
35
+ it 'parses version, policy, and other tags' do
36
+ record = 'v=DMARC1;p=none;sp=reject;adkim=r;aspf=r'
37
+ expect(subject.parse(record)).to eq([
38
+ {v: 'DMARC1'},
39
+ {p: 'none'},
40
+ {sp: 'reject'},
41
+ {adkim: 'r'},
42
+ {aspf: 'r'}
43
+ ])
44
+ end
45
+
46
+ it 'parses version, policy, and rua' do
47
+ record = 'v=DMARC1;p=quarantine;rua=mailto:foo@example.com,mailto:bar@example.com'
48
+ expect(subject.parse record).to eq([
49
+ {v: 'DMARC1'},
50
+ {p: 'quarantine'},
51
+ {rua: [
52
+ {uri: 'mailto:foo@example.com'},
53
+ {uri: 'mailto:bar@example.com'}
54
+ ]}
55
+ ])
56
+ end
57
+
58
+ it 'ignores order of tags' do
59
+ record = 'v=DMARC1; rua=mailto:foo@example.com; p=none'
60
+
61
+ expect(subject.parse(record)).to eq([
62
+ {v: 'DMARC1'},
63
+ {rua: {uri: 'mailto:foo@example.com'}},
64
+ {p: 'none'}
65
+ ])
66
+ end
67
+
68
+ it 'ignores spacing' do
69
+ record1 = 'v=DMARC1;p=none;sp=reject'
70
+ record2 = 'v = DMARC1 ; p = none ; sp = reject'
71
+ expect(subject.parse record1).to eq(subject.parse record2)
72
+ end
73
+
74
+ it 'ignores trailing separators' do
75
+ record = 'v=DMARC1; p=none; '
76
+
77
+ expect(subject.parse(record)).to eq([
78
+ {v: 'DMARC1'},
79
+ {p: 'none'}
80
+ ])
81
+ end
82
+
83
+ it 'ignores trailing spacing without final separator' do
84
+ record = 'v=DMARC1; p=none '
85
+
86
+ expect(subject.parse(record)).to eq([
87
+ {v: 'DMARC1'},
88
+ {p: 'none'}
89
+ ])
90
+ end
91
+
92
+ it "ignores unknown tags" do
93
+ record = 'v=DMARC1;p=none;foo=xxx;sp=reject;bar=xxx;adkim=r;aspf=r'
94
+ expect(subject.parse(record)).to eq([
95
+ {v: 'DMARC1'},
96
+ {p: 'none'},
97
+ {sp: 'reject'},
98
+ {adkim: 'r'},
99
+ {aspf: 'r'}
100
+ ])
101
+ end
102
+
103
+ it "ignores syntax errors" do
104
+ record = 'v=DMARC1;p=none;sp;adkim=X;aspf='
105
+ expect(subject.parse(record)).to eq([
106
+ {v: 'DMARC1'},
107
+ {p: 'none'}
108
+ ])
109
+ end
110
+ end
111
+
112
+ describe 'dmarc_version' do
113
+ subject { described_class.new.dmarc_version }
114
+
115
+ it 'parses DMARC versions' do
116
+ expect(subject.parse('v=DMARC1')).to eq(v: 'DMARC1')
117
+ end
118
+
119
+ it 'ignores spacing' do
120
+ expect(subject.parse 'v = DMARC1').to eq(subject.parse 'v=DMARC1')
121
+ end
122
+ end
123
+
124
+ describe 'dmarc_request' do
125
+ subject { described_class.new.dmarc_request }
126
+
127
+ it 'parses "none" requests' do
128
+ expect(subject.parse('p=none')).to eq(p: 'none')
129
+ end
130
+
131
+ it 'parses quarantine requests' do
132
+ expect(subject.parse('p=quarantine')).to eq(p: 'quarantine')
133
+ end
134
+
135
+ it 'parses reject requests' do
136
+ expect(subject.parse('p=reject')).to eq(p: 'reject')
137
+ end
138
+
139
+ it 'ingores spacing' do
140
+ expect(subject.parse 'p=none').to eq(subject.parse 'p = none')
141
+ end
142
+ end
143
+
144
+ describe 'dmarc_srequest' do
145
+ subject { described_class.new.dmarc_srequest }
146
+
147
+ it 'parses "none" requests' do
148
+ expect(subject.parse('sp=none')).to eq(sp: 'none')
149
+ end
150
+
151
+ it 'parses quarantine requests' do
152
+ expect(subject.parse('sp=quarantine')).to eq(sp: 'quarantine')
153
+ end
154
+
155
+ it 'parses reject requests' do
156
+ expect(subject.parse('sp=reject')).to eq(sp: 'reject')
157
+ end
158
+
159
+ it 'ignores spacing' do
160
+ expect(subject.parse 'sp=none').to eq(subject.parse 'sp = none')
161
+ end
162
+ end
163
+
164
+ describe 'dmarc_auri' do
165
+ subject { described_class.new.dmarc_auri }
166
+
167
+ it 'parses one URI' do
168
+ expect(subject.parse('rua=mailto:user@example.org')).to eq(
169
+ rua: {
170
+ uri: 'mailto:user@example.org'
171
+ }
172
+ )
173
+ end
174
+
175
+ it 'parses many URIs' do
176
+ expect(
177
+ subject.parse('rua = mailto:user1@example.org, mailto:user2@example.org')
178
+ ).to eq(
179
+ rua: [
180
+ {uri: 'mailto:user1@example.org'},
181
+ {uri: 'mailto:user2@example.org'}
182
+ ]
183
+ )
184
+ end
185
+
186
+ it 'parses maximum report size' do
187
+ expect(subject.parse('rua = mailto:user1@example.com!20m')).to eq(
188
+ rua: {
189
+ uri: 'mailto:user1@example.com',
190
+ size: '20',
191
+ unit: 'm'
192
+ }
193
+ )
194
+ end
195
+ end
196
+
197
+ describe 'dmarc_ainterval' do
198
+ subject { described_class.new.dmarc_ainterval }
199
+
200
+ it 'parses a one digit interval' do
201
+ expect(subject.parse('ri=1')).to eq(ri: '1')
202
+ end
203
+
204
+ it 'parses a many digit interval' do
205
+ expect(subject.parse('ri=86400')).to eq(ri: '86400')
206
+ end
207
+
208
+ it 'ignores spacing' do
209
+ expect(subject.parse 'ri = 86400').to eq(subject.parse 'ri=86400')
210
+ end
211
+ end
212
+
213
+ describe 'dmarc_furi' do
214
+ subject { described_class.new.dmarc_furi }
215
+
216
+ it 'parses one URI' do
217
+ expect(subject.parse('ruf=mailto:user@example.org')).to eq(
218
+ ruf: {
219
+ uri: 'mailto:user@example.org'
220
+ }
221
+ )
222
+ end
223
+
224
+ it 'parses many URIs' do
225
+ expect(
226
+ subject.parse('ruf = mailto:user1@example.org, mailto:user2@example.org')
227
+ ).to eq(
228
+ ruf: [
229
+ {uri: 'mailto:user1@example.org'},
230
+ {uri: 'mailto:user2@example.org'}
231
+ ]
232
+ )
233
+ end
234
+
235
+ it 'parses maximum report size' do
236
+ expect(subject.parse('ruf = mailto:user1@example.com!20m')).to eq(
237
+ ruf: {
238
+ uri: 'mailto:user1@example.com',
239
+ size: '20',
240
+ unit: 'm'
241
+ }
242
+ )
243
+ end
244
+ end
245
+
246
+ describe 'dmarc_fo' do
247
+ let(:fo) { described_class.new.dmarc_fo }
248
+
249
+ context 'one value' do
250
+ %w[0 1 d s].each do |value|
251
+ it "parses #{value}" do
252
+ expect(fo.parse("fo=#{value}")).to eq(fo: {fo_opt: value})
253
+ end
254
+ end
255
+ end
256
+
257
+ it 'parses many values' do
258
+ expect(fo.parse('fo=0:1:d:s')).to eq(
259
+ fo: [{fo_opt: '0'}, {fo_opt: '1'}, {fo_opt: 'd'}, {fo_opt: 's'}]
260
+ )
261
+ end
262
+ end
263
+
264
+ describe 'dmarc_rfmt' do
265
+ subject { described_class.new.dmarc_rfmt }
266
+
267
+ it 'parses afrf format' do
268
+ expect(subject.parse('rf=afrf')).to eq(rf: 'afrf')
269
+ end
270
+
271
+ it 'parses iodef format' do
272
+ expect(subject.parse('rf=iodef')).to eq(rf: 'iodef')
273
+ end
274
+
275
+ it 'ignores spacing' do
276
+ expect(subject.parse 'rf=iodef').to eq(subject.parse 'rf = iodef')
277
+ end
278
+ end
279
+
280
+ describe 'dmarc_percent' do
281
+ subject { described_class.new.dmarc_percent }
282
+
283
+ it 'parses a one-digit percent' do
284
+ expect(subject.parse('pct=1')).to eq(pct: '1')
285
+ end
286
+
287
+ it 'parses a two-digit percent' do
288
+ expect(subject.parse('pct=10')).to eq(pct: '10')
289
+ end
290
+
291
+ it 'parses a three-digit percent' do
292
+ expect(subject.parse('pct=100')).to eq(pct: '100')
293
+ end
294
+
295
+ it 'ignores spacing' do
296
+ expect(subject.parse 'pct = 100').to eq(subject.parse 'pct=100')
297
+ end
298
+ end
299
+
300
+ describe 'dmarc_adkim' do
301
+ subject { described_class.new.dmarc_adkim }
302
+
303
+ it 'parses a relaxed DKIM policy' do
304
+ expect(subject.parse('adkim=r')).to eq(adkim: 'r')
305
+ end
306
+
307
+ it 'parses a strict DKIM policy' do
308
+ expect(subject.parse('adkim=s')).to eq(adkim: 's')
309
+ end
310
+
311
+ it 'ignores spacing' do
312
+ expect(subject.parse 'adkim=r').to eq(subject.parse 'adkim = r')
313
+ end
314
+ end
315
+
316
+ describe 'dmarc_aspf' do
317
+ subject { described_class.new.dmarc_aspf }
318
+
319
+ it 'parses a relaxed SPF policy' do
320
+ expect(subject.parse('aspf=r')).to eq(aspf: 'r')
321
+ end
322
+
323
+ it 'parses a strict SPF policy' do
324
+ expect(subject.parse('aspf=s')).to eq(aspf: 's')
325
+ end
326
+
327
+ it 'ignores spacing' do
328
+ expect(subject.parse 'aspf=s').to eq(subject.parse 'aspf = s')
329
+ end
330
+ end
331
+ end
332
+
333
+ describe Parser::Transform do
334
+ let(:tree) do
335
+ [
336
+ {v: 'DMARC1'},
337
+ {p: 'none'},
338
+ {sp: 'reject'},
339
+ {pct: '100'},
340
+ {adkim: 'r'},
341
+ {aspf: 'r'},
342
+ {fo: [{fo_opt: '0'}, {fo_opt: '1'}, {fo_opt: 'd'}, {fo_opt: 's'}]},
343
+ {rua: {uri: "mailto:d@rua.agari.com"}}
344
+ ]
345
+ end
346
+
347
+ subject { described_class.new.apply(tree) }
348
+
349
+ it "should coerce :v into a Symbol" do
350
+ expect(subject).to include(v: :DMARC1)
351
+ end
352
+
353
+ it "should coerce :p into a Symbol" do
354
+ expect(subject).to include(p: :none)
355
+ end
356
+
357
+ it "should coerce :sp into a Symbol" do
358
+ expect(subject).to include(sp: :reject)
359
+ end
360
+
361
+ it "should flatten :fo options" do
362
+ expect(subject).to include(fo: %w[0 1 d s])
363
+ end
364
+
365
+ it "should coerce :pct into an Integer" do
366
+ expect(subject).to include(pct: 100)
367
+ end
368
+
369
+ it "should coerce :adkim into a Symbol" do
370
+ expect(subject).to include(adkim: :r)
371
+ end
372
+
373
+ it "should coerce :aspf into a Symbol" do
374
+ expect(subject).to include(aspf: :r)
375
+ end
376
+
377
+ it "should convert {uri: ...} to Uri objects" do
378
+ expect(subject).to include(rua: [Uri.new(URI("mailto:d@rua.agari.com"))])
379
+ end
380
+ end
381
+
382
+ describe ".parse" do
383
+ subject { described_class }
384
+
385
+ let(:record) { "v=DMARC1;p=none;pct=100;fo=0:1:d:s" }
386
+
387
+ it "should combine all tags into one Hash" do
388
+ expect(subject.parse(record)).to eq(
389
+ v: :DMARC1,
390
+ p: :none,
391
+ pct: 100,
392
+ fo: ['0', '1', 'd', 's']
393
+ )
394
+ end
395
+
396
+ context "Alexa Top 500", :gauntlet do
397
+ require 'csv'
398
+
399
+ path = File.expand_path('../data/alexa.csv', __FILE__)
400
+ csv = CSV.new(open(path), headers: false)
401
+
402
+ csv.each do |row|
403
+ rank, domain, dmarc = row
404
+
405
+ if dmarc
406
+ it "should parse #{dmarc.inspect}" do
407
+ expect { subject.parse(dmarc) }.to_not raise_error
408
+ end
409
+ end
410
+ end
411
+ end
412
+ end
413
+ end
@@ -0,0 +1,229 @@
1
+ require 'spec_helper'
2
+ require 'dmarcator/record'
3
+
4
+ describe Record do
5
+ describe '#initialize' do
6
+ let(:attributes) do
7
+ {
8
+ v: :DMARC1,
9
+ p: :none,
10
+ adkim: :r
11
+ }
12
+ end
13
+
14
+ subject { described_class.new(attributes) }
15
+
16
+ it 'assigns the fields to its properties' do
17
+ expect(subject.v).to be :DMARC1
18
+ expect(subject.p).to be :none
19
+ expect(subject.adkim).to be :r
20
+ end
21
+
22
+ it 'gives "sp" the same value as "p" if undefined' do
23
+ expect(subject.sp).to be :none
24
+ end
25
+ end
26
+
27
+ context 'with default values' do
28
+ subject { described_class.new(v: :DMARC1) }
29
+
30
+ describe "#adkim" do
31
+ it "should return :r" do
32
+ expect(subject.adkim).to be == :r
33
+ end
34
+ end
35
+
36
+ describe "#aspf" do
37
+ it "should return :r" do
38
+ expect(subject.aspf).to be == :r
39
+ end
40
+ end
41
+
42
+ describe "#fo" do
43
+ it "should return ['0']" do
44
+ expect(subject.fo).to be == ['0']
45
+ end
46
+ end
47
+
48
+ describe "#pct" do
49
+ it "should return 100" do
50
+ expect(subject.pct).to be == 100
51
+ end
52
+ end
53
+
54
+ describe "#rf" do
55
+ it "should return afrf" do
56
+ expect(subject.rf).to be == :afrf
57
+ end
58
+ end
59
+
60
+ describe "#ri" do
61
+ it "should return 86400" do
62
+ expect(subject.ri).to be == 86400
63
+ end
64
+ end
65
+ end
66
+
67
+ describe '.parse' do
68
+ subject { described_class }
69
+
70
+ context 'with a valid record' do
71
+ it 'parse and returns a record' do
72
+ rec = subject.parse('v=DMARC1; p=quarantine')
73
+
74
+ expect(rec).to be_a Record
75
+ expect(rec.p).to eq :quarantine
76
+ end
77
+ end
78
+
79
+ context 'with an invalid record' do
80
+ it 'raises an InvalidRecord error' do
81
+ expect {
82
+ subject.parse('v=XXXXXXXXXX')
83
+ }.to raise_error(InvalidRecord)
84
+ end
85
+
86
+ it 'raises when the p tag is missing' do
87
+ expect {
88
+ subject.parse('v=DMARC1')
89
+ }.to raise_error(InvalidRecord)
90
+ end
91
+
92
+ it 'raises when pct is out of range' do
93
+ expect {
94
+ subject.parse('v=DMARC1; p=none; pct=999')
95
+ }.to raise_error(InvalidRecord)
96
+ end
97
+
98
+ it 'raises when tags are duplicated' do
99
+ expect {
100
+ subject.parse('v=DMARC1; p=none; p=reject')
101
+ }.to raise_error(InvalidRecord)
102
+ end
103
+ end
104
+
105
+ context 'with a non-mailto rua URI' do
106
+ it 'ignores the invalid rua tag' do
107
+ rec = subject.parse('v=DMARC1; p=none; rua=http://169.254.169.254/?x=1')
108
+
109
+ expect(rec).to be_a Record
110
+ expect(rec.rua).to be_nil
111
+ end
112
+ end
113
+ end
114
+
115
+ describe ".query" do
116
+ subject { described_class }
117
+
118
+ context "when the domain has a DMARC record" do
119
+ it "queries and parses the DMARC record" do
120
+ expect(DMARCator).to receive(:query)
121
+ .with("google.com", anything)
122
+ .and_return("v=DMARC1; p=reject")
123
+
124
+ record = subject.query("google.com")
125
+
126
+ expect(record).to be_kind_of(Record)
127
+ expect(record.v).to be == :DMARC1
128
+ end
129
+ end
130
+
131
+ context "when the domain has no DMARC record" do
132
+ it "returns nil" do
133
+ expect(DMARCator).to receive(:query)
134
+ .with("foobar.heythere", anything)
135
+ .and_return(nil)
136
+
137
+ expect(subject.query('foobar.heythere')).to be_nil
138
+ end
139
+ end
140
+ end
141
+
142
+ shared_examples "question mark method" do |field,value|
143
+ describe "#{field}?" do
144
+ context "when #{field} is set" do
145
+ subject do
146
+ described_class.new(v: :DMARC1, field => value)
147
+ end
148
+
149
+ it { expect(subject.send(:"#{field}?")).to be(true) }
150
+ end
151
+
152
+ context "when #{field} is omitted" do
153
+ subject { described_class.new(v: :DMARC1) }
154
+
155
+ it { expect(subject.send(:"#{field}?")).to be(false) }
156
+ end
157
+ end
158
+ end
159
+
160
+ include_examples "question mark method", :adkim, :r
161
+ include_examples "question mark method", :aspf, :r
162
+ include_examples "question mark method", :fo, %w[0 1 d]
163
+ include_examples "question mark method", :p, :reject
164
+ include_examples "question mark method", :pct, 100
165
+ include_examples "question mark method", :rf, :afrf
166
+ include_examples "question mark method", :ri, 86400
167
+ include_examples "question mark method", :rua, [URI("mailto:bob@example.com")]
168
+ include_examples "question mark method", :ruf, [URI("mailto:bob@example.com")]
169
+ include_examples "question mark method", :sp, :reject
170
+
171
+ describe "#sp?" do
172
+ context "when sp is set" do
173
+ end
174
+ end
175
+
176
+ describe "#to_h" do
177
+ let(:v) { :DMARC1 }
178
+ let(:p) { :reject }
179
+ let(:rua) { [URI.parse('mailto:d@rua.agari.com')] }
180
+ let(:ruf) { [URI.parse('mailto:d@rua.agari.com')] }
181
+ let(:fo) { %w[0 1 d] }
182
+
183
+ subject do
184
+ described_class.new(
185
+ v: v,
186
+ p: p,
187
+ rua: rua,
188
+ ruf: ruf,
189
+ fo: fo
190
+ )
191
+ end
192
+
193
+ it "should return a Hash" do
194
+ expect(subject.to_h).to be_kind_of(Hash)
195
+ end
196
+
197
+ it "should only include fields that have values" do
198
+ expect(subject.to_h).to be == {
199
+ v: v,
200
+ p: p,
201
+ rua: rua,
202
+ ruf: ruf,
203
+ fo: fo
204
+ }
205
+ end
206
+ end
207
+
208
+ describe "#to_s" do
209
+ let(:v) { :DMARC1 }
210
+ let(:p) { :reject }
211
+ let(:rua) { [URI.parse('mailto:d@rua.agari.com')] }
212
+ let(:ruf) { [URI.parse('mailto:d@rua.agari.com')] }
213
+ let(:fo) { %w[0 1 d] }
214
+
215
+ subject do
216
+ described_class.new(
217
+ v: v,
218
+ p: p,
219
+ rua: rua,
220
+ ruf: ruf,
221
+ fo: fo
222
+ )
223
+ end
224
+
225
+ it "should convert the record to a String" do
226
+ expect(subject.to_s).to be == "v=#{v}; p=#{p}; rua=#{rua[0]}; ruf=#{ruf[0]}; fo=#{fo[0]}:#{fo[1]}:#{fo[2]}"
227
+ end
228
+ end
229
+ end
@@ -0,0 +1,11 @@
1
+ require 'simplecov'
2
+ SimpleCov.start
3
+
4
+ require 'rspec'
5
+ require 'dmarcator'
6
+
7
+ include DMARCator
8
+
9
+ RSpec.configure do |specs|
10
+ specs.filter_run_excluding :gauntlet
11
+ end