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