File Coverage

blib/lib/Mail/BIMI/Indicator.pm
Criterion Covered Total %
statement 133 155 85.8
branch 34 50 68.0
condition 2 8 25.0
subroutine 21 22 95.4
pod 6 6 100.0
total 196 241 81.3


line stmt bran cond sub pod time code
1             package Mail::BIMI::Indicator;
2             # ABSTRACT: Class to model a BIMI indicator
3             our $VERSION = '3.20210225'; # VERSION
4 30     30   494 use 5.20.0;
  30         602  
5 30     30   175 use Moose;
  30         56  
  30         276  
6 30     30   211585 use Moose::Util::TypeConstraints;
  30         75  
  30         305  
7 30     30   67535 use Mail::BIMI::Prelude;
  30         75  
  30         290  
8 30     30   8823 use File::Slurp qw{ read_file write_file };
  30         72  
  30         1459  
9 30     30   21316 use IO::Uncompress::Gunzip;
  30         1129414  
  30         1858  
10 30     30   608 use MIME::Base64;
  30         76  
  30         1648  
11 30     30   201 use Term::ANSIColor qw{ :constants };
  30         152  
  30         8807  
12 30     30   22147 use XML::LibXML 2.0202;
  30         1142973  
  30         2258  
13             our @VALIDATOR_PROFILES = qw{ SVG_1.2_BIMI SVG_1.2_PS Tiny-1.2 };
14              
15             extends 'Mail::BIMI::Base';
16             with(
17             'Mail::BIMI::Role::HasError',
18             'Mail::BIMI::Role::HasHTTPClient',
19             'Mail::BIMI::Role::Data',
20             'Mail::BIMI::Role::Cacheable',
21             );
22             has uri => ( is => 'rw', isa => 'Str', traits => ['CacheKey'],
23             documentation => 'inputs: URL to retrieve Indicator from', );
24             has source => ( is => 'rw', isa => 'Str', traits => ['Cacheable'],
25             documentation => 'Human readable summary of where this indicator was retrieved from' );
26             has data => ( is => 'rw', isa => 'Str', lazy => 1, builder => '_build_data', traits => ['Cacheable'],
27             documentation => 'inputs: Raw data representing the Indicator; Fetches from uri if not given', );
28             has data_uncompressed => ( is => 'rw', isa => 'Str', lazy => 1, builder => '_build_data_uncompressed', traits => ['Cacheable'],
29             documentation => 'Raw data in uncompressed form' );
30             has data_xml => ( is => 'rw', lazy => 1, builder => '_build_data_xml',
31             documentation => 'XML::LibXML object representing the Indicator' );
32             has is_valid => ( is => 'rw', lazy => 1, builder => '_build_is_valid', traits => ['Cacheable'],
33             documentation => 'Is this indicator valid' );
34             has parser => ( is => 'rw', lazy => 1, builder => '_build_parser',
35             documentation => 'XML::LibXML::RelaxNG parser object used to validate the Indicator XML' );
36             has header => ( is => 'rw', lazy => 1, builder => '_build_header', traits => ['Cacheable'],
37             documentation => 'Indicator data encoded as Base64 ready for insertion as BIMI-Indicator header' );
38             has validator_profile => ( is => 'rw', isa => enum(\@VALIDATOR_PROFILES), lazy => 1, builder => '_build_validator_profile', traits => ['Cacheable'],
39             documentation => 'inputs: Validator profile used to validate the Indicator', );
40              
41              
42 14     14   29 sub _build_validator_profile($self) {
  14         32  
  14         26  
43 14         463 return $self->bimi_object->options->svg_profile;
44             }
45              
46              
47 4     4 1 11 sub cache_valid_for($self) { return 3600 }
  4         8  
  4         7  
  4         93  
48              
49              
50 9     9 1 21 sub http_client_max_fetch_size($self) { return $self->bimi_object->options->svg_max_fetch_size };
  9         20  
  9         19  
  9         261  
51              
52 33     33   97 sub _build_data_uncompressed($self) {
  33         87  
  33         63  
53 33         976 my $data = $self->data;
54 33 100       233 if ( $data =~ /^\037\213/ ) {
55 2         16 $self->log_verbose('Uncompressing SVG');
56              
57 2         4 my $unzipped;
58 2         16 IO::Uncompress::Gunzip::gunzip(\$data,\$unzipped);
59 2 100       9372 if ( !$unzipped ) {
60 1         8 $self->add_error('SVG_UNZIP_ERROR');
61 1         27 return '';
62             }
63 1         39 return $unzipped;
64             }
65             else {
66 31         869 return $data;
67             }
68             }
69              
70              
71 1     1 1 2 sub data_maybe_compressed($self) {
  1         3  
  1         2  
72             # Alias for clarity, the data is as received.
73 1         32 return $self->data;
74             }
75              
76              
77 0     0 1 0 sub data_uncompressed_normalized($self) {
  0         0  
  0         0  
78 0         0 my $data = $self->data_uncompressed;
79 0         0 $data =~ s/\r\n?/\n/g;
80 0         0 return $data;
81             }
82              
83 31     31   78 sub _build_data_xml($self) {
  31         70  
  31         55  
84 31         59 my $xml;
85 31         859 my $data = $self->data_uncompressed;
86 31 100       120 if ( !$data ) {
87 1         5 $self->add_error('SVG_GET_ERROR');
88 1         23 return;
89             }
90             eval {
91 30         402 $xml = XML::LibXML->new->load_xml(string => $self->data_uncompressed);
92 29         14650 1;
93 30 100       71 } || do {
94 1         856 $self->add_error('SVG_INVALID_XML');
95 1         25 $self->log_verbose("Invalid XML :\n".$self->data_uncompressed);
96 1         23 return;
97             };
98 29         1055 return $xml;
99             }
100              
101 28     28   62 sub _build_parser($self) {
  28         70  
  28         56  
102 28         401 state $parser = XML::LibXML::RelaxNG->new( string => $self->get_data_from_file($self->validator_profile.'.rng'), no_network => 1 );
103 28         174072 return $parser;
104             }
105              
106 26     26   106 sub _build_data($self) {
  26         50  
  26         45  
107 26 100       747 if ( ! $self->uri ) {
108 1         7 $self->add_error('CODE_MISSING_LOCATION');
109 1         22 return '';
110             }
111 25 100       646 if ($self->bimi_object->options->svg_from_file) {
112 2         53 $self->log_verbose('Reading SVG from file '.$self->bimi_object->options->svg_from_file);
113 2         50 return scalar read_file $self->bimi_object->options->svg_from_file;
114             }
115 23         697 $self->log_verbose('HTTP Fetch: '.$self->uri);
116 23         715 my $response = $self->http_client->get( $self->uri );
117 23 50       6448219 if ( !$response->{success} ) {
118 0 0       0 if ( $response->{status} == 599 ) {
119 0         0 $self->add_error('SVG_FETCH_ERROR',$response->{content});
120             }
121             else {
122 0         0 $self->add_error('SVG_FETCH_ERROR',$response->{status});
123             }
124 0         0 return '';
125             }
126 23         1524 return $response->{content};
127             }
128              
129 30     30   81 sub _build_is_valid($self) {
  30         74  
  30         58  
130              
131 30 50 33     914 if (!(defined $self->data||$self->uri)) {
132 0         0 $self->add_error('CODE_NOTHING_TO_VALIDATE');
133 0         0 return 0;
134             }
135              
136 30 50       843 if (!defined $self->data) {
137 0         0 $self->add_error('CODE_NO_DATA');
138 0         0 return 0;
139             }
140              
141 30 50 33     824 if (!$self->data && $self->errors->@*) {
142 0         0 return 0;
143             }
144              
145 30         69 my $is_valid;
146 30 100       916 if ( length $self->data_uncompressed > $self->bimi_object->options->svg_max_size ) {
147 1         6 $self->add_error('SVG_SIZE');
148             }
149             else {
150 29 100       758 if ( $self->bimi_object->options->no_validate_svg ) {
151 1         3 $is_valid=1;
152 1         3 $self->log_verbose('Skipping SVG validation');
153             }
154             else {
155             eval {
156 28         823 my $data_xml = $self->data_xml;
157 28 50       792 if ($data_xml) {
158 28         1104 $self->parser->validate( $data_xml );
159 27         117 $is_valid=1;
160 27         227 $self->log_verbose('SVG is valid');
161             }
162 27         201 1;
163 28 100       96 } || do {
164 1         325 my $validation_error = $@;
165 1 50       13 my $error_text = ref $validation_error eq 'XML::LibXML::Error' ? $validation_error->as_string : $validation_error;
166 1         104 $self->add_error('SVG_VALIDATION_ERROR',$error_text);
167             };
168             }
169             }
170              
171 30 100       962 return 0 if $self->errors->@*;
172 28         859 return 1;
173             }
174              
175 10     10   27 sub _build_header($self) {
  10         22  
  10         21  
176 10 50       286 return if !$self->is_valid;
177 10         299 my $base64 = encode_base64( $self->data_uncompressed );
178 10         138 $base64 =~ s/\n//g;
179 10         160 my @parts = unpack("(A70)*", $base64);
180 10         358 return join("\n ", @parts);
181             }
182              
183              
184 8     8 1 21 sub finish($self) {
  8         17  
  8         32  
185 8         85 $self->_write_cache;
186             }
187              
188              
189 6     6 1 17 sub app_validate($self) {
  6         17  
  6         14  
190 6 100       184 say 'Indicator'.($self->source ? ' (From '.$self->source.')' : '' ).' Returned: '.($self->is_valid ? GREEN."\x{2713}" : BRIGHT_RED."\x{26A0}").RESET;
    50          
191 6 50       374 say YELLOW.' GZipped '.WHITE.': '.CYAN.($self->data_uncompressed eq $self->data?'No':'Yes').RESET;
192 6 50       296 say YELLOW.' BIMI-Indicator '.WHITE.': '.CYAN.$self->header.RESET if $self->is_valid;
193 6         283 say YELLOW.' Profile Used '.WHITE.': '.CYAN.$self->validator_profile.RESET;
194 6 50       231 say YELLOW.' Is Valid '.WHITE.': '.($self->is_valid?GREEN.'Yes':BRIGHT_RED.'No').RESET;
195 6 50       391 if ( ! $self->is_valid ) {
196 0           say "Errors:";
197 0           foreach my $error ( $self->errors->@* ) {
198 0           my $error_code = $error->code;
199 0           my $error_text = $error->description;
200 0   0       my $error_detail = $error->detail // '';
201 0           $error_detail =~ s/\n/\n /g;
202 0 0         say BRIGHT_RED." $error_code ".WHITE.': '.CYAN.$error_text.($error_detail?"\n ".$error_detail:'').RESET;
203             }
204             }
205             }
206              
207             1;
208              
209             __END__
210              
211             =pod
212              
213             =encoding UTF-8
214              
215             =head1 NAME
216              
217             Mail::BIMI::Indicator - Class to model a BIMI indicator
218              
219             =head1 VERSION
220              
221             version 3.20210225
222              
223             =head1 DESCRIPTION
224              
225             Class for representing, retrieving, validating, and processing a BIMI Indicator
226              
227             =head1 INPUTS
228              
229             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
230              
231             =head2 data
232              
233             is=rw
234              
235             Raw data representing the Indicator; Fetches from uri if not given
236              
237             =head2 uri
238              
239             is=rw
240              
241             URL to retrieve Indicator from
242              
243             =head2 validator_profile
244              
245             is=rw
246              
247             Validator profile used to validate the Indicator
248              
249             =head1 ATTRIBUTES
250              
251             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
252              
253             =head2 cache_backend
254              
255             is=ro
256              
257             =head2 data_uncompressed
258              
259             is=rw
260              
261             Raw data in uncompressed form
262              
263             =head2 data_xml
264              
265             is=rw
266              
267             XML::LibXML object representing the Indicator
268              
269             =head2 errors
270              
271             is=rw
272              
273             =head2 header
274              
275             is=rw
276              
277             Indicator data encoded as Base64 ready for insertion as BIMI-Indicator header
278              
279             =head2 http_client
280              
281             is=rw
282              
283             HTTP::Tiny::Paranoid (or similar) object used for HTTP operations
284              
285             =head2 is_valid
286              
287             is=rw
288              
289             Is this indicator valid
290              
291             =head2 parser
292              
293             is=rw
294              
295             XML::LibXML::RelaxNG parser object used to validate the Indicator XML
296              
297             =head2 source
298              
299             is=rw
300              
301             Human readable summary of where this indicator was retrieved from
302              
303             =head2 warnings
304              
305             is=rw
306              
307             =head1 CONSUMES
308              
309             =over 4
310              
311             =item * L<Mail::BIMI::Role::Cacheable>
312              
313             =item * L<Mail::BIMI::Role::Data>
314              
315             =item * L<Mail::BIMI::Role::HasError>
316              
317             =item * L<Mail::BIMI::Role::HasError|Mail::BIMI::Role::HasHTTPClient|Mail::BIMI::Role::Data|Mail::BIMI::Role::Cacheable>
318              
319             =item * L<Mail::BIMI::Role::HasHTTPClient>
320              
321             =back
322              
323             =head1 EXTENDS
324              
325             =over 4
326              
327             =item * L<Mail::BIMI::Base>
328              
329             =back
330              
331             =head1 METHODS
332              
333             =head2 I<cache_valid_for()>
334              
335             How long should the cache for this class be valid
336              
337             =head2 I<http_client_max_fetch_size()>
338              
339             Maximum permitted HTTP fetch
340              
341             =head2 I<data_maybe_compressed()>
342              
343             Synonym for data; returns the data in a maybe compressed format
344              
345             =head2 I<data_uncompressed_normalized()>
346              
347             Returns the uncompressed data with normalized line endings
348              
349             =head2 I<finish()>
350              
351             Finish and clean up, write cache if enabled.
352              
353             =head2 I<app_validate()>
354              
355             Output human readable validation status of this object
356              
357             =head1 REQUIRES
358              
359             =over 4
360              
361             =item * L<File::Slurp|File::Slurp>
362              
363             =item * L<IO::Uncompress::Gunzip|IO::Uncompress::Gunzip>
364              
365             =item * L<MIME::Base64|MIME::Base64>
366              
367             =item * L<Mail::BIMI::Prelude|Mail::BIMI::Prelude>
368              
369             =item * L<Moose|Moose>
370              
371             =item * L<Moose::Util::TypeConstraints|Moose::Util::TypeConstraints>
372              
373             =item * L<Term::ANSIColor|Term::ANSIColor>
374              
375             =item * L<XML::LibXML|XML::LibXML>
376              
377             =back
378              
379             =head1 AUTHOR
380              
381             Marc Bradshaw <marc@marcbradshaw.net>
382              
383             =head1 COPYRIGHT AND LICENSE
384              
385             This software is copyright (c) 2020 by Marc Bradshaw.
386              
387             This is free software; you can redistribute it and/or modify it under
388             the same terms as the Perl 5 programming language system itself.
389              
390             =cut