File Coverage

blib/lib/Mail/BIMI/Indicator.pm
Criterion Covered Total %
statement 124 155 80.0
branch 27 50 54.0
condition 2 8 25.0
subroutine 20 22 90.9
pod 6 6 100.0
total 179 241 74.2


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.20210512'; # VERSION
4 29     29   429 use 5.20.0;
  29         598  
5 29     29   162 use Moose;
  29         52  
  29         252  
6 29     29   204331 use Moose::Util::TypeConstraints;
  29         71  
  29         292  
7 29     29   65050 use Mail::BIMI::Prelude;
  29         88  
  29         254  
8 29     29   8544 use File::Slurp qw{ read_file write_file };
  29         66  
  29         1416  
9 29     29   20637 use IO::Uncompress::Gunzip;
  29         1116251  
  29         1798  
10 29     29   289 use MIME::Base64;
  29         67  
  29         1601  
11 29     29   191 use Term::ANSIColor qw{ :constants };
  29         101  
  29         8777  
12 29     29   21874 use XML::LibXML 2.0202;
  29         931793  
  29         1856  
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 9     9   20 sub _build_validator_profile($self) {
  9         22  
  9         17  
43 9         240 return $self->bimi_object->options->svg_profile;
44             }
45              
46              
47 4     4 1 10 sub cache_valid_for($self) { return 3600 }
  4         10  
  4         9  
  4         93  
48              
49              
50 8     8 1 26 sub http_client_max_fetch_size($self) { return $self->bimi_object->options->svg_max_fetch_size };
  8         18  
  8         17  
  8         237  
51              
52 27     27   67 sub _build_data_uncompressed($self) {
  27         64  
  27         48  
53 27         672 my $data = $self->data;
54 27 100       180 if ( $data =~ /^\037\213/ ) {
55 2         8 $self->log_verbose('Uncompressing SVG');
56              
57 2         4 my $unzipped;
58 2         20 IO::Uncompress::Gunzip::gunzip(\$data,\$unzipped);
59 2 100       9679 if ( !$unzipped ) {
60 1         6 $self->add_error('SVG_UNZIP_ERROR');
61 1         25 return '';
62             }
63 1         33 return $unzipped;
64             }
65             else {
66 25         740 return $data;
67             }
68             }
69              
70              
71 1     1 1 3 sub data_maybe_compressed($self) {
  1         2  
  1         1  
72             # Alias for clarity, the data is as received.
73 1         31 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 25     25   51 sub _build_data_xml($self) {
  25         57  
  25         37  
84 25         76 my $xml;
85 25         706 my $data = $self->data_uncompressed;
86 25 100       86 if ( !$data ) {
87 1         6 $self->add_error('SVG_GET_ERROR');
88 1         23 return;
89             }
90             eval {
91 24         299 $xml = XML::LibXML->new->load_xml(string => $self->data_uncompressed);
92 23         11278 1;
93 24 100       57 } || do {
94 1         880 $self->add_error('SVG_INVALID_XML');
95 1         26 $self->log_verbose("Invalid XML :\n".$self->data_uncompressed);
96 1         22 return;
97             };
98 23         782 return $xml;
99             }
100              
101 22     22   54 sub _build_parser($self) {
  22         49  
  22         46  
102 22         357 state $parser = XML::LibXML::RelaxNG->new( string => $self->get_data_from_file($self->validator_profile.'.rng'), no_network => 1 );
103 22         154510 return $parser;
104             }
105              
106 22     22   46 sub _build_data($self) {
  22         43  
  22         46  
107 22 100       612 if ( ! $self->uri ) {
108 1         6 $self->add_error('CODE_MISSING_LOCATION');
109 1         22 return '';
110             }
111 21 100       602 if ($self->bimi_object->options->svg_from_file) {
112 1         23 $self->log_verbose('Reading SVG from file '.$self->bimi_object->options->svg_from_file);
113 1         21 return scalar read_file $self->bimi_object->options->svg_from_file;
114             }
115 20         498 $self->log_verbose('HTTP Fetch: '.$self->uri);
116 20         606 my $response = $self->http_client->get( $self->uri );
117 20 50       4974105 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 20         1261 return $response->{content};
127             }
128              
129 24     24   56 sub _build_is_valid($self) {
  24         70  
  24         49  
130              
131 24 50 33     668 if (!(defined $self->data||$self->uri)) {
132 0         0 $self->add_error('CODE_NOTHING_TO_VALIDATE');
133 0         0 return 0;
134             }
135              
136 24 50       695 if (!defined $self->data) {
137 0         0 $self->add_error('CODE_NO_DATA');
138 0         0 return 0;
139             }
140              
141 24 50 33     572 if (!$self->data && $self->errors->@*) {
142 0         0 return 0;
143             }
144              
145 24         62 my $is_valid;
146 24 100       751 if ( length $self->data_uncompressed > $self->bimi_object->options->svg_max_size ) {
147 1         7 $self->add_error('SVG_SIZE');
148             }
149             else {
150 23 100       555 if ( $self->bimi_object->options->no_validate_svg ) {
151 1         2 $is_valid=1;
152 1         4 $self->log_verbose('Skipping SVG validation');
153             }
154             else {
155             eval {
156 22         619 my $data_xml = $self->data_xml;
157 22 50       708 if ($data_xml) {
158 22         827 $self->parser->validate( $data_xml );
159 21         96 $is_valid=1;
160 21         195 $self->log_verbose('SVG is valid');
161             }
162 21         144 1;
163 22 100       69 } || do {
164 1         285 my $validation_error = $@;
165 1 50       7 my $error_text = ref $validation_error eq 'XML::LibXML::Error' ? $validation_error->as_string : $validation_error;
166 1         96 $self->add_error('SVG_VALIDATION_ERROR',$error_text);
167             };
168             }
169             }
170              
171 24 100       823 return 0 if $self->errors->@*;
172 22         640 return 1;
173             }
174              
175 4     4   13 sub _build_header($self) {
  4         11  
  4         8  
176 4 50       125 return if !$self->is_valid;
177 4         127 my $base64 = encode_base64( $self->data_uncompressed );
178 4         72 $base64 =~ s/\n//g;
179 4         63 my @parts = unpack("(A70)*", $base64);
180 4         151 return join("\n ", @parts);
181             }
182              
183              
184 5     5 1 11 sub finish($self) {
  5         12  
  5         9  
185 5         59 $self->_write_cache;
186             }
187              
188              
189 0     0 1   sub app_validate($self) {
  0            
  0            
190 0 0         say 'Indicator'.($self->source ? ' (From '.$self->source.')' : '' ).' Returned: '.($self->is_valid ? GREEN."\x{2713}" : BRIGHT_RED."\x{26A0}").RESET;
    0          
191 0 0         say YELLOW.' GZipped '.WHITE.': '.CYAN.($self->data_uncompressed eq $self->data?'No':'Yes').RESET;
192 0 0         say YELLOW.' BIMI-Indicator '.WHITE.': '.CYAN.$self->header.RESET if $self->is_valid;
193 0           say YELLOW.' Profile Used '.WHITE.': '.CYAN.$self->validator_profile.RESET;
194 0 0         say YELLOW.' Is Valid '.WHITE.': '.($self->is_valid?GREEN.'Yes':BRIGHT_RED.'No').RESET;
195 0 0         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.20210512
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