#!/usr/bin/perl
#
# sendpage is the tool that will handle all the paging functions
#
# $Id: sendpage 319 2009-04-16 19:19:06Z keescook $
#
# Copyright (C) 2000-2004 Kees Cook
# kees@outflux.net, http://outflux.net/
#
# 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., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
# http://www.gnu.org/copyleft/gpl.html

=head1 NAME

sendpage - listen for pages via SNPP, and send pages via modem

=head1 SYNOPSIS

sendpage [OPTIONS] [recipient ...]

=head1 OPTIONS

=over 4

=item -bd

Start sendpage in "daemon mode" where it will start all the Paging
Central queues and wait for pages to be delivered.  When sendpage
runs as a daemon, it must be running as the 'sendpage' user as specified
in the sendpage.cf file.

=item -bp

Display all the pages waiting in the Paging Central queues.

=item -bv

Try to expand the "recipient" name, using the recipient aliases specified
in the configuration file.

=item -bs

Shutdown the running sendpage daemon and all its children.  If a Paging
Central is in the middle of delivering a page, it will finish up and
exit as soon as its current page is handled.

=item -br

This will send a SIGHUP to the master daemon.  When the master gets the
SIGHUP, it will re-read its configuration file, and restart all the
Paging Centrals.  It will wait for any busy Paging Centrals to finish
before continuing.

=item -bq

This displays the state of the running daemons: Running or Not running.
If a pid file is stale (the file exists, but the process doesn't), it
will mark that pid as "Stale".

=item -q[R pc]

This will send a SIGUSR1 signal to either the master daemon, or,
if the Paging Central is specified, just that Paging Central in particular.
When the master gets a SIGUSR1, it will send it to each of the running Paging
Centrals.  If the Paging Central is not busy, it will immediately start a
queue run.

=item -C FILE

Read the configuration file FILE instead of the default /etc/sendpage.cf

=item -h

Display a summary of all the available command line options.

=item -d

Turn on debugging (like "debug=true" in /etc/sendpage.cf)

=item -f USER

Show that the sent page is coming from USER.  Default is the current user.

=item -m MESSAGE

Send the given MESSAGE instead of reading text from stdin.

=item -n

Do not notify the 'from' user about the status of the page.

=back

=head1 DESCRIPTION

Sendpage can run as the delivery agent, or as a client to insert a page
into the paging queue.  For the various command-line arguments, 
the idea here was to use sendmail-style arguments where I can, not to
fully implement every option that sendmail has.  I just want the
learning curve of sendpage to be small for people already familiar
with sendmail.

=head1 FILES

=over 4

=item F</etc/sendpage.cf>

Default location for sendpage.cf, which holds all the configuration
information for sendpage, including Paging Central definitions,
recipients, and various other behaviors.

=item F</var/spool/sendpage>

Default directory for all the Paging Central queues and pid files.

=item F</var/lock>

Default directory to keep the UUCP-style device locks.

=back

=head1 AUTHOR

Kees Cook <kees@outflux.net>

=head1 BUGS

Oh, I bet this code is crawling with them.  :)  I've done my best to 
test this code, but I'm only one person.  If you find strange behavior,
please let me know.

=head1 COPYRIGHT

sendpage is free software; it can be used under the terms of the GNU
General Public License.

=head1 SEE ALSO

L<perl>, L<kill>, L<Device::SerialPort>, L<Mail::Send>,
L<Sendpage::KeesConf>, L<Sendpage::KeesLog>,
L<Sendpage::Modem>, L<Sendpage::PagingCentral>, L<Sendpage::PageQueue>,
L<Sendpage::Page>, L<Sendpage::Recipient>, L<Sendpage::Queue>

=cut

# we need at least this version.  FIXME: I forgot why, though.  :P
require 5.006;

# Global variables;
$VERSION=1.001_001; # our version!
$VERSION_human=sprintf("%d.%d.%d",
		       int(${VERSION}),
		       substr((${VERSION}=~/^\d\.(\d+)$/)[0],0,3),
		       substr((${VERSION}=~/^\d\.(\d+)$/)[0],3,3));

my $config;			# holds the configuration object
undef $log;			# holds logging object
my $db;				# holds the db object, if used

# Module-global variables
my %CHILDREN;			# who the childrens are
my $SHUTDOWN;			# when to shutdown
my $RELOAD;			# when we're reloading
my $DEBUG;			# for debugging
my $DEBUG_SELECT;		# for debugging select loop
my $DEBUG_SNPP;			# for debugging SNPP issues
my %opts;			# holds the command line args hash
undef @modems;			# List of available modems
undef @pcs;			# List of enabled PCs

# Global socket variables
undef $server;			# holds the SNPP server obj
undef $s;			# holds our Select set
undef %CNXNS;		  # List of open SNPP client pipes by filehandle
undef %PIPES;			# List of open SNPP client pipes by PID

# Global queue run variables
my $PC;				# holds name of PC for queue runners
my $sleeptime;			# holds sleeptime for next queue delay

# FIXME: load modules in a nice error-correcting fashion (borrow from mr house)
use POSIX;
use Getopt::Std;
use Sendpage::Modem;
use Sendpage::KeesConf;
use Sendpage::PagingCentral;
use Sendpage::PageQueue;
use Sendpage::Page;
use Sendpage::Recipient;
use Sendpage::KeesLog;
use Sendpage::SNPPServer;
use Sendpage::Db;
use Socket;
use IO::Select;
use IO::Pipe;
use Sys::Hostname::Long;

sub Usage
{
    die "Usage: $0 [OPTIONS] [alias ...]
version $VERSION_human

General Options
	-h		you're reading it.  :)
	-d		turn debug on
	-C FILE		use FILE as the sendpage.cf file
	-t		test all configured modems

Daemon Options
	-bd		run in daemon mode
	-bp		display the queues
	-bv		verify addresses
	-bs		shutdown server
	-br		have server reload configurations
	-bq		query state of daemons
	-q		force a queue run
	-qR PC		force a queue run only for the PC paging central

Page Queuing Options
	-f USER         force page to be from USER (default is current user)
	-m MESSAGE      message to send (reads from stdin by default)
	-n              no email status sent to 'from' for this page

";
}

# Start logging immediately
$log=Sendpage::KeesLog->new(Syslog => 0);

# get our options
if (!getopts('htvdqC:b:R:f:m:n',\%opts) || $opts{h}) {
    Usage();
}

# build default configuration, with any command line info
$config=initConfig(\%opts);

# load configuration
Initialize();

# Restart logging
$log->reconfig(Syslog => $config->get("syslog"),
	       Opts  => $config->get("syslog-opt"),
	       Facility => $config->get("syslog-facility"),
	       MinLevel => $config->get("syslog-minlevel"),
	      );

# Mode of operation selection
#
#	Modes:
#		- daemon (spawn queue runners, listen for pages)
#		- queue display
#		- address expansion
#		- force a queue run (optionally for only a certain PC)
#
if ($opts{t}) {
    ModemInit();
    exit(0);
}
if ($opts{b}) {
    QueryDaemons(1) if ($opts{b} eq "q");
    SendHUP() if ($opts{b} eq "r");
    ShutdownEverything() if ($opts{b} eq "s");
    BecomeDaemon() if ($opts{b} eq "d");
    DisplayQueue() if ($opts{b} eq "p");
    VerifyAddress(@ARGV) if ($opts{b} eq "v");

    warn "Unknown run mode: '$opts{b}'\n";
    Usage();
}
if ($opts{q}) {
    my $ret=SendSignal('USR1',$opts{R} ? $opts{R} : "");

    # FIXME: should run the queue by hand if no one else can
    die "Failed to notify queue manager: $!\nMaybe you should run me with -bd?\n" if (!defined($ret) || $ret != 0);
    exit;
}


die "Must be root to write pages directly to queue.  Please use 'snpp' instead.\n"
    if (!VerifyIdentities());
DropPrivs();

if (!@ARGV) {
    Usage();
}
else {
    my $msg=$opts{m};

    # it's time to write a file directly to the queue

    # who is it from?
    if (!defined($opts{f}) && !$opts{n}) {
	$opts{f}=scalar(getpwuid($<))."\@";
	$opts{f}.=hostname_long();
    }

    if (!defined($msg)) {
	my $line;
	while ($line=<STDIN>) {
	    $msg.=$line;
	}
    }

    # generate errors to the stderr
    ArrayDig(@ARGV);

    # turn on syslog for queue logging
    $log->on();

    # try to write the pages
    exit Sendpage::SNPPServer->write_queued_pages(undef,$opts{f},$msg,
						  $config,$log,$DEBUG,@ARGV);
}

sub QueryDaemons
{
    my($display)=@_;
    my(@check,$pc,$pid,$state,$running,$disabled);

    undef $running;
    @check=@pcs;
    unshift(@check,"");
    foreach $pc (@check) {
	if ($pc ne "" && $config->get("pc:$pc\@enabled")==0) {
	    $disabled=1;
	}
	else {
	    $disabled=0;
	}
	$pid=PidOf($pc,1);
	if ($pid==0) {
	    $state="Not running";
	}
	else {
	    undef $!;
	    kill 0, $pid;
	    if ($! == ESRCH) {
		$state="Stale: not running";
	    }
	    else {
		$state="Running";
		$running=1;
	    }
	}
	printf("%-6d %20s : %s%s\n",$pid,
	       ($pc eq "") ? 'Queue Manager' : $pc, $state,
	       $disabled ? " (disabled)" : "")
	    if ($display);
    }

    exit(0) if ($display);

    return $running;
}

sub SendHUP
{
    my $ret=SendSignal('HUP',"");
    die "Failed to notify queue manager: $!\n" if (!defined($ret) || $ret != 0);
    exit;
}

sub ShutdownEverything
{
    # there's no need for individual killing is there?
    #my $ret=SendSignal('QUIT',$opts{R} ? $opts{R} : "");
    my $ret=SendSignal('QUIT',"");
    warn "Failed to notify queue manager: $!\n" if (!defined($ret) || $ret != 0);
    exit;
}

sub Initialize
{
    &loadConfig();
    $DEBUG=$config->get("debug");
    $DEBUG_SELECT=$config->get("debug-select");
    $DEBUG_SNPP=$config->get("debug-snpp");
    @pcs=$config->instances("pc");
    undef %pcs; grep($pcs{$_}=1,@pcs);
    @modems=$config->instances("modem");
}

# plops down a pid file
sub RecordPidFile
{
    my($name,$pid)=@_;
    my($file);	

    $name=".$name" if ($name ne "");
    $file=$config->get("pidfileprefix").$name.".pid";
    open(FILE,">$file") || $log->do('err',"Cannot write to '$file': %s",$!);
    print FILE $pid,"\n";
    close(FILE);
}

# deletes a pid file by name
sub YankPidFile
{
    my($name)=@_;
    my($file);

    $name=".$name" if ($name ne "");
    $file=$config->get("pidfileprefix").$name.".pid";
    unlink($file) || $log->do('err',"Cannot unlink '$file': %s",$!);
}

# sends a signal to the specified PID
# returns non-0 on failure
sub SendSignal
{
    my($sig,$pid)=@_;

    if ($pid !~ /^\d+$/) {
	$pid=PidOf($pid);
    }

    return undef if ($pid == 0);

    $log->do('debug',"signalling '$sig' to pid '$pid'") if ($DEBUG);
    undef $!;
    kill $sig, $pid;
    return $!+0;
}

# tries to find the PID of a certain sendpage
# return PID, or 0 or failure
sub PidOf
{
    my($name,$quiet)=@_;
    my($file,$pid);

    if ($name ne "") {
	if (!defined($pcs{$name})) {
	    $log->do('warning',"No such PC '$name'");
	    return 0;
	}
	$name=".$name";
    }

    $pid=0;
    $file=$config->get("pidfileprefix").$name.".pid";
    if (-f $file) {
	my $line;

	open(FILE,"<$file") || $log->do('err',"Cannot read '$file': %s",$!);
	chomp($line=<FILE>);

	# this is used to untaint for a sendpage -q
	if ($line=~/^(\d+)$/) {
	    $pid=$1;
	}
	close(FILE);
    }
    else {
	my $warning=sprintf("No pid file found for sendpage%s!",
			    $name);
	$log->do('warning',"%s",$warning) if (!defined($quiet));
    }
    return ($pid+0);
}

sub NiceShutdown
{
    $SIG{QUIT}=$SIG{INT}=DEFAULT;

    $SHUTDOWN=1;
    if ($PC eq "") {
	my($pc,$cnxn);

	# Shutdown PCs
	foreach $pc (@pcs) {
	    if ($config->get("pc:$pc\@enabled")==0) {
		next;
	    }
	    $log->do('debug',"Signalling '$pc' ...") if ($DEBUG);
	    SendSignal('QUIT',$pc);
	}
	# Shut down SNPP clients
	foreach $cnxn (keys %PIPES) {
	    $log->do('debug',"Signalling SNPP client '$cnxn' ...")
		if ($DEBUG);
	    SendSignal('QUIT',$cnxn);
	}
    }
    else {
	$log->do('debug',"Shutting down nicely: '$PC'") if ($DEBUG);
    }
}

sub ImmediateShutdown
{
    $SIG{TERM}=DEFAULT;
    $log->do('debug',"Shutting down immediately") if ($DEBUG);
    exit(0);
}

sub QueueRun
{
    #$log->do('debug',"Pid $$ heard signal USR1") if ($DEBUG);

    # we need to signal all the PC's if we're master
    if ($PC eq "") {
	my $pc;
	foreach $pc (@pcs) {
	    if ($config->get("pc:$pc\@enabled")==0) {
		next;
	    }
	    $log->do('debug',"Signalling '$pc' ...") if ($DEBUG);
	    SendSignal('USR1',$pc);
	}
    }
    else {
	# perform queue run
	$log->do('debug',"QueueRun requested for '$PC' ...")
	    if ($DEBUG);

	# if we get a request for this DURING a queue run, we
	#  should immediately rescan our queue.  To do this,
	#  we set our next sleeptime to 0
	$sleeptime=0;
    }
}

sub DisplayQueue
{
    my($queue, $waiting, $page, $recip);

    foreach $pc (@pcs) {
	$queue=Sendpage::PageQueue->new($config,$config->get("queuedir")."/$pc");

	if (($waiting=$queue->ready())>-1) {
	    print "\nin the '$pc' queue: ".($waiting+1)."\n";
	    while (defined($page=$queue->getPage())) {
		print "\tqueue filename: ".$queue->file()."\n";
		print "\tattempts:       ".$page->attempts()."\n";
		print "\tfrom:           ".$page->option('from')."\n"
		    if ($page->option('from') ne "");
		print "\tqueued at:      ".scalar(localtime($page->option('queued')))."\n"
		    if ($page->option('queued') ne "");
		print "\tdeliverable:    ".($page->deliverable() ? "yes" : "pending")."\n";
		print "\tdeliverable at: ".scalar(localtime($page->option('when')))."\n"
		    if ($page->option('when') ne "");
		for ($page->reset(), $page->next();
		     defined($recip=$page->recip());
		     $page->next()) {
		    print "\tdest: '".$recip->name()."' (pin '".$recip->pin()."', email '".$recip->datum('email-cc')."')\n";
		}
		$queue->fileDone();
		print "\n";
	    }
	}
    }
    exit(0);
}

sub VerifyAddress
{
    my ($fail,@recips);
    ($fail,@recips)=ArrayDig(@ARGV);
    if ($fail != 0) {
	exit(1);
    }
    foreach $recip (@recips) {
	print "deliverable: ".$recip->name()." as ".$recip->pin().
	    " via ".$recip->pc()." (email is '".$recip->datum('email-cc')."')\n";
    }
    exit(0);
}

sub ModemInit
{
    my $modref;

    # test modems, keeping functioning ones in a list for the PCs to pick
    if ($DEBUG) {
	grep($log->do('debug',"found listing for modem: $_"),@modems);
	grep($log->do('debug',"found listing for pc: $_"),@pcs);
    }

    # should be limit which modems we're using?
    if (defined($modref=$config->get("modems",1))) {
	# we should limit the modem list
	@modems=@{ $modref };
	grep($log->do('debug',"using specified modem: $_"),@modems)
	    if ($DEBUG);
    }
    else {
	# pull from instance list
	@modems=$config->instances("modem");
    }
    # check the modems
    @modems=verifyModems(@modems);
    grep($log->do('debug',"found functioning modem: $_"),@modems)
	if ($DEBUG);

    $log->do('alert',"no functioning modems!") if (!defined($modems[0]));
}

sub BlockSignals
{
    # define the signals to block
    my $sigset = POSIX::SigSet->new(SIGINT,  SIGTERM, SIGQUIT, 
				    SIGUSR1, SIGHUP, SIGPIPE);

    # start blocking signals
    unless (defined sigprocmask(SIG_BLOCK, $sigset)) {
	$log->do('alert',"Could not block signals!");
    }

    return $sigset;
}

sub UnblockSignals
{
    # define the signals to block
    my $sigset = POSIX::SigSet->new(SIGINT,  SIGTERM, SIGQUIT, 
				    SIGUSR1, SIGHUP, SIGPIPE);

    # stop blocking signals
    unless (defined sigprocmask(SIG_UNBLOCK, $sigset)) {
	$log->do('alert',"Could not unblock signals!");
    }
}

# start one or all the children
sub SpawnChildren
{
    my($which)=@_;

    my($pid,$pc,@which,$sigset);

    if (defined($which)) {
	undef @which;
	push(@which,$which);
    }
    else {
	@which=@pcs;
	undef %CHILDREN;
	undef %STARTED;
	$log->do('info',"starting Queue Manager (sendpage v$VERSION_human)");
    }

    # there was a race condition here between the fork
    # 	 and the call to "SignalInit" where a child could
    #	 think it was still the manager, and receive yet
    #	 another signal, and act on it.   Now we block signals.

    # spawn PCs
    foreach $pc (@which) {
	if ($config->get("pc:$pc\@enabled")==0) {
	    next;
	}

	# start blocking signals
	$sigset=&BlockSignals();

	$pid=fork();
	if ($pid<0) {
	    # failure
	    $log->do('emerg',"Cripes!  Cannot spawn process: %s",$!);
	}
	elsif ($pid>0) {
	    # parent
	    $log->do('debug',"spawned child: $pid for PC '$pc'")
		if ($DEBUG);
	    $CHILDREN{$pid}=$pc;
	    $STARTED{$pc}=time;
	    RecordPidFile($pc,$pid);
	}
	else {
	    # child

	    # set up
	    &DropPrivs(1);
	    &SignalInit();

	    $log->do('debug',"I am child: $$ for PC '$pc'")
		if ($DEBUG);
	    &CheckFDs();

	    &StartQueue($pc,$sigset);
	    $log->do('crit',"PC '$pc' stopped processing!  ".
		     "Whoops, that can't happen!");
	    exit(1);
	}

	# stop blocking signals
	&UnblockSignals($sigset);
    }
}

# should they get serial port rights?
sub DropPrivs
{
    my ($serial)=@_;

    my $grouplist="nobody";

    if (!defined($setUID)) {
	$log->do('crit',"Effective User ID unknown -- aborting!");
	exit(1);
    }
    if (defined($serial)) {
	if (!defined($lockGID)) {
	    $log->do('crit',"Effective Group ID for locking unknown -- aborting!");
	    exit(1);
	}
	if (!defined($ttyGID)) {
	    $log->do('crit',"Effective Group ID for tty read/write unknown -- aborting!");
	    exit(1);
	}
	$grouplist="$lockGID $ttyGID";
    }

    $(=$)=$grouplist;
    if ($( != $grouplist) {
	$log->do('crit',"Could not setgid to '$grouplist': %s -- aborting!",$!);
    }

    $<=$>=$setUID;
    if ($< != $setUID) {
	$log->do('crit',"Could not setuid: %s -- aborting!",$!);
    }
}

# Debugging routine to check on the state of open file descriptors.
# I used this will tracking weird problems with the select loop.
sub CheckFDs
{
    return unless ($DEBUG);
    for (my $fd = 0; $fd < 10; $fd ++ ) {
	my $fh = IO::Handle->new_from_fd( $fd, "r" );
	if (defined($fh)) {
	    $log->do('debug',"$fd: open");
	}
	#		else {
	#			$log->do('debug',"$fd: $!");
	#		}
    }
}

sub Respawn
{
    my($undef,$forwarded) = @_;
    my($pid,$pc,$now);

    $!=0;
    if ($forwarded) {
	$pid=$forwarded;
    }
    else {
	$pid=wait;
    }

    # quit out if we're done (wait will sleep)
    return if ($SHUTDOWN==1);

    if ($pid==-1) {
	if ($!==ECHILD) {
	    $log->do('warning',"No children on SIGCHLD?!  Shutting down...");
	    $SHUTDOWN=1;
	    return;
	}
	else {
	    $log->do('warning',"Oops: waitpid spat totally unexpected error: %s",$!);
	    return;
	}
    }
    if (defined($CHILDREN{$pid})) {
	$log->do('debug', "pid $pid died: '".$CHILDREN{$pid}."'")
	    if ($DEBUG);

	$pc=$CHILDREN{$pid};
	$now=time;

	# restart within the same 10 seconds??
	if ($now<($STARTED{$pc}+10)) {
	    $log->do('alert',"Ugly nasty problem with $CHILDREN{$pid} queue manager!");
	    $log->do('alert',"The same PC has died twice rather quickly.  Shutting it down.");
	}
	else {
	    $log->do('err',"Whoa!  The '$CHILDREN{$pid}' PC died unexpectedly -- restarting it.");
	    SpawnChildren($CHILDREN{$pid});
	}
    }
    elsif (defined($PIPES{$pid})) {
	$log->do('debug',"SNPP connection (pid $pid) finished") if ($DEBUG_SNPP);
	# Only handle PID shutdown here
	#		$log->do('debug',"(select: removing pipe ".fileno($PIPES{$pid}).")")
	#			if ($DEBUG_SELECT);
	#		$s->remove($PIPES{$pid}); # don't select on this handle anymore
	#		PipeShutdownPipe($PIPES{$pid});
	PipeShutdownPid($pid);
    }
    else {
	# defunct children?  no!  bastards!  :)
	$log->do('warning',"Bastard child detected!  Unknown PID '$pid' was reaped.");
    }
}

sub SignalInit
{
    # set up signal handlers for children
    #$SIG{'CHLD'}='IGNORE';
    $SIG{'HUP'}='IGNORE';
    $SIG{'USR1'}='IGNORE';
    $SIG{'PIPE'} = 'IGNORE';
    $SIG{'INT'}=$SIG{'QUIT'}=\&NiceShutdown;
    $SIG{'TERM'}=\&ImmediateShutdown;
}

sub VerifyIdentities
{
    my($user,$group,$name);

    # check for our setuid user
    $user=$config->get('user');
    ($name,undef,$setUID)=getpwnam($user);
    if (!defined($name)) {
	$log->do('crit',"There is no such user named '$user'!  Aborting...");
	return undef;
    }

    # check for our locking group
    $group=$config->get('group-lock');
    ($name,undef,$lockGID)=getgrnam($group);
    if (!defined($name)) {
	$log->do('crit',"There is no such group named '$group'!  Aborting...");
	return undef;
    }

    # check for our tty r/w group
    $group=$config->get('group-tty');
    ($name,undef,$ttyGID)=getgrnam($group);
    if (!defined($name)) {
	$log->do('crit',"There is no such group named '$group'!  Aborting...");
	return undef;
    }

    # are we root?
    if (0 != $<) {
	return undef;
    }

    return 1;
}

sub BecomeDaemon
{
    # check to see if we're already running
    if (QueryDaemons()) {
	warn "Already running:\n";
	QueryDaemons(1);
    }

    if (!VerifyIdentities()) {
	$log->do('crit',"Must be running as 'root' to daemonize!  Aborting...");
	exit(1);
    }

    # daemon mode starts here
    ModemInit();

    SignalInit();

    $PC="";

    # close file handles.  (unless debugging)
    close(STDIN);
    close(STDOUT);
    close(STDERR) unless ($DEBUG || $DEBUG_SELECT || $DEBUG_SNPP);

    # Become a daemon
    my $pid = fork;
    exit if $pid;
    die "Couldn't fork: $!" unless defined($pid);
    POSIX::setsid() or die "Can't start a new session: $!";

    # reconfig, and reopen syslog connection
    $log->reconfig(Syslog => $config->get("syslog"),
		   Opts  => $config->get("syslog-opt"),
		   Facility => $config->get("syslog-facility"),
		   MinLevel => $config->get("syslog-minlevel"),
		  );
    $log->on();

    $0="sendpage: accepting connections";
    $SHUTDOWN=0;
    RecordPidFile($PC,$$);

    # build new loop selector
    $s=IO::Select->new();

    # start the queue runners
    SpawnChildren();

    # listen for USR1 to send USR1s
    $SIG{'USR1'} = \&QueueRun;
    # listen for reload info
    $SIG{'HUP'}=\&Reload;
    # listen for children death
    #$SIG{'CHLD'}=\&Respawn;

    # start the SNPP stuff now
    StartSNPP();

    # Enter the main processing loop
    MainLoop();

    die "parent died: this should never have happened: $!\n";
}

sub initSNPP
{
    my $host=$config->get("snpp-addr");
    my $port=$config->get("snpp-port");

    # need to use "create" so that the "accept"s don't call the constructor
    $server=Sendpage::SNPPServer->create(Addr => $host, Port => $port);

    if (defined($server)) {
	$log->do('debug',"SNPP listener running on %s:%d",
		 $server->sockhost,
		 $server->sockport) if ($DEBUG_SNPP);
    }
    else {
	$log->do('crit',"SNPP listener for '%s:%s' failed: %s",
		 $host,$port,$!);
    }

    # Expand our snpp ACLs so we don't have to during each connection
    @ACLs=();
    my $item;
    foreach $item (@{$config->get("snpp-acl")}) {
	my ($netmask,$way) = split(/:/,$item,2);
	$way=uc($way);
	my ($net_str,$mask_str) = split(/\//,$netmask,2);
	$log->do('debug',"ACL loaded: '$net_str'/'$mask_str' is '$way'")
	    if ($DEBUG_SNPP);

	my $net  = inet_aton($net_str);
	my $mask = inet_aton($mask_str);

	push(@ACLs,[ $net, $mask, $net_str, ($way eq "ALLOW") ]);
    }
}

sub StopSNPP
{
    $log->do('debug',"SNPP listeners shutting down")
	if ($DEBUG_SNPP);
    $log->do('debug',"(select: dropping ".fileno($server).")")
	if ($DEBUG_SELECT);
    $s->remove($server);
    $server->close();
    undef $server;
}

sub StartSNPP
{
    $log->do('info',"starting SNPP listeners");
    initSNPP();
    if (defined($server)) {
	$log->do('debug',"(select: adding server ".fileno($server).")")
	    if ($DEBUG_SELECT);
	$s->add($server);
    }
}

sub RestartSNPP
{
    StopSNPP();
    StartSNPP();
}

# FIXME: have all the ACLs pre-expanded for us...
sub IPAllowed
{
    my $sock=shift;

    # Verify that this connection is allowed

    my $peer=$sock->peerhost();

    my $other_end = $sock->peername();
    if (!defined($other_end)) {
	$log->do('alert',"SNPP client '$peer' failed getpeername!");
	return undef;
    }
    my ($port, $iaddr) = unpack_sockaddr_in($other_end);
    my $other_ip_address = inet_ntoa($iaddr);

    # Compare this IP address to our ACL list
    my $item;
    my $allowed=0;
    my $found=0;
    foreach $item (@ACLs) {
	my ($net,$mask,$net_str,$allow) = @{$item};

	# Drop the peer IP through the mask
	my $net_check = ($iaddr & $mask);

	my $check_str=inet_ntoa($net_check);

	if ($DEBUG_SNPP) {
	    my $mask_str=inet_ntoa($mask);
	    $log->do('debug',"ip: '$peer' mask: '$mask_str' ".
		     "masked: '$check_str' net: '$net_str'");
	}

	# if result is our network, we have a hit
	if ($check_str eq $net_str) {
	    $found=1;
	    $log->do('debug', "Matched ACL") if ($DEBUG_SNPP);
	    if ($allow==1) {
		$allowed=1;
	    }
	    # if not allow, then reject
	    last;
	}
    }
    if ($DEBUG_SNPP && $found == 0) {
	$log->do('debug',"No ACL matched '$peer'");
    }
    if ($allowed != 1) {
	$sock->command("421 Connection denied");
	$log->do('info',"SNPP client '$peer' rejected");
	return undef;
    }
    return 1;
}

# This main loop cycles through all the pending socket connections:
#  - SNPP listeners to spawn SNPP clients
#  - SNPP client pipes to start PC queue runs
sub MainLoop
{
    my($fh,$read,$exc,$pipe,$sigset,$match,$pid);

    while ($SHUTDOWN!=1) {
	if (!defined($server)) {
	    $log->do('crit',"Cannot start any SNPP listeners -- ".
		     "aborting!");
	    NiceShutdown();
	    YankPidFile("");
	    exit(1);
	}

	# reset my containers
	$read=$exc=undef;

	if ($DEBUG_SELECT) {
	    grep($log->do('debug',"select set: ".fileno($_)),
		 $s->handles());
	}

	# handle children dying
	while (($pid = waitpid(-1,&WNOHANG))>0) {
	    &Respawn('',$pid);
	}

	if ($s->count()<1) {
	    $log->do('warning',"Whoa!  Nothing left in the select array -- restarting SNPP!");
	    RestartSNPP();
	}

	$log->do('debug',"(select starting)") if ($DEBUG_SELECT);
	$match=0;
	$!=0;
	my @events = IO::Select->select($s,undef,$s,1.0);
	if (scalar(@events)==0 && $! != 0) {
	    if ($! == &EINTR()) {
		$match=1;
		$log->do('debug',"select loop: %s -- continuing",$!)
		    if ($DEBUG_SELECT);
	    }
	    else {
		$log->do('warning',"select loop failed: %s",$!);
	    }
	}
	$log->do('debug',"(select finished)") if ($DEBUG_SELECT);

	($read,undef,$exc)=@events;
	foreach $fh (@$read) {
	    # Handle a new SNPP connection
	    if ($fh == $server) {
		my $pid;
		my $sock = $fh->accept;

		$match=1;

		if (!defined($sock)) {
		    $log->do('err',"SNPP accept: %s",$!);
		    next;
		}

		$log->do('debug',"got connection from %s",
			 $sock->peerhost) if ($DEBUG_SNPP);

		if (!IPAllowed($sock)) {
		    close($sock);
		    next;
		}

		$pipe = new IO::Pipe;

		$sigset=&BlockSignals();

		if (($pid = fork())>0) {
		    # Parent

		    # close other side of pipe
		    $pipe->reader();
		    # close our forked socket
		    close($sock);

		    $log->do('debug',"(select: adding pipe ".fileno($pipe).")")
			if ($DEBUG_SELECT);
		    $s->add($pipe);
		    PipeRemember($pipe,$pid);
		}
		elsif ($pid==0) {
		    # Child

		    # close other side of pipe
		    $pipe->writer();
		    $pipe->autoflush(1);

		    # close master socket
		    close $fh;
		    # FIXME: close ALL snpp listeners

		    # set up identity
		    $PC="SNPP client";
		    $0="sendpage: SNPP client: ".
			$sock->peerhost;
		    &DropPrivs();
		    &SignalInit();

		    $log->do('debug',"I am child: $$ for SNPP")
			if ($DEBUG);
		    &CheckFDs();

		    # we will unblock signals in
		    #  the snpp handler
		    $sock->HandleSNPP(
				      "SNPP Sendpage $VERSION_human",
				      $pipe, $config, $log,
				      $DEBUG_SNPP, $sigset);
		    $log->do('debug',
			     "leaving SNPP client cnxn")
			if ($DEBUG_SNPP);

		    exit(0);

		}
		else {
		    $log->do('err',"SNPP fork: %s",$!);
		    # error on fork
		    close($sock);
		}

		&UnblockSignals($sigset);
	    }
	    # Handle SNPP client notifications
	    elsif (defined($pid=$CNXNS{$fh})) {
		$match=1;

		# is this pipe shutdown?
		if ($fh->eof()) {
		    PipeShutdownPipe($fh);
		    $log->do('debug',"(select: removing pipe ".fileno($fh).")")
			if ($DEBUG_SELECT);
		    $s->remove($fh);
		    close($fh);
		}
		# something is readable from a pipe
		else {
		    chomp(my $pc=<$fh>);

		    SendSignal('USR1',$pc) if ($pc ne "");
		}
	    }
	    else {
		$match=1;

		# toss any straggling pipes
		$log->do('debug',"(select: removing stale selectable file handle: ".fileno($fh).")")
		    if ($DEBUG_SELECT);
		$s->remove($fh);
		close($fh);
	    }
	}
	# Deal with exceptions
	foreach $fh (@$exc) {
	    if ($fh == $server) {
		$match=1;

		$log->do('err',"Whoa!  Server socket took a hit!  -- reopening it");
		RestartSNPP();
	    }
	    else {
		$match=1;

		$log->do('warning',"SNPP connection took a hit!");
		$log->do('debug',"(select: removing server ".fileno($fh).")")
		    if ($DEBUG_SELECT);
		$s->remove($fh);
		close($fh);
	    }
	}

	#   if ($match==0) {
	#       $log->do('warning',"Select produced an unmatched file descriptor?!");
	#   }
    }
    $log->do('info',"stopping Queue Manager and SNPP listeners (sendpage v$VERSION_human)");
    StopSNPP();
    YankPidFile("");
    exit(0);
}

sub PipeShutdownPipe
{
    my $pipe = shift;

    delete $CNXNS{$pipe};
}

sub PipeShutdownPid
{
    my $pid = shift;

    delete $PIPES{$pid};
}

sub PipeRemember
{
    my($pipe,$pid)=@_;

    $PIPES{$pid}=$pipe;
    $CNXNS{$pipe}=$pid;
}


sub StartQueue
{
    my($name,$sigset)=@_;

    # Queue-runner variables
    my($rundelay,$pc,$waiting);
    $rundelay=$config->get("pc:${name}\@rundelay");
    #warn "run delay: $rundelay\n";

    $PC=$name;
    $pc=Sendpage::PagingCentral->new($config,$PC,\@modems);

    # rename myself
    $0="sendpage: $PC queue";

    # set up handler
    $SIG{'USR1'} = \&QueueRun;

    $log->do('debug', "starting queue runs for '$PC'") if ($DEBUG);

    my $dir=$config->get("queuedir")."/$PC";
    if (! -d $dir) {
	if (!mkdir($dir,0700)) {
	    $log->do('alert',"Cannot mkdir '$dir': $!");
	    exit(1);
	}
    }

    $queue=Sendpage::PageQueue->new($config, $dir);

    if (!defined($queue)) {
	$log->do('alert',"Failed to open queue for '$PC'  --  exiting");
	exit(1);
    }

    # stop blocking signals
    &UnblockSignals($sigset);

    while ($SHUTDOWN!=1) {
	# reset our sleep time
	$sleeptime=$rundelay;

	# search queue, gathering pages
	$waiting=$queue->ready()+1;
	if ($waiting>0) {
	    while (defined($page=$queue->getPage())) {
		if ($page->deliverable()) {
		    $pc->deliver($page);

		    if ($page->has_recips()) {
			$log->do('debug',"$PC: rewriting page to queue") if ($DEBUG);
			# something requires rerun
			$queue->writePage($page);
			$queue->fileDone();
		    }
		    else {
			$log->do('debug',"$PC: tossing queue file") if ($DEBUG);
			$queue->fileToss();
		    }
		}
		else {
		    $queue->fileDone();
		}
	    }
	}

	# don't hang up if need to rescan our queue
	if ($sleeptime != 0) {
	    $pc->disconnect();


	    # strange eval needed to wake up on USR1 signals
	    eval {
		local $SIG{USR1} = sub { die "usr1\n" };

		# pause for the next queue run
		sleep($sleeptime);
	    };
	    if ($@) {
		QueueRun();
	    }
	    else {
		# nothing: we're done sleeping
	    }
	}

	# check and see if we should shutdown (parent is init)
	if (!$SHUTDOWN && getppid==1) {
	    $log->do('err',"Parent process died -- aborting");
	    $SHUTDOWN=1;
	}
    }
    $log->do('debug', "Queue runner for '$PC' shutting down") if ($DEBUG);
    # remove our pid file
    YankPidFile($PC);
    exit(0);
}

sub DoNothing
{
    # no code here, but just have HAVING a signal handler, I'll wake up
    # during a SIGCHLD for my waitpid
}

sub Reload {
    my($pid);

    $log->do('info', "initiating reload ...");

    if ($PC eq "") {
	my %OLD=%CHILDREN;
	undef %CHILDREN;
	undef %STARTED;

	StopSNPP();

	$RELOAD=1;

	my $finished=0;

	# shutdown the PCs
	foreach $pc (@pcs) {
	    if ($config->get("pc:$pc\@enabled")==0) {
		next;
	    }
	    $log->do('debug', "Stopping '$pc' ...") if ($DEBUG);
	    SendSignal('QUIT',$pc);
	    $finished++;
	}
	# shut down all the SNPP clients too
	foreach $pc (keys %PIPES) {
	    $log->do('debug',"Stopping SNPP client '$pc' ...")
		if ($DEBUG);
	    SendSignal('QUIT',$pc);
	    $finished++;
	}

	# Note:
	# can't do "ModemInit" until everyone is dead because we
	# need to re-init (to validate) all the modems.

	my @keys=keys %OLD;

	undef $!;
	$log->do('debug', "Waiting for $finished PCs/SNPPs to die off...") if ($DEBUG);
	while ($finished!=0) {
	    $pid=wait;
	    if ($pid==-1) {
		if ($!==ECHILD) {
		    $log->do('warning',"Ran out of children too early?!  Continuing anyway...");
		    $finished=0;
		}
		else {
		    $log->do('warning',"Oops: waitpid spat totally unexpected error: %s",$!);
		}
	    }	
	    elsif ($pid==0) {
		$log->do('warning',"Got 0 pid somehow");
	    }
	    elsif (defined($OLD{$pid})) {
		$log->do('debug', "Letting old PC '$OLD{$pid}' rest in peace") if ($DEBUG);
		$finished--;
	    }
	    elsif (defined($PIPES{$pid})) {
		PipeShutdownPid($pid);
		$log->do('debug',"SNPP connection (pid $pid) finished") if ($DEBUG || $DEBUG_SNPP);
		$finished--;
	    }
	    else {
		$log->do('warning',"Strange, I got an unknown child PID: '$pid'");
	    }
	}

	$log->do('debug', "Reinitializing...") if ($DEBUG);

	$RELOAD=0;

	# reload our configurations
	Initialize();
	ModemInit();

	# Restart logging
	$log->reconfig(Syslog => $config->get("syslog"),
		       Opts  => $config->get("syslog-opt"),
		       Facility => $config->get("syslog-facility"),
		       MinLevel => $config->get("syslog-minlevel"),
		      );
	$log->on();

	# Start up all our children
	SpawnChildren();
	StartSNPP();
    }
    else {
	$log->do('warning',"Weird: PC '$PC' caught a Reload signal somehow.");
    }
}

sub verifyModems
{
    my (@totest) = @_;

    # find all our valid modems, keeping those that are either in use (e.g.
    # we have been HUPd) or respond to initialization
    my $m;
    my $result;
    my $modem;
    my @okay;

    foreach $modem (@totest) {
	$m=Sendpage::Modem->new(Name => $modem,
				Dev => $config->get("modem:${modem}\@dev"),
				Lockprefix => $config->get("lockprefix"),
				Debug => $config->get("modem:${modem}\@debug"),
				Log => $log,
				Baud => $config->get("modem:${modem}\@baud"),
				Parity => $config->get("modem:${modem}\@parity"),
				StrictParity => $config->get("modem:${modem}\@strict-parity"),
				Data => $config->get("modem:${modem}\@data"),
				Stop => $config->get("modem:${modem}\@stop"),
				Flow => $config->get("modem:${modem}\@flow"),
				Init => $config->get("modem:${modem}\@init"),
				InitOK => $config->get("modem:${modem}\@initok"),
				InitWait => $config->get("modem:${modem}\@initwait"),
				InitRetry => $config->get("modem:${modem}\@initretries"),
				Error => $config->get("modem:${modem}\@error"),
				Dial => $config->get("modem:${modem}\@dial"),
				DialOK => $config->get("modem:${modem}\@dialok"),
				DialWait => $config->get("modem:${modem}\@dialwait"),
				DialRetry => $config->get("modem:${modem}\@dialretries"),
				NoCarrier => $config->get("modem:${modem}\@no-carrier"),
				DTRToggleTime => $config->get("modem:${modem}\@dtrtime"),
				CarrierDetect => $config->get("modem:${modem}\@carrier-detect",1),
				AreaCode => $config->get("modem:${modem}\@areacode",1),
				LongDist => $config->get("modem:${modem}\@longdist"),
				DialOut =>  $config->get("modem:${modem}\@dialout")
			       );
	if (!defined($m)) {
	    $log->do('warning',"Cannot find modem '$modem'");
	    next;
	}
	if (!defined($result=$m->init())) {
	    $log->do('alert',"Cannot initialize modem '$modem'");
	    next;
	}
	undef $m;
	push(@okay,$modem);
    }

    return @okay;
}

sub RecipDig
{
    my($recip,$seen)=@_;
    my($dests,$one,%hash,$result);

    if (!defined($seen)) {
	my %holder;
	$holder{$recip->name()}=1;
	$seen=\%holder;
    }
    else {
	$seen->{$recip->name()}++;
    }

    if ($seen->{$recip->name()}>1) {
	$log->do('alert',"Loop found in alias expansion!  Culprit recip: '%s'",
		 $recip->name());
	exit(1);
    }

    # no alias, just return this one (leaf node)
    return ($recip) if (!$recip->alias());

    $log->do('debug',"from: '%s'",$recip->name())
	if ($config->get("alias-debug"));

    # get expanded list
    $dests=$recip->dests();

    # dump list
    grep($log->do('debug',"starting with: '%s'",$_),@{$dests})
	if ($config->get("alias-debug"));

    # expand each one
    foreach $one (@{ $dests }) {
	$log->do('debug',"expanding: '%s'",$one) if ($config->get("alias-debug"));
	my %copy=%{$seen};
	my $r=Sendpage::Recipient->new($config,$db,$one,$recip->data());
	if (!defined($r)) {
	    $log->do('err',"undeliverable: '%s'",$one);
	}
	else {
	    my @results=RecipDig($r,\%copy);

	    # add them to our hash
	    foreach $result (@results) {
		$log->do('debug',"got: '%s'",$result->name())
		    if ($config->get("alias-debug"));
		$hash{$result->name()}=$result;
	    }
	}
    }

    undef @results;
    foreach $one (keys %hash) {
	$log->do('debug',"passing back: '%s'",$hash{$one}->name())
	    if ($config->get("alias-debug"));
	push(@results,$hash{$one});
    }

    return @results;
}

sub ArrayDig
{
    my(@array)=@_;
    my ($one,$result,%hash,@results,$fail);

    # did a look-up fail?
    $fail=0;
    # dump list
    grep($log->do('debug',"starting with: '%s'",$_),@array)
	if ($config->get("alias-debug"));

    # expand each one
    foreach $one (@array) {
	$log->do('debug',"expanding: '%s'",$one)
	    if ($config->get("alias-debug"));
	my $recip=Sendpage::Recipient->new($config,$db,$one);
	if (!defined($recip)) {
	    $log->do('err',"undeliverable: '%s'",$one);
	    $fail=1;
	}
	else {
	    my @results=RecipDig($recip);

	    # add them to our hash
	    foreach $result (@results) {
		$log->do('debug',"got: '%s'",$result->name())
		    if ($config->get("alias-debug"));
		$hash{$result->name()}=$result;
	    }
	}
    }

    undef @results;
    foreach $one (keys %hash) {
	$log->do('debug',"passing back: '%s'",$hash{$one}->name())
	    if ($config->get("alias-debug"));
	push(@results,$hash{$one});
    }

    return ($fail,@results);
}

sub initConfig
{
    my(%opts) = %{ $_[0] };

    # set up default values  (this is ignored by KeesConf...)
    my %cfg=(
	     PEDANTIC=> 1,
	     CASE	=> 1,
	     CREATE  => 1,
	     GLOBAL  => {
			 DEFAULT   => "<unset>",
			 ARGCOUNT  => ARGCOUNT_ONE,
			},
	    );
    my $config = Sendpage::KeesConf->new(\%cfg);

    # global variables
    $config->define("dsn", { DEFAULT => "" });
    $config->define("dbuser", { DEFAULT => "" });
    $config->define("dbpass", { DEFAULT => "" });
    $config->define("dbtable", { DEFAULT => "" });
    $config->define("cfgfile",   { DEFAULT => "/etc/sendpage.cf" });
    $config->define("pidfileprefix",{ DEFAULT => "/var/spool/sendpage/sendpage" });
    $config->define("lockprefix",{ DEFAULT => "/var/lock/LCK.." });
    $config->define("queuedir", { DEFAULT => "/var/spool/sendpage" });
    $config->define("mail-agent", { DEFAULT => "sendmail" });
    $config->define("user", { DEFAULT => "sendpage" });
    $config->define("group-lock", { DEFAULT => "uucp" });
    $config->define("group-tty", { DEFAULT => "dialout" });
    $config->define("page-daemon", { DEFAULT => "sendpage" });
    $config->define("cc-on-error", { ARGCOUNT => 0, DEFAULT => 1 });
    $config->define("modems",   { ARGCOUNT => 2 });
    $config->define("alias-debug",    { ARGCOUNT => 0, DEFAULT => 0 });
    $config->define("debug",    { ARGCOUNT => 0,
				  DEFAULT => $opts{d} ? 1 : 0 });
    $config->define("debug-select",    { ARGCOUNT => 0,
					 DEFAULT => $opts{d} ? 1 : 0 });
    $config->define("debug-snpp",    { ARGCOUNT => 0,
				       DEFAULT => $opts{d} ? 1 : 0 });
    # should the sender be notified of failures?
    $config->define("fail-notify", { ARGCOUNT => 0, DEFAULT => 1 });
    # sender should be notified how on every X temp fails? (0=never)
    $config->define("tempfail-notify-after", { DEFAULT => 5 });
    # how many temp fails does it take to produce a perm failure?
    $config->define("max-tempfail", { DEFAULT => 20 });
    #   max age in seconds of a page in the queue (0 = unlimited)
    $config->define("max-age",		{ DEFAULT => 0 });
    # default email CC domain
    $config->define("fallback-email-domain", { DEFAULT => undef });
    # command to run after each successful or failed page
    $config->define("completion-cmd", { UNSET => 1 });
    # syslog toggle: strerr is used if not syslog
    $config->define("syslog", { DEFAULT => 1, ARGCOUNT => 0 });
    # syslog logopt words (any of "pid", "ndelay", "cons", "nowait")
    $config->define("syslog-opt", { DEFAULT => "pid" });
    # syslog facility to log with (one of "auth", "authpriv", "cron", "daemon",
    #	"kern", "local0" through "local7", "lpr", "mail", "news", "syslog",
    #	"user", or "uucp"
    $config->define("syslog-facility", { DEFAULT => "daemon" });
    # syslog reports internally range from debug through crit.  Most people
    # figure out how to configure sendpage to report debug, but don't change
    # their syslog configs.  So, we'll have a "minlevel".  Anything below
    # "minlevel" will be elevated to "minlevel" before being sent to syslog.
    $config->define("syslog-minlevel", { DEFAULT => "info" });
    # SNPP settings
    $config->define("snpp-port", { DEFAULT => "444" });
    $config->define("snpp-addr", { DEFAULT => "localhost" });
    $config->define("snpp-acl", { ARGCOUNT => 2, DEFAULT => [ "127.0.0.1/255.255.255.255:ALLOW" ] });

    # aliases defaults
    #$config->define("recip:", { ARGCOUNT => 2 });
    # where to send email-cc's of pages.  none if blank, defaults to 
    #	ALIAS @ fallback-email-domain if unset
    $config->define("recip:email-cc", { UNSET => 1 });
    # page is PIN@PC, alias is just the recip name again
    $config->define("recip:dest", { ARGCOUNT => 2 });

    # modem defaults
    $config->define("modem:debug",  { ARGCOUNT => 0, 
				      DEFAULT => $opts{d} ? 1 : 0 });
    $config->define("modem:baud",   { DEFAULT => 9600 });
    $config->define("modem:data",   { DEFAULT => 7 });
    $config->define("modem:parity", { DEFAULT => "even" });
    $config->define("modem:stop",   { DEFAULT => 1 });
    $config->define("modem:flow",   { DEFAULT => "rts" });
    $config->define("modem:strict-parity", { ARGCOUNT => 0, DEFAULT => 0 });
    $config->define("modem:dev",    { DEFAULT => "/dev/null" });
    $config->define("modem:carrier-detect", { DEFAULT => "on" });
    # time to force DTR down during reset (0 = don't do it at all)
    $config->define("modem:dtrtime", { DEFAULT => 0.5 });
    $config->define("modem:init",   { DEFAULT => "ATZ" });
    $config->define("modem:initok", { DEFAULT => "OK" });
    $config->define("modem:initwait",{ DEFAULT => 4 });
    $config->define("modem:initretries",{ DEFAULT => 2 });
    $config->define("modem:dial",   { DEFAULT => "ATDT" });
    $config->define("modem:dialok", { DEFAULT => "CONNECT.*\r" });
    $config->define("modem:dialwait",{ DEFAULT => 60 });
    $config->define("modem:error", { DEFAULT => "ERROR" });
    $config->define("modem:no-carrier",
		    {
		     DEFAULT => "ERROR|NO CARRIER|BUSY|NO DIAL|VOICE" });
    $config->define("modem:areacode", { UNSET => 1 });
    $config->define("modem:longdist", { DEFAULT => 1 });
    $config->define("modem:dialout", { DEFAULT => "" });

    # FIXME:
    # currently not implemented -- perhaps never, better to stall and try again
    $config->define("modem:dialretries",{ DEFAULT => 3 });

    # paging central defaults
    #     Paging centrals can override baud, data, parity, stop, flow, dialwait,
    #       dialretries
    #  modem connect info
    $config->define("pc:enabled",{ ARGCOUNT => 0, DEFAULT => 1 });
    $config->define("pc:debug",  { ARGCOUNT => 0, 
				   DEFAULT => $opts{d} ? 1 : 0 });
    $config->define("pc:page-daemon", { UNSET => 1 });
    $config->define("pc:cc-on-error",  { ARGCOUNT => 0, UNSET => 1 });
    $config->define("pc:cc-simple",  { ARGCOUNT => 0, DEFAULT => 0 });
    $config->define("pc:fail-notify", { ARGCOUNT => 0, UNSET => 1 });
    $config->define("pc:tempfail-notify-after", { UNSET => 1 });
    $config->define("pc:max-tempfail", { UNSET => 1 });
    $config->define("pc:max-age",		{ UNSET => 1 });
    # command to run after each successful or failed page
    $config->define("pc:completion-cmd", { UNSET => 1 });
    $config->define("pc:modems", { ARGCOUNT => 2 });
    $config->define("pc:baud",   { DEFAULT => 9600 });
    $config->define("pc:data",   { DEFAULT => 7 });
    $config->define("pc:parity", { DEFAULT => "even" });
    $config->define("pc:stop",   { DEFAULT => 1 });
    $config->define("pc:flow",   { DEFAULT => "rts" });
    $config->define("pc:strict-parity", { ARGCOUNT => 0, DEFAULT => 0 });
    $config->define("pc:phonenum",{DEFAULT => "" });
    $config->define("pc:areacode",{ UNSET => 1 });
    # how many chars per page before auto-splitting?
    $config->define("pc:maxchars",{DEFAULT => 1024 });
    # how many page splits allowed per page?
    $config->define("pc:maxsplits",{DEFAULT => 6 });
    # PC uses it's own dialwait for delaying
    $config->define("pc:dialwait",{ UNSET => 1 });
    $config->define("pc:rundelay",{DEFAULT => 30 });
    $config->define("pc:dialretries",{ DEFAULT => 3 });
    # allow for selecting delivery protocol (TAP (PG1, PG3), UCP, SMS, SNPP)
    $config->define("pc:proto",{DEFAULT => "PG1" });
    # allow for forced multiple fields in BlockTrans
    $config->define("pc:fields",{DEFAULT => 2 });
    $config->define("pc:password",{DEFAULT => "000000" });
    #  proto establishment info
    $config->define("pc:answerwait", { DEFAULT => 2 });
    $config->define("pc:answerretries", { DEFAULT => 3 });
    #  protocol settings
    #   MUST have the leading "<CR>" for each answer?
    $config->define("pc:stricttap",		{ DEFAULT => 0, ARGCOUNT => 0 });
    #   chars less than 0x20 are allowed in a field
    $config->define("pc:ctrl",		{ DEFAULT => 0, ARGCOUNT => 0 });
    #   chars CAN be escaped (if false, "LF" is allowed, it seems?)
    $config->define("pc:esc",  		{ DEFAULT => 0, ARGCOUNT => 0 });
    #   is LF allowed (some PCs allow it, but no other ctrl chars)
    $config->define("pc:lfok",		{ DEFAULT => 0, ARGCOUNT => 0 });
    #   fields cannot be split across blocks? (FIXME: unimplemented)
    $config->define("pc:fieldsplits",	{ DEFAULT => 1, ARGCOUNT => 0 });
    #  paging central limits
    #   max blocks per connection (0 = unlimited)
    $config->define("pc:maxblocks",		{ DEFAULT => 0 });
    #   max pages per connection (0 = unlimited)
    $config->define("pc:maxpages",		{ DEFAULT => 0 });
    #   max chars per block: 250 is protocol standard: 256 - 3 ctrl - 3 chksum
    $config->define("pc:chars-per-block",	{ DEFAULT => 250 });

    return $config;
}

sub loadConfig {
    my($cfgfile);

    $cfgfile=$config->get("cfgfile");
    $cfgfile=$opts{C} if (defined($opts{C}));

    # toss our config
    $config->dump();


    # yes, this seems silly, but we allow cmdline options to change
    # various defaults, including this one
    $config->file($cfgfile);

    if ($config->get("dsn")) {
	my ($dsn, $dbuser, $dbpass, $dbtable);
	$dsn = $config->get("dsn");
	$dbuser = $config->get("dbuser");
	$dbpass = $config->get("dbpass");
	$dbtable = $config->get("dbtable");
	if ($db) {
	    $db->setdb($dsn,$dbuser,$dbpass,$dbtable);
	} else {
	    $db = Sendpage::Db->new($dsn,$dbuser,$dbpass,$dbtable);
	}
    }
}


#
# file locking example from the Perl Cookbook
#
# use Fcntl qw(:DEFAULT :flock);
# 
# sysopen(FH, "numfile", O_RDWR|O_CREAT)
#                                     or die "can't open numfile: $!";
# flock(FH, LOCK_EX)                  or die "can't write-lock numfile: $!";
# # Now we have acquired the lock, it's safe for I/O
# $num = <FH> || 0;                   # DO NOT USE "or" THERE!!
# seek(FH, 0, 0)                      or die "can't rewind numfile : $!";
# truncate(FH, 0)                     or die "can't truncate numfile: $!";
# print FH $num+1, "\n"               or die "can't write numfile: $!";
# close(FH)                           or die "can't close numfile: $!";
#
#
