#!/opt/SUNWmail/gtw/perl/bin/perl -ww

# $Id: dirsync_agent,v 1.46.14.1 1998/02/06 18:05:47 kevin Exp $

my $legal = <<'EOF';
This is UNPUBLISHED PROPRIETARY SOURCE CODE of Wingra Technologies
Incorporated; the contents of this file may not be disclosed to third parties,
copied or duplicated in any form, in whole or in part, without the prior
written permission of Wingra Technologies Incorporated.

Permission is hereby granted solely to the licensee for use of this source code
in its unaltered state. This source code may not be modified by licensee
except under specific direction of Wingra Technologies Incorporated. This
source code may not be given under any circumstances to non-licensees in any 
form, including source or binary. Unauthorized modification of this source 
constitutes breach of contract, which voids any potential pending support 
responsibilities by Wingra Technologies Incorporated. Divulging the exact or 
paraphrased contents of this source code to unlicensed parties either directly 
or indirectly constitutes violation of federal and international copyright and 
trade secret laws, and will be duly prosecuted to the fullest extent permitted 
under law.

This software is provided by Wingra Technologies Incorporated ``as is'' and any
express or implied warranties, including, but not limited to, the implied
warranties of merchantability and fitness for a particular purpose are
disclaimed.  In no event shall the regents or contributors be liable for any
direct, indirect, incidental, special, exemplary, or consequential damages
(including, but not limited to, procurement of substitute goods or services;
loss of use, data, or profits; or business interruption) however caused and on
any theory of liability, whether in contract, strict liability, or tort
(including negligence or otherwise) arising in any way out of the use of this
software, even if advised of the possibility of such damage.
EOF

BEGIN
{
    my $os;
    my $addPath;
    $os = `uname -s`;
    chomp $os;
    $os = lc $os;
    
    if (not defined($ENV{"__GETROOT__"}))
    {
        $! = -2;
        print STDERR "Environment variable __GETROOT__ must be set!\n";
        die "RUNTIME error encountered";
    }

    $addPath = `$ENV{"__GETROOT__"} / libDir`;
    chomp $addPath;
    unshift(@INC, $addPath);

    $addPath = `$ENV{"__GETROOT__"} / perlDir`;
    chomp $addPath;
    unshift(@INC, $addPath . "/lib/" . $os . "/5.002");
    unshift(@INC, $addPath . "/lib");
    unshift(@INC, $addPath . "/lib/site_perl/" . $os);
    unshift(@INC, $addPath . "/lib/site_perl");
}

use strict;		# require variable declarations, strict refs, etc
use Getopt::Long 2.2;	# long-style command-line parser
use File::Basename;	# # for parsing filenames

use Dirsync;		# for .cf file
use MsvLog;		# Missive-style logging

=head1 NAME

dirsync_agent - do initial processing of directory updates 

=head1 SYNOPSIS

B<dirsync_agent> 
(B<-debug>)
(B<-help>)
B<-channel> I<channel>
(B<-cf> I<filename>)
B<-input> I<filename*>
B<-output> I<filename> 
[ B<-import> | B<-export> ]
(B<-log> (I<filename>))

=head1 OPTIONS

=over 5

=item B<-debug>

Print out debugging information. 

=item B<-help>

Displays a brief help message.

=item B<-input> I<filename*>

I<Required>. The name of the input file. This filename is expanded, so that
if you enter /x/y/z, it will search for /x/y/z* and use the first such file
it finds.

=item B<-output> I<filename>

I<Required>. The name of the MDEF-format output file.

=item B<-channel> I<channel>

I<Required>. The name of the channel to import/export for.

=item B<-cf> I<filename>

The configuration file. By default, /usr/lpp/missive/etc/dirsync.cf.

=item B<-import> | B<-export>

I<Required>. Use B<-import> to read raw files (with channel-specific header
and wrapper information) and convert them into MDEF format. Use export to do 
the reverse (again with channel-specific formatting conventions).

=item B<-log> (I<filename>)

Name of the log file to write to. If no flag given, no logging is performed.
If the flag is given, but no argument is specified, the default log location
is used.

=head1 DESCRIPTION

This program converts a directory update file from its proprietary format to
a canonical format (MDEF, blank-line separated stanzas of "a: b"), but does not
process the attributes or values (a's or b's).

=cut

# appease the strict and -ww
my $debug;		# dump stuff to stderr?
my $result;		# general-purpose short-term-use result variable
my %dir;		# global variable to store the directory 
my %olddir;		# global variable to store the directory 
my $tmpinput;		# temp file for watching a mailbox
my $oper = "add";	# current operation
my $cf;			# ref to Dirsync config file object
my $mailrecip;		# who to send mail to
my $log;		# ref to MsvLog object
my $input_file;		# file to read from
my $dsmTRANS = -3;	# Transient error, restartable
my $dsmCONFIG = -2;	# Configuration error, not restartable
my $dsmNODATA = 2;	# Couldn't find raw file
local ($::a, $::b);	# for byccmail, which is weird...

undef $::opt_debug;
undef $::opt_help;
undef $::opt_import;
undef $::opt_export;

$result = GetOptions("debug", "help", "input=s", "cf=s",
	"channel=s", "output=s", "import", "export", "log:s");

$! = $dsmCONFIG;
die "Usage: $0 (-debug) (-help) -channel <channel> (-cf <filename>) \n" .
	"\t-input <filename*> -output <filename> \n" . 
	"\t[ -import | -export ] (-log <filename>)\n" 
	if ($::opt_help or (!$result) or (!$::opt_channel) or
	(!$::opt_input) or (!$::opt_output) or 
	(!$::opt_import and !$::opt_export));

if (defined($::opt_debug)) {
  $debug = 1;
} else {
  # redirect STDOUT and STDERR
  open (STDOUT, ">/dev/null") || die ("Unable to redirect STDOUT!");
  open (STDERR, ">/dev/null") || die ("Unable to redirect STDERR!");
}
# define some base pathnames 
my $p_bin = `$ENV{"__GETROOT__"} / binDir`;
chomp($p_bin);
$p_bin .= "/";

my $p_etc = `$ENV{"__GETROOT__"} / cfgDir`;
chomp($p_etc);
$p_etc .= "/";

my $p_ds = `$ENV{"__GETROOT__"} / dirsyncDir`;
chomp($p_ds);
$p_ds .= "/";

my $p_dsc = $p_ds . lc($::opt_channel) . "/";

my $p_log = `$ENV{"__GETROOT__"} / logDir`;
chomp($p_log);
$p_log .= "/dirsync/";

# default log directory may not exist, so create it if not there
mkdir($p_log, 0770) if ( ! -d $p_log );

# open the log file, keepopen
if (defined($::opt_log))
{
    my ($basename) = File::Basename::basename($0);

    # generate a log file name if none provided
    if ($::opt_log eq "")
    {
	$::opt_log = $p_log . MsvLog->logname(lc($::opt_channel));
    }
    my $logpath = &abspath($p_dsc, $::opt_log);
    $log = new MsvLog($logpath, "$basename\[$$\]", 1);
    $! = $dsmCONFIG;
    die $$log . "\n" if (ref($log) eq "SCALAR");

    $result = $log->report(MsvLog::NLS("i"), $::opt_import ? 
			   MsvLog::NLS("Starting import") : 
			   MsvLog::NLS("Starting export"));
    $log->abort($dsmTRANS, $$result) if (ref($result) eq "SCALAR"); # just check once...
}
else
{
    $log = stub MsvLog;
}

# open config file
$::opt_cf = "dirsync.cf" if not defined($::opt_cf);
$cf = Dirsync->new(&abspath($p_etc, $::opt_cf));
if (ref($cf) eq "SCALAR")
{
    $log->abort($dsmCONFIG, $$cf);
}

if ($::opt_input)
{
    if ($::opt_import)
    {
	$input_file = &process_inputfiles(&abspath($p_dsc, $::opt_input),
	    $cf->config($::opt_channel, "channel_type"));
    }
    else
    {
	$input_file = &abspath($p_dsc, $::opt_input);
    }

    if (not defined($input_file))
    {
	$log->report(MsvLog::NLS("?"), 
		     MsvLog::NLS("No input files match: %1\$s%2\$s"), 
		     $p_dsc, $::opt_input);
	exit $dsmNODATA;
    }

    open(INPUT, $input_file) or 
	$log->abort($dsmCONFIG, 
		    MsvLog::NLS("Can't open input file: (%1\$s)"),
		    $input_file);
}
else
{
    $log->abort($dsmCONFIG, MsvLog::NLS("Need -input!"));
}

if ($::opt_output)
{
    open(OUTPUT, ">" . &abspath($p_dsc, $::opt_output)) || 
	$log->abort($dsmTRANS, MsvLog::NLS("Can't open output file: (%1\$s)"),
		    &abspath($p_dsc, $::opt_output));
}
else
{
    $log->abort($dsmCONFIG, MsvLog::NLS("Need -output!"));
}


# this is the main branch - either import or export, then decide on the MD
#  type...

if ($::opt_import)
{
    if ($cf->config($::opt_channel, "channel_type") eq "ccmail")
    {
	print STDERR "parsing ccmail input file..." if $debug;
	&parse_ccmail;

	#print OUTPUT "OPERATION=REPLACE\n\n";
    }
    elsif ($cf->config($::opt_channel, "channel_type") eq "notes")
    {
	print STDERR "parsing notes input file..." if $debug;
	&parse_notes;

	#print OUTPUT "OPERATION=REPLACE\n\n";
    }
    elsif ($cf->config($::opt_channel, "channel_type") eq "msmail")
    {
	open(OLDPUT, ">" . &abspath($p_dsc, "oldput")) || 
	    $log->abort($dsmTRANS, 
			MsvLog::NLS("Can't open oldput file: (%1\$s)"),
			&abspath($p_dsc, "oldput"));

	print STDERR "parsing msmail input file..." if $debug;
	&parse_msmail;
	#print OUTPUT "OPERATION=REPLACE\n\n";
	&write_oldput;
    }
    elsif ($cf->config($::opt_channel, "channel_type") eq "profs")
    {
	print STDERR "parsing profs input file..." if $debug;
	&parse_profs;

	#print OUTPUT "OPERATION=REPLACE\n\n";
    }
    else
    {
	$log->abort($dsmCONFIG, MsvLog::NLS("Invalid -channel: %1\$s"), 
	    	    $cf->config($::opt_channel, "channel_type"));
    }

    print STDERR "writing canonical output file...\n" if $debug;
    close INPUT;

    &write_canonical;

    # delete the input file
    # This is NOT a normal step action, but only we are sufficiently smart to
    #  know what to delete (since we had to do some relatively complex 
    #  processing to figure out what file to use).
    #unlink $input_file;

    # Write $input_file to file "$p_dsc/.rm" to allow DSM to clean up after
    # dirsync_agent
    if (open(RAWRM, ">$p_dsc/.rm"))
    {
	print RAWRM $input_file;
	close RAWRM;
    }
}
else	# $::opt_export
{
    if ($cf->config($::opt_channel, "channel_type") eq "ccmail")
    {
	print STDERR "parsing canonical input file...\n" if $debug;
	&parse_canonical("Name");

	print STDERR "writing ccmail output file...\n" if $debug;
	&write_ccmail;
    }
    elsif ($cf->config($::opt_channel, "channel_type") eq "notes")
    {
	print STDERR "parsing canonical input file...\n" if $debug;
	&parse_canonical("MailDomain", 1);

	print STDERR "writing notes output file...\n" if $debug;
	&write_notes;
    }
    elsif ($cf->config($::opt_channel, "channel_type") eq "msmail")
    {
	print STDERR "parsing canonical input file...\n" if $debug;
	&parse_canonical("MSaddress");
	open(OLDPUT, &abspath($p_dsc, "oldput")) or 
	    $log->abort($dsmCONFIG, 
			MsvLog::NLS("Can't open oldput file: (%1\$s)"),
			$input_file);
	print STDERR "parsing oldput file...\n" if $debug;
	&parse_oldput("MSaddress");

	print STDERR "writing notes output file...\n" if $debug;
	&write_msmail;
    }
    elsif ($cf->config($::opt_channel, "channel_type") eq "profs")
    {
	print STDERR "parsing canonical input file...\n" if $debug;
	&parse_canonical("USERID");

	print STDERR "writing profs output file...\n" if $debug;
	&write_profs;
    }
    else
    {
	$log->abort($dsmCONFIG, MsvLog::NLS("Invalid -channel"), 
	    $cf->config($::opt_channel, "channel_type"));
    }
}

$log->report(MsvLog::NLS("i"), MsvLog::NLS("Exiting normally"));

# Function: write_canonical
# Arguments: none
# Returns: nothing
# Description: Takes the %dir array and prints it out in the normal export
#			   form.
sub write_canonical
{
    my $name;
    my $field;
    my $value;
    my $count = 0;
    my $oper = "";

    foreach $name (keys %dir)
    {
	next if ($name eq "bogus");

	print STDERR "==$name==\n" if $debug;

	if ($dir{$name}{"operation"} ne $oper)
	{
	    $oper = $dir{$name}{"operation"};
	    print OUTPUT "operation=$oper\n\n";
	    print STDERR "operation=$oper\n\n" if $debug;
	}

	foreach $field (keys %{$dir{$name}})
	{
	    next if $field eq "operation";

	    $value = $dir{$name}{$field};

	    print OUTPUT "$field=$value\n";
	    print STDERR "$field=$value\n" if $debug;
	}
	print OUTPUT "\n";
	print STDERR "\n" if $debug;

	$count++;
    }

    $log->report(MsvLog::NLS("i"), MsvLog::NLS("Wrote %1\$s canonical entries"), $count);
}

# Function: write_oldput
# Arguments: none
# Returns: nothing
# Description: Takes the %dir array and prints it out in the normal export
#			   form.
sub write_oldput
{
    my $name;
    my $field;
    my $value;
    my $count = 0;
    my $oper = "";

    foreach $name (keys %olddir)
    {
	next if ($name eq "bogus");

	print STDERR "==$name==\n" if $debug;

	if ($olddir{$name}{"operation"} ne $oper)
	{
	    $oper = $olddir{$name}{"operation"};
	    print OLDPUT "operation=$oper\n\n";
	    print STDERR "operation=$oper\n\n" if $debug;
	}

	foreach $field (keys %{$olddir{$name}})
	{
	    next if $field eq "operation";

	    $value = $olddir{$name}{$field};

	    print OLDPUT "$field=$value\n";
	    print STDERR "$field=$value\n" if $debug;
	}
	print OLDPUT "\n";
	print STDERR "\n" if $debug;

	$count++;
    }

    $log->report(MsvLog::NLS("i"), MsvLog::NLS("Wrote %1\$s oldput entries"), $count);
    close OLDPUT;
}

# Function: write_ccmail
# Arguments: none
# Returns: none
# Description: Outputs the %dir array in cc:Mail format. Adds cc:Mail client-
#              specific wrapper information.
sub write_ccmail
{
    my $count = 0;
    my $seq;
    my $op;

    $seq = &update_seq;

    # write the header info for the DSM which REQUIRES \n\n prior to the
    # message body
    print OUTPUT "Subject: DAssist Update: " . 
                  uc($::opt_channel) . 
		  " CHANGES $seq\n\n";
    print STDERR "Subject: DAssist Update: " .
		 uc($::opt_channel) .
		 " CHANGES $seq\n\n" if $debug;

    # check for op=replace
    $op = lc $dir{(keys %dir)[0]}{"operation"};
    if ($op eq "replace")	# it's a replace
    {
	# write the content header
	print OUTPUT "\@Begin UPDATE : " . 
		     uc($::opt_channel) . 
		     " FULL $seq\n";

	print STDERR "\@Begin UPDATE : " . 
		     uc($::opt_channel) . 
		     " FULL $seq\n" if $debug;

	print OUTPUT "\@Begin FULL\n";
	print STDERR "\@Begin FULL\n" if $debug;
	$count += &write_ccmail_type();
	print OUTPUT "\@End FULL\n";
	print STDERR "\@End FULL\n" if $debug;

	# now write the footers
	print OUTPUT "\@End UPDATE\n";
	print STDERR "\@End UPDATE\n" if $debug;
    }
    else
    {
	# write the content header
	print OUTPUT "\@Begin UPDATE : " . 
	             uc($::opt_channel) . 
		     " CHANGES $seq\n";
	print STDERR "\@Begin UPDATE : " .
		     uc($::opt_channel) .
	             " CHANGES $seq\n" if $debug;

	# sort the records into delete/add/modify

	print OUTPUT "\@Begin DELETE\n";
	print STDERR "\@Begin DELETE\n" if $debug;
	$count += &write_ccmail_type("delete");
	print OUTPUT "\@End DELETE\n";
	print STDERR "\@End DELETE\n" if $debug;

	print OUTPUT "\@Begin ADD\n";
	print STDERR "\@Begin ADD\n" if $debug;
	$count += &write_ccmail_type("add");
	print OUTPUT "\@End ADD\n";
	print STDERR "\@End ADD\n" if $debug;

	print OUTPUT "\@Begin MODIFY\n";
	print STDERR "\@Begin MODIFY\n" if $debug;
	$count += &write_ccmail_type("change", "modify");
	print OUTPUT "\@End MODIFY\n";
	print STDERR "\@End MODIFY\n" if $debug;

	# now write the footers
	print OUTPUT "\@End UPDATE\n";
	print STDERR "\@End UPDATE\n" if $debug;
    }

    $log->report(MsvLog::NLS("i"), MsvLog::NLS("Wrote %1\$s cc:Mail entries"), $count);
}

# Function: write_ccmail_type
# Arguments: a list of types to output
# Returns: the number of records written
# Description: Yanked from write_ccmail to allow filtering of output based on
#              operation type. 
sub write_ccmail_type
{
    my (@types) = @_;
    my $name;
    my $field;
    my $value;
    my $op;
    my $count = 0;

    foreach $name (keys %dir)
    {
	next if ($name eq "bogus");

	$op = $dir{$name}{"operation"};
	next unless grep(/$op/i, @types);

	print STDERR "==$name==\n" if $debug;

	# make delete work
	if ($op eq "delete")
	{
	    $dir{$name}{"Locn"} = "D";
	}

	foreach $field (sort byccmail keys %{$dir{$name}})
	{
	    next if $field eq "operation";

	    $value = $dir{$name}{$field};

	    print OUTPUT "$field: $value\n";
	    print STDERR "$field: $value\n" if $debug;
	}

	$count++;
    }

    $count;
}

# Function: byccmail
# Description: A sorting comparison routine for sorting cc:Mail records:
#	       Name, Locn, Addr, Cmts
sub byccmail
{
    my %ccmailorder = (Name => 1, Locn => 2, Addr => 3, Cmts => 4);

    return -1 if not defined $ccmailorder{$::a};
    return 1 if not defined $ccmailorder{$::b};

    $ccmailorder{$::a} - $ccmailorder{$::b};
}

# Function: parse_ccmail
# Arguments: none
# Returns: none
# Description: Loads up a cc:Mail file and stores it into %dir. Note that
#              the @-lines are completely ignored, since cc:Mail actually
#              exports only full updates. If it ever started exporting changes,
#              this would have to increase in complexity to allow for the
#              three sections (@BEGIN DELETE, ADD, CHANGES).
sub parse_ccmail
{
    my $word;	# LHS for parsing
    my $field;	# RHS for parsing
    my $curname = "bogus";	# name to store entries in
    my $count = 0;

    # skip initial header lines
    while (<INPUT>)
    {
	last if $_ eq "\n";
    }

    while (<INPUT>)
    {
	print STDERR $_ if $debug;

	next if (substr($_, 0, 1) eq "@");
	next if (length $_ < 2);

	chomp;

	($word, $field) = /^(\w+?)\s*:\s*(.*)$/;

	$curname = "bogus", next unless defined($word);

	if ($word eq "Name")
	{
	    my $bogfield;

	    $curname = $field;
	    $dir{$curname}{"Name"} = $field;

	    # also, change all bogus entries to us, in case the Name field
	    #  isn't first

	    foreach $bogfield (keys %{$dir{"bogus"}})
	    {
		$dir{$curname}{$bogfield} = $dir{"bogus"}{$bogfield};
		delete $dir{"bogus"}{$bogfield};
	    }

	    # and add an operation line (always the same)
	    $dir{$curname}{"operation"} = "replace";

	    print STDERR join(", ", keys %dir), "\n" if $debug;
	}
	else
	{
		$dir{$curname}{$word} = $field;
	}
    }

    # delete all bogus entries (should be none)
    delete $dir{"bogus"};

    $log->report(MsvLog::NLS("i"), MsvLog::NLS("Read %1\$s cc:Mail entries"), scalar(keys %dir)) ;
}
# Function: parse_canonical
# Arguments: keyfield - name of field to use as attr index
#            randomize - if set, randomize the key field (ie, it's not unique)
# Returns: none
# Description: Loads up a canonical file and stores it into %dir.
sub parse_canonical
{
    my ($keyfield, $randomize) = @_;
    my $word;	# LHS for parsing
    my $field;	# RHS for parsing
    my $curname = "bogus";	# name to store entries in

    while (<INPUT>)
    {
	print STDERR $_ if $debug;
	chomp;

	# if we're changing the operation, store the new oper and reset the
	#  current line so as not to create a bogus record
	if (/operation\s*=\s*(\w+)/i)
	{
	    $oper = lc $1;  # lc - perl5ish lower-caser

	    next;
	}

	($word, $field) = /^(.+?)\s*=\s*(.*)$/;

	$curname = "bogus", next unless defined($word);

	if ($word eq $keyfield)
	{
	    my $bogfield;

	    $curname = $field;
	    if (defined($randomize) and $randomize)
	    {
		do
		{
		    $curname .= int(rand(9));
		}
		while (defined $dir{$curname}{$keyfield});
	    }
	    $dir{$curname}{$keyfield} = $field;

	    # also set the operation, eh?
	    $dir{$curname}{"operation"} = $oper;

	    # also, change all bogus entries to us, in case the Name field
	    #  isn't first

	    foreach $bogfield (keys %{$dir{"bogus"}})
	    {
		print STDERR "renaming bogus $bogfield to $curname\n" 
		    if $debug;
		$dir{$curname}{$bogfield} = $dir{"bogus"}{$bogfield};
		delete $dir{"bogus"}{$bogfield};
	    }

	    print STDERR join(", ", keys %dir), "\n" if $debug;
	}
	else
	{
	    $dir{$curname}{$word} = $field;
	}
    }

    # delete all bogus entries (should be none)
    delete $dir{"bogus"};

    $log->report(MsvLog::NLS("i"), MsvLog::NLS("Read %1\$s canonical entries"), scalar(keys %dir));
}

# Function: search_oldput
# Arguments: Fullname and MSaddress of the current entry
# Returns: oldname and oldaddress
# Description: Returns the old entries corresponding to modify.
sub search_oldput
{
    my ($givenname, $givenaddress) = @_;
    my $name;
    my $oldname;
    my $oldaddress;

    $oldname = "bogus";
    $oldaddress = "bogus";
    foreach $name (keys %olddir)
    {
	if($olddir{$name}{"MSaddress"} eq $givenaddress)
	{
	    $oldname = $olddir{$name}{"Fullname"};
	    $oldaddress = $olddir{$name}{"MSaddress"};
	}

	if ($olddir{$name}{"Fullname"} eq $givenname)
	{
	    $oldname = $olddir{$name}{"Fullname"};
	    $oldaddress = $olddir{$name}{"MSaddress"};
	}
	last if($givenaddress eq $olddir{$name}{"MSaddress"});
	last if($name eq $olddir{$name}{"Fullname"});
    }
    
#return the values
($oldname, $oldaddress);
}

# Function:parse_oldput
# Arguments: keyfield - name of field to use as attr index
#            randomize - if set, randomize the key field (ie, it's not unique)
# Returns: none
# Description: Loads up a canonical file and stores it into %dir.
sub parse_oldput
{
    my ($keyfield, $randomize) = @_;
    my $word;	# LHS for parsing
    my $field;	# RHS for parsing
    my $curname = "bogus";	# name to store entries in

    while (<OLDPUT>)
    {
	print STDERR $_ if $debug;
	chomp;

	# if we're changing the operation, store the new oper and reset the
	#  current line so as not to create a bogus record
	if (/operation\s*=\s*(\w+)/i)
	{
	    $oper = lc $1;  # lc - perl5ish lower-caser

	    next;
	}

	($word, $field) = /^(.+?)\s*=\s*(.*)$/;

	$curname = "bogus", next unless defined($word);

	if ($word eq $keyfield)
	{
	    my $bogfield;

	    $curname = $field;
	    if (defined($randomize) and $randomize)
	    {
		do
		{
		    $curname .= int(rand(9));
		}
		while (defined $olddir{$curname}{$keyfield});
	    }
	    $olddir{$curname}{$keyfield} = $field;

	    # also set the operation, eh?
	    $olddir{$curname}{"operation"} = $oper;

	    # also, change all bogus entries to us, in case the Name field
	    #  isn't first

	    foreach $bogfield (keys %{$olddir{"bogus"}})
	    {
		print STDERR "renaming bogus $bogfield to $curname\n" 
		    if $debug;
		$olddir{$curname}{$bogfield} = $olddir{"bogus"}{$bogfield};
		delete $olddir{"bogus"}{$bogfield};
	    }

	    print STDERR join(", ", keys %olddir), "\n" if $debug;
	}
	else
	{
	    $olddir{$curname}{$word} = $field;
	}
    }

    # delete all bogus entries (should be none)
    delete $olddir{"bogus"};

    $log->report(MsvLog::NLS("i"), MsvLog::NLS("Read %1\$s oldput entries"), scalar(keys %olddir));
}

# Function: write_notes
# Arguments: none
# Returns: nothing
# Description: Writes a message in the format expected by the Notes client.
#              For now, that's MDEF.
sub write_notes
{
    # write the header info for the DSM which REQUIRES \n\n prior to the
    # message body
    print OUTPUT "Subject: MD<--CD\n\n";
    print STDERR "Subject: MD<--CD\n\n" if $debug;

    &write_canonical;
}

# Function: parse_notes
# Arguments: none
# Returns: nothing
# Description: Reads a message imported from Notes. For now, that's MDEF.
#              Deletes the leading message header info.
sub parse_notes
{
    while (<INPUT>)
    {
	last if $_ eq "\n";
    }

    &parse_canonical("MailDomain", 1); # not unique!
}

# Function: write_msmail
# Arguments: none
# Returns: nothing
# Description: Writes a message in the format expected by the MSMail client.
sub write_msmail
{
    my $op;
    my $name;
    my $field;
    my $msline;
    my $omsline;
    my $Fullname;
    my $MSaddress;
    my $oldname;
    my $oldaddress;
    # write the header info for the DSM which REQUIRES \n\n prior to the
    # message body
    print OUTPUT "Subject: MD<--CD\n\n";
    print STDERR "Subject: MD<--CD\n\n" if $debug;

    print OUTPUT "BEGIN\n";
    foreach $name (keys %dir)
    {
	next if ($name eq "bogus");

	$op = lc $dir{$name}{"operation"};
	#next if (!defined($dir{$name}{"Fullname"}));
	$Fullname = $dir{$name}{"Fullname"};
	#if($name ne $Fullname)
	#{
	    #print "$name is $Fullname\n";
	#}
	$MSaddress = $dir{$name}{"MSaddress"};
	if ($op eq "add")
	{
	    $msline = sprintf "A %-31s%s\n",$Fullname,$MSaddress;
	    print OUTPUT "$msline";
	    if (not ((substr($MSaddress, 0, 5) eq "smtp:") || 
		(substr($MSaddress, 0, 5) eq "SMTP:")))
            {
	    	foreach $field  (keys %{$dir{$name}})
	    	{
    		    next if ($field eq "Fullname");
		    next if ($field eq "MSaddress");
		    next if ($field eq "operation");
		    print OUTPUT "- $field:/$dir{$name}{$field}\n";
	    	}
	    }
	    
	}
	elsif ($op eq "delete")
	{
	    $msline = sprintf "D %-31s%s\n",$Fullname,$MSaddress;
	    print OUTPUT $msline
	}
	elsif (($op eq "modify") || ($op eq "change"))
	{
	    ($oldname, $oldaddress) = &search_oldput($Fullname, $MSaddress);
	    if($oldname ne "bogus")
	    {
		$msline = sprintf "M %-31s%s\n",$oldname,$oldaddress;
		print OUTPUT "$msline";
		$msline = sprintf "  %-31s%s\n",$Fullname,$MSaddress;
		print OUTPUT "$msline";
	    	if (not ((substr($MSaddress, 0, 5) eq "smtp:") || 
		   (substr($MSaddress, 0, 5) eq "SMTP:")))
            	{
	    
		    foreach $field  (keys %{$dir{$name}})
		    {
			next if ($field eq "Fullname");
		    	next if ($field eq "MSaddress");
		    	next if ($field eq "operation");
		    	print OUTPUT "- $field:/$dir{$name}{$field}\n";
		    }
		}
	    }
	    else
	    {
		$msline = sprintf "A %-31s%s\n",$Fullname,$MSaddress;
		print OUTPUT "$msline";
		
	    	if (not ((substr($MSaddress, 0, 5) eq "smtp:") || 
		   (substr($MSaddress, 0, 5) eq "SMTP:")))
            	{
		   foreach $field  (keys %{$dir{$name}})
		   {
			next if ($field eq "Fullname");
			next if ($field eq "MSaddress");
			next if ($field eq "operation");
			print OUTPUT "- $field:/$dir{$name}{$field}\n";
		   }
		}
	    }
	}
	else
	{
	    $log->report(MsvLog::NLS("?"), MsvLog::NLS("Ignoring %1\$s"), $name);
	}
    }

    print OUTPUT "END\n"
}

# Function: parse_msmail
# Arguments: none
# Returns: none
# Description: Loads up a MSMail file and stores it into %dir. 
sub parse_msmail
{
    my $word;	# LHS for parsing
    my $field;	# RHS for parsing
    my $curname = "bogus";	# name to store entries in
    my $key = "bogus";		# key for entries MUST be address
    my $count = 0;

    # skip initial header lines
    while (<INPUT>)
    {
	last if (substr($_,0,5) eq "BEGIN");
    }
    
    while (<INPUT>)
    {
	print STDERR $_ if $debug;

	next if (substr($_, 0, 1) eq "@");
	next if (length $_ < 2);
	next if (length $_ < 3);
	last if (substr($_,0,3) eq "END");
	if(!defined($key))
	{
	    $key = "bogus";
	}
	$/ = "\r\n";
	chomp;
	$/ = "\n";
	if(substr($_,0,1) eq "A")
	{
	    if (length($_) >= 33)
	    {
		$key = substr($_,33);
		chomp($key);
	    	$dir{$key}{"MSaddress"}=$key;
	    	# and add an operation line (always the same)
	    	$dir{$key}{"operation"} = "replace";
	    }

	    $curname = substr($_,2,30);
	    $curname =~ s/^\s+//;
	    $curname =~ s/\s+$//;
	    $dir{$key}{"Fullname"} = $curname;

	    if ($key ne "bogus")
	    {
	    	$olddir{$key}{"Fullname"} = $curname;
	    	$olddir{$key}{"MSaddress"} = substr($_,33);
	    	chomp($olddir{$key}{"MSaddress"});
	    	$olddir{$key}{"operation"} = "replace";
	    }
	}
	
	if(substr($_,0,1) eq "-")
	{
	    ($word, $field) = /^- (.+)\s*:\/\s*(.*)$/;
	    next if(!defined($word));
	    if(!defined($field))
	    {
		$field = " ";
	    }
	    $dir{$key}{$word} = $field;
	}

    }

    # delete all bogus entries (should be none)
    delete $dir{"bogus"};

    $log->report(MsvLog::NLS("i"), 
		 MsvLog::NLS("Read %1\$s MSMail entries"), scalar(keys %dir));
}


# Function: write_profs
# Arguments: none
# Returns: nothing
# Description: Writes a message in the format expected by the Notes client.
#              For now, that's MDEF.
sub write_profs
{
    my $name;
    my $field;
    my $value;
    my $pair;
    my $char;
    my @arraypair;
    my $x;
    my $count = 0;
    my $oper = "";

    # write the header info for the DSM which REQUIRES \n\n prior to the
    # message body
    print OUTPUT "Subject: MD<--CD\n\n";
    print STDERR "Subject: MD<--CD\n\n" if $debug;

    # write entries
    foreach $name (keys %dir)
    {
	next if ($name eq "bogus");

	print STDERR "==$name==\n" if $debug;

	if ($dir{$name}{"operation"} ne $oper)
	{
	    $oper = $dir{$name}{"operation"};
	    print OUTPUT "operation=$oper\n\n";
	    print STDERR "operation=$oper\n\n" if $debug;
	}

	foreach $field (keys %{$dir{$name}})
	{
	    next if $field eq "operation";

	    $value = $dir{$name}{$field};

	    # profs has maximum 79 char per line
	    if ( ( length($field) + length($value) ) <= 78 )  {
		print OUTPUT "$field=$value\n";
		print STDERR "$field=$value\n" if $debug;
	    }
	    else  {
		$pair = $field."=".$value."\n";
	        @arraypair = split(//, $pair);

		# trancate long line to 79 char line
		for ($x=0; $x<79; $x++)  {
		    # pirnt one char at a time
		    $char = shift(@arraypair);
		    print OUTPUT $char;
		    print STDERR $char if $debug;
		}
		print OUTPUT "\n";
		print STDERR "\n" if $debug;
	    }
	}
	# print the record delimiter
	print OUTPUT "\n";
	print STDERR "\n" if $debug;

	$count++;
    }

    $log->report(MsvLog::NLS("i"),
	       MsvLog::NLS("Wrote %1\$s canonical entries"),
		 $count);
}

# Function: parse_profs
# Arguments: none
# Returns: none
# Description: Loads up a Profs file and stores it into %dir. Note that
#	       only full update import is supported.
sub parse_profs
{
    my $word;	# LHS for parsing
    my $field;	# RHS for parsing
    my $curname = "bogus";	# name to store entries in
    my $count = 0;

    # skip initial header lines.
    while (<INPUT>)
    {
	last if $_ eq "\n";
    }

    while (<INPUT>)
    {
	print STDERR $_ if $debug;

	$curname = "bogus", next if (length $_ < 2);

	chomp;
	($word, $field) = /^(\w+?)\s*=\s*(.*)$/;

	$curname = "bogus", next unless defined($word);

	if ($word eq "USERID")
	{
	    my $bogfield;

	    $curname = $field;
	    $dir{$curname}{"USERID"} = $field;

	    # also, change all bogus entries to us, in case the USERID field
	    #  isn't first

	    foreach $bogfield (keys %{$dir{"bogus"}})
	    {
		$dir{$curname}{$bogfield} = $dir{"bogus"}{$bogfield};
		delete $dir{"bogus"}{$bogfield};
	    }

	    # and add an operation line (always the same)
	    $dir{$curname}{"operation"} = "replace";

	    print STDERR join(", ", keys %dir), "\n" if $debug;
	}
	else
	{
		$dir{$curname}{$word} = $field;
	}
    }

    # delete all bogus entries (should be none)
    delete $dir{"bogus"};

    $log->report(MsvLog::NLS("i"),
	       MsvLog::NLS("Read %1\$s Profs entries"),
		 scalar(keys %dir));
}

# Function: update_seq
# Arguments: none
# Returns: old seq, or 0 if none found
# Description: Reads the sequence number off the disk and writes the new one.
#              Also returns the old version.
sub update_seq
{
    my $seq = 0;

    # get/update the sequence number
    if (open(SEQ, "$p_dsc/.seq"))
    {
	chomp($seq = <SEQ>);
	close SEQ;
    }

    # and write it out
    if (open(SEQ, ">$p_dsc/.seq"))
    {
	print SEQ (int($seq) + 1), "\n";
	close SEQ;
    }

    $seq;
}

# Function: myglob
# Arguments: fullname - a directory plus base filename
# Returns: a list of filenames or undef
# Description: From an argument of the form /dir/base, generates all
#              files in /dir with the form base\d+.
sub myglob
{
    my ($fullname) = @_;
    my ($dirname, $basename);
    my $filename;
    my @results;

    # chop up the input string
    $dirname = File::Basename::dirname($fullname);
    $basename = File::Basename::basename($fullname);

    # open the directory
    opendir(GLOBDIR, $dirname) or 
	$log->abort($dsmCONFIG, 
		    MsvLog::NLS("Can't open directory: (%1\$s)"),
		    $dirname);
    
    # loop til we find one or run out of files
    for (;;)
    {
	$filename = readdir(GLOBDIR);
	last if not defined($filename);

	if ($filename =~ /^$basename\d+$/)
	{
	    push(@results, "$dirname/$filename");
	}
    }

    closedir(GLOBDIR);
	
    @results;
}

# Function: process_inputfiles
# Arguments: filespec - base file to look for
#            type - channel type
# Returns: name of the file to use
# Description: Finds the best file to use for the dirsync. Deletes files that
#              will never be useful, and ignores non-ideal files. This is done
#              by sorting into two lists (full, changes), then deleting all 
#              but the last full, then all changes older than that full. Then,
#              the full is used, unless there is none, in which case the 
#              oldest changes fils is used.
sub process_inputfiles
{
    my ($filespec, $chantype) = @_;
    my @files;
    my $file;
    my $rec;
    my ($date, $type, $seq);
    my @full;
    my @changes;
    my $filename;

    @files = myglob($filespec);
    print STDERR "filespec = $filespec, files = @files\n" if $debug;

    # loop through each file, check for type of file, and sort into two
    #  lists
    foreach $file (@files)
    {
	$rec = {};	# generate an anonymous hash

	#  get date, type, sequence number
	if ($chantype eq "ccmail")
	{
	    ($date, $type, $seq) = &analyze_ccmail($file);
	}
	elsif ($chantype eq "notes")
	{
	    ($date, $type, $seq) = &analyze_notes($file);
	}
	elsif ($chantype eq "msmail")
	{
	    ($date, $type, $seq) = &analyze_msmail($file);
	}
	elsif ($chantype eq "profs")
	{
	    ($date, $type, $seq) = &analyze_profs($file);
	}
	else
	{
	    $log->report(MsvLog::NLS("?"), 
		MsvLog::NLS("Channel type \"%1\$s\" not supported by process_inputfiles"), 
		$chantype);
	    return undef;
	}

	# fill in this anonymous hash
	$rec->{"date"} = $date;
	$rec->{"seq"} = $seq;
	$rec->{"name"} = $file;

	#  sort by type into two lists (full, changes)
	if ($type eq "full")
	{
	    push(@full, $rec);
	}
	else
	{
	    push(@changes, $rec);
	}
    }

    # sort lists by date (latest/youngest first, ie, reversed)
    @full = sort {$b->{"date"} <=> $a->{"date"}} @full;
    @changes = sort {$b->{"date"} <=> $a->{"date"}} @changes;

    # if exists 1+ full
    if ($#full >= 0)
    {
	#  get date/seq of most recent full
	#  delete all older files (in either list)
	print STDERR "fulls:\n" if $debug;
	@full = &deleteolderthan($full[0]{"date"}, @full);
	print STDERR "changes:\n" if $debug;
	@changes = &deleteolderthan($full[0]{"date"}, @changes);
    }

    # if exists full
    if ($#full >= 0)
    {
	#  choose it
	$filename = $full[0]->{"name"};
    }
    else
    {
	#  choose first changes (merge??? probably not)
	$filename = $changes[0]->{"name"};
    }

    # return that best file's name
    $filename;
}

# Function: analyze_ccmail
# Arguments: file - filename
# Returns: a list ($date, $type, $seq) about the file
# Description: Gets info about the file for use in sorting and prioritizing.
sub analyze_ccmail
{
    my ($file) = @_;	# input
    my ($date, $type, $seq); # return values
    my ($gotdate, $gotsubj) = (0, 0);	# flags

    # get the date using a file test (in case there's no Time: line)
    $date = (stat $file)[9];

    # open the file
    open(INFILE, "<$file") or $log->abort($dsmCONFIG, 
					MsvLog::NLS("Can't open file: (%1\$s)"),
					  $file);

    # get the type and sequence from the subject line
    LINE:
    while (<INFILE>)
    {
	# this is always the subject for a cc:Mail update
	if (/^Subject: DAssist Update: \w+ (\w+) (\d+)/)
	{
	    # got it - store the data and quit the loop
	    $type = lc $1;
	    $seq = $2;

	    $gotsubj++;
	}

	# date here, time there, but they mean the same
	if (/^Time: (\d+)/)
	{
	    $date = $1;

	    $gotdate++;
	}

	last LINE if ($gotdate && $gotsubj);
    }

    close INFILE;

    # return a list of values
    ($date, $type, $seq);
}

# Function: analyze_notes
# Arguments: file - filename
# Returns: a list ($date, $type, $seq) about the file
# Description: Gets info about the file for use in sorting and prioritizing.
sub analyze_notes
{
    my ($file) = @_;	# input
    my ($date, $type, $seq); # return values
    my ($gotop, $gotdate) = (0, 0);	# flags

    # get the date using a file test (default if no Time: line)
    $date = (stat $file)[9];

    # no sequence available
    $seq = 0;

    # open the file
    open(INFILE, "<$file") or $log->abort($dsmCONFIG, 
					 MsvLog::NLS("Can't open file: (%1\$s)"),
					 $file);

    # get the type from the first operation line
    LINE:
    while (<INFILE>)
    {
	if (/^operation=(\w+)/i)
	{
	    # got it - store the data and quit the loop
	    if (lc $1 eq "replace")
	    {
		$type = "full";
	    }
	    else
	    {
		$type = "changes";
	    }

	    $gotop++;
	}

	# date here, time there, but they mean the same
	if (/^Time: (\d+)/)
	{
	    $date = $1;

	    $gotdate++;
	}

	last LINE if ($gotop && $gotdate);
    }

    close INFILE;

    # return a list of values
    ($date, $type, $seq);
}

# Function: analyze_msmail
# Arguments: file - filename
# Returns: a list ($date, $type, $seq) about the file
# Description: Gets info about the file for use in sorting and prioritizing.
sub analyze_msmail
{
    my ($file) = @_;	# input
    my ($date, $type, $seq); # return values
    my ($gotop, $gotdate) = (0, 0);	# flags

    # get the date using a file test (default if no Time: line)
    $date = (stat $file)[9];

    # no sequence available
    $seq = 0;

    # open the file
    open(INFILE, "<$file") or $log->abort($dsmCONFIG, 
					 MsvLog::NLS("Can't open file: (%1\$s)"),
					 $file);

    # get the type from the first operation line
    $type = "full";
    $gotop++;
  LINE:
    while (<INFILE>)
    {
	# trick, to avoid conversion here.
	if (/^Time: (\d+)/)
	{
	    $date = $1;
	    $gotdate++;
	}

	last LINE if ($gotop && $gotdate);
    }

    close INFILE;
    # return a list of values
    ($date, $type, $seq);
}

# Function: analyze_profs
# Arguments: file - filename
# Returns: a list ($date, $type, $seq) about the file
# Description: Gets info about the file for use in sorting and prioritizing.
sub analyze_profs
{
    my ($file) = @_;	# input
    my ($date, $type, $seq); # return values
    my ($gotdate, $gotsubj, $gotop) = (0, 0, 0);	# flags
    
    # get the date using a file test (in case there's no Time: line)
    $date = (stat $file)[9];

    # no sequence available
    $seq = 0;

    # open the file
    open(INFILE, "<$file") or $log->abort($dsmCONFIG, "Can't open file: (%1\$s)",$file);

    # get the type from the operation line, doing full update for now
    $type = "full";
    $gotop++;

  LINE:
    while (<INFILE>)
    {
	# date here, time there, but they mean the same
	if (/^Time: (\d+)/)
	{
	    $date = $1;

	    $gotdate++;
	}

	last LINE if ($gotdate && $gotsubj);
    }

    close INFILE;

    # return a list of values
    ($date, $type, $seq);
}

# Function: deleteolderthan
# Arguments: date - a date/time in seconds past the epoch
#            files - a list of files to potentially delete
# Returns: a list of all remaining files from @files
# Description: Deletes all files in @list with a modification time later then
#              $date.
sub deleteolderthan
{
    my ($date, @files) = @_;	# inputs
    my $file;		# index through @files
    my @remaining;	# temporary list of undeleted files
    my $fdate;		# modification date of $file
    my $name;		# name of $file (for simplicity)
    my $timeline;	# header line with Time:

    foreach $file (@files)
    {
	$name = $file->{"name"};

	$fdate = (stat $name)[9];	# that's "mtime"

	# try the Time: line also
	open(INFILE, "<$name") or 
			$log->abort($dsmCONFIG, 
			            MsvLog::NLS("Can't open file: (%1\$s)"),
				    $name);
	$timeline = (grep(/^Time:/, <INFILE>))[0];
	close INFILE;
	if (defined($timeline) and $timeline =~ /^Time: (\d+)/)
	{
	    $fdate = $1;
	}
	# else, go with the stat version

	if ($fdate < $date)	# if $file was modified earlier, it's older
	{
	    print STDERR "deleting $name\n" if $debug;
	    unlink $name;
	}
	else
	{
	    print STDERR "saving $name\n" if $debug;
	    push @remaining, $file;
	}
    }

    @remaining;
}
