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