File Coverage

blib/lib/Mail/SpamAssassin/Plugin/ImageInfo.pm
Criterion Covered Total %
statement 28 122 22.9
branch 0 70 0.0
condition 1 50 2.0
subroutine 6 15 40.0
pod 1 9 11.1
total 36 266 13.5


line stmt bran cond sub pod time code
1             # <@LICENSE>
2             # Licensed to the Apache Software Foundation (ASF) under one or more
3             # contributor license agreements. See the NOTICE file distributed with
4             # this work for additional information regarding copyright ownership.
5             # The ASF licenses this file to you under the Apache License, Version 2.0
6             # (the "License"); you may not use this file except in compliance with
7             # the License. You may obtain a copy of the License at:
8             #
9             # http://www.apache.org/licenses/LICENSE-2.0
10             #
11             # Unless required by applicable law or agreed to in writing, software
12             # distributed under the License is distributed on an "AS IS" BASIS,
13             # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14             # See the License for the specific language governing permissions and
15             # limitations under the License.
16             # </@LICENSE>
17             #
18             # -------------------------------------------------------
19             # ImageInfo Plugin for SpamAssassin
20             # Version: 0.7
21             # Created: 2006-08-02
22             # Modified: 2007-01-17
23             #
24             # Changes:
25             # 0.7 - added image_name_regex to allow pattern matching on the image name
26             # - added support for image/pjpeg content types (progressive jpeg)
27             # - updated imageinfo.cf with a few sample rules for using image_name_regex()
28             # 0.6 - fixed dems_ bug in image_size_range_
29             # 0.5 - added image_named and image_to_text_ratio
30             # 0.4 - added image_size_exact and image_size_range
31             # 0.3 - added jpeg support
32             # 0.2 - optimized by theo
33             # 0.1 - added gif/png support
34             #
35             #
36             # Usage:
37             # image_count()
38             #
39             # body RULENAME eval:image_count(<type>,<min>,[max])
40             # type: 'all','gif','png', or 'jpeg'
41             # min: required, message contains at least this
42             # many images
43             # max: optional, if specified, message must not
44             # contain more than this number of images
45             #
46             # image_count() examples
47             #
48             # body ONE_IMAGE eval:image_count('all',1,1)
49             # body ONE_OR_MORE_IMAGES eval:image_count('all',1)
50             # body ONE_PNG eval:image_count('png',1,1)
51             # body TWO_GIFS eval:image_count('gif',2,2)
52             # body MANY_JPEGS eval:image_count('gif',5)
53             #
54             # pixel_coverage()
55             #
56             # body RULENAME eval:pixel_coverage(<type>,<min>,[max])
57             # type: 'all','gif','png', or 'jpeg'
58             # min: required, message contains at least this
59             # much pixel area
60             # max: optional, if specified, message must not
61             # contain more than this much pixel area
62             #
63             # pixel_coverage() examples
64             #
65             # body LARGE_IMAGE_AREA eval:pixel_coverage('all',150000) # catches any images that are 150k pixel/sq or higher
66             # body SMALL_GIF_AREA eval:pixel_coverage('gif',1,40000) # catches only gifs that 1 to 40k pixel/sql
67             #
68             # image_name_regex()
69             #
70             # body RULENAME eval:image_name_regex(<regex>)
71             # regex: full quoted regexp, see examples below
72             #
73             # image_name_regex() examples
74             #
75             # body CG_DOUBLEDOT_GIF eval:image_name_regex('/^\w{2,9}\.\.gif$/i') # catches double dot gifs abcd..gif
76             #
77             #
78             #
79             # -------------------------------------------------------
80              
81              
82             use Mail::SpamAssassin::Plugin;
83 22     22   150 use Mail::SpamAssassin::Logger;
  22         44  
  22         627  
84 22     22   109 use strict;
  22         46  
  22         1182  
85 22     22   137 use warnings;
  22         46  
  22         537  
86 22     22   119 # use bytes;
  22         43  
  22         683  
87             use re 'taint';
88 22     22   138  
  22         62  
  22         56898  
89             our @ISA = qw(Mail::SpamAssassin::Plugin);
90              
91             # constructor: register the eval rule
92             my $class = shift;
93             my $mailsaobject = shift;
94 63     63 1 217  
95 63         150 # some boilerplate...
96             $class = ref($class) || $class;
97             my $self = $class->SUPER::new($mailsaobject);
98 63   33     385 bless ($self, $class);
99 63         318  
100 63         160 $self->register_eval_rule ("image_count");
101             $self->register_eval_rule ("pixel_coverage");
102 63         253 $self->register_eval_rule ("image_size_exact");
103 63         202 $self->register_eval_rule ("image_size_range");
104 63         186 $self->register_eval_rule ("image_named");
105 63         201 $self->register_eval_rule ("image_name_regex");
106 63         223 $self->register_eval_rule ("image_to_text_ratio");
107 63         175  
108 63         188 return $self;
109             }
110 63         517  
111             # -----------------------------------------
112              
113             my %get_details = (
114             'gif' => sub {
115             my ($pms, $part) = @_;
116             my $header = $part->decode(13);
117              
118             # make sure this is actually a valid gif..
119             return unless $header =~ s/^GIF(8[79]a)//;
120             my $version = $1;
121              
122             my ($width, $height, $packed, $bgcolor, $aspect) = unpack("vvCCC", $header);
123             my $color_table_size = 1 << (($packed & 0x07) + 1);
124              
125             # for future enhancements
126             #my $global_color_table = $packed & 0x80;
127             #my $has_global_color_table = $global_color_table ? 1 : 0;
128             #my $sorted_colors = ($packed & 0x08)?1:0;
129             #my $resolution = ((($packed & 0x70) >> 4) + 1);
130              
131             if ($height && $width) {
132             my $area = $width * $height;
133             $pms->{imageinfo}->{pc_gif} += $area;
134             $pms->{imageinfo}->{dems_gif}->{"${height}x${width}"} = 1;
135             $pms->{imageinfo}->{names_all}->{$part->{'name'}} = 1 if $part->{'name'};
136             dbg("imageinfo: gif image ".($part->{'name'} ? $part->{'name'} : '')." is $height x $width pixels ($area pixels sq.), with $color_table_size color table");
137             }
138             },
139              
140             'png' => sub {
141             my ($pms, $part) = @_;
142             my $data = $part->decode();
143              
144             return unless (substr($data, 0, 8) eq "\x89PNG\x0d\x0a\x1a\x0a");
145              
146             my $datalen = length $data;
147             my $pos = 8;
148             my $chunksize = 8;
149             my ($width, $height) = ( 0, 0 );
150             my ($depth, $ctype, $compression, $filter, $interlace);
151              
152             while ($pos < $datalen) {
153             my ($len, $type) = unpack("Na4", substr($data, $pos, $chunksize));
154             $pos += $chunksize;
155              
156             last if $type eq "IEND"; # end of png image.
157              
158             next unless ( $type eq "IHDR" && $len == 13 );
159              
160             my $bytes = substr($data, $pos, $len + 4);
161             my $crc = unpack("N", substr($bytes, -4, 4, ""));
162              
163             if ($type eq "IHDR" && $len == 13) {
164             ($width, $height, $depth, $ctype, $compression, $filter, $interlace) = unpack("NNCCCCC", $bytes);
165             last;
166             }
167             }
168              
169             if ($height && $width) {
170             my $area = $width * $height;
171             $pms->{imageinfo}->{pc_png} += $area;
172             $pms->{imageinfo}->{dems_png}->{"${height}x${width}"} = 1;
173             $pms->{imageinfo}->{names_all}->{$part->{'name'}} = 1 if $part->{'name'};
174             dbg("imageinfo: png image ".($part->{'name'} ? $part->{'name'} : '')." is $height x $width pixels ($area pixels sq.)");
175             }
176             },
177              
178             'jpeg' => sub {
179             my ($pms, $part) = @_;
180              
181             my $data = $part->decode();
182              
183             my $index = substr($data, 0, 2);
184             return unless $index eq "\xFF\xD8";
185              
186             my $pos = 2;
187             my $chunksize = 4;
188             my ($prec, $height, $width, $comps) = (undef,0,0,undef);
189             while (1) {
190             my ($xx, $mark, $len) = unpack("CCn", substr($data, $pos, $chunksize));
191             last if (!defined $xx || $xx != 0xFF);
192             last if (!defined $mark || $mark == 0xDA || $mark == 0xD9);
193             last if (!defined $len || $len < 2);
194             $pos += $chunksize;
195             my $block = substr($data, $pos, $len - 2);
196             my $blocklen = length($block);
197             if ( ($mark >= 0xC0 && $mark <= 0xC3) || ($mark >= 0xC5 && $mark <= 0xC7) ||
198             ($mark >= 0xC9 && $mark <= 0xCB) || ($mark >= 0xCD && $mark <= 0xCF) ) {
199             ($prec, $height, $width, $comps) = unpack("CnnC", substr($block, 0, 6, ""));
200             last;
201             }
202             $pos += $blocklen;
203             }
204              
205             if ($height && $width) {
206             my $area = $height * $width;
207             $pms->{imageinfo}->{pc_jpeg} += $area;
208             $pms->{imageinfo}->{dems_jpeg}->{"${height}x${width}"} = 1;
209             $pms->{imageinfo}->{names_all}->{$part->{'name'}} = 1 if $part->{'name'};
210             dbg("imageinfo: jpeg image ".($part->{'name'} ? $part->{'name'} : '')." is $height x $width pixels ($area pixels sq.)");
211             }
212              
213             },
214              
215             );
216              
217             my ($self,$pms) = @_;
218             my $result = 0;
219              
220 0     0     foreach my $type ( 'all', keys %get_details ) {
221 0           $pms->{'imageinfo'}->{"pc_$type"} = 0;
222             $pms->{'imageinfo'}->{"count_$type"} = 0;
223 0           }
224 0            
225 0           foreach my $p ($pms->{msg}->find_parts(qr@^image/(?:gif|png|jpe?g)$@, 1)) {
226             # make sure its base64 encoded
227             my $cte = lc($p->get_header('content-transfer-encoding') || '');
228 0           next if ($cte !~ /^base64$/);
229              
230 0   0       my ($type) = $p->{'type'} =~ m@/(\w+)$@;
231 0 0         $type = 'jpeg' if $type eq 'jpg';
232             if ($type && exists $get_details{$type}) {
233 0           $get_details{$type}->($pms,$p);
234 0 0         $pms->{'imageinfo'}->{"count_$type"} ++;
235 0 0 0       }
236 0           }
237 0            
238             foreach my $name ( keys %{$pms->{'imageinfo'}->{"names_all"}} ) {
239             dbg("imageinfo: image name $name found");
240             }
241 0            
  0            
242 0           foreach my $type ( keys %get_details ) {
243             $pms->{'imageinfo'}->{'pc_all'} += $pms->{'imageinfo'}->{"pc_$type"};
244             $pms->{'imageinfo'}->{'count_all'} += $pms->{'imageinfo'}->{"count_$type"};
245 0           foreach my $dem ( keys %{$pms->{'imageinfo'}->{"dems_$type"}} ) {
246 0           dbg("imageinfo: adding $dem to dems_all");
247 0           $pms->{'imageinfo'}->{'dems_all'}->{$dem} = 1;
248 0           }
  0            
249 0           }
250 0           }
251              
252             # -----------------------------------------
253              
254             my ($self,$pms,$body,$name) = @_;
255             return unless (defined $name);
256              
257             # make sure we have image data read in.
258 0     0 0   if (!exists $pms->{'imageinfo'}) {
259 0 0         $self->_get_images($pms);
260             }
261              
262 0 0         return 0 unless (exists $pms->{'imageinfo'}->{"names_all"});
263 0           return 1 if (exists $pms->{'imageinfo'}->{"names_all"}->{$name});
264             return 0;
265             }
266 0 0          
267 0 0         # -----------------------------------------
268 0            
269             my ($self,$pms,$body,$re) = @_;
270             return unless (defined $re);
271              
272             # make sure we have image data read in.
273             if (!exists $pms->{'imageinfo'}) {
274 0     0 0   $self->_get_images($pms);
275 0 0         }
276              
277             return 0 unless (exists $pms->{'imageinfo'}->{"names_all"});
278 0 0          
279 0           my $hit = 0;
280             foreach my $name (keys %{$pms->{'imageinfo'}->{"names_all"}}) {
281             dbg("imageinfo: checking image named $name against regex $re");
282 0 0         if (eval { $name =~ /$re/ }) { $hit = 1 }
283             dbg("imageinfo: error in regex /$re/ - $@") if $@;
284 0           if ($hit) {
285 0           dbg("imageinfo: image_name_regex hit on $name");
  0            
286 0           return 1;
287 0 0         }
  0            
  0            
288 0 0         }
289 0 0         return 0;
290 0            
291 0           }
292              
293             # -----------------------------------------
294 0            
295             my ($self,$pms,$body,$type,$min,$max) = @_;
296              
297             return unless defined $min;
298              
299             # make sure we have image data read in.
300             if (!exists $pms->{'imageinfo'}) {
301 0     0 0   $self->_get_images($pms);
302             }
303 0 0          
304             # dbg("imageinfo: count: $min, ".($max ? $max:'').", $type, ".$pms->{'imageinfo'}->{"count_$type"});
305             return result_check($min, $max, $pms->{'imageinfo'}->{"count_$type"});
306 0 0         }
307 0            
308             # -----------------------------------------
309              
310             my ($self,$pms,$body,$type,$min,$max) = @_;
311 0            
312             return unless (defined $type && defined $min);
313              
314             # make sure we have image data read in.
315             if (!exists $pms->{'imageinfo'}) {
316             $self->_get_images($pms);
317 0     0 0   }
318              
319 0 0 0       # dbg("imageinfo: pc_$type: $min, ".($max ? $max:'').", $type, ".$pms->{'imageinfo'}->{"pc_$type"});
320             return result_check($min, $max, $pms->{'imageinfo'}->{"pc_$type"});
321             }
322 0 0          
323 0           # -----------------------------------------
324              
325             my ($self,$pms,$body,$type,$min,$max) = @_;
326             return unless (defined $type && defined $min && defined $max);
327 0            
328             # make sure we have image data read in.
329             if (!exists $pms->{'imageinfo'}) {
330             $self->_get_images($pms);
331             }
332              
333 0     0 0   # depending on how you call this eval (body vs rawbody),
334 0 0 0       # the $textlen will differ.
      0        
335             my $textlen = length(join('',@$body));
336              
337 0 0         return 0 unless ( $textlen > 0 && exists $pms->{'imageinfo'}->{"pc_$type"} && $pms->{'imageinfo'}->{"pc_$type"} > 0);
338 0            
339             my $ratio = $textlen / $pms->{'imageinfo'}->{"pc_$type"};
340             dbg("imageinfo: image ratio=$ratio, min=$min max=$max");
341             return result_check($min, $max, $ratio, 1);
342             }
343 0            
344             # -----------------------------------------
345 0 0 0        
      0        
346             my ($self,$pms,$body,$type,$height,$width) = @_;
347 0           return unless (defined $type && defined $height && defined $width);
348 0            
349 0           # make sure we have image data read in.
350             if (!exists $pms->{'imageinfo'}) {
351             $self->_get_images($pms);
352             }
353              
354             return 0 unless (exists $pms->{'imageinfo'}->{"dems_$type"});
355 0     0 0   return 1 if (exists $pms->{'imageinfo'}->{"dems_$type"}->{"${height}x${width}"});
356 0 0 0       return 0;
      0        
357             }
358              
359 0 0         # -----------------------------------------
360 0            
361             my ($self,$pms,$body,$type,$minh,$minw,$maxh,$maxw) = @_;
362             return unless (defined $type && defined $minh && defined $minw);
363 0 0          
364 0 0         # make sure we have image data read in.
365 0           if (!exists $pms->{'imageinfo'}) {
366             $self->_get_images($pms);
367             }
368              
369             my $name = 'dems_'.$type;
370             return unless (exists $pms->{'imageinfo'}->{$name});
371 0     0 0    
372 0 0 0       foreach my $dem ( keys %{$pms->{'imageinfo'}->{"dems_$type"}}) {
      0        
373             my ($h,$w) = split(/x/,$dem);
374             next if ($h < $minh); # height less than min height
375 0 0         next if ($w < $minw); # width less than min width
376 0           next if (defined $maxh && $h > $maxh); # height more than max height
377             next if (defined $maxw && $w > $maxw); # width more than max width
378              
379 0           # if we make it here, we have a match
380 0 0         return 1;
381             }
382 0            
  0            
383 0           return 0;
384 0 0         }
385 0 0          
386 0 0 0       # -----------------------------------------
387 0 0 0        
388             my ($min, $max, $value, $nomaxequal) = @_;
389             return 0 unless defined $value;
390 0           return 0 if ($value < $min);
391             return 0 if (defined $max && $value > $max);
392             return 0 if (defined $nomaxequal && $nomaxequal && $value == $max);
393 0           return 1;
394             }
395              
396             # -----------------------------------------
397              
398             1;