#!/usr/bin/perl # $VERSION = 'aliasman.pl v0.2, http://www.mmedia.is/~bre/aliasman/'; # # (C) July 1999, Bjarni R. Einarsson # This program may be used according to the terms of the # GNU General Public License, version 2 or above. # ############################################################################## # TODO # # - log the scripts actions to syslog or a file # - write instructions # # maybe: # - rewrite the script to be more modular, so it can # handle genericstable/virtusertable aliases as well. # ############################################################################## # # This is a simple program for managing /etc/aliases via email. Most # of the ideas here were borrowed from Majordomo. # # See http://www.mmedia.is/~bre/aliasman/ for installation & configuration # instructions. # ############################################################################## # # Different parts of the aliases file can be marked with different # access levels. The levels are open, owned, closed and system. # Default is "system". # # open Open aliases may be modified by anyone. # owned Owned aliases may be modified by the current # recipient (modifications require confirmation). # closed Closed aliases may only be modified by the admin. # system System aliases may not be modified by the script. # # The access levels of different parts of the file can be modified by # a comment-line containing a phrase like: # # aliasman access level: owned # # The level specified will apply to all following lines, until a # new access level is specified (the file is read top-to-bottom). # # The create command requires authorization from the administrator, # unless a part of the aliases file is marked with: # # aliasman user additions: on # # The end of such a section is marked with: # # aliasman user additions: off # ############################################################################## ###[ Configuration ]########################################################## ## ## # # # Make sure this is something sane! $ENV{'PATH'} = '/bin:/sbin:/usr/bin:/usr/sbin'; # Files & directories $aliases = '/home/aliasman/aliases'; # /etc/aliases, or equivalent. $backup = '/home/aliasman/aliases.bak'; # Backup alias list. $tmpfile = '/home/aliasman/tmp/'; # Prefix for temporary aliases file, # .. must be on same fs as /etc/. $reqdir = '/home/aliasman/data/'; # Store unconfirmed requests here. $myemail = 'aliasman@localhost'; # Aliasman's email address. $adminemail = 'postmaster@localhost'; # Aliasman's administrator. # Programs $sendmail = 'sendmail -t'; # Command used for sending mail. #$sendmail = 'cat'; $newaliases = 'newaliases'; # Newaliases command. # Command line for generating some random characters $randprog = 'dd if=/dev/urandom bs=512 count=2 2>/dev/null|uuencode -m x'; # Filters $f_reply = 'formail -r'; # <- message -> reply headers $f_md5sum = 'md5sum'; # <- text -> MD5 sum ###[ Global variables ]####################################################### ## ## # # # All messages sent to the user, and all command names are defined # here - so this is the part to change if you e.g. want to translate # the program to a different language. # # TODO: Include language definitions from some other file. # $message = ""; # Message being parsed $reply_headers = ""; # Headers for reply to user. $reply = ""; # Reply to send to user. $randstuff = ""; # Random characters, used for various things. $administrator = 0; # Was the administrators password given? $confs = { }; # The confirmation requests that need to be sent. $triggers = { }; # A hash of triggers. $cmds = { #Administrative requests: confirm => ( ), help => ( ), password => ( ), #Unconfirmed requests: change => ( ), create => ( ), remove => ( ), show => ( ), # Confirmed requests: c_change => ( ), c_create => ( ), c_remove => ( ), }; # Command & level definitions $help = 'help'; $show = 'show'; $password = 'password'; $confirm = 'confirm'; $change = 'change'; $remove = 'remove'; $create = 'create'; $system = 'system'; $closed = 'closed'; $open = 'open'; $owned = 'owned'; $off = 'off'; $on = 'on'; $default = 'default'; # Useful regexps. $reg_commands = "$help|$show|$password|$confirm|$change|$remove|$create"; $reg_level = "$system|$closed|$open|$owned"; $reg_alias = '[a-zA-Z0-9\.\-]+'; $reg_email = '[a-zA-Z0-9\.\-]+\@[a-zA-Z0-9\.\-]+'; # Default valid/invalid regexps for validating changes to the aliases file. $reg_valid = "^$reg_alias:\\s+$reg_email\$"; $reg_invalid = "^(?i)(root|admin.*|abuse|(post|host)master|help|mail|majordom.*):"; # Command syntax - commands must match these, or a syntax error is returned. # These rules are also used to grab arguments from the command lines, so put # the parens in the right places! # $syntax = { $confirm => "$confirm\\s+(.+)", $help => "$help\\s+.*", $password => "$password\\s+(.+)", $change => "$change\\s+alias\\s+($reg_alias)\\s+to\\s+($reg_email)", $create => "$create\\s+($reg_level)\\s+alias\\s+($reg_alias)\\s+as\\s+($reg_email)", $remove => "$remove\\s+alias\\s+($reg_alias)", $show => "$show\\s+alias\\s+($reg_alias)", }; $syntax_error = { $confirm => "What were you trying to confirm?\n\n", $password => "You didn't specify a password!\n\n", $change => "Broken $change command! It should look like this:\n" . "\t$change alias fred to fred\@anywhere.com\n\n", $create => "Broken $create command! It should look like this:\n" . "\t$create $owned alias fred as fred\@anywhere.com\n\n", $remove => "Broken $remove command! It should look like this:\n" . "\t$remove alias fred\n\n", $show => "Broken $show command! It should look like this:\n" . "\t$show alias fred\n\n", }; # These commands are found within the /etc/aliases file. $reg_a_setlevel = "aliasman\\s+access\\s+level:\\s+($reg_level)"; $reg_a_useradd = "aliasman\\s+user\\s+additions:\\s+($on|$off)"; $reg_a_valid = "aliasman\\s+valid\\s+regexp:\\s+(.*)"; $reg_a_invalid = "aliasman\\s+invalid\\s+regexp:\\s+(.*)"; # Misc messages. # $conf_nomatch = "Sorry, CNF doesn't match any commands.\n"; $alias_nomatch = "Sorry, that doesn't match any aliases I can see!\n"; $conf_done = "Okay!\n"; $passwd_wrong = "That's not the password!\n"; $passwd_good = "Hi boss! That password is so cute...\n"; $you_wrote = "You wrote:\n"; $confirming = "Delaying command, pending confirmation.\n"; $unimplemented = "This command hasn't been coded yet, sorry.\n"; $notallowed = "Sorry, that's not allowed.\n"; $changed_alias = "The alias has been changed.\n"; $created_alias = "The alias has been created.\n"; $removed_alias = "The alias has been removed.\n"; $alreadyexists = "You can't create that alias - it already exists!\n"; $icky_problem = "Something terrible happened, and my mission failed... :-(\n"; $misc_headers = "MIME-Version: 1.0\n" . "Content-Type: text/plain; charset=iso-8859-1\n" . "Content-Transfer-Encoding: 8bit\n" . "X-Mailer: $VERSION\n"; $confirm_msg = "From: $myemail\n" . "Reply-To: $myemail\n" . "To: RECIPIENT\n" . "Subject: Alias modification request\n" . $misc_headers . "\n" . "Mail from PERSON\n" . "requested that the following changes (prefixed by '>') to\n" . "your alias list be made. If you approve, please reply to\n" . "this message, including the \"confirm\" commands shown\n" . "below in the message body. If you don't approve, just\n" . "ignore this message.\n" . "\n"; $signature = "\n-- \n" . "This is an automatic message from $myemail\n" . "$VERSION\n"; ###[ Subroutines ]############################################################ ## ## # # # My replacement for die. # sub panic { my @errmsg = @_; if (open (SENDMAIL, "| $sendmail")) { print SENDMAIL "To: $adminemail\n"; print SENDMAIL "From: $myemail\n"; print SENDMAIL $misc_headers; print SENDMAIL "Subject: Aliasman: ERROR!!\n"; print SENDMAIL @errmsg, $signature, "\n"; close SENDMAIL; } die @errmsg; } # Dumps the contents of a file into a single string. # Returns undef on failure. # sub cat { my ($infile) = @_; open (INF, "$infile") || return undef; my $lines = join('',); close (INF); return $lines; } # This funky subroutine forks, pipes it's first argument through # the invocation of it's second argument, and returns the output # as a string. # # The program dies if this fails - and a filter returning nothing # but white-space is considered failure! # sub filter { my ($input,$filter) = @_; my ($pid, $sleep_count) = (undef,0); do { $pid = open(FILTER_KID, "-|"); unless (defined $pid) { warn "Cannot fork: $!"; panic "Gave up, stopped" if ($sleep_count > 6); sleep 10; } } until defined $pid; if ($pid) { # parent my $data = join('',); panic "No output, stopped" if ($data =~ /^\s*$/); return $data; } else { # child if (open (FILTER, "| $filter")) { print FILTER $input; close FILTER; } exit; } } # Insert the text of reply to a command into $reply, at the # corrent place. # sub reply { my ($cmd,$msg) = @_; $reply =~ s/_C0MMAND_\($cmd->[6]\)/> $cmd->[5]$msg/; } # Generate a unique string (based on the time & pid), containing a given # number of random characters. # # This generates random characters in "chunks", using $randprog. # $randindex = 0; sub randstr { my ($length) = @_; while ($randindex + $length > length($randstuff)) { # Create more random crap! my $newstuff = `$randprog`; $newstuff =~ s/[^a-zA-Z0-9]//g; $newstuff =~ s/(begin|end|base64|664)//g; $randstuff .= $newstuff; } my $str .= sprintf "%x%s%x", $$, substr($randstuff,$randindex,$length), time() ; $randindex += $length; # Guarantee we won't generate this string ever again.. sleep(1); return $str; } # Record a command & create a trigger for it if necessary. # sub add_command { my ($command) = @_; my $reg_arg1 = "^(?i)(c_)?($change|$remove|$show)\$"; my $reg_arg2 = "^(?i)(c_)?$create\$"; my $tkey = undef; printf "Added: %s %s %s %s %s\n", @{ $command }; $tkey = $command->[1] if ($command->[0] =~ $reg_arg1); $tkey = $command->[2] if ($command->[0] =~ $reg_arg2); if ($tkey) { $triggers->{$tkey} = [ ] unless $triggers->{$tkey}; push @{ $triggers->{$tkey} }, $command; } push @{ $cmds->{$command->[0]} }, $command; } # Handle commands that need confirmation. # sub h_need_confirmation { my ($line,$cmd,@perms) = @_; my $reg_def_alias = "^(?i)\\s*($reg_alias):\\s+($reg_alias|$reg_email)\\s*(#.*)?\$"; if (($administrator) || ($perms[0] eq $open) || (($cmd->[0] eq $create) && (defined $line))) { # We don' need no stinkin' confirmashun... $cmd->[0] = "ac_" . $cmd->[0]; my $handler; if ($handler = $handle{ $cmd->[0] }) { return &$handler($line, $cmd, @perms); } } my $confirmer = ""; # Do some confirming stuff here... :) my $hash = randstr(12); if ($perms[0] ne $system) { if (($perms[0] eq $owned) && ($line =~ $reg_def_alias)) { $confirmer = $1; } else { $confirmer = $adminemail; } print "Send confirmation request to: $confirmer\n"; # Save this command to a file if (open(CONF, "> $reqdir/$hash")) { reply($cmd,$confirming); my $c, $t; foreach $c (@{ $cmd }) { $t = $c; $t =~ s/\n/\\n/g; # Unsafe ... print CONF "$t\n"; } close CONF; $confs{$confirmer} = "" unless $confs{$confirmer}; $confs{$confirmer} .= "> " . $cmd->[5] . $confirm . " " . $hash . "\n\n"; } else { reply($cmd,$icky_problem); } } else { reply($cmd,$notallowed); } @{ $cmd } = ( ); return $line; } $handle{$create} = \&h_need_confirmation; $handle{$remove} = \&h_need_confirmation; $handle{$change} = \&h_need_confirmation; $handle{$show} = \&h_need_confirmation; # Handle confirmed remove requests. # sub h_remove { my ($line,$cmd,@perms) = @_; my $ret = ""; if ($perms[0] eq $system) { $ret = $notallowed; # Leave system aliases alone.. } else { $line = "# Removed: " . $line; $ret = $removed_alias; } reply($cmd,$ret); @{ $cmd } = ( ); return $line; } $handle{"c_" . $remove} = \&h_remove; $handle{"ac_" . $remove} = \&h_remove; # Handle confirmed change requests. # sub h_change { my ($line,$cmd,@perms) = @_; my $ret = ""; my $newline = $cmd->[1] . ":\t" . $cmd->[2]; if (($perms[0] eq $system) || # Check alias's level. ($newline !~ $perms[2]) || # Does it match the good regexp? ($newline =~ $perms[3])) # Does it match the bad regexp? { $ret = $notallowed; # Leave system aliases alone.. } else { $line = $newline . "\n"; $ret = $changed_alias; } reply($cmd,$ret); @{ $cmd } = ( ); return $line; } $handle{"c_" . $change} = \&h_change; $handle{"ac_" . $change} = \&h_change; # Handle confirmed remove requests. # sub h_show { my ($line,$cmd,@perms) = @_; my $ret = ""; if ($perms[0] eq $system) { $ret = $notallowed; # Leave system aliases alone.. } else { $ret = $line; } reply($cmd,$ret); @{ $cmd } = ( ); return $line; } $handle{"c_" . $show} = \&h_show; $handle{"ac_" . $show} = \&h_show; # Report that a creation request is invalid, because the alias # exists already - iff the create request has actually been # confirmed by someone, otherwise lie about the whole thing. # # The lies are to keep people from regenerating a list of aliases # on this machine by using brute-force against this script. There # are more lies elsewhere, for this same reason. # sub h_create { my ($line,$cmd,@perms) = @_; if (defined $line) { reply($cmd,$alreadyexists); @{ $cmd } = ( ); } else { return h_acreate(@_); } return undef; } sub h_acreate { my ($line,$cmd,@perms) = @_; my $ret; if (defined $line) { if ($administrator) { # Tell the truth. reply($cmd,$alreadyexists); } else { # Lie. reply($cmd,$confirming); } $ret = undef; } else { $ret = $cmd->[2] . ":\t" . $cmd->[3]; reply($cmd,$created_alias); } @{ $cmd } = ( ); return $ret; } $handle{"c_" . $create} = \&h_create; $handle{"ac_" . $create} = \&h_acreate; ###[ MAIN ]################################################################### ## ## # # if (! -d $reqdir) { mkdir $reqdir, 0700 || panic "Couldn't create $reqdir: $!\nStopped"; } open (TEST, ">$reqdir/lastrun") || panic "Unwritable: $reqdir, stopped"; close (TEST); # Scan the message, collect the commands. { my $reg_scan = '^(?i)\s*(' . $reg_commands . ')\s+(.*?)\s*$'; my $line; my $linec = 0; my $inheader = 1; while ($line = ) { $message .= $line; if ($inheader) { if ($line =~ /^\s*$/) { $inheader = 0; $reply = $you_wrote; } next; } # else $linec++; if ($line =~ $reg_scan) { # A command was found on this line my $c = lc($1); my $a = $2; # Check syntax my $reg_syntax = '^(?i)' . $syntax->{$c} . '$'; if ("$c $a" =~ $reg_syntax) { add_command( [ $c,"$1","$2","$3","$4",$line,$linec ] ); $reply .= "_C0MMAND_($linec)\n"; } else { $reply .= "> " . $line; $reply .= $syntax_error->{$c}; } } else { $reply .= "> " . $line if ($line !~ /^\s*$/); } } $reply_headers = filter($message, $f_reply); $reply_headers .= "X-Loop: aliasman.pl\n"; $reply_headers =~ s/\n\n+/\n/g; $reply_headers .= "\n"; } # Go through all confirmations, check them. { my $conf; foreach $conf ( @{ $cmds->{$confirm} } ) { my $cnf = $conf->[1]; my $r; # md5sums are supposed to be alphanumeric, so... $cnf =~ s/[^a-zA-Z0-9]/_/g; if (open (CMD, "< $reqdir/$cnf")) { my $ccmd = [ ]; while () { s/\n//g; s/\\n/\n/g; push @{ $ccmd }, $_; } close (CMD); $ccmd->[0] = "c_" . $ccmd->[0]; add_command($ccmd); $r = $conf_done; unlink "$reqdir/$cnf"; } else { # The user is confirming a nonexistant request! $r = $conf_nomatch; } $r =~ s/CNF/$cnf/g; reply($conf,$r); } } # Check if the given password is correct { my $pwdc = shift @{ $cmds->{$password} }; my $rep = ""; if ($pwdc) { my $pwd = filter($pwdc->[1], $f_md5sum); $pwd =~ s/[^a-zA-Z0-9]//g; if ($pwd eq $ARGV[0]) { $rep = $passwd_good; $administrator = 1; } else { $rep = $passwd_wrong; } reply($pwdc,$rep); } } # Step through /etc/aliases, executing pending commands as we go. { # Default values my @defaults = ($system, $off, $reg_valid, $reg_invalid); my $reg_ac_setlevel = '^(?i)#\\s+' . $reg_a_setlevel . '$'; my $reg_ac_valid = '^(?i)#\\s+' . $reg_a_valid . '$'; my $reg_ac_invalid = '^(?i)#\\s+' . $reg_a_invalid . '$'; my $reg_ac_useradd = '^(?i)#\\s+' . $reg_a_useradd . '$'; my $reg_def_alias = "^(?i)\\s*($reg_alias):\\s+($reg_alias|$reg_email)\\s*(#.*)?\$"; my $filename = $tmpfile . randstr(6); my $line_c = 0; my $line = ""; my $changed = 0; my @newaliases = ( ); my @sections = ( ); my ($level, $useradd, $valid, $invalid) = @defaults; # Initial section has default permissions. push @sections, [ @defaults, 0 ]; # Scan the aliases file my $lastline = "xyzzy foo"; open (ALIASES, "<$aliases") || panic "Couldn't read $aliases, stopped"; while ($line = ) { # Does this line contain an access-control command? if ($line =~ $reg_ac_setlevel) { $level = lc($1); push @sections, [ $level, $useradd, $valid, $invalid, $line_c ]; } elsif ($line =~ $reg_ac_useradd) { $useradd = lc($1); push @sections, [ $level, $useradd, $valid, $invalid, $line_c ]; } elsif ($line =~ $reg_ac_valid) { $valid = $1; $valid =~ s/$default/$reg_valid/; push @sections, [ $level, $useradd, $valid, $invalid, $line_c ]; } elsif ($line =~ $reg_ac_invalid) { $invalid = $1; $invalid =~ s/$default/$reg_invalid/; push @sections, [ $level, $useradd, $valid, $invalid, $line_c ]; } # Nope, does it contain an alias? elsif ($line =~ $reg_def_alias) { my $trig; my $handler; # Activate triggers for this alias, if any exist. while ((defined $triggers->{$1}) && (0 <= $#{ $triggers->{$1} })) { $trig = shift @{ $triggers->{$1} }; if ($handler = $handle{ $trig->[0] }) { my $nl = &$handler( $line, $trig, $level, $useradd, $valid, $invalid ); if ($nl) { $line = $nl; $changed = 1; } } } } if ($line !~ /^#\s+Removed:/) { push @newaliases, $line if ($line ne $lastline); $lastline = $line; } else { push @newaliases, ""; } $line_c++; } close (ALIASES); # End the last section. push @sections, [ @defaults, $line_c ]; # Process the rest of the commands, both creation requests # and change/remove requests for non-existant aliases. my $reg_c = '(?i)' . $create . '$'; my $alias; my $cmd; my $i; foreach $alias (keys(%{ $triggers })) { foreach $cmd (@{ $triggers->{$alias} }) { my $sec = undef; if ($cmd->[0] =~ $reg_c) { for ($i = 0; $i <= $#sections; $i++) { my $newalias = $cmd->[2] . ":\t" . $cmd->[3]; if ((($administrator) || ($sections[$i]->[1] eq $on)) && ($cmd->[1] eq $sections[$i]->[0]) && ($newalias =~ $sections[$i]->[2]) && ($newalias !~ $sections[$i]->[3])) { $sec = $i; } } if (defined $sec) { # Process the create request if ($handler = $handle{ $cmd->[0] }) { my $nl = &$handler( undef, $cmd, @{ $sections[$sec] } ); my $l = $sections[$sec+1]->[4]; my $b = $sections[$sec]->[4]; while (($l > $b) && ($newaliases[$l] =~ /^#/)) { $l--; } $l-- if ($newaliases[$l] =~ /^\s*$/); $newaliases[$l] .= $nl . "\n"; $changed = 1; } } else { # This alias won't fit in any section reply($cmd,$notallowed); } } else { if (($cmd->[0] =~ /^c_/) || ($administrator)) { # Tell the truth. reply($cmd,$alias_nomatch); } else { # Lie. reply($cmd,$confirming); } } } } unless (($changed) && (open (TMPFILE, ">$filename")) && (print TMPFILE @newaliases) && (close (TMPFILE)) ) { panic "Error creating $filename, stopped" if ($changed); } else { if (-e $backup) { (unlink $backup) == 1 || panic "Couldn't remove $backup, stopped"; } link $aliases, $backup || panic "Couldn't create backup!"; (unlink $aliases) == 1 || panic "Couldn't remove $aliases!"; if (link $filename, $aliases) { unlink $filename; } else { # Oops! Try and fix things. link $backup, $aliases || panic "PANIC!! I've destroyed $aliases, and can't get up!\nStopped"; panic "Failed to link $filename to $aliases, stopped"; } system($newaliases); } } # Reply to user. # if (open (SENDMAIL, "| $sendmail")) { print SENDMAIL $reply_headers, $reply, $signature, "\n"; close SENDMAIL; } # Send confirmation requests. # my $person = $reply_headers; if ($person =~ /^To:\s*(.*)$/m) { $person = $1; } else { $person = "Thispro Gram Isbroken"; } foreach $conf (keys(%confs)) { my $msg = $confirm_msg; $msg =~ s/RECIPIENT/$conf/g; $msg =~ s/PERSON/$person/g; if (open (SENDMAIL, "| $sendmail")) { print SENDMAIL $msg, $confs{$conf}, $signature, "\n"; close SENDMAIL; } }