3 # F*EX CGI for administration
5 # Original author: Andre Hafner <andrehafner@gmx.net>
8 BEGIN { ($ENV{PERLINIT}||'') =~ /(.+)/s and eval $1 }
16 (our $FEXLIB) = $ENV{FEXLIB} =~ /(.+)/ or die "no \$FEXLIB\n";
18 # import from fex.pp and fex.ph
19 our ($FEXHOME,$spooldir,$logdir,$docdir,$akeydir,$durl,$mdomain,$bs,$hostname);
20 our ($keep_default,$keep_max,$recipient_quota,$sender_quota,$autodelete);
21 our ($admin,$admin_pw,$admin_hosts);
23 our $error = 'FAC error';
25 # load common code, local config : $HOME/lib/fex.ph
26 require "$FEXLIB/fex.pp";
29 my $ra = $ENV{REMOTE_ADDR}||0;
31 if (not @admin_hosts or not ipin($ra,@admin_hosts)) {
32 html_error($error,"Administration from your host ($ra) is not allowed.");
35 html_error($error,"\$admin not configured in $FEXLIB/fex.ph\n") unless $admin;
37 chdir $spooldir or http_die("$spooldir - $!");
38 chomp($admin_pw = slurp("$admin/@")||'');
39 html_error($error,"no F*EX account for admin $admin\n") unless $admin_pw;
41 # redirect to https if configured
42 (undef,$port) = split(':',$ENV{HTTP_HOST}||'');
43 $port ||= $ENV{PROTO} eq 'https' ? 443 : 80;
44 if ($port == 80 and open my $x,'/etc/xinetd.d/fexs') {
46 if (/^\s*disable\s*=\s*no/) {
48 "HTTP/1.1 301 Moved Permanently",
49 "Location: https://$hostname$ENV{REQUEST_URI}",
62 $action = $PARAM{"action"}||'';
70 my $http_client = $ENV{HTTP_USER_AGENT} || '';
72 # files to save with backup function
73 my @backup_files = qw(
82 if ($action eq "backup") {
87 http_header('200 OK');
89 $_ = html_header("F*EX Admin Control for $hostname");
90 s:</h1>: (<a href="?action=logout">logout</a>)</h1>:;
94 "<li><a href=\"?action=create\">Create new user</a>\n".
95 "<li><a href=\"?action=change-auth\">Change user auth-ID</a>\n".
96 "<li><a href=\"?action=edit\">Edit user restrictions file</a>\n".
97 "<li><a href=\"?action=delete\">Delete existing user</a>\n".
98 "<li><a href=\"?action=quota\">Manage disk quota</a>\n";
101 "<li><a href=\"?action=fup.log\">Get fup.log</a>\n".
102 "<li><a href=\"?action=fop.log\">Get fop.log</a>\n".
103 "<li><a href=\"?action=error.log\">Get error.log</a>\n";
105 if (-f "$logdir/fexsrv.log") {
107 "<li><a href=\"?action=watch\">Watch logfile</a>\n".
108 "<li><a href=\"?action=fexsrv.log\">Get fexsrv.log</a>\n".
113 "<li><a href=\"?action=backup\">Download backup<br>(config only)</a>\n".
114 "<li><a href=\"?action=restore\">Restore backup</a>\n";
117 "<li><a href=\"?action=list\">List spooled files</a>\n".
118 "<li><a href=\"?action=showquota\">Show quotas (sender/recipient)</a>\n".
119 "<li><a href=\"?action=showconfig\">Show server config</a>\n".
120 "<li><a href=\"?action=userconfig\">Show user config</a>\n";
123 "<li><a href=\"?action=editconfig\">Edit config</a>\n".
124 "<li><a href=\"?action=editindex\">Edit index.html</a>\n";
128 ' <th>manage user</th>'
130 ' <th>log files</th>'
134 ' <td><ul>$nav_user</ul>'
135 ' <td><ul>$nav_show</ul>'
136 ' <td><ul>$nav_log</ul>'
137 ' <td><ul>$nav_edit</ul>'
138 ' <td><ul>$nav_backup</ul>'
144 my @user_items = &userList;
146 if ($action eq "create") { &createUserForm }
147 elsif ($action eq "change-auth") { &changeAuthForm }
148 elsif ($action eq "edit") { &editRestrictionsForm }
149 elsif ($action eq "delete") { &deleteUserForm }
150 elsif ($action eq "quota") { &changeQuotaForm }
151 elsif ($action eq "list") { &listFiles }
152 elsif ($action eq "showquota") { &showQuota }
153 elsif ($action eq "showconfig") { &showConfig }
154 elsif ($action eq "userconfig") { &userConfigForm }
155 elsif ($action eq "watch") { &watchLog }
156 elsif ($action eq "fexsrv.log") { &getlog("fexsrv.log") }
157 elsif ($action eq "fup.log") { &getlog("fup.log") }
158 elsif ($action eq "fop.log") { &getlog("fop.log") }
159 elsif ($action eq "error.log") { &getlog("error.log") }
160 elsif ($action eq "editconfig") { &editFile("$FEXLIB/fex.ph") }
161 elsif ($action eq "editindex") { &editFile("$docdir/index.html") }
162 elsif ($action eq "backup") { &backup }
163 elsif ($action eq "restore") { &restoreForm }
165 if (defined $PARAM{"createUser"}) {
166 createUser($PARAM{"createUser"}, $PARAM{"authID"});
167 } elsif (defined $PARAM{"changeAuthUser"}) {
168 if ($PARAM{"changeAuthUser"} =~ /^#.*/) {
171 changeUser($PARAM{"changeAuthUser"}, $PARAM{"authID"});
173 } elsif (defined $PARAM{"showUserConfig"}) {
174 if ($PARAM{"showUserConfig"} =~ /^#.*/) {
177 showUserConfig($PARAM{"showUserConfig"});
179 } elsif (defined $PARAM{"deleteUser"}) {
180 if ($PARAM{"deleteUser"} =~ /^#.*/) {
183 deleteUser($PARAM{"deleteUser"});
185 } elsif (defined $PARAM{"userQuota"}) {
186 if ($PARAM{"userQuota"} =~ /^#.*/) {
189 if (defined $PARAM{"default quota"}) {
190 $user = normalize_user($PARAM{"userQuota"});
191 unlink "$user/\@QUOTA";
192 print "$user has now default quota:<p>\n";
193 print "recipient quota: $recipient_quota MB<br>\n";
194 print "sender quota: $sender_quota MB<br>\n";
199 $PARAM{"recipientQuota"},
200 $PARAM{"senderQuota"}
204 } elsif (defined $PARAM{"editUser"}) {
205 if ($PARAM{"editUser"} =~ /^#.*/) {
206 &editRestrictionsForm;
208 $user = normalize_user($PARAM{"editUser"});
209 if (defined $PARAM{"delete file"}) {
210 unlink "$user/\@ALLOWED_RECIPIENTS";
211 print "upload restrictions for $user have been deleted\n";
217 } elsif ($PARAM{"contentBox"} and $PARAM{"ar"}) {
218 saveFile($PARAM{"contentBox"},$PARAM{"ar"});
219 } elsif ($PARAM{"upload_archive"}) {
220 restore($PARAM{"upload_archive"}{data});
226 # declaration of formular functions
229 # formular for creating new users
230 # required arguments: -
232 print h3("Create new user");
234 '<form action="/$fac" method="post" enctype="multipart/form-data">'
237 '<td>user</td><td><input type="text" name="createUser" size="80"></td>'
240 '<td>auth-ID:</td><td><input type="text" name="authID" size="16"></td>'
243 '<input type="submit" name="create user" value="create user">'
249 # formular for changing auth-id of an user
250 # required arguments: -
252 my @option = map { "<option value=\"$_\">$_</option>\n" } @user_items;
254 print h3("change auth-ID");
256 '<form action="/$fac" method="post" enctype="multipart/form-data">'
259 '<td>user:</td><td><select name="changeAuthUser">@option</select></td>'
262 '<td>new auth-ID:</td><td><input type="text" name="authID" size="16"></td>'
265 '<input type="submit" name="change" value="change">'
271 # formular choosing user, whose config files shall be shown
272 # required arguments: -
274 my @option = map { "<option value=\"$_\">$_</option>\n" } @user_items;
276 print h3("Show user config files");
278 '<form action="/$fac" method="post enctype="multipart/form-data">'
281 '<td>user:</td><td><select name="showUserConfig">@option</select></td>'
284 '<input type="submit" name="show config files" value="show config files">'
290 # formular for choosing user, whose restriction file shall be edited
291 # required arguments: -
292 sub editRestrictionsForm {
293 my @option = map { "<option value=\"$_\">$_</option>\n" } @user_items;
295 print h3("Edit user restriction file");
297 '<form action="/$fac" method="post enctype="multipart/form-data">'
300 '<td>user:</td><td><select name="editUser">@option</select></td>'
303 '<input type="submit" name="edit file" value="edit file">'
304 '<input type="submit" name="delete file" value="delete file">'
310 # formular for choosing user, who shall be removed
311 # required arguments: -
313 my @option = map { "<option value=\"$_\">$_</option>\n" } @user_items;
315 print h3("Delete existing user");
317 '<form action="/$fac" method="post enctype="multipart/form-data">'
320 '<td>user:</td><td><select name="deleteUser">@option</select></td>'
323 '<input type="submit" name="delete user" value="delete user">'
329 # formular for changing an user's quota file
330 # required arguments: -
331 sub changeQuotaForm {
337 if ($user = $PARAM{"user"}) {
339 $user = normalize_user($user);
340 $rquota = $1 if ($PARAM{"rquota"}||'') =~ /^(\d+)$/;
341 $squota = $1 if ($PARAM{"squota"}||'') =~ /^(\d+)$/;
344 foreach (@user_items) {
345 if ($user and $user eq $_) {
346 push @option,"<option value=\"$_\" selected>$_</option>\n";
348 push @option,"<option value=\"$_\">$_</option>\n";
352 print h3("Manage disk quota");
354 '<form action="/$fac" method="post" enctype="multipart/form-data">'
357 '<td>user:</td><td><select name="userQuota">@option</select></td>'
360 '<td>new quota for recipient:</td>'
361 '<td><input type="text" name="recipientQuota" size="12" value=\"$rquota\">'
362 ' MB (optional)</td>'
365 '<td>new quota for sender:</td>'
366 '<td><input type="text" name="senderQuota" size="12" value=\"$squota\">'
367 ' MB (optional)</td>'
370 '<input type="submit" name="change quota" value="change quota">'
371 '<input type="submit" name="default quota" value="default quota">'
377 # formular for choosing backup file to restore
378 # required arguments: -
380 print h2("restore config");
382 'Specify the backup-archive you want to restore:<br>'
383 '<form action="/$fac" method="post" enctype="multipart/form-data">'
384 '<input type="file" name="upload_archive" size="80"><br>'
385 '<input type="submit" name="restore" value="restore">'
393 # declaration user functions
396 # function for creating new users
397 # required arguments: username, auth-id
402 http_die("not enough arguments in createUser") unless $id;
404 $user = normalize_user($user);
406 unless (-d "$user") {
407 mkdir "$user",0755 or http_die("cannot mkdir $user - $!");
413 html_error($error,"There is already an user $user!");
416 open $idf,'>',$idf or http_die("cannot write $idf - $!");
417 print {$idf} $id,"\n";
418 close $idf or http_die("cannot write $idf - $!");
420 printf "%s?from=%s&ID=%s<br>\n",$fup,$user,$id;
421 printf "%s/%s<p>\n",$fup,b64("from=$user&id=$id");
423 notifyUser($user,$id);
424 print "An information e-mail to $user has been sent.\n";
428 # function for changing an user's auth-ID
429 # required arguments: username, auth-id
433 http_die("not enough arguments in changeUser") unless $id;
436 $user = normalize_user($user);
441 open $idf,'>',$idf or http_die("cannot write $idf - $!");
442 print {$idf} $id,"\n";
443 close $idf or http_die("cannot write $idf - $!");
444 printf "%s?from=%s&ID=%s<br>\n",$fup,$user,$id;
445 printf "%s/%s\n",$fup,b64("from=$user&id=$id");
446 print "</code><p>\n";
447 notifyUser($user,$id,"change-auth");
448 print "An information e-mail to $user has been sent.\n";
452 # function for showing an user's config files
453 # required arguments: username
457 http_die("not enough arguments in showUserConfig!") unless $user;
458 $user = normalize_user($user);
460 chdir "$user" or http_die("could not change directory $user - $!");
461 print h2("Config files of <code>$user</code>");
463 foreach my $file (glob('.auto @* @GROUP/*')) {
464 if (-f $file and not -l $file and $file !~ /.*~$/) {
465 print h3($file), "\n";
466 open $file,'<',$file or http_die("cannot open $file - $!");
467 # print "<table border=1><tr><td>\n";
469 # print "</tr></table>\n";
476 # function for editing an user's recipient/sender restrictions
477 # required arguments: username
482 http_die("not enough arguments in editUser") unless $user;
483 $user = normalize_user($user);
484 http_die("no user $user") unless -d $user;
485 my $ar = "$user/\@ALLOWED_RECIPIENTS";
487 open $ar,'>',$ar or http_die("cannot open $ar - $!");
489 # Restrict allowed recipients. Only those listed here are allowed.
490 # Make this file COMPLETLY empty if you want to disable the restriction.
491 # An allowed recipient is an e-mail address, you can use * as wildcard.
492 # Example: *@flupp.org
496 $content = dehtml(slurp($ar));
498 'Edit restrictions file for user $user :<br>'
499 '<form action="/$fac" method="post" enctype="multipart/form-data">'
500 '<textarea name="contentBox" rows="10" cols="80">'
503 '<input type="hidden" name="ar" value="$ar">'
504 '<input type="submit" name="save changes" value="save changes">'
510 # function for deleting files
511 # required arguments: list of Files
513 http_die("not enough arguments in deleteFiles") unless (my @files = @_);
518 print "file has been deleted: $_<br>\n";
520 print "file could not be deleted: $_ - $!<br>\n";
523 print "file does not exists: $_<br>\n";
529 # function for saving a single file
530 # required arguments: content, location
535 http_die("not enough arguments in saveFile") unless $ar;
537 if ($ar eq 'index.html') {
538 $ar = "$docdir/index.html"
539 } elsif ($ar eq 'fex.ph') {
540 $ar = "$FEXLIB/fex.ph"
541 } elsif ($ar =~ m'^([^/]+/\@ALLOWED_RECIPIENTS)$') {
544 http_die("unknown file $ar")
548 if ($ar =~ /fex.ph$/) {
549 open $new,'>',$new or http_die("cannot open ${ar}_new - $!");
551 close $new or http_die("cannot write $new - $!");;
552 my $status = dehtml(`perl -c $FEXLIB/fex.ph_new 2>&1`);
553 if ($status =~ /syntax OK/ ) {
556 http_die("cannot write $ar~ - $!") if $?;
560 'No valid syntax in configuration file:'
561 '<p><pre>$status</pre><p>'
562 '<a href="javascript:history.back()">back</a>'
567 system 'mv',$ar,"$ar~";
570 open $ar,'>',$ar or http_die("cannot write $ar - $!");
572 close $ar or http_die("cannot write $ar - $!");;
573 print "<code>$ar</code> has been saved\n";
577 # function for deleting existing user
578 # required arguments: username
582 http_die("not enough arguments in deleteUser") unless $user;
584 $user = normalize_user($user);
587 http_die("no such user $user") unless -f $idf;
588 unlink $idf or http_die("cannot remove $idf - $!");
589 unlink "$user/\@ALLOWED_RECIPIENTS";
590 unlink "$user/\@SUBUSER";
591 rmrf("$user/\@GROUP");
592 print "$user deleted\n";
596 # function for saving quota information for one single user
597 # required arguments: username, recipient-quota, sender-quota
599 my ($user,$rq,$sq) = @_;
600 my ($rquota,$squota);
603 $user = normalize_user($user);
604 http_die("$user is not a F*EX user") unless -d $user;
606 $rquota = $squota = '';
607 $qf = "$user/\@QUOTA";
611 $rquota = $1 if /recipient.*?(\d+)/i;
612 $squota = $1 if /sender.*?(\d+)/i;
617 $rquota = $1 if $rq and $rq =~ /(\d+)/;
618 $squota = $1 if $sq and $sq =~ /(\d+)/;
619 open $qf,'>',$qf or http_die("cannot write $qf - $!");
620 print {$qf} "recipient:$rquota\n" if $rquota;
621 print {$qf} "sender:$squota\n" if $squota;
622 close $qf or http_die("cannot write $qf - $!");
624 $rquota = $recipient_quota unless $rquota;
625 $squota = $sender_quota unless $squota;
626 print h3("New quotas for $user");
627 print "recipient quota: $rquota MB<br>\n";
628 print "sender quota: $squota MB<br>\n";
632 # function for listing f*exed files
633 # required arguments: -
635 print h3("List current files");
638 foreach $recipient (glob "*@*") {
639 next if -l $recipient;
640 foreach $file (glob "$recipient/*/*") {
641 if (-s "$file/data" and $dkey = readlink("$file/dkey") and -l ".dkeys/$dkey") {
642 ($to,$from,$file) = split "/",$file;
643 $file = html_quote($file);
644 print "$from → $to : $durl/$dkey/$file\n";
652 # function for watching the fex-logfile
653 # required arguments: -
655 if (-f "$logdir/fexsrv.log") {
656 print h2("polling fexsrv.log"),"\n";
657 open my $log,"$FEXHOME/bin/logwatch|"
658 or http_die("cannot run $FEXHOME/bin/logwatch - $!");
661 print h2("no fexsrv.log");
666 # function for showing logfiles
667 # required arguments: logfile-name
669 my $log = shift or http_die("not enough arguments in getLog");
671 print h2("show $log");
672 if (open $log,"$logdir/$log") {
676 http_die("cannot open $logdir/$log - $!");
681 # function for creating a new backup file
682 # required arguments: -
684 my @d = localtime time;
685 my $date = sprintf('%d%02d%02d',$d[5]+1900,$d[4]+1,$d[3]);
686 my $backup = "backup/config-$date.tar";
687 my $http_client = $ENV{HTTP_USER_AGENT} || '';
691 $home = $1 if $ENV{VHOST} and $ENV{VHOST} =~ /:(.+)/;
693 chdir $home or http_die("$home - $!");
695 unless (-d "backup") {
696 mkdir "backup",0700 or http_die("cannot mkdir backup - $!");
699 system "tar -cf $backup @backup_files 2>/dev/null";
701 $size = -s $backup or http_die("backup file empty");
703 open $backup,'<',$backup or http_die("cannot open $backup - $!");
707 "Content-Length: $size",
708 "Content-Type: application/octet-stream; filename=fex-backup-$date.tar",
709 "Content-Disposition: attachment; filename=\"fex-backup-$date.tar\"",
713 while (read($backup,my $b,$bs)) {
720 # function for restoring an old configuration file
721 # required arguments: uploaded archive
723 my $archive_file = shift or http_die("not enough arguments in restore!");
724 my $restore = "backup.tar";
727 $home = $1 if $ENV{VHOST} and $ENV{VHOST} =~ /:(.+)/;
729 chdir $home or http_die("$home - $!");
732 open $restore,'>',$restore or http_die("cannot open $restore - $!");
733 print {$restore} $archive_file;
734 close $restore or http_die("cannot write $restore - $!");
736 print "file upload successful<br>\n";
737 print "saving actual config in $home/backup/config.tar<br>\n";
739 system "tar -cf backup/config.tar @backup_files";
741 print "starting restore:\n<p>\n";
743 system "tar -xvf $restore";
747 http_die("upload error - no file data received");
751 # function for editing a text-file
752 # required arguments: filepath, filename
757 $file = dehtml(slurp($ar));
761 print h2("edit <code>$ar<code>");
764 '<form action="/$fac" enctype="multipart/form-data" method="post">'
765 '<textarea name="contentBox" rows="26" cols="80">'
768 '<input type="hidden" name="ar" value="$ar">'
769 '<input type="submit" name="save changes" value="save changes">'
775 # function for showing all users' quotas
776 # required arguments: -
779 print h2("Show quotas (domain sorted, values in MB)");
780 print "<table border=\"1\"><tr>";
781 foreach (@user_items) {
782 if (/\#\#\#\s(\S+)/) {
784 print "<th>\@$1</th>";
785 print "<th>sender</th>";
786 print "<th>sender (used)</th>";
787 print "<th>recipient</th>";
788 print "<th>recipient (used)</th>";
792 my $rquota = $recipient_quota;
793 my $squota = $sender_quota;
797 ($squota,$squota_used) = check_sender_quota($user);
798 ($rquota,$rquota_used) = check_recipient_quota($user);
799 my $action = "quota&user=$user&rquota=$rquota&squota=$squota";
802 print "<td><a href=\"?action=$action\">$_</a></td>";
803 print "<td align=\"right\">$squota</td>";
804 print "<td align=\"right\">$squota_used</td>";
805 print "<td align=\"right\">$rquota</td>";
806 print "<td align=\"right\">$rquota_used</td>";
815 # function for showing fex-server configuration
816 # required arguments: -
818 print h3("Show config");
819 print "<table border=\"0\">\n";
820 printf "<tr><td>spooldir:</td><td>%s</td>\n",$spooldir;
821 printf "<tr><td>logdir:</td><td>%s</td>\n",$logdir;
822 printf "<tr><td>docdir:</td><td>%s</td>\n",$docdir;
823 printf "<tr><td>durl:</td><td>%s</td>\n",$durl;
824 printf "<tr><td>mdomain:</td><td>%s</td>\n",$mdomain||'';
825 printf "<tr><td>autodelete:</td><td>%s</td>\n",$autodelete;
826 printf "<tr><td>keep:</td><td>%s</td>\n",$keep_default;
827 printf "<tr><td>keep_max:</td><td>%s</td>\n",$keep_max;
828 printf "<tr><td>recipient_quota:</td><td>%s</td>\n",$recipient_quota;
829 printf "<tr><td>sender_quota:</td><td>%s</td>\n",$sender_quota;
830 printf "<tr><td>admin:</td><td>%s</td>\n",$admin;
835 # require authentification
840 if ($action eq 'logout') {
841 if (($ENV{HTTP_COOKIE}||'') =~ /akey=(\w+)/) {
842 unlink "$akeydir/$1";
845 "HTTP/1.1 301 Moved Permanently",
848 "Set-Cookie: akey=; Max-Age=0; Discard",
854 $rid = slurp("$admin/@") or html_error($error,"no F*EX account for $admin");
862 $akey = md5_hex("$admin:$rid");
867 return if $akey eq md5_hex("$admin:$rid");
870 http_header('200 OK');
871 print html_header("F*EX Admin Control for $hostname");
875 '<font color="red"><h3>'
876 ' wrong akey for <code>$admin</code>'
881 if ($id and $id ne $rid) {
883 '<font color="red"><h3>'
884 ' wrong auth-ID for <code>$admin</code>'
890 '<form action="/$fac" method="post" enctype="multipart/form-data">'
891 ' auth-ID for <code>$admin</code>:'
892 ' <input type="password" name="id" size="16" autocomplete="off">'
899 # function for checking simple HTTP authentication
900 # (not used any more, replaced with require_akey)
902 if ($ENV{HTTP_AUTHORIZATION} and $ENV{HTTP_AUTHORIZATION} =~ /Basic\s+(.+)/)
903 { @http_auth = split(':',decode_b64($1)) }
906 or $http_auth[0] !~ /^(fexmaster|admin|\Q$admin\E)$/
907 or $http_auth[1] ne $admin_pw
910 '401 Authorization Required',
911 "WWW-Authenticate: Basic realm=$admin F*EX admin authentification",
914 # control back to fexsrv for further HTTP handling
920 # function for sending notification mails to an user
921 # required arguments: username, auth-id, message-type
923 my ($user,$id,$type) = @_;
925 my $message = 'A F*EX account has been created for you. Use';
927 http_die("not enough arguments in createUser") unless $id;
928 if ($type and $type eq "change-auth") {
929 $message = 'New auth-ID for your F*EX account has been set. Use'
932 $user = normalize_user($user);
933 open my $mail,'|-',$sendmail,'-f',$admin,$user,$bcc
934 or http_die("cannot start sendmail - $!");
939 'Subject: your F*EX account on $hostname'
944 '$url/fup?from=$user'
947 'See $url/index.html for more information about F*EX.'
949 'Questions? ==> F*EX admin: $admin'
952 or http_die("cannot send notification e-mail (sendmail error $!)");
956 # sort key is the (inverse) domain
957 # required arguments: list of usernames (e-mail addresses)
959 # http_die("not enough arguments in domainsort") unless (my @d = @_);
965 s/\./,/ while /\..*@/;
967 $_ = join('.',reverse(split /\./));
970 @d = sort { lc $a cmp lc $b } @d;
973 $_ = join('.',reverse(split /\./));
981 # function for creating a sorted list of all users
982 # required arguments: -
988 foreach $u (glob('*@*')) {
990 push @u,$u if -f "$u/@";
993 foreach (domainsort(@u)) {
996 push @list,"### $1 ###";
1011 while (<$file>) { print dehtml($_) }
1019 return "<h2>$_</h2>\n";
1026 return "<h3>$_</h3>\n";
1031 print "</body></html>\n";