#!/usr/bin/perl -w

##############################################################################
#
# Print billing management system
#
# Version 4.1.2
#
# 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 POSIX 'setsid';
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;

my $server;
my $client;
my $pid;
my $version = '4.1.2';
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);

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

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

%params = pcfg ($config);

die_cleanup ($JREMOVE, "$0: problems parsing configuration file\n") if (!defined scalar (%params));

setlogsock ('unix') or die_cleanup ($JREMOVE, "$0: cannot set log type: $!\n");

# Openlog doesn't return success or failure. Stupid.

openlog ($0, 'cons,pid', 'lpr');

# Get a Unix-domain socket

unlink $sockname;

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

# 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);
					
$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', "Billing process $pid started")
				or die_cleanup ($JREMOVE, "$0: could not write to syslog: $!\n");
		}
	} 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';
		
		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', "Accepting print job from $opts{'user'}")
						or die_cleanup ($JREMOVE, "$0: could not write to syslog: $!\n");
				}

				&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', "Rejecting print job from $opts{'user'}")
						or die_cleanup ($JREMOVE, "$0: could not write to syslog: $!\n");
				}
	
				if ($total_file_info [0] == 0) {
					&inform_user ($opts{'user'}, "Zero pages. I'm certainly not going to print that.");
				} else {
					&inform_user ($opts{'user'}, "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'}, "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'}, "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'}, "There was a serious printbilld server misconfiguration. Tell the sysadmin to check his/her e-mail.\n");
				&mail_admin ("$params{'printbilld_user'} has no write access for $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', "Accepting print job from $opts{'user'}")
						or die_cleanup ($JREMOVE, "$0: could not write to syslog: $!\n");
				}

				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, "Could not open $params{'db_home'}/tmp/.printbill_count_lock for writing: $!\n");

	flock COUNTLOCK, LOCK_EX
		or die_cleanup ($JREMOVE, "Could not lock $params{'db_home'}/tmp/.printbill_count_lock: $!\n");

# 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, "Directory $params{'db_home'}/tmp/.printbill_pids does not exist and could not be created: $!\n");
	} else {
		while (1) {
# Get a count of still-valid PIDs...
			opendir PIDS_DIR, "$params{'db_home'}/tmp/.printbill_pids/"
				or die_cleanup ($JREMOVE, "$0: Cannot open directory $params{'db_home'}/tmp/.printbill_pids/ for reading: $!\n");
			
			@pids = readdir PIDS_DIR
				or die_cleanup ($JREMOVE, "$0: Cannot read from directory $params{'db_home'}/tmp/.printbill_pids/: $!\n");

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

			closedir PIDS_DIR
				or die_cleanup ($JREMOVE, "$0: Cannot close directory $params{'db_home'}/tmp/.printbill_pids/: $!\n");
		
			$valid_pids = 0;
		
			for $i (@pids) {
				if ($i =~ /^([-\@\w.\/+:\$\s,]+)$/) {
					$i = $1;
				} else {
					die_cleanup (-1, "$0: illegal characters in alleged PID \"$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, "$0: Cannot delete PID file $params{'db_home'}/tmp/.printbill_pids/$i: $!\n");
				}
			}
			
			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, "$0: Unable to create $params{'db_home'}/tmp/.printbill_pids/$$\n");
	};
	
	close PIDLOCK;

	close COUNTLOCK
		or die_cleanup ($JREMOVE, "$0: Unable to close COUNTLOCK: $!\n");
}

# 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, "$0: Unable to stat $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, "$0: Printbill::printbill failed [user = $opts{'user'}]:\n$file_info[0]\nOffending file saved as $params{'save_bad_path'}/FAILED_$opts{'printer'}_$filenames[$i]\n");
			} else {
				die_cleanup ($JREMOVE, "$0: Printbill::printbill failed [user = $opts{'user'}]:\n$file_info[0]\n");
			}
		}
				
		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, "$0: Cannot open stats file $params{'stats_path'}/printbill_stats_$opts{'printer'}.dat for writing: $!\n");

			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, "$0: Cannot write to stats file $params{'stats_path'}/printbill_stats_$opts{'printer'}.dat: $!\n");

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

			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
		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'};
	}
}

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, "You have no quota. See the quota administrator.\n");
		&die_cleanup ($JREMOVE, "Cannot open file $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, $msg);

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

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

# 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 ($params{'verbosity'} eq "HIGH") {
		$msg = sprintf ("$user has a remaining credit of $params{'currency_symbol'}%.2f", $userhash{'quota'}); 
		syslog ('info', $msg)
			or die_cleanup ($JREMOVE, "$0: could not write to syslog: $!\n");
	}

	untie %userhash;
}

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

	tie %mischash, "Printbill::PTDB_File", "$params{'db_home'}/misc.db", "FALSE" 
		or &die_cleanup ($JREMOVE, "$0: cannot open file \"$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'}");

	tie %printerhash, "Printbill::PTDB_File", "$params{'db_home'}/printers/$opts{'printer'}.db", "FALSE" 
		or &die_cleanup ($JREMOVE, "Cannot open file \"$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;

	&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") {
		print STDERR "Errors have occurred:\n\n$the_errors\n";
	}

# 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 ($params{'response_method'} =~ /mail/) {
		open MAIL, "|$params{'mta'} $the_user"
			or die_cleanup ($JREMOVE, "Can't open pipe to \"$params{'mta'}\": $!\n");
	
		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: 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 "Total cost of print job: $params{'currency_symbol'}%.2f\n", $price;
			printf MAIL "Total remaining credit: $params{'currency_symbol'}%.2f\n", $userhash{'quota'};
		}
	
		close MAIL
			or die_cleanup ($JREMOVE, "$0: Unable to close pipe to \"$params{'mta'}\": $!\n");
	}

# 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.

# Just a quick sanity check... we are unlikely to have a print job from a
# Unix machine with this filename. And if we do, it doesn't really matter...

	if ($params{'response_method'} =~ /smbclient/ && $opts{'jobname'} =~ 'smbprn' && $opts{'jobname'} =~ '@@@') {{
		my $smbhostname = (split '@@@', $opts{'jobname'})[1];

# Abort if that doesn't seem to have yielded a meaningful hostname...

		last if (!defined $smbhostname);
	
		open SMBCLIENT,	"|$params{'smbclient'} -M $smbhostname"
			or die_cleanup ($JREMOVE, "Can't open pipe to \"$params{'smbclient'}\": $!\n");

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

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, "Can't open $params{'mta'}: $!\n");

	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: Quote for your print job.\n\n";

	tie %userhash, "Printbill::PTDB_File", "$params{'db_home'}/users/$opts{'user'}.db", "TRUE"
		or syslog ('err', "Cannot open file $params{'db_home'}/users/$opts{'user'}.db: $!");

	if ($params{'response_method'} =~ /mail/) {
		if (defined (tied %userhash)) {
			printf MAIL "
Your print job to printer \"$opts{'printer'}\" has been costed at $params{'currency_symbol'}%.2f.
You have $params{'currency_symbol'}%.2f remaining on your print quota.

Total number of pages: $total_file_info[0]

Regards,

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

Total number of pages: $total_file_info[0]

Regards,

Your friendly print billing service.
", $price;
		}
	
		close MAIL
			or die_cleanup ($JREMOVE, "$0: Unable to close $params{'mta'}: $!\n");
	}

# Just a quick sanity check... we are unlikely to have a print job from a
# Unix machine with this filename. And if we do, it doesn't really matter...

	if ($params{'response_method'} =~ /smbclient/ && $opts{'jobname'} =~ 'smbprn' && $opts{'jobname'} =~ '@@@') {{
		my $smbhostname = (split '@@@', $opts{'jobname'})[1];
		
# Abort if that doesn't seem to have yielded a meaningful hostname...

		last if (!defined $smbhostname);
	
		open SMBCLIENT,	"|$params{'smbclient'} -M $smbhostname"
			or die_cleanup ($JREMOVE, "Can't open pipe to \"$params{'smbclient'}\": $!\n");

		my $msg = sprintf "Total pages: $total_file_info[0]\n";
		$msg .= sprintf "Total cost of print job: $params{'currency_symbol'}%.2f\n", $price;
	
		if (defined ($userhash{'quota'})) {
			$msg .= sprintf "Total remaining credit: $params{'currency_symbol'}%.2f\n", $userhash{'quota'};
		} else {
			$msg .= "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, "$0: cannot open lockfile $params{'db_home'}/tmp/.printbill_$text.lock: $!\n");
		
		flock LOCKFILE, LOCK_EX
			or die_cleanup ($JREMOVE, "$0: cannot lock lockfile $params{'db_home'}/tmp/.printbill_$text.lock: $!\n");

		$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, "$0: cannot close lockfile: $!\n");

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

	print LOCKFILE $$
		or die_cleanup ($JREMOVE, "$0: cannot write to lockfile: $!\n");

	$locks{$text} = 1;

	close LOCKFILE
		or die_cleanup ($JREMOVE, "$0: cannot close lockfile: $!\n");
}

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, "$0: cannot remove lockfile $params{'db_home'}/tmp/.printbill_$text.lock: $!\n");
	
		delete $locks{$text};
	} else {
		print "$params{'db_home'}/tmp/.printbill_$text.lock doesn't exist...\n";
	}
}

# 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', "$0: cannot remove directory \"$opts{'tempdir'}\": $!")
				or die "$0: cannot write to syslog: $!\n";
		}
	}

# This may have already been deleted.

	if (-f "$params{'db_home'}/tmp/.printbill_pids/$$") {
		unlink "$params{'db_home'}/tmp/.printbill_pids/$$"
			or syslog ('err', "$0: Unable to remove $params{'db_home'}/tmp/.printbill_pids/$$: $!")
				or die "$0: cannot write to syslog: $!\n";
	}
}

# Try to write to syslog - if it fails, write to stderr.

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

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

	syslog ('err', $msg)
		or die "$0: cannot write \`$msg\' to syslog: $!\n";

	exit 0;
}

sub mail_admin
{
	my ($the_errors) = @_;
	my $lockpid;

	open MAIL, "|$params{'mta'} $params{'admin_mail'}"
		or syslog ('err', "$0: Unable to open $params{'mta'}: $!")
		or die "$0: cannot write to syslog: $!\n";
	
	print MAIL "Reply-to: $params{'admin_mail'}\n";
	print MAIL "From: $params{'admin_mail'}\n";
	print MAIL "Subject: Print job failed\n\n";
	print MAIL "$the_errors\n";
	
	close MAIL
		or syslog ('err', "$0: Unable to close $params{'mta'}: $!")
		or die "$0: cannot write to syslog: $!\n";
}

# 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 "Configuration file reloaded\n";

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

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