#!/usr/bin/perl
########################################################################
# Copyright (c) 2004 Art Sackett #
# #
# This program is free software; you can redistribute it and/or modify #
# it under the terms of the GNU General Public License as published by #
# the Free Software Foundation; either version 2 of the License, or #
# (at your option) any later version. #
# #
# This program is distributed in the hope that it will be useful, but #
# WITHOUT ANY WARRANTY; without even the implied warranty of #
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU #
# General Public License for more details. #
# #
# You should have received a copy of the GNU General Public License #
# along with this program; if not, write to the Free Software #
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 #
# USA #
########################################################################
## Configuration
my $inifile = '/etc/generate_dspam_aliases.ini';
my $verbose = 0;
# Note that the only fields whose names matter are user, uid, and gid:
my @fields = qw(user password uid gid comment homedir shell);
my $min_uid = 0;
my $max_uid = 65535;
## End configuration
BEGIN {
use vars qw($VERSION);
$VERSION = do{my@r=q$Revision: 0.04 $=~/\d+/g;sprintf '%d.'.'%02d'x$#r,@r};
}
#use strict;
#use warnings;
use Config;
use Fcntl qw(:flock);
srand;
my (%arg, %state);
my @known_args = qw(inifile input output);
sub check_config {
my %required_parameters = ('Alias File' => [qw(alias-spam format program)],
'Password File' => [qw(fields)]
);
my $config = config();
foreach my $section(sort keys %required_parameters) {
foreach (@{$required_parameters{$section}}) {
my $value = $config -> value($section, $_);
if (!defined $value || !length($value)) {
print STDERR "Configuration does not provide required value $section\::$_\n";
exit 3;
}
}
}
}
sub config {
if (!defined $state{'config'}) {
$state{'config'} = new Config(verbose => defined $arg{'verbose'} ? 1 : $verbose);
$state{'config'} -> file(defined $arg{'inifile'} ? $arg{'inifile'} : $inifile);
}
return $state{'config'};
}
sub generate_aliases {
my $config = config();
my $infile = $config -> value('Password File', 'file');
my $outfile = $config -> value('Alias File', 'file');
my $tempfile = int(rand(100000000)) . '.' . $$;
if ($outfile =~ /\//) {
my @path_parts = split(/\//, $outfile);
pop @path_parts;
push(@path_parts, $tempfile);
$tempfile = join('/', @path_parts);
if ($outfile =~ /^\//) {
$tempfile = '/' . $tempfile;
}
}
my $inifile_fields = $config -> value('Password File', 'fields');
if (defined $inifile_fields && length($inifile_fields)) {
$inifile_fields =~ s/\s+/ /g;
@fields = split(/ /, $inifile_fields);
}
$state{'position'} = {'user' => undef,
'uid' => undef,
'gid' => undef
};
my $i = 0;
foreach (@fields) {
if (exists($state{'position'} -> {$_})) {
if (defined $state{'position'} -> {$_}) {
print STDERR "input file field $_ defined more than once.\n";
exit 6;
} else {
$state{'position'} -> {$_} = $i;
}
}
$i++;
}
foreach ('user', 'uid', 'gid') {
if (!defined $state{'position'} -> {$_}) {
print STDERR "input file field $_ not defined in configuration.\n";
exit 6;
}
}
my (%templates, %user_filter);
my @params = qw(alias-notspam alias-spam args-falsepositive args-spam format
program);
foreach (@params) {
my $value = $config -> value('Alias File', $_);
if (defined $value && length($value)) {
if (defined $templates{$_}) {
print STDERR "configuration: Alias File::$_ defined twice.\n";
exit 6;
}
$templates{$_} = $value;
}
}
my $create_spam_alias = defined $templates{'alias-spam'};
my $create_notspam_alias = defined $templates{'alias-notspam'};
if (!$create_spam_alias && !$create_notspam_alias) {
print STDERR "configuration provides neither alias-spam nor alias-notspam; nothing to do.\n";
exit 6;
}
my @users_params = qw(ignore-gid ignore-uid ignore-user maximum-gid
maximum-uid minimum-gid minimum-uid);
foreach (@users_params) {
my $config_string = $config -> value('Users', $_);
if (defined $config_string && length($config_string)) {
$config_string =~ s/\s+/ /g;
my @values = split(/ /, $config_string);
if ($_ =~ /^(maxi|mini)mum/) {
if (scalar(@values) > 1) {
print STDERR "configuration: Users::$_ contains more than one value.\n";
exit 6;
}
if ($values[0] !~ /^\d+$/ || $values[0] < $min_uid || $values[0] > $max_uid) {
print STDERR "configuration: Users::$_ contains illegal value.\n";
exit 6;
}
$user_filter{$_} = $values[0];
} else {
if ($_ =~ /uid$/) {
foreach my $val(@values) {
if ($val !~ /^\d+$/ || $val < $min_uid || $val > $max_uid) {
print STDERR "configuration: Users::$_ contains illegal value.\n";
exit 6;
}
}
}
foreach my $val(@values) {
$user_filter{$_} -> {$val} = 1;
}
}
}
}
if (open(INPUT, "<$infile")) {
if (open(OUTPUT, ">$tempfile")) {
if (flock(OUTPUT, LOCK_EX|LOCK_NB)) {
while () {
chomp;
s/^\s+//;
s/\s+$//;
if (!length($_) || $_ !~ /:/) {
next;
}
my (%data);
my @passwd = split(/:/);
foreach ('user', 'uid', 'gid') {
$data{$_} = $passwd[$state{'position'} -> {$_}];
if (!length($data{$_})) {
print STDERR "bogus input file $infile: does not provide $_ in (at least one of many) records.\n";
close OUTPUT;
unlink($tempfile);
exit 6;
}
}
if ($user_filter{'ignore-user'} -> {$data{'user'}}) {
next;
} elsif ($user_filter{'ignore-uid'} -> {$data{'uid'}}) {
next;
} elsif ($user_filter{'ignore-gid'} -> {$data{'gid'}}) {
next;
} elsif (defined($user_filter{'minimum-uid'}) && $data{'uid'} < $user_filter{'minimum-uid'}) {
next;
} elsif (defined $user_filter{'maximum-uid'} && $data{'uid'} > $user_filter{'maximum-uid'}) {
next;
} elsif (defined($user_filter{'minimum-gid'}) && $data{'gid'} < $user_filter{'minimum-gid'}) {
next;
} elsif (defined $user_filter{'maximum-gid'} && $data{'gid'} > $user_filter{'maximum-gid'}) {
next;
}
if ($create_spam_alias) {
$data{'alias'} = interpolate($templates{'alias-spam'}, \%data);
if ($templates{'args-spam'}) {
$data{'args'} = interpolate($templates{'args-spam'}, \%data);
} else {
$data{'args'} = '';
}
if ($templates{'program'}) {
$data{'program'} = interpolate($templates{'program'}, \%data);
}
my $line = interpolate($templates{'format'}, \%data);
print OUTPUT $line, "\n";
foreach ('alias', 'args', 'program') {
delete $data{$_};
}
}
if ($create_notspam_alias) {
$data{'alias'} = interpolate($templates{'alias-notspam'}, \%data);
if ($templates{'args-falsepositive'}) {
$data{'args'} = interpolate($templates{'args-falsepositive'}, \%data);
} else {
$data{'args'} = '';
}
if ($templates{'program'}) {
$data{'program'} = interpolate($templates{'program'}, \%data);
}
my $line = interpolate($templates{'format'}, \%data);
$line =~ s/^\s+//;
$line =~ s/\s+$//;
print OUTPUT $line, "\n";
foreach ('alias', 'args', 'program') {
delete $data{$_};
}
}
}
close OUTPUT;
close INPUT;
rename($tempfile, $outfile);
} else {
print STDERR "cannot lock temporary output file.\n";
close OUTPUT;
unlink($tempfile);
exit 6;
}
} else {
print STDERR "cannot open $outfile for writing: $!\n";
exit 5;
}
} else {
print STDERR "cannot open $infile for reading: $!\n";
exit 4;
}
}
sub interpolate {
my $target = shift;
if (!defined $target || !length($target) || $target !~ /<|>/) {
return $target;
}
my $source = shift;
if (!defined $source || !ref($source) || ref($source) ne 'HASH') {
return $target;
}
foreach (keys %$source) {
$target =~ s/<$_>/$source->{$_}/g;
}
return $target;
}
sub merge_config_and_argv {
my $config = config();
if (defined $arg{'input'}) {
if (!length($arg{'input'})) {
print STDERR "--input argument is invalid.\n";
exit 4;
}
$config -> set('Password File', 'file', $arg{'input'});
}
if (defined $arg{'output'}) {
if (!length($arg{'output'})) {
print STDERR "--output argument is invalid.\n";
exit 5;
}
if ($arg{'output'} =~ /\//) {
my @path_parts = split(/\//, $arg{'output'});
pop(@path_parts);
my $directory = join('/', @path_parts);
if (!-d $directory) {
print STDERR "directory $directory does not exist.\n";
exit 5;
}
$config -> set('Alias File', 'file', $arg{'output'});
}
}
if (!$config -> basic_file_test($config -> value('Password File', 'file'), 3, 20480)) {
exit 4;
}
my $outfile = $config -> value('Alias File', 'file');
if (-e $outfile) {
if (!-w $outfile) {
print STDERR "output file $outfile is not writable.\n";
exit 5;
}
} else {
if (!open(OUTPUT, ">$outfile")) {
print STDERR "cannot open output file $outfile for writing.\n";
exit 5;
}
close OUTPUT;
unlink($outfile);
}
}
sub parse_argv {
if (@ARGV) {
push(@known_args, 'verbose');
foreach (@ARGV) {
if ($_ =~ /^--(help|usage)/) {
usage();
exit 0;
} elsif ($_ =~ /^--version/) {
my $version = version();
print "This is generate_dspam_aliases version $version.\nCopyright 2004
Art Sackett\nReleased under the terms of the GNU General Public License.\n";
exit 0;
} elsif ($_ =~ /^--verbose/) {
$arg{'verbose'} = 1;
$verbose = 1;
}
}
my %known_arg = ();
map {$known_arg{$_} = 1 } @known_args;
my $last_name = '';
while (@ARGV) {
my $chunk = shift(@ARGV);
$chunk =~ s/^\s+|\s+$//g;
my ($name, $value);
if ($chunk =~ /^\-\-/) {
$chunk =~ s/^\-\-//;
# contains a name
if (length($last_name)) {
$arg{$last_name} = 1;
$last_name = '';
}
if ($chunk =~ /=/) {
# also contains value
($name, $value) = split(/=/, $chunk, 2);
if (!$known_arg{$name}) {
print STDERR "Unknown argument: $name.\n";
exit 1;
}
$arg{$name} = $value;
$last_name = '';
} else {
# is just a name
if (!$known_arg{$chunk}) {
print STDERR "Unknown argument: $name.\n";
exit 1;
}
$last_name = $chunk;
next;
}
} else {
# is a value, or junk
if (length($last_name)) {
$arg{$last_name} = $chunk;
$last_name = '';
} else {
print STDERR "Junk argument: $chunk\n";
exit 1;
}
}
}
if (length($last_name)) {
$arg{$last_name} = 1;
}
}
$state{'argv_parsed'} = 1;
}
sub usage {
my %known_arg = (inifile => 'full path to ini (configuration) file',
input => 'full path to passwd file to parse for user names',
output => 'full path to output (alias) file',
verbose => 'cause generate_dspam_aliases to be more chatty'
);
print "\nusage: generate_dspam_aliases [options]\n";
print "options:\n";
print "\t--inifile=/path/to/generate_dspam_aliases.ini\n";
print "\t--input=/path/to/input/file (e.g. /etc/passwd)\n";
print "\t--output=/path/to/output/file (e.g. /etc/mail/aliases/dspam)\n";
print "\t--verbose (useful for troubleshooting configuration problems)\n";
print "\t--version (emits version information, then exits)\n";
print "\nNo options are required if the default behavior specified in the\ninifile are acceptable.\n\n";
exit 0;
}
sub version {
return ($VERSION);
}
parse_argv();
check_config();
merge_config_and_argv();
generate_aliases();
###
package Config;
sub basic_file_test {
my $self = shift;
my ($file, $min, $max) = @_;
if (!defined $file) {
if ($self -> verbose()) {
print "Config::basic_file_test() requires argument: name of file to test\n";
}
return (undef);
}
$self -> trim($file);
if (!length($file)) {
if ($self -> verbose()) {
print "Config::basic_file_test() requires argument: name of file to test.\n";
}
return (undef);
}
if (!-e $file) {
if ($self -> verbose()) {
print "$file does not exist.\n";
}
return (0);
} elsif (!-f $file) {
if ($self -> verbose()) {
print "$file is not a regular file.\n";
}
return (0);
} elsif (!-r $file) {
if ($self -> verbose()) {
print "$file is not readable.\n";
}
return (0);
} elsif (!-T $file) {
if ($self -> verbose()) {
print "$file is not plain text.\n";
}
return (0);
} elsif (-z $file) {
if ($self -> verbose()) {
print "$file is empty (zero bytes).\n";
}
return (0);
} else {
my $bytes = (-s $file);
if (defined $min) {
if ($min && $bytes < $min) {
if ($self -> verbose()) {
print "$file size ($bytes bytes) is less than specified minimum of $min bytes.\n";
}
return (0);
}
}
if (defined $max) {
if ($max && $bytes > $max) {
if ($self -> verbose()) {
print "basic_file_test(): $file size ($bytes bytes) exceeds specified maximum of $max bytes.\n";
}
return (0);
}
}
return $bytes;
}
}
sub file {
my $self = shift;
if (@_) {
my $file = shift;
if ($self -> basic_file_test($file)) {
$self -> {'FILE'} = $file;
} else {
if ($self -> verbose()) {
print "Cannot use $file.\n";
}
$self -> {'FILE'} = undef;
}
}
return $self -> {'FILE'};
}
sub has_section {
my $self = shift;
my $test = shift;
if (!defined $test || !length($test)) {
if ($self -> verbose()) {
print "Config::has_section(): requires argument.\n";
}
return (undef);
}
if (!defined $self -> {'HASH'} || !$self -> {'FILE_SLURPED'}) {
my $loaded_okay = $self -> _read_parse();
if (!defined $loaded_okay || !$loaded_okay) {
if ($self -> verbose()) {
print "Config::has_section(): unable to load ini file.\n";
}
return undef;
}
}
if (wantarray) {
my (@results);
if (defined $self -> {'HASH'} -> {$test}) {
push(@results, $test);
}
my $normalized = uc($test);
foreach (@{$self -> sections()}) {
if (uc($_) eq $normalized) {
push(@results, $_);
}
}
return @results;
} else {
if (defined $self -> {'HASH'} -> {$test}) {
return (1);
}
}
return (0);
}
sub new {
my $proto = shift;
my $class = ref($proto) || $proto;
my $self = {};
srand;
%$self = @_;
bless ($self, $class);
while (my ($key, $value) = each %$self) {
if ($self -> can($key)) {
$self -> $key($value);
}
}
return $self;
}
sub parameters {
my $self = shift;
my $section = shift;
if (!defined $section || !length($section)) {
return undef;
}
if (!defined $self -> {'HASH'} || !$self -> {'FILE_SLURPED'}) {
my $loaded_okay = $self -> _read_parse();
if (!defined $loaded_okay || !$loaded_okay) {
if ($self -> verbose()) {
print "Config::parameters(): unable to load ini file.\n";
}
return undef;
}
}
my @parameters = keys(%{$self -> {'HASH'} -> {$section}});
return \@parameters;
}
sub _read_parse {
my $self = shift;
my $file = shift;
if (defined $file) {
if (!$self -> basic_file_test($file)) {
return undef;
}
} else {
$file = $self -> file();
}
my (@input);
if (open (FILE, "<$file")) {
@input = ;
close FILE;
} else {
return undef;
}
if (!scalar @input) {
return undef;
}
my ($section);
foreach my $line(@input) {
chomp($line);
$line =~ s/\#.*$//;
$self -> trim(\$line);
if ($line =~ /^\[.*\]$/) {
$line =~ s/^\[(.*)\]$/$1/;
if (length($line)) {
$section = $line;
next;
} else {
$section = undef;
}
}
if (!defined $section) {
next;
}
if ($line !~ /=/) {
next;
}
my ($param, $value) = split(/=/, $line, 2);
foreach ($param, $value) {
$self -> trim(\$_);
}
if (defined $param && length($param)) {
$self -> {'HASH'} -> {$section} -> {$param} = $value;
}
$self -> {'FILE_SLURPED'} = 1;
}
return (1);
}
sub sections {
my $self = shift;
if (!defined $self -> {'HASH'} || !$self -> {'FILE_SLURPED'}) {
my $loaded_okay = $self -> _read_parse();
if (!defined $loaded_okay || !$loaded_okay) {
if ($self -> verbose()) {
print "Config::sections(): unable to load ini file.\n";
}
return undef;
}
}
my @sections = keys(%{$self -> {'HASH'}});
return \@sections;
}
sub set {
my $self = shift;
my $section = shift;
my $parameter = shift;
foreach ($section, $parameter) {
if (!defined $_ || !length($_)) {
return undef;
}
}
$self -> {'HASH'} -> {$section} -> {$parameter} = shift;
return (1);
}
sub trim {
my $self = shift;
my $stuff = shift;
if (!defined $stuff) {
return (undef);
} elsif (ref($stuff) eq 'ARRAY') {
foreach (@$stuff) {
$_ =~ s/^\s+//;
$_ =~ s/\s+$//;
}
} elsif (ref($stuff) eq 'HASH') {
foreach (keys %$stuff) {
$$stuff{$_} =~ s/^\s+//;
$$stuff{$_} =~ s/\s+$//;
}
} elsif (ref($stuff) eq 'SCALAR') {
$$stuff =~ s/^\s+//;
$$stuff =~ s/\s+$//;
} else {
$stuff =~ s/^\s+//;
$stuff =~ s/\s+$//;
return ($stuff);
}
}
sub value {
my $self = shift;
if (!defined $self -> {'HASH'} || !$self -> {'FILE_SLURPED'}) {
my $loaded_okay = $self -> _read_parse();
if (!defined $loaded_okay || !$loaded_okay) {
if ($self -> verbose()) {
print "Config::value(): unable to load ini file.\n";
}
return undef;
}
}
my $section = shift;
my $param = shift;
return $self -> {'HASH'} -> {$section} -> {$param};
}
sub verbose {
my $self = shift;
if (@_) {
if ($_[0]) {
$self -> {'VERBOSE'} = 1;
} else {
$self -> {'VERBOSE'} = 0;
}
}
if (!defined $self -> {'VERBOSE'}) {
return 0;
}
return $self -> {'VERBOSE'};
}