coppertone 0.0.7 → 0.0.12

Sign up to get free protection for your applications and to get access to all the features.
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
data/Rakefile CHANGED
@@ -23,15 +23,16 @@ desc 'Parse OpenSPF tests'
23
23
 
24
24
  def spec_file_name(doc, output_path)
25
25
  description = doc['description']
26
- file_name = description.gsub(/[^\w\s_-]+/, '')
27
- .gsub(/(^|\b\s)\s+($|\s?\b)/, '\\1\\2')
28
- .gsub(/\s+/, '_') + '_spec.rb'
26
+ file_prefix = description.gsub(/[^\w\s_-]+/, '')
27
+ .gsub(/(^|\b\s)\s+($|\s?\b)/, '\\1\\2')
28
+ .gsub(/\s+/, '_')
29
+ file_name = "#{file_prefix}_spec.rb"
29
30
  File.join(output_path, file_name)
30
31
  end
31
32
 
32
33
  INDENT_STRING = ' '.freeze
33
34
  def indented_string(num_indents = 1)
34
- Array.new(num_indents) { INDENT_STRING }.join('')
35
+ Array.new(num_indents) { INDENT_STRING }.join
35
36
  end
36
37
 
37
38
  def puts_prefixed_string(f, s, indent = 0)
@@ -57,21 +58,25 @@ end
57
58
 
58
59
  def escape_quote(val)
59
60
  return unless val
61
+
60
62
  val.gsub(/'/) { |_s| "\\'" }
61
63
  end
62
64
 
63
65
  def clean_description(description)
64
66
  return unless description
67
+
65
68
  description.tr("\n", ' ').delete("'")
66
69
  end
67
70
 
68
71
  def as_array(val)
69
72
  return [] unless val
73
+
70
74
  val.is_a?(Array) ? val : [val]
71
75
  end
72
76
 
73
77
  def write_comment(f, comment, indent)
74
78
  return unless comment
79
+
75
80
  comment.lines do |comment_line|
76
81
  puts_prefixed_string(f, "# #{comment_line}", indent)
77
82
  end
@@ -85,10 +90,11 @@ def write_result(f, host, mailfrom, helo, indent)
85
90
  end
86
91
 
87
92
  def write_expects(f, results, explanation, indent)
88
- results_array = "[#{results.map { |r| ':' + r } .join(',')}]"
93
+ results_array = "[#{results.map { |r| ":#{r}" }.join(',')}]"
89
94
  code_expect = "expect(#{results_array}).to include(result.code)"
90
95
  puts_prefixed_string(f, code_expect, indent)
91
96
  return unless explanation
97
+
92
98
  exp_expect = "expect(result.explanation).to eq('#{explanation}')"
93
99
  puts_prefixed_string(f, exp_expect, indent)
94
100
  end
@@ -116,7 +122,7 @@ def write_spec(f, spec, indent = 1)
116
122
  end
117
123
 
118
124
  def write_doc(doc, output_path)
119
- open(spec_file_name(doc, output_path), 'w') do |f|
125
+ File.open(spec_file_name(doc, output_path), 'w') do |f|
120
126
  puts_prefixed_string(f, "require 'spec_helper'")
121
127
  empty_line(f)
122
128
  description = doc['description']
data/coppertone.gemspec CHANGED
@@ -1,5 +1,4 @@
1
- # coding: utf-8
2
- lib = File.expand_path('../lib', __FILE__)
1
+ lib = File.expand_path('lib', __dir__)
3
2
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
3
  require 'coppertone/version'
5
4
 
@@ -7,22 +6,23 @@ Gem::Specification.new do |spec|
7
6
  spec.name = 'coppertone'
8
7
  spec.version = Coppertone::VERSION
9
8
  spec.authors = ['Peter M. Goldstein']
10
- spec.email = ['peter.m.goldstein@gmail.com']
9
+ spec.email = ['peter@valimail.com']
11
10
  spec.summary = 'A Sender Policy Framework (SPF) toolkit'
12
11
  spec.description = 'Coppertone includes tools for parsing SPF DNS records, evaluating the result of SPF checks for received emails, and creating appropriate email headers from SPF results.'
13
- spec.homepage = 'https://github.com/petergoldstein/coppertone'
14
- spec.license = 'Apache'
12
+ spec.homepage = 'https://github.com/ValiMail/coppertone'
13
+ spec.license = 'MIT'
15
14
  spec.files = `git ls-files -z`.split("\x0")
16
15
  spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
17
16
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
18
17
  spec.require_paths = ['lib']
18
+ spec.required_ruby_version = '>= 2.7'
19
19
 
20
+ spec.add_runtime_dependency 'activesupport', '>= 3.0'
21
+ spec.add_runtime_dependency 'addressable'
20
22
  spec.add_runtime_dependency 'dns_adapter'
21
23
  spec.add_runtime_dependency 'i18n'
22
- spec.add_runtime_dependency 'addressable'
23
- spec.add_runtime_dependency 'activesupport', '>= 3.0'
24
24
  spec.add_development_dependency 'bundler'
25
- spec.add_development_dependency 'rake', '~> 10.0'
25
+ spec.add_development_dependency 'flay'
26
+ spec.add_development_dependency 'rake', '~> 13.0'
26
27
  spec.add_development_dependency 'rspec', '>= 3.0'
27
- spec.add_development_dependency 'rubocop'
28
28
  end
@@ -12,8 +12,10 @@ module Coppertone
12
12
 
13
13
  def build(type, attributes)
14
14
  return nil unless type
15
+
15
16
  klass = map[type]
16
17
  return nil unless klass
18
+
17
19
  klass.create(attributes)
18
20
  end
19
21
  end
@@ -7,6 +7,7 @@ module Coppertone
7
7
  # SPF specification (see section 4.6.1).
8
8
  class Directive
9
9
  attr_reader :qualifier, :mechanism
10
+
10
11
  delegate :context_dependent?, :dns_lookup_term?,
11
12
  :includes_ptr?, to: :mechanism
12
13
 
@@ -25,6 +26,7 @@ module Coppertone
25
26
 
26
27
  def target_domain
27
28
  raise NeedsContextError unless dns_lookup_term?
29
+
28
30
  mechanism.target_domain
29
31
  end
30
32
 
@@ -37,17 +39,20 @@ module Coppertone
37
39
  qualifier.default? ? mechanism_s : "#{qualifier}#{mechanism_s}"
38
40
  end
39
41
 
40
- DIRECTIVE_REGEXP = /\A([\+\-\~\?]?)([a-zA-Z0-9]*)((:?)\S*)\z/
42
+ DIRECTIVE_REGEXP = /\A([+\-~?]?)([a-zA-Z0-9]*)((:?)\S*)\z/.freeze
41
43
  def self.matching_term(text)
42
44
  return nil if text.include?('=')
45
+
43
46
  matches = DIRECTIVE_REGEXP.match(text)
44
47
  return nil unless matches
48
+
45
49
  qualifier_txt = matches[1]
46
50
  mechanism_type = matches[2].downcase
47
51
  attributes = matches[3]
48
52
  qualifier = Qualifier.find_by_text(qualifier_txt)
49
53
  mechanism = Mechanism.build(mechanism_type, attributes)
50
54
  return nil unless qualifier && mechanism
55
+
51
56
  new(qualifier, mechanism)
52
57
  end
53
58
  end
@@ -15,10 +15,11 @@ module Coppertone
15
15
 
16
16
  def validate_domain_spec_restrictions
17
17
  return if only_allowed_macros? && ends_in_allowed_term?
18
+
18
19
  raise Coppertone::DomainSpecParsingError
19
20
  end
20
21
 
21
- EXP_ONLY_MACRO_LETTERS = %w(c r t).freeze
22
+ EXP_ONLY_MACRO_LETTERS = %w[c r t].freeze
22
23
  def only_allowed_macros?
23
24
  @macros.select { |m| m.is_a?(Coppertone::MacroString::MacroExpand) }
24
25
  .none? { |m| EXP_ONLY_MACRO_LETTERS.include?(m.macro_letter) }
@@ -29,6 +30,7 @@ module Coppertone
29
30
  return true unless lm
30
31
  return false if lm.is_a?(Coppertone::MacroString::MacroStaticExpand)
31
32
  return true if lm.is_a?(Coppertone::MacroString::MacroExpand)
33
+
32
34
  ends_with_top_label?
33
35
  end
34
36
 
@@ -37,8 +39,10 @@ module Coppertone
37
39
  ends_with = ends_with[0..-2] if ends_with[-1] == '.'
38
40
  _, match, tail = ends_with.rpartition('.')
39
41
  return false if match.blank?
42
+
40
43
  hostname = Coppertone::Utils::DomainUtils.valid_tld?(tail)
41
44
  return false unless hostname
45
+
42
46
  true
43
47
  end
44
48
  end
@@ -4,11 +4,13 @@ module Coppertone
4
4
 
5
5
  # Error classes mapping to the SPF result codes
6
6
  class TemperrorError < Coppertone::Error; end
7
+
7
8
  class PermerrorError < Coppertone::Error; end
8
9
 
9
10
  # Errors occurring when the string representation of a MacroString
10
11
  # or DomainSpec does not obey the syntax requirements.
11
12
  class MacroStringParsingError < Coppertone::PermerrorError; end
13
+
12
14
  class DomainSpecParsingError < MacroStringParsingError; end
13
15
 
14
16
  # Occurs when an SPF record cannot be parsed.
@@ -42,8 +44,11 @@ module Coppertone
42
44
 
43
45
  # Errors generated when certain spec-defined limits are exceeded.
44
46
  class LimitExceededError < Coppertone::PermerrorError; end
47
+
45
48
  class TermLimitExceededError < PermerrorError; end
49
+
46
50
  class VoidLimitExceededError < PermerrorError; end
51
+
47
52
  class MXLimitExceededError < PermerrorError; end
48
53
 
49
54
  # Raised when context is required to evaluate a value, but
@@ -10,9 +10,11 @@ module Coppertone
10
10
  # will fail if passed an address with a non-trivial network prefix
11
11
  class IPAddressWrapper
12
12
  attr_reader :string_representation, :ip
13
+
13
14
  def initialize(s)
14
15
  @ip = self.class.parse(s)
15
16
  raise ArgumentError unless @ip
17
+
16
18
  @string_representation = s
17
19
  end
18
20
 
@@ -26,6 +28,7 @@ module Coppertone
26
28
  def self.parse(s)
27
29
  return nil unless s
28
30
  return nil if s.index('/')
31
+
29
32
  ip_addr = IPAddr.new(s)
30
33
  normalize_ip(ip_addr)
31
34
  rescue IP_PARSE_ERROR
@@ -33,7 +36,8 @@ module Coppertone
33
36
  end
34
37
 
35
38
  def self.normalize_ip(parsed_ip)
36
- return parsed_ip unless parsed_ip && parsed_ip.ipv6?
39
+ return parsed_ip unless parsed_ip&.ipv6?
40
+
37
41
  parsed_ip.ipv4_mapped? ? parsed_ip.native : parsed_ip
38
42
  end
39
43
 
@@ -7,7 +7,7 @@ module Coppertone
7
7
  # A context used to evaluate MacroStrings. Responds to all of the
8
8
  # macro letter directives except 'p'.
9
9
  class MacroContext
10
- attr_reader :domain, :ip_address_wrapper, :sender_identity, :helo_domain
10
+ attr_reader :domain, :ip_address_wrapper, :sender_identity, :helo_domain, :hostname
11
11
 
12
12
  delegate :s, :l, :o, to: :sender_identity
13
13
  alias d domain
@@ -16,7 +16,6 @@ module Coppertone
16
16
  to: :ip_address_wrapper
17
17
  alias h helo_domain
18
18
 
19
- attr_reader :hostname
20
19
  def initialize(domain, ip_as_s, sender, helo_domain = 'unknown',
21
20
  options = {})
22
21
  @ip_address_wrapper = IPAddressWrapper.new(ip_as_s)
@@ -37,10 +36,18 @@ module Coppertone
37
36
  end
38
37
 
39
38
  RESERVED_REGEXP = Regexp.new("[^#{URI::PATTERN::UNRESERVED}]")
40
- %w(s l o d i v h c r t).each do |m|
39
+
40
+ def escape(string)
41
+ encoding = string.encoding
42
+ string.b.gsub(RESERVED_REGEXP) do |m|
43
+ "%#{m.unpack('H2' * m.bytesize).join('%').upcase}"
44
+ end.force_encoding(encoding)
45
+ end
46
+
47
+ %w[s l o d i v h c r t].each do |m|
41
48
  define_method(m.upcase) do
42
49
  unencoded = send(m)
43
- unencoded ? ::URI.escape(unencoded, RESERVED_REGEXP) : nil
50
+ unencoded ? escape(unencoded) : nil
44
51
  end
45
52
  end
46
53
 
@@ -9,6 +9,7 @@ module Coppertone
9
9
  # context for interpolation.
10
10
  class MacroString
11
11
  attr_reader :macro_text
12
+
12
13
  def initialize(macro_text)
13
14
  @macro_text = macro_text
14
15
  macros
@@ -19,11 +20,11 @@ module Coppertone
19
20
  end
20
21
 
21
22
  def expand(context, request = nil)
22
- macros.map { |m| m.expand(context, request) }.join('')
23
+ macros.map { |m| m.expand(context, request) }.join
23
24
  end
24
25
 
25
26
  def to_s
26
- macros.map(&:to_s).join('')
27
+ macros.map(&:to_s).join
27
28
  end
28
29
 
29
30
  def context_dependent?
@@ -39,6 +40,7 @@ module Coppertone
39
40
 
40
41
  def ==(other)
41
42
  return false unless other.instance_of? self.class
43
+
42
44
  macro_text == other.macro_text
43
45
  end
44
46
  alias eql? ==
@@ -9,10 +9,10 @@ module Coppertone
9
9
  # grammer defined in section 7.1.
10
10
  class MacroExpand
11
11
  MACRO_LETTER_CHAR_SET = '[slodiphcrtvSLODIPHCRTV]'.freeze
12
- PTR_MACRO_CHAR_SET = %w(p P).freeze
12
+ PTR_MACRO_CHAR_SET = %w[p P].freeze
13
13
  DELIMITER_CHAR_SET = '[\.\-\+\,\/\_\=]'.freeze
14
14
  VALID_BODY_REGEXP =
15
- /\A(#{MACRO_LETTER_CHAR_SET})(\d*)(r?)(#{DELIMITER_CHAR_SET}*)\z/
15
+ /\A(#{MACRO_LETTER_CHAR_SET})(\d*)(r?)(#{DELIMITER_CHAR_SET}*)\z/.freeze
16
16
 
17
17
  attr_reader :macro_letter, :digit_transformers, :reverse,
18
18
  :delimiter_regexp
@@ -20,6 +20,7 @@ module Coppertone
20
20
  def initialize(s)
21
21
  matches = VALID_BODY_REGEXP.match(s)
22
22
  raise Coppertone::MacroStringParsingError if matches.nil?
23
+
23
24
  @macro_letter = matches[1]
24
25
  initialize_digit_transformers(matches[2])
25
26
  @reverse = (matches[3] == 'r')
@@ -29,6 +30,7 @@ module Coppertone
29
30
 
30
31
  def initialize_digit_transformers(raw_value)
31
32
  return unless raw_value
33
+
32
34
  @digit_transformers = raw_value.to_i unless raw_value.empty?
33
35
  return unless @digit_transformers
34
36
  raise Coppertone::MacroStringParsingError if @digit_transformers.zero?
@@ -44,6 +46,7 @@ module Coppertone
44
46
  Coppertone::Utils::ValidatedDomainFinder
45
47
  .new(context, request, false).find(context.d)
46
48
  return 'unknown' unless ptr
49
+
47
50
  @macro_letter == 'P' ? ::Addressable::URI.encode_component(ptr) : ptr
48
51
  end
49
52
 
@@ -64,6 +67,7 @@ module Coppertone
64
67
 
65
68
  def ==(other)
66
69
  return false unless other.instance_of? self.class
70
+
67
71
  to_s == other.to_s
68
72
  end
69
73
 
@@ -17,6 +17,7 @@ module Coppertone
17
17
 
18
18
  def ==(other)
19
19
  return false unless other.instance_of? self.class
20
+
20
21
  to_s == other.to_s
21
22
  end
22
23
  end
@@ -9,6 +9,7 @@ module Coppertone
9
9
  # in the context of a particular SPF check.
10
10
  class MacroParser
11
11
  attr_reader :macros
12
+
12
13
  def initialize(s)
13
14
  @s = s.dup
14
15
  @macros = []
@@ -31,20 +32,23 @@ module Coppertone
31
32
 
32
33
  def parse_contextual_interpolated_macro
33
34
  raise MacroStringParsingError unless @s[1] == '{'
35
+
34
36
  closing_index = @s.index('}')
35
37
  raise MacroStringParsingError unless closing_index
38
+
36
39
  interpolated_body = @s[2, closing_index - 2]
37
40
  @macros << MacroExpand.new(interpolated_body)
38
- @s = @s[(closing_index + 1)..-1]
41
+ @s = @s[(closing_index + 1)..]
39
42
  end
40
43
 
41
- SIMPLE_MACRO_LETTERS = %w(% _ -).freeze
44
+ SIMPLE_MACRO_LETTERS = %w[% _ -].freeze
42
45
  def parse_interpolated_macro
43
46
  raise MacroStringParsingError if @s.length == 1
47
+
44
48
  macro_code = @s[0, 2]
45
49
  if MacroStaticExpand.exists_for?(macro_code)
46
50
  @macros << MacroStaticExpand.macro_for(macro_code)
47
- @s = @s[2..-1]
51
+ @s = @s[2..]
48
52
  else
49
53
  parse_contextual_interpolated_macro
50
54
  end
@@ -54,7 +58,7 @@ module Coppertone
54
58
  new_idx = @s.index('%')
55
59
  new_idx ||= @s.length
56
60
  @macros << MacroLiteral.new(@s[0, new_idx])
57
- @s = @s[new_idx..-1]
61
+ @s = @s[new_idx..]
58
62
  new_idx
59
63
  end
60
64
  end
@@ -28,14 +28,16 @@ module Coppertone
28
28
  # Replaces '%-' in a macro string
29
29
  URL_ENCODED_SPACE_MACRO = new('%-'.freeze, '%20'.freeze)
30
30
 
31
- SIMPLE_INTERPOLATED_MACRO_LETTERS = %w(% _ -).freeze
31
+ SIMPLE_INTERPOLATED_MACRO_LETTERS = %w[% _ -].freeze
32
32
  def self.exists_for?(x)
33
33
  return false unless x && (x.length == 2) && (x[0] == '%')
34
+
34
35
  SIMPLE_INTERPOLATED_MACRO_LETTERS.include?(x[1])
35
36
  end
36
37
 
37
38
  def self.macro_for(x)
38
39
  raise Coppertone::MacroStringParsingError unless exists_for?(x)
40
+
39
41
  case x[1]
40
42
  when '%'
41
43
  PERCENT_MACRO
@@ -18,6 +18,7 @@ module Coppertone
18
18
 
19
19
  def self.register(klass)
20
20
  raise ArgumentError unless klass < self
21
+
21
22
  class_builder.register(klass.label, klass)
22
23
  end
23
24
 
@@ -26,6 +27,7 @@ module Coppertone
26
27
  end
27
28
 
28
29
  attr_reader :arguments
30
+
29
31
  def initialize(arguments)
30
32
  @arguments = arguments
31
33
  end
@@ -5,6 +5,7 @@ module Coppertone
5
5
  class All < Mechanism
6
6
  def self.create(attributes)
7
7
  raise InvalidMechanismError unless attributes.blank?
8
+
8
9
  SINGLETON
9
10
  end
10
11