our
@ISA
=
qw(Mail::SpamAssassin::Plugin)
;
sub
new {
my
$class
=
shift
;
my
$mailsaobject
=
shift
;
$class
=
ref
(
$class
) ||
$class
;
my
$self
=
$class
->SUPER::new(
$mailsaobject
);
bless
(
$self
,
$class
);
$self
->register_eval_rule (
"image_count"
,
$Mail::SpamAssassin::Conf::TYPE_BODY_EVALS
);
$self
->register_eval_rule (
"pixel_coverage"
,
$Mail::SpamAssassin::Conf::TYPE_BODY_EVALS
);
$self
->register_eval_rule (
"image_size_exact"
,
$Mail::SpamAssassin::Conf::TYPE_BODY_EVALS
);
$self
->register_eval_rule (
"image_size_range"
,
$Mail::SpamAssassin::Conf::TYPE_BODY_EVALS
);
$self
->register_eval_rule (
"image_named"
,
$Mail::SpamAssassin::Conf::TYPE_BODY_EVALS
);
$self
->register_eval_rule (
"image_name_regex"
,
$Mail::SpamAssassin::Conf::TYPE_BODY_EVALS
);
$self
->register_eval_rule (
"image_to_text_ratio"
,
$Mail::SpamAssassin::Conf::TYPE_BODY_EVALS
);
return
$self
;
}
my
%get_details
= (
'gif'
=>
sub
{
my
(
$pms
,
$part
) =
@_
;
my
$header
=
$part
->decode(13);
return
unless
$header
=~ s/^GIF(8[79]a)//;
my
$version
= $1;
my
(
$width
,
$height
,
$packed
,
$bgcolor
,
$aspect
) =
unpack
(
"vvCCC"
,
$header
);
my
$color_table_size
= 1 << ((
$packed
& 0x07) + 1);
if
(
$height
&&
$width
) {
my
$area
=
$width
*
$height
;
$pms
->{imageinfo}->{pc_gif} +=
$area
;
$pms
->{imageinfo}->{dems_gif}->{
"${height}x${width}"
} = 1;
$pms
->{imageinfo}->{names_all}->{
$part
->{
'name'
}} = 1
if
$part
->{
'name'
};
dbg(
"imageinfo: gif image "
.(
$part
->{
'name'
} ?
$part
->{
'name'
} :
''
).
" is $height x $width pixels ($area pixels sq.), with $color_table_size color table"
);
}
},
'png'
=>
sub
{
my
(
$pms
,
$part
) =
@_
;
my
$data
=
$part
->decode();
return
unless
(
substr
(
$data
, 0, 8) eq
"\x89PNG\x0d\x0a\x1a\x0a"
);
my
$datalen
=
length
$data
;
my
$pos
= 8;
my
$chunksize
= 8;
my
(
$width
,
$height
) = ( 0, 0 );
my
(
$depth
,
$ctype
,
$compression
,
$filter
,
$interlace
);
while
(
$pos
<
$datalen
) {
my
(
$len
,
$type
) =
unpack
(
"Na4"
,
substr
(
$data
,
$pos
,
$chunksize
));
$pos
+=
$chunksize
;
last
if
$type
eq
"IEND"
;
next
unless
(
$type
eq
"IHDR"
&&
$len
== 13 );
my
$bytes
=
substr
(
$data
,
$pos
,
$len
+ 4);
my
$crc
=
unpack
(
"N"
,
substr
(
$bytes
, -4, 4,
""
));
if
(
$type
eq
"IHDR"
&&
$len
== 13) {
(
$width
,
$height
,
$depth
,
$ctype
,
$compression
,
$filter
,
$interlace
) =
unpack
(
"NNCCCCC"
,
$bytes
);
last
;
}
}
if
(
$height
&&
$width
) {
my
$area
=
$width
*
$height
;
$pms
->{imageinfo}->{pc_png} +=
$area
;
$pms
->{imageinfo}->{dems_png}->{
"${height}x${width}"
} = 1;
$pms
->{imageinfo}->{names_all}->{
$part
->{
'name'
}} = 1
if
$part
->{
'name'
};
dbg(
"imageinfo: png image "
.(
$part
->{
'name'
} ?
$part
->{
'name'
} :
''
).
" is $height x $width pixels ($area pixels sq.)"
);
}
},
'jpeg'
=>
sub
{
my
(
$pms
,
$part
) =
@_
;
my
$data
=
$part
->decode();
my
$index
=
substr
(
$data
, 0, 2);
return
unless
$index
eq
"\xFF\xD8"
;
my
$pos
= 2;
my
$chunksize
= 4;
my
(
$prec
,
$height
,
$width
,
$comps
) = (
undef
,0,0,
undef
);
while
(1) {
my
(
$xx
,
$mark
,
$len
) =
unpack
(
"CCn"
,
substr
(
$data
,
$pos
,
$chunksize
));
last
if
(!
defined
$xx
||
$xx
!= 0xFF);
last
if
(!
defined
$mark
||
$mark
== 0xDA ||
$mark
== 0xD9);
last
if
(!
defined
$len
||
$len
< 2);
$pos
+=
$chunksize
;
my
$block
=
substr
(
$data
,
$pos
,
$len
- 2);
my
$blocklen
=
length
(
$block
);
if
( (
$mark
>= 0xC0 &&
$mark
<= 0xC3) || (
$mark
>= 0xC5 &&
$mark
<= 0xC7) ||
(
$mark
>= 0xC9 &&
$mark
<= 0xCB) || (
$mark
>= 0xCD &&
$mark
<= 0xCF) ) {
(
$prec
,
$height
,
$width
,
$comps
) =
unpack
(
"CnnC"
,
substr
(
$block
, 0, 6,
""
));
last
;
}
$pos
+=
$blocklen
;
}
if
(
$height
&&
$width
) {
my
$area
=
$height
*
$width
;
$pms
->{imageinfo}->{pc_jpeg} +=
$area
;
$pms
->{imageinfo}->{dems_jpeg}->{
"${height}x${width}"
} = 1;
$pms
->{imageinfo}->{names_all}->{
$part
->{
'name'
}} = 1
if
$part
->{
'name'
};
dbg(
"imageinfo: jpeg image "
.(
$part
->{
'name'
} ?
$part
->{
'name'
} :
''
).
" is $height x $width pixels ($area pixels sq.)"
);
}
},
);
sub
_get_images {
my
(
$self
,
$pms
) =
@_
;
my
$result
= 0;
foreach
my
$type
(
'all'
,
keys
%get_details
) {
$pms
->{
'imageinfo'
}->{
"pc_$type"
} = 0;
$pms
->{
'imageinfo'
}->{
"count_$type"
} = 0;
}
foreach
my
$p
(
$pms
->{msg}->find_parts(
qr@^image/(?:gif|png|jpe?g)$@
, 1)) {
my
$cte
=
lc
(
$p
->get_header(
'content-transfer-encoding'
) ||
''
);
next
if
(
$cte
!~ /^base64$/);
my
(
$type
) =
$p
->{
'type'
} =~ m@/(\w+)$@;
$type
=
'jpeg'
if
$type
eq
'jpg'
;
if
(
$type
&&
exists
$get_details
{
$type
}) {
$get_details
{
$type
}->(
$pms
,
$p
);
$pms
->{
'imageinfo'
}->{
"count_$type"
} ++;
}
}
foreach
my
$name
(
keys
%{
$pms
->{
'imageinfo'
}->{
"names_all"
}} ) {
dbg(
"imageinfo: image name $name found"
);
}
foreach
my
$type
(
keys
%get_details
) {
$pms
->{
'imageinfo'
}->{
'pc_all'
} +=
$pms
->{
'imageinfo'
}->{
"pc_$type"
};
$pms
->{
'imageinfo'
}->{
'count_all'
} +=
$pms
->{
'imageinfo'
}->{
"count_$type"
};
foreach
my
$dem
(
keys
%{
$pms
->{
'imageinfo'
}->{
"dems_$type"
}} ) {
dbg(
"imageinfo: adding $dem to dems_all"
);
$pms
->{
'imageinfo'
}->{
'dems_all'
}->{
$dem
} = 1;
}
}
}
sub
image_named {
my
(
$self
,
$pms
,
$body
,
$name
) =
@_
;
return
0
unless
(
defined
$name
);
if
(!
exists
$pms
->{
'imageinfo'
}) {
$self
->_get_images(
$pms
);
}
return
0
unless
(
exists
$pms
->{
'imageinfo'
}->{
"names_all"
});
return
1
if
(
exists
$pms
->{
'imageinfo'
}->{
"names_all"
}->{
$name
});
return
0;
}
sub
image_name_regex {
my
(
$self
,
$pms
,
$body
,
$re
) =
@_
;
return
0
unless
(
defined
$re
);
if
(!
exists
$pms
->{
'imageinfo'
}) {
$self
->_get_images(
$pms
);
}
return
0
unless
(
exists
$pms
->{
'imageinfo'
}->{
"names_all"
});
my
$hit
= 0;
foreach
my
$name
(
keys
%{
$pms
->{
'imageinfo'
}->{
"names_all"
}}) {
dbg(
"imageinfo: checking image named $name against regex $re"
);
if
(
eval
{
$name
=~ /
$re
/ }) {
$hit
= 1 }
dbg(
"imageinfo: error in regex /$re/ - $@"
)
if
$@;
if
(
$hit
) {
dbg(
"imageinfo: image_name_regex hit on $name"
);
return
1;
}
}
return
0;
}
sub
image_count {
my
(
$self
,
$pms
,
$body
,
$type
,
$min
,
$max
) =
@_
;
return
0
unless
defined
$min
;
if
(!
exists
$pms
->{
'imageinfo'
}) {
$self
->_get_images(
$pms
);
}
return
result_check(
$min
,
$max
,
$pms
->{
'imageinfo'
}->{
"count_$type"
});
}
sub
pixel_coverage {
my
(
$self
,
$pms
,
$body
,
$type
,
$min
,
$max
) =
@_
;
return
0
unless
(
defined
$type
&&
defined
$min
);
if
(!
exists
$pms
->{
'imageinfo'
}) {
$self
->_get_images(
$pms
);
}
return
result_check(
$min
,
$max
,
$pms
->{
'imageinfo'
}->{
"pc_$type"
});
}
sub
image_to_text_ratio {
my
(
$self
,
$pms
,
$body
,
$type
,
$min
,
$max
) =
@_
;
return
0
unless
(
defined
$type
&&
defined
$min
&&
defined
$max
);
if
(!
exists
$pms
->{
'imageinfo'
}) {
$self
->_get_images(
$pms
);
}
my
$textlen
=
length
(
join
(
''
,
@$body
));
return
0
unless
(
$textlen
> 0 &&
exists
$pms
->{
'imageinfo'
}->{
"pc_$type"
} &&
$pms
->{
'imageinfo'
}->{
"pc_$type"
} > 0);
my
$ratio
=
$textlen
/
$pms
->{
'imageinfo'
}->{
"pc_$type"
};
dbg(
"imageinfo: image ratio=$ratio, min=$min max=$max"
);
return
result_check(
$min
,
$max
,
$ratio
, 1);
}
sub
image_size_exact {
my
(
$self
,
$pms
,
$body
,
$type
,
$height
,
$width
) =
@_
;
return
0
unless
(
defined
$type
&&
defined
$height
&&
defined
$width
);
if
(!
exists
$pms
->{
'imageinfo'
}) {
$self
->_get_images(
$pms
);
}
return
0
unless
(
exists
$pms
->{
'imageinfo'
}->{
"dems_$type"
});
return
1
if
(
exists
$pms
->{
'imageinfo'
}->{
"dems_$type"
}->{
"${height}x${width}"
});
return
0;
}
sub
image_size_range {
my
(
$self
,
$pms
,
$body
,
$type
,
$minh
,
$minw
,
$maxh
,
$maxw
) =
@_
;
return
0
unless
(
defined
$type
&&
defined
$minh
&&
defined
$minw
);
if
(!
exists
$pms
->{
'imageinfo'
}) {
$self
->_get_images(
$pms
);
}
my
$name
=
'dems_'
.
$type
;
return
0
unless
(
exists
$pms
->{
'imageinfo'
}->{
$name
});
foreach
my
$dem
(
keys
%{
$pms
->{
'imageinfo'
}->{
"dems_$type"
}}) {
my
(
$h
,
$w
) =
split
(/x/,
$dem
);
next
if
(
$h
<
$minh
);
next
if
(
$w
<
$minw
);
next
if
(
defined
$maxh
&&
$h
>
$maxh
);
next
if
(
defined
$maxw
&&
$w
>
$maxw
);
return
1;
}
return
0;
}
sub
result_check {
my
(
$min
,
$max
,
$value
,
$nomaxequal
) =
@_
;
return
0
unless
defined
$value
;
return
0
if
(
$value
<
$min
);
return
0
if
(
defined
$max
&&
$value
>
$max
);
return
0
if
(
defined
$nomaxequal
&&
$nomaxequal
&&
$value
==
$max
);
return
1;
}
1;