Remove any LMTP.
[PerlMail.git] / perlmail-accept
index 43b4d62..e704077 100755 (executable)
@@ -1,6 +1,22 @@
 #! /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; };
@@ -10,7 +26,7 @@ use warnings;
 
 INIT {
        require Sys::Syslog;
-       Sys::Syslog::openlog("lacemail","pid","mail");
+       Sys::Syslog::openlog("perlmail","pid","mail");
        my @syslogging_stack;
        sub syslogging_on_save
        {
@@ -27,13 +43,25 @@ INIT {
        }
 
 
+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 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;
@@ -46,28 +74,23 @@ require HTTP::Cookies;
 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";
 
 
-my $HOME="/home/lace";
-my $Mail="$HOME/Mail";
-my @ValidUsers=qw(root lace short kratochvil _local);
-my $IdleMax=60;
-my $MaxBodySMS=0x1000; # max bytes to pass to Lingua::EN::Squeeze
-my $SMSmailError='short+err@ucw.cz';
-my $SMSwebRcpt='00420602431329';
-my $SMSmailRcpt=$SMSwebRcpt.'@sms.eurotel.cz';
-my $SMScontact='<short@ucw.cz>';
-
-our($Message,$Audit,@AuditStored,$store_ignore,$store_ignorenewmail,$store_profile,$DoBell);
-our(%audit_profile,@sms_squeezes,@alternates_host);    # imported
+our($Message,@AuditStored,$DoBell,$Dry);
 my %alternates_host;   # from @alternates_host
+my %dnsbl_whitelist;   # from @dnsbl_whitelist
 
 # from RedHat "procmail-3.22-5"
 # /i should be only $procmailFROM_DAEMON but how it can hurt to /i all?
 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;
@@ -75,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;
@@ -88,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="";
                        }
@@ -98,7 +121,7 @@ sub stdin
        exit 0;
 }
 
-# FIXME: separate 'lacemail'-transfer together with lacemail-submit away
+# FIXME: separate 'perlmail'-transfer together with perlmail-submit away
 sub inetd
 {
        die "Excessive arguments" if @ARGV;
@@ -107,8 +130,9 @@ sub inetd
 
        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+$/;
@@ -117,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) {
@@ -141,11 +167,12 @@ sub bell
 
 sub useridle
 {
+       return 0 if ! -e "$HOME/away";
        my %valid_users=map(($_=>1),@ValidUsers);
        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];
@@ -171,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)=@_;
@@ -245,129 +278,6 @@ my($smsi,$smscount)=@_;
        return "";
 }
 
-# FIXME: rewrite &send_cz_eurotel properly by own code
-# patch for http://kiwi.ms.mff.cuni.cz/%7Etom/programming/src/sendsms.tar.gz/sendsms.pl
-my $agent=LWP::UserAgent->new();
-$agent->agent("LaceMail $VERSION; contact=$SMScontact; ");
-my($request1,$response1);      # for &send_cz_eurotel
-my($name,$value,$type,$disabled,$q2);
-
-# &send_cz_eurotel returns: error
-# BEGIN http://kiwi.ms.mff.cuni.cz/%7Etom/programming/src/sendsms.tar.gz/sendsms.pl
-sub parse_inputs
-{
-       my ($resp) = @_;
-       my @inputs;
-       my $ct;
-       my @c;
-       $ct=$resp->content();
-
-       @c=split '>', $ct;
-       grep {
-          if (/(<input|<select|<textarea)([^<>]*)(>|$)/i) { 
-            my $txt=$2, $name="", $value="", $type="x", $disabled=0;
-            my $ipoc;
-            if ($txt =~ /type="([^"]*)"/i) { $type=$1; }
-            elsif ($txt =~ /type=([^" ]*)[ >]/i) { $type=$1; }
-            if ($txt =~ /name="([^"]*)"/i) { $name=$1; }
-            elsif ($txt =~ /name=([^" ]*)[ >]/i) { $name=$1; }
-            if ($txt =~ /value="([^"]*)"/i) { $value=$1; }
-            elsif ($txt =~ /value=([^" ]*)[ >]/i) { $value=$1; }
-            if ($txt =~ /disabled/i) { $disabled=1; }
-            if ($name ne "" && $type ne "" && $type ne "button" && ! $disabled)
-            {
-              $ipoc=$#inputs;
-              $inputs[$ipoc+1][0]=$name;
-              $inputs[$ipoc+1][1]=$value;
-            }
-          }
-        0; } @c;
-       return @inputs;
-}
-
-sub make_query
-{
-       my (@inputs) = @_;
-
-       my $i;
-       my $query = "";
-
-       for ($i=0; $i<=$#inputs; $i++)
-       {
-         my($q1, $q2);
-         if ($i>0) { $query="$query&"; }
-         $q1=uri_escape($inputs[$i][0]);
-         $q2=uri_escape($inputs[$i][1]);
-         $query="$query$q1=$q2";
-       }
-
-       #change @ and space back
-       $query=~ s/%20/+/g;
-       $query=~ s/%40/@/g;
-       return $query;
-}
-
-sub send_cz_eurotel
-{
-       my ($id,$text,$mail,$directd) = @_;
-       my $src_url = "http://www2.eurotel.cz/sms/index.html";
-       my @inputs;
-       my $query = "";
-       my $cookie = HTTP::Cookies->new;
-       my $pref;
-
-       #check if correct number
-       if (substr($id,0,5)!="00420") { return -1; }
-       $pref=substr($id,5,3);
-       if (!($pref eq "601" || $pref eq "602" || $pref eq "606" || $pref eq "607"  || ($pref ge "720" && $pref le "729"))) { return -1; }
-       
-       #get form page, extract cookies
-       $request1=new HTTP::Request('GET', "$src_url?n_pagestyle=new");
-       $response1=$agent->request($request1);
-       if ($response1->code != 200) { return -3; }
-       $cookie->extract_cookies($response1);
-
-       #parse the form
-       @inputs=parse_inputs($response1);
-       
-       #fill the form
-       $inputs[2][1]=substr($id,5,3);
-       $inputs[3][1]=substr($id,8,6);
-       $inputs[4][1]=$mail;
-
-       #direct display
-       $inputs[6][1]="sms";
-       $inputs[6][1]="show" if ($directd>0);
-
-       $inputs[7][1]=$text;
-
-#      for ($i=0; $i<=$#inputs; $i++) { print "[$i] $inputs[$i][0] $inputs[$i][1]\n"; } 
-
-       #make query
-       $query=make_query(@inputs);
-
-       #POST the form
-       my $header = new HTTP::Headers( 
-               'Content-Length' => length($query),
-               'Content-Type' => 'application/x-www-form-urlencoded',
-               'Accept' => '*/*',
-               'Referer' => $src_url
-       );
-       my $request2 = new HTTP::Request('POST',$src_url, $header, $query);
-       $cookie->add_cookie_header($request2);
-       my $response2 = $agent->request($request2);
-
-       if ($response2->code != 200) { return -3; }
-
-       #check for success
-       if ($response2->content() !~ /byla.*odesl.*na na SMS centrum/)
-       {
-         return -2;
-       }
-       return 0;
-}
-# END http://kiwi.ms.mff.cuni.cz/%7Etom/programming/src/sendsms.tar.gz/sendsms.pl
-
 sub smslens
 {
 my($ignorenewmail,$smscount,%args)=@_;
@@ -375,12 +285,11 @@ my($ignorenewmail,$smscount,%args)=@_;
        return map({
                        my $l=160;
                        if (!$ignorenewmail) {  # send by mail
-                               $l-=length("Z emailu $SMSmailError: ");
+                               $l-=length("Z emailu FIXME SMSmailError: ");
                                $l-=length(smsbuild($_,$smscount));
                                }
                        else {  # send by web
-                               $l-=length("Z WWW x/5: ");
-                               $l-=length(smsbuild($_,POSIX::ceil($smscount/5)));
+                               $l-=6;  # 154 is the max length before split; why?
                                }
                        $l;
                        } (0..$smscount-1));
@@ -396,7 +305,18 @@ my($squeezed,$smscount,@lens)=@_;
                $squeezed=~/^.{0,$len}/s;
                my $frag=$&;
                $squeezed=$';
-               return 0 if send_cz_eurotel($SMSwebRcpt,$frag,"",0);
+               return 0 if 3!=@SMSwebRcpt;
+               local *F;
+               open F,"$HOME/priv/WWW-SMS-$SMSwebRcpt_username.pwd" or return 0;
+               my $pwd=<F>;
+               chomp $pwd;
+               close F;
+               my $sms=WWW::SMS->new(@SMSwebRcpt,$frag,"username"=>$SMSwebRcpt_username,"passwd"=>$pwd);
+               for ($sms->gateways("sorted"=>"reliability")) {
+                       last if $sms->send($_);
+                       Sys::Syslog::syslog("warning","Web SMS send failed: %s",$WWW::SMS::Error);
+                       my $void=$WWW::SMS::Error;      # Prevent: Name "WWW::SMS::Error" used only once
+                       }
                }
        return 1;
 }
@@ -405,34 +325,14 @@ sub smssend_mail
 {
 my($squeezed,$smscount,@lens)=@_;
 
-       my $recalclen=0;
-       for ($smscount=0;$recalclen<length $squeezed;$smscount++) {
-               $recalclen+=$lens[$smscount];
-               }
-       for my $smsi (0..$smscount-1) {
-               my $len=$lens[$smsi];
-               $squeezed=~/^.{0,$len}/s;
-               my $frag=$&;
-               $squeezed=$';
-               my $mail=Mail::Mailer->new("sendmail","-f","$SMSmailError");
-               $mail->open({
-                               "To"=>$SMSmailRcpt,
-                               "From"=>$SMSmailError,  # no longer displayed anyway
-                               "X-LaceMail-Version"=>$VERSION,
-                               "X-LaceMail-Contact"=>$SMScontact,
-                               });
-               print $mail smsbuild($smsi,$smscount).$frag."\n";
-               # FIXME: check errors
-               $mail->close();
-               }
-       return 1;
+       return 0;
 }
 
 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)),
@@ -492,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;
@@ -527,17 +430,17 @@ 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);
+       write_message($folder) or die;
        smssend_tryall $store_ignorenewmail,$do{"sms"},%args if $do{"sms"};
 }
 
@@ -555,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; };
@@ -584,53 +498,68 @@ 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/.lacemail.log",
+                       "log"=>"$HOME/.perlmail.log",
                        "loglevel"=>99,
                        );
        local @AuditStored=();
        do { smssend 0,$opt_smstest; return; } if $opt_smstest;
-       write_message("$Mail/input");
-       audit();
+       write_message("$Mail/input") or die;
+       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 "lacemail['.$$.']" -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=();
@@ -645,101 +574,77 @@ sub Received_for
        return @r;
 }
 
-my %dnsbl_whitelist=map(($_=>1),(
-               "195.250.128.83"        # smtp3.vol.cz; 83.128.250.195.blackholes.five-ten-sg.com
-               ));
-
 # 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({ /^([^:@]*):/ && !$alternates_host{$1}    # leave only foreign hosts
-                                       && !$dnsbl_whitelist{$1}; } (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 <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;
                }
-       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,$_;
-               }
-       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
@@ -756,20 +661,6 @@ sub muttrc_aliases
        return %r;
 }
 
-# FIXME: Unify
-# BEGIN lacemail-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 lacemail-sendmail
-
 # FIXME: host may get multiple recipients and thus not showing "for <...>"
 # FIXME: muttrc_get("from") is too strict
 sub store_muttrc_alternates
@@ -785,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 || []);
@@ -848,6 +734,7 @@ sub headeris
 {
 my($header,$string)=@_;
 
+       cluck if !defined $string;
        return _headercore(qr/\Q$string\E/i,1,$header,$string);
 }
 
@@ -862,37 +749,28 @@ my($header,$map)=@_;
                $text=~s/\b\Q$from\E\b/$to/gsi;
                }
        return if $text eq $orig;
-       $Audit->put_header("X-LaceMail-header_remap-$header",$orig);
+       $Audit->put_header("X-PerlMail-header_remap-$header",$orig);
        $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);
-       }
-close AUDIT or warn "close \"$filenameMyAudit\": $!";
+%alternates_host=map((lc($_)=>1),@alternates_host);
+%dnsbl_whitelist=map((   $_ =>1),@dnsbl_whitelist);
 
 &$opt_mode();
 die "NOTREACHED";