#!/usr/bin/perl -w
#
# Alias Editor for _big_ sendmail alias files (>1000 entries), where
# keeping the whole file in memory is not practical.
#
# Changelog:
# 991119: First rewrite. Don't carry complete alias list in hidden
#	  fields around.
# 991203: Change textfields into one big textarea.
# 001125: README, fm announcement, cleanups, cleaner HTML, documentation
#	  (well, sort of)
#
# -------------------------------------------------------------------------

use CGI;
use CGI qw(:netscape :html3 :standard);
use CGI::Carp qw(fatalsToBrowser);
use File::Copy;

my $version = '20001125';
my $d=1;				# Debugging anschalten

my $mxdomain = '@mail01.bla.fasel.net';
my $aliasfile = '/etc/aliases';
my $abackup = 'aliases.o'; my $abackup2 = 'aliases.oo';	# 2 Generationen
my $temp = '/tmp/.aliases';	# Temp file template
my $bgcolor = '#999999';
my $tbgcolor = '#cccccc';
my $minlen = 2;	    # gilt für AIlases UND für Zieladressen!
my $helpmain = "Geben Sie den Suchbegriff für den Mailalias oder das Ziel ein. Benutzen Sie ".b("keine")." Wildcards wie ".b("*")." oder " .b(".")." !";
my $helpedit = "Ändern Sie die angezeigten Zieladressen nach Belieben. Um einen Alias-Eintrag zu löschen, entfernen Sie <b>nur</b> den Text <b>rechts</b> vom Doppelpunkt, dann wird er nicht mehr in die Datenbank übernommen.  Die MX-Domain <b>$mxdomain</b> wird bei der Speicherung automagisch angehängt (falls hier nicht eine andere Domain angehängt wurde).<p><i>Sämtliche Leerzeichen/TABs in einer Zeile werden entfernt</i>.";


# Hacking-absicherung:
$CGI::POST_MAX = 1024*100;  # 100kb
$CGI::DISABLE_UPLOADS = 1;


# ----------------------------------------------------------------------
# HTML HEADER darstellen.
print header(), start_html(-TITLE=>"Alias Editor", -BGCOLOR=>$bgcolor),
    center(b(h1("Alias Editor (Version $version)")));


# ----------------------------------------------------------------------
# HAUPTMENU darstellen.
if(!param())
{
    # Anfangsbuchstaben-Selektor aufstellen.
    print center(table({-BORDER=>0, -BGCOLOR=>$tbgcolor, -WIDTH=>"100%"},
	TR(td( center(h3(b("Schnellauswahl:")),
	    map { a({href=>url()."?show=^$_"}, "&nbsp;".$_."&nbsp;") } (A..Z) ),
	)))), p();
    
    print 
	center(table({-BORDER=>0, -BGCOLOR=>$tbgcolor, -WIDTH=>"100%"},
	TR(td( center( b(h3("Schnellsuche:")), $helpmain, p(),
	start_form(), b("Suchbegriff:"), "&nbsp;&nbsp; ",
	textfield(-name=>"show", -size=>60), p(), 
	submit(-name=>"Suchen"), end_form(), 
    )))), p(), );
    

    print start_form(), 
	center(table({-BORDER=>0, -BGCOLOR=>$tbgcolor, -WIDTH=>"100%"},
	TR(td( center( b(h3("Neueinträge:")),
	    "Format: \"Mailalias:Ziel\", Domain $mxdomain wird
		automagisch angehängt, falls nicht angegeben", p,
	    textarea(-name=>"aliases", -rows=>10, -columns=>60),
	), 
    center(submit(-name=>"Speichern"), "&nbsp;", reset(-name=>"Verwerfen"))),
	end_form )));
}


# ----------------------------------------------------------------------
# Es gibt einen Suchbegriff. Gefundene Einträge auslesen und darstellen.
elsif(param('show'))
{
    # Einzelner Buchstabe? -> nur Anfang suchen
    $wildcard = param('show');
    my $p = $$;	# für eindeutige Dateinamen in /tmp
    print "Prozess: $p", br() if($d);
    print "Wildcard: ", b($wildcard), br() if($d);

    print p(), table({border=>"0", -BGCOLOR=>$tbgcolor, -WIDTH=>"100%"},
	TR(td(center($helpedit)))), p();

    #system("rm -vf $temp-*") and die "Kann Tempdatei nicht löschen: $!";

    # Alles "passende" zum editieren anzeigen, alles "unpassende" gleich
    # unmodifiziert wegschreiben.
    open(ALIASFILE, $aliasfile) or die "Kann Aliasfile nicht öffnen: $!";
    open(TEMP, ">$temp-$p") or die "Kann Tempdatei nicht erstellen: $!";
    my %aliases;
    my $count = 0;
    while(<ALIASFILE>) {
	if(/$wildcard/i) {  # Kommentare etc. wechhauen ...
	    s/#.*//; s/\s*//g; s/^[^\s\d\:\.\-\_\@]+$//g;
	    next if(length($_) <=1 );
	    my ($alias, $dest) = split /:/;
	    $aliases{$alias} = $dest;
	    $count++;
	    # print "<small>$alias / $aliases{$alias}<br></small>" if($d);
	} else {
	    print TEMP ;
	}
    };
    close ALIASFILE; close TEMP;

    # Alles in Tabellen zum Editieren anzeigen.
    print start_form(), "<center><table border=\"1\" bgcolor=\"$tbgcolor\" >\n",
	hidden(-name=>"process", -value=>$p),
	hidden(-name=>"tempfile_exists", -value=>1);

    my $formtext;
    while (@a = each(%aliases)) {
	$formtext .= $a[0]."\t:  ".$a[1]."\n";
    };

    print TR(center(textarea(
	-name=>'aliases', -value=>$formtext, -rows=>40, -columns=>60)));

    print "</table></center>\n", center(
	p(), table({border=>"0", -BGCOLOR=>$tbgcolor, -WIDTH=>"100%"},
	    TR(td(center("Anzahl Fundstellen: ", b($count))))),  p(),
	submit(-name=>"Speichern"), "&nbsp;", reset(-name=>"Verwerfen")),
	end_form(); 
}


# ----------------------------------------------------------------------
# Es wurden Aliases geändert (es existiert eine /tmp/aliases-$$ Datei).
# $TEMP wird eingelesen und in %aliases (hash) eingetragen. Dann werden die
# neu eingetragenen aliases an %aliases angehängt. Da ein hash keine
# Dubletten akzeptiert, werden diese effizient vermieden (ohne sich mit
# uniq/sort/.. rumzuquälen.
#
elsif(param('Speichern'))
{
    system("cp -f $abackup $abackup2") and die "Backup 2 nicht erstellt: $!";
    system("cp -f $aliasfile $abackup") and die "Backup nicht erstellt: $!";

    # TEMPdatei einlesen.
    my $p=param("process");
    if(param("tempfile_exists")) {
	open(TEMPFILE, "$temp-$p") or die "Kann TEMPfile nicht öffnen: $!";
	print "TEMPFILE = $temp-$p<br>" if($d);
    } else {
	open(TEMPFILE, $aliasfile) or die "Kann Aliasdatei nicht öffnen: $!";
	print "TEMPFILE = $aliasfile<br>" if($d);
    };
    my %aliases;
    my $acount = 0;
    while(<TEMPFILE>) {
	s/#.*//; s/\s*//g; s/^[^\s\d\:\.\-\_\@]+$//g;
	next if(length($_) <=1 );
	my ($alias, $dest) = split /:/;
	$aliases{$alias} = $dest;
	$acount++;
    };
    close TEMPFILE;
    if(param("tempfile_exists")) {
	system("rm -f $temp-$p")
	    and die "Kann Tempdatei ($temp-$p) nicht löschen: $!";
    };

    # Neue/geänderte aliases einlesen + ggf. MXdomain anhängen.
    @adata = split(/\n/, param("aliases"));
    my $ncount = 0;
    for(@adata) {
	s/#.*//; s/\s*//g; s/^[^\s\d\:\.\-\_\@]+$//g;
	next if((length($_) <=1) );
	$_ = $_.$mxdomain if(!/\@/);
	($alias, $dest) = split /:/;
	$aliases{$alias} = $dest;
	$ncount++;
    };

    open(ALIASFILE, ">$aliasfile") or die "Kann neue Aliasdatei nicht schreiben: $!";

    # Dubletten vermeiden und gleich schön sortieren :)
    # for(sort grep($_ ne $prev && ($prev = $_), sort each(%aliases))) {
    @a = keys(%aliases);
    my $tcount = 0;
    for(@a) {
	print small($_.":".$aliases{$_}." &nbsp;&nbsp;") if($d);
	if((length($_)>=$minlen) && (length($aliases{$_})>=$minlen)) {
	    print ALIASFILE $_.":".$aliases{$_}."\n";
	    $tcount++;
	}
    };

    close ALIASFILE;

    $dcount=$acount-$ncount;
    print p(), center(b("$tcount Einträge ($dcount alte, $ncount neue/geänderte) geschrieben."), p(),
	a({href=>url()}, h3(b("Zurück"))));

}


# ----------------------------------------------------------------------
# HTML FOOTER + shameless plug darstellen :-)
print p(), table({border=>"0", -BGCOLOR=>$tbgcolor, -WIDTH=>"100%"}, TR(td(
    center "Erstellt von <a href=\"mailto:jens-cgiservice\@spamfreemail.de\">".
    "Jens Benecke</a>. Benutzung ohne Gewähr. Lizensiert unter der GPL".
    " (GNU Public Licence) Version 2.0." ) ) ),
     end_html();



# ======================================================================
# vim:set softtabstop=4 nowrap:

# Strategy:
#
# - Show intro screen with search function and A-Z selectors.
# - A-Z points to url."show=^$LETTER". (regexp for line beginning)
# - Search string points to url."show=$STRING".
# - Insert points to url."insert=1".
#   - Make backup of $aliases, append param() to $aliases, return. Simple.
# - If param('show'), read $aliases file, dump everything except matches
#   into $TEMP. Show matches in TABLE. Don't change $aliases.
#   - CANCEL: Delete $TEMP. 
#   - SAVE: -> url."save=1" Write changes (params) to $TEMP, mv $TEMP
#     $aliases.  Ignore (don't save) entries where del_$entry is marked.
#
# - Return to main screen.
#
# ======================================================================


