coppertone 0.0.1

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 (114) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +34 -0
  3. data/.travis.yml +7 -0
  4. data/Gemfile +7 -0
  5. data/LICENSE +201 -0
  6. data/README.md +58 -0
  7. data/Rakefile +140 -0
  8. data/coppertone.gemspec +27 -0
  9. data/lib/coppertone/class_builder.rb +20 -0
  10. data/lib/coppertone/directive.rb +38 -0
  11. data/lib/coppertone/dns/error.rb +9 -0
  12. data/lib/coppertone/dns/mock_client.rb +106 -0
  13. data/lib/coppertone/dns/resolv_client.rb +110 -0
  14. data/lib/coppertone/dns.rb +3 -0
  15. data/lib/coppertone/domain_spec.rb +45 -0
  16. data/lib/coppertone/error.rb +29 -0
  17. data/lib/coppertone/ip_address_wrapper.rb +75 -0
  18. data/lib/coppertone/macro_context.rb +67 -0
  19. data/lib/coppertone/macro_string/macro_expand.rb +84 -0
  20. data/lib/coppertone/macro_string/macro_literal.rb +24 -0
  21. data/lib/coppertone/macro_string/macro_parser.rb +62 -0
  22. data/lib/coppertone/macro_string/macro_static_expand.rb +52 -0
  23. data/lib/coppertone/macro_string.rb +31 -0
  24. data/lib/coppertone/mechanism/a.rb +16 -0
  25. data/lib/coppertone/mechanism/all.rb +24 -0
  26. data/lib/coppertone/mechanism/cidr_parser.rb +14 -0
  27. data/lib/coppertone/mechanism/domain_spec_mechanism.rb +18 -0
  28. data/lib/coppertone/mechanism/domain_spec_optional.rb +46 -0
  29. data/lib/coppertone/mechanism/domain_spec_required.rb +37 -0
  30. data/lib/coppertone/mechanism/domain_spec_with_dual_cidr.rb +114 -0
  31. data/lib/coppertone/mechanism/exists.rb +14 -0
  32. data/lib/coppertone/mechanism/include.rb +18 -0
  33. data/lib/coppertone/mechanism/include_matcher.rb +34 -0
  34. data/lib/coppertone/mechanism/ip4.rb +13 -0
  35. data/lib/coppertone/mechanism/ip6.rb +13 -0
  36. data/lib/coppertone/mechanism/ip_mechanism.rb +48 -0
  37. data/lib/coppertone/mechanism/mx.rb +40 -0
  38. data/lib/coppertone/mechanism/ptr.rb +17 -0
  39. data/lib/coppertone/mechanism.rb +32 -0
  40. data/lib/coppertone/modifier/base.rb +24 -0
  41. data/lib/coppertone/modifier/exp.rb +34 -0
  42. data/lib/coppertone/modifier/redirect.rb +17 -0
  43. data/lib/coppertone/modifier/unknown.rb +16 -0
  44. data/lib/coppertone/modifier.rb +30 -0
  45. data/lib/coppertone/qualifier.rb +45 -0
  46. data/lib/coppertone/record.rb +86 -0
  47. data/lib/coppertone/record_evaluator.rb +63 -0
  48. data/lib/coppertone/record_finder.rb +34 -0
  49. data/lib/coppertone/request.rb +68 -0
  50. data/lib/coppertone/request_context.rb +67 -0
  51. data/lib/coppertone/request_count_limiter.rb +36 -0
  52. data/lib/coppertone/result.rb +50 -0
  53. data/lib/coppertone/sender_identity.rb +39 -0
  54. data/lib/coppertone/spf_service.rb +9 -0
  55. data/lib/coppertone/term.rb +13 -0
  56. data/lib/coppertone/utils/domain_utils.rb +59 -0
  57. data/lib/coppertone/utils/host_utils.rb +22 -0
  58. data/lib/coppertone/utils/ip_in_domain_checker.rb +53 -0
  59. data/lib/coppertone/utils/validated_domain_finder.rb +40 -0
  60. data/lib/coppertone/utils.rb +4 -0
  61. data/lib/coppertone/version.rb +3 -0
  62. data/lib/coppertone.rb +48 -0
  63. data/lib/resolv/dns/resource/in/spf.rb +15 -0
  64. data/spec/directive_spec.rb +41 -0
  65. data/spec/dns/resolv_client_spec.rb +307 -0
  66. data/spec/domain_spec_spec.rb +35 -0
  67. data/spec/ip_address_wrapper_spec.rb +67 -0
  68. data/spec/macro_context_spec.rb +69 -0
  69. data/spec/macro_string/macro_expand_spec.rb +79 -0
  70. data/spec/macro_string/macro_literal_spec.rb +27 -0
  71. data/spec/macro_string/macro_static_expand_spec.rb +67 -0
  72. data/spec/macro_string_spec.rb +20 -0
  73. data/spec/mechanism/a_spec.rb +198 -0
  74. data/spec/mechanism/all_spec.rb +22 -0
  75. data/spec/mechanism/exists_spec.rb +91 -0
  76. data/spec/mechanism/include_spec.rb +43 -0
  77. data/spec/mechanism/ip4_spec.rb +110 -0
  78. data/spec/mechanism/ip6_spec.rb +104 -0
  79. data/spec/mechanism/mx_spec.rb +51 -0
  80. data/spec/mechanism/ptr_spec.rb +43 -0
  81. data/spec/mechanism_spec.rb +4 -0
  82. data/spec/modifier_spec.rb +4 -0
  83. data/spec/open_spf/ALL_mechanism_syntax_spec.rb +38 -0
  84. data/spec/open_spf/A_mechanism_syntax_spec.rb +159 -0
  85. data/spec/open_spf/EXISTS_mechanism_syntax_spec.rb +46 -0
  86. data/spec/open_spf/IP4_mechanism_syntax_spec.rb +59 -0
  87. data/spec/open_spf/IP6_mechanism_syntax_spec.rb +60 -0
  88. data/spec/open_spf/Include_mechanism_semantics_and_syntax_spec.rb +56 -0
  89. data/spec/open_spf/Initial_processing_spec.rb +77 -0
  90. data/spec/open_spf/MX_mechanism_syntax_spec.rb +119 -0
  91. data/spec/open_spf/Macro_expansion_rules_spec.rb +154 -0
  92. data/spec/open_spf/PTR_mechanism_syntax_spec.rb +42 -0
  93. data/spec/open_spf/Processing_limits_spec.rb +72 -0
  94. data/spec/open_spf/Record_evaluation_spec.rb +75 -0
  95. data/spec/open_spf/Record_lookup_spec.rb +48 -0
  96. data/spec/open_spf/Selecting_records_spec.rb +61 -0
  97. data/spec/open_spf/Semantics_of_exp_and_other_modifiers_spec.rb +167 -0
  98. data/spec/open_spf/Test_cases_from_implementation_bugs_spec.rb +17 -0
  99. data/spec/qualifier_spec.rb +54 -0
  100. data/spec/record_evaluator_spec.rb +4 -0
  101. data/spec/record_finder_spec.rb +4 -0
  102. data/spec/record_spec.rb +100 -0
  103. data/spec/request_context_spec.rb +43 -0
  104. data/spec/request_count_limiter_spec.rb +28 -0
  105. data/spec/result_spec.rb +4 -0
  106. data/spec/rfc7208-tests.yml +2548 -0
  107. data/spec/sender_identity_spec.rb +69 -0
  108. data/spec/spec_helper.rb +8 -0
  109. data/spec/term_spec.rb +38 -0
  110. data/spec/utils/domain_utils_spec.rb +60 -0
  111. data/spec/utils/host_utils_spec.rb +32 -0
  112. data/spec/utils/ip_in_domain_checker_spec.rb +4 -0
  113. data/spec/utils/validated_domain_finder_spec.rb +4 -0
  114. metadata +306 -0
@@ -0,0 +1,51 @@
1
+ require 'spec_helper'
2
+
3
+ describe Coppertone::Mechanism::MX do
4
+ context '#new' do
5
+ it 'should not fail if called with a nil argument' do
6
+ mech = Coppertone::Mechanism::MX.new(nil)
7
+ expect(mech).not_to be_nil
8
+ expect(mech.domain_spec).to be_nil
9
+ expect(mech.ip_v4_cidr_length).to eq(32)
10
+ expect(mech.ip_v6_cidr_length).to eq(128)
11
+ end
12
+
13
+ it 'should not fail if called with a blank argument' do
14
+ mech = Coppertone::Mechanism::MX.new('')
15
+ expect(mech).not_to be_nil
16
+ expect(mech.domain_spec).to be_nil
17
+ expect(mech.ip_v4_cidr_length).to eq(32)
18
+ expect(mech.ip_v6_cidr_length).to eq(128)
19
+ end
20
+
21
+ it 'should fail if called with an invalid macrostring' do
22
+ expect do
23
+ Coppertone::Mechanism::MX.new('abc%:def')
24
+ end.to raise_error(Coppertone::InvalidMechanismError)
25
+ end
26
+ end
27
+
28
+ context '#create' do
29
+ it 'should fail if called with a nil argument' do
30
+ mech = Coppertone::Mechanism::MX.create(nil)
31
+ expect(mech).not_to be_nil
32
+ expect(mech.domain_spec).to be_nil
33
+ expect(mech.ip_v4_cidr_length).to eq(32)
34
+ expect(mech.ip_v6_cidr_length).to eq(128)
35
+ end
36
+
37
+ it 'should fail if called with a blank argument' do
38
+ mech = Coppertone::Mechanism::MX.create('')
39
+ expect(mech).not_to be_nil
40
+ expect(mech.domain_spec).to be_nil
41
+ expect(mech.ip_v4_cidr_length).to eq(32)
42
+ expect(mech.ip_v6_cidr_length).to eq(128)
43
+ end
44
+
45
+ it 'should fail if called with an argument invalid macrostring' do
46
+ expect do
47
+ Coppertone::Mechanism::MX.create('abc%:def')
48
+ end.to raise_error(Coppertone::InvalidMechanismError)
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,43 @@
1
+ require 'spec_helper'
2
+
3
+ describe Coppertone::Mechanism::Ptr do
4
+ context '#new' do
5
+ it 'should not fail if called with a nil argument' do
6
+ mech = Coppertone::Mechanism::Ptr.new(nil)
7
+ expect(mech).not_to be_nil
8
+ expect(mech.domain_spec).to be_nil
9
+ end
10
+
11
+ it 'should not fail if called with a blank argument' do
12
+ mech = Coppertone::Mechanism::Ptr.new('')
13
+ expect(mech).not_to be_nil
14
+ expect(mech.domain_spec).to be_nil
15
+ end
16
+
17
+ it 'should fail if called with an invalid macrostring' do
18
+ expect do
19
+ Coppertone::Mechanism::Ptr.new('abc%:def')
20
+ end.to raise_error(Coppertone::InvalidMechanismError)
21
+ end
22
+ end
23
+
24
+ context '#create' do
25
+ it 'should fail if called with a nil argument' do
26
+ mech = Coppertone::Mechanism::Ptr.create(nil)
27
+ expect(mech).not_to be_nil
28
+ expect(mech.domain_spec).to be_nil
29
+ end
30
+
31
+ it 'should fail if called with a blank argument' do
32
+ mech = Coppertone::Mechanism::Ptr.create('')
33
+ expect(mech).not_to be_nil
34
+ expect(mech.domain_spec).to be_nil
35
+ end
36
+
37
+ it 'should fail if called with an argument invalid macrostring' do
38
+ expect do
39
+ Coppertone::Mechanism::Ptr.create('abc%:def')
40
+ end.to raise_error(Coppertone::InvalidMechanismError)
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,4 @@
1
+ require 'spec_helper'
2
+
3
+ describe Coppertone::Mechanism do
4
+ end
@@ -0,0 +1,4 @@
1
+ require 'spec_helper'
2
+
3
+ describe Coppertone::Modifier do
4
+ end
@@ -0,0 +1,38 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'ALL mechanism syntax' do
4
+ let(:zonefile) do
5
+ { 'mail.example.com' => [{ 'A' => '1.2.3.4' }], 'e1.example.com' => [{ 'TXT' => 'v=spf1 -all.' }], 'e2.example.com' => [{ 'TXT' => 'v=spf1 -all:foobar' }], 'e3.example.com' => [{ 'TXT' => 'v=spf1 -all/8' }], 'e4.example.com' => [{ 'TXT' => 'v=spf1 ?all' }], 'e5.example.com' => [{ 'TXT' => 'v=spf1 all -all' }] }
6
+ end
7
+
8
+ let(:dns_client) { Coppertone::DNS::MockClient.new(zonefile) }
9
+ let(:options) { { dns_client: dns_client } }
10
+
11
+ it 'all = "all" ' do
12
+ # At least one implementation got this wrong
13
+ result = Coppertone::SpfService.authenticate_email('1.2.3.4', 'foo@e1.example.com', 'mail.example.com', options)
14
+ expect(%i(permerror)).to include(result.code)
15
+ end
16
+
17
+ it 'all = "all" ' do
18
+ # At least one implementation got this wrong
19
+ result = Coppertone::SpfService.authenticate_email('1.2.3.4', 'foo@e2.example.com', 'mail.example.com', options)
20
+ expect(%i(permerror)).to include(result.code)
21
+ end
22
+
23
+ it 'all = "all" ' do
24
+ result = Coppertone::SpfService.authenticate_email('1.2.3.4', 'foo@e3.example.com', 'mail.example.com', options)
25
+ expect(%i(permerror)).to include(result.code)
26
+ end
27
+
28
+ it 'all = "all" ' do
29
+ result = Coppertone::SpfService.authenticate_email('1.2.3.4', 'foo@e4.example.com', 'mail.example.com', options)
30
+ expect(%i(neutral)).to include(result.code)
31
+ end
32
+
33
+ it 'all = "all" ' do
34
+ result = Coppertone::SpfService.authenticate_email('1.2.3.4', 'foo@e5.example.com', 'mail.example.com', options)
35
+ expect(%i(pass)).to include(result.code)
36
+ end
37
+
38
+ end
@@ -0,0 +1,159 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'A mechanism syntax' do
4
+ let(:zonefile) do
5
+ { 'mail.example.com' => [{ 'A' => '1.2.3.4' }], 'e1.example.com' => [{ 'TXT' => 'v=spf1 a/0 -all' }], 'e2.example.com' => [{ 'A' => '1.1.1.1' }, { 'AAAA' => '1234::2' }, { 'TXT' => 'v=spf1 a/0 -all' }], 'e2a.example.com' => [{ 'AAAA' => '1234::1' }, { 'TXT' => 'v=spf1 a//0 -all' }], 'e2b.example.com' => [{ 'A' => '1.1.1.1' }, { 'TXT' => 'v=spf1 a//0 -all' }], 'ipv6.example.com' => [{ 'AAAA' => '1234::1' }, { 'A' => '1.1.1.1' }, { 'TXT' => 'v=spf1 a -all' }], 'e3.example.com' => [{ 'TXT' => "v=spf1 a:foo.example.com\u0000" }], 'e4.example.com' => [{ 'TXT' => 'v=spf1 a:111.222.33.44' }], 'e5.example.com' => [{ 'TXT' => 'v=spf1 a:abc.123' }], 'e5a.example.com' => [{ 'TXT' => 'v=spf1 a:museum' }], 'e5b.example.com' => [{ 'TXT' => 'v=spf1 a:museum.' }], 'e6.example.com' => [{ 'TXT' => 'v=spf1 a//33 -all' }], 'e6a.example.com' => [{ 'TXT' => 'v=spf1 a/33 -all' }], 'e7.example.com' => [{ 'TXT' => 'v=spf1 a//129 -all' }], 'e8.example.com' => [{ 'A' => '1.2.3.5' }, { 'AAAA' => '2001:db8:1234::dead:beef' }, { 'TXT' => 'v=spf1 a/24//64 -all' }], 'e8e.example.com' => [{ 'A' => '1.2.3.5' }, { 'AAAA' => '2001:db8:1234::dead:beef' }, { 'TXT' => 'v=spf1 a/24/64 -all' }], 'e8a.example.com' => [{ 'A' => '1.2.3.5' }, { 'AAAA' => '2001:db8:1234::dead:beef' }, { 'TXT' => 'v=spf1 a/24 -all' }], 'e8b.example.com' => [{ 'A' => '1.2.3.5' }, { 'AAAA' => '2001:db8:1234::dead:beef' }, { 'TXT' => 'v=spf1 a//64 -all' }], 'e9.example.com' => [{ 'TXT' => 'v=spf1 a:example.com:8080' }], 'e10.example.com' => [{ 'TXT' => 'v=spf1 a:foo.example.com/24' }], 'foo.example.com' => [{ 'A' => '1.1.1.1' }, { 'A' => '1.2.3.5' }], 'e11.example.com' => [{ 'TXT' => 'v=spf1 a:foo:bar/baz.example.com' }], 'foo:bar/baz.example.com' => [{ 'A' => '1.2.3.4' }], 'e12.example.com' => [{ 'TXT' => 'v=spf1 a:example.-com' }], 'e13.example.com' => [{ 'TXT' => 'v=spf1 a:' }], 'e14.example.com' => [{ 'TXT' => 'v=spf1 a:foo.example.xn--zckzah -all' }], 'foo.example.xn--zckzah' => [{ 'A' => '1.2.3.4' }] }
6
+ end
7
+
8
+ let(:dns_client) { Coppertone::DNS::MockClient.new(zonefile) }
9
+ let(:options) { { dns_client: dns_client } }
10
+
11
+ it 'A = "a" [ ":" domain-spec ] [ dual-cidr-length ] dual-cidr-length = [ ip4-cidr-length ] [ "/" ip6-cidr-length ] ' do
12
+ result = Coppertone::SpfService.authenticate_email('1.2.3.4', 'foo@e6.example.com', 'mail.example.com', options)
13
+ expect(%i(fail)).to include(result.code)
14
+ end
15
+
16
+ it 'A = "a" [ ":" domain-spec ] [ dual-cidr-length ] dual-cidr-length = [ ip4-cidr-length ] [ "/" ip6-cidr-length ] ' do
17
+ result = Coppertone::SpfService.authenticate_email('1.2.3.4', 'foo@e6a.example.com', 'mail.example.com', options)
18
+ expect(%i(permerror)).to include(result.code)
19
+ end
20
+
21
+ it 'A = "a" [ ":" domain-spec ] [ dual-cidr-length ] dual-cidr-length = [ ip4-cidr-length ] [ "/" ip6-cidr-length ] ' do
22
+ result = Coppertone::SpfService.authenticate_email('1.2.3.4', 'foo@e7.example.com', 'mail.example.com', options)
23
+ expect(%i(permerror)).to include(result.code)
24
+ end
25
+
26
+ it 'A = "a" [ ":" domain-spec ] [ dual-cidr-length ] dual-cidr-length = [ ip4-cidr-length ] [ "/" ip6-cidr-length ] ' do
27
+ result = Coppertone::SpfService.authenticate_email('1.2.3.4', 'foo@e8.example.com', 'mail.example.com', options)
28
+ expect(%i(pass)).to include(result.code)
29
+ end
30
+
31
+ it 'A = "a" [ ":" domain-spec ] [ dual-cidr-length ] dual-cidr-length = [ ip4-cidr-length ] [ "/" ip6-cidr-length ] ' do
32
+ result = Coppertone::SpfService.authenticate_email('1.2.3.4', 'foo@e8e.example.com', 'mail.example.com', options)
33
+ expect(%i(permerror)).to include(result.code)
34
+ end
35
+
36
+ it 'A = "a" [ ":" domain-spec ] [ dual-cidr-length ] dual-cidr-length = [ ip4-cidr-length ] [ "/" ip6-cidr-length ] ' do
37
+ result = Coppertone::SpfService.authenticate_email('2001:db8:1234::cafe:babe', 'foo@e8.example.com', 'mail.example.com', options)
38
+ expect(%i(pass)).to include(result.code)
39
+ end
40
+
41
+ it 'A = "a" [ ":" domain-spec ] [ dual-cidr-length ] dual-cidr-length = [ ip4-cidr-length ] [ "/" ip6-cidr-length ] ' do
42
+ result = Coppertone::SpfService.authenticate_email('1.2.3.4', 'foo@e8b.example.com', 'mail.example.com', options)
43
+ expect(%i(fail)).to include(result.code)
44
+ end
45
+
46
+ it 'A = "a" [ ":" domain-spec ] [ dual-cidr-length ] dual-cidr-length = [ ip4-cidr-length ] [ "/" ip6-cidr-length ] ' do
47
+ result = Coppertone::SpfService.authenticate_email('2001:db8:1234::cafe:babe', 'foo@e8a.example.com', 'mail.example.com', options)
48
+ expect(%i(fail)).to include(result.code)
49
+ end
50
+
51
+ it 'A matches any returned IP.' do
52
+ result = Coppertone::SpfService.authenticate_email('1.2.3.4', 'foo@e10.example.com', 'mail.example.com', options)
53
+ expect(%i(pass)).to include(result.code)
54
+ end
55
+
56
+ it 'A matches any returned IP.' do
57
+ result = Coppertone::SpfService.authenticate_email('1.2.3.4', 'foo@e10.example.com', 'mail.example.com', options)
58
+ expect(%i(pass)).to include(result.code)
59
+ end
60
+
61
+ it 'domain-spec must pass basic syntax checks; a : may appear in domain-spec, but not in top-label' do
62
+ result = Coppertone::SpfService.authenticate_email('1.2.3.4', 'foo@e9.example.com', 'mail.example.com', options)
63
+ expect(%i(permerror)).to include(result.code)
64
+ end
65
+
66
+ it 'If no ips are returned, A mechanism does not match, even with /0.' do
67
+ result = Coppertone::SpfService.authenticate_email('1.2.3.4', 'foo@e1.example.com', 'mail.example.com', options)
68
+ expect(%i(fail)).to include(result.code)
69
+ end
70
+
71
+ it 'Matches if any A records are present in DNS.' do
72
+ result = Coppertone::SpfService.authenticate_email('1.2.3.4', 'foo@e2.example.com', 'mail.example.com', options)
73
+ expect(%i(pass)).to include(result.code)
74
+ end
75
+
76
+ it 'Matches if any A records are present in DNS.' do
77
+ result = Coppertone::SpfService.authenticate_email('1234::1', 'foo@e2.example.com', 'mail.example.com', options)
78
+ expect(%i(fail)).to include(result.code)
79
+ end
80
+
81
+ it 'Would match if any AAAA records are present in DNS, but not for an IP4 connection.' do
82
+ result = Coppertone::SpfService.authenticate_email('1.2.3.4', 'foo@e2a.example.com', 'mail.example.com', options)
83
+ expect(%i(fail)).to include(result.code)
84
+ end
85
+
86
+ it 'Would match if any AAAA records are present in DNS, but not for an IP4 connection.' do
87
+ result = Coppertone::SpfService.authenticate_email('::FFFF:1.2.3.4', 'foo@e2a.example.com', 'mail.example.com', options)
88
+ expect(%i(fail)).to include(result.code)
89
+ end
90
+
91
+ it 'Matches if any AAAA records are present in DNS.' do
92
+ result = Coppertone::SpfService.authenticate_email('1234::1', 'foo@e2a.example.com', 'mail.example.com', options)
93
+ expect(%i(pass)).to include(result.code)
94
+ end
95
+
96
+ it 'Simple IP6 Address match with dual stack.' do
97
+ result = Coppertone::SpfService.authenticate_email('1234::1', 'foo@ipv6.example.com', 'mail.example.com', options)
98
+ expect(%i(pass)).to include(result.code)
99
+ end
100
+
101
+ it 'No match if no AAAA records are present in DNS.' do
102
+ result = Coppertone::SpfService.authenticate_email('1234::1', 'foo@e2b.example.com', 'mail.example.com', options)
103
+ expect(%i(fail)).to include(result.code)
104
+ end
105
+
106
+ it 'Null octets not allowed in toplabel' do
107
+ result = Coppertone::SpfService.authenticate_email('1.2.3.5', 'foo@e3.example.com', 'mail.example.com', options)
108
+ expect(%i(permerror)).to include(result.code)
109
+ end
110
+
111
+ it 'toplabel may not be all numeric' do
112
+ # A common publishing mistake is using ip4 addresses with A mechanism. This should receive special diagnostic attention in the permerror.
113
+ result = Coppertone::SpfService.authenticate_email('1.2.3.4', 'foo@e4.example.com', 'mail.example.com', options)
114
+ expect(%i(permerror)).to include(result.code)
115
+ end
116
+
117
+ it 'toplabel may not be all numeric' do
118
+ result = Coppertone::SpfService.authenticate_email('1.2.3.4', 'foo@e5.example.com', 'mail.example.com', options)
119
+ expect(%i(permerror)).to include(result.code)
120
+ end
121
+
122
+ it 'toplabel may contain dashes' do
123
+ # Going from the "toplabel" grammar definition, an implementation using regular expressions in incrementally parsing SPF records might erroneously try to match a TLD such as ".xn--zckzah" (cf. IDN TLDs!) to \'( *alphanum ALPHA *alphanum )\' first before trying the alternative \'( 1*alphanum "-" *( alphanum / "-" ) alphanum )\', essentially causing a non-greedy, and thus, incomplete match. Make sure a greedy match is performed!
124
+ result = Coppertone::SpfService.authenticate_email('1.2.3.4', 'foo@e14.example.com', 'mail.example.com', options)
125
+ expect(%i(pass)).to include(result.code)
126
+ end
127
+
128
+ it 'toplabel may not begin with a dash' do
129
+ result = Coppertone::SpfService.authenticate_email('1.2.3.4', 'foo@e12.example.com', 'mail.example.com', options)
130
+ expect(%i(permerror)).to include(result.code)
131
+ end
132
+
133
+ it 'domain-spec may not consist of only a toplabel.' do
134
+ result = Coppertone::SpfService.authenticate_email('1.2.3.4', 'foo@e5a.example.com', 'mail.example.com', options)
135
+ expect(%i(permerror)).to include(result.code)
136
+ end
137
+
138
+ it 'domain-spec may not consist of only a toplabel.' do
139
+ # "A trailing dot doesn\'t help."
140
+ result = Coppertone::SpfService.authenticate_email('1.2.3.4', 'foo@e5b.example.com', 'mail.example.com', options)
141
+ expect(%i(permerror)).to include(result.code)
142
+ end
143
+
144
+ it 'domain-spec may contain any visible char except %' do
145
+ result = Coppertone::SpfService.authenticate_email('1.2.3.4', 'foo@e11.example.com', 'mail.example.com', options)
146
+ expect(%i(pass)).to include(result.code)
147
+ end
148
+
149
+ it 'domain-spec may contain any visible char except %' do
150
+ result = Coppertone::SpfService.authenticate_email('::FFFF:1.2.3.4', 'foo@e11.example.com', 'mail.example.com', options)
151
+ expect(%i(pass)).to include(result.code)
152
+ end
153
+
154
+ it 'domain-spec cannot be empty.' do
155
+ result = Coppertone::SpfService.authenticate_email('1.2.3.4', 'foo@e13.example.com', 'mail.example.com', options)
156
+ expect(%i(permerror)).to include(result.code)
157
+ end
158
+
159
+ end
@@ -0,0 +1,46 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'EXISTS mechanism syntax' do
4
+ let(:zonefile) do
5
+ { 'mail.example.com' => [{ 'A' => '1.2.3.4' }], 'mail6.example.com' => [{ 'AAAA' => 'CAFE:BABE::4' }], 'err.example.com' => ['TIMEOUT'], 'e1.example.com' => [{ 'TXT' => 'v=spf1 exists:' }], 'e2.example.com' => [{ 'TXT' => 'v=spf1 exists' }], 'e3.example.com' => [{ 'TXT' => 'v=spf1 exists:mail.example.com/24' }], 'e4.example.com' => [{ 'TXT' => 'v=spf1 exists:mail.example.com' }], 'e5.example.com' => [{ 'TXT' => 'v=spf1 exists:mail6.example.com -all' }], 'e6.example.com' => [{ 'TXT' => 'v=spf1 exists:err.example.com -all' }] }
6
+ end
7
+
8
+ let(:dns_client) { Coppertone::DNS::MockClient.new(zonefile) }
9
+ let(:options) { { dns_client: dns_client } }
10
+
11
+ it 'domain-spec cannot be empty.' do
12
+ result = Coppertone::SpfService.authenticate_email('1.2.3.4', 'foo@e1.example.com', 'mail.example.com', options)
13
+ expect(%i(permerror)).to include(result.code)
14
+ end
15
+
16
+ it 'exists = "exists" ":" domain-spec' do
17
+ result = Coppertone::SpfService.authenticate_email('1.2.3.4', 'foo@e2.example.com', 'mail.example.com', options)
18
+ expect(%i(permerror)).to include(result.code)
19
+ end
20
+
21
+ it 'exists = "exists" ":" domain-spec' do
22
+ result = Coppertone::SpfService.authenticate_email('1.2.3.4', 'foo@e3.example.com', 'mail.example.com', options)
23
+ expect(%i(permerror)).to include(result.code)
24
+ end
25
+
26
+ it 'mechanism matches if any DNS A RR exists' do
27
+ result = Coppertone::SpfService.authenticate_email('1.2.3.4', 'foo@e4.example.com', 'mail.example.com', options)
28
+ expect(%i(pass)).to include(result.code)
29
+ end
30
+
31
+ it 'The lookup type is A even when the connection is ip6' do
32
+ result = Coppertone::SpfService.authenticate_email('CAFE:BABE::3', 'foo@e4.example.com', 'mail.example.com', options)
33
+ expect(%i(pass)).to include(result.code)
34
+ end
35
+
36
+ it 'The lookup type is A even when the connection is ip6' do
37
+ result = Coppertone::SpfService.authenticate_email('CAFE:BABE::3', 'foo@e5.example.com', 'mail.example.com', options)
38
+ expect(%i(fail)).to include(result.code)
39
+ end
40
+
41
+ it 'Result for DNS error clarified in RFC7208: MTAs or other processors SHOULD impose a limit on the maximum amount of elapsed time to evaluate check_host(). Such a limit SHOULD allow at least 20 seconds. If such a limit is exceeded, the result of authorization SHOULD be "temperror".' do
42
+ result = Coppertone::SpfService.authenticate_email('CAFE:BABE::3', 'foo@e6.example.com', 'mail.example.com', options)
43
+ expect(%i(temperror)).to include(result.code)
44
+ end
45
+
46
+ end
@@ -0,0 +1,59 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'IP4 mechanism syntax' do
4
+ let(:zonefile) do
5
+ { 'mail.example.com' => [{ 'A' => '1.2.3.4' }], 'e1.example.com' => [{ 'TXT' => 'v=spf1 ip4:1.1.1.1/0 -all' }], 'e2.example.com' => [{ 'TXT' => 'v=spf1 ip4:1.2.3.4/32 -all' }], 'e3.example.com' => [{ 'TXT' => 'v=spf1 ip4:1.2.3.4/33 -all' }], 'e4.example.com' => [{ 'TXT' => 'v=spf1 ip4:1.2.3.4/032 -all' }], 'e5.example.com' => [{ 'TXT' => 'v=spf1 ip4' }], 'e6.example.com' => [{ 'TXT' => 'v=spf1 ip4:1.2.3.4//32' }], 'e7.example.com' => [{ 'TXT' => 'v=spf1 -ip4:1.2.3.4 ip6:::FFFF:1.2.3.4' }], 'e8.example.com' => [{ 'TXT' => 'v=spf1 ip4:1.2.3.4:8080' }], 'e9.example.com' => [{ 'TXT' => 'v=spf1 ip4:1.2.3' }] }
6
+ end
7
+
8
+ let(:dns_client) { Coppertone::DNS::MockClient.new(zonefile) }
9
+ let(:options) { { dns_client: dns_client } }
10
+
11
+ it 'ip4-cidr-length = "/" 1*DIGIT' do
12
+ result = Coppertone::SpfService.authenticate_email('1.2.3.4', 'foo@e1.example.com', 'mail.example.com', options)
13
+ expect(%i(pass)).to include(result.code)
14
+ end
15
+
16
+ it 'ip4-cidr-length = "/" 1*DIGIT' do
17
+ result = Coppertone::SpfService.authenticate_email('1.2.3.4', 'foo@e2.example.com', 'mail.example.com', options)
18
+ expect(%i(pass)).to include(result.code)
19
+ end
20
+
21
+ it 'Invalid CIDR should get permerror.' do
22
+ # The RFC4408 was silent on ip4 CIDR > 32 or ip6 CIDR > 128, but RFC7208 is explicit. Invalid CIDR is prohibited.
23
+ result = Coppertone::SpfService.authenticate_email('1.2.3.4', 'foo@e3.example.com', 'mail.example.com', options)
24
+ expect(%i(permerror)).to include(result.code)
25
+ end
26
+
27
+ it 'Invalid CIDR should get permerror.' do
28
+ # Leading zeros are not explicitly prohibited by the RFC. However, since the RFC explicity prohibits leading zeros in ip4-network, our interpretation is that CIDR should be also.
29
+ result = Coppertone::SpfService.authenticate_email('1.2.3.4', 'foo@e4.example.com', 'mail.example.com', options)
30
+ expect(%i(permerror)).to include(result.code)
31
+ end
32
+
33
+ it 'IP4 = "ip4" ":" ip4-network [ ip4-cidr-length ]' do
34
+ result = Coppertone::SpfService.authenticate_email('1.2.3.4', 'foo@e5.example.com', 'mail.example.com', options)
35
+ expect(%i(permerror)).to include(result.code)
36
+ end
37
+
38
+ it 'IP4 = "ip4" ":" ip4-network [ ip4-cidr-length ]' do
39
+ # This has actually been published in SPF records.
40
+ result = Coppertone::SpfService.authenticate_email('1.2.3.4', 'foo@e8.example.com', 'mail.example.com', options)
41
+ expect(%i(permerror)).to include(result.code)
42
+ end
43
+
44
+ it 'It is not permitted to omit parts of the IP address instead of using CIDR notations.' do
45
+ result = Coppertone::SpfService.authenticate_email('1.2.3.4', 'foo@e9.example.com', 'mail.example.com', options)
46
+ expect(%i(permerror)).to include(result.code)
47
+ end
48
+
49
+ it 'dual-cidr-length not permitted on ip4' do
50
+ result = Coppertone::SpfService.authenticate_email('1.2.3.4', 'foo@e6.example.com', 'mail.example.com', options)
51
+ expect(%i(permerror)).to include(result.code)
52
+ end
53
+
54
+ it 'IP4 mapped IP6 connections MUST be treated as IP4' do
55
+ result = Coppertone::SpfService.authenticate_email('::FFFF:1.2.3.4', 'foo@e7.example.com', 'mail.example.com', options)
56
+ expect(%i(fail)).to include(result.code)
57
+ end
58
+
59
+ end
@@ -0,0 +1,60 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'IP6 mechanism syntax' do
4
+ let(:zonefile) do
5
+ { 'mail.example.com' => [{ 'A' => '1.2.3.4' }], 'e1.example.com' => [{ 'TXT' => 'v=spf1 -all ip6' }], 'e2.example.com' => [{ 'TXT' => 'v=spf1 ip6:::1.1.1.1/0' }], 'e3.example.com' => [{ 'TXT' => 'v=spf1 ip6:::1.1.1.1/129' }], 'e4.example.com' => [{ 'TXT' => 'v=spf1 ip6:::1.1.1.1//33' }], 'e5.example.com' => [{ 'TXT' => 'v=spf1 ip6:CAFE:BABE:8000::/33' }], 'e6.example.com' => [{ 'TXT' => 'v=spf1 ip6::CAFE::BABE' }] }
6
+ end
7
+
8
+ let(:dns_client) { Coppertone::DNS::MockClient.new(zonefile) }
9
+ let(:options) { { dns_client: dns_client } }
10
+
11
+ it 'IP6 = "ip6" ":" ip6-network [ ip6-cidr-length ]' do
12
+ result = Coppertone::SpfService.authenticate_email('1.2.3.4', 'foo@e1.example.com', 'mail.example.com', options)
13
+ expect(%i(permerror)).to include(result.code)
14
+ end
15
+
16
+ it 'IP4 connections do not match ip6.' do
17
+ # There was controversy over IPv4 mapped connections. RFC7208 clearly states IPv4 mapped addresses only match ip4: mechanisms.
18
+ result = Coppertone::SpfService.authenticate_email('1.2.3.4', 'foo@e2.example.com', 'mail.example.com', options)
19
+ expect(%i(neutral)).to include(result.code)
20
+ end
21
+
22
+ it 'Even if the SMTP connection is via IPv6, an IPv4-mapped IPv6 IP address (see RFC 3513, Section 2.5.5) MUST still be considered an IPv4 address.' do
23
+ # There was controversy over ip4 mapped connections. RFC7208 clearly requires such connections to be considered as ip4 only.
24
+ result = Coppertone::SpfService.authenticate_email('::FFFF:1.2.3.4', 'foo@e2.example.com', 'mail.example.com', options)
25
+ expect(%i(neutral)).to include(result.code)
26
+ end
27
+
28
+ it 'Match any IP6' do
29
+ result = Coppertone::SpfService.authenticate_email('DEAF:BABE::CAB:FEE', 'foo@e2.example.com', 'mail.example.com', options)
30
+ expect(%i(pass)).to include(result.code)
31
+ end
32
+
33
+ it 'Invalid CIDR' do
34
+ # IP4 only implementations MUST fully syntax check all mechanisms, even if they otherwise ignore them.
35
+ result = Coppertone::SpfService.authenticate_email('1.2.3.4', 'foo@e3.example.com', 'mail.example.com', options)
36
+ expect(%i(permerror)).to include(result.code)
37
+ end
38
+
39
+ it 'dual-cidr syntax not used for ip6' do
40
+ # IP4 only implementations MUST fully syntax check all mechanisms, even if they otherwise ignore them.
41
+ result = Coppertone::SpfService.authenticate_email('1.2.3.4', 'foo@e4.example.com', 'mail.example.com', options)
42
+ expect(%i(permerror)).to include(result.code)
43
+ end
44
+
45
+ it 'make sure ip4 cidr restriction are not used for ip6' do
46
+ result = Coppertone::SpfService.authenticate_email('CAFE:BABE:8000::', 'foo@e5.example.com', 'mail.example.com', options)
47
+ expect(%i(pass)).to include(result.code)
48
+ end
49
+
50
+ it 'make sure ip4 cidr restriction are not used for ip6' do
51
+ result = Coppertone::SpfService.authenticate_email('1.2.3.4', 'foo@e5.example.com', 'mail.example.com', options)
52
+ expect(%i(neutral)).to include(result.code)
53
+ end
54
+
55
+ it '' do
56
+ result = Coppertone::SpfService.authenticate_email('1.2.3.4', 'foo@e6.example.com', 'mail.example.com', options)
57
+ expect(%i(permerror)).to include(result.code)
58
+ end
59
+
60
+ end
@@ -0,0 +1,56 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'Include mechanism semantics and syntax' do
4
+ let(:zonefile) do
5
+ { 'mail.example.com' => [{ 'A' => '1.2.3.4' }], 'ip5.example.com' => [{ 'TXT' => 'v=spf1 ip4:1.2.3.5 -all' }], 'ip6.example.com' => [{ 'TXT' => 'v=spf1 ip4:1.2.3.6 ~all' }], 'ip7.example.com' => [{ 'TXT' => 'v=spf1 ip4:1.2.3.7 ?all' }], 'ip8.example.com' => ['TIMEOUT'], 'erehwon.example.com' => [{ 'TXT' => 'v=spfl am not an SPF record' }], 'e1.example.com' => [{ 'TXT' => 'v=spf1 include:ip5.example.com ~all' }], 'e2.example.com' => [{ 'TXT' => 'v=spf1 include:ip6.example.com all' }], 'e3.example.com' => [{ 'TXT' => 'v=spf1 include:ip7.example.com -all' }], 'e4.example.com' => [{ 'TXT' => 'v=spf1 include:ip8.example.com -all' }], 'e5.example.com' => [{ 'TXT' => 'v=spf1 include:e6.example.com -all' }], 'e6.example.com' => [{ 'TXT' => 'v=spf1 include +all' }], 'e7.example.com' => [{ 'TXT' => 'v=spf1 include:erehwon.example.com -all' }], 'e8.example.com' => [{ 'TXT' => 'v=spf1 include: -all' }], 'e9.example.com' => [{ 'TXT' => 'v=spf1 include:ip5.example.com/24 -all' }] }
6
+ end
7
+
8
+ let(:dns_client) { Coppertone::DNS::MockClient.new(zonefile) }
9
+ let(:options) { { dns_client: dns_client } }
10
+
11
+ it 'recursive check_host() result of fail causes include to not match.' do
12
+ result = Coppertone::SpfService.authenticate_email('1.2.3.4', 'foo@e1.example.com', 'mail.example.com', options)
13
+ expect(%i(softfail)).to include(result.code)
14
+ end
15
+
16
+ it 'recursive check_host() result of softfail causes include to not match.' do
17
+ result = Coppertone::SpfService.authenticate_email('1.2.3.4', 'foo@e2.example.com', 'mail.example.com', options)
18
+ expect(%i(pass)).to include(result.code)
19
+ end
20
+
21
+ it 'recursive check_host() result of neutral causes include to not match.' do
22
+ result = Coppertone::SpfService.authenticate_email('1.2.3.4', 'foo@e3.example.com', 'mail.example.com', options)
23
+ expect(%i(fail)).to include(result.code)
24
+ end
25
+
26
+ it 'recursive check_host() result of temperror causes include to temperror' do
27
+ result = Coppertone::SpfService.authenticate_email('1.2.3.4', 'foo@e4.example.com', 'mail.example.com', options)
28
+ expect(%i(temperror)).to include(result.code)
29
+ end
30
+
31
+ it 'recursive check_host() result of permerror causes include to permerror' do
32
+ result = Coppertone::SpfService.authenticate_email('1.2.3.4', 'foo@e5.example.com', 'mail.example.com', options)
33
+ expect(%i(permerror)).to include(result.code)
34
+ end
35
+
36
+ it 'include = "include" ":" domain-spec' do
37
+ result = Coppertone::SpfService.authenticate_email('1.2.3.4', 'foo@e6.example.com', 'mail.example.com', options)
38
+ expect(%i(permerror)).to include(result.code)
39
+ end
40
+
41
+ it 'include = "include" ":" domain-spec' do
42
+ result = Coppertone::SpfService.authenticate_email('1.2.3.4', 'foo@e9.example.com', 'mail.example.com', options)
43
+ expect(%i(permerror)).to include(result.code)
44
+ end
45
+
46
+ it 'recursive check_host() result of none causes include to permerror' do
47
+ result = Coppertone::SpfService.authenticate_email('1.2.3.4', 'foo@e7.example.com', 'mail.example.com', options)
48
+ expect(%i(permerror)).to include(result.code)
49
+ end
50
+
51
+ it 'domain-spec cannot be empty.' do
52
+ result = Coppertone::SpfService.authenticate_email('1.2.3.4', 'foo@e8.example.com', 'mail.example.com', options)
53
+ expect(%i(permerror)).to include(result.code)
54
+ end
55
+
56
+ end
@@ -0,0 +1,77 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'Initial processing' do
4
+ let(:zonefile) do
5
+ { 'example.com' => ['TIMEOUT'], 'example.net' => [{ 'TXT' => 'v=spf1 -all exp=exp.example.net' }], 'a.example.net' => [{ 'TXT' => 'v=spf1 -all exp=exp.example.net' }], 'exp.example.net' => [{ 'TXT' => '%{l}' }], 'a12345678901234567890123456789012345678901234567890123456789012.example.com' => [{ 'TXT' => 'v=spf1 -all' }], 'hosed.example.com' => [{ 'TXT' => 'v=spf1 a:garbage.example.net -all' }], 'hosed2.example.com' => [{ 'TXT' => "v=spf1 \u0080a:example.net -all" }], 'hosed3.example.com' => [{ 'TXT' => "v=spf1 a:example.net \u0096all" }], 'nothosed.example.com' => [{ 'TXT' => 'v=spf1 a:example.net -all' }, { 'TXT' => "\u0096" }], 'ctrl.example.com' => [{ 'TXT' => "v=spf1 a:ctrl.example.com\rptr -all" }, { 'A' => '192.0.2.3' }] }
6
+ end
7
+
8
+ let(:dns_client) { Coppertone::DNS::MockClient.new(zonefile) }
9
+ let(:options) { { dns_client: dns_client } }
10
+
11
+ it 'DNS labels limited to 63 chars.' do
12
+ # For initial processing, a long label results in None, not TempError
13
+ result = Coppertone::SpfService.authenticate_email('1.2.3.5', 'lyme.eater@A123456789012345678901234567890123456789012345678901234567890123.example.com', 'mail.example.net', options)
14
+ expect(%i(none)).to include(result.code)
15
+ end
16
+
17
+ it 'DNS labels limited to 63 chars.' do
18
+ result = Coppertone::SpfService.authenticate_email('1.2.3.5', 'lyme.eater@A12345678901234567890123456789012345678901234567890123456789012.example.com', 'mail.example.net', options)
19
+ expect(%i(fail)).to include(result.code)
20
+ end
21
+
22
+ it 'emptylabel' do
23
+ result = Coppertone::SpfService.authenticate_email('1.2.3.5', 'lyme.eater@A...example.com', 'mail.example.net', options)
24
+ expect(%i(none)).to include(result.code)
25
+ end
26
+
27
+ it 'helo-not-fqdn' do
28
+ result = Coppertone::SpfService.authenticate_email('1.2.3.5', '', 'A2345678', options)
29
+ expect(%i(none)).to include(result.code)
30
+ end
31
+
32
+ it 'helo-domain-literal' do
33
+ result = Coppertone::SpfService.authenticate_email('1.2.3.5', '', '[1.2.3.5]', options)
34
+ expect(%i(none)).to include(result.code)
35
+ end
36
+
37
+ it 'nolocalpart' do
38
+ result = Coppertone::SpfService.authenticate_email('1.2.3.4', '@example.net', 'mail.example.net', options)
39
+ expect(%i(fail)).to include(result.code)
40
+ expect(result.explanation).to eq('postmaster')
41
+ end
42
+
43
+ it 'domain-literal' do
44
+ result = Coppertone::SpfService.authenticate_email('1.2.3.5', 'foo@[1.2.3.5]', 'OEMCOMPUTER', options)
45
+ expect(%i(none)).to include(result.code)
46
+ end
47
+
48
+ it 'SPF policies are restricted to 7-bit ascii.' do
49
+ result = Coppertone::SpfService.authenticate_email('1.2.3.4', 'foobar@hosed.example.com', 'hosed', options)
50
+ expect(%i(permerror)).to include(result.code)
51
+ end
52
+
53
+ it 'SPF policies are restricted to 7-bit ascii.' do
54
+ # Checking a possibly different code path for non-ascii chars.
55
+ result = Coppertone::SpfService.authenticate_email('1.2.3.4', 'foobar@hosed2.example.com', 'hosed', options)
56
+ expect(%i(permerror)).to include(result.code)
57
+ end
58
+
59
+ it 'SPF policies are restricted to 7-bit ascii.' do
60
+ # Checking yet another code path for non-ascii chars.
61
+ result = Coppertone::SpfService.authenticate_email('1.2.3.4', 'foobar@hosed3.example.com', 'hosed', options)
62
+ expect(%i(permerror)).to include(result.code)
63
+ end
64
+
65
+ it 'Non-ascii content in non-SPF related records.' do
66
+ # Non-SPF related TXT records are none of our business.
67
+ result = Coppertone::SpfService.authenticate_email('1.2.3.4', 'foobar@nothosed.example.com', 'hosed', options)
68
+ expect(%i(fail)).to include(result.code)
69
+ expect(result.explanation).to eq('DEFAULT')
70
+ end
71
+
72
+ it 'Mechanisms are separated by spaces only, not any control char.' do
73
+ result = Coppertone::SpfService.authenticate_email('192.0.2.3', 'foobar@ctrl.example.com', 'hosed', options)
74
+ expect(%i(permerror)).to include(result.code)
75
+ end
76
+
77
+ end