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