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