#!/usr/bin/perl -w use strict; ################################################################################ # Configuration ################################################################################ # Location of virtual folders my $maildirs = '/var/mail/virtual'; # Courier config directory my $etc_courier = '/usr/local/etc/courier'; # A skeleton directory for virtual users my $skeldir = "$etc_courier/skel"; # The owner of the virtual directories my $courier_user = 'courier'; my $courier_group = 'courier'; # MySQL Auth config file my $authmysql = "$etc_courier/authmysqlrc"; my $hosteddomains = "$etc_courier/hosteddomains"; my $makehosteddomains = "/usr/local/sbin/makehosteddomains"; my $esmtpacceptmailfor = "$etc_courier/esmtpacceptmailfor"; my $makeacceptmailfor = "/usr/local/sbin/makeacceptmailfor"; my $courier_rc = "/usr/local/etc/rc.d/courier.sh"; my $logindomainlist = "$etc_courier/logindomainlist"; # SpamAssassin options my $use_sa = 1; # 1 => yes; 0 => no my $sa_conf = '/usr/local/etc/mail/spamassassin/local.cf'; # Users's quota my $quota = "20000000S"; # SquirrelMail options my $sm_config = '/usr/local/squirrelmail-live/config/config.php'; # Use SM address book my $use_sm_address = 1; # 1 => yes; 0 => no # Use SM prefs my $use_sm_userpref = 1; # 1 => yes; 0 => no # Location of pod2usage (pod2text may be used instead) my $pod2usage = '/usr/local/bin/pod2usage'; # Mailman options my $use_mailman = 1; my $mm_dir = '/usr/local/mailman'; my $mm_newlist = "$mm_dir/bin/newlist"; my $mm_rmlist = "$mm_dir/bin/rmlist"; my $mm_mailman = "$mm_dir/mail/mailman"; my $mm_domain = 'lists.amigo.net'; ################################################################################ # Code - Nothing to see here. Move along ################################################################################ use DBI; # Keys are the XXX in MYSQL_XXX from authmysql my %db_conf = (SERVER => 'localhost', USERNAME => 'courier', PASSWORD => '', SOCKET => '/tmp/mysql.sock', PORT => '3306', DATABASE => '', USER_TABLE => '', CRYPT_PWFIELD => '', CLEAR_PWFIELD => '', UID_FIELD => '', GID_FIELD => '', LOGIN_FIELD => '', HOME_FIELD => '', NAME_FIELD => '', MAILDIR_FIELD => '', QUOTA_FIELD => '', ALIAS_FIELD => 'alias_for' ); my %sa_conf = (dsn => '', sql_username => '', sql_password => '', sql_table => '' ); my %sm_address_conf = (dsn => '', username => '', password => '', table => '' ); my %sm_userpref_conf = (dsn => '', username => '', password => '', table => '', user_field => '', key_field => '', val_field => '' ); open (AUTHMYSQL, $authmysql) or die "Can't open $authmysql: $!\n"; while (my $line = ) { chomp $line; next if ($line =~ /^\s*#/ or $line =~ /^\s*$/ ); # Skip comments and blank lines. $line =~ s/\s*$//; # Strip extra ws from the end if ($line =~ /^\s*MYSQL_(\w+) # Get MYSQL_XXX. XXX -> $1 \s+(.+)$/x) # Get value. -> $2 { $db_conf{$1} = $2; } } close AUTHMYSQL; if ($use_sa) # If we're using SpamAssassin { # Read SA conf file open (SA, $sa_conf) or die "Can't open $sa_conf: $1\n"; while (my $line = ) { chomp $line; next if ($line =~ /^\s*#/ # Skip comments ... or $line =~ /^\s*$/); # ... and blank lines $line =~ s/\s*$//; # Strip extra ws from the end if ($line =~ /^\s*user_scores_(\w+) # Get user_scores_* variables -> $1 \s+(.+)/x) # and values -> $2 { $sa_conf{$1} = $2; } } close SA; } # Get SquirrelMail settings if ($use_sm_address or $use_sm_userpref) { open (SM, $sm_config) or die "Can't open SquirrelMail config '$sm_config': $!\n"; while (my $line = ) { chomp $line; if ($line =~ /^\s*\$addrbook_dsn\s*=\s*'(.*)';/) { my $dsn = $1; my ($type, $user, $pass, $host, $db) = $dsn =~ m|^(.*?)://(.*?):(.*?)\@(.*?)/(.*)$|; @sm_address_conf{ qw(dsn username password) } = ( "DBI:$type:$db:$host", $user, $pass ); } elsif ($line =~ /^\s*\$addrbook_table\s*=\s*'(.*)';$/) { $sm_address_conf{table} = $1; } elsif ($line =~ /^\s*\$prefs_dsn\s*=\s*'(.*)';*/) { my $dsn = $1; my ($type, $user, $pass, $host, $db) = $dsn =~ m|^(.*?)://(.*?):(.*?)\@(.*?)/(.*)$|; @sm_userpref_conf{ qw(dsn username password) } = ( "DBI:$type:$db:$host", $user, $pass ); } elsif ($line =~ /^\s*\$prefs_table\s*=\s*'(.*)';$/) { $sm_userpref_conf{table} = $1; } elsif ($line =~ /^\s*\$prefs_user_field\s*=\s*'(.*)';$/) { $sm_userpref_conf{user_field} = $1; } elsif ($line =~ /^\s*\$prefs_key_field\s*=\s*'(.*)';$/) { $sm_userpref_conf{key_field} = $1; } elsif ($line =~ /^\s*\$prefs_val_field\s*=\s*'(.*)';$/) { $sm_userpref_conf{val_field} = $1; } } close SM; #use Data::Dumper; print Dumper %sm_address_conf; #use Data::Dumper; print Dumper %sm_userpref_conf; } # Connect to courier accounts DB my $cdb = DBI->connect ("DBI:mysql:$db_conf{DATABASE}:$db_conf{SERVER}:$db_conf{PORT}", $db_conf{USERNAME}, $db_conf{PASSWORD}); die "Can't connect to Courier DB: $DBI::errstr\n" unless $cdb; $cdb->do("use $db_conf{DATABASE}"); # Connect to SpamAssassin DB if we need to. my $sadb; if ($use_sa) { $sadb = DBI->connect ($sa_conf{dsn}, $sa_conf{sql_username}, $sa_conf{sql_password}); die "Can't connect to SpamAssassin DB: $DBI::errstr\n" unless $sadb; $sadb->do("use $db_conf{DATABASE}"); } # Connect to SM address DB, if needed. my $sm_addr_db; if ($use_sm_address) { $sm_addr_db = DBI->connect ($sm_address_conf{dsn}, $sm_address_conf{username}, $sm_address_conf{password}); die "Can't connect to SquirrelMail (Address) DB: $DBI::errstr\n" unless $sm_addr_db; } # Connect to SM userpref DB, if needed. my $sm_pref_db; if ($use_sm_userpref) { $sm_pref_db = DBI->connect ($sm_userpref_conf{dsn}, $sm_userpref_conf{username}, $sm_userpref_conf{password}); die "Can't connect to SquirrelMail (Userpref) DB: $DBI::errstr\n" unless $sm_pref_db; } # Ok. All the setup work has been done. Let's do some real work. if (@ARGV < 2) { help(); exit 0 }; my $cmd = shift @ARGV; $cmd = lc $cmd; if ($cmd eq 'add') { add(@ARGV); } elsif ($cmd eq 'del') { del(@ARGV); } elsif ($cmd eq 'mod') { mod(@ARGV); } elsif ($cmd eq 'show') { show(@ARGV); } elsif ($cmd eq 'alias') { alias(@ARGV); } elsif ($cmd eq 'domain'){ domain(@ARGV); } elsif ($cmd eq 'list') { list(@ARGV); } elsif ($cmd eq 'help' or $cmd eq '-h') { help(@ARGV); } else { help(); } exit 0; sub add { my ($account, @params) = @_; die "No account specified\n" if not $account; my ($user, $domain) = $account =~ m/^(\S+?)\@(\S+)$/; $domain = 'default' if not $domain; $domain = lc($domain); # Get other params. my $name = ''; my $password = ''; for (my $i = 0; $i < @params; $i++) { if ($params[$i] eq '-n') { $name = $params[$i+1]; } elsif ($params[$i] eq '-p') { $password = $params[$i+1]; } } $password = generate_password(10) if (not $password); my $XY = substr($user, 0, 2); # Get the first 2 chars of $user if (not -e "$maildirs/$domain") { mkdir ("$maildirs/$domain", 0755) or die "Can't create directory $maildirs/$domain: $!\n"; chown_ug($courier_user, $courier_group, "$maildirs/$domain"); } if (not -e "$maildirs/$domain/$XY") { mkdir ("$maildirs/$domain/$XY", 0755) or die "Can't create directory $maildirs/$domain/$XY: $!\n"; chown_ug($courier_user, $courier_group, "$maildirs/$domain/$XY"); } die "Can't add account: User exists\n" if (-e "$maildirs/$domain/$XY/$user"); my $rc = 0xffff & system ('cp', '-R', $skeldir, "$maildirs/$domain/$XY/$user"); $rc <<= 8; die "Can't copy skel dir to $maildirs/$domain/$XY/$user: $!\n" if $rc != 0; system('chown', '-R', "$courier_user:$courier_group", "$maildirs/$domain/$XY/$user"); # Add the user to MySQL my $sql = "INSERT into $db_conf{USER_TABLE} set "; $sql .= " $db_conf{LOGIN_FIELD} = ".$cdb->quote($account); $sql .= ", $db_conf{UID_FIELD} = ".$cdb->quote((getpwnam($courier_user))[2]); $sql .= ", $db_conf{GID_FIELD} = ".$cdb->quote((getgrnam($courier_group))[2]); $sql .= ", $db_conf{CRYPT_PWFIELD} = ".$cdb->quote(crypt($password, $password)) if $db_conf{CRYPT_PWFIELD}; $sql .= ", $db_conf{CLEAR_PWFIELD} = ".$cdb->quote($password) if $db_conf{CLEAR_PWFIELD}; $sql .= ", $db_conf{HOME_FIELD} = ".$cdb->quote("$maildirs/$domain/$XY/$user"); $sql .= ", $db_conf{NAME_FIELD} = ".$cdb->quote($name); $sql .= ", $db_conf{QUOTA_FIELD} = ".$cdb->quote($quota); $sql .= ";"; $cdb->do($sql) or die "Can't add account: ".$cdb->errstr()."\n"; } sub del { my ($account, @params) = @_; die "No account specified\n" if not $account; my ($user, $domain) = $account =~ m/^(\S+?)\@(\S+)$/; $domain = 'default' if not $domain; $domain = lc($domain); my $XY = substr($user, 0, 2); # Get the first 2 chars of $user # delete aliases first alias_del($account); system ('rm', '-r', "$maildirs/$domain/$XY/$user"); my $sql = "DELETE from $db_conf{USER_TABLE} where $db_conf{LOGIN_FIELD} = ".$cdb->quote($account).";"; $cdb->do($sql) or die "Can't delete account: ".$cdb->errstr()."\n"; # If we're using SpamAssassin, we're going to have to clean out those prefs as well. if ($use_sa) { my $sql = "DELETE from $sa_conf{sql_table} where username = ".$sadb->quote($account).";"; $sadb->do($sql) or die "Can't delete account SpamAssassin preferences: ".$sadb->errstr()."\n"; } # Delete SquirrelMail address book, if used if ($use_sm_address) { my $sql = "DELETE from $sm_address_conf{table} where owner = ".$sm_addr_db->quote($account).";"; $sm_addr_db->do($sql) or die "Can't delete account SquirrelMail address book: ".$sm_addr_db->errstr()."\n"; } # Delete SquirrelMail preferences, if used if ($use_sm_userpref) { my $sql = "DELETE from $sm_userpref_conf{table} where $sm_userpref_conf{user_field} = ".$sm_pref_db->quote($account).";"; $sm_pref_db->do($sql) or die "Can't delete account SquirrelMail preferences: ".$sm_pref_db->errstr()."\n"; } } sub mod { my ($account, @params) = @_; die "No account specified\n" if not $account; my ($user, $domain) = $account =~ m/^(\S+?)\@(\S+)$/; $domain = 'default' if not $domain; $domain = lc($domain); # Get other params. my $name = ''; my $password = ''; my $new_acct = ''; for (my $i = 0; $i < @params; $i++) { if ($params[$i] eq '-n') { $name = $params[$i+1]; } elsif ($params[$i] eq '-p') { $password = $params[$i+1]; } elsif ($params[$i] eq '-a') { $new_acct = $params[$i+1]; } } my ($n_user, $n_domain) = $new_acct =~ m/^(\S+?)\@(\S+)/ if $new_acct; my $XY = substr($user, 0, 2); # Get the first 2 chars of $user my $newXY = substr($n_user, 0, 2) if $n_user; # Get the first 2 chars of $n_user my $sql = ''; if ($name) { $sql = "UPDATE $db_conf{USER_TABLE} set $db_conf{NAME_FIELD} = ".$cdb->quote($name); $sql .= " where $db_conf{LOGIN_FIELD} = ".$cdb->quote($account).";"; $cdb->do($sql) or warn "Can't change name: ".$cdb->errstr()."\n"; } if ($password) { $sql = "UPDATE $db_conf{USER_TABLE} set"; $sql .= " $db_conf{CRYPT_PWFIELD} = ".$cdb->quote(crypt($password, $password)) if $db_conf{CRYPT_PWFIELD}; $sql .= "," if ($db_conf{CRYPT_PWFIELD} and $db_conf{CLEAR_PWFIELD}); $sql .= " $db_conf{CLEAR_PWFIELD} = ".$cdb->quote($password) if $db_conf{CLEAR_PWFIELD}; $sql .= " where $db_conf{LOGIN_FIELD} = ".$cdb->quote($account).";"; $cdb->do($sql) or warn "Can't change password: ".$cdb->errstr()."\n"; } if ($new_acct) { die "Can't rename account: User exists\n" if (-e "$maildirs/$n_domain/$newXY/$n_user"); $sql = "UPDATE $db_conf{USER_TABLE} set $db_conf{LOGIN_FIELD} = ".$cdb->quote($new_acct); $sql .= " , $db_conf{HOME_FIELD} = ".$cdb->quote("$maildirs/$n_domain/$newXY/$n_user"); $sql .= " where $db_conf{LOGIN_FIELD} = ".$cdb->quote($account).";"; $cdb->do($sql) or die "Can't change account name: ".$cdb->errstr()."\n"; # We need to change the aliases as well. $sql = "UPDATE $db_conf{USER_TABLE} set $db_conf{ALIAS_FIELD} = ".$cdb->quote($new_acct); $sql .= " where $db_conf{ALIAS_FIELD} = ".$cdb->quote($account).";"; $cdb->do($sql) or die "Can't update aliases to reflect account name change: ".$cdb->errstr()."\n"; if (not -e "$maildirs/$n_domain") { mkdir ("$maildirs/$n_domain", 0755) or die "Can't create directory $maildirs/$n_domain: $!\n"; chown_ug($courier_user, $courier_group, "$maildirs/$n_domain"); } if (not -e "$maildirs/$n_domain/$newXY") { mkdir ("$maildirs/$n_domain/$newXY", 0755) or die "Can't create directory $maildirs/$n_domain/$newXY: $!\n"; chown_ug($courier_user, $courier_group, "$maildirs/$n_domain/$newXY"); } system ('mv', "$maildirs/$domain/$XY/$user", "$maildirs/$n_domain/$newXY/$n_user"); # We'll need to update the SpamAssassin preferences as well. if ($use_sa) { # Note: table names are hardcoded into SA and are not (easily) configurable. $sql = "UPDATE $sa_conf{sql_table} set username = ".$sadb->quote($new_acct); $sql .= " where username = ".$sadb->quote($account); $sadb->do($sql) or die "Can't update SpamAssassin preferences: ".$sadb->errstr()."\n"; } # ... and SM address book if ($use_sm_address) { # Note: The column names are hardcoded and not easy to change. my $sql = "UPDATE $sm_address_conf{table} set owner = ".$sm_addr_db->quote($new_acct); $sql .= " where owner = ".$sm_addr_db->quote($account); $sm_addr_db->do($sql) or die "Can't update SquirrelMail address book: ".$sm_addr_db->errstr()."\n"; } # ... and SM prefs if ($use_sm_userpref) { my $sql = "UPDATE $sm_userpref_conf{table} set $sm_userpref_conf{user_field} = ".$sm_pref_db->quote($new_acct); $sql .= " where $sm_userpref_conf{user_field} = ".$sm_pref_db->quote($account); $sm_pref_db->do($sql) or die "Can't update SquirrelMail preferences: ".$sm_pref_db->errstr()."\n"; } } } sub show { my ($account, @params) = @_; my $sql = "SELECT * from $db_conf{USER_TABLE} where $db_conf{LOGIN_FIELD} LIKE ".$cdb->quote($account).";"; my $sth = $cdb->prepare($sql) or die "Can't access DB: ".$cdb->errstr()."\n"; $sth->execute() or die "Can't access DB: ".$sth->errstr()."\n"; while (my $acct = $sth->fetchrow_hashref()) { print "$acct->{$db_conf{LOGIN_FIELD}}"; print ":$acct->{$db_conf{CRYPT_PWFIELD}}" if $db_conf{CRYPT_PWFIELD}; print ":$acct->{$db_conf{CLEAR_PWFIELD}}" if $db_conf{CLEAR_PWFIELD}; print ":$acct->{$db_conf{UID_FIELD}}"; print ":$acct->{$db_conf{GID_FIELD}}"; print ":$acct->{$db_conf{NAME_FIELD}}"; print ":$acct->{$db_conf{HOME_FIELD}}"; print ":$acct->{$db_conf{MAILDIR_FIELD}}" if $db_conf{MAILDIR_FIELD}; print ":$acct->{$db_conf{QUOTA_FIELD}}" if $db_conf{QUOTA_FIELD}; print "\n"; } $sth->finish(); } sub alias { my ($alias_cmd, @params) = @_; if ($alias_cmd eq 'add') { alias_add (@params); } elsif ($alias_cmd eq 'del') { alias_del (@params); } elsif ($alias_cmd eq 'mod') { alias_mod (@params); } elsif ($alias_cmd eq 'show'){ alias_show(@params); } else { alias_help(@params); } } sub alias_add { my ($account, $alias, @params) = @_; if (not defined $account or not defined $alias) { alias_help(); exit; } my $name = ''; my $password = ''; for (my $i = 0; $i < @params; $i++) { if ($params[$i] eq '-n') { $name = $params[$i+1]; } elsif ($params[$i] eq '-p') { $password = $params[$i+1]; } } # Get account info. my $sql = "SELECT * from $db_conf{USER_TABLE} where $db_conf{LOGIN_FIELD} = ".$cdb->quote($account).";"; my $sth = $cdb->prepare($sql) or die "Can't access DB: ".$cdb->errstr()."\n"; $sth->execute() or die "Can't access DB: ".$cdb->errstr()."\n"; my $acct = $sth->fetchrow_hashref(); die "Unknown account $account.\n" if (ref $acct ne 'HASH'); $name = $acct->{$db_conf{NAME_FIELD}} unless $name; $sql = "INSERT into $db_conf{USER_TABLE} set "; $sql .= " $db_conf{LOGIN_FIELD} = ".$cdb->quote($alias); $sql .= ", $db_conf{UID_FIELD} = ". $cdb->quote((getpwnam($courier_user))[2]); $sql .= ", $db_conf{GID_FIELD} = ".$cdb->quote((getgrnam($courier_group))[2]); if ($password) { $sql .= ", $db_conf{CRYPT_PWFIELD} = ".$cdb->quote(crypt($password, $password)) if $db_conf{CRYPT_PWFIELD}; $sql .= ", $db_conf{CLEAR_PWFIELD} = ".$cdb->quote($password) if $db_conf{CLEAR_PWFIELD}; } else { $sql .= ", $db_conf{CRYPT_PWFIELD} = ".$cdb->quote($acct->{$db_conf{CRYPT_PWFIELD}}) if $db_conf{CRYPT_PWFIELD}; $sql .= ", $db_conf{CLEAR_PWFIELD} = ".$cdb->quote($acct->{$db_conf{CLEAR_PWFIELD}}) if $db_conf{CLEAR_PWFIELD}; } $sql .= ", $db_conf{HOME_FIELD} = ".$cdb->quote($acct->{$db_conf{HOME_FIELD}}); $sql .= ", $db_conf{NAME_FIELD} = ".$cdb->quote($name); $sql .= ", $db_conf{QUOTA_FIELD} = ".$cdb->quote($acct->{$db_conf{QUOTA_FIELD}}); $sql .= ", $db_conf{ALIAS_FIELD} = ".$cdb->quote($account); $sql .= ";"; $cdb->do($sql) or die "Can't add account: ".$cdb->errstr()."\n"; } sub alias_del { my ($account, $alias, @params) = @_; if (not defined $account) { alias_help(); exit; } $alias = '%' unless $alias; my $sql = "DELETE from $db_conf{USER_TABLE} where $db_conf{LOGIN_FIELD} LIKE ".$cdb->quote($alias); $sql .= " and $db_conf{ALIAS_FIELD} = ".$cdb->quote($account).";"; $cdb->do($sql) or die "Can't delete alias(es) for account: ".$cdb->errstr()."\n"; # Remove alias settings in SpamAssassin if it's being used. if ($use_sa) { $sql = "DELETE from $sa_conf{sql_table} where username = ".$sadb->quote($alias).";"; $sadb->do($sql) or die "Can't delete alias SpamAssassin preferences: ".$sadb->errstr()."\n"; } } sub alias_mod { my ($account, $alias, @params) = @_; if (not defined $account or not defined $alias) { alias_help(); exit; } my $name = ''; my $password = ''; my $new_alias = ''; for (my $i = 0; $i < @params; $i++) { if ($params[$i] eq '-n') { $name = $params[$i+1]; } elsif ($params[$i] eq '-p') { $password = $params[$i+1]; } elsif ($params[$i] eq '-a') { $new_alias = $params[$i+1]; } } # Get account info. my $sql = "SELECT * from $db_conf{USER_TABLE} where $db_conf{LOGIN_FIELD} = ".$cdb->quote($account).";"; my $sth = $cdb->prepare($sql) or die "Can't access DB: ".$cdb->errstr()."\n"; $sth->execute() or die "Can't access DB: ".$cdb->errstr()."\n"; my $acct = $sth->fetchrow_hashref(); die "Unknown account $account.\n" if (ref $acct ne 'HASH'); $name = $acct->{$db_conf{NAME_FIELD}} unless $name; $password = generate_password(10) unless $password; $sql = ''; if ($name) { $sql = "UPDATE $db_conf{USER_TABLE} set $db_conf{NAME_FIELD} = ".$cdb->quote($name); $sql .= " where $db_conf{LOGIN_FIELD} = ".$cdb->quote($alias).";"; $cdb->do($sql) or warn "Can't change name: ".$cdb->errstr()."\n"; } if ($password) { $sql = "UPDATE $db_conf{USER_TABLE} set"; $sql .= " $db_conf{CRYPT_PWFIELD} = ".$cdb->quote(crypt($password, $password)) if $db_conf{CRYPT_PWFIELD}; $sql .= "," if ($db_conf{CRYPT_PWFIELD} and $db_conf{CLEAR_PWFIELD}); $sql .= " $db_conf{CLEAR_PWFIELD} = ".$cdb->quote($password) if $db_conf{CLEAR_PWFIELD}; $sql .= " where $db_conf{LOGIN_FIELD} = ".$cdb->quote($alias).";"; $cdb->do($sql) or warn "Can't change password: ".$cdb->errstr()."\n"; } if ($new_alias) { $sql = "UPDATE $db_conf{USER_TABLE} set "; $sql .= " $db_conf{LOGIN_FIELD} = ".$cdb->quote($new_alias); $sql .= " where $db_conf{ALIAS_FIELD} = ".$cdb->quote($account); $sql .= " and $db_conf{LOGIN_FIELD} = ".$cdb->quote($alias); $sql .= ";"; $cdb->do($sql) or die "Can't add account: ".$cdb->errstr()."\n"; # We need to update the SpamAssassin table if the alias has changed if ($new_alias and $use_sa) { $sql = "UPDATE $sa_conf{sql_table} set username = ".$sadb->quote($new_alias); $sql .= " where username = ".$sadb->quote($alias); $sadb->do($sql) or die "Can't update SpamAssassin preferences: ".$sadb->errstr()."\n"; } } } sub alias_show { my ($account, $alias, @params) = @_; if (not defined $account) { alias_help(); exit; } $alias = '%' if not $alias; my $sql = "SELECT * from $db_conf{USER_TABLE} where $db_conf{LOGIN_FIELD} LIKE ".$cdb->quote($alias); $sql .= " and $db_conf{ALIAS_FIELD} = ".$cdb->quote($account).";"; my $sth = $cdb->prepare($sql) or die "Can't access DB: ".$cdb->errstr()."\n"; $sth->execute() or die "Can't access DB: ".$sth->errstr()."\n"; while (my $alias = $sth->fetchrow_hashref()) { print "$alias->{$db_conf{LOGIN_FIELD}}"; print ":$alias->{$db_conf{CRYPT_PWFIELD}}" if $db_conf{CRYPT_PWFIELD}; print ":$alias->{$db_conf{CLEAR_PWFIELD}}" if $db_conf{CLEAR_PWFIELD}; print ":$alias->{$db_conf{UID_FIELD}}"; print ":$alias->{$db_conf{GID_FIELD}}"; print ":$alias->{$db_conf{NAME_FIELD}}"; print ":$alias->{$db_conf{HOME_FIELD}}"; print ":$alias->{$db_conf{MAILDIR_FIELD}}" if $db_conf{MAILDIR_FIELD}; print ":$alias->{$db_conf{QUOTA_FIELD}}" if $db_conf{QUOTA_FIELD}; print "\n"; } $sth->finish(); } sub alias_help { help(); } sub domain { my ($domain_cmd, @params) = @_; if ($domain_cmd eq 'add') { domain_add (@params); } elsif ($domain_cmd eq 'del') { domain_del (@params); } elsif ($domain_cmd eq 'mod') { domain_mod (@params); } elsif ($domain_cmd eq 'show'){ domain_show(@params); } else { domain_help(@params); } } sub domain_add { my ($domain, @params) = @_; if (not defined $domain) { domain_help(); exit(); } $domain = lc($domain); for (my $i = 0; $i < @params; $i++) { # No params } if (-d "$maildirs/$domain") { die "Can't add $domain: Domain exists\n"; } else { mkdir ("$maildirs/$domain", 0755) or die "Can't create directory $maildirs/$domain: $!\n"; chown_ug($courier_user, $courier_group, "$maildirs/$domain"); } add_line_to_file ($hosteddomains, $domain); add_line_to_file ($esmtpacceptmailfor, $domain); add_line_fo_file ($logindomainlist, $domain) if (-e $logindomainlist); system($makehosteddomains); system($makeacceptmailfor); system($courier_rc, 'restart'); } sub domain_del { my ($domain, @params) = @_; if (not defined $domain) { domain_help(); exit(); } $domain = lc($domain); # Watch out for '..' in the domain name die "Can't delete domain: Bad domiain name '$domain'.\n" if $domain =~/\.\./; # Clean domain out of hostedomainlist and logindomainslist del_line_from_file($hosteddomains, $domain); del_line_from_file($esmtpacceptmailfor, $domain); del_line_from_file($logindomainlist, $domain) if -e $logindomainlist; system ($makehosteddomains); system ($makeacceptmailfor); system ($courier_rc, 'restart'); my $recursive = 0; for (my $i = 0; $i < @params; $i++) { if ($params[$i] eq '-r') { $recursive = 1; } } open (VUSER, "$0 show \"\@$domain\"|") or die "Can't get user list: $!\n"; my $hasaccts = 0; while () { $hasaccts = 1; my ($acct, @junk) = split(':'); if ($recursive) { del($acct); } else { last; } } close VUSER; if (not $recursive and $hasaccts) { die "Can't delete domain: Domain not empty.\n"; } # It should now be safe to delete the domain directory. system ('rm', '-r', "$maildirs/$domain"); } sub domain_mod { my ($domain, @params) = @_; if (not defined $domain) { domain_help(); exit(); } $domain = lc($domain); # Watch out for '..' in the domain name die "Can't delete domain: Bad domiain name '$domain'.\n" if $domain =~/\.\./; my $new_domain = ''; my $recurse = 0; for (my $i = 0; $i < @params; $i++) { if ($params[$i] eq '-d') { $new_domain = $params[$i+1]; } elsif ($params[$i] eq '-r') { $recurse = 1; } } if ($new_domain) { $new_domain = lc($new_domain); if (-d "$maildirs/$new_domain") { die "Can't rename domain to $new_domain: Domain exists.\n"; } repl_line_in_file ($hosteddomains, $domain, $new_domain); repl_line_in_file ($esmtpacceptmailfor, $domain, $new_domain); repl_line_in_file ($logindomainlist, $domain, $new_domain) if -e $logindomainlist; system ($makehosteddomains); system ($makeacceptmailfor); system ($courier_rc, 'restart'); mkdir "$maildirs/$new_domain" or die "Can't create directory $maildirs/$new_domain: $!\n"; chown_ug($courier_user, $courier_group, "$maildirs/$new_domain"); if ($recurse) { my $sql = "SELECT $db_conf{LOGIN_FIELD},$db_conf{HOME_FIELD} from $db_conf{USER_TABLE}"; $sql .= " where $db_conf{LOGIN_FIELD} LIKE ". $cdb->quote("%\@$domain"); $sql .= ";"; my $sth = $cdb->prepare($sql) or die "Can't modify domain: Can't get user list: ".$cdb->errstr."\n"; $sth->execute() or die "Can't modify domain: Can't get user list: ".$sth->errstr."\n"; while (my ($acct, $home) = $sth->fetchrow_array()) { my $new_acct = $acct; $new_acct =~ s/\Q$domain\E$/$new_domain/e; mod($acct, '-a', $new_acct); # Rename account. } $sth->finish; } # Ok, it's now safe to remove the old directory. system ('rm', '-r', "$maildirs/$domain"); } } sub domain_show { my ($domain, @params) = @_; $domain = lc($domain); my @domains = (); if ($domain) { @domains = ($domain); } else { open (HOSTED, $hosteddomains) or die "Can't open $hosteddomains: $!\n"; @domains = ; close HOSTED; chomp @domains; } my $sql = "select count(*) from $db_conf{USER_TABLE}"; $sql .= " where $db_conf{LOGIN_FIELD} LIKE ? and ISNULL($db_conf{ALIAS_FIELD});"; my $sth = $cdb->prepare($sql) or die "Can't prepare SQL: ".$cdb->errstr."\n"; foreach my $dom (sort @domains) { print "$dom:"; print ((-d "$maildirs/$dom") ? "$maildirs/$dom" : ""); print ":"; $sth->execute("%\@$dom") or die "Can't get account count: ".$cdb->errstr."\n"; my @count = $sth->fetchrow_array(); $sth->finish; print "$count[0]\n"; } } sub domain_help { help(); } sub list { die "Mailman is not enabled\n" unless $use_mailman; my ($list_cmd, @params) = @_; if ($list_cmd eq 'add') { list_add (@params); } elsif ($list_cmd eq 'del') { list_del (@params); } elsif ($list_cmd eq 'mod') { list_mod (@params); } else { list_help(@params); } } sub list_add { my ($name, $account, $manager, @params) = @_; die "No name specified\n" if not $name; die "No account specified\n" if not $account; die "No manager specified\n" if not $manager; # Get other params. my $password = ''; for (my $i = 0; $i < @params; $i++) { if ($params[$i] eq '-p') { $password = $params[$i+1]; } } $password = generate_password(10) if (not $password); eval { add($account, '-p', $password, '-n', $name); }; die "Can't create list: $@" if $@; # Get account's homedir so we can add files to it. my $sql = "SELECT $db_conf{HOME_FIELD} from $db_conf{USER_TABLE} where $db_conf{LOGIN_FIELD} = ".$cdb->quote($account); my $sth = $cdb->prepare($sql) or die "Can't access DB: ".$cdb->errstr()."\n"; $sth->execute or die "Can't access DB: ".$sth->errstr()."\n"; if (my $acct = $sth->fetchrow_hashref()) { my $homedir = $acct->{$db_conf{HOME_DIR}}; my @aliases = qw(admin bounces confirm join leave owner request subscribe unsubscribe); open (DOT_COURIER, ">$homedir/.courier") or die "Can't create alias file $homedir/.courier\n"; print DOT_COURIER "|$mm_mailman post $name\n"; close DOT_COURIER; chown_ug ($courier_user, $courier_group, "$homedir/.courier"); foreach my $alias (@aliases) { open (DOT_COURIER, ">$homedir/.courier-$alias") or die "Can't create alias file $homedir/.courier-alias\n"; print DOT_COURIER "|$mm_mailman $alias $name\n"; close DOT_COURIER; chown_ug ($courier_user, $courier_group, "$homedir/.courier-$alias"); } } else { # This should never, ever happen since we *just* created this # account above. die "Egad! $account doesn't exist even though it was just created.\n"; } # Create the list. my @cmd = ($mm_newlist, $name. ($mm_domain? "\@$mm_domain": ''), $manager, $password); my $rc = 0xffff & system (@cmd); $rc <<=8; if ($rc != 0) { del ($account); die "Can't add list $name: $!\n"; } } sub list_del { my ($name, $account, @params) = @_; die "No name specified\n" if not $name; die "No account specified\n" if not $account; # Get other params. my $recursive = 0; for (my $i = 0; $i < @params; $i++) { if ($params[$i] eq '-r') { $recursive = 1; } } # Delete the list. my @cmd = ($mm_rmlist, $name); my $rc = 0xffff & system (@cmd); $rc <<=8; if ($rc != 0) { die "Can't add list $name: $!\n"; } eval { del ($account); }; die "Problem deleting list: $@" if $@; } sub list_mod { my ($name, $account, @params) = @_; die "No name specified\n" if not $name; die "No account specified\n" if not $account; # Get other params. my $new_account = ''; for (my $i = 0; $i < @params; $i++) { if ($params[$i] eq '-a') { $new_account = $params[$i+1]; } } if ($new_account) { eval { mod ($account, '-a', $new_account); }; die "Can't modify list address: $@" if $@; } } sub list_help { help(@_); } sub chown_ug { my ($user, $group, @files) = @_; my $uid = (getpwnam($user))[2]; # Get the numerical user ID my $gid = (getgrnam($group))[2]; # Get the numerical group ID chown $uid, $gid, @files; } sub help { system($pod2usage, $0); } sub generate_password { my $len = shift || 10; my @valid = (0..9, 'a'..'z', 'A'..'Z', '@', '#', '%', '^', '*'); my $password = ''; for (1 .. $len) { $password .= $valid[int (rand $#valid)]; } return $password; } sub add_line_to_file { my ($file, $line) = @_[0,1]; open (FILE, ">>$file") or die "Can't open $file: $!\n"; print FILE "$line\n"; close FILE; } sub del_line_from_file { my ($file, $line) = @_[0,1]; while (-e "$file.tmp") { sleep(rand(int 3)); } open (FILE, $file) or die "Can't open $file: $!\n"; open (TMP, ">$file.tmp") or die "Can't open $file.tmp: $!\n"; while () { chomp; print TMP "$_\n" unless /^\Q$line\E$/; } close FILE; close TMP; rename ("$file.tmp", $file) or die "Can't rename $file.tmp to $file: $!\n"; } sub repl_line_in_file { my ($file, $oline, $nline) = @_[0..2]; while (-e "$file.tmp") { sleep(rand(int 3)); } open (FILE, $file) or die "Can't open $file: $!\n"; open (TMP, ">$file.tmp") or die "Can't open $file.tmp: $!\n"; while () { chomp; if (/^\Q$oline\E$/) { print TMP "$nline\n"; } else { print TMP "$_\n"; } } close FILE; close TMP; rename "$file.tmp", $file or die "Can't rename $file.tmp to $file: $!\n"; } __END__ =head1 NAME vuser - Adds/Removes/Modifies MySQL virtual users for Courier =head1 SYNOPSIS vuser add account [-n name] [-p password] vuser del account vuser mod account [-n name] [-p password] [-a new_account] vuser show account vuser alias add account alias [-n name] [-p password] vuser alias del account [alias] vuser alias mod account alias [-n name] [-p password] [-a new_alias] vuser alias show account vuser domain add domain vuser domain del domain [-r] vuser domain mod domain [-d new_domain] vuser domain show domain vuser list add name account manager [-p password] vuser list del name account [-r] vuser list mod name account [-a new_account] vuser help =head1 DESCRIPTION B handles virtual accounts for courier and has limited support for SpamAssassin and SquirrelMail. In all cases, I is the name of the account to operate on. =head2 add B =over 4 =item -n name The real name for this user. Ex: "John Doe" =item -p password If provided, then the account's password is set to I. Otherwise, the account is assigned a random password. =back =head2 del Delete account =head2 mod B =over 4 =item -n name The real name for this user. Ex: "John Doe" =item -p password If provided, then the account's password is set to I. Otherwise, the password is not changed. =item -a new_account Change the account name to I. =back =head2 show Show account information. I may contain SQL wildcards. =head2 help Display help message. =head2 alias add TODO =head2 alias del TODO =head2 alias mod TODO =head2 alias show TODO =head2 alias help TODO =head2 domain add TODO =head2 domain del B Speecifying C<-r> will delete I accounts in this domain. You have been warned. =head2 domain mod TODO =head2 domain show TODO =head2 domain help TODO =head1 FILES =over 4 =item /usr/local/etc/courier/authmysqlrc Courier's authmysql config file. =item /usr/local/etc/courier/virtual/hosteddomains See L. =item /usr/local/etc/courier/virtual/logindomainlist See L. =item /etc/mail/spamassassin/local.cf SpamAssassin config file. =back =head1 BUGS There is no way to prompt the user for a password for the account and passwords are passed in from the command line. It is possible that someone may be able to see the password if they run B at the right time. It's possible (even likely) that B will connect to the same database twice when SpamAssassin is being used. This is because, in theory, the SpamAssassin DB can be on a seperate system. If it's on the same system, you get two connections. This is particularly true if you are using SquirrelMail too. I should be more careful when parsing the SquirrelMail config file. =head1 AUTHOR Randy Smith =head1 COPYRIGHT Copyright (c) 2002 Randy Smith All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. =cut