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.20210301'; # VERSION
4 30     30   390 use 5.20.0;
  30         650  
5 30     30   161 use Moose;
  30         49  
  30         240  
6 30     30   191576 use Moose::Util::TypeConstraints;
  30         64  
  30         272  
7 30     30   60921 use Mail::BIMI::Prelude;
  30         70  
  30         261  
8 30     30   8099 use File::Slurp qw{ read_file write_file };
  30         66  
  30         1343  
9 30     30   18821 use IO::Uncompress::Gunzip;
  30         1038482  
  30         1679  
10 30     30   289 use MIME::Base64;
  30         66  
  30         1554  
11 30     30   186 use Term::ANSIColor qw{ :constants };
  30         109  
  30         8090  
12 30     30   21067 use XML::LibXML 2.0202;
  30         1072941  
  30         1813  
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   31 sub _build_validator_profile($self) {
  14         25  
  14         25  
43 14         388 return $self->bimi_object->options->svg_profile;
44             }
45              
46              
47 4     4 1 9 sub cache_valid_for($self) { return 3600 }
  4         7  
  4         7  
  4         88  
48              
49              
50 9     9 1 18 sub http_client_max_fetch_size($self) { return $self->bimi_object->options->svg_max_fetch_size };
  9         19  
  9         16  
  9         226  
51              
52 33     33   72 sub _build_data_uncompressed($self) {
  33         87  
  33         85  
53 33         857 my $data = $self->data;
54 33 100       242 if ( $data =~ /^\037\213/ ) {
55 2         12 $self->log_verbose('Uncompressing SVG');
56              
57 2         5 my $unzipped;
58 2         37 IO::Uncompress::Gunzip::gunzip(\$data,\$unzipped);
59 2 100       9610 if ( !$unzipped ) {
60 1         11 $self->add_error('SVG_UNZIP_ERROR');
61 1         26 return '';
62             }
63 1         54 return $unzipped;
64             }
65             else {
66 31         781 return $data;
67             }
68             }
69              
70              
71 1     1 1 3 sub data_maybe_compressed($self) {
  1         3  
  1         2  
72             # Alias for clarity, the data is as received.
73 1         33 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   59 sub _build_data_xml($self) {
  31         60  
  31         61  
84 31         63 my $xml;
85 31         868 my $data = $self->data_uncompressed;
86 31 100       102 if ( !$data ) {
87 1         6 $self->add_error('SVG_GET_ERROR');
88 1         22 return;
89             }
90             eval {
91 30         334 $xml = XML::LibXML->new->load_xml(string => $self->data_uncompressed);
92 29         12741 1;
93 30 100       78 } || do {
94 1         877 $self->add_error('SVG_INVALID_XML');
95 1         26 $self->log_verbose("Invalid XML :\n".$self->data_uncompressed);
96 1         24 return;
97             };
98 29         933 return $xml;
99             }
100              
101 28     28   60 sub _build_parser($self) {
  28         54  
  28         48  
102 28         354 state $parser = XML::LibXML::RelaxNG->new( string => $self->get_data_from_file($self->validator_profile.'.rng'), no_network => 1 );
103 28         156332 return $parser;
104             }
105              
106 26     26   47 sub _build_data($self) {
  26         49  
  26         42  
107 26 100       664 if ( ! $self->uri ) {
108 1         7 $self->add_error('CODE_MISSING_LOCATION');
109 1         23 return '';
110             }
111 25 100       555 if ($self->bimi_object->options->svg_from_file) {
112 2         57 $self->log_verbose('Reading SVG from file '.$self->bimi_object->options->svg_from_file);
113 2         45 return scalar read_file $self->bimi_object->options->svg_from_file;
114             }
115 23         538 $self->log_verbose('HTTP Fetch: '.$self->uri);
116 23         625 my $response = $self->http_client->get( $self->uri );
117 23 50       7620367 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         1244 return $response->{content};
127             }
128              
129 30     30   68 sub _build_is_valid($self) {
  30         64  
  30         58  
130              
131 30 50 33     798 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       823 if (!defined $self->data) {
137 0         0 $self->add_error('CODE_NO_DATA');
138 0         0 return 0;
139             }
140              
141 30 50 33     690 if (!$self->data && $self->errors->@*) {
142 0         0 return 0;
143             }
144              
145 30         67 my $is_valid;
146 30 100       898 if ( length $self->data_uncompressed > $self->bimi_object->options->svg_max_size ) {
147 1         8 $self->add_error('SVG_SIZE');
148             }
149             else {
150 29 100       656 if ( $self->bimi_object->options->no_validate_svg ) {
151 1         3 $is_valid=1;
152 1         4 $self->log_verbose('Skipping SVG validation');
153             }
154             else {
155             eval {
156 28         704 my $data_xml = $self->data_xml;
157 28 50       743 if ($data_xml) {
158 28         918 $self->parser->validate( $data_xml );
159 27         98 $is_valid=1;
160 27         235 $self->log_verbose('SVG is valid');
161             }
162 27         161 1;
163 28 100       64 } || do {
164 1         351 my $validation_error = $@;
165 1 50       14 my $error_text = ref $validation_error eq 'XML::LibXML::Error' ? $validation_error->as_string : $validation_error;
166 1         109 $self->add_error('SVG_VALIDATION_ERROR',$error_text);
167             };
168             }
169             }
170              
171 30 100       919 return 0 if $self->errors->@*;
172 28         680 return 1;
173             }
174              
175 10     10   23 sub _build_header($self) {
  10         19  
  10         18  
176 10 50       245 return if !$self->is_valid;
177 10         251 my $base64 = encode_base64( $self->data_uncompressed );
178 10         116 $base64 =~ s/\n//g;
179 10         125 my @parts = unpack("(A70)*", $base64);
180 10         344 return join("\n ", @parts);
181             }
182              
183              
184 8     8 1 18 sub finish($self) {
  8         53  
  8         14  
185 8         56 $self->_write_cache;
186             }
187              
188              
189 6     6 1 9 sub app_validate($self) {
  6         12  
  6         9  
190 6 100       150 say 'Indicator'.($self->source ? ' (From '.$self->source.')' : '' ).' Returned: '.($self->is_valid ? GREEN."\x{2713}" : BRIGHT_RED."\x{26A0}").RESET;
    50          
191 6 50       308 say YELLOW.' GZipped '.WHITE.': '.CYAN.($self->data_uncompressed eq $self->data?'No':'Yes').RESET;
192 6 50       234 say YELLOW.' BIMI-Indicator '.WHITE.': '.CYAN.$self->header.RESET if $self->is_valid;
193 6         240 say YELLOW.' Profile Used '.WHITE.': '.CYAN.$self->validator_profile.RESET;
194 6 50       193 say YELLOW.' Is Valid '.WHITE.': '.($self->is_valid?GREEN.'Yes':BRIGHT_RED.'No').RESET;
195 6 50       338 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.20210301
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