#!/usr/local/bin/perl -w
use strict;

=head1 NAME

  sysadmin.pl - administer system entities such as user and domain

=head1 SYNOPSIS

  sysadmin.pl domain foo.org
  sysadmin.pl user username userpass domain1.arg domain2.org ...
  sysadmin.pl list listemail listwwwdomain owneremail listpass
  sysadmin.pl list uhuruweb@lists.uhurumovement.org uhurumovement.org mda@discerning.com SECRET

Must specify -doit=1 to make it happen.

=head1 DESCRIPTION

We create a new unix group for each domain (with a heuristic name mapping).

We keep all users and domains parallel in the file system (/home/* and /var/www/*).
This means quota management is a bit trickier

We keep all user names (/etc/passwd), group names (/etc/group),
domain names (/var/www) unique in a global namespace.

We use normal passwd authentication.
We don't try to force servers to use mysql auth, or some PAM hook to us.
Note that PAM provides no means of adding a user,
or determining programmatically how an application is using PAM.
There is a way to change a credential, but the client has no way to know
what authentication service is being updated.

Users are ftp chrooted to their home directory, and are given a union mount into
the domains they have access to.

We currently have no configuration interface to services (for example,
to query/change proftpd.conf).

One approach (dreamhost.com) is that:
  - each account is a unix group
  - all shell users in an account are members of that group
  - user home directories are created like:
    drwxr-x--x username primgroup /home/username
  - all domain docroots are subdirectories of some user home
    drwxrwsr-x username primgroup /home/username/domain.org
  - there is no group access control within an account
    - all docroots within an account are readable by all users in the account (with ftp/shell access)
    - if a docroot is writable by group members, then all group members can do so.
  - users in other accounts cannot see any of the files (including docroots) of other accounts
  - the actually httpd server runs on a different host on the shared file system,
    which presumably allows it to read all the files.

Another approach (pair.com):
  - each account gets a single shell user
  - all shell users are in the same unix group of "users"
  - any shell user can read other user's files
    drwxr-xr-x username users /usr/home/username
  - there can be multiple ftp users (and mailboxes) per account
  - ~user/public_html is a sym link to /usr/www/users/username
  - ~user/public_ftp is a sym link /usr/public_ftp/username
  - each domain is in ~user/public_html/domainname = /usr/www/users/username/domainname
  - domains are "other" but not group readable. 
    drwx---r-x username users www.domain.com
    since all users of all accounts are in the same group, this prevents reading
    by everyone except the owner and staff (and whatever account the httpd server runs as)
    Note that the home directories could be made 701 and the public_html be 705,
    and the contents be 644 (public) or 604 (web only),
    and this would still work (for reachability, all directories in a path must be x).

My approach:
  - each domain gets its own unix group
  - each user allowed to write that domain's docroot gets that domain's unix group as a secondary group
  - docroots are like:
  drwxrwsr-x root domain-org /group/domain.org
  - user homes are their own user groups (redhat convention)
    drwxrwxr-x username username /home/username
    But could just as well be:
    drwx------ username username /home/username
    except for the inability to run ~user home pages

CGIs may or may not be run with suexec.


We do not:
   manage ip addresses
   manage hosts -- let alone distinct hosts for ftp/ssh, http, smtp, and pop/imap.
   handle applications for SSL certs

=head1 TODO

Support -group=what when creating a new domain, to set its group.

We now detect duplicate shortened group names.
But after creation, we need to determine group name through some other means,
such as checking owner.

Things that require a server restart to take effect.
Things that require a user to login again to take effect (like adding to a group, at least in linux).

Listing functionality which can show things missing (groups without domains, etc.).

Backups.

Log monitoring, and status of key daemons:
    httpd, mysql, mailman qrunner, qpsmtpd, postfix, proftpd, sshd

Central admin page for each domain

SSL domains
http://www.rapidssl.com/resources/csr/apache_mod_ssl.htm

=head2 User Admin

Policy about whether primary user group is based on user or not.
More precise management of primary vs. secondary groups.

Other useradd options, such as -m (make home dir) and -c (comment/gecos field)

User profiles/groups/roles: so don't bother with union mounts if their ftp isn't chroot.

Deleting a user (archive files? remove password? forward email?)
Migrating a user name to another.

=head2 Group Admin

A group password, perhaps synchronized with a htpasswd group password, for a private area in
the group.

Maintaining a htpasswd file
    can do: 
    - copy first two fields from /etc/shadow
    - htpasswd -cb /group/domain.org/htpasswd.usr username password
    - use an apache module to allow direct use of /etc/passwd

=head2 DNS Admin

Get a local dns admin program, then replicate to a primary service?

=head2 List Admin

List serve administration (add to apache, to mailman, to dns A and MX, to /etc/aliases)  

to create in mailman:

    web-based create can be done on http://domain.org/mailman/create
    command-line create can be done with /usr/local/bin/newlist
        newlist -q listname@lists.domain.org listadmin@email.org listadminpassword
    assume a single host-wide install of mailman.
    file locations:
      Global config is /var/mailman/Mailman/mm_cfg.py
      list pickle files go in /var/mailman/lists/
      list archives go in /var/mailman/archives/private/
    web locations: 
      list administration (requires password) is at http://$wwwdomain/mailman/admin/$listname/
      user page at http://$wwwdomain/mailman/listinfo/$listname
      archive page at http://$wwwdomain/mailman/private/$listname/
    Because of aliases config and single mailman db, can't have the same list name across different domains.

Note also that any domains on host can administer any lists;
it is just that on the listinfo page, it looks through to limit
to ones advertised on that domain.
The per-list pick file has a value for "host_name", such as "lists.domain.org".
This is looked up in VIRTUAL_HOSTS? but that is from www domain to list domain...
The cgi C program /var/mailman/cgi-bin/roster invokes the python program /var/mailman/Mailman/Cgi/roster.py 

to add to incoming smtp:
    we are using qpsmtpd (/etc/rc.local starts in /home/smtpd/qpsmtpd)
    we have our own "do_aliases" plugin that reads /etc/aliases

apacam overrides:
send_welcome_msg = 0
private_roster = 2

first_strip_reply_to = 1
reply_goes_to_list = 1
# email reminders
send_reminders = 0
# notify admin of changes
admin_notify_mchanges = 1
# members receive digests
digestable = 0
# approved subscribe, members view members, private archive, yearly archive
advertised = 0
subscribe_policy = 2
private_roster = 1
archive_private = 1
archive_volume_frequency = 0

Must set through other means:
  real_name
  owner
  moderator
  description
  subject_prefix
  msg_footer
  host_name

=head1 NAMES

Traditional unix had a limit of 8 chars per username. 
On linux libc6, names above 32 are truncated in utmp/wtmp (see Linux Admin FAQ).
But other than that, user names on linux are now effectively unlimited (can exceed 100 chars).

Traditionally, uids were 16bit numbers (<32768). Now Linux can support unsigned 32-bit uid.

To create a group name we map periods to hyphens, because periods make chown username.groupname hard problematic.
note that there is also a group name size limit, which might be as low as 8 chars.
Linux 2.4 seems to limit group names to 16 chars, as does FreeBSD 4.x.
Solaris 8.x seems to want group names of 8 chars or fewer.

Having longer ids might interfere sometimes with use of tar/untar across systems,
shared file systems, and so on: it isn't really just one OS you have to worry about.

There are limits to how many users and groups can be on the system, and line
length limits for the /etc/passwd and /etc/group files.
And limits to how many groups a user can be a member of.

=head1 SEE ALSO

Software:

  http://www.atmarkit.co.jp/flinux/samba/sambatips03/smbadduserscript.txt

References:

  http://docs.rinet.ru:8080/UNIXs/ch17.htm (UNIX Unleashed, System Administrator's Edition)
  http://peace.tbcnet.com/~cis270/notes/class4/class4.html

=cut

use File::stat;

my $MAX_GROUP_NAME = 16;
my $GROUPROOT = '/group';
my $USERROOT = '/home';

#use FindBin;
#use lib $FindBin::Bin;
#my $INSTALLDIR = $FindBin::Bin;
my $INSTALLDIR;
BEGIN {$INSTALLDIR = '/group/discerning.com/public_html/hacks/sysadmin'}
use lib $INSTALLDIR;

our $DBFILES = '/home/mdaoh/sysadmindb';

require sysadmindb;

# $HELPERDIR contains subfile.pl and insertfile.pl
my $HELPERDIR = $INSTALLDIR;

my $APACHE_CONF_D = '/etc/httpd/conf.d';
my $APACHE_CONF_SKEL = "$INSTALLDIR/httpd.conf.skel";
my $APACHE_CONF_MAIN = '/etc/httpd/conf/httpd.conf';

my $MAILMAN_CFG = '/var/mailman/Mailman/mm_cfg.py';
my $MAILMAN_BIN = '/usr/local/bin';
my $MAILMAN_ALIASES_SKEL = "$INSTALLDIR/mailman.aliases.skel";

my $SMTP_INCOMING_ALIASES = '/etc/aliases';
my $SMTP_INCOMING_RCPTHOSTS = '/home/smtpd/qpsmtpd/config/rcpthosts';

my $OPTS = {};

my $DEBUG = 0;
sub debug {
    print STDERR "DEBUG: ", @_, "\n" if $DEBUG;
}

sub warning {
    print STDERR "WARNING: ", @_, "\n";
}

################################################################
# accumulate commands to be executed
my @COMMANDS = ();
my $CATEGORY = '';
sub push_command {
    my ($cmd, $comment) = @_;
    push(@COMMANDS, "# $comment", $cmd);
}
sub push_noop {
    my ($comment) = @_;
    push(@COMMANDS, "# $comment");
}
sub start_category {
    my ($cat) = @_;
    $CATEGORY = $cat;
}

################################################################

sub user_in_group {
    my ($un, $gn) = @_;
    return `id $un` =~ m/\($gn\)/;
}

# determine list of groups user is a member of
sub get_user_groups {
    my ($un) = @_;
    die "no username" unless $un; 
    my $idout = `id $un 2>&1`;
    if (!$idout || $idout =~ m/no such/i) {
	debug("user $un does not exist, so has no groups");
	return ();
    }
    die "bad id output '$idout'" unless $idout =~ m/groups=(.*)/;
    my $user_groups = $1;
    my @usergroups = ($user_groups =~ m/\((.*?)\)/g);
    debug("user $un has groups '$user_groups' resulting in group list '@usergroups'");
    return @usergroups;
}

sub user_exists {
    my ($un) = @_;
    my $userdef = `grep $un /etc/passwd`;
    chomp($userdef);
    return $userdef;
}

sub get_all_domains {
    my $grouplines = `ls $GROUPROOT`;
    my @domains = split(/\s+/, $grouplines);
    debug("the available domains on the system are '@domains' from '$grouplines'");
    return @domains;
}

sub should_mount_domains {
    my ($un) = @_;
    return !is_user_in_group($un, 'wheel');
}

sub is_user_in_group {
    my ($un, $gn) = @_;
    my @usergroups = get_user_groups($un);
    my @matching = grep {$_ eq $gn} @usergroups;
    debug("user $un in $gn: '@matching'");
    return @matching;
} 

# if extras is present, append to the result, removing duplicates.
# it is a list of domains about to be added, so we won't know about otherwise.
sub get_user_domains {
    my ($un, $extras) = @_;

    my @usergroups = get_user_groups($un);
    my @domains = get_all_domains();

    my @user_domains = ();
    for my $dn (@domains) {
	next if grep {$dn eq $_} @$extras;
	my $gr = domain2group($dn);
	push(@user_domains, $dn) if (grep {$_ eq $gr} @usergroups);
    }
    return [@$extras, @user_domains];
}

sub domain2group {
    my ($dn, $in_recurse) = @_;

    # first see if there is a groupname already set
    my $groupname = db_query_value("SELECT groupname FROM domains WHERE domainname = '$dn'");

    if ($groupname) {
    }
    # if domain name is long, we have to shorten it, and make sure it is unique
    elsif (length($dn) > $MAX_GROUP_NAME) {
	$groupname = $dn;
	# take first N letters, plus the last part of the domain
	my ($front, $suffix) = ($groupname =~ m/(.*)(\..*)/);
	if ($suffix) {
	    $groupname = substr($front,0,$MAX_GROUP_NAME - length($suffix)) . $suffix;
	}
	else  {
	    $groupname = substr($groupname,0,$MAX_GROUP_NAME);
	}
	$groupname =~ s/\./-/g;

	# don't accept it if there is another existing domain with same shortened name
	if (!$in_recurse) {
	    my @other_domains = grep {$_ ne $dn} get_all_domains();
	    my @other_groups = map {domain2group($_,1)} @other_domains;
	    my @same_group = grep {$_ eq $groupname} @other_groups;

	    # warning("group '$groupname' (from domain '$dn') is too long; shortening to $MAX_GROUP_NAME chars as '$groupname'");

	    if (@same_group) {
		die "You must specify a groupname in the database because there are other domains with same default shortened group '$groupname': @other_domains";
	    }
	    else {
		# warning("no other groups match '$groupname':  '@other_groups'");
	    }
	}
    }
    else {
	$groupname = $dn;
	$groupname =~ s/\./-/g;
    }
    return $groupname;
}

sub ensure_dir {
    my ($dirname, $what, $group_owner, $mode, $user_owner) = @_;

    # mkdir
    if (-d $dirname) {
	push_noop("$what '$dirname' already exists");
    }
    else {
	push_command("mkdir $dirname", "create $what '$dirname'");
    }
    
    # group owner
    # we don't care about user (root ok and typical)
    # but we do have to set group
    #    chown loginname.smokers $GROUPROOT/smokers
    # or chgrp smokers $GROUPROOT/smokers . make owned by a member of the group
    my $st = stat($dirname);
    my $oct_perms = '';
    if ($st) {
	my $perms = $st->mode() & 07777;
	$oct_perms = sprintf "%.5lo", $perms;
    }
    my $gid = $st ? $st->gid() : undef;
    my $current_group_owner = defined($gid) ? getgrgid($gid) : '';
    if ($current_group_owner eq $group_owner) {
	push_noop("$what '$dirname' already has group owner '$group_owner'");
    }
    else {
	push_command("chgrp $group_owner $dirname", "make $what '$dirname' have group owner '$group_owner'" . 
		     ($current_group_owner ? " (was '$current_group_owner')" : " (directory just created)"));
    }

    # user owner
    if ($user_owner) {
	my $uid = $st ? $st->uid() : undef; # $uid might be 0
	my $current_user_owner = defined($uid) ? getpwuid($uid) : '';
	if ($current_user_owner eq $user_owner) {
	    push_noop("$what '$dirname' already has user owner '$user_owner'");
	}
	else {
	    push_command("chown $user_owner $dirname", "make $what '$dirname' have user owner '$user_owner'" . 
			 ($current_user_owner ? " (was '$current_user_owner')" : " (directory just created)"));
	}
    }

    # permissions
    if ($oct_perms eq $mode) {
	push_noop("$what '$dirname' already has mode '$mode'");
    }
    else {
	push_command("chmod $mode $dirname", "make $what '$dirname' have mode '$mode'" . 
		     ($oct_perms ? " (was '$oct_perms')" : " (directory just created)"));
   }
}

################################################################
sub setup_user {
    my ($un, $password, $domains) = @_;

    $domains ||= [];
    my $groups = [map {domain2group($_)} @$domains];

    # on freebsd, see "pw" and "chpass"
    #   pw useradd -n test -c "Test User" -d $USERROOT/test -s /bin/sh
    #   chpass -p $md5encpass test
    # or to supply password to useradd:
    #   echo unf | pw useradd testuser -h 0

    # perl -e 'print crypt("plaintext", "\$1\$saltxxxx"), "\n"'
    # Where `saltxxx' should typically be 8 random characters from the set [./0-9A-Za-z]

    start_category("unix user");
    if (user_exists($un)) {
	push_noop("user '$un' already exists");
    }
    else {
	my $initialgroups = @$groups ? " -G " . join(',',@$groups) : '';
	push_command("/usr/sbin/useradd -m$initialgroups $un", "add user '$un'");
    }
	
    start_category("password");
    if ($password) {
	push_command("echo '$password' | /usr/bin/passwd --stdin $un", "set password for user '$un'");
    }
    else {
	push_noop("no new password provided to set for user '$un'");
    }

    start_category("unix group membership");
    # if any new groups add them (unless already added) 
    for(@$groups) {
	if (user_in_group($un, $_)) {
	    push_noop("user '$un' already in group '$_'");
	}
	else {
	    push_command("/usr/bin/gpasswd -a $un $_", "add user '$un' to group '$_'");
	}
    }

    # make sure they have union mounts for domains they are members of, so they can be chrooted
    start_category("mount group directories for ftp chroot");
    # TODO: how ensure happens again on reboot?
    if (should_mount_domains($un)) {
	my $member_domains = get_user_domains($un, $domains);
	for (@$member_domains) {
	    if (-d "$USERROOT/$un/$_") {
		push_noop("directory for union bind of domain '$_' already exists for user '$un'");
	    }
	    else {
		push_command("mkdir $USERROOT/$un/$_", "create '$un' user directory for union bind of domain '$_'");
	    }

	    if (-d "$USERROOT/$un/$_" && `ls $USERROOT/$un/$_`) {
		push_noop("union bind for domain '$_' and user '$un' already done");
	    }
	    else {
		push_command("mount --bind $GROUPROOT/$_/public_html $USERROOT/$un/$_", "union bind the domain '$_' directory for user '$un'");
	    }
	}
	if (!@$member_domains) {
	    warning("user '$un' is not a member of any domains");
	}
    }
    else {
	push_noop("user $un does not require mounted domains");
    }
}

################################################################
sub setup_domain {
    my ($dn) = @_;

    start_category("unix group");
    # make a group by the almost same name ("foo.com" becomes "foo-com")
    my $groupname = $OPTS->{group} || domain2group($dn);
    my $groupdef = `grep $groupname /etc/group`;
    chomp($groupdef);
    if ($groupdef) {
	push_noop("group '$groupname' already defined: '$groupdef'");
    }
    else {
	push_command("/usr/sbin/groupadd $groupname", "add group '$groupname'");
    }

    start_category("group directory");
    # directory is named identically after domain
    my $dirname = "$GROUPROOT/$dn";
    ensure_dir($dirname, 'group directory', $groupname, '02775');

    start_category("group docroot");
    my $docroot = "$GROUPROOT/$dn/public_html";
    ensure_dir($docroot, 'group docroot', $groupname, '02775');

    start_category("apache log directory");
    my $logdir = "$GROUPROOT/$dn/logs";
    ensure_dir($logdir, 'group apache log dir', $groupname, '02775', 'apache');

    # create a per-domain apache config file in /etc/httpd/conf.d/ or insert in /etc/httpd/conf/httpd.conf
    start_category("apache conf");
    my $conffile = "$APACHE_CONF_D/$dn.conf";

    # TODO: find some way to check for match to template (so for example it has CustomLog directives) 

    # check if in main conf file
    my $main_conf_match = `grep $dn $APACHE_CONF_MAIN 2>/dev/null`;
    $main_conf_match =~ s/#.*//g; $main_conf_match =~ s/^\s*\n//mg;
    my ($first_match) = ($main_conf_match =~ m/(.*)/);
    if ($first_match) {
	push_noop("apache conf file $APACHE_CONF_MAIN already has an entry: $first_match");
    }
    elsif (-f $conffile && -s $conffile) {
	push_noop("apache conf file $conffile already exists");
    }
    else {
	push_command("$HELPERDIR/subfile.pl DOMAIN=$dn $APACHE_CONF_SKEL > $conffile", "create apache conf file $conffile");
    }

    # TODO: www.reportmagic.org instead of awstats
    # make sure that awstats knows about it.
    # see http://awstats.sourceforge.net/docs/awstats_security.html
    # for awstats:
    #     create a /etc/awstats/awstats.$dn.conf (processed by daily cron "/usr/local/awstats/tools/awstats_updateall.pl now")
    # could also do: http://apscuhuru.org/awstats/awstats.pl?config=apscuhuru.org
    # but then have to copy (or sym link) entire install just to have access control. even then how prevent other domains being used?
    # so we do batch conversion, run from /etc/cron.daily/00awstats, so add a suitable line to it:
    #   /usr/local/awstats/tools/awstats_buildstaticpages.pl -config=apacam.org -dir=$GROUPROOT/apacam.org/public_html/awstatic
    # after having created config, run this once:
    #  /usr/local/awstats/wwwroot/cgi-bin/awstats.pl -config=apacam.org -update     
    if (0) {
	my $awdir = "$docroot/awstatic";
	ensure_dir($awdir, 'group website awstats dir', $groupname, '02775');
    }
    # todo: make a .htaccess and htpasswd file?
    # NOTE: we already have a .htaccess for the master awstats.pl program 
}

################################################################
sub setup_list {
    my ($listemail, $listwwwdomain, $owner_email, $list_pass) = @_;
    my ($email_local, $email_domain) = ($listemail =~ m/(.*)\@(.*)/);
    if (!$email_domain) {
	die "list email '$listemail' not a full email address";
    }

    # ensure virtual host mapping in mm_cfg.py
    my @virtualhostlines = `grep add_virtualhost $MAILMAN_CFG`;
    my %virtual_hosts = ();
    for(@virtualhostlines) {
	next if m/^\s*#/;
	if (m/add_virtualhost\(\s*[\"\'](.*?)[\"\']\s*,\s*[\"\'](.*?)[\"\']\s*\)/) {
	    $virtual_hosts{$1} = $2;
	}
	else {
	    warn("unparsed line in $MAILMAN_CFG : $_");
	}
    }
    my $already_email_domain = $virtual_hosts{$listwwwdomain};
    if ($already_email_domain) {
	if ($already_email_domain ne $email_domain) {
	    warn("list www domain $listwwwdomain already has email domain $already_email_domain, != $email_domain");
	    # could fix with: bin/withlist -l -r fix_url mylistname -u my_url_here
	}
	else {
	    push_noop("list www domain $listwwwdomain already has email domain $already_email_domain");
	}
    }
    else {
	push_command("$HELPERDIR/insertfile.pl $MAILMAN_CFG afterline add_virtualhost -- \"add_virtualhost('$listwwwdomain','$email_domain')\"", "declare mailman virtual host mapping");
    }
    
    # create the list
    my @existing_lists = `$MAILMAN_BIN/list_lists | grep -v found:`;
    if (grep {m/$email_local/} @existing_lists) {
	push_noop("list '$email_local' already exists");
    }
    else {
	my $cmd = "$MAILMAN_BIN/newlist -q $listemail";
	$cmd .= " $owner_email" if $owner_email;
	$cmd .= " $list_pass" if $list_pass;
	push_command($cmd, "create new mailman list");

	# newlist creates a list with config "real_name" defaulted to 'Ucfirst', and no value for "description"
	# you can't pipe into config_list, you can only use "-" for -o, not -i.
	# but anyhow, there are so many things to configure. the main benefit of this functionality
	# is it does what the mailman utils don't do well: alter aliases, and set up virtual mappings.
    }

    # make sure entries are in /etc/aliases
    die "no such aliases file $SMTP_INCOMING_ALIASES" unless -f $SMTP_INCOMING_ALIASES;
    if (`grep $email_local: $SMTP_INCOMING_ALIASES`) {
	push_noop("list '$email_local' already has aliases established");
    }
    else {
	push_command("$HELPERDIR/subfile.pl LISTNAME=$email_local $MAILMAN_ALIASES_SKEL | $HELPERDIR/insertfile.pl $SMTP_INCOMING_ALIASES append -", "establish incoming aliases for list '$email_local'");
    }

    # make sure incoming domain exists in rcpthosts
    die "no such rcpthosts file $SMTP_INCOMING_RCPTHOSTS" unless -f $SMTP_INCOMING_RCPTHOSTS;
    if (`grep $email_domain $SMTP_INCOMING_RCPTHOSTS`) {
	push_noop("list email domain '$email_domain' already in rcpthosts");
    }
    else {
	push_command("echo $email_domain >> $SMTP_INCOMING_RCPTHOSTS", "add list email domain '$email_domain' to rcpthosts file"); 
    }
}

################################################################
sub usage {
    my ($err) = @_;
    print STDERR "ERROR: $err\n";
    print STDERR <<EOF;
Usage: $0 [options] command arg1 ...
options are:
   -doit=1        actually make the changes
   -group=what    force the groupname used for a domain
commands are:
   domain foo.org
   user fred freddpassword domain1.org domain2.org ...
   list listemail listwwwdomain owneremail listpass
EOF
    exit(1);
}
 
sub main {
    while(@ARGV) {
	$_ = shift @ARGV;
	$OPTS->{$1} = $2, next if m/^-(.*)=(.*)$/;
	usage("bad argument '$_'") if m/^-/;
	unshift(@ARGV, $_), last; 
    }

    my $cmd = shift @ARGV || usage("no command supplied");
    if ($cmd eq 'domain') {
	my $dn = shift @ARGV || usage("no domain supplied"); 
	setup_domain($dn);
    }
    elsif ($cmd eq 'user') {
	my $username = shift @ARGV || usage("no user supplied"); 
	my $password = shift @ARGV;
	my @domains = @ARGV;
	setup_user($username, $password, \@domains);
    }
    elsif ($cmd eq 'list') {
	my ($listemail, $listwwwdomain, $owner_email, $list_pass) = @ARGV;
	# mailman newlist command requires all 4 args
	usage("no listwwwdomain supplied") unless $list_pass;
	setup_list($listemail, $listwwwdomain, $owner_email, $list_pass);
    }
    else {usage("bad command '$cmd'");}

    print "commands:\n", join("\n", @COMMANDS), "\n";
    if ($OPTS->{doit}) {
	for my $cmd (@COMMANDS) {
	    next if $cmd =~ m/^#/;
	    print "about to run: $cmd\n";
	    my $status = system($cmd);
	    # my $status = 0;
	    die "Command '$cmd' failed with exitcode=", ($status>>8), " and signals=", ($status & 127), " and errno=$!\n" if $status;
	}
    }
}
 
main();

