5
5
# The ASF licenses this file to you under the Apache License, Version 2.0
6
6
# (the "License"); you may not use this file except in compliance with
7
7
# the License. You may obtain a copy of the License at:
9
9
# http://www.apache.org/licenses/LICENSE-2.0
11
11
# Unless required by applicable law or agreed to in writing, software
12
12
# distributed under the License is distributed on an "AS IS" BASIS,
13
13
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18
18
# -------------------------------------------------------
19
# ImageInfo Plugin for SpamAssassin
22
# Modified: 2007-01-17
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
23
# body RULENAME eval:image_count(<type>,<min>,[max])
24
# type: 'all','gif','png', or 'jpeg'
25
# min: required, message contains at least this
39
# body RULENAME eval:image_count(<type>,<min>,[max])
40
# type: 'all','gif','png', or 'jpeg'
41
# min: required, message contains at least this
27
# max: optional, if specified, message must not
43
# max: optional, if specified, message must not
28
44
# contain more than this number of images
32
# body ONE_IMAGE eval:image_count('all',1,1)
46
# image_count() examples
48
# body ONE_IMAGE eval:image_count('all',1,1)
33
49
# body ONE_OR_MORE_IMAGES eval:image_count('all',1)
34
50
# body ONE_PNG eval:image_count('png',1,1)
35
51
# body TWO_GIFS eval:image_count('gif',2,2)
44
60
# max: optional, if specified, message must not
45
61
# contain more than this much pixel area
49
# body LARGE_IMAGE_AREA eval:pixel_coverage('all',150000)
50
# body SMALL_GIF_AREA eval:pixel_coverage('gif',1,40000)
52
# See the ruleset for ways to meta image_count()
53
# and pixel_coverage() together.
63
# pixel_coverage() examples
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
70
# body RULENAME eval:image_name_regex(<regex>)
71
# regex: full quoted regexp, see examples below
73
# image_name_regex() examples
75
# body CG_DOUBLEDOT_GIF eval:image_name_regex('/^\w{2,9}\.\.gif$/i') # catches double dot gifs abcd..gif
55
79
# -------------------------------------------------------
75
99
$class = ref($class) || $class;
76
100
my $self = $class->SUPER::new($mailsaobject);
77
101
bless ($self, $class);
79
103
$self->register_eval_rule ("image_count");
80
104
$self->register_eval_rule ("pixel_coverage");
81
105
$self->register_eval_rule ("image_size_exact");
82
106
$self->register_eval_rule ("image_size_range");
83
107
$self->register_eval_rule ("image_named");
108
$self->register_eval_rule ("image_name_regex");
84
109
$self->register_eval_rule ("image_to_text_ratio");
105
130
#my $has_global_color_table = $global_color_table ? 1 : 0;
106
131
#my $sorted_colors = ($packed & 0x08)?1:0;
107
132
#my $resolution = ((($packed & 0x70) >> 4) + 1);
109
134
if ($height && $width) {
110
135
my $area = $width * $height;
111
136
$pms->{imageinfo}->{pc_gif} += $area;
112
137
$pms->{imageinfo}->{dems_gif}->{"${height}x${width}"} = 1;
113
138
$pms->{imageinfo}->{names_all}->{$part->{'name'}} = 1 if $part->{'name'};
114
dbg("imageinfo: gif image ".($part->{'name'} ? $part->{'name'} : '')." is $height x $width pixels ($area pixels sq.), with $color_table_size color table");
139
dbg("imageinfo: gif image ".($part->{'name'} ? $part->{'name'} : '')." is $height x $width pixels ($area pixels sq.), with $color_table_size color table");
126
151
my $chunksize = 8;
127
152
my ($width, $height) = ( 0, 0 );
128
153
my ($depth, $ctype, $compression, $filter, $interlace);
130
155
while ($pos < $datalen) {
131
156
my ($len, $type) = unpack("Na4", substr($data, $pos, $chunksize));
132
157
$pos += $chunksize;
134
159
last if $type eq "IEND"; # end of png image.
136
161
next unless ( $type eq "IHDR" && $len == 13 );
138
163
my $bytes = substr($data, $pos, $len + 4);
139
164
my $crc = unpack("N", substr($bytes, -4, 4, ""));
183
208
if ($height && $width) {
184
my $area = $height * $width;
209
my $area = $height * $width;
185
210
$pms->{imageinfo}->{pc_jpeg} += $area;
186
211
$pms->{imageinfo}->{dems_jpeg}->{"${height}x${width}"} = 1;
187
212
$pms->{imageinfo}->{names_all}->{$part->{'name'}} = 1 if $part->{'name'};
201
226
$pms->{'imageinfo'}->{"count_$type"} = 0;
204
foreach my $p ($pms->{msg}->find_parts(qr@^image/(?:gif|png|jpe?g)$@, 1)) {
229
foreach my $p ($pms->{msg}->find_parts(qr@^image/(?:gif|png|jpeg)$@, 1)) {
205
230
# make sure its base64 encoded
206
231
my $cte = lc $p->get_header('content-transfer-encoding') || '';
207
232
next if ($cte !~ /^base64$/);
209
234
my ($type) = $p->{'type'} =~ m@/(\w+)$@;
210
$type='jpeg' if $type eq 'jpg';
211
235
if ($type && exists $get_details{$type}) {
212
236
$get_details{$type}->($pms,$p);
213
237
$pms->{'imageinfo'}->{"count_$type"} ++;
247
271
# -----------------------------------------
273
sub image_name_regex {
274
my ($self,$pms,$body,$re) = @_;
275
return unless (defined $re);
277
# make sure we have image data read in.
278
if (!exists $pms->{'imageinfo'}) {
279
$self->_get_images($pms);
282
return 0 unless (exists $pms->{'imageinfo'}->{"names_all"});
285
foreach my $name (keys %{$pms->{'imageinfo'}->{"names_all"}}) {
286
dbg("imageinfo: checking image named $name against regex $re");
287
if (eval { $name =~ /$re/ }) { $hit = 1 }
288
dbg("imageinfo: error in regex /$re/ - $@") if $@;
290
dbg("imageinfo: image_name_regex hit on $name");
298
# -----------------------------------------
249
300
sub image_count {
250
301
my ($self,$pms,$body,$type,$min,$max) = @_;
252
303
return unless defined $min;
254
305
# make sure we have image data read in.
271
322
if (!exists $pms->{'imageinfo'}) {
272
323
$self->_get_images($pms);
275
326
# dbg("imageinfo: pc_$type: $min, ".($max ? $max:'').", $type, ".$pms->{'imageinfo'}->{"pc_$type"});
276
327
return result_check($min, $max, $pms->{'imageinfo'}->{"pc_$type"});
287
338
$self->_get_images($pms);
290
# depending on how you call this eval (body vs rawbody),
341
# depending on how you call this eval (body vs rawbody),
291
342
# the $textlen will differ.
292
343
my $textlen = length(join('',@$body));
294
345
return 0 unless ( $textlen > 0 && exists $pms->{'imageinfo'}->{"pc_$type"} && $pms->{'imageinfo'}->{"pc_$type"} > 0);
296
347
my $ratio = $textlen / $pms->{'imageinfo'}->{"pc_$type"};
297
348
dbg("imageinfo: image ratio=$ratio, min=$min max=$max");
298
349
return result_check($min, $max, $ratio, 1);
325
376
$self->_get_images($pms);
328
return unless (exists $pms->{'imageinfo'}->{"dems_$type"});
379
my $name = 'dems_'.$type;
380
return unless (exists $pms->{'imageinfo'}->{$name});
330
382
foreach my $dem ( keys %{$pms->{'imageinfo'}->{"dems_$type"}}) {
331
383
my ($h,$w) = split(/x/,$dem);