coppertone 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +1 -0
- data/LICENSE +1 -1
- data/Rakefile +2 -2
- data/coppertone.gemspec +1 -0
- data/lib/coppertone/directive.rb +14 -7
- data/lib/coppertone/error.rb +24 -5
- data/lib/coppertone/ip_address_wrapper.rb +9 -6
- data/lib/coppertone/macro_context.rb +7 -13
- data/lib/coppertone/macro_string/macro_expand.rb +3 -2
- data/lib/coppertone/macro_string.rb +12 -5
- data/lib/coppertone/mechanism/a.rb +5 -1
- data/lib/coppertone/mechanism/all.rb +11 -4
- data/lib/coppertone/mechanism/cidr_parser.rb +1 -1
- data/lib/coppertone/mechanism/domain_spec_mechanism.rb +9 -0
- data/lib/coppertone/mechanism/domain_spec_optional.rb +1 -0
- data/lib/coppertone/mechanism/domain_spec_required.rb +1 -0
- data/lib/coppertone/mechanism/domain_spec_with_dual_cidr.rb +1 -0
- data/lib/coppertone/mechanism/exists.rb +5 -1
- data/lib/coppertone/mechanism/include.rb +17 -5
- data/lib/coppertone/mechanism/ip4.rb +5 -1
- data/lib/coppertone/mechanism/ip6.rb +5 -1
- data/lib/coppertone/mechanism/ip_mechanism.rb +9 -1
- data/lib/coppertone/mechanism/mx.rb +5 -1
- data/lib/coppertone/mechanism/ptr.rb +5 -1
- data/lib/coppertone/mechanism.rb +25 -2
- data/lib/coppertone/modifier/base.rb +1 -0
- data/lib/coppertone/modifier/exp.rb +6 -2
- data/lib/coppertone/modifier/redirect.rb +5 -1
- data/lib/coppertone/modifier/unknown.rb +6 -1
- data/lib/coppertone/modifier.rb +11 -2
- data/lib/coppertone/null_macro_context.rb +23 -0
- data/lib/coppertone/qualifier.rb +8 -0
- data/lib/coppertone/record.rb +33 -41
- data/lib/coppertone/record_finder.rb +3 -1
- data/lib/coppertone/record_term_parser.rb +30 -0
- data/lib/coppertone/request.rb +3 -1
- data/lib/coppertone/request_context.rb +1 -2
- data/lib/coppertone/result.rb +6 -2
- data/lib/coppertone/utils/validated_domain_finder.rb +6 -4
- data/lib/coppertone/version.rb +1 -1
- data/lib/coppertone.rb +3 -1
- data/spec/directive_spec.rb +13 -0
- data/spec/ip_address_wrapper_spec.rb +3 -0
- data/spec/macro_string_spec.rb +20 -0
- data/spec/mechanism/a_spec.rb +22 -7
- data/spec/mechanism/all_spec.rb +8 -0
- data/spec/mechanism/exists_spec.rb +14 -6
- data/spec/mechanism/include_spec.rb +13 -1
- data/spec/mechanism/ip4_spec.rb +15 -5
- data/spec/mechanism/ip6_spec.rb +18 -11
- data/spec/mechanism/mx_spec.rb +56 -0
- data/spec/mechanism/ptr_spec.rb +7 -0
- data/spec/modifier/exp_spec.rb +10 -0
- data/spec/modifier/redirect_spec.rb +10 -0
- data/spec/open_spf/ALL_mechanism_syntax_spec.rb +6 -6
- data/spec/open_spf/A_mechanism_syntax_spec.rb +30 -30
- data/spec/open_spf/EXISTS_mechanism_syntax_spec.rb +8 -8
- data/spec/open_spf/IP4_mechanism_syntax_spec.rb +10 -10
- data/spec/open_spf/IP6_mechanism_syntax_spec.rb +10 -10
- data/spec/open_spf/Include_mechanism_semantics_and_syntax_spec.rb +10 -10
- data/spec/open_spf/Initial_processing_spec.rb +21 -14
- data/spec/open_spf/MX_mechanism_syntax_spec.rb +22 -22
- data/spec/open_spf/Macro_expansion_rules_spec.rb +26 -30
- data/spec/open_spf/PTR_mechanism_syntax_spec.rb +7 -7
- data/spec/open_spf/Processing_limits_spec.rb +12 -12
- data/spec/open_spf/Record_evaluation_spec.rb +13 -13
- data/spec/open_spf/Record_lookup_spec.rb +8 -8
- data/spec/open_spf/Selecting_records_spec.rb +11 -11
- data/spec/open_spf/Semantics_of_exp_and_other_modifiers_spec.rb +27 -25
- data/spec/open_spf/Test_cases_from_implementation_bugs_spec.rb +2 -2
- data/spec/record_spec.rb +34 -13
- data/spec/request_context_spec.rb +1 -1
- data/spec/rfc7208-tests.yml +10 -0
- metadata +59 -48
- data/lib/coppertone/dns/error.rb +0 -9
- data/lib/coppertone/dns/mock_client.rb +0 -106
- data/lib/coppertone/dns/resolv_client.rb +0 -110
- data/lib/coppertone/dns.rb +0 -3
- data/lib/resolv/dns/resource/in/spf.rb +0 -15
- data/spec/dns/resolv_client_spec.rb +0 -307
@@ -5,52 +5,52 @@ describe 'Include mechanism semantics and syntax' do
|
|
5
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
6
|
end
|
7
7
|
|
8
|
-
let(:dns_client) {
|
8
|
+
let(:dns_client) { DNSAdapter::MockClient.new(zonefile) }
|
9
9
|
let(:options) { { dns_client: dns_client } }
|
10
10
|
|
11
11
|
it 'recursive check_host() result of fail causes include to not match.' do
|
12
12
|
result = Coppertone::SpfService.authenticate_email('1.2.3.4', 'foo@e1.example.com', 'mail.example.com', options)
|
13
|
-
expect(
|
13
|
+
expect([:softfail]).to include(result.code)
|
14
14
|
end
|
15
15
|
|
16
16
|
it 'recursive check_host() result of softfail causes include to not match.' do
|
17
17
|
result = Coppertone::SpfService.authenticate_email('1.2.3.4', 'foo@e2.example.com', 'mail.example.com', options)
|
18
|
-
expect(
|
18
|
+
expect([:pass]).to include(result.code)
|
19
19
|
end
|
20
20
|
|
21
21
|
it 'recursive check_host() result of neutral causes include to not match.' do
|
22
22
|
result = Coppertone::SpfService.authenticate_email('1.2.3.4', 'foo@e3.example.com', 'mail.example.com', options)
|
23
|
-
expect(
|
23
|
+
expect([:fail]).to include(result.code)
|
24
24
|
end
|
25
25
|
|
26
26
|
it 'recursive check_host() result of temperror causes include to temperror' do
|
27
27
|
result = Coppertone::SpfService.authenticate_email('1.2.3.4', 'foo@e4.example.com', 'mail.example.com', options)
|
28
|
-
expect(
|
28
|
+
expect([:temperror]).to include(result.code)
|
29
29
|
end
|
30
30
|
|
31
31
|
it 'recursive check_host() result of permerror causes include to permerror' do
|
32
32
|
result = Coppertone::SpfService.authenticate_email('1.2.3.4', 'foo@e5.example.com', 'mail.example.com', options)
|
33
|
-
expect(
|
33
|
+
expect([:permerror]).to include(result.code)
|
34
34
|
end
|
35
35
|
|
36
36
|
it 'include = "include" ":" domain-spec' do
|
37
37
|
result = Coppertone::SpfService.authenticate_email('1.2.3.4', 'foo@e6.example.com', 'mail.example.com', options)
|
38
|
-
expect(
|
38
|
+
expect([:permerror]).to include(result.code)
|
39
39
|
end
|
40
40
|
|
41
41
|
it 'include = "include" ":" domain-spec' do
|
42
42
|
result = Coppertone::SpfService.authenticate_email('1.2.3.4', 'foo@e9.example.com', 'mail.example.com', options)
|
43
|
-
expect(
|
43
|
+
expect([:permerror]).to include(result.code)
|
44
44
|
end
|
45
45
|
|
46
46
|
it 'recursive check_host() result of none causes include to permerror' do
|
47
47
|
result = Coppertone::SpfService.authenticate_email('1.2.3.4', 'foo@e7.example.com', 'mail.example.com', options)
|
48
|
-
expect(
|
48
|
+
expect([:permerror]).to include(result.code)
|
49
49
|
end
|
50
50
|
|
51
51
|
it 'domain-spec cannot be empty.' do
|
52
52
|
result = Coppertone::SpfService.authenticate_email('1.2.3.4', 'foo@e8.example.com', 'mail.example.com', options)
|
53
|
-
expect(
|
53
|
+
expect([:permerror]).to include(result.code)
|
54
54
|
end
|
55
55
|
|
56
56
|
end
|
@@ -1,77 +1,84 @@
|
|
1
|
+
# -- encoding : utf-8 --
|
2
|
+
|
1
3
|
require 'spec_helper'
|
2
4
|
|
3
5
|
describe 'Initial processing' do
|
4
6
|
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' }] }
|
7
|
+
{ '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' }], 'fine.example.com' => [{ 'TXT' => 'v=spf1 a -all' }] }
|
6
8
|
end
|
7
9
|
|
8
|
-
let(:dns_client) {
|
10
|
+
let(:dns_client) { DNSAdapter::MockClient.new(zonefile) }
|
9
11
|
let(:options) { { dns_client: dns_client } }
|
10
12
|
|
11
13
|
it 'DNS labels limited to 63 chars.' do
|
12
14
|
# For initial processing, a long label results in None, not TempError
|
13
15
|
result = Coppertone::SpfService.authenticate_email('1.2.3.5', 'lyme.eater@A123456789012345678901234567890123456789012345678901234567890123.example.com', 'mail.example.net', options)
|
14
|
-
expect(
|
16
|
+
expect([:none]).to include(result.code)
|
15
17
|
end
|
16
18
|
|
17
19
|
it 'DNS labels limited to 63 chars.' do
|
18
20
|
result = Coppertone::SpfService.authenticate_email('1.2.3.5', 'lyme.eater@A12345678901234567890123456789012345678901234567890123456789012.example.com', 'mail.example.net', options)
|
19
|
-
expect(
|
21
|
+
expect([:fail]).to include(result.code)
|
20
22
|
end
|
21
23
|
|
22
24
|
it 'emptylabel' do
|
23
25
|
result = Coppertone::SpfService.authenticate_email('1.2.3.5', 'lyme.eater@A...example.com', 'mail.example.net', options)
|
24
|
-
expect(
|
26
|
+
expect([:none]).to include(result.code)
|
25
27
|
end
|
26
28
|
|
27
29
|
it 'helo-not-fqdn' do
|
28
30
|
result = Coppertone::SpfService.authenticate_email('1.2.3.5', '', 'A2345678', options)
|
29
|
-
expect(
|
31
|
+
expect([:none]).to include(result.code)
|
30
32
|
end
|
31
33
|
|
32
34
|
it 'helo-domain-literal' do
|
33
35
|
result = Coppertone::SpfService.authenticate_email('1.2.3.5', '', '[1.2.3.5]', options)
|
34
|
-
expect(
|
36
|
+
expect([:none]).to include(result.code)
|
35
37
|
end
|
36
38
|
|
37
39
|
it 'nolocalpart' do
|
38
40
|
result = Coppertone::SpfService.authenticate_email('1.2.3.4', '@example.net', 'mail.example.net', options)
|
39
|
-
expect(
|
41
|
+
expect([:fail]).to include(result.code)
|
40
42
|
expect(result.explanation).to eq('postmaster')
|
41
43
|
end
|
42
44
|
|
43
45
|
it 'domain-literal' do
|
44
46
|
result = Coppertone::SpfService.authenticate_email('1.2.3.5', 'foo@[1.2.3.5]', 'OEMCOMPUTER', options)
|
45
|
-
expect(
|
47
|
+
expect([:none]).to include(result.code)
|
46
48
|
end
|
47
49
|
|
48
50
|
it 'SPF policies are restricted to 7-bit ascii.' do
|
49
51
|
result = Coppertone::SpfService.authenticate_email('1.2.3.4', 'foobar@hosed.example.com', 'hosed', options)
|
50
|
-
expect(
|
52
|
+
expect([:permerror]).to include(result.code)
|
51
53
|
end
|
52
54
|
|
53
55
|
it 'SPF policies are restricted to 7-bit ascii.' do
|
54
56
|
# Checking a possibly different code path for non-ascii chars.
|
55
57
|
result = Coppertone::SpfService.authenticate_email('1.2.3.4', 'foobar@hosed2.example.com', 'hosed', options)
|
56
|
-
expect(
|
58
|
+
expect([:permerror]).to include(result.code)
|
57
59
|
end
|
58
60
|
|
59
61
|
it 'SPF policies are restricted to 7-bit ascii.' do
|
60
62
|
# Checking yet another code path for non-ascii chars.
|
61
63
|
result = Coppertone::SpfService.authenticate_email('1.2.3.4', 'foobar@hosed3.example.com', 'hosed', options)
|
62
|
-
expect(
|
64
|
+
expect([:permerror]).to include(result.code)
|
63
65
|
end
|
64
66
|
|
65
67
|
it 'Non-ascii content in non-SPF related records.' do
|
66
68
|
# Non-SPF related TXT records are none of our business.
|
67
69
|
result = Coppertone::SpfService.authenticate_email('1.2.3.4', 'foobar@nothosed.example.com', 'hosed', options)
|
68
|
-
expect(
|
70
|
+
expect([:fail]).to include(result.code)
|
69
71
|
expect(result.explanation).to eq('DEFAULT')
|
70
72
|
end
|
71
73
|
|
72
74
|
it 'Mechanisms are separated by spaces only, not any control char.' do
|
73
75
|
result = Coppertone::SpfService.authenticate_email('192.0.2.3', 'foobar@ctrl.example.com', 'hosed', options)
|
74
|
-
expect(
|
76
|
+
expect([:permerror]).to include(result.code)
|
77
|
+
end
|
78
|
+
|
79
|
+
it 'ABNF for term separation is one or more spaces, not just one.' do
|
80
|
+
result = Coppertone::SpfService.authenticate_email('1.2.3.4', 'actually@fine.example.com', 'hosed', options)
|
81
|
+
expect([:fail]).to include(result.code)
|
75
82
|
end
|
76
83
|
|
77
84
|
end
|
@@ -5,115 +5,115 @@ describe 'MX mechanism syntax' do
|
|
5
5
|
{ 'mail.example.com' => [{ 'A' => '1.2.3.4' }, { 'MX' => [0, ''] }, { 'TXT' => 'v=spf1 mx' }], 'e1.example.com' => [{ 'TXT' => 'v=spf1 mx/0 -all' }, { 'MX' => [0, 'e1.example.com'] }], 'e2.example.com' => [{ 'A' => '1.1.1.1' }, { 'AAAA' => '1234::2' }, { 'MX' => [0, 'e2.example.com'] }, { 'TXT' => 'v=spf1 mx/0 -all' }], 'e2a.example.com' => [{ 'AAAA' => '1234::1' }, { 'MX' => [0, 'e2a.example.com'] }, { 'TXT' => 'v=spf1 mx//0 -all' }], 'e2b.example.com' => [{ 'A' => '1.1.1.1' }, { 'MX' => [0, 'e2b.example.com'] }, { 'TXT' => 'v=spf1 mx//0 -all' }], 'e3.example.com' => [{ 'TXT' => "v=spf1 mx:foo.example.com\u0000" }], 'e4.example.com' => [{ 'TXT' => 'v=spf1 mx' }, { 'A' => '1.2.3.4' }], 'e5.example.com' => [{ 'TXT' => 'v=spf1 mx:abc.123' }], 'e6.example.com' => [{ 'TXT' => 'v=spf1 mx//33 -all' }], 'e6a.example.com' => [{ 'TXT' => 'v=spf1 mx/33 -all' }], 'e7.example.com' => [{ 'TXT' => 'v=spf1 mx//129 -all' }], 'e9.example.com' => [{ 'TXT' => 'v=spf1 mx:example.com:8080' }], 'e10.example.com' => [{ 'TXT' => 'v=spf1 mx:foo.example.com/24' }], 'foo.example.com' => [{ 'MX' => [0, 'foo1.example.com'] }], 'foo1.example.com' => [{ 'A' => '1.1.1.1' }, { 'A' => '1.2.3.5' }], 'e11.example.com' => [{ 'TXT' => 'v=spf1 mx:foo:bar/baz.example.com' }], 'foo:bar/baz.example.com' => [{ 'MX' => [0, 'foo:bar/baz.example.com'] }, { 'A' => '1.2.3.4' }], 'e12.example.com' => [{ 'TXT' => 'v=spf1 mx:example.-com' }], 'e13.example.com' => [{ 'TXT' => 'v=spf1 mx: -all' }] }
|
6
6
|
end
|
7
7
|
|
8
|
-
let(:dns_client) {
|
8
|
+
let(:dns_client) { DNSAdapter::MockClient.new(zonefile) }
|
9
9
|
let(:options) { { dns_client: dns_client } }
|
10
10
|
|
11
11
|
it 'MX = "mx" [ ":" domain-spec ] [ dual-cidr-length ] dual-cidr-length = [ ip4-cidr-length ] [ "/" ip6-cidr-length ] ' do
|
12
12
|
result = Coppertone::SpfService.authenticate_email('1.2.3.4', 'foo@e6.example.com', 'mail.example.com', options)
|
13
|
-
expect(
|
13
|
+
expect([:fail]).to include(result.code)
|
14
14
|
end
|
15
15
|
|
16
16
|
it 'MX = "mx" [ ":" domain-spec ] [ dual-cidr-length ] dual-cidr-length = [ ip4-cidr-length ] [ "/" ip6-cidr-length ] ' do
|
17
17
|
result = Coppertone::SpfService.authenticate_email('1.2.3.4', 'foo@e6a.example.com', 'mail.example.com', options)
|
18
|
-
expect(
|
18
|
+
expect([:permerror]).to include(result.code)
|
19
19
|
end
|
20
20
|
|
21
21
|
it 'MX = "mx" [ ":" domain-spec ] [ dual-cidr-length ] dual-cidr-length = [ ip4-cidr-length ] [ "/" ip6-cidr-length ] ' do
|
22
22
|
result = Coppertone::SpfService.authenticate_email('1.2.3.4', 'foo@e7.example.com', 'mail.example.com', options)
|
23
|
-
expect(
|
23
|
+
expect([:permerror]).to include(result.code)
|
24
24
|
end
|
25
25
|
|
26
26
|
it 'MX matches any returned IP.' do
|
27
27
|
result = Coppertone::SpfService.authenticate_email('1.2.3.4', 'foo@e10.example.com', 'mail.example.com', options)
|
28
|
-
expect(
|
28
|
+
expect([:pass]).to include(result.code)
|
29
29
|
end
|
30
30
|
|
31
31
|
it 'MX matches any returned IP.' do
|
32
32
|
result = Coppertone::SpfService.authenticate_email('1.2.3.4', 'foo@e10.example.com', 'mail.example.com', options)
|
33
|
-
expect(
|
33
|
+
expect([:pass]).to include(result.code)
|
34
34
|
end
|
35
35
|
|
36
36
|
it 'domain-spec must pass basic syntax checks' do
|
37
37
|
# A \':\' may appear in domain-spec, but not in top-label.
|
38
38
|
result = Coppertone::SpfService.authenticate_email('1.2.3.4', 'foo@e9.example.com', 'mail.example.com', options)
|
39
|
-
expect(
|
39
|
+
expect([:permerror]).to include(result.code)
|
40
40
|
end
|
41
41
|
|
42
42
|
it 'If no ips are returned, MX mechanism does not match, even with /0.' do
|
43
43
|
result = Coppertone::SpfService.authenticate_email('1.2.3.4', 'foo@e1.example.com', 'mail.example.com', options)
|
44
|
-
expect(
|
44
|
+
expect([:fail]).to include(result.code)
|
45
45
|
end
|
46
46
|
|
47
47
|
it 'Matches if any A records for any MX records are present in DNS.' do
|
48
48
|
result = Coppertone::SpfService.authenticate_email('1.2.3.4', 'foo@e2.example.com', 'mail.example.com', options)
|
49
|
-
expect(
|
49
|
+
expect([:pass]).to include(result.code)
|
50
50
|
end
|
51
51
|
|
52
52
|
it 'cidr4 doesnt apply to IP6 connections.' do
|
53
53
|
# The IP6 CIDR starts with a double slash.
|
54
54
|
result = Coppertone::SpfService.authenticate_email('1234::1', 'foo@e2.example.com', 'mail.example.com', options)
|
55
|
-
expect(
|
55
|
+
expect([:fail]).to include(result.code)
|
56
56
|
end
|
57
57
|
|
58
58
|
it 'Would match if any AAAA records for MX records are present in DNS, but not for an IP4 connection.' do
|
59
59
|
result = Coppertone::SpfService.authenticate_email('1.2.3.4', 'foo@e2a.example.com', 'mail.example.com', options)
|
60
|
-
expect(
|
60
|
+
expect([:fail]).to include(result.code)
|
61
61
|
end
|
62
62
|
|
63
63
|
it 'Would match if any AAAA records for MX records are present in DNS, but not for an IP4 connection.' do
|
64
64
|
result = Coppertone::SpfService.authenticate_email('::FFFF:1.2.3.4', 'foo@e2a.example.com', 'mail.example.com', options)
|
65
|
-
expect(
|
65
|
+
expect([:fail]).to include(result.code)
|
66
66
|
end
|
67
67
|
|
68
68
|
it 'Matches if any AAAA records for any MX records are present in DNS.' do
|
69
69
|
result = Coppertone::SpfService.authenticate_email('1234::1', 'foo@e2a.example.com', 'mail.example.com', options)
|
70
|
-
expect(
|
70
|
+
expect([:pass]).to include(result.code)
|
71
71
|
end
|
72
72
|
|
73
73
|
it 'No match if no AAAA records for any MX records are present in DNS.' do
|
74
74
|
result = Coppertone::SpfService.authenticate_email('1234::1', 'foo@e2b.example.com', 'mail.example.com', options)
|
75
|
-
expect(
|
75
|
+
expect([:fail]).to include(result.code)
|
76
76
|
end
|
77
77
|
|
78
78
|
it 'Null not allowed in top-label.' do
|
79
79
|
result = Coppertone::SpfService.authenticate_email('1.2.3.5', 'foo@e3.example.com', 'mail.example.com', options)
|
80
|
-
expect(
|
80
|
+
expect([:permerror]).to include(result.code)
|
81
81
|
end
|
82
82
|
|
83
83
|
it 'Top-label may not be all numeric' do
|
84
84
|
result = Coppertone::SpfService.authenticate_email('1.2.3.4', 'foo@e5.example.com', 'mail.example.com', options)
|
85
|
-
expect(
|
85
|
+
expect([:permerror]).to include(result.code)
|
86
86
|
end
|
87
87
|
|
88
88
|
it 'Domain-spec may contain any visible char except %' do
|
89
89
|
result = Coppertone::SpfService.authenticate_email('1.2.3.4', 'foo@e11.example.com', 'mail.example.com', options)
|
90
|
-
expect(
|
90
|
+
expect([:pass]).to include(result.code)
|
91
91
|
end
|
92
92
|
|
93
93
|
it 'Domain-spec may contain any visible char except %' do
|
94
94
|
result = Coppertone::SpfService.authenticate_email('::FFFF:1.2.3.4', 'foo@e11.example.com', 'mail.example.com', options)
|
95
|
-
expect(
|
95
|
+
expect([:pass]).to include(result.code)
|
96
96
|
end
|
97
97
|
|
98
98
|
it 'Toplabel may not begin with -' do
|
99
99
|
result = Coppertone::SpfService.authenticate_email('1.2.3.4', 'foo@e12.example.com', 'mail.example.com', options)
|
100
|
-
expect(
|
100
|
+
expect([:permerror]).to include(result.code)
|
101
101
|
end
|
102
102
|
|
103
103
|
it 'test null MX' do
|
104
104
|
# Some implementations have had trouble with null MX
|
105
105
|
result = Coppertone::SpfService.authenticate_email('1.2.3.4', '', 'mail.example.com', options)
|
106
|
-
expect(
|
106
|
+
expect([:neutral]).to include(result.code)
|
107
107
|
end
|
108
108
|
|
109
109
|
it 'If the target name has no MX records, check_host() MUST NOT pretend the target is its single MX, and MUST NOT default to an A lookup on the target-name directly.' do
|
110
110
|
result = Coppertone::SpfService.authenticate_email('1.2.3.4', 'foo@e4.example.com', 'mail.example.com', options)
|
111
|
-
expect(
|
111
|
+
expect([:neutral]).to include(result.code)
|
112
112
|
end
|
113
113
|
|
114
114
|
it 'domain-spec cannot be empty.' do
|
115
115
|
result = Coppertone::SpfService.authenticate_email('1.2.3.4', 'foo@e13.example.com', 'mail.example.com', options)
|
116
|
-
expect(
|
116
|
+
expect([:permerror]).to include(result.code)
|
117
117
|
end
|
118
118
|
|
119
119
|
end
|
@@ -5,150 +5,146 @@ describe 'Macro expansion rules' do
|
|
5
5
|
{ 'example.com.d.spf.example.com' => [{ 'TXT' => 'v=spf1 redirect=a.spf.example.com' }], 'a.spf.example.com' => [{ 'TXT' => 'v=spf1 include:o.spf.example.com. ~all' }], 'o.spf.example.com' => [{ 'TXT' => 'v=spf1 ip4:192.168.218.40' }], 'msgbas2x.cos.example.com' => [{ 'A' => '192.168.218.40' }], 'example.com' => [{ 'A' => '192.168.90.76' }, { 'TXT' => 'v=spf1 redirect=%{d}.d.spf.example.com.' }], 'exp.example.com' => [{ 'TXT' => 'v=spf1 exp=msg.example.com. -all' }], 'msg.example.com' => [{ 'TXT' => 'This is a test.' }], 'e1.example.com' => [{ 'TXT' => 'v=spf1 -exists:%(ir).sbl.example.com ?all' }], 'e1e.example.com' => [{ 'TXT' => 'v=spf1 exists:foo%(ir).sbl.example.com ?all' }], 'e1t.example.com' => [{ 'TXT' => 'v=spf1 exists:foo%.sbl.example.com ?all' }], 'e1a.example.com' => [{ 'TXT' => 'v=spf1 a:macro%%percent%_%_space%-url-space.example.com -all' }], 'macro%percent space%20url-space.example.com' => [{ 'A' => '1.2.3.4' }], 'e2.example.com' => [{ 'TXT' => 'v=spf1 -all exp=%{r}.example.com' }], 'e3.example.com' => [{ 'TXT' => 'v=spf1 -all exp=%{ir}.example.com' }], '40.218.168.192.example.com' => [{ 'TXT' => 'Connections from %{c} not authorized.' }], 'somewhat.long.exp.example.com' => [{ 'TXT' => 'v=spf1 -all exp=foobar.%{o}.%{o}.%{o}.%{o}.%{o}.%{o}.%{o}.%{o}.example.com' }], 'somewhat.long.exp.example.com.somewhat.long.exp.example.com.somewhat.long.exp.example.com.somewhat.long.exp.example.com.somewhat.long.exp.example.com.somewhat.long.exp.example.com.somewhat.long.exp.example.com.somewhat.long.exp.example.com.example.com' => [{ 'TXT' => 'Congratulations! That was tricky.' }], 'e4.example.com' => [{ 'TXT' => 'v=spf1 -all exp=e4msg.example.com' }], 'e4msg.example.com' => [{ 'TXT' => '%{c} is queried as %{ir}.%{v}.arpa' }], 'e5.example.com' => [{ 'TXT' => 'v=spf1 a:%{a}.example.com -all' }], 'e6.example.com' => [{ 'TXT' => 'v=spf1 -all exp=e6msg.example.com' }], 'e6msg.example.com' => [{ 'TXT' => 'connect from %{p}' }], 'mx.example.com' => [{ 'A' => '192.168.218.41' }, { 'A' => '192.168.218.42' }, { 'AAAA' => 'CAFE:BABE::2' }, { 'AAAA' => 'CAFE:BABE::3' }], '40.218.168.192.in-addr.arpa' => [{ 'PTR' => 'mx.example.com' }], '41.218.168.192.in-addr.arpa' => [{ 'PTR' => 'mx.example.com' }], '42.218.168.192.in-addr.arpa' => [{ 'PTR' => 'mx.example.com' }, { 'PTR' => 'mx.e7.example.com' }], '1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.E.B.A.B.E.F.A.C.ip6.arpa' => [{ 'PTR' => 'mx.example.com' }], '3.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.E.B.A.B.E.F.A.C.ip6.arpa' => [{ 'PTR' => 'mx.example.com' }], 'mx.e7.example.com' => [{ 'A' => '192.168.218.42' }], 'mx.e7.example.com.should.example.com' => [{ 'A' => '127.0.0.2' }], 'mx.example.com.ok.example.com' => [{ 'A' => '127.0.0.2' }], 'e7.example.com' => [{ 'TXT' => 'v=spf1 exists:%{p}.should.example.com ~exists:%{p}.ok.example.com' }], 'e8.example.com' => [{ 'TXT' => 'v=spf1 -all exp=msg8.%{D2}' }], 'msg8.example.com' => [{ 'TXT' => 'http://example.com/why.html?l=%{L}' }], 'e9.example.com' => [{ 'TXT' => 'v=spf1 a:%{H} -all' }], 'e10.example.com' => [{ 'TXT' => 'v=spf1 -include:_spfh.%{d2} ip4:1.2.3.0/24 -all' }], '_spfh.example.com' => [{ 'TXT' => 'v=spf1 -a:%{h} +all' }], 'e11.example.com' => [{ 'TXT' => 'v=spf1 exists:%{i}.%{l2r-}.user.%{d2}' }], '1.2.3.4.gladstone.philip.user.example.com' => [{ 'A' => '127.0.0.2' }], 'e12.example.com' => [{ 'TXT' => 'v=spf1 exists:%{l2r+-}.user.%{d2}' }], 'bar.foo.user.example.com' => [{ 'A' => '127.0.0.2' }] }
|
6
6
|
end
|
7
7
|
|
8
|
-
let(:dns_client) {
|
8
|
+
let(:dns_client) { DNSAdapter::MockClient.new(zonefile) }
|
9
9
|
let(:options) { { dns_client: dns_client } }
|
10
10
|
|
11
11
|
it 'trailing dot is ignored for domains' do
|
12
12
|
result = Coppertone::SpfService.authenticate_email('192.168.218.40', 'test@example.com', 'msgbas2x.cos.example.com', options)
|
13
|
-
expect(
|
13
|
+
expect([:pass]).to include(result.code)
|
14
14
|
end
|
15
15
|
|
16
16
|
it 'trailing dot is not removed from explanation' do
|
17
17
|
# A simple way for an implementation to ignore trailing dots on domains is to remove it when present. But be careful not to remove it for explanation text.
|
18
18
|
result = Coppertone::SpfService.authenticate_email('192.168.218.40', 'test@exp.example.com', 'msgbas2x.cos.example.com', options)
|
19
|
-
expect(
|
19
|
+
expect([:fail]).to include(result.code)
|
20
20
|
expect(result.explanation).to eq('This is a test.')
|
21
21
|
end
|
22
22
|
|
23
23
|
it 'The following macro letters are allowed only in "exp" text: c, r, t' do
|
24
24
|
result = Coppertone::SpfService.authenticate_email('192.168.218.40', 'test@e2.example.com', 'msgbas2x.cos.example.com', options)
|
25
|
-
expect(
|
25
|
+
expect([:permerror]).to include(result.code)
|
26
26
|
end
|
27
27
|
|
28
28
|
it 'A % character not followed by a {, %, -, or _ character is a syntax error.' do
|
29
29
|
result = Coppertone::SpfService.authenticate_email('192.168.218.40', 'test@e1.example.com', 'msgbas2x.cos.example.com', options)
|
30
|
-
expect(
|
30
|
+
expect([:permerror]).to include(result.code)
|
31
31
|
end
|
32
32
|
|
33
33
|
it 'A % character not followed by a {, %, -, or _ character is a syntax error.' do
|
34
34
|
result = Coppertone::SpfService.authenticate_email('192.168.218.40', 'test@e1e.example.com', 'msgbas2x.cos.example.com', options)
|
35
|
-
expect(
|
35
|
+
expect([:permerror]).to include(result.code)
|
36
36
|
end
|
37
37
|
|
38
38
|
it 'A % character not followed by a {, %, -, or _ character is a syntax error.' do
|
39
39
|
result = Coppertone::SpfService.authenticate_email('192.168.218.40', 'test@e1t.example.com', 'msgbas2x.cos.example.com', options)
|
40
|
-
expect(
|
40
|
+
expect([:permerror]).to include(result.code)
|
41
41
|
end
|
42
42
|
|
43
43
|
it 'macro-encoded percents (%%), spaces (%_), and URL-percent-encoded spaces (%-)' do
|
44
44
|
result = Coppertone::SpfService.authenticate_email('1.2.3.4', 'test@e1a.example.com', 'mail.example.com', options)
|
45
|
-
expect(
|
45
|
+
expect([:pass]).to include(result.code)
|
46
46
|
end
|
47
47
|
|
48
48
|
it 'For IPv4 addresses, both the "i" and "c" macros expand to the standard dotted-quad format.' do
|
49
49
|
result = Coppertone::SpfService.authenticate_email('192.168.218.40', 'test@e3.example.com', 'msgbas2x.cos.example.com', options)
|
50
|
-
expect(
|
50
|
+
expect([:fail]).to include(result.code)
|
51
51
|
expect(result.explanation).to eq('Connections from 192.168.218.40 not authorized.')
|
52
52
|
end
|
53
53
|
|
54
54
|
it 'When the result of macro expansion is used in a domain name query, if the expanded domain name exceeds 253 characters, the left side is truncated to fit, by removing successive domain labels until the total length does not exceed 253 characters.' do
|
55
55
|
result = Coppertone::SpfService.authenticate_email('192.168.218.40', 'test@somewhat.long.exp.example.com', 'msgbas2x.cos.example.com', options)
|
56
|
-
expect(
|
56
|
+
expect([:fail]).to include(result.code)
|
57
57
|
expect(result.explanation).to eq('Congratulations! That was tricky.')
|
58
58
|
end
|
59
59
|
|
60
60
|
it 'v = the string "in-addr" if <ip> is ipv4, or "ip6" if <ip> is ipv6' do
|
61
61
|
result = Coppertone::SpfService.authenticate_email('192.168.218.40', 'test@e4.example.com', 'msgbas2x.cos.example.com', options)
|
62
|
-
expect(
|
62
|
+
expect([:fail]).to include(result.code)
|
63
63
|
expect(result.explanation).to eq('192.168.218.40 is queried as 40.218.168.192.in-addr.arpa')
|
64
64
|
end
|
65
65
|
|
66
66
|
it 'v = the string "in-addr" if <ip> is ipv4, or "ip6" if <ip> is ipv6' do
|
67
67
|
result = Coppertone::SpfService.authenticate_email('CAFE:BABE::1', 'test@e4.example.com', 'msgbas2x.cos.example.com', options)
|
68
|
-
expect(
|
68
|
+
expect([:fail]).to include(result.code)
|
69
69
|
expect(result.explanation).to eq('cafe:babe::1 is queried as 1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.E.B.A.B.E.F.A.C.ip6.arpa')
|
70
70
|
end
|
71
71
|
|
72
72
|
it 'Allowed macros chars are slodipvh plus crt in explanation.' do
|
73
73
|
result = Coppertone::SpfService.authenticate_email('CAFE:BABE::192.168.218.40', 'test@e5.example.com', 'msgbas2x.cos.example.com', options)
|
74
|
-
expect(
|
74
|
+
expect([:permerror]).to include(result.code)
|
75
75
|
end
|
76
76
|
|
77
77
|
it 'p = the validated domain name of <ip>' do
|
78
78
|
# The PTR in this example does not validate.
|
79
79
|
result = Coppertone::SpfService.authenticate_email('192.168.218.40', 'test@e6.example.com', 'msgbas2x.cos.example.com', options)
|
80
|
-
expect(
|
80
|
+
expect([:fail]).to include(result.code)
|
81
81
|
expect(result.explanation).to eq('connect from unknown')
|
82
82
|
end
|
83
83
|
|
84
84
|
it 'p = the validated domain name of <ip>' do
|
85
|
-
pending 'It is currently unclear, based on RFC 7208, why this spec should pass.'
|
86
85
|
# If a subdomain of the <domain> is present, it SHOULD be used.
|
87
|
-
result = Coppertone::SpfService.authenticate_email('192.168.218.41',
|
88
|
-
|
89
|
-
'msgbas2x.cos.example.com', options)
|
90
|
-
expect(%i(fail)).to include(result.code)
|
86
|
+
result = Coppertone::SpfService.authenticate_email('192.168.218.41', 'test@e6.example.com', 'msgbas2x.cos.example.com', options)
|
87
|
+
expect([:fail]).to include(result.code)
|
91
88
|
expect(result.explanation).to eq('connect from mx.example.com')
|
92
89
|
end
|
93
90
|
|
94
91
|
it 'p = the validated domain name of <ip>' do
|
95
92
|
# The PTR in this example does not validate.
|
96
93
|
result = Coppertone::SpfService.authenticate_email('CAFE:BABE::1', 'test@e6.example.com', 'msgbas2x.cos.example.com', options)
|
97
|
-
expect(
|
94
|
+
expect([:fail]).to include(result.code)
|
98
95
|
expect(result.explanation).to eq('connect from unknown')
|
99
96
|
end
|
100
97
|
|
101
98
|
it 'p = the validated domain name of <ip>' do
|
102
|
-
pending 'It is currently unclear, based on RFC 7208, why this spec should pass.'
|
103
99
|
# If a subdomain of the <domain> is present, it SHOULD be used.
|
104
100
|
result = Coppertone::SpfService.authenticate_email('CAFE:BABE::3', 'test@e6.example.com', 'msgbas2x.cos.example.com', options)
|
105
|
-
expect(
|
101
|
+
expect([:fail]).to include(result.code)
|
106
102
|
expect(result.explanation).to eq('connect from mx.example.com')
|
107
103
|
end
|
108
104
|
|
109
105
|
it 'p = the validated domain name of <ip>' do
|
110
106
|
# If a subdomain of the <domain> is present, it SHOULD be used.
|
111
107
|
result = Coppertone::SpfService.authenticate_email('192.168.218.42', 'test@e7.example.com', 'msgbas2x.cos.example.com', options)
|
112
|
-
expect(
|
108
|
+
expect([:pass, :softfail]).to include(result.code)
|
113
109
|
end
|
114
110
|
|
115
111
|
it 'Uppercased macros expand exactly as their lowercased equivalents, and are then URL escaped. All chars not in the unreserved set MUST be escaped.' do
|
116
112
|
# unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
|
117
113
|
result = Coppertone::SpfService.authenticate_email('192.168.218.42', '~jack&jill=up-a_b3.c@e8.example.com', 'msgbas2x.cos.example.com', options)
|
118
|
-
expect(
|
114
|
+
expect([:fail]).to include(result.code)
|
119
115
|
expect(result.explanation).to eq('http://example.com/why.html?l=~jack%26jill%3Dup-a_b3.c')
|
120
116
|
end
|
121
117
|
|
122
118
|
it 'h = HELO/EHLO domain' do
|
123
119
|
result = Coppertone::SpfService.authenticate_email('192.168.218.40', 'test@e9.example.com', 'msgbas2x.cos.example.com', options)
|
124
|
-
expect(
|
120
|
+
expect([:pass]).to include(result.code)
|
125
121
|
end
|
126
122
|
|
127
123
|
it 'h = HELO/EHLO domain, but HELO is invalid' do
|
128
124
|
# Domain-spec must end in either a macro, or a valid toplabel. It is not correct to check syntax after macro expansion.
|
129
125
|
result = Coppertone::SpfService.authenticate_email('192.168.218.40', 'test@e9.example.com', 'JUMPIN\' JUPITER', options)
|
130
|
-
expect(
|
126
|
+
expect([:fail]).to include(result.code)
|
131
127
|
end
|
132
128
|
|
133
129
|
it 'h = HELO/EHLO domain, but HELO is a domain literal' do
|
134
130
|
# Domain-spec must end in either a macro, or a valid toplabel. It is not correct to check syntax after macro expansion.
|
135
131
|
result = Coppertone::SpfService.authenticate_email('192.168.218.40', 'test@e9.example.com', '[192.168.218.40]', options)
|
136
|
-
expect(
|
132
|
+
expect([:fail]).to include(result.code)
|
137
133
|
end
|
138
134
|
|
139
135
|
it 'Example of requiring valid helo in sender policy. This is a complex policy testing several points at once.' do
|
140
136
|
result = Coppertone::SpfService.authenticate_email('1.2.3.4', 'test@e10.example.com', 'OEMCOMPUTER', options)
|
141
|
-
expect(
|
137
|
+
expect([:fail]).to include(result.code)
|
142
138
|
end
|
143
139
|
|
144
140
|
it 'Macro value transformation (splitting on arbitrary characters, reversal, number of right-hand parts to use)' do
|
145
141
|
result = Coppertone::SpfService.authenticate_email('1.2.3.4', 'philip-gladstone-test@e11.example.com', 'mail.example.com', options)
|
146
|
-
expect(
|
142
|
+
expect([:pass]).to include(result.code)
|
147
143
|
end
|
148
144
|
|
149
145
|
it 'Multiple delimiters may be specified in a macro expression. macro-expand = ( "%{" macro-letter transformers *delimiter "}" ) / "%%" / "%_" / "%-"' do
|
150
146
|
result = Coppertone::SpfService.authenticate_email('1.2.3.4', 'foo-bar+zip+quux@e12.example.com', 'mail.example.com', options)
|
151
|
-
expect(
|
147
|
+
expect([:pass]).to include(result.code)
|
152
148
|
end
|
153
149
|
|
154
150
|
end
|
@@ -5,38 +5,38 @@ describe 'PTR mechanism syntax' do
|
|
5
5
|
{ 'mail.example.com' => [{ 'A' => '1.2.3.4' }], 'e1.example.com' => [{ 'TXT' => 'v=spf1 ptr/0 -all' }], 'e2.example.com' => [{ 'TXT' => 'v=spf1 ptr:example.com -all' }], '4.3.2.1.in-addr.arpa' => [{ 'PTR' => 'e3.example.com' }, { 'PTR' => 'e4.example.com' }, { 'PTR' => 'mail.example.com' }], '1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.E.B.A.B.E.F.A.C.ip6.arpa' => [{ 'PTR' => 'e3.example.com' }], 'e3.example.com' => [{ 'TXT' => 'v=spf1 ptr -all' }, { 'A' => '1.2.3.4' }, { 'AAAA' => 'CAFE:BABE::1' }], 'e4.example.com' => [{ 'TXT' => 'v=spf1 ptr -all' }], 'e5.example.com' => [{ 'TXT' => 'v=spf1 ptr:' }] }
|
6
6
|
end
|
7
7
|
|
8
|
-
let(:dns_client) {
|
8
|
+
let(:dns_client) { DNSAdapter::MockClient.new(zonefile) }
|
9
9
|
let(:options) { { dns_client: dns_client } }
|
10
10
|
|
11
11
|
it 'PTR = "ptr" [ ":" domain-spec ]' do
|
12
12
|
result = Coppertone::SpfService.authenticate_email('1.2.3.4', 'foo@e1.example.com', 'mail.example.com', options)
|
13
|
-
expect(
|
13
|
+
expect([:permerror]).to include(result.code)
|
14
14
|
end
|
15
15
|
|
16
16
|
it 'Check all validated domain names to see if they end in the <target-name> domain.' do
|
17
17
|
result = Coppertone::SpfService.authenticate_email('1.2.3.4', 'foo@e2.example.com', 'mail.example.com', options)
|
18
|
-
expect(
|
18
|
+
expect([:pass]).to include(result.code)
|
19
19
|
end
|
20
20
|
|
21
21
|
it 'Check all validated domain names to see if they end in the <target-name> domain.' do
|
22
22
|
result = Coppertone::SpfService.authenticate_email('1.2.3.4', 'foo@e3.example.com', 'mail.example.com', options)
|
23
|
-
expect(
|
23
|
+
expect([:pass]).to include(result.code)
|
24
24
|
end
|
25
25
|
|
26
26
|
it 'Check all validated domain names to see if they end in the <target-name> domain.' do
|
27
27
|
# This PTR record does not validate
|
28
28
|
result = Coppertone::SpfService.authenticate_email('1.2.3.4', 'foo@e4.example.com', 'mail.example.com', options)
|
29
|
-
expect(
|
29
|
+
expect([:fail]).to include(result.code)
|
30
30
|
end
|
31
31
|
|
32
32
|
it 'Check all validated domain names to see if they end in the <target-name> domain.' do
|
33
33
|
result = Coppertone::SpfService.authenticate_email('CAFE:BABE::1', 'foo@e3.example.com', 'mail.example.com', options)
|
34
|
-
expect(
|
34
|
+
expect([:pass]).to include(result.code)
|
35
35
|
end
|
36
36
|
|
37
37
|
it 'domain-spec cannot be empty.' do
|
38
38
|
result = Coppertone::SpfService.authenticate_email('1.2.3.4', 'foo@e5.example.com', 'mail.example.com', options)
|
39
|
-
expect(
|
39
|
+
expect([:permerror]).to include(result.code)
|
40
40
|
end
|
41
41
|
|
42
42
|
end
|