#! /usr/bin/perl 
#
# dupload - utility to upload Debian packages
#
# Copyright (C) 1996, 1997 Heiko Schlittermann
# Copyright (C) 1999 Stephane Bortzmeyer
# Licensed under the GNU GPL v2.
#
# see dupload(1) for help.

#BEGIN { 
#	$ENV{PERL_INC} # for my tests only
#		and unshift @INC, $ENV{PERL_INC};
#	unshift @INC, ""; 
#}

use strict;
use 5.003; # Because of the prototypes
use Cwd;
use Getopt::Long;
use File::Basename;
use Net::FTP;
use English;

# more or less configurable constants
my $version = "2.6";
my $progname = basename($0);
my $user = getlogin() || $ENV{LOGNAME} || $ENV{USER};
my $myhost = `hostname --fqdn`; chomp $myhost;
my $cwd = cwd();

my $debug = 0;	# for somewhat more verbose output from the ftp module
my $force = 0;	# do it, even when already done
my $keep = 0;   # keep going, even if checksum errors
my $quiet = 0;	# don't talk too much
my $configfile = 0; # By default, we do NOT read ./dupload.conf, for
		    # security resons

my $host = undef;				# target host
my $method = "ftp";				# transfer method
my $login = "anonymous";		# default login
my $passwd = "$user\@$myhost";	# ...
my $options = "";	# extra options for rsync or scp

my $sendmail = "/usr/sbin/sendmail";

# global Variables
my (@changes,	# the files we'll have to read from
    @skipped,	# the packages we skipped
    @all_the_files,     # ... we installed (for postupload processing)
    @all_the_debs,      # ... we installed (for postupload processing)
    %all_packages,      # All Debian binary packages we installed 
                        # (for postupload processing)
    $copiedfiles,
    $dry,		# if do-nothing
    $mailonly,
    $fqdn, 		# per host
    $server,
    $dinstall_runs,
    $nonus,
    $passive,
    $nomail, $archive, $noarchive,
    %preupload, %postupload,
    $result,
    $incoming, $queuedir,   # ...
    $mailto, $mailtx, $cc,  # ...
    $visiblename, $visibleuser,
    $fullname,
    %files, %package, %version, %arch,	# per job
    %dir, %changes, %log, %announce,    # ...
    %extra,
    $suspicious_but_proceed,
);

### Prototypes
sub configure(@);	# reads the config file(s)
sub ftp_open($$$);	# establishs the ftp connection
sub info($);		# print the available info (for a given host)
sub fatal(@);		# bail out
sub getpass();		# read password
sub w(@);		# warn (to STDERR if quiet, to STDOUT else)
sub p(@);		# print (suppress if quiet, to STDOUT else)
sub announce_if_necessary($);
sub run ($$);	   # Runs an external program and return its exit status

# some tests on constants
$user or fatal("Who am I? (can't get user identity)\n");
$myhost or fatal("Who am I? (can't get hostname)\n");
$cwd or fatal("Where am I? (can't get current directory)\n");

unless (-x $sendmail) {
  $nomail = 1;
  w "mail options disabled, can't run `$sendmail': $!\n";
}

### Main
configure(
	"/etc/dupload.conf",
	$ENV{HOME} && "$ENV{HOME}/.dupload.conf");

$Getopt::Long::ignorecase = 0;
GetOptions qw(
	debug:i 
	help
	force keep configfile no nomail noarchive
	mailonly
	to=s print 
       quiet Version version
) or fatal("Bad Options\n");

$configfile = $::opt_configfile || $configfile;
configure("./dupload.conf") if $configfile;

$dry = defined($::opt_no);
$mailonly = defined($::opt_mailonly);
if ($mailonly) {
    $dry = 1;
}
$debug = $::opt_debug || $debug;
$keep = $::opt_keep || $keep;
$host = $::opt_to || $config::default_host;
$force = $::opt_force || $force;
$nomail = $::opt_nomail || 0;
$quiet = $::opt_quiet;

# only info or version?
info($host), exit 0 if $::opt_print;
p("$progname version: $version\n"), exit 0 if 
    ($::opt_Version or $::opt_version);

if ($::opt_help) {
    p ("Usage: $progname --to HOST FILE.changes ...\n" .
       "\tUploads the files listed in the above '.changes' to the\n".
       "\thost HOST.\n" .
       "\tSee dupload(1) for details.\n");
    exit 0;
}

# get the configuration for that host
# global, job independent information

$host or fatal("Need host to upload to.  (See --to option or the default_host configuration variable)\n");

{
  my $nick = $config::cfg{$host};
  $method = $nick->{method} || $method;
  $options = $nick->{options} || $options;
  $fqdn = $nick->{fqdn} or fatal("Nothing known about host $host\n");
  $incoming = $nick->{incoming} or fatal("No Incoming dir\n");
  $queuedir = $nick->{queuedir};
  $mailto = $nick->{mailto};
  $mailtx = $nick->{mailtx} || $mailto;
  $cc = $nick->{cc};
  $dinstall_runs = $nick->{dinstall_runs};
  $nonus = $nick->{nonus};
  $passive = $nick->{passive}; 
  if ($passive and ($method ne "ftp")) { 
      fatal ("Passive mode is only for FTP ($host)");
  }
  if (defined ($nick->{archive})) {
      $archive = $nick->{archive};
  } 
  else {
      $archive = 1;
  }
  foreach my $category (qw/changes sourcepackage package file deb/) {
      if (defined ($nick->{preupload}{$category})) {
	  $preupload{$category} = $nick->{preupload}{$category};
      }
      else {
	  $preupload{$category} = $config::preupload{$category};
      }
      if (defined ($nick->{postupload}{$category})) {
      	  $postupload{$category} = $nick->{postupload}{$category};
      }
      else {
      	  $postupload{$category} = $config::postupload{$category};
      }
  }
  
  $login = $nick->{login} || $login if $method eq "ftp";
  $login = $nick->{login} || $user if ($method eq "scp" || $method eq "scpb" || $method eq "rsync");
  $visibleuser = $nick->{visibleuser} || $user; chomp($visibleuser);
  $visiblename = $nick->{visiblename} || ''; chomp($visiblename);
  $fullname = $nick->{fullname} || '';
  # Do not accept passwords in configuration file,
  # except for anonymous logins.
  undef $passwd unless $login =~ /^anonymous|ftp$/;
  if ($nick->{password} && ($login =~ /^anonymous|ftp$/)) { 
      $passwd = $nick->{password};
  }
}

# Command-line options have precedence over configuration files:

($mailto || $mailtx) or p "dupload note: no announcement will be sent.\n";

$noarchive = $::opt_noarchive || (! $archive);

# get the changes file names
@ARGV or push @ARGV, ".";	# use currend dir if no args
foreach (@ARGV) {
	my @f = undef;
	-r $_ or fatal("Can't read $_: $!\n");
	-f _ and do {
		/\.changes$/ or w("no .changes extension: $_\n");
		unshift(@changes, $_); 
		next;
	};
	-d _ and do {
		@f = <$_/*.changes> or w("no changes file in dir $_\n"); 
		unshift @changes, @f;
		next;
	};
}

@changes or die("No changes file, so nothing to do.\n");

# preupload code for changes files
foreach my $change (@changes) {
	if ($preupload{'changes'}) {
		my ($result) = run $preupload{'changes'}, [$change];
		if (! $result) {
			fatal "Pre-upload \'$preupload{'changes'}\' failed for $change\n  ";
		}
	}
}

p("Uploading ($method) to $fqdn:$incoming");
p("and moving to $fqdn:$queuedir") if $queuedir;
p("\n");

select((select(STDOUT), $| = 1)[0]);

# parse the changes files and update some 
# hashs, indexed by the jobname: 
#  %job - the files to be uploaded
#  %log - the logfile name
#  %dir - where the files are located
#  %announce -

PACKAGE: foreach my $change (@changes) {
	my $dir = dirname($change);
	my $cf = basename($change);
	my $job = basename($cf, ".changes");
	my ($package, $version, $arch) = (split("_", $job, 3));
	my ($upstream, $debian) = (split("-", $version, 2));
	my $log = "$job.upload";

	my %md5;
	my (@files, @done, @extra);
	my (%mailto, %fields);

	chdir $dir or fatal("Can't chdir to $dir: $!\n");

	$dir{$job} = $dir;
	$changes{$job} = $cf;
	$package{$job} = $package;
	$version{$job} = $version;

	# preupload code for source package
	if ($preupload{'sourcepackage'}) {
	    my ($result) = run $preupload{'sourcepackage'}, 
			       [basename($package) . " $version"];
	    if (! $result) {
		fatal "Pre-upload \'$preupload{'sourcepackage'}\' " .
		    "failed for " . basename($package) . " $version\n  ";
	    }
	}

	p "[ job $job from $cf";

	# scan the log file (iff any) for 
	# the files we've already put to the host
	# and the announcements already done
	if (-f $log) {
		open(L, "<$log") or fatal("Can't read $log: $!\n");
		while (<L>) {
			chomp;
			if (/^. /) { 
				/^u .*\s(${host}|${fqdn})\s/ and push(@done, $_),  next;
				/^a / and push(@done, $_), next;
			} else {
				/\s(${host}|${fqdn})\s/ and push @done, "u $_";
			}
			next;
		}
		close(L);
	}

	# if the dinstall_runs variable is set, we don't want the
	# announcement emails, because dinstall will attend to that.
	if ($dinstall_runs) {
		$nomail = 1;
	}

	# scan the changes file for architecture,
	# distribution code and the files
	# avoid duplicate mail addressees
	open(C, "<$cf") or fatal("Can't read $cf: $!\n");
	my ($field);
	while (<C>) { 
		chomp;
		/^changes:\s*/i and do {
		    $fields{changes}++;
		    $field = undef;
		    next;
		};
		/^architecture:\s+/i and do {
		    chomp($arch{$job} = "$'");
		    $field = undef;
		    next;
		};
		/^distribution:\s+/i and do { $_ = " $'";
			/\Wstable/i and $mailto{$mailto}++;
			/\Wunstable/i and $mailto{$mailtx}++;
			/\Wexperimental/i and $mailto{$mailtx}++;
			/\WUNRELEASED/ and fatal "distribution: UNRELEASED";
			$field = undef;
			next;
		};
		/^(files|checksums-(?:sha1|sha256)):\s*$/i and do {
		    $field = lc $1;
		    push @{$fields{$field}}, $' if $';
		    next;
		};
		/^\s+/ and $field and do {
		    push @{$fields{$field}}, $' if $';
		    next;
		};
		/^[\w.-]+:/ and do {
		    $field = undef;
		};
	}
	foreach (keys %mailto) {
		my $k = $_;  
		unless ($nomail) {
			p "\n  announce ($cf) to $k";
			if (grep(/^a .*\s${k}\s/, @done)) {
				p " already done";
			} else { 
				$announce{$job} = join(" ", $announce{$job}, $_);
				p " will be sent";
			}
		}
	}

	# search for extra announcement files
	foreach ("${package}", 
			"${package}_${upstream}",
			"${package}_${upstream}-${debian}") {
		$_ .= ".announce";
		-r $_ and push @extra, $_;
	}
	if (@extra) {
		p ", as well as\n  ", join(", ", @extra);
		$extra{$job} = [@extra];
	}

	my %checksums;
	foreach my $alg (qw(sha1 sha256)) {
	    foreach (@{$fields{"checksums-$alg"}}) {
		chomp;
		my ($chksum, $size, $file) = split;
		$checksums{$file}{$alg} = $chksum;
		if (exists $checksums{$file}{size}
		    and $checksums{$file}{size} != $size) {
		    fatal "differing sizes for file $file: $size != $checksums{$file}{size}";
		}
		$checksums{$file}{size} = $size;
	    }
	}
	foreach (@{$fields{files}}) {
	    chomp;
	    my ($chksum, $size, undef, undef, $file) = split;
	    $checksums{$file}{md5} = $chksum;
	    if (exists $checksums{$file}{size}
		and $checksums{$file}{size} != $size) {
		fatal "differing sizes for file $file: $size != $checksums{$file}{size}";
	    }
	    $checksums{$file}{size} = $size;
	}
	close(C);
	%checksums && $fields{changes} or p(": not a changes file ]\n"), next PACKAGE;

	# test the md5sums
	foreach my $file (keys %checksums) {
	    p "\n $file";
	    if ($checksums{$file}{size} != -s $file) {
		$keep or fatal("Size mismatch for $file\n");
		w("Size mismatch for $file, skipping $job\n");
		push @skipped, $cf;
		next PACKAGE;
	    }
	    p ", size ok";

	    foreach my $alg (qw(md5 sha1 sha256)) {
		next unless $checksums{$file}{$alg};

		if (-r $file) {
		    $_ = `${alg}sum $file`;
		    $_ = (split)[0];
		} else {
		    print ": $!";
		    $_ = "";
		}

		$checksums{$file}{$alg} eq $_ or do {
		    $keep or fatal(uc($alg)."sum mismatch for $file\n");
		    w(uc($alg)."sum mismatch for $file, skipping $job\n");
		    push @skipped, $cf;
		    next PACKAGE;
		};
		p ", ${alg}sum ok";
	    }
	    if (!$force && @done && grep(/^u \Q${file}\E/, @done)) {
		p ", already done for $host";
	    } else {
		push @files, $file;
	    }
	    next;
	}

	# The changes file itself
	p "\n $cf ok";
	if (!$force && @done && grep(/^u \Q${cf}\E/, @done)) {
		p ", already done for $host";
	} else { push @files, $cf; }

	if (@files) {
		$log{$job} = $log;
		$files{$job} = [ @files ];
       	} else {
	    $log{$job} = $log;
	    announce_if_necessary($job);
	    if (!$dry) {
		open(L, ">>$log{$job}") 
		    or w("can't open logfile $log{$job}: $!\n");
		print L "s $changes{$job} $fqdn " . localtime() . "\n";
		close(L);
	    } else {
		p "\n+ log successful upload\n";
	    }
	}
	p " ]\n";

	# preupload code for all files and for '.deb' 
	foreach my $file (@files) {
		push @all_the_files, $file;
		if ($preupload{'file'}) {
			my ($result) = run $preupload{'file'}, [$file];
			if (! $result) {
				fatal "Pre-upload \'$preupload{'file'}\' " .
				      "failed for $file\n  ";
			}
		}
		if ($file =~ /\.deb$/) {
			# non-US sanity check
			if ((`dpkg -I $file | grep '^ Section:'` =~ /non-US/i) &&
			    ($fqdn !~ /(non-us|security).debian.org/i) && !$nonus) {
				if (!defined($suspicious_but_proceed) &&
                                    $suspicious_but_proceed !~ /^y/i) {
					print "Looks like you're uploading non-US packages to a normal upload queue.\n";
					print "Are you sure you want to proceed? ";
					$suspicious_but_proceed = <STDIN>;
					die "Aborting upload.\n" unless $suspicious_but_proceed =~ /^y/i;
				}
			}
			push @all_the_debs, $file;
			my ($binary_package, $version, $garbage) = split ('_', $file);
			$binary_package = basename($binary_package);
			$all_packages{$binary_package} = $version;
			if ($preupload{'package'}) {
				my ($result) = run $preupload{'package'}, 
                                   [$binary_package, $version];
				if (! $result) {
					fatal "Pre-upload \'$preupload{'dpackage'}\' " .
					      "failed for $binary_package $version\n  ";
				}
			}
			if ($preupload{'deb'}) {
				my ($result) = run $preupload{'deb'}, [$file];
				if (! $result) {
					fatal "Pre-upload \'$preupload{'deb'}\' " .
					      "failed for $file\n  ";
				}
			}
		}
	}

} continue {
	chdir $cwd or fatal("Can't chdir back to $cwd\n");
}

chdir $cwd or fatal("Can't chdir to $cwd: $!\n");

@skipped and w("skipped: @skipped\n");
%files or (p("Nothing to upload\n"), exit(0));

if ($method eq "ftp") {
	if (!$dry) {
		$passwd = getpass() unless defined $passwd;
	} else { 
		p "+ getpass()\n";
	}
	p "Uploading (ftp) to $host ($fqdn)\n";
	if (!$dry) {
		ftp_open($fqdn, $login, $passwd);
		$server->cwd($incoming);
	} else {
		p "+ ftp_open($fqdn, $login, $passwd)\n";
		p "+ ftp::cwd($incoming\n";
	}
} elsif ($method eq "scp" || $method eq "scpb") {
	p "Uploading (scp) to $host ($fqdn)\n";
} elsif ($method eq "rsync") {
	p "Uploading (rsync) to $host ($fqdn)\n";
} else {
	fatal("Unknown upload method\n");
}

JOB: foreach (keys %files) {
	my $job = $_;
	my @files = @{$files{$job}};
	my $mode;
	my $batchmode;
	my $allfiles;
	$copiedfiles = "";

	my ($package, $version, $arch) = (split("_", $job, 3));
	my ($upstream, $debian) = (split("-", $version, 2));

	$incoming =~ s/_package_/$package/g;
	$incoming =~ s/_version_/$version/g;
	$incoming =~ s/_arch_/$arch/g;
	$incoming =~ s/_upstream_/$upstream/g;
	$incoming =~ s/_debian_/$debian/g;

	chdir $cwd or fatal("Can't chdir to $cwd: $!\n");
	chdir $dir{$job} or fatal("Can't chdir to $dir{$job}: $!\n");

	p "[ Uploading job $job";
	@files or p ("\n nothing to do ]"), next;

	my $wrong_mode = 0; # For scpb only. A priori, the mode is right for every file
	foreach (@files) {
		my $file = $_;
		my $size = -s;
		my $t;

		p(sprintf "\n $file %0.1f kB", $size / 1024);
		$t = time();
		if ($method eq "ftp") {
			unless ($dry) {
				unless ($server->put($file, $file)) {
                                        $result = $server->message();
                                        $server->delete($file) ;
					fatal("Can't upload $file: $result");
				}
				$t = time() - $t;
			} else {
				p "\n+ ftp::put($file)";
				$t = 1;
			}
		} elsif ($method eq "scp") {
                        $mode = (stat($file))[2];
			unless ($dry) {
				system("scp -p -q $options $file $login\@$fqdn:$incoming");
				fatal("scp $file failed\n") if $?;
				$t = time() - $t;
                                # Small optimization
                                if ($mode != 33188) { # rw-r--r-- aka 0644
				    system("ssh -x -l $login $fqdn chmod 0644 $incoming/$file");
				    fatal("ssh ... chmod 0644 failed\n") if $?;
                                }
			} else {
				p "\n+ scp -p -q $options $file $login\@$fqdn:$incoming";
                                if ($mode != 33188) { # rw-r--r-- aka 0644
                                     p "\n+ ssh -x -l $login $fqdn chmod 0644 $incoming/$file";
                                }
				$t = 1;
			}
                } elsif ($method eq "scpb") {
                	$copiedfiles .= "$file ";
			$mode = (stat($file))[2];
			# Small optimization
			if ($mode != 33188) { # rw-r--r-- aka 0644
			   $wrong_mode = 1;
			}
			$t = 1;
			$batchmode = 1;
                } elsif ($method eq "rsync") {
			$copiedfiles .= "$file ";
			$mode = (stat($file))[2];
			# Small optimization
			if ($mode != 33188) { # rw-r--r-- aka 0644
			   $wrong_mode = 1;
			}
			$t = 1;
			$batchmode = 1;
		}

		if ($queuedir) {
			p", renaming";
			if ($method eq "ftp") {
				unless ($dry) {
					$server->rename($file, $queuedir . $file) 
						or 
                                                $result=$server->message(),
                                                $server->delete($file),
						fatal("Can't rename $file -> $queuedir$file\n");
				} else {
					p "\n+ ftp::rename($file, $queuedir$file)";
				}
			} elsif ($method eq "scp") {
				unless ($dry) {
					system("ssh -x -l $login $fqdn \"mv $incoming$file $queuedir$file\"");
					fatal("ssh -x -l $login $fqdn: mv failed\n") if $?;
				} else {
					p "\n+ ssh -x -l $login $fqdn \"mv $incoming$file $queuedir$file\"";
				}
			}
		}

		p ", ok";
# the batch methods don't produce the $t statistic, so filter on that
		p (sprintf " (${t} s, %.2f kB/s)", $size / 1024 / ($t || 1)) unless ($batchmode);

		unless ($batchmode) {
			unless ($dry) {
				open(L, ">>$log{$job}") or w "Can't open $log{$job}: $!\n";
				print L "u $file $fqdn " . localtime() . "\n";
				close(L);
			} else {
				p "\n+ log to $log{$job}\n";
			}
		}
	}
# and now the batch mode uploads
	my $needcmd = 0;
	my $cmd = "ssh -x -l $login $fqdn 'cd $incoming;";
	if ($wrong_mode) {
		$cmd .= "chmod 0644 $copiedfiles;";
		$needcmd = 1;
	}
	if (length($queuedir) > 0) {
		$cmd .= "mv $copiedfiles $queuedir;";
		$needcmd = 1;
	}
	$cmd .= "'";
	if ($method eq "scpb") {
		unless ($dry) {
			p "\n";
			system("scp $options $copiedfiles $login\@$fqdn:$incoming");
			if ($?) {
				unlink $log{$job};
				fatal("scp $copiedfiles failed\n");
			}
			if ($needcmd) {
				system($cmd);
			}
			fatal("$cmd failed\n") if $?;
		} else {
			p "\n+ scp $options $copiedfiles $login\@$fqdn:$incoming";
			p "\n+ $cmd";
		}
		$allfiles = $copiedfiles;
        }

	if ($method eq "rsync") {
		unless ($dry) {
			p "\n";
			system("rsync --partial -zave ssh $options -x $copiedfiles $login" . "@" . "$fqdn:$incoming");
			if ($?) {
				unlink $log{$job};
				fatal("rsync $copiedfiles failed\n");
			}
			if ($needcmd) {
				system($cmd);
			}
			fatal("$cmd failed\n") if $?;
		} else {
			p "\n+ rsync --partial -zave ssh $options -x $copiedfiles $login" . "@" . "$fqdn:$incoming";
			p "\n+ $cmd";
		}
		$allfiles = $copiedfiles;
	}
	if ($batchmode) {
		unless ($dry) {
			open(L, ">>$log{$job}") or w "Can't open $log{$job}: $!\n";
			foreach (split(/ /, $allfiles)) {
				print L "u $_ $fqdn " . localtime() . "\n";
			}
			close(L);
		} else {
			p "\n+ log to $log{$job}\n";
		}
		$batchmode = 0;
	}

        announce_if_necessary($job);
        unless ($dry) {
            open(L, ">>$log{$job}") 
                or w("can't open logfile $log{$job}: $!\n");
            print L "s $changes{$job} $fqdn " . localtime() . "\n";
            close(L);
        } else {
            p "\n+ log successful upload\n";
        }
	p " ]\n";

}

if ($method eq "ftp") {
  unless ($dry) {
    $server->close();
  } else {
    p "\n+ ftp::close\n";
  }
}

# postupload code for changes files
unless ($dry) {
    foreach my $change (@changes) {
	if ($postupload{'changes'}) {
	    my ($result) = run $postupload{'changes'}, [$change];
	    if (! $result) {
		fatal "Post-upload \'$postupload{'changes'}\' " . 
		    "failed for $change\n  ";
	    }
	}
	my ($package, $version, $arch) = (split("_", $_, 3));
	if ($postupload{'sourcepackage'}) {
	    my ($result) = run $postupload{'sourcepackage'}, 
	                       [basename($package), $version];
	    if (! $result) {
		fatal "Post-upload \'$postupload{'sourcepackage'}\' " . 
		    "failed for " . basename($package) . " $version\n  ";
	    }
	}
    }
    foreach my $file (@all_the_files) {	
	if ($postupload{'file'}) {
	    my ($result) = run $postupload{'file'}, [$file];
	    if (! $result) {
		fatal "Post-upload \'$postupload{'file'}\' " . 
		    "failed for $file\n  ";
	    }
	}
    }
    foreach my $file (@all_the_debs) {	
	if ($postupload{'deb'}) {
	    my ($result) = run $postupload{'deb'}, [$file];
	    if (! $result) {
		fatal "Post-upload \'$postupload{'deb'}\' " . 
		    "failed for $file\n  ";
	    }
	}
    }
    foreach my $package (keys (%all_packages)) {	
	if ($postupload{'package'}) {
	    my ($result) = run $postupload{'package'}, 
                               [$package, $all_packages{$package}];
	    if (! $result) {
		fatal "Post-upload \'$postupload{'package'}\' " . 
		    "failed for $package $all_packages{$package}\n  ";
	    }
	}
    }
}

@skipped and w("skipped: @skipped\n");

exit 0;

### SUBS

###
sub announce_if_necessary ($) {
    my ($job) = @_[0];
    my ($opt_fullname) = " -F '($fullname)'";
    my ($msg);
    if ($announce{$job} and (! $nomail)) {
	if ($config::no_parentheses_to_fullname) {
	       $opt_fullname = " -F '$fullname'";
	}
	$fullname =~ s/\'/''/;
	my $sendmail_cmd = "|$sendmail -f $visibleuser"
	    . ($visiblename  ? "\@$visiblename" : "") 
		. ($fullname  ? $opt_fullname : "")
		    . " $announce{$job}";
	$msg = "announcing to $announce{$job}";
	if ($cc) {
	    $sendmail_cmd .= " " . $cc;
	    $msg .= " and $cc";
	}
	p $msg . "\n";
	if ((!$dry) or ($mailonly)) {
	    open(M, $sendmail_cmd) or fatal("Can't pipe to $sendmail $!\n");
	} else {
	    p "\n+ announce to $announce{$job} using command ``$sendmail_cmd''\n";
	    open(M, ">&STDOUT");
	}
	
	print M <<xxx;
X-dupload: $version
To: $announce{$job}
xxx
        $cc and print M <<xxx;
Cc: $cc
xxx
        $noarchive and print M <<xxx;
X-No-Archive: yes
xxx
                
	print M <<xxx;
Subject: Uploaded $package{$job} $version{$job} ($arch{$job}) to $host

xxx
        foreach (@{$extra{$job}}) {
	    my $line;
	    open (A, "<$_") 
		or w("Can't open extra announce $_: $!\n"), next;
	    p " ($_";
	    while ($line = <A>) { print M  $line; }
	    close(A);
	    p(" ok)");
	}
	
	open(C, "<$changes{$job}") 
	    or fatal("Can't open $changes{$job} $!\n");
	while (<C>) { print M; }
	close(C);
	
	close(M);
	if ($?) { p ", failed"; }
	else { p ", ok"; }
	
	if (!$dry) {
	    open(L, ">>$log{$job}") 
		or w("can't open logfile $log{$job}: $!\n");
	    print L "a $changes{$job} $announce{$job} " . localtime() . "\n";
	    close(L);
	} else {
	    p "\n+ log announcement\n";
	}
    }
}

### open the connection
sub ftp_open($$$) {
	my ($remote, $user, $pass) = @_;
	my ($ftp_port, $retry_call, $attempts) = (21, 1, 1);
	my ($request_passive) = 0;
	
	if (($user =~ /@/) or ($passive)) {
	    $request_passive = 1;
	    p "+ FTP passive mode selected\n";
	}
	
	# It may seems complicated, but it is to be sure that the
	# environment variable FTP_PASSIVE works (which needs no
        # Passive argument).
	if ($request_passive) {
	    $server = Net::FTP->new ("$fqdn", Passive => $request_passive);
	}
	else {
	    $server = Net::FTP->new ("$fqdn");
	}
	if (! $server) {
	    fatal ($@);
	}
	$server->debug($debug);

	$_ = $server->login($user, $pass)
		or die("Login as $user failed\n");
	$server->type('I')
		or fatal("Can't set binary type\n");
}

### Display what whe know ...
sub info($) {
	my ($host) = @_;

	foreach ($host || keys %config::cfg) {
		my $r = $config::cfg{$_};
		print <<xxx;
nick name     : $_
real name     : $r->{fqdn}
login         : $r->{login}
incoming      : $r->{incoming}
queuedir      : $r->{queuedir}
mail to       : $r->{mailto}
mail to x     : $r->{mailtx}
cc            : $r->{cc}
passive FTP   : $r->{passive}
dinstall runs : $r->{dinstall_runs}
archive mail  : $r->{archive}
non-US        : $r->{nonus}

xxx
	}
}

### Read the configuration
sub configure(@) {
	my @conffiles = @_;
	my @read = ();
	foreach (@conffiles) { 
		-r or next;
		-s or next;
		do $_ or fatal("$@\n");
		push @read, $_;
	}
	@read or fatal("No configuration files\n");
}

### password
sub getpass() {
	system "stty -echo cbreak </dev/tty"; $? and fatal("stty");
	print "\a${login}\@${fqdn}'s ftp account password: ";
	chomp($_ = <STDIN>);
	print "\n";
	system "stty echo -cbreak </dev/tty"; $? and fatal("stty");
	return $_;
};

###
# output
# p() prints to STDOUT if !$quiet
# w()          ....             ,
#     but to STDERR if $quiet
# fatal() dies
											{
my $nl;
sub p(@) { 
        return if $quiet;
	$nl = $_[$#_] =~ /\n$/;
	print STDOUT @_;
}

sub w(@) {
    if ($quiet) { print STDERR "$progname warning: ", @_; }
    else {
	$nl = $_[$#_] =~ /\n$/;
	unshift @_, "$progname warning: "; 
	unshift @_, "\n" if !$nl;
	print STDOUT @_;
    }
}

sub fatal(@) {
    my ($pack,$file,$line);
    ($pack,$file,$line) = caller();
    (my $msg = "$progname fatal error: @_ at $file line $line\n") =~ tr/\0//d;
    die $msg;
}

sub run ($$) {
    my ($command, $args) = @_;
    my (@args) = @{$args};
    my ($result);
    my ($i);
    foreach $i (0..$#args) {
	$args[$i] =~ s#/#\\/#g;
	my ($mycode) = "\$command =~ s/\%" . ($i+1) . "/$args[$i]/g;";
	# Substitute %1 by the first argument, etc
        $result = eval ($mycode);
	if (! defined ($result)) {
	    fatal ("Cannot eval arguments substitution $mycode: $@");
	}
    }
    system "$command";
    $result = $CHILD_ERROR>>8;
    return (! $result);
}

} 


# ex:set ts=4 sw=4:
