host1:~lace/src/PerlMail/ update.
[PerlMail.git] / perlmail-accept
1 #! /usr/bin/perl
2
3 #       $Id: perlmail-accept,v 1.40 2010/07/16 07:19:36 lace Exp $
4 # Copyright (C) 2002-2003 Jan Kratochvil <project-PerlMail@jankratochvil.net>
5
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
10
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 # GNU General Public License for more details.
15
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
19
20
21 use vars qw($VERSION);
22 $VERSION=do { my @r=(q$Revision: 1.40 $=~/\d+/g); sprintf "%d.".("%03d"x$#r),@r; };
23 use strict;
24 use warnings;
25
26
27 INIT {
28         require Sys::Syslog;
29         Sys::Syslog::openlog("perlmail","pid","mail");
30         my @syslogging_stack;
31         sub syslogging_on_save
32         {
33                 push @syslogging_stack,$SIG{"__WARN__"},$SIG{"__DIE__" };
34                 $SIG{"__WARN__"}=sub { Sys::Syslog::syslog("warning","WARN: %s",$_[0]); };      # disabled: print STDERR $_[0];
35                 $SIG{"__DIE__" }=sub { Sys::Syslog::syslog("crit"   ,"DIE: %s" ,$_[0]); };
36         }
37         syslogging_on_save();
38         sub syslogging_restore
39         {
40                 $SIG{"__DIE__" }=pop @syslogging_stack;
41                 $SIG{"__WARN__"}=pop @syslogging_stack;
42         }
43         }
44
45
46 use File::Basename;
47 BEGIN {
48         use lib $ENV{"PERLMAIL_BASEDIR"} || File::Basename::dirname($0);
49
50         # FIXME:
51 #       use lib split /:/,$ENV{"PERL5LIB"};
52
53         use PerlMail::Config;
54         use PerlMail::Lib;
55         }
56
57 use Mail::Audit qw(MAPS);
58 require IO::Handle;
59 use Carp qw(cluck confess);
60 use POSIX qw(WIFEXITED WEXITSTATUS WIFSIGNALED WTERMSIG WIFSTOPPED WSTOPSIG);
61 require POSIX;  # for ceil
62 use User::Utmp;
63 use Getopt::Long;
64 require Mail::Address;
65 require MIME::Words;
66 require Cz::Cstocs;
67 require HTML::Entities;
68 require MIME::Head;
69 require Lingua::EN::Squeeze;
70 require Mail::Mailer;
71 require HTTP::Cookies;
72 require HTTP::Request;
73 require LWP::UserAgent;
74 use URI::Escape 'uri_escape';
75 require WWW::SMS;
76 #require Authen::SASL;  # Sanity check for &Net::SMTP::auth
77 use MIME::Base64;
78 use IPC::Open3;
79 use POSIX ":sys_wait_h";
80
81
82 our($Message,@AuditStored,$DoBell,$Dry);
83 my %alternates_host;    # from @alternates_host
84 my %dnsbl_whitelist;    # from @dnsbl_whitelist
85
86 # from RedHat "procmail-3.22-5"
87 # /i should be only $procmailFROM_DAEMON but how it can hurt to /i all?
88 our $procmailTO_        =qr'^((Original-)?(Resent-)?(To|Cc|Bcc)|(X-Envelope|Apparently(-Resent)?)-To):(.*[^-a-zA-Z0-9_.])?'mio;
89 our $procmailTO         =qr'^((Original-)?(Resent-)?(To|Cc|Bcc)|(X-Envelope|Apparently(-Resent)?)-To):(.*[^a-zA-Z])?'mio;
90 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;
91 $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;
92 # perl-5.8.0 does not cope w/original FROM_MAILER on the third '?' character
93 # Thus we did '([^>]*[^(.%@a-z0-9])?' -> '[^>]*\b', I hope it is somehow similiar
94 # 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;
95
96 my $opt_mode;
97 my $opt_smstest;        # 1 or $smscount
98 my $opt_idle;
99 my $opt_single;
100
101
102 sub process;
103
104 sub stdin
105 {
106         syslogging_restore();   # This is more a debugging session
107         local $/="\n";
108         my $message="";
109         local $_;
110         while (<>) {
111                 die "Invalid 'From ' line: $_" if $message eq "" && !/^From /;
112                 if (!$opt_single && /^From / && $message) {
113                         process $message;
114                         $message="";
115                         }
116                 $message.=$_;
117                 }
118         process $message if $message;
119         exit 0;
120 }
121
122 # FIXME: separate 'perlmail'-transfer together with perlmail-submit away
123 sub inetd
124 {
125         die "Excessive arguments" if @ARGV;
126
127         IO::Handle::autoflush STDOUT 1;
128
129         while (1) {
130                 local $/="\n";
131                 $!=undef();
132                 my $length=<STDIN>;
133                 confess "Unexpected EOF: $!" if !defined $length;
134                 confess "Missing EOL" if $length!~s/\n$//s;
135                 exit 0 if $length eq "BYE";
136                 confess "Unrecognized length: $length" if $length!~/^\d+$/;
137                 my $message;
138                 local $_;
139                 $length==($_=read STDIN,$message,$length) or confess "Got $_ out of required $length bytes";
140                 $length==length $message or confess "False read return ".length($message)." instead of $length";
141                 {
142                         # Do not: local *STDOUT;        # FIXME: fd's inherited by spawned processes are not closed this way!
143                         #         local *STDERR;        # FIXME: fd's inherited by spawned processes are not closed this way!
144                         # as IPC::Open3 and IPC::Open2 will not redirect the output
145                         # and send it to the original socket instead!
146                         local $DoBell=0;
147                         process $message;
148                         if ($DoBell) {
149                                 bell() or warn "Unable to BELL";
150                                 }
151                         }
152                 print STDOUT "1";
153                 }
154         die "NOTREACHED";
155 }
156
157 sub bell
158 {
159         local *BELL;
160         open BELL,">/dev/tty11" or return 0;
161         print BELL "\x07";
162         close BELL or return 0;
163         return 1;
164 }
165
166 sub useridle
167 {
168         return 0 if ! -e "$HOME/away";
169         my %valid_users=map(($_=>1),@ValidUsers);
170         my($idlebest,$linebest);
171         for my $utmp (User::Utmp::getut(),{ "ut_line"=>"psaux" }) {
172                 local $_;
173                 next if defined($_=$utmp->{"ut_type"}) && $_!=User::Utmp::USER_PROCESS();
174                 next if defined($_=$utmp->{"ut_user"}) && !$valid_users{$_};
175                 my $line="/dev/".$utmp->{"ut_line"};
176                 my $atime=(stat $line)[8];
177                 my $what="user \"".($utmp->{"ut_user"} || "<local>")."\", line \"$line\"";
178                 warn "Unable to stat $what" and next if !$atime;
179                 my $idle=time()-$atime;
180                 warn "atime in future for $what" and next if $idle<0;
181                 next if $idle>$IdleMax;
182                 next if defined $idlebest && $idlebest<=$idle;
183                 $idlebest=$idle;
184                 $linebest=$line;
185                 }
186         return !wantarray() ? $idlebest : ($idlebest,$linebest);
187 }
188
189 # return only the very (recursive) first part
190 sub body_first
191 {
192         return $Audit if !$Audit->is_mime();
193         my $first=$Audit;
194         local $_;
195         $first=$_ while $_=$first->parts(0);
196         return $first;
197 }
198
199 sub is_multipart
200 {
201         return 0 if !$Audit->is_mime();
202         return $Audit->is_multipart();
203 }
204
205 sub mimehead
206 {
207 my($part)=@_;
208
209         return $Audit->is_mime() ? $part->head()
210                         : MIME::Head->new([ split "\n",$Audit->head()->as_string() ])
211                         ;
212 }
213
214 sub mimebody
215 {
216 my($part)=@_;
217
218         # be vary cautious here as most of $part methods will encode it!
219         return join "",@{$Audit->body()} if !$Audit->is_mime();
220         my $bodyhandle=$part->bodyhandle();
221         # If MIME is corrupted we don't get bodyhandle() for this part
222         # It may occur when "boundary" is specified by header but no such boundary is found in the body
223         return $bodyhandle->as_string() if $bodyhandle;
224         warn "MIME corrupted, adapting";
225         return $part->body_as_string();
226 }
227
228 sub mime_type
229 {
230 my($part)=@_;
231
232         return $Audit->is_mime() ? $part->effective_type() : mimehead($part)->mime_type();
233 }
234
235 sub body_simple
236 {
237         my $first=body_first();
238         my $r=mimebody($first);
239         my $mime_type=mime_type($first);
240            if ($mime_type eq "text/html") {
241                 # HTML::FormatText just does a useless text layouts
242                 # PerlIO::via::StripHTML probably needs PerlIO input (?)
243                 $r=~s/<[^>]*>//gs;
244                 $r=HTML::Entities::decode($r);
245                 # FIXME: detect charset from <meta> tag: "Content-type: text/html; charset=<???>"
246                 }
247         elsif ($mime_type eq "application/pgp-encrypted"
248                && (my $filename=mimehead($first)->mime_attr("Content-Disposition.filename"))
249                ) {
250                 # first part contains just "Version: 1" as of GnuPG v1.0.4 (GNU/Linux)
251                 $r="pgp($filename)";
252                 }
253         if ((my $charset=mimehead($first)->mime_attr("Content-Type.charset"))) {
254                 my $cstocs=Cz::Cstocs->new($charset,"ascii");
255                 $r=&$cstocs($r) if $cstocs;     # charset may be unknown
256                 }
257         return $r;
258 }
259
260 sub parts_linear
261 {
262 my($part)=@_;
263
264         return $Audit if !$part && !$Audit->is_mime();
265         $part||=$Audit;
266         # don't use '!$part->parts()' as even 0-parts-multiparts are still multiparts
267         return $part if $part->bodyhandle();
268         return map { (parts_linear($_)); } $part->parts();
269 }
270
271 sub smsbuild
272 {
273 my($smsi,$smscount)=@_;
274
275         return "$smsi/$smscount:" if $smscount>1;
276         return "";
277 }
278
279 sub smslens
280 {
281 my($ignorenewmail,$smscount,%args)=@_;
282
283         return map({
284                         my $l=160;
285                         if (!$ignorenewmail) {  # send by mail
286                                 $l-=length("Z emailu FIXME SMSmailError: ");
287                                 $l-=length(smsbuild($_,$smscount));
288                                 }
289                         else {  # send by web
290                                 $l-=6;  # 154 is the max length before split; why?
291                                 }
292                         $l;
293                         } (0..$smscount-1));
294 }
295
296 sub smssend_web
297 {
298 my($squeezed,$smscount,@lens)=@_;
299
300         $smscount=POSIX::ceil($smscount/5);
301         for my $smsi (0..$smscount-1) {
302                 my $len=$lens[$smsi];
303                 $squeezed=~/^.{0,$len}/s;
304                 my $frag=$&;
305                 $squeezed=$';
306                 return 0 if 3!=@SMSwebRcpt;
307                 local *F;
308                 open F,"$HOME/priv/WWW-SMS-$SMSwebRcpt_username.pwd" or return 0;
309                 my $pwd=<F>;
310                 chomp $pwd;
311                 close F;
312                 my $sms=WWW::SMS->new(@SMSwebRcpt,$frag,"username"=>$SMSwebRcpt_username,"passwd"=>$pwd);
313                 for ($sms->gateways("sorted"=>"reliability")) {
314                         last if $sms->send($_);
315                         Sys::Syslog::syslog("warning","Web SMS send failed: %s",$WWW::SMS::Error);
316                         my $void=$WWW::SMS::Error;      # Prevent: Name "WWW::SMS::Error" used only once
317                         }
318                 }
319         return 1;
320 }
321
322 sub smssend_mail
323 {
324 my($squeezed,$smscount,@lens)=@_;
325
326         return 0;
327 }
328
329 sub smssend
330 {
331 my($ignorenewmail,$smscount,%args)=@_;
332
333         my $text=PerlMail::Config::audit_sms(
334                         "subject"=>unmime($Audit->subject()),
335                         "from"=>[ Mail::Address->parse(unmime($Audit->from())) ],
336                         "body"=>substr(body_simple(),0,$MaxBodySMS*(1+0.25*$smscount)),
337                         %args);
338         my $texthead="";
339         ($texthead,$text)=@$text if ref $text;
340         do { print "$texthead\n$text\n"; return; } if $opt_smstest;
341         my @lens=smslens($ignorenewmail,$smscount,%args);
342         my $maxlen=0;
343         $maxlen+=$_ for (@lens);
344         my $squeezed;
345         for my $squeeze (@sms_squeezes) {
346                 local $_;
347                  Lingua::EN::Squeeze::SqueezeControl($_)    if defined ($_=$squeeze->{"SqueezeControl"});
348                 $Lingua::EN::Squeeze::SQZ_OPTIMIZE_LEVEL or 1;  # prevent: Name "$_" used only once: possible typo
349                 $Lingua::EN::Squeeze::SQZ_OPTIMIZE_LEVEL=$_ if defined ($_=$squeeze->{"SQZ_OPTIMIZE_LEVEL"});
350                 $squeezed=Lingua::EN::Squeeze::SqueezeText($text);
351                 chomp $squeezed;
352                 last if $maxlen>=length($texthead.$squeezed);
353                 }
354         $squeezed=substr $texthead.$squeezed,0,$maxlen; # strip if we passed thru last for() above
355         my $recalclen=0;
356         for ($smscount=0;$recalclen<length $squeezed;$smscount++) {
357                 $recalclen+=$lens[$smscount];
358                 }
359         my $func=($ignorenewmail ? \&smssend_web : \&smssend_mail);
360         &$func($squeezed,$smscount,@lens);
361 }
362
363 sub smssend_tryall
364 {
365 my($ignorenewmail,@args)=@_;
366
367         return if !$opt_smstest && !$opt_idle && defined useridle();
368         local $_;
369         return $_ if                     $_=smssend(1,@args);   # web
370         return $_ if !$ignorenewmail && ($_=smssend(0,@args));  # mail
371         warn "Unable to SMSsend the mail";
372         return 0;
373 }
374
375 sub cut
376 {
377         local $_=$_[0];
378         return "<???>" if !defined($_) || /^\s*$/s;
379         s/^\s*//s;
380         s/\s*$//s;
381         return $_ if length($_)<128;
382         return substr($_,0,128)."...";
383 }
384
385 our $profile_eval_depth=0;
386 # ($name || @$name)
387 sub profile_eval
388 {
389 my($name)=@_;
390
391         die "Nesting profile: $name" if 0x10<=(local $profile_eval_depth=$profile_eval_depth+1);
392         return @$name if ref $name;
393         if (!exists $audit_profile{$name}) {
394                 cluck "Profile not found: $name";
395                 return "did";
396         }
397         my @this=@{$audit_profile{$name}};
398         return (profile_eval($'),@this[1..$#this]) if $this[0] && $this[0]=~/^=/;
399         return @this;
400 }
401
402 sub address_show
403 {
404 my($text)=@_;
405
406         return join(",",map({ $_->name() or $_->address(); } Mail::Address->parse($text)));
407 }
408
409 sub unmime
410 {
411 my($text)=@_;
412
413         return join "",map({
414                         my $cstocs;
415                         for (${$_}[1],"iso-8859-2") {
416                                 last if $_ && ($cstocs=Cz::Cstocs->new($_,"ascii"));
417                                 }
418                         &$cstocs(${$_}[0]);
419                         } MIME::Words::decode_mimewords($text));
420 }
421
422 # $folder: "$folder; comment"
423 # $profile as profile_eval($name)
424 sub store
425 {
426 my($folder,$profile,%args)=@_;
427
428         $profile=$store_profile if !$profile;
429         my %do=map({ (!/=/ ? ($_=>1) : ($`=>$')); } profile_eval($profile));
430         Sys::Syslog::syslog("info","%s%s%s: %s: %s",
431                                         (!$Dry ? "" : "--dry: "),
432                                         (!$store_ignore ? "" : "IGNORED[$store_ignore]: "),
433                                         map({ cut($_); } $folder,address_show(unmime($Audit->from())),unmime($Audit->subject())),
434                                         )
435                         if $do{"syslog"} || $Dry;
436         $folder=~s/;.*$//s;
437         push @AuditStored,$folder if $do{"did"};
438         return if $store_ignore || $Dry;
439         $DoBell++ if $do{"bell"};
440         my $folder1="$Mail/".$' if $folder=~/^=/;
441         write_message($folder1) or die $folder1;
442         if ($Mail2) {
443           my $folder2="$Mail2/".$' if $folder=~/^=/;
444           my $old=umask 0117;
445           write_message($folder2) or die $folder2;
446           umask $old;
447         }
448         smssend_tryall $store_ignorenewmail,$do{"sms"},%args if $do{"sms"};
449 }
450
451 our $did_last=0;
452
453 # no &$funcref=>did smth in this block
454 # &$funcref,@funcargs
455 sub did
456 {
457 my($funcref,@funcargs)=@_;
458
459         return @AuditStored!=$did_last if !$funcref;
460         local $did_last=@AuditStored;
461         &$funcref(@funcargs);
462         return @AuditStored!=$did_last;
463 }
464
465 sub writeto
466 {
467 my($filename)=@_;
468
469         local *F;
470         open F,$filename or confess "open $filename: $!";
471         print F $Message or confess "write $filename: $!";
472         close F or confess "close $filename: $!";
473         return 1;
474 }
475
476 # Never use Mail::Audit->store() as it will reformat MIME bodies and possibly corrupt OpenPGP!
477 sub write_message
478 {
479 my($folder)=@_;
480
481         return 1 if $Dry;       # simulate OK
482         local *F;
483         open F,">>$folder" or do { warn "Append \"$folder\": $!"; return 0; };
484         {
485                 local $_;
486                 ($_=$Audit->_audit_get_lock(\*F,$folder)) and do { warn "Lock \"$folder\": $!"; last; };
487                 seek F,0,IO::Handle::SEEK_END or do { warn "Seek-end \"$folder\": $!"; last; };
488                 # FIXME: Check for '^From ' to not to rely on our network peer
489                 print F $Message or do { warn "Write to \"$folder\": $!"; last; };
490                 do { print F "\n"; warn "Missing trailing newline, fixed"; } if $Message!~/\n$/s;
491                 close F or do { warn "Close \"$folder\""; last; };
492                 return 1;       # OK
493                 }
494         warn "MAIL DROPPED for folder: $folder";
495         close F;
496         return 0;       # failed
497 }
498
499 sub process
500 {
501 my($message)=@_;
502
503         local $_=$_;
504         my $save_=$_;
505         $message=~s/(\n)(From )/$1>$2/sg;
506         local $Message=$message;
507         # Cannot call 'local' for our-imported variable:
508         my $Audit_save=$Audit;
509         $Audit=Mail::Audit->new(
510                         "emergency"=>"$Mail/emergency",
511                         "data"=>[map("$_\n",split("\n",$message))],
512                         "log"=>"$HOME/.perlmail.log",
513                         "loglevel"=>99,
514                         );
515         local @AuditStored=();
516         do { smssend 0,$opt_smstest; return; } if $opt_smstest;
517         write_message("$Mail/input") or die;
518         PerlMail::Config::audit();
519         warn 'Corrupted $_, repaired' if defined($save_)!=defined($_) || (defined($_) && $save_ ne $_);
520         # restore:
521         $Audit=$Audit_save;
522 }
523
524 # utility functions:
525
526 sub _spamchildcode
527 {
528 my($err,$isspam)=@_;
529
530         $err=$? if !defined $err;
531         return undef()    if !WIFEXITED($?);
532         return undef()    if  WIFSIGNALED($?);
533         return undef()    if  WIFSTOPPED($?);
534         return 0 if !WEXITSTATUS($?);
535         return $isspam||1 if 1==WEXITSTATUS($?);        # isspam
536         cluck "Possible FIXME or your system is broken (WEXITSTATUS==".WEXITSTATUS($?).")";
537         return 0;       # simulate as not spam
538 }
539
540 # return: true (error-message or "1") if is spam
541 sub spamassassin
542 {
543 my($cmd)=@_;
544
545         #$cmd||="nice spamassassin --exit-code 1 --mbox";
546         $cmd||="spamc -c -s 50000000";
547         # spamassassin has the specified exit code if IS spam, code 0 if NOT spam
548         # See &_spamchildcode for the code 1.
549         local *CHILD;
550         local $SIG{"PIPE"}=sub { warn "spamassassin gave me SIGPIPE: broken pipe"; };
551         # prevent Razor2's: Can't call method "log" on unblessed reference at Razor2/Client/Agent.pm line 212.
552         local $ENV{"HOME"}=$HOME;
553         # 2>/dev/null to prevent error messages to corrupt inetd() output of perlmail-accept(1)
554         open CHILD,"|$cmd &>/dev/null"
555                                         # Workaround: spamassassin-3.1.3-1.fc5
556                                         #.q{|awk '/^X-Spam-Flag: YES$/{if (!body) exit 1;}/^$/{body=1;}'}
557                                         # Original:
558                                         #." >/dev/null 2>/dev/null"
559                         or return 0;
560         print CHILD $Message;
561         close CHILD;
562         return _spamchildcode;
563 }
564
565 # NOTE: returns undef() if !wantarray and the first header is unrecognized
566 # Returns always HOST:IP pair(s).
567 sub Received_for
568 {
569         my @r=();
570         for my $hdr ($Audit->head->get("Received")) {
571                 my($for)=($hdr=~/\bfor\s+\<?(\S+)\>?\b/);
572                 return $for if !wantarray();
573                 push @r,$for if $for;
574                 my($from,$fromaddr)=($hdr=~/\bfrom\s+(?:(\S+)\b.*?)??\[((?:\d{1,3}\.){3}\d{1,3})\]/);
575                 $from=$fromaddr if !defined $from;
576                 push @r,"$from:$fromaddr" if $from;
577                 }
578         return @r;
579 }
580
581 # Extended Mail::Audit::MAPS
582 # $domain,$full,[$timeout]
583 # Returns false if valid, code if spam detected.
584 sub dnsbl
585 {
586 my($domain,$full,$timeout)=@_;
587
588         $timeout||=2;   # sec
589         $Mail::Audit::MAPS::host=$domain;
590         for my $host (Received_for()) {
591                 next if $host!~/^([^:@]*):/;
592                 my $ip=$';
593                 # $1 is DNS name, $ip is IP address
594                 next if $alternates_host{$1};   # leave only foreign hosts
595                 next if $dnsbl_whitelist{$ip};
596
597 #               FIXME: Faking
598 #               {
599 #                       package My::Audit::Faked;
600 #                       sub received { return @{$_[0]->{"received"}}; }
601 #                       }
602 #               my $self_faked=Mail::Audit->new();
603 #               $self_faked->{"received"}=["[$ip]"];
604 #               bless $self_faked,"My::Audit::Faked";
605 #               my $code=Mail::Audit::rblcheck($self_faked,$timeout);
606                 my $code=$Audit->rblcheck($timeout);
607
608                 next if !$code;
609                 # Some 0.0.0.0 etc. found for <linux-kernel@>, see: &Mail::Audit::MAPS::_checkit
610                 # Do not: $code!='1 Invalid IP address '
611                 # as it causes warn.
612                 return $code if $code ne '1 Invalid IP address ';
613                 return if !$full;
614                 }
615 }
616
617 # Returns true if IS virus; the message will contain the virus name
618 sub clamscan
619 {
620 my($cmd)=@_;
621
622         $cmd||='nice clamscan --no-summary -';
623         # clamscan has exit code 1 if IS virus , code 0 if NOT virus
624         # Do not use IPC::Open2 as it would try to use our STDERR which is not valid by: local *STDERR;
625         local(*WR,*RD,*ERR);
626         local $SIG{"PIPE"}=sub { warn "clamscan '$cmd' gave me SIGPIPE: broken pipe"; };
627         my $pid=open3(\*WR,\*RD,\*ERR,$cmd.' 2>&1')
628                         or do { cluck "IPC::Open3 $cmd: $!"; return 0; };
629         print WR $Message;
630         close WR or do { cluck "close WR of $cmd: $!"; return 0; };
631         my $status=do { local $/=undef(); <RD>; };
632         close RD or do { cluck "close RD of $cmd: $!"; return 0; };
633         # Do not: $status.=do { local $/=undef(); <ERR>; };
634         #         close ERR or do { cluck "close ERR of $cmd: $!"; return 0; };
635         # (FIXME) as it causes: Use of uninitialized value in <HANDLE>
636         # waitpid fills $? for: &_spamchildcode
637         local $SIG{"ALRM"}=sub { warn "Timeout $clamscan_waitpid_timeout sec waiting for child $cmd"; };
638         alarm $clamscan_waitpid_timeout;
639         # Do not: WNOHANG
640         # as it would not be enough for clamscan(1) even after all close-s above.
641         my $pidcheck=waitpid($pid,0);
642         alarm 0;
643         my $err=$?;
644         $pidcheck && $pidcheck==$pid
645                         or do { cluck "waitpid for $cmd returned $pidcheck!=$pid"; return 0; };
646         $status=~s/^stdin: //mg;
647         # Prevent: LibClamAV Warning: PGP encoded attachment not scanned
648         $status=~s/^.*\bwarning:.*\n//img;
649         $status=~s/\n$//;
650         return $status if $status ne "OK" && $status;
651         return _spamchildcode $err,$status;
652 }
653
654 sub muttrc_aliases
655 {
656         my %r=();
657         for (muttrc()) {
658                 next if !(my $key=(/^alias\s+(\S+)\s+/)[0]);
659                 for my $addrobj (Mail::Address->parse($')) {
660                         my $addr=$addrobj->address();
661                         my $ref=\$r{"\L$addr"};
662                         $$ref=$key if !$$ref;   # use always the first occurence to prefer nicks
663                         }
664                 }
665         return %r;
666 }
667
668 # FIXME: host may get multiple recipients and thus not showing "for <...>"
669 # FIXME: muttrc_get("from") is too strict
670 sub store_muttrc_alternates
671 {
672 my($prefix,$profile)=@_;
673
674         my $alternates=muttrc_get("alternates") or return;
675         my $alternatesre=qr/$alternates/si;
676         my $From=muttrc_get("from") or return;
677         my $Fromre=qr/^\Q$From\E$/si;
678         my $Fromobj=parseone $From or return;
679         warn "'From' \"$From\" not matched by 'alternates': $alternatesre"
680                         if $From!~/$alternates/si;
681         for my $for (reverse Received_for()) {
682                 $for=~s/:.*$//; # strip IP address here
683                 my $forobj=parseone $for;
684                 if ($forobj && $forobj->host()) {
685                         # it is 'for' our primary address
686                         next if lc($forobj->host()) eq lc($Fromobj->host());    # or 'return'? shouldn't matter
687                         }
688                 next if !$alternates_host{lc $for} && $for!~/$alternatesre/si;
689                 store "$prefix\L$for",($profile || []);
690                 return;
691                 }
692 }
693
694 # $header: ref CODE
695 # $header: !ref => $Audit->get($header)
696 # $maybeaddress: qr/regex/i
697 # $maybeaddress: "string"
698 # $maybeaddress: "<Regexp:regex>"       # hack :-(
699 # $maybeaddress: "<user@host>"
700 # $maybeaddress: "<user@>"
701 # $maybeaddress: "<@host>"
702 sub _headercore
703 {
704 my($re,$justone,$header,$maybeaddress)=@_;
705
706         if (ref $header) {
707                 $header=join(",",&$header());
708                 }
709         elsif ($justone) {
710                 $header=$Audit->get($header);
711                 }
712         else {
713                 $header=join(",",$Audit->get($header));
714         }
715         return 0 if !$header;
716         return $header=~/$maybeaddress/i if "Regexp" eq ref $maybeaddress;
717         return $header=~/$re/i if !defined(my $want=($maybeaddress=~/^\<(.*)\>$/)[0]);
718         my @parsed=Mail::Address->parse($header);
719         warn "'mailto:' forbidden in pattern: $want" if $want=~/^\Qmailto:\E/;
720         return 0 if $justone && 1!=@parsed;
721         return grep {
722                            if ($want=~/^Regexp:/)
723                                 { $_->address()=~/$'/i; }
724                         elsif ($want=~/\@$/)
725                                 { $_->user()   =~/^(?:\Qmailto:\E)?\Q$`\E/i; }
726                         elsif ($want=~/^\@/&&defined $')
727                                 { $_->host()   =~/^\Q$'\E/i; }
728                         else
729                                 { $_->address()=~/^(?:\Qmailto:\E)?\Q$want\E/i; }
730                         } @parsed;
731 }
732
733 sub headerhas
734 {
735 my($header,$substr)=@_;
736
737         return _headercore(qr/\Q$substr\E/i,0,$header,$substr);
738 }
739
740 sub headeris
741 {
742 my($header,$string)=@_;
743
744         cluck if !defined $string;
745         return _headercore(qr/\Q$string\E/i,1,$header,$string);
746 }
747
748 # $header,%$map
749 sub header_remap
750 {
751 my($header,$map)=@_;
752
753         my $text=$Audit->get($header);
754         my $orig=$text;
755         while (my($from,$to)=each(%$map)) {
756                 $text=~s/\b\Q$from\E\b/$to/gsi;
757                 }
758         return if $text eq $orig;
759         $Audit->put_header("X-PerlMail-header_remap-$header",$orig);
760         $Audit->replace_header($header,$text);
761 }
762
763 # MAIN
764
765 $Getopt::Long::ignorecase=0;
766 die "GetOptions error" if !Getopt::Long::GetOptions(
767                   "inetd"    ,sub { $opt_mode=\&inetd; },
768                   "stdin"    ,sub { $opt_mode=\&stdin; },
769                   "single!"  ,\$opt_single,
770                   "dry"      ,\$Dry,
771                   "smstest:s",sub { $opt_mode=\&stdin; $opt_smstest=($_[1] || 1); },
772                   "idle!"    ,\$opt_idle,
773                   "idletest" ,sub { syslogging_restore(); print((defined($_=useridle()) ? $_ : "<undef>")."\n"); exit 0; },
774                   "muttrc"   ,sub { syslogging_restore(); print scalar muttrc(); exit 0; },
775                 );
776 # "Excessive arguments" checked in &inetd
777 die "Missing mode" if !$opt_mode;
778
779 %alternates_host=map((lc($_)=>1),@alternates_host);
780 %dnsbl_whitelist=map((   $_ =>1),@dnsbl_whitelist);
781
782 &$opt_mode();
783 die "NOTREACHED";