#!/usr/bin/perl -w

##############################################################################
#
# Print billing management system
#
# Version 4.2.0
#
# Copyright (C) 2000, 2001, 2002, 2003 Daniel Franklin
#
# This program is distributed under the terms of the GNU General Public
# License Version 2.
#
# This is a print accounting and billing filter.
#
# To use this, you need the following (or similar) lines in your printcap
# file:
#
#inkjet|Inkjet printer, user-accessable queue
#	 :lp=/dev/lp0
#	 :achk=true
#	 :as=|/usr/sbin/printbill -type [bill|lazybill|account] [-printbill_secondary printer]
#        :if=/etc/magicfilter/psonly600-filter
#	 :sd=/var/spool/lpd/inkjet
#	 :mx#0
#	 :sh
#
# Note that if type is "bill" you need a secondary printer definition - see
# printbill (8) for details.
#
##############################################################################

use Locale::gettext;
use POSIX;
use IO::Socket::UNIX;
use Printbill::PTDB_File;
use Printbill::printbill;
use Printbill::printbill_pcfg;
use Sys::Syslog qw(:DEFAULT setlogsock);
use Fcntl qw(:DEFAULT :flock);
use File::Path;
use strict;
use Getopt::Long;

setlocale (LC_MESSAGES, "");
textdomain ("printbill");

my $server;
my $client;
my $pid;
my $version = '4.2.0';
my $sockname = "/tmp/printbilld";
my $configdir = "/etc/printbill";
my $config = "$configdir/printbillrc";
my $printer = "";
my $user = "";
my $JSUCC = 0;
my $JREMOVE = 3;
my $nfiles = 0;
my (%params, $line, @filenames, %userhash, %locks);
my (%opts, @tmp, @my_argv, @total_file_info, $price, $stay, $mum);

BEGIN { $ENV{PATH} = '/bin:/usr/bin' }

GetOptions ('stay' => \$stay);

# Get my PID...

$mum = $$;

setlogsock('unix') or &die_cleanup ($JREMOVE, sprintf gettext ("%s: Error: cannot set log type: %s.\n"), $0, $!);

openlog $0, 'cons', 'lpr';

%params = pcfg ($config);

&die_cleanup ($JREMOVE, sprintf (gettext ("%s: Error: problems parsing configuration file.\n"), $0)) if (! scalar (%params));

# Get a Unix-domain socket

if (-e $sockname) {
	unlink $sockname
		or die_cleanup ($JREMOVE, sprintf (gettext ("%s: Error: cannot delete file %s: %s.\n"), $0, $sockname, $!));
}

if (!defined $stay) {
	chdir '/' or die "$0: can't chdir /: $!\n";
	open STDIN, '/dev/null' or die (sprintf (gettext ("%s: Error: cannot open %s for reading: %s.\n"), $0, "/dev/null", $!));
	open STDOUT, '>/dev/null' or die (sprintf (gettext ("%s: Error: cannot open %s for writing: %s.\n"), $0, "/dev/null", $!));
	defined (my $newpid = fork) or die (sprintf (gettext ("%s: Error: cannot fork: %s.\n"), $0, $!));
	exit 0 if $newpid;
	setsid or die (sprintf (gettext ("%s: Error: cannot set session ID: %s.\n"), $0, $!));
	open STDERR, '>&STDOUT' or die (sprintf (gettext ("%s: Error: cannot open %s for writing: %s.\n"), $0, "/dev/null", $!));
}

# Drop root, excess group privilidges ASAP

my ($GID1, $GID2);

if (defined $params{'printbilld_group'}) {
	$GID1 = (getpwnam ($params{'printbilld_user'}))[3];
	$GID2 = getgrnam ($params{'printbilld_group'});
	$) = "$GID1 $GID2";
} else {
	$GID1 = (getpwnam ($params{'printbilld_user'}))[3];
	$) = "$GID1 $GID1";
}

# Set UID/GID etc. as appropriate

$> = (getpwnam ($params{'printbilld_user'}))[2];
$< = $>;
$( = $);

# I'm an antisocial daemon, after all...

umask 077;

$server = IO::Socket::UNIX -> new (Local => $sockname, Listen => SOMAXCONN)
	or die_cleanup ($JREMOVE, sprintf (gettext ("%s: Error: cannot create socket %s: %s.\n"), $0, $sockname, $!));
					
$SIG{CHLD} = 'IGNORE';
$SIG{HUP} = \&reload_conf;
$SIG{INT} = \&catch_zap;
$SIG{TERM} = \&catch_zap;

while ($client = $server -> accept ()) {
	if ($pid = fork) {
		if ($params{'verbosity'} eq "HIGH") {
			syslog ('info', sprintf (gettext ("Billing process %s started"), $pid))
				or die_cleanup ($JREMOVE, sprintf (gettext ("%s: Error: could not write to syslog: %s.\n"), $0, $!));
		}
	} else {
# We don't want to re-load the configuration file in the middle of a
# processing job. More serious signals will be caught anyway.

		$SIG{HUP} = 'IGNORE';

# Don't internationalise this ;-)
		
		print $client "$0 version: $version\n";
		
		$line = <$client>;
		
		@my_argv = split (';', $line);
		$#my_argv--;
		
		foreach (@my_argv) {
			@tmp = split /\s?:\s?/;
			$tmp[0] =~ /^(.+)$/;
			$tmp[0] = $1;
			$tmp[1] =~ /^(.+)$/;
			$tmp[1] = $1;
			
			$opts{$tmp[0]} = $tmp[1];
		}
		
		@filenames = split /\s+/, $opts{'filenames'};

# Depending on the type of billing process, we either want to return
# immediately or await further processing.

		if ($opts{'type'} eq "bill") {
# lp=/dev/null anyway - so we send the remove command

			print $client "$JSUCC\n";

			&get_slot;

# Now we know the printer name, check the config again...
			
			%params = pcfg ($config, $opts{'printer'});
			
			@total_file_info = &calculate_totals;
			
			$price = &calculate_price (@total_file_info);
			
# Determine whether or not the user is allowed to print.

			&lock ("user_$opts{'user'}");

			if ($total_file_info [0] > 0 && &can_afford ($opts{'user'}, $price)) {
				if ($params{'verbosity'} eq "HIGH") {
					syslog ('info', sprintf (gettext ("Accepting print job from %s."), $opts{'user'}))
						or die_cleanup ($JREMOVE, sprintf (gettext ("%s: Error: could not write to syslog: %s.\n"), $0, $!));
				}

				&update_user_stats ($opts{'user'}, $price, "YES", @total_file_info);
				
				&update_global_stats ($price, @total_file_info);
				
				&print_to_secondary;
			} else {
				if ($params{'verbosity'} eq "HIGH") {
					syslog ('info', sprintf (gettext ("Rejecting print job from %s."), $opts{'user'}))
						or die_cleanup ($JREMOVE, sprintf (gettext ("%s: Error: could not write to syslog: %s.\n"), $0, $!));
				}
	
				if ($total_file_info [0] == 0) {
					&inform_user ($opts{'user'}, gettext ("Zero pages. Nothing to print."));
				} else {
					&inform_user ($opts{'user'}, gettext ("Inadequate quota. See the quota administrator."));
				}
			}
			
			&unlock ("user_$opts{'user'}");
		} elsif ($opts{'type'} eq "lazybill") {
# Check to see if the user has a positive quota before doing any processing.
# This is read-only so doesn't need to be locked.

			tie %userhash, "Printbill::PTDB_File", "$params{'db_home'}/users/$opts{'user'}.db", "TRUE" or do {
				&inform_user ($opts{'user'}, gettext ("You have no quota. See the quota administrator.\n"));
				&die_cleanup ($JREMOVE, "Cannot open file $params{'db_home'}/users/$opts{'user'}.db: $!\n");
			};

			if ($userhash{'quota'} <= 0 && (!defined $userhash{'infinitism'} || (defined $userhash{'infinitism'} && $userhash{'infinitism'} ne "YES"))) {
				&inform_user ($opts{'user'}, gettext ("You do not have sufficient quota to print. See the quota administrator.\n"));

# No need to inform the system administrator. No locking was done - just
# remove the job.
				print $client "$JREMOVE\n";
				
				untie %userhash;
			} elsif (! -w "$params{'db_home'}/users/$opts{'user'}.db") {
				&inform_user ($opts{'user'}, gettext ("There was a serious printbilld server misconfiguration. Tell the sysadmin to check his/her e-mail.\n"));
				&mail_admin (sprintf (gettext ("%s has no write access for %s."), $params{'printbilld_user'}, "$params{'db_home'}/users/$opts{'user'}.db"));

# No need to inform the system administrator. No locking was done - just
# remove the job.
				print $client "$JREMOVE\n";
				
				untie %userhash;
			} else {
				print $client "$JSUCC\n";
				
				if ($params{'verbosity'} eq "HIGH") {
					syslog ('info', sprintf (gettext ("Accepting print job from %s."), $opts{'user'}))
						or die_cleanup ($JREMOVE, sprintf (gettext ("%s: Error: could not write to syslog: %s.\n"), $0, $!));
				}

				untie %userhash;
			
				&get_slot;
			
				%params = pcfg ($config, $opts{'printer'});
				
				@total_file_info = &calculate_totals;
				
				$price = &calculate_price (@total_file_info);
				
				&lock ("user_$opts{'user'}");
				&update_user_stats ($opts{'user'}, $price, "YES", @total_file_info);
				&unlock ("user_$opts{'user'}");
				
				&update_global_stats ($price, @total_file_info);
			}
		} elsif ($opts{'type'} eq "account") {
# Just account the job - we always return success, worry about billing
# afterwards

			print $client "$JSUCC\n";
			
			&get_slot;
			
			%params = pcfg ($config, $opts{'printer'});
			
			@total_file_info = &calculate_totals;
			
			$price = &calculate_price (@total_file_info);
			
			&lock ("user_$opts{'user'}");
			&update_user_stats ($opts{'user'}, $price, "NO", @total_file_info);
			&unlock ("user_$opts{'user'}");
				
			&update_global_stats ($price, @total_file_info);
		} elsif ($opts{'type'} eq "quote") {
			print $client "$JSUCC\n";
			
			&get_slot;

			if (defined $opts{'quote_printer'}) {
				%params = pcfg ($config, $opts{'quote_printer'});
			} else {
				%params = pcfg ($config, $opts{'printer'});
			}

			@total_file_info = &calculate_totals;
			
			$price = &calculate_price (@total_file_info);
			
			&send_quote (@total_file_info);
		}

		&cleanup;

		close $client;
		
		exit 0;
	}
}

# Get a process slot.

sub get_slot {
	my ($i, @pids, $valid_pids);

	open COUNTLOCK, ">$params{'db_home'}/tmp/.printbill_count_lock"
		or die_cleanup ($JREMOVE, sprintf (gettext ("%s: Error: cannot open %s for writing: %s.\n"), $0, "$params{'db_home'}/tmp/.printbill_count_lock", $!));

	flock COUNTLOCK, LOCK_EX
		or die_cleanup ($JREMOVE, sprintf (gettext ("%s: Error: cannot lock %s: %s.\n"), $0, "$params{'db_home'}/tmp/.printbill_count_lock", $!));

# If there is no PID directory, create it. Otherwise, check to see if we
# have a process slot available.

	if (! -d "$params{'db_home'}/tmp/.printbill_pids") {
		mkdir "$params{'db_home'}/tmp/.printbill_pids", 448
			or die_cleanup ($JREMOVE, sprintf (gettext ("%s: Error: directory %s does not exist and could not be created: %s.\n"), $0, "$params{'db_home'}/tmp/.printbill_pids", $!));
	} else {
		while (1) {
# Get a count of still-valid PIDs...
			opendir PIDS_DIR, "$params{'db_home'}/tmp/.printbill_pids/"
				or die_cleanup ($JREMOVE, sprintf (gettext ("%s: Error: cannot open directory %s for reading: %s.\n"), $0, "$params{'db_home'}/tmp/.printbill_pids/", $!));
			
			@pids = readdir PIDS_DIR
				or die_cleanup ($JREMOVE, sprintf (gettext ("%s: Error: cannot read from directory %s: %s.\n"), $0, "$params{'db_home'}/tmp/.printbill_pids/", $!));

			@pids = grep { !/^\./ } @pids;

			closedir PIDS_DIR
				or die_cleanup ($JREMOVE, sprintf (gettext ("%s: Error: cannot close directory %s: %s.\n", $0), "$params{'db_home'}/tmp/.printbill_pids/", $!));
		
			$valid_pids = 0;
		
			for $i (@pids) {
				if ($i =~ /^([-\@\w.\/+:\$\s,]+)$/) {
					$i = $1;
				} else {
					die_cleanup (-1, sprintf (gettext ("%s: Error: illegal characters in PID \"%i\".\n"), $0, $i));
				}
				
				if (-d "/proc/$i") { # Found one still running
					$valid_pids++;
				} else { # If a stale PID is still here, remove it.
					unlink "$params{'db_home'}/tmp/.printbill_pids/$i"
						or die_cleanup ($JREMOVE, sprintf (gettext ("%s: Error: cannot delete PID file %s: %s.\n"), $0, "$params{'db_home'}/tmp/.printbill_pids/$i", $!));
				}
			}
			
			last if ($valid_pids < $params{'bill_max_processes'});
		
			sleep ($params{'retry_interval'});
		}
	}

# Claim a process slot and release the lockfile.

	open PIDLOCK, ">$params{'db_home'}/tmp/.printbill_pids/$$" or do {
		&die_cleanup ($JREMOVE, sprintf (gettext ("%s: Error: cannot create PID file %s: %s.\n"), $0, "$params{'db_home'}/tmp/.printbill_pids/$$", $!));
	};
	
	close PIDLOCK;

	close COUNTLOCK
		or die_cleanup ($JREMOVE, "%s: Error: cannot close lockfile %s: %s.\n", $0, "$params{'db_home'}/tmp/.printbill_count_lock", $!);
}

# This is the CPU-intensive part. We start up the billing procedure. Step
# through all files, issue a call to printbill and work out the cost based
# on the prices in the configuration file.

sub calculate_totals {
	my ($i, $j, @times, @prev_times, @delta_t, $fsize, $niceness, @file_info, @total_file_info, $now);

	@times = (0, 0, 0, 0);
	@prev_times = (0, 0, 0, 0);
	
	for ($i = 0; $i <= $#filenames; $i++) {
		$fsize = (stat ("$opts{'tempdir'}/$filenames[$i]"))[7]
			or die_cleanup ($JREMOVE, sprintf (gettext ("%s: Error: cannot stat %s: %s.\n"), $0, "$opts{'tempdir'}/$filenames[$i]", $!));

		if ($fsize > $params{'bill_nicethreshold'}) {
			$niceness = $params{'large_bill_niceness'};
		} else {
			$niceness = $params{'small_bill_niceness'};
		}
		
# $now corresponds approximately to the time the job was received at the
# printbill daemon. This may be quite different from the time when the job
# was enqueued, but hopefully close enough.

		$now = time;
		@prev_times = times;
		@file_info = printbill ("$opts{'tempdir'}/$filenames[$i]", $niceness, "$params{'db_home'}/tmp", %params);
		@times = times;

# Try to hard link/copy the file into $params{'save_bad_path'}, if not, no
# sweat, we're about to die anyway...

		if ($file_info [0] ne "") {
			if (defined $params{'save_bad_path'}) {
				link "$opts{'tempdir'}/$filenames[$i]", "$params{'save_bad_path'}/FAILED_$opts{'printer'}_$filenames[$i]"
					or `/bin/cp $opts{'tempdir'}/$filenames[$i] $params{'save_bad_path'}/FAILED_$opts{'printer'}_$filenames[$i] 2>&1`;
			
				die_cleanup ($JREMOVE, sprintf (gettext ("%s: Error: Printbill::printbill failed [user = %s]:\n%s\nOffending file saved as %s.\n"), $0, $opts{'user'}, $file_info[0], "$params{'save_bad_path'}/FAILED_$opts{'printer'}_$filenames[$i]"));
			} else {
				die_cleanup ($JREMOVE, sprintf (gettext ("%s: Error: Printbill::printbill failed [user = %s]:\n%s.\n"), $0, $opts{'user'}, $file_info[0]));
			}
		}
				
		for ($j = 0; $j < 5; $j++) {
			$total_file_info [$j] += $file_info [$j + 1];
		}
		
# We print user/system times, child user/system times, file size, page count, cyan, magenta, yellow, black
# Note: CMY is zero if the printer is mono. Nothing will be printed if there
# are no pages of output.

		if (defined $params{'stats_path'} && $file_info [1] != 0) {
			for ($j = 0; $j < 4; $j++) {
				$delta_t[$j] = $times[$j] - $prev_times[$j];
			}

			&lock ("stats");
			
			umask 033;

			open STATS, ">>$params{'stats_path'}/printbill_stats_$opts{'printer'}.dat"
				or die_cleanup ($JREMOVE, sprintf (gettext ("%s: Error: cannot open stats file %s for writing: %s.\n"), $0, "$params{'stats_path'}/printbill_stats_$opts{'printer'}.dat", $!));

			print STATS "$now\t$delta_t[0]\t$delta_t[1]\t$delta_t[2]\t$delta_t[3]\t$fsize\t$file_info[1]\t$file_info[2]\t$file_info[3]\t$file_info[4]\t$file_info[5]\n"
				or die_cleanup ($JREMOVE, sprintf (gettext ("%s: Error: cannot write to stats file %s: %s.\n"), $0, "$params{'stats_path'}/printbill_stats_$opts{'printer'}.dat", $!));

			close STATS
				or die_cleanup ($JREMOVE, sprintf (gettext ("%s: Error: cannot close stats file %s: %s.\n"), $0, "$params{'stats_path'}/printbill_stats_$opts{'printer'}.dat", $!));

			umask 077;

			&unlock ("stats");
		}
	}
	
	return @total_file_info;
}

sub calculate_price {
	my @total_file_info = @_;

# Mono + pagecount is default.

	if (!defined $params{'colourspace'} || $params{'colourspace'} eq 'mono') {
		return $total_file_info [0] * $params{'price_per_page'} +
			$total_file_info [4] * $params{'price_per_percent_black'};
	} elsif ($params{'colourspace'} eq 'pagecount') {
# Pagecount only
		return $total_file_info [0] * $params{'price_per_page'};
	} else {
# (colour + pagecount) * copies (copies parameter only from printbill.cups)
		return ($total_file_info [0] * $params{'price_per_page'} +
			$total_file_info [1] * $params{'price_per_percent_colour'} +
			$total_file_info [2] * $params{'price_per_percent_colour'} +
			$total_file_info [3] * $params{'price_per_percent_colour'} +
			$total_file_info [4] * $params{'price_per_percent_black'})
			* (defined $opts{'copies'}) ? $opts{'copies'} : 1;
	}
}

sub can_afford {
	my ($user, $price) = @_;
	my $yes;
	
	tie %userhash, "Printbill::PTDB_File", "$params{'db_home'}/users/$user.db", "TRUE" or do {
		&inform_user ($user, gettext ("You have no quota. See the quota administrator.\n"));
		&die_cleanup ($JREMOVE, sprintf (gettext ("%s: Error: cannot open %s for writing: %s.\n"), $0, "$params{'db_home'}/users/$user.db", $!));
	};

	$yes = (($userhash{'quota'} - $price) >= 0) || (defined $userhash{'infinitism'} && $userhash{'infinitism'} eq "YES");
	
	untie %userhash;
	
	return $yes;
}

sub update_user_stats {
	my ($user, $price, $update_quota, @total_file_info) = @_;
	my (%userhash);

	tie %userhash, "Printbill::PTDB_File", "$params{'db_home'}/users/$user.db", "FALSE" or do {
		&inform_user ($user, gettext ("You have no quota. See the quota administrator.\n"));
		&die_cleanup ($JREMOVE, sprintf (gettext ("%s: Error: cannot open file %s for writing: %s.\n"), $0, "$params{'db_home'}/users/$user.db", $!));
	};

	if ($params{'verbosity'} eq "HIGH") {
		syslog ('info', sprintf (gettext ("%s has requested a total of %i pages at a price of %s%.2f from remaining credit of %s%.2f."), $user, $total_file_info [0], $params{'currency_symbol'}, $price, $params{'currency_symbol'}, $userhash{'quota'}))
			or die_cleanup ($JREMOVE, sprintf (gettext ("%s: Error: could not write to syslog: %s.\n"), $0, $!));
	}

# Update the quota, total spending and cumulative total pagecount

	if ($update_quota eq "YES" && !(defined $userhash{'infinitism'} && $userhash{'infinitism'} eq "YES")) {
		$userhash{'quota'} -= $price;
	}

# Update the user's total printing expenditure

	$userhash{'spent'} += $price;

# Update the user's page count

	$userhash{'pages'} += $total_file_info [0];

# Update the user's toner/ink consumption stats

	$userhash{'cyan'} += $total_file_info [1];
	$userhash{'magenta'} += $total_file_info [2];
	$userhash{'yellow'} += $total_file_info [3];
	$userhash{'black'} += $total_file_info [4];

	if (!defined $userhash{"$opts{'printer'}.cyan"}) {
		$userhash{"$opts{'printer'}.cyan"} = 0;
	}
	
	if (!defined $userhash{"$opts{'printer'}.magenta"}) {
		$userhash{"$opts{'printer'}.magenta"} = 0;
	}
	
	if (!defined $userhash{"$opts{'printer'}.yellow"}) {
		$userhash{"$opts{'printer'}.yellow"} = 0;
	}
	
	if (!defined $userhash{"$opts{'printer'}.black"}) {
		$userhash{"$opts{'printer'}.black"} = 0;
	}
	
	if (!defined $userhash{"$opts{'printer'}.pages"}) {
		$userhash{"$opts{'printer'}.pages"} = 0;
	}
	
	$userhash{"$opts{'printer'}.cyan"} += $total_file_info [1];
	$userhash{"$opts{'printer'}.magenta"} += $total_file_info [2];
	$userhash{"$opts{'printer'}.yellow"} += $total_file_info [3];
	$userhash{"$opts{'printer'}.black"} += $total_file_info [4];
	$userhash{"$opts{'printer'}.pages"} += $total_file_info [0];

	if ($params{'verbosity'} eq "HIGH") {
		syslog ('info', sprintf (gettext ("%s has a remaining credit of %s%.2f"), $user, $params{'currency_symbol'}, $userhash{'quota'}))
			or die_cleanup ($JREMOVE, sprintf (gettext ("%s: Error: could not write to syslog: %s.\n"), $0, $!));
	}

	untie %userhash;
}

sub update_global_stats {
	my ($price, @total_file_info) = @_;
	my (%mischash, %printerhash, $existing, $uid, $gid);
	
	&lock ("misc");

	tie %mischash, "Printbill::PTDB_File", "$params{'db_home'}/misc.db", "FALSE" 
		or &die_cleanup ($JREMOVE, sprintf (gettext ("%s: Error: cannot open file %s for writing: %s.\n"), $0, "$params{'db_home'}/misc.db", $!));

	$mischash{'total spent'} += $price;
	$mischash{'total pages'} += $total_file_info [0];

	untie %mischash;

	&unlock ("misc");
		
# Update stats for this printer

	&lock ("printer_$opts{'printer'}");
	
	if (-f "$params{'db_home'}/printers/$opts{'printer'}.db") {
		$existing = 1;
	} else {
		$existing = 0;
	}

	tie %printerhash, "Printbill::PTDB_File", "$params{'db_home'}/printers/$opts{'printer'}.db", "FALSE" 
		or &die_cleanup ($JREMOVE, sprintf (gettext ("%s: Error: cannot open file %s for writing: %s.\n"), $0, "$params{'db_home'}/printers/$opts{'printer'}.db", $!));

	if (!defined ($printerhash{'total pages'})) {
		$printerhash{'total pages'} = 0;
	}

	$printerhash{'total pages'} += $total_file_info [0];
	$printerhash{'colourspace'} = $params{'colourspace'};

	if ($params{'colourspace'} ne "cmy") {
		if (!defined ($printerhash{'estimated black'})) {
			$printerhash{'estimated black'} = 0;
		}
		
		$printerhash{'estimated black'} += $total_file_info [4];
	}

	if ($params{'colourspace'} ne "mono") {
		if (!defined ($printerhash{'estimated cyan'})) {
			$printerhash{'estimated cyan'} = 0;
		}

		if (!defined ($printerhash{'estimated magenta'})) {
			$printerhash{'estimated magenta'} = 0;
		}

		if (!defined ($printerhash{'estimated yellow'})) {
			$printerhash{'estimated yellow'} = 0;
		}

		$printerhash{'estimated cyan'} += $total_file_info [1];
		$printerhash{'estimated magenta'} += $total_file_info [2];
		$printerhash{'estimated yellow'} += $total_file_info [3];
	}
	
	untie %printerhash;

	if (! $existing) {
		$uid = (getpwnam ($params{'printbilld_user'}))[2];

		if (defined $params{'printbilld_group'}) {
			$gid = getgrnam ($params{'printbilld_group'});
		} else {
			$gid = (getpwnam ($params{'printbilld_user'}))[3];
		}
		
		chown $uid, $gid, "$params{'db_home'}/printers/$opts{'printer'}.db"
			or die_cleanup (-1, sprintf (gettext ("%s: could not set ownership on file %s to %s: %s.\n"), $0, "$params{'db_home'}/printers/$opts{'printer'}.db", "$uid.$gid", $!));

		chmod 0664, "$params{'db_home'}/printers/$opts{'printer'}.db"
			or die_cleanup (-1, sprintf (gettext ("%s: could not set mode on file %s to %s: %s.\n"), $0, "$params{'db_home'}/printers/$opts{'printer'}.db", "0664", $!));
	}

	&unlock ("printer_$opts{'printer'}");
}

sub print_to_secondary {
	my $i;
	
	for ($i = 0; $i <= $#filenames; $i++) {
		`$params{'lpr'} -P$opts{'secondary_queue'} $opts{'tempdir'}/$filenames[$i]`;
	}
}

sub inform_user
{
	my ($the_user, $the_errors, $price) = @_;

	my ($name, @pwent);
	my $text = `/bin/cat $configdir/mail_message`;
	my %userhash;
	
# Should we syslog here too? Probably not necessary

	if ($params{'verbosity'} eq "HIGH") {
		printf STDERR gettext ("%s: Errors have occurred:\n\n%s\n"), $0, $the_errors;
	}

# Try to look up the user's real name, if that fails, just use the username

	if (@pwent = getpwnam ($the_user)) {
		$name = (split ",", $pwent[6])[0];
	} else {
		$name = $the_user;
	}

	if ($the_errors eq "") {
		$the_errors = "No errors.";
	}

# If the user has a quota, we can tell them what it is (in the event of some
# problem...)

	tie %userhash, "Printbill::PTDB_File", "$params{'db_home'}/users/$opts{'user'}.db", "TRUE"
		or syslog ('err', sprintf (gettext ("%s: Error: cannot open %s for reading: %s.\n"), $0, "$params{'db_home'}/users/$opts{'user'}.db", $!));

	if ($params{'response_method'} =~ /mail/) {
		open MAIL, "|$params{'mta'} $the_user"
			or die_cleanup ($JREMOVE, sprintf (gettext ("%s: Error: cannot open pipe to \"%s\": %s.\n"), $0, $params{'mta'}, $!));
	
		print MAIL "Reply-to: $params{'admin_mail'}\n";
		print MAIL "From: $params{'admin_mail'}\n";
		print MAIL "To: $name <$the_user>\n";
		print MAIL "Subject: ", gettext ("Print job failed\n\n");
		print MAIL $text;
		print MAIL "Job no: $opts{'job'}\n";
		print MAIL "Errors: $the_errors\n";

		if (defined ($userhash{'quota'}) && defined $price) {
			printf MAIL gettext ("Total cost of print job: %s%.2f.\n"), $params{'currency_symbol'}, $price;
			printf MAIL gettext ("Total remaining credit: %s%.2f.\n"), $params{'currency_symbol'}, $userhash{'quota'};
		}
	
		close MAIL
			or die_cleanup ($JREMOVE, sprintf (gettext ("%s: Error: cannot close pipe to \"%s\": %s.\n"), $0, $params{'mta'}, $!));
	}

# If the host is not running smbclient (say, a Linux box in a mixed
# Linux/Windows environment) the open() succeeds but it only lets you enter
# a single line. You mightn't want it to die_cleanup() in this case. So we
# just terminate the loop if this happens - don't even make a note in the
# log. If it isn't working, the users will notice soon enough.

	if ($params{'response_method'} =~ /smbclient/ && $opts{'jobname'} =~ 'smbprn' && $opts{'remote_host'}) {{
		open SMBCLIENT,	"|$params{'smbclient'} -M $opts{'remote_host'}"
			or die_cleanup ($JREMOVE, sprintf (gettext ("%s: Error: cannot open pipe to \"%s\": %s.\n"), $0, $params{'smbclient'}, $!));

		my $msg = sprintf gettext ("Print job failed\n\nJob no: %s\nErrors: %s\n"), $opts{'job'}, $the_errors;
		
		if (defined ($userhash{'quota'}) && defined $price) {
			$msg .= sprintf gettext ("Total cost of print job: %s%.2f.\n"), $params{'currency_symbol'}, $price;
			$msg .= sprintf gettext ("Total remaining credit: %s%.2f.\n"), $params{'currency_symbol'}, $userhash{'quota'};
		}
		
		last if print SMBCLIENT $msg;
		last if close SMBCLIENT;
	}}
	
	untie %userhash;
}

sub send_quote {
	my (@total_file_info) = @_;
	my ($name, %userhash, @pwent);
	
	if (@pwent = getpwnam ($opts{'user'})) {
		$name = (split ",", $pwent[6])[0];
	} else {
		$name = $opts{'user'};
	}

	$name = (split (",", $name))[0];

	open MAIL, "|$params{'mta'} $opts{'user'}"
		or die_cleanup ($JREMOVE, sprintf (gettext ("%s: Error: cannot open pipe to \"%s\": %s.\n"), $0, $params{'mta'}, $!));

	print MAIL "Reply-to: $params{'admin_mail'}\n";
	print MAIL "From: $params{'admin_mail'}\n";
	print MAIL "To: $name <$opts{'user'}>\n";
	print MAIL "Subject: ", gettext ("Quote for your print job.\n\n");

	tie %userhash, "Printbill::PTDB_File", "$params{'db_home'}/users/$opts{'user'}.db", "TRUE"
		or syslog ('err', sprintf (gettext ("%s: Error: cannot open %s for reading: %s.\n"), $0, "$params{'db_home'}/users/$opts{'user'}.db", $!));

	if ($params{'response_method'} =~ /mail/) {
		if (defined (tied %userhash)) {
			printf MAIL gettext ("
Your print job to printer \"%s\" will cost %s%.2f.
You have %s%.2f remaining on your print quota.

Total number of pages: %i

Regards,

Your friendly print billing service.
"), $opts{'printer'}, $params{'currency_symbol'}, $price, $params{'currency_symbol'}, $userhash{'quota'}, $total_file_info[0];
		} else {
			printf MAIL gettext ("
Your print job on printer \"%s\" has been costed at %s%.2f.
You have NO ENTRY in the print quota system. Talk to the system
administrator to get some quota.

Total number of pages: %i

Regards,

Your friendly print billing service.
"), $opts{'printer'}, $params{'currency_symbol'}, $price, $total_file_info[0];
		}
	
		close MAIL
			or die_cleanup ($JREMOVE, sprintf (gettext ("%s: Error: cannot close pipe to \"%s\": %s.\n"), $0, $params{'mta'}, $!));
	}

	if ($params{'response_method'} =~ /smbclient/ && $opts{'jobname'} =~ 'smbprn' && $opts{'remote_host'}) {{
		open SMBCLIENT,	"|$params{'smbclient'} -M $opts{'remote_host'}"
			or die_cleanup ($JREMOVE, sprintf (gettext ("%s: Error: cannot open pipe to \"%s\": %s.\n"), $0, $params{'smbclient'}, $!));

		my $msg = sprintf gettext ("Total pages: %i\n"), $total_file_info[0];
		$msg .= sprintf gettext ("Total cost of print job: %s%.2f.\n"), $params{'currency_symbol'}, $price;
	
		if (defined ($userhash{'quota'})) {
			$msg .= sprintf gettext ("Total remaining credit: %s%.2f.\n"), $params{'currency_symbol'}, $userhash{'quota'};
		} else {
			$msg .= gettext ("You have NO ENTRY in the print quota system.\n");
		}
	
		last if print SMBCLIENT $msg;
		last if close SMBCLIENT;
	}}

	untie %userhash;
}

# We don't use proper flock() or fcntl() because we would need to keep track
# of file descriptors. This way is simple - we just need to keep track of
# the filename.

sub lock
{
	my ($text) = @_;
	my $lockpid;

	while (-e "$params{'db_home'}/tmp/.printbill_$text.lock") {
		open (LOCKFILE, "<$params{'db_home'}/tmp/.printbill_$text.lock")
			or die_cleanup ($JREMOVE, sprintf (gettext ("%s: Error: cannot open %s for reading: %s.\n"), $0, "$params{'db_home'}/tmp/.printbill_$text.lock", $!));
		
		flock LOCKFILE, LOCK_EX
			or die_cleanup ($JREMOVE, sprintf (gettext ("%s: Error: cannot lock %s: %s.\n"), $0, "$params{'db_home'}/tmp/.printbill_$text.lock", $!));

		$lockpid = <LOCKFILE>;
		chomp $lockpid;
		
# Is the locking process still running? If not, we can safely nuke the file
# and lock it ourselves.

		last if (! -d "/proc/$lockpid");

# Otherwise, we have to wait.

		close LOCKFILE
			or die_cleanup ($JREMOVE, sprintf (gettext ("%s: Error: cannot close lockfile %s: %s.\n"), $0, "$params{'db_home'}/tmp/.printbill_$text.lock", $!));

		sleep $params{'retry_interval'};
	}
	
	open (LOCKFILE, ">$params{'db_home'}/tmp/.printbill_$text.lock")
		or die_cleanup ($JREMOVE, sprintf (gettext ("%s: Error: cannot open %s for writing: %s.\n"), $0, "$params{'db_home'}/tmp/.printbill_$text.lock", $!));
	
	flock LOCKFILE, LOCK_EX
		or die_cleanup ($JREMOVE, sprintf (gettext ("%s: Error: cannot lock %s: %s.\n"), $0, "$params{'db_home'}/tmp/.printbill_$text.lock", $!));

	print LOCKFILE $$
		or die_cleanup ($JREMOVE, sprintf (gettext ("%s: Error: cannot write to %s: %s.\n"), $0, "$params{'db_home'}/tmp/.printbill_$text.lock", $!));

	$locks{$text} = 1;

	close LOCKFILE
		or die_cleanup ($JREMOVE, sprintf (gettext ("%s: Error: cannot close lockfile %s: %s.\n"), $0, "$params{'db_home'}/tmp/.printbill_$text.lock", $!));
}

sub unlock
{
	my ($text) = @_;

	if (-e "$params{'db_home'}/tmp/.printbill_$text.lock") {
		unlink "$params{'db_home'}/tmp/.printbill_$text.lock"
			or die_cleanup ($JREMOVE, "%s: Error: cannot remove lockfile %s: %s.\n", $0, "$params{'db_home'}/tmp/.printbill_$text.lock", $!);
	
		delete $locks{$text};
	} else {
		printf gettext ("%s: Warning: %s doesn't exist.\n"), $0, "$params{'db_home'}/tmp/.printbill_$text.lock";
	}
}

# Wash our hands of the whole affair

sub cleanup {
	my $key;
	
	if (%locks) {
		for $key (keys %locks) {
			&unlock ($key);
		}
	}

	if (defined scalar (%opts) && defined $opts{'tempdir'}) {
		my $answer = `/bin/rm -rf $opts{'tempdir'}`;
	
		if ($answer ne "") {
			syslog ('err', sprintf (gettext ("%s: Error: cannot recursively remove directory %s: %s.\n"), $0, $opts{'tempdir'}, $!))
				or die sprintf (gettext ("%s: Error: could not write to syslog: %s.\n"), $0, $!);
		}
	}

# This may have already been deleted.

	if (%params && -f "$params{'db_home'}/tmp/.printbill_pids/$$") {
		unlink "$params{'db_home'}/tmp/.printbill_pids/$$"
			or syslog ('err', sprintf (gettext ("%s: Error: cannot delete PID file %s: %s.\n"), $0, "$params{'db_home'}/tmp/.printbill_pids/$$", $!))
				or die sprintf (gettext ("%s: Error: could not write to syslog: %s.\n"), $0, $!);
	}
}

# Try to write to syslog & stderr

sub die_cleanup
{
	my ($the_return_val, $msg) = @_;

	&cleanup;

# Remove the socket...
	
	if ($$ == $mum && -e $sockname) {
		unlink $sockname
			or syslog ('err', sprintf (gettext ("%s: Error: cannot delete PID file %s: %s.\n"), $0, $sockname, $!))
				or die sprintf (gettext ("%s: Error: could not write to syslog: %s.\n"), $0, $!);
	}

	&mail_admin ($msg);

	printf STDERR $msg;

	syslog ('err', $msg)
		or die sprintf (gettext ("%s: Error: could not write to syslog: %s.\n"), $0, $!);

	if (defined $client) {
		print $client "$the_return_val\n"
	}

	exit 0;
}

sub mail_admin
{
	my ($the_errors) = @_;
	my $lockpid;
	
	return if (! %params);

	open MAIL, "|$params{'mta'} $params{'admin_mail'}"
		or syslog ('err', sprintf (gettext ("%s: Error: cannot open pipe to \"%s\": %s.\n"), $0, $params{'mta'}, $!))
		or printf STDERR (gettext ("%s: Error: could not write to syslog: %s.\n"), $0, $!);
	
	print MAIL "Reply-to: $params{'admin_mail'}\n";
	print MAIL "From: $params{'admin_mail'}\n";
	print MAIL "Subject: ", gettext ("Print job failed\n\n");
	print MAIL "$the_errors\n";
	
	close MAIL
		or syslog ('err', sprintf (gettext ("%s: Error: cannot close pipe to \"%s\": %s.\n"), $0, $params{'mta'}, $!))
		or printf STDERR (gettext ("%s: Error: could not write to syslog: %s.\n"), $0, $!);
}

# Deal with signals. Die, but clean up our mess first.

sub catch_zap {
	&cleanup;
	exit 0;
}

sub reload_conf {
	$SIG{HUP} = \&reload_conf;
	print STDERR gettext ("Configuration file reloaded.\n");

	syslog ('info', gettext ("Configuration file reloaded.\n"))
		or die_cleanup ($JREMOVE, sprintf (gettext ("%s: Error: could not write to syslog: %s.\n"), $0, $!));

	%params = pcfg ($config, $opts{'printer'});
}
