our
@ISA
=
qw(Mail::SpamAssassin::Plugin)
;
my
$IPV4_ADDRESS
= IPV4_ADDRESS;
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(
"check_for_numeric_helo"
);
$self
->register_eval_rule(
"check_for_illegal_ip"
);
$self
->register_eval_rule(
"check_all_trusted"
);
$self
->register_eval_rule(
"check_no_relays"
);
$self
->register_eval_rule(
"check_relays_unparseable"
);
$self
->register_eval_rule(
"check_for_sender_no_reverse"
);
$self
->register_eval_rule(
"check_for_from_domain_in_received_headers"
,
$Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS
);
$self
->register_eval_rule(
"check_for_forged_received_trail"
);
$self
->register_eval_rule(
"check_for_forged_received_ip_helo"
);
$self
->register_eval_rule(
"helo_ip_mismatch"
);
$self
->register_eval_rule(
"check_for_no_rdns_dotcom_helo"
);
return
$self
;
}
sub
hostname_to_domain {
my
(
$hostname
) =
@_
;
if
(
$hostname
!~ /[a-zA-Z]/) {
return
$hostname
; }
my
@parts
=
split
(/\./,
$hostname
);
if
(
@parts
> 1 &&
$parts
[-1] =~ /(?:\S{3,}|ie|fr|de)/) {
return
join
(
'.'
,
@parts
[-2..-1]);
}
elsif
(
@parts
> 2) {
return
join
(
'.'
,
@parts
[-3..-1]);
}
else
{
return
$hostname
;
}
}
sub
_helo_forgery_welcomelisted {
my
(
$helo
,
$rdns
) =
@_
;
if
(
$helo
eq
'msn.com'
&&
$rdns
eq
'hotmail.com'
) {
return
1; }
0;
}
sub
check_for_numeric_helo {
my
(
$self
,
$pms
) =
@_
;
my
$rcvd
=
$pms
->{relays_untrusted_str};
if
(
$rcvd
) {
local
$1;
if
(
$rcvd
=~ /\bhelo=(
$IPV4_ADDRESS
)(?=[\000-\040,;\[()<>]|\z)/i
&& $1 !~ IS_IP_PRIVATE) {
return
1;
}
}
return
0;
}
sub
check_for_illegal_ip {
my
(
$self
,
$pms
) =
@_
;
dbg(
'eval: the "check_for_illegal_ip" eval rule no longer available, '
.
'please update your rules'
);
return
0;
}
sub
helo_ip_mismatch {
my
(
$self
,
$pms
) =
@_
;
for
my
$relay
(@{
$pms
->{relays_untrusted}}) {
next
unless
(
$relay
->{helo} =~ IS_IPV4_ADDRESS &&
$relay
->{helo} !~ IS_IP_PRIVATE);
return
1
if
(
$relay
->{ip} =~ IS_IPV4_ADDRESS &&
$relay
->{ip} !~ IS_IP_PRIVATE &&
$relay
->{helo} ne
$relay
->{ip} &&
$relay
->{helo} =~ /^(\d+\.\d+\.\d+\.)/ &&
index
(
$relay
->{ip}, $1) != 0);
}
return
0;
}
sub
check_all_trusted {
my
(
$self
,
$pms
) =
@_
;
return
$pms
->{num_relays_trusted}
&& !
$pms
->{num_relays_untrusted}
&& !
$pms
->{num_relays_unparseable};
}
sub
check_no_relays {
my
(
$self
,
$pms
) =
@_
;
return
!
$pms
->{num_relays_trusted}
&& !
$pms
->{num_relays_untrusted}
&& !
$pms
->{num_relays_unparseable};
}
sub
check_relays_unparseable {
my
(
$self
,
$pms
) =
@_
;
return
$pms
->{num_relays_unparseable} ? 1 : 0;
}
sub
check_for_sender_no_reverse {
my
(
$self
,
$pms
) =
@_
;
my
$srcvd
=
$pms
->{relays_untrusted}->
[
$pms
->{num_relays_untrusted} - 1];
return
0
unless
(
defined
$srcvd
);
return
0
unless
(
$srcvd
->{rdns} =~ /\./);
return
0
if
(
$srcvd
->{ip_private});
return
1;
}
sub
check_for_from_domain_in_received_headers {
my
(
$self
,
$pms
,
$domain
,
$desired
) =
@_
;
if
(
exists
$pms
->{from_domain_in_received}) {
if
(
exists
$pms
->{from_domain_in_received}->{
$domain
}) {
if
(
$desired
eq
'true'
) {
return
int
(
$pms
->{from_domain_in_received}->{
$domain
});
}
else
{
return
!
$pms
->{from_domain_in_received}->{
$domain
};
}
}
}
else
{
$pms
->{from_domain_in_received} = {};
}
my
$from
=
$pms
->get(
'From:addr'
);
if
(
$from
!~ /\b\Q
$domain
\E/i) {
$pms
->{from_domain_in_received}->{
$domain
} =
'0e0'
;
return
0;
}
my
$rcvd
=
$pms
->{relays_trusted_str}.
"\n"
.
$pms
->{relays_untrusted_str};
if
(
$rcvd
=~ / rdns=\S*\b${domain} [^\]]
*by
=\S*\b${domain} /) {
$pms
->{from_domain_in_received}->{
$domain
} = 1;
return
(
$desired
eq
'true'
);
}
$pms
->{from_domain_in_received}->{
$domain
} = 0;
return
(
$desired
ne
'true'
);
}
sub
check_for_no_rdns_dotcom_helo {
my
(
$self
,
$pms
) =
@_
;
if
(!
exists
$pms
->{no_rdns_dotcom_helo}) {
$self
->_check_received_helos(
$pms
); }
return
$pms
->{no_rdns_dotcom_helo} ? 1 : 0;
}
sub
_check_received_helos {
my
(
$self
,
$pms
) =
@_
;
for
(
my
$i
= 0;
$i
<
$pms
->{num_relays_untrusted};
$i
++) {
my
$rcvd
=
$pms
->{relays_untrusted}->[
$i
];
next
if
(
$rcvd
->{ip_private});
my
$from_host
=
$rcvd
->{rdns};
my
$helo_host
=
$rcvd
->{helo};
my
$by_host
=
$rcvd
->{by};
my
$no_rdns
=
$rcvd
->{no_reverse_dns};
next
unless
defined
(
$helo_host
);
$pms
->{no_rdns_dotcom_helo} = 0;
if
(
$helo_host
=~ /(?:\.|^)(lycos\.com|lycos\.co\.uk|hotmail\.com
|localhost\.com|excite\.com|caramail\.com
|cs\.com|aol\.com|msn\.com|yahoo\.com|drizzle\.com)$/ix)
{
my
$dom
= $1;
if
(
$no_rdns
) {
dbg2(
"eval: Received: no rDNS for dotcom HELO: from=$from_host HELO=$helo_host"
);
$pms
->{no_rdns_dotcom_helo} = 1;
}
}
}
}
sub
check_for_forged_received_trail {
my
(
$self
,
$pms
) =
@_
;
$self
->_check_for_forged_received(
$pms
)
unless
exists
$pms
->{mismatch_from};
return
(
$pms
->{mismatch_from} > 1);
}
sub
check_for_forged_received_ip_helo {
my
(
$self
,
$pms
) =
@_
;
$self
->_check_for_forged_received(
$pms
)
unless
exists
$pms
->{mismatch_ip_helo};
return
(
$pms
->{mismatch_ip_helo} > 0);
}
sub
_check_for_forged_received {
my
(
$self
,
$pms
) =
@_
;
$pms
->{mismatch_from} = 0;
$pms
->{mismatch_ip_helo} = 0;
my
$IP_PRIVATE
= IP_PRIVATE;
my
@fromip
=
map
{
$_
->{ip} } @{
$pms
->{relays_untrusted}};
my
@by
=
map
{
hostname_to_domain (
$_
->{lc_by});
} @{
$pms
->{relays_untrusted}};
my
@from
=
map
{
hostname_to_domain (
$_
->{lc_rdns});
} @{
$pms
->{relays_untrusted}};
my
@helo
=
map
{
hostname_to_domain (
$_
->{lc_helo});
} @{
$pms
->{relays_untrusted}};
for
(
my
$i
= 0;
$i
<
$pms
->{num_relays_untrusted};
$i
++) {
next
if
(!
defined
$by
[
$i
] ||
$by
[
$i
] !~ /^\w+(?:[\w.-]+\.)+\w+$/);
if
(
defined
(
$from
[
$i
]) &&
defined
(
$fromip
[
$i
])) {
if
(
$from
[
$i
] =~ /^localhost(?:\.localdomain)?$/) {
if
(
$fromip
[
$i
] eq
'127.0.0.1'
) {
$from
[
$i
] =
undef
;
}
}
}
my
$frm
=
$from
[
$i
];
my
$hlo
=
$helo
[
$i
];
my
$by
=
$by
[
$i
];
dbg2(
"eval: forged-HELO: from="
.(
defined
$frm
?
$frm
:
"(undef)"
).
" helo="
.(
defined
$hlo
?
$hlo
:
"(undef)"
).
" by="
.(
defined
$by
?
$by
:
"(undef)"
));
next
unless
(
$by
=~ /^\w+(?:[\w.-]+\.)+\w+$/);
my
$fip
=
$fromip
[
$i
];
if
(
defined
(
$hlo
) &&
defined
(
$fip
)) {
if
(
$hlo
=~ /^\d+\.\d+\.\d+\.\d+$/
&&
$fip
=~ /^\d+\.\d+\.\d+\.\d+$/
&&
$fip
ne
$hlo
)
{
$hlo
=~ /^(\d+\.\d+)\.\d+\.\d+$/;
my
$hclassb
= $1;
$fip
=~ /^(\d+\.\d+)\.\d+\.\d+$/;
my
$fclassb
= $1;
if
(
$hclassb
&&
$fclassb
&&
$hclassb
ne
$fclassb
&&
$hlo
!~ IS_IP_PRIVATE)
{
dbg2(
"eval: forged-HELO: massive mismatch on IP-addr HELO: '$hlo' != '$fip'"
);
$pms
->{mismatch_ip_helo}++;
}
}
}
my
$prev
=
$from
[
$i
-1];
if
(
defined
(
$prev
) &&
$i
> 0
&&
$prev
=~ /^\w+(?:[\w.-]+\.)+\w+$/
&&
$by
ne
$prev
&& !_helo_forgery_welcomelisted(
$by
,
$prev
))
{
dbg2(
"eval: forged-HELO: mismatch on from: '$prev' != '$by'"
);
$pms
->{mismatch_from}++;
}
}
}
sub
dbg2 {
if
(would_log(
'dbg'
,
'eval'
) == 2) {
dbg(
@_
);
}
}
1;