X-Git-Url: http://git.jankratochvil.net/?p=PerlMail.git;a=blobdiff_plain;f=perlmail-accept;h=e704077bce8d6a163b0e5cf34e23e6578c0d1fea;hp=d0909da2f1a5764cd9565fb9a3061fe45d7717fb;hb=0dd6e159920b9402e163cd35bdc8465dcfee65c9;hpb=c23c7da69d6fabcd20481ca42cfe551626bfbcc5 diff --git a/perlmail-accept b/perlmail-accept index d0909da..e704077 100755 --- a/perlmail-accept +++ b/perlmail-accept @@ -1,6 +1,22 @@ #! /usr/bin/perl -# -# $Id$ +# +# $Id$ +# Copyright (C) 2002-2003 Jan Kratochvil +# +# 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; }; @@ -27,7 +43,18 @@ INIT { } -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; @@ -35,7 +62,6 @@ use Carp qw(cluck confess); 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; @@ -49,10 +75,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,$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 @@ -61,7 +90,7 @@ 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; @@ -69,7 +98,7 @@ our $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; sub process; @@ -82,7 +111,7 @@ sub stdin local $_; while (<>) { die "Invalid 'From ' line: $_" if $message eq "" && !/^From /; - if (/^From / && $message) { + if (!$opt_single && /^From / && $message) { process $message; $message=""; } @@ -101,8 +130,9 @@ sub inetd while (1) { local $/="\n"; + $!=undef(); my $length=; - 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+$/; @@ -111,8 +141,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; - 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) { @@ -140,7 +172,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]; @@ -166,6 +198,12 @@ sub body_first return $first; } +sub is_multipart +{ + return 0 if !$Audit->is_mime(); + return $Audit->is_multipart(); +} + sub mimehead { my($part)=@_; @@ -294,7 +332,7 @@ sub smssend { 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)), @@ -354,7 +392,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; @@ -389,15 +430,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"}; @@ -417,17 +458,28 @@ my($funcref,@funcargs)=@_; 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; }; @@ -446,8 +498,11 @@ my($message)=@_; 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", @@ -456,43 +511,55 @@ my($message)=@_; 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= || 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=(); @@ -509,96 +576,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"}}; } - } - 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 () { - 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 , 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(); ; }; + 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 @@ -615,20 +661,6 @@ 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 @@ -644,15 +676,10 @@ my($prefix,$profile)=@_; 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 || []); @@ -707,6 +734,7 @@ sub headeris { my($header,$string)=@_; + cluck if !defined $string; return _headercore(qr/\Q$string\E/i,1,$header,$string); } @@ -725,34 +753,24 @@ my($header,$map)=@_; $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()) ? $_ : "")."\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 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";