coppertone 0.0.7 → 0.0.12

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.
Files changed (66) hide show
  1. checksums.yaml +5 -5
  2. data/.circleci/config.yml +79 -0
  3. data/.codeclimate.yml +28 -0
  4. data/.github/dependabot.yml +7 -0
  5. data/.rubocop.yml +47 -6
  6. data/.travis.yml +2 -6
  7. data/Gemfile +12 -2
  8. data/LICENSE +21 -201
  9. data/Rakefile +12 -6
  10. data/coppertone.gemspec +9 -9
  11. data/lib/coppertone/class_builder.rb +2 -0
  12. data/lib/coppertone/directive.rb +6 -1
  13. data/lib/coppertone/domain_spec.rb +5 -1
  14. data/lib/coppertone/error.rb +5 -0
  15. data/lib/coppertone/ip_address_wrapper.rb +5 -1
  16. data/lib/coppertone/macro_context.rb +11 -4
  17. data/lib/coppertone/macro_string.rb +4 -2
  18. data/lib/coppertone/macro_string/macro_expand.rb +6 -2
  19. data/lib/coppertone/macro_string/macro_literal.rb +1 -0
  20. data/lib/coppertone/macro_string/macro_parser.rb +8 -4
  21. data/lib/coppertone/macro_string/macro_static_expand.rb +3 -1
  22. data/lib/coppertone/mechanism.rb +2 -0
  23. data/lib/coppertone/mechanism/all.rb +1 -0
  24. data/lib/coppertone/mechanism/cidr_parser.rb +3 -3
  25. data/lib/coppertone/mechanism/domain_spec_mechanism.rb +10 -3
  26. data/lib/coppertone/mechanism/domain_spec_optional.rb +1 -0
  27. data/lib/coppertone/mechanism/domain_spec_required.rb +1 -0
  28. data/lib/coppertone/mechanism/domain_spec_with_dual_cidr.rb +8 -1
  29. data/lib/coppertone/mechanism/include_matcher.rb +3 -0
  30. data/lib/coppertone/mechanism/ip_mechanism.rb +20 -10
  31. data/lib/coppertone/mechanism/mx.rb +1 -0
  32. data/lib/coppertone/modifier.rb +3 -1
  33. data/lib/coppertone/modifier/base.rb +7 -2
  34. data/lib/coppertone/modifier/exp.rb +6 -2
  35. data/lib/coppertone/modifier/redirect.rb +1 -0
  36. data/lib/coppertone/modifier/unknown.rb +1 -0
  37. data/lib/coppertone/null_macro_context.rb +1 -1
  38. data/lib/coppertone/qualifier.rb +1 -0
  39. data/lib/coppertone/record.rb +6 -2
  40. data/lib/coppertone/record_evaluator.rb +5 -0
  41. data/lib/coppertone/record_finder.rb +2 -2
  42. data/lib/coppertone/record_term_parser.rb +9 -5
  43. data/lib/coppertone/redirect_record_finder.rb +3 -1
  44. data/lib/coppertone/request.rb +26 -23
  45. data/lib/coppertone/request_context.rb +1 -0
  46. data/lib/coppertone/request_count_limiter.rb +2 -0
  47. data/lib/coppertone/result.rb +2 -1
  48. data/lib/coppertone/sender_identity.rb +2 -1
  49. data/lib/coppertone/term.rb +1 -0
  50. data/lib/coppertone/terms_parser.rb +3 -1
  51. data/lib/coppertone/utils/domain_utils.rb +21 -8
  52. data/lib/coppertone/utils/ip_in_domain_checker.rb +1 -1
  53. data/lib/coppertone/utils/validated_domain_finder.rb +2 -1
  54. data/lib/coppertone/version.rb +1 -1
  55. data/spec/domain_spec_spec.rb +1 -1
  56. data/spec/macro_string/macro_static_expand_spec.rb +2 -2
  57. data/spec/mechanism/ip4_spec.rb +3 -0
  58. data/spec/mechanism/ip6_spec.rb +3 -0
  59. data/spec/null_macro_context_spec.rb +1 -1
  60. data/spec/open_spf/Initial_processing_spec.rb +0 -2
  61. data/spec/open_spf/Semantics_of_exp_and_other_modifiers_spec.rb +0 -2
  62. data/spec/record_spec.rb +1 -1
  63. data/spec/spec_helper.rb +14 -3
  64. data/spec/term_spec.rb +1 -1
  65. data/spec/utils/domain_utils_spec.rb +10 -2
  66. metadata +34 -32
@@ -2,6 +2,7 @@ module Coppertone
2
2
  # A helper class for finding SPF records for a redirect modifier.
3
3
  class RedirectRecordFinder
4
4
  attr_reader :redirect, :macro_context, :request_context
5
+
5
6
  def initialize(redirect, macro_context, request_context)
6
7
  @redirect = redirect
7
8
  @macro_context = macro_context
@@ -9,11 +10,12 @@ module Coppertone
9
10
  end
10
11
 
11
12
  def target
12
- @redirect_target ||= redirect.evaluate(macro_context, request_context)
13
+ @target ||= redirect.evaluate(macro_context, request_context)
13
14
  end
14
15
 
15
16
  def record
16
17
  return unless target
18
+
17
19
  @record ||= RecordFinder.new(request_context.dns_client, target).record
18
20
  end
19
21
  end
@@ -1,8 +1,8 @@
1
1
  module Coppertone
2
2
  # Represents an SPF request.
3
3
  class Request
4
- attr_reader :ip_as_s, :sender, :helo_domain, :options, :result
5
- attr_reader :helo_result, :mailfrom_result
4
+ attr_reader :ip_as_s, :sender, :helo_domain, :options, :result, :helo_result, :mailfrom_result
5
+
6
6
  def initialize(ip_as_s, sender, helo_domain, options = {})
7
7
  @ip_as_s = ip_as_s
8
8
  @sender = sender
@@ -16,13 +16,15 @@ module Coppertone
16
16
 
17
17
  def process_helo
18
18
  check_spf_for_helo
19
- return nil if helo_result && helo_result.none?
19
+ return nil if helo_result&.none?
20
+
20
21
  helo_result
21
22
  end
22
23
 
23
24
  def process_mailfrom
24
25
  check_spf_for_mailfrom
25
- return nil if mailfrom_result && mailfrom_result.none?
26
+ return nil if mailfrom_result&.none?
27
+
26
28
  mailfrom_result
27
29
  end
28
30
 
@@ -34,25 +36,6 @@ module Coppertone
34
36
  helo_result.nil? && mailfrom_result.nil?
35
37
  end
36
38
 
37
- def check_spf_for_helo
38
- @helo_result ||= check_spf_for_context(helo_context, 'helo')
39
- end
40
-
41
- def check_spf_for_mailfrom
42
- @mailfrom_result ||= check_spf_for_context(mailfrom_context, 'mailfrom')
43
- end
44
-
45
- def check_spf_for_context(macro_context, identity)
46
- record = spf_record(macro_context)
47
- @result = spf_request(macro_context, record, identity) if record
48
- rescue DNSAdapter::Error => e
49
- Result.temperror(e.message)
50
- rescue Coppertone::TemperrorError => e
51
- Result.temperror(e.message)
52
- rescue Coppertone::PermerrorError => e
53
- Result.permerror(e.message)
54
- end
55
-
56
39
  def request_context
57
40
  @request_context ||= RequestContext.new(options)
58
41
  end
@@ -72,9 +55,29 @@ module Coppertone
72
55
 
73
56
  def spf_request(macro_context, record, identity)
74
57
  return Result.new(:none) if record.nil?
58
+
75
59
  r = RecordEvaluator.new(record).evaluate(macro_context, request_context)
76
60
  r.identity = identity
77
61
  r
78
62
  end
63
+
64
+ private
65
+
66
+ def check_spf_for_helo
67
+ @helo_result = check_spf_for_context(helo_context, 'helo')
68
+ end
69
+
70
+ def check_spf_for_mailfrom
71
+ @mailfrom_result = check_spf_for_context(mailfrom_context, 'mailfrom')
72
+ end
73
+
74
+ def check_spf_for_context(macro_context, identity)
75
+ record = spf_record(macro_context)
76
+ @result = spf_request(macro_context, record, identity) if record
77
+ rescue DNSAdapter::Error, Coppertone::TemperrorError => e
78
+ Result.temperror(e.message)
79
+ rescue Coppertone::PermerrorError => e
80
+ Result.permerror(e.message)
81
+ end
79
82
  end
80
83
  end
@@ -60,6 +60,7 @@ module Coppertone
60
60
 
61
61
  def config_value(k)
62
62
  return @options[k] if @options.key?(k)
63
+
63
64
  Coppertone.config.send(k)
64
65
  end
65
66
  end
@@ -3,6 +3,7 @@ module Coppertone
3
3
  # used to track and limit the number of DNS queries of various types.
4
4
  class RequestCountLimiter
5
5
  attr_accessor :count, :limit, :counter_description
6
+
6
7
  def initialize(limit = nil, counter_description = nil)
7
8
  self.limit = limit
8
9
  self.counter_description = counter_description
@@ -26,6 +27,7 @@ module Coppertone
26
27
 
27
28
  def exceeded?
28
29
  return false unless limited?
30
+
29
31
  count > limit
30
32
  end
31
33
 
@@ -16,6 +16,7 @@ module Coppertone
16
16
 
17
17
  attr_reader :code, :mechanism
18
18
  attr_accessor :explanation, :problem, :identity
19
+
19
20
  def initialize(code, mechanism = nil)
20
21
  @code = code
21
22
  @mechanism = mechanism
@@ -45,7 +46,7 @@ module Coppertone
45
46
  Result.new(:neutral)
46
47
  end
47
48
 
48
- %w(none pass fail softfail neutral temperror permerror).each do |t|
49
+ %w[none pass fail softfail neutral temperror permerror].each do |t|
49
50
  define_method("#{t}?") do
50
51
  self.class.const_get(t.upcase) == send(:code)
51
52
  end
@@ -4,9 +4,10 @@ module Coppertone
4
4
  # for the macro letters.
5
5
  class SenderIdentity
6
6
  DEFAULT_LOCALPART = 'postmaster'.freeze
7
- EMAIL_ADDRESS_SPLIT_REGEXP = /^(.*)@(.*?)$/
7
+ EMAIL_ADDRESS_SPLIT_REGEXP = /^(.*)@(.*?)$/.freeze
8
8
 
9
9
  attr_reader :sender, :localpart, :domain
10
+
10
11
  def initialize(sender)
11
12
  @sender = sender
12
13
  initialize_localpart_and_domain
@@ -7,6 +7,7 @@ module Coppertone
7
7
  class Term
8
8
  def self.build_from_token(token)
9
9
  return nil unless token
10
+
10
11
  Directive.matching_term(token) || Modifier.matching_term(token)
11
12
  end
12
13
  end
@@ -2,6 +2,7 @@ module Coppertone
2
2
  # Parses a un-prefixed string into terms
3
3
  class TermsParser
4
4
  attr_reader :text
5
+
5
6
  def initialize(text)
6
7
  @text = text
7
8
  end
@@ -11,12 +12,13 @@ module Coppertone
11
12
  end
12
13
 
13
14
  def tokens
14
- text.split(/ /).select { |s| !s.blank? }
15
+ text.split(/ /).reject(&:blank?)
15
16
  end
16
17
 
17
18
  def parse_token(token)
18
19
  term = Term.build_from_token(token)
19
20
  raise RecordParsingError unless term
21
+
20
22
  term
21
23
  end
22
24
  end
@@ -7,12 +7,17 @@ module Coppertone
7
7
  class DomainUtils
8
8
  def self.valid?(domain)
9
9
  return false if domain.blank?
10
- labels = to_ascii_labels(domain)
11
- return false if labels.length <= 1
12
10
  return false if domain.length > 253
13
- return false if labels.any? { |l| !valid_label?(l) }
11
+
12
+ labels_valid?(to_ascii_labels(domain))
13
+ end
14
+
15
+ def self.labels_valid?(labels)
16
+ return false if labels.length <= 1
17
+ return false if labels.first == '*'
14
18
  return false unless valid_tld?(labels.last)
15
- true
19
+
20
+ labels.all? { |l| valid_label?(l) }
16
21
  end
17
22
 
18
23
  def self.to_labels(domain)
@@ -22,6 +27,7 @@ module Coppertone
22
27
  def self.parent_domain(domain)
23
28
  labels = to_labels(domain)
24
29
  return '.' if labels.size == 1
30
+
25
31
  labels.shift
26
32
  labels.join('.')
27
33
  end
@@ -34,33 +40,38 @@ module Coppertone
34
40
  to_ascii_labels(domain).join('.')
35
41
  end
36
42
 
37
- NO_DASH_NONNUMERIC_REGEXP = /\A[a-zA-Z0-9]*[a-zA-Z]+[a-zA-Z0-9]*\z/
38
- NO_DASH_REGEXP = /\A[a-zA-Z0-9]+\z/
39
- DASH_REGEXP = /\A[a-zA-Z0-9]+\-[a-zA-Z0-9\-]*[a-zA-Z0-9]+\z/
43
+ NO_DASH_NONNUMERIC_REGEXP = /\A[a-zA-Z0-9]*[a-zA-Z]+[a-zA-Z0-9]*\z/.freeze
44
+ NO_DASH_REGEXP = /\A[a-zA-Z0-9]+\z/.freeze
45
+ DASH_REGEXP = /\A[a-zA-Z0-9]+-[a-zA-Z0-9\-]*[a-zA-Z0-9]+\z/.freeze
40
46
 
41
47
  def self.valid_hostname_label?(l)
42
48
  return false unless valid_label?(l)
49
+
43
50
  NO_DASH_REGEXP.match(l) || DASH_REGEXP.match(l)
44
51
  end
45
52
 
46
53
  def self.valid_tld?(l)
47
54
  return false unless valid_label?(l)
55
+
48
56
  NO_DASH_NONNUMERIC_REGEXP.match(l) || DASH_REGEXP.match(l)
49
57
  end
50
58
 
51
59
  def self.valid_ldh_domain?(domain)
52
60
  return false unless valid?(domain)
61
+
53
62
  labels = to_ascii_labels(domain)
54
63
  return false unless valid_tld?(labels.last)
64
+
55
65
  labels.all? { |l| valid_hostname_label?(l) }
56
66
  end
57
67
 
58
68
  def self.valid_label?(l)
59
- (l.length >= 0) && (l.length <= 63) && !l.match(/\s/)
69
+ !l.empty? && (l.length <= 63) && !l.match(/\s/)
60
70
  end
61
71
 
62
72
  def self.macro_expanded_domain(domain)
63
73
  return nil if domain.blank?
74
+
64
75
  labels = to_ascii_labels(domain)
65
76
  domain = labels.join('.')
66
77
  while domain.length > 253
@@ -75,12 +86,14 @@ module Coppertone
75
86
  domain_labels = to_ascii_labels(domain)
76
87
  num_labels_in_domain = domain_labels.length
77
88
  return false if subdomain_labels.length <= domain_labels.length
89
+
78
90
  subdomain_labels.last(num_labels_in_domain) == domain_labels
79
91
  end
80
92
 
81
93
  def self.subdomain_or_same?(candidate, domain)
82
94
  return false unless valid?(domain) && valid?(candidate)
83
95
  return true if normalized_domain(domain) == normalized_domain(candidate)
96
+
84
97
  subdomain_of?(candidate, domain)
85
98
  end
86
99
  end
@@ -49,7 +49,7 @@ module Coppertone
49
49
  ips = recs.map do |r|
50
50
  IPAddr.new(r[:address]).mask(cidr_length.to_i)
51
51
  end
52
- ips.select { |i| !i.nil? }
52
+ ips.reject(&:nil?)
53
53
  end
54
54
  end
55
55
  end
@@ -4,7 +4,8 @@ module Coppertone
4
4
  # section 5.5 of the RFC.
5
5
  class ValidatedDomainFinder
6
6
  attr_reader :subdomain_only
7
- def initialize(macro_context, request_context, subdomain_only = true)
7
+
8
+ def initialize(macro_context, request_context, subdomain_only = true) # rubocop:disable Style/OptionalBooleanParameter
8
9
  @mc = macro_context
9
10
  @request_context = request_context
10
11
  @subdomain_only = subdomain_only
@@ -1,3 +1,3 @@
1
1
  module Coppertone
2
- VERSION = '0.0.7'.freeze
2
+ VERSION = '0.0.12'.freeze
3
3
  end
@@ -26,7 +26,7 @@ describe Coppertone::DomainSpec do
26
26
  end
27
27
 
28
28
  it 'raises an error when the macro string contains forbidden macros' do
29
- %w(c r t).each do |m|
29
+ %w[c r t].each do |m|
30
30
  expect do
31
31
  Coppertone::DomainSpec.new("%{#{m}}")
32
32
  end.to raise_error(Coppertone::DomainSpecParsingError)
@@ -2,7 +2,7 @@ require 'spec_helper'
2
2
 
3
3
  describe Coppertone::MacroString::MacroStaticExpand do
4
4
  context '#exists_for?' do
5
- %w(%% %_ %-).each do |x|
5
+ ['%%', '%_', '%-'].each do |x|
6
6
  it "should resolve a macro for the key #{x}" do
7
7
  expect(Coppertone::MacroString::MacroStaticExpand.exists_for?(x))
8
8
  .to eq(true)
@@ -16,7 +16,7 @@ describe Coppertone::MacroString::MacroStaticExpand do
16
16
  end
17
17
 
18
18
  context 'macro_for' do
19
- %w(%% %_ %-).each do |x|
19
+ ['%%', '%_', '%-'].each do |x|
20
20
  it "should resolve a macro for the key #{x}" do
21
21
  expect(Coppertone::MacroString::MacroStaticExpand.macro_for(x))
22
22
  .not_to be_nil
@@ -23,6 +23,7 @@ describe Coppertone::Mechanism::IP4 do
23
23
  it 'should not fail if called with an IP v6' do
24
24
  mech = Coppertone::Mechanism::IP4.new(':fe80::202:b3ff:fe1e:8329')
25
25
  expect(mech.netblock).to eq(IPAddr.new('fe80::202:b3ff:fe1e:8329'))
26
+ expect(mech.cidr_length).to eq(128)
26
27
  expect(mech.to_s).to eq('ip4:fe80::202:b3ff:fe1e:8329')
27
28
  expect(mech).not_to be_includes_ptr
28
29
  expect(mech).not_to be_context_dependent
@@ -32,6 +33,7 @@ describe Coppertone::Mechanism::IP4 do
32
33
  it 'should work if called with an IP4' do
33
34
  mech = Coppertone::Mechanism::IP4.new(':1.2.3.4')
34
35
  expect(mech.netblock).to eq(IPAddr.new('1.2.3.4'))
36
+ expect(mech.cidr_length).to eq(32)
35
37
  expect(mech.to_s).to eq('ip4:1.2.3.4')
36
38
  expect(mech).not_to be_includes_ptr
37
39
  expect(mech).not_to be_context_dependent
@@ -41,6 +43,7 @@ describe Coppertone::Mechanism::IP4 do
41
43
  it 'should work if called with an IP4 with a pfxlen' do
42
44
  mech = Coppertone::Mechanism::IP4.new(':1.2.3.4/4')
43
45
  expect(mech.netblock).to eq(IPAddr.new('1.2.3.4/4'))
46
+ expect(mech.cidr_length).to eq(4)
44
47
  expect(mech.to_s).to eq('ip4:1.2.3.4/4')
45
48
  expect(mech).not_to be_includes_ptr
46
49
  expect(mech).not_to be_context_dependent
@@ -23,6 +23,7 @@ describe Coppertone::Mechanism::IP6 do
23
23
  it 'should not fail if called with an IP v4' do
24
24
  mech = Coppertone::Mechanism::IP6.new(':1.2.3.4')
25
25
  expect(mech.netblock).to eq(IPAddr.new('1.2.3.4'))
26
+ expect(mech.cidr_length).to eq(32)
26
27
  expect(mech).not_to be_dns_lookup_term
27
28
  end
28
29
 
@@ -30,6 +31,7 @@ describe Coppertone::Mechanism::IP6 do
30
31
  mech = Coppertone::Mechanism::IP6.new(':fe80::202:b3ff:fe1e:8329')
31
32
  expect(mech.netblock)
32
33
  .to eq(IPAddr.new('fe80::202:b3ff:fe1e:8329'))
34
+ expect(mech.cidr_length).to eq(128)
33
35
  expect(mech).not_to be_dns_lookup_term
34
36
  end
35
37
 
@@ -37,6 +39,7 @@ describe Coppertone::Mechanism::IP6 do
37
39
  mech = Coppertone::Mechanism::IP6.new(':fe80::202:b3ff:fe1e:8329/64')
38
40
  expect(mech.netblock)
39
41
  .to eq(IPAddr.new('fe80::202:b3ff:fe1e:8329/64'))
42
+ expect(mech.cidr_length).to eq(64)
40
43
  expect(mech).not_to be_dns_lookup_term
41
44
  end
42
45
 
@@ -1,7 +1,7 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe Coppertone::NullMacroContext do
4
- %w(s l o d i p v h c r t).each do |i|
4
+ %w[s l o d i p v h c r t].each do |i|
5
5
  it "should raise an error for #{i}" do
6
6
  expect do
7
7
  Coppertone::NullMacroContext::NULL_CONTEXT.send(i)
@@ -1,5 +1,3 @@
1
- # -- encoding : utf-8 --
2
-
3
1
  require 'spec_helper'
4
2
 
5
3
  describe 'Initial processing' do
@@ -1,5 +1,3 @@
1
- # -- encoding : utf-8 --
2
-
3
1
  require 'spec_helper'
4
2
 
5
3
  describe 'Semantics of exp and other modifiers' do
data/spec/record_spec.rb CHANGED
@@ -141,7 +141,7 @@ describe Coppertone::Record do
141
141
  .new('v=spf1 include:_spf.domain1.com ip4:1.2.3.4 include:_spf.domain2.com ~all exp=explain._spf.%{d}')
142
142
  includes = r.includes
143
143
  expect(includes.map(&:mechanism).all? { |i| i.is_a?(Coppertone::Mechanism::Include) }).to be_truthy
144
- expect(includes.map(&:target_domain)).to eq(%w(_spf.domain1.com _spf.domain2.com))
144
+ expect(includes.map(&:target_domain)).to eq(%w[_spf.domain1.com _spf.domain2.com])
145
145
  end
146
146
  end
147
147
 
data/spec/spec_helper.rb CHANGED
@@ -1,8 +1,19 @@
1
1
  require 'simplecov'
2
- require 'codeclimate-test-reporter'
3
- CodeClimate::TestReporter.start || SimpleCov.start
2
+
3
+ if ENV['CI'] == 'true'
4
+ require 'codecov'
5
+ SimpleCov.formatter = SimpleCov::Formatter::Codecov
6
+ end
7
+
8
+ # save to CircleCI's artifacts directory if we're on CircleCI
9
+ if ENV['CIRCLE_ARTIFACTS']
10
+ dir = File.join(ENV['CIRCLE_ARTIFACTS'], 'coverage')
11
+ SimpleCov.coverage_dir(dir)
12
+ end
13
+
14
+ SimpleCov.start
4
15
 
5
16
  require 'coppertone'
6
17
 
7
18
  # Support files
8
- Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }
19
+ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].sort.each { |f| require f }