Support 9210 CSV contacts format.
[PerlMail.git] / perlmail-accept
index d0909da..55bef4a 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; };
@@ -27,7 +43,12 @@ INIT {
        }
 
 
-use PerlMail::Config;
+use File::Basename;
+BEGIN {
+       use lib $ENV{"PERLMAIL_BASEDIR"} || File::Basename::dirname($0);
+       use PerlMail::Config;
+       use PerlMail::Lib;
+       }
 
 use Mail::Audit qw(MAPS);
 require IO::Handle;
@@ -35,7 +56,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;
@@ -51,8 +71,7 @@ use URI::Escape 'uri_escape';
 require WWW::SMS;
 
 
-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);
 my %alternates_host;   # from @alternates_host
 my %dnsbl_whitelist;   # from @dnsbl_whitelist
 
@@ -61,7 +80,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;
@@ -70,6 +89,7 @@ my $opt_mode;
 my $opt_smstest;       # 1 or $smscount
 my $opt_idle;
 my $opt_dry;
+my $opt_single;
 
 
 sub process;
@@ -82,7 +102,7 @@ sub stdin
        local $_;
        while (<>) {
                die "Invalid 'From ' line: $_" if $message eq "" && !/^From /;
-               if (/^From / && $message) {
+               if (!$opt_single && /^From / && $message) {
                        process $message;
                        $message="";
                        }
@@ -111,8 +131,8 @@ 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;
+                       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!
                        local $DoBell=0;
                        process $message;
                        if ($DoBell) {
@@ -294,7 +314,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)),
@@ -422,7 +442,7 @@ sub write_message
 {
 my($folder)=@_;
 
-       return if $opt_dry;
+       return 1 if $opt_dry;   # simulate OK
        local *F;
        open F,">>$folder" or do { warn "Append \"$folder\": $!"; return 0; };
        {
@@ -446,8 +466,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,39 +479,35 @@ 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:
 
 # 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||="$HOME/bin/spamassassin --exit-code";
+       # spamassassin has exit code 1 if IS spam, code 0 if NOT spam
        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 --mbox >/dev/null 2>/dev/null"
                        or return 0;
        print CHILD $Message;
-       my $return;
-       {
-               local $/=undef();
-               $return=<CHILD> || 1;
-               }
-       close CHILD;
+       my $return=close CHILD;
        return undef() if !WIFEXITED($?);
        return undef() if  WIFSIGNALED($?);
        return undef() if  WIFSTOPPED($?);
-       return undef() if WEXITSTATUS($?);
-       return $return; # is-spam
+       return 1       if  WEXITSTATUS($?);     # is-spam
+       return 0;       # not-spam
 }
 
 # NOTE: returns undef() if !wantarray and the first header is unrecognized
@@ -532,75 +551,6 @@ my($domain,$full,$timeout)=@_;
        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]};
-}
-
 sub muttrc_aliases
 {
        my %r=();
@@ -615,20 +565,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 +580,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 || []);
@@ -728,31 +659,22 @@ my($header,$map)=@_;
 
 # 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; },
+                 "single!"  ,\$opt_single,
                  "dry"      ,\$opt_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";