First production release
[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("lacemail","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 Mail::Audit qw(MAPS);
31 require IO::Handle;
32 use Carp qw(cluck confess);
33 use POSIX qw(WIFEXITED WEXITSTATUS WIFSIGNALED WTERMSIG WIFSTOPPED WSTOPSIG);
34 require POSIX;  # for ceil
35 use User::Utmp;
36 require File::Basename;
37 use Getopt::Long;
38 require Mail::Address;
39 require MIME::Words;
40 require Cz::Cstocs;
41 require HTML::Entities;
42 require MIME::Head;
43 require Lingua::EN::Squeeze;
44 require Mail::Mailer;
45 require HTTP::Cookies;
46 require HTTP::Request;
47 require LWP::UserAgent;
48 use URI::Escape 'uri_escape';
49
50
51 my $HOME="/home/lace";
52 my $Mail="$HOME/Mail";
53 my @ValidUsers=qw(root lace short kratochvil _local);
54 my $IdleMax=60;
55 my $MaxBodySMS=0x1000;  # max bytes to pass to Lingua::EN::Squeeze
56 my $SMSmailError='short+err@ucw.cz';
57 my $SMSwebRcpt='00420602431329';
58 my $SMSmailRcpt=$SMSwebRcpt.'@sms.eurotel.cz';
59 my $SMScontact='<short@ucw.cz>';
60
61 our($Message,$Audit,@AuditStored,$store_ignore,$store_ignorenewmail,$store_profile,$DoBell);
62 our(%audit_profile,@sms_squeezes);      # imported
63
64 # from RedHat "procmail-3.22-5"
65 # /i should be only $procmailFROM_DAEMON but how it can hurt to /i all?
66 our $procmailTO_        =qr'^((Original-)?(Resent-)?(To|Cc|Bcc)|(X-Envelope|Apparently(-Resent)?)-To):(.*[^-a-zA-Z0-9_.])?'mio;
67 our $procmailTO         =qr'^((Original-)?(Resent-)?(To|Cc|Bcc)|(X-Envelope|Apparently(-Resent)?)-To):(.*[^a-zA-Z])?'mio;
68 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;
69 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;
70 # perl-5.8.0 does not cope w/original FROM_MAILER on the third '?' character
71 # Thus we did '([^>]*[^(.%@a-z0-9])?' -> '[^>]*\b', I hope it is somehow similiar
72 # 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;
73
74 my $opt_mode;
75 my $opt_smstest;        # 1 or $smscount
76 my $opt_idle;
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 sub inetd
100 {
101         die "Excessive arguments" if @ARGV;
102
103         IO::Handle::autoflush STDOUT 1;
104
105         while (1) {
106                 local $/="\n";
107                 my $length=<STDIN>;
108                 confess "Unexpected EOF" if !defined $length;
109                 confess "Missing EOL" if $length!~s/\n$//s;
110                 exit 0 if $length eq "BYE";
111                 confess "Unrecognized length: $length" if $length!~/^\d+$/;
112                 my $message;
113                 local $_;
114                 $length==($_=read STDIN,$message,$length) or confess "Got $_ out of required $length bytes";
115                 $length==length $message or confess "False read return ".length($message)." instead of $length";
116                 {
117                         local *STDOUT;
118                         local *STDERR;
119                         local $DoBell=0;
120                         process $message;
121                         if ($DoBell) {
122                                 bell() or warn "Unable to BELL";
123                                 }
124                         }
125                 print STDOUT "1";
126                 }
127         die "NOTREACHED";
128 }
129
130 sub bell
131 {
132         local *BELL;
133         open BELL,">/dev/tty11" or return 0;
134         print BELL "\x07";
135         close BELL or return 0;
136         return 1;
137 }
138
139 sub useridle
140 {
141         my %valid_users=map(($_=>1),@ValidUsers);
142         my($idlebest,$linebest);
143         for my $utmp (User::Utmp::getut(),{ "ut_line"=>"psaux" }) {
144                 local $_;
145                 next if defined($_=$utmp->{"ut_type"}) && $_!=User::Utmp::USER_PROCESS;
146                 next if defined($_=$utmp->{"ut_user"}) && !$valid_users{$_};
147                 my $line="/dev/".$utmp->{"ut_line"};
148                 my $atime=(stat $line)[8];
149                 my $what="user \"".($utmp->{"ut_user"} || "<local>")."\", line \"$line\"";
150                 warn "Unable to stat $what" and next if !$atime;
151                 my $idle=time()-$atime;
152                 warn "atime in future for $what" and next if $idle<0;
153                 next if $idle>$IdleMax;
154                 next if defined $idlebest && $idlebest<=$idle;
155                 $idlebest=$idle;
156                 $linebest=$line;
157                 }
158         return !wantarray() ? $idlebest : ($idlebest,$linebest);
159 }
160
161 # return only the very (recursive) first part
162 sub body_first
163 {
164         return $Audit if !$Audit->is_mime();
165         my $first=$Audit;
166         local $_;
167         $first=$_ while $_=$first->parts(0);
168         return $first;
169 }
170
171 sub mimehead
172 {
173 my($part)=@_;
174
175         return $Audit->is_mime() ? $part->head()
176                         : MIME::Head->new([ split "\n",$Audit->head()->as_string() ])
177                         ;
178 }
179
180 sub mimebody
181 {
182 my($part)=@_;
183
184         # be vary cautious here as most of $part methods will encode it!
185         return join "",@{$Audit->body()} if !$Audit->is_mime();
186         my $bodyhandle=$part->bodyhandle();
187         # If MIME is corrupted we don't get bodyhandle() for this part
188         # It may occur when "boundary" is specified by header but no such boundary is found in the body
189         return $bodyhandle->as_string() if $bodyhandle;
190         warn "MIME corrupted, adapting";
191         return $part->body_as_string();
192 }
193
194 sub mime_type
195 {
196 my($part)=@_;
197
198         return $Audit->is_mime() ? $part->effective_type() : mimehead($part)->mime_type();
199 }
200
201 sub body_simple
202 {
203         my $first=body_first();
204         my $r=mimebody($first);
205         my $mime_type=mime_type($first);
206            if ($mime_type eq "text/html") {
207                 # HTML::FormatText just does a useless text layouts
208                 # PerlIO::via::StripHTML probably needs PerlIO input (?)
209                 $r=~s/<[^>]*>//gs;
210                 $r=HTML::Entities::decode($r);
211                 # FIXME: detect charset from <meta> tag: "Content-type: text/html; charset=<???>"
212                 }
213         elsif ($mime_type eq "application/pgp-encrypted"
214                && (my $filename=mimehead($first)->mime_attr("Content-Disposition.filename"))
215                ) {
216                 # first part contains just "Version: 1" as of GnuPG v1.0.4 (GNU/Linux)
217                 $r="pgp($filename)";
218                 }
219         if ((my $charset=mimehead($first)->mime_attr("Content-Type.charset"))) {
220                 my $cstocs=Cz::Cstocs->new($charset,"ascii");
221                 $r=&$cstocs($r) if $cstocs;     # charset may be unknown
222                 }
223         return $r;
224 }
225
226 sub parts_linear
227 {
228 my($part)=@_;
229
230         return $Audit if !$part && !$Audit->is_mime();
231         $part||=$Audit;
232         # don't use '!$part->parts()' as even 0-parts-multiparts are still multiparts
233         return $part if $part->bodyhandle();
234         return map { (parts_linear($_)); } $part->parts();
235 }
236
237 sub smsbuild
238 {
239 my($smsi,$smscount)=@_;
240
241         return "$smsi/$smscount:" if $smscount>1;
242         return "";
243 }
244
245 # patch for http://kiwi.ms.mff.cuni.cz/%7Etom/programming/src/sendsms.tar.gz/sendsms.pl
246 my $agent=LWP::UserAgent->new();
247 $agent->agent("LaceMail $VERSION; contact=$SMScontact; ");
248 my($request1,$response1);       # for &send_cz_eurotel
249 my($name,$value,$type,$disabled,$q2);
250
251 # &send_cz_eurotel returns: error
252 # BEGIN http://kiwi.ms.mff.cuni.cz/%7Etom/programming/src/sendsms.tar.gz/sendsms.pl
253 sub parse_inputs
254 {
255         my ($resp) = @_;
256         my @inputs;
257         my $ct;
258         my @c;
259         $ct=$resp->content();
260
261         @c=split '>', $ct;
262         grep {
263            if (/(<input|<select|<textarea)([^<>]*)(>|$)/i) { 
264              my $txt=$2, $name="", $value="", $type="x", $disabled=0;
265              my $ipoc;
266              if ($txt =~ /type="([^"]*)"/i) { $type=$1; }
267              elsif ($txt =~ /type=([^" ]*)[ >]/i) { $type=$1; }
268              if ($txt =~ /name="([^"]*)"/i) { $name=$1; }
269              elsif ($txt =~ /name=([^" ]*)[ >]/i) { $name=$1; }
270              if ($txt =~ /value="([^"]*)"/i) { $value=$1; }
271              elsif ($txt =~ /value=([^" ]*)[ >]/i) { $value=$1; }
272              if ($txt =~ /disabled/i) { $disabled=1; }
273              if ($name ne "" && $type ne "" && $type ne "button" && ! $disabled)
274              {
275                $ipoc=$#inputs;
276                $inputs[$ipoc+1][0]=$name;
277                $inputs[$ipoc+1][1]=$value;
278              }
279            }
280         0; } @c;
281         return @inputs;
282 }
283
284 sub make_query
285 {
286         my (@inputs) = @_;
287
288         my $i;
289         my $query = "";
290
291         for ($i=0; $i<=$#inputs; $i++)
292         {
293           my($q1, $q2);
294           if ($i>0) { $query="$query&"; }
295           $q1=uri_escape($inputs[$i][0]);
296           $q2=uri_escape($inputs[$i][1]);
297           $query="$query$q1=$q2";
298         }
299
300         #change @ and space back
301         $query=~ s/%20/+/g;
302         $query=~ s/%40/@/g;
303         return $query;
304 }
305
306 sub send_cz_eurotel
307 {
308         my ($id,$text,$mail,$directd) = @_;
309         my $src_url = "http://www2.eurotel.cz/sms/index.html";
310         my @inputs;
311         my $query = "";
312         my $cookie = HTTP::Cookies->new;
313         my $pref;
314
315         #check if correct number
316         if (substr($id,0,5)!="00420") { return -1; }
317         $pref=substr($id,5,3);
318         if (!($pref eq "601" || $pref eq "602" || $pref eq "606" || $pref eq "607"  || ($pref ge "720" && $pref le "729"))) { return -1; }
319         
320         #get form page, extract cookies
321         $request1=new HTTP::Request('GET', "$src_url?n_pagestyle=new");
322         $response1=$agent->request($request1);
323         if ($response1->code != 200) { return -3; }
324         $cookie->extract_cookies($response1);
325
326         #parse the form
327         @inputs=parse_inputs($response1);
328         
329         #fill the form
330         $inputs[2][1]=substr($id,5,3);
331         $inputs[3][1]=substr($id,8,6);
332         $inputs[4][1]=$mail;
333
334         #direct display
335         $inputs[6][1]="sms";
336         $inputs[6][1]="show" if ($directd>0);
337
338         $inputs[7][1]=$text;
339
340 #       for ($i=0; $i<=$#inputs; $i++) { print "[$i] $inputs[$i][0] $inputs[$i][1]\n"; } 
341
342         #make query
343         $query=make_query(@inputs);
344
345         #POST the form
346         my $header = new HTTP::Headers( 
347                 'Content-Length' => length($query),
348                 'Content-Type' => 'application/x-www-form-urlencoded',
349                 'Accept' => '*/*',
350                 'Referer' => $src_url
351         );
352         my $request2 = new HTTP::Request('POST',$src_url, $header, $query);
353         $cookie->add_cookie_header($request2);
354         my $response2 = $agent->request($request2);
355
356         if ($response2->code != 200) { return -3; }
357
358         #check for success
359         if ($response2->content() !~ /byla.*odesl.*na na SMS centrum/)
360         {
361           return -2;
362         }
363         return 0;
364 }
365 # END http://kiwi.ms.mff.cuni.cz/%7Etom/programming/src/sendsms.tar.gz/sendsms.pl
366
367 sub smslens
368 {
369 my($ignorenewmail,$smscount,%args)=@_;
370
371         return map({
372                         my $l=160;
373                         if (!$ignorenewmail) {  # send by mail
374                                 $l-=length("Z emailu $SMSmailError: ");
375                                 $l-=length(smsbuild($_,$smscount));
376                                 }
377                         else {  # send by web
378                                 $l-=length("Z WWW x/5: ");
379                                 $l-=length(smsbuild($_,POSIX::ceil($smscount/5)));
380                                 }
381                         $l;
382                         } (0..$smscount-1));
383 }
384
385 sub smssend_web
386 {
387 my($squeezed,$smscount,@lens)=@_;
388
389         $smscount=POSIX::ceil($smscount/5);
390         for my $smsi (0..$smscount-1) {
391                 my $len=$lens[$smsi];
392                 $squeezed=~/^.{0,$len}/s;
393                 my $frag=$&;
394                 $squeezed=$';
395                 return 0 if send_cz_eurotel($SMSwebRcpt,$frag,"",0);
396                 }
397         return 1;
398 }
399
400 sub smssend_mail
401 {
402 my($squeezed,$smscount,@lens)=@_;
403
404         my $recalclen=0;
405         for ($smscount=0;$recalclen<length $squeezed;$smscount++) {
406                 $recalclen+=$lens[$smscount];
407                 }
408         for my $smsi (0..$smscount-1) {
409                 my $len=$lens[$smsi];
410                 $squeezed=~/^.{0,$len}/s;
411                 my $frag=$&;
412                 $squeezed=$';
413                 my $mail=Mail::Mailer->new("sendmail","-f","$SMSmailError");
414                 $mail->open({
415                                 "To"=>$SMSmailRcpt,
416                                 "From"=>$SMSmailError,  # no longer displayed anyway
417                                 "X-LaceMail-Version"=>$VERSION,
418                                 "X-LaceMail-Contact"=>$SMScontact,
419                                 });
420                 print $mail smsbuild($smsi,$smscount).$frag."\n";
421                 # FIXME: check errors
422                 $mail->close();
423                 }
424         return 1;
425 }
426
427 sub smssend
428 {
429 my($ignorenewmail,$smscount,%args)=@_;
430
431         my %aliases=muttrc_aliases();
432         my $text=audit_sms(
433                         "subject"=>unmime($Audit->subject()),
434                         "from"=>[ map({ $_=$_->address(); $_="\L$_"; $aliases{$_} || $_; } Mail::Address->parse(unmime($Audit->from()))) ],
435                         "body"=>substr(body_simple(),0,$MaxBodySMS*(1+0.25*$smscount)),
436                         %args);
437         my $texthead="";
438         ($texthead,$text)=@$text if ref $text;
439         do { print "$texthead\n$text\n"; return; } if $opt_smstest;
440         my @lens=smslens($ignorenewmail,$smscount,%args);
441         my $maxlen=0;
442         $maxlen+=$_ for (@lens);
443         my $squeezed;
444         for my $squeeze (@sms_squeezes) {
445                 local $_;
446                  Lingua::EN::Squeeze::SqueezeControl($_)    if defined ($_=$squeeze->{"SqueezeControl"});
447                 $Lingua::EN::Squeeze::SQZ_OPTIMIZE_LEVEL or 1;  # prevent: Name "$_" used only once: possible typo
448                 $Lingua::EN::Squeeze::SQZ_OPTIMIZE_LEVEL=$_ if defined ($_=$squeeze->{"SQZ_OPTIMIZE_LEVEL"});
449                 $squeezed=Lingua::EN::Squeeze::SqueezeText($text);
450                 chomp $squeezed;
451                 last if $maxlen>=length($texthead.$squeezed);
452                 }
453         $squeezed=substr $texthead.$squeezed,0,$maxlen; # strip if we passed thru last for() above
454         my $recalclen=0;
455         for ($smscount=0;$recalclen<length $squeezed;$smscount++) {
456                 $recalclen+=$lens[$smscount];
457                 }
458         my $func=($ignorenewmail ? \&smssend_web : \&smssend_mail);
459         &$func($squeezed,$smscount,@lens);
460 }
461
462 sub smssend_tryall
463 {
464 my($ignorenewmail,@args)=@_;
465
466         return if !$opt_smstest && !$opt_idle && defined useridle();
467         local $_;
468         return $_ if                     $_=smssend(1,@args);   # web
469         return $_ if !$ignorenewmail && ($_=smssend(0,@args));  # mail
470         warn "Unable to SMSsend the mail";
471         return 0;
472 }
473
474 sub cut
475 {
476         local $_=$_[0];
477         return "<???>" if !defined($_) || /^\s*$/s;
478         s/^\s*//s;
479         s/\s*$//s;
480         return $_ if length($_)<64;
481         return substr($_,0,64)."...";
482 }
483
484 our $profile_eval_depth=0;
485 # ($name || @$name)
486 sub profile_eval
487 {
488 my($name)=@_;
489
490         die "Nesting profile: $name" if 0x10<=(local $profile_eval_depth=$profile_eval_depth+1);
491         return @$name if ref $name;
492         die "Profile not found: $name" if !exists $audit_profile{$name};
493         my @this=@{$audit_profile{$name}};
494         return (profile_eval($'),@this[1..$#this]) if $this[0] && $this[0]=~/^=/;
495         return @this;
496 }
497
498 sub address_show
499 {
500 my($text)=@_;
501
502         return join(",",map({ $_->name() or $_->address(); } Mail::Address->parse($text)));
503 }
504
505 sub unmime
506 {
507 my($text)=@_;
508
509         return join "",map({
510                         my $cstocs;
511                         for (${$_}[1],"iso-8859-2") {
512                                 last if $_ && ($cstocs=Cz::Cstocs->new($_,"ascii"));
513                                 }
514                         &$cstocs(${$_}[0]);
515                         } MIME::Words::decode_mimewords($text));
516 }
517
518 # $folder: "$folder; comment"
519 # $profile as profile_eval($name)
520 sub store
521 {
522 my($folder,$profile,%args)=@_;
523
524         $profile=$store_profile if !$profile;
525         my %do=map({ (!/=/ ? ($_=>1) : ($`=>$')); } profile_eval($profile));
526         Sys::Syslog::syslog("info","%s%s: %s: %s",
527                                         (!$store_ignore ? "" : "IGNORED[$store_ignore]: "),
528                                         map({ cut($_); } $folder,address_show(unmime($Audit->from())),unmime($Audit->subject())),
529                                         )
530                         if $do{"syslog"};
531         $DoBell++ if $do{"bell"};
532         $folder=~s/;.*$//s;
533         $folder="$Mail/".$' if $folder=~/^=/;
534         if (!$store_ignore) {
535                 $Audit->noexit(1);
536                 $Audit->accept($folder);
537                 }
538         smssend_tryall $store_ignorenewmail,$do{"sms"},%args if $do{"sms"};
539         push @AuditStored,$folder if $do{"did"};
540 }
541
542 our $did_last=0;
543
544 # no &$funcref=>did smth in this block
545 # &$funcref,@funcargs
546 sub did
547 {
548 my($funcref,@funcargs)=@_;
549
550         return @AuditStored!=$did_last if !$funcref;
551         local $did_last=@AuditStored;
552         &$funcref(@funcargs);
553         return @AuditStored!=$did_last;
554 }
555
556 sub process
557 {
558 my($message)=@_;
559
560         local $_=$_;
561         my $save_=$_;
562         local $Message=$message;
563         local $Audit=Mail::Audit->new(
564                         "emergency"=>"$Mail/emergency",
565                         "data"=>[map("$_\n",split("\n",$message))],
566                         );
567         local @AuditStored=();
568         do { smssend $opt_smstest; return; } if $opt_smstest;
569         audit();
570         warn 'Corrupted $_, repaired' if defined($save_)!=defined($_) || (defined($_) && $save_ ne $_);
571 }
572
573 # utility functions:
574
575 # return: true (error-message or "1") if is spam
576 sub razor2
577 {
578         # razor-check has exit code 1 if NOT spam, code 0 if IS spam
579         local *CHILD;
580         # prevent Razor2's: Can't call method "log" on unblessed reference at Razor2/Client/Agent.pm line 212.
581         local $ENV{"HOME"}=$HOME;
582         open CHILD,'|'
583                                         .'('.'(razor-check 2>&1;echo >&3 $?)'
584                                                         .'|sed "s/^/razor-check: /"'
585                                                         .'|logger -t "lacemail['.$$.']" -p mail.crit'
586                                                         .') 3>&1'
587                                         .'|exit `cat`'
588                         or return 0;
589         print CHILD $Message;
590         my $return;
591         {
592                 local $/=undef();
593                 $return=<CHILD> || 1;
594                 }
595         close CHILD;
596         return undef() if !WIFEXITED($?);
597         return undef() if  WIFSIGNALED($?);
598         return undef() if  WIFSTOPPED($?);
599         return undef() if WEXITSTATUS($?);
600         return $return; # is-spam
601 }
602
603 # NOTE: returns undef() if !wantarray and the first header is unrecognized
604 sub Received_for
605 {
606         my @r=();
607         for my $hdr ($Audit->head->get("Received")) {
608                 my($for)=($hdr=~/\bfor\s+\<?(\S+)\>?\b/);
609                 return $for if !wantarray();
610                 push @r,$for if $for;
611                 }
612         return @r;
613 }
614
615 our %muttrc_pending=();
616 sub muttrc
617 {
618 my($muttrc)=@_;
619
620         $muttrc||="$HOME/.muttrc";
621         $muttrc=~s/^\~/$HOME/;
622         do { warn "Looping muttrc, ignoring: $muttrc"; return (); } if $muttrc_pending{$muttrc};
623         local $muttrc_pending{$muttrc}=1;
624         local *MUTTRC;
625         open MUTTRC,$muttrc or do { warn "open \"$muttrc\": $!"; return (); };
626         local $/="\n";
627         local $_;
628         my @r=();
629         # far emulation mutt/init.c/mutt_parse_rc_line()
630         while (<MUTTRC>) {
631                 s/^[\s;]*//s;
632                 s/[#;].*$//s;
633                 s/\s*$//s;
634                 next if !/^(\S+)\s*/s;
635                 if ($1 eq "source") {
636                         $_=$';
637                         do { warn "Wrong 'source' parameters at $muttrc:$.: $_"; next; } if !/^\S+$/;
638                         push @r,muttrc($_);
639                         next;
640                         }
641                 push @r,$_;
642                 }
643         close MUTTRC or warn "close \"$muttrc\": $!";
644         return wantarray() ? @r : join("",map("$_\n",@r));
645 }
646
647 sub muttrc_get
648 {
649 my(@headers)=@_;
650
651         my @r=map({ (ref $_ ? $_ : qr/^\s*set\s+\Q$_\E\s*=\s*"([^"]*)"\s*$/si); } @headers);
652         my %r=map(($_=>undef()),@r);
653         for (muttrc()) {
654                 for my $ritem (@r) {
655                         /$ritem/si or next;
656                         $r{$ritem}=$1;
657                         }
658                 }
659         for my $var (grep { !defined($r{$_}) } @r) {
660                 warn "Variable '$var' not found in muttrc";
661                 return undef();
662                 }
663         return wantarray() ? %r : $r{$r[0]};
664 }
665
666 sub muttrc_aliases
667 {
668         my %r=();
669         for (muttrc()) {
670                 next if !(my $key=(/^alias\s+(\S+)\s+/)[0]);
671                 for my $addrobj (Mail::Address->parse($')) {
672                         my $addr=$addrobj->address();
673                         my $ref=\$r{"\L$addr"};
674                         $$ref=$key if !$$ref;
675                         }
676                 }
677         return %r;
678 }
679
680 sub store_muttrc_alternates
681 {
682 my($prefix,$profile)=@_;
683
684         my $alternates=muttrc_get("alternates") or return;
685         my $alternatesre=qr/$alternates/si;
686         my $From=muttrc_get(qr/^\s*my_hdr\s+From:.*\<(\S+)\>\s*$/si) or return;
687         my $Fromre=qr/^\Q$From\E$/si;
688         warn "'From' \"$From\" not matches by 'alternates': $alternatesre"
689                         if $From!~/$alternates/si;
690         for my $for (reverse Received_for()) {
691                 return if $for=~/$From/si;
692                 next if $for!~/$alternatesre/si;
693                 store "$prefix\L$for",($profile || []);
694                 return;
695                 }
696 }
697
698 # $header: ref CODE
699 # $header: !ref => $Audit->get($header)
700 # $maybeaddress: qr/regex/i
701 # $maybeaddress: "string"
702 # $maybeaddress: "<Regexp:regex>"       # hack :-(
703 # $maybeaddress: "<user@host>"
704 # $maybeaddress: "<user@>"
705 # $maybeaddress: "<@host>"
706 sub _headercore
707 {
708 my($re,$justone,$header,$maybeaddress)=@_;
709
710         if (ref $header) {
711                 $header=join(",",&$header());
712                 }
713         else {
714                 $header=$Audit->get($header);
715                 }
716         return 0 if !$header;
717         return $header=~/$maybeaddress/i if "Regexp" eq ref $maybeaddress;
718         return $header=~/$re/i if !defined(my $want=($maybeaddress=~/^\<(.*)\>$/)[0]);
719         my @parsed=Mail::Address->parse($header);
720         warn "'mailto:' forbidden in pattern: $want" if $want=~/^\Qmailto:\E/;
721         return 0 if $justone && 1!=@parsed;
722         return grep {
723                            if ($want=~/^Regexp:/)
724                                 { $_->address()=~/$'/i; }
725                         elsif ($want=~/\@$/)
726                                 { $_->user()   =~/^(?:\Qmailto:\E)?\Q$`\E/i; }
727                         elsif ($want=~/^\@/)
728                                 { $_->host()   =~/^\Q$'\E/i; }
729                         else
730                                 { $_->address()=~/^(?:\Qmailto:\E)?\Q$want\E/i; }
731                         } @parsed;
732 }
733
734 sub headerhas
735 {
736 my($header,$substr)=@_;
737
738         return _headercore(qr/\Q$substr\E/i,0,$header,$substr);
739 }
740
741 sub headeris
742 {
743 my($header,$string)=@_;
744
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-LaceMail-header_remap-$header",$orig);
760         $Audit->replace_header($header,$text);
761 }
762
763
764 # MAIN
765
766 my $basedir=File::Basename::dirname($0);
767 $Getopt::Long::ignorecase=0;
768 die "GetOptions error" if !Getopt::Long::GetOptions(
769                   "inetd"    ,sub { $opt_mode=\&inetd; },
770                   "stdin"    ,sub { $opt_mode=\&stdin; },
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                 "d|basedir=s",\&basedir,
776                 );
777 # "Excessive arguments" checked in &inetd
778 die "Missing mode" if !$opt_mode;
779
780 my $filenameMyAudit="$basedir/My-Audit.pm";
781 open AUDIT,$filenameMyAudit or die "open \"$filenameMyAudit\": $!";
782 {
783         local $/=undef();
784         eval <AUDIT> or die "eval \"$filenameMyAudit\": $@";
785         audit_init();
786         }
787 close AUDIT or warn "close \"$filenameMyAudit\": $!";
788
789 &$opt_mode();
790 die "NOTREACHED";