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 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