dmarc 0.2.0 → 0.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/.travis.yml +15 -5
- data/ChangeLog.md +13 -1
- data/Gemfile +5 -0
- data/README.md +43 -2
- data/dmarc.gemspec +1 -0
- data/lib/dmarc.rb +1 -0
- data/lib/dmarc/dmarc.rb +30 -0
- data/lib/dmarc/exceptions.rb +3 -16
- data/lib/dmarc/parser.rb +29 -17
- data/lib/dmarc/record.rb +190 -17
- data/lib/dmarc/version.rb +1 -1
- data/spec/dmarc_spec.rb +24 -0
- data/spec/parser_spec.rb +285 -239
- data/spec/record_spec.rb +91 -35
- data/spec/spec_helper.rb +3 -0
- metadata +18 -15
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9a12630bf22487064d3d35386f55e7af00f6b542
|
4
|
+
data.tar.gz: 35f68803f6820445c7e38d9e37078737075807e4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3214787e24b80ce2eb162473d43e9e77f152d8e1ff6cfcf1fbbb2fadfe8d565b27c4e0b97c3c17180783e10ff6c62a18714c8be3925a97309a886f79a7d2eee4
|
7
|
+
data.tar.gz: f7ed4b603e8541e97c7d29d733f45d5b7f93ee1d737f9cc70916c1459098a570a01e1cdece1b2b182dd683c8b2322d279ed4db2c3fbe5187ec8be9eb2389d7fe
|
data/.travis.yml
CHANGED
@@ -1,7 +1,17 @@
|
|
1
1
|
language: ruby
|
2
2
|
rvm:
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
3
|
+
- 1.9.3
|
4
|
+
- 2.0
|
5
|
+
- 2.1
|
6
|
+
- 2.2
|
7
|
+
- jruby
|
8
|
+
- rbx-2
|
9
|
+
matrix:
|
10
|
+
allow_failures:
|
11
|
+
- rvm: rbx-2
|
12
|
+
addons:
|
13
|
+
code_climate:
|
14
|
+
repo_token: 27519af871c1d9576b91e6f264e841eb7b7babbdcda315bf94fcbd095c202949
|
15
|
+
notifications:
|
16
|
+
slack:
|
17
|
+
secure: Jqj30lwH2fnyu8adMI2nXa7rfF70o5JSrL0ZOe425XVu3YMMpfIp8J4DD0Ks/nCxgGRhJpq35oAHXjKmJAEHyda9oxx7GV9QwOuu2oviUrW3VUQQj9UsXPQP0sc0GD9BPI+KVNp4dbGki1ik90I6HVVsqDauek/z5MWt6MJY+tA=
|
data/ChangeLog.md
CHANGED
@@ -1,6 +1,18 @@
|
|
1
|
+
### 0.3.0 / 2015-07-01
|
2
|
+
|
3
|
+
* Added {DMARC::Record.query}.
|
4
|
+
* Added {DMARC::Record#to_s}.
|
5
|
+
* Added {DMARC::InvalidRecord}.
|
6
|
+
* Deprecate {DMARC::Record.from_txt}.
|
7
|
+
* {DMARC::Record#v} now returns `:DMARC1`.
|
8
|
+
* {DMARC::Record#p} and {DMARC::Record#sp} now return Symbols
|
9
|
+
* {DMARC::Record#rua} and {DMARC::Record#ruf} will always return Arrays.
|
10
|
+
* Fixed a bug in {DMARC::Parser} with respect to order of tags.
|
11
|
+
* {DMARC::Parser::Transform} now coerces URIs into URI objects.
|
12
|
+
|
1
13
|
### 0.2.0 / 2014-10-20
|
2
14
|
|
3
|
-
* Added
|
15
|
+
* Added `DMARC::Error`.
|
4
16
|
* Added {DMARC::InvalidRecord}.
|
5
17
|
* Add support for parsing `fo` tokens.
|
6
18
|
* Ignore unknown tags instead of raising a parser exception.
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -3,6 +3,7 @@
|
|
3
3
|
[](https://codeclimate.com/github/trailofbits/dmarc) [](https://travis-ci.org/trailofbits/dmarc)
|
4
4
|
[](http://badge.fury.io/rb/dmarc)
|
5
5
|
[](http://rubydoc.info/gems/dmarc)
|
6
|
+
[](https://codeclimate.com/github/trailofbits/dmarc)
|
6
7
|
|
7
8
|
[DMARC] is a technical specification intended to solve a couple of long-standing
|
8
9
|
email authentication problems. DMARC policies are described in DMARC "records,"
|
@@ -11,9 +12,49 @@ parser for DMARC records.
|
|
11
12
|
|
12
13
|
## Example
|
13
14
|
|
15
|
+
Parse a SPF record:
|
16
|
+
|
14
17
|
require 'dmarc'
|
15
18
|
|
16
|
-
record = DMARC::Record.
|
19
|
+
record = DMARC::Record.parse("v=DMARC1; p=reject; rua=mailto:d@rua.agari.com; ruf=mailto:d@ruf.agari.com; fo=1")
|
20
|
+
|
21
|
+
record.v
|
22
|
+
# => :DMARC1
|
23
|
+
|
24
|
+
record.adkim
|
25
|
+
# => :r
|
26
|
+
|
27
|
+
record.aspf
|
28
|
+
# => :r
|
29
|
+
|
30
|
+
record.fo
|
31
|
+
# => ["0"]
|
32
|
+
|
33
|
+
record.p
|
34
|
+
# => :reject
|
35
|
+
|
36
|
+
record.pct
|
37
|
+
# => 100
|
38
|
+
|
39
|
+
record.rf
|
40
|
+
# => :afrf
|
41
|
+
|
42
|
+
record.ri
|
43
|
+
# => 86400
|
44
|
+
|
45
|
+
record.rua
|
46
|
+
# => [#<URI::MailTo:0x000000034a1cc8 URL:mailto:d@rua.agari.com>]
|
47
|
+
|
48
|
+
record.ruf
|
49
|
+
# => [#<URI::MailTo:0x000000034a02b0 URL:mailto:d@ruf.agari.com>]
|
50
|
+
|
51
|
+
record.sp
|
52
|
+
# => :reject
|
53
|
+
|
54
|
+
Query the SPF record for a domain:
|
55
|
+
|
56
|
+
record = DMARC::Record.query('twitter.com')
|
57
|
+
# => #<DMARC::Record:0x0000000313bd90 @adkim=:r, @aspf=:r, @fo=["1"@79], @p=:reject, @pct=100, @rf=:afrf, @ri=86400, @rua=[#<URI::MailTo:0x00000003124e38 URL:mailto:d@rua.agari.com>], @ruf=[#<URI::MailTo:0x00000003132678 URL:mailto:d@ruf.agari.com>], @sp=:reject, @v=:DMARC1>
|
17
58
|
|
18
59
|
## Requirements
|
19
60
|
|
@@ -37,5 +78,5 @@ To test the parser against the Alexa Top 500:
|
|
37
78
|
|
38
79
|
See the {file:LICENSE.txt} file.
|
39
80
|
|
40
|
-
[DMARC]:
|
81
|
+
[DMARC]: https://tools.ietf.org/html/rfc7489
|
41
82
|
[parslet]: http://kschiess.github.io/parslet/
|
data/dmarc.gemspec
CHANGED
data/lib/dmarc.rb
CHANGED
data/lib/dmarc/dmarc.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'resolv'
|
2
|
+
|
3
|
+
module DMARC
|
4
|
+
#
|
5
|
+
# Queries a domain for the DMARC record.
|
6
|
+
#
|
7
|
+
# @param [String] domain
|
8
|
+
# The domain to query DMARC for.
|
9
|
+
#
|
10
|
+
# @param [Resolv::DNS] resolver
|
11
|
+
# The resolver to use.
|
12
|
+
#
|
13
|
+
# @return [String, nil]
|
14
|
+
# The domain's DMARC record or `nil` if none exists.
|
15
|
+
#
|
16
|
+
# @api semipublic
|
17
|
+
#
|
18
|
+
# @since 0.3.0
|
19
|
+
#
|
20
|
+
def self.query(domain,resolver=Resolv::DNS.new)
|
21
|
+
host = "_dmarc.#{domain}"
|
22
|
+
|
23
|
+
begin
|
24
|
+
return resolver.getresource(
|
25
|
+
host, Resolv::DNS::Resource::IN::TXT
|
26
|
+
).strings.join
|
27
|
+
rescue Resolv::ResolvError
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
data/lib/dmarc/exceptions.rb
CHANGED
@@ -1,19 +1,6 @@
|
|
1
|
-
|
2
|
-
class Error < StandardError; end
|
3
|
-
class InvalidRecord < Error
|
4
|
-
attr_reader :original
|
5
|
-
|
6
|
-
def initialize(msg = nil, original = $!)
|
7
|
-
super msg
|
8
|
-
@original = original
|
9
|
-
end
|
1
|
+
require 'parslet'
|
10
2
|
|
11
|
-
|
12
|
-
|
13
|
-
# Not to be confused with ruby 2.1's Exception#cause method
|
14
|
-
if self.original != nil
|
15
|
-
self.original.cause.ascii_tree
|
16
|
-
end
|
17
|
-
end
|
3
|
+
module DMARC
|
4
|
+
class InvalidRecord < Parslet::ParseFailed
|
18
5
|
end
|
19
6
|
end
|
data/lib/dmarc/parser.rb
CHANGED
@@ -1,13 +1,19 @@
|
|
1
1
|
require 'parslet'
|
2
2
|
|
3
|
+
require 'uri'
|
4
|
+
|
3
5
|
module DMARC
|
6
|
+
#
|
7
|
+
# DMARC parser.
|
8
|
+
#
|
9
|
+
# @see https://tools.ietf.org/html/rfc7489#section-6.4
|
10
|
+
#
|
4
11
|
class Parser < Parslet::Parser
|
5
12
|
|
6
13
|
root :dmarc_record
|
7
14
|
|
8
15
|
rule(:dmarc_record) do
|
9
|
-
dmarc_version.repeat(1,1) >>
|
10
|
-
dmarc_request.maybe >>
|
16
|
+
dmarc_version.repeat(1,1) >>
|
11
17
|
(dmarc_sep >> dmarc_tag).repeat >>
|
12
18
|
dmarc_sep.maybe
|
13
19
|
end
|
@@ -20,15 +26,8 @@ module DMARC
|
|
20
26
|
str('DMARC1').as(:v)
|
21
27
|
end
|
22
28
|
|
23
|
-
rule(:dmarc_request) do
|
24
|
-
str('p') >> wsp? >> str('=') >> wsp? >> (
|
25
|
-
str('none') |
|
26
|
-
str('quarantine') |
|
27
|
-
str('reject')
|
28
|
-
).as(:p)
|
29
|
-
end
|
30
|
-
|
31
29
|
rule(:dmarc_tag) do
|
30
|
+
dmarc_request |
|
32
31
|
dmarc_srequest |
|
33
32
|
dmarc_auri |
|
34
33
|
dmarc_furi |
|
@@ -48,6 +47,10 @@ module DMARC
|
|
48
47
|
end
|
49
48
|
end
|
50
49
|
|
50
|
+
tag_rule(:request,'p') do
|
51
|
+
str('none') | str('quarantine') | str('reject')
|
52
|
+
end
|
53
|
+
|
51
54
|
tag_rule(:srequest,'sp') do
|
52
55
|
str('none') | str('quarantine') | str('reject')
|
53
56
|
end
|
@@ -167,13 +170,22 @@ module DMARC
|
|
167
170
|
|
168
171
|
class Transform < Parslet::Transform
|
169
172
|
|
170
|
-
rule(:
|
173
|
+
rule(fo_opt: simple(:fo_opt)) { fo_opt }
|
174
|
+
rule(fo: simple(:fo_opt)) { {fo: [fo_opt]} }
|
175
|
+
|
176
|
+
rule(v: simple(:version)) { {v: version.to_sym} }
|
177
|
+
rule(p: simple(:p)) { {p: p.to_sym } }
|
178
|
+
rule(sp: simple(:sp)) { {sp: sp.to_sym} }
|
179
|
+
rule(rf: simple(:rf)) { {rf: rf.to_sym} }
|
180
|
+
rule(adkim: simple(:adkim)) { {adkim: adkim.to_sym} }
|
181
|
+
rule(aspf: simple(:aspf)) { {aspf: aspf.to_sym} }
|
171
182
|
|
172
|
-
rule(:
|
173
|
-
rule(:
|
183
|
+
rule(pct: simple(:pct)) { {pct: pct.to_i} }
|
184
|
+
rule(ri: simple(:ri)) { {ri: ri.to_i} }
|
174
185
|
|
175
|
-
rule(:
|
176
|
-
rule(:
|
186
|
+
rule(uri: simple(:uri)) { URI.parse(uri) }
|
187
|
+
rule(rua: subtree(:uris)) { {rua: Array(uris)} }
|
188
|
+
rule(ruf: subtree(:uris)) { {ruf: Array(uris)} }
|
177
189
|
|
178
190
|
end
|
179
191
|
|
@@ -186,8 +198,8 @@ module DMARC
|
|
186
198
|
# @return [Hash{Symbol => Object}]
|
187
199
|
# The Hash of tags within the record.
|
188
200
|
#
|
189
|
-
def parse(record)
|
190
|
-
tags = Transform.new.apply(
|
201
|
+
def self.parse(record)
|
202
|
+
tags = Transform.new.apply(new.parse(record))
|
191
203
|
hash = {}
|
192
204
|
|
193
205
|
tags.each { |tag| hash.merge!(tag) }
|
data/lib/dmarc/record.rb
CHANGED
@@ -1,30 +1,203 @@
|
|
1
|
+
require 'dmarc/dmarc'
|
1
2
|
require 'dmarc/parser'
|
2
3
|
require 'dmarc/exceptions'
|
3
4
|
|
5
|
+
require 'resolv'
|
6
|
+
|
4
7
|
module DMARC
|
5
|
-
class Record
|
8
|
+
class Record
|
6
9
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
10
|
+
# `p` field.
|
11
|
+
#
|
12
|
+
# @return [:none, :quarantine, :reject]
|
13
|
+
attr_reader :p
|
14
|
+
|
15
|
+
# `rua` field.
|
16
|
+
#
|
17
|
+
# @return [Array<URI::MailTo>]
|
18
|
+
attr_reader :rua
|
12
19
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
pct: 100,
|
18
|
-
rf: 'afrf',
|
19
|
-
ri: 86400,
|
20
|
-
}
|
20
|
+
# `rua` field.
|
21
|
+
#
|
22
|
+
# @return [Array<URI::MailTo>]
|
23
|
+
attr_reader :ruf
|
21
24
|
|
25
|
+
# `sp` field.
|
26
|
+
#
|
27
|
+
# @return [:none, :quarantine, :reject]
|
28
|
+
attr_reader :sp
|
29
|
+
|
30
|
+
# `v` field.
|
31
|
+
#
|
32
|
+
# @return [:DMARC1]
|
33
|
+
attr_reader :v
|
34
|
+
|
35
|
+
#
|
36
|
+
# Initializes the record.
|
37
|
+
#
|
38
|
+
# @param [Hash{Symbol => Object}] attributes
|
39
|
+
# Attributes for the record.
|
40
|
+
#
|
41
|
+
# @option attributes [:r, :s] :adkim (:r)
|
42
|
+
#
|
43
|
+
# @option attributes [:r, :s] :aspf (:r)
|
44
|
+
#
|
45
|
+
# @option attributes [Array<'0', '1', 'd', 's'>] :fo ('0')
|
46
|
+
#
|
47
|
+
# @option attributes [:none, :quarantine, :reject] :p
|
48
|
+
#
|
49
|
+
# @option attributes [Integer] :pct (100)
|
50
|
+
#
|
51
|
+
# @option attributes [:afrf, :iodef] :rf (:afrf)
|
52
|
+
#
|
53
|
+
# @option attributes [Integer] :ri (86400)
|
54
|
+
#
|
55
|
+
# @option attributes [Array<URI::MailTo>] :rua
|
56
|
+
#
|
57
|
+
# @option attributes [Array<URI::MailTo>] :ruf
|
58
|
+
#
|
59
|
+
# @option attributes [:none, :quarantine, :reject] :sp
|
60
|
+
#
|
61
|
+
# @option attributes [:DMARC1] :v
|
62
|
+
#
|
22
63
|
def initialize(attributes={})
|
23
|
-
attributes.
|
24
|
-
|
64
|
+
@adkim, @aspf, @fo, @p, @pct, @rf, @ri, @rua, @ruf, @sp, @v = attributes.values_at(:adkim, :aspf, :fo, :p, :pct, :rf, :ri, :rua, :ruf, :sp, :v)
|
65
|
+
end
|
66
|
+
|
67
|
+
def sp
|
68
|
+
@sp || @p
|
69
|
+
end
|
70
|
+
|
71
|
+
#
|
72
|
+
# `adkim=` field.
|
73
|
+
#
|
74
|
+
# @return [:r, :s]
|
75
|
+
#
|
76
|
+
def adkim
|
77
|
+
@adkim || :r
|
78
|
+
end
|
79
|
+
|
80
|
+
#
|
81
|
+
# `aspf` field.
|
82
|
+
#
|
83
|
+
# @return [:r, :s]
|
84
|
+
#
|
85
|
+
def aspf
|
86
|
+
@aspf || :r
|
87
|
+
end
|
88
|
+
|
89
|
+
#
|
90
|
+
# `fo` field.
|
91
|
+
#
|
92
|
+
# @return [Array<'0', '1', 'd', 's'>]
|
93
|
+
#
|
94
|
+
def fo
|
95
|
+
@fo || %w[0]
|
96
|
+
end
|
97
|
+
|
98
|
+
#
|
99
|
+
# `pct` field.
|
100
|
+
#
|
101
|
+
# @return [Integer]
|
102
|
+
#
|
103
|
+
def pct
|
104
|
+
@pct || 100
|
105
|
+
end
|
106
|
+
|
107
|
+
#
|
108
|
+
# `rf` field.
|
109
|
+
#
|
110
|
+
# @return [:afrf, :iodef]
|
111
|
+
#
|
112
|
+
def rf
|
113
|
+
@rf || :afrf
|
114
|
+
end
|
115
|
+
|
116
|
+
#
|
117
|
+
# `ri` field.
|
118
|
+
#
|
119
|
+
# @return [Integer]
|
120
|
+
#
|
121
|
+
def ri
|
122
|
+
@ri || 86400
|
123
|
+
end
|
124
|
+
|
125
|
+
#
|
126
|
+
# Parses a DMARC record.
|
127
|
+
#
|
128
|
+
# @param [String] record
|
129
|
+
# The raw DMARC record.
|
130
|
+
#
|
131
|
+
# @return [Record]
|
132
|
+
# The parsed DMARC record.
|
133
|
+
#
|
134
|
+
# @raise [InvalidRecord]
|
135
|
+
# The DMARC record could not be parsed.
|
136
|
+
#
|
137
|
+
# @since 0.3.0
|
138
|
+
#
|
139
|
+
# @api public
|
140
|
+
#
|
141
|
+
def self.parse(record)
|
142
|
+
new(Parser.parse(record))
|
143
|
+
rescue Parslet::ParseFailed => error
|
144
|
+
raise(InvalidRecord.new(error.message,error.cause))
|
145
|
+
end
|
146
|
+
|
147
|
+
#
|
148
|
+
# @deprecated use {parse} instead.
|
149
|
+
#
|
150
|
+
def self.from_txt(rec)
|
151
|
+
parse(rec)
|
152
|
+
end
|
153
|
+
|
154
|
+
#
|
155
|
+
# Queries and parses the DMARC record for a domain.
|
156
|
+
#
|
157
|
+
# @param [String] domain
|
158
|
+
# The domain to query DMARC for.
|
159
|
+
#
|
160
|
+
# @param [Resolv::DNS] resolver
|
161
|
+
# The resolver to use.
|
162
|
+
#
|
163
|
+
# @return [Record, nil]
|
164
|
+
# The parsed DMARC record. If no DMARC record was found, `nil` will be
|
165
|
+
# returned.
|
166
|
+
#
|
167
|
+
# @raise [InvalidRecord]
|
168
|
+
# The DMARC record could not be parsed.
|
169
|
+
#
|
170
|
+
# @since 0.3.0
|
171
|
+
#
|
172
|
+
# @api public
|
173
|
+
#
|
174
|
+
def self.query(domain,resolver=Resolv::DNS.new)
|
175
|
+
if (dmarc = DMARC.query(domain,resolver))
|
176
|
+
parse(dmarc)
|
25
177
|
end
|
178
|
+
end
|
179
|
+
|
180
|
+
#
|
181
|
+
# Converts the record back to a DMARC String.
|
182
|
+
#
|
183
|
+
# @return [String]
|
184
|
+
#
|
185
|
+
def to_s
|
186
|
+
tags = []
|
187
|
+
|
188
|
+
tags << "v=#{@v}" if @v
|
189
|
+
tags << "p=#{@p}" if @p
|
190
|
+
tags << "sp=#{@sp}" if @sp
|
191
|
+
tags << "rua=#{@rua.join(',')}" if @rua
|
192
|
+
tags << "ruf=#{@ruf.join(',')}" if @ruf
|
193
|
+
tags << "adkim=#{@adkim}" if @adkim
|
194
|
+
tags << "aspf=#{@aspf}" if @aspf
|
195
|
+
tags << "ri=#{@ri}" if @ri
|
196
|
+
tags << "fo=#{@fo.join(':')}" if @fo
|
197
|
+
tags << "rf=#{@rf}" if @rf
|
198
|
+
tags << "pct=#{@pct}" if @pct
|
26
199
|
|
27
|
-
|
200
|
+
return tags.join('; ')
|
28
201
|
end
|
29
202
|
|
30
203
|
end
|