dmarc 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: da372737bb262f1d63188a9bfac317660bb8cdb4
4
- data.tar.gz: d29823cc84b2e12a8aa6c37d4cfe0c1c0ad95006
3
+ metadata.gz: 9a12630bf22487064d3d35386f55e7af00f6b542
4
+ data.tar.gz: 35f68803f6820445c7e38d9e37078737075807e4
5
5
  SHA512:
6
- metadata.gz: 938905706d15411759c04133451472d0d7d9565982240ed9747e1566b2c450cab830b66ae6edffbfe8e193ea91bd1aae0dab655a4b6c453b181755f549930e21
7
- data.tar.gz: 764c5540136f70d7d7abd04a5b28253473c8288d82f934f1ad2f57f93d1bbc85ad832277d967957b568f8a27615007ce472a1d09a002cc7ab6c6bd2523145c41
6
+ metadata.gz: 3214787e24b80ce2eb162473d43e9e77f152d8e1ff6cfcf1fbbb2fadfe8d565b27c4e0b97c3c17180783e10ff6c62a18714c8be3925a97309a886f79a7d2eee4
7
+ data.tar.gz: f7ed4b603e8541e97c7d29d733f45d5b7f93ee1d737f9cc70916c1459098a570a01e1cdece1b2b182dd683c8b2322d279ed4db2c3fbe5187ec8be9eb2389d7fe
@@ -1,7 +1,17 @@
1
1
  language: ruby
2
2
  rvm:
3
- - 1.9.3
4
- - 2.0.0
5
- - 2.1.0
6
- - jruby-19mode
7
- - rbx-2
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=
@@ -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 {DMARC::Error}.
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
@@ -10,3 +10,8 @@ group :development do
10
10
  gem 'kramdown'
11
11
  gem 'yard', '~> 0.8'
12
12
  end
13
+
14
+ group :test do
15
+ gem 'json'
16
+ gem 'codeclimate-test-reporter', require: nil
17
+ end
data/README.md CHANGED
@@ -3,6 +3,7 @@
3
3
  [![Code Climate](https://codeclimate.com/github/trailofbits/dmarc.png)](https://codeclimate.com/github/trailofbits/dmarc) [![Build Status](https://travis-ci.org/trailofbits/dmarc.svg)](https://travis-ci.org/trailofbits/dmarc)
4
4
  [![Gem Version](https://badge.fury.io/rb/dmarc.svg)](http://badge.fury.io/rb/dmarc)
5
5
  [![YARD Docs](http://img.shields.io/badge/yard-docs-blue.svg)](http://rubydoc.info/gems/dmarc)
6
+ [![Test Coverage](https://codeclimate.com/github/trailofbits/dmarc/badges/coverage.svg)](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.from_txt(txt)
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]: http://tools.ietf.org/html/draft-kucherawy-dmarc-base-02
81
+ [DMARC]: https://tools.ietf.org/html/rfc7489
41
82
  [parslet]: http://kschiess.github.io/parslet/
@@ -16,6 +16,7 @@ Gem::Specification.new do |gem|
16
16
  gem.files = `git ls-files`.split($/)
17
17
  gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
18
  gem.require_paths = ["lib"]
19
+ gem.required_ruby_version = '>= 1.9.1'
19
20
 
20
21
  gem.add_dependency 'parslet', '~> 1.5'
21
22
 
@@ -1,4 +1,5 @@
1
1
  require 'dmarc/exceptions'
2
+ require 'dmarc/dmarc'
2
3
  require 'dmarc/record'
3
4
  require 'dmarc/parser'
4
5
  require 'dmarc/version'
@@ -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
@@ -1,19 +1,6 @@
1
- module DMARC
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
- def ascii_tree
12
- # `cause` is a method defined by parslet on the ParseFailed error
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
@@ -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) >> dmarc_sep >>
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(:fo_opt => simple(:fo_opt)) { fo_opt }
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(:p => simple(:p)) { {p: p.to_sym } }
173
- rule(:sp => simple(:sp)) { {sp: sp.to_sym} }
183
+ rule(pct: simple(:pct)) { {pct: pct.to_i} }
184
+ rule(ri: simple(:ri)) { {ri: ri.to_i} }
174
185
 
175
- rule(:pct => simple(:pct)) { {pct: pct.to_i} }
176
- rule(:ri => simple(:ri)) { {ri: ri.to_i} }
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(super(record))
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) }
@@ -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 < Struct.new(:adkim, :aspf, :fo, :p, :pct, :rf, :ri, :rua, :ruf, :sp, :v)
8
+ class Record
6
9
 
7
- def self.from_txt(rec)
8
- new(Parser.new.parse(rec))
9
- rescue Parslet::ParseFailed
10
- raise InvalidRecord
11
- end
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
- DEFAULTS = {
14
- adkim: 'r',
15
- aspf: 'r',
16
- fo: '0',
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.merge(DEFAULTS).each_pair do |k,v|
24
- self[k] = v
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
- self.sp ||= p
200
+ return tags.join('; ')
28
201
  end
29
202
 
30
203
  end