]> git.treefish.org Git - fex.git/blob - lib/fex.pp
Original release 20160919
[fex.git] / lib / fex.pp
1 #  -*- perl -*-
2
3 use 5.008;
4 use utf8;
5 use Fcntl               qw':flock :seek :mode';
6 use IO::Handle;
7 use IPC::Open3;
8 use Encode;
9 use Digest::MD5         qw'md5_hex';
10 use File::Basename;
11 use Sys::Hostname;
12 use Symbol              qw'gensym';
13
14 # set and untaint ENV if not in CLI (fexsrv provides clean ENV)
15 unless (-t) {
16   foreach my $v (keys %ENV) {
17     ($ENV{$v}) = ($ENV{$v} =~ /(.*)/s) if defined $ENV{$v};
18   }
19   $ENV{PATH}     = '/usr/local/bin:/bin:/usr/bin';
20   $ENV{IFS}      = " \t\n";
21   $ENV{BASH_ENV} = '';
22 }
23
24 unless ($FEXLIB = $ENV{FEXLIB} and -d $FEXLIB) {
25   die "$0: found no FEXLIB - fexsrv needs full path\n"
26 }
27
28 $FEXLIB =~ s:/+:/:g;
29 $FEXLIB =~ s:/$::;
30
31 # $FEXHOME is top-level directory of F*EX installation or vhost
32 # $ENV{HOME} is login-directory of user fex
33 # in default-installation both are equal, but they may differ
34 $FEXHOME = $ENV{FEXHOME} or $ENV{FEXHOME} = $FEXHOME = dirname($FEXLIB);
35
36 umask 077;
37
38 # defaults
39 $hostname = gethostname();
40 $tmpdir = $ENV{TMPDIR} || '/var/tmp';
41 $spooldir = $FEXHOME.'/spool';
42 $docdir = $FEXHOME.'/htdocs';
43 $logdir = $spooldir;
44 $autodelete = 'YES';
45 $overwrite = 'YES';
46 $limited_download = 'YES';      # multiple downloads only from same client
47 $fex_yourself = 'YES';          # allow SENDER = RECIPIENT
48 $keep = 5;                      # days
49 $recipient_quota = 0;           # MB
50 $sender_quota = 0;              # MB
51 $timeout = 30;                  # seconds
52 $bs = 2**16;                    # I/O blocksize
53 $DS = 60*60*24;                 # seconds in a day
54 $MB = 1024*1024;                # binary Mega
55 $use_cookies = 1;
56 $sendmail = '/usr/lib/sendmail';
57 $sendmail = '/usr/sbin/sendmail' unless -x $sendmail;
58 $mailmode = 'auto';
59 $bcc = 'fex';
60 $default_locale = '';
61 $fop_auth = 0;
62 $mail_authid = 'yes';
63 $force_https = 0;
64 $debug = 0;
65 @forbidden_user_agents = ('FDM');
66
67 # https://securityheaders.io/
68 # https://scotthelme.co.uk/hardening-your-http-response-headers/
69 # http://content-security-policy.com/
70 @extra_header = (
71   # "Content-Security-Policy: sandbox allow-forms allow-scripts",
72   "Content-Security-Policy: script-src 'self' 'unsafe-inline'",
73   "X-Frame-Options: SAMEORIGIN",
74   "X-XSS-Protection: 1; mode=block",
75   "X-Content-Type-Options: nosniff",
76 );
77
78 $FHS = -f '/etc/fex/fex.ph' and -d '/usr/share/fex/lib';
79 # Debian FHS
80 if ($FHS) {
81   $ENV{FEXHOME} = $FEXHOME = '/usr/share/fex';
82   $spooldir = '/var/spool/fex';
83   $logdir = '/var/log/fex';
84   $docdir = '/var/lib/fex/htdocs';
85   $notify_newrelease = '';
86 }
87
88 # allowed download managers (HTTP User-Agent)
89 $adlm = '^(Axel|fex)';
90
91 # local config
92 require "$FEXLIB/fex.ph" or die "$0: cannot load $FEXLIB/fex.ph - $!";
93
94 $fop_auth       = 0 if $fop_auth        =~ /no/i;
95 $mail_authid    = 0 if $mail_authid     =~ /no/i;
96 $force_https    = 0 if $force_https     =~ /no/i;
97 $debug          = 0 if $debug           =~ /no/i;
98
99 @logdir = ($logdir) unless @logdir;
100 $logdir = $logdir[0];
101
102 # allowed multi download recipients: from any ip, any times
103 if (@mailing_lists) {
104   $amdl = '^('.join('|',map { quotewild($_) } @mailing_lists).')$';
105 } else {
106   $amdl = '^-$';
107 }
108
109 # check for name based virtual host
110 $vhost = vhost($ENV{'HTTP_HOST'});
111
112 $RB = 0; # read POST bytes
113
114 push @doc_dirs,$docdir;
115 foreach my $ld (glob "$FEXHOME/locale/*/htdocs") {
116   push @doc_dirs,$ld;
117 }
118
119 $nomail = ($mailmode =~ /^MANUAL|nomail$/i);
120
121 if (not $nomail and not -x $sendmail) {
122   http_die("found no sendmail");
123 }
124 http_die("cannot determine the server hostname") unless $hostname;
125
126 $ENV{PROTO} = 'http' unless $ENV{PROTO};
127 $keep = $keep_default ||= $keep || 5;
128 $purge ||= 3*$keep;
129 $fra = $ENV{REMOTE_ADDR} || '';
130 $sid = $ENV{SID} || '';
131
132 $dkeydir = "$spooldir/.dkeys"; # download keys
133 $ukeydir = "$spooldir/.ukeys"; # upload keys
134 $akeydir = "$spooldir/.akeys"; # authentification keys
135 $skeydir = "$spooldir/.skeys"; # subuser authentification keys
136 $gkeydir = "$spooldir/.gkeys"; # group authentification keys
137 $xkeydir = "$spooldir/.xkeys"; # extra download keys
138 $lockdir = "$spooldir/.locks"; # download lock files
139
140 if (my $ra = $ENV{REMOTE_ADDR} and $max_fail) {
141   mkdirp("$spooldir/.fail");
142   $faillog = "$spooldir/.fail/$ra";
143 }
144
145 unless ($admin) {
146   $admin = $ENV{SERVER_ADMIN} ? $ENV{SERVER_ADMIN} : 'fex@'.$hostname;
147 }
148
149 # $ENV{SERVER_ADMIN} may be set empty in fex.ph!
150 $ENV{SERVER_ADMIN} = $admin unless defined $ENV{SERVER_ADMIN};
151
152 $mdomain ||= '';
153
154 if ($use_cookies) {
155   if (my $cookie = $ENV{HTTP_COOKIE}) {
156     if    ($cookie =~ /\bakey=(\w+)/) { $akey = $1 }
157     # elsif ($cookie =~ /\bskey=(\w+)/) { $skey = $1 }
158   }
159 }
160
161 if (@locales) {
162   if ($default_locale and not grep /^$default_locale$/,@locales) {
163     push @locales,$default_locale;
164   }
165   if (@locales == 1) {
166     $default_locale = $locales[0];
167   }
168 }
169
170 $default_locale ||= 'english';
171
172 # $durl is first default fop download URL
173 # @durl is optional mandatory fop download URL list (from fex.ph)
174 unless ($durl) {
175   my $host = '';
176   my $port = 80;
177   my $xinetd = '/etc/xinetd.d/fex';
178
179   if (@durl) {
180     $durl = $durl[0];
181   } elsif ($ENV{HTTP_HOST} and $ENV{PROTO}) {
182
183     ($host,$port) = split(':',$ENV{HTTP_HOST}||'');
184     $host = $hostname;
185
186     unless ($port) {
187       $port = 80;
188       if (open $xinetd,$xinetd) {
189         while (<$xinetd>) {
190           if (/^\s*port\s*=\s*(\d+)/) {
191             $port = $1;
192             last;
193           }
194         }
195         close $xinetd;
196       }
197     }
198
199     # use same protocal as uploader for download
200     if ($ENV{PROTO} eq 'https' and $port == 443 or $port == 80) {
201       $durl = "$ENV{PROTO}://$host/fop";
202     } else {
203       $durl = "$ENV{PROTO}://$host:$port/fop";
204     }
205   } else {
206     if (open $xinetd,$xinetd) {
207       while (<$xinetd>) {
208         if (/^\s*port\s*=\s*(\d+)/) {
209           $port = $1;
210           last;
211         }
212       }
213       close $xinetd;
214     }
215     if ($port == 80) {
216       $durl = "http://$hostname/fop";
217     } else {
218       $durl = "http://$hostname:$port/fop";
219     }
220   }
221 }
222 @durl = ($durl) unless @durl;
223
224
225 sub reexec {
226   exec($FEXHOME.'/bin/fexsrv') if $ENV{KEEP_ALIVE};
227   exit;
228 }
229
230
231 sub jsredirect {
232   $url = shift;
233   $cont = shift || 'request accepted: continue';
234
235   http_header('200 ok');
236   print html_header($head||$ENV{SERVER_NAME});
237   pq(qq(
238     '<script type="text/javascript">'
239     '  window.location.replace("$url");'
240     '</script>'
241     '<noscript>'
242     '  <h3><a href="$url">$cont</a></h3>'
243     '</noscript>'
244     '</body></html>'
245   ));
246   &reexec;
247 }
248
249
250 sub debug {
251   print header(),"<pre>\n";
252   print "file = $file\n";
253   foreach $v (keys %ENV) {
254     print $v,' = "',$ENV{$v},"\"\n";
255   }
256   print "</pre><p>\n";
257 }
258
259
260 sub nvt_print {
261   foreach (@_) { syswrite STDOUT,"$_\r\n" }
262 }
263
264
265 sub html_quote {
266   local $_ = shift;
267
268   s/&/&amp;/g;
269   s/</&lt;/g;
270   s/\"/&quot;/g;
271
272   return $_;
273 }
274
275
276
277 sub http_header {
278
279   my $status = shift;
280   my $msg = $status;
281
282   return if $HTTP_HEADER;
283   $HTTP_HEADER = $status;
284
285   $msg =~ s/^\d+\s*//;
286
287   nvt_print("HTTP/1.1 $status");
288   nvt_print("X-Message: $msg");
289   # nvt_print("X-SID: $ENV{SID}") if $ENV{SID};
290   nvt_print("Server: fexsrv");
291   nvt_print("Expires: 0");
292   nvt_print("Cache-Control: no-cache");
293   if ($force_https) {
294     # https://www.owasp.org/index.php/HTTP_Strict_Transport_Security
295     # https://scotthelme.co.uk/hsts-the-missing-link-in-tls/
296     nvt_print("Strict-Transport-Security: max-age=2851200; preload");
297   }
298   nvt_print($_) foreach(@extra_header);
299   if ($use_cookies) {
300     $akey = md5_hex("$from:$id") if $id and $from;
301     if ($akey) {
302       nvt_print("Set-Cookie: akey=$akey; path=/; Max-Age=9999; Discard");
303     }
304     # if ($skey) {
305     #   nvt_print("Set-Cookie: skey=$skey; Max-Age=9999; Discard");
306     # }
307     if ($locale) {
308       nvt_print("Set-Cookie: locale=$locale");
309     }
310   }
311   unless (grep /^Content-Type:/i,@_) {
312     # nvt_print("Content-Type: text/html; charset=ISO-8859-1");
313     nvt_print("Content-Type: text/html; charset=UTF-8");
314   }
315
316   nvt_print(@_,'');
317 }
318
319
320 sub html_header {
321   my $title = shift;
322   my $header = 'header.html';
323   my $head;
324
325   binmode(STDOUT,':utf8'); # for text/html !
326
327   # http://www.w3.org/TR/html401/struct/global.html
328   # http://www.w3.org/International/O-charset
329   $head = qqq(qq(
330     '<html>'
331     '<head>'
332     '  <meta http-equiv="expires" content="0">'
333     '  <meta http-equiv="Content-Type" content="text/html;charset=utf-8">'
334     '  <title>$title</title>'
335     '</head>'
336   ));
337   # '<!-- <style type="text/css">\@import "/fex.css";</style> -->'
338
339   if ($0 =~ /fexdev/) { $head .= "<body bgcolor=\"pink\">\n" }
340   else                { $head .= "<body>\n" }
341
342   $title =~ s:F\*EX:<a href="/index.html">F*EX</a>:;
343
344   if (open $header,'<',"$docdir/$header") {
345     $head .= $_ while <$header>;
346     close $header;
347   }
348
349   $head .= &$prolog($title) if defined($prolog);
350
351   if (@H1_extra) {
352     $head .= sprintf(
353       '<h1><a href="%s"><img align=center src="%s" border=0></a>%s</h1>',
354       $H1_extra[0],$H1_extra[1]||'',$title
355     );
356   } else {
357     $head .= "<h1>$title</h1>";
358   }
359   $head .= "\n";
360
361   return $head;
362 }
363
364
365 sub html_error {
366   my $error = shift;
367   my $msg = "@_";
368   my @msg = @_;
369   my $isodate = isodate(time);
370
371   $msg =~ s/[\s\n]+/ /g;
372   $msg =~ s/<.+?>//g; # remove HTML
373   map { s/<script.*?>//gi } @msg;
374
375   errorlog($msg);
376
377   $SIG{ALRM} = sub {
378     $SIG{__DIE__} = 'DEFAULT';
379     die "TIMEOUT\n";
380   };
381   alarm($timeout);
382
383   # cannot send standard HTTP Status-Code 400, because stupid
384   # Internet Explorer then refuses to display HTML body!
385   http_header("666 Bad Request - $msg");
386   print html_header($error);
387   print 'ERROR: ',join("<p>\n",@msg),"\n";
388   pq(qq(
389     '<p><hr><p>'
390     '<address>
391     '  $ENV{HTTP_HOST}'
392     '  $isodate'
393     '  <a href="mailto:$ENV{SERVER_ADMIN}">$ENV{SERVER_ADMIN}</a>'
394     '</address>'
395     '</body></html>'
396   ));
397   exit;
398 }
399
400
401 sub http_die {
402
403   # not in CGI mode
404   unless ($ENV{GATEWAY_INTERFACE}) {
405     warn "$0: @_\n"; # must not die, because of fex_cleanup!
406     return;
407   }
408
409   debuglog(@_);
410
411   # create special error file on upload
412   if ($uid) {
413     my $ukey = "$spooldir/.ukeys/$uid";
414     $ukey .= "/error" if -d $ukey;
415     unlink $ukey;
416     if (open $ukey,'>',$ukey) {
417       print {$ukey} join("\n",@_),"\n";
418       close $ukey;
419     }
420   }
421
422   html_error($error||'',@_);
423 }
424
425
426 sub check_maint {
427   if (my $status = readlink '@MAINTENANCE') {
428     my $isodate = isodate(time);
429     http_header('666 MAINTENANCE');
430     print html_header($head||'');
431     pq(qq(
432       "<center>"
433       "<h1>Server is in maintenance mode</h1>"
434       "<h3>($status)</h3>"
435       "</center>"
436       "<p><hr><p>"
437       "<address>$ENV{HTTP_HOST} $isodate</address>"
438       "</body></html>"
439     ));
440     exit;
441   }
442 }
443
444
445 sub check_status {
446   my $user = shift;
447
448   $user = lc $user;
449   $user .= '@'.$mdomain if $mdomain and $user !~ /@/;
450
451   if (-e "$user/\@DISABLED") {
452     my $isodate = isodate(time);
453     http_header('666 DISABLED');
454     print html_header($head);
455     pq(qq(
456       "<h3>$user is disabled</h3>"
457       "Contact $ENV{SERVER_ADMIN} for details"
458       "<p><hr><p>"
459       "<address>$ENV{HTTP_HOST} $isodate</address>"
460       "</body></html>"
461     ));
462     exit;
463   }
464 }
465
466
467 sub isodate {
468   my @d = localtime shift;
469   return sprintf('%d-%02d-%02d %02d:%02d:%02d',
470                  $d[5]+1900,$d[4]+1,$d[3],$d[2],$d[1],$d[0]);
471 }
472
473
474 sub encode_Q {
475   my $s = shift;
476   $s =~ s{([\=\x00-\x20\x7F-\xA0])}{sprintf("=%02X",ord($1))}eog;
477   return $s;
478 }
479
480
481 # from MIME::Base64::Perl
482 sub decode_b64 {
483   local $_ = shift;
484   my $uu = '';
485   my ($i,$l);
486
487   tr|A-Za-z0-9+=/||cd;
488   s/=+$//;
489   tr|A-Za-z0-9+/| -_|;
490   return '' unless length;
491   $l = (length)-60;
492   for ($i = 0; $i <= $l; $i += 60) {
493     $uu .= "M" . substr($_,$i,60);
494   }
495   $_ = substr($_,$i);
496   $uu .= chr(32+(length)*3/4) . $_ if $_;
497   return unpack ("u",$uu);
498 }
499
500
501 # short base64 encoding
502 sub b64 {
503   local $_ = '';
504   my $x = 0;
505
506   pos($_[0]) = 0;
507   $_ = join '',map(pack('u',$_)=~ /^.(\S*)/, ($_[0]=~/(.{1,45})/gs));
508   tr|` -_|AA-Za-z0-9+/|;
509   $x = (3 - length($_[0]) % 3) % 3;
510   s/.{$x}$//;
511
512   return $_;
513 }
514
515
516 # simulate a "rm -rf", but never removes '..'
517 # return number of removed files
518 sub rmrf {
519   my @files = @_;
520   my $dels = 0;
521   my ($file,$dir);
522   local *D;
523   local $_;
524
525   foreach (@files) {
526     next if /(^|\/)\.\.$/;
527     /(.*)/; $file = $1;
528     if (-d $file and not -l $file) {
529       $dir = $file;
530       opendir D,$dir or next;
531       while ($file = readdir D) {
532         next if $file eq '.' or $file eq '..';
533         $dels += rmrf("$dir/$file");
534       }
535       closedir D;
536       rmdir $dir and $dels++;
537     } else {
538       unlink $file and $dels++;
539     }
540   }
541   return $dels;
542 }
543
544
545 sub gethostname {
546   my $hostname = hostname;
547   my $domain;
548   local $_;
549
550   unless ($hostname) {
551     $_ = `hostname 2>/dev/null`;
552     $hostname = /(.+)/ ? $1 : '';
553   }
554   if ($hostname !~ /\./ and open my $rc,'/etc/resolv.conf') {
555     while (<$rc>) {
556       if (/^\s*domain\s+([\w.-]+)/) {
557         $domain = $1;
558         last;
559       }
560       if (/^\s*search\s+([\w.-]+)/) {
561         $domain = $1;
562       }
563     }
564     close $rc;
565     $hostname .= ".$domain" if $domain;
566   }
567   if ($hostname !~ /\./ and $admin and $admin =~ /\@([\w.-]+)/) {
568     $hostname .= '.'.$1;
569   }
570
571   return $hostname;
572 }
573
574
575 # strip off path names (Windows or UNIX)
576 sub strip_path {
577   local $_ = shift;
578
579   s/.*\\// if /^([A-Z]:)?\\/;
580   s:.*/::;
581
582   return $_;
583 }
584
585
586 # substitute all critcal chars
587 sub normalize {
588   local $_ = shift;
589
590   return '' unless defined $_;
591
592   # we need perl native utf8 (see perldoc utf8)
593   $_ = decode_utf8($_) unless utf8::is_utf8($_);
594
595   s/[\r\n\t]+/ /g;
596   s/[\x00-\x1F\x80-\x9F]/_/g;
597   s/^\s+//;
598   s/\s+$//;
599
600   return encode_utf8($_);
601 }
602
603
604 # substitute all critcal chars
605 sub normalize_html {
606   local $_ = shift;
607
608   return '' unless defined $_;
609
610   $_ = normalize($_);
611   s/[\"<>]//g;
612
613   return $_;
614 }
615
616
617
618 # substitute all critcal chars with underscore
619 sub normalize_filename {
620   local $_ = shift;
621
622   return $_ unless $_;
623
624   # we need native utf8
625   $_ = decode_utf8($_) unless utf8::is_utf8($_);
626
627   $_ = strip_path($_);
628
629   # substitute all critcal chars with underscore
630   s/[^a-zA-Z0-9_=.+-]/_/g;
631   s/^\./_/;
632
633   return encode_utf8($_);
634 }
635
636
637 sub normalize_email {
638   local $_ = lc shift;
639
640   s/[^\w_.+=!~#^\@\-]//g;
641   s/^\./_/;
642   /(.*)/;
643   return $1;
644 }
645
646
647 sub normalize_user {
648   my $user = shift;
649
650   $user = lc(urldecode(despace($user)));
651   $user .= '@'.$mdomain if $mdomain and $user !~ /@/;
652   checkaddress($user) or http_die("$user is not a valid e-mail address");
653   return untaint($user);
654 }
655
656
657 sub urldecode {
658   local $_ = shift;
659   s/%([a-f0-9]{2})/chr(hex($1))/gie;
660   return $_;
661 }
662
663
664 sub untaint {
665   local $_ = shift;
666   /(.*)/s;
667   return $1;
668 }
669
670
671 sub checkchars {
672   my $input = shift;
673   local $_ = shift;
674
675   if (/^([|+.])/) {
676     http_die("\"$1\" is not allowed at beginning of $input");
677   }
678   if (/([\/\"\'\\<>;])/) {
679     http_die(sprintf("\"&#%s;\" is not allowed in %s",ord($1),$input));
680   }
681   if (/(\|)$/) {
682     http_die("\"$1\" is not allowed at end of $input");
683   }
684   if (/[\000-\037]/) {
685     http_die("control characters are not allowed in $input");
686   }
687   /(.*)/;
688   return $1;
689 }
690
691
692 sub checkaddress {
693   my $a = shift;
694   my $re;
695   local $_;
696   local ($domain,$dns);
697
698   $a =~ s/:\w+=.*//; # remove options from address
699
700   return $a if $a eq 'anonymous';
701
702   $a .= '@'.$mdomain if $mdomain and $a !~ /@/;
703
704   $re = '^[.@-]|@.*@|local(host|domain)$|["\'\`\|\s()<>/;,]';
705   if ($a =~ /$re/i) {
706     debuglog("$a has illegal syntax ($re)");
707     return '';
708   }
709   $re = '^[!^=~#_:.+*{}\w\-\[\]]+\@(\w[.\w\-]*\.[a-z]+)$';
710   if ($a =~ /$re/i) {
711     $domain = $dns = $1;
712     {
713       local $SIG{__DIE__} = sub { die "\n" };
714       eval q{
715         use Net::DNS;
716         $dns = Net::DNS::Resolver->new->query($domain)||mx($domain);
717         unless ($dns or mx('uni-stuttgart.de')) {
718           http_die("Internal error: bad resolver");
719         }
720       }
721     };
722     if ($dns) {
723       return untaint($a);
724     } else {
725       debuglog("no A or MX DNS record found for $domain");
726       return '';
727     }
728   } else {
729     debuglog("$a does not match e-mail regexp ($re)");
730     return '';
731   }
732 }
733
734
735 # check forbidden addresses
736 sub checkforbidden {
737   my $a = shift;
738   my ($fr,$pr);
739   local $_;
740
741   $a .= '@'.$mdomain if $mdomain and $a !~ /@/;
742   return $a if -d "$spooldir/$a"; # ok, if user already exists
743   if (@forbidden_recipients) {
744     foreach (@forbidden_recipients) {
745       $fr = quotewild($_);
746       # skip public recipients
747       if (@public_recipients) {
748         foreach $pr (@public_recipients) {
749           return $a if $a eq lc $pr;
750         }
751       }
752       return '' if $a =~ /^$fr$/i;
753     }
754   }
755   return $a;
756 }
757
758
759 sub randstring {
760   my $n = shift;
761   my @rc = ('A'..'Z','a'..'z',0..9 );
762   my $rn = @rc;
763   my $rs;
764
765   for (1..$n) { $rs .= $rc[int(rand($rn))] };
766   return $rs;
767 }
768
769
770 # emulate mkdir -p
771 sub mkdirp {
772   my $dir = shift;
773   my $pdir;
774
775   return if -d $dir;
776   $dir =~ s:/+$::;
777   http_die("cannot mkdir /") unless $dir;
778   $pdir = $dir;
779   if ($pdir =~ s:/[^/]+$::) {
780     mkdirp($pdir) unless -d $pdir;
781   }
782   unless (-d $dir) {
783     mkdir $dir,0770 or http_die("mkdir $dir - $!");
784   }
785 }
786
787
788 # hash with SID
789 sub sidhash {
790   my ($rid,$id) = @_;
791
792   if ($rid and $ENV{SID} and $id =~ /^MD5H:/) {
793     $rid = 'MD5H:'.md5_hex($rid.$ENV{SID});
794   }
795   return $rid;
796 }
797
798
799 # test if ip is in iplist (ipv4/ipv6)
800 # iplist is an array with ips and ip-ranges
801 sub ipin {
802   my ($ip,@list) = @_;
803   my ($i,$ia,$ib);
804
805   $ipe = lc(ipe($ip));
806   map { lc } @list;
807
808   foreach $i (@list) {
809     if ($ip =~ /\./ and $i =~ /\./ or $ip =~ /:/ and $i =~ /:/) {
810       if ($i =~ /(.+)-(.+)/) {
811         ($ia,$ib) = ($1,$2);
812         $ia = ipe($ia);
813         $ib = ipe($ib);
814         return $ip if $ipe ge $ia and $ipe le $ib;
815       } else {
816         return $ip if $ipe eq ipe($i);
817       }
818     }
819   }
820   return '';
821 }
822
823 # ip expand (ipv4/ipv6)
824 sub ipe {
825   local $_ = shift;
826
827   if (/^\d+\.\d+\.\d+\.\d+$/) {
828     s/\b(\d\d?)\b/sprintf "%03d",$1/ge;
829   } elsif (/^[:\w]+:\w+$/) {
830     s/\b(\w+)\b/sprintf "%04s",$1/ge;
831     s/^:/0000:/;
832     while (s/::/::0000:/) { last if length > 39 }
833     s/::/:/;
834   } else {
835     $_ = '';
836   }
837   return $_;
838 }
839
840
841 sub filename {
842   my $file = shift;
843   my $filename;
844
845   if (open $file,'<',"$file/filename") {
846     $filename = <$file>||'';
847     chomp $filename;
848     close $file;
849   }
850
851   unless ($filename) {
852     $filename = $file;
853     $filename =~ s:.*/::;
854   }
855
856   return $filename;
857 }
858
859
860 sub urlencode {
861   local $_ = shift;
862   s/(^[.~]|[^\w.,=:~^+-])/sprintf "%%%X",ord($1)/ge;
863   return $_;
864 }
865
866
867 # file and document log
868 sub fdlog {
869   my ($log,$file,$s,$size) = @_;
870   my $ra = $ENV{REMOTE_ADDR}||'-';
871   my $msg;
872
873   $ra .= '/'.$ENV{HTTP_X_FORWARDED_FOR} if $ENV{HTTP_X_FORWARDED_FOR};
874   $ra =~ s/\s//g;
875   $file =~ s:/data$::;
876   $msg = sprintf "%s [%s_%s] %s %s %s/%s\n",
877          isodate(time),$$,$ENV{REQUESTCOUNT},$ra,encode_Q($file),$s,$size;
878
879   writelog($log,$msg);
880 }
881
882
883 # extra debug log
884 sub debuglog {
885   my $prg = $0;
886   local $_;
887
888   return unless $debug and @_;
889   unless ($debuglog and fileno $debuglog) {
890     my $ddir = "$spooldir/.debug";
891     mkdir $ddir,0770 unless -d $ddir;
892     $prg =~ s:.*/::;
893     $prg = untaint($prg);
894     $debuglog = sprintf("%s/%s_%s_%s.%s",
895                         $ddir,time,$$,$ENV{REQUESTCOUNT}||0,$prg);
896     $debuglog =~ s/\s/_/g;
897     # http://perldoc.perl.org/perlunifaq.html#What-is-a-%22wide-character%22%3f
898     # open $debuglog,'>>:encoding(UTF-8)',$debuglog or return;
899     open $debuglog,'>>',$debuglog or return;
900     # binmode($debuglog,":utf8");
901     autoflush $debuglog 1;
902     # printf {$debuglog} "\n### %s ###\n",isodate(time);
903   }
904   while ($_ = shift @_) {
905     $_ = encode_utf8($_) if utf8::is_utf8($_);
906     s/\n*$/\n/;
907     s/<.+?>//g; # remove HTML
908     print {$debuglog} $_;
909     print "DEBUG: $_" if -t;
910   }
911 }
912
913
914 # extra debug log
915 sub errorlog {
916   my $prg = $0;
917   my $msg = "@_";
918   my $ra = $ENV{REMOTE_ADDR}||'-';
919
920   $ra .= '/'.$ENV{HTTP_X_FORWARDED_FOR} if $ENV{HTTP_X_FORWARDED_FOR};
921   $ra =~ s/\s//g;
922   $prg =~ s:.*/::;
923   $msg =~ s/[\r\n]+$//;
924   $msg =~ s/[\r\n]+/ /;
925   $msg =~ s/\s*<p>.*//;
926   $msg = sprintf "%s %s %s %s\n",isodate(time),$prg,$ra,$msg;
927
928   writelog('error.log',$msg);
929 }
930
931
932 sub writelog {
933   my $log = shift;
934   my $msg = shift;
935
936   foreach my $logdir (@logdir) {
937     if (open $log,'>>',"$logdir/$log") {
938       flock $log,LOCK_EX;
939       seek $log,0,SEEK_END;
940       print {$log} $msg;
941       close $log;
942     }
943   }
944 }
945
946
947 # failed authentification log
948 sub faillog {
949   my $request = shift;
950   my $n = 1;
951
952   if ($faillog and $max_fail_handler and open $faillog,"+>>$faillog") {
953     flock($faillog,LOCK_EX);
954     seek $faillog,0,SEEK_SET;
955     $n++ while <$faillog>;
956     printf {$faillog} "%s %s\n",isodate(time),$request;
957     close $faillog;
958     &$max_fail_handler($ENV{REMOTE_ADDR}) if $n > $max_fail;
959   }
960 }
961
962 # remove all white space
963 sub despace {
964   local $_ = shift;
965   s/\s//g;
966   return $_;
967 }
968
969
970 # superquoting
971 sub qqq {
972   local $_ = shift;
973   my ($s,$i,@s);
974   my $q = "[\'\"]"; # quote delimiter chars " and '
975
976   # remove first newline and look for default indention
977   s/^((\d+)?)?\n//;
978   $i = ' ' x ($2||0);
979
980   # remove trailing spaces at end
981   s/[ \t]*?$//;
982
983   @s = split "\n";
984
985   # first line have a quote delimiter char?
986   if (/^\s+$q/) {
987     # remove heading spaces and delimiter chars
988     foreach (@s) {
989       s/^\s*$q//;
990       s/$q\s*$//;
991     }
992   } else {
993     # find the line with the fewest heading spaces (and count them)
994     # (beware of tabs!)
995     $s = length;
996     foreach (@s) {
997       if (/^( *)\S/ and length($1) < $s) { $s = length($1) };
998     }
999     # adjust indention
1000     foreach (@s) {
1001       s/^ {$s}/$i/;
1002     }
1003   }
1004
1005   return join("\n",@s)."\n";
1006 }
1007
1008
1009 # print superquoted
1010 sub pq {
1011   my $H = STDOUT;
1012
1013   if (@_ > 1 and defined fileno $_[0]) { $H = shift }
1014   binmode($H,':utf8');
1015   print {$H} qqq(@_);
1016 }
1017
1018
1019 # check sender quota
1020 sub check_sender_quota {
1021   my $sender = shift;
1022   my $squota = $sender_quota||0;
1023   my $du = 0;
1024   my ($file,$size,%file,$data,$upload);
1025   local $_;
1026
1027   if (open $qf,'<',"$sender/\@QUOTA") {
1028     while (<$qf>) {
1029       s/#.*//;
1030       $squota = $1 if /sender.*?(\d+)/i;
1031     }
1032     close $qf;
1033   }
1034
1035   foreach $file (glob "*/$sender/*") {
1036     $data = "$file/data";
1037     $upload = "$file/upload";
1038     if (not -l $data and $size = -s $data) {
1039       # count hard links only once (= same inode)
1040       my $i = (stat($data))[1]||0;
1041       unless ($file{$i}) {
1042         $du += $size;
1043         $file{$i} = $i;
1044       }
1045     } elsif (-f $upload) {
1046       # count hard links only once (= same inode)
1047       my $i = (stat($upload))[1]||0;
1048       unless ($file{$i}) {
1049         $size = readlink "$file/size" and $du += $size;
1050         $file{$i} = $i;
1051       }
1052     }
1053   }
1054
1055   return($squota,int($du/1024/1024));
1056 }
1057
1058
1059 # check recipient quota
1060 sub check_recipient_quota {
1061   my $recipient = shift;
1062   my $rquota = $recipient_quota||0;
1063   my $du = 0;
1064   my ($file,$size);
1065   local $_;
1066
1067   if (open my $qf,'<',"$recipient/\@QUOTA") {
1068     while (<$qf>) {
1069       s/#.*//;
1070       $rquota = $1 if /recipient.*?(\d+)/i;
1071     }
1072     close $qf;
1073   }
1074
1075   foreach $file (glob "$recipient/*/*") {
1076     if (-f "$file/upload" and $size = readlink "$file/size") {
1077       $du += $size;
1078     } elsif (not -l "$file/data" and $size = -s "$file/data") {
1079       $du += $size;
1080     }
1081   }
1082
1083   return($rquota,int($du/1024/1024));
1084 }
1085
1086
1087 sub getline {
1088   my $file = shift;
1089   local $_;
1090   chomp($_ = <$file>||'');
1091   return $_;
1092 }
1093
1094
1095 # (shell) wildcard matching
1096 sub wcmatch {
1097   local $_ = shift;
1098   my $p = quotemeta shift;
1099
1100   $p =~ s/\\\*/.*/g;
1101   $p =~ s/\\\?/./g;
1102   $p =~ s/\\\[/[/g;
1103   $p =~ s/\\\]/]/g;
1104
1105   return /$p/;
1106 }
1107
1108
1109 sub logout {
1110   my $logout;
1111   if    ($skey) { $logout = "/fup?logout=skey:$skey" }
1112   elsif ($gkey) { $logout = "/fup?logout=gkey:$gkey" }
1113   elsif ($akey) { $logout = "/fup?logout=akey:$akey" }
1114   else          { $logout = "/fup?logout" }
1115   return qqq(qq(
1116     '<p>'
1117     '<form name="logout" action="$logout">'
1118     '  <input type="submit" name="logout" value="logout">'
1119     '</form>'
1120     '<p>'
1121   ));
1122 }
1123
1124
1125 # print data dump of global or local variables in HTML
1126 # input musst be a string, eg: '%ENV'
1127 sub DD {
1128   my $v = shift;
1129   local $_;
1130
1131   $n =~ s/.//;
1132   $_ = eval(qq(use Data::Dumper;Data::Dumper->Dump([\\$v])));
1133   s/\$VAR1/$v/;
1134   s/&/&amp;/g;
1135   s/</&lt;/g;
1136   print "<pre>\n$_\n</pre>\n";
1137 }
1138
1139 # make symlink
1140 sub mksymlink {
1141   my ($file,$link) = @_;
1142   unlink $file;
1143   return symlink untaint($link),$file;
1144 }
1145
1146
1147 # copy file (and modify) or symlink
1148 # returns chomped file contents or link name
1149 # preserves permissions and time stamps
1150 sub copy {
1151   my ($from,$to,$mod) = @_;
1152   my $link;
1153   local $/;
1154   local $_;
1155
1156   $to .= '/'.basename($from) if -d $to;
1157
1158   if (defined($link = readlink $from)) {
1159     mksymlink($to,$link);
1160     return $link;
1161   } else {
1162     open $from,'<',$from or return;
1163     open $to,'>',$to or return;
1164     $_ = <$from>;
1165     close $from;
1166     eval $mod if $mod;
1167     print {$to} $_;
1168     close $to or http_die("internal error: $to - $!");
1169     if (my @s = stat($from)) {
1170       chmod $s[2],$to;
1171       utime @s[8,9],$to unless $mod;
1172     }
1173     chomp;
1174     return $_;
1175   }
1176 }
1177
1178
1179 sub slurp {
1180   my $file = shift;
1181   local $_;
1182   local $/;
1183
1184   if (open $file,$file) {
1185     $_ = <$file>;
1186     close $file;
1187   }
1188
1189   return $_;
1190 }
1191
1192
1193 # read one line from STDIN (net socket) and assign it to $_
1194 # return number of read bytes
1195 # also set global variable $RB (read bytes)
1196 sub nvt_read {
1197   my $len = 0;
1198
1199   if (defined ($_ = <STDIN>)) {
1200     debuglog($_);
1201     $len = length;
1202     $RB += $len;
1203     s/\r?\n//;
1204   }
1205   return $len;
1206 }
1207
1208
1209 # read forward to given pattern
1210 sub nvt_skip_to {
1211   my $pattern = shift;
1212
1213   while (&nvt_read) { return if /$pattern/ }
1214 }
1215
1216
1217 # HTTP GET and POST parameters
1218 # (not used by fup)
1219 # fills global variable %PARAM :
1220 # normal parameter is $PARAM{$parameter}
1221 # file parameter is $PARAM{$parameter}{filename} $PARAM{$parameter}{data}
1222 sub parse_parameters {
1223   my $cl = $ENV{X_CONTENT_LENGTH} || $ENV{CONTENT_LENGTH} || 0;
1224   my $data = '';
1225   my $filename;
1226   local $_;
1227
1228   if ($cl > 128*$MB) {
1229     http_die("request too large");
1230   }
1231
1232   binmode(STDIN,':raw');
1233
1234   foreach (split('&',$ENV{QUERY_STRING})) {
1235     if (/(.+?)=(.*)/) { $PARAM{$1} = $2 }
1236     else              { $PARAM{$_} = $_ }
1237   }
1238   $_ = $ENV{CONTENT_TYPE}||'';
1239   if ($ENV{REQUEST_METHOD} eq 'POST' and /boundary=\"?([\w\-\+\/_]+)/) {
1240     my $boundary = $1;
1241     while ($RB<$cl and &nvt_read) { last if /^--\Q$boundary/ }
1242     # continuation lines are not checked!
1243     while ($RB<$cl and &nvt_read) {
1244       $filename = '';
1245       if (/^Content-Disposition:.*\s*filename="(.+?)"/i) {
1246         $filename = $1;
1247       }
1248       if (/^Content-Disposition:\s*form-data;\s*name="(.+?)"/i) {
1249         my $p = $1;
1250         # skip rest of mime part header
1251         while ($RB<$cl and &nvt_read) { last if /^\s*$/ }
1252         $data = '';
1253         while (<STDIN>) {
1254           if ($p =~ /password/i) {
1255             debuglog('*' x length)
1256           } else {
1257             debuglog($_)
1258           }
1259           $RB += length;
1260           last if /^--\Q$boundary/;
1261           $data .= $_;
1262         }
1263         unless (defined $_) { die "premature end of HTTP POST\n" }
1264         $data =~ s/\r?\n$//;
1265         if ($filename) {
1266           $PARAM{$p}{filename} = $filename;
1267           $PARAM{$p}{data} = $data;
1268         } else {
1269           $PARAM{$p} = $data;
1270         }
1271         last if /^--\Q$boundary--/;
1272       }
1273     }
1274   }
1275 }
1276
1277
1278 # name based virtual host?
1279 sub vhost {
1280   my $hh = shift; # HTTP_HOST
1281   my $vhost;
1282   my $locale = $ENV{LOCALE};
1283
1284   # memorized vhost? (default is in fex.ph)
1285   %vhost = split(':',$ENV{VHOST}) if $ENV{VHOST};
1286
1287   if (%vhost and $hh and $hh =~ s/^([\w\.-]+).*/$1/) {
1288     if ($vhost = $vhost{$hh} and -f "$vhost/lib/fex.ph") {
1289       $ENV{VHOST} = "$hh:$vhost"; # memorize vhost for next run
1290       $ENV{FEXLIB} = $FEXLIB = "$vhost/lib";
1291       $logdir = $spooldir    = "$vhost/spool";
1292       $docdir                = "$vhost/htdocs";
1293       @logdir = ($logdir);
1294       if ($locale and -e "$vhost/locale/$locale/lib/fex.ph") {
1295         $ENV{FEXLIB} = $FEXLIB = "$vhost/locale/$locale/lib";
1296       }
1297       require "$FEXLIB/fex.ph" or die "$0: cannot load $FEXLIB/fex.ph - $!";
1298       $ENV{SERVER_NAME} = $hostname;
1299       @doc_dirs = ($docdir);
1300       foreach my $ld (glob "$FEXHOME/locale/*/htdocs") {
1301         push @doc_dirs,$ld;
1302       }
1303       return $vhost;
1304     }
1305   }
1306 }
1307
1308
1309 sub gpg_encrypt {
1310   my ($plain,$to,$keyring,$from) = @_;
1311   my ($pid,$pi,$po,$pe,$enc,$err);
1312   local $_;
1313
1314   $pe = gensym;
1315
1316   $pid = open3($po,$pi,$pe,
1317     "gpg --batch --trust-model always --keyring $keyring".
1318     "    -a -e -r $bcc -r $to"
1319   ) or return;
1320
1321   print {$po} "\n",$plain,"\n";
1322   close $po;
1323
1324   $enc .= $_ while <$pi>;
1325   $err .= $_ while <$pe>;
1326   errorlog("($from --> $to) $err") if $err;
1327
1328   close $pi;
1329   close $pe;
1330   waitpid($pid,0);
1331
1332   return $enc;
1333 }
1334
1335
1336 sub mtime {
1337   my @s = stat(shift) or return;
1338   return $s[9];
1339 }
1340
1341
1342 # wildcard * to perl regexp
1343 sub quotewild {
1344   local $_ = quotemeta shift;
1345   s/\\\*/.*/g; # allow wildcard *
1346   return $_;
1347 }
1348
1349
1350 # extract locale functions into hash of subroutine references
1351 # e.g. \&german ==> $notify{german}
1352 sub locale_functions {
1353   my $locale = shift;
1354   local $/;
1355   local $_;
1356
1357   if ($locale and open my $fexpp,"$FEXHOME/locale/$locale/lib/fex.pp") {
1358     $_ = <$fexpp>;
1359     s/.*\n(\#\#\# locale functions)/$1/s;
1360     # sub xx {} ==> xx{$locale} = sub {}
1361     s/\nsub (\w+)/\n\$$1\{$locale\} = sub/gs;
1362     s/\n}\n/\n};\n/gs;
1363     eval $_;
1364     close $fexpp;
1365   }
1366 }
1367
1368 sub notify_locale {
1369   my $dkey = shift;
1370   my $status = shift || 'new';
1371   my ($to,$keep,$locale,$file,$filename,$comment,$autodelete,$replyto,$mtime);
1372   local $_;
1373
1374   if ($dkey =~ m:/.+/.+/:) {
1375     $file = $dkey;
1376     $dkey = readlink("$file/dkey");
1377   } else {
1378     $file = readlink("$dkeydir/$dkey")
1379       or http_die("internal error: no DKEY $DKEY");
1380   }
1381   $file =~ s:^../::;
1382   $filename = filename($file);
1383   $to = $file;
1384   $to =~ s:/.*::;
1385   $mtime = mtime("$file/data") or http_die("internal error: no $file/data");
1386   $comment = slurp("$file/comment") || '';
1387   $replyto = readlink "$file/replyto" || '';
1388   $autodelete = readlink "$file/autodelete"
1389              || readlink "$to/\@AUTODELETE"
1390              || $::autodelete;
1391   $keep = readlink "$file/keep"
1392        || readlink "$to/\@KEEP"
1393        || $keep_default;
1394
1395   $locale = readlink "$to/\@LOCALE" || readlink "$file/locale" || 'english';
1396   $_ = untaint("$FEXHOME/locale/$locale/lib/lf.pl");
1397   require if -f;
1398   unless ($notify{$locale}) {
1399     $locale = 'english';
1400     $notify{$locale} ||= \&notify;
1401   }
1402   return &{$notify{$locale}}(
1403     status     => $status,
1404     dkey       => $dkey,
1405     filename   => $filename,
1406     keep       => $keep-int((time-$mtime)/$DS),
1407     comment    => $comment,
1408     autodelete => $autodelete,
1409     replyto    => $replyto,
1410   );
1411 }
1412
1413 ########################### locale functions ###########################
1414 # Will be extracted by install process and saved in $FEXHOME/lib/lf.pl #
1415 # You cannot modify them here without re-installing!                   #
1416 ########################################################################
1417
1418 # locale function!
1419 sub notify {
1420   # my ($status,$dkey,$filename,$keep,$warn,$comment,$autodelete) = @_;
1421   my %P = @_;
1422   my ($to,$from,$file,$mimefilename,$receiver,$warn,$comment,$autodelete);
1423   my ($size,$bytes,$days,$header,$data,$replyto,$uurl);
1424   my ($mfrom,$mto,$dfrom,$dto);
1425   my $proto = 'http';
1426   my $durl = $::durl;
1427   my $index;
1428   my $fileid = 0;
1429   my $fua = $ENV{HTTP_USER_AGENT}||'';
1430   my $warning = '';
1431   my $disclaimer = '';
1432   my $download = '';
1433   my $keyring;
1434   my $boundary = randstring(16);
1435   my ($body,$enc_body);
1436
1437   return if $nomail;
1438
1439   $warn = $P{warn}||2;
1440   $comment = $P{comment}||'';
1441   $comment = encode_utf8($P{comment}||'') if utf8::is_utf8($comment);
1442   $comment =~ s/^!\*!//; # multi download allow flag
1443   $autodelete = $P{autodelete}||$::autodelete;
1444
1445   $file = untaint(readlink("$dkeydir/$P{dkey}"));
1446   $file =~ s/^\.\.\///;
1447   # make download protocal same as upload protocol
1448   if ($uurl = readlink("$file/uurl") and $uurl =~ /^(\w+):/) {
1449     $proto = $1;
1450     $durl =~ s/^\w+::/$proto::/;
1451   }
1452   $index = "$proto://$hostname/index.html";
1453   ($to,$from,$file) = split('/',$file);
1454   $filename = strip_path($P{filename});
1455   $mfrom = $from;
1456   $mto = $to;
1457   $mfrom .= '@'.$mdomain if $mdomain and $mfrom !~ /@/;
1458   $mto .=   '@'.$mdomain if $mdomain and $mto   !~ /@/;
1459   $keyring = $to.'/@GPG';
1460   # $to = '' if $to eq $from; # ???
1461   $replyto = $P{replyto}||$mfrom;
1462   $header = "From: <$mfrom> ($mfrom via F*EX service $hostname)\n";
1463   $header .= "Reply-To: <$replyto>\n" if $replyto ne $mfrom;
1464   $header .= "To: <$mto>\n";
1465   $data = "$dkeydir/$P{dkey}/data";
1466   $size = $bytes = -s $data;
1467   return unless $size;
1468   $warning =
1469     "We recommend fexget or fexit for download,\n".
1470     "because these clients can resume the download after an interruption.\n".
1471     "See $proto://$hostname/tools.html";
1472   # if ($nowarning) {
1473   #   $warning = '';
1474   # } else {
1475   #   $warning =
1476   #     "Please avoid download with Internet Explorer, ".
1477   #     "because it has too many bugs.\n\n";
1478   # }
1479   if ($filename =~ /\.(tar|zip|7z|arj|rar)$/) {
1480     $warning .= "\n\n".
1481       "$filename is a container file.\n".
1482       "You can unpack it for example with 7zip ".
1483       "(http://www.7-zip.org/download.html)";
1484   }
1485   if ($limited_download =~ /^y/i) {
1486     $warning .= "\n\n".
1487       'This download link only works for you, you cannot distribute it.';
1488   }
1489   if ($size < 2048) {
1490     $size = "$size Bytes";
1491   } elsif ($size/1024 < 2048) {
1492     $size = int($size/1024)." kB";
1493   } else {
1494     $size = int($size/1024/1024)." MB";
1495   }
1496   if ($autodelete eq 'YES') {
1497     $autodelete = "WARNING: After download (or view with a web browser!), "
1498                 . "the file will be deleted!";
1499   } elsif ($autodelete eq 'DELAY') {
1500     $autodelete = "WARNING: When you download the file it will be deleted "
1501                 . "soon afterwards!";
1502   } else {
1503     $autodelete = '';
1504   }
1505
1506   if (-s $keyring) {
1507     $mimefilename = '';
1508   } else {
1509     $mimefilename = $filename;
1510     if ($mimefilename =~ s/([_\?\=\x00-\x1F\x7F-\xFF])/sprintf("=%02X",ord($1))/eog) {
1511       $mimefilename =~ s/ /_/g;
1512       $mimefilename = '=?UTF-8?Q?'.$mimefilename.'?=';
1513     }
1514   }
1515
1516   unless ($fileid = readlink("$dkeydir/$P{dkey}/id")) {
1517     my @s = stat($data);
1518     $fileid =  @s ? $s[1].$s[9] : 0;
1519   }
1520
1521   if ($P{status} eq 'new') {
1522     $days = $P{keep};
1523     $header .= "Subject: F*EX-upload: $mimefilename\n";
1524   } else {
1525     $days = $warn;
1526     $header .= "Subject: reminder F*EX-upload: $mimefilename\n";
1527   }
1528   $header .= "X-FEX-Client-Address: $fra\n" if $fra;
1529   $header .= "X-FEX-Client-Agent: $fua\n"   if $fua;
1530   foreach my $u (@durl?@durl:($durl)) {
1531     my $durl = sprintf("%s/%s/%s",$u,$P{dkey},normalize_filename($filename));
1532     $header .= "X-FEX-URL: $durl\n" unless -s $keyring;
1533     $download .= "$durl\n";
1534   }
1535   $header .=
1536     "X-FEX-Filesize: $bytes\n".
1537     "X-FEX-File-ID: $fileid\n".
1538     "X-FEX-Fexmaster: $ENV{SERVER_ADMIN}\n".
1539     "X-Mailer: F*EX\n".
1540     "MIME-Version: 1.0\n";
1541   if ($comment =~ s/^\[(\@(.*?))\]\s*//) {
1542     $receiver = "group $1";
1543     if ($_ = readlink "$from/\@GROUP/$2" and m:^../../(.+?)/:) {
1544       $receiver .= " (maintainer: $1)";
1545     }
1546   } else {
1547     $receiver = 'you';
1548   }
1549   if ($days == 1) { $days .= " day" }
1550   else            { $days .= " days" }
1551
1552   # explicite sender set in fex.ph?
1553   if ($sender_from) {
1554     map { s/^From: <$mfrom/From: <$sender_from/ } $header;
1555     open $sendmail,'|-',$sendmail,$mto,$bcc
1556       or http_die("cannot start sendmail - $!");
1557   } else {
1558     # for special remote domains do not use same domain in From,
1559     # because remote MTA will probably reject this e-mail
1560     $dfrom = $1 if $mfrom =~ /@(.+)/;
1561     $dto   = $1 if $mto   =~ /@(.+)/;
1562     if ($dfrom and $dto and @remote_domains and
1563         grep {
1564           $dfrom =~ /(^|\.)$_$/ and $dto =~ /(^|\.)$_$/
1565         } @remote_domains)
1566     {
1567       $header =~ s/(From: <)\Q$mfrom\E(.*?)\n/$1$admin$2\nReply-To: $mfrom\n/;
1568       open $sendmail,'|-',$sendmail,$mto,$bcc
1569         or http_die("cannot start sendmail - $!");
1570     } else {
1571       open $sendmail,'|-',$sendmail,'-f',$mfrom,$mto,$bcc
1572         or http_die("cannot start sendmail - $!");
1573     }
1574   }
1575   $comment = "\n$comment\n" if $comment;
1576   if ($comment =~ s/\n!(shortmail|\.)!\s*//i
1577     or (readlink("$to/\@NOTIFICATION")||'') =~ /short/i
1578   ) {
1579     $body = qqq(qq(
1580       '$comment'
1581       '$download'
1582       '$size'
1583     ));
1584   } else {
1585     $disclaimer = slurp("$from/\@DISCLAIMER") || qqq(qq(
1586       '$warning'
1587       ''
1588       'F*EX is not an archive, it is a transfer system for personal files.'
1589       'For more information see $index'
1590       ''
1591       'Questions? ==> F*EX admin: $admin'
1592     ));
1593     $disclaimer .= "\n$::disclaimer\n" if $::disclaimer;
1594     $body = qqq(qq(
1595       '$comment'
1596       '$from has uploaded the file'
1597       '  "$filename"'
1598       '($size) for $receiver. Use'
1599       ''
1600       '$download'
1601       'to download this file within $days.'
1602       ''
1603       '$autodelete'
1604       ''
1605       '$disclaimer'
1606     ));
1607   }
1608   $body =~ s/\n\n+/\n\n/g;
1609   if (-s $keyring) {
1610     $enc_body = gpg_encrypt($body,$to,$keyring,$from);
1611   }
1612   if ($enc_body) {
1613     # RFC3156
1614     $header .= qqq(qq(
1615       'Content-Type: multipart/encrypted; protocol="application/pgp-encrypted";'
1616       '\tboundary="$boundary"'
1617       'Content-Disposition: inline'
1618     ));
1619     $body = qqq(qq(
1620       '--$boundary'
1621       'Content-Type: application/pgp-encrypted'
1622       'Content-Disposition: attachment'
1623       ''
1624       'Version: 1'
1625       ''
1626       '--$boundary'
1627       'Content-Type: application/octet-stream'
1628       'Content-Disposition: inline; filename="fex.pgp"'
1629       ''
1630       '$enc_body'
1631       '--$boundary--'
1632     ));
1633   } else {
1634     $header .=
1635       "Content-Type: text/plain; charset=UTF-8\n".
1636       "Content-Transfer-Encoding: 8bit\n";
1637   }
1638   print {$sendmail} $header,"\n",$body;
1639   close $sendmail and return $to;
1640   http_die("cannot send notification e-mail (sendmail error $!)");
1641 }
1642
1643
1644 # locale function!
1645 sub reactivation {
1646   my ($expire,$user) = @_;
1647   my $fexsend = "$FEXHOME/bin/fexsend";
1648   my $reactivation = "$FEXLIB/reactivation.txt";
1649
1650   return if $nomail;
1651
1652   if (-x $fexsend) {
1653     if ($locale) {
1654       my $lr = "$FEXHOME/locale/$locale/lib/reactivation.txt";
1655       $reactivation = $lr if -f $lr and -s $lr;
1656     }
1657     $fexsend .= " -M -D -k 30 -C"
1658                ." 'Your F*EX account has been inactive for $expire days,"
1659                ." you must download this file to reactivate it."
1660                ." Otherwise your account will be deleted.'"
1661                ." $reactivation $user";
1662     # on error show STDOUT and STDERR
1663     my $fo = `$fexsend 2>&1`;
1664     warn $fexsend.'\n'.$fo if $?;
1665   } else {
1666     warn "$0: cannot execute $fexsend for reactivation()\n";
1667   }
1668 }
1669
1670 1;