#!/usr/bin/perl

# randomsig -  A program that picks a random line from one or more files for
#              for use as an email signature.
#
# Copyright (C) 2001 Suso Banderas

# 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.
#
# You may contact the author at <suso@suso.org>.


# Description:  randomsig is a program that, when run, will create a .signature file
#               in your home directory that is either a named pipe or if the program
#               is run in timed mode the .signature file will be updated every so
#               often.  The idea that each time you read from the named pipe or the
#               file is updated the signature will be changed.

# Description:  randomsig is a program that, when run, will create a .signature file
#               in your home directory that has random signatures in it either every
#               time you read from it.  It can also run in a timed mode where it just
#               flat out updates the .signature file every so often.  Of course, it
#               is much more configurable than this description lets on, you'll just
#               have to read more about it.


################
# MAIN PROGRAM #
################

$version = '1.10.0';
use Getopt::Std;
use File::Copy;
use Text::Tabs;

my $gzip = "/bin/gzip";   # Set the path to the gzip program.

# If the user doesn't already have a config file, we want to help them out by
# copying one for them from the distrobution.
my $user_homedir = (getpwuid($<))[7];
if (!-e "$user_homedir/.randomsigrc") {
	print "No .randomsigrc file found in $user_homedir. Trying to creating one.\n";
	if (-e '/usr/local/etc/randomsig/.randomsigrc') {
		copy('/usr/local/etc/randomsig/.randomsigrc', "$user_homedir/.randomsigrc") ||
			die "Can't create .randomsigrc file: $!\n";
	} else {
		print "No example .randomsigrc file found, please use the example .randomsigrc\nfrom the distrobution you downloaded.\n";
		exit(1);
	}
	print "Now, edit the .randomsigrc file just created in your home directory and then re-run randomsig.\n";
	exit(0);
}


getopts('hvflptV');
if ($opt_h || $opt_v) { &usage; exit(0); }

# See the subroutines section for an explaination
# on why there are here.
$SIG{PIPE}	= 'signal_pipe';     # pine sends these sometimes. :-(
$SIG{TERM}	= 'signal_handler';  # A normal kill command sends this.
$SIG{INT}	= 'signal_handler';  # Ctrl-C from the keyboard.
$SIG{HUP}	= 'read_config';     # This will happen after a while anyways.

chdir;  # go home
$| = 1;

# All configuration is now done in a .randomsigrc file
# in the user's home directory.
read_config();


srand;

# Before we fork or anything else let's see if another randomsig
# process is running in the place where we will.  NOTE: Right now
# this only checks for a named pipe file called. signature.  In a
# future version it should probably also make sure there is really
# a process attached to it.
unless ($opt_l) {
	if (-p $SIGFILE) {
		print "\nERROR: Another randomsig named pipe called $SIGFILE file is already in place.\nExiting\n";
		exit(1);
	}
}
# We're going to try forking away from the shell so
# that the user doesn't have to know about process
# controll.  Unless they specify the -f option,
# let's fork baby.

if (defined($opt_f) || defined($opt_l)) {
	print "Running main program non-forked\n" if ($opt_V);
	&main_program();
} else {
	FORK: {
		if ($pid = fork) { # parent
			exit(0);
		} elsif (defined $pid) { # child
			print "Forked to PID $pid\n" if ($opt_V);
			&main_program();
		} elsif ($! =~ /No more process/) {
			print "Sleeping 5 seconds because can't fork\n" if ($opt_V);
			sleep 5;
			redo FORK;
		} else {
			die "Can't fork: $!\n";
		}
	}
}

# I normally wouldn't write the main part of the
# program as a subroutine but I've never wrote 
# a REAL program that's forked away from the shell
# before so I have to get used to it. :-)
sub main_program () {

	# This will give each file an initial time last read
	# value and the global sig hashes.  We want it to be
	# 0 and empty for each one at this time.
	foreach $file (@FILES, @QUOTES) {
		print "Setting $file lastread time to 0\n" if ($opt_V);
		$sig_file_lastread{$file} = 0;
	}
	
	#******************************************************************************
	# This first loop is the outside loop.  It gets read when the program starts
	# and creates the array that holds all the valid lines.  It will get reread
	# after the first read from the pipe after $time seconds has passed.  This
	# keeps your random lines array up to date with what's in the files.
	#
	READ: while (1) {
	
		if (!defined($lasttime)) {
			$lasttime = 0;
		} else {
			$lasttime = time();
		}

		@line_array = ();

		if (defined($CANCELFILE) && -f $CANCELFILE) {
			open (CANCEL, "$CANCELFILE");
			print "Reading in cancelfile: $CANCELFILE\n" if ($opt_V);
			while(<CANCEL>) {
				$cancelline = $_;
				chomp($cancelline);
				unless ($cancelline =~ /^#/ || $cancelline =~ /^[ ]*$/) {  # take out comments and blank lines.
					push(@cancel_array, $cancelline);
				}
			}
			close(CANCEL);
		}
	

		if (defined(@FILES)) {	
			foreach $file (@FILES) {

				# if the file has changed since
				# the last time we read things in
				# then read in that file.

				# Maildir support.
				if ($file =~ /\/$/ && -d "$file" && -d "$file/cur" && -d "$file/new" &&
					($sig_file_lastread{$file} < (stat("${file}new"))[9])) {

					%$file = ();  # Since the Maildir has been updated, clear the old contents.


					# First, check the cur/ directory.
					opendir(CURDIR, "$file/cur") || warn "$file is a maildir but couldn't open 'cur' for reading: $!\n";
					foreach $curdirfile (grep !/^\.\.?$/, readdir CURDIR) {
						$filepath = "$file/cur/$curdirfile";
						open (FILE, $filepath);
						my @lines = <FILE>;
						close(FILE);
						&line_add($file, \@lines, \$MAIN_EXPRESSION, \@cancel_array);
					}
					closedir(CURDIR);

					# Next, the new/ directory.
					opendir(NEWDIR, "$file/new")  || warn "$file is a maildir but couldn't open 'new' for reading: $!\n";
					foreach $newdirfile (grep !/^\.\.?$/, readdir NEWDIR) {
						$filepath = "$file/new/$newdirfile";
						open (FILE, $filepath);
						my @lines = <FILE>;
						close(FILE);
						&line_add($file, \@lines, \$MAIN_EXPRESSION, \@cancel_array);
					}
					closedir(NEWDIR);

					$sig_file_lastread{$file} = time();

					print "Set last read time for $file to $sig_file_lastread{$file}\n" if ($opt_V);
					print "Added files from Maildir formated mailbox $file to file array\n" if ($opt_V);
				} elsif ($file =~ /.gz$/ && -f $file && ($sig_file_lastread{$file} < (stat($file))[9])) {
				# gzipped mbox support.  - thanks to Joe Bowman for this one. -jbowman@kiva.net
					print "Reading $file as gzipped mbox folder\n" if ($opt_V);
					%$file = ();

					my @lines = `$gzip -dc $file`;
					&line_add($file, \@lines, \$MAIN_EXPRESSION, \@cancel_array);

					$sig_file_lastread{$file} = time();
					print "Set last read time for $file to $sig_file_lastread{$file}\n" if ($opt_V);

				} elsif (-f $file && ($sig_file_lastread{$file} < (stat($file))[9])) {
				# Else, basic mbox format.
					print "Reading $file as non-quote file\n" if ($opt_V);
					%$file = ();
					open (FILE, $file);	# Just continue so that there
					my @lines = <FILE>;	# are no problems if the file
					close(FILE);		# is empty, etc.
					
					&line_add($file, \@lines, \$MAIN_EXPRESSION, \@cancel_array);

					$sig_file_lastread{$file} = time();
					print "Set last read time for $file to $sig_file_lastread{$file}\n" if ($opt_V);
				}
			}
		}
	
		#*********************************************************
		# If you have a special file with lines that you want
		# included in the line array no matter what this loop
		# will read them in.  Just add them to the @quotes array
		# at the top of the program.
		#
		foreach $quotefile (@QUOTES) {
			if (-f $quotefile && ($sig_file_lastread{$quotefile} < (stat($quotefile))[9])) {
				%$quotefile = ();
				print "Reading in $quotefile as a quoted file\n" if ($opt_V);
				open (QUOTES, $quotefile);
				while (<QUOTES>) {
					my $line = $_;
					chomp($line);
					$$quotefile{$line}++;
				}
				close(QUOTES);
				$sig_file_lastread{$quotefile} = time();
			}
		}
	
		# Make a sorted array of the lines that qualify.
		foreach $file (@FILES, @QUOTES) {
			print "Adding qualifying lines from $file to global lines array\n" if ($opt_V);
			push(@line_array, sort(keys(%$file)));
		}
	
	        #*************************************************************************
	        # This is the inside loop that continuously runs, waiting for you to
	        # read from the .signature file.  If a certain amount of time has passed
	        # it will back once to the outter loop.
	        #
		if($opt_l) {        # Just list the qualifying lines and exit.
				%outputhash = ();
				print "\nPreparing to print out all qualifying lines\n" if ($opt_V);
				foreach $file (@FILES, @QUOTES) {
					foreach $line (keys(%$file)) {
						$outputhash{$line} = $$file{$line};
					}
				}
				@linearray = sort keys(%outputhash);
				foreach $line (@linearray) {
					print $outputhash{$line}, ":$line\n";
				}
				print "Total sigs: ", scalar @linearray, "\n" if ($opt_V);
				exit(0);
		} else {
			if (-e $SIGFILE) {
				$lastreadtime = (stat($SIGFILE))[8];
				$prev_lastreadtime = $lastreadtime;
			} else {  # Going this route will create the .signature file if it's not there.
				$prev_lastreadtime = 0;
				$lastreadtime = 1;
			}
			while (1) {
				if ($opt_p) {
					unless (-p $SIGFILE) {     # just listing out the lines or the pipe is 
						print "Creating $SIGFILE as named pipe\n" if ($opt_V);

						# Just in case there is already one there.
						if (-e $SIGFILE) {
							unlink($SIGFILE) or die "Can't unlink previous sigfile $SIGFILE: $!\n";
						}

						# System return values are backwards from what you'd expect so you have
						# to use an and instead of an or.
						system('mknod', $SIGFILE, 'p') and die "Could not mknod $SIGFILE: $!\n";
					}
				}
				# This random number will be used to determine which line to use
				# out of all the lines read in from all the files.
				$numlines = scalar(@line_array);
				print "$numlines total qualifying lines\n" if ($opt_V);

				$random = int (rand($numlines));
				print "Random number: $random\n" if ($opt_V);
			
				$randomline = $line_array[$random];
				# Once we've set the $randomline scalar, go ahead and remove the line from the @line_array because we
				# don't want it to be used again.
				print "Removing line: $line_array[$random]\n" if ($opt_V);
				splice(@line_array, $random, 1);
				chomp($randomline);
				print "Random line: $randomline\n" if ($opt_V);

				# These two lines allow people to put extra newlines and
				# tabs in their sigs for formatting.
				$randomline =~ s/\\n/\n/g;
				$randomline =~ s/\\t/\t/g;

				# Wrap the line if it's longer than $wrap_at chars.
				if (defined($WRAP_AT)) {
					$columns = $WRAP_AT;
					$randomline = wrap($randomline);
					chomp($randomline);
				}

				if ($opt_t || $opt_p || ($lastreadtime > $prev_lastreadtime)) {
					open (SIGNATURE, "> $SIGFILE") or die "can't write $SIGFILE: $!";

					if (-e $SIGREAD) {	
						print "Opening signature read in file: $SIGREAD\n" if ($opt_V);
						open(SIGREAD, "$SIGREAD") || warn "Cannot read $SIGREAD template signature file: $!\n";
						print SIGNATURE "-- \n";
						$n = 0;
						OUT: while (<SIGREAD>) {
							if (grep(/^--[ ]{0,1}$/, $_) && $n == 0) {
								print "Already a double-dash-space line, skipping this line\n" if ($opt_V);
								next OUT;
							}
							$_ =~ s/\$randomline/$randomline/g;
							print SIGNATURE "$_";
							$n++;
						}
						close(SIGREAD);
					} else {
						print "No SIGREAD template file($SIGREAD), printing default.\n" if ($opt_V);
						print SIGNATURE "-- \n$randomline\n";
					}

					close (SIGNATURE);
					print "Finished writing to signature file\n" if ($opt_V);
					$prev_lastreadtime = $lastreadtime;
					$lastreadtime = (stat($SIGFILE))[8];
				}

				if ($opt_t) {
					print "Timed mode: sleeping " . calculate_time($UPDATE_TIME) . " or 1800 seconds\n" if ($opt_V);
					sleep (calculate_time($UPDATE_TIME) || 1800);
				} elsif ($opt_p) {
					print "Pipe mode: Sleeping 1 second\n" if ($opt_V);
					sleep 1;  # Don't change this value to anything less than 1.
				} else {
					while ($lastreadtime <= $prev_lastreadtime) {
						$lastreadtime = (stat($SIGFILE))[8];
						select (undef,undef,undef,$CHECK_TIME);
					}
				}

				#*****************************************************
				# This piece of code is so that we're not always  
				# checking the files to see if they've changed. The
				# $reread_time variable determines how often we
				# check to see if any of the files have been updated.
				# UPDATE: Now, if the REREAD_TIME is set to 0, it will
				# not reread signature lines until they are all used up.
				unless ($REREAD_TIME =~ /^0$/) {
					if ((time() - $lasttime) > (calculate_time($REREAD_TIME) || 1800)) {
						print "wait time is up, going back to re-read files\n" if ($opt_V);
						next READ;
					}
				}
				print "Sigs left: ", scalar @line_array, "\n" if ($opt_V);
				if ($#line_array < 0) {
					print "Re-reading signatures because we ran out\n" if ($opt_V);
					next READ;  # Get more sig lines, because we're out of em.
				}
			}
		}
	}
}




###############
# SUBROUTINES #
###############

sub usage {
	print <<EOF;
--------------------------
 randomsig version $version
--------------------------
 Description:  Program that will pull a random line
               out of some files and send it to a
               named pipe for reading.
               See top of program file for details.

 Usage:
        $ randomsig
        $ randomsig [options]

 Options:
           -h -v  -- Show this message
           -V     -- Turn on extra verbosity.
           -l     -- Only print out all qualifying
                     lines and exit.
           -f     -- Don't fork away from the shell.
           -t     -- Timed mode.  Update the signature
		             file every so often as specified
                     by \$update_time.  Whether the file
                     is read or not.
           -p     -- Run in named pipe mode.  This mode
                     is being depreciated and will not be
                     in the next version.
 
EOF
} 

#****************************************
# If for some reason the program
# dies we don't want to leave the named
# pipe file behind because if another
# program reads it that program might
# freeze up.  So we have our own signal
# handler.

sub read_config {
	$signal = shift;
	my $user_homedir = (getpwuid($<))[7];
	my $randomsig_config_file = "$user_homedir/.randomsigrc";
	print "Reading config\n" if ($opt_V);
	do $randomsig_config_file;
	if ($signal) {
		print "\nSuccessfully re-read the randomsig config.\n" if ($opt_V);
	}
}

sub line_add {
	my $filename = shift;
	my $lines_array_ref = shift;
	my $match_expression_ref = shift;
	my $cancel_array_ref = shift;
	#**************************************************************
	# This is the loop that goes through the file $file
	# and checks lines for validity.  Unless you're just weird
	# you probably don't want every line to be considered valid,
	# Especially if your files are Mailbox format. I've found that
	# searching for lines with punctuation at the end works well.
	# In the future I may make this configurable.
	#
	my $quotefrom;
	LINE: foreach $line (@$lines_array_ref) {
		chomp($line);
		# If mail quoting is turned on, then search for From: lines in the headers.
		if ($MAIL_QUOTING && $line =~ /^From: (.*)$/) {
			$quotefrom = $1;
			$quotefrom =~ s/.*\b([A-Za-z0-9\._-]+\@[A-Za-z0-9-\.]+\b).*/$1/;
			
		}
		if ($line =~ /$$match_expression_ref/) {
			foreach $cancelexpression (@$cancel_array_ref) {
				if ($line =~ /$cancelexpression/) {  # Skip this $line.
					next LINE;
				}
			}
			print "Adding line: $line\n" if ($opt_V);

			#
			# Now here comes all the mutation functions for formatting the line.
			#
			if (defined($MUTATION_FROM) && defined($MUTATION_TO)) {
				print "Mutating randomline using user specified MUTATION variables\n" if ($opt_V);
				$line =~ s/$MUTATION_FROM/$MUTATION_TO/g;
				print "Post mutation randomline: $line\n" if ($opt_V);
			}

			if ($quotefrom) {
				if ($MAIL_QUOTE_LOCALDOMAIN) {
					# Take out the domain part if it's a local domain.
					$quotefrom =~ s/\@$MAIL_QUOTE_LOCALDOMAIN//;
				}
				$line .= " -$quotefrom";
			}
			$$filename{$line}++;
		}
	}

}


# based on certain strings, we should allow the user
# to use m for minutes, h for hours, etc.
sub calculate_time {
	$string = shift;
	$_ = $string;
	my $myseconds = 0;

	# weeks.
	if (/([0-9\.]+)[ ]*w/ig) {
		$myseconds += ($1 * 604800);
	}

	# days.
	if (/([0-9\.]+)[ ]*d/ig) {
		$myseconds += ($1 * 86400);
	}

	# hours.
	if (/([0-9\.]+)[ ]*h/ig) {
		$myseconds += ($1 * 3600);
	}

	# minutes.
	if (/([0-9\.]+)[ ]*m/ig) {
		$myseconds += ($1 * 60);
	}

	# seconds.
	if (/([0-9]+)[ ]*s/ig) {
		$myseconds += $1;
	}
	if (/^[0-9]+$/) {
		$myseconds = $string;
	}
	$myseconds =~ s/\..*$//;
	return $myseconds;
}

sub signal_handler {
	$signal = shift;
	print STDERR "\nGot a SIG $signal! Cleaning up and exiting.\n" if ($opt_V);
	if ($opt_p) {
		unlink($SIGFILE);
	}
	exit(1);
}

# Because Pine is a wank, we have to give SIGPIPE it's own handler
# in an attempt to fix problems with it reading improperly from
# the signature file when it's a named pipe.
sub signal_pipe {
	$sig = shift;
	print STDERR "\nGot a SIG $signal! Sleeping 0.05 seconds.\nIf you're running pine, try restarting it.\n\n" if ($opt_V);
	select(undef,undef,undef,0.05);
}

# Taken almost completely from the Text::Wrap wrap function.
# With a few modifications for randomsig's functionality.

sub wrap
{
	$break = '\s';
	my $t = shift;

	$t = expand($t);

	my $r = "";
	my $ll = $columns - 1;
	my $nll = $columns - 1;
	my $nl = "";
	my $remainder = "";

	while ($t !~ /^\s*$/) {
		if ($t =~ s/^([^\n]{0,$ll})($break|\Z(?!\n))//xm) {
			$r .= unexpand($nl . $1);
			$remainder = $2;
		} elsif ($t =~ s/^([^\n]{$ll})//) {
			$r .= unexpand($nl . $1);
			$remainder = "\n";
		} else {
			die "This shouldn't happen";
		}
		
		$ll = $nll;
		$nl = "\n";
	}
	$r .= $remainder;

	$r .= $t if $t ne "";

	return $r;
}


#--end--
