X-Git-Url: http://git.jankratochvil.net/?p=PerlMail.git;a=blobdiff_plain;f=perlmail-accept;h=20b5b2068d2cf7ca3219d203676180e94cfa09be;hp=7eb3f75ddc0c5f349d34d75d1b2467a6944afe2c;hb=2bdf40aefdf914867b2aadb3130f0c845f2257c2;hpb=130d4621a51128a582392422074bb8954a78c254 diff --git a/perlmail-accept b/perlmail-accept index 7eb3f75..20b5b20 100755 --- a/perlmail-accept +++ b/perlmail-accept @@ -46,6 +46,11 @@ INIT { use File::Basename; BEGIN { use lib $ENV{"PERLMAIL_BASEDIR"} || File::Basename::dirname($0); + + # FIXME: + use lib "/home/lace/lib/perl5/site_perl/5.10.0"; + use lib "/home/lace/lib64/perl5/site_perl/5.10.0/x86_64-linux-thread-multi"; + use PerlMail::Config; use PerlMail::Lib; } @@ -69,9 +74,13 @@ require HTTP::Request; 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,@AuditStored,$DoBell); +our($Message,@AuditStored,$DoBell,$Dry); my %alternates_host; # from @alternates_host my %dnsbl_whitelist; # from @dnsbl_whitelist @@ -88,7 +97,6 @@ $procmailFROM_MAILER=qr'^(((Resent-)?(From|Sender)|X-Envelope-From):|>?From )[^> my $opt_mode; my $opt_smstest; # 1 or $smscount my $opt_idle; -my $opt_dry; my $opt_single; @@ -131,8 +139,10 @@ sub inetd $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; # 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! + # 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) { @@ -160,7 +170,7 @@ sub useridle 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]; @@ -186,6 +196,12 @@ sub body_first return $first; } +sub is_multipart +{ + return 0 if !$Audit->is_mime(); + return $Audit->is_multipart(); +} + sub mimehead { my($part)=@_; @@ -374,7 +390,10 @@ my($name)=@_; 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; @@ -409,15 +428,15 @@ my($folder,$profile,%args)=@_; $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"}; @@ -442,12 +461,12 @@ sub write_message { my($folder)=@_; - return 1 if $opt_dry; # simulate OK + 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; }; @@ -487,31 +506,43 @@ my($message)=@_; # 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 spamassassin { my($cmd)=@_; - $cmd||="spamassassin --exit-code"; - # spamassassin has exit code 1 if IS spam, code 0 if NOT spam + #$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 "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; # 2>/dev/null to prevent error messages to corrupt inetd() output of perlmail-accept(1) - open CHILD,"|$cmd --mbox >/dev/null 2>/dev/null" + open CHILD,"|$cmd >/dev/null 2>/dev/null" or return 0; print CHILD $Message; - my $return=close CHILD; - return undef() if !WIFEXITED($?); - return undef() if WIFSIGNALED($?); - return undef() if WIFSTOPPED($?); - return 1 if WEXITSTATUS($?); # is-spam - return 0; # not-spam + close CHILD; + 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=(); @@ -528,27 +559,75 @@ sub Received_for # 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"}}; } + 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 , 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; } - my $self_faked={ - "received"=>[@hosts], - }; - bless $self_faked,"My::Audit::Faked"; - return Mail::Audit::rblcheck($self_faked,$timeout); +} + +# 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(); ; }; + close RD or do { cluck "close RD of $cmd: $!"; return 0; }; + # Do not: $status.=do { local $/=undef(); ; }; + # close ERR or do { cluck "close ERR of $cmd: $!"; return 0; }; + # (FIXME) as it causes: Use of uninitialized value in + # 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 @@ -638,6 +717,7 @@ sub headeris { my($header,$string)=@_; + cluck if !defined $string; return _headercore(qr/\Q$string\E/i,1,$header,$string); } @@ -656,6 +736,63 @@ my($header,$map)=@_; $Audit->replace_header($header,$text); } +# LMTP engine: +use Net::Cmd qw(CMD_OK CMD_MORE); +{ + package My::Net::SMTP::LMTP; + require Net::SMTP; + our @ISA=qw(Net::SMTP); + use Net::SMTP; + use Net::Cmd qw(CMD_OK); + use Carp qw(confess cluck); + + # Do not: sub _HELO + # as it would not set {'net_smtp_esmtp'} + sub _EHLO { shift->command("LHLO", @_)->response() == CMD_OK } + + sub clucked + { + my($self,$func,@args)=@_; + + do { return $_ if defined $_; } for $self->$func(@args); + cluck $func; + return; + } +} + + +sub lmtp_deliver +{ +my($admin_user,$admin_pwd,$user_from,$user_to)=@_; + + my $lmtp=My::Net::SMTP::LMTP->clucked("new","localhost","Port"=>"lmtp", +# "Debug"=>1, + ) or return; + bless $lmtp,"My::Net::SMTP::LMTP"; +# Prevent: +# due to: +# $lmtp->auth(Authen::SASL->new( +# "mechanism"=>"PLAIN", +# "callback"=>{ +# "user"=>$admin_user, +# "pass"=>$admin_pwd, +# # Prevent: "authname"=>$admin_user +# # as it causes: DIE: Unknown callback: 'authname'. (user|auth|language|pass) +# })); + # FIXME: Authentication hack: + $lmtp->command("AUTH PLAIN")->response()==CMD_MORE + or do { cluck "auth announce"; return; }; + $lmtp->clucked("command",encode_base64($user_from."\x00".$admin_user."\x00".$admin_pwd)) or return; + $lmtp->clucked("mail",$user_from) or return; + $lmtp->clucked("to",$user_to) or return; + $lmtp->clucked("data"); # Do not: or return; + # Prevent: 554 5.6.0 Message contains invalid header + (my $data=$Message)=~s/\AFrom .*\r?\n//; + $lmtp->clucked("datasend",$data) or return; + $lmtp->clucked("dataend") or return; + $lmtp->clucked("quit") or return; +} + # MAIN @@ -664,7 +801,7 @@ die "GetOptions error" if !Getopt::Long::GetOptions( "inetd" ,sub { $opt_mode=\&inetd; }, "stdin" ,sub { $opt_mode=\&stdin; }, "single!" ,\$opt_single, - "dry" ,\$opt_dry, + "dry" ,\$Dry, "smstest:s",sub { $opt_mode=\&stdin; $opt_smstest=($_[1] || 1); }, "idle!" ,\$opt_idle, "idletest" ,sub { syslogging_restore(); print((defined($_=useridle()) ? $_ : "")."\n"); exit 0; },