line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
1
|
|
|
|
|
|
|
package Mail::BIMI::Record; |
2
|
|
|
|
|
|
|
# ABSTRACT: Class to model a BIMI record |
3
|
|
|
|
|
|
|
our $VERSION = '3.20210512'; # VERSION |
4
|
29
|
|
|
29
|
|
506
|
use 5.20.0; |
|
29
|
|
|
|
|
109
|
|
5
|
29
|
|
|
29
|
|
174
|
use Moose; |
|
29
|
|
|
|
|
60
|
|
|
29
|
|
|
|
|
230
|
|
6
|
29
|
|
|
29
|
|
199647
|
use Mail::BIMI::Prelude; |
|
29
|
|
|
|
|
72
|
|
|
29
|
|
|
|
|
259
|
|
7
|
29
|
|
|
29
|
|
30462
|
use Term::ANSIColor qw{ :constants }; |
|
29
|
|
|
|
|
247114
|
|
|
29
|
|
|
|
|
34563
|
|
8
|
29
|
|
|
29
|
|
17639
|
use Mail::BIMI::Record::Authority; |
|
29
|
|
|
|
|
120
|
|
|
29
|
|
|
|
|
3447
|
|
9
|
29
|
|
|
29
|
|
20206
|
use Mail::BIMI::Record::Location; |
|
29
|
|
|
|
|
122
|
|
|
29
|
|
|
|
|
1526
|
|
10
|
29
|
|
|
29
|
|
22049
|
use Mail::DMARC::PurePerl; |
|
29
|
|
|
|
|
9714304
|
|
|
29
|
|
|
|
|
85646
|
|
11
|
|
|
|
|
|
|
|
12
|
|
|
|
|
|
|
extends 'Mail::BIMI::Base'; |
13
|
|
|
|
|
|
|
with( |
14
|
|
|
|
|
|
|
'Mail::BIMI::Role::HasError', |
15
|
|
|
|
|
|
|
'Mail::BIMI::Role::Cacheable', |
16
|
|
|
|
|
|
|
); |
17
|
|
|
|
|
|
|
has domain => ( is => 'rw', isa => 'Str', required => 1, traits => ['CacheKey'], |
18
|
|
|
|
|
|
|
documentation => 'inputs: Domain the for the record', ); |
19
|
|
|
|
|
|
|
has retrieved_domain => ( is => 'rw', isa => 'Str', traits => ['Cacheable'], |
20
|
|
|
|
|
|
|
documentation => 'Domain the record was retrieved from', ); |
21
|
|
|
|
|
|
|
has retrieved_record => ( is => 'rw', traits => ['Cacheable'], |
22
|
|
|
|
|
|
|
documentation => 'Record as retrieved' ); |
23
|
|
|
|
|
|
|
has retrieved_selector => ( is => 'rw', isa => 'Str', traits => ['Cacheable'], |
24
|
|
|
|
|
|
|
documentation => 'Selector the record was retrieved from', ); |
25
|
|
|
|
|
|
|
has selector => ( is => 'rw', isa => 'Str', traits => ['CacheKey'], |
26
|
|
|
|
|
|
|
documentation => 'inputs: Selector used to retrieve the record; will become default if fallback was used', ); |
27
|
|
|
|
|
|
|
has version => ( is => 'rw', isa => 'Maybe[Str]', lazy => 1, builder => '_build_version', traits => ['Cacheable'], |
28
|
|
|
|
|
|
|
documentation => 'BIMI Version tag' ); |
29
|
|
|
|
|
|
|
has authority => ( is => 'rw', isa => 'Mail::BIMI::Record::Authority', lazy => 1, builder => '_build_authority', |
30
|
|
|
|
|
|
|
documentation => 'Mail::BIMI::Record::Authority object for this record' ); |
31
|
|
|
|
|
|
|
has location => ( is => 'rw', isa => 'Mail::BIMI::Record::Location', lazy => 1, builder => '_build_location', |
32
|
|
|
|
|
|
|
documentation => 'Mail::BIMI::Record::Location object for this record' ); |
33
|
|
|
|
|
|
|
has record_hashref => ( is => 'rw', isa => 'HashRef', lazy => 1, builder => '_build_record_hashref', traits => ['Cacheable'], |
34
|
|
|
|
|
|
|
documentation => 'Hashref of record values' ); |
35
|
|
|
|
|
|
|
has is_valid => ( is => 'rw', lazy => 1, builder => '_build_is_valid', traits => ['Cacheable'], |
36
|
|
|
|
|
|
|
documentation => 'Is this record valid' ); |
37
|
|
|
|
|
|
|
|
38
|
|
|
|
|
|
|
|
39
|
|
|
|
|
|
|
|
40
|
6
|
|
|
6
|
1
|
15
|
sub cache_valid_for($self) { return 3600 } |
|
6
|
|
|
|
|
14
|
|
|
6
|
|
|
|
|
8
|
|
|
6
|
|
|
|
|
145
|
|
41
|
|
|
|
|
|
|
|
42
|
30
|
|
|
30
|
|
62
|
sub _build_version($self) { |
|
30
|
|
|
|
|
70
|
|
|
30
|
|
|
|
|
60
|
|
43
|
30
|
100
|
|
|
|
772
|
if ( !exists $self->record_hashref->{v} ) { |
44
|
1
|
|
|
|
|
26
|
return undef; |
45
|
|
|
|
|
|
|
} |
46
|
29
|
|
|
|
|
710
|
return $self->record_hashref->{v}; |
47
|
|
|
|
|
|
|
} |
48
|
|
|
|
|
|
|
|
49
|
35
|
|
|
35
|
|
76
|
sub _build_authority($self) { |
|
35
|
|
|
|
|
75
|
|
|
35
|
|
|
|
|
81
|
|
50
|
35
|
|
|
|
|
69
|
my $uri; |
51
|
35
|
100
|
|
|
|
937
|
if ( exists $self->record_hashref->{a} ) { |
52
|
2
|
|
50
|
|
|
53
|
$uri = $self->record_hashref->{a} // ''; |
53
|
|
|
|
|
|
|
} |
54
|
|
|
|
|
|
|
# TODO better parser here |
55
|
35
|
|
|
|
|
1001
|
return Mail::BIMI::Record::Authority->new( uri => $uri, bimi_object => $self->bimi_object ); |
56
|
|
|
|
|
|
|
} |
57
|
|
|
|
|
|
|
|
58
|
29
|
|
|
29
|
|
62
|
sub _build_location($self) { |
|
29
|
|
|
|
|
62
|
|
|
29
|
|
|
|
|
55
|
|
59
|
29
|
|
|
|
|
56
|
my $uri; |
60
|
29
|
100
|
|
|
|
778
|
if ( exists $self->record_hashref->{l} ) { |
61
|
24
|
|
50
|
|
|
573
|
$uri = $self->record_hashref->{l} // ''; |
62
|
|
|
|
|
|
|
} |
63
|
|
|
|
|
|
|
# TODO better parser here |
64
|
|
|
|
|
|
|
# Need to decode , and ; as per spec> |
65
|
29
|
|
|
|
|
161
|
my $location = Mail::BIMI::Record::Location->new( uri => $uri, is_relevant => $self->location_is_relevant, bimi_object => $self->bimi_object ); |
66
|
29
|
|
|
|
|
44401
|
return $location; |
67
|
|
|
|
|
|
|
} |
68
|
|
|
|
|
|
|
|
69
|
|
|
|
|
|
|
|
70
|
54
|
|
|
54
|
1
|
103
|
sub location_is_relevant($self) { |
|
54
|
|
|
|
|
89
|
|
|
54
|
|
|
|
|
89
|
|
71
|
|
|
|
|
|
|
# True if we don't have a relevant authority OR if we are checking VMC AND Location |
72
|
54
|
50
|
|
|
|
1328
|
return 1 unless $self->bimi_object->options->no_location_with_vmc; |
73
|
0
|
0
|
0
|
|
|
0
|
if ( $self->authority && $self->authority->is_relevant ) { |
74
|
0
|
|
|
|
|
0
|
$self->log_verbose('Location is not relevant'); |
75
|
0
|
|
|
|
|
0
|
return 0; |
76
|
|
|
|
|
|
|
} |
77
|
0
|
|
|
|
|
0
|
return 1; |
78
|
|
|
|
|
|
|
} |
79
|
|
|
|
|
|
|
|
80
|
36
|
|
|
36
|
|
89
|
sub _build_is_valid($self) { |
|
36
|
|
|
|
|
103
|
|
|
36
|
|
|
|
|
63
|
|
81
|
36
|
100
|
|
|
|
1073
|
return 0 if ! keys $self->record_hashref->%*; |
82
|
|
|
|
|
|
|
|
83
|
30
|
100
|
|
|
|
830
|
if ( !defined $self->version ) { |
84
|
1
|
|
|
|
|
10
|
$self->add_error('MISSING_V_TAG'); |
85
|
1
|
|
|
|
|
28
|
return 0; |
86
|
|
|
|
|
|
|
} |
87
|
|
|
|
|
|
|
else { |
88
|
29
|
100
|
|
|
|
721
|
$self->add_error('EMPTY_V_TAG') if lc $self->version eq ''; |
89
|
29
|
100
|
|
|
|
704
|
$self->add_error('INVALID_V_TAG') if lc $self->version ne 'bimi1'; |
90
|
29
|
100
|
|
|
|
822
|
return 0 if $self->errors->@*; |
91
|
|
|
|
|
|
|
} |
92
|
25
|
50
|
33
|
|
|
671
|
if ($self->authority->is_relevant && !$self->authority->is_valid) { |
93
|
0
|
|
|
|
|
0
|
$self->add_error_object( $self->authority->errors ); |
94
|
|
|
|
|
|
|
} |
95
|
25
|
100
|
66
|
|
|
173
|
if ($self->location_is_relevant && !$self->location->is_valid) { |
96
|
3
|
|
|
|
|
80
|
$self->add_error_object( $self->location->errors ); |
97
|
|
|
|
|
|
|
} |
98
|
|
|
|
|
|
|
|
99
|
25
|
100
|
|
|
|
741
|
return 0 if $self->errors->@*; |
100
|
|
|
|
|
|
|
|
101
|
22
|
50
|
|
|
|
545
|
if ( $self->bimi_object->options->require_vmc ) { |
102
|
0
|
0
|
0
|
|
|
0
|
unless ( $self->authority && $self->authority->vmc && $self->authority->vmc->is_valid ) { |
|
|
|
0
|
|
|
|
|
103
|
0
|
|
|
|
|
0
|
$self->add_error('VMC_REQUIRED'); |
104
|
|
|
|
|
|
|
} |
105
|
|
|
|
|
|
|
} |
106
|
|
|
|
|
|
|
|
107
|
22
|
50
|
33
|
|
|
608
|
if ( $self->authority && $self->authority->is_relevant ) { |
108
|
|
|
|
|
|
|
# Check the SVG payloads are identical |
109
|
|
|
|
|
|
|
## Compare raw? or Uncompressed? |
110
|
0
|
0
|
0
|
|
|
0
|
if ( !$self->authority->vmc ) { |
|
|
0
|
0
|
|
|
|
|
|
|
0
|
0
|
|
|
|
|
|
|
0
|
0
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
111
|
|
|
|
|
|
|
# We could not get a vmc to check, return an error. |
112
|
0
|
|
|
|
|
0
|
$self->add_error('VMC_PARSE_ERROR'); |
113
|
|
|
|
|
|
|
} |
114
|
|
|
|
|
|
|
elsif ( !$self->authority->vmc->indicator ) { |
115
|
|
|
|
|
|
|
# We could not get an indicator from the vmc to check, return an error. |
116
|
0
|
|
|
|
|
0
|
$self->add_error('VMC_PARSE_ERROR','Could not extract SVG from VMC'); |
117
|
|
|
|
|
|
|
} |
118
|
|
|
|
|
|
|
elsif ( $self->location_is_relevant && !$self->location ) { |
119
|
|
|
|
|
|
|
# We could not get a location to check against, return an error. |
120
|
0
|
|
|
|
|
0
|
$self->add_error('SVG_MISMATCH'); |
121
|
|
|
|
|
|
|
} |
122
|
|
|
|
|
|
|
elsif ( $self->location_is_relevant && !$self->location->indicator ) { |
123
|
|
|
|
|
|
|
# We could not get an indicator from the location to check against, return an error. |
124
|
0
|
|
|
|
|
0
|
$self->add_error('SVG_MISMATCH'); |
125
|
|
|
|
|
|
|
} |
126
|
|
|
|
|
|
|
elsif ( $self->location_is_relevant && $self->authority->vmc->indicator->data_uncompressed_normalized ne $self->location->indicator->data_uncompressed_normalized ) { |
127
|
0
|
|
|
|
|
0
|
$self->add_error('SVG_MISMATCH'); |
128
|
|
|
|
|
|
|
} |
129
|
|
|
|
|
|
|
elsif ( $self->location_is_relevant && $self->authority->vmc->indicator->data_uncompressed ne $self->location->indicator->data_uncompressed ) { |
130
|
0
|
|
|
|
|
0
|
$self->add_warning('Line encoding for SVG in bimi-location did not match SVG in VMC'); |
131
|
|
|
|
|
|
|
} |
132
|
|
|
|
|
|
|
} |
133
|
|
|
|
|
|
|
|
134
|
22
|
50
|
|
|
|
546
|
return 0 if $self->errors->@*; |
135
|
22
|
|
|
|
|
113
|
$self->log_verbose('Record is valid'); |
136
|
22
|
|
|
|
|
568
|
return 1; |
137
|
|
|
|
|
|
|
} |
138
|
|
|
|
|
|
|
|
139
|
26
|
|
|
26
|
|
59
|
sub _build_record_hashref($self) { |
|
26
|
|
|
|
|
53
|
|
|
26
|
|
|
|
|
66
|
|
140
|
26
|
|
|
|
|
764
|
my $domain = $self->domain; |
141
|
26
|
|
|
|
|
769
|
my $selector = $self->selector; |
142
|
26
|
|
|
|
|
656
|
my $fallback_selector = $self->selector; |
143
|
26
|
|
|
|
|
359
|
my $fallback_domain = Mail::DMARC::PurePerl->new->get_organizational_domain($domain); |
144
|
|
|
|
|
|
|
|
145
|
26
|
|
|
|
|
897238
|
my @records; |
146
|
|
|
|
|
|
|
eval { |
147
|
26
|
|
|
|
|
173
|
@records = $self->_get_from_dns($selector,$domain); |
148
|
25
|
|
|
|
|
139
|
1; |
149
|
26
|
100
|
|
|
|
94
|
} || do { |
150
|
1
|
|
|
|
|
78
|
my $error = $@; |
151
|
1
|
|
|
|
|
9
|
$error =~ s/ at \/.*$//s; |
152
|
1
|
|
|
|
|
9
|
$self->add_error('DNS_ERROR',$error); |
153
|
1
|
|
|
|
|
30
|
return {}; |
154
|
|
|
|
|
|
|
}; |
155
|
|
|
|
|
|
|
|
156
|
25
|
|
|
|
|
82
|
@records = grep { $_ =~ /^v=bimi1;/i } @records; |
|
20
|
|
|
|
|
191
|
|
157
|
|
|
|
|
|
|
|
158
|
25
|
100
|
|
|
|
144
|
if ( !@records ) { |
|
|
100
|
|
|
|
|
|
159
|
6
|
100
|
66
|
|
|
45
|
if ( $domain eq $fallback_domain && $selector eq $fallback_selector ) { |
160
|
|
|
|
|
|
|
# nothing to fall back to |
161
|
2
|
|
|
|
|
22
|
$self->add_error('NO_BIMI_RECORD'); |
162
|
2
|
|
|
|
|
60
|
return {}; |
163
|
|
|
|
|
|
|
} |
164
|
|
|
|
|
|
|
|
165
|
4
|
|
|
|
|
29
|
$self->log_verbose('Trying fallback domain'); |
166
|
4
|
|
|
|
|
11
|
my @records; |
167
|
|
|
|
|
|
|
eval { |
168
|
4
|
|
|
|
|
21
|
@records = $self->_get_from_dns($fallback_selector,$fallback_domain); |
169
|
3
|
|
|
|
|
11
|
1; |
170
|
4
|
100
|
|
|
|
13
|
} || do { |
171
|
1
|
|
|
|
|
50
|
my $error = $@; |
172
|
1
|
|
|
|
|
8
|
$error =~ s/ at \/.*$//; |
173
|
1
|
|
|
|
|
9
|
$self->add_error('DNS_ERROR',$error); |
174
|
1
|
|
|
|
|
32
|
return {}; |
175
|
|
|
|
|
|
|
}; |
176
|
|
|
|
|
|
|
|
177
|
3
|
|
|
|
|
10
|
@records = grep { $_ =~ /^v=bimi1;/i } @records; |
|
4
|
|
|
|
|
26
|
|
178
|
|
|
|
|
|
|
|
179
|
3
|
50
|
|
|
|
17
|
if ( !@records ) { |
|
|
100
|
|
|
|
|
|
180
|
0
|
|
|
|
|
0
|
$self->add_error('NO_BIMI_RECORD'); |
181
|
0
|
|
|
|
|
0
|
return {}; |
182
|
|
|
|
|
|
|
} |
183
|
|
|
|
|
|
|
elsif ( scalar @records > 1 ) { |
184
|
1
|
|
|
|
|
11
|
$self->add_error('MULTI_BIMI_RECORD'); |
185
|
1
|
|
|
|
|
39
|
return {}; |
186
|
|
|
|
|
|
|
} |
187
|
|
|
|
|
|
|
else { |
188
|
|
|
|
|
|
|
# We have one record, let's use that. |
189
|
2
|
|
|
|
|
76
|
$self->retrieved_record($records[0]); |
190
|
2
|
|
|
|
|
59
|
$self->retrieved_domain($fallback_domain); |
191
|
2
|
|
|
|
|
59
|
$self->retrieved_selector($fallback_selector); |
192
|
2
|
|
|
|
|
9
|
return $self->_parse_record($records[0]); |
193
|
|
|
|
|
|
|
} |
194
|
|
|
|
|
|
|
} |
195
|
|
|
|
|
|
|
elsif ( scalar @records > 1 ) { |
196
|
1
|
|
|
|
|
14
|
$self->add_error('MULTI_BIMI_RECORD'); |
197
|
1
|
|
|
|
|
31
|
return {}; |
198
|
|
|
|
|
|
|
} |
199
|
|
|
|
|
|
|
else { |
200
|
|
|
|
|
|
|
# We have one record, let's use that. |
201
|
18
|
|
|
|
|
834
|
$self->retrieved_record($records[0]); |
202
|
18
|
|
|
|
|
521
|
$self->retrieved_domain($domain); |
203
|
18
|
|
|
|
|
554
|
$self->retrieved_selector($selector); |
204
|
18
|
|
|
|
|
88
|
return $self->_parse_record($records[0]); |
205
|
|
|
|
|
|
|
} |
206
|
|
|
|
|
|
|
} |
207
|
|
|
|
|
|
|
|
208
|
30
|
|
|
30
|
|
78
|
sub _get_from_dns($self,$selector,$domain) { |
|
30
|
|
|
|
|
79
|
|
|
30
|
|
|
|
|
70
|
|
|
30
|
|
|
|
|
68
|
|
|
30
|
|
|
|
|
57
|
|
209
|
30
|
|
|
|
|
99
|
my @matches; |
210
|
|
|
|
|
|
|
my $cname; |
211
|
30
|
50
|
|
|
|
1386
|
if ($self->bimi_object->options->force_record) { |
212
|
0
|
|
|
|
|
0
|
$self->log_verbose('Using fake record'); |
213
|
0
|
|
|
|
|
0
|
push @matches, $self->bimi_object->options->force_record; |
214
|
0
|
|
|
|
|
0
|
return @matches; |
215
|
|
|
|
|
|
|
} |
216
|
30
|
|
|
|
|
748
|
my $res = $self->bimi_object->resolver; |
217
|
30
|
100
|
|
|
|
661
|
my $query = $res->query( "$selector._bimi.$domain", 'TXT' ) or do { |
218
|
6
|
|
|
|
|
18053
|
return @matches; |
219
|
|
|
|
|
|
|
}; |
220
|
22
|
|
|
|
|
45309
|
for my $rr ( $query->answer ) { |
221
|
24
|
100
|
|
|
|
442
|
$cname = $rr->cname if $rr->type eq 'CNAME'; |
222
|
24
|
100
|
|
|
|
373
|
next if $rr->type ne 'TXT'; |
223
|
23
|
|
|
|
|
333
|
push @matches, scalar $rr->txtdata; |
224
|
|
|
|
|
|
|
} |
225
|
|
|
|
|
|
|
|
226
|
22
|
100
|
66
|
|
|
1969
|
if (!@matches && $cname) { |
227
|
|
|
|
|
|
|
# follow a single CNAME |
228
|
1
|
50
|
|
|
|
5
|
$query = $res->query( $cname, 'TXT' ) or do { |
229
|
0
|
|
|
|
|
0
|
return @matches; |
230
|
|
|
|
|
|
|
}; |
231
|
1
|
|
|
|
|
1431
|
for my $rr ( $query->answer ) { |
232
|
1
|
50
|
|
|
|
10
|
next if $rr->type ne 'TXT'; |
233
|
1
|
|
|
|
|
15
|
push @matches, scalar $rr->txtdata; |
234
|
|
|
|
|
|
|
} |
235
|
|
|
|
|
|
|
} |
236
|
|
|
|
|
|
|
|
237
|
22
|
|
|
|
|
313
|
return @matches; |
238
|
|
|
|
|
|
|
} |
239
|
|
|
|
|
|
|
|
240
|
35
|
|
|
35
|
|
155
|
sub _parse_record($self,$record) { |
|
35
|
|
|
|
|
79
|
|
|
35
|
|
|
|
|
76
|
|
|
35
|
|
|
|
|
73
|
|
241
|
35
|
|
|
|
|
98
|
my $data = {}; |
242
|
35
|
|
|
|
|
206
|
my @parts = split ';', $record; |
243
|
35
|
|
|
|
|
132
|
foreach my $part ( @parts ) { |
244
|
71
|
|
|
|
|
305
|
$part =~ s/^ +//; |
245
|
71
|
|
|
|
|
185
|
$part =~ s/ +$//; |
246
|
71
|
|
|
|
|
265
|
my ( $key, $value ) = split '=', $part, 2; |
247
|
71
|
|
|
|
|
162
|
$key = lc $key; |
248
|
71
|
100
|
|
|
|
214
|
if ( exists $data->{ $key } ) { |
249
|
1
|
|
|
|
|
7
|
$self->add_error('DUPLICATE_KEY'); |
250
|
|
|
|
|
|
|
} |
251
|
71
|
50
|
|
|
|
151
|
if ( grep { $key eq $_ } ( qw{ v l a } ) ) { |
|
213
|
|
|
|
|
569
|
|
252
|
71
|
|
|
|
|
283
|
$data->{$key} = $value; |
253
|
|
|
|
|
|
|
} |
254
|
|
|
|
|
|
|
} |
255
|
35
|
|
|
|
|
1104
|
return $data; |
256
|
|
|
|
|
|
|
} |
257
|
|
|
|
|
|
|
|
258
|
|
|
|
|
|
|
|
259
|
5
|
|
|
5
|
1
|
11
|
sub finish($self) { |
|
5
|
|
|
|
|
13
|
|
|
5
|
|
|
|
|
10
|
|
260
|
5
|
50
|
|
|
|
240
|
$self->authority->finish if $self->authority; |
261
|
5
|
50
|
|
|
|
134
|
$self->location->finish if $self->location; |
262
|
5
|
|
|
|
|
5360
|
$self->_write_cache; |
263
|
|
|
|
|
|
|
} |
264
|
|
|
|
|
|
|
|
265
|
|
|
|
|
|
|
|
266
|
0
|
|
|
0
|
1
|
|
sub app_validate($self) { |
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
267
|
0
|
0
|
|
|
|
|
say 'Record Returned: '.($self->is_valid ? GREEN."\x{2713}" : BRIGHT_RED."\x{26A0}").RESET; |
268
|
0
|
|
|
|
|
|
$self->is_valid; # To set retrieved record and retrieved domain/selector |
269
|
0
|
|
0
|
|
|
|
say YELLOW.' Record : '.($self->retrieved_record//'-none-').RESET; |
270
|
0
|
0
|
|
|
|
|
if ($self->retrieved_record){ |
271
|
0
|
|
0
|
|
|
|
say YELLOW.' Version '.WHITE.': '.CYAN.($self->version//'-none-').RESET; |
272
|
0
|
|
0
|
|
|
|
say YELLOW.' Domain '.WHITE.': '.CYAN.($self->retrieved_domain//'-none-').RESET; |
273
|
0
|
|
0
|
|
|
|
say YELLOW.' Selector '.WHITE.': '.CYAN.($self->retrieved_selector//'-none-').RESET; |
274
|
0
|
0
|
0
|
|
|
|
say YELLOW.' Authority '.WHITE.': '.CYAN.($self->authority->uri//'-none-').RESET if $self->authority; |
275
|
0
|
0
|
0
|
|
|
|
say YELLOW.' Location '.WHITE.': '.CYAN.($self->location->uri//'-none-').RESET if $self->location_is_relevant && $self->location; |
|
|
|
0
|
|
|
|
|
276
|
0
|
0
|
|
|
|
|
say YELLOW.' Is Valid '.WHITE.': '.($self->is_valid?GREEN.'Yes':BRIGHT_RED.'No').RESET; |
277
|
|
|
|
|
|
|
} |
278
|
|
|
|
|
|
|
|
279
|
0
|
0
|
|
|
|
|
if ( $self->warnings->@* ) { |
280
|
0
|
|
|
|
|
|
say "Warnings:"; |
281
|
0
|
|
|
|
|
|
foreach my $warning ( $self->warnings->@* ) { |
282
|
0
|
|
|
|
|
|
say CYAN.' '.$warning.RESET; |
283
|
|
|
|
|
|
|
} |
284
|
|
|
|
|
|
|
} |
285
|
|
|
|
|
|
|
|
286
|
0
|
0
|
|
|
|
|
if ( ! $self->is_valid ) { |
287
|
0
|
|
|
|
|
|
say "Errors:"; |
288
|
0
|
|
|
|
|
|
foreach my $error ( $self->errors->@* ) { |
289
|
0
|
|
|
|
|
|
my $error_code = $error->code; |
290
|
0
|
|
|
|
|
|
my $error_text = $error->description; |
291
|
0
|
|
0
|
|
|
|
my $error_detail = $error->detail // ''; |
292
|
0
|
|
|
|
|
|
$error_detail =~ s/\n/\n /g; |
293
|
0
|
0
|
|
|
|
|
say BRIGHT_RED." $error_code ".WHITE.': '.CYAN.$error_text.($error_detail?"\n ".$error_detail:'').RESET; |
294
|
|
|
|
|
|
|
} |
295
|
|
|
|
|
|
|
} |
296
|
|
|
|
|
|
|
} |
297
|
|
|
|
|
|
|
|
298
|
|
|
|
|
|
|
1; |
299
|
|
|
|
|
|
|
|
300
|
|
|
|
|
|
|
__END__ |
301
|
|
|
|
|
|
|
|
302
|
|
|
|
|
|
|
=pod |
303
|
|
|
|
|
|
|
|
304
|
|
|
|
|
|
|
=encoding UTF-8 |
305
|
|
|
|
|
|
|
|
306
|
|
|
|
|
|
|
=head1 NAME |
307
|
|
|
|
|
|
|
|
308
|
|
|
|
|
|
|
Mail::BIMI::Record - Class to model a BIMI record |
309
|
|
|
|
|
|
|
|
310
|
|
|
|
|
|
|
=head1 VERSION |
311
|
|
|
|
|
|
|
|
312
|
|
|
|
|
|
|
version 3.20210512 |
313
|
|
|
|
|
|
|
|
314
|
|
|
|
|
|
|
=head1 DESCRIPTION |
315
|
|
|
|
|
|
|
|
316
|
|
|
|
|
|
|
Class for representing, retrieving, validating, and processing a BIMI Record |
317
|
|
|
|
|
|
|
|
318
|
|
|
|
|
|
|
=head1 INPUTS |
319
|
|
|
|
|
|
|
|
320
|
|
|
|
|
|
|
These values are used as inputs for lookups and verifications, they are typically set by the caller based on values found in the message being processed |
321
|
|
|
|
|
|
|
|
322
|
|
|
|
|
|
|
=head2 domain |
323
|
|
|
|
|
|
|
|
324
|
|
|
|
|
|
|
is=rw required |
325
|
|
|
|
|
|
|
|
326
|
|
|
|
|
|
|
Domain the for the record |
327
|
|
|
|
|
|
|
|
328
|
|
|
|
|
|
|
=head2 selector |
329
|
|
|
|
|
|
|
|
330
|
|
|
|
|
|
|
is=rw |
331
|
|
|
|
|
|
|
|
332
|
|
|
|
|
|
|
Selector used to retrieve the record; will become default if fallback was used |
333
|
|
|
|
|
|
|
|
334
|
|
|
|
|
|
|
=head1 ATTRIBUTES |
335
|
|
|
|
|
|
|
|
336
|
|
|
|
|
|
|
These values are derived from lookups and verifications made based upon the input values, it is however possible to override these with other values should you wish to, for example, validate a record before it is published in DNS, or validate an Indicator which is only available locally |
337
|
|
|
|
|
|
|
|
338
|
|
|
|
|
|
|
=head2 authority |
339
|
|
|
|
|
|
|
|
340
|
|
|
|
|
|
|
is=rw |
341
|
|
|
|
|
|
|
|
342
|
|
|
|
|
|
|
Mail::BIMI::Record::Authority object for this record |
343
|
|
|
|
|
|
|
|
344
|
|
|
|
|
|
|
=head2 cache_backend |
345
|
|
|
|
|
|
|
|
346
|
|
|
|
|
|
|
is=ro |
347
|
|
|
|
|
|
|
|
348
|
|
|
|
|
|
|
=head2 errors |
349
|
|
|
|
|
|
|
|
350
|
|
|
|
|
|
|
is=rw |
351
|
|
|
|
|
|
|
|
352
|
|
|
|
|
|
|
=head2 is_valid |
353
|
|
|
|
|
|
|
|
354
|
|
|
|
|
|
|
is=rw |
355
|
|
|
|
|
|
|
|
356
|
|
|
|
|
|
|
Is this record valid |
357
|
|
|
|
|
|
|
|
358
|
|
|
|
|
|
|
=head2 location |
359
|
|
|
|
|
|
|
|
360
|
|
|
|
|
|
|
is=rw |
361
|
|
|
|
|
|
|
|
362
|
|
|
|
|
|
|
Mail::BIMI::Record::Location object for this record |
363
|
|
|
|
|
|
|
|
364
|
|
|
|
|
|
|
=head2 record_hashref |
365
|
|
|
|
|
|
|
|
366
|
|
|
|
|
|
|
is=rw |
367
|
|
|
|
|
|
|
|
368
|
|
|
|
|
|
|
Hashref of record values |
369
|
|
|
|
|
|
|
|
370
|
|
|
|
|
|
|
=head2 retrieved_domain |
371
|
|
|
|
|
|
|
|
372
|
|
|
|
|
|
|
is=rw |
373
|
|
|
|
|
|
|
|
374
|
|
|
|
|
|
|
Domain the record was retrieved from |
375
|
|
|
|
|
|
|
|
376
|
|
|
|
|
|
|
=head2 retrieved_record |
377
|
|
|
|
|
|
|
|
378
|
|
|
|
|
|
|
is=rw |
379
|
|
|
|
|
|
|
|
380
|
|
|
|
|
|
|
Record as retrieved |
381
|
|
|
|
|
|
|
|
382
|
|
|
|
|
|
|
=head2 retrieved_selector |
383
|
|
|
|
|
|
|
|
384
|
|
|
|
|
|
|
is=rw |
385
|
|
|
|
|
|
|
|
386
|
|
|
|
|
|
|
Selector the record was retrieved from |
387
|
|
|
|
|
|
|
|
388
|
|
|
|
|
|
|
=head2 version |
389
|
|
|
|
|
|
|
|
390
|
|
|
|
|
|
|
is=rw |
391
|
|
|
|
|
|
|
|
392
|
|
|
|
|
|
|
BIMI Version tag |
393
|
|
|
|
|
|
|
|
394
|
|
|
|
|
|
|
=head2 warnings |
395
|
|
|
|
|
|
|
|
396
|
|
|
|
|
|
|
is=rw |
397
|
|
|
|
|
|
|
|
398
|
|
|
|
|
|
|
=head1 CONSUMES |
399
|
|
|
|
|
|
|
|
400
|
|
|
|
|
|
|
=over 4 |
401
|
|
|
|
|
|
|
|
402
|
|
|
|
|
|
|
=item * L<Mail::BIMI::Role::Cacheable> |
403
|
|
|
|
|
|
|
|
404
|
|
|
|
|
|
|
=item * L<Mail::BIMI::Role::HasError> |
405
|
|
|
|
|
|
|
|
406
|
|
|
|
|
|
|
=item * L<Mail::BIMI::Role::HasError|Mail::BIMI::Role::Cacheable> |
407
|
|
|
|
|
|
|
|
408
|
|
|
|
|
|
|
=back |
409
|
|
|
|
|
|
|
|
410
|
|
|
|
|
|
|
=head1 EXTENDS |
411
|
|
|
|
|
|
|
|
412
|
|
|
|
|
|
|
=over 4 |
413
|
|
|
|
|
|
|
|
414
|
|
|
|
|
|
|
=item * L<Mail::BIMI::Base> |
415
|
|
|
|
|
|
|
|
416
|
|
|
|
|
|
|
=back |
417
|
|
|
|
|
|
|
|
418
|
|
|
|
|
|
|
=head1 METHODS |
419
|
|
|
|
|
|
|
|
420
|
|
|
|
|
|
|
=head2 I<cache_valid_for()> |
421
|
|
|
|
|
|
|
|
422
|
|
|
|
|
|
|
How long should the cache for this class be valid |
423
|
|
|
|
|
|
|
|
424
|
|
|
|
|
|
|
=head2 I<location_is_relevant()> |
425
|
|
|
|
|
|
|
|
426
|
|
|
|
|
|
|
Return true is the location is relevant to the validation of the record. |
427
|
|
|
|
|
|
|
|
428
|
|
|
|
|
|
|
If we don't have a relevant authority, or we are checking BOTH authority and location. |
429
|
|
|
|
|
|
|
|
430
|
|
|
|
|
|
|
=head2 I<finish()> |
431
|
|
|
|
|
|
|
|
432
|
|
|
|
|
|
|
Finish and clean up, write cache if enabled. |
433
|
|
|
|
|
|
|
|
434
|
|
|
|
|
|
|
=head2 I<app_validate()> |
435
|
|
|
|
|
|
|
|
436
|
|
|
|
|
|
|
Output human readable validation status of this object |
437
|
|
|
|
|
|
|
|
438
|
|
|
|
|
|
|
=head1 REQUIRES |
439
|
|
|
|
|
|
|
|
440
|
|
|
|
|
|
|
=over 4 |
441
|
|
|
|
|
|
|
|
442
|
|
|
|
|
|
|
=item * L<Mail::BIMI::Prelude|Mail::BIMI::Prelude> |
443
|
|
|
|
|
|
|
|
444
|
|
|
|
|
|
|
=item * L<Mail::BIMI::Record::Authority|Mail::BIMI::Record::Authority> |
445
|
|
|
|
|
|
|
|
446
|
|
|
|
|
|
|
=item * L<Mail::BIMI::Record::Location|Mail::BIMI::Record::Location> |
447
|
|
|
|
|
|
|
|
448
|
|
|
|
|
|
|
=item * L<Mail::DMARC::PurePerl|Mail::DMARC::PurePerl> |
449
|
|
|
|
|
|
|
|
450
|
|
|
|
|
|
|
=item * L<Moose|Moose> |
451
|
|
|
|
|
|
|
|
452
|
|
|
|
|
|
|
=item * L<Term::ANSIColor|Term::ANSIColor> |
453
|
|
|
|
|
|
|
|
454
|
|
|
|
|
|
|
=back |
455
|
|
|
|
|
|
|
|
456
|
|
|
|
|
|
|
=head1 AUTHOR |
457
|
|
|
|
|
|
|
|
458
|
|
|
|
|
|
|
Marc Bradshaw <marc@marcbradshaw.net> |
459
|
|
|
|
|
|
|
|
460
|
|
|
|
|
|
|
=head1 COPYRIGHT AND LICENSE |
461
|
|
|
|
|
|
|
|
462
|
|
|
|
|
|
|
This software is copyright (c) 2020 by Marc Bradshaw. |
463
|
|
|
|
|
|
|
|
464
|
|
|
|
|
|
|
This is free software; you can redistribute it and/or modify it under |
465
|
|
|
|
|
|
|
the same terms as the Perl 5 programming language system itself. |
466
|
|
|
|
|
|
|
|
467
|
|
|
|
|
|
|
=cut |