our
(
%conf_backup
,
%msa_backup
);
our
$SPAMD_VER
=
'1.4'
;
our
%resphash
= (
EX_OK
=> 0,
EX_USAGE
=> 64,
EX_DATAERR
=> 65,
EX_NOINPUT
=> 66,
EX_NOUSER
=> 67,
EX_NOHOST
=> 68,
EX_UNAVAILABLE
=> 69,
EX_SOFTWARE
=> 70,
EX_OSERR
=> 71,
EX_OSFILE
=> 72,
EX_CANTCREAT
=> 73,
EX_IOERR
=> 74,
EX_TEMPFAIL
=> 75,
EX_PROTOCOL
=> 76,
EX_NOPERM
=> 77,
EX_CONFIG
=> 78,
EX_TIMEOUT
=> 79,
);
sub
log_connection {
my
(
$self
) =
@_
;
info(
sprintf
"connection from %s [%s] at port %s\n"
,
$self
->_remote_host,
$self
->_remote_ip,
$self
->_remote_port);
}
sub
log_start_work {
my
(
$self
) =
@_
;
info(
sprintf
"%s message %s%s for %s:%d\n"
,
(
$self
->{method} eq
'PROCESS'
?
'processing'
:
'checking'
),
(
defined
$self
->{msgid} ?
$self
->{msgid} :
'(unknown)'
),
(
defined
$self
->{rmsgid} ?
'aka '
.
$self
->{rmsgid} :
''
),
$self
->user,
$>,
);
}
sub
log_end_work {
my
(
$self
) =
@_
;
if
(
$self
->{method} eq
'TELL'
) {
my
$info_str
;
$info_str
.=
'Setting'
.
join
','
, @{
$self
->{did_set} }
if
@{
$self
->{did_set} };
$info_str
.=
'Removing'
.
join
','
, @{
$self
->{did_remove} }
if
@{
$self
->{did_remove} };
info(
sprintf
'spamd: Tell: %s for $current_user:%d in'
.
' %.1f seconds, %d bytes'
,
(
defined
$info_str
?
$info_str
:
'Did nothing'
),
$>,
$self
->{scan_time},
$self
->{actual_length},
);
}
else
{
info(
sprintf
"%s (%.1f/%.1f) for %s:%d in %.1f seconds, %d bytes.\n"
,
(
$self
->status->is_spam ?
'identified spam'
:
'clean message'
),
$self
->status->get_score,
$self
->status->get_required_score,
$self
->user,
$>,
$self
->{scan_time},
$self
->{actual_length},
);
}
}
sub
log_result {
my
(
$self
) =
@_
;
my
@extra
= (
'scantime='
.
sprintf
(
'%.1f'
,
$_
[0]->{scan_time}),
'size='
.
$self
->{actual_length},
'user='
.
$self
->user,
'uid='
. $>,
'required_score='
.
$self
->status->get_required_score,
'rhost='
.
$self
->_remote_host,
'raddr='
.
$self
->_remote_ip,
'rport='
.
$self
->_remote_port,
);
{
(
my
$safe
=
defined
$self
->{msgid} ?
$self
->{msgid} :
'(unknown)'
) =~
s/[\x00-\x20\s,]/_/gs;
push
@extra
,
"mid=$safe"
;
}
if
(
$self
->{rmsgid}) {
(
my
$safe
=
$self
->{rmsgid}) =~ s/[\x00-\x20\s,]/_/gs;
push
@extra
,
"rmid=$safe"
;
}
push
@extra
,
"bayes="
.
$self
->status->{bayes_score}
if
defined
$self
->status->{bayes_score};
push
@extra
,
"autolearn="
.
$self
->status->get_autolearn_status;
my
$yorn
=
$self
->status->is_spam ?
'Y'
:
'.'
;
my
$tests
=
join
","
,
sort
grep
length
,
$self
->status->get_names_of_tests_hit;
access_info(
sprintf
"result: %s %2d - %s %s\n"
,
$yorn
,
$self
->status->get_score,
$tests
,
join
','
,
@extra
);
}
sub
check_headers {
my
$self
=
shift
;
if
(
$self
->cfg->{auth_ident}) {
unless
(
exists
$self
->headers_in->{user}) {
$self
->service_unavailable_error(
'User header required'
);
return
0;
}
$self
->auth_ident(
$self
->headers_in->{user})
or
return
0;
}
my
$content_length
=
$self
->headers_in->{content_length};
if
(
defined
$content_length
) {
if
(
$content_length
!~ /^\d{1,15}$/
||
$content_length
== 0)
{
$self
->protocol_error(
'Content-Length too ugly'
);
return
0;
}
elsif
(
$self
->cfg->{msg_size_limit}
&&
$content_length
>
$self
->cfg->{msg_size_limit})
{
$self
->service_unavailable_error(
'Content-Length exceeds limit'
);
return
0;
}
}
if
(
$self
->cfg->{allow_tell} &&
$self
->{method} eq
'TELL'
) {
my
(
$set_local
,
$set_remote
,
$remove_local
,
$remove_remote
) = (
$self
->headers_in->{set} =~ /
local
/,
$self
->headers_in->{set} =~ /remote/,
$self
->headers_in->{remove} =~ /
local
/,
$self
->headers_in->{remove} =~ /remote/,
);
if
(
$set_local
&&
$remove_local
) {
$self
->protocol_error(
"Unable to set local and remove local in the same operation."
);
return
0;
}
if
(
$set_remote
&&
$remove_remote
) {
$self
->protocol_error(
"Unable to set remote and remove remote in the same operation."
);
return
0;
}
}
if
(
$self
->headers_in->{compress} &&
length
$self
->headers_in->{compress}) {
$self
->protocol_error(
'Compress not supported yet'
);
return
0;
}
1;
}
sub
parse_msgids {
my
$self
=
shift
;
$self
->{msgid} =
$self
->{parsed}->get_pristine_header(
"Message-Id"
);
$self
->{rmsgid} =
$self
->{parsed}->get_pristine_header(
"Resent-Message-Id"
);
foreach
my
$id
(
grep
$self
->{
$_
},
qw(msgid rmsgid)
) {
1
while
$self
->{
$id
} =~ s/\([^\(\)]*\)//;
$self
->{
$id
} =~ s/^\s+|\s+$//g;
$self
->{
$id
} =~ s/\s+/ /g;
$self
->{
$id
} =~ s/^.*?<(.*?)>.*$/$1/;
$self
->{
$id
} =~ s/[^\x21-\x7e]/?/g;
$self
->{
$id
} =~ s/[<>]/?/g;
$self
->{
$id
} =~ s/^(.+)$/<$1>/;
}
}
sub
service_unavailable_error {
my
$self
=
shift
;
my
$msg
=
join
''
,
@_
;
$self
->send_status_line(
'EX_UNAVAILABLE'
,
$msg
);
warn
"spamd: service unavailable: $msg\n"
;
}
sub
protocol_error {
my
$self
=
shift
;
my
$msg
=
join
''
,
@_
;
$self
->send_status_line(
'EX_PROTOCOL'
,
$msg
);
warn
"spamd: bad protocol: header error: $msg\n"
;
}
sub
service_timeout {
my
$self
=
shift
;
my
$msg
=
join
''
,
@_
;
$self
->send_status_line(
'EX_TIMEOUT'
,
$msg
);
warn
"spamd: timeout: $msg\n"
;
}
sub
send_status_line {
my
$self
=
shift
;
my
(
$resp
,
$msg
) =
@_
;
$resp
=
defined
$resp
?
$resp
:
'EX_OK'
;
$msg
=
defined
$msg
?
$msg
:
$resp
;
$self
->send_buffer(
"SPAMD/$SPAMD_VER $resphash{$resp} $msg\r\n"
);
}
sub
send_response {
my
$self
=
shift
;
my
$msg_resp
=
''
;
if
(
$self
->{method} eq
'PROCESS'
or
$self
->{method} eq
'HEADERS'
) {
$self
->status->set_tag(
'REMOTEHOSTNAME'
,
$self
->_remote_host);
$self
->status->set_tag(
'REMOTEHOSTADDR'
,
$self
->_remote_ip);
$msg_resp
=
$self
->status->rewrite_mail;
if
(
$self
->{method} eq
'HEADERS'
) {
$msg_resp
=~ s/(\015?\012\015?\012).*$/$1/s;
}
open
my
$dfh
,
'>'
,
'/tmp/sadebug'
or
die
$!;
print
$dfh
$msg_resp
;
close
$dfh
or
die
$!;
$self
->send_buffer(
$self
->spamhdr)
if
$self
->{client_version} >= 1.3;
$self
->send_buffer(
'Content-length: '
.
length
(
$msg_resp
) .
"\r\n\r\n"
)
if
$self
->{client_version} >= 1.2;
}
elsif
(
$self
->{method} eq
'TELL'
) {
my
$response
;
$response
.=
'DidSet: '
.
join
(
','
, @{
$self
->{did_set} }) .
"\r\n"
if
@{
$self
->{did_set} };
$response
.=
'DidRemove: '
.
join
(
','
, @{
$self
->{did_remove} }) .
"\r\n"
if
@{
$self
->{did_remove} };
$self
->send_buffer(
$response
,
"Content-Length: 0\r\n"
,
"\r\n"
);
}
else
{
if
(
$self
->{method} eq
'CHECK'
) {
}
elsif
(
$self
->{method} eq
'REPORT'
or
$self
->{method} eq
'REPORT_IFSPAM'
&&
$self
->status->is_spam)
{
$msg_resp
=
$self
->status->get_report;
}
elsif
(
$self
->{method} eq
'REPORT_IFSPAM'
) {
}
elsif
(
$self
->{method} eq
'SYMBOLS'
) {
$msg_resp
=
$self
->status->get_names_of_tests_hit;
$msg_resp
.=
"\r\n"
if
$self
->{client_version} < 1.3;
}
else
{
die
"spamd: unknown method '$self->{method}'"
;
}
$self
->send_buffer(
'Content-length: '
.
length
(
$msg_resp
) .
"\r\n"
)
if
$self
->{client_version} >= 1.3;
$self
->send_buffer(
$self
->spamhdr,
"\r\n"
);
}
$self
->send_buffer(
$msg_resp
);
$self
->{scan_time} =
time
-
$self
->{start_time};
}
sub
pass_through_sa {
my
$self
=
shift
;
if
(
$self
->{method} eq
'TELL'
) {
if
(
$self
->{parsed}->get_header(
"X-Spam-Checker-Version"
)) {
my
$new_mail
=
$self
->spamtest->parse(
$self
->spamtest->remove_spamassassin_markup(
$self
->{parsed}), 1);
$self
->{parsed}->finish;
$self
->{parsed} =
$new_mail
;
}
my
(
$set_local
,
$set_remote
,
$remove_local
,
$remove_remote
) = (
$self
->headers_in->{set} =~ /
local
/,
$self
->headers_in->{set} =~ /remote/,
$self
->headers_in->{remove} =~ /
local
/,
$self
->headers_in->{remove} =~ /remote/,
);
if
(
$set_local
) {
my
$status
=
$self
->spamtest->learn(
$mail
,
undef
,
(
$self
->headers_in->{message_class} eq
'spam'
? 1 : 0), 0);
push
@{
$self
->{did_set} },
'local'
if
$status
->did_learn;
$status
->finish;
}
if
(
$remove_local
) {
my
$status
=
$self
->spamtest->learn(
$mail
,
undef
,
undef
, 1);
push
@{
$self
->{did_remove} },
'local'
if
$status
->did_learn;
$status
->finish;
}
if
(
$set_remote
) {
my
$msgrpt
=
Mail::SpamAssassin::Reporter->new(
$self
->spamtest,
$self
->{parsed});
push
@{
$self
->{did_set} },
'remote'
if
$msgrpt
->report;
}
if
(
$remove_remote
) {
my
$msgrpt
=
Mail::SpamAssassin::Reporter->new(
$self
->spamtest,
$self
->{parsed});
push
@{
$self
->{did_remove} },
'remote'
if
$msgrpt
->revoke;
}
}
else
{
$self
->{status} =
$self
->spamtest->check(
$self
->{parsed});
}
undef
;
}
sub
spamhdr {
my
$self
=
shift
;
my
$msg_score
=
sprintf
(
'%.1f'
,
$self
->status->get_score);
my
$msg_threshold
=
sprintf
(
'%.1f'
,
$self
->status->get_required_score);
my
$response_spam_status
;
if
(
$self
->status->is_spam) {
$response_spam_status
=
$self
->{method} eq
'REPORT_IFSPAM'
?
'Yes'
:
'True'
;
}
else
{
$response_spam_status
=
$self
->{method} eq
'REPORT_IFSPAM'
?
'No'
:
'False'
;
}
return
"Spam: $response_spam_status ; $msg_score / $msg_threshold\r\n"
;
}
{
my
%mapping
= (
'local'
=>
'handle_user_local'
,
'sql'
=>
'handle_user_sql'
,
'ldap'
=>
'handle_user_ldap'
,
);
sub
read_user_config {
my
$self
=
shift
;
return
unless
defined
$self
->headers_in->{user};
for
my
$src
(
grep
$self
->can(
$_
),
map
{
exists
$mapping
{
$_
} ?
$mapping
{
$_
} :
$_
}
@{
$self
->cfg->{sa_users} }
)
{
my
$ret
=
$self
->
$src
(
$self
->headers_in->{user});
next
unless
$ret
;
$self
->cleanup_register(\
&restore_config
,
$self
->spamtest);
return
$ret
;
}
return
0;
}
}
sub
handle_user_sql {
my
$self
=
shift
;
my
(
$username
) =
@_
;
$self
->spamtest->load_scoreonly_sql(
$username
)
or
return
0;
$self
->spamtest->signal_user_changed({
username
=>
$username
,
user_dir
=>
undef
, });
return
1;
}
sub
handle_user_ldap {
my
$self
=
shift
;
my
(
$username
) =
@_
;
dbg(
"ldap: entering handle_user_ldap($username)"
);
$self
->spamtest->load_scoreonly_ldap(
$username
)
or
return
0;
$self
->spamtest->signal_user_changed({
username
=>
$username
,
user_dir
=>
undef
, });
return
1;
}
sub
status {
$_
[0]->{status} }
sub
spamtest {
$_
[0]->{spamtest} }
sub
access_info { info(
@_
) }
sub
user {
defined
$_
[0]->headers_in->{user} ?
$_
[0]->headers_in->{user} :
'(unknown)'
;
}
sub
cfg {
$_
[0]->{cfg} }
sub
headers_in {
$_
[0]->{headers_in} }
sub
cleanup_register {
my
$self
=
shift
;
$self
->{pool} ||= Mail::SpamAssassin::Pool->new;
$self
->{pool}->cleanup_register(
@_
);
}
sub
backup_config {
my
$spamtest
=
shift
;
for
my
$key
(
qw(username user_dir userstate_dir learn_to_journal)
) {
$msa_backup
{
$key
} =
$spamtest
->{
$key
}
if
exists
$spamtest
->{
$key
};
}
$spamtest
->copy_config(
undef
, \
%conf_backup
)
||
die
"spamd: error returned from copy_config\n"
;
}
sub
restore_config {
my
$spamtest
=
shift
;
for
my
$key
(
keys
%msa_backup
) {
$spamtest
->{
$key
} =
$msa_backup
{
$key
};
}
$spamtest
->copy_config(\
%conf_backup
,
undef
)
||
die
"spamd: error returned from copy_config\n"
;
}
{
local
$@;
}
sub
new {
$INC
{
'APR/Pool.pm'
} ? APR::Pool->new :
bless
[],
shift
;
}
sub
cleanup_register {
my
$self
=
shift
;
push
@$self
, [
@_
];
}
sub
DESTROY {
my
$self
=
shift
;
for
my
$cleaner
(
@$self
) {
(
shift
@$cleaner
)->(
@$cleaner
);
}
}
1;