unser neuer mailadmin 2. Versuch 000
authorheiko
Fri, 04 Nov 2005 06:29:26 +0000
changeset 0 2a5f2464f8c6
child 1 5c55c8f7986a
unser neuer mailadmin 2. Versuch
Installed
Makefile
Steps
account.pm
alias.pm
im
imap.pm
injectMail
ldap.schema
ldapBase.pm
ma
ma.8.pod
man8/ma.8.gz
manual.tex
mergePasswdShadowAliases
password.pm
populate.ldif
split
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Installed	Fri Nov 04 06:29:26 2005 +0000
@@ -0,0 +1,1 @@
+Autohaus Dresden Reick
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Makefile	Fri Nov 04 06:29:26 2005 +0000
@@ -0,0 +1,77 @@
+# © 2005 Heiko Schlittermann
+# $Id$
+# $URL$
+
+package = ma
+prefix = /usr/local
+sbindir = $(prefix)/sbin
+libdir = $(prefix)/lib/ma
+mandir = $(prefix)/man
+man8dir = $(mandir)/man8
+
+DESTDIR ?=
+
+stowdir=/usr/local/stow/$(package)
+
+
+SCRIPTS = ma
+PM = account.pm password.pm ldapBase.pm alias.pm
+MAN = ma.8.gz
+
+CHECKED = $(addprefix .ok.,$(SCRIPTS) $(PM))
+RUBBER_FLAGS = -s -I ${HOME}/Office/lib -I ${HOME}/Office/lib/pictures 
+
+PDF = manual.pdf
+DVI = manual.dvi
+DOC = $(PDF) $(DVI)
+
+.PHONY:	all install uninstall clean distclean dvi pdf
+
+all:	$(CHECKED) man
+
+man:	$(MAN)
+
+doc:	$(DOC)
+
+install:	all
+	install -m755 -d $(DESTDIR)$(sbindir)
+	install -m755 -d $(DESTDIR)$(libdir)
+	install -m755 -d $(DESTDIR)$(man8dir)
+	install -m755 $(SCRIPTS) $(DESTDIR)$(sbindir)/
+	install -m644 $(PM) $(DESTDIR)$(libdir)/
+	install -m644 $(MAN) $(DESTDIR)$(man8dir)/
+
+clean:
+	-rm -f $(CHECKED) $(MAN)
+	-rm -f $(CLEANFILES)
+
+distclean:	clean
+	-rm -f $(DOC)
+
+
+stow: 		all
+	make DESTDIR=$(stowdir) prefix=/ install
+	stow -v -d $(dir $(stowdir)) -R $(package)
+
+
+unstow:
+	stow -v -d $(dir $(stowdir)) -D $(package)
+	rm -rv $(stowdir)
+
+### 
+
+dvi:	$(DVI)
+pdf:	$(PDF)
+
+.ok.%:	%
+	@perl -c $<
+	@touch $@
+
+%.pdf:	%.tex
+	rubber --pdf ${RUBBER_FLAGS} $<
+
+%.dvi:	%.tex
+	rubber ${RUBBER_FLAGS} $<
+
+%.gz:	%.pod
+	pod2man --section $(subst .,,$(suffix $@)) $< | gzip >$@
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Steps	Fri Nov 04 06:29:26 2005 +0000
@@ -0,0 +1,127 @@
+Damit cyrus22 mit LDAP funktioniert.
+
+__LDAP aufsetzen (logisch)
+
+  - nicht erforderlich sollten ldap_pam und ldap_nss sein
+  - Den Cyrus-Admin eintragen, z.B:
+
+  Als Anregung 'populate.ldif'... 
+
+
+    
+
+__cyrus22
+  - sources.list: deb http://mail.incase.de/cyrus22/i386/ ./ 
+  - auch den cyrus22-imapd (Der wird für cyradm gebraucht.  Wie wollen die
+    Mailboxen sonst angelegt werden?)
+
+  - virtual Domain support.  Achtung, die 'default_domain' wird immer
+    abgeschnitten und dann werden ldap usw gefragt ohne diese Domain
+    hinten dran.  Scheint ein anderes Konzept zu sein...
+
+  - unixhierarchysep: yes
+  - altnamespace: yes
+
+  Offenbar muss auxprop wissen, dass ldap genutzt werden soll.  Wie das?
+  Oder der saslauthd muss es wissen.
+
+  (Oder aber über pam?  Vielleicht auch das als Lösung.  Aber kann Cyrus
+  mit pam? - das wird als die krankeste Lösung dargestellt.)
+
+  Jetzt mit saslauthd (aus sasl2-bin) - der muß in
+  /etc/default/saslauthd ldap eingetragen gekommen und natürlich eine
+  Config-Datei haben.
+
+  In /etc/saslauthd.conf
+
+    Wenn über ein LDAP-Bind geprüft werden soll, dann muss dort
+    kein Nutzer und kein Passwort drinstehen.
+
+    Mit der Methode 'custom' wird nach userPassword gesucht und dann
+    offenbar wirklich das Passwort verglichen. (?)
+
+    ,---[ saslauthd.conf ]---
+      ldap_auth_method: bind
+      #ldap_auth_method: custom
+      #ldap_bind_dn: cn=admin,dc=ddc-consult,dc=de
+      #ldap_bind_pw: x
+
+      ldap_search_base: dc=ddc-consult,dc=de
+      #ldap_servers: ldap://localhost:389/
+      #ldap_default_realm: ddc-dresden.de
+    `--------------------------
+
+  => APOP geht nicht (siehe auch Manual-Seite: dazu müsste auxprop
+  Klartext-Passworte haben, die's aber nicht hat, wenn es saslauthd
+  nutzt.
+
+__exim4
+  
+  Exim soll mit Cyrus über LMTP sprechen, also muss ein exim-Transport
+  her (und gleich noch einer für Amavis):
+    begin transports
+    ...
+
+    cyrdeliver:
+      driver = lmtp
+      socket = /var/run/cyrus/socket/lmtp
+      transport_filter = /usr/bin/spamc -u $local_part@$domain
+      group = mail
+
+    amavis:
+      driver = smtp
+      port = 10024
+      allow_localhost
+
+
+  Und natürlich müssen die Router auch Bescheid wissen:
+     ldap_default_servers = localhost::389
+     LDAP_USERS = ou=MailAccounts,dc=ddc-consult,dc=de
+     LDAP_ALIASES = ou=MailAliases,dc=ddc-consult,dc=de
+
+     local_interfaces = 0.0.0.0.25 : 127.0.0.1.10025
+
+    begin router
+
+    amavis:
+	driver = manualroute
+	condition = ${if eq{interface_port}{10025} {0}{1}}
+	route_list = * localhost byname
+	transport = amavis
+	self = send
+	no_verify
+
+    # Ein Router der so wie die alten Aliase funktioniert
+    ldap_aliases:
+	driver = redirect
+	data = ${lookup ldap{ldap:///cn=${quote_ldap:$local_part},LDAP_ALIASES?rfc822MailMember?base?}}
+     
+     ...
+     # Ein Router, der die Alternativen Namen der
+     # Nutzer findet
+     alternative_names:
+         driver = redirect
+	 data = ${lookup ldap{ldap:///LDAP_USERS?uid?sub?(mail=${quote_ldap:$local_part@$domain})}}
+
+
+     # Ein Router, der dann wirklich nur an Cyrus übergibt, wenn LDAP
+     # vermuten läßt, dass das funktionieren wird.
+     # nach den local users
+     cyrus:
+	driver = accept
+	condition = ${lookup ldapdn{ldap:///uid=${quote_ldap:$local_part},LDAP_USERS??base?}}
+	transport = cyrdeliver
+
+
+
+__SpamAssassin
+
+  Das PID-File geht nicht zu schreiben, wenn er als Nutzer spamd
+  läuft -> /etc/default/spamassassin
+  (dort auch gleich noch 'export LANG=de_DE' eintragen, damit die
+  Meldungen eventuell in Deutsch kommen)
+
+
+# $Id$
+# $URL$
+# vim:sts=2 sw=2 aw ai sm fo+=n tw=72:
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/account.pm	Fri Nov 04 06:29:26 2005 +0000
@@ -0,0 +1,415 @@
+package account;
+# © Heiko Schlittermann
+# $Id$
+# $URL$
+
+use strict;
+use warnings;
+use Net::LDAP;
+use Net::LDAP::Constant qw(LDAP_ALREADY_EXISTS LDAP_NO_SUCH_OBJECT LDAP_TYPE_OR_VALUE_EXISTS);
+use Net::LDAP::Entry;
+use Cyrus::IMAP::Admin;
+use Text::Wrap;
+use password;
+
+
+my $Cf;
+my ($ldap, $ubase, $abase);
+my ($imap);
+END { $imap and $imap = undef; };
+
+
+sub _add();
+sub _list();
+sub _delete();
+sub _mkpw($);
+sub uniq(@);
+sub verbose(@);
+
+sub OU_ACCOUNTS();
+sub OU_ALIASES();
+sub AT_PRIMARYADDRESS();
+sub OC_RECIPIENT();
+sub AT_ADDRESS();
+sub AT_GROUP();
+sub AT_FORWARDINGADDRESS();
+
+sub import(@) {
+    $Cf = shift;
+
+    require constant;
+    import constant OU_ACCOUNTS => $Cf->ldap_ou_accounts;
+    import constant OU_ALIASES => $Cf->ldap_ou_aliases;
+    import constant OC_RECIPIENT => $Cf->ldap_oc_recipient;
+    import constant AT_PRIMARYADDRESS => $Cf->ldap_at_primaryaddress;
+    import constant AT_ADDRESS => $Cf->ldap_at_address;
+    import constant AT_GROUP => $Cf->ldap_at_group;
+    import constant AT_FORWARDINGADDRESS => $Cf->ldap_at_forwardingaddress;
+
+    $ubase = OU_ACCOUNTS . "," . $Cf->ldap_base;
+    $abase = OU_ALIASES . "," . $Cf->ldap_base;
+}
+
+sub run($) {
+    # Eigentlich brauchen wir für alles imap und ldap
+    $ldap = new Net::LDAP $Cf->ldap_server or die;
+    my $r = $ldap->bind($Cf->ldap_bind_dn,
+	password => $Cf->ldap_password || $ENV{LDAP_PASS} || password::ask("LDAP (". $Cf->ldap_bind_dn .") password: "));
+    die $r->error, "\n" if $r->code;
+
+    $imap = new Cyrus::IMAP::Admin or die $@;
+    $imap->authenticate(-server => $Cf->imap_server, -user => $Cf->imap_admin, 
+	-password => $Cf->imap_password || $ENV{IMAP_PASS} || password::ask("IMAP (". $Cf->imap_admin .") password: "))
+    or die $@;
+
+
+    if ($Cf->list) { _list() }
+    elsif ($Cf->add) { _add() }
+    elsif ($Cf->delete) { _delete() }
+    elsif ($Cf->modify) { _modify() }
+    else { die "Need action (--add|--modify|--list|--delete)\n" };
+
+}
+
+sub _add() {
+# Beim Hinzufügen tragen wir nur das unbedingt notwendige
+# ein.  Wenn es schon eine mailPrimaryAddress gibt oder eine
+# mail, machen wir gar nichts.
+# Ansonsten:
+# uid wird hinzugefügt
+# cn, sn bleiben unangetastet
+# Wenn die mailbox-Option gesetzt ist, wird die
+# IMAP-Mailbox angelegt.
+
+
+    die "Need user name for creation\n" if not @ARGV;
+    my $user = shift @ARGV;
+    my $mbox = "user/$user";
+    my $cn = $Cf->fullname || $user;
+    my $sn = (reverse split " ", $cn)[0];
+    my $mailPrimaryAddress = $Cf->primary || $user;
+    my $mailAddress = [$user, split /,/, $Cf->other || ""];
+    my $mailGroup = [split /,/, $Cf->group || ""];
+    my $pw = _mkpw($Cf->password || "{pwgen}");
+
+
+    my $dn = "uid=$user,$ubase";
+    my $r;
+
+    verbose("$user:\n");
+
+    verbose("\t$dn...");
+
+    $r = $ldap->search(base => $ubase, filter => "(uid=$user)");
+    die $r->error if $r->code;
+    die "Multiple entries not expected" if $r->count > 1;
+
+    my $e;
+    if ($r->count) {
+	$e = $r->shift_entry;
+    } else {
+	$e = new Net::LDAP::Entry;
+	$e->dn($dn);
+    }
+
+    if ($e->exists("mail") || $e->exists(AT_PRIMARYADDRESS) || $e->exists("userPassword")) {
+	verbose "exists\n";
+    } else {
+
+	# Bevor wir ans Werk gehen, noch ein paar Tests (mailPrimaryAddress und mail darf
+	# darf noch nicht vergeben sein)
+	foreach my $a ($mailPrimaryAddress, @$mailAddress) {
+	    $a =~ s/!$// and next;   # wenn ein ! am Ende steht, dann ist es so gewollt und wird
+				    # nicht geprüft
+	    $r = $ldap->search(filter => "(mail=$a)", base => $ubase);
+	    die $r->error if $r->code;
+	    die "$a ist schon vergeben\n" if $r->count;
+	}
+
+	$e->replace(objectClass => [uniq $e->get("objectClass"), qw/uidObject person/, OC_RECIPIENT]);
+	$e->replace(uid => [uniq $e->get("uid"), $user]);
+
+	$e->add((AT_ADDRESS) => $mailAddress);
+	$e->add((AT_PRIMARYADDRESS) => $mailPrimaryAddress);
+	$e->add(userPassword => $pw);
+	$e->add((AT_GROUP) => $mailGroup) if @$mailGroup;
+	# $e->add(iusRestrictedMail => $Cf->internal) if $Cf->internal ne ":";
+
+	$e->exists("sn") or $e->add(sn => $sn);
+	$e->exists("cn") or $e->add(cn => $cn);
+
+
+	$r = $e->update($ldap);
+	die $r->error if $r->code;
+
+	verbose("ok");
+	verbose(" Password: $pw") if not $Cf->password or $Cf->password eq "{pwgen}";
+    }
+
+    if($Cf->mbox) {
+	verbose("\n\t$mbox...");
+
+	if ($imap->list($mbox)) { verbose("exists") }
+	else {
+	    $imap->create($mbox) and verbose("ok") or die $@;
+	    $imap->setacl($mbox, $Cf->imap_admin => "lrswipcda") or die $@;
+	    $imap->setquota($mbox, STORAGE => 1024 * $Cf->imap_quota) or die $@;
+	}
+    }
+
+
+    verbose("\n");
+}
+
+sub _modify() {
+# Auch hier gehen wir davon aus, daß die dn direkt aus dem User-Namen folgt:
+# dn: uid=USER,...
+    my (@users) = @ARGV or die "Need username(s)\n";
+    my @dns;
+
+    my $r = $ldap->search(base => $ubase, 
+	filter => "(|" . join("", map { "(uid=$_)" } @ARGV) . ")");
+    die $r->error if $r->code;
+    die "No entries found.\n" if $r->count == 0;
+
+    while (my $e = $r->shift_entry) {
+	my $r;
+
+	my $user = $e->get_value("uid");
+	my $dn = $e->dn;
+	my $mbox = "user/$user";
+
+	my $modified = 0;
+	verbose "$user:";
+
+	verbose "\n\t$dn...";
+
+	# Fix: iusMailOptions wurde erst später eingeführt, bei Bedarf also hinzufügen
+	#if (!grep /^iusMailOptions$/, @{$e->get("objectClass")}) {
+	    #$e->add(objectClass => "iusMailOptions");
+	#}
+
+	if (my $cn = $Cf->fullname) {
+	    # Aus dem Fullnamen leiten wir cn und sn ab.
+	    my $sn = (reverse split " ", $cn)[0];
+
+	    if ($cn =~ s/^\+//) {
+		$e->replace(
+		    cn => [uniq $e->get("cn"), $cn], 
+		    sn => [uniq $e->get("sn"), $sn]);
+	    } elsif ($cn =~ s/^-//) {
+		$e->delete(cn => [$cn], sn => [$sn]);
+	    } else { $e->replace(cn => $cn, sn => $sn); }
+	    $modified++;
+	}
+
+	if (defined $Cf->other) {
+	    my @o = split /,/, $Cf->other;
+	    grep { /^[+-]/ } @o or $e->delete(AT_ADDRESS);
+
+	    foreach my $a (split /,/, $Cf->other) {
+		if ($a =~ s/^-//) { 
+		    $e->delete((AT_ADDRESS) => [$a]) 
+		} else {
+		    $a =~ s/^\+//;
+
+		    # Darf noch nicht woanders sein
+		    $r = $ldap->search(base => $ubase, filter => "(mail=$a)");
+		    die $r->error if $r->code;
+		    die "$a ist schon vergeben\n" if $r->count;
+
+		    $e->add((AT_ADDRESS) => [$a]) 
+		}
+	    }
+	    $modified++;
+	}
+
+	if (defined $Cf->group) {
+	    my @g = split /,/, $Cf->group;
+	    grep { /^[+-]/ } @g or $e->delete(AT_GROUP);
+
+	    foreach my $g (@g) {
+		if ($g =~ s/^-//) {
+		    $e->delete((AT_GROUP) => [$g])
+		} else {
+		    $g =~ s/^\+//;
+		    $e->add((AT_GROUP) => [$g])
+		}
+	    }
+	    $modified++;
+	}
+
+	if (my $a = $Cf->primary) {
+	    $r = $ldap->search(base => $ubase, 
+		# filter => "(|(mailPrimaryAddress=$a)(mail=$a))");
+		filter => "(mail=$a)");
+	    die $r->error if $r->code;
+	    die "$a ist schon vergeben\n" if $r->count;
+    
+	    $e->replace((AT_PRIMARYADDRESS) => $Cf->primary);
+	    $modified++;
+	}
+
+	if (my $pw = _mkpw($Cf->password)) {
+	    $e->replace(userPassword => $pw);
+	    $modified++;
+	}
+
+	#if ($Cf->internal ne ":") {
+	    #$e->replace(iusRestrictedMail => $Cf->internal ? "TRUE" : "FALSE");
+	    #$modified++;
+	#}
+
+	$e->dump if $Cf->debug;
+
+	if ($modified) {
+	    $r = $e->update($ldap);
+	    die $r->error.$r->code if $r->code;
+	}
+
+	# FIXME: Wenn keine Mailbox existiert, gibt es hier ein Problem
+	if (defined $Cf->imap_quota) {
+	    $imap->setquota($mbox, STORAGE => $Cf->imap_quota * 1024)
+	    or die $@;
+	}
+
+	verbose "ok\n";
+
+	print "\n";
+    }
+}
+
+sub _delete() {
+# Wir gehen davon aus, daß es einen dn uid=USER,ou=.... gibt, den wir löschen können.
+# Wir löschen den kompletten Container.  Es kann natürlich sein, daß er noch jemand anders gehört.  
+# Dann ist das Pech.  Um es besser zu haben, müßten wir für alles unsere eigenen
+# Objektklassen haben...
+
+    if (!@ARGV) {
+	print "User: ";
+	chomp($_ = <>);
+	@ARGV = ($_);
+    }
+
+
+    foreach (@ARGV) {
+	my $user = $_;
+	my $dn = "uid=$user,$ubase";
+	my $mbox = "user/$user";
+
+	verbose("$user:\n");
+
+	# Nachsehen, ob es noch aliase gibt, in denen dieser Nutzer steht:
+	my $r = $ldap->search(base => $abase,
+	    filter => "(".AT_FORWARDINGADDRESS."=$_)",
+	    attrs => ["mail", AT_FORWARDINGADDRESS]);
+	while (my $e = $r->shift_entry) {
+	    verbose("\tdeleting $user from alias ".$e->get_value("mail")."...");
+	    $e->delete((AT_FORWARDINGADDRESS) => [$user]);
+
+	    my $r = $e->update($ldap);
+	    if ($r->code == 0) { verbose("ok\n") } 
+	    else { die $r->error }
+	}
+
+	verbose("\tdeleting $dn...");
+	$r = $ldap->delete($dn);
+
+	if ($r->code == LDAP_NO_SUCH_OBJECT) {
+	    verbose("doesn't exist");
+	} elsif ($r->code == 0) {
+	    verbose("ok");
+	} else {
+	    die $r->error;
+	}
+	verbose("\n");
+	
+	if ($Cf->mbox) {
+	    verbose("\tdeleting mbox $mbox...");
+	    $imap->delete($mbox) and verbose("ok")
+	    or verbose($imap->error);
+	}
+
+	verbose("\n");
+
+    }
+}
+
+sub _list() {
+    my $filter;
+    @ARGV = ("*") unless @ARGV;
+    $filter = "(|" . join("", map { "(uid=$_)" } @ARGV) . ")";
+
+    my $r = $ldap->search(
+	filter => $filter,
+	base => $ubase,
+	attrs => [qw/uid cn mail userPassword/]
+    );
+    die $r->error if $r->code;
+
+
+    while (my $e = $r->shift_entry) {
+	my $uid = $e->get_value("uid");
+	my $cn = join(", ", $e->get_value("cn"));
+	my $mr = $e->get_value(AT_PRIMARYADDRESS) || "??";
+	my $ml = join(", ", $e->get_value(AT_ADDRESS)) || "??";
+	my $mg = join(", ", $e->get_value(AT_GROUP)) || "??";
+	my $mbox = "user/$uid";
+
+	print "$uid: $cn <$mr>";
+
+	#if (($e->get_value("iusRestrictedMail")||"") eq "TRUE") {
+	    #print " INTERNAL";
+	#}
+
+	MBOX: {
+	    if (!$imap->list($mbox)) {
+		print ", no mbox";
+		last MBOX;
+	    }
+	    print ", mbox";
+	    my %q = $imap->listquota($mbox);
+	    my ($used, $max) = map { int($_ / 1024) } @{$q{STORAGE}};
+
+	    if (!$max) {
+		print ", no quota";
+		last MBOX;
+	    }
+	    print ", quota ($used/$max): " . int(100 * $used/$max) . "%";
+	}
+	print "\n";
+
+	print "\tPassword: ", $> == 0 ? $e->get_value("userPassword") : "*", "\n";
+	
+	print  wrap("\t", "\t\t", "Local Adresses: $ml\n") if $ml;
+	print wrap("\t", "\t\t", "Mail Groups: $mg\n") if $mg;
+
+    }
+}
+
+sub verbose(@) {
+    printf STDERR @_;
+}
+
+sub uniq(@) {
+    my %x;
+    @x{@_} = ();
+    return keys %x;
+}
+
+{   my @pw;
+sub _mkpw($) {
+    my $in = $_[0];
+
+    return $in unless $in and $in eq "{pwgen}";
+
+    if (!@pw) {
+	chomp(@pw = `pwgen 8 10 2>/dev/null|| mkpasswd`);
+	die "pwgen/mkpasswd: $!" if $?;
+    }
+    return shift @pw;
+    
+} }
+
+1;
+# vim:sts=4 sw=4 aw ai sm nohlsearch:
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/alias.pm	Fri Nov 04 06:29:26 2005 +0000
@@ -0,0 +1,215 @@
+package alias;
+# © Heiko Schlittermann
+# $Id$
+# $URL$
+
+use strict;
+use warnings;
+use Net::LDAP;
+use Net::LDAP::Constant qw(
+    LDAP_ALREADY_EXISTS 
+    LDAP_NO_SUCH_OBJECT
+    LDAP_NO_SUCH_ATTRIBUTE
+    LDAP_TYPE_OR_VALUE_EXISTS);
+use Net::LDAP::Entry;
+use Text::Wrap;
+
+use password;
+
+my $Cf;
+my ($ldap, $abase);
+
+sub _add();
+sub _list();
+sub _delete();
+sub uniq(@);
+sub verbose(@);
+sub columns();
+
+sub OU_ALIASES();
+sub OC_ALIAS();
+sub AT_FORWARDINGADDRESS();
+
+sub import(@) {
+    $Cf = shift;
+
+    require constant;
+    import constant OU_ALIASES => $Cf->ldap_ou_aliases;
+    import constant OC_ALIAS => $Cf->ldap_oc_alias;
+    import constant AT_FORWARDINGADDRESS => $Cf->ldap_at_forwardingaddress;
+
+    $abase = OU_ALIASES . "," . $Cf->ldap_base;
+}
+
+sub run($) {
+    # Eigentlich brauchen wir für alles ldap
+    $ldap = new Net::LDAP $Cf->ldap_server or die;
+    my $r = $ldap->bind($Cf->ldap_bind_dn,
+	password => $Cf->ldap_password || $ENV{LDAP_PASS} || password::ask("LDAP (". $Cf->ldap_bind_dn .") password: "));
+    die $r->error, "\n" if $r->code;
+
+
+    if ($Cf->list) { _list() }
+    elsif ($Cf->add) { _add() }
+    elsif ($Cf->delete) { _delete() }
+    elsif ($Cf->modify) { _modify() }
+    else { die "Need action (--add|--modify|--list|--delete)\n" };
+
+}
+
+sub _add() {
+# Wenn's den Alias schon gibt, wird er nicht mehr
+# angelegt
+
+    die "Need alias name for creation\n" if not @ARGV;
+    die "Need members\n" if not defined $Cf->members;
+    my $alias = shift @ARGV;
+    my @members = split /,/, $Cf->members;
+    my $dn = "mail=$alias,$abase";
+
+    my $r;
+
+    verbose("$alias:\n");
+    verbose("\t$dn...");
+
+    $r = $ldap->search(base => $abase, filter => "(mail=$alias)");
+    die $r->error if $r->code;
+    die "Multiple entries not expected" if $r->count > 1;
+    
+    $r = $ldap->add($dn, attrs => [
+	objectClass => OC_ALIAS,
+	mail => $alias,
+	(AT_FORWARDINGADDRESS) => \@members
+    ]);
+    if ($r->code == LDAP_ALREADY_EXISTS) { verbose "exists" }
+    elsif ($r->code) { die $r->error } 
+    else { verbose "ok" }
+
+    verbose("\n");
+}
+
+sub _modify() {
+# Auch hier gehen wir davon aus, daß die dn direkt aus dem Alias-Namen folgt:
+# dn: cn=USER,...
+# Jetzt behandeln wir lediglich die Modifikation auf Basis eines
+# alias-Namens!
+
+    my (@users) = @ARGV or die "Need alias names(s)\n";
+    my @members = split /,/, $Cf->members;
+    my @add = grep { s/^\+// } @_ = @members;
+    my @del = grep { s/^-// } @_ = @members;
+    my @set = grep { !/^[\+-]/ } @members;
+
+
+    foreach my $alias (@ARGV) {
+	my $dn = "mail=$alias,$abase";
+	verbose "$alias:";
+
+	my $r = $ldap->search(base => $abase, filter => "(mail=$alias)");
+	die $r->error if $r->code;
+
+	if ($r->count == 0) {
+	    verbose " not found\n";
+	    next;
+	}
+
+	while (my $e = $r->shift_entry) {
+
+	    verbose "\n\t" . $e->dn . " ";
+
+	    if (@set) {
+		$e->replace((AT_FORWARDINGADDRESS) => \@set);
+	    } else {
+		@add and $e->replace((AT_FORWARDINGADDRESS) => [uniq $e->get(AT_FORWARDINGADDRESS), @add]);
+		@del and $e->delete((AT_FORWARDINGADDRESS) => \@del);
+	    }
+
+	    $e->dump if $Cf->debug;
+
+	    my $r = $e->update($ldap);
+	    if ($r->code == LDAP_NO_SUCH_ATTRIBUTE) {
+		verbose "no member";
+	    } elsif ($r->code) {
+		die $r->error . "/" . $r->code;
+	    }  else {
+		verbose "ok";
+	    }
+	}
+
+	print "\n";
+    }
+}
+
+sub _delete() {
+# Wir gehen davon aus, daß es einen dn mail=ALIAS,ou=MailAliases,...
+# gibt und löschen diesen gnadenlos.
+
+    if (!@ARGV) {
+	print "User: ";
+	chomp($_ = <>);
+	@ARGV = ($_);
+    }
+
+    foreach (@ARGV) {
+	my $dn = "mail=$_,$abase";
+
+	verbose("$_:\n");
+	verbose("\tdeleting $dn...");
+	my $r = $ldap->delete($dn);
+
+	if ($r->code == LDAP_NO_SUCH_OBJECT) {
+	    verbose("doesn't exist");
+	} elsif ($r->code == 0) {
+	    verbose("ok");
+	} else {
+	    die $r->error;
+	}
+	
+	verbose("\n");
+
+    }
+}
+
+sub _list() {
+    my $filter;
+    @ARGV = ("*") unless @ARGV;
+    $filter = "(|" . join("", map { "(mail=$_)" } @ARGV) . ")";
+
+    my $r = $ldap->search(
+	filter => $filter,
+	base => $abase,
+	attrs => [qw/mail/, AT_FORWARDINGADDRESS],
+    );
+
+    die $r->error if $r->code;
+
+    $Text::Wrap::columns = columns() || 80;
+
+    while (my $e = $r->shift_entry) {
+	my $mail = $e->get("mail");
+
+	print wrap("", "\t", $e->get_value("mail") 
+	    . ": "
+	    . join(", ", $e->get(AT_FORWARDINGADDRESS))
+	    . "\n");
+	    
+    }
+}
+
+sub verbose(@) {
+    printf STDERR @_;
+}
+
+sub uniq(@) {
+    my %x;
+    @x{@_} = ();
+    return keys %x;
+}
+
+sub columns() {
+    `stty -a` =~ /columns\s+(\d+)/;
+    $1;
+}
+
+1;
+# vim:sts=4 sw=4 aw ai sm:
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/im	Fri Nov 04 06:29:26 2005 +0000
@@ -0,0 +1,53 @@
+#! /usr/bin/perl
+# © 2005 Heiko Schlittermann
+# $Id$
+# $URL$
+
+# small template for action on all imap folders
+# or simlar quatsch
+
+use strict;
+use warnings;
+use AppConfig;
+
+use Mail::IMAPClient;
+use Cyrus::IMAP::Admin;
+
+use constant QUOTA => 200000;	# kiloByte
+
+use constant CONFIG => (
+	{ CASE => 1 },
+	quota => { DEFAULT => 200000, ARGS => "=i" },
+	force => { DEFAULT => undef, ARGS => "!" },
+);
+
+my $Cf = new AppConfig CONFIG;
+   $Cf->getopt(\@ARGV);
+
+my $imap = new Cyrus::IMAP::Admin("localhost") or die;
+# $imap->authenticate(-user => "root", -password => "hallo") or die;
+$imap->authenticate(-user => "root") or die;
+
+foreach ($imap->list("user/*")) {
+	my ($mb, $att, $sep) = @$_;
+
+	next unless $mb =~ /^user\/[^\/]+$/;
+	print "$mb";
+
+	my ($qroot, $quota) = $imap->listquotaroot($mb);
+	if ($qroot && $qroot ne $mb) { 
+		print " QuotaRoot; $qroot\n";
+		next;
+	}
+
+	my %q = $imap->listquota($mb);
+	if (exists $q{STORAGE} and !$Cf->force) {
+		my ($used, $max) = @{$q{STORAGE}};
+		print " Quota $used/$max " . int($used/$max*100) . "%";
+	} else {
+		$imap->setquota($mb, "STORAGE" => $Cf->quota) or die;
+	}
+
+	print "\n";
+	
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/imap.pm	Fri Nov 04 06:29:26 2005 +0000
@@ -0,0 +1,44 @@
+
+sub createMbox($) {
+    my $user = shift;
+    my $mbox = "user/$user";
+
+    verbose(" imap:");
+    my $imap = connectImap();
+
+    $imap->setacl($mbox, $Cf->imap_admin => "lrswipcda");
+    if ($imap->list($mbox)) {
+	verbose("(exists)");
+    } else {
+	$imap->create($mbox) or die ":$@: $mbox\n";
+	$imap->setquota($mbox, STORAGE => 1024 * $Cf->imap_quota) or die ":$@: $mbox\n";
+    }
+    verbose("ok");
+
+}
+
+sub removeMbox($) {
+    my $user = shift;
+    my $mbox = "user/$user";
+    verbose(" imap:");
+    my $imap = connectImap();
+
+    $imap->setacl($mbox, $Cf->imap_admin, "rc");
+
+    if (not $imap->exists($mbox)) {
+	verbose("does not exist");
+    } else {
+	$imap->delete($mbox) or die "$@";
+    }
+    verbose("ok");
+}
+
+
+sub connectImap() {
+    my $imap = new Cyrus::IMAP::Admin($Cf->imap_server) or die "$@";
+    $imap->authenticate(-user => $Cf->imap_admin,
+	-password => $ENV{IMAP_PASS} || askPass("IMAP (" . $Cf->imap_admin .") password: "));
+
+    return $imap;
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/injectMail	Fri Nov 04 06:29:26 2005 +0000
@@ -0,0 +1,90 @@
+#! /usr/bin/perl
+# $Id$
+# $URL$
+my $USAGE = <<'#';
+{{ME}} [options] --user={USER} [mboxfile]
+    --domain=s  default domain for mailbox id [{{$Cf->domain}}]
+
+    Version: $Id$
+#
+
+use strict;
+use warnings;
+
+use Mail::IMAPClient;
+use File::Basename;
+use AppConfig;
+
+use constant CONFIG => (
+    { CASE => 1 },
+    domain => { ARGS => "=s", DEFAULT => "ddc-consult.de" },
+    user => { ARGS => "=s", DEFAULT => undef },
+    help => { ARGS => "!", DEFAULT => undef },
+);
+
+use constant ME => basename $0;
+
+use constant IMAP_SERVER => "localhost";
+use constant IMAP_ADMIN => "root";
+use constant IMAP_PASS => $ENV{IMAP_PASS};
+
+sub askPass($);
+
+my $Cf = new AppConfig CONFIG or die;
+   $Cf->getopt(\@ARGV) or die;
+
+HELP: {
+    last unless $Cf->help;
+    $USAGE =~ s/{{(.*?)}}/eval $1/eg;
+    print $USAGE;
+    exit;
+}
+
+MAIN: {
+    $Cf->user or die "Need user...";
+    my $mbox = $Cf->user;
+    $mbox .= "@" . $Cf->domain unless $mbox =~ /@/;
+    $mbox = "user/$mbox";
+
+
+    my $imap = new Mail::IMAPClient 
+	Server => IMAP_SERVER,
+	User => IMAP_ADMIN,
+	Password => IMAP_PASS || askPass("IMAP (". IMAP_ADMIN .") password: ")
+    or die "Can't connect: $@";
+
+    $imap->setacl($mbox, IMAP_ADMIN, "lrswipcda") or die "$@: $mbox";
+    $imap->select($mbox) or die "$@: $mbox";
+
+    {
+	my $message;
+	while (<>) {
+	    if (/^From / or eof) {
+		if ($message) {
+		    print "Appending message (" . length($message) . ") bytes\n";
+		    $imap->append($mbox, $message) or die "$@";
+		    $message = "";
+		}
+	    } else {
+		$message .= $_;
+	    }
+	}
+    }
+    
+}
+
+sub askPass($) {
+    local $| = 1;
+    return undef if not -t;
+
+    print $_[0];
+    system(stty => "-echo");
+    my $ans = <STDIN>;
+    system(stty => "echo");
+    print "\n";
+
+    chomp $ans;
+    return $ans;
+}
+
+# vim:sts=4 sw=4 aw ai sm:
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ldap.schema	Fri Nov 04 06:29:26 2005 +0000
@@ -0,0 +1,27 @@
+
+# Output (to LDAP-Server)
+# Normaler Mail-Account:
+# ,---
+# |dn: uid=hh,ou=[MailAccounts],...
+# |objectClass: person
+# |objectClass: uidObject
+# |objectClass: [mailRecipient]
+# |[mailPrimaryAddress]: hans.hanson 	(from --address or the user) 
+# |mail: h.hanson	  		(from --alias)
+# |mail: hanson	  			(from --alias)
+# |mailGroup: edv, postmaster
+# |uid: hh
+# |userPassword: {crypt}*
+# |sn: Hanson
+# |cn: Hans Hanson
+# `---
+
+# ,---
+# |dn: mail=root,ou=[MailAliases],...
+# |objectClass: [MailAlias]
+# |mail: root
+# |[MailForwardingAddress]: rudi
+# |[MailForwardingAddress]: hans
+# `---
+
+Die [..] sind konfigurierbar.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ldapBase.pm	Fri Nov 04 06:29:26 2005 +0000
@@ -0,0 +1,21 @@
+package ldapBase;
+# © Heiko Schlittermann
+# $Id$
+# $URL$
+
+use strict;
+use warnings;
+use Exporter();
+our @ISA = qw/Exporter/;
+our @EXPORT = qw/&ldapBase/;
+
+
+sub ldapBase(@) { 
+    no warnings 'once'; 
+    local @ARGV = grep { -f } @_; 
+    die "Can't find ldap.conf (searched @_)\n" if !@ARGV;
+    my $r = (reverse grep { /^\s*BASE\s+(.*?)\s*$/ and $_ = $1 } <>)[0];
+    return $r;
+};
+
+# vim:sts=4 sw=4 aw ai sm:
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ma	Fri Nov 04 06:29:26 2005 +0000
@@ -0,0 +1,163 @@
+#! /usr/bin/perl
+# Usage:
+# © 2005 Heiko Schlittermann <hs@schlittermann.de>
+# $URL$
+# $Id$
+#
+use constant USAGE => <<'#';
+Usage: !ME! account|alias --add|--list|--modify|--delete [options] [user|alias]
+       * common options *
+       --ldap_server=s	LDAP-Server	[!$Cf->ldap_server!]
+       --ldap_base=s	LDAP-Basis	[!$Cf->ldap_base!]
+       --ldap_admin=s	LDAP BIND DN	[!$Cf->ldap_admin!]
+       --ldap_password=s		[!$Cf->ldap_password!]
+
+       --imap_server=s  IMAP Server	[!$Cf->imap_server!]
+       --imap_admin=s   IMAP Server	[!$Cf->imap_admin!]
+       --imap_password=s		[!$Cf->imap_password!]
+
+       * account options *
+       --[no]mbox	Create MBox	[!$Cf->mbox!]
+       --imap_quota=i	Mail Quota (MB) [!$Cf->imap_quota!]
+       --address=s	Primary Mail	[!$Cf->address!]
+       --other:s	Alternative Mail addresses
+		        (comma sep.)    [!$Cf->other!]
+       --group:s	Mail Group(s) this account is member of
+			(comma sep.)	[!$Cf->group!]
+       --fullname=s	Real Name	[!$Cf->fullname!]
+       --password=s	Passwort	[!$Cf->password!]
+
+       * alias options *
+       --members=s	List of Members	[!$Cf->members!]
+
+Passwords for LDAP and IMAP can be read from environment LDAP_PASS resp. IMAP_PASS.
+Options can be read from config file named in $MA_CONF [!$ENV{MA_CONF}!].
+
+$Id$
+$URL$
+© 2005 Heiko Schlittermann <hs@schlittermann.de>
+
+#
+
+use strict;
+use warnings;
+
+use IO::File;
+use Cyrus::IMAP::Admin;
+use AppConfig qw(:expand);
+use File::Basename;
+use Carp;
+
+use lib qw(. /usr/local/lib/ma);
+use ldapBase;
+
+use constant ME => basename $0;
+use constant CONFIG => (
+    { CASE => 1 },
+    GLOBAL => { DEFAULT => undef },
+
+    # * common *
+    add =>	    { ARGS => "!",  ALIAS => [qw/new create/] },
+    list =>	    { ARGS => "!",  ALIAS => "ls" },
+    modify =>	    { ARGS => "!",  ALIAS => "change" },
+    delete =>	    { ARGS => "!",  ALIAS => "remove" },
+
+    ldap_base =>    { ARGS => "=s", DEFAULT => ldapBase(qw(/etc/openldap/ldap.conf /etc/ldap/ldap.conf)) },
+    ldap_server =>  { ARGS => "=s", DEFAULT => "localhost" },
+    ldap_bind_dn => { ARGS => "=s", DEFAULT => "cn=admin", ALIAS => "ldap_admin" },
+    ldap_password =>{ ARGS => "=s" },
+
+    help =>	    { ARGS => "!" },
+    debug =>	    { ARGS => "!" },
+
+
+    # * account *
+    imap_server =>  { ARGS => "=s", DEFAULT => "localhost" },
+    imap_admin =>   { ARGS => "=s", DEFAULT => $ENV{USER} },
+    imap_password =>{ ARGS => "=s" },
+    imap_quota =>   { ARGS => "=i", DEFAULT => 300, ALIAS => "quota" },
+
+    mbox =>	    { ARGS => "!",  DEFAULT => 1 },
+    password =>	    { ARGS => "=s" },
+#   internal =>	    { ARGS => "!",  DEFAULT => ":", ALIAS => "restricted" },	    
+
+    other =>	    { ARGS => ":s" },
+    group =>	    { ARGS => ":s" },
+    fullname =>	    { ARGS => "=s", ALIAS => "realname" },
+    address =>	    { ARGS => "=s", ALIAS => "primary" },
+
+    # * alias *
+    members =>	    { ARGS => ":s" },
+
+    # * ldap intern *
+    ldap_ou_aliases =>	    { ARGS => "=s", DEFAULT => "ou=MailAliases" },
+    ldap_ou_accounts =>	    { ARGS => "=s", DEFAULT => "ou=MailAccounts" },
+
+    ldap_oc_alias =>	    { ARGS => "=s", DEFAULT => "XXXmailAlias" },
+    ldap_oc_recipient =>    { ARGS => "=s", DEFAULT => "XXXmailRecipient" },
+
+    ldap_at_address =>	    { ARGS => "=s", DEFAULT => "XXXmailAddress" },
+    ldap_at_group =>	    { ARGS => "=s", DEFAULT => "XXXmailGroup" },
+    ldap_at_forwardingaddress =>
+			    { ARGS => "=s", DEFAULT => "XXXmailForwardingAddress" },
+    ldap_at_primaryaddress => 
+			    { ARGS => "=s", DEFAULT => "XXXmailPrimaryAddress" },
+
+);
+our $Cf;
+
+sub help();
+
+my $Module = shift if @ARGV && $ARGV[0] !~ /^-/;
+   $Module ||= "UNKNOWN";
+
+
+$SIG{__DIE__} = sub { die "\n".ME.": ", @_ };
+
+
+MAIN: {
+
+    $Cf = new AppConfig CONFIG or die;
+
+    if (exists $ENV{MA_CONF} and -f $ENV{MA_CONF}) {
+	my $f = $ENV{MA_CONF};
+	die ": $f is group/world readable/writeable\n" if  077 & (stat _)[2];
+	$Cf->file($f) or die;
+    }
+    $Cf->getopt(\@ARGV) or die "Bad Usage.  Try --help.\n";
+
+    die "Need ldap base.\n" if not $Cf->ldap_base;
+    if ($Cf->ldap_admin !~ /\Q$Cf->ldap_base/) {
+	$Cf->ldap_admin($Cf->ldap_admin . "," . $Cf->ldap_base);
+    }
+
+    print help() and exit 0 if $Cf->help;
+
+    @_ = grep { $_ =~ /^\Q$Module\E/ } qw/account alias/;
+    die "Need module.  Try --help\n" if @_ == 0;
+    die "Module ambigous. (@_)\n" if @_ > 1;
+
+    if ($_[0] eq 'account') {
+	require account;
+	account::import($Cf);
+	account::run();
+    } elsif ($_[0] eq 'alias') {
+	require alias;
+	alias::import($Cf);
+	alias::run();
+    } else {
+	die "Shit";
+    }
+
+}
+
+sub verbose(@) {
+    print STDERR @_;
+}
+
+sub help() {
+    ($_ = USAGE) =~ s/!(.*?)!/(eval $1) || ""/eg;
+    return $_;
+}
+
+# vim:sts=4 sw=4 aw ai sm nohlsearch incsearch:
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ma.8.pod	Fri Nov 04 06:29:26 2005 +0000
@@ -0,0 +1,164 @@
+=head1 NAME
+
+ma -- mailadmin tool
+
+=head1 SYNOPSIS
+
+B<ma> account [--add|--modify|--delete|--list] 
+[--address=s]
+[--fullname=s]
+[--group=s]
+[--[no]mbox]
+[--other=s]
+[--password=s]
+I<name>
+
+B<ma> alias  [--add|--modify|--delete|--list] [--members=s] I<name>
+
+B<ma> --help
+
+
+=head1 DESCRIPTION
+
+B<ma> ist ein Tool zur Administration vom Mailnutzern, deren Einstellungen im LDAP
+gespeichert sind.
+
+Es gliedert sich in zwei Sub-Kommands: C<account> und C<alias>.
+
+=head2 account
+
+Mit diesem Subkommando werden die Details des Accounts (Nutzer) modifiziert.
+Das sind vor allem Dinge wie 
+
+=over 4
+
+=item	. Name
+
+=item 	. Password
+
+=item	. Gruppenzugehörigkeit
+
+=item	. alternative Mailadressen
+
+=back
+
+Beeinflußt wird sowohl das LDAP als auch der Cyrus-Mailserver.  Im LDAP werden die Nutzerspezifischen 
+Daten gehalten, im Cyrus die Mailboxen.  Bei LDAP-Eintragungen wird davon ausgegangen, daß
+keine anderen Elemente in den entsprechenden Subbäumen liegen.  Besonders beim Löschen ist das zu berücksichtigen,
+denn z.B. das Entfernen eines Nutzer entfernt dessen B<gesamtes> LDAP-Objekt!
+
+=head1 OPTIONS
+
+=over 4
+
+=head2 OPTIONS für beide Sub-Kommandos
+
+=item B<--add>|B<--modify>|B<--delete>|B<--list>
+
+Ist eigentlich selbsterklärend :)
+
+=back
+
+=head2 OPTIONS für C<account>
+
+=over 4
+
+=item B<--address>=I<primäre Mailadresse>
+
+Die primäre Mailadresse des Nutzers.  Wird u.U. für das Masquerading bei ausgehender Mail
+genutzt.  Diese Adresse darf nicht schon existieren.  Mit einem C<!> am Ende der Adresse läßt sich 
+dieser Test ausschalten.
+
+=item B<--fullname>=I<Klartext-Name>
+
+Der volle Klartextname des Nutzers.  Wird nur für Dokumentationszwecke genutzt.
+
+=item B<--group>=I<Gruppenzuhörigkeit>
+
+Die Liste der Mailgruppen, in denen der Nutzer Mitglied ist.  Mail an diese Gruppen werden dann 
+auch an diesen Nutzer zugestellt.   Beim Neuanlegen genügt eine kommangetrennte Liste (keine Leerzeichen!).
+Beim Modifizieren können mit dem Präfix C<+> bzw. C<-> einzelne Gruppen hinzugefügt bzw. entfernt werden:
+
+	ma account --modify --group=+groupA,-groupB,+groupC hans
+
+entfernt C<hans> aus der Gruppe C<groupB> und fügt ihn in C<groupA> und C<groupB> ein.
+
+	ma account --modify --group= hans
+
+entfernt alle Gruppenzugehörigkeiten.
+
+	ma account --modify --group=groupA,groupB hans
+
+setzt genau die Zugehörigkeiten C<groupA> und C<groupB>.
+
+=item B<--[no]mbox>
+
+Mailbox-Anlegen oder auch nicht.  (Nur beim Neu-Anlegen.)
+
+=item B<--other=>=I<alternative Mailadressen>
+
+Wenn der Nutzer unter alternativen Mailadressen erreichbar sein soll, dann sind diese hier erwähnt.
+Es lassen sich Adressen hinzufügen und auch wieder löschen.  Die Syntax dazu entspricht der bei der 
+Gruppenzugehörigkeit.
+
+=item B<--password>=I<Passwort>
+
+Wenn beim Nutzeranlegen das Passwort nicht generiert werden soll, dann ist es hier anzugeben.
+Nachträglich läßt es sich auch hier ändern.
+
+=back
+
+=head2 Options für C<alias>
+
+=over 4
+
+=item B<--members>=I<Liste der Anliasangehörigen>
+
+Es können Alias-Mitglieder ein- und ausgetragen werden.  Die Syntax entspricht der oben bei der 
+Gruppenzugehörigkeit angegebenen.
+
+=back
+
+=head1 EXAMPLES
+
+Anlegen, Anzeigen, Verändern und Löschen eines Nutzers:
+
+	ma account --add --fullname "Hans Hanson" --other=h.hanson,hh --group=edv hans
+	ma account --list
+	ma account --modify --password=x --group=-edv,+it hans
+	ma account --delete hans
+
+Beim Löschen wird die B<Mailbox entfernt>!  Eventuelle Mitgliedschaften des Nutzers in
+Aliasen werden B<auch> berücksichtigt.  Die Nutzung 
+der Gruppen ist hier jedoch deutlich einfacher.
+
+Anlegen, Anzeigen, Verändern und Löschen eines Alias:
+
+	ma alias --add --members=hans,rudi operators
+	ma alias --list
+	ma alias --modify --members=+fritz operators
+	ma alias --delete operators
+
+=head1 Umgebungsvariablen
+
+=over 4
+
+=item	MA_CONF
+
+Konfigurations-Datei-Name.  Diese Variable B<muß> gesetzt werden, damit
+B<ma> sich überhaupt für eine Konfigurations-Datei interessiert.
+
+=item	LDAP_PASS
+
+Passwort für den LDAP-Administrator.  Optional.
+
+=item	IMAP_PASS
+
+Passwort für den IMAP-Administrator.  Optional.
+
+=back
+
+
+=head1 AUTHOR
+
+Heiko Schlittermann <hs@schlittermann.de>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/man8/ma.8.gz	Fri Nov 04 06:29:26 2005 +0000
@@ -0,0 +1,1 @@
+../ma.8.gz
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/manual.tex	Fri Nov 04 06:29:26 2005 +0000
@@ -0,0 +1,467 @@
+\documentclass[final]{article}
+% © Heiko Schlittermann
+% $Id$
+% $URL$
+
+\usepackage{ius}
+\usepackage{makeidx}
+\newcommand{\N}[1]{\textit{#1\/}}	% name
+\newcommand{\T}[1]{\texttt{#1}}	% typed
+\newcommand{\U}[1]{\underline{#1}}
+\newcommand{\C}[1]{}
+
+\title{Mailsystem --- Deutsche Post AG / TCB  --- Systemdokumentation}
+\author{$<$hs@schlittermann.de$>$}
+\date{\today}
+%\makeindex
+
+\begin{document}
+\maketitle
+
+%\begin{abstract}
+\subsubsection*{Zusammenfassung}
+  Ein Mailserver.  Post wird per \N{fetchmail} geholt, an \N{exim}
+  (SMTP) übergeben und dann in den \N{cyrus}-Mailstore gelegt.  Dort
+  ist dann alles per POP/IMAP verfügbar.
+%\end{abstract}
+
+\parskip0mm
+\tableofcontents
+\parskip0.5em
+
+%\setlength{\parindent}{0mm}
+%\setlength{\parskip}{1em}
+
+\section{Verwendete Komponenten}
+
+  \begin{tabular}{ll}
+    Post-Abholer & fetchmail \\
+    SMTP-Server	& exim 4.5x \\
+    POP3/IMAP-Server  & cyrus 2.2 \\
+    OpenLDAP  &	  2.2 \\
+  \end{tabular}
+
+\section{LDAP}
+
+  Im LDAP sind die Nutzerdaten (Accounts, Passworte, \ldots) hinterlegt.
+  Mit einem LDAP-Browser ist jederzeit der Zugang zum LDAP-Server
+  möglich, so sind Änderungen relativ einfach und schnell möglich.\index{LDAP-Tools}
+
+  \subsection{SSL/TLS}
+
+  Der LDAP-Server stellt seine Dienste unverschlüsselt zur Verfügung. Verschlüsselung über
+  STARTTLS bzw. \texttt{ldaps:///} ist möglich und konfiguriert.  Die Herausforderung ist
+  aber, daß dann der Client das auch unterstützen muß und vor allem mit den von einer nicht
+  offiziellen CA signierten Zertifikaten auskommen muß.
+
+  \subsection{LDAP-Browser}
+
+    Es gibt einige GUI-LDAP-Browser.
+
+      \begin{description}
+	\item{\N{ldapbrowser}}  Bei Google man nach \qq{ldapbrowser java}
+	  suchen.  Er läuft unter Linux und unter Win32 mit einer aktuellen
+	  Java-Runtime.
+
+      \item{\N{JXplorer}} Alternative zu dem o.g.
+	  \end{description}
+
+    Und es gibt natürlich noch für die (Linux)-Kommandozeile einiges.
+    Am besten geeignet: \N{ldapvi}.  Für die Kontrolle über die
+    letzten Schrauben dann \N{ldap\{add,delete,modify\}}.  Für
+    \N{ldapvi} ist ein Alias \T{ldapvi} in der \T{~.bashrc} eingetragen,
+    der sich versucht, als Admin an den LDAP-Server zu binden.
+
+    Die Zugangsdaten $\rightarrow$ Seite~\pageref{p:Zugangsdaten}.
+
+  \subsection{Struktur}
+
+    Unterhalb des Knotens \N{ou=MailAccounts} sind Nutzer-Objekte angelegt, die
+    für die Authentifizierung notwendig sind.  Außerdem enthalten diese
+    Objekte auch alternative Mail-Adressen und die Mailgruppen-Zugehörigkeit 
+    für die jeweiligen Nutzer.
+
+    \begin{sloppypar}
+    Unterhalb von \N{ou=MailAliases} sind Objekte, die den bisherigen
+    Aliasen/Verteilerlisten aus \T{/etc/aliases} entsprechen.
+    \end{sloppypar}
+
+    Es existieren also zwei Mechanismen für eine Realisierung von Alias-ähnlichen
+    Verteilern: über \N{ou=MailAliases} und über das \N{MailGroup}-Attribut.
+
+    \fussy
+    Wichtige Bestandteile der Mail-Accounts sind:
+
+    \begin{tabular}{ll}
+      objectClass   & \N{uidObject}, \N{tcbMailRecipient}, \N{person} \\
+      uid & Nutzer-ID (Anmeldung am POP3/IMAP-Server, Name der Mailbox) \\
+      userPassword & Passwort für SMTP/POP3/IMAP \\
+      tcbMailPrimaryAddress & primäre Mailadresse \\
+      tcbMailAddress & alternative Adressen \\
+      tcbMailGroup & Mitgliedschaft in Mailgruppen \\
+      tcbMailForwardingAddress & Weiterleitungsadresse (analog .forward) \\
+      sn & Nachname (ungenutzt, muß aber gesetzt sein) \\
+      cn & allgemeiner Name (ungenutzt, muß aber gesetzt sein) \\
+    \end{tabular}
+
+    Die Aliase sind einfacher aufgebaut:
+
+    \begin{tabular}{ll}
+      objectClass & \N{tcbMailAlias} \\
+      mail & allgemeiner Name des Aliases (hier: \qq{linke Seite}) \\
+      tcbMailForwardingAddress & Alias, als der Empfänger \\
+    \end{tabular}
+
+  \subsection{Files}
+
+    \begin{tabular}{ll}
+      Konfiguration   &	  \T{/etc/ldap/*} \\
+      Logfiles	      &	  \T{/var/log/syslog} \\
+      Datenbank	      &	  \T{/var/lib/ldap/*} \\
+      Backup	      &	  \T{/var/backups/ldap.week*} \\
+    \end{tabular}
+
+  \subsection{Administration}
+
+    Am LDAP-Server selbst ist nicht viel zu administrieren.  Nutzer an-
+    und ablegen ist auf Seite~\pageref{p:admin} beschrieben.
+
+  \subsection{Backup/Recovery}
+
+    Sollte das LDAP sich über Datenbank-Probleme beschweren, gibt es
+    etwa folgenden Weg:
+
+      \begin{enumerate}
+      \item   LDAP-Server neustarten: \T{service lap restart}
+      \item   Datenbank-Recovery versuchen:
+	      \begin{quote}\T{%
+	      service ldap stop \\
+	      cd /var/lib/ldap\\
+	      db41\_recover\\
+	      service ldap start\\
+	      }\end{quote}
+      \item   Backup wieder einspielen:
+	      \begin{quote}\T{%
+	      service ldap stop \\
+	      rm -r /var/lib/ldap/* \\
+	      bzcat </var/backups/ldap/XXX.bz2 | slapadd \\
+	      service ldap start \\
+	      }\end{quote}
+      \item   0351.8029981
+      \end{enumerate}
+
+\section{SMTP}
+
+  Um unerwünschtes Mailweiterleiten\index{Relaying} zu verhinden, sind Mails von
+  unauthentifizierten Nutzern nur zugelassen, wenn der Empfänger
+  in einer der bekannten lokalen Domains ist.  Zum Senden von Mails an
+  andere Domains ist eine Authentifizierung\footnote{Name/Passwort wie
+  am POP3/IMAP-Server} am Mailserver notwendig.
+
+  \subsection{SMTP-Authentifizerung}
+
+    Es werden die Authentifizierungsverfahren PLAIN und
+    LOGIN unterstützt.  
+
+    Authentifizierung ist nur möglich, wenn eine verschlüsselte Verbindung
+    zwischen MUA und MTA besteht.  Dafür bietet der MTA \N{STARTTLS} an.
+
+    \subsubsection{Authorisierung}
+
+      Authentifizierte Nutzer dürfen Mails an beliebige Adressen versenden.
+      Dabei wird jedoch immer die \N{PrimaryMailAddress} als deren Absender
+      eingetragen.
+
+  \subsection{Mailrouting}\label{s:mailrouting}
+
+    \begin{enumerate}
+
+      \item Mail an nicht lokale Domains (lokal ist
+	    \N{tcb.deutschepost.de}) wird an den Smart-Relay (\N{mimesweeper} 
+	    über DNS-Auflösung) weitergeleitet.
+
+      \item Es wird im LDAP nach Aliasen für die aktuelle Adresse gesucht (unterhalb
+	    \N{ou=MailAliases}).  Diese Aliase entsprechen den bisherigen
+	    \T{/etc/aliases}.  Aliase werden dort ohne angehängte Domain gesucht.
+
+      \item Es wird unterhalb von \N{ou=MailAccounts} gesucht, ob der Adressat eine 
+	    Mailgruppe ist.  Wenn ja, werden alle Gruppenmitglieder zu neuen 
+	    Adressaten.
+
+      \item Es wird unterhalb von \N{ou=MailAccounts} gesucht, ob
+	    es für den Adressaten eine Mailbox/UID gibt.
+
+      \item Es wird unterhalb von \N{ou=MailAccounts} gesucht, ob für den
+	    Adressaten/UID Weiterleitungsadressen existieren.  Wenn ja, werden diese
+	    eingesetzt und das Routing beginnt von vorn.
+
+      \item Wenn dieser Schritt erfolgreich ist, wird die Mail an den
+	    POP3/IMAP-Server übergeben.  
+
+    \end{enumerate}
+      
+
+  \subsection{Adress-Rewriting}
+
+    In ausgehenden Mails (zum Mimesweeper) werden alle Absender-Adressen
+    so umgeschrieben, daß als Absender (in \N{From:} und \N{Sender:} und auch im
+    \N{Return-Path} die primäre Adresse des authentifizierten Nutzers steht.
+
+    Enthält die Primäre Adresse kein \T{@}, dann wird der Return-Path
+    auf \T{$<>$} gesetzt und die \N{From:} und \N{Sender:}-Header
+    bleiben unverändert.  Mit dieser Einstellung sollte die Mail dann
+    (wegen des leeren Return-Path) die meisten Mailserver passieren
+    können.\footnote{Die meisten.  Einige prüfen inzwischen auch die
+    Verfügbarkeit von brauchbaren Header-Zeilen \N{From:} und
+    \N{Sender:}.}
+
+    In \N{CC:}, \N{Reply-To:}, \N{To:}-Headern wird, wenn sie eine
+    Adresse \N{@tcb.deutschepost.de} enthalten, diese ersetzt durch die
+    primäre Adresse des entsprechenden Nutzers (gesucht wird im Ldap
+    nach Einträgen, deren \N{tcbMailAddress} dem Nutzer lokalen Teil der
+    fraglichen Adresse entspricht).
+    
+  \subsection{Files}
+    
+    \begin{tabular}{ll}
+      Konfiguration \N{Exim}		&   \T{/etc/exim/*}	\\
+      SMTP-Spool			&   \T{/var/spool/exim/*} \\
+      SMTP-Log				&   \T{/var/log/exim/\{main,reject,panic\}.log} \\
+    \end{tabular}
+
+  \subsection{Administration}
+
+    Viel zu administrieren sollte es am \N{Exim} nicht geben.  Ab und zu
+    kann man die Warteschlange mit \T{mailq} oder \T{exipick} kontrollieren.
+    Eingefrorene Mails sollten nach einigen (7) Tagen verschwinden.
+    
+    Im Zweifelsfall ist ein Blick in die Logfiles (vor allem
+    \T{panic.log} und \T{reject.log}) interessant.
+
+    Zum Adress-Test biete sich folgendes an:
+
+    \begin{quote}\T{exim -v -bv hans@domain.de}\end{quote}
+
+    Damit können lokale und auch externe Adressen getestet werden und es
+    ist zu erkennen, wie der Mailserver die jeweilige Adresse routen
+    würde.
+
+
+\section{POP3/IMAP}
+
+  Vom SMTP-Server \N{Exim} kommt die Mail per LMTP zum \N{Cyrus}.  Der
+  verwaltet sein eigenes Mailstore in \T{/var/spool/cyrus}.  Dort
+  manuell einzugreifen ist nicht die beste Idee, da der \N{Cyrus} sich
+  Index-Dateien erzeugt, über die er dann den Zustand der Mailboxen zu
+  kennen glaubt.
+
+
+  \subsection{Clients}
+
+    Die Clients können per POP3 und/oder IMAP auf ihre Postfächer
+    zugreifen.  Allerdings steht per POP3 nur die Inbox zur Verfügung.
+
+    Der übertragenen User-Name wird im LDAP unter \N{ou=MailAccounts}
+    gesucht und dann wird über ein LDAP-Bind die Korrektheit des
+    Passworts geprüft.
+
+  \subsection{Files}
+
+    \begin{tabular}{ll}
+      Konfiguration   &	\T{/etc/imapd.conf} \\
+		      & \T{/etc/cyrus.conf} \\
+      Spool	      & \T{/var/spool/cyrus} \\
+      Logfiles	      & \T{/var/log/mail.log}
+    \end{tabular}
+
+  \subsection{Administration}
+
+    Sollten die unten (S.~\pageref{p:admin}) beschriebenen Werkzeuge
+    nicht ausreichen, ist mit \T{cyradm} noch einiges zu retten:
+    \T{cyradm localhost}:
+
+    \begin{verbatim}
+      lm                         # alle Mailboxen anzeigen
+      cm user/hannes             # Mailbox anlegen für hannes
+      lm user/hannes             # Hannes Mailbox
+      lam user/hannes            # Berechtigungen zeigen
+      sam user/hannes root all   # Berechtigungen setzten
+      lq user/hannes             # Quota anzeigen
+      sq user/hannes STORAGE 200 # Quota auf 200k setzen
+    \end{verbatim}
+
+    Eine direkte Verbindung mit den Mailboxen ist mit jedem beliebigen
+    IMAP-Client möglich, z.B. auch mit Mutt: 
+
+    \begin{quote}
+    \T{mutt -f imap://cyrus@localhost/user/hannes} 
+    \end{quote}
+      
+    bzw.\ direkt als Hannes: 
+      
+    \begin{quote}
+    \T{mutt -f imap://hannes@localhost/}
+    \end{quote}
+    
+
+\section{Administration}\label{p:admin}
+
+  Zur Administration des Mailsystem liegt ein einfach(!) zu benutzender
+  Script \T{ma} vor.  Die aktuelle Dokumentation dazu ist \N{ma(8)} zu entnehmen.
+
+
+  \subsection{Accounts}
+
+    \subsubsection{Anzeigen}
+
+    Vorhandene Accounts können angezeigt werden:
+
+    \begin{quote}
+      \T{ma \U{account} --ls}\\
+      \T{ma \U{account} --ls `h*'}
+    \end{quote}
+
+    \subsubsection{Anlegen}\label{ss:account}
+
+      Beim Anlegen werden nur Accounts und Mailboxen angelegt, wenn sie
+      noch nicht da sind.  In bestehende Accounts werden nur die
+      Mail-Attribute (\N{tcbMailPrimaryAddress}, \N{tcbMailAddress}) eingetragen, 
+      wenn Sie noch nicht vorhanden sind.  Andernfalls gehen wir davon
+      aus, daß es kein Neueintrag ist.
+
+      \begin{quote}
+      \T{ma \U{account} \U{--add} --fullname='Hans Hanson' --other='hh' hans.hanson}
+      \end{quote}
+
+      Weitere Optionen sind:
+
+      \begin{description}
+      \item[\T{--address=s}]  Primäre Mailadresse (default: entspricht dem Account-Namen)
+      \item[\T{--fullname=s}] Klartext-Name (default: -)
+      \item[\T{--other:s}]  Weitere alternative Mailadressen für \emph{diesen\/} Nutzer (default: -)
+      \item[\T{--imap\_quota=i}] Imap-Quota in MegaByte (default: 300\,MB)
+      \item[\T{--[no]mbox}]   Anlegen der Mailbox (default: Mailbox wird angelegt)
+      \item[\T{--password=s}] Passwort (default: \{pwgen\}, d.h.\ es wird generiert und 
+			      dann angezeigt)
+      \item[\T{--group:s}] Zugehörigkeit zu Mailgruppen.
+      \end{description}
+
+
+    \subsubsection{Verändern}\label{ss:change}
+
+      Vorhandene Einträge können natürlich auch verändert werden:
+
+      \begin{quote}
+      \T{ma \U{account} \U{--modify} --fullname='Hans-Georg Hanson' hans.hanson}\\
+      \end{quote}
+
+      Für einige Attribute ist eine kumulative Änderung möglich (z.B.
+      Hinzufügen von alternativen Namen), bei anderen (primary
+      Mailadresse) nur ein Verändern.  Der Präfix \T{+} oder \T{-} ist
+      wichtig, ohne den Präfix wird das Attribut \emph{genau\/} auf den
+      angegebenen Wert gesetzt:
+
+      \begin{quote}
+      \T{ma \U{account} \U{--modify} --also=+hans,-hh hans.hanson} \\
+      \T{ma \U{account} \U{--modify} --also=h.hanson hans.hanson}
+      \end{quote}
+
+      Veränderbar sind die selben Optionen wie oben
+      (\ref{ss:account}).\footnote{Außer die Mailbox-Option, das ist
+      noch nicht implementiert.}
+
+    \subsubsection{Löschen}
+
+      \emph{Vorsicht.\/}  Es wird rigoros der komplette LDAP-Eintrag
+      gelöscht!  Und bei Bedarf die Mailbox.
+
+      \begin{quote}
+      \T{ma \U{account} \U{--delete} hans.hanson}\\
+      \T{ma \U{account} \U{--delete} --nombox hans.hanson}\\
+      \end{quote}
+
+      Die zweite Zeile würde die Mailbox bestehen lassen.
+      Optionen gibt es keine weiteren.
+
+  \subsection{Aliase}
+
+    \subsubsection{Anzeigen}
+
+      Alle vorhandenen Aliase werden in einem bequemen
+      \T{/etc/aliases}-Format ausgegeben:
+
+      \begin{quote}
+      \T{ma \U{alias} --list} \\
+      \T{ma \U{alias} --list `h*'}
+      \end{quote}
+
+    \subsubsection{Anlegen}
+
+      Beim Anlegen wird der Eintrag nur gemacht, wenn er noch nicht
+      vorhanden ist.
+
+      \begin{quote}
+      \T{ma \U{alias} \U{--add} --members=hans,suse fam.ente}
+      \end{quote}
+
+      Das trägt einen Alias \N{fam.ente} ein, der als Verteiler wirkt
+      und die Mails an \N{hans} und \N{suse} weiterleitet.
+
+      Weitere Optionen gibt es nicht.
+      
+    \subsubsection{Verändern}
+
+      Zum Verändern gilt das selbe wie schon in~\ref{ss:change} zu den
+      Accounts.  Hinzufügen von Mitgliedern und Entfernen von
+      Mitgliedern ist möglich.  Dabei sind auch \qq{Masseneintragungen}
+      möglich:
+
+      \begin{quote}
+      \T{ma \U{alias} \U{--modify} --members=+big.brother '*'}
+      \end{quote}
+
+    \subsubsection{Löschen}
+
+      Der gesamt Alias-Eintrag wird entfernt.
+
+      \begin{quote}
+      \T{ma \U{alias} \U{--delete} fam.ente}
+      \end{quote}
+
+
+\section{Zugangsdaten}\label{p:Zugangsdaten}
+\index{Passwort}
+
+  \subsection{LDAP}
+
+    \begin{tabular}{ll}
+      Server:Port	    &	  mail.tcb.deutschepost.de:389 (Plain + STARTTLS)\\
+			    &	  mail.tcb.deutschepost.de:636 (SSL) \\
+      Base-DN		    &	  \T{dc=tcb,dc=deutschepost,dc=de} \\
+      Admin-User (Bind-DN)  &	  \T{cn=admin,dc=tcb,dc=deutschepost,dc=de}\\
+      Admin-Passwort	    &	  \T{anfang} \\
+    \end{tabular}
+
+  \subsection{IMAP}
+
+    \begin{tabular}{ll}
+      Server:Port	    &	  mail.tcb.deutschepost.de:143 \\
+      Admin-User	    &	  \T{cyrus} \\
+      Admin-Passwort	    &	  \T{anfang} \\
+      Korrespondierender LDAP-Eintrag &	\T{uid=cyrus,ou=Cyrus,\dots} \\
+    \end{tabular}
+
+  Um die nervigen Fragen nach Passworten\index{Passwort} für LDAP und IMAP zu umgehen,
+  können die Passworte in Umgebungsvariablen abgelegt werden: \T{export
+  IMAP\_PASS=XXX} und \T{export LDAP\_PASS=XXX}.
+
+\printindex
+
+\vfill
+Stand: \verb!$Id$!\\
+Quelle: \verb!$URL$!
+
+
+\end{document}
+
+% vim:sts=2 sw=2 aw ai sm:
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mergePasswdShadowAliases	Fri Nov 04 06:29:26 2005 +0000
@@ -0,0 +1,137 @@
+#! /usr/bin/perl
+# $Id$
+# $URL$
+
+# passwd + shadow + aliases > uid:gecos:[alias ...]:password
+
+use strict;
+use warnings;
+use IO::File;
+
+use AppConfig;
+
+use constant CONFIG => (
+    { CASE => 1 },
+    passwd =>	    { ARGS => "=s", DEFAULT => "passwd" },
+    shadow =>	    { ARGS => "=s", DEFAULT => "shadow" },
+    aliases =>	    { ARGS => "=s", DEFAULT => "aliases" },
+);
+
+
+use constant FS => ":";
+use constant RS => "\n";
+
+
+my $Cf = new AppConfig CONFIG or die;
+   $Cf->getopt(\@ARGV) or die;
+
+MAIN: {
+
+    my %visible;
+    {
+	# Zuerst müssen wir die Aliase irgendwie in nutzbare Form bringen.
+	# Wir können nicht einfach sequentiell durch das Aliasfile rennen und die Aliase 
+	# irgendwie verteilen, sondern wir sammen sie erstmal schön, denn am Ende müssen wir ja 
+	# wissen, welche noch nicht aufgelöst sind.
+	my $aliases = new IO::File $_ = $Cf->aliases or die "Can't open $_: $!\n";
+	while (<$aliases>) { chomp;
+	    s/#.*//g;
+	    next if /^\s*$/;
+	    s/[:,]/ /g;
+	    my ($visible, @real) = split;	# "hans.hanson hh"
+						# "root wgs hh"
+
+	    foreach (@real) {
+		$visible{$_}->{$visible} = 1;	# hh -> hans.hanson, root
+	    }
+	}
+    }
+
+    my %passwords;
+    {   
+	my $sh = new IO::File $_ = $Cf->shadow or die "Can't open $_: $!\n";
+	%passwords = map { (split /:/, $_)[0,1] } <$sh>;
+	@passwords{keys %passwords} = map { "{crypt}$passwords{$_}" } keys %passwords;
+    }
+
+    my %users;
+    {
+	# Passwort-File ist die primäre Quelle von Information
+	my $pw = new IO::File $_ = $Cf->passwd or die "Can't open $_: $!\n";
+	while (<$pw>) {
+	    my ($name, undef, $uid, undef, $gecos, undef) = split /:/, $_;
+
+	    my ($cn, $sn);
+	    ($cn = $gecos) =~ s/,.*//;
+	    ($sn = (split " ", $cn)[-1]);
+
+	    $users{$name}->{gecos} = $gecos;
+	    $users{$name}->{uid} = $uid;
+	    $users{$name}->{cn} = $cn || "";
+	    $users{$name}->{sn} = $sn || "";
+	    $users{$name}->{aliases} = [];
+	    $users{$name}->{password} = $passwords{$name};
+	    $users{$name}->{sorter} = $name;
+
+
+	    # Gleich mal nach dem Alias sehen
+	    if (exists $visible{$name}) {
+		my $visible = $visible{$name};	
+
+		foreach (keys %$visible) {
+		    if (/$sn/i) {
+			push @{$users{$name}->{aliases}}, $_;
+			delete $visible->{$_};
+		    }
+		}
+	    }
+	}
+    }
+
+
+    # delete users with short password or uid < 1000
+    delete @users{grep {
+	$users{$_}->{password} =~ /^{crypt}.$/
+	or $users{$_}->{uid} < 1000
+    } keys %users};
+
+
+    # Und nun noch die Aliase, die nicht verarbeitet wurden.
+    {
+	
+	# Alle löschen, die nur noch leere Hashs haben
+	delete @visible{ grep { not scalar %{$visible{$_}} } keys %visible};
+
+	# keys sind die Ziele(!) aber wir müssen es wieder genau umgekehrt haben!
+	# so ist es jetzt:
+	# uid: alternatives
+	# wgs: bauleitung wieder.platz
+
+	foreach my $name (keys %visible) {
+	    my @aliases = keys %{$visible{$name}};
+	    foreach (keys %{$visible{$name}}) {
+		if (not exists $users{$_}) {
+		    $users{$_}->{gecos} = "ALIAS";
+		    $users{$_}->{password} = "";
+		    $users{$_}->{cn} = "";
+		    $users{$_}->{sn} = "";
+		    $users{$_}->{sorter} = "00$_";
+		} elsif ($users{$_}->{gecos} ne "ALIAS") {
+		    die "ALIAS $_ and user $_ conflict!";
+		}
+		push @{$users{$_}->{aliases}}, $name;
+	    }
+	}
+    }
+
+    foreach my $name (sort { $users{$a}->{sorter} cmp $users{$b}->{sorter} } keys %users) {
+	$users{$name}->{aliases} = join ",", @{$users{$name}->{aliases}};
+	print join(FS, $name, @{$users{$name}}{qw/password gecos cn sn aliases/}), RS;
+    }
+
+    print "\n";
+
+
+}
+
+# vim:sts=4 sw=4 aw ai sm:
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/password.pm	Fri Nov 04 06:29:26 2005 +0000
@@ -0,0 +1,19 @@
+package password;
+
+sub ask($) {
+    local $| = 1;
+    return undef if not -t;
+
+    print $_[0];
+    system(stty => "-echo");
+    my $ans = <STDIN>;
+    system(stty => "echo");
+    print "\n";
+
+    chomp $ans;
+    return $ans;
+}
+
+1;
+
+# vim:sts=4 sw=4 aw ai sm:
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/populate.ldif	Fri Nov 04 06:29:26 2005 +0000
@@ -0,0 +1,25 @@
+# $Id$
+# $URL$
+
+dn: ou=Cyrus,o=ahreick
+objectClass: organizationalUnit
+ou: Cyrus
+description: cyrus mail administration objects
+
+dn: uid=root,ou=Cyrus,o=ahreick
+objectClass: simpleSecurityObject
+objectClass: account
+uid: root
+userPassword: {crypt}KycC6akPBSoYY
+description: cyrus administrator 
+
+dn: ou=MailAccounts,o=ahreick
+objectClass: OrganizationalUnit
+ou: MailAccounts
+description: all mail users
+
+dn: ou=MailAliases,o=ahreick
+objectClass: OrganizationalUnit
+ou: MailAliases
+description: global e-mail aliases
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/split	Fri Nov 04 06:29:26 2005 +0000
@@ -0,0 +1,105 @@
+#! /usr/bin/perl
+# © 2005 Heiko Schlittermann <hs@schlittermannn.de>
+# $Id$
+# $URL$
+#
+# Einlesen von Schubis mail.csv und aufteilen in fetchmailrc/users/aliases
+# zur bequemen Weiterverarbeitung
+my $USAGE = <<'#';
+Usage: {{ME}} [options] ...
+       --fetchmailrc=s  Name of the fetchmailrc file [{{$::Cf->fetchmailrc}}]
+       --users=s	Name of the fetchmailrc file [{{$::Cf->users}}]
+       --aliases=s	Name of the fetchmailrc file [{{$::Cf->aliases}}]
+       --help
+
+       Hint: use '/dev/null' as file name to supress the generation
+       $Id$
+#
+
+
+use strict;
+use warnings;
+use AppConfig;
+use File::Basename;
+use IO::File;
+
+use constant ME => basename $0;
+
+use constant CONFIG => (
+    { CASE => 1 },
+    fetchmailrc =>  { DEFAULT => "fetchmailrc", ARGS => "=s" },	
+    users =>	    { DEFAULT => "users", ARGS => "=s" },
+    aliases =>	    { DEFAULT => "aliases", ARGS => "=s" },
+    help =>	    { DEFAULT => 0, ARGS => "!" }
+
+);
+
+$::Cf = new AppConfig CONFIG;
+$::Cf->getopt(\@ARGV);
+
+if ($::Cf->help) {
+    $USAGE =~ s/{{(.*?)}}/eval $1/eg;
+    print $USAGE;
+    exit;
+}
+
+
+
+MAIN: {
+    our $Cf;
+    my %groups;
+
+    my $fetchmailrc = new IO::File $_ = $Cf->fetchmailrc, "w" or die ME.": Can't open >$_: $!\n" 
+	if $Cf->fetchmailrc;
+    my $users = new IO::File $_ = $Cf->users, "w" or die ME.": Can't open >$_: $!\n" 
+	if $Cf->users;
+
+
+    while (<>) {
+	$. == 1 and next;
+	s/\s*$//;
+	my ($mailto, $popbox, $poppass, $intern, $group, $imapuser, $imappass)
+	    = split /;/;
+
+	# externes Postfach?
+	if ($fetchmailrc and $popbox) {
+	    print $fetchmailrc "user \"$popbox\" with password \"$poppass\" is $mailto here\n";
+	}
+
+	# realser Nutzer?
+	if ($mailto =~ /^\Q$imapuser\E/) {
+	    my ($first, $last) = split /\./, $imapuser;
+	    $last or $last = $first, $first = "";
+	    print $users join(":", 
+		    $imapuser, 
+		    $imappass, 
+		    join(" ", $first ? ucfirst($first) : (), ucfirst($last)),
+		    join(",", $imapuser, $last, $first ? substr($first, 0, 1) . ".$last" : ()), 
+		    "\n");
+	} else {
+	    $mailto =~ s/\@.*//;
+	    push @{$groups{$mailto}}, $imapuser;
+	}
+    }
+
+    if ($Cf->aliases) {
+	my $aliases = new IO::File $_ = $Cf->aliases, "w" or die ME . ": Can't open >$_: $!\n";
+	print $aliases map { "$_: ". join(", ", @{$groups{$_}}). "\n" } sort keys %groups;
+    }
+
+}
+
+__END__
+
+mailadresse;popmailbox;popmailpw;intern;intern-gruppe;imapaccount;imappw
+annika.henze@vw-audi-dresden-reick.de;;;intern;;annika.henze;ahenze
+isabell.zeiske@vw-audi-dresden-reick.de;;;intern;;isabell.zeiske;izeiske
+julia.micheel@vw-audi-dresden-reick.de;;;intern;;jilia.micheel;jmicheel
+mandy.stranz@vw-audi-dresden-reick.de;;;intern;;mandy.stranz;mstranz
+susann.kolodziejczyk@vw-audi-dresden-reick.de;;;intern;;susann.kolodziejczyk;skolo
+renate.feurich@vw-audi-dresden-reick.de;;;intern;;renate.feurich;rfeurich
+rwks01@vw-audi-dresden-reick.de;;;intern;;rwks01;rwks01
+rwks02@vw-audi-dresden-reick.de;;;intern;;rwks02;rwks02
+rwks03@vw-audi-dresden-reick.de;;;intern;;rwks03;rwks03
+
+# vim:sts=4 sw=4 aw ai sm: