#! /usr/bin/perl
-#
-# $Id$
+#
+# $Id$
+# Copyright (C) 2002-2003 Jan Kratochvil <project-PerlMail@jankratochvil.net>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
use vars qw($VERSION);
$VERSION=do { my @r=(q$Revision$=~/\d+/g); sprintf "%d.".("%03d"x$#r),@r; };
}
-use PerlMail::Config;
+use File::Basename;
+BEGIN {
+ use lib $ENV{"PERLMAIL_BASEDIR"} || File::Basename::dirname($0);
+
+ # FIXME:
+ use lib $ENV{"HOME"}."/lib64/perl5";
+ use lib $ENV{"HOME"}."/lib/perl5";
+ use lib $ENV{"HOME"}."/share/perl5";
+
+ use PerlMail::Config;
+ use PerlMail::Lib;
+ }
use Mail::Audit qw(MAPS);
require IO::Handle;
use POSIX qw(WIFEXITED WEXITSTATUS WIFSIGNALED WTERMSIG WIFSTOPPED WSTOPSIG);
require POSIX; # for ceil
use User::Utmp;
-require File::Basename;
use Getopt::Long;
require Mail::Address;
require MIME::Words;
require LWP::UserAgent;
use URI::Escape 'uri_escape';
require WWW::SMS;
+#require Authen::SASL; # Sanity check for &Net::SMTP::auth
+use MIME::Base64;
+use IPC::Open3;
+use POSIX ":sys_wait_h";
-our($Message,$Audit,@AuditStored,$store_ignore,$store_ignorenewmail,$store_profile,$DoBell);
-our(%audit_profile,@sms_squeezes,@alternates_host,@dnsbl_whitelist); # imported
+our($Message,@AuditStored,$DoBell,$Dry);
my %alternates_host; # from @alternates_host
my %dnsbl_whitelist; # from @dnsbl_whitelist
our $procmailTO_ =qr'^((Original-)?(Resent-)?(To|Cc|Bcc)|(X-Envelope|Apparently(-Resent)?)-To):(.*[^-a-zA-Z0-9_.])?'mio;
our $procmailTO =qr'^((Original-)?(Resent-)?(To|Cc|Bcc)|(X-Envelope|Apparently(-Resent)?)-To):(.*[^a-zA-Z])?'mio;
our $procmailFROM_DAEMON=qr'^(Mailing-List:|Precedence:.*(junk|bulk|list)|To: Multiple recipients of |(((Resent-)?(From|Sender)|X-Envelope-From):|>?From )([^>]*[^(.%@a-z0-9])?(Post(ma?(st(e?r)?|n)|office)|(send)?Mail(er)?|daemon|m(mdf|ajordomo)|n?uucp|LIST(SERV|proc)|NETSERV|o(wner|ps)|r(e(quest|sponse)|oot)|b(ounce|bs\.smtp)|echo|mirror|s(erv(ices?|er)|mtp(error)?|ystem)|A(dmin(istrator)?|MMGR|utoanswer))(([^).!:a-z0-9][-_a-z0-9]*)?[%@> ][^<)]*(\(.*\).*)?)?$([^>]|$))'mio;
-our $procmailFROM_MAILER=qr'^(((Resent-)?(From|Sender)|X-Envelope-From):|>?From )[^>]*\b(Post(ma(st(er)?|n)|office)|(send)?Mail(er)?|daemon|mmdf|n?uucp|ops|r(esponse|oot)|(bbs\.)?smtp(error)?|s(erv(ices?|er)|ystem)|A(dmin(istrator)?|MMGR))(([^).!:a-z0-9][-_a-z0-9]*)?[%@> ][^<)]*(\(.*\).*)?)?$([^>]|$)'mio;
+$procmailFROM_MAILER=qr'^(((Resent-)?(From|Sender)|X-Envelope-From):|>?From )[^>]*\b(Post(ma(st(er)?|n)|office)|(send)?Mail(er)?|daemon|mmdf|n?uucp|ops|r(esponse|oot)|(bbs\.)?smtp(error)?|s(erv(ices?|er)|ystem)|A(dmin(istrator)?|MMGR))(([^).!:a-z0-9][-_a-z0-9]*)?[%@> ][^<)]*(\(.*\).*)?)?$([^>]|$)'mio;
# perl-5.8.0 does not cope w/original FROM_MAILER on the third '?' character
# Thus we did '([^>]*[^(.%@a-z0-9])?' -> '[^>]*\b', I hope it is somehow similiar
# original FROM_MAILER =qr'^(((Resent-)?(From|Sender)|X-Envelope-From):|>?From )([^>]*[^(.%@a-z0-9])?(Post(ma(st(er)?|n)|office)|(send)?Mail(er)?|daemon|mmdf|n?uucp|ops|r(esponse|oot)|(bbs\.)?smtp(error)?|s(erv(ices?|er)|ystem)|A(dmin(istrator)?|MMGR))(([^).!:a-z0-9][-_a-z0-9]*)?[%@> ][^<)]*(\(.*\).*)?)?$([^>]|$)'mio;
my $opt_mode;
my $opt_smstest; # 1 or $smscount
my $opt_idle;
-my $opt_dry;
+my $opt_single;
sub process;
local $_;
while (<>) {
die "Invalid 'From ' line: $_" if $message eq "" && !/^From /;
- if (/^From / && $message) {
+ if (!$opt_single && /^From / && $message) {
process $message;
$message="";
}
while (1) {
local $/="\n";
+ $!=undef();
my $length=<STDIN>;
- confess "Unexpected EOF" if !defined $length;
+ confess "Unexpected EOF: $!" if !defined $length;
confess "Missing EOL" if $length!~s/\n$//s;
exit 0 if $length eq "BYE";
confess "Unrecognized length: $length" if $length!~/^\d+$/;
$length==($_=read STDIN,$message,$length) or confess "Got $_ out of required $length bytes";
$length==length $message or confess "False read return ".length($message)." instead of $length";
{
- local *STDOUT;
- local *STDERR;
+ # Do not: local *STDOUT; # FIXME: fd's inherited by spawned processes are not closed this way!
+ # local *STDERR; # FIXME: fd's inherited by spawned processes are not closed this way!
+ # as IPC::Open3 and IPC::Open2 will not redirect the output
+ # and send it to the original socket instead!
local $DoBell=0;
process $message;
if ($DoBell) {
my($idlebest,$linebest);
for my $utmp (User::Utmp::getut(),{ "ut_line"=>"psaux" }) {
local $_;
- next if defined($_=$utmp->{"ut_type"}) && $_!=User::Utmp::USER_PROCESS;
+ next if defined($_=$utmp->{"ut_type"}) && $_!=User::Utmp::USER_PROCESS();
next if defined($_=$utmp->{"ut_user"}) && !$valid_users{$_};
my $line="/dev/".$utmp->{"ut_line"};
my $atime=(stat $line)[8];
return $first;
}
+sub is_multipart
+{
+ return 0 if !$Audit->is_mime();
+ return $Audit->is_multipart();
+}
+
sub mimehead
{
my($part)=@_;
{
my($ignorenewmail,$smscount,%args)=@_;
- my $text=audit_sms(
+ my $text=PerlMail::Config::audit_sms(
"subject"=>unmime($Audit->subject()),
"from"=>[ Mail::Address->parse(unmime($Audit->from())) ],
"body"=>substr(body_simple(),0,$MaxBodySMS*(1+0.25*$smscount)),
die "Nesting profile: $name" if 0x10<=(local $profile_eval_depth=$profile_eval_depth+1);
return @$name if ref $name;
- die "Profile not found: $name" if !exists $audit_profile{$name};
+ if (!exists $audit_profile{$name}) {
+ cluck "Profile not found: $name";
+ return "did";
+ }
my @this=@{$audit_profile{$name}};
return (profile_eval($'),@this[1..$#this]) if $this[0] && $this[0]=~/^=/;
return @this;
$profile=$store_profile if !$profile;
my %do=map({ (!/=/ ? ($_=>1) : ($`=>$')); } profile_eval($profile));
Sys::Syslog::syslog("info","%s%s%s: %s: %s",
- (!$opt_dry ? "" : "--dry: "),
+ (!$Dry ? "" : "--dry: "),
(!$store_ignore ? "" : "IGNORED[$store_ignore]: "),
map({ cut($_); } $folder,address_show(unmime($Audit->from())),unmime($Audit->subject())),
)
- if $do{"syslog"} || $opt_dry;
+ if $do{"syslog"} || $Dry;
$folder=~s/;.*$//s;
$folder="$Mail/".$' if $folder=~/^=/;
push @AuditStored,$folder if $do{"did"};
- return if $store_ignore || $opt_dry;
+ return if $store_ignore || $Dry;
$DoBell++ if $do{"bell"};
write_message($folder) or die;
smssend_tryall $store_ignorenewmail,$do{"sms"},%args if $do{"sms"};
return @AuditStored!=$did_last;
}
+sub writeto
+{
+my($filename)=@_;
+
+ local *F;
+ open F,$filename or confess "open $filename: $!";
+ print F $Message or confess "write $filename: $!";
+ close F or confess "close $filename: $!";
+ return 1;
+}
+
# Never use Mail::Audit->store() as it will reformat MIME bodies and possibly corrupt OpenPGP!
sub write_message
{
my($folder)=@_;
- return if $opt_dry;
+ return 1 if $Dry; # simulate OK
local *F;
open F,">>$folder" or do { warn "Append \"$folder\": $!"; return 0; };
{
local $_;
- ($_=Mail::Audit::audit_get_lock(\*F,$folder)) and do { warn "Lock \"$folder\": $!"; last; };
+ ($_=$Audit->_audit_get_lock(\*F,$folder)) and do { warn "Lock \"$folder\": $!"; last; };
seek F,0,IO::Handle::SEEK_END or do { warn "Seek-end \"$folder\": $!"; last; };
# FIXME: Check for '^From ' to not to rely on our network peer
print F $Message or do { warn "Write to \"$folder\": $!"; last; };
local $_=$_;
my $save_=$_;
+ $message=~s/(\n)(From )/$1>$2/sg;
local $Message=$message;
- local $Audit=Mail::Audit->new(
+ # Cannot call 'local' for our-imported variable:
+ my $Audit_save=$Audit;
+ $Audit=Mail::Audit->new(
"emergency"=>"$Mail/emergency",
"data"=>[map("$_\n",split("\n",$message))],
"log"=>"$HOME/.perlmail.log",
local @AuditStored=();
do { smssend 0,$opt_smstest; return; } if $opt_smstest;
write_message("$Mail/input") or die;
- audit();
+ PerlMail::Config::audit();
warn 'Corrupted $_, repaired' if defined($save_)!=defined($_) || (defined($_) && $save_ ne $_);
+ # restore:
+ $Audit=$Audit_save;
}
# utility functions:
+sub _spamchildcode
+{
+my($err,$isspam)=@_;
+
+ $err=$? if !defined $err;
+ return undef() if !WIFEXITED($?);
+ return undef() if WIFSIGNALED($?);
+ return undef() if WIFSTOPPED($?);
+ return 0 if !WEXITSTATUS($?);
+ return $isspam||1 if 1==WEXITSTATUS($?); # isspam
+ cluck "Possible FIXME or your system is broken (WEXITSTATUS==".WEXITSTATUS($?).")";
+ return 0; # simulate as not spam
+}
+
# return: true (error-message or "1") if is spam
-sub razor2
+sub spamassassin
{
- # razor-check has exit code 1 if NOT spam, code 0 if IS spam
+my($cmd)=@_;
+
+ #$cmd||="nice spamassassin --exit-code 1 --mbox";
+ $cmd||="spamc -c -s 50000000";
+ # spamassassin has the specified exit code if IS spam, code 0 if NOT spam
+ # See &_spamchildcode for the code 1.
local *CHILD;
- local $SIG{"PIPE"}=sub { warn "razor2 gave me SIGPIPE: broken pipe"; };
+ local $SIG{"PIPE"}=sub { warn "spamassassin gave me SIGPIPE: broken pipe"; };
# prevent Razor2's: Can't call method "log" on unblessed reference at Razor2/Client/Agent.pm line 212.
local $ENV{"HOME"}=$HOME;
- open CHILD,'|'
- .'('.'(razor-check 2>&1;echo >&3 $?)'
- .'|sed "s/^/razor-check: /"'
- .'|logger -t "perlmail['.$$.']" -p mail.crit'
- .') 3>&1'
- .'|exit `cat`'
+ # 2>/dev/null to prevent error messages to corrupt inetd() output of perlmail-accept(1)
+ open CHILD,"|$cmd &>/dev/null"
+ # Workaround: spamassassin-3.1.3-1.fc5
+ #.q{|awk '/^X-Spam-Flag: YES$/{if (!body) exit 1;}/^$/{body=1;}'}
+ # Original:
+ #." >/dev/null 2>/dev/null"
or return 0;
print CHILD $Message;
- my $return;
- {
- local $/=undef();
- $return=<CHILD> || 1;
- }
close CHILD;
- return undef() if !WIFEXITED($?);
- return undef() if WIFSIGNALED($?);
- return undef() if WIFSTOPPED($?);
- return undef() if WEXITSTATUS($?);
- return $return; # is-spam
+ return _spamchildcode;
}
# NOTE: returns undef() if !wantarray and the first header is unrecognized
-# Returns also hosts
+# Returns always HOST:IP pair(s).
sub Received_for
{
my @r=();
# Extended Mail::Audit::MAPS
# $domain,$full,[$timeout]
+# Returns false if valid, code if spam detected.
sub dnsbl
{
my($domain,$full,$timeout)=@_;
- $timeout||=30; # sec
+ $timeout||=2; # sec
$Mail::Audit::MAPS::host=$domain;
- my @hosts=map({ s/^.*://; "[$_]"; } # strip DNS part
- grep({ /^([^:@]*):/ # $1 is DNS name, $' is IP address
- && !$alternates_host{$1} # leave only foreign hosts
- && !$dnsbl_whitelist{$'}; } (Received_for()))
- );
- splice @hosts,1 if !$full && @hosts; # "&& @hosts" to prevent: WARN: splice() offset past end of array
- {
- package My::Audit::Faked;
- sub received { return @{$_[0]->{"received"}}; }
- }
- my $self_faked={
- "received"=>[@hosts],
- };
- bless $self_faked,"My::Audit::Faked";
- return Mail::Audit::rblcheck($self_faked,$timeout);
-}
-
-our %muttrc_pending=();
-sub muttrc
-{
-my($muttrc)=@_;
-
- $muttrc||="$HOME/.muttrc";
- $muttrc=~s/^\~/$HOME/;
- do { warn "Looping muttrc, ignoring: $muttrc"; return (); } if $muttrc_pending{$muttrc};
- local $muttrc_pending{$muttrc}=1;
- local *MUTTRC;
- open MUTTRC,$muttrc or do { warn "open \"$muttrc\": $!"; return (); };
- local $/="\n";
- local $_;
- my @r=();
- # far emulation mutt/init.c/mutt_parse_rc_line()
- while (<MUTTRC>) {
- s/^[\s;]*//s;
- s/[#;].*$//s;
- s/\s*$//s;
- next if !/^(\S+)\s*/s;
- if ($1 eq "source") {
- $_=$';
- do { warn "Wrong 'source' parameters at $muttrc:$.: $_"; next; } if !/^\S+$/;
- push @r,muttrc($_);
- next;
- }
- push @r,$_;
+ for my $host (Received_for()) {
+ next if $host!~/^([^:@]*):/;
+ my $ip=$';
+ # $1 is DNS name, $ip is IP address
+ next if $alternates_host{$1}; # leave only foreign hosts
+ next if $dnsbl_whitelist{$ip};
+
+# FIXME: Faking
+# {
+# package My::Audit::Faked;
+# sub received { return @{$_[0]->{"received"}}; }
+# }
+# my $self_faked=Mail::Audit->new();
+# $self_faked->{"received"}=["[$ip]"];
+# bless $self_faked,"My::Audit::Faked";
+# my $code=Mail::Audit::rblcheck($self_faked,$timeout);
+ my $code=$Audit->rblcheck($timeout);
+
+ next if !$code;
+ # Some 0.0.0.0 etc. found for <linux-kernel@>, see: &Mail::Audit::MAPS::_checkit
+ # Do not: $code!='1 Invalid IP address '
+ # as it causes warn.
+ return $code if $code ne '1 Invalid IP address ';
+ return if !$full;
}
- close MUTTRC or warn "close \"$muttrc\": $!";
- return wantarray() ? @r : join("",map("$_\n",@r));
}
-my %mutteval_charmap=( # WARNING: Don't use "" or "0" here, see below for "|| warn"!
- '\\'=>"\\",
- 'r'=>"\r",
- 'n'=>"\n",
- 't'=>"\t",
- 'f'=>"\f",
- 'e'=>"\e",
- );
-# mutt/init.c/mutt_extract_token()
-sub mutteval
-{
- local $_=$_[0];
- return $_ if !s/^"//;
- do { warn "Missing trailing quote in: $_"; return $_; } if !s/"$//;
- s/\\(.)/$mutteval_charmap{$1} || warn "Undefined '\\$1' sequence in: $_";/ges;
- return $_;
-}
-
-sub muttrc_get
-{
-my(@headers)=@_;
-
- my @r=map({ (ref $_ ? $_ : qr/^\s*set\s+\Q$_\E\s*=\s*(.*?)\s*$/si); } @headers);
- my %r=map(($_=>undef()),@r);
- for (muttrc()) {
- for my $ritem (@r) {
- /$ritem/si or next;
- $r{$ritem}=mutteval $1;
- }
- }
- for my $var (grep { !defined($r{$_}) } @r) {
- warn "Variable '$var' not found in muttrc";
- return undef();
- }
- return wantarray() ? %r : $r{$r[0]};
+# Returns true if IS virus; the message will contain the virus name
+sub clamscan
+{
+my($cmd)=@_;
+
+ $cmd||='nice clamscan --no-summary -';
+ # clamscan has exit code 1 if IS virus , code 0 if NOT virus
+ # Do not use IPC::Open2 as it would try to use our STDERR which is not valid by: local *STDERR;
+ local(*WR,*RD,*ERR);
+ local $SIG{"PIPE"}=sub { warn "clamscan '$cmd' gave me SIGPIPE: broken pipe"; };
+ my $pid=open3(\*WR,\*RD,\*ERR,$cmd.' 2>&1')
+ or do { cluck "IPC::Open3 $cmd: $!"; return 0; };
+ print WR $Message;
+ close WR or do { cluck "close WR of $cmd: $!"; return 0; };
+ my $status=do { local $/=undef(); <RD>; };
+ close RD or do { cluck "close RD of $cmd: $!"; return 0; };
+ # Do not: $status.=do { local $/=undef(); <ERR>; };
+ # close ERR or do { cluck "close ERR of $cmd: $!"; return 0; };
+ # (FIXME) as it causes: Use of uninitialized value in <HANDLE>
+ # waitpid fills $? for: &_spamchildcode
+ local $SIG{"ALRM"}=sub { warn "Timeout $clamscan_waitpid_timeout sec waiting for child $cmd"; };
+ alarm $clamscan_waitpid_timeout;
+ # Do not: WNOHANG
+ # as it would not be enough for clamscan(1) even after all close-s above.
+ my $pidcheck=waitpid($pid,0);
+ alarm 0;
+ my $err=$?;
+ $pidcheck && $pidcheck==$pid
+ or do { cluck "waitpid for $cmd returned $pidcheck!=$pid"; return 0; };
+ $status=~s/^stdin: //mg;
+ # Prevent: LibClamAV Warning: PGP encoded attachment not scanned
+ $status=~s/^.*\bwarning:.*\n//img;
+ $status=~s/\n$//;
+ return $status if $status ne "OK" && $status;
+ return _spamchildcode $err,$status;
}
sub muttrc_aliases
return %r;
}
-# FIXME: Unify
-# BEGIN perlmail-sendmail
-# return: Mail::Address instance or undef()
-sub parseone
-{
-my($line)=@_;
-
- return undef() if !defined $line;
- my @r=Mail::Address->parse($line);
- warn "Got ".scalar(@r)." addresses while wanting just one; when parsing: $line" if 1!=@r;
- return $r[0];
-}
-# END perlmail-sendmail
-
# FIXME: host may get multiple recipients and thus not showing "for <...>"
# FIXME: muttrc_get("from") is too strict
sub store_muttrc_alternates
if $From!~/$alternates/si;
for my $for (reverse Received_for()) {
$for=~s/:.*$//; # strip IP address here
- if ($Fromobj->user() ne "prog-mutt") {
- next if lc($for) eq lc($From);
- }
- else {
- my $forobj=parseone $for;
- if ($forobj && $forobj->host()) {
- # it is 'for' our primary address
- next if lc($forobj->host()) eq lc($Fromobj->host()); # or 'return'? shouldn't matter
- }
+ my $forobj=parseone $for;
+ if ($forobj && $forobj->host()) {
+ # it is 'for' our primary address
+ next if lc($forobj->host()) eq lc($Fromobj->host()); # or 'return'? shouldn't matter
}
next if !$alternates_host{lc $for} && $for!~/$alternatesre/si;
store "$prefix\L$for",($profile || []);
{
my($header,$string)=@_;
+ cluck if !defined $string;
return _headercore(qr/\Q$string\E/i,1,$header,$string);
}
$Audit->replace_header($header,$text);
}
-
# MAIN
-my $basedir=File::Basename::dirname($0);
$Getopt::Long::ignorecase=0;
die "GetOptions error" if !Getopt::Long::GetOptions(
"inetd" ,sub { $opt_mode=\&inetd; },
"stdin" ,sub { $opt_mode=\&stdin; },
- "dry" ,\$opt_dry,
+ "single!" ,\$opt_single,
+ "dry" ,\$Dry,
"smstest:s",sub { $opt_mode=\&stdin; $opt_smstest=($_[1] || 1); },
"idle!" ,\$opt_idle,
"idletest" ,sub { syslogging_restore(); print((defined($_=useridle()) ? $_ : "<undef>")."\n"); exit 0; },
"muttrc" ,sub { syslogging_restore(); print scalar muttrc(); exit 0; },
- "d|basedir=s",\&basedir,
);
# "Excessive arguments" checked in &inetd
die "Missing mode" if !$opt_mode;
-my $filenameMyAudit="$basedir/My-Audit.pm";
-open AUDIT,$filenameMyAudit or die "open \"$filenameMyAudit\": $!";
-{
- local $/=undef();
- eval <AUDIT> or die "eval \"$filenameMyAudit\": $@";
- audit_init();
- %alternates_host=map((lc($_)=>1),@alternates_host);
- %dnsbl_whitelist=map(( $_ =>1),@dnsbl_whitelist);
- }
-close AUDIT or warn "close \"$filenameMyAudit\": $!";
+%alternates_host=map((lc($_)=>1),@alternates_host);
+%dnsbl_whitelist=map(( $_ =>1),@dnsbl_whitelist);
&$opt_mode();
die "NOTREACHED";